@ifc-lite/collab 0.2.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 +373 -0
- package/README.md +92 -0
- package/dist/awareness/agent.d.ts +36 -0
- package/dist/awareness/agent.d.ts.map +1 -0
- package/dist/awareness/agent.js +39 -0
- package/dist/awareness/agent.js.map +1 -0
- package/dist/awareness/color.d.ts +31 -0
- package/dist/awareness/color.d.ts.map +1 -0
- package/dist/awareness/color.js +61 -0
- package/dist/awareness/color.js.map +1 -0
- package/dist/awareness/index.d.ts +6 -0
- package/dist/awareness/index.d.ts.map +1 -0
- package/dist/awareness/index.js +9 -0
- package/dist/awareness/index.js.map +1 -0
- package/dist/awareness/overlay.d.ts +59 -0
- package/dist/awareness/overlay.d.ts.map +1 -0
- package/dist/awareness/overlay.js +110 -0
- package/dist/awareness/overlay.js.map +1 -0
- package/dist/awareness/presence.d.ts +95 -0
- package/dist/awareness/presence.d.ts.map +1 -0
- package/dist/awareness/presence.js +114 -0
- package/dist/awareness/presence.js.map +1 -0
- package/dist/awareness/render.d.ts +58 -0
- package/dist/awareness/render.d.ts.map +1 -0
- package/dist/awareness/render.js +66 -0
- package/dist/awareness/render.js.map +1 -0
- package/dist/branch/branch-tree.d.ts +40 -0
- package/dist/branch/branch-tree.d.ts.map +1 -0
- package/dist/branch/branch-tree.js +66 -0
- package/dist/branch/branch-tree.js.map +1 -0
- package/dist/branch/branch.d.ts +44 -0
- package/dist/branch/branch.d.ts.map +1 -0
- package/dist/branch/branch.js +109 -0
- package/dist/branch/branch.js.map +1 -0
- package/dist/branch/history-automerge.d.ts +31 -0
- package/dist/branch/history-automerge.d.ts.map +1 -0
- package/dist/branch/history-automerge.js +237 -0
- package/dist/branch/history-automerge.js.map +1 -0
- package/dist/branch/history.d.ts +147 -0
- package/dist/branch/history.d.ts.map +1 -0
- package/dist/branch/history.js +223 -0
- package/dist/branch/history.js.map +1 -0
- package/dist/branch/index.d.ts +5 -0
- package/dist/branch/index.d.ts.map +1 -0
- package/dist/branch/index.js +8 -0
- package/dist/branch/index.js.map +1 -0
- package/dist/conflicts/detector.d.ts +52 -0
- package/dist/conflicts/detector.d.ts.map +1 -0
- package/dist/conflicts/detector.js +226 -0
- package/dist/conflicts/detector.js.map +1 -0
- package/dist/conflicts/index.d.ts +3 -0
- package/dist/conflicts/index.d.ts.map +1 -0
- package/dist/conflicts/index.js +6 -0
- package/dist/conflicts/index.js.map +1 -0
- package/dist/conflicts/ui-bridge.d.ts +80 -0
- package/dist/conflicts/ui-bridge.d.ts.map +1 -0
- package/dist/conflicts/ui-bridge.js +126 -0
- package/dist/conflicts/ui-bridge.js.map +1 -0
- package/dist/doc/entity.d.ts +84 -0
- package/dist/doc/entity.d.ts.map +1 -0
- package/dist/doc/entity.js +345 -0
- package/dist/doc/entity.js.map +1 -0
- package/dist/doc/geometry.d.ts +39 -0
- package/dist/doc/geometry.d.ts.map +1 -0
- package/dist/doc/geometry.js +99 -0
- package/dist/doc/geometry.js.map +1 -0
- package/dist/doc/index.d.ts +8 -0
- package/dist/doc/index.d.ts.map +1 -0
- package/dist/doc/index.js +11 -0
- package/dist/doc/index.js.map +1 -0
- package/dist/doc/migration-ifc4-to-ifc4x3.d.ts +21 -0
- package/dist/doc/migration-ifc4-to-ifc4x3.d.ts.map +1 -0
- package/dist/doc/migration-ifc4-to-ifc4x3.js +55 -0
- package/dist/doc/migration-ifc4-to-ifc4x3.js.map +1 -0
- package/dist/doc/relationship.d.ts +27 -0
- package/dist/doc/relationship.d.ts.map +1 -0
- package/dist/doc/relationship.js +110 -0
- package/dist/doc/relationship.js.map +1 -0
- package/dist/doc/schema-version.d.ts +47 -0
- package/dist/doc/schema-version.d.ts.map +1 -0
- package/dist/doc/schema-version.js +41 -0
- package/dist/doc/schema-version.js.map +1 -0
- package/dist/doc/schema.d.ts +131 -0
- package/dist/doc/schema.d.ts.map +1 -0
- package/dist/doc/schema.js +117 -0
- package/dist/doc/schema.js.map +1 -0
- package/dist/doc/units.d.ts +45 -0
- package/dist/doc/units.d.ts.map +1 -0
- package/dist/doc/units.js +120 -0
- package/dist/doc/units.js.map +1 -0
- package/dist/federation/bridge.d.ts +34 -0
- package/dist/federation/bridge.d.ts.map +1 -0
- package/dist/federation/bridge.js +52 -0
- package/dist/federation/bridge.js.map +1 -0
- package/dist/federation/index.d.ts +4 -0
- package/dist/federation/index.d.ts.map +1 -0
- package/dist/federation/index.js +7 -0
- package/dist/federation/index.js.map +1 -0
- package/dist/federation/resolver.d.ts +58 -0
- package/dist/federation/resolver.d.ts.map +1 -0
- package/dist/federation/resolver.js +43 -0
- package/dist/federation/resolver.js.map +1 -0
- package/dist/federation/session.d.ts +79 -0
- package/dist/federation/session.d.ts.map +1 -0
- package/dist/federation/session.js +126 -0
- package/dist/federation/session.js.map +1 -0
- package/dist/geometry/blob-store.d.ts +106 -0
- package/dist/geometry/blob-store.d.ts.map +1 -0
- package/dist/geometry/blob-store.js +266 -0
- package/dist/geometry/blob-store.js.map +1 -0
- package/dist/geometry/csg.d.ts +63 -0
- package/dist/geometry/csg.d.ts.map +1 -0
- package/dist/geometry/csg.js +101 -0
- package/dist/geometry/csg.js.map +1 -0
- package/dist/geometry/determinism.d.ts +45 -0
- package/dist/geometry/determinism.d.ts.map +1 -0
- package/dist/geometry/determinism.js +92 -0
- package/dist/geometry/determinism.js.map +1 -0
- package/dist/geometry/gc.d.ts +63 -0
- package/dist/geometry/gc.d.ts.map +1 -0
- package/dist/geometry/gc.js +109 -0
- package/dist/geometry/gc.js.map +1 -0
- package/dist/geometry/index.d.ts +6 -0
- package/dist/geometry/index.d.ts.map +1 -0
- package/dist/geometry/index.js +9 -0
- package/dist/geometry/index.js.map +1 -0
- package/dist/geometry/parametric.d.ts +92 -0
- package/dist/geometry/parametric.d.ts.map +1 -0
- package/dist/geometry/parametric.js +200 -0
- package/dist/geometry/parametric.js.map +1 -0
- package/dist/index.d.ts +27 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +46 -0
- package/dist/index.js.map +1 -0
- package/dist/mutations/bind.d.ts +56 -0
- package/dist/mutations/bind.d.ts.map +1 -0
- package/dist/mutations/bind.js +68 -0
- package/dist/mutations/bind.js.map +1 -0
- package/dist/mutations/index.d.ts +2 -0
- package/dist/mutations/index.d.ts.map +1 -0
- package/dist/mutations/index.js +5 -0
- package/dist/mutations/index.js.map +1 -0
- package/dist/perf/benchmark.d.ts +25 -0
- package/dist/perf/benchmark.d.ts.map +1 -0
- package/dist/perf/benchmark.js +97 -0
- package/dist/perf/benchmark.js.map +1 -0
- package/dist/perf/index.d.ts +3 -0
- package/dist/perf/index.d.ts.map +1 -0
- package/dist/perf/index.js +6 -0
- package/dist/perf/index.js.map +1 -0
- package/dist/perf/latency.d.ts +39 -0
- package/dist/perf/latency.d.ts.map +1 -0
- package/dist/perf/latency.js +79 -0
- package/dist/perf/latency.js.map +1 -0
- package/dist/privacy.d.ts +45 -0
- package/dist/privacy.d.ts.map +1 -0
- package/dist/privacy.js +66 -0
- package/dist/privacy.js.map +1 -0
- package/dist/providers/indexeddb.d.ts +37 -0
- package/dist/providers/indexeddb.d.ts.map +1 -0
- package/dist/providers/indexeddb.js +45 -0
- package/dist/providers/indexeddb.js.map +1 -0
- package/dist/providers/webrtc.d.ts +40 -0
- package/dist/providers/webrtc.d.ts.map +1 -0
- package/dist/providers/webrtc.js +81 -0
- package/dist/providers/webrtc.js.map +1 -0
- package/dist/providers/websocket.d.ts +45 -0
- package/dist/providers/websocket.d.ts.map +1 -0
- package/dist/providers/websocket.js +56 -0
- package/dist/providers/websocket.js.map +1 -0
- package/dist/security/e2e.d.ts +54 -0
- package/dist/security/e2e.d.ts.map +1 -0
- package/dist/security/e2e.js +147 -0
- package/dist/security/e2e.js.map +1 -0
- package/dist/security/index.d.ts +2 -0
- package/dist/security/index.d.ts.map +1 -0
- package/dist/security/index.js +5 -0
- package/dist/security/index.js.map +1 -0
- package/dist/session.d.ts +79 -0
- package/dist/session.d.ts.map +1 -0
- package/dist/session.js +112 -0
- package/dist/session.js.map +1 -0
- package/dist/snapshot/from-ifcx.d.ts +24 -0
- package/dist/snapshot/from-ifcx.d.ts.map +1 -0
- package/dist/snapshot/from-ifcx.js +93 -0
- package/dist/snapshot/from-ifcx.js.map +1 -0
- package/dist/snapshot/index.d.ts +6 -0
- package/dist/snapshot/index.d.ts.map +1 -0
- package/dist/snapshot/index.js +9 -0
- package/dist/snapshot/index.js.map +1 -0
- package/dist/snapshot/layers.d.ts +56 -0
- package/dist/snapshot/layers.d.ts.map +1 -0
- package/dist/snapshot/layers.js +82 -0
- package/dist/snapshot/layers.js.map +1 -0
- package/dist/snapshot/minimal-layer.d.ts +40 -0
- package/dist/snapshot/minimal-layer.d.ts.map +1 -0
- package/dist/snapshot/minimal-layer.js +123 -0
- package/dist/snapshot/minimal-layer.js.map +1 -0
- package/dist/snapshot/to-ifcx.d.ts +26 -0
- package/dist/snapshot/to-ifcx.d.ts.map +1 -0
- package/dist/snapshot/to-ifcx.js +54 -0
- package/dist/snapshot/to-ifcx.js.map +1 -0
- package/dist/snapshot/worker.d.ts +45 -0
- package/dist/snapshot/worker.d.ts.map +1 -0
- package/dist/snapshot/worker.js +73 -0
- package/dist/snapshot/worker.js.map +1 -0
- package/dist/sync/room.d.ts +15 -0
- package/dist/sync/room.d.ts.map +1 -0
- package/dist/sync/room.js +20 -0
- package/dist/sync/room.js.map +1 -0
- package/dist/undo.d.ts +25 -0
- package/dist/undo.d.ts.map +1 -0
- package/dist/undo.js +37 -0
- package/dist/undo.js.map +1 -0
- package/dist/viewer-bridge.d.ts +27 -0
- package/dist/viewer-bridge.d.ts.map +1 -0
- package/dist/viewer-bridge.js +82 -0
- package/dist/viewer-bridge.js.map +1 -0
- package/package.json +83 -0
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
/* This Source Code Form is subject to the terms of the Mozilla Public
|
|
2
|
+
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
3
|
+
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
|
4
|
+
import { snapshotToIfcx } from '../snapshot/to-ifcx.js';
|
|
5
|
+
import { extractMinimalLayer } from '../snapshot/minimal-layer.js';
|
|
6
|
+
import * as Y from 'yjs';
|
|
7
|
+
/**
|
|
8
|
+
* Cheap deep clone for IFCX snapshots. Uses `structuredClone` when
|
|
9
|
+
* available (Node 17+, all modern browsers) and falls back to a
|
|
10
|
+
* JSON round-trip otherwise — IFCX is plain JSON so the round-trip is
|
|
11
|
+
* sufficient and loses nothing.
|
|
12
|
+
*/
|
|
13
|
+
function cloneIfcx(file) {
|
|
14
|
+
if (typeof structuredClone === 'function') {
|
|
15
|
+
return structuredClone(file);
|
|
16
|
+
}
|
|
17
|
+
return JSON.parse(JSON.stringify(file));
|
|
18
|
+
}
|
|
19
|
+
/* ------------------------------------------------------------------ */
|
|
20
|
+
/* In-memory implementation */
|
|
21
|
+
/* ------------------------------------------------------------------ */
|
|
22
|
+
export class MemoryHistorySidecar {
|
|
23
|
+
entriesByBranch = new Map();
|
|
24
|
+
branchInfo = new Map();
|
|
25
|
+
counter = 0;
|
|
26
|
+
constructor() {
|
|
27
|
+
this.branchInfo.set('main', {
|
|
28
|
+
name: 'main',
|
|
29
|
+
createdAt: new Date().toISOString(),
|
|
30
|
+
});
|
|
31
|
+
this.entriesByBranch.set('main', []);
|
|
32
|
+
}
|
|
33
|
+
nextEntryId() {
|
|
34
|
+
this.counter += 1;
|
|
35
|
+
return `e${this.counter}-${Date.now().toString(36)}`;
|
|
36
|
+
}
|
|
37
|
+
async record(input) {
|
|
38
|
+
const branch = input.branch ?? 'main';
|
|
39
|
+
if (!this.branchInfo.has(branch)) {
|
|
40
|
+
this.branchInfo.set(branch, { name: branch, createdAt: new Date().toISOString() });
|
|
41
|
+
this.entriesByBranch.set(branch, []);
|
|
42
|
+
}
|
|
43
|
+
// Deep-clone snapshot/diff so callers that reuse buffers or mutate
|
|
44
|
+
// their input objects (common in streaming snapshot pipelines)
|
|
45
|
+
// cannot poison historical entries after they were recorded.
|
|
46
|
+
const entry = {
|
|
47
|
+
entryId: this.nextEntryId(),
|
|
48
|
+
at: new Date().toISOString(),
|
|
49
|
+
branch,
|
|
50
|
+
authorClientId: input.authorClientId,
|
|
51
|
+
label: input.label,
|
|
52
|
+
snapshot: cloneIfcx(input.snapshot),
|
|
53
|
+
diff: input.diff ? cloneIfcx(input.diff) : undefined,
|
|
54
|
+
};
|
|
55
|
+
const arr = this.entriesByBranch.get(branch);
|
|
56
|
+
arr.push(entry);
|
|
57
|
+
return entry;
|
|
58
|
+
}
|
|
59
|
+
async entries(branch) {
|
|
60
|
+
if (branch)
|
|
61
|
+
return [...(this.entriesByBranch.get(branch) ?? [])];
|
|
62
|
+
const all = [];
|
|
63
|
+
for (const arr of this.entriesByBranch.values())
|
|
64
|
+
all.push(...arr);
|
|
65
|
+
return all.sort((a, b) => a.at.localeCompare(b.at));
|
|
66
|
+
}
|
|
67
|
+
async at(at, branch) {
|
|
68
|
+
const target = at instanceof Date ? at.toISOString() : at;
|
|
69
|
+
const list = await this.entries(branch);
|
|
70
|
+
let best = null;
|
|
71
|
+
for (const e of list) {
|
|
72
|
+
if (e.at <= target)
|
|
73
|
+
best = e;
|
|
74
|
+
else
|
|
75
|
+
break;
|
|
76
|
+
}
|
|
77
|
+
return best;
|
|
78
|
+
}
|
|
79
|
+
async diff(fromEntryId, toEntryId) {
|
|
80
|
+
const all = await this.entries();
|
|
81
|
+
const from = all.find((e) => e.entryId === fromEntryId);
|
|
82
|
+
const to = all.find((e) => e.entryId === toEntryId);
|
|
83
|
+
if (!from || !to) {
|
|
84
|
+
return { from: fromEntryId, to: toEntryId, added: [], removed: [], changed: [] };
|
|
85
|
+
}
|
|
86
|
+
const fromPaths = new Map();
|
|
87
|
+
const toPaths = new Map();
|
|
88
|
+
for (const n of from.snapshot.data ?? [])
|
|
89
|
+
fromPaths.set(n.path, n);
|
|
90
|
+
for (const n of to.snapshot.data ?? [])
|
|
91
|
+
toPaths.set(n.path, n);
|
|
92
|
+
const added = [];
|
|
93
|
+
const removed = [];
|
|
94
|
+
const changed = [];
|
|
95
|
+
for (const path of toPaths.keys()) {
|
|
96
|
+
if (!fromPaths.has(path))
|
|
97
|
+
added.push(path);
|
|
98
|
+
else if (JSON.stringify(toPaths.get(path)) !== JSON.stringify(fromPaths.get(path))) {
|
|
99
|
+
changed.push(path);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
for (const path of fromPaths.keys()) {
|
|
103
|
+
if (!toPaths.has(path))
|
|
104
|
+
removed.push(path);
|
|
105
|
+
}
|
|
106
|
+
return {
|
|
107
|
+
from: fromEntryId,
|
|
108
|
+
to: toEntryId,
|
|
109
|
+
added: added.sort(),
|
|
110
|
+
removed: removed.sort(),
|
|
111
|
+
changed: changed.sort(),
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
async branches() {
|
|
115
|
+
return Array.from(this.branchInfo.values()).sort((a, b) => a.createdAt.localeCompare(b.createdAt));
|
|
116
|
+
}
|
|
117
|
+
async branch(name, fromEntryId) {
|
|
118
|
+
if (this.branchInfo.has(name)) {
|
|
119
|
+
throw new Error(`@ifc-lite/collab: branch "${name}" already exists`);
|
|
120
|
+
}
|
|
121
|
+
// Honor the documented default: when no explicit fork point is
|
|
122
|
+
// given, take the current head of `main`. Persisting `undefined`
|
|
123
|
+
// dropped fork ancestry for every caller that relied on the API
|
|
124
|
+
// signature's documented behavior.
|
|
125
|
+
let resolvedFork = fromEntryId;
|
|
126
|
+
if (!resolvedFork) {
|
|
127
|
+
const mainEntries = this.entriesByBranch.get('main') ?? [];
|
|
128
|
+
const head = mainEntries[mainEntries.length - 1];
|
|
129
|
+
if (head)
|
|
130
|
+
resolvedFork = head.entryId;
|
|
131
|
+
}
|
|
132
|
+
const info = {
|
|
133
|
+
name,
|
|
134
|
+
forkedFromEntryId: resolvedFork,
|
|
135
|
+
createdAt: new Date().toISOString(),
|
|
136
|
+
};
|
|
137
|
+
this.branchInfo.set(name, info);
|
|
138
|
+
this.entriesByBranch.set(name, []);
|
|
139
|
+
return info;
|
|
140
|
+
}
|
|
141
|
+
async merge(branch, into, mergedSnapshot) {
|
|
142
|
+
// Validate the source branch up front. Previously only `into` was
|
|
143
|
+
// checked, so a typo like merge('experimnet', 'main', …) wrote an
|
|
144
|
+
// irreversible merge entry with no resolvable source.
|
|
145
|
+
const sourceEntries = this.entriesByBranch.get(branch);
|
|
146
|
+
if (!sourceEntries) {
|
|
147
|
+
throw new Error(`@ifc-lite/collab: source branch "${branch}" not found`);
|
|
148
|
+
}
|
|
149
|
+
const targetEntries = this.entriesByBranch.get(into);
|
|
150
|
+
if (!targetEntries) {
|
|
151
|
+
throw new Error(`@ifc-lite/collab: target branch "${into}" not found`);
|
|
152
|
+
}
|
|
153
|
+
// Capture the source branch's CURRENT tip — the commit that was
|
|
154
|
+
// actually merged — into immutable metadata so the merge edge in
|
|
155
|
+
// the branch-tree view doesn't drift if the source branch keeps
|
|
156
|
+
// moving later.
|
|
157
|
+
const sourceTip = sourceEntries[sourceEntries.length - 1];
|
|
158
|
+
const entry = {
|
|
159
|
+
entryId: this.nextEntryId(),
|
|
160
|
+
at: new Date().toISOString(),
|
|
161
|
+
branch: into,
|
|
162
|
+
label: `merge ${branch} → ${into}`,
|
|
163
|
+
snapshot: cloneIfcx(mergedSnapshot),
|
|
164
|
+
mergedFromBranch: branch,
|
|
165
|
+
mergedFromEntryId: sourceTip?.entryId,
|
|
166
|
+
};
|
|
167
|
+
targetEntries.push(entry);
|
|
168
|
+
return entry;
|
|
169
|
+
}
|
|
170
|
+
async clear() {
|
|
171
|
+
this.entriesByBranch.clear();
|
|
172
|
+
this.branchInfo.clear();
|
|
173
|
+
this.branchInfo.set('main', { name: 'main', createdAt: new Date().toISOString() });
|
|
174
|
+
this.entriesByBranch.set('main', []);
|
|
175
|
+
this.counter = 0;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* Drive a `HistorySidecar` from a live `CollabSession`. Records a
|
|
180
|
+
* snapshot every `intervalMs`, plus on-demand via `capture(label)`.
|
|
181
|
+
*/
|
|
182
|
+
export function attachHistorySidecar(session, sidecar, options = {}) {
|
|
183
|
+
const intervalMs = options.intervalMs ?? 60_000;
|
|
184
|
+
const branch = options.branch ?? 'main';
|
|
185
|
+
const includeDiff = options.includeDiff ?? true;
|
|
186
|
+
let lastBaseline = null;
|
|
187
|
+
const capture = async (label) => {
|
|
188
|
+
const ifcx = snapshotToIfcx(session.doc, options.snapshot);
|
|
189
|
+
const diff = includeDiff && lastBaseline
|
|
190
|
+
? extractMinimalLayer(session.doc, lastBaseline, { snapshot: options.snapshot })
|
|
191
|
+
: undefined;
|
|
192
|
+
// Capture the candidate baseline BEFORE record() so we don't read
|
|
193
|
+
// the doc twice with a window in between, but only commit it to
|
|
194
|
+
// `lastBaseline` AFTER record() resolves. If persistence rejects,
|
|
195
|
+
// the next capture must still diff against the previously-recorded
|
|
196
|
+
// state — otherwise history can silently skip changes.
|
|
197
|
+
const nextBaseline = Y.encodeStateAsUpdate(session.doc);
|
|
198
|
+
const entry = await sidecar.record({
|
|
199
|
+
branch,
|
|
200
|
+
snapshot: ifcx,
|
|
201
|
+
diff,
|
|
202
|
+
label,
|
|
203
|
+
authorClientId: session.clientId,
|
|
204
|
+
});
|
|
205
|
+
lastBaseline = nextBaseline;
|
|
206
|
+
return entry;
|
|
207
|
+
};
|
|
208
|
+
const timer = setInterval(() => {
|
|
209
|
+
void capture().catch((err) => {
|
|
210
|
+
// eslint-disable-next-line no-console
|
|
211
|
+
console.error('[collab] history capture failed:', err);
|
|
212
|
+
});
|
|
213
|
+
}, intervalMs);
|
|
214
|
+
// Don't keep Node alive solely for this timer.
|
|
215
|
+
timer.unref?.();
|
|
216
|
+
return {
|
|
217
|
+
capture,
|
|
218
|
+
detach() {
|
|
219
|
+
clearInterval(timer);
|
|
220
|
+
},
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
//# sourceMappingURL=history.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"history.js","sourceRoot":"","sources":["../../src/branch/history.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/branch/index.ts"],"names":[],"mappings":"AAIA,cAAc,aAAa,CAAC;AAC5B,cAAc,cAAc,CAAC;AAC7B,cAAc,wBAAwB,CAAC;AACvC,cAAc,kBAAkB,CAAC"}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/* This Source Code Form is subject to the terms of the Mozilla Public
|
|
2
|
+
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
3
|
+
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
|
4
|
+
export * from './branch.js';
|
|
5
|
+
export * from './history.js';
|
|
6
|
+
export * from './history-automerge.js';
|
|
7
|
+
export * from './branch-tree.js';
|
|
8
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/branch/index.ts"],"names":[],"mappings":"AAAA;;+DAE+D;AAE/D,cAAc,aAAa,CAAC;AAC5B,cAAc,cAAc,CAAC;AAC7B,cAAc,wBAAwB,CAAC;AACvC,cAAc,kBAAkB,CAAC"}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Conflict detector.
|
|
3
|
+
*
|
|
4
|
+
* Observes Y.Doc transactions and emits structured events when concurrent
|
|
5
|
+
* edits land on the same target within a tunable window. The viewer (or
|
|
6
|
+
* any UI) listens to these events and renders a conflict badge plus an
|
|
7
|
+
* optional ghost overlay (§9.4 / §9.7).
|
|
8
|
+
*
|
|
9
|
+
* The CRDT itself never blocks or rolls back on conflict — it converges
|
|
10
|
+
* deterministically. Detection here is purely advisory: it tells the user
|
|
11
|
+
* "your peer changed this at the same time."
|
|
12
|
+
*
|
|
13
|
+
* Implementation note: we use `Transaction.changed` (struct-level
|
|
14
|
+
* changes) rather than `YEvent.keys` (head-level changes) because LWW
|
|
15
|
+
* can keep the existing head value when a remote struct loses, in which
|
|
16
|
+
* case `YEvent.keys` is empty even though there was a concurrent write.
|
|
17
|
+
* The struct-level view is what we want for the conflict surface.
|
|
18
|
+
*/
|
|
19
|
+
import * as Y from 'yjs';
|
|
20
|
+
export type ConflictKind = 'attribute' | 'pset-property' | 'hierarchy' | 'geometry-blob' | 'geometry-param' | 'relationship-target' | 'concurrent-delete';
|
|
21
|
+
export interface ConflictEvent {
|
|
22
|
+
kind: ConflictKind;
|
|
23
|
+
/** Entity / relationship / geometry path involved. */
|
|
24
|
+
path: string;
|
|
25
|
+
/** Sub-path (e.g. attribute name, `pset.prop`, role). */
|
|
26
|
+
field?: string;
|
|
27
|
+
/** clientIDs that contributed conflicting writes within the window. */
|
|
28
|
+
contributors: number[];
|
|
29
|
+
/** Wall-clock ms when the detector first flagged this conflict. */
|
|
30
|
+
detectedAt: number;
|
|
31
|
+
}
|
|
32
|
+
export type ConflictListener = (event: ConflictEvent) => void;
|
|
33
|
+
export interface ConflictDetectorOptions {
|
|
34
|
+
/** Window in ms inside which two writes count as concurrent (default 750). */
|
|
35
|
+
windowMs?: number;
|
|
36
|
+
}
|
|
37
|
+
export interface ConflictDetector {
|
|
38
|
+
onConflict(listener: ConflictListener): () => void;
|
|
39
|
+
/** All currently-active conflicts (cleared after `windowMs * 4`). */
|
|
40
|
+
active(): ConflictEvent[];
|
|
41
|
+
destroy(): void;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Build a conflict detector for `doc`.
|
|
45
|
+
*
|
|
46
|
+
* Subscribes to `afterTransaction` and inspects `tr.changed`, which
|
|
47
|
+
* reports every key whose underlying struct list changed (independent of
|
|
48
|
+
* the head value). For each entry we resolve where in the tree the
|
|
49
|
+
* change happened and classify it into one of `ConflictKind`s.
|
|
50
|
+
*/
|
|
51
|
+
export declare function createConflictDetector(doc: Y.Doc, options?: ConflictDetectorOptions): ConflictDetector;
|
|
52
|
+
//# sourceMappingURL=detector.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"detector.d.ts","sourceRoot":"","sources":["../../src/conflicts/detector.ts"],"names":[],"mappings":"AAIA;;;;;;;;;;;;;;;;;GAiBG;AAEH,OAAO,KAAK,CAAC,MAAM,KAAK,CAAC;AAWzB,MAAM,MAAM,YAAY,GACpB,WAAW,GACX,eAAe,GACf,WAAW,GACX,eAAe,GACf,gBAAgB,GAChB,qBAAqB,GACrB,mBAAmB,CAAC;AAExB,MAAM,WAAW,aAAa;IAC5B,IAAI,EAAE,YAAY,CAAC;IACnB,sDAAsD;IACtD,IAAI,EAAE,MAAM,CAAC;IACb,yDAAyD;IACzD,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,uEAAuE;IACvE,YAAY,EAAE,MAAM,EAAE,CAAC;IACvB,mEAAmE;IACnE,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,MAAM,gBAAgB,GAAG,CAAC,KAAK,EAAE,aAAa,KAAK,IAAI,CAAC;AAE9D,MAAM,WAAW,uBAAuB;IACtC,8EAA8E;IAC9E,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,gBAAgB;IAC/B,UAAU,CAAC,QAAQ,EAAE,gBAAgB,GAAG,MAAM,IAAI,CAAC;IACnD,qEAAqE;IACrE,MAAM,IAAI,aAAa,EAAE,CAAC;IAC1B,OAAO,IAAI,IAAI,CAAC;CACjB;AAaD;;;;;;;GAOG;AACH,wBAAgB,sBAAsB,CACpC,GAAG,EAAE,CAAC,CAAC,GAAG,EACV,OAAO,GAAE,uBAA4B,GACpC,gBAAgB,CAyElB"}
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
/* This Source Code Form is subject to the terms of the Mozilla Public
|
|
2
|
+
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
3
|
+
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
|
4
|
+
import { ENTITY_KEY, GEOMETRY_KEY, RELATIONSHIP_KEY, TOP, } from '../doc/schema.js';
|
|
5
|
+
/**
|
|
6
|
+
* Build a conflict detector for `doc`.
|
|
7
|
+
*
|
|
8
|
+
* Subscribes to `afterTransaction` and inspects `tr.changed`, which
|
|
9
|
+
* reports every key whose underlying struct list changed (independent of
|
|
10
|
+
* the head value). For each entry we resolve where in the tree the
|
|
11
|
+
* change happened and classify it into one of `ConflictKind`s.
|
|
12
|
+
*/
|
|
13
|
+
export function createConflictDetector(doc, options = {}) {
|
|
14
|
+
const windowMs = options.windowMs ?? 750;
|
|
15
|
+
const listeners = new Set();
|
|
16
|
+
const conflicts = [];
|
|
17
|
+
/** key = `${kind}|${path}|${field}` → recent writers. */
|
|
18
|
+
const recentWrites = new Map();
|
|
19
|
+
const flag = (info, writers) => {
|
|
20
|
+
const contributors = Array.from(new Set(writers.map((w) => w.client)));
|
|
21
|
+
if (contributors.length < 2)
|
|
22
|
+
return;
|
|
23
|
+
const event = {
|
|
24
|
+
kind: info.kind,
|
|
25
|
+
path: info.path,
|
|
26
|
+
field: info.field,
|
|
27
|
+
contributors,
|
|
28
|
+
detectedAt: Date.now(),
|
|
29
|
+
};
|
|
30
|
+
conflicts.push(event);
|
|
31
|
+
listeners.forEach((l) => l(event));
|
|
32
|
+
};
|
|
33
|
+
const record = (info, client) => {
|
|
34
|
+
if (client < 0)
|
|
35
|
+
return;
|
|
36
|
+
const key = `${info.kind}|${info.path}|${info.field ?? ''}`;
|
|
37
|
+
const now = Date.now();
|
|
38
|
+
const arr = (recentWrites.get(key) ?? []).filter((w) => now - w.at < windowMs);
|
|
39
|
+
arr.push({ client, at: now });
|
|
40
|
+
recentWrites.set(key, arr);
|
|
41
|
+
if (arr.length >= 2)
|
|
42
|
+
flag(info, arr);
|
|
43
|
+
};
|
|
44
|
+
const onAfterTransaction = (tr) => {
|
|
45
|
+
if (tr.changed.size === 0)
|
|
46
|
+
return;
|
|
47
|
+
const client = tr.local ? doc.clientID : guessRemoteClient(tr);
|
|
48
|
+
if (client < 0)
|
|
49
|
+
return;
|
|
50
|
+
for (const [type, keys] of tr.changed.entries()) {
|
|
51
|
+
const top = topLevelKey(type);
|
|
52
|
+
if (top !== TOP.ENTITIES && top !== TOP.RELATIONSHIPS && top !== TOP.GEOMETRY)
|
|
53
|
+
continue;
|
|
54
|
+
const path = pathFromTop(type);
|
|
55
|
+
if (!path)
|
|
56
|
+
continue;
|
|
57
|
+
for (const key of keys) {
|
|
58
|
+
// For top-level Y.Map changes the parent map's `has(key)` tells
|
|
59
|
+
// us whether this was a delete (key absent → yes) or an add.
|
|
60
|
+
// Only deletes count as conflict-inducing at the top level.
|
|
61
|
+
const isDelete = path.length === 0 && key != null && !type.has(key);
|
|
62
|
+
const info = classify(top, path, key, isDelete);
|
|
63
|
+
if (info)
|
|
64
|
+
record(info, client);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
const cutoff = Date.now() - windowMs * 4;
|
|
68
|
+
while (conflicts.length > 0 && conflicts[0].detectedAt < cutoff) {
|
|
69
|
+
conflicts.shift();
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
doc.on('afterTransaction', onAfterTransaction);
|
|
73
|
+
return {
|
|
74
|
+
onConflict(listener) {
|
|
75
|
+
listeners.add(listener);
|
|
76
|
+
return () => listeners.delete(listener);
|
|
77
|
+
},
|
|
78
|
+
active: () => [...conflicts],
|
|
79
|
+
destroy() {
|
|
80
|
+
doc.off('afterTransaction', onAfterTransaction);
|
|
81
|
+
listeners.clear();
|
|
82
|
+
conflicts.length = 0;
|
|
83
|
+
recentWrites.clear();
|
|
84
|
+
},
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Resolve a changed Y.AbstractType's path from its top-level shared
|
|
89
|
+
* type as a list of keys/indices. Keys at intermediate levels are
|
|
90
|
+
* always strings — we only nest Y.Maps under named keys.
|
|
91
|
+
*/
|
|
92
|
+
function pathFromTop(type) {
|
|
93
|
+
const out = [];
|
|
94
|
+
let node = type;
|
|
95
|
+
while (node) {
|
|
96
|
+
const item = node._item;
|
|
97
|
+
if (!item)
|
|
98
|
+
break; // top-level
|
|
99
|
+
const parentSub = item.parentSub;
|
|
100
|
+
if (typeof parentSub !== 'string')
|
|
101
|
+
return null; // array-indexed nesting; unsupported here
|
|
102
|
+
out.unshift(parentSub);
|
|
103
|
+
node = item.parent;
|
|
104
|
+
}
|
|
105
|
+
return out;
|
|
106
|
+
}
|
|
107
|
+
function topLevelKey(type) {
|
|
108
|
+
let node = type;
|
|
109
|
+
while (node) {
|
|
110
|
+
const item = node._item;
|
|
111
|
+
if (!item) {
|
|
112
|
+
const doc = node.doc;
|
|
113
|
+
if (!doc)
|
|
114
|
+
return undefined;
|
|
115
|
+
for (const [name, shared] of doc.share) {
|
|
116
|
+
if (shared === node)
|
|
117
|
+
return name;
|
|
118
|
+
}
|
|
119
|
+
return undefined;
|
|
120
|
+
}
|
|
121
|
+
node = item.parent;
|
|
122
|
+
}
|
|
123
|
+
return undefined;
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Map a (top, path[], key) triple to a ConflictKind + path + field.
|
|
127
|
+
* `path` is the list of map keys descending from the top-level shared
|
|
128
|
+
* type to the changed AbstractType. `key` is the changed leaf key (or
|
|
129
|
+
* null for array-shaped changes). `isDelete` only matters at
|
|
130
|
+
* `path.length === 0` (top-level entity churn).
|
|
131
|
+
*/
|
|
132
|
+
function classify(top, path, key, isDelete) {
|
|
133
|
+
if (top === TOP.ENTITIES) {
|
|
134
|
+
if (path.length === 0) {
|
|
135
|
+
// Entity create OR delete on the top-level entities map. We only
|
|
136
|
+
// surface deletes — concurrent creates are CRDT-friendly (both
|
|
137
|
+
// entities coexist if they have different paths).
|
|
138
|
+
if (isDelete && key)
|
|
139
|
+
return { kind: 'concurrent-delete', path: key };
|
|
140
|
+
return null;
|
|
141
|
+
}
|
|
142
|
+
const entityPath = path[0];
|
|
143
|
+
if (path.length === 1) {
|
|
144
|
+
// Change on the entity Y.Map itself; sub-key is `key` (e.g. the
|
|
145
|
+
// attributes Y.Map being added/replaced). Not a leaf conflict.
|
|
146
|
+
return null;
|
|
147
|
+
}
|
|
148
|
+
const subMap = path[1];
|
|
149
|
+
if (path.length === 2) {
|
|
150
|
+
switch (subMap) {
|
|
151
|
+
case ENTITY_KEY.ATTRIBUTES:
|
|
152
|
+
return key ? { kind: 'attribute', path: entityPath, field: key } : null;
|
|
153
|
+
case ENTITY_KEY.CHILDREN:
|
|
154
|
+
return key ? { kind: 'hierarchy', path: entityPath, field: key } : null;
|
|
155
|
+
case ENTITY_KEY.PSETS:
|
|
156
|
+
// A new (or replaced) Pset Y.Map at the entity level. Treat
|
|
157
|
+
// the pset name as the field — useful when two peers seed the
|
|
158
|
+
// same Pset concurrently.
|
|
159
|
+
return key ? { kind: 'pset-property', path: entityPath, field: key } : null;
|
|
160
|
+
default:
|
|
161
|
+
return null;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
if (path.length === 3 && subMap === ENTITY_KEY.PSETS) {
|
|
165
|
+
const psetName = path[2];
|
|
166
|
+
return key
|
|
167
|
+
? { kind: 'pset-property', path: entityPath, field: `${psetName}.${key}` }
|
|
168
|
+
: null;
|
|
169
|
+
}
|
|
170
|
+
return null;
|
|
171
|
+
}
|
|
172
|
+
if (top === TOP.GEOMETRY) {
|
|
173
|
+
if (path.length === 0) {
|
|
174
|
+
return null;
|
|
175
|
+
}
|
|
176
|
+
const geomId = path[0];
|
|
177
|
+
if (path.length === 1) {
|
|
178
|
+
// direct field on a geometry node
|
|
179
|
+
if (key === GEOMETRY_KEY.BLOB_HASH) {
|
|
180
|
+
return { kind: 'geometry-blob', path: geomId };
|
|
181
|
+
}
|
|
182
|
+
return null;
|
|
183
|
+
}
|
|
184
|
+
if (path.length === 2 && path[1] === GEOMETRY_KEY.PARAMS) {
|
|
185
|
+
return key ? { kind: 'geometry-param', path: geomId, field: key } : null;
|
|
186
|
+
}
|
|
187
|
+
return null;
|
|
188
|
+
}
|
|
189
|
+
if (top === TOP.RELATIONSHIPS) {
|
|
190
|
+
if (path.length === 0) {
|
|
191
|
+
return null;
|
|
192
|
+
}
|
|
193
|
+
const relPath = path[0];
|
|
194
|
+
if (path.length === 1 && key === RELATIONSHIP_KEY.TARGETS) {
|
|
195
|
+
return { kind: 'relationship-target', path: relPath };
|
|
196
|
+
}
|
|
197
|
+
// Targets array changes: the Y.Array is at path=[relPath, 'targets']
|
|
198
|
+
if (path.length === 2 && path[1] === RELATIONSHIP_KEY.TARGETS) {
|
|
199
|
+
return { kind: 'relationship-target', path: relPath };
|
|
200
|
+
}
|
|
201
|
+
return null;
|
|
202
|
+
}
|
|
203
|
+
return null;
|
|
204
|
+
}
|
|
205
|
+
/**
|
|
206
|
+
* Best-effort attribution of a remote transaction to a clientID.
|
|
207
|
+
*
|
|
208
|
+
* Yjs doesn't carry "the client that authored this transaction" directly;
|
|
209
|
+
* it carries per-struct clientIDs in the `afterState`/`beforeState`
|
|
210
|
+
* vectors. We pick the client whose clock advanced the furthest. Ties
|
|
211
|
+
* are rare and harmless — the conflict UI surfaces all contributors.
|
|
212
|
+
*/
|
|
213
|
+
function guessRemoteClient(tr) {
|
|
214
|
+
let bestClient = -1;
|
|
215
|
+
let bestDelta = 0;
|
|
216
|
+
for (const [client, after] of tr.afterState) {
|
|
217
|
+
const before = tr.beforeState.get(client) ?? 0;
|
|
218
|
+
const delta = after - before;
|
|
219
|
+
if (delta > bestDelta) {
|
|
220
|
+
bestDelta = delta;
|
|
221
|
+
bestClient = client;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
return bestClient;
|
|
225
|
+
}
|
|
226
|
+
//# sourceMappingURL=detector.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"detector.js","sourceRoot":"","sources":["../../src/conflicts/detector.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/conflicts/index.ts"],"names":[],"mappings":"AAIA,cAAc,eAAe,CAAC;AAC9B,cAAc,gBAAgB,CAAC"}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/* This Source Code Form is subject to the terms of the Mozilla Public
|
|
2
|
+
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
3
|
+
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
|
4
|
+
export * from './detector.js';
|
|
5
|
+
export * from './ui-bridge.js';
|
|
6
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/conflicts/index.ts"],"names":[],"mappings":"AAAA;;+DAE+D;AAE/D,cAAc,eAAe,CAAC;AAC9B,cAAc,gBAAgB,CAAC"}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* UI bridge for conflict surfacing (spec §9.7).
|
|
3
|
+
*
|
|
4
|
+
* The detector reports raw events at every concurrent write. The viewer
|
|
5
|
+
* (or any UI) needs:
|
|
6
|
+
* - a stable list of currently-active conflicts grouped by entity path
|
|
7
|
+
* - notifications when a conflict appears, ages out, or resolves
|
|
8
|
+
* - a clean "resolve mine" / "accept theirs" handle that emits a
|
|
9
|
+
* follow-up edit on resolution
|
|
10
|
+
*
|
|
11
|
+
* This module sits between the detector and the UI. It does NOT touch
|
|
12
|
+
* the CRDT — resolution is a normal write that the caller dispatches
|
|
13
|
+
* inside its own transaction.
|
|
14
|
+
*/
|
|
15
|
+
import type { ConflictDetector, ConflictEvent } from './detector.js';
|
|
16
|
+
export type ConflictBucketKey = string;
|
|
17
|
+
export interface ConflictBucket {
|
|
18
|
+
key: ConflictBucketKey;
|
|
19
|
+
/** Entity / relationship / geometry path involved. */
|
|
20
|
+
path: string;
|
|
21
|
+
/** Optional sub-path (e.g. attribute name). */
|
|
22
|
+
field?: string;
|
|
23
|
+
kind: ConflictEvent['kind'];
|
|
24
|
+
/** All clientIDs that wrote in the conflict window. */
|
|
25
|
+
contributors: Set<number>;
|
|
26
|
+
/** Wall-clock ms of the first event in the bucket. */
|
|
27
|
+
firstSeenAt: number;
|
|
28
|
+
/** Wall-clock ms of the most-recent event. */
|
|
29
|
+
lastSeenAt: number;
|
|
30
|
+
/** Number of detector events folded into this bucket. */
|
|
31
|
+
count: number;
|
|
32
|
+
}
|
|
33
|
+
export type BridgeEventType = 'open' | 'update' | 'close';
|
|
34
|
+
export interface BridgeEvent {
|
|
35
|
+
type: BridgeEventType;
|
|
36
|
+
bucket: ConflictBucket;
|
|
37
|
+
}
|
|
38
|
+
export type BridgeListener = (event: BridgeEvent) => void;
|
|
39
|
+
export interface ConflictUIBridgeOptions {
|
|
40
|
+
/** A bucket closes after this many ms with no new events (default 4_000). */
|
|
41
|
+
closeAfterMs?: number;
|
|
42
|
+
}
|
|
43
|
+
export interface ResolutionContext {
|
|
44
|
+
bucket: ConflictBucket;
|
|
45
|
+
}
|
|
46
|
+
export type ResolutionAction = (ctx: ResolutionContext) => void | Promise<void>;
|
|
47
|
+
export interface ConflictUIBridge {
|
|
48
|
+
/** All currently-open buckets. */
|
|
49
|
+
active(): ConflictBucket[];
|
|
50
|
+
/** Subscribe to bucket lifecycle (open / update / close). */
|
|
51
|
+
on(listener: BridgeListener): () => void;
|
|
52
|
+
/**
|
|
53
|
+
* Mark a bucket resolved (close immediately). Useful when the UI
|
|
54
|
+
* dispatches a "keep mine" edit and wants the badge to disappear.
|
|
55
|
+
*/
|
|
56
|
+
resolve(key: ConflictBucketKey): boolean;
|
|
57
|
+
/**
|
|
58
|
+
* Run the registered "keep mine" callback (if any) and close the
|
|
59
|
+
* bucket. Apps register the callback per kind via `onKeepMine`. The
|
|
60
|
+
* callback is responsible for emitting the follow-up CRDT edit that
|
|
61
|
+
* re-asserts the local user's value.
|
|
62
|
+
*/
|
|
63
|
+
keepMine(key: ConflictBucketKey): Promise<boolean>;
|
|
64
|
+
/** Same shape as `keepMine`, but for "accept theirs". */
|
|
65
|
+
acceptTheirs(key: ConflictBucketKey): Promise<boolean>;
|
|
66
|
+
/** Register a "keep mine" handler for a given conflict kind. */
|
|
67
|
+
onKeepMine(kind: ConflictBucket['kind'], action: ResolutionAction): () => void;
|
|
68
|
+
/** Register an "accept theirs" handler for a given conflict kind. */
|
|
69
|
+
onAcceptTheirs(kind: ConflictBucket['kind'], action: ResolutionAction): () => void;
|
|
70
|
+
destroy(): void;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Build a UI bridge subscribed to `detector`.
|
|
74
|
+
*
|
|
75
|
+
* Each `(kind, path, field)` triple maps to one bucket. A new event
|
|
76
|
+
* folded into an existing bucket emits `update`; a new triple emits
|
|
77
|
+
* `open`; an idle timeout or explicit `resolve()` emits `close`.
|
|
78
|
+
*/
|
|
79
|
+
export declare function createConflictUIBridge(detector: ConflictDetector, opts?: ConflictUIBridgeOptions): ConflictUIBridge;
|
|
80
|
+
//# sourceMappingURL=ui-bridge.d.ts.map
|