@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.
- package/README.md +632 -0
- package/dist/client/PatchDoc.d.ts +85 -0
- package/dist/client/PatchDoc.js +299 -0
- package/dist/client/index.d.ts +2 -0
- package/dist/client/index.js +1 -0
- package/dist/event-signal.d.ts +31 -0
- package/dist/event-signal.js +40 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1 -0
- package/dist/json-patch/JSONPatch.d.ts +126 -0
- package/dist/json-patch/JSONPatch.js +221 -0
- package/dist/json-patch/applyPatch.d.ts +11 -0
- package/dist/json-patch/applyPatch.js +37 -0
- package/dist/json-patch/composePatch.d.ts +2 -0
- package/dist/json-patch/composePatch.js +38 -0
- package/dist/json-patch/createJSONPatch.d.ts +35 -0
- package/dist/json-patch/createJSONPatch.js +41 -0
- package/dist/json-patch/index.d.ts +9 -0
- package/dist/json-patch/index.js +8 -0
- package/dist/json-patch/invertPatch.d.ts +2 -0
- package/dist/json-patch/invertPatch.js +31 -0
- package/dist/json-patch/ops/add.d.ts +2 -0
- package/dist/json-patch/ops/add.js +52 -0
- package/dist/json-patch/ops/bitmask.d.ts +14 -0
- package/dist/json-patch/ops/bitmask.js +48 -0
- package/dist/json-patch/ops/copy.d.ts +2 -0
- package/dist/json-patch/ops/copy.js +34 -0
- package/dist/json-patch/ops/increment.d.ts +5 -0
- package/dist/json-patch/ops/increment.js +21 -0
- package/dist/json-patch/ops/index.d.ts +22 -0
- package/dist/json-patch/ops/index.js +25 -0
- package/dist/json-patch/ops/move.d.ts +2 -0
- package/dist/json-patch/ops/move.js +211 -0
- package/dist/json-patch/ops/remove.d.ts +2 -0
- package/dist/json-patch/ops/remove.js +31 -0
- package/dist/json-patch/ops/replace.d.ts +2 -0
- package/dist/json-patch/ops/replace.js +44 -0
- package/dist/json-patch/ops/test.d.ts +2 -0
- package/dist/json-patch/ops/test.js +22 -0
- package/dist/json-patch/ops/text.d.ts +2 -0
- package/dist/json-patch/ops/text.js +57 -0
- package/dist/json-patch/patchProxy.d.ts +41 -0
- package/dist/json-patch/patchProxy.js +125 -0
- package/dist/json-patch/state.d.ts +2 -0
- package/dist/json-patch/state.js +8 -0
- package/dist/json-patch/transformPatch.d.ts +19 -0
- package/dist/json-patch/transformPatch.js +37 -0
- package/dist/json-patch/types.d.ts +52 -0
- package/dist/json-patch/types.js +1 -0
- package/dist/json-patch/utils/deepEqual.d.ts +1 -0
- package/dist/json-patch/utils/deepEqual.js +33 -0
- package/dist/json-patch/utils/exit.d.ts +2 -0
- package/dist/json-patch/utils/exit.js +4 -0
- package/dist/json-patch/utils/get.d.ts +2 -0
- package/dist/json-patch/utils/get.js +6 -0
- package/dist/json-patch/utils/getOpData.d.ts +2 -0
- package/dist/json-patch/utils/getOpData.js +10 -0
- package/dist/json-patch/utils/getType.d.ts +3 -0
- package/dist/json-patch/utils/getType.js +6 -0
- package/dist/json-patch/utils/index.d.ts +14 -0
- package/dist/json-patch/utils/index.js +14 -0
- package/dist/json-patch/utils/log.d.ts +2 -0
- package/dist/json-patch/utils/log.js +7 -0
- package/dist/json-patch/utils/ops.d.ts +14 -0
- package/dist/json-patch/utils/ops.js +103 -0
- package/dist/json-patch/utils/paths.d.ts +9 -0
- package/dist/json-patch/utils/paths.js +53 -0
- package/dist/json-patch/utils/pluck.d.ts +5 -0
- package/dist/json-patch/utils/pluck.js +30 -0
- package/dist/json-patch/utils/shallowCopy.d.ts +1 -0
- package/dist/json-patch/utils/shallowCopy.js +20 -0
- package/dist/json-patch/utils/softWrites.d.ts +7 -0
- package/dist/json-patch/utils/softWrites.js +18 -0
- package/dist/json-patch/utils/toArrayIndex.d.ts +1 -0
- package/dist/json-patch/utils/toArrayIndex.js +12 -0
- package/dist/json-patch/utils/toKeys.d.ts +1 -0
- package/dist/json-patch/utils/toKeys.js +15 -0
- package/dist/json-patch/utils/updateArrayIndexes.d.ts +5 -0
- package/dist/json-patch/utils/updateArrayIndexes.js +38 -0
- package/dist/json-patch/utils/updateArrayPath.d.ts +5 -0
- package/dist/json-patch/utils/updateArrayPath.js +45 -0
- package/dist/net/AbstractTransport.d.ts +47 -0
- package/dist/net/AbstractTransport.js +37 -0
- package/dist/net/PatchesOfflineFirst.d.ts +3 -0
- package/dist/net/PatchesOfflineFirst.js +3 -0
- package/dist/net/PatchesRealtime.d.ts +90 -0
- package/dist/net/PatchesRealtime.js +257 -0
- package/dist/net/index.d.ts +9 -0
- package/dist/net/index.js +8 -0
- package/dist/net/protocol/JSONRPCClient.d.ts +55 -0
- package/dist/net/protocol/JSONRPCClient.js +106 -0
- package/dist/net/protocol/types.d.ts +142 -0
- package/dist/net/protocol/types.js +1 -0
- package/dist/net/webrtc/WebRTCAwareness.d.ts +81 -0
- package/dist/net/webrtc/WebRTCAwareness.js +119 -0
- package/dist/net/webrtc/WebRTCTransport.d.ts +80 -0
- package/dist/net/webrtc/WebRTCTransport.js +157 -0
- package/dist/net/websocket/PatchesWebSocket.d.ts +107 -0
- package/dist/net/websocket/PatchesWebSocket.js +144 -0
- package/dist/net/websocket/SignalingService.d.ts +91 -0
- package/dist/net/websocket/SignalingService.js +140 -0
- package/dist/net/websocket/WebSocketTransport.d.ts +47 -0
- package/dist/net/websocket/WebSocketTransport.js +138 -0
- package/dist/persist/IndexedDBStore.d.ts +72 -0
- package/dist/persist/IndexedDBStore.js +283 -0
- package/dist/persist/index.d.ts +2 -0
- package/dist/persist/index.js +1 -0
- package/dist/server/BranchManager.d.ts +40 -0
- package/dist/server/BranchManager.js +138 -0
- package/dist/server/HistoryManager.d.ts +63 -0
- package/dist/server/HistoryManager.js +92 -0
- package/dist/server/PatchServer.d.ts +129 -0
- package/dist/server/PatchServer.js +358 -0
- package/dist/server/index.d.ts +4 -0
- package/dist/server/index.js +3 -0
- package/dist/types.d.ts +158 -0
- package/dist/types.js +1 -0
- package/dist/utils.d.ts +36 -0
- package/dist/utils.js +83 -0
- 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
|
+
}
|