@dabble/patches 0.3.2 → 0.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/dist/algorithms/client/makeChange.d.ts +2 -3
  2. package/dist/algorithms/client/makeChange.js +1 -1
  3. package/dist/algorithms/server/getSnapshotAtRevision.d.ts +1 -1
  4. package/dist/algorithms/server/getStateAtRevision.d.ts +1 -1
  5. package/dist/algorithms/server/handleOfflineSessionsAndBatches.d.ts +1 -1
  6. package/dist/client/InMemoryStore.js +1 -3
  7. package/dist/client/IndexedDBStore.js +345 -342
  8. package/dist/client/Patches.js +156 -156
  9. package/dist/client/PatchesDoc.d.ts +4 -5
  10. package/dist/client/PatchesDoc.js +16 -13
  11. package/dist/client/PatchesHistoryClient.js +12 -8
  12. package/dist/json-patch/JSONPatch.js +2 -0
  13. package/dist/json-patch/createJSONPatch.d.ts +15 -18
  14. package/dist/json-patch/createJSONPatch.js +18 -20
  15. package/dist/json-patch/pathProxy.d.ts +22 -0
  16. package/dist/json-patch/pathProxy.js +50 -0
  17. package/dist/json-patch/utils/getType.d.ts +1 -1
  18. package/dist/net/PatchesSync.js +307 -303
  19. package/dist/net/error.js +1 -0
  20. package/dist/net/protocol/JSONRPCClient.js +4 -3
  21. package/dist/net/protocol/JSONRPCServer.js +6 -8
  22. package/dist/net/webrtc/WebRTCAwareness.js +12 -7
  23. package/dist/net/webrtc/WebRTCTransport.d.ts +1 -1
  24. package/dist/net/webrtc/WebRTCTransport.js +27 -21
  25. package/dist/net/websocket/PatchesWebSocket.js +7 -2
  26. package/dist/net/websocket/RPCServer.js +5 -0
  27. package/dist/net/websocket/SignalingService.js +1 -3
  28. package/dist/net/websocket/WebSocketServer.js +2 -0
  29. package/dist/net/websocket/WebSocketTransport.js +21 -19
  30. package/dist/net/websocket/onlineState.js +2 -2
  31. package/dist/server/PatchesBranchManager.js +2 -0
  32. package/dist/server/PatchesHistoryManager.js +2 -0
  33. package/dist/server/PatchesServer.d.ts +2 -2
  34. package/dist/server/PatchesServer.js +7 -5
  35. package/dist/types.d.ts +15 -6
  36. package/dist/types.js +1 -1
  37. package/package.json +3 -2
  38. package/dist/json-patch/patchProxy.d.ts +0 -42
  39. package/dist/json-patch/patchProxy.js +0 -126
@@ -41,170 +41,170 @@ import { PatchesDoc } from './PatchesDoc.js';
41
41
  * Can be used standalone or with PatchesSync for network synchronization.
42
42
  */
