@kontsedal/olas-core 0.0.2 → 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 (43) hide show
  1. package/dist/index.cjs +34 -1
  2. package/dist/index.cjs.map +1 -1
  3. package/dist/index.d.cts +52 -2
  4. package/dist/index.d.cts.map +1 -1
  5. package/dist/index.d.mts +52 -2
  6. package/dist/index.d.mts.map +1 -1
  7. package/dist/index.mjs +33 -2
  8. package/dist/index.mjs.map +1 -1
  9. package/dist/{root-De-6KWIZ.mjs → root-Cnkb3I--.mjs} +248 -18
  10. package/dist/root-Cnkb3I--.mjs.map +1 -0
  11. package/dist/{root-XKEsSmcd.cjs → root-D_xAdcom.cjs} +248 -18
  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-C-zV1JZA.d.mts → types-CRn4UoLn.d.mts} +64 -6
  22. package/dist/types-CRn4UoLn.d.mts.map +1 -0
  23. package/dist/{types-DKfpkm17.d.cts → types-r_TVaRkD.d.cts} +64 -6
  24. package/dist/types-r_TVaRkD.d.cts.map +1 -0
  25. package/package.json +1 -1
  26. package/src/controller/types.ts +20 -0
  27. package/src/forms/field.ts +42 -9
  28. package/src/forms/form-types.ts +37 -0
  29. package/src/forms/form.ts +118 -0
  30. package/src/forms/index.ts +12 -1
  31. package/src/forms/standard-schema.ts +37 -0
  32. package/src/forms/validators.ts +31 -0
  33. package/src/index.ts +13 -3
  34. package/src/query/entry.ts +10 -3
  35. package/src/query/infinite.ts +8 -1
  36. package/src/query/structural-share.ts +114 -0
  37. package/src/query/types.ts +15 -2
  38. package/src/query/use.ts +47 -13
  39. package/src/testing.ts +2 -0
  40. package/dist/root-De-6KWIZ.mjs.map +0 -1
  41. package/dist/root-XKEsSmcd.cjs.map +0 -1
  42. package/dist/types-C-zV1JZA.d.mts.map +0 -1
  43. package/dist/types-DKfpkm17.d.cts.map +0 -1
@@ -248,6 +248,83 @@ function abortableSleep(ms, signal) {
248
248
  });
249
249
  }
250
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
251
328
  //#region src/query/entry.ts
