@ggui-ai/iframe-runtime 0.1.0-rc.1

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 (92) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +33 -0
  3. package/dist/bootstrap.d.ts +72 -0
  4. package/dist/bootstrap.js +500 -0
  5. package/dist/canvas/animator/animator-host.d.ts +63 -0
  6. package/dist/canvas/animator/animator-host.js +105 -0
  7. package/dist/canvas/animator/animator-pill.d.ts +42 -0
  8. package/dist/canvas/animator/animator-pill.js +119 -0
  9. package/dist/canvas/animator/state-machine.d.ts +106 -0
  10. package/dist/canvas/animator/state-machine.js +150 -0
  11. package/dist/canvas/canvas-shell.d.ts +77 -0
  12. package/dist/canvas/canvas-shell.js +117 -0
  13. package/dist/canvas/display-mode-policy.d.ts +104 -0
  14. package/dist/canvas/display-mode-policy.js +98 -0
  15. package/dist/canvas/mount.d.ts +150 -0
  16. package/dist/canvas/mount.js +202 -0
  17. package/dist/canvas/nav-stack.d.ts +133 -0
  18. package/dist/canvas/nav-stack.js +172 -0
  19. package/dist/channel-transport.d.ts +182 -0
  20. package/dist/channel-transport.js +344 -0
  21. package/dist/channels/channel-payload.d.ts +20 -0
  22. package/dist/channels/channel-payload.js +34 -0
  23. package/dist/channels/data.d.ts +48 -0
  24. package/dist/channels/data.js +103 -0
  25. package/dist/channels/drain-ack.d.ts +13 -0
  26. package/dist/channels/drain-ack.js +15 -0
  27. package/dist/channels/feedback.d.ts +14 -0
  28. package/dist/channels/feedback.js +19 -0
  29. package/dist/channels/index.d.ts +7 -0
  30. package/dist/channels/index.js +7 -0
  31. package/dist/channels/props-update.d.ts +53 -0
  32. package/dist/channels/props-update.js +130 -0
  33. package/dist/channels/push.d.ts +47 -0
  34. package/dist/channels/push.js +54 -0
  35. package/dist/channels/system.d.ts +26 -0
  36. package/dist/channels/system.js +44 -0
  37. package/dist/compiled-validators.d.ts +40 -0
  38. package/dist/compiled-validators.js +74 -0
  39. package/dist/context-observer.d.ts +205 -0
  40. package/dist/context-observer.js +312 -0
  41. package/dist/gadget-loader.d.ts +118 -0
  42. package/dist/gadget-loader.js +136 -0
  43. package/dist/globals.d.ts +234 -0
  44. package/dist/globals.js +101 -0
  45. package/dist/host-context-emitter.d.ts +91 -0
  46. package/dist/host-context-emitter.js +213 -0
  47. package/dist/iframe-runtime.js +261 -0
  48. package/dist/index.d.ts +38 -0
  49. package/dist/index.js +4 -0
  50. package/dist/lifecycle.d.ts +66 -0
  51. package/dist/lifecycle.js +49 -0
  52. package/dist/mcp-app-iframe-host.d.ts +69 -0
  53. package/dist/mcp-app-iframe-host.js +214 -0
  54. package/dist/observability.d.ts +225 -0
  55. package/dist/observability.js +28 -0
  56. package/dist/protocol-error.d.ts +199 -0
  57. package/dist/protocol-error.js +116 -0
  58. package/dist/provisional-renderer.d.ts +62 -0
  59. package/dist/provisional-renderer.js +323 -0
  60. package/dist/react-renderer.d.ts +95 -0
  61. package/dist/react-renderer.js +343 -0
  62. package/dist/registry-subscribe.d.ts +102 -0
  63. package/dist/registry-subscribe.js +319 -0
  64. package/dist/runtime.d.ts +531 -0
  65. package/dist/runtime.js +2623 -0
  66. package/dist/server.d.ts +36 -0
  67. package/dist/server.js +72 -0
  68. package/dist/stack-item-renderer.d.ts +103 -0
  69. package/dist/stack-item-renderer.js +270 -0
  70. package/dist/stack.d.ts +90 -0
  71. package/dist/stack.js +118 -0
  72. package/dist/status-dom.d.ts +72 -0
  73. package/dist/status-dom.js +51 -0
  74. package/dist/system-cards/NoCredentialsCard.d.ts +33 -0
  75. package/dist/system-cards/NoCredentialsCard.js +189 -0
  76. package/dist/system-cards/ProtocolProbeCard.d.ts +64 -0
  77. package/dist/system-cards/ProtocolProbeCard.js +463 -0
  78. package/dist/system-cards/QrCode.d.ts +46 -0
  79. package/dist/system-cards/QrCode.js +55 -0
  80. package/dist/system-cards/host-detect.d.ts +31 -0
  81. package/dist/system-cards/host-detect.js +108 -0
  82. package/dist/system-cards/host-intents.d.ts +85 -0
  83. package/dist/system-cards/host-intents.js +170 -0
  84. package/dist/system-cards/index.d.ts +70 -0
  85. package/dist/system-cards/index.js +134 -0
  86. package/dist/types.d.ts +69 -0
  87. package/dist/types.js +3 -0
  88. package/dist/validation.d.ts +83 -0
  89. package/dist/validation.js +106 -0
  90. package/dist/wire-config.d.ts +214 -0
  91. package/dist/wire-config.js +290 -0
  92. package/package.json +81 -0