43
43
  let Patches = (() => {
44
- var _a;
45
44
  let _instanceExtraInitializers = [];
46
45
  let _openDoc_decorators;
47
- return _a = class Patches {
48
- constructor(opts) {
49
- this.options = __runInitializers(this, _instanceExtraInitializers);
50
- this.docs = new Map();
51
- this.trackedDocs = new Set();
52
- // Public signals
53
- this.onError = signal();
54
- this.onServerCommit = signal();
55
- this.onTrackDocs = signal();
56
- this.onUntrackDocs = signal();
57
- this.onDeleteDoc = signal();
58
- this.onChange = signal();
59
- this.options = opts;
60
- this.store = opts.store;
61
- this.docOptions = opts.docOptions ?? {};
62
- this.store.listDocs().then(docs => {
63
- this.trackDocs(docs.map(({ docId }) => docId));
64
- });
65
- }
66
- // --- Public API Methods ---
67
- /**
68
- * Tracks the given document IDs, adding them to the set of tracked documents and notifying listeners.
69
- * Tracked docs are kept in sync with the server, even when not open locally.
70
- * This allows for background syncing and updates of unopened documents.
71
- * @param docIds - Array of document IDs to track.
72
- */
73
- async trackDocs(docIds) {
74
- docIds = docIds.filter(id => !this.trackedDocs.has(id));
75
- if (!docIds.length)
76
- return;
77
- docIds.forEach(this.trackedDocs.add, this.trackedDocs);
78
- this.onTrackDocs.emit(docIds);
79
- await this.store.trackDocs(docIds);
80
- }
81
- /**
82
- * Untracks the given document IDs, removing them from the set of tracked documents and notifying listeners.
83
- * Untracked docs will no longer be kept in sync with the server, even if not open locally.
84
- * Closes any open docs and removes them from the store.
85
- * @param docIds - Array of document IDs to untrack.
86
- */
87
- async untrackDocs(docIds) {
88
- docIds = docIds.filter(id => this.trackedDocs.has(id));
89
- if (!docIds.length)
90
- return;
91
- docIds.forEach(this.trackedDocs.delete, this.trackedDocs);
92
- this.onUntrackDocs.emit(docIds);
93
- // Close any open PatchesDoc instances first
94
- const closedPromises = docIds.filter(id => this.docs.has(id)).map(id => this.closeDoc(id)); // closeDoc removes from this.docs map
95
- await Promise.all(closedPromises);
96
- // Remove from store
97
- await this.store.untrackDocs(docIds);
98
- }
99
- /**
100
- * Opens a document by ID, loading its state from the store and setting up change listeners.
101
- * If the doc is already open, returns the existing instance.
102
- * @param docId - The document ID to open.
103
- * @param opts - Optional metadata to merge with the doc's metadata.
104
- * @returns The opened PatchesDoc instance.
105
- */
106
- async openDoc(docId, opts = {}) {
107
- const existing = this.docs.get(docId);
108
- if (existing)
109
- return existing.doc;
110
- // Ensure the doc is tracked before proceeding
111
- await this.trackDocs([docId]);
112
- // Load initial state from store
113
- const snapshot = await this.store.getDoc(docId);
114
- const initialState = (snapshot?.state ?? {});
115
- const mergedMetadata = { ...this.options.metadata, ...opts.metadata };
116
- const doc = new PatchesDoc(initialState, mergedMetadata, this.docOptions);
117
- doc.setId(docId);
118
- if (snapshot) {
119
- doc.import(snapshot);
120
- }
121
- // Set up local listener -> store
122
- const unsubscribe = doc.onChange(changes => this._savePendingChanges(docId, changes));
123
- this.docs.set(docId, { doc, unsubscribe });
124
- return doc;
125
- }
126
- /**
127
- * Closes an open document by ID, removing listeners and optionally untracking it.
128
- * @param docId - The document ID to close.
129
- * @param options - Optional: set untrack to true to also untrack the doc.
130
- */
131
- async closeDoc(docId, { untrack = false } = {}) {
132
- const managed = this.docs.get(docId);
133
- if (managed) {
134
- managed.unsubscribe();
135
- this.docs.delete(docId);
136
- if (untrack) {
137
- await this.untrackDocs([docId]);
138
- }
139
- }
46
+ return class Patches {
47
+ static {
48
+ const _metadata = typeof Symbol === "function" && Symbol.metadata ? Object.create(null) : void 0;
49
+ _openDoc_decorators = [singleInvocation(true)];
50
+ __esDecorate(this, null, _openDoc_decorators, { kind: "method", name: "openDoc", static: false, private: false, access: { has: obj => "openDoc" in obj, get: obj => obj.openDoc }, metadata: _metadata }, null, _instanceExtraInitializers);
51
+ if (_metadata) Object.defineProperty(this, Symbol.metadata, { enumerable: true, configurable: true, writable: true, value: _metadata });
52
+ }
53
+ options = __runInitializers(this, _instanceExtraInitializers);
54
+ docs = new Map();
55
+ docOptions;
56
+ store;
57
+ trackedDocs = new Set();
58
+ // Public signals
59
+ onError = signal();
60
+ onServerCommit = signal();
61
+ onTrackDocs = signal();
62
+ onUntrackDocs = signal();
63
+ onDeleteDoc = signal();
64
+ onChange = signal();
65
+ constructor(opts) {
66
+ this.options = opts;
67
+ this.store = opts.store;
68
+ this.docOptions = opts.docOptions ?? {};
69
+ this.store.listDocs().then(docs => {
70
+ this.trackDocs(docs.map(({ docId }) => docId));
71
+ });
72
+ }
73
+ // --- Public API Methods ---
74
+ /**
75
+ * Tracks the given document IDs, adding them to the set of tracked documents and notifying listeners.
76
+ * Tracked docs are kept in sync with the server, even when not open locally.
77
+ * This allows for background syncing and updates of unopened documents.
78
+ * @param docIds - Array of document IDs to track.
79
+ */
80
+ async trackDocs(docIds) {
81
+ docIds = docIds.filter(id => !this.trackedDocs.has(id));
82
+ if (!docIds.length)
83
+ return;
84
+ docIds.forEach(this.trackedDocs.add, this.trackedDocs);
85
+ this.onTrackDocs.emit(docIds);
86
+ await this.store.trackDocs(docIds);
87
+ }
88
+ /**
89
+ * Untracks the given document IDs, removing them from the set of tracked documents and notifying listeners.
90
+ * Untracked docs will no longer be kept in sync with the server, even if not open locally.
91
+ * Closes any open docs and removes them from the store.
92
+ * @param docIds - Array of document IDs to untrack.
93
+ */
94
+ async untrackDocs(docIds) {
95
+ docIds = docIds.filter(id => this.trackedDocs.has(id));
96
+ if (!docIds.length)
97
+ return;
98
+ docIds.forEach(this.trackedDocs.delete, this.trackedDocs);
99
+ this.onUntrackDocs.emit(docIds);
100
+ // Close any open PatchesDoc instances first
101
+ const closedPromises = docIds.filter(id => this.docs.has(id)).map(id => this.closeDoc(id)); // closeDoc removes from this.docs map
102
+ await Promise.all(closedPromises);
103
+ // Remove from store
104
+ await this.store.untrackDocs(docIds);
105
+ }
106
+ /**
107
+ * Opens a document by ID, loading its state from the store and setting up change listeners.
108
+ * If the doc is already open, returns the existing instance.
109
+ * @param docId - The document ID to open.
110
+ * @param opts - Optional metadata to merge with the doc's metadata.
111
+ * @returns The opened PatchesDoc instance.
112
+ */
113
+ async openDoc(docId, opts = {}) {
114
+ const existing = this.docs.get(docId);
115
+ if (existing)
116
+ return existing.doc;
117
+ // Ensure the doc is tracked before proceeding
118
+ await this.trackDocs([docId]);
119
+ // Load initial state from store
120
+ const snapshot = await this.store.getDoc(docId);
121
+ const initialState = (snapshot?.state ?? {});
122
+ const mergedMetadata = { ...this.options.metadata, ...opts.metadata };
123
+ const doc = new PatchesDoc(initialState, mergedMetadata, this.docOptions);
124
+ doc.setId(docId);
125
+ if (snapshot) {
126
+ doc.import(snapshot);
140
127
  }
141
- /**
142
- * Deletes a document by ID, closing it if open, untracking it, and removing it from the store.
143
- * Emits the onDeleteDoc signal.
144
- * @param docId - The document ID to delete.
145
- */
146
- async deleteDoc(docId) {
147
- // Close if open locally
148
- if (this.docs.has(docId)) {
149
- await this.closeDoc(docId);
150
- }
151
- // Unsubscribe from server if tracked (deletes the doc from the store before the next step adds a tombstone)
152
- if (this.trackedDocs.has(docId)) {
128
+ // Set up local listener -> store
129
+ const unsubscribe = doc.onChange(changes => this._savePendingChanges(docId, changes));
130
+ this.docs.set(docId, { doc, unsubscribe });
131
+ return doc;
132
+ }
133
+ /**
134
+ * Closes an open document by ID, removing listeners and optionally untracking it.
135
+ * @param docId - The document ID to close.
136
+ * @param options - Optional: set untrack to true to also untrack the doc.
137
+ */
138
+ async closeDoc(docId, { untrack = false } = {}) {
139
+ const managed = this.docs.get(docId);
140
+ if (managed) {
141
+ managed.unsubscribe();
142
+ this.docs.delete(docId);
143
+ if (untrack) {
153
144
  await this.untrackDocs([docId]);
154
145
  }
155
- // Mark document as deleted in store (adds a tombstone until sync commits it)
156
- await this.store.deleteDoc(docId);
157
- this.onDeleteDoc.emit(docId);
158
146
  }
159
- /**
160
- * Gets an open document instance by ID, if it exists.
161
- * Used by PatchesSync for applying server changes to open docs.
162
- * @param docId - The document ID to get.
163
- * @returns The PatchesDoc instance or undefined if not open.
164
- */
165
- getOpenDoc(docId) {
166
- return this.docs.get(docId)?.doc;
147
+ }
148
+ /**
149
+ * Deletes a document by ID, closing it if open, untracking it, and removing it from the store.
150
+ * Emits the onDeleteDoc signal.
151
+ * @param docId - The document ID to delete.
152
+ */
153
+ async deleteDoc(docId) {
154
+ // Close if open locally
155
+ if (this.docs.has(docId)) {
156
+ await this.closeDoc(docId);
167
157
  }
168
- /**
169
- * Closes all open documents and cleans up listeners and store connections.
170
- * Should be called when shutting down the client.
171
- */
172
- close() {
173
- // Clean up local PatchesDoc listeners
174
- this.docs.forEach(managed => managed.unsubscribe());
175
- this.docs.clear();
176
- // Close store connection
177
- this.store.close();
178
- this.onChange.clear();
179
- this.onDeleteDoc.clear();
180
- this.onUntrackDocs.clear();
181
- this.onTrackDocs.clear();
182
- this.onServerCommit.clear();
183
- this.onError.clear();
158
+ // Unsubscribe from server if tracked (deletes the doc from the store before the next step adds a tombstone)
159
+ if (this.trackedDocs.has(docId)) {
160
+ await this.untrackDocs([docId]);
184
161
  }
185
- /**
186
- * Internal handler for saving pending changes to the store.
187
- * @param docId - The document ID to save the changes for.
188
- * @param changes - The changes to save.
189
- */
190
- async _savePendingChanges(docId, changes) {
191
- try {
192
- await this.store.savePendingChanges(docId, changes);
193
- // Only after it is persisted, emit the change (for PatchesSync to flush)
194
- this.onChange.emit(docId, changes);
195
- }
196
- catch (err) {
197
- console.error(`Error saving pending changes for doc ${docId}:`, err);
198
- this.onError.emit(err, { docId });
199
- }
162
+ // Mark document as deleted in store (adds a tombstone until sync commits it)
163
+ await this.store.deleteDoc(docId);
164
+ this.onDeleteDoc.emit(docId);
165
+ }
166
+ /**
167
+ * Gets an open document instance by ID, if it exists.
168
+ * Used by PatchesSync for applying server changes to open docs.
169
+ * @param docId - The document ID to get.
170
+ * @returns The PatchesDoc instance or undefined if not open.
171
+ */
172
+ getOpenDoc(docId) {
173
+ return this.docs.get(docId)?.doc;
174
+ }
175
+ /**
176
+ * Closes all open documents and cleans up listeners and store connections.
177
+ * Should be called when shutting down the client.
178
+ */
179
+ close() {
180
+ // Clean up local PatchesDoc listeners
181
+ this.docs.forEach(managed => managed.unsubscribe());
182
+ this.docs.clear();
183
+ // Close store connection
184
+ this.store.close();
185
+ this.onChange.clear();
186
+ this.onDeleteDoc.clear();
187
+ this.onUntrackDocs.clear();
188
+ this.onTrackDocs.clear();
189
+ this.onServerCommit.clear();
190
+ this.onError.clear();
191
+ }
192
+ /**
193
+ * Internal handler for saving pending changes to the store.
194
+ * @param docId - The document ID to save the changes for.
195
+ * @param changes - The changes to save.
196
+ */
197
+ async _savePendingChanges(docId, changes) {
198
+ try {
199
+ await this.store.savePendingChanges(docId, changes);
200
+ // Only after it is persisted, emit the change (for PatchesSync to flush)
201
+ this.onChange.emit(docId, changes);
200
202
  }
201
- },
202
- (() => {
203
- const _metadata = typeof Symbol === "function" && Symbol.metadata ? Object.create(null) : void 0;
204
- _openDoc_decorators = [singleInvocation(true)];
205
- __esDecorate(_a, null, _openDoc_decorators, { kind: "method", name: "openDoc", static: false, private: false, access: { has: obj => "openDoc" in obj, get: obj => obj.openDoc }, metadata: _metadata }, null, _instanceExtraInitializers);
206
- if (_metadata) Object.defineProperty(_a, Symbol.metadata, { enumerable: true, configurable: true, writable: true, value: _metadata });
207
- })(),
208
- _a;
203
+ catch (err) {
204
+ console.error(`Error saving pending changes for doc ${docId}:`, err);
205
+ this.onError.emit(err, { docId });
206
+ }
207
+ }
208
+ };
209
209
  })();
210
210
  export { Patches };
@@ -1,6 +1,5 @@
1
1
  import { type Unsubscriber } from '../event-signal.js';
2
- import type { JSONPatch } from '../json-patch/JSONPatch.js';
3
- import type { Change, DeepRequired, PatchesSnapshot, SyncingState } from '../types.js';
2
+ import type { Change, ChangeMutator, PatchesSnapshot, SyncingState } from '../types.js';
4
3
  /**
5
4
  * Options for creating a PatchesDoc instance
6
5
  */
@@ -68,10 +67,10 @@ export declare class PatchesDoc<T extends object = object> {
68
67
  setChangeMetadata(metadata: Record<string, any>): void;
69
68
  /**
70
69
  * Applies an update to the local state, generating a patch and adding it to pending changes.
71
- * @param mutator Function modifying a draft state.
72
- * @returns The generated Change object or null if no changes occurred.
70
+ * @param mutator Function that uses JSONPatch methods with type-safe paths.
71
+ * @returns The generated Change objects.
73
72
  */
74
- change(mutator: (draft: DeepRequired<T>, patch: JSONPatch) => void): Change[];
73
+ change(mutator: ChangeMutator<T>): Change[];
75
74
  /**
76
75
  * Returns the pending changes for this document.
77
76
  * @returns The pending changes.
@@ -8,6 +8,20 @@ import { signal } from '../event-signal.js';
8
8
  * changes currently being sent to the server.
9
9
  */
10
10
  export class PatchesDoc {
11
+ _id = null;
12
+ _state;
13
+ _snapshot;
14
+ _changeMetadata = {};
15
+ _syncing = null;
16
+ _maxPayloadBytes;
17
+ /** Subscribe to be notified before local state changes. */
18
+ onBeforeChange = signal();
19
+ /** Subscribe to be notified after local state changes are applied. */
20
+ onChange = signal();
21
+ /** Subscribe to be notified whenever state changes from any source. */
22
+ onUpdate = signal();
23
+ /** Subscribe to be notified when syncing state changes. */
24
+ onSyncing = signal();
11
25
  /**
12
26
  * Creates an instance of PatchesDoc.
13
27
  * @param initialState Optional initial state.
@@ -15,17 +29,6 @@ export class PatchesDoc {
15
29
  * @param options Additional options for the document.
16
30
  */
17
31
  constructor(initialState = {}, initialMetadata = {}, options = {}) {
18
- this._id = null;
19
- this._changeMetadata = {};
20
- this._syncing = null;
21
- /** Subscribe to be notified before local state changes. */
22
- this.onBeforeChange = signal();
23
- /** Subscribe to be notified after local state changes are applied. */
24
- this.onChange = signal();
25
- /** Subscribe to be notified whenever state changes from any source. */
26
- this.onUpdate = signal();
27
- /** Subscribe to be notified when syncing state changes. */
28
- this.onSyncing = signal();
29
32
  this._state = structuredClone(initialState);
30
33
  this._snapshot = { state: this._state, rev: 0, changes: [] };
31
34
  this._changeMetadata = initialMetadata;
@@ -83,8 +86,8 @@ export class PatchesDoc {
83
86
  }
84
87
  /**
85
88
  * Applies an update to the local state, generating a patch and adding it to pending changes.
86
- * @param mutator Function modifying a draft state.
87
- * @returns The generated Change object or null if no changes occurred.
89
+ * @param mutator Function that uses JSONPatch methods with type-safe paths.
90
+ * @returns The generated Change objects.
88
91
  */
89
92
  change(mutator) {
90
93
  const changes = makeChange(this._snapshot, mutator, this._changeMetadata, this._maxPayloadBytes);
@@ -1,9 +1,10 @@
1
1
  import { applyChanges } from '../algorithms/shared/applyChanges.js';
2
2
  import { signal } from '../event-signal.js';
3
3
  class LRUCache {
4
+ maxSize;
5
+ cache = new Map();
4
6
  constructor(maxSize) {
5
7
  this.maxSize = maxSize;
6
- this.cache = new Map();
7
8
  }
8
9
  get(key) {
9
10
  const value = this.cache.get(key);
@@ -36,15 +37,18 @@ class LRUCache {
36
37
  * Read-only: allows listing versions, loading states/changes, and scrubbing.
37
38
  */
38
39
  export class PatchesHistoryClient {
40
+ api;
41
+ /** Document ID */
42
+ id;
43
+ /** Event signal for versions changes */
44
+ onVersionsChange = signal();
45
+ /** Event signal for state changes */
46
+ onStateChange = signal();
47
+ _versions = [];
48
+ _state = null;
49
+ cache = new LRUCache(6);
39
50
  constructor(id, api) {
40
51
  this.api = api;
41
- /** Event signal for versions changes */
42
- this.onVersionsChange = signal();
43
- /** Event signal for state changes */
44
- this.onStateChange = signal();
45
- this._versions = [];
46
- this._state = null;
47
- this.cache = new LRUCache(6);
48
52
  this.id = id;
49
53
  }
50
54
  /** List of loaded versions */
@@ -21,6 +21,8 @@ import { transformPatch } from './transformPatch.js';
21
21
  * together which may form a single operation or transaction.
22
22
  */
23
23
  export class JSONPatch {
24
+ ops;
25
+ custom;
24
26
  /**
25
27
  * Create a new JSONPatch, optionally with an existing array of operations.
26
28
  */
@@ -1,36 +1,33 @@
1
- import type { DeepRequired } from '../types.js';
1
+ import type { ChangeMutator } from '../types.js';
2
2
  import { JSONPatch } from './JSONPatch.js';
3
3
  /**
4
- * Creates a `JSONPatch` instance by tracking changes made to a proxy object within an updater function.
4
+ * Creates a `JSONPatch` instance using a path-only proxy for type-safe operation generation.
5
5
  *
6
- * This provides a convenient way to generate patches based on direct object manipulation.
7
- * The `updater` function receives a proxy of the `target` object and the `JSONPatch` instance.
8
- * Modifications made to the proxy object (setting properties, calling array methods)
9
- * are automatically converted into JSON Patch operations and added to the patch instance.
10
- * You can also directly call methods on the `patch` instance within the updater.
6
+ * The mutator function receives a JSONPatch instance and a PathProxy for creating
7
+ * type-safe JSON Pointer paths. All modifications must be done through explicit
8
+ * JSONPatch methods - the path proxy will throw errors if mutation is attempted.
11
9
  *
12
10
  * @template T The type of the target object.
13
- * @param target The initial state of the object.
14
- * @param updater A function that receives a proxy of the target and a `JSONPatch` instance.
15
- * Modify the proxy or call patch methods within this function to generate operations.
16
- * @returns A `JSONPatch` instance containing the operations generated within the updater.
11
+ * @param target The initial state of the object (used for type inference only).
12
+ * @param mutator A function that receives a JSONPatch instance and a PathProxy.
13
+ * @returns A `JSONPatch` instance containing the operations generated within the mutator.
17
14
  *
18
15
  * @example
19
16
  * ```ts
20
17
  * const myObj = { name: { first: 'Alice' }, age: 30, tags: ['a'] };
21
18
  *
22
- * const patch = createJSONPatch(myObj, (proxy, p) => {
23
- * proxy.name.first = 'Bob'; // Generates a 'replace' op via the proxy
24
- * proxy.tags.push('b'); // Generates an 'add' op via the proxy
25
- * p.increment(proxy.age, 1); // Directly adds an 'increment' op
19
+ * const patch = createJSONPatch(myObj, (patch, path) => {
20
+ * patch.replace(path.name.first, 'Bob'); // Type-safe path creation
21
+ * patch.increment(path.age, 1); // Explicit operations only
22
+ * patch.add(path.tags[1], 'b'); // Array path handling
26
23
  * });
27
24
  *
28
25
  * console.log(patch.ops);
29
26
  * // [
30
27
  * // { op: 'replace', path: '/name/first', value: 'Bob' },
31
- * // { op: 'add', path: '/tags/1', value: 'b' },
32
- * // { op: 'increment', path: '/age', value: 1 }
28
+ * // { op: 'increment', path: '/age', value: 1 },
29
+ * // { op: 'add', path: '/tags/1', value: 'b' }
33
30
  * // ]
34
31
  * ```
35
32
  */
36
- export declare function createJSONPatch<T>(target: T, updater: (proxy: DeepRequired<T>, patch: JSONPatch) => void): JSONPatch;
33
+ export declare function createJSONPatch<T>(mutator: ChangeMutator<T>): JSONPatch;
@@ -1,41 +1,39 @@
1
1
  import { JSONPatch } from './JSONPatch.js';
2
- import { createPatchProxy } from './patchProxy.js';
2
+ import { createPathProxy } from './pathProxy.js';
3
3
  /**
4
- * Creates a `JSONPatch` instance by tracking changes made to a proxy object within an updater function.
4
+ * Creates a `JSONPatch` instance using a path-only proxy for type-safe operation generation.
5
5
  *
6
- * This provides a convenient way to generate patches based on direct object manipulation.
7
- * The `updater` function receives a proxy of the `target` object and the `JSONPatch` instance.
8
- * Modifications made to the proxy object (setting properties, calling array methods)
9
- * are automatically converted into JSON Patch operations and added to the patch instance.
10
- * You can also directly call methods on the `patch` instance within the updater.
6
+ * The mutator function receives a JSONPatch instance and a PathProxy for creating
7
+ * type-safe JSON Pointer paths. All modifications must be done through explicit
8
+ * JSONPatch methods - the path proxy will throw errors if mutation is attempted.
11
9
  *
12
10
  * @template T The type of the target object.
13
- * @param target The initial state of the object.
14
- * @param updater A function that receives a proxy of the target and a `JSONPatch` instance.
15
- * Modify the proxy or call patch methods within this function to generate operations.
16
- * @returns A `JSONPatch` instance containing the operations generated within the updater.
11
+ * @param target The initial state of the object (used for type inference only).
12
+ * @param mutator A function that receives a JSONPatch instance and a PathProxy.
13
+ * @returns A `JSONPatch` instance containing the operations generated within the mutator.
17
14
  *
18
15
  * @example
19
16
  * ```ts
20
17
  * const myObj = { name: { first: 'Alice' }, age: 30, tags: ['a'] };
21
18
  *
22
- * const patch = createJSONPatch(myObj, (proxy, p) => {
23
- * proxy.name.first = 'Bob'; // Generates a 'replace' op via the proxy
24
- * proxy.tags.push('b'); // Generates an 'add' op via the proxy
25
- * p.increment(proxy.age, 1); // Directly adds an 'increment' op
19
+ * const patch = createJSONPatch(myObj, (patch, path) => {
20
+ * patch.replace(path.name.first, 'Bob'); // Type-safe path creation
21
+ * patch.increment(path.age, 1); // Explicit operations only
22
+ * patch.add(path.tags[1], 'b'); // Array path handling
26
23
  * });
27
24
  *
28
25
  * console.log(patch.ops);
29
26
  * // [
30
27
  * // { op: 'replace', path: '/name/first', value: 'Bob' },
31
- * // { op: 'add', path: '/tags/1', value: 'b' },
32
- * // { op: 'increment', path: '/age', value: 1 }
28
+ * // { op: 'increment', path: '/age', value: 1 },
29
+ * // { op: 'add', path: '/tags/1', value: 'b' }
33
30
  * // ]
34
31
  * ```
35
32
  */
36
- export function createJSONPatch(target, updater) {
33
+ export function createJSONPatch(mutator) {
37
34
  const patch = new JSONPatch();
38
- // Use the specific overload of createPatchProxy that takes target and patch
39
- updater(createPatchProxy(target, patch), patch);
35
+ // Create path-only proxy for type-safe path generation
36
+ const pathProxy = createPathProxy();
37
+ mutator(patch, pathProxy);
40
38
  return patch;
41
39
  }
@@ -0,0 +1,22 @@
1
+ import type { PathProxy } from '../types.js';
2
+ /**
3
+ * Creates a path proxy for generating JSON Pointer paths in a type-safe way.
4
+ * This proxy should ONLY be used for path creation with JSONPatch methods.
5
+ *
6
+ * Usage:
7
+ * ```ts
8
+ * const patch = new JSONPatch();
9
+ * const path = createPathProxy<MyType>();
10
+ * patch.replace(path.content, 'new text'); // Path is '/content'
11
+ * patch.increment(path.counter, 5); // Path is '/counter'
12
+ * patch.add(path.items[0], newItem); // Path is '/items/0'
13
+ * ```
14
+ *
15
+ * The proxy will throw errors if you attempt to set properties or delete properties.
16
+ * This prevents accidental mutation and ensures explicit patch operations are used.
17
+ *
18
+ * @template T The type of the object to create paths for.
19
+ * @returns A path proxy object.
20
+ */
21
+ export declare const createPathProxy: <T>() => PathProxy<T>;
22
+ export declare function pathProxy<T>(path?: string): PathProxy<T>;