@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.
@@ -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.1",
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.1"
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,
@@ -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 set `null` and
176
- * nested field paths. Two patches of one item at the same time keep the fields of each. A storage
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";