@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 +201 -0
- package/README.md +430 -0
- package/composables/idbConnection.ts +81 -0
- package/composables/idbHelpers.ts +165 -0
- package/composables/idbSchema.ts +93 -0
- package/composables/syncTypes.ts +145 -0
- package/composables/useCommitLog.ts +521 -0
- package/composables/useCrossTab.ts +246 -0
- package/composables/useModelStore.ts +174 -0
- package/composables/useSyncEngine.ts +494 -0
- package/composables/useTripleStore.ts +135 -0
- package/index.ts +15 -0
- package/nuxt.config.ts +16 -0
- package/package.json +56 -0
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
|