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/core/instance.ts
CHANGED
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
*/
|
|
16
16
|
import xs from '../extra/xstreamCompat'
|
|
17
17
|
import {StateSource} from '../cycle/state/StateSource'
|
|
18
|
-
import {uidPart} from '../shared'
|
|
18
|
+
import {uidPart, BINDING} from '../shared'
|
|
19
19
|
import {warn, error as logError, fail} from '../extra/diagnostics/legacy'
|
|
20
20
|
import type {App} from './runtime'
|
|
21
21
|
import {tearDown, INST, SEED} from './teardown'
|
|
@@ -29,6 +29,20 @@ import {attach, detach} from './statics'
|
|
|
29
29
|
import {dbg} from './debug'
|
|
30
30
|
import {sameData} from '../cycle/dom/utils'
|
|
31
31
|
|
|
32
|
+
// v7: a known binding target joins key/id/path in the lifetime identity. Fresh equivalent
|
|
33
|
+
// byId helpers retain identity; opaque lenses need an explicit key for a new target.
|
|
34
|
+
function bindingChanged(a: any, b: any): boolean {
|
|
35
|
+
if (a === b) return false
|
|
36
|
+
const x = a?.[BINDING], y = b?.[BINDING]
|
|
37
|
+
return x && y ? !x.every((v: any, i: number) => Object.is(v, y[i]))
|
|
38
|
+
: !!(x || y || typeof a == 'string' || typeof b == 'string' || a === undefined || b === undefined)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function bindingState(k: any): any {
|
|
42
|
+
const c = k.base || k.cell || k.arr
|
|
43
|
+
return c && (c.has?.() ?? c.get() !== undefined) ? (k.cell || k.arr).get() : undefined
|
|
44
|
+
}
|
|
45
|
+
|
|
32
46
|
const ERR_FIX = 'See the attached error'
|
|
33
47
|
|
|
34
48
|
// 4-I G-557: a fragment root (a Collection's): each of its top-level elements is marked.
|
|
@@ -65,8 +79,8 @@ const idOf = (n: any, path: string) => {
|
|
|
65
79
|
}
|
|
66
80
|
|
|
67
81
|
/**
|
|
68
|
-
* a component vnode's props, without `resetState` (D174: read at creation,
|
|
69
|
-
* prop of the child)
|
|
82
|
+
* a component vnode's props, without `resetState` (D174: read at creation, never a
|
|
83
|
+
* prop of the child). Known `state` target changes start a new lifetime in v7.
|
|
70
84
|
*/
|
|
71
85
|
function propsOf(p: any) {
|
|
72
86
|
if (!p) return {}
|
|
@@ -118,12 +132,15 @@ const ALL = '\u0000all'
|
|
|
118
132
|
|
|
119
133
|
let IDS = 0
|
|
120
134
|
|
|
121
|
-
/**
|
|
135
|
+
/**
|
|
136
|
+
* a child that failed to instantiate: renders the owner's error fallback (SYG408). It keeps its
|
|
137
|
+
* props, so only a changed binding target makes another attempt (not every owner render)
|
|
138
|
+
*/
|
|
122
139
|
class Failed {
|
|
123
140
|
last: any; lr = true; ready = true; def: any = null; disposed = false; shown = true
|
|
124
|
-
constructor(public outv: any) {}
|
|
141
|
+
constructor(public outv: any, public props?: any) {}
|
|
125
142
|
render() { return this.outv }
|
|
126
|
-
setProps() {}
|
|
143
|
+
setProps(p: any) { this.props = p }
|
|
127
144
|
dispose() {}
|
|
128
145
|
}
|
|
129
146
|
|
|
@@ -144,6 +161,8 @@ export class Inst {
|
|
|
144
161
|
raw!: any[]
|
|
145
162
|
slots: Record<string, any[]> = {}
|
|
146
163
|
kids = new Map<string, any>()
|
|
164
|
+
/** the children with a string / byId binding: the presence scan's (notify) */
|
|
165
|
+
bound?: Set<any>
|
|
147
166
|
uidBase: string
|
|
148
167
|
/** the parent's view of this instance's output (last vnode / READY it injected) */
|
|
149
168
|
last: any; lr = true
|
|
@@ -177,7 +196,7 @@ export class Inst {
|
|
|
177
196
|
rep: any[] | null = null
|
|
178
197
|
|
|
179
198
|
constructor(
|
|
180
|
-
public app: App, public def: CoreDef, public parent: Inst | null, base: Cell,
|
|
199
|
+
public app: App, public def: CoreDef, public parent: Inst | null, public base: Cell,
|
|
181
200
|
public dom: any, props: Record<string, any>, children: any[], public scope: string | undefined,
|
|
182
201
|
uidBase: string, public kind: 'root' | 'child' | 'item' | 'page' = parent ? 'child' : 'root', reset = false,
|
|
183
202
|
) {
|
|
@@ -285,8 +304,26 @@ export class Inst {
|
|
|
285
304
|
}
|
|
286
305
|
/** called after each action and before each render pass: STATE.stream emits a new state */
|
|
287
306
|
notify() {
|
|
288
|
-
|
|
289
|
-
|
|
307
|
+
// A completed action can remove and reinsert a target before the next render. Owners
|
|
308
|
+
// observe each transition, including hidden pages; the current action finishes first.
|
|
309
|
+
// only the bound children (a string / byId binding), not every child
|
|
310
|
+
if (this.bound) for (const k of this.bound) {
|
|
311
|
+
if (k.disposed) { this.bound.delete(k); continue }
|
|
312
|
+
const previous = k.bs, state = k.bs = bindingState(k)
|
|
313
|
+
if (previous !== undefined && state === undefined) {
|
|
314
|
+
// Cleanup reads its last owned snapshot. A dying path must not recreate the
|
|
315
|
+
// deleted target; the fresh lifetime receives a new writable cell at reconciliation.
|
|
316
|
+
const cell = k.cell || k.arr
|
|
317
|
+
cell.get = () => previous
|
|
318
|
+
cell.set = () => {}
|
|
319
|
+
k.dispose()
|
|
320
|
+
this.forced = true
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
if (this.st$) {
|
|
324
|
+
const v = this.cell.raw()
|
|
325
|
+
if (v !== this.stLast && v !== undefined) { this.stLast = v; this.st$.shamefullySendNext(v) }
|
|
326
|
+
}
|
|
290
327
|
}
|
|
291
328
|
dispose$() {
|
|
292
329
|
return this.disp$ ||= xs.create()
|
|
@@ -495,9 +532,16 @@ export class Inst {
|
|
|
495
532
|
let view = data.c
|
|
496
533
|
if (view) for (const r of resolvers) view = r(view, this) || view
|
|
497
534
|
let k = this.kids.get(id)
|
|
498
|
-
|
|
535
|
+
// Collection and VirtualCollection share an array cell; Switchable binds `state`.
|
|
536
|
+
const binding = host && sel != 'switchable' ? 'from' : 'state'
|
|
537
|
+
if (k && (k.disposed || (view && k.def && k.def.view !== view) || bindingChanged(k.props?.[binding], props[binding]))) { k.dispose(); this.kids.delete(id); k = undefined }
|
|
499
538
|
if (k) k.setProps(props, children, n, id)
|
|
500
|
-
else this.kids.set(id, host ? this.host(host, props, children, id, n) : this.child(view, props, children, id, !!data.props?.resetState))
|
|
539
|
+
else this.kids.set(id, k = host ? this.host(host, props, children, id, n) : this.child(view, props, children, id, !!data.props?.resetState))
|
|
540
|
+
if (typeof props[binding] == 'string' || props[binding]?.[BINDING]) {
|
|
541
|
+
k.bs = bindingState(k)
|
|
542
|
+
;(this.bound ||= new Set()).add(k)
|
|
543
|
+
this.app.watchers.add(this)
|
|
544
|
+
}
|
|
501
545
|
return
|
|
502
546
|
}
|
|
503
547
|
if (posts[sel] && !(ps ||= []).includes(sel)) ps.push(sel)
|
|
@@ -511,11 +555,11 @@ export class Inst {
|
|
|
511
555
|
|
|
512
556
|
/** a host (Collection, Switchable); one that throws renders the owner's error fallback (SYG408) */
|
|
513
557
|
host(make: any, props: Record<string, any>, children: any[], id: string, n: any): any {
|
|
514
|
-
try { return make(this, props, children, id, n) } catch (err) { return this.failed(err) }
|
|
558
|
+
try { return make(this, props, children, id, n) } catch (err) { return this.failed(err, props) }
|
|
515
559
|
}
|
|
516
560
|
|
|
517
561
|
/** the owner's error fallback for a child that failed to instantiate (SYG408) */
|
|
518
|
-
failed(err: any): Failed {
|
|
562
|
+
failed(err: any, props?: any): Failed {
|
|
519
563
|
const e = err instanceof Error ? err : new Error(String(err))
|
|
520
564
|
let out: any = errorDiv(this.def.name)
|
|
521
565
|
if (this.def.onError) {
|
|
@@ -523,7 +567,7 @@ export class Inst {
|
|
|
523
567
|
catch (fe) { logError('SYG407', this, 'onError threw; rendering an empty error <div>', 'Make onError return a vnode', fe) }
|
|
524
568
|
}
|
|
525
569
|
this.app.caught(this, 'SYG408', 'Sub-component threw; rendering the error fallback', e, 'instantiate')
|
|
526
|
-
return new Failed(out)
|
|
570
|
+
return new Failed(out, props)
|
|
527
571
|
}
|
|
528
572
|
|
|
529
573
|
/** a tag child: state="key" | state={lens} | none (the parent's state) | isolatedState (D174: `resetState`) */
|
|
@@ -543,7 +587,7 @@ export class Inst {
|
|
|
543
587
|
return new Inst(app, def, this, cell, this.dom && this.dom.isolateSource(this.dom, scope), props, children, scope,
|
|
544
588
|
this.uid(uidPart(id.replace(/.*::(r\.)?/, ''))), 'child', reset)
|
|
545
589
|
} catch (err) {
|
|
546
|
-
return this.failed(err)
|
|
590
|
+
return this.failed(err, props)
|
|
547
591
|
}
|
|
548
592
|
}
|
|
549
593
|
|
|
@@ -589,7 +633,7 @@ export class Inst {
|
|
|
589
633
|
const subs = this.subs
|
|
590
634
|
if (subs.length) tearDown(() => { for (const s of subs) s.unsubscribe() }, app.dq, this)
|
|
591
635
|
this.subs = []
|
|
592
|
-
|
|
636
|
+
app.watchers.delete(this)
|
|
593
637
|
this.hub = null
|
|
594
638
|
}
|
|
595
639
|
}
|
package/src/core/runtime.ts
CHANGED
|
@@ -96,6 +96,8 @@ export class App {
|
|
|
96
96
|
/** run()'s hooks, then each addHooks() layer (G-297) */
|
|
97
97
|
layers: Hooks[]
|
|
98
98
|
born: Inst[] = []
|
|
99
|
+
/** Last state version delivered to stream listeners and binding owners. */
|
|
100
|
+
nv?: number
|
|
99
101
|
watchers = new Set<Inst>()
|
|
100
102
|
/** streams the disposed instances left without listeners, stopped when the drain / flush ends */
|
|
101
103
|
dq: any[] = []
|
|
@@ -185,6 +187,8 @@ export class App {
|
|
|
185
187
|
}
|
|
186
188
|
/** STATE.stream: the instances whose state changed emit it */
|
|
187
189
|
notify() {
|
|
190
|
+
if (this.nv === this.ver) return
|
|
191
|
+
this.nv = this.ver
|
|
188
192
|
for (const i of this.watchers) i.notify()
|
|
189
193
|
}
|
|
190
194
|
|
|
@@ -280,7 +284,7 @@ export class App {
|
|
|
280
284
|
this.ran = false
|
|
281
285
|
this.last = v
|
|
282
286
|
this.hooks.onPatch?.(v)
|
|
283
|
-
this.vdomL?.next(v)
|
|
287
|
+
this.vdomL?.next(v ?? null)
|
|
284
288
|
}
|
|
285
289
|
if (this.dq.length) this.stopLater()
|
|
286
290
|
} finally {
|
package/src/core/view.ts
CHANGED
|
@@ -14,6 +14,8 @@ class View implements InstanceView {
|
|
|
14
14
|
get state() { return this.i.cell.get() }
|
|
15
15
|
get props() { return this.i.props }
|
|
16
16
|
get context() { return this.i.context() }
|
|
17
|
+
/** D530: the context keys the last render's view read (null: none rendered, or no context) */
|
|
18
|
+
get reads() { return this.i.keys }
|
|
17
19
|
get isRoot() { return !this.i.parent }
|
|
18
20
|
get kind() { return this.i.kind }
|
|
19
21
|
/** false on a hidden Switchable page and below it */
|
package/src/cycle/dom/utils.ts
CHANGED
|
@@ -88,9 +88,11 @@ export function makeInsert(
|
|
|
88
88
|
}
|
|
89
89
|
|
|
90
90
|
// G-261: a DOM change Sygnal makes outside a patch (a Transition's leave, a Portal mounted late)
|
|
91
|
-
// re-emits the DOM source of the app whose root contains `el` (makeDOMDriver listens on its root)
|
|
91
|
+
// re-emits the DOM source of the app whose root contains `el` (makeDOMDriver listens on its root).
|
|
92
|
+
// The event comes from the element's own window: jsdom refuses an `Event` from another realm (a
|
|
93
|
+
// Transition's leave timer that fires after a jsdom test environment was torn down)
|
|
92
94
|
export const POKE = 'sygnal-dom';
|
|
93
|
-
export const pokeDOM = (el: any): any => el?.dispatchEvent(new Event(POKE, {bubbles: true}));
|
|
95
|
+
export const pokeDOM = (el: any): any => el?.dispatchEvent(new (el.ownerDocument?.defaultView?.Event || Event)(POKE, {bubbles: true}));
|
|
94
96
|
|
|
95
97
|
const obj = (o: any) => o !== null && typeof o == 'object' && !Array.isArray(o);
|
|
96
98
|
/**
|
package/src/devtools.d.ts
CHANGED
|
@@ -168,3 +168,16 @@ export declare function installMcpBridge(
|
|
|
168
168
|
hot: { on(event: string, cb: (data: any) => void): void; send(event: string, data?: any): void } | undefined,
|
|
169
169
|
options?: McpBridgeOptions
|
|
170
170
|
): void
|
|
171
|
+
|
|
172
|
+
// ── PLAN-10 P-2: the app side of sygnal/vite's graph page (/__sygnal/graph) ──
|
|
173
|
+
|
|
174
|
+
export interface GraphBridgeOptions {
|
|
175
|
+
/** The BroadcastChannel the graph page listens on (default 'sygnal:graph') */
|
|
176
|
+
channel?: string
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* Answer the dev server's graph page ("Live data": instances, action flashes, state) over a
|
|
180
|
+
* same-origin BroadcastChannel. sygnal/vite calls it in dev while the graph route is on (the
|
|
181
|
+
* default; `graph: false` turns it off); you don't call it yourself.
|
|
182
|
+
*/
|
|
183
|
+
export declare function installGraphBridge(options?: GraphBridgeOptions): void
|
package/src/devtools.ts
CHANGED
|
@@ -35,3 +35,6 @@ export function copyAsTest(target?: any, options?: CopyAsTestOptions): string {
|
|
|
35
35
|
|
|
36
36
|
// PLAN-6 E-1: the page side of sygnal/vite's dev MCP endpoint (`sygnal({ mcp: true })`)
|
|
37
37
|
export {installMcpBridge} from './extra/devMcp'
|
|
38
|
+
|
|
39
|
+
// PLAN-10 P-2 (D544): the app side of the graph page's "Live data" (sygnal/vite's /__sygnal/graph)
|
|
40
|
+
export {installGraphBridge} from './extra/devGraph'
|
|
@@ -92,7 +92,7 @@
|
|
|
92
92
|
* closed as not run, an app confirm dropped (a server approval stays answerable), and a card that
|
|
93
93
|
* was composing composed again; open client calls stay answerable.
|
|
94
94
|
*/
|
|
95
|
-
import {defineBehavior} from '../../behaviors'
|
|
95
|
+
import {defineBehavior, fromPrevious} from '../../behaviors'
|
|
96
96
|
import {ABORT} from '../../../shared'
|
|
97
97
|
import {report} from '../../diagnostics/index'
|
|
98
98
|
import xs from '../../xstreamCompat'
|
|
@@ -529,7 +529,7 @@ export const chat = (options: any = {}): any => {
|
|
|
529
529
|
...(c.ask?.controls && DOM && {CONTROL: DOM.select('[data-compose-control]').events('click').map(controlOf).filter((x: any) => !!x)}),
|
|
530
530
|
}
|
|
531
531
|
},
|
|
532
|
-
model: {
|
|
532
|
+
model: fromPrevious({
|
|
533
533
|
PROMPT: (s: any, p: any) => typeof p == 'string' && p !== s.prompt ? {...s, prompt: p} : ABORT,
|
|
534
534
|
SEND: {
|
|
535
535
|
STATE: (s: any, d: any) => sendStep(s, d) || ABORT,
|
|
@@ -609,7 +609,7 @@ export const chat = (options: any = {}): any => {
|
|
|
609
609
|
ANSWER: {STATE: stateOf(S.answerCard), ...sinks(S.answerCard)},
|
|
610
610
|
CONTROL: (s: any, d: any) => S.controlStep(s, d) || ABORT,
|
|
611
611
|
}),
|
|
612
|
-
},
|
|
612
|
+
}),
|
|
613
613
|
})(options)
|
|
614
614
|
// the `prompt` option is a selector, not the slice's start value (defineBehavior's option override)
|
|
615
615
|
b.state = {...b.state, prompt: ''}
|
|
@@ -48,7 +48,7 @@
|
|
|
48
48
|
* It reaches the runtime through the shared link (./link.ts, D283), as `chat` does.
|
|
49
49
|
*/
|
|
50
50
|
import xs from 'xstream'
|
|
51
|
-
import {defineBehavior} from '../behaviors'
|
|
51
|
+
import {defineBehavior, fromPrevious} from '../behaviors'
|
|
52
52
|
import {ABORT} from '../../shared'
|
|
53
53
|
import {agentTools, labelsUntrusted} from './agent/index'
|
|
54
54
|
import {decide as decideRequest} from './decide'
|
|
@@ -242,7 +242,7 @@ export const commandBar = (options: any = {}): any => {
|
|
|
242
242
|
...(deny && {DENY: on(DOM, deny)}),
|
|
243
243
|
}
|
|
244
244
|
},
|
|
245
|
-
model: {
|
|
245
|
+
model: fromPrevious({
|
|
246
246
|
INPUT: (s: any, v: any) => typeof v == 'string' && v !== s.text ? {...s, text: v} : ABORT,
|
|
247
247
|
RUN: {
|
|
248
248
|
STATE: (s: any, d: any, _n: any, p: any, o: any, k: string) => runStep(s, d, E(p, k, o))?.s || ABORT,
|
|
@@ -275,7 +275,7 @@ export const commandBar = (options: any = {}): any => {
|
|
|
275
275
|
// a command ran (or was refused): the result; the input is cleared when it was this command
|
|
276
276
|
DONE: (s: any, d: any) => s.status != 'running' ? ABORT
|
|
277
277
|
: {...s, status: 'ready', command: null, pending: null, result: d, text: d?.ok && s.text.trim() === d.command ? '' : s.text},
|
|
278
|
-
},
|
|
278
|
+
}),
|
|
279
279
|
})(options)
|
|
280
280
|
return linked(b)
|
|
281
281
|
}
|
|
@@ -34,7 +34,7 @@
|
|
|
34
34
|
* reads it as `props.state` (the pre-action state), and one memo per behavior value makes STATE,
|
|
35
35
|
* the sink and EFFECT of one action compute the same step.
|
|
36
36
|
*/
|
|
37
|
-
import {defineBehavior} from '../../behaviors'
|
|
37
|
+
import {defineBehavior, fromPrevious} from '../../behaviors'
|
|
38
38
|
import {ABORT} from '../../../shared'
|
|
39
39
|
import {fail} from '../../diagnostics/legacy'
|
|
40
40
|
import {finish, fromTree, removeNode, toTree} from './layout'
|
|
@@ -144,7 +144,7 @@ export function composeBehavior(catalog: Catalog, options: any = {}): any {
|
|
|
144
144
|
.map(({key, name, value}: any) => ({key, name, value}))}),
|
|
145
145
|
}
|
|
146
146
|
},
|
|
147
|
-
model: {
|
|
147
|
+
model: fromPrevious({
|
|
148
148
|
PROMPT: (s: any, p: any) => typeof p == 'string' && p !== s.prompt ? {...s, prompt: p} : ABORT,
|
|
149
149
|
COMPOSE: {HOST: host(start), [S]: sink(start)},
|
|
150
150
|
REPLY: {
|
|
@@ -178,6 +178,6 @@ export function composeBehavior(catalog: Catalog, options: any = {}): any {
|
|
|
178
178
|
CONTROL: (s: any, c: any) => c && typeof c.key == 'string' && typeof c.name == 'string'
|
|
179
179
|
? {...s, ui: {...s.ui, [c.key]: {...s.ui?.[c.key], [c.name]: c.value}}} : ABORT,
|
|
180
180
|
DONE: () => ABORT,
|
|
181
|
-
},
|
|
181
|
+
}),
|
|
182
182
|
})(rest)
|
|
183
183
|
}
|
|
@@ -60,8 +60,12 @@ export async function post(url: string, body: any, req: any, signal: AbortSignal
|
|
|
60
60
|
return res;
|
|
61
61
|
}
|
|
62
62
|
|
|
63
|
-
/**
|
|
64
|
-
|
|
63
|
+
/**
|
|
64
|
+
* the SSE frames of a response body: `{ event, data }` (data: the joined data lines). PLAN-11
|
|
65
|
+
* E-1: `meta` (optional) follows the stream's `id` (the last event id, kept across frames) and
|
|
66
|
+
* `retry` (ms) fields, as EventSource does, for a reconnect
|
|
67
|
+
*/
|
|
68
|
+
export async function* sse(res: any, meta?: {id?: string; retry?: number}): AsyncGenerator<{event: string; data: string}> {
|
|
65
69
|
const reader = res.body.getReader(), dec = new TextDecoder();
|
|
66
70
|
let buf = '', cr = '', done = false;
|
|
67
71
|
try {
|
|
@@ -84,6 +88,8 @@ export async function* sse(res: any): AsyncGenerator<{event: string; data: strin
|
|
|
84
88
|
const v = c < 0 ? '' : line.slice(c + 1).replace(/^ /, '');
|
|
85
89
|
if (field == 'event') event = v;
|
|
86
90
|
else if (field == 'data') data.push(v);
|
|
91
|
+
else if (meta && field == 'id' && !v.includes('\0')) meta.id = v;
|
|
92
|
+
else if (meta && field == 'retry' && /^\d+$/.test(v)) meta.retry = +v;
|
|
87
93
|
}
|
|
88
94
|
if (data.length) yield {event, data: data.join('\n')};
|
|
89
95
|
}
|
package/src/extra/behaviors.ts
CHANGED
|
@@ -45,6 +45,27 @@
|
|
|
45
45
|
*/
|
|
46
46
|
import xs from './xstreamCompat'
|
|
47
47
|
import {isAbort} from '../shared'
|
|
48
|
+
/** A behavior output's slice from before the action (outputs get the updated one): for a transition shared with STATE */
|
|
49
|
+
export const was = (s: any, p: any, k: string) => p?.previousState ? p.previousState[k] : s
|
|
50
|
+
/**
|
|
51
|
+
* A behavior model whose outputs compute from the state before the action (its slice and
|
|
52
|
+
* `props.state`), for behaviors whose STATE and outputs share one transition; STATE / HOST as is
|
|
53
|
+
*/
|
|
54
|
+
export const fromPrevious = (model: any) => {
|
|
55
|
+
const out: any = {}
|
|
56
|
+
for (const a in model) {
|
|
57
|
+
const e = model[a]
|
|
58
|
+
if (!e || typeof e != 'object') { out[a] = e; continue }
|
|
59
|
+
const m: any = {}
|
|
60
|
+
for (const s in e) {
|
|
61
|
+
const f = e[s]
|
|
62
|
+
m[s] = s == 'STATE' || s == 'HOST' || typeof f != 'function' ? f
|
|
63
|
+
: (x: any, d: any, n: any, p: any, o: any, k: string) => f(was(x, p, k), d, n, p?.previousState ? {...p, state: p.previousState} : p, o, k)
|
|
64
|
+
}
|
|
65
|
+
out[a] = m
|
|
66
|
+
}
|
|
67
|
+
return out
|
|
68
|
+
}
|
|
48
69
|
// a model entry as { sink: fn }: a constant is sent as is, `true` sends the action's data, an
|
|
49
70
|
// EFFECT constant does nothing (as the core treats them)
|
|
50
71
|
const sinksOf = (e: any, S: string): any => {
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PLAN-10 P-2 (D544): the app side of the graph page's "Live data" (sygnal/vite serves the page at
|
|
3
|
+
* `/__sygnal/graph` in dev, W-2 / D543). Dev only: it is part of the 'sygnal/devtools' entry, and
|
|
4
|
+
* sygnal/vite's 'virtual:sygnal/graph' module (served by the dev server, never in a build) calls
|
|
5
|
+
* installGraphBridge().
|
|
6
|
+
*
|
|
7
|
+
* The page is served by the dev server on the app's own origin, so the two talk over a
|
|
8
|
+
* BroadcastChannel ('sygnal:graph'): no server round trip, no HMR traffic, nothing a page on
|
|
9
|
+
* another origin can join. Every message carries `v: 1`.
|
|
10
|
+
*
|
|
11
|
+
* page → apps { type: 'who', watch? } every 2 s while the page is open; `watch` names
|
|
12
|
+
* the tab the page follows (it keeps that tab
|
|
13
|
+
* sending for 6 s)
|
|
14
|
+
* app → page { type: 'here', tab, url, title, at, ok } the answer, and on load / focus
|
|
15
|
+
* (`at`: when the tab was last loaded or focused;
|
|
16
|
+
* `ok`: inspect() is there, i.e. diagnostics on)
|
|
17
|
+
* app → page { type: 'graph', tab, graph } inspect({ actions: 200 }) of the page's apps, JSON
|
|
18
|
+
* safe: when it starts being watched, then 150 ms
|
|
19
|
+
* after the last action or component change
|
|
20
|
+
* app → page { type: 'action', tab, record } { type, instance, component } at once (a flash)
|
|
21
|
+
* page → app { type: 'get-state', tab, req, id } an instance's state
|
|
22
|
+
* app → page { type: 'state', tab, req, id, value?, error? }
|
|
23
|
+
* app → page { type: 'bye', tab } the tab is going away (pagehide)
|
|
24
|
+
*
|
|
25
|
+
* State values go through the DevTools preview (JSON safe, as the MCP tools send them). The static
|
|
26
|
+
* keys the page joins on are on the component functions (sygnal/vite's dev transform) and come out
|
|
27
|
+
* of inspect() as `key`.
|
|
28
|
+
*/
|
|
29
|
+
import {getDevTools} from './devtools'
|
|
30
|
+
import {onAction} from './devtoolsActions'
|
|
31
|
+
import {addLayer} from './devtoolsNext'
|
|
32
|
+
import {pageTools, trackApps} from './devMcp'
|
|
33
|
+
|
|
34
|
+
export const GRAPH_CHANNEL = 'sygnal:graph'
|
|
35
|
+
const WATCH_MS = 6000
|
|
36
|
+
const SETTLE_MS = 150
|
|
37
|
+
|
|
38
|
+
export interface GraphBridgeOptions {
|
|
39
|
+
/** the BroadcastChannel name (default 'sygnal:graph') */
|
|
40
|
+
channel?: string
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
let installed = false
|
|
44
|
+
|
|
45
|
+
/** Connect this page to the graph page's "Live data" (sygnal/vite calls it in dev). Once per page. */
|
|
46
|
+
export function installGraphBridge(options: GraphBridgeOptions = {}): void {
|
|
47
|
+
const g: any = globalThis
|
|
48
|
+
if (installed || typeof g.BroadcastChannel != 'function' || !g.window) return
|
|
49
|
+
installed = true
|
|
50
|
+
trackApps()
|
|
51
|
+
const bc = new g.BroadcastChannel(options.channel || GRAPH_CHANNEL)
|
|
52
|
+
const tab = Math.random().toString(36).slice(2, 10) + Date.now().toString(36)
|
|
53
|
+
const tools = pageTools({})
|
|
54
|
+
let at = Date.now()
|
|
55
|
+
let watchedUntil = 0
|
|
56
|
+
let timer: any = 0
|
|
57
|
+
|
|
58
|
+
const inspect = () => {
|
|
59
|
+
const f = g.__SYGNAL_DIAGNOSTICS__?.inspect || (getDevTools() as any)?.inspect
|
|
60
|
+
return typeof f == 'function' ? f : null
|
|
61
|
+
}
|
|
62
|
+
const post = (m: any) => { try { bc.postMessage({v: 1, tab, ...m}) } catch (_) { /* closed */ } }
|
|
63
|
+
const here = () => post({type: 'here', url: String(g.location?.href || ''), title: String(g.document?.title || ''), at, ok: !!inspect()})
|
|
64
|
+
const watched = () => Date.now() < watchedUntil
|
|
65
|
+
|
|
66
|
+
const sendGraph = () => {
|
|
67
|
+
clearTimeout(timer)
|
|
68
|
+
timer = 0
|
|
69
|
+
const f = inspect()
|
|
70
|
+
if (!watched() || !f) return
|
|
71
|
+
let graph: any
|
|
72
|
+
try { graph = JSON.parse(JSON.stringify(f({actions: 200}))) } catch (_) { return }
|
|
73
|
+
post({type: 'graph', graph})
|
|
74
|
+
}
|
|
75
|
+
const settle = () => {
|
|
76
|
+
if (!watched()) return
|
|
77
|
+
clearTimeout(timer)
|
|
78
|
+
timer = setTimeout(sendGraph, SETTLE_MS)
|
|
79
|
+
}
|
|
80
|
+
const watch = () => {
|
|
81
|
+
const was = watched()
|
|
82
|
+
watchedUntil = Date.now() + WATCH_MS
|
|
83
|
+
if (!was) sendGraph()
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
bc.onmessage = (e: any) => {
|
|
87
|
+
const m = e && e.data
|
|
88
|
+
if (!m || m.v !== 1 || typeof m.type != 'string') return
|
|
89
|
+
if (m.type == 'who') {
|
|
90
|
+
if (m.watch === tab) watch()
|
|
91
|
+
here()
|
|
92
|
+
} else if (m.type == 'get-state' && m.tab === tab) {
|
|
93
|
+
let value: any, error: string | undefined
|
|
94
|
+
try { value = (tools.get_state({component: String(m.id)}, m.req) as any).state } catch (err: any) { error = String(err?.message || err) }
|
|
95
|
+
post({type: 'state', req: m.req, id: m.id, ...(error ? {error} : {value})})
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
onAction((a: any, kind) => {
|
|
100
|
+
if (!watched()) return
|
|
101
|
+
if (kind == 'add' && a) post({type: 'action', record: {type: a.type, instance: String(a.instance), component: a.component}})
|
|
102
|
+
settle()
|
|
103
|
+
})
|
|
104
|
+
addLayer(() => ({onCreate: settle, onDispose: settle}))
|
|
105
|
+
|
|
106
|
+
const seen = () => { at = Date.now(); here() }
|
|
107
|
+
g.addEventListener?.('focus', seen)
|
|
108
|
+
g.document?.addEventListener?.('visibilitychange', () => { if (g.document.visibilityState == 'visible') seen() })
|
|
109
|
+
g.addEventListener?.('pagehide', () => post({type: 'bye'}))
|
|
110
|
+
here()
|
|
111
|
+
}
|
package/src/extra/devMcp.ts
CHANGED
package/src/extra/devtools.ts
CHANGED
|
@@ -146,6 +146,24 @@ export class SygnalDevTools {
|
|
|
146
146
|
case 'GET_STATE':
|
|
147
147
|
this._sendComponentState(msg.payload.componentId);
|
|
148
148
|
break;
|
|
149
|
+
case 'GET_GRAPH':
|
|
150
|
+
this._post('GRAPH', this._graph(msg.payload));
|
|
151
|
+
break;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* PLAN-10 P-1: the runtime app graph for the panel's App graph view: inspect() with the recent
|
|
157
|
+
* actions, JSON-safe. inspect() is on this object only when 'sygnal/diagnostics' is loaded
|
|
158
|
+
* (sygnal/vite loads it in dev); without it the panel shows why.
|
|
159
|
+
*/
|
|
160
|
+
_graph(opts?: {actions?: number}): {graph: any; reason?: string; message?: string} {
|
|
161
|
+
const inspect = (this as any).inspect;
|
|
162
|
+
if (typeof inspect !== 'function') return {graph: null, reason: 'no-diagnostics'};
|
|
163
|
+
try {
|
|
164
|
+
return {graph: JSON.parse(JSON.stringify(inspect({actions: opts?.actions ?? 200})))};
|
|
165
|
+
} catch (e: any) {
|
|
166
|
+
return {graph: null, reason: 'error', message: String(e?.message ?? e)};
|
|
149
167
|
}
|
|
150
168
|
}
|
|
151
169
|
|
|
@@ -12,6 +12,9 @@
|
|
|
12
12
|
* SYG635 — `cache: true` / `staleTime` / `{ prefetch }` sent to a driver without a
|
|
13
13
|
* `queryCache()` (D88): nothing is cached (warn). The source's `__inspect().cache`
|
|
14
14
|
* is undefined without one
|
|
15
|
+
* SYG636 — PLAN-11 U-1: `progress` that can't be reported as asked: on a resource or a
|
|
16
|
+
* `batch` request (ignored), or with fetch-only `init` options that XMLHttpRequest
|
|
17
|
+
* doesn't support (ignored for that request) (warn)
|
|
15
18
|
*
|
|
16
19
|
* Mechanism: onModel wraps the component's own model stream for every sink whose source
|
|
17
20
|
* takes the `resources` static (makeFetchDriver and renderComponent's HTTP fake) and checks
|
|
@@ -24,6 +27,12 @@ import {devReport, once, nameOf} from './shared'
|
|
|
24
27
|
import {isStandardSchema} from '../../standardSchema'
|
|
25
28
|
|
|
26
29
|
const IDEMPOTENT = /^(GET|HEAD)$/i
|
|
30
|
+
// PLAN-11 U-1: fetch() init options a progress request (XMLHttpRequest) can't honour
|
|
31
|
+
const XHR_IGNORES = ['keepalive', 'priority', 'cache', 'redirect', 'integrity', 'mode', 'referrer', 'referrerPolicy', 'window', 'duplex']
|
|
32
|
+
const reportProgress = (component: any, sink: string, url: string, why: string, fix: string) => {
|
|
33
|
+
if (!once(`SYG636:${nameOf(component)}:${url}:${why}`)) return
|
|
34
|
+
devReport('SYG636', {component, message: `${sink} ${url ? url + ' ' : ''}has progress, but ${why}`, fix, data: {sink, url}})
|
|
35
|
+
}
|
|
27
36
|
|
|
28
37
|
/** instance → reply key lanes it has sent, and ok name → the other key it was sent under */
|
|
29
38
|
const lanes = new WeakMap<object, {used: Set<string>; keyed: Map<string, string>}>()
|
|
@@ -48,7 +57,7 @@ function checkOne(component: any, sink: string, req: any, where: string) {
|
|
|
48
57
|
if (!req || typeof req !== 'object') return
|
|
49
58
|
const name = nameOf(component)
|
|
50
59
|
const url = typeof req.url === 'string' ? req.url : ''
|
|
51
|
-
const method = String(req.method || (req.json !== undefined || req.body !== undefined ? 'POST' : 'GET')).toUpperCase()
|
|
60
|
+
const method = String(req.method || (req.json !== undefined || req.body !== undefined || req.multipart !== undefined ? 'POST' : 'GET')).toUpperCase()
|
|
52
61
|
if ((req.cache === true || req.staleTime != null) && !IDEMPOTENT.test(method) && once(`SYG630:${name}:${method}:${url}`)) {
|
|
53
62
|
devReport('SYG630', {
|
|
54
63
|
component,
|
|
@@ -58,6 +67,17 @@ function checkOne(component: any, sink: string, req: any, where: string) {
|
|
|
58
67
|
})
|
|
59
68
|
}
|
|
60
69
|
if (req.cache === true || req.staleTime != null) reportNoCache(component, sink, req.cache === true ? 'cache: true' : 'staleTime', url)
|
|
70
|
+
if (req.progress) {
|
|
71
|
+
if (where.includes('.resources.'))
|
|
72
|
+
reportProgress(component, sink, url, 'resources report no progress (it is ignored)', 'Send the download as a request with progress and reply actions instead of a resource, or remove progress')
|
|
73
|
+
else if (req.batch)
|
|
74
|
+
reportProgress(component, sink, url, 'a batch request reports no progress (it is ignored)', 'Send the parts as separate requests with progress, or remove progress')
|
|
75
|
+
else {
|
|
76
|
+
const bad = XHR_IGNORES.filter(k => req.init && req.init[k] !== undefined)
|
|
77
|
+
if (bad.length) reportProgress(component, sink, url, `it goes over XMLHttpRequest, which ignores init.${bad.join(', init.')}`,
|
|
78
|
+
`Remove ${bad.join(', ')} from init for this request, or drop progress if the request needs ${bad.length > 1 ? 'them' : 'it'}`)
|
|
79
|
+
}
|
|
80
|
+
}
|
|
61
81
|
if ('validate' in req && !isStandardSchema(req.validate) && once(`SYG631:${name}:${url}`)) {
|
|
62
82
|
devReport('SYG631', {
|
|
63
83
|
component,
|
|
@@ -133,14 +133,15 @@ export const formsCheck: DiagnosticCheck = {
|
|
|
133
133
|
})
|
|
134
134
|
const model = Object.keys(view.model || {}).map(a => a.split('|')[0].trim()), own = ownActions(b)
|
|
135
135
|
const clash = own.includes(o.submit)
|
|
136
|
-
|
|
136
|
+
// PLAN-11 F-3: with `request`, the form sends the submit itself and `submit` is optional
|
|
137
|
+
if (o.request ? o.submit !== undefined && (typeof o.submit != 'string' || !model.includes(o.submit) || clash) : typeof o.submit != 'string' || !model.includes(o.submit) || clash) {
|
|
137
138
|
devReport('SYG234', {
|
|
138
139
|
component,
|
|
139
|
-
message: typeof o.submit != 'string' ? `${name}'s form '${key}' has no submit action, so a valid submit dispatches nothing`
|
|
140
|
+
message: typeof o.submit != 'string' ? `${name}'s form '${key}' has no submit action and no request, so a valid submit dispatches nothing`
|
|
140
141
|
: clash ? `${name}'s form '${key}' names submit: '${o.submit}', one of the form's own actions; a valid submit dispatches the form's '${key}.${o.submit}'${model.includes(o.submit) ? `, never ${name}'s '${o.submit}' entry` : ''}`
|
|
141
142
|
: `${name}'s form '${key}' dispatches '${o.submit}' on a valid submit, but ${name} has no model entry '${o.submit}'`,
|
|
142
143
|
fix: clash ? SUBMIT_FIX(key, o.submit, own)
|
|
143
|
-
: `
|
|
144
|
+
: `Give the form a request, form(schema, { values, request: { url: '/api/signup' } }) (it sends the valid submit itself), or name a host action and add it to the model: submit: 'SIGN_UP', model: { SIGN_UP: { HTTP: (state, values) => ({ url, json: values, ok: '${key}.DONE', error: '${key}.ERRORS' }) } }`,
|
|
144
145
|
data: {key, submit: o.submit},
|
|
145
146
|
})
|
|
146
147
|
}
|
|
@@ -107,10 +107,11 @@ import {nextHooks} from './next'
|
|
|
107
107
|
|
|
108
108
|
export {checkEventBus} from './events'
|
|
109
109
|
export {configureStrict, isStrictEnabled} from './strict'
|
|
110
|
+
export {configureTimingAudit, isTimingAuditEnabled} from './timingAudit'
|
|
110
111
|
export type {EventBusSummary} from './events'
|
|
111
112
|
export {RXJS_HINTS} from './rxjsHints'
|
|
112
113
|
export {inspect} from './inspect'
|
|
113
|
-
export type {InspectResource, InspectCacheEntry, InspectGraph, InspectComponent, InspectAction, InspectActionTrigger, InspectChild, InspectSelector, InspectControl, InspectDiagnostic, InspectRelatedLocation, InspectFix, InspectTextEdit, InspectOptions, InspectRecentAction} from './public'
|
|
114
|
+
export type {InspectResource, InspectCacheEntry, InspectGraph, InspectComponent, InspectAction, InspectActionTrigger, InspectChild, InspectSelector, InspectControl, InspectDiagnostic, InspectRelatedLocation, InspectFix, InspectTextEdit, InspectOptions, InspectRecentAction, InspectLocation, InspectChildSelect} from './public'
|
|
114
115
|
export {listCodes, getCodeInfo} from '../codes'
|
|
115
116
|
export type {DiagnosticCodeInfo} from '../codes'
|
|
116
117
|
|