@businessmaps/metaontology-nuxt 0.63.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,201 @@
1
+ Apache License
2
+ Version 2.0, January 2004
3
+ http://www.apache.org/licenses/
4
+
5
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6
+
7
+ 1. Definitions.
8
+
9
+ "License" shall mean the terms and conditions for use, reproduction,
10
+ and distribution as defined by Sections 1 through 9 of this document.
11
+
12
+ "Licensor" shall mean the copyright owner or entity authorized by
13
+ the copyright owner that is granting the License.
14
+
15
+ "Legal Entity" shall mean the union of the acting entity and all
16
+ other entities that control, are controlled by, or are under common
17
+ control with that entity. For the purposes of this definition,
18
+ "control" means (i) the power, direct or indirect, to cause the
19
+ direction or management of such entity, whether by contract or
20
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
21
+ outstanding shares, or (iii) beneficial ownership of such entity.
22
+
23
+ "You" (or "Your") shall mean an individual or Legal Entity
24
+ exercising permissions granted by this License.
25
+
26
+ "Source" form shall mean the preferred form for making modifications,
27
+ including but not limited to software source code, documentation
28
+ source, and configuration files.
29
+
30
+ "Object" form shall mean any form resulting from mechanical
31
+ transformation or translation of a Source form, including but
32
+ not limited to compiled object code, generated documentation,
33
+ and conversions to other media types.
34
+
35
+ "Work" shall mean the work of authorship, whether in Source or
36
+ Object form, made available under the License, as indicated by a
37
+ copyright notice that is included in or attached to the work
38
+ (an example is provided in the Appendix below).
39
+
40
+ "Derivative Works" shall mean any work, whether in Source or Object
41
+ form, that is based on (or derived from) the Work and for which the
42
+ editorial revisions, annotations, elaborations, or other modifications
43
+ represent, as a whole, an original work of authorship. For the purposes
44
+ of this License, Derivative Works shall not include works that remain
45
+ separable from, or merely link (or bind by name) to the interfaces of,
46
+ the Work and Derivative Works thereof.
47
+
48
+ "Contribution" shall mean any work of authorship, including
49
+ the original version of the Work and any modifications or additions
50
+ to that Work or Derivative Works thereof, that is intentionally
51
+ submitted to Licensor for inclusion in the Work by the copyright owner
52
+ or by an individual or Legal Entity authorized to submit on behalf of
53
+ the copyright owner. For the purposes of this definition, "submitted"
54
+ means any form of electronic, verbal, or written communication sent
55
+ to the Licensor or its representatives, including but not limited to
56
+ communication on electronic mailing lists, source code control systems,
57
+ and issue tracking systems that are managed by, or on behalf of, the
58
+ Licensor for the purpose of discussing and improving the Work, but
59
+ excluding communication that is conspicuously marked or otherwise
60
+ designated in writing by the copyright owner as "Not a Contribution."
61
+
62
+ "Contributor" shall mean Licensor and any individual or Legal Entity
63
+ on behalf of whom a Contribution has been received by Licensor and
64
+ subsequently incorporated within the Work.
65
+
66
+ 2. Grant of Copyright License. Subject to the terms and conditions of
67
+ this License, each Contributor hereby grants to You a perpetual,
68
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69
+ copyright license to reproduce, prepare Derivative Works of,
70
+ publicly display, publicly perform, sublicense, and distribute the
71
+ Work and such Derivative Works in Source or Object form.
72
+
73
+ 3. Grant of Patent License. Subject to the terms and conditions of
74
+ this License, each Contributor hereby grants to You a perpetual,
75
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76
+ (except as stated in this section) patent license to make, have made,
77
+ use, offer to sell, sell, import, and otherwise transfer the Work,
78
+ where such license applies only to those patent claims licensable
79
+ by such Contributor that are necessarily infringed by their
80
+ Contribution(s) alone or by combination of their Contribution(s)
81
+ with the Work to which such Contribution(s) was submitted. If You
82
+ institute patent litigation against any entity (including a
83
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
84
+ or a Contribution incorporated within the Work constitutes direct
85
+ or contributory patent infringement, then any patent licenses
86
+ granted to You under this License for that Work shall terminate
87
+ as of the date such litigation is filed.
88
+
89
+ 4. Redistribution. You may reproduce and distribute copies of the
90
+ Work or Derivative Works thereof in any medium, with or without
91
+ modifications, and in Source or Object form, provided that You
92
+ meet the following conditions:
93
+
94
+ (a) You must give any other recipients of the Work or
95
+ Derivative Works a copy of this License; and
96
+
97
+ (b) You must cause any modified files to carry prominent notices
98
+ stating that You changed the files; and
99
+
100
+ (c) You must retain, in the Source form of any Derivative Works
101
+ that You distribute, all copyright, patent, trademark, and
102
+ attribution notices from the Source form of the Work,
103
+ excluding those notices that do not pertain to any part of
104
+ the Derivative Works; and
105
+
106
+ (d) If the Work includes a "NOTICE" text file as part of its
107
+ distribution, then any Derivative Works that You distribute must
108
+ include a readable copy of the attribution notices contained
109
+ within such NOTICE file, excluding those notices that do not
110
+ pertain to any part of the Derivative Works, in at least one
111
+ of the following places: within a NOTICE text file distributed
112
+ as part of the Derivative Works; within the Source form or
113
+ documentation, if provided along with the Derivative Works; or,
114
+ within a display generated by the Derivative Works, if and
115
+ wherever such third-party notices normally appear. The contents
116
+ of the NOTICE file are for informational purposes only and
117
+ do not modify the License. You may add Your own attribution
118
+ notices within Derivative Works that You distribute, alongside
119
+ or as an addendum to the NOTICE text from the Work, provided
120
+ that such additional attribution notices cannot be construed
121
+ as modifying the License.
122
+
123
+ You may add Your own copyright statement to Your modifications and
124
+ may provide additional or different license terms and conditions
125
+ for use, reproduction, or distribution of Your modifications, or
126
+ for any such Derivative Works as a whole, provided Your use,
127
+ reproduction, and distribution of the Work otherwise complies with
128
+ the conditions stated in this License.
129
+
130
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
131
+ any Contribution intentionally submitted for inclusion in the Work
132
+ by You to the Licensor shall be under the terms and conditions of
133
+ this License, without any additional terms or conditions.
134
+ Notwithstanding the above, nothing herein shall supersede or modify
135
+ the terms of any separate license agreement you may have executed
136
+ with Licensor regarding such Contributions.
137
+
138
+ 6. Trademarks. This License does not grant permission to use the trade
139
+ names, trademarks, service marks, or product names of the Licensor,
140
+ except as required for reasonable and customary use in describing the
141
+ origin of the Work and reproducing the content of the NOTICE file.
142
+
143
+ 7. Disclaimer of Warranty. Unless required by applicable law or
144
+ agreed to in writing, Licensor provides the Work (and each
145
+ Contributor provides its Contributions) on an "AS IS" BASIS,
146
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147
+ implied, including, without limitation, any warranties or conditions
148
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149
+ PARTICULAR PURPOSE. You are solely responsible for determining the
150
+ appropriateness of using or redistributing the Work and assume any
151
+ risks associated with Your exercise of permissions under this License.
152
+
153
+ 8. Limitation of Liability. In no event and under no legal theory,
154
+ whether in tort (including negligence), contract, or otherwise,
155
+ unless required by applicable law (such as deliberate and grossly
156
+ negligent acts) or agreed to in writing, shall any Contributor be
157
+ liable to You for damages, including any direct, indirect, special,
158
+ incidental, or consequential damages of any character arising as a
159
+ result of this License or out of the use or inability to use the
160
+ Work (including but not limited to damages for loss of goodwill,
161
+ work stoppage, computer failure or malfunction, or any and all
162
+ other commercial damages or losses), even if such Contributor
163
+ has been advised of the possibility of such damages.
164
+
165
+ 9. Accepting Warranty or Additional Liability. While redistributing
166
+ the Work or Derivative Works thereof, You may choose to offer,
167
+ and charge a fee for, acceptance of support, warranty, indemnity,
168
+ or other liability obligations and/or rights consistent with this
169
+ License. However, in accepting such obligations, You may act only
170
+ on Your own behalf and on Your sole responsibility, not on behalf
171
+ of any other Contributor, and only if You agree to indemnify,
172
+ defend, and hold each Contributor harmless for any liability
173
+ incurred by, or claims asserted against, such Contributor by reason
174
+ of your accepting any such warranty or additional liability.
175
+
176
+ END OF TERMS AND CONDITIONS
177
+
178
+ APPENDIX: How to apply the Apache License to your work.
179
+
180
+ To apply the Apache License to your work, attach the following
181
+ boilerplate notice, with the fields enclosed by brackets "[]"
182
+ replaced with your own identifying information. (Don't include
183
+ the brackets!) The text should be enclosed in the appropriate
184
+ comment syntax for the file format. We also recommend that a
185
+ file or class name and description of purpose be included on the
186
+ same "printed page" as the copyright notice for easier
187
+ identification within third-party archives.
188
+
189
+ Copyright 2025 Business Maps
190
+
191
+ Licensed under the Apache License, Version 2.0 (the "License");
192
+ you may not use this file except in compliance with the License.
193
+ You may obtain a copy of the License at
194
+
195
+ http://www.apache.org/licenses/LICENSE-2.0
196
+
197
+ Unless required by applicable law or agreed to in writing, software
198
+ distributed under the License is distributed on an "AS IS" BASIS,
199
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200
+ See the License for the specific language governing permissions and
201
+ limitations under the License.
package/README.md ADDED
@@ -0,0 +1,430 @@
1
+ # @businessmaps/metaontology-nuxt
2
+
3
+ Vue 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.
4
+
5
+ `@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.
6
+
7
+ This 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.
8
+
9
+ ```ts
10
+ import { useModelStore, createTripleIndex } from '@businessmaps/metaontology-nuxt'
11
+
12
+ const store = useModelStore()
13
+
14
+ // Load a map from IndexedDB (checkpoint + replay)
15
+ const model = await store.loadModel('my-map')
16
+
17
+ // Create a reactive triple index
18
+ const triples = createTripleIndex(() => store.root!)
19
+
20
+ // O(1) lookup: what type is this entity?
21
+ triples.entityType('thing-order') // 'Thing'
22
+
23
+ // All entities linked by 'performs'
24
+ triples.byPredicate('performs') // Triple[]
25
+ ```
26
+
27
+ ---
28
+
29
+ ## Getting started
30
+
31
+ ```bash
32
+ npm install @businessmaps/metaontology-nuxt
33
+ ```
34
+
35
+ ### 1. Extend the layer
36
+
37
+ ```ts
38
+ // nuxt.config.ts
39
+ export default defineNuxtConfig({
40
+ extends: ['@businessmaps/metaontology-nuxt'],
41
+ })
42
+ ```
43
+
44
+ Composables in the layer's `composables/` directory are auto-imported by Nuxt. No manual registration needed.
45
+
46
+ ### 2. Wire the store
47
+
48
+ ```ts
49
+ // composables/useMyStore.ts
50
+ import { useModelStore, createTripleIndex } from '@businessmaps/metaontology-nuxt'
51
+ import { applyCommand, computeInverse } from '@businessmaps/metaontology/engine'
52
+ import type { RootContext } from '@businessmaps/metaontology/types/context'
53
+
54
+ export function useMyStore() {
55
+ const modelStore = useModelStore()
56
+ const commitLog = modelStore.commitLog
57
+
58
+ // Reactive triple index over the current model
59
+ const tripleIndex = createTripleIndex(() => modelStore.root!)
60
+
61
+ function dispatch(cmd) {
62
+ const before = modelStore.root!
63
+ const result = applyCommand(before, cmd)
64
+ if (!result.success) return result
65
+
66
+ modelStore.root = result.state
67
+ const inverse = computeInverse(cmd, before, result.state)
68
+ commitLog.appendCommit(cmd, inverse)
69
+
70
+ return result
71
+ }
72
+
73
+ return { root: modelStore.root, tripleIndex, dispatch }
74
+ }
75
+ ```
76
+
77
+ ### 3. Explicit imports (non-Nuxt)
78
+
79
+ For tools and scripts that don't run inside Nuxt, import directly:
80
+
81
+ ```ts
82
+ import { useCommitLog } from '@businessmaps/metaontology-nuxt/composables/useCommitLog'
83
+ import { createTripleIndex } from '@businessmaps/metaontology-nuxt/composables/useTripleStore'
84
+ ```
85
+
86
+ The barrel `index.ts` re-exports everything from `@businessmaps/metaontology` plus all composables, so a single import source covers both packages:
87
+
88
+ ```ts
89
+ import { defineThing, useModelStore, createTripleIndex }
90
+ from '@businessmaps/metaontology-nuxt'
91
+ ```
92
+
93
+ ---
94
+
95
+ ## Composables
96
+
97
+ All 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.
98
+
99
+ ### useCommitLog
100
+
101
+ Append-only commit log with undo/redo, checkpoint-and-replay loading, and unload safety.
102
+
103
+ ```ts
104
+ const commitLog = useCommitLog()
105
+
106
+ // Append a commit (command + its computed inverse)
107
+ commitLog.appendCommit(command, inverse)
108
+
109
+ // Undo: pop the inverse command, dispatch it
110
+ const entry = commitLog.popUndo()
111
+ if (entry) dispatch(entry.inverseCommand)
112
+
113
+ // Redo: pop the original command, re-dispatch it
114
+ const redo = commitLog.popRedo()
115
+ if (redo) dispatch(redo.originalCommand)
116
+
117
+ // Load from IndexedDB: latest checkpoint + replay commits since
118
+ const result = await commitLog.loadFromStorage('my-map', 'main')
119
+ // result: { model, m0, replayFailures }
120
+
121
+ // Initialize a new map with a genesis checkpoint
122
+ await commitLog.initFromSnapshot('new-map', emptyRootContext)
123
+
124
+ // Subscribe to commit appends (for cross-tab broadcast, telemetry, etc.)
125
+ const unbind = commitLog.onAppend((commit) => {
126
+ console.log('committed:', commit.id, commit.command.type)
127
+ })
128
+ ```
129
+
130
+ Key behaviors:
131
+ - Debounced flush to IndexedDB (800ms after last append)
132
+ - Cap-triggered flush when the pending buffer exceeds 25 commits (prevents debounce starvation during rapid AI tool calls)
133
+ - Automatic checkpointing every 100 commits
134
+ - Unload safety via `visibilitychange`, `pagehide`, and `beforeunload` handlers
135
+ - Session undo/redo stacks (max 50 entries, reset on page reload)
136
+ - Schema migration runs on the checkpoint at load time, not inside `applyCommand`
137
+
138
+ ### useSyncEngine
139
+
140
+ Cloud sync state machine with debounced push, pull with fast-forward or three-way merge, and exponential backoff.
141
+
142
+ ```ts
143
+ const sync = useSyncEngine()
144
+
145
+ // Activate with an adapter and a host
146
+ sync.activate(adapter, {
147
+ getRoot: () => store.root,
148
+ applyFastForward: (commits) => { /* replay remote commits */ },
149
+ applyMerged: (mergedModel) => { /* install merged model */ },
150
+ onConflict: (result) => { /* surface conflicts for resolution */ },
151
+ })
152
+
153
+ // Schedule a push after the 5s debounce
154
+ sync.schedulePush()
155
+
156
+ // Manual push/pull
157
+ await sync.push()
158
+ await sync.pull()
159
+
160
+ // Full cycle: pull then push
161
+ await sync.sync()
162
+
163
+ // Read-only reactive state
164
+ sync.status.value // 'idle' | 'pushing' | 'pulling' | 'conflict' | 'error'
165
+ sync.pendingCount.value // number of unsynced commits
166
+ sync.lastError.value // friendly error message or null
167
+ sync.enabled.value // true if activated
168
+
169
+ // Restore persisted sync cursor on page load
170
+ await sync.primeCursor('my-map', 'main')
171
+
172
+ // Deactivate
173
+ sync.deactivate()
174
+ ```
175
+
176
+ Key behaviors:
177
+ - 5-second debounce on push (coalesces rapid edits)
178
+ - Exponential backoff on failure (5s base, 60s cap, max 5 retries)
179
+ - Error classification: `network`, `cors`, `auth`, `conflict`, `server`, `crypto`, `unknown`
180
+ - Console log throttling (one line per error category per retry cycle)
181
+ - Pull handles two cases: fast-forward (no local divergence) or three-way merge (local and remote diverged)
182
+ - Push filter support for cross-tab dedup (set by `useCrossTab`)
183
+ - Sync cursor persisted to IDB so page refresh doesn't re-push already-synced commits
184
+
185
+ ### createTripleIndex
186
+
187
+ Vue `computed()` wrapper around the pure triple projection. Rebuilds reactively when the model changes.
188
+
189
+ ```ts
190
+ import { createTripleIndex } from '@businessmaps/metaontology-nuxt'
191
+
192
+ const index = createTripleIndex(
193
+ () => store.root, // reactive model accessor
194
+ () => store.m0State, // optional M0 state for cross-tier triples
195
+ )
196
+
197
+ // O(1) lookups
198
+ index.bySubject('thing-order') // all triples about this entity
199
+ index.byPredicate('performs') // all 'performs' relationships
200
+ index.bySP('persona-1', 'performs') // what does persona-1 perform?
201
+ index.byPO('performs', 'action-checkout') // who performs the checkout action?
202
+ index.has('p1', 'performs', 'a1') // existence check
203
+
204
+ // Convenience helpers
205
+ index.objectIds('persona-1', 'performs') // string[] of target entity IDs
206
+ index.subjectIds('action-1', 'performs') // string[] of source entity IDs
207
+ index.firstObjectId('p1', 'owns') // string | undefined (1:1 relationships)
208
+ index.entityType('thing-order') // 'Thing' | 'Persona' | ...
209
+ ```
210
+
211
+ This is a factory, not a singleton. Each call creates a new reactive index bound to the provided accessor. Use one per store.
212
+
213
+ ### useModelStore
214
+
215
+ High-level CRUD over maps in IndexedDB. Wraps `useCommitLog` with load, save, list, and delete operations.
216
+
217
+ ```ts
218
+ const store = useModelStore()
219
+
220
+ // Load a map (checkpoint + replay)
221
+ const model = await store.loadModel('my-map', 'main')
222
+
223
+ // Save a new map (genesis checkpoint)
224
+ await store.saveModel(newRootContext)
225
+
226
+ // List all persisted map IDs
227
+ const mapIds = await store.listMaps()
228
+
229
+ // Delete a map and all its commits, checkpoints, and branch heads
230
+ await store.deleteMap('old-map')
231
+
232
+ // Reactive state
233
+ store.root.value // RootContext | null
234
+ store.isLoaded.value // boolean
235
+ store.loading.value // boolean
236
+ store.currentMapId.value // string
237
+ store.error.value // string | null
238
+
239
+ // Access the underlying commit log for advanced operations
240
+ store.commitLog.canUndo.value // boolean
241
+ ```
242
+
243
+ ### useCrossTab
244
+
245
+ BroadcastChannel-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.
246
+
247
+ ```ts
248
+ const crossTab = useCrossTab()
249
+
250
+ // Activate for a map with a host that handles incoming commits
251
+ crossTab.activate('my-map', {
252
+ applyRemoteCommit: (commit) => {
253
+ // Apply the command to local model state.
254
+ // Must NOT call appendCommit (which would re-broadcast).
255
+ },
256
+ })
257
+
258
+ // Per-tab identity (stable for the life of this tab)
259
+ crossTab.getTabId() // string
260
+
261
+ // Check if a commit was received from another tab
262
+ crossTab.wasReceivedFromAnotherTab(commitId) // boolean
263
+
264
+ // Deactivate (closes the BroadcastChannel)
265
+ crossTab.deactivate()
266
+ ```
267
+
268
+ Key behaviors:
269
+ - Per-tab identity via `nanoid()` for echo prevention
270
+ - Automatic push filter installation on `useSyncEngine` so commits received cross-tab are not re-pushed to the cloud by this tab
271
+ - Idempotent receive (duplicate commit IDs are dropped)
272
+ - Graceful degradation when `BroadcastChannel` is unavailable (non-browser environments)
273
+
274
+ ### syncTypes
275
+
276
+ Types and utilities for the sync adapter contract. Not a composable, but auto-imported alongside the composables.
277
+
278
+ ```ts
279
+ import type { SyncAdapter, SyncStatus, SyncErrorCategory,
280
+ PushResult, PullResult, SyncTargetDescriptor } from '@businessmaps/metaontology-nuxt'
281
+ import { classifySyncError, friendlySyncErrorMessage } from '@businessmaps/metaontology-nuxt'
282
+
283
+ // Classify any thrown error
284
+ classifySyncError(new Error('Failed to fetch')) // 'network'
285
+ classifySyncError({ statusCode: 401 }) // 'auth'
286
+ classifySyncError({ statusCode: 409 }) // 'conflict'
287
+
288
+ // User-facing message
289
+ friendlySyncErrorMessage('network') // 'Cloud unreachable - working locally'
290
+ friendlySyncErrorMessage('auth') // 'Sign-in expired'
291
+ ```
292
+
293
+ ---
294
+
295
+ ## IDB schema
296
+
297
+ The 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:
298
+
299
+ **Model-tier (layer-owned, fully typed):**
300
+
301
+ | Store | Key | Indexes | Purpose |
302
+ |---|---|---|---|
303
+ | `commits` | `id` | `by-map-branch-seq` | Append-only command log |
304
+ | `checkpoints` | `id` | `by-map-branch-seq` | Periodic model snapshots |
305
+ | `heads` | `[mapId, branchId]` | - | Branch pointers |
306
+
307
+ **Legacy / canvas-tier (app-owned, typed as `unknown` at the layer):**
308
+
309
+ `documents`, `branches`, `history`, `config`, `tabs`, `conversations`, `blobs`, `sync_queue`
310
+
311
+ The 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`.
312
+
313
+ ---
314
+
315
+ ## Implementing a SyncAdapter
316
+
317
+ The `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.
318
+
319
+ ```ts
320
+ import type { SyncAdapter, PushResult, PullResult } from '@businessmaps/metaontology-nuxt'
321
+ import type { Commit } from '@businessmaps/metaontology/types/commits'
322
+
323
+ class MyCloudAdapter implements SyncAdapter {
324
+ readonly descriptor = {
325
+ kind: 'cloud' as const,
326
+ label: 'My Cloud',
327
+ icon: 'cloud' as const,
328
+ }
329
+
330
+ async push(
331
+ mapId: string,
332
+ branchId: string,
333
+ commits: Commit[],
334
+ baseSequence: number,
335
+ ): Promise<PushResult> {
336
+ // Upload commits to your backend.
337
+ // Return { success: true, newHeadSequence } on success.
338
+ // Return { success: false, conflict: true } on 409.
339
+ const res = await fetch(`/api/sync/${mapId}/${branchId}`, {
340
+ method: 'POST',
341
+ body: JSON.stringify({ commits, baseSequence }),
342
+ })
343
+
344
+ if (res.status === 409) {
345
+ return { success: false, newHeadSequence: baseSequence, conflict: true }
346
+ }
347
+
348
+ const data = await res.json()
349
+ return { success: true, newHeadSequence: data.headSequence }
350
+ }
351
+
352
+ async pull(
353
+ mapId: string,
354
+ branchId: string,
355
+ sinceSequence: number,
356
+ ): Promise<PullResult> {
357
+ // Fetch commits since a given sequence.
358
+ const res = await fetch(
359
+ `/api/sync/${mapId}/${branchId}?since=${sinceSequence}`,
360
+ )
361
+ const data = await res.json()
362
+ return {
363
+ success: true,
364
+ commits: data.commits,
365
+ remoteHead: data.headSequence,
366
+ }
367
+ }
368
+
369
+ async getRemoteHead(mapId: string, branchId: string): Promise<number> {
370
+ const res = await fetch(`/api/sync/${mapId}/${branchId}/head`)
371
+ const data = await res.json()
372
+ return data.headSequence
373
+ }
374
+ }
375
+ ```
376
+
377
+ Pass the adapter to `useSyncEngine().activate(adapter, host)`. The engine handles debouncing, retries, merge, and error classification. The adapter handles transport.
378
+
379
+ ---
380
+
381
+ ## Dependency injection seams
382
+
383
+ Two host interfaces keep the layer from importing app code:
384
+
385
+ **`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.
386
+
387
+ **`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.
388
+
389
+ This 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`.
390
+
391
+ ---
392
+
393
+ ## Directory structure
394
+
395
+ ```
396
+ composables/
397
+ useCommitLog.ts Append-only commit log, undo/redo, checkpoint/replay
398
+ useSyncEngine.ts Cloud sync state machine, merge, retry
399
+ useTripleStore.ts Reactive triple index (Vue computed wrapper)
400
+ useModelStore.ts High-level map CRUD over IndexedDB
401
+ useCrossTab.ts BroadcastChannel cross-tab commit relay
402
+ syncTypes.ts SyncAdapter interface, error classification, result types
403
+ idbConnection.ts Singleton IndexedDB connection (owns the upgrade callback)
404
+ idbSchema.ts Typed DB schema (11 stores, 3 layer-owned + 8 legacy)
405
+ idbHelpers.ts Commit/checkpoint/branch-head CRUD, sync cursor persistence
406
+ index.ts Barrel: re-exports @businessmaps/metaontology + all composables
407
+ nuxt.config.ts Nuxt layer config (auto-imports composables)
408
+ ```
409
+
410
+ ---
411
+
412
+ ## Design decisions
413
+
414
+ **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.
415
+
416
+ **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.
417
+
418
+ **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.
419
+
420
+ **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.
421
+
422
+ **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.
423
+
424
+ ---
425
+
426
+ ## License
427
+
428
+ Apache License 2.0
429
+
430
+ Copyright 2025 Business Maps