@kontsedal/olas-core 0.0.1-rc.1 → 0.0.2

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 (45) hide show
  1. package/dist/index.cjs +40 -10
  2. package/dist/index.cjs.map +1 -1
  3. package/dist/index.d.cts +32 -11
  4. package/dist/index.d.cts.map +1 -1
  5. package/dist/index.d.mts +32 -11
  6. package/dist/index.d.mts.map +1 -1
  7. package/dist/index.mjs +40 -11
  8. package/dist/index.mjs.map +1 -1
  9. package/dist/{root-BImHnGj1.mjs → root-De-6KWIZ.mjs} +750 -149
  10. package/dist/root-De-6KWIZ.mjs.map +1 -0
  11. package/dist/{root-Bazp5_Ik.cjs → root-XKEsSmcd.cjs} +755 -148
  12. package/dist/root-XKEsSmcd.cjs.map +1 -0
  13. package/dist/testing.cjs +1 -1
  14. package/dist/testing.d.cts +1 -1
  15. package/dist/testing.d.mts +1 -1
  16. package/dist/testing.mjs +1 -1
  17. package/dist/{types-CAMgqCMz.d.mts → types-C-zV1JZA.d.mts} +215 -13
  18. package/dist/types-C-zV1JZA.d.mts.map +1 -0
  19. package/dist/{types-emq_lZd7.d.cts → types-DKfpkm17.d.cts} +215 -13
  20. package/dist/types-DKfpkm17.d.cts.map +1 -0
  21. package/package.json +1 -1
  22. package/src/controller/index.ts +6 -0
  23. package/src/controller/instance.ts +432 -18
  24. package/src/controller/root.ts +9 -1
  25. package/src/controller/types.ts +148 -7
  26. package/src/emitter.ts +34 -3
  27. package/src/forms/field.ts +73 -8
  28. package/src/forms/form-types.ts +16 -0
  29. package/src/forms/form.ts +218 -26
  30. package/src/index.ts +12 -1
  31. package/src/query/client.ts +161 -6
  32. package/src/query/define.ts +14 -0
  33. package/src/query/entry.ts +64 -42
  34. package/src/query/infinite.ts +77 -55
  35. package/src/query/mutation.ts +11 -21
  36. package/src/query/plugin.ts +50 -0
  37. package/src/query/use.ts +80 -3
  38. package/src/signals/readonly.ts +3 -3
  39. package/src/timing/debounced.ts +24 -4
  40. package/src/timing/throttled.ts +22 -3
  41. package/src/utils.ts +32 -4
  42. package/dist/root-BImHnGj1.mjs.map +0 -1
  43. package/dist/root-Bazp5_Ik.cjs.map +0 -1
  44. package/dist/types-CAMgqCMz.d.mts.map +0 -1
  45. package/dist/types-emq_lZd7.d.cts.map +0 -1
@@ -212,16 +212,41 @@ function untracked$1(fn) {
212
212
  //#endregion
213
213
  //#region src/utils.ts
214
214
  /**
215
- * True iff `err` is an AbortError. Used to filter superseded latest-wins
216
- * mutations and aborted fetches from genuine failures.
215
+ * True iff `err` looks like an AbortError. Matches the standard `DOMException`
216
+ * shape thrown by `AbortController` AND any object whose `name === 'AbortError'`
217
+ * — that covers axios / msw / user-thrown plain Errors that signal abort.
217
218
  *
218
- * Spec: §20.12 checks `err instanceof DOMException && err.name === 'AbortError'`.
219
- * Node 17+ exposes a global DOMException, so this works server-side too.
219
+ * Spec: §20.12. Node 17+ exposes a global DOMException, so the instanceof
220
+ * branch works server-side; the name-based branch is the portable fallback.
220
221
  */
221
222
  function isAbortError(err) {
222
223
  if (typeof DOMException !== "undefined" && err instanceof DOMException) return err.name === "AbortError";
224
+ if (err != null && typeof err === "object" && "name" in err) return err.name === "AbortError";
223
225
  return false;
224
226
  }
227
+ /**
228
+ * `setTimeout` wrapped in a promise that rejects with `AbortError` if the
229
+ * passed signal fires. Internal — used by the retry loops in `Entry`,
230
+ * `InfiniteEntry`, and `Mutation` so a slow backoff never blocks a supersede.
231
+ */
232
+ function abortableSleep(ms, signal) {
233
+ return new Promise((resolve, reject) => {
234
+ if (signal.aborted) {
235
+ reject(new DOMException("Aborted", "AbortError"));
236
+ return;
237
+ }
238
+ const timer = setTimeout(() => {
239
+ signal.removeEventListener("abort", onAbort);
240
+ resolve();
241
+ }, ms);
242
+ const onAbort = () => {
243
+ clearTimeout(timer);
244
+ signal.removeEventListener("abort", onAbort);
245
+ reject(new DOMException("Aborted", "AbortError"));
246
+ };
247
+ signal.addEventListener("abort", onAbort, { once: true });
248
+ });
249
+ }
225
250
  //#endregion
226
251
  //#region src/query/entry.ts
227
252
  /**
@@ -250,18 +275,37 @@ var Entry = class {
250
275
  nextSnapshotId = 0;
251
276
  disposed = false;
252
277
  events;
278
+ onSuccessData;
253
279
  fetchStartTime = 0;
280
+ /**
281
+ * Promises returned by `firstValue()` that haven't settled. Rejected on
282
+ * `dispose()` so awaiters (most notably `prefetch` and `subscription.firstValue`)
283
+ * don't hang when the controller tree is torn down mid-fetch.
284
+ */
285
+ pendingFirstValueRejects = [];
254
286
  constructor(options) {
255
287
  this.fetcherProvider = options.fetcher;
256
288
  this.staleTime = options.staleTime ?? 0;
257
289
  this.retry = options.retry ?? 0;
258
290
  this.retryDelay = options.retryDelay ?? 1e3;
259
291
  this.events = options.events ?? {};
292
+ this.onSuccessData = options.onSuccessData;
260
293
  this.data = signal$1(options.initialData);
261
294
  if (options.initialData !== void 0) {
262
295
  this.status = signal$1("success");
263
- this.scheduleStaleness();
264
- this.isStale.set(this.staleTime === 0);
296
+ if (this.staleTime === 0) this.isStale.set(true);
297
+ else {
298
+ const last = options.initialUpdatedAt;
299
+ const alreadyStale = last === void 0 || Date.now() - last >= this.staleTime;
300
+ this.isStale.set(alreadyStale);
301
+ if (!alreadyStale) {
302
+ const remaining = this.staleTime - (Date.now() - last);
303
+ this.staleTimer = setTimeout(() => {
304
+ this.staleTimer = null;
305
+ if (!this.disposed) this.isStale.set(true);
306
+ }, remaining);
307
+ }
308
+ }
265
309
  } else this.status = signal$1("idle");
266
310
  this.lastUpdatedAt = signal$1(options.initialUpdatedAt);
267
311
  }
@@ -294,7 +338,7 @@ var Entry = class {
294
338
  } catch (err) {
295
339
  if (myId !== this.currentFetchId || this.disposed || isAbortError(err)) throw err;
296
340
  if (!this.shouldRetry(attempt, err)) return this.applyFailure(err);
297
- await abortableSleep$2(this.computeDelay(attempt), abort.signal);
341
+ await abortableSleep(this.computeDelay(attempt), abort.signal);
298
342
  attempt += 1;
299
343
  }
300
344
  }
@@ -323,6 +367,7 @@ var Entry = class {
323
367
  try {
324
368
  this.events.onFetchSuccess?.(Date.now() - this.fetchStartTime);
325
369
  } catch {}
370
+ this.onSuccessData?.(result);
326
371
  return result;
327
372
  }
328
373
  applyFailure(err) {
@@ -401,26 +446,24 @@ var Entry = class {
401
446
  }
402
447
  };
403
448
  }
404
- finalizeSnapshot(snapshot) {
405
- const id = snapshotIds.get(snapshot);
406
- if (id === void 0) return;
407
- const record = this.snapshots.find((s) => s.live && s.id === id);
408
- if (!record) return;
409
- record.live = false;
410
- this.snapshots = this.snapshots.filter((s) => s !== record);
411
- if (!this.snapshots.some((s) => s.live)) this.hasPendingMutations.set(false);
412
- }
413
449
  firstValue() {
450
+ if (this.disposed) return Promise.reject(new DOMException("Entry disposed", "AbortError"));
414
451
  if (this.status.peek() === "success") return Promise.resolve(this.data.peek());
415
452
  if (this.status.peek() === "error") return Promise.reject(this.error.peek());
416
453
  return new Promise((resolve, reject) => {
454
+ const tracked = (err) => {
455
+ this.pendingFirstValueRejects = this.pendingFirstValueRejects.filter((f) => f !== tracked);
456
+ reject(err);
457
+ };
458
+ this.pendingFirstValueRejects.push(tracked);
417
459
  const unsub = this.status.subscribe((s) => {
418
460
  if (s === "success") {
419
461
  unsub();
462
+ this.pendingFirstValueRejects = this.pendingFirstValueRejects.filter((f) => f !== tracked);
420
463
  resolve(this.data.peek());
421
464
  } else if (s === "error") {
422
465
  unsub();
423
- reject(this.error.peek());
466
+ tracked(this.error.peek());
424
467
  }
425
468
  });
426
469
  });
@@ -443,27 +486,14 @@ var Entry = class {
443
486
  }
444
487
  this.currentAbort?.abort();
445
488
  this.currentAbort = null;
489
+ if (this.pendingFirstValueRejects.length > 0) {
490
+ const disposed = new DOMException("Entry disposed", "AbortError");
491
+ const rejects = this.pendingFirstValueRejects;
492
+ this.pendingFirstValueRejects = [];
493
+ for (const fn of rejects) fn(disposed);
494
+ }
446
495
  }
