@kontsedal/olas-core 0.0.1 → 0.0.3

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 (50) hide show
  1. package/dist/index.cjs +72 -10
  2. package/dist/index.cjs.map +1 -1
  3. package/dist/index.d.cts +72 -12
  4. package/dist/index.d.cts.map +1 -1
  5. package/dist/index.d.mts +72 -12
  6. package/dist/index.d.mts.map +1 -1
  7. package/dist/index.mjs +71 -11
  8. package/dist/index.mjs.map +1 -1
  9. package/dist/{root-BCZDC5Fv.mjs → root-Cnkb3I--.mjs} +556 -28
  10. package/dist/root-Cnkb3I--.mjs.map +1 -0
  11. package/dist/{root-DXV1gVbQ.cjs → root-D_xAdcom.cjs} +556 -28
  12. package/dist/root-D_xAdcom.cjs.map +1 -0
  13. package/dist/testing.cjs +2 -1
  14. package/dist/testing.cjs.map +1 -1
  15. package/dist/testing.d.cts +2 -1
  16. package/dist/testing.d.cts.map +1 -1
  17. package/dist/testing.d.mts +2 -1
  18. package/dist/testing.d.mts.map +1 -1
  19. package/dist/testing.mjs +2 -1
  20. package/dist/testing.mjs.map +1 -1
  21. package/dist/{types-CffZ1QXt.d.cts → types-CRn4UoLn.d.mts} +196 -8
  22. package/dist/types-CRn4UoLn.d.mts.map +1 -0
  23. package/dist/{types-DSlDowpE.d.mts → types-r_TVaRkD.d.cts} +196 -8
  24. package/dist/types-r_TVaRkD.d.cts.map +1 -0
  25. package/package.json +1 -1
  26. package/src/controller/index.ts +6 -0
  27. package/src/controller/instance.ts +317 -3
  28. package/src/controller/types.ts +151 -0
  29. package/src/emitter.ts +34 -3
  30. package/src/forms/field.ts +42 -9
  31. package/src/forms/form-types.ts +37 -0
  32. package/src/forms/form.ts +165 -5
  33. package/src/forms/index.ts +12 -1
  34. package/src/forms/standard-schema.ts +37 -0
  35. package/src/forms/validators.ts +31 -0
  36. package/src/index.ts +20 -4
  37. package/src/query/entry.ts +10 -3
  38. package/src/query/infinite.ts +8 -1
  39. package/src/query/structural-share.ts +114 -0
  40. package/src/query/types.ts +15 -2
  41. package/src/query/use.ts +47 -13
  42. package/src/signals/readonly.ts +3 -3
  43. package/src/testing.ts +2 -0
  44. package/src/timing/debounced.ts +24 -4
  45. package/src/timing/throttled.ts +22 -3
  46. package/src/utils.ts +8 -4
  47. package/dist/root-BCZDC5Fv.mjs.map +0 -1
  48. package/dist/root-DXV1gVbQ.cjs.map +0 -1
  49. package/dist/types-CffZ1QXt.d.cts.map +0 -1
  50. package/dist/types-DSlDowpE.d.mts.map +0 -1
@@ -212,14 +212,16 @@ 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
  }
