@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 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 };
@@ -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);
@@ -1,7 +1,17 @@
1
1
  declare class StatusError extends Error {
2
2
  code: number;
3
- constructor(code: number, message: string);
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
  };
@@ -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.
@@ -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.12",
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": {