@arcmantle/chronicle 0.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (113) hide show
  1. package/README.md +563 -0
  2. package/dist/api-methods.d.ts +28 -0
  3. package/dist/api-methods.d.ts.map +1 -0
  4. package/dist/api-methods.js +206 -0
  5. package/dist/api-methods.js.map +1 -0
  6. package/dist/api.d.ts +12 -0
  7. package/dist/api.d.ts.map +1 -0
  8. package/dist/api.js +30 -0
  9. package/dist/api.js.map +1 -0
  10. package/dist/array-mutations.d.ts +31 -0
  11. package/dist/array-mutations.d.ts.map +1 -0
  12. package/dist/array-mutations.js +50 -0
  13. package/dist/array-mutations.js.map +1 -0
  14. package/dist/batch-transaction.d.ts +25 -0
  15. package/dist/batch-transaction.d.ts.map +1 -0
  16. package/dist/batch-transaction.js +138 -0
  17. package/dist/batch-transaction.js.map +1 -0
  18. package/dist/chronicle.d.ts +41 -0
  19. package/dist/chronicle.d.ts.map +1 -0
  20. package/dist/chronicle.js +40 -0
  21. package/dist/chronicle.js.map +1 -0
  22. package/dist/collection-adapters.d.ts +29 -0
  23. package/dist/collection-adapters.d.ts.map +1 -0
  24. package/dist/collection-adapters.js +184 -0
  25. package/dist/collection-adapters.js.map +1 -0
  26. package/dist/config.d.ts +7 -0
  27. package/dist/config.d.ts.map +1 -0
  28. package/dist/config.js +11 -0
  29. package/dist/config.js.map +1 -0
  30. package/dist/grouping.d.ts +17 -0
  31. package/dist/grouping.d.ts.map +1 -0
  32. package/dist/grouping.js +35 -0
  33. package/dist/grouping.js.map +1 -0
  34. package/dist/history-recorder.d.ts +39 -0
  35. package/dist/history-recorder.d.ts.map +1 -0
  36. package/dist/history-recorder.js +112 -0
  37. package/dist/history-recorder.js.map +1 -0
  38. package/dist/history.d.ts +29 -0
  39. package/dist/history.d.ts.map +1 -0
  40. package/dist/history.js +47 -0
  41. package/dist/history.js.map +1 -0
  42. package/dist/listener-affinity.d.ts +16 -0
  43. package/dist/listener-affinity.d.ts.map +1 -0
  44. package/dist/listener-affinity.js +58 -0
  45. package/dist/listener-affinity.js.map +1 -0
  46. package/dist/listener-trie.d.ts +10 -0
  47. package/dist/listener-trie.d.ts.map +1 -0
  48. package/dist/listener-trie.js +83 -0
  49. package/dist/listener-trie.js.map +1 -0
  50. package/dist/nameof.d.ts +12 -0
  51. package/dist/nameof.d.ts.map +1 -0
  52. package/dist/nameof.js +30 -0
  53. package/dist/nameof.js.map +1 -0
  54. package/dist/path-key.d.ts +11 -0
  55. package/dist/path-key.d.ts.map +1 -0
  56. package/dist/path-key.js +11 -0
  57. package/dist/path-key.js.map +1 -0
  58. package/dist/path.d.ts +7 -0
  59. package/dist/path.d.ts.map +1 -0
  60. package/dist/path.js +53 -0
  61. package/dist/path.js.map +1 -0
  62. package/dist/proxy-cache.d.ts +32 -0
  63. package/dist/proxy-cache.d.ts.map +1 -0
  64. package/dist/proxy-cache.js +72 -0
  65. package/dist/proxy-cache.js.map +1 -0
  66. package/dist/proxy-factory.d.ts +17 -0
  67. package/dist/proxy-factory.d.ts.map +1 -0
  68. package/dist/proxy-factory.js +124 -0
  69. package/dist/proxy-factory.js.map +1 -0
  70. package/dist/schedule-queue.d.ts +10 -0
  71. package/dist/schedule-queue.d.ts.map +1 -0
  72. package/dist/schedule-queue.js +112 -0
  73. package/dist/schedule-queue.js.map +1 -0
  74. package/dist/snapshot-diff.d.ts +6 -0
  75. package/dist/snapshot-diff.d.ts.map +1 -0
  76. package/dist/snapshot-diff.js +67 -0
  77. package/dist/snapshot-diff.js.map +1 -0
  78. package/dist/symbol-id.d.ts +3 -0
  79. package/dist/symbol-id.d.ts.map +1 -0
  80. package/dist/symbol-id.js +16 -0
  81. package/dist/symbol-id.js.map +1 -0
  82. package/dist/types.d.ts +48 -0
  83. package/dist/types.d.ts.map +1 -0
  84. package/dist/types.js +3 -0
  85. package/dist/types.js.map +1 -0
  86. package/dist/undo-redo.d.ts +12 -0
  87. package/dist/undo-redo.d.ts.map +1 -0
  88. package/dist/undo-redo.js +216 -0
  89. package/dist/undo-redo.js.map +1 -0
  90. package/package.json +45 -0
  91. package/src/api-methods.ts +292 -0
  92. package/src/api.ts +53 -0
  93. package/src/array-mutations.ts +64 -0
  94. package/src/batch-transaction.ts +183 -0
  95. package/src/chronicle.ts +100 -0
  96. package/src/collection-adapters.ts +224 -0
  97. package/src/config.ts +16 -0
  98. package/src/grouping.ts +47 -0
  99. package/src/history-recorder.ts +145 -0
  100. package/src/history.ts +75 -0
  101. package/src/listener-affinity.ts +69 -0
  102. package/src/listener-trie.ts +103 -0
  103. package/src/nameof.ts +42 -0
  104. package/src/path-key.ts +10 -0
  105. package/src/path.ts +69 -0
  106. package/src/proxy-cache.ts +86 -0
  107. package/src/proxy-factory.ts +168 -0
  108. package/src/schedule-queue.ts +144 -0
  109. package/src/snapshot-diff.ts +90 -0
  110. package/src/symbol-id.ts +20 -0
  111. package/src/tsconfig.json +3 -0
  112. package/src/types.ts +59 -0
  113. package/src/undo-redo.ts +249 -0
