@dabble/patches 0.7.16 → 0.7.18

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.
@@ -81,8 +81,6 @@ async function commitChanges(store, docId, changes, sessionTimeoutMillis, option
81
81
  baseRev,
82
82
  batchId,
83
83
  origin,
84
- true,
85
- // isOffline
86
84
  maxStorageBytes
87
85
  );
88
86
  if (canFastForward) {
@@ -7,15 +7,19 @@ import '@dabble/delta';
7
7
  /**
8
8
  * Handles offline/large batch versioning logic for multi-batch uploads.
9
9
  * Groups changes into sessions, merges with previous batch if needed, and creates/extends versions.
10
+ *
11
+ * Each session's `isOffline` metadata is determined per-session by comparing `committedAt` and
12
+ * `createdAt` on the first change — if the gap exceeds `sessionTimeoutMillis`, the session is
13
+ * marked offline.
14
+ *
10
15
  * @param docId Document ID
11
16
  * @param changes The incoming changes (all with the same batchId)
12
17
  * @param baseRev The base revision for the batch
13
18
  * @param batchId The batch identifier
14
19
  * @param origin The origin to use for created versions (default: 'offline-branch')
15
- * @param isOffline Whether these changes were created offline (metadata flag)
16
20
  * @param maxStorageBytes If set, break collapsed changes that exceed this size
17
21
  * @returns The changes (collapsed into one if divergent, unchanged if fast-forward)
18
22
  */
19
- declare function handleOfflineSessionsAndBatches(store: OTStoreBackend, sessionTimeoutMillis: number, docId: string, changes: Change[], baseRev: number, batchId?: string, origin?: 'main' | 'offline-branch', isOffline?: boolean, maxStorageBytes?: number): Promise<Change[]>;
23
+ declare function handleOfflineSessionsAndBatches(store: OTStoreBackend, sessionTimeoutMillis: number, docId: string, changes: Change[], baseRev: number, batchId?: string, origin?: 'main' | 'offline-branch', maxStorageBytes?: number): Promise<Change[]>;
20
24
 
21
25
  export { handleOfflineSessionsAndBatches };
@@ -3,7 +3,7 @@ import { createVersionMetadata } from "../../../data/version.js";
3
3
  import { applyChanges } from "../shared/applyChanges.js";
4
4
  import { breakChanges } from "../shared/changeBatching.js";
5
5
  import { getStateAtRevision } from "./getStateAtRevision.js";
6
- async function handleOfflineSessionsAndBatches(store, sessionTimeoutMillis, docId, changes, baseRev, batchId, origin = "offline-branch", isOffline = true, maxStorageBytes) {
6
+ async function handleOfflineSessionsAndBatches(store, sessionTimeoutMillis, docId, changes, baseRev, batchId, origin = "offline-branch", maxStorageBytes) {
7
7
  const [lastVersion] = await store.listVersions(docId, {
8
8
  groupId: batchId,
9
9
  reverse: true,
@@ -35,11 +35,12 @@ async function handleOfflineSessionsAndBatches(store, sessionTimeoutMillis, docI
35
35
  parentId = lastVersion.parentId;
36
36
  } else {
37
37
  offlineBaseState = applyChanges(offlineBaseState, sessionChanges);
38
+ const isOffline = sessionChanges[0].committedAt - sessionChanges[0].createdAt > sessionTimeoutMillis;
38
39
  const sessionMetadata = createVersionMetadata({
39
40
  parentId,
40
41
  groupId: batchId,
41
42
  origin,
42
- isOffline,
43
+ ...isOffline ? { isOffline } : {},
43
44
  startedAt: sessionChanges[0].createdAt,
44
45
  endedAt: sessionChanges[sessionChanges.length - 1].createdAt,
45
46
  endRev: sessionChanges[sessionChanges.length - 1].rev,
@@ -56,6 +56,14 @@ interface ClientAlgorithm {
56
56
  * @returns The changes created (for broadcast to other tabs)
57
57
  */
58
58
  handleDocChange<T extends object>(docId: string, ops: JSONPatchOp[], doc: PatchesDoc<T> | undefined, metadata: Record<string, any>): Promise<Change[]>;
59
+ /**
60
+ * Read-only check for whether a document has any pending local data.
61
+ * - OT: Checks pendingChanges
62
+ * - LWW: Checks both pendingOps and sendingChange
63
+ *
64
+ * Unlike getPendingToSend, this has no side effects.
65
+ */
66
+ hasPending(docId: string): Promise<boolean>;
59
67
  /**
60
68
  * Gets pending data to send to the server.
61
69
  * - OT: Returns all pending changes (may batch)
@@ -28,6 +28,7 @@ declare class LWWAlgorithm implements ClientAlgorithm {
28
28
  createDoc<T extends object>(docId: string, snapshot?: PatchesSnapshot<T>): PatchesDoc<T>;
29
29
  loadDoc(docId: string): Promise<PatchesSnapshot | undefined>;
30
30
  handleDocChange<T extends object>(docId: string, ops: JSONPatchOp[], doc: PatchesDoc<T> | undefined, metadata: Record<string, any>): Promise<Change[]>;
31
+ hasPending(docId: string): Promise<boolean>;
31
32
  getPendingToSend(docId: string): Promise<Change[] | null>;
32
33
  applyServerChanges<T extends object>(docId: string, serverChanges: Change[], doc: PatchesDoc<T> | undefined): Promise<Change[]>;
33
34
  confirmSent(docId: string, _changes: Change[]): Promise<void>;
@@ -30,6 +30,12 @@ class LWWAlgorithm {
30
30
  }
31
31
  return changes;
32
32
  }
33
+ async hasPending(docId) {
34
+ const sendingChange = await this.store.getSendingChange(docId);
35
+ if (sendingChange) return true;
36
+ const pendingOps = await this.store.getPendingOps(docId);
37
+ return pendingOps.length > 0;
38
+ }
33
39
  async getPendingToSend(docId) {
34
40
  const sendingChange = await this.store.getSendingChange(docId);
35
41
  if (sendingChange) {
@@ -23,6 +23,7 @@ declare class OTAlgorithm implements ClientAlgorithm {
23
23
  createDoc<T extends object>(docId: string, snapshot?: PatchesSnapshot<T>): PatchesDoc<T>;
24
24
  loadDoc(docId: string): Promise<PatchesSnapshot | undefined>;
25
25
  handleDocChange<T extends object>(docId: string, ops: JSONPatchOp[], doc: PatchesDoc<T> | undefined, metadata: Record<string, any>): Promise<Change[]>;
26
+ hasPending(docId: string): Promise<boolean>;
26
27
  getPendingToSend(docId: string): Promise<Change[] | null>;
27
28
  applyServerChanges<T extends object>(docId: string, serverChanges: Change[], doc: PatchesDoc<T> | undefined): Promise<Change[]>;
28
29
  confirmSent(_docId: string, _changes: Change[]): Promise<void>;
@@ -40,6 +40,10 @@ class OTAlgorithm {
40
40
  }
41
41
  return changes;
42
42
  }
43
+ async hasPending(docId) {
44
+ const pending = await this.store.getPendingChanges(docId);
45
+ return pending.length > 0;
46
+ }
43
47
  async getPendingToSend(docId) {
44
48
  const pending = await this.store.getPendingChanges(docId);
45
49
  return pending.length > 0 ? pending : null;
@@ -164,10 +164,10 @@ class PatchesSync extends (_a = ReadonlyStoreClass, _syncDoc_dec = [blockable],
164
164
  const syncedEntries = {};
165
165
  for (const doc of activeDocs) {
166
166
  const algorithm = this._getAlgorithm(doc.docId);
167
- const pending = await algorithm.getPendingToSend(doc.docId);
167
+ const hasPending = await algorithm.hasPending(doc.docId);
168
168
  const entry = {
169
169
  committedRev: doc.committedRev,
170
- hasPending: pending != null && pending.length > 0,
170
+ hasPending,
171
171
  syncStatus: doc.committedRev === 0 ? "unsynced" : "synced",
172
172
  syncError: void 0,
173
173
  isLoaded: false
@@ -291,7 +291,7 @@ class PatchesSync extends (_a = ReadonlyStoreClass, _syncDoc_dec = [blockable],
291
291
  await algorithm.confirmSent(docId, changeBatch);
292
292
  pending = await algorithm.getPendingToSend(docId) ?? [];
293
293
  }
294
- const stillHasPending = pending != null && pending.length > 0;
294
+ const stillHasPending = await algorithm.hasPending(docId);
295
295
  this._updateDocSyncState(docId, { hasPending: stillHasPending, syncStatus: "synced" });
296
296
  } catch (err) {
297
297
  if (this._isDocDeletedError(err)) {
@@ -381,11 +381,11 @@ class PatchesSync extends (_a = ReadonlyStoreClass, _syncDoc_dec = [blockable],
381
381
  for (const docId of newIds) {
382
382
  const algorithm = this._getAlgorithm(docId);
383
383
  const committedRev = await algorithm.getCommittedRev(docId);
384
- const pending = await algorithm.getPendingToSend(docId);
384
+ const hasPending = await algorithm.hasPending(docId);
385
385
  docData.push({
386
386
  docId,
387
387
  committedRev,
388
- hasPending: pending != null && pending.length > 0
388
+ hasPending
389
389
  });
390
390
  }
391
391
  batch(() => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dabble/patches",
3
- "version": "0.7.16",
3
+ "version": "0.7.18",
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": {