@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.
- package/README.md +563 -0
- package/dist/api-methods.d.ts +28 -0
- package/dist/api-methods.d.ts.map +1 -0
- package/dist/api-methods.js +206 -0
- package/dist/api-methods.js.map +1 -0
- package/dist/api.d.ts +12 -0
- package/dist/api.d.ts.map +1 -0
- package/dist/api.js +30 -0
- package/dist/api.js.map +1 -0
- package/dist/array-mutations.d.ts +31 -0
- package/dist/array-mutations.d.ts.map +1 -0
- package/dist/array-mutations.js +50 -0
- package/dist/array-mutations.js.map +1 -0
- package/dist/batch-transaction.d.ts +25 -0
- package/dist/batch-transaction.d.ts.map +1 -0
- package/dist/batch-transaction.js +138 -0
- package/dist/batch-transaction.js.map +1 -0
- package/dist/chronicle.d.ts +41 -0
- package/dist/chronicle.d.ts.map +1 -0
- package/dist/chronicle.js +40 -0
- package/dist/chronicle.js.map +1 -0
- package/dist/collection-adapters.d.ts +29 -0
- package/dist/collection-adapters.d.ts.map +1 -0
- package/dist/collection-adapters.js +184 -0
- package/dist/collection-adapters.js.map +1 -0
- package/dist/config.d.ts +7 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +11 -0
- package/dist/config.js.map +1 -0
- package/dist/grouping.d.ts +17 -0
- package/dist/grouping.d.ts.map +1 -0
- package/dist/grouping.js +35 -0
- package/dist/grouping.js.map +1 -0
- package/dist/history-recorder.d.ts +39 -0
- package/dist/history-recorder.d.ts.map +1 -0
- package/dist/history-recorder.js +112 -0
- package/dist/history-recorder.js.map +1 -0
- package/dist/history.d.ts +29 -0
- package/dist/history.d.ts.map +1 -0
- package/dist/history.js +47 -0
- package/dist/history.js.map +1 -0
- package/dist/listener-affinity.d.ts +16 -0
- package/dist/listener-affinity.d.ts.map +1 -0
- package/dist/listener-affinity.js +58 -0
- package/dist/listener-affinity.js.map +1 -0
- package/dist/listener-trie.d.ts +10 -0
- package/dist/listener-trie.d.ts.map +1 -0
- package/dist/listener-trie.js +83 -0
- package/dist/listener-trie.js.map +1 -0
- package/dist/nameof.d.ts +12 -0
- package/dist/nameof.d.ts.map +1 -0
- package/dist/nameof.js +30 -0
- package/dist/nameof.js.map +1 -0
- package/dist/path-key.d.ts +11 -0
- package/dist/path-key.d.ts.map +1 -0
- package/dist/path-key.js +11 -0
- package/dist/path-key.js.map +1 -0
- package/dist/path.d.ts +7 -0
- package/dist/path.d.ts.map +1 -0
- package/dist/path.js +53 -0
- package/dist/path.js.map +1 -0
- package/dist/proxy-cache.d.ts +32 -0
- package/dist/proxy-cache.d.ts.map +1 -0
- package/dist/proxy-cache.js +72 -0
- package/dist/proxy-cache.js.map +1 -0
- package/dist/proxy-factory.d.ts +17 -0
- package/dist/proxy-factory.d.ts.map +1 -0
- package/dist/proxy-factory.js +124 -0
- package/dist/proxy-factory.js.map +1 -0
- package/dist/schedule-queue.d.ts +10 -0
- package/dist/schedule-queue.d.ts.map +1 -0
- package/dist/schedule-queue.js +112 -0
- package/dist/schedule-queue.js.map +1 -0
- package/dist/snapshot-diff.d.ts +6 -0
- package/dist/snapshot-diff.d.ts.map +1 -0
- package/dist/snapshot-diff.js +67 -0
- package/dist/snapshot-diff.js.map +1 -0
- package/dist/symbol-id.d.ts +3 -0
- package/dist/symbol-id.d.ts.map +1 -0
- package/dist/symbol-id.js +16 -0
- package/dist/symbol-id.js.map +1 -0
- package/dist/types.d.ts +48 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +3 -0
- package/dist/types.js.map +1 -0
- package/dist/undo-redo.d.ts +12 -0
- package/dist/undo-redo.d.ts.map +1 -0
- package/dist/undo-redo.js +216 -0
- package/dist/undo-redo.js.map +1 -0
- package/package.json +45 -0
- package/src/api-methods.ts +292 -0
- package/src/api.ts +53 -0
- package/src/array-mutations.ts +64 -0
- package/src/batch-transaction.ts +183 -0
- package/src/chronicle.ts +100 -0
- package/src/collection-adapters.ts +224 -0
- package/src/config.ts +16 -0
- package/src/grouping.ts +47 -0
- package/src/history-recorder.ts +145 -0
- package/src/history.ts +75 -0
- package/src/listener-affinity.ts +69 -0
- package/src/listener-trie.ts +103 -0
- package/src/nameof.ts +42 -0
- package/src/path-key.ts +10 -0
- package/src/path.ts +69 -0
- package/src/proxy-cache.ts +86 -0
- package/src/proxy-factory.ts +168 -0
- package/src/schedule-queue.ts +144 -0
- package/src/snapshot-diff.ts +90 -0
- package/src/symbol-id.ts +20 -0
- package/src/tsconfig.json +3 -0
- package/src/types.ts +59 -0
- package/src/undo-redo.ts +249 -0
package/src/undo-redo.ts
ADDED
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
import { ensureHistory, historyGet, nextGroupId } from './history.ts';
|
|
2
|
+
import { deleteAtPath, ensureParents, getParentAndKey, isArrayIndexKey, setAtPath } from './path.ts';
|
|
3
|
+
import type { ChangeRecord } from './types.ts';
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
// Redo cache per root
|
|
7
|
+
const redoCache: WeakMap<object, ChangeRecord[]> = new WeakMap();
|
|
8
|
+
|
|
9
|
+
// Write suspension counter per root (used to avoid recording/dispatch during programmatic changes)
|
|
10
|
+
const suspendWriteCounter: WeakMap<object, number> = new WeakMap();
|
|
11
|
+
|
|
12
|
+
export const isSuspended = (root: object): boolean => (suspendWriteCounter.get(root) ?? 0) > 0;
|
|
13
|
+
export const suspendWrites = (root: object): void => { suspendWriteCounter.set(root, (suspendWriteCounter.get(root) ?? 0) + 1); };
|
|
14
|
+
export const resumeWrites = (root: object): void => {
|
|
15
|
+
const n = (suspendWriteCounter.get(root) ?? 0) - 1;
|
|
16
|
+
if (n <= 0)
|
|
17
|
+
suspendWriteCounter.delete(root);
|
|
18
|
+
else
|
|
19
|
+
suspendWriteCounter.set(root, n);
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
const getRedo = (root: object): ChangeRecord[] => {
|
|
23
|
+
let r = redoCache.get(root);
|
|
24
|
+
if (!r) {
|
|
25
|
+
r = [];
|
|
26
|
+
redoCache.set(root, r);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return r;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
export const clearRedoCache = (root: object): void => { redoCache.delete(root); };
|
|
33
|
+
|
|
34
|
+
export const canUndo = (root: object): boolean => (historyGet(root) ?? []).length > 0;
|
|
35
|
+
export const canRedo = (root: object): boolean => (redoCache.get(root) ?? []).length > 0;
|
|
36
|
+
|
|
37
|
+
// Helper to get object at path
|
|
38
|
+
const getAtPath = (rootNode: any, p: string[]) => {
|
|
39
|
+
let node = rootNode;
|
|
40
|
+
for (const seg of p) {
|
|
41
|
+
if (node == null)
|
|
42
|
+
return undefined;
|
|
43
|
+
|
|
44
|
+
node = node[seg as any];
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return node;
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
// Apply a change record forward (redo side) without emitting notifications
|
|
51
|
+
const applyForward = (root: any, rec: ChangeRecord) => {
|
|
52
|
+
if (rec.collection === 'map') {
|
|
53
|
+
const m: Map<any, any> | undefined = getAtPath(root, rec.path);
|
|
54
|
+
if (m && m instanceof Map) {
|
|
55
|
+
if (rec.type === 'set')
|
|
56
|
+
m.set(rec.key, rec.newValue);
|
|
57
|
+
else if (rec.type === 'delete')
|
|
58
|
+
m.delete(rec.key);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (rec.collection === 'set') {
|
|
65
|
+
const s: Set<any> | undefined = getAtPath(root, rec.path);
|
|
66
|
+
if (s && s instanceof Set) {
|
|
67
|
+
if (rec.type === 'set')
|
|
68
|
+
s.add(rec.key);
|
|
69
|
+
else if (rec.type === 'delete')
|
|
70
|
+
s.delete(rec.key);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (rec.type === 'set') {
|
|
77
|
+
setAtPath(root, rec.path, rec.newValue);
|
|
78
|
+
}
|
|
79
|
+
else if (rec.type === 'delete') {
|
|
80
|
+
const parentAndKey = getParentAndKey(root, rec.path);
|
|
81
|
+
if (parentAndKey) {
|
|
82
|
+
const [ parent, key ] = parentAndKey;
|
|
83
|
+
if (Array.isArray(parent) && isArrayIndexKey(String(key)))
|
|
84
|
+
(parent as any).splice(Number(key), 1);
|
|
85
|
+
else
|
|
86
|
+
Reflect.deleteProperty(parent, key as any);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
export const undo = (root: object, steps: number = Number.POSITIVE_INFINITY): void => {
|
|
92
|
+
const history = historyGet(root);
|
|
93
|
+
if (!history || history.length === 0)
|
|
94
|
+
return;
|
|
95
|
+
|
|
96
|
+
suspendWrites(root);
|
|
97
|
+
try {
|
|
98
|
+
let remaining = steps;
|
|
99
|
+
const undone: ChangeRecord[] = [];
|
|
100
|
+
while (history.length > 0 && remaining > 0) {
|
|
101
|
+
const rec = history.pop()!;
|
|
102
|
+
|
|
103
|
+
if (rec.collection === 'map') {
|
|
104
|
+
const m: Map<any, any> | undefined = getAtPath(root as any, rec.path);
|
|
105
|
+
if (m && m instanceof Map) {
|
|
106
|
+
if (rec.type === 'set') {
|
|
107
|
+
if (rec.existedBefore === false)
|
|
108
|
+
m.delete(rec.key);
|
|
109
|
+
else
|
|
110
|
+
m.set(rec.key, rec.oldValue);
|
|
111
|
+
}
|
|
112
|
+
else if (rec.type === 'delete') {
|
|
113
|
+
m.set(rec.key, rec.oldValue);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
else if (rec.collection === 'set') {
|
|
118
|
+
const s: Set<any> | undefined = getAtPath(root as any, rec.path);
|
|
119
|
+
if (s && s instanceof Set) {
|
|
120
|
+
if (rec.type === 'set')
|
|
121
|
+
s.delete(rec.key);
|
|
122
|
+
else if (rec.type === 'delete')
|
|
123
|
+
s.add(rec.key);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
else {
|
|
127
|
+
// Ensure parent containers exist for plain object/array paths
|
|
128
|
+
ensureParents(root as any, rec.path);
|
|
129
|
+
if (rec.type === 'set') {
|
|
130
|
+
if (rec.existedBefore === false)
|
|
131
|
+
deleteAtPath(root as any, rec.path);
|
|
132
|
+
else
|
|
133
|
+
setAtPath(root as any, rec.path, rec.oldValue);
|
|
134
|
+
}
|
|
135
|
+
else if (rec.type === 'delete') {
|
|
136
|
+
// If the path points into an array at a numeric index, use splice to re-insert
|
|
137
|
+
const parentAndKey = getParentAndKey(root as any, rec.path);
|
|
138
|
+
if (parentAndKey) {
|
|
139
|
+
const [ parent, key ] = parentAndKey;
|
|
140
|
+
if (Array.isArray(parent) && isArrayIndexKey(String(key)))
|
|
141
|
+
(parent as any).splice(Number(key), 0, rec.oldValue);
|
|
142
|
+
else
|
|
143
|
+
setAtPath(root as any, rec.path, rec.oldValue);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
remaining--;
|
|
149
|
+
undone.push(rec);
|
|
150
|
+
}
|
|
151
|
+
if (undone.length > 0)
|
|
152
|
+
getRedo(root).push(...undone);
|
|
153
|
+
}
|
|
154
|
+
finally {
|
|
155
|
+
resumeWrites(root);
|
|
156
|
+
}
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
export const undoSince = (root: object, historyLengthBefore: number): void => {
|
|
160
|
+
const history = historyGet(root);
|
|
161
|
+
if (!history)
|
|
162
|
+
return;
|
|
163
|
+
|
|
164
|
+
const steps = Math.max(0, history.length - Math.max(0, historyLengthBefore | 0));
|
|
165
|
+
if (steps > 0)
|
|
166
|
+
undo(root, steps);
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
export const undoGroups = (root: object, groups: number = 1): void => {
|
|
170
|
+
const history = historyGet(root);
|
|
171
|
+
if (!history || history.length === 0)
|
|
172
|
+
return;
|
|
173
|
+
|
|
174
|
+
const toUndo = Math.max(0, groups | 0);
|
|
175
|
+
if (toUndo === 0)
|
|
176
|
+
return;
|
|
177
|
+
|
|
178
|
+
let steps = 0;
|
|
179
|
+
const seen: Set<string> = new Set();
|
|
180
|
+
for (let i = history.length - 1; i >= 0; i--) {
|
|
181
|
+
const gid = (history[i]!.groupId ?? `__g#${ i }`);
|
|
182
|
+
if (seen.size === toUndo && !seen.has(gid))
|
|
183
|
+
break;
|
|
184
|
+
|
|
185
|
+
seen.add(gid);
|
|
186
|
+
steps++;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
if (steps > 0)
|
|
190
|
+
undo(root, steps);
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
export const redo = (root: object, steps: number = Number.POSITIVE_INFINITY): void => {
|
|
194
|
+
const redo = redoCache.get(root);
|
|
195
|
+
if (!redo || redo.length === 0)
|
|
196
|
+
return;
|
|
197
|
+
|
|
198
|
+
suspendWrites(root);
|
|
199
|
+
try {
|
|
200
|
+
let remaining = steps;
|
|
201
|
+
const gid = nextGroupId(root);
|
|
202
|
+
while (redo.length > 0 && remaining > 0) {
|
|
203
|
+
const rec = redo.pop()!; // get earliest undone first due to push order
|
|
204
|
+
applyForward(root as any, rec);
|
|
205
|
+
const history = ensureHistory(root);
|
|
206
|
+
const copy: ChangeRecord = { ...rec, groupId: gid, timestamp: Date.now() };
|
|
207
|
+
history.push(copy);
|
|
208
|
+
remaining--;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
finally {
|
|
212
|
+
resumeWrites(root);
|
|
213
|
+
}
|
|
214
|
+
};
|
|
215
|
+
|
|
216
|
+
export const redoGroups = (root: object, groups: number = 1): void => {
|
|
217
|
+
const redo = redoCache.get(root);
|
|
218
|
+
if (!redo || redo.length === 0)
|
|
219
|
+
return;
|
|
220
|
+
|
|
221
|
+
const toRedo = Math.max(0, groups | 0);
|
|
222
|
+
if (toRedo === 0)
|
|
223
|
+
return;
|
|
224
|
+
|
|
225
|
+
suspendWrites(root);
|
|
226
|
+
try {
|
|
227
|
+
let doneGroups = 0;
|
|
228
|
+
while (redo.length > 0 && doneGroups < toRedo) {
|
|
229
|
+
const lastGid = redo[redo.length - 1]!.groupId ?? `__g#${ redo.length - 1 }`;
|
|
230
|
+
const gidNew = nextGroupId(root);
|
|
231
|
+
// pop until group boundary changes
|
|
232
|
+
while (redo.length > 0) {
|
|
233
|
+
const rec = redo[redo.length - 1]!;
|
|
234
|
+
const recG = rec.groupId ?? `__g#${ redo.length - 1 }`;
|
|
235
|
+
if (recG !== lastGid)
|
|
236
|
+
break;
|
|
237
|
+
|
|
238
|
+
redo.pop();
|
|
239
|
+
applyForward(root as any, rec);
|
|
240
|
+
const history = ensureHistory(root);
|
|
241
|
+
history.push({ ...rec, groupId: gidNew, timestamp: Date.now() });
|
|
242
|
+
}
|
|
243
|
+
doneGroups++;
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
finally {
|
|
247
|
+
resumeWrites(root);
|
|
248
|
+
}
|
|
249
|
+
};
|