@procedures/collections 0.13.1 → 0.13.2
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/CHANGELOG.md +9 -0
- package/README.md +29 -0
- package/dist/.tsbuildinfo +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/storage/interface.d.ts +3 -2
- package/dist/storage/interface.d.ts.map +1 -1
- package/dist/storage/interface.js.map +1 -1
- package/dist/testing/storage-contract.d.ts.map +1 -1
- package/dist/testing/storage-contract.js +20 -0
- package/dist/testing/storage-contract.js.map +1 -1
- package/dist/undo/index.d.ts +7 -0
- package/dist/undo/index.d.ts.map +1 -0
- package/dist/undo/index.js +7 -0
- package/dist/undo/index.js.map +1 -0
- package/dist/undo/inverse.d.ts +22 -0
- package/dist/undo/inverse.d.ts.map +1 -0
- package/dist/undo/inverse.js +186 -0
- package/dist/undo/inverse.js.map +1 -0
- package/dist/undo/undo-log.d.ts +120 -0
- package/dist/undo/undo-log.d.ts.map +1 -0
- package/dist/undo/undo-log.js +165 -0
- package/dist/undo/undo-log.js.map +1 -0
- package/package.json +2 -2
- package/src/index.ts +3 -0
- package/src/storage/interface.ts +3 -2
- package/src/testing/storage-contract.ts +29 -0
- package/src/undo/index.ts +6 -0
- package/src/undo/inverse.ts +184 -0
- package/src/undo/undo-log.ts +227 -0
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
import { applyPatch, isPatchOperations, patchProblems } from "@procedures/client/query";
|
|
2
|
+
const INDEX = /^(?:0|[1-9][0-9]*)$/;
|
|
3
|
+
/** True for an object that is not an array and not null. */
|
|
4
|
+
function isObject(value) {
|
|
5
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
6
|
+
}
|
|
7
|
+
/** The keys of an object whose values are not `undefined`. */
|
|
8
|
+
function definedKeys(value) {
|
|
9
|
+
return value === undefined ? [] : Object.keys(value).filter((key) => value[key] !== undefined);
|
|
10
|
+
}
|
|
11
|
+
/** The child of a container at a key, and whether the container has it. */
|
|
12
|
+
function childOf(container, key) {
|
|
13
|
+
if (Array.isArray(container)) {
|
|
14
|
+
return INDEX.test(key) && Number(key) < container.length ? { found: true, value: container[Number(key)] } : { found: false };
|
|
15
|
+
}
|
|
16
|
+
return isObject(container) && Object.hasOwn(container, key) ? { found: true, value: container[key] } : { found: false };
|
|
17
|
+
}
|
|
18
|
+
/** The value at a path, and whether it is there. */
|
|
19
|
+
function valueAt(value, path) {
|
|
20
|
+
let current = { found: true, value };
|
|
21
|
+
for (const key of path) {
|
|
22
|
+
current = childOf(current.value, key);
|
|
23
|
+
if (!current.found)
|
|
24
|
+
return current;
|
|
25
|
+
}
|
|
26
|
+
return current;
|
|
27
|
+
}
|
|
28
|
+
/** This function sets a key as an own property, also the key `__proto__`. */
|
|
29
|
+
function define(target, key, value) {
|
|
30
|
+
Object.defineProperty(target, key, { value, enumerable: true, writable: true, configurable: true });
|
|
31
|
+
}
|
|
32
|
+
/** True when two JSON values are equal. The order of the keys of an object does not count. */
|
|
33
|
+
function equal(a, b) {
|
|
34
|
+
if (a === b)
|
|
35
|
+
return true;
|
|
36
|
+
if (Array.isArray(a))
|
|
37
|
+
return Array.isArray(b) && a.length === b.length && a.every((item, i) => equal(item, b[i]));
|
|
38
|
+
if (!isObject(a) || !isObject(b))
|
|
39
|
+
return false;
|
|
40
|
+
const keys = definedKeys(a);
|
|
41
|
+
return keys.length === definedKeys(b).length && keys.every((key) => b[key] !== undefined && equal(a[key], b[key]));
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* This function gives the patch that undoes a patch: applied to the item after the patch, the
|
|
45
|
+
* inverse gives the item before the patch again. It gives `undefined` when no patch can undo it
|
|
46
|
+
* exactly. Then keep the whole item before the change.
|
|
47
|
+
*
|
|
48
|
+
* - The inverse of `$remove` of an element is an `$insert` of the element, and the inverse of an
|
|
49
|
+
* `$insert` is a `$remove`. The inverse does not keep the whole array.
|
|
50
|
+
* - The inverse of `$set` and `$unset` sets each old value again, or removes the new path.
|
|
51
|
+
* - The indices of the inverse refer to the item after the patch.
|
|
52
|
+
* - The function applies the inverse to check it. It gives `undefined` when the check fails, for
|
|
53
|
+
* example after the removal of two adjacent elements, or when `before` is not an object.
|
|
54
|
+
*
|
|
55
|
+
* @example
|
|
56
|
+
* ```ts
|
|
57
|
+
* inversePatch({ tags: ["a", "b"] }, { $remove: ["tags.0"] }); // { $insert: { "tags.0": "a" } }
|
|
58
|
+
* ```
|
|
59
|
+
*
|
|
60
|
+
* @throws TypeError when `applyPatch` refuses the patch
|
|
61
|
+
*/
|
|
62
|
+
export function inversePatch(before, patch) {
|
|
63
|
+
const after = applyPatch(before, patch);
|
|
64
|
+
if (!isObject(before))
|
|
65
|
+
return undefined;
|
|
66
|
+
const inverse = isPatchOperations(patch) ? inverseOfOperations(before, patch) : inverseOfMerge(before, patch);
|
|
67
|
+
if (inverse === undefined || patchProblems(inverse).length > 0)
|
|
68
|
+
return undefined;
|
|
69
|
+
try {
|
|
70
|
+
return equal(applyPatch(after, inverse), before) ? inverse : undefined;
|
|
71
|
+
}
|
|
72
|
+
catch {
|
|
73
|
+
return undefined;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
/** The inverse of a JSON Merge Patch: the old value of each top field, or its removal. */
|
|
77
|
+
function inverseOfMerge(before, patch) {
|
|
78
|
+
const set = {};
|
|
79
|
+
const remove = [];
|
|
80
|
+
for (const key of definedKeys(patch)) {
|
|
81
|
+
// A field name with a dot is a path for the operations, so the operations cannot name it
|
|
82
|
+
if (key.includes(".") || key.startsWith("$"))
|
|
83
|
+
return undefined;
|
|
84
|
+
if (Object.hasOwn(before, key))
|
|
85
|
+
define(set, key, before[key]);
|
|
86
|
+
else
|
|
87
|
+
remove.push(key);
|
|
88
|
+
}
|
|
89
|
+
return operations(set, remove, {});
|
|
90
|
+
}
|
|
91
|
+
/** The inverse of the operations of a patch. */
|
|
92
|
+
function inverseOfOperations(before, patch) {
|
|
93
|
+
// The moves of each array, by the path of the array in the item before the patch
|
|
94
|
+
const moves = new Map();
|
|
95
|
+
const addMove = (path, kind) => {
|
|
96
|
+
const array = path.slice(0, -1).join(".");
|
|
97
|
+
const entry = moves.get(array) ?? { kind, indices: [] };
|
|
98
|
+
entry.indices.push(Number(path.at(-1)));
|
|
99
|
+
moves.set(array, entry);
|
|
100
|
+
};
|
|
101
|
+
const removals = (patch.$remove ?? []).map((key) => key.split("."));
|
|
102
|
+
for (const path of removals) {
|
|
103
|
+
const parent = valueAt(before, path.slice(0, -1));
|
|
104
|
+
if (parent.found && Array.isArray(parent.value) && childOf(parent.value, path.at(-1)).found)
|
|
105
|
+
addMove(path, "remove");
|
|
106
|
+
}
|
|
107
|
+
const insertions = definedKeys(patch.$insert).map((key) => key.split("."));
|
|
108
|
+
for (const path of insertions)
|
|
109
|
+
addMove(path, "insert");
|
|
110
|
+
// A path of the item before the patch, as a path of the item after it
|
|
111
|
+
const translate = (path) => {
|
|
112
|
+
const out = [...path];
|
|
113
|
+
for (let i = 0; i < path.length; i++) {
|
|
114
|
+
const move = moves.get(path.slice(0, i).join("."));
|
|
115
|
+
if (!move || !INDEX.test(path[i]))
|
|
116
|
+
continue;
|
|
117
|
+
const index = Number(path[i]);
|
|
118
|
+
const shift = move.kind === "remove" ? -move.indices.filter((r) => r < index).length : move.indices.filter((n) => n <= index).length;
|
|
119
|
+
out[i] = String(index + shift);
|
|
120
|
+
}
|
|
121
|
+
return out.join(".");
|
|
122
|
+
};
|
|
123
|
+
const set = {};
|
|
124
|
+
const remove = new Set();
|
|
125
|
+
const insert = {};
|
|
126
|
+
const put = (target, key, value) => {
|
|
127
|
+
if (Object.hasOwn(target, key))
|
|
128
|
+
return false;
|
|
129
|
+
define(target, key, value);
|
|
130
|
+
return true;
|
|
131
|
+
};
|
|
132
|
+
// `$set` and `$unset`: the old value, or the removal of the part that the patch made
|
|
133
|
+
const restore = (path, made) => {
|
|
134
|
+
let current = before;
|
|
135
|
+
for (let n = 0; n < path.length; n++) {
|
|
136
|
+
const child = childOf(current, path[n]);
|
|
137
|
+
if (!child.found) {
|
|
138
|
+
if (made)
|
|
139
|
+
remove.add(translate(path.slice(0, n + 1)));
|
|
140
|
+
return true;
|
|
141
|
+
}
|
|
142
|
+
current = child.value;
|
|
143
|
+
// `$set` makes an object of a null on its path
|
|
144
|
+
if (made && current === null && n < path.length - 1)
|
|
145
|
+
return put(set, translate(path.slice(0, n + 1)), null);
|
|
146
|
+
}
|
|
147
|
+
return put(set, translate(path), current);
|
|
148
|
+
};
|
|
149
|
+
for (const key of definedKeys(patch.$set))
|
|
150
|
+
if (!restore(key.split("."), true))
|
|
151
|
+
return undefined;
|
|
152
|
+
for (const key of patch.$unset ?? [])
|
|
153
|
+
if (!restore(key.split("."), false))
|
|
154
|
+
return undefined;
|
|
155
|
+
// `$remove`: an `$insert` of the element, or the old value of a field of an object
|
|
156
|
+
for (const path of removals) {
|
|
157
|
+
const parent = valueAt(before, path.slice(0, -1));
|
|
158
|
+
if (!parent.found || !Array.isArray(parent.value)) {
|
|
159
|
+
if (!restore(path, false))
|
|
160
|
+
return undefined;
|
|
161
|
+
continue;
|
|
162
|
+
}
|
|
163
|
+
const element = childOf(parent.value, path.at(-1));
|
|
164
|
+
if (!element.found)
|
|
165
|
+
continue;
|
|
166
|
+
const index = Number(path.at(-1));
|
|
167
|
+
const target = `${translate(path.slice(0, -1))}.${index - moves.get(path.slice(0, -1).join(".")).indices.filter((r) => r < index).length}`;
|
|
168
|
+
if (!put(insert, target, element.value))
|
|
169
|
+
return undefined;
|
|
170
|
+
}
|
|
171
|
+
// `$insert`: a `$remove` of the new element
|
|
172
|
+
for (const path of insertions) {
|
|
173
|
+
const index = Number(path.at(-1));
|
|
174
|
+
remove.add(`${translate(path.slice(0, -1))}.${index + moves.get(path.slice(0, -1).join(".")).indices.filter((n) => n < index).length}`);
|
|
175
|
+
}
|
|
176
|
+
return operations(set, [...remove], insert);
|
|
177
|
+
}
|
|
178
|
+
/** The operations, without the empty ones. */
|
|
179
|
+
function operations(set, remove, insert) {
|
|
180
|
+
return {
|
|
181
|
+
...(Object.keys(set).length > 0 ? { $set: set } : {}),
|
|
182
|
+
...(remove.length > 0 ? { $remove: remove } : {}),
|
|
183
|
+
...(Object.keys(insert).length > 0 ? { $insert: insert } : {}),
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
//# sourceMappingURL=inverse.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"inverse.js","sourceRoot":"","sources":["../../src/undo/inverse.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { type StoragePatch, type StoragePatchOperations } from "@procedures/client/query";
|
|
2
|
+
import type { CollectionStorage } from "../storage/interface.js";
|
|
3
|
+
/** One change of one item, as the caller gives it to `UndoLog.record`. */
|
|
4
|
+
export interface UndoChange {
|
|
5
|
+
/** The name of the collection of the item. */
|
|
6
|
+
readonly collection: string;
|
|
7
|
+
/** The id of the item. */
|
|
8
|
+
readonly id: string;
|
|
9
|
+
/** The item before the change. `undefined` or `null`: the item did not exist. */
|
|
10
|
+
readonly before: unknown;
|
|
11
|
+
/** The patch of the change. Without a patch, the change replaced or removed the whole item. */
|
|
12
|
+
readonly patch?: StoragePatch;
|
|
13
|
+
/**
|
|
14
|
+
* The item after the change, as the storage keeps it. `undefined` or `null`: the change removed
|
|
15
|
+
* the item. With a patch, the default is `applyPatch(before, patch)`. Give the result of
|
|
16
|
+
* `patch` of the storage when the storage changes the value, for example when it adds the id.
|
|
17
|
+
*/
|
|
18
|
+
readonly after?: unknown;
|
|
19
|
+
/**
|
|
20
|
+
* The changes with the same group undo together, for example the two items of a move. Give each
|
|
21
|
+
* action a new group id: two actions with one id undo as one.
|
|
22
|
+
*/
|
|
23
|
+
readonly group?: string;
|
|
24
|
+
}
|
|
25
|
+
/** One entry of an undo log. */
|
|
26
|
+
export interface UndoEntry {
|
|
27
|
+
/** The id of the entry in the storage of the log. */
|
|
28
|
+
readonly key: string;
|
|
29
|
+
/** The name of the collection of the item. */
|
|
30
|
+
readonly collection: string;
|
|
31
|
+
/** The id of the item. */
|
|
32
|
+
readonly id: string;
|
|
33
|
+
/** The patch that undoes the change. An entry without it restores `item`. */
|
|
34
|
+
readonly inverse?: StoragePatchOperations;
|
|
35
|
+
/** The whole item before the change, when the entry has no `inverse`. `null`: the item did not exist. */
|
|
36
|
+
readonly item?: unknown;
|
|
37
|
+
/** The fingerprint of the item after the change. The undo refuses an item that changed since. */
|
|
38
|
+
readonly after: string;
|
|
39
|
+
/** The group of the change. */
|
|
40
|
+
readonly group?: string;
|
|
41
|
+
/** The time of the change, in milliseconds. */
|
|
42
|
+
readonly at: number;
|
|
43
|
+
/** The order of the entries of one log with the same time. */
|
|
44
|
+
readonly seq: number;
|
|
45
|
+
}
|
|
46
|
+
/** The options of an `UndoLog`. */
|
|
47
|
+
export interface UndoLogOptions {
|
|
48
|
+
/** The most entries. The log removes the oldest entries past this number. The default is 50. */
|
|
49
|
+
readonly cap?: number;
|
|
50
|
+
/** The clock, in milliseconds. The default is `Date.now`. */
|
|
51
|
+
readonly now?: () => number;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* The error of an undo of an item that changed after the recorded change, for example by another
|
|
55
|
+
* writer. The undo writes nothing, and its entries stay in the log.
|
|
56
|
+
*/
|
|
57
|
+
export declare class UndoConflictError extends Error {
|
|
58
|
+
/** The entry whose item changed. */
|
|
59
|
+
readonly entry: UndoEntry;
|
|
60
|
+
constructor(entry: UndoEntry);
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* The fingerprint of an item: a 64-bit hash of its canonical JSON text, in hexadecimal. A missing
|
|
64
|
+
* item and `null` have the same fingerprint. Two items with the same fields in another order have
|
|
65
|
+
* the same fingerprint.
|
|
66
|
+
*/
|
|
67
|
+
export declare function fingerprintOf(item: unknown): string;
|
|
68
|
+
/**
|
|
69
|
+
* An undo log of the changes of items in collection storages (Graph's split, wave 2, W10). Each
|
|
70
|
+
* entry keeps the patch that undoes one change (`inversePatch`), or the whole item before the
|
|
71
|
+
* change when no patch can undo it, and the fingerprint of the item after the change.
|
|
72
|
+
*
|
|
73
|
+
* `undo()` undoes the newest entry and the entries of its group. It first reads each item and
|
|
74
|
+
* compares it with the fingerprint. When another writer changed an item after the recorded change,
|
|
75
|
+
* the undo writes nothing and gives an `UndoConflictError`. Then each item gets one write through
|
|
76
|
+
* its storage, so an undo reaches the observers of the storage as a change.
|
|
77
|
+
*
|
|
78
|
+
* @example
|
|
79
|
+
* ```ts
|
|
80
|
+
* const log = new UndoLog(new InMemoryStorage<UndoEntry>(), (name) => storages[name]);
|
|
81
|
+
* const before = await notes.get("n1");
|
|
82
|
+
* const after = await notes.patch!("n1", { $remove: ["tags.0"] });
|
|
83
|
+
* await log.record({ collection: "notes", id: "n1", before, patch: { $remove: ["tags.0"] }, after });
|
|
84
|
+
* await log.undo(); // the tag is back
|
|
85
|
+
* ```
|
|
86
|
+
*/
|
|
87
|
+
export declare class UndoLog {
|
|
88
|
+
#private;
|
|
89
|
+
/**
|
|
90
|
+
* @param entries - The storage of the entries of the log
|
|
91
|
+
* @param storageOf - The storage of each collection that the log can undo
|
|
92
|
+
* @param options - The cap and the clock
|
|
93
|
+
*/
|
|
94
|
+
constructor(entries: CollectionStorage<UndoEntry>, storageOf: (collection: string) => CollectionStorage<unknown> | undefined, options?: UndoLogOptions);
|
|
95
|
+
/**
|
|
96
|
+
* This method records one change, and gives its entry. It keeps the inverse patch of the change
|
|
97
|
+
* when one exists, or else the whole item before the change, and the fingerprint of the item
|
|
98
|
+
* after the change. Then it removes the oldest entries past the cap.
|
|
99
|
+
*
|
|
100
|
+
* @throws TypeError when `applyPatch` refuses the patch of the change
|
|
101
|
+
*/
|
|
102
|
+
record(change: UndoChange): Promise<UndoEntry>;
|
|
103
|
+
/** This method gives the entries, the newest first. */
|
|
104
|
+
entries(): Promise<UndoEntry[]>;
|
|
105
|
+
/**
|
|
106
|
+
* This method undoes the newest change, and each change of its group, the newest first. It
|
|
107
|
+
* removes their entries, and gives them. An empty list means that the log is empty.
|
|
108
|
+
*
|
|
109
|
+
* It reads and checks each item before the first write. A refused undo writes nothing, and its
|
|
110
|
+
* entries stay in the log.
|
|
111
|
+
*
|
|
112
|
+
* A change by another writer between that read and the write of the undo is still overwritten. A
|
|
113
|
+
* storage with versions can close that window later with a conditional write.
|
|
114
|
+
*
|
|
115
|
+
* @throws UndoConflictError when an item changed after its recorded change
|
|
116
|
+
* @throws Error when the log has no storage for a collection
|
|
117
|
+
*/
|
|
118
|
+
undo(): Promise<UndoEntry[]>;
|
|
119
|
+
}
|
|
120
|
+
//# sourceMappingURL=undo-log.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"undo-log.d.ts","sourceRoot":"","sources":["../../src/undo/undo-log.ts"],"names":[],"mappings":"AAAA,OAAO,EAAc,KAAK,YAAY,EAAE,KAAK,sBAAsB,EAAE,MAAM,0BAA0B,CAAC;AACtG,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,yBAAyB,CAAC;AAGjE,0EAA0E;AAC1E,MAAM,WAAW,UAAU;IACzB,8CAA8C;IAC9C,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,0BAA0B;IAC1B,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,iFAAiF;IACjF,QAAQ,CAAC,MAAM,EAAE,OAAO,CAAC;IACzB,+FAA+F;IAC/F,QAAQ,CAAC,KAAK,CAAC,EAAE,YAAY,CAAC;IAC9B;;;;OAIG;IACH,QAAQ,CAAC,KAAK,CAAC,EAAE,OAAO,CAAC;IACzB;;;OAGG;IACH,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC;CACzB;AAED,gCAAgC;AAChC,MAAM,WAAW,SAAS;IACxB,qDAAqD;IACrD,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,8CAA8C;IAC9C,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,0BAA0B;IAC1B,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,6EAA6E;IAC7E,QAAQ,CAAC,OAAO,CAAC,EAAE,sBAAsB,CAAC;IAC1C,yGAAyG;IACzG,QAAQ,CAAC,IAAI,CAAC,EAAE,OAAO,CAAC;IACxB,iGAAiG;IACjG,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,+BAA+B;IAC/B,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC;IACxB,+CAA+C;IAC/C,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,8DAA8D;IAC9D,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;CACtB;AAED,mCAAmC;AACnC,MAAM,WAAW,cAAc;IAC7B,gGAAgG;IAChG,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,CAAC;IACtB,6DAA6D;IAC7D,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;CAC7B;AAED;;;GAGG;AACH,qBAAa,iBAAkB,SAAQ,KAAK;IAC1C,oCAAoC;IACpC,QAAQ,CAAC,KAAK,EAAE,SAAS,CAAC;IAE1B,YAAY,KAAK,EAAE,SAAS,EAI3B;CACF;AAkBD;;;;GAIG;AACH,wBAAgB,aAAa,CAAC,IAAI,EAAE,OAAO,GAAG,MAAM,CAWnD;AAOD;;;;;;;;;;;;;;;;;;GAkBG;AACH,qBAAa,OAAO;;IAOlB;;;;OAIG;IACH,YACE,OAAO,EAAE,iBAAiB,CAAC,SAAS,CAAC,EACrC,SAAS,EAAE,CAAC,UAAU,EAAE,MAAM,KAAK,iBAAiB,CAAC,OAAO,CAAC,GAAG,SAAS,EACzE,OAAO,GAAE,cAAmB,EAM7B;IAED;;;;;;OAMG;IACG,MAAM,CAAC,MAAM,EAAE,UAAU,GAAG,OAAO,CAAC,SAAS,CAAC,CAoBnD;IAED,uDAAuD;IACjD,OAAO,IAAI,OAAO,CAAC,SAAS,EAAE,CAAC,CAEpC;IAED;;;;;;;;;;;;OAYG;IACG,IAAI,IAAI,OAAO,CAAC,SAAS,EAAE,CAAC,CAyBjC;CACF"}
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import { applyPatch } from "@procedures/client/query";
|
|
2
|
+
import { inversePatch } from "./inverse.js";
|
|
3
|
+
/**
|
|
4
|
+
* The error of an undo of an item that changed after the recorded change, for example by another
|
|
5
|
+
* writer. The undo writes nothing, and its entries stay in the log.
|
|
6
|
+
*/
|
|
7
|
+
export class UndoConflictError extends Error {
|
|
8
|
+
/** The entry whose item changed. */
|
|
9
|
+
entry;
|
|
10
|
+
constructor(entry) {
|
|
11
|
+
super(`The undo of ${entry.collection}/${entry.id} is refused: the item changed after the recorded change`);
|
|
12
|
+
this.name = "UndoConflictError";
|
|
13
|
+
this.entry = entry;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
/** True for an object that is not an array and not null. */
|
|
17
|
+
function isObject(value) {
|
|
18
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
19
|
+
}
|
|
20
|
+
/** The JSON text of a value with the keys of each object in order. A missing item is `undefined`. */
|
|
21
|
+
function canonical(value) {
|
|
22
|
+
if (value === undefined || value === null)
|
|
23
|
+
return "null";
|
|
24
|
+
if (Array.isArray(value))
|
|
25
|
+
return `[${value.map((item) => (item === undefined ? "null" : canonical(item))).join(",")}]`;
|
|
26
|
+
if (isObject(value)) {
|
|
27
|
+
const keys = Object.keys(value).filter((key) => value[key] !== undefined).sort();
|
|
28
|
+
return `{${keys.map((key) => `${JSON.stringify(key)}:${canonical(value[key])}`).join(",")}}`;
|
|
29
|
+
}
|
|
30
|
+
return JSON.stringify(value) ?? "null";
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* The fingerprint of an item: a 64-bit hash of its canonical JSON text, in hexadecimal. A missing
|
|
34
|
+
* item and `null` have the same fingerprint. Two items with the same fields in another order have
|
|
35
|
+
* the same fingerprint.
|
|
36
|
+
*/
|
|
37
|
+
export function fingerprintOf(item) {
|
|
38
|
+
const text = canonical(item);
|
|
39
|
+
// Two 32-bit FNV-1a hashes with different offsets: no dependency, and the same in each runtime
|
|
40
|
+
let a = 0x811c9dc5;
|
|
41
|
+
let b = 0x01000193 ^ 0x5bd1e995;
|
|
42
|
+
for (let i = 0; i < text.length; i++) {
|
|
43
|
+
const code = text.charCodeAt(i);
|
|
44
|
+
a = Math.imul(a ^ code, 0x01000193);
|
|
45
|
+
b = Math.imul(b ^ code, 0x01000193) ^ (b >>> 15);
|
|
46
|
+
}
|
|
47
|
+
return (a >>> 0).toString(16).padStart(8, "0") + (b >>> 0).toString(16).padStart(8, "0");
|
|
48
|
+
}
|
|
49
|
+
/** The newest entry first: by time, then by the order of the log. */
|
|
50
|
+
function newestFirst(a, b) {
|
|
51
|
+
return b.at - a.at || b.seq - a.seq;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* An undo log of the changes of items in collection storages (Graph's split, wave 2, W10). Each
|
|
55
|
+
* entry keeps the patch that undoes one change (`inversePatch`), or the whole item before the
|
|
56
|
+
* change when no patch can undo it, and the fingerprint of the item after the change.
|
|
57
|
+
*
|
|
58
|
+
* `undo()` undoes the newest entry and the entries of its group. It first reads each item and
|
|
59
|
+
* compares it with the fingerprint. When another writer changed an item after the recorded change,
|
|
60
|
+
* the undo writes nothing and gives an `UndoConflictError`. Then each item gets one write through
|
|
61
|
+
* its storage, so an undo reaches the observers of the storage as a change.
|
|
62
|
+
*
|
|
63
|
+
* @example
|
|
64
|
+
* ```ts
|
|
65
|
+
* const log = new UndoLog(new InMemoryStorage<UndoEntry>(), (name) => storages[name]);
|
|
66
|
+
* const before = await notes.get("n1");
|
|
67
|
+
* const after = await notes.patch!("n1", { $remove: ["tags.0"] });
|
|
68
|
+
* await log.record({ collection: "notes", id: "n1", before, patch: { $remove: ["tags.0"] }, after });
|
|
69
|
+
* await log.undo(); // the tag is back
|
|
70
|
+
* ```
|
|
71
|
+
*/
|
|
72
|
+
export class UndoLog {
|
|
73
|
+
#entries;
|
|
74
|
+
#storageOf;
|
|
75
|
+
#cap;
|
|
76
|
+
#now;
|
|
77
|
+
#seq = 0;
|
|
78
|
+
/**
|
|
79
|
+
* @param entries - The storage of the entries of the log
|
|
80
|
+
* @param storageOf - The storage of each collection that the log can undo
|
|
81
|
+
* @param options - The cap and the clock
|
|
82
|
+
*/
|
|
83
|
+
constructor(entries, storageOf, options = {}) {
|
|
84
|
+
this.#entries = entries;
|
|
85
|
+
this.#storageOf = storageOf;
|
|
86
|
+
this.#cap = Math.max(1, Math.floor(options.cap ?? 50));
|
|
87
|
+
this.#now = options.now ?? Date.now;
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* This method records one change, and gives its entry. It keeps the inverse patch of the change
|
|
91
|
+
* when one exists, or else the whole item before the change, and the fingerprint of the item
|
|
92
|
+
* after the change. Then it removes the oldest entries past the cap.
|
|
93
|
+
*
|
|
94
|
+
* @throws TypeError when `applyPatch` refuses the patch of the change
|
|
95
|
+
*/
|
|
96
|
+
async record(change) {
|
|
97
|
+
const at = this.#now();
|
|
98
|
+
const seq = ++this.#seq;
|
|
99
|
+
const inverse = change.patch === undefined ? undefined : inversePatch(change.before, change.patch);
|
|
100
|
+
const after = "after" in change || change.patch === undefined ? change.after : applyPatch(change.before, change.patch);
|
|
101
|
+
const entry = {
|
|
102
|
+
key: `undo_${at}_${seq}_${Math.random().toString(36).slice(2, 8)}`,
|
|
103
|
+
collection: change.collection,
|
|
104
|
+
id: change.id,
|
|
105
|
+
...(inverse === undefined ? { item: change.before ?? null } : { inverse }),
|
|
106
|
+
after: fingerprintOf(after),
|
|
107
|
+
...(change.group === undefined ? {} : { group: change.group }),
|
|
108
|
+
at,
|
|
109
|
+
seq,
|
|
110
|
+
};
|
|
111
|
+
await this.#entries.set(entry.key, entry);
|
|
112
|
+
const all = (await this.#entries.getAll()).sort(newestFirst);
|
|
113
|
+
const old = all.slice(this.#cap).map((e) => e.key);
|
|
114
|
+
if (old.length > 0)
|
|
115
|
+
await this.#entries.deleteBatch(old);
|
|
116
|
+
return entry;
|
|
117
|
+
}
|
|
118
|
+
/** This method gives the entries, the newest first. */
|
|
119
|
+
async entries() {
|
|
120
|
+
return (await this.#entries.getAll()).sort(newestFirst);
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* This method undoes the newest change, and each change of its group, the newest first. It
|
|
124
|
+
* removes their entries, and gives them. An empty list means that the log is empty.
|
|
125
|
+
*
|
|
126
|
+
* It reads and checks each item before the first write. A refused undo writes nothing, and its
|
|
127
|
+
* entries stay in the log.
|
|
128
|
+
*
|
|
129
|
+
* A change by another writer between that read and the write of the undo is still overwritten. A
|
|
130
|
+
* storage with versions can close that window later with a conditional write.
|
|
131
|
+
*
|
|
132
|
+
* @throws UndoConflictError when an item changed after its recorded change
|
|
133
|
+
* @throws Error when the log has no storage for a collection
|
|
134
|
+
*/
|
|
135
|
+
async undo() {
|
|
136
|
+
const all = await this.entries();
|
|
137
|
+
const top = all[0];
|
|
138
|
+
if (top === undefined)
|
|
139
|
+
return [];
|
|
140
|
+
const entries = top.group === undefined ? [top] : all.filter((e) => e.group === top.group);
|
|
141
|
+
// The check, and the value of each item after the undo of its entries (newest first)
|
|
142
|
+
const items = new Map();
|
|
143
|
+
for (const entry of entries) {
|
|
144
|
+
const storage = this.#storageOf(entry.collection);
|
|
145
|
+
if (storage === undefined)
|
|
146
|
+
throw new Error(`The undo log has no storage for the collection ${entry.collection}`);
|
|
147
|
+
const address = JSON.stringify([entry.collection, entry.id]);
|
|
148
|
+
const item = items.get(address) ?? { storage, id: entry.id, value: await storage.get(entry.id) };
|
|
149
|
+
if (fingerprintOf(item.value) !== entry.after)
|
|
150
|
+
throw new UndoConflictError(entry);
|
|
151
|
+
item.value = entry.inverse === undefined ? (entry.item ?? undefined) : applyPatch(item.value, entry.inverse);
|
|
152
|
+
items.set(address, item);
|
|
153
|
+
}
|
|
154
|
+
// One write for each item
|
|
155
|
+
for (const { storage, id, value } of items.values()) {
|
|
156
|
+
if (value === undefined || value === null)
|
|
157
|
+
await storage.delete(id);
|
|
158
|
+
else
|
|
159
|
+
await storage.set(id, value);
|
|
160
|
+
}
|
|
161
|
+
await this.#entries.deleteBatch(entries.map((e) => e.key));
|
|
162
|
+
return entries;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
//# sourceMappingURL=undo-log.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"undo-log.js","sourceRoot":"","sources":["../../src/undo/undo-log.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://json.schemastore.org/package",
|
|
3
3
|
"name": "@procedures/collections",
|
|
4
|
-
"version": "0.13.
|
|
4
|
+
"version": "0.13.2",
|
|
5
5
|
"description": "Collections for TypeScript: ArrayList, LinkedList, ArrayDeque, HashMap, LinkedHashMap, TreeMap (red-black), PriorityQueue, LRU and TTL maps, async queues, channels and collectors, tested with fast-check.",
|
|
6
6
|
"keywords": [
|
|
7
7
|
"collections",
|
|
@@ -59,7 +59,7 @@
|
|
|
59
59
|
"provenance": true
|
|
60
60
|
},
|
|
61
61
|
"dependencies": {
|
|
62
|
-
"@procedures/client": "0.13.
|
|
62
|
+
"@procedures/client": "0.13.2"
|
|
63
63
|
},
|
|
64
64
|
"devDependencies": {
|
|
65
65
|
"typescript": "^7.0.2",
|
package/src/index.ts
CHANGED
|
@@ -78,6 +78,9 @@ export * from "./overlay/index.js";
|
|
|
78
78
|
// The order of a list as data: a rank between two neighbors, and the index of a drop
|
|
79
79
|
export * from "./order/index.js";
|
|
80
80
|
|
|
81
|
+
// The undo of the changes of items: the inverse of a patch, and an undo log
|
|
82
|
+
export * from "./undo/index.js";
|
|
83
|
+
|
|
81
84
|
// Export queue interfaces with disambiguation for PriorityQueue
|
|
82
85
|
export type {
|
|
83
86
|
ReadonlyQueue,
|
package/src/storage/interface.ts
CHANGED
|
@@ -172,8 +172,9 @@ export interface CollectionStorage<T> {
|
|
|
172
172
|
/**
|
|
173
173
|
* This method changes one item in one step. The fields are a JSON Merge Patch (RFC 7386): each
|
|
174
174
|
* field replaces the field of the item, an object field merges into an object field, and `null`
|
|
175
|
-
* removes the field. Or they are the operations `{ $set, $unset }`: `$set` can
|
|
176
|
-
* nested field paths
|
|
175
|
+
* removes the field. Or they are the operations `{ $set, $unset, $remove, $insert }`: `$set` can
|
|
176
|
+
* set `null` and nested field paths, and `$remove` and `$insert` move the elements of arrays.
|
|
177
|
+
* Two patches of one item at the same time keep the fields of each. A storage
|
|
177
178
|
* without this method cannot change an item in one step.
|
|
178
179
|
*
|
|
179
180
|
* @param id - Item identifier
|
|
@@ -940,6 +940,35 @@ export function collectionStorageContract(options: CollectionStorageContractOpti
|
|
|
940
940
|
}),
|
|
941
941
|
);
|
|
942
942
|
|
|
943
|
+
add("patch() with $remove and $insert moves the elements of an array, and each index refers to the item before the patch", (context) =>
|
|
944
|
+
withStorage(create, async (typed) => {
|
|
945
|
+
if (lacks(typed, "patch", context)) return;
|
|
946
|
+
const storage = typed as unknown as JsonStorage;
|
|
947
|
+
await storage.set("a", { id: "a", tags: ["a", "b", "c", "d"], rows: [{ n: 1, keys: ["x"] }, { n: 2, keys: ["y"] }], meta: { kind: "k1" } });
|
|
948
|
+
same(
|
|
949
|
+
await storage.patch!("a", { $remove: ["tags.1", "tags.3", "meta.kind"], $insert: { "rows.1.keys.0": "w" } }),
|
|
950
|
+
{ id: "a", tags: ["a", "c"], rows: [{ n: 1, keys: ["x"] }, { n: 2, keys: ["w", "y"] }], meta: {} },
|
|
951
|
+
"patch() with $remove and $insert",
|
|
952
|
+
);
|
|
953
|
+
same(
|
|
954
|
+
await storage.patch!("a", { $insert: { "tags.0": "z", "tags.2": "e" }, $remove: ["rows.0", "missing.0"], $set: { "rows.1.n": 3 } }),
|
|
955
|
+
{ id: "a", tags: ["z", "a", "c", "e"], rows: [{ n: 3, keys: ["w", "y"] }], meta: {} },
|
|
956
|
+
"patch() that inserts at the start and at the end, and removes a missing path",
|
|
957
|
+
);
|
|
958
|
+
const after = { id: "a", tags: ["z", "a", "c", "e"], rows: [{ n: 3, keys: ["w", "y"] }], meta: {} };
|
|
959
|
+
const wrong: Array<[string, unknown]> = [
|
|
960
|
+
["a $remove that is not a list of paths", { $remove: "tags" }],
|
|
961
|
+
["an $insert path that does not end with an index", { $insert: { "meta.kind": 1 } }],
|
|
962
|
+
["a $remove and an $insert in the same array", { $remove: ["tags.0"], $insert: { "tags.1": "q" } }],
|
|
963
|
+
["a path under a removed element", { $remove: ["rows.0"], $set: { "rows.0.n": 4 } }],
|
|
964
|
+
["an $insert past the end of an array", { $insert: { "tags.9": "q" } }],
|
|
965
|
+
["an $insert into a value that is not an array", { $insert: { "meta.0": "q" } }],
|
|
966
|
+
];
|
|
967
|
+
for (const [what, fields] of wrong) await rejects(() => storage.patch!("a", fields as never), `patch() with ${what}`);
|
|
968
|
+
same(await storage.get("a"), after, "get() after the patches that were not correct");
|
|
969
|
+
}),
|
|
970
|
+
);
|
|
971
|
+
|
|
943
972
|
add("patch() runs in one step: patches of one item at the same time keep each field", (context) =>
|
|
944
973
|
withStorage(create, async (typed) => {
|
|
945
974
|
if (lacks(typed, "patch", context)) return;
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The undo of the changes of items (Graph's split, wave 2, W10). `inversePatch` gives the patch
|
|
3
|
+
* that undoes a patch, and `UndoLog` keeps the undo of each change in a storage.
|
|
4
|
+
*/
|
|
5
|
+
export { inversePatch } from "./inverse.js";
|
|
6
|
+
export { fingerprintOf, UndoConflictError, UndoLog, type UndoChange, type UndoEntry, type UndoLogOptions } from "./undo-log.js";
|