@dabble/patches 0.6.0 → 0.7.1
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/README.md +221 -208
- package/dist/BaseDoc-DkP3tUhT.d.ts +206 -0
- package/dist/algorithms/lww/consolidateOps.d.ts +40 -0
- package/dist/algorithms/lww/consolidateOps.js +103 -0
- package/dist/algorithms/lww/mergeServerWithLocal.d.ts +22 -0
- package/dist/algorithms/lww/mergeServerWithLocal.js +32 -0
- package/dist/algorithms/{client → ot/client}/applyCommittedChanges.d.ts +10 -3
- package/dist/algorithms/{client → ot/client}/applyCommittedChanges.js +7 -4
- package/dist/algorithms/{client → ot/client}/createStateFromSnapshot.d.ts +3 -3
- package/dist/algorithms/{client → ot/client}/createStateFromSnapshot.js +1 -1
- package/dist/algorithms/ot/server/commitChanges.d.ts +43 -0
- package/dist/algorithms/{server → ot/server}/commitChanges.js +22 -7
- package/dist/algorithms/{server → ot/server}/createVersion.d.ts +5 -5
- package/dist/algorithms/{server → ot/server}/createVersion.js +2 -2
- package/dist/algorithms/{server → ot/server}/getSnapshotAtRevision.d.ts +5 -5
- package/dist/algorithms/{server → ot/server}/getSnapshotAtRevision.js +1 -1
- package/dist/algorithms/{server → ot/server}/getStateAtRevision.d.ts +5 -5
- package/dist/algorithms/{server → ot/server}/getStateAtRevision.js +1 -1
- package/dist/algorithms/{server → ot/server}/handleOfflineSessionsAndBatches.d.ts +5 -5
- package/dist/algorithms/{server → ot/server}/handleOfflineSessionsAndBatches.js +3 -3
- package/dist/algorithms/{server → ot/server}/transformIncomingChanges.d.ts +3 -3
- package/dist/algorithms/{server → ot/server}/transformIncomingChanges.js +3 -3
- package/dist/algorithms/{shared → ot/shared}/applyChanges.d.ts +3 -3
- package/dist/algorithms/{shared → ot/shared}/applyChanges.js +2 -2
- package/dist/algorithms/{shared → ot/shared}/changeBatching.d.ts +3 -3
- package/dist/algorithms/{shared → ot/shared}/changeBatching.js +2 -2
- package/dist/algorithms/{shared → ot/shared}/rebaseChanges.d.ts +3 -3
- package/dist/algorithms/{shared → ot/shared}/rebaseChanges.js +2 -2
- package/dist/client/BaseDoc.d.ts +6 -0
- package/dist/client/BaseDoc.js +70 -0
- package/dist/client/ClientAlgorithm.d.ts +101 -0
- package/dist/client/ClientAlgorithm.js +0 -0
- package/dist/client/InMemoryStore.d.ts +5 -7
- package/dist/client/InMemoryStore.js +7 -36
- package/dist/client/IndexedDBStore.d.ts +39 -73
- package/dist/client/IndexedDBStore.js +17 -220
- package/dist/client/LWWAlgorithm.d.ts +43 -0
- package/dist/client/LWWAlgorithm.js +87 -0
- package/dist/client/LWWClientStore.d.ts +73 -0
- package/dist/client/LWWClientStore.js +0 -0
- package/dist/client/LWWDoc.d.ts +56 -0
- package/dist/client/LWWDoc.js +84 -0
- package/dist/client/LWWInMemoryStore.d.ts +88 -0
- package/dist/client/LWWInMemoryStore.js +208 -0
- package/dist/client/LWWIndexedDBStore.d.ts +91 -0
- package/dist/client/LWWIndexedDBStore.js +275 -0
- package/dist/client/OTAlgorithm.d.ts +42 -0
- package/dist/client/OTAlgorithm.js +113 -0
- package/dist/client/OTClientStore.d.ts +50 -0
- package/dist/client/OTClientStore.js +0 -0
- package/dist/client/OTDoc.d.ts +6 -0
- package/dist/client/OTDoc.js +97 -0
- package/dist/client/OTIndexedDBStore.d.ts +84 -0
- package/dist/client/OTIndexedDBStore.js +163 -0
- package/dist/client/Patches.d.ts +36 -16
- package/dist/client/Patches.js +60 -27
- package/dist/client/PatchesDoc.d.ts +4 -113
- package/dist/client/PatchesDoc.js +3 -153
- package/dist/client/PatchesHistoryClient.js +1 -1
- package/dist/client/PatchesStore.d.ts +8 -105
- package/dist/client/factories.d.ts +72 -0
- package/dist/client/factories.js +80 -0
- package/dist/client/index.d.ts +14 -5
- package/dist/client/index.js +9 -0
- package/dist/compression/index.d.ts +2 -2
- package/dist/compression/index.js +1 -1
- package/dist/{algorithms/shared → compression}/lz.js +1 -1
- package/dist/data/change.js +2 -0
- package/dist/fractionalIndex.d.ts +67 -0
- package/dist/fractionalIndex.js +241 -0
- package/dist/index.d.ts +13 -3
- package/dist/index.js +1 -0
- package/dist/json-patch/types.d.ts +2 -0
- package/dist/net/PatchesClient.js +15 -15
- package/dist/net/PatchesSync.d.ts +20 -8
- package/dist/net/PatchesSync.js +57 -65
- package/dist/net/index.d.ts +7 -11
- package/dist/net/index.js +6 -1
- package/dist/net/protocol/JSONRPCClient.d.ts +4 -4
- package/dist/net/protocol/JSONRPCClient.js +6 -4
- package/dist/net/protocol/JSONRPCServer.d.ts +45 -9
- package/dist/net/protocol/JSONRPCServer.js +63 -8
- package/dist/net/serverContext.d.ts +38 -0
- package/dist/net/serverContext.js +20 -0
- package/dist/net/webrtc/WebRTCTransport.js +1 -1
- package/dist/net/websocket/AuthorizationProvider.d.ts +3 -3
- package/dist/net/websocket/WebSocketServer.d.ts +29 -20
- package/dist/net/websocket/WebSocketServer.js +23 -12
- package/dist/server/BranchManager.d.ts +50 -0
- package/dist/server/BranchManager.js +0 -0
- package/dist/server/CompressedStoreBackend.d.ts +10 -8
- package/dist/server/CompressedStoreBackend.js +7 -13
- package/dist/server/LWWBranchManager.d.ts +82 -0
- package/dist/server/LWWBranchManager.js +99 -0
- package/dist/server/LWWMemoryStoreBackend.d.ts +78 -0
- package/dist/server/LWWMemoryStoreBackend.js +182 -0
- package/dist/server/LWWServer.d.ts +130 -0
- package/dist/server/LWWServer.js +214 -0
- package/dist/server/{PatchesBranchManager.d.ts → OTBranchManager.d.ts} +32 -12
- package/dist/server/{PatchesBranchManager.js → OTBranchManager.js} +27 -42
- package/dist/server/OTServer.d.ts +108 -0
- package/dist/server/OTServer.js +141 -0
- package/dist/server/PatchesHistoryManager.d.ts +21 -16
- package/dist/server/PatchesHistoryManager.js +23 -11
- package/dist/server/PatchesServer.d.ts +70 -81
- package/dist/server/PatchesServer.js +0 -175
- package/dist/server/branchUtils.d.ts +82 -0
- package/dist/server/branchUtils.js +66 -0
- package/dist/server/index.d.ts +18 -7
- package/dist/server/index.js +33 -4
- package/dist/server/tombstone.d.ts +29 -0
- package/dist/server/tombstone.js +32 -0
- package/dist/server/types.d.ts +109 -27
- package/dist/server/utils.d.ts +12 -0
- package/dist/server/utils.js +23 -0
- package/dist/solid/context.d.ts +4 -3
- package/dist/solid/doc-manager.d.ts +3 -3
- package/dist/solid/index.d.ts +4 -3
- package/dist/solid/primitives.d.ts +2 -3
- package/dist/types.d.ts +4 -2
- package/dist/vue/composables.d.ts +2 -3
- package/dist/vue/doc-manager.d.ts +3 -3
- package/dist/vue/index.d.ts +4 -3
- package/dist/vue/provider.d.ts +4 -3
- package/package.json +1 -1
- package/dist/algorithms/client/collapsePendingChanges.d.ts +0 -30
- package/dist/algorithms/client/collapsePendingChanges.js +0 -78
- package/dist/algorithms/client/makeChange.d.ts +0 -9
- package/dist/algorithms/client/makeChange.js +0 -29
- package/dist/algorithms/server/commitChanges.d.ts +0 -19
- package/dist/net/websocket/RPCServer.d.ts +0 -141
- package/dist/net/websocket/RPCServer.js +0 -204
- /package/dist/{algorithms/shared → compression}/lz.d.ts +0 -0
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
import "../chunk-IZ2YBCUP.js";
|
|
2
|
+
class LWWMemoryStoreBackend {
|
|
3
|
+
docs = /* @__PURE__ */ new Map();
|
|
4
|
+
tombstones = /* @__PURE__ */ new Map();
|
|
5
|
+
versions = /* @__PURE__ */ new Map();
|
|
6
|
+
branches = /* @__PURE__ */ new Map();
|
|
7
|
+
getOrCreateDoc(docId) {
|
|
8
|
+
let doc = this.docs.get(docId);
|
|
9
|
+
if (!doc) {
|
|
10
|
+
doc = { snapshot: null, ops: [], rev: 0 };
|
|
11
|
+
this.docs.set(docId, doc);
|
|
12
|
+
}
|
|
13
|
+
return doc;
|
|
14
|
+
}
|
|
15
|
+
// === Revision ===
|
|
16
|
+
async getCurrentRev(docId) {
|
|
17
|
+
return this.docs.get(docId)?.rev ?? 0;
|
|
18
|
+
}
|
|
19
|
+
// === Snapshot ===
|
|
20
|
+
async getSnapshot(docId) {
|
|
21
|
+
return this.docs.get(docId)?.snapshot ?? null;
|
|
22
|
+
}
|
|
23
|
+
async saveSnapshot(docId, state, rev) {
|
|
24
|
+
const doc = this.getOrCreateDoc(docId);
|
|
25
|
+
doc.snapshot = { state, rev };
|
|
26
|
+
doc.ops = doc.ops.filter((op) => (op.rev ?? 0) > rev);
|
|
27
|
+
}
|
|
28
|
+
// === Ops ===
|
|
29
|
+
async saveOps(docId, newOps, pathsToDelete) {
|
|
30
|
+
const doc = this.getOrCreateDoc(docId);
|
|
31
|
+
const newRev = ++doc.rev;
|
|
32
|
+
if (pathsToDelete) {
|
|
33
|
+
for (const path of pathsToDelete) {
|
|
34
|
+
doc.ops = doc.ops.filter((op) => op.path !== path);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
for (const op of newOps) {
|
|
38
|
+
op.rev = newRev;
|
|
39
|
+
const childPrefix = op.path + "/";
|
|
40
|
+
doc.ops = doc.ops.filter((existing) => existing.path !== op.path && !existing.path.startsWith(childPrefix));
|
|
41
|
+
doc.ops.push(op);
|
|
42
|
+
}
|
|
43
|
+
return newRev;
|
|
44
|
+
}
|
|
45
|
+
async listOps(docId, options) {
|
|
46
|
+
const doc = this.docs.get(docId);
|
|
47
|
+
if (!doc) {
|
|
48
|
+
return [];
|
|
49
|
+
}
|
|
50
|
+
if (!options) {
|
|
51
|
+
return [...doc.ops];
|
|
52
|
+
}
|
|
53
|
+
if ("sinceRev" in options) {
|
|
54
|
+
return doc.ops.filter((op) => (op.rev ?? 0) > options.sinceRev);
|
|
55
|
+
}
|
|
56
|
+
if ("paths" in options) {
|
|
57
|
+
const pathSet = new Set(options.paths);
|
|
58
|
+
return doc.ops.filter((op) => pathSet.has(op.path));
|
|
59
|
+
}
|
|
60
|
+
return [];
|
|
61
|
+
}
|
|
62
|
+
// === Deletion ===
|
|
63
|
+
async deleteDoc(docId) {
|
|
64
|
+
this.docs.delete(docId);
|
|
65
|
+
}
|
|
66
|
+
// === Tombstones ===
|
|
67
|
+
async createTombstone(tombstone) {
|
|
68
|
+
this.tombstones.set(tombstone.docId, tombstone);
|
|
69
|
+
}
|
|
70
|
+
async getTombstone(docId) {
|
|
71
|
+
return this.tombstones.get(docId);
|
|
72
|
+
}
|
|
73
|
+
async removeTombstone(docId) {
|
|
74
|
+
this.tombstones.delete(docId);
|
|
75
|
+
}
|
|
76
|
+
// === Versioning ===
|
|
77
|
+
async createVersion(docId, metadata, state, _changes) {
|
|
78
|
+
const versions = this.versions.get(docId) || [];
|
|
79
|
+
versions.push({ metadata, state });
|
|
80
|
+
this.versions.set(docId, versions);
|
|
81
|
+
}
|
|
82
|
+
async listVersions(docId, options) {
|
|
83
|
+
const versions = this.versions.get(docId) || [];
|
|
84
|
+
let result = versions.map((v) => v.metadata);
|
|
85
|
+
if (!options) return result;
|
|
86
|
+
if (options.origin) {
|
|
87
|
+
result = result.filter((v) => v.origin === options.origin);
|
|
88
|
+
}
|
|
89
|
+
if (options.groupId) {
|
|
90
|
+
result = result.filter((v) => v.groupId === options.groupId);
|
|
91
|
+
}
|
|
92
|
+
const orderBy = options.orderBy || "endRev";
|
|
93
|
+
result.sort((a, b) => {
|
|
94
|
+
const aVal = a[orderBy];
|
|
95
|
+
const bVal = b[orderBy];
|
|
96
|
+
return aVal - bVal;
|
|
97
|
+
});
|
|
98
|
+
if (options.reverse) {
|
|
99
|
+
result.reverse();
|
|
100
|
+
}
|
|
101
|
+
if (options.startAfter !== void 0) {
|
|
102
|
+
const startVal = options.startAfter;
|
|
103
|
+
result = result.filter((v) => v[orderBy] > startVal);
|
|
104
|
+
}
|
|
105
|
+
if (options.endBefore !== void 0) {
|
|
106
|
+
const endVal = options.endBefore;
|
|
107
|
+
result = result.filter((v) => v[orderBy] < endVal);
|
|
108
|
+
}
|
|
109
|
+
if (options.limit !== void 0) {
|
|
110
|
+
result = result.slice(0, options.limit);
|
|
111
|
+
}
|
|
112
|
+
return result;
|
|
113
|
+
}
|
|
114
|
+
async loadVersionState(docId, versionId) {
|
|
115
|
+
const versions = this.versions.get(docId) || [];
|
|
116
|
+
const version = versions.find((v) => v.metadata.id === versionId);
|
|
117
|
+
return version?.state;
|
|
118
|
+
}
|
|
119
|
+
async updateVersion(docId, versionId, metadata) {
|
|
120
|
+
const versions = this.versions.get(docId) || [];
|
|
121
|
+
const version = versions.find((v) => v.metadata.id === versionId);
|
|
122
|
+
if (version) {
|
|
123
|
+
Object.assign(version.metadata, metadata);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
// === Branching ===
|
|
127
|
+
async listBranches(docId) {
|
|
128
|
+
const result = [];
|
|
129
|
+
for (const branch of this.branches.values()) {
|
|
130
|
+
if (branch.docId === docId) {
|
|
131
|
+
result.push(branch);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
return result;
|
|
135
|
+
}
|
|
136
|
+
async loadBranch(branchId) {
|
|
137
|
+
return this.branches.get(branchId) || null;
|
|
138
|
+
}
|
|
139
|
+
async createBranch(branch) {
|
|
140
|
+
this.branches.set(branch.id, branch);
|
|
141
|
+
}
|
|
142
|
+
async updateBranch(branchId, updates) {
|
|
143
|
+
const branch = this.branches.get(branchId);
|
|
144
|
+
if (branch) {
|
|
145
|
+
Object.assign(branch, updates);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
async closeBranch(branchId) {
|
|
149
|
+
await this.updateBranch(branchId, { status: "closed" });
|
|
150
|
+
}
|
|
151
|
+
// === Testing utilities ===
|
|
152
|
+
/**
|
|
153
|
+
* Clears all data from the store. Useful for test cleanup.
|
|
154
|
+
*/
|
|
155
|
+
clear() {
|
|
156
|
+
this.docs.clear();
|
|
157
|
+
this.tombstones.clear();
|
|
158
|
+
this.versions.clear();
|
|
159
|
+
this.branches.clear();
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* Gets the raw document data for inspection in tests.
|
|
163
|
+
*/
|
|
164
|
+
getDocData(docId) {
|
|
165
|
+
return this.docs.get(docId);
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* Gets the versions for a document for inspection in tests.
|
|
169
|
+
*/
|
|
170
|
+
getVersions(docId) {
|
|
171
|
+
return this.versions.get(docId);
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* Gets all branches for inspection in tests.
|
|
175
|
+
*/
|
|
176
|
+
getBranches() {
|
|
177
|
+
return this.branches;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
export {
|
|
181
|
+
LWWMemoryStoreBackend
|
|
182
|
+
};
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import { Signal } from '../event-signal.js';
|
|
2
|
+
import { ApiDefinition } from '../net/protocol/JSONRPCServer.js';
|
|
3
|
+
import { Change, DeleteDocOptions, PatchesState, ChangeInput, CommitChangesOptions, ChangeMutator, EditableVersionMetadata } from '../types.js';
|
|
4
|
+
import { PatchesServer } from './PatchesServer.js';
|
|
5
|
+
import { LWWStoreBackend } from './types.js';
|
|
6
|
+
import '../net/websocket/AuthorizationProvider.js';
|
|
7
|
+
import '../json-patch/types.js';
|
|
8
|
+
import '../json-patch/JSONPatch.js';
|
|
9
|
+
import '@dabble/delta';
|
|
10
|
+
import '../net/protocol/types.js';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Configuration options for LWWServer.
|
|
14
|
+
*/
|
|
15
|
+
interface LWWServerOptions {
|
|
16
|
+
/**
|
|
17
|
+
* Number of revisions between automatic snapshots.
|
|
18
|
+
* Defaults to 200.
|
|
19
|
+
*/
|
|
20
|
+
snapshotInterval?: number;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Last-Write-Wins (LWW) server implementation.
|
|
24
|
+
*
|
|
25
|
+
* Unlike OTServer which stores changes and uses Operational Transformation,
|
|
26
|
+
* LWWServer stores fields with timestamps. Conflicts are resolved by comparing
|
|
27
|
+
* timestamps - the later timestamp wins.
|
|
28
|
+
*
|
|
29
|
+
* Key differences from OT:
|
|
30
|
+
* - Stores fields, not changes
|
|
31
|
+
* - No transformation needed
|
|
32
|
+
* - Simpler conflict resolution
|
|
33
|
+
* - Better suited for settings, preferences, status data
|
|
34
|
+
*
|
|
35
|
+
* @example
|
|
36
|
+
* ```typescript
|
|
37
|
+
* import { LWWServer } from '@dabble/patches/server';
|
|
38
|
+
*
|
|
39
|
+
* const store = new MyLWWStoreBackend();
|
|
40
|
+
* const server = new LWWServer(store);
|
|
41
|
+
*
|
|
42
|
+
* // Commit changes with timestamps
|
|
43
|
+
* const changes = await server.commitChanges('doc1', [{
|
|
44
|
+
* id: 'change1',
|
|
45
|
+
* ops: [{ op: 'replace', path: '/name', value: 'Alice', ts: Date.now() }],
|
|
46
|
+
* }]);
|
|
47
|
+
* ```
|
|
48
|
+
*/
|
|
49
|
+
declare class LWWServer implements PatchesServer {
|
|
50
|
+
/**
|
|
51
|
+
* Static API definition for use with JSONRPCServer.register().
|
|
52
|
+
* Maps method names to required access levels.
|
|
53
|
+
*/
|
|
54
|
+
static api: ApiDefinition;
|
|
55
|
+
readonly store: LWWStoreBackend;
|
|
56
|
+
private readonly snapshotInterval;
|
|
57
|
+
/** Notifies listeners whenever a batch of changes is successfully committed. */
|
|
58
|
+
readonly onChangesCommitted: Signal<(docId: string, changes: Change[], originClientId?: string) => void>;
|
|
59
|
+
/** Notifies listeners when a document is deleted. */
|
|
60
|
+
readonly onDocDeleted: Signal<(docId: string, options?: DeleteDocOptions, originClientId?: string) => void>;
|
|
61
|
+
constructor(store: LWWStoreBackend, options?: LWWServerOptions);
|
|
62
|
+
/**
|
|
63
|
+
* Get the current state of a document.
|
|
64
|
+
* Reconstructs state from snapshot + ops changed since snapshot.
|
|
65
|
+
*
|
|
66
|
+
* @param docId - The document ID.
|
|
67
|
+
* @returns The document state and revision, or `{ state: {}, rev: 0 }` if not found.
|
|
68
|
+
*/
|
|
69
|
+
getDoc(docId: string): Promise<PatchesState>;
|
|
70
|
+
/**
|
|
71
|
+
* Get changes that occurred after a specific revision.
|
|
72
|
+
* LWW doesn't store changes, so this synthesizes a change from ops.
|
|
73
|
+
*
|
|
74
|
+
* @param docId - The document ID.
|
|
75
|
+
* @param rev - The revision number to get changes after.
|
|
76
|
+
* @returns Array containing 0 or 1 synthesized changes.
|
|
77
|
+
*/
|
|
78
|
+
getChangesSince(docId: string, rev: number): Promise<Change[]>;
|
|
79
|
+
/**
|
|
80
|
+
* Commit changes to a document using LWW conflict resolution.
|
|
81
|
+
*
|
|
82
|
+
* Uses the consolidateOps algorithm to:
|
|
83
|
+
* 1. Handle parent hierarchy validation (returns correction ops)
|
|
84
|
+
* 2. Consolidate incoming ops with existing ops using LWW rules
|
|
85
|
+
* 3. Convert delta ops (@inc, @bit, etc.) to concrete replace ops
|
|
86
|
+
*
|
|
87
|
+
* @param docId - The document ID.
|
|
88
|
+
* @param changes - The changes to commit (always 1 for LWW).
|
|
89
|
+
* @param _options - Optional commit options (ignored for LWW).
|
|
90
|
+
* @returns Array containing 0-1 changes with catchup ops and new rev.
|
|
91
|
+
*/
|
|
92
|
+
commitChanges(docId: string, changes: ChangeInput[], _options?: CommitChangesOptions): Promise<Change[]>;
|
|
93
|
+
/**
|
|
94
|
+
* Delete a document and emit deletion signal.
|
|
95
|
+
* Creates a tombstone if the store supports it.
|
|
96
|
+
*
|
|
97
|
+
* @param docId - The document ID.
|
|
98
|
+
* @param options - Optional deletion options.
|
|
99
|
+
*/
|
|
100
|
+
deleteDoc(docId: string, options?: DeleteDocOptions): Promise<void>;
|
|
101
|
+
/**
|
|
102
|
+
* Removes the tombstone for a deleted document, allowing it to be recreated.
|
|
103
|
+
* @param docId The document ID.
|
|
104
|
+
* @returns True if tombstone was found and removed, false if no tombstone existed.
|
|
105
|
+
*/
|
|
106
|
+
undeleteDoc(docId: string): Promise<boolean>;
|
|
107
|
+
/**
|
|
108
|
+
* Make a server-side change to a document.
|
|
109
|
+
* @param docId - The document ID.
|
|
110
|
+
* @param mutator - A function that receives a JSONPatch and PathProxy to define the changes.
|
|
111
|
+
* @param metadata - Optional metadata for the change.
|
|
112
|
+
* @returns The created change, or null if no operations were generated.
|
|
113
|
+
*/
|
|
114
|
+
change<T = Record<string, any>>(docId: string, mutator: ChangeMutator<T>, metadata?: Record<string, any>): Promise<Change | null>;
|
|
115
|
+
/**
|
|
116
|
+
* Captures the current state of a document as a new version.
|
|
117
|
+
* Only works if store implements VersioningStoreBackend.
|
|
118
|
+
*
|
|
119
|
+
* @param docId - The document ID.
|
|
120
|
+
* @param metadata - Optional metadata for the version.
|
|
121
|
+
* @returns The ID of the created version, or null if no document exists.
|
|
122
|
+
*/
|
|
123
|
+
captureCurrentVersion(docId: string, metadata?: EditableVersionMetadata): Promise<string | null>;
|
|
124
|
+
/**
|
|
125
|
+
* Type guard to check if the store supports versioning.
|
|
126
|
+
*/
|
|
127
|
+
private isVersioningStore;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export { LWWServer, type LWWServerOptions };
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
import "../chunk-IZ2YBCUP.js";
|
|
2
|
+
import { createVersionMetadata } from "../data/version.js";
|
|
3
|
+
import { consolidateOps, convertDeltaOps } from "../algorithms/lww/consolidateOps.js";
|
|
4
|
+
import { createChange } from "../data/change.js";
|
|
5
|
+
import { signal } from "../event-signal.js";
|
|
6
|
+
import { createJSONPatch } from "../json-patch/createJSONPatch.js";
|
|
7
|
+
import { JSONPatch } from "../json-patch/JSONPatch.js";
|
|
8
|
+
import { getClientId } from "../net/serverContext.js";
|
|
9
|
+
import { createTombstoneIfSupported, removeTombstoneIfExists } from "./tombstone.js";
|
|
10
|
+
import { assertVersionMetadata } from "./utils.js";
|
|
11
|
+
class LWWServer {
|
|
12
|
+
/**
|
|
13
|
+
* Static API definition for use with JSONRPCServer.register().
|
|
14
|
+
* Maps method names to required access levels.
|
|
15
|
+
*/
|
|
16
|
+
static api = {
|
|
17
|
+
getDoc: "read",
|
|
18
|
+
getChangesSince: "read",
|
|
19
|
+
commitChanges: "write",
|
|
20
|
+
deleteDoc: "write",
|
|
21
|
+
undeleteDoc: "write"
|
|
22
|
+
};
|
|
23
|
+
store;
|
|
24
|
+
snapshotInterval;
|
|
25
|
+
/** Notifies listeners whenever a batch of changes is successfully committed. */
|
|
26
|
+
onChangesCommitted = signal();
|
|
27
|
+
/** Notifies listeners when a document is deleted. */
|
|
28
|
+
onDocDeleted = signal();
|
|
29
|
+
constructor(store, options = {}) {
|
|
30
|
+
this.store = store;
|
|
31
|
+
this.snapshotInterval = options.snapshotInterval ?? 200;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Get the current state of a document.
|
|
35
|
+
* Reconstructs state from snapshot + ops changed since snapshot.
|
|
36
|
+
*
|
|
37
|
+
* @param docId - The document ID.
|
|
38
|
+
* @returns The document state and revision, or `{ state: {}, rev: 0 }` if not found.
|
|
39
|
+
*/
|
|
40
|
+
async getDoc(docId) {
|
|
41
|
+
const snapshot = await this.store.getSnapshot(docId);
|
|
42
|
+
const baseState = snapshot?.state ?? {};
|
|
43
|
+
const baseRev = snapshot?.rev ?? 0;
|
|
44
|
+
const ops = await this.store.listOps(docId, { sinceRev: baseRev });
|
|
45
|
+
if (ops.length === 0) {
|
|
46
|
+
return { state: baseState, rev: baseRev };
|
|
47
|
+
}
|
|
48
|
+
const state = new JSONPatch(ops).apply(baseState);
|
|
49
|
+
const rev = Math.max(baseRev, ...ops.map((op) => op.rev ?? 0));
|
|
50
|
+
return { state, rev };
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Get changes that occurred after a specific revision.
|
|
54
|
+
* LWW doesn't store changes, so this synthesizes a change from ops.
|
|
55
|
+
*
|
|
56
|
+
* @param docId - The document ID.
|
|
57
|
+
* @param rev - The revision number to get changes after.
|
|
58
|
+
* @returns Array containing 0 or 1 synthesized changes.
|
|
59
|
+
*/
|
|
60
|
+
async getChangesSince(docId, rev) {
|
|
61
|
+
const ops = await this.store.listOps(docId, { sinceRev: rev });
|
|
62
|
+
if (ops.length === 0) {
|
|
63
|
+
return [];
|
|
64
|
+
}
|
|
65
|
+
const sortedOps = [...ops].sort((a, b) => (a.ts ?? 0) - (b.ts ?? 0));
|
|
66
|
+
const maxRev = Math.max(...ops.map((op) => op.rev ?? 0));
|
|
67
|
+
const maxTs = Math.max(...ops.map((op) => op.ts ?? 0));
|
|
68
|
+
return [createChange(rev, maxRev, sortedOps, { committedAt: maxTs || Date.now() })];
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Commit changes to a document using LWW conflict resolution.
|
|
72
|
+
*
|
|
73
|
+
* Uses the consolidateOps algorithm to:
|
|
74
|
+
* 1. Handle parent hierarchy validation (returns correction ops)
|
|
75
|
+
* 2. Consolidate incoming ops with existing ops using LWW rules
|
|
76
|
+
* 3. Convert delta ops (@inc, @bit, etc.) to concrete replace ops
|
|
77
|
+
*
|
|
78
|
+
* @param docId - The document ID.
|
|
79
|
+
* @param changes - The changes to commit (always 1 for LWW).
|
|
80
|
+
* @param _options - Optional commit options (ignored for LWW).
|
|
81
|
+
* @returns Array containing 0-1 changes with catchup ops and new rev.
|
|
82
|
+
*/
|
|
83
|
+
async commitChanges(docId, changes, _options) {
|
|
84
|
+
if (changes.length === 0) {
|
|
85
|
+
return [];
|
|
86
|
+
}
|
|
87
|
+
const change = changes[0];
|
|
88
|
+
const serverNow = Date.now();
|
|
89
|
+
const clientRev = change.rev;
|
|
90
|
+
const newOps = change.ops.map((op) => op.ts ? op : { ...op, ts: serverNow });
|
|
91
|
+
const existingOps = await this.store.listOps(docId);
|
|
92
|
+
const { opsToSave, pathsToDelete, opsToReturn } = consolidateOps(existingOps, newOps);
|
|
93
|
+
const opsToStore = convertDeltaOps(opsToSave);
|
|
94
|
+
const currentRev = await this.store.getCurrentRev(docId);
|
|
95
|
+
let newRev = currentRev;
|
|
96
|
+
if (opsToStore.length > 0 || pathsToDelete.length > 0) {
|
|
97
|
+
newRev = await this.store.saveOps(docId, opsToStore, pathsToDelete);
|
|
98
|
+
}
|
|
99
|
+
if (newRev > 0 && newRev % this.snapshotInterval === 0) {
|
|
100
|
+
const { state } = await this.getDoc(docId);
|
|
101
|
+
await this.store.saveSnapshot(docId, state, newRev);
|
|
102
|
+
}
|
|
103
|
+
const responseOps = [...opsToReturn];
|
|
104
|
+
if (clientRev !== void 0) {
|
|
105
|
+
const opsSince = await this.store.listOps(docId, { sinceRev: clientRev });
|
|
106
|
+
const sentPaths = new Set(change.ops.map((o) => o.path));
|
|
107
|
+
const catchupOps = opsSince.filter((op) => !isPathOrChild(op.path, sentPaths)).sort((a, b) => (a.ts ?? 0) - (b.ts ?? 0));
|
|
108
|
+
responseOps.push(...catchupOps);
|
|
109
|
+
}
|
|
110
|
+
const responseChange = createChange(clientRev ?? 0, newRev, responseOps, {
|
|
111
|
+
id: change.id,
|
|
112
|
+
committedAt: serverNow
|
|
113
|
+
});
|
|
114
|
+
if (opsToStore.length > 0) {
|
|
115
|
+
try {
|
|
116
|
+
const broadcastChange = createChange(currentRev, newRev, opsToStore, {
|
|
117
|
+
id: change.id,
|
|
118
|
+
committedAt: serverNow
|
|
119
|
+
});
|
|
120
|
+
await this.onChangesCommitted.emit(docId, [broadcastChange], getClientId());
|
|
121
|
+
} catch (error) {
|
|
122
|
+
console.error(`Failed to notify clients about committed changes for doc ${docId}:`, error);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
return [responseChange];
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Delete a document and emit deletion signal.
|
|
129
|
+
* Creates a tombstone if the store supports it.
|
|
130
|
+
*
|
|
131
|
+
* @param docId - The document ID.
|
|
132
|
+
* @param options - Optional deletion options.
|
|
133
|
+
*/
|
|
134
|
+
async deleteDoc(docId, options) {
|
|
135
|
+
const clientId = getClientId();
|
|
136
|
+
const rev = await this.store.getCurrentRev(docId);
|
|
137
|
+
await createTombstoneIfSupported(this.store, docId, rev, clientId, options?.skipTombstone);
|
|
138
|
+
await this.store.deleteDoc(docId);
|
|
139
|
+
await this.onDocDeleted.emit(docId, options, clientId);
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Removes the tombstone for a deleted document, allowing it to be recreated.
|
|
143
|
+
* @param docId The document ID.
|
|
144
|
+
* @returns True if tombstone was found and removed, false if no tombstone existed.
|
|
145
|
+
*/
|
|
146
|
+
async undeleteDoc(docId) {
|
|
147
|
+
return removeTombstoneIfExists(this.store, docId);
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* Make a server-side change to a document.
|
|
151
|
+
* @param docId - The document ID.
|
|
152
|
+
* @param mutator - A function that receives a JSONPatch and PathProxy to define the changes.
|
|
153
|
+
* @param metadata - Optional metadata for the change.
|
|
154
|
+
* @returns The created change, or null if no operations were generated.
|
|
155
|
+
*/
|
|
156
|
+
async change(docId, mutator, metadata) {
|
|
157
|
+
const { state, rev } = await this.getDoc(docId);
|
|
158
|
+
const patch = createJSONPatch(mutator);
|
|
159
|
+
if (patch.ops.length === 0) {
|
|
160
|
+
return null;
|
|
161
|
+
}
|
|
162
|
+
const serverNow = Date.now();
|
|
163
|
+
const opsWithTs = patch.ops.map((op) => ({ ...op, ts: serverNow }));
|
|
164
|
+
const change = createChange(rev, rev + 1, opsWithTs, metadata);
|
|
165
|
+
patch.apply(state);
|
|
166
|
+
await this.commitChanges(docId, [change]);
|
|
167
|
+
return change;
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* Captures the current state of a document as a new version.
|
|
171
|
+
* Only works if store implements VersioningStoreBackend.
|
|
172
|
+
*
|
|
173
|
+
* @param docId - The document ID.
|
|
174
|
+
* @param metadata - Optional metadata for the version.
|
|
175
|
+
* @returns The ID of the created version, or null if no document exists.
|
|
176
|
+
*/
|
|
177
|
+
async captureCurrentVersion(docId, metadata) {
|
|
178
|
+
assertVersionMetadata(metadata);
|
|
179
|
+
if (!this.isVersioningStore(this.store)) {
|
|
180
|
+
throw new Error("LWW versioning requires a store that implements VersioningStoreBackend");
|
|
181
|
+
}
|
|
182
|
+
const { state, rev } = await this.getDoc(docId);
|
|
183
|
+
if (rev === 0) {
|
|
184
|
+
return null;
|
|
185
|
+
}
|
|
186
|
+
const versionMetadata = createVersionMetadata({
|
|
187
|
+
origin: "main",
|
|
188
|
+
startedAt: Date.now(),
|
|
189
|
+
endedAt: Date.now(),
|
|
190
|
+
startRev: rev,
|
|
191
|
+
endRev: rev,
|
|
192
|
+
...metadata
|
|
193
|
+
});
|
|
194
|
+
await this.store.createVersion(docId, versionMetadata, state);
|
|
195
|
+
return versionMetadata.id;
|
|
196
|
+
}
|
|
197
|
+
/**
|
|
198
|
+
* Type guard to check if the store supports versioning.
|
|
199
|
+
*/
|
|
200
|
+
isVersioningStore(store) {
|
|
201
|
+
return "createVersion" in store;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
function isPathOrChild(path, sentPaths) {
|
|
205
|
+
for (const sent of sentPaths) {
|
|
206
|
+
if (path === sent || path.startsWith(sent + "/")) {
|
|
207
|
+
return true;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
return false;
|
|
211
|
+
}
|
|
212
|
+
export {
|
|
213
|
+
LWWServer
|
|
214
|
+
};
|
|
@@ -1,23 +1,39 @@
|
|
|
1
|
+
import { ApiDefinition } from '../net/protocol/JSONRPCServer.js';
|
|
1
2
|
import { Branch, EditableBranchMetadata, BranchStatus, Change } from '../types.js';
|
|
3
|
+
import { BranchManager } from './BranchManager.js';
|
|
2
4
|
import { PatchesServer } from './PatchesServer.js';
|
|
3
|
-
import { BranchingStoreBackend } from './types.js';
|
|
5
|
+
import { OTStoreBackend, BranchingStoreBackend } from './types.js';
|
|
6
|
+
export { assertBranchMetadata } from './branchUtils.js';
|
|
7
|
+
import '../event-signal.js';
|
|
8
|
+
import '../net/websocket/AuthorizationProvider.js';
|
|
9
|
+
import '../json-patch/types.js';
|
|
4
10
|
import '../json-patch/JSONPatch.js';
|
|
5
11
|
import '@dabble/delta';
|
|
6
|
-
import '../
|
|
7
|
-
import '../event-signal.js';
|
|
8
|
-
import '../compression/index.js';
|
|
9
|
-
import '../algorithms/shared/lz.js';
|
|
12
|
+
import '../net/protocol/types.js';
|
|
10
13
|
|
|
11
14
|
/**
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
* development of a document with the ability to merge changes back into the original document later.
|
|
15
|
+
* Combined store backend type for OT branch management.
|
|
16
|
+
* Requires both OT operations and branch metadata operations.
|
|
15
17
|
*/
|
|
16
|
-
|
|
18
|
+
type OTBranchStore = OTStoreBackend & BranchingStoreBackend;
|
|
19
|
+
/**
|
|
20
|
+
* OT-specific branch manager implementation.
|
|
21
|
+
*
|
|
22
|
+
* Manages branches for documents using Operational Transformation semantics:
|
|
23
|
+
* - Creates branches at specific revision points
|
|
24
|
+
* - Uses fast-forward merge when possible (no concurrent changes on source)
|
|
25
|
+
* - Falls back to flattened merge for divergent histories
|
|
26
|
+
*
|
|
27
|
+
* A branch is a document that originates from another document at a specific revision.
|
|
28
|
+
* Its first version represents the source document's state at that revision.
|
|
29
|
+
* Branches allow parallel development with the ability to merge changes back.
|
|
30
|
+
*/
|
|
31
|
+
declare class OTBranchManager implements BranchManager {
|
|
17
32
|
private readonly store;
|
|
18
33
|
private readonly patchesServer;
|
|
19
34
|
private readonly maxPayloadBytes?;
|
|
20
|
-
|
|
35
|
+
static api: ApiDefinition;
|
|
36
|
+
constructor(store: OTBranchStore, patchesServer: PatchesServer, maxPayloadBytes?: number | undefined);
|
|
21
37
|
/**
|
|
22
38
|
* Lists all open branches for a document.
|
|
23
39
|
* @param docId - The ID of the document.
|
|
@@ -53,6 +69,10 @@ declare class PatchesBranchManager {
|
|
|
53
69
|
*/
|
|
54
70
|
mergeBranch(branchId: string): Promise<Change[]>;
|
|
55
71
|
}
|
|
56
|
-
declare function assertBranchMetadata(metadata?: EditableBranchMetadata): void;
|
|
57
72
|
|
|
58
|
-
|
|
73
|
+
/**
|
|
74
|
+
* @deprecated Use OTBranchManager instead. This alias will be removed in a future version.
|
|
75
|
+
*/
|
|
76
|
+
declare const PatchesBranchManager: typeof OTBranchManager;
|
|
77
|
+
|
|
78
|
+
export { OTBranchManager, PatchesBranchManager };
|