@dabble/patches 0.1.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 (120) hide show
  1. package/README.md +632 -0
  2. package/dist/client/PatchDoc.d.ts +85 -0
  3. package/dist/client/PatchDoc.js +299 -0
  4. package/dist/client/index.d.ts +2 -0
  5. package/dist/client/index.js +1 -0
  6. package/dist/event-signal.d.ts +31 -0
  7. package/dist/event-signal.js +40 -0
  8. package/dist/index.d.ts +2 -0
  9. package/dist/index.js +1 -0
  10. package/dist/json-patch/JSONPatch.d.ts +126 -0
  11. package/dist/json-patch/JSONPatch.js +221 -0
  12. package/dist/json-patch/applyPatch.d.ts +11 -0
  13. package/dist/json-patch/applyPatch.js +37 -0
  14. package/dist/json-patch/composePatch.d.ts +2 -0
  15. package/dist/json-patch/composePatch.js +38 -0
  16. package/dist/json-patch/createJSONPatch.d.ts +35 -0
  17. package/dist/json-patch/createJSONPatch.js +41 -0
  18. package/dist/json-patch/index.d.ts +9 -0
  19. package/dist/json-patch/index.js +8 -0
  20. package/dist/json-patch/invertPatch.d.ts +2 -0
  21. package/dist/json-patch/invertPatch.js +31 -0
  22. package/dist/json-patch/ops/add.d.ts +2 -0
  23. package/dist/json-patch/ops/add.js +52 -0
  24. package/dist/json-patch/ops/bitmask.d.ts +14 -0
  25. package/dist/json-patch/ops/bitmask.js +48 -0
  26. package/dist/json-patch/ops/copy.d.ts +2 -0
  27. package/dist/json-patch/ops/copy.js +34 -0
  28. package/dist/json-patch/ops/increment.d.ts +5 -0
  29. package/dist/json-patch/ops/increment.js +21 -0
  30. package/dist/json-patch/ops/index.d.ts +22 -0
  31. package/dist/json-patch/ops/index.js +25 -0
  32. package/dist/json-patch/ops/move.d.ts +2 -0
  33. package/dist/json-patch/ops/move.js +211 -0
  34. package/dist/json-patch/ops/remove.d.ts +2 -0
  35. package/dist/json-patch/ops/remove.js +31 -0
  36. package/dist/json-patch/ops/replace.d.ts +2 -0
  37. package/dist/json-patch/ops/replace.js +44 -0
  38. package/dist/json-patch/ops/test.d.ts +2 -0
  39. package/dist/json-patch/ops/test.js +22 -0
  40. package/dist/json-patch/ops/text.d.ts +2 -0
  41. package/dist/json-patch/ops/text.js +57 -0
  42. package/dist/json-patch/patchProxy.d.ts +41 -0
  43. package/dist/json-patch/patchProxy.js +125 -0
  44. package/dist/json-patch/state.d.ts +2 -0
  45. package/dist/json-patch/state.js +8 -0
  46. package/dist/json-patch/transformPatch.d.ts +19 -0
  47. package/dist/json-patch/transformPatch.js +37 -0
  48. package/dist/json-patch/types.d.ts +52 -0
  49. package/dist/json-patch/types.js +1 -0
  50. package/dist/json-patch/utils/deepEqual.d.ts +1 -0
  51. package/dist/json-patch/utils/deepEqual.js +33 -0
  52. package/dist/json-patch/utils/exit.d.ts +2 -0
  53. package/dist/json-patch/utils/exit.js +4 -0
  54. package/dist/json-patch/utils/get.d.ts +2 -0
  55. package/dist/json-patch/utils/get.js +6 -0
  56. package/dist/json-patch/utils/getOpData.d.ts +2 -0
  57. package/dist/json-patch/utils/getOpData.js +10 -0
  58. package/dist/json-patch/utils/getType.d.ts +3 -0
  59. package/dist/json-patch/utils/getType.js +6 -0
  60. package/dist/json-patch/utils/index.d.ts +14 -0
  61. package/dist/json-patch/utils/index.js +14 -0
  62. package/dist/json-patch/utils/log.d.ts +2 -0
  63. package/dist/json-patch/utils/log.js +7 -0
  64. package/dist/json-patch/utils/ops.d.ts +14 -0
  65. package/dist/json-patch/utils/ops.js +103 -0
  66. package/dist/json-patch/utils/paths.d.ts +9 -0
  67. package/dist/json-patch/utils/paths.js +53 -0
  68. package/dist/json-patch/utils/pluck.d.ts +5 -0
  69. package/dist/json-patch/utils/pluck.js +30 -0
  70. package/dist/json-patch/utils/shallowCopy.d.ts +1 -0
  71. package/dist/json-patch/utils/shallowCopy.js +20 -0
  72. package/dist/json-patch/utils/softWrites.d.ts +7 -0
  73. package/dist/json-patch/utils/softWrites.js +18 -0
  74. package/dist/json-patch/utils/toArrayIndex.d.ts +1 -0
  75. package/dist/json-patch/utils/toArrayIndex.js +12 -0
  76. package/dist/json-patch/utils/toKeys.d.ts +1 -0
  77. package/dist/json-patch/utils/toKeys.js +15 -0
  78. package/dist/json-patch/utils/updateArrayIndexes.d.ts +5 -0
  79. package/dist/json-patch/utils/updateArrayIndexes.js +38 -0
  80. package/dist/json-patch/utils/updateArrayPath.d.ts +5 -0
  81. package/dist/json-patch/utils/updateArrayPath.js +45 -0
  82. package/dist/net/AbstractTransport.d.ts +47 -0
  83. package/dist/net/AbstractTransport.js +37 -0
  84. package/dist/net/PatchesOfflineFirst.d.ts +3 -0
  85. package/dist/net/PatchesOfflineFirst.js +3 -0
  86. package/dist/net/PatchesRealtime.d.ts +90 -0
  87. package/dist/net/PatchesRealtime.js +257 -0
  88. package/dist/net/index.d.ts +9 -0
  89. package/dist/net/index.js +8 -0
  90. package/dist/net/protocol/JSONRPCClient.d.ts +55 -0
  91. package/dist/net/protocol/JSONRPCClient.js +106 -0
  92. package/dist/net/protocol/types.d.ts +142 -0
  93. package/dist/net/protocol/types.js +1 -0
  94. package/dist/net/webrtc/WebRTCAwareness.d.ts +81 -0
  95. package/dist/net/webrtc/WebRTCAwareness.js +119 -0
  96. package/dist/net/webrtc/WebRTCTransport.d.ts +80 -0
  97. package/dist/net/webrtc/WebRTCTransport.js +157 -0
  98. package/dist/net/websocket/PatchesWebSocket.d.ts +107 -0
  99. package/dist/net/websocket/PatchesWebSocket.js +144 -0
  100. package/dist/net/websocket/SignalingService.d.ts +91 -0
  101. package/dist/net/websocket/SignalingService.js +140 -0
  102. package/dist/net/websocket/WebSocketTransport.d.ts +47 -0
  103. package/dist/net/websocket/WebSocketTransport.js +138 -0
  104. package/dist/persist/IndexedDBStore.d.ts +72 -0
  105. package/dist/persist/IndexedDBStore.js +283 -0
  106. package/dist/persist/index.d.ts +2 -0
  107. package/dist/persist/index.js +1 -0
  108. package/dist/server/BranchManager.d.ts +40 -0
  109. package/dist/server/BranchManager.js +138 -0
  110. package/dist/server/HistoryManager.d.ts +63 -0
  111. package/dist/server/HistoryManager.js +92 -0
  112. package/dist/server/PatchServer.d.ts +129 -0
  113. package/dist/server/PatchServer.js +358 -0
  114. package/dist/server/index.d.ts +4 -0
  115. package/dist/server/index.js +3 -0
  116. package/dist/types.d.ts +158 -0
  117. package/dist/types.js +1 -0
  118. package/dist/utils.d.ts +36 -0
  119. package/dist/utils.js +83 -0
  120. package/package.json +78 -0
