sygnal 7.0.0 → 7.1.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.
package/dist/index.esm.js CHANGED
@@ -695,6 +695,7 @@ const DEV_CODE_SEVERITY = {
695
695
  SYG112: 'error',
696
696
  SYG115: 'warn',
697
697
  SYG116: 'error',
698
+ SYG117: 'warn',
698
699
  // PLAN-4 1-D: controls, static (sygnal-check)
699
700
  SYG124: 'error',
700
701
  SYG125: 'error',
@@ -706,6 +707,10 @@ const DEV_CODE_SEVERITY = {
706
707
  SYG129: 'warn',
707
708
  // PLAN-10 X-1: static only (sygnal-check)
708
709
  SYG170: 'warn',
710
+ // PLAN-13 X-1: static only (sygnal-check)
711
+ SYG302: 'info',
712
+ // PLAN-13 F-3 (checks/next.ts)
713
+ SYG429: 'warn',
709
714
  SYG171: 'warn',
710
715
  SYG221: 'error',
711
716
  // PLAN-4 GS-4 (state.ts)
@@ -1792,23 +1797,24 @@ const shownOf = (inst) => {
1792
1797
  function checkStatics(inst) {
1793
1798
  if (inst.disposed)
1794
1799
  return;
1795
- const s = inst.cell.get(), shown = shownOf(inst);
1800
+ const s = inst.cell.get(), shown = shownOf(inst), c = inst.context();
1796
1801
  // no state yet: nothing declared (as the state stream, which skips undefined)
1797
- if (s === undefined || (s === inst.sS && shown === inst.sH))
1802
+ if (s === undefined || (s === inst.sS && shown === inst.sH && c === inst.sC))
1798
1803
  return;
1799
1804
  inst.sS = s;
1800
1805
  inst.sH = shown;
1806
+ inst.sC = c;
1801
1807
  const view = inst.def.view, sv = inst.sv;
1802
1808
  for (const [n, k] of inst.st) {
1803
1809
  const f = view[k];
1804
1810
  let v = f;
1805
1811
  try {
1806
1812
  if (typeof f == 'function')
1807
- v = f(s);
1813
+ v = f(s, c);
1808
1814
  else if (f && typeof f == 'object') {
1809
1815
  v = {};
1810
1816
  for (const r in f)
1811
- v[r] = f[r](s);
1817
+ v[r] = f[r](s, c);
1812
1818
  }
1813
1819
  }
1814
1820
  catch (e) {
@@ -2219,6 +2225,14 @@ function bindingChanged(a, b) {
2219
2225
  return x && y ? !x.every((v, i) => Object.is(v, y[i]))
2220
2226
  : !!(x || y || typeof a == 'string' || typeof b == 'string' || a === undefined || b === undefined);
2221
2227
  }
2228
+ /** a binding whose target is known (a key, a byId selection): its deletion ends the child */
2229
+ const known$1 = (b) => typeof b == 'string' || b?.[BINDING];
2230
+ /** F-3: `owner` watches child (or Switchable page) `k`'s target: notify() disposes it when deleted */
2231
+ function watch(owner, k) {
2232
+ k.bs = bindingState(k);
2233
+ (owner.bound || (owner.bound = new Set())).add(k);
2234
+ owner.app.watchers.add(owner);
2235
+ }
2222
2236
  function bindingState(k) {
2223
2237
  const c = k.base || k.cell || k.arr;
2224
2238
  return c && (c.has?.() ?? c.get() !== undefined) ? (k.cell || k.arr).get() : undefined;
@@ -2805,11 +2819,8 @@ class Inst {
2805
2819
  k.setProps(props, children, n, id);
2806
2820
  else
2807
2821
  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
- }
2822
+ if (known$1(props[binding]))
2823
+ watch(this, k);
2813
2824
  return;
2814
2825
  }
2815
2826
  if (posts[sel] && !(ps || (ps = [])).includes(sel))
@@ -4218,6 +4229,8 @@ function check$3(owner, props) {
4218
4229
  if (!(cur in of))
4219
4230
  fail$2('SYG416', owner, `Switchable 'current' '${cur}' is not a key of 'of'`, CUR_FIX);
4220
4231
  }
4232
+ /** F-3: a per-page map (an object that is not a lens or a byId selection: no `get`), or null */
4233
+ const mapOf = (st) => isObj$2(st) && typeof st.get != 'function' ? st : null;
4221
4234
  /** a page's props: the Switchable's, kept by identity while equal (`of` compared shallowly) */
4222
4235
  function sameProps(a, b) {
4223
4236
  if (!a || !b)
@@ -4239,10 +4252,11 @@ class SwitchableHost {
4239
4252
  this.lr = true;
4240
4253
  this.kids = [];
4241
4254
  check$3(owner, props);
4242
- const st = props.state, calc = typeof st == 'string' && owner.def.calcNames?.has(pathOf$1(st)[0]);
4243
- this.cell = typeof st == 'string' ? keyCell(owner.cell, st, calc && owner.def.name, undefined, 'Switchable sub-component')
4244
- : st !== undefined ? lensCell(owner.cell, st, owner.def.name, (e) => owner.app.appError(owner, e, 'view'))
4245
- : owner.cell;
4255
+ const st = props.state;
4256
+ // F-3: `state={{ page: key | lens | byId }}` (not a lens, not a byId selection): each listed
4257
+ // page binds its own target, with a tag child's lifetime rules; the others share the owner's state
4258
+ this.map = mapOf(st);
4259
+ this.cell = this.bind(this.map ? undefined : st);
4246
4260
  this.uidBase = owner.uid(uidPart(id.replace(/.*::(r\.)?/, '')));
4247
4261
  this.props = props;
4248
4262
  this.kids = children;
@@ -4252,9 +4266,16 @@ class SwitchableHost {
4252
4266
  }
4253
4267
  this.setProps(props, children);
4254
4268
  }
4269
+ bind(st) {
4270
+ const owner = this.owner, calc = typeof st == 'string' && owner.def.calcNames?.has(pathOf$1(st)[0]);
4271
+ return typeof st == 'string' ? keyCell(owner.cell, st, calc && owner.def.name, undefined, 'Switchable sub-component')
4272
+ : st !== undefined ? lensCell(owner.cell, st, owner.def.name, (e) => owner.app.appError(owner, e, 'view'))
4273
+ : owner.cell;
4274
+ }
4255
4275
  make(name, view) {
4256
4276
  const o = this.owner, app = o.app, def = app.def(view), scope = app.scope();
4257
- const inst = new Inst(app, def, o, def.isolated ? localCell(app, this.cell) : this.cell, o.dom && o.dom.isolateSource(o.dom, scope), this.props, this.kids, scope, this.uidBase + '-' + uidPart(name), 'page');
4277
+ const c = this.map ? this.bind(this.map[name]) : this.cell;
4278
+ const inst = new Inst(app, def, o, def.isolated ? localCell(app, c) : c, o.dom && o.dom.isolateSource(o.dom, scope), this.props, this.kids, scope, this.uidBase + '-' + uidPart(name), 'page');
4258
4279
  inst.shown = false;
4259
4280
  return inst;
4260
4281
  }
@@ -4271,15 +4292,21 @@ class SwitchableHost {
4271
4292
  for (const n in this.pages)
4272
4293
  this.pages[n].inst.setProps(props, children);
4273
4294
  }
4295
+ // F-3: a page whose own binding target changed (7.0's rule, per page), or that the owner
4296
+ // disposed because its target was deleted, is made again
4297
+ const m = this.map, nm = this.map = mapOf(props.state);
4274
4298
  // G-317: a lazy() page that has loaded is made again with the loaded component
4275
4299
  for (const n in this.pages) {
4276
4300
  const p = this.pages[n], fn = resolve$2(p.view, this.owner);
4277
- if (fn !== p.fn) {
4301
+ if (fn !== p.fn || p.inst.disposed || m && nm && bindingChanged(m[n], nm[n])) {
4278
4302
  const shown = p.inst.shown;
4279
- p.inst.dispose();
4303
+ p.inst.disposed || p.inst.dispose();
4280
4304
  p.inst = this.make(n, p.fn = fn);
4281
4305
  p.inst.shown = shown;
4282
4306
  }
4307
+ // the owner disposes a page whose target is deleted, as a tag child; it is made again above
4308
+ if (nm && known$1(nm[n]))
4309
+ watch(this.owner, p.inst);
4283
4310
  }
4284
4311
  const page = this.pages[cur];
4285
4312
  if (page) {
@@ -5891,6 +5918,10 @@ function makeFetchDriver(options = {}) {
5891
5918
  write(s, n, R, L.status, L.data, L.error, true);
5892
5919
  else
5893
5920
  write(s, n, R, 'loading');
5921
+ // R-1: the write is dispatched synchronously; when it changed the request (a navigation
5922
+ // recomputed the statics and a nested load sent the new one) or paused it, q is stale
5923
+ if (R.q !== q || R.p)
5924
+ return;
5894
5925
  if (go || q.validate)
5895
5926
  R.i = send(q, s, n, after, R);
5896
5927
  };
@@ -6355,6 +6386,9 @@ async function* sse(res, meta) {
6355
6386
  try {
6356
6387
  for (;;) {
6357
6388
  const r = await reader.read();
6389
+ // PLAN-13 S-1: any bytes (a heartbeat comment too) show the stream is alive
6390
+ if (!r.done)
6391
+ meta?.beat?.();
6358
6392
  // line ends -> \n (a \r at a chunk's end waits: it may be half of a \r\n)
6359
6393
  let s = cr + (r.done ? '\n\n' : dec.decode(r.value, { stream: true }));
6360
6394
  cr = !r.done && s.endsWith('\r') ? '\r' : '';
@@ -6436,6 +6470,8 @@ const dev$8 = (...a) => g$6.__SYGNAL_DIAGNOSTICS__?.chat?.(...a);
6436
6470
  * - a non-2xx status or another content type: `error` with readyState CLOSED (no retry; the
6437
6471
  * driver's reconnect policy decides);
6438
6472
  * - `close()` aborts the request and stops retrying.
6473
+ * - PLAN-13 S-1: `init.lastEventId` seeds the id sent as `Last-Event-ID` (a driver reconnect that
6474
+ * resumes); `onbeat` (not EventSource's) is called for any bytes read, heartbeat comments too.
6439
6475
  */
6440
6476
  const NAMES = ['CONNECTING', 'OPEN', 'CLOSED'];
6441
6477
  class FetchEventSource {
@@ -6444,11 +6480,14 @@ class FetchEventSource {
6444
6480
  this.onopen = null;
6445
6481
  this.onmessage = null;
6446
6482
  this.onerror = null;
6483
+ this.onbeat = null;
6447
6484
  this.ls = new Map();
6448
- this.meta = { id: '', retry: 3000 };
6485
+ this.meta = { id: '', retry: 3000, beat: () => this.onbeat?.() };
6449
6486
  this.url = url;
6450
6487
  this.withCredentials = !!init.withCredentials;
6451
6488
  this.init = init;
6489
+ if (init.lastEventId)
6490
+ this.meta.id = init.lastEventId;
6452
6491
  queueMicrotask(() => this.connect());
6453
6492
  }
6454
6493
  addEventListener(type, f) {
@@ -6584,6 +6623,12 @@ FetchEventSource.CLOSED = 2;
6584
6623
  * changed header reconnects) is read with FetchEventSource (fetch, `options.fetch` if given),
6585
6624
  * since EventSource can't send headers. An `options.EventSource` (a polyfill, the test fake)
6586
6625
  * still wins and gets the headers in its init.
6626
+ * - PLAN-13 S-1: SSE `meta: true`: message and `events` actions get `{ data, id, event }`. `resume:
6627
+ * true | 'param'`: the transport keeps the last event id, and a reconnect the driver makes (after
6628
+ * EventSource gave up) asks to resume from it: `?lastEventId=` (or `?param=`) on the URL, or
6629
+ * `Last-Event-ID` on the fetch path (`init.lastEventId`). `staleAfterMs` (SSE and WebSocket): no
6630
+ * event for that long (over fetch: no bytes, heartbeat comments count) closes the connection as a
6631
+ * drop (`close` gets `stale: true`), so the reconnect policy applies.
6587
6632
  */
6588
6633
  const SCOPE$1 = '__sygnalScope';
6589
6634
  const scopeOf = (v) => v[SCOPE$1] || [];
@@ -6653,20 +6698,48 @@ function makeSocketDriver(options = {}) {
6653
6698
  deliver(e, 'error', { error });
6654
6699
  }
6655
6700
  };
6701
+ // S-1: an SSE event as a subscriber gets it (`meta`: with its id and type); the transport
6702
+ // keeps the last id (resume) and counts it as life (staleAfterMs)
6703
+ const got = (t, m, e) => e.sse && e.spec.meta ? { data: parse(m.data), id: m.lastEventId ?? '', event: m.type || 'message' } : parse(m.data);
6704
+ const seen = (t, m) => { if (m.lastEventId)
6705
+ t.id = m.lastEventId; alive(t); };
6656
6706
  // SSE named events: one listener per name across the subscribers
6657
6707
  const listen = (t) => t.s && t.sse && each(t, e => Object.keys(e.spec.events || {}).forEach(name => {
6658
6708
  if (t.on.has(name))
6659
6709
  return;
6660
6710
  t.on.add(name);
6661
6711
  const s = t.s;
6662
- s.addEventListener(name, (m) => t.s === s && each(t, x => {
6663
- const a = x.spec.events?.[name];
6664
- if (typeof a == 'string' && x.sender !== undefined)
6665
- reply(x.sender, a, parse(m.data));
6666
- }));
6712
+ s.addEventListener(name, (m) => {
6713
+ if (t.s !== s)
6714
+ return;
6715
+ seen(t, m);
6716
+ each(t, x => {
6717
+ const a = x.spec.events?.[name];
6718
+ if (typeof a == 'string' && x.sender !== undefined)
6719
+ reply(x.sender, a, got(t, m, x));
6720
+ });
6721
+ });
6667
6722
  }));
6723
+ // S-1: staleAfterMs (the smallest of the subscribers'): re-armed by every event; when it fires
6724
+ // the connection is dropped (as if the server had gone) and the reconnect policy applies
6725
+ const alive = (t) => {
6726
+ clearTimeout(t.st);
6727
+ const ms = Math.min(...[...t.subs].map(e => e.spec.staleAfterMs).filter(x => x > 0));
6728
+ const s = t.s;
6729
+ if (s && ms < 1 / 0)
6730
+ t.st = setTimeout(() => {
6731
+ if (t.s !== s)
6732
+ return;
6733
+ try {
6734
+ s.close();
6735
+ }
6736
+ catch (_) { }
6737
+ gone(t, t.sse ? { stale: true } : { code: 1006, reason: 'stale', stale: true });
6738
+ }, ms);
6739
+ };
6668
6740
  // the socket closed (or failed to open) without the driver closing it
6669
6741
  const gone = (t, info) => {
6742
+ clearTimeout(t.st);
6670
6743
  t.s = null;
6671
6744
  t.open = false;
6672
6745
  let again;
@@ -6687,9 +6760,19 @@ function makeSocketDriver(options = {}) {
6687
6760
  const C = t.sse ? options.EventSource ?? (hdr && typeof g.fetch == 'function' ? FetchEventSource : g.EventSource) : options.WebSocket ?? g.WebSocket;
6688
6761
  if (typeof C != 'function')
6689
6762
  return;
6763
+ // S-1: resume from the last event id: Last-Event-ID on the fetch path (init.lastEventId, which
6764
+ // a polyfill or the test fake gets too), else a URL parameter
6765
+ // (a parameter the URL already has is replaced, not added again)
6766
+ const r = t.sse && t.id && [...t.subs].map(e => e.spec.resume).find(x => x);
6767
+ let url = t.url;
6768
+ if (r && !hdr) {
6769
+ const p = encodeURIComponent(r === true ? 'lastEventId' : r) + '=', v = p + encodeURIComponent(t.id);
6770
+ const re = new RegExp('([?&])' + p.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + '[^&#]*');
6771
+ url = re.test(url) ? url.replace(re, '$1' + v) : url + (url.includes('?') ? '&' : '?') + v;
6772
+ }
6690
6773
  let s;
6691
6774
  try {
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);
6775
+ s = t.sse ? new C(url, { withCredentials: !!t.spec.withCredentials, ...(hdr && { headers: hdr, fetch: options.fetch }), ...(r && { lastEventId: t.id }) }) : new C(t.url, t.spec.protocols);
6693
6776
  }
6694
6777
  catch (error) {
6695
6778
  // a URL the constructor rejects never opens: no retry
@@ -6704,12 +6787,22 @@ function makeSocketDriver(options = {}) {
6704
6787
  s.onopen = mine(() => {
6705
6788
  t.n = 0;
6706
6789
  t.open = true;
6790
+ alive(t);
6707
6791
  each(t, e => { deliver(e, 'open', { reconnected: !!e.opened }); e.opened = true; });
6708
6792
  const q = t.queue;
6709
6793
  t.queue = [];
6710
6794
  q.forEach((x) => t.s === s && out(t, x.data, x.e));
6711
6795
  });
6712
- s.onmessage = mine((m) => { const d = parse(m.data); each(t, e => deliver(e, 'message', d)); });
6796
+ s.onmessage = mine((m) => {
6797
+ if (t.sse)
6798
+ seen(t, m);
6799
+ else
6800
+ alive(t);
6801
+ const d = parse(m.data);
6802
+ each(t, e => deliver(e, 'message', e.spec.meta && t.sse ? got(t, m, e) : d));
6803
+ });
6804
+ // S-1: FetchEventSource's bytes (heartbeat comments) count as life
6805
+ s.onbeat = mine(() => alive(t));
6713
6806
  s.onerror = mine((error) => {
6714
6807
  each(t, e => deliver(e, 'error', { error }));
6715
6808
  if (t.sse) {
@@ -6731,6 +6824,7 @@ function makeSocketDriver(options = {}) {
6731
6824
  // closes the driver makes: no `close` action, late events ignored, retry cancelled
6732
6825
  const end = (t) => {
6733
6826
  clearTimeout(t.timer);
6827
+ clearTimeout(t.st);
6734
6828
  transports.delete(t.key);
6735
6829
  const s = t.s;
6736
6830
  t.s = null;
@@ -8529,7 +8623,8 @@ function makeHeadDriver(options = {}) {
8529
8623
  * falsy one stops, a changed spec restarts (an equal one keeps running).
8530
8624
  * - `every`: drift-free; tick n is due at start + n * every (each delay is computed from the
8531
8625
  * start, not chained). A late tick (a throttled background tab) coalesces the missed ones:
8532
- * n jumps. Data `{ n, t }`: the tick number from 1, `t` = Date.now().
8626
+ * n jumps. A delay is at most `every`, so a frozen Date.now() (a test clock that fixes the time
8627
+ * while timers run) ticks once per interval. Data `{ n, t }`: the tick number from 1, `t` = Date.now().
8533
8628
  * - `after`: fires once, data `{ t }`. It doesn't fire again while the same spec stays declared.
8534
8629
  * - `frame`: every animation frame (requestAnimationFrame; setTimeout 16 ms without it), data
8535
8630
  * `{ t, dt }`: Date.now() and the ms since the previous frame (0 on the first).
@@ -8561,7 +8656,9 @@ const start$1 = (s, send, e) => {
8561
8656
  }
8562
8657
  else if (every) {
8563
8658
  n = Math.max(n + 1, Math.floor((t - t0) / every));
8564
- later(t0 + (n + 1) * every - t);
8659
+ // never more than one interval: under a frozen Date.now() (Playwright's setFixedTime: Date is
8660
+ // fixed, timers run) the drift-free delay grew by one interval per tick
8661
+ later(Math.min(every, t0 + (n + 1) * every - t));
8565
8662
  send(s.action, { n, t });
8566
8663
  }
8567
8664
  else {
@@ -10634,13 +10731,13 @@ const addTimers = (v) => {
10634
10731
  return;
10635
10732
  timed.add(v);
10636
10733
  let own = v.timers;
10637
- const all = (st) => {
10734
+ const all = (st, c) => {
10638
10735
  const o = {}, f = own;
10639
10736
  if (typeof f == 'function')
10640
- Object.assign(o, f(st));
10737
+ Object.assign(o, f(st, c));
10641
10738
  else if (f && typeof f == 'object')
10642
10739
  for (const r in f)
10643
- o[r] = f[r](st);
10740
+ o[r] = f[r](st, c);
10644
10741
  const u = v.uses;
10645
10742
  for (const k in u) {
10646
10743
  const b = u[k], t = b?.timers?.(st?.[k] ?? b.state, b.options, k), rn = (a) => a in (b.model || {}) ? k + '.' + a : a;
@@ -12274,7 +12371,7 @@ function withUses(def, state) {
12274
12371
  * state reads from the cache ('success' with the cached data), else 'loading' ('idle' for a
12275
12372
  * falsy request), as the client's first paint does with the same (hydrated) cache
12276
12373
  */
12277
- function withResources(def, state) {
12374
+ function withResources(def, state, ctx) {
12278
12375
  state = withUses(def, state);
12279
12376
  const res = def && def.resources;
12280
12377
  if (!res || !state || typeof state !== 'object' || Array.isArray(state))
@@ -12285,7 +12382,7 @@ function withResources(def, state) {
12285
12382
  continue;
12286
12383
  let q;
12287
12384
  try {
12288
- q = typeof res[n] === 'function' ? res[n](state) : res[n];
12385
+ q = typeof res[n] === 'function' ? res[n](state, ctx) : res[n];
12289
12386
  }
12290
12387
  catch (_) { }
12291
12388
  const E = q && ssrCache ? ssrCache.get(ssrCache.key(q, 1)) : undefined;
@@ -12332,15 +12429,33 @@ function viewFailed(def, err, name) {
12332
12429
  }
12333
12430
  return vnode;
12334
12431
  }
12335
- function collectHead(def, state) {
12432
+ function collectHead(def, state, ctx) {
12336
12433
  const h = def && def.head;
12337
12434
  if (!heads || !h)
12338
12435
  return;
12339
12436
  try {
12340
- heads.push(typeof h === 'function' ? h(state) : h);
12437
+ heads.push(typeof h === 'function' ? h(state, ctx) : h);
12341
12438
  }
12342
12439
  catch (_) { }
12343
12440
  }
12441
+ /**
12442
+ * PLAN-13 F-1: the context a component sees (its parent's, plus its own entries computed on
12443
+ * `state`). The statics get it as on the client; for `resources` it is computed on the state
12444
+ * before the resources are filled in (a context entry can't read the component's own resource)
12445
+ */
12446
+ function contextOf(def, state, parent) {
12447
+ const own = def.context || {}, out = { ...parent };
12448
+ for (const key of Object.keys(own)) {
12449
+ const f = own[key];
12450
+ if (typeof f === 'function' && state != null) {
12451
+ try {
12452
+ out[key] = f(state);
12453
+ }
12454
+ catch (_) { }
12455
+ }
12456
+ }
12457
+ return out;
12458
+ }
12344
12459
  /**
12345
12460
  * Render a Sygnal component to an HTML string.
12346
12461
  *
@@ -12381,20 +12496,10 @@ function renderRoot(componentDef, options) {
12381
12496
  const uid = (options.uid || 'u').replace(/[^\w-]+/g, '_');
12382
12497
  const ownState = state !== undefined ? state : componentDef.initialState;
12383
12498
  // 5-5: the view sees its resources; the hydration script keeps the state as it was given
12384
- const resolvedState = withResources(componentDef, ownState);
12385
- collectHead(componentDef, resolvedState);
12499
+ const resolvedState = withResources(componentDef, ownState, contextOf(componentDef, ownState, context));
12386
12500
  // Build context: merge parent context with component's own context definitions
12387
- const componentContext = componentDef.context || {};
12388
- const mergedContext = { ...context };
12389
- for (const key of Object.keys(componentContext)) {
12390
- const contextFn = componentContext[key];
12391
- if (typeof contextFn === 'function' && resolvedState != null) {
12392
- try {
12393
- mergedContext[key] = contextFn(resolvedState);
12394
- }
12395
- catch (_) { }
12396
- }
12397
- }
12501
+ const mergedContext = contextOf(componentDef, resolvedState, context);
12502
+ collectHead(componentDef, resolvedState, mergedContext);
12398
12503
  // Call the view function
12399
12504
  let vnode;
12400
12505
  try {
@@ -12605,19 +12710,9 @@ function renderSubComponent(vnode, context, parentState, uid) {
12605
12710
  // State passed directly
12606
12711
  childState = stateProp;
12607
12712
  }
12608
- childState = withResources(componentDef, childState);
12713
+ childState = withResources(componentDef, childState, contextOf(componentDef, childState, context));
12609
12714
  // Build child context
12610
- const childContext = { ...context };
12611
- const componentContext = componentDef.context || {};
12612
- for (const key of Object.keys(componentContext)) {
12613
- const contextFn = componentContext[key];
12614
- if (typeof contextFn === 'function' && childState != null) {
12615
- try {
12616
- childContext[key] = contextFn(childState);
12617
- }
12618
- catch (_) { }
12619
- }
12620
- }
12715
+ const childContext = contextOf(componentDef, childState, context);
12621
12716
  // Extract slots from children
12622
12717
  const slots = {};
12623
12718
  const defaultSlotChildren = [];
@@ -12639,7 +12734,7 @@ function renderSubComponent(vnode, context, parentState, uid) {
12639
12734
  slots.default = [];
12640
12735
  slots.default.push(...defaultSlotChildren);
12641
12736
  }
12642
- collectHead(componentDef, childState);
12737
+ collectHead(componentDef, childState, childContext);
12643
12738
  // Call the view function
12644
12739
  let result;
12645
12740
  try {
@@ -12856,19 +12951,10 @@ function renderSwitchable(vnode, context, parentState, uid) {
12856
12951
  * Internal helper: render a component def to a VNode (not HTML string).
12857
12952
  */
12858
12953
  function renderToStringInternal(componentDef, state, context, uid) {
12859
- const resolvedState = withResources(componentDef, state !== undefined ? state : componentDef.initialState);
12860
- const componentContext = componentDef.context || {};
12861
- const mergedContext = { ...context };
12862
- for (const key of Object.keys(componentContext)) {
12863
- const contextFn = componentContext[key];
12864
- if (typeof contextFn === 'function' && resolvedState != null) {
12865
- try {
12866
- mergedContext[key] = contextFn(resolvedState);
12867
- }
12868
- catch (_) { }
12869
- }
12870
- }
12871
- collectHead(componentDef, resolvedState);
12954
+ const own = state !== undefined ? state : componentDef.initialState;
12955
+ const resolvedState = withResources(componentDef, own, contextOf(componentDef, own, context));
12956
+ const mergedContext = contextOf(componentDef, resolvedState, context);
12957
+ collectHead(componentDef, resolvedState, mergedContext);
12872
12958
  let vnode;
12873
12959
  try {
12874
12960
  vnode = componentDef({
@@ -15640,6 +15726,8 @@ const sockKey = (sse, url, arg) => (sse ? 'e' + !!(arg && arg.withCredentials) :
15640
15726
  * PLAN-3 2-C: the in-memory WebSocket / EventSource the fake's makeSocketDriver opens. The
15641
15727
  * harness drives it (t.open / t.push / t.drop); the driver's own close() is the app's close.
15642
15728
  */
15729
+ /** `url` without its query parameter `name` (PLAN-13 S-1: a resumed reconnect's id parameter) */
15730
+ const withoutParam = (url, name) => url.replace(new RegExp('([?&])' + name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + '=[^&#]*&?'), '$1').replace(/[?&](#|$)/, '$1');
15643
15731
  const fakeSocketClass = (sse, made) => class {
15644
15732
  constructor(url, arg) {
15645
15733
  this.readyState = 0;
@@ -15647,7 +15735,14 @@ const fakeSocketClass = (sse, made) => class {
15647
15735
  this.byApp = false;
15648
15736
  this.ls = {};
15649
15737
  this.url = String(url);
15650
- this.k = sockKey(sse, this.url, arg);
15738
+ // PLAN-13 S-1: a resumed SSE reconnect's URL has the id parameter (added, or replacing the
15739
+ // declared one): its key leaves that parameter out (`rn`: its name; made() compares the
15740
+ // connections' URLs without it too)
15741
+ const id = sse && arg?.lastEventId;
15742
+ const m = id && new RegExp('[?&]([^?&=]+)=' + encodeURIComponent(id).replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + '(?=&|#|$)').exec(this.url);
15743
+ if (m)
15744
+ this.rn = m[1];
15745
+ this.k = sockKey(sse, m ? withoutParam(this.url, m[1]) : this.url, arg);
15651
15746
  made(this);
15652
15747
  }
15653
15748
  send() { }
@@ -16246,7 +16341,7 @@ function renderComponent(componentDef, options = {}) {
16246
16341
  const w = { src: null, in$: xs$1.create(), conns: new Map(), sockets: [], sent: [], declaring: null };
16247
16342
  const made = (s) => {
16248
16343
  w.sockets.push(s);
16249
- const all = [...w.conns.values()].flatMap(m => [...m.values()]).filter(c => c.k == s.k);
16344
+ const all = [...w.conns.values()].flatMap(m => [...m.values()]).filter(c => (s.rn ? sockKey(true, withoutParam(c.url, s.rn), c.spec) : c.k) == s.k);
16250
16345
  if (w.declaring) {
16251
16346
  // a declaration opened it: the first new connection with its key (and, shared, the
16252
16347
  // other new shared ones)
@@ -16383,7 +16478,7 @@ function renderComponent(componentDef, options = {}) {
16383
16478
  names.push(k + '.' + a);
16384
16479
  // G-275: the caller's initialState is owned (the dev statics freeze leaves it alone). G-289: a
16385
16480
  // copy is marked, not the caller's object
16386
- const init = initialState !== undefined ? ownedCopy(initialState) : componentDef.initialState;
16481
+ let init = initialState !== undefined ? ownedCopy(initialState) : componentDef.initialState;
16387
16482
  // G-028: with no intent, model or initialState nothing would ever emit state, so the view
16388
16483
  // never renders. Leave intent/model unset so the component falls back to the same no-op
16389
16484
  // model run() uses, and renders.
@@ -16675,6 +16770,18 @@ function renderComponent(componentDef, options = {}) {
16675
16770
  },
16676
16771
  };
16677
16772
  };
16773
+ // PLAN-13 F-2: a root that declares `route` starts from the test URL's route: its initialState's
16774
+ // router.current() ran at module load, before the test's URL existed. The route entry's STATE
16775
+ // reducer is applied to the initial state (its other sinks, a guard's redirect, run once when the
16776
+ // router answers the declaration), so the first render and the statics see the page the test
16777
+ // starts on: no render of '/' and no request for it
16778
+ const re = rt && typeof componentDef.route == 'string' && model[componentDef.route];
16779
+ const seed = typeof re == 'function' ? re : re && typeof re == 'object' && typeof re.STATE == 'function' && re.STATE;
16780
+ if (seed && init !== undefined) {
16781
+ const v = seed(init, rt.r.current(), noop, { state: init, previousState: init });
16782
+ if (v !== undefined && !isAbort(v))
16783
+ init = v;
16784
+ }
16678
16785
  let sources, sinks;
16679
16786
  try {
16680
16787
  // the root runs with the test intent, model, initial state and name, and the drivers above.
@@ -17383,11 +17490,14 @@ function renderComponent(componentDef, options = {}) {
17383
17490
  };
17384
17491
  const open = (name, target) => sockCall('open', name, target, [0], sockOpen);
17385
17492
  const push = (name, data, target) => {
17386
- const o = target && typeof target == 'object' && !Array.isArray(target) && Object.keys(target).length && Object.keys(target).every(k => k == 'event' || k == 'connection') ? target : { connection: target };
17493
+ const o = target && typeof target == 'object' && !Array.isArray(target) && Object.keys(target).length && Object.keys(target).every(k => k == 'event' || k == 'connection' || k == 'id') ? target : { connection: target };
17387
17494
  const raw = typeof data == 'string' || (data && typeof data == 'object' && (data instanceof ArrayBuffer || ArrayBuffer.isView(data) || (typeof Blob != 'undefined' && data instanceof Blob))) ? data : JSON.stringify(data);
17388
17495
  const ev = o.event;
17389
17496
  return sockCall('push', name, o.connection, [1], s => {
17390
- const m = { type: ev || 'message', data: raw };
17497
+ // PLAN-13 S-1: `id` sets the event id; as in EventSource, lastEventId keeps the last one
17498
+ if (s.sse && o.id != null)
17499
+ s.lastId = String(o.id);
17500
+ const m = { type: ev || 'message', data: raw, ...(s.sse && { lastEventId: s.lastId ?? '' }) };
17391
17501
  if (!ev || ev == 'message')
17392
17502
  sockFire(s, 'message', m);
17393
17503
  if (s.sse)
@@ -26386,5 +26496,5 @@ function flattenSequentially(ins) {
26386
26496
  return new Stream(new FlattenSeqOperator(ins));
26387
26497
  }
26388
26498
 
26389
- export { ABORT, Collection, Composed, MainDOMSource, MockedDOMSource, Portal, Slot, Suspense, Switchable, Transition, VirtualCollection, abort, agentTools, agui, answers, anthropicMessages, askCard, byId, chat, chatCompletions, checkForm, choice, chromePrompt, classes, clearDiagnostics, clipboardSource, commandBar, compose, composeWith, concat, controls, createCommand, createElement, createInstallPrompt, createRef, createRef$, debounce, decide, defineBehavior, defineCatalog, defineComponent, defineWidget, delay, driverFromAsync, dropRepeats, emit$1 as emit, enableHMR, encodeOpenResponses, event, exactState, experimentalExposeWebMcp, fieldName, fieldNames, flattenConcurrently, flattenSequentially, focusInvalid, focusWithin, form, formErrors, formTool, fromAISDK, fromTree, geolocationSource, getDevTools, getDiagnostics, getField, hasField, intersectionSource, isSelected, jsonSchema, lazy, makeBrowserDriver, makeBrowserDriverWith, makeChatDriver, makeDOMDriver, makeDragDriver, makeFetchDriver, makeHeadDriver, makeMcpAppDriver, makeRouter, makeRouterDriver, makeServiceWorkerDriver, makeSocketDriver, makeTimerDriver, makeViewTransitionDOMDriver, mediaSource, messageText, mockDOMSource, noul, onDiagnostic, onlineSource, onlineStatus$, openResponses, openToolCalls, outputJsonSchema, pager, parseInput, persist, Portal as portal, processDrag, processForm, queryCache, removeNode, renderComponent, renderHead, renderToString, replyErrors, resizeSource, run, sampleCombine, score, selection, set, setField, sortable, storageSource, strictSchemas, throttle, thunk, toJsonSchema, toTree, toggle, uiMessageStream, undo, undoable, visibilitySource, withToolResults, xs$1 as xs };
26499
+ export { ABORT, Collection, Composed, MainDOMSource, MockedDOMSource, Portal, Slot, Suspense, Switchable, Transition, VirtualCollection, abort, agentTools, agui, answers, anthropicMessages, askCard, byId, chat, chatCompletions, checkForm, choice, chromePrompt, classes, clearDiagnostics, clipboardSource, commandBar, compose, composeWith, concat, controls, createCommand, createElement, createInstallPrompt, createRef, createRef$, debounce, decide, defineBehavior, defineCatalog, defineComponent, defineWidget, delay, driverFromAsync, dropRepeats, emit$1 as emit, enableHMR, encodeOpenResponses, event, exactState, experimentalExposeWebMcp, fieldName, fieldNames, flattenConcurrently, flattenSequentially, focusInvalid, focusWithin, form, formErrors, formTool, fromAISDK, fromTree, geolocationSource, getDevTools, getDiagnostics, getField, hasField, intersectionSource, isSelected, jsonSchema, lazy, makeBrowserDriver, makeBrowserDriverWith, makeChatDriver, makeDOMDriver, makeDragDriver, makeFetchDriver, makeHeadDriver, makeMcpAppDriver, makeReplies, makeRouter, makeRouterDriver, makeServiceWorkerDriver, makeSocketDriver, makeTimerDriver, makeViewTransitionDOMDriver, mediaSource, messageText, mockDOMSource, noul, onDiagnostic, onlineSource, onlineStatus$, openResponses, openToolCalls, outputJsonSchema, pager, parseInput, persist, Portal as portal, processDrag, processForm, queryCache, removeNode, renderComponent, renderHead, renderToString, replyErrors, resizeSource, run, sampleCombine, score, selection, senderOf, set, setField, sortable, storageSource, strictSchemas, throttle, thunk, toJsonSchema, toTree, toggle, uiMessageStream, undo, undoable, visibilitySource, withToolResults, xs$1 as xs };
26390
26500
  //# sourceMappingURL=index.esm.js.map