@askrjs/askr 0.3.1 → 0.4.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.
Files changed (123) hide show
  1. package/CHANGELOG.md +898 -1
  2. package/README.md +61 -29
  3. package/capabilities.json +19 -9
  4. package/dist/actions/index.js +56 -12
  5. package/dist/benchmark.js +10 -15
  6. package/dist/boot/index.d.ts +3 -2
  7. package/dist/boot/index.js +1 -1
  8. package/dist/boot-C5HnQZio.js +4032 -0
  9. package/dist/browser-activity-2r5gA1Cb.js +222 -0
  10. package/dist/components/index.d.ts +8 -1
  11. package/dist/components/index.js +1 -43
  12. package/dist/{aria-hKjThHcp.js → compose-ref-ccRU59BM.js} +48 -5
  13. package/dist/control/index.d.ts +2 -0
  14. package/dist/control/index.js +1 -1
  15. package/dist/control-hr3B0QaD.js +211 -0
  16. package/dist/core.d.ts +5 -9
  17. package/dist/csp-nonce-D-XM_uuJ.js +35 -0
  18. package/dist/data/index.js +3 -3
  19. package/dist/{data-2edW0jhF.js → data-BsnOUpQ6.js} +294 -95
  20. package/dist/data-runtime-i-RD0Ffm.js +242 -0
  21. package/dist/{deferred-CbgNhf67.js → deferred-nGZglpj6.js} +42 -20
  22. package/dist/domains/actions.d.ts +8 -3
  23. package/dist/domains/component.d.ts +1 -107
  24. package/dist/domains/context.d.ts +0 -15
  25. package/dist/domains/control.d.ts +36 -249
  26. package/dist/domains/data.d.ts +31 -13
  27. package/dist/domains/diagnostics.d.ts +1 -2
  28. package/dist/domains/lifecycle.d.ts +2 -5
  29. package/dist/domains/refs.d.ts +0 -2
  30. package/dist/domains/routing.d.ts +43 -8
  31. package/dist/domains/scheduler.d.ts +1 -76
  32. package/dist/domains/server.d.ts +5 -2
  33. package/dist/domains/state.d.ts +2 -5
  34. package/dist/domains/telemetry.d.ts +0 -2
  35. package/dist/elements.d.ts +214 -51
  36. package/dist/error-boundary-ChXoIFVl.js +293 -0
  37. package/dist/foundations/icon/index.d.ts +0 -58
  38. package/dist/foundations/icon/index.js +2 -2
  39. package/dist/foundations/index.js +2 -1
  40. package/dist/foundations/interactions/index.js +1 -1
  41. package/dist/foundations/state/index.d.ts +3 -3
  42. package/dist/foundations/state/index.js +5 -3
  43. package/dist/foundations/structures/index.js +2 -1
  44. package/dist/foundations/utilities/index.d.ts +2 -0
  45. package/dist/foundations/utilities/index.js +1 -2
  46. package/dist/fx/index.d.ts +31 -58
  47. package/dist/fx/index.js +313 -195
  48. package/dist/hooks-kjLigwDJ.js +202 -0
  49. package/dist/index.d.ts +11 -74
  50. package/dist/index.js +8 -447
  51. package/dist/instance-DL-YKrj6.js +992 -0
  52. package/dist/{interactions-CTeACdT_.js → interactions-BAa2xuJ-.js} +3 -2
  53. package/dist/jsx-dev-runtime.d.ts +41 -18
  54. package/dist/jsx-dev-runtime.js +1 -6
  55. package/dist/{jsx-runtime-CPJXAOBF.js → jsx-runtime-C4idqHY0.js} +18 -7
  56. package/dist/jsx-runtime.js +1 -1
  57. package/dist/jsx.d.ts +61 -37
  58. package/dist/{manifest-D1P3UNWV.js → manifest-BNupb86f.js} +3 -3
  59. package/dist/{navigate-prC5V8XV.js → navigate-DsfO12hB.js} +468 -85
  60. package/dist/navigation.d.ts +12 -3
  61. package/dist/peer-types.d.ts +8 -0
  62. package/dist/portal-LYf4cbHd.js +349 -0
  63. package/dist/promise-Dz8iIHAm.js +16 -0
  64. package/dist/query-registry-tzSiVrV_.js +325 -0
  65. package/dist/readable-DFWOsTac.js +21 -0
  66. package/dist/{render-context-B7ohlh6t.js → render-context-DGHnEveC.js} +27 -11
  67. package/dist/render-sync-CUxvoVab.js +1256 -0
  68. package/dist/resolution-CF1BqHLm.js +1258 -0
  69. package/dist/resource-BhcaFqdB.js +377 -0
  70. package/dist/resources/index.d.ts +21 -11
  71. package/dist/resources/index.js +65 -266
  72. package/dist/router/index.d.ts +14 -5
  73. package/dist/router/index.js +22 -53
  74. package/dist/scope-ila_zaId.js +55 -0
  75. package/dist/{shared-kU8yvryp.js → shared-O2ZnmYAf.js} +1 -7
  76. package/dist/{snapshot-source-An4iKajG.js → snapshot-Cjx4N988.js} +6 -1
  77. package/dist/ssg/index.d.ts +1 -2
  78. package/dist/ssg/index.js +43 -22
  79. package/dist/ssr/index.d.ts +26 -28
  80. package/dist/ssr/index.js +4 -4
  81. package/dist/{ssr-CiNm6Vzl.js → ssr-9LOx_bEu.js} +133 -122
  82. package/dist/ssr-DPfdcq0e.js +229 -0
  83. package/dist/state-i9D4EtY6.js +250 -0
  84. package/dist/structures-CkehlOT3.js +231 -0
  85. package/dist/testing/index.d.ts +8 -4
  86. package/dist/testing/index.js +29 -34
  87. package/dist/url-C9xMpMMv.js +185 -0
  88. package/dist/utilities.d.ts +48 -2
  89. package/dist/verify-hydration-D_cyn1OX.js +152 -0
  90. package/package.json +19 -9
  91. package/dist/access-CEA2pOry.js +0 -1046
  92. package/dist/activity-oy2YFTFF.js +0 -201
  93. package/dist/boot-DLplW9h4.js +0 -1104
  94. package/dist/capabilities-Bbf0mlp9.js +0 -42
  95. package/dist/compose-ref-v0qGlHoN.js +0 -26
  96. package/dist/control-BR_mrX__.js +0 -173
  97. package/dist/control-C-ApnFs1.js +0 -20
  98. package/dist/csp-nonce-BfFWcd0w.js +0 -173
  99. package/dist/data-runtime-CxoY98lX.js +0 -149
  100. package/dist/default-portal-runtime-DvMvWJVp.js +0 -20
  101. package/dist/dom-renderer.d.ts +0 -77
  102. package/dist/domains/renderer.d.ts +0 -127
  103. package/dist/eager-control.d.ts +0 -8
  104. package/dist/effect-C3Zsc1k6.js +0 -292
  105. package/dist/for-state-BDykeOJL.js +0 -1753
  106. package/dist/instance-VAFYVJOa.js +0 -1027
  107. package/dist/notify-DxxuDPeY.js +0 -266
  108. package/dist/portal-w3oQ6oQU.js +0 -29
  109. package/dist/query-registry-LrYpQsDJ.js +0 -83
  110. package/dist/render-resolved-BC0W7_5b.js +0 -1123
  111. package/dist/renderer-CT0HeY58.js +0 -8623
  112. package/dist/resolution-3DskUkjG.js +0 -577
  113. package/dist/resource-18-sFRl9.js +0 -344
  114. package/dist/route-matching-CUzZ31gT.js +0 -551
  115. package/dist/router-internal-7nuNEHfG.js +0 -36
  116. package/dist/scope-Cq_iYbTR.js +0 -403
  117. package/dist/selector-BSPVkjKP.js +0 -311
  118. package/dist/ssr-EPjwJH5R.js +0 -29
  119. package/dist/state-BXJfudq1.js +0 -91
  120. package/dist/structures-CjEzfj4h.js +0 -654
  121. package/dist/timer-C7uqnJVt.js +0 -85
  122. package/dist/verify-hydration-ikaxotf7.js +0 -27
  123. package/dist/vnode-D24p6IW2.js +0 -7
