sygnal 6.5.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 +156 -0
- package/README.md +2 -0
- package/dist/astro/index.cjs.js +423 -27
- package/dist/astro/index.cjs.js.map +1 -1
- package/dist/astro/index.mjs +423 -27
- 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 +141 -3
- package/dist/devtools.cjs.js.map +1 -1
- package/dist/devtools.esm.js +141 -4
- package/dist/devtools.esm.js.map +1 -1
- package/dist/diagnostics.cjs.js +169 -14
- package/dist/diagnostics.cjs.js.map +1 -1
- package/dist/diagnostics.esm.js +168 -15
- 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 +781 -320
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.d.ts +319 -43
- package/dist/index.esm.js +781 -320
- 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/vike/onRenderClient.cjs.js +63 -43
- package/dist/vike/onRenderClient.cjs.js.map +1 -1
- package/dist/vike/onRenderClient.mjs +63 -43
- package/dist/vike/onRenderClient.mjs.map +1 -1
- package/dist/vike/onRenderHtml.cjs.js +21 -25
- package/dist/vike/onRenderHtml.cjs.js.map +1 -1
- package/dist/vike/onRenderHtml.mjs +21 -25
- package/dist/vike/onRenderHtml.mjs.map +1 -1
- package/dist/vite/plugin.cjs.js +410 -27
- package/dist/vite/plugin.cjs.js.map +1 -1
- package/dist/vite/plugin.mjs +410 -27
- 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 +8 -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/core/view.ts +2 -0
- package/src/cycle/dom/utils.ts +4 -2
- package/src/devtools.d.ts +13 -0
- package/src/devtools.ts +3 -0
- 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/devGraph.ts +111 -0
- package/src/extra/devMcp.ts +1 -1
- package/src/extra/devtools.ts +18 -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 +2 -1
- package/src/extra/diagnostics/checks/inspect.ts +68 -6
- package/src/extra/diagnostics/checks/next.ts +3 -0
- package/src/extra/diagnostics/checks/public.d.ts +133 -12
- package/src/extra/diagnostics/checks/timingAudit.ts +47 -0
- package/src/extra/diagnostics/codes.ts +19 -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 +199 -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/vike/onRenderClient.ts +44 -46
- package/src/vike/onRenderHtml.ts +5 -26
- package/src/vike/shell.ts +33 -0
- package/src/vite/graph.ts +255 -0
- package/src/vite/mcp.ts +3 -3
- package/src/vite/plugin.d.ts +19 -0
- package/src/vite/plugin.ts +144 -25
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 }>
|
|
@@ -1811,6 +1911,8 @@ export type {
|
|
|
1811
1911
|
InspectRecentAction,
|
|
1812
1912
|
InspectCommand,
|
|
1813
1913
|
InspectTimer,
|
|
1914
|
+
InspectLocation,
|
|
1915
|
+
InspectChildSelect,
|
|
1814
1916
|
} from './extra/diagnostics/checks/public'
|
|
1815
1917
|
|
|
1816
1918
|
/**
|
|
@@ -2290,6 +2392,9 @@ export type FetchInit = {
|
|
|
2290
2392
|
* descendants'): two instances, or Collection items, using the same category never see or
|
|
2291
2393
|
* cancel each other's requests. The root component sees every reply.
|
|
2292
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
|
+
|
|
2293
2398
|
export type FetchRequest = string | {
|
|
2294
2399
|
/** Request URL (prefixed with the driver's `baseUrl`) */
|
|
2295
2400
|
url: string;
|
|
@@ -2315,8 +2420,24 @@ export type FetchRequest = string | {
|
|
|
2315
2420
|
query?: Record<string, string | number | boolean | null | undefined | Array<string | number | boolean | null | undefined>>;
|
|
2316
2421
|
/** Sent as JSON.stringify(json), with `Content-Type: application/json` unless set */
|
|
2317
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>>;
|
|
2318
2430
|
/** Raw body (string, FormData, Blob, ...) */
|
|
2319
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;
|
|
2320
2441
|
/**
|
|
2321
2442
|
* Latest only: sending this request aborts this component's requests still in flight in the
|
|
2322
2443
|
* same category; their responses and errors are never delivered. Default: the driver's `latest`
|
|
@@ -2431,10 +2552,15 @@ export type FetchRetry = SocketReconnect & {
|
|
|
2431
2552
|
count?: number;
|
|
2432
2553
|
}
|
|
2433
2554
|
|
|
2434
|
-
/**
|
|
2435
|
-
|
|
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> = {
|
|
2436
2561
|
readonly '~standard': {
|
|
2437
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;
|
|
2438
2564
|
[key: string]: any;
|
|
2439
2565
|
};
|
|
2440
2566
|
}
|
|
@@ -2561,11 +2687,30 @@ export type FetchError<REQUEST = any> = {
|
|
|
2561
2687
|
body?: any;
|
|
2562
2688
|
}
|
|
2563
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
|
+
|
|
2564
2707
|
export type FetchSource<VALUE = any> = {
|
|
2565
2708
|
/** 2xx responses: all of them, one category, or those a predicate accepts */
|
|
2566
2709
|
select: (category?: string | ((response: FetchResponse<VALUE>) => boolean)) => Stream<FetchResponse<VALUE>>
|
|
2567
2710
|
/** Failures. Filters like select(). While nothing listens, failures are console.error'd */
|
|
2568
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>
|
|
2569
2714
|
}
|
|
2570
2715
|
|
|
2571
2716
|
export type FetchDriverOptions = {
|
|
@@ -2584,8 +2729,10 @@ export type FetchDriverOptions = {
|
|
|
2584
2729
|
timeoutMs?: number;
|
|
2585
2730
|
/** Default `parse` for every request. Default 'auto' */
|
|
2586
2731
|
parse?: 'auto' | 'json' | 'text' | 'response' | ((response: Response) => any);
|
|
2587
|
-
/** 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 */
|
|
2588
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;
|
|
2589
2736
|
/**
|
|
2590
2737
|
* PLAN-3 D88: the query cache, off by default: `cache: queryCache({ staleTime })`. On:
|
|
2591
2738
|
* resources' GET/HEAD replies are cached (stale-while-revalidate: cached data shows at once,
|
|
@@ -2668,6 +2815,12 @@ export type SseConnection = SocketActions & {
|
|
|
2668
2815
|
/** URL (after `baseUrl`) */
|
|
2669
2816
|
sse: string;
|
|
2670
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>;
|
|
2671
2824
|
/** Named events → actions: `{ 'price-update': 'PRICE' }` (data JSON-parsed when it parses) */
|
|
2672
2825
|
events?: Record<string, string>;
|
|
2673
2826
|
}
|
|
@@ -2720,8 +2873,10 @@ export type SocketDriverOptions = {
|
|
|
2720
2873
|
queueLimit?: number;
|
|
2721
2874
|
/** The WebSocket class. Default: `globalThis.WebSocket`, read at connect time (so test stubs apply) */
|
|
2722
2875
|
WebSocket?: any;
|
|
2723
|
-
/** 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) */
|
|
2724
2877
|
EventSource?: any;
|
|
2878
|
+
/** PLAN-11: the fetch for `sse` connections with `headers`. Default: `globalThis.fetch` */
|
|
2879
|
+
fetch?: (input: string, init?: any) => Promise<any>;
|
|
2725
2880
|
}
|
|
2726
2881
|
|
|
2727
2882
|
/**
|
|
@@ -3145,6 +3300,12 @@ export type FakeCacheEntry = {
|
|
|
3145
3300
|
}
|
|
3146
3301
|
|
|
3147
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;
|
|
3148
3309
|
/** Override initial state (defaults to component's .initialState) */
|
|
3149
3310
|
initialState?: any;
|
|
3150
3311
|
/**
|
|
@@ -3470,6 +3631,14 @@ export interface RenderResult<STATE = any> {
|
|
|
3470
3631
|
* throws (the driver would only log it, SYG678).
|
|
3471
3632
|
*/
|
|
3472
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>;
|
|
3473
3642
|
/**
|
|
3474
3643
|
* PLAN-6 L-4: stream into a pending request of the LLM fake (the sink named by `llmSink`,
|
|
3475
3644
|
* default 'LLM': the real makeChatDriver over an in-memory transport). Chunks: strings (text),
|
|
@@ -3601,13 +3770,13 @@ export interface RenderResult<STATE = any> {
|
|
|
3601
3770
|
* `await t.next(pred)` / `waitForState` / `settle()` / `ready()` it shows the state the wait resolved with.
|
|
3602
3771
|
*/
|
|
3603
3772
|
query: {
|
|
3604
|
-
(selector: string):
|
|
3773
|
+
<E extends Element = Element>(selector: string): E | null;
|
|
3605
3774
|
/** A control's element (`t.query(Draft)` is an HTMLInputElement for an 'input' control), or null */
|
|
3606
3775
|
<CONTROL extends AnyControl>(control: CONTROL): ControlElementOf<CONTROL> | null;
|
|
3607
3776
|
};
|
|
3608
3777
|
/** Every element matching a selector (or a control) in the rendered tree (Portals included), as query() */
|
|
3609
3778
|
queryAll: {
|
|
3610
|
-
(selector: string):
|
|
3779
|
+
<E extends Element = Element>(selector: string): E[];
|
|
3611
3780
|
<CONTROL extends AnyControl>(control: CONTROL): Array<ControlElementOf<CONTROL>>;
|
|
3612
3781
|
};
|
|
3613
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)}
|