@dabble/patches 0.5.12 → 0.5.13
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/index.d.ts +1 -1
- package/dist/net/PatchesClient.d.ts +2 -0
- package/dist/net/PatchesClient.js +5 -0
- package/dist/net/PatchesSync.d.ts +14 -0
- package/dist/net/PatchesSync.js +34 -0
- package/dist/net/error.d.ts +12 -2
- package/dist/net/error.js +9 -1
- package/dist/net/index.d.ts +2 -2
- package/dist/net/protocol/JSONRPCServer.d.ts +1 -0
- package/dist/net/websocket/AuthorizationProvider.d.ts +26 -1
- package/dist/net/websocket/AuthorizationProvider.js +11 -0
- package/dist/net/websocket/RPCServer.d.ts +8 -0
- package/dist/net/websocket/RPCServer.js +11 -0
- package/dist/net/websocket/WebSocketServer.d.ts +2 -1
- package/dist/net/websocket/WebSocketServer.js +13 -1
- package/dist/server/CompressedStoreBackend.d.ts +4 -1
- package/dist/server/CompressedStoreBackend.js +9 -0
- package/dist/server/PatchesServer.d.ts +9 -2
- package/dist/server/PatchesServer.js +28 -1
- package/dist/server/types.d.ts +7 -1
- package/dist/types.d.ts +25 -1
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -17,7 +17,7 @@ export { createPathProxy, pathProxy } from './json-patch/pathProxy.js';
|
|
|
17
17
|
export { transformPatch } from './json-patch/transformPatch.js';
|
|
18
18
|
export { JSONPatch, PathLike, WriteOptions } from './json-patch/JSONPatch.js';
|
|
19
19
|
export { ApplyJSONPatchOptions, JSONPatchOpHandlerMap as JSONPatchCustomTypes, JSONPatchOp } from './json-patch/types.js';
|
|
20
|
-
export { Branch, BranchStatus, Change, ChangeInput, ChangeMutator, CommitChangesOptions, EditableBranchMetadata, EditableVersionMetadata, ListChangesOptions, ListVersionsOptions, PatchesSnapshot, PatchesState, PathProxy, SyncingState, VersionMetadata } from './types.js';
|
|
20
|
+
export { Branch, BranchStatus, Change, ChangeInput, ChangeMutator, CommitChangesOptions, DeleteDocOptions, DocumentTombstone, EditableBranchMetadata, EditableVersionMetadata, ListChangesOptions, ListVersionsOptions, PatchesSnapshot, PatchesState, PathProxy, SyncingState, VersionMetadata } from './types.js';
|
|
21
21
|
export { clampTimestamp, extractTimezoneOffset, getISO, getLocalISO, getLocalTimezoneOffset, timestampDiff } from './utils/dates.js';
|
|
22
22
|
export { add } from './json-patch/ops/add.js';
|
|
23
23
|
export { copy } from './json-patch/ops/copy.js';
|
|
@@ -17,6 +17,8 @@ declare class PatchesClient implements PatchesAPI {
|
|
|
17
17
|
transport: ClientTransport;
|
|
18
18
|
/** Signal emitted when the server pushes document changes. */
|
|
19
19
|
readonly onChangesCommitted: Signal<(docId: string, changes: Change[]) => void>;
|
|
20
|
+
/** Signal emitted when a document is deleted (either by another client or discovered on subscribe). */
|
|
21
|
+
readonly onDocDeleted: Signal<(docId: string) => void>;
|
|
20
22
|
/**
|
|
21
23
|
* Creates a new Patches WebSocket client instance.
|
|
22
24
|
* @param url - The WebSocket server URL to connect to
|
|
@@ -7,6 +7,8 @@ class PatchesClient {
|
|
|
7
7
|
// --- Public Signals ---
|
|
8
8
|
/** Signal emitted when the server pushes document changes. */
|
|
9
9
|
onChangesCommitted = signal();
|
|
10
|
+
/** Signal emitted when a document is deleted (either by another client or discovered on subscribe). */
|
|
11
|
+
onDocDeleted = signal();
|
|
10
12
|
/**
|
|
11
13
|
* Creates a new Patches WebSocket client instance.
|
|
12
14
|
* @param url - The WebSocket server URL to connect to
|
|
@@ -19,6 +21,9 @@ class PatchesClient {
|
|
|
19
21
|
const { docId, changes } = params;
|
|
20
22
|
this.onChangesCommitted.emit(docId, changes);
|
|
21
23
|
});
|
|
24
|
+
this.rpc.on("docDeleted", (params) => {
|
|
25
|
+
this.onDocDeleted.emit(params.docId);
|
|
26
|
+
});
|
|
22
27
|
}
|
|
23
28
|
// --- Patches API Methods ---
|
|
24
29
|
// === Subscription Operations ===
|
|
@@ -53,6 +53,11 @@ declare class PatchesSync {
|
|
|
53
53
|
readonly onError: Signal<(error: Error, context?: {
|
|
54
54
|
docId?: string;
|
|
55
55
|
}) => void>;
|
|
56
|
+
/**
|
|
57
|
+
* Signal emitted when a document is deleted remotely (by another client or discovered via tombstone).
|
|
58
|
+
* Provides the pending changes that were discarded so the application can handle them.
|
|
59
|
+
*/
|
|
60
|
+
readonly onRemoteDocDeleted: Signal<(docId: string, pendingChanges: Change[]) => void>;
|
|
56
61
|
constructor(patches: Patches, url: string, options?: PatchesSyncOptions | undefined);
|
|
57
62
|
/**
|
|
58
63
|
* Gets the URL of the WebSocket connection.
|
|
@@ -119,6 +124,15 @@ declare class PatchesSync {
|
|
|
119
124
|
protected _handleDocsTracked(docIds: string[]): Promise<void>;
|
|
120
125
|
protected _handleDocsUntracked(docIds: string[]): Promise<void>;
|
|
121
126
|
protected _handleDocChange(docId: string, _changes: Change[]): Promise<void>;
|
|
127
|
+
/**
|
|
128
|
+
* Unified handler for remote document deletion (both real-time notifications and offline discovery).
|
|
129
|
+
* Cleans up local state and notifies the application with any pending changes that were lost.
|
|
130
|
+
*/
|
|
131
|
+
protected _handleRemoteDocDeleted(docId: string): Promise<void>;
|
|
132
|
+
/**
|
|
133
|
+
* Helper to detect DOC_DELETED (410) errors from the server.
|
|
134
|
+
*/
|
|
135
|
+
protected _isDocDeletedError(err: unknown): boolean;
|
|
122
136
|
}
|
|
123
137
|
|
|
124
138
|
export { PatchesSync, type PatchesSyncOptions, type PatchesSyncState };
|
package/dist/net/PatchesSync.js
CHANGED
|
@@ -35,6 +35,11 @@ class PatchesSync {
|
|
|
35
35
|
* Signal emitted when an error occurs.
|
|
36
36
|
*/
|
|
37
37
|
__publicField(this, "onError", signal());
|
|
38
|
+
/**
|
|
39
|
+
* Signal emitted when a document is deleted remotely (by another client or discovered via tombstone).
|
|
40
|
+
* Provides the pending changes that were discarded so the application can handle them.
|
|
41
|
+
*/
|
|
42
|
+
__publicField(this, "onRemoteDocDeleted", signal());
|
|
38
43
|
this.patches = patches;
|
|
39
44
|
this.store = patches.store;
|
|
40
45
|
this.maxPayloadBytes = options?.maxPayloadBytes;
|
|
@@ -46,6 +51,7 @@ class PatchesSync {
|
|
|
46
51
|
onlineState.onOnlineChange((online) => this.updateState({ online }));
|
|
47
52
|
this.ws.onStateChange(this._handleConnectionChange.bind(this));
|
|
48
53
|
this.ws.onChangesCommitted(this._receiveCommittedChanges.bind(this));
|
|
54
|
+
this.ws.onDocDeleted((docId) => this._handleRemoteDocDeleted(docId));
|
|
49
55
|
patches.onTrackDocs(this._handleDocsTracked.bind(this));
|
|
50
56
|
patches.onUntrackDocs(this._handleDocsUntracked.bind(this));
|
|
51
57
|
patches.onDeleteDoc(this._handleDocDeleted.bind(this));
|
|
@@ -183,6 +189,10 @@ class PatchesSync {
|
|
|
183
189
|
doc.updateSyncing(null);
|
|
184
190
|
}
|
|
185
191
|
} catch (err) {
|
|
192
|
+
if (this._isDocDeletedError(err)) {
|
|
193
|
+
await this._handleRemoteDocDeleted(docId);
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
186
196
|
console.error(`Error syncing doc ${docId}:`, err);
|
|
187
197
|
this.onError.emit(err, { docId });
|
|
188
198
|
if (doc) {
|
|
@@ -221,6 +231,10 @@ class PatchesSync {
|
|
|
221
231
|
pending = await this.store.getPendingChanges(docId);
|
|
222
232
|
}
|
|
223
233
|
} catch (err) {
|
|
234
|
+
if (this._isDocDeletedError(err)) {
|
|
235
|
+
await this._handleRemoteDocDeleted(docId);
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
224
238
|
console.error(`Flush failed for doc ${docId}:`, err);
|
|
225
239
|
this.onError.emit(err, { docId });
|
|
226
240
|
throw err;
|
|
@@ -328,6 +342,26 @@ class PatchesSync {
|
|
|
328
342
|
if (!this.trackedDocs.has(docId)) return;
|
|
329
343
|
await this.flushDoc(docId);
|
|
330
344
|
}
|
|
345
|
+
/**
|
|
346
|
+
* Unified handler for remote document deletion (both real-time notifications and offline discovery).
|
|
347
|
+
* Cleans up local state and notifies the application with any pending changes that were lost.
|
|
348
|
+
*/
|
|
349
|
+
async _handleRemoteDocDeleted(docId) {
|
|
350
|
+
const pendingChanges = await this.store.getPendingChanges(docId);
|
|
351
|
+
const doc = this.patches.getOpenDoc(docId);
|
|
352
|
+
if (doc) {
|
|
353
|
+
await this.patches.closeDoc(docId);
|
|
354
|
+
}
|
|
355
|
+
this.trackedDocs.delete(docId);
|
|
356
|
+
await this.store.confirmDeleteDoc(docId);
|
|
357
|
+
await this.onRemoteDocDeleted.emit(docId, pendingChanges);
|
|
358
|
+
}
|
|
359
|
+
/**
|
|
360
|
+
* Helper to detect DOC_DELETED (410) errors from the server.
|
|
361
|
+
*/
|
|
362
|
+
_isDocDeletedError(err) {
|
|
363
|
+
return typeof err === "object" && err !== null && "code" in err && err.code === 410;
|
|
364
|
+
}
|
|
331
365
|
}
|
|
332
366
|
_init = __decoratorStart(null);
|
|
333
367
|
__decorateElement(_init, 1, "syncDoc", _syncDoc_dec, PatchesSync);
|
package/dist/net/error.d.ts
CHANGED
|
@@ -1,7 +1,17 @@
|
|
|
1
1
|
declare class StatusError extends Error {
|
|
2
2
|
code: number;
|
|
3
|
-
|
|
3
|
+
data?: Record<string, any> | undefined;
|
|
4
|
+
constructor(code: number, message: string, data?: Record<string, any> | undefined);
|
|
4
5
|
}
|
|
6
|
+
/**
|
|
7
|
+
* Standard error codes for Patches operations.
|
|
8
|
+
*/
|
|
9
|
+
declare const ErrorCodes: {
|
|
10
|
+
/** Document was deleted (tombstone exists). */
|
|
11
|
+
readonly DOC_DELETED: 410;
|
|
12
|
+
/** Document not found (never existed). */
|
|
13
|
+
readonly DOC_NOT_FOUND: 404;
|
|
14
|
+
};
|
|
5
15
|
/**
|
|
6
16
|
* Error thrown when the JSON-RPC client receives a message that cannot be parsed as JSON.
|
|
7
17
|
* This typically indicates a server-side error (HTTP 500, load balancer timeout, etc.)
|
|
@@ -13,4 +23,4 @@ declare class JSONRPCParseError extends Error {
|
|
|
13
23
|
constructor(rawMessage: string, parseError: Error);
|
|
14
24
|
}
|
|
15
25
|
|
|
16
|
-
export { JSONRPCParseError, StatusError };
|
|
26
|
+
export { ErrorCodes, JSONRPCParseError, StatusError };
|
package/dist/net/error.js
CHANGED
|
@@ -1,10 +1,17 @@
|
|
|
1
1
|
import "../chunk-IZ2YBCUP.js";
|
|
2
2
|
class StatusError extends Error {
|
|
3
|
-
constructor(code, message) {
|
|
3
|
+
constructor(code, message, data) {
|
|
4
4
|
super(message);
|
|
5
5
|
this.code = code;
|
|
6
|
+
this.data = data;
|
|
6
7
|
}
|
|
7
8
|
}
|
|
9
|
+
const ErrorCodes = {
|
|
10
|
+
/** Document was deleted (tombstone exists). */
|
|
11
|
+
DOC_DELETED: 410,
|
|
12
|
+
/** Document not found (never existed). */
|
|
13
|
+
DOC_NOT_FOUND: 404
|
|
14
|
+
};
|
|
8
15
|
class JSONRPCParseError extends Error {
|
|
9
16
|
rawMessage;
|
|
10
17
|
parseError;
|
|
@@ -17,6 +24,7 @@ class JSONRPCParseError extends Error {
|
|
|
17
24
|
}
|
|
18
25
|
}
|
|
19
26
|
export {
|
|
27
|
+
ErrorCodes,
|
|
20
28
|
JSONRPCParseError,
|
|
21
29
|
StatusError
|
|
22
30
|
};
|
package/dist/net/index.d.ts
CHANGED
|
@@ -6,7 +6,7 @@ export { ConnectionSignalSubscriber, JSONRPCServer, MessageHandler } from './pro
|
|
|
6
6
|
export { AwarenessUpdateNotificationParams, ClientTransport, ConnectionState, JsonRpcNotification, JsonRpcRequest, JsonRpcResponse, ListOptions, Message, PatchesAPI, PatchesNotificationParams, ServerTransport, SignalNotificationParams } from './protocol/types.js';
|
|
7
7
|
export { rpcError, rpcNotification, rpcResponse } from './protocol/utils.js';
|
|
8
8
|
export { PatchesState, SyncingState } from './types.js';
|
|
9
|
-
export { Access, AuthContext, AuthorizationProvider, allowAll, denyAll } from './websocket/AuthorizationProvider.js';
|
|
9
|
+
export { Access, AuthContext, AuthorizationProvider, allowAll, assertNotDeleted, denyAll } from './websocket/AuthorizationProvider.js';
|
|
10
10
|
export { onlineState } from './websocket/onlineState.js';
|
|
11
11
|
export { PatchesWebSocket } from './websocket/PatchesWebSocket.js';
|
|
12
12
|
export { RPCServer, RPCServerOptions } from './websocket/RPCServer.js';
|
|
@@ -19,9 +19,9 @@ import '../algorithms/shared/changeBatching.js';
|
|
|
19
19
|
import '../client/Patches.js';
|
|
20
20
|
import '../client/PatchesDoc.js';
|
|
21
21
|
import '../client/PatchesStore.js';
|
|
22
|
+
import '../server/types.js';
|
|
22
23
|
import '../server/PatchesBranchManager.js';
|
|
23
24
|
import '../server/PatchesServer.js';
|
|
24
|
-
import '../server/types.js';
|
|
25
25
|
import '../compression/index.js';
|
|
26
26
|
import '../algorithms/shared/lz.js';
|
|
27
27
|
import '../json-patch/types.js';
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Signal, Unsubscriber } from '../../event-signal.js';
|
|
2
2
|
import { AuthContext } from '../websocket/AuthorizationProvider.js';
|
|
3
3
|
import { JsonRpcNotification, Message, JsonRpcResponse } from './types.js';
|
|
4
|
+
import '../../server/types.js';
|
|
4
5
|
import '../../types.js';
|
|
5
6
|
import '../../json-patch/JSONPatch.js';
|
|
6
7
|
import '@dabble/delta';
|
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
import { PatchesStoreBackend } from '../../server/types.js';
|
|
2
|
+
import '../../types.js';
|
|
3
|
+
import '../../json-patch/JSONPatch.js';
|
|
4
|
+
import '@dabble/delta';
|
|
5
|
+
import '../../json-patch/types.js';
|
|
6
|
+
|
|
1
7
|
/**
|
|
2
8
|
* Access level requested for an operation.
|
|
3
9
|
*
|
|
@@ -19,6 +25,10 @@ interface AuthContext {
|
|
|
19
25
|
* a certain action on a document. Implementations are entirely application-
|
|
20
26
|
* specific – they may look at a JWT decoded during the WebSocket handshake,
|
|
21
27
|
* consult an ACL service, inspect the actual RPC method, etc.
|
|
28
|
+
*
|
|
29
|
+
* **Tombstone checking:** If your store implements tombstones, call
|
|
30
|
+
* `assertNotDeleted(store, docId)` at the start of canAccess() to automatically
|
|
31
|
+
* reject access to deleted documents with a 410 error.
|
|
22
32
|
*/
|
|
23
33
|
interface AuthorizationProvider<T extends AuthContext = AuthContext> {
|
|
24
34
|
/**
|
|
@@ -51,5 +61,20 @@ declare const allowAll: AuthorizationProvider;
|
|
|
51
61
|
* Use this as the default to ensure security by default.
|
|
52
62
|
*/
|
|
53
63
|
declare const denyAll: AuthorizationProvider;
|
|
64
|
+
/**
|
|
65
|
+
* Helper for AuthorizationProvider implementations.
|
|
66
|
+
* Call this in your canAccess() to check for document tombstones.
|
|
67
|
+
* Throws StatusError(410) if the document has been deleted.
|
|
68
|
+
*
|
|
69
|
+
* @example
|
|
70
|
+
* const myAuthProvider: AuthorizationProvider = {
|
|
71
|
+
* async canAccess(ctx, docId, kind, method, params) {
|
|
72
|
+
* await assertNotDeleted(store, docId);
|
|
73
|
+
* // ... rest of your auth logic
|
|
74
|
+
* return true;
|
|
75
|
+
* }
|
|
76
|
+
* };
|
|
77
|
+
*/
|
|
78
|
+
declare function assertNotDeleted(store: PatchesStoreBackend, docId: string): Promise<void>;
|
|
54
79
|
|
|
55
|
-
export { type Access, type AuthContext, type AuthorizationProvider, allowAll, denyAll };
|
|
80
|
+
export { type Access, type AuthContext, type AuthorizationProvider, allowAll, assertNotDeleted, denyAll };
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import "../../chunk-IZ2YBCUP.js";
|
|
2
|
+
import { ErrorCodes, StatusError } from "../error.js";
|
|
2
3
|
const allowAll = {
|
|
3
4
|
canAccess: () => true
|
|
4
5
|
};
|
|
@@ -8,7 +9,17 @@ const denyAll = {
|
|
|
8
9
|
return false;
|
|
9
10
|
}
|
|
10
11
|
};
|
|
12
|
+
async function assertNotDeleted(store, docId) {
|
|
13
|
+
const tombstone = await store.getTombstone?.(docId);
|
|
14
|
+
if (tombstone) {
|
|
15
|
+
throw new StatusError(ErrorCodes.DOC_DELETED, `Document ${docId} was deleted`, {
|
|
16
|
+
deletedAt: tombstone.deletedAt,
|
|
17
|
+
lastRev: tombstone.lastRev
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
}
|
|
11
21
|
export {
|
|
12
22
|
allowAll,
|
|
23
|
+
assertNotDeleted,
|
|
13
24
|
denyAll
|
|
14
25
|
};
|
|
@@ -82,6 +82,14 @@ declare class RPCServer {
|
|
|
82
82
|
deleteDoc(params: {
|
|
83
83
|
docId: string;
|
|
84
84
|
}, ctx?: AuthContext): Promise<void>;
|
|
85
|
+
/**
|
|
86
|
+
* Removes the tombstone for a deleted document, allowing it to be recreated.
|
|
87
|
+
* @param params - The undelete parameters
|
|
88
|
+
* @param params.docId - The ID of the document to undelete
|
|
89
|
+
*/
|
|
90
|
+
undeleteDoc(params: {
|
|
91
|
+
docId: string;
|
|
92
|
+
}, ctx?: AuthContext): Promise<boolean>;
|
|
85
93
|
listVersions(params: {
|
|
86
94
|
docId: string;
|
|
87
95
|
options?: ListVersionsOptions;
|
|
@@ -25,6 +25,7 @@ class RPCServer {
|
|
|
25
25
|
this.rpc.registerMethod("getChangesSince", this.getChangesSince.bind(this));
|
|
26
26
|
this.rpc.registerMethod("commitChanges", this.commitChanges.bind(this));
|
|
27
27
|
this.rpc.registerMethod("deleteDoc", this.deleteDoc.bind(this));
|
|
28
|
+
this.rpc.registerMethod("undeleteDoc", this.undeleteDoc.bind(this));
|
|
28
29
|
if (this.history) {
|
|
29
30
|
this.rpc.registerMethod("listVersions", this.listVersions.bind(this));
|
|
30
31
|
this.rpc.registerMethod("createVersion", this.createVersion.bind(this));
|
|
@@ -95,6 +96,16 @@ class RPCServer {
|
|
|
95
96
|
await this.assertWrite(ctx, docId, "deleteDoc", params);
|
|
96
97
|
await this.patches.deleteDoc(docId, ctx?.clientId);
|
|
97
98
|
}
|
|
99
|
+
/**
|
|
100
|
+
* Removes the tombstone for a deleted document, allowing it to be recreated.
|
|
101
|
+
* @param params - The undelete parameters
|
|
102
|
+
* @param params.docId - The ID of the document to undelete
|
|
103
|
+
*/
|
|
104
|
+
async undeleteDoc(params, ctx) {
|
|
105
|
+
const { docId } = params;
|
|
106
|
+
await this.assertWrite(ctx, docId, "undeleteDoc", params);
|
|
107
|
+
return this.patches.undeleteDoc(docId);
|
|
108
|
+
}
|
|
98
109
|
// ---------------------------------------------------------------------------
|
|
99
110
|
// History Manager wrappers
|
|
100
111
|
// ---------------------------------------------------------------------------
|
|
@@ -6,9 +6,9 @@ import '../../types.js';
|
|
|
6
6
|
import '../../json-patch/JSONPatch.js';
|
|
7
7
|
import '@dabble/delta';
|
|
8
8
|
import '../../json-patch/types.js';
|
|
9
|
+
import '../../server/types.js';
|
|
9
10
|
import '../../server/PatchesBranchManager.js';
|
|
10
11
|
import '../../server/PatchesServer.js';
|
|
11
|
-
import '../../server/types.js';
|
|
12
12
|
import '../../compression/index.js';
|
|
13
13
|
import '../../algorithms/shared/lz.js';
|
|
14
14
|
import '../../server/PatchesHistoryManager.js';
|
|
@@ -30,6 +30,7 @@ declare class WebSocketServer {
|
|
|
30
30
|
constructor(transport: ServerTransport, rpcServer: RPCServer);
|
|
31
31
|
/**
|
|
32
32
|
* Subscribes the client to one or more documents to receive real-time updates.
|
|
33
|
+
* If a document has been deleted (tombstone exists), sends immediate docDeleted notification.
|
|
33
34
|
* @param connectionId - The ID of the connection making the request
|
|
34
35
|
* @param params - The subscription parameters
|
|
35
36
|
* @param params.ids - Document ID or IDs to subscribe to
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import "../../chunk-IZ2YBCUP.js";
|
|
2
|
+
import { ErrorCodes, StatusError } from "../error.js";
|
|
2
3
|
import { denyAll } from "./AuthorizationProvider.js";
|
|
3
4
|
class WebSocketServer {
|
|
4
5
|
transport;
|
|
@@ -26,6 +27,7 @@ class WebSocketServer {
|
|
|
26
27
|
}
|
|
27
28
|
/**
|
|
28
29
|
* Subscribes the client to one or more documents to receive real-time updates.
|
|
30
|
+
* If a document has been deleted (tombstone exists), sends immediate docDeleted notification.
|
|
29
31
|
* @param connectionId - The ID of the connection making the request
|
|
30
32
|
* @param params - The subscription parameters
|
|
31
33
|
* @param params.ids - Document ID or IDs to subscribe to
|
|
@@ -41,7 +43,17 @@ class WebSocketServer {
|
|
|
41
43
|
if (await this.auth.canAccess(ctx, id, "read", "subscribe", params)) {
|
|
42
44
|
allowed.push(id);
|
|
43
45
|
}
|
|
44
|
-
} catch {
|
|
46
|
+
} catch (err) {
|
|
47
|
+
if (err instanceof StatusError && err.code === ErrorCodes.DOC_DELETED) {
|
|
48
|
+
this.transport.send(
|
|
49
|
+
ctx.clientId,
|
|
50
|
+
JSON.stringify({
|
|
51
|
+
jsonrpc: "2.0",
|
|
52
|
+
method: "docDeleted",
|
|
53
|
+
params: { docId: id }
|
|
54
|
+
})
|
|
55
|
+
);
|
|
56
|
+
}
|
|
45
57
|
}
|
|
46
58
|
})
|
|
47
59
|
);
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { OpsCompressor } from '../compression/index.js';
|
|
2
|
-
import { Change, ListChangesOptions, VersionMetadata, EditableVersionMetadata, ListVersionsOptions } from '../types.js';
|
|
2
|
+
import { Change, ListChangesOptions, VersionMetadata, EditableVersionMetadata, ListVersionsOptions, DocumentTombstone } from '../types.js';
|
|
3
3
|
import { PatchesStoreBackend } from './types.js';
|
|
4
4
|
import '../algorithms/shared/lz.js';
|
|
5
5
|
import '../json-patch/types.js';
|
|
@@ -39,6 +39,9 @@ declare class CompressedStoreBackend implements PatchesStoreBackend {
|
|
|
39
39
|
listVersions(docId: string, options: ListVersionsOptions): Promise<VersionMetadata[]>;
|
|
40
40
|
loadVersionState(docId: string, versionId: string): Promise<any | undefined>;
|
|
41
41
|
deleteDoc(docId: string): Promise<void>;
|
|
42
|
+
createTombstone(tombstone: DocumentTombstone): Promise<void>;
|
|
43
|
+
getTombstone(docId: string): Promise<DocumentTombstone | undefined>;
|
|
44
|
+
removeTombstone(docId: string): Promise<void>;
|
|
42
45
|
}
|
|
43
46
|
|
|
44
47
|
export { CompressedStoreBackend };
|
|
@@ -73,6 +73,15 @@ class CompressedStoreBackend {
|
|
|
73
73
|
async deleteDoc(docId) {
|
|
74
74
|
return this.store.deleteDoc(docId);
|
|
75
75
|
}
|
|
76
|
+
async createTombstone(tombstone) {
|
|
77
|
+
return this.store.createTombstone(tombstone);
|
|
78
|
+
}
|
|
79
|
+
async getTombstone(docId) {
|
|
80
|
+
return this.store.getTombstone(docId);
|
|
81
|
+
}
|
|
82
|
+
async removeTombstone(docId) {
|
|
83
|
+
return this.store.removeTombstone(docId);
|
|
84
|
+
}
|
|
76
85
|
}
|
|
77
86
|
export {
|
|
78
87
|
CompressedStoreBackend
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Signal } from '../event-signal.js';
|
|
2
2
|
import { PatchesStoreBackend } from './types.js';
|
|
3
|
-
import { Change, PatchesState, ChangeInput, CommitChangesOptions, ChangeMutator, EditableVersionMetadata } from '../types.js';
|
|
3
|
+
import { Change, PatchesState, ChangeInput, CommitChangesOptions, ChangeMutator, DeleteDocOptions, EditableVersionMetadata } from '../types.js';
|
|
4
4
|
import { OpsCompressor } from '../compression/index.js';
|
|
5
5
|
import '../json-patch/JSONPatch.js';
|
|
6
6
|
import '@dabble/delta';
|
|
@@ -89,8 +89,15 @@ declare class PatchesServer {
|
|
|
89
89
|
* Deletes a document.
|
|
90
90
|
* @param docId The document ID.
|
|
91
91
|
* @param originClientId - The ID of the client that initiated the delete operation.
|
|
92
|
+
* @param options - Optional deletion settings (e.g., skipTombstone for testing).
|
|
92
93
|
*/
|
|
93
|
-
deleteDoc(docId: string, originClientId?: string): Promise<void>;
|
|
94
|
+
deleteDoc(docId: string, originClientId?: string, options?: DeleteDocOptions): Promise<void>;
|
|
95
|
+
/**
|
|
96
|
+
* Removes the tombstone for a deleted document, allowing it to be recreated.
|
|
97
|
+
* @param docId The document ID.
|
|
98
|
+
* @returns True if tombstone was found and removed, false if no tombstone existed.
|
|
99
|
+
*/
|
|
100
|
+
undeleteDoc(docId: string): Promise<boolean>;
|
|
94
101
|
/**
|
|
95
102
|
* Captures the current state of a document as a new version.
|
|
96
103
|
* @param docId The document ID.
|
|
@@ -7,6 +7,7 @@ import { applyChanges } from "../algorithms/shared/applyChanges.js";
|
|
|
7
7
|
import { createChange } from "../data/change.js";
|
|
8
8
|
import { signal } from "../event-signal.js";
|
|
9
9
|
import { createJSONPatch } from "../json-patch/createJSONPatch.js";
|
|
10
|
+
import { getISO } from "../utils/dates.js";
|
|
10
11
|
import { CompressedStoreBackend } from "./CompressedStoreBackend.js";
|
|
11
12
|
class PatchesServer {
|
|
12
13
|
sessionTimeoutMillis;
|
|
@@ -100,11 +101,37 @@ class PatchesServer {
|
|
|
100
101
|
* Deletes a document.
|
|
101
102
|
* @param docId The document ID.
|
|
102
103
|
* @param originClientId - The ID of the client that initiated the delete operation.
|
|
104
|
+
* @param options - Optional deletion settings (e.g., skipTombstone for testing).
|
|
103
105
|
*/
|
|
104
|
-
async deleteDoc(docId, originClientId) {
|
|
106
|
+
async deleteDoc(docId, originClientId, options) {
|
|
107
|
+
if (this.store.createTombstone && !options?.skipTombstone) {
|
|
108
|
+
const { rev: lastRev } = await this.getDoc(docId);
|
|
109
|
+
await this.store.createTombstone({
|
|
110
|
+
docId,
|
|
111
|
+
deletedAt: getISO(),
|
|
112
|
+
lastRev,
|
|
113
|
+
deletedByClientId: originClientId
|
|
114
|
+
});
|
|
115
|
+
}
|
|
105
116
|
await this.store.deleteDoc(docId);
|
|
106
117
|
await this.onDocDeleted.emit(docId, originClientId);
|
|
107
118
|
}
|
|
119
|
+
/**
|
|
120
|
+
* Removes the tombstone for a deleted document, allowing it to be recreated.
|
|
121
|
+
* @param docId The document ID.
|
|
122
|
+
* @returns True if tombstone was found and removed, false if no tombstone existed.
|
|
123
|
+
*/
|
|
124
|
+
async undeleteDoc(docId) {
|
|
125
|
+
if (!this.store.removeTombstone) {
|
|
126
|
+
return false;
|
|
127
|
+
}
|
|
128
|
+
const tombstone = await this.store.getTombstone?.(docId);
|
|
129
|
+
if (!tombstone) {
|
|
130
|
+
return false;
|
|
131
|
+
}
|
|
132
|
+
await this.store.removeTombstone(docId);
|
|
133
|
+
return true;
|
|
134
|
+
}
|
|
108
135
|
// === Version Operations ===
|
|
109
136
|
/**
|
|
110
137
|
* Captures the current state of a document as a new version.
|
package/dist/server/types.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Change, ListChangesOptions, PatchesState, VersionMetadata, EditableVersionMetadata, ListVersionsOptions, Branch } from '../types.js';
|
|
1
|
+
import { Change, ListChangesOptions, PatchesState, VersionMetadata, EditableVersionMetadata, ListVersionsOptions, DocumentTombstone, Branch } from '../types.js';
|
|
2
2
|
import '../json-patch/JSONPatch.js';
|
|
3
3
|
import '@dabble/delta';
|
|
4
4
|
import '../json-patch/types.js';
|
|
@@ -36,6 +36,12 @@ interface PatchesStoreBackend {
|
|
|
36
36
|
loadVersionChanges(docId: string, versionId: string): Promise<Change[]>;
|
|
37
37
|
/** Deletes a document. */
|
|
38
38
|
deleteDoc(docId: string): Promise<void>;
|
|
39
|
+
/** Creates a tombstone for a deleted document. Called before deleteDoc() to preserve deletion metadata. */
|
|
40
|
+
createTombstone(tombstone: DocumentTombstone): Promise<void>;
|
|
41
|
+
/** Retrieves a tombstone for a document if it exists. Returns undefined if the document was never deleted or tombstone has expired. */
|
|
42
|
+
getTombstone(docId: string): Promise<DocumentTombstone | undefined>;
|
|
43
|
+
/** Removes a tombstone (for undelete or TTL cleanup). */
|
|
44
|
+
removeTombstone(docId: string): Promise<void>;
|
|
39
45
|
}
|
|
40
46
|
/**
|
|
41
47
|
* Extends PatchesStoreBackend with methods specifically for managing branches.
|
package/dist/types.d.ts
CHANGED
|
@@ -81,6 +81,30 @@ interface Branch {
|
|
|
81
81
|
[metadata: string]: any;
|
|
82
82
|
}
|
|
83
83
|
type EditableBranchMetadata = Disallowed<Branch, 'id' | 'docId' | 'branchedAtRev' | 'createdAt' | 'status'>;
|
|
84
|
+
/**
|
|
85
|
+
* Represents a tombstone for a deleted document.
|
|
86
|
+
* Tombstones persist after deletion to inform late-connecting clients
|
|
87
|
+
* that a document has been deleted rather than never existing.
|
|
88
|
+
*/
|
|
89
|
+
interface DocumentTombstone {
|
|
90
|
+
/** The ID of the deleted document. */
|
|
91
|
+
docId: string;
|
|
92
|
+
/** ISO timestamp when the document was deleted (UTC with Z). */
|
|
93
|
+
deletedAt: string;
|
|
94
|
+
/** The last revision number before deletion. */
|
|
95
|
+
lastRev: number;
|
|
96
|
+
/** Optional client ID that initiated the deletion. */
|
|
97
|
+
deletedByClientId?: string;
|
|
98
|
+
/** Optional ISO timestamp for automatic tombstone expiration (UTC with Z). */
|
|
99
|
+
expiresAt?: string;
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Options for deleting a document.
|
|
103
|
+
*/
|
|
104
|
+
interface DeleteDocOptions {
|
|
105
|
+
/** Skip creating tombstone (useful for testing/migration scripts). */
|
|
106
|
+
skipTombstone?: boolean;
|
|
107
|
+
}
|
|
84
108
|
/**
|
|
85
109
|
* Metadata, state snapshot, and included changes for a specific version.
|
|
86
110
|
*/
|
|
@@ -194,4 +218,4 @@ type PathProxy<T = any> = IsAny<T> extends true ? DeepPathProxy : {
|
|
|
194
218
|
*/
|
|
195
219
|
type ChangeMutator<T> = (patch: JSONPatch, root: PathProxy<T>) => void;
|
|
196
220
|
|
|
197
|
-
export type { Branch, BranchStatus, Change, ChangeInput, ChangeMutator, CommitChangesOptions, EditableBranchMetadata, EditableVersionMetadata, ListChangesOptions, ListVersionsOptions, PatchesSnapshot, PatchesState, PathProxy, SyncingState, VersionMetadata };
|
|
221
|
+
export type { Branch, BranchStatus, Change, ChangeInput, ChangeMutator, CommitChangesOptions, DeleteDocOptions, DocumentTombstone, EditableBranchMetadata, EditableVersionMetadata, ListChangesOptions, ListVersionsOptions, PatchesSnapshot, PatchesState, PathProxy, SyncingState, VersionMetadata };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dabble/patches",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.13",
|
|
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": {
|