225
227
  /**
@@ -246,6 +248,83 @@ function abortableSleep(ms, signal) {
246
248
  });
247
249
  }
248
250
  //#endregion
251
+ //#region src/query/structural-share.ts
252
+ /**
253
+ * Walk `prev` and `next` in parallel. Wherever a sub-tree in `next` is
254
+ * structurally equal to the corresponding sub-tree in `prev`, return `prev`'s
255
+ * reference for that sub-tree. Otherwise return `next`'s.
256
+ *
257
+ * Result: a value that is `===` to `prev` on every refetch where the payload
258
+ * didn't actually change, and shares maximum ref-identity on partial changes.
259
+ * Downstream `computed`s and React `useSyncExternalStore` snapshots stop
260
+ * thrashing because reference equality holds where content equality holds.
261
+ *
262
+ * Bails (returns the `next` ref unchanged, no recursion) on:
263
+ * - Mismatched constructors / different `typeof` between `prev` and `next`
264
+ * - `Map`, `Set`, `Date`, `RegExp`, class instances (anything where the
265
+ * plain-object / array fast path isn't safe)
266
+ * - Functions, symbols, promises
267
+ *
268
+ * Handles cycles via a `WeakSet` of in-progress objects — a self-referential
269
+ * payload that compares structurally identical against itself won't loop.
270
+ */
271
+ function structuralShare(prev, next) {
272
+ if (Object.is(prev, next)) return prev;
273
+ return walk(prev, next, /* @__PURE__ */ new WeakSet());
274
+ }
275
+ function walk(prev, next, seen) {
276
+ if (Object.is(prev, next)) return prev;
277
+ if (prev === null || next === null) return next;
278
+ if (typeof prev !== "object" || typeof next !== "object") return next;
279
+ if (seen.has(prev) || seen.has(next)) return next;
280
+ if (Array.isArray(prev) && Array.isArray(next)) return walkArray(prev, next, seen);
281
+ if (Array.isArray(prev) !== Array.isArray(next)) return next;
282
+ const prevProto = Object.getPrototypeOf(prev);
283
+ if (prevProto !== Object.getPrototypeOf(next)) return next;
284
+ if (prevProto !== Object.prototype && prevProto !== null) return next;
285
+ return walkPlainObject(prev, next, seen);
286
+ }
287
+ function walkArray(prev, next, seen) {
288
+ if (prev.length !== next.length) {}
289
+ seen.add(prev);
290
+ seen.add(next);
291
+ try {
292
+ const out = new Array(next.length);
293
+ let changed = next.length !== prev.length;
294
+ for (let i = 0; i < next.length; i++) {
295
+ const shared = walk(i < prev.length ? prev[i] : void 0, next[i], seen);
296
+ out[i] = shared;
297
+ if (shared !== prev[i]) changed = true;
298
+ }
299
+ if (!changed) return prev;
300
+ return out;
301
+ } finally {
302
+ seen.delete(prev);
303
+ seen.delete(next);
304
+ }
305
+ }
306
+ function walkPlainObject(prev, next, seen) {
307
+ const prevKeys = Object.keys(prev);
308
+ const nextKeys = Object.keys(next);
309
+ let changed = prevKeys.length !== nextKeys.length;
310
+ seen.add(prev);
311
+ seen.add(next);
312
+ try {
313
+ const out = {};
314
+ for (const key of nextKeys) {
315
+ const shared = walk(prev[key], next[key], seen);
316
+ out[key] = shared;
317
+ if (shared !== prev[key]) changed = true;
318
+ else if (!(key in prev)) changed = true;
319
+ }
320
+ if (!changed) return prev;
321
+ return out;
322
+ } finally {
323
+ seen.delete(prev);
324
+ seen.delete(next);
325
+ }
326
+ }
327
+ //#endregion
249
328
  //#region src/query/entry.ts
250
329
  /**
251
330
  * One cache entry's state machine. Owns the AsyncState signals, race
@@ -352,8 +431,10 @@ var Entry = class {
352
431
  return typeof d === "function" ? d(attempt) : d;
353
432
  }
354
433
  applySuccess(result) {
434
+ const prev = this.data.peek();
435
+ const shared = prev === void 0 ? result : structuralShare(prev, result);
355
436
  batch$1(() => {
356
- this.data.set(result);
437
+ this.data.set(shared);
357
438
  this.error.set(void 0);
358
439
  this.status.set("success");
359
440
  this.isLoading.set(false);
@@ -365,8 +446,8 @@ var Entry = class {
365
446
  try {
366
447
  this.events.onFetchSuccess?.(Date.now() - this.fetchStartTime);
367
448
  } catch {}
368
- this.onSuccessData?.(result);
369
- return result;
449
+ this.onSuccessData?.(shared);
450
+ return shared;
370
451
  }
371
452
  applyFailure(err) {
372
453
  batch$1(() => {
@@ -634,8 +715,10 @@ var InfiniteEntry = class {
634
715
  });
635
716
  return this.runFetch(myId, abort.signal, this.initialPageParam, (page, param) => {
636
717
  if (myId !== this.currentFetchId || this.disposed) return;
718
+ const prevPages = this.pages.peek();
719
+ const sharedPage = prevPages.length > 0 ? structuralShare(prevPages[0], page) : page;
637
720
  batch$1(() => {
638
- this.pages.set([page]);
721
+ this.pages.set([sharedPage]);
639
722
  this.pageParams.set([param]);
640
723
  this.error.set(void 0);
641
724
  this.status.set("success");
@@ -1630,12 +1713,25 @@ function waitUntilFalse(sig) {
1630
1713
  //#endregion
1631
1714
  //#region src/emitter.ts
1632
1715
  var EmitterImpl = class {
1716
+ onError;
1633
1717
  handlers = /* @__PURE__ */ new Set();
1634
1718
  disposed = false;
