@marimo-team/islands 0.23.14-dev9 → 0.23.15-dev0

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 (160) hide show
  1. package/dist/{ConnectedDataExplorerComponent-DXBx_nQg.js → ConnectedDataExplorerComponent-CU4fZJzG.js} +4 -4
  2. package/dist/assets/__vite-browser-external-BQCLNwri.js +1 -0
  3. package/dist/assets/{worker-DEDLIQQV.js → worker-DAWRHcPq.js} +2 -2
  4. package/dist/{chat-ui-DYBcNEdd.js → chat-ui-DinOvbWu.js} +3256 -3052
  5. package/dist/{click-outside-container-BDd67_1U.js → click-outside-container-BLPjMamz.js} +141 -113
  6. package/dist/{code-visibility-BZArWgy6.js → code-visibility-DDwWvOQa.js} +905 -889
  7. package/dist/data-grid-overlay-editor-C6lxUJp-.js +136 -0
  8. package/dist/{dist-D_bzzWBm.js → dist-Bf9f8MuT.js} +3 -3
  9. package/dist/{formats-WsOgyW_K.js → formats-Dzx4J_z1.js} +1 -1
  10. package/dist/{glide-data-editor-qpmKyAPn.js → glide-data-editor-CjTu7ukN.js} +2084 -1889
  11. package/dist/{html-to-image-MqcD07Bw.js → html-to-image-_wGfk8V-.js} +2389 -2323
  12. package/dist/{input-BSdZp5Ng.js → input-DtsN7xm-.js} +1 -1
  13. package/dist/main.js +3667 -1838
  14. package/dist/{mermaid-D-HYBMEV.js → mermaid-BYqXy_NE.js} +2 -2
  15. package/dist/{number-overlay-editor-D-a0qCT8.js → number-overlay-editor-BLJXvX9c.js} +1 -1
  16. package/dist/{process-output-Dt3icftd.js → process-output-Mh4UrjwM.js} +1 -1
  17. package/dist/{reveal-component-CKomb1B5.js → reveal-component-CHu9H-QX.js} +600 -596
  18. package/dist/{spec-CnTgI25l.js → spec-Cz-Bj1JI.js} +1 -1
  19. package/dist/style.css +1 -1
  20. package/dist/{toDate-D1Z7ZXWh.js → toDate-CWNNlFEX.js} +15 -7
  21. package/dist/{useAsyncData-BMc8itk2.js → useAsyncData-KfHB8wQR.js} +1 -1
  22. package/dist/{useDeepCompareMemoize-ZwmDBRDY.js → useDeepCompareMemoize-nJMtxhm4.js} +1 -1
  23. package/dist/{useLifecycle-CxffarYV.js → useLifecycle-DegSo0lV.js} +1 -1
  24. package/dist/{useTheme-yGsGEk82.js → useTheme-6eZ3GOTS.js} +5 -2
  25. package/dist/{vega-component-BFJTyykA.js → vega-component-DzyyM9fc.js} +5 -5
  26. package/package.json +4 -4
  27. package/src/__mocks__/requests.ts +1 -0
  28. package/src/__tests__/CellStatus.test.tsx +1 -2
  29. package/src/components/app-config/user-config-form.tsx +52 -0
  30. package/src/components/data-table/__tests__/column-explorer.test.tsx +25 -0
  31. package/src/components/data-table/__tests__/column-visibility-dropdown.test.tsx +60 -3
  32. package/src/components/data-table/charts/__tests__/altair-generator.test.ts +24 -0
  33. package/src/components/data-table/charts/__tests__/merge-index-fields.test.ts +33 -0
  34. package/src/components/data-table/charts/chart-spec/altair-generator.ts +4 -1
  35. package/src/components/data-table/charts/charts.tsx +23 -1
  36. package/src/components/data-table/column-explorer-panel/column-explorer.tsx +33 -12
  37. package/src/components/data-table/column-visibility-dropdown.tsx +15 -0
  38. package/src/components/debugger/debugger-code.tsx +7 -2
  39. package/src/components/editor/cell/CellStatus.tsx +1 -20
  40. package/src/components/editor/cell/PendingDeleteConfirmation.tsx +1 -1
  41. package/src/components/editor/cell/cell-context-menu.tsx +2 -2
  42. package/src/components/editor/chrome/panels/snippets-panel.tsx +3 -3
  43. package/src/components/editor/chrome/wrapper/footer-items/pyodide-status.tsx +6 -36
  44. package/src/components/editor/code/__tests__/readonly-python-code.test.tsx +79 -0
  45. package/src/components/editor/code/readonly-python-code.tsx +60 -33
  46. package/src/components/editor/errors/__tests__/auto-fix.test.ts +11 -107
  47. package/src/components/editor/errors/auto-fix.tsx +11 -31
  48. package/src/components/editor/notebook-cell.tsx +7 -0
  49. package/src/components/editor/output/ImageOutput.tsx +20 -6
  50. package/src/components/editor/output/MarimoTracebackOutput.tsx +20 -3
  51. package/src/components/editor/output/__tests__/ImageOutput.test.tsx +53 -0
  52. package/src/components/editor/renderers/grid-layout/grid-layout.tsx +7 -2
  53. package/src/components/editor/renderers/vertical-layout/vertical-layout.tsx +7 -3
  54. package/src/components/lifecycle/ProgressiveBoundary.tsx +42 -0
  55. package/src/components/lifecycle/RuntimeStatusBadge.tsx +59 -0
  56. package/src/components/lifecycle/__tests__/ProgressiveBoundary.test.tsx +147 -0
  57. package/src/components/lifecycle/__tests__/RuntimeStatusBadge.test.tsx +72 -0
  58. package/src/components/slides/__tests__/slide.test.tsx +33 -0
  59. package/src/components/slides/minimap.tsx +7 -1
  60. package/src/components/slides/reveal-component.tsx +3 -0
  61. package/src/components/slides/slide-cell-view.tsx +10 -6
  62. package/src/components/slides/slide.tsx +16 -13
  63. package/src/components/tracing/tracing.tsx +2 -1
  64. package/src/core/ai/context/__tests__/registry.test.ts +133 -3
  65. package/src/core/ai/context/providers/__tests__/datasource.test.ts +3 -1
  66. package/src/core/ai/context/providers/__tests__/error.test.ts +200 -15
  67. package/src/core/ai/context/providers/datasource.ts +27 -2
  68. package/src/core/ai/context/providers/error.ts +226 -36
  69. package/src/core/ai/context/registry.ts +77 -43
  70. package/src/core/cells/__tests__/cell.test.ts +39 -0
  71. package/src/core/cells/__tests__/readonly-code-display.test.ts +46 -0
  72. package/src/core/cells/cell.ts +5 -3
  73. package/src/core/cells/readonly-code-display.ts +35 -0
  74. package/src/core/codemirror/__tests__/setup.test.ts +33 -1
  75. package/src/core/codemirror/ai/resources.ts +15 -10
  76. package/src/core/codemirror/cells/__tests__/debugger-decorations.test.ts +185 -0
  77. package/src/core/codemirror/cells/__tests__/debugger-state.test.ts +224 -0
  78. package/src/core/codemirror/cells/__tests__/line-timing-decorations.test.ts +126 -0
  79. package/src/core/codemirror/cells/debugger-decorations.ts +188 -0
  80. package/src/core/codemirror/cells/debugger-state.ts +133 -0
  81. package/src/core/codemirror/cells/extensions.ts +32 -4
  82. package/src/core/codemirror/cells/line-timing-decorations.ts +165 -0
  83. package/src/core/codemirror/go-to-definition/__tests__/utils.test.ts +75 -3
  84. package/src/core/codemirror/go-to-definition/extension.ts +3 -3
  85. package/src/core/codemirror/go-to-definition/underline.ts +5 -14
  86. package/src/core/codemirror/go-to-definition/utils.ts +49 -1
  87. package/src/core/config/__tests__/config-schema.test.ts +17 -0
  88. package/src/core/config/config-schema.ts +1 -0
  89. package/src/core/config/config.ts +10 -0
  90. package/src/core/config/feature-flag.tsx +4 -0
  91. package/src/core/edit-app.tsx +8 -5
  92. package/src/core/errors/errors.ts +2 -1
  93. package/src/core/islands/bootstrap.ts +4 -3
  94. package/src/core/islands/bridge.ts +1 -0
  95. package/src/core/lifecycle/__tests__/render-policy.test.ts +247 -0
  96. package/src/core/lifecycle/render-policy.ts +125 -0
  97. package/src/core/mime.ts +11 -4
  98. package/src/core/network/__tests__/api.test.ts +17 -0
  99. package/src/core/network/__tests__/requests-toasting.test.tsx +46 -0
  100. package/src/core/network/api.ts +17 -6
  101. package/src/core/network/requests-lazy.ts +1 -0
  102. package/src/core/network/requests-network.ts +8 -0
  103. package/src/core/network/requests-static.ts +1 -0
  104. package/src/core/network/requests-toasting.tsx +10 -1
  105. package/src/core/network/types.ts +2 -0
  106. package/src/core/run-app.tsx +27 -23
  107. package/src/core/runtime/__tests__/adapter.test.ts +160 -0
  108. package/src/core/runtime/__tests__/runtime.test.ts +45 -0
  109. package/src/core/runtime/adapter.ts +182 -0
  110. package/src/core/runtime/runtime.ts +30 -16
  111. package/src/core/wasm/PyodideLoader.tsx +20 -62
  112. package/src/core/wasm/bridge.ts +13 -4
  113. package/src/core/wasm/state.ts +8 -19
  114. package/src/core/websocket/__tests__/useMarimoKernelConnection.test.ts +4 -0
  115. package/src/core/websocket/__tests__/useWebSocket.test.ts +49 -0
  116. package/src/core/websocket/transports/__tests__/sse.test.ts +338 -0
  117. package/src/core/websocket/transports/sse.ts +308 -0
  118. package/src/core/websocket/useMarimoKernelConnection.tsx +58 -7
  119. package/src/core/websocket/useWebSocket.tsx +12 -2
  120. package/src/css/app/codemirror.css +14 -0
  121. package/src/hooks/__tests__/useDelayElapsed.test.tsx +55 -0
  122. package/src/hooks/useDelayElapsed.ts +27 -0
  123. package/src/mount.tsx +3 -3
  124. package/src/plugins/core/RenderHTML.tsx +35 -1
  125. package/src/plugins/core/__test__/RenderHTML.test.ts +44 -0
  126. package/src/plugins/impl/DataTablePlugin.tsx +1 -0
  127. package/src/plugins/impl/anywidget/AnyWidgetPlugin.tsx +33 -254
  128. package/src/plugins/impl/anywidget/__tests__/AnyWidgetPlugin.test.tsx +116 -174
  129. package/src/plugins/impl/anywidget/__tests__/host.test.ts +313 -0
  130. package/src/plugins/impl/anywidget/__tests__/model-proxy.test.ts +158 -0
  131. package/src/plugins/impl/anywidget/__tests__/model.test.ts +8 -177
  132. package/src/plugins/impl/anywidget/__tests__/registry.test.ts +708 -0
  133. package/src/plugins/impl/anywidget/__tests__/resolve-widget.test.ts +132 -0
  134. package/src/plugins/impl/anywidget/__tests__/widget-binding.test.ts +305 -133
  135. package/src/plugins/impl/anywidget/__tests__/widget-ref.test.ts +28 -0
  136. package/src/plugins/impl/anywidget/host.ts +54 -0
  137. package/src/plugins/impl/anywidget/model-proxy.ts +70 -0
  138. package/src/plugins/impl/anywidget/model.ts +59 -239
  139. package/src/plugins/impl/anywidget/registry.ts +268 -0
  140. package/src/plugins/impl/anywidget/resolve-widget.ts +138 -0
  141. package/src/plugins/impl/anywidget/runtime.ts +422 -0
  142. package/src/plugins/impl/anywidget/types.ts +14 -0
  143. package/src/plugins/impl/anywidget/widget-binding.ts +280 -119
  144. package/src/plugins/impl/anywidget/widget-ref.ts +29 -0
  145. package/src/plugins/impl/data-editor/__tests__/glide-data-editor.test.tsx +187 -0
  146. package/src/plugins/impl/data-editor/glide-data-editor.tsx +6 -0
  147. package/src/plugins/impl/data-editor/glide-portal.tsx +73 -0
  148. package/src/plugins/impl/mpl-interactive/MplInteractivePlugin.tsx +3 -2
  149. package/src/plugins/impl/mpl-interactive/__tests__/MplInteractivePlugin.test.tsx +16 -13
  150. package/src/plugins/impl/mpl-interactive/mpl-websocket-shim.ts +1 -1
  151. package/src/utils/errors.ts +15 -0
  152. package/src/utils/time.ts +20 -0
  153. package/dist/assets/__vite-browser-external-DAm_YW43.js +0 -1
  154. package/dist/data-grid-overlay-editor-mfEJ5475.js +0 -128
  155. package/src/components/editor/chrome/panels/__tests__/snippet-display.test.ts +0 -22
  156. package/src/components/editor/chrome/panels/snippet-display.ts +0 -27
  157. package/src/core/ai/context/providers/__tests__/__snapshots__/error.test.ts.snap +0 -3
  158. package/src/core/wasm/__tests__/PyodideLoader.test.ts +0 -72
  159. package/src/core/wasm/__tests__/state.test.ts +0 -124
  160. 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
+ }