@firsthandjs/data 0.6.0 → 0.6.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.
package/README.md CHANGED
@@ -9,7 +9,7 @@ what is loaded, what state that is in, and when it has to be loaded again.
9
9
  npm install @firsthandjs/data
10
10
  ```
11
11
 
12
- 3.65 kB gzip. It depends on `@firsthandjs/core` and `@firsthandjs/dom`.
12
+ 3.84 kB gzip. It depends on `@firsthandjs/core` and `@firsthandjs/dom`.
13
13
 
14
14
  ```tsx
15
15
  const api = createFetchClient({ baseUrl: '/api' });
@@ -67,6 +67,11 @@ components asking at once make one request, which is waste removed rather than
67
67
  staleness introduced. `force` drops the entry, which is how an invalidation
68
68
  reaches all the way down.
69
69
 
70
+ A key is an **identity** and a **request**: `GET /api/me` is the same URL for
71
+ everybody, so the identity — the `authorization` header by default, or a
72
+ `scope` function you give — is what stops one account being served the answer
73
+ cached for another.
74
+
70
75
  ## Tags are for invalidation
71
76
 
72
77
  They may be coarse, they may overlap, and two unrelated sources may share one.
package/dist/cache.d.ts CHANGED
@@ -38,6 +38,16 @@
38
38
  * goes when `max` is reached.
39
39
  */
40
40
  import type { Loader } from './store.js';
