@dabble/patches 0.5.21 → 0.6.0

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,4 @@
1
1
  import "../../chunk-IZ2YBCUP.js";
2
- import { clampTimestamp, getISO, timestampDiff } from "../../utils/dates.js";
3
2
  import { filterSoftWritesAgainstState } from "../../json-patch/utils/softWrites.js";
4
3
  import { applyChanges } from "../shared/applyChanges.js";
5
4
  import { createVersion } from "./createVersion.js";
@@ -28,7 +27,7 @@ async function commitChanges(store, docId, changes, sessionTimeoutMillis, option
28
27
  });
29
28
  }
30
29
  }
31
- const serverNow = getISO();
30
+ const serverNow = Date.now();
32
31
  let rev = baseRev + 1;
33
32
  changes.forEach((c) => {
34
33
  if (c.baseRev == null) c.baseRev = baseRev;
@@ -40,7 +39,7 @@ async function commitChanges(store, docId, changes, sessionTimeoutMillis, option
40
39
  if (!options?.historicalImport || !c.committedAt) {
41
40
  c.committedAt = serverNow;
42
41
  }
43
- c.createdAt = c.createdAt ? clampTimestamp(c.createdAt, serverNow) : serverNow;
42
+ c.createdAt = c.createdAt ? Math.min(c.createdAt, serverNow) : serverNow;
44
43
  });
45
44
  if (baseRev > currentRev) {
46
45
  throw new Error(
@@ -54,7 +53,7 @@ async function commitChanges(store, docId, changes, sessionTimeoutMillis, option
54
53
  }
55
54
  const lastChange = currentChanges[currentChanges.length - 1];
56
55
  const compareTime = options?.historicalImport ? changes[0].createdAt : serverNow;
57
- if (lastChange && timestampDiff(compareTime, lastChange.createdAt) > sessionTimeoutMillis) {
56
+ if (lastChange && compareTime - lastChange.createdAt > sessionTimeoutMillis) {
58
57
  await createVersion(store, docId, currentState, currentChanges);
59
58
  }
60
59
  const committedChanges = await store.listChanges(docId, {
@@ -66,7 +65,7 @@ async function commitChanges(store, docId, changes, sessionTimeoutMillis, option
66
65
  if (incomingChanges.length === 0) {
67
66
  return [committedChanges, []];
68
67
  }
69
- const isOfflineTimestamp = timestampDiff(serverNow, incomingChanges[0].createdAt) > sessionTimeoutMillis;
68
+ const isOfflineTimestamp = serverNow - incomingChanges[0].createdAt > sessionTimeoutMillis;
70
69
  if (isOfflineTimestamp || batchId) {
71
70
  const canFastForward = committedChanges.length === 0;
72
71
  const origin = options?.historicalImport ? "main" : canFastForward ? "main" : "offline-branch";
@@ -1,6 +1,5 @@
1
1
  import "../../chunk-IZ2YBCUP.js";
2
2
  import { createVersionMetadata } from "../../data/version.js";
3
- import { getISO } from "../../utils/dates.js";
4
3
  async function createVersion(store, docId, state, changes, metadata) {
5
4
  if (changes.length === 0) return;
6
5
  const startRev = changes[0].rev;
@@ -9,9 +8,8 @@ async function createVersion(store, docId, state, changes, metadata) {
9
8
  }
10
9
  const sessionMetadata = createVersionMetadata({
11
10
  origin: "main",
12
- // Convert client timestamps to UTC for version metadata (enables lexicographic sorting)
13
- startedAt: getISO(changes[0].createdAt),
14
- endedAt: getISO(changes[changes.length - 1].createdAt),
11
+ startedAt: changes[0].createdAt,
12
+ endedAt: changes[changes.length - 1].createdAt,
15
13
  endRev: changes[changes.length - 1].rev,
16
14
  startRev,
17
15
  ...metadata
@@ -1,6 +1,5 @@
1
1
  import "../../chunk-IZ2YBCUP.js";
2
2
  import { createVersionMetadata } from "../../data/version.js";
3
- import { getISO, timestampDiff } from "../../utils/dates.js";
4
3
  import { applyChanges } from "../shared/applyChanges.js";
5
4
  import { breakChanges } from "../shared/changeBatching.js";
6
5
  import { getStateAtRevision } from "./getStateAtRevision.js";
@@ -22,14 +21,14 @@ async function handleOfflineSessionsAndBatches(store, sessionTimeoutMillis, docI
22
21
  let sessionStartIndex = 0;
23
22
  for (let i = 1; i <= changes.length; i++) {
24
23
  const isLastChange = i === changes.length;
25
- const timeDiff = isLastChange ? Infinity : timestampDiff(changes[i].createdAt, changes[i - 1].createdAt);
24
+ const timeDiff = isLastChange ? Infinity : changes[i].createdAt - changes[i - 1].createdAt;
26
25
  if (timeDiff > sessionTimeoutMillis || isLastChange) {
27
26
  const sessionChanges = changes.slice(sessionStartIndex, i);
28
27
  if (sessionChanges.length > 0) {
29
- const isContinuation = !!lastVersion && timestampDiff(sessionChanges[0].createdAt, lastVersion.endedAt) <= sessionTimeoutMillis;
28
+ const isContinuation = !!lastVersion && sessionChanges[0].createdAt - lastVersion.endedAt <= sessionTimeoutMillis;
30
29
  if (isContinuation) {
31
30
  const mergedState = applyChanges(offlineBaseState, sessionChanges);
32
- const newEndedAt = getISO(sessionChanges[sessionChanges.length - 1].createdAt);
31
+ const newEndedAt = sessionChanges[sessionChanges.length - 1].createdAt;
33
32
  const newRev = sessionChanges[sessionChanges.length - 1].rev;
34
33
  await store.appendVersionChanges(docId, lastVersion.id, sessionChanges, newEndedAt, newRev, mergedState);
35
34
  offlineBaseState = mergedState;
@@ -41,9 +40,8 @@ async function handleOfflineSessionsAndBatches(store, sessionTimeoutMillis, docI
41
40
  groupId: batchId,
42
41
  origin,
43
42
  isOffline,
44
- // Convert client timestamps to UTC for version metadata (enables lexicographic sorting)
45
- startedAt: getISO(sessionChanges[0].createdAt),
46
- endedAt: getISO(sessionChanges[sessionChanges.length - 1].createdAt),
43
+ startedAt: sessionChanges[0].createdAt,
44
+ endedAt: sessionChanges[sessionChanges.length - 1].createdAt,
47
45
  endRev: sessionChanges[sessionChanges.length - 1].rev,
48
46
  startRev: sessionChanges[0].rev
49
47
  });
@@ -10,10 +10,10 @@ import '../json-patch/types.js';
10
10
  * - snapshots<{ docId: string; rev: number; state: any }> (primary key: docId)
11
11
  * - committedChanges<Change & { docId: string; }> (primary key: [docId, rev])
12
12
  * - pendingChanges<Change & { docId: string; }> (primary key: [docId, rev])
13
- * - docs<{ docId: string; committedRev: number; deleted?: boolean }> (primary key: docId)
13
+ * - docs<{ docId: string; committedRev: number; lastAttemptedSubmissionRev?: number; deleted?: boolean }> (primary key: docId)
14
14
  *
15
15
  * Under the hood, this class will store snapshots of the document only for committed state. It will not update the
16
- * committed state on *every* received committed change as this can cause issues with IndexedDB with many large updates.
16
+ * committed state on *every* received committed change as this can cause issues with IndexedDB with many small updates.
17
17
  * After every 200 committed changes, the class will save the current state to the snapshot store and delete the committed changes that went into it.
18
18
  * A snapshot will not be created if there are pending changes based on revisions older than the 200th committed change until those pending changes are committed.
19
19
  */
@@ -1,7 +1,6 @@
1
1
  import "../chunk-IZ2YBCUP.js";
2
2
  import { inc } from "alphacounter";
3
3
  import { createId } from "crypto-id";
4
- import { getLocalISO } from "../utils/dates.js";
5
4
  function createChangeId(rev) {
6
5
  return inc.from(rev) + createId(4);
7
6
  }
@@ -10,7 +9,7 @@ function createChange(baseRev, rev, ops, metadata) {
10
9
  return {
11
10
  id: createId(8),
12
11
  ops: baseRev,
13
- createdAt: getLocalISO(),
12
+ createdAt: Date.now(),
14
13
  ...rev
15
14
  };
16
15
  } else {
@@ -19,7 +18,7 @@ function createChange(baseRev, rev, ops, metadata) {
19
18
  baseRev,
20
19
  rev,
21
20
  ops,
22
- createdAt: getLocalISO(),
21
+ createdAt: Date.now(),
23
22
  ...metadata
24
23
  };
25
24
  }
@@ -3,6 +3,7 @@ import { add } from './json-patch/ops/add.js';
3
3
  import { bit } from './json-patch/ops/bitmask.js';
4
4
  import { copy } from './json-patch/ops/copy.js';
5
5
  import { increment } from './json-patch/ops/increment.js';
6
+ import { max, min } from './json-patch/ops/minmax.js';
6
7
  import { move } from './json-patch/ops/move.js';
7
8
  import { remove } from './json-patch/ops/remove.js';
8
9
  import { replace } from './json-patch/ops/replace.js';
@@ -18,6 +19,8 @@ declare function getTypes(custom?: JSONPatchOpHandlerMap): {
18
19
  '@inc': JSONPatchOpHandler;
19
20
  '@bit': JSONPatchOpHandler;
20
21
  '@txt': JSONPatchOpHandler;
22
+ '@max': JSONPatchOpHandler;
23
+ '@min': JSONPatchOpHandler;
21
24
  };
22
25
 
23
26
  declare const index_add: typeof add;
@@ -25,12 +28,14 @@ declare const index_bit: typeof bit;
25
28
  declare const index_copy: typeof copy;
26
29
  declare const index_getTypes: typeof getTypes;
27
30
  declare const index_increment: typeof increment;
31
+ declare const index_max: typeof max;
32
+ declare const index_min: typeof min;
28
33
  declare const index_move: typeof move;
29
34
  declare const index_remove: typeof remove;
30
35
  declare const index_replace: typeof replace;
31
36
  declare const index_test: typeof test;
32
37
  declare namespace index {
33
- export { index_add as add, index_bit as bit, index_copy as copy, index_getTypes as getTypes, index_increment as increment, index_move as move, index_remove as remove, index_replace as replace, index_test as test };
38
+ export { index_add as add, index_bit as bit, index_copy as copy, index_getTypes as getTypes, index_increment as increment, index_max as max, index_min as min, index_move as move, index_remove as remove, index_replace as replace, index_test as test };
34
39
  }
35
40
 
36
41
  export { getTypes as g, index as i };
package/dist/index.d.ts CHANGED
@@ -12,16 +12,16 @@ export { applyPatch } from './json-patch/applyPatch.js';
12
12
  export { composePatch } from './json-patch/composePatch.js';
13
13
  export { invertPatch } from './json-patch/invertPatch.js';
14
14
  export { applyBitmask, bit, bitmask, combineBitmasks } from './json-patch/ops/bitmask.js';
15
- export { i as defaultOps, g as getTypes } from './index-CvQws3AB.js';
15
+ export { i as defaultOps, g as getTypes } from './index-C7ZhU2kS.js';
16
16
  export { createPathProxy, pathProxy } from './json-patch/pathProxy.js';
17
17
  export { transformPatch } from './json-patch/transformPatch.js';
18
18
  export { JSONPatch, PathLike, WriteOptions } from './json-patch/JSONPatch.js';
19
19
  export { ApplyJSONPatchOptions, JSONPatchOpHandlerMap as JSONPatchCustomTypes, JSONPatchOp } from './json-patch/types.js';
20
20
  export { Branch, BranchStatus, Change, ChangeInput, ChangeMutator, CommitChangesOptions, DeleteDocOptions, DocumentTombstone, EditableBranchMetadata, EditableVersionMetadata, ListChangesOptions, ListVersionsOptions, PatchesSnapshot, PatchesState, PathProxy, SyncingState, VersionMetadata } from './types.js';
21
- export { clampTimestamp, extractTimezoneOffset, getISO, getLocalISO, getLocalTimezoneOffset, timestampDiff } from './utils/dates.js';
22
21
  export { add } from './json-patch/ops/add.js';
23
22
  export { copy } from './json-patch/ops/copy.js';
24
23
  export { increment } from './json-patch/ops/increment.js';
24
+ export { max, min } from './json-patch/ops/minmax.js';
25
25
  export { move } from './json-patch/ops/move.js';
26
26
  export { remove } from './json-patch/ops/remove.js';
27
27
  export { replace } from './json-patch/ops/replace.js';
package/dist/index.js CHANGED
@@ -5,7 +5,6 @@ export * from "./data/change.js";
5
5
  export * from "./data/version.js";
6
6
  export * from "./event-signal.js";
7
7
  export * from "./json-patch/index.js";
8
- export * from "./utils/dates.js";
9
8
  export {
10
9
  Delta
11
10
  };
@@ -70,6 +70,16 @@ declare class JSONPatch {
70
70
  * Decrements a numeric value by 1 or the given amount.
71
71
  */
72
72
  decrement(path: PathLike, value?: number): this;
73
+ /**
74
+ * Sets a value only if it's greater than the current value.
75
+ * Works with numbers or strings (ISO dates compare correctly).
76
+ */
77
+ max(path: PathLike, value: number | string): this;
78
+ /**
79
+ * Sets a value only if it's less than the current value.
80
+ * Works with numbers or strings (ISO dates compare correctly).
81
+ */
82
+ min(path: PathLike, value: number | string): this;
73
83
  /**
74
84
  * Flips a bit at the given index in a bitmask to the given value.
75
85
  */
@@ -96,6 +96,22 @@ class JSONPatch {
96
96
  decrement(path, value = 1) {
97
97
  return this.op("@inc", path, -value);
98
98
  }
99
+ /**
100
+ * Sets a value only if it's greater than the current value.
101
+ * Works with numbers or strings (ISO dates compare correctly).
102
+ */
103
+ max(path, value) {
104
+ if (value && value.toJSON) value = value.toJSON();
105
+ return this.op("@max", path, value);
106
+ }
107
+ /**
108
+ * Sets a value only if it's less than the current value.
109
+ * Works with numbers or strings (ISO dates compare correctly).
110
+ */
111
+ min(path, value) {
112
+ if (value && value.toJSON) value = value.toJSON();
113
+ return this.op("@min", path, value);
114
+ }
99
115
  /**
100
116
  * Flips a bit at the given index in a bitmask to the given value.
101
117
  */
@@ -2,7 +2,7 @@ export { applyPatch } from './applyPatch.js';
2
2
  export { composePatch } from './composePatch.js';
3
3
  export { invertPatch } from './invertPatch.js';
4
4
  export { applyBitmask, bit, bitmask, combineBitmasks } from './ops/bitmask.js';
5
- export { i as defaultOps, g as getTypes } from '../index-CvQws3AB.js';
5
+ export { i as defaultOps, g as getTypes } from '../index-C7ZhU2kS.js';
6
6
  export { createPathProxy, pathProxy } from './pathProxy.js';
7
7
  export { transformPatch } from './transformPatch.js';
8
8
  export { JSONPatch, PathLike, WriteOptions } from './JSONPatch.js';
@@ -10,6 +10,7 @@ export { ApplyJSONPatchOptions, JSONPatchOpHandlerMap as JSONPatchCustomTypes, J
10
10
  export { add } from './ops/add.js';
11
11
  export { copy } from './ops/copy.js';
12
12
  export { increment } from './ops/increment.js';
13
+ export { max, min } from './ops/minmax.js';
13
14
  export { move } from './ops/move.js';
14
15
  export { remove } from './ops/remove.js';
15
16
  export { replace } from './ops/replace.js';
@@ -3,8 +3,9 @@ export { add } from './add.js';
3
3
  export { bit } from './bitmask.js';
4
4
  export { copy } from './copy.js';
5
5
  export { increment } from './increment.js';
6
+ export { max, min } from './minmax.js';
6
7
  export { move } from './move.js';
7
8
  export { remove } from './remove.js';
8
9
  export { replace } from './replace.js';
9
10
  export { test } from './test.js';
10
- export { g as getTypes } from '../../index-CvQws3AB.js';
11
+ export { g as getTypes } from '../../index-C7ZhU2kS.js';
@@ -3,6 +3,7 @@ import { add } from "./add.js";
3
3
  import { bit } from "./bitmask.js";
4
4
  import { copy } from "./copy.js";
5
5
  import { increment } from "./increment.js";
6
+ import { max, min } from "./minmax.js";
6
7
  import { move } from "./move.js";
7
8
  import { remove } from "./remove.js";
8
9
  import { replace } from "./replace.js";
@@ -19,6 +20,8 @@ function getTypes(custom) {
19
20
  "@inc": increment,
20
21
  "@bit": bit,
21
22
  "@txt": text,
23
+ "@max": max,
24
+ "@min": min,
22
25
  ...custom
23
26
  };
24
27
  }
@@ -28,6 +31,8 @@ export {
28
31
  copy,
29
32
  getTypes,
30
33
  increment,
34
+ max,
35
+ min,
31
36
  move,
32
37
  remove,
33
38
  replace,
@@ -0,0 +1,14 @@
1
+ import { JSONPatchOpHandler } from '../types.js';
2
+
3
+ /**
4
+ * Set a value only if it's greater than the current value. Works with numbers or strings
5
+ * (ISO dates compare correctly). Useful for tracking "latest" values like lastModifiedAt.
6
+ */
7
+ declare const max: JSONPatchOpHandler;
8
+ /**
9
+ * Set a value only if it's less than the current value. Works with numbers or strings
10
+ * (ISO dates compare correctly). Useful for tracking "earliest" values like createdAt.
11
+ */
12
+ declare const min: JSONPatchOpHandler;
13
+
14
+ export { max, min };
@@ -0,0 +1,44 @@
1
+ import "../../chunk-IZ2YBCUP.js";
2
+ import { get } from "../utils/get.js";
3
+ import { updateRemovedOps } from "../utils/ops.js";
4
+ import { replace } from "./replace.js";
5
+ const max = {
6
+ like: "replace",
7
+ apply(state, path, value) {
8
+ const current = get(state, path);
9
+ if (current == null || value > current) {
10
+ return replace.apply(state, path, value);
11
+ }
12
+ },
13
+ transform(state, thisOp, otherOps) {
14
+ return updateRemovedOps(state, thisOp.path, otherOps, false, true);
15
+ },
16
+ invert(state, op, value, changedObj, isIndex) {
17
+ return replace.invert(state, op, value, changedObj, isIndex);
18
+ },
19
+ compose(_state, value1, value2) {
20
+ return value1 > value2 ? value1 : value2;
21
+ }
22
+ };
23
+ const min = {
24
+ like: "replace",
25
+ apply(state, path, value) {
26
+ const current = get(state, path);
27
+ if (current == null || value < current) {
28
+ return replace.apply(state, path, value);
29
+ }
30
+ },
31
+ transform(state, thisOp, otherOps) {
32
+ return updateRemovedOps(state, thisOp.path, otherOps, false, true);
33
+ },
34
+ invert(state, op, value, changedObj, isIndex) {
35
+ return replace.invert(state, op, value, changedObj, isIndex);
36
+ },
37
+ compose(_state, value1, value2) {
38
+ return value1 < value2 ? value1 : value2;
39
+ }
40
+ };
41
+ export {
42
+ max,
43
+ min
44
+ };
@@ -99,7 +99,7 @@ declare class RPCServer {
99
99
  createVersion(params: {
100
100
  docId: string;
101
101
  metadata: EditableVersionMetadata;
102
- }, ctx?: AuthContext): Promise<string>;
102
+ }, ctx?: AuthContext): Promise<string | null>;
103
103
  updateVersion(params: {
104
104
  docId: string;
105
105
  versionId: string;
@@ -31,7 +31,7 @@ declare class CompressedStoreBackend implements PatchesStoreBackend {
31
31
  saveChanges(docId: string, changes: Change[]): Promise<void>;
32
32
  listChanges(docId: string, options: ListChangesOptions): Promise<Change[]>;
33
33
  createVersion(docId: string, metadata: VersionMetadata, state: any, changes: Change[]): Promise<void>;
34
- appendVersionChanges(docId: string, versionId: string, changes: Change[], newEndedAt: string, newRev: number, newState: any): Promise<void>;
34
+ appendVersionChanges(docId: string, versionId: string, changes: Change[], newEndedAt: number, newRev: number, newState: any): Promise<void>;
35
35
  loadVersionChanges(docId: string, versionId: string): Promise<Change[]>;
36
36
  get loadLastVersionState(): PatchesStoreBackend['loadLastVersionState'];
37
37
  get saveLastVersionState(): PatchesStoreBackend['saveLastVersionState'];
@@ -3,7 +3,6 @@ import { createId } from "crypto-id";
3
3
  import { breakChanges } from "../algorithms/shared/changeBatching.js";
4
4
  import { createChange } from "../data/change.js";
5
5
  import { createVersionMetadata } from "../data/version.js";
6
- import { getISO } from "../utils/dates.js";
7
6
  class PatchesBranchManager {
8
7
  constructor(store, patchesServer, maxPayloadBytes) {
9
8
  this.store = store;
@@ -33,7 +32,7 @@ class PatchesBranchManager {
33
32
  }
34
33
  const stateAtRev = (await this.patchesServer.getStateAtRevision(docId, rev)).state;
35
34
  const branchDocId = this.store.createBranchId ? await Promise.resolve(this.store.createBranchId(docId)) : createId(22);
36
- const now = getISO();
35
+ const now = Date.now();
37
36
  const initialVersionMetadata = createVersionMetadata({
38
37
  origin: "main",
39
38
  // Branch doc versions are 'main' until merged
@@ -27,9 +27,9 @@ declare class PatchesHistoryManager {
27
27
  * Create a new named version snapshot of a document's current state.
28
28
  * @param docId The document ID.
29
29
  * @param name The name of the version.
30
- * @returns The ID of the created version.
30
+ * @returns The ID of the created version, or null if no changes to capture.
31
31
  */
32
- createVersion(docId: string, metadata?: EditableVersionMetadata): Promise<string>;
32
+ createVersion(docId: string, metadata?: EditableVersionMetadata): Promise<string | null>;
33
33
  /**
34
34
  * Updates the name of a specific version.
35
35
  * @param docId - The ID of the document.
@@ -22,7 +22,7 @@ class PatchesHistoryManager {
22
22
  * Create a new named version snapshot of a document's current state.
23
23
  * @param docId The document ID.
24
24
  * @param name The name of the version.
25
- * @returns The ID of the created version.
25
+ * @returns The ID of the created version, or null if no changes to capture.
26
26
  */
27
27
  async createVersion(docId, metadata) {
28
28
  assertVersionMetadata(metadata);
@@ -104,7 +104,7 @@ declare class PatchesServer {
104
104
  * @param metadata Optional metadata for the version.
105
105
  * @returns The ID of the created version.
106
106
  */
107
- captureCurrentVersion(docId: string, metadata?: EditableVersionMetadata): Promise<string>;
107
+ captureCurrentVersion(docId: string, metadata?: EditableVersionMetadata): Promise<string | null>;
108
108
  }
109
109
  declare function assertVersionMetadata(metadata?: EditableVersionMetadata): void;
110
110
 
@@ -7,7 +7,6 @@ import { applyChanges } from "../algorithms/shared/applyChanges.js";
7
7
  import { createChange } from "../data/change.js";
8
8
  import { signal } from "../event-signal.js";
9
9
  import { createJSONPatch } from "../json-patch/createJSONPatch.js";
10
- import { getISO } from "../utils/dates.js";
11
10
  import { CompressedStoreBackend } from "./CompressedStoreBackend.js";
12
11
  class PatchesServer {
13
12
  sessionTimeoutMillis;
@@ -108,7 +107,7 @@ class PatchesServer {
108
107
  const { rev: lastRev } = await this.getDoc(docId);
109
108
  await this.store.createTombstone({
110
109
  docId,
111
- deletedAt: getISO(),
110
+ deletedAt: Date.now(),
112
111
  lastRev,
113
112
  deletedByClientId: originClientId
114
113
  });
@@ -146,7 +145,7 @@ class PatchesServer {
146
145
  state = applyChanges(state, changes);
147
146
  const version = await createVersion(this.store, docId, state, changes, metadata);
148
147
  if (!version) {
149
- throw new Error(`No changes to create a version for doc ${docId}.`);
148
+ return null;
150
149
  }
151
150
  return version.id;
152
151
  }
@@ -27,7 +27,7 @@ interface PatchesStoreBackend {
27
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, newEndRev: number, newState: any): Promise<void>;
30
+ appendVersionChanges(docId: string, versionId: string, changes: Change[], newEndedAt: number, 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. */
package/dist/types.d.ts CHANGED
@@ -17,8 +17,8 @@ interface ChangeInput {
17
17
  baseRev?: number;
18
18
  /** Optional revision number. If omitted, server assigns based on current state. */
19
19
  rev?: number;
20
- /** Client-side ISO timestamp when the change was created (with timezone offset). */
21
- createdAt: string;
20
+ /** Unix timestamp in milliseconds when the change was created. */
21
+ createdAt: number;
22
22
  /** Optional batch identifier for grouping changes that belong to the same client batch (for multi-batch offline/large edits). */
23
23
  batchId?: string;
24
24
  /** Optional arbitrary metadata associated with the change. */
@@ -33,8 +33,8 @@ interface Change extends ChangeInput {
33
33
  baseRev: number;
34
34
  /** The revision number assigned by the server after commit. */
35
35
  rev: number;
36
- /** Server-side ISO timestamp when the change was committed (UTC with Z). */
37
- committedAt: string;
36
+ /** Unix timestamp in milliseconds when the change was committed. */
37
+ committedAt: number;
38
38
  }
39
39
  /**
40
40
  * Represents the state of a document in the OT protocol.
@@ -71,8 +71,8 @@ interface Branch {
71
71
  docId: string;
72
72
  /** The revision number on the source document where the branch occurred. */
73
73
  branchedAtRev: number;
74
- /** Server-side ISO timestamp when the branch was created (UTC with Z). */
75
- createdAt: string;
74
+ /** Unix timestamp in milliseconds when the branch was created. */
75
+ createdAt: number;
76
76
  /** Optional user-friendly name for the branch. */
77
77
  name?: string;
78
78
  /** Current status of the branch. */
@@ -89,14 +89,14 @@ type EditableBranchMetadata = Disallowed<Branch, 'id' | 'docId' | 'branchedAtRev
89
89
  interface DocumentTombstone {
90
90
  /** The ID of the deleted document. */
91
91
  docId: string;
92
- /** ISO timestamp when the document was deleted (UTC with Z). */
93
- deletedAt: string;
92
+ /** Unix timestamp in milliseconds when the document was deleted. */
93
+ deletedAt: number;
94
94
  /** The last revision number before deletion. */
95
95
  lastRev: number;
96
96
  /** Optional client ID that initiated the deletion. */
97
97
  deletedByClientId?: string;
98
- /** Optional ISO timestamp for automatic tombstone expiration (UTC with Z). */
99
- expiresAt?: string;
98
+ /** Optional Unix timestamp in milliseconds for automatic tombstone expiration. */
99
+ expiresAt?: number;
100
100
  }
101
101
  /**
102
102
  * Options for deleting a document.
@@ -122,10 +122,10 @@ interface VersionMetadata {
122
122
  isOffline?: boolean;
123
123
  /** User-defined name if origin is 'branch'. */
124
124
  branchName?: string;
125
- /** Server-side ISO timestamp of version start (UTC with Z). */
126
- startedAt: string;
127
- /** Server-side ISO timestamp of version end (UTC with Z). */
128
- endedAt: string;
125
+ /** Unix timestamp in milliseconds of version start. */
126
+ startedAt: number;
127
+ /** Unix timestamp in milliseconds of version end. */
128
+ endedAt: number;
129
129
  /** The ending revision number of this version (the last change's rev). */
130
130
  endRev: number;
131
131
  /** The starting revision number of this version (the first change's rev). */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dabble/patches",
3
- "version": "0.5.21",
3
+ "version": "0.6.0",
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": {
@@ -1,43 +0,0 @@
1
- /**
2
- * Date utility functions for creating and manipulating ISO 8601 timestamps.
3
- *
4
- * Client-side timestamps use local timezone offsets (e.g., +04:00).
5
- * Server-side timestamps use UTC with Z suffix.
6
- */
7
- /**
8
- * Converts a Date or ISO string to UTC format without milliseconds.
9
- * Example: "2025-12-26T10:00:00Z"
10
- */
11
- declare function getISO(date?: Date | string): string;
12
- /**
13
- * Formats a Date with a specific timezone offset string.
14
- * The date is adjusted to display the correct local time for that offset.
15
- */
16
- declare function getLocalISO(date?: Date | string, offset?: string): string;
17
- /**
18
- * Calculates milliseconds between two ISO timestamps.
19
- * Returns (a - b) in milliseconds.
20
- */
21
- declare function timestampDiff(a: string, b: string): number;
22
- /**
23
- * Clamps a timestamp to not exceed a limit, preserving the original timezone offset.
24
- * Returns the original if it's <= limit, otherwise returns limit in original's timezone.
25
- *
26
- * Example:
27
- * timestamp: "2025-12-26T18:00:00+04:00" (future)
28
- * limit: "2025-12-26T10:00:00Z" (server time)
29
- * result: "2025-12-26T14:00:00+04:00" (clamped, same instant as limit)
30
- */
31
- declare function clampTimestamp(timestamp: string, limit: string): string;
32
- /**
33
- * Extracts the timezone offset string from an ISO timestamp.
34
- * Returns "+04:00", "-05:00", or "Z".
35
- */
36
- declare function extractTimezoneOffset(iso: string): string;
37
- /**
38
- * Gets the local timezone offset string for the current environment.
39
- * Returns "+04:00", "-05:00", or "Z" for UTC.
40
- */
41
- declare function getLocalTimezoneOffset(): string;
42
-
43
- export { clampTimestamp, extractTimezoneOffset, getISO, getLocalISO, getLocalTimezoneOffset, timestampDiff };
@@ -1,47 +0,0 @@
1
- import "../chunk-IZ2YBCUP.js";
2
- function getISO(date = /* @__PURE__ */ new Date()) {
3
- const d = typeof date === "string" ? new Date(date) : date;
4
- return d.toISOString().replace(/\.\d{3}/, "");
5
- }
6
- function getLocalISO(date = /* @__PURE__ */ new Date(), offset = getLocalTimezoneOffset()) {
7
- const match = offset.match(/([+-])(\d{2}):(\d{2})/);
8
- if (offset === "Z" || !match) return getISO(date);
9
- const sign = match[1] === "+" ? 1 : -1;
10
- const offsetMinutes = sign * (parseInt(match[2]) * 60 + parseInt(match[3]));
11
- const localDate = new Date((typeof date === "string" ? new Date(date) : date).getTime() + offsetMinutes * 60 * 1e3);
12
- return getISO(localDate).slice(0, -1) + offset;
13
- }
14
- function timestampDiff(a, b) {
15
- return new Date(a).getTime() - new Date(b).getTime();
16
- }
17
- function clampTimestamp(timestamp, limit) {
18
- if (!timestamp || !limit) throw new Error("Timestamp and limit are required");
19
- const timestampDate = new Date(timestamp);
20
- const limitDate = new Date(limit);
21
- if (timestampDate <= limitDate) {
22
- return timestamp;
23
- }
24
- const offset = extractTimezoneOffset(timestamp);
25
- return getLocalISO(limitDate, offset);
26
- }
27
- function extractTimezoneOffset(iso) {
28
- if (!iso) return "Z";
29
- const match = iso.match(/([+-]\d{2}:\d{2}|Z)$/);
30
- return match ? match[1] : "Z";
31
- }
32
- function getLocalTimezoneOffset() {
33
- const offset = -(/* @__PURE__ */ new Date()).getTimezoneOffset();
34
- if (offset === 0) return "Z";
35
- const hours = Math.floor(Math.abs(offset) / 60);
36
- const mins = Math.abs(offset) % 60;
37
- const sign = offset >= 0 ? "+" : "-";
38
- return `${sign}${String(hours).padStart(2, "0")}:${String(mins).padStart(2, "0")}`;
39
- }
40
- export {
41
- clampTimestamp,
42
- extractTimezoneOffset,
43
- getISO,
44
- getLocalISO,
45
- getLocalTimezoneOffset,
46
- timestampDiff
47
- };