shelving 1.276.0 → 1.277.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/api/cache/APICache.d.ts +2 -2
- package/api/cache/APICache.js +4 -4
- package/api/provider/CachedAPIProvider.d.ts +2 -2
- package/api/provider/CachedAPIProvider.js +4 -4
- package/db/provider/ChangesDBProvider.d.ts +20 -14
- package/db/provider/ChangesDBProvider.js +27 -18
- package/db/provider/DBProvider.d.ts +3 -0
- package/db/provider/DebugDBProvider.d.ts +3 -0
- package/db/provider/DebugDBProvider.js +6 -3
- package/db/provider/ThroughDBProvider.d.ts +9 -0
- package/db/provider/ThroughDBProvider.js +21 -8
- package/db/provider/ValidationDBProvider.d.ts +9 -2
- package/db/provider/ValidationDBProvider.js +12 -5
- package/firebase/FirestoreProvider.d.ts +1 -1
- package/firebase/FirestoreProvider.js +6 -2
- package/package.json +5 -5
package/api/cache/APICache.d.ts
CHANGED
|
@@ -74,7 +74,7 @@ export declare class APICache<P, R> implements AsyncDisposable {
|
|
|
74
74
|
* @example cache.refresh(getUser, { id: "abc" })
|
|
75
75
|
* @see https://shelving.cc/api/APICache/refresh
|
|
76
76
|
*/
|
|
77
|
-
refresh<PP extends P, RR extends R>(endpoint: Endpoint<PP, RR>, payload: PP, maxAge?: number): void
|
|
77
|
+
refresh<PP extends P, RR extends R>(endpoint: Endpoint<PP, RR>, payload: PP, maxAge?: number): Promise<void>;
|
|
78
78
|
/**
|
|
79
79
|
* Trigger a refetch on all stores for an endpoint.
|
|
80
80
|
*
|
|
@@ -83,6 +83,6 @@ export declare class APICache<P, R> implements AsyncDisposable {
|
|
|
83
83
|
* @example cache.refreshAll(getUser)
|
|
84
84
|
* @see https://shelving.cc/api/APICache/refreshAll
|
|
85
85
|
*/
|
|
86
|
-
refreshAll<PP extends P, RR extends R>(endpoint: Endpoint<PP, RR>, maxAge?: number): void
|
|
86
|
+
refreshAll<PP extends P, RR extends R>(endpoint: Endpoint<PP, RR>, maxAge?: number): Promise<void>;
|
|
87
87
|
[Symbol.asyncDispose](): Promise<void>;
|
|
88
88
|
}
|
package/api/cache/APICache.js
CHANGED
|
@@ -78,8 +78,8 @@ export class APICache {
|
|
|
78
78
|
* @example cache.refresh(getUser, { id: "abc" })
|
|
79
79
|
* @see https://shelving.cc/api/APICache/refresh
|
|
80
80
|
*/
|
|
81
|
-
refresh(endpoint, payload, maxAge) {
|
|
82
|
-
this._get(endpoint)?.refresh(payload, maxAge);
|
|
81
|
+
async refresh(endpoint, payload, maxAge) {
|
|
82
|
+
await this._get(endpoint)?.refresh(payload, maxAge);
|
|
83
83
|
}
|
|
84
84
|
/**
|
|
85
85
|
* Trigger a refetch on all stores for an endpoint.
|
|
@@ -89,8 +89,8 @@ export class APICache {
|
|
|
89
89
|
* @example cache.refreshAll(getUser)
|
|
90
90
|
* @see https://shelving.cc/api/APICache/refreshAll
|
|
91
91
|
*/
|
|
92
|
-
refreshAll(endpoint, maxAge) {
|
|
93
|
-
this._get(endpoint)?.refreshAll(maxAge);
|
|
92
|
+
async refreshAll(endpoint, maxAge) {
|
|
93
|
+
await this._get(endpoint)?.refreshAll(maxAge);
|
|
94
94
|
}
|
|
95
95
|
// Implement `AsyncDisposable`
|
|
96
96
|
[Symbol.asyncDispose]() {
|
|
@@ -43,13 +43,13 @@ export declare class CachedAPIProvider<P, R> extends ThroughAPIProvider<P, R> im
|
|
|
43
43
|
* @param payload The payload identifying the cached result.
|
|
44
44
|
* @see https://shelving.cc/api/CachedAPIProvider/refresh
|
|
45
45
|
*/
|
|
46
|
-
refresh<PP extends P, RR extends R>(endpoint: Endpoint<PP, RR>, payload: PP): void
|
|
46
|
+
refresh<PP extends P, RR extends R>(endpoint: Endpoint<PP, RR>, payload: PP): Promise<void>;
|
|
47
47
|
/**
|
|
48
48
|
* Refresh every cached result for an endpoint, across all payloads.
|
|
49
49
|
*
|
|
50
50
|
* @param endpoint The endpoint whose cached results should be refreshed.
|
|
51
51
|
* @see https://shelving.cc/api/CachedAPIProvider/refreshAll
|
|
52
52
|
*/
|
|
53
|
-
refreshAll<PP extends P, RR extends R>(endpoint: Endpoint<PP, RR>): void
|
|
53
|
+
refreshAll<PP extends P, RR extends R>(endpoint: Endpoint<PP, RR>): Promise<void>;
|
|
54
54
|
[Symbol.asyncDispose](): Promise<void>;
|
|
55
55
|
}
|
|
@@ -52,8 +52,8 @@ export class CachedAPIProvider extends ThroughAPIProvider {
|
|
|
52
52
|
* @param payload The payload identifying the cached result.
|
|
53
53
|
* @see https://shelving.cc/api/CachedAPIProvider/refresh
|
|
54
54
|
*/
|
|
55
|
-
refresh(endpoint, payload) {
|
|
56
|
-
this._cache.refresh(endpoint, payload, this.maxAge);
|
|
55
|
+
async refresh(endpoint, payload) {
|
|
56
|
+
await this._cache.refresh(endpoint, payload, this.maxAge);
|
|
57
57
|
}
|
|
58
58
|
/**
|
|
59
59
|
* Refresh every cached result for an endpoint, across all payloads.
|
|
@@ -61,8 +61,8 @@ export class CachedAPIProvider extends ThroughAPIProvider {
|
|
|
61
61
|
* @param endpoint The endpoint whose cached results should be refreshed.
|
|
62
62
|
* @see https://shelving.cc/api/CachedAPIProvider/refreshAll
|
|
63
63
|
*/
|
|
64
|
-
refreshAll(endpoint) {
|
|
65
|
-
this._cache.refreshAll(endpoint, this.maxAge);
|
|
64
|
+
async refreshAll(endpoint) {
|
|
65
|
+
await this._cache.refreshAll(endpoint, this.maxAge);
|
|
66
66
|
}
|
|
67
67
|
// Implement `AsyncDisposable`
|
|
68
68
|
async [Symbol.asyncDispose]() {
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import type { MutableArray } from "../../util/array.js";
|
|
2
2
|
import type { Data } from "../../util/data.js";
|
|
3
3
|
import type { Identifier, Item } from "../../util/item.js";
|
|
4
|
-
import type { Query } from "../../util/query.js";
|
|
5
4
|
import type { Updates } from "../../util/update.js";
|
|
6
5
|
import type { Collection } from "../collection/Collection.js";
|
|
7
6
|
import type { DBProvider } from "./DBProvider.js";
|
|
@@ -9,15 +8,14 @@ import { ThroughDBProvider } from "./ThroughDBProvider.js";
|
|
|
9
8
|
/**
|
|
10
9
|
* Structured log entry recording a single database write performed through a `ChangesDBProvider`.
|
|
11
10
|
*
|
|
12
|
-
* - `action` is the kind of write; `collection` is the
|
|
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.
|
|
13
12
|
*
|
|
14
13
|
* @see https://shelving.cc/db/DBChange
|
|
15
14
|
*/
|
|
16
|
-
export type DBChange<I extends Identifier> = {
|
|
15
|
+
export type DBChange<I extends Identifier, T extends Data = Data> = {
|
|
17
16
|
readonly action: "add" | "set" | "update" | "delete";
|
|
18
|
-
readonly collection: string
|
|
19
|
-
readonly id
|
|
20
|
-
readonly query?: unknown;
|
|
17
|
+
readonly collection: Collection<string, I, T>;
|
|
18
|
+
readonly id: I;
|
|
21
19
|
readonly data?: unknown;
|
|
22
20
|
readonly updates?: unknown;
|
|
23
21
|
};
|
|
@@ -25,6 +23,8 @@ export type DBChange<I extends Identifier> = {
|
|
|
25
23
|
* Database provider that records every write it performs to its `changes` log.
|
|
26
24
|
*
|
|
27
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
28
|
* - Useful for building audit logging, change feeds, or assertions in tests; reads are passed straight through and not logged.
|
|
29
29
|
*
|
|
30
30
|
* @see https://shelving.cc/db/ChangesDBProvider
|
|
@@ -35,8 +35,8 @@ export declare class ChangesDBProvider<I extends Identifier, T extends Data> ext
|
|
|
35
35
|
*
|
|
36
36
|
* @see https://shelving.cc/db/ChangesDBProvider/changes
|
|
37
37
|
*/
|
|
38
|
-
get changes(): ReadonlyArray<DBChange<I>>;
|
|
39
|
-
readonly _changes: MutableArray<DBChange<I>>;
|
|
38
|
+
get changes(): ReadonlyArray<DBChange<I, T>>;
|
|
39
|
+
readonly _changes: MutableArray<DBChange<I, T>>;
|
|
40
40
|
/** Log an `"add"` change after writing. */
|
|
41
41
|
addItem<II extends I, TT extends T>(collection: Collection<string, II, TT>, data: TT): Promise<II>;
|
|
42
42
|
/** Log a `"set"` change after writing. */
|
|
@@ -45,12 +45,18 @@ export declare class ChangesDBProvider<I extends Identifier, T extends Data> ext
|
|
|
45
45
|
updateItem<II extends I, TT extends T>(collection: Collection<string, II, TT>, id: II, updates: Updates<Item<II, TT>>): Promise<void>;
|
|
46
46
|
/** Log a `"delete"` change after writing. */
|
|
47
47
|
deleteItem<II extends I, TT extends T>(collection: Collection<string, II, TT>, id: II): Promise<void>;
|
|
48
|
-
/**
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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.
|
|
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>;
|
|
54
60
|
cloneWith(source: DBProvider<I, T>): this;
|
|
55
61
|
transact<X>(callback: (provider: DBProvider<I, T>) => Promise<X>): Promise<X>;
|
|
56
62
|
}
|
|
@@ -3,6 +3,8 @@ import { ThroughDBProvider } from "./ThroughDBProvider.js";
|
|
|
3
3
|
* Database provider that records every write it performs to its `changes` log.
|
|
4
4
|
*
|
|
5
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()`.
|
|
6
8
|
* - Useful for building audit logging, change feeds, or assertions in tests; reads are passed straight through and not logged.
|
|
7
9
|
*
|
|
8
10
|
* @see https://shelving.cc/db/ChangesDBProvider
|
|
@@ -20,38 +22,45 @@ export class ChangesDBProvider extends ThroughDBProvider {
|
|
|
20
22
|
/** Log an `"add"` change after writing. */
|
|
21
23
|
async addItem(collection, data) {
|
|
22
24
|
const id = await super.addItem(collection, data);
|
|
23
|
-
this._changes.push({ action: "add", collection
|
|
25
|
+
this._changes.push({ action: "add", collection, id, data });
|
|
24
26
|
return id;
|
|
25
27
|
}
|
|
26
28
|
/** Log a `"set"` change after writing. */
|
|
27
29
|
async setItem(collection, id, data) {
|
|
28
30
|
await super.setItem(collection, id, data);
|
|
29
|
-
this._changes.push({ action: "set", collection
|
|
31
|
+
this._changes.push({ action: "set", collection, id, data });
|
|
30
32
|
}
|
|
31
33
|
/** Log an `"update"` change after writing. */
|
|
32
34
|
async updateItem(collection, id, updates) {
|
|
33
35
|
await super.updateItem(collection, id, updates);
|
|
34
|
-
this._changes.push({ action: "update", collection
|
|
36
|
+
this._changes.push({ action: "update", collection, id, updates });
|
|
35
37
|
}
|
|
36
38
|
/** Log a `"delete"` change after writing. */
|
|
37
39
|
async deleteItem(collection, id) {
|
|
38
40
|
await super.deleteItem(collection, id);
|
|
39
|
-
this._changes.push({ action: "delete", collection
|
|
41
|
+
this._changes.push({ action: "delete", collection, id });
|
|
40
42
|
}
|
|
41
|
-
/**
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
async
|
|
53
|
-
|
|
54
|
-
|
|
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.
|
|
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
|
+
}
|
|
55
64
|
}
|
|
56
65
|
// Override so that transaction copies get their own log.
|
|
57
66
|
cloneWith(source) {
|
|
@@ -119,6 +119,7 @@ export declare abstract class DBProvider<I extends Identifier = Identifier, T ex
|
|
|
119
119
|
abstract getQuerySequence<II extends I, TT extends T>(collection: Collection<string, II, TT>, query?: Query<Item<II, TT>>): ItemsSequence<II, TT>;
|
|
120
120
|
/**
|
|
121
121
|
* Set (overwrite) the data for every item matching a query.
|
|
122
|
+
* - Not guaranteed atomic: an implementation may resolve the matching items first and then write per item (two-step — see `ThroughDBProvider`), so wrap the call in `transact()` when atomicity matters.
|
|
122
123
|
*
|
|
123
124
|
* @param collection Collection to write to.
|
|
124
125
|
* @param query Query selecting the items to set.
|
|
@@ -129,6 +130,7 @@ export declare abstract class DBProvider<I extends Identifier = Identifier, T ex
|
|
|
129
130
|
abstract setQuery<II extends I, TT extends T>(collection: Collection<string, II, TT>, query: Query<Item<II, TT>>, data: TT): Promise<void>;
|
|
130
131
|
/**
|
|
131
132
|
* Apply partial updates to every item matching a query.
|
|
133
|
+
* - Not guaranteed atomic: an implementation may resolve the matching items first and then write per item (two-step — see `ThroughDBProvider`), so wrap the call in `transact()` when atomicity matters.
|
|
132
134
|
*
|
|
133
135
|
* @param collection Collection to write to.
|
|
134
136
|
* @param query Query selecting the items to update.
|
|
@@ -139,6 +141,7 @@ export declare abstract class DBProvider<I extends Identifier = Identifier, T ex
|
|
|
139
141
|
abstract updateQuery<II extends I, TT extends T>(collection: Collection<string, II, TT>, query: Query<Item<II, TT>>, updates: Updates<TT>): Promise<void>;
|
|
140
142
|
/**
|
|
141
143
|
* Delete every item matching a query.
|
|
144
|
+
* - Not guaranteed atomic: an implementation may resolve the matching items first and then delete per item (two-step — see `ThroughDBProvider`), so wrap the call in `transact()` when atomicity matters.
|
|
142
145
|
*
|
|
143
146
|
* @param collection Collection to delete from.
|
|
144
147
|
* @param query Query selecting the items to delete.
|
|
@@ -27,8 +27,11 @@ export declare class DebugDBProvider<I extends Identifier, T extends Data> exten
|
|
|
27
27
|
countQuery<II extends I, TT extends T>(collection: Collection<string, II, TT>, query?: Query<Item<II, TT>>): Promise<number>;
|
|
28
28
|
getQuery<II extends I, TT extends T>(collection: Collection<string, II, TT>, query?: Query<Item<II, TT>>): Promise<Items<II, TT>>;
|
|
29
29
|
getQuerySequence<II extends I, TT extends T>(collection: Collection<string, II, TT>, query?: Query<Item<II, TT>>): ItemsSequence<II, TT>;
|
|
30
|
+
/** Passthrough, not two-step: log the query write the caller made, not the per-item writes it implies. */
|
|
30
31
|
setQuery<II extends I, TT extends T>(collection: Collection<string, II, TT>, query: Query<Item<II, TT>>, data: TT): Promise<void>;
|
|
32
|
+
/** Passthrough, not two-step: log the query write the caller made, not the per-item writes it implies. */
|
|
31
33
|
updateQuery<II extends I, TT extends T>(collection: Collection<string, II, TT>, query: Query<Item<II, TT>>, updates: Updates<TT>): Promise<void>;
|
|
34
|
+
/** Passthrough, not two-step: log the query write the caller made, not the per-item writes it implies. */
|
|
32
35
|
deleteQuery<II extends I, TT extends T>(collection: Collection<string, II, TT>, query: Query<Item<II, TT>>): Promise<void>;
|
|
33
36
|
/** Adds TRANSACT logging around the base behaviour, which re-wraps so operations inside the transaction are logged too. */
|
|
34
37
|
transact<X>(callback: (provider: DBProvider<I, T>) => Promise<X>): Promise<X>;
|
|
@@ -120,10 +120,11 @@ export class DebugDBProvider extends ThroughDBProvider {
|
|
|
120
120
|
console.error(`${ANSI_FAILURE} SEQUENCE QUERY`, collection.name, query, thrown);
|
|
121
121
|
}
|
|
122
122
|
}
|
|
123
|
+
/** Passthrough, not two-step: log the query write the caller made, not the per-item writes it implies. */
|
|
123
124
|
async setQuery(collection, query, data) {
|
|
124
125
|
try {
|
|
125
126
|
console.debug(`${ANSI_RIGHT} SET QUERY`, collection.name, query, data);
|
|
126
|
-
await
|
|
127
|
+
await this.source.setQuery(collection, query, data);
|
|
127
128
|
console.debug(`${ANSI_SUCCESS} SET QUERY`, collection.name, query, data);
|
|
128
129
|
}
|
|
129
130
|
catch (reason) {
|
|
@@ -131,10 +132,11 @@ export class DebugDBProvider extends ThroughDBProvider {
|
|
|
131
132
|
throw reason;
|
|
132
133
|
}
|
|
133
134
|
}
|
|
135
|
+
/** Passthrough, not two-step: log the query write the caller made, not the per-item writes it implies. */
|
|
134
136
|
async updateQuery(collection, query, updates) {
|
|
135
137
|
try {
|
|
136
138
|
console.debug(`${ANSI_RIGHT} UPDATE QUERY`, collection.name, query, updates);
|
|
137
|
-
await
|
|
139
|
+
await this.source.updateQuery(collection, query, updates);
|
|
138
140
|
console.debug(`${ANSI_SUCCESS} UPDATE QUERY`, collection.name, query, updates);
|
|
139
141
|
}
|
|
140
142
|
catch (reason) {
|
|
@@ -142,10 +144,11 @@ export class DebugDBProvider extends ThroughDBProvider {
|
|
|
142
144
|
throw reason;
|
|
143
145
|
}
|
|
144
146
|
}
|
|
147
|
+
/** Passthrough, not two-step: log the query write the caller made, not the per-item writes it implies. */
|
|
145
148
|
async deleteQuery(collection, query) {
|
|
146
149
|
try {
|
|
147
150
|
console.debug(`${ANSI_RIGHT} DELETE QUERY`, collection.name, query);
|
|
148
|
-
await
|
|
151
|
+
await this.source.deleteQuery(collection, query);
|
|
149
152
|
console.debug(`${ANSI_SUCCESS} DELETE QUERY`, collection.name, query);
|
|
150
153
|
}
|
|
151
154
|
catch (reason) {
|
|
@@ -9,6 +9,7 @@ import type { DBProvider } from "./DBProvider.js";
|
|
|
9
9
|
* Database provider that passes every operation straight through to a wrapped `source` provider.
|
|
10
10
|
*
|
|
11
11
|
* - Base for the layered `Through*Provider` family (validation, caching, logging, change tracking); subclasses override individual methods to add behaviour and call `super` to delegate.
|
|
12
|
+
* - Query writes (`setQuery()`, `updateQuery()`, `deleteQuery()`) are two-step by default — resolved to their matching items with `getQuery()`, then written per item through this provider's own item methods — so wrapper behaviour applies to every implied write. Subclasses that don't need per-item behaviour override them to pass through to `source` directly.
|
|
12
13
|
* - Exposes `source` and implements `Sourceable`, so wrapped providers can be discovered with `getSource()` / `requireSource()`.
|
|
13
14
|
*
|
|
14
15
|
* @see https://shelving.cc/db/ThroughDBProvider
|
|
@@ -31,8 +32,16 @@ export declare class ThroughDBProvider<I extends Identifier, T extends Data> imp
|
|
|
31
32
|
countQuery<II extends I, TT extends T>(collection: Collection<string, II, TT>, query?: Query<Item<II, TT>>): Promise<number>;
|
|
32
33
|
getQuery<II extends I, TT extends T>(collection: Collection<string, II, TT>, query?: Query<Item<II, TT>>): Promise<Items<II, TT>>;
|
|
33
34
|
getQuerySequence<II extends I, TT extends T>(collection: Collection<string, II, TT>, query?: Query<Item<II, TT>>): ItemsSequence<II, TT>;
|
|
35
|
+
/**
|
|
36
|
+
* Two-step: resolve the query to its matching items with `getQuery()`, then set each one with `setItem()`.
|
|
37
|
+
* - Routes every implied write through this provider's own item methods, so wrapper behaviour applies to each item — the same theory as `transact()` re-wrapping the transaction provider.
|
|
38
|
+
* - The per-item writes run concurrently (`awaitValues()`), so a batch over a remote source costs one round-trip of latency, not one per item.
|
|
39
|
+
* - The resolve and the writes are separate steps, so this is only atomic inside `transact()`.
|
|
40
|
+
*/
|
|
34
41
|
setQuery<II extends I, TT extends T>(collection: Collection<string, II, TT>, query: Query<Item<II, TT>>, data: TT): Promise<void>;
|
|
42
|
+
/** Two-step: resolve the query to its matching items with `getQuery()`, then update each one concurrently with `updateItem()` — see `ThroughDBProvider.setQuery()`. */
|
|
35
43
|
updateQuery<II extends I, TT extends T>(collection: Collection<string, II, TT>, query: Query<Item<II, TT>>, updates: Updates<TT>): Promise<void>;
|
|
44
|
+
/** Two-step: resolve the query to its matching items with `getQuery()`, then delete each one concurrently with `deleteItem()` — see `ThroughDBProvider.setQuery()`. */
|
|
36
45
|
deleteQuery<II extends I, TT extends T>(collection: Collection<string, II, TT>, query: Query<Item<II, TT>>): Promise<void>;
|
|
37
46
|
getFirst<II extends I, TT extends T>(collection: Collection<string, II, TT>, query: Query<Item<II, TT>>): Promise<OptionalItem<II, TT>>;
|
|
38
47
|
requireFirst<II extends I, TT extends T>(collection: Collection<string, II, TT>, query: Query<Item<II, TT>>): Promise<Item<II, TT>>;
|
|
@@ -1,8 +1,10 @@
|
|
|
1
|
+
import { awaitValues } from "../../util/async.js";
|
|
1
2
|
import { awaitDispose } from "../../util/dispose.js";
|
|
2
3
|
/**
|
|
3
4
|
* Database provider that passes every operation straight through to a wrapped `source` provider.
|
|
4
5
|
*
|
|
5
6
|
* - Base for the layered `Through*Provider` family (validation, caching, logging, change tracking); subclasses override individual methods to add behaviour and call `super` to delegate.
|
|
7
|
+
* - Query writes (`setQuery()`, `updateQuery()`, `deleteQuery()`) are two-step by default — resolved to their matching items with `getQuery()`, then written per item through this provider's own item methods — so wrapper behaviour applies to every implied write. Subclasses that don't need per-item behaviour override them to pass through to `source` directly.
|
|
6
8
|
* - Exposes `source` and implements `Sourceable`, so wrapped providers can be discovered with `getSource()` / `requireSource()`.
|
|
7
9
|
*
|
|
8
10
|
* @see https://shelving.cc/db/ThroughDBProvider
|
|
@@ -47,14 +49,25 @@ export class ThroughDBProvider {
|
|
|
47
49
|
getQuerySequence(collection, query) {
|
|
48
50
|
return this.source.getQuerySequence(collection, query);
|
|
49
51
|
}
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
52
|
+
/**
|
|
53
|
+
* Two-step: resolve the query to its matching items with `getQuery()`, then set each one with `setItem()`.
|
|
54
|
+
* - Routes every implied write through this provider's own item methods, so wrapper behaviour applies to each item — the same theory as `transact()` re-wrapping the transaction provider.
|
|
55
|
+
* - The per-item writes run concurrently (`awaitValues()`), so a batch over a remote source costs one round-trip of latency, not one per item.
|
|
56
|
+
* - The resolve and the writes are separate steps, so this is only atomic inside `transact()`.
|
|
57
|
+
*/
|
|
58
|
+
async setQuery(collection, query, data) {
|
|
59
|
+
const items = await this.getQuery(collection, query);
|
|
60
|
+
await awaitValues(...items.map(({ id }) => this.setItem(collection, id, data)));
|
|
61
|
+
}
|
|
62
|
+
/** Two-step: resolve the query to its matching items with `getQuery()`, then update each one concurrently with `updateItem()` — see `ThroughDBProvider.setQuery()`. */
|
|
63
|
+
async updateQuery(collection, query, updates) {
|
|
64
|
+
const items = await this.getQuery(collection, query);
|
|
65
|
+
await awaitValues(...items.map(({ id }) => this.updateItem(collection, id, updates)));
|
|
66
|
+
}
|
|
67
|
+
/** Two-step: resolve the query to its matching items with `getQuery()`, then delete each one concurrently with `deleteItem()` — see `ThroughDBProvider.setQuery()`. */
|
|
68
|
+
async deleteQuery(collection, query) {
|
|
69
|
+
const items = await this.getQuery(collection, query);
|
|
70
|
+
await awaitValues(...items.map(({ id }) => this.deleteItem(collection, id)));
|
|
58
71
|
}
|
|
59
72
|
getFirst(collection, query) {
|
|
60
73
|
return this.source.getFirst(collection, query);
|
|
@@ -30,9 +30,16 @@ export declare class ValidationDBProvider<I extends Identifier, T extends Data>
|
|
|
30
30
|
getQuery<II extends I, TT extends T>(collection: Collection<string, II, TT>, query?: Query<Item<II, TT>>): Promise<Items<II, TT>>;
|
|
31
31
|
/** Validate each emitted result; throws `ValueError` if any items fail the collection schema. */
|
|
32
32
|
getQuerySequence<II extends I, TT extends T>(collection: Collection<string, II, TT>, query?: Query<Item<II, TT>>): ItemsSequence<II, TT>;
|
|
33
|
-
/**
|
|
33
|
+
/**
|
|
34
|
+
* Validate the data before writing; throws `ValueError` if it fails the collection schema.
|
|
35
|
+
* - Passthrough, not two-step: validation applies to the query write's own inputs up front, so per-item routing adds nothing — pass through to keep the source's native query write.
|
|
36
|
+
*/
|
|
34
37
|
setQuery<II extends I, TT extends T>(collection: Collection<string, II, TT>, query: Query<Item<II, TT>>, data: TT): Promise<void>;
|
|
35
|
-
/**
|
|
38
|
+
/**
|
|
39
|
+
* Validate the updates before writing; throws `ValueError` if they fail the collection schema.
|
|
40
|
+
* - Passthrough, not two-step: validation applies to the query write's own inputs up front, so per-item routing adds nothing — pass through to keep the source's native query write.
|
|
41
|
+
*/
|
|
36
42
|
updateQuery<II extends I, TT extends T>(collection: Collection<string, II, TT>, query: Query<Item<II, TT>>, updates: Updates<TT>): Promise<void>;
|
|
43
|
+
/** Passthrough, not two-step: there are no inputs to validate per item, so keep the source's native query write. */
|
|
37
44
|
deleteQuery<II extends I, TT extends T>(collection: Collection<string, II, TT>, query: Query<Item<II, TT>>): Promise<void>;
|
|
38
45
|
}
|
|
@@ -46,16 +46,23 @@ export class ValidationDBProvider extends ThroughDBProvider {
|
|
|
46
46
|
for await (const items of super.getQuerySequence(collection, query))
|
|
47
47
|
yield _validateItems(collection, items, this.getQuerySequence);
|
|
48
48
|
}
|
|
49
|
-
/**
|
|
49
|
+
/**
|
|
50
|
+
* Validate the data before writing; throws `ValueError` if it fails the collection schema.
|
|
51
|
+
* - Passthrough, not two-step: validation applies to the query write's own inputs up front, so per-item routing adds nothing — pass through to keep the source's native query write.
|
|
52
|
+
*/
|
|
50
53
|
setQuery(collection, query, data) {
|
|
51
|
-
return
|
|
54
|
+
return this.source.setQuery(collection, query, collection.validate(data));
|
|
52
55
|
}
|
|
53
|
-
/**
|
|
56
|
+
/**
|
|
57
|
+
* Validate the updates before writing; throws `ValueError` if they fail the collection schema.
|
|
58
|
+
* - Passthrough, not two-step: validation applies to the query write's own inputs up front, so per-item routing adds nothing — pass through to keep the source's native query write.
|
|
59
|
+
*/
|
|
54
60
|
updateQuery(collection, query, updates) {
|
|
55
|
-
return
|
|
61
|
+
return this.source.updateQuery(collection, query, _validateUpdates(collection, updates, this.updateQuery));
|
|
56
62
|
}
|
|
63
|
+
/** Passthrough, not two-step: there are no inputs to validate per item, so keep the source's native query write. */
|
|
57
64
|
deleteQuery(collection, query) {
|
|
58
|
-
return
|
|
65
|
+
return this.source.deleteQuery(collection, query);
|
|
59
66
|
}
|
|
60
67
|
}
|
|
61
68
|
/**
|
|
@@ -108,7 +108,7 @@ export declare class FirestoreProvider<I extends string = string, T extends Data
|
|
|
108
108
|
deleteQuery<II extends I, TT extends T>(collection: Collection<string, II, TT>, query: Query<Item<II, TT>>): Promise<void>;
|
|
109
109
|
/**
|
|
110
110
|
* Runs the callback in a Firestore transaction: begin → reads with the transaction id → buffered writes committed atomically.
|
|
111
|
-
* - Retries the whole callback (up to 5 attempts) when the commit is aborted by contention, so the callback must have no side effects other than through its provider.
|
|
111
|
+
* - Retries the whole callback (up to 5 attempts, with jittered exponential backoff) when the commit is aborted by contention, so the callback must have no side effects other than through its provider.
|
|
112
112
|
* - Rolls back and rethrows if the callback throws.
|
|
113
113
|
* - Reads see a consistent snapshot and never the transaction's own buffered writes.
|
|
114
114
|
*/
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import { DBProvider } from "../db/provider/DBProvider.js";
|
|
2
2
|
import { ResponseError } from "../error/ResponseError.js";
|
|
3
3
|
import { UnsupportedError } from "../error/UnsupportedError.js";
|
|
4
|
+
import { getDelay } from "../util/async.js";
|
|
4
5
|
import { joinDataPath } from "../util/data.js";
|
|
5
6
|
import { BLACKHOLE } from "../util/function.js";
|
|
6
7
|
import { getItem } from "../util/item.js";
|
|
7
8
|
import { isPlainObject } from "../util/object.js";
|
|
8
9
|
import { getQueryFilters, getQueryLimit, getQueryOrders } from "../util/query.js";
|
|
9
|
-
import { getRandomKey } from "../util/random.js";
|
|
10
|
+
import { getRandom, getRandomKey } from "../util/random.js";
|
|
10
11
|
import { getUpdates } from "../util/update.js";
|
|
11
12
|
import { toData, toFirestoreFields, toFirestoreValue } from "./value.js";
|
|
12
13
|
// Constants.
|
|
@@ -260,7 +261,7 @@ export class FirestoreProvider extends DBProvider {
|
|
|
260
261
|
}
|
|
261
262
|
/**
|
|
262
263
|
* Runs the callback in a Firestore transaction: begin → reads with the transaction id → buffered writes committed atomically.
|
|
263
|
-
* - Retries the whole callback (up to 5 attempts) when the commit is aborted by contention, so the callback must have no side effects other than through its provider.
|
|
264
|
+
* - Retries the whole callback (up to 5 attempts, with jittered exponential backoff) when the commit is aborted by contention, so the callback must have no side effects other than through its provider.
|
|
264
265
|
* - Rolls back and rethrows if the callback throws.
|
|
265
266
|
* - Reads see a consistent snapshot and never the transaction's own buffered writes.
|
|
266
267
|
*/
|
|
@@ -268,6 +269,9 @@ export class FirestoreProvider extends DBProvider {
|
|
|
268
269
|
let retryTransaction;
|
|
269
270
|
let aborted;
|
|
270
271
|
for (let attempt = 0; attempt < TRANSACTION_ATTEMPTS; attempt++) {
|
|
272
|
+
// Back off with jitter before each retry so contending transactions de-synchronise instead of re-aborting each other in lockstep.
|
|
273
|
+
if (attempt)
|
|
274
|
+
await getDelay(getRandom(0, 100 * 2 ** attempt));
|
|
271
275
|
const { transaction } = (await this._request("beginTransaction", {
|
|
272
276
|
options: { readWrite: retryTransaction ? { retryTransaction } : {} },
|
|
273
277
|
}));
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "shelving",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.277.0",
|
|
4
4
|
"author": "Dave Houlbrooke <dave@shax.com>",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -9,14 +9,14 @@
|
|
|
9
9
|
"main": "./index.js",
|
|
10
10
|
"module": "./index.js",
|
|
11
11
|
"devDependencies": {
|
|
12
|
-
"@biomejs/biome": "^2.5.
|
|
12
|
+
"@biomejs/biome": "^2.5.7",
|
|
13
13
|
"@heroicons/react": "^2.2.0",
|
|
14
14
|
"@types/bun": "^1.3.14",
|
|
15
|
-
"@types/react": "^19.2.
|
|
16
|
-
"@types/react-dom": "^19.2.
|
|
15
|
+
"@types/react": "^19.2.18",
|
|
16
|
+
"@types/react-dom": "^19.2.4",
|
|
17
17
|
"react": "^19.3.0-canary-fef12a01-20260413",
|
|
18
18
|
"react-dom": "^19.3.0-canary-fef12a01-20260413",
|
|
19
|
-
"stylelint": "^17.14.
|
|
19
|
+
"stylelint": "^17.14.1",
|
|
20
20
|
"stylelint-config-standard": "^40.0.0",
|
|
21
21
|
"typescript": "^7.0.2"
|
|
22
22
|
},
|