447
496
  };
448
- const snapshotIds = /* @__PURE__ */ new WeakMap();
449
- function abortableSleep$2(ms, signal) {
450
- return new Promise((resolve, reject) => {
451
- if (signal.aborted) {
452
- reject(new DOMException("Aborted", "AbortError"));
453
- return;
454
- }
455
- const timer = setTimeout(() => {
456
- signal.removeEventListener("abort", onAbort);
457
- resolve();
458
- }, ms);
459
- const onAbort = () => {
460
- clearTimeout(timer);
461
- signal.removeEventListener("abort", onAbort);
462
- reject(new DOMException("Aborted", "AbortError"));
463
- };
464
- signal.addEventListener("abort", onAbort, { once: true });
465
- });
466
- }
467
497
  //#endregion
468
498
  //#region src/query/focus-online.ts
469
499
  const focusSubs = /* @__PURE__ */ new Set();
@@ -539,6 +569,8 @@ var InfiniteEntry = class {
539
569
  snapshots = [];
540
570
  nextSnapshotId = 0;
541
571
  disposed = false;
572
+ /** Mirrors `Entry.pendingFirstValueRejects` — see that field for context. */
573
+ pendingFirstValueRejects = [];
542
574
  fetcher;
543
575
  initialPageParam;
544
576
  getNextPageParam;
@@ -547,6 +579,13 @@ var InfiniteEntry = class {
547
579
  retry;
548
580
  retryDelay;
549
581
  itemsOf;
582
+ /**
583
+ * Mirrors `Entry.onSuccessData`. Fires from `applyFetchSuccess`-equivalent
584
+ * branches AFTER `pages.set(...)` settles. Used by `InfiniteClientEntry`
585
+ * to emit `SetDataEvent { kind: 'infinite', source: 'fetch' }` for
586
+ * `QueryClientPlugin`s (e.g. entity normalization).
587
+ */
588
+ onSuccessData;
550
589
  constructor(opts) {
551
590
  this.fetcher = opts.fetcher;
552
591
  this.initialPageParam = opts.initialPageParam;
@@ -556,6 +595,7 @@ var InfiniteEntry = class {
556
595
  this.staleTime = opts.staleTime ?? 0;
557
596
  this.retry = opts.retry ?? 0;
558
597
  this.retryDelay = opts.retryDelay ?? 1e3;
598
+ this.onSuccessData = opts.onSuccessData;
559
599
  this.pageParams = signal$1([]);
560
600
  this.data = computed$1(() => {
561
601
  const ps = this.pages.value;
@@ -607,6 +647,7 @@ var InfiniteEntry = class {
607
647
  this.isStale.set(this.staleTime === 0);
608
648
  });
609
649
  if (this.staleTime > 0) this.scheduleStaleness();
650
+ this.onSuccessData?.(this.pages.peek());
610
651
  }, "initial");
611
652
  }
612
653
  fetchNextPage() {
@@ -633,6 +674,7 @@ var InfiniteEntry = class {
633
674
  this.isFetching.set(false);
634
675
  this.lastUpdatedAt.set(Date.now());
635
676
  });
677
+ this.onSuccessData?.(this.pages.peek());
636
678
  }, "next").then(() => {});
637
679
  }
638
680
  fetchPreviousPage() {
@@ -660,36 +702,46 @@ var InfiniteEntry = class {
660
702
  this.isFetching.set(false);
661
703
  this.lastUpdatedAt.set(Date.now());
662
704
  });
705
+ this.onSuccessData?.(this.pages.peek());
663
706
  }, "prev").then(() => {});
664
707
  }
