shelving 1.280.0 → 1.282.0
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/db/index.d.ts +2 -1
- package/db/index.js +2 -1
- package/db/provider/CacheDBProvider.d.ts +15 -18
- package/db/provider/CacheDBProvider.js +40 -38
- package/db/provider/MemoryDBProvider.d.ts +4 -4
- package/db/provider/MemoryDBProvider.js +7 -7
- package/db/provider/RecordingDBProvider.d.ts +115 -0
- package/db/provider/RecordingDBProvider.js +146 -0
- package/db/provider/UndoDBProvider.d.ts +44 -0
- package/db/provider/UndoDBProvider.js +62 -0
- package/package.json +1 -1
- package/db/provider/ChangesDBProvider.d.ts +0 -62
- package/db/provider/ChangesDBProvider.js +0 -80
package/db/index.d.ts
CHANGED
|
@@ -6,16 +6,17 @@ export * from "./migrate/PostgresMigrator.js";
|
|
|
6
6
|
export * from "./migrate/SQLiteMigrator.js";
|
|
7
7
|
export * from "./migrate/SQLMigrator.js";
|
|
8
8
|
export * from "./provider/CacheDBProvider.js";
|
|
9
|
-
export * from "./provider/ChangesDBProvider.js";
|
|
10
9
|
export * from "./provider/DBProvider.js";
|
|
11
10
|
export * from "./provider/DebugDBProvider.js";
|
|
12
11
|
export * from "./provider/MemoryDBProvider.js";
|
|
13
12
|
export * from "./provider/MockDBProvider.js";
|
|
14
13
|
export * from "./provider/PostgresProvider.js";
|
|
14
|
+
export * from "./provider/RecordingDBProvider.js";
|
|
15
15
|
export * from "./provider/SQLiteProvider.js";
|
|
16
16
|
export * from "./provider/SQLProvider.js";
|
|
17
17
|
export * from "./provider/StorageDBProvider.js";
|
|
18
18
|
export * from "./provider/ThroughDBProvider.js";
|
|
19
|
+
export * from "./provider/UndoDBProvider.js";
|
|
19
20
|
export * from "./provider/ValidationDBProvider.js";
|
|
20
21
|
export * from "./store/ItemStore.js";
|
|
21
22
|
export * from "./store/QueryStore.js";
|
package/db/index.js
CHANGED
|
@@ -6,16 +6,17 @@ export * from "./migrate/PostgresMigrator.js";
|
|
|
6
6
|
export * from "./migrate/SQLiteMigrator.js";
|
|
7
7
|
export * from "./migrate/SQLMigrator.js";
|
|
8
8
|
export * from "./provider/CacheDBProvider.js";
|
|
9
|
-
export * from "./provider/ChangesDBProvider.js";
|
|
10
9
|
export * from "./provider/DBProvider.js";
|
|
11
10
|
export * from "./provider/DebugDBProvider.js";
|
|
12
11
|
export * from "./provider/MemoryDBProvider.js";
|
|
13
12
|
export * from "./provider/MockDBProvider.js";
|
|
14
13
|
export * from "./provider/PostgresProvider.js";
|
|
14
|
+
export * from "./provider/RecordingDBProvider.js";
|
|
15
15
|
export * from "./provider/SQLiteProvider.js";
|
|
16
16
|
export * from "./provider/SQLProvider.js";
|
|
17
17
|
export * from "./provider/StorageDBProvider.js";
|
|
18
18
|
export * from "./provider/ThroughDBProvider.js";
|
|
19
|
+
export * from "./provider/UndoDBProvider.js";
|
|
19
20
|
export * from "./provider/ValidationDBProvider.js";
|
|
20
21
|
export * from "./store/ItemStore.js";
|
|
21
22
|
export * from "./store/QueryStore.js";
|
|
@@ -1,27 +1,23 @@
|
|
|
1
1
|
import type { Data } from "../../util/data.js";
|
|
2
2
|
import type { Identifier, Item, Items, ItemsSequence, OptionalItem, OptionalItemSequence } from "../../util/item.js";
|
|
3
3
|
import type { Query } from "../../util/query.js";
|
|
4
|
-
import type { Sourceable } from "../../util/source.js";
|
|
5
4
|
import type { Updates } from "../../util/update.js";
|
|
6
5
|
import type { Collection } from "../collection/Collection.js";
|
|
7
|
-
import { DBProvider } from "./DBProvider.js";
|
|
6
|
+
import type { DBProvider } from "./DBProvider.js";
|
|
8
7
|
import { MemoryDBProvider } from "./MemoryDBProvider.js";
|
|
8
|
+
import { ThroughDBProvider } from "./ThroughDBProvider.js";
|
|
9
9
|
/**
|
|
10
10
|
* Database provider that keeps a copy of asynchronous remote data in a local synchronous cache.
|
|
11
11
|
*
|
|
12
12
|
* - Wraps a `source` provider and mirrors every read and write into an in-memory `MemoryDBProvider`, so subsequent reads can be served synchronously and live subscriptions stay seeded.
|
|
13
13
|
* - Reads fetch from `source`, then refresh the cache; writes hit `source`, then mirror the change into the cache.
|
|
14
|
+
* - Fetch-first item writes: `updateItem()` and `deleteItem()` fetch the item first (caching it) and skip the source write when it doesn't exist. Query writes are inherited two-step, resolving through this provider's own `getQuery()` — so the matched items are cached, and each per-item write mirrors exactly. The fetch and the writes are separate steps, so wrap them in `transact()` when they must be atomic.
|
|
15
|
+
* - Transactions run on `source` via `transact()` with a transaction-scoped mirror — only a committed transaction's writes reach the cache.
|
|
14
16
|
* - Discover the cache from a wrapping layer with `getSource(CacheDBProvider, provider)` to seed stores from `.memory`.
|
|
15
17
|
*
|
|
16
18
|
* @see https://shelving.cc/db/CacheDBProvider
|
|
17
19
|
*/
|
|
18
|
-
export declare class CacheDBProvider<I extends Identifier, T extends Data> extends
|
|
19
|
-
/**
|
|
20
|
-
* The wrapped source provider that data is fetched from and written to.
|
|
21
|
-
*
|
|
22
|
-
* @see https://shelving.cc/db/CacheDBProvider/source
|
|
23
|
-
*/
|
|
24
|
-
readonly source: DBProvider<I, T>;
|
|
20
|
+
export declare class CacheDBProvider<I extends Identifier, T extends Data> extends ThroughDBProvider<I, T> {
|
|
25
21
|
/**
|
|
26
22
|
* The in-memory provider holding the local synchronous cache of `source` data.
|
|
27
23
|
*
|
|
@@ -40,20 +36,21 @@ export declare class CacheDBProvider<I extends Identifier, T extends Data> exten
|
|
|
40
36
|
addItem<II extends I, TT extends T>(collection: Collection<string, II, TT>, data: TT): Promise<II>;
|
|
41
37
|
/** Mirror the set item into the cache. */
|
|
42
38
|
setItem<II extends I, TT extends T>(collection: Collection<string, II, TT>, id: II, data: TT): Promise<void>;
|
|
43
|
-
/**
|
|
39
|
+
/** Fetch the item first (caching it), then update it only if it exists. */
|
|
44
40
|
updateItem<II extends I, TT extends T>(collection: Collection<string, II, TT>, id: II, updates: Updates<Item<II, TT>>): Promise<void>;
|
|
45
|
-
/**
|
|
41
|
+
/** Fetch the item first, then delete it only if it exists. */
|
|
46
42
|
deleteItem<II extends I, TT extends T>(collection: Collection<string, II, TT>, id: II): Promise<void>;
|
|
47
|
-
countQuery<II extends I, TT extends T>(collection: Collection<string, II, TT>, query?: Query<Item<II, TT>>): Promise<number>;
|
|
48
43
|
/** Read from `source`, then refresh the cache. */
|
|
49
44
|
getQuery<II extends I, TT extends T>(collection: Collection<string, II, TT>, query?: Query<Item<II, TT>>): Promise<Items<II, TT>>;
|
|
50
45
|
/** Mirror each emission into the cache. */
|
|
51
46
|
getQuerySequence<II extends I, TT extends T>(collection: Collection<string, II, TT>, query?: Query<Item<II, TT>>): ItemsSequence<II, TT>;
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
47
|
+
cloneWith(source: DBProvider<I, T>): this;
|
|
48
|
+
/**
|
|
49
|
+
* Runs the transaction on `source`, recording the callback's operations, then commits the recorded writes into the cache once the source commits.
|
|
50
|
+
* - The callback's provider is this cache over the source's transaction (with its own transaction-scoped mirror), so fetch-first writes and query resolution behave exactly as they do outside a transaction — and the fetch-then-write steps are atomic because both run in the source transaction.
|
|
51
|
+
* - Uncommitted data never touches the cache: a thrown callback commits nothing, and if the backend retries the callback only the committed attempt's writes are mirrored.
|
|
52
|
+
* - Update writes commit to the cache as deltas, so they refresh cached items and skip uncached ones — an item only read inside the transaction stays uncached until its next read.
|
|
53
|
+
*/
|
|
54
|
+
transact<X>(callback: (provider: DBProvider<I, T>) => Promise<X>): Promise<X>;
|
|
58
55
|
[Symbol.asyncDispose](): Promise<void>;
|
|
59
56
|
}
|
|
@@ -1,22 +1,19 @@
|
|
|
1
1
|
import { awaitDispose } from "../../util/dispose.js";
|
|
2
|
-
import { DBProvider } from "./DBProvider.js";
|
|
3
2
|
import { MemoryDBProvider } from "./MemoryDBProvider.js";
|
|
3
|
+
import { RecordingDBProvider } from "./RecordingDBProvider.js";
|
|
4
|
+
import { ThroughDBProvider } from "./ThroughDBProvider.js";
|
|
4
5
|
/**
|
|
5
6
|
* Database provider that keeps a copy of asynchronous remote data in a local synchronous cache.
|
|
6
7
|
*
|
|
7
8
|
* - Wraps a `source` provider and mirrors every read and write into an in-memory `MemoryDBProvider`, so subsequent reads can be served synchronously and live subscriptions stay seeded.
|
|
8
9
|
* - Reads fetch from `source`, then refresh the cache; writes hit `source`, then mirror the change into the cache.
|
|
10
|
+
* - Fetch-first item writes: `updateItem()` and `deleteItem()` fetch the item first (caching it) and skip the source write when it doesn't exist. Query writes are inherited two-step, resolving through this provider's own `getQuery()` — so the matched items are cached, and each per-item write mirrors exactly. The fetch and the writes are separate steps, so wrap them in `transact()` when they must be atomic.
|
|
11
|
+
* - Transactions run on `source` via `transact()` with a transaction-scoped mirror — only a committed transaction's writes reach the cache.
|
|
9
12
|
* - Discover the cache from a wrapping layer with `getSource(CacheDBProvider, provider)` to seed stores from `.memory`.
|
|
10
13
|
*
|
|
11
14
|
* @see https://shelving.cc/db/CacheDBProvider
|
|
12
15
|
*/
|
|
13
|
-
export class CacheDBProvider extends
|
|
14
|
-
/**
|
|
15
|
-
* The wrapped source provider that data is fetched from and written to.
|
|
16
|
-
*
|
|
17
|
-
* @see https://shelving.cc/db/CacheDBProvider/source
|
|
18
|
-
*/
|
|
19
|
-
source;
|
|
16
|
+
export class CacheDBProvider extends ThroughDBProvider {
|
|
20
17
|
/**
|
|
21
18
|
* The in-memory provider holding the local synchronous cache of `source` data.
|
|
22
19
|
*
|
|
@@ -27,74 +24,79 @@ export class CacheDBProvider extends DBProvider {
|
|
|
27
24
|
* @param cache In-memory provider to use as the cache (a fresh `MemoryDBProvider` by default).
|
|
28
25
|
*/
|
|
29
26
|
constructor(source, cache = new MemoryDBProvider()) {
|
|
30
|
-
super();
|
|
31
|
-
this.source = source;
|
|
27
|
+
super(source);
|
|
32
28
|
this.memory = cache;
|
|
33
29
|
}
|
|
34
30
|
/** Read from `source`, then refresh the cache. */
|
|
35
31
|
async getItem(collection, id) {
|
|
36
|
-
const item = await
|
|
32
|
+
const item = await super.getItem(collection, id);
|
|
37
33
|
const table = this.memory.getTable(collection);
|
|
38
34
|
item ? table.setItem(id, item) : table.deleteItem(id);
|
|
39
35
|
return item;
|
|
40
36
|
}
|
|
41
37
|
/** Mirror each emission into the cache. */
|
|
42
38
|
getItemSequence(collection, id) {
|
|
43
|
-
return this.memory.getTable(collection).setItemSequence(id,
|
|
39
|
+
return this.memory.getTable(collection).setItemSequence(id, super.getItemSequence(collection, id));
|
|
44
40
|
}
|
|
45
41
|
/** Mirror the added item into the cache. */
|
|
46
42
|
async addItem(collection, data) {
|
|
47
|
-
const id = await
|
|
43
|
+
const id = await super.addItem(collection, data);
|
|
48
44
|
this.memory.getTable(collection).setItem(id, data);
|
|
49
45
|
return id;
|
|
50
46
|
}
|
|
51
47
|
/** Mirror the set item into the cache. */
|
|
52
48
|
async setItem(collection, id, data) {
|
|
53
|
-
await
|
|
49
|
+
await super.setItem(collection, id, data);
|
|
54
50
|
this.memory.getTable(collection).setItem(id, data);
|
|
55
51
|
}
|
|
56
|
-
/**
|
|
52
|
+
/** Fetch the item first (caching it), then update it only if it exists. */
|
|
57
53
|
async updateItem(collection, id, updates) {
|
|
58
|
-
await this.
|
|
54
|
+
const item = await this.getItem(collection, id);
|
|
55
|
+
if (!item)
|
|
56
|
+
return;
|
|
57
|
+
await super.updateItem(collection, id, updates);
|
|
59
58
|
this.memory.getTable(collection).updateItem(id, updates);
|
|
60
59
|
}
|
|
61
|
-
/**
|
|
60
|
+
/** Fetch the item first, then delete it only if it exists. */
|
|
62
61
|
async deleteItem(collection, id) {
|
|
63
|
-
await this.
|
|
62
|
+
const item = await this.getItem(collection, id);
|
|
63
|
+
if (!item)
|
|
64
|
+
return;
|
|
65
|
+
await super.deleteItem(collection, id);
|
|
64
66
|
this.memory.getTable(collection).deleteItem(id);
|
|
65
67
|
}
|
|
66
|
-
countQuery(collection, query) {
|
|
67
|
-
return this.source.countQuery(collection, query);
|
|
68
|
-
}
|
|
69
68
|
/** Read from `source`, then refresh the cache. */
|
|
70
69
|
async getQuery(collection, query) {
|
|
71
|
-
const items = await
|
|
70
|
+
const items = await super.getQuery(collection, query);
|
|
72
71
|
this.memory.getTable(collection).setItems(items);
|
|
73
72
|
return items;
|
|
74
73
|
}
|
|
75
74
|
/** Mirror each emission into the cache. */
|
|
76
75
|
getQuerySequence(collection, query) {
|
|
77
|
-
return this.memory.getTable(collection).setItemsSequence(
|
|
76
|
+
return this.memory.getTable(collection).setItemsSequence(super.getQuerySequence(collection, query));
|
|
78
77
|
}
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
78
|
+
// Override so transaction copies get their own transaction-scoped mirror — uncommitted writes must never touch the real cache.
|
|
79
|
+
cloneWith(source) {
|
|
80
|
+
const clone = super.cloneWith(source);
|
|
81
|
+
Object.defineProperty(clone, "memory", { value: new MemoryDBProvider(), enumerable: true });
|
|
82
|
+
return clone;
|
|
83
83
|
}
|
|
84
|
-
/**
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
async
|
|
91
|
-
|
|
92
|
-
this.
|
|
84
|
+
/**
|
|
85
|
+
* Runs the transaction on `source`, recording the callback's operations, then commits the recorded writes into the cache once the source commits.
|
|
86
|
+
* - The callback's provider is this cache over the source's transaction (with its own transaction-scoped mirror), so fetch-first writes and query resolution behave exactly as they do outside a transaction — and the fetch-then-write steps are atomic because both run in the source transaction.
|
|
87
|
+
* - Uncommitted data never touches the cache: a thrown callback commits nothing, and if the backend retries the callback only the committed attempt's writes are mirrored.
|
|
88
|
+
* - Update writes commit to the cache as deltas, so they refresh cached items and skip uncached ones — an item only read inside the transaction stays uncached until its next read.
|
|
89
|
+
*/
|
|
90
|
+
async transact(callback) {
|
|
91
|
+
let transaction;
|
|
92
|
+
const result = await this.source.transact(provider => callback((transaction = new RecordingDBProvider(this.cloneWith(provider)))));
|
|
93
|
+
if (transaction)
|
|
94
|
+
await transaction.replayWrites(this.memory); // Commit the recorded writes into the cache.
|
|
95
|
+
return result;
|
|
93
96
|
}
|
|
94
97
|
// Implement `AsyncDisposable`
|
|
95
98
|
async [Symbol.asyncDispose]() {
|
|
96
|
-
await awaitDispose(this.
|
|
97
|
-
this.memory, // Dispose the source API provider.
|
|
99
|
+
await awaitDispose(this.memory, // Dispose the cache memory provider.
|
|
98
100
|
super[Symbol.asyncDispose]());
|
|
99
101
|
}
|
|
100
102
|
}
|
|
@@ -11,7 +11,7 @@ import { DBProvider } from "./DBProvider.js";
|
|
|
11
11
|
* - Extremely fast (ideal as the cache behind `CacheDBProvider`!), but does not persist data after the process or browser window closes.
|
|
12
12
|
* - Identity-preserving: `getItem()` etc. return the exact same object instance that was passed into `setItem()`.
|
|
13
13
|
* - Supports live subscriptions, so it can back `ItemStore` / `QueryStore` reads.
|
|
14
|
-
* - Supports transactions: `transact()` runs the callback against a snapshot clone,
|
|
14
|
+
* - Supports transactions: `transact()` runs the callback against a snapshot clone, records its operations with `RecordingDBProvider`, and replays the recorded writes onto this provider on success — sequences and nested transactions work inside the callback, scoped to the transaction.
|
|
15
15
|
*
|
|
16
16
|
* @see https://shelving.cc/db/MemoryDBProvider
|
|
17
17
|
*/
|
|
@@ -69,11 +69,11 @@ export declare class MemoryDBProvider<I extends Identifier = Identifier, T exten
|
|
|
69
69
|
*/
|
|
70
70
|
clone(): MemoryDBProvider<I, T>;
|
|
71
71
|
/**
|
|
72
|
-
* Runs the callback against a shallow clone of this provider,
|
|
73
|
-
* - If the callback throws, the clone and its
|
|
72
|
+
* Runs the callback against a shallow clone of this provider, recording its operations with `RecordingDBProvider`, then replays the recorded writes onto this provider when the callback resolves.
|
|
73
|
+
* - If the callback throws, the clone and its recorded operations are discarded and nothing is committed.
|
|
74
74
|
* - Reads inside the callback see a snapshot from when the transaction began, plus the transaction's own writes. Realtime sequences and nested `transact()` also work inside the callback, scoped to the transaction — portable code must not rely on any of this (see `DBProvider.transact()`).
|
|
75
75
|
* - The clone is disposed when the transaction completes or fails, ending any sequences opened inside the callback.
|
|
76
|
-
* - Writes made to this provider while the callback is running are kept — the
|
|
76
|
+
* - Writes made to this provider while the callback is running are kept — the recorded writes replay on top in order (updates apply as deltas, sets and deletes overwrite), with no conflict detection; overlapping transactions replay in completion order (last write wins per item).
|
|
77
77
|
* - Query writes resolve two-step against the clone, so they commit to exactly the items they matched inside the transaction, even if concurrent writes changed which items match.
|
|
78
78
|
*/
|
|
79
79
|
transact<X>(callback: (provider: DBProvider<I, T>) => Promise<X>): Promise<X>;
|
|
@@ -8,15 +8,15 @@ import { countItems } from "../../util/iterate.js";
|
|
|
8
8
|
import { queryItems, queryWritableItems } from "../../util/query.js";
|
|
9
9
|
import { getRandom, getRandomKey } from "../../util/random.js";
|
|
10
10
|
import { updateData } from "../../util/update.js";
|
|
11
|
-
import { ChangesDBProvider } from "./ChangesDBProvider.js";
|
|
12
11
|
import { DBProvider } from "./DBProvider.js";
|
|
12
|
+
import { RecordingDBProvider } from "./RecordingDBProvider.js";
|
|
13
13
|
/**
|
|
14
14
|
* Synchronous in-memory database provider, storing each collection in a `MemoryTable`.
|
|
15
15
|
*
|
|
16
16
|
* - Extremely fast (ideal as the cache behind `CacheDBProvider`!), but does not persist data after the process or browser window closes.
|
|
17
17
|
* - Identity-preserving: `getItem()` etc. return the exact same object instance that was passed into `setItem()`.
|
|
18
18
|
* - Supports live subscriptions, so it can back `ItemStore` / `QueryStore` reads.
|
|
19
|
-
* - Supports transactions: `transact()` runs the callback against a snapshot clone,
|
|
19
|
+
* - Supports transactions: `transact()` runs the callback against a snapshot clone, records its operations with `RecordingDBProvider`, and replays the recorded writes onto this provider on success — sequences and nested transactions work inside the callback, scoped to the transaction.
|
|
20
20
|
*
|
|
21
21
|
* @see https://shelving.cc/db/MemoryDBProvider
|
|
22
22
|
*/
|
|
@@ -108,19 +108,19 @@ export class MemoryDBProvider extends DBProvider {
|
|
|
108
108
|
return clone;
|
|
109
109
|
}
|
|
110
110
|
/**
|
|
111
|
-
* Runs the callback against a shallow clone of this provider,
|
|
112
|
-
* - If the callback throws, the clone and its
|
|
111
|
+
* Runs the callback against a shallow clone of this provider, recording its operations with `RecordingDBProvider`, then replays the recorded writes onto this provider when the callback resolves.
|
|
112
|
+
* - If the callback throws, the clone and its recorded operations are discarded and nothing is committed.
|
|
113
113
|
* - Reads inside the callback see a snapshot from when the transaction began, plus the transaction's own writes. Realtime sequences and nested `transact()` also work inside the callback, scoped to the transaction — portable code must not rely on any of this (see `DBProvider.transact()`).
|
|
114
114
|
* - The clone is disposed when the transaction completes or fails, ending any sequences opened inside the callback.
|
|
115
|
-
* - Writes made to this provider while the callback is running are kept — the
|
|
115
|
+
* - Writes made to this provider while the callback is running are kept — the recorded writes replay on top in order (updates apply as deltas, sets and deletes overwrite), with no conflict detection; overlapping transactions replay in completion order (last write wins per item).
|
|
116
116
|
* - Query writes resolve two-step against the clone, so they commit to exactly the items they matched inside the transaction, even if concurrent writes changed which items match.
|
|
117
117
|
*/
|
|
118
118
|
async transact(callback) {
|
|
119
119
|
const clone = this.clone();
|
|
120
120
|
try {
|
|
121
|
-
const transaction = new
|
|
121
|
+
const transaction = new RecordingDBProvider(clone);
|
|
122
122
|
const result = await callback(transaction);
|
|
123
|
-
await transaction.
|
|
123
|
+
await transaction.replayWrites(this); // Commit the recorded writes.
|
|
124
124
|
return result;
|
|
125
125
|
}
|
|
126
126
|
finally {
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import type { ImmutableArray, MutableArray } from "../../util/array.js";
|
|
2
|
+
import type { Data } from "../../util/data.js";
|
|
3
|
+
import type { Identifier, Item, Items, OptionalItem } from "../../util/item.js";
|
|
4
|
+
import type { Query } from "../../util/query.js";
|
|
5
|
+
import type { Updates } from "../../util/update.js";
|
|
6
|
+
import type { Collection } from "../collection/Collection.js";
|
|
7
|
+
import type { DBProvider } from "./DBProvider.js";
|
|
8
|
+
import { ThroughDBProvider } from "./ThroughDBProvider.js";
|
|
9
|
+
/**
|
|
10
|
+
* Structured log entry recording a single database operation performed through a `RecordingDBProvider`.
|
|
11
|
+
*
|
|
12
|
+
* - `action` is the kind of operation; `collection` is the `Collection` it applies to; `id` is the item involved; `data` and `updates` carry whichever fields apply to that operation.
|
|
13
|
+
* - A `"get"` operation records a read — `data` is the item that was observed, or `undefined` if the read confirmed the item absent.
|
|
14
|
+
*
|
|
15
|
+
* @see https://shelving.cc/db/DBOperation
|
|
16
|
+
*/
|
|
17
|
+
export type DBOperation<I extends Identifier = Identifier, T extends Data = Data> = {
|
|
18
|
+
readonly action: "get" | "add" | "set" | "update" | "delete";
|
|
19
|
+
readonly collection: Collection<string, I, T>;
|
|
20
|
+
readonly id: I;
|
|
21
|
+
readonly data?: unknown;
|
|
22
|
+
readonly updates?: unknown;
|
|
23
|
+
};
|
|
24
|
+
/**
|
|
25
|
+
* Readonly array of `DBOperation` entries, e.g. the log recorded by a `RecordingDBProvider`.
|
|
26
|
+
*
|
|
27
|
+
* @see https://shelving.cc/db/DBOperations
|
|
28
|
+
*/
|
|
29
|
+
export type DBOperations<I extends Identifier = Identifier, T extends Data = Data> = ImmutableArray<DBOperation<I, T>>;
|
|
30
|
+
/**
|
|
31
|
+
* Replay a list of database operations onto a provider, re-issuing each one in order.
|
|
32
|
+
*
|
|
33
|
+
* - Writes re-issue as the corresponding item write — `"add"` replays as `DBProvider.setItem()` with the logged id, so the target keeps the same generated ids.
|
|
34
|
+
* - `"get"` reads apply what they observed — the item is set, or deleted when the read confirmed it absent. That's right for refreshing a mirror or cache, and wrong for an authoritative target, where an observed snapshot would overwrite newer data — replay only the writes there (see `RecordingDBProvider.replayWrites()`).
|
|
35
|
+
* - Operations re-issue as a sequence of awaited writes — the replay itself is not atomic.
|
|
36
|
+
*
|
|
37
|
+
* @param provider Provider to replay the operations onto.
|
|
38
|
+
* @param operations Operations to replay, in order.
|
|
39
|
+
* @example await replayOperations(mirror, recording.operations);
|
|
40
|
+
* @see https://shelving.cc/db/replayOperations
|
|
41
|
+
*/
|
|
42
|
+
export declare function replayOperations<I extends Identifier, T extends Data>(provider: DBProvider<I, T>, operations: DBOperations<I, T>): Promise<void>;
|
|
43
|
+
/**
|
|
44
|
+
* Database provider that records every operation it performs to its `operations` log.
|
|
45
|
+
*
|
|
46
|
+
* - Wraps a `source` provider, delegates each operation, then appends a `DBOperation` entry describing what happened.
|
|
47
|
+
* - Records reads as well as writes: `getItem()` logs a `"get"` with the item it observed (or its confirmed absence), and `getQuery()` logs a `"get"` per item returned. Derived reads and two-step query writes are inherited, so everything they do is recorded per item too. Realtime sequences are not recorded.
|
|
48
|
+
* - Replay the log onto another provider with `replay()`, `replayWrites()`, or `replayReads()`.
|
|
49
|
+
* - Useful for building audit logging, change feeds, optimistic updates (see `UndoDBProvider`), or assertions in tests.
|
|
50
|
+
*
|
|
51
|
+
* @see https://shelving.cc/db/RecordingDBProvider
|
|
52
|
+
*/
|
|
53
|
+
export declare class RecordingDBProvider<I extends Identifier, T extends Data> extends ThroughDBProvider<I, T> {
|
|
54
|
+
/**
|
|
55
|
+
* The log of operations performed through this provider, in the order they happened.
|
|
56
|
+
*
|
|
57
|
+
* @see https://shelving.cc/db/RecordingDBProvider/operations
|
|
58
|
+
*/
|
|
59
|
+
get operations(): DBOperations<I, T>;
|
|
60
|
+
readonly _operations: MutableArray<DBOperation<I, T>>;
|
|
61
|
+
/**
|
|
62
|
+
* The write operations from the `operations` log, in the order they happened.
|
|
63
|
+
*
|
|
64
|
+
* @see https://shelving.cc/db/RecordingDBProvider/writes
|
|
65
|
+
*/
|
|
66
|
+
get writes(): DBOperations<I, T>;
|
|
67
|
+
/**
|
|
68
|
+
* The `"get"` read operations from the `operations` log, in the order they happened.
|
|
69
|
+
*
|
|
70
|
+
* @see https://shelving.cc/db/RecordingDBProvider/reads
|
|
71
|
+
*/
|
|
72
|
+
get reads(): DBOperations<I, T>;
|
|
73
|
+
/** Log a `"get"` operation recording the item that was observed (or its confirmed absence). */
|
|
74
|
+
getItem<II extends I, TT extends T>(collection: Collection<string, II, TT>, id: II): Promise<OptionalItem<II, TT>>;
|
|
75
|
+
/** Log a `"get"` operation for each item the query observed. */
|
|
76
|
+
getQuery<II extends I, TT extends T>(collection: Collection<string, II, TT>, query?: Query<Item<II, TT>>): Promise<Items<II, TT>>;
|
|
77
|
+
/** Log an `"add"` operation after writing. */
|
|
78
|
+
addItem<II extends I, TT extends T>(collection: Collection<string, II, TT>, data: TT): Promise<II>;
|
|
79
|
+
/** Log a `"set"` operation after writing. */
|
|
80
|
+
setItem<II extends I, TT extends T>(collection: Collection<string, II, TT>, id: II, data: TT): Promise<void>;
|
|
81
|
+
/** Log an `"update"` operation after writing. */
|
|
82
|
+
updateItem<II extends I, TT extends T>(collection: Collection<string, II, TT>, id: II, updates: Updates<Item<II, TT>>): Promise<void>;
|
|
83
|
+
/** Log a `"delete"` operation after writing. */
|
|
84
|
+
deleteItem<II extends I, TT extends T>(collection: Collection<string, II, TT>, id: II): Promise<void>;
|
|
85
|
+
/**
|
|
86
|
+
* Replay every recorded operation onto another provider, in order.
|
|
87
|
+
* - Reads apply what they observed and writes re-issue, so this refreshes a mirror or cache exactly — see `replayOperations()`.
|
|
88
|
+
* - Use `replayWrites()` for an authoritative target, where applying observed reads would overwrite newer data.
|
|
89
|
+
*
|
|
90
|
+
* @param provider Provider to replay the operations onto.
|
|
91
|
+
* @example await recording.replay(mirror);
|
|
92
|
+
* @see https://shelving.cc/db/RecordingDBProvider/replay
|
|
93
|
+
*/
|
|
94
|
+
replay(provider: DBProvider<I, T>): Promise<void>;
|
|
95
|
+
/**
|
|
96
|
+
* Replay only the recorded write operations onto another provider, in order.
|
|
97
|
+
* - The right call for authoritative targets — applying a log to a real database, audit replay, syncing a second source of truth — where updates should compose onto current state and observed reads must never overwrite newer data.
|
|
98
|
+
*
|
|
99
|
+
* @param provider Provider to replay the writes onto.
|
|
100
|
+
* @example await recording.replayWrites(db);
|
|
101
|
+
* @see https://shelving.cc/db/RecordingDBProvider/replayWrites
|
|
102
|
+
*/
|
|
103
|
+
replayWrites(provider: DBProvider<I, T>): Promise<void>;
|
|
104
|
+
/**
|
|
105
|
+
* Replay only the recorded `"get"` read operations onto another provider, in order.
|
|
106
|
+
* - Applies what each read observed (setting the item, or deleting it when the read confirmed absence) — e.g. warming a cache from a recorded session.
|
|
107
|
+
*
|
|
108
|
+
* @param provider Provider to replay the reads onto.
|
|
109
|
+
* @example await recording.replayReads(cache.memory);
|
|
110
|
+
* @see https://shelving.cc/db/RecordingDBProvider/replayReads
|
|
111
|
+
*/
|
|
112
|
+
replayReads(provider: DBProvider<I, T>): Promise<void>;
|
|
113
|
+
cloneWith(source: DBProvider<I, T>): this;
|
|
114
|
+
transact<X>(callback: (provider: DBProvider<I, T>) => Promise<X>): Promise<X>;
|
|
115
|
+
}
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import { ThroughDBProvider } from "./ThroughDBProvider.js";
|
|
2
|
+
/**
|
|
3
|
+
* Replay a list of database operations onto a provider, re-issuing each one in order.
|
|
4
|
+
*
|
|
5
|
+
* - Writes re-issue as the corresponding item write — `"add"` replays as `DBProvider.setItem()` with the logged id, so the target keeps the same generated ids.
|
|
6
|
+
* - `"get"` reads apply what they observed — the item is set, or deleted when the read confirmed it absent. That's right for refreshing a mirror or cache, and wrong for an authoritative target, where an observed snapshot would overwrite newer data — replay only the writes there (see `RecordingDBProvider.replayWrites()`).
|
|
7
|
+
* - Operations re-issue as a sequence of awaited writes — the replay itself is not atomic.
|
|
8
|
+
*
|
|
9
|
+
* @param provider Provider to replay the operations onto.
|
|
10
|
+
* @param operations Operations to replay, in order.
|
|
11
|
+
* @example await replayOperations(mirror, recording.operations);
|
|
12
|
+
* @see https://shelving.cc/db/replayOperations
|
|
13
|
+
*/
|
|
14
|
+
export async function replayOperations(provider, operations) {
|
|
15
|
+
// `as` casts needed: the log stores `data` and `updates` loosely as `unknown`.
|
|
16
|
+
for (const { action, collection, id, data, updates } of operations) {
|
|
17
|
+
if (action === "update")
|
|
18
|
+
await provider.updateItem(collection, id, updates);
|
|
19
|
+
else if (action === "delete")
|
|
20
|
+
await provider.deleteItem(collection, id);
|
|
21
|
+
else if (action === "get" && !data)
|
|
22
|
+
await provider.deleteItem(collection, id);
|
|
23
|
+
else
|
|
24
|
+
await provider.setItem(collection, id, data); // Observed items, adds, and sets all replay as the item's full data.
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Database provider that records every operation it performs to its `operations` log.
|
|
29
|
+
*
|
|
30
|
+
* - Wraps a `source` provider, delegates each operation, then appends a `DBOperation` entry describing what happened.
|
|
31
|
+
* - Records reads as well as writes: `getItem()` logs a `"get"` with the item it observed (or its confirmed absence), and `getQuery()` logs a `"get"` per item returned. Derived reads and two-step query writes are inherited, so everything they do is recorded per item too. Realtime sequences are not recorded.
|
|
32
|
+
* - Replay the log onto another provider with `replay()`, `replayWrites()`, or `replayReads()`.
|
|
33
|
+
* - Useful for building audit logging, change feeds, optimistic updates (see `UndoDBProvider`), or assertions in tests.
|
|
34
|
+
*
|
|
35
|
+
* @see https://shelving.cc/db/RecordingDBProvider
|
|
36
|
+
*/
|
|
37
|
+
export class RecordingDBProvider extends ThroughDBProvider {
|
|
38
|
+
/**
|
|
39
|
+
* The log of operations performed through this provider, in the order they happened.
|
|
40
|
+
*
|
|
41
|
+
* @see https://shelving.cc/db/RecordingDBProvider/operations
|
|
42
|
+
*/
|
|
43
|
+
get operations() {
|
|
44
|
+
return this._operations;
|
|
45
|
+
}
|
|
46
|
+
_operations = [];
|
|
47
|
+
/**
|
|
48
|
+
* The write operations from the `operations` log, in the order they happened.
|
|
49
|
+
*
|
|
50
|
+
* @see https://shelving.cc/db/RecordingDBProvider/writes
|
|
51
|
+
*/
|
|
52
|
+
get writes() {
|
|
53
|
+
return this._operations.filter(({ action }) => action !== "get");
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* The `"get"` read operations from the `operations` log, in the order they happened.
|
|
57
|
+
*
|
|
58
|
+
* @see https://shelving.cc/db/RecordingDBProvider/reads
|
|
59
|
+
*/
|
|
60
|
+
get reads() {
|
|
61
|
+
return this._operations.filter(({ action }) => action === "get");
|
|
62
|
+
}
|
|
63
|
+
/** Log a `"get"` operation recording the item that was observed (or its confirmed absence). */
|
|
64
|
+
async getItem(collection, id) {
|
|
65
|
+
const item = await super.getItem(collection, id);
|
|
66
|
+
this._operations.push({ action: "get", collection, id, data: item });
|
|
67
|
+
return item;
|
|
68
|
+
}
|
|
69
|
+
/** Log a `"get"` operation for each item the query observed. */
|
|
70
|
+
async getQuery(collection, query) {
|
|
71
|
+
const items = await super.getQuery(collection, query);
|
|
72
|
+
for (const item of items)
|
|
73
|
+
this._operations.push({ action: "get", collection, id: item.id, data: item });
|
|
74
|
+
return items;
|
|
75
|
+
}
|
|
76
|
+
/** Log an `"add"` operation after writing. */
|
|
77
|
+
async addItem(collection, data) {
|
|
78
|
+
const id = await super.addItem(collection, data);
|
|
79
|
+
this._operations.push({ action: "add", collection, id, data });
|
|
80
|
+
return id;
|
|
81
|
+
}
|
|
82
|
+
/** Log a `"set"` operation after writing. */
|
|
83
|
+
async setItem(collection, id, data) {
|
|
84
|
+
await super.setItem(collection, id, data);
|
|
85
|
+
this._operations.push({ action: "set", collection, id, data });
|
|
86
|
+
}
|
|
87
|
+
/** Log an `"update"` operation after writing. */
|
|
88
|
+
async updateItem(collection, id, updates) {
|
|
89
|
+
await super.updateItem(collection, id, updates);
|
|
90
|
+
this._operations.push({ action: "update", collection, id, updates });
|
|
91
|
+
}
|
|
92
|
+
/** Log a `"delete"` operation after writing. */
|
|
93
|
+
async deleteItem(collection, id) {
|
|
94
|
+
await super.deleteItem(collection, id);
|
|
95
|
+
this._operations.push({ action: "delete", collection, id });
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Replay every recorded operation onto another provider, in order.
|
|
99
|
+
* - Reads apply what they observed and writes re-issue, so this refreshes a mirror or cache exactly — see `replayOperations()`.
|
|
100
|
+
* - Use `replayWrites()` for an authoritative target, where applying observed reads would overwrite newer data.
|
|
101
|
+
*
|
|
102
|
+
* @param provider Provider to replay the operations onto.
|
|
103
|
+
* @example await recording.replay(mirror);
|
|
104
|
+
* @see https://shelving.cc/db/RecordingDBProvider/replay
|
|
105
|
+
*/
|
|
106
|
+
replay(provider) {
|
|
107
|
+
return replayOperations(provider, this._operations);
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Replay only the recorded write operations onto another provider, in order.
|
|
111
|
+
* - The right call for authoritative targets — applying a log to a real database, audit replay, syncing a second source of truth — where updates should compose onto current state and observed reads must never overwrite newer data.
|
|
112
|
+
*
|
|
113
|
+
* @param provider Provider to replay the writes onto.
|
|
114
|
+
* @example await recording.replayWrites(db);
|
|
115
|
+
* @see https://shelving.cc/db/RecordingDBProvider/replayWrites
|
|
116
|
+
*/
|
|
117
|
+
replayWrites(provider) {
|
|
118
|
+
return replayOperations(provider, this.writes);
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Replay only the recorded `"get"` read operations onto another provider, in order.
|
|
122
|
+
* - Applies what each read observed (setting the item, or deleting it when the read confirmed absence) — e.g. warming a cache from a recorded session.
|
|
123
|
+
*
|
|
124
|
+
* @param provider Provider to replay the reads onto.
|
|
125
|
+
* @example await recording.replayReads(cache.memory);
|
|
126
|
+
* @see https://shelving.cc/db/RecordingDBProvider/replayReads
|
|
127
|
+
*/
|
|
128
|
+
replayReads(provider) {
|
|
129
|
+
return replayOperations(provider, this.reads);
|
|
130
|
+
}
|
|
131
|
+
// Override so that transaction copies get their own log.
|
|
132
|
+
cloneWith(source) {
|
|
133
|
+
const clone = super.cloneWith(source);
|
|
134
|
+
Object.defineProperty(clone, "_operations", { value: [], enumerable: false });
|
|
135
|
+
return clone;
|
|
136
|
+
}
|
|
137
|
+
// Override to log the transaction's operations after it commits — a failed transaction logs nothing.
|
|
138
|
+
// The merge must happen after `source.transact()` resolves: backends may retry the callback or fail the commit itself, and only the committed attempt's operations belong in the log.
|
|
139
|
+
async transact(callback) {
|
|
140
|
+
let transaction;
|
|
141
|
+
const result = await this.source.transact(provider => callback((transaction = this.cloneWith(provider))));
|
|
142
|
+
if (transaction)
|
|
143
|
+
this._operations.push(...transaction.operations);
|
|
144
|
+
return result;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import type { Data } from "../../util/data.js";
|
|
2
|
+
import type { Identifier, Item } from "../../util/item.js";
|
|
3
|
+
import type { Updates } from "../../util/update.js";
|
|
4
|
+
import type { Collection } from "../collection/Collection.js";
|
|
5
|
+
import { RecordingDBProvider } from "./RecordingDBProvider.js";
|
|
6
|
+
/**
|
|
7
|
+
* Database provider that records every operation and can undo its own writes.
|
|
8
|
+
*
|
|
9
|
+
* - Extends `RecordingDBProvider`, additionally reading each item before the first write that touches it — so the log always contains every touched item's original state.
|
|
10
|
+
* - Call `undo()` to restore the wrapped provider to the state the log first observed, e.g. rolling back optimistic local updates after a failed server call.
|
|
11
|
+
* - The extra read is skipped when the log already establishes the item's state (an earlier read observed it, or an earlier add created it) — over a local `MemoryDBProvider` the reads are effectively free anyway.
|
|
12
|
+
*
|
|
13
|
+
* @example
|
|
14
|
+
* const local = new UndoDBProvider(memory);
|
|
15
|
+
* await runServiceLogic(local); // Applies locally right away.
|
|
16
|
+
* try {
|
|
17
|
+
* await api.push(local.writes); // Send the writes to the server.
|
|
18
|
+
* } catch {
|
|
19
|
+
* await local.undo(); // Server failed — restore the local copy.
|
|
20
|
+
* }
|
|
21
|
+
*
|
|
22
|
+
* @see https://shelving.cc/db/UndoDBProvider
|
|
23
|
+
*/
|
|
24
|
+
export declare class UndoDBProvider<I extends Identifier, T extends Data> extends RecordingDBProvider<I, T> {
|
|
25
|
+
/** Whether the log already establishes the original state of an item (an earlier read observed it, or an earlier add created it). */
|
|
26
|
+
protected _isEstablished(collection: Collection<string, I, T>, id: I): boolean;
|
|
27
|
+
/** Read the item first (recording its original state) if the log doesn't already establish it. */
|
|
28
|
+
setItem<II extends I, TT extends T>(collection: Collection<string, II, TT>, id: II, data: TT): Promise<void>;
|
|
29
|
+
/** Read the item first (recording its original state) if the log doesn't already establish it. */
|
|
30
|
+
updateItem<II extends I, TT extends T>(collection: Collection<string, II, TT>, id: II, updates: Updates<Item<II, TT>>): Promise<void>;
|
|
31
|
+
/** Read the item first (recording its original state) if the log doesn't already establish it. */
|
|
32
|
+
deleteItem<II extends I, TT extends T>(collection: Collection<string, II, TT>, id: II): Promise<void>;
|
|
33
|
+
/**
|
|
34
|
+
* Restore the wrapped provider to the state the log first observed, undoing this provider's writes.
|
|
35
|
+
*
|
|
36
|
+
* - Applies the state-establishing operations in reverse order, so the earliest observed state of each item wins: reads restore the item that was observed (or delete it when the read confirmed it absent), and adds delete (the item did not exist before).
|
|
37
|
+
* - Restores touched items unconditionally, so concurrent writes made to those items since the recording are overwritten.
|
|
38
|
+
* - Writes directly to `source` without recording, so the log still describes the original operations afterwards.
|
|
39
|
+
*
|
|
40
|
+
* @example await provider.undo();
|
|
41
|
+
* @see https://shelving.cc/db/UndoDBProvider/undo
|
|
42
|
+
*/
|
|
43
|
+
undo(): Promise<void>;
|
|
44
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { RecordingDBProvider } from "./RecordingDBProvider.js";
|
|
2
|
+
/**
|
|
3
|
+
* Database provider that records every operation and can undo its own writes.
|
|
4
|
+
*
|
|
5
|
+
* - Extends `RecordingDBProvider`, additionally reading each item before the first write that touches it — so the log always contains every touched item's original state.
|
|
6
|
+
* - Call `undo()` to restore the wrapped provider to the state the log first observed, e.g. rolling back optimistic local updates after a failed server call.
|
|
7
|
+
* - The extra read is skipped when the log already establishes the item's state (an earlier read observed it, or an earlier add created it) — over a local `MemoryDBProvider` the reads are effectively free anyway.
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* const local = new UndoDBProvider(memory);
|
|
11
|
+
* await runServiceLogic(local); // Applies locally right away.
|
|
12
|
+
* try {
|
|
13
|
+
* await api.push(local.writes); // Send the writes to the server.
|
|
14
|
+
* } catch {
|
|
15
|
+
* await local.undo(); // Server failed — restore the local copy.
|
|
16
|
+
* }
|
|
17
|
+
*
|
|
18
|
+
* @see https://shelving.cc/db/UndoDBProvider
|
|
19
|
+
*/
|
|
20
|
+
export class UndoDBProvider extends RecordingDBProvider {
|
|
21
|
+
/** Whether the log already establishes the original state of an item (an earlier read observed it, or an earlier add created it). */
|
|
22
|
+
_isEstablished(collection, id) {
|
|
23
|
+
return this._operations.some(operation => operation.collection === collection && operation.id === id && (operation.action === "get" || operation.action === "add"));
|
|
24
|
+
}
|
|
25
|
+
/** Read the item first (recording its original state) if the log doesn't already establish it. */
|
|
26
|
+
async setItem(collection, id, data) {
|
|
27
|
+
if (!this._isEstablished(collection, id))
|
|
28
|
+
await this.getItem(collection, id);
|
|
29
|
+
await super.setItem(collection, id, data);
|
|
30
|
+
}
|
|
31
|
+
/** Read the item first (recording its original state) if the log doesn't already establish it. */
|
|
32
|
+
async updateItem(collection, id, updates) {
|
|
33
|
+
if (!this._isEstablished(collection, id))
|
|
34
|
+
await this.getItem(collection, id);
|
|
35
|
+
await super.updateItem(collection, id, updates);
|
|
36
|
+
}
|
|
37
|
+
/** Read the item first (recording its original state) if the log doesn't already establish it. */
|
|
38
|
+
async deleteItem(collection, id) {
|
|
39
|
+
if (!this._isEstablished(collection, id))
|
|
40
|
+
await this.getItem(collection, id);
|
|
41
|
+
await super.deleteItem(collection, id);
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Restore the wrapped provider to the state the log first observed, undoing this provider's writes.
|
|
45
|
+
*
|
|
46
|
+
* - Applies the state-establishing operations in reverse order, so the earliest observed state of each item wins: reads restore the item that was observed (or delete it when the read confirmed it absent), and adds delete (the item did not exist before).
|
|
47
|
+
* - Restores touched items unconditionally, so concurrent writes made to those items since the recording are overwritten.
|
|
48
|
+
* - Writes directly to `source` without recording, so the log still describes the original operations afterwards.
|
|
49
|
+
*
|
|
50
|
+
* @example await provider.undo();
|
|
51
|
+
* @see https://shelving.cc/db/UndoDBProvider/undo
|
|
52
|
+
*/
|
|
53
|
+
async undo() {
|
|
54
|
+
// `as` cast needed: the log stores `data` loosely as `unknown`.
|
|
55
|
+
for (const { action, collection, id, data } of this._operations.toReversed()) {
|
|
56
|
+
if (action === "get" && data)
|
|
57
|
+
await this.source.setItem(collection, id, data);
|
|
58
|
+
else if (action === "get" || action === "add")
|
|
59
|
+
await this.source.deleteItem(collection, id);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
package/package.json
CHANGED
|
@@ -1,62 +0,0 @@
|
|
|
1
|
-
import type { MutableArray } from "../../util/array.js";
|
|
2
|
-
import type { Data } from "../../util/data.js";
|
|
3
|
-
import type { Identifier, Item } from "../../util/item.js";
|
|
4
|
-
import type { Updates } from "../../util/update.js";
|
|
5
|
-
import type { Collection } from "../collection/Collection.js";
|
|
6
|
-
import type { DBProvider } from "./DBProvider.js";
|
|
7
|
-
import { ThroughDBProvider } from "./ThroughDBProvider.js";
|
|
8
|
-
/**
|
|
9
|
-
* Structured log entry recording a single database write performed through a `ChangesDBProvider`.
|
|
10
|
-
*
|
|
11
|
-
* - `action` is the kind of write; `collection` is the `Collection` the write applies to; `id` is the item that was written; `data` and `updates` carry whichever fields apply to that write.
|
|
12
|
-
*
|
|
13
|
-
* @see https://shelving.cc/db/DBChange
|
|
14
|
-
*/
|
|
15
|
-
export type DBChange<I extends Identifier, T extends Data = Data> = {
|
|
16
|
-
readonly action: "add" | "set" | "update" | "delete";
|
|
17
|
-
readonly collection: Collection<string, I, T>;
|
|
18
|
-
readonly id: I;
|
|
19
|
-
readonly data?: unknown;
|
|
20
|
-
readonly updates?: unknown;
|
|
21
|
-
};
|
|
22
|
-
/**
|
|
23
|
-
* Database provider that records every write it performs to its `changes` log.
|
|
24
|
-
*
|
|
25
|
-
* - Wraps a `source` provider, delegates each write, then appends a `DBChange` entry describing what happened.
|
|
26
|
-
* - Every change is an explicit per-item write with an `id` — query writes are inherited two-step from `ThroughDBProvider`, so they arrive here as the individual item writes they resolved to.
|
|
27
|
-
* - Replay the log onto another provider with `ChangesDBProvider.replay()`.
|
|
28
|
-
* - Useful for building audit logging, change feeds, or assertions in tests; reads are passed straight through and not logged.
|
|
29
|
-
*
|
|
30
|
-
* @see https://shelving.cc/db/ChangesDBProvider
|
|
31
|
-
*/
|
|
32
|
-
export declare class ChangesDBProvider<I extends Identifier, T extends Data> extends ThroughDBProvider<I, T> {
|
|
33
|
-
/**
|
|
34
|
-
* The log of writes performed through this provider, in the order they happened.
|
|
35
|
-
*
|
|
36
|
-
* @see https://shelving.cc/db/ChangesDBProvider/changes
|
|
37
|
-
*/
|
|
38
|
-
get changes(): ReadonlyArray<DBChange<I, T>>;
|
|
39
|
-
readonly _changes: MutableArray<DBChange<I, T>>;
|
|
40
|
-
/** Log an `"add"` change after writing. */
|
|
41
|
-
addItem<II extends I, TT extends T>(collection: Collection<string, II, TT>, data: TT): Promise<II>;
|
|
42
|
-
/** Log a `"set"` change after writing. */
|
|
43
|
-
setItem<II extends I, TT extends T>(collection: Collection<string, II, TT>, id: II, data: TT): Promise<void>;
|
|
44
|
-
/** Log an `"update"` change after writing. */
|
|
45
|
-
updateItem<II extends I, TT extends T>(collection: Collection<string, II, TT>, id: II, updates: Updates<Item<II, TT>>): Promise<void>;
|
|
46
|
-
/** Log a `"delete"` change after writing. */
|
|
47
|
-
deleteItem<II extends I, TT extends T>(collection: Collection<string, II, TT>, id: II): Promise<void>;
|
|
48
|
-
/**
|
|
49
|
-
* Replay the `changes` log onto another provider, re-issuing each write in order.
|
|
50
|
-
*
|
|
51
|
-
* - Useful for audit replay or syncing a secondary store — and the commit mechanism for `MemoryDBProvider.transact()`.
|
|
52
|
-
* - `"add"` changes replay as `DBProvider.setItem()` with the logged id, so the target keeps the same generated ids.
|
|
53
|
-
* - The changes replay as a sequence of awaited writes — the replay itself is not atomic.
|
|
54
|
-
*
|
|
55
|
-
* @param provider Provider to replay the changes onto.
|
|
56
|
-
* @example await changes.replay(mirror);
|
|
57
|
-
* @see https://shelving.cc/db/ChangesDBProvider/replay
|
|
58
|
-
*/
|
|
59
|
-
replay(provider: DBProvider<I, T>): Promise<void>;
|
|
60
|
-
cloneWith(source: DBProvider<I, T>): this;
|
|
61
|
-
transact<X>(callback: (provider: DBProvider<I, T>) => Promise<X>): Promise<X>;
|
|
62
|
-
}
|
|
@@ -1,80 +0,0 @@
|
|
|
1
|
-
import { ThroughDBProvider } from "./ThroughDBProvider.js";
|
|
2
|
-
/**
|
|
3
|
-
* Database provider that records every write it performs to its `changes` log.
|
|
4
|
-
*
|
|
5
|
-
* - Wraps a `source` provider, delegates each write, then appends a `DBChange` entry describing what happened.
|
|
6
|
-
* - Every change is an explicit per-item write with an `id` — query writes are inherited two-step from `ThroughDBProvider`, so they arrive here as the individual item writes they resolved to.
|
|
7
|
-
* - Replay the log onto another provider with `ChangesDBProvider.replay()`.
|
|
8
|
-
* - Useful for building audit logging, change feeds, or assertions in tests; reads are passed straight through and not logged.
|
|
9
|
-
*
|
|
10
|
-
* @see https://shelving.cc/db/ChangesDBProvider
|
|
11
|
-
*/
|
|
12
|
-
export class ChangesDBProvider extends ThroughDBProvider {
|
|
13
|
-
/**
|
|
14
|
-
* The log of writes performed through this provider, in the order they happened.
|
|
15
|
-
*
|
|
16
|
-
* @see https://shelving.cc/db/ChangesDBProvider/changes
|
|
17
|
-
*/
|
|
18
|
-
get changes() {
|
|
19
|
-
return this._changes;
|
|
20
|
-
}
|
|
21
|
-
_changes = [];
|
|
22
|
-
/** Log an `"add"` change after writing. */
|
|
23
|
-
async addItem(collection, data) {
|
|
24
|
-
const id = await super.addItem(collection, data);
|
|
25
|
-
this._changes.push({ action: "add", collection, id, data });
|
|
26
|
-
return id;
|
|
27
|
-
}
|
|
28
|
-
/** Log a `"set"` change after writing. */
|
|
29
|
-
async setItem(collection, id, data) {
|
|
30
|
-
await super.setItem(collection, id, data);
|
|
31
|
-
this._changes.push({ action: "set", collection, id, data });
|
|
32
|
-
}
|
|
33
|
-
/** Log an `"update"` change after writing. */
|
|
34
|
-
async updateItem(collection, id, updates) {
|
|
35
|
-
await super.updateItem(collection, id, updates);
|
|
36
|
-
this._changes.push({ action: "update", collection, id, updates });
|
|
37
|
-
}
|
|
38
|
-
/** Log a `"delete"` change after writing. */
|
|
39
|
-
async deleteItem(collection, id) {
|
|
40
|
-
await super.deleteItem(collection, id);
|
|
41
|
-
this._changes.push({ action: "delete", collection, id });
|
|
42
|
-
}
|
|
43
|
-
/**
|
|
44
|
-
* Replay the `changes` log onto another provider, re-issuing each write in order.
|
|
45
|
-
*
|
|
46
|
-
* - Useful for audit replay or syncing a secondary store — and the commit mechanism for `MemoryDBProvider.transact()`.
|
|
47
|
-
* - `"add"` changes replay as `DBProvider.setItem()` with the logged id, so the target keeps the same generated ids.
|
|
48
|
-
* - The changes replay as a sequence of awaited writes — the replay itself is not atomic.
|
|
49
|
-
*
|
|
50
|
-
* @param provider Provider to replay the changes onto.
|
|
51
|
-
* @example await changes.replay(mirror);
|
|
52
|
-
* @see https://shelving.cc/db/ChangesDBProvider/replay
|
|
53
|
-
*/
|
|
54
|
-
async replay(provider) {
|
|
55
|
-
// `as` casts needed: the log stores `data` and `updates` loosely as `unknown`.
|
|
56
|
-
for (const { action, collection, id, data, updates } of this._changes) {
|
|
57
|
-
if (action === "delete")
|
|
58
|
-
await provider.deleteItem(collection, id);
|
|
59
|
-
else if (action === "update")
|
|
60
|
-
await provider.updateItem(collection, id, updates);
|
|
61
|
-
else
|
|
62
|
-
await provider.setItem(collection, id, data);
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
// Override so that transaction copies get their own log.
|
|
66
|
-
cloneWith(source) {
|
|
67
|
-
const clone = super.cloneWith(source);
|
|
68
|
-
Object.defineProperty(clone, "_changes", { value: [], enumerable: false });
|
|
69
|
-
return clone;
|
|
70
|
-
}
|
|
71
|
-
// Override to log the transaction's writes after it commits — a failed transaction logs nothing.
|
|
72
|
-
// The merge must happen after `source.transact()` resolves: backends may retry the callback or fail the commit itself, and only the committed attempt's writes belong in the log.
|
|
73
|
-
async transact(callback) {
|
|
74
|
-
let transaction;
|
|
75
|
-
const result = await this.source.transact(provider => callback((transaction = this.cloneWith(provider))));
|
|
76
|
-
if (transaction)
|
|
77
|
-
this._changes.push(...transaction.changes);
|
|
78
|
-
return result;
|
|
79
|
-
}
|
|
80
|
-
}
|