@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.
Files changed (133) hide show
  1. package/README.md +221 -208
  2. package/dist/BaseDoc-DkP3tUhT.d.ts +206 -0
  3. package/dist/algorithms/lww/consolidateOps.d.ts +40 -0
  4. package/dist/algorithms/lww/consolidateOps.js +103 -0
  5. package/dist/algorithms/lww/mergeServerWithLocal.d.ts +22 -0
  6. package/dist/algorithms/lww/mergeServerWithLocal.js +32 -0
  7. package/dist/algorithms/{client → ot/client}/applyCommittedChanges.d.ts +10 -3
  8. package/dist/algorithms/{client → ot/client}/applyCommittedChanges.js +7 -4
  9. package/dist/algorithms/{client → ot/client}/createStateFromSnapshot.d.ts +3 -3
  10. package/dist/algorithms/{client → ot/client}/createStateFromSnapshot.js +1 -1
  11. package/dist/algorithms/ot/server/commitChanges.d.ts +43 -0
  12. package/dist/algorithms/{server → ot/server}/commitChanges.js +22 -7
  13. package/dist/algorithms/{server → ot/server}/createVersion.d.ts +5 -5
  14. package/dist/algorithms/{server → ot/server}/createVersion.js +2 -2
  15. package/dist/algorithms/{server → ot/server}/getSnapshotAtRevision.d.ts +5 -5
  16. package/dist/algorithms/{server → ot/server}/getSnapshotAtRevision.js +1 -1
  17. package/dist/algorithms/{server → ot/server}/getStateAtRevision.d.ts +5 -5
  18. package/dist/algorithms/{server → ot/server}/getStateAtRevision.js +1 -1
  19. package/dist/algorithms/{server → ot/server}/handleOfflineSessionsAndBatches.d.ts +5 -5
  20. package/dist/algorithms/{server → ot/server}/handleOfflineSessionsAndBatches.js +3 -3
  21. package/dist/algorithms/{server → ot/server}/transformIncomingChanges.d.ts +3 -3
  22. package/dist/algorithms/{server → ot/server}/transformIncomingChanges.js +3 -3
  23. package/dist/algorithms/{shared → ot/shared}/applyChanges.d.ts +3 -3
  24. package/dist/algorithms/{shared → ot/shared}/applyChanges.js +2 -2
  25. package/dist/algorithms/{shared → ot/shared}/changeBatching.d.ts +3 -3
  26. package/dist/algorithms/{shared → ot/shared}/changeBatching.js +2 -2
  27. package/dist/algorithms/{shared → ot/shared}/rebaseChanges.d.ts +3 -3
  28. package/dist/algorithms/{shared → ot/shared}/rebaseChanges.js +2 -2
  29. package/dist/client/BaseDoc.d.ts +6 -0
  30. package/dist/client/BaseDoc.js +70 -0
  31. package/dist/client/ClientAlgorithm.d.ts +101 -0
  32. package/dist/client/ClientAlgorithm.js +0 -0
  33. package/dist/client/InMemoryStore.d.ts +5 -7
  34. package/dist/client/InMemoryStore.js +7 -36
  35. package/dist/client/IndexedDBStore.d.ts +39 -73
  36. package/dist/client/IndexedDBStore.js +17 -220
  37. package/dist/client/LWWAlgorithm.d.ts +43 -0
  38. package/dist/client/LWWAlgorithm.js +87 -0
  39. package/dist/client/LWWClientStore.d.ts +73 -0
  40. package/dist/client/LWWClientStore.js +0 -0
  41. package/dist/client/LWWDoc.d.ts +56 -0
  42. package/dist/client/LWWDoc.js +84 -0
  43. package/dist/client/LWWInMemoryStore.d.ts +88 -0
  44. package/dist/client/LWWInMemoryStore.js +208 -0
  45. package/dist/client/LWWIndexedDBStore.d.ts +91 -0
  46. package/dist/client/LWWIndexedDBStore.js +275 -0
  47. package/dist/client/OTAlgorithm.d.ts +42 -0
  48. package/dist/client/OTAlgorithm.js +113 -0
  49. package/dist/client/OTClientStore.d.ts +50 -0
  50. package/dist/client/OTClientStore.js +0 -0
  51. package/dist/client/OTDoc.d.ts +6 -0
  52. package/dist/client/OTDoc.js +97 -0
  53. package/dist/client/OTIndexedDBStore.d.ts +84 -0
  54. package/dist/client/OTIndexedDBStore.js +163 -0
  55. package/dist/client/Patches.d.ts +36 -16
  56. package/dist/client/Patches.js +60 -27
  57. package/dist/client/PatchesDoc.d.ts +4 -113
  58. package/dist/client/PatchesDoc.js +3 -153
  59. package/dist/client/PatchesHistoryClient.js +1 -1
  60. package/dist/client/PatchesStore.d.ts +8 -105
  61. package/dist/client/factories.d.ts +72 -0
  62. package/dist/client/factories.js +80 -0
  63. package/dist/client/index.d.ts +14 -5
  64. package/dist/client/index.js +9 -0
  65. package/dist/compression/index.d.ts +2 -2
  66. package/dist/compression/index.js +1 -1
  67. package/dist/{algorithms/shared → compression}/lz.js +1 -1
  68. package/dist/data/change.js +2 -0
  69. package/dist/fractionalIndex.d.ts +67 -0
  70. package/dist/fractionalIndex.js +241 -0
  71. package/dist/index.d.ts +13 -3
  72. package/dist/index.js +1 -0
  73. package/dist/json-patch/types.d.ts +2 -0
  74. package/dist/net/PatchesClient.js +15 -15
  75. package/dist/net/PatchesSync.d.ts +20 -8
  76. package/dist/net/PatchesSync.js +57 -65
  77. package/dist/net/index.d.ts +7 -11
  78. package/dist/net/index.js +6 -1
  79. package/dist/net/protocol/JSONRPCClient.d.ts +4 -4
  80. package/dist/net/protocol/JSONRPCClient.js +6 -4
  81. package/dist/net/protocol/JSONRPCServer.d.ts +45 -9
  82. package/dist/net/protocol/JSONRPCServer.js +63 -8
  83. package/dist/net/serverContext.d.ts +38 -0
  84. package/dist/net/serverContext.js +20 -0
  85. package/dist/net/webrtc/WebRTCTransport.js +1 -1
  86. package/dist/net/websocket/AuthorizationProvider.d.ts +3 -3
  87. package/dist/net/websocket/WebSocketServer.d.ts +29 -20
  88. package/dist/net/websocket/WebSocketServer.js +23 -12
  89. package/dist/server/BranchManager.d.ts +50 -0
  90. package/dist/server/BranchManager.js +0 -0
  91. package/dist/server/CompressedStoreBackend.d.ts +10 -8
  92. package/dist/server/CompressedStoreBackend.js +7 -13
  93. package/dist/server/LWWBranchManager.d.ts +82 -0
  94. package/dist/server/LWWBranchManager.js +99 -0
  95. package/dist/server/LWWMemoryStoreBackend.d.ts +78 -0
  96. package/dist/server/LWWMemoryStoreBackend.js +182 -0
  97. package/dist/server/LWWServer.d.ts +130 -0
  98. package/dist/server/LWWServer.js +214 -0
  99. package/dist/server/{PatchesBranchManager.d.ts → OTBranchManager.d.ts} +32 -12
  100. package/dist/server/{PatchesBranchManager.js → OTBranchManager.js} +27 -42
  101. package/dist/server/OTServer.d.ts +108 -0
  102. package/dist/server/OTServer.js +141 -0
  103. package/dist/server/PatchesHistoryManager.d.ts +21 -16
  104. package/dist/server/PatchesHistoryManager.js +23 -11
  105. package/dist/server/PatchesServer.d.ts +70 -81
  106. package/dist/server/PatchesServer.js +0 -175
  107. package/dist/server/branchUtils.d.ts +82 -0
  108. package/dist/server/branchUtils.js +66 -0
  109. package/dist/server/index.d.ts +18 -7
  110. package/dist/server/index.js +33 -4
  111. package/dist/server/tombstone.d.ts +29 -0
  112. package/dist/server/tombstone.js +32 -0
  113. package/dist/server/types.d.ts +109 -27
  114. package/dist/server/utils.d.ts +12 -0
  115. package/dist/server/utils.js +23 -0
  116. package/dist/solid/context.d.ts +4 -3
  117. package/dist/solid/doc-manager.d.ts +3 -3
  118. package/dist/solid/index.d.ts +4 -3
  119. package/dist/solid/primitives.d.ts +2 -3
  120. package/dist/types.d.ts +4 -2
  121. package/dist/vue/composables.d.ts +2 -3
  122. package/dist/vue/doc-manager.d.ts +3 -3
  123. package/dist/vue/index.d.ts +4 -3
  124. package/dist/vue/provider.d.ts +4 -3
  125. package/package.json +1 -1
  126. package/dist/algorithms/client/collapsePendingChanges.d.ts +0 -30
  127. package/dist/algorithms/client/collapsePendingChanges.js +0 -78
  128. package/dist/algorithms/client/makeChange.d.ts +0 -9
  129. package/dist/algorithms/client/makeChange.js +0 -29
  130. package/dist/algorithms/server/commitChanges.d.ts +0 -19
  131. package/dist/net/websocket/RPCServer.d.ts +0 -141
  132. package/dist/net/websocket/RPCServer.js +0 -204
  133. /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", { to: peerId, data });
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 { PatchesStoreBackend } from '../../server/types.js';
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: PatchesStoreBackend, docId: string): Promise<void>;
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
- * High-level client for the Patches real-time collaboration service.
19
- * This class provides document subscription, patch notification handling,
20
- * versioning, and other OT-specific functionality over a WebSocket connection.
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 client instance.
32
+ * Creates a new Patches WebSocket server instance.
27
33
  *