665
708
  async runFetch(myId, signal, pageParam, onSuccess, direction) {
666
709
  let attempt = 0;
667
- while (true) {
668
- if (myId !== this.currentFetchId || this.disposed) throw new DOMException("Superseded", "AbortError");
669
- try {
670
- const page = await this.fetcher({
671
- pageParam,
672
- signal
673
- });
710
+ let succeeded = false;
711
+ try {
712
+ while (true) {
674
713
  if (myId !== this.currentFetchId || this.disposed) throw new DOMException("Superseded", "AbortError");
675
- onSuccess(page, pageParam);
676
- return page;
677
- } catch (err) {
678
- if (myId !== this.currentFetchId || this.disposed || isAbortError(err)) throw err;
679
- if (!(typeof this.retry === "number" ? attempt < this.retry : this.retry(attempt, err))) {
680
- batch$1(() => {
681
- this.error.set(err);
682
- this.status.set("error");
683
- this.isLoading.set(false);
684
- this.isFetching.set(false);
685
- if (direction === "next") this.isFetchingNextPage.set(false);
686
- if (direction === "prev") this.isFetchingPreviousPage.set(false);
714
+ try {
715
+ const page = await this.fetcher({
716
+ pageParam,
717
+ signal
687
718
  });
688
- throw err;
719
+ if (myId !== this.currentFetchId || this.disposed) throw new DOMException("Superseded", "AbortError");
720
+ onSuccess(page, pageParam);
721
+ succeeded = true;
722
+ return page;
723
+ } catch (err) {
724
+ if (myId !== this.currentFetchId || this.disposed || isAbortError(err)) throw err;
725
+ if (!(typeof this.retry === "number" ? attempt < this.retry : this.retry(attempt, err))) {
726
+ batch$1(() => {
727
+ this.error.set(err);
728
+ this.status.set("error");
729
+ this.isLoading.set(false);
730
+ this.isFetching.set(false);
731
+ if (direction === "next") this.isFetchingNextPage.set(false);
732
+ if (direction === "prev") this.isFetchingPreviousPage.set(false);
733
+ });
734
+ throw err;
735
+ }
736
+ await abortableSleep(typeof this.retryDelay === "function" ? this.retryDelay(attempt) : this.retryDelay, signal);
737
+ attempt += 1;
689
738
  }
690
- await abortableSleep$1(typeof this.retryDelay === "function" ? this.retryDelay(attempt) : this.retryDelay, signal);
691
- attempt += 1;
692
739
  }
740
+ } finally {
741
+ if (!succeeded) batch$1(() => {
742
+ if (direction === "next") this.isFetchingNextPage.set(false);
743
+ if (direction === "prev") this.isFetchingPreviousPage.set(false);
744
+ });
693
745
  }
694
746
  }
695
747
  refetch() {
@@ -749,16 +801,23 @@ var InfiniteEntry = class {
749
801
  };
750
802
  }
751
803
  firstValue() {
804
+ if (this.disposed) return Promise.reject(new DOMException("Entry disposed", "AbortError"));
752
805
  if (this.status.peek() === "success") return Promise.resolve(this.pages.peek());
753
806
  if (this.status.peek() === "error") return Promise.reject(this.error.peek());
754
807
  return new Promise((resolve, reject) => {
808
+ const tracked = (err) => {
809
+ this.pendingFirstValueRejects = this.pendingFirstValueRejects.filter((f) => f !== tracked);
810
+ reject(err);
811
+ };
812
+ this.pendingFirstValueRejects.push(tracked);
755
813
  const unsub = this.status.subscribe((s) => {
756
814
  if (s === "success") {
757
815
  unsub();
816
+ this.pendingFirstValueRejects = this.pendingFirstValueRejects.filter((f) => f !== tracked);
758
817
  resolve(this.pages.peek());
759
818
  } else if (s === "error") {
760
819
  unsub();
761
- reject(this.error.peek());
820
+ tracked(this.error.peek());
762
821
  }
763
822
  });
764
823
  });
@@ -784,26 +843,14 @@ var InfiniteEntry = class {
784
843
  }
785
844
  this.currentAbort?.abort();
786
845
  this.currentAbort = null;
846
+ if (this.pendingFirstValueRejects.length > 0) {
847
+ const disposed = new DOMException("Entry disposed", "AbortError");
848
+ const rejects = this.pendingFirstValueRejects;
849
+ this.pendingFirstValueRejects = [];
850
+ for (const fn of rejects) fn(disposed);
851
+ }
787
852
  }
788
853
  };
789
- function abortableSleep$1(ms, signal) {
790
- return new Promise((resolve, reject) => {
791
- if (signal.aborted) {
792
- reject(new DOMException("Aborted", "AbortError"));
793
- return;
794
- }
795
- const timer = setTimeout(() => {
796
- signal.removeEventListener("abort", onAbort);
797
- resolve();
798
- }, ms);
799
- const onAbort = () => {
800
- clearTimeout(timer);
801
- signal.removeEventListener("abort", onAbort);
802
- reject(new DOMException("Aborted", "AbortError"));
803
- };
804
- signal.addEventListener("abort", onAbort, { once: true });
805
- });
806
- }
807
854
  //#endregion
808
855
  //#region src/query/keys.ts
809
856
  /**
@@ -870,7 +917,7 @@ var ClientEntry = class {
870
917
  refetchInterval;
871
918
  refetchOnWindowFocus;
872
919
  refetchOnReconnect;
873
- constructor(client, query, callArgs, keyArgs, spec, hydrated) {
920
+ constructor(client, query, callArgs, keyArgs, spec, hydrated, onFetchSuccess) {
874
921
  this.client = client;
875
922
  this.query = query;
876
923
  this.callArgs = callArgs;
@@ -893,7 +940,8 @@ var ClientEntry = class {
893
940
  retryDelay: spec.retryDelay,
894
941
  initialData: hydrated?.data,
895
942
  initialUpdatedAt: hydrated?.lastUpdatedAt,
896
- events: void 0
943
+ events: void 0,
944
+ onSuccessData: onFetchSuccess
897
945
  });
898
946
  }
899
947
  acquire() {
@@ -993,7 +1041,7 @@ var InfiniteClientEntry = class {
993
1041
  intervalTimer = null;
994
1042
  gcTime;
995
1043
  refetchInterval;
996
- constructor(client, query, callArgs, keyArgs, spec) {
1044
+ constructor(client, query, callArgs, keyArgs, spec, onFetchSuccess) {
997
1045
  this.client = client;
998
1046
  this.query = query;
999
1047
  this.callArgs = callArgs;
@@ -1014,7 +1062,8 @@ var InfiniteClientEntry = class {
1014
1062
  itemsOf: spec.itemsOf,
1015
1063
  staleTime: spec.staleTime,
1016
1064
  retry: spec.retry,
1017
- retryDelay: spec.retryDelay
1065
+ retryDelay: spec.retryDelay,
1066
+ onSuccessData: onFetchSuccess
1018
1067
  });
1019
1068
  }
1020
1069
  acquire() {
@@ -1130,6 +1179,9 @@ var QueryClient = class {
1130
1179
  applyRemoteInvalidate(queryId, keyArgs) {
1131
1180
  self.applyRemoteInvalidate(queryId, keyArgs);
1132
1181
  },
1182
+ setEntryData(queryId, keyArgs, updater) {
1183
+ self.setEntryData(queryId, keyArgs, updater);
1184
+ },
1133
1185
  subscribedKeys(queryId) {
1134
1186
  return self.subscribedKeysFor(queryId);
1135
1187
  }
@@ -1146,7 +1198,24 @@ var QueryClient = class {
1146
1198
  });
1147
1199
  }
1148
1200
  }
1149
- emitSetData(query, keyArgs, data, kind) {
1201
+ /**
1202
+ * Emit a `SetDataEvent` to every installed plugin. The `source` field
1203
+ * tells layered plugins where the write originated:
1204
+ * - `'set'`: explicit `client.setData`, including mutations and plugin-
1205
+ * initiated `setEntryData` calls (e.g. entity backpropagation).
1206
+ * - `'fetch'`: a query fetcher resolved successfully (`Entry.applySuccess`
1207
+ * reaches this through `onSuccessData`), or hydrated data was first
1208
+ * bound (a per-tab arrival of pre-fetched data; cross-tab skips
1209
+ * `'fetch'` so this stays a per-tab concern).
1210
+ * - `'remote'`: `applyRemoteSetData` — cross-tab / server-push. Mirrors
1211
+ * `isRemote === true`.
1212
+ *
1213
+ * Private — fetcher-success emission goes through the `onFetchSuccess`
1214
+ * closure that `bindEntry` builds and hands to each new `ClientEntry`.
1215
+ * Hydrated emission goes through this method directly from `bindEntry`.
1216
+ * Mutation / remote paths call it from within QueryClient methods.
1217
+ */
1218
+ emitSetData(query, keyArgs, data, kind, source) {
1150
1219
  if (this.plugins.length === 0) return;
1151
1220
  const queryId = query.__spec.queryId;
1152
1221
  if (queryId == null) return;
@@ -1155,7 +1224,8 @@ var QueryClient = class {
1155
1224
  keyArgs,
1156
1225
  data,
1157
1226
  kind,
1158
- isRemote: this.applyingRemote
1227
+ isRemote: this.applyingRemote,
1228
+ source
1159
1229
  };
1160
1230
  for (const plugin of this.plugins) if (plugin.onSetData) {
1161
1231
  const cb = plugin.onSetData;
@@ -1233,11 +1303,44 @@ var QueryClient = class {
1233
1303
  this.applyingRemote = true;
1234
1304
  try {
1235
1305
  entry.entry.setData(() => data);
1236
- this.emitSetData(internal, entry.keyArgs, data, "data");
1306
+ this.emitSetData(internal, entry.keyArgs, data, "data", "remote");
1237
1307
  } finally {
1238
1308
  this.applyingRemote = false;
1239
1309
  }
1240
1310
  }
1311
+ /**
1312
+ * Local-originated `setData` keyed by `queryId + keyArgs`. Plugin-facing
1313
+ * (exposed via `QueryClientPluginApi.setEntryData`); used by the
1314
+ * `@kontsedal/olas-entities` plugin to backpropagate entity patches into
1315
+ * every query holding the entity, without forcing the plugin to recover
1316
+ * the original `callArgs`.
1317
+ *
1318
+ * Drops silently in the same cases as `applyRemoteSetData` (unknown
1319
+ * queryId / infinite query / no local entry). Emits `SetDataEvent` with
1320
+ * `isRemote: false`, `source: 'set'` — cross-tab WILL rebroadcast.
1321
+ */
1322
+ setEntryData(queryId, keyArgs, updater) {
1323
+ const query = lookupRegisteredQuery(queryId);
1324
+ if (!query) return;
1325
+ const hash = stableHash(keyArgs);
1326
+ if (query.__olas === "query") {
1327
+ const internal = query;
1328
+ const map = this.maps.get(internal);
1329
+ if (!map) return;
1330
+ const entry = map.get(hash);
1331
+ if (!entry) return;
1332
+ entry.entry.setData(updater);
1333
+ this.emitSetData(internal, entry.keyArgs, entry.entry.data.peek(), "data", "set");
1334
+ return;
1335
+ }
1336
+ const internal = query;
1337
+ const map = this.infiniteMaps.get(internal);
1338
+ if (!map) return;
1339
+ const entry = map.get(hash);
1340
+ if (!entry) return;
1341
+ entry.entry.setData(updater);
1342
+ this.emitSetData(internal, entry.keyArgs, entry.entry.pages.peek(), "infinite", "set");
1343
+ }
1241
1344
  applyRemoteInvalidate(queryId, keyArgs) {
1242
1345
  const query = lookupRegisteredQuery(queryId);
1243
1346
  if (!query) return;
@@ -1251,6 +1354,7 @@ var QueryClient = class {
1251
1354
  this.applyingRemote = true;
1252
1355
  try {
1253
1356
  entry.entry.invalidate().catch((err) => {
1357
+ if (isAbortError(err)) return;
1254
1358
  dispatchError(this.onError, err, {
1255
1359
  kind: "cache",
1256
1360
  controllerPath: [],
@@ -1346,9 +1450,11 @@ var QueryClient = class {
1346
1450
  if (!entry) {
1347
1451
  const hydrated = this.hydratedData.get(hash);
1348
1452
  if (hydrated) this.hydratedData.delete(hash);
1349
- entry = new ClientEntry(this, internal, args, keyArgs, internal.__spec, hydrated);
1453
+ const onFetchSuccess = internal.__spec.queryId != null ? (data) => this.emitSetData(internal, keyArgs, data, "data", "fetch") : void 0;
1454
+ entry = new ClientEntry(this, internal, args, keyArgs, internal.__spec, hydrated, onFetchSuccess);
1350
1455
  map.set(hash, entry);
1351
1456
  entry.scheduleGcIfOrphan();
1457
+ if (hydrated !== void 0) this.emitSetData(internal, keyArgs, hydrated.data, "data", "fetch");
1352
1458
  }
1353
1459
  return entry;
1354
1460
  }
@@ -1371,6 +1477,7 @@ var QueryClient = class {
1371
1477
  const entry = map.get(hash);
1372
1478
  if (!entry) return;
1373
1479
  entry.entry.invalidate().catch((err) => {
1480
+ if (isAbortError(err)) return;
1374
1481
  dispatchError(this.onError, err, {
1375
1482
  kind: "cache",
1376
1483
  controllerPath: [],
@@ -1385,6 +1492,7 @@ var QueryClient = class {
1385
1492
  if (!map) return;
1386
1493
  for (const [hash, entry] of map) {
1387
1494
  entry.entry.invalidate().catch((err) => {
1495
+ if (isAbortError(err)) return;
1388
1496
  dispatchError(this.onError, err, {
1389
1497
  kind: "cache",
1390
1498
  controllerPath: [],
@@ -1397,7 +1505,7 @@ var QueryClient = class {
1397
1505
  setData(query, args, updater) {
1398
1506
  const entry = this.bindEntry(query, args);
1399
1507
  const snapshot = entry.entry.setData(updater);
1400
- this.emitSetData(entry.query, entry.keyArgs, entry.entry.data.peek(), "data");
1508
+ this.emitSetData(entry.query, entry.keyArgs, entry.entry.data.peek(), "data", "set");
1401
1509
  return snapshot;
1402
1510
  }
1403
1511
  bindInfiniteEntry(query, args) {
@@ -1413,7 +1521,8 @@ var QueryClient = class {
1413
1521
  const hash = stableHash(keyArgs);
1414
1522
  let entry = map.get(hash);
1415
1523
  if (!entry) {
1416
- entry = new InfiniteClientEntry(this, internal, args, keyArgs, internal.__spec);
1524
+ const onFetchSuccess = internal.__spec.queryId != null ? (pages) => this.emitSetData(internal, keyArgs, pages, "infinite", "fetch") : void 0;
1525
+ entry = new InfiniteClientEntry(this, internal, args, keyArgs, internal.__spec, onFetchSuccess);
1417
1526
  map.set(hash, entry);
1418
1527
  entry.scheduleGcIfOrphan();
1419
1528
  }
@@ -1438,6 +1547,7 @@ var QueryClient = class {
1438
1547
  const entry = map.get(hash);
1439
1548
  if (!entry) return;
1440
1549
  entry.entry.invalidate().catch((err) => {
1550
+ if (isAbortError(err)) return;
1441
1551
  dispatchError(this.onError, err, {
1442
1552
  kind: "cache",
1443
1553
  controllerPath: [],
@@ -1452,6 +1562,7 @@ var QueryClient = class {
1452
1562
  if (!map) return;
1453
1563
  for (const entry of map.values()) {
1454
1564
  entry.entry.invalidate().catch((err) => {
1565
+ if (isAbortError(err)) return;
1455
1566
  dispatchError(this.onError, err, {
1456
1567
  kind: "cache",
1457
1568
  controllerPath: [],
@@ -1464,7 +1575,7 @@ var QueryClient = class {
1464
1575
  setInfiniteData(query, args, updater) {
1465
1576
  const entry = this.bindInfiniteEntry(query, args);
1466
1577
  const snapshot = entry.entry.setData(updater);
1467
- this.emitSetData(entry.query, entry.keyArgs, entry.entry.pages.peek(), "infinite");
1578
+ this.emitSetData(entry.query, entry.keyArgs, entry.entry.pages.peek(), "infinite", "set");
1468
1579
  return snapshot;
1469
1580
  }
1470
1581
  prefetchInfinite(query, args) {
@@ -1521,12 +1632,25 @@ function waitUntilFalse(sig) {
1521
1632
  //#endregion
1522
1633
  //#region src/emitter.ts
1523
1634
  var EmitterImpl = class {
1635
+ onError;
1524
1636
  handlers = /* @__PURE__ */ new Set();
1525
1637
  disposed = false;
1638
+ constructor(onError) {
1639
+ this.onError = onError;
1640
+ }
1526
1641
  emit(value) {
1527
1642
  if (this.disposed) return;
1528
1643
  const snapshot = Array.from(this.handlers);
1529
- for (const handler of snapshot) handler(value);
1644
+ for (const handler of snapshot) try {
1645
+ handler(value);
1646
+ } catch (err) {
1647
+ if (this.onError) try {
1648
+ this.onError(err);
1649
+ } catch {
1650
+ console.error("[olas] emitter handler threw and reporter threw:", err);
1651
+ }
1652
+ else console.error("[olas] emitter handler threw:", err);
1653
+ }
1530
1654
  }
1531
1655
  on(handler) {
1532
1656
  if (this.disposed) return () => {};
@@ -1558,9 +1682,14 @@ var EmitterImpl = class {
1558
1682
  * (or the emitter is disposed). Use this for emitters that live outside any
1559
1683
  * single controller — typically in deps. Use `ctx.emitter()` for emitters that
1560
1684
  * should auto-clean with a controller.
1685
+ *
1686
+ * Pass `onError` to receive emit-time handler throws (spec §20.6 — one
1687
+ * throwing handler must not block the rest of the fan-out). `ctx.emitter()`
1688
+ * wires this to the root's `onError` so deps-level emitters get isolation
1689
+ * by default when constructed via `ctx`.
1561
1690
  */
1562
- function createEmitter() {
1563
- const impl = new EmitterImpl();
1691
+ function createEmitter(options) {
1692
+ const impl = new EmitterImpl(options?.onError);
1564
1693
  return {
1565
1694
  emit: ((value) => impl.emit(value)),
1566
1695
  on: (handler) => impl.on(handler),
@@ -1587,9 +1716,11 @@ var FieldImpl = class {
1587
1716
  runId = 0;
1588
1717
  disposed = false;
1589
1718
  devtoolsOwner = null;
1590
- constructor(initial, validators = []) {
1719
+ onValidatorError = null;
1720
+ constructor(initial, validators = [], options) {
1591
1721
  this.initial = initial;
1592
1722
  this.validators = validators;
1723
+ this.onValidatorError = options?.onValidatorError ?? null;
1593
1724
  this.value$ = signal$1(initial);
1594
1725
  this.errors$ = signal$1([]);
1595
1726
  this.touched$ = signal$1(false);
@@ -1601,6 +1732,13 @@ var FieldImpl = class {
1601
1732
  this.runValidators();
1602
1733
  });
1603
1734
  }
1735
+ /**
1736
+ * Internal hook for `ctx.field` / `createForm` to route synchronous
1737
+ * validator throws through `root.onError`. See `ValidatorErrorReporter`.
1738
+ */
1739
+ bindValidatorErrorReporter(reporter) {
1740
+ this.onValidatorError = reporter;
1741
+ }
1604
1742
  get value() {
1605
1743
  return this.value$.value;
1606
1744
  }
@@ -1707,10 +1845,15 @@ var FieldImpl = class {
1707
1845
  const myId = ++this.runId;
1708
1846
  const syncErrors = [];
1709
1847
  const asyncPromises = [];
1710
- for (const validator of this.validators) {
1848
+ for (const validator of this.validators) try {
1711
1849
  const result = validator(value, abort.signal);
1712
1850
  if (result instanceof Promise) asyncPromises.push(result);
1713
1851
  else if (result != null) syncErrors.push(result);
1852
+ } catch (err) {
1853
+ try {
1854
+ this.onValidatorError?.(err);
1855
+ } catch {}
1856
+ syncErrors.push(err instanceof Error ? err.message : String(err));
1714
1857
  }
1715
1858
  if (syncErrors.length > 0) {
1716
1859
  batch$1(() => {
@@ -1758,8 +1901,18 @@ function bindFieldDevtoolsOwner(field, owner) {
1758
1901
  const impl = field;
1759
1902
  if (typeof impl.bindDevtoolsOwner === "function") impl.bindDevtoolsOwner(owner);
1760
1903
  }
1761
- function createField(initial, validators) {
1762
- return new FieldImpl(initial, validators);
1904
+ /**
1905
+ * Internal install a synchronous-validator-throw reporter on a `Field`
1906
+ * (matched structurally to keep the public `Field<T>` surface stable).
1907
+ * Called by `ctx.field` and `bindTreeToDevtools` so leaves inside a form/
1908
+ * field-array tree get the same reporting as a standalone field.
1909
+ */
1910
+ function bindFieldValidatorErrorReporter(field, reporter) {
1911
+ const impl = field;
1912
+ if (typeof impl.bindValidatorErrorReporter === "function") impl.bindValidatorErrorReporter(reporter);
1913
+ }
1914
+ function createField(initial, validators, options) {
1915
+ return new FieldImpl(initial, validators, options);
1763
1916
  }
1764
1917
  /**
1765
1918
  * Wrap an async validator with a debounce. The debounce timer resets on every
@@ -1806,17 +1959,40 @@ var FormImpl = class {
1806
1959
  validators;
1807
1960
  options;
1808
1961
  validatorDispose = null;
1962
+ initialDispose = null;
1809
1963
  currentValidatorRun = 0;
1810
1964
  currentValidatorAbort = null;
1811
1965
  disposed = false;
1812
- constructor(schema, options) {
1966
+ onValidatorError = null;
1967
+ /** Internal — wire a sync-throw reporter for the top-level validators. */
1968
+ bindValidatorErrorReporter(reporter) {
1969
+ this.onValidatorError = reporter;
1970
+ }
1971
+ constructor(schema, options, internalOptions) {
1813
1972
  this.fields = schema;
1814
1973
  this.options = options;
1815
1974
  this.validators = options?.validators ?? [];
1816
- if (options?.initial !== void 0) {
1817
- const ini = typeof options.initial === "function" ? options.initial() : options.initial;
1818
- if (ini !== void 0) this.applyPartial(ini, true);
1819
- }
1975
+ this.onValidatorError = internalOptions?.onValidatorError ?? null;
1976
+ if (options?.initial !== void 0) if (typeof options.initial === "function") {
1977
+ const initialFn = options.initial;
1978
+ const mode = options.resetOnInitialChange ?? "when-clean";
1979
+ let firstRun = true;
1980
+ this.initialDispose = effect$1(() => {
1981
+ const ini = initialFn();
1982
+ if (ini === void 0) return;
1983
+ untracked$1(() => {
1984
+ if (this.disposed) return;
1985
+ if (firstRun) {
1986
+ firstRun = false;
1987
+ this.applyPartial(ini, true);
1988
+ return;
1989
+ }
1990
+ if (mode === "never") return;
1991
+ if (mode === "when-clean" && this.isDirty.peek()) return;
1992
+ this.applyPartial(ini, true);
1993
+ });
1994
+ });
1995
+ } else this.applyPartial(options.initial, true);
1820
1996
  this.value = computed$1(() => this.computeValue());
1821
1997
  this.errors = computed$1(() => this.computeErrors());
1822
1998
  this.isDirty = computed$1(() => this.computeBool("isDirty"));
@@ -1873,12 +2049,28 @@ var FormImpl = class {
1873
2049
  for (const [k, val] of Object.entries(partial)) {
1874
2050
  const child = this.fields[k];
1875
2051
  if (!child) continue;
2052
+ if (val === void 0) continue;
1876
2053
  if (isForm(child)) if (asInitial) child.resetWithInitial(val);
1877
2054
  else child.set(val);
1878
2055
  else if (isFieldArray(child)) {
1879
2056
  const arr = child;
1880
- arr.clear();
1881
- for (const itemVal of val) arr.add(itemVal);
2057
+ const newValues = val;
2058
+ if (asInitial) {
2059
+ arr.clear();
2060
+ for (const itemVal of newValues) arr.add(itemVal);
2061
+ arr.replaceInitialItems(newValues);
2062
+ } else {
2063
+ const current = arr.items.peek();
2064
+ const overlap = Math.min(current.length, newValues.length);
2065
+ for (let i = 0; i < overlap; i++) {
2066
+ const item = current[i];
2067
+ const v = newValues[i];
2068
+ if (isForm(item)) item.set(v);
2069
+ else item.set(v);
2070
+ }
2071
+ for (let i = current.length; i < newValues.length; i++) arr.add(newValues[i]);
2072
+ for (let i = current.length - 1; i >= newValues.length; i--) arr.remove(i);
2073
+ }
1882
2074
  } else {
1883
2075
  const f = child;
1884
2076
  if (asInitial) f.setAsInitial(val);
@@ -1915,6 +2107,7 @@ var FormImpl = class {
1915
2107
  for (const child of Object.values(this.fields)) if (isForm(child) || isFieldArray(child)) tasks.push(child.validate());
1916
2108
  else tasks.push(child.revalidate());
1917
2109
  await Promise.all(tasks);
2110
+ if (this.validators.length > 0) this.runTopLevelValidators();
1918
2111
  if (this.topLevelValidating$.peek()) await new Promise((resolve) => {
1919
2112
  const unsub = this.topLevelValidating$.subscribe((v) => {
1920
2113
  if (!v) {
@@ -1929,6 +2122,7 @@ var FormImpl = class {
1929
2122
  if (this.disposed) return;
1930
2123
  this.disposed = true;
1931
2124
  this.validatorDispose?.();
2125
+ this.initialDispose?.();
1932
2126
  this.currentValidatorAbort?.abort();
1933
2127
  for (const child of Object.values(this.fields)) child.dispose?.();
1934
2128
  }
@@ -1941,10 +2135,15 @@ var FormImpl = class {
1941
2135
  const myId = ++this.currentValidatorRun;
1942
2136
  const syncErrors = [];
1943
2137
  const asyncPromises = [];
1944
- for (const v of this.validators) {
2138
+ for (const v of this.validators) try {
1945
2139
  const r = v(value, abort.signal);
1946
2140
  if (r instanceof Promise) asyncPromises.push(r);
1947
2141
  else if (r != null) syncErrors.push(r);
2142
+ } catch (err) {
2143
+ try {
2144
+ this.onValidatorError?.(err);
2145
+ } catch {}
2146
+ syncErrors.push(err instanceof Error ? err.message : String(err));
1948
2147
  }
1949
2148
  if (syncErrors.length > 0) {
1950
2149
  batch$1(() => {
@@ -2038,9 +2237,15 @@ var FieldArrayImpl = class {
2038
2237
  currentValidatorAbort = null;
2039
2238
  validatorDispose = null;
2040
2239
  disposed = false;
2041
- constructor(itemFactory, options) {
2240
+ onValidatorError = null;
2241
+ /** Internal — see `FormImpl.bindValidatorErrorReporter`. */
2242
+ bindValidatorErrorReporter(reporter) {
2243
+ this.onValidatorError = reporter;
2244
+ }
2245
+ constructor(itemFactory, options, internalOptions) {
2042
2246
  this.itemFactory = itemFactory;
2043
2247
  this.validators = options?.validators ?? [];
2248
+ this.onValidatorError = internalOptions?.onValidatorError ?? null;
2044
2249
  this.items$ = signal$1([]);
2045
2250
  if (options?.initial) {
2046
2251
  this.initialItems = options.initial;
@@ -2113,6 +2318,15 @@ var FieldArrayImpl = class {
2113
2318
  for (const item of this.items$.peek()) item.dispose?.();
2114
2319
  this.items$.set([]);
2115
2320
  }
2321
+ /**
2322
+ * Internal — used by `Form.resetWithInitial` to re-anchor the array's
2323
+ * initial items after a parent-driven `applyPartial(..., asInitial: true)`.
2324
+ * Without this, a subsequent `reset()` would revert to the construction-
2325
+ * time initials rather than the most-recently-applied ones.
2326
+ */
2327
+ replaceInitialItems(items) {
2328
+ this.initialItems = [...items];
2329
+ }
2116
2330
  reset() {
2117
2331
  if (this.disposed) return;
2118
2332
  batch$1(() => {
@@ -2131,6 +2345,7 @@ var FieldArrayImpl = class {
2131
2345
  for (const item of this.items$.peek()) if (isForm(item)) tasks.push(item.validate());
2132
2346
  else tasks.push(item.revalidate());
2133
2347
  await Promise.all(tasks);
2348
+ if (this.validators.length > 0) this.runTopLevelValidators();
2134
2349
  if (this.topLevelValidating$.peek()) await new Promise((resolve) => {
2135
2350
  const unsub = this.topLevelValidating$.subscribe((v) => {
2136
2351
  if (!v) {
@@ -2157,10 +2372,15 @@ var FieldArrayImpl = class {
2157
2372
  const myId = ++this.currentValidatorRun;
2158
2373
  const syncErrors = [];
2159
2374
  const asyncPromises = [];
2160
- for (const v of this.validators) {
2375
+ for (const v of this.validators) try {
2161
2376
  const r = v(value, abort.signal);
2162
2377
  if (r instanceof Promise) asyncPromises.push(r);
2163
2378
  else if (r != null) syncErrors.push(r);
2379
+ } catch (err) {
2380
+ try {
2381
+ this.onValidatorError?.(err);
2382
+ } catch {}
2383
+ syncErrors.push(err instanceof Error ? err.message : String(err));
2164
2384
  }
2165
2385
  if (syncErrors.length > 0) {
2166
2386
  batch$1(() => {
@@ -2191,11 +2411,11 @@ var FieldArrayImpl = class {
2191
2411
  });
2192
2412
  }
2193
2413
  };
2194
- function createForm(schema, options) {
2195
- return new FormImpl(schema, options);
2414
+ function createForm(schema, options, internalOptions) {
2415
+ return new FormImpl(schema, options, internalOptions);
2196
2416
  }
2197
- function createFieldArray(itemFactory, options) {
2198
- return new FieldArrayImpl(itemFactory, options);
2417
+ function createFieldArray(itemFactory, options, internalOptions) {
2418
+ return new FieldArrayImpl(itemFactory, options, internalOptions);
2199
2419
  }
2200
2420
  /**
2201
2421
  * Recursively wire every leaf `Field` in a form / field-array tree to a
@@ -2222,12 +2442,24 @@ function bindTreeToDevtoolsInto(node, prefix, controllerPath, emitter, disposers
2222
2442
  }
2223
2443
  if (isFieldArray(node)) {
2224
2444
  const arr = node;
2445
+ let perPass = [];
2225
2446
  const stop = effect$1(() => {
2226
- arr.items.value.forEach((item, idx) => {
2227
- bindTreeToDevtoolsInto(item, `${prefix}[${idx}]`, controllerPath, emitter, disposers);
2447
+ const items = arr.items.value;
2448
+ for (const d of perPass) try {
2449
+ d();
2450
+ } catch {}
2451
+ perPass = [];
2452
+ items.forEach((item, idx) => {
2453
+ bindTreeToDevtoolsInto(item, `${prefix}[${idx}]`, controllerPath, emitter, perPass);
2228
2454
  });
2229
2455
  });
2230
2456
  disposers.push(stop);
2457
+ disposers.push(() => {
2458
+ for (const d of perPass) try {
2459
+ d();
2460
+ } catch {}
2461
+ perPass = [];
2462
+ });
2231
2463
  return;
2232
2464
  }
2233
2465
  bindFieldDevtoolsOwner(node, {
@@ -2236,6 +2468,26 @@ function bindTreeToDevtoolsInto(node, prefix, controllerPath, emitter, disposers
2236
2468
  emitter
2237
2469
  });
2238
2470
  }
2471
+ /**
2472
+ * Walk a Form/FieldArray subtree and install `reporter` on every level —
2473
+ * leaf fields, nested forms' top-level validators, and field-arrays' top-level
2474
+ * validators. Called by `ctx.form` / `ctx.fieldArray` so synchronous validator
2475
+ * throws anywhere in the tree route through `root.onError`. See
2476
+ * `ValidatorErrorReporter` in `./field.ts`.
2477
+ */
2478
+ function bindTreeValidatorErrorReporter(node, reporter) {
2479
+ if (isForm(node)) {
2480
+ node.bindValidatorErrorReporter?.(reporter);
2481
+ for (const child of Object.values(node.fields)) bindTreeValidatorErrorReporter(child, reporter);
2482
+ return;
2483
+ }
2484
+ if (isFieldArray(node)) {
2485
+ node.bindValidatorErrorReporter?.(reporter);
2486
+ for (const item of node.items.value) bindTreeValidatorErrorReporter(item, reporter);
2487
+ return;
2488
+ }
2489
+ bindFieldValidatorErrorReporter(node, reporter);
2490
+ }
2239
2491
  //#endregion
2240
2492
  //#region src/query/local.ts
2241
2493
  var LocalCacheImpl = class {
@@ -2469,7 +2721,13 @@ var MutationImpl = class {
2469
2721
  reset() {
2470
2722
  if (this.disposed) return;
2471
2723
  for (const handle of this.inflight) handle.abort.abort();
2472
- this.serialQueue.length = 0;
2724
+ if (this.serialQueue.length > 0) {
2725
+ const aborted = new DOMException("Aborted", "AbortError");
2726
+ const queue = this.serialQueue;
2727
+ this.serialQueue = [];
2728
+ for (const queued of queue) queued.reject(aborted);
2729
+ }
2730
+ this.serialActive = false;
2473
2731
  batch$1(() => {
2474
2732
  this.data.set(void 0);
2475
2733
  this.error.set(void 0);
@@ -2517,24 +2775,6 @@ function raceAbort(promise, signal) {
2517
2775
  });
2518
2776
  });
2519
2777
  }
2520
- function abortableSleep(ms, signal) {
2521
- return new Promise((resolve, reject) => {
2522
- if (signal.aborted) {
2523
- reject(new DOMException("Aborted", "AbortError"));
2524
- return;
2525
- }
2526
- const timer = setTimeout(() => {
2527
- signal.removeEventListener("abort", onAbort);
2528
- resolve();
2529
- }, ms);
2530
- const onAbort = () => {
2531
- clearTimeout(timer);
2532
- signal.removeEventListener("abort", onAbort);
2533
- reject(new DOMException("Aborted", "AbortError"));
2534
- };
2535
- signal.addEventListener("abort", onAbort, { once: true });
2536
- });
2537
- }
2538
2778
  //#endregion
2539
2779
  //#region src/query/use.ts
2540
2780
  var SubscriptionImpl = class {
@@ -2605,7 +2845,9 @@ function createUse(client, query, keyOrOptions) {
2605
2845
  const enabledFn = typeof keyOrOptions === "object" && keyOrOptions !== null ? keyOrOptions.enabled : void 0;
2606
2846
  const sub = new SubscriptionImpl(keepPreviousData);
2607
2847
  let currentEntry = null;
2848
+ let suspended = false;
2608
2849
  const effectDispose = effect$1(() => {
2850
+ if (suspended) return;
2609
2851
  if (!(enabledFn ? enabledFn() : true)) {
2610
2852
  untracked$1(() => {
2611
2853
  if (currentEntry) {
@@ -2636,9 +2878,31 @@ function createUse(client, query, keyOrOptions) {
2636
2878
  }
2637
2879
  sub.detach();
2638
2880
  };
2881
+ const suspend = () => {
2882
+ if (suspended) return;
2883
+ suspended = true;
2884
+ if (currentEntry) {
2885
+ currentEntry.release();
2886
+ currentEntry = null;
2887
+ }
2888
+ };
2889
+ const resume = () => {
2890
+ if (!suspended) return;
2891
+ suspended = false;
2892
+ if (!(enabledFn ? enabledFn() : true)) return;
2893
+ const args = keyFn ? keyFn() : [];
2894
+ const entry = client.bindEntry(query, args);
2895
+ entry.acquire();
2896
+ currentEntry = entry;
2897
+ sub.attach(entry);
2898
+ const status = entry.entry.status.peek();
2899
+ if (status === "idle" || entry.entry.isStaleNow() || status === "error") entry.entry.startFetch().catch(() => {});
2900
+ };
2639
2901
  return {
2640
2902
  subscription: sub,
2641
- dispose
2903
+ dispose,
2904
+ suspend,
2905
+ resume
2642
2906
  };
2643
2907
  }
2644
2908
  var InfiniteSubscriptionImpl = class {
@@ -2738,7 +3002,9 @@ function createInfiniteUse(client, query, keyOrOptions) {
2738
3002
  const enabledFn = typeof keyOrOptions === "object" && keyOrOptions !== null ? keyOrOptions.enabled : void 0;
2739
3003
  const sub = new InfiniteSubscriptionImpl(keepPreviousData);
2740
3004
  let currentEntry = null;
3005
+ let suspended = false;
2741
3006
  const effectDispose = effect$1(() => {
3007
+ if (suspended) return;
2742
3008
  if (!(enabledFn ? enabledFn() : true)) {
2743
3009
  untracked$1(() => {
2744
3010
  if (currentEntry) {
@@ -2769,9 +3035,31 @@ function createInfiniteUse(client, query, keyOrOptions) {
2769
3035
  }
2770
3036
  sub.detach();
2771
3037
  };
3038
+ const suspend = () => {
3039
+ if (suspended) return;
3040
+ suspended = true;
3041
+ if (currentEntry) {
3042
+ currentEntry.release();
3043
+ currentEntry = null;
3044
+ }
3045
+ };
3046
+ const resume = () => {
3047
+ if (!suspended) return;
3048
+ suspended = false;
3049
+ if (!(enabledFn ? enabledFn() : true)) return;
3050
+ const args = keyFn ? keyFn() : [];
3051
+ const entry = client.bindInfiniteEntry(query, args);
3052
+ entry.acquire();
3053
+ currentEntry = entry;
3054
+ sub.attach(entry);
3055
+ const status = entry.entry.status.peek();
3056
+ if (status === "idle" || entry.entry.isStaleNow() || status === "error") entry.entry.startFetch().catch(() => {});
3057
+ };
2772
3058
  return {
2773
3059
  subscription: sub,
2774
- dispose
3060
+ dispose,
3061
+ suspend,
3062
+ resume
2775
3063
  };
2776
3064
  }
2777
3065
  //#endregion
@@ -2821,7 +3109,6 @@ var ControllerInstance = class ControllerInstance {
2821
3109
  }
2822
3110
  dispose() {
2823
3111
  if (this.state === "disposed") return;
2824
- this.state;
2825
3112
  this.state = "disposed";
2826
3113
  for (let i = this.entries.length - 1; i >= 0; i--) {
2827
3114
  const entry = this.entries[i];
@@ -2847,6 +3134,9 @@ var ControllerInstance = class ControllerInstance {
2847
3134
  case "cleanup":
2848
3135
  entry.dispose();
2849
3136
  break;
3137
+ case "subscription-cache":
3138
+ entry.dispose();
3139
+ break;
2850
3140
  case "child":
2851
3141
  entry.instance.dispose();
2852
3142
  break;
@@ -2872,6 +3162,9 @@ var ControllerInstance = class ControllerInstance {
2872
3162
  entry.dispose?.();
2873
3163
  entry.dispose = null;
2874
3164
  break;
3165
+ case "subscription-cache":
3166
+ entry.suspend();
3167
+ break;
2875
3168
  case "child":
2876
3169
  entry.instance.suspend();
2877
3170
  break;
@@ -2896,6 +3189,9 @@ var ControllerInstance = class ControllerInstance {
2896
3189
  case "effect":
2897
3190
  entry.dispose = effect$1(entry.factory);
2898
3191
  break;
3192
+ case "subscription-cache":
3193
+ entry.resume();
3194
+ break;
2899
3195
  case "child":
2900
3196
  entry.instance.resume();
2901
3197
  break;
@@ -2936,7 +3232,7 @@ var ControllerInstance = class ControllerInstance {
2936
3232
  }
2937
3233
  };
2938
3234
  entry.factory = wrapped;
2939
- entry.dispose = effect$1(wrapped);
3235
+ if (self.state !== "suspended") entry.dispose = effect$1(wrapped);
2940
3236
  self.entries.push(entry);
2941
3237
  },
2942
3238
  cache(fetcher, options) {
@@ -2949,19 +3245,23 @@ var ControllerInstance = class ControllerInstance {
2949
3245
  },
2950
3246
  use(query, keyOrOptions) {
2951
3247
  if (query.__olas === "infiniteQuery") {
2952
- const { subscription, dispose: d } = createInfiniteUse(self.rootShared.queryClient, query, keyOrOptions);
3248
+ const handle = createInfiniteUse(self.rootShared.queryClient, query, keyOrOptions);
2953
3249
  self.entries.push({
2954
- kind: "cleanup",
2955
- dispose: d
3250
+ kind: "subscription-cache",
3251
+ dispose: handle.dispose,
3252
+ suspend: handle.suspend,
3253
+ resume: handle.resume
2956
3254
  });
2957
- return subscription;
3255
+ return handle.subscription;
2958
3256
  }
2959
- const { subscription, dispose: d } = createUse(self.rootShared.queryClient, query, keyOrOptions);
3257
+ const handle = createUse(self.rootShared.queryClient, query, keyOrOptions);
2960
3258
  self.entries.push({
2961
- kind: "cleanup",
2962
- dispose: d
3259
+ kind: "subscription-cache",
3260
+ dispose: handle.dispose,
3261
+ suspend: handle.suspend,
3262
+ resume: handle.resume
2963
3263
  });
2964
- return subscription;
3264
+ return handle.subscription;
2965
3265
  },
2966
3266
  mutation(spec) {
2967
3267
  const m = createMutation(spec, self.rootShared.onError, self.path, self.rootShared.queryClient.mutationsInflight$, self.rootShared.devtools);
@@ -2972,7 +3272,12 @@ var ControllerInstance = class ControllerInstance {
2972
3272
  return m;
2973
3273
  },
2974
3274
  emitter() {
2975
- const e = createEmitter();
3275
+ const e = createEmitter({ onError: (err) => {
3276
+ dispatchError(self.rootShared.onError, err, {
3277
+ kind: "emitter",
3278
+ controllerPath: self.path
3279
+ });
3280
+ } });
2976
3281
  self.entries.push({
2977
3282
  kind: "cleanup",
2978
3283
  dispose: () => e.dispose()
@@ -2980,7 +3285,12 @@ var ControllerInstance = class ControllerInstance {
2980
3285
  return e;
2981
3286
  },
2982
3287
  field(initial, validators) {
2983
- const f = createField(initial, validators);
3288
+ const f = createField(initial, validators, { onValidatorError: (err) => {
3289
+ dispatchError(self.rootShared.onError, err, {
3290
+ kind: "effect",
3291
+ controllerPath: self.path
3292
+ });
3293
+ } });
2984
3294
  self.entries.push({
2985
3295
  kind: "cleanup",
2986
3296
  dispose: () => f.dispose()
@@ -2993,7 +3303,13 @@ var ControllerInstance = class ControllerInstance {
2993
3303
  return f;
2994
3304
  },
2995
3305
  form(schema, options) {
2996
- const f = createForm(schema, options);
3306
+ const reporter = (err) => {
3307
+ dispatchError(self.rootShared.onError, err, {
3308
+ kind: "effect",
3309
+ controllerPath: self.path
3310
+ });
3311
+ };
3312
+ const f = createForm(schema, options, { onValidatorError: reporter });
2997
3313
  self.entries.push({
2998
3314
  kind: "cleanup",
2999
3315
  dispose: () => f.dispose()
@@ -3003,10 +3319,17 @@ var ControllerInstance = class ControllerInstance {
3003
3319
  kind: "cleanup",
3004
3320
  dispose: stop
3005
3321
  });
3322
+ bindTreeValidatorErrorReporter(f, reporter);
3006
3323
  return f;
3007
3324
  },
3008
3325
  fieldArray(itemFactory, options) {
3009
- const fa = createFieldArray(itemFactory, options);
3326
+ const reporter = (err) => {
3327
+ dispatchError(self.rootShared.onError, err, {
3328
+ kind: "effect",
3329
+ controllerPath: self.path
3330
+ });
3331
+ };
3332
+ const fa = createFieldArray(itemFactory, options, { onValidatorError: reporter });
3010
3333
  self.entries.push({
3011
3334
  kind: "cleanup",
3012
3335
  dispose: () => fa.dispose()
@@ -3016,6 +3339,7 @@ var ControllerInstance = class ControllerInstance {
3016
3339
  kind: "cleanup",
3017
3340
  dispose: stop
3018
3341
  });
3342
+ bindTreeValidatorErrorReporter(fa, reporter);
3019
3343
  return fa;
3020
3344
  },
3021
3345
  provide(scope, value) {
@@ -3095,9 +3419,280 @@ var ControllerInstance = class ControllerInstance {
3095
3419
  controllerPath: self.path
3096
3420
  });
3097
3421
  }
3422
+ },
3423
+ suspend: () => {
3424
+ if (disposed) return;
3425
+ try {
3426
+ childInstance.suspend();
3427
+ } catch (err) {
3428
+ dispatchError(self.rootShared.onError, err, {
3429
+ kind: "effect",
3430
+ controllerPath: self.path
3431
+ });
3432
+ }
3433
+ },
3434
+ resume: () => {
3435
+ if (disposed) return;
3436
+ try {
3437
+ childInstance.resume();
3438
+ } catch (err) {
3439
+ dispatchError(self.rootShared.onError, err, {
3440
+ kind: "effect",
3441
+ controllerPath: self.path
3442
+ });
3443
+ }
3098
3444
  }
3099
3445
  };
3100
3446
  },
3447
+ session(def, props, options) {
3448
+ const segment = self.makeChildSegment(getFactory(def), getName(def));
3449
+ const override = options?.deps;
3450
+ const childDeps = override !== void 0 ? {
3451
+ ...self.deps,
3452
+ ...override
3453
+ } : self.deps;
3454
+ const childInstance = new ControllerInstance(self, self.rootShared, segment, childDeps);
3455
+ const api = childInstance.construct(getFactory(def), props);
3456
+ const entry = {
3457
+ kind: "child",
3458
+ instance: childInstance
3459
+ };
3460
+ self.entries.push(entry);
3461
+ let disposed = false;
3462
+ const dispose = () => {
3463
+ if (disposed) return;
3464
+ disposed = true;
3465
+ const idx = self.entries.indexOf(entry);
3466
+ if (idx >= 0) self.entries.splice(idx, 1);
3467
+ try {
3468
+ childInstance.dispose();
3469
+ } catch (err) {
3470
+ dispatchError(self.rootShared.onError, err, {
3471
+ kind: "effect",
3472
+ controllerPath: self.path
3473
+ });
3474
+ }
3475
+ };
3476
+ return [api, dispose];
3477
+ },
3478
+ collection(options) {
3479
+ const childMap = /* @__PURE__ */ new Map();
3480
+ const items$ = signal$1([]);
3481
+ const size$ = computed$1(() => items$.value.length);
3482
+ const isFactoryForm = options.factory !== void 0;
3483
+ const buildChild = (item) => {
3484
+ let def;
3485
+ let childProps;
3486
+ if (isFactoryForm) {
3487
+ const result = options.factory(item);
3488
+ def = result.controller;
3489
+ childProps = result.props;
3490
+ } else {
3491
+ const homoOpts = options;
3492
+ def = homoOpts.controller;
3493
+ childProps = homoOpts.propsOf(item);
3494
+ }
3495
+ const segment = self.makeChildSegment(getFactory(def), getName(def));
3496
+ const childDeps = options.deps !== void 0 ? {
3497
+ ...self.deps,
3498
+ ...options.deps
3499
+ } : self.deps;
3500
+ const instance = new ControllerInstance(self, self.rootShared, segment, childDeps);
3501
+ try {
3502
+ return {
3503
+ instance,
3504
+ api: instance.construct(getFactory(def), childProps),
3505
+ def
3506
+ };
3507
+ } catch (err) {
3508
+ dispatchError(self.rootShared.onError, err, {
3509
+ kind: "construction",
3510
+ controllerPath: self.path
3511
+ });
3512
+ return null;
3513
+ }
3514
+ };
3515
+ const removeKey = (key) => {
3516
+ const info = childMap.get(key);
3517
+ if (info === void 0) return;
3518
+ childMap.delete(key);
3519
+ const idx = self.entries.indexOf(info.entry);
3520
+ if (idx >= 0) self.entries.splice(idx, 1);
3521
+ try {
3522
+ info.instance.dispose();
3523
+ } catch (err) {
3524
+ dispatchError(self.rootShared.onError, err, {
3525
+ kind: "effect",
3526
+ controllerPath: self.path
3527
+ });
3528
+ }
3529
+ };
3530
+ const reconcile = () => {
3531
+ const source = options.source.value;
3532
+ const itemByKey = /* @__PURE__ */ new Map();
3533
+ for (const item of source) {
3534
+ const key = options.keyOf(item);
3535
+ if (!itemByKey.has(key)) itemByKey.set(key, item);
3536
+ }
3537
+ for (const key of [...childMap.keys()]) if (!itemByKey.has(key)) removeKey(key);
3538
+ for (const [key, item] of itemByKey) {
3539
+ const existing = childMap.get(key);
3540
+ if (existing !== void 0) {
3541
+ if (isFactoryForm) {
3542
+ if (options.factory(item).controller !== existing.def) {
3543
+ removeKey(key);
3544
+ const built = buildChild(item);
3545
+ if (built !== null) {
3546
+ const entry = {
3547
+ kind: "child",
3548
+ instance: built.instance
3549
+ };
3550
+ self.entries.push(entry);
3551
+ childMap.set(key, {
3552
+ ...built,
3553
+ entry
3554
+ });
3555
+ }
3556
+ }
3557
+ }
3558
+ continue;
3559
+ }
3560
+ const built = buildChild(item);
3561
+ if (built !== null) {
3562
+ const entry = {
3563
+ kind: "child",
3564
+ instance: built.instance
3565
+ };
3566
+ self.entries.push(entry);
3567
+ childMap.set(key, {
3568
+ ...built,
3569
+ entry
3570
+ });
3571
+ }
3572
+ }
3573
+ const next = [];
3574
+ const seen = /* @__PURE__ */ new Set();
3575
+ for (const item of source) {
3576
+ const key = options.keyOf(item);
3577
+ if (seen.has(key)) continue;
3578
+ seen.add(key);
3579
+ const info = childMap.get(key);
3580
+ if (info !== void 0) next.push({
3581
+ key,
3582
+ api: info.api
3583
+ });
3584
+ }
3585
+ items$.set(next);
3586
+ };
3587
+ const wrapped = () => {
3588
+ try {
3589
+ reconcile();
3590
+ } catch (err) {
3591
+ dispatchError(self.rootShared.onError, err, {
3592
+ kind: "effect",
3593
+ controllerPath: self.path
3594
+ });
3595
+ }
3596
+ };
3597
+ const effectEntry = {
3598
+ kind: "effect",
3599
+ factory: wrapped,
3600
+ dispose: null
3601
+ };
3602
+ if (self.state !== "suspended") effectEntry.dispose = effect$1(wrapped);
3603
+ self.entries.push(effectEntry);
3604
+ return {
3605
+ items: items$,
3606
+ size: size$,
3607
+ get: (key) => childMap.get(key)?.api,
3608
+ has: (key) => childMap.has(key)
3609
+ };
3610
+ },
3611
+ lazyChild(loader, props, options) {
3612
+ const status$ = signal$1("idle");
3613
+ const api$ = signal$1(void 0);
3614
+ const error$ = signal$1(void 0);
3615
+ let childInstance = null;
3616
+ let childEntry = null;
3617
+ let pendingLoad = null;
3618
+ let disposed = false;
3619
+ const flagEntry = {
3620
+ kind: "onDispose",
3621
+ fn: () => {
3622
+ disposed = true;
3623
+ }
3624
+ };
3625
+ self.entries.push(flagEntry);
3626
+ const handleFailure = (err) => {
3627
+ status$.set("error");
3628
+ error$.set(err);
3629
+ dispatchError(self.rootShared.onError, err, {
3630
+ kind: "construction",
3631
+ controllerPath: self.path
3632
+ });
3633
+ };
3634
+ const load = () => {
3635
+ if (disposed) return Promise.reject(/* @__PURE__ */ new Error("[olas] ctx.lazyChild: cannot load after dispose"));
3636
+ if (pendingLoad !== null) return pendingLoad;
3637
+ status$.set("loading");
3638
+ pendingLoad = loader().then((def) => {
3639
+ if (disposed) throw new Error("[olas] ctx.lazyChild: disposed during load");
3640
+ const segment = self.makeChildSegment(getFactory(def), getName(def));
3641
+ const childDeps = options?.deps !== void 0 ? {
3642
+ ...self.deps,
3643
+ ...options.deps
3644
+ } : self.deps;
3645
+ const instance = new ControllerInstance(self, self.rootShared, segment, childDeps);
3646
+ try {
3647
+ const api = instance.construct(getFactory(def), props);
3648
+ childInstance = instance;
3649
+ childEntry = {
3650
+ kind: "child",
3651
+ instance
3652
+ };
3653
+ self.entries.push(childEntry);
3654
+ api$.set(api);
3655
+ status$.set("ready");
3656
+ return api;
3657
+ } catch (err) {
3658
+ handleFailure(err);
3659
+ throw err;
3660
+ }
3661
+ }, (err) => {
3662
+ if (disposed) throw err;
3663
+ handleFailure(err);
3664
+ throw err;
3665
+ });
3666
+ return pendingLoad;
3667
+ };
3668
+ const dispose = () => {
3669
+ if (disposed) return;
3670
+ disposed = true;
3671
+ if (childEntry !== null && childInstance !== null) {
3672
+ const idx = self.entries.indexOf(childEntry);
3673
+ if (idx >= 0) self.entries.splice(idx, 1);
3674
+ try {
3675
+ childInstance.dispose();
3676
+ } catch (err) {
3677
+ dispatchError(self.rootShared.onError, err, {
3678
+ kind: "effect",
3679
+ controllerPath: self.path
3680
+ });
3681
+ }
3682
+ childInstance = null;
3683
+ childEntry = null;
3684
+ }
3685
+ const flagIdx = self.entries.indexOf(flagEntry);
3686
+ if (flagIdx >= 0) self.entries.splice(flagIdx, 1);
3687
+ };
3688
+ return {
3689
+ status: status$,
3690
+ api: api$,
3691
+ error: error$,
3692
+ load,
3693
+ dispose
3694
+ };
3695
+ },
3101
3696
  onDispose(fn) {
3102
3697
  self.entries.push({
3103
3698
  kind: "onDispose",
@@ -3186,7 +3781,13 @@ function createRootWithProps(def, props, options) {
3186
3781
  onError: options.onError,
3187
3782
  queryClient
3188
3783
  }, "root", options.deps);
3189
- const api = instance.construct(getFactory(def), props);
3784
+ let api;
3785
+ try {
3786
+ api = instance.construct(getFactory(def), props);
3787
+ } catch (err) {
3788
+ queryClient.dispose();
3789
+ throw err;
3790
+ }
3190
3791
  if (typeof api !== "object" || api === null) return attachRootControls({ value: api }, instance, devtools, queryClient);
3191
3792
  return attachRootControls(api, instance, devtools, queryClient);
3192
3793
  }
@@ -3265,6 +3866,6 @@ function createRoot(def, options) {
3265
3866
  return createRootWithProps(def, void 0, options);
3266
3867
  }
3267
3868
  //#endregion
3268
- export { lookupRegisteredQuery as a, batch$1 as c, signal$1 as d, untracked$1 as f, createEmitter as i, computed$1 as l, createRootWithProps as n, registerQueryById as o, defineController as p, debouncedValidator as r, isAbortError as s, createRoot as t, effect$1 as u };
3869
+ export { lookupRegisteredQuery as a, isAbortError as c, effect$1 as d, signal$1 as f, createEmitter as i, batch$1 as l, defineController as m, createRootWithProps as n, registerQueryById as o, untracked$1 as p, debouncedValidator as r, stableHash as s, createRoot as t, computed$1 as u };
3269
3870
 
3270
- //# sourceMappingURL=root-BImHnGj1.mjs.map
3871
+ //# sourceMappingURL=root-De-6KWIZ.mjs.map