41
+ /**
42
+ * A key for a value, stable however the value was written.
43
+ *
44
+ * `{ a: 1, b: 2 }` and `{ b: 2, a: 1 }` are the same request, so they must be
45
+ * the same key — which `JSON.stringify` alone does not give you, because it
46
+ * keeps insertion order. Exported because a caller building a `cacheKey` of
47
+ * their own needs the same guarantee, and getting it wrong means one answer
48
+ * served for two different requests.
49
+ */
50
+ export declare function stableKey(value: unknown): string;
41
51
  export interface CacheOptions {
42
52
  /**
43
53
  * How long an answer is served again without running the producer, in ms.
@@ -1 +1 @@
1
- {"version":3,"file":"cache.d.ts","sourceRoot":"","sources":["../src/cache.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAsCG;AACH,OAAO,KAAK,EAAe,MAAM,EAAE,MAAM,YAAY,CAAC;AAEtD,MAAM,WAAW,YAAY;IAC3B;;;;;;OAMG;IACH,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,CAAC;IACtB;;;;;;OAMG;IACH,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,CAAC;IACtB,8CAA8C;IAC9C,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;CAC7B;AAED,MAAM,WAAW,WAAW;IAC1B;;;;;;OAMG;IACH,IAAI,CAAC,CAAC,EAAE,GAAG,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,CAAC,CAAC,CAAC,GAAG,MAAM,CAAC,CAAC,CAAC,CAAC;IACpD;;;;;;OAMG;IACH,KAAK,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,GAAG,IAAI,CAAC;IACzC,0EAA0E;IAC1E,IAAI,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;IAC3B,sCAAsC;IACtC,MAAM,CAAC,GAAG,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,qDAAqD;IACrD,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;CACvB;AAaD,wBAAgB,iBAAiB,CAAC,OAAO,GAAE,YAAiB,GAAG,WAAW,CAkHzE"}
1
+ {"version":3,"file":"cache.d.ts","sourceRoot":"","sources":["../src/cache.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAsCG;AACH,OAAO,KAAK,EAAe,MAAM,EAAE,MAAM,YAAY,CAAC;AAEtD;;;;;;;;GAQG;AACH,wBAAgB,SAAS,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,CAmBhD;AAED,MAAM,WAAW,YAAY;IAC3B;;;;;;OAMG;IACH,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,CAAC;IACtB;;;;;;OAMG;IACH,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,CAAC;IACtB,8CAA8C;IAC9C,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;CAC7B;AAED,MAAM,WAAW,WAAW;IAC1B;;;;;;OAMG;IACH,IAAI,CAAC,CAAC,EAAE,GAAG,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,CAAC,CAAC,CAAC,GAAG,MAAM,CAAC,CAAC,CAAC,CAAC;IACpD;;;;;;OAMG;IACH,KAAK,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,GAAG,IAAI,CAAC;IACzC,0EAA0E;IAC1E,IAAI,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;IAC3B,sCAAsC;IACtC,MAAM,CAAC,GAAG,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,qDAAqD;IACrD,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;CACvB;AAaD,wBAAgB,iBAAiB,CAAC,OAAO,GAAE,YAAiB,GAAG,WAAW,CAkHzE"}
package/dist/http.d.ts CHANGED
@@ -36,6 +36,11 @@ export interface JsonRequest extends Omit<RequestInit, 'body'> {
36
36
  * nowhere.
37
37
  *
38
38
  * A key is for when the URL is not the identity — a POST that reads, say.
39
+ * It must then carry **everything that varies**, the body included:
40
+ * `` cacheKey: `search:${stableKey(body)}` `` rather than `'search'`, or
41
+ * two different searches share one answer. The client's identity scope is added
42
+ * for you either way.
43
+ *
39
44
  * `false` is for the read that must never be served from memory. The
40
45
  * platform's own `cache` option is untouched and still passed to `fetch`,
41
46
  * because that one is the HTTP cache and a different thing entirely.
@@ -63,6 +68,24 @@ export interface FetchClientOptions {
63
68
  readonly cache?: false | CacheOptions | CacheClient;
64
69
  /** Applied to every request: `credentials`, `mode`, `referrerPolicy`, … */
65
70
  readonly init?: RequestInit;
71
+ /**
72
+ * Who the cached answers belong to.
73
+ *
74
+ * A cache keyed by URL alone is a cache that can serve one account's answer
75
+ * to the next one, because `GET /api/me` is the same URL for everybody. So
76
+ * the key carries an identity, and by default that identity is the
77
+ * `authorization` header this request would be sent with — which means the
78
+ * dangerous case is handled without anybody opting in.
79
+ *
80
+ * Set this when the identity is somewhere else: a cookie session, a tenant
81
+ * header, an account picker. It is read per request and untracked, like
82
+ * `headers`. Returning `''` says every caller shares one cache.
83
+ *
84
+ * Clearing the cache on sign-out (`client.cache?.forget()`) is still worth
85
+ * doing — it frees the memory, and it is what `store.clear()` is beside —
86
+ * but forgetting to is no longer a correctness bug.
87
+ */
88
+ readonly scope?: () => string;
66
89
  /**
67
90
  * The seam. Wrap `fetch` to log, to retry, or to end a session on a 401 —
68
91
  * and keep that in one place rather than in a plugin system.
@@ -1 +1 @@
1
- {"version":3,"file":"http.d.ts","sourceRoot":"","sources":["../src/http.ts"],"names":[],"mappings":"AA8BA,OAAO,EAAqB,KAAK,WAAW,EAAE,KAAK,YAAY,EAAE,MAAM,YAAY,CAAC;AACpF,OAAO,KAAK,EAAe,MAAM,EAAE,MAAM,YAAY,CAAC;AAEtD;;;;;GAKG;AACH,qBAAa,kBAAmB,SAAQ,KAAK;IAEzC,QAAQ,CAAC,MAAM,EAAE,MAAM;IACvB,QAAQ,CAAC,GAAG,EAAE,MAAM;IACpB,yCAAyC;IACzC,QAAQ,CAAC,IAAI,EAAE,OAAO;gBAHb,MAAM,EAAE,MAAM,EACd,GAAG,EAAE,MAAM;IACpB,yCAAyC;IAChC,IAAI,EAAE,OAAO;CAKzB;AAED;;;;;;;;;;;GAWG;AACH,MAAM,WAAW,WAAY,SAAQ,IAAI,CAAC,WAAW,EAAE,MAAM,CAAC;IAC5D,QAAQ,CAAC,IAAI,CAAC,EAAE,QAAQ,GAAG,IAAI,CAAC;IAChC,6EAA6E;IAC7E,QAAQ,CAAC,IAAI,CAAC,EAAE,OAAO,CAAC;IACxB;;;;;;;;;;;;OAYG;IACH,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,GAAG,KAAK,CAAC;CACpC;AAED,MAAM,WAAW,kBAAkB;IACjC,mFAAmF;IACnF,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC;IAC1B;;;;OAIG;IACH,QAAQ,CAAC,OAAO,CAAC,EAAE,WAAW,GAAG,CAAC,MAAM,WAAW,CAAC,CAAC;IACrD;;;;OAIG;IACH,QAAQ,CAAC,KAAK,CAAC,EAAE,KAAK,GAAG,YAAY,GAAG,WAAW,CAAC;IACpD,2EAA2E;IAC3E,QAAQ,CAAC,IAAI,CAAC,EAAE,WAAW,CAAC;IAC5B;;;OAGG;IACH,QAAQ,CAAC,KAAK,CAAC,EAAE,OAAO,UAAU,CAAC,KAAK,CAAC;CAC1C;AAED,MAAM,WAAW,WAAW;IAC1B,uEAAuE;IACvE,OAAO,CAAC,CAAC,EAAE,GAAG,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,WAAW,GAAG,MAAM,CAAC,CAAC,CAAC,CAAC;IACvD,GAAG,CAAC,CAAC,EAAE,GAAG,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,WAAW,GAAG,MAAM,CAAC,CAAC,CAAC,CAAC;IACnD,IAAI,CAAC,CAAC,EAAE,GAAG,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,WAAW,GAAG,MAAM,CAAC,CAAC,CAAC,CAAC;IACpD,GAAG,CAAC,CAAC,EAAE,GAAG,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,WAAW,GAAG,MAAM,CAAC,CAAC,CAAC,CAAC;IACnD,KAAK,CAAC,CAAC,EAAE,GAAG,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,WAAW,GAAG,MAAM,CAAC,CAAC,CAAC,CAAC;IACrD,mEAAmE;IACnE,MAAM,CAAC,CAAC,EAAE,GAAG,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,WAAW,GAAG,MAAM,CAAC,CAAC,CAAC,CAAC;IACtD;;;;OAIG;IACH,IAAI,CAAC,OAAO,EAAE,kBAAkB,GAAG,WAAW,CAAC;IAC/C,iEAAiE;IACjE,QAAQ,CAAC,KAAK,EAAE,WAAW,GAAG,SAAS,CAAC;CACzC;AAmDD,wBAAgB,iBAAiB,CAAC,OAAO,GAAE,kBAAuB,GAAG,WAAW,CAmD/E"}
1
+ {"version":3,"file":"http.d.ts","sourceRoot":"","sources":["../src/http.ts"],"names":[],"mappings":"AA8BA,OAAO,EAAqB,KAAK,WAAW,EAAE,KAAK,YAAY,EAAE,MAAM,YAAY,CAAC;AACpF,OAAO,KAAK,EAAe,MAAM,EAAE,MAAM,YAAY,CAAC;AAEtD;;;;;GAKG;AACH,qBAAa,kBAAmB,SAAQ,KAAK;IAEzC,QAAQ,CAAC,MAAM,EAAE,MAAM;IACvB,QAAQ,CAAC,GAAG,EAAE,MAAM;IACpB,yCAAyC;IACzC,QAAQ,CAAC,IAAI,EAAE,OAAO;gBAHb,MAAM,EAAE,MAAM,EACd,GAAG,EAAE,MAAM;IACpB,yCAAyC;IAChC,IAAI,EAAE,OAAO;CAKzB;AAED;;;;;;;;;;;GAWG;AACH,MAAM,WAAW,WAAY,SAAQ,IAAI,CAAC,WAAW,EAAE,MAAM,CAAC;IAC5D,QAAQ,CAAC,IAAI,CAAC,EAAE,QAAQ,GAAG,IAAI,CAAC;IAChC,6EAA6E;IAC7E,QAAQ,CAAC,IAAI,CAAC,EAAE,OAAO,CAAC;IACxB;;;;;;;;;;;;;;;;;OAiBG;IACH,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,GAAG,KAAK,CAAC;CACpC;AAED,MAAM,WAAW,kBAAkB;IACjC,mFAAmF;IACnF,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC;IAC1B;;;;OAIG;IACH,QAAQ,CAAC,OAAO,CAAC,EAAE,WAAW,GAAG,CAAC,MAAM,WAAW,CAAC,CAAC;IACrD;;;;OAIG;IACH,QAAQ,CAAC,KAAK,CAAC,EAAE,KAAK,GAAG,YAAY,GAAG,WAAW,CAAC;IACpD,2EAA2E;IAC3E,QAAQ,CAAC,IAAI,CAAC,EAAE,WAAW,CAAC;IAC5B;;;;;;;;;;;;;;;;OAgBG;IACH,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,MAAM,CAAC;IAC9B;;;OAGG;IACH,QAAQ,CAAC,KAAK,CAAC,EAAE,OAAO,UAAU,CAAC,KAAK,CAAC;CAC1C;AAED,MAAM,WAAW,WAAW;IAC1B,uEAAuE;IACvE,OAAO,CAAC,CAAC,EAAE,GAAG,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,WAAW,GAAG,MAAM,CAAC,CAAC,CAAC,CAAC;IACvD,GAAG,CAAC,CAAC,EAAE,GAAG,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,WAAW,GAAG,MAAM,CAAC,CAAC,CAAC,CAAC;IACnD,IAAI,CAAC,CAAC,EAAE,GAAG,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,WAAW,GAAG,MAAM,CAAC,CAAC,CAAC,CAAC;IACpD,GAAG,CAAC,CAAC,EAAE,GAAG,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,WAAW,GAAG,MAAM,CAAC,CAAC,CAAC,CAAC;IACnD,KAAK,CAAC,CAAC,EAAE,GAAG,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,WAAW,GAAG,MAAM,CAAC,CAAC,CAAC,CAAC;IACrD,mEAAmE;IACnE,MAAM,CAAC,CAAC,EAAE,GAAG,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,WAAW,GAAG,MAAM,CAAC,CAAC,CAAC,CAAC;IACtD;;;;OAIG;IACH,IAAI,CAAC,OAAO,EAAE,kBAAkB,GAAG,WAAW,CAAC;IAC/C,iEAAiE;IACjE,QAAQ,CAAC,KAAK,EAAE,WAAW,GAAG,SAAS,CAAC;CACzC;AAmDD,wBAAgB,iBAAiB,CAAC,OAAO,GAAE,kBAAuB,GAAG,WAAW,CA6D/E"}
package/dist/index.d.ts CHANGED
@@ -17,7 +17,7 @@ export type { ActionContext, DataOptions, DataRequest, DataStore, LoadContext, L
17
17
  * own: the same `read`, the same lifetimes, and the same `force` reaching
18
18
  * through when a resource is invalidated (ADR-0023).
19
19
  */
20
- export { createCacheClient } from './cache.js';
20
+ export { createCacheClient, stableKey } from './cache.js';
21
21
  export type { CacheClient, CacheOptions } from './cache.js';
22
22
  export { tag, tagMatches, anyTagMatches } from './tags.js';
23
23
  export type { Tag, TagVars, Variables } from './tags.js';
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,OAAO,EAAE,UAAU,EAAE,WAAW,EAAE,OAAO,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AAChF,OAAO,EAAE,WAAW,EAAE,SAAS,EAAE,cAAc,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AACpF,YAAY,EAAE,MAAM,EAAE,cAAc,EAAE,aAAa,EAAE,eAAe,EAAE,MAAM,eAAe,CAAC;AAC5F,YAAY,EACV,aAAa,EACb,WAAW,EACX,WAAW,EACX,SAAS,EACT,WAAW,EACX,MAAM,EACN,QAAQ,EACR,MAAM,EACN,OAAO,GACR,MAAM,YAAY,CAAC;AAEpB;;;;;;GAMG;AACH,OAAO,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAC;AAC/C,YAAY,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAE5D,OAAO,EAAE,GAAG,EAAE,UAAU,EAAE,aAAa,EAAE,MAAM,WAAW,CAAC;AAC3D,YAAY,EAAE,GAAG,EAAE,OAAO,EAAE,SAAS,EAAE,MAAM,WAAW,CAAC;AAEzD,OAAO,EAAE,YAAY,EAAE,WAAW,EAAE,uBAAuB,EAAE,MAAM,eAAe,CAAC;AACnF,YAAY,EAAE,iBAAiB,EAAE,eAAe,EAAE,WAAW,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC;AAE/F;;;GAGG;AACH,OAAO,EAAE,iBAAiB,EAAE,kBAAkB,EAAE,MAAM,WAAW,CAAC;AAClE,YAAY,EAAE,WAAW,EAAE,kBAAkB,EAAE,WAAW,EAAE,MAAM,WAAW,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,OAAO,EAAE,UAAU,EAAE,WAAW,EAAE,OAAO,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AAChF,OAAO,EAAE,WAAW,EAAE,SAAS,EAAE,cAAc,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AACpF,YAAY,EAAE,MAAM,EAAE,cAAc,EAAE,aAAa,EAAE,eAAe,EAAE,MAAM,eAAe,CAAC;AAC5F,YAAY,EACV,aAAa,EACb,WAAW,EACX,WAAW,EACX,SAAS,EACT,WAAW,EACX,MAAM,EACN,QAAQ,EACR,MAAM,EACN,OAAO,GACR,MAAM,YAAY,CAAC;AAEpB;;;;;;GAMG;AACH,OAAO,EAAE,iBAAiB,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAC1D,YAAY,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAE5D,OAAO,EAAE,GAAG,EAAE,UAAU,EAAE,aAAa,EAAE,MAAM,WAAW,CAAC;AAC3D,YAAY,EAAE,GAAG,EAAE,OAAO,EAAE,SAAS,EAAE,MAAM,WAAW,CAAC;AAEzD,OAAO,EAAE,YAAY,EAAE,WAAW,EAAE,uBAAuB,EAAE,MAAM,eAAe,CAAC;AACnF,YAAY,EAAE,iBAAiB,EAAE,eAAe,EAAE,WAAW,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC;AAE/F;;;GAGG;AACH,OAAO,EAAE,iBAAiB,EAAE,kBAAkB,EAAE,MAAM,WAAW,CAAC;AAClE,YAAY,EAAE,WAAW,EAAE,kBAAkB,EAAE,WAAW,EAAE,MAAM,WAAW,CAAC"}
package/dist/index.dev.js CHANGED
@@ -316,6 +316,22 @@ async function void_(promise) {
316
316
  }
317
317
 
318
318
  // packages/data/src/cache.ts
319
+ function stableKey(value) {
320
+ if (value === void 0) {
321
+ return "undefined";
322
+ }
323
+ if (value === null || typeof value !== "object") {
324
+ return JSON.stringify(value);
325
+ }
326
+ if (Array.isArray(value)) {
327
+ return `[${value.map(stableKey).join(",")}]`;
328
+ }
329
+ if (value instanceof Date) {
330
+ return value.toISOString();
331
+ }
332
+ const entries = Object.entries(value).filter(([, held]) => held !== void 0).sort(([one], [other]) => one < other ? -1 : 1);
333
+ return `{${entries.map(([name, held]) => `${name}:${stableKey(held)}`).join(",")}}`;
334
+ }
319
335
  function createCacheClient(options = {}) {
320
336
  const ttl = options.ttl ?? 0;
321
337
  const max = options.max ?? 100;
@@ -678,7 +694,9 @@ function createFetchClient(options = {}) {
678
694
  request: (url, init = {}) => async (request) => {
679
695
  const [target, merged] = resolve(options, url, init);
680
696
  const method = (merged.method ?? "GET").toUpperCase();
681
- const key = typeof init.cacheKey === "string" ? init.cacheKey : method === "GET" || method === "HEAD" ? `${method} ${target}` : void 0;
697
+ const scope = options.scope === void 0 ? merged.headers.get("authorization") ?? "" : untrack2(options.scope);
698
+ const named = typeof init.cacheKey === "string" ? init.cacheKey : method === "GET" || method === "HEAD" ? `${method} ${target}` : void 0;
699
+ const key = named === void 0 ? void 0 : `${scope}\0${named}`;
682
700
  if (cache === void 0 || init.cacheKey === false || key === void 0) {
683
701
  return await send(options, target, merged, request);
684
702
  }
@@ -714,6 +732,7 @@ export {
714
732
  fromPromise,
715
733
  parseGraphQL,
716
734
  resolveTags,
735
+ stableKey,
717
736
  tag,
718
737
  tagMatches,
719
738
  useAction,
package/dist/index.js CHANGED
@@ -1,2 +1,2 @@
1
- import{createContext as J,effect as j,onCleanup as D,untrack as z,useContext as N}from"@firsthandjs/core";import{batch as L,signal as b}from"@firsthandjs/core";var F=Object.freeze({});function R(e,n=F){return{name:e,vars:n}}function O(e,n){if(e.name!==n.name)return!1;for(let a of Object.keys(e.vars))if(!Object.is(e.vars[a],n.vars[a]))return!1;return!0}function m(e,n){for(let a of e)for(let t of n)if(O(a,t))return!0;return!1}function S(e={}){let n=new Set;return{storage:e.storage,hold:a=>(n.add(a),()=>n.delete(a)),invalidate:async(...a)=>{let t=[];for(let r of[...n])r.controller!==null&&r.pending.push(...a),m(a,r.tags)&&(r.name,r.tags,t.push(r.run(!0)));await Promise.all(t)},clear:()=>{for(let a of[...n])a.controller?.abort(),n.delete(a);try{e.storage?.clear?.()}catch{}},get size(){return n.size}}}function x(e,n){return{data:b(void 0),error:b(void 0),status:b("idle"),loading:b(!1),tags:[],controller:null,pending:[],superseded:!1,disposed:!1,name:n}}function h(e,n){L(()=>{e.data.value=n,e.error.value=void 0,e.status.value="success",e.loading.value=!1})}function v(e,n){L(()=>{e.error.value=n,e.status.value="error",e.loading.value=!1})}function C(e,n){return{data:e.data,error:e.error,status:e.status,loading:e.loading,reload:()=>e.run(!0),dispose:()=>{e.disposed=!0,e.controller?.abort(),n()}}}var P=J();function y(){return N(P).value}function Q(){let e=y();return(...n)=>e.invalidate(...n)}function V(e,n={}){let a=y(),t=x(a,n.persist),r=a.hold(t);n.persist,t.run=o=>{if(t.disposed)return Promise.resolve(void 0);t.controller?.abort();let i=new AbortController;t.controller=i,t.pending=[],t.superseded=!1,t.loading.value=!0,t.data.peek()===void 0&&(t.status.value="loading");let d=(...u)=>{t.tags=u,t.pending.length>0&&m(t.pending,u)&&(t.superseded=!0)},c={signal:i.signal,force:o,tags:d},p={...c,request:c,tags:d};return e(p).then(async u=>{if(!(i.signal.aborted||t.disposed)){if(t.controller=null,h(t,u),n.persist!==void 0)try{a.storage?.write?.(n.persist,u)}catch{}return t.superseded?(t.superseded=!1,t.run(!0)):u}}).catch(u=>{i.signal.aborted||t.disposed||(t.controller=null,v(t,u))})};let s=n.persist,l=a.storage;return s!==void 0&&l?.read!==void 0&&(async()=>{try{let o=await l.read?.(s);o!==void 0&&t.data.peek()===void 0&&!t.disposed&&(h(t,o),t.loading.value=!0)}catch{}})(),j(()=>{t.run(!1)}),D(()=>{t.disposed=!0,t.controller?.abort(),n.persist,t.tags,r()}),C(t,r)}function _(e){let n=y(),a=x(n,void 0),t=null;return D(()=>{a.disposed=!0,t?.abort()}),{data:a.data,error:a.error,status:a.status,running:a.loading,run:async r=>{t?.abort(),t=new AbortController;let s=t,l=[];a.loading.value=!0,a.status.value="loading";try{let o=(...d)=>{l=d},i=await z(()=>e(r,{signal:s.signal,invalidates:o,request:{signal:s.signal,force:!0,tags:o}}));return s.signal.aborted||a.disposed?void 0:(h(a,i),l.length>0&&await n.invalidate(...l),i)}catch(o){if(s.signal.aborted||a.disposed)return;v(a,o);return}}}}function G(e,n={}){let a=y(),t=x(a,void 0),r=a.hold(t);t.run=()=>n.reload===void 0?Promise.resolve(void 0):K(n.reload()),t.loading.value=!0,t.status.value="loading";let s=e.subscribe({next:o=>{h(t,o)},error:o=>{v(t,o)}}),l=typeof s=="function"?s:()=>{s.unsubscribe()};return D(()=>{t.disposed=!0,l(),r()}),C(t,()=>{l(),r()})}function M(e){return V(()=>e())}async function K(e){await e}function k(e={}){let n=e.ttl??0,a=e.max??100,t=e.now??(()=>Date.now()),r=new Map,s=o=>{r.get(o)?.controller?.abort(),r.delete(o)},l=(o,i)=>{if(r.delete(o),r.set(o,i),r.size>a)for(let d of r.keys()){s(d);break}};return{get size(){return r.size},peek:o=>{let i=r.get(o);if(!(i===void 0||i.expires<=t()))return i.value},write:(o,i)=>{l(o,{value:i,expires:n===0?1/0:t()+n,inflight:null,controller:null,waiting:0})},forget:o=>{if(o===void 0){for(let i of[...r.keys()])s(i);return}s(o)},read:(o,i)=>async d=>{let c=r.get(o);if(d.force)s(o);else if(c!==void 0){if(c.inflight!==null)return await A(c,d);if(c.expires>t())return l(o,c),c.value;r.delete(o)}let p=new AbortController,u={value:void 0,expires:0,inflight:null,controller:p,waiting:0},T=i({signal:p.signal,force:d.force}).then(f=>(r.get(o)===u&&(n===0?r.delete(o):(u.value=f,u.expires=t()+n,u.inflight=null,u.controller=null)),f)).catch(f=>{throw r.get(o)===u&&r.delete(o),f});return u.inflight=T,l(o,u),await A(u,d)}}}async function A(e,n){e.waiting+=1;let a=()=>{e.waiting<=1&&e.inflight!==null&&e.controller?.abort()};n.signal.addEventListener("abort",a,{once:!0});try{return await e.inflight}finally{n.signal.removeEventListener("abort",a),e.waiting-=1}}var B=/^@(tag|invalidates)\b/,U=/\b(query|mutation|subscription)\b[^\S\n]*([A-Za-z_]\w*)?/,E=/^[_A-Za-z][_0-9A-Za-z]*/,g=class extends Error{constructor(n){super(n),this.name="FirsthandDirectiveError"}};function q(e,n){if(e.startsWith('"""',n)){let t=e.indexOf('"""',n+3);return t===-1?e.length:t+3}let a=n+1;for(;a<e.length&&e[a]!=='"';)a+=e[a]==="\\"?2:1;return a+1}function Z(e,n){let a=0,t=n;for(;t<e.length;){let r=e[t];if(r==='"'){t=q(e,t);continue}if(r==="("||r==="["||r==="{")a++;else if((r===")"||r==="]"||r==="}")&&(a--,a===0))return t+1;t++}throw new g(`unclosed arguments in ${e.slice(n,n+40)}`)}function W(e,n){try{return JSON.parse(e)}catch{throw new g(`@${n}: ${e} is not a valid string`)}}function X(e,n){let a={},t=0,r=()=>{for(;t<e.length&&/[\s,]/.test(e[t]);)t++};for(r();t<e.length;){let s=E.exec(e.slice(t));if(s===null)throw new g(`@${n}: expected an argument name at "${e.slice(t)}"`);if(t+=s[0].length,r(),e[t]!==":")throw new g(`@${n}: argument ${s[0]} has no value`);t++,r();let l=Y(e,n,t);a[s[0]]=l.value,t=l.next,r()}return a}function Y(e,n,a){let t=e[a];if(t==="$"){let i=E.exec(e.slice(a+1));if(i===null)throw new g(`@${n}: expected a variable name after $`);return{value:{variable:i[0]},next:a+1+i[0].length}}if(t==='"'){let i=q(e,a),d=e.slice(a,i);return{value:{literal:d.startsWith('"""')?d.slice(3,-3).trim():W(d,n)},next:i}}if(t==="["||t==="{")throw new g(`@${n}: a tag variable must be a scalar, not a list or object`);let r=/^[^\s,)]+/.exec(e.slice(a));if(r===null)throw new g(`@${n}: expected a value`);let s=r[0],l=a+s.length;if(s==="true"||s==="false")return{value:{literal:s==="true"},next:l};if(s==="null")return{value:{literal:null},next:l};let o=Number(s);return{value:{literal:Number.isNaN(o)?s:o},next:l}}function ee(e){let n=[],a=[],t="",r=0,s=0;for(;r<e.length;){let l=e[r];if(l==='"'){r=q(e,r);continue}if(l==="#"){let f=e.indexOf(`
2
- `,r);r=f===-1?e.length:f;continue}if(l!=="@"){r++;continue}let o=B.exec(e.slice(r));if(o===null){r++;continue}let i=r+o[0].length,d="",c=i;for(;c<e.length&&/\s/.test(e[c]);)c++;if(e[c]==="("){let f=Z(e,c);d=e.slice(c+1,f-1),i=f}let p=X(d,o[1]),u=p.name;if(u===void 0||!("literal"in u)||typeof u.literal!="string")throw new g(`@${o[1]} needs a literal name, as in @${o[1]}(name: "user", id: $id)`);delete p.name,(o[1]==="tag"?n:a).push({name:u.literal,vars:p});let T=r;for(;T>s&&/\s/.test(e[T-1]);)T--;t+=e.slice(s,T),s=i,r=i}return{tags:n,invalidates:a,stripped:t+e.slice(s)}}function te(e){let{tags:n,invalidates:a,stripped:t}=ee(e),r=U.exec(t.replace(/#[^\n]*/g,""));return{source:t,operation:r?.[2]??"",kind:r?.[1]??"query",tags:n,invalidates:a}}function ne(e,n){return e.map(a=>{let t={};for(let[r,s]of Object.entries(a.vars)){if("literal"in s){t[r]=s.literal;continue}let l=n[s.variable];l!=null&&(t[r]=typeof l=="object"?JSON.stringify(l):l)}return R(a.name,t)})}import{untrack as re}from"@firsthandjs/core";var w=class extends Error{constructor(a,t,r){super(`HTTP ${String(a)} for ${t}`);this.status=a;this.url=t;this.body=r;this.name="FirsthandHttpError"}status;url;body};function ae(e,n,a){let t=/^[a-z]+:\/\//i.test(n)?n:`${e.baseUrl??""}${n}`,{json:r,cacheKey:s,...l}=a,o={...e.init,...l},i=new Headers(typeof e.headers=="function"?re(e.headers):e.headers);for(let[d,c]of new Headers(a.headers??{}))i.set(d,c);return r!==void 0&&(o.body=JSON.stringify(r),i.has("content-type")||i.set("content-type","application/json")),o.headers=i,[t,o]}async function I(e,n,a,t){let s=await(e.fetch??globalThis.fetch)(n,{...a,signal:t.signal}),l=await s.text(),o=l===""?void 0:JSON.parse(l);if(!s.ok)throw new w(s.status,n,o);return o}function $(e={}){let n=e.cache===void 0||e.cache===!1?void 0:"read"in e.cache?e.cache:k(e.cache),a={cache:n,request:(t,r={})=>async s=>{let[l,o]=ae(e,t,r),i=(o.method??"GET").toUpperCase(),d=typeof r.cacheKey=="string"?r.cacheKey:i==="GET"||i==="HEAD"?`${i} ${l}`:void 0;return n===void 0||r.cacheKey===!1||d===void 0?await I(e,l,o,s):await n.read(d,c=>I(e,l,o,c))(s)},get:(t,r={})=>a.request(t,r),post:(t,r={})=>a.request(t,{method:"POST",...r}),put:(t,r={})=>a.request(t,{method:"PUT",...r}),patch:(t,r={})=>a.request(t,{method:"PATCH",...r}),remove:(t,r={})=>a.request(t,{method:"DELETE",...r}),with:t=>$({...e,...t,cache:t.cache??n??!1})};return a}export{P as DataContext,g as FirsthandDirectiveError,w as FirsthandHttpError,m as anyTagMatches,k as createCacheClient,S as createData,$ as createFetchClient,G as fromObservable,M as fromPromise,te as parseGraphQL,ne as resolveTags,R as tag,O as tagMatches,_ as useAction,y as useData,Q as useInvalidate,V as useResource};
1
+ import{createContext as z,effect as N,onCleanup as D,untrack as Q,useContext as _}from"@firsthandjs/core";import{batch as S,signal as b}from"@firsthandjs/core";var J=Object.freeze({});function R(e,r=J){return{name:e,vars:r}}function L(e,r){if(e.name!==r.name)return!1;for(let n of Object.keys(e.vars))if(!Object.is(e.vars[n],r.vars[n]))return!1;return!0}function m(e,r){for(let n of e)for(let t of r)if(L(n,t))return!0;return!1}function P(e={}){let r=new Set;return{storage:e.storage,hold:n=>(r.add(n),()=>r.delete(n)),invalidate:async(...n)=>{let t=[];for(let a of[...r])a.controller!==null&&a.pending.push(...n),m(n,a.tags)&&(a.name,a.tags,t.push(a.run(!0)));await Promise.all(t)},clear:()=>{for(let n of[...r])n.controller?.abort(),r.delete(n);try{e.storage?.clear?.()}catch{}},get size(){return r.size}}}function x(e,r){return{data:b(void 0),error:b(void 0),status:b("idle"),loading:b(!1),tags:[],controller:null,pending:[],superseded:!1,disposed:!1,name:r}}function h(e,r){S(()=>{e.data.value=r,e.error.value=void 0,e.status.value="success",e.loading.value=!1})}function v(e,r){S(()=>{e.error.value=r,e.status.value="error",e.loading.value=!1})}function C(e,r){return{data:e.data,error:e.error,status:e.status,loading:e.loading,reload:()=>e.run(!0),dispose:()=>{e.disposed=!0,e.controller?.abort(),r()}}}var A=z();function y(){return _(A).value}function G(){let e=y();return(...r)=>e.invalidate(...r)}function V(e,r={}){let n=y(),t=x(n,r.persist),a=n.hold(t);r.persist,t.run=o=>{if(t.disposed)return Promise.resolve(void 0);t.controller?.abort();let i=new AbortController;t.controller=i,t.pending=[],t.superseded=!1,t.loading.value=!0,t.data.peek()===void 0&&(t.status.value="loading");let u=(...d)=>{t.tags=d,t.pending.length>0&&m(t.pending,d)&&(t.superseded=!0)},c={signal:i.signal,force:o,tags:u},g={...c,request:c,tags:u};return e(g).then(async d=>{if(!(i.signal.aborted||t.disposed)){if(t.controller=null,h(t,d),r.persist!==void 0)try{n.storage?.write?.(r.persist,d)}catch{}return t.superseded?(t.superseded=!1,t.run(!0)):d}}).catch(d=>{i.signal.aborted||t.disposed||(t.controller=null,v(t,d))})};let s=r.persist,l=n.storage;return s!==void 0&&l?.read!==void 0&&(async()=>{try{let o=await l.read?.(s);o!==void 0&&t.data.peek()===void 0&&!t.disposed&&(h(t,o),t.loading.value=!0)}catch{}})(),N(()=>{t.run(!1)}),D(()=>{t.disposed=!0,t.controller?.abort(),r.persist,t.tags,a()}),C(t,a)}function M(e){let r=y(),n=x(r,void 0),t=null;return D(()=>{n.disposed=!0,t?.abort()}),{data:n.data,error:n.error,status:n.status,running:n.loading,run:async a=>{t?.abort(),t=new AbortController;let s=t,l=[];n.loading.value=!0,n.status.value="loading";try{let o=(...u)=>{l=u},i=await Q(()=>e(a,{signal:s.signal,invalidates:o,request:{signal:s.signal,force:!0,tags:o}}));return s.signal.aborted||n.disposed?void 0:(h(n,i),l.length>0&&await r.invalidate(...l),i)}catch(o){if(s.signal.aborted||n.disposed)return;v(n,o);return}}}}function K(e,r={}){let n=y(),t=x(n,void 0),a=n.hold(t);t.run=()=>r.reload===void 0?Promise.resolve(void 0):U(r.reload()),t.loading.value=!0,t.status.value="loading";let s=e.subscribe({next:o=>{h(t,o)},error:o=>{v(t,o)}}),l=typeof s=="function"?s:()=>{s.unsubscribe()};return D(()=>{t.disposed=!0,l(),a()}),C(t,()=>{l(),a()})}function B(e){return V(()=>e())}async function U(e){await e}function k(e){return e===void 0?"undefined":e===null||typeof e!="object"?JSON.stringify(e):Array.isArray(e)?`[${e.map(k).join(",")}]`:e instanceof Date?e.toISOString():`{${Object.entries(e).filter(([,n])=>n!==void 0).sort(([n],[t])=>n<t?-1:1).map(([n,t])=>`${n}:${k(t)}`).join(",")}}`}function O(e={}){let r=e.ttl??0,n=e.max??100,t=e.now??(()=>Date.now()),a=new Map,s=o=>{a.get(o)?.controller?.abort(),a.delete(o)},l=(o,i)=>{if(a.delete(o),a.set(o,i),a.size>n)for(let u of a.keys()){s(u);break}};return{get size(){return a.size},peek:o=>{let i=a.get(o);if(!(i===void 0||i.expires<=t()))return i.value},write:(o,i)=>{l(o,{value:i,expires:r===0?1/0:t()+r,inflight:null,controller:null,waiting:0})},forget:o=>{if(o===void 0){for(let i of[...a.keys()])s(i);return}s(o)},read:(o,i)=>async u=>{let c=a.get(o);if(u.force)s(o);else if(c!==void 0){if(c.inflight!==null)return await $(c,u);if(c.expires>t())return l(o,c),c.value;a.delete(o)}let g=new AbortController,d={value:void 0,expires:0,inflight:null,controller:g,waiting:0},T=i({signal:g.signal,force:u.force}).then(p=>(a.get(o)===d&&(r===0?a.delete(o):(d.value=p,d.expires=t()+r,d.inflight=null,d.controller=null)),p)).catch(p=>{throw a.get(o)===d&&a.delete(o),p});return d.inflight=T,l(o,d),await $(d,u)}}}async function $(e,r){e.waiting+=1;let n=()=>{e.waiting<=1&&e.inflight!==null&&e.controller?.abort()};r.signal.addEventListener("abort",n,{once:!0});try{return await e.inflight}finally{r.signal.removeEventListener("abort",n),e.waiting-=1}}var Z=/^@(tag|invalidates)\b/,W=/\b(query|mutation|subscription)\b[^\S\n]*([A-Za-z_]\w*)?/,E=/^[_A-Za-z][_0-9A-Za-z]*/,f=class extends Error{constructor(r){super(r),this.name="FirsthandDirectiveError"}};function q(e,r){if(e.startsWith('"""',r)){let t=e.indexOf('"""',r+3);return t===-1?e.length:t+3}let n=r+1;for(;n<e.length&&e[n]!=='"';)n+=e[n]==="\\"?2:1;return n+1}function X(e,r){let n=0,t=r;for(;t<e.length;){let a=e[t];if(a==='"'){t=q(e,t);continue}if(a==="("||a==="["||a==="{")n++;else if((a===")"||a==="]"||a==="}")&&(n--,n===0))return t+1;t++}throw new f(`unclosed arguments in ${e.slice(r,r+40)}`)}function Y(e,r){try{return JSON.parse(e)}catch{throw new f(`@${r}: ${e} is not a valid string`)}}function ee(e,r){let n={},t=0,a=()=>{for(;t<e.length&&/[\s,]/.test(e[t]);)t++};for(a();t<e.length;){let s=E.exec(e.slice(t));if(s===null)throw new f(`@${r}: expected an argument name at "${e.slice(t)}"`);if(t+=s[0].length,a(),e[t]!==":")throw new f(`@${r}: argument ${s[0]} has no value`);t++,a();let l=te(e,r,t);n[s[0]]=l.value,t=l.next,a()}return n}function te(e,r,n){let t=e[n];if(t==="$"){let i=E.exec(e.slice(n+1));if(i===null)throw new f(`@${r}: expected a variable name after $`);return{value:{variable:i[0]},next:n+1+i[0].length}}if(t==='"'){let i=q(e,n),u=e.slice(n,i);return{value:{literal:u.startsWith('"""')?u.slice(3,-3).trim():Y(u,r)},next:i}}if(t==="["||t==="{")throw new f(`@${r}: a tag variable must be a scalar, not a list or object`);let a=/^[^\s,)]+/.exec(e.slice(n));if(a===null)throw new f(`@${r}: expected a value`);let s=a[0],l=n+s.length;if(s==="true"||s==="false")return{value:{literal:s==="true"},next:l};if(s==="null")return{value:{literal:null},next:l};let o=Number(s);return{value:{literal:Number.isNaN(o)?s:o},next:l}}function ne(e){let r=[],n=[],t="",a=0,s=0;for(;a<e.length;){let l=e[a];if(l==='"'){a=q(e,a);continue}if(l==="#"){let p=e.indexOf(`
2
+ `,a);a=p===-1?e.length:p;continue}if(l!=="@"){a++;continue}let o=Z.exec(e.slice(a));if(o===null){a++;continue}let i=a+o[0].length,u="",c=i;for(;c<e.length&&/\s/.test(e[c]);)c++;if(e[c]==="("){let p=X(e,c);u=e.slice(c+1,p-1),i=p}let g=ee(u,o[1]),d=g.name;if(d===void 0||!("literal"in d)||typeof d.literal!="string")throw new f(`@${o[1]} needs a literal name, as in @${o[1]}(name: "user", id: $id)`);delete g.name,(o[1]==="tag"?r:n).push({name:d.literal,vars:g});let T=a;for(;T>s&&/\s/.test(e[T-1]);)T--;t+=e.slice(s,T),s=i,a=i}return{tags:r,invalidates:n,stripped:t+e.slice(s)}}function re(e){let{tags:r,invalidates:n,stripped:t}=ne(e),a=W.exec(t.replace(/#[^\n]*/g,""));return{source:t,operation:a?.[2]??"",kind:a?.[1]??"query",tags:r,invalidates:n}}function ae(e,r){return e.map(n=>{let t={};for(let[a,s]of Object.entries(n.vars)){if("literal"in s){t[a]=s.literal;continue}let l=r[s.variable];l!=null&&(t[a]=typeof l=="object"?JSON.stringify(l):l)}return R(n.name,t)})}import{untrack as j}from"@firsthandjs/core";var w=class extends Error{constructor(n,t,a){super(`HTTP ${String(n)} for ${t}`);this.status=n;this.url=t;this.body=a;this.name="FirsthandHttpError"}status;url;body};function oe(e,r,n){let t=/^[a-z]+:\/\//i.test(r)?r:`${e.baseUrl??""}${r}`,{json:a,cacheKey:s,...l}=n,o={...e.init,...l},i=new Headers(typeof e.headers=="function"?j(e.headers):e.headers);for(let[u,c]of new Headers(n.headers??{}))i.set(u,c);return a!==void 0&&(o.body=JSON.stringify(a),i.has("content-type")||i.set("content-type","application/json")),o.headers=i,[t,o]}async function I(e,r,n,t){let s=await(e.fetch??globalThis.fetch)(r,{...n,signal:t.signal}),l=await s.text(),o=l===""?void 0:JSON.parse(l);if(!s.ok)throw new w(s.status,r,o);return o}function H(e={}){let r=e.cache===void 0||e.cache===!1?void 0:"read"in e.cache?e.cache:O(e.cache),n={cache:r,request:(t,a={})=>async s=>{let[l,o]=oe(e,t,a),i=(o.method??"GET").toUpperCase(),u=e.scope===void 0?o.headers.get("authorization")??"":j(e.scope),c=typeof a.cacheKey=="string"?a.cacheKey:i==="GET"||i==="HEAD"?`${i} ${l}`:void 0,g=c===void 0?void 0:`${u}\0${c}`;return r===void 0||a.cacheKey===!1||g===void 0?await I(e,l,o,s):await r.read(g,d=>I(e,l,o,d))(s)},get:(t,a={})=>n.request(t,a),post:(t,a={})=>n.request(t,{method:"POST",...a}),put:(t,a={})=>n.request(t,{method:"PUT",...a}),patch:(t,a={})=>n.request(t,{method:"PATCH",...a}),remove:(t,a={})=>n.request(t,{method:"DELETE",...a}),with:t=>H({...e,...t,cache:t.cache??r??!1})};return n}export{A as DataContext,f as FirsthandDirectiveError,w as FirsthandHttpError,m as anyTagMatches,O as createCacheClient,P as createData,H as createFetchClient,K as fromObservable,B as fromPromise,re as parseGraphQL,ae as resolveTags,k as stableKey,R as tag,L as tagMatches,M as useAction,y as useData,G as useInvalidate,V as useResource};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@firsthandjs/data",
3
- "version": "0.6.0",
3
+ "version": "0.6.2",
4
4
  "description": "Resources, actions and invalidation for Firsthand: reactive async state, with the cache one layer down.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -28,8 +28,8 @@
28
28
  "LICENSE"
29
29
  ],
30
30
  "dependencies": {
31
- "@firsthandjs/core": "0.6.0",
32
- "@firsthandjs/dom": "0.6.0"
31
+ "@firsthandjs/core": "0.6.2",
32
+ "@firsthandjs/dom": "0.6.2"
33
33
  },
34
34
  "engines": {
35
35
  "node": ">=20.11.0"