sygnal 5.3.7 → 5.4.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.
Files changed (226) hide show
  1. package/README.md +53 -38
  2. package/dist/astro/client.cjs.js +18 -8106
  3. package/dist/astro/client.cjs.js.map +1 -0
  4. package/dist/astro/client.mjs +17 -8105
  5. package/dist/astro/client.mjs.map +1 -0
  6. package/dist/astro/index.cjs.js +768 -9
  7. package/dist/astro/index.cjs.js.map +1 -0
  8. package/dist/astro/index.mjs +768 -9
  9. package/dist/astro/index.mjs.map +1 -0
  10. package/dist/astro/server.cjs.js +4 -1
  11. package/dist/astro/server.cjs.js.map +1 -0
  12. package/dist/astro/server.mjs +4 -1
  13. package/dist/astro/server.mjs.map +1 -0
  14. package/dist/diagnostics.cjs.js +1297 -0
  15. package/dist/diagnostics.cjs.js.map +1 -0
  16. package/dist/diagnostics.esm.js +1267 -0
  17. package/dist/diagnostics.esm.js.map +1 -0
  18. package/dist/index.cjs.js +2981 -1386
  19. package/dist/index.cjs.js.map +1 -0
  20. package/dist/index.d.ts +848 -111
  21. package/dist/index.esm.js +2997 -1411
  22. package/dist/index.esm.js.map +1 -0
  23. package/dist/jsx-dev-runtime.cjs.js +15 -9
  24. package/dist/jsx-dev-runtime.cjs.js.map +1 -0
  25. package/dist/jsx-dev-runtime.esm.js +15 -9
  26. package/dist/jsx-dev-runtime.esm.js.map +1 -0
  27. package/dist/jsx-runtime.cjs.js +15 -9
  28. package/dist/jsx-runtime.cjs.js.map +1 -0
  29. package/dist/jsx-runtime.esm.js +15 -9
  30. package/dist/jsx-runtime.esm.js.map +1 -0
  31. package/dist/jsx.cjs.js +15 -9
  32. package/dist/jsx.cjs.js.map +1 -0
  33. package/dist/jsx.esm.js +15 -9
  34. package/dist/jsx.esm.js.map +1 -0
  35. package/dist/sygnal.min.js +2 -1
  36. package/dist/sygnal.min.js.map +1 -0
  37. package/dist/vike/+config.cjs.js +1 -0
  38. package/dist/vike/+config.cjs.js.map +1 -0
  39. package/dist/vike/+config.js +1 -0
  40. package/dist/vike/+config.js.map +1 -0
  41. package/dist/vike/ClientOnly.cjs.js +1 -0
  42. package/dist/vike/ClientOnly.cjs.js.map +1 -0
  43. package/dist/vike/ClientOnly.mjs +1 -0
  44. package/dist/vike/ClientOnly.mjs.map +1 -0
  45. package/dist/vike/onRenderClient.cjs.js +1 -0
  46. package/dist/vike/onRenderClient.cjs.js.map +1 -0
  47. package/dist/vike/onRenderClient.mjs +1 -0
  48. package/dist/vike/onRenderClient.mjs.map +1 -0
  49. package/dist/vike/onRenderHtml.cjs.js +1 -0
  50. package/dist/vike/onRenderHtml.cjs.js.map +1 -0
  51. package/dist/vike/onRenderHtml.mjs +1 -0
  52. package/dist/vike/onRenderHtml.mjs.map +1 -0
  53. package/dist/vite/plugin.cjs.js +670 -44
  54. package/dist/vite/plugin.cjs.js.map +1 -0
  55. package/dist/vite/plugin.mjs +670 -44
  56. package/dist/vite/plugin.mjs.map +1 -0
  57. package/llms.txt +250 -0
  58. package/package.json +15 -6
  59. package/src/astro/client.ts +13 -2
  60. package/src/astro/index.d.ts +24 -1
  61. package/src/astro/index.ts +40 -9
  62. package/src/astro/server.ts +3 -1
  63. package/src/collection.ts +5 -2
  64. package/src/component.ts +375 -330
  65. package/src/cycle/dom/MainDOMSource.ts +4 -0
  66. package/src/cycle/dom/classNameModule.ts +74 -0
  67. package/src/cycle/dom/controlledInputModule.ts +33 -0
  68. package/src/cycle/dom/enrichEventStream.ts +9 -3
  69. package/src/cycle/dom/makeDOMDriver.ts +1 -2
  70. package/src/cycle/dom/mockDOMSource.ts +62 -8
  71. package/src/cycle/dom/modules.ts +5 -1
  72. package/src/cycle/state/Collection.ts +13 -3
  73. package/src/cycle/state/StateSource.ts +1 -1
  74. package/src/cycle/state/pickCombine.ts +35 -1
  75. package/src/cycle/state/withState.ts +1 -1
  76. package/src/extra/devtools.ts +8 -0
  77. package/src/extra/diagnostics/checks/collections.ts +67 -0
  78. package/src/extra/diagnostics/checks/dom.ts +241 -0
  79. package/src/extra/diagnostics/checks/events.ts +68 -0
  80. package/src/extra/diagnostics/checks/index.ts +109 -0
  81. package/src/extra/diagnostics/checks/inspect.ts +319 -0
  82. package/src/extra/diagnostics/checks/props.ts +51 -0
  83. package/src/extra/diagnostics/checks/public.d.ts +211 -0
  84. package/src/extra/diagnostics/checks/rxjsHints.ts +88 -0
  85. package/src/extra/diagnostics/checks/shared.ts +119 -0
  86. package/src/extra/diagnostics/checks/state.ts +55 -0
  87. package/src/extra/diagnostics/checks/strict.ts +112 -0
  88. package/src/extra/diagnostics/checks/wiring.ts +65 -0
  89. package/src/extra/diagnostics/codes.ts +232 -0
  90. package/src/extra/diagnostics/index.ts +328 -0
  91. package/src/extra/diagnostics/legacy.ts +59 -0
  92. package/src/extra/driverFactories.ts +106 -58
  93. package/src/extra/eventDriver.ts +4 -0
  94. package/src/extra/flatten.ts +75 -0
  95. package/src/extra/reducers.ts +12 -5
  96. package/src/extra/run.ts +11 -0
  97. package/src/extra/testing.ts +890 -180
  98. package/src/extra/xstreamExtras.ts +269 -0
  99. package/src/index.d.ts +428 -17
  100. package/src/index.ts +7 -6
  101. package/src/pragma/index.ts +14 -9
  102. package/src/switchable.ts +10 -8
  103. package/src/vike/types.ts +21 -4
  104. package/src/vite/plugin.d.ts +95 -2
  105. package/src/vite/plugin.ts +720 -54
  106. package/dist/astro/astro/client.d.ts +0 -5
  107. package/dist/astro/astro/index.d.ts +0 -16
  108. package/dist/astro/astro/server.d.ts +0 -12
  109. package/dist/astro/client.d.ts +0 -5
  110. package/dist/astro/client.esm.js +0 -4665
  111. package/dist/astro/collection.d.ts +0 -12
  112. package/dist/astro/component.d.ts +0 -22
  113. package/dist/astro/cycle/dom/BodyDOMSource.d.ts +0 -10
  114. package/dist/astro/cycle/dom/DOMSource.d.ts +0 -11
  115. package/dist/astro/cycle/dom/DocumentDOMSource.d.ts +0 -12
  116. package/dist/astro/cycle/dom/ElementFinder.d.ts +0 -8
  117. package/dist/astro/cycle/dom/EventDelegator.d.ts +0 -34
  118. package/dist/astro/cycle/dom/IsolateModule.d.ts +0 -23
  119. package/dist/astro/cycle/dom/MainDOMSource.d.ts +0 -32
  120. package/dist/astro/cycle/dom/PriorityQueue.d.ts +0 -7
  121. package/dist/astro/cycle/dom/ScopeChecker.d.ts +0 -9
  122. package/dist/astro/cycle/dom/SymbolTree.d.ts +0 -9
  123. package/dist/astro/cycle/dom/VNodeWrapper.d.ts +0 -8
  124. package/dist/astro/cycle/dom/enrichEventStream.d.ts +0 -24
  125. package/dist/astro/cycle/dom/fromEvent.d.ts +0 -6
  126. package/dist/astro/cycle/dom/index.d.ts +0 -8
  127. package/dist/astro/cycle/dom/isolate.d.ts +0 -9
  128. package/dist/astro/cycle/dom/makeDOMDriver.d.ts +0 -11
  129. package/dist/astro/cycle/dom/mockDOMSource.d.ts +0 -17
  130. package/dist/astro/cycle/dom/modules.d.ts +0 -5
  131. package/dist/astro/cycle/dom/snabbdom.d.ts +0 -22
  132. package/dist/astro/cycle/dom/styleModule.d.ts +0 -13
  133. package/dist/astro/cycle/dom/thunk.d.ts +0 -11
  134. package/dist/astro/cycle/dom/utils.d.ts +0 -8
  135. package/dist/astro/cycle/isolate/index.d.ts +0 -42
  136. package/dist/astro/cycle/run/adapt.d.ts +0 -6
  137. package/dist/astro/cycle/run/index.d.ts +0 -36
  138. package/dist/astro/cycle/run/internals.d.ts +0 -8
  139. package/dist/astro/cycle/run/types.d.ts +0 -49
  140. package/dist/astro/cycle/state/Collection.d.ts +0 -29
  141. package/dist/astro/cycle/state/StateSource.d.ts +0 -20
  142. package/dist/astro/cycle/state/index.d.ts +0 -5
  143. package/dist/astro/cycle/state/pickCombine.d.ts +0 -3
  144. package/dist/astro/cycle/state/pickMerge.d.ts +0 -3
  145. package/dist/astro/cycle/state/types.d.ts +0 -18
  146. package/dist/astro/cycle/state/withState.d.ts +0 -17
  147. package/dist/astro/extra/classes.d.ts +0 -7
  148. package/dist/astro/extra/devtools.d.ts +0 -78
  149. package/dist/astro/extra/dragDriver.d.ts +0 -20
  150. package/dist/astro/extra/driverFactories.d.ts +0 -12
  151. package/dist/astro/extra/eventDriver.d.ts +0 -9
  152. package/dist/astro/extra/exactState.d.ts +0 -1
  153. package/dist/astro/extra/hmr.d.ts +0 -17
  154. package/dist/astro/extra/logDriver.d.ts +0 -2
  155. package/dist/astro/extra/processDrag.d.ts +0 -19
  156. package/dist/astro/extra/processForm.d.ts +0 -15
  157. package/dist/astro/extra/run.d.ts +0 -13
  158. package/dist/astro/extra/xstreamCompat.d.ts +0 -5
  159. package/dist/astro/index.d.ts +0 -19
  160. package/dist/astro/index.esm.js +0 -27
  161. package/dist/astro/jsx-dev-runtime.d.ts +0 -1
  162. package/dist/astro/jsx-runtime.d.ts +0 -4
  163. package/dist/astro/jsx.d.ts +0 -2
  164. package/dist/astro/pragma/fn.d.ts +0 -7
  165. package/dist/astro/pragma/index.d.ts +0 -7
  166. package/dist/astro/pragma/is.d.ts +0 -10
  167. package/dist/astro/server.d.ts +0 -12
  168. package/dist/astro/server.esm.js +0 -25
  169. package/dist/astro/switchable.d.ts +0 -7
  170. package/dist/collection.d.ts +0 -12
  171. package/dist/component.d.ts +0 -22
  172. package/dist/cycle/dom/BodyDOMSource.d.ts +0 -10
  173. package/dist/cycle/dom/DOMSource.d.ts +0 -11
  174. package/dist/cycle/dom/DocumentDOMSource.d.ts +0 -12
  175. package/dist/cycle/dom/ElementFinder.d.ts +0 -8
  176. package/dist/cycle/dom/EventDelegator.d.ts +0 -34
  177. package/dist/cycle/dom/IsolateModule.d.ts +0 -23
  178. package/dist/cycle/dom/MainDOMSource.d.ts +0 -32
  179. package/dist/cycle/dom/PriorityQueue.d.ts +0 -7
  180. package/dist/cycle/dom/ScopeChecker.d.ts +0 -9
  181. package/dist/cycle/dom/SymbolTree.d.ts +0 -9
  182. package/dist/cycle/dom/VNodeWrapper.d.ts +0 -8
  183. package/dist/cycle/dom/enrichEventStream.d.ts +0 -24
  184. package/dist/cycle/dom/fromEvent.d.ts +0 -6
  185. package/dist/cycle/dom/index.d.ts +0 -8
  186. package/dist/cycle/dom/isolate.d.ts +0 -9
  187. package/dist/cycle/dom/makeDOMDriver.d.ts +0 -11
  188. package/dist/cycle/dom/mockDOMSource.d.ts +0 -17
  189. package/dist/cycle/dom/modules.d.ts +0 -5
  190. package/dist/cycle/dom/snabbdom.d.ts +0 -22
  191. package/dist/cycle/dom/styleModule.d.ts +0 -13
  192. package/dist/cycle/dom/thunk.d.ts +0 -11
  193. package/dist/cycle/dom/utils.d.ts +0 -8
  194. package/dist/cycle/isolate/index.d.ts +0 -42
  195. package/dist/cycle/run/adapt.d.ts +0 -6
  196. package/dist/cycle/run/index.d.ts +0 -36
  197. package/dist/cycle/run/internals.d.ts +0 -8
  198. package/dist/cycle/run/types.d.ts +0 -49
  199. package/dist/cycle/state/Collection.d.ts +0 -29
  200. package/dist/cycle/state/StateSource.d.ts +0 -20
  201. package/dist/cycle/state/index.d.ts +0 -5
  202. package/dist/cycle/state/pickCombine.d.ts +0 -3
  203. package/dist/cycle/state/pickMerge.d.ts +0 -3
  204. package/dist/cycle/state/types.d.ts +0 -18
  205. package/dist/cycle/state/withState.d.ts +0 -17
  206. package/dist/extra/classes.d.ts +0 -7
  207. package/dist/extra/devtools.d.ts +0 -78
  208. package/dist/extra/dragDriver.d.ts +0 -20
  209. package/dist/extra/driverFactories.d.ts +0 -12
  210. package/dist/extra/eventDriver.d.ts +0 -9
  211. package/dist/extra/exactState.d.ts +0 -1
  212. package/dist/extra/hmr.d.ts +0 -17
  213. package/dist/extra/logDriver.d.ts +0 -2
  214. package/dist/extra/processDrag.d.ts +0 -19
  215. package/dist/extra/processForm.d.ts +0 -15
  216. package/dist/extra/run.d.ts +0 -13
  217. package/dist/extra/xstreamCompat.d.ts +0 -5
  218. package/dist/jsx-dev-runtime.d.ts +0 -1
  219. package/dist/jsx-dev-runtime.js +0 -224
  220. package/dist/jsx-runtime.d.ts +0 -4
  221. package/dist/jsx-runtime.js +0 -224
  222. package/dist/jsx.d.ts +0 -2
  223. package/dist/pragma/fn.d.ts +0 -7
  224. package/dist/pragma/index.d.ts +0 -7
  225. package/dist/pragma/is.d.ts +0 -10
  226. package/dist/switchable.d.ts +0 -7