@@ -1,11 +1,9 @@
1
- import { b as ScheduledWork, f as requestRuntimeWork, v as logger } from "./access-CEA2pOry.js";
2
- import { d as getCurrentAppRenderRuntime, f as getCurrentComponentInstance, i as claimHookIndex } from "./scope-Cq_iYbTR.js";
3
- import { a as ownComponentCleanup, r as getComponentLifetimeIdentity } from "./capabilities-Bbf0mlp9.js";
4
- import { v as recordReadableRead } from "./notify-DxxuDPeY.js";
5
- import { n as getActiveRenderContext } from "./render-context-B7ohlh6t.js";
6
- import { a as routeActive, i as windowFocused, n as documentVisible, t as timer } from "./timer-C7uqnJVt.js";
7
- import { a as getMutationSlotStore, c as resolveDataRuntimeState, i as getDefaultDataRuntime, n as ensureMutationCleanup, o as getQuerySlotStore, r as ensureQueryCleanup, s as invalidateQueriesForRuntime } from "./data-runtime-CxoY98lX.js";
8
- import { a as normalizeAsyncDataError, i as isCurrentAsyncOperation, n as isAbortError, o as notifySource, t as createReadableSource } from "./shared-kU8yvryp.js";
1
+ import { C as schedule, I as logger } from "./instance-DL-YKrj6.js";
2
+ import { i as currentComponent, n as createSource, r as currentAppRuntime, s as notify, t as claimHookIndex, u as readSource } from "./hooks-kjLigwDJ.js";
3
+ import { n as getActiveRenderContext } from "./render-context-DGHnEveC.js";
4
+ import { a as getDefaultDataRuntime, c as invalidateQueriesForRuntime, f as invalidateCollectionCell, l as readQueryData, m as unregisterCollectionCell, n as ensureMutationCleanup, o as getMutationSlotStore, p as registerCollectionCell, r as ensureQueryCleanup, s as getQuerySlotStore, u as resolveDataRuntimeState } from "./data-runtime-i-RD0Ffm.js";
5
+ import { a as routeActive, n as windowFocused, s as timer, t as documentVisible } from "./browser-activity-2r5gA1Cb.js";
6
+ import { i as normalizeAsyncDataError, r as isCurrentAsyncOperation, t as isAbortError } from "./shared-O2ZnmYAf.js";
9
7
  //#region src/data/query-key.ts
