@dabble/patches 0.5.5 → 0.5.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.
@@ -3,8 +3,8 @@ import { createVersionMetadata } from "../../data/version.js";
3
3
  import { getISO } from "../../utils/dates.js";
4
4
  async function createVersion(store, docId, state, changes, metadata) {
5
5
  if (changes.length === 0) return;
6
- const baseRev = changes[0].baseRev;
7
- if (baseRev === void 0) {
6
+ const startRev = changes[0].baseRev;
7
+ if (startRev === void 0) {
8
8
  throw new Error(`Client changes must include baseRev for doc ${docId}.`);
9
9
  }
10
10
  const sessionMetadata = createVersionMetadata({
@@ -12,8 +12,8 @@ async function createVersion(store, docId, state, changes, metadata) {
12
12
  // Convert client timestamps to UTC for version metadata (enables lexicographic sorting)
13
13
  startedAt: getISO(changes[0].createdAt),
14
14
  endedAt: getISO(changes[changes.length - 1].createdAt),
15
- rev: changes[changes.length - 1].rev,
16
- baseRev,
15
+ endRev: changes[changes.length - 1].rev,
16
+ startRev,
17
17
  ...metadata
18
18
  });
19
19
  await store.createVersion(docId, sessionMetadata, state, changes);
@@ -5,11 +5,11 @@ async function getSnapshotAtRevision(store, docId, rev) {
5
5
  reverse: true,
6
6
  startAfter: rev ? rev + 1 : void 0,
7
7
  origin: "main",
8
- orderBy: "rev"
8
+ orderBy: "endRev"
9
9
  });
10
10
  const latestMainVersion = versions[0];
11
11
  const versionState = latestMainVersion && await store.loadVersionState(docId, latestMainVersion.id) || null;
12
- const versionRev = latestMainVersion?.rev ?? 0;
12
+ const versionRev = latestMainVersion?.endRev ?? 0;
13
13
  const changesSinceVersion = await store.listChanges(docId, {
14
14
  startAfter: versionRev,
15
15
  endBefore: rev ? rev + 1 : void 0
@@ -46,8 +46,8 @@ async function handleOfflineSessionsAndBatches(store, sessionTimeoutMillis, docI
46
46
  // Convert client timestamps to UTC for version metadata (enables lexicographic sorting)
47
47
  startedAt: getISO(sessionChanges[0].createdAt),
48
48
  endedAt: getISO(sessionChanges[sessionChanges.length - 1].createdAt),
49
- rev: sessionChanges[sessionChanges.length - 1].rev,
50
- baseRev
49
+ endRev: sessionChanges[sessionChanges.length - 1].rev,
50
+ startRev: baseRev
51
51
  });
52
52
  await store.createVersion(docId, sessionMetadata, offlineBaseState, sessionChanges);
53
53
  parentId = sessionMetadata.id;
@@ -142,7 +142,10 @@ function breakTextOp(origChange, textOp, maxBytes, startRev, sizeCalculator) {
142
142
  testBatchOps.push({ retain: retainToPrefixCurrentPiece });
143
143
  }
144
144
  testBatchOps.push(op);
145
- const testBatchSize = getSizeForStorage({ ...origChange, ops: [{ ...textOp, value: testBatchOps }] }, sizeCalculator);
145
+ const testBatchSize = getSizeForStorage(
146
+ { ...origChange, ops: [{ ...textOp, value: testBatchOps }] },
147
+ sizeCalculator
148
+ );
146
149
  if (currentOpsForNextChangePiece.length > 0 && testBatchSize > maxBytes) {
147
150
  flushCurrentChangePiece();
148
151
  }
@@ -88,7 +88,13 @@ class PatchesDoc {
88
88
  * @returns The generated Change objects.
89
89
  */
90
90
  change(mutator) {
91
- const changes = makeChange(this._snapshot, mutator, this._changeMetadata, this._maxStorageBytes, this._sizeCalculator);
91
+ const changes = makeChange(
92
+ this._snapshot,
93
+ mutator,
94
+ this._changeMetadata,
95
+ this._maxStorageBytes,
96
+ this._sizeCalculator
97
+ );
92
98
  if (changes.length === 0) {
93
99
  return changes;
94
100
  }
@@ -1,17 +1,17 @@
1
- import { Signal } from '../event-signal.js';
2
- import { ConnectionState } from './protocol/types.js';
3
- import { SyncingState, Change } from '../types.js';
4
1
  import { JSONRPCClient } from './protocol/JSONRPCClient.js';
5
- import { PatchesWebSocket } from './websocket/PatchesWebSocket.js';
6
- import { WebSocketOptions } from './websocket/WebSocketTransport.js';
2
+ import { Signal } from '../event-signal.js';
7
3
  import { SizeCalculator } from '../algorithms/shared/changeBatching.js';
8
4
  import { Patches } from '../client/Patches.js';
9
5
  import { PatchesStore } from '../client/PatchesStore.js';
6
+ import { SyncingState, Change } from '../types.js';
7
+ import { ConnectionState } from './protocol/types.js';
8
+ import { PatchesWebSocket } from './websocket/PatchesWebSocket.js';
9
+ import { WebSocketOptions } from './websocket/WebSocketTransport.js';
10
10
  import '../json-patch/JSONPatch.js';
11
11
  import '@dabble/delta';
12
12
  import '../json-patch/types.js';
13
- import './PatchesClient.js';
14
13
  import '../client/PatchesDoc.js';
14
+ import './PatchesClient.js';
15
15
 
16
16
  interface PatchesSyncState {
17
17
  online: boolean;
@@ -32,15 +32,15 @@ class PatchesBranchManager {
32
32
  throw new Error("Cannot create a branch from another branch.");
33
33
  }
34
34
  const stateAtRev = (await this.patchesServer.getStateAtRevision(docId, rev)).state;
35
- const branchDocId = createId();
35
+ const branchDocId = this.store.createBranchId ? await Promise.resolve(this.store.createBranchId(docId)) : createId(22);
36
36
  const now = getISO();
37
37
  const initialVersionMetadata = createVersionMetadata({
38
38
  origin: "main",
39
39
  // Branch doc versions are 'main' until merged
40
40
  startedAt: now,
41
41
  endedAt: now,
42
- rev,
43
- baseRev: rev,
42
+ endRev: rev,
43
+ startRev: rev,
44
44
  name: metadata?.name,
45
45
  groupId: branchDocId,
46
46
  branchName: metadata?.name
@@ -49,8 +49,8 @@ class PatchesBranchManager {
49
49
  const branch = {
50
50
  ...metadata,
51
51
  id: branchDocId,
52
- branchedFromId: docId,
53
- branchedRev: rev,
52
+ docId,
53
+ branchedAtRev: rev,
54
54
  createdAt: now,
55
55
  status: "open"
56
56
  };
@@ -88,8 +88,8 @@ class PatchesBranchManager {
88
88
  if (branch.status !== "open") {
89
89
  throw new Error(`Branch ${branchId} is not open (status: ${branch.status}). Cannot merge.`);
90
90
  }
91
- const sourceDocId = branch.branchedFromId;
92
- const branchStartRevOnSource = branch.branchedRev;
91
+ const sourceDocId = branch.docId;
92
+ const branchStartRevOnSource = branch.branchedAtRev;
93
93
  const branchChanges = await this.store.listChanges(branchId, {});
94
94
  if (branchChanges.length === 0) {
95
95
  console.log(`Branch ${branchId} has no changes to merge.`);
@@ -107,7 +107,7 @@ class PatchesBranchManager {
107
107
  const newVersionMetadata = createVersionMetadata({
108
108
  ...v,
109
109
  origin: versionOrigin,
110
- baseRev: branchStartRevOnSource,
110
+ startRev: branchStartRevOnSource,
111
111
  groupId: branchId,
112
112
  branchName: branch.name,
113
113
  // Keep branchName for traceability
@@ -149,7 +149,7 @@ class PatchesBranchManager {
149
149
  return committedMergeChanges;
150
150
  }
151
151
  }
152
- const nonModifiableMetadataFields = /* @__PURE__ */ new Set(["id", "branchedFromId", "branchedRev", "createdAt", "status"]);
152
+ const nonModifiableMetadataFields = /* @__PURE__ */ new Set(["id", "docId", "branchedAtRev", "createdAt", "status"]);
153
153
  function assertBranchMetadata(metadata) {
154
154
  if (!metadata) return;
155
155
  for (const key in metadata) {
@@ -24,10 +24,10 @@ interface PatchesStoreBackend {
24
24
  /** Update a version's metadata. */
25
25
  updateVersion(docId: string, versionId: string, metadata: EditableVersionMetadata): Promise<void>;
26
26
  /**
27
- * Appends changes to an existing version, updating its state snapshot, endedAt, and rev.
27
+ * Appends changes to an existing version, updating its state snapshot, endedAt, and endRev.
28
28
  * Used when a session spans multiple batch submissions.
29
29
  */
30
- appendVersionChanges(docId: string, versionId: string, changes: Change[], newEndedAt: string, newRev: number, newState: any): Promise<void>;
30
+ appendVersionChanges(docId: string, versionId: string, changes: Change[], newEndedAt: string, newEndRev: number, newState: any): Promise<void>;
31
31
  /** Lists version metadata based on filtering/sorting options. */
32
32
  listVersions(docId: string, options: ListVersionsOptions): Promise<VersionMetadata[]>;
33
33
  /** Loads the state snapshot for a specific version ID. */
@@ -41,6 +41,12 @@ interface PatchesStoreBackend {
41
41
  * Extends PatchesStoreBackend with methods specifically for managing branches.
42
42
  */
43
43
  interface BranchingStoreBackend extends PatchesStoreBackend {
44
+ /**
45
+ * Generates a unique ID for a new branch document.
46
+ * If not provided, a random 22-character ID is generated using createId().
47
+ * @param docId - The source document ID being branched from
48
+ */
49
+ createBranchId?(docId: string): Promise<string> | string;
44
50
  /** Lists metadata records for branches originating from a document. */
45
51
  listBranches(docId: string): Promise<Branch[]>;
46
52
  /** Loads the metadata record for a specific branch ID. */
@@ -0,0 +1,67 @@
1
+ import { JSX } from 'solid-js';
2
+ import { Patches } from '../client/Patches.js';
3
+ import { PatchesSync } from '../net/PatchesSync.js';
4
+ import '../event-signal.js';
5
+ import '../types.js';
6
+ import '../json-patch/JSONPatch.js';
7
+ import '@dabble/delta';
8
+ import '../json-patch/types.js';
9
+ import '../client/PatchesDoc.js';
10
+ import '../algorithms/shared/changeBatching.js';
11
+ import '../client/PatchesStore.js';
12
+ import '../net/protocol/JSONRPCClient.js';
13
+ import '../net/protocol/types.js';
14
+ import '../net/websocket/PatchesWebSocket.js';
15
+ import '../net/PatchesClient.js';
16
+ import '../net/websocket/WebSocketTransport.js';
17
+
18
+ /**
19
+ * Context value containing Patches and optional PatchesSync instances.
20
+ */
21
+ interface PatchesContextValue {
22
+ patches: Patches;
23
+ sync?: PatchesSync;
24
+ }
25
+ /**
26
+ * Props for the PatchesProvider component.
27
+ */
28
+ interface PatchesProviderProps {
29
+ patches: Patches;
30
+ sync?: PatchesSync;
31
+ children: JSX.Element;
32
+ }
33
+ /**
34
+ * Provider component for making Patches and PatchesSync available to child components.
35
+ *
36
+ * @example
37
+ * ```tsx
38
+ * import { PatchesProvider } from '@dabble/patches/solid';
39
+ * import { Patches, InMemoryStore } from '@dabble/patches/client';
40
+ *
41
+ * const patches = new Patches({ store: new InMemoryStore() });
42
+ *
43
+ * <PatchesProvider patches={patches}>
44
+ * <App />
45
+ * </PatchesProvider>
46
+ * ```
47
+ */
48
+ declare function PatchesProvider(props: PatchesProviderProps): any;
49
+ /**
50
+ * Hook to access the Patches context.
51
+ *
52
+ * @throws Error if called outside of a PatchesProvider
53
+ * @returns PatchesContextValue containing patches and optional sync instances
54
+ *
55
+ * @example
56
+ * ```tsx
57
+ * import { usePatchesContext } from '@dabble/patches/solid';
58
+ *
59
+ * function MyComponent() {
60
+ * const { patches, sync } = usePatchesContext();
61
+ * // Use patches and sync...
62
+ * }
63
+ * ```
64
+ */
65
+ declare function usePatchesContext(): PatchesContextValue;
66
+
67
+ export { type PatchesContextValue, PatchesProvider, type PatchesProviderProps, usePatchesContext };
@@ -0,0 +1,20 @@
1
+ import "../chunk-IZ2YBCUP.js";
2
+ import { createContext, useContext } from "solid-js";
3
+ const PatchesContext = createContext();
4
+ function PatchesProvider(props) {
5
+ const value = { patches: props.patches, sync: props.sync };
6
+ return /* @__PURE__ */ React.createElement(PatchesContext.Provider, { value, children: props.children });
7
+ }
8
+ function usePatchesContext() {
9
+ const context = useContext(PatchesContext);
10
+ if (!context) {
11
+ throw new Error(
12
+ "usePatchesContext must be called within a PatchesProvider. Make sure your component is wrapped with <PatchesProvider>."
13
+ );
14
+ }
15
+ return context;
16
+ }
17
+ export {
18
+ PatchesProvider,
19
+ usePatchesContext
20
+ };
@@ -0,0 +1,88 @@
1
+ import { Patches } from '../client/Patches.js';
2
+ import { PatchesDoc } from '../client/PatchesDoc.js';
3
+ import '../event-signal.js';
4
+ import '../types.js';
5
+ import '../json-patch/JSONPatch.js';
6
+ import '@dabble/delta';
7
+ import '../json-patch/types.js';
8
+ import '../client/PatchesStore.js';
9
+ import '../algorithms/shared/changeBatching.js';
10
+
11
+ /**
12
+ * Reference counting manager for PatchesDoc instances.
13
+ *
14
+ * Tracks how many Solid components are using each document and only opens/closes
15
+ * documents when the reference count goes to/from zero.
16
+ *
17
+ * This prevents the footgun where multiple components open the same doc but the
18
+ * first one to unmount closes it for everyone else.
19
+ */
20
+ declare class DocManager {
21
+ private refCounts;
22
+ private pendingOps;
23
+ /**
24
+ * Opens a document with reference counting.
25
+ *
26
+ * - If this is the first reference, calls patches.openDoc()
27
+ * - If doc is already open, returns existing instance and increments count
28
+ * - Handles concurrent opens to the same doc safely
29
+ *
30
+ * @param patches - Patches instance
31
+ * @param docId - Document ID to open
32
+ * @returns Promise resolving to PatchesDoc instance
33
+ */
34
+ openDoc<T extends object>(patches: Patches, docId: string): Promise<PatchesDoc<T>>;
35
+ /**
36
+ * Closes a document with reference counting.
37
+ *
38
+ * - Decrements the reference count
39
+ * - Only calls patches.closeDoc() when count reaches zero
40
+ * - Safe to call even if doc was never opened
41
+ *
42
+ * @param patches - Patches instance
43
+ * @param docId - Document ID to close
44
+ */
45
+ closeDoc(patches: Patches, docId: string): Promise<void>;
46
+ /**
47
+ * Increments the reference count for a document without opening it.
48
+ *
49
+ * Used in explicit mode to track usage and prevent premature closes
50
+ * from autoClose mode.
51
+ *
52
+ * @param docId - Document ID
53
+ */
54
+ incrementRefCount(docId: string): void;
55
+ /**
56
+ * Decrements the reference count for a document without closing it.
57
+ *
58
+ * Used in explicit mode to release usage tracking.
59
+ *
60
+ * @param docId - Document ID
61
+ */
62
+ decrementRefCount(docId: string): void;
63
+ /**
64
+ * Gets the current reference count for a document.
65
+ *
66
+ * Useful for debugging or advanced use cases.
67
+ *
68
+ * @param docId - Document ID
69
+ * @returns Current reference count (0 if not tracked)
70
+ */
71
+ getRefCount(docId: string): number;
72
+ /**
73
+ * Clears all reference counts without closing documents.
74
+ *
75
+ * Use with caution - this is mainly for testing or cleanup scenarios
76
+ * where you want to reset the manager state.
77
+ */
78
+ reset(): void;
79
+ }
80
+ /**
81
+ * Gets or creates a DocManager for a Patches instance.
82
+ *
83
+ * @param patches - Patches instance
84
+ * @returns DocManager for this Patches instance
85
+ */
86
+ declare function getDocManager(patches: Patches): DocManager;
87
+
88
+ export { DocManager, getDocManager };
@@ -0,0 +1,125 @@
1
+ import "../chunk-IZ2YBCUP.js";
2
+ class DocManager {
3
+ refCounts = /* @__PURE__ */ new Map();
4
+ pendingOps = /* @__PURE__ */ new Map();
5
+ /**
6
+ * Opens a document with reference counting.
7
+ *
8
+ * - If this is the first reference, calls patches.openDoc()
9
+ * - If doc is already open, returns existing instance and increments count
10
+ * - Handles concurrent opens to the same doc safely
11
+ *
12
+ * @param patches - Patches instance
13
+ * @param docId - Document ID to open
14
+ * @returns Promise resolving to PatchesDoc instance
15
+ */
16
+ async openDoc(patches, docId) {
17
+ const currentCount = this.refCounts.get(docId) || 0;
18
+ if (currentCount === 0 && this.pendingOps.has(docId)) {
19
+ const doc = await this.pendingOps.get(docId);
20
+ this.refCounts.set(docId, (this.refCounts.get(docId) || 0) + 1);
21
+ return doc;
22
+ }
23
+ if (currentCount > 0) {
24
+ this.refCounts.set(docId, currentCount + 1);
25
+ const doc = patches.getOpenDoc(docId);
26
+ if (!doc) {
27
+ throw new Error(`Document ${docId} has ref count ${currentCount} but is not open in Patches`);
28
+ }
29
+ return doc;
30
+ }
31
+ const openPromise = patches.openDoc(docId);
32
+ this.pendingOps.set(docId, openPromise);
33
+ try {
34
+ const doc = await openPromise;
35
+ this.refCounts.set(docId, 1);
36
+ return doc;
37
+ } catch (error) {
38
+ this.refCounts.delete(docId);
39
+ throw error;
40
+ } finally {
41
+ this.pendingOps.delete(docId);
42
+ }
43
+ }
44
+ /**
45
+ * Closes a document with reference counting.
46
+ *
47
+ * - Decrements the reference count
48
+ * - Only calls patches.closeDoc() when count reaches zero
49
+ * - Safe to call even if doc was never opened
50
+ *
51
+ * @param patches - Patches instance
52
+ * @param docId - Document ID to close
53
+ */
54
+ async closeDoc(patches, docId) {
55
+ const currentCount = this.refCounts.get(docId) || 0;
56
+ if (currentCount === 0) {
57
+ return;
58
+ }
59
+ if (currentCount === 1) {
60
+ this.refCounts.delete(docId);
61
+ await patches.closeDoc(docId);
62
+ } else {
63
+ this.refCounts.set(docId, currentCount - 1);
64
+ }
65
+ }
66
+ /**
67
+ * Increments the reference count for a document without opening it.
68
+ *
69
+ * Used in explicit mode to track usage and prevent premature closes
70
+ * from autoClose mode.
71
+ *
72
+ * @param docId - Document ID
73
+ */
74
+ incrementRefCount(docId) {
75
+ const currentCount = this.refCounts.get(docId) || 0;
76
+ this.refCounts.set(docId, currentCount + 1);
77
+ }
78
+ /**
79
+ * Decrements the reference count for a document without closing it.
80
+ *
81
+ * Used in explicit mode to release usage tracking.
82
+ *
83
+ * @param docId - Document ID
84
+ */
85
+ decrementRefCount(docId) {
86
+ const currentCount = this.refCounts.get(docId) || 0;
87
+ if (currentCount > 0) {
88
+ this.refCounts.set(docId, currentCount - 1);
89
+ }
90
+ }
91
+ /**
92
+ * Gets the current reference count for a document.
93
+ *
94
+ * Useful for debugging or advanced use cases.
95
+ *
96
+ * @param docId - Document ID
97
+ * @returns Current reference count (0 if not tracked)
98
+ */
99
+ getRefCount(docId) {
100
+ return this.refCounts.get(docId) || 0;
101
+ }
102
+ /**
103
+ * Clears all reference counts without closing documents.
104
+ *
105
+ * Use with caution - this is mainly for testing or cleanup scenarios
106
+ * where you want to reset the manager state.
107
+ */
108
+ reset() {
109
+ this.refCounts.clear();
110
+ this.pendingOps.clear();
111
+ }
112
+ }
113
+ const managers = /* @__PURE__ */ new WeakMap();
114
+ function getDocManager(patches) {
115
+ let manager = managers.get(patches);
116
+ if (!manager) {
117
+ manager = new DocManager();
118
+ managers.set(patches, manager);
119
+ }
120
+ return manager;
121
+ }
122
+ export {
123
+ DocManager,
124
+ getDocManager
125
+ };
@@ -0,0 +1,19 @@
1
+ export { PatchesContextValue, PatchesProvider, PatchesProviderProps, usePatchesContext } from './context.js';
2
+ export { MaybeAccessor, UsePatchesDocOptions, UsePatchesDocReturn, UsePatchesSyncReturn, createPatchesDoc, usePatchesDoc, usePatchesSync } from './primitives.js';
3
+ export { DocManager, getDocManager } from './doc-manager.js';
4
+ import 'solid-js';
5
+ import '../client/Patches.js';
6
+ import '../event-signal.js';
7
+ import '../types.js';
8
+ import '../json-patch/JSONPatch.js';
9
+ import '@dabble/delta';
10
+ import '../json-patch/types.js';
11
+ import '../client/PatchesDoc.js';
12
+ import '../algorithms/shared/changeBatching.js';
13
+ import '../client/PatchesStore.js';
14
+ import '../net/PatchesSync.js';
15
+ import '../net/protocol/JSONRPCClient.js';
16
+ import '../net/protocol/types.js';
17
+ import '../net/websocket/PatchesWebSocket.js';
18
+ import '../net/PatchesClient.js';
19
+ import '../net/websocket/WebSocketTransport.js';
@@ -0,0 +1,17 @@
1
+ import "../chunk-IZ2YBCUP.js";
2
+ import { PatchesProvider, usePatchesContext } from "./context.js";
3
+ import {
4
+ usePatchesDoc,
5
+ usePatchesSync,
6
+ createPatchesDoc
7
+ } from "./primitives.js";
8
+ import { getDocManager, DocManager } from "./doc-manager.js";
9
+ export {
10
+ DocManager,
11
+ PatchesProvider,
12
+ createPatchesDoc,
13
+ getDocManager,
14
+ usePatchesContext,
15
+ usePatchesDoc,
16
+ usePatchesSync
17
+ };