sygnal 7.0.0 → 7.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +126 -0
- package/dist/astro/server.cjs.js +32 -43
- package/dist/astro/server.cjs.js.map +1 -1
- package/dist/astro/server.mjs +32 -43
- package/dist/astro/server.mjs.map +1 -1
- package/dist/diagnostics.cjs.js +67 -2
- package/dist/diagnostics.cjs.js.map +1 -1
- package/dist/diagnostics.esm.js +67 -2
- package/dist/diagnostics.esm.js.map +1 -1
- package/dist/guide/accessibility.md +1 -1
- package/dist/guide/resources.md +2 -0
- package/dist/guide/timers.md +3 -3
- package/dist/index.cjs.js +189 -77
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.d.ts +103 -16
- package/dist/index.esm.js +188 -78
- 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/vike/config/package.json +1 -1
- package/llms.txt +7 -7
- package/package.json +1 -1
- package/src/core/hosts/switchable.ts +26 -8
- package/src/core/instance.ts +12 -7
- package/src/core/statics.ts +5 -5
- package/src/extra/ai/transports/shared.ts +3 -1
- package/src/extra/behaviors.ts +3 -3
- package/src/extra/diagnostics/checks/next.ts +46 -3
- package/src/extra/diagnostics/checks/props.ts +6 -1
- package/src/extra/diagnostics/codes.ts +11 -0
- package/src/extra/fetchDriver.ts +3 -0
- package/src/extra/fetchEventSource.ts +6 -2
- package/src/extra/socketDriver.ts +51 -6
- package/src/extra/ssr.ts +30 -41
- package/src/extra/testing.ts +32 -7
- package/src/extra/timers.ts +5 -2
- package/src/index.d.ts +102 -15
- package/src/index.ts +2 -0
package/src/extra/ssr.ts
CHANGED
|
@@ -147,7 +147,7 @@ function withUses(def: any, state: any): any {
|
|
|
147
147
|
* state reads from the cache ('success' with the cached data), else 'loading' ('idle' for a
|
|
148
148
|
* falsy request), as the client's first paint does with the same (hydrated) cache
|
|
149
149
|
*/
|
|
150
|
-
function withResources(def: any, state: any): any {
|
|
150
|
+
function withResources(def: any, state: any, ctx?: any): any {
|
|
151
151
|
state = withUses(def, state)
|
|
152
152
|
const res = def && def.resources
|
|
153
153
|
if (!res || !state || typeof state !== 'object' || Array.isArray(state)) return state
|
|
@@ -155,7 +155,7 @@ function withResources(def: any, state: any): any {
|
|
|
155
155
|
for (const n of Object.keys(res)) {
|
|
156
156
|
if (out[n] !== undefined) continue
|
|
157
157
|
let q: any
|
|
158
|
-
try { q = typeof res[n] === 'function' ? res[n](state) : res[n] } catch (_) {}
|
|
158
|
+
try { q = typeof res[n] === 'function' ? res[n](state, ctx) : res[n] } catch (_) {}
|
|
159
159
|
const E = q && ssrCache ? ssrCache.get(ssrCache.key(q, 1)) : undefined
|
|
160
160
|
out[n] = E && E.at > 0 ? {status: 'success', data: E.v} : {status: q ? 'loading' : 'idle'}
|
|
161
161
|
}
|
|
@@ -198,10 +198,26 @@ function viewFailed(def: any, err: any, name: string): any {
|
|
|
198
198
|
return vnode
|
|
199
199
|
}
|
|
200
200
|
|
|
201
|
-
function collectHead(def: any, state: any): void {
|
|
201
|
+
function collectHead(def: any, state: any, ctx?: any): void {
|
|
202
202
|
const h = def && def.head
|
|
203
203
|
if (!heads || !h) return
|
|
204
|
-
try { heads.push(typeof h === 'function' ? h(state) : h) } catch (_) {}
|
|
204
|
+
try { heads.push(typeof h === 'function' ? h(state, ctx) : h) } catch (_) {}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* PLAN-13 F-1: the context a component sees (its parent's, plus its own entries computed on
|
|
209
|
+
* `state`). The statics get it as on the client; for `resources` it is computed on the state
|
|
210
|
+
* before the resources are filled in (a context entry can't read the component's own resource)
|
|
211
|
+
*/
|
|
212
|
+
function contextOf(def: any, state: any, parent: Record<string, any>): Record<string, any> {
|
|
213
|
+
const own = def.context || {}, out: Record<string, any> = {...parent}
|
|
214
|
+
for (const key of Object.keys(own)) {
|
|
215
|
+
const f = own[key]
|
|
216
|
+
if (typeof f === 'function' && state != null) {
|
|
217
|
+
try { out[key] = f(state) } catch (_) {}
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
return out
|
|
205
221
|
}
|
|
206
222
|
|
|
207
223
|
/**
|
|
@@ -245,20 +261,11 @@ function renderRoot(componentDef: any, options: RenderToStringOptions): string {
|
|
|
245
261
|
|
|
246
262
|
const ownState = state !== undefined ? state : componentDef.initialState
|
|
247
263
|
// 5-5: the view sees its resources; the hydration script keeps the state as it was given
|
|
248
|
-
const resolvedState = withResources(componentDef, ownState)
|
|
249
|
-
collectHead(componentDef, resolvedState)
|
|
264
|
+
const resolvedState = withResources(componentDef, ownState, contextOf(componentDef, ownState, context))
|
|
250
265
|
|
|
251
266
|
// Build context: merge parent context with component's own context definitions
|
|
252
|
-
const
|
|
253
|
-
|
|
254
|
-
for (const key of Object.keys(componentContext)) {
|
|
255
|
-
const contextFn = componentContext[key]
|
|
256
|
-
if (typeof contextFn === 'function' && resolvedState != null) {
|
|
257
|
-
try {
|
|
258
|
-
mergedContext[key] = contextFn(resolvedState)
|
|
259
|
-
} catch (_) {}
|
|
260
|
-
}
|
|
261
|
-
}
|
|
267
|
+
const mergedContext = contextOf(componentDef, resolvedState, context)
|
|
268
|
+
collectHead(componentDef, resolvedState, mergedContext)
|
|
262
269
|
|
|
263
270
|
// Call the view function
|
|
264
271
|
let vnode: any
|
|
@@ -474,19 +481,10 @@ function renderSubComponent(vnode: any, context: Record<string, any>, parentStat
|
|
|
474
481
|
// State passed directly
|
|
475
482
|
childState = stateProp
|
|
476
483
|
}
|
|
477
|
-
childState = withResources(componentDef, childState)
|
|
484
|
+
childState = withResources(componentDef, childState, contextOf(componentDef, childState, context))
|
|
478
485
|
|
|
479
486
|
// Build child context
|
|
480
|
-
const childContext
|
|
481
|
-
const componentContext = componentDef.context || {}
|
|
482
|
-
for (const key of Object.keys(componentContext)) {
|
|
483
|
-
const contextFn = componentContext[key]
|
|
484
|
-
if (typeof contextFn === 'function' && childState != null) {
|
|
485
|
-
try {
|
|
486
|
-
childContext[key] = contextFn(childState)
|
|
487
|
-
} catch (_) {}
|
|
488
|
-
}
|
|
489
|
-
}
|
|
487
|
+
const childContext = contextOf(componentDef, childState, context)
|
|
490
488
|
|
|
491
489
|
// Extract slots from children
|
|
492
490
|
const slots: Record<string, any[]> = {}
|
|
@@ -507,7 +505,7 @@ function renderSubComponent(vnode: any, context: Record<string, any>, parentStat
|
|
|
507
505
|
slots.default.push(...defaultSlotChildren)
|
|
508
506
|
}
|
|
509
507
|
|
|
510
|
-
collectHead(componentDef, childState)
|
|
508
|
+
collectHead(componentDef, childState, childContext)
|
|
511
509
|
|
|
512
510
|
// Call the view function
|
|
513
511
|
let result: any
|
|
@@ -713,20 +711,11 @@ function renderSwitchable(vnode: any, context: Record<string, any>, parentState:
|
|
|
713
711
|
* Internal helper: render a component def to a VNode (not HTML string).
|
|
714
712
|
*/
|
|
715
713
|
function renderToStringInternal(componentDef: any, state: any, context: Record<string, any>, uid: string): any {
|
|
716
|
-
const
|
|
717
|
-
|
|
718
|
-
const
|
|
719
|
-
const mergedContext: Record<string, any> = {...context}
|
|
720
|
-
for (const key of Object.keys(componentContext)) {
|
|
721
|
-
const contextFn = componentContext[key]
|
|
722
|
-
if (typeof contextFn === 'function' && resolvedState != null) {
|
|
723
|
-
try {
|
|
724
|
-
mergedContext[key] = contextFn(resolvedState)
|
|
725
|
-
} catch (_) {}
|
|
726
|
-
}
|
|
727
|
-
}
|
|
714
|
+
const own = state !== undefined ? state : componentDef.initialState
|
|
715
|
+
const resolvedState = withResources(componentDef, own, contextOf(componentDef, own, context))
|
|
716
|
+
const mergedContext = contextOf(componentDef, resolvedState, context)
|
|
728
717
|
|
|
729
|
-
collectHead(componentDef, resolvedState)
|
|
718
|
+
collectHead(componentDef, resolvedState, mergedContext)
|
|
730
719
|
let vnode: any
|
|
731
720
|
try {
|
|
732
721
|
vnode = componentDef({
|
package/src/extra/testing.ts
CHANGED
|
@@ -5,6 +5,7 @@ import {enrichEventStream} from '../cycle/dom/enrichEventStream';
|
|
|
5
5
|
import eventBusDriver from './eventDriver';
|
|
6
6
|
import logDriver from './logDriver';
|
|
7
7
|
import {ownedCopy} from './owned';
|
|
8
|
+
import {isAbort} from '../shared';
|
|
8
9
|
import {renderToInnerHtml} from './ssr';
|
|
9
10
|
import {_getDiagnosticsConfig, configureDiagnostics, getDiagnosticsMode, isDiagnosticsEnabled, onDiagnostic, report} from './diagnostics/index';
|
|
10
11
|
import xs from './xstreamCompat';
|
|
@@ -496,9 +497,10 @@ export interface RenderResult {
|
|
|
496
497
|
open: (sinkName: string, target?: FakeConnectionTarget) => Promise<void>;
|
|
497
498
|
/**
|
|
498
499
|
* PLAN-3 2-C: the server sends `data` (objects as JSON text) on the open connection(s): `message`
|
|
499
|
-
* fires (the data JSON-parsed when it parses). `{ event, connection? }`: an SSE named event
|
|
500
|
+
* fires (the data JSON-parsed when it parses). `{ event, connection? }`: an SSE named event;
|
|
501
|
+
* `{ id }` (PLAN-13 S-1): its SSE event id (`meta`, `resume`).
|
|
500
502
|
*/
|
|
501
|
-
push: (sinkName: string, data: any, target?: FakeConnectionTarget | {event?: string; connection?: FakeConnectionTarget}) => Promise<void>;
|
|
503
|
+
push: (sinkName: string, data: any, target?: FakeConnectionTarget | {event?: string; id?: string | number; connection?: FakeConnectionTarget}) => Promise<void>;
|
|
502
504
|
/**
|
|
503
505
|
* PLAN-3 2-C: the connection(s) close without the app closing them (a connecting one fails to
|
|
504
506
|
* open: `error` first): `close` fires with `{ code, reason, willReconnect }` and the fake
|
|
@@ -1273,17 +1275,27 @@ const sockKey = (sse: boolean, url: string, arg: any) => (sse ? 'e' + !!(arg &&
|
|
|
1273
1275
|
* PLAN-3 2-C: the in-memory WebSocket / EventSource the fake's makeSocketDriver opens. The
|
|
1274
1276
|
* harness drives it (t.open / t.push / t.drop); the driver's own close() is the app's close.
|
|
1275
1277
|
*/
|
|
1278
|
+
/** `url` without its query parameter `name` (PLAN-13 S-1: a resumed reconnect's id parameter) */
|
|
1279
|
+
const withoutParam = (url: string, name: string) =>
|
|
1280
|
+
url.replace(new RegExp('([?&])' + name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + '=[^&#]*&?'), '$1').replace(/[?&](#|$)/, '$1');
|
|
1276
1281
|
const fakeSocketClass = (sse: boolean, made: (s: any) => void) => class {
|
|
1277
1282
|
readyState = 0;
|
|
1278
1283
|
sse = sse;
|
|
1279
1284
|
byApp = false;
|
|
1280
1285
|
k: string;
|
|
1286
|
+
rn?: string;
|
|
1281
1287
|
url: string;
|
|
1282
1288
|
ls: Record<string, any[]> = {};
|
|
1283
1289
|
onopen: any; onmessage: any; onerror: any; onclose: any;
|
|
1284
1290
|
constructor(url: string, arg?: any) {
|
|
1285
1291
|
this.url = String(url);
|
|
1286
|
-
|
|
1292
|
+
// PLAN-13 S-1: a resumed SSE reconnect's URL has the id parameter (added, or replacing the
|
|
1293
|
+
// declared one): its key leaves that parameter out (`rn`: its name; made() compares the
|
|
1294
|
+
// connections' URLs without it too)
|
|
1295
|
+
const id = sse && arg?.lastEventId;
|
|
1296
|
+
const m = id && new RegExp('[?&]([^?&=]+)=' + encodeURIComponent(id).replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + '(?=&|#|$)').exec(this.url);
|
|
1297
|
+
if (m) this.rn = m[1];
|
|
1298
|
+
this.k = sockKey(sse, m ? withoutParam(this.url, m[1]) : this.url, arg);
|
|
1287
1299
|
made(this);
|
|
1288
1300
|
}
|
|
1289
1301
|
send() {}
|
|
@@ -1851,7 +1863,7 @@ export function renderComponent(
|
|
|
1851
1863
|
const w: Sock = {src: null, in$: xs.create(), conns: new Map(), sockets: [], sent: [], declaring: null};
|
|
1852
1864
|
const made = (s: any) => {
|
|
1853
1865
|
w.sockets.push(s);
|
|
1854
|
-
const all = [...w.conns.values()].flatMap(m => [...m.values()]).filter(c => c.k == s.k);
|
|
1866
|
+
const all = [...w.conns.values()].flatMap(m => [...m.values()]).filter(c => (s.rn ? sockKey(true, withoutParam(c.url, s.rn), c.spec) : c.k) == s.k);
|
|
1855
1867
|
if (w.declaring) {
|
|
1856
1868
|
// a declaration opened it: the first new connection with its key (and, shared, the
|
|
1857
1869
|
// other new shared ones)
|
|
@@ -1952,7 +1964,7 @@ export function renderComponent(
|
|
|
1952
1964
|
for (const k in uses) for (const a in uses[k]?.model || {}) names.push(k + '.' + a);
|
|
1953
1965
|
// G-275: the caller's initialState is owned (the dev statics freeze leaves it alone). G-289: a
|
|
1954
1966
|
// copy is marked, not the caller's object
|
|
1955
|
-
|
|
1967
|
+
let init = initialState !== undefined ? ownedCopy(initialState) : componentDef.initialState;
|
|
1956
1968
|
// G-028: with no intent, model or initialState nothing would ever emit state, so the view
|
|
1957
1969
|
// never renders. Leave intent/model unset so the component falls back to the same no-op
|
|
1958
1970
|
// model run() uses, and renders.
|
|
@@ -2213,6 +2225,17 @@ export function renderComponent(
|
|
|
2213
2225
|
},
|
|
2214
2226
|
};
|
|
2215
2227
|
};
|
|
2228
|
+
// PLAN-13 F-2: a root that declares `route` starts from the test URL's route: its initialState's
|
|
2229
|
+
// router.current() ran at module load, before the test's URL existed. The route entry's STATE
|
|
2230
|
+
// reducer is applied to the initial state (its other sinks, a guard's redirect, run once when the
|
|
2231
|
+
// router answers the declaration), so the first render and the statics see the page the test
|
|
2232
|
+
// starts on: no render of '/' and no request for it
|
|
2233
|
+
const re = rt && typeof componentDef.route == 'string' && model[componentDef.route]
|
|
2234
|
+
const seed = typeof re == 'function' ? re : re && typeof re == 'object' && typeof re.STATE == 'function' && re.STATE
|
|
2235
|
+
if (seed && init !== undefined) {
|
|
2236
|
+
const v = seed(init, rt!.r.current(), noop, {state: init, previousState: init})
|
|
2237
|
+
if (v !== undefined && !isAbort(v)) init = v
|
|
2238
|
+
}
|
|
2216
2239
|
let sources: any, sinks: any;
|
|
2217
2240
|
try {
|
|
2218
2241
|
// the root runs with the test intent, model, initial state and name, and the drivers above.
|
|
@@ -2848,11 +2871,13 @@ export function renderComponent(
|
|
|
2848
2871
|
};
|
|
2849
2872
|
const open = (name: string, target?: FakeConnectionTarget) => sockCall('open', name, target, [0], sockOpen);
|
|
2850
2873
|
const push = (name: string, data: any, target?: any) => {
|
|
2851
|
-
const o = target && typeof target == 'object' && !Array.isArray(target) && Object.keys(target).length && Object.keys(target).every(k => k == 'event' || k == 'connection') ? target : {connection: target};
|
|
2874
|
+
const o = target && typeof target == 'object' && !Array.isArray(target) && Object.keys(target).length && Object.keys(target).every(k => k == 'event' || k == 'connection' || k == 'id') ? target : {connection: target};
|
|
2852
2875
|
const raw = typeof data == 'string' || (data && typeof data == 'object' && (data instanceof ArrayBuffer || ArrayBuffer.isView(data) || (typeof Blob != 'undefined' && data instanceof Blob))) ? data : JSON.stringify(data);
|
|
2853
2876
|
const ev = o.event;
|
|
2854
2877
|
return sockCall('push', name, o.connection, [1], s => {
|
|
2855
|
-
|
|
2878
|
+
// PLAN-13 S-1: `id` sets the event id; as in EventSource, lastEventId keeps the last one
|
|
2879
|
+
if (s.sse && o.id != null) s.lastId = String(o.id);
|
|
2880
|
+
const m: any = {type: ev || 'message', data: raw, ...(s.sse && {lastEventId: s.lastId ?? ''})};
|
|
2856
2881
|
if (!ev || ev == 'message') sockFire(s, 'message', m);
|
|
2857
2882
|
if (s.sse) (s.ls[ev || 'message'] || []).forEach((f: any) => { try { f(m); } catch (e) { console.error(e); } });
|
|
2858
2883
|
});
|
package/src/extra/timers.ts
CHANGED
|
@@ -13,7 +13,8 @@ import {makeReplies} from './replies';
|
|
|
13
13
|
* falsy one stops, a changed spec restarts (an equal one keeps running).
|
|
14
14
|
* - `every`: drift-free; tick n is due at start + n * every (each delay is computed from the
|
|
15
15
|
* start, not chained). A late tick (a throttled background tab) coalesces the missed ones:
|
|
16
|
-
* n jumps.
|
|
16
|
+
* n jumps. A delay is at most `every`, so a frozen Date.now() (a test clock that fixes the time
|
|
17
|
+
* while timers run) ticks once per interval. Data `{ n, t }`: the tick number from 1, `t` = Date.now().
|
|
17
18
|
* - `after`: fires once, data `{ t }`. It doesn't fire again while the same spec stays declared.
|
|
18
19
|
* - `frame`: every animation frame (requestAnimationFrame; setTimeout 16 ms without it), data
|
|
19
20
|
* `{ t, dt }`: Date.now() and the ms since the previous frame (0 on the first).
|
|
@@ -47,7 +48,9 @@ const start = (s: any, send: (type: string, data: any) => void, e: any): (() =>
|
|
|
47
48
|
last = ts;
|
|
48
49
|
} else if (every) {
|
|
49
50
|
n = Math.max(n + 1, Math.floor((t - t0) / every));
|
|
50
|
-
|
|
51
|
+
// never more than one interval: under a frozen Date.now() (Playwright's setFixedTime: Date is
|
|
52
|
+
// fixed, timers run) the drift-free delay grew by one interval per tick
|
|
53
|
+
later(Math.min(every, t0 + (n + 1) * every - t));
|
|
51
54
|
send(s.action, {n, t});
|
|
52
55
|
} else {
|
|
53
56
|
e.d = 1;
|
package/src/index.d.ts
CHANGED
|
@@ -77,8 +77,11 @@ export type StateProp = string | Lense<any, any>
|
|
|
77
77
|
*/
|
|
78
78
|
export type ElementProps<PROPS> =
|
|
79
79
|
0 extends (1 & PROPS) ? PROPS
|
|
80
|
-
: '
|
|
81
|
-
: PROPS
|
|
80
|
+
// PLAN-13 F-3: Switchable's own `state` (a per-page map too) is kept as it declares it
|
|
81
|
+
: PROPS extends { of: Record<string, any>; current: string } ? PROPS
|
|
82
|
+
: WithStateProp<PROPS>
|
|
83
|
+
|
|
84
|
+
type WithStateProp<PROPS> = 'state' extends keyof PROPS ? WithoutViewOnlyProps<PROPS> & { state?: StateProp; resetState?: boolean } : PROPS
|
|
82
85
|
|
|
83
86
|
/** PROPS without `state`, `context`, `slots` and `uid` (keeps optionality and index signatures, unlike Omit). */
|
|
84
87
|
type WithoutViewOnlyProps<PROPS> = {
|
|
@@ -1568,10 +1571,12 @@ export type Component<
|
|
|
1568
1571
|
* actions on this instance; send with `{ to: 'room', json }` from a model entry (a connection
|
|
1569
1572
|
* the same action opens or changes is declared first). Dispose closes them.
|
|
1570
1573
|
*/
|
|
1571
|
-
connections?: (state: STATE & CALCULATED) => Connections;
|
|
1574
|
+
connections?: (state: STATE & CALCULATED, context: CONTEXT) => Connections;
|
|
1572
1575
|
/**
|
|
1573
1576
|
* PLAN-3: declarative reads (`makeFetchDriver()`). Each entry derives a
|
|
1574
|
-
* request from state
|
|
1577
|
+
* request from state (and, PLAN-13 F-1, the context as its second argument, as every
|
|
1578
|
+
* declaration static: `(state, { route }) => '/api/items/' + route.params.id`; re-run when the
|
|
1579
|
+
* context changes); falsy means idle:
|
|
1575
1580
|
* `Quote.resources = { quote: (state) => state.id && '/api/quotes/' + state.id }`.
|
|
1576
1581
|
* `state.quote` is a `Resource`: `{ status: 'idle' | 'loading' | 'success' | 'error', data,
|
|
1577
1582
|
* error, refreshing }`, written by the built-in RESOURCE action (idle until the first request).
|
|
@@ -1581,7 +1586,7 @@ export type Component<
|
|
|
1581
1586
|
* `data`, `error` and `status` and sets `refreshing: true` (D78). `ok` / `error` on the
|
|
1582
1587
|
* request also dispatch those actions after the write.
|
|
1583
1588
|
*/
|
|
1584
|
-
resources?: { [name: string]: (state: STATE & CALCULATED) => ResourceRequest | false | null | undefined | '' | 0 };
|
|
1589
|
+
resources?: { [name: string]: (state: STATE & CALCULATED, context: CONTEXT) => ResourceRequest | false | null | undefined | '' | 0 };
|
|
1585
1590
|
/**
|
|
1586
1591
|
* The router's reply action (`makeRouter()`): `App.route = 'ROUTE'`. The driver sends this
|
|
1587
1592
|
* instance ROUTE with the `Route` (`{ name, params, query, hash, path }`) once declared and on
|
|
@@ -1603,7 +1608,7 @@ export type Component<
|
|
|
1603
1608
|
* `renderToString(App, { head: list })` for SSR (`renderHead(list)`). Like every declaration
|
|
1604
1609
|
* static, it is sent with or without a model; a root needs `initialState` (SYG132).
|
|
1605
1610
|
*/
|
|
1606
|
-
head?: (state: STATE & CALCULATED) => HeadValue | false | null | undefined;
|
|
1611
|
+
head?: (state: STATE & CALCULATED, context: CONTEXT) => HeadValue | false | null | undefined;
|
|
1607
1612
|
/**
|
|
1608
1613
|
* PLAN-4 GS-1: behaviors this component uses, each under its state key:
|
|
1609
1614
|
* `TaskList.uses = { pager: pager({ pageSize: 10, next: Newer, prev: Older }) }` gives
|
|
@@ -1620,7 +1625,7 @@ export type Component<
|
|
|
1620
1625
|
* fires once (`{ t }`), `frame` runs every animation frame (`{ t, dt }`). A hidden Switchable
|
|
1621
1626
|
* page's timers stop unless `background: true`; dispose stops them; nothing runs during SSR.
|
|
1622
1627
|
*/
|
|
1623
|
-
timers?: (state: STATE & CALCULATED) => Timers<ActionNameOf<ACTIONS>>;
|
|
1628
|
+
timers?: (state: STATE & CALCULATED, context: CONTEXT) => Timers<ActionNameOf<ACTIONS>>;
|
|
1624
1629
|
/**
|
|
1625
1630
|
* PLAN-5 B-3: browser sources derived from state, run by `makeBrowserDriver()` (registered:
|
|
1626
1631
|
* `run(App, { BROWSER: makeBrowserDriver() })`; renderComponent provides a fake, `t.browser`)
|
|
@@ -1630,7 +1635,7 @@ export type Component<
|
|
|
1630
1635
|
* restarts. A hidden Switchable page's sources stop unless `background: true`; dispose stops
|
|
1631
1636
|
* them; nothing runs during SSR.
|
|
1632
1637
|
*/
|
|
1633
|
-
browser?: (state: STATE & CALCULATED) => BrowserSources<ActionNameOf<ACTIONS>>;
|
|
1638
|
+
browser?: (state: STATE & CALCULATED, context: CONTEXT) => BrowserSources<ActionNameOf<ACTIONS>>;
|
|
1634
1639
|
/**
|
|
1635
1640
|
* PLAN-4 GS-5: save this root component's state and restore it at startup:
|
|
1636
1641
|
* `TodoApp.persist = persist({ key: 'todo-app', pick: ['todos', 'filter'] })`. `pick` / `omit`
|
|
@@ -1753,8 +1758,8 @@ export type VirtualCollectionProps<PROPS = any, STATE = any> = {
|
|
|
1753
1758
|
/** Where `scrollToIndex` / `scrollToId` put the row: 'auto' (default) scrolls only when it isn't in view */
|
|
1754
1759
|
export type ScrollToAlign = 'auto' | 'start' | 'center' | 'end'
|
|
1755
1760
|
|
|
1756
|
-
export type SwitchableProps<PROPS = any> = {
|
|
1757
|
-
of:
|
|
1761
|
+
export type SwitchableProps<PROPS = any, OF extends Record<string, AnyComponent> = Record<string, AnyComponent>> = {
|
|
1762
|
+
of: OF;
|
|
1758
1763
|
current: string;
|
|
1759
1764
|
/**
|
|
1760
1765
|
* The current page's instance key. When it changes, the current page is disposed and created
|
|
@@ -1762,7 +1767,14 @@ export type SwitchableProps<PROPS = any> = {
|
|
|
1762
1767
|
* show. Switching `current` alone keeps pages alive. Router recipe: `instance={state.route.path}`
|
|
1763
1768
|
*/
|
|
1764
1769
|
instance?: string | number;
|
|
1765
|
-
|
|
1770
|
+
/**
|
|
1771
|
+
* The pages' state: a key or path (`'pages'`), a lens, or (PLAN-13 F-3) a map from page names to
|
|
1772
|
+
* those (`{ projects: 'pages.projects', settings: byId('teams', state.teamId) }`): each listed
|
|
1773
|
+
* page binds its own target with a tag child's lifetime rules (another key or id makes that page
|
|
1774
|
+
* again, a deleted target disposes it), and the pages not listed share the owner's state. A key
|
|
1775
|
+
* that names no page is SYG429
|
|
1776
|
+
*/
|
|
1777
|
+
state?: string | Lense | { [PAGE in keyof NoInfer<OF>]?: string | Lense };
|
|
1766
1778
|
} & Omit<PROPS, 'of' | 'state' | 'current' | 'instance'>
|
|
1767
1779
|
|
|
1768
1780
|
export type PortalProps = {
|
|
@@ -2261,7 +2273,7 @@ export function Collection<PROPS extends { [prop: string]: any }, STATE = any>(p
|
|
|
2261
2273
|
* <VirtualCollection of={Row} from="rows" className="rows" estimateSize={32} />
|
|
2262
2274
|
*/
|
|
2263
2275
|
export function VirtualCollection<PROPS extends { [prop: string]: any }, STATE = any>(props: VirtualCollectionProps<PROPS, STATE>): JSX.Element
|
|
2264
|
-
export function Switchable<PROPS extends { [prop: string]: any }>(props: SwitchableProps<PROPS>): JSX.Element
|
|
2276
|
+
export function Switchable<PROPS extends { [prop: string]: any }, OF extends Record<string, AnyComponent> = Record<string, AnyComponent>>(props: SwitchableProps<PROPS, OF>): JSX.Element
|
|
2265
2277
|
export function Portal(props: PortalProps): JSX.Element
|
|
2266
2278
|
export function Transition(props: TransitionProps): JSX.Element
|
|
2267
2279
|
export function Suspense(props: SuspenseProps): JSX.Element
|
|
@@ -2322,6 +2334,48 @@ export type ReplyRequest = {
|
|
|
2322
2334
|
catch?: never;
|
|
2323
2335
|
}
|
|
2324
2336
|
|
|
2337
|
+
declare const replySenderBrand: unique symbol
|
|
2338
|
+
/**
|
|
2339
|
+
* The component instance that sent a request to a driver (opaque). `senderOf(request)` reads it;
|
|
2340
|
+
* pass it to `reply()` to answer that instance with one of its actions.
|
|
2341
|
+
*/
|
|
2342
|
+
export type ReplySender = { readonly [replySenderBrand]: true }
|
|
2343
|
+
|
|
2344
|
+
/** What makeReplies() returns: the source part that marks the driver, and the way to answer */
|
|
2345
|
+
export type ReplyChannel = {
|
|
2346
|
+
/**
|
|
2347
|
+
* Spread into the driver's source (`return { ...replies, select }`): the core then stamps each
|
|
2348
|
+
* request a component sends with its sender and delivers `reply()`s as that instance's actions.
|
|
2349
|
+
*/
|
|
2350
|
+
replies: { readonly __sygnalReplies: true; replies(sender: ReplySender): Stream<{ type: string; data: any }> };
|
|
2351
|
+
/** Dispatch `type` with `data` on the sender (dropped when it is gone). Call it as often as needed */
|
|
2352
|
+
reply(sender: ReplySender, type: string, data?: any): void;
|
|
2353
|
+
/** Whether the sender is still mounted (a reply would reach it) */
|
|
2354
|
+
listening(sender: ReplySender): boolean;
|
|
2355
|
+
}
|
|
2356
|
+
|
|
2357
|
+
/**
|
|
2358
|
+
* PLAN-13 D-1: reply actions for a driver you write (the protocol makeFetchDriver,
|
|
2359
|
+
* driverFromAsync and the socket driver use). `onStop(sender)` runs when a sender is disposed:
|
|
2360
|
+
* abort what it still has in flight.
|
|
2361
|
+
*
|
|
2362
|
+
* const { replies, reply } = makeReplies((sender) => inflight.get(sender)?.abort())
|
|
2363
|
+
* sink$.addListener({ next: (req) => {
|
|
2364
|
+
* const sender = senderOf(req)
|
|
2365
|
+
* if (sender === undefined) return // not sent by a component
|
|
2366
|
+
* run(req, (f) => req.progress && reply(sender, req.progress, f))
|
|
2367
|
+
* .then((v) => req.ok && reply(sender, req.ok, v), (error) => req.error && reply(sender, req.error, { error, request: req }))
|
|
2368
|
+
* } })
|
|
2369
|
+
* return { ...replies }
|
|
2370
|
+
*
|
|
2371
|
+
* A name with no model entry on the sender is SYG112 in the dev entry (ok / error / progress
|
|
2372
|
+
* statically). Guide: https://sygnal.js.org/guide/custom-drivers/
|
|
2373
|
+
*/
|
|
2374
|
+
export function makeReplies(onStop?: (sender: ReplySender) => void): ReplyChannel
|
|
2375
|
+
|
|
2376
|
+
/** The component instance that sent `request` to a driver with reply actions, or undefined (not sent by a component) */
|
|
2377
|
+
export function senderOf(request: object): ReplySender | undefined
|
|
2378
|
+
|
|
2325
2379
|
/**
|
|
2326
2380
|
* A request to a driverFromAsync() sink: your own fields (`value`, the args, ...) plus the
|
|
2327
2381
|
* reply-action keys `ok` / `error`. Type the driver's sink with it: `{ QUOTE: { source:
|
|
@@ -2797,6 +2851,15 @@ export type SocketActions = {
|
|
|
2797
2851
|
* hidden page's connections close, and open again as new ones when it is shown)
|
|
2798
2852
|
*/
|
|
2799
2853
|
background?: boolean;
|
|
2854
|
+
/**
|
|
2855
|
+
* PLAN-13 S-1: a connection that stays silent this long (ms) is taken as dead (a half-open
|
|
2856
|
+
* socket behind a proxy): it is closed as a drop, `close` gets `{ stale: true, willReconnect }`,
|
|
2857
|
+
* and the reconnect policy applies. Every event counts; over fetch (an `sse` with `headers`)
|
|
2858
|
+
* any bytes do, heartbeat comments (`: ping`) included, while the native EventSource needs a
|
|
2859
|
+
* real event (a `message`, or a named event listed in `events`). Set it above the server's
|
|
2860
|
+
* heartbeat interval
|
|
2861
|
+
*/
|
|
2862
|
+
staleAfterMs?: number;
|
|
2800
2863
|
/** Not allowed: a `then` key makes the value a thenable (SYG610). Use `message` / `open` */
|
|
2801
2864
|
then?: never;
|
|
2802
2865
|
catch?: never;
|
|
@@ -2823,6 +2886,29 @@ export type SseConnection = SocketActions & {
|
|
|
2823
2886
|
headers?: Record<string, string>;
|
|
2824
2887
|
/** Named events → actions: `{ 'price-update': 'PRICE' }` (data JSON-parsed when it parses) */
|
|
2825
2888
|
events?: Record<string, string>;
|
|
2889
|
+
/**
|
|
2890
|
+
* PLAN-13 S-1: the `message` and `events` actions get `SseEvent` (`{ data, id, event }`: the
|
|
2891
|
+
* event id and type) instead of the data alone
|
|
2892
|
+
*/
|
|
2893
|
+
meta?: boolean;
|
|
2894
|
+
/**
|
|
2895
|
+
* PLAN-13 S-1: resume after a reconnect the driver makes (EventSource gave up: a proxy 502, a
|
|
2896
|
+
* non-2xx on the `headers` path) from the last event id. Over fetch (`headers`) it is sent as
|
|
2897
|
+
* `Last-Event-ID`; the native EventSource can't send it, so it goes on the URL:
|
|
2898
|
+
* `?lastEventId=<id>` (`true`) or `?<name>=<id>` (a parameter name). EventSource's own retries
|
|
2899
|
+
* send `Last-Event-ID` anyway
|
|
2900
|
+
*/
|
|
2901
|
+
resume?: true | string;
|
|
2902
|
+
}
|
|
2903
|
+
|
|
2904
|
+
/** PLAN-13 S-1: an SSE event as a `meta: true` connection's actions get it */
|
|
2905
|
+
export type SseEvent<DATA = any> = {
|
|
2906
|
+
/** The data, JSON-parsed when it parses */
|
|
2907
|
+
data: DATA;
|
|
2908
|
+
/** The event's `id:` (the last one sent when this event has none; '' when there was none) */
|
|
2909
|
+
id: string;
|
|
2910
|
+
/** The event type: 'message', or the named event's name */
|
|
2911
|
+
event: string;
|
|
2826
2912
|
}
|
|
2827
2913
|
|
|
2828
2914
|
/**
|
|
@@ -2848,7 +2934,7 @@ export type SocketRequest =
|
|
|
2848
2934
|
/** Data of a connection's `open` action */
|
|
2849
2935
|
export type SocketOpen = { reconnected: boolean }
|
|
2850
2936
|
/** Data of a connection's `close` action (SSE: no code / reason) */
|
|
2851
|
-
export type SocketClose = { code?: number; reason?: string; willReconnect: boolean }
|
|
2937
|
+
export type SocketClose = { code?: number; reason?: string; willReconnect: boolean; /** PLAN-13 S-1: closed by staleAfterMs (nothing arrived) */ stale?: true }
|
|
2852
2938
|
/** Data of a connection's `error` action: the error Event (or the constructor's exception) */
|
|
2853
2939
|
export type SocketError = { error: any }
|
|
2854
2940
|
|
|
@@ -3670,9 +3756,10 @@ export interface RenderResult<STATE = any> {
|
|
|
3670
3756
|
open: (sinkName: string, target?: FakeConnectionTarget) => Promise<void>;
|
|
3671
3757
|
/**
|
|
3672
3758
|
* The server sends `data` (objects as JSON text) on open connection(s): `message` fires with
|
|
3673
|
-
* the data, JSON-parsed when it parses. `{ event, connection? }` sends an SSE named event
|
|
3759
|
+
* the data, JSON-parsed when it parses. `{ event, connection? }` sends an SSE named event;
|
|
3760
|
+
* `{ id }` gives an SSE event its id (what `meta: true` reports and `resume` resumes from).
|
|
3674
3761
|
*/
|
|
3675
|
-
push: (sinkName: string, data: any, target?: FakeConnectionTarget | { event?: string; connection?: FakeConnectionTarget }) => Promise<void>;
|
|
3762
|
+
push: (sinkName: string, data: any, target?: FakeConnectionTarget | { event?: string; id?: string | number; connection?: FakeConnectionTarget }) => Promise<void>;
|
|
3676
3763
|
/**
|
|
3677
3764
|
* Connection(s) close without the app closing them: `close` fires with `{ code, reason,
|
|
3678
3765
|
* willReconnect }` (default `{ code: 1006, reason: '' }`; a connecting one fails to open: `error`
|
package/src/index.ts
CHANGED
|
@@ -12,6 +12,8 @@ export { Suspense } from "./suspense"
|
|
|
12
12
|
export { Slot } from "./slot"
|
|
13
13
|
export { lazy } from "./lazy"
|
|
14
14
|
export { driverFromAsync } from "./extra/driverFactories"
|
|
15
|
+
// PLAN-13 D-1: the reply-action protocol, for drivers written by hand
|
|
16
|
+
export { makeReplies, senderOf } from './extra/replies'
|
|
15
17
|
export { makeFetchDriver } from "./extra/fetchDriver"
|
|
16
18
|
export { queryCache } from "./extra/queryCache"
|
|
17
19
|
export { makeSocketDriver } from "./extra/socketDriver"
|