@@ -0,0 +1,328 @@
1
+ /**
2
+ * Sygnal diagnostics core.
3
+ *
4
+ * ===========================================================================
5
+ * FROZEN INTERFACE (PLAN-1 workstream 0B). Changes after the 0B merge go
6
+ * through the coordinator. Later workstreams add checks (registerCheck) and
7
+ * codes (./codes.ts, inside their reserved range) — they do not change these
8
+ * signatures.
9
+ * ===========================================================================
10
+ *
11
+ * --- Reporting ------------------------------------------------------------
12
+ *
13
+ * report(code: string, details: {
14
+ * component?: string | { name?: string } // component name or instance
15
+ * message: string // what is wrong
16
+ * fix?: string // how to fix it
17
+ * data?: any // structured payload
18
+ * severity?: 'error' | 'warn' | 'info' // override the code's default
19
+ * }): Diagnostic | undefined
20
+ *
21
+ * - mode 'off' → no-op, returns undefined (nothing collected/printed)
22
+ * - code in ignore list → no-op, returns undefined
23
+ * - mode 'collect' → collected, no console output
24
+ * - mode 'warn' → collected; 'warn' → console.warn, 'error' → console.error,
25
+ * 'info' → collected only
26
+ * - mode 'error' → collected; 'warn'/'error' severities throw a
27
+ * DiagnosticError (err.diagnostic holds the Diagnostic);
28
+ * 'info' collected only. Called directly this throws
29
+ * synchronously; raised during a hook (i.e. from a
30
+ * check) it is rethrown asynchronously — see Hooks.
31
+ *
32
+ * `diagnostic.text` is formatted lazily (getter) on first read.
33
+ *
34
+ * formatDiagnostic(code, details): string
35
+ * → `[Sygnal SYG123] <Component>: <message>. <fix> <docsUrl>`
36
+ * (works in every mode; for call sites that must keep printing in 'off')
37
+ *
38
+ * getDiagnostics(): Diagnostic[] // copy of the collected list
39
+ * clearDiagnostics(): void
40
+ * onDiagnostic(cb: (d: Diagnostic) => void): () => void // returns unsubscribe
41
+ *
42
+ * --- Configuration --------------------------------------------------------
43
+ *
44
+ * configureDiagnostics({ mode?: DiagnosticsMode | undefined, ignore?: string[] }): void
45
+ * getDiagnosticsMode(): 'off' | 'collect' | 'warn' | 'error'
46
+ * isDiagnosticsEnabled(): boolean
47
+ *
48
+ * Mode resolution — first defined wins:
49
+ * 1. explicit mode: run(App, drivers, { diagnostics }) or configureDiagnostics({ mode })
50
+ * 2. globalThis.__SYGNAL_DEV__ (=== true → 'warn'; injected by the Vite plugin in serve)
51
+ * 3. 'off'
52
+ * The mode is resolved at module load, on every run() call and on every
53
+ * configureDiagnostics() call. `configureDiagnostics({ mode: undefined })`
54
+ * clears the explicit mode and falls back to steps 2-3.
55
+ * Every run() call is authoritative: it sets both the explicit mode and the
56
+ * ignore list from its `diagnostics` option, and run() WITHOUT the option
57
+ * resets them to the defaults (no explicit mode, empty ignore list), so a
58
+ * setting from an earlier run()/configureDiagnostics() does not leak in.
59
+ *
60
+ * --- Checks ---------------------------------------------------------------
61
+ *
62
+ * registerCheck(check: DiagnosticCheck): () => void // returns unregister
63
+ *
64
+ * interface DiagnosticCheck {
65
+ * id: string
66
+ * onIntent?(component, actionNames: string[], selectorsUsed: string[] | undefined): void
67
+ * onModel?(component, modelMap: Record<string, string[]>): void
68
+ * onRender?(component, rootVnode: any): void
69
+ * onReducer?(component, action: string, prevState: any, nextState: any, sinkName: string): void
70
+ * onDispose?(component): void
71
+ * }
72
+ *
73
+ * A check that throws (other than a DiagnosticError from 'error' mode) is
74
+ * isolated: the exception is reported as SYG900 and other checks still run.
75
+ *
76
+ * --- Hooks (called from src/component.ts, marked `// [diagnostics hook]`) ----
77
+ *
78
+ * onIntent(component, actionNames, selectorsUsed?)
79
+ * after intent is built. actionNames = keys of the intent object ([] for a
80
+ * single-stream intent). selectorsUsed is currently always undefined —
81
+ * TODO(1A): collect via MainDOMSource.select instrumentation in src/cycle/dom.
82
+ * onModel(component, modelMap)
83
+ * after the model is normalized; modelMap = action → sink names, shorthand
84
+ * ('A | SINK') expanded, plain functions mapped to the state sink,
85
+ * includes built-ins such as INITIALIZE.
86
+ * onRender(component, rootVnode)
87
+ * after each render of the component (fully injected vnode, before the DOM
88
+ * driver patches it; `rootVnode.elm` is populated after the patch, so DOM
89
+ * checks should defer, e.g. with a microtask/timeout).
90
+ * onReducer(component, action, prevState, nextState, sinkName)
91
+ * after each STATE reducer returns (not for ABORT, not when the reducer
92
+ * throws). nextState is the raw reducer return value (before calculated
93
+ * field cleanup). Currently only fired for the state sink; sinkName is the
94
+ * component's state source name.
95
+ * onDispose(component)
96
+ * at the start of component disposal.
97
+ *
98
+ * onSelector(domSource, selector) (additive, 1A)
99
+ * from MainDOMSource.select() in src/cycle/dom, for every CSS selector
100
+ * (not 'document' / 'body' / ':root'). domSource is the source select()
101
+ * was called on (its `.namespace` holds the isolation scopes and any
102
+ * earlier selectors; `._isolateModule` the DOM driver's isolate module).
103
+ * onBusEmit(type, emitterName?) (additive, 1A)
104
+ * from the EVENTS driver (src/extra/eventDriver.ts) for every bus event.
105
+ * onBusSelect(type) (additive, 1A)
106
+ * from EVENTS.select(type); type is the raw argument (string, string[]
107
+ * or undefined for "all events").
108
+ *
109
+ * Checks live in the separate 'sygnal/diagnostics' entry
110
+ * (src/extra/diagnostics/checks). That bundle reaches THIS module instance
111
+ * through globalThis.__SYGNAL_DIAGNOSTICS__ = { registerCheck, report }
112
+ * (set below), so it never carries a second copy of the core state.
113
+ *
114
+ * onIntent and onModel are each called exactly once per component instance,
115
+ * in that order, during construction (also when the component has no
116
+ * intent → actionNames [], or no model → modelMap {}).
117
+ *
118
+ * `component` is the internal Component instance (stable identity per
119
+ * instance; `.name` is the component name).
120
+ *
121
+ * Every hook starts with a single boolean check and returns immediately
122
+ * when the mode is 'off'.
123
+ *
124
+ * Hooks NEVER throw synchronously into Sygnal's stream pipeline (a throw
125
+ * there would kill the state/view stream for good). Every exception from a
126
+ * check is caught. In 'error' mode a DiagnosticError raised during a hook —
127
+ * by a check's report() call, or by the SYG900 report for a check that
128
+ * threw a plain Error — is rethrown ASYNCHRONOUSLY (queueMicrotask, falling
129
+ * back to setTimeout), so test runners still see an uncaught error while
130
+ * the app's streams keep running. The remaining checks still run.
131
+ * `_setAsyncThrow(fn?)` replaces the async rethrow (test seam; no argument
132
+ * restores the default; `_resetDiagnostics()` also restores it).
133
+ *
134
+ * (This comment is attached to the type-only import below so the TypeScript
135
+ * emit drops it — keeps it out of the published bundle and the size gate.)
136
+ */
137
+ import type { DiagnosticSeverity } from './codes'
138
+ import { CODE_SEVERITY, docsUrlFor } from './codes'
139
+
140
+
141
+ export type DiagnosticsMode = 'off' | 'collect' | 'warn' | 'error'
142
+
143
+ export interface DiagnosticDetails {
144
+ component?: string | { name?: string }
145
+ message: string
146
+ fix?: string
147
+ data?: any
148
+ severity?: DiagnosticSeverity
149
+ }
150
+
151
+ export interface Diagnostic {
152
+ code: string
153
+ severity: DiagnosticSeverity
154
+ component?: string
155
+ message: string
156
+ fix?: string
157
+ data?: any
158
+ docsUrl: string
159
+ text: string
160
+ timestamp: number
161
+ }
162
+
163
+ export interface DiagnosticsOptions {
164
+ mode?: DiagnosticsMode
165
+ ignore?: string[]
166
+ }
167
+
168
+ export interface DiagnosticCheck {
169
+ id: string
170
+ onIntent?: (component: any, actionNames: string[], selectorsUsed: string[] | undefined) => void
171
+ onModel?: (component: any, modelMap: Record<string, string[]>) => void
172
+ onRender?: (component: any, rootVnode: any) => void
173
+ onReducer?: (component: any, action: string, prevState: any, nextState: any, sinkName: string) => void
174
+ onDispose?: (component: any) => void
175
+ onSelector?: (domSource: any, selector: string) => void
176
+ onBusEmit?: (type: string, emitterName?: string) => void
177
+ onBusSelect?: (type: string | string[] | undefined) => void
178
+ }
179
+
180
+ export class DiagnosticError extends Error {
181
+ diagnostic: Diagnostic
182
+ constructor(diagnostic: Diagnostic) {
183
+ super(diagnostic.text)
184
+ this.diagnostic = diagnostic
185
+ }
186
+ }
187
+
188
+ let explicitMode: DiagnosticsMode | undefined
189
+ let mode: DiagnosticsMode = 'off'
190
+ let enabled = false
191
+ let ignored = new Set<string>()
192
+ let collected: Diagnostic[] = []
193
+ let listeners: Array<(d: Diagnostic) => void> = []
194
+ let checks: DiagnosticCheck[] = []
195
+
196
+ export function resolveDiagnosticsMode(): DiagnosticsMode {
197
+ mode = explicitMode || ((globalThis as any).__SYGNAL_DEV__ === true ? 'warn' : 'off')
198
+ enabled = mode !== 'off'
199
+ return mode
200
+ }
201
+
202
+ export function configureDiagnostics(options: DiagnosticsOptions = {}): void {
203
+ if ('mode' in options) explicitMode = options.mode
204
+ if (options.ignore) ignored = new Set(options.ignore)
205
+ resolveDiagnosticsMode()
206
+ }
207
+
208
+ /** Internal: the explicit configuration (not the resolved mode), so it can be restored exactly. */
209
+ export function _getDiagnosticsConfig(): { mode: DiagnosticsMode | undefined, ignore: string[] } {
210
+ return { mode: explicitMode, ignore: [...ignored] }
211
+ }
212
+
213
+ export function getDiagnosticsMode(): DiagnosticsMode {
214
+ return mode
215
+ }
216
+
217
+ export function isDiagnosticsEnabled(): boolean {
218
+ return enabled
219
+ }
220
+
221
+ const nameOf = (c: DiagnosticDetails['component']) => (c && typeof c === 'object' ? c.name : c) || undefined
222
+ const sentence = (s: string) => /[.!?]$/.test(s) ? s : s + '.'
223
+
224
+ export function formatDiagnostic(code: string, details: DiagnosticDetails): string {
225
+ const name = nameOf(details.component)
226
+ return `[Sygnal ${code}] ${name ? name + ': ' : ''}${sentence(details.message)}` +
227
+ (details.fix ? ' ' + sentence(details.fix) : '') + ' ' + docsUrlFor(code)
228
+ }
229
+
230
+ export function report(code: string, details: DiagnosticDetails): Diagnostic | undefined {
231
+ if (!enabled || ignored.has(code)) return
232
+ const severity = details.severity || CODE_SEVERITY[code] || 'warn'
233
+ const d: Diagnostic = {
234
+ code,
235
+ severity,
236
+ component: nameOf(details.component),
237
+ message: details.message,
238
+ fix: details.fix,
239
+ data: details.data,
240
+ docsUrl: docsUrlFor(code),
241
+ get text() { return formatDiagnostic(code, this) },
242
+ timestamp: Date.now(),
243
+ }
244
+ if (collected.push(d) > 500) collected.shift()
245
+ listeners.forEach(cb => { try { cb(d) } catch (_) {} })
246
+ if (severity !== 'info') {
247
+ if (mode === 'error') throw new DiagnosticError(d)
248
+ if (mode === 'warn') console[severity](d.text)
249
+ }
250
+ return d
251
+ }
252
+
253
+ export function getDiagnostics(): Diagnostic[] {
254
+ return collected.slice()
255
+ }
256
+
257
+ export function clearDiagnostics(): void {
258
+ collected = []
259
+ }
260
+
261
+ export function onDiagnostic(cb: (d: Diagnostic) => void): () => void {
262
+ listeners = [...listeners, cb]
263
+ return () => { listeners = listeners.filter(l => l !== cb) }
264
+ }
265
+
266
+ export function registerCheck(check: DiagnosticCheck): () => void {
267
+ checks = [...checks, check]
268
+ return () => { checks = checks.filter(c => c !== check) }
269
+ }
270
+
271
+ type HookName = Exclude<keyof DiagnosticCheck, 'id'>
272
+
273
+ const defaultAsyncThrow = (err: any): void => {
274
+ const raise = () => { throw err }
275
+ typeof queueMicrotask === 'function' ? queueMicrotask(raise) : setTimeout(raise)
276
+ }
277
+ let asyncThrow = defaultAsyncThrow
278
+
279
+ /** Test seam: replace the async rethrow used for 'error'-mode diagnostics raised in hooks. */
280
+ export function _setAsyncThrow(fn?: (err: any) => void): void {
281
+ asyncThrow = fn || defaultAsyncThrow
282
+ }
283
+
284
+ const hook = (name: HookName) => (component: any, ...args: any[]): void => {
285
+ if (!enabled) return
286
+ for (const check of checks) {
287
+ try {
288
+ (check[name] as any)?.(component, ...args)
289
+ } catch (err: any) {
290
+ // Never throw into the stream pipeline: 'error'-mode diagnostics (also a
291
+ // SYG900 escalated by 'error' mode) are rethrown asynchronously.
292
+ try {
293
+ if (err instanceof DiagnosticError) throw err
294
+ report('SYG900', {
295
+ component,
296
+ message: `Diagnostics check '${check.id}' threw in ${name}: ${err?.message}`,
297
+ data: { check: check.id, hook: name, error: err },
298
+ })
299
+ } catch (e) {
300
+ asyncThrow(e)
301
+ }
302
+ }
303
+ }
304
+ }
305
+
306
+ export const onIntent: (component: any, actionNames: string[], selectorsUsed?: string[]) => void = hook('onIntent')
307
+ export const onModel: (component: any, modelMap: Record<string, string[]>) => void = hook('onModel')
308
+ export const onRender: (component: any, rootVnode: any) => void = hook('onRender')
309
+ export const onReducer: (component: any, action: string, prevState: any, nextState: any, sinkName: string) => void = hook('onReducer')
310
+ export const onDispose: (component: any) => void = hook('onDispose')
311
+ export const onSelector: (domSource: any, selector: string) => void = hook('onSelector')
312
+ export const onBusEmit: (type: string, emitterName?: string) => void = hook('onBusEmit')
313
+ export const onBusSelect: (type: string | string[] | undefined) => void = hook('onBusSelect')
314
+
315
+ // Bridge for the separately bundled 'sygnal/diagnostics' checks entry.
316
+ ;(globalThis as any).__SYGNAL_DIAGNOSTICS__ = { registerCheck, report }
317
+
318
+ export function _resetDiagnostics(): void {
319
+ explicitMode = undefined
320
+ ignored = new Set()
321
+ collected = []
322
+ listeners = []
323
+ checks = []
324
+ asyncThrow = defaultAsyncThrow
325
+ resolveDiagnosticsMode()
326
+ }
327
+
328
+ resolveDiagnosticsMode()
@@ -0,0 +1,59 @@
1
+ /**
2
+ * Coded console output for Sygnal's pre-existing warnings and errors
3
+ * (PLAN-1 workstream 1E).
4
+ *
5
+ * warn(code, component, message, fix?, extra?)
6
+ * error(code, component, message, fix?, extra?)
7
+ * fail(code, component, message, fix?): never
8
+ *
9
+ * `component` is a component instance (anything with `.name`), a name string,
10
+ * or undefined. `extra` (an Error, the offending value, ...) is passed to the
11
+ * console after the formatted text, as the old messages did.
12
+ *
13
+ * Unlike report(), warn()/error() keep printing when diagnostics are 'off',
14
+ * so production apps see the same messages as before (now with a code):
15
+ * - 'off' → console.warn/error(formatDiagnostic(...), extra?)
16
+ * - 'collect' → report(): collected; error severity is also printed with
17
+ * console.error (so exceptions stay visible in tests), warn isn't
18
+ * - 'warn' → report(): collected and printed once by report(); `extra`
19
+ * (e.g. the caught Error with its stack) printed after it
20
+ * - 'error' → report() throws a DiagnosticError; it is rethrown
21
+ * asynchronously so the stream pipeline that hit the problem
22
+ * keeps running (same contract as the hooks in ./index)
23
+ * - code in the ignore list → nothing printed
24
+ *
25
+ * fail() always throws an Error whose message is the formatted text and whose
26
+ * `.code` is the diagnostic code. It does not go through report(): the throw
27
+ * itself is the signal, in every mode.
28
+ *
29
+ * (This comment is attached to the type-only import below so the TypeScript
30
+ * emit drops it.)
31
+ */
32
+ import type { DiagnosticSeverity } from './codes'
33
+ import { report, formatDiagnostic, getDiagnosticsMode } from './index'
34
+
35
+ const emit = (severity: Extract<DiagnosticSeverity, 'warn' | 'error'>) =>
36
+ (code: string, component: any, message: string, fix?: string, ...extra: any[]): void => {
37
+ const details = { component, message, fix, severity, data: extra[0] }
38
+ const mode = getDiagnosticsMode()
39
+ if (mode === 'off') {
40
+ console[severity](formatDiagnostic(code, details), ...extra)
41
+ return
42
+ }
43
+ try {
44
+ const d = report(code, details)
45
+ if (d && mode === 'collect' && severity === 'error') console.error(d.text, ...extra)
46
+ else if (d && mode === 'warn' && extra.length) console[severity](...extra)
47
+ } catch (err) {
48
+ setTimeout(() => { throw err })
49
+ }
50
+ }
51
+
52
+ export const warn = emit('warn')
53
+ export const error = emit('error')
54
+
55
+ export function fail(code: string, component: any, message: string, fix?: string): never {
56
+ const err: any = new Error(formatDiagnostic(code, { component, message, fix }))
57
+ err.code = code
58
+ throw err
59
+ }
@@ -8,10 +8,36 @@ interface DriverFromAsyncOptions {
8
8
  post?: (val: any, incoming?: any) => any;
9
9
  }