252
329
  /**
253
330
  * One cache entry's state machine. Owns the AsyncState signals, race
@@ -354,8 +431,10 @@ var Entry = class {
354
431
  return typeof d === "function" ? d(attempt) : d;
355
432
  }
356
433
  applySuccess(result) {
434
+ const prev = this.data.peek();
435
+ const shared = prev === void 0 ? result : structuralShare(prev, result);
357
436
  batch(() => {
358
- this.data.set(result);
437
+ this.data.set(shared);
359
438
  this.error.set(void 0);
360
439
  this.status.set("success");
361
440
  this.isLoading.set(false);
@@ -367,8 +446,8 @@ var Entry = class {
367
446
  try {
368
447
  this.events.onFetchSuccess?.(Date.now() - this.fetchStartTime);
369
448
  } catch {}
370
- this.onSuccessData?.(result);
371
- return result;
449
+ this.onSuccessData?.(shared);
450
+ return shared;
372
451
  }
373
452
  applyFailure(err) {
374
453
  batch(() => {
@@ -636,8 +715,10 @@ var InfiniteEntry = class {
636
715
  });
637
716
  return this.runFetch(myId, abort.signal, this.initialPageParam, (page, param) => {
638
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;
639
720
  batch(() => {
640
- this.pages.set([page]);
721
+ this.pages.set([sharedPage]);
641
722
  this.pageParams.set([param]);
642
723
  this.error.set(void 0);
643
724
  this.status.set("success");
@@ -1701,6 +1782,17 @@ function createEmitter(options) {
1701
1782
  //#region src/forms/field.ts
1702
1783
  var FieldImpl = class {
1703
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$;
1704
1796
  errors$;
1705
1797
  touched$;
1706
1798
  dirty$;
@@ -1722,11 +1814,19 @@ var FieldImpl = class {
1722
1814
  this.validators = validators;
1723
1815
  this.onValidatorError = options?.onValidatorError ?? null;
1724
1816
  this.value$ = signal(initial);
1725
- this.errors$ = signal([]);
1817
+ this.validatorErrors$ = signal([]);
1818
+ this.serverErrors$ = signal([]);
1726
1819
  this.touched$ = signal(false);
1727
1820
  this.dirty$ = signal(false);
1728
1821
  this.validating$ = signal(false);
1729
1822
  this.revalidateTrigger$ = signal(0);
1823
+ this.errors$ = computed(() => {
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
+ });
1730
1830
  this.isValid$ = computed(() => this.errors$.value.length === 0 && !this.validating$.value);
1731
1831
  if (validators.length > 0) this.validatorDispose = effect(() => {
1732
1832
  this.runValidators();
@@ -1765,8 +1865,16 @@ var FieldImpl = class {
1765
1865
  }
1766
1866
  set(value) {
1767
1867
  if (this.disposed) return;
1768
- this.value$.set(value);
1769
- this.dirty$.set(true);
1868
+ batch(() => {
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);
1770
1878
  }
1771
1879
  /**
1772
1880
  * Reseat the field as if this value had been its constructor `initial`.
@@ -1791,7 +1899,8 @@ var FieldImpl = class {
1791
1899
  this.value$.set(this.initial);
1792
1900
  this.dirty$.set(false);
1793
1901
  this.touched$.set(false);
1794
- this.errors$.set([]);
1902
+ this.validatorErrors$.set([]);
1903
+ this.serverErrors$.set([]);
1795
1904
  this.validating$.set(false);
1796
1905
  });
1797
1906
  }
@@ -1857,7 +1966,7 @@ var FieldImpl = class {
1857
1966
  }
1858
1967
  if (syncErrors.length > 0) {
1859
1968
  batch(() => {
1860
- this.errors$.set(syncErrors);
1969
+ this.validatorErrors$.set(syncErrors);
1861
1970
  this.validating$.set(false);
1862
1971
  });
1863
1972
  this.emitValidated(false, syncErrors);
@@ -1865,14 +1974,14 @@ var FieldImpl = class {
1865
1974
  }
1866
1975
  if (asyncPromises.length === 0) {
1867
1976
  batch(() => {
1868
- this.errors$.set([]);
1977
+ this.validatorErrors$.set([]);
1869
1978
  this.validating$.set(false);
1870
1979
  });
1871
1980
  this.emitValidated(true, []);
1872
1981
  return;
1873
1982
  }
1874
1983
  batch(() => {
1875
- this.errors$.set([]);
1984
+ this.validatorErrors$.set([]);
1876
1985
  this.validating$.set(true);
1877
1986
  });
1878
1987
  Promise.allSettled(asyncPromises).then((results) => {
@@ -1885,7 +1994,7 @@ var FieldImpl = class {
1885
1994
  asyncErrors.push(msg);
1886
1995
  }
1887
1996
  batch(() => {
1888
- this.errors$.set(asyncErrors);
1997
+ this.validatorErrors$.set(asyncErrors);
1889
1998
  this.validating$.set(false);
1890
1999
  });
1891
2000
  this.emitValidated(asyncErrors.length === 0, asyncErrors);
@@ -1956,6 +2065,12 @@ var FormImpl = class {
1956
2065
  topLevelErrors$ = signal([]);
1957
2066
  topLevelErrors = this.topLevelErrors$;
1958
2067
  topLevelValidating$ = signal(false);
2068
+ isSubmitting$ = signal(false);
2069
+ submitCount$ = signal(0);
2070
+ submitError$ = signal(void 0);
2071
+ isSubmitting = this.isSubmitting$;
2072
+ submitCount = this.submitCount$;
2073
+ submitError = this.submitError$;
1959
2074
  validators;
1960
2075
  options;
1961
2076
  validatorDispose = null;
@@ -2118,6 +2233,107 @@ var FormImpl = class {
2118
2233
  });
2119
2234
  return this.isValid.peek();
2120
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(() => {
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(() => {
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(() => {
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
+ }
2121
2337
  dispose() {
2122
2338
  if (this.disposed) return;
2123
2339
  this.disposed = true;
@@ -2779,6 +2995,7 @@ function raceAbort(promise, signal) {
2779
2995
  //#region src/query/use.ts
2780
2996
  var SubscriptionImpl = class {
2781
2997
  keepPreviousData;
2998
+ select;
2782
2999
  current$ = signal(null);
2783
3000
  previousData$ = signal(void 0);
2784
3001
  data;
@@ -2789,13 +3006,18 @@ var SubscriptionImpl = class {
2789
3006
  isStale;
2790
3007
  lastUpdatedAt;
2791
3008
  hasPendingMutations;
2792
- constructor(keepPreviousData) {
3009
+ constructor(keepPreviousData, select) {
2793
3010
  this.keepPreviousData = keepPreviousData;
2794
- this.data = computed(() => {
3011
+ this.select = select;
3012
+ const rawData = computed(() => {
2795
3013
  const curData = this.current$.value?.entry.data.value;
2796
3014
  if (curData !== void 0) return curData;
2797
3015
  if (keepPreviousData) return this.previousData$.value;
2798
3016
  });
3017
+ this.data = select === void 0 ? rawData : computed(() => {
3018
+ const raw = rawData.value;
3019
+ return raw === void 0 ? void 0 : select(raw);
3020
+ });
2799
3021
  this.error = computed(() => this.current$.value?.entry.error.value);
2800
3022
  this.status = computed(() => this.current$.value?.entry.status.value ?? "idle");
2801
3023
  this.isLoading = computed(() => {
@@ -2824,7 +3046,7 @@ var SubscriptionImpl = class {
2824
3046
  refetch = () => {
2825
3047
  const cur = this.current$.peek();
2826
3048
  if (!cur) return Promise.reject(/* @__PURE__ */ new Error("[olas] no active subscription"));
