sygnal 6.6.0 → 7.0.0

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 (82) hide show
  1. package/CHANGELOG.md +132 -0
  2. package/README.md +2 -0
  3. package/dist/astro/index.cjs.js +32 -9
  4. package/dist/astro/index.cjs.js.map +1 -1
  5. package/dist/astro/index.mjs +32 -9
  6. package/dist/astro/index.mjs.map +1 -1
  7. package/dist/astro/server.cjs.js.map +1 -1
  8. package/dist/astro/server.mjs.map +1 -1
  9. package/dist/devtools.cjs.js.map +1 -1
  10. package/dist/devtools.esm.js.map +1 -1
  11. package/dist/diagnostics.cjs.js +87 -4
  12. package/dist/diagnostics.cjs.js.map +1 -1
  13. package/dist/diagnostics.esm.js +86 -5
  14. package/dist/diagnostics.esm.js.map +1 -1
  15. package/dist/guide/accessibility.md +2 -0
  16. package/dist/guide/ai-chat.md +6 -6
  17. package/dist/guide/behaviors.md +1 -1
  18. package/dist/guide/element-commands.md +4 -2
  19. package/dist/guide/forms-reference.md +89 -3
  20. package/dist/guide/forms.md +8 -8
  21. package/dist/guide/http.md +77 -7
  22. package/dist/guide/migrating-to-7.md +314 -0
  23. package/dist/guide/recipes/ai-summarize.md +2 -1
  24. package/dist/guide/recipes/i18n.md +1 -1
  25. package/dist/guide/ui/tabs.md +5 -4
  26. package/dist/guide/ui/tooltip.md +24 -0
  27. package/dist/index.cjs.js +772 -318
  28. package/dist/index.cjs.js.map +1 -1
  29. package/dist/index.d.ts +198 -31
  30. package/dist/index.esm.js +772 -318
  31. package/dist/index.esm.js.map +1 -1
  32. package/dist/sygnal.min.js +1 -1
  33. package/dist/sygnal.min.js.map +1 -1
  34. package/dist/ui.cjs.js +96 -11
  35. package/dist/ui.cjs.js.map +1 -1
  36. package/dist/ui.esm.js +96 -12
  37. package/dist/ui.esm.js.map +1 -1
  38. package/dist/vike/config/package.json +1 -1
  39. package/dist/vite/plugin.cjs.js +19 -9
  40. package/dist/vite/plugin.cjs.js.map +1 -1
  41. package/dist/vite/plugin.mjs +19 -9
  42. package/dist/vite/plugin.mjs.map +1 -1
  43. package/llms.txt +17 -16
  44. package/package.json +1 -1
  45. package/src/astro/index.d.ts +8 -0
  46. package/src/astro/index.ts +18 -0
  47. package/src/core/actions.ts +28 -21
  48. package/src/core/define.ts +2 -0
  49. package/src/core/hooks.ts +2 -2
  50. package/src/core/hosts/collection.ts +4 -3
  51. package/src/core/instance.ts +60 -16
  52. package/src/core/runtime.ts +5 -1
  53. package/src/extra/ai/chat/behavior.ts +3 -3
  54. package/src/extra/ai/commandBar.ts +3 -3
  55. package/src/extra/ai/compose/behavior.ts +3 -3
  56. package/src/extra/ai/transports/shared.ts +8 -2
  57. package/src/extra/behaviors.ts +21 -0
  58. package/src/extra/diagnostics/checks/fetch.ts +21 -1
  59. package/src/extra/diagnostics/checks/forms.ts +4 -3
  60. package/src/extra/diagnostics/checks/index.ts +1 -0
  61. package/src/extra/diagnostics/checks/next.ts +3 -0
  62. package/src/extra/diagnostics/checks/public.d.ts +12 -0
  63. package/src/extra/diagnostics/checks/timingAudit.ts +47 -0
  64. package/src/extra/diagnostics/codes.ts +13 -0
  65. package/src/extra/fetchDriver.ts +112 -7
  66. package/src/extra/fetchEventSource.ts +113 -0
  67. package/src/extra/form.ts +68 -26
  68. package/src/extra/formHelpers.ts +3 -1
  69. package/src/extra/lens.ts +3 -2
  70. package/src/extra/queryCache.ts +4 -3
  71. package/src/extra/socketDriver.ts +10 -4
  72. package/src/extra/testing.ts +68 -11
  73. package/src/index.d.ts +197 -30
  74. package/src/index.ts +1 -0
  75. package/src/shared.ts +3 -0
  76. package/src/ui/dialog.ts +3 -2
  77. package/src/ui/tooltips.ts +92 -0
  78. package/src/ui.d.ts +35 -0
  79. package/src/ui.ts +1 -0
  80. package/src/vite/graph.ts +11 -3
  81. package/src/vite/plugin.d.ts +7 -0
  82. package/src/vite/plugin.ts +10 -5