@@ -0,0 +1,144 @@
1
+ import { signal } from '../../event-signal.js';
2
+ import { JSONRPCClient } from '../protocol/JSONRPCClient.js';
3
+ import { WebSocketTransport } from './WebSocketTransport.js';
4
+ /**
5
+ * High-level client for the Patches real-time collaboration service.
6
+ * This class provides document subscription, patch notification handling,
7
+ * versioning, and other OT-specific functionality
8
+ * over a WebSocket connection.
9
+ */
10
+ export class PatchesWebSocket {
11
+ /**
12
+ * Creates a new Patches WebSocket client instance.
13
+ * @param url - The WebSocket server URL to connect to
14
+ * @param wsOptions - Optional configuration for the underlying WebSocket connection
15
+ */
16
+ constructor(url, wsOptions) {
17
+ /** Signal emitted when the server pushes document changes. */
18
+ this.onChangesCommitted = signal();
19
+ this.transport = new WebSocketTransport(url, wsOptions);
20
+ this.rpc = new JSONRPCClient(this.transport);
21
+ this.onStateChange = this.transport.onStateChange;
22
+ // Register handlers for server-sent notifications
23
+ // Note: Type assertions might be needed if rpc.on doesn't infer strongly enough
24
+ this.rpc.on('changesCommitted', (params /*: PatchesNotificationParams */) => {
25
+ this.onChangesCommitted.emit(params);
26
+ });
27
+ }
28
+ // --- Connection Management ---
29
+ /**
30
+ * Establishes a connection to the Patches server.
31
+ * @returns A promise that resolves when the connection is established
32
+ */
33
+ async connect() {
34
+ await this.transport.connect();
35
+ }
36
+ /**
37
+ * Terminates the connection to the Patches server.
38
+ */
39
+ disconnect() {
40
+ // Unsubscribe rpc listeners? JSONRPCClient should handle this if transport disconnects.
41
+ this.transport.disconnect();
42
+ // Consider clearing signal listeners here if needed, though they are instance-based.
43
+ }
44
+ // --- Patches API Methods ---
45
+ // === Subscription Operations ===
46
+ /**
47
+ * Subscribes the client to one or more documents to receive real-time updates.
48
+ * @param ids - Document ID or IDs to subscribe to.
49
+ * @returns A promise resolving with the list of successfully subscribed document IDs.
50
+ */
51
+ async subscribe(ids) {
52
+ return this.rpc.request('subscribe', { ids });
53
+ }
54
+ /**
55
+ * Unsubscribes the client from one or more documents.
56
+ * @param ids - Document ID or IDs to unsubscribe from.
57
+ * @returns A promise resolving when the unsubscription is confirmed.
58
+ */
59
+ async unsubscribe(ids) {
60
+ return this.rpc.request('unsubscribe', { ids });
61
+ }
62
+ // === Document Operations ===
63
+ /**
64
+ * Gets the latest state (content and revision) of a document.
65
+ * @param docId - The ID of the document.
66
+ * @returns A promise resolving with the document snapshot.
67
+ */
68
+ async getDoc(docId) {
69
+ return this.rpc.request('getDoc', { docId });
70
+ }
71
+ /**
72
+ * Gets changes that occurred for a document after a specific revision number.
73
+ * @param docId - The ID of the document.
74
+ * @param rev - The revision number after which to fetch changes.
75
+ * @returns A promise resolving with an array of changes.
76
+ */
77
+ async getChangesSince(docId, rev) {
78
+ return this.rpc.request('getChangesSince', { docId, rev });
79
+ }
80
+ /**
81
+ * Applies a set of client-generated changes to a document on the server.
82
+ * @param docId - The ID of the document.
83
+ * @param changes - An array of changes to apply.
84
+ * @returns A promise resolving with the changes as committed by the server (potentially transformed).
85
+ */
86
+ async commitChanges(docId, changes) {
87
+ return this.rpc.request('commitChanges', { docId, changes });
88
+ }
89
+ /**
90
+ * Deletes a document on the server.
91
+ * @param docId - The ID of the document to delete.
92
+ * @returns A promise resolving when the deletion is confirmed.
93
+ */
94
+ async deleteDoc(docId) {
95
+ return this.rpc.request('deleteDoc', { docId });
96
+ }
97
+ // === Version Operations ===
98
+ /**
99
+ * Creates a named version snapshot of a document's current state on the server.
100
+ * @param docId - The ID of the document.
101
+ * @param name - A descriptive name for the version.
102
+ * @returns A promise resolving with the unique ID of the newly created version.
103
+ */
104
+ async createVersion(docId, name) {
105
+ return this.rpc.request('createVersion', { docId, name });
106
+ }
107
+ /**
108
+ * Lists metadata for saved versions of a document.
109
+ * @param docId - The ID of the document.
110
+ * @param options - Options for filtering or pagination (e.g., limit, offset).
111
+ * @returns A promise resolving with an array of version metadata objects.
112
+ */
113
+ async listVersions(docId, options = {}) {
114
+ return this.rpc.request('listVersions', { docId, options });
115
+ }
116
+ /**
117
+ * Gets the document state snapshot corresponding to a specific version ID.
118
+ * @param docId - The ID of the document.
119
+ * @param versionId - The ID of the version to retrieve.
120
+ * @returns A promise resolving with the document snapshot for that version.
121
+ */
122
+ async getVersionState(docId, versionId) {
123
+ return this.rpc.request('getVersionState', { docId, versionId });
124
+ }
125
+ /**
126
+ * Gets the original changes associated with a specific version ID.
127
+ * @param docId - The ID of the document.
128
+ * @param versionId - The ID of the version.
129
+ * @returns A promise resolving with an array of changes that constitute that version.
130
+ */
131
+ async getVersionChanges(docId, versionId) {
132
+ return this.rpc.request('getVersionChanges', { docId, versionId });
133
+ }
134
+ /**
135
+ * Updates the name of a specific version.
136
+ * @param docId - The ID of the document.
137
+ * @param versionId - The ID of the version to update.
138
+ * @param name - The new name for the version.
139
+ * @returns A promise resolving when the update is confirmed.
140
+ */
141
+ async updateVersion(docId, versionId, name) {
142
+ return this.rpc.request('updateVersion', { docId, versionId, name });
143
+ }
144
+ }
@@ -0,0 +1,91 @@
1
+ /**
2
+ * Represents a JSON-RPC 2.0 request object.
3
+ */
4
+ export interface JsonRpcRequest {
5
+ /** JSON-RPC protocol version, always "2.0" */
6
+ jsonrpc: '2.0';
7
+ /** Name of the remote procedure to call */
8
+ method: string;
9
+ /** Parameters to pass to the remote procedure */
10
+ params?: any;
11
+ /** Request identifier, used to match responses to requests */
12
+ id?: number | string;
13
+ }
14
+ /**
15
+ * Represents a JSON-RPC 2.0 response object.
16
+ */
17
+ export interface JsonRpcResponse {
18
+ /** JSON-RPC protocol version, always "2.0" */
19
+ jsonrpc: '2.0';
20
+ /** Result of the successful procedure call */
21
+ result?: any;
22
+ /** Error information if the procedure call failed */
23
+ error?: {
24
+ code: number;
25
+ message: string;
26
+ };
27
+ /** Response identifier, matches the id of the corresponding request */
28
+ id: number | string;
29
+ }
30
+ /** Union type for all possible JSON-RPC message types */
31
+ export type JsonRpcMessage = JsonRpcRequest | JsonRpcResponse;
32
+ /** Function type for sending JSON-RPC messages */
33
+ export type SendFn = (message: JsonRpcMessage) => void;
34
+ /**
35
+ * Service that facilitates WebRTC connection establishment by relaying signaling messages.
36
+ * Acts as a central hub for WebRTC peers to exchange connection information.
37
+ */
38
+ export declare class SignalingService {
39
+ private clients;
40
+ /**
41
+ * Registers a new client connection with the signaling service.
42
+ * Assigns a unique ID to the client and informs them of other connected peers.
43
+ *
44
+ * @param send - Function to send messages to this client
45
+ * @param id - Optional client ID (generated if not provided)
46
+ * @returns The client's assigned ID
47
+ */
48
+ onClientConnected(send: SendFn, id?: string): string;
49
+ /**
50
+ * Handles a client disconnection by removing them from the registry
51
+ * and notifying all other connected clients.
52
+ *
53
+ * @param id - ID of the disconnected client
54
+ */
55
+ onClientDisconnected(id: string): void;
56
+ /**
57
+ * Handles a signaling message from a client, relaying WebRTC session data
58
+ * between peers to facilitate connection establishment.
59
+ *
60
+ * @param fromId - ID of the client sending the message
61
+ * @param message - The JSON-RPC message or its string representation
62
+ * @returns True if the message was a valid signaling message and was handled, false otherwise
63
+ */
64
+ handleClientMessage(fromId: string, message: string | JsonRpcRequest): boolean;
65
+ /**
66
+ * Sends a successful JSON-RPC response to a client.
67
+ *
68
+ * @private
69
+ * @param toId - ID of the client to send the response to
70
+ * @param id - Request ID to match in the response
71
+ * @param result - Result data to include in the response
72
+ */
73
+ private respond;
74
+ /**
75
+ * Sends an error JSON-RPC response to a client.
76
+ *
77
+ * @private
78
+ * @param toId - ID of the client to send the error response to
79
+ * @param id - Request ID to match in the response, or undefined for notifications
80
+ * @param message - Error message to include
81
+ */
82
+ private respondError;
83
+ /**
84
+ * Broadcasts a message to all connected clients, optionally excluding one.
85
+ *
86
+ * @private
87
+ * @param message - The message to broadcast
88
+ * @param excludeId - Optional ID of a client to exclude from the broadcast
89
+ */
90
+ private broadcast;
91
+ }
@@ -0,0 +1,140 @@
1
+ import { createId } from 'crypto-id';
2
+ /**
3
+ * Service that facilitates WebRTC connection establishment by relaying signaling messages.
4
+ * Acts as a central hub for WebRTC peers to exchange connection information.
5
+ */
6
+ export class SignalingService {
7
+ constructor() {
8
+ this.clients = new Map();
9
+ }
10
+ /**
11
+ * Registers a new client connection with the signaling service.
12
+ * Assigns a unique ID to the client and informs them of other connected peers.
13
+ *
14
+ * @param send - Function to send messages to this client
15
+ * @param id - Optional client ID (generated if not provided)
16
+ * @returns The client's assigned ID
17
+ */
18
+ onClientConnected(send, id = createId(14)) {
19
+ this.clients.set(id, { send });
20
+ const welcome = {
21
+ jsonrpc: '2.0',
22
+ method: 'peer-welcome',
23
+ params: {
24
+ id,
25
+ peers: Array.from(this.clients.keys()).filter(pid => pid !== id),
26
+ },
27
+ };
28
+ send(welcome);
29
+ return id;
30
+ }
31
+ /**
32
+ * Handles a client disconnection by removing them from the registry
33
+ * and notifying all other connected clients.
34
+ *
35
+ * @param id - ID of the disconnected client
36
+ */
37
+ onClientDisconnected(id) {
38
+ this.clients.delete(id);
39
+ // Broadcast to all others
40
+ this.broadcast({
41
+ jsonrpc: '2.0',
42
+ method: 'peer-disconnected',
43
+ params: { id },
44
+ });
45
+ }
46
+ /**
47
+ * Handles a signaling message from a client, relaying WebRTC session data
48
+ * between peers to facilitate connection establishment.
49
+ *
50
+ * @param fromId - ID of the client sending the message
51
+ * @param message - The JSON-RPC message or its string representation
52
+ * @returns True if the message was a valid signaling message and was handled, false otherwise
53
+ */
54
+ handleClientMessage(fromId, message) {
55
+ let parsed;
56
+ try {
57
+ parsed = typeof message === 'string' ? JSON.parse(message) : message;
58
+ }
59
+ catch (err) {
60
+ return false;
61
+ }
62
+ if (parsed.jsonrpc !== '2.0' || parsed.method !== 'peer-signal' || !parsed.params?.to)
63
+ return false;
64
+ const { params, id } = parsed;
65
+ const { to, data } = params;
66
+ const target = this.clients.get(to);
67
+ if (!target) {
68
+ this.respondError(fromId, id, 'Target not connected');
69
+ // Was a signaling message, even if the target is not connected
70
+ return true;
71
+ }
72
+ const outbound = {
73
+ jsonrpc: '2.0',
74
+ method: 'signal',
75
+ params: {
76
+ from: fromId,
77
+ data,
78
+ },
79
+ };
80
+ target.send(outbound);
81
+ if (id !== undefined) {
82
+ this.respond(fromId, id, 'ok');
83
+ }
84
+ return true;
85
+ }
86
+ /**
87
+ * Sends a successful JSON-RPC response to a client.
88
+ *
89
+ * @private
90
+ * @param toId - ID of the client to send the response to
91
+ * @param id - Request ID to match in the response
92
+ * @param result - Result data to include in the response
93
+ */
94
+ respond(toId, id, result) {
95
+ const client = this.clients.get(toId);
96
+ if (!client)
97
+ return;
98
+ const response = {
99
+ jsonrpc: '2.0',
100
+ result,
101
+ id,
102
+ };
103
+ client.send(response);
104
+ }
105
+ /**
106
+ * Sends an error JSON-RPC response to a client.
107
+ *
108
+ * @private
109
+ * @param toId - ID of the client to send the error response to
110
+ * @param id - Request ID to match in the response, or undefined for notifications
111
+ * @param message - Error message to include
112
+ */
113
+ respondError(toId, id, message) {
114
+ if (id === undefined)
115
+ return;
116
+ const client = this.clients.get(toId);
117
+ if (!client)
118
+ return;
119
+ const response = {
120
+ jsonrpc: '2.0',
121
+ error: { code: -32000, message },
122
+ id,
123
+ };
124
+ client.send(response);
125
+ }
126
+ /**
127
+ * Broadcasts a message to all connected clients, optionally excluding one.
128
+ *
129
+ * @private
130
+ * @param message - The message to broadcast
131
+ * @param excludeId - Optional ID of a client to exclude from the broadcast
132
+ */
133
+ broadcast(message, excludeId) {
134
+ for (const [id, client] of this.clients.entries()) {
135
+ if (id !== excludeId) {
136
+ client.send(message);
137
+ }
138
+ }
139
+ }
140
+ }
@@ -0,0 +1,47 @@
1
+ import { AbstractTransport } from '../AbstractTransport.js';
2
+ /** WebSocket constructor options (subset) */
3
+ export interface WebSocketOptions {
4
+ protocol?: string | string[];
5
+ }
6
+ /**
7
+ * WebSocket-based transport implementation that provides communication over the WebSocket protocol.
8
+ * Includes automatic reconnection with exponential backoff.
9
+ */
10
+ export declare class WebSocketTransport extends AbstractTransport {
11
+ private url;
12
+ private wsOptions?;
13
+ private ws;
14
+ private reconnectTimer;
15
+ private backoff;
16
+ private connecting;
17
+ private connectionPromise;
18
+ /**
19
+ * Creates a new WebSocket transport instance.
20
+ * @param url - The WebSocket server URL to connect to
21
+ * @param wsOptions - Optional configuration for the WebSocket connection
22
+ */
23
+ constructor(url: string, wsOptions?: WebSocketOptions | undefined);
24
+ /**
25
+ * Establishes a connection to the WebSocket server.
26
+ * If a connection is already open or in progress, this method returns immediately.
27
+ * On connection failure, an automatic reconnection attempt will be scheduled.
28
+ * @returns A promise that resolves when the connection is established or rejects on error
29
+ */
30
+ connect(): Promise<void>;
31
+ /**
32
+ * Terminates the WebSocket connection and cancels any pending reconnection attempts.
33
+ */
34
+ disconnect(): void;
35
+ /**
36
+ * Sends data through the WebSocket connection.
37
+ * @param data - The string data to send
38
+ * @throws {Error} If the WebSocket is not connected
39
+ */
40
+ send(data: string): void;
41
+ /**
42
+ * Schedules a reconnection attempt using exponential backoff.
43
+ * The backoff time increases with each failed attempt, up to a maximum of 30 seconds.
44
+ * @private
45
+ */
46
+ private scheduleReconnect;
47
+ }
@@ -0,0 +1,138 @@
1
+ import { AbstractTransport } from '../AbstractTransport.js';
2
+ /**
3
+ * WebSocket-based transport implementation that provides communication over the WebSocket protocol.
4
+ * Includes automatic reconnection with exponential backoff.
5
+ */
6
+ export class WebSocketTransport extends AbstractTransport {
7
+ /**
8
+ * Creates a new WebSocket transport instance.
9
+ * @param url - The WebSocket server URL to connect to
10
+ * @param wsOptions - Optional configuration for the WebSocket connection
11
+ */
12
+ constructor(url, wsOptions) {
13
+ super();
14
+ this.url = url;
15
+ this.wsOptions = wsOptions;
16
+ this.ws = null;
17
+ this.reconnectTimer = null;
18
+ this.backoff = 1000;
19
+ this.connecting = false;
20
+ this.connectionPromise = null;
21
+ }
22
+ /**
23
+ * Establishes a connection to the WebSocket server.
24
+ * If a connection is already open or in progress, this method returns immediately.
25
+ * On connection failure, an automatic reconnection attempt will be scheduled.
26
+ * @returns A promise that resolves when the connection is established or rejects on error
27
+ */
28
+ async connect() {
29
+ // Return existing connection if already connected
30
+ if (this.ws && this.ws.readyState === WebSocket.OPEN) {
31
+ return Promise.resolve();
32
+ }
33
+ // Return pending connection promise if already connecting
34
+ if (this.connecting && this.connectionPromise) {
35
+ return this.connectionPromise;
36
+ }
37
+ this.connecting = true;
38
+ this.state = 'connecting';
39
+ // Create a new connection promise
40
+ this.connectionPromise = new Promise((resolve, reject) => {
41
+ try {
42
+ // Pass protocol option if available (standard 2nd arg)
43
+ // Other options like headers are not standard and require specific server/client handling
44
+ // or a different WebSocket client library.
45
+ this.ws = new WebSocket(this.url, this.wsOptions?.protocol);
46
+ this.ws.onopen = () => {
47
+ this.backoff = 1000; // Reset backoff on successful connection
48
+ this.state = 'connected';
49
+ this.connecting = false;
50
+ resolve();
51
+ };
52
+ this.ws.onclose = () => {
53
+ this.state = 'disconnected';
54
+ // If we were in the process of connecting, reject the promise
55
+ if (this.connecting) {
56
+ reject(new Error('Connection closed'));
57
+ this.connecting = false;
58
+ }
59
+ // Schedule reconnect regardless of whether it was a clean close
60
+ // as WebSockets don't always emit error events before closing
61
+ this.scheduleReconnect();
62
+ };
63
+ this.ws.onerror = error => {
64
+ this.state = 'error';
65
+ // If we're in the connection phase, reject the promise
66
+ if (this.connecting) {
67
+ this.connecting = false;
68
+ reject(error);
69
+ }
70
+ else {
71
+ // If error happens after established connection,
72
+ // schedule a reconnect. The socket will likely close
73
+ // right after this, but we schedule it anyway to be sure.
74
+ this.scheduleReconnect();
75
+ }
76
+ // Log the error for debugging
77
+ console.error('WebSocket error:', error);
78
+ };
79
+ this.ws.onmessage = event => {
80
+ this.onMessage.emit(event.data);
81
+ };
82
+ }
83
+ catch (error) {
84
+ this.state = 'error';
85
+ this.connecting = false;
86
+ reject(error);
87
+ this.scheduleReconnect();
88
+ }
89
+ });
90
+ return this.connectionPromise;
91
+ }
92
+ /**
93
+ * Terminates the WebSocket connection and cancels any pending reconnection attempts.
94
+ */
95
+ disconnect() {
96
+ if (this.reconnectTimer) {
97
+ clearTimeout(this.reconnectTimer);
98
+ this.reconnectTimer = null;
99
+ }
100
+ this.connecting = false;
101
+ if (this.ws) {
102
+ // Only attempt to close if not already closed
103
+ if (this.ws.readyState !== WebSocket.CLOSED && this.ws.readyState !== WebSocket.CLOSING) {
104
+ this.ws.close();
105
+ }
106
+ this.ws = null;
107
+ }
108
+ this.state = 'disconnected';
109
+ }
110
+ /**
111
+ * Sends data through the WebSocket connection.
112
+ * @param data - The string data to send
113
+ * @throws {Error} If the WebSocket is not connected
114
+ */
115
+ send(data) {
116
+ if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
117
+ throw new Error('WebSocket is not connected');
118
+ }
119
+ this.ws.send(data);
120
+ }
121
+ /**
122
+ * Schedules a reconnection attempt using exponential backoff.
123
+ * The backoff time increases with each failed attempt, up to a maximum of 30 seconds.
124
+ * @private
125
+ */
126
+ scheduleReconnect() {
127
+ if (this.reconnectTimer) {
128
+ return;
129
+ }
130
+ this.reconnectTimer = setTimeout(() => {
131
+ this.reconnectTimer = null;
132
+ this.connect().catch(err => {
133
+ console.error('WebSocket reconnect failed:', err);
134
+ });
135
+ }, this.backoff);
136
+ this.backoff = Math.min(this.backoff * 1.5, 30000);
137
+ }
138
+ }
@@ -0,0 +1,72 @@
1
+ import type { Change, PatchSnapshot } from '../types.js';
2
+ /**
3
+ * Creates a new IndexedDB database with stores:
4
+ * - snapshots<{ docId: string; rev: number; state: any }> (primary key: docId)
5
+ * - committedChanges<Change & { docId: string; }> (primary key: [docId, rev])
6
+ * - pendingChanges<Change & { docId: string; }> (primary key: [docId, rev])
7
+ * - deleted<{ docId: string; }> (primary key: docId)
8
+ *
9
+ * Under the hood, this class will store snapshots of the document only for committed state. It will not update the
10
+ * committed state on *every* received committed change as this can cause issues with IndexedDB with many large updates.
11
+ * After every 200 committed changes, the class will save the current state to the snapshot store and delete the committed changes that went into it.
12
+ * A snapshot will not be created if there are pending changes based on revisions older than the 200th committed change until those pending changes are committed.
13
+ */
14
+ export declare class IndexedDBStore {
15
+ private db;
16
+ private dbName;
17
+ private dbPromise;
18
+ /** Subscribe to be notified after local state changes are saved to the database. */
19
+ readonly onPendingChanges: import("../event-signal.js").Signal<(docId: string, changes: Change[]) => void>;
20
+ constructor(dbName: string);
21
+ private initDB;
22
+ private getDB;
23
+ /**
24
+ * Closes the database connection. After calling this method, the store
25
+ * will no longer be usable. A new instance must be created to reopen
26
+ * the database.
27
+ */
28
+ close(): Promise<void>;
29
+ private transaction;
30
+ /**
31
+ * Rebuilds a document snapshot + pending queue *without* loading
32
+ * the full PatchDoc into memory.
33
+ *
34
+ * 1. load the last snapshot (state + rev)
35
+ * 2. load committedChanges[rev > snapshot.rev]
36
+ * 3. load pendingChanges
37
+ * 4. apply committed changes, rebase pending
38
+ * 5. return { state, rev, changes: pending }
39
+ */
40
+ getDoc(docId: string): Promise<PatchSnapshot | undefined>;
41
+ /**
42
+ * Completely remove all data for this docId and mark it
43
+ * as deleted (tombstone). Provider will call `patchAPI.deleteDoc`
44
+ * on reconnect.
45
+ */
46
+ deleteDoc(docId: string): Promise<void>;
47
+ /**
48
+ * Append an array of local changes to the pending queue.
49
+ * Called *before* you attempt to send them to the server.
50
+ */
51
+ savePendingChanges(docId: string, changes: Change[]): Promise<void>;
52
+ /** Read back all pending changes for this docId (in order). */
53
+ getPendingChanges(docId: string): Promise<Change[]>;
54
+ /**
55
+ * Store server‐confirmed changes. Will:
56
+ * - persist them in the committedChanges store
57
+ * - remove any pending changes whose rev falls within `sentPendingRange`
58
+ * - optionally compact a new snapshot after N changes (hidden internally)
59
+ * @param docId - The ID of the document to save the changes for
60
+ * @param changes - The changes to save
61
+ * @param sentPendingRange - The range of pending changes to remove, *must* be provided after receiving the changes
62
+ * from the server in response to a patchDoc request.
63
+ */
64
+ saveCommittedChanges(docId: string, changes: Change[], sentPendingRange?: [number, number]): Promise<void>;
65
+ /**
66
+ * Tell me the last committed revision you have *and* the highest
67
+ * rev of any change. Use these to drive:
68
+ * - fetch changes: api.getChangesSince(docId, committedRev)
69
+ * - build new patch: newChange.rev = pendingRev; baseRev = committedRev
70
+ */
71
+ getLastRevs(docId: string): Promise<[number, number]>;
72
+ }