@dabble/patches 0.7.6 → 0.7.7

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.
@@ -1,5 +1,6 @@
1
1
  import "../../chunk-IZ2YBCUP.js";
2
2
  import { applyBitmask, combineBitmasks } from "../../json-patch/ops/bitmask.js";
3
+ import { isEmptyContainer } from "../../json-patch/utils/softWrites.js";
3
4
  const combinableOps = {
4
5
  "@inc": {
5
6
  apply: (a, b) => a + b,
@@ -33,6 +34,9 @@ function consolidateFieldOp(existing, incoming) {
33
34
  }
34
35
  return { ...incoming, op, value };
35
36
  }
37
+ if (isSoftOp(incoming)) {
38
+ return null;
39
+ }
36
40
  if (isExistingNewer(existing.ts, incoming.ts)) {
37
41
  return null;
38
42
  }
@@ -58,6 +62,19 @@ function consolidateOps(existingOps, newOps) {
58
62
  opsToSave.push(consolidated);
59
63
  }
60
64
  } else {
65
+ if (isSoftOp(newOp)) {
66
+ let dataExists = false;
67
+ for (const existingPath of existingByPath.keys()) {
68
+ if (existingPath.startsWith(newOp.path + "/")) {
69
+ dataExists = true;
70
+ break;
71
+ }
72
+ }
73
+ if (!dataExists) {
74
+ dataExists = pathExistsInParentOp(newOp.path, existingByPath);
75
+ }
76
+ if (dataExists) continue;
77
+ }
61
78
  for (const existingPath of existingByPath.keys()) {
62
79
  if (existingPath.startsWith(newOp.path + "/")) {
63
80
  pathsToDelete.add(existingPath);
@@ -92,6 +109,30 @@ function parentFixes(path, existing) {
92
109
  }
93
110
  return pathsToDelete;
94
111
  }
112
+ function pathExistsInParentOp(path, existingByPath) {
113
+ let parent = path;
114
+ while (parent.lastIndexOf("/") > 0) {
115
+ parent = parent.substring(0, parent.lastIndexOf("/"));
116
+ const parentOp = existingByPath.get(parent);
117
+ if (parentOp && isObject(parentOp.value)) {
118
+ const remainingKeys = path.substring(parent.length + 1).split("/");
119
+ let current = parentOp.value;
120
+ let found = true;
121
+ for (const key of remainingKeys) {
122
+ if (!isObject(current) || !(key in current)) {
123
+ found = false;
124
+ break;
125
+ }
126
+ current = current[key];
127
+ }
128
+ if (found) return true;
129
+ }
130
+ }
131
+ return false;
132
+ }
133
+ function isSoftOp(op) {
134
+ return op.soft === true || op.op === "add" && isEmptyContainer(op.value);
135
+ }
95
136
  function isObject(value) {
96
137
  return value !== null && typeof value === "object";
97
138
  }
@@ -7,6 +7,15 @@ import { AlgorithmName } from './PatchesStore.js';
7
7
  import '../json-patch/JSONPatch.js';
8
8
  import '@dabble/delta';
9
9
 
10
+ /**
11
+ * Options for opening a document, passed through to `patches.openDoc()`.
12
+ */
13
+ interface OpenDocOptions {
14
+ /** Optional metadata to attach to the document. */
15
+ metadata?: Record<string, any>;
16
+ /** Override the algorithm for this document (defaults to the Patches instance default). */
17
+ algorithm?: AlgorithmName;
18
+ }
10
19
  /**
11
20
  * Options for creating a Patches instance.
12
21
  * Provides algorithms map and optional default algorithm.
@@ -85,10 +94,7 @@ declare class Patches {
85
94
  * @param opts - Optional metadata and algorithm override.
86
95
  * @returns The opened PatchesDoc instance.
87
96
  */
88
- openDoc<T extends object>(docId: string, opts?: {
89
- metadata?: Record<string, any>;
90
- algorithm?: AlgorithmName;
91
- }): Promise<PatchesDoc<T>>;
97
+ openDoc<T extends object>(docId: string, opts?: OpenDocOptions): Promise<PatchesDoc<T>>;
92
98
  /**
93
99
  * Closes an open document by ID, removing listeners and optionally untracking it.
94
100
  * @param docId - The document ID to close.
@@ -122,4 +128,4 @@ declare class Patches {
122
128
  protected _handleDocChange<T extends object>(docId: string, ops: JSONPatchOp[], doc: PatchesDoc<T>, algorithm: ClientAlgorithm, metadata: Record<string, any>): Promise<void>;
123
129
  }
124
130
 
125
- export { Patches, type PatchesOptions };
131
+ export { type OpenDocOptions, Patches, type PatchesOptions };
@@ -9,7 +9,7 @@ export { LWWDoc } from './LWWDoc.js';
9
9
  export { LWWAlgorithm } from './LWWAlgorithm.js';
10
10
  export { LWWBatcher } from './LWWBatcher.js';
11
11
  export { OTAlgorithm } from './OTAlgorithm.js';
12
- export { Patches, PatchesOptions } from './Patches.js';
12
+ export { OpenDocOptions, Patches, PatchesOptions } from './Patches.js';
13
13
  export { PatchesHistoryClient } from './PatchesHistoryClient.js';
14
14
  export { AlgorithmName, PatchesStore, TrackedDoc } from './PatchesStore.js';
15
15
  export { OTClientStore } from './OTClientStore.js';
package/dist/index.d.ts CHANGED
@@ -10,7 +10,7 @@ export { LWWDoc } from './client/LWWDoc.js';
10
10
  export { LWWAlgorithm } from './client/LWWAlgorithm.js';
11
11
  export { LWWBatcher } from './client/LWWBatcher.js';
12
12
  export { OTAlgorithm } from './client/OTAlgorithm.js';
13
- export { Patches, PatchesOptions } from './client/Patches.js';
13
+ export { OpenDocOptions, Patches, PatchesOptions } from './client/Patches.js';
14
14
  export { PatchesHistoryClient } from './client/PatchesHistoryClient.js';
15
15
  export { AlgorithmName, PatchesStore, TrackedDoc } from './client/PatchesStore.js';
16
16
  export { OTClientStore } from './client/OTClientStore.js';
@@ -1,4 +1,4 @@
1
- import { Patches } from '../client/Patches.js';
1
+ import { Patches, OpenDocOptions } from '../client/Patches.js';
2
2
  import { a as PatchesDoc } from '../BaseDoc-DkP3tUhT.js';
3
3
  import '../event-signal.js';
4
4
  import '../json-patch/types.js';
@@ -31,7 +31,7 @@ declare class DocManager {
31
31
  * @param docId - Document ID to open
32
32
  * @returns Promise resolving to PatchesDoc instance
33
33
  */
34
- openDoc<T extends object>(patches: Patches, docId: string): Promise<PatchesDoc<T>>;
34
+ openDoc<T extends object>(patches: Patches, docId: string, opts?: OpenDocOptions): Promise<PatchesDoc<T>>;
35
35
  /**
36
36
  * Closes a document with reference counting.
37
37
  *
@@ -13,7 +13,7 @@ class DocManager {
13
13
  * @param docId - Document ID to open
14
14
  * @returns Promise resolving to PatchesDoc instance
15
15
  */
16
- async openDoc(patches, docId) {
16
+ async openDoc(patches, docId, opts) {
17
17
  const currentCount = this.refCounts.get(docId) || 0;
18
18
  if (currentCount === 0 && this.pendingOps.has(docId)) {
19
19
  const doc = await this.pendingOps.get(docId);
@@ -28,7 +28,7 @@ class DocManager {
28
28
  }
29
29
  return doc;
30
30
  }
31
- const openPromise = patches.openDoc(docId);
31
+ const openPromise = patches.openDoc(docId, opts);
32
32
  this.pendingOps.set(docId, openPromise);
33
33
  try {
34
34
  const doc = await openPromise;
@@ -1,15 +1,18 @@
1
1
  import { Accessor } from 'solid-js';
2
+ import { OpenDocOptions } from '../client/Patches.js';
2
3
  import { a as PatchesDoc } from '../BaseDoc-DkP3tUhT.js';
3
4
  import { JSONPatch } from '../json-patch/JSONPatch.js';
4
5
  import { ChangeMutator } from '../types.js';
5
6
  import '../event-signal.js';
6
7
  import '../json-patch/types.js';
8
+ import '../client/ClientAlgorithm.js';
9
+ import '../client/PatchesStore.js';
7
10
  import '@dabble/delta';
8
11
 
9
12
  /**
10
13
  * Options for usePatchesDoc primitive (eager mode with docId).
11
14
  */
12
- interface UsePatchesDocOptions {
15
+ interface UsePatchesDocOptions extends OpenDocOptions {
13
16
  /**
14
17
  * Controls document lifecycle management on cleanup.
15
18
  *
@@ -89,8 +92,9 @@ interface UsePatchesDocLazyReturn<T extends object> extends UsePatchesDocReturn<
89
92
  * Open a document by path. Closes any previously loaded document first.
90
93
  *
91
94
  * @param docPath - The document path to open
95
+ * @param options - Optional algorithm and metadata overrides
92
96
  */
93
- load: (docPath: string) => Promise<void>;
97
+ load: (docPath: string, options?: OpenDocOptions) => Promise<void>;
94
98
  /**
95
99
  * Close the current document, unsubscribe, and reset all state.
96
100
  * Calls `patches.closeDoc()` but does not untrack — tracking is managed separately.
@@ -102,8 +106,9 @@ interface UsePatchesDocLazyReturn<T extends object> extends UsePatchesDocReturn<
102
106
  *
103
107
  * @param docPath - The document path to create
104
108
  * @param initialState - Initial state object or JSONPatch to apply
109
+ * @param options - Optional algorithm and metadata overrides
105
110
  */
106
- create: (docPath: string, initialState: T | JSONPatch) => Promise<void>;
111
+ create: (docPath: string, initialState: T | JSONPatch, options?: OpenDocOptions) => Promise<void>;
107
112
  }
108
113
  /**
109
114
  * Solid primitive for reactive Patches document state.
@@ -185,7 +190,7 @@ type MaybeAccessor<T> = T | Accessor<T>;
185
190
  /**
186
191
  * Props for the Provider component returned by createPatchesDoc.
187
192
  */
188
- interface PatchesDocProviderProps {
193
+ interface PatchesDocProviderProps extends OpenDocOptions {
189
194
  docId: MaybeAccessor<string>;
190
195
  autoClose?: boolean | 'untrack';
191
196
  children: any;
@@ -10,47 +10,87 @@ import {
10
10
  import { JSONPatch } from "../json-patch/JSONPatch.js";
11
11
  import { usePatchesContext } from "./context.js";
12
12
  import { getDocManager } from "./doc-manager.js";
13
- function usePatchesDoc(docIdOrOptions, options) {
14
- if (typeof docIdOrOptions === "string" || typeof docIdOrOptions === "function") {
15
- return _usePatchesDocEager(docIdOrOptions, options ?? {});
16
- }
17
- return _usePatchesDocLazy(docIdOrOptions ?? {});
18
- }
19
- function _usePatchesDocEager(docId, options) {
20
- const { patches } = usePatchesContext();
21
- const autoClose = options.autoClose ?? false;
22
- const shouldUntrack = autoClose === "untrack";
13
+ function createDocReactiveState(options) {
14
+ const { initialLoading = true, transformState, changeBehavior } = options;
23
15
  const [doc, setDoc] = createSignal(void 0);
24
16
  const [data, setData] = createSignal(void 0);
25
- const [loading, setLoading] = createSignal(true);
17
+ const [loading, setLoading] = createSignal(initialLoading);
26
18
  const [error, setError] = createSignal(null);
27
19
  const [rev, setRev] = createSignal(0);
28
20
  const [hasPending, setHasPending] = createSignal(false);
29
- const manager = getDocManager(patches);
30
- const docIdAccessor = toAccessor(docId);
31
21
  function setupDoc(patchesDoc) {
32
22
  setDoc(patchesDoc);
33
23
  const unsubState = patchesDoc.subscribe((state) => {
24
+ if (transformState && state) {
25
+ state = transformState(state, patchesDoc);
26
+ }
34
27
  setData(() => state);
35
28
  setRev(patchesDoc.committedRev);
36
29
  setHasPending(patchesDoc.hasPending);
37
30
  });
38
- onCleanup(() => unsubState());
39
31
  const unsubSync = patchesDoc.onSyncing((syncState) => {
40
32
  setLoading(syncState === "initial" || syncState === "updating");
41
33
  setError(syncState instanceof Error ? syncState : null);
42
34
  });
43
- onCleanup(() => unsubSync());
44
35
  setLoading(patchesDoc.syncing !== null);
36
+ return () => {
37
+ unsubState();
38
+ unsubSync();
39
+ };
45
40
  }
41
+ function resetSignals() {
42
+ setDoc(void 0);
43
+ setData(void 0);
44
+ setLoading(false);
45
+ setError(null);
46
+ setRev(0);
47
+ setHasPending(false);
48
+ }
49
+ function change(mutator) {
50
+ if (changeBehavior === "throw") {
51
+ const currentDoc = doc();
52
+ if (!currentDoc) {
53
+ throw new Error("Cannot make changes: document not loaded yet");
54
+ }
55
+ currentDoc.change(mutator);
56
+ } else {
57
+ doc()?.change(mutator);
58
+ }
59
+ }
60
+ const baseReturn = {
61
+ data,
62
+ loading,
63
+ error,
64
+ rev,
65
+ hasPending,
66
+ change,
67
+ doc
68
+ };
69
+ return { doc, setDoc, data, setData, loading, setLoading, error, setError, rev, setRev, hasPending, setHasPending, setupDoc, resetSignals, change, baseReturn };
70
+ }
71
+ function usePatchesDoc(docIdOrOptions, options) {
72
+ if (typeof docIdOrOptions === "string" || typeof docIdOrOptions === "function") {
73
+ return _usePatchesDocEager(docIdOrOptions, options ?? {});
74
+ }
75
+ return _usePatchesDocLazy(docIdOrOptions ?? {});
76
+ }
77
+ function _usePatchesDocEager(docId, options) {
78
+ const { patches } = usePatchesContext();
79
+ const { autoClose = false, algorithm, metadata } = options;
80
+ const shouldUntrack = autoClose === "untrack";
81
+ const openDocOpts = { algorithm, metadata };
82
+ const manager = getDocManager(patches);
83
+ const { setupDoc, setError, setLoading, baseReturn } = createDocReactiveState({ changeBehavior: "throw" });
84
+ const docIdAccessor = toAccessor(docId);
46
85
  if (autoClose) {
47
86
  const [docResource] = createResource(docIdAccessor, async (id) => {
48
- return await manager.openDoc(patches, id);
87
+ return await manager.openDoc(patches, id, openDocOpts);
49
88
  });
50
89
  createEffect(() => {
51
90
  const loadedDoc = docResource();
52
91
  if (loadedDoc) {
53
- setupDoc(loadedDoc);
92
+ const unsub = setupDoc(loadedDoc);
93
+ onCleanup(() => unsub());
54
94
  }
55
95
  const resourceError = docResource.error;
56
96
  if (resourceError) {
@@ -72,75 +112,31 @@ function _usePatchesDocEager(docId, options) {
72
112
  );
73
113
  }
74
114
  manager.incrementRefCount(id);
75
- setupDoc(patchesDoc);
115
+ const unsub = setupDoc(patchesDoc);
76
116
  onCleanup(() => {
117
+ unsub();
77
118
  manager.decrementRefCount(id);
78
119
  });
79
120
  });
80
121
  }
81
- function change(mutator) {
82
- const currentDoc = doc();
83
- if (!currentDoc) {
84
- throw new Error("Cannot make changes: document not loaded yet");
85
- }
86
- currentDoc.change(mutator);
87
- }
88
- return {
89
- data,
90
- loading,
91
- error,
92
- rev,
93
- hasPending,
94
- change,
95
- doc
96
- };
122
+ return baseReturn;
97
123
  }
98
124
  function _usePatchesDocLazy(options) {
99
125
  const { patches } = usePatchesContext();
100
126
  const { idProp } = options;
101
- let currentDoc = null;
127
+ const { setupDoc, resetSignals, setError, setLoading, baseReturn } = createDocReactiveState({
128
+ initialLoading: false,
129
+ changeBehavior: "noop",
130
+ transformState: idProp ? (state, patchesDoc) => ({ ...state, [idProp]: patchesDoc.id }) : void 0
131
+ });
102
132
  let unsubscribe = null;
103
133
  const [path, setPath] = createSignal(null);
104
- const [doc, setDoc] = createSignal(void 0);
105
- const [data, setData] = createSignal(void 0);
106
- const [loading, setLoading] = createSignal(false);
107
- const [error, setError] = createSignal(null);
108
- const [rev, setRev] = createSignal(0);
109
- const [hasPending, setHasPending] = createSignal(false);
110
- function setupDoc(patchesDoc) {
111
- currentDoc = patchesDoc;
112
- setDoc(patchesDoc);
113
- unsubscribe = patchesDoc.subscribe((state) => {
114
- if (state && idProp && currentDoc) {
115
- state = { ...state, [idProp]: currentDoc.id };
116
- }
117
- setData(() => state);
118
- setRev(patchesDoc.committedRev);
119
- setHasPending(patchesDoc.hasPending);
120
- });
121
- const unsubSync = patchesDoc.onSyncing((syncState) => {
122
- setLoading(syncState === "initial" || syncState === "updating");
123
- setError(syncState instanceof Error ? syncState : null);
124
- });
125
- const origUnsub = unsubscribe;
126
- unsubscribe = () => {
127
- origUnsub();
128
- unsubSync();
129
- };
130
- setLoading(patchesDoc.syncing !== null);
131
- }
132
134
  function teardown() {
133
135
  unsubscribe?.();
134
136
  unsubscribe = null;
135
- currentDoc = null;
136
- setDoc(void 0);
137
- setData(void 0);
138
- setLoading(false);
139
- setError(null);
140
- setRev(0);
141
- setHasPending(false);
137
+ resetSignals();
142
138
  }
143
- async function load(docPath) {
139
+ async function load(docPath, options2) {
144
140
  if (path()) {
145
141
  const prevPath = path();
146
142
  teardown();
@@ -148,8 +144,8 @@ function _usePatchesDocLazy(options) {
148
144
  }
149
145
  setPath(docPath);
150
146
  try {
151
- const patchesDoc = await patches.openDoc(docPath);
152
- setupDoc(patchesDoc);
147
+ const patchesDoc = await patches.openDoc(docPath, options2);
148
+ unsubscribe = setupDoc(patchesDoc);
153
149
  } catch (err) {
154
150
  setError(err);
155
151
  setLoading(false);
@@ -163,8 +159,8 @@ function _usePatchesDocLazy(options) {
163
159
  await patches.closeDoc(prevPath);
164
160
  }
165
161
  }
166
- async function create(docPath, initialState) {
167
- const newDoc = await patches.openDoc(docPath);
162
+ async function create(docPath, initialState, options2) {
163
+ const newDoc = await patches.openDoc(docPath, options2);
168
164
  newDoc.change((patch, root) => {
169
165
  if (initialState instanceof JSONPatch) {
170
166
  patch.ops = initialState.ops;
@@ -176,22 +172,7 @@ function _usePatchesDocLazy(options) {
176
172
  });
177
173
  await patches.closeDoc(docPath);
178
174
  }
179
- function change(mutator) {
180
- currentDoc?.change(mutator);
181
- }
182
- return {
183
- data,
184
- loading,
185
- error,
186
- rev,
187
- hasPending,
188
- change,
189
- doc,
190
- path,
191
- load,
192
- close,
193
- create
194
- };
175
+ return { ...baseReturn, path, load, close, create };
195
176
  }
196
177
  function usePatchesSync() {
197
178
  const { sync } = usePatchesContext();
@@ -225,36 +206,18 @@ function createPatchesDoc(name) {
225
206
  const manager = getDocManager(patches);
226
207
  const autoClose = props.autoClose ?? false;
227
208
  const shouldUntrack = autoClose === "untrack";
228
- const [doc, setDoc] = createSignal(void 0);
229
- const [data, setData] = createSignal(void 0);
230
- const [loading, setLoading] = createSignal(true);
231
- const [error, setError] = createSignal(null);
232
- const [rev, setRev] = createSignal(0);
233
- const [hasPending, setHasPending] = createSignal(false);
234
- function setupDoc(patchesDoc) {
235
- setDoc(patchesDoc);
236
- const unsubState = patchesDoc.subscribe((state) => {
237
- setData(() => state);
238
- setRev(patchesDoc.committedRev);
239
- setHasPending(patchesDoc.hasPending);
240
- });
241
- onCleanup(() => unsubState());
242
- const unsubSync = patchesDoc.onSyncing((syncState) => {
243
- setLoading(syncState === "initial" || syncState === "updating");
244
- setError(syncState instanceof Error ? syncState : null);
245
- });
246
- onCleanup(() => unsubSync());
247
- setLoading(patchesDoc.syncing !== null);
248
- }
209
+ const openDocOpts = { algorithm: props.algorithm, metadata: props.metadata };
210
+ const { setupDoc, setError, setLoading, baseReturn } = createDocReactiveState({ changeBehavior: "throw" });
249
211
  const docIdAccessor = toAccessor(props.docId);
250
212
  if (autoClose) {
251
213
  const [docResource] = createResource(docIdAccessor, async (id) => {
252
- return await manager.openDoc(patches, id);
214
+ return await manager.openDoc(patches, id, openDocOpts);
253
215
  });
254
216
  createEffect(() => {
255
217
  const loadedDoc = docResource();
256
218
  if (loadedDoc) {
257
- setupDoc(loadedDoc);
219
+ const unsub = setupDoc(loadedDoc);
220
+ onCleanup(() => unsub());
258
221
  }
259
222
  const resourceError = docResource.error;
260
223
  if (resourceError) {
@@ -286,7 +249,8 @@ function createPatchesDoc(name) {
286
249
  );
287
250
  }
288
251
  manager.incrementRefCount(id);
289
- setupDoc(patchesDoc);
252
+ const unsub = setupDoc(patchesDoc);
253
+ onCleanup(() => unsub());
290
254
  return id;
291
255
  });
292
256
  onCleanup(() => {
@@ -294,23 +258,7 @@ function createPatchesDoc(name) {
294
258
  manager.decrementRefCount(id);
295
259
  });
296
260
  }
297
- function change(mutator) {
298
- const currentDoc = doc();
299
- if (!currentDoc) {
300
- throw new Error("Cannot make changes: document not loaded yet");
301
- }
302
- currentDoc.change(mutator);
303
- }
304
- const value = {
305
- data,
306
- loading,
307
- error,
308
- rev,
309
- hasPending,
310
- change,
311
- doc
312
- };
313
- return /* @__PURE__ */ React.createElement(Context.Provider, { value, children: props.children });
261
+ return /* @__PURE__ */ React.createElement(Context.Provider, { value: baseReturn, children: props.children });
314
262
  }
315
263
  function useDoc() {
316
264
  const context = useContext(Context);
@@ -1,15 +1,18 @@
1
1
  import { ShallowRef, Ref, MaybeRef } from 'vue';
2
+ import { OpenDocOptions } from '../client/Patches.js';
2
3
  import { a as PatchesDoc } from '../BaseDoc-DkP3tUhT.js';
3
4
  import { JSONPatch } from '../json-patch/JSONPatch.js';
4
5
  import { ChangeMutator } from '../types.js';
5
6
  import '../event-signal.js';
6
7
  import '../json-patch/types.js';
8
+ import '../client/ClientAlgorithm.js';
9
+ import '../client/PatchesStore.js';
7
10
  import '@dabble/delta';
8
11
 
9
12
  /**
10
13
  * Options for usePatchesDoc composable (eager mode with docId).
11
14
  */
12
- interface UsePatchesDocOptions {
15
+ interface UsePatchesDocOptions extends OpenDocOptions {
13
16
  /**
14
17
  * Controls document lifecycle management on component unmount.
15
18
  *
@@ -89,8 +92,9 @@ interface UsePatchesDocLazyReturn<T extends object> extends UsePatchesDocReturn<
89
92
  * Open a document by path. Closes any previously loaded document first.
90
93
  *
91
94
  * @param docPath - The document path to open
95
+ * @param options - Optional algorithm and metadata overrides
92
96
  */
93
- load: (docPath: string) => Promise<void>;
97
+ load: (docPath: string, options?: OpenDocOptions) => Promise<void>;
94
98
  /**
95
99
  * Close the current document, unsubscribe, and reset all state.
96
100
  * Calls `patches.closeDoc()` but does not untrack — tracking is managed separately.
@@ -102,8 +106,9 @@ interface UsePatchesDocLazyReturn<T extends object> extends UsePatchesDocReturn<
102
106
  *
103
107
  * @param docPath - The document path to create
104
108
  * @param initialState - Initial state object or JSONPatch to apply
109
+ * @param options - Optional algorithm and metadata overrides
105
110
  */
106
- create: (docPath: string, initialState: T | JSONPatch) => Promise<void>;
111
+ create: (docPath: string, initialState: T | JSONPatch, options?: OpenDocOptions) => Promise<void>;
107
112
  }
108
113
  /**
109
114
  * Vue composable for reactive Patches document state.
@@ -11,48 +11,86 @@ import {
11
11
  import { JSONPatch } from "../json-patch/JSONPatch.js";
12
12
  import { usePatchesContext } from "./provider.js";
13
13
  import { getDocManager } from "./doc-manager.js";
14
- function usePatchesDoc(docIdOrOptions, options) {
15
- if (typeof docIdOrOptions === "string") {
16
- return _usePatchesDocEager(docIdOrOptions, options ?? {});
17
- }
18
- return _usePatchesDocLazy(docIdOrOptions ?? {});
19
- }
20
- function _usePatchesDocEager(docId, options) {
21
- const { patches } = usePatchesContext();
22
- const { autoClose = false } = options;
23
- const shouldUntrack = autoClose === "untrack";
14
+ function createDocReactiveState(options) {
15
+ const { initialLoading = true, transformState, changeBehavior } = options;
24
16
  const doc = ref(void 0);
25
17
  const data = shallowRef(void 0);
26
- const loading = ref(true);
18
+ const loading = ref(initialLoading);
27
19
  const error = ref(null);
28
20
  const rev = ref(0);
29
21
  const hasPending = ref(false);
30
- const unsubscribers = [];
31
- const manager = getDocManager(patches);
32
22
  function setupDoc(patchesDoc) {
33
23
  doc.value = patchesDoc;
34
24
  const unsubState = patchesDoc.subscribe((state) => {
25
+ if (transformState && state) {
26
+ state = transformState(state, patchesDoc);
27
+ }
35
28
  data.value = state;
36
29
  rev.value = patchesDoc.committedRev;
37
30
  hasPending.value = patchesDoc.hasPending;
38
31
  });
39
- unsubscribers.push(unsubState);
40
32
  const unsubSync = patchesDoc.onSyncing((syncState) => {
41
33
  loading.value = syncState === "initial" || syncState === "updating";
42
34
  error.value = syncState instanceof Error ? syncState : null;
43
35
  });
44
- unsubscribers.push(unsubSync);
45
36
  loading.value = patchesDoc.syncing !== null;
37
+ return () => {
38
+ unsubState();
39
+ unsubSync();
40
+ };
41
+ }
42
+ function resetRefs() {
43
+ doc.value = void 0;
44
+ data.value = void 0;
45
+ loading.value = false;
46
+ error.value = null;
47
+ rev.value = 0;
48
+ hasPending.value = false;
49
+ }
50
+ function change(mutator) {
51
+ if (changeBehavior === "throw") {
52
+ if (!doc.value) {
53
+ throw new Error("Cannot make changes: document not loaded yet");
54
+ }
55
+ doc.value.change(mutator);
56
+ } else {
57
+ doc.value?.change(mutator);
58
+ }
59
+ }
60
+ const baseReturn = {
61
+ data,
62
+ loading,
63
+ error,
64
+ rev,
65
+ hasPending,
66
+ change,
67
+ doc
68
+ };
69
+ return { doc, data, loading, error, rev, hasPending, setupDoc, resetRefs, change, baseReturn };
70
+ }
71
+ function usePatchesDoc(docIdOrOptions, options) {
72
+ if (typeof docIdOrOptions === "string") {
73
+ return _usePatchesDocEager(docIdOrOptions, options ?? {});
46
74
  }
75
+ return _usePatchesDocLazy(docIdOrOptions ?? {});
76
+ }
77
+ function _usePatchesDocEager(docId, options) {
78
+ const { patches } = usePatchesContext();
79
+ const { autoClose = false, algorithm, metadata } = options;
80
+ const shouldUntrack = autoClose === "untrack";
81
+ const openDocOpts = { algorithm, metadata };
82
+ const manager = getDocManager(patches);
83
+ const { setupDoc, baseReturn } = createDocReactiveState({ changeBehavior: "throw" });
84
+ let unsubscribe = null;
47
85
  if (autoClose) {
48
- manager.openDoc(patches, docId).then((patchesDoc) => {
49
- setupDoc(patchesDoc);
86
+ manager.openDoc(patches, docId, openDocOpts).then((patchesDoc) => {
87
+ unsubscribe = setupDoc(patchesDoc);
50
88
  }).catch((err) => {
51
- error.value = err;
52
- loading.value = false;
89
+ baseReturn.error.value = err;
90
+ baseReturn.loading.value = false;
53
91
  });
54
92
  onBeforeUnmount(() => {
55
- unsubscribers.forEach((unsub) => unsub());
93
+ unsubscribe?.();
56
94
  manager.closeDoc(patches, docId, shouldUntrack);
57
95
  });
58
96
  } else {
@@ -63,74 +101,30 @@ function _usePatchesDocEager(docId, options) {
63
101
  );
64
102
  }
65
103
  manager.incrementRefCount(docId);
66
- setupDoc(patchesDoc);
104
+ unsubscribe = setupDoc(patchesDoc);
67
105
  onBeforeUnmount(() => {
68
- unsubscribers.forEach((unsub) => unsub());
106
+ unsubscribe?.();
69
107
  manager.decrementRefCount(docId);
70
108
  });
71
109
  }
72
- function change(mutator) {
73
- if (!doc.value) {
74
- throw new Error("Cannot make changes: document not loaded yet");
75
- }
76
- doc.value.change(mutator);
77
- }
78
- return {
79
- data,
80
- loading,
81
- error,
82
- rev,
83
- hasPending,
84
- change,
85
- doc
86
- };
110
+ return baseReturn;
87
111
  }
88
112
  function _usePatchesDocLazy(options) {
89
113
  const { patches } = usePatchesContext();
90
114
  const { idProp } = options;
91
- let currentDoc = null;
115
+ const { setupDoc, resetRefs, baseReturn } = createDocReactiveState({
116
+ initialLoading: false,
117
+ changeBehavior: "noop",
118
+ transformState: idProp ? (state, patchesDoc) => ({ ...state, [idProp]: patchesDoc.id }) : void 0
119
+ });
92
120
  let unsubscribe = null;
93
121
  const path = ref(null);
94
- const doc = ref(void 0);
95
- const data = shallowRef(void 0);
96
- const loading = ref(false);
97
- const error = ref(null);
98
- const rev = ref(0);
99
- const hasPending = ref(false);
100
- function setupDoc(patchesDoc) {
101
- currentDoc = patchesDoc;
102
- doc.value = patchesDoc;
103
- unsubscribe = patchesDoc.subscribe((state) => {
104
- if (state && idProp && currentDoc) {
105
- state = { ...state, [idProp]: currentDoc.id };
106
- }
107
- data.value = state;
108
- rev.value = patchesDoc.committedRev;
109
- hasPending.value = patchesDoc.hasPending;
110
- });
111
- const unsubSync = patchesDoc.onSyncing((syncState) => {
112
- loading.value = syncState === "initial" || syncState === "updating";
113
- error.value = syncState instanceof Error ? syncState : null;
114
- });
115
- const origUnsub = unsubscribe;
116
- unsubscribe = () => {
117
- origUnsub();
118
- unsubSync();
119
- };
120
- loading.value = patchesDoc.syncing !== null;
121
- }
122
122
  function teardown() {
123
123
  unsubscribe?.();
124
124
  unsubscribe = null;
125
- currentDoc = null;
126
- doc.value = void 0;
127
- data.value = void 0;
128
- loading.value = false;
129
- error.value = null;
130
- rev.value = 0;
131
- hasPending.value = false;
125
+ resetRefs();
132
126
  }
133
- async function load(docPath) {
127
+ async function load(docPath, options2) {
134
128
  if (path.value) {
135
129
  const prevPath = path.value;
136
130
  teardown();
@@ -138,11 +132,11 @@ function _usePatchesDocLazy(options) {
138
132
  }
139
133
  path.value = docPath;
140
134
  try {
141
- const patchesDoc = await patches.openDoc(docPath);
142
- setupDoc(patchesDoc);
135
+ const patchesDoc = await patches.openDoc(docPath, options2);
136
+ unsubscribe = setupDoc(patchesDoc);
143
137
  } catch (err) {
144
- error.value = err;
145
- loading.value = false;
138
+ baseReturn.error.value = err;
139
+ baseReturn.loading.value = false;
146
140
  }
147
141
  }
148
142
  async function close() {
@@ -153,8 +147,8 @@ function _usePatchesDocLazy(options) {
153
147
  await patches.closeDoc(prevPath);
154
148
  }
155
149
  }
156
- async function create(docPath, initialState) {
157
- const newDoc = await patches.openDoc(docPath);
150
+ async function create(docPath, initialState, options2) {
151
+ const newDoc = await patches.openDoc(docPath, options2);
158
152
  newDoc.change((patch, root) => {
159
153
  if (initialState instanceof JSONPatch) {
160
154
  patch.ops = initialState.ops;
@@ -166,22 +160,7 @@ function _usePatchesDocLazy(options) {
166
160
  });
167
161
  await patches.closeDoc(docPath);
168
162
  }
169
- function change(mutator) {
170
- currentDoc?.change(mutator);
171
- }
172
- return {
173
- data,
174
- loading,
175
- error,
176
- rev,
177
- hasPending,
178
- change,
179
- doc,
180
- path,
181
- load,
182
- close,
183
- create
184
- };
163
+ return { ...baseReturn, path, load, close, create };
185
164
  }
186
165
  function usePatchesSync() {
187
166
  const { sync } = usePatchesContext();
@@ -210,43 +189,24 @@ function createDocInjectionKey(name) {
210
189
  }
211
190
  function providePatchesDoc(name, docId, options = {}) {
212
191
  const { patches } = usePatchesContext();
213
- const { autoClose = false } = options;
192
+ const { autoClose = false, algorithm, metadata } = options;
214
193
  const shouldUntrack = autoClose === "untrack";
194
+ const openDocOpts = { algorithm, metadata };
215
195
  const manager = getDocManager(patches);
216
- const doc = ref(void 0);
217
- const data = shallowRef(void 0);
218
- const loading = ref(true);
219
- const error = ref(null);
220
- const rev = ref(0);
221
- const hasPending = ref(false);
196
+ const { setupDoc, baseReturn } = createDocReactiveState({ changeBehavior: "throw" });
222
197
  const currentDocId = ref(unref(docId));
223
- const unsubscribers = [];
224
- function setupDoc(patchesDoc) {
225
- unsubscribers.forEach((unsub) => unsub());
226
- unsubscribers.length = 0;
227
- doc.value = patchesDoc;
228
- const unsubState = patchesDoc.subscribe((state) => {
229
- data.value = state;
230
- rev.value = patchesDoc.committedRev;
231
- hasPending.value = patchesDoc.hasPending;
232
- });
233
- unsubscribers.push(unsubState);
234
- const unsubSync = patchesDoc.onSyncing((syncState) => {
235
- loading.value = syncState === "initial" || syncState === "updating";
236
- error.value = syncState instanceof Error ? syncState : null;
237
- });
238
- unsubscribers.push(unsubSync);
239
- loading.value = patchesDoc.syncing !== null;
240
- }
198
+ let unsubscribe = null;
241
199
  async function initDoc(id) {
242
200
  currentDocId.value = id;
201
+ unsubscribe?.();
202
+ unsubscribe = null;
243
203
  if (autoClose) {
244
204
  try {
245
- const patchesDoc = await manager.openDoc(patches, id);
246
- setupDoc(patchesDoc);
205
+ const patchesDoc = await manager.openDoc(patches, id, openDocOpts);
206
+ unsubscribe = setupDoc(patchesDoc);
247
207
  } catch (err) {
248
- error.value = err;
249
- loading.value = false;
208
+ baseReturn.error.value = err;
209
+ baseReturn.loading.value = false;
250
210
  }
251
211
  } else {
252
212
  try {
@@ -257,36 +217,21 @@ function providePatchesDoc(name, docId, options = {}) {
257
217
  );
258
218
  }
259
219
  manager.incrementRefCount(id);
260
- setupDoc(patchesDoc);
220
+ unsubscribe = setupDoc(patchesDoc);
261
221
  } catch (err) {
262
- error.value = err;
263
- loading.value = false;
222
+ baseReturn.error.value = err;
223
+ baseReturn.loading.value = false;
264
224
  }
265
225
  }
266
226
  }
267
- function change(mutator) {
268
- if (!doc.value) {
269
- throw new Error("Cannot make changes: document not loaded yet");
270
- }
271
- doc.value.change(mutator);
272
- }
273
- const docReturn = {
274
- data,
275
- loading,
276
- error,
277
- rev,
278
- hasPending,
279
- change,
280
- doc
281
- };
282
227
  const key = createDocInjectionKey(name);
283
- provide(key, docReturn);
228
+ provide(key, baseReturn);
284
229
  initDoc(unref(docId));
285
230
  if (typeof docId !== "string") {
286
231
  watch(docId, async (newDocId, oldDocId) => {
287
232
  if (newDocId === oldDocId) return;
288
- unsubscribers.forEach((unsub) => unsub());
289
- unsubscribers.length = 0;
233
+ unsubscribe?.();
234
+ unsubscribe = null;
290
235
  if (autoClose) {
291
236
  await manager.closeDoc(patches, oldDocId, shouldUntrack);
292
237
  } else {
@@ -296,14 +241,14 @@ function providePatchesDoc(name, docId, options = {}) {
296
241
  });
297
242
  }
298
243
  onBeforeUnmount(async () => {
299
- unsubscribers.forEach((unsub) => unsub());
244
+ unsubscribe?.();
300
245
  if (autoClose) {
301
246
  await manager.closeDoc(patches, currentDocId.value, shouldUntrack);
302
247
  } else {
303
248
  manager.decrementRefCount(currentDocId.value);
304
249
  }
305
250
  });
306
- return docReturn;
251
+ return baseReturn;
307
252
  }
308
253
  function useCurrentDoc(name) {
309
254
  const key = createDocInjectionKey(name);
@@ -1,9 +1,18 @@
1
1
  import { Ref, ShallowRef } from 'vue';
2
+ import { OpenDocOptions } from '../client/Patches.js';
3
+ import '../event-signal.js';
4
+ import '../json-patch/types.js';
5
+ import '../types.js';
6
+ import '../json-patch/JSONPatch.js';
7
+ import '@dabble/delta';
8
+ import '../client/ClientAlgorithm.js';
9
+ import '../BaseDoc-DkP3tUhT.js';
10
+ import '../client/PatchesStore.js';
2
11
 
3
12
  /**
4
13
  * Options for useManagedDocs composable.
5
14
  */
6
- interface UseManagedDocsOptions {
15
+ interface UseManagedDocsOptions extends OpenDocOptions {
7
16
  /**
8
17
  * Inject doc.id into state under this key on every state update.
9
18
  * Useful when the document ID is derived from the path but needed in the data.
@@ -5,7 +5,8 @@ import { areSetsEqual } from "./utils.js";
5
5
  const emptyPaths = /* @__PURE__ */ new Set();
6
6
  function useManagedDocs(pathsRef, initialData, reducer, options) {
7
7
  const { patches } = usePatchesContext();
8
- const { idProp } = options ?? {};
8
+ const { idProp, algorithm, metadata } = options ?? {};
9
+ const openDocOpts = { algorithm, metadata };
9
10
  const data = shallowRef(initialData);
10
11
  const docs = /* @__PURE__ */ new Map();
11
12
  const unsubscribes = /* @__PURE__ */ new Map();
@@ -28,7 +29,7 @@ function useManagedDocs(pathsRef, initialData, reducer, options) {
28
29
  });
29
30
  async function openPath(path) {
30
31
  try {
31
- const doc = await patches.openDoc(path);
32
+ const doc = await patches.openDoc(path, openDocOpts);
32
33
  if (currentPaths.has(path)) {
33
34
  docs.set(path, doc);
34
35
  let initialState = doc.state;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dabble/patches",
3
- "version": "0.7.6",
3
+ "version": "0.7.7",
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": {