package/dist/index.esm.js CHANGED
@@ -63,6 +63,8 @@ const NOT_SINK = /^(__|(props|children|dispose|commands)\$$|(CHILD|PARENT|EFFECT
63
63
  */
64
64
  const abortReason = {};
65
65
  const abort = (reason) => (abortReason.r = reason, ABORT);
66
+ /** Private semantic binding identity, shared by built-in helpers and the runtime. */
67
+ const BINDING = /*#__PURE__*/ Symbol.for('sygnal.binding');
66
68
 
67
69
  /**
68
70
  * PLAN-4.6 (D162): `defineComponent(opts)` builds a component from an options object, for code
@@ -753,6 +755,8 @@ const DEV_CODE_SEVERITY = {
753
755
  SYG634: 'info',
754
756
  // PLAN-3 5-5 (fetch.ts)
755
757
  SYG635: 'warn',
758
+ // PLAN-11 U-1 (fetch.ts)
759
+ SYG636: 'warn',
756
760
  // PLAN-4 GS-2 (checks/elementCommands.ts, through the core bridge; renderComponent too)
757
761
  SYG640: 'warn',
758
762
  SYG641: 'error',
@@ -869,6 +873,12 @@ const DEV_CODE_SEVERITY = {
869
873
  // PLAN-9 X-2: static only (sygnal-check); listed so the entry knows their titles
870
874
  SYG253: 'warn',
871
875
  SYG254: 'info',
876
+ // PLAN-12: static only; proven after-state output guard contradiction.
877
+ SYG256: 'warn',
878
+ // 7.0: the opt-in timing audit (checks/next.ts; configureTimingAudit)
879
+ SYG257: 'info',
880
+ // PLAN-12: static only for legacy (6.x) binding contracts. Sygnal 7 remounts known targets.
881
+ SYG454: 'warn',
872
882
  };
873
883
  function docsUrlFor(code) {
874
884
  return `${DOCS_BASE_URL}/reference/errors#${code.toLowerCase()}`;
@@ -1264,6 +1274,8 @@ function normalize$1(view, src) {
1264
1274
  const list = [];
1265
1275
  for (const sink in e)
1266
1276
  list.push([sink, e[sink]]), sinks.add(sink);
1277
+ // 7.0: STATE first, so the other sinks get the state it committed
1278
+ list.sort((a, b) => +(a[0] != 'STATE') - +(b[0] != 'STATE'));
1267
1279
  handlers.set(key, list);
1268
1280
  }
1269
1281
  const c = normCalc(name, src.calculated, src.initialState);
@@ -1855,7 +1867,7 @@ function handle(inst, type, data, cause, src) {
1855
1867
  return;
1856
1868
  const def = inst.def;
1857
1869
  const pre = inst.cell.get();
1858
- let props, outs;
1870
+ let props, afterProps, post = pre, outs;
1859
1871
  const next = (t, d, ms = 10, effect) => {
1860
1872
  if (typeof ms !== 'number')
1861
1873
  fail$2('SYG215', inst, `next() delay in '${type}' must be a number`, "Use next('ACTION', data, ms)");
@@ -1870,35 +1882,28 @@ function handle(inst, type, data, cause, src) {
1870
1882
  };
1871
1883
  for (const [sink, h0] of hs) {
1872
1884
  const h = H.wrapHandler ? H.wrapHandler(viewOf(inst), type, sink, h0) || h0 : h0;
1873
- if (sink == 'EFFECT') {
1874
- if (typeof h != 'function')
1875
- continue;
1876
- const failed = (e) => app.caught(inst, 'SYG214', `EFFECT handler '${type}' threw`, e, 'effect', type);
1877
- try {
1878
- const p = { ...(props || (props = inst_props(inst, pre))), signal: (inst.ac || (inst.ac = globalThis.AbortController && new AbortController()))?.signal };
1879
- const r = h(pre, data, (t, d, ms) => next(t, d, ms, true), p);
1880
- if (r?.then)
1881
- r.then(null, failed);
1882
- else if (r !== undefined && !isAbort(r))
1883
- warn('SYG219', inst, `EFFECT handler '${type}' returned a value, which is ignored`, 'Use a STATE or driver sink');
1884
- }
1885
- catch (e) {
1886
- failed(e);
1887
- }
1888
- continue;
1889
- }
1885
+ const effect = sink == 'EFFECT';
1890
1886
  let v;
1891
1887
  if (typeof h == 'function') {
1892
1888
  try {
1893
- v = h(pre, data, next, props || (props = inst_props(inst, pre)));
1889
+ const p = sink == 'STATE' ? props || (props = inst_props(inst, pre, pre)) : afterProps || (afterProps = inst_props(inst, post, pre));
1890
+ v = h(p.state, data, effect ? (t, d, ms) => next(t, d, ms, true) : next, effect ? { ...p, signal: (inst.ac || (inst.ac = globalThis.AbortController && new AbortController()))?.signal } : p);
1891
+ if (effect) {
1892
+ if (v?.then)
1893
+ v.then(null, (e) => handlerFailed(inst, type, sink, e));
1894
+ else if (v !== undefined && !isAbort(v))
1895
+ warn('SYG219', inst, `EFFECT handler '${type}' returned a value, which is ignored`, 'Use a STATE or driver sink');
1896
+ }
1894
1897
  }
1895
1898
  catch (e) {
1896
- app.caught(inst, 'SYG216', `Reducer for '${type}' threw; ${sink == 'STATE' ? 'state unchanged' : 'nothing sent'}`, e, 'reducer', type);
1899
+ handlerFailed(inst, type, sink, e);
1897
1900
  continue;
1898
1901
  }
1899
1902
  }
1900
1903
  else
1901
1904
  v = h === undefined || h === true ? data : h;
1905
+ if (effect)
1906
+ continue;
1902
1907
  if (sink == 'STATE' && (isAbort(v) || v === pre)) {
1903
1908
  // D205: the DOM event being dispatched (window.event) is the one this action came from
1904
1909
  if (/^(input|change)$/.test(globalThis.event?.type))
@@ -1911,6 +1916,11 @@ function handle(inst, type, data, cause, src) {
1911
1916
  H.onReducer?.(viewOf(inst), type, pre, v);
1912
1917
  dbg(inst, () => `<${type}> State reducer added`);
1913
1918
  inst.cell.set(v);
1919
+ // a STATE that removed the component's own state (a deleted record it is bound to): the
1920
+ // outputs get its last state, as a Collection item's do
1921
+ post = inst.cell.get();
1922
+ if (post === undefined)
1923
+ post = pre;
1914
1924
  // GS-12: this app's DOM driver patches inside a View Transition (makeViewTransitionDOMDriver
1915
1925
  // reads the flag on its IsolateModule); includes?.: a non-array static lists nothing (G-228).
1916
1926
  // Only for a state that changed structurally: today an equal one renders nothing (its
@@ -1959,9 +1969,14 @@ function handle(inst, type, data, cause, src) {
1959
1969
  send$1(inst, outs[i], outs[i + 1]);
1960
1970
  }
1961
1971
  }
1962
- /** the handlers' 4th argument: the props, children, slots, context, uid and the pre-action state */
1963
- function inst_props(inst, pre) {
1964
- return { ...inst.props, children: inst.children, slots: inst.slots, context: inst.context(), uid: inst.uid, state: pre };
1972
+ /** Keep the same failure boundary for sync handlers and rejected EFFECT promises. */
1973
+ function handlerFailed(inst, type, sink, e) {
1974
+ const effect = sink == 'EFFECT';
1975
+ inst.app.caught(inst, effect ? 'SYG214' : 'SYG216', effect ? `EFFECT handler '${type}' threw` : `Reducer for '${type}' threw; ${sink == 'STATE' ? 'state unchanged' : 'nothing sent'}`, e, effect ? 'effect' : 'reducer', type);
1976
+ }
1977
+ /** the handlers' 4th argument: the props, children, slots, context, uid, the state and the pre-action state */
1978
+ function inst_props(inst, state, previousState) {
1979
+ return { ...inst.props, children: inst.children, slots: inst.slots, context: inst.context(), uid: inst.uid, state, previousState };
1965
1980
  }
1966
1981
  /**
1967
1982
  * A sink value of an instance: stamped with its sender (EVENTS; object values for a reply-capable
@@ -2195,6 +2210,19 @@ const flat = (v) => {
2195
2210
  * - dispose(): DISPOSE, onDispose, dispose$, the EFFECT signal aborted, the subtree, then the
2196
2211
  * streams (the scoped _remove swap: no xstream stop timers, D165/Q18).
2197
2212
  */
2213
+ // v7: a known binding target joins key/id/path in the lifetime identity. Fresh equivalent
2214
+ // byId helpers retain identity; opaque lenses need an explicit key for a new target.
2215
+ function bindingChanged(a, b) {
2216
+ if (a === b)
2217
+ return false;
2218
+ const x = a?.[BINDING], y = b?.[BINDING];
2219
+ return x && y ? !x.every((v, i) => Object.is(v, y[i]))
2220
+ : !!(x || y || typeof a == 'string' || typeof b == 'string' || a === undefined || b === undefined);
2221
+ }
2222
+ function bindingState(k) {
2223
+ const c = k.base || k.cell || k.arr;
2224
+ return c && (c.has?.() ?? c.get() !== undefined) ? (k.cell || k.arr).get() : undefined;
2225
+ }
2198
2226
  const ERR_FIX$1 = 'See the attached error';
2199
2227
  // 4-I G-557: a fragment root (a Collection's): each of its top-level elements is marked.
2200
2228
  // 4-K G-572: a text root (no element; an attribute would land on a Text node) gets `ready: false`
@@ -2237,8 +2265,8 @@ const idOf$1 = (n, path) => {
2237
2265
  return `${n.sel}::${(s && s.replace(/"/g, '')) || path}`;
2238
2266
  };
2239
2267
  /**
2240
- * a component vnode's props, without `resetState` (D174: read at creation, like `state`, never a
2241
- * prop of the child)
2268
+ * a component vnode's props, without `resetState` (D174: read at creation, never a
2269
+ * prop of the child). Known `state` target changes start a new lifetime in v7.
2242
2270
  */
2243
2271
  function propsOf(p) {
2244
2272
  if (!p)
@@ -2302,10 +2330,14 @@ const EMPTY_CTX = Object.freeze({});
2302
2330
  /** a view read every key (spread, Object.keys): any context change re-renders it */
2303
2331
  const ALL = '\u0000all';
2304
2332
  let IDS = 0;
2305
- /** a child that failed to instantiate: renders the owner's error fallback (SYG408) */
2333
+ /**
2334
+ * a child that failed to instantiate: renders the owner's error fallback (SYG408). It keeps its
2335
+ * props, so only a changed binding target makes another attempt (not every owner render)
2336
+ */
2306
2337
  class Failed {
2307
- constructor(outv) {
2338
+ constructor(outv, props) {
2308
2339
  this.outv = outv;
2340
+ this.props = props;
2309
2341
  this.lr = true;
2310
2342
  this.ready = true;
2311
2343
  this.def = null;
@@ -2313,7 +2345,7 @@ class Failed {
2313
2345
  this.shown = true;
2314
2346
  }
2315
2347
  render() { return this.outv; }
2316
- setProps() { }
2348
+ setProps(p) { this.props = p; }
2317
2349
  dispose() { }
2318
2350
  }
2319
2351
  class Inst {
@@ -2321,6 +2353,7 @@ class Inst {
2321
2353
  this.app = app;
2322
2354
  this.def = def;
2323
2355
  this.parent = parent;
2356
+ this.base = base;
2324
2357
  this.dom = dom;
2325
2358
  this.scope = scope;
2326
2359
  this.kind = kind;
@@ -2487,10 +2520,32 @@ class Inst {
2487
2520
  }
2488
2521
  /** called after each action and before each render pass: STATE.stream emits a new state */
2489
2522
  notify() {
2490
- const v = this.cell.raw();
2491
- if (v !== this.stLast && v !== undefined) {
2492
- this.stLast = v;
2493
- this.st$.shamefullySendNext(v);
2523
+ // A completed action can remove and reinsert a target before the next render. Owners
2524
+ // observe each transition, including hidden pages; the current action finishes first.
2525
+ // only the bound children (a string / byId binding), not every child
2526
+ if (this.bound)
2527
+ for (const k of this.bound) {
2528
+ if (k.disposed) {
2529
+ this.bound.delete(k);
2530
+ continue;
2531
+ }
2532
+ const previous = k.bs, state = k.bs = bindingState(k);
2533
+ if (previous !== undefined && state === undefined) {
2534
+ // Cleanup reads its last owned snapshot. A dying path must not recreate the
2535
+ // deleted target; the fresh lifetime receives a new writable cell at reconciliation.
2536
+ const cell = k.cell || k.arr;
2537
+ cell.get = () => previous;
2538
+ cell.set = () => { };
2539
+ k.dispose();
2540
+ this.forced = true;
2541
+ }
2542
+ }
2543
+ if (this.st$) {
2544
+ const v = this.cell.raw();
2545
+ if (v !== this.stLast && v !== undefined) {
2546
+ this.stLast = v;
2547
+ this.st$.shamefullySendNext(v);
2548
+ }
2494
2549
  }
2495
2550
  }
2496
2551
  dispose$() {
@@ -2739,7 +2794,9 @@ class Inst {
2739
2794
  for (const r of resolvers)
2740
2795
  view = r(view, this) || view;
2741
2796
  let k = this.kids.get(id);
2742
- if (k && view && k.def && k.def.view !== view) {
2797
+ // Collection and VirtualCollection share an array cell; Switchable binds `state`.
2798
+ const binding = host && sel != 'switchable' ? 'from' : 'state';
2799
+ if (k && (k.disposed || (view && k.def && k.def.view !== view) || bindingChanged(k.props?.[binding], props[binding]))) {
2743
2800
  k.dispose();
2744
2801
  this.kids.delete(id);
2745
2802
  k = undefined;
@@ -2747,7 +2804,12 @@ class Inst {
2747
2804
  if (k)
2748
2805
  k.setProps(props, children, n, id);
2749
2806
  else
2750
- this.kids.set(id, host ? this.host(host, props, children, id, n) : this.child(view, props, children, id, !!data.props?.resetState));
2807
+ this.kids.set(id, k = host ? this.host(host, props, children, id, n) : this.child(view, props, children, id, !!data.props?.resetState));
2808
+ if (typeof props[binding] == 'string' || props[binding]?.[BINDING]) {
2809
+ k.bs = bindingState(k);
2810
+ (this.bound || (this.bound = new Set())).add(k);
2811
+ this.app.watchers.add(this);
2812
+ }
2751
2813
  return;
2752
2814
  }
2753
2815
  if (posts[sel] && !(ps || (ps = [])).includes(sel))
@@ -2772,11 +2834,11 @@ class Inst {
2772
2834
  return make(this, props, children, id, n);
2773
2835
  }
2774
2836
  catch (err) {
2775
- return this.failed(err);
2837
+ return this.failed(err, props);
2776
2838
  }
2777
2839
  }
2778
2840
  /** the owner's error fallback for a child that failed to instantiate (SYG408) */
2779
- failed(err) {
2841
+ failed(err, props) {
2780
2842
  const e = err instanceof Error ? err : new Error(String(err));
2781
2843
  let out = errorDiv$1(this.def.name);
2782
2844
  if (this.def.onError) {
@@ -2788,7 +2850,7 @@ class Inst {
2788
2850
  }
2789
2851
  }
2790
2852
  this.app.caught(this, 'SYG408', 'Sub-component threw; rendering the error fallback', e, 'instantiate');
2791
- return new Failed(out);
2853
+ return new Failed(out, props);
2792
2854
  }
2793
2855
  /** a tag child: state="key" | state={lens} | none (the parent's state) | isolatedState (D174: `resetState`) */
2794
2856
  child(view, props, children, id, reset = false) {
@@ -2807,7 +2869,7 @@ class Inst {
2807
2869
  return new Inst(app, def, this, cell, this.dom && this.dom.isolateSource(this.dom, scope), props, children, scope, this.uid(uidPart(id.replace(/.*::(r\.)?/, ''))), 'child', reset);
2808
2870
  }
2809
2871
  catch (err) {
2810
- return this.failed(err);
2872
+ return this.failed(err, props);
2811
2873
  }
2812
2874
  }
2813
2875
  inject(n, path) {
@@ -2870,8 +2932,7 @@ class Inst {
2870
2932
  tearDown(() => { for (const s of subs)
2871
2933
  s.unsubscribe(); }, app.dq, this);
2872
2934
  this.subs = [];
2873
- if (this.st$)
2874
- app.watchers.delete(this);
2935
+ app.watchers.delete(this);
2875
2936
  this.hub = null;
2876
2937
  }
2877
2938
  }
@@ -2970,7 +3031,7 @@ function arrayCell(owner, from) {
2970
3031
  if (typeof from == 'string')
2971
3032
  return keyCell(oc, from, owner.def.calcNames?.has(pathOf$1(from)[0]) && name, undefined, 'Collection');
2972
3033
  if (isObj$2(from) && typeof from.get == 'function') {
2973
- let lp = {}, lv;
3034
+ let lp = {}, lv, raw;
2974
3035
  return {
2975
3036
  get() {
2976
3037
  const p = oc.get();
@@ -2986,7 +3047,7 @@ function arrayCell(owner, from) {
2986
3047
  owner.app.appError(owner, e, 'view');
2987
3048
  return lv;
2988
3049
  }
2989
- lv = g;
3050
+ raw = lv = g;
2990
3051
  if (!Array.isArray(lv)) {
2991
3052
  warn('SYG401', name, "Collection 'from' getter returned a non-array; it renders nothing", 'Return an array from get()', lv);
2992
3053
  lv = NONE$1;
@@ -2994,6 +3055,7 @@ function arrayCell(owner, from) {
2994
3055
  }
2995
3056
  return lv;
2996
3057
  },
3058
+ has() { this.get(); return raw !== undefined; },
2997
3059
  set(v) {
2998
3060
  if (typeof from.set != 'function')
2999
3061
  return;
@@ -5355,8 +5417,11 @@ const validateWith = async (schema, value) => {
5355
5417
  * declarations in src/index.d.ts and docs/guide/drivers.md. Summary:
5356
5418
  *
5357
5419
  * - Request (sink value): `{ url, ok?, error?, key?, category?, method?, headers?, query?, json?,
5358
- * body?, latest?, timeoutMs?, parse?, init? }` or a URL string. `json` is stringified with a JSON
5359
- * content-type; method defaults to POST with a body/json, else GET. `query` is appended as a
5420
+ * multipart?, body?, latest?, timeoutMs?, parse?, init? }` or a URL string. `json` is stringified
5421
+ * with a JSON content-type; PLAN-11 U-5: `multipart` (an object) becomes a FormData body (a
5422
+ * File / Blob a file part, an array repeats the name, null / undefined skipped, an object JSON,
5423
+ * anything else a string), the content-type left to the browser (its boundary); method defaults
5424
+ * to POST with a body/json/multipart, else GET. `query` is appended as a
5360
5425
  * query string (null/undefined values skipped, arrays repeat the key, before any #fragment).
5361
5426
  * Other fetch options (credentials, mode, cache, ...) go under `init` (R4-5); other top-level
5362
5427
  * keys are the app's own: not sent, but returned on `request`.
@@ -5433,6 +5498,15 @@ const validateWith = async (schema, value) => {
5433
5498
  * the request itself) into requests sent again in parallel and combined, at most 3 deep. One
5434
5499
  * failure fails the request once (`error.status` / `error.body`). No cache entry sharing, no
5435
5500
  * per-part retry. The test fake (`_whole`) sends and answers it as one request. decide() uses it.
5501
+ * - PLAN-11 U-1 (D555): `progress: 'ACTION'` replies to the sender with each progress step,
5502
+ * `progress: true` (or a string with no sender) sends it down `progress(category?)`:
5503
+ * `{ phase: 'upload' | 'download', loaded, total, fraction, category, request }` (`total` /
5504
+ * `fraction` null when the length is unknown). Such a request goes over XMLHttpRequest (the
5505
+ * only transport with upload progress in every engine; S-1), turned back into a Response so
5506
+ * parsing, status, retry and timeouts are the same. Steps are throttled to one per 50 ms per
5507
+ * phase, keeping the first and the last. Not for resources or `batch`; never shares a fetch.
5508
+ * No XMLHttpRequest (Node, a service worker): plain fetch, no steps. The transport is
5509
+ * `options.XMLHttpRequest`, else the global one, read at request time.
5436
5510
  * - dispose / sink completion aborts everything in flight.
5437
5511
  * - `fetch` is read at request time (options.fetch, else globalThis.fetch), so test stubs work.
5438
5512
  * - PLAN-3 5-1 (H-9): renderComponent's HTTP fake runs this driver over an in-memory fetch;
@@ -5526,6 +5600,81 @@ const mergeHeaders = (...all) => {
5526
5600
  h.forEach((v, k) => { out[k] = v; });
5527
5601
  return out;
5528
5602
  };
5603
+ // PLAN-11 U-1: statuses whose Response must have a null body
5604
+ const NULL_BODY = [101, 103, 204, 205, 304];
5605
+ const abortError$1 = () => typeof DOMException == 'function'
5606
+ ? new DOMException('The operation was aborted.', 'AbortError')
5607
+ : Object.assign(new Error('The operation was aborted.'), { name: 'AbortError' });
5608
+ /**
5609
+ * PLAN-11 U-1 (S-1): a fetch-shaped call over XMLHttpRequest that reports progress:
5610
+ * `on(phase, loaded, total)` (total 0 when unknown), at most every 50 ms per phase, always the
5611
+ * first and the final step. Resolves with a real Response (blob body, parsed headers); network
5612
+ * errors and timeouts reject with a TypeError, an abort with an AbortError, like fetch.
5613
+ */
5614
+ const xhrFetch = (X, url, init, on) => new Promise((res, rej) => {
5615
+ const x = new X();
5616
+ const last = {};
5617
+ const step = (phase, loaded, total, final) => {
5618
+ const l = last[phase], now = Date.now();
5619
+ if (l && (l.loaded === loaded || (!final && now - l.at < 50 && !(total && loaded >= total))))
5620
+ return;
5621
+ last[phase] = { loaded, at: now };
5622
+ on(phase, loaded, total);
5623
+ };
5624
+ const ev = (phase) => (e) => step(phase, e.loaded, e.lengthComputable ? e.total : 0);
5625
+ x.open(init.method || 'GET', url);
5626
+ x.responseType = 'blob';
5627
+ const h = init.headers || {};
5628
+ Object.keys(h).forEach(k => x.setRequestHeader(k, h[k]));
5629
+ if (init.credentials == 'include')
5630
+ x.withCredentials = true;
5631
+ const sig = init.signal;
5632
+ if (sig) {
5633
+ if (sig.aborted)
5634
+ return rej(abortError$1());
5635
+ sig.addEventListener?.('abort', () => x.abort(), { once: true });
5636
+ }
5637
+ if (init.body != null && x.upload) {
5638
+ x.upload.onprogress = ev('upload');
5639
+ x.upload.onload = (e) => step('upload', e.loaded, e.lengthComputable ? e.total : e.loaded, 1);
5640
+ }
5641
+ x.onprogress = ev('download');
5642
+ x.onload = () => {
5643
+ const size = x.response?.size ?? last.download?.loaded ?? 0;
5644
+ step('download', size, size, 1);
5645
+ const headers = typeof Headers == 'function' ? new Headers() : undefined;
5646
+ String(x.getAllResponseHeaders?.() || '').trim().split(/[\r\n]+/).forEach((line) => {
5647
+ const i = line.indexOf(':');
5648
+ if (i > 0)
5649
+ try {
5650
+ headers?.append(line.slice(0, i).trim(), line.slice(i + 1).trim());
5651
+ }
5652
+ catch (_) { }
5653
+ });
5654
+ try {
5655
+ res(new Response(NULL_BODY.includes(x.status) ? null : x.response, { status: x.status, statusText: x.statusText, headers }));
5656
+ }
5657
+ catch (e) {
5658
+ rej(e);
5659
+ }
5660
+ };
5661
+ x.onerror = x.ontimeout = () => rej(new TypeError(`Network request failed: ${url}`));
5662
+ x.onabort = () => rej(abortError$1());
5663
+ x.send(init.body ?? null);
5664
+ });
5665
+ /** PLAN-11 U-5: a request's `multipart` object as FormData (see the header) */
5666
+ const toFormData = (parts) => {
5667
+ const fd = new FormData();
5668
+ Object.keys(parts || {}).forEach(k => [].concat(parts[k]).forEach((v) => {
5669
+ if (v == null)
5670
+ return;
5671
+ if (typeof Blob != 'undefined' && v instanceof Blob)
5672
+ fd.append(k, v, v.name ?? 'blob');
5673
+ else
5674
+ fd.append(k, typeof v == 'object' ? JSON.stringify(v) : String(v));
5675
+ }));
5676
+ return fd;
5677
+ };
5529
5678
  const IDEMPOTENT$1 = /^(GET|HEAD)$/i;
5530
5679
  // D80: an invalidate value matches a request by tag, URL prefix ('/…') or predicate
5531
5680
  const hit = (x, q) => {
@@ -5550,6 +5699,22 @@ function makeFetchDriver(options = {}) {
5550
5699
  // q (the request), timer, f (the fetch it waits on), ok / ko (its outcomes) }
5551
5700
  const inflight = new Map();
5552
5701
  const subs = new Set();
5702
+ // PLAN-11 U-1: progress(category?) listeners (not held while nobody listens)
5703
+ const psubs = new Set();
5704
+ const emitProgress = (v) => psubs.forEach(s => {
5705
+ let m = false;
5706
+ try {
5707
+ m = matches(s, v);
5708
+ }
5709
+ catch (_) { }
5710
+ if (m)
5711
+ try {
5712
+ s.l.next(v);
5713
+ }
5714
+ catch (e) {
5715
+ console.error('[Sygnal] makeFetchDriver: a listener threw', e);
5716
+ }
5717
+ });
5553
5718
  // R4-4: replies held until the first select() listener, failures until the first errors()
5554
5719
  // listener (or the next macrotask, when they're logged if nothing listens)
5555
5720
  const started = [false, false];
@@ -5612,6 +5777,10 @@ function makeFetchDriver(options = {}) {
5612
5777
  return {
5613
5778
  select: make(false),
5614
5779
  errors: make(true),
5780
+ progress: (sel) => {
5781
+ let sub;
5782
+ return xs$2.create({ start: l => { psubs.add((sub = { l, sel, ns })); }, stop: () => { psubs.delete(sub); } });
5783
+ },
5615
5784
  isolateSource: (_, scope) => source(ns.concat(scope)),
5616
5785
  isolateSink: (sink$, scope) => sink$.map((req) => tagRequest(req, scope)),
5617
5786
  // PLAN-4.6: the same per value (the core scopes a request without a stream per instance)
@@ -5731,7 +5900,7 @@ function makeFetchDriver(options = {}) {
5731
5900
  req = { url: req };
5732
5901
  if (!req || typeof req != 'object' || disposed || !allowed$1(req, 'makeFetchDriver'))
5733
5902
  return;
5734
- const { url, category, method, headers, query, json, body, latest, timeoutMs, parse, abort, init, ok, error, key, resources, refresh, invalidates, updates, retry, validate, prefetch } = req;
5903
+ const { url, category, method, headers, query, json, body, latest, timeoutMs, parse, abort, init, ok, error, key, resources, refresh, invalidates, updates, retry, validate, prefetch, progress } = req;
5735
5904
  if ('invalidate' in req)
5736
5905
  return inval(req.invalidate);
5737
5906
  if (prefetch)
@@ -5783,7 +5952,9 @@ function makeFetchDriver(options = {}) {
5783
5952
  }
5784
5953
  const scope = scopeKey(req);
5785
5954
  // reply actions: keyed per (sender, key ?? ok ?? error); a resource per (sender, '\0' + name)
5786
- const rk = rn !== undefined ? '\0' + rn : !pf && sender !== undefined && (ok || error) ? key ?? ok ?? error : undefined;
5955
+ // PLAN-11 U-1: a progress action is a reply action too (abort on dispose, its own lane)
5956
+ const pr = !pf && rn === undefined && !req.batch && progress;
5957
+ const rk = rn !== undefined ? '\0' + rn : !pf && sender !== undefined && (ok || error || typeof pr == 'string') ? key ?? ok ?? error ?? pr : undefined;
5787
5958
  if (sender !== undefined && (typeof abort == 'string' || (abort && key !== undefined)))
5788
5959
  return cancel(r => r.sender === sender && r.rk === (key ?? abort));
5789
5960
  // per (scope, category): another component's requests are never cancelled
@@ -5793,7 +5964,7 @@ function makeFetchDriver(options = {}) {
5793
5964
  cancel(rk === undefined ? r => r.rk === undefined && r.scope === scope && r.category === category : r => r.sender === sender && r.rk === rk);
5794
5965
  const base = options.init;
5795
5966
  // a request's URL and fetch() init (the batch's sub-requests inherit the request's fields)
5796
- const prep = ({ url, method, headers, query, json, body, init: own }) => {
5967
+ const prep = ({ url, method, headers, query, json, multipart, body, init: own }) => {
5797
5968
  const h = mergeHeaders(options.headers, base?.headers, own?.headers, headers);
5798
5969
  let b = body ?? own?.body;
5799
5970
  if (json !== undefined) {
@@ -5801,6 +5972,12 @@ function makeFetchDriver(options = {}) {
5801
5972
  if (!Object.keys(h).some(k => k.toLowerCase() == 'content-type'))
5802
5973
  h['content-type'] = 'application/json';
5803
5974
  }
5975
+ else if (multipart !== undefined) {
5976
+ b = toFormData(multipart);
5977
+ // the browser writes multipart/form-data with its boundary; a content-type set here would break it
5978
+ Object.keys(h).forEach(k => { if (k.toLowerCase() == 'content-type')
5979
+ delete h[k]; });
5980
+ }
5804
5981
  return [withQuery((options.baseUrl || '') + (url ?? ''), query), { ...pickInit(base), ...pickInit(own), method: method || own?.method || base?.method || (b !== undefined ? 'POST' : 'GET'), headers: h, body: b }];
5805
5982
  };
5806
5983
  const [href, fetchInit] = prep(req);
@@ -5809,7 +5986,8 @@ function makeFetchDriver(options = {}) {
5809
5986
  // parts, at most 3 deep), their parsed bodies merged by `combine` into one reply. The test
5810
5987
  // fake (_whole) answers such a request as one
5811
5988
  const { batch, split, combine = (x) => x } = options._whole ? {} : req;
5812
- const k = C?.key(req, rn !== undefined || pf);
5989
+ // PLAN-11 U-1: a progress request never shares a fetch (each sender gets its own steps)
5990
+ const k = pr ? undefined : C?.key(req, rn !== undefined || pf);
5813
5991
  const id = ++seq;
5814
5992
  const r = { category, scope, sender, rk };
5815
5993
  inflight.set(id, r);
@@ -5857,7 +6035,19 @@ function makeFetchDriver(options = {}) {
5857
6035
  const parser = parserOf(parse || options.parse);
5858
6036
  // the global fetch is called as a method: a detached window.fetch throws "Illegal invocation"
5859
6037
  const g = globalThis;
5860
- const doFetch = options.fetch || (typeof g.fetch == 'function' && ((u, i) => g.fetch(u, i)));
6038
+ let doFetch = options.fetch || (typeof g.fetch == 'function' && ((u, i) => g.fetch(u, i)));
6039
+ // PLAN-11 U-1: a progress request over XMLHttpRequest (renderComponent's fake: `_progress`)
6040
+ const X = options.XMLHttpRequest ?? g.XMLHttpRequest;
6041
+ if (pr && (options._progress || typeof X == 'function')) {
6042
+ const step = (phase, loaded, total) => {
6043
+ if (!inflight.has(id) || disposed)
6044
+ return;
6045
+ const t = total > 0 ? total : null;
6046
+ const v = { phase, loaded, total: t, fraction: t ? Math.min(1, loaded / t) : null, category, request: req };
6047
+ typeof pr == 'string' && sender !== undefined ? reply(sender, pr, v) : emitProgress(v);
6048
+ };
6049
+ doFetch = options._progress ? (u, i) => options._progress(u, i, step, req) : (u, i) => xhrFetch(X, u, i, step);
6050
+ }
5861
6051
  // the outcome, to every request waiting on this fetch (the failure once, after the last attempt)
5862
6052
  const end = (e, x, status) => {
5863
6053
  done(F);
@@ -6019,9 +6209,10 @@ function queryCache(o = {}) {
6019
6209
  const key = (q, res) => {
6020
6210
  if (typeof q == 'string')
6021
6211
  q = { url: q };
6022
- const { method, json, body, parse, query } = q;
6023
- const m = (method || (json !== undefined || body !== undefined ? 'POST' : 'GET')).toUpperCase();
6024
- const b = json !== undefined ? JSON.stringify(json) : body ?? '';
6212
+ const { method, json, body, parse, query, multipart } = q;
6213
+ const m = (method || (json !== undefined || body !== undefined || multipart !== undefined ? 'POST' : 'GET')).toUpperCase();
6214
+ // PLAN-11 U-5: a multipart request is never cached (its body isn't a string)
6215
+ const b = json !== undefined ? JSON.stringify(json) : multipart !== undefined ? multipart : body ?? '';
6025
6216
  if (q.cache === false || typeof b != 'string' || typeof parse == 'function' || parse == 'response' ||
6026
6217
  !(q.cache || q.staleTime != null || (res && IDEMPOTENT.test(m))))
6027
6218
  return;
@@ -6090,6 +6281,272 @@ function queryCache(o = {}) {
6090
6281
  return C;
6091
6282
  }
6092
6283
 
6284
+ /*
6285
+ * PLAN-6 L-2: what the HTTP transports share (side-effect free; each transport imports only what
6286
+ * it uses).
6287
+ * - post(): the fetch (the `fetch` option, 0-S5), the headers option (an object or a function of
6288
+ * the request), HTTP errors as Errors with `status` and the provider's message, and SYG670.
6289
+ * - sse(): a fetch Response body as SSE frames ({ event, data }), per the HTML spec's parsing
6290
+ * rules: CRLF / LF, comments, multi-line data. The reader is cancelled when the consumer stops
6291
+ * (abort, a thrown error), so the connection closes.
6292
+ * - SYG670: in a browser, a request with an auth header (Authorization, x-api-key, api-key,
6293
+ * x-goog-api-key) to a host that is neither local (localhost, 127.0.0.1, [::1], *.localhost)
6294
+ * nor the page's own origin is refused unless the transport has `dangerouslyAllowBrowser: true`:
6295
+ * the key would ship to every visitor. Same-origin requests are the app's own server (its
6296
+ * proxy), so a session header there is fine.
6297
+ */
6298
+ const g$6 = globalThis;
6299
+ const AUTH = /^(authorization|x-api-key|api-key|x-goog-api-key)$/i;
6300
+ const LOCAL = /^(localhost|127\.0\.0\.1|\[::1\]|.+\.localhost)$/;
6301
+ /** the URL a transport posts to, or a SYG670 Error when it must not (see the header) */
6302
+ function guard(url, headers, opts, name) {
6303
+ if (opts.dangerouslyAllowBrowser || typeof g$6.document == 'undefined' || !Object.keys(headers).some(h => AUTH.test(h)))
6304
+ return;
6305
+ const here = g$6.location?.href;
6306
+ let u;
6307
+ try {
6308
+ u = new URL(url, here);
6309
+ }
6310
+ catch (_) {
6311
+ return;
6312
+ }
6313
+ if (LOCAL.test(u.hostname) || (here && u.origin == new URL(here).origin))
6314
+ return;
6315
+ const fix = `Call ${u.origin} from your server and point the transport at that route (an AI SDK route with uiMessageStream('/api/chat'), or your own proxy), so the key stays there. For a key the user typed in themselves, pass ${name}({ ..., dangerouslyAllowBrowser: true })`;
6316
+ const msg = `${name}: refused to send an auth header from the browser to ${u.origin}: anyone who opens the page can read it`;
6317
+ error('SYG670', undefined, msg, fix);
6318
+ fail$2('SYG670', undefined, msg, fix);
6319
+ }
6320
+ /**
6321
+ * POST JSON; a non-2xx response is an Error with `status`, `body` and the provider's message.
6322
+ * The transport's `body` option and the request's `body` are merged over the built body (so they
6323
+ * can override provider defaults such as `max_tokens`); `fixed` fields are merged last and can't be
6324
+ * overridden (G-654: uiMessageStream's protocol fields, as the AI SDK's DefaultChatTransport)
6325
+ */
6326
+ async function post$1(url, body, req, signal, opts, name, fixed) {
6327
+ const h = typeof opts.headers == 'function' ? await opts.headers(req) : opts.headers || {};
6328
+ guard(url, h, opts, name);
6329
+ const f = opts.fetch || ((u, i) => g$6.fetch(u, i));
6330
+ const res = await f(url, { method: 'POST', headers: { 'content-type': 'application/json', ...h }, body: JSON.stringify({ ...body, ...opts.body, ...req.body, ...fixed }), signal });
6331
+ if (!res.ok) {
6332
+ let text = '', message = '';
6333
+ try {
6334
+ text = await res.text();
6335
+ const j = JSON.parse(text);
6336
+ message = j.error?.message ?? j.error ?? j.message ?? '';
6337
+ }
6338
+ catch (_) {
6339
+ message || (message = text.slice(0, 200));
6340
+ }
6341
+ throw Object.assign(new Error(`${name}: HTTP ${res.status}${message ? ': ' + message : ''}`), { status: res.status, body: text });
6342
+ }
6343
+ if (!res.body)
6344
+ throw new Error(`${name}: the response has no body`);
6345
+ return res;
6346
+ }
6347
+ /**
6348
+ * the SSE frames of a response body: `{ event, data }` (data: the joined data lines). PLAN-11
6349
+ * E-1: `meta` (optional) follows the stream's `id` (the last event id, kept across frames) and
6350
+ * `retry` (ms) fields, as EventSource does, for a reconnect
6351
+ */
6352
+ async function* sse(res, meta) {
6353
+ const reader = res.body.getReader(), dec = new TextDecoder();
6354
+ let buf = '', cr = '', done = false;
6355
+ try {
6356
+ for (;;) {
6357
+ const r = await reader.read();
6358
+ // line ends -> \n (a \r at a chunk's end waits: it may be half of a \r\n)
6359
+ let s = cr + (r.done ? '\n\n' : dec.decode(r.value, { stream: true }));
6360
+ cr = !r.done && s.endsWith('\r') ? '\r' : '';
6361
+ if (cr)
6362
+ s = s.slice(0, -1);
6363
+ buf += s.replace(/\r\n?/g, '\n');
6364
+ done = r.done;
6365
+ let i;
6366
+ while ((i = buf.indexOf('\n\n')) >= 0) {
6367
+ const frame = buf.slice(0, i);
6368
+ buf = buf.slice(i + 2);
6369
+ let event = 'message', data = [];
6370
+ for (const line of frame.split('\n')) {
6371
+ if (!line || line[0] == ':')
6372
+ continue;
6373
+ const c = line.indexOf(':'), field = c < 0 ? line : line.slice(0, c);
6374
+ const v = c < 0 ? '' : line.slice(c + 1).replace(/^ /, '');
6375
+ if (field == 'event')
6376
+ event = v;
6377
+ else if (field == 'data')
6378
+ data.push(v);
6379
+ else if (meta && field == 'id' && !v.includes('\0'))
6380
+ meta.id = v;
6381
+ else if (meta && field == 'retry' && /^\d+$/.test(v))
6382
+ meta.retry = +v;
6383
+ }
6384
+ if (data.length)
6385
+ yield { event, data: data.join('\n') };
6386
+ }
6387
+ if (done)
6388
+ return;
6389
+ }
6390
+ }
6391
+ finally {
6392
+ if (!done)
6393
+ reader.cancel().catch(() => { });
6394
+ }
6395
+ }
6396
+ /** JSON.parse, or the text itself when it isn't JSON (a model's malformed tool arguments go to validation as is) */
6397
+ const json$1 = (s) => {
6398
+ if (typeof s != 'string')
6399
+ return s;
6400
+ try {
6401
+ return JSON.parse(s || '{}');
6402
+ }
6403
+ catch (_) {
6404
+ return s;
6405
+ }
6406
+ };
6407
+ /** a provider's usage as `{ inputTokens, outputTokens, totalTokens, reasoningTokens? }` (AI SDK names) */
6408
+ const usageOf = (u) => u && {
6409
+ inputTokens: u.input_tokens ?? u.prompt_tokens,
6410
+ outputTokens: u.output_tokens ?? u.completion_tokens,
6411
+ totalTokens: u.total_tokens,
6412
+ ...((u.output_tokens_details ?? u.completion_tokens_details)?.reasoning_tokens != null && { reasoningTokens: (u.output_tokens_details ?? u.completion_tokens_details).reasoning_tokens }),
6413
+ };
6414
+ /** the parts of a message (a `content` string is one text part) */
6415
+ const partsOf = (m) => m.parts ?? (typeof m.content == 'string' ? [{ type: 'text', text: m.content }] : []);
6416
+ /** the tool name of a `tool-<name>` / `dynamic-tool` part, or '' */
6417
+ const toolName = (p) => p.type == 'dynamic-tool' ? p.toolName : p.type.startsWith('tool-') ? p.type.slice(5) : '';
6418
+ /** what a tool part sends back to the model, or undefined when it has no result yet */
6419
+ const toolOutput = (p) => p.state == 'output-available' ? (typeof p.output == 'string' ? p.output : JSON.stringify(p.output ?? null))
6420
+ : p.state == 'output-error' ? JSON.stringify({ error: p.errorText ?? 'error' })
6421
+ : p.state == 'output-denied' ? JSON.stringify({ error: 'The user denied this tool call' })
6422
+ : undefined;
6423
+ // dev-only diagnostics (SYG675): the dev entry formats them (checks/chat.ts)
6424
+ const dev$8 = (...a) => g$6.__SYGNAL_DIAGNOSTICS__?.chat?.(...a);
6425
+
6426
+ /*
6427
+ * PLAN-11 E-1: an EventSource over fetch, for server-sent events that need request headers
6428
+ * (`Authorization`), which the native EventSource can't send. makeSocketDriver uses it for an
6429
+ * `sse` connection with `headers`. It behaves as EventSource does where the driver relies on it:
6430
+ *
6431
+ * - connects on construction (a microtask later, so handlers can be set first); `readyState`
6432
+ * CONNECTING (0) → OPEN (1) on a 2xx `text/event-stream` response;
6433
+ * - `onmessage` and `addEventListener(type)` get `{ type, data, lastEventId }`;
6434
+ * - the stream ending or a network error: `error`, readyState CONNECTING, a reconnect after
6435
+ * `retry` ms (the stream's `retry:` field, default 3000) with `Last-Event-ID`;
6436
+ * - a non-2xx status or another content type: `error` with readyState CLOSED (no retry; the
6437
+ * driver's reconnect policy decides);
6438
+ * - `close()` aborts the request and stops retrying.
6439
+ */
6440
+ const NAMES = ['CONNECTING', 'OPEN', 'CLOSED'];
6441
+ class FetchEventSource {
6442
+ constructor(url, init = {}) {
6443
+ this.readyState = 0;
6444
+ this.onopen = null;
6445
+ this.onmessage = null;
6446
+ this.onerror = null;
6447
+ this.ls = new Map();
6448
+ this.meta = { id: '', retry: 3000 };
6449
+ this.url = url;
6450
+ this.withCredentials = !!init.withCredentials;
6451
+ this.init = init;
6452
+ queueMicrotask(() => this.connect());
6453
+ }
6454
+ addEventListener(type, f) {
6455
+ let s = this.ls.get(type);
6456
+ if (!s)
6457
+ this.ls.set(type, (s = new Set()));
6458
+ s.add(f);
6459
+ }
6460
+ removeEventListener(type, f) { this.ls.get(type)?.delete(f); }
6461
+ close() {
6462
+ this.readyState = 2;
6463
+ clearTimeout(this.timer);
6464
+ try {
6465
+ this.ctl?.abort();
6466
+ }
6467
+ catch (_) { }
6468
+ }
6469
+ emit(type, ev) {
6470
+ if (type == 'message')
6471
+ this.onmessage?.(ev);
6472
+ else if (type == 'open')
6473
+ this.onopen?.(ev);
6474
+ else if (type == 'error')
6475
+ this.onerror?.(ev);
6476
+ this.ls.get(type)?.forEach(f => { try {
6477
+ f(ev);
6478
+ }
6479
+ catch (e) {
6480
+ console.error(e);
6481
+ } });
6482
+ }
6483
+ async connect() {
6484
+ if (this.readyState == 2)
6485
+ return;
6486
+ const g = globalThis;
6487
+ const f = this.init.fetch || (typeof g.fetch == 'function' && ((u, i) => g.fetch(u, i)));
6488
+ if (typeof f != 'function')
6489
+ return this.fail(new Error('fetch is not available in this environment'));
6490
+ const ctl = this.ctl = typeof AbortController == 'function' ? new AbortController() : {};
6491
+ const headers = { accept: 'text/event-stream', ...this.init.headers };
6492
+ if (this.meta.id)
6493
+ headers['last-event-id'] = this.meta.id;
6494
+ let res;
6495
+ try {
6496
+ res = await f(this.url, { headers, credentials: this.withCredentials ? 'include' : 'same-origin', cache: 'no-store', signal: ctl.signal });
6497
+ }
6498
+ catch (error) {
6499
+ return this.retry(error);
6500
+ }
6501
+ if (this.readyState == 2 || this.ctl !== ctl)
6502
+ return;
6503
+ const type = res.headers?.get?.('content-type') || '';
6504
+ if (!(res.status >= 200 && res.status < 300) || !/^text\/event-stream/i.test(type) || !res.body) {
6505
+ try {
6506
+ res.body?.cancel?.();
6507
+ }
6508
+ catch (_) { }
6509
+ return this.fail(Object.assign(new Error(`SSE ${res.status}${type ? ' ' + type : ''}: ${this.url}`), { status: res.status }));
6510
+ }
6511
+ this.readyState = 1;
6512
+ this.emit('open', { type: 'open' });
6513
+ try {
6514
+ for await (const fr of sse(res, this.meta)) {
6515
+ if (this.readyState == 2 || this.ctl !== ctl)
6516
+ return;
6517
+ this.emit(fr.event, { type: fr.event, data: fr.data, lastEventId: this.meta.id });
6518
+ }
6519
+ }
6520
+ catch (error) {
6521
+ if (this.readyState == 2 || this.ctl !== ctl)
6522
+ return;
6523
+ return this.retry(error);
6524
+ }
6525
+ if (this.readyState != 2 && this.ctl === ctl)
6526
+ this.retry();
6527
+ }
6528
+ /** a drop: error, CONNECTING, try again after `retry` ms (unless the error handler closed it) */
6529
+ retry(error) {
6530
+ if (this.readyState == 2)
6531
+ return;
6532
+ this.readyState = 0;
6533
+ this.emit('error', { type: 'error', error });
6534
+ if (this.readyState == 2)
6535
+ return;
6536
+ this.timer = setTimeout(() => this.connect(), this.meta.retry);
6537
+ }
6538
+ /** a failed connection: error, CLOSED */
6539
+ fail(error) {
6540
+ this.readyState = 2;
6541
+ this.emit('error', { type: 'error', error });
6542
+ }
6543
+ /** the state's name (debugging) */
6544
+ get state() { return NAMES[this.readyState]; }
6545
+ }
6546
+ FetchEventSource.CONNECTING = 0;
6547
+ FetchEventSource.OPEN = 1;
6548
+ FetchEventSource.CLOSED = 2;
6549
+
6093
6550
  /*
6094
6551
  * makeSocketDriver(options?) (PLAN-3 §1.3): WebSocket and server-sent events, declared per
6095
6552
  * component instance and answered with reply actions (./replies.ts). Docs on the declarations
@@ -6098,7 +6555,7 @@ function queryCache(o = {}) {
6098
6555
  * - Sink values:
6099
6556
  * - `{ connections: { [name]: spec | falsy } }`: the sender's whole set of connections. Diffed
6100
6557
  * per (sender, name): a new name opens, a removed or falsy one closes, a changed URL /
6101
- * protocols / withCredentials / share closes and reopens, anything else (action names,
6558
+ * protocols / withCredentials / headers / share closes and reopens, anything else (action names,
6102
6559
  * reconnect) only rebinds.
6103
6560
  * - `{ to: name, json? | text? | binary? }`: send on the sender's connection `name` (json is
6104
6561
  * stringified). Queued while (re)connecting, bounded (`queueLimit`, oldest dropped). A send
@@ -6123,6 +6580,10 @@ function queryCache(o = {}) {
6123
6580
  * - A disposed sender's connections close (and their retries stop); app dispose closes all.
6124
6581
  * - No WebSocket / EventSource (SSR): nothing opens, nothing is reported.
6125
6582
  * - `WebSocket` / `EventSource` are read at connect time: options.WebSocket ?? globalThis.WebSocket.
6583
+ * - PLAN-11 E-1: an `sse` connection with `headers` (data: a token in it comes from state, and a
6584
+ * changed header reconnects) is read with FetchEventSource (fetch, `options.fetch` if given),
6585
+ * since EventSource can't send headers. An `options.EventSource` (a polyfill, the test fake)
6586
+ * still wins and gets the headers in its init.
6126
6587
  */
6127
6588
  const SCOPE$1 = '__sygnalScope';
6128
6589
  const scopeOf = (v) => v[SCOPE$1] || [];
@@ -6222,12 +6683,13 @@ function makeSocketDriver(options = {}) {
6222
6683
  };
6223
6684
  const connect = (t) => {
6224
6685
  t.timer = 0;
6225
- const C = t.sse ? options.EventSource ?? g.EventSource : options.WebSocket ?? g.WebSocket;
6686
+ const hdr = t.sse && t.spec.headers && typeof t.spec.headers == 'object' ? t.spec.headers : undefined;
6687
+ const C = t.sse ? options.EventSource ?? (hdr && typeof g.fetch == 'function' ? FetchEventSource : g.EventSource) : options.WebSocket ?? g.WebSocket;
6226
6688
  if (typeof C != 'function')
6227
6689
  return;
6228
6690
  let s;
6229
6691
  try {
6230
- s = t.sse ? new C(t.url, { withCredentials: !!t.spec.withCredentials }) : new C(t.url, t.spec.protocols);
6692
+ s = t.sse ? new C(t.url, { withCredentials: !!t.spec.withCredentials, ...(hdr && { headers: hdr, fetch: options.fetch }) }) : new C(t.url, t.spec.protocols);
6231
6693
  }
6232
6694
  catch (error) {
6233
6695
  // a URL the constructor rejects never opens: no retry
@@ -6331,7 +6793,7 @@ function makeSocketDriver(options = {}) {
6331
6793
  const sse = typeof spec.sse == 'string';
6332
6794
  const url = resolve(sse ? spec.sse : spec.socket, sse);
6333
6795
  // the connection's identity: a change reconnects, anything else only rebinds
6334
- const key = (spec.share === false ? 'u' : '') + (sse ? 'e' + !!spec.withCredentials : 's' + JSON.stringify(spec.protocols)) + url;
6796
+ const key = (spec.share === false ? 'u' : '') + (sse ? 'e' + !!spec.withCredentials + (spec.headers ? JSON.stringify(spec.headers) : '') : 's' + JSON.stringify(spec.protocols)) + url;
6335
6797
  if (old && old.key === key) {
6336
6798
  old.spec = spec;
6337
6799
  if (old.t)
@@ -6408,7 +6870,7 @@ const dec = (s) => { try {
6408
6870
  catch (_) {
6409
6871
  return s;
6410
6872
  } };
6411
- const g$6 = globalThis;
6873
+ const g$5 = globalThis;
6412
6874
  const PARAM$1 = /:(\w+)/g;
6413
6875
  // G-571: synchronous navigations (redirects) before a microtask, after which each waits a task
6414
6876
  const HOPS = 32;
@@ -6416,7 +6878,7 @@ const HOPS = 32;
6416
6878
  const paramsOf$2 = (pat) => (pat.match(PARAM$1) || []).map(p => p.slice(1));
6417
6879
  // the core's coded error() (published on the diagnostics bridge): no import, so the router
6418
6880
  // costs nothing more standalone; prints in production too (like SYG611)
6419
- const bad$1 = (v, msg, fix) => (g$6.__SYGNAL_DIAGNOSTICS__?.error || console.error)('SYG620', v.__emitterName, `router: ${msg}; not navigated`, fix, v);
6881
+ const bad$1 = (v, msg, fix) => (g$5.__SYGNAL_DIAGNOSTICS__?.error || console.error)('SYG620', v.__emitterName, `router: ${msg}; not navigated`, fix, v);
6420
6882
  function makeRouter(options = {}) {
6421
6883
  const { mode, prefetch, navigate } = options, routes = options.routes || {};
6422
6884
  const base = (options.base || '').replace(/\/$/, '');
@@ -6425,7 +6887,7 @@ function makeRouter(options = {}) {
6425
6887
  const doScroll = options.scroll ?? !navigate;
6426
6888
  const href = (name, params = {}, query, hash) => {
6427
6889
  // dev entry only (SYG130: unknown name, missing or extra params)
6428
- g$6.__SYGNAL_DIAGNOSTICS__?.routerHref?.(routes, name, params);
6890
+ g$5.__SYGNAL_DIAGNOSTICS__?.routerHref?.(routes, name, params);
6429
6891
  const pat = routes[name];
6430
6892
  const path = pat == null || pat == '*' ? '/' : pat.replace(PARAM$1, (_, k) => enc(params[k] ?? ''));
6431
6893
  const q = query && new URLSearchParams(Object.entries(query).filter(([, v]) => v != null)) + '';
@@ -6535,7 +6997,7 @@ function makeRouter(options = {}) {
6535
6997
  if (!hops++)
6536
6998
  queueMicrotask(() => { hops = 0; });
6537
6999
  if (hops > HOPS) {
6538
- if (g$6.__SYGNAL_DIAGNOSTICS__?.hooks?.on?.() && !warned++)
7000
+ if (g$5.__SYGNAL_DIAGNOSTICS__?.hooks?.on?.() && !warned++)
6539
7001
  console.warn(`[Sygnal] router: more than ${HOPS} navigations in one flush (a redirect loop?); each next one waits a task (reported once)`, r.path);
6540
7002
  setTimeout(send);
6541
7003
  }
@@ -8077,7 +8539,7 @@ function makeHeadDriver(options = {}) {
8077
8539
  * - An invalid spec (non-positive `every`, negative `after`, no action) doesn't start; the dev
8078
8540
  * entry reports SYG422 through the `timerSpec` diagnostics hook.
8079
8541
  */
8080
- const g$5 = globalThis;
8542
+ const g$4 = globalThis;
8081
8543
  const now$1 = () => Date.now();
8082
8544
  const valid = (s) => s.frame
8083
8545
  ? typeof s.frame == 'string'
@@ -8087,7 +8549,7 @@ const valid = (s) => s.frame
8087
8549
  // changes) the timer clears the pending one.
8088
8550
  const start$1 = (s, send, e) => {
8089
8551
  let id, n = 0, last;
8090
- const t0 = now$1(), every = s.every, raf = s.frame && g$5.requestAnimationFrame;
8552
+ const t0 = now$1(), every = s.every, raf = s.frame && g$4.requestAnimationFrame;
8091
8553
  const later = (ms) => id = raf ? requestAnimationFrame(tick) : setTimeout(tick, ms);
8092
8554
  // a frame's `ts` is requestAnimationFrame's timestamp (the fallback passes none: the clock)
8093
8555
  const tick = (ts = now$1()) => {
@@ -8132,7 +8594,7 @@ const timerDriver = (runners) => (sink$) => {
8132
8594
  if (e.ok = valid(s))
8133
8595
  e.x = start$1(s, (type, data) => reply(id, type, data), e);
8134
8596
  else
8135
- g$5.__SYGNAL_DIAGNOSTICS__?.timerSpec?.(n, s, c);
8597
+ g$4.__SYGNAL_DIAGNOSTICS__?.timerSpec?.(n, s, c);
8136
8598
  }
8137
8599
  if (!d)
8138
8600
  runners.delete(id);
@@ -8190,10 +8652,10 @@ const makeTimerDriver = () => timerDriver(new Map());
8190
8652
  * an intersection / resize declaration with nothing to observe (no DOM source bound). A
8191
8653
  * component declaring `browser` with no driver is SYG643, as timers.
8192
8654
  */
8193
- const g$4 = globalThis;
8655
+ const g$3 = globalThis;
8194
8656
  const noop = () => { };
8195
8657
  const KINDS = ['intersection', 'resize', 'media', 'storage', 'visibility', 'online', 'geolocation'];
8196
- const diag = (...a) => g$4.__SYGNAL_DIAGNOSTICS__?.browserSource?.(...a);
8658
+ const diag = (...a) => g$3.__SYGNAL_DIAGNOSTICS__?.browserSource?.(...a);
8197
8659
  // the driver names in use (the definition hook looks each up in a declaring instance's sources)
8198
8660
  const names$1 = new Set();
8199
8661
  let hooked;
@@ -8279,16 +8741,16 @@ const track = (c, kind, target, Obs, opts, data) => {
8279
8741
  };
8280
8742
  /** Element visibility: `{ intersection: '.cover' | true, action, threshold?, rootMargin? }`, data `{ visible, ratio, index, dataset }` */
8281
8743
  const intersectionSource = { d: {
8282
- intersection: (s, c) => track(c, 'i', s.intersection, g$4.IntersectionObserver, { threshold: s.threshold, rootMargin: s.rootMargin }, (e) => ({ visible: e.isIntersecting, ratio: e.intersectionRatio })),
8744
+ intersection: (s, c) => track(c, 'i', s.intersection, g$3.IntersectionObserver, { threshold: s.threshold, rootMargin: s.rootMargin }, (e) => ({ visible: e.isIntersecting, ratio: e.intersectionRatio })),
8283
8745
  } };
8284
8746
  /** Element size: `{ resize: '.chart' | true, action }`, data `{ width, height, index, dataset }` (the content box) */
8285
8747
  const resizeSource = { d: {
8286
- resize: (s, c) => track(c, 'r', s.resize, g$4.ResizeObserver, undefined, (e) => ({ width: e.contentRect.width, height: e.contentRect.height })),
8748
+ resize: (s, c) => track(c, 'r', s.resize, g$3.ResizeObserver, undefined, (e) => ({ width: e.contentRect.width, height: e.contentRect.height })),
8287
8749
  } };
8288
8750
  /** A media query: `{ media: '(prefers-color-scheme: dark)', action }`, data `{ matches, media }` */
8289
8751
  const mediaSource = { d: {
8290
8752
  media: (s, c) => {
8291
- const m = g$4.matchMedia?.(s.media);
8753
+ const m = g$3.matchMedia?.(s.media);
8292
8754
  if (!m)
8293
8755
  return;
8294
8756
  const f = () => c.send({ matches: m.matches, media: s.media });
@@ -8297,7 +8759,7 @@ const mediaSource = { d: {
8297
8759
  return () => m.removeEventListener('change', f);
8298
8760
  },
8299
8761
  } };
8300
- const area = (s) => g$4[s.area == 'session' ? 'sessionStorage' : 'localStorage'];
8762
+ const area = (s) => g$3[s.area == 'session' ? 'sessionStorage' : 'localStorage'];
8301
8763
  const failed$1 = (e) => ({ name: e?.name, message: e?.message });
8302
8764
  /**
8303
8765
  * A storage key (read and observed, other tabs' writes included): `{ storage: 'theme', action,
@@ -8326,8 +8788,8 @@ const storageSource = {
8326
8788
  };
8327
8789
  f();
8328
8790
  watching.add(f);
8329
- g$4.addEventListener?.('storage', f);
8330
- return () => { watching.delete(f); g$4.removeEventListener?.('storage', f); };
8791
+ g$3.addEventListener?.('storage', f);
8792
+ return () => { watching.delete(f); g$3.removeEventListener?.('storage', f); };
8331
8793
  } },
8332
8794
  c: (() => {
8333
8795
  const write = (v, value, ok, fail) => {
@@ -8354,7 +8816,7 @@ const storageSource = {
8354
8816
  /** The document's visibility: `{ visibility: true, action }`, data `{ visible }` */
8355
8817
  const visibilitySource = { d: {
8356
8818
  visibility: (_, c) => {
8357
- const d = g$4.document;
8819
+ const d = g$3.document;
8358
8820
  if (!d)
8359
8821
  return;
8360
8822
  const f = () => c.send({ visible: d.visibilityState != 'hidden' });
@@ -8366,13 +8828,13 @@ const visibilitySource = { d: {
8366
8828
  /** The network: `{ online: true, action }`, data `{ online }` */
8367
8829
  const onlineSource = { d: {
8368
8830
  online: (_, c) => {
8369
- if (!g$4.navigator || !g$4.addEventListener)
8831
+ if (!g$3.navigator || !g$3.addEventListener)
8370
8832
  return;
8371
- const f = () => c.send({ online: g$4.navigator.onLine });
8833
+ const f = () => c.send({ online: g$3.navigator.onLine });
8372
8834
  f();
8373
- g$4.addEventListener('online', f);
8374
- g$4.addEventListener('offline', f);
8375
- return () => { g$4.removeEventListener('online', f); g$4.removeEventListener('offline', f); };
8835
+ g$3.addEventListener('online', f);
8836
+ g$3.addEventListener('offline', f);
8837
+ return () => { g$3.removeEventListener('online', f); g$3.removeEventListener('offline', f); };
8376
8838
  },
8377
8839
  } };
8378
8840
  /**
@@ -8382,7 +8844,7 @@ const onlineSource = { d: {
8382
8844
  */
8383
8845
  const geolocationSource = { d: {
8384
8846
  geolocation: (s, c) => {
8385
- const geo = g$4.navigator?.geolocation;
8847
+ const geo = g$3.navigator?.geolocation;
8386
8848
  if (!geo)
8387
8849
  return c.fail({ code: 2, message: 'Geolocation is not available' });
8388
8850
  const id = geo.watchPosition((p) => {
@@ -8398,11 +8860,11 @@ const geolocationSource = { d: {
8398
8860
  */
8399
8861
  const clipboardSource = { c: {
8400
8862
  copy: (v, ok, fail) => {
8401
- const text = '' + v.copy, cb = g$4.navigator?.clipboard;
8863
+ const text = '' + v.copy, cb = g$3.navigator?.clipboard;
8402
8864
  cb ? cb.writeText(text).then(() => ok({ text }), (e) => fail(failed$1(e))) : fail({ name: 'NotSupportedError', message: 'Clipboard is not available' });
8403
8865
  },
8404
8866
  paste: (_, ok, fail) => {
8405
- const cb = g$4.navigator?.clipboard;
8867
+ const cb = g$3.navigator?.clipboard;
8406
8868
  cb ? cb.readText().then((text) => ok({ text }), (e) => fail(failed$1(e))) : fail({ name: 'NotSupportedError', message: 'Clipboard is not available' });
8407
8869
  },
8408
8870
  } };
@@ -9107,6 +9569,9 @@ class App {
9107
9569
  }
9108
9570
  /** STATE.stream: the instances whose state changed emit it */
9109
9571
  notify() {
9572
+ if (this.nv === this.ver)
9573
+ return;
9574
+ this.nv = this.ver;
9110
9575
  for (const i of this.watchers)
9111
9576
  i.notify();
9112
9577
  }
@@ -9234,7 +9699,7 @@ class App {
9234
9699
  this.ran = false;
9235
9700
  this.last = v;
9236
9701
  this.hooks.onPatch?.(v);
9237
- this.vdomL?.next(v);
9702
+ this.vdomL?.next(v ?? null);
9238
9703
  }
9239
9704
  if (this.dq.length)
9240
9705
  this.stopLater();
@@ -9923,7 +10388,7 @@ const W$1 = '__sw';
9923
10388
  const HOST = /^(id|class(Name)?|style|title|name|placeholder|role|tab[iI]ndex|hidden|lang|dir|attrs)$|^(aria|data)-/;
9924
10389
  /** the dev entry's reporter (checks/widgets.ts); returns the diagnostic when one was reported
9925
10390
  * (r: G-580, a 661 the widget reported through error(e), not a throwing update) */
9926
- const dev$8 = (code, w, o, x, r) => globalThis.__SYGNAL_DIAGNOSTICS__?.widget?.(code, w, o, x, r);
10391
+ const dev$7 = (code, w, o, x, r) => globalThis.__SYGNAL_DIAGNOSTICS__?.widget?.(code, w, o, x, r);
9927
10392
  let ids$1 = 0;
9928
10393
  // shallow; G-362: an object host prop (style, attrs, a class map) compares one level deep, so a
9929
10394
  // new object with the same entries is no change (snabbdom patches the host from it anyway)
@@ -9936,16 +10401,16 @@ const same$2 = (a, b, d) => {
9936
10401
  // reported through error(e)
9937
10402
  function fail$1(o, w, k, code, e, r) {
9938
10403
  e = e instanceof Error ? e : Error(e);
9939
- dev$8(code, w, o, e, r) || console.error(`[Sygnal SYG${code}]`, e);
10404
+ dev$7(code, w, o, e, r) || console.error(`[Sygnal SYG${code}]`, e);
9940
10405
  o.app.appError(o, e, 'widget');
9941
10406
  if (code < 662 && !o.disposed)
9942
10407
  (o.$wf || (o.$wf = new Map())).set(k, [e]), o.refresh();
9943
10408
  }
9944
10409
  function mount(v) {
9945
10410
  const el = v.elm, { ww: w, wp: p, wo: o } = v.data;
9946
- const d = (n, detail) => { dev$8(140, w, o, n); el.dispatchEvent(new CustomEvent(n, { detail, bubbles: true })); };
10411
+ const d = (n, detail) => { dev$7(140, w, o, n); el.dispatchEvent(new CustomEvent(n, { detail, bubbles: true })); };
9947
10412
  let i;
9948
- dev$8(144, w, o, el);
10413
+ dev$7(144, w, o, el);
9949
10414
  // G-409: mount's fourth parameter reports a later failure (an adapter's own redraw): SYG661,
9950
10415
  // the owner's onError fallback in this instance's place, as for a throwing update (G-580: the
9951
10416
  // dev text says error(e) reported it)
@@ -10065,7 +10530,7 @@ function defineWidget(def) {
10065
10530
  },
10066
10531
  $ssr: host,
10067
10532
  // D190/D199: a widget tag is not a selector (SYG143 in dev): select its className or a control
10068
- toString: () => (dev$8(143, w), ':not(*)'),
10533
+ toString: () => (dev$7(143, w), ':not(*)'),
10069
10534
  });
10070
10535
  }
10071
10536
 
@@ -10114,6 +10579,30 @@ function defineWidget(def) {
10114
10579
  * can still be assigned), which reads the host's current `uses`.
10115
10580
  * - Limitations: a host intent must return an object (not one stream).
10116
10581
  */
10582
+ /** A behavior output's slice from before the action (outputs get the updated one): for a transition shared with STATE */
10583
+ const was = (s, p, k) => p?.previousState ? p.previousState[k] : s;
10584
+ /**
10585
+ * A behavior model whose outputs compute from the state before the action (its slice and
10586
+ * `props.state`), for behaviors whose STATE and outputs share one transition; STATE / HOST as is
10587
+ */
10588
+ const fromPrevious = (model) => {
10589
+ const out = {};
10590
+ for (const a in model) {
10591
+ const e = model[a];
10592
+ if (!e || typeof e != 'object') {
10593
+ out[a] = e;
10594
+ continue;
10595
+ }
10596
+ const m = {};
10597
+ for (const s in e) {
10598
+ const f = e[s];
10599
+ m[s] = s == 'STATE' || s == 'HOST' || typeof f != 'function' ? f
10600
+ : (x, d, n, p, o, k) => f(was(x, p, k), d, n, p?.previousState ? { ...p, state: p.previousState } : p, o, k);
10601
+ }
10602
+ out[a] = m;
10603
+ }
10604
+ return out;
10605
+ };
10117
10606
  // a model entry as { sink: fn }: a constant is sent as is, `true` sends the action's data, an
10118
10607
  // EFFECT constant does nothing (as the core treats them)
10119
10608
  const sinksOf = (e, S) => {
@@ -10296,6 +10785,8 @@ const pager = /*#__PURE__*/ defineBehavior({
10296
10785
  });
10297
10786
 
10298
10787
  const isRow = (r) => r?.id != null;
10788
+ /** PLAN-11 F-2: a File / Blob is a field value (a leaf), not an object of fields */
10789
+ const isFile = (v) => typeof Blob != 'undefined' && v instanceof Blob;
10299
10790
  // the key of segment `s` in `v`: an array's row with that id, else the index unless the element
10300
10791
  // there is a row (G-380: a row is named by its id only; -1, no element), an object's key
10301
10792
  const key = (v, s) => {
@@ -10342,7 +10833,7 @@ const setField = (values, name, value) => {
10342
10833
  * array-level error such as "at least one address"); a row's `id` is not a field
10343
10834
  */
10344
10835
  const fieldNames = (v, at = '', row) => {
10345
- if (!v || typeof v != 'object')
10836
+ if (!v || typeof v != 'object' || isFile(v))
10346
10837
  return at ? [at] : [];
10347
10838
  const a = Array.isArray(v), out = a ? [at] : [];
10348
10839
  for (const k in v) {
@@ -10423,7 +10914,12 @@ const focusInvalid = (names, within) => {
10423
10914
  * field.
10424
10915
  *
10425
10916
  * Options: `values` (the start values; D239: or a function of the host's state, called when the
10426
- * form starts), `submit` (host action), `check` (async per-field checks through a driver: a
10917
+ * form starts), `submit` (host action), `request` (PLAN-11 F-3: the form sends the valid submit
10918
+ * itself on the `http` sink: a request, or `(data, hostState) => request`, completed with the
10919
+ * body (`multipart: data` when a top-level value is a File / Blob or an array of them, else
10920
+ * `json: data`, unless it sets json / multipart / body), `progress: '<key>.PROGRESS'` (unless set)
10921
+ * and its own `ok: '<key>.DONE'`, `error: '<key>.ERRORS'`; with `submit` too, that host action is
10922
+ * still dispatched (for state), and should send no request), `check` (async per-field checks through a driver: a
10427
10923
  * request with a reply action, `latest`), `show` ('blur' default: a schema error shows once its
10428
10924
  * field was blurred; 'input'; 'submit'), `form` (selector), `http` (the driver sink of the
10429
10925
  * checks, 'HTTP'), `resetOnShow` (D239), `tool` (PLAN-6 A-3, experimental: `formTool({ … })` from
@@ -10442,6 +10938,8 @@ const focusInvalid = (names, within) => {
10442
10938
  * before that (a child's first render) the fields come from calling it with a blank state.
10443
10939
  *
10444
10940
  * Slice (state.form): values, initial, errors (all current schema errors by name), touched,
10941
+ * progress (PLAN-11 F-2: the submit's last progress step or null), resets (counts RESET and
10942
+ * resetOnShow starts: key a file input with it, since a file input can't be cleared by state),
10445
10943
  * server (server errors), remote (async check results: '' passed), pending (name -> value being
10446
10944
  * checked), submitting, submitted, submitCount, queued (a submit waits for a check or an async
10447
10945
  * schema), validating (an async schema runs), validated (the schema has answered since the start
@@ -10451,11 +10949,14 @@ const focusInvalid = (names, within) => {
10451
10949
  *
10452
10950
  * Actions: form.CHANGE ({ name, value, item? }: G-376: a checkbox gives `checked`, and `item`, its
10453
10951
  * value, which toggles membership when the field is an array; <select multiple> an array of the
10454
- * selected values; type=file is ignored), form.SET ({ values }: G-647, field name -> value, several
10952
+ * selected values; PLAN-11 F-2: a file input its File, `multiple` an array of them, none null),
10953
+ * form.SET ({ values }: G-647, field name -> value, several
10455
10954
  * fields changed at once as if typed: one validation, `touched` as for CHANGE, unknown names
10456
10955
  * skipped; e.g. an AI fill), form.BLUR (name), form.SUBMIT, form.ADD ({ field,
10457
10956
  * value }), form.REMOVE ({ field, id }), form.ERRORS (an error reply or a map: server errors),
10458
- * form.DONE (saved), form.RESET (values?); internal: form.RESULT (an async schema's result),
10957
+ * form.DONE (saved), form.RESET (values?), form.PROGRESS (PLAN-11 F-2: a progress step of the
10958
+ * submit request, `progress: 'form.PROGRESS'`: kept as `progress` while submitting, cleared by
10959
+ * DONE / ERRORS / RESET); internal: form.RESULT (an async schema's result),
10459
10960
  * form.CHECKED_<name> (a check's reply; a failed check passes: the server checks on submit),
10460
10961
  * form.VALIDATE (the first validation, when the host starts: G-375; `valid` is false until the
10461
10962
  * schema answered; with resetOnShow also each time the form element appears, D239).
@@ -10479,8 +10980,14 @@ const focusInvalid = (names, within) => {
10479
10980
  * fields. The host checks (SYG234 submit, SYG235 check names, SYG236 row ids)
10480
10981
  * run when the host is created.
10481
10982
  */
10482
- const dev$7 = (...a) => globalThis.__SYGNAL_DIAGNOSTICS__?.form?.(...a);
10983
+ // PLAN-11 F-3: a submit's data goes as multipart when a top-level value is a file (or files)
10984
+ const hasFile = (d) => !!d && typeof d == 'object' && Object.values(d).some(x => isFile(x) || Array.isArray(x) && x.some(isFile));
10985
+ const dev$6 = (...a) => globalThis.__SYGNAL_DIAGNOSTICS__?.form?.(...a);
10483
10986
  const keys = (o) => Object.keys(o).filter(k => o[k]);
10987
+ // PLAN-11 F-2: values compared as JSON, a File by identity-ish fields (JSON gives every File {})
10988
+ const fileIds = new WeakMap();
10989
+ let fileSeq = 0;
10990
+ const json = (v) => JSON.stringify(v, (_k, x) => isFile(x) ? `\0file:${fileIds.get(x) ?? (fileIds.set(x, ++fileSeq), fileSeq)}` : x);
10484
10991
  const drop = (o, p) => {
10485
10992
  const r = {};
10486
10993
  for (const k in o)
@@ -10489,8 +10996,8 @@ const drop = (o, p) => {
10489
10996
  return r;
10490
10997
  };
10491
10998
  const form = (schema, o = {}) => {
10492
- isStandardSchema(schema) || dev$7(231, schema);
10493
- const { values: vo = {}, submit, check: checks = {}, show = 'blur', http = 'HTTP', form: sel = 'form', resetOnShow: again } = o;
10999
+ isStandardSchema(schema) || dev$6(231, schema);
11000
+ const { values: vo = {}, submit, request: rq, check: checks = {}, show = 'blur', http = 'HTTP', form: sel = 'form', resetOnShow: again } = o;
10494
11001
  // D239: `values` can be a function of the host's state, called when the form starts (and each
10495
11002
  // show, resetOnShow). Before that (the host's first render, before its first action) the
10496
11003
  // fields come from calling it with a blank state: every read gives '' in the result
@@ -10522,7 +11029,7 @@ const form = (schema, o = {}) => {
10522
11029
  // from a named button)
10523
11030
  const known = (s, n, quiet) => {
10524
11031
  const k = n && hasField(s.values, n);
10525
- n && !k && !quiet && dev$7(230, n, s.values);
11032
+ n && !k && !quiet && dev$6(230, n, s.values);
10526
11033
  return k;
10527
11034
  };
10528
11035
  // new values: errors now (sync schema), or after the schema's Promise (RESULT)
@@ -10537,7 +11044,7 @@ const form = (schema, o = {}) => {
10537
11044
  });
10538
11045
  const ask = (s, f, k, queued) => {
10539
11046
  const x = getField(s.values, f), q = checks[f].request(x), ok = `${k}.CHECKED_${f}`;
10540
- dev$7(235, f, q);
11047
+ dev$6(235, f, q);
10541
11048
  return { s: { ...s, queued, pending: { ...s.pending, [f]: x } }, req: { ...q, ok, error: ok, latest: true } };
10542
11049
  };
10543
11050
  // a submit with s's errors: blocked (focus the first), a check to run or to wait for, or sent
@@ -10548,10 +11055,11 @@ const form = (schema, o = {}) => {
10548
11055
  : Object.keys(s.pending).length ? { s: { ...s, queued: true } }
10549
11056
  : { s: req ? { ...s, queued: false, submitting: true } : done({ ...s, queued: false }), send: 1 };
10550
11057
  };
10551
- const base = (vals) => ({ initial: vals, values: vals, touched: {}, server: {}, remote: {}, pending: {},
10552
- submitting: false, submitted: false, submitCount: 0, queued: false, errors: {}, validated: false });
10553
- const fresh = (vals) => edit(base(vals), vals);
10554
- const done = (s) => ({ ...s, submitting: false, submitted: true, initial: s.values, server: {}, touched: {} });
11058
+ const base = (vals, resets = 0) => ({ initial: vals, values: vals, touched: {}, server: {}, remote: {}, pending: {},
11059
+ submitting: false, submitted: false, submitCount: 0, queued: false, errors: {}, validated: false, progress: null, resets });
11060
+ // a start over (RESET, resetOnShow): `resets` counts them, so a file input keyed by it is made anew
11061
+ const fresh = (vals, s) => edit(base(vals, s ? (s.resets || 0) + 1 : 0), vals);
11062
+ const done = (s) => ({ ...s, submitting: false, submitted: true, initial: s.values, server: {}, touched: {}, progress: null });
10555
11063
  const group = (cur, d) => {
10556
11064
  if (!Array.isArray(cur) || !('item' in d))
10557
11065
  return d.value;
@@ -10560,10 +11068,10 @@ const form = (schema, o = {}) => {
10560
11068
  };
10561
11069
  // fields changed as typed ones ([name, value] pairs; unknown names skipped, SYG230): their
10562
11070
  // server errors, check results and running checks go, `touched` as show 'input' says; one validation
10563
- const put = (s, d) => {
11071
+ const put = (s, d, quiet) => {
10564
11072
  let vals = s.values, server = drop(s.server, ''), remote = s.remote, pending = s.pending, touched = s.touched;
10565
11073
  for (const [n, y] of d)
10566
- if (known(s, n)) {
11074
+ if (known(s, n, quiet)) {
10567
11075
  vals = setField(vals, n, y), server = drop(server, n), remote = drop(remote, n), pending = drop(pending, n);
10568
11076
  if (show == 'input')
10569
11077
  touched = { ...touched, [n]: true };
@@ -10573,7 +11081,9 @@ const form = (schema, o = {}) => {
10573
11081
  const steps = {
10574
11082
  // G-376: a checkbox (`item`: its value) on an array field is one of a group: checked adds
10575
11083
  // its value, unchecked removes it
10576
- CHANGE: (s, d) => put(s, d?.name ? [[d.name, group(getField(s.values, d.name), d)]] : []),
11084
+ // PLAN-11 F-2: a file input whose name isn't in values is left alone, quietly (an app that reads
11085
+ // its files in its own intent, as before 6.7)
11086
+ CHANGE: (s, d) => put(s, d?.name ? [[d.name, group(getField(s.values, d.name), d)]] : [], d?.file),
10577
11087
  BLUR: (s, n, k) => {
10578
11088
  if (!known(s, n, 1))
10579
11089
  return null;
@@ -10582,7 +11092,7 @@ const form = (schema, o = {}) => {
10582
11092
  },
10583
11093
  SUBMIT: (s, _, k) => {
10584
11094
  if (s.submitting || s.queued)
10585
- return void dev$7(232, s);
11095
+ return void dev$6(232, s);
10586
11096
  const n = { ...s, submitCount: s.submitCount + 1 }, r = v(s.values);
10587
11097
  // G-371: a submit waiting for an async schema is queued (a second one is dropped)
10588
11098
  return r.then ? { s: { ...n, validating: true, queued: true }, wait: { values: s.values, submit: 1 } } : attempt({ ...n, errors: r.errors }, k);
@@ -10606,16 +11116,20 @@ const form = (schema, o = {}) => {
10606
11116
  SET: (s, d) => put(s, Object.entries(d?.values || {})),
10607
11117
  ERRORS: (s, d) => {
10608
11118
  const e = replyErrors(d, s.values);
10609
- return { s: { ...s, server: e, submitting: false, queued: false }, focus: focusInvalid(e, sel) };
11119
+ return { s: { ...s, server: e, submitting: false, queued: false, progress: null }, focus: focusInvalid(e, sel) };
10610
11120
  },
10611
11121
  DONE: (s) => ({ s: done(s) }),
10612
- RESET: (s, d) => fresh(d && typeof d == 'object' ? d : s.initial),
11122
+ // PLAN-11 F-2: a progress step of the submit's request (`progress: 'form.PROGRESS'`), while it is pending
11123
+ PROGRESS: (s, d) => s.submitting && d && typeof d == 'object'
11124
+ ? { s: { ...s, progress: { phase: d.phase, loaded: d.loaded, total: d.total ?? null, fraction: d.fraction ?? null } } } : null,
11125
+ // PLAN-11: only a plain object is new values (a click event from `DOM.click('.reset')` is not)
11126
+ RESET: (s, d) => fresh(d && typeof d == 'object' && [Object.prototype, null].includes(Object.getPrototypeOf(d)) ? d : s.initial, s),
10613
11127
  // G-375: the first validation, when the host starts (form() doesn't validate at module load).
10614
11128
  // G-575: a slice kept in parent state from a host that unmounted mid-submit starts unstuck.
10615
11129
  // D239: with resetOnShow (also each time the form element appears again), or a `values`
10616
11130
  // function not called yet, the slice starts over from the start values (h: the host state)
10617
- VALIDATE: (s, _, _k, h) => again || fn && s.values === values ? fresh(fn ? vo(h) : vo)
10618
- : edit({ ...s, submitting: false, queued: false, pending: {} }, s.values),
11131
+ VALIDATE: (s, _, _k, h) => again || fn && s.values === values ? fresh(fn ? vo(h) : vo, again && s)
11132
+ : edit({ ...s, submitting: false, queued: false, pending: {}, progress: null }, s.values),
10619
11133
  };
10620
11134
  // a check's reply (ok and error: an error reply carries an Error and passes)
10621
11135
  for (const f of checked)
@@ -10625,6 +11139,14 @@ const form = (schema, o = {}) => {
10625
11139
  const n = { ...s, pending: drop(s.pending, f), remote: { ...s.remote, [f]: d?.error instanceof Error ? '' : checks[f].error?.(d) || '' } };
10626
11140
  return s.queued ? attempt(n, k) : { s: n };
10627
11141
  };
11142
+ // PLAN-11 F-3: the `request` option's request for a valid submit (data: the schema's output)
11143
+ const send = (vals, h, k) => {
11144
+ const data = v(vals).value, q = typeof rq == 'function' ? rq(data, h) : rq;
11145
+ if (!q || q === ABORT)
11146
+ return ABORT;
11147
+ const own = 'json' in q || 'multipart' in q || 'body' in q;
11148
+ return { ...(!own && (hasFile(data) ? { multipart: data } : { json: data })), progress: `${k}.PROGRESS`, ...q, ok: `${k}.DONE`, error: `${k}.ERRORS` };
11149
+ };
10628
11150
  const model = {};
10629
11151
  for (const a in steps) {
10630
11152
  let last = [];
@@ -10634,19 +11156,28 @@ const form = (schema, o = {}) => {
10634
11156
  // VALIDATE: on the host's state (HOST), for a `values` function; the slice's own steps on it
10635
11157
  ...a == 'VALIDATE' ? { HOST: (h, d, _n, _p, _o, k) => { const x = out(h[k], d, k, h); return x ? { ...h, [k]: x.s } : h; } }
10636
11158
  : { STATE: (s, d, _n, _p, _o, k) => out(s, d, k)?.s || ABORT },
10637
- EFFECT: (s, d, next, _p, _o, k) => {
11159
+ // outputs share STATE's transition: computed from the slice before the action
11160
+ EFFECT: (s, d, next, p, _o, k) => {
11161
+ s = was(s, p, k);
10638
11162
  const x = out(s, d, k), w = x?.wait;
10639
11163
  if (x?.send) {
10640
11164
  const r = v(x.s.values).value;
10641
- dev$7(233, s.values, r);
11165
+ dev$6(233, s.values, r);
10642
11166
  // G-577: a submit named after one of the form's own actions goes to that one
10643
- submit in steps && dev$7(234, submit, k, b);
10644
- next(submit, r, 0);
11167
+ submit in steps && dev$6(234, submit, k, b);
11168
+ // PLAN-11 F-3: with `request`, `submit` is optional (dispatched for state when given)
11169
+ if (submit !== undefined || !rq)
11170
+ next(submit, r, 0);
10645
11171
  }
10646
11172
  w && Promise.resolve(v(w.values)).then(() => next('RESULT', w, 0));
10647
11173
  },
10648
- ELEMENT: (s, d, _n, _p, _o, k) => out(s, d, k)?.focus || ABORT,
10649
- ...(checked.length && { [http]: (s, d, _n, _p, _o, k) => out(s, d, k)?.req || ABORT }),
11174
+ ELEMENT: (s, d, _n, p, _o, k) => out(was(s, p, k), d, k)?.focus || ABORT,
11175
+ // the checks' requests, and (PLAN-11 F-3) the `request` option's for a valid submit; the
11176
+ // step from the slice before the action (7.0: outputs get the updated one), the host state as committed
11177
+ ...((checked.length || rq) && { [http]: (s, d, _n, p, _o, k) => {
11178
+ const x = out(was(s, p, k), d, k);
11179
+ return x?.req || (x?.send && rq ? send(x.s.values, p?.state, k) : ABORT);
11180
+ } }),
10650
11181
  };
10651
11182
  }
10652
11183
  const b = defineBehavior({
@@ -10662,9 +11193,11 @@ const form = (schema, o = {}) => {
10662
11193
  VALIDATE: again ? xs$1.merge(xs$1.of(0), f.elements().filter((e) => { const r = e.length && !on; on = e.length; return r; })) : xs$1.of(0),
10663
11194
  // G-376: a checkbox (native, or a hyphenated tag with a boolean `checked`: a
10664
11195
  // form-associated custom checkbox / switch) gives `checked` and its value as `item`; a
10665
- // <select multiple> the selected values; type=file is left alone; others give `value`
10666
- CHANGE: f.events('input').filter(({ target: t }) => t.type != 'file').map(({ target: t }) => t.type == 'checkbox' || /-/.test(t.tagName) && typeof t.checked == 'boolean' ? { name: t.name, value: t.checked, item: t.value }
10667
- : { name: t.name, value: t.type == 'select-multiple' ? [...t.selectedOptions].map((o) => o.value) : t.value }),
11196
+ // <select multiple> the selected values; PLAN-11 F-2: a file input its File (`multiple`:
11197
+ // an array of them; none: null); others give `value`
11198
+ CHANGE: f.events('input').map(({ target: t }) => t.type == 'file' ? { name: t.name, value: t.multiple || t.getAttribute?.('multiple') != null ? [...(t.files || [])] : t.files?.[0] ?? null, file: true }
11199
+ : t.type == 'checkbox' || /-/.test(t.tagName) && typeof t.checked == 'boolean' ? { name: t.name, value: t.checked, item: t.value }
11200
+ : { name: t.name, value: t.type == 'select-multiple' ? [...t.selectedOptions].map((o) => o.value) : t.value }),
10668
11201
  BLUR: f.events('focusout').map((e) => e.target.name),
10669
11202
  SUBMIT: f.events('submit', { preventDefault: true }),
10670
11203
  };
@@ -10682,7 +11215,7 @@ const form = (schema, o = {}) => {
10682
11215
  },
10683
11216
  // G-382: an async schema re-validating keeps the last answer's validity (no flicker)
10684
11217
  valid: (s) => s.validated && !keys(s.errors).length && !keys(s.remote).length,
10685
- dirty: (s) => JSON.stringify(s.values) != JSON.stringify(s.initial),
11218
+ dirty: (s) => json(s.values) != json(s.initial),
10686
11219
  error: (s) => s.server[''] || (s.submitCount && s.errors['']) || '',
10687
11220
  },
10688
11221
  })({ ...o, values }), merge = b.merge;
@@ -10690,7 +11223,7 @@ const form = (schema, o = {}) => {
10690
11223
  o.tool?.$?.(b, v);
10691
11224
  b.merge = (c, k) => {
10692
11225
  const e = c.model?.[submit];
10693
- req = !!e && typeof e == 'object' && http in e;
11226
+ req = !!rq || !!e && typeof e == 'object' && http in e;
10694
11227
  merge(c, k);
10695
11228
  };
10696
11229
  return b;
@@ -10839,7 +11372,7 @@ const selection = (options = {}) => {
10839
11372
  * interaction, SYG147 a `from` key that isn't an array in the host state, SYG435 (3-H) a list
10840
11373
  * shown in another order than its array's (a Collection's sort / filter).
10841
11374
  */
10842
- const dev$6 = (...a) => globalThis.__SYGNAL_DIAGNOSTICS__?.sortable?.(...a);
11375
+ const dev$5 = (...a) => globalThis.__SYGNAL_DIAGNOSTICS__?.sortable?.(...a);
10843
11376
  const LIFT = /^( |Enter|Spacebar)$/;
10844
11377
  const KEYS = /^(Arrow(Up|Down|Left|Right)|Home|End|Escape|Tab)$/;
10845
11378
  const S$1 = String;
@@ -10953,7 +11486,7 @@ const sortable = (options = {}) => {
10953
11486
  } } } } };
10954
11487
  };
10955
11488
  // dev: SYG435, the list shown in another order than the array's (G-449)
10956
- const check = (st, seen, k) => seen && dev$6(435, seen, st, lists, idField, k);
11489
+ const check = (st, seen, k) => seen && dev$5(435, seen, st, lists, idField, k);
10957
11490
  // a press or key handler: drag state this instance didn't start (its token `n` isn't the
10958
11491
  // event's: restored, synced) is dropped first (G-452)
10959
11492
  const live = (h) => (st, d, next, _p, _o, k) => {
@@ -10994,7 +11527,7 @@ const sortable = (options = {}) => {
10994
11527
  const gripOf = (e, self) => {
10995
11528
  const t = e.target, r = e.ownerTarget || e.currentTarget;
10996
11529
  if (!checked)
10997
- checked = dev$6(146, r, item, handle, attr);
11530
+ checked = dev$5(146, r, item, handle, attr);
10998
11531
  const g = !claimed.has(e) && t?.closest?.(grip);
10999
11532
  if (!g || self && g !== t)
11000
11533
  return null;
@@ -11008,14 +11541,14 @@ const sortable = (options = {}) => {
11008
11541
  return null;
11009
11542
  const id = idOf(it);
11010
11543
  if (id == null)
11011
- dev$6(145, it, item, attr);
11544
+ dev$5(145, it, item, attr);
11012
11545
  return id;
11013
11546
  };
11014
11547
  const keyed = (keys, kb) => (e) => keys.test(e.key) && (kb || e.key != 'Tab') && gripOf(e, true) != null;
11015
11548
  const lift = keyed(LIFT), step = keyed(KEYS, 1);
11016
11549
  // dev (SYG435): this host's own items' ids in the order they're shown (computed by the
11017
11550
  // diagnostics entry only)
11018
- const shown = (e) => dev$6(435, () => {
11551
+ const shown = (e) => dev$5(435, () => {
11019
11552
  const r = e.ownerTarget || e.currentTarget;
11020
11553
  return [[...r.querySelectorAll(item)].filter((it) => own(it, r)), r];
11021
11554
  }, key, lists, idOf);
@@ -11059,7 +11592,7 @@ const sortable = (options = {}) => {
11059
11592
  // again by HMR, a second host of the slice) is reset, as at END
11060
11593
  INIT: { HOST: (st, _d, _n, _p, _o, k) => {
11061
11594
  for (const l of lists)
11062
- Array.isArray(st?.[l]) || dev$6(147, l, st, k);
11595
+ Array.isArray(st?.[l]) || dev$5(147, l, st, k);
11063
11596
  return st[k].press || st[k].dragging ? back(st, k) : ABORT;
11064
11597
  } },
11065
11598
  // unmounted mid-drag: the drag is cancelled (no DROPPED; a keyboard drag's item goes back
@@ -11471,14 +12004,14 @@ const undo = (options) => {
11471
12004
  * too), once per kind (restore / save / clear; a save that works again re-arms it); the app
11472
12005
  * continues on initialState / unsaved, and a failed save is retried at the next change.
11473
12006
  */
11474
- const g$3 = globalThis;
12007
+ const g$2 = globalThis;
11475
12008
  const setupPersist = (c, o) => {
11476
12009
  const { key, pick, omit, version = 1, migrate, sync, debounceMs = 100 } = o, plain = o.format == 'plain', src = c.sources, m = src.__m;
11477
12010
  const env = src.__storage, calc = c.calculated || {};
11478
12011
  // hydrating: the option, else the integration's signal (__hydrate: Astro, Vike), else whether
11479
12012
  // run()'s mount point starts with renderToString's markup (its root element is marked; a
11480
12013
  // client-only app's loading placeholder isn't server markup)
11481
- const hydrate = o.hydrate ?? src.__hydrate ?? !!(typeof m == 'string' ? g$3.document?.querySelector(m) : m)?.firstElementChild?.hasAttribute('data-sygnal-ssr');
12014
+ const hydrate = o.hydrate ?? src.__hydrate ?? !!(typeof m == 'string' ? g$2.document?.querySelector(m) : m)?.firstElementChild?.hasAttribute('data-sygnal-ssr');
11482
12015
  let S, t, last, raw, skip, off;
11483
12016
  // (the docs link the message ends with explains the causes and fixes). Once per kind (restore,
11484
12017
  // save, clear): a storage that throws on every write must not warn on every change; a save that
@@ -11551,7 +12084,7 @@ const setupPersist = (c, o) => {
11551
12084
  }
11552
12085
  c.model = model;
11553
12086
  try {
11554
- S = typeof o.storage == 'object' ? o.storage : (env || g$3)[(o.storage || 'local') + (env ? '' : 'Storage')];
12087
+ S = typeof o.storage == 'object' ? o.storage : (env || g$2)[(o.storage || 'local') + (env ? '' : 'Storage')];
11555
12088
  }
11556
12089
  catch (_) { }
11557
12090
  if (!S)
@@ -11588,14 +12121,14 @@ const setupPersist = (c, o) => {
11588
12121
  send(read(raw = v)); };
11589
12122
  const onStorage = (e) => (!e.storageArea || e.storageArea == S) && onSync(e.key, e.newValue);
11590
12123
  if (sync)
11591
- off = S.subscribe ? S.subscribe(onSync) : (g$3.addEventListener?.('storage', onStorage), () => g$3.removeEventListener?.('storage', onStorage));
11592
- g$3.addEventListener?.('pagehide', flush);
12124
+ off = S.subscribe ? S.subscribe(onSync) : (g$2.addEventListener?.('storage', onStorage), () => g$2.removeEventListener?.('storage', onStorage));
12125
+ g$2.addEventListener?.('pagehide', flush);
11593
12126
  env?.f.add(flush);
11594
12127
  c._dispose$.addListener({ next: () => {
11595
12128
  flush();
11596
12129
  state$.removeListener(L);
11597
12130
  off?.();
11598
- g$3.removeEventListener?.('pagehide', flush);
12131
+ g$2.removeEventListener?.('pagehide', flush);
11599
12132
  env?.f.delete(flush);
11600
12133
  } });
11601
12134
  };
@@ -12827,7 +13360,7 @@ const snake = (s) => s.replace(/([a-z0-9])([A-Z])/g, '$1_$2').replace(/\W+/g, '_
12827
13360
  const keyOf$1 = (iv) => iv.state?.id;
12828
13361
  const done = new WeakMap();
12829
13362
  /** a dev diagnostic, once per (declaration, what); 'error' mode rethrows outside the caller */
12830
- const dev$5 = (code, decl, what, component, message) => {
13363
+ const dev$4 = (code, decl, what, component, message) => {
12831
13364
  let s = done.get(decl);
12832
13365
  if (!s)
12833
13366
  done.set(decl, s = new Set());
@@ -13036,7 +13569,7 @@ function agentTools(target, options = {}) {
13036
13569
  v = d.read(s);
13037
13570
  }
13038
13571
  catch (e) {
13039
- dev$5('SYG241', d, 'read', iv.name, `agent.read threw: ${e?.message ?? e}`);
13572
+ dev$4('SYG241', d, 'read', iv.name, `agent.read threw: ${e?.message ?? e}`);
13040
13573
  }
13041
13574
  reads.set(iv, { s, v });
13042
13575
  return v;
@@ -13055,7 +13588,7 @@ function agentTools(target, options = {}) {
13055
13588
  v = !!w(s);
13056
13589
  }
13057
13590
  catch (e) {
13058
- dev$5('SYG241', declOf(iv), 'when' + action, iv.name, `agent.actions.${action}.when threw: ${e?.message ?? e}`);
13591
+ dev$4('SYG241', declOf(iv), 'when' + action, iv.name, `agent.actions.${action}.when threw: ${e?.message ?? e}`);
13059
13592
  }
13060
13593
  m.set(action, { s, v });
13061
13594
  return v;
@@ -13066,7 +13599,7 @@ function agentTools(target, options = {}) {
13066
13599
  return d.label ? String(d.label(iv.state)) : undefined;
13067
13600
  }
13068
13601
  catch (e) {
13069
- dev$5('SYG241', d, 'label', iv.name, `agent.label threw: ${e?.message ?? e}`);
13602
+ dev$4('SYG241', d, 'label', iv.name, `agent.label threw: ${e?.message ?? e}`);
13070
13603
  }
13071
13604
  };
13072
13605
  function walk(iv, out) {
@@ -13101,7 +13634,7 @@ function agentTools(target, options = {}) {
13101
13634
  else if (e.decl === d && e.item && item)
13102
13635
  e.ivs.push(iv);
13103
13636
  else
13104
- dev$5('SYG440', d, name, iv.name, `two agent declarations are named '${name}'; only the first is offered`);
13637
+ dev$4('SYG440', d, name, iv.name, `two agent declarations are named '${name}'; only the first is offered`);
13105
13638
  }
13106
13639
  for (const e of by.values())
13107
13640
  if (e.item) {
@@ -13110,9 +13643,9 @@ function agentTools(target, options = {}) {
13110
13643
  const k = keyOf$1(iv);
13111
13644
  // an item without an id is keyed by its index (the core's `_i<index>` uid part): unstable
13112
13645
  if (k == null || /-_i\d+$/.test(iv.uid))
13113
- dev$5('SYG441', e.decl, 'nokey', iv.name, `items of '${e.name}' have no id: agents address them by index`);
13646
+ dev$4('SYG441', e.decl, 'nokey', iv.name, `items of '${e.name}' have no id: agents address them by index`);
13114
13647
  else if (ks.has(k + ''))
13115
- dev$5('SYG441', e.decl, 'dup', iv.name, `two items of '${e.name}' have the id ${JSON.stringify(k)}; calls reach the first`);
13648
+ dev$4('SYG441', e.decl, 'dup', iv.name, `two items of '${e.name}' have the id ${JSON.stringify(k)}; calls reach the first`);
13116
13649
  else
13117
13650
  ks.add(k + '');
13118
13651
  }
@@ -13138,11 +13671,11 @@ function agentTools(target, options = {}) {
13138
13671
  for (const [action, a, isQuery] of defs) {
13139
13672
  const name = e.name + '_' + snake(action), comp = e.ivs[0].name, where = `agent.${isQuery ? 'queries' : 'actions'}.${action}`;
13140
13673
  if (isQuery && recs.has(name)) {
13141
- dev$5('SYG247', d, action, comp, `${where} has the tool name ${name} of an action; the query is not offered (rename it)`);
13674
+ dev$4('SYG247', d, action, comp, `${where} has the tool name ${name} of an action; the query is not offered (rename it)`);
13142
13675
  continue;
13143
13676
  }
13144
13677
  if (isQuery && typeof a?.run != 'function') {
13145
- dev$5('SYG246', d, action, comp, `${where}.run is not a function; the query is not offered`);
13678
+ dev$4('SYG246', d, action, comp, `${where}.run is not a function; the query is not offered`);
13146
13679
  continue;
13147
13680
  }
13148
13681
  const c = a.input ? toJsonSchema(a.input) : undefined;
@@ -13157,11 +13690,11 @@ function agentTools(target, options = {}) {
13157
13690
  if (c?.error) {
13158
13691
  tool.error = `SYG240 ${c.error}`;
13159
13692
  rec.off = 1;
13160
- dev$5('SYG240', d, action, comp, `${where}.input: ${c.error}; the tool is not offered`);
13693
+ dev$4('SYG240', d, action, comp, `${where}.input: ${c.error}; the tool is not offered`);
13161
13694
  continue;
13162
13695
  }
13163
13696
  if (c?.lossy)
13164
- dev$5('SYG243', d, action, comp, `${where}.input: the model doesn't see ${c.lossy}`);
13697
+ dev$4('SYG243', d, action, comp, `${where}.input: the model doesn't see ${c.lossy}`);
13165
13698
  const live = e.ivs.filter(iv => whenOk(iv, action, a.when));
13166
13699
  if (!live.length) {
13167
13700
  rec.off = 1;
@@ -13333,7 +13866,7 @@ function agentTools(target, options = {}) {
13333
13866
  return;
13334
13867
  const res = iv.def.view.resources;
13335
13868
  if (!res || !(u in res)) {
13336
- dev$5('SYG248', declOf(iv), 'until' + action, iv.name, `agent.actions.${action}.until names '${u}', which is not one of ${iv.name}'s resources${res ? ` (${Object.keys(res).join(', ')})` : ''}; the call answers at once`);
13869
+ dev$4('SYG248', declOf(iv), 'until' + action, iv.name, `agent.actions.${action}.until names '${u}', which is not one of ${iv.name}'s resources${res ? ` (${Object.keys(res).join(', ')})` : ''}; the call answers at once`);
13337
13870
  return;
13338
13871
  }
13339
13872
  return (s) => { const r = s?.[u]; return !r || (r.status != 'loading' && !r.refreshing); };
@@ -13351,7 +13884,7 @@ function agentTools(target, options = {}) {
13351
13884
  return !!test(iv.state);
13352
13885
  }
13353
13886
  catch (err) {
13354
- dev$5('SYG241', declOf(iv), 'until' + action, iv.name, `agent.actions.${action}.until threw: ${err?.message ?? err}`);
13887
+ dev$4('SYG241', declOf(iv), 'until' + action, iv.name, `agent.actions.${action}.until threw: ${err?.message ?? err}`);
13355
13888
  return true;
13356
13889
  }
13357
13890
  };
@@ -13377,11 +13910,11 @@ function agentTools(target, options = {}) {
13377
13910
  }
13378
13911
  catch (err) {
13379
13912
  const m = err?.message ?? err;
13380
- dev$5('SYG241', declOf(iv), 'query' + query, iv.name, `agent.queries.${query}.run threw: ${m}`);
13913
+ dev$4('SYG241', declOf(iv), 'query' + query, iv.name, `agent.queries.${query}.run threw: ${m}`);
13381
13914
  return { ok: false, error: `${name} failed: ${m}` };
13382
13915
  }
13383
13916
  if (result && typeof result.then == 'function') {
13384
- dev$5('SYG246', declOf(iv), 'promise' + query, iv.name, `agent.queries.${query}.run returned a promise: a query is a pure, synchronous function of the state`);
13917
+ dev$4('SYG246', declOf(iv), 'promise' + query, iv.name, `agent.queries.${query}.run returned a promise: a query is a pure, synchronous function of the state`);
13385
13918
  return { ok: false, error: `${name} is not available: it returned a promise` };
13386
13919
  }
13387
13920
  return { ok: true, result };
@@ -13742,28 +14275,28 @@ function withToolResults(message, results) {
13742
14275
  const SCOPE = '__sygnalScope';
13743
14276
  const tag$1 = (v, scope) => v && typeof v == 'object'
13744
14277
  ? Object.defineProperty(keepSender(v, { ...v }), SCOPE, { value: [scope, ...(v[SCOPE] || [])] }) : v;
13745
- const g$2 = globalThis;
13746
- const now = () => g$2.performance?.now?.() ?? Date.now();
14278
+ const g$1 = globalThis;
14279
+ const now = () => g$1.performance?.now?.() ?? Date.now();
13747
14280
  // internal (D254): the 120 Hz floor and the hidden-tab fallback
13748
14281
  const MIN_GAP = 15, FALLBACK = 100;
13749
14282
  /** rAF raced with a timer (a hidden tab never runs rAF), else a 16 ms timer; `f` gets the frame's time */
13750
14283
  const frame = (f) => {
13751
- if (typeof g$2.requestAnimationFrame != 'function')
14284
+ if (typeof g$1.requestAnimationFrame != 'function')
13752
14285
  return void setTimeout(() => f(now()), 16);
13753
14286
  let done = 0, a, t;
13754
14287
  const go = (ts) => {
13755
14288
  if (done++)
13756
14289
  return;
13757
- g$2.cancelAnimationFrame?.(a);
14290
+ g$1.cancelAnimationFrame?.(a);
13758
14291
  clearTimeout(t);
13759
14292
  f(typeof ts == 'number' ? ts : now());
13760
14293
  };
13761
- a = g$2.requestAnimationFrame(go);
14294
+ a = g$1.requestAnimationFrame(go);
13762
14295
  t = setTimeout(go, FALLBACK);
13763
14296
  };
13764
14297
  // dev-only diagnostics (SYG673, SYG677): the dev entry formats them (checks/chat.ts), so their
13765
14298
  // text costs apps nothing; without it they're silent
13766
- const dev$4 = (...a) => g$2.__SYGNAL_DIAGNOSTICS__?.chat?.(...a);
14299
+ const dev$3 = (...a) => g$1.__SYGNAL_DIAGNOSTICS__?.chat?.(...a);
13767
14300
  const FIX = "Send { messages, ok: 'DONE', error: 'FAILED' }";
13768
14301
  /** why a request can't be sent ('' when it can) */
13769
14302
  const problem = (r) => !Array.isArray(r.messages) ? 'no messages array'
@@ -13785,7 +14318,7 @@ function makeChatDriver(options = {}) {
13785
14318
  const { replies, reply } = makeReplies(sender => abortWhere(r => r.sender === sender));
13786
14319
  // SYG673 (dev): `why` is 'type' | 'delta' | 'name' | 'id'; the first five per driver
13787
14320
  const malformed = (req, ev, why) => { if (warned++ < 5)
13788
- dev$4('SYG673', req, ev, why); };
14321
+ dev$3('SYG673', req, ev, why); };
13789
14322
  const start = async (req, sender) => {
13790
14323
  const key = req.key ?? req.ok ?? req.error ?? 'default';
13791
14324
  if (req.latest !== false)
@@ -13978,7 +14511,7 @@ function makeChatDriver(options = {}) {
13978
14511
  return;
13979
14512
  const sender = senderOf(req);
13980
14513
  if (sender === undefined)
13981
- return void dev$4('SYG677', req);
14514
+ return void dev$3('SYG677', req);
13982
14515
  const { abort } = req;
13983
14516
  if (abort !== undefined && abort !== false) {
13984
14517
  // D469 (PLAN-9 H-2): a list of keys stops each (STOP: the reply and a card being composed)
@@ -15034,6 +15567,17 @@ const validMs = (v) => typeof v == 'number' && Number.isFinite(v) && v >= 0 && v
15034
15567
  // too, so its time is the clock's (Date may be left real by `toFake`), and its waits drive it.
15035
15568
  const fakeClock = () => setTimeout.clock;
15036
15569
  const clockNow = () => { const c = fakeClock(); return c ? c.now : Date.now(); };
15570
+ /** PLAN-11 Q-3: ms until the next pending fake timer (0: none, or real timers) */
15571
+ const nextFakeTimer = () => {
15572
+ const c = fakeClock();
15573
+ let due = Infinity;
15574
+ try {
15575
+ Object.values(c?.timers || {}).forEach((t) => { if (t && t.callAt > c.now && t.callAt < due)
15576
+ due = t.callAt; });
15577
+ }
15578
+ catch (_) { }
15579
+ return due < Infinity ? due - c.now : 0;
15580
+ };
15037
15581
  /**
15038
15582
  * Under fake timers, advance the clock timer by timer (nextAsync flushes promises around each
15039
15583
  * one) until `p` settles. Every pending wait has a timer (its timeout, a quiet-window tick), so
@@ -15224,6 +15768,7 @@ const headFake = () => {
15224
15768
  };
15225
15769
  let savedConfig;
15226
15770
  let savedStrict;
15771
+ let savedAudit;
15227
15772
  /*
15228
15773
  * PLAN-4 GS-2, mock DOM: could the element a command targets have this method? The core runs any
15229
15774
  * method of the element; the mock DOM has no elements, so: the documented commands, common
@@ -15334,7 +15879,7 @@ function fakeElementMethods(W) {
15334
15879
  Object.defineProperty(proto, name, own); });
15335
15880
  }
15336
15881
  function renderComponent(componentDef, options = {}) {
15337
- const { initialState, mockConfig = {}, drivers = {}, diagnostics, strict, dom = 'mock', autoConnect = true, socketSink = 'WS', resourceSink = 'HTTP', llmSink = 'LLM', http: httpOptions, context: ancestors } = options;
15882
+ const { initialState, mockConfig = {}, drivers = {}, diagnostics, strict, timingAudit, dom = 'mock', autoConnect = true, socketSink = 'WS', resourceSink = 'HTTP', llmSink = 'LLM', http: httpOptions, context: ancestors } = options;
15338
15883
  const { intent, model = {} } = componentDef;
15339
15884
  // E4: real DOM mode
15340
15885
  const real = dom == 'real';
@@ -15382,6 +15927,7 @@ function renderComponent(componentDef, options = {}) {
15382
15927
  if (!active++) {
15383
15928
  savedConfig = _getDiagnosticsConfig();
15384
15929
  savedStrict = core.strict;
15930
+ savedAudit = core.timingAudit;
15385
15931
  // G-051: forget the checks' report dedupe (once()), so a finding from an earlier test is
15386
15932
  // reported again. Only that set: resetChecks() would also drop inspect()'s records of
15387
15933
  // other live instances / apps.
@@ -15390,6 +15936,9 @@ function renderComponent(componentDef, options = {}) {
15390
15936
  configureDiagnostics({ mode: diagnostics || (prevMode == 'off' ? 'collect' : prevMode) });
15391
15937
  if (strict !== undefined)
15392
15938
  core.strict = strict;
15939
+ // 7.0 timing audit (SYG257, 'sygnal/diagnostics'): same bridge flag as configureTimingAudit()
15940
+ if (timingAudit !== undefined)
15941
+ core.timingAudit = timingAudit;
15393
15942
  // PLAN-4 GS-2: element commands run on the real DOM (jsdom gets the missing methods), and their
15394
15943
  // SYG640/SYG641 are reported without the dev entry too
15395
15944
  if (real && !domFakes)
@@ -15580,6 +16129,7 @@ function renderComponent(componentDef, options = {}) {
15580
16129
  if (!--active) {
15581
16130
  configureDiagnostics(savedConfig);
15582
16131
  core.strict = savedStrict;
16132
+ core.timingAudit = savedAudit;
15583
16133
  domFakes?.();
15584
16134
  domFakes = undefined;
15585
16135
  }
@@ -15625,14 +16175,22 @@ function renderComponent(componentDef, options = {}) {
15625
16175
  const lin$ = xs$1.create();
15626
16176
  llm = { in$: lin$, src: makeChatDriver({ transport: tr, _frame: (f) => { frames.push(f); } })(lin$) };
15627
16177
  }
16178
+ let memFetch;
15628
16179
  const http = makeFetchDriver({
15629
16180
  ...httpOptions,
16181
+ // PLAN-11 U-2: a progress request is answered like any other (t.respond / t.fail); its
16182
+ // pending entry keeps the driver's progress callback for t.progress
16183
+ _progress: (url, init, step) => {
16184
+ const r = memFetch(url, init);
16185
+ pending[pending.length - 1].step = step;
16186
+ return r;
16187
+ },
15630
16188
  // 5-3: focus / online come from t.focus() / t.online() only
15631
16189
  _on: (f) => { signals.add(f); return () => signals.delete(f); },
15632
16190
  _tap: (req, res, pf) => { tapped = [req, res, pf]; },
15633
16191
  // D472: a batch / split request (a large decide()) is one fetch here, answered as one
15634
16192
  _whole: true,
15635
- fetch: (url, init) => new Promise((resolve, reject) => {
16193
+ fetch: memFetch = (url, init) => new Promise((resolve, reject) => {
15636
16194
  const [req, res, pf] = tapped || [{ url }];
15637
16195
  tapped = undefined;
15638
16196
  // G-171(1): a resource fetch is listed as { url, ...request, resource: name }; 5-5: a
@@ -15666,6 +16224,7 @@ function renderComponent(componentDef, options = {}) {
15666
16224
  return {
15667
16225
  select: (sel) => xs$1.merge(hs.select(sel), own(false)(sel), sock.select(sel)),
15668
16226
  errors: (sel) => xs$1.merge(hs.errors(sel), own(true)(sel)),
16227
+ progress: (sel) => hs.progress(sel),
15669
16228
  subs, at, pending, in$, http, ws, llm,
15670
16229
  // 5-3: the driver's matcher / inspection (the dev checks' SYG632, t.cache)
15671
16230
  __matches: http.__matches, __inspect: http.__inspect,
@@ -16431,7 +16990,7 @@ function renderComponent(componentDef, options = {}) {
16431
16990
  return { match: cat, desc: catDesc, o };
16432
16991
  if (typeof tg == 'string') {
16433
16992
  // an ok/error action name, key, category, resource name or URL
16434
- return { match: r => { const v = r.value || {}; return cat(r) && [v.ok, v.error, v.key, v.category, r.res, v.url].includes(tg); }, desc: ` matching '${tg}'${catDesc}`, o };
16993
+ return { match: r => { const v = r.value || {}; return cat(r) && [v.ok, v.error, v.progress, v.key, v.category, r.res, v.url].includes(tg); }, desc: ` matching '${tg}'${catDesc}`, o };
16435
16994
  }
16436
16995
  if (typeof tg == 'function') {
16437
16996
  return { match: r => { try {
@@ -16688,6 +17247,27 @@ function renderComponent(componentDef, options = {}) {
16688
17247
  const err = typeof error == 'number' || resp ? Object.assign(new Error(`HTTP ${status}`), { status }) : x;
16689
17248
  return { error: err, category, request: e?.req, status: status ?? x?.status, body };
16690
17249
  });
17250
+ // PLAN-11 U-2: t.progress. One progress step of a pending request sent with `progress` (its
17251
+ // reply action, or progress() for `progress: true`); the request stays pending. Targets as
17252
+ // t.respond's; the phase defaults to 'upload' for a request with a body, else 'download'
17253
+ const progress = (name, step, opts) => {
17254
+ throwFailure();
17255
+ if (drivers[name])
17256
+ throw new Error(`[Sygnal] t.progress('${name}'): ${name} has a real driver (passed in drivers), so there is nothing to script`);
17257
+ if (!step || typeof step != 'object' || typeof step.loaded != 'number')
17258
+ throw new Error(`[Sygnal] t.progress('${name}', step): step is { loaded, total?, phase? } (got ${brief(step)})`);
17259
+ const tg = targetOf(opts);
17260
+ const what = `t.progress('${name}'${typeof opts == 'string' ? `, …, '${opts}'` : ''})`;
17261
+ const now = isReady && !inputs.length ? pick(name, tg) : undefined;
17262
+ if (now && !now.step)
17263
+ throw new Error(`[Sygnal] ${what}: the request ${brief(now.value)} has no progress: send it with progress: 'UPLOADING' (a reply action) or progress: true`);
17264
+ return scripted(() => pick(name, tg), w => noPending(what, name, tg, w), (e) => {
17265
+ if (!e.step)
17266
+ return new Error(`[Sygnal] ${what}: the request ${brief(e.value)} has no progress: send it with progress: 'UPLOADING' (a reply action) or progress: true`);
17267
+ const r = e.req || {};
17268
+ e.step(step.phase || (r.body !== undefined || r.json !== undefined || r.multipart !== undefined ? 'upload' : 'download'), step.loaded, step.total ?? 0);
17269
+ }, sendDue);
17270
+ };
16691
17271
  // PLAN-6 L-4: t.stream. Targets as t.respond's (an action name or key, a partial request, a
16692
17272
  // predicate, { request, nth }); `end: false` (the last argument, or in the target options)
16693
17273
  // leaves the stream open. Chunks become ChatEvents pushed into the in-memory stream; the fake's
@@ -17192,7 +17772,14 @@ function renderComponent(componentDef, options = {}) {
17192
17772
  let msg = `${name} timed out after ${timeoutMs}ms.`;
17193
17773
  const [why, pending] = explainNext(start);
17194
17774
  msg += why;
17195
- if (pending)
17775
+ if (inputs.length)
17776
+ msg += ` ${inputs.length} simulated input(s) still pending.`;
17777
+ // PLAN-11 Q-3: on the fake clock a state a timer (TIMER driver, periodic, delay) brings
17778
+ // later than the timeout can't arrive in time: name the next pending timer
17779
+ const later = pending ? 0 : nextFakeTimer();
17780
+ if (later)
17781
+ msg += ` A timer is still pending on the fake clock (it fires ${later}ms after this timeout): if the state comes from it, this wait ended first.`;
17782
+ if (pending || later)
17196
17783
  msg += ` Wait longer: t.${name}(pred, ms), or the renderComponent timeoutMs option (default for every wait).`;
17197
17784
  // G-065: a state from before the call matched; next() only looks at new ones
17198
17785
  for (let i = from - 1; i >= 0; i--) {
@@ -17392,6 +17979,7 @@ function renderComponent(componentDef, options = {}) {
17392
17979
  requests,
17393
17980
  respond,
17394
17981
  fail,
17982
+ progress,
17395
17983
  stream,
17396
17984
  connections,
17397
17985
  cache,
@@ -17968,7 +18556,7 @@ function jsonSchema(schema, options = {}) {
17968
18556
  * Side-effect free: an app that doesn't import it pays 0 B.
17969
18557
  */
17970
18558
  const NAME = 30, DESC = 500, PARAM = 150, OUT = 1500, SUMMARY = 300;
17971
- const g$1 = globalThis;
18559
+ const g = globalThis;
17972
18560
  /** cut to n characters with an ellipsis, never inside a surrogate pair */
17973
18561
  const cut = (s, n) => {
17974
18562
  if (s.length <= n)
@@ -17985,7 +18573,7 @@ const hash = (s) => {
17985
18573
  return (h % 1679616).toString(36).padStart(4, '0');
17986
18574
  };
17987
18575
  function experimentalExposeWebMcp(app, options = {}) {
17988
- const mc = options.modelContext ?? g$1.document?.modelContext ?? g$1.navigator?.modelContext;
18576
+ const mc = options.modelContext ?? g.document?.modelContext ?? g.navigator?.modelContext;
17989
18577
  const said = new Set();
17990
18578
  const dev = (code, key, message, severity = 'warn') => {
17991
18579
  if (said.has(code + key))
@@ -18188,7 +18776,7 @@ function experimentalExposeWebMcp(app, options = {}) {
18188
18776
  let n$2 = 0;
18189
18777
  /** the default confirmation: a native modal dialog outside the app's tree (Escape and "Deny" decline) */
18190
18778
  function dialog(info, setClose) {
18191
- const doc = g$1.document;
18779
+ const doc = g.document;
18192
18780
  if (!doc?.createElement)
18193
18781
  return Promise.resolve(false);
18194
18782
  return new Promise(resolve => {
@@ -18444,7 +19032,7 @@ const toolsOf$1 = (e) => e.o.agent === false ? null : e.tools || (e.tools = agen
18444
19032
  }),
18445
19033
  }));
18446
19034
  // dev diagnostics (SYG684, SYG685): no-ops with diagnostics off
18447
- const dev$3 = (code, message, fix) => { try {
19035
+ const dev$2 = (code, message, fix) => { try {
18448
19036
  report(code, { component: 'chat', message, fix, severity: 'warn' });
18449
19037
  }
18450
19038
  catch (e) {
@@ -18654,7 +19242,7 @@ function steps(c, o) {
18654
19242
  const openPart = (s, id, what, guard = (_p) => true) => {
18655
19243
  const p = openToolCalls(s).find(x => x.toolCallId === id && isClient(c, x) && guard(x));
18656
19244
  if (!p)
18657
- dev$3('SYG685', `${what} for the tool call '${id}', which is not waiting for an answer (answered, closed, or not a client tool call of the last message); it is ignored`, 'Answer only the calls in openToolCalls(state); a late answer after STOP, SEND or REGENERATE is dropped on purpose');
19245
+ dev$2('SYG685', `${what} for the tool call '${id}', which is not waiting for an answer (answered, closed, or not a client tool call of the last message); it is ignored`, 'Answer only the calls in openToolCalls(state); a late answer after STOP, SEND or REGENERATE is dropped on purpose');
18658
19246
  return p;
18659
19247
  };
18660
19248
  /** after a call closed or a card changed: the next queued card, then rule 3 */
@@ -18782,7 +19370,7 @@ function request(e, o, k, messages, fresh, c, host) {
18782
19370
  for (const n in c.client) {
18783
19371
  if (n in tools && !shadowed.has(n)) {
18784
19372
  shadowed.add(n);
18785
- dev$3('SYG684', `the client tool '${n}' has the name of an agent tool; the client tool is sent and its calls are the app's to answer`, 'Rename the client tool, or the agent declaration or action');
19373
+ dev$2('SYG684', `the client tool '${n}' has the name of an agent tool; the client tool is sent and its calls are the app's to answer`, 'Rename the client tool, or the agent declaration or action');
18786
19374
  }
18787
19375
  const x = c.client[n];
18788
19376
  tools[n] = c.ask && n == 'ask_user' ? c.kit.askTool(c.ask, host) : { description: x.description, inputSchema: schemaOf(x) };
@@ -18899,7 +19487,7 @@ const chat = (options = {}) => {
18899
19487
  ...(c.ask?.controls && DOM && { CONTROL: DOM.select('[data-compose-control]').events('click').map(controlOf$1).filter((x) => !!x) }),
18900
19488
  };
18901
19489
  },
18902
- model: {
19490
+ model: fromPrevious({
18903
19491
  PROMPT: (s, p) => typeof p == 'string' && p !== s.prompt ? { ...s, prompt: p } : ABORT,
18904
19492
  SEND: {
18905
19493
  STATE: (s, d) => sendStep(s, d) || ABORT,
@@ -18997,153 +19585,19 @@ const chat = (options = {}) => {
18997
19585
  ANSWER: { STATE: stateOf(S.answerCard), ...sinks(S.answerCard) },
18998
19586
  CONTROL: (s, d) => S.controlStep(s, d) || ABORT,
18999
19587
  }),
19000
- },
19588
+ }),
19001
19589
  })(options);
19002
19590
  // the `prompt` option is a selector, not the slice's start value (defineBehavior's option override)
19003
19591
  b.state = { ...b.state, prompt: '' };
19004
19592
  return linked(b);
19005
19593
  };
19006
19594
 
19007
- /*
19008
- * PLAN-6 L-2: what the HTTP transports share (side-effect free; each transport imports only what
19009
- * it uses).
19010
- * - post(): the fetch (the `fetch` option, 0-S5), the headers option (an object or a function of
19011
- * the request), HTTP errors as Errors with `status` and the provider's message, and SYG670.
19012
- * - sse(): a fetch Response body as SSE frames ({ event, data }), per the HTML spec's parsing
19013
- * rules: CRLF / LF, comments, multi-line data. The reader is cancelled when the consumer stops
19014
- * (abort, a thrown error), so the connection closes.
19015
- * - SYG670: in a browser, a request with an auth header (Authorization, x-api-key, api-key,
19016
- * x-goog-api-key) to a host that is neither local (localhost, 127.0.0.1, [::1], *.localhost)
19017
- * nor the page's own origin is refused unless the transport has `dangerouslyAllowBrowser: true`:
19018
- * the key would ship to every visitor. Same-origin requests are the app's own server (its
19019
- * proxy), so a session header there is fine.
19020
- */
19021
- const g = globalThis;
19022
- const AUTH = /^(authorization|x-api-key|api-key|x-goog-api-key)$/i;
19023
- const LOCAL = /^(localhost|127\.0\.0\.1|\[::1\]|.+\.localhost)$/;
19024
- /** the URL a transport posts to, or a SYG670 Error when it must not (see the header) */
19025
- function guard(url, headers, opts, name) {
19026
- if (opts.dangerouslyAllowBrowser || typeof g.document == 'undefined' || !Object.keys(headers).some(h => AUTH.test(h)))
19027
- return;
19028
- const here = g.location?.href;
19029
- let u;
19030
- try {
19031
- u = new URL(url, here);
19032
- }
19033
- catch (_) {
19034
- return;
19035
- }
19036
- if (LOCAL.test(u.hostname) || (here && u.origin == new URL(here).origin))
19037
- return;
19038
- const fix = `Call ${u.origin} from your server and point the transport at that route (an AI SDK route with uiMessageStream('/api/chat'), or your own proxy), so the key stays there. For a key the user typed in themselves, pass ${name}({ ..., dangerouslyAllowBrowser: true })`;
19039
- const msg = `${name}: refused to send an auth header from the browser to ${u.origin}: anyone who opens the page can read it`;
19040
- error('SYG670', undefined, msg, fix);
19041
- fail$2('SYG670', undefined, msg, fix);
19042
- }
19043
- /**
19044
- * POST JSON; a non-2xx response is an Error with `status`, `body` and the provider's message.
19045
- * The transport's `body` option and the request's `body` are merged over the built body (so they
19046
- * can override provider defaults such as `max_tokens`); `fixed` fields are merged last and can't be
19047
- * overridden (G-654: uiMessageStream's protocol fields, as the AI SDK's DefaultChatTransport)
19048
- */
19049
- async function post$1(url, body, req, signal, opts, name, fixed) {
19050
- const h = typeof opts.headers == 'function' ? await opts.headers(req) : opts.headers || {};
19051
- guard(url, h, opts, name);
19052
- const f = opts.fetch || ((u, i) => g.fetch(u, i));
19053
- const res = await f(url, { method: 'POST', headers: { 'content-type': 'application/json', ...h }, body: JSON.stringify({ ...body, ...opts.body, ...req.body, ...fixed }), signal });
19054
- if (!res.ok) {
19055
- let text = '', message = '';
19056
- try {
19057
- text = await res.text();
19058
- const j = JSON.parse(text);
19059
- message = j.error?.message ?? j.error ?? j.message ?? '';
19060
- }
19061
- catch (_) {
19062
- message || (message = text.slice(0, 200));
19063
- }
19064
- throw Object.assign(new Error(`${name}: HTTP ${res.status}${message ? ': ' + message : ''}`), { status: res.status, body: text });
19065
- }
19066
- if (!res.body)
19067
- throw new Error(`${name}: the response has no body`);
19068
- return res;
19069
- }
19070
- /** the SSE frames of a response body: `{ event, data }` (data: the joined data lines) */
19071
- async function* sse(res) {
19072
- const reader = res.body.getReader(), dec = new TextDecoder();
19073
- let buf = '', cr = '', done = false;
19074
- try {
19075
- for (;;) {
19076
- const r = await reader.read();
19077
- // line ends -> \n (a \r at a chunk's end waits: it may be half of a \r\n)
19078
- let s = cr + (r.done ? '\n\n' : dec.decode(r.value, { stream: true }));
19079
- cr = !r.done && s.endsWith('\r') ? '\r' : '';
19080
- if (cr)
19081
- s = s.slice(0, -1);
19082
- buf += s.replace(/\r\n?/g, '\n');
19083
- done = r.done;
19084
- let i;
19085
- while ((i = buf.indexOf('\n\n')) >= 0) {
19086
- const frame = buf.slice(0, i);
19087
- buf = buf.slice(i + 2);
19088
- let event = 'message', data = [];
19089
- for (const line of frame.split('\n')) {
19090
- if (!line || line[0] == ':')
19091
- continue;
19092
- const c = line.indexOf(':'), field = c < 0 ? line : line.slice(0, c);
19093
- const v = c < 0 ? '' : line.slice(c + 1).replace(/^ /, '');
19094
- if (field == 'event')
19095
- event = v;
19096
- else if (field == 'data')
19097
- data.push(v);
19098
- }
19099
- if (data.length)
19100
- yield { event, data: data.join('\n') };
19101
- }
19102
- if (done)
19103
- return;
19104
- }
19105
- }
19106
- finally {
19107
- if (!done)
19108
- reader.cancel().catch(() => { });
19109
- }
19110
- }
19111
- /** JSON.parse, or the text itself when it isn't JSON (a model's malformed tool arguments go to validation as is) */
19112
- const json = (s) => {
19113
- if (typeof s != 'string')
19114
- return s;
19115
- try {
19116
- return JSON.parse(s || '{}');
19117
- }
19118
- catch (_) {
19119
- return s;
19120
- }
19121
- };
19122
- /** a provider's usage as `{ inputTokens, outputTokens, totalTokens, reasoningTokens? }` (AI SDK names) */
19123
- const usageOf = (u) => u && {
19124
- inputTokens: u.input_tokens ?? u.prompt_tokens,
19125
- outputTokens: u.output_tokens ?? u.completion_tokens,
19126
- totalTokens: u.total_tokens,
19127
- ...((u.output_tokens_details ?? u.completion_tokens_details)?.reasoning_tokens != null && { reasoningTokens: (u.output_tokens_details ?? u.completion_tokens_details).reasoning_tokens }),
19128
- };
19129
- /** the parts of a message (a `content` string is one text part) */
19130
- const partsOf = (m) => m.parts ?? (typeof m.content == 'string' ? [{ type: 'text', text: m.content }] : []);
19131
- /** the tool name of a `tool-<name>` / `dynamic-tool` part, or '' */
19132
- const toolName = (p) => p.type == 'dynamic-tool' ? p.toolName : p.type.startsWith('tool-') ? p.type.slice(5) : '';
19133
- /** what a tool part sends back to the model, or undefined when it has no result yet */
19134
- const toolOutput = (p) => p.state == 'output-available' ? (typeof p.output == 'string' ? p.output : JSON.stringify(p.output ?? null))
19135
- : p.state == 'output-error' ? JSON.stringify({ error: p.errorText ?? 'error' })
19136
- : p.state == 'output-denied' ? JSON.stringify({ error: 'The user denied this tool call' })
19137
- : undefined;
19138
- // dev-only diagnostics (SYG675): the dev entry formats them (checks/chat.ts)
19139
- const dev$2 = (...a) => g.__SYGNAL_DIAGNOSTICS__?.chat?.(...a);
19140
-
19141
19595
  const EMPTY$1 = { type: 'object', properties: {} };
19142
19596
  function prepare(req, strict, seen, transport, dialect = 'openai', limits) {
19143
19597
  const sent = {};
19144
19598
  if (strict && typeof strict != 'function' && !seen.has('')) {
19145
19599
  seen.add('');
19146
- dev$2('SYG672', req, { transport });
19600
+ dev$8('SYG672', req, { transport });
19147
19601
  }
19148
19602
  const fn = typeof strict == 'function' ? strict : undefined;
19149
19603
  // what the request has left of `limits` (strictSchemas counts it down)
@@ -19156,7 +19610,7 @@ function prepare(req, strict, seen, transport, dialect = 'openai', limits) {
19156
19610
  return { schema: s.schema, strict: true };
19157
19611
  if (!seen.has(what)) {
19158
19612
  seen.add(what);
19159
- dev$2('SYG675', req, { tool: what, errors: s.errors });
19613
+ dev$8('SYG675', req, { tool: what, errors: s.errors });
19160
19614
  }
19161
19615
  return { schema, strict: false };
19162
19616
  };
@@ -19227,12 +19681,12 @@ function openResponses(options = {}) {
19227
19681
  c.done = 1;
19228
19682
  called++;
19229
19683
  const name = c.name ?? item?.name;
19230
- return { type: 'tool-call', id: c.call_id ?? item?.call_id ?? id, name, input: p.input(name, json(args ?? item?.arguments ?? c.args)) };
19684
+ return { type: 'tool-call', id: c.call_id ?? item?.call_id ?? id, name, input: p.input(name, json$1(args ?? item?.arguments ?? c.args)) };
19231
19685
  };
19232
19686
  for await (const { data } of sse(res)) {
19233
19687
  if (data == '[DONE]')
19234
19688
  break;
19235
- const e = json(data), t = e?.type;
19689
+ const e = json$1(data), t = e?.type;
19236
19690
  if (typeof t != 'string')
19237
19691
  continue;
19238
19692
  if (t == 'response.output_text.delta' || t == 'response.refusal.delta' || REASONING.test(t)) {
@@ -19324,13 +19778,13 @@ function chatCompletions(options = {}) {
19324
19778
  const done = function* () {
19325
19779
  for (const c of calls)
19326
19780
  if (c)
19327
- yield { type: 'tool-call', id: c.id, name: c.name, input: p.input(c.name, json(c.args)) };
19781
+ yield { type: 'tool-call', id: c.id, name: c.name, input: p.input(c.name, json$1(c.args)) };
19328
19782
  calls = [];
19329
19783
  };
19330
19784
  for await (const { data } of sse(res)) {
19331
19785
  if (data == '[DONE]')
19332
19786
  break;
19333
- const e = json(data);
19787
+ const e = json$1(data);
19334
19788
  if (!e || typeof e != 'object')
19335
19789
  continue;
19336
19790
  if (e.error)
@@ -19418,7 +19872,7 @@ function uiMessageStream(url, options = {}) {
19418
19872
  for await (const { data } of sse(res)) {
19419
19873
  if (data == '[DONE]')
19420
19874
  break;
19421
- const c = json(data), t = c?.type;
19875
+ const c = json$1(data), t = c?.type;
19422
19876
  if (typeof t != 'string')
19423
19877
  continue;
19424
19878
  if (ONE.test(t)) {
@@ -20149,7 +20603,7 @@ const commandBar = (options = {}) => {
20149
20603
  ...(deny && { DENY: on(DOM, deny) }),
20150
20604
  };
20151
20605
  },
20152
- model: {
20606
+ model: fromPrevious({
20153
20607
  INPUT: (s, v) => typeof v == 'string' && v !== s.text ? { ...s, text: v } : ABORT,
20154
20608
  RUN: {
20155
20609
  STATE: (s, d, _n, p, o, k) => runStep(s, d, E(p, k, o))?.s || ABORT,
@@ -20185,7 +20639,7 @@ const commandBar = (options = {}) => {
20185
20639
  // a command ran (or was refused): the result; the input is cleared when it was this command
20186
20640
  DONE: (s, d) => s.status != 'running' ? ABORT
20187
20641
  : { ...s, status: 'ready', command: null, pending: null, result: d, text: d?.ok && s.text.trim() === d.command ? '' : s.text },
20188
- },
20642
+ }),
20189
20643
  })(options);
20190
20644
  return linked(b);
20191
20645
  };
@@ -20285,7 +20739,7 @@ function anthropicMessages(options = {}) {
20285
20739
  return { inputTokens: i, outputTokens: o, totalTokens: i + o, ...(u.output_tokens_details?.thinking_tokens != null && { reasoningTokens: u.output_tokens_details.thinking_tokens }) };
20286
20740
  };
20287
20741
  for await (const { data } of sse(res)) {
20288
- const e = json(data), t = e?.type;
20742
+ const e = json$1(data), t = e?.type;
20289
20743
  if (typeof t != 'string')
20290
20744
  continue;
20291
20745
  if (t == 'message_start') {
@@ -20321,9 +20775,9 @@ function anthropicMessages(options = {}) {
20321
20775
  if (b?.type == 'thinking' && b.signature)
20322
20776
  yield { type: 'reasoning', delta: '', providerMetadata: { anthropic: { signature: b.signature } } };
20323
20777
  else if (b?.type == 'tool_use')
20324
- yield { type: 'tool-call', id: b.id, name: b.name, input: p.input(b.name, b.json ? json(b.json) : b.input ?? {}) };
20778
+ yield { type: 'tool-call', id: b.id, name: b.name, input: p.input(b.name, b.json ? json$1(b.json) : b.input ?? {}) };
20325
20779
  else if (b?.type == 'server_tool_use')
20326
- yield { type: 'tool-call', id: b.id, name: b.name, input: b.json ? json(b.json) : b.input ?? {}, executed: true, providerExecuted: true };
20780
+ yield { type: 'tool-call', id: b.id, name: b.name, input: b.json ? json$1(b.json) : b.input ?? {}, executed: true, providerExecuted: true };
20327
20781
  }
20328
20782
  else if (t == 'message_delta') {
20329
20783
  if (e.delta?.stop_reason)
@@ -20463,10 +20917,10 @@ function agui(url, options = {}) {
20463
20917
  if (!c || c.done)
20464
20918
  return;
20465
20919
  c.done = 1;
20466
- open.push({ type: 'tool-call', id, name: c.name, input: json(c.args || '{}') });
20920
+ open.push({ type: 'tool-call', id, name: c.name, input: json$1(c.args || '{}') });
20467
20921
  };
20468
20922
  for await (const { data } of sse(res)) {
20469
- const e = json(data), t = e?.type;
20923
+ const e = json$1(data), t = e?.type;
20470
20924
  if (typeof t != 'string')
20471
20925
  continue;
20472
20926
  if (t == 'TEXT_MESSAGE_START') {
@@ -20514,7 +20968,7 @@ function agui(url, options = {}) {
20514
20968
  else if (t == 'TOOL_CALL_RESULT') {
20515
20969
  end(e.toolCallId);
20516
20970
  yield* take(e.toolCallId);
20517
- yield { type: 'tool-result', id: e.toolCallId, output: typeof e.content == 'string' ? json(e.content) : e.content };
20971
+ yield { type: 'tool-result', id: e.toolCallId, output: typeof e.content == 'string' ? json$1(e.content) : e.content };
20518
20972
  }
20519
20973
  else if (t == 'STATE_SNAPSHOT' || t == 'STATE_DELTA') {
20520
20974
  state = t == 'STATE_SNAPSHOT' ? e.snapshot : applyPatch(state, e.delta);
@@ -21715,7 +22169,7 @@ function composeBehavior(catalog, options = {}) {
21715
22169
  .map(({ key, name, value }) => ({ key, name, value })) }),
21716
22170
  };
21717
22171
  },
21718
- model: {
22172
+ model: fromPrevious({
21719
22173
  PROMPT: (s, p) => typeof p == 'string' && p !== s.prompt ? { ...s, prompt: p } : ABORT,
21720
22174
  COMPOSE: { HOST: host(start), [S]: sink(start) },
21721
22175
  REPLY: {
@@ -21757,7 +22211,7 @@ function composeBehavior(catalog, options = {}) {
21757
22211
  CONTROL: (s, c) => c && typeof c.key == 'string' && typeof c.name == 'string'
21758
22212
  ? { ...s, ui: { ...s.ui, [c.key]: { ...s.ui?.[c.key], [c.name]: c.value } } } : ABORT,
21759
22213
  DONE: () => ABORT,
21760
- },
22214
+ }),
21761
22215
  })(rest);
21762
22216
  }
21763
22217
 
@@ -25822,7 +26276,7 @@ const compose = /*#__PURE__*/ Object.assign((catalog, options) => composeBehavio
25822
26276
  * is gone); set: the array with that element replaced (the state unchanged when it is gone).
25823
26277
  * Side-effect free.
25824
26278
  */
25825
- const byId = (field, id, idKey = 'id') => ({
26279
+ const byId = (field, id, idKey = 'id') => Object.defineProperty({
25826
26280
  get: (s) => (Array.isArray(s?.[field]) ? s[field].find((x) => x?.[idKey] === id) : undefined),
25827
26281
  set: (s, v) => {
25828
26282
  const a = s?.[field];
@@ -25830,7 +26284,7 @@ const byId = (field, id, idKey = 'id') => ({
25830
26284
  return s;
25831
26285
  return { ...s, [field]: a.map((x) => (x?.[idKey] === id ? v : x)) };
25832
26286
  },
25833
- });
26287
+ }, BINDING, { value: [field, id, idKey] });
25834
26288
 
25835
26289
  /**
25836
26290
  * ESM ports of xstream's `flattenConcurrently` and `flattenSequentially` extras