@firsthandjs/data 0.9.1 → 0.10.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/dist/action.d.ts +30 -0
- package/dist/action.d.ts.map +1 -0
- package/dist/cache.d.ts.map +1 -1
- package/dist/document.d.ts +0 -4
- package/dist/document.d.ts.map +1 -1
- package/dist/errors.d.ts +27 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/http.d.ts +0 -15
- package/dist/http.d.ts.map +1 -1
- package/dist/index.d.ts +7 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.dev.js +462 -415
- package/dist/index.js +2 -2
- package/dist/resource.d.ts +0 -20
- package/dist/resource.d.ts.map +1 -1
- package/dist/store.d.ts.map +1 -1
- package/dist/vite.dev.js +47 -39
- package/dist/vite.js +47 -39
- package/package.json +3 -3
package/dist/index.dev.js
CHANGED
|
@@ -4,7 +4,6 @@ import {
|
|
|
4
4
|
effect,
|
|
5
5
|
isRendering,
|
|
6
6
|
onCleanup,
|
|
7
|
-
untrack,
|
|
8
7
|
useContext
|
|
9
8
|
} from "@firsthandjs/core";
|
|
10
9
|
|
|
@@ -57,85 +56,91 @@ function readable(value) {
|
|
|
57
56
|
|
|
58
57
|
// packages/data/src/store.ts
|
|
59
58
|
function createData(options = {}) {
|
|
60
|
-
const
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
59
|
+
const state = {
|
|
60
|
+
options,
|
|
61
|
+
held: /* @__PURE__ */ new Set(),
|
|
62
|
+
recent: [],
|
|
63
|
+
remember: options.remember ?? 6e4
|
|
64
|
+
};
|
|
64
65
|
return {
|
|
65
66
|
storage: options.storage,
|
|
66
|
-
missed: (entry, tags) =>
|
|
67
|
-
if (remember === 0 || recent.length === 0) {
|
|
68
|
-
return false;
|
|
69
|
-
}
|
|
70
|
-
const since = now() - remember;
|
|
71
|
-
recent = recent.filter((one) => one.at >= since);
|
|
72
|
-
return recent.some(
|
|
73
|
-
// Younger than this resource's last answer, and about what it has just
|
|
74
|
-
// said it is about. A resource that has answered *since* the
|
|
75
|
-
// invalidation has already taken it into account.
|
|
76
|
-
(one) => one.at > entry.answeredAt && anyTagMatches(one.patterns, tags)
|
|
77
|
-
);
|
|
78
|
-
},
|
|
67
|
+
missed: (entry, tags) => missed(state, entry, tags),
|
|
79
68
|
settled: (tags) => {
|
|
80
|
-
|
|
81
|
-
return;
|
|
82
|
-
}
|
|
83
|
-
recent = recent.filter((one) => !anyTagMatches(one.patterns, tags));
|
|
84
|
-
},
|
|
85
|
-
settle: async (passes = 10) => {
|
|
86
|
-
for (let pass = 0; pass < passes; pass++) {
|
|
87
|
-
const waiting = [];
|
|
88
|
-
for (const entry of held) {
|
|
89
|
-
if (entry.inflight !== null) {
|
|
90
|
-
waiting.push(entry.inflight);
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
if (waiting.length === 0) {
|
|
94
|
-
return;
|
|
95
|
-
}
|
|
96
|
-
await Promise.all(waiting);
|
|
97
|
-
}
|
|
69
|
+
settled(state, tags);
|
|
98
70
|
},
|
|
71
|
+
settle: (passes = 10) => settle(state, passes),
|
|
99
72
|
hold: (entry) => {
|
|
100
|
-
held.add(entry);
|
|
101
|
-
return () => held.delete(entry);
|
|
102
|
-
},
|
|
103
|
-
invalidate: async (...patterns) => {
|
|
104
|
-
const waiting = [];
|
|
105
|
-
for (const cache of options.caches ?? []) {
|
|
106
|
-
cache.forgetTagged(patterns);
|
|
107
|
-
}
|
|
108
|
-
if (remember > 0) {
|
|
109
|
-
recent.push({ patterns, at: now() });
|
|
110
|
-
}
|
|
111
|
-
for (const entry of [...held]) {
|
|
112
|
-
if (entry.controller !== null) {
|
|
113
|
-
entry.pending.push(...patterns);
|
|
114
|
-
}
|
|
115
|
-
if (!anyTagMatches(patterns, entry.tags)) {
|
|
116
|
-
continue;
|
|
117
|
-
}
|
|
118
|
-
devQuery("invalidated", entry.name ?? "(call site)", entry.tags);
|
|
119
|
-
waiting.push(entry.run(true));
|
|
120
|
-
}
|
|
121
|
-
await Promise.all(waiting);
|
|
73
|
+
state.held.add(entry);
|
|
74
|
+
return () => state.held.delete(entry);
|
|
122
75
|
},
|
|
76
|
+
invalidate: (...patterns) => invalidate(state, patterns),
|
|
123
77
|
clear: () => {
|
|
124
|
-
|
|
125
|
-
for (const entry of [...held]) {
|
|
126
|
-
entry.controller?.abort();
|
|
127
|
-
held.delete(entry);
|
|
128
|
-
}
|
|
129
|
-
try {
|
|
130
|
-
options.storage?.clear?.();
|
|
131
|
-
} catch {
|
|
132
|
-
}
|
|
78
|
+
clear(state);
|
|
133
79
|
},
|
|
134
80
|
get size() {
|
|
135
|
-
return held.size;
|
|
81
|
+
return state.held.size;
|
|
136
82
|
}
|
|
137
83
|
};
|
|
138
84
|
}
|
|
85
|
+
function missed(state, entry, tags) {
|
|
86
|
+
if (state.remember === 0 || state.recent.length === 0) {
|
|
87
|
+
return false;
|
|
88
|
+
}
|
|
89
|
+
const since = Date.now() - state.remember;
|
|
90
|
+
state.recent = state.recent.filter((one) => one.at >= since);
|
|
91
|
+
return state.recent.some((one) => one.at > entry.answeredAt && anyTagMatches(one.patterns, tags));
|
|
92
|
+
}
|
|
93
|
+
function settled(state, tags) {
|
|
94
|
+
if (state.recent.length === 0) {
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
state.recent = state.recent.filter((one) => !anyTagMatches(one.patterns, tags));
|
|
98
|
+
}
|
|
99
|
+
async function settle(state, passes) {
|
|
100
|
+
for (let pass = 0; pass < passes; pass++) {
|
|
101
|
+
const waiting = [];
|
|
102
|
+
for (const entry of state.held) {
|
|
103
|
+
if (entry.inflight !== null) {
|
|
104
|
+
waiting.push(entry.inflight);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
if (waiting.length === 0) {
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
await Promise.all(waiting);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
async function invalidate(state, patterns) {
|
|
114
|
+
const waiting = [];
|
|
115
|
+
for (const cache of state.options.caches ?? []) {
|
|
116
|
+
cache.forgetTagged(patterns);
|
|
117
|
+
}
|
|
118
|
+
if (state.remember > 0) {
|
|
119
|
+
state.recent.push({ patterns, at: Date.now() });
|
|
120
|
+
}
|
|
121
|
+
for (const entry of [...state.held]) {
|
|
122
|
+
if (entry.controller !== null) {
|
|
123
|
+
entry.pending.push(...patterns);
|
|
124
|
+
}
|
|
125
|
+
if (!anyTagMatches(patterns, entry.tags)) {
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
devQuery("invalidated", entry.name ?? "(call site)", entry.tags);
|
|
129
|
+
waiting.push(entry.run(true));
|
|
130
|
+
}
|
|
131
|
+
await Promise.all(waiting);
|
|
132
|
+
}
|
|
133
|
+
function clear(state) {
|
|
134
|
+
state.recent = [];
|
|
135
|
+
for (const entry of [...state.held]) {
|
|
136
|
+
entry.controller?.abort();
|
|
137
|
+
state.held.delete(entry);
|
|
138
|
+
}
|
|
139
|
+
try {
|
|
140
|
+
state.options.storage?.clear?.();
|
|
141
|
+
} catch {
|
|
142
|
+
}
|
|
143
|
+
}
|
|
139
144
|
function createHeld(store, name) {
|
|
140
145
|
const entry = {
|
|
141
146
|
data: signal(void 0),
|
|
@@ -208,187 +213,143 @@ function useResource(load, options = {}) {
|
|
|
208
213
|
const entry = createHeld(store, options.persist);
|
|
209
214
|
const release = store.hold(entry);
|
|
210
215
|
devQuery("created", options.persist ?? "(call site)", []);
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
216
|
+
const loading = { entry, store, persist: options.persist };
|
|
217
|
+
entry.run = (force) => runOnce(loading, load, force);
|
|
218
|
+
seedFromStorage(entry, store, options.persist);
|
|
219
|
+
startLoading(entry);
|
|
220
|
+
onCleanup(() => {
|
|
221
|
+
entry.disposed = true;
|
|
215
222
|
entry.controller?.abort();
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
};
|
|
238
|
-
const request = {
|
|
239
|
-
signal: controller.signal,
|
|
240
|
-
get force() {
|
|
241
|
-
asked = true;
|
|
242
|
-
return forced;
|
|
243
|
-
},
|
|
244
|
-
// Read after `tags()` by a cache that keeps them, which is every client
|
|
245
|
-
// in this project: they declare first and look in their cache second.
|
|
246
|
-
get declared() {
|
|
247
|
-
return entry.tags;
|
|
248
|
-
},
|
|
249
|
-
tags: declare
|
|
250
|
-
};
|
|
251
|
-
const context = {
|
|
252
|
-
signal: controller.signal,
|
|
253
|
-
get force() {
|
|
254
|
-
asked = true;
|
|
255
|
-
return forced;
|
|
256
|
-
},
|
|
257
|
-
request,
|
|
258
|
-
tags: declare
|
|
259
|
-
};
|
|
260
|
-
const running = load(context).then(async (value) => {
|
|
261
|
-
if (controller.signal.aborted || entry.disposed) {
|
|
262
|
-
return void 0;
|
|
263
|
-
}
|
|
264
|
-
entry.controller = null;
|
|
265
|
-
succeed(entry, value);
|
|
266
|
-
if (!entry.superseded) {
|
|
267
|
-
store.settled(entry.tags);
|
|
268
|
-
}
|
|
269
|
-
if (options.persist !== void 0) {
|
|
270
|
-
try {
|
|
271
|
-
store.storage?.write?.(options.persist, value);
|
|
272
|
-
} catch {
|
|
273
|
-
}
|
|
274
|
-
}
|
|
275
|
-
if (entry.superseded) {
|
|
276
|
-
entry.superseded = false;
|
|
277
|
-
return entry.run(true);
|
|
278
|
-
}
|
|
279
|
-
return value;
|
|
280
|
-
}).catch((error) => {
|
|
281
|
-
if (controller.signal.aborted || entry.disposed) {
|
|
282
|
-
return void 0;
|
|
283
|
-
}
|
|
284
|
-
entry.controller = null;
|
|
285
|
-
fail(entry, error);
|
|
223
|
+
devQuery("dropped", options.persist ?? "(call site)", entry.tags);
|
|
224
|
+
release();
|
|
225
|
+
});
|
|
226
|
+
return expose(entry, release);
|
|
227
|
+
}
|
|
228
|
+
function runOnce(loading, load, force) {
|
|
229
|
+
const { entry, store } = loading;
|
|
230
|
+
if (entry.disposed) {
|
|
231
|
+
return Promise.resolve(void 0);
|
|
232
|
+
}
|
|
233
|
+
entry.controller?.abort();
|
|
234
|
+
const controller = new AbortController();
|
|
235
|
+
entry.controller = controller;
|
|
236
|
+
entry.pending = [];
|
|
237
|
+
entry.superseded = false;
|
|
238
|
+
entry.loading.value = true;
|
|
239
|
+
if (entry.data.peek() === void 0) {
|
|
240
|
+
entry.status.value = "loading";
|
|
241
|
+
}
|
|
242
|
+
const running = load(contextFor(entry, store, controller, force)).then(async (value) => answered(loading, controller, value)).catch((error) => {
|
|
243
|
+
if (controller.signal.aborted || entry.disposed) {
|
|
286
244
|
return void 0;
|
|
287
|
-
}
|
|
288
|
-
entry.
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
245
|
+
}
|
|
246
|
+
entry.controller = null;
|
|
247
|
+
fail(entry, error);
|
|
248
|
+
return void 0;
|
|
249
|
+
});
|
|
250
|
+
entry.inflight = running;
|
|
251
|
+
void running.then(() => {
|
|
252
|
+
if (entry.inflight === running) {
|
|
253
|
+
entry.inflight = null;
|
|
254
|
+
}
|
|
255
|
+
});
|
|
256
|
+
return running;
|
|
257
|
+
}
|
|
258
|
+
async function answered(loading, controller, value) {
|
|
259
|
+
const { entry, store, persist } = loading;
|
|
260
|
+
if (controller.signal.aborted || entry.disposed) {
|
|
261
|
+
return void 0;
|
|
262
|
+
}
|
|
263
|
+
entry.controller = null;
|
|
264
|
+
succeed(entry, value);
|
|
265
|
+
if (!entry.superseded) {
|
|
266
|
+
store.settled(entry.tags);
|
|
267
|
+
}
|
|
268
|
+
if (persist !== void 0) {
|
|
300
269
|
try {
|
|
301
|
-
|
|
270
|
+
store.storage?.write?.(persist, value);
|
|
302
271
|
} catch {
|
|
303
|
-
stored = void 0;
|
|
304
|
-
}
|
|
305
|
-
if (isThenable(stored)) {
|
|
306
|
-
void (async () => {
|
|
307
|
-
try {
|
|
308
|
-
seed(entry, await stored);
|
|
309
|
-
} catch {
|
|
310
|
-
}
|
|
311
|
-
})();
|
|
312
|
-
} else {
|
|
313
|
-
seed(entry, stored);
|
|
314
272
|
}
|
|
315
273
|
}
|
|
316
|
-
if (
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
274
|
+
if (entry.superseded) {
|
|
275
|
+
entry.superseded = false;
|
|
276
|
+
return entry.run(true);
|
|
277
|
+
}
|
|
278
|
+
return value;
|
|
279
|
+
}
|
|
280
|
+
function seedFromStorage(entry, store, persist) {
|
|
281
|
+
const storage = store.storage;
|
|
282
|
+
if (persist === void 0 || storage?.read === void 0) {
|
|
283
|
+
return;
|
|
284
|
+
}
|
|
285
|
+
let stored;
|
|
286
|
+
try {
|
|
287
|
+
stored = storage.read(persist);
|
|
288
|
+
} catch {
|
|
289
|
+
stored = void 0;
|
|
290
|
+
}
|
|
291
|
+
if (!isThenable(stored)) {
|
|
292
|
+
seed(entry, stored);
|
|
293
|
+
return;
|
|
294
|
+
}
|
|
295
|
+
void (async () => {
|
|
296
|
+
try {
|
|
297
|
+
seed(entry, await stored);
|
|
298
|
+
} catch {
|
|
321
299
|
}
|
|
322
|
-
}
|
|
300
|
+
})();
|
|
301
|
+
}
|
|
302
|
+
function startLoading(entry) {
|
|
303
|
+
if (!isRendering()) {
|
|
323
304
|
effect(() => {
|
|
324
305
|
void entry.run(false);
|
|
325
306
|
});
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
309
|
+
if (entry.data.peek() === void 0) {
|
|
310
|
+
void entry.run(false);
|
|
311
|
+
} else {
|
|
312
|
+
entry.loading.value = false;
|
|
326
313
|
}
|
|
327
|
-
onCleanup(() => {
|
|
328
|
-
entry.disposed = true;
|
|
329
|
-
entry.controller?.abort();
|
|
330
|
-
devQuery("dropped", options.persist ?? "(call site)", entry.tags);
|
|
331
|
-
release();
|
|
332
|
-
});
|
|
333
|
-
return expose(entry, release);
|
|
334
314
|
}
|
|
335
|
-
function
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
entry
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
data: entry.data,
|
|
345
|
-
error: entry.error,
|
|
346
|
-
status: entry.status,
|
|
347
|
-
running: entry.loading,
|
|
348
|
-
run: async (input) => {
|
|
349
|
-
controller?.abort();
|
|
350
|
-
controller = new AbortController();
|
|
351
|
-
const current = controller;
|
|
352
|
-
let invalidating = [];
|
|
353
|
-
entry.loading.value = true;
|
|
354
|
-
entry.status.value = "loading";
|
|
355
|
-
try {
|
|
356
|
-
const invalidates = (...tags) => {
|
|
357
|
-
invalidating = tags;
|
|
358
|
-
};
|
|
359
|
-
const result = await untrack(
|
|
360
|
-
() => run(input, {
|
|
361
|
-
signal: current.signal,
|
|
362
|
-
invalidates,
|
|
363
|
-
// An action changes something, so nothing it sends may be
|
|
364
|
-
// answered out of a cache (`force`) or kept in one (`mutating`).
|
|
365
|
-
// `tags` is the store's invalidation, so a client that knows what
|
|
366
|
-
// a mutation changed — a document with `@invalidates` — reports it
|
|
367
|
-
// without the call site repeating it.
|
|
368
|
-
request: {
|
|
369
|
-
signal: current.signal,
|
|
370
|
-
force: true,
|
|
371
|
-
mutating: true,
|
|
372
|
-
tags: invalidates
|
|
373
|
-
}
|
|
374
|
-
})
|
|
375
|
-
);
|
|
376
|
-
if (current.signal.aborted || entry.disposed) {
|
|
377
|
-
return void 0;
|
|
378
|
-
}
|
|
379
|
-
succeed(entry, result);
|
|
380
|
-
if (invalidating.length > 0) {
|
|
381
|
-
await store.invalidate(...invalidating);
|
|
382
|
-
}
|
|
383
|
-
return result;
|
|
384
|
-
} catch (error) {
|
|
385
|
-
if (current.signal.aborted || entry.disposed) {
|
|
386
|
-
return void 0;
|
|
387
|
-
}
|
|
388
|
-
fail(entry, error);
|
|
389
|
-
return void 0;
|
|
315
|
+
function contextFor(entry, store, controller, force) {
|
|
316
|
+
let forced = force;
|
|
317
|
+
let asked = false;
|
|
318
|
+
const declare = (...next) => {
|
|
319
|
+
entry.tags = next;
|
|
320
|
+
if (!forced && store.missed(entry, next)) {
|
|
321
|
+
forced = true;
|
|
322
|
+
if (asked) {
|
|
323
|
+
entry.superseded = true;
|
|
390
324
|
}
|
|
391
325
|
}
|
|
326
|
+
if (entry.pending.length > 0 && anyTagMatches(entry.pending, next)) {
|
|
327
|
+
entry.superseded = true;
|
|
328
|
+
}
|
|
329
|
+
};
|
|
330
|
+
const why = () => {
|
|
331
|
+
asked = true;
|
|
332
|
+
return forced;
|
|
333
|
+
};
|
|
334
|
+
const request = {
|
|
335
|
+
signal: controller.signal,
|
|
336
|
+
get force() {
|
|
337
|
+
return why();
|
|
338
|
+
},
|
|
339
|
+
// Read after `tags()` by a cache that keeps them, which is every client in
|
|
340
|
+
// this project: they declare first and look in their cache second.
|
|
341
|
+
get declared() {
|
|
342
|
+
return entry.tags;
|
|
343
|
+
},
|
|
344
|
+
tags: declare
|
|
345
|
+
};
|
|
346
|
+
return {
|
|
347
|
+
signal: controller.signal,
|
|
348
|
+
get force() {
|
|
349
|
+
return why();
|
|
350
|
+
},
|
|
351
|
+
request,
|
|
352
|
+
tags: declare
|
|
392
353
|
};
|
|
393
354
|
}
|
|
394
355
|
function fromObservable(source, options = {}) {
|
|
@@ -429,6 +390,66 @@ async function void_(promise) {
|
|
|
429
390
|
return void 0;
|
|
430
391
|
}
|
|
431
392
|
|
|
393
|
+
// packages/data/src/action.ts
|
|
394
|
+
import { onCleanup as onCleanup2, untrack } from "@firsthandjs/core";
|
|
395
|
+
function useAction(run) {
|
|
396
|
+
const store = useData();
|
|
397
|
+
const entry = createHeld(store, void 0);
|
|
398
|
+
const holder = { controller: null, invalidating: [] };
|
|
399
|
+
onCleanup2(() => {
|
|
400
|
+
entry.disposed = true;
|
|
401
|
+
holder.controller?.abort();
|
|
402
|
+
});
|
|
403
|
+
return {
|
|
404
|
+
data: entry.data,
|
|
405
|
+
error: entry.error,
|
|
406
|
+
status: entry.status,
|
|
407
|
+
running: entry.loading,
|
|
408
|
+
run: (input) => once(entry, store, holder, () => run(input, contextFor2(holder)))
|
|
409
|
+
};
|
|
410
|
+
}
|
|
411
|
+
function contextFor2(holder) {
|
|
412
|
+
const current = holder.controller;
|
|
413
|
+
const invalidates = (...tags) => {
|
|
414
|
+
holder.invalidating = tags;
|
|
415
|
+
};
|
|
416
|
+
return {
|
|
417
|
+
signal: current.signal,
|
|
418
|
+
invalidates,
|
|
419
|
+
request: {
|
|
420
|
+
signal: current.signal,
|
|
421
|
+
force: true,
|
|
422
|
+
mutating: true,
|
|
423
|
+
tags: invalidates
|
|
424
|
+
}
|
|
425
|
+
};
|
|
426
|
+
}
|
|
427
|
+
async function once(entry, store, holder, body) {
|
|
428
|
+
holder.controller?.abort();
|
|
429
|
+
holder.controller = new AbortController();
|
|
430
|
+
const current = holder.controller;
|
|
431
|
+
holder.invalidating = [];
|
|
432
|
+
entry.loading.value = true;
|
|
433
|
+
entry.status.value = "loading";
|
|
434
|
+
try {
|
|
435
|
+
const result = await untrack(body);
|
|
436
|
+
if (current.signal.aborted || entry.disposed) {
|
|
437
|
+
return void 0;
|
|
438
|
+
}
|
|
439
|
+
succeed(entry, result);
|
|
440
|
+
if (holder.invalidating.length > 0) {
|
|
441
|
+
await store.invalidate(...holder.invalidating);
|
|
442
|
+
}
|
|
443
|
+
return result;
|
|
444
|
+
} catch (error) {
|
|
445
|
+
if (current.signal.aborted || entry.disposed) {
|
|
446
|
+
return void 0;
|
|
447
|
+
}
|
|
448
|
+
fail(entry, error);
|
|
449
|
+
return void 0;
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
|
|
432
453
|
// packages/data/src/cache.ts
|
|
433
454
|
function stableKey(value) {
|
|
434
455
|
if (value === void 0) {
|
|
@@ -447,142 +468,151 @@ function stableKey(value) {
|
|
|
447
468
|
return `{${entries.map(([name, held]) => `${name}:${stableKey(held)}`).join(",")}}`;
|
|
448
469
|
}
|
|
449
470
|
function createCacheClient(options = {}) {
|
|
450
|
-
const
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
entries.delete(key);
|
|
458
|
-
};
|
|
459
|
-
const put = (key, value) => {
|
|
460
|
-
keep(key, {
|
|
461
|
-
value,
|
|
462
|
-
tags: [],
|
|
463
|
-
expires: ttl === 0 ? Infinity : now() + ttl,
|
|
464
|
-
inflight: null,
|
|
465
|
-
controller: null,
|
|
466
|
-
waiting: 0
|
|
467
|
-
});
|
|
471
|
+
const store = {
|
|
472
|
+
ttl: options.ttl ?? 0,
|
|
473
|
+
max: options.max ?? 100,
|
|
474
|
+
now: options.now ?? (() => Date.now()),
|
|
475
|
+
// Insertion order is the eviction order, and a read moves an entry to the
|
|
476
|
+
// end: a `Map` already keeps that order, so there is no list to maintain.
|
|
477
|
+
entries: /* @__PURE__ */ new Map()
|
|
468
478
|
};
|
|
469
|
-
const
|
|
470
|
-
|
|
471
|
-
entries.set(key, entry);
|
|
472
|
-
if (entries.size > max) {
|
|
473
|
-
for (const oldest of entries.keys()) {
|
|
474
|
-
drop(oldest);
|
|
475
|
-
break;
|
|
476
|
-
}
|
|
477
|
-
}
|
|
479
|
+
const write = (key, value) => {
|
|
480
|
+
put(store, key, value);
|
|
478
481
|
};
|
|
479
482
|
return {
|
|
480
483
|
get size() {
|
|
481
|
-
return entries.size;
|
|
482
|
-
},
|
|
483
|
-
dump: () => {
|
|
484
|
-
const out = {};
|
|
485
|
-
for (const [key, entry] of entries) {
|
|
486
|
-
if (entry.inflight === null) {
|
|
487
|
-
out[key] = entry.value;
|
|
488
|
-
}
|
|
489
|
-
}
|
|
490
|
-
return out;
|
|
484
|
+
return store.entries.size;
|
|
491
485
|
},
|
|
486
|
+
dump: () => dump(store),
|
|
492
487
|
seed: (values) => {
|
|
493
488
|
for (const key in values) {
|
|
494
|
-
|
|
495
|
-
}
|
|
496
|
-
},
|
|
497
|
-
settle: async (passes = 10) => {
|
|
498
|
-
for (let pass = 0; pass < passes; pass++) {
|
|
499
|
-
const waiting = [];
|
|
500
|
-
for (const entry of entries.values()) {
|
|
501
|
-
if (entry.inflight !== null) {
|
|
502
|
-
waiting.push(entry.inflight);
|
|
503
|
-
}
|
|
504
|
-
}
|
|
505
|
-
if (waiting.length === 0) {
|
|
506
|
-
return;
|
|
507
|
-
}
|
|
508
|
-
await Promise.all(waiting.map(async (one) => one.catch(() => void 0)));
|
|
489
|
+
write(key, values[key]);
|
|
509
490
|
}
|
|
510
491
|
},
|
|
492
|
+
settle: (passes = 10) => settle2(store, passes),
|
|
511
493
|
peek: (key) => {
|
|
512
|
-
const entry = entries.get(key);
|
|
513
|
-
|
|
514
|
-
return void 0;
|
|
515
|
-
}
|
|
516
|
-
return entry.value;
|
|
494
|
+
const entry = store.entries.get(key);
|
|
495
|
+
return entry === void 0 || entry.expires <= store.now() ? void 0 : entry.value;
|
|
517
496
|
},
|
|
518
|
-
write
|
|
497
|
+
write,
|
|
519
498
|
forget: (key) => {
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
drop(held);
|
|
523
|
-
}
|
|
524
|
-
return;
|
|
499
|
+
for (const held of key === void 0 ? [...store.entries.keys()] : [key]) {
|
|
500
|
+
drop(store, held);
|
|
525
501
|
}
|
|
526
|
-
drop(key);
|
|
527
502
|
},
|
|
528
503
|
forgetTagged: (patterns) => {
|
|
529
|
-
for (const [key, entry] of [...entries]) {
|
|
504
|
+
for (const [key, entry] of [...store.entries]) {
|
|
530
505
|
if (entry.tags.length > 0 && anyTagMatches(patterns, entry.tags)) {
|
|
531
|
-
drop(key);
|
|
506
|
+
drop(store, key);
|
|
532
507
|
}
|
|
533
508
|
}
|
|
534
509
|
},
|
|
535
|
-
read: (key, produce) => async (request) =>
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
510
|
+
read: (key, produce) => async (request) => read(store, key, produce, request)
|
|
511
|
+
};
|
|
512
|
+
}
|
|
513
|
+
function drop(store, key) {
|
|
514
|
+
const entry = store.entries.get(key);
|
|
515
|
+
entry?.controller?.abort();
|
|
516
|
+
store.entries.delete(key);
|
|
517
|
+
}
|
|
518
|
+
function keep(store, key, entry) {
|
|
519
|
+
store.entries.delete(key);
|
|
520
|
+
store.entries.set(key, entry);
|
|
521
|
+
if (store.entries.size > store.max) {
|
|
522
|
+
for (const oldest of store.entries.keys()) {
|
|
523
|
+
drop(store, oldest);
|
|
524
|
+
break;
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
function put(store, key, value) {
|
|
529
|
+
keep(store, key, {
|
|
530
|
+
value,
|
|
531
|
+
tags: [],
|
|
532
|
+
expires: store.ttl === 0 ? Infinity : store.now() + store.ttl,
|
|
533
|
+
inflight: null,
|
|
534
|
+
controller: null,
|
|
535
|
+
waiting: 0
|
|
536
|
+
});
|
|
537
|
+
}
|
|
538
|
+
function dump(store) {
|
|
539
|
+
const out = {};
|
|
540
|
+
for (const [key, entry] of store.entries) {
|
|
541
|
+
if (entry.inflight === null) {
|
|
542
|
+
out[key] = entry.value;
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
return out;
|
|
546
|
+
}
|
|
547
|
+
async function settle2(store, passes) {
|
|
548
|
+
for (let pass = 0; pass < passes; pass++) {
|
|
549
|
+
const waiting = [];
|
|
550
|
+
for (const entry of store.entries.values()) {
|
|
551
|
+
if (entry.inflight !== null) {
|
|
552
|
+
waiting.push(entry.inflight);
|
|
551
553
|
}
|
|
552
|
-
const controller = new AbortController();
|
|
553
|
-
const entry = {
|
|
554
|
-
value: void 0,
|
|
555
|
-
// What the request has been declared to be about by now. A client
|
|
556
|
-
// declares before it looks here, which is what makes this possible.
|
|
557
|
-
tags: request.declared ?? [],
|
|
558
|
-
expires: 0,
|
|
559
|
-
inflight: null,
|
|
560
|
-
controller,
|
|
561
|
-
waiting: 0
|
|
562
|
-
};
|
|
563
|
-
const run = produce({ signal: controller.signal, force: request.force }).then((value) => {
|
|
564
|
-
if (entries.get(key) === entry) {
|
|
565
|
-
if (ttl === 0) {
|
|
566
|
-
entries.delete(key);
|
|
567
|
-
} else {
|
|
568
|
-
entry.value = value;
|
|
569
|
-
entry.expires = now() + ttl;
|
|
570
|
-
entry.inflight = null;
|
|
571
|
-
entry.controller = null;
|
|
572
|
-
}
|
|
573
|
-
}
|
|
574
|
-
return value;
|
|
575
|
-
}).catch((error) => {
|
|
576
|
-
if (entries.get(key) === entry) {
|
|
577
|
-
entries.delete(key);
|
|
578
|
-
}
|
|
579
|
-
throw error;
|
|
580
|
-
});
|
|
581
|
-
entry.inflight = run;
|
|
582
|
-
keep(key, entry);
|
|
583
|
-
return await share(entry, request);
|
|
584
554
|
}
|
|
555
|
+
if (waiting.length === 0) {
|
|
556
|
+
return;
|
|
557
|
+
}
|
|
558
|
+
await Promise.all(waiting.map(async (one) => one.catch(() => void 0)));
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
async function read(store, key, produce, request) {
|
|
562
|
+
if (request.mutating === true) {
|
|
563
|
+
return await produce(request);
|
|
564
|
+
}
|
|
565
|
+
const held = store.entries.get(key);
|
|
566
|
+
if (request.force) {
|
|
567
|
+
drop(store, key);
|
|
568
|
+
} else if (held !== void 0) {
|
|
569
|
+
if (held.inflight !== null) {
|
|
570
|
+
return await share(held, request);
|
|
571
|
+
}
|
|
572
|
+
if (held.expires > store.now()) {
|
|
573
|
+
keep(store, key, held);
|
|
574
|
+
return held.value;
|
|
575
|
+
}
|
|
576
|
+
store.entries.delete(key);
|
|
577
|
+
}
|
|
578
|
+
return await start(store, key, produce, request);
|
|
579
|
+
}
|
|
580
|
+
async function start(store, key, produce, request) {
|
|
581
|
+
const controller = new AbortController();
|
|
582
|
+
const entry = {
|
|
583
|
+
value: void 0,
|
|
584
|
+
// What the request has been declared to be about by now. A client declares
|
|
585
|
+
// before it looks here, which is what makes this possible.
|
|
586
|
+
tags: request.declared ?? [],
|
|
587
|
+
expires: 0,
|
|
588
|
+
inflight: null,
|
|
589
|
+
controller,
|
|
590
|
+
waiting: 0
|
|
585
591
|
};
|
|
592
|
+
const run = produce({ signal: controller.signal, force: request.force }).then((value) => {
|
|
593
|
+
if (store.entries.get(key) === entry) {
|
|
594
|
+
settleEntry(store, key, entry, value);
|
|
595
|
+
}
|
|
596
|
+
return value;
|
|
597
|
+
}).catch((error) => {
|
|
598
|
+
if (store.entries.get(key) === entry) {
|
|
599
|
+
store.entries.delete(key);
|
|
600
|
+
}
|
|
601
|
+
throw error;
|
|
602
|
+
});
|
|
603
|
+
entry.inflight = run;
|
|
604
|
+
keep(store, key, entry);
|
|
605
|
+
return await share(entry, request);
|
|
606
|
+
}
|
|
607
|
+
function settleEntry(store, key, entry, value) {
|
|
608
|
+
if (store.ttl === 0) {
|
|
609
|
+
store.entries.delete(key);
|
|
610
|
+
return;
|
|
611
|
+
}
|
|
612
|
+
entry.value = value;
|
|
613
|
+
entry.expires = store.now() + store.ttl;
|
|
614
|
+
entry.inflight = null;
|
|
615
|
+
entry.controller = null;
|
|
586
616
|
}
|
|
587
617
|
async function share(entry, request) {
|
|
588
618
|
entry.waiting += 1;
|
|
@@ -632,30 +662,44 @@ var ESCAPES = {
|
|
|
632
662
|
"\u2029": "\\u2029"
|
|
633
663
|
};
|
|
634
664
|
|
|
635
|
-
// packages/data/src/
|
|
636
|
-
var DIRECTIVE = /^@(tag|invalidates)\b/;
|
|
637
|
-
var OPERATION = /\b(query|mutation|subscription)\b[^\S\n]*([A-Za-z_]\w*)?/;
|
|
638
|
-
var NAME = /^[_A-Za-z][_0-9A-Za-z]*/;
|
|
665
|
+
// packages/data/src/errors.ts
|
|
639
666
|
var FirsthandDirectiveError = class extends Error {
|
|
640
667
|
constructor(message) {
|
|
641
668
|
super(message);
|
|
642
669
|
this.name = "FirsthandDirectiveError";
|
|
643
670
|
}
|
|
644
671
|
};
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
672
|
+
var FirsthandHttpError = class extends Error {
|
|
673
|
+
constructor(status, url, body) {
|
|
674
|
+
super(`HTTP ${String(status)} for ${url}`);
|
|
675
|
+
this.status = status;
|
|
676
|
+
this.url = url;
|
|
677
|
+
this.body = body;
|
|
678
|
+
this.name = "FirsthandHttpError";
|
|
679
|
+
}
|
|
680
|
+
status;
|
|
681
|
+
url;
|
|
682
|
+
body;
|
|
683
|
+
};
|
|
684
|
+
|
|
685
|
+
// packages/data/src/document.ts
|
|
686
|
+
var DIRECTIVE = /^@(tag|invalidates)\b/;
|
|
687
|
+
var OPERATION = /\b(query|mutation|subscription)\b[^\S\n]*([A-Za-z_]\w*)?/;
|
|
688
|
+
var NAME = /^[_A-Za-z][_0-9A-Za-z]*/;
|
|
689
|
+
function endOfString(source, start2) {
|
|
690
|
+
if (source.startsWith('"""', start2)) {
|
|
691
|
+
const close = source.indexOf('"""', start2 + 3);
|
|
648
692
|
return close === -1 ? source.length : close + 3;
|
|
649
693
|
}
|
|
650
|
-
let at =
|
|
694
|
+
let at = start2 + 1;
|
|
651
695
|
while (at < source.length && source[at] !== '"') {
|
|
652
696
|
at += source[at] === "\\" ? 2 : 1;
|
|
653
697
|
}
|
|
654
698
|
return at + 1;
|
|
655
699
|
}
|
|
656
|
-
function endOfArguments(source,
|
|
700
|
+
function endOfArguments(source, start2) {
|
|
657
701
|
let depth = 0;
|
|
658
|
-
let at =
|
|
702
|
+
let at = start2;
|
|
659
703
|
while (at < source.length) {
|
|
660
704
|
const character = source[at];
|
|
661
705
|
if (character === '"') {
|
|
@@ -672,7 +716,7 @@ function endOfArguments(source, start) {
|
|
|
672
716
|
}
|
|
673
717
|
at++;
|
|
674
718
|
}
|
|
675
|
-
throw new FirsthandDirectiveError(`unclosed arguments in ${source.slice(
|
|
719
|
+
throw new FirsthandDirectiveError(`unclosed arguments in ${source.slice(start2, start2 + 40)}`);
|
|
676
720
|
}
|
|
677
721
|
function unquote(quoted, directive) {
|
|
678
722
|
try {
|
|
@@ -704,9 +748,9 @@ function parseArguments(raw, directive) {
|
|
|
704
748
|
}
|
|
705
749
|
at++;
|
|
706
750
|
skipIgnored();
|
|
707
|
-
const
|
|
708
|
-
vars[name[0]] =
|
|
709
|
-
at =
|
|
751
|
+
const read2 = readValue(raw, directive, at);
|
|
752
|
+
vars[name[0]] = read2.value;
|
|
753
|
+
at = read2.next;
|
|
710
754
|
skipIgnored();
|
|
711
755
|
}
|
|
712
756
|
return vars;
|
|
@@ -747,24 +791,14 @@ function readValue(raw, directive, at) {
|
|
|
747
791
|
return { value: { literal: Number.isNaN(asNumber) ? text : asNumber }, next };
|
|
748
792
|
}
|
|
749
793
|
function scan(source) {
|
|
750
|
-
const
|
|
751
|
-
const invalidates = [];
|
|
794
|
+
const into = { tags: [], invalidates: [] };
|
|
752
795
|
let stripped = "";
|
|
753
796
|
let at = 0;
|
|
754
797
|
let kept = 0;
|
|
755
798
|
while (at < source.length) {
|
|
756
|
-
const
|
|
757
|
-
if (
|
|
758
|
-
at =
|
|
759
|
-
continue;
|
|
760
|
-
}
|
|
761
|
-
if (character === "#") {
|
|
762
|
-
const newline = source.indexOf("\n", at);
|
|
763
|
-
at = newline === -1 ? source.length : newline;
|
|
764
|
-
continue;
|
|
765
|
-
}
|
|
766
|
-
if (character !== "@") {
|
|
767
|
-
at++;
|
|
799
|
+
const skipped = skipPast(source, at);
|
|
800
|
+
if (skipped !== null) {
|
|
801
|
+
at = skipped;
|
|
768
802
|
continue;
|
|
769
803
|
}
|
|
770
804
|
const directive = DIRECTIVE.exec(source.slice(at));
|
|
@@ -772,26 +806,7 @@ function scan(source) {
|
|
|
772
806
|
at++;
|
|
773
807
|
continue;
|
|
774
808
|
}
|
|
775
|
-
|
|
776
|
-
let args = "";
|
|
777
|
-
let probe = end;
|
|
778
|
-
while (probe < source.length && /\s/.test(source[probe])) {
|
|
779
|
-
probe++;
|
|
780
|
-
}
|
|
781
|
-
if (source[probe] === "(") {
|
|
782
|
-
const close = endOfArguments(source, probe);
|
|
783
|
-
args = source.slice(probe + 1, close - 1);
|
|
784
|
-
end = close;
|
|
785
|
-
}
|
|
786
|
-
const vars = parseArguments(args, directive[1]);
|
|
787
|
-
const named = vars["name"];
|
|
788
|
-
if (named === void 0 || !("literal" in named) || typeof named.literal !== "string") {
|
|
789
|
-
throw new FirsthandDirectiveError(
|
|
790
|
-
`@${directive[1]} needs a literal name, as in @${directive[1]}(name: "user", id: $id)`
|
|
791
|
-
);
|
|
792
|
-
}
|
|
793
|
-
delete vars["name"];
|
|
794
|
-
(directive[1] === "tag" ? tags : invalidates).push({ name: named.literal, vars });
|
|
809
|
+
const end = takeDirective(source, at, directive, into);
|
|
795
810
|
let from = at;
|
|
796
811
|
while (from > kept && /\s/.test(source[from - 1])) {
|
|
797
812
|
from--;
|
|
@@ -800,7 +815,42 @@ function scan(source) {
|
|
|
800
815
|
kept = end;
|
|
801
816
|
at = end;
|
|
802
817
|
}
|
|
803
|
-
return {
|
|
818
|
+
return { ...into, stripped: stripped + source.slice(kept) };
|
|
819
|
+
}
|
|
820
|
+
function skipPast(source, at) {
|
|
821
|
+
const character = source[at];
|
|
822
|
+
if (character === '"') {
|
|
823
|
+
return endOfString(source, at);
|
|
824
|
+
}
|
|
825
|
+
if (character === "#") {
|
|
826
|
+
const newline = source.indexOf("\n", at);
|
|
827
|
+
return newline === -1 ? source.length : newline;
|
|
828
|
+
}
|
|
829
|
+
return character === "@" ? null : at + 1;
|
|
830
|
+
}
|
|
831
|
+
function takeDirective(source, at, directive, into) {
|
|
832
|
+
const kind = directive[1];
|
|
833
|
+
let end = at + directive[0].length;
|
|
834
|
+
let args = "";
|
|
835
|
+
let probe = end;
|
|
836
|
+
while (probe < source.length && /\s/.test(source[probe])) {
|
|
837
|
+
probe++;
|
|
838
|
+
}
|
|
839
|
+
if (source[probe] === "(") {
|
|
840
|
+
const close = endOfArguments(source, probe);
|
|
841
|
+
args = source.slice(probe + 1, close - 1);
|
|
842
|
+
end = close;
|
|
843
|
+
}
|
|
844
|
+
const vars = parseArguments(args, kind);
|
|
845
|
+
const named = vars["name"];
|
|
846
|
+
if (named === void 0 || !("literal" in named) || typeof named.literal !== "string") {
|
|
847
|
+
throw new FirsthandDirectiveError(
|
|
848
|
+
`@${kind} needs a literal name, as in @${kind}(name: "user", id: $id)`
|
|
849
|
+
);
|
|
850
|
+
}
|
|
851
|
+
delete vars["name"];
|
|
852
|
+
(kind === "tag" ? into.tags : into.invalidates).push({ name: named.literal, vars });
|
|
853
|
+
return end;
|
|
804
854
|
}
|
|
805
855
|
function parseGraphQL(source) {
|
|
806
856
|
const { tags, invalidates, stripped } = scan(source);
|
|
@@ -832,18 +882,6 @@ function resolveTags(templates, variables) {
|
|
|
832
882
|
|
|
833
883
|
// packages/data/src/http.ts
|
|
834
884
|
import { untrack as untrack2 } from "@firsthandjs/core";
|
|
835
|
-
var FirsthandHttpError = class extends Error {
|
|
836
|
-
constructor(status, url, body) {
|
|
837
|
-
super(`HTTP ${String(status)} for ${url}`);
|
|
838
|
-
this.status = status;
|
|
839
|
-
this.url = url;
|
|
840
|
-
this.body = body;
|
|
841
|
-
this.name = "FirsthandHttpError";
|
|
842
|
-
}
|
|
843
|
-
status;
|
|
844
|
-
url;
|
|
845
|
-
body;
|
|
846
|
-
};
|
|
847
885
|
function resolve(options, url, init) {
|
|
848
886
|
const target = /^[a-z]+:\/\//i.test(url) ? url : `${options.baseUrl ?? ""}${url}`;
|
|
849
887
|
const { json: payload, cacheKey: _key, ...rest } = init;
|
|
@@ -876,17 +914,26 @@ async function send(options, url, init, request) {
|
|
|
876
914
|
}
|
|
877
915
|
return parsed;
|
|
878
916
|
}
|
|
917
|
+
function cacheKeyFor(options, init, target, merged) {
|
|
918
|
+
if (init.cacheKey === false) {
|
|
919
|
+
return void 0;
|
|
920
|
+
}
|
|
921
|
+
const method = (merged.method ?? "GET").toUpperCase();
|
|
922
|
+
const named = typeof init.cacheKey === "string" ? init.cacheKey : method === "GET" || method === "HEAD" ? `${method} ${target}` : void 0;
|
|
923
|
+
if (named === void 0) {
|
|
924
|
+
return void 0;
|
|
925
|
+
}
|
|
926
|
+
const scope = options.scope === void 0 ? merged.headers.get("authorization") ?? "" : untrack2(options.scope);
|
|
927
|
+
return `${scope}\0${named}`;
|
|
928
|
+
}
|
|
879
929
|
function createFetchClient(options = {}) {
|
|
880
930
|
const cache = options.cache === void 0 || options.cache === false ? void 0 : "read" in options.cache ? options.cache : createCacheClient(options.cache);
|
|
881
931
|
const client = {
|
|
882
932
|
cache,
|
|
883
933
|
request: (url, init = {}) => async (request) => {
|
|
884
934
|
const [target, merged] = resolve(options, url, init);
|
|
885
|
-
const
|
|
886
|
-
|
|
887
|
-
const named = typeof init.cacheKey === "string" ? init.cacheKey : method === "GET" || method === "HEAD" ? `${method} ${target}` : void 0;
|
|
888
|
-
const key = named === void 0 ? void 0 : `${scope}\0${named}`;
|
|
889
|
-
if (cache === void 0 || init.cacheKey === false || key === void 0 || // An action. Even a `cacheKey` does not put its answer in here: the
|
|
935
|
+
const key = cacheKeyFor(options, init, target, merged);
|
|
936
|
+
if (cache === void 0 || key === void 0 || // An action. Even a `cacheKey` does not put its answer in here: the
|
|
890
937
|
// call site asked for a key, not for its writes to be remembered.
|
|
891
938
|
request.mutating === true) {
|
|
892
939
|
return await send(options, target, merged, request);
|