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.
- package/CHANGELOG.md +716 -0
- package/README.md +24 -9
- package/dist/ai.cjs.js +115 -0
- package/dist/ai.cjs.js.map +1 -0
- package/dist/ai.esm.js +2 -0
- package/dist/ai.esm.js.map +1 -0
- package/dist/astro/client.cjs.js +16 -9
- package/dist/astro/client.cjs.js.map +1 -1
- package/dist/astro/client.mjs +16 -9
- package/dist/astro/client.mjs.map +1 -1
- package/dist/astro/index.cjs.js +958 -92
- package/dist/astro/index.cjs.js.map +1 -1
- package/dist/astro/index.mjs +958 -93
- package/dist/astro/index.mjs.map +1 -1
- package/dist/astro/server.cjs.js +3396 -165
- package/dist/astro/server.cjs.js.map +1 -1
- package/dist/astro/server.mjs +3396 -165
- package/dist/astro/server.mjs.map +1 -1
- package/dist/devtools.cjs.js +1840 -0
- package/dist/devtools.cjs.js.map +1 -0
- package/dist/devtools.esm.js +1827 -0
- package/dist/devtools.esm.js.map +1 -0
- package/dist/diagnostics.cjs.js +3190 -296
- package/dist/diagnostics.cjs.js.map +1 -1
- package/dist/diagnostics.esm.js +3190 -296
- package/dist/diagnostics.esm.js.map +1 -1
- package/dist/element.cjs.js +230 -0
- package/dist/element.cjs.js.map +1 -0
- package/dist/element.esm.js +228 -0
- package/dist/element.esm.js.map +1 -0
- package/dist/guide/accessibility.md +310 -0
- package/dist/guide/adapters.md +221 -0
- package/dist/guide/agent.md +588 -0
- package/dist/guide/ai-chat.md +693 -0
- package/dist/guide/ai-decisions.md +330 -0
- package/dist/guide/behaviors.md +312 -0
- package/dist/guide/browser-sources.md +203 -0
- package/dist/guide/drag-and-drop.md +342 -0
- package/dist/guide/element-commands.md +306 -0
- package/dist/guide/error-boundaries.md +169 -0
- package/dist/guide/forms-reference.md +409 -0
- package/dist/guide/forms.md +251 -0
- package/dist/guide/http.md +301 -0
- package/dist/guide/inputs.md +173 -0
- package/dist/guide/mcp-apps.md +272 -0
- package/dist/guide/persistence.md +245 -0
- package/dist/guide/recipes/ai-form-fill.md +163 -0
- package/dist/guide/recipes/ai-summarize.md +155 -0
- package/dist/guide/recipes/ai-support-inbox.md +190 -0
- package/dist/guide/recipes/carousel.md +155 -0
- package/dist/guide/recipes/charts.md +177 -0
- package/dist/guide/recipes/code-editor.md +135 -0
- package/dist/guide/recipes/data-grid.md +155 -0
- package/dist/guide/recipes/data-table.md +196 -0
- package/dist/guide/recipes/i18n.md +222 -0
- package/dist/guide/recipes/icons.md +115 -0
- package/dist/guide/recipes/overview.md +43 -0
- package/dist/guide/recipes/rich-text.md +154 -0
- package/dist/guide/resources.md +471 -0
- package/dist/guide/ssr.md +255 -0
- package/dist/guide/timers.md +178 -0
- package/dist/guide/ui/accordion.md +93 -0
- package/dist/guide/ui/combobox.md +144 -0
- package/dist/guide/ui/dialog.md +124 -0
- package/dist/guide/ui/disclosure.md +60 -0
- package/dist/guide/ui/menu.md +125 -0
- package/dist/guide/ui/overview.md +166 -0
- package/dist/guide/ui/popover.md +101 -0
- package/dist/guide/ui/select.md +114 -0
- package/dist/guide/ui/tabs.md +161 -0
- package/dist/guide/ui/toaster.md +152 -0
- package/dist/guide/ui/tooltip.md +103 -0
- package/dist/guide/undo.md +178 -0
- package/dist/guide/virtual-collections.md +183 -0
- package/dist/guide/web-components.md +179 -0
- package/dist/guide/webmcp.md +226 -0
- package/dist/guide/widgets.md +217 -0
- package/dist/index.cjs.js +19288 -7408
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.d.ts +4466 -552
- package/dist/index.esm.js +19274 -7461
- package/dist/index.esm.js.map +1 -1
- package/dist/jsx-dev-runtime.cjs.js +26 -334
- package/dist/jsx-dev-runtime.cjs.js.map +1 -1
- package/dist/jsx-dev-runtime.esm.js +19 -327
- package/dist/jsx-dev-runtime.esm.js.map +1 -1
- package/dist/jsx-runtime.cjs.js +26 -334
- package/dist/jsx-runtime.cjs.js.map +1 -1
- package/dist/jsx-runtime.esm.js +19 -327
- package/dist/jsx-runtime.esm.js.map +1 -1
- package/dist/jsx.cjs.js +10 -314
- package/dist/jsx.cjs.js.map +1 -1
- package/dist/jsx.esm.js +8 -315
- package/dist/jsx.esm.js.map +1 -1
- package/dist/react.cjs.js +117 -0
- package/dist/react.cjs.js.map +1 -0
- package/dist/react.esm.js +115 -0
- package/dist/react.esm.js.map +1 -0
- package/dist/shims/globalthis.cjs +20 -0
- package/dist/sygnal.min.js +1 -1
- package/dist/sygnal.min.js.map +1 -1
- package/dist/ui-combobox.cjs.js +169 -0
- package/dist/ui-combobox.cjs.js.map +1 -0
- package/dist/ui-combobox.esm.js +148 -0
- package/dist/ui-combobox.esm.js.map +1 -0
- package/dist/ui-menu.cjs.js +87 -0
- package/dist/ui-menu.cjs.js.map +1 -0
- package/dist/ui-menu.esm.js +66 -0
- package/dist/ui-menu.esm.js.map +1 -0
- package/dist/ui-select.cjs.js +104 -0
- package/dist/ui-select.cjs.js.map +1 -0
- package/dist/ui-select.esm.js +83 -0
- package/dist/ui-select.esm.js.map +1 -0
- package/dist/ui.cjs.js +738 -0
- package/dist/ui.cjs.js.map +1 -0
- package/dist/ui.esm.js +727 -0
- package/dist/ui.esm.js.map +1 -0
- package/dist/vike/ClientOnly.cjs.js +4 -14
- package/dist/vike/ClientOnly.cjs.js.map +1 -1
- package/dist/vike/ClientOnly.mjs +4 -14
- package/dist/vike/ClientOnly.mjs.map +1 -1
- package/dist/vike/{+config.js → config/+config.js} +9 -1
- package/dist/vike/config/+config.js.map +1 -0
- package/dist/vike/config/package.json +6 -0
- package/dist/vike/onRenderClient.cjs.js +165 -95
- package/dist/vike/onRenderClient.cjs.js.map +1 -1
- package/dist/vike/onRenderClient.mjs +165 -95
- package/dist/vike/onRenderClient.mjs.map +1 -1
- package/dist/vike/onRenderHtml.cjs.js +59 -24
- package/dist/vike/onRenderHtml.cjs.js.map +1 -1
- package/dist/vike/onRenderHtml.mjs +60 -25
- package/dist/vike/onRenderHtml.mjs.map +1 -1
- package/dist/vite/plugin.cjs.js +919 -89
- package/dist/vite/plugin.cjs.js.map +1 -1
- package/dist/vite/plugin.mjs +919 -90
- package/dist/vite/plugin.mjs.map +1 -1
- package/dist/zag.cjs.js +408 -0
- package/dist/zag.cjs.js.map +1 -0
- package/dist/zag.esm.js +405 -0
- package/dist/zag.esm.js.map +1 -0
- package/llms.txt +172 -103
- package/package.json +112 -14
- package/src/ai.d.ts +1186 -0
- package/src/ai.ts +23 -0
- package/src/astro/client.ts +27 -16
- package/src/astro/index.d.ts +24 -0
- package/src/astro/index.ts +69 -3
- package/src/astro/server.ts +8 -2
- package/src/collection.ts +6 -93
- package/src/core/actions.ts +134 -0
- package/src/core/cell.ts +202 -0
- package/src/core/debug.ts +23 -0
- package/src/core/define.ts +242 -0
- package/src/core/hooks.ts +314 -0
- package/src/core/hosts/collection.ts +325 -0
- package/src/core/hosts/switchable.ts +146 -0
- package/src/core/instance.ts +592 -0
- package/src/core/markers/clientonly.ts +13 -0
- package/src/core/markers/lazy.ts +40 -0
- package/src/core/markers/portal.ts +96 -0
- package/src/core/markers/suspense.ts +41 -0
- package/src/core/markers/transition.ts +70 -0
- package/src/core/registry.ts +25 -0
- package/src/core/runtime.ts +573 -0
- package/src/core/statics.ts +104 -0
- package/src/core/teardown.ts +60 -0
- package/src/core/view.ts +37 -0
- package/src/cycle/dom/DocumentDOMSource.ts +13 -8
- package/src/cycle/dom/ElementFinder.ts +7 -9
- package/src/cycle/dom/EventDelegator.ts +174 -223
- package/src/cycle/dom/IsolateModule.ts +65 -35
- package/src/cycle/dom/MainDOMSource.ts +9 -2
- package/src/cycle/dom/PriorityQueue.ts +4 -2
- package/src/cycle/dom/SymbolTree.ts +18 -47
- package/src/cycle/dom/classNameModule.ts +9 -7
- package/src/cycle/dom/controlledInputModule.ts +23 -6
- package/src/cycle/dom/enrichEventStream.ts +25 -47
- package/src/cycle/dom/fragment.ts +7 -0
- package/src/cycle/dom/isolate.ts +30 -27
- package/src/cycle/dom/makeDOMDriver.ts +115 -56
- package/src/cycle/dom/mockDOMSource.ts +21 -1
- package/src/cycle/dom/modules.ts +16 -3
- package/src/cycle/dom/propsModule.ts +65 -0
- package/src/cycle/dom/selectModule.ts +22 -17
- package/src/cycle/dom/snabbdom.ts +1 -6
- package/src/cycle/dom/thunk.ts +3 -0
- package/src/cycle/dom/utils.ts +98 -0
- package/src/cycle/dom/viewTransition.ts +43 -0
- package/src/cycle/state/StateSource.ts +19 -3
- package/src/cycle/state/objIsEqual.ts +50 -0
- package/src/cycle/state/types.ts +0 -10
- package/src/defineComponent.ts +34 -0
- package/src/devtools.d.ts +170 -0
- package/src/devtools.ts +37 -0
- package/src/element.d.ts +59 -0
- package/src/element.ts +235 -0
- package/src/extra/ai/agent/index.ts +484 -0
- package/src/extra/ai/answers.ts +115 -0
- package/src/extra/ai/chat/behavior.ts +336 -0
- package/src/extra/ai/chat/driver.ts +271 -0
- package/src/extra/ai/chat/memoryTransport.ts +61 -0
- package/src/extra/ai/chat/output.ts +37 -0
- package/src/extra/ai/commandBar.ts +281 -0
- package/src/extra/ai/decide.ts +104 -0
- package/src/extra/ai/index.ts +42 -0
- package/src/extra/ai/link.ts +90 -0
- package/src/extra/ai/mcpApp.ts +314 -0
- package/src/extra/ai/messages.ts +51 -0
- package/src/extra/ai/schema/index.ts +166 -0
- package/src/extra/ai/schema/jsonSchema.ts +72 -0
- package/src/extra/ai/schema/strict.ts +152 -0
- package/src/extra/ai/transports/agui.ts +177 -0
- package/src/extra/ai/transports/anthropicMessages.ts +175 -0
- package/src/extra/ai/transports/chatCompletions.ts +105 -0
- package/src/extra/ai/transports/chromePrompt.ts +77 -0
- package/src/extra/ai/transports/encodeOpenResponses.ts +87 -0
- package/src/extra/ai/transports/fromAISDK.ts +126 -0
- package/src/extra/ai/transports/openResponses.ts +105 -0
- package/src/extra/ai/transports/shared.ts +125 -0
- package/src/extra/ai/transports/tools.ts +59 -0
- package/src/extra/ai/transports/uiMessageStream.ts +107 -0
- package/src/extra/ai/webmcp.ts +260 -0
- package/src/extra/backoff.ts +9 -0
- package/src/extra/behaviors.ts +169 -0
- package/src/extra/browserSignals.ts +23 -0
- package/src/extra/browserSources.ts +326 -0
- package/src/extra/command.ts +3 -3
- package/src/extra/controls.ts +47 -0
- package/src/extra/copyAsTest.ts +286 -0
- package/src/extra/devMcp.ts +386 -0
- package/src/extra/devtools.ts +101 -31
- package/src/extra/devtoolsActions.ts +379 -0
- package/src/extra/devtoolsHook.ts +9 -0
- package/src/extra/devtoolsNext.ts +83 -0
- package/src/extra/diagnostics/checks/actionLog.ts +108 -0
- package/src/extra/diagnostics/checks/behaviors.ts +49 -0
- package/src/extra/diagnostics/checks/browserSources.ts +96 -0
- package/src/extra/diagnostics/checks/chat.ts +71 -0
- package/src/extra/diagnostics/checks/collections.ts +1 -1
- package/src/extra/diagnostics/checks/controls.ts +148 -0
- package/src/extra/diagnostics/checks/dataset.ts +57 -0
- package/src/extra/diagnostics/checks/dom.ts +20 -11
- package/src/extra/diagnostics/checks/elementCommands.ts +175 -0
- package/src/extra/diagnostics/checks/events.ts +17 -2
- package/src/extra/diagnostics/checks/fetch.ts +135 -0
- package/src/extra/diagnostics/checks/forms.ts +171 -0
- package/src/extra/diagnostics/checks/index.ts +103 -4
- package/src/extra/diagnostics/checks/inspect.ts +140 -32
- package/src/extra/diagnostics/checks/next.ts +417 -0
- package/src/extra/diagnostics/checks/persist.ts +54 -0
- package/src/extra/diagnostics/checks/props.ts +10 -5
- package/src/extra/diagnostics/checks/public.d.ts +115 -7
- package/src/extra/diagnostics/checks/replies.ts +128 -0
- package/src/extra/diagnostics/checks/router.ts +100 -0
- package/src/extra/diagnostics/checks/rxjsHints.ts +1 -1
- package/src/extra/diagnostics/checks/shared.ts +86 -2
- package/src/extra/diagnostics/checks/shorthand.ts +115 -0
- package/src/extra/diagnostics/checks/sortable.ts +129 -0
- package/src/extra/diagnostics/checks/state.ts +100 -7
- package/src/extra/diagnostics/checks/statics.ts +48 -0
- package/src/extra/diagnostics/checks/strict.ts +10 -67
- package/src/extra/diagnostics/checks/timers.ts +80 -0
- package/src/extra/diagnostics/checks/viewTransitions.ts +47 -0
- package/src/extra/diagnostics/checks/virtual.ts +73 -0
- package/src/extra/diagnostics/checks/widgets.ts +109 -0
- package/src/extra/diagnostics/checks/wiring.ts +69 -10
- package/src/extra/diagnostics/codes.ts +335 -23
- package/src/extra/diagnostics/index.ts +34 -9
- package/src/extra/diagnostics/legacy.ts +17 -0
- package/src/extra/driverFactories.ts +83 -19
- package/src/extra/elementCommands.ts +53 -0
- package/src/extra/fetchDriver.ts +519 -0
- package/src/extra/focusWithin.ts +27 -0
- package/src/extra/form.ts +284 -0
- package/src/extra/formHelpers.ts +139 -0
- package/src/extra/formTool.ts +150 -0
- package/src/extra/head.ts +106 -0
- package/src/extra/hmr.ts +2 -3
- package/src/extra/owned.ts +24 -0
- package/src/extra/pager.ts +50 -0
- package/src/extra/persist.ts +133 -0
- package/src/extra/pwa.ts +1 -1
- package/src/extra/queryCache.ts +107 -0
- package/src/extra/reducers.ts +1 -1
- package/src/extra/reduxDevtools.ts +92 -0
- package/src/extra/replies.ts +54 -0
- package/src/extra/router.ts +323 -0
- package/src/extra/run.ts +90 -217
- package/src/extra/selection.ts +76 -0
- package/src/extra/socketDriver.ts +265 -0
- package/src/extra/sortable.ts +377 -0
- package/src/extra/ssr.ts +427 -160
- package/src/extra/standardSchema.ts +27 -0
- package/src/extra/testing.ts +2550 -175
- package/src/extra/timers.ts +95 -0
- package/src/extra/undo.ts +261 -0
- package/src/extra/viewTransitions.ts +38 -0
- package/src/extra/virtual.ts +513 -0
- package/src/extra/widget.ts +201 -0
- package/src/index.d.ts +2754 -127
- package/src/index.ts +45 -4
- package/src/jsx-runtime.ts +15 -1
- package/src/jsx.ts +2 -1
- package/src/lazy.ts +50 -7
- package/src/portal.ts +3 -1
- package/src/pragma/index.ts +256 -133
- package/src/react-peers.d.ts +5 -0
- package/src/react.d.ts +43 -0
- package/src/react.ts +110 -0
- package/src/shared.ts +57 -0
- package/src/slot.ts +1 -1
- package/src/suspense.ts +3 -1
- package/src/switchable.ts +6 -121
- package/src/transition.ts +3 -1
- package/src/ui/accordion.ts +64 -0
- package/src/ui/dialog.ts +129 -0
- package/src/ui/disclosure.ts +35 -0
- package/src/ui/popover.ts +45 -0
- package/src/ui/shared.ts +94 -0
- package/src/ui/tabs.ts +82 -0
- package/src/ui/toaster.ts +198 -0
- package/src/ui/tooltip.ts +70 -0
- package/src/ui/zag/combobox.ts +115 -0
- package/src/ui/zag/menu.ts +40 -0
- package/src/ui/zag/select.ts +54 -0
- package/src/ui/zag/shared.ts +43 -0
- package/src/ui-combobox.d.ts +20 -0
- package/src/ui-combobox.ts +6 -0
- package/src/ui-menu.d.ts +30 -0
- package/src/ui-menu.ts +6 -0
- package/src/ui-select.d.ts +17 -0
- package/src/ui-select.ts +6 -0
- package/src/ui-zag-types.d.ts +42 -0
- package/src/ui.d.ts +208 -0
- package/src/ui.ts +15 -0
- package/src/vike/+config.ts +9 -1
- package/src/vike/ClientOnly.ts +1 -1
- package/src/vike/onRenderClient.ts +144 -96
- package/src/vike/onRenderHtml.ts +69 -26
- package/src/vike/types.ts +7 -1
- package/src/vite/globalthis-shim.ts +18 -0
- package/src/vite/globalthis.ts +56 -0
- package/src/vite/mcp.ts +568 -0
- package/src/vite/plugin.d.ts +47 -3
- package/src/vite/plugin.ts +262 -25
- package/src/zag.d.ts +64 -0
- package/src/zag.ts +238 -0
- package/dist/vike/+config.cjs.js +0 -66
- package/dist/vike/+config.cjs.js.map +0 -1
- package/dist/vike/+config.js.map +0 -1
- package/src/component.ts +0 -2301
- package/src/cycle/isolate/index.ts +0 -196
- package/src/cycle/run/index.ts +0 -151
- package/src/cycle/run/internals.ts +0 -143
- package/src/cycle/state/Collection.ts +0 -184
- package/src/cycle/state/index.ts +0 -5
- package/src/cycle/state/pickCombine.ts +0 -201
- package/src/cycle/state/pickMerge.ts +0 -122
- package/src/cycle/state/withState.ts +0 -47
- 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
|
+
};
|