10
8
  function serializeQueryKeyNumber(part) {
11
9
  if (Number.isNaN(part)) return "NaN";
@@ -23,7 +21,12 @@ function serializeQueryKeyPart(part) {
23
21
  case "number": return `n=${serializeQueryKeyNumber(part)}`;
24
22
  case "boolean": return `b=${part ? "1" : "0"}`;
25
23
  case "object":
26
- if (Array.isArray(part)) return `a[${part.map((item) => serializeQueryKeyPart(item)).join(",")}]`;
24
+ if (Array.isArray(part)) {
25
+ const items = [];
26
+ const length = part.length;
27
+ for (let index = 0; index < length; index++) items.push(serializeQueryKeyPart(part[index]));
28
+ return `a[${items.join(",")}]`;
29
+ }
27
30
  const objectPart = part;
28
31
  return `o{${Object.keys(objectPart).sort().map((key) => `${encodeURIComponent(key)}=${serializeQueryKeyPart(objectPart[key])}`).join(",")}}`;
29
32
  default: return String(part);
@@ -46,18 +49,28 @@ function createQueryScope(namespace, invalidate) {
46
49
  }
47
50
  //#endregion
48
51
  //#region src/data/invalidation.ts
49
- /** Mark all cached queries whose key starts with `prefix` as stale, triggering a refresh. */
52
+ /**
53
+ * Mark all cached queries under `prefix` as stale, triggering a refresh.
54
+ * Matching is by `:`-delimited segment: a key matches when it equals `prefix`,
55
+ * when `prefix` ends in `:`, or when the key continues `prefix` at a `:`
56
+ * (`'user:1'` matches `user:1:posts` but not `user:10`). Only `:` is a
57
+ * segment boundary; the empty prefix matches every key.
58
+ */
50
59
  function invalidate(prefix, options) {
51
60
  invalidateQueriesForRuntime(resolveDataRuntimeState(options?.runtime), prefix, options?.markPendingWrite ?? false);
52
61
  }
53
- /** Create a {@link QueryScope} that namespaces keys and invalidations under `namespace`. */
54
- function queryScope(namespace) {
55
- return createQueryScope(namespace, invalidate);
62
+ /** Create a {@link QueryScope} that namespaces keys and can bind invalidations to a runtime. */
63
+ function queryScope(namespace, options) {
64
+ return createQueryScope(namespace, (prefix, callOptions) => invalidate(prefix, {
65
+ ...callOptions,
66
+ runtime: callOptions?.runtime ?? options?.runtime
67
+ }));
56
68
  }
57
69
  const INVALIDATE_ON_INTERVAL_OPTIONS_ERROR = "[Askr] invalidateOnInterval() requires an options object with a finite numeric intervalMs.";
58
70
  /**
59
71
  * Periodically invalidate queries matching `prefix` on a fixed interval,
60
72
  * optionally gated by active route, document visibility, or window focus.
73
+ * `prefix` matches key segments the same way as {@link invalidate}.
61
74
  */
62
75
  function invalidateOnInterval(prefix, options) {
63
76
  if (!options || typeof options !== "object" || typeof options.intervalMs !== "number" || !Number.isFinite(options.intervalMs)) throw new Error(INVALIDATE_ON_INTERVAL_OPTIONS_ERROR);
@@ -74,8 +87,10 @@ function invalidateOnInterval(prefix, options) {
74
87
  //#region src/data/mutation-cell.ts
75
88
  var MutationCell = class {
76
89
  constructor(options, runtimeState) {
77
- this.source = createReadableSource();
90
+ this.source = createSource();
78
91
  this.controller = null;
92
+ this.activeControllers = /* @__PURE__ */ new Set();
93
+ this.rollbacks = /* @__PURE__ */ new Map();
79
94
  this.generation = 0;
80
95
  this.state = {
81
96
  status: "idle",
@@ -84,28 +99,30 @@ var MutationCell = class {
84
99
  };
85
100
  this.runtimeState = runtimeState;
86
101
  this.action = options.action;
102
+ this.optimistic = options.optimistic;
87
103
  this.affects = options.affects;
88
104
  this.afterSuccess = options.afterSuccess;
89
105
  }
90
106
  setOptions(options) {
91
107
  this.action = options.action;
108
+ this.optimistic = options.optimistic;
92
109
  this.affects = options.affects;
93
110
  this.afterSuccess = options.afterSuccess;
94
111
  }
95
112
  get status() {
96
- recordReadableRead(this.source);
113
+ readSource(this.source);
97
114
  return this.state.status;
98
115
  }
99
116
  get pending() {
100
- recordReadableRead(this.source);
117
+ readSource(this.source);
101
118
  return this.state.status === "pending";
102
119
  }
103
120
  get error() {
104
- recordReadableRead(this.source);
121
+ readSource(this.source);
105
122
  return this.state.error;
106
123
  }
107
124
  get result() {
108
- recordReadableRead(this.source);
125
+ readSource(this.source);
109
126
  return this.state.result;
110
127
  }
111
128
  setState(next) {
@@ -113,17 +130,18 @@ var MutationCell = class {
113
130
  ...this.state,
114
131
  ...next
115
132
  };
116
- notifySource(this.source);
133
+ notify(this.source);
117
134
  }
118
135
  async execute(input) {
119
136
  const action = this.action;
137
+ const optimistic = this.optimistic;
120
138
  const affects = this.affects;
121
139
  const afterSuccess = this.afterSuccess;
122
140
  this.generation += 1;
123
141
  const generation = this.generation;
124
- this.controller?.abort();
125
142
  const controller = new AbortController();
126
143
  this.controller = controller;
144
+ this.activeControllers.add(controller);
127
145
  this.setState({
128
146
  status: "pending",
129
147
  error: null,
@@ -131,8 +149,17 @@ var MutationCell = class {
131
149
  });
132
150
  let result;
133
151
  try {
152
+ const rollback = optimistic?.(input, { signal: controller.signal });
153
+ if (rollback) this.rollbacks.set(controller, rollback);
134
154
  result = await action(input, { signal: controller.signal });
135
- } catch (error) {
155
+ this.rollbacks.delete(controller);
156
+ } catch (cause) {
157
+ let error = cause;
158
+ try {
159
+ this.rollbackOptimistic(controller);
160
+ } catch (rollbackError) {
161
+ error = new AggregateError([cause, rollbackError], "Mutation failed and its optimistic rollback failed");
162
+ }
136
163
  if (!isCurrentAsyncOperation(this.generation, generation, this.controller, controller)) throw error;
137
164
  if (isAbortError(error, controller.signal)) {
138
165
  this.setState({
@@ -146,6 +173,9 @@ var MutationCell = class {
146
173
  error: normalizeAsyncDataError(error, "Unknown mutation error")
147
174
  });
148
175
  throw error;
176
+ } finally {
177
+ this.activeControllers.delete(controller);
178
+ this.rollbacks.delete(controller);
149
179
  }
150
180
  if (isCurrentAsyncOperation(this.generation, generation, this.controller, controller)) this.setState({
151
181
  status: "success",
@@ -159,25 +189,44 @@ var MutationCell = class {
159
189
  return result;
160
190
  }
161
191
  abort() {
162
- if (this.state.status !== "pending") return;
192
+ if (this.activeControllers.size === 0) return;
163
193
  this.generation += 1;
164
- this.controller?.abort();
165
194
  this.controller = null;
166
- this.setState({
195
+ if (this.state.status === "pending") this.setState({
167
196
  status: "idle",
168
197
  error: null,
169
198
  result: null
170
199
  });
200
+ this.cancelActive();
171
201
  }
172
202
  reset() {
173
203
  this.generation += 1;
174
- this.controller?.abort();
175
204
  this.controller = null;
176
205
  this.setState({
177
206
  status: "idle",
178
207
  error: null,
179
208
  result: null
180
209
  });
210
+ this.cancelActive();
211
+ }
212
+ rollbackOptimistic(controller) {
213
+ const rollback = this.rollbacks.get(controller);
214
+ this.rollbacks.delete(controller);
215
+ rollback?.();
216
+ }
217
+ cancelActive() {
218
+ const controllers = [...this.activeControllers];
219
+ this.activeControllers.clear();
220
+ const errors = [];
221
+ for (const controller of controllers) {
222
+ try {
223
+ this.rollbackOptimistic(controller);
224
+ } catch (error) {
225
+ errors.push(error);
226
+ }
227
+ controller.abort();
228
+ }
229
+ if (errors.length > 0) throw new AggregateError(errors, "Optimistic mutation rollback failed");
181
230
  }
182
231
  };
183
232
  /**
@@ -187,14 +236,14 @@ var MutationCell = class {
187
236
  */
188
237
  function createMutation(options) {
189
238
  if (options.key !== void 0 && options.key.length === 0) throw new TypeError("createMutation key must be a non-empty string.");
190
- const instance = getCurrentComponentInstance();
239
+ const instance = currentComponent();
191
240
  const runtimeState = resolveDataRuntimeState(options.runtime);
192
241
  const override = options.key ? runtimeState.mutationTestOverrides.get(options.key) : void 0;
193
242
  if (!instance) {
194
243
  if (override) return override;
195
244
  return new MutationCell(options, runtimeState);
196
245
  }
197
- const hookIndex = claimHookIndex(instance, "mutation");
246
+ const hookIndex = claimHookIndex(instance, "createMutation");
198
247
  ensureMutationCleanup(runtimeState, instance);
199
248
  const slotStore = getMutationSlotStore(runtimeState, instance);
200
249
  if (override) {
@@ -281,19 +330,24 @@ function errorQueryState(data, error) {
281
330
  //#region src/data/query-cell.ts
282
331
  const RECONCILE_MAX_ATTEMPTS = 3;
283
332
  const RECONCILE_RETRY_DELAY_MS = 25;
284
- var QueryStartWork = class extends ScheduledWork {
333
+ const MAX_GC_TIME_MS = 2147483647;
334
+ const DEFAULT_OWNERLESS_GC_TIME_MS = 3e5;
335
+ function validateGcTime(gcTime) {
336
+ if (gcTime !== void 0 && (!Number.isFinite(gcTime) || gcTime < 0 || gcTime > MAX_GC_TIME_MS)) throw new RangeError("Query gcTime must be a finite, non-negative timer delay.");
337
+ }
338
+ /** Starts a query fetch in the flush; settles its promise if dropped. */
339
+ var QueryStartWork = class {
285
340
  constructor(run, settle) {
286
- super(run);
341
+ this.run = run;
287
342
  this.settle = settle;
288
343
  }
289
344
  cancel() {
290
- super.cancel();
291
345
  this.settle();
292
346
  }
293
347
  };
294
348
  var QueryCell = class {
295
349
  constructor(options, key, cache) {
296
- this.source = createReadableSource();
350
+ this.source = createSource();
297
351
  this.controller = null;
298
352
  this.generation = 0;
299
353
  this.pendingRefresh = null;
@@ -304,22 +358,36 @@ var QueryCell = class {
304
358
  this.reconcileSequence = 0;
305
359
  this.destroyed = false;
306
360
  this.ownerCount = 0;
361
+ this.gcTimer = null;
362
+ this.unownedTimer = null;
307
363
  this.owners = /* @__PURE__ */ new Map();
308
364
  this.warnedDefinitionConflictKeys = /* @__PURE__ */ new Set();
365
+ this.definitionOwner = null;
366
+ this.definitionOwnerHook = -1;
367
+ this.conflictCheck = null;
309
368
  this.state = loadingQueryState();
369
+ validateGcTime(options.gcTime);
310
370
  this.options = options;
311
371
  this.key = key;
312
372
  this.cache = cache;
313
373
  if (options.initialData !== void 0) this.state = freshQueryState(options.initialData);
314
374
  }
315
375
  attach(generation, hookIndex) {
376
+ if (this.unownedTimer !== null) {
377
+ clearTimeout(this.unownedTimer);
378
+ this.unownedTimer = null;
379
+ }
380
+ if (this.gcTimer !== null) {
381
+ clearTimeout(this.gcTimer);
382
+ this.gcTimer = null;
383
+ }
316
384
  let hooks = this.owners.get(generation);
317
385
  if (!hooks) {
318
- hooks = /* @__PURE__ */ new Set();
386
+ hooks = /* @__PURE__ */ new Map();
319
387
  this.owners.set(generation, hooks);
320
388
  }
321
389
  if (hooks.has(hookIndex)) return;
322
- hooks.add(hookIndex);
390
+ hooks.set(hookIndex, null);
323
391
  this.ownerCount += 1;
324
392
  }
325
393
  detach(generation, hookIndex) {
@@ -327,7 +395,54 @@ var QueryCell = class {
327
395
  if (!hooks || !hooks.delete(hookIndex)) return;
328
396
  this.ownerCount -= 1;
329
397
  if (hooks.size === 0) this.owners.delete(generation);
330
- if (this.ownerCount <= 0) this.destroy();
398
+ if (this.ownerCount <= 0) {
399
+ const gcTime = this.options.gcTime ?? 0;
400
+ if (gcTime === 0 || this.state.data === null || isServerRender()) this.destroy();
401
+ else {
402
+ this.generation += 1;
403
+ const controller = this.controller;
404
+ this.controller = null;
405
+ this.finishPendingRefresh();
406
+ this.pendingRefreshToken += 1;
407
+ this.definitionOwner = null;
408
+ this.definitionOwnerHook = -1;
409
+ if (this.state.refreshing) this.setState(staleQueryState(this.state.data, "aborted"));
410
+ this.gcTimer = setTimeout(() => this.destroy(), gcTime);
411
+ controller?.abort();
412
+ }
413
+ return;
414
+ }
415
+ if (this.definitionOwner === generation && this.definitionOwnerHook === hookIndex) this.promoteDefinitionOwner();
416
+ }
417
+ promoteDefinitionOwner() {
418
+ this.definitionOwner = null;
419
+ for (const [generation, hooks] of this.owners) for (const [hookIndex, options] of hooks) if (options) {
420
+ this.options = options;
421
+ this.definitionOwner = generation;
422
+ this.definitionOwnerHook = hookIndex;
423
+ return;
424
+ }
425
+ }
426
+ /**
427
+ * Record an attached reader's definition for this key. The owning reader's
428
+ * renders replace the definition; with no owner, the reader takes over.
429
+ * Other readers are checked for conflicts once render work settles.
430
+ */
431
+ define(options, generation, hookIndex) {
432
+ validateGcTime(options.gcTime);
433
+ const hooks = this.owners.get(generation);
434
+ if (this.destroyed || !hooks?.has(hookIndex)) return;
435
+ hooks.set(hookIndex, options);
436
+ if (this.definitionOwner === null || this.definitionOwner === generation && this.definitionOwnerHook === hookIndex) {
437
+ this.options = options;
438
+ this.definitionOwner = generation;
439
+ this.definitionOwnerHook = hookIndex;
440
+ return;
441
+ }
442
+ }
443
+ warnOnConflictingReaders() {
444
+ if (this.destroyed) return;
445
+ for (const hooks of this.owners.values()) for (const options of hooks.values()) if (options) this.warnOnConflictingDefinition(options);
331
446
  }
332
447
  warnOnConflictingDefinition(options) {
333
448
  const conflicts = this.getDefinitionConflicts(options);
@@ -341,14 +456,45 @@ var QueryCell = class {
341
456
  destroy() {
342
457
  if (this.destroyed) return;
343
458
  this.destroyed = true;
459
+ if (this.gcTimer !== null) {
460
+ clearTimeout(this.gcTimer);
461
+ this.gcTimer = null;
462
+ }
463
+ if (this.unownedTimer !== null) {
464
+ clearTimeout(this.unownedTimer);
465
+ this.unownedTimer = null;
466
+ }
344
467
  this.controller?.abort();
345
468
  this.controller = null;
346
469
  this.reconcileAttemptCount = 0;
347
470
  this.ownerCount = 0;
348
471
  this.owners.clear();
349
- this.cache.delete(this.key);
472
+ this.evictFromCache();
350
473
  this.finishPendingRefresh();
351
474
  }
475
+ evictFromCache() {
476
+ if (this.cache.get(this.key) === this) this.cache.delete(this.key);
477
+ }
478
+ /** A component's inactive definition must not become an ownerless fetcher. */
479
+ retireInactiveReaderCacheEntry() {
480
+ if (this.gcTimer === null) return false;
481
+ this.destroy();
482
+ return true;
483
+ }
484
+ /** Ownerless handles stay usable after their cache lookup window expires. */
485
+ scheduleUnownedCacheEviction(gcTime) {
486
+ if (isServerRender() || this.ownerCount > 0 || this.destroyed) return;
487
+ if (this.unownedTimer !== null) clearTimeout(this.unownedTimer);
488
+ this.unownedTimer = null;
489
+ if (gcTime === 0) {
490
+ this.evictFromCache();
491
+ return;
492
+ }
493
+ this.unownedTimer = setTimeout(() => {
494
+ this.unownedTimer = null;
495
+ if (this.ownerCount === 0) this.evictFromCache();
496
+ }, gcTime);
497
+ }
352
498
  getDefinitionConflicts(options) {
353
499
  const conflicts = [];
354
500
  if ((this.options.definitionIdentity ?? this.options.fetch) !== (options.definitionIdentity ?? options.fetch)) conflicts.push("fetch");
@@ -357,31 +503,31 @@ var QueryCell = class {
357
503
  return conflicts;
358
504
  }
359
505
  get data() {
360
- recordReadableRead(this.source);
506
+ readSource(this.source);
361
507
  return this.state.data;
362
508
  }
363
509
  get error() {
364
- recordReadableRead(this.source);
510
+ readSource(this.source);
365
511
  return this.state.error;
366
512
  }
367
513
  get loading() {
368
- recordReadableRead(this.source);
514
+ readSource(this.source);
369
515
  return this.state.loading;
370
516
  }
371
517
  get refreshing() {
372
- recordReadableRead(this.source);
518
+ readSource(this.source);
373
519
  return this.state.refreshing;
374
520
  }
375
521
  get stale() {
376
- recordReadableRead(this.source);
522
+ readSource(this.source);
377
523
  return this.state.stale;
378
524
  }
379
525
  get consistency() {
380
- recordReadableRead(this.source);
526
+ readSource(this.source);
381
527
  return this.state.consistency;
382
528
  }
383
529
  get staleReason() {
384
- recordReadableRead(this.source);
530
+ readSource(this.source);
385
531
  return this.state.staleReason;
386
532
  }
387
533
  /** @internal Whether a collection should schedule this cell's first load. */
@@ -394,8 +540,13 @@ var QueryCell = class {
394
540
  }
395
541
  refresh() {
396
542
  if (this.destroyed) return Promise.resolve();
543
+ if (this.gcTimer !== null) {
544
+ this.destroy();
545
+ return Promise.resolve();
546
+ }
397
547
  if (this.pendingRefresh) {
398
548
  if (this.pendingRefreshKind === "invalidation") {
549
+ this.generation += 1;
399
550
  this.controller?.abort();
400
551
  this.queueStart(void 0, "manual", true);
401
552
  return this.pendingRefresh ?? Promise.resolve();
@@ -404,14 +555,27 @@ var QueryCell = class {
404
555
  this.queueStart(void 0, "manual");
405
556
  return this.pendingRefresh ?? Promise.resolve();
406
557
  }
407
- invalidate() {
558
+ /** @internal Show an invalidation while a collection waits for a fetch slot. */
559
+ markQueuedInvalidation() {
408
560
  if (this.destroyed) return;
561
+ this.generation += 1;
562
+ this.controller?.abort();
563
+ this.setState(this.state.data === null ? loadingQueryState() : refreshingQueryState(this.state.data, this.state.consistency === "pending-write" ? "pending-write" : "refreshing"));
564
+ }
565
+ invalidate() {
566
+ if (this.destroyed) return Promise.resolve();
567
+ if (this.gcTimer !== null) {
568
+ this.destroy();
569
+ return Promise.resolve();
570
+ }
409
571
  if (this.pendingRefresh) {
572
+ this.generation += 1;
410
573
  this.controller?.abort();
411
574
  this.queueStart(void 0, "invalidation", true);
412
- return;
575
+ return this.pendingRefresh ?? Promise.resolve();
413
576
  }
414
577
  this.queueStart(void 0, "invalidation");
578
+ return this.pendingRefresh ?? Promise.resolve();
415
579
  }
416
580
  markPendingWrite() {
417
581
  if (this.destroyed) return;
@@ -429,7 +593,7 @@ var QueryCell = class {
429
593
  if (!continuePending || !this.pendingRefresh) this.pendingRefresh = new Promise((resolve) => {
430
594
  this.pendingRefreshResolve = resolve;
431
595
  });
432
- requestRuntimeWork("component", new QueryStartWork(() => {
596
+ schedule(new QueryStartWork(() => {
433
597
  if (token !== this.pendingRefreshToken) return;
434
598
  if (this.destroyed) {
435
599
  this.finishPendingRefresh(token);
@@ -442,7 +606,7 @@ var QueryCell = class {
442
606
  if (token !== this.pendingRefreshToken) return;
443
607
  this.generation += 1;
444
608
  this.finishPendingRefresh(token);
445
- }));
609
+ }), "effect");
446
610
  }
447
611
  finishPendingRefresh(token = this.pendingRefreshToken) {
448
612
  if (token !== this.pendingRefreshToken) return;
@@ -455,7 +619,7 @@ var QueryCell = class {
455
619
  setState(next) {
456
620
  if (this.destroyed) return;
457
621
  this.state = next;
458
- notifySource(this.source);
622
+ notify(this.source);
459
623
  }
460
624
  async start(reconcileSequence) {
461
625
  if (this.destroyed) return;
@@ -466,9 +630,10 @@ var QueryCell = class {
466
630
  this.controller = controller;
467
631
  const hasData = this.state.data !== null;
468
632
  this.setState(hasData ? refreshingQueryState(this.state.data) : loadingQueryState());
633
+ const { fetch, isConsistent: checkConsistent, reconcile } = this.options;
469
634
  let nextData;
470
635
  try {
471
- nextData = await this.options.fetch({ signal: controller.signal });
636
+ nextData = await fetch({ signal: controller.signal });
472
637
  } catch (error) {
473
638
  if (this.destroyed || !isCurrentAsyncOperation(this.generation, generation, this.controller, controller)) return;
474
639
  if (isAbortError(error, controller.signal)) {
@@ -481,7 +646,7 @@ var QueryCell = class {
481
646
  if (this.destroyed || !isCurrentAsyncOperation(this.generation, generation, this.controller, controller)) return;
482
647
  let isConsistent;
483
648
  try {
484
- isConsistent = this.options.isConsistent?.(nextData) ?? true;
649
+ isConsistent = checkConsistent?.(nextData) ?? true;
485
650
  } catch (error) {
486
651
  this.setState(errorQueryState(this.state.data, normalizeAsyncDataError(error, "Query consistency check failed")));
487
652
  return;
@@ -489,7 +654,7 @@ var QueryCell = class {
489
654
  if (!isConsistent) {
490
655
  this.setState(staleQueryState(nextData, "inconsistent"));
491
656
  try {
492
- await this.reconcile(nextData, generation, controller, reconcileSequence);
657
+ await this.reconcile(reconcile, nextData, generation, controller, reconcileSequence);
493
658
  } catch (error) {
494
659
  if (isCurrentAsyncOperation(this.generation, generation, this.controller, controller)) this.setState(errorQueryState(this.state.data, normalizeAsyncDataError(error, "Query reconciliation failed")));
495
660
  }
@@ -498,8 +663,8 @@ var QueryCell = class {
498
663
  this.reconcileAttemptCount = 0;
499
664
  this.setState(freshQueryState(nextData));
500
665
  }
501
- async reconcile(data, generation, controller, reconcileSequence) {
502
- if (!await (this.options.reconcile?.(data, { key: this.options.key }) ?? false) || this.destroyed || reconcileSequence !== this.reconcileSequence || !isCurrentAsyncOperation(this.generation, generation, this.controller, controller)) return;
666
+ async reconcile(reconcile, data, generation, controller, reconcileSequence) {
667
+ if (!await (reconcile?.(data, { key: this.key }) ?? false) || this.destroyed || reconcileSequence !== this.reconcileSequence || !isCurrentAsyncOperation(this.generation, generation, this.controller, controller)) return;
503
668
  this.reconcileAttemptCount += 1;
504
669
  if (this.reconcileAttemptCount > RECONCILE_MAX_ATTEMPTS) {
505
670
  this.setState(staleQueryState(this.state.data, "inconsistent"));
@@ -511,63 +676,71 @@ var QueryCell = class {
511
676
  this.queueStart(reconcileSequence, "reconcile", true);
512
677
  }
513
678
  };
679
+ function isServerRender() {
680
+ const context = getActiveRenderContext();
681
+ return context?.mode === "ssr" || context?.mode === void 0 && typeof window === "undefined";
682
+ }
683
+ function createCell(options, cache) {
684
+ const cell = new QueryCell(options.takeInitialData ? {
685
+ ...options,
686
+ initialData: options.takeInitialData() ?? options.initialData
687
+ } : options, options.key, cache);
688
+ cache.set(options.key, cell);
689
+ cell.ensureStarted();
690
+ return cell;
691
+ }
514
692
  function createLegacyQuery(options) {
515
- const instance = getCurrentComponentInstance();
693
+ validateGcTime(options.gcTime);
694
+ const instance = currentComponent();
516
695
  const runtimeState = resolveDataRuntimeState(options.runtime);
517
696
  const cache = runtimeState.queryCache;
518
697
  const override = runtimeState.queryTestOverrides.get(options.key);
519
698
  if (!instance) {
520
699
  if (override) return override;
521
700
  let cell = cache.get(options.key);
522
- if (!cell) {
523
- cell = new QueryCell(options, options.key, cache);
524
- cache.set(options.key, cell);
525
- cell.ensureStarted();
526
- } else cell.warnOnConflictingDefinition(options);
701
+ if (cell?.retireInactiveReaderCacheEntry()) cell = void 0;
702
+ if (!cell) cell = createCell(options, cache);
703
+ else cell.warnOnConflictingDefinition(options);
704
+ cell.scheduleUnownedCacheEviction(options.gcTime ?? DEFAULT_OWNERLESS_GC_TIME_MS);
527
705
  return cell;
528
706
  }
529
- const hookIndex = claimHookIndex(instance, "query");
707
+ const hookIndex = claimHookIndex(instance, "createQuery");
530
708
  ensureQueryCleanup(runtimeState, instance);
709
+ const generation = instance;
531
710
  const slotStore = getQuerySlotStore(runtimeState, instance);
532
711
  const existingSlot = slotStore.get(hookIndex);
533
712
  if (existingSlot && existingSlot.key === options.key) {
534
- existingSlot.cell.warnOnConflictingDefinition(options);
713
+ existingSlot.cell.define(options, generation, hookIndex);
535
714
  return existingSlot.cell;
536
715
  }
537
- if (existingSlot) existingSlot.cell.detach(getComponentLifetimeIdentity(instance), hookIndex);
716
+ if (existingSlot) existingSlot.cell.detach(generation, hookIndex);
538
717
  if (override) return override;
539
- let cell = cache.get(options.key);
540
- if (!cell) {
541
- cell = new QueryCell(options, options.key, cache);
542
- cache.set(options.key, cell);
543
- cell.ensureStarted();
544
- } else cell.warnOnConflictingDefinition(options);
718
+ const cell = cache.get(options.key) ?? createCell(options, cache);
545
719
  slotStore.set(hookIndex, {
546
720
  key: options.key,
547
721
  cell
548
722
  });
549
- cell.attach(getComponentLifetimeIdentity(instance), hookIndex);
723
+ cell.attach(generation, hookIndex);
724
+ cell.define(options, generation, hookIndex);
725
+ cell.ensureStarted();
550
726
  return cell;
551
727
  }
552
728
  function createDefinedQuery(definition, input, options = {}) {
553
729
  const runtime = options.runtime;
554
- const context = getActiveRenderContext();
555
730
  const key = definition.key(input);
556
- const dataRuntime = runtime ?? getActiveRenderContext()?.dataRuntime ?? getCurrentAppRenderRuntime()?.dataRuntime ?? getDefaultDataRuntime();
557
- const initialData = dataRuntime?.queryData.get(key);
731
+ const dataRuntime = runtime ?? getActiveRenderContext()?.dataRuntime ?? currentAppRuntime()?.dataRuntime ?? getDefaultDataRuntime();
732
+ const serverRender = isServerRender();
733
+ const runtimeState = resolveDataRuntimeState(dataRuntime);
558
734
  return createLegacyQuery({
559
735
  ...options,
560
736
  key,
561
737
  definitionIdentity: definition,
562
- fetch: ({ signal }) => definition.fetch({
563
- ...input,
564
- signal
565
- }),
738
+ fetch: ({ signal }) => definition.fetch(input, { signal }),
566
739
  isConsistent: definition.isConsistent,
567
740
  reconcile: definition.reconcile,
568
- initialData,
569
- skipInitialFetch: context?.mode === "ssr" || context?.mode === void 0 && typeof window === "undefined",
570
- runtime: dataRuntime ?? options.runtime
741
+ takeInitialData: () => readQueryData(runtimeState, key, !serverRender),
742
+ skipInitialFetch: serverRender || options.skipInitialFetch === true,
743
+ runtime: dataRuntime
571
744
  });
572
745
  }
573
746
  function createQuery(optionsOrDefinition, input, options) {
@@ -592,17 +765,13 @@ function getCollectionStore(generation) {
592
765
  }
593
766
  return store;
594
767
  }
595
- function createCellOptions(runtimeState, query, input, queryKey) {
768
+ function createCellOptions(query, input, queryKey) {
596
769
  return {
597
770
  key: queryKey,
598
771
  definitionIdentity: query,
599
- fetch: ({ signal }) => query.fetch({
600
- ...input,
601
- signal
602
- }),
772
+ fetch: ({ signal }) => query.fetch(input, { signal }),
603
773
  isConsistent: query.isConsistent,
604
774
  reconcile: query.reconcile,
605
- initialData: runtimeState.queryData.get(queryKey),
606
775
  skipInitialFetch: true
607
776
  };
608
777
  }
@@ -611,12 +780,25 @@ var QueryCollectionCell = class {
611
780
  this.runtimeState = runtimeState;
612
781
  this.records = /* @__PURE__ */ new Map();
613
782
  this.ordered = [];
783
+ this.registeredCells = /* @__PURE__ */ new Set();
614
784
  this.tasks = /* @__PURE__ */ new Map();
615
785
  this.queue = [];
616
786
  this.activeCount = 0;
617
787
  this.concurrency = DEFAULT_QUERY_COLLECTION_CONCURRENCY;
618
788
  this.disposed = false;
619
789
  }
790
+ get invalidationConcurrency() {
791
+ return this.concurrency;
792
+ }
793
+ invalidateCell(cell) {
794
+ const ownedCell = cell;
795
+ if (this.tasks.get(ownedCell)?.state === "active") {
796
+ ownedCell.invalidate();
797
+ return;
798
+ }
799
+ ownedCell.markQueuedInvalidation();
800
+ this.schedule(ownedCell, "invalidation");
801
+ }
620
802
  get entries() {
621
803
  return this.ordered;
622
804
  }
@@ -663,12 +845,12 @@ var QueryCollectionCell = class {
663
845
  const startCandidates = /* @__PURE__ */ new Set();
664
846
  const detachedCells = /* @__PURE__ */ new Set();
665
847
  for (const { input, key, queryKey } of desired) {
666
- const cellOptions = createCellOptions(this.runtimeState, query, input, queryKey);
848
+ const cellOptions = createCellOptions(query, input, queryKey);
667
849
  const previous = this.records.get(key);
668
850
  let record;
669
851
  if (previous?.queryKey === queryKey) {
670
852
  previous.input = input;
671
- previous.cell.warnOnConflictingDefinition(cellOptions);
853
+ previous.cell.define(cellOptions, previous.owner, 0);
672
854
  record = previous;
673
855
  } else {
674
856
  if (previous) {
@@ -678,11 +860,16 @@ var QueryCollectionCell = class {
678
860
  const cache = this.runtimeState.queryCache;
679
861
  let cell = cache.get(queryKey);
680
862
  if (!cell) {
681
- cell = new QueryCell(cellOptions, queryKey, cache);
863
+ const initialData = readQueryData(this.runtimeState, queryKey, startInitialFetches);
864
+ cell = new QueryCell({
865
+ ...cellOptions,
866
+ initialData
867
+ }, queryKey, cache);
682
868
  cache.set(queryKey, cell);
683
- } else cell.warnOnConflictingDefinition(cellOptions);
869
+ }
684
870
  const owner = {};
685
871
  cell.attach(owner, 0);
872
+ cell.define(cellOptions, owner, 0);
686
873
  record = {
687
874
  key,
688
875
  input,
@@ -700,8 +887,12 @@ var QueryCollectionCell = class {
700
887
  this.detach(record);
701
888
  detachedCells.add(record.cell);
702
889
  }
890
+ const nextCells = new Set(nextOrdered.map((record) => record.cell));
703
891
  this.records = nextRecords;
704
892
  this.ordered = Object.freeze(nextOrdered);
893
+ for (const cell of this.registeredCells) if (!nextCells.has(cell)) unregisterCollectionCell(cell, this);
894
+ for (const cell of nextCells) if (!this.registeredCells.has(cell)) registerCollectionCell(cell, this);
895
+ this.registeredCells = nextCells;
705
896
  for (const cell of detachedCells) this.cancelIfUnused(cell);
706
897
  for (const cell of startCandidates) this.schedule(cell);
707
898
  this.pump();
@@ -709,6 +900,8 @@ var QueryCollectionCell = class {
709
900
  dispose() {
710
901
  if (this.disposed) return;
711
902
  this.disposed = true;
903
+ for (const cell of this.registeredCells) unregisterCollectionCell(cell, this);
904
+ this.registeredCells.clear();
712
905
  const cells = /* @__PURE__ */ new Set();
713
906
  for (const record of this.records.values()) {
714
907
  cells.add(record.cell);
@@ -730,15 +923,20 @@ var QueryCollectionCell = class {
730
923
  const task = this.tasks.get(cell);
731
924
  if (!task || task.state === "cancelled" || task.state === "done") return;
732
925
  if (task.state === "active") this.activeCount -= 1;
926
+ const rescheduleInvalidation = task.state === "queued" && task.kind === "invalidation";
733
927
  task.state = "cancelled";
734
928
  this.tasks.delete(cell);
735
929
  task.resolve();
930
+ if (rescheduleInvalidation && !invalidateCollectionCell(cell)) cell.invalidate();
736
931
  this.pump();
737
932
  }
738
- schedule(cell) {
933
+ schedule(cell, kind = "refresh") {
739
934
  if (this.disposed) return Promise.resolve();
740
935
  const existing = this.tasks.get(cell);
741
- if (existing) return existing.promise;
936
+ if (existing) {
937
+ if (kind === "invalidation") existing.kind = kind;
938
+ return existing.promise;
939
+ }
742
940
  let resolve;
743
941
  const promise = new Promise((done) => {
744
942
  resolve = done;
@@ -747,7 +945,8 @@ var QueryCollectionCell = class {
747
945
  cell,
748
946
  promise,
749
947
  resolve,
750
- state: "queued"
948
+ state: "queued",
949
+ kind
751
950
  };
752
951
  this.tasks.set(cell, task);
753
952
  this.queue.push(task);
@@ -762,7 +961,7 @@ var QueryCollectionCell = class {
762
961
  if (task.state !== "queued" || this.tasks.get(task.cell) !== task) continue;
763
962
  task.state = "active";
764
963
  this.activeCount += 1;
765
- task.cell.refresh().finally(() => this.finish(task));
964
+ (task.kind === "invalidation" ? task.cell.invalidate() : task.cell.refresh()).finally(() => this.finish(task));
766
965
  }
767
966
  }
768
967
  finish(task) {
@@ -779,13 +978,13 @@ var QueryCollectionCell = class {
779
978
  * reusable query definition, with bounded collection-started fetches.
780
979
  */
781
980
  function createQueryCollection(options) {
782
- const instance = getCurrentComponentInstance();
981
+ const instance = currentComponent();
783
982
  if (!instance) throw new Error("[Askr] createQueryCollection() must be called during component render execution.");
784
983
  const hookIndex = claimHookIndex(instance, "createQueryCollection");
785
984
  const concurrency = normalizeQueryCollectionConcurrency(options.concurrency);
786
985
  const inputs = options.inputs();
787
986
  if (!Array.isArray(inputs)) throw new Error("[Askr] createQueryCollection() inputs must return a readonly array.");
788
- const generation = getComponentLifetimeIdentity(instance);
987
+ const generation = instance;
789
988
  const runtimeState = resolveDataRuntimeState(options.runtime);
790
989
  const store = getCollectionStore(generation);
791
990
  let slot = store.get(hookIndex);
@@ -800,7 +999,7 @@ function createQueryCollection(options) {
800
999
  collection: new QueryCollectionCell(runtimeState)
801
1000
  };
802
1001
  store.set(hookIndex, slot);
803
- ownComponentCleanup(instance, () => {
1002
+ instance.onCleanup(() => {
804
1003
  const current = store.get(hookIndex);
805
1004
  try {
806
1005
  current?.collection.dispose();