2827
- return cur.entry.refetch();
3049
+ return cur.entry.refetch().then((v) => this.project(v));
2828
3050
  };
2829
3051
  reset = () => {
2830
3052
  this.current$.peek()?.entry.reset();
@@ -2832,18 +3054,26 @@ var SubscriptionImpl = class {
2832
3054
  firstValue = () => {
2833
3055
  const cur = this.current$.peek();
2834
3056
  if (!cur) return Promise.reject(/* @__PURE__ */ new Error("[olas] no active subscription"));
2835
- return cur.entry.firstValue();
3057
+ return cur.entry.firstValue().then((v) => this.project(v));
2836
3058
  };
3059
+ project(v) {
3060
+ return this.select === void 0 ? v : this.select(v);
3061
+ }
2837
3062
  };
2838
3063
  /**
2839
3064
  * Build a subscription + the effect that keeps it bound to the right entry.
2840
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.
2841
3071
  */
2842
3072
  function createUse(client, query, keyOrOptions) {
2843
3073
  const keepPreviousData = query.__spec.keepPreviousData ?? false;
2844
3074
  const keyFn = typeof keyOrOptions === "function" ? keyOrOptions : keyOrOptions?.key;
2845
3075
  const enabledFn = typeof keyOrOptions === "object" && keyOrOptions !== null ? keyOrOptions.enabled : void 0;
2846
- const sub = new SubscriptionImpl(keepPreviousData);
3076
+ const sub = new SubscriptionImpl(keepPreviousData, typeof keyOrOptions === "object" && keyOrOptions !== null ? keyOrOptions.select : void 0);
2847
3077
  let currentEntry = null;
2848
3078
  let suspended = false;
2849
3079
  const effectDispose = effect(() => {
@@ -3951,4 +4181,4 @@ Object.defineProperty(exports, "untracked", {
3951
4181
  }
3952
4182
  });
3953
4183
 
3954
- //# sourceMappingURL=root-XKEsSmcd.cjs.map
4184
+ //# sourceMappingURL=root-D_xAdcom.cjs.map