sygnal 5.4.0 → 6.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.
Files changed (360) hide show
  1. package/CHANGELOG.md +716 -0
  2. package/README.md +24 -9
  3. package/dist/ai.cjs.js +115 -0
  4. package/dist/ai.cjs.js.map +1 -0
  5. package/dist/ai.esm.js +2 -0
  6. package/dist/ai.esm.js.map +1 -0
  7. package/dist/astro/client.cjs.js +16 -9
  8. package/dist/astro/client.cjs.js.map +1 -1
  9. package/dist/astro/client.mjs +16 -9
  10. package/dist/astro/client.mjs.map +1 -1
  11. package/dist/astro/index.cjs.js +958 -92
  12. package/dist/astro/index.cjs.js.map +1 -1
  13. package/dist/astro/index.mjs +958 -93
  14. package/dist/astro/index.mjs.map +1 -1
  15. package/dist/astro/server.cjs.js +3396 -165
  16. package/dist/astro/server.cjs.js.map +1 -1
  17. package/dist/astro/server.mjs +3396 -165
  18. package/dist/astro/server.mjs.map +1 -1
  19. package/dist/devtools.cjs.js +1840 -0
  20. package/dist/devtools.cjs.js.map +1 -0
  21. package/dist/devtools.esm.js +1827 -0
  22. package/dist/devtools.esm.js.map +1 -0
  23. package/dist/diagnostics.cjs.js +3190 -296
  24. package/dist/diagnostics.cjs.js.map +1 -1
  25. package/dist/diagnostics.esm.js +3190 -296
  26. package/dist/diagnostics.esm.js.map +1 -1
  27. package/dist/element.cjs.js +230 -0
  28. package/dist/element.cjs.js.map +1 -0
  29. package/dist/element.esm.js +228 -0
  30. package/dist/element.esm.js.map +1 -0
  31. package/dist/guide/accessibility.md +310 -0
  32. package/dist/guide/adapters.md +221 -0
  33. package/dist/guide/agent.md +588 -0
  34. package/dist/guide/ai-chat.md +693 -0
  35. package/dist/guide/ai-decisions.md +330 -0
  36. package/dist/guide/behaviors.md +312 -0
  37. package/dist/guide/browser-sources.md +203 -0
  38. package/dist/guide/drag-and-drop.md +342 -0
  39. package/dist/guide/element-commands.md +306 -0
  40. package/dist/guide/error-boundaries.md +169 -0
  41. package/dist/guide/forms-reference.md +409 -0
  42. package/dist/guide/forms.md +251 -0
  43. package/dist/guide/http.md +301 -0
  44. package/dist/guide/inputs.md +173 -0
  45. package/dist/guide/mcp-apps.md +272 -0
  46. package/dist/guide/persistence.md +245 -0
  47. package/dist/guide/recipes/ai-form-fill.md +163 -0
  48. package/dist/guide/recipes/ai-summarize.md +155 -0
  49. package/dist/guide/recipes/ai-support-inbox.md +190 -0
  50. package/dist/guide/recipes/carousel.md +155 -0
  51. package/dist/guide/recipes/charts.md +177 -0
  52. package/dist/guide/recipes/code-editor.md +135 -0
  53. package/dist/guide/recipes/data-grid.md +155 -0
  54. package/dist/guide/recipes/data-table.md +196 -0
  55. package/dist/guide/recipes/i18n.md +222 -0
  56. package/dist/guide/recipes/icons.md +115 -0
  57. package/dist/guide/recipes/overview.md +43 -0
  58. package/dist/guide/recipes/rich-text.md +154 -0
  59. package/dist/guide/resources.md +471 -0
  60. package/dist/guide/ssr.md +255 -0
  61. package/dist/guide/timers.md +178 -0
  62. package/dist/guide/ui/accordion.md +93 -0
  63. package/dist/guide/ui/combobox.md +144 -0
  64. package/dist/guide/ui/dialog.md +124 -0
  65. package/dist/guide/ui/disclosure.md +60 -0
  66. package/dist/guide/ui/menu.md +125 -0
  67. package/dist/guide/ui/overview.md +166 -0
  68. package/dist/guide/ui/popover.md +101 -0
  69. package/dist/guide/ui/select.md +114 -0
  70. package/dist/guide/ui/tabs.md +161 -0
  71. package/dist/guide/ui/toaster.md +152 -0
  72. package/dist/guide/ui/tooltip.md +103 -0
  73. package/dist/guide/undo.md +178 -0
  74. package/dist/guide/virtual-collections.md +183 -0
  75. package/dist/guide/web-components.md +179 -0
  76. package/dist/guide/webmcp.md +226 -0
  77. package/dist/guide/widgets.md +217 -0
  78. package/dist/index.cjs.js +19288 -7408
  79. package/dist/index.cjs.js.map +1 -1
  80. package/dist/index.d.ts +4466 -552
  81. package/dist/index.esm.js +19274 -7461
  82. package/dist/index.esm.js.map +1 -1
  83. package/dist/jsx-dev-runtime.cjs.js +26 -334
  84. package/dist/jsx-dev-runtime.cjs.js.map +1 -1
  85. package/dist/jsx-dev-runtime.esm.js +19 -327
  86. package/dist/jsx-dev-runtime.esm.js.map +1 -1
  87. package/dist/jsx-runtime.cjs.js +26 -334
  88. package/dist/jsx-runtime.cjs.js.map +1 -1
  89. package/dist/jsx-runtime.esm.js +19 -327
  90. package/dist/jsx-runtime.esm.js.map +1 -1
  91. package/dist/jsx.cjs.js +10 -314
  92. package/dist/jsx.cjs.js.map +1 -1
  93. package/dist/jsx.esm.js +8 -315
  94. package/dist/jsx.esm.js.map +1 -1
  95. package/dist/react.cjs.js +117 -0
  96. package/dist/react.cjs.js.map +1 -0
  97. package/dist/react.esm.js +115 -0
  98. package/dist/react.esm.js.map +1 -0
  99. package/dist/shims/globalthis.cjs +20 -0
  100. package/dist/sygnal.min.js +1 -1
  101. package/dist/sygnal.min.js.map +1 -1
  102. package/dist/ui-combobox.cjs.js +169 -0
  103. package/dist/ui-combobox.cjs.js.map +1 -0
  104. package/dist/ui-combobox.esm.js +148 -0
  105. package/dist/ui-combobox.esm.js.map +1 -0
  106. package/dist/ui-menu.cjs.js +87 -0
  107. package/dist/ui-menu.cjs.js.map +1 -0
  108. package/dist/ui-menu.esm.js +66 -0
  109. package/dist/ui-menu.esm.js.map +1 -0
  110. package/dist/ui-select.cjs.js +104 -0
  111. package/dist/ui-select.cjs.js.map +1 -0
  112. package/dist/ui-select.esm.js +83 -0
  113. package/dist/ui-select.esm.js.map +1 -0
  114. package/dist/ui.cjs.js +738 -0
  115. package/dist/ui.cjs.js.map +1 -0
  116. package/dist/ui.esm.js +727 -0
  117. package/dist/ui.esm.js.map +1 -0
  118. package/dist/vike/ClientOnly.cjs.js +4 -14
  119. package/dist/vike/ClientOnly.cjs.js.map +1 -1
  120. package/dist/vike/ClientOnly.mjs +4 -14
  121. package/dist/vike/ClientOnly.mjs.map +1 -1
  122. package/dist/vike/{+config.js → config/+config.js} +9 -1
  123. package/dist/vike/config/+config.js.map +1 -0
  124. package/dist/vike/config/package.json +6 -0
  125. package/dist/vike/onRenderClient.cjs.js +165 -95
  126. package/dist/vike/onRenderClient.cjs.js.map +1 -1
  127. package/dist/vike/onRenderClient.mjs +165 -95
  128. package/dist/vike/onRenderClient.mjs.map +1 -1
  129. package/dist/vike/onRenderHtml.cjs.js +59 -24
  130. package/dist/vike/onRenderHtml.cjs.js.map +1 -1
  131. package/dist/vike/onRenderHtml.mjs +60 -25
  132. package/dist/vike/onRenderHtml.mjs.map +1 -1
  133. package/dist/vite/plugin.cjs.js +919 -89
  134. package/dist/vite/plugin.cjs.js.map +1 -1
  135. package/dist/vite/plugin.mjs +919 -90
  136. package/dist/vite/plugin.mjs.map +1 -1
  137. package/dist/zag.cjs.js +408 -0
  138. package/dist/zag.cjs.js.map +1 -0
  139. package/dist/zag.esm.js +405 -0
  140. package/dist/zag.esm.js.map +1 -0
  141. package/llms.txt +172 -103
  142. package/package.json +112 -14
  143. package/src/ai.d.ts +1186 -0
  144. package/src/ai.ts +23 -0
  145. package/src/astro/client.ts +27 -16
  146. package/src/astro/index.d.ts +24 -0
  147. package/src/astro/index.ts +69 -3
  148. package/src/astro/server.ts +8 -2
  149. package/src/collection.ts +6 -93
  150. package/src/core/actions.ts +134 -0
  151. package/src/core/cell.ts +202 -0
  152. package/src/core/debug.ts +23 -0
  153. package/src/core/define.ts +242 -0
  154. package/src/core/hooks.ts +314 -0
  155. package/src/core/hosts/collection.ts +325 -0
  156. package/src/core/hosts/switchable.ts +146 -0
  157. package/src/core/instance.ts +592 -0
  158. package/src/core/markers/clientonly.ts +13 -0
  159. package/src/core/markers/lazy.ts +40 -0
  160. package/src/core/markers/portal.ts +96 -0
  161. package/src/core/markers/suspense.ts +41 -0
  162. package/src/core/markers/transition.ts +70 -0
  163. package/src/core/registry.ts +25 -0
  164. package/src/core/runtime.ts +573 -0
  165. package/src/core/statics.ts +104 -0
  166. package/src/core/teardown.ts +60 -0
  167. package/src/core/view.ts +37 -0
  168. package/src/cycle/dom/DocumentDOMSource.ts +13 -8
  169. package/src/cycle/dom/ElementFinder.ts +7 -9
  170. package/src/cycle/dom/EventDelegator.ts +174 -223
  171. package/src/cycle/dom/IsolateModule.ts +65 -35
  172. package/src/cycle/dom/MainDOMSource.ts +9 -2
  173. package/src/cycle/dom/PriorityQueue.ts +4 -2
  174. package/src/cycle/dom/SymbolTree.ts +18 -47
  175. package/src/cycle/dom/classNameModule.ts +9 -7
  176. package/src/cycle/dom/controlledInputModule.ts +23 -6
  177. package/src/cycle/dom/enrichEventStream.ts +25 -47
  178. package/src/cycle/dom/fragment.ts +7 -0
  179. package/src/cycle/dom/isolate.ts +30 -27
  180. package/src/cycle/dom/makeDOMDriver.ts +115 -56
  181. package/src/cycle/dom/mockDOMSource.ts +21 -1
  182. package/src/cycle/dom/modules.ts +16 -3
  183. package/src/cycle/dom/propsModule.ts +65 -0
  184. package/src/cycle/dom/selectModule.ts +22 -17
  185. package/src/cycle/dom/snabbdom.ts +1 -6
  186. package/src/cycle/dom/thunk.ts +3 -0
  187. package/src/cycle/dom/utils.ts +98 -0
  188. package/src/cycle/dom/viewTransition.ts +43 -0
  189. package/src/cycle/state/StateSource.ts +19 -3
  190. package/src/cycle/state/objIsEqual.ts +50 -0
  191. package/src/cycle/state/types.ts +0 -10
  192. package/src/defineComponent.ts +34 -0
  193. package/src/devtools.d.ts +170 -0
  194. package/src/devtools.ts +37 -0
  195. package/src/element.d.ts +59 -0
  196. package/src/element.ts +235 -0
  197. package/src/extra/ai/agent/index.ts +484 -0
  198. package/src/extra/ai/answers.ts +115 -0
  199. package/src/extra/ai/chat/behavior.ts +336 -0
  200. package/src/extra/ai/chat/driver.ts +271 -0
  201. package/src/extra/ai/chat/memoryTransport.ts +61 -0
  202. package/src/extra/ai/chat/output.ts +37 -0
  203. package/src/extra/ai/commandBar.ts +281 -0
  204. package/src/extra/ai/decide.ts +104 -0
  205. package/src/extra/ai/index.ts +42 -0
  206. package/src/extra/ai/link.ts +90 -0
  207. package/src/extra/ai/mcpApp.ts +314 -0
  208. package/src/extra/ai/messages.ts +51 -0
  209. package/src/extra/ai/schema/index.ts +166 -0
  210. package/src/extra/ai/schema/jsonSchema.ts +72 -0
  211. package/src/extra/ai/schema/strict.ts +152 -0
  212. package/src/extra/ai/transports/agui.ts +177 -0
  213. package/src/extra/ai/transports/anthropicMessages.ts +175 -0
  214. package/src/extra/ai/transports/chatCompletions.ts +105 -0
  215. package/src/extra/ai/transports/chromePrompt.ts +77 -0
  216. package/src/extra/ai/transports/encodeOpenResponses.ts +87 -0
  217. package/src/extra/ai/transports/fromAISDK.ts +126 -0
  218. package/src/extra/ai/transports/openResponses.ts +105 -0
  219. package/src/extra/ai/transports/shared.ts +125 -0
  220. package/src/extra/ai/transports/tools.ts +59 -0
  221. package/src/extra/ai/transports/uiMessageStream.ts +107 -0
  222. package/src/extra/ai/webmcp.ts +260 -0
  223. package/src/extra/backoff.ts +9 -0
  224. package/src/extra/behaviors.ts +169 -0
  225. package/src/extra/browserSignals.ts +23 -0
  226. package/src/extra/browserSources.ts +326 -0
  227. package/src/extra/command.ts +3 -3
  228. package/src/extra/controls.ts +47 -0
  229. package/src/extra/copyAsTest.ts +286 -0
  230. package/src/extra/devMcp.ts +386 -0
  231. package/src/extra/devtools.ts +101 -31
  232. package/src/extra/devtoolsActions.ts +379 -0
  233. package/src/extra/devtoolsHook.ts +9 -0
  234. package/src/extra/devtoolsNext.ts +83 -0
  235. package/src/extra/diagnostics/checks/actionLog.ts +108 -0
  236. package/src/extra/diagnostics/checks/behaviors.ts +49 -0
  237. package/src/extra/diagnostics/checks/browserSources.ts +96 -0
  238. package/src/extra/diagnostics/checks/chat.ts +71 -0
  239. package/src/extra/diagnostics/checks/collections.ts +1 -1
  240. package/src/extra/diagnostics/checks/controls.ts +148 -0
  241. package/src/extra/diagnostics/checks/dataset.ts +57 -0
  242. package/src/extra/diagnostics/checks/dom.ts +20 -11
  243. package/src/extra/diagnostics/checks/elementCommands.ts +175 -0
  244. package/src/extra/diagnostics/checks/events.ts +17 -2
  245. package/src/extra/diagnostics/checks/fetch.ts +135 -0
  246. package/src/extra/diagnostics/checks/forms.ts +171 -0
  247. package/src/extra/diagnostics/checks/index.ts +103 -4
  248. package/src/extra/diagnostics/checks/inspect.ts +140 -32
  249. package/src/extra/diagnostics/checks/next.ts +417 -0
  250. package/src/extra/diagnostics/checks/persist.ts +54 -0
  251. package/src/extra/diagnostics/checks/props.ts +10 -5
  252. package/src/extra/diagnostics/checks/public.d.ts +115 -7
  253. package/src/extra/diagnostics/checks/replies.ts +128 -0
  254. package/src/extra/diagnostics/checks/router.ts +100 -0
  255. package/src/extra/diagnostics/checks/rxjsHints.ts +1 -1
  256. package/src/extra/diagnostics/checks/shared.ts +86 -2
  257. package/src/extra/diagnostics/checks/shorthand.ts +115 -0
  258. package/src/extra/diagnostics/checks/sortable.ts +129 -0
  259. package/src/extra/diagnostics/checks/state.ts +100 -7
  260. package/src/extra/diagnostics/checks/statics.ts +48 -0
  261. package/src/extra/diagnostics/checks/strict.ts +10 -67
  262. package/src/extra/diagnostics/checks/timers.ts +80 -0
  263. package/src/extra/diagnostics/checks/viewTransitions.ts +47 -0
  264. package/src/extra/diagnostics/checks/virtual.ts +73 -0
  265. package/src/extra/diagnostics/checks/widgets.ts +109 -0
  266. package/src/extra/diagnostics/checks/wiring.ts +69 -10
  267. package/src/extra/diagnostics/codes.ts +335 -23
  268. package/src/extra/diagnostics/index.ts +34 -9
  269. package/src/extra/diagnostics/legacy.ts +17 -0
  270. package/src/extra/driverFactories.ts +83 -19
  271. package/src/extra/elementCommands.ts +53 -0
  272. package/src/extra/fetchDriver.ts +519 -0
  273. package/src/extra/focusWithin.ts +27 -0
  274. package/src/extra/form.ts +284 -0
  275. package/src/extra/formHelpers.ts +139 -0
  276. package/src/extra/formTool.ts +150 -0
  277. package/src/extra/head.ts +106 -0
  278. package/src/extra/hmr.ts +2 -3
  279. package/src/extra/owned.ts +24 -0
  280. package/src/extra/pager.ts +50 -0
  281. package/src/extra/persist.ts +133 -0
  282. package/src/extra/pwa.ts +1 -1
  283. package/src/extra/queryCache.ts +107 -0
  284. package/src/extra/reducers.ts +1 -1
  285. package/src/extra/reduxDevtools.ts +92 -0
  286. package/src/extra/replies.ts +54 -0
  287. package/src/extra/router.ts +323 -0
  288. package/src/extra/run.ts +90 -217
  289. package/src/extra/selection.ts +76 -0
  290. package/src/extra/socketDriver.ts +265 -0
  291. package/src/extra/sortable.ts +377 -0
  292. package/src/extra/ssr.ts +427 -160
  293. package/src/extra/standardSchema.ts +27 -0
  294. package/src/extra/testing.ts +2550 -175
  295. package/src/extra/timers.ts +95 -0
  296. package/src/extra/undo.ts +261 -0
  297. package/src/extra/viewTransitions.ts +38 -0
  298. package/src/extra/virtual.ts +513 -0
  299. package/src/extra/widget.ts +201 -0
  300. package/src/index.d.ts +2754 -127
  301. package/src/index.ts +45 -4
  302. package/src/jsx-runtime.ts +15 -1
  303. package/src/jsx.ts +2 -1
  304. package/src/lazy.ts +50 -7
  305. package/src/portal.ts +3 -1
  306. package/src/pragma/index.ts +256 -133
  307. package/src/react-peers.d.ts +5 -0
  308. package/src/react.d.ts +43 -0
  309. package/src/react.ts +110 -0
  310. package/src/shared.ts +57 -0
  311. package/src/slot.ts +1 -1
  312. package/src/suspense.ts +3 -1
  313. package/src/switchable.ts +6 -121
  314. package/src/transition.ts +3 -1
  315. package/src/ui/accordion.ts +64 -0
  316. package/src/ui/dialog.ts +129 -0
  317. package/src/ui/disclosure.ts +35 -0
  318. package/src/ui/popover.ts +45 -0
  319. package/src/ui/shared.ts +94 -0
  320. package/src/ui/tabs.ts +82 -0
  321. package/src/ui/toaster.ts +198 -0
  322. package/src/ui/tooltip.ts +70 -0
  323. package/src/ui/zag/combobox.ts +115 -0
  324. package/src/ui/zag/menu.ts +40 -0
  325. package/src/ui/zag/select.ts +54 -0
  326. package/src/ui/zag/shared.ts +43 -0
  327. package/src/ui-combobox.d.ts +20 -0
  328. package/src/ui-combobox.ts +6 -0
  329. package/src/ui-menu.d.ts +30 -0
  330. package/src/ui-menu.ts +6 -0
  331. package/src/ui-select.d.ts +17 -0
  332. package/src/ui-select.ts +6 -0
  333. package/src/ui-zag-types.d.ts +42 -0
  334. package/src/ui.d.ts +208 -0
  335. package/src/ui.ts +15 -0
  336. package/src/vike/+config.ts +9 -1
  337. package/src/vike/ClientOnly.ts +1 -1
  338. package/src/vike/onRenderClient.ts +144 -96
  339. package/src/vike/onRenderHtml.ts +69 -26
  340. package/src/vike/types.ts +7 -1
  341. package/src/vite/globalthis-shim.ts +18 -0
  342. package/src/vite/globalthis.ts +56 -0
  343. package/src/vite/mcp.ts +568 -0
  344. package/src/vite/plugin.d.ts +47 -3
  345. package/src/vite/plugin.ts +262 -25
  346. package/src/zag.d.ts +64 -0
  347. package/src/zag.ts +238 -0
  348. package/dist/vike/+config.cjs.js +0 -66
  349. package/dist/vike/+config.cjs.js.map +0 -1
  350. package/dist/vike/+config.js.map +0 -1
  351. package/src/component.ts +0 -2301
  352. package/src/cycle/isolate/index.ts +0 -196
  353. package/src/cycle/run/index.ts +0 -151
  354. package/src/cycle/run/internals.ts +0 -143
  355. package/src/cycle/state/Collection.ts +0 -184
  356. package/src/cycle/state/index.ts +0 -5
  357. package/src/cycle/state/pickCombine.ts +0 -201
  358. package/src/cycle/state/pickMerge.ts +0 -122
  359. package/src/cycle/state/withState.ts +0 -47
  360. package/src/pragma/fn.ts +0 -55
