@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
|
@@ -0,0 +1,422 @@
|
|
|
1
|
+
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
|
+
import { Deferred } from "@/utils/Deferred";
|
|
3
|
+
import { Logger } from "@/utils/Logger";
|
|
4
|
+
import type { Host, ResolvedWidget } from "@anywidget/types";
|
|
5
|
+
import type { Model } from "./model";
|
|
6
|
+
import {
|
|
7
|
+
getInvalidAnyWidgetModuleError,
|
|
8
|
+
resolveAnyWidget,
|
|
9
|
+
} from "./resolve-widget";
|
|
10
|
+
import type { EsmSpec, ModelState, WidgetModelId } from "./types";
|
|
11
|
+
import { WIDGET_DEF_REGISTRY, WidgetBinding } from "./widget-binding";
|
|
12
|
+
|
|
13
|
+
interface Generation {
|
|
14
|
+
hash: string;
|
|
15
|
+
controller: AbortController;
|
|
16
|
+
promise: Promise<WidgetBinding<ModelState>>;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
interface RuntimeView {
|
|
20
|
+
el: HTMLElement;
|
|
21
|
+
signal: AbortSignal;
|
|
22
|
+
generation?: Generation;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
interface StyleMount {
|
|
26
|
+
references: number;
|
|
27
|
+
update(css: string): void;
|
|
28
|
+
dispose(): void;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
interface WidgetRuntimeOptions {
|
|
32
|
+
timeout: number;
|
|
33
|
+
isEditMode: () => boolean;
|
|
34
|
+
createHost: (signal: AbortSignal) => Host;
|
|
35
|
+
onModelTimeout: () => void;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
type StyleRoot = Document | ShadowRoot;
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* The complete browser-side lifetime of one widget model.
|
|
42
|
+
*
|
|
43
|
+
* A runtime survives ESM generation changes. It owns initialization,
|
|
44
|
+
* mounted views, composition hosts, styles, and teardown so callers never
|
|
45
|
+
* need to coordinate those concerns themselves.
|
|
46
|
+
*/
|
|
47
|
+
export class WidgetRuntime {
|
|
48
|
+
readonly #key: WidgetModelId;
|
|
49
|
+
readonly #timeout: number;
|
|
50
|
+
readonly #isEditMode: () => boolean;
|
|
51
|
+
readonly #createHost: (signal: AbortSignal) => Host;
|
|
52
|
+
readonly #onModelTimeout: () => void;
|
|
53
|
+
readonly #controller = new AbortController();
|
|
54
|
+
readonly #model = new Deferred<Model<ModelState>>();
|
|
55
|
+
readonly #views = new Set<RuntimeView>();
|
|
56
|
+
readonly #styleMounts = new Map<StyleRoot, StyleMount>();
|
|
57
|
+
#modelTimeout: ReturnType<typeof setTimeout> | undefined;
|
|
58
|
+
#esmSpec: EsmSpec | undefined;
|
|
59
|
+
#generation: Generation | undefined;
|
|
60
|
+
#generationCleanup = Promise.resolve();
|
|
61
|
+
#css = "";
|
|
62
|
+
|
|
63
|
+
constructor(key: WidgetModelId, options: WidgetRuntimeOptions) {
|
|
64
|
+
this.#key = key;
|
|
65
|
+
this.#timeout = options.timeout;
|
|
66
|
+
this.#isEditMode = options.isEditMode;
|
|
67
|
+
this.#createHost = options.createHost;
|
|
68
|
+
this.#onModelTimeout = options.onModelTimeout;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
getModel(): Promise<Model<ModelState>> {
|
|
72
|
+
if (this.#model.status === "pending" && !this.#modelTimeout) {
|
|
73
|
+
this.#modelTimeout = setTimeout(() => {
|
|
74
|
+
if (this.#model.status === "pending") {
|
|
75
|
+
this.#modelTimeout = undefined;
|
|
76
|
+
this.#model.reject(
|
|
77
|
+
new Error(`Model not found for key: ${this.#key}`),
|
|
78
|
+
);
|
|
79
|
+
this.#onModelTimeout();
|
|
80
|
+
}
|
|
81
|
+
}, this.#timeout);
|
|
82
|
+
}
|
|
83
|
+
return this.#model.promise;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
getModelSync(): Model<ModelState> | undefined {
|
|
87
|
+
return this.#model.status === "resolved" ? this.#model.value : undefined;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
createModel(factory: (signal: AbortSignal) => Model<ModelState>): void {
|
|
91
|
+
if (this.#model.status !== "pending") {
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
this.#resolveModel(factory(this.#controller.signal));
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
setModel(model: Model<ModelState>): void {
|
|
98
|
+
if (this.#model.status !== "pending") {
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
this.#resolveModel(model);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
#resolveModel(model: Model<ModelState>): void {
|
|
105
|
+
if (this.#modelTimeout) {
|
|
106
|
+
clearTimeout(this.#modelTimeout);
|
|
107
|
+
this.#modelTimeout = undefined;
|
|
108
|
+
}
|
|
109
|
+
this.#model.resolve(model);
|
|
110
|
+
const css = model.get("_css");
|
|
111
|
+
this.#css = typeof css === "string" ? css : "";
|
|
112
|
+
model.on(
|
|
113
|
+
"change:_css",
|
|
114
|
+
(value: unknown) => {
|
|
115
|
+
this.#css = typeof value === "string" ? value : "";
|
|
116
|
+
for (const mount of this.#styleMounts.values()) {
|
|
117
|
+
mount.update(this.#css);
|
|
118
|
+
}
|
|
119
|
+
},
|
|
120
|
+
{ signal: this.#controller.signal },
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
setSpec(spec: EsmSpec): void {
|
|
125
|
+
if (!this.#esmSpec) {
|
|
126
|
+
this.#esmSpec = spec;
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
// A viewer's code is immutable for the lifetime of its model. This
|
|
130
|
+
// also covers a viewer that receives an editor's update before its
|
|
131
|
+
// first view creates a generation.
|
|
132
|
+
if (!this.#isEditMode()) {
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
this.#esmSpec = spec;
|
|
136
|
+
if (this.#generation && this.#generation.hash !== spec.hash) {
|
|
137
|
+
this.#swapGeneration(spec);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
async getWidget<T = unknown>(): Promise<ResolvedWidget<T>> {
|
|
142
|
+
const binding = await this.#getCurrentBinding();
|
|
143
|
+
return {
|
|
144
|
+
exports: binding.exports as T,
|
|
145
|
+
render: async ({ el, signal }) => {
|
|
146
|
+
await this.createView({
|
|
147
|
+
el,
|
|
148
|
+
signal: signal ?? this.#controller.signal,
|
|
149
|
+
});
|
|
150
|
+
},
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
async createView(options: {
|
|
155
|
+
el: HTMLElement;
|
|
156
|
+
signal: AbortSignal;
|
|
157
|
+
}): Promise<void> {
|
|
158
|
+
if (options.signal.aborted || this.#controller.signal.aborted) {
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
const view: RuntimeView = {
|
|
162
|
+
el: options.el,
|
|
163
|
+
signal: options.signal,
|
|
164
|
+
};
|
|
165
|
+
this.#views.add(view);
|
|
166
|
+
this.#mountStyle(view);
|
|
167
|
+
options.signal.addEventListener("abort", () => this.#views.delete(view), {
|
|
168
|
+
once: true,
|
|
169
|
+
});
|
|
170
|
+
await this.#renderView(view);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
async #getCurrentBinding(): Promise<WidgetBinding<ModelState>> {
|
|
174
|
+
while (!this.#controller.signal.aborted) {
|
|
175
|
+
const model = await this.getModel();
|
|
176
|
+
let generation = this.#generation;
|
|
177
|
+
if (!generation) {
|
|
178
|
+
const spec = this.#esmSpec;
|
|
179
|
+
if (!spec) {
|
|
180
|
+
throw new Error(
|
|
181
|
+
`[anywidget] No ESM spec for model ${this.#key}: ` +
|
|
182
|
+
"nothing ever provided this widget's code",
|
|
183
|
+
);
|
|
184
|
+
}
|
|
185
|
+
generation = this.#startGeneration(spec, model);
|
|
186
|
+
}
|
|
187
|
+
try {
|
|
188
|
+
const binding = await generation.promise;
|
|
189
|
+
if (generation === this.#generation) {
|
|
190
|
+
return binding;
|
|
191
|
+
}
|
|
192
|
+
} catch (error) {
|
|
193
|
+
if (!this.#generation || generation === this.#generation) {
|
|
194
|
+
throw error;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
throw new Error("[anywidget] widget runtime destroyed");
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
async #createBinding(options: {
|
|
202
|
+
spec: EsmSpec;
|
|
203
|
+
model: Model<ModelState>;
|
|
204
|
+
controller: AbortController;
|
|
205
|
+
}): Promise<WidgetBinding<ModelState>> {
|
|
206
|
+
const { spec, model, controller } = options;
|
|
207
|
+
const mod = await WIDGET_DEF_REGISTRY.getModule({
|
|
208
|
+
jsUrl: spec.url,
|
|
209
|
+
jsHash: spec.hash,
|
|
210
|
+
kernelAuthored: true,
|
|
211
|
+
});
|
|
212
|
+
const widget = resolveAnyWidget(mod, spec.url);
|
|
213
|
+
if (!widget) {
|
|
214
|
+
throw getInvalidAnyWidgetModuleError(mod, spec.url);
|
|
215
|
+
}
|
|
216
|
+
return WidgetBinding.create({
|
|
217
|
+
widgetDef: widget,
|
|
218
|
+
model,
|
|
219
|
+
createHost: this.#createHost,
|
|
220
|
+
controller,
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
#startGeneration(spec: EsmSpec, model: Model<ModelState>): Generation {
|
|
225
|
+
const controller = new AbortController();
|
|
226
|
+
const generation: Generation = {
|
|
227
|
+
hash: spec.hash,
|
|
228
|
+
controller,
|
|
229
|
+
promise: this.#createBinding({ spec, model, controller }),
|
|
230
|
+
};
|
|
231
|
+
generation.promise.catch(() => {
|
|
232
|
+
if (this.#generation === generation) {
|
|
233
|
+
this.#generation = undefined;
|
|
234
|
+
}
|
|
235
|
+
});
|
|
236
|
+
this.#generation = generation;
|
|
237
|
+
return generation;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
#swapGeneration(spec: EsmSpec): void {
|
|
241
|
+
const previous = this.#generation;
|
|
242
|
+
if (!previous) {
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
Logger.debug(
|
|
246
|
+
`[WidgetRuntime] Hot-swapping generation for model=${this.#key}`,
|
|
247
|
+
);
|
|
248
|
+
const cleanup = this.#queueGenerationCleanup(previous);
|
|
249
|
+
const model = this.getModelSync();
|
|
250
|
+
if (!model) {
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
253
|
+
const generation = this.#startGeneration(spec, model);
|
|
254
|
+
for (const view of this.#views) {
|
|
255
|
+
view.generation = undefined;
|
|
256
|
+
void cleanup
|
|
257
|
+
.then(() => this.#renderView(view, generation))
|
|
258
|
+
.catch((error) => {
|
|
259
|
+
Logger.error("[WidgetRuntime] Error replacing widget view", error);
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
#queueGenerationCleanup(generation: Generation): Promise<void> {
|
|
265
|
+
// Abort immediately so pending initialize/render hooks see cancellation,
|
|
266
|
+
// but serialize their settled cleanup before replacement DOM is rendered.
|
|
267
|
+
generation.controller.abort();
|
|
268
|
+
const cleanup = this.#generationCleanup.then(async () => {
|
|
269
|
+
try {
|
|
270
|
+
const binding = await generation.promise;
|
|
271
|
+
await binding.destroy();
|
|
272
|
+
} catch {
|
|
273
|
+
// A generation aborted during import/initialize has no binding to
|
|
274
|
+
// settle. Late initialize cleanup is handled by WidgetBinding.create.
|
|
275
|
+
}
|
|
276
|
+
});
|
|
277
|
+
this.#generationCleanup = cleanup;
|
|
278
|
+
return cleanup;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
async #renderView(
|
|
282
|
+
view: RuntimeView,
|
|
283
|
+
requestedGeneration?: Generation,
|
|
284
|
+
): Promise<void> {
|
|
285
|
+
if (view.signal.aborted || this.#controller.signal.aborted) {
|
|
286
|
+
return;
|
|
287
|
+
}
|
|
288
|
+
const generation = requestedGeneration ?? this.#generation;
|
|
289
|
+
const binding = generation
|
|
290
|
+
? await generation.promise
|
|
291
|
+
: await this.#getCurrentBinding();
|
|
292
|
+
const current = this.#generation;
|
|
293
|
+
if (!current || current !== (generation ?? current)) {
|
|
294
|
+
return;
|
|
295
|
+
}
|
|
296
|
+
if (view.generation === current) {
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
299
|
+
view.generation = current;
|
|
300
|
+
try {
|
|
301
|
+
await binding.createView({ el: view.el }, { signal: view.signal });
|
|
302
|
+
} catch (error) {
|
|
303
|
+
if (current !== this.#generation) {
|
|
304
|
+
view.generation = undefined;
|
|
305
|
+
return;
|
|
306
|
+
}
|
|
307
|
+
throw error;
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
#mountStyle(view: RuntimeView): void {
|
|
312
|
+
const root = view.el.getRootNode();
|
|
313
|
+
// `instanceof` misses cross-realm roots (e.g. a Document
|
|
314
|
+
// Picture-in-Picture window); the guards check realm-independent nodeType.
|
|
315
|
+
if (!isDocument(root) && !isShadowRoot(root)) {
|
|
316
|
+
return;
|
|
317
|
+
}
|
|
318
|
+
let mount = this.#styleMounts.get(root);
|
|
319
|
+
if (!mount) {
|
|
320
|
+
mount = createStyleMount(root, this.#css);
|
|
321
|
+
this.#styleMounts.set(root, mount);
|
|
322
|
+
}
|
|
323
|
+
mount.references += 1;
|
|
324
|
+
let released = false;
|
|
325
|
+
const release = () => {
|
|
326
|
+
if (released) {
|
|
327
|
+
return;
|
|
328
|
+
}
|
|
329
|
+
released = true;
|
|
330
|
+
const current = this.#styleMounts.get(root);
|
|
331
|
+
if (!current) {
|
|
332
|
+
return;
|
|
333
|
+
}
|
|
334
|
+
current.references -= 1;
|
|
335
|
+
if (current.references === 0) {
|
|
336
|
+
current.dispose();
|
|
337
|
+
this.#styleMounts.delete(root);
|
|
338
|
+
}
|
|
339
|
+
};
|
|
340
|
+
view.signal.addEventListener("abort", release, { once: true });
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
dispose(): void {
|
|
344
|
+
if (this.#controller.signal.aborted) {
|
|
345
|
+
return;
|
|
346
|
+
}
|
|
347
|
+
Logger.debug(`[WidgetRuntime] Disposing model=${this.#key}`);
|
|
348
|
+
if (this.#modelTimeout) {
|
|
349
|
+
clearTimeout(this.#modelTimeout);
|
|
350
|
+
this.#modelTimeout = undefined;
|
|
351
|
+
}
|
|
352
|
+
if (this.#model.status === "pending") {
|
|
353
|
+
this.#model.promise.catch(() => undefined);
|
|
354
|
+
this.#model.reject(new Error("[anywidget] widget runtime destroyed"));
|
|
355
|
+
}
|
|
356
|
+
if (this.#generation) {
|
|
357
|
+
void this.#queueGenerationCleanup(this.#generation);
|
|
358
|
+
}
|
|
359
|
+
this.#controller.abort();
|
|
360
|
+
this.#views.clear();
|
|
361
|
+
for (const mount of this.#styleMounts.values()) {
|
|
362
|
+
mount.dispose();
|
|
363
|
+
}
|
|
364
|
+
this.#styleMounts.clear();
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
function isDocument(node: Node): node is Document {
|
|
369
|
+
return node.nodeType === Node.DOCUMENT_NODE;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
function isShadowRoot(node: Node): node is ShadowRoot {
|
|
373
|
+
return node.nodeType === Node.DOCUMENT_FRAGMENT_NODE && "host" in node;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
function createStyleMount(root: StyleRoot, initialCss: string): StyleMount {
|
|
377
|
+
// Build stylesheets in the root's own realm so they apply in a cross-realm
|
|
378
|
+
// root (e.g. a Document Picture-in-Picture window).
|
|
379
|
+
const doc = isDocument(root) ? root : (root.ownerDocument ?? document);
|
|
380
|
+
const view = doc.defaultView ?? window;
|
|
381
|
+
if (
|
|
382
|
+
"adoptedStyleSheets" in root &&
|
|
383
|
+
typeof view.CSSStyleSheet !== "undefined" &&
|
|
384
|
+
typeof view.CSSStyleSheet.prototype.replaceSync === "function"
|
|
385
|
+
) {
|
|
386
|
+
try {
|
|
387
|
+
const sheet = new view.CSSStyleSheet();
|
|
388
|
+
sheet.replaceSync(initialCss);
|
|
389
|
+
root.adoptedStyleSheets = [...root.adoptedStyleSheets, sheet];
|
|
390
|
+
return {
|
|
391
|
+
references: 0,
|
|
392
|
+
update(css) {
|
|
393
|
+
sheet.replaceSync(css);
|
|
394
|
+
},
|
|
395
|
+
dispose() {
|
|
396
|
+
root.adoptedStyleSheets = root.adoptedStyleSheets.filter(
|
|
397
|
+
(candidate) => candidate !== sheet,
|
|
398
|
+
);
|
|
399
|
+
},
|
|
400
|
+
};
|
|
401
|
+
} catch {
|
|
402
|
+
// Fall through to a style element when construction or parsing fails.
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
const style = doc.createElement("style");
|
|
407
|
+
style.textContent = initialCss;
|
|
408
|
+
if (isDocument(root)) {
|
|
409
|
+
doc.head.append(style);
|
|
410
|
+
} else {
|
|
411
|
+
root.append(style);
|
|
412
|
+
}
|
|
413
|
+
return {
|
|
414
|
+
references: 0,
|
|
415
|
+
update(css) {
|
|
416
|
+
style.textContent = css;
|
|
417
|
+
},
|
|
418
|
+
dispose() {
|
|
419
|
+
style.remove();
|
|
420
|
+
},
|
|
421
|
+
};
|
|
422
|
+
}
|
|
@@ -10,6 +10,10 @@ export type EventHandler = (...args: any[]) => void;
|
|
|
10
10
|
*/
|
|
11
11
|
export type WidgetModelId = TypedString<"WidgetModelId">;
|
|
12
12
|
|
|
13
|
+
export function isWidgetModelId(value: unknown): value is WidgetModelId {
|
|
14
|
+
return typeof value === "string" && value.length > 0;
|
|
15
|
+
}
|
|
16
|
+
|
|
13
17
|
/**
|
|
14
18
|
* AnyWidget model state with buffers.
|
|
15
19
|
*/
|
|
@@ -25,3 +29,13 @@ export interface WireFormat<T = Record<string, unknown>> {
|
|
|
25
29
|
bufferPaths: (string | number)[][];
|
|
26
30
|
buffers: Base64String[];
|
|
27
31
|
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Where a widget's ESM can be imported from, and which version.
|
|
35
|
+
* Structural mirror of the backend `EsmSpec`; `hash` keys the module
|
|
36
|
+
* cache and detects code changes (hot reload).
|
|
37
|
+
*/
|
|
38
|
+
export interface EsmSpec {
|
|
39
|
+
url: string;
|
|
40
|
+
hash: string;
|
|
41
|
+
}
|