@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,70 @@
|
|
|
1
|
+
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
|
+
|
|
3
|
+
import type { AnyModel } from "@anywidget/types";
|
|
4
|
+
import type { ModelState } from "./types";
|
|
5
|
+
|
|
6
|
+
type ModelEventCallback = Parameters<AnyModel<ModelState>["on"]>[1];
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* A listener registered through a model proxy (see the hydration
|
|
10
|
+
* replay in `WidgetBinding.createView`).
|
|
11
|
+
*/
|
|
12
|
+
export interface ProxyRegistration {
|
|
13
|
+
event: string;
|
|
14
|
+
callback: ModelEventCallback;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Wrap a model so every `on()` call from inside `initialize` or `render`
|
|
19
|
+
* is auto-tied to a lifetime `AbortSignal`. When the signal aborts,
|
|
20
|
+
* every listener registered through the proxy is removed.
|
|
21
|
+
*
|
|
22
|
+
* Without this, a parent widget that re-renders a child via
|
|
23
|
+
* `host.getWidget().render` would accumulate listeners on the child's
|
|
24
|
+
* model on every re-render — the widget author would have to know to
|
|
25
|
+
* pass `signal` to each `on()` call themselves.
|
|
26
|
+
*
|
|
27
|
+
* The proxy is purely a host-side ergonomic helper. The widget author
|
|
28
|
+
* still writes `model.on("change:foo", handler)` exactly as before; the
|
|
29
|
+
* cleanup signal is supplied transparently.
|
|
30
|
+
*
|
|
31
|
+
* `onRegister`, when given, observes each `on()` registration.
|
|
32
|
+
*/
|
|
33
|
+
// oxlint-disable-next-line marimo/prefer-object-params -- concise internal helper used at protocol call sites
|
|
34
|
+
export function modelProxy<T extends ModelState>(
|
|
35
|
+
model: AnyModel<T>,
|
|
36
|
+
signal: AbortSignal,
|
|
37
|
+
onRegister?: (registration: ProxyRegistration) => void,
|
|
38
|
+
): AnyModel<T> {
|
|
39
|
+
return {
|
|
40
|
+
get(key) {
|
|
41
|
+
return model.get(key);
|
|
42
|
+
},
|
|
43
|
+
set(key, value) {
|
|
44
|
+
model.set(key, value);
|
|
45
|
+
},
|
|
46
|
+
save_changes() {
|
|
47
|
+
model.save_changes();
|
|
48
|
+
},
|
|
49
|
+
send: model.send.bind(model),
|
|
50
|
+
on(name: string, callback: ModelEventCallback): void {
|
|
51
|
+
if (signal.aborted) {
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
model.on(name, callback);
|
|
55
|
+
signal.addEventListener("abort", () => model.off(name, callback), {
|
|
56
|
+
once: true,
|
|
57
|
+
});
|
|
58
|
+
onRegister?.({ event: name, callback });
|
|
59
|
+
},
|
|
60
|
+
off(name?: string | null, callback?: ModelEventCallback | null): void {
|
|
61
|
+
model.off(name ?? null, callback ?? null);
|
|
62
|
+
},
|
|
63
|
+
widget_manager: {
|
|
64
|
+
async get_model<TT extends ModelState>(modelId: string) {
|
|
65
|
+
const child = await model.widget_manager.get_model<TT>(modelId);
|
|
66
|
+
return modelProxy(child, signal);
|
|
67
|
+
},
|
|
68
|
+
},
|
|
69
|
+
};
|
|
70
|
+
}
|
|
@@ -3,104 +3,24 @@
|
|
|
3
3
|
|
|
4
4
|
import type { AnyModel } from "@anywidget/types";
|
|
5
5
|
import { debounce } from "lodash-es";
|
|
6
|
-
import type { NotificationMessageData } from "@/core/kernel/messages";
|
|
7
|
-
import { getRequestClient } from "@/core/network/requests";
|
|
8
|
-
import { isStaticNotebook } from "@/core/static/static-state";
|
|
9
|
-
import {
|
|
10
|
-
decodeFromWire,
|
|
11
|
-
serializeBuffersToBase64,
|
|
12
|
-
} from "@/plugins/impl/anywidget/serialization";
|
|
13
|
-
import { assertNever } from "@/utils/assertNever";
|
|
14
|
-
import { Deferred } from "@/utils/Deferred";
|
|
15
|
-
import {
|
|
16
|
-
type Base64String,
|
|
17
|
-
base64ToDataView,
|
|
18
|
-
dataViewToBase64,
|
|
19
|
-
} from "@/utils/json/base64";
|
|
20
6
|
import { Logger } from "@/utils/Logger";
|
|
21
|
-
import { repl } from "@/utils/repl";
|
|
22
|
-
import type { AnyWidgetMessage } from "./schemas";
|
|
23
7
|
import type { EventHandler, ModelState, WidgetModelId } from "./types";
|
|
24
|
-
import { BINDING_MANAGER } from "./widget-binding";
|
|
25
8
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
constructor(timeout = 10_000) {
|
|
36
|
-
this.#timeout = timeout;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
#getOrCreateEntry(key: WidgetModelId): ModelEntry {
|
|
40
|
-
let entry = this.#entries.get(key);
|
|
41
|
-
if (!entry) {
|
|
42
|
-
entry = {
|
|
43
|
-
deferred: new Deferred<Model<ModelState>>(),
|
|
44
|
-
controller: new AbortController(),
|
|
45
|
-
};
|
|
46
|
-
this.#entries.set(key, entry);
|
|
47
|
-
}
|
|
48
|
-
return entry;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
get(key: WidgetModelId): Promise<Model<any>> {
|
|
52
|
-
const entry = this.#getOrCreateEntry(key);
|
|
53
|
-
if (entry.deferred.status === "pending") {
|
|
54
|
-
// Add timeout to prevent hanging
|
|
55
|
-
setTimeout(() => {
|
|
56
|
-
if (entry.deferred.status !== "pending") {
|
|
57
|
-
return;
|
|
58
|
-
}
|
|
59
|
-
entry.deferred.reject(new Error(`Model not found for key: ${key}`));
|
|
60
|
-
this.#entries.delete(key);
|
|
61
|
-
}, this.#timeout);
|
|
62
|
-
}
|
|
63
|
-
return entry.deferred.promise;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
/**
|
|
67
|
-
* Create a model with a managed lifecycle signal.
|
|
68
|
-
* The signal is aborted when the model is deleted.
|
|
69
|
-
*/
|
|
70
|
-
create(
|
|
71
|
-
key: WidgetModelId,
|
|
72
|
-
factory: (signal: AbortSignal) => Model<ModelState>,
|
|
73
|
-
): void {
|
|
74
|
-
const entry = this.#getOrCreateEntry(key);
|
|
75
|
-
entry.deferred.resolve(factory(entry.controller.signal));
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
set(key: WidgetModelId, model: Model<any>): void {
|
|
79
|
-
this.#getOrCreateEntry(key).deferred.resolve(model);
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
/**
|
|
83
|
-
* Get a model synchronously if it exists and has been resolved.
|
|
84
|
-
* Returns undefined if the model doesn't exist or is still pending.
|
|
85
|
-
*/
|
|
86
|
-
getSync(key: WidgetModelId): Model<any> | undefined {
|
|
87
|
-
const entry = this.#entries.get(key);
|
|
88
|
-
if (entry && entry.deferred.status === "resolved") {
|
|
89
|
-
return entry.deferred.value;
|
|
90
|
-
}
|
|
91
|
-
return undefined;
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
delete(key: WidgetModelId): void {
|
|
95
|
-
Logger.debug(
|
|
96
|
-
`[ModelManager] Deleting model=${key}, aborting lifecycle signal`,
|
|
97
|
-
);
|
|
98
|
-
this.#entries.get(key)?.controller.abort();
|
|
99
|
-
this.#entries.delete(key);
|
|
100
|
-
}
|
|
9
|
+
/**
|
|
10
|
+
* The custom-message shape delivered to `msg:custom` listeners — the
|
|
11
|
+
* only message shape the frontend re-declares; the wire contract is
|
|
12
|
+
* the generated `model-lifecycle` type.
|
|
13
|
+
*/
|
|
14
|
+
export interface CustomMessage {
|
|
15
|
+
method: "custom";
|
|
16
|
+
// oxlint-disable-next-line typescript/no-explicit-any
|
|
17
|
+
content: any;
|
|
101
18
|
}
|
|
102
19
|
|
|
103
|
-
|
|
20
|
+
/**
|
|
21
|
+
* The model's channel back to the kernel; a no-op in static exports.
|
|
22
|
+
*/
|
|
23
|
+
export interface MarimoComm<T> {
|
|
104
24
|
sendUpdate: (value: Partial<T>) => Promise<void>;
|
|
105
25
|
sendCustomMessage: (content: unknown, buffers: DataView[]) => Promise<void>;
|
|
106
26
|
}
|
|
@@ -112,15 +32,11 @@ interface MarimoInternalApi<T extends ModelState> {
|
|
|
112
32
|
* Update model state and emit change events for any differences.
|
|
113
33
|
*/
|
|
114
34
|
updateAndEmitDiffs: (value: T) => void;
|
|
115
|
-
/**
|
|
116
|
-
* Re-emit current state as change events.
|
|
117
|
-
*/
|
|
118
|
-
reemitState: () => void;
|
|
119
35
|
/**
|
|
120
36
|
* Emit a custom message to listeners.
|
|
121
37
|
*/
|
|
122
38
|
emitCustomMessage: (
|
|
123
|
-
message:
|
|
39
|
+
message: CustomMessage,
|
|
124
40
|
buffers?: readonly DataView[],
|
|
125
41
|
) => void;
|
|
126
42
|
}
|
|
@@ -135,8 +51,6 @@ export function getMarimoInternal<T extends ModelState>(
|
|
|
135
51
|
return model[marimoSymbol];
|
|
136
52
|
}
|
|
137
53
|
|
|
138
|
-
export const MODEL_MANAGER = new ModelManager();
|
|
139
|
-
|
|
140
54
|
export class Model<T extends ModelState> implements AnyModel<T> {
|
|
141
55
|
#ANY_CHANGE_EVENT = "change";
|
|
142
56
|
#dirtyFields: Map<keyof T, unknown>;
|
|
@@ -144,7 +58,14 @@ export class Model<T extends ModelState> implements AnyModel<T> {
|
|
|
144
58
|
#comm: MarimoComm<T>;
|
|
145
59
|
#listeners: Record<string, Set<EventHandler> | undefined> = {};
|
|
146
60
|
|
|
147
|
-
|
|
61
|
+
/**
|
|
62
|
+
* Resolves models for the legacy ipywidgets escape hatch
|
|
63
|
+
* (`widget_manager.get_model`). Assigned by the registry module at
|
|
64
|
+
* import time to avoid an import cycle.
|
|
65
|
+
*/
|
|
66
|
+
static _modelManager: {
|
|
67
|
+
getModel(model_id: WidgetModelId): Promise<Model<any>>;
|
|
68
|
+
};
|
|
148
69
|
|
|
149
70
|
constructor(data: T, comm: MarimoComm<T>, signal?: AbortSignal) {
|
|
150
71
|
this.#data = data;
|
|
@@ -164,9 +85,8 @@ export class Model<T extends ModelState> implements AnyModel<T> {
|
|
|
164
85
|
*/
|
|
165
86
|
[marimoSymbol]: MarimoInternalApi<T> = {
|
|
166
87
|
updateAndEmitDiffs: (value: T) => this.#updateAndEmitDiffs(value),
|
|
167
|
-
reemitState: () => this.#reemitState(),
|
|
168
88
|
emitCustomMessage: (
|
|
169
|
-
message:
|
|
89
|
+
message: CustomMessage,
|
|
170
90
|
buffers?: readonly DataView[],
|
|
171
91
|
) => this.#emitCustomMessage(message, buffers),
|
|
172
92
|
};
|
|
@@ -204,7 +124,7 @@ export class Model<T extends ModelState> implements AnyModel<T> {
|
|
|
204
124
|
async get_model<TT extends ModelState>(
|
|
205
125
|
model_id: WidgetModelId,
|
|
206
126
|
): Promise<AnyModel<TT>> {
|
|
207
|
-
const model = await Model._modelManager.
|
|
127
|
+
const model = await Model._modelManager.getModel(model_id);
|
|
208
128
|
if (!model) {
|
|
209
129
|
throw new Error(
|
|
210
130
|
`Model not found with id: ${model_id}. This is likely because the model was not registered.`,
|
|
@@ -239,19 +159,43 @@ export class Model<T extends ModelState> implements AnyModel<T> {
|
|
|
239
159
|
this.#comm.sendUpdate(partialData);
|
|
240
160
|
}
|
|
241
161
|
|
|
242
|
-
|
|
162
|
+
/**
|
|
163
|
+
* Register an event listener.
|
|
164
|
+
*
|
|
165
|
+
* Passing `signal` ties the listener's lifetime to an `AbortSignal` —
|
|
166
|
+
* when the signal aborts, the listener is auto-removed. This is the
|
|
167
|
+
* preferred cleanup mechanism (matches the web platform pattern of
|
|
168
|
+
* `addEventListener({ signal })`) and is what `modelProxy` uses to
|
|
169
|
+
* scope listeners to view / binding lifetimes.
|
|
170
|
+
*/
|
|
171
|
+
on(
|
|
172
|
+
eventName: string,
|
|
173
|
+
callback: EventHandler,
|
|
174
|
+
options?: { signal?: AbortSignal },
|
|
175
|
+
): void {
|
|
176
|
+
const signal = options?.signal;
|
|
177
|
+
if (signal?.aborted) {
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
243
180
|
if (!this.#listeners[eventName]) {
|
|
244
181
|
this.#listeners[eventName] = new Set();
|
|
245
182
|
}
|
|
246
183
|
this.#listeners[eventName].add(callback);
|
|
184
|
+
signal?.addEventListener("abort", () => this.off(eventName, callback), {
|
|
185
|
+
once: true,
|
|
186
|
+
});
|
|
247
187
|
}
|
|
248
188
|
|
|
249
189
|
#emit<K extends keyof T>(event: `change:${K & string}`, value: T[K]) {
|
|
250
|
-
|
|
190
|
+
const listeners = this.#listeners[event];
|
|
191
|
+
if (!listeners) {
|
|
251
192
|
return;
|
|
252
193
|
}
|
|
253
|
-
|
|
254
|
-
|
|
194
|
+
// Snapshot before iterating: a callback may unregister itself
|
|
195
|
+
// (typical signal-based abort handler calling `model.off`), and a
|
|
196
|
+
// Set mutated mid-iteration drops the very next element.
|
|
197
|
+
// oxlint-disable-next-line no-useless-spread -- snapshot is intentional
|
|
198
|
+
for (const listener of [...listeners]) {
|
|
255
199
|
try {
|
|
256
200
|
listener(value);
|
|
257
201
|
} catch (error) {
|
|
@@ -274,29 +218,21 @@ export class Model<T extends ModelState> implements AnyModel<T> {
|
|
|
274
218
|
});
|
|
275
219
|
}
|
|
276
220
|
|
|
277
|
-
#reemitState() {
|
|
278
|
-
for (const [key, value] of Object.entries(this.#data) as [
|
|
279
|
-
keyof T & string,
|
|
280
|
-
T[keyof T],
|
|
281
|
-
][]) {
|
|
282
|
-
this.#emit(`change:${key}`, value);
|
|
283
|
-
}
|
|
284
|
-
this.#emitAnyChange();
|
|
285
|
-
}
|
|
286
|
-
|
|
287
221
|
/**
|
|
288
222
|
* When receiving a message from the backend.
|
|
289
223
|
* We want to notify all listeners with `msg:custom`
|
|
290
224
|
*/
|
|
291
225
|
#emitCustomMessage(
|
|
292
|
-
message:
|
|
226
|
+
message: CustomMessage,
|
|
293
227
|
buffers: readonly DataView[] = [],
|
|
294
228
|
) {
|
|
295
229
|
const listeners = this.#listeners["msg:custom"];
|
|
296
230
|
if (!listeners) {
|
|
297
231
|
return;
|
|
298
232
|
}
|
|
299
|
-
|
|
233
|
+
// Snapshot before iterating: see `#emit` for rationale.
|
|
234
|
+
// oxlint-disable-next-line no-useless-spread -- snapshot is intentional
|
|
235
|
+
for (const listener of [...listeners]) {
|
|
300
236
|
try {
|
|
301
237
|
listener(message.content, buffers);
|
|
302
238
|
} catch (error) {
|
|
@@ -311,7 +247,9 @@ export class Model<T extends ModelState> implements AnyModel<T> {
|
|
|
311
247
|
if (!listeners) {
|
|
312
248
|
return;
|
|
313
249
|
}
|
|
314
|
-
|
|
250
|
+
// Snapshot before iterating: see `#emit` for rationale.
|
|
251
|
+
// oxlint-disable-next-line no-useless-spread -- snapshot is intentional
|
|
252
|
+
for (const listener of [...listeners]) {
|
|
315
253
|
try {
|
|
316
254
|
listener();
|
|
317
255
|
} catch (error) {
|
|
@@ -320,121 +258,3 @@ export class Model<T extends ModelState> implements AnyModel<T> {
|
|
|
320
258
|
}
|
|
321
259
|
}, 0);
|
|
322
260
|
}
|
|
323
|
-
|
|
324
|
-
/**
|
|
325
|
-
* Handle an incoming model lifecycle notification from the backend.
|
|
326
|
-
*
|
|
327
|
-
* Messages are dispatched by method type:
|
|
328
|
-
* - "open": Initialize a new model or update existing one with initial state
|
|
329
|
-
* - "update": Update model state with new values
|
|
330
|
-
* - "custom": Forward custom message to model listeners
|
|
331
|
-
* - "close": Remove model from manager
|
|
332
|
-
*/
|
|
333
|
-
export async function handleWidgetMessage(
|
|
334
|
-
modelManager: ModelManager,
|
|
335
|
-
notification: NotificationMessageData<"model-lifecycle">,
|
|
336
|
-
): Promise<void> {
|
|
337
|
-
const modelId = notification.model_id as WidgetModelId;
|
|
338
|
-
const msg = notification.message;
|
|
339
|
-
|
|
340
|
-
// Decode base64 buffers to DataViews (present in open/update/custom messages)
|
|
341
|
-
const base64Buffers: Base64String[] = "buffers" in msg ? msg.buffers : [];
|
|
342
|
-
const buffers = base64Buffers.map(base64ToDataView);
|
|
343
|
-
|
|
344
|
-
switch (msg.method) {
|
|
345
|
-
case "open": {
|
|
346
|
-
const { state, buffer_paths = [] } = msg;
|
|
347
|
-
const stateWithBuffers = decodeFromWire({
|
|
348
|
-
state,
|
|
349
|
-
bufferPaths: buffer_paths,
|
|
350
|
-
buffers,
|
|
351
|
-
});
|
|
352
|
-
|
|
353
|
-
// Check if a model already exists (created by the plugin using model_id reference)
|
|
354
|
-
// If so, just update its state instead of creating a duplicate
|
|
355
|
-
const existingModel = modelManager.getSync(modelId);
|
|
356
|
-
if (existingModel) {
|
|
357
|
-
getMarimoInternal(existingModel).updateAndEmitDiffs(stateWithBuffers);
|
|
358
|
-
return;
|
|
359
|
-
}
|
|
360
|
-
|
|
361
|
-
modelManager.create(modelId, (signal) => {
|
|
362
|
-
// In static exports there is no kernel, so comm calls are no-ops.
|
|
363
|
-
const comm: MarimoComm<ModelState> = isStaticNotebook()
|
|
364
|
-
? {
|
|
365
|
-
sendUpdate: async () => undefined,
|
|
366
|
-
sendCustomMessage: async () => undefined,
|
|
367
|
-
}
|
|
368
|
-
: {
|
|
369
|
-
async sendUpdate(changeData) {
|
|
370
|
-
if (signal.aborted) {
|
|
371
|
-
Logger.debug(
|
|
372
|
-
`[Model] sendUpdate suppressed for model=${modelId} (signal aborted)`,
|
|
373
|
-
);
|
|
374
|
-
return;
|
|
375
|
-
}
|
|
376
|
-
const { state, buffers, bufferPaths } =
|
|
377
|
-
serializeBuffersToBase64(changeData);
|
|
378
|
-
await getRequestClient().sendModelValue({
|
|
379
|
-
modelId,
|
|
380
|
-
message: { method: "update", state, bufferPaths },
|
|
381
|
-
buffers,
|
|
382
|
-
});
|
|
383
|
-
},
|
|
384
|
-
async sendCustomMessage(content, buffers) {
|
|
385
|
-
if (signal.aborted) {
|
|
386
|
-
Logger.debug(
|
|
387
|
-
`[Model] sendCustomMessage suppressed for model=${modelId} (signal aborted)`,
|
|
388
|
-
);
|
|
389
|
-
return;
|
|
390
|
-
}
|
|
391
|
-
await getRequestClient().sendModelValue({
|
|
392
|
-
modelId,
|
|
393
|
-
message: { method: "custom", content },
|
|
394
|
-
buffers: buffers.map(dataViewToBase64),
|
|
395
|
-
});
|
|
396
|
-
},
|
|
397
|
-
};
|
|
398
|
-
|
|
399
|
-
return new Model(stateWithBuffers, comm, signal);
|
|
400
|
-
});
|
|
401
|
-
return;
|
|
402
|
-
}
|
|
403
|
-
|
|
404
|
-
case "custom": {
|
|
405
|
-
const model = await modelManager.get(modelId);
|
|
406
|
-
// For custom messages, we need to reconstruct the AnyWidgetMessage format
|
|
407
|
-
getMarimoInternal(model).emitCustomMessage(
|
|
408
|
-
{ method: "custom", content: msg.content },
|
|
409
|
-
buffers,
|
|
410
|
-
);
|
|
411
|
-
return;
|
|
412
|
-
}
|
|
413
|
-
|
|
414
|
-
case "close":
|
|
415
|
-
BINDING_MANAGER.destroy(modelId);
|
|
416
|
-
modelManager.delete(modelId); // aborts the model's signal, clearing listeners
|
|
417
|
-
return;
|
|
418
|
-
|
|
419
|
-
case "update": {
|
|
420
|
-
const { state, buffer_paths = [] } = msg;
|
|
421
|
-
const stateWithBuffers = decodeFromWire({
|
|
422
|
-
state,
|
|
423
|
-
bufferPaths: buffer_paths,
|
|
424
|
-
buffers,
|
|
425
|
-
});
|
|
426
|
-
const model = await modelManager.get(modelId);
|
|
427
|
-
getMarimoInternal(model).updateAndEmitDiffs(stateWithBuffers);
|
|
428
|
-
return;
|
|
429
|
-
}
|
|
430
|
-
|
|
431
|
-
default:
|
|
432
|
-
assertNever(msg);
|
|
433
|
-
}
|
|
434
|
-
}
|
|
435
|
-
|
|
436
|
-
repl(MODEL_MANAGER, "MODEL_MANAGER");
|
|
437
|
-
|
|
438
|
-
export const visibleForTesting = {
|
|
439
|
-
ModelManager,
|
|
440
|
-
};
|