@@ -0,0 +1,107 @@
1
+ import {post, sse, json, partsOf, toolName} from './shared';
2
+ import type {HttpOptions} from './shared';
3
+ import {outputJsonSchema} from '../chat/output';
4
+
5
+ /*
6
+ * PLAN-6 L-2: uiMessageStream(url, { headers?, fetch?, body?, dangerouslyAllowBrowser? }), a chat
7
+ * transport for an AI SDK server (the production default, D243): a route that answers with
8
+ * `result.toUIMessageStreamResponse()` / `createUIMessageStreamResponse()` (AI SDK 7's UI message
9
+ * stream protocol v1: SSE, header `x-vercel-ai-ui-message-stream: v1`, `data: [DONE]` at the end).
10
+ *
11
+ * Request: the body DefaultChatTransport sends (`{ id, messages, trigger: 'submit-message',
12
+ * messageId }`; `messageId` when the request continues the last message, `continue: true`, G-628, so `convertToModelMessages(messages)` works on the server as is: the messages
13
+ * are UIMessages, D252; their ids as given, G-640: the chat behavior and driver give every message a
14
+ * stable one; `m<index>` only for a message written without one), plus what the server may use: `instructions`, `model`, `tools` (name ->
15
+ * { description, inputSchema }: client tools the server can declare without `execute`) and
16
+ * `output` (`{ schema }`); `body` merged in.
17
+ *
18
+ * Response: a version other than v1 in the header fails the request with a clear Error (a
19
+ * missing header is accepted: proxies strip headers). Chunks:
20
+ * - `start` -> start (the message id); text-delta / reasoning-delta -> text / reasoning;
21
+ * - `tool-input-available` -> a tool call, held until the step ends: a call the server answers
22
+ * (tool-output-available / -error / -denied, tool-approval-request) is yielded `executed: true`
23
+ * (the app doesn't run it; no `tool` reply), a call still open when the step or stream ends is
24
+ * a client tool call (the `tool` reply); `tool-input-error` -> an executed call + tool-error;
25
+ * - `tool-approval-request` -> tool-approval (the part goes to `approval-requested`; the app
26
+ * answers by setting the part to `approval-responded` and sending the messages again, as with
27
+ * the AI SDK's addToolApprovalResponse); `tool-output-denied` -> tool-denied; the output of an
28
+ * approved call (the next request: only `tool-output-available` comes back) is yielded as
29
+ * that call (name and input from the request's messages) + its result, in the new message;
30
+ * - `data-*` parts -> data events (a transient one is skipped: it isn't part of the message);
31
+ * `file`, `source-url`, `source-document` -> as is;
32
+ * - `finish` -> finish (finishReason); `abort` -> finish ('abort'); `error` -> a thrown Error;
33
+ * - everything else (text-start/end, tool-input-start/delta, start-step, message-metadata, ...)
34
+ * is ignored.
35
+ */
36
+
37
+ export interface UIMessageStreamOptions extends HttpOptions {}
38
+
39
+ const VERSION = 'v1';
40
+ const ONE = /^(start-step|finish-step|finish)$/;
41
+
42
+ export function uiMessageStream(url: string, options: UIMessageStreamOptions = {}) {
43
+ return {
44
+ async *stream(req: any, signal: AbortSignal): AsyncGenerator<any, void, any> {
45
+ const o = req.output !== undefined ? outputJsonSchema(req.output) : undefined;
46
+ const messages = req.messages.map((m: any, i: number) => ({id: m.id ?? `m${i}`, role: m.role, parts: partsOf(m), ...(m.metadata !== undefined && {metadata: m.metadata})}));
47
+ const last = messages[messages.length - 1];
48
+ const res = await post(url, {
49
+ ...(req.instructions && {instructions: req.instructions}),
50
+ ...(req.model && {model: req.model}),
51
+ ...(req.tools && {tools: req.tools}),
52
+ ...(o && {output: o}),
53
+ }, req, signal, options, 'uiMessageStream', {
54
+ // the protocol fields: merged after `body`, which can't override them (G-654)
55
+ ...(req.chatId != null && {id: req.chatId}),
56
+ messages,
57
+ trigger: 'submit-message',
58
+ // G-628: a continued assistant message (DefaultChatTransport's messageId)
59
+ ...(req.continue === true && last?.role == 'assistant' && {messageId: last.id}),
60
+ });
61
+ const v = res.headers?.get?.('x-vercel-ai-ui-message-stream');
62
+ if (v && v != VERSION) {
63
+ res.body.cancel?.().catch?.(() => {});
64
+ throw Object.assign(new Error(`uiMessageStream: the server speaks UI message stream ${v}; this transport speaks ${VERSION} (AI SDK 7). Update sygnal, or pin the server's ai package to a version that sends ${VERSION}`), {version: v});
65
+ }
66
+ // client tool calls not yet answered by the server, in order
67
+ let open: any[] = [];
68
+ // the call `id` as an executed call: an open one, or one from the request's messages (the
69
+ // server runs an approved call on the next request and sends only its output); [] once yielded
70
+ const yielded = new Set<string>();
71
+ const take = (id: string) => {
72
+ if (yielded.has(id)) return [];
73
+ yielded.add(id);
74
+ const i = open.findIndex(c => c.id === id);
75
+ if (i >= 0) return [{...open.splice(i, 1)[0], executed: true}];
76
+ for (const m of req.messages) for (const p of partsOf(m)) {
77
+ const name = toolName(p);
78
+ if (name && p.toolCallId === id) return [{type: 'tool-call', id, name, input: p.input, executed: true}];
79
+ }
80
+ return [];
81
+ };
82
+ for await (const {data} of sse(res)) {
83
+ if (data == '[DONE]') break;
84
+ const c = json(data), t = c?.type;
85
+ if (typeof t != 'string') continue;
86
+ if (ONE.test(t)) { for (const o of open) yielded.add(o.id); yield* open; open = []; }
87
+ if (t == 'text-delta' || t == 'reasoning-delta') yield {type: t.slice(0, -6), delta: c.delta};
88
+ else if (t == 'start') { if (c.messageId != null) yield {type: 'start', id: c.messageId}; }
89
+ else if (t == 'tool-input-available') open.push({type: 'tool-call', id: c.toolCallId, name: c.toolName, input: c.input, ...(c.providerExecuted && {executed: true, providerExecuted: true})});
90
+ else if (t == 'tool-input-error') yield* [{type: 'tool-call', id: c.toolCallId, name: c.toolName, input: c.input, executed: true}, {type: 'tool-error', id: c.toolCallId, error: c.errorText}];
91
+ else if (t == 'tool-output-available') yield* [...take(c.toolCallId), {type: 'tool-result', id: c.toolCallId, output: c.output}];
92
+ else if (t == 'tool-output-error') yield* [...take(c.toolCallId), {type: 'tool-error', id: c.toolCallId, error: c.errorText}];
93
+ else if (t == 'tool-output-denied') yield* [...take(c.toolCallId), {type: 'tool-denied', id: c.toolCallId}];
94
+ else if (t == 'tool-approval-request') {
95
+ yield* take(c.toolCallId);
96
+ yield {type: 'tool-approval', id: c.toolCallId, approval: {id: c.approvalId, ...(c.reason && {requestReason: c.reason}), ...(c.approvalDescriptor !== undefined && {descriptor: c.approvalDescriptor}), ...(c.isAutomatic && {isAutomatic: true}), ...(c.signature && {signature: c.signature})}};
97
+ }
98
+ else if (t.startsWith('data-')) { if (!c.transient) yield {type: t, ...(c.id != null && {id: c.id}), data: c.data}; }
99
+ else if (t == 'file' || t == 'source-url' || t == 'source-document') { const {providerMetadata, ...part} = c; yield part; }
100
+ else if (t == 'finish') yield {type: 'finish', ...(c.finishReason && {reason: c.finishReason})};
101
+ else if (t == 'abort') yield {type: 'finish', reason: 'abort'};
102
+ else if (t == 'error') throw new Error('uiMessageStream: ' + (c.errorText ?? 'the server failed'));
103
+ }
104
+ yield* open;
105
+ },
106
+ };
107
+ }
@@ -0,0 +1,260 @@
1
+ /*
2
+ * PLAN-6 A-2 (D241: experimental, outside semver until WebMCP's origin trial ends):
3
+ * `experimentalExposeWebMcp(app, { exposedTo?, confirm?, prefix?, modelContext? })` offers the
4
+ * A-1 tools of an app to the browser's agent through WebMCP (`document.modelContext`, Chrome 153
5
+ * with `--enable-features=WebMCP`, or a polyfill). Spike 0-S3 (`research/p6-spikes/0-S3.md`).
6
+ *
7
+ * - Feature detection: `modelContext` option ?? `document.modelContext` ?? `navigator.modelContext`
8
+ * (the deprecated form); without one it does nothing and returns a `stop()` with
9
+ * `available: false` (SYG674, info, dev).
10
+ * - One `registerTool(tool, { signal, exposedTo? })` per A-1 tool; a tool whose name, description,
11
+ * schema or annotations changed (a `when`, the live Collection keys, the state summary) is
12
+ * unregistered by aborting its signal and registered again. `registerTool` returns a Promise:
13
+ * a rejection (a duplicate or invalid name) is SYG676, never thrown.
14
+ * - `execute` never throws and always resolves an object (strings and `undefined` differ between
15
+ * Chrome and the polyfill, 0-S3 §3); A-1 validates the input.
16
+ * - Chrome's budgets, which neither Chrome 153 nor the polyfill enforce (G-601): name ≤ 30,
17
+ * description ≤ 500, parameter description ≤ 150, result ≤ 1,500 characters of JSON. Over a
18
+ * budget is SYG242 (dev, once per tool and part), then cut safely (a name keeps a hash of the
19
+ * whole, so names stay unique; a result keeps `ok` / `error` and cuts `state`).
20
+ * - Annotations: A-1's `readOnlyHint` and `consequentialHint` (advisory: Chrome 153 drops it, so
21
+ * `confirm` is the safeguard), and `untrustedContentHint` on the tools whose results carry a
22
+ * projection with user strings: `untrusted: true` on the declaration, or (unless `untrusted:
23
+ * false`) inferred from string values (A-1's `hasUserText`: not under `id` / `status` / `type` /
24
+ * `kind` keys, not one of the declaration's input enum values; G-623), with SYG244 (dev).
25
+ * - Context (D250): A-1's read tool per declaration, plus a short summary of that declaration's
26
+ * projection at the end of each of its tool descriptions, re-registered when it changes. An
27
+ * untrusted projection gets no summary (D286, G-650): user text never goes into a description,
28
+ * where an agent would read it as instructions, and a structure-only one misled small models.
29
+ * - Item tools whose key parameter lists ids without labels (`unlabel`, or no `label`) name the read
30
+ * tool with the items' data: "Call board_read first to find the card's id." (G-650: small models
31
+ * guessed ids otherwise).
32
+ * - Consequential calls: the `confirm` option, by default a native modal `<dialog>` appended to
33
+ * `document.body` (outside the app's tree): labelled and described, the rest of the page inert
34
+ * while it's open, "Deny" focused, Escape denies.
35
+ * Side-effect free: an app that doesn't import it pays 0 B.
36
+ */
37
+ import {agentTools, hasUserText, labelsUntrusted, unlabel, lookUp, Confirm, ConfirmInfo, AgentTool, AgentResult} from './agent/index'
38
+ import {report} from '../diagnostics/index'
39
+
40
+ export interface ExposeWebMcpOptions {
41
+ /** origins the tools are exposed to (passed to `registerTool` where the API takes it) */
42
+ exposedTo?: string[]
43
+ /** consequential calls: `true` runs them, `false` declines, a function asks (default: a native `<dialog>`) */
44
+ confirm?: Confirm
45
+ /** prepended to every tool name (`shop_` → shop_todos_add) */
46
+ prefix?: string
47
+ /** the WebMCP context (default `document.modelContext ?? navigator.modelContext`) */
48
+ modelContext?: any
49
+ }
50
+ export type WebMcpHandle = (() => void) & {readonly available: boolean}
51
+
52
+ const NAME = 30, DESC = 500, PARAM = 150, OUT = 1500, SUMMARY = 300
53
+
54
+ const g: any = globalThis
55
+
56
+ /** cut to n characters with an ellipsis, never inside a surrogate pair */
57
+ const cut = (s: string, n: number) => {
58
+ if (s.length <= n) return s
59
+ let i = n - 1
60
+ if (/[\ud800-\udbff]/.test(s[i - 1] || '')) i--
61
+ return s.slice(0, i) + '…'
62
+ }
63
+ const hash = (s: string) => {
64
+ let h = 5381
65
+ for (let i = 0; i < s.length; i++) h = (h * 33 ^ s.charCodeAt(i)) >>> 0
66
+ return (h % 1679616).toString(36).padStart(4, '0')
67
+ }
68
+
69
+ export function experimentalExposeWebMcp(app: any, options: ExposeWebMcpOptions = {}): WebMcpHandle {
70
+ const mc = options.modelContext ?? g.document?.modelContext ?? g.navigator?.modelContext
71
+ const said = new Set<string>()
72
+ const dev = (code: string, key: string, message: string, severity: 'warn' | 'info' = 'warn') => {
73
+ if (said.has(code + key)) return
74
+ said.add(code + key)
75
+ try { report(code, {component: 'experimentalExposeWebMcp', message, severity}) } catch (e) { queueMicrotask(() => { throw e }) }
76
+ }
77
+ if (!mc || typeof mc.registerTool != 'function') {
78
+ dev('SYG674', '', 'no WebMCP here (document.modelContext is undefined): the agent tools are not exposed', 'info')
79
+ return Object.assign(() => {}, {available: false})
80
+ }
81
+
82
+ let closeDialog: (() => void) | undefined
83
+ const confirm: Confirm = options.confirm ?? (info => dialog(info, f => { closeDialog = f }))
84
+ const layer: any = agentTools(app, {confirm})
85
+ const prefix = options.prefix || ''
86
+ const reg = new Map<string, {sig: string; ac: AbortController}>()
87
+ let stopped = false
88
+
89
+ /** the WebMCP name of an A-1 tool: prefixed, legal characters, ≤ 30 (a hash keeps it unique) */
90
+ const nameOf = (n: string) => {
91
+ const full = (prefix + n).replace(/[^\w.-]/g, '_')
92
+ if (full.length <= NAME) return full
93
+ dev('SYG242', full + 'n', `the tool name '${full}' is longer than ${NAME} characters; it is offered as '${full.slice(0, NAME - 5)}_${hash(full)}'`)
94
+ return full.slice(0, NAME - 5) + '_' + hash(full)
95
+ }
96
+ /** parameter descriptions ≤ 150, in a copy of the schema */
97
+ const params = (s: any, tool: string, path: string): any => {
98
+ if (Array.isArray(s)) return s.map(x => params(x, tool, path))
99
+ if (!s || typeof s != 'object') return s
100
+ const o: any = {...s}
101
+ if (typeof s.description == 'string' && s.description.length > PARAM) {
102
+ dev('SYG242', tool + path, `the description of ${tool}'s parameter ${path || '(the input)'} is longer than ${PARAM} characters; it is cut`)
103
+ o.description = cut(s.description, PARAM)
104
+ }
105
+ for (const k of ['properties', '$defs', 'definitions', 'patternProperties']) if (s[k] && typeof s[k] == 'object') {
106
+ o[k] = {}
107
+ for (const p in s[k]) o[k][p] = params(s[k][p], tool, path ? path + '.' + p : p)
108
+ }
109
+ for (const k of ['items', 'prefixItems', 'anyOf', 'allOf', 'oneOf', 'additionalProperties', 'not']) if (s[k] && typeof s[k] == 'object') o[k] = params(s[k], tool, path)
110
+ return o
111
+ }
112
+ /** a result within 1,500 characters of JSON: `state` and long fields are cut first */
113
+ const fit = (r: any, tool: string): any => {
114
+ if (!r || typeof r != 'object') r = {ok: false, error: 'no result'}
115
+ let s = JSON.stringify(r)
116
+ if (s.length <= OUT) return r
117
+ dev('SYG242', tool + 'out', `a result of ${tool} is ${s.length} characters of JSON, over the budget of ${OUT}; its state is cut (make the agent.read projection smaller)`)
118
+ const {state, issues, keys, ...rest} = r
119
+ const o: any = {...rest, truncated: true}
120
+ if (typeof o.error == 'string') o.error = cut(o.error, 400)
121
+ if (issues) o.issues = issues.slice(0, 3)
122
+ for (let room = OUT; room > 20; room = room / 2 | 0) {
123
+ if (state !== undefined) o.state = cut(JSON.stringify(state), room)
124
+ if (JSON.stringify(o).length <= OUT) return o
125
+ }
126
+ delete o.state
127
+ delete o.issues
128
+ return o
129
+ }
130
+
131
+ function sync(tools: AgentTool[], context: Record<string, any>) {
132
+ if (stopped) return
133
+ // per A-1 tool: its declaration's summary and whether its results carry user strings
134
+ const summary = new Map<string, string>(), untrusted = new Set<string>()
135
+ let anyUntrusted = false
136
+ const groups = layer.groups()
137
+ for (const gr of groups) if (gr.read) {
138
+ const v = context[gr.name]
139
+ let u = gr.untrusted
140
+ if (u === undefined && hasUserText(v, gr.enums)) {
141
+ u = true
142
+ dev('SYG244', gr.name, `agent '${gr.name}': read returns strings, so its WebMCP tools get untrustedContentHint; declare untrusted: true (user-entered text) or untrusted: false (only the app's own text)`)
143
+ }
144
+ // D286 (G-622): descriptions are read as instructions and untrustedContentHint covers only
145
+ // results, so an untrusted projection gets no summary; the read tool returns the contents
146
+ // with the hint. G-650: the structure-only summary D286 had at first ("5 items (ids 1, 2, …)")
147
+ // read as the read tool's output to small models ("it has no titles"), which then guessed ids
148
+ const json = v === undefined || u ? '' : JSON.stringify(v)
149
+ for (const t of gr.tools) {
150
+ if (u) untrusted.add(t)
151
+ if (json) summary.set(t, '\n\nCurrent state: ' + cut(json, SUMMARY))
152
+ }
153
+ anyUntrusted ||= !!u
154
+ }
155
+ // a declaration with no read: its results carry an ancestor's projection
156
+ if (anyUntrusted) for (const gr of groups) if (!gr.read) for (const t of gr.tools) untrusted.add(t)
157
+ // G-644: user-entered labels leave the key parameter even when no projection is untrusted
158
+ const bare = new Set<string>(untrusted)
159
+ for (const gr of groups) if (labelsUntrusted(gr)) for (const t of gr.tools) bare.add(t)
160
+ // G-650: an item tool whose ids come without labels says which read tool to call first
161
+ const reader = new Map<string, {g: any; read: string}>()
162
+ for (const gr of groups) if (gr.reader) for (const t of gr.tools) if (!gr.labels || bare.has(t)) reader.set(t, {g: gr, read: nameOf(gr.reader)})
163
+
164
+ const want = new Map<string, any>()
165
+ for (const t of tools) {
166
+ const name = nameOf(t.name)
167
+ const look = reader.get(t.name), room = DESC - (look ? lookUp('', look.g, look.read).length + 2 : 0)
168
+ let d = t.description || t.name.replace(/_/g, ' ')
169
+ if (d.length > room) {
170
+ dev('SYG242', name + 'd', `the description of ${name} is longer than ${room} characters; it is cut`)
171
+ d = cut(d, room)
172
+ }
173
+ if (look) d = lookUp(d, look.g, look.read)
174
+ const sum = summary.get(t.name)
175
+ if (sum && DESC - d.length > 60) d += cut(sum, DESC - d.length)
176
+ const annotations = {...t.annotations, ...(untrusted.has(t.name) && {untrustedContentHint: true})}
177
+ const a1 = t.name
178
+ want.set(name, {
179
+ name, description: d, inputSchema: params(bare.has(t.name) ? unlabel(t.inputSchema, look ? `${look.read} has their contents` : groups.some((g: any) => g.read) ? 'the read tool has their contents' : 'their labels are user text, not shown') : t.inputSchema, name, ''),
180
+ ...(Object.keys(annotations).length && {annotations}),
181
+ // never throws, always an object (0-S3 §5)
182
+ execute: async (input: any) => {
183
+ let r: AgentResult
184
+ try { r = await layer.call(a1, typeof input == 'string' ? JSON.parse(input) : input ?? {}) } catch (e: any) { r = {ok: false, error: String(e?.message ?? e)} }
185
+ return fit(r, name)
186
+ },
187
+ })
188
+ }
189
+ for (const [name, r] of reg) {
190
+ const w = want.get(name)
191
+ if (!w || sig(w) !== r.sig) { r.ac.abort(); reg.delete(name) }
192
+ }
193
+ for (const [name, tool] of want) {
194
+ if (reg.has(name)) continue
195
+ const ac = new AbortController()
196
+ reg.set(name, {sig: sig(tool), ac})
197
+ const o: any = {signal: ac.signal}
198
+ if (options.exposedTo) o.exposedTo = options.exposedTo
199
+ const fail = (e: any) => dev('SYG676', name + sig(tool), `registerTool('${name}') was rejected: ${e?.name ? e.name + ': ' : ''}${e?.message ?? e}`)
200
+ try { Promise.resolve(mc.registerTool(tool, o)).catch(e => { if (!ac.signal.aborted) fail(e) }) } catch (e) { fail(e) }
201
+ }
202
+ }
203
+ const sig = (t: any) => JSON.stringify([t.description, t.inputSchema, t.annotations])
204
+
205
+ sync(layer.list(), layer.context())
206
+ const unsub = layer.subscribe((c: any) => sync(c.tools, c.context))
207
+
208
+ const stop = () => {
209
+ if (stopped) return
210
+ stopped = true
211
+ unsub()
212
+ layer.stop()
213
+ for (const r of reg.values()) r.ac.abort()
214
+ reg.clear()
215
+ closeDialog?.()
216
+ }
217
+ return Object.assign(stop, {available: true})
218
+ }
219
+
220
+ let n = 0
221
+ /** the default confirmation: a native modal dialog outside the app's tree (Escape and "Deny" decline) */
222
+ function dialog(info: ConfirmInfo, setClose: (f: () => void) => void): Promise<boolean> {
223
+ const doc = g.document
224
+ if (!doc?.createElement) return Promise.resolve(false)
225
+ return new Promise(resolve => {
226
+ const id = 'sygnal-webmcp-confirm-' + ++n, el = (tag: string, text?: string) => { const e = doc.createElement(tag); if (text) e.textContent = text; return e }
227
+ const dlg = el('dialog'), h = el('h2', 'Allow the AI agent to do this?'), p = el('p', info.description + (info.label ? ` (${info.label})` : '')), row = el('div')
228
+ const deny = el('button', 'Deny'), allow = el('button', 'Allow'), back = doc.activeElement
229
+ h.id = id + '-t'
230
+ p.id = id + '-d'
231
+ dlg.setAttribute('aria-labelledby', h.id)
232
+ dlg.setAttribute('aria-describedby', p.id)
233
+ dlg.setAttribute('data-sygnal-webmcp', '')
234
+ deny.type = allow.type = 'button'
235
+ deny.autofocus = true
236
+ dlg.append(h, p)
237
+ if (info.input !== undefined) dlg.append(el('pre', cut(JSON.stringify(info.input), 300)))
238
+ row.append(deny, allow)
239
+ dlg.append(row)
240
+ let open = true
241
+ const done = (v: boolean) => {
242
+ if (!open) return
243
+ open = false
244
+ try { dlg.close() } catch (_) {}
245
+ dlg.remove()
246
+ if (back?.isConnected && back.focus) try { back.focus() } catch (_) {}
247
+ resolve(v)
248
+ }
249
+ setClose(() => done(false))
250
+ deny.onclick = () => done(false)
251
+ allow.onclick = () => done(true)
252
+ // Escape: the modal dialog's cancel (and a keydown where showModal is missing)
253
+ dlg.addEventListener('cancel', (e: Event) => { e.preventDefault(); done(false) })
254
+ dlg.addEventListener('close', () => done(false))
255
+ dlg.addEventListener('keydown', (e: KeyboardEvent) => { if (e.key == 'Escape') { e.preventDefault(); done(false) } })
256
+ doc.body.append(dlg)
257
+ try { dlg.showModal() } catch (_) { dlg.setAttribute('open', '') }
258
+ deny.focus()
259
+ })
260
+ }
@@ -0,0 +1,9 @@
1
+ /*
2
+ * PLAN-3: the retry delay shared by makeSocketDriver's `reconnect` and makeFetchDriver's `retry`.
3
+ * Delay of retry n (from 0): `min(maxDelayMs, delayMs * 2^n)`, varied by ±`jitter` (a fraction;
4
+ * `false` = none). Defaults `{ delayMs: 500, maxDelayMs: 10000, jitter: 0.2 }`.
5
+ */
6
+ export const backoff = (p: {delayMs?: number; maxDelayMs?: number; jitter?: number | boolean}, n: number) => {
7
+ const j = p.jitter === false ? 0 : p.jitter == null || p.jitter === true ? 0.2 : +p.jitter;
8
+ return Math.min(p.maxDelayMs ?? 10000, (p.delayMs ?? 500) * 2 ** n) * (1 + j * (2 * Math.random() - 1));
9
+ };
@@ -0,0 +1,169 @@
1
+ /*
2
+ * PLAN-4 GS-1: reusable behaviors.
3
+ *
4
+ * const pager = defineBehavior({ initialState, intent: (sources, options) => ..., model, calculated })
5
+ * TaskList.uses = { pager: pager({ pageSize: 10, next: Newer, prev: Older }) }
6
+ *
7
+ * A behavior runs on state[key] (a lens). Its actions are '<key>.<ACTION>' (D109). Each value a
8
+ * factory returns carries its own `merge(component, key)`; the core only loops over `uses` and
9
+ * calls it (D114), so an app that never imports this module pays nothing.
10
+ *
11
+ * Merge rules (applied to the component instance, never to its statics):
12
+ * - initialState: state[key] = the behavior's initialState, with the options that name one of its
13
+ * keys (pageSize) overriding it, plus its calculated fields. On a sub-component host (a
14
+ * Collection item, a child with a `state` prop) the slice isn't written into initialState
15
+ * (that would replace the parent's slice, SYG405): state[key] reads as that default until the
16
+ * first write (the core's `_idle` defaults, as for resources).
17
+ * - calculated: fields of the slice, stored on it (state.pager.offset), recomputed after every
18
+ * behavior reducer and after any host STATE reducer that changes state[key].
19
+ * - model: each entry gets the slice (STATE returns the new slice; ABORT and the slice itself
20
+ * mean no change, GS-4), `next('ACTION')` names the behavior's own actions. A host entry for
21
+ * the same action ('pager.NEXT') runs after the behavior's: a host STATE reducer gets the full
22
+ * state with the behavior's update applied (its ABORT keeps that update), a host EFFECT runs
23
+ * after the behavior's, and a host value sink (EVENTS, PARENT, a driver) replaces the
24
+ * behavior's value for that sink.
25
+ * - intent: gets the host's sources (DOM is the host's isolated DOM source; CHILD, EVENTS and
26
+ * drivers as they are) with STATE lensed to the slice, plus the options. A host intent action
27
+ * with the same namespaced name overrides the behavior's trigger.
28
+ * - `instance._behaviorActions` maps each behavior-owned action to its key (t.actions, 2-C).
29
+ * A host intent action of the same name makes it host-owned (removed from the map).
30
+ * - renderComponent marks the streams it adds for simulateAction (`__sygnalTestActions`); they
31
+ * are merged with a behavior trigger of the same name instead of overriding it.
32
+ * - D197 (PLAN-5): every model handler gets `(slice, data, next, props, options, key)`: the
33
+ * use's options and its key in `uses` (so a driver request can name a reply action
34
+ * `key + '.LOADED'`); the intent gets `(sources, options, key)`.
35
+ * - D197: `HOST: (state, data, next, props, options, key) => state` in a model entry is a STATE
36
+ * reducer on the host's whole state (sortable reorders the host's array); ABORT or the same
37
+ * state is no change; the slice's calculated fields are recomputed when it changed. G-378:
38
+ * with a STATE in the same entry, STATE runs first and HOST gets its result.
39
+ * - D197: `timers: (slice, options, key) => ({ name: spec })` declares timers (makeTimerDriver)
40
+ * for the host: named '<key>.<name>', a spec's action / frame naming one of the behavior's
41
+ * actions is namespaced as next() does. They join the host's own `timers` static through an
42
+ * accessor on the component function (installed once; the host's own value is kept as is and
43
+ * can still be assigned), which reads the host's current `uses`.
44
+ * - Limitations: a host intent must return an object (not one stream).
45
+ */
46
+ import xs from './xstreamCompat'
47
+ import {isAbort} from '../shared'
48
+ // a model entry as { sink: fn }: a constant is sent as is, `true` sends the action's data, an
49
+ // EFFECT constant does nothing (as the core treats them)
50
+ const sinksOf = (e: any, S: string): any => {
51
+ const o: any = typeof e == 'function' ? {[S]: e} : {...e}
52
+ for (const s in o) {
53
+ const v = o[s]
54
+ if (typeof v != 'function') o[s] = s == 'EFFECT' ? () => {} : v === true || v === undefined ? (_: any, d: any) => d : () => v
55
+ }
56
+ return o
57
+ }
58
+
59
+ // two STATE reducers in turn: the second gets the first's state (the given one when it ABORTs);
60
+ // an ABORT from the second keeps the first's
61
+ const seq = (bf: any, hf: any) => (st: any, ...x: any[]) => {
62
+ const r = bf(st, ...x), q = hf(isAbort(r) ? st : r, ...x)
63
+ return isAbort(q) ? r : q
64
+ }
65
+
66
+ const calcOf = (calcs: any) => (r: any) => {
67
+ if (r && typeof r == 'object') for (const f in calcs) r = {...r, [f]: calcs[f](r)}
68
+ return r
69
+ }
70
+
71
+ // the hosts whose `timers` static is an accessor (D197); the accessor's getter: the host's own
72
+ // timers plus each behavior's (from the host's current `uses`)
73
+ const timed = new WeakSet<any>()
74
+ const addTimers = (v: any): void => {
75
+ if (timed.has(v)) return
76
+ timed.add(v)
77
+ let own = v.timers
78
+ const all = (st: any) => {
79
+ const o: any = {}, f = own
80
+ if (typeof f == 'function') Object.assign(o, f(st))
81
+ else if (f && typeof f == 'object') for (const r in f) o[r] = f[r](st)
82
+ const u = v.uses
83
+ for (const k in u) {
84
+ const b = u[k], t = b?.timers?.(st?.[k] ?? b.state, b.options, k), rn = (a: any) => a in (b.model || {}) ? k + '.' + a : a
85
+ for (const n in t) {
86
+ const x = t[n]
87
+ o[k + '.' + n] = x && {...x, ...(x.action && {action: rn(x.action)}), ...(x.frame && {frame: rn(x.frame)})}
88
+ }
89
+ }
90
+ return o
91
+ }
92
+ Object.defineProperty(v, 'timers', {configurable: true, enumerable: true, get: () => all, set: (x: any) => { own = x }})
93
+ }
94
+
95
+ const mergeBehavior = (c: any, k: string, b: any): void => {
96
+ const S = c.stateSourceName, calcs = b.calculated, calc = calcOf(calcs), model = c.model = {...c.model}, slice = b.state
97
+ const owned = c._behaviorActions ||= {}, ns = (a: string) => k + '.' + a, opts = b.options
98
+ if (b.timers && c.view) addTimers(c.view)
99
+ if (c.isSubComponent && !c.isolatedState) c._idle = {...c._idle, [k]: slice}
100
+ else c.initialState = {...(typeof c.initialState == 'object' ? c.initialState : {}), [k]: slice}
101
+
102
+ // the slice's calculated fields stay fresh when a host reducer changes state[key]
103
+ const fresh = (f: any) => (st: any, ...x: any[]) => {
104
+ const r = f(st, ...x)
105
+ return r && typeof r == 'object' && st && r[k] !== st[k] ? {...r, [k]: calc(r[k])} : r
106
+ }
107
+ if (calcs) for (const a in model) {
108
+ const e = model[a]
109
+ if (typeof e == 'function') { if (!a.includes('|') || a.split('|')[1].trim() == S) model[a] = fresh(e) }
110
+ else if (e?.[S]) model[a] = {...e, [S]: fresh(e[S])}
111
+ }
112
+
113
+ for (const a in b.model) {
114
+ const e = sinksOf(b.model[a], S), m: any = {}, host = model[ns(a)]
115
+ for (const s in e) {
116
+ // HOST: a STATE reducer on the host's whole state (D197). G-378: with a STATE in the same
117
+ // entry, STATE runs first and HOST gets the state with its update (seq)
118
+ const whole = s == 'HOST', o = whole ? S : s
119
+ const f = (st: any, d: any, next: any, p: any) => {
120
+ const r = e[s](whole ? st : st?.[k], d, next && ((t: string, ...y: any[]) => next(t in b.model ? ns(t) : t, ...y)), p, opts, k)
121
+ return whole ? (isAbort(r) || r === st || !r || r[k] === st?.[k] ? r : {...r, [k]: calc(r[k])})
122
+ : s != S || isAbort(r) ? r : r === st[k] ? st : {...st, [k]: calc(r)}
123
+ }
124
+ m[o] = !m[o] ? f : whole ? seq(m[o], f) : seq(f, m[o])
125
+ }
126
+ if (host) {
127
+ const hs = sinksOf(host, S)
128
+ for (const s in hs) {
129
+ const bf = m[s], hf = hs[s]
130
+ m[s] = !bf ? hf
131
+ : s == S ? seq(bf, hf)
132
+ : s == 'EFFECT' ? (...x: any[]) => { bf(...x); hf(...x) }
133
+ : hf
134
+ }
135
+ }
136
+ model[ns(a)] = m
137
+ owned[ns(a)] = k
138
+ }
139
+
140
+ // one combined intent per instance: the behaviors' actions, then the host's own (which win)
141
+ if (!c._uses) {
142
+ const own = c.intent, list: any[] = c._uses = []
143
+ c.intent = (so: any) => {
144
+ const o: any = {}
145
+ for (const [k, b, slice] of list) {
146
+ const st = so[S], i = b.intent?.({...so, [S]: st?.select({get: (s: any) => s?.[k] ?? slice})}, b.options, k)
147
+ for (const a in i) o[k + '.' + a] = i[a], owned[k + '.' + a] = k
148
+ }
149
+ const h = own?.(so), test = h?.__sygnalTestActions || []
150
+ // renderComponent adds a stream for each static model action (simulateAction): merged in, not an override
151
+ for (const a in h) test.includes(a) && o[a] ? o[a] = xs.merge(o[a], h[a]) : (delete owned[a], o[a] = h[a])
152
+ return o
153
+ }
154
+ }
155
+ c._uses.push([k, b, slice])
156
+ }
157
+
158
+ /**
159
+ * defineBehavior({ initialState, intent?, model?, calculated?, timers? }) returns a factory; call
160
+ * it with the options for one use (`pager({ pageSize: 10, next: Newer })`) in a component's `uses`.
161
+ */
162
+ export const defineBehavior = (def: any) => (options: any = {}): any => {
163
+ const init = {...def.initialState}, b: any = {...def, options}
164
+ for (const o in options) if (o in init) init[o] = options[o]
165
+ // the slice a host starts with: initialState, the options naming its keys, its calculated fields
166
+ b.state = calcOf(def.calculated)(init)
167
+ b.merge = (c: any, k: string) => mergeBehavior(c, k, b)
168
+ return b
169
+ }
@@ -0,0 +1,23 @@
1
+ /*
2
+ * Browser signals (PLAN-3 §1.6, H-6): window focus / the page becoming visible, and the network
3
+ * coming back online. makeFetchDriver's refetchOnFocus / refetchOnReconnect and refetchEvery use
4
+ * them; PLAN-4's browser sources reuse them. Safe without a DOM (SSR, Node): nothing fires.
5
+ */
6
+
7
+ export type BrowserSignal = 'focus' | 'online';
8
+
9
+ /** true while the document is hidden (a background tab); false without a DOM */
10
+ export const isHidden = (): boolean => (globalThis as any).document?.visibilityState == 'hidden';
11
+
12
+ /**
13
+ * Calls `fn('focus')` when the window gains focus or the document becomes visible, and
14
+ * `fn('online')` when the browser goes online. Returns the unsubscribe function.
15
+ */
16
+ export const onBrowserSignals = (fn: (signal: BrowserSignal) => void): (() => void) => {
17
+ const w: any = (globalThis as any).window;
18
+ if (!w?.addEventListener) return () => {};
19
+ const focus = () => isHidden() || fn('focus'), online = () => fn('online');
20
+ const on: Array<[any, string, () => void]> = [[w, 'focus', focus], [w, 'online', online], [w.document, 'visibilitychange', focus]];
21
+ on.forEach(([t, e, f]) => t?.addEventListener(e, f));
22
+ return () => on.forEach(([t, e, f]) => t?.removeEventListener(e, f));
23
+ };