@firsthandjs/data 0.9.1 → 0.10.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/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 held = /* @__PURE__ */ new Set();
61
- const remember = options.remember ?? 6e4;
62
- let recent = [];
63
- const now = () => Date.now();
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
- if (recent.length === 0) {
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
- recent = [];
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
- entry.run = (force) => {
212
- if (entry.disposed) {
213
- return Promise.resolve(void 0);
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
- const controller = new AbortController();
217
- entry.controller = controller;
218
- entry.pending = [];
219
- entry.superseded = false;
220
- entry.loading.value = true;
221
- if (entry.data.peek() === void 0) {
222
- entry.status.value = "loading";
223
- }
224
- let forced = force;
225
- let asked = false;
226
- const declare = (...next) => {
227
- entry.tags = next;
228
- if (!forced && store.missed(entry, next)) {
229
- forced = true;
230
- if (asked) {
231
- entry.superseded = true;
232
- }
233
- }
234
- if (entry.pending.length > 0 && anyTagMatches(entry.pending, next)) {
235
- entry.superseded = true;
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.inflight = running;
289
- void running.then(() => {
290
- if (entry.inflight === running) {
291
- entry.inflight = null;
292
- }
293
- });
294
- return running;
295
- };
296
- const persist = options.persist;
297
- const storage = store.storage;
298
- if (persist !== void 0 && storage?.read !== void 0) {
299
- let stored;
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
- stored = storage.read(persist);
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 (isRendering()) {
317
- if (entry.data.peek() === void 0) {
318
- void entry.run(false);
319
- } else {
320
- entry.loading.value = false;
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
- } else {
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 useAction(run) {
336
- const store = useData();
337
- const entry = createHeld(store, void 0);
338
- let controller = null;
339
- onCleanup(() => {
340
- entry.disposed = true;
341
- controller?.abort();
342
- });
343
- return {
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 ttl = options.ttl ?? 0;
451
- const max = options.max ?? 100;
452
- const now = options.now ?? (() => Date.now());
453
- const entries = /* @__PURE__ */ new Map();
454
- const drop = (key) => {
455
- const entry = entries.get(key);
456
- entry?.controller?.abort();
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 keep = (key, entry) => {
470
- entries.delete(key);
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
- put(key, values[key]);
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
- if (entry === void 0 || entry.expires <= now()) {
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: put,
497
+ write,
519
498
  forget: (key) => {
520
- if (key === void 0) {
521
- for (const held of [...entries.keys()]) {
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
- if (request.mutating === true) {
537
- return await produce(request);
538
- }
539
- const held = entries.get(key);
540
- if (request.force) {
541
- drop(key);
542
- } else if (held !== void 0) {
543
- if (held.inflight !== null) {
544
- return await share(held, request);
545
- }
546
- if (held.expires > now()) {
547
- keep(key, held);
548
- return held.value;
549
- }
550
- entries.delete(key);
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;
@@ -642,20 +672,20 @@ var FirsthandDirectiveError = class extends Error {
642
672
  this.name = "FirsthandDirectiveError";
643
673
  }
644
674
  };
645
- function endOfString(source, start) {
646
- if (source.startsWith('"""', start)) {
647
- const close = source.indexOf('"""', start + 3);
675
+ function endOfString(source, start2) {
676
+ if (source.startsWith('"""', start2)) {
677
+ const close = source.indexOf('"""', start2 + 3);
648
678
  return close === -1 ? source.length : close + 3;
649
679
  }
650
- let at = start + 1;
680
+ let at = start2 + 1;
651
681
  while (at < source.length && source[at] !== '"') {
652
682
  at += source[at] === "\\" ? 2 : 1;
653
683
  }
654
684
  return at + 1;
655
685
  }
656
- function endOfArguments(source, start) {
686
+ function endOfArguments(source, start2) {
657
687
  let depth = 0;
658
- let at = start;
688
+ let at = start2;
659
689
  while (at < source.length) {
660
690
  const character = source[at];
661
691
  if (character === '"') {
@@ -672,7 +702,7 @@ function endOfArguments(source, start) {
672
702
  }
673
703
  at++;
674
704
  }
675
- throw new FirsthandDirectiveError(`unclosed arguments in ${source.slice(start, start + 40)}`);
705
+ throw new FirsthandDirectiveError(`unclosed arguments in ${source.slice(start2, start2 + 40)}`);
676
706
  }
677
707
  function unquote(quoted, directive) {
678
708
  try {
@@ -704,9 +734,9 @@ function parseArguments(raw, directive) {
704
734
  }
705
735
  at++;
706
736
  skipIgnored();
707
- const read = readValue(raw, directive, at);
708
- vars[name[0]] = read.value;
709
- at = read.next;
737
+ const read2 = readValue(raw, directive, at);
738
+ vars[name[0]] = read2.value;
739
+ at = read2.next;
710
740
  skipIgnored();
711
741
  }
712
742
  return vars;
@@ -747,24 +777,14 @@ function readValue(raw, directive, at) {
747
777
  return { value: { literal: Number.isNaN(asNumber) ? text : asNumber }, next };
748
778
  }
749
779
  function scan(source) {
750
- const tags = [];
751
- const invalidates = [];
780
+ const into = { tags: [], invalidates: [] };
752
781
  let stripped = "";
753
782
  let at = 0;
754
783
  let kept = 0;
755
784
  while (at < source.length) {
756
- const character = source[at];
757
- if (character === '"') {
758
- at = endOfString(source, 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++;
785
+ const skipped = skipPast(source, at);
786
+ if (skipped !== null) {
787
+ at = skipped;
768
788
  continue;
769
789
  }
770
790
  const directive = DIRECTIVE.exec(source.slice(at));
@@ -772,26 +792,7 @@ function scan(source) {
772
792
  at++;
773
793
  continue;
774
794
  }
775
- let end = at + directive[0].length;
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 });
795
+ const end = takeDirective(source, at, directive, into);
795
796
  let from = at;
796
797
  while (from > kept && /\s/.test(source[from - 1])) {
797
798
  from--;
@@ -800,7 +801,42 @@ function scan(source) {
800
801
  kept = end;
801
802
  at = end;
802
803
  }
803
- return { tags, invalidates, stripped: stripped + source.slice(kept) };
804
+ return { ...into, stripped: stripped + source.slice(kept) };
805
+ }
806
+ function skipPast(source, at) {
807
+ const character = source[at];
808
+ if (character === '"') {
809
+ return endOfString(source, at);
810
+ }
811
+ if (character === "#") {
812
+ const newline = source.indexOf("\n", at);
813
+ return newline === -1 ? source.length : newline;
814
+ }
815
+ return character === "@" ? null : at + 1;
816
+ }
817
+ function takeDirective(source, at, directive, into) {
818
+ const kind = directive[1];
819
+ let end = at + directive[0].length;
820
+ let args = "";
821
+ let probe = end;
822
+ while (probe < source.length && /\s/.test(source[probe])) {
823
+ probe++;
824
+ }
825
+ if (source[probe] === "(") {
826
+ const close = endOfArguments(source, probe);
827
+ args = source.slice(probe + 1, close - 1);
828
+ end = close;
829
+ }
830
+ const vars = parseArguments(args, kind);
831
+ const named = vars["name"];
832
+ if (named === void 0 || !("literal" in named) || typeof named.literal !== "string") {
833
+ throw new FirsthandDirectiveError(
834
+ `@${kind} needs a literal name, as in @${kind}(name: "user", id: $id)`
835
+ );
836
+ }
837
+ delete vars["name"];
838
+ (kind === "tag" ? into.tags : into.invalidates).push({ name: named.literal, vars });
839
+ return end;
804
840
  }
805
841
  function parseGraphQL(source) {
806
842
  const { tags, invalidates, stripped } = scan(source);
@@ -876,17 +912,26 @@ async function send(options, url, init, request) {
876
912
  }
877
913
  return parsed;
878
914
  }
915
+ function cacheKeyFor(options, init, target, merged) {
916
+ if (init.cacheKey === false) {
917
+ return void 0;
918
+ }
919
+ const method = (merged.method ?? "GET").toUpperCase();
920
+ const named = typeof init.cacheKey === "string" ? init.cacheKey : method === "GET" || method === "HEAD" ? `${method} ${target}` : void 0;
921
+ if (named === void 0) {
922
+ return void 0;
923
+ }
924
+ const scope = options.scope === void 0 ? merged.headers.get("authorization") ?? "" : untrack2(options.scope);
925
+ return `${scope}\0${named}`;
926
+ }
879
927
  function createFetchClient(options = {}) {
880
928
  const cache = options.cache === void 0 || options.cache === false ? void 0 : "read" in options.cache ? options.cache : createCacheClient(options.cache);
881
929
  const client = {
882
930
  cache,
883
931
  request: (url, init = {}) => async (request) => {
884
932
  const [target, merged] = resolve(options, url, init);
885
- const method = (merged.method ?? "GET").toUpperCase();
886
- const scope = options.scope === void 0 ? merged.headers.get("authorization") ?? "" : untrack2(options.scope);
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
933
+ const key = cacheKeyFor(options, init, target, merged);
934
+ if (cache === void 0 || key === void 0 || // An action. Even a `cacheKey` does not put its answer in here: the
890
935
  // call site asked for a key, not for its writes to be remembered.
891
936
  request.mutating === true) {
892
937
  return await send(options, target, merged, request);