@@ -0,0 +1,343 @@
1
+ /**
2
+ * React component renderer for the iframe runtime.
3
+ *
4
+ * Port of `@ggui-ai/react::components/ReactComponentRenderer.tsx` (L10-384)
5
+ * with the enclosing React component stripped — the iframe runtime
6
+ * does not have an outer React tree; it owns the root `createRoot`
7
+ * mount itself. The inner eval + data-URL-shim + error-boundary
8
+ * pattern is preserved verbatim so today's generated code (which
9
+ * reads `globalThis.__ggui__` via the shim rewrite) keeps working.
10
+ *
11
+ * Lifecycle seams:
12
+ *
13
+ * - `mount(container, stackItem)` — replace the container's
14
+ * children with a React root rendering the compiled component.
15
+ * Returns a `ReactRootMount` handle; callers use `update()` to
16
+ * swap props without re-evaluating the module, `unmount()` to
17
+ * tear down.
18
+ * - `update(stackItem)` — swap props (and/or componentCode). When
19
+ * componentCode changes, re-evaluates; when only props change,
20
+ * re-renders the existing component tree with new props.
21
+ * - `unmount()` — `root.unmount()` + detach observability.
22
+ *
23
+ * Error handling — ErrorBoundary with auto-retry (AUTO_RETRY_LIMIT +
24
+ * AUTO_RETRY_DELAY) matches the `ReactComponentRenderer` contract.
25
+ * On terminal error, the callback `onRequestRepair` fires; the
26
+ * stack-item dispatcher wires this to the `ggui:request-repair`
27
+ * live-channel envelope `StackItemRenderer` dispatches.
28
+ *
29
+ * No JSX in this file — the mount-root code uses
30
+ * `React.createElement` + `React.Fragment` directly so the renderer's
31
+ * TypeScript config doesn't need a JSX runtime plumbing.
32
+ */
33
+ import React, { Component, createElement, Fragment, } from 'react';
34
+ import { createRoot } from 'react-dom/client';
35
+ import { stripMarkers, rewriteImports, getScopedThemeCss, getScopedCssTokens, getThemeCss, getCssTokens, } from '@ggui-ai/design/rendering';
36
+ import { hoistImports, loadModule } from '@ggui-ai/design/module-loader';
37
+ // =============================================================================
38
+ // Scope class helpers — port of RCR's `makeScopeClass` + counter.
39
+ // =============================================================================
40
+ let scopeCounter = 0;
41
+ /**
42
+ * Generate a CSS-safe scope class. The iframe-internal mount doesn't
43
+ * have access to React's `useId` (we're not inside a React tree at
44
+ * the call site), so the counter is the always-available fallback.
45
+ * `data-` attribute wrapping plus the counter-based class is fine —
46
+ * only one ReactRootMount exists per iframe by construction.
47
+ */
48
+ function makeScopeClass() {
49
+ scopeCounter += 1;
50
+ return `ggui-rcr-${scopeCounter}`;
51
+ }
52
+ // =============================================================================
53
+ // Error boundary — port of RCR's internal ErrorBoundary (L68–203).
54
+ // =============================================================================
55
+ const AUTO_RETRY_LIMIT = 1;
56
+ const AUTO_RETRY_DELAY = 500;
57
+ class RcrErrorBoundary extends Component {
58
+ state = {
59
+ error: null,
60
+ catchCount: 0,
61
+ autoRetrying: false,
62
+ repairRequested: false,
63
+ };
64
+ retryTimer = null;
65
+ static getDerivedStateFromError(error) {
66
+ return { error };
67
+ }
68
+ componentDidCatch(error, _info) {
69
+ const nextCount = this.state.catchCount + 1;
70
+ this.setState({ catchCount: nextCount });
71
+ if (nextCount <= AUTO_RETRY_LIMIT) {
72
+ this.setState({ autoRetrying: true });
73
+ this.retryTimer = setTimeout(() => {
74
+ this.setState({ error: null, autoRetrying: false });
75
+ }, AUTO_RETRY_DELAY);
76
+ return;
77
+ }
78
+ this.props.onError?.(error);
79
+ if (this.props.onRequestRepair && !this.state.repairRequested) {
80
+ this.setState({ repairRequested: true });
81
+ this.props.onRequestRepair(error);
82
+ }
83
+ }
84
+ componentWillUnmount() {
85
+ if (this.retryTimer !== null)
86
+ clearTimeout(this.retryTimer);
87
+ }
88
+ render() {
89
+ const { error, autoRetrying, repairRequested, catchCount } = this.state;
90
+ if (error === null)
91
+ return this.props.children;
92
+ // The three fallback UIs match the host-SDK error boundary
93
+ // verbatim (same inline styles so operator-visible DOM is
94
+ // identical between the host-SDK and iframe-renderer paths).
95
+ const FONT = '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif';
96
+ if (autoRetrying) {
97
+ return createElement('div', {
98
+ style: {
99
+ display: 'flex',
100
+ alignItems: 'center',
101
+ justifyContent: 'center',
102
+ padding: 24,
103
+ minHeight: 80,
104
+ gap: 8,
105
+ fontFamily: FONT,
106
+ fontSize: 13,
107
+ color: 'rgba(255,255,255,0.4)',
108
+ },
109
+ }, createElement('div', {
110
+ style: {
111
+ width: 16,
112
+ height: 16,
113
+ border: '2px solid rgba(255,255,255,0.15)',
114
+ borderTopColor: 'rgba(255,255,255,0.5)',
115
+ borderRadius: '50%',
116
+ animation: 'ggui-err-spin 0.6s linear infinite',
117
+ },
118
+ }), 'Retrying...', createElement('style', null, '@keyframes ggui-err-spin { to { transform: rotate(360deg); } }'));
119
+ }
120
+ if (repairRequested) {
121
+ return createElement('div', {
122
+ style: {
123
+ display: 'flex',
124
+ flexDirection: 'column',
125
+ alignItems: 'center',
126
+ justifyContent: 'center',
127
+ padding: '32px 24px',
128
+ minHeight: 120,
129
+ gap: 10,
130
+ textAlign: 'center',
131
+ fontFamily: FONT,
132
+ },
133
+ }, createElement('div', {
134
+ style: {
135
+ width: 16,
136
+ height: 16,
137
+ border: '2px solid rgba(139,92,246,0.2)',
138
+ borderTopColor: 'rgba(139,92,246,0.7)',
139
+ borderRadius: '50%',
140
+ animation: 'ggui-err-spin 0.8s linear infinite',
141
+ },
142
+ }), createElement('div', { style: { fontSize: 13, color: 'rgba(255,255,255,0.5)' } }, 'Repairing component...'), createElement('style', null, '@keyframes ggui-err-spin { to { transform: rotate(360deg); } }'));
143
+ }
144
+ return createElement('div', {
145
+ style: {
146
+ display: 'flex',
147
+ flexDirection: 'column',
148
+ alignItems: 'center',
149
+ justifyContent: 'center',
150
+ padding: '32px 24px',
151
+ minHeight: 120,
152
+ gap: 12,
153
+ textAlign: 'center',
154
+ fontFamily: FONT,
155
+ },
156
+ }, createElement('div', {
157
+ style: {
158
+ width: 48,
159
+ height: 48,
160
+ borderRadius: 14,
161
+ background: 'rgba(239, 68, 68, 0.1)',
162
+ display: 'flex',
163
+ alignItems: 'center',
164
+ justifyContent: 'center',
165
+ fontSize: 22,
166
+ },
167
+ }, '!'), createElement('div', { style: { fontSize: 14, fontWeight: 600, color: 'rgba(255,255,255,0.8)' } }, 'Something went wrong'), createElement('div', { style: { fontSize: 12, color: 'rgba(255,255,255,0.4)', maxWidth: 280, lineHeight: 1.4 } }, 'This component encountered an error. Try asking for a different view.'), createElement('button', {
168
+ onClick: () => this.setState({ error: null, catchCount: 0, repairRequested: false }),
169
+ style: {
170
+ marginTop: 4,
171
+ padding: '8px 20px',
172
+ borderRadius: 8,
173
+ border: '1px solid rgba(255,255,255,0.15)',
174
+ background: 'rgba(255,255,255,0.08)',
175
+ color: 'rgba(255,255,255,0.7)',
176
+ fontSize: 13,
177
+ fontWeight: 500,
178
+ cursor: 'pointer',
179
+ },
180
+ }, 'Retry'), catchCount > 1
181
+ ? createElement('div', { style: { fontSize: 11, color: 'rgba(255,255,255,0.2)' } }, error.message)
182
+ : null);
183
+ }
184
+ }
185
+ /**
186
+ * Mount a React root inside `container` and render `stackItem`'s
187
+ * compiled componentCode. The container is fully owned after this
188
+ * call (existing children are replaced).
189
+ */
190
+ export async function mountReactRoot(container, opts) {
191
+ container.replaceChildren();
192
+ const root = createRoot(container);
193
+ const scopeClass = makeScopeClass();
194
+ let currentOpts = opts;
195
+ let currentCode = null;
196
+ let currentComponent = null;
197
+ async function evaluate(code) {
198
+ // 0. If code is a URL (S3 presigned), fetch the bytes first.
199
+ // Matches RCR's first-step URL-fetch guard verbatim.
200
+ let resolved = code;
201
+ if (code.startsWith('https://')) {
202
+ const response = await fetch(code);
203
+ if (!response.ok) {
204
+ throw new Error(`Failed to fetch component code: ${response.status}`);
205
+ }
206
+ resolved = await response.text();
207
+ }
208
+ // 1. Strip metadata markers (__GGUI_META__ / __GGUI_STREAM_SPEC__).
209
+ const cleaned = stripMarkers(resolved);
210
+ // 2. Hoist imports above var declarations.
211
+ const hoisted = hoistImports(cleaned);
212
+ // 3. Rewrite bare specifiers to data-url shims reading from
213
+ // `globalThis.__ggui__`. The __ggui__ registry is installed
214
+ // by `runtime.ts::bootSequence` BEFORE any mountReactRoot call
215
+ // — see globals.ts docstring on TOCTOU ordering.
216
+ const rewritten = rewriteImports(hoisted, {
217
+ mode: 'data-url',
218
+ gadgetPackages: currentOpts.gadgetPackages,
219
+ });
220
+ // 4. Dynamically import the module.
221
+ const mod = await loadModule(rewritten);
222
+ // 5. Extract default export (or first function export).
223
+ const Comp = (mod.default ??
224
+ Object.values(mod).find((v) => typeof v === 'function'));
225
+ if (Comp === undefined) {
226
+ throw new Error('Module does not export a default component');
227
+ }
228
+ return Comp;
229
+ }
230
+ function renderTree(opts) {
231
+ const themeCss = opts.themeId
232
+ ? getScopedThemeCss(opts.themeId, scopeClass, opts.themeMode)
233
+ : getScopedCssTokens(scopeClass, opts.themeMode);
234
+ // Also inject theme CSS at `:root` on `document.head`. The scoped
235
+ // injection above isolates token resolution to the React tree, but
236
+ // the embedding shell's body styles (font-family, color, background)
237
+ // resolve against `:root` — without this, body chrome falls back to
238
+ // the inline-style defaults (-apple-system, etc.) even when the
239
+ // tree itself paints correctly. Idempotent — last mount wins on
240
+ // repeat calls. Same shared id as `@ggui-ai/design/themes`'
241
+ // `ThemeProvider` so a Studio/Portal context with both runtime + RP
242
+ // active doesn't double-stack.
243
+ if (typeof document !== 'undefined') {
244
+ const rootCss = opts.themeId
245
+ ? getThemeCss(opts.themeId, opts.themeMode)
246
+ : getCssTokens(opts.themeMode);
247
+ const styleId = 'ggui-theme-vars';
248
+ let styleEl = document.getElementById(styleId);
249
+ if (!styleEl) {
250
+ styleEl = document.createElement('style');
251
+ styleEl.id = styleId;
252
+ document.head.appendChild(styleEl);
253
+ }
254
+ if (styleEl.textContent !== rootCss) {
255
+ styleEl.textContent = rootCss;
256
+ }
257
+ }
258
+ const componentElement = currentComponent === null
259
+ ? null
260
+ : createElement(currentComponent, opts.stackItem.props ?? {});
261
+ const wrapped = opts.renderWrapper && componentElement !== null
262
+ ? opts.renderWrapper(componentElement)
263
+ : componentElement;
264
+ // The scope `<div>` + inline `<style>` + error-boundary-wrapped
265
+ // children match RCR L376–383. `key={currentCode?.length}` mirrors
266
+ // RCR's "reset boundary when code changes" heuristic.
267
+ root.render(createElement('div', { className: scopeClass }, createElement('style', null, `${themeCss}${opts.cssOverrides ?? ''}`), createElement(RcrErrorBoundary, {
268
+ key: currentCode?.length ?? 0,
269
+ ...(opts.onError ? { onError: opts.onError } : {}),
270
+ ...(opts.onRequestRepair ? { onRequestRepair: opts.onRequestRepair } : {}),
271
+ }, createElement(Fragment, null, wrapped))));
272
+ }
273
+ async function initialEvaluate() {
274
+ const code = currentOpts.stackItem.componentCode ?? '';
275
+ if (code.trim().length === 0) {
276
+ currentComponent = null;
277
+ currentCode = null;
278
+ renderTree(currentOpts);
279
+ return;
280
+ }
281
+ try {
282
+ currentComponent = await evaluate(code);
283
+ currentCode = code;
284
+ renderTree(currentOpts);
285
+ }
286
+ catch (err) {
287
+ const e = err instanceof Error ? err : new Error(String(err));
288
+ // Fail-loud on eval errors. The OPTIONAL `onError` callback is
289
+ // not enough on its own — bootSelfContained doesn't wire one,
290
+ // so without this log a self-contained mount with a broken
291
+ // component renders a blank iframe and emits nothing.
292
+ // eslint-disable-next-line no-console -- operator-visible failure signal
293
+ console.error('[ggui] mountReactRoot: component evaluation failed —', e);
294
+ currentComponent = null;
295
+ currentCode = null;
296
+ renderTree(currentOpts);
297
+ currentOpts.onError?.(e);
298
+ }
299
+ }
300
+ await initialEvaluate();
301
+ return {
302
+ get currentCode() {
303
+ return currentCode;
304
+ },
305
+ async update(next) {
306
+ const prevCode = currentCode;
307
+ currentOpts = next;
308
+ const nextCode = next.stackItem.componentCode ?? '';
309
+ // Props-only change — skip module eval. Re-render with new props.
310
+ if (nextCode === prevCode && currentComponent !== null) {
311
+ renderTree(currentOpts);
312
+ return;
313
+ }
314
+ if (nextCode.trim().length === 0) {
315
+ currentComponent = null;
316
+ currentCode = null;
317
+ renderTree(currentOpts);
318
+ return;
319
+ }
320
+ try {
321
+ currentComponent = await evaluate(nextCode);
322
+ currentCode = nextCode;
323
+ renderTree(currentOpts);
324
+ }
325
+ catch (err) {
326
+ const e = err instanceof Error ? err : new Error(String(err));
327
+ currentComponent = null;
328
+ currentCode = null;
329
+ renderTree(currentOpts);
330
+ next.onError?.(e);
331
+ }
332
+ },
333
+ unmount() {
334
+ root.unmount();
335
+ container.replaceChildren();
336
+ },
337
+ };
338
+ }
339
+ // Re-export React so downstream renderer modules can import it via
340
+ // the same module specifier as the mount consumer without a separate
341
+ // `react` import (keeps the dependency graph flat inside the renderer
342
+ // package).
343
+ export { React };
@@ -0,0 +1,102 @@
1
+ import type { AckPayload, ContractErrorPayload } from '@ggui-ai/protocol';
2
+ import type { ConnectionStatus } from '@ggui-ai/protocol/transport/websocket';
3
+ import type { GguiBootstrapMeta } from '@ggui-ai/protocol/integrations/mcp-apps';
4
+ import type { ChannelRegistry, WsTransportHandle } from '@ggui-ai/channel-client';
5
+ import { type ProtocolErrorEmitter } from './protocol-error.js';
6
+ import type { ObservabilityEmitter } from './observability.js';
7
+ /**
8
+ * Result of a successful handshake — the live typed transport handle
9
+ * plus the resolved `AckPayload` from the first ack frame. The caller
10
+ * attaches `handle` to the triad's outbound send surface (via the
11
+ * BufferedManagerShim flush) and applies `ack.stack` to the stack
12
+ * model.
13
+ */
14
+ export interface RegistrySubscribeHandle {
15
+ readonly handle: WsTransportHandle;
16
+ readonly ack: AckPayload;
17
+ }
18
+ /**
19
+ * Caller-provided callbacks. The runtime's `bootSequence` owns the
20
+ * concrete implementations; tests pass mocks via the `connectFn` seam.
21
+ */
22
+ export interface ConnectViaRegistryOptions {
23
+ readonly bootstrap: GguiBootstrapMeta;
24
+ readonly registry: ChannelRegistry;
25
+ /**
26
+ * Fires on every transport-status transition mapped onto the
27
+ * protocol-level `ConnectionStatus` axis. The mapping is:
28
+ *
29
+ * - First `'connecting'` after construction → `'connecting'`.
30
+ * - First `'open'` → `'connected'`.
31
+ * - `'closed'` after a prior `'open'` (drop) → `'disconnected'`.
32
+ * - `'connecting'` after a prior `'open'` (reconnect attempt) →
33
+ * `'reconnecting'`.
34
+ * - `'failed'` → `'disconnected'` (terminal — the reconnect ladder
35
+ * gave up). Also rejects the handshake promise pre-ack.
36
+ *
37
+ * The mapping is load-bearing for the renderer's status DOM logic
38
+ * (which already keyed off `ConnectionStatus`) and the
39
+ * `subscribe-failed` observability emission rule (fires on
40
+ * `'reconnecting'` only).
41
+ */
42
+ readonly onStatusChange: (status: ConnectionStatus) => void;
43
+ /**
44
+ * Optional typed {@link ProtocolError} sink. Pre-ack `error`
45
+ * frames are classified through this emitter in parallel with the
46
+ * throw/reject path.
47
+ */
48
+ readonly onProtocolError?: ProtocolErrorEmitter;
49
+ /**
50
+ * Optional {@link ObservabilityEmitter} sink. Two events flow:
51
+ *
52
+ * - `schema-version-mismatch` — on every UPGRADE_REQUIRED
53
+ * classification (client-side ack-mismatch + server-emitted
54
+ * error frame).
55
+ * - `subscribe-failed` — on non-terminal transport status
56
+ * transitions to `'reconnecting'`. Terminal failures route
57
+ * through the ProtocolError path only.
58
+ */
59
+ readonly onObserve?: ObservabilityEmitter;
60
+ /**
61
+ * Optional post-handshake ack callback. Fires for every ack frame
62
+ * AFTER the initial handshake settled — i.e. on every reconnect's
63
+ * subscribe ack. The runtime uses this to reapply the server's
64
+ * authoritative `stack` snapshot so a push or update that landed
65
+ * during a WS dropout window flows back into the iframe on
66
+ * reconnect.
67
+ *
68
+ * Idempotent at the runtime layer: `stackModel.setAll(ack.stack)`
69
+ * is a snapshot replace. Channels re-subscribed by
70
+ * `ChannelTransportRouter.onWsStatusChange('connected')` upstream
71
+ * of this callback, so the per-channel transport state is already
72
+ * coherent by the time the resubscribe-ack arrives.
73
+ */
74
+ readonly onResubscribeAck?: (ack: AckPayload) => void;
75
+ }
76
+ /**
77
+ * Open the WS via the registry, send subscribe (composed by the
78
+ * registry's `subscribeFrameBuilder`), await the first `ack`. Resolves
79
+ * with `{ handle, ack }` on success; rejects with
80
+ * `UpgradeRequiredError` on a version-handshake failure (either side);
81
+ * rejects with a plain `Error` on any other server-emitted pre-ack
82
+ * error.
83
+ *
84
+ * Once the promise settles, subsequent server frames flow through the
85
+ * registered channel handlers (props_update, drain_ack, push, data,
86
+ * etc.) — there is no longer a separate `onMessage` callback fanning
87
+ * frames across multiple handler chains.
88
+ */
89
+ export declare function connectViaRegistry(opts: ConnectViaRegistryOptions): Promise<RegistrySubscribeHandle>;
90
+ /**
91
+ * Default `connectFn` seam — tests can swap this for a fake to drive
92
+ * the handshake without a real WebSocket. The renderer's `bootSequence`
93
+ * accepts an optional override via the `BootSequenceOptions.connectFn`
94
+ * field; the default below points back at this module's
95
+ * `connectViaRegistry` export.
96
+ */
97
+ export type ConnectFn = typeof connectViaRegistry;
98
+ /**
99
+ * Type re-export for callers that want to thread the handle type
100
+ * through their own seams (e.g. `bootSequence`'s shim attach).
101
+ */
102
+ export type { WsTransportHandle, ContractErrorPayload };