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.
- package/CHANGELOG.md +132 -0
- package/README.md +2 -0
- package/dist/astro/index.cjs.js +32 -9
- package/dist/astro/index.cjs.js.map +1 -1
- package/dist/astro/index.mjs +32 -9
- package/dist/astro/index.mjs.map +1 -1
- package/dist/astro/server.cjs.js.map +1 -1
- package/dist/astro/server.mjs.map +1 -1
- package/dist/devtools.cjs.js.map +1 -1
- package/dist/devtools.esm.js.map +1 -1
- package/dist/diagnostics.cjs.js +87 -4
- package/dist/diagnostics.cjs.js.map +1 -1
- package/dist/diagnostics.esm.js +86 -5
- package/dist/diagnostics.esm.js.map +1 -1
- package/dist/guide/accessibility.md +2 -0
- package/dist/guide/ai-chat.md +6 -6
- package/dist/guide/behaviors.md +1 -1
- package/dist/guide/element-commands.md +4 -2
- package/dist/guide/forms-reference.md +89 -3
- package/dist/guide/forms.md +8 -8
- package/dist/guide/http.md +77 -7
- package/dist/guide/migrating-to-7.md +314 -0
- package/dist/guide/recipes/ai-summarize.md +2 -1
- package/dist/guide/recipes/i18n.md +1 -1
- package/dist/guide/ui/tabs.md +5 -4
- package/dist/guide/ui/tooltip.md +24 -0
- package/dist/index.cjs.js +772 -318
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.d.ts +198 -31
- package/dist/index.esm.js +772 -318
- package/dist/index.esm.js.map +1 -1
- package/dist/sygnal.min.js +1 -1
- package/dist/sygnal.min.js.map +1 -1
- package/dist/ui.cjs.js +96 -11
- package/dist/ui.cjs.js.map +1 -1
- package/dist/ui.esm.js +96 -12
- package/dist/ui.esm.js.map +1 -1
- package/dist/vike/config/package.json +1 -1
- package/dist/vite/plugin.cjs.js +19 -9
- package/dist/vite/plugin.cjs.js.map +1 -1
- package/dist/vite/plugin.mjs +19 -9
- package/dist/vite/plugin.mjs.map +1 -1
- package/llms.txt +17 -16
- package/package.json +1 -1
- package/src/astro/index.d.ts +8 -0
- package/src/astro/index.ts +18 -0
- package/src/core/actions.ts +28 -21
- package/src/core/define.ts +2 -0
- package/src/core/hooks.ts +2 -2
- package/src/core/hosts/collection.ts +4 -3
- package/src/core/instance.ts +60 -16
- package/src/core/runtime.ts +5 -1
- package/src/extra/ai/chat/behavior.ts +3 -3
- package/src/extra/ai/commandBar.ts +3 -3
- package/src/extra/ai/compose/behavior.ts +3 -3
- package/src/extra/ai/transports/shared.ts +8 -2
- package/src/extra/behaviors.ts +21 -0
- package/src/extra/diagnostics/checks/fetch.ts +21 -1
- package/src/extra/diagnostics/checks/forms.ts +4 -3
- package/src/extra/diagnostics/checks/index.ts +1 -0
- package/src/extra/diagnostics/checks/next.ts +3 -0
- package/src/extra/diagnostics/checks/public.d.ts +12 -0
- package/src/extra/diagnostics/checks/timingAudit.ts +47 -0
- package/src/extra/diagnostics/codes.ts +13 -0
- package/src/extra/fetchDriver.ts +112 -7
- package/src/extra/fetchEventSource.ts +113 -0
- package/src/extra/form.ts +68 -26
- package/src/extra/formHelpers.ts +3 -1
- package/src/extra/lens.ts +3 -2
- package/src/extra/queryCache.ts +4 -3
- package/src/extra/socketDriver.ts +10 -4
- package/src/extra/testing.ts +68 -11
- package/src/index.d.ts +197 -30
- package/src/index.ts +1 -0
- package/src/shared.ts +3 -0
- package/src/ui/dialog.ts +3 -2
- package/src/ui/tooltips.ts +92 -0
- package/src/ui.d.ts +35 -0
- package/src/ui.ts +1 -0
- package/src/vite/graph.ts +11 -3
- package/src/vite/plugin.d.ts +7 -0
- package/src/vite/plugin.ts +10 -5
package/src/extra/testing.ts
CHANGED
|
@@ -252,6 +252,13 @@ export interface RenderOptions {
|
|
|
252
252
|
* Default: unchanged (off unless configureStrict(true) was called).
|
|
253
253
|
*/
|
|
254
254
|
strict?: boolean;
|
|
255
|
+
/**
|
|
256
|
+
* 7.0 timing audit (SYG257, info) while the component is rendered; restored on dispose():
|
|
257
|
+
* each output sink that reads a top-level state field its action's STATE changed is reported
|
|
258
|
+
* (it gets the new value since 7.0; 6.x gave it the old one). Requires 'sygnal/diagnostics'.
|
|
259
|
+
* Default: unchanged (off unless configureTimingAudit(true) was called).
|
|
260
|
+
*/
|
|
261
|
+
timingAudit?: boolean;
|
|
255
262
|
/**
|
|
256
263
|
* settle()'s quiet window in ms (default 20): settle() resolves once nothing in the tree has
|
|
257
264
|
* rendered, reduced or changed state for this long. A model `next('X', data, ms)` with a
|
|
@@ -454,6 +461,11 @@ export interface RenderResult {
|
|
|
454
461
|
* `status`/`body`), or an Error / message (a network failure: the fetch rejects with it).
|
|
455
462
|
*/
|
|
456
463
|
fail: (sinkName: string, error: any, target?: FakeReplyTarget) => Promise<void>;
|
|
464
|
+
/**
|
|
465
|
+
* PLAN-11 U-2: one progress step `{ loaded, total?, phase? }` of a pending request sent with
|
|
466
|
+
* `progress` (chosen as in respond()); it stays pending until respond()/fail().
|
|
467
|
+
*/
|
|
468
|
+
progress: (sinkName: string, step: {loaded: number; total?: number; phase?: 'upload' | 'download'}, target?: FakeReplyTarget) => Promise<void>;
|
|
457
469
|
/**
|
|
458
470
|
* PLAN-6 L-4: stream chunks into a pending request of the LLM fake (the sink named by `llmSink`,
|
|
459
471
|
* default 'LLM': the real chat driver over an in-memory transport). Chunks: strings (text),
|
|
@@ -550,9 +562,9 @@ export interface RenderResult {
|
|
|
550
562
|
* render (await t.ready() first). 4-A1: after `await` of any wait, it shows the state the wait
|
|
551
563
|
* resolved with (a later render is held back until the next macrotask).
|
|
552
564
|
*/
|
|
553
|
-
query: (selector: string) =>
|
|
565
|
+
query: <E extends Element = Element>(selector: string) => E | null;
|
|
554
566
|
/** Every element matching a selector in the rendered tree (Portals included), as query() */
|
|
555
|
-
queryAll: (selector: string) =>
|
|
567
|
+
queryAll: <E extends Element = Element>(selector: string) => E[];
|
|
556
568
|
/**
|
|
557
569
|
* PLAN-5 W-1: a widget's host by selector or control: `.props` (what the view passed it),
|
|
558
570
|
* `.instance` (`dom: 'real'`: what mount returned) and `.dispatch(name, detail)` (the event
|
|
@@ -1197,6 +1209,13 @@ const validMs = (v: any) => typeof v == 'number' && Number.isFinite(v) && v >= 0
|
|
|
1197
1209
|
// too, so its time is the clock's (Date may be left real by `toFake`), and its waits drive it.
|
|
1198
1210
|
const fakeClock = (): any => (setTimeout as any).clock;
|
|
1199
1211
|
const clockNow = (): number => { const c = fakeClock(); return c ? c.now : Date.now(); };
|
|
1212
|
+
/** PLAN-11 Q-3: ms until the next pending fake timer (0: none, or real timers) */
|
|
1213
|
+
const nextFakeTimer = (): number => {
|
|
1214
|
+
const c = fakeClock();
|
|
1215
|
+
let due = Infinity;
|
|
1216
|
+
try { Object.values(c?.timers || {}).forEach((t: any) => { if (t && t.callAt > c.now && t.callAt < due) due = t.callAt; }); } catch (_) {}
|
|
1217
|
+
return due < Infinity ? due - c.now : 0;
|
|
1218
|
+
};
|
|
1200
1219
|
/**
|
|
1201
1220
|
* Under fake timers, advance the clock timer by timer (nextAsync flushes promises around each
|
|
1202
1221
|
* one) until `p` settles. Every pending wait has a timer (its timeout, a quiet-window tick), so
|
|
@@ -1369,6 +1388,7 @@ const headFake = () => {
|
|
|
1369
1388
|
};
|
|
1370
1389
|
let savedConfig: ReturnType<typeof _getDiagnosticsConfig>;
|
|
1371
1390
|
let savedStrict: any;
|
|
1391
|
+
let savedAudit: any;
|
|
1372
1392
|
|
|
1373
1393
|
/*
|
|
1374
1394
|
* PLAN-4 GS-2, mock DOM: could the element a command targets have this method? The core runs any
|
|
@@ -1468,7 +1488,7 @@ export function renderComponent(
|
|
|
1468
1488
|
componentDef: any,
|
|
1469
1489
|
options: RenderOptions = {}
|
|
1470
1490
|
): RenderResult {
|
|
1471
|
-
const {initialState, mockConfig = {}, drivers = {}, diagnostics, strict, dom = 'mock', autoConnect = true, socketSink = 'WS', resourceSink = 'HTTP', llmSink = 'LLM', http: httpOptions, context: ancestors} = options;
|
|
1491
|
+
const {initialState, mockConfig = {}, drivers = {}, diagnostics, strict, timingAudit, dom = 'mock', autoConnect = true, socketSink = 'WS', resourceSink = 'HTTP', llmSink = 'LLM', http: httpOptions, context: ancestors} = options;
|
|
1472
1492
|
const {intent, model = {}} = componentDef;
|
|
1473
1493
|
// E4: real DOM mode
|
|
1474
1494
|
const real = dom == 'real';
|
|
@@ -1511,6 +1531,7 @@ export function renderComponent(
|
|
|
1511
1531
|
if (!active++) {
|
|
1512
1532
|
savedConfig = _getDiagnosticsConfig();
|
|
1513
1533
|
savedStrict = core.strict;
|
|
1534
|
+
savedAudit = core.timingAudit;
|
|
1514
1535
|
// G-051: forget the checks' report dedupe (once()), so a finding from an earlier test is
|
|
1515
1536
|
// reported again. Only that set: resetChecks() would also drop inspect()'s records of
|
|
1516
1537
|
// other live instances / apps.
|
|
@@ -1518,6 +1539,8 @@ export function renderComponent(
|
|
|
1518
1539
|
}
|
|
1519
1540
|
configureDiagnostics({mode: diagnostics || (prevMode == 'off' ? 'collect' : prevMode)});
|
|
1520
1541
|
if (strict !== undefined) core.strict = strict;
|
|
1542
|
+
// 7.0 timing audit (SYG257, 'sygnal/diagnostics'): same bridge flag as configureTimingAudit()
|
|
1543
|
+
if (timingAudit !== undefined) core.timingAudit = timingAudit;
|
|
1521
1544
|
// PLAN-4 GS-2: element commands run on the real DOM (jsdom gets the missing methods), and their
|
|
1522
1545
|
// SYG640/SYG641 are reported without the dev entry too
|
|
1523
1546
|
if (real && !domFakes) domFakes = fakeElementMethods(document.defaultView || globalThis);
|
|
@@ -1683,6 +1706,7 @@ export function renderComponent(
|
|
|
1683
1706
|
if (!--active) {
|
|
1684
1707
|
configureDiagnostics(savedConfig);
|
|
1685
1708
|
core.strict = savedStrict;
|
|
1709
|
+
core.timingAudit = savedAudit;
|
|
1686
1710
|
domFakes?.();
|
|
1687
1711
|
domFakes = undefined;
|
|
1688
1712
|
}
|
|
@@ -1716,7 +1740,8 @@ export function renderComponent(
|
|
|
1716
1740
|
// value: what t.requests lists (normalised); req: the request the driver got; res: the resource;
|
|
1717
1741
|
// pf: a { prefetch } fetch (5-5: answered into the cache, no reply)
|
|
1718
1742
|
// llm: the in-memory stream handle of an LLM fake request (PLAN-6 L-4)
|
|
1719
|
-
|
|
1743
|
+
// step: PLAN-11 U-2, the driver's progress callback of a request sent with `progress` (t.progress)
|
|
1744
|
+
type Pending = {value: any; req: any; category: any; res?: string; pf?: any; live: boolean; settle: (ok: boolean, v: any) => void; llm?: any; step?: (phase: string, loaded: number, total: number) => void};
|
|
1720
1745
|
type Fake = {select: any; errors: any; subs: Set<FakeSub>; at: (ns: any[]) => any; pending: Pending[]; in$: any; http: any; ws: Sock; llm?: any};
|
|
1721
1746
|
const fakes = new Map<string, Fake>();
|
|
1722
1747
|
// 5-3: the fake drivers' focus / online listeners (t.focus, t.online)
|
|
@@ -1749,14 +1774,22 @@ export function renderComponent(
|
|
|
1749
1774
|
const lin$ = xs.create();
|
|
1750
1775
|
llm = {in$: lin$, src: makeChatDriver({transport: tr, _frame: (f: () => void) => { frames.push(f); }})(lin$)};
|
|
1751
1776
|
}
|
|
1777
|
+
let memFetch: any;
|
|
1752
1778
|
const http = makeFetchDriver({
|
|
1753
1779
|
...httpOptions,
|
|
1780
|
+
// PLAN-11 U-2: a progress request is answered like any other (t.respond / t.fail); its
|
|
1781
|
+
// pending entry keeps the driver's progress callback for t.progress
|
|
1782
|
+
_progress: (url: string, init: any, step: any) => {
|
|
1783
|
+
const r = memFetch(url, init);
|
|
1784
|
+
pending[pending.length - 1].step = step;
|
|
1785
|
+
return r;
|
|
1786
|
+
},
|
|
1754
1787
|
// 5-3: focus / online come from t.focus() / t.online() only
|
|
1755
1788
|
_on: (f: any) => { signals.add(f); return () => signals.delete(f); },
|
|
1756
1789
|
_tap: (req: any, res?: string, pf?: any) => { tapped = [req, res, pf]; },
|
|
1757
1790
|
// D472: a batch / split request (a large decide()) is one fetch here, answered as one
|
|
1758
1791
|
_whole: true,
|
|
1759
|
-
fetch: (url: string, init: any) => new Promise((resolve, reject) => {
|
|
1792
|
+
fetch: memFetch = (url: string, init: any) => new Promise((resolve, reject) => {
|
|
1760
1793
|
const [req, res, pf] = tapped || [{url}];
|
|
1761
1794
|
tapped = undefined;
|
|
1762
1795
|
// G-171(1): a resource fetch is listed as { url, ...request, resource: name }; 5-5: a
|
|
@@ -1788,6 +1821,7 @@ export function renderComponent(
|
|
|
1788
1821
|
return {
|
|
1789
1822
|
select: (sel?: any) => xs.merge(hs.select(sel), own(false)(sel), sock.select(sel)),
|
|
1790
1823
|
errors: (sel?: any) => xs.merge(hs.errors(sel), own(true)(sel)),
|
|
1824
|
+
progress: (sel?: any) => hs.progress(sel),
|
|
1791
1825
|
subs, at, pending, in$, http, ws, llm,
|
|
1792
1826
|
// 5-3: the driver's matcher / inspection (the dev checks' SYG632, t.cache)
|
|
1793
1827
|
__matches: http.__matches, __inspect: http.__inspect,
|
|
@@ -2473,7 +2507,7 @@ export function renderComponent(
|
|
|
2473
2507
|
if (tg === undefined) return {match: cat, desc: catDesc, o};
|
|
2474
2508
|
if (typeof tg == 'string') {
|
|
2475
2509
|
// an ok/error action name, key, category, resource name or URL
|
|
2476
|
-
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};
|
|
2510
|
+
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};
|
|
2477
2511
|
}
|
|
2478
2512
|
if (typeof tg == 'function') {
|
|
2479
2513
|
return {match: r => { try { return cat(r) && !!tg(r.value); } catch (_) { return false; } }, desc: ` matching the predicate${catDesc}`, o};
|
|
@@ -2706,6 +2740,23 @@ export function renderComponent(
|
|
|
2706
2740
|
return {error: err, category, request: e?.req, status: status ?? x?.status, body};
|
|
2707
2741
|
});
|
|
2708
2742
|
|
|
2743
|
+
// PLAN-11 U-2: t.progress. One progress step of a pending request sent with `progress` (its
|
|
2744
|
+
// reply action, or progress() for `progress: true`); the request stays pending. Targets as
|
|
2745
|
+
// t.respond's; the phase defaults to 'upload' for a request with a body, else 'download'
|
|
2746
|
+
const progress = (name: string, step: any, opts?: FakeReplyTarget) => {
|
|
2747
|
+
throwFailure();
|
|
2748
|
+
if (drivers[name]) throw new Error(`[Sygnal] t.progress('${name}'): ${name} has a real driver (passed in drivers), so there is nothing to script`);
|
|
2749
|
+
if (!step || typeof step != 'object' || typeof step.loaded != 'number') throw new Error(`[Sygnal] t.progress('${name}', step): step is { loaded, total?, phase? } (got ${brief(step)})`);
|
|
2750
|
+
const tg = targetOf(opts);
|
|
2751
|
+
const what = `t.progress('${name}'${typeof opts == 'string' ? `, …, '${opts}'` : ''})`;
|
|
2752
|
+
const now = isReady && !inputs.length ? pick(name, tg) : undefined;
|
|
2753
|
+
if (now && !now.step) throw new Error(`[Sygnal] ${what}: the request ${brief(now.value)} has no progress: send it with progress: 'UPLOADING' (a reply action) or progress: true`);
|
|
2754
|
+
return scripted(() => pick(name, tg), w => noPending(what, name, tg, w), (e: Pending) => {
|
|
2755
|
+
if (!e.step) return new Error(`[Sygnal] ${what}: the request ${brief(e.value)} has no progress: send it with progress: 'UPLOADING' (a reply action) or progress: true`);
|
|
2756
|
+
const r = e.req || {};
|
|
2757
|
+
e.step(step.phase || (r.body !== undefined || r.json !== undefined || r.multipart !== undefined ? 'upload' : 'download'), step.loaded, step.total ?? 0);
|
|
2758
|
+
}, sendDue);
|
|
2759
|
+
};
|
|
2709
2760
|
// PLAN-6 L-4: t.stream. Targets as t.respond's (an action name or key, a partial request, a
|
|
2710
2761
|
// predicate, { request, nth }); `end: false` (the last argument, or in the target options)
|
|
2711
2762
|
// leaves the stream open. Chunks become ChatEvents pushed into the in-memory stream; the fake's
|
|
@@ -2904,11 +2955,11 @@ export function renderComponent(
|
|
|
2904
2955
|
};
|
|
2905
2956
|
// 6-B (G-185): on the mock DOM, MockElement snapshots of the latest rendered tree
|
|
2906
2957
|
const mockAll = (s: string): any[] => findAll(vtree, parse(norm(s)), [], []).map(c => mockOf(c, htmlOf));
|
|
2907
|
-
const queryAll = (s: string):
|
|
2958
|
+
const queryAll = <E extends Element = Element>(s: string): E[] => {
|
|
2908
2959
|
s = selOf(s);
|
|
2909
2960
|
notYet(`t.queryAll('${s}')`);
|
|
2910
2961
|
if (!real) return mockAll(s);
|
|
2911
|
-
return roots().flatMap(r => Array.from(r.querySelectorAll(s)));
|
|
2962
|
+
return roots().flatMap(r => Array.from(r.querySelectorAll<E>(s)));
|
|
2912
2963
|
};
|
|
2913
2964
|
const queryIn = (s: string): Element | null => {
|
|
2914
2965
|
for (const r of roots()) {
|
|
@@ -2917,11 +2968,11 @@ export function renderComponent(
|
|
|
2917
2968
|
}
|
|
2918
2969
|
return null;
|
|
2919
2970
|
};
|
|
2920
|
-
const query = (s: string):
|
|
2971
|
+
const query = <E extends Element = Element>(s: string): E | null => {
|
|
2921
2972
|
s = selOf(s);
|
|
2922
2973
|
notYet(`t.query('${s}')`);
|
|
2923
2974
|
if (!real) return mockAll(s)[0] ?? null;
|
|
2924
|
-
return queryIn(s);
|
|
2975
|
+
return queryIn(s) as E | null;
|
|
2925
2976
|
};
|
|
2926
2977
|
|
|
2927
2978
|
// PLAN-5 W-1: a widget host (selector or control): the props it was rendered with (the mock
|
|
@@ -3134,7 +3185,12 @@ export function renderComponent(
|
|
|
3134
3185
|
let msg = `${name} timed out after ${timeoutMs}ms.`;
|
|
3135
3186
|
const [why, pending] = explainNext(start);
|
|
3136
3187
|
msg += why;
|
|
3137
|
-
if (
|
|
3188
|
+
if (inputs.length) msg += ` ${inputs.length} simulated input(s) still pending.`;
|
|
3189
|
+
// PLAN-11 Q-3: on the fake clock a state a timer (TIMER driver, periodic, delay) brings
|
|
3190
|
+
// later than the timeout can't arrive in time: name the next pending timer
|
|
3191
|
+
const later = pending ? 0 : nextFakeTimer();
|
|
3192
|
+
if (later) 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.`;
|
|
3193
|
+
if (pending || later) msg += ` Wait longer: t.${name}(pred, ms), or the renderComponent timeoutMs option (default for every wait).`;
|
|
3138
3194
|
// G-065: a state from before the call matched; next() only looks at new ones
|
|
3139
3195
|
for (let i = from - 1; i >= 0; i--) {
|
|
3140
3196
|
if (test(i)) {
|
|
@@ -3317,6 +3373,7 @@ export function renderComponent(
|
|
|
3317
3373
|
requests,
|
|
3318
3374
|
respond,
|
|
3319
3375
|
fail,
|
|
3376
|
+
progress,
|
|
3320
3377
|
stream,
|
|
3321
3378
|
connections,
|
|
3322
3379
|
cache,
|
package/src/index.d.ts
CHANGED
|
@@ -53,9 +53,12 @@ type ComponentProps<STATE, PROPS, CONTEXT> = (
|
|
|
53
53
|
*/
|
|
54
54
|
export type UidFunction = (name?: string) => string
|
|
55
55
|
|
|
56
|
-
/**
|
|
56
|
+
/**
|
|
57
|
+
* The first argument of a component's view: its props plus `state`, `context`, `children`, `slots` and `uid`.
|
|
58
|
+
* PLAN-11 Q-1: PROPS = `any` is read as "any other props" (`any & X` would make `state` `any` too).
|
|
59
|
+
*/
|
|
57
60
|
export type ViewProps<STATE = any, PROPS = {}, CONTEXT = {}> =
|
|
58
|
-
PROPS & { state: STATE; context: CONTEXT; children?: JSX.Element | JSX.Element[]; slots?: Record<string, JSX.Element[]>; uid: UidFunction }
|
|
61
|
+
(0 extends (1 & PROPS) ? { [prop: string]: any } : PROPS) & { state: STATE; context: CONTEXT; children?: JSX.Element | JSX.Element[]; slots?: Record<string, JSX.Element[]>; uid: UidFunction }
|
|
59
62
|
|
|
60
63
|
/**
|
|
61
64
|
* The `state` prop a parent passes to a sub-component in JSX: the name of a field of the
|
|
@@ -90,13 +93,19 @@ type NextFunction<ACTIONS = any> = ACTIONS extends object
|
|
|
90
93
|
) => void
|
|
91
94
|
: (action: string, data?: any, delay?: number) => void
|
|
92
95
|
|
|
93
|
-
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* A reducer's 4th argument. 7.0: STATE runs first; every other sink gets the committed state as
|
|
99
|
+
* `state` (and as its 1st argument), with the state from before the action as `previousState`
|
|
100
|
+
* (in a STATE reducer both are the old state)
|
|
101
|
+
*/
|
|
102
|
+
type ReducerExtras<PROPS, CONTEXT, STATE = any> = PROPS & { state: STATE; previousState: STATE; context: CONTEXT; children?: JSX.Element | JSX.Element[]; slots?: Record<string, JSX.Element[]>; uid: UidFunction }
|
|
94
103
|
|
|
95
104
|
type Reducer<STATE, PROPS, ACTIONS = any, DATA = any, RETURN = any, CONTEXT = {}> = (
|
|
96
105
|
state: STATE,
|
|
97
106
|
args: DATA,
|
|
98
107
|
next: NextFunction<ACTIONS>,
|
|
99
|
-
props: ReducerExtras<PROPS, CONTEXT>
|
|
108
|
+
props: ReducerExtras<PROPS, CONTEXT, STATE>
|
|
100
109
|
) => RETURN | ABORT | undefined
|
|
101
110
|
|
|
102
111
|
export type ExactShape<EXPECTED, ACTUAL extends EXPECTED> = ACTUAL &
|
|
@@ -232,7 +241,7 @@ type AnySinkConstant =
|
|
|
232
241
|
* `props.signal` aborts on DISPOSE (undefined where AbortController is missing).
|
|
233
242
|
*/
|
|
234
243
|
type EffectReducer<STATE, PROPS, ACTIONS, DATA, CALCULATED, CONTEXT = {}> =
|
|
235
|
-
| ((state: STATE & CALCULATED, args: DATA, next: NextFunction<ACTIONS>, props: ReducerExtras<PROPS, CONTEXT> & { signal?: AbortSignal }) => void)
|
|
244
|
+
| ((state: STATE & CALCULATED, args: DATA, next: NextFunction<ACTIONS>, props: ReducerExtras<PROPS, CONTEXT, STATE & CALCULATED> & { signal?: AbortSignal }) => void)
|
|
236
245
|
|
|
237
246
|
type DefaultSinks<STATE, PROPS, ACTIONS, DATA, CALCULATED, SINK_RETURNS extends NonStateSinkReturns = {}, CONTEXT = {}> = {
|
|
238
247
|
STATE?: SinkValue<STATE, PROPS, ACTIONS, DATA, STATE, CALCULATED, CONTEXT>;
|
|
@@ -902,10 +911,11 @@ export function pager(options?: PagerOptions): Behavior<PagerState, PagerActions
|
|
|
902
911
|
export type FieldErrors = Record<string, string>
|
|
903
912
|
|
|
904
913
|
/** One field of a `form` slice: `state.form.fields.email`, `state.form.fields['addresses.7.city']`. */
|
|
905
|
-
export interface FormField {
|
|
914
|
+
export interface FormField<T = any> {
|
|
906
915
|
/** The field name, for `name={f.name}` (the path in `values`; rows of an array by id) */
|
|
907
916
|
name: string;
|
|
908
|
-
value:
|
|
917
|
+
/** The field's value: typed for a top-level field of a typed form (`fields.email.value: string`) */
|
|
918
|
+
value: T;
|
|
909
919
|
/** What to show: a server or check error at once, a schema error once the field is touched (per `show`) or after a submit; '' when none */
|
|
910
920
|
error: string;
|
|
911
921
|
/** `!!error`, for `aria-invalid={f.email.invalid}` */
|
|
@@ -942,12 +952,36 @@ export interface FormState<V = any> {
|
|
|
942
952
|
validating: boolean;
|
|
943
953
|
/** The schema has answered at least once since the start (or the last `form.RESET`) */
|
|
944
954
|
validated: boolean;
|
|
955
|
+
/**
|
|
956
|
+
* PLAN-11 (6.7): the submit request's last progress step, when it is sent with
|
|
957
|
+
* `progress: 'form.PROGRESS'`; null otherwise and after `form.DONE` / `form.ERRORS` / `form.RESET`
|
|
958
|
+
*/
|
|
959
|
+
progress: { phase: 'upload' | 'download'; loaded: number; total: number | null; fraction: number | null } | null;
|
|
960
|
+
/**
|
|
961
|
+
* PLAN-11 (6.7): how many times the form started over (`form.RESET`, `resetOnShow`). A file
|
|
962
|
+
* input can't be cleared through state: key it with this, `key={'avatar' + state.form.resets}`
|
|
963
|
+
*/
|
|
964
|
+
resets: number;
|
|
945
965
|
}
|
|
946
966
|
|
|
967
|
+
type FormIsAny<T> = 0 extends (1 & T) ? true : false
|
|
968
|
+
|
|
969
|
+
/**
|
|
970
|
+
* A form's `fields` (PLAN-11 F-1): the top-level fields of `values` by name, typed
|
|
971
|
+
* (`fields.email.value: string`), and nested paths ('addresses.7.city', 'addresses.7') with any
|
|
972
|
+
* value. A top-level name that isn't in `values` is a type error (a typo). Untyped values (any):
|
|
973
|
+
* `Record<string, FormField>`.
|
|
974
|
+
*/
|
|
975
|
+
export type FormFields<V = any> = FormIsAny<V> extends true
|
|
976
|
+
? Record<string, FormField>
|
|
977
|
+
: V extends object
|
|
978
|
+
? { [K in keyof V & string]-?: FormField<V[K]> } & { [path: `${string}.${string}`]: FormField }
|
|
979
|
+
: Record<string, FormField>
|
|
980
|
+
|
|
947
981
|
/** A `form` slice's calculated fields. */
|
|
948
|
-
export interface FormCalculated {
|
|
982
|
+
export interface FormCalculated<V = any> {
|
|
949
983
|
/** Every field of `values` by name (leaves, arrays, and array rows' fields by row id) */
|
|
950
|
-
fields:
|
|
984
|
+
fields: FormFields<V>;
|
|
951
985
|
/** No schema error and no failed check, as of the schema's last answer (false until its first; an async re-validation keeps the previous value) */
|
|
952
986
|
valid: boolean;
|
|
953
987
|
dirty: boolean;
|
|
@@ -963,15 +997,38 @@ export interface FormFieldCheck {
|
|
|
963
997
|
error?: (body: any) => string | false | null | undefined;
|
|
964
998
|
}
|
|
965
999
|
|
|
966
|
-
|
|
1000
|
+
/** Types only: the key of `FormOptions`' phantom output type (not exported, never set) */
|
|
1001
|
+
declare const formOutput: unique symbol
|
|
1002
|
+
|
|
1003
|
+
/** PLAN-11: the `request` option of form(): a request without reply actions (the form names them), or a function of the submit data and the host state */
|
|
1004
|
+
type FormRequestObject = Exclude<FetchRequest, string> & { url: string; ok?: never; error?: never }
|
|
1005
|
+
export type FormRequest<OUT = any> =
|
|
1006
|
+
| FormRequestObject
|
|
1007
|
+
| ((data: OUT, state: any) => FormRequestObject | typeof ABORT | null | undefined | false)
|
|
1008
|
+
|
|
1009
|
+
export interface FormOptions<V = any, OUT = any> {
|
|
1010
|
+
/** Phantom, types only: the schema's output (`FormOutput<typeof uses.form>`); never set */
|
|
1011
|
+
readonly [formOutput]?: OUT;
|
|
967
1012
|
/**
|
|
968
1013
|
* The start values; field names are paths in it (rows of an array of objects need an `id`, SYG236).
|
|
969
1014
|
* Or a function of the host component's state, for start values that come from state (a default
|
|
970
1015
|
* from the settings): called when the form starts, and each time it starts over (`resetOnShow`)
|
|
971
1016
|
*/
|
|
972
1017
|
values: V | ((state: any) => V);
|
|
973
|
-
/**
|
|
974
|
-
|
|
1018
|
+
/**
|
|
1019
|
+
* The host action a valid submit dispatches with the schema's output (SYG234 when it has no model entry). With an `http` sink in its entry the submit is pending until `form.DONE` / `form.ERRORS`; otherwise it is done at once.
|
|
1020
|
+
* Optional with `request` (then dispatched too, for state; its entry should send no request)
|
|
1021
|
+
*/
|
|
1022
|
+
submit?: string;
|
|
1023
|
+
/**
|
|
1024
|
+
* PLAN-11 (6.7): the form sends a valid submit itself on the `http` sink: a request, or a
|
|
1025
|
+
* function of the schema's output and the host's state (`(data, state) => ({ url: '/api/items/' + state.id, method: 'PUT' })`;
|
|
1026
|
+
* `ABORT` / falsy sends nothing and leaves the submit pending). The form adds the body
|
|
1027
|
+
* (`multipart: data` when a top-level value is a File or files, else `json: data`, unless the
|
|
1028
|
+
* request sets `json`, `multipart` or `body`), `progress: 'form.PROGRESS'` (unless set) and its own
|
|
1029
|
+
* `ok: 'form.DONE'`, `error: 'form.ERRORS'`
|
|
1030
|
+
*/
|
|
1031
|
+
request?: FormRequest<OUT>;
|
|
975
1032
|
/** Async checks by field name: run on blur and before a submit, which waits for them */
|
|
976
1033
|
check?: Record<string, FormFieldCheck>;
|
|
977
1034
|
/** When a schema error shows: after the field's blur (default), on input, or only after a submit */
|
|
@@ -1001,9 +1058,19 @@ export interface FormOptions<V = any> {
|
|
|
1001
1058
|
tool?: import('./ai.d').FormToolHandle;
|
|
1002
1059
|
}
|
|
1003
1060
|
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1061
|
+
/**
|
|
1062
|
+
* What `form.ERRORS` takes (the runtime's `replyErrors`): an error reply (a driver's
|
|
1063
|
+
* `{ error, status?, body?, request }` or `{ status, body }`, the body `{ errors }` or the map
|
|
1064
|
+
* itself), a map `{ name: message | messages }`, or a list of issues `[{ path, message }]`
|
|
1065
|
+
*/
|
|
1066
|
+
export type FormServerErrors =
|
|
1067
|
+
| Record<string, string | ReadonlyArray<string>>
|
|
1068
|
+
| ReadonlyArray<{ message: string; path?: ReadonlyArray<any> }>
|
|
1069
|
+
| { status?: number; body?: any; error?: any; request?: any }
|
|
1070
|
+
|
|
1071
|
+
export interface FormActions<V = any> {
|
|
1072
|
+
/** A field changed (the form element's input events): `{ name, value }`; a checkbox gives `checked` as `value` and its own value as `item` (on an array field: added or removed); a file input its File (`multiple`: File[]; none: null) */
|
|
1073
|
+
CHANGE: { name: string; value: any; item?: any; file?: boolean };
|
|
1007
1074
|
/**
|
|
1008
1075
|
* Several fields at once, by field name (`{ values: { name: 'Dana', 'addresses.7.city': 'Hilo' } }`),
|
|
1009
1076
|
* as if typed: one validation, `touched` as for CHANGE, their server errors and check results
|
|
@@ -1012,20 +1079,49 @@ export interface FormActions {
|
|
|
1012
1079
|
SET: { values: Record<string, any> };
|
|
1013
1080
|
/** A field lost focus (focusout): its name */
|
|
1014
1081
|
BLUR: string;
|
|
1015
|
-
/** The form element's submit (default prevented) */
|
|
1016
|
-
SUBMIT:
|
|
1082
|
+
/** The form element's submit (default prevented); its data is ignored (a host may dispatch it from a button) */
|
|
1083
|
+
SUBMIT: unknown;
|
|
1017
1084
|
/** Adds a row to an array field (with the next id): `{ field: 'addresses', value: { street: '' } }` */
|
|
1018
1085
|
ADD: { field: string; value?: Record<string, any> };
|
|
1019
1086
|
/** Removes a row by id: `{ field: 'addresses', id }` */
|
|
1020
1087
|
REMOVE: { field: string; id: any };
|
|
1021
|
-
/** Server errors: an error reply (`{ status, body: { errors } }`)
|
|
1022
|
-
ERRORS:
|
|
1023
|
-
/** The submit was saved: `initial` becomes `values
|
|
1024
|
-
DONE:
|
|
1025
|
-
/**
|
|
1026
|
-
|
|
1088
|
+
/** Server errors: an error reply (`{ status, body: { errors } }`), a map `{ name: message }` or a list of issues; focuses the first */
|
|
1089
|
+
ERRORS: FormServerErrors;
|
|
1090
|
+
/** The submit was saved: `initial` becomes `values`. Its data (the reply) is ignored */
|
|
1091
|
+
DONE: unknown;
|
|
1092
|
+
/** PLAN-11: a progress step of the submit request (`progress: 'form.PROGRESS'` on it): kept as `progress` while submitting */
|
|
1093
|
+
PROGRESS: FetchProgress;
|
|
1094
|
+
/** Back to `initial`, or to the values given (whole values, not merged; anything but a plain object, such as a click event, means `initial`) */
|
|
1095
|
+
RESET: V | null | undefined;
|
|
1027
1096
|
}
|
|
1028
1097
|
|
|
1098
|
+
/**
|
|
1099
|
+
* The schema's input type (Standard Schema `~standard.types.input`; zod, valibot and arktype declare
|
|
1100
|
+
* it); `any` for a schema that doesn't declare `types`
|
|
1101
|
+
*/
|
|
1102
|
+
export type SchemaInput<S> = S extends { readonly '~standard': { readonly types?: infer T } }
|
|
1103
|
+
? [NonNullable<T>] extends [{ readonly input: infer I }] ? I : any
|
|
1104
|
+
: any
|
|
1105
|
+
|
|
1106
|
+
type FormSchemaOutput<S> = S extends { readonly '~standard': { readonly types?: infer T } }
|
|
1107
|
+
? [NonNullable<T>] extends [{ readonly output: infer O }] ? O : any
|
|
1108
|
+
: any
|
|
1109
|
+
|
|
1110
|
+
/** The values type of `form(schema, { values })`: the schema's input when it declares one, else what `values` gives */
|
|
1111
|
+
type FormValuesOf<V, S> = FormIsAny<SchemaInput<S>> extends true ? V : SchemaInput<S>
|
|
1112
|
+
|
|
1113
|
+
/** What `form()` returns: a behavior whose types carry the values (V) and the schema's output (OUT, the submit payload) */
|
|
1114
|
+
export type FormBehavior<V = any, OUT = any> = Behavior<FormState<V>, FormActions<V>, FormCalculated<V>, FormOptions<V, OUT>>
|
|
1115
|
+
|
|
1116
|
+
/**
|
|
1117
|
+
* The data the host's submit action gets (the schema's output) from a `form()` use:
|
|
1118
|
+
* `SIGN_UP: { HTTP: (state, data: FormOutput<typeof uses.form>) => ... }`. Or from the schema:
|
|
1119
|
+
* `SchemaOutput<typeof signupSchema>`. `any` for a schema that doesn't declare `types`.
|
|
1120
|
+
*/
|
|
1121
|
+
export type FormOutput<F> = F extends Behavior<any, any, any, infer OPTIONS>
|
|
1122
|
+
? OPTIONS extends { readonly [formOutput]?: infer O } ? (unknown extends O ? any : O) : any
|
|
1123
|
+
: any
|
|
1124
|
+
|
|
1029
1125
|
/**
|
|
1030
1126
|
* A form with validation (PLAN-5 F-1), any Standard Schema validator (zod, valibot, arktype, a
|
|
1031
1127
|
* hand-written object):
|
|
@@ -1040,7 +1136,11 @@ export interface FormActions {
|
|
|
1040
1136
|
* that sends nothing (STATE, PARENT, EVENTS) is done at once. Throws SYG231 when
|
|
1041
1137
|
* `schema` isn't a Standard Schema.
|
|
1042
1138
|
*/
|
|
1043
|
-
export function form<V = any
|
|
1139
|
+
export function form<V = any, S extends StandardSchemaLike = StandardSchemaLike>(
|
|
1140
|
+
schema: S,
|
|
1141
|
+
options: FormOptions<V, FormSchemaOutput<S>> & { values: FormIsAny<SchemaInput<S>> extends true ? unknown : SchemaInput<S> | ((state: any) => SchemaInput<S>) }
|
|
1142
|
+
& ({ submit: string } | { request: FormRequest<FormSchemaOutput<S>> })
|
|
1143
|
+
): FormBehavior<FormValuesOf<V, S>, FormSchemaOutput<S>>
|
|
1044
1144
|
|
|
1045
1145
|
/** A Standard Schema's result for `values`: errors by field name, and the output when there are none. A Promise for an async schema. */
|
|
1046
1146
|
export function checkForm(schema: StandardSchemaLike, values: any): { errors: FieldErrors; value: any } | Promise<{ errors: FieldErrors; value: any }>
|
|
@@ -2292,6 +2392,9 @@ export type FetchInit = {
|
|
|
2292
2392
|
* descendants'): two instances, or Collection items, using the same category never see or
|
|
2293
2393
|
* cancel each other's requests. The root component sees every reply.
|
|
2294
2394
|
*/
|
|
2395
|
+
/** PLAN-11: one value of a request's `multipart` body */
|
|
2396
|
+
export type MultipartValue = Blob | string | number | boolean | null | undefined | Record<string, any>
|
|
2397
|
+
|
|
2295
2398
|
export type FetchRequest = string | {
|
|
2296
2399
|
/** Request URL (prefixed with the driver's `baseUrl`) */
|
|
2297
2400
|
url: string;
|
|
@@ -2317,8 +2420,24 @@ export type FetchRequest = string | {
|
|
|
2317
2420
|
query?: Record<string, string | number | boolean | null | undefined | Array<string | number | boolean | null | undefined>>;
|
|
2318
2421
|
/** Sent as JSON.stringify(json), with `Content-Type: application/json` unless set */
|
|
2319
2422
|
json?: any;
|
|
2423
|
+
/**
|
|
2424
|
+
* PLAN-11 (6.7): a multipart/form-data body, written as data: `{ file: state.file, title: state.title }`.
|
|
2425
|
+
* A File or Blob is a file part, an array repeats the name (`files: [a, b]`), null/undefined are
|
|
2426
|
+
* skipped, an object is sent as JSON text, anything else as a string. The browser sets the
|
|
2427
|
+
* content-type (with its boundary); a `content-type` header is dropped. POST by default
|
|
2428
|
+
*/
|
|
2429
|
+
multipart?: Record<string, MultipartValue | ReadonlyArray<MultipartValue>>;
|
|
2320
2430
|
/** Raw body (string, FormData, Blob, ...) */
|
|
2321
2431
|
body?: any;
|
|
2432
|
+
/**
|
|
2433
|
+
* PLAN-11 (6.7): report upload and download progress. An action name: each step arrives as that
|
|
2434
|
+
* action of the sending instance (`UPLOADING: (state, { phase, fraction }) => ...`); `true`: on
|
|
2435
|
+
* `HTTP.progress(category?)`. Data: FetchProgress. The request then goes over XMLHttpRequest
|
|
2436
|
+
* (the only way to see upload progress in every browser); fetch-only `init` options
|
|
2437
|
+
* (`keepalive`, `priority`, `cache`, `redirect`, `integrity`, `mode`, `referrerPolicy`) don't
|
|
2438
|
+
* apply (SYG636). Not for resources or `batch`. Without XMLHttpRequest (Node): no steps
|
|
2439
|
+
*/
|
|
2440
|
+
progress?: string | true;
|
|
2322
2441
|
/**
|
|
2323
2442
|
* Latest only: sending this request aborts this component's requests still in flight in the
|
|
2324
2443
|
* same category; their responses and errors are never delivered. Default: the driver's `latest`
|
|
@@ -2433,10 +2552,15 @@ export type FetchRetry = SocketReconnect & {
|
|
|
2433
2552
|
count?: number;
|
|
2434
2553
|
}
|
|
2435
2554
|
|
|
2436
|
-
/**
|
|
2437
|
-
|
|
2555
|
+
/**
|
|
2556
|
+
* Any Standard Schema (https://standardschema.dev): an object with `~standard.validate`. `IN` / `OUT`
|
|
2557
|
+
* are its declared `~standard.types` (input / output; zod, valibot, arktype declare them), read by
|
|
2558
|
+
* `form()`, `SchemaInput` and `SchemaOutput`; a schema without `types` is `StandardSchemaLike<any>`.
|
|
2559
|
+
*/
|
|
2560
|
+
export type StandardSchemaLike<IN = any, OUT = IN> = {
|
|
2438
2561
|
readonly '~standard': {
|
|
2439
2562
|
validate: (value: unknown) => {value?: any; issues?: ReadonlyArray<{message: string; path?: ReadonlyArray<any>}>} | Promise<{value?: any; issues?: ReadonlyArray<{message: string; path?: ReadonlyArray<any>}>}>;
|
|
2563
|
+
readonly types?: { readonly input: IN; readonly output: OUT } | undefined;
|
|
2440
2564
|
[key: string]: any;
|
|
2441
2565
|
};
|
|
2442
2566
|
}
|
|
@@ -2563,11 +2687,30 @@ export type FetchError<REQUEST = any> = {
|
|
|
2563
2687
|
body?: any;
|
|
2564
2688
|
}
|
|
2565
2689
|
|
|
2690
|
+
/**
|
|
2691
|
+
* PLAN-11 (6.7): one progress step of a request sent with `progress`. Steps come at most every
|
|
2692
|
+
* 50 ms per phase, always including the first and the last (`fraction: 1`). A request with a
|
|
2693
|
+
* body reports 'upload' then 'download'; a GET only 'download'. A retry starts again at 0.
|
|
2694
|
+
*/
|
|
2695
|
+
export type FetchProgress<REQUEST = any> = {
|
|
2696
|
+
phase: 'upload' | 'download';
|
|
2697
|
+
/** Bytes sent ('upload') or received ('download') so far */
|
|
2698
|
+
loaded: number;
|
|
2699
|
+
/** Total bytes, or null when unknown (a download without Content-Length) */
|
|
2700
|
+
total: number | null;
|
|
2701
|
+
/** loaded / total (0..1), or null when the total is unknown */
|
|
2702
|
+
fraction: number | null;
|
|
2703
|
+
category: string | undefined;
|
|
2704
|
+
request: REQUEST;
|
|
2705
|
+
}
|
|
2706
|
+
|
|
2566
2707
|
export type FetchSource<VALUE = any> = {
|
|
2567
2708
|
/** 2xx responses: all of them, one category, or those a predicate accepts */
|
|
2568
2709
|
select: (category?: string | ((response: FetchResponse<VALUE>) => boolean)) => Stream<FetchResponse<VALUE>>
|
|
2569
2710
|
/** Failures. Filters like select(). While nothing listens, failures are console.error'd */
|
|
2570
2711
|
errors: (category?: string | ((failure: FetchError) => boolean)) => Stream<FetchError>
|
|
2712
|
+
/** PLAN-11: progress steps of requests sent with `progress: true`. Filters like select() */
|
|
2713
|
+
progress: (category?: string | ((step: FetchProgress) => boolean)) => Stream<FetchProgress>
|
|
2571
2714
|
}
|
|
2572
2715
|
|
|
2573
2716
|
export type FetchDriverOptions = {
|
|
@@ -2586,8 +2729,10 @@ export type FetchDriverOptions = {
|
|
|
2586
2729
|
timeoutMs?: number;
|
|
2587
2730
|
/** Default `parse` for every request. Default 'auto' */
|
|
2588
2731
|
parse?: 'auto' | 'json' | 'text' | 'response' | ((response: Response) => any);
|
|
2589
|
-
/** The fetch implementation. Default: `globalThis.fetch`, read at each request (so test stubs apply) */
|
|
2732
|
+
/** The fetch implementation. Default: `globalThis.fetch`, read at each request (so test stubs apply). Not used for `progress` requests */
|
|
2590
2733
|
fetch?: (input: string, init?: any) => Promise<any>;
|
|
2734
|
+
/** PLAN-11: the XMLHttpRequest class for `progress` requests. Default: the global one, read at each request */
|
|
2735
|
+
XMLHttpRequest?: any;
|
|
2591
2736
|
/**
|
|
2592
2737
|
* PLAN-3 D88: the query cache, off by default: `cache: queryCache({ staleTime })`. On:
|
|
2593
2738
|
* resources' GET/HEAD replies are cached (stale-while-revalidate: cached data shows at once,
|
|
@@ -2670,6 +2815,12 @@ export type SseConnection = SocketActions & {
|
|
|
2670
2815
|
/** URL (after `baseUrl`) */
|
|
2671
2816
|
sse: string;
|
|
2672
2817
|
withCredentials?: boolean;
|
|
2818
|
+
/**
|
|
2819
|
+
* PLAN-11 (6.7): request headers, e.g. `{ authorization: 'Bearer ' + state.token }`. The native
|
|
2820
|
+
* EventSource can't send headers, so a connection with them is read with `fetch` (same events,
|
|
2821
|
+
* reconnects with `Last-Event-ID`). A changed header (a new token in state) reconnects
|
|
2822
|
+
*/
|
|
2823
|
+
headers?: Record<string, string>;
|
|
2673
2824
|
/** Named events → actions: `{ 'price-update': 'PRICE' }` (data JSON-parsed when it parses) */
|
|
2674
2825
|
events?: Record<string, string>;
|
|
2675
2826
|
}
|
|
@@ -2722,8 +2873,10 @@ export type SocketDriverOptions = {
|
|
|
2722
2873
|
queueLimit?: number;
|
|
2723
2874
|
/** The WebSocket class. Default: `globalThis.WebSocket`, read at connect time (so test stubs apply) */
|
|
2724
2875
|
WebSocket?: any;
|
|
2725
|
-
/** The EventSource class. Default: `globalThis.EventSource`, read at connect time */
|
|
2876
|
+
/** The EventSource class. Default: `globalThis.EventSource`, read at connect time (an `sse` connection with `headers`: a fetch-based one, unless this is given) */
|
|
2726
2877
|
EventSource?: any;
|
|
2878
|
+
/** PLAN-11: the fetch for `sse` connections with `headers`. Default: `globalThis.fetch` */
|
|
2879
|
+
fetch?: (input: string, init?: any) => Promise<any>;
|
|
2727
2880
|
}
|
|
2728
2881
|
|
|
2729
2882
|
/**
|
|
@@ -3147,6 +3300,12 @@ export type FakeCacheEntry = {
|
|
|
3147
3300
|
}
|
|
3148
3301
|
|
|
3149
3302
|
export interface RenderOptions {
|
|
3303
|
+
/**
|
|
3304
|
+
* 7.0 timing audit (SYG257, info): report each output sink that reads a top-level state field
|
|
3305
|
+
* its action's STATE changed (it gets the new value since 7.0; 6.x gave it the old one). For
|
|
3306
|
+
* migrating 6.x code; default off (configureTimingAudit() from 'sygnal/diagnostics' sets it for all)
|
|
3307
|
+
*/
|
|
3308
|
+
timingAudit?: boolean;
|
|
3150
3309
|
/** Override initial state (defaults to component's .initialState) */
|
|
3151
3310
|
initialState?: any;
|
|
3152
3311
|
/**
|
|
@@ -3472,6 +3631,14 @@ export interface RenderResult<STATE = any> {
|
|
|
3472
3631
|
* throws (the driver would only log it, SYG678).
|
|
3473
3632
|
*/
|
|
3474
3633
|
fail: (sinkName: string, error: any, target?: string | FakeReplyOptions | Record<string, any> | ((request: any) => boolean)) => Promise<void>;
|
|
3634
|
+
/**
|
|
3635
|
+
* PLAN-11 (6.7): one progress step of a pending request sent with `progress` (chosen as in
|
|
3636
|
+
* respond; `'UPLOADING'` matches its progress action too): `t.progress('HTTP', { loaded: 50, total: 100 }, 'UPLOADING')`.
|
|
3637
|
+
* The component gets FetchProgress (`fraction: 0.5`) as that action, or on `HTTP.progress()`
|
|
3638
|
+
* for `progress: true`. `phase` defaults to 'upload' for a request with a body, else
|
|
3639
|
+
* 'download'. The request stays pending until respond()/fail(). Resolves once reduced and rendered.
|
|
3640
|
+
*/
|
|
3641
|
+
progress: (sinkName: string, step: { loaded: number; total?: number; phase?: 'upload' | 'download' }, target?: string | FakeReplyOptions | Record<string, any> | ((request: any) => boolean)) => Promise<void>;
|
|
3475
3642
|
/**
|
|
3476
3643
|
* PLAN-6 L-4: stream into a pending request of the LLM fake (the sink named by `llmSink`,
|
|
3477
3644
|
* default 'LLM': the real makeChatDriver over an in-memory transport). Chunks: strings (text),
|
|
@@ -3603,13 +3770,13 @@ export interface RenderResult<STATE = any> {
|
|
|
3603
3770
|
* `await t.next(pred)` / `waitForState` / `settle()` / `ready()` it shows the state the wait resolved with.
|
|
3604
3771
|
*/
|
|
3605
3772
|
query: {
|
|
3606
|
-
(selector: string):
|
|
3773
|
+
<E extends Element = Element>(selector: string): E | null;
|
|
3607
3774
|
/** A control's element (`t.query(Draft)` is an HTMLInputElement for an 'input' control), or null */
|
|
3608
3775
|
<CONTROL extends AnyControl>(control: CONTROL): ControlElementOf<CONTROL> | null;
|
|
3609
3776
|
};
|
|
3610
3777
|
/** Every element matching a selector (or a control) in the rendered tree (Portals included), as query() */
|
|
3611
3778
|
queryAll: {
|
|
3612
|
-
(selector: string):
|
|
3779
|
+
<E extends Element = Element>(selector: string): E[];
|
|
3613
3780
|
<CONTROL extends AnyControl>(control: CONTROL): Array<ControlElementOf<CONTROL>>;
|
|
3614
3781
|
};
|
|
3615
3782
|
/**
|
package/src/index.ts
CHANGED
|
@@ -86,3 +86,4 @@ export * from './cycle/dom/index'
|
|
|
86
86
|
// and ESM classes tree-shake when an app does not use them.
|
|
87
87
|
export { concat, debounce, throttle, delay, dropRepeats, sampleCombine } from './extra/xstreamExtras'
|
|
88
88
|
export { flattenConcurrently, flattenSequentially } from './extra/flatten'
|
|
89
|
+
|
package/src/shared.ts
CHANGED
|
@@ -55,3 +55,6 @@ export const NOT_SINK = /^(__|(props|children|dispose|commands)\$$|(CHILD|PARENT
|
|
|
55
55
|
*/
|
|
56
56
|
export const abortReason: {r?: string} = {}
|
|
57
57
|
export const abort = (reason?: string): typeof ABORT => (abortReason.r = reason, ABORT)
|
|
58
|
+
|
|
59
|
+
/** Private semantic binding identity, shared by built-in helpers and the runtime. */
|
|
60
|
+
export const BINDING = /*#__PURE__*/ Symbol.for('sygnal.binding')
|
package/src/ui/dialog.ts
CHANGED
|
@@ -110,9 +110,10 @@ const base = /*#__PURE__*/ defineBehavior({
|
|
|
110
110
|
TOGGLED: (s: any, open: boolean) => (open && !s.open ? {...s, open, returnValue: ''} : ABORT),
|
|
111
111
|
CLOSED: {
|
|
112
112
|
STATE: (s: any, returnValue: string) => (!s.open && s.returnValue === returnValue ? ABORT : {...s, open: false, returnValue}),
|
|
113
|
-
ELEMENT: (s: any, _d: any, _n: any,
|
|
113
|
+
ELEMENT: (s: any, _d: any, _n: any, p: any, o: any, k: string) => {
|
|
114
114
|
const f = o.returnFocus ?? true, sel = f === true ? o.trigger : f
|
|
115
|
-
|
|
115
|
+
// only a dialog that was open before this close returns the focus
|
|
116
|
+
if (!(p?.previousState ? p.previousState[k] : s).open || !sel) return ABORT
|
|
116
117
|
// true: the element that opened it (OPEN's close listener, G-430), else the trigger; a
|
|
117
118
|
// selector: that element (each only when the focus was lost)
|
|
118
119
|
return {focus: refocus(sel)}
|