@marimo-team/islands 0.19.8-dev41 → 0.19.8-dev49
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/assets/__vite-browser-external-WSlCcXn_.js +1 -0
- package/dist/assets/worker-DUYMdbtA.js +73 -0
- package/dist/main.js +1740 -1691
- package/dist/style.css +1 -1
- package/dist/{useDeepCompareMemoize-CMGprt3H.js → useDeepCompareMemoize-BhZZsis0.js} +7 -3
- package/dist/{vega-component-DU3aSp4m.js → vega-component-DCxUyPnb.js} +1 -1
- package/package.json +1 -1
- package/src/components/app-config/optional-features.tsx +1 -1
- package/src/components/chat/__tests__/useFileState.test.tsx +93 -0
- package/src/components/chat/acp/agent-panel.tsx +26 -77
- package/src/components/chat/chat-components.tsx +114 -1
- package/src/components/chat/chat-panel.tsx +32 -104
- package/src/components/chat/chat-utils.ts +42 -0
- package/src/components/editor/ai/add-cell-with-ai.tsx +85 -53
- package/src/components/editor/ai/ai-completion-editor.tsx +15 -38
- package/src/components/editor/chrome/panels/packages-panel.tsx +12 -9
- package/src/core/islands/__tests__/bridge.test.ts +7 -2
- package/src/core/islands/bridge.ts +1 -1
- package/src/core/islands/main.ts +7 -0
- package/src/core/network/types.ts +2 -2
- package/src/core/wasm/bridge.ts +1 -1
- package/src/core/websocket/useMarimoKernelConnection.tsx +5 -15
- package/src/plugins/impl/anywidget/AnyWidgetPlugin.tsx +86 -167
- package/src/plugins/impl/anywidget/__tests__/AnyWidgetPlugin.test.tsx +37 -123
- package/src/plugins/impl/anywidget/__tests__/model.test.ts +128 -122
- package/src/{utils/__tests__/data-views.test.ts → plugins/impl/anywidget/__tests__/serialization.test.ts} +42 -96
- package/src/plugins/impl/anywidget/model.ts +348 -223
- package/src/plugins/impl/anywidget/schemas.ts +32 -0
- package/src/{utils/data-views.ts → plugins/impl/anywidget/serialization.ts} +13 -36
- package/src/plugins/impl/anywidget/types.ts +27 -0
- package/src/plugins/impl/chat/chat-ui.tsx +22 -20
- package/src/utils/Deferred.ts +21 -0
- package/src/utils/json/base64.ts +38 -8
- package/dist/assets/__vite-browser-external-6-UwTyQC.js +0 -1
- package/dist/assets/worker-D3e5wDxM.js +0 -73
|
@@ -1,35 +1,49 @@
|
|
|
1
1
|
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
2
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
3
3
|
|
|
4
|
-
import type { AnyModel } from "@anywidget/types";
|
|
4
|
+
import type { AnyModel, AnyWidget, Experimental } from "@anywidget/types";
|
|
5
5
|
import { debounce } from "lodash-es";
|
|
6
|
-
import {
|
|
6
|
+
import type { NotificationMessageData } from "@/core/kernel/messages";
|
|
7
7
|
import { getRequestClient } from "@/core/network/requests";
|
|
8
|
+
import {
|
|
9
|
+
decodeFromWire,
|
|
10
|
+
serializeBuffersToBase64,
|
|
11
|
+
} from "@/plugins/impl/anywidget/serialization";
|
|
8
12
|
import { assertNever } from "@/utils/assertNever";
|
|
9
13
|
import { Deferred } from "@/utils/Deferred";
|
|
10
|
-
import {
|
|
11
|
-
|
|
12
|
-
|
|
14
|
+
import {
|
|
15
|
+
type Base64String,
|
|
16
|
+
base64ToDataView,
|
|
17
|
+
dataViewToBase64,
|
|
18
|
+
} from "@/utils/json/base64";
|
|
13
19
|
import { Logger } from "@/utils/Logger";
|
|
14
|
-
|
|
15
|
-
|
|
20
|
+
import { repl } from "@/utils/repl";
|
|
21
|
+
import type { AnyWidgetMessage } from "./schemas";
|
|
22
|
+
import type { EventHandler, ModelState, WidgetModelId } from "./types";
|
|
16
23
|
|
|
17
24
|
class ModelManager {
|
|
18
|
-
|
|
19
|
-
|
|
25
|
+
/**
|
|
26
|
+
* Map of model ids to deferred promises
|
|
27
|
+
*/
|
|
28
|
+
#models = new Map<WidgetModelId, Deferred<Model<ModelState>>>();
|
|
29
|
+
/**
|
|
30
|
+
* Timeout for model lookup
|
|
31
|
+
*/
|
|
32
|
+
#timeout: number;
|
|
33
|
+
|
|
20
34
|
constructor(timeout = 10_000) {
|
|
21
|
-
this
|
|
35
|
+
this.#timeout = timeout;
|
|
22
36
|
}
|
|
23
37
|
|
|
24
|
-
get(key:
|
|
25
|
-
let deferred = this
|
|
38
|
+
get(key: WidgetModelId): Promise<Model<any>> {
|
|
39
|
+
let deferred = this.#models.get(key);
|
|
26
40
|
if (deferred) {
|
|
27
41
|
return deferred.promise;
|
|
28
42
|
}
|
|
29
43
|
|
|
30
44
|
// If the model is not yet created, create the new deferred promise without resolving it
|
|
31
|
-
deferred = new Deferred<Model<
|
|
32
|
-
this
|
|
45
|
+
deferred = new Deferred<Model<ModelState>>();
|
|
46
|
+
this.#models.set(key, deferred);
|
|
33
47
|
|
|
34
48
|
// Add timeout to prevent hanging
|
|
35
49
|
setTimeout(() => {
|
|
@@ -39,90 +53,216 @@ class ModelManager {
|
|
|
39
53
|
}
|
|
40
54
|
|
|
41
55
|
deferred.reject(new Error(`Model not found for key: ${key}`));
|
|
42
|
-
this
|
|
43
|
-
}, this
|
|
56
|
+
this.#models.delete(key);
|
|
57
|
+
}, this.#timeout);
|
|
44
58
|
|
|
45
59
|
return deferred.promise;
|
|
46
60
|
}
|
|
47
61
|
|
|
48
|
-
set(key:
|
|
49
|
-
let deferred = this
|
|
62
|
+
set(key: WidgetModelId, model: Model<any>): void {
|
|
63
|
+
let deferred = this.#models.get(key);
|
|
50
64
|
if (!deferred) {
|
|
51
|
-
deferred = new Deferred<Model<
|
|
52
|
-
this
|
|
65
|
+
deferred = new Deferred<Model<ModelState>>();
|
|
66
|
+
this.#models.set(key, deferred);
|
|
53
67
|
}
|
|
54
68
|
deferred.resolve(model);
|
|
55
69
|
}
|
|
56
70
|
|
|
57
|
-
|
|
58
|
-
|
|
71
|
+
/**
|
|
72
|
+
* Check if a model exists and has been resolved (not pending).
|
|
73
|
+
* This is useful for checking if a model was already created by the plugin
|
|
74
|
+
* before the 'open' message arrives.
|
|
75
|
+
*/
|
|
76
|
+
has(key: WidgetModelId): boolean {
|
|
77
|
+
const deferred = this.#models.get(key);
|
|
78
|
+
return deferred !== undefined && deferred.status === "resolved";
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Get a model synchronously if it exists and has been resolved.
|
|
83
|
+
* Returns undefined if the model doesn't exist or is still pending.
|
|
84
|
+
*/
|
|
85
|
+
getSync(key: WidgetModelId): Model<any> | undefined {
|
|
86
|
+
const deferred = this.#models.get(key);
|
|
87
|
+
if (deferred && deferred.status === "resolved") {
|
|
88
|
+
return deferred.value;
|
|
89
|
+
}
|
|
90
|
+
return undefined;
|
|
59
91
|
}
|
|
92
|
+
|
|
93
|
+
delete(key: WidgetModelId): void {
|
|
94
|
+
this.#models.delete(key);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
interface MarimoComm<T> {
|
|
99
|
+
sendUpdate: (value: Partial<T>) => Promise<void>;
|
|
100
|
+
sendCustomMessage: (content: unknown, buffers: DataView[]) => Promise<void>;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const marimoSymbol = Symbol("marimo");
|
|
104
|
+
|
|
105
|
+
const experimental: Experimental = {
|
|
106
|
+
invoke: async () => {
|
|
107
|
+
const message =
|
|
108
|
+
"anywidget.invoke not supported in marimo. Please file an issue at https://github.com/marimo-team/marimo/issues";
|
|
109
|
+
Logger.warn(message);
|
|
110
|
+
throw new Error(message);
|
|
111
|
+
},
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
type RenderFn = (el: HTMLElement, signal: AbortSignal) => Promise<void>;
|
|
115
|
+
|
|
116
|
+
interface MarimoInternalApi<T extends ModelState> {
|
|
117
|
+
/**
|
|
118
|
+
* Resolve the widget definition and initialize if needed.
|
|
119
|
+
* Returns a render function that can be called for each view.
|
|
120
|
+
*
|
|
121
|
+
* Per AFM spec:
|
|
122
|
+
* - widgetDef() is called once per model
|
|
123
|
+
* - initialize() is called once per model
|
|
124
|
+
* - render() (the returned function) is called once per view
|
|
125
|
+
*/
|
|
126
|
+
resolveWidget: (widgetDef: AnyWidget<T>) => Promise<RenderFn>;
|
|
127
|
+
/**
|
|
128
|
+
* Update model state and emit change events for any differences.
|
|
129
|
+
*/
|
|
130
|
+
updateAndEmitDiffs: (value: T) => void;
|
|
131
|
+
/**
|
|
132
|
+
* Emit a custom message to listeners.
|
|
133
|
+
*/
|
|
134
|
+
emitCustomMessage: (
|
|
135
|
+
message: Extract<AnyWidgetMessage, { method: "custom" }>,
|
|
136
|
+
buffers?: readonly DataView[],
|
|
137
|
+
) => void;
|
|
138
|
+
/**
|
|
139
|
+
* Destroy the model, triggering initialize cleanup.
|
|
140
|
+
*/
|
|
141
|
+
destroy: () => void;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Get the internal marimo API for a Model instance.
|
|
146
|
+
* These are not part of the public AnyModel interface.
|
|
147
|
+
*/
|
|
148
|
+
export function getMarimoInternal<T extends ModelState>(
|
|
149
|
+
model: Model<T>,
|
|
150
|
+
): MarimoInternalApi<T> {
|
|
151
|
+
return model[marimoSymbol];
|
|
60
152
|
}
|
|
61
153
|
|
|
62
154
|
export const MODEL_MANAGER = new ModelManager();
|
|
63
155
|
|
|
64
|
-
export class Model<T extends
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
initialDirtyFields: Set<keyof T>,
|
|
83
|
-
) {
|
|
84
|
-
this.data = data;
|
|
85
|
-
this.onChange = onChange;
|
|
86
|
-
this.sendToWidget = sendToWidget;
|
|
87
|
-
this.dirtyFields = new Map(
|
|
88
|
-
[...initialDirtyFields].map((key) => [key, this.data[key]]),
|
|
89
|
-
);
|
|
156
|
+
export class Model<T extends ModelState> implements AnyModel<T> {
|
|
157
|
+
#ANY_CHANGE_EVENT = "change";
|
|
158
|
+
#dirtyFields: Map<keyof T, unknown>;
|
|
159
|
+
#data: T;
|
|
160
|
+
#comm: MarimoComm<T>;
|
|
161
|
+
#listeners: Record<string, Set<EventHandler> | undefined> = {};
|
|
162
|
+
#controller = new AbortController();
|
|
163
|
+
#widgetDef: AnyWidget<T> | undefined;
|
|
164
|
+
#render:
|
|
165
|
+
| ((el: HTMLElement, signal: AbortSignal) => Promise<void>)
|
|
166
|
+
| undefined;
|
|
167
|
+
|
|
168
|
+
static _modelManager: ModelManager = MODEL_MANAGER;
|
|
169
|
+
|
|
170
|
+
constructor(data: T, comm: MarimoComm<T>) {
|
|
171
|
+
this.#data = data;
|
|
172
|
+
this.#comm = comm;
|
|
173
|
+
this.#dirtyFields = new Map();
|
|
90
174
|
}
|
|
91
175
|
|
|
92
|
-
|
|
176
|
+
/**
|
|
177
|
+
* Internal marimo API - not part of AnyWidget AFM.
|
|
178
|
+
* Access via getMarimoInternal().
|
|
179
|
+
*/
|
|
180
|
+
[marimoSymbol]: MarimoInternalApi<T> = {
|
|
181
|
+
updateAndEmitDiffs: (value: T) => this.#updateAndEmitDiffs(value),
|
|
182
|
+
emitCustomMessage: (
|
|
183
|
+
message: Extract<AnyWidgetMessage, { method: "custom" }>,
|
|
184
|
+
buffers?: readonly DataView[],
|
|
185
|
+
) => this.#emitCustomMessage(message, buffers),
|
|
186
|
+
resolveWidget: async (widgetDef: AnyWidget<T>): Promise<RenderFn> => {
|
|
187
|
+
// Already initialized with the same widget - return cached render
|
|
188
|
+
if (this.#render && this.#widgetDef === widgetDef) {
|
|
189
|
+
return this.#render;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// If widgetDef changed (hot reload), destroy old and re-initialize
|
|
193
|
+
if (this.#render && this.#widgetDef !== widgetDef) {
|
|
194
|
+
this.#controller.abort();
|
|
195
|
+
this.#controller = new AbortController();
|
|
196
|
+
this.#render = undefined;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
this.#widgetDef = widgetDef;
|
|
200
|
+
|
|
201
|
+
// Resolve the widget definition (call if it's a function)
|
|
202
|
+
const widget =
|
|
203
|
+
typeof widgetDef === "function" ? await widgetDef() : widgetDef;
|
|
204
|
+
|
|
205
|
+
// Call initialize once per model
|
|
206
|
+
const cleanup = await widget.initialize?.({ model: this, experimental });
|
|
207
|
+
if (cleanup) {
|
|
208
|
+
this.#controller.signal.addEventListener("abort", cleanup);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// Store and return the render closure
|
|
212
|
+
this.#render = async (el: HTMLElement, signal: AbortSignal) => {
|
|
213
|
+
const renderCleanup = await widget.render?.({
|
|
214
|
+
model: this,
|
|
215
|
+
el,
|
|
216
|
+
experimental,
|
|
217
|
+
});
|
|
218
|
+
if (renderCleanup) {
|
|
219
|
+
// Cleanup when either the view unmounts or the model is destroyed
|
|
220
|
+
AbortSignal.any([signal, this.#controller.signal]).addEventListener(
|
|
221
|
+
"abort",
|
|
222
|
+
renderCleanup,
|
|
223
|
+
);
|
|
224
|
+
}
|
|
225
|
+
};
|
|
226
|
+
|
|
227
|
+
return this.#render;
|
|
228
|
+
},
|
|
229
|
+
destroy: () => {
|
|
230
|
+
this.#controller.abort();
|
|
231
|
+
},
|
|
232
|
+
};
|
|
93
233
|
|
|
94
234
|
off(eventName?: string | null, callback?: EventHandler | null): void {
|
|
95
235
|
if (!eventName) {
|
|
96
|
-
this
|
|
236
|
+
this.#listeners = {};
|
|
97
237
|
return;
|
|
98
238
|
}
|
|
99
239
|
|
|
100
240
|
if (!callback) {
|
|
101
|
-
this
|
|
241
|
+
this.#listeners[eventName] = new Set();
|
|
102
242
|
return;
|
|
103
243
|
}
|
|
104
244
|
|
|
105
|
-
this
|
|
245
|
+
this.#listeners[eventName]?.delete(callback);
|
|
106
246
|
}
|
|
107
247
|
|
|
108
248
|
send(
|
|
109
249
|
content: any,
|
|
110
250
|
callbacks?: any,
|
|
111
|
-
|
|
112
|
-
): void {
|
|
113
|
-
const
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
251
|
+
buffers?: ArrayBuffer[] | ArrayBufferView[],
|
|
252
|
+
): Promise<void> {
|
|
253
|
+
const dataViews = (buffers ?? []).map((buf) =>
|
|
254
|
+
buf instanceof ArrayBuffer
|
|
255
|
+
? new DataView(buf)
|
|
256
|
+
: new DataView(buf.buffer, buf.byteOffset, buf.byteLength),
|
|
257
|
+
);
|
|
258
|
+
return this.#comm
|
|
259
|
+
.sendCustomMessage(content, dataViews)
|
|
260
|
+
.then(() => callbacks?.());
|
|
121
261
|
}
|
|
122
262
|
|
|
123
263
|
widget_manager = {
|
|
124
|
-
async get_model<TT extends
|
|
125
|
-
model_id:
|
|
264
|
+
async get_model<TT extends ModelState>(
|
|
265
|
+
model_id: WidgetModelId,
|
|
126
266
|
): Promise<AnyModel<TT>> {
|
|
127
267
|
const model = await Model._modelManager.get(model_id);
|
|
128
268
|
if (!model) {
|
|
@@ -135,31 +275,52 @@ export class Model<T extends Record<string, any>> implements AnyModel<T> {
|
|
|
135
275
|
};
|
|
136
276
|
|
|
137
277
|
get<K extends keyof T>(key: K): T[K] {
|
|
138
|
-
return this
|
|
278
|
+
return this.#data[key];
|
|
139
279
|
}
|
|
140
280
|
|
|
141
281
|
set<K extends keyof T>(key: K, value: T[K]): void {
|
|
142
|
-
this
|
|
143
|
-
this
|
|
144
|
-
this
|
|
145
|
-
this
|
|
282
|
+
this.#data = { ...this.#data, [key]: value };
|
|
283
|
+
this.#dirtyFields.set(key, value);
|
|
284
|
+
this.#emit(`change:${key as K & string}`, value);
|
|
285
|
+
this.#emitAnyChange();
|
|
146
286
|
}
|
|
147
287
|
|
|
148
288
|
save_changes(): void {
|
|
149
|
-
if (this
|
|
289
|
+
if (this.#dirtyFields.size === 0) {
|
|
150
290
|
return;
|
|
151
291
|
}
|
|
152
292
|
// Only send the dirty fields, not the entire state.
|
|
153
293
|
const partialData = Object.fromEntries(
|
|
154
|
-
this
|
|
294
|
+
this.#dirtyFields.entries(),
|
|
155
295
|
) as Partial<T>;
|
|
156
296
|
|
|
157
297
|
// Clear the dirty fields to avoid sending again.
|
|
158
|
-
this
|
|
159
|
-
this.
|
|
298
|
+
this.#dirtyFields.clear();
|
|
299
|
+
this.#comm.sendUpdate(partialData);
|
|
160
300
|
}
|
|
161
301
|
|
|
162
|
-
|
|
302
|
+
on(eventName: string, callback: EventHandler): void {
|
|
303
|
+
if (!this.#listeners[eventName]) {
|
|
304
|
+
this.#listeners[eventName] = new Set();
|
|
305
|
+
}
|
|
306
|
+
this.#listeners[eventName].add(callback);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
#emit<K extends keyof T>(event: `change:${K & string}`, value: T[K]) {
|
|
310
|
+
if (!this.#listeners[event]) {
|
|
311
|
+
return;
|
|
312
|
+
}
|
|
313
|
+
const listeners = this.#listeners[event];
|
|
314
|
+
for (const listener of listeners) {
|
|
315
|
+
try {
|
|
316
|
+
listener(value);
|
|
317
|
+
} catch (error) {
|
|
318
|
+
Logger.error("Error emitting event", error);
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
#updateAndEmitDiffs(value: T) {
|
|
163
324
|
if (value == null) {
|
|
164
325
|
return;
|
|
165
326
|
}
|
|
@@ -167,7 +328,7 @@ export class Model<T extends Record<string, any>> implements AnyModel<T> {
|
|
|
167
328
|
Object.keys(value).forEach((key) => {
|
|
168
329
|
const k = key as keyof T;
|
|
169
330
|
// Shallow equal since these can be large objects
|
|
170
|
-
if (this
|
|
331
|
+
if (this.#data[k] !== value[k]) {
|
|
171
332
|
this.set(k, value[k]);
|
|
172
333
|
}
|
|
173
334
|
});
|
|
@@ -177,175 +338,139 @@ export class Model<T extends Record<string, any>> implements AnyModel<T> {
|
|
|
177
338
|
* When receiving a message from the backend.
|
|
178
339
|
* We want to notify all listeners with `msg:custom`
|
|
179
340
|
*/
|
|
180
|
-
|
|
181
|
-
message:
|
|
341
|
+
#emitCustomMessage(
|
|
342
|
+
message: Extract<AnyWidgetMessage, { method: "custom" }>,
|
|
182
343
|
buffers: readonly DataView[] = [],
|
|
183
|
-
)
|
|
184
|
-
const
|
|
185
|
-
if (
|
|
186
|
-
|
|
187
|
-
switch (data.method) {
|
|
188
|
-
case "update":
|
|
189
|
-
this.updateAndEmitDiffs(
|
|
190
|
-
decodeFromWire<T>({
|
|
191
|
-
state: data.state as T,
|
|
192
|
-
bufferPaths: data.buffer_paths ?? [],
|
|
193
|
-
buffers,
|
|
194
|
-
}),
|
|
195
|
-
);
|
|
196
|
-
break;
|
|
197
|
-
case "custom":
|
|
198
|
-
this.listeners["msg:custom"]?.forEach((cb) =>
|
|
199
|
-
cb(data.content, buffers),
|
|
200
|
-
);
|
|
201
|
-
break;
|
|
202
|
-
case "open":
|
|
203
|
-
this.updateAndEmitDiffs(
|
|
204
|
-
decodeFromWire<T>({
|
|
205
|
-
state: data.state as T,
|
|
206
|
-
bufferPaths: data.buffer_paths ?? [],
|
|
207
|
-
buffers,
|
|
208
|
-
}),
|
|
209
|
-
);
|
|
210
|
-
break;
|
|
211
|
-
case "echo_update":
|
|
212
|
-
// We don't need to do anything with this message
|
|
213
|
-
break;
|
|
214
|
-
default:
|
|
215
|
-
Logger.error("[anywidget] Unknown message method", data.method);
|
|
216
|
-
break;
|
|
217
|
-
}
|
|
218
|
-
} else {
|
|
219
|
-
Logger.error("Failed to parse message", response.error);
|
|
220
|
-
Logger.error("Message", message);
|
|
344
|
+
) {
|
|
345
|
+
const listeners = this.#listeners["msg:custom"];
|
|
346
|
+
if (!listeners) {
|
|
347
|
+
return;
|
|
221
348
|
}
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
349
|
+
for (const listener of listeners) {
|
|
350
|
+
try {
|
|
351
|
+
listener(message.content, buffers);
|
|
352
|
+
} catch (error) {
|
|
353
|
+
Logger.error("Error emitting event", error);
|
|
354
|
+
}
|
|
227
355
|
}
|
|
228
|
-
this.listeners[eventName].add(callback);
|
|
229
356
|
}
|
|
230
357
|
|
|
231
|
-
|
|
232
|
-
|
|
358
|
+
// Debounce 0 to send off one request in a single frame
|
|
359
|
+
#emitAnyChange = debounce(() => {
|
|
360
|
+
const listeners = this.#listeners[this.#ANY_CHANGE_EVENT];
|
|
361
|
+
if (!listeners) {
|
|
233
362
|
return;
|
|
234
363
|
}
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
364
|
+
for (const listener of listeners) {
|
|
365
|
+
try {
|
|
366
|
+
listener();
|
|
367
|
+
} catch (error) {
|
|
368
|
+
Logger.error("Error emitting event", error);
|
|
369
|
+
}
|
|
370
|
+
}
|
|
241
371
|
}, 0);
|
|
242
372
|
}
|
|
243
373
|
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
if (msg == null) {
|
|
276
|
-
return false;
|
|
277
|
-
}
|
|
374
|
+
/**
|
|
375
|
+
* Handle an incoming model lifecycle notification from the backend.
|
|
376
|
+
*
|
|
377
|
+
* Messages are dispatched by method type:
|
|
378
|
+
* - "open": Initialize a new model or update existing one with initial state
|
|
379
|
+
* - "update": Update model state with new values
|
|
380
|
+
* - "custom": Forward custom message to model listeners
|
|
381
|
+
* - "close": Remove model from manager
|
|
382
|
+
*/
|
|
383
|
+
export async function handleWidgetMessage(
|
|
384
|
+
modelManager: ModelManager,
|
|
385
|
+
notification: NotificationMessageData<"model-lifecycle">,
|
|
386
|
+
): Promise<void> {
|
|
387
|
+
const modelId = notification.model_id as WidgetModelId;
|
|
388
|
+
const msg = notification.message;
|
|
389
|
+
|
|
390
|
+
Logger.debug("AnyWidget message", msg);
|
|
391
|
+
|
|
392
|
+
// Decode base64 buffers to DataViews (present in open/update/custom messages)
|
|
393
|
+
const base64Buffers: Base64String[] =
|
|
394
|
+
"buffers" in msg ? (msg.buffers as Base64String[]) : [];
|
|
395
|
+
const buffers = base64Buffers.map(base64ToDataView);
|
|
396
|
+
|
|
397
|
+
switch (msg.method) {
|
|
398
|
+
case "open": {
|
|
399
|
+
const { state, buffer_paths = [] } = msg;
|
|
400
|
+
const stateWithBuffers = decodeFromWire({
|
|
401
|
+
state,
|
|
402
|
+
bufferPaths: buffer_paths,
|
|
403
|
+
buffers,
|
|
404
|
+
});
|
|
278
405
|
|
|
279
|
-
|
|
280
|
-
|
|
406
|
+
// Check if a model already exists (created by the plugin using model_id reference)
|
|
407
|
+
// If so, just update its state instead of creating a duplicate
|
|
408
|
+
const existingModel = modelManager.getSync(modelId);
|
|
409
|
+
if (existingModel) {
|
|
410
|
+
getMarimoInternal(existingModel).updateAndEmitDiffs(stateWithBuffers);
|
|
411
|
+
return;
|
|
412
|
+
}
|
|
281
413
|
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
414
|
+
const model = new Model(stateWithBuffers, {
|
|
415
|
+
async sendUpdate(changeData) {
|
|
416
|
+
const { state, buffers, bufferPaths } =
|
|
417
|
+
serializeBuffersToBase64(changeData);
|
|
418
|
+
await getRequestClient().sendModelValue({
|
|
419
|
+
modelId,
|
|
420
|
+
message: { method: "update", state, bufferPaths },
|
|
421
|
+
buffers,
|
|
422
|
+
});
|
|
423
|
+
},
|
|
424
|
+
async sendCustomMessage(content, buffers) {
|
|
425
|
+
await getRequestClient().sendModelValue({
|
|
426
|
+
modelId,
|
|
427
|
+
message: { method: "custom", content },
|
|
428
|
+
buffers: buffers.map(dataViewToBase64),
|
|
429
|
+
});
|
|
430
|
+
},
|
|
431
|
+
});
|
|
432
|
+
modelManager.set(modelId, model);
|
|
433
|
+
return;
|
|
434
|
+
}
|
|
297
435
|
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
436
|
+
case "custom": {
|
|
437
|
+
const model = await modelManager.get(modelId);
|
|
438
|
+
// For custom messages, we need to reconstruct the AnyWidgetMessage format
|
|
439
|
+
getMarimoInternal(model).emitCustomMessage(
|
|
440
|
+
{ method: "custom", content: msg.content },
|
|
441
|
+
buffers,
|
|
442
|
+
);
|
|
443
|
+
return;
|
|
444
|
+
}
|
|
303
445
|
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
446
|
+
case "close": {
|
|
447
|
+
const model = modelManager.getSync(modelId);
|
|
448
|
+
if (model) {
|
|
449
|
+
getMarimoInternal(model).destroy();
|
|
450
|
+
}
|
|
451
|
+
modelManager.delete(modelId);
|
|
452
|
+
return;
|
|
453
|
+
}
|
|
308
454
|
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
});
|
|
315
|
-
|
|
316
|
-
if (method === "open") {
|
|
317
|
-
const handleDataChange = (changeData: Record<string, any>) => {
|
|
318
|
-
const { state, buffers, bufferPaths } =
|
|
319
|
-
serializeBuffersToBase64(changeData);
|
|
320
|
-
getRequestClient().sendModelValue({
|
|
321
|
-
modelId: modelId,
|
|
322
|
-
message: {
|
|
323
|
-
state,
|
|
324
|
-
bufferPaths,
|
|
325
|
-
},
|
|
455
|
+
case "update": {
|
|
456
|
+
const { state, buffer_paths = [] } = msg;
|
|
457
|
+
const stateWithBuffers = decodeFromWire({
|
|
458
|
+
state,
|
|
459
|
+
bufferPaths: buffer_paths,
|
|
326
460
|
buffers,
|
|
327
461
|
});
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
handleDataChange,
|
|
333
|
-
throwNotImplemented,
|
|
334
|
-
new Set(),
|
|
335
|
-
);
|
|
336
|
-
modelManager.set(modelId, model);
|
|
337
|
-
return;
|
|
338
|
-
}
|
|
462
|
+
const model = await modelManager.get(modelId);
|
|
463
|
+
getMarimoInternal(model).updateAndEmitDiffs(stateWithBuffers);
|
|
464
|
+
return;
|
|
465
|
+
}
|
|
339
466
|
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
model.updateAndEmitDiffs(stateWithBuffers);
|
|
343
|
-
return;
|
|
467
|
+
default:
|
|
468
|
+
assertNever(msg);
|
|
344
469
|
}
|
|
345
|
-
|
|
346
|
-
assertNever(method);
|
|
347
470
|
}
|
|
348
471
|
|
|
472
|
+
repl(MODEL_MANAGER, "MODEL_MANAGER");
|
|
473
|
+
|
|
349
474
|
export const visibleForTesting = {
|
|
350
475
|
ModelManager,
|
|
351
476
|
};
|