@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,38 @@
|
|
|
1
|
+
import { AuthContext } from './websocket/AuthorizationProvider.js';
|
|
2
|
+
import '../server/types.js';
|
|
3
|
+
import '../json-patch/types.js';
|
|
4
|
+
import '../types.js';
|
|
5
|
+
import '../json-patch/JSONPatch.js';
|
|
6
|
+
import '@dabble/delta';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Get the current auth context for the active request.
|
|
10
|
+
* Must be called synchronously at the start of a handler, before any await.
|
|
11
|
+
*
|
|
12
|
+
* @returns The auth context, or undefined if not in a request context
|
|
13
|
+
*/
|
|
14
|
+
declare function getAuthContext(): AuthContext | undefined;
|
|
15
|
+
/**
|
|
16
|
+
* Set the auth context for the current request.
|
|
17
|
+
* Called by the RPC server before invoking a handler.
|
|
18
|
+
*
|
|
19
|
+
* @param ctx - The auth context to set
|
|
20
|
+
* @internal
|
|
21
|
+
*/
|
|
22
|
+
declare function setAuthContext(ctx: AuthContext | undefined): void;
|
|
23
|
+
/**
|
|
24
|
+
* Clear the auth context after request handling completes.
|
|
25
|
+
* Called by the RPC server after a handler returns.
|
|
26
|
+
*
|
|
27
|
+
* @internal
|
|
28
|
+
*/
|
|
29
|
+
declare function clearAuthContext(): void;
|
|
30
|
+
/**
|
|
31
|
+
* Get the client ID from the current auth context.
|
|
32
|
+
* Convenience helper for the common case of needing just the client ID.
|
|
33
|
+
*
|
|
34
|
+
* @returns The client ID, or undefined if not in a request context
|
|
35
|
+
*/
|
|
36
|
+
declare function getClientId(): string | undefined;
|
|
37
|
+
|
|
38
|
+
export { clearAuthContext, getAuthContext, getClientId, setAuthContext };
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import "../chunk-IZ2YBCUP.js";
|
|
2
|
+
let _ctx;
|
|
3
|
+
function getAuthContext() {
|
|
4
|
+
return _ctx;
|
|
5
|
+
}
|
|
6
|
+
function setAuthContext(ctx) {
|
|
7
|
+
_ctx = ctx;
|
|
8
|
+
}
|
|
9
|
+
function clearAuthContext() {
|
|
10
|
+
_ctx = void 0;
|
|
11
|
+
}
|
|
12
|
+
function getClientId() {
|
|
13
|
+
return _ctx?.clientId;
|
|
14
|
+
}
|
|
15
|
+
export {
|
|
16
|
+
clearAuthContext,
|
|
17
|
+
getAuthContext,
|
|
18
|
+
getClientId,
|
|
19
|
+
setAuthContext
|
|
20
|
+
};
|
|
@@ -112,7 +112,7 @@ class WebRTCTransport {
|
|
|
112
112
|
_connectToPeer(peerId, initiator) {
|
|
113
113
|
const peer = new Peer({ initiator, trickle: false });
|
|
114
114
|
peer.on("signal", (data) => {
|
|
115
|
-
this.rpc.call("peer-signal",
|
|
115
|
+
this.rpc.call("peer-signal", peerId, data);
|
|
116
116
|
});
|
|
117
117
|
peer.on("connect", () => {
|
|
118
118
|
this.peers.set(peerId, { id: peerId, peer, connected: true });
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { ServerStoreBackend, TombstoneStoreBackend } from '../../server/types.js';
|
|
2
|
+
import '../../json-patch/types.js';
|
|
2
3
|
import '../../types.js';
|
|
3
4
|
import '../../json-patch/JSONPatch.js';
|
|
4
5
|
import '@dabble/delta';
|
|
5
|
-
import '../../json-patch/types.js';
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
8
|
* Access level requested for an operation.
|
|
@@ -75,6 +75,6 @@ declare const denyAll: AuthorizationProvider;
|
|
|
75
75
|
* }
|
|
76
76
|
* };
|
|
77
77
|
*/
|
|
78
|
-
declare function assertNotDeleted(store:
|
|
78
|
+
declare function assertNotDeleted(store: ServerStoreBackend & Partial<TombstoneStoreBackend>, docId: string): Promise<void>;
|
|
79
79
|
|
|
80
80
|
export { type Access, type AuthContext, type AuthorizationProvider, allowAll, assertNotDeleted, denyAll };
|
|
@@ -1,52 +1,61 @@
|
|
|
1
|
+
import { JSONRPCServer } from '../protocol/JSONRPCServer.js';
|
|
1
2
|
import { ServerTransport } from '../protocol/types.js';
|
|
2
3
|
import { AuthorizationProvider, AuthContext } from './AuthorizationProvider.js';
|
|
3
|
-
import { RPCServer } from './RPCServer.js';
|
|
4
4
|
import '../../event-signal.js';
|
|
5
|
+
import '../../server/types.js';
|
|
6
|
+
import '../../json-patch/types.js';
|
|
5
7
|
import '../../types.js';
|
|
6
8
|
import '../../json-patch/JSONPatch.js';
|
|
7
9
|
import '@dabble/delta';
|
|
8
|
-
import '../../json-patch/types.js';
|
|
9
|
-
import '../../server/types.js';
|
|
10
|
-
import '../../server/PatchesBranchManager.js';
|
|
11
|
-
import '../../server/PatchesServer.js';
|
|
12
|
-
import '../../compression/index.js';
|
|
13
|
-
import '../../algorithms/shared/lz.js';
|
|
14
|
-
import '../../server/PatchesHistoryManager.js';
|
|
15
|
-
import '../protocol/JSONRPCServer.js';
|
|
16
10
|
|
|
17
11
|
/**
|
|
18
|
-
*
|
|
19
|
-
|
|
20
|
-
|
|
12
|
+
* Options for creating a WebSocketServer instance.
|
|
13
|
+
*/
|
|
14
|
+
interface WebSocketServerOptions {
|
|
15
|
+
/** The transport layer for WebSocket connections */
|
|
16
|
+
transport: ServerTransport;
|
|
17
|
+
/** The JSON-RPC server for handling RPC calls */
|
|
18
|
+
rpc: JSONRPCServer;
|
|
19
|
+
/** Authorization provider for subscription access control */
|
|
20
|
+
auth?: AuthorizationProvider;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* High-level WebSocket server for the Patches real-time collaboration service.
|
|
24
|
+
* This class provides document subscription and notification routing over a WebSocket connection.
|
|
25
|
+
* It works with a JSONRPCServer instance that handles the RPC protocol and has servers registered.
|
|
21
26
|
*/
|
|
22
27
|
declare class WebSocketServer {
|
|
28
|
+
readonly rpc: JSONRPCServer;
|
|
23
29
|
protected transport: ServerTransport;
|
|
24
30
|
protected auth: AuthorizationProvider;
|
|
25
31
|
/**
|
|
26
|
-
* Creates a new Patches WebSocket
|
|
32
|
+
* Creates a new Patches WebSocket server instance.
|
|
27
33
|
*
|
|
28
|
-
* @param
|
|
34
|
+
* @param options - Configuration options including transport, rpc, and optional auth
|
|
35
|
+
*/
|
|
36
|
+
constructor({ transport, rpc, auth }: WebSocketServerOptions);
|
|
37
|
+
/**
|
|
38
|
+
* Process an incoming message from a client.
|
|
39
|
+
* Delegates to the RPC server for handling.
|
|
29
40
|
*/
|
|
30
|
-
|
|
41
|
+
processMessage(raw: string, ctx?: AuthContext): Promise<string | undefined>;
|
|
31
42
|
/**
|
|
32
43
|
* Subscribes the client to one or more documents to receive real-time updates.
|
|
33
44
|
* If a document has been deleted (tombstone exists), sends immediate docDeleted notification.
|
|
34
|
-
* @param connectionId - The ID of the connection making the request
|
|
35
45
|
* @param params - The subscription parameters
|
|
36
46
|
* @param params.ids - Document ID or IDs to subscribe to
|
|
37
47
|
*/
|
|
38
48
|
subscribe(params: {
|
|
39
49
|
ids: string | string[];
|
|
40
|
-
}
|
|
50
|
+
}): Promise<string[]>;
|
|
41
51
|
/**
|
|
42
52
|
* Unsubscribes the client from one or more documents.
|
|
43
|
-
* @param connectionId - The ID of the connection making the request
|
|
44
53
|
* @param params - The unsubscription parameters
|
|
45
54
|
* @param params.ids - Document ID or IDs to unsubscribe from
|
|
46
55
|
*/
|
|
47
56
|
unsubscribe(params: {
|
|
48
57
|
ids: string | string[];
|
|
49
|
-
}
|
|
58
|
+
}): Promise<string[]>;
|
|
50
59
|
}
|
|
51
60
|
|
|
52
|
-
export { WebSocketServer };
|
|
61
|
+
export { WebSocketServer, type WebSocketServerOptions };
|
|
@@ -1,38 +1,49 @@
|
|
|
1
1
|
import "../../chunk-IZ2YBCUP.js";
|
|
2
2
|
import { ErrorCodes, StatusError } from "../error.js";
|
|
3
|
+
import { getAuthContext } from "../serverContext.js";
|
|
3
4
|
import { denyAll } from "./AuthorizationProvider.js";
|
|
4
5
|
class WebSocketServer {
|
|
6
|
+
rpc;
|
|
5
7
|
transport;
|
|
6
8
|
auth;
|
|
7
9
|
/**
|
|
8
|
-
* Creates a new Patches WebSocket
|
|
10
|
+
* Creates a new Patches WebSocket server instance.
|
|
9
11
|
*
|
|
10
|
-
* @param
|
|
12
|
+
* @param options - Configuration options including transport, rpc, and optional auth
|
|
11
13
|
*/
|
|
12
|
-
constructor(transport,
|
|
14
|
+
constructor({ transport, rpc, auth = denyAll }) {
|
|
13
15
|
this.transport = transport;
|
|
14
|
-
|
|
15
|
-
this.auth = auth
|
|
16
|
+
this.rpc = rpc;
|
|
17
|
+
this.auth = auth;
|
|
16
18
|
rpc.registerMethod("subscribe", this.subscribe.bind(this));
|
|
17
19
|
rpc.registerMethod("unsubscribe", this.unsubscribe.bind(this));
|
|
18
|
-
rpc.onNotify(async (msg) => {
|
|
20
|
+
rpc.onNotify(async (msg, exceptConnectionId) => {
|
|
19
21
|
if (!msg.params?.docId) return;
|
|
20
22
|
const { docId } = msg.params;
|
|
21
23
|
const msgString = JSON.stringify(msg);
|
|
22
24
|
const clientIds = await this.transport.listSubscriptions(docId);
|
|
23
25
|
clientIds.forEach((clientId) => {
|
|
24
|
-
|
|
26
|
+
if (clientId !== exceptConnectionId) {
|
|
27
|
+
this.transport.send(clientId, msgString);
|
|
28
|
+
}
|
|
25
29
|
});
|
|
26
30
|
});
|
|
27
31
|
}
|
|
32
|
+
/**
|
|
33
|
+
* Process an incoming message from a client.
|
|
34
|
+
* Delegates to the RPC server for handling.
|
|
35
|
+
*/
|
|
36
|
+
async processMessage(raw, ctx) {
|
|
37
|
+
return this.rpc.processMessage(raw, ctx);
|
|
38
|
+
}
|
|
28
39
|
/**
|
|
29
40
|
* Subscribes the client to one or more documents to receive real-time updates.
|
|
30
41
|
* If a document has been deleted (tombstone exists), sends immediate docDeleted notification.
|
|
31
|
-
* @param connectionId - The ID of the connection making the request
|
|
32
42
|
* @param params - The subscription parameters
|
|
33
43
|
* @param params.ids - Document ID or IDs to subscribe to
|
|
34
44
|
*/
|
|
35
|
-
async subscribe(params
|
|
45
|
+
async subscribe(params) {
|
|
46
|
+
const ctx = getAuthContext();
|
|
36
47
|
if (!ctx?.clientId) return [];
|
|
37
48
|
const { ids } = params;
|
|
38
49
|
const allIds = Array.isArray(ids) ? ids : [ids];
|
|
@@ -64,14 +75,14 @@ class WebSocketServer {
|
|
|
64
75
|
}
|
|
65
76
|
/**
|
|
66
77
|
* Unsubscribes the client from one or more documents.
|
|
67
|
-
* @param connectionId - The ID of the connection making the request
|
|
68
78
|
* @param params - The unsubscription parameters
|
|
69
79
|
* @param params.ids - Document ID or IDs to unsubscribe from
|
|
70
80
|
*/
|
|
71
|
-
async unsubscribe(params
|
|
81
|
+
async unsubscribe(params) {
|
|
82
|
+
const ctx = getAuthContext();
|
|
72
83
|
if (!ctx?.clientId) return [];
|
|
73
84
|
const { ids } = params;
|
|
74
|
-
return this.transport.removeSubscription(ctx
|
|
85
|
+
return this.transport.removeSubscription(ctx.clientId, Array.isArray(ids) ? ids : [ids]);
|
|
75
86
|
}
|
|
76
87
|
}
|
|
77
88
|
export {
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { Branch, EditableBranchMetadata, BranchStatus, Change } from '../types.js';
|
|
2
|
+
import '../json-patch/JSONPatch.js';
|
|
3
|
+
import '@dabble/delta';
|
|
4
|
+
import '../json-patch/types.js';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Interface for managing document branches.
|
|
8
|
+
* Implementations handle algorithm-specific branching and merging logic.
|
|
9
|
+
*
|
|
10
|
+
* A branch is a document that originates from another document at a specific point.
|
|
11
|
+
* Its first version represents the source document's state at the branch point.
|
|
12
|
+
* Branches allow parallel development with the ability to merge changes back.
|
|
13
|
+
*/
|
|
14
|
+
interface BranchManager {
|
|
15
|
+
/**
|
|
16
|
+
* Lists all branches for a document.
|
|
17
|
+
* @param docId - The source document ID.
|
|
18
|
+
* @returns Array of branch metadata.
|
|
19
|
+
*/
|
|
20
|
+
listBranches(docId: string): Promise<Branch[]>;
|
|
21
|
+
/**
|
|
22
|
+
* Creates a new branch from a document.
|
|
23
|
+
* @param docId - The source document ID.
|
|
24
|
+
* @param atPoint - Algorithm-specific branching point (revision for OT, typically current rev for LWW).
|
|
25
|
+
* @param metadata - Optional branch metadata (name, custom fields).
|
|
26
|
+
* @returns The new branch document ID.
|
|
27
|
+
*/
|
|
28
|
+
createBranch(docId: string, atPoint: number, metadata?: EditableBranchMetadata): Promise<string>;
|
|
29
|
+
/**
|
|
30
|
+
* Updates branch metadata.
|
|
31
|
+
* @param branchId - The branch document ID.
|
|
32
|
+
* @param metadata - The metadata fields to update.
|
|
33
|
+
*/
|
|
34
|
+
updateBranch(branchId: string, metadata: EditableBranchMetadata): Promise<void>;
|
|
35
|
+
/**
|
|
36
|
+
* Closes a branch with the specified status.
|
|
37
|
+
* @param branchId - The branch document ID.
|
|
38
|
+
* @param status - The status to set (defaults to 'closed').
|
|
39
|
+
*/
|
|
40
|
+
closeBranch(branchId: string, status?: Exclude<BranchStatus, 'open'>): Promise<void>;
|
|
41
|
+
/**
|
|
42
|
+
* Merges a branch back into its source document.
|
|
43
|
+
* Algorithm-specific: OT uses fast-forward or flattened merge, LWW uses timestamp resolution.
|
|
44
|
+
* @param branchId - The branch document ID to merge.
|
|
45
|
+
* @returns The changes applied to the source document.
|
|
46
|
+
*/
|
|
47
|
+
mergeBranch(branchId: string): Promise<Change[]>;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export type { BranchManager };
|
|
File without changes
|
|
@@ -1,13 +1,17 @@
|
|
|
1
1
|
import { OpsCompressor } from '../compression/index.js';
|
|
2
2
|
import { Change, ListChangesOptions, VersionMetadata, EditableVersionMetadata, ListVersionsOptions, DocumentTombstone } from '../types.js';
|
|
3
|
-
import {
|
|
4
|
-
import '../
|
|
3
|
+
import { OTStoreBackend, TombstoneStoreBackend } from './types.js';
|
|
4
|
+
import '../compression/lz.js';
|
|
5
5
|
import '../json-patch/types.js';
|
|
6
6
|
import '../json-patch/JSONPatch.js';
|
|
7
7
|
import '@dabble/delta';
|
|
8
8
|
|
|
9
9
|
/**
|
|
10
|
-
*
|
|
10
|
+
* Store backend type that supports OT operations and optionally tombstones.
|
|
11
|
+
*/
|
|
12
|
+
type CompressibleStore = OTStoreBackend & Partial<TombstoneStoreBackend>;
|
|
13
|
+
/**
|
|
14
|
+
* Wraps an OTStoreBackend to transparently compress/decompress the ops field of Changes.
|
|
11
15
|
* Compression happens before save and decompression happens after load.
|
|
12
16
|
*
|
|
13
17
|
* This allows backends with row-size limits to store larger changes by compressing the payload.
|
|
@@ -16,10 +20,10 @@ import '@dabble/delta';
|
|
|
16
20
|
* import { base64Compressor } from '@dabble/patches/compression';
|
|
17
21
|
* const backend = new CompressedStoreBackend(myStore, base64Compressor);
|
|
18
22
|
*/
|
|
19
|
-
declare class CompressedStoreBackend implements
|
|
23
|
+
declare class CompressedStoreBackend implements OTStoreBackend, Partial<TombstoneStoreBackend> {
|
|
20
24
|
private readonly store;
|
|
21
25
|
private readonly compressor;
|
|
22
|
-
constructor(store:
|
|
26
|
+
constructor(store: CompressibleStore, compressor: OpsCompressor);
|
|
23
27
|
/**
|
|
24
28
|
* Compresses a single change's ops field.
|
|
25
29
|
*/
|
|
@@ -30,11 +34,9 @@ declare class CompressedStoreBackend implements PatchesStoreBackend {
|
|
|
30
34
|
private decompressChange;
|
|
31
35
|
saveChanges(docId: string, changes: Change[]): Promise<void>;
|
|
32
36
|
listChanges(docId: string, options: ListChangesOptions): Promise<Change[]>;
|
|
33
|
-
createVersion(docId: string, metadata: VersionMetadata, state: any, changes
|
|
37
|
+
createVersion(docId: string, metadata: VersionMetadata, state: any, changes?: Change[]): Promise<void>;
|
|
34
38
|
appendVersionChanges(docId: string, versionId: string, changes: Change[], newEndedAt: number, newRev: number, newState: any): Promise<void>;
|
|
35
39
|
loadVersionChanges(docId: string, versionId: string): Promise<Change[]>;
|
|
36
|
-
get loadLastVersionState(): PatchesStoreBackend['loadLastVersionState'];
|
|
37
|
-
get saveLastVersionState(): PatchesStoreBackend['saveLastVersionState'];
|
|
38
40
|
updateVersion(docId: string, versionId: string, metadata: EditableVersionMetadata): Promise<void>;
|
|
39
41
|
listVersions(docId: string, options: ListVersionsOptions): Promise<VersionMetadata[]>;
|
|
40
42
|
loadVersionState(docId: string, versionId: string): Promise<any | undefined>;
|
|
@@ -36,12 +36,12 @@ class CompressedStoreBackend {
|
|
|
36
36
|
}
|
|
37
37
|
// === Version Operations (compress changes and state ops) ===
|
|
38
38
|
async createVersion(docId, metadata, state, changes) {
|
|
39
|
-
const compressedChanges = changes
|
|
39
|
+
const compressedChanges = changes?.map((c) => this.compressChange(c));
|
|
40
40
|
return this.store.createVersion(docId, metadata, state, compressedChanges);
|
|
41
41
|
}
|
|
42
42
|
async appendVersionChanges(docId, versionId, changes, newEndedAt, newRev, newState) {
|
|
43
43
|
const compressedChanges = changes.map((c) => this.compressChange(c));
|
|
44
|
-
return this.store.appendVersionChanges(
|
|
44
|
+
return this.store.appendVersionChanges?.(
|
|
45
45
|
docId,
|
|
46
46
|
versionId,
|
|
47
47
|
compressedChanges,
|
|
@@ -51,16 +51,10 @@ class CompressedStoreBackend {
|
|
|
51
51
|
);
|
|
52
52
|
}
|
|
53
53
|
async loadVersionChanges(docId, versionId) {
|
|
54
|
-
const stored = await this.store.loadVersionChanges(docId, versionId);
|
|
55
|
-
return stored
|
|
54
|
+
const stored = await this.store.loadVersionChanges?.(docId, versionId);
|
|
55
|
+
return stored?.map((s) => this.decompressChange(s)) ?? [];
|
|
56
56
|
}
|
|
57
57
|
// === Pass-through Operations (no compression needed) ===
|
|
58
|
-
get loadLastVersionState() {
|
|
59
|
-
return this.store.loadLastVersionState?.bind(this.store);
|
|
60
|
-
}
|
|
61
|
-
get saveLastVersionState() {
|
|
62
|
-
return this.store.saveLastVersionState?.bind(this.store);
|
|
63
|
-
}
|
|
64
58
|
async updateVersion(docId, versionId, metadata) {
|
|
65
59
|
return this.store.updateVersion(docId, versionId, metadata);
|
|
66
60
|
}
|
|
@@ -74,13 +68,13 @@ class CompressedStoreBackend {
|
|
|
74
68
|
return this.store.deleteDoc(docId);
|
|
75
69
|
}
|
|
76
70
|
async createTombstone(tombstone) {
|
|
77
|
-
return this.store.createTombstone(tombstone);
|
|
71
|
+
return this.store.createTombstone?.(tombstone);
|
|
78
72
|
}
|
|
79
73
|
async getTombstone(docId) {
|
|
80
|
-
return this.store.getTombstone(docId);
|
|
74
|
+
return this.store.getTombstone?.(docId);
|
|
81
75
|
}
|
|
82
76
|
async removeTombstone(docId) {
|
|
83
|
-
return this.store.removeTombstone(docId);
|
|
77
|
+
return this.store.removeTombstone?.(docId);
|
|
84
78
|
}
|
|
85
79
|
}
|
|
86
80
|
export {
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { ApiDefinition } from '../net/protocol/JSONRPCServer.js';
|
|
2
|
+
import { Branch, EditableBranchMetadata, BranchStatus, Change } from '../types.js';
|
|
3
|
+
import { BranchManager } from './BranchManager.js';
|
|
4
|
+
import { LWWServer } from './LWWServer.js';
|
|
5
|
+
import { LWWStoreBackend, BranchingStoreBackend } from './types.js';
|
|
6
|
+
import '../event-signal.js';
|
|
7
|
+
import '../net/websocket/AuthorizationProvider.js';
|
|
8
|
+
import '../json-patch/types.js';
|
|
9
|
+
import '../json-patch/JSONPatch.js';
|
|
10
|
+
import '@dabble/delta';
|
|
11
|
+
import '../net/protocol/types.js';
|
|
12
|
+
import './PatchesServer.js';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Combined store type for LWW branch management.
|
|
16
|
+
* Requires LWW ops operations and branch metadata operations.
|
|
17
|
+
*/
|
|
18
|
+
type LWWBranchStore = LWWStoreBackend & BranchingStoreBackend;
|
|
19
|
+
/**
|
|
20
|
+
* LWW-specific branch manager implementation.
|
|
21
|
+
*
|
|
22
|
+
* Manages branches for documents using Last-Write-Wins semantics:
|
|
23
|
+
* - Creates branches from the current document state (copies ops with timestamps)
|
|
24
|
+
* - Merges by applying branch ops changes to source; timestamps resolve conflicts automatically
|
|
25
|
+
* - No transformation needed - LWW conflicts resolve deterministically by timestamp
|
|
26
|
+
*
|
|
27
|
+
* LWW branching is simpler than OT because:
|
|
28
|
+
* - Timestamps are immutable and survive branching/merging
|
|
29
|
+
* - Conflict resolution is deterministic (later timestamp wins)
|
|
30
|
+
* - Merging is idempotent - merge the same ops multiple times, get the same result
|
|
31
|
+
*/
|
|
32
|
+
declare class LWWBranchManager implements BranchManager {
|
|
33
|
+
private readonly store;
|
|
34
|
+
private readonly lwwServer;
|
|
35
|
+
static api: ApiDefinition;
|
|
36
|
+
constructor(store: LWWBranchStore, lwwServer: LWWServer);
|
|
37
|
+
/**
|
|
38
|
+
* Lists all branches for a document.
|
|
39
|
+
* @param docId - The source document ID.
|
|
40
|
+
* @returns Array of branch metadata.
|
|
41
|
+
*/
|
|
42
|
+
listBranches(docId: string): Promise<Branch[]>;
|
|
43
|
+
/**
|
|
44
|
+
* Creates a new branch from a document's current state.
|
|
45
|
+
*
|
|
46
|
+
* Note: Unlike OT, LWW cannot access historical states, so branches
|
|
47
|
+
* always start from the current state. The `atPoint` parameter is
|
|
48
|
+
* recorded for tracking purposes.
|
|
49
|
+
*
|
|
50
|
+
* @param docId - The source document ID.
|
|
51
|
+
* @param atPoint - The revision number (recorded for tracking).
|
|
52
|
+
* @param metadata - Optional branch metadata.
|
|
53
|
+
* @returns The new branch document ID.
|
|
54
|
+
*/
|
|
55
|
+
createBranch(docId: string, atPoint: number, metadata?: EditableBranchMetadata): Promise<string>;
|
|
56
|
+
/**
|
|
57
|
+
* Updates branch metadata.
|
|
58
|
+
* @param branchId - The branch document ID.
|
|
59
|
+
* @param metadata - The metadata ops to update.
|
|
60
|
+
*/
|
|
61
|
+
updateBranch(branchId: string, metadata: EditableBranchMetadata): Promise<void>;
|
|
62
|
+
/**
|
|
63
|
+
* Closes a branch with the specified status.
|
|
64
|
+
* @param branchId - The branch document ID.
|
|
65
|
+
* @param status - The status to set (defaults to 'closed').
|
|
66
|
+
*/
|
|
67
|
+
closeBranch(branchId: string, status?: Exclude<BranchStatus, 'open'>): Promise<void>;
|
|
68
|
+
/**
|
|
69
|
+
* Merges a branch back into its source document.
|
|
70
|
+
*
|
|
71
|
+
* LWW merge strategy:
|
|
72
|
+
* 1. Get all ops changes made on the branch since it was created
|
|
73
|
+
* 2. Apply those changes to the source document
|
|
74
|
+
* 3. Timestamps automatically resolve any conflicts (later wins)
|
|
75
|
+
*
|
|
76
|
+
* @param branchId - The branch document ID.
|
|
77
|
+
* @returns The changes applied to the source document.
|
|
78
|
+
*/
|
|
79
|
+
mergeBranch(branchId: string): Promise<Change[]>;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export { LWWBranchManager };
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import "../chunk-IZ2YBCUP.js";
|
|
2
|
+
import {
|
|
3
|
+
assertBranchMetadata,
|
|
4
|
+
assertBranchOpenForMerge,
|
|
5
|
+
assertNotABranch,
|
|
6
|
+
branchManagerApi,
|
|
7
|
+
createBranchRecord,
|
|
8
|
+
generateBranchId,
|
|
9
|
+
wrapMergeCommit
|
|
10
|
+
} from "./branchUtils.js";
|
|
11
|
+
class LWWBranchManager {
|
|
12
|
+
constructor(store, lwwServer) {
|
|
13
|
+
this.store = store;
|
|
14
|
+
this.lwwServer = lwwServer;
|
|
15
|
+
}
|
|
16
|
+
static api = branchManagerApi;
|
|
17
|
+
/**
|
|
18
|
+
* Lists all branches for a document.
|
|
19
|
+
* @param docId - The source document ID.
|
|
20
|
+
* @returns Array of branch metadata.
|
|
21
|
+
*/
|
|
22
|
+
async listBranches(docId) {
|
|
23
|
+
return await this.store.listBranches(docId);
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Creates a new branch from a document's current state.
|
|
27
|
+
*
|
|
28
|
+
* Note: Unlike OT, LWW cannot access historical states, so branches
|
|
29
|
+
* always start from the current state. The `atPoint` parameter is
|
|
30
|
+
* recorded for tracking purposes.
|
|
31
|
+
*
|
|
32
|
+
* @param docId - The source document ID.
|
|
33
|
+
* @param atPoint - The revision number (recorded for tracking).
|
|
34
|
+
* @param metadata - Optional branch metadata.
|
|
35
|
+
* @returns The new branch document ID.
|
|
36
|
+
*/
|
|
37
|
+
async createBranch(docId, atPoint, metadata) {
|
|
38
|
+
await assertNotABranch(this.store, docId);
|
|
39
|
+
const doc = await this.lwwServer.getDoc(docId);
|
|
40
|
+
const ops = await this.store.listOps(docId);
|
|
41
|
+
const branchDocId = await generateBranchId(this.store, docId);
|
|
42
|
+
await this.store.saveSnapshot(branchDocId, doc.state, doc.rev);
|
|
43
|
+
if (ops.length > 0) {
|
|
44
|
+
await this.store.saveOps(branchDocId, ops);
|
|
45
|
+
}
|
|
46
|
+
const branch = createBranchRecord(branchDocId, docId, atPoint, metadata);
|
|
47
|
+
await this.store.createBranch(branch);
|
|
48
|
+
return branchDocId;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Updates branch metadata.
|
|
52
|
+
* @param branchId - The branch document ID.
|
|
53
|
+
* @param metadata - The metadata ops to update.
|
|
54
|
+
*/
|
|
55
|
+
async updateBranch(branchId, metadata) {
|
|
56
|
+
assertBranchMetadata(metadata);
|
|
57
|
+
await this.store.updateBranch(branchId, metadata);
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Closes a branch with the specified status.
|
|
61
|
+
* @param branchId - The branch document ID.
|
|
62
|
+
* @param status - The status to set (defaults to 'closed').
|
|
63
|
+
*/
|
|
64
|
+
async closeBranch(branchId, status = "closed") {
|
|
65
|
+
await this.store.updateBranch(branchId, { status });
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Merges a branch back into its source document.
|
|
69
|
+
*
|
|
70
|
+
* LWW merge strategy:
|
|
71
|
+
* 1. Get all ops changes made on the branch since it was created
|
|
72
|
+
* 2. Apply those changes to the source document
|
|
73
|
+
* 3. Timestamps automatically resolve any conflicts (later wins)
|
|
74
|
+
*
|
|
75
|
+
* @param branchId - The branch document ID.
|
|
76
|
+
* @returns The changes applied to the source document.
|
|
77
|
+
*/
|
|
78
|
+
async mergeBranch(branchId) {
|
|
79
|
+
const branch = await this.store.loadBranch(branchId);
|
|
80
|
+
assertBranchOpenForMerge(branch, branchId);
|
|
81
|
+
const sourceDocId = branch.docId;
|
|
82
|
+
const branchChanges = await this.lwwServer.getChangesSince(branchId, branch.branchedAtRev);
|
|
83
|
+
if (branchChanges.length === 0) {
|
|
84
|
+
console.log(`Branch ${branchId} has no changes to merge.`);
|
|
85
|
+
await this.closeBranch(branchId, "merged");
|
|
86
|
+
return [];
|
|
87
|
+
}
|
|
88
|
+
const committedChanges = await wrapMergeCommit(
|
|
89
|
+
branchId,
|
|
90
|
+
sourceDocId,
|
|
91
|
+
() => this.lwwServer.commitChanges(sourceDocId, branchChanges)
|
|
92
|
+
);
|
|
93
|
+
await this.closeBranch(branchId, "merged");
|
|
94
|
+
return committedChanges;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
export {
|
|
98
|
+
LWWBranchManager
|
|
99
|
+
};
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { JSONPatchOp } from '../json-patch/types.js';
|
|
2
|
+
import { DocumentTombstone, VersionMetadata, Change, ListVersionsOptions, EditableVersionMetadata, Branch } from '../types.js';
|
|
3
|
+
import { LWWStoreBackend, VersioningStoreBackend, TombstoneStoreBackend, BranchingStoreBackend, ListFieldsOptions } from './types.js';
|
|
4
|
+
import '../json-patch/JSONPatch.js';
|
|
5
|
+
import '@dabble/delta';
|
|
6
|
+
|
|
7
|
+
interface DocData {
|
|
8
|
+
snapshot: {
|
|
9
|
+
state: any;
|
|
10
|
+
rev: number;
|
|
11
|
+
} | null;
|
|
12
|
+
ops: JSONPatchOp[];
|
|
13
|
+
rev: number;
|
|
14
|
+
}
|
|
15
|
+
interface VersionData {
|
|
16
|
+
metadata: VersionMetadata;
|
|
17
|
+
state: any;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* In-memory implementation of LWWStoreBackend for testing.
|
|
21
|
+
* Also implements TombstoneStoreBackend, VersioningStoreBackend, and BranchingStoreBackend
|
|
22
|
+
* for comprehensive testing of all LWW functionality.
|
|
23
|
+
*
|
|
24
|
+
* @example
|
|
25
|
+
* ```typescript
|
|
26
|
+
* import { LWWServer } from '@dabble/patches/server';
|
|
27
|
+
* import { LWWMemoryStoreBackend } from '@dabble/patches/server';
|
|
28
|
+
*
|
|
29
|
+
* const store = new LWWMemoryStoreBackend();
|
|
30
|
+
* const server = new LWWServer(store);
|
|
31
|
+
* ```
|
|
32
|
+
*/
|
|
33
|
+
declare class LWWMemoryStoreBackend implements LWWStoreBackend, VersioningStoreBackend, TombstoneStoreBackend, BranchingStoreBackend {
|
|
34
|
+
private docs;
|
|
35
|
+
private tombstones;
|
|
36
|
+
private versions;
|
|
37
|
+
private branches;
|
|
38
|
+
private getOrCreateDoc;
|
|
39
|
+
getCurrentRev(docId: string): Promise<number>;
|
|
40
|
+
getSnapshot(docId: string): Promise<{
|
|
41
|
+
state: any;
|
|
42
|
+
rev: number;
|
|
43
|
+
} | null>;
|
|
44
|
+
saveSnapshot(docId: string, state: any, rev: number): Promise<void>;
|
|
45
|
+
saveOps(docId: string, newOps: JSONPatchOp[], pathsToDelete?: string[]): Promise<number>;
|
|
46
|
+
listOps(docId: string, options?: ListFieldsOptions): Promise<JSONPatchOp[]>;
|
|
47
|
+
deleteDoc(docId: string): Promise<void>;
|
|
48
|
+
createTombstone(tombstone: DocumentTombstone): Promise<void>;
|
|
49
|
+
getTombstone(docId: string): Promise<DocumentTombstone | undefined>;
|
|
50
|
+
removeTombstone(docId: string): Promise<void>;
|
|
51
|
+
createVersion(docId: string, metadata: VersionMetadata, state: any, _changes?: Change[]): Promise<void>;
|
|
52
|
+
listVersions(docId: string, options?: ListVersionsOptions): Promise<VersionMetadata[]>;
|
|
53
|
+
loadVersionState(docId: string, versionId: string): Promise<any | undefined>;
|
|
54
|
+
updateVersion(docId: string, versionId: string, metadata: EditableVersionMetadata): Promise<void>;
|
|
55
|
+
listBranches(docId: string): Promise<Branch[]>;
|
|
56
|
+
loadBranch(branchId: string): Promise<Branch | null>;
|
|
57
|
+
createBranch(branch: Branch): Promise<void>;
|
|
58
|
+
updateBranch(branchId: string, updates: Partial<Pick<Branch, 'status' | 'name' | 'metadata'>>): Promise<void>;
|
|
59
|
+
closeBranch(branchId: string): Promise<void>;
|
|
60
|
+
/**
|
|
61
|
+
* Clears all data from the store. Useful for test cleanup.
|
|
62
|
+
*/
|
|
63
|
+
clear(): void;
|
|
64
|
+
/**
|
|
65
|
+
* Gets the raw document data for inspection in tests.
|
|
66
|
+
*/
|
|
67
|
+
getDocData(docId: string): DocData | undefined;
|
|
68
|
+
/**
|
|
69
|
+
* Gets the versions for a document for inspection in tests.
|
|
70
|
+
*/
|
|
71
|
+
getVersions(docId: string): VersionData[] | undefined;
|
|
72
|
+
/**
|
|
73
|
+
* Gets all branches for inspection in tests.
|
|
74
|
+
*/
|
|
75
|
+
getBranches(): Map<string, Branch>;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export { LWWMemoryStoreBackend };
|