shelving 1.179.0 → 1.179.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -3,8 +3,13 @@ import { type PossibleURL } from "../../util/url.js";
3
3
  import type { Endpoint } from "./Endpoint.js";
4
4
  /**
5
5
  * A function that handles an endpoint request, with a payload and returns a result.
6
- * - `payload` has already been validated against the endpoint payload schema before the callback is invoked.
7
- * - `request` is always the original incoming request object.
6
+ *
7
+ * @param payload The payload for the callback combining the `{placeholders}`, `?search` params, and body content (this has been validated against the Endpoint's payload schema).
8
+ * @param request The original incoming request object.
9
+ * @param args Any additional arguments to pass into the endpoint callback
10
+ *
11
+ * @returns {Response} Returning a `Response` object (this will pass back to the client without validation).
12
+ * @returns {R} Returning the return type of the handler (this will be validated against the Endpoint's result schema).
8
13
  */
9
14
  export type EndpointCallback<P, R, A extends Arguments = []> = (payload: P, request: Request, ...args: A) => R | Response | Promise<R | Response>;
10
15
  /** A typed endpoint definition paired with its implementation callback. */
@@ -12,6 +17,7 @@ export interface EndpointHandler<P = unknown, R = unknown, A extends Arguments =
12
17
  readonly endpoint: Endpoint<P, R>;
13
18
  readonly callback: EndpointCallback<P, R, A>;
14
19
  }
20
+ /** Any endpoint handler. */
15
21
  export type AnyEndpointHandler<A extends Arguments = []> = EndpointHandler<any, any, A>;
16
22
  /** A collection of endpoint handlers that can be matched and invoked by `handleEndpoints()`. */
17
23
  export type EndpointHandlers<A extends Arguments = []> = Iterable<AnyEndpointHandler<A>>;
@@ -19,6 +25,10 @@ export type EndpointHandlers<A extends Arguments = []> = Iterable<AnyEndpointHan
19
25
  * Handle a `Request` with the first matching endpoint handler after stripping any base-path prefix from the request pathname.
20
26
  * - The original `Request` object is passed through to the callback unchanged.
21
27
  * - Path params and query params are merged before payload validation.
22
- * @param base The base URL for the API, e.g. `https://myapi.com/`
28
+ *
29
+ * @param request The input request to handle.
30
+ *
31
+ * @param base The base URL for the API, e.g. `https://myapi.com/a/b`
32
+ * - `pathname` of this URL gets trimmed from `request.path` to form the target path when matching against endpoints, e.g. `/a/b/c/d` will produce `/c/d` for matching.
23
33
  */
24
34
  export declare function handleEndpoints<A extends Arguments = []>(request: Request, base: PossibleURL, handlers: EndpointHandlers<A>, ...args: A): Promise<Response>;
@@ -8,7 +8,11 @@ import { requireURL } from "../../util/url.js";
8
8
  * Handle a `Request` with the first matching endpoint handler after stripping any base-path prefix from the request pathname.
9
9
  * - The original `Request` object is passed through to the callback unchanged.
10
10
  * - Path params and query params are merged before payload validation.
11
- * @param base The base URL for the API, e.g. `https://myapi.com/`
11
+ *
12
+ * @param request The input request to handle.
13
+ *
14
+ * @param base The base URL for the API, e.g. `https://myapi.com/a/b`
15
+ * - `pathname` of this URL gets trimmed from `request.path` to form the target path when matching against endpoints, e.g. `/a/b/c/d` will produce `/c/d` for matching.
12
16
  */
