@craftainer/crud-sdk 0.7.1 → 0.8.1
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 +21 -1
- package/dist/cache/index.mjs +70 -10
- package/dist/cache/index.mjs.map +1 -1
- package/dist/{client-CzbFW5YG.mjs → client-DikBkRMm.mjs} +7 -214
- package/dist/client-DikBkRMm.mjs.map +1 -0
- package/dist/codegen/cli.mjs +32 -16
- package/dist/codegen/cli.mjs.map +1 -1
- package/dist/errors-ZdOt0_zk.mjs +232 -0
- package/dist/errors-ZdOt0_zk.mjs.map +1 -0
- package/dist/{events-Dfz-rzC8.mjs → events-swCOTQOS.mjs} +27 -8
- package/dist/{events-Dfz-rzC8.mjs.map → events-swCOTQOS.mjs.map} +1 -1
- package/dist/index.mjs +2 -1
- package/dist/tanstack/index.mjs +1 -1
- package/package.json +1 -1
- package/dist/client-CzbFW5YG.mjs.map +0 -1
package/README.md
CHANGED
|
@@ -127,7 +127,7 @@ await heroes.update(1, { name: "Ada" }, { ifMatch: etag }); // 412 -> Preconditi
|
|
|
127
127
|
return next({ ...ctx, request: new Request(ctx.request, { headers }) });
|
|
128
128
|
};
|
|
129
129
|
const client = createClient({
|
|
130
|
-
baseUrl:
|
|
130
|
+
baseUrl: `${location.origin}/api/crud`, // absolute: a relative baseUrl throws
|
|
131
131
|
credentials: "same-origin", // sends the session cookie
|
|
132
132
|
cacheScope: () => currentUser.sub, // string, or sync/async function
|
|
133
133
|
onUnauthorized: () => location.assign("/bff/login"),
|
|
@@ -164,6 +164,16 @@ await heroes.update(1, { name: "Ada" }, { ifMatch: etag }); // 412 -> Preconditi
|
|
|
164
164
|
property.
|
|
165
165
|
- There is no `headers` option: add static headers such as `X-CSRF` with a
|
|
166
166
|
middleware, as above.
|
|
167
|
+
- **Logout and user switch.** The SDK does not notice that the session
|
|
168
|
+
changed. On logout, or whenever the `cacheScope` value changes, the app
|
|
169
|
+
must call `cache.clear()` (the `./cache` object; this also drops the
|
|
170
|
+
remembered `If-Match` validators and, with `IndexedDbStore`, the persisted
|
|
171
|
+
entries) and tear down its event streams (call every unsubscribe function
|
|
172
|
+
that `subscribeEvents` / `invalidateOnEvents` returned, or reload the page), so the next user neither reads the previous
|
|
173
|
+
user's data nor receives events from the previous user's stream.
|
|
174
|
+
- `Error.message` is bounded (server `detail` is cut at 200 characters, the
|
|
175
|
+
URL has no query string); the full text stays on `.detail` / `.errors`.
|
|
176
|
+
Treat it as a diagnostic, not as text to show to users.
|
|
167
177
|
|
|
168
178
|
- **Middleware.** > [!WARNING]
|
|
169
179
|
> Middleware must carry context fields through: build the next context with
|
|
@@ -226,6 +236,12 @@ objects, snake_case members).
|
|
|
226
236
|
as a read answered `404`/`410` does. A bulk write of unknown outcome clears
|
|
227
237
|
the whole resource, as its ids are not known. A write aborted before it
|
|
228
238
|
was sent clears nothing.
|
|
239
|
+
- A `no-store` response (the BFF default) is never stored, but its `ETag` is
|
|
240
|
+
remembered per record and scope (an LRU of 1024) for `If-Match` only:
|
|
241
|
+
`cachedEtag` and `autoIfMatch` use it, a read never does. A newer `ETag`
|
|
242
|
+
replaces it; a write, `412`, `404`/`410`, `DELETE`, an SSE invalidation,
|
|
243
|
+
`clear` or a scope switch drops it (see
|
|
244
|
+
[ADR 0004](docs/adrs/0004-remember-validators-of-no-store-responses.md)).
|
|
229
245
|
- Wire the stream with `invalidateOnEvents(cache, getEventHub({ fetch, url,
|
|
230
246
|
events }), "heroes")`; `fetch` must be authenticated. Events invalidate and
|
|
231
247
|
never apply snapshots. `subscribeEvents` is the lower-level entry.
|
|
@@ -237,6 +253,10 @@ events }), "heroes")`; `fetch` must be authenticated. Events invalidate and
|
|
|
237
253
|
same manifest `events` object to every subscriber of one stream.
|
|
238
254
|
- A `keepalive_seconds` that is zero, negative or not finite is treated as
|
|
239
255
|
unset: the 15 s default applies (a stream silent for three intervals is dead).
|
|
256
|
+
The timer keeps running while handlers do and restarts per event, so a
|
|
257
|
+
handler that takes longer than the dead interval (never settles, say)
|
|
258
|
+
ends the connection (the hub reconnects). A line or event longer than 1 MiB
|
|
259
|
+
also ends it.
|
|
240
260
|
- Stores: `MemoryStore` (LRU) and `IndexedDbStore` (store failures are a miss).
|
|
241
261
|
`IndexedDbStore` keeps an `expiresAt` index (database version 2; a version 1
|
|
242
262
|
database upgrades in place and keeps its entries), deletes every expired
|
package/dist/cache/index.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { n as cacheScope, t as ANON } from "../auth-C5__H0Ln.mjs";
|
|
2
|
-
import { a as tanstackCachePolicy, i as cachePolicyFromManifest, n as subscribeEvents, r as resetOnce, t as getEventHub } from "../events-
|
|
2
|
+
import { a as tanstackCachePolicy, i as cachePolicyFromManifest, n as subscribeEvents, r as resetOnce, t as getEventHub } from "../events-swCOTQOS.mjs";
|
|
3
3
|
//#region src/cache/entry.ts
|
|
4
4
|
const statuses = /* @__PURE__ */ new WeakMap();
|
|
5
5
|
/**
|
|
@@ -189,6 +189,8 @@ async function contextScope(ctx) {
|
|
|
189
189
|
//#endregion
|
|
190
190
|
//#region src/cache/middleware.ts
|
|
191
191
|
const MIN_RETENTION_MS = 36e5;
|
|
192
|
+
/** Bound on remembered validators (LRU), the same as `MemoryStore`'s default size. */
|
|
193
|
+
const MAX_VALIDATORS = 1024;
|
|
192
194
|
const MANIFEST_POLICY = cachePolicyFromManifest({});
|
|
193
195
|
const WRITE_METHODS = /* @__PURE__ */ new Set([
|
|
194
196
|
"POST",
|
|
@@ -249,6 +251,24 @@ function createCache(options) {
|
|
|
249
251
|
const inflight = /* @__PURE__ */ new Set();
|
|
250
252
|
const tasks = /* @__PURE__ */ new Set();
|
|
251
253
|
const background = new AbortController();
|
|
254
|
+
/**
|
|
255
|
+
* The last ETag seen per record key from any successful response, `no-store`
|
|
256
|
+
* included. RFC 9111 forbids storing such a response, not reading its
|
|
257
|
+
* validator, and the validator is used only for `If-Match` (`cachedEtag`,
|
|
258
|
+
* `autoIfMatch`), never to serve or revalidate content. Map order is the
|
|
259
|
+
* recency order. Keys carry the caller scope, so scopes stay isolated;
|
|
260
|
+
* cleared by the same prefixes as the store.
|
|
261
|
+
*/
|
|
262
|
+
const validators = /* @__PURE__ */ new Map();
|
|
263
|
+
const rememberValidator = (key, etag) => {
|
|
264
|
+
validators.delete(key);
|
|
265
|
+
if (etag === void 0) return;
|
|
266
|
+
validators.set(key, etag);
|
|
267
|
+
while (validators.size > MAX_VALIDATORS) validators.delete(validators.keys().next().value);
|
|
268
|
+
};
|
|
269
|
+
const forgetValidators = (prefixes) => {
|
|
270
|
+
for (const key of validators.keys()) if (prefixes === void 0 || prefixes.some((prefix) => key.startsWith(prefix))) validators.delete(key);
|
|
271
|
+
};
|
|
252
272
|
let globalGeneration = 0;
|
|
253
273
|
const generation = (resource) => (generations.get(resource ?? "") ?? 0) + globalGeneration;
|
|
254
274
|
const bump = (resource) => {
|
|
@@ -365,6 +385,12 @@ function createCache(options) {
|
|
|
365
385
|
]);
|
|
366
386
|
return clears([...names].sort().map((name) => kindPrefix(name, "filters")));
|
|
367
387
|
};
|
|
388
|
+
/** The validator op for a record read or write response (none for other kinds). */
|
|
389
|
+
const validatorOps = (state, response, key) => [{
|
|
390
|
+
type: "validator",
|
|
391
|
+
key,
|
|
392
|
+
etag: state.policy.etag ? response.headers.get("etag") ?? void 0 : void 0
|
|
393
|
+
}];
|
|
368
394
|
const readPlan = (state, response, body) => {
|
|
369
395
|
const key = state.key;
|
|
370
396
|
const kind = state.kind;
|
|
@@ -372,6 +398,13 @@ function createCache(options) {
|
|
|
372
398
|
const at = now();
|
|
373
399
|
const { fresh, stale } = entryTimes(state, kind);
|
|
374
400
|
const old = state.entry;
|
|
401
|
+
const vkey = buildKey({
|
|
402
|
+
resource: state.ctx.resource,
|
|
403
|
+
kind: "record",
|
|
404
|
+
recordId: state.ctx.recordId,
|
|
405
|
+
scope: state.scope,
|
|
406
|
+
apiVersion: state.ctx.apiVersion
|
|
407
|
+
});
|
|
375
408
|
if (status === 304 && state.validated === true && old !== void 0) {
|
|
376
409
|
const updated = {
|
|
377
410
|
...old,
|
|
@@ -388,7 +421,11 @@ function createCache(options) {
|
|
|
388
421
|
key,
|
|
389
422
|
entry: updated,
|
|
390
423
|
ttlMs: retentionMs(fresh, stale)
|
|
391
|
-
}
|
|
424
|
+
}, ...kind === "record" ? [{
|
|
425
|
+
type: "validator",
|
|
426
|
+
key: vkey,
|
|
427
|
+
etag: updated.etag
|
|
428
|
+
}] : []]
|
|
392
429
|
};
|
|
393
430
|
}
|
|
394
431
|
if (status === 200 && body !== void 0) {
|
|
@@ -410,7 +447,7 @@ function createCache(options) {
|
|
|
410
447
|
key,
|
|
411
448
|
entry,
|
|
412
449
|
ttlMs: retentionMs(fresh, stale)
|
|
413
|
-
}];
|
|
450
|
+
}, ...kind === "record" ? validatorOps(state, response, vkey) : []];
|
|
414
451
|
if (kind === "manifest") ops.push(...manifestOps(old, entry));
|
|
415
452
|
return {
|
|
416
453
|
result: response,
|
|
@@ -422,7 +459,7 @@ function createCache(options) {
|
|
|
422
459
|
ops: [{
|
|
423
460
|
type: "delete",
|
|
424
461
|
key
|
|
425
|
-
}]
|
|
462
|
+
}, ...kind === "record" && status === 200 ? validatorOps(state, response, vkey) : []]
|
|
426
463
|
};
|
|
427
464
|
return {
|
|
428
465
|
result: response,
|
|
@@ -473,7 +510,13 @@ function createCache(options) {
|
|
|
473
510
|
...collections,
|
|
474
511
|
revisionsPrefix(resource, recordId),
|
|
475
512
|
recordPrefix(resource, recordId)
|
|
476
|
-
]), ...op === "update" && response !== void 0 && ok ? writeThrough(state, response, body, resource, recordId)
|
|
513
|
+
]), ...op === "update" && response !== void 0 && ok ? [...writeThrough(state, response, body, resource, recordId), ...validatorOps(state, response, buildKey({
|
|
514
|
+
resource,
|
|
515
|
+
kind: "record",
|
|
516
|
+
recordId,
|
|
517
|
+
scope: state.scope,
|
|
518
|
+
apiVersion: state.ctx.apiVersion
|
|
519
|
+
}))] : []];
|
|
477
520
|
if (ok && (BULK_OPS.has(op) || op === "restore")) {
|
|
478
521
|
const ids = idsFromBody(body);
|
|
479
522
|
if (ids !== void 0) return clears([...collections, ...ids.flatMap((id) => [recordPrefix(resource, id), revisionsPrefix(resource, id)])]);
|
|
@@ -482,13 +525,19 @@ function createCache(options) {
|
|
|
482
525
|
};
|
|
483
526
|
/** One pass when the store can, else one `clear` per prefix; a failure is a miss. */
|
|
484
527
|
const clearPrefixes = async (prefixes) => {
|
|
528
|
+
forgetValidators(prefixes);
|
|
485
529
|
if (store.clearMany !== void 0) {
|
|
486
530
|
await safe(() => store.clearMany?.(prefixes));
|
|
487
531
|
return;
|
|
488
532
|
}
|
|
489
533
|
for (const prefix of prefixes) await safe(() => store.clear(prefix));
|
|
490
534
|
};
|
|
491
|
-
const apply = async (op) => {
|
|
535
|
+
const apply = async (op, keepValidator = false) => {
|
|
536
|
+
if (op.type === "validator") {
|
|
537
|
+
rememberValidator(op.key, op.etag);
|
|
538
|
+
return;
|
|
539
|
+
}
|
|
540
|
+
if (op.type === "delete" && !keepValidator) validators.delete(op.key);
|
|
492
541
|
await safe(() => op.type === "clear" ? clearPrefixes(op.prefixes) : op.type === "set" ? store.set(op.key, op.entry, op.ttlMs) : store.delete(op.key));
|
|
493
542
|
};
|
|
494
543
|
/** Plan, then apply, the store changes a response calls for. */
|
|
@@ -505,8 +554,12 @@ function createCache(options) {
|
|
|
505
554
|
const applyOps = async (state, ops) => {
|
|
506
555
|
if (state.kind === void 0 && ops.some((op) => op.type === "clear")) bump(state.ctx.resource);
|
|
507
556
|
const expected = generation(state.ctx.resource);
|
|
508
|
-
|
|
509
|
-
|
|
557
|
+
const stale = state.kind !== void 0 && expected !== state.generation;
|
|
558
|
+
for (const op of ops) {
|
|
559
|
+
if (stale && op.type === "validator") continue;
|
|
560
|
+
await apply(op, stale);
|
|
561
|
+
}
|
|
562
|
+
if (generation(state.ctx.resource) !== expected) for (const op of ops.filter((candidate) => candidate.type !== "clear")) await apply({
|
|
510
563
|
type: "delete",
|
|
511
564
|
key: op.key
|
|
512
565
|
});
|
|
@@ -588,13 +641,19 @@ function createCache(options) {
|
|
|
588
641
|
return next(ctx);
|
|
589
642
|
};
|
|
590
643
|
const lookupEtag = async (resource, recordId, scope, apiVersion) => {
|
|
591
|
-
|
|
644
|
+
const key = buildKey({
|
|
592
645
|
resource,
|
|
593
646
|
kind: "record",
|
|
594
647
|
recordId,
|
|
595
648
|
scope,
|
|
596
649
|
apiVersion
|
|
597
|
-
})
|
|
650
|
+
});
|
|
651
|
+
const remembered = validators.get(key);
|
|
652
|
+
if (remembered !== void 0) {
|
|
653
|
+
rememberValidator(key, remembered);
|
|
654
|
+
return remembered;
|
|
655
|
+
}
|
|
656
|
+
return (await safe(() => store.get(key)))?.etag;
|
|
598
657
|
};
|
|
599
658
|
return {
|
|
600
659
|
middleware,
|
|
@@ -614,6 +673,7 @@ function createCache(options) {
|
|
|
614
673
|
},
|
|
615
674
|
async clear(prefix) {
|
|
616
675
|
globalGeneration += 1;
|
|
676
|
+
forgetValidators(prefix === void 0 ? void 0 : [prefix]);
|
|
617
677
|
await safe(() => store.clear(prefix));
|
|
618
678
|
},
|
|
619
679
|
cachedEtag: lookupEtag,
|