@marimo-team/islands 0.23.14-dev6 → 0.23.14-dev61
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/dist/{ConnectedDataExplorerComponent-Du3_nUzI.js → ConnectedDataExplorerComponent-CU4fZJzG.js} +4 -4
- package/dist/assets/__vite-browser-external-BQCLNwri.js +1 -0
- package/dist/assets/{worker-DEDLIQQV.js → worker-DAWRHcPq.js} +2 -2
- package/dist/{chat-ui-BZxLHwyD.js → chat-ui-DinOvbWu.js} +3248 -3044
- package/dist/{click-outside-container-BDd67_1U.js → click-outside-container-BLPjMamz.js} +141 -113
- package/dist/{code-visibility-C90Am97q.js → code-visibility-EvXXzjfW.js} +1169 -960
- package/dist/data-grid-overlay-editor-C6lxUJp-.js +136 -0
- package/dist/{dist-D_bzzWBm.js → dist-Bf9f8MuT.js} +3 -3
- package/dist/{formats-d6MhLuQ9.js → formats-Dzx4J_z1.js} +1 -1
- package/dist/{glide-data-editor-DkzAInWG.js → glide-data-editor-CjTu7ukN.js} +2084 -1889
- package/dist/{html-to-image-CGp_08St.js → html-to-image-_wGfk8V-.js} +2389 -2322
- package/dist/{input-CbEz_aj_.js → input-DtsN7xm-.js} +1 -1
- package/dist/main.js +3630 -1801
- package/dist/{mermaid-CJW9vIyO.js → mermaid-BYqXy_NE.js} +2 -2
- package/dist/{number-overlay-editor-D-a0qCT8.js → number-overlay-editor-BLJXvX9c.js} +1 -1
- package/dist/{process-output-R6JsYrv3.js → process-output-Mh4UrjwM.js} +1 -1
- package/dist/{reveal-component-C_u9FY4_.js → reveal-component-CKfV5wlk.js} +600 -596
- package/dist/{spec-Bv-XlYiv.js → spec-Cz-Bj1JI.js} +1 -1
- package/dist/style.css +1 -1
- package/dist/{toDate-D-l5s8nn.js → toDate-CWNNlFEX.js} +15 -7
- package/dist/{useAsyncData-1Dhzjfwf.js → useAsyncData-KfHB8wQR.js} +1 -1
- package/dist/{useDeepCompareMemoize-CDWT3BDz.js → useDeepCompareMemoize-nJMtxhm4.js} +1 -1
- package/dist/{useLifecycle-AHlswLw-.js → useLifecycle-DegSo0lV.js} +1 -1
- package/dist/{useTheme-BrYvK-_A.js → useTheme-6eZ3GOTS.js} +31 -26
- package/dist/{vega-component-Pk6lyc_a.js → vega-component-DzyyM9fc.js} +5 -5
- package/package.json +4 -4
- package/src/__mocks__/requests.ts +1 -0
- package/src/__tests__/CellStatus.test.tsx +1 -2
- package/src/components/app-config/user-config-form.tsx +52 -0
- package/src/components/chat/acp/agent-panel.tsx +35 -1
- package/src/components/chat/chat-panel.tsx +68 -29
- package/src/components/data-table/__tests__/column-explorer.test.tsx +25 -0
- package/src/components/data-table/__tests__/column-visibility-dropdown.test.tsx +60 -3
- package/src/components/data-table/charts/__tests__/altair-generator.test.ts +24 -0
- package/src/components/data-table/charts/__tests__/merge-index-fields.test.ts +33 -0
- package/src/components/data-table/charts/chart-spec/altair-generator.ts +4 -1
- package/src/components/data-table/charts/charts.tsx +23 -1
- package/src/components/data-table/column-explorer-panel/column-explorer.tsx +33 -12
- package/src/components/data-table/column-visibility-dropdown.tsx +15 -0
- package/src/components/debugger/debugger-code.tsx +7 -2
- package/src/components/editor/actions/useNotebookActions.tsx +2 -2
- package/src/components/editor/cell/CellStatus.tsx +1 -20
- package/src/components/editor/cell/PendingDeleteConfirmation.tsx +1 -1
- package/src/components/editor/cell/cell-context-menu.tsx +2 -2
- package/src/components/editor/chrome/panels/snippets-panel.tsx +3 -3
- package/src/components/editor/chrome/wrapper/__tests__/useOpenAiAssistant.test.ts +36 -0
- package/src/components/editor/chrome/wrapper/footer-items/pyodide-status.tsx +6 -36
- package/src/components/editor/chrome/wrapper/useAiPanel.ts +3 -1
- package/src/components/editor/chrome/wrapper/useOpenAiAssistant.ts +88 -0
- package/src/components/editor/code/__tests__/readonly-python-code.test.tsx +79 -0
- package/src/components/editor/code/readonly-python-code.tsx +60 -33
- package/src/components/editor/errors/__tests__/auto-fix.test.ts +23 -0
- package/src/components/editor/errors/auto-fix.tsx +88 -34
- package/src/components/editor/errors/fix-mode.ts +1 -1
- package/src/components/editor/notebook-cell.tsx +7 -0
- package/src/components/editor/output/ImageOutput.tsx +20 -6
- package/src/components/editor/output/MarimoTracebackOutput.tsx +28 -2
- package/src/components/editor/output/__tests__/ImageOutput.test.tsx +53 -0
- package/src/components/editor/output/__tests__/traceback.test.tsx +14 -6
- package/src/components/editor/renderers/grid-layout/grid-layout.tsx +7 -2
- package/src/components/editor/renderers/vertical-layout/vertical-layout.tsx +7 -3
- package/src/components/lifecycle/ProgressiveBoundary.tsx +42 -0
- package/src/components/lifecycle/RuntimeStatusBadge.tsx +59 -0
- package/src/components/lifecycle/__tests__/ProgressiveBoundary.test.tsx +147 -0
- package/src/components/lifecycle/__tests__/RuntimeStatusBadge.test.tsx +72 -0
- package/src/components/slides/__tests__/slide.test.tsx +33 -0
- package/src/components/slides/minimap.tsx +7 -1
- package/src/components/slides/reveal-component.tsx +3 -0
- package/src/components/slides/slide-cell-view.tsx +10 -6
- package/src/components/slides/slide.tsx +16 -13
- package/src/components/tracing/tracing.tsx +2 -1
- package/src/core/ai/config.ts +2 -2
- package/src/core/ai/context/__tests__/registry.test.ts +133 -3
- package/src/core/ai/context/providers/__tests__/datasource.test.ts +3 -1
- package/src/core/ai/context/providers/__tests__/error.test.ts +200 -15
- package/src/core/ai/context/providers/datasource.ts +27 -2
- package/src/core/ai/context/providers/error.ts +226 -36
- package/src/core/ai/context/registry.ts +77 -43
- package/src/core/ai/state.ts +11 -0
- package/src/core/cells/__tests__/cell.test.ts +39 -0
- package/src/core/cells/__tests__/readonly-code-display.test.ts +46 -0
- package/src/core/cells/cell.ts +5 -3
- package/src/core/cells/readonly-code-display.ts +35 -0
- package/src/core/codemirror/__tests__/setup.test.ts +33 -1
- package/src/core/codemirror/ai/resources.ts +15 -10
- package/src/core/codemirror/cells/__tests__/debugger-decorations.test.ts +185 -0
- package/src/core/codemirror/cells/__tests__/debugger-state.test.ts +224 -0
- package/src/core/codemirror/cells/__tests__/line-timing-decorations.test.ts +126 -0
- package/src/core/codemirror/cells/debugger-decorations.ts +188 -0
- package/src/core/codemirror/cells/debugger-state.ts +133 -0
- package/src/core/codemirror/cells/extensions.ts +32 -4
- package/src/core/codemirror/cells/line-timing-decorations.ts +165 -0
- package/src/core/codemirror/go-to-definition/__tests__/utils.test.ts +75 -3
- package/src/core/codemirror/go-to-definition/extension.ts +3 -3
- package/src/core/codemirror/go-to-definition/underline.ts +5 -14
- package/src/core/codemirror/go-to-definition/utils.ts +49 -1
- package/src/core/codemirror/utils.ts +15 -0
- package/src/core/config/__tests__/config-schema.test.ts +17 -0
- package/src/core/config/config-schema.ts +1 -0
- package/src/core/config/config.ts +10 -0
- package/src/core/config/feature-flag.tsx +4 -0
- package/src/core/edit-app.tsx +8 -5
- package/src/core/errors/errors.ts +2 -1
- package/src/core/islands/bootstrap.ts +4 -3
- package/src/core/islands/bridge.ts +1 -0
- package/src/core/lifecycle/__tests__/render-policy.test.ts +247 -0
- package/src/core/lifecycle/render-policy.ts +125 -0
- package/src/core/mime.ts +11 -4
- package/src/core/network/__tests__/api.test.ts +17 -0
- package/src/core/network/__tests__/requests-toasting.test.tsx +46 -0
- package/src/core/network/api.ts +17 -6
- package/src/core/network/requests-lazy.ts +1 -0
- package/src/core/network/requests-network.ts +8 -0
- package/src/core/network/requests-static.ts +1 -0
- package/src/core/network/requests-toasting.tsx +10 -1
- package/src/core/network/types.ts +2 -0
- package/src/core/run-app.tsx +27 -23
- package/src/core/runtime/__tests__/adapter.test.ts +160 -0
- package/src/core/runtime/__tests__/runtime.test.ts +45 -0
- package/src/core/runtime/adapter.ts +182 -0
- package/src/core/runtime/runtime.ts +30 -16
- package/src/core/wasm/PyodideLoader.tsx +20 -62
- package/src/core/wasm/bridge.ts +13 -4
- package/src/core/wasm/state.ts +8 -19
- package/src/core/websocket/__tests__/useMarimoKernelConnection.test.ts +4 -0
- package/src/core/websocket/__tests__/useWebSocket.test.ts +49 -0
- package/src/core/websocket/transports/__tests__/sse.test.ts +338 -0
- package/src/core/websocket/transports/sse.ts +308 -0
- package/src/core/websocket/useMarimoKernelConnection.tsx +58 -7
- package/src/core/websocket/useWebSocket.tsx +12 -2
- package/src/css/app/codemirror.css +14 -0
- package/src/hooks/__tests__/useDelayElapsed.test.tsx +55 -0
- package/src/hooks/useDelayElapsed.ts +27 -0
- package/src/mount.tsx +3 -3
- package/src/plugins/core/RenderHTML.tsx +35 -1
- package/src/plugins/core/__test__/RenderHTML.test.ts +44 -0
- package/src/plugins/impl/DataTablePlugin.tsx +1 -0
- package/src/plugins/impl/anywidget/AnyWidgetPlugin.tsx +33 -254
- package/src/plugins/impl/anywidget/__tests__/AnyWidgetPlugin.test.tsx +116 -174
- package/src/plugins/impl/anywidget/__tests__/host.test.ts +313 -0
- package/src/plugins/impl/anywidget/__tests__/model-proxy.test.ts +158 -0
- package/src/plugins/impl/anywidget/__tests__/model.test.ts +8 -177
- package/src/plugins/impl/anywidget/__tests__/registry.test.ts +708 -0
- package/src/plugins/impl/anywidget/__tests__/resolve-widget.test.ts +132 -0
- package/src/plugins/impl/anywidget/__tests__/widget-binding.test.ts +305 -133
- package/src/plugins/impl/anywidget/__tests__/widget-ref.test.ts +28 -0
- package/src/plugins/impl/anywidget/host.ts +54 -0
- package/src/plugins/impl/anywidget/model-proxy.ts +70 -0
- package/src/plugins/impl/anywidget/model.ts +59 -239
- package/src/plugins/impl/anywidget/registry.ts +268 -0
- package/src/plugins/impl/anywidget/resolve-widget.ts +138 -0
- package/src/plugins/impl/anywidget/runtime.ts +422 -0
- package/src/plugins/impl/anywidget/types.ts +14 -0
- package/src/plugins/impl/anywidget/widget-binding.ts +280 -119
- package/src/plugins/impl/anywidget/widget-ref.ts +29 -0
- package/src/plugins/impl/data-editor/__tests__/glide-data-editor.test.tsx +187 -0
- package/src/plugins/impl/data-editor/glide-data-editor.tsx +6 -0
- package/src/plugins/impl/data-editor/glide-portal.tsx +73 -0
- package/src/plugins/impl/mpl-interactive/MplInteractivePlugin.tsx +3 -2
- package/src/plugins/impl/mpl-interactive/__tests__/MplInteractivePlugin.test.tsx +16 -13
- package/src/plugins/impl/mpl-interactive/mpl-websocket-shim.ts +1 -1
- package/src/utils/errors.ts +15 -0
- package/src/utils/time.ts +20 -0
- package/dist/assets/__vite-browser-external-DAm_YW43.js +0 -1
- package/dist/data-grid-overlay-editor-mfEJ5475.js +0 -128
- package/src/components/editor/chrome/panels/__tests__/snippet-display.test.ts +0 -22
- package/src/components/editor/chrome/panels/snippet-display.ts +0 -27
- package/src/core/ai/context/providers/__tests__/__snapshots__/error.test.ts.snap +0 -3
- package/src/core/wasm/__tests__/PyodideLoader.test.ts +0 -72
- package/src/core/wasm/__tests__/state.test.ts +0 -124
- package/src/plugins/impl/anywidget/schemas.ts +0 -32
|
@@ -1,14 +1,19 @@
|
|
|
1
1
|
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
2
|
+
import type {
|
|
3
|
+
AnyWidget,
|
|
4
|
+
Experimental,
|
|
5
|
+
Initialize,
|
|
6
|
+
Render,
|
|
7
|
+
} from "@anywidget/types";
|
|
5
8
|
import { asRemoteURL } from "@/core/runtime/config";
|
|
6
9
|
import { resolveVirtualFileURL } from "@/core/static/files";
|
|
7
10
|
import { isStaticNotebook } from "@/core/static/static-state";
|
|
8
11
|
import { isTrustedVirtualFileUrl } from "@/plugins/core/trusted-url";
|
|
9
12
|
import { Logger } from "@/utils/Logger";
|
|
13
|
+
import type { Host } from "./host";
|
|
10
14
|
import type { Model } from "./model";
|
|
11
|
-
import
|
|
15
|
+
import { modelProxy, type ProxyRegistration } from "./model-proxy";
|
|
16
|
+
import type { ModelState } from "./types";
|
|
12
17
|
|
|
13
18
|
export const experimental: Experimental = {
|
|
14
19
|
invoke: async () => {
|
|
@@ -19,8 +24,6 @@ export const experimental: Experimental = {
|
|
|
19
24
|
},
|
|
20
25
|
};
|
|
21
26
|
|
|
22
|
-
export type RenderFn = (el: HTMLElement, signal: AbortSignal) => Promise<void>;
|
|
23
|
-
|
|
24
27
|
/**
|
|
25
28
|
* Polyfill for AbortSignal.any. Returns a signal that aborts when any of the
|
|
26
29
|
* input signals abort. This can be removed once the Node.js test environment
|
|
@@ -48,19 +51,26 @@ function abortSignalAny(signals: AbortSignal[]): AbortSignal {
|
|
|
48
51
|
* A single import is shared across all widget instances using the same module.
|
|
49
52
|
*/
|
|
50
53
|
class WidgetDefRegistry {
|
|
51
|
-
#cache = new Map<string, Promise<
|
|
54
|
+
#cache = new Map<string, Promise<unknown>>();
|
|
52
55
|
|
|
53
56
|
/**
|
|
54
|
-
* Get (or start) the ESM import for a widget module
|
|
55
|
-
*
|
|
57
|
+
* Get (or start) the ESM import for a widget module, cached by
|
|
58
|
+
* jsHash so multiple instances share one import. Pass
|
|
59
|
+
* `kernelAuthored: true` only for URLs from an `EsmSpec`; it widens
|
|
60
|
+
* the import gate to remote and data URLs.
|
|
56
61
|
*/
|
|
57
|
-
getModule(
|
|
62
|
+
getModule(options: {
|
|
63
|
+
jsUrl: string;
|
|
64
|
+
jsHash: string;
|
|
65
|
+
kernelAuthored?: boolean;
|
|
66
|
+
}): Promise<unknown> {
|
|
67
|
+
const { jsUrl, jsHash, kernelAuthored = false } = options;
|
|
58
68
|
const cached = this.#cache.get(jsHash);
|
|
59
69
|
if (cached) {
|
|
60
70
|
return cached;
|
|
61
71
|
}
|
|
62
72
|
|
|
63
|
-
const promise = this.#doImport(jsUrl).catch((error) => {
|
|
73
|
+
const promise = this.#doImport(jsUrl, { kernelAuthored }).catch((error) => {
|
|
64
74
|
// On failure, remove from cache so a retry with a new URL can work
|
|
65
75
|
this.#cache.delete(jsHash);
|
|
66
76
|
throw error;
|
|
@@ -70,23 +80,19 @@ class WidgetDefRegistry {
|
|
|
70
80
|
return promise;
|
|
71
81
|
}
|
|
72
82
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
)
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
// embedded in a markdown cell dynamically import attacker-controlled
|
|
87
|
-
// JavaScript at same origin (the HTML sanitizer allows any marimo-*
|
|
88
|
-
// custom element with any attribute through to the plugin layer).
|
|
89
|
-
if (!isTrustedVirtualFileUrl(jsUrl)) {
|
|
83
|
+
async #doImport(
|
|
84
|
+
jsUrl: string,
|
|
85
|
+
opts: { kernelAuthored?: boolean } = {},
|
|
86
|
+
): Promise<unknown> {
|
|
87
|
+
// By default, only trust marimo virtual file paths: arbitrary URLs
|
|
88
|
+
// would let markdown-injected elements import attacker-controlled
|
|
89
|
+
// JavaScript. Kernel-authored URLs (from an `EsmSpec`) are exempt —
|
|
90
|
+
// a client cannot forge a notification to its peers. data: URLs
|
|
91
|
+
// cover runtimes where virtual files are unsupported.
|
|
92
|
+
const trusted =
|
|
93
|
+
isTrustedVirtualFileUrl(jsUrl) ||
|
|
94
|
+
(opts.kernelAuthored === true && /^(https?:|data:)/.test(jsUrl));
|
|
95
|
+
if (!trusted) {
|
|
90
96
|
throw new Error(
|
|
91
97
|
`Refusing to load anywidget module from untrusted URL: ${String(
|
|
92
98
|
jsUrl,
|
|
@@ -102,128 +108,283 @@ class WidgetDefRegistry {
|
|
|
102
108
|
}
|
|
103
109
|
|
|
104
110
|
/**
|
|
105
|
-
*
|
|
106
|
-
*
|
|
111
|
+
* Resolved widget def — what `initialize` and `render` are actually called
|
|
112
|
+
* on. `AnyWidget` is either this shape directly or a factory that returns it.
|
|
113
|
+
*/
|
|
114
|
+
interface ResolvedWidget<T extends ModelState> {
|
|
115
|
+
initialize?: Initialize<T>;
|
|
116
|
+
render?: Render<T>;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
type HostFactory = (signal: AbortSignal) => Host;
|
|
120
|
+
type Cleanup = () => void | PromiseLike<void>;
|
|
121
|
+
|
|
122
|
+
function isCleanup(value: unknown): value is Cleanup {
|
|
123
|
+
return typeof value === "function";
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
interface CreateWidgetBindingOptions<T extends ModelState> {
|
|
127
|
+
widgetDef: AnyWidget<T>;
|
|
128
|
+
model: Model<T>;
|
|
129
|
+
createHost: HostFactory;
|
|
130
|
+
controller?: AbortController;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
async function safelyRunCleanup(cleanup: Cleanup, reason: string) {
|
|
134
|
+
try {
|
|
135
|
+
await cleanup();
|
|
136
|
+
} catch (error) {
|
|
137
|
+
Logger.warn(`[WidgetBinding] ${reason} cleanup failed`, error);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* One immutable generation of a widget: a resolved AnyWidget definition
|
|
143
|
+
* paired with a Model. Constructed by `WidgetBinding.create` and never
|
|
144
|
+
* rebinds; a code change replaces the whole generation.
|
|
107
145
|
*
|
|
108
146
|
* Per AFM spec:
|
|
109
|
-
* - initialize()
|
|
110
|
-
* - render()
|
|
147
|
+
* - initialize() ran exactly once, in `create`
|
|
148
|
+
* - render() runs once per view (zero or more per generation)
|
|
111
149
|
*/
|
|
112
|
-
class WidgetBinding<T extends ModelState = ModelState> {
|
|
113
|
-
#controller: AbortController
|
|
114
|
-
#
|
|
115
|
-
#
|
|
150
|
+
export class WidgetBinding<T extends ModelState = ModelState> {
|
|
151
|
+
#controller: AbortController;
|
|
152
|
+
#widget: ResolvedWidget<T>;
|
|
153
|
+
#model: Model<T>;
|
|
154
|
+
#exports: unknown;
|
|
155
|
+
#createHost: HostFactory;
|
|
156
|
+
#cleanupTasks = new Set<Promise<void>>();
|
|
157
|
+
#viewTasks = new Set<Promise<void>>();
|
|
158
|
+
|
|
159
|
+
private constructor(options: {
|
|
160
|
+
widget: ResolvedWidget<T>;
|
|
161
|
+
model: Model<T>;
|
|
162
|
+
exports: unknown;
|
|
163
|
+
controller: AbortController;
|
|
164
|
+
createHost: HostFactory;
|
|
165
|
+
}) {
|
|
166
|
+
this.#widget = options.widget;
|
|
167
|
+
this.#model = options.model;
|
|
168
|
+
this.#exports = options.exports;
|
|
169
|
+
this.#controller = options.controller;
|
|
170
|
+
this.#createHost = options.createHost;
|
|
171
|
+
}
|
|
116
172
|
|
|
117
173
|
/**
|
|
118
|
-
*
|
|
119
|
-
*
|
|
120
|
-
*
|
|
121
|
-
* and
|
|
174
|
+
* Resolve the widget definition (calling it if it is a factory) and
|
|
175
|
+
* run `initialize` exactly once, with its listeners scoped to
|
|
176
|
+
* `controller`'s signal. Aborting the controller mid-initialize
|
|
177
|
+
* still runs any legacy cleanup callback, and `create` rejects.
|
|
122
178
|
*/
|
|
123
|
-
async
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
179
|
+
static async create<T extends ModelState>({
|
|
180
|
+
widgetDef,
|
|
181
|
+
model,
|
|
182
|
+
createHost,
|
|
183
|
+
controller = new AbortController(),
|
|
184
|
+
}: CreateWidgetBindingOptions<T>): Promise<WidgetBinding<T>> {
|
|
185
|
+
const signal = controller.signal;
|
|
186
|
+
const widget: ResolvedWidget<T> =
|
|
187
|
+
typeof widgetDef === "function" ? await widgetDef() : widgetDef;
|
|
128
188
|
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
189
|
+
const initPromise = Promise.resolve(
|
|
190
|
+
widget.initialize?.({
|
|
191
|
+
model: modelProxy(model, signal),
|
|
192
|
+
experimental,
|
|
193
|
+
signal,
|
|
194
|
+
}),
|
|
195
|
+
);
|
|
196
|
+
// If destroyed mid-initialize, still run a late-arriving legacy
|
|
197
|
+
// cleanup callback: the widget acquired resources.
|
|
198
|
+
initPromise
|
|
199
|
+
.then(async (settled) => {
|
|
200
|
+
if (signal.aborted && isCleanup(settled)) {
|
|
201
|
+
await safelyRunCleanup(settled, "late initialize");
|
|
202
|
+
}
|
|
203
|
+
})
|
|
204
|
+
.catch(() => undefined);
|
|
138
205
|
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
const
|
|
206
|
+
// Race against destruction so a hung `initialize` can't strand
|
|
207
|
+
// callers whose model already closed.
|
|
208
|
+
const result = await Promise.race([
|
|
209
|
+
initPromise,
|
|
210
|
+
new Promise<never>((_, reject) => {
|
|
211
|
+
const abort = () => reject(new Error("[anywidget] binding destroyed"));
|
|
212
|
+
if (signal.aborted) {
|
|
213
|
+
abort();
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
signal.addEventListener("abort", abort, { once: true });
|
|
217
|
+
}),
|
|
218
|
+
]);
|
|
142
219
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
220
|
+
if (signal.aborted) {
|
|
221
|
+
throw new Error("[anywidget] binding destroyed");
|
|
222
|
+
}
|
|
146
223
|
|
|
147
|
-
//
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
224
|
+
// Distinguish anywidget's three return shapes:
|
|
225
|
+
// function → legacy cleanup callback (run on destroy)
|
|
226
|
+
// object → widget exports (anywidget>=0.11)
|
|
227
|
+
// void → nothing
|
|
228
|
+
let exports: unknown;
|
|
229
|
+
let initializeCleanup: Cleanup | undefined;
|
|
230
|
+
if (isCleanup(result)) {
|
|
231
|
+
initializeCleanup = result;
|
|
232
|
+
exports = undefined;
|
|
233
|
+
} else if (typeof result === "object" && result !== null) {
|
|
234
|
+
exports = result;
|
|
235
|
+
} else {
|
|
236
|
+
exports = undefined;
|
|
151
237
|
}
|
|
152
238
|
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
);
|
|
170
|
-
renderCleanup();
|
|
171
|
-
});
|
|
172
|
-
}
|
|
173
|
-
};
|
|
239
|
+
const binding = new WidgetBinding({
|
|
240
|
+
widget,
|
|
241
|
+
model,
|
|
242
|
+
exports,
|
|
243
|
+
controller,
|
|
244
|
+
createHost,
|
|
245
|
+
});
|
|
246
|
+
if (initializeCleanup) {
|
|
247
|
+
signal.addEventListener(
|
|
248
|
+
"abort",
|
|
249
|
+
() => void binding.#trackCleanup(initializeCleanup, "initialize"),
|
|
250
|
+
{ once: true },
|
|
251
|
+
);
|
|
252
|
+
}
|
|
253
|
+
return binding;
|
|
254
|
+
}
|
|
174
255
|
|
|
175
|
-
|
|
256
|
+
/**
|
|
257
|
+
* The object returned from `initialize`, or `undefined` if it
|
|
258
|
+
* returned a cleanup function or nothing.
|
|
259
|
+
*/
|
|
260
|
+
get exports(): unknown {
|
|
261
|
+
return this.#exports;
|
|
176
262
|
}
|
|
177
263
|
|
|
178
264
|
/**
|
|
179
|
-
*
|
|
265
|
+
* Mount a view of this widget into `el`. Each view's signal aborts
|
|
266
|
+
* when the caller's `signal` fires or the binding is destroyed, and
|
|
267
|
+
* listeners registered inside `render` auto-clear on abort.
|
|
268
|
+
*
|
|
269
|
+
* Hydration guarantee: listeners attached during `render` observe
|
|
270
|
+
* current model state exactly once, after `render` settles. Scoped
|
|
271
|
+
* to this view's listeners; re-firing at other views double-paints.
|
|
180
272
|
*/
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
this.#
|
|
186
|
-
this.#
|
|
187
|
-
|
|
188
|
-
|
|
273
|
+
async createView(
|
|
274
|
+
target: { el: HTMLElement },
|
|
275
|
+
options: { signal: AbortSignal },
|
|
276
|
+
): Promise<void> {
|
|
277
|
+
const task = this.#createView(target, options);
|
|
278
|
+
this.#viewTasks.add(task);
|
|
279
|
+
try {
|
|
280
|
+
await task;
|
|
281
|
+
} finally {
|
|
282
|
+
this.#viewTasks.delete(task);
|
|
283
|
+
}
|
|
189
284
|
}
|
|
190
|
-
}
|
|
191
285
|
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
getOrCreate(modelId: WidgetModelId): WidgetBinding<any> {
|
|
200
|
-
let binding = this.#bindings.get(modelId);
|
|
201
|
-
if (!binding) {
|
|
202
|
-
binding = new WidgetBinding();
|
|
203
|
-
this.#bindings.set(modelId, binding);
|
|
286
|
+
async #createView(
|
|
287
|
+
target: { el: HTMLElement },
|
|
288
|
+
options: { signal: AbortSignal },
|
|
289
|
+
): Promise<void> {
|
|
290
|
+
const widget = this.#widget;
|
|
291
|
+
if (!widget.render) {
|
|
292
|
+
return;
|
|
204
293
|
}
|
|
205
|
-
|
|
294
|
+
// `renderSignal` aborts when either the caller's view unmounts or
|
|
295
|
+
// the binding itself is destroyed.
|
|
296
|
+
const renderSignal = abortSignalAny([
|
|
297
|
+
options.signal,
|
|
298
|
+
this.#controller.signal,
|
|
299
|
+
]);
|
|
300
|
+
if (renderSignal.aborted) {
|
|
301
|
+
return;
|
|
302
|
+
}
|
|
303
|
+
// Clear whatever a previous generation or render left behind.
|
|
304
|
+
target.el.innerHTML = "";
|
|
305
|
+
// Each view gets a host scoped to its own signal so child views tear
|
|
306
|
+
// down with this view.
|
|
307
|
+
const host = this.#createHost(renderSignal);
|
|
308
|
+
const registrations: ProxyRegistration[] = [];
|
|
309
|
+
const renderCleanup = await widget.render({
|
|
310
|
+
model: modelProxy(this.#model, renderSignal, (registration) =>
|
|
311
|
+
registrations.push(registration),
|
|
312
|
+
),
|
|
313
|
+
el: target.el,
|
|
314
|
+
experimental,
|
|
315
|
+
signal: renderSignal,
|
|
316
|
+
host,
|
|
317
|
+
});
|
|
318
|
+
if (isCleanup(renderCleanup)) {
|
|
319
|
+
const runCleanup = () => {
|
|
320
|
+
const reason = options.signal.aborted
|
|
321
|
+
? "view unmount"
|
|
322
|
+
: "binding destroyed";
|
|
323
|
+
Logger.debug(
|
|
324
|
+
`[WidgetBinding] Render cleanup triggered (reason: ${reason})`,
|
|
325
|
+
);
|
|
326
|
+
return this.#trackCleanup(renderCleanup, "render");
|
|
327
|
+
};
|
|
328
|
+
if (renderSignal.aborted) {
|
|
329
|
+
await runCleanup();
|
|
330
|
+
return;
|
|
331
|
+
}
|
|
332
|
+
renderSignal.addEventListener("abort", () => void runCleanup(), {
|
|
333
|
+
once: true,
|
|
334
|
+
});
|
|
335
|
+
}
|
|
336
|
+
this.#replayState(registrations, renderSignal);
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
#trackCleanup(cleanup: Cleanup, reason: string): Promise<void> {
|
|
340
|
+
const task = safelyRunCleanup(cleanup, reason);
|
|
341
|
+
this.#cleanupTasks.add(task);
|
|
342
|
+
void task.finally(() => this.#cleanupTasks.delete(task));
|
|
343
|
+
return task;
|
|
206
344
|
}
|
|
207
345
|
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
346
|
+
/**
|
|
347
|
+
* The hydration guarantee documented on `createView`.
|
|
348
|
+
*/
|
|
349
|
+
#replayState(
|
|
350
|
+
registrations: readonly ProxyRegistration[],
|
|
351
|
+
renderSignal: AbortSignal,
|
|
352
|
+
): void {
|
|
353
|
+
const changePrefix = "change:";
|
|
354
|
+
for (const { event, callback } of registrations) {
|
|
355
|
+
if (renderSignal.aborted) {
|
|
356
|
+
return;
|
|
357
|
+
}
|
|
358
|
+
try {
|
|
359
|
+
if (event.startsWith(changePrefix)) {
|
|
360
|
+
const key = event.slice(changePrefix.length);
|
|
361
|
+
callback(this.#model.get(key));
|
|
362
|
+
} else if (event === "change") {
|
|
363
|
+
callback();
|
|
364
|
+
}
|
|
365
|
+
} catch (error) {
|
|
366
|
+
Logger.error("[WidgetBinding] Error replaying state", error);
|
|
367
|
+
}
|
|
214
368
|
}
|
|
215
369
|
}
|
|
216
370
|
|
|
217
|
-
|
|
218
|
-
|
|
371
|
+
/**
|
|
372
|
+
* Destroy this generation, running initialize/render cleanups and
|
|
373
|
+
* clearing listeners registered through its model proxies.
|
|
374
|
+
*/
|
|
375
|
+
async destroy(): Promise<void> {
|
|
376
|
+
Logger.debug("[WidgetBinding] Destroying binding generation");
|
|
377
|
+
this.#controller.abort();
|
|
378
|
+
await Promise.allSettled(this.#viewTasks);
|
|
379
|
+
while (this.#cleanupTasks.size > 0) {
|
|
380
|
+
await Promise.all(this.#cleanupTasks);
|
|
381
|
+
}
|
|
219
382
|
}
|
|
220
383
|
}
|
|
221
384
|
|
|
222
385
|
export const WIDGET_DEF_REGISTRY = new WidgetDefRegistry();
|
|
223
|
-
export const BINDING_MANAGER = new BindingManager();
|
|
224
386
|
|
|
225
387
|
export const visibleForTesting = {
|
|
226
388
|
WidgetDefRegistry,
|
|
227
389
|
WidgetBinding,
|
|
228
|
-
BindingManager,
|
|
229
390
|
};
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
|
+
|
|
3
|
+
import { isWidgetModelId, type WidgetModelId } from "./types";
|
|
4
|
+
|
|
5
|
+
export const WIDGET_REF_PREFIX = "anywidget:";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Parse a widget reference string into its model id.
|
|
9
|
+
*
|
|
10
|
+
* Per anywidget>=0.11, widget refs are serialized as
|
|
11
|
+
* `anywidget:<model_id>` strings embedded directly in widget state.
|
|
12
|
+
* `host.getWidget(ref)` consumes these strings.
|
|
13
|
+
*
|
|
14
|
+
* The legacy `IPY_MODEL_<id>` prefix used by ipywidgets'
|
|
15
|
+
* `widget_serialization` is intentionally NOT accepted here — widgets
|
|
16
|
+
* using that path resolve children manually via
|
|
17
|
+
* `model.widget_manager.get_model(id)` after stripping the prefix.
|
|
18
|
+
*/
|
|
19
|
+
export function parseWidgetRef(ref: unknown): WidgetModelId {
|
|
20
|
+
if (typeof ref === "string" && ref.startsWith(WIDGET_REF_PREFIX)) {
|
|
21
|
+
const modelId = ref.slice(WIDGET_REF_PREFIX.length);
|
|
22
|
+
if (isWidgetModelId(modelId)) {
|
|
23
|
+
return modelId;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
throw new Error(
|
|
27
|
+
`[anywidget] Invalid widget reference: ${JSON.stringify(ref)}`,
|
|
28
|
+
);
|
|
29
|
+
}
|