{"_id":"@businessmaps/metaontology-nuxt","name":"@businessmaps/metaontology-nuxt","dist-tags":{"latest":"0.63.0"},"versions":{"0.63.0":{"name":"@businessmaps/metaontology-nuxt","version":"0.63.0","description":"Vue 3 and Nuxt 4 integration layer for @businessmaps/metaontology. Reactive composables for commit-sourced persistence, cloud sync, cross-tab coordination, and a reactive triple store.","author":{"name":"Business Maps"},"license":"Apache-2.0","keywords":["metaontology","nuxt","vue","domain-modeling","ddd","triple-store","indexeddb","sync","offline-first"],"repository":{"type":"git","url":"git+https://github.com/Business-Maps/metaontology-nuxt.git"},"homepage":"https://github.com/Business-Maps/metaontology-nuxt#readme","bugs":{"url":"https://github.com/Business-Maps/metaontology-nuxt/issues"},"type":"module","sideEffects":false,"dependencies":{"@businessmaps/metaontology":"*","@vueuse/core":"^14.0.0","idb":"^8.0.0","nanoid":"^5.0.0"},"peerDependencies":{"nuxt":">=4","vue":">=3"},"exports":{".":"./index.ts","./nuxt.config":"./nuxt.config.ts","./composables/*":"./composables/*.ts"},"scripts":{"test":"vitest run","test:watch":"vitest"},"publishConfig":{"access":"public"},"_id":"@businessmaps/metaontology-nuxt@0.63.0","gitHead":"fb5e955dccce3af217f2ccd50b659c2f74a5fa35","_nodeVersion":"22.22.2","_npmVersion":"10.9.7","dist":{"integrity":"sha512-OGtM93620PzzfdVk3DK+7idUe3Ft78x5p5rzCfUCADoGq28FNuU3Upc2IrA2dee/UV/lI4a/U0EvuIoMTJ0hsg==","shasum":"b2b1d48c5577b1be85f8f02ae1f3d0c42aadcd4b","tarball":"https://registry.npmjs.org/@businessmaps/metaontology-nuxt/-/metaontology-nuxt-0.63.0.tgz","fileCount":14,"unpackedSize":107242,"attestations":{"url":"https://registry.npmjs.org/-/npm/v1/attestations/@businessmaps%2fmetaontology-nuxt@0.63.0","provenance":{"predicateType":"https://slsa.dev/provenance/v1"}},"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEUCIDNE9StXad2Z9Y1z0oFccPwZfPJUcRSJyGaeyWqAoUU+AiEAgQPBdXmpyCcS/7G3Jm1Mp9nW5CeZ/6iXP1GmAm+FxYw="}]},"_npmUser":{"name":"shalashtein","email":"shalashtein@gmail.com"},"directories":{},"maintainers":[{"name":"shalashtein","email":"shalashtein@gmail.com"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/metaontology-nuxt_0.63.0_1776166429498_0.7268667556034814"},"_hasShrinkwrap":false}},"time":{"created":"2026-04-14T11:33:49.376Z","0.63.0":"2026-04-14T11:33:49.636Z","modified":"2026-04-14T11:33:50.123Z"},"maintainers":[{"name":"shalashtein","email":"shalashtein@gmail.com"}],"description":"Vue 3 and Nuxt 4 integration layer for @businessmaps/metaontology. Reactive composables for commit-sourced persistence, cloud sync, cross-tab coordination, and a reactive triple store.","homepage":"https://github.com/Business-Maps/metaontology-nuxt#readme","keywords":["metaontology","nuxt","vue","domain-modeling","ddd","triple-store","indexeddb","sync","offline-first"],"repository":{"type":"git","url":"git+https://github.com/Business-Maps/metaontology-nuxt.git"},"author":{"name":"Business Maps"},"bugs":{"url":"https://github.com/Business-Maps/metaontology-nuxt/issues"},"license":"Apache-2.0","readme":"# @businessmaps/metaontology-nuxt\n\nVue 3 / Nuxt 4 integration layer for `@businessmaps/metaontology`. Provides reactive composables for commit-sourced persistence, cloud sync, cross-tab coordination, and a reactive triple store.\n\n`@businessmaps/metaontology` is a pure TypeScript package. It has no opinion about where your state lives, how it's persisted, or how mutations flow through a UI framework. That's deliberate - it keeps the engine testable and portable. But if you're building a browser application with Vue and Nuxt, you need the glue: reactive state, IndexedDB persistence, undo/redo session management, cross-tab coordination, and sync. Writing that glue correctly means handling debounced flushing, unload safety, checkpoint-and-replay loading, three-way merge on pull, echo prevention across tabs, and exponential backoff on network failures.\n\nThis package is that glue. It wraps the pure engine in six composables that are auto-imported by Nuxt's layer convention, so consumers get reactive commit-sourced persistence and sync without writing any of the plumbing themselves. State is singleton per tab (module-level, not per-component), IndexedDB connection ownership lives here (one upgrade callback per database), and sync is transport-agnostic - the package defines the `SyncAdapter` interface, consumers provide implementations.\n\n```ts\nimport { useModelStore, createTripleIndex } from '@businessmaps/metaontology-nuxt'\n\nconst store = useModelStore()\n\n// Load a map from IndexedDB (checkpoint + replay)\nconst model = await store.loadModel('my-map')\n\n// Create a reactive triple index\nconst triples = createTripleIndex(() => store.root!)\n\n// O(1) lookup: what type is this entity?\ntriples.entityType('thing-order')  // 'Thing'\n\n// All entities linked by 'performs'\ntriples.byPredicate('performs')    // Triple[]\n```\n\n---\n\n## Getting started\n\n```bash\nnpm install @businessmaps/metaontology-nuxt\n```\n\n### 1. Extend the layer\n\n```ts\n// nuxt.config.ts\nexport default defineNuxtConfig({\n  extends: ['@businessmaps/metaontology-nuxt'],\n})\n```\n\nComposables in the layer's `composables/` directory are auto-imported by Nuxt. No manual registration needed.\n\n### 2. Wire the store\n\n```ts\n// composables/useMyStore.ts\nimport { useModelStore, createTripleIndex } from '@businessmaps/metaontology-nuxt'\nimport { applyCommand, computeInverse } from '@businessmaps/metaontology/engine'\nimport type { RootContext } from '@businessmaps/metaontology/types/context'\n\nexport function useMyStore() {\n  const modelStore = useModelStore()\n  const commitLog = modelStore.commitLog\n\n  // Reactive triple index over the current model\n  const tripleIndex = createTripleIndex(() => modelStore.root!)\n\n  function dispatch(cmd) {\n    const before = modelStore.root!\n    const result = applyCommand(before, cmd)\n    if (!result.success) return result\n\n    modelStore.root = result.state\n    const inverse = computeInverse(cmd, before, result.state)\n    commitLog.appendCommit(cmd, inverse)\n\n    return result\n  }\n\n  return { root: modelStore.root, tripleIndex, dispatch }\n}\n```\n\n### 3. Explicit imports (non-Nuxt)\n\nFor tools and scripts that don't run inside Nuxt, import directly:\n\n```ts\nimport { useCommitLog } from '@businessmaps/metaontology-nuxt/composables/useCommitLog'\nimport { createTripleIndex } from '@businessmaps/metaontology-nuxt/composables/useTripleStore'\n```\n\nThe barrel `index.ts` re-exports everything from `@businessmaps/metaontology` plus all composables, so a single import source covers both packages:\n\n```ts\nimport { defineThing, useModelStore, createTripleIndex }\n  from '@businessmaps/metaontology-nuxt'\n```\n\n---\n\n## Composables\n\nAll composables are singletons per JavaScript context (per browser tab). Multiple call sites see the same state. Cross-tab isolation comes from the module being loaded separately in each tab, not from per-call instantiation.\n\n### useCommitLog\n\nAppend-only commit log with undo/redo, checkpoint-and-replay loading, and unload safety.\n\n```ts\nconst commitLog = useCommitLog()\n\n// Append a commit (command + its computed inverse)\ncommitLog.appendCommit(command, inverse)\n\n// Undo: pop the inverse command, dispatch it\nconst entry = commitLog.popUndo()\nif (entry) dispatch(entry.inverseCommand)\n\n// Redo: pop the original command, re-dispatch it\nconst redo = commitLog.popRedo()\nif (redo) dispatch(redo.originalCommand)\n\n// Load from IndexedDB: latest checkpoint + replay commits since\nconst result = await commitLog.loadFromStorage('my-map', 'main')\n// result: { model, m0, replayFailures }\n\n// Initialize a new map with a genesis checkpoint\nawait commitLog.initFromSnapshot('new-map', emptyRootContext)\n\n// Subscribe to commit appends (for cross-tab broadcast, telemetry, etc.)\nconst unbind = commitLog.onAppend((commit) => {\n  console.log('committed:', commit.id, commit.command.type)\n})\n```\n\nKey behaviors:\n- Debounced flush to IndexedDB (800ms after last append)\n- Cap-triggered flush when the pending buffer exceeds 25 commits (prevents debounce starvation during rapid AI tool calls)\n- Automatic checkpointing every 100 commits\n- Unload safety via `visibilitychange`, `pagehide`, and `beforeunload` handlers\n- Session undo/redo stacks (max 50 entries, reset on page reload)\n- Schema migration runs on the checkpoint at load time, not inside `applyCommand`\n\n### useSyncEngine\n\nCloud sync state machine with debounced push, pull with fast-forward or three-way merge, and exponential backoff.\n\n```ts\nconst sync = useSyncEngine()\n\n// Activate with an adapter and a host\nsync.activate(adapter, {\n  getRoot: () => store.root,\n  applyFastForward: (commits) => { /* replay remote commits */ },\n  applyMerged: (mergedModel) => { /* install merged model */ },\n  onConflict: (result) => { /* surface conflicts for resolution */ },\n})\n\n// Schedule a push after the 5s debounce\nsync.schedulePush()\n\n// Manual push/pull\nawait sync.push()\nawait sync.pull()\n\n// Full cycle: pull then push\nawait sync.sync()\n\n// Read-only reactive state\nsync.status.value      // 'idle' | 'pushing' | 'pulling' | 'conflict' | 'error'\nsync.pendingCount.value // number of unsynced commits\nsync.lastError.value   // friendly error message or null\nsync.enabled.value     // true if activated\n\n// Restore persisted sync cursor on page load\nawait sync.primeCursor('my-map', 'main')\n\n// Deactivate\nsync.deactivate()\n```\n\nKey behaviors:\n- 5-second debounce on push (coalesces rapid edits)\n- Exponential backoff on failure (5s base, 60s cap, max 5 retries)\n- Error classification: `network`, `cors`, `auth`, `conflict`, `server`, `crypto`, `unknown`\n- Console log throttling (one line per error category per retry cycle)\n- Pull handles two cases: fast-forward (no local divergence) or three-way merge (local and remote diverged)\n- Push filter support for cross-tab dedup (set by `useCrossTab`)\n- Sync cursor persisted to IDB so page refresh doesn't re-push already-synced commits\n\n### createTripleIndex\n\nVue `computed()` wrapper around the pure triple projection. Rebuilds reactively when the model changes.\n\n```ts\nimport { createTripleIndex } from '@businessmaps/metaontology-nuxt'\n\nconst index = createTripleIndex(\n  () => store.root,      // reactive model accessor\n  () => store.m0State,   // optional M0 state for cross-tier triples\n)\n\n// O(1) lookups\nindex.bySubject('thing-order')             // all triples about this entity\nindex.byPredicate('performs')              // all 'performs' relationships\nindex.bySP('persona-1', 'performs')        // what does persona-1 perform?\nindex.byPO('performs', 'action-checkout')  // who performs the checkout action?\nindex.has('p1', 'performs', 'a1')          // existence check\n\n// Convenience helpers\nindex.objectIds('persona-1', 'performs')   // string[] of target entity IDs\nindex.subjectIds('action-1', 'performs')   // string[] of source entity IDs\nindex.firstObjectId('p1', 'owns')          // string | undefined (1:1 relationships)\nindex.entityType('thing-order')            // 'Thing' | 'Persona' | ...\n```\n\nThis is a factory, not a singleton. Each call creates a new reactive index bound to the provided accessor. Use one per store.\n\n### useModelStore\n\nHigh-level CRUD over maps in IndexedDB. Wraps `useCommitLog` with load, save, list, and delete operations.\n\n```ts\nconst store = useModelStore()\n\n// Load a map (checkpoint + replay)\nconst model = await store.loadModel('my-map', 'main')\n\n// Save a new map (genesis checkpoint)\nawait store.saveModel(newRootContext)\n\n// List all persisted map IDs\nconst mapIds = await store.listMaps()\n\n// Delete a map and all its commits, checkpoints, and branch heads\nawait store.deleteMap('old-map')\n\n// Reactive state\nstore.root.value         // RootContext | null\nstore.isLoaded.value     // boolean\nstore.loading.value      // boolean\nstore.currentMapId.value // string\nstore.error.value        // string | null\n\n// Access the underlying commit log for advanced operations\nstore.commitLog.canUndo.value  // boolean\n```\n\n### useCrossTab\n\nBroadcastChannel-based cross-tab commit relay. When two browser tabs have the same map open, edits in one tab appear in the other without a server round-trip.\n\n```ts\nconst crossTab = useCrossTab()\n\n// Activate for a map with a host that handles incoming commits\ncrossTab.activate('my-map', {\n  applyRemoteCommit: (commit) => {\n    // Apply the command to local model state.\n    // Must NOT call appendCommit (which would re-broadcast).\n  },\n})\n\n// Per-tab identity (stable for the life of this tab)\ncrossTab.getTabId()  // string\n\n// Check if a commit was received from another tab\ncrossTab.wasReceivedFromAnotherTab(commitId)  // boolean\n\n// Deactivate (closes the BroadcastChannel)\ncrossTab.deactivate()\n```\n\nKey behaviors:\n- Per-tab identity via `nanoid()` for echo prevention\n- Automatic push filter installation on `useSyncEngine` so commits received cross-tab are not re-pushed to the cloud by this tab\n- Idempotent receive (duplicate commit IDs are dropped)\n- Graceful degradation when `BroadcastChannel` is unavailable (non-browser environments)\n\n### syncTypes\n\nTypes and utilities for the sync adapter contract. Not a composable, but auto-imported alongside the composables.\n\n```ts\nimport type { SyncAdapter, SyncStatus, SyncErrorCategory,\n  PushResult, PullResult, SyncTargetDescriptor } from '@businessmaps/metaontology-nuxt'\nimport { classifySyncError, friendlySyncErrorMessage } from '@businessmaps/metaontology-nuxt'\n\n// Classify any thrown error\nclassifySyncError(new Error('Failed to fetch'))  // 'network'\nclassifySyncError({ statusCode: 401 })           // 'auth'\nclassifySyncError({ statusCode: 409 })           // 'conflict'\n\n// User-facing message\nfriendlySyncErrorMessage('network')  // 'Cloud unreachable - working locally'\nfriendlySyncErrorMessage('auth')     // 'Sign-in expired'\n```\n\n---\n\n## IDB schema\n\nThe layer owns the IndexedDB connection (because IDB only allows one upgrade callback per database version). The schema defines 11 stores in a single `businessmaps` database:\n\n**Model-tier (layer-owned, fully typed):**\n\n| Store | Key | Indexes | Purpose |\n|---|---|---|---|\n| `commits` | `id` | `by-map-branch-seq` | Append-only command log |\n| `checkpoints` | `id` | `by-map-branch-seq` | Periodic model snapshots |\n| `heads` | `[mapId, branchId]` | - | Branch pointers |\n\n**Legacy / canvas-tier (app-owned, typed as `unknown` at the layer):**\n\n`documents`, `branches`, `history`, `config`, `tabs`, `conversations`, `blobs`, `sync_queue`\n\nThe layer creates all stores in the upgrade callback but only reads/writes the model-tier stores. App code accesses the same connection via a thin proxy that re-exports `getDb` and `closeDb`.\n\n---\n\n## Implementing a SyncAdapter\n\nThe `SyncAdapter` interface is transport-agnostic. The sync engine calls `push`, `pull`, and `getRemoteHead` without knowing whether commits travel over HTTP, WebSocket, filesystem, or carrier pigeon.\n\n```ts\nimport type { SyncAdapter, PushResult, PullResult } from '@businessmaps/metaontology-nuxt'\nimport type { Commit } from '@businessmaps/metaontology/types/commits'\n\nclass MyCloudAdapter implements SyncAdapter {\n  readonly descriptor = {\n    kind: 'cloud' as const,\n    label: 'My Cloud',\n    icon: 'cloud' as const,\n  }\n\n  async push(\n    mapId: string,\n    branchId: string,\n    commits: Commit[],\n    baseSequence: number,\n  ): Promise<PushResult> {\n    // Upload commits to your backend.\n    // Return { success: true, newHeadSequence } on success.\n    // Return { success: false, conflict: true } on 409.\n    const res = await fetch(`/api/sync/${mapId}/${branchId}`, {\n      method: 'POST',\n      body: JSON.stringify({ commits, baseSequence }),\n    })\n\n    if (res.status === 409) {\n      return { success: false, newHeadSequence: baseSequence, conflict: true }\n    }\n\n    const data = await res.json()\n    return { success: true, newHeadSequence: data.headSequence }\n  }\n\n  async pull(\n    mapId: string,\n    branchId: string,\n    sinceSequence: number,\n  ): Promise<PullResult> {\n    // Fetch commits since a given sequence.\n    const res = await fetch(\n      `/api/sync/${mapId}/${branchId}?since=${sinceSequence}`,\n    )\n    const data = await res.json()\n    return {\n      success: true,\n      commits: data.commits,\n      remoteHead: data.headSequence,\n    }\n  }\n\n  async getRemoteHead(mapId: string, branchId: string): Promise<number> {\n    const res = await fetch(`/api/sync/${mapId}/${branchId}/head`)\n    const data = await res.json()\n    return data.headSequence\n  }\n}\n```\n\nPass the adapter to `useSyncEngine().activate(adapter, host)`. The engine handles debouncing, retries, merge, and error classification. The adapter handles transport.\n\n---\n\n## Dependency injection seams\n\nTwo host interfaces keep the layer from importing app code:\n\n**`SyncHost`** (used by `useSyncEngine`): the app provides `getRoot()`, `applyFastForward(commits)`, `applyMerged(model)`, and `onConflict(result)`. The engine drives sync state and retries; it delegates side effects to the host.\n\n**`CrossTabHost`** (used by `useCrossTab`): the app provides `applyRemoteCommit(commit)`. When a commit arrives from another tab, the composable calls the host. The host applies the command to its local model state without re-broadcasting or re-pushing.\n\nThis pattern means the layer never imports from `app/` or `layers/bm/`. The app implements the interfaces and passes instances during activation. The boundary is enforced by `eslint-plugin-boundaries`.\n\n---\n\n## Directory structure\n\n```\ncomposables/\n  useCommitLog.ts       Append-only commit log, undo/redo, checkpoint/replay\n  useSyncEngine.ts      Cloud sync state machine, merge, retry\n  useTripleStore.ts     Reactive triple index (Vue computed wrapper)\n  useModelStore.ts      High-level map CRUD over IndexedDB\n  useCrossTab.ts        BroadcastChannel cross-tab commit relay\n  syncTypes.ts          SyncAdapter interface, error classification, result types\n  idbConnection.ts      Singleton IndexedDB connection (owns the upgrade callback)\n  idbSchema.ts          Typed DB schema (11 stores, 3 layer-owned + 8 legacy)\n  idbHelpers.ts         Commit/checkpoint/branch-head CRUD, sync cursor persistence\nindex.ts                Barrel: re-exports @businessmaps/metaontology + all composables\nnuxt.config.ts          Nuxt layer config (auto-imports composables)\n```\n\n---\n\n## Design decisions\n\n**Singleton per tab, not per component.** Composable state lives at module level. `useCommitLog()` returns the same object whether called from a store, a sync engine, or a UI component. Cross-tab isolation is a property of JavaScript module loading (each tab loads its own module instance), not of per-call factoring. This avoids the class of bugs where two consumers see different commit histories.\n\n**Layer owns the IDB connection.** IndexedDB allows one upgrade callback per database version. Splitting the upgrade between the metaontology layer and the app would create a coordination problem (who runs first on a fresh install? who creates the legacy stores?). The layer creates every store the database needs; app code accesses the same connection through a thin proxy.\n\n**Transport-agnostic sync.** The `SyncAdapter` interface is three methods: `push`, `pull`, `getRemoteHead`. Encryption, authentication, presigned URLs, compression - all internal to the adapter. The sync engine handles debouncing, retry, merge, and error classification without knowing how commits move.\n\n**Commit-sourced, not snapshot-sourced.** Loading a map means loading the latest checkpoint and replaying commits since. The commit log is the source of truth; the model is a derived projection. This makes undo (append the inverse), sync (exchange commits), branching (fork the log), and merge (three-way merge of replayed states) all compose from the same data structure.\n\n**Unload safety without Service Worker.** Three browser signals (`visibilitychange` hidden, `pagehide`, `beforeunload`) trigger a fire-and-forget IDB flush. The cap-triggered flush (25 pending commits) limits the worst-case data loss to 24 commits during a crash. Combined, these cover tab close, navigation, mobile backgrounding, and back/forward cache entry.\n\n---\n\n## License\n\nApache License 2.0\n\nCopyright 2025 Business Maps\n","readmeFilename":"README.md","_rev":"1-2a2a9b7594183c7b9f4973192950d8b6"}