13
17
  export function handleEndpoints(request, base, handlers, ...args) {
14
18
  const caller = handleEndpoints;
@@ -18,27 +22,31 @@ export function handleEndpoints(request, base, handlers, ...args) {
18
22
  const { origin: baseOrigin, pathname: basePath } = requireURL(base, undefined, caller);
19
23
  const { origin: requestOrigin, pathname: requestPath, searchParams } = requireURL(url, base, caller);
20
24
  if (baseOrigin !== requestOrigin)
21
- throw new NotFoundError("No matching origin", { expected: baseOrigin, received: requestOrigin, caller });
22
- const targetPath = _stripPathPrefix(requestPath, basePath, caller);
25
+ throw new NotFoundError("No matching base origin", { expected: baseOrigin, received: requestOrigin, caller });
26
+ const targetPath = _stripPathPrefix(requestPath, basePath);
27
+ if (!targetPath)
28
+ throw new NotFoundError("No matching base path", { received: requestPath, expected: basePath, caller });
23
29
  for (const handler of handlers) {
24
30
  const pathParams = handler.endpoint.match(method, targetPath, caller);
25
31
  if (!pathParams)
26
32
  continue;
27
33
  const params = searchParams.size ? { ...getDictionary(searchParams), ...pathParams } : pathParams;
28
- return handleEndpoint(handler, params, request, args, handleEndpoints);
34
+ return _handleEndpoint(handler, params, request, args, handleEndpoints);
29
35
  }
30
36
  throw new NotFoundError("No matching endpoint", { received: targetPath, caller });
31
37
  }
32
38
  /**
33
39
  * Validate and invoke an endpoint callback after the routing layer has already matched URL params.
34
40
  */
35
- async function handleEndpoint({ endpoint, callback },
41
+ async function _handleEndpoint({ endpoint, callback },
36
42
  /** Params we already matched/parsed from the URL. */
37
- params, request, args, caller = handleEndpoint) {
43
+ params, request, args, caller = _handleEndpoint) {
38
44
  const content = await getRequestContent(request, caller);
39
45
  const unsafePayload = content === undefined ? params : isPlainObject(content) ? { ...content, ...params } : content;
40
46
  const payload = endpoint.payload.validate(unsafePayload);
41
47
  const unsafeResult = await callback(payload, request, ...args);
48
+ if (unsafeResult instanceof Response)
49
+ return unsafeResult;
42
50
  try {
43
51
  return getResponse(endpoint.result.validate(unsafeResult));
44
52
  }
@@ -49,7 +57,7 @@ params, request, args, caller = handleEndpoint) {
49
57
  }
50
58
  }
51
59
  /** Strip a prefix like `/a/b` from a path like `/a/b/c/d` to produce a remainder path like `/c/d`. */
52
- function _stripPathPrefix(path, prefix, caller) {
60
+ function _stripPathPrefix(path, prefix) {
53
61
  prefix = prefix === "/" ? "/" : prefix.replace(/\/$/, "");
54
62
  if (prefix === "/")
55
63
  return path;
@@ -57,5 +65,4 @@ function _stripPathPrefix(path, prefix, caller) {
57
65
  return "/";
58
66
  if (path.startsWith(`${prefix}/`))
59
67
  return path.slice(prefix.length);
60
- throw new NotFoundError("No matching endpoint", { received: path, expected: prefix, caller });
61
68
  }
@@ -12,19 +12,14 @@ export type MockAPICall = {
12
12
  readonly response: Response;
13
13
  readonly result: unknown;
14
14
  };
15
- /**
16
- * Construction options for a `MockAPIProvider`.
17
- * - Accepts the normal `APIProvider` options.
18
- */
15
+ /** Construction options for a `MockAPIProvider`. */
19
16
  export interface MockAPIProviderOptions extends ClientAPIProviderOptions {
20
- /** Implement this handler to mock the the request/response input/output. */
21
- readonly handler: RequestHandler;
22
17
  }
23
18
  /** Provider that logs API calls without sending network requests. */
24
19
  export declare class MockAPIProvider extends ClientAPIProvider {
25
20
  readonly calls: MockAPICall[];
26
21
  readonly handler: RequestHandler;
27
- constructor({ handler, ...options }: MockAPIProviderOptions);
22
+ constructor(handler: RequestHandler, options?: MockAPIProviderOptions);
28
23
  /**
29
24
  * Log a `fetch()` call without using the network.
30
25
  * - If `getResult` is configured, its return value is returned as-is (no schema validation).
@@ -4,7 +4,7 @@ import { ClientAPIProvider } from "./ClientAPIProvider.js";
4
4
  export class MockAPIProvider extends ClientAPIProvider {
5
5
  calls = [];
6
6
  handler;
7
- constructor({ handler, ...options }) {
7
+ constructor(handler, options = { url: "https://api.mock.com" }) {
8
8
  super(options);
9
9
  this.handler = handler;
10
10
  }
@@ -11,17 +11,6 @@ export interface KVNamespace {
11
11
  }): Promise<unknown>;
12
12
  put(key: string, value: string): Promise<void>;
13
13
  delete(key: string): Promise<void>;
14
- list(options?: {
15
- prefix?: string;
16
- limit?: number;
17
- cursor?: string;
18
- }): Promise<{
19
- keys: readonly {
20
- name: string;
21
- }[];
22
- list_complete: boolean;
23
- cursor?: string;
24
- }>;
25
14
  }
26
15
  /**
27
16
  * Cloudflare Workers KV database provider.
@@ -31,26 +20,21 @@ export interface KVNamespace {
31
20
  *
32
21
  * ### Supported
33
22
  * - Single item operations: `getItem`, `setItem`, `addItem`, `updateItem`, `deleteItem`.
34
- * - Query operations: `getQuery`, `setQuery`, `updateQuery`, `deleteQuery`, `countQuery`.
35
- * - All filter operators: equality, not, in, out, contains, gt, gte, lt, lte.
36
- * - Sorting (`$order`) and limiting (`$limit`).
37
23
  * - ID generation: `addItem()` generates a UUID v4 identifier automatically.
38
24
  *
39
25
  * ### Not supported
40
26
  * - **Realtime subscriptions:** `getItemSequence()` and `getQuerySequence()` throw `UnimplementedError`.
41
27
  * KV has no change feed or push notification mechanism.
28
+ * - **Updates:** `updateItem()` and `updateQuery()` throw `UnimplementedError`.
29
+ * - **Collection queries:** `getQuery()`, `setQuery()`, `deleteQuery()`, and `countQuery()` are not supported.
30
+ * KV does not expose efficient filtering, sorting, or collection scans, so this provider avoids
31
+ * the old "read everything and filter in memory" behavior.
42
32
  *
43
33
  * ### Performance limitations
44
- * - **Querying is expensive:** KV has no native query support. Every `getQuery()` call lists all keys
45
- * in the collection (paginated, 1000 keys per page), fetches each value individually, then applies
46
- * filtering, sorting, and limiting in-memory. Avoid large collections where possible.
47
- * - **Non-atomic updates:** `updateItem()` performs a read-modify-write cycle. Concurrent writes to the
48
- * same item may cause one write to be lost.
49
- * - **Eventual consistency:** KV is eventually consistent — reads may return stale data, particularly
50
- * shortly after writes. This also affects `updateItem`, `setQuery`, `updateQuery`, and `deleteQuery`
51
- * since they read before writing.
52
- * - **No bulk get:** Each item value must be fetched individually — there is no multi-get API.
53
- * Query operations that match N items require N+1 KV requests (one `list` + N `get` calls per page).
34
+ * - **Single-key store only:** This provider is intentionally limited to direct key reads and writes.
35
+ * If you need collection queries, filtering, sorting, or bulk mutations, use a different backend.
36
+ * - **Eventual consistency:** KV is eventually consistent, so reads may briefly return stale values
37
+ * shortly after writes.
54
38
  */
55
39
  export declare class CloudflareKVProvider extends DBProvider<string> {
56
40
  private readonly _kv;
@@ -59,13 +43,11 @@ export declare class CloudflareKVProvider extends DBProvider<string> {
59
43
  getItemSequence<T extends Data>(_c: Collection<string, string, T>, _id: string): AsyncIterable<OptionalItem<string, T>>;
60
44
  addItem<T extends Data>({ name }: Collection<string, string, T>, data: T): Promise<string>;
61
45
  setItem<T extends Data>({ name }: Collection<string, string, T>, id: string, data: T): Promise<void>;
62
- updateItem<T extends Data>(c: Collection<string, string, T>, id: string, updates: Updates<T>): Promise<void>;
46
+ updateItem<T extends Data>(_c: Collection<string, string, T>, _id: string, _updates: Updates<T>): Promise<void>;
63
47
  deleteItem<T extends Data>({ name }: Collection<string, string, T>, id: string): Promise<void>;
64
- getQuery<T extends Data>({ name }: Collection<string, string, T>, q?: ItemQuery<string, T>): Promise<Items<string, T>>;
48
+ getQuery<T extends Data>(_c: Collection<string, string, T>, _q?: ItemQuery<string, T>): Promise<Items<string, T>>;
65
49
  getQuerySequence<T extends Data>(_c: Collection<string, string, T>, _q?: ItemQuery<string, T>): AsyncIterable<Items<string, T>>;
66
- setQuery<T extends Data>(c: Collection<string, string, T>, q: ItemQuery<string, T>, data: T): Promise<void>;
67
- updateQuery<T extends Data>(c: Collection<string, string, T>, q: ItemQuery<string, T>, updates: Updates<T>): Promise<void>;
68
- deleteQuery<T extends Data>(c: Collection<string, string, T>, q: ItemQuery<string, T>): Promise<void>;
69
- /** Fetch all items in a collection, paginating through `kv.list()`. */
70
- private _getAllItems;
50
+ setQuery<T extends Data>(_c: Collection<string, string, T>, _q: ItemQuery<string, T>, _data: T): Promise<void>;
51
+ updateQuery<T extends Data>(_c: Collection<string, string, T>, _q: ItemQuery<string, T>, _updates: Updates<T>): Promise<void>;
52
+ deleteQuery<T extends Data>(c: Collection<string, string, T>, _q: ItemQuery<string, T>): Promise<void>;
71
53
  }
@@ -1,16 +1,7 @@
1
1
  import { DBProvider } from "../db/provider/DBProvider.js";
2
2
  import { UnimplementedError } from "../error/UnimplementedError.js";
3
- import { requireArray } from "../util/array.js";
4
3
  import { getItem } from "../util/item.js";
5
- import { queryItems } from "../util/query.js";
6
- import { updateData } from "../util/update.js";
7
4
  import { randomUUID } from "../util/uuid.js";
8
- function _getKey(collection, id) {
9
- return `${collection}:${id}`;
10
- }
11
- function _getPrefix(collection) {
12
- return `${collection}:`;
13
- }
14
5
  /**
15
6
  * Cloudflare Workers KV database provider.
16
7
  *
@@ -19,26 +10,21 @@ function _getPrefix(collection) {
19
10
  *
20
11
  * ### Supported
21
12
  * - Single item operations: `getItem`, `setItem`, `addItem`, `updateItem`, `deleteItem`.
22
- * - Query operations: `getQuery`, `setQuery`, `updateQuery`, `deleteQuery`, `countQuery`.
23
- * - All filter operators: equality, not, in, out, contains, gt, gte, lt, lte.
24
- * - Sorting (`$order`) and limiting (`$limit`).
25
13
  * - ID generation: `addItem()` generates a UUID v4 identifier automatically.
26
14
  *
27
15
  * ### Not supported
28
16
  * - **Realtime subscriptions:** `getItemSequence()` and `getQuerySequence()` throw `UnimplementedError`.
29
17
  * KV has no change feed or push notification mechanism.
18
+ * - **Updates:** `updateItem()` and `updateQuery()` throw `UnimplementedError`.
19
+ * - **Collection queries:** `getQuery()`, `setQuery()`, `deleteQuery()`, and `countQuery()` are not supported.
20
+ * KV does not expose efficient filtering, sorting, or collection scans, so this provider avoids
21
+ * the old "read everything and filter in memory" behavior.
30
22
  *
31
23
  * ### Performance limitations
32
- * - **Querying is expensive:** KV has no native query support. Every `getQuery()` call lists all keys
33
- * in the collection (paginated, 1000 keys per page), fetches each value individually, then applies
34
- * filtering, sorting, and limiting in-memory. Avoid large collections where possible.
35
- * - **Non-atomic updates:** `updateItem()` performs a read-modify-write cycle. Concurrent writes to the
36
- * same item may cause one write to be lost.
37
- * - **Eventual consistency:** KV is eventually consistent — reads may return stale data, particularly
38
- * shortly after writes. This also affects `updateItem`, `setQuery`, `updateQuery`, and `deleteQuery`
39
- * since they read before writing.
40
- * - **No bulk get:** Each item value must be fetched individually — there is no multi-get API.
41
- * Query operations that match N items require N+1 KV requests (one `list` + N `get` calls per page).
24
+ * - **Single-key store only:** This provider is intentionally limited to direct key reads and writes.
25
+ * If you need collection queries, filtering, sorting, or bulk mutations, use a different backend.
26
+ * - **Eventual consistency:** KV is eventually consistent, so reads may briefly return stale values
27
+ * shortly after writes.
42
28
  */
43
29
  export class CloudflareKVProvider extends DBProvider {
44
30
  _kv;
@@ -62,52 +48,28 @@ export class CloudflareKVProvider extends DBProvider {
62
48
  async setItem({ name }, id, data) {
63
49
  await this._kv.put(_getKey(name, id), JSON.stringify(data));
64
50
  }
65
- async updateItem(c, id, updates) {
66
- const existing = await this.getItem(c, id);
67
- if (existing)
68
- await this.setItem(c, id, updateData(existing, updates));
51
+ async updateItem(_c, _id, _updates) {
52
+ throw new UnimplementedError("CloudflareKVProvider does not support updates to items");
69
53
  }
70
54
  async deleteItem({ name }, id) {
71
55
  await this._kv.delete(_getKey(name, id));
72
56
  }
73
- async getQuery({ name }, q) {
74
- const all = await this._getAllItems(name);
75
- return q ? requireArray(queryItems(all, q)) : all;
57
+ async getQuery(_c, _q) {
58
+ throw new UnimplementedError("CloudflareKVProvider does not support querying items");
76
59
  }
77
60
  getQuerySequence(_c, _q) {
78
61
  throw new UnimplementedError("CloudflareKVProvider does not support realtime subscriptions");
79
62
  }
80
- async setQuery(c, q, data) {
81
- const items = await this.getQuery(c, q);
82
- await Promise.all(items.map(item => this.setItem(c, item.id, data)));
63
+ async setQuery(_c, _q, _data) {
64
+ throw new UnimplementedError("CloudflareKVProvider does not support querying items");
83
65
  }
84
- async updateQuery(c, q, updates) {
85
- const items = await this.getQuery(c, q);
86
- await Promise.all(items.map(item => this.setItem(c, item.id, updateData(item, updates))));
66
+ async updateQuery(_c, _q, _updates) {
67
+ throw new UnimplementedError("CloudflareKVProvider does not support updates to items");
87
68
  }
88
- async deleteQuery(c, q) {
89
- const items = await this.getQuery(c, q);
90
- await Promise.all(items.map(item => this._kv.delete(_getKey(c.name, item.id))));
91
- }
92
- /** Fetch all items in a collection, paginating through `kv.list()`. */
93
- async _getAllItems(collection) {
94
- const prefix = _getPrefix(collection);
95
- const items = [];
96
- let cursor;
97
- do {
98
- const result = await this._kv.list(cursor ? { prefix, cursor } : { prefix });
99
- const values = await Promise.all(result.keys.map(async (key) => {
100
- const id = key.name.slice(prefix.length);
101
- const data = (await this._kv.get(key.name, { type: "json" }));
102
- if (data)
103
- return getItem(id, data);
104
- }));
105
- for (const item of values) {
106
- if (item)
107
- items.push(item);
108
- }
109
- cursor = result.list_complete ? undefined : result.cursor;
110
- } while (cursor);
111
- return items;
69
+ async deleteQuery(c, _q) {
70
+ throw new UnimplementedError("CloudflareKVProvider does not support querying items");
112
71
  }
113
72
  }
73
+ function _getKey(collection, id) {
74
+ return `${collection}:${id}`;
75
+ }
@@ -11,51 +11,51 @@ export class CacheDBProvider extends DBProvider {
11
11
  }
12
12
  async getItem(collection, id) {
13
13
  const item = await this.source.getItem(collection, id);
14
- const table = this.memory.getTable(collection.name);
14
+ const table = this.memory.getTable(collection);
15
15
  item ? table.setItem(id, item) : table.deleteItem(id);
16
16
  return item;
17
17
  }
18
18
  getItemSequence(collection, id) {
19
- return this.memory.setItemSequence(collection, id, this.source.getItemSequence(collection, id));
19
+ return this.memory.getTable(collection).setItemSequence(id, this.source.getItemSequence(collection, id));
20
20
  }
21
21
  async addItem(collection, data) {
22
22
  const id = await this.source.addItem(collection, data);
23
- this.memory.getTable(collection.name).setItem(id, data);
23
+ this.memory.getTable(collection).setItem(id, data);
24
24
  return id;
25
25
  }
26
26
  async setItem(collection, id, data) {
27
27
  await this.source.setItem(collection, id, data);
28
- this.memory.getTable(collection.name).setItem(id, data);
28
+ this.memory.getTable(collection).setItem(id, data);
29
29
  }
30
30
  async updateItem(collection, id, updates) {
31
31
  await this.source.updateItem(collection, id, updates);
32
- this.memory.getTable(collection.name).updateItem(id, updates);
32
+ this.memory.getTable(collection).updateItem(id, updates);
33
33
  }
34
34
  async deleteItem(collection, id) {
35
35
  await this.source.deleteItem(collection, id);
36
- this.memory.getTable(collection.name).deleteItem(id);
36
+ this.memory.getTable(collection).deleteItem(id);
37
37
  }
38
38
  countQuery(collection, query) {
39
39
  return this.source.countQuery(collection, query);
40
40
  }
41
41
  async getQuery(collection, query) {
42
42
  const items = await this.source.getQuery(collection, query);
43
- this.memory.getTable(collection.name).setItems(items, query);
43
+ this.memory.getTable(collection).setItems(items);
44
44
  return items;
45
45
  }
46
46
  getQuerySequence(collection, query) {
47
- return this.memory.setItemsSequence(collection, this.source.getQuerySequence(collection, query), query);
47
+ return this.memory.getTable(collection).setItemsSequence(this.source.getQuerySequence(collection, query));
48
48
  }
49
49
  async setQuery(collection, query, data) {
50
50
  await this.source.setQuery(collection, query, data);
51
- this.memory.getTable(collection.name).setQuery(query, data);
51
+ this.memory.getTable(collection).setQuery(query, data);
52
52
  }
53
53
  async updateQuery(collection, query, updates) {
54
54
  await this.source.updateQuery(collection, query, updates);
55
- this.memory.getTable(collection.name).updateQuery(query, updates);
55
+ this.memory.getTable(collection).updateQuery(query, updates);
56
56
  }
57
57
  async deleteQuery(collection, query) {
58
58
  await this.source.deleteQuery(collection, query);
59
- this.memory.getTable(collection.name).deleteQuery(query);
59
+ this.memory.getTable(collection).deleteQuery(query);
60
60
  }
61
61
  }
@@ -7,7 +7,7 @@ export class ChangesDBProvider extends ThroughDBProvider {
7
7
  _changes = [];
8
8
  async addItem(collection, data) {
9
9
  const id = await super.addItem(collection, data);
10
- this._changes.push({ action: "set", collection: collection.name, id, data });
10
+ this._changes.push({ action: "add", collection: collection.name, id, data });
11
11
  return id;
12
12
  }
13
13
  async setItem(collection, id, data) {
@@ -14,39 +14,34 @@ export declare class MemoryDBProvider<I extends Identifier = Identifier> extends
14
14
  /** List of tables in `{ collection: Table }` format. */
15
15
  private _tables;
16
16
  /** Get a table for a collection. */
17
- getTable<T extends Data>(collection: string): MemoryTable<I, T>;
18
- getItemTime<T extends Data>(collection: Collection<string, I, T>, id: I): number | undefined;
17
+ getTable<T extends Data>({ name }: Collection<string, I, T>): MemoryTable<I, T>;
19
18
  getItem<T extends Data>(collection: Collection<string, I, T>, id: I): Promise<OptionalItem<I, T>>;
20
19
  getItemSequence<T extends Data>(collection: Collection<string, I, T>, id: I): AsyncIterable<OptionalItem<I, T>>;
21
- getCachedItemSequence<T extends Data>(collection: Collection<string, I, T>, id: I): AsyncIterable<OptionalItem<I, T>>;
22
20
  addItem<T extends Data>(collection: Collection<string, I, T>, data: T): Promise<I>;
23
21
  setItem<T extends Data>(collection: Collection<string, I, T>, id: I, data: T): Promise<void>;
24
- setItemSequence<T extends Data>(collection: Collection<string, I, T>, id: I, sequence: AsyncIterable<OptionalItem<I, T>>): AsyncIterable<OptionalItem<I, T>>;
25
22
  updateItem<T extends Data>(collection: Collection<string, I, T>, id: I, updates: Updates<T>): Promise<void>;
26
23
  deleteItem<T extends Data>(collection: Collection<string, I, T>, id: I): Promise<void>;
27
- getQueryTime<T extends Data>(collection: Collection<string, I, T>, query?: ItemQuery<I, T>): number | undefined;
28
24
  countQuery<T extends Data>(collection: Collection<string, I, T>, query?: ItemQuery<I, T>): Promise<number>;
29
25
  getQuery<T extends Data>(collection: Collection<string, I, T>, query?: ItemQuery<I, T>): Promise<Items<I, T>>;
30
26
  getQuerySequence<T extends Data>(collection: Collection<string, I, T>, query?: ItemQuery<I, T>): AsyncIterable<Items<I, T>>;
31
- getCachedQuerySequence<T extends Data>(collection: Collection<string, I, T>, query?: ItemQuery<I, T>): AsyncIterable<Items<I, T>>;
32
27
  setQuery<T extends Data>(collection: Collection<string, I, T>, query: ItemQuery<I, T>, data: T): Promise<void>;
33
28
  updateQuery<T extends Data>(collection: Collection<string, I, T>, query: ItemQuery<I, T>, updates: Updates<T>): Promise<void>;
34
29
  deleteQuery<T extends Data>(collection: Collection<string, I, T>, query: ItemQuery<I, T>): Promise<void>;
35
- setItems<T extends Data>(collection: Collection<string, I, T>, items: Items<I, T>, query?: ItemQuery<I, T>): void;
36
- setItemsSequence<T extends Data>(collection: Collection<string, I, T>, sequence: AsyncIterable<Items<I, T>>, query?: ItemQuery<I, T>): AsyncIterable<Items<I, T>>;
30
+ setItems<T extends Data>(collection: Collection<string, I, T>, items: Items<I, T>): void;
37
31
  }
38
32
  /** An individual table of data. */
39
33
  export declare class MemoryTable<I extends Identifier, T extends Data> {
40
34
  /** Actual data in this table. */
41
35
  protected readonly _data: Map<Identifier, Item<I, T>>;
42
- /** Times data was last updated. */
43
- protected readonly _times: Map<Identifier, number>;
44
36
  /** Deferred sequence of next values. */
45
37
  readonly next: DeferredSequence<void>;
46
- getItemTime(id: I): number | undefined;
47
38
  getItem(id: I): OptionalItem<I, T>;
39
+ /**
40
+ * Subscribe to all changes for this item key.
41
+ * - Emits the current item immediately, including `undefined` when absent.
42
+ * - Wakes on every table change, but only yields when this item's value actually changed.
43
+ */
48
44
  getItemSequence(id: I): AsyncIterable<OptionalItem<I, T>>;
49
- getCachedItemSequence(id: I): AsyncIterable<OptionalItem<I, T>>;
50
45
  /** Function to generate a random ID for this table. */
51
46
  generateUniqueID(): I;
52
47
  addItem(data: T): I;
@@ -54,14 +49,17 @@ export declare class MemoryTable<I extends Identifier, T extends Data> {
54
49
  setItemSequence(id: I, sequence: AsyncIterable<OptionalItem<I, T>>): AsyncIterable<OptionalItem<I, T>>;
55
50
  updateItem(id: I, updates: Updates<T>): void;
56
51
  deleteItem(id: I): void;
57
- getQueryTime(query?: ItemQuery<I, T>): number | undefined;
58
52
  countQuery(query?: ItemQuery<I, T>): number;
59
53
  getQuery(query?: ItemQuery<I, T>): Items<I, T>;
54
+ /**
55
+ * Subscribe to the live result of a query.
56
+ * - Emits the current query result immediately, even if empty.
57
+ * - Wakes on every table change, but only yields when the computed query result changed.
58
+ */
60
59
  getQuerySequence(query?: ItemQuery<I, T>): AsyncIterable<Items<I, T>>;
61
- getCachedQuerySequence(query?: ItemQuery<I, T>): AsyncIterable<Items<I, T>>;
62
60
  setQuery(query: ItemQuery<I, T>, data: T): void;
63
61
  updateQuery(query: ItemQuery<I, T>, updates: Updates<T>): void;
64
62
  deleteQuery(query: ItemQuery<I, T>): void;
65
- setItems(items: Items<I, T>, query?: ItemQuery<I, T>): void;
66
- setItemsSequence(sequence: AsyncIterable<Items<I, T>>, query?: ItemQuery<I, T>): AsyncIterable<Items<I, T>>;
63
+ setItems(items: Items<I, T>): void;
64
+ setItemsSequence(sequence: AsyncIterable<Items<I, T>>): AsyncIterable<Items<I, T>>;
67
65
  }
@@ -17,82 +17,63 @@ export class MemoryDBProvider extends DBProvider {
17
17
  // biome-ignore lint/suspicious/noExplicitAny: Internal storage erases T; getTable<T> restores it per-call.
18
18
  _tables = {};
19
19
  /** Get a table for a collection. */
20
- getTable(collection) {
21
- // biome-ignore lint/suspicious/noAssignInExpressions: This is convenient.
22
- return (this._tables[collection] ||= new MemoryTable());
23
- }
24
- getItemTime(collection, id) {
25
- return this.getTable(collection.name).getItemTime(id);
20
+ getTable({ name }) {
21
+ return (this._tables[name] ||= new MemoryTable());
26
22
  }
27
23
  async getItem(collection, id) {
28
- return this.getTable(collection.name).getItem(id);
24
+ return this.getTable(collection).getItem(id);
29
25
  }
30
26
  async *getItemSequence(collection, id) {
31
- yield* this.getTable(collection.name).getItemSequence(id);
32
- }
33
- getCachedItemSequence(collection, id) {
34
- return this.getTable(collection.name).getCachedItemSequence(id);
27
+ yield* this.getTable(collection).getItemSequence(id);
35
28
  }
36
29
  async addItem(collection, data) {
37
- return this.getTable(collection.name).addItem(data);
30
+ return this.getTable(collection).addItem(data);
38
31
  }
39
32
  async setItem(collection, id, data) {
40
- this.getTable(collection.name).setItem(id, data);
41
- }
42
- setItemSequence(collection, id, sequence) {
43
- return this.getTable(collection.name).setItemSequence(id, sequence);
33
+ this.getTable(collection).setItem(id, data);
44
34
  }
45
35
  async updateItem(collection, id, updates) {
46
- this.getTable(collection.name).updateItem(id, updates);
36
+ this.getTable(collection).updateItem(id, updates);
47
37
  }
48
38
  async deleteItem(collection, id) {
49
- this.getTable(collection.name).deleteItem(id);
50
- }
51
- getQueryTime(collection, query) {
52
- return this.getTable(collection.name).getQueryTime(query);
39
+ this.getTable(collection).deleteItem(id);
53
40
  }
54
41
  async countQuery(collection, query) {
55
- return this.getTable(collection.name).countQuery(query);
42
+ return this.getTable(collection).countQuery(query);
56
43
  }
57
44
  async getQuery(collection, query) {
58
- return this.getTable(collection.name).getQuery(query);
45
+ return this.getTable(collection).getQuery(query);
59
46
  }
60
47
  async *getQuerySequence(collection, query) {
61
- yield* this.getTable(collection.name).getQuerySequence(query);
62
- }
63
- getCachedQuerySequence(collection, query) {
64
- return this.getTable(collection.name).getCachedQuerySequence(query);
48
+ yield* this.getTable(collection).getQuerySequence(query);
65
49
  }
66
50
  async setQuery(collection, query, data) {
67
- this.getTable(collection.name).setQuery(query, data);
51
+ this.getTable(collection).setQuery(query, data);
68
52
  }
69
53
  async updateQuery(collection, query, updates) {
70
- this.getTable(collection.name).updateQuery(query, updates);
54
+ this.getTable(collection).updateQuery(query, updates);
71
55
  }
72
56
  async deleteQuery(collection, query) {
73
- this.getTable(collection.name).deleteQuery(query);
74
- }
75
- setItems(collection, items, query) {
76
- this.getTable(collection.name).setItems(items, query);
57
+ this.getTable(collection).deleteQuery(query);
77
58
  }
78
- setItemsSequence(collection, sequence, query) {
79
- return this.getTable(collection.name).setItemsSequence(sequence, query);
59
+ setItems(collection, items) {
60
+ this.getTable(collection).setItems(items);
80
61
  }
81
62
  }
82
63
  /** An individual table of data. */
83
64
  export class MemoryTable {
84
65
  /** Actual data in this table. */
85
66
  _data = new Map();
86
- /** Times data was last updated. */
87
- _times = new Map();
88
67
  /** Deferred sequence of next values. */
89
68
  next = new DeferredSequence();
90
- getItemTime(id) {
91
- return this._times.get(id);
92
- }
93
69
  getItem(id) {
94
70
  return this._data.get(id);
95
71
  }
72
+ /**
73
+ * Subscribe to all changes for this item key.
74
+ * - Emits the current item immediately, including `undefined` when absent.
75
+ * - Wakes on every table change, but only yields when this item's value actually changed.
76
+ */
96
77
  async *getItemSequence(id) {
97
78
  let lastValue = this.getItem(id);
98
79
  yield lastValue;
@@ -105,20 +86,6 @@ export class MemoryTable {
105
86
  }
106
87
  }
107
88
  }
108
- async *getCachedItemSequence(id) {
109
- let lastTime = this._times.get(id);
110
- if (typeof lastTime === "number")
111
- yield this.getItem(id);
112
- while (true) {
113
- await this.next;
114
- const nextTime = this._times.get(id);
115
- if (nextTime !== lastTime) {
116
- if (typeof nextTime === "number")
117
- yield this.getItem(id);
118
- lastTime = nextTime;
119
- }
120
- }
121
- }
122
89
  generateUniqueID() {
123
90
  const gen = typeof this._data.keys().next().value === "number" ? getRandom : getRandomKey;
124
91
  let id = gen();
@@ -135,7 +102,6 @@ export class MemoryTable {
135
102
  const item = getItem(id, data);
136
103
  if (this._data.get(id) !== item) {
137
104
  this._data.set(id, item);
138
- this._times.set(id, Date.now());
139
105
  this.next.resolve();
140
106
  }
141
107
  }
@@ -147,25 +113,31 @@ export class MemoryTable {
147
113
  }
148
114
  updateItem(id, updates) {
149
115
  const oldItem = this._data.get(id);
150
- if (oldItem)
151
- this.setItem(id, updateData(oldItem, updates));
116
+ if (!oldItem)
117
+ return;
118
+ const nextItem = updateData(oldItem, updates);
119
+ if (this._data.get(id) !== nextItem) {
120
+ this._data.set(id, nextItem);
121
+ this.next.resolve();
122
+ }
152
123
  }
153
124
  deleteItem(id) {
154
125
  if (this._data.has(id)) {
155
126
  this._data.delete(id);
156
- this._times.set(id, Date.now());
157
127
  this.next.resolve();
158
128
  }
159
129
  }
160
- getQueryTime(query) {
161
- return this._times.get(_getQueryKey(query));
162
- }
163
130
  countQuery(query) {
164
131
  return query ? countItems(queryItems(this._data.values(), query)) : this._data.size;
165
132
  }
166
133
  getQuery(query) {
167
134
  return requireArray(query ? queryItems(this._data.values(), query) : this._data.values());
168
135
  }
136
+ /**
137
+ * Subscribe to the live result of a query.
138
+ * - Emits the current query result immediately, even if empty.
139
+ * - Wakes on every table change, but only yields when the computed query result changed.
140
+ */
169
141
  async *getQuerySequence(query) {
170
142
  let lastItems = this.getQuery(query);
171
143
  yield lastItems;
@@ -178,72 +150,59 @@ export class MemoryTable {
178
150
  }
179
151
  }
180
152
  }
181
- async *getCachedQuerySequence(query) {
182
- const key = _getQueryKey(query);
183
- let lastTime = this._times.get(key);
184
- if (typeof lastTime === "number")
185
- yield this.getQuery(query);
186
- while (true) {
187
- await this.next;
188
- const nextTime = this._times.get(key);
189
- if (lastTime !== nextTime) {
190
- if (typeof nextTime === "number")
191
- yield this.getQuery(query);
192
- lastTime = nextTime;
193
- }
194
- }
195
- }
196
153
  setQuery(query, data) {
197
- let changed = 0;
154
+ let changed = false;
198
155
  for (const { id } of queryWritableItems(this._data.values(), query)) {
199
- this.setItem(id, data);
200
- changed++;
156
+ const item = getItem(id, data);
157
+ if (this._data.get(id) !== item) {
158
+ this._data.set(id, item);
159
+ changed = true;
160
+ }
201
161
  }
202
- if (changed) {
203
- const key = _getQueryKey(query);
204
- this._times.set(key, Date.now());
162
+ if (changed)
205
163
  this.next.resolve();
206
- }
207
164
  }
208
165
  updateQuery(query, updates) {
209
- let count = 0;
166
+ let changed = false;
210
167
  for (const { id } of queryWritableItems(this._data.values(), query)) {
211
- this.updateItem(id, updates);
212
- count++;
168
+ const oldItem = this._data.get(id);
169
+ if (!oldItem)
170
+ continue;
171
+ const nextItem = updateData(oldItem, updates);
172
+ if (this._data.get(id) !== nextItem) {
173
+ this._data.set(id, nextItem);
174
+ changed = true;
175
+ }
213
176
  }
214
- if (count) {
215
- const key = _getQueryKey(query);
216
- this._times.set(key, Date.now());
177
+ if (changed)
217
178
  this.next.resolve();
218
- }
219
179
  }
220
180
  deleteQuery(query) {
221
- let count = 0;
181
+ let changed = false;
222
182
  for (const { id } of queryWritableItems(this._data.values(), query)) {
223
- this.deleteItem(id);
224
- count++;
183
+ if (this._data.has(id)) {
184
+ this._data.delete(id);
185
+ changed = true;
186
+ }
225
187
  }
226
- if (count) {
227
- const key = _getQueryKey(query);
228
- this._times.set(key, Date.now());
188
+ if (changed)
229
189
  this.next.resolve();
230
- }
231
190
  }
232
- setItems(items, query) {
233
- for (const item of items)
234
- this.setItem(item.id, item);
235
- if (query) {
236
- const key = _getQueryKey(query);
237
- this._times.set(key, Date.now());
238
- this.next.resolve();
191
+ setItems(items) {
192
+ let changed = false;
193
+ for (const item of items) {
194
+ if (this._data.get(item.id) !== item) {
195
+ this._data.set(item.id, item);
196
+ changed = true;
197
+ }
239
198
  }
199
+ if (changed)
200
+ this.next.resolve();
240
201
  }
241
- async *setItemsSequence(sequence, query) {
202
+ async *setItemsSequence(sequence) {
242
203
  for await (const items of sequence) {
243
- this.setItems(items, query);
204
+ this.setItems(items);
244
205
  yield items;
245
206
  }
246
207
  }
247
208
  }
248
- // Queries that have no limit don't care about sorting either.
249
- const _getQueryKey = (query) => (query ? JSON.stringify(query) : "{}");
@@ -29,17 +29,15 @@ export class ItemStore extends OptionalDataStore {
29
29
  this.value = getItem(this.id, data);
30
30
  }
31
31
  constructor(collection, id, provider, memory) {
32
- const time = memory?.getItemTime(collection, id);
33
- const item = memory?.getTable(collection.name).getItem(id);
34
- super(typeof time === "number" || item ? item : NONE, time); // Use the cached value if it was definitely cached or is not undefined.
32
+ const item = memory?.getTable(collection).getItem(id);
33
+ super(item ?? NONE); // Use the current memory snapshot if available.
35
34
  if (memory)
36
- this.starter = store => runSequence(store.through(memory.getCachedItemSequence(collection, id)));
35
+ this.starter = store => runSequence(store.through(memory.getItemSequence(collection, id)));
37
36
  this.provider = provider;
38
37
  this.collection = collection;
39
38
  this.id = id;
40
- // Start loading the value from the provider if it wasn't cached.
41
- if (typeof time !== "number")
42
- this.refresh();
39
+ // Always refresh from source, even if memory supplied an initial value.
40
+ this.refresh();
43
41
  }
44
42
  /** Refresh this store from the source provider. */
45
43
  refresh(provider = this.provider) {
@@ -44,18 +44,16 @@ export class QueryStore extends ArrayStore {
44
44
  return last;
45
45
  }
46
46
  constructor(collection, query, provider, memory) {
47
- const time = memory?.getQueryTime(collection, query);
48
- const items = memory?.getTable(collection.name).getQuery(query) || [];
49
- super(typeof time === "number" || items.length ? items : NONE, time); // Use the value if it was definitely cached or is not empty.
47
+ const items = memory?.getTable(collection).getQuery(query);
48
+ super(items ?? NONE); // Use the current memory snapshot if available.
50
49
  if (memory)
51
- this.starter = store => runSequence(store.through(memory.getCachedQuerySequence(collection, query)));
50
+ this.starter = store => runSequence(store.through(memory.getQuerySequence(collection, query)));
52
51
  this.provider = provider;
53
52
  this.collection = collection;
54
53
  this.query = query;
55
54
  this.limit = getLimit(query) ?? Number.POSITIVE_INFINITY;
56
- // Start loading the value from the provider if it is not definitely cached.
57
- if (typeof time !== "number")
58
- this.refresh();
55
+ // Always refresh from source, even if memory supplied an initial value.
56
+ this.refresh();
59
57
  }
60
58
  /** Refresh this store from the source provider. */
61
59
  refresh(provider = this.provider) {
package/package.json CHANGED
@@ -1,27 +1,29 @@
1
1
  {
2
2
  "name": "shelving",
3
- "description": "Toolkit for using data in JavaScript.",
4
- "keywords": [
5
- "javascript",
6
- "typescript",
7
- "schema",
8
- "validation",
9
- "database",
10
- "database-connector",
11
- "state-management",
12
- "query-builder"
13
- ],
14
- "version": "1.179.0",
3
+ "version": "1.179.2",
4
+ "author": "Dave Houlbrooke <dave@shax.com>",
15
5
  "repository": {
16
6
  "type": "git",
17
7
  "url": "git+https://github.com/dhoulb/shelving.git"
18
8
  },
19
- "author": "Dave Houlbrooke <dave@shax.com>",
20
- "license": "0BSD",
21
- "type": "module",
22
- "module": "./index.js",
23
9
  "main": "./index.js",
24
- "types": "./index.d.ts",
10
+ "module": "./index.js",
11
+ "devDependencies": {
12
+ "@biomejs/biome": "^2.4.9",
13
+ "@google-cloud/firestore": "^8.3.0",
14
+ "@types/bun": "^1.3.11",
15
+ "@types/react": "^19.2.14",
16
+ "@types/react-dom": "^19.2.3",
17
+ "firebase": "^12.11.0",
18
+ "react": "^19.2.4",
19
+ "react-dom": "^19.2.4",
20
+ "typescript": "^5.9.3"
21
+ },
22
+ "peerDependencies": {
23
+ "@google-cloud/firestore": ">=7.0.0",
24
+ "firebase": ">=11.0.0",
25
+ "react": ">=19.0.0"
26
+ },
25
27
  "exports": {
26
28
  ".": "./index.js",
27
29
  "./api": "./api/index.js",
@@ -40,11 +42,22 @@
40
42
  "./test": "./test/index.js",
41
43
  "./util": "./util/index.js"
42
44
  },
43
- "sideEffects": false,
45
+ "description": "Toolkit for using data in JavaScript.",
44
46
  "engineStrict": true,
45
47
  "engines": {
46
48
  "node": ">=16.0.0"
47
49
  },
50
+ "keywords": [
51
+ "javascript",
52
+ "typescript",
53
+ "schema",
54
+ "validation",
55
+ "database",
56
+ "database-connector",
57
+ "state-management",
58
+ "query-builder"
59
+ ],
60
+ "license": "0BSD",
48
61
  "scripts": {
49
62
  "test": "bun run --parallel test:*",
50
63
  "test:lint": "biome check .",
@@ -59,20 +72,7 @@
59
72
  "build:test:syntax": "bun run ./dist/index.js",
60
73
  "build:test:unit": "bun test ./dist/**/*.test.js --bail"
61
74
  },
62
- "devDependencies": {
63
- "@biomejs/biome": "^2.4.9",
64
- "@google-cloud/firestore": "^8.3.0",
65
- "@types/bun": "^1.3.11",
66
- "@types/react": "^19.2.14",
67
- "@types/react-dom": "^19.2.3",
68
- "firebase": "^12.11.0",
69
- "react": "^19.2.4",
70
- "react-dom": "^19.2.4",
71
- "typescript": "^5.9.3"
72
- },
73
- "peerDependencies": {
74
- "@google-cloud/firestore": ">=7.0.0",
75
- "firebase": ">=11.0.0",
76
- "react": ">=19.0.0"
77
- }
75
+ "sideEffects": false,
76
+ "type": "module",
77
+ "types": "./index.d.ts"
78
78
  }