@dabble/patches 0.2.21 → 0.2.22
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/dist/net/protocol/JSONRPCServer.d.ts +20 -28
- package/dist/net/protocol/JSONRPCServer.js +44 -60
- package/dist/net/protocol/types.d.ts +6 -0
- package/dist/net/websocket/AuthorizationProvider.d.ts +12 -3
- package/dist/net/websocket/RPCServer.d.ts +120 -0
- package/dist/net/websocket/RPCServer.js +187 -0
- package/dist/net/websocket/WebSocketServer.d.ts +9 -117
- package/dist/net/websocket/WebSocketServer.js +30 -191
- package/dist/server/PatchesServer.d.ts +8 -16
- package/dist/server/PatchesServer.js +15 -24
- package/dist/server/types.d.ts +0 -4
- package/package.json +1 -1
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import { type Unsubscriber } from '../../event-signal.js';
|
|
2
|
-
import type {
|
|
3
|
-
|
|
4
|
-
export type
|
|
1
|
+
import { type Signal, type Unsubscriber } from '../../event-signal.js';
|
|
2
|
+
import type { AuthContext } from '../websocket/AuthorizationProvider.js';
|
|
3
|
+
import type { Notification } from './types.js';
|
|
4
|
+
export type ConnectionSignalSubscriber = (params: any, clientId?: string) => any;
|
|
5
|
+
export type MessageHandler<P = any, R = any> = (params: P, ctx?: AuthContext) => Promise<R> | R;
|
|
5
6
|
/**
|
|
6
7
|
* Lightweight JSON-RPC 2.0 server adapter for {@link PatchesServer}.
|
|
7
8
|
*
|
|
@@ -19,12 +20,12 @@ export type MessageHandler<P = any, R = any> = (connectionId: string, params: P)
|
|
|
19
20
|
* to the host application.
|
|
20
21
|
*/
|
|
21
22
|
export declare class JSONRPCServer {
|
|
22
|
-
protected transport: ServerTransport;
|
|
23
23
|
/** Map of fully-qualified JSON-RPC method → handler function */
|
|
24
24
|
private readonly handlers;
|
|
25
25
|
/** Allow external callers to emit server-initiated notifications. */
|
|
26
26
|
private readonly notificationSignals;
|
|
27
|
-
|
|
27
|
+
/** Allow external callers to emit server-initiated notifications. */
|
|
28
|
+
readonly onNotify: Signal<(msg: Notification, exceptConnectionId?: string) => void>;
|
|
28
29
|
/**
|
|
29
30
|
* Registers a JSON-RPC method.
|
|
30
31
|
*
|
|
@@ -45,25 +46,20 @@ export declare class JSONRPCServer {
|
|
|
45
46
|
* Sends a JSON-RPC notification (no `id`, therefore no response expected) to
|
|
46
47
|
* the connected client.
|
|
47
48
|
*/
|
|
48
|
-
notify(
|
|
49
|
-
/**
|
|
50
|
-
* Handles incoming messages from the client.
|
|
51
|
-
* @param connectionId - The WebSocket transport object.
|
|
52
|
-
* @param raw - The raw message string.
|
|
53
|
-
*/
|
|
54
|
-
protected _onMessage(connectionId: string, raw: string): Promise<void>;
|
|
55
|
-
/**
|
|
56
|
-
* Handles incoming JSON-RPC requests from the client.
|
|
57
|
-
* @param connectionId - The WebSocket transport object.
|
|
58
|
-
* @param req - The JSON-RPC request object.
|
|
59
|
-
*/
|
|
60
|
-
protected _handleRequest(connectionId: string, req: Request): Promise<void>;
|
|
49
|
+
notify(method: string, params?: any, exceptConnectionId?: string): Promise<void>;
|
|
61
50
|
/**
|
|
62
|
-
*
|
|
63
|
-
*
|
|
64
|
-
*
|
|
51
|
+
* Synchronously processes a raw JSON-RPC frame from a client and returns the
|
|
52
|
+
* encoded response frame – or `undefined` when the message is a notification
|
|
53
|
+
* (no response expected).
|
|
54
|
+
*
|
|
55
|
+
* This helper makes the RPC engine usable for stateless transports such as
|
|
56
|
+
* HTTP: the host simply passes the request body and sends back the returned
|
|
57
|
+
* string (if any).
|
|
58
|
+
*
|
|
59
|
+
* WebSocket and other bidirectional transports delegate to the same logic
|
|
60
|
+
* internally; the returned string is forwarded over the socket.
|
|
65
61
|
*/
|
|
66
|
-
|
|
62
|
+
processMessage(raw: string, ctx?: AuthContext): Promise<string | undefined>;
|
|
67
63
|
/**
|
|
68
64
|
* Maps JSON-RPC method names to {@link PatchesServer} calls.
|
|
69
65
|
* @param connectionId - The WebSocket transport object.
|
|
@@ -71,9 +67,5 @@ export declare class JSONRPCServer {
|
|
|
71
67
|
* @param params - The JSON-RPC parameters.
|
|
72
68
|
* @returns The result of the {@link PatchesServer} call.
|
|
73
69
|
*/
|
|
74
|
-
protected _dispatch(
|
|
75
|
-
/**
|
|
76
|
-
* Sends a JSON-RPC error object back to the client.
|
|
77
|
-
*/
|
|
78
|
-
private _sendError;
|
|
70
|
+
protected _dispatch(method: string, params: any, ctx?: AuthContext): Promise<any>;
|
|
79
71
|
}
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { signal } from '../../event-signal.js';
|
|
2
|
-
import { PatchesServer } from '../../server/PatchesServer.js';
|
|
3
2
|
/**
|
|
4
3
|
* Lightweight JSON-RPC 2.0 server adapter for {@link PatchesServer}.
|
|
5
4
|
*
|
|
@@ -17,13 +16,13 @@ import { PatchesServer } from '../../server/PatchesServer.js';
|
|
|
17
16
|
* to the host application.
|
|
18
17
|
*/
|
|
19
18
|
export class JSONRPCServer {
|
|
20
|
-
constructor(
|
|
21
|
-
this.transport = transport;
|
|
19
|
+
constructor() {
|
|
22
20
|
/** Map of fully-qualified JSON-RPC method → handler function */
|
|
23
21
|
this.handlers = new Map();
|
|
24
22
|
/** Allow external callers to emit server-initiated notifications. */
|
|
25
23
|
this.notificationSignals = new Map();
|
|
26
|
-
|
|
24
|
+
/** Allow external callers to emit server-initiated notifications. */
|
|
25
|
+
this.onNotify = signal();
|
|
27
26
|
}
|
|
28
27
|
// -------------------------------------------------------------------------
|
|
29
28
|
// Registration API
|
|
@@ -63,63 +62,56 @@ export class JSONRPCServer {
|
|
|
63
62
|
* Sends a JSON-RPC notification (no `id`, therefore no response expected) to
|
|
64
63
|
* the connected client.
|
|
65
64
|
*/
|
|
66
|
-
notify(
|
|
65
|
+
async notify(method, params, exceptConnectionId) {
|
|
67
66
|
const msg = { jsonrpc: '2.0', method, params };
|
|
68
|
-
|
|
69
|
-
connectionIds.forEach(id => this.transport.send(id, msgStr));
|
|
67
|
+
this.onNotify.emit(msg, exceptConnectionId);
|
|
70
68
|
}
|
|
71
69
|
/**
|
|
72
|
-
*
|
|
73
|
-
*
|
|
74
|
-
*
|
|
70
|
+
* Synchronously processes a raw JSON-RPC frame from a client and returns the
|
|
71
|
+
* encoded response frame – or `undefined` when the message is a notification
|
|
72
|
+
* (no response expected).
|
|
73
|
+
*
|
|
74
|
+
* This helper makes the RPC engine usable for stateless transports such as
|
|
75
|
+
* HTTP: the host simply passes the request body and sends back the returned
|
|
76
|
+
* string (if any).
|
|
77
|
+
*
|
|
78
|
+
* WebSocket and other bidirectional transports delegate to the same logic
|
|
79
|
+
* internally; the returned string is forwarded over the socket.
|
|
75
80
|
*/
|
|
76
|
-
async
|
|
81
|
+
async processMessage(raw, ctx) {
|
|
77
82
|
let message;
|
|
83
|
+
// --- Parse & basic validation ------------------------------------------------
|
|
78
84
|
try {
|
|
79
85
|
message = JSON.parse(raw);
|
|
80
86
|
}
|
|
81
87
|
catch (err) {
|
|
82
|
-
|
|
83
|
-
|
|
88
|
+
return rpcError(-32700, 'Parse error', err);
|
|
89
|
+
}
|
|
90
|
+
// Ensure it looks like a JSON-RPC call (must have a method field)
|
|
91
|
+
if (!message || typeof message !== 'object' || !('method' in message)) {
|
|
92
|
+
const invalidId = message?.id ?? null;
|
|
93
|
+
return rpcError(-32600, 'Invalid Request', invalidId);
|
|
84
94
|
}
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
95
|
+
// --- Distinguish request vs. notification -----------------------------------
|
|
96
|
+
if ('id' in message && message.id !== undefined) {
|
|
97
|
+
// -> Request ----------------------------------------------------------------
|
|
98
|
+
try {
|
|
99
|
+
const result = await this._dispatch(message.method, message.params, ctx);
|
|
100
|
+
const response = { jsonrpc: '2.0', id: message.id, result };
|
|
101
|
+
return JSON.stringify(response);
|
|
89
102
|
}
|
|
90
|
-
|
|
91
|
-
|
|
103
|
+
catch (err) {
|
|
104
|
+
return rpcError(err?.code ?? -32000, err?.message ?? 'Server error', err?.stack);
|
|
92
105
|
}
|
|
93
106
|
}
|
|
94
107
|
else {
|
|
95
|
-
//
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
* @param req - The JSON-RPC request object.
|
|
103
|
-
*/
|
|
104
|
-
async _handleRequest(connectionId, req) {
|
|
105
|
-
try {
|
|
106
|
-
const result = await this._dispatch(connectionId, req.method, req.params);
|
|
107
|
-
const response = { jsonrpc: '2.0', id: req.id, result };
|
|
108
|
-
this.transport.send(connectionId, JSON.stringify(response));
|
|
109
|
-
}
|
|
110
|
-
catch (err) {
|
|
111
|
-
this._sendError(connectionId, req.id, -32000, err?.message ?? 'Server error', err?.stack);
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
/**
|
|
115
|
-
* Handles incoming JSON-RPC notifications from the client.
|
|
116
|
-
* @param connectionId - The WebSocket transport object.
|
|
117
|
-
* @param note - The JSON-RPC notification object.
|
|
118
|
-
*/
|
|
119
|
-
async _handleNotification(connectionId, note) {
|
|
120
|
-
const thisSignal = this.notificationSignals.get(note.method);
|
|
121
|
-
if (thisSignal) {
|
|
122
|
-
thisSignal.emit(connectionId, note.params);
|
|
108
|
+
// -> Notification -----------------------------------------------------------
|
|
109
|
+
// Forward the notification to any listeners and return nothing.
|
|
110
|
+
const thisSignal = this.notificationSignals.get(message.method);
|
|
111
|
+
if (thisSignal) {
|
|
112
|
+
thisSignal.emit(message.params, ctx?.clientId);
|
|
113
|
+
}
|
|
114
|
+
return undefined;
|
|
123
115
|
}
|
|
124
116
|
}
|
|
125
117
|
/**
|
|
@@ -129,7 +121,7 @@ export class JSONRPCServer {
|
|
|
129
121
|
* @param params - The JSON-RPC parameters.
|
|
130
122
|
* @returns The result of the {@link PatchesServer} call.
|
|
131
123
|
*/
|
|
132
|
-
async _dispatch(
|
|
124
|
+
async _dispatch(method, params, ctx) {
|
|
133
125
|
const handler = this.handlers.get(method);
|
|
134
126
|
if (!handler) {
|
|
135
127
|
throw new Error(`Unknown method '${method}'.`);
|
|
@@ -137,17 +129,9 @@ export class JSONRPCServer {
|
|
|
137
129
|
if (!params || typeof params !== 'object' || Array.isArray(params)) {
|
|
138
130
|
throw new Error(`Invalid parameters for method '${method}'.`);
|
|
139
131
|
}
|
|
140
|
-
return handler(
|
|
141
|
-
}
|
|
142
|
-
/**
|
|
143
|
-
* Sends a JSON-RPC error object back to the client.
|
|
144
|
-
*/
|
|
145
|
-
_sendError(connectionId, id, code, message, data) {
|
|
146
|
-
const errorObj = {
|
|
147
|
-
jsonrpc: '2.0',
|
|
148
|
-
id: id,
|
|
149
|
-
error: { code, message, data },
|
|
150
|
-
}; // type cast because TS cannot narrow when error present
|
|
151
|
-
this.transport.send(connectionId, JSON.stringify(errorObj));
|
|
132
|
+
return handler(params, ctx);
|
|
152
133
|
}
|
|
153
134
|
}
|
|
135
|
+
function rpcError(code, message, data) {
|
|
136
|
+
return JSON.stringify({ jsonrpc: '2.0', id: null, error: { code, message, data } });
|
|
137
|
+
}
|
|
@@ -39,6 +39,12 @@ export interface ServerTransport {
|
|
|
39
39
|
send(toConnectionId: string, raw: string): void | Promise<void>;
|
|
40
40
|
/** Subscribe to incoming raw frames from any client */
|
|
41
41
|
onMessage(cb: (fromConnectionId: string, raw: string) => void): Unsubscriber;
|
|
42
|
+
/** Lists all subscriptions for a document. */
|
|
43
|
+
listSubscriptions(docId: string): Promise<string[]>;
|
|
44
|
+
/** Adds a subscription for a client to one or more documents. */
|
|
45
|
+
addSubscription(clientId: string, docIds: string[]): Promise<string[]>;
|
|
46
|
+
/** Removes a subscription for a client from one or more documents. */
|
|
47
|
+
removeSubscription(clientId: string, docIds: string[]): Promise<string[]>;
|
|
42
48
|
}
|
|
43
49
|
/**
|
|
44
50
|
* Represents a JSON-RPC 2.0 request object.
|
|
@@ -5,13 +5,22 @@
|
|
|
5
5
|
* "write" – mutating operations (commitChanges, deleteDoc, …)
|
|
6
6
|
*/
|
|
7
7
|
export type Access = 'read' | 'write';
|
|
8
|
+
/**
|
|
9
|
+
* Context object for authorization providers.
|
|
10
|
+
* @property {string} clientId - The ID of the client making the request.
|
|
11
|
+
* @property {any} [data] - Additional data associated with the request.
|
|
12
|
+
*/
|
|
13
|
+
export interface AuthContext {
|
|
14
|
+
clientId?: string;
|
|
15
|
+
[k: string]: any;
|
|
16
|
+
}
|
|
8
17
|
/**
|
|
9
18
|
* Allows the host application to decide whether a given connection can perform
|
|
10
19
|
* a certain action on a document. Implementations are entirely application-
|
|
11
20
|
* specific – they may look at a JWT decoded during the WebSocket handshake,
|
|
12
21
|
* consult an ACL service, inspect the actual RPC method, etc.
|
|
13
22
|
*/
|
|
14
|
-
export interface AuthorizationProvider {
|
|
23
|
+
export interface AuthorizationProvider<T extends AuthContext = AuthContext> {
|
|
15
24
|
/**
|
|
16
25
|
* General-purpose hook executed for every JSON-RPC call that targets a
|
|
17
26
|
* document. Implementations are free to look only at the first three
|
|
@@ -22,13 +31,13 @@ export interface AuthorizationProvider {
|
|
|
22
31
|
* Returning `true` (or a resolved promise with `true`) permits the action.
|
|
23
32
|
* Returning `false` or throwing will cause the RPC to fail with an error.
|
|
24
33
|
*
|
|
25
|
-
* @param
|
|
34
|
+
* @param ctx Context object containing client ID and additional data
|
|
26
35
|
* @param docId Logical document to be accessed (branch IDs count)
|
|
27
36
|
* @param kind High-level access category – `'read' | 'write'`
|
|
28
37
|
* @param method JSON-RPC method name (e.g. `getDoc`, `branch.merge`)
|
|
29
38
|
* @param params The exact parameter object supplied by the client
|
|
30
39
|
*/
|
|
31
|
-
canAccess(
|
|
40
|
+
canAccess(ctx: T | undefined, docId: string, kind: Access, method: string, params?: Record<string, any>): boolean | Promise<boolean>;
|
|
32
41
|
}
|
|
33
42
|
/**
|
|
34
43
|
* A permissive provider that authorises every action. Used as the default so
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import type { PatchesBranchManager } from '../../server/PatchesBranchManager.js';
|
|
2
|
+
import type { PatchesHistoryManager } from '../../server/PatchesHistoryManager.js';
|
|
3
|
+
import type { PatchesServer } from '../../server/PatchesServer.js';
|
|
4
|
+
import type { Change, EditableVersionMetadata, ListChangesOptions, ListVersionsOptions } from '../../types.js';
|
|
5
|
+
import { JSONRPCServer } from '../protocol/JSONRPCServer.js';
|
|
6
|
+
import type { ServerTransport } from '../protocol/types.js';
|
|
7
|
+
import type { AuthContext, AuthorizationProvider } from './AuthorizationProvider.js';
|
|
8
|
+
/**
|
|
9
|
+
* High-level client for the Patches real-time collaboration service.
|
|
10
|
+
* This class provides document subscription, patch notification handling,
|
|
11
|
+
* versioning, and other OT-specific functionality over a JSON RPC interface.
|
|
12
|
+
*/
|
|
13
|
+
export interface RPCServerOptions {
|
|
14
|
+
transport: ServerTransport;
|
|
15
|
+
patches: PatchesServer;
|
|
16
|
+
history?: PatchesHistoryManager;
|
|
17
|
+
branches?: PatchesBranchManager;
|
|
18
|
+
auth?: AuthorizationProvider;
|
|
19
|
+
}
|
|
20
|
+
export declare class RPCServer {
|
|
21
|
+
rpc: JSONRPCServer;
|
|
22
|
+
auth: AuthorizationProvider;
|
|
23
|
+
protected patches: PatchesServer;
|
|
24
|
+
protected history?: PatchesHistoryManager;
|
|
25
|
+
protected branches?: PatchesBranchManager;
|
|
26
|
+
/**
|
|
27
|
+
* Creates a new Patches WebSocket client instance.
|
|
28
|
+
* @param patches - The patches server instance to handle document operations
|
|
29
|
+
* @param history - (Optional) History manager instance to handle versioning operations
|
|
30
|
+
* @param branches - (Optional) Branch manager instance to handle branching operations
|
|
31
|
+
* @param auth - (Optional) Authorization provider implementation. Defaults to a permissive provider.
|
|
32
|
+
*/
|
|
33
|
+
constructor({ patches, history, branches, auth }: RPCServerOptions);
|
|
34
|
+
/**
|
|
35
|
+
* Gets the latest state (content and revision) of a document.
|
|
36
|
+
* @param connectionId - The ID of the connection making the request
|
|
37
|
+
* @param params - The document parameters
|
|
38
|
+
* @param params.docId - The ID of the document
|
|
39
|
+
* @param params.atRev - Optional revision number to get document state at
|
|
40
|
+
*/
|
|
41
|
+
getDoc(params: {
|
|
42
|
+
docId: string;
|
|
43
|
+
atRev?: number;
|
|
44
|
+
}, ctx?: AuthContext): Promise<import("../../types.js").PatchesSnapshot<any>>;
|
|
45
|
+
/**
|
|
46
|
+
* Gets changes that occurred for a document after a specific revision number.
|
|
47
|
+
* @param connectionId - The ID of the connection making the request
|
|
48
|
+
* @param params - The change request parameters
|
|
49
|
+
* @param params.docId - The ID of the document
|
|
50
|
+
* @param params.rev - The revision number after which to fetch changes
|
|
51
|
+
*/
|
|
52
|
+
getChangesSince(params: {
|
|
53
|
+
docId: string;
|
|
54
|
+
rev: number;
|
|
55
|
+
}, ctx?: AuthContext): Promise<Change[]>;
|
|
56
|
+
/**
|
|
57
|
+
* Applies a set of client-generated changes to a document on the server.
|
|
58
|
+
* @param connectionId - The ID of the connection making the request
|
|
59
|
+
* @param params - The change parameters
|
|
60
|
+
* @param params.docId - The ID of the document
|
|
61
|
+
* @param params.changes - An array of changes to apply
|
|
62
|
+
*/
|
|
63
|
+
commitChanges(params: {
|
|
64
|
+
docId: string;
|
|
65
|
+
changes: Change[];
|
|
66
|
+
}, ctx?: AuthContext): Promise<Change[]>;
|
|
67
|
+
/**
|
|
68
|
+
* Deletes a document on the server.
|
|
69
|
+
* @param connectionId - The ID of the connection making the request
|
|
70
|
+
* @param params - The deletion parameters
|
|
71
|
+
* @param params.docId - The ID of the document to delete
|
|
72
|
+
*/
|
|
73
|
+
deleteDoc(params: {
|
|
74
|
+
docId: string;
|
|
75
|
+
}, ctx?: AuthContext): Promise<void>;
|
|
76
|
+
listVersions(params: {
|
|
77
|
+
docId: string;
|
|
78
|
+
options?: ListVersionsOptions;
|
|
79
|
+
}, ctx?: AuthContext): Promise<import("../../types.js").VersionMetadata[]>;
|
|
80
|
+
createVersion(params: {
|
|
81
|
+
docId: string;
|
|
82
|
+
metadata: EditableVersionMetadata;
|
|
83
|
+
}, ctx?: AuthContext): Promise<string>;
|
|
84
|
+
updateVersion(params: {
|
|
85
|
+
docId: string;
|
|
86
|
+
versionId: string;
|
|
87
|
+
metadata: EditableVersionMetadata;
|
|
88
|
+
}, ctx?: AuthContext): Promise<void>;
|
|
89
|
+
getStateAtVersion(params: {
|
|
90
|
+
docId: string;
|
|
91
|
+
versionId: string;
|
|
92
|
+
}, ctx?: AuthContext): Promise<any>;
|
|
93
|
+
getChangesForVersion(params: {
|
|
94
|
+
docId: string;
|
|
95
|
+
versionId: string;
|
|
96
|
+
}, ctx?: AuthContext): Promise<Change[]>;
|
|
97
|
+
listServerChanges(params: {
|
|
98
|
+
docId: string;
|
|
99
|
+
options?: ListChangesOptions;
|
|
100
|
+
}, ctx?: AuthContext): Promise<Change[]>;
|
|
101
|
+
listBranches(params: {
|
|
102
|
+
docId: string;
|
|
103
|
+
}, ctx?: AuthContext): Promise<import("../../types.js").Branch[]>;
|
|
104
|
+
createBranch(params: {
|
|
105
|
+
docId: string;
|
|
106
|
+
rev: number;
|
|
107
|
+
metadata?: EditableVersionMetadata;
|
|
108
|
+
}, ctx?: AuthContext): Promise<string>;
|
|
109
|
+
closeBranch(params: {
|
|
110
|
+
branchId: string;
|
|
111
|
+
}, ctx?: AuthContext): Promise<void>;
|
|
112
|
+
mergeBranch(params: {
|
|
113
|
+
branchId: string;
|
|
114
|
+
}, ctx?: AuthContext): Promise<Change[]>;
|
|
115
|
+
protected assertAccess(ctx: AuthContext | undefined, docId: string, kind: 'read' | 'write', method: string, params?: Record<string, any>): Promise<void>;
|
|
116
|
+
protected assertRead(ctx: AuthContext | undefined, docId: string, method: string, params?: Record<string, any>): Promise<void>;
|
|
117
|
+
protected assertWrite(ctx: AuthContext | undefined, docId: string, method: string, params?: Record<string, any>): Promise<void>;
|
|
118
|
+
protected assertHistoryEnabled(): void;
|
|
119
|
+
protected assertBranchingEnabled(): void;
|
|
120
|
+
}
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
import { JSONRPCServer } from '../protocol/JSONRPCServer.js';
|
|
2
|
+
import { allowAll } from './AuthorizationProvider.js';
|
|
3
|
+
export class RPCServer {
|
|
4
|
+
/**
|
|
5
|
+
* Creates a new Patches WebSocket client instance.
|
|
6
|
+
* @param patches - The patches server instance to handle document operations
|
|
7
|
+
* @param history - (Optional) History manager instance to handle versioning operations
|
|
8
|
+
* @param branches - (Optional) Branch manager instance to handle branching operations
|
|
9
|
+
* @param auth - (Optional) Authorization provider implementation. Defaults to a permissive provider.
|
|
10
|
+
*/
|
|
11
|
+
constructor({ patches, history, branches, auth = allowAll }) {
|
|
12
|
+
this.rpc = new JSONRPCServer();
|
|
13
|
+
this.patches = patches;
|
|
14
|
+
this.history = history;
|
|
15
|
+
this.branches = branches;
|
|
16
|
+
this.auth = auth;
|
|
17
|
+
// Document operations
|
|
18
|
+
this.rpc.registerMethod('getDoc', this.getDoc.bind(this));
|
|
19
|
+
this.rpc.registerMethod('getChangesSince', this.getChangesSince.bind(this));
|
|
20
|
+
this.rpc.registerMethod('commitChanges', this.commitChanges.bind(this));
|
|
21
|
+
this.rpc.registerMethod('deleteDoc', this.deleteDoc.bind(this));
|
|
22
|
+
// History manager operations (if provided)
|
|
23
|
+
if (this.history) {
|
|
24
|
+
this.rpc.registerMethod('listVersions', this.listVersions.bind(this));
|
|
25
|
+
this.rpc.registerMethod('createVersion', this.createVersion.bind(this));
|
|
26
|
+
this.rpc.registerMethod('updateVersion', this.updateVersion.bind(this));
|
|
27
|
+
this.rpc.registerMethod('getVersionState', this.getStateAtVersion.bind(this));
|
|
28
|
+
this.rpc.registerMethod('getVersionChanges', this.getChangesForVersion.bind(this));
|
|
29
|
+
this.rpc.registerMethod('listServerChanges', this.listServerChanges.bind(this));
|
|
30
|
+
}
|
|
31
|
+
// Branch manager operations (if provided)
|
|
32
|
+
if (this.branches) {
|
|
33
|
+
this.rpc.registerMethod('listBranches', this.listBranches.bind(this));
|
|
34
|
+
this.rpc.registerMethod('createBranch', this.createBranch.bind(this));
|
|
35
|
+
this.rpc.registerMethod('closeBranch', this.closeBranch.bind(this));
|
|
36
|
+
this.rpc.registerMethod('mergeBranch', this.mergeBranch.bind(this));
|
|
37
|
+
}
|
|
38
|
+
// -------------------------------------------------------------------------
|
|
39
|
+
// Listen to core server events and forward as JSON-RPC notifications
|
|
40
|
+
// -------------------------------------------------------------------------
|
|
41
|
+
this.patches.onChangesCommitted((docId, changes, originClientId) => {
|
|
42
|
+
this.rpc.notify('changesCommitted', { docId, changes }, originClientId);
|
|
43
|
+
});
|
|
44
|
+
this.patches.onDocDeleted((docId, originClientId) => {
|
|
45
|
+
this.rpc.notify('docDeleted', { docId }, originClientId);
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Gets the latest state (content and revision) of a document.
|
|
50
|
+
* @param connectionId - The ID of the connection making the request
|
|
51
|
+
* @param params - The document parameters
|
|
52
|
+
* @param params.docId - The ID of the document
|
|
53
|
+
* @param params.atRev - Optional revision number to get document state at
|
|
54
|
+
*/
|
|
55
|
+
async getDoc(params, ctx) {
|
|
56
|
+
const { docId, atRev } = params;
|
|
57
|
+
await this.assertRead(ctx, docId, 'getDoc', params);
|
|
58
|
+
return this.patches.getDoc(docId, atRev);
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Gets changes that occurred for a document after a specific revision number.
|
|
62
|
+
* @param connectionId - The ID of the connection making the request
|
|
63
|
+
* @param params - The change request parameters
|
|
64
|
+
* @param params.docId - The ID of the document
|
|
65
|
+
* @param params.rev - The revision number after which to fetch changes
|
|
66
|
+
*/
|
|
67
|
+
async getChangesSince(params, ctx) {
|
|
68
|
+
const { docId, rev } = params;
|
|
69
|
+
await this.assertRead(ctx, docId, 'getChangesSince', params);
|
|
70
|
+
return this.patches.getChangesSince(docId, rev);
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Applies a set of client-generated changes to a document on the server.
|
|
74
|
+
* @param connectionId - The ID of the connection making the request
|
|
75
|
+
* @param params - The change parameters
|
|
76
|
+
* @param params.docId - The ID of the document
|
|
77
|
+
* @param params.changes - An array of changes to apply
|
|
78
|
+
*/
|
|
79
|
+
async commitChanges(params, ctx) {
|
|
80
|
+
const { docId, changes } = params;
|
|
81
|
+
await this.assertWrite(ctx, docId, 'commitChanges', params);
|
|
82
|
+
const [priorChanges, newChanges] = await this.patches.commitChanges(docId, changes, ctx?.clientId);
|
|
83
|
+
return [...priorChanges, ...newChanges];
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Deletes a document on the server.
|
|
87
|
+
* @param connectionId - The ID of the connection making the request
|
|
88
|
+
* @param params - The deletion parameters
|
|
89
|
+
* @param params.docId - The ID of the document to delete
|
|
90
|
+
*/
|
|
91
|
+
async deleteDoc(params, ctx) {
|
|
92
|
+
const { docId } = params;
|
|
93
|
+
await this.assertWrite(ctx, docId, 'deleteDoc', params);
|
|
94
|
+
await this.patches.deleteDoc(docId, ctx?.clientId);
|
|
95
|
+
}
|
|
96
|
+
// ---------------------------------------------------------------------------
|
|
97
|
+
// History Manager wrappers
|
|
98
|
+
// ---------------------------------------------------------------------------
|
|
99
|
+
async listVersions(params, ctx) {
|
|
100
|
+
this.assertHistoryEnabled();
|
|
101
|
+
const { docId, options } = params;
|
|
102
|
+
await this.assertRead(ctx, docId, 'listVersions', params);
|
|
103
|
+
return this.history.listVersions(docId, options ?? {});
|
|
104
|
+
}
|
|
105
|
+
async createVersion(params, ctx) {
|
|
106
|
+
this.assertHistoryEnabled();
|
|
107
|
+
const { docId, metadata } = params;
|
|
108
|
+
await this.assertWrite(ctx, docId, 'createVersion', params);
|
|
109
|
+
return this.history.createVersion(docId, metadata);
|
|
110
|
+
}
|
|
111
|
+
async updateVersion(params, ctx) {
|
|
112
|
+
this.assertHistoryEnabled();
|
|
113
|
+
const { docId, versionId, metadata } = params;
|
|
114
|
+
await this.assertWrite(ctx, docId, 'updateVersion', params);
|
|
115
|
+
return this.history.updateVersion(docId, versionId, metadata);
|
|
116
|
+
}
|
|
117
|
+
async getStateAtVersion(params, ctx) {
|
|
118
|
+
this.assertHistoryEnabled();
|
|
119
|
+
const { docId, versionId } = params;
|
|
120
|
+
await this.assertRead(ctx, docId, 'getStateAtVersion', params);
|
|
121
|
+
return this.history.getStateAtVersion(docId, versionId);
|
|
122
|
+
}
|
|
123
|
+
async getChangesForVersion(params, ctx) {
|
|
124
|
+
this.assertHistoryEnabled();
|
|
125
|
+
const { docId, versionId } = params;
|
|
126
|
+
await this.assertRead(ctx, docId, 'getChangesForVersion', params);
|
|
127
|
+
return this.history.getChangesForVersion(docId, versionId);
|
|
128
|
+
}
|
|
129
|
+
async listServerChanges(params, ctx) {
|
|
130
|
+
this.assertHistoryEnabled();
|
|
131
|
+
const { docId, options } = params;
|
|
132
|
+
await this.assertRead(ctx, docId, 'listServerChanges', params);
|
|
133
|
+
return this.history.listServerChanges(docId, options ?? {});
|
|
134
|
+
}
|
|
135
|
+
// ---------------------------------------------------------------------------
|
|
136
|
+
// Branch Manager wrappers
|
|
137
|
+
// ---------------------------------------------------------------------------
|
|
138
|
+
async listBranches(params, ctx) {
|
|
139
|
+
this.assertBranchingEnabled();
|
|
140
|
+
const { docId } = params;
|
|
141
|
+
await this.assertRead(ctx, docId, 'listBranches', params);
|
|
142
|
+
return this.branches.listBranches(docId);
|
|
143
|
+
}
|
|
144
|
+
async createBranch(params, ctx) {
|
|
145
|
+
this.assertBranchingEnabled();
|
|
146
|
+
const { docId, rev, metadata } = params;
|
|
147
|
+
await this.assertWrite(ctx, docId, 'createBranch', params);
|
|
148
|
+
return this.branches.createBranch(docId, rev, metadata);
|
|
149
|
+
}
|
|
150
|
+
async closeBranch(params, ctx) {
|
|
151
|
+
this.assertBranchingEnabled();
|
|
152
|
+
const { branchId } = params;
|
|
153
|
+
await this.assertWrite(ctx, branchId, 'closeBranch', params);
|
|
154
|
+
return this.branches.closeBranch(branchId, 'closed');
|
|
155
|
+
}
|
|
156
|
+
async mergeBranch(params, ctx) {
|
|
157
|
+
this.assertBranchingEnabled();
|
|
158
|
+
const { branchId } = params;
|
|
159
|
+
await this.assertWrite(ctx, branchId, 'mergeBranch', params);
|
|
160
|
+
return this.branches.mergeBranch(branchId);
|
|
161
|
+
}
|
|
162
|
+
// ---------------------------------------------------------------------------
|
|
163
|
+
// Authorization helpers
|
|
164
|
+
// ---------------------------------------------------------------------------
|
|
165
|
+
async assertAccess(ctx, docId, kind, method, params) {
|
|
166
|
+
const ok = await this.auth.canAccess(ctx, docId, kind, method, params);
|
|
167
|
+
if (!ok) {
|
|
168
|
+
throw new Error(`${kind.toUpperCase()}_FORBIDDEN:${docId}`);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
assertRead(ctx, docId, method, params) {
|
|
172
|
+
return this.assertAccess(ctx, docId, 'read', method, params);
|
|
173
|
+
}
|
|
174
|
+
assertWrite(ctx, docId, method, params) {
|
|
175
|
+
return this.assertAccess(ctx, docId, 'write', method, params);
|
|
176
|
+
}
|
|
177
|
+
assertHistoryEnabled() {
|
|
178
|
+
if (!this.history) {
|
|
179
|
+
throw new Error('History is not enabled');
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
assertBranchingEnabled() {
|
|
183
|
+
if (!this.branches) {
|
|
184
|
+
throw new Error('Branching is not enabled');
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
@@ -1,144 +1,36 @@
|
|
|
1
|
-
import type { PatchesBranchManager } from '../../server/PatchesBranchManager.js';
|
|
2
|
-
import type { PatchesHistoryManager } from '../../server/PatchesHistoryManager.js';
|
|
3
|
-
import type { PatchesServer } from '../../server/PatchesServer.js';
|
|
4
|
-
import type { Change, EditableVersionMetadata, ListVersionsOptions } from '../../types.js';
|
|
5
|
-
import { JSONRPCServer } from '../protocol/JSONRPCServer.js';
|
|
6
1
|
import type { ServerTransport } from '../protocol/types.js';
|
|
7
|
-
import type { AuthorizationProvider } from './AuthorizationProvider.js';
|
|
2
|
+
import type { AuthContext, AuthorizationProvider } from './AuthorizationProvider.js';
|
|
3
|
+
import type { RPCServer } from './RPCServer.js';
|
|
8
4
|
/**
|
|
9
5
|
* High-level client for the Patches real-time collaboration service.
|
|
10
6
|
* This class provides document subscription, patch notification handling,
|
|
11
|
-
* versioning, and other OT-specific functionality
|
|
12
|
-
* over a WebSocket connection.
|
|
7
|
+
* versioning, and other OT-specific functionality over a WebSocket connection.
|
|
13
8
|
*/
|
|
14
|
-
export interface WebSocketServerOptions {
|
|
15
|
-
transport: ServerTransport;
|
|
16
|
-
patches: PatchesServer;
|
|
17
|
-
history?: PatchesHistoryManager;
|
|
18
|
-
branches?: PatchesBranchManager;
|
|
19
|
-
auth?: AuthorizationProvider;
|
|
20
|
-
}
|
|
21
9
|
export declare class WebSocketServer {
|
|
22
10
|
protected transport: ServerTransport;
|
|
23
|
-
protected rpc: JSONRPCServer;
|
|
24
|
-
protected patches: PatchesServer;
|
|
25
|
-
protected history?: PatchesHistoryManager;
|
|
26
|
-
protected branches?: PatchesBranchManager;
|
|
27
11
|
protected auth: AuthorizationProvider;
|
|
28
12
|
/**
|
|
29
13
|
* Creates a new Patches WebSocket client instance.
|
|
14
|
+
*
|
|
30
15
|
* @param transport - The transport layer implementation that will be used for sending/receiving messages
|
|
31
|
-
* @param patches - The patches server instance to handle document operations
|
|
32
|
-
* @param auth - (Optional) Authorization provider implementation. Defaults to a permissive provider.
|
|
33
16
|
*/
|
|
34
|
-
constructor(
|
|
35
|
-
protected assertAccess(connectionId: string, docId: string, kind: 'read' | 'write', method: string, params?: Record<string, any>): Promise<void>;
|
|
36
|
-
protected assertRead(connectionId: string, docId: string, method: string, params?: Record<string, any>): Promise<void>;
|
|
37
|
-
protected assertWrite(connectionId: string, docId: string, method: string, params?: Record<string, any>): Promise<void>;
|
|
38
|
-
protected assertHistoryEnabled(): void;
|
|
39
|
-
protected assertBranchingEnabled(): void;
|
|
17
|
+
constructor(transport: ServerTransport, rpcServer: RPCServer);
|
|
40
18
|
/**
|
|
41
19
|
* Subscribes the client to one or more documents to receive real-time updates.
|
|
42
20
|
* @param connectionId - The ID of the connection making the request
|
|
43
21
|
* @param params - The subscription parameters
|
|
44
22
|
* @param params.ids - Document ID or IDs to subscribe to
|
|
45
23
|
*/
|
|
46
|
-
subscribe(
|
|
24
|
+
subscribe(params: {
|
|
47
25
|
ids: string | string[];
|
|
48
|
-
}): Promise<string[]>;
|
|
26
|
+
}, ctx?: AuthContext): Promise<string[]>;
|
|
49
27
|
/**
|
|
50
28
|
* Unsubscribes the client from one or more documents.
|
|
51
29
|
* @param connectionId - The ID of the connection making the request
|
|
52
30
|
* @param params - The unsubscription parameters
|
|
53
31
|
* @param params.ids - Document ID or IDs to unsubscribe from
|
|
54
32
|
*/
|
|
55
|
-
unsubscribe(
|
|
33
|
+
unsubscribe(params: {
|
|
56
34
|
ids: string | string[];
|
|
57
|
-
}): Promise<string[]>;
|
|
58
|
-
/**
|
|
59
|
-
* Gets the latest state (content and revision) of a document.
|
|
60
|
-
* @param _connectionId - The ID of the connection making the request
|
|
61
|
-
* @param params - The document parameters
|
|
62
|
-
* @param params.docId - The ID of the document
|
|
63
|
-
* @param params.atRev - Optional revision number to get document state at
|
|
64
|
-
*/
|
|
65
|
-
getDoc(_connectionId: string, params: {
|
|
66
|
-
docId: string;
|
|
67
|
-
atRev?: number;
|
|
68
|
-
}): Promise<import("../../types.js").PatchesSnapshot<any>>;
|
|
69
|
-
/**
|
|
70
|
-
* Gets changes that occurred for a document after a specific revision number.
|
|
71
|
-
* @param _connectionId - The ID of the connection making the request
|
|
72
|
-
* @param params - The change request parameters
|
|
73
|
-
* @param params.docId - The ID of the document
|
|
74
|
-
* @param params.rev - The revision number after which to fetch changes
|
|
75
|
-
*/
|
|
76
|
-
getChangesSince(_connectionId: string, params: {
|
|
77
|
-
docId: string;
|
|
78
|
-
rev: number;
|
|
79
|
-
}): Promise<Change[]>;
|
|
80
|
-
/**
|
|
81
|
-
* Applies a set of client-generated changes to a document on the server.
|
|
82
|
-
* @param connectionId - The ID of the connection making the request
|
|
83
|
-
* @param params - The change parameters
|
|
84
|
-
* @param params.docId - The ID of the document
|
|
85
|
-
* @param params.changes - An array of changes to apply
|
|
86
|
-
*/
|
|
87
|
-
commitChanges(connectionId: string, params: {
|
|
88
|
-
docId: string;
|
|
89
|
-
changes: Change[];
|
|
90
|
-
}): Promise<Change[]>;
|
|
91
|
-
/**
|
|
92
|
-
* Deletes a document on the server.
|
|
93
|
-
* @param connectionId - The ID of the connection making the request
|
|
94
|
-
* @param params - The deletion parameters
|
|
95
|
-
* @param params.docId - The ID of the document to delete
|
|
96
|
-
*/
|
|
97
|
-
deleteDoc(connectionId: string, params: {
|
|
98
|
-
docId: string;
|
|
99
|
-
}): Promise<void>;
|
|
100
|
-
listVersions(connectionId: string, params: {
|
|
101
|
-
docId: string;
|
|
102
|
-
options?: ListVersionsOptions;
|
|
103
|
-
}): Promise<import("../../types.js").VersionMetadata[]>;
|
|
104
|
-
createVersion(connectionId: string, params: {
|
|
105
|
-
docId: string;
|
|
106
|
-
metadata: EditableVersionMetadata;
|
|
107
|
-
}): Promise<string>;
|
|
108
|
-
updateVersion(connectionId: string, params: {
|
|
109
|
-
docId: string;
|
|
110
|
-
versionId: string;
|
|
111
|
-
metadata: EditableVersionMetadata;
|
|
112
|
-
}): Promise<void>;
|
|
113
|
-
getStateAtVersion(connectionId: string, params: {
|
|
114
|
-
docId: string;
|
|
115
|
-
versionId: string;
|
|
116
|
-
}): Promise<any>;
|
|
117
|
-
getChangesForVersion(connectionId: string, params: {
|
|
118
|
-
docId: string;
|
|
119
|
-
versionId: string;
|
|
120
|
-
}): Promise<Change[]>;
|
|
121
|
-
listServerChanges(connectionId: string, params: {
|
|
122
|
-
docId: string;
|
|
123
|
-
options?: {
|
|
124
|
-
limit?: number;
|
|
125
|
-
startAfterRev?: number;
|
|
126
|
-
endBeforeRev?: number;
|
|
127
|
-
reverse?: boolean;
|
|
128
|
-
};
|
|
129
|
-
}): Promise<Change[]>;
|
|
130
|
-
listBranches(connectionId: string, params: {
|
|
131
|
-
docId: string;
|
|
132
|
-
}): Promise<import("../../types.js").Branch[]>;
|
|
133
|
-
createBranch(connectionId: string, params: {
|
|
134
|
-
docId: string;
|
|
135
|
-
rev: number;
|
|
136
|
-
metadata?: EditableVersionMetadata;
|
|
137
|
-
}): Promise<string>;
|
|
138
|
-
closeBranch(connectionId: string, params: {
|
|
139
|
-
branchId: string;
|
|
140
|
-
}): Promise<void>;
|
|
141
|
-
mergeBranch(connectionId: string, params: {
|
|
142
|
-
branchId: string;
|
|
143
|
-
}): Promise<Change[]>;
|
|
35
|
+
}, ctx?: AuthContext): Promise<string[]>;
|
|
144
36
|
}
|
|
@@ -1,84 +1,48 @@
|
|
|
1
|
-
import { JSONRPCServer } from '../protocol/JSONRPCServer.js';
|
|
2
1
|
import { allowAll } from './AuthorizationProvider.js';
|
|
2
|
+
/**
|
|
3
|
+
* High-level client for the Patches real-time collaboration service.
|
|
4
|
+
* This class provides document subscription, patch notification handling,
|
|
5
|
+
* versioning, and other OT-specific functionality over a WebSocket connection.
|
|
6
|
+
*/
|
|
3
7
|
export class WebSocketServer {
|
|
4
8
|
/**
|
|
5
9
|
* Creates a new Patches WebSocket client instance.
|
|
10
|
+
*
|
|
6
11
|
* @param transport - The transport layer implementation that will be used for sending/receiving messages
|
|
7
|
-
* @param patches - The patches server instance to handle document operations
|
|
8
|
-
* @param auth - (Optional) Authorization provider implementation. Defaults to a permissive provider.
|
|
9
12
|
*/
|
|
10
|
-
constructor(
|
|
13
|
+
constructor(transport, rpcServer) {
|
|
11
14
|
this.transport = transport;
|
|
12
|
-
|
|
13
|
-
this.
|
|
14
|
-
this.history = history;
|
|
15
|
-
this.branches = branches;
|
|
16
|
-
this.auth = auth;
|
|
15
|
+
const { rpc, auth } = rpcServer;
|
|
16
|
+
this.auth = auth || allowAll;
|
|
17
17
|
// Subscription operations
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
this.rpc.registerMethod('getVersionState', this.getStateAtVersion.bind(this));
|
|
31
|
-
this.rpc.registerMethod('getVersionChanges', this.getChangesForVersion.bind(this));
|
|
32
|
-
this.rpc.registerMethod('listServerChanges', this.listServerChanges.bind(this));
|
|
33
|
-
}
|
|
34
|
-
// Branch manager operations (if provided)
|
|
35
|
-
if (this.branches) {
|
|
36
|
-
this.rpc.registerMethod('listBranches', this.listBranches.bind(this));
|
|
37
|
-
this.rpc.registerMethod('createBranch', this.createBranch.bind(this));
|
|
38
|
-
this.rpc.registerMethod('closeBranch', this.closeBranch.bind(this));
|
|
39
|
-
this.rpc.registerMethod('mergeBranch', this.mergeBranch.bind(this));
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
// ---------------------------------------------------------------------------
|
|
43
|
-
// Authorization helpers
|
|
44
|
-
// ---------------------------------------------------------------------------
|
|
45
|
-
async assertAccess(connectionId, docId, kind, method, params) {
|
|
46
|
-
const ok = await this.auth.canAccess(connectionId, docId, kind, method, params);
|
|
47
|
-
if (!ok) {
|
|
48
|
-
throw new Error(`${kind.toUpperCase()}_FORBIDDEN:${docId}`);
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
assertRead(connectionId, docId, method, params) {
|
|
52
|
-
return this.assertAccess(connectionId, docId, 'read', method, params);
|
|
53
|
-
}
|
|
54
|
-
assertWrite(connectionId, docId, method, params) {
|
|
55
|
-
return this.assertAccess(connectionId, docId, 'write', method, params);
|
|
56
|
-
}
|
|
57
|
-
assertHistoryEnabled() {
|
|
58
|
-
if (!this.history) {
|
|
59
|
-
throw new Error('History is not enabled');
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
assertBranchingEnabled() {
|
|
63
|
-
if (!this.branches) {
|
|
64
|
-
throw new Error('Branching is not enabled');
|
|
65
|
-
}
|
|
18
|
+
rpc.registerMethod('subscribe', this.subscribe.bind(this));
|
|
19
|
+
rpc.registerMethod('unsubscribe', this.unsubscribe.bind(this));
|
|
20
|
+
rpc.onNotify(async (msg) => {
|
|
21
|
+
if (!msg.params?.docId)
|
|
22
|
+
return;
|
|
23
|
+
const { docId } = msg.params;
|
|
24
|
+
const msgString = JSON.stringify(msg);
|
|
25
|
+
const clientIds = await this.transport.listSubscriptions(docId);
|
|
26
|
+
clientIds.forEach(clientId => {
|
|
27
|
+
this.transport.send(clientId, msgString);
|
|
28
|
+
});
|
|
29
|
+
});
|
|
66
30
|
}
|
|
67
|
-
// --- Patches API Methods ---
|
|
68
|
-
// === Subscription Operations ===
|
|
69
31
|
/**
|
|
70
32
|
* Subscribes the client to one or more documents to receive real-time updates.
|
|
71
33
|
* @param connectionId - The ID of the connection making the request
|
|
72
34
|
* @param params - The subscription parameters
|
|
73
35
|
* @param params.ids - Document ID or IDs to subscribe to
|
|
74
36
|
*/
|
|
75
|
-
async subscribe(
|
|
37
|
+
async subscribe(params, ctx) {
|
|
38
|
+
if (!ctx?.clientId)
|
|
39
|
+
return [];
|
|
76
40
|
const { ids } = params;
|
|
77
41
|
const allIds = Array.isArray(ids) ? ids : [ids];
|
|
78
42
|
const allowed = [];
|
|
79
43
|
await Promise.all(allIds.map(async (id) => {
|
|
80
44
|
try {
|
|
81
|
-
if (await this.auth.canAccess(
|
|
45
|
+
if (await this.auth.canAccess(ctx, id, 'read', 'subscribe', params)) {
|
|
82
46
|
allowed.push(id);
|
|
83
47
|
}
|
|
84
48
|
}
|
|
@@ -89,9 +53,7 @@ export class WebSocketServer {
|
|
|
89
53
|
if (allowed.length === 0) {
|
|
90
54
|
return [];
|
|
91
55
|
}
|
|
92
|
-
|
|
93
|
-
const input = Array.isArray(ids) ? allowed : allowed[0];
|
|
94
|
-
return this.patches.subscribe(connectionId, input);
|
|
56
|
+
return this.transport.addSubscription(ctx.clientId, allowed);
|
|
95
57
|
}
|
|
96
58
|
/**
|
|
97
59
|
* Unsubscribes the client from one or more documents.
|
|
@@ -99,136 +61,13 @@ export class WebSocketServer {
|
|
|
99
61
|
* @param params - The unsubscription parameters
|
|
100
62
|
* @param params.ids - Document ID or IDs to unsubscribe from
|
|
101
63
|
*/
|
|
102
|
-
async unsubscribe(
|
|
64
|
+
async unsubscribe(params, ctx) {
|
|
65
|
+
if (!ctx?.clientId)
|
|
66
|
+
return [];
|
|
103
67
|
const { ids } = params;
|
|
104
68
|
// We deliberately do **not** enforce authorization here –
|
|
105
69
|
// removing a subscription doesn't leak information and helps
|
|
106
70
|
// clean up server-side state if a client has lost access mid-session.
|
|
107
|
-
return this.
|
|
108
|
-
}
|
|
109
|
-
// === Document Operations ===
|
|
110
|
-
/**
|
|
111
|
-
* Gets the latest state (content and revision) of a document.
|
|
112
|
-
* @param _connectionId - The ID of the connection making the request
|
|
113
|
-
* @param params - The document parameters
|
|
114
|
-
* @param params.docId - The ID of the document
|
|
115
|
-
* @param params.atRev - Optional revision number to get document state at
|
|
116
|
-
*/
|
|
117
|
-
async getDoc(_connectionId, params) {
|
|
118
|
-
const { docId, atRev } = params;
|
|
119
|
-
await this.assertRead(_connectionId, docId, 'getDoc', params);
|
|
120
|
-
return this.patches.getDoc(docId, atRev);
|
|
121
|
-
}
|
|
122
|
-
/**
|
|
123
|
-
* Gets changes that occurred for a document after a specific revision number.
|
|
124
|
-
* @param _connectionId - The ID of the connection making the request
|
|
125
|
-
* @param params - The change request parameters
|
|
126
|
-
* @param params.docId - The ID of the document
|
|
127
|
-
* @param params.rev - The revision number after which to fetch changes
|
|
128
|
-
*/
|
|
129
|
-
async getChangesSince(_connectionId, params) {
|
|
130
|
-
const { docId, rev } = params;
|
|
131
|
-
await this.assertRead(_connectionId, docId, 'getChangesSince', params);
|
|
132
|
-
return this.patches.getChangesSince(docId, rev);
|
|
133
|
-
}
|
|
134
|
-
/**
|
|
135
|
-
* Applies a set of client-generated changes to a document on the server.
|
|
136
|
-
* @param connectionId - The ID of the connection making the request
|
|
137
|
-
* @param params - The change parameters
|
|
138
|
-
* @param params.docId - The ID of the document
|
|
139
|
-
* @param params.changes - An array of changes to apply
|
|
140
|
-
*/
|
|
141
|
-
async commitChanges(connectionId, params) {
|
|
142
|
-
const { docId, changes } = params;
|
|
143
|
-
await this.assertWrite(connectionId, docId, 'commitChanges', params);
|
|
144
|
-
const [priorChanges, newChanges] = await this.patches.commitChanges(docId, changes);
|
|
145
|
-
// Notify other clients that the new changes have been committed
|
|
146
|
-
const connectionIds = this.transport.getConnectionIds().filter(id => id !== connectionId);
|
|
147
|
-
if (connectionIds.length > 0) {
|
|
148
|
-
this.rpc.notify(connectionIds, 'changesCommitted', { docId, changes: newChanges });
|
|
149
|
-
}
|
|
150
|
-
return [...priorChanges, ...newChanges];
|
|
151
|
-
}
|
|
152
|
-
/**
|
|
153
|
-
* Deletes a document on the server.
|
|
154
|
-
* @param connectionId - The ID of the connection making the request
|
|
155
|
-
* @param params - The deletion parameters
|
|
156
|
-
* @param params.docId - The ID of the document to delete
|
|
157
|
-
*/
|
|
158
|
-
async deleteDoc(connectionId, params) {
|
|
159
|
-
const { docId } = params;
|
|
160
|
-
await this.assertWrite(connectionId, docId, 'deleteDoc', params);
|
|
161
|
-
await this.patches.deleteDoc(docId);
|
|
162
|
-
// Notify other clients that the document has been deleted
|
|
163
|
-
const connectionIds = this.transport.getConnectionIds().filter(id => id !== connectionId);
|
|
164
|
-
if (connectionIds.length > 0) {
|
|
165
|
-
this.rpc.notify(connectionIds, 'docDeleted', { docId });
|
|
166
|
-
}
|
|
167
|
-
}
|
|
168
|
-
// ---------------------------------------------------------------------------
|
|
169
|
-
// History Manager wrappers
|
|
170
|
-
// ---------------------------------------------------------------------------
|
|
171
|
-
async listVersions(connectionId, params) {
|
|
172
|
-
this.assertHistoryEnabled();
|
|
173
|
-
const { docId, options } = params;
|
|
174
|
-
await this.assertRead(connectionId, docId, 'listVersions', params);
|
|
175
|
-
return this.history.listVersions(docId, options ?? {});
|
|
176
|
-
}
|
|
177
|
-
async createVersion(connectionId, params) {
|
|
178
|
-
this.assertHistoryEnabled();
|
|
179
|
-
const { docId, metadata } = params;
|
|
180
|
-
await this.assertWrite(connectionId, docId, 'createVersion', params);
|
|
181
|
-
return this.history.createVersion(docId, metadata);
|
|
182
|
-
}
|
|
183
|
-
async updateVersion(connectionId, params) {
|
|
184
|
-
this.assertHistoryEnabled();
|
|
185
|
-
const { docId, versionId, metadata } = params;
|
|
186
|
-
await this.assertWrite(connectionId, docId, 'updateVersion', params);
|
|
187
|
-
return this.history.updateVersion(docId, versionId, metadata);
|
|
188
|
-
}
|
|
189
|
-
async getStateAtVersion(connectionId, params) {
|
|
190
|
-
this.assertHistoryEnabled();
|
|
191
|
-
const { docId, versionId } = params;
|
|
192
|
-
await this.assertRead(connectionId, docId, 'getStateAtVersion', params);
|
|
193
|
-
return this.history.getStateAtVersion(docId, versionId);
|
|
194
|
-
}
|
|
195
|
-
async getChangesForVersion(connectionId, params) {
|
|
196
|
-
this.assertHistoryEnabled();
|
|
197
|
-
const { docId, versionId } = params;
|
|
198
|
-
await this.assertRead(connectionId, docId, 'getChangesForVersion', params);
|
|
199
|
-
return this.history.getChangesForVersion(docId, versionId);
|
|
200
|
-
}
|
|
201
|
-
async listServerChanges(connectionId, params) {
|
|
202
|
-
this.assertHistoryEnabled();
|
|
203
|
-
const { docId, options } = params;
|
|
204
|
-
await this.assertRead(connectionId, docId, 'listServerChanges', params);
|
|
205
|
-
return this.history.listServerChanges(docId, options ?? {});
|
|
206
|
-
}
|
|
207
|
-
// ---------------------------------------------------------------------------
|
|
208
|
-
// Branch Manager wrappers
|
|
209
|
-
// ---------------------------------------------------------------------------
|
|
210
|
-
async listBranches(connectionId, params) {
|
|
211
|
-
this.assertBranchingEnabled();
|
|
212
|
-
const { docId } = params;
|
|
213
|
-
await this.assertRead(connectionId, docId, 'listBranches', params);
|
|
214
|
-
return this.branches.listBranches(docId);
|
|
215
|
-
}
|
|
216
|
-
async createBranch(connectionId, params) {
|
|
217
|
-
this.assertBranchingEnabled();
|
|
218
|
-
const { docId, rev, metadata } = params;
|
|
219
|
-
await this.assertWrite(connectionId, docId, 'createBranch', params);
|
|
220
|
-
return this.branches.createBranch(docId, rev, metadata);
|
|
221
|
-
}
|
|
222
|
-
async closeBranch(connectionId, params) {
|
|
223
|
-
this.assertBranchingEnabled();
|
|
224
|
-
const { branchId } = params;
|
|
225
|
-
await this.assertWrite(connectionId, branchId, 'closeBranch', params);
|
|
226
|
-
return this.branches.closeBranch(branchId, 'closed');
|
|
227
|
-
}
|
|
228
|
-
async mergeBranch(connectionId, params) {
|
|
229
|
-
this.assertBranchingEnabled();
|
|
230
|
-
const { branchId } = params;
|
|
231
|
-
await this.assertWrite(connectionId, branchId, 'mergeBranch', params);
|
|
232
|
-
return this.branches.mergeBranch(branchId);
|
|
71
|
+
return this.transport.removeSubscription(ctx?.clientId, Array.isArray(ids) ? ids : [ids]);
|
|
233
72
|
}
|
|
234
73
|
}
|
|
@@ -19,21 +19,11 @@ export interface PatchesServerOptions {
|
|
|
19
19
|
export declare class PatchesServer {
|
|
20
20
|
readonly store: PatchesStoreBackend;
|
|
21
21
|
private readonly sessionTimeoutMillis;
|
|
22
|
+
/** Notifies listeners whenever a batch of changes is *successfully* committed. */
|
|
23
|
+
readonly onChangesCommitted: import("../event-signal.js").Signal<(docId: string, changes: Change[], originClientId?: string) => void>;
|
|
24
|
+
/** Notifies listeners when a document is deleted. */
|
|
25
|
+
readonly onDocDeleted: import("../event-signal.js").Signal<(docId: string, originClientId?: string) => void>;
|
|
22
26
|
constructor(store: PatchesStoreBackend, options?: PatchesServerOptions);
|
|
23
|
-
/**
|
|
24
|
-
* Subscribes the connected client to one or more documents.
|
|
25
|
-
* @param clientId - The ID of the client.
|
|
26
|
-
* @param ids Document ID(s) to subscribe to.
|
|
27
|
-
* @returns A list of document IDs the client is now successfully subscribed to.
|
|
28
|
-
*/
|
|
29
|
-
subscribe(clientId: string, ids: string | string[]): Promise<string[]>;
|
|
30
|
-
/**
|
|
31
|
-
* Unsubscribes the connected client from one or more documents.
|
|
32
|
-
* @param clientId - The ID of the client.
|
|
33
|
-
* @param ids Document ID(s) to unsubscribe from.
|
|
34
|
-
* @returns A list of document IDs the client is now successfully unsubscribed from.
|
|
35
|
-
*/
|
|
36
|
-
unsubscribe(clientId: string, ids: string | string[]): Promise<string[]>;
|
|
37
27
|
/**
|
|
38
28
|
* Get the latest version of a document and changes since the last version.
|
|
39
29
|
* @param docId - The ID of the document.
|
|
@@ -51,16 +41,18 @@ export declare class PatchesServer {
|
|
|
51
41
|
* Commits a set of changes to a document, applying operational transformation as needed.
|
|
52
42
|
* @param docId - The ID of the document.
|
|
53
43
|
* @param changes - The changes to commit.
|
|
44
|
+
* @param originClientId - The ID of the client that initiated the commit.
|
|
54
45
|
* @returns A tuple of [committedChanges, transformedChanges] where:
|
|
55
46
|
* - committedChanges: Changes that were already committed to the server after the client's base revision
|
|
56
47
|
* - transformedChanges: The client's changes after being transformed against concurrent changes
|
|
57
48
|
*/
|
|
58
|
-
commitChanges(docId: string, changes: Change[]): Promise<[Change[], Change[]]>;
|
|
49
|
+
commitChanges(docId: string, changes: Change[], originClientId?: string): Promise<[Change[], Change[]]>;
|
|
59
50
|
/**
|
|
60
51
|
* Deletes a document.
|
|
61
52
|
* @param docId The document ID.
|
|
53
|
+
* @param originClientId - The ID of the client that initiated the delete operation.
|
|
62
54
|
*/
|
|
63
|
-
deleteDoc(docId: string): Promise<void>;
|
|
55
|
+
deleteDoc(docId: string, originClientId?: string): Promise<void>;
|
|
64
56
|
/**
|
|
65
57
|
* Create a new named version snapshot of a document's current state.
|
|
66
58
|
* @param docId The document ID.
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { createId } from 'crypto-id';
|
|
2
|
+
import { signal } from '../event-signal.js';
|
|
2
3
|
import { applyPatch } from '../json-patch/applyPatch.js';
|
|
3
4
|
import { transformPatch } from '../json-patch/transformPatch.js';
|
|
4
5
|
import { applyChanges } from '../utils.js';
|
|
@@ -10,29 +11,12 @@ import { applyChanges } from '../utils.js';
|
|
|
10
11
|
export class PatchesServer {
|
|
11
12
|
constructor(store, options = {}) {
|
|
12
13
|
this.store = store;
|
|
14
|
+
/** Notifies listeners whenever a batch of changes is *successfully* committed. */
|
|
15
|
+
this.onChangesCommitted = signal();
|
|
16
|
+
/** Notifies listeners when a document is deleted. */
|
|
17
|
+
this.onDocDeleted = signal();
|
|
13
18
|
this.sessionTimeoutMillis = (options.sessionTimeoutMinutes ?? 30) * 60 * 1000;
|
|
14
19
|
}
|
|
15
|
-
// --- Patches API Methods ---
|
|
16
|
-
// === Subscription Operations ===
|
|
17
|
-
/**
|
|
18
|
-
* Subscribes the connected client to one or more documents.
|
|
19
|
-
* @param clientId - The ID of the client.
|
|
20
|
-
* @param ids Document ID(s) to subscribe to.
|
|
21
|
-
* @returns A list of document IDs the client is now successfully subscribed to.
|
|
22
|
-
*/
|
|
23
|
-
subscribe(clientId, ids) {
|
|
24
|
-
return this.store.addSubscription(clientId, Array.isArray(ids) ? ids : [ids]);
|
|
25
|
-
}
|
|
26
|
-
/**
|
|
27
|
-
* Unsubscribes the connected client from one or more documents.
|
|
28
|
-
* @param clientId - The ID of the client.
|
|
29
|
-
* @param ids Document ID(s) to unsubscribe from.
|
|
30
|
-
* @returns A list of document IDs the client is now successfully unsubscribed from.
|
|
31
|
-
*/
|
|
32
|
-
unsubscribe(clientId, ids) {
|
|
33
|
-
return this.store.removeSubscription(clientId, Array.isArray(ids) ? ids : [ids]);
|
|
34
|
-
}
|
|
35
|
-
// === Document Operations ===
|
|
36
20
|
/**
|
|
37
21
|
* Get the latest version of a document and changes since the last version.
|
|
38
22
|
* @param docId - The ID of the document.
|
|
@@ -54,11 +38,12 @@ export class PatchesServer {
|
|
|
54
38
|
* Commits a set of changes to a document, applying operational transformation as needed.
|
|
55
39
|
* @param docId - The ID of the document.
|
|
56
40
|
* @param changes - The changes to commit.
|
|
41
|
+
* @param originClientId - The ID of the client that initiated the commit.
|
|
57
42
|
* @returns A tuple of [committedChanges, transformedChanges] where:
|
|
58
43
|
* - committedChanges: Changes that were already committed to the server after the client's base revision
|
|
59
44
|
* - transformedChanges: The client's changes after being transformed against concurrent changes
|
|
60
45
|
*/
|
|
61
|
-
async commitChanges(docId, changes) {
|
|
46
|
+
async commitChanges(docId, changes, originClientId) {
|
|
62
47
|
if (changes.length === 0) {
|
|
63
48
|
return [[], []];
|
|
64
49
|
}
|
|
@@ -142,15 +127,21 @@ export class PatchesServer {
|
|
|
142
127
|
if (transformedChanges.length > 0) {
|
|
143
128
|
await this.store.saveChanges(docId, transformedChanges);
|
|
144
129
|
}
|
|
130
|
+
// Fire event for realtime transports (WebSocket, etc.)
|
|
131
|
+
if (transformedChanges.length > 0) {
|
|
132
|
+
await this.onChangesCommitted.emit(docId, transformedChanges, originClientId);
|
|
133
|
+
}
|
|
145
134
|
// Return committed changes and newly transformed changes separately
|
|
146
135
|
return [committedChanges, transformedChanges];
|
|
147
136
|
}
|
|
148
137
|
/**
|
|
149
138
|
* Deletes a document.
|
|
150
139
|
* @param docId The document ID.
|
|
140
|
+
* @param originClientId - The ID of the client that initiated the delete operation.
|
|
151
141
|
*/
|
|
152
|
-
deleteDoc(docId) {
|
|
153
|
-
|
|
142
|
+
async deleteDoc(docId, originClientId) {
|
|
143
|
+
await this.store.deleteDoc(docId);
|
|
144
|
+
await this.onDocDeleted.emit(docId, originClientId);
|
|
154
145
|
}
|
|
155
146
|
// === Version Operations ===
|
|
156
147
|
/**
|
package/dist/server/types.d.ts
CHANGED
|
@@ -4,10 +4,6 @@ import type { Branch, Change, EditableVersionMetadata, ListChangesOptions, ListV
|
|
|
4
4
|
* Defines methods needed by PatchesServer, PatchesHistoryManager, etc.
|
|
5
5
|
*/
|
|
6
6
|
export interface PatchesStoreBackend {
|
|
7
|
-
/** Adds a subscription for a client to one or more documents. */
|
|
8
|
-
addSubscription(clientId: string, docIds: string[]): Promise<string[]>;
|
|
9
|
-
/** Removes a subscription for a client from one or more documents. */
|
|
10
|
-
removeSubscription(clientId: string, docIds: string[]): Promise<string[]>;
|
|
11
7
|
/** Saves a batch of committed server changes. */
|
|
12
8
|
saveChanges(docId: string, changes: Change[]): Promise<void>;
|
|
13
9
|
/** Lists committed server changes based on revision numbers. */
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dabble/patches",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.22",
|
|
4
4
|
"description": "Immutable JSON Patch implementation based on RFC 6902 supporting operational transformation and last-writer-wins",
|
|
5
5
|
"author": "Jacob Wright <jacwright@gmail.com>",
|
|
6
6
|
"bugs": {
|