@@ -0,0 +1,292 @@
1
+ import { clearLastUngrouped, historyDelete, historyGet } from './history.ts';
2
+ import { addListenerToTrie, cleanupListenerBucket, ensureListenerBucket, removeListenerFromTrie } from './listener-trie.ts';
3
+ import { nameofSegments } from './nameof.ts';
4
+ import { clearProxyCache as pfClearProxyCache } from './proxy-factory.ts';
5
+ import { buildEffectiveListener, flush as scheduleFlush, pause as schedulePause, resume as scheduleResume } from './schedule-queue.ts';
6
+ import { cloneWithOptions, diffValues, originalSnapshotCache } from './snapshot-diff.ts';
7
+ import type { ChangeListener, ChangeRecord, DiffRecord, ListenerOptions, PathMode, PathSelector } from './types.ts';
8
+ import { canRedo as coreCanRedo, canUndo as coreCanUndo, clearRedoCache, redo as coreRedo, redoGroups as coreRedoGroups, resumeWrites, suspendWrites, undo as coreUndo, undoGroups as coreUndoGroups, undoSince as coreUndoSince } from './undo-redo.ts';
9
+
10
+
11
+ export interface ApiDeps {
12
+ getRoot: (obj: object) => object;
13
+ }
14
+
15
+
16
+ const isObject = (v: unknown): v is Record<string, unknown> => typeof v === 'object' && v !== null;
17
+
18
+
19
+ export interface ChronicleApiMethods {
20
+ listen: <T extends object>(
21
+ object: T,
22
+ selector: PathSelector<T>,
23
+ listener: ChangeListener,
24
+ modeOrOptions?: PathMode | ListenerOptions,
25
+ maybeOptions?: ListenerOptions,
26
+ ) => () => void;
27
+ onAny: (obj: object, listener: ChangeListener, options?: ListenerOptions) => () => void;
28
+ pause: (obj: object) => void;
29
+ resume: (obj: object) => void;
30
+ flush: (obj: object) => void;
31
+ getHistory: (obj: object) => ChangeRecord[];
32
+ clearHistory: (obj: object) => void;
33
+ reset: (obj: object) => void;
34
+ markPristine: (obj: object) => void;
35
+ diff: (obj: object) => DiffRecord[];
36
+ isPristine: (obj: object) => boolean;
37
+ mark: (obj: object) => number;
38
+ undo: (obj: object, steps?: number) => void;
39
+ undoSince: (obj: object, historyLengthBefore: number) => void;
40
+ undoGroups: (obj: object, groups?: number) => void;
41
+ canUndo: (obj: object) => boolean;
42
+ canRedo: (obj: object) => boolean;
43
+ clearRedo: (obj: object) => void;
44
+ redo: (obj: object, steps?: number) => void;
45
+ redoGroups: (obj: object, groups?: number) => void;
46
+ }
47
+
48
+ export const createApiMethods = (deps: ApiDeps): ChronicleApiMethods => {
49
+ // listen/onAny --------------------------------------------------------------
50
+ const listen = <T extends object>(
51
+ object: T,
52
+ selector: PathSelector<T>,
53
+ listener: ChangeListener,
54
+ modeOrOptions?: PathMode | ListenerOptions,
55
+ maybeOptions?: ListenerOptions,
56
+ ) => {
57
+ const segs = nameofSegments(selector);
58
+ const root = deps.getRoot(object as object);
59
+ const bucket = ensureListenerBucket(root);
60
+
61
+ let mode: PathMode = 'down';
62
+ let options: ListenerOptions | undefined;
63
+ if (typeof modeOrOptions === 'string') {
64
+ mode = modeOrOptions;
65
+ options = maybeOptions;
66
+ }
67
+ else {
68
+ options = modeOrOptions;
69
+ }
70
+
71
+ let unsubscribe: (() => void) | undefined;
72
+ const { effective: effectiveListener, setUnsubscribe } = buildEffectiveListener(listener, options);
73
+ setUnsubscribe(() => {
74
+ if (unsubscribe)
75
+ unsubscribe();
76
+ });
77
+
78
+ if (segs.length === 0) {
79
+ bucket.global.add(effectiveListener);
80
+
81
+ unsubscribe = () => {
82
+ bucket.global.delete(effectiveListener);
83
+ cleanupListenerBucket(root, bucket);
84
+ };
85
+
86
+ return unsubscribe;
87
+ }
88
+
89
+ addListenerToTrie(bucket.trie, segs, mode, effectiveListener);
90
+
91
+ unsubscribe = () => {
92
+ removeListenerFromTrie(bucket.trie, segs, mode, effectiveListener);
93
+ cleanupListenerBucket(root, bucket);
94
+ };
95
+
96
+ return unsubscribe;
97
+ };
98
+
99
+ const onAny = (obj: object, listener: ChangeListener, options?: ListenerOptions): () => void => {
100
+ return listen(obj as any, s => s as any, listener, options);
101
+ };
102
+
103
+ // pause/resume/flush --------------------------------------------------------
104
+ const pause = (obj: object): void => {
105
+ const root = deps.getRoot(obj);
106
+ schedulePause(root);
107
+ };
108
+
109
+ const resume = (obj: object): void => {
110
+ const root = deps.getRoot(obj);
111
+ scheduleResume(root);
112
+ };
113
+
114
+ const flush = (obj: object): void => {
115
+ const root = deps.getRoot(obj);
116
+ scheduleFlush(root);
117
+ };
118
+
119
+ // history ------------------------------------------------------------------
120
+ const getHistory = (obj: object): ChangeRecord[] => {
121
+ const root = deps.getRoot(obj);
122
+
123
+ return (historyGet(root) ?? []).slice();
124
+ };
125
+
126
+ const clearHistory = (obj: object): void => {
127
+ const root = deps.getRoot(obj);
128
+ historyDelete(root);
129
+ clearLastUngrouped(root);
130
+ clearRedoCache(root);
131
+ };
132
+
133
+ // reset/markPristine/diff/pristine ----------------------------------------
134
+ const markPristine = (obj: object): void => {
135
+ const root = deps.getRoot(obj);
136
+ originalSnapshotCache.set(root, cloneWithOptions(root, root));
137
+ historyDelete(root);
138
+ clearLastUngrouped(root);
139
+ clearRedoCache(root);
140
+ pfClearProxyCache(root);
141
+ };
142
+
143
+ const reset = (obj: object): void => {
144
+ const root = deps.getRoot(obj);
145
+ const snapshot = originalSnapshotCache.get(root);
146
+ if (!snapshot) {
147
+ markPristine(root);
148
+
149
+ return;
150
+ }
151
+
152
+ const overwriteDeep = (target: any, source: any) => {
153
+ if (Array.isArray(target) && Array.isArray(source)) {
154
+ target.length = source.length;
155
+ for (let i = 0; i < source.length; i++)
156
+ target[i] = cloneWithOptions(root, source[i]);
157
+
158
+ return;
159
+ }
160
+
161
+ const isPlainObject = (v: any) => Object.prototype.toString.call(v) === '[object Object]';
162
+ if (isObject(target) && isObject(source) && isPlainObject(target) && isPlainObject(source)) {
163
+ for (const k of Reflect.ownKeys(target)) {
164
+ if (!Object.prototype.hasOwnProperty.call(source, k))
165
+ // eslint-disable-next-line @typescript-eslint/no-dynamic-delete
166
+ delete (target as any)[k as any];
167
+ }
168
+ for (const k of Reflect.ownKeys(source)) {
169
+ const sv = (source as any)[k as any];
170
+ const tv = (target as any)[k as any];
171
+ const bothArrays = Array.isArray(sv) && Array.isArray(tv);
172
+ const bothObjects = isObject(sv) && isObject(tv) && isPlainObject(sv) && isPlainObject(tv);
173
+ if (bothArrays || bothObjects)
174
+ overwriteDeep(tv, sv);
175
+ else
176
+ (target as any)[k as any] = cloneWithOptions(root, sv);
177
+ }
178
+
179
+ return;
180
+ }
181
+
182
+ for (const k of Reflect.ownKeys(target))
183
+ // eslint-disable-next-line @typescript-eslint/no-dynamic-delete
184
+ delete (target as any)[k];
185
+ for (const k of Reflect.ownKeys(source))
186
+ (target as any)[k] = cloneWithOptions(root, (source as any)[k]);
187
+ };
188
+
189
+ suspendWrites(root);
190
+ try {
191
+ overwriteDeep(root as any, snapshot);
192
+ }
193
+ finally {
194
+ resumeWrites(root);
195
+ }
196
+
197
+ markPristine(root);
198
+ clearRedoCache(root);
199
+ };
200
+
201
+ const diff = (obj: object): DiffRecord[] => {
202
+ const root = deps.getRoot(obj);
203
+ const original = originalSnapshotCache.get(root) ?? cloneWithOptions(root, root as any);
204
+ const out: DiffRecord[] = [];
205
+ diffValues(original, root, [], out, root);
206
+
207
+ return out;
208
+ };
209
+
210
+ const isPristine = (obj: object): boolean => {
211
+ const diffs = diff(obj);
212
+
213
+ return diffs.length === 0;
214
+ };
215
+
216
+ // marks/undo/redo -----------------------------------------------------------
217
+ const mark = (obj: object): number => {
218
+ const root = deps.getRoot(obj);
219
+ const history = historyGet(root);
220
+
221
+ return history ? history.length : 0;
222
+ };
223
+
224
+ const undo = (obj: object, steps: number = Number.POSITIVE_INFINITY): void => {
225
+ const root = deps.getRoot(obj);
226
+ coreUndo(root, steps);
227
+ };
228
+
229
+ const undoSince = (obj: object, historyLengthBefore: number): void => {
230
+ const root = deps.getRoot(obj);
231
+ coreUndoSince(root, historyLengthBefore);
232
+ clearLastUngrouped(root);
233
+ };
234
+
235
+ const undoGroups = (obj: object, groups: number = 1): void => {
236
+ const root = deps.getRoot(obj);
237
+ coreUndoGroups(root, groups);
238
+ clearLastUngrouped(root);
239
+ };
240
+
241
+ const canUndo = (obj: object): boolean => {
242
+ const root = deps.getRoot(obj);
243
+
244
+ return coreCanUndo(root);
245
+ };
246
+
247
+ const canRedo = (obj: object): boolean => {
248
+ const root = deps.getRoot(obj);
249
+
250
+ return coreCanRedo(root);
251
+ };
252
+
253
+ const clearRedo = (obj: object): void => {
254
+ const root = deps.getRoot(obj);
255
+ clearRedoCache(root);
256
+ };
257
+
258
+ const redo = (obj: object, steps: number = Number.POSITIVE_INFINITY): void => {
259
+ const root = deps.getRoot(obj);
260
+ coreRedo(root, steps);
261
+ clearLastUngrouped(root);
262
+ };
263
+
264
+ const redoGroups = (obj: object, groups: number = 1): void => {
265
+ const root = deps.getRoot(obj);
266
+ coreRedoGroups(root, groups);
267
+ clearLastUngrouped(root);
268
+ };
269
+
270
+ return {
271
+ listen: listen,
272
+ onAny: onAny,
273
+ pause: pause,
274
+ resume: resume,
275
+ flush: flush,
276
+ getHistory: getHistory,
277
+ clearHistory: clearHistory,
278
+ reset: reset,
279
+ markPristine: markPristine,
280
+ diff: diff,
281
+ isPristine: isPristine,
282
+ mark: mark,
283
+ undo: undo,
284
+ undoSince: undoSince,
285
+ undoGroups: undoGroups,
286
+ canUndo: canUndo,
287
+ canRedo: canRedo,
288
+ clearRedo: clearRedo,
289
+ redo: redo,
290
+ redoGroups: redoGroups,
291
+ };
292
+ };
package/src/api.ts ADDED
@@ -0,0 +1,53 @@
1
+ import type { ProxyFactory } from './proxy-factory.ts';
2
+ import { createProxyFactory } from './proxy-factory.ts';
3
+ import { cloneWithOptions, originalSnapshotCache } from './snapshot-diff.ts';
4
+
5
+
6
+ export interface ChronicleCoreDeps {
7
+ getBatchFrames: (root: object) => { marker: number; id: string; }[] | undefined;
8
+ }
9
+
10
+ export interface ChronicleCore {
11
+ chronicle: <T extends object>(object: T) => T;
12
+ getRoot: (obj: object) => object;
13
+ }
14
+
15
+
16
+ export const createChronicleCore = (deps: ChronicleCoreDeps): ChronicleCore => {
17
+ const proxyToRoot: WeakMap<object, object> = new WeakMap();
18
+ let proxyFactory: ProxyFactory | undefined;
19
+
20
+ const chronicle = (object => {
21
+ const existingRoot = proxyToRoot.get(object);
22
+ if (!proxyFactory) {
23
+ proxyFactory = createProxyFactory({
24
+ getBatchFrames: (r) => deps.getBatchFrames(r),
25
+ setProxyRoot: (proxy, r) => proxyToRoot.set(proxy, r),
26
+ });
27
+ }
28
+
29
+ // If called on an already observed proxy, return it to avoid double-proxying
30
+ if (existingRoot) {
31
+ if (!originalSnapshotCache.has(existingRoot)) {
32
+ originalSnapshotCache.set(
33
+ existingRoot,
34
+ cloneWithOptions(existingRoot, existingRoot),
35
+ );
36
+ }
37
+
38
+ return object;
39
+ }
40
+
41
+ const root = (object as object);
42
+ const { createProxy } = proxyFactory!;
43
+
44
+ if (!originalSnapshotCache.has(root))
45
+ originalSnapshotCache.set(root, cloneWithOptions(root, root));
46
+
47
+ return createProxy(root as object, [], root);
48
+ }) as (<T extends object>(object: T) => T);
49
+
50
+ const getRoot = (obj: object): object => proxyToRoot.get(obj) ?? obj;
51
+
52
+ return { chronicle: chronicle, getRoot };
53
+ };
@@ -0,0 +1,64 @@
1
+ import { isArrayIndexKey } from './path.ts';
2
+ import { isSuspended, resumeWrites, suspendWrites } from './undo-redo.ts';
3
+
4
+
5
+ /**
6
+ * Capture array elements that will be removed when shrinking array length.
7
+ *
8
+ * @param targetArray - The array being modified
9
+ * @param oldLen - The current length
10
+ * @param newLen - The new (smaller) length
11
+ * @returns Array of {index, value} pairs for elements that will be removed
12
+ */
13
+ export const captureShrinkRemovals = (
14
+ targetArray: any[],
15
+ oldLen: number,
16
+ newLen: number,
17
+ ): { index: number; value: any; }[] => {
18
+ const removed: { index: number; value: any; }[] = [];
19
+ for (let i = oldLen - 1; i >= newLen; i--)
20
+ removed.push({ index: i, value: targetArray[i] });
21
+
22
+ return removed;
23
+ };
24
+
25
+
26
+ /**
27
+ * Delete an array element by index using splice to avoid sparse arrays.
28
+ * Suspends write notifications during the splice to avoid noisy intermediate records.
29
+ *
30
+ * @param root - The root object (for suspend/resume context)
31
+ * @param arrayTarget - The array to modify
32
+ * @param index - The numeric index to delete
33
+ * @returns true if deletion succeeded
34
+ */
35
+ export const deleteIndex = (root: object, arrayTarget: any[], index: number): boolean => {
36
+ if (isSuspended(root)) {
37
+ // Already suspended, just splice
38
+ arrayTarget.splice(index, 1);
39
+
40
+ return true;
41
+ }
42
+
43
+ // Suspend writes to avoid intermediate shift/length records
44
+ suspendWrites(root);
45
+ try {
46
+ arrayTarget.splice(index, 1);
47
+
48
+ return true;
49
+ }
50
+ finally {
51
+ resumeWrites(root);
52
+ }
53
+ };
54
+
55
+
56
+ /**
57
+ * Check if a delete operation is for an array index.
58
+ *
59
+ * @param target - The target object
60
+ * @param key - The normalized key
61
+ * @returns true if this is an array index deletion
62
+ */
63
+ export const isArrayIndexDeletion = (target: any, key: string): boolean =>
64
+ Array.isArray(target) && isArrayIndexKey(key);
@@ -0,0 +1,183 @@
1
+ import type { ChronicleCore } from './api.ts';
2
+ import { clearLastUngrouped, ensureHistory, historyGet, nextGroupId } from './history.ts';
3
+ import { undoGroups, undoSince } from './undo-redo.ts';
4
+
5
+
6
+ const batchStack: WeakMap<object, BatchFrame[]> = new WeakMap();
7
+
8
+
9
+ export type BatchDeps = Pick<ChronicleCore, 'chronicle' | 'getRoot'>;
10
+
11
+ export interface BatchFrame { marker: number; id: string; }
12
+
13
+ export interface BatchAPI {
14
+ getBatchFrames: (root: object) => BatchFrame[] | undefined;
15
+ beginBatch: (obj: object) => void;
16
+ commitBatch: (obj: object) => void;
17
+ rollbackBatch: (obj: object) => void;
18
+ batch: <T extends object, R>(object: T, action: (observed: T) => R) => R;
19
+ transaction: <T extends object, R>(object: T, action: (observed: T) => R) =>
20
+ { result: R; marker: number; undo: () => void; };
21
+ transactionAsync: <T extends object, R>(object: T, action: (observed: T) => Promise<R>) =>
22
+ Promise<{ result: R; marker: number; undo: () => void; }>;
23
+ }
24
+
25
+
26
+ export const createBatchTransaction = (deps: BatchDeps): BatchAPI => {
27
+ const getBatchFrames: BatchAPI['getBatchFrames'] = (root) => batchStack.get(root);
28
+
29
+ const beginBatch: BatchAPI['beginBatch'] = (obj) => {
30
+ const root = deps.getRoot(obj);
31
+ const history = ensureHistory(root);
32
+ const frames = batchStack.get(root) ?? [];
33
+ const id = nextGroupId(root);
34
+ frames.push({ marker: history.length, id });
35
+ batchStack.set(root, frames);
36
+ clearLastUngrouped(root);
37
+ };
38
+
39
+ const commitBatch: BatchAPI['commitBatch'] = (obj) => {
40
+ const root = deps.getRoot(obj);
41
+ const frames = batchStack.get(root);
42
+ if (!frames || frames.length === 0)
43
+ return;
44
+
45
+ frames.pop();
46
+ if (frames.length === 0)
47
+ batchStack.delete(root);
48
+
49
+ clearLastUngrouped(root);
50
+ };
51
+
52
+ const rollbackBatch: BatchAPI['rollbackBatch'] = (obj) => {
53
+ const root = deps.getRoot(obj);
54
+ const frames = batchStack.get(root);
55
+ if (!frames || frames.length === 0)
56
+ return;
57
+
58
+ const frame = frames.pop()!;
59
+ undoSince(root, frame.marker);
60
+ if (frames.length === 0)
61
+ batchStack.delete(root);
62
+
63
+ clearLastUngrouped(root);
64
+ };
65
+
66
+ const batch: BatchAPI['batch'] = (object, action) => {
67
+ const root = deps.getRoot(object as unknown as object);
68
+ beginBatch(root);
69
+ const observed = deps.chronicle(object);
70
+ try {
71
+ const result = action(observed);
72
+ commitBatch(root);
73
+
74
+ return result;
75
+ }
76
+ catch (err) {
77
+ rollbackBatch(root);
78
+ throw err;
79
+ }
80
+ };
81
+
82
+ const transaction: BatchAPI['transaction'] = (object, action) => {
83
+ const root = deps.getRoot(object as unknown as object);
84
+ const marker = (historyGet(root) ?? []).length;
85
+
86
+ const framesBefore = (batchStack.get(root) ?? []).length;
87
+ const isTopLevel = framesBefore === 0;
88
+ if (isTopLevel)
89
+ beginBatch(root);
90
+
91
+ const observed = deps.chronicle(object);
92
+ let groupId: string | undefined;
93
+ try {
94
+ const result = action(observed);
95
+ const frames = (batchStack.get(root) ?? []);
96
+ groupId = frames.length > 0 ? frames[frames.length - 1]!.id : undefined;
97
+ if (isTopLevel)
98
+ commitBatch(root);
99
+
100
+ return {
101
+ result,
102
+ marker,
103
+ undo: () => {
104
+ const h = historyGet(root);
105
+ if (groupId && h && h.length > 0) {
106
+ const topGroup = h[h.length - 1]!.groupId ?? `__g#${ h.length - 1 }`;
107
+ if (topGroup === groupId) {
108
+ undoGroups(root, 1);
109
+
110
+ return;
111
+ }
112
+ }
113
+
114
+ undoSince(root, marker);
115
+ },
116
+ };
117
+ }
118
+ catch (err) {
119
+ if (isTopLevel)
120
+ rollbackBatch(root);
121
+ else
122
+ undoSince(root, marker);
123
+
124
+ throw err;
125
+ }
126
+ };
127
+
128
+ const transactionAsync: BatchAPI['transactionAsync'] = async (object, action) => {
129
+ const root = deps.getRoot(object as unknown as object);
130
+ const marker = (historyGet(root) ?? []).length;
131
+
132
+ const framesBefore = (batchStack.get(root) ?? []).length;
133
+ const isTopLevel = framesBefore === 0;
134
+ if (isTopLevel)
135
+ beginBatch(root);
136
+
137
+ const observed = deps.chronicle(object);
138
+ let groupId: string | undefined;
139
+ try {
140
+ const result = await action(observed);
141
+ const frames = (batchStack.get(root) ?? []);
142
+ groupId = frames.length > 0 ? frames[frames.length - 1]!.id : undefined;
143
+ if (isTopLevel)
144
+ commitBatch(root);
145
+
146
+ return {
147
+ result,
148
+ marker,
149
+ undo: () => {
150
+ const h = historyGet(root);
151
+ if (groupId && h && h.length > 0) {
152
+ const topGroup = h[h.length - 1]!.groupId ?? `__g#${ h.length - 1 }`;
153
+ if (topGroup === groupId) {
154
+ undoGroups(root, 1);
155
+
156
+ return;
157
+ }
158
+ }
159
+
160
+ undoSince(root, marker);
161
+ },
162
+ };
163
+ }
164
+ catch (err) {
165
+ if (isTopLevel)
166
+ rollbackBatch(root);
167
+ else
168
+ undoSince(root, marker);
169
+
170
+ throw err;
171
+ }
172
+ };
173
+
174
+ return {
175
+ getBatchFrames,
176
+ beginBatch,
177
+ commitBatch,
178
+ rollbackBatch,
179
+ batch,
180
+ transaction,
181
+ transactionAsync,
182
+ };
183
+ };
@@ -0,0 +1,100 @@
1
+ import { createChronicleCore } from './api.ts';
2
+ import { createApiMethods } from './api-methods.ts';
3
+ import { createBatchTransaction } from './batch-transaction.ts';
4
+ import { type ConfigureOptions, configureRoot } from './config.ts';
5
+ import type { ChangeListener, ChangeRecord, DiffRecord, ListenerOptions, PathMode, PathSelector } from './types.ts';
6
+
7
+
8
+ interface Chronicle {
9
+ <T extends object>(object: T): T;
10
+
11
+ listen<T extends object>(
12
+ object: T,
13
+ selector: PathSelector<T>,
14
+ listener: ChangeListener,
15
+ modeOrOptions?: PathMode | ListenerOptions,
16
+ maybeOptions?: ListenerOptions
17
+ ): () => void;
18
+ onAny(obj: object, listener: ChangeListener, options?: ListenerOptions): () => void;
19
+ pause(obj: object): void;
20
+ resume(obj: object): void;
21
+ flush(obj: object): void;
22
+ getHistory(obj: object): ChangeRecord[];
23
+ clearHistory(obj: object): void;
24
+ reset(obj: object): void;
25
+ markPristine(obj: object): void;
26
+ undo(obj: object, steps?: number): void;
27
+ undoSince(obj: object, historyLengthBefore: number): void;
28
+ diff(obj: object): DiffRecord[];
29
+ isPristine(obj: object): boolean;
30
+ mark(obj: object): number;
31
+ transaction<T extends object, R>(object: T, action: (observed: T) => R): TransactionResult<R>;
32
+ transactionAsync<T extends object, R>(object: T, action: (observed: T) => Promise<R>): Promise<TransactionResult<R>>;
33
+ beginBatch(obj: object): void;
34
+ commitBatch(obj: object): void;
35
+ rollbackBatch(obj: object): void;
36
+ batch<T extends object, R>(object: T, action: (observed: T) => R): R;
37
+ undoGroups(obj: object, groups?: number): void;
38
+ canUndo(obj: object): boolean;
39
+ canRedo(obj: object): boolean;
40
+ clearRedo(obj: object): void;
41
+ redo(obj: object, steps?: number): void;
42
+ redoGroups(obj: object, groups?: number): void;
43
+ configure(
44
+ obj: object,
45
+ options: ConfigureOptions,
46
+ ): void;
47
+ }
48
+
49
+ interface TransactionResult<R> {
50
+ result: R;
51
+ marker: number;
52
+ undo: () => void;
53
+ }
54
+
55
+ const core = createChronicleCore({ getBatchFrames: r => batchApi.getBatchFrames(r) });
56
+ const chronicle = core.chronicle as Chronicle;
57
+
58
+ const batchApi = createBatchTransaction(core);
59
+ const api = createApiMethods({ getRoot: core.getRoot });
60
+
61
+ chronicle.listen = api.listen;
62
+ chronicle.onAny = api.onAny;
63
+ chronicle.pause = api.pause;
64
+ chronicle.resume = api.resume;
65
+ chronicle.flush = api.flush;
66
+ chronicle.getHistory = api.getHistory;
67
+ chronicle.clearHistory = api.clearHistory;
68
+ chronicle.reset = api.reset;
69
+ chronicle.markPristine = api.markPristine;
70
+ chronicle.undo = api.undo;
71
+ chronicle.undoSince = api.undoSince;
72
+ chronicle.diff = api.diff;
73
+ chronicle.isPristine = api.isPristine;
74
+ chronicle.mark = api.mark;
75
+ chronicle.undoGroups = api.undoGroups;
76
+ chronicle.canUndo = api.canUndo;
77
+ chronicle.canRedo = api.canRedo;
78
+ chronicle.clearRedo = api.clearRedo;
79
+ chronicle.redo = api.redo;
80
+ chronicle.redoGroups = api.redoGroups;
81
+
82
+
83
+ chronicle.transaction = <T extends object, R>(object: T, action: (observed: T) => R) =>
84
+ batchApi.transaction(object, action);
85
+ chronicle.transactionAsync = async <T extends object, R>(object: T, action: (observed: T) => Promise<R>) =>
86
+ batchApi.transactionAsync(object, action);
87
+ chronicle.beginBatch = (obj) => batchApi.beginBatch(core.getRoot(obj));
88
+ chronicle.commitBatch = (obj) => batchApi.commitBatch(core.getRoot(obj));
89
+ chronicle.rollbackBatch = (obj) => batchApi.rollbackBatch(core.getRoot(obj));
90
+ chronicle.batch = <T extends object, R>(object: T, action: (observed: T) => R) =>
91
+ batchApi.batch(object, action);
92
+
93
+
94
+ chronicle.configure = (obj: object, options: ConfigureOptions) => {
95
+ const root = core.getRoot(obj);
96
+ configureRoot(root, options);
97
+ };
98
+
99
+ export { chronicle };
100
+ export type { Chronicle };