@craftainer/crud-sdk 0.0.0-stage → 0.1.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/LICENSE +674 -0
- package/README.md +176 -2
- package/dist/auth-DwtNEKrw.mjs +22 -0
- package/dist/auth-DwtNEKrw.mjs.map +1 -0
- package/dist/cache/index.d.mts +166 -0
- package/dist/cache/index.mjs +972 -0
- package/dist/cache/index.mjs.map +1 -0
- package/dist/client-BLd126Jt.d.mts +303 -0
- package/dist/client-D0drumeA.mjs +913 -0
- package/dist/client-D0drumeA.mjs.map +1 -0
- package/dist/codegen/cli.d.mts +1 -0
- package/dist/codegen/cli.mjs +131 -0
- package/dist/codegen/cli.mjs.map +1 -0
- package/dist/events-Cqj9Rbon.mjs +616 -0
- package/dist/events-Cqj9Rbon.mjs.map +1 -0
- package/dist/events-DpSWzSN9.d.mts +88 -0
- package/dist/index.d.mts +98 -0
- package/dist/index.mjs +3 -0
- package/dist/interfaces-s73cVfIF.d.mts +109 -0
- package/dist/tanstack/index.d.mts +182 -0
- package/dist/tanstack/index.mjs +226 -0
- package/dist/tanstack/index.mjs.map +1 -0
- package/package.json +80 -4
|
@@ -0,0 +1,972 @@
|
|
|
1
|
+
import { n as cacheScope, t as ANON } from "../auth-DwtNEKrw.mjs";
|
|
2
|
+
import { i as tanstackCachePolicy, n as subscribeEvents, r as cachePolicyFromManifest, t as getEventHub } from "../events-Cqj9Rbon.mjs";
|
|
3
|
+
//#region src/cache/entry.ts
|
|
4
|
+
const statuses = /* @__PURE__ */ new WeakMap();
|
|
5
|
+
/**
|
|
6
|
+
* How the cache answered `response`: `hit` (fresh, no network), `stale`
|
|
7
|
+
* (inside the stale-while-revalidate window, refreshed in the background),
|
|
8
|
+
* `revalidated` (a `304` rebuilt from the stored entry) or `miss`.
|
|
9
|
+
* `undefined` when the cache did not handle the request.
|
|
10
|
+
*/
|
|
11
|
+
function cacheStatus(response) {
|
|
12
|
+
return statuses.get(response);
|
|
13
|
+
}
|
|
14
|
+
/** Tag `response` with how the cache answered. */
|
|
15
|
+
function tagStatus(response, status) {
|
|
16
|
+
statuses.set(response, status);
|
|
17
|
+
return response;
|
|
18
|
+
}
|
|
19
|
+
const TRANSFER_HEADERS = /* @__PURE__ */ new Set([
|
|
20
|
+
"content-length",
|
|
21
|
+
"content-encoding",
|
|
22
|
+
"transfer-encoding",
|
|
23
|
+
"connection",
|
|
24
|
+
"keep-alive"
|
|
25
|
+
]);
|
|
26
|
+
const VARY_OK = /* @__PURE__ */ new Set([
|
|
27
|
+
"authorization",
|
|
28
|
+
"accept",
|
|
29
|
+
"accept-encoding"
|
|
30
|
+
]);
|
|
31
|
+
/** The lower-cased directive names of the `Cache-Control` header. */
|
|
32
|
+
function cacheControl(headers) {
|
|
33
|
+
const names = (headers.get("cache-control") ?? "").split(",").map((part) => part.split("=", 1)[0].trim().toLowerCase()).filter((name) => name !== "");
|
|
34
|
+
return new Set(names);
|
|
35
|
+
}
|
|
36
|
+
/** Whether the media type is JSON (`application/json` or `+json`). */
|
|
37
|
+
function isJson(response) {
|
|
38
|
+
const media = (response.headers.get("content-type") ?? "").split(";", 1)[0].trim().toLowerCase();
|
|
39
|
+
return media === "application/json" || media.endsWith("+json");
|
|
40
|
+
}
|
|
41
|
+
/** `Vary` may only name headers the key already accounts for; `*` never caches. */
|
|
42
|
+
function varyAllowsCaching(response) {
|
|
43
|
+
return (response.headers.get("vary") ?? "").split(",").map((part) => part.trim().toLowerCase()).filter((name) => name !== "").every((name) => VARY_OK.has(name));
|
|
44
|
+
}
|
|
45
|
+
function storableHeaders(headers) {
|
|
46
|
+
const items = [];
|
|
47
|
+
for (const [name, value] of headers) if (!TRANSFER_HEADERS.has(name)) items.push([name, value]);
|
|
48
|
+
return items;
|
|
49
|
+
}
|
|
50
|
+
/** Snapshot a read response whose body text was already read. */
|
|
51
|
+
function buildEntry(response, body, times, keepEtag) {
|
|
52
|
+
const etag = keepEtag ? response.headers.get("etag") : null;
|
|
53
|
+
const base = {
|
|
54
|
+
status: response.status,
|
|
55
|
+
headers: storableHeaders(response.headers),
|
|
56
|
+
body,
|
|
57
|
+
storedAt: times.now,
|
|
58
|
+
freshUntil: times.now + times.fresh,
|
|
59
|
+
staleUntil: times.now + times.fresh + times.stale
|
|
60
|
+
};
|
|
61
|
+
return etag === null ? base : {
|
|
62
|
+
...base,
|
|
63
|
+
etag
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
/** RFC 9111 4.3.4: headers of a `304` replace the stored ones of the same name. */
|
|
67
|
+
function mergeHeaders(stored, update) {
|
|
68
|
+
const fresh = storableHeaders(update);
|
|
69
|
+
const replaced = new Set(fresh.map(([name]) => name));
|
|
70
|
+
return [...stored.filter(([name]) => !replaced.has(name)), ...fresh];
|
|
71
|
+
}
|
|
72
|
+
/** Rebuild a response from an entry, tagged with how the cache produced it. */
|
|
73
|
+
function toResponse(entry, status) {
|
|
74
|
+
const headers = new Headers();
|
|
75
|
+
for (const [name, value] of entry.headers) headers.append(name, value);
|
|
76
|
+
return tagStatus(new Response(entry.body, {
|
|
77
|
+
status: entry.status,
|
|
78
|
+
headers
|
|
79
|
+
}), status);
|
|
80
|
+
}
|
|
81
|
+
//#endregion
|
|
82
|
+
//#region src/cache/keys.ts
|
|
83
|
+
const KINDS = /* @__PURE__ */ new Set([
|
|
84
|
+
"list",
|
|
85
|
+
"record",
|
|
86
|
+
"stats",
|
|
87
|
+
"predict",
|
|
88
|
+
"revisions",
|
|
89
|
+
"filters"
|
|
90
|
+
]);
|
|
91
|
+
const STATS_KINDS = /* @__PURE__ */ new Set(["stats", "predict"]);
|
|
92
|
+
const COLLECTION_KINDS = [
|
|
93
|
+
"list",
|
|
94
|
+
"stats",
|
|
95
|
+
"predict"
|
|
96
|
+
];
|
|
97
|
+
const PER_RECORD_KINDS = ["record", "revisions"];
|
|
98
|
+
/** `true` when the op is a cacheable read kind. */
|
|
99
|
+
function isCacheKind(op) {
|
|
100
|
+
return KINDS.has(op);
|
|
101
|
+
}
|
|
102
|
+
/** `true` for `stats` and `predict`, which `stats_ttl` bounds. */
|
|
103
|
+
function isStatsKind(kind) {
|
|
104
|
+
return STATS_KINDS.has(kind);
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Sorts the query pairs the client sent and drops `id` (it is the subject).
|
|
108
|
+
* Values are kept verbatim: the client already emits canonical,
|
|
109
|
+
* percent-encoded text, so nothing is decoded or re-encoded here.
|
|
110
|
+
*/
|
|
111
|
+
function canonicalQuery(query) {
|
|
112
|
+
const pairs = (query.startsWith("?") ? query.slice(1) : query).split("&").filter((part) => part !== "" && part.split("=", 1)[0] !== "id").map((part) => {
|
|
113
|
+
const at = part.indexOf("=");
|
|
114
|
+
return at < 0 ? [part, ""] : [part.slice(0, at), part.slice(at)];
|
|
115
|
+
});
|
|
116
|
+
pairs.sort(([an, av], [bn, bv]) => an === bn ? av < bv ? -1 : av > bv ? 1 : 0 : an < bn ? -1 : 1);
|
|
117
|
+
return pairs.map(([name, rest]) => name + rest).join("&");
|
|
118
|
+
}
|
|
119
|
+
/** The key of one cached response. */
|
|
120
|
+
function buildKey(parts) {
|
|
121
|
+
const subject = (parts.kind === "record" || parts.kind === "revisions") && parts.recordId !== void 0 ? String(parts.recordId) : "_";
|
|
122
|
+
const version = parts.apiVersion ?? "_";
|
|
123
|
+
return `v1/${parts.resource}/${parts.kind}/${subject}/${version}/${parts.scope}/${canonicalQuery(parts.query ?? "")}`;
|
|
124
|
+
}
|
|
125
|
+
/** Key of the manifest document for one caller scope. */
|
|
126
|
+
function manifestKey(scope) {
|
|
127
|
+
return `v1/_manifest/${scope}`;
|
|
128
|
+
}
|
|
129
|
+
/** Prefix of all entries of one kind for `resource` (every scope and subject). */
|
|
130
|
+
function kindPrefix(resource, kind) {
|
|
131
|
+
return `v1/${resource}/${kind}/`;
|
|
132
|
+
}
|
|
133
|
+
/** Prefix of the `record` entries of one record (every version, scope, query). */
|
|
134
|
+
function recordPrefix(resource, recordId) {
|
|
135
|
+
return `v1/${resource}/record/${recordId}/`;
|
|
136
|
+
}
|
|
137
|
+
/** Prefix of the `revisions` entries of one record. */
|
|
138
|
+
function revisionsPrefix(resource, recordId) {
|
|
139
|
+
return `v1/${resource}/revisions/${recordId}/`;
|
|
140
|
+
}
|
|
141
|
+
/** The list, stats and predict prefixes of `resource`. */
|
|
142
|
+
function collectionPrefixes(resource) {
|
|
143
|
+
return COLLECTION_KINDS.map((kind) => kindPrefix(resource, kind));
|
|
144
|
+
}
|
|
145
|
+
/** Everything cached for `resource` except `filters` (a reconnect or an unknown change). */
|
|
146
|
+
function resourcePrefixes(resource) {
|
|
147
|
+
return [...COLLECTION_KINDS, ...PER_RECORD_KINDS].map((kind) => kindPrefix(resource, kind));
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* Prefixes an SSE event clears, per the spec's table: `create` clears the
|
|
151
|
+
* collection kinds; `update`/`delete`/`restore` also clear the record and its
|
|
152
|
+
* revisions; an unknown action or a missing record id clears the resource.
|
|
153
|
+
*/
|
|
154
|
+
function eventPrefixes(resource, action, recordId) {
|
|
155
|
+
if (action === "create") return collectionPrefixes(resource);
|
|
156
|
+
if ((action === "update" || action === "delete" || action === "restore") && recordId !== void 0) return [
|
|
157
|
+
...collectionPrefixes(resource),
|
|
158
|
+
recordPrefix(resource, recordId),
|
|
159
|
+
revisionsPrefix(resource, recordId)
|
|
160
|
+
];
|
|
161
|
+
return resourcePrefixes(resource);
|
|
162
|
+
}
|
|
163
|
+
//#endregion
|
|
164
|
+
//#region src/cache/scope.ts
|
|
165
|
+
/**
|
|
166
|
+
* `"anon"` when the request has no Authorization header; otherwise the first
|
|
167
|
+
* 32 hex characters of the SHA-256 of the whole credential, the same digest as
|
|
168
|
+
* the client's `cacheScope(token)` (a `Bearer ` prefix is dropped, any other value is hashed as it is). It is read from the
|
|
169
|
+
* request the cache sees, so it must be the innermost middleware that matters:
|
|
170
|
+
* whatever sets Authorization has to run before the cache. The raw credential
|
|
171
|
+
* never enters a key or a log, and the unverified `sub` claim is never used.
|
|
172
|
+
*/
|
|
173
|
+
async function credentialScope(headers) {
|
|
174
|
+
const value = headers.get("authorization");
|
|
175
|
+
if (value === null) return ANON;
|
|
176
|
+
const credential = /^bearer\s+(.+)$/i.exec(value)?.[1] ?? value;
|
|
177
|
+
return cacheScope(credential);
|
|
178
|
+
}
|
|
179
|
+
//#endregion
|
|
180
|
+
//#region src/cache/middleware.ts
|
|
181
|
+
const MIN_RETENTION_MS = 36e5;
|
|
182
|
+
const MANIFEST_POLICY = cachePolicyFromManifest({});
|
|
183
|
+
const WRITE_METHODS = /* @__PURE__ */ new Set([
|
|
184
|
+
"POST",
|
|
185
|
+
"PATCH",
|
|
186
|
+
"PUT",
|
|
187
|
+
"DELETE"
|
|
188
|
+
]);
|
|
189
|
+
const BULK_OPS = /* @__PURE__ */ new Set(["bulk_update", "bulk_delete"]);
|
|
190
|
+
const RECORD_WRITES = /* @__PURE__ */ new Set([
|
|
191
|
+
"delete",
|
|
192
|
+
"publish",
|
|
193
|
+
"restore"
|
|
194
|
+
]);
|
|
195
|
+
const CREATES = /* @__PURE__ */ new Set([
|
|
196
|
+
"create",
|
|
197
|
+
"clone",
|
|
198
|
+
"draft"
|
|
199
|
+
]);
|
|
200
|
+
function isStorable(response) {
|
|
201
|
+
return isJson(response) && !cacheControl(response.headers).has("no-store") && varyAllowsCaching(response);
|
|
202
|
+
}
|
|
203
|
+
function retentionMs(freshMs, staleMs) {
|
|
204
|
+
return Math.max(freshMs + staleMs, MIN_RETENTION_MS);
|
|
205
|
+
}
|
|
206
|
+
/** The ids of a bulk response (`{matched, ids}`), or `undefined` if unreadable. */
|
|
207
|
+
function idsFromBody(body) {
|
|
208
|
+
try {
|
|
209
|
+
const ids = JSON.parse(body).ids;
|
|
210
|
+
return Array.isArray(ids) && ids.every((id) => Number.isInteger(id)) ? ids : void 0;
|
|
211
|
+
} catch {
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
/** The resource names in a manifest document, or none if unreadable. */
|
|
216
|
+
function manifestResources(body) {
|
|
217
|
+
try {
|
|
218
|
+
const resources = JSON.parse(body).resources;
|
|
219
|
+
return Array.isArray(resources) ? resources.map((item) => item.name).filter((name) => typeof name === "string") : [];
|
|
220
|
+
} catch {
|
|
221
|
+
return [];
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
async function safe(call) {
|
|
225
|
+
try {
|
|
226
|
+
return await call();
|
|
227
|
+
} catch {
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
function createCache(options) {
|
|
232
|
+
const { store } = options;
|
|
233
|
+
const policies = new Map(Object.entries(options.policies));
|
|
234
|
+
const policyFor = (resource, apiVersion) => resource === void 0 ? void 0 : policies.get(`${resource}@${apiVersion}`) ?? policies.get(resource);
|
|
235
|
+
const cacheManifest = options.cacheManifest ?? true;
|
|
236
|
+
const now = options.now ?? Date.now;
|
|
237
|
+
const eventsActive = /* @__PURE__ */ new Set();
|
|
238
|
+
const generations = /* @__PURE__ */ new Map();
|
|
239
|
+
const inflight = /* @__PURE__ */ new Set();
|
|
240
|
+
const tasks = /* @__PURE__ */ new Set();
|
|
241
|
+
const background = new AbortController();
|
|
242
|
+
let globalGeneration = 0;
|
|
243
|
+
const generation = (resource) => (generations.get(resource ?? "") ?? 0) + globalGeneration;
|
|
244
|
+
const bump = (resource) => {
|
|
245
|
+
generations.set(resource, generation(resource) + 1);
|
|
246
|
+
};
|
|
247
|
+
/**
|
|
248
|
+
* Whether an entry of `kind` may be served without the network right now:
|
|
249
|
+
* only while the stream is up, except `stats`/`predict`, which `stats_ttl`
|
|
250
|
+
* bounds on its own. Never `filters` (events never clear them) or the manifest.
|
|
251
|
+
*/
|
|
252
|
+
const gateOpen = (policy, resource, kind) => !policy.revalidateOnly && kind !== "manifest" && kind !== "filters" && (isStatsKind(kind) || eventsActive.has(resource));
|
|
253
|
+
/** Stale-while-revalidate needs the stream for every kind, stats and predict included. */
|
|
254
|
+
const staleOpen = (policy, resource, kind) => gateOpen(policy, resource, kind) && eventsActive.has(resource);
|
|
255
|
+
/** Milliseconds fresh and of stale-while-revalidate for a new entry; none if the response may predate an invalidation. */
|
|
256
|
+
const entryTimes = (state, kind) => {
|
|
257
|
+
const resource = state.ctx.resource;
|
|
258
|
+
if (generation(resource) !== state.generation || !gateOpen(state.policy, resource, kind)) return {
|
|
259
|
+
fresh: 0,
|
|
260
|
+
stale: 0
|
|
261
|
+
};
|
|
262
|
+
return {
|
|
263
|
+
fresh: (isStatsKind(kind) ? state.policy.statsTtl : state.policy.maxAge) * 1e3,
|
|
264
|
+
stale: staleOpen(state.policy, resource, kind) ? state.policy.staleWhileRevalidate * 1e3 : 0
|
|
265
|
+
};
|
|
266
|
+
};
|
|
267
|
+
const decide = (state, entry) => {
|
|
268
|
+
if (entry === void 0) return "miss";
|
|
269
|
+
if (!state.forceRevalidate && gateOpen(state.policy, state.ctx.resource, state.kind)) {
|
|
270
|
+
const at = now();
|
|
271
|
+
if (at < entry.freshUntil) return "hit";
|
|
272
|
+
if (at < entry.staleUntil && staleOpen(state.policy, state.ctx.resource, state.kind)) return "stale";
|
|
273
|
+
}
|
|
274
|
+
return entry.etag !== void 0 && state.policy.etag ? "revalidate" : "miss";
|
|
275
|
+
};
|
|
276
|
+
const begin = async (ctx) => {
|
|
277
|
+
const { request, resource } = ctx;
|
|
278
|
+
const scope = await credentialScope(request.headers);
|
|
279
|
+
const generationNow = generation(resource);
|
|
280
|
+
if (request.method !== "GET") {
|
|
281
|
+
if (!WRITE_METHODS.has(request.method)) return;
|
|
282
|
+
const policy = policyFor(resource, ctx.apiVersion);
|
|
283
|
+
return policy?.enabled === true ? {
|
|
284
|
+
ctx,
|
|
285
|
+
policy,
|
|
286
|
+
kind: void 0,
|
|
287
|
+
key: void 0,
|
|
288
|
+
generation: generationNow,
|
|
289
|
+
scope,
|
|
290
|
+
forceRevalidate: false
|
|
291
|
+
} : void 0;
|
|
292
|
+
}
|
|
293
|
+
let policy;
|
|
294
|
+
let kind;
|
|
295
|
+
let key;
|
|
296
|
+
if (ctx.op === "manifest") {
|
|
297
|
+
if (!cacheManifest) return;
|
|
298
|
+
policy = MANIFEST_POLICY;
|
|
299
|
+
kind = "manifest";
|
|
300
|
+
key = manifestKey(scope);
|
|
301
|
+
} else {
|
|
302
|
+
policy = policyFor(resource, ctx.apiVersion);
|
|
303
|
+
if (resource === void 0 || policy?.enabled !== true || !isCacheKind(ctx.op)) return;
|
|
304
|
+
kind = ctx.op;
|
|
305
|
+
key = buildKey({
|
|
306
|
+
resource,
|
|
307
|
+
kind,
|
|
308
|
+
recordId: ctx.recordId,
|
|
309
|
+
scope,
|
|
310
|
+
query: new URL(request.url).search,
|
|
311
|
+
apiVersion: ctx.apiVersion
|
|
312
|
+
});
|
|
313
|
+
}
|
|
314
|
+
const control = cacheControl(request.headers);
|
|
315
|
+
if (control.has("no-store") || request.headers.has("if-none-match")) return;
|
|
316
|
+
return {
|
|
317
|
+
ctx,
|
|
318
|
+
policy,
|
|
319
|
+
kind,
|
|
320
|
+
key,
|
|
321
|
+
generation: generationNow,
|
|
322
|
+
scope,
|
|
323
|
+
forceRevalidate: control.has("no-cache")
|
|
324
|
+
};
|
|
325
|
+
};
|
|
326
|
+
/** A copy of `ctx` whose request carries our validator (a `GET` has no body to lose). */
|
|
327
|
+
const withValidator = (ctx, etag) => {
|
|
328
|
+
const headers = new Headers(ctx.request.headers);
|
|
329
|
+
headers.set("if-none-match", etag);
|
|
330
|
+
return {
|
|
331
|
+
...ctx,
|
|
332
|
+
request: new Request(ctx.request, { headers })
|
|
333
|
+
};
|
|
334
|
+
};
|
|
335
|
+
const readBody = async (state, response) => {
|
|
336
|
+
const { status } = response;
|
|
337
|
+
const { op, recordId } = state.ctx;
|
|
338
|
+
let wanted;
|
|
339
|
+
if (state.kind !== void 0) wanted = status === 200 && isStorable(response);
|
|
340
|
+
else if (op === "update") wanted = status === 200 && recordId !== void 0 && isStorable(response);
|
|
341
|
+
else wanted = status >= 200 && status < 300 && (BULK_OPS.has(op) || op === "restore" && recordId === void 0);
|
|
342
|
+
return wanted ? response.clone().text() : void 0;
|
|
343
|
+
};
|
|
344
|
+
/** One clear op for all `prefixes`: the store clears them in a single pass. */
|
|
345
|
+
const clears = (prefixes) => [{
|
|
346
|
+
type: "clear",
|
|
347
|
+
prefixes
|
|
348
|
+
}];
|
|
349
|
+
const manifestOps = (old, entry) => {
|
|
350
|
+
if (old === void 0 || old.etag !== void 0 && old.etag === entry.etag) return [];
|
|
351
|
+
const names = /* @__PURE__ */ new Set([
|
|
352
|
+
...[...policies.keys()].map((key) => key.replace(/@[^@]*$/, "")),
|
|
353
|
+
...manifestResources(old.body),
|
|
354
|
+
...manifestResources(entry.body)
|
|
355
|
+
]);
|
|
356
|
+
return clears([...names].sort().map((name) => kindPrefix(name, "filters")));
|
|
357
|
+
};
|
|
358
|
+
const readPlan = (state, response, body) => {
|
|
359
|
+
const key = state.key;
|
|
360
|
+
const kind = state.kind;
|
|
361
|
+
const { status } = response;
|
|
362
|
+
const at = now();
|
|
363
|
+
const { fresh, stale } = entryTimes(state, kind);
|
|
364
|
+
const old = state.entry;
|
|
365
|
+
if (status === 304 && state.validated === true && old !== void 0) {
|
|
366
|
+
const updated = {
|
|
367
|
+
...old,
|
|
368
|
+
headers: mergeHeaders(old.headers, response.headers),
|
|
369
|
+
storedAt: at,
|
|
370
|
+
freshUntil: at + fresh,
|
|
371
|
+
staleUntil: at + fresh + stale,
|
|
372
|
+
etag: response.headers.get("etag") ?? old.etag
|
|
373
|
+
};
|
|
374
|
+
return {
|
|
375
|
+
result: toResponse(updated, "revalidated"),
|
|
376
|
+
ops: [{
|
|
377
|
+
type: "set",
|
|
378
|
+
key,
|
|
379
|
+
entry: updated,
|
|
380
|
+
ttlMs: retentionMs(fresh, stale)
|
|
381
|
+
}]
|
|
382
|
+
};
|
|
383
|
+
}
|
|
384
|
+
if (status === 200 && body !== void 0) {
|
|
385
|
+
tagStatus(response, "miss");
|
|
386
|
+
const entry = buildEntry(response, body, {
|
|
387
|
+
now: at,
|
|
388
|
+
fresh,
|
|
389
|
+
stale
|
|
390
|
+
}, state.policy.etag);
|
|
391
|
+
if (entry.etag === void 0 && fresh <= 0) return {
|
|
392
|
+
result: response,
|
|
393
|
+
ops: [{
|
|
394
|
+
type: "delete",
|
|
395
|
+
key
|
|
396
|
+
}]
|
|
397
|
+
};
|
|
398
|
+
const ops = [{
|
|
399
|
+
type: "set",
|
|
400
|
+
key,
|
|
401
|
+
entry,
|
|
402
|
+
ttlMs: retentionMs(fresh, stale)
|
|
403
|
+
}];
|
|
404
|
+
if (kind === "manifest") ops.push(...manifestOps(old, entry));
|
|
405
|
+
return {
|
|
406
|
+
result: response,
|
|
407
|
+
ops
|
|
408
|
+
};
|
|
409
|
+
}
|
|
410
|
+
if (status === 200 || status === 404 || status === 410) return {
|
|
411
|
+
result: response,
|
|
412
|
+
ops: [{
|
|
413
|
+
type: "delete",
|
|
414
|
+
key
|
|
415
|
+
}]
|
|
416
|
+
};
|
|
417
|
+
return {
|
|
418
|
+
result: response,
|
|
419
|
+
ops: []
|
|
420
|
+
};
|
|
421
|
+
};
|
|
422
|
+
/** Store the PATCH response as the record's entry (it is the read view plus ETag). */
|
|
423
|
+
const writeThrough = (state, response, body, resource, recordId) => {
|
|
424
|
+
if (body === void 0) return [];
|
|
425
|
+
const { fresh, stale } = entryTimes(state, "record");
|
|
426
|
+
const entry = buildEntry(response, body, {
|
|
427
|
+
now: now(),
|
|
428
|
+
fresh,
|
|
429
|
+
stale
|
|
430
|
+
}, state.policy.etag);
|
|
431
|
+
if (entry.etag === void 0 && fresh <= 0) return [];
|
|
432
|
+
return [{
|
|
433
|
+
type: "set",
|
|
434
|
+
key: buildKey({
|
|
435
|
+
resource,
|
|
436
|
+
kind: "record",
|
|
437
|
+
recordId,
|
|
438
|
+
scope: state.scope,
|
|
439
|
+
apiVersion: state.ctx.apiVersion
|
|
440
|
+
}),
|
|
441
|
+
entry,
|
|
442
|
+
ttlMs: retentionMs(fresh, stale)
|
|
443
|
+
}];
|
|
444
|
+
};
|
|
445
|
+
const writeOps = (state, response, body) => {
|
|
446
|
+
const { op, recordId } = state.ctx;
|
|
447
|
+
const resource = state.ctx.resource;
|
|
448
|
+
const { status } = response;
|
|
449
|
+
if (status === 412) return recordId === void 0 ? [] : clears([recordPrefix(resource, recordId)]);
|
|
450
|
+
if (status < 200 || status >= 300) return [];
|
|
451
|
+
const collections = collectionPrefixes(resource);
|
|
452
|
+
if (CREATES.has(op)) return clears(collections);
|
|
453
|
+
if (recordId !== void 0 && (RECORD_WRITES.has(op) || op === "update")) return [...clears([
|
|
454
|
+
...collections,
|
|
455
|
+
revisionsPrefix(resource, recordId),
|
|
456
|
+
recordPrefix(resource, recordId)
|
|
457
|
+
]), ...op === "update" ? writeThrough(state, response, body, resource, recordId) : []];
|
|
458
|
+
if (BULK_OPS.has(op) || op === "restore") {
|
|
459
|
+
const ids = idsFromBody(body);
|
|
460
|
+
if (ids !== void 0) return clears([...collections, ...ids.flatMap((id) => [recordPrefix(resource, id), revisionsPrefix(resource, id)])]);
|
|
461
|
+
}
|
|
462
|
+
return clears(resourcePrefixes(resource));
|
|
463
|
+
};
|
|
464
|
+
/** One pass when the store can, else one `clear` per prefix; a failure is a miss. */
|
|
465
|
+
const clearPrefixes = async (prefixes) => {
|
|
466
|
+
if (store.clearMany !== void 0) {
|
|
467
|
+
await safe(() => store.clearMany?.(prefixes));
|
|
468
|
+
return;
|
|
469
|
+
}
|
|
470
|
+
for (const prefix of prefixes) await safe(() => store.clear(prefix));
|
|
471
|
+
};
|
|
472
|
+
const apply = async (op) => {
|
|
473
|
+
await safe(() => op.type === "clear" ? clearPrefixes(op.prefixes) : op.type === "set" ? store.set(op.key, op.entry, op.ttlMs) : store.delete(op.key));
|
|
474
|
+
};
|
|
475
|
+
/** Plan, then apply, the store changes a response calls for. */
|
|
476
|
+
const finish = async (state, response) => {
|
|
477
|
+
const body = await readBody(state, response);
|
|
478
|
+
const { result, ops } = state.kind === void 0 ? {
|
|
479
|
+
result: response,
|
|
480
|
+
ops: writeOps(state, response, body)
|
|
481
|
+
} : readPlan(state, response, body);
|
|
482
|
+
if (state.kind === void 0 && ops.some((op) => op.type === "clear")) bump(state.ctx.resource);
|
|
483
|
+
const expected = generation(state.ctx.resource);
|
|
484
|
+
for (const op of ops) await apply(op);
|
|
485
|
+
if (generation(state.ctx.resource) !== expected) for (const op of ops.filter((candidate) => candidate.type === "set")) await apply({
|
|
486
|
+
type: "delete",
|
|
487
|
+
key: op.key
|
|
488
|
+
});
|
|
489
|
+
return result;
|
|
490
|
+
};
|
|
491
|
+
const schedule = (state, next) => {
|
|
492
|
+
const key = state.key;
|
|
493
|
+
if (background.signal.aborted || inflight.has(key)) return;
|
|
494
|
+
inflight.add(key);
|
|
495
|
+
const entry = state.entry;
|
|
496
|
+
const task = (async () => {
|
|
497
|
+
try {
|
|
498
|
+
const signal = background.signal;
|
|
499
|
+
const headers = new Headers(state.ctx.request.headers);
|
|
500
|
+
if (entry.etag !== void 0) headers.set("if-none-match", entry.etag);
|
|
501
|
+
const request = new Request(state.ctx.request.url, {
|
|
502
|
+
method: "GET",
|
|
503
|
+
headers,
|
|
504
|
+
signal
|
|
505
|
+
});
|
|
506
|
+
const fresh = {
|
|
507
|
+
...state,
|
|
508
|
+
validated: entry.etag !== void 0
|
|
509
|
+
};
|
|
510
|
+
const response = await next({
|
|
511
|
+
...state.ctx,
|
|
512
|
+
request,
|
|
513
|
+
signal
|
|
514
|
+
});
|
|
515
|
+
await (await finish(fresh, response)).body?.cancel();
|
|
516
|
+
} catch {} finally {
|
|
517
|
+
inflight.delete(key);
|
|
518
|
+
}
|
|
519
|
+
})();
|
|
520
|
+
tasks.add(task);
|
|
521
|
+
task.then(() => tasks.delete(task));
|
|
522
|
+
};
|
|
523
|
+
const middleware = async (ctx, next) => {
|
|
524
|
+
const state = await begin(ctx);
|
|
525
|
+
if (state === void 0) return next(ctx);
|
|
526
|
+
if (state.kind === void 0) return finish(state, await next(ctx));
|
|
527
|
+
state.entry = await safe(() => store.get(state.key));
|
|
528
|
+
const decision = decide(state, state.entry);
|
|
529
|
+
const entry = state.entry;
|
|
530
|
+
if (decision === "hit") return toResponse(entry, "hit");
|
|
531
|
+
if (decision === "stale") {
|
|
532
|
+
schedule(state, next);
|
|
533
|
+
return toResponse(entry, "stale");
|
|
534
|
+
}
|
|
535
|
+
let sent = ctx;
|
|
536
|
+
if (decision === "revalidate") {
|
|
537
|
+
sent = withValidator(ctx, entry.etag);
|
|
538
|
+
state.validated = true;
|
|
539
|
+
}
|
|
540
|
+
return finish(state, await next(sent));
|
|
541
|
+
};
|
|
542
|
+
const autoIfMatch = async (ctx, next) => {
|
|
543
|
+
const { request, resource, recordId } = ctx;
|
|
544
|
+
if ((ctx.op === "update" || ctx.op === "delete") && recordId !== void 0 && resource !== void 0 && policyFor(resource, ctx.apiVersion)?.enabled === true && !request.headers.has("if-match")) {
|
|
545
|
+
const etag = await lookupEtag(resource, recordId, await credentialScope(request.headers), ctx.apiVersion);
|
|
546
|
+
if (etag !== void 0) {
|
|
547
|
+
const headers = new Headers(request.headers);
|
|
548
|
+
headers.set("if-match", etag);
|
|
549
|
+
return next({
|
|
550
|
+
...ctx,
|
|
551
|
+
request: new Request(request, { headers })
|
|
552
|
+
});
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
return next(ctx);
|
|
556
|
+
};
|
|
557
|
+
const lookupEtag = async (resource, recordId, scope, apiVersion) => {
|
|
558
|
+
return (await safe(() => store.get(buildKey({
|
|
559
|
+
resource,
|
|
560
|
+
kind: "record",
|
|
561
|
+
recordId,
|
|
562
|
+
scope,
|
|
563
|
+
apiVersion
|
|
564
|
+
}))))?.etag;
|
|
565
|
+
};
|
|
566
|
+
return {
|
|
567
|
+
middleware,
|
|
568
|
+
autoIfMatch,
|
|
569
|
+
setEventsActive(resource, active) {
|
|
570
|
+
if (active) eventsActive.add(resource);
|
|
571
|
+
else eventsActive.delete(resource);
|
|
572
|
+
},
|
|
573
|
+
eventsActive: (resource) => eventsActive.has(resource),
|
|
574
|
+
async invalidateEvent(resource, action, recordId) {
|
|
575
|
+
bump(resource);
|
|
576
|
+
await clearPrefixes(eventPrefixes(resource, action, recordId));
|
|
577
|
+
},
|
|
578
|
+
async clearResource(resource) {
|
|
579
|
+
bump(resource);
|
|
580
|
+
await clearPrefixes(resourcePrefixes(resource));
|
|
581
|
+
},
|
|
582
|
+
async clear(prefix) {
|
|
583
|
+
globalGeneration += 1;
|
|
584
|
+
await safe(() => store.clear(prefix));
|
|
585
|
+
},
|
|
586
|
+
cachedEtag: lookupEtag,
|
|
587
|
+
async close() {
|
|
588
|
+
background.abort();
|
|
589
|
+
await Promise.allSettled(tasks);
|
|
590
|
+
}
|
|
591
|
+
};
|
|
592
|
+
}
|
|
593
|
+
/** LRU store (default 1024 entries) that also honours each entry's ttl. */
|
|
594
|
+
var MemoryStore = class {
|
|
595
|
+
data = /* @__PURE__ */ new Map();
|
|
596
|
+
maxEntries;
|
|
597
|
+
defaultTtlMs;
|
|
598
|
+
now;
|
|
599
|
+
constructor(options = {}) {
|
|
600
|
+
const maxEntries = options.maxEntries ?? 1024;
|
|
601
|
+
if (!Number.isInteger(maxEntries) || maxEntries < 1) throw new RangeError("maxEntries must be an integer of at least 1");
|
|
602
|
+
this.maxEntries = maxEntries;
|
|
603
|
+
this.defaultTtlMs = options.defaultTtlMs ?? 36e5;
|
|
604
|
+
this.now = options.now ?? Date.now;
|
|
605
|
+
}
|
|
606
|
+
/** Number of stored entries (expired ones included until touched). */
|
|
607
|
+
get size() {
|
|
608
|
+
return this.data.size;
|
|
609
|
+
}
|
|
610
|
+
get(key) {
|
|
611
|
+
const item = this.data.get(key);
|
|
612
|
+
if (item === void 0) return;
|
|
613
|
+
if (item.expiresAt <= this.now()) {
|
|
614
|
+
this.data.delete(key);
|
|
615
|
+
return;
|
|
616
|
+
}
|
|
617
|
+
this.data.delete(key);
|
|
618
|
+
this.data.set(key, item);
|
|
619
|
+
return item.entry;
|
|
620
|
+
}
|
|
621
|
+
set(key, entry, ttlMs) {
|
|
622
|
+
this.data.delete(key);
|
|
623
|
+
this.data.set(key, {
|
|
624
|
+
expiresAt: this.now() + (ttlMs ?? this.defaultTtlMs),
|
|
625
|
+
entry
|
|
626
|
+
});
|
|
627
|
+
while (this.data.size > this.maxEntries) this.data.delete(this.data.keys().next().value);
|
|
628
|
+
}
|
|
629
|
+
delete(key) {
|
|
630
|
+
this.data.delete(key);
|
|
631
|
+
}
|
|
632
|
+
/** Remove every key under any of `prefixes` in one walk. */
|
|
633
|
+
clearMany(prefixes) {
|
|
634
|
+
for (const key of this.data.keys()) if (prefixes.some((prefix) => key.startsWith(prefix))) this.data.delete(key);
|
|
635
|
+
}
|
|
636
|
+
clear(prefix) {
|
|
637
|
+
for (const key of this.data.keys()) if (prefix === void 0 || key.startsWith(prefix)) this.data.delete(key);
|
|
638
|
+
}
|
|
639
|
+
};
|
|
640
|
+
//#endregion
|
|
641
|
+
//#region node_modules/.pnpm/idb@8.0.4/node_modules/idb/build/index.js
|
|
642
|
+
const instanceOfAny = (object, constructors) => constructors.some((c) => object instanceof c);
|
|
643
|
+
let idbProxyableTypes;
|
|
644
|
+
let cursorAdvanceMethods;
|
|
645
|
+
function getIdbProxyableTypes() {
|
|
646
|
+
return idbProxyableTypes || (idbProxyableTypes = [
|
|
647
|
+
IDBIndex,
|
|
648
|
+
IDBObjectStore,
|
|
649
|
+
IDBCursor,
|
|
650
|
+
IDBDatabase,
|
|
651
|
+
IDBTransaction
|
|
652
|
+
]);
|
|
653
|
+
}
|
|
654
|
+
function getCursorAdvanceMethods() {
|
|
655
|
+
return cursorAdvanceMethods || (cursorAdvanceMethods = [
|
|
656
|
+
"advance",
|
|
657
|
+
"continue",
|
|
658
|
+
"continuePrimaryKey"
|
|
659
|
+
].map((method) => IDBCursor.prototype[method]));
|
|
660
|
+
}
|
|
661
|
+
const transactionDoneMap = /* @__PURE__ */ new WeakMap();
|
|
662
|
+
const transformCache = /* @__PURE__ */ new WeakMap();
|
|
663
|
+
const reverseTransformCache = /* @__PURE__ */ new WeakMap();
|
|
664
|
+
function promisifyRequest(request) {
|
|
665
|
+
const promise = new Promise((resolve, reject) => {
|
|
666
|
+
const unlisten = () => {
|
|
667
|
+
request.removeEventListener("success", success);
|
|
668
|
+
request.removeEventListener("error", error);
|
|
669
|
+
};
|
|
670
|
+
const success = () => {
|
|
671
|
+
resolve(wrap(request.result));
|
|
672
|
+
unlisten();
|
|
673
|
+
};
|
|
674
|
+
const error = () => {
|
|
675
|
+
reject(request.error);
|
|
676
|
+
unlisten();
|
|
677
|
+
};
|
|
678
|
+
request.addEventListener("success", success);
|
|
679
|
+
request.addEventListener("error", error);
|
|
680
|
+
});
|
|
681
|
+
reverseTransformCache.set(promise, request);
|
|
682
|
+
return promise;
|
|
683
|
+
}
|
|
684
|
+
function cacheDonePromiseForTransaction(tx) {
|
|
685
|
+
if (transactionDoneMap.has(tx)) return;
|
|
686
|
+
const done = new Promise((resolve, reject) => {
|
|
687
|
+
const unlisten = () => {
|
|
688
|
+
tx.removeEventListener("complete", complete);
|
|
689
|
+
tx.removeEventListener("error", error);
|
|
690
|
+
tx.removeEventListener("abort", error);
|
|
691
|
+
};
|
|
692
|
+
const complete = () => {
|
|
693
|
+
resolve();
|
|
694
|
+
unlisten();
|
|
695
|
+
};
|
|
696
|
+
const error = () => {
|
|
697
|
+
reject(tx.error || new DOMException("AbortError", "AbortError"));
|
|
698
|
+
unlisten();
|
|
699
|
+
};
|
|
700
|
+
tx.addEventListener("complete", complete);
|
|
701
|
+
tx.addEventListener("error", error);
|
|
702
|
+
tx.addEventListener("abort", error);
|
|
703
|
+
});
|
|
704
|
+
transactionDoneMap.set(tx, done);
|
|
705
|
+
}
|
|
706
|
+
let idbProxyTraps = {
|
|
707
|
+
get(target, prop, receiver) {
|
|
708
|
+
if (target instanceof IDBTransaction) {
|
|
709
|
+
if (prop === "done") return transactionDoneMap.get(target);
|
|
710
|
+
if (prop === "store") return receiver.objectStoreNames[1] ? void 0 : receiver.objectStore(receiver.objectStoreNames[0]);
|
|
711
|
+
}
|
|
712
|
+
return wrap(target[prop]);
|
|
713
|
+
},
|
|
714
|
+
set(target, prop, value) {
|
|
715
|
+
target[prop] = value;
|
|
716
|
+
return true;
|
|
717
|
+
},
|
|
718
|
+
has(target, prop) {
|
|
719
|
+
if (target instanceof IDBTransaction && (prop === "done" || prop === "store")) return true;
|
|
720
|
+
return prop in target;
|
|
721
|
+
}
|
|
722
|
+
};
|
|
723
|
+
function replaceTraps(callback) {
|
|
724
|
+
idbProxyTraps = callback(idbProxyTraps);
|
|
725
|
+
}
|
|
726
|
+
function wrapFunction(func) {
|
|
727
|
+
if (getCursorAdvanceMethods().includes(func)) return function(...args) {
|
|
728
|
+
func.apply(unwrap(this), args);
|
|
729
|
+
return wrap(this.request);
|
|
730
|
+
};
|
|
731
|
+
return function(...args) {
|
|
732
|
+
return wrap(func.apply(unwrap(this), args));
|
|
733
|
+
};
|
|
734
|
+
}
|
|
735
|
+
function transformCachableValue(value) {
|
|
736
|
+
if (typeof value === "function") return wrapFunction(value);
|
|
737
|
+
if (value instanceof IDBTransaction) cacheDonePromiseForTransaction(value);
|
|
738
|
+
if (instanceOfAny(value, getIdbProxyableTypes())) return new Proxy(value, idbProxyTraps);
|
|
739
|
+
return value;
|
|
740
|
+
}
|
|
741
|
+
function wrap(value) {
|
|
742
|
+
if (value instanceof IDBRequest) return promisifyRequest(value);
|
|
743
|
+
if (transformCache.has(value)) return transformCache.get(value);
|
|
744
|
+
const newValue = transformCachableValue(value);
|
|
745
|
+
if (!Object.is(newValue, value)) {
|
|
746
|
+
transformCache.set(value, newValue);
|
|
747
|
+
reverseTransformCache.set(newValue, value);
|
|
748
|
+
}
|
|
749
|
+
return newValue;
|
|
750
|
+
}
|
|
751
|
+
const unwrap = (value) => reverseTransformCache.get(value);
|
|
752
|
+
/**
|
|
753
|
+
* Open a database.
|
|
754
|
+
*
|
|
755
|
+
* @param name Name of the database.
|
|
756
|
+
* @param version Schema version.
|
|
757
|
+
* @param callbacks Additional callbacks.
|
|
758
|
+
*/
|
|
759
|
+
function openDB(name, version, { blocked, upgrade, blocking, terminated } = {}) {
|
|
760
|
+
const request = indexedDB.open(name, version);
|
|
761
|
+
const openPromise = wrap(request);
|
|
762
|
+
if (upgrade) request.addEventListener("upgradeneeded", (event) => {
|
|
763
|
+
upgrade(wrap(request.result), event.oldVersion, event.newVersion, wrap(request.transaction), event);
|
|
764
|
+
});
|
|
765
|
+
if (blocked) request.addEventListener("blocked", (event) => blocked(event.oldVersion, event.newVersion, event));
|
|
766
|
+
openPromise.then((db) => {
|
|
767
|
+
if (terminated) db.addEventListener("close", () => terminated());
|
|
768
|
+
if (blocking) db.addEventListener("versionchange", (event) => blocking(event.oldVersion, event.newVersion, event));
|
|
769
|
+
}).catch(() => {});
|
|
770
|
+
return openPromise;
|
|
771
|
+
}
|
|
772
|
+
const readMethods = [
|
|
773
|
+
"get",
|
|
774
|
+
"getKey",
|
|
775
|
+
"getAll",
|
|
776
|
+
"getAllKeys",
|
|
777
|
+
"count"
|
|
778
|
+
];
|
|
779
|
+
const writeMethods = [
|
|
780
|
+
"put",
|
|
781
|
+
"add",
|
|
782
|
+
"delete",
|
|
783
|
+
"clear"
|
|
784
|
+
];
|
|
785
|
+
const cachedMethods = {};
|
|
786
|
+
function getMethod(target, prop) {
|
|
787
|
+
if (!(target instanceof IDBDatabase && !(prop in target) && typeof prop === "string")) return;
|
|
788
|
+
if (cachedMethods[prop]) return cachedMethods[prop];
|
|
789
|
+
const targetFuncName = prop.replace(/FromIndex$/, "");
|
|
790
|
+
const useIndex = prop !== targetFuncName;
|
|
791
|
+
const isWrite = writeMethods.includes(targetFuncName);
|
|
792
|
+
if (!(targetFuncName in (useIndex ? IDBIndex : IDBObjectStore).prototype) || !(isWrite || readMethods.includes(targetFuncName))) return;
|
|
793
|
+
const method = async function(storeName, ...args) {
|
|
794
|
+
const tx = this.transaction(storeName, isWrite ? "readwrite" : "readonly");
|
|
795
|
+
let target = tx.store;
|
|
796
|
+
if (useIndex) target = target.index(args.shift());
|
|
797
|
+
return (await Promise.all([target[targetFuncName](...args), isWrite && tx.done]))[0];
|
|
798
|
+
};
|
|
799
|
+
return cachedMethods[prop] = method;
|
|
800
|
+
}
|
|
801
|
+
replaceTraps((oldTraps) => ({
|
|
802
|
+
...oldTraps,
|
|
803
|
+
get: (target, prop, receiver) => getMethod(target, prop) || oldTraps.get(target, prop, receiver),
|
|
804
|
+
has: (target, prop) => !!getMethod(target, prop) || oldTraps.has(target, prop)
|
|
805
|
+
}));
|
|
806
|
+
const advanceMethodProps = [
|
|
807
|
+
"advance",
|
|
808
|
+
"continue",
|
|
809
|
+
"continuePrimaryKey"
|
|
810
|
+
];
|
|
811
|
+
const methodMap = {};
|
|
812
|
+
const advanceResults = /* @__PURE__ */ new WeakMap();
|
|
813
|
+
const ittrProxiedCursorToOriginalProxy = /* @__PURE__ */ new WeakMap();
|
|
814
|
+
const cursorIteratorTraps = { get(target, prop) {
|
|
815
|
+
if (!advanceMethodProps.includes(prop)) return target[prop];
|
|
816
|
+
let cachedFunc = methodMap[prop];
|
|
817
|
+
if (!cachedFunc) cachedFunc = methodMap[prop] = function(...args) {
|
|
818
|
+
advanceResults.set(this, ittrProxiedCursorToOriginalProxy.get(this)[prop](...args));
|
|
819
|
+
};
|
|
820
|
+
return cachedFunc;
|
|
821
|
+
} };
|
|
822
|
+
async function* iterate(...args) {
|
|
823
|
+
let cursor = this;
|
|
824
|
+
if (!(cursor instanceof IDBCursor)) cursor = await cursor.openCursor(...args);
|
|
825
|
+
if (!cursor) return;
|
|
826
|
+
cursor = cursor;
|
|
827
|
+
const proxiedCursor = new Proxy(cursor, cursorIteratorTraps);
|
|
828
|
+
ittrProxiedCursorToOriginalProxy.set(proxiedCursor, cursor);
|
|
829
|
+
reverseTransformCache.set(proxiedCursor, unwrap(cursor));
|
|
830
|
+
while (cursor) {
|
|
831
|
+
yield proxiedCursor;
|
|
832
|
+
cursor = await (advanceResults.get(proxiedCursor) || cursor.continue());
|
|
833
|
+
advanceResults.delete(proxiedCursor);
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
function isIteratorProp(target, prop) {
|
|
837
|
+
return prop === Symbol.asyncIterator && instanceOfAny(target, [
|
|
838
|
+
IDBIndex,
|
|
839
|
+
IDBObjectStore,
|
|
840
|
+
IDBCursor
|
|
841
|
+
]) || prop === "iterate" && instanceOfAny(target, [IDBIndex, IDBObjectStore]);
|
|
842
|
+
}
|
|
843
|
+
replaceTraps((oldTraps) => ({
|
|
844
|
+
...oldTraps,
|
|
845
|
+
get(target, prop, receiver) {
|
|
846
|
+
if (isIteratorProp(target, prop)) return iterate;
|
|
847
|
+
return oldTraps.get(target, prop, receiver);
|
|
848
|
+
},
|
|
849
|
+
has(target, prop) {
|
|
850
|
+
return isIteratorProp(target, prop) || oldTraps.has(target, prop);
|
|
851
|
+
}
|
|
852
|
+
}));
|
|
853
|
+
//#endregion
|
|
854
|
+
//#region src/cache/idb.ts
|
|
855
|
+
/**
|
|
856
|
+
* IndexedDB cache store (one object store, out-of-line string keys).
|
|
857
|
+
*
|
|
858
|
+
* Every call is wrapped: a blocked database, private mode or a missing
|
|
859
|
+
* `indexedDB` is a miss (or a no-op), never a failed request. `clear(prefix)`
|
|
860
|
+
* is one `IDBKeyRange.bound(prefix, prefix + "\uffff")` delete.
|
|
861
|
+
*/
|
|
862
|
+
const STORE = "entries";
|
|
863
|
+
var IndexedDbStore = class {
|
|
864
|
+
name;
|
|
865
|
+
defaultTtlMs;
|
|
866
|
+
now;
|
|
867
|
+
opening;
|
|
868
|
+
constructor(options = {}) {
|
|
869
|
+
this.name = options.name ?? "craftainer-crud-cache";
|
|
870
|
+
if (this.name === "") throw new RangeError("IndexedDbStore: name must not be empty");
|
|
871
|
+
this.defaultTtlMs = options.defaultTtlMs ?? 36e5;
|
|
872
|
+
this.now = options.now ?? Date.now;
|
|
873
|
+
}
|
|
874
|
+
open() {
|
|
875
|
+
this.opening ??= openDB(this.name, 1, { upgrade(db) {
|
|
876
|
+
db.createObjectStore(STORE);
|
|
877
|
+
} });
|
|
878
|
+
return this.opening;
|
|
879
|
+
}
|
|
880
|
+
/** Run `work` on the database; a failure drops the handle (so a later call retries) and yields `fallback`. */
|
|
881
|
+
async run(fallback, work) {
|
|
882
|
+
try {
|
|
883
|
+
return await work(await this.open());
|
|
884
|
+
} catch {
|
|
885
|
+
const dropped = this.opening;
|
|
886
|
+
this.opening = void 0;
|
|
887
|
+
try {
|
|
888
|
+
(await dropped)?.close();
|
|
889
|
+
} catch {}
|
|
890
|
+
return fallback;
|
|
891
|
+
}
|
|
892
|
+
}
|
|
893
|
+
get(key) {
|
|
894
|
+
return this.run(void 0, async (db) => {
|
|
895
|
+
const stored = await db.get(STORE, key);
|
|
896
|
+
if (stored === void 0) return;
|
|
897
|
+
if (stored.expiresAt <= this.now()) {
|
|
898
|
+
await db.delete(STORE, key);
|
|
899
|
+
return;
|
|
900
|
+
}
|
|
901
|
+
return stored.entry;
|
|
902
|
+
});
|
|
903
|
+
}
|
|
904
|
+
set(key, entry, ttlMs) {
|
|
905
|
+
return this.run(void 0, async (db) => {
|
|
906
|
+
const stored = {
|
|
907
|
+
expiresAt: this.now() + (ttlMs ?? this.defaultTtlMs),
|
|
908
|
+
entry
|
|
909
|
+
};
|
|
910
|
+
await db.put(STORE, stored, key);
|
|
911
|
+
});
|
|
912
|
+
}
|
|
913
|
+
delete(key) {
|
|
914
|
+
return this.run(void 0, (db) => db.delete(STORE, key));
|
|
915
|
+
}
|
|
916
|
+
clear(prefix) {
|
|
917
|
+
return this.run(void 0, (db) => prefix === void 0 ? db.clear(STORE) : db.delete(STORE, IDBKeyRange.bound(prefix, `${prefix}\uffff`)));
|
|
918
|
+
}
|
|
919
|
+
/** Clear several prefixes in one transaction (one bound range each). */
|
|
920
|
+
clearMany(prefixes) {
|
|
921
|
+
return this.run(void 0, async (db) => {
|
|
922
|
+
const tx = db.transaction(STORE, "readwrite");
|
|
923
|
+
await Promise.all([...prefixes.map((prefix) => tx.store.delete(IDBKeyRange.bound(prefix, `${prefix}\uffff`))), tx.done]);
|
|
924
|
+
});
|
|
925
|
+
}
|
|
926
|
+
/** Close the database handle; a later call reopens it. */
|
|
927
|
+
async close() {
|
|
928
|
+
const opening = this.opening;
|
|
929
|
+
this.opening = void 0;
|
|
930
|
+
try {
|
|
931
|
+
(await opening)?.close();
|
|
932
|
+
} catch {}
|
|
933
|
+
}
|
|
934
|
+
};
|
|
935
|
+
//#endregion
|
|
936
|
+
//#region src/cache/wire.ts
|
|
937
|
+
/** The subscriptions that currently see a connected stream, per cache and resource. */
|
|
938
|
+
const connected = /* @__PURE__ */ new WeakMap();
|
|
939
|
+
/**
|
|
940
|
+
* Subscribe `cache` to `hub` for the manifest resource `resource`. Events
|
|
941
|
+
* clear the spec's prefixes, a reconnect clears the resource, and fresh hits
|
|
942
|
+
* are allowed only while the stream is connected. Returns the unsubscribe
|
|
943
|
+
* (it also closes the freshness gate).
|
|
944
|
+
*/
|
|
945
|
+
function invalidateOnEvents(cache, hub, resource) {
|
|
946
|
+
const byResource = connected.get(cache) ?? /* @__PURE__ */ new Map();
|
|
947
|
+
connected.set(cache, byResource);
|
|
948
|
+
const live = byResource.get(resource) ?? /* @__PURE__ */ new Set();
|
|
949
|
+
byResource.set(resource, live);
|
|
950
|
+
const self = {};
|
|
951
|
+
const mark = (up) => {
|
|
952
|
+
if (up) live.add(self);
|
|
953
|
+
else live.delete(self);
|
|
954
|
+
cache.setEventsActive(resource, live.size > 0);
|
|
955
|
+
};
|
|
956
|
+
const unsubscribe = hub.subscribe({
|
|
957
|
+
onEvent: (event) => cache.invalidateEvent(resource, event.action, event.recordId),
|
|
958
|
+
onReset: () => cache.clearResource(resource),
|
|
959
|
+
onStatus: mark
|
|
960
|
+
});
|
|
961
|
+
return () => {
|
|
962
|
+
try {
|
|
963
|
+
unsubscribe();
|
|
964
|
+
} finally {
|
|
965
|
+
mark(false);
|
|
966
|
+
}
|
|
967
|
+
};
|
|
968
|
+
}
|
|
969
|
+
//#endregion
|
|
970
|
+
export { IndexedDbStore, MemoryStore, buildKey, cachePolicyFromManifest, cacheStatus, canonicalQuery, createCache, credentialScope, eventPrefixes, getEventHub, invalidateOnEvents, manifestKey, recordPrefix, resourcePrefixes, subscribeEvents, tanstackCachePolicy };
|
|
971
|
+
|
|
972
|
+
//# sourceMappingURL=index.mjs.map
|