1719
+ constructor(onError) {
1720
+ this.onError = onError;
1721
+ }
1635
1722
  emit(value) {
1636
1723
  if (this.disposed) return;
1637
1724
  const snapshot = Array.from(this.handlers);
1638
- for (const handler of snapshot) handler(value);
1725
+ for (const handler of snapshot) try {
1726
+ handler(value);
1727
+ } catch (err) {
1728
+ if (this.onError) try {
1729
+ this.onError(err);
1730
+ } catch {
1731
+ console.error("[olas] emitter handler threw and reporter threw:", err);
1732
+ }
1733
+ else console.error("[olas] emitter handler threw:", err);
1734
+ }
1639
1735
  }
1640
1736
  on(handler) {
1641
1737
  if (this.disposed) return () => {};
@@ -1667,9 +1763,14 @@ var EmitterImpl = class {
1667
1763
  * (or the emitter is disposed). Use this for emitters that live outside any
1668
1764
  * single controller — typically in deps. Use `ctx.emitter()` for emitters that
1669
1765
  * should auto-clean with a controller.
1766
+ *
1767
+ * Pass `onError` to receive emit-time handler throws (spec §20.6 — one
1768
+ * throwing handler must not block the rest of the fan-out). `ctx.emitter()`
1769
+ * wires this to the root's `onError` so deps-level emitters get isolation
1770
+ * by default when constructed via `ctx`.
1670
1771
  */
1671
- function createEmitter() {
1672
- const impl = new EmitterImpl();
1772
+ function createEmitter(options) {
1773
+ const impl = new EmitterImpl(options?.onError);
1673
1774
  return {
1674
1775
  emit: ((value) => impl.emit(value)),
1675
1776
  on: (handler) => impl.on(handler),
@@ -1681,6 +1782,17 @@ function createEmitter() {
1681
1782
  //#region src/forms/field.ts
1682
1783
  var FieldImpl = class {
1683
1784
  value$;
1785
+ /**
1786
+ * Validator-produced errors. The public `errors` getter merges this with
1787
+ * `serverErrors$` so consumers see a single flat array. Kept separate so a
1788
+ * re-run of validators (after a new value) doesn't clobber server errors.
1789
+ */
1790
+ validatorErrors$;
1791
+ /**
1792
+ * Externally-injected errors — see `setErrors`. Cleared on the next user
1793
+ * `set()`, on `reset()`, or via an explicit `setErrors([])`.
1794
+ */
1795
+ serverErrors$;
1684
1796
  errors$;
1685
1797
  touched$;
1686
1798
  dirty$;
@@ -1702,11 +1814,19 @@ var FieldImpl = class {
1702
1814
  this.validators = validators;
1703
1815
  this.onValidatorError = options?.onValidatorError ?? null;
1704
1816
  this.value$ = signal$1(initial);
1705
- this.errors$ = signal$1([]);
1817
+ this.validatorErrors$ = signal$1([]);
1818
+ this.serverErrors$ = signal$1([]);
1706
1819
  this.touched$ = signal$1(false);
1707
1820
  this.dirty$ = signal$1(false);
1708
1821
  this.validating$ = signal$1(false);
1709
1822
  this.revalidateTrigger$ = signal$1(0);
1823
+ this.errors$ = computed$1(() => {
1824
+ const v = this.validatorErrors$.value;
1825
+ const s = this.serverErrors$.value;
1826
+ if (s.length === 0) return v;
1827
+ if (v.length === 0) return s;
1828
+ return [...v, ...s];
1829
+ });
1710
1830
  this.isValid$ = computed$1(() => this.errors$.value.length === 0 && !this.validating$.value);
1711
1831
  if (validators.length > 0) this.validatorDispose = effect$1(() => {
1712
1832
  this.runValidators();
@@ -1745,8 +1865,16 @@ var FieldImpl = class {
1745
1865
  }
1746
1866
  set(value) {
1747
1867
  if (this.disposed) return;
1748
- this.value$.set(value);
1749
- this.dirty$.set(true);
1868
+ batch$1(() => {
1869
+ this.value$.set(value);
1870
+ this.dirty$.set(true);
1871
+ if (this.serverErrors$.peek().length > 0) this.serverErrors$.set([]);
1872
+ });
1873
+ }
1874
+ setErrors(errors) {
1875
+ if (this.disposed) return;
1876
+ const next = errors.length === 0 ? [] : [...errors];
1877
+ this.serverErrors$.set(next);
1750
1878
  }
1751
1879
  /**
1752
1880
  * Reseat the field as if this value had been its constructor `initial`.
@@ -1771,7 +1899,8 @@ var FieldImpl = class {
1771
1899
  this.value$.set(this.initial);
1772
1900
  this.dirty$.set(false);
1773
1901
  this.touched$.set(false);
1774
- this.errors$.set([]);
1902
+ this.validatorErrors$.set([]);
1903
+ this.serverErrors$.set([]);
1775
1904
  this.validating$.set(false);
1776
1905
  });
1777
1906
  }
@@ -1837,7 +1966,7 @@ var FieldImpl = class {
1837
1966
  }
1838
1967
  if (syncErrors.length > 0) {
1839
1968
  batch$1(() => {
1840
- this.errors$.set(syncErrors);
1969
+ this.validatorErrors$.set(syncErrors);
1841
1970
  this.validating$.set(false);
1842
1971
  });
1843
1972
  this.emitValidated(false, syncErrors);
@@ -1845,14 +1974,14 @@ var FieldImpl = class {
1845
1974
  }
1846
1975
  if (asyncPromises.length === 0) {
1847
1976
  batch$1(() => {
1848
- this.errors$.set([]);
1977
+ this.validatorErrors$.set([]);
1849
1978
  this.validating$.set(false);
1850
1979
  });
1851
1980
  this.emitValidated(true, []);
1852
1981
  return;
1853
1982
  }
1854
1983
  batch$1(() => {
1855
- this.errors$.set([]);
1984
+ this.validatorErrors$.set([]);
1856
1985
  this.validating$.set(true);
1857
1986
  });
1858
1987
  Promise.allSettled(asyncPromises).then((results) => {
@@ -1865,7 +1994,7 @@ var FieldImpl = class {
1865
1994
  asyncErrors.push(msg);
1866
1995
  }
1867
1996
  batch$1(() => {
1868
- this.errors$.set(asyncErrors);
1997
+ this.validatorErrors$.set(asyncErrors);
1869
1998
  this.validating$.set(false);
1870
1999
  });
1871
2000
  this.emitValidated(asyncErrors.length === 0, asyncErrors);
@@ -1936,6 +2065,12 @@ var FormImpl = class {
1936
2065
  topLevelErrors$ = signal$1([]);
1937
2066
  topLevelErrors = this.topLevelErrors$;
1938
2067
  topLevelValidating$ = signal$1(false);
2068
+ isSubmitting$ = signal$1(false);
2069
+ submitCount$ = signal$1(0);
2070
+ submitError$ = signal$1(void 0);
2071
+ isSubmitting = this.isSubmitting$;
2072
+ submitCount = this.submitCount$;
2073
+ submitError = this.submitError$;
1939
2074
  validators;
1940
2075
  options;
1941
2076
  validatorDispose = null;
@@ -2034,8 +2169,23 @@ var FormImpl = class {
2034
2169
  else child.set(val);
2035
2170
  else if (isFieldArray(child)) {
2036
2171
  const arr = child;
2037
- arr.clear();
2038
- for (const itemVal of val) arr.add(itemVal);
2172
+ const newValues = val;
2173
+ if (asInitial) {
2174
+ arr.clear();
2175
+ for (const itemVal of newValues) arr.add(itemVal);
2176
+ arr.replaceInitialItems(newValues);
2177
+ } else {
2178
+ const current = arr.items.peek();
2179
+ const overlap = Math.min(current.length, newValues.length);
2180
+ for (let i = 0; i < overlap; i++) {
2181
+ const item = current[i];
2182
+ const v = newValues[i];
2183
+ if (isForm(item)) item.set(v);
2184
+ else item.set(v);
2185
+ }
2186
+ for (let i = current.length; i < newValues.length; i++) arr.add(newValues[i]);
2187
+ for (let i = current.length - 1; i >= newValues.length; i--) arr.remove(i);
2188
+ }
2039
2189
  } else {
2040
2190
  const f = child;
2041
2191
  if (asInitial) f.setAsInitial(val);
@@ -2083,6 +2233,107 @@ var FormImpl = class {
2083
2233
  });
2084
2234
  return this.isValid.peek();
2085
2235
  }
2236
+ /**
2237
+ * Run a submission against this form. Wraps `handler(value)` with:
2238
+ * - `isSubmitting` set true while the handler is in flight.
2239
+ * - `submitCount` incremented before the handler runs.
2240
+ * - `submitError` set to the throw, if any.
2241
+ * - Optional pre-submit `validate()` (default true). When invalid every
2242
+ * field is marked touched and the handler is skipped — the returned
2243
+ * promise resolves with `{ ok: false }` and `submitError` is left
2244
+ * untouched (validation failure is not a thrown error).
2245
+ *
2246
+ * The handler may return a value (synchronously or via Promise); it's
2247
+ * captured in the resolved object's `data` field. Throws are captured
2248
+ * unless `onError: 'rethrow'`. A `resetOnSuccess: true` option calls
2249
+ * `reset()` after the handler resolves successfully.
2250
+ */
2251
+ async submit(handler, options) {
2252
+ if (this.disposed) return {
2253
+ ok: false,
2254
+ error: /* @__PURE__ */ new Error("form is disposed")
2255
+ };
2256
+ if (this.isSubmitting$.peek()) return {
2257
+ ok: false,
2258
+ error: /* @__PURE__ */ new Error("submit already in progress")
2259
+ };
2260
+ const validateFirst = options?.validateBeforeSubmit ?? true;
2261
+ const onErrorMode = options?.onError ?? "capture";
2262
+ batch$1(() => {
2263
+ this.submitCount$.update((n) => n + 1);
2264
+ this.submitError$.set(void 0);
2265
+ this.isSubmitting$.set(true);
2266
+ });
2267
+ try {
2268
+ if (validateFirst) {
2269
+ if (!await this.validate()) {
2270
+ this.markAllTouched();
2271
+ this.isSubmitting$.set(false);
2272
+ return { ok: false };
2273
+ }
2274
+ }
2275
+ const result = await handler(this.value.peek());
2276
+ if (options?.resetOnSuccess) this.reset();
2277
+ this.isSubmitting$.set(false);
2278
+ return {
2279
+ ok: true,
2280
+ data: result
2281
+ };
2282
+ } catch (err) {
2283
+ batch$1(() => {
2284
+ this.submitError$.set(err);
2285
+ this.isSubmitting$.set(false);
2286
+ });
2287
+ if (onErrorMode === "rethrow") throw err;
2288
+ return {
2289
+ ok: false,
2290
+ error: err
2291
+ };
2292
+ }
2293
+ }
2294
+ /**
2295
+ * Pin externally-sourced errors on specific fields — typically server-side
2296
+ * validation results from a failed submit. Paths are dot-separated and
2297
+ * traverse nested `Form` / `FieldArray` children (numeric segments are
2298
+ * array indices). Errors land in the field's `serverErrors` channel and
2299
+ * clear automatically on the next user write to that field. Passing an
2300
+ * empty array for a path clears that field's server errors immediately.
2301
+ */
2302
+ setErrors(errors) {
2303
+ if (this.disposed) return;
2304
+ batch$1(() => {
2305
+ for (const [path, msgs] of Object.entries(errors)) {
2306
+ const target = this.resolvePath(path);
2307
+ if (target === void 0) continue;
2308
+ if (target.setErrors === void 0) continue;
2309
+ target.setErrors(msgs);
2310
+ }
2311
+ });
2312
+ }
2313
+ resolvePath(path) {
2314
+ if (path === "") return void 0;
2315
+ const segments = path.split(".");
2316
+ let cursor = this;
2317
+ for (const seg of segments) {
2318
+ if (cursor === void 0 || cursor === null) return void 0;
2319
+ if (isForm(cursor)) {
2320
+ cursor = cursor.fields[seg];
2321
+ continue;
2322
+ }
2323
+ if (isFieldArray(cursor)) {
2324
+ const idx = Number(seg);
2325
+ if (!Number.isInteger(idx) || idx < 0) return void 0;
2326
+ cursor = cursor.at(idx);
2327
+ continue;
2328
+ }
2329
+ if (cursor === this) {
2330
+ cursor = this.fields[seg];
2331
+ continue;
2332
+ }
2333
+ return;
2334
+ }
2335
+ return cursor;
2336
+ }
2086
2337
  dispose() {
2087
2338
  if (this.disposed) return;
2088
2339
  this.disposed = true;
@@ -2283,6 +2534,15 @@ var FieldArrayImpl = class {
2283
2534
  for (const item of this.items$.peek()) item.dispose?.();
2284
2535
  this.items$.set([]);
2285
2536
  }
2537
+ /**
2538
+ * Internal — used by `Form.resetWithInitial` to re-anchor the array's
2539
+ * initial items after a parent-driven `applyPartial(..., asInitial: true)`.
2540
+ * Without this, a subsequent `reset()` would revert to the construction-
2541
+ * time initials rather than the most-recently-applied ones.
2542
+ */
2543
+ replaceInitialItems(items) {
2544
+ this.initialItems = [...items];
2545
+ }
2286
2546
  reset() {
2287
2547
  if (this.disposed) return;
2288
2548
  batch$1(() => {
@@ -2735,6 +2995,7 @@ function raceAbort(promise, signal) {
2735
2995
  //#region src/query/use.ts
2736
2996
  var SubscriptionImpl = class {
2737
2997
  keepPreviousData;
2998
+ select;
2738
2999
  current$ = signal$1(null);
2739
3000
  previousData$ = signal$1(void 0);
2740
3001
  data;
@@ -2745,13 +3006,18 @@ var SubscriptionImpl = class {
2745
3006
  isStale;
2746
3007
  lastUpdatedAt;
2747
3008
  hasPendingMutations;
2748
- constructor(keepPreviousData) {
3009
+ constructor(keepPreviousData, select) {
2749
3010
  this.keepPreviousData = keepPreviousData;
2750
- this.data = computed$1(() => {
3011
+ this.select = select;
3012
+ const rawData = computed$1(() => {
2751
3013
  const curData = this.current$.value?.entry.data.value;
2752
3014
  if (curData !== void 0) return curData;
2753
3015
  if (keepPreviousData) return this.previousData$.value;
2754
3016
  });
3017
+ this.data = select === void 0 ? rawData : computed$1(() => {
3018
+ const raw = rawData.value;
3019
+ return raw === void 0 ? void 0 : select(raw);
3020
+ });
2755
3021
  this.error = computed$1(() => this.current$.value?.entry.error.value);
2756
3022
  this.status = computed$1(() => this.current$.value?.entry.status.value ?? "idle");
2757
3023
  this.isLoading = computed$1(() => {
@@ -2780,7 +3046,7 @@ var SubscriptionImpl = class {
2780
3046
  refetch = () => {
2781
3047
  const cur = this.current$.peek();
2782
3048
  if (!cur) return Promise.reject(/* @__PURE__ */ new Error("[olas] no active subscription"));
2783
- return cur.entry.refetch();
3049
+ return cur.entry.refetch().then((v) => this.project(v));
2784
3050
  };
2785
3051
  reset = () => {
2786
3052
  this.current$.peek()?.entry.reset();
@@ -2788,18 +3054,26 @@ var SubscriptionImpl = class {
2788
3054
  firstValue = () => {
2789
3055
  const cur = this.current$.peek();
2790
3056
  if (!cur) return Promise.reject(/* @__PURE__ */ new Error("[olas] no active subscription"));
2791
- return cur.entry.firstValue();
3057
+ return cur.entry.firstValue().then((v) => this.project(v));
2792
3058
  };
3059
+ project(v) {
3060
+ return this.select === void 0 ? v : this.select(v);
3061
+ }
2793
3062
  };
2794
3063
  /**
2795
3064
  * Build a subscription + the effect that keeps it bound to the right entry.
2796
3065
  * The controller container wires the disposer into the lifecycle.
3066
+ *
3067
+ * `keyOrOptions` may carry an optional `select` projection that maps the
3068
+ * underlying `T` to a view `U`; the returned subscription's data shape
3069
+ * widens accordingly. Without `select`, `U = T` and the projection
3070
+ * computed is skipped.
2797
3071
  */
2798
3072
  function createUse(client, query, keyOrOptions) {
2799
3073
  const keepPreviousData = query.__spec.keepPreviousData ?? false;
2800
3074
  const keyFn = typeof keyOrOptions === "function" ? keyOrOptions : keyOrOptions?.key;
2801
3075
  const enabledFn = typeof keyOrOptions === "object" && keyOrOptions !== null ? keyOrOptions.enabled : void 0;
2802
- const sub = new SubscriptionImpl(keepPreviousData);
3076
+ const sub = new SubscriptionImpl(keepPreviousData, typeof keyOrOptions === "object" && keyOrOptions !== null ? keyOrOptions.select : void 0);
2803
3077
  let currentEntry = null;
2804
3078
  let suspended = false;
2805
3079
  const effectDispose = effect$1(() => {
@@ -3228,7 +3502,12 @@ var ControllerInstance = class ControllerInstance {
3228
3502
  return m;
3229
3503
  },
3230
3504
  emitter() {
3231
- const e = createEmitter();
3505
+ const e = createEmitter({ onError: (err) => {
3506
+ dispatchError(self.rootShared.onError, err, {
3507
+ kind: "emitter",
3508
+ controllerPath: self.path
3509
+ });
3510
+ } });
3232
3511
  self.entries.push({
3233
3512
  kind: "cleanup",
3234
3513
  dispose: () => e.dispose()
@@ -3395,6 +3674,255 @@ var ControllerInstance = class ControllerInstance {
3395
3674
  }
3396
3675
  };
3397
3676
  },
3677
+ session(def, props, options) {
3678
+ const segment = self.makeChildSegment(getFactory(def), getName(def));
3679
+ const override = options?.deps;
3680
+ const childDeps = override !== void 0 ? {
3681
+ ...self.deps,
3682
+ ...override
3683
+ } : self.deps;
3684
+ const childInstance = new ControllerInstance(self, self.rootShared, segment, childDeps);
3685
+ const api = childInstance.construct(getFactory(def), props);
3686
+ const entry = {
3687
+ kind: "child",
3688
+ instance: childInstance
3689
+ };
3690
+ self.entries.push(entry);
3691
+ let disposed = false;
3692
+ const dispose = () => {
3693
+ if (disposed) return;
3694
+ disposed = true;
3695
+ const idx = self.entries.indexOf(entry);
3696
+ if (idx >= 0) self.entries.splice(idx, 1);
3697
+ try {
3698
+ childInstance.dispose();
3699
+ } catch (err) {
3700
+ dispatchError(self.rootShared.onError, err, {
3701
+ kind: "effect",
3702
+ controllerPath: self.path
3703
+ });
3704
+ }
3705
+ };
3706
+ return [api, dispose];
3707
+ },
3708
+ collection(options) {
3709
+ const childMap = /* @__PURE__ */ new Map();
3710
+ const items$ = signal$1([]);
3711
+ const size$ = computed$1(() => items$.value.length);
3712
+ const isFactoryForm = options.factory !== void 0;
3713
+ const buildChild = (item) => {
3714
+ let def;
3715
+ let childProps;
3716
+ if (isFactoryForm) {
3717
+ const result = options.factory(item);
3718
+ def = result.controller;
3719
+ childProps = result.props;
3720
+ } else {
3721
+ const homoOpts = options;
3722
+ def = homoOpts.controller;
3723
+ childProps = homoOpts.propsOf(item);
3724
+ }
3725
+ const segment = self.makeChildSegment(getFactory(def), getName(def));
3726
+ const childDeps = options.deps !== void 0 ? {
3727
+ ...self.deps,
3728
+ ...options.deps
3729
+ } : self.deps;
3730
+ const instance = new ControllerInstance(self, self.rootShared, segment, childDeps);
3731
+ try {
3732
+ return {
3733
+ instance,
3734
+ api: instance.construct(getFactory(def), childProps),
3735
+ def
3736
+ };
3737
+ } catch (err) {
3738
+ dispatchError(self.rootShared.onError, err, {
3739
+ kind: "construction",
3740
+ controllerPath: self.path
3741
+ });
3742
+ return null;
3743
+ }
3744
+ };
3745
+ const removeKey = (key) => {
3746
+ const info = childMap.get(key);
3747
+ if (info === void 0) return;
3748
+ childMap.delete(key);
3749
+ const idx = self.entries.indexOf(info.entry);
3750
+ if (idx >= 0) self.entries.splice(idx, 1);
3751
+ try {
3752
+ info.instance.dispose();
3753
+ } catch (err) {
3754
+ dispatchError(self.rootShared.onError, err, {
3755
+ kind: "effect",
3756
+ controllerPath: self.path
3757
+ });
3758
+ }
3759
+ };
3760
+ const reconcile = () => {
3761
+ const source = options.source.value;
3762
+ const itemByKey = /* @__PURE__ */ new Map();
3763
+ for (const item of source) {
3764
+ const key = options.keyOf(item);
3765
+ if (!itemByKey.has(key)) itemByKey.set(key, item);
3766
+ }
3767
+ for (const key of [...childMap.keys()]) if (!itemByKey.has(key)) removeKey(key);
3768
+ for (const [key, item] of itemByKey) {
3769
+ const existing = childMap.get(key);
3770
+ if (existing !== void 0) {
3771
+ if (isFactoryForm) {
3772
+ if (options.factory(item).controller !== existing.def) {
3773
+ removeKey(key);
3774
+ const built = buildChild(item);
3775
+ if (built !== null) {
3776
+ const entry = {
3777
+ kind: "child",
3778
+ instance: built.instance
3779
+ };
3780
+ self.entries.push(entry);
3781
+ childMap.set(key, {
3782
+ ...built,
3783
+ entry
3784
+ });
3785
+ }
3786
+ }
3787
+ }
3788
+ continue;
3789
+ }
3790
+ const built = buildChild(item);
3791
+ if (built !== null) {
3792
+ const entry = {
3793
+ kind: "child",
3794
+ instance: built.instance
3795
+ };
3796
+ self.entries.push(entry);
3797
+ childMap.set(key, {
3798
+ ...built,
3799
+ entry
3800
+ });
3801
+ }
3802
+ }
3803
+ const next = [];
3804
+ const seen = /* @__PURE__ */ new Set();
3805
+ for (const item of source) {
3806
+ const key = options.keyOf(item);
3807
+ if (seen.has(key)) continue;
3808
+ seen.add(key);
3809
+ const info = childMap.get(key);
3810
+ if (info !== void 0) next.push({
3811
+ key,
3812
+ api: info.api
3813
+ });
3814
+ }
3815
+ items$.set(next);
3816
+ };
3817
+ const wrapped = () => {
3818
+ try {
3819
+ reconcile();
3820
+ } catch (err) {
3821
+ dispatchError(self.rootShared.onError, err, {
3822
+ kind: "effect",
3823
+ controllerPath: self.path
3824
+ });
3825
+ }
3826
+ };
3827
+ const effectEntry = {
3828
+ kind: "effect",
3829
+ factory: wrapped,
3830
+ dispose: null
3831
+ };
3832
+ if (self.state !== "suspended") effectEntry.dispose = effect$1(wrapped);
3833
+ self.entries.push(effectEntry);
3834
+ return {
3835
+ items: items$,
3836
+ size: size$,
3837
+ get: (key) => childMap.get(key)?.api,
3838
+ has: (key) => childMap.has(key)
3839
+ };
3840
+ },
3841
+ lazyChild(loader, props, options) {
3842
+ const status$ = signal$1("idle");
3843
+ const api$ = signal$1(void 0);
3844
+ const error$ = signal$1(void 0);
3845
+ let childInstance = null;
3846
+ let childEntry = null;
3847
+ let pendingLoad = null;
3848
+ let disposed = false;
3849
+ const flagEntry = {
3850
+ kind: "onDispose",
3851
+ fn: () => {
3852
+ disposed = true;
3853
+ }
3854
+ };
3855
+ self.entries.push(flagEntry);
3856
+ const handleFailure = (err) => {
3857
+ status$.set("error");
3858
+ error$.set(err);
3859
+ dispatchError(self.rootShared.onError, err, {
3860
+ kind: "construction",
3861
+ controllerPath: self.path
3862
+ });
3863
+ };
3864
+ const load = () => {
3865
+ if (disposed) return Promise.reject(/* @__PURE__ */ new Error("[olas] ctx.lazyChild: cannot load after dispose"));
3866
+ if (pendingLoad !== null) return pendingLoad;
3867
+ status$.set("loading");
3868
+ pendingLoad = loader().then((def) => {
3869
+ if (disposed) throw new Error("[olas] ctx.lazyChild: disposed during load");
3870
+ const segment = self.makeChildSegment(getFactory(def), getName(def));
3871
+ const childDeps = options?.deps !== void 0 ? {
3872
+ ...self.deps,
3873
+ ...options.deps
3874
+ } : self.deps;
3875
+ const instance = new ControllerInstance(self, self.rootShared, segment, childDeps);
3876
+ try {
3877
+ const api = instance.construct(getFactory(def), props);
3878
+ childInstance = instance;
3879
+ childEntry = {
3880
+ kind: "child",
3881
+ instance
3882
+ };
3883
+ self.entries.push(childEntry);
3884
+ api$.set(api);
3885
+ status$.set("ready");
3886
+ return api;
3887
+ } catch (err) {
3888
+ handleFailure(err);
3889
+ throw err;
3890
+ }
3891
+ }, (err) => {
3892
+ if (disposed) throw err;
3893
+ handleFailure(err);
3894
+ throw err;
3895
+ });
3896
+ return pendingLoad;
3897
+ };
3898
+ const dispose = () => {
3899
+ if (disposed) return;
3900
+ disposed = true;
3901
+ if (childEntry !== null && childInstance !== null) {
3902
+ const idx = self.entries.indexOf(childEntry);
3903
+ if (idx >= 0) self.entries.splice(idx, 1);
3904
+ try {
3905
+ childInstance.dispose();
3906
+ } catch (err) {
3907
+ dispatchError(self.rootShared.onError, err, {
3908
+ kind: "effect",
3909
+ controllerPath: self.path
3910
+ });
3911
+ }
3912
+ childInstance = null;
3913
+ childEntry = null;
3914
+ }
3915
+ const flagIdx = self.entries.indexOf(flagEntry);
3916
+ if (flagIdx >= 0) self.entries.splice(flagIdx, 1);
3917
+ };
3918
+ return {
3919
+ status: status$,
3920
+ api: api$,
3921
+ error: error$,
3922
+ load,
3923
+ dispose
3924
+ };
3925
+ },
3398
3926
  onDispose(fn) {
3399
3927
  self.entries.push({
3400
3928
  kind: "onDispose",
@@ -3570,4 +4098,4 @@ function createRoot(def, options) {
3570
4098
  //#endregion
3571
4099
  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 };
3572
4100
 
3573
- //# sourceMappingURL=root-BCZDC5Fv.mjs.map
4101
+ //# sourceMappingURL=root-Cnkb3I--.mjs.map