shelving 1.281.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.
|
@@ -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
|
}
|