@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/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;
@@ -632,30 +662,44 @@ var ESCAPES = {
632
662
  "\u2029": "\\u2029"
633
663
  };
634
664
 
635
- // packages/data/src/document.ts
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
- function endOfString(source, start) {
646
- if (source.startsWith('"""', start)) {
647
- const close = source.indexOf('"""', start + 3);
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 = start + 1;
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, start) {
700
+ function endOfArguments(source, start2) {
657
701
  let depth = 0;
658
- let at = start;
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(start, start + 40)}`);
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 read = readValue(raw, directive, at);
708
- vars[name[0]] = read.value;
709
- at = read.next;
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 tags = [];
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 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++;
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
- 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 });
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 { tags, invalidates, stripped: stripped + source.slice(kept) };
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 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
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);