@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,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
+ };