10
10
 
11
+ /**
12
+ * Create a driver from a promise-returning function.
13
+ *
14
+ * Each value the app sends to the driver calls the function (arguments picked
15
+ * by `args`, after `pre`). The resolved value, after `post`, is delivered as
16
+ * `{ [return]: value, [selector]: request[selector] }` (defaults:
17
+ * `{ value, category }`) and read with `source.select(category)`. A promise
18
+ * that resolves to `null`/`undefined` is delivered the same way.
19
+ *
20
+ * Errors: if the function's promise (or a promise returned by `post`)
21
+ * rejects, or `post` throws, the error is delivered on `source.errors()`,
22
+ * never on `select()`:
23
+ *
24
+ * ```js
25
+ * // payload: { error, category: request.category, request }
26
+ * intent: ({ QUOTE }) => ({
27
+ * GOT_QUOTE: QUOTE.select('quote'),
28
+ * QUOTE_ERROR: QUOTE.errors('quote'), // or .errors() / .errors(e => ...)
29
+ * })
30
+ * ```
31
+ *
32
+ * `errors(selector?)` filters like `select()`: no argument = all errors, a
33
+ * string matches the request's selector property, a function is a predicate on
34
+ * the error payload. While nothing listens to `errors()`, failures are only
35
+ * logged with console.error (the pre-existing behavior).
36
+ */
11
37
  function driverFromAsync(
12
38
  promiseReturningFunction: (...args: any[]) => Promise<any>,
13
39
  opts: DriverFromAsyncOptions = {}
14
- ): (fromApp$: Stream<any>) => {select: (selector?: any) => Stream<any>} {
40
+ ): (fromApp$: Stream<any>) => {select: (selector?: any) => Stream<any>; errors: (selector?: any) => Stream<any>} {
15
41
  const {
16
42
  selector: selectorProperty = 'category',
17
43
  args: functionArgs = 'value',
@@ -48,6 +74,19 @@ function driverFromAsync(
48
74
  stop: () => {},
49
75
  });
50
76
 
77
+ // Active errors(selector) streams (1H-6): an error goes to the ones it matches, and is
78
+ // logged when none matches.
79
+ const errorSubs = new Set<{listener: any; selector: any}>();
80
+ const matches = (selector: any, val: any) =>
81
+ selector === undefined ||
82
+ (typeof selector === 'function' ? selector(val) : val?.[selectorProperty] === selector);
83
+ const filterBy = (stream: Stream<any>, selector?: any) =>
84
+ selector === undefined ? stream : stream.filter((val: any) => matches(selector, val));
85
+
86
+ // 3E/R11: set by the source's dispose(), which Cycle's engine calls on teardown just
87
+ // before it completes the sink proxies; that completion is expected, not worth a warning.
88
+ let disposing = false;
89
+
51
90
  fromApp$.addListener({
52
91
  next: (incoming: any) => {
53
92
  const preProcessed = preFunction(incoming);
@@ -65,61 +104,59 @@ function driverFromAsync(
65
104
  }
66
105
  }
67
106
  const errMsg = `Error in driver created using driverFromAsync(${functionName})`;
68
- promiseReturningFunction(...argArr)
69
- .then((innerVal: any) => {
70
- const constructReply = (rawVal: any) => {
71
- let outgoing: any;
72
- if (returnProperty === undefined) {
73
- outgoing = rawVal;
74
- if (typeof outgoing === 'object' && outgoing !== null) {
75
- outgoing[selectorProperty] = incoming[selectorProperty];
76
- } else {
77
- console.warn(
78
- `The 'return' option for driverFromAsync(${functionName}) was not set, but the promise returned an non-object. The result will be returned as-is, but the '${selectorProperty}' property will not be set, so will not be filtered by the 'select' method of the driver.`
79
- );
80
- }
81
- } else if (typeof returnProperty === 'string') {
82
- outgoing = {
83
- [returnProperty]: rawVal,
84
- [selectorProperty]: incoming[selectorProperty],
85
- };
86
- } else {
87
- throw new Error(
88
- `The 'return' option for driverFromAsync(${functionName}) must be a string. Received ${typeof returnProperty}`
89
- );
90
- }
91
- return outgoing;
92
- };
93
-
94
- if (typeof innerVal.then === 'function') {
95
- innerVal
96
- .then((innerOutgoing: any) => {
97
- const processedOutgoing = postFunction(innerOutgoing, incoming);
98
- if (typeof processedOutgoing.then === 'function') {
99
- processedOutgoing
100
- .then((innerProcessedOutgoing: any) => {
101
- sendFn!(constructReply(innerProcessedOutgoing));
102
- })
103
- .catch((err: any) => console.error(`${errMsg}: ${err}`));
104
- } else {
105
- sendFn!(constructReply(processedOutgoing));
106
- }
107
- })
108
- .catch((err: any) => console.error(`${errMsg}: ${err}`));
107
+ const constructReply = (rawVal: any) => {
108
+ let outgoing: any;
109
+ if (returnProperty === undefined) {
110
+ outgoing = rawVal;
111
+ if (typeof outgoing === 'object' && outgoing !== null) {
112
+ outgoing[selectorProperty] = incoming[selectorProperty];
109
113
  } else {
110
- const processedOutgoing = postFunction(innerVal, incoming);
111
- if (typeof processedOutgoing.then === 'function') {
112
- processedOutgoing
113
- .then((innerProcessedOutgoing: any) => {
114
- sendFn!(constructReply(innerProcessedOutgoing));
115
- })
116
- .catch((err: any) => console.error(`${errMsg}: ${err}`));
117
- } else {
118
- sendFn!(constructReply(processedOutgoing));
119
- }
114
+ console.warn(
115
+ `The 'return' option for driverFromAsync(${functionName}) was not set, but the promise returned an non-object. The result will be returned as-is, but the '${selectorProperty}' property will not be set, so will not be filtered by the 'select' method of the driver.`
116
+ );
120
117
  }
121
- })
122
- .catch((err: any) => console.error(`${errMsg}: ${err}`));
118
+ } else if (typeof returnProperty === 'string') {
119
+ outgoing = {
120
+ [returnProperty]: rawVal,
121
+ [selectorProperty]: incoming[selectorProperty],
122
+ };
123
+ } else {
124
+ throw new Error(
125
+ `The 'return' option for driverFromAsync(${functionName}) must be a string. Received ${typeof returnProperty}`
126
+ );
127
+ }
128
+ return outgoing;
129
+ };
130
+ // Rejections (of the function, a thenable it resolves to, or post()) go to errors();
131
+ // an error no active errors() selector matches is logged, as before.
132
+ const reportError = (err: any) => {
133
+ const val = {error: err, [selectorProperty]: incoming?.[selectorProperty], request: incoming};
134
+ let handled = false;
135
+ errorSubs.forEach(sub => {
136
+ let hit = false;
137
+ try { hit = matches(sub.selector, val); } catch (_) {}
138
+ if (hit) {
139
+ handled = true;
140
+ sub.listener.next(val);
141
+ }
142
+ });
143
+ if (!handled) console.error(`${errMsg}: ${err}`);
144
+ };
145
+ const isThenable = (val: any) => val != null && typeof val.then === 'function';
146
+ promiseReturningFunction(...argArr)
147
+ .then((innerVal: any) =>
148
+ isThenable(innerVal)
149
+ ? innerVal.then((innerOutgoing: any) => postFunction(innerOutgoing, incoming))
150
+ : postFunction(innerVal, incoming)
151
+ )
152
+ .then(constructReply)
153
+ .then((reply: any) => {
154
+ try {
155
+ sendFn!(reply);
156
+ } catch (err) {
157
+ console.error(`${errMsg}: ${err}`);
158
+ }
159
+ }, reportError);
123
160
  },
124
161
  error: (err: any) => {
125
162
  console.error(
@@ -128,6 +165,7 @@ function driverFromAsync(
128
165
  );
129
166
  },
130
167
  complete: () => {
168
+ if (disposing) return;
131
169
  console.warn(
132
170
  `Unexpected completion of sink stream to driver created using driverFromAsync(${functionName})`
133
171
  );
@@ -135,10 +173,20 @@ function driverFromAsync(
135
173
  });
136
174
 
137
175
  return {
138
- select: (selector?: any) => {
139
- if (selector === undefined) return toApp$;
140
- if (typeof selector === 'function') return toApp$.filter(selector);
141
- return toApp$.filter((val: any) => val?.[selectorProperty] === selector);
176
+ dispose: () => {
177
+ disposing = true;
178
+ },
179
+ select: (selector?: any) => filterBy(toApp$, selector),
180
+ errors: (selector?: any) => {
181
+ let sub: any;
182
+ return xs.create<any>({
183
+ start: (listener) => {
184
+ errorSubs.add((sub = {listener, selector}));
185
+ },
186
+ stop: () => {
187
+ errorSubs.delete(sub);
188
+ },
189
+ });
142
190
  },
143
191
  };
144
192
  };
@@ -1,5 +1,7 @@
1
1
  import xs, {Stream} from 'xstream';
2
2
  import {adapt} from '../cycle/run/adapt';
3
+ // [diagnostics hook] no-op when diagnostics are off
4
+ import {onBusEmit, onBusSelect} from './diagnostics/index';
3
5
 
4
6
  export interface EventBusSource {
5
7
  select(type?: string | string[]): any;
@@ -15,6 +17,7 @@ export default function eventBusDriver(out$: Stream<BusEvent>): EventBusSource {
15
17
 
16
18
  out$.subscribe({
17
19
  next: (event: BusEvent) => {
20
+ onBusEmit(event && event.type, (event as any)?.__emitterName);
18
21
  events.dispatchEvent(new CustomEvent('data', {detail: event}));
19
22
  if (typeof window !== 'undefined' && (window as any).__SYGNAL_DEVTOOLS__?.connected) {
20
23
  (window as any).__SYGNAL_DEVTOOLS__.onBusEvent(event);
@@ -26,6 +29,7 @@ export default function eventBusDriver(out$: Stream<BusEvent>): EventBusSource {
26
29
 
27
30
  return {
28
31
  select: (type?: string | string[]) => {
32
+ onBusSelect(type);
29
33
  const all = !type;
30
34
  const _type = Array.isArray(type) ? type : [type];
31
35
  let cb: ((e: Event) => void) | undefined;