28
- * @param transport - The transport layer implementation that will be used for sending/receiving messages
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
- constructor(transport: ServerTransport, rpcServer: RPCServer);
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
- }, ctx?: AuthContext): Promise<string[]>;
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
- }, ctx?: AuthContext): Promise<string[]>;
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 client instance.
10
+ * Creates a new Patches WebSocket server instance.
9
11
  *
10
- * @param transport - The transport layer implementation that will be used for sending/receiving messages
12
+ * @param options - Configuration options including transport, rpc, and optional auth
11
13
  */
12
- constructor(transport, rpcServer) {
14
+ constructor({ transport, rpc, auth = denyAll }) {
13
15
  this.transport = transport;
14
- const { rpc, auth } = rpcServer;
15
- this.auth = auth || denyAll;
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
- this.transport.send(clientId, msgString);
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, ctx) {
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, ctx) {
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?.clientId, Array.isArray(ids) ? ids : [ids]);
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 { PatchesStoreBackend } from './types.js';
4
- import '../algorithms/shared/lz.js';
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
- * Wraps a PatchesStoreBackend to transparently compress/decompress the ops field of Changes.
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 PatchesStoreBackend {
23
+ declare class CompressedStoreBackend implements OTStoreBackend, Partial<TombstoneStoreBackend> {
20
24
  private readonly store;
21
25
  private readonly compressor;
22
- constructor(store: PatchesStoreBackend, compressor: OpsCompressor);
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: Change[]): Promise<void>;
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.map((c) => this.compressChange(c));
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.map((s) => this.decompressChange(s));
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 };