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 DBProvider<I, T> implements Sourceable<DBProvider<I, T>> {
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
- /** Mirror the updates into the cache. */
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
- /** Remove the deleted item from the cache. */
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
- /** Mirror the change into the cache. */
53
- setQuery<II extends I, TT extends T>(collection: Collection<string, II, TT>, query: Query<Item<II, TT>>, data: TT): Promise<void>;
54
- /** Mirror the updates into the cache. */
55
- updateQuery<II extends I, TT extends T>(collection: Collection<string, II, TT>, query: Query<Item<II, TT>>, updates: Updates<TT>): Promise<void>;
56
- /** Remove the deleted items from the cache. */
57
- deleteQuery<II extends I, TT extends T>(collection: Collection<string, II, TT>, query: Query<Item<II, TT>>): Promise<void>;
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 DBProvider {
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 this.source.getItem(collection, id);
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, this.source.getItemSequence(collection, 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 this.source.addItem(collection, data);
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 this.source.setItem(collection, id, data);
49
+ await super.setItem(collection, id, data);
54
50
  this.memory.getTable(collection).setItem(id, data);
55
51
  }
56
- /** Mirror the updates into the cache. */
52
+ /** Fetch the item first (caching it), then update it only if it exists. */
57
53
  async updateItem(collection, id, updates) {
58
- await this.source.updateItem(collection, id, updates);
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
- /** Remove the deleted item from the cache. */
60
+ /** Fetch the item first, then delete it only if it exists. */
62
61
  async deleteItem(collection, id) {
63
- await this.source.deleteItem(collection, id);
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 this.source.getQuery(collection, query);
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(this.source.getQuerySequence(collection, query));
76
+ return this.memory.getTable(collection).setItemsSequence(super.getQuerySequence(collection, query));
78
77
  }
79
- /** Mirror the change into the cache. */
80
- async setQuery(collection, query, data) {
81
- await this.source.setQuery(collection, query, data);
82
- this.memory.getTable(collection).setQuery(query, data);
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
- /** Mirror the updates into the cache. */
85
- async updateQuery(collection, query, updates) {
86
- await this.source.updateQuery(collection, query, updates);
87
- this.memory.getTable(collection).updateQuery(query, updates);
88
- }
89
- /** Remove the deleted items from the cache. */
90
- async deleteQuery(collection, query) {
91
- await this.source.deleteQuery(collection, query);
92
- this.memory.getTable(collection).deleteQuery(query);
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.source, // Dispose the source API provider.
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
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "shelving",
3
- "version": "1.281.0",
3
+ "version": "1.282.0",
4
4
  "author": "Dave Houlbrooke <dave@shax.com>",
5
5
  "repository": {
6
6
  "type": "git",