@marimo-team/islands 0.23.15-dev30 → 0.23.15-dev32
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-CjNAy01L.js +1 -0
- package/dist/assets/worker-B3XPCb6y.js +98 -0
- package/dist/{chat-ui-CbyDW7Rb.js → chat-ui-C0wkSwxB.js} +33 -33
- package/dist/{code-visibility-D1Y9p_ja.js → code-visibility-CEHWVGz1.js} +162 -162
- package/dist/{html-to-image-CtsZpKdg.js → html-to-image-D8Q5SLSJ.js} +119 -118
- package/dist/main.js +1178 -1075
- package/dist/{process-output-DApSJpc6.js → process-output-C_A12w9J.js} +1 -1
- package/dist/{reveal-component-Clfz4vIR.js → reveal-component-kLDhMoa2.js} +2 -2
- package/package.json +1 -1
- package/src/core/islands/__tests__/bridge.test.ts +341 -46
- package/src/core/islands/__tests__/parse.test.ts +105 -0
- package/src/core/islands/bootstrap.ts +8 -3
- package/src/core/islands/bridge.ts +116 -23
- package/src/core/islands/components/__tests__/web-components.test.tsx +214 -0
- package/src/core/islands/components/web-components.tsx +76 -15
- package/src/core/islands/constants.ts +1 -0
- package/src/core/islands/main.ts +35 -3
- package/src/core/islands/parse.ts +70 -23
- package/src/core/islands/worker/__tests__/controller.test.ts +173 -0
- package/src/core/islands/worker/worker.tsx +145 -57
- package/src/core/wasm/worker/bootstrap.ts +113 -20
- package/dist/assets/__vite-browser-external-BQCLNwri.js +0 -1
- package/dist/assets/worker-DAWRHcPq.js +0 -73
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
2
|
|
|
3
3
|
import type { PyodideInterface } from "pyodide";
|
|
4
|
+
import type { PyProxy } from "pyodide/ffi";
|
|
4
5
|
import {
|
|
5
6
|
createRPC,
|
|
6
7
|
createRPCRequestHandler,
|
|
@@ -16,7 +17,6 @@ import { MessageBuffer } from "@/core/wasm/worker/message-buffer";
|
|
|
16
17
|
import type { RawBridge, SerializedBridge } from "@/core/wasm/worker/types";
|
|
17
18
|
import type { JsonString } from "@/utils/json/base64";
|
|
18
19
|
import { Logger } from "@/utils/Logger";
|
|
19
|
-
import { Deferred } from "../../../utils/Deferred";
|
|
20
20
|
import { prettyError } from "../../../utils/errors";
|
|
21
21
|
import { invariant } from "../../../utils/invariant";
|
|
22
22
|
import { ReadonlyWasmController } from "./controller";
|
|
@@ -50,59 +50,128 @@ const messageBuffer = new MessageBuffer(
|
|
|
50
50
|
rpc.send.kernelMessage({ message });
|
|
51
51
|
},
|
|
52
52
|
);
|
|
53
|
-
|
|
53
|
+
interface SessionRequest {
|
|
54
|
+
code: string;
|
|
55
|
+
appId: string;
|
|
56
|
+
sessionGeneration: number;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
let activeSession:
|
|
60
|
+
| (Omit<SessionRequest, "code"> & { bridge: SerializedBridge })
|
|
61
|
+
| undefined;
|
|
62
|
+
let sessionQueue = Promise.resolve();
|
|
63
|
+
|
|
64
|
+
async function startSession(
|
|
65
|
+
opts: SessionRequest,
|
|
66
|
+
replace: boolean,
|
|
67
|
+
): Promise<void> {
|
|
68
|
+
await pyodideReadyPromise;
|
|
69
|
+
|
|
70
|
+
try {
|
|
71
|
+
invariant(self.controller, "Controller not loaded");
|
|
72
|
+
if (replace) {
|
|
73
|
+
await stopActiveSession();
|
|
74
|
+
}
|
|
75
|
+
const notebook = await self.controller.mountFilesystem({
|
|
76
|
+
code: opts.code,
|
|
77
|
+
filename: `app-${opts.appId}.py`,
|
|
78
|
+
});
|
|
79
|
+
const nextBridge = await self.controller.startSession({
|
|
80
|
+
...notebook,
|
|
81
|
+
onMessage: messageBuffer.push,
|
|
82
|
+
});
|
|
83
|
+
if (activeSession) {
|
|
84
|
+
(nextBridge as unknown as PyProxy).destroy();
|
|
85
|
+
} else {
|
|
86
|
+
activeSession = {
|
|
87
|
+
appId: opts.appId,
|
|
88
|
+
bridge: nextBridge,
|
|
89
|
+
sessionGeneration: opts.sessionGeneration,
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
rpc.send.initialized({});
|
|
93
|
+
} catch (error) {
|
|
94
|
+
rpc.send.initializedError({
|
|
95
|
+
error: prettyError(error),
|
|
96
|
+
});
|
|
97
|
+
throw error;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
async function stopActiveSession(): Promise<void> {
|
|
102
|
+
const session = activeSession;
|
|
103
|
+
await self.controller.stopSession();
|
|
104
|
+
activeSession = undefined;
|
|
105
|
+
(session?.bridge as unknown as PyProxy | undefined)?.destroy();
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function enqueueSession<T>(operation: () => Promise<T>): Promise<T> {
|
|
109
|
+
const result = sessionQueue.then(operation);
|
|
110
|
+
sessionQueue = result.then(
|
|
111
|
+
() => undefined,
|
|
112
|
+
() => undefined,
|
|
113
|
+
);
|
|
114
|
+
return result;
|
|
115
|
+
}
|
|
54
116
|
|
|
55
117
|
// Handle RPC requests
|
|
56
118
|
const requestHandler = createRPCRequestHandler({
|
|
57
119
|
/**
|
|
58
120
|
* Start the session
|
|
59
121
|
*/
|
|
60
|
-
startSession: async (opts:
|
|
61
|
-
await
|
|
122
|
+
startSession: async (opts: SessionRequest) => {
|
|
123
|
+
await enqueueSession(() => startSession(opts, false));
|
|
124
|
+
},
|
|
62
125
|
|
|
63
|
-
|
|
126
|
+
replaceSession: async (opts: SessionRequest) => {
|
|
127
|
+
await enqueueSession(() => startSession(opts, true));
|
|
128
|
+
},
|
|
129
|
+
|
|
130
|
+
stopSession: async (opts: { appId: string; sessionGeneration: number }) => {
|
|
131
|
+
await enqueueSession(async () => {
|
|
132
|
+
if (
|
|
133
|
+
activeSession?.appId !== opts.appId ||
|
|
134
|
+
activeSession?.sessionGeneration !== opts.sessionGeneration
|
|
135
|
+
) {
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
64
138
|
invariant(self.controller, "Controller not loaded");
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
filename: `app-${opts.appId}.py`,
|
|
68
|
-
});
|
|
69
|
-
const bridge = await self.controller.startSession({
|
|
70
|
-
...notebook,
|
|
71
|
-
onMessage: messageBuffer.push,
|
|
72
|
-
});
|
|
73
|
-
bridgeReady.resolve(bridge);
|
|
74
|
-
rpc.send.initialized({});
|
|
75
|
-
} catch (error) {
|
|
76
|
-
rpc.send.initializedError({
|
|
77
|
-
error: prettyError(error),
|
|
78
|
-
});
|
|
79
|
-
}
|
|
80
|
-
return;
|
|
139
|
+
await stopActiveSession();
|
|
140
|
+
});
|
|
81
141
|
},
|
|
82
142
|
|
|
83
143
|
/**
|
|
84
144
|
* Load packages
|
|
85
145
|
*/
|
|
86
|
-
loadPackages: async (
|
|
146
|
+
loadPackages: async (opts: {
|
|
147
|
+
appId: string;
|
|
148
|
+
code: string;
|
|
149
|
+
sessionGeneration: number;
|
|
150
|
+
}) => {
|
|
87
151
|
await pyodideReadyPromise; // Make sure loading is done
|
|
152
|
+
await enqueueSession(async () => {
|
|
153
|
+
requireActiveBridge(opts);
|
|
154
|
+
|
|
155
|
+
let { code } = opts;
|
|
88
156
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
157
|
+
if (shouldLoadDuckDBPackages(code)) {
|
|
158
|
+
// Add pandas and duckdb to the code for mo.sql and for remote duckdb sources
|
|
159
|
+
code = `import pandas\n${code}`;
|
|
160
|
+
code = `import duckdb\n${code}`;
|
|
161
|
+
code = `import sqlglot\n${code}`;
|
|
162
|
+
|
|
163
|
+
// Polars + SQL requires pyarrow, and installing
|
|
164
|
+
// after notebook load does not work. As a heuristic,
|
|
165
|
+
// if it appears that the notebook uses polars, add pyarrow.
|
|
166
|
+
if (code.includes("polars")) {
|
|
167
|
+
code = `import pyarrow\n${code}`;
|
|
168
|
+
}
|
|
100
169
|
}
|
|
101
|
-
}
|
|
102
170
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
171
|
+
await self.pyodide.loadPackagesFromImports(code, {
|
|
172
|
+
messageCallback: Logger.log,
|
|
173
|
+
errorCallback: Logger.error,
|
|
174
|
+
});
|
|
106
175
|
});
|
|
107
176
|
},
|
|
108
177
|
|
|
@@ -110,37 +179,56 @@ const requestHandler = createRPCRequestHandler({
|
|
|
110
179
|
* Call a function on the bridge
|
|
111
180
|
*/
|
|
112
181
|
bridge: async (opts: {
|
|
182
|
+
appId: string;
|
|
113
183
|
functionName: keyof RawBridge;
|
|
114
184
|
payload: {} | undefined | null;
|
|
185
|
+
sessionGeneration: number;
|
|
115
186
|
}) => {
|
|
116
187
|
await pyodideReadyPromise; // Make sure loading is done
|
|
117
188
|
|
|
118
189
|
const { functionName, payload } = opts;
|
|
119
190
|
|
|
120
191
|
// Perform the function call to the Python bridge
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
192
|
+
return enqueueSession(async () => {
|
|
193
|
+
const bridge = requireActiveBridge(opts);
|
|
194
|
+
|
|
195
|
+
// Serialize the payload
|
|
196
|
+
const payloadString =
|
|
197
|
+
payload == null
|
|
198
|
+
? null
|
|
199
|
+
: typeof payload === "string"
|
|
200
|
+
? payload
|
|
201
|
+
: JSON.stringify(payload);
|
|
202
|
+
|
|
203
|
+
// Make the request
|
|
204
|
+
const response =
|
|
205
|
+
payloadString == null
|
|
206
|
+
? // @ts-expect-error ehh TypeScript
|
|
207
|
+
await bridge[functionName]()
|
|
208
|
+
: // @ts-expect-error ehh TypeScript
|
|
209
|
+
await bridge[functionName](payloadString);
|
|
210
|
+
|
|
211
|
+
// Post the response back to the main thread
|
|
212
|
+
return typeof response === "string" ? JSON.parse(response) : response;
|
|
213
|
+
});
|
|
141
214
|
},
|
|
142
215
|
});
|
|
143
216
|
|
|
217
|
+
function requireActiveBridge(opts: {
|
|
218
|
+
appId: string;
|
|
219
|
+
sessionGeneration: number;
|
|
220
|
+
}): SerializedBridge {
|
|
221
|
+
const session = activeSession;
|
|
222
|
+
if (
|
|
223
|
+
!session ||
|
|
224
|
+
opts.appId !== session.appId ||
|
|
225
|
+
opts.sessionGeneration !== session.sessionGeneration
|
|
226
|
+
) {
|
|
227
|
+
throw new Error("The marimo app session is no longer active.");
|
|
228
|
+
}
|
|
229
|
+
return session.bridge;
|
|
230
|
+
}
|
|
231
|
+
|
|
144
232
|
// create the iframe's schema
|
|
145
233
|
export type WorkerSchema = RPCSchema<
|
|
146
234
|
{
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
2
|
import { loadPyodide, type PyodideInterface } from "pyodide";
|
|
3
|
+
import type { PyCallable, PyProxy } from "pyodide/ffi";
|
|
3
4
|
import type { UserConfig } from "@/core/config/config-schema";
|
|
4
5
|
import type { NotificationPayload } from "@/core/kernel/messages";
|
|
5
6
|
import type { JsonString } from "@/utils/json/base64";
|
|
@@ -12,6 +13,12 @@ import type { SerializedBridge, WasmController } from "./types";
|
|
|
12
13
|
import { shouldLoadDuckDBPackages } from "../utils";
|
|
13
14
|
|
|
14
15
|
const MAKE_SNAPSHOT = false;
|
|
16
|
+
type SessionResources = [
|
|
17
|
+
bridge: PyProxy,
|
|
18
|
+
init: PyCallable,
|
|
19
|
+
packages: PyProxy,
|
|
20
|
+
stop: PyCallable,
|
|
21
|
+
];
|
|
15
22
|
|
|
16
23
|
// This class initializes the wasm environment
|
|
17
24
|
// We would like this initialization to be parallelizable
|
|
@@ -25,6 +32,9 @@ const MAKE_SNAPSHOT = false;
|
|
|
25
32
|
|
|
26
33
|
export class DefaultWasmController implements WasmController {
|
|
27
34
|
protected pyodide: PyodideInterface | null = null;
|
|
35
|
+
private packageLoadQueue = Promise.resolve();
|
|
36
|
+
private sessionGeneration = 0;
|
|
37
|
+
private activeSessionStops = new Set<PyCallable>();
|
|
28
38
|
|
|
29
39
|
get requirePyodide() {
|
|
30
40
|
invariant(this.pyodide, "Pyodide not loaded");
|
|
@@ -111,6 +121,7 @@ export class DefaultWasmController implements WasmController {
|
|
|
111
121
|
onMessage: (message: JsonString<NotificationPayload>) => void;
|
|
112
122
|
userConfig: UserConfig;
|
|
113
123
|
}): Promise<SerializedBridge> {
|
|
124
|
+
const sessionGeneration = this.sessionGeneration;
|
|
114
125
|
const { code, filename, onMessage, queryParameters, userConfig } = opts;
|
|
115
126
|
// We pass down a messenger object to the code
|
|
116
127
|
// This is used to have synchronous communication between the JS and Python code
|
|
@@ -126,47 +137,129 @@ export class DefaultWasmController implements WasmController {
|
|
|
126
137
|
|
|
127
138
|
const span = t.startSpan("startSession.runPython");
|
|
128
139
|
const nbFilename = filename || WasmFileSystem.NOTEBOOK_FILENAME;
|
|
129
|
-
const
|
|
140
|
+
const sessionResources = this.requirePyodide.runPython(
|
|
130
141
|
`
|
|
131
142
|
print("[py] Starting marimo...")
|
|
132
143
|
import asyncio
|
|
144
|
+
import gc
|
|
133
145
|
import js
|
|
134
146
|
from marimo._pyodide.bootstrap import create_session, instantiate
|
|
135
147
|
|
|
136
148
|
assert js.messenger, "messenger is not defined"
|
|
137
149
|
assert js.query_params, "query_params is not defined"
|
|
138
150
|
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
151
|
+
def create_session_resources():
|
|
152
|
+
session, bridge = create_session(
|
|
153
|
+
filename="${nbFilename}",
|
|
154
|
+
query_params=js.query_params.to_py(),
|
|
155
|
+
message_callback=js.messenger.callback,
|
|
156
|
+
user_config=js.user_config.to_py(),
|
|
157
|
+
)
|
|
158
|
+
session_task = None
|
|
145
159
|
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
160
|
+
def init(auto_instantiate=True):
|
|
161
|
+
nonlocal session_task
|
|
162
|
+
instantiate(session, auto_instantiate)
|
|
163
|
+
session_task = asyncio.create_task(session.start())
|
|
149
164
|
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
165
|
+
async def stop():
|
|
166
|
+
nonlocal bridge, session, session_task
|
|
167
|
+
task = session_task
|
|
168
|
+
kernel_task = getattr(session, "kernel_task", None)
|
|
169
|
+
if kernel_task is None:
|
|
170
|
+
if task is not None:
|
|
171
|
+
task.cancel()
|
|
172
|
+
else:
|
|
173
|
+
kernel_task.stop()
|
|
174
|
+
if task is not None:
|
|
175
|
+
await asyncio.gather(task, return_exceptions=True)
|
|
176
|
+
if bridge is not None:
|
|
177
|
+
bridge.session = None
|
|
178
|
+
task = None
|
|
179
|
+
kernel_task = None
|
|
180
|
+
session_task = None
|
|
181
|
+
session = None
|
|
182
|
+
bridge = None
|
|
183
|
+
gc.collect()
|
|
153
184
|
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
span.end();
|
|
185
|
+
with open("${nbFilename}", "r") as f:
|
|
186
|
+
packages = session.find_packages(f.read())
|
|
157
187
|
|
|
158
|
-
|
|
188
|
+
return bridge, init, packages, stop
|
|
189
|
+
|
|
190
|
+
create_session_resources()`,
|
|
191
|
+
) as PyProxy;
|
|
192
|
+
span.end();
|
|
193
|
+
let bridgeProxy!: PyProxy;
|
|
194
|
+
let initSession!: PyCallable;
|
|
195
|
+
let packagesProxy!: PyProxy;
|
|
196
|
+
let stopSession!: PyCallable;
|
|
197
|
+
try {
|
|
198
|
+
[bridgeProxy, initSession, packagesProxy, stopSession] =
|
|
199
|
+
sessionResources as unknown as SessionResources;
|
|
200
|
+
} finally {
|
|
201
|
+
sessionResources.destroy();
|
|
202
|
+
}
|
|
203
|
+
let foundPackages: Set<string>;
|
|
204
|
+
try {
|
|
205
|
+
foundPackages = new Set<string>(packagesProxy.toJs());
|
|
206
|
+
} catch (error) {
|
|
207
|
+
bridgeProxy.destroy();
|
|
208
|
+
initSession.destroy();
|
|
209
|
+
stopSession.destroy();
|
|
210
|
+
throw error;
|
|
211
|
+
} finally {
|
|
212
|
+
packagesProxy.destroy();
|
|
213
|
+
}
|
|
214
|
+
this.activeSessionStops.add(stopSession);
|
|
159
215
|
|
|
160
216
|
// Fire and forget:
|
|
161
217
|
// Load notebook dependencies and instantiate the session
|
|
162
218
|
// We don't want to wait for this to finish,
|
|
163
219
|
// so we can show the initial code immediately giving
|
|
164
220
|
// a sense of responsiveness.
|
|
165
|
-
|
|
166
|
-
|
|
221
|
+
const dependenciesReady = this.packageLoadQueue.then(() => {
|
|
222
|
+
if (sessionGeneration !== this.sessionGeneration) {
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
return this.loadNotebookDeps(code, foundPackages);
|
|
167
226
|
});
|
|
227
|
+
this.packageLoadQueue = dependenciesReady.catch(() => undefined);
|
|
228
|
+
void dependenciesReady
|
|
229
|
+
.then(() => {
|
|
230
|
+
if (sessionGeneration !== this.sessionGeneration) {
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
return initSession(userConfig.runtime.auto_instantiate);
|
|
234
|
+
})
|
|
235
|
+
.catch((error: unknown) => {
|
|
236
|
+
Logger.error("Failed to load notebook dependencies", error);
|
|
237
|
+
})
|
|
238
|
+
.finally(() => initSession.destroy());
|
|
239
|
+
|
|
240
|
+
return bridgeProxy as unknown as SerializedBridge;
|
|
241
|
+
}
|
|
168
242
|
|
|
169
|
-
|
|
243
|
+
async stopSession(): Promise<void> {
|
|
244
|
+
this.sessionGeneration += 1;
|
|
245
|
+
const stops = [...this.activeSessionStops];
|
|
246
|
+
let hasFailure = false;
|
|
247
|
+
let firstFailure: unknown;
|
|
248
|
+
for (const stop of stops) {
|
|
249
|
+
try {
|
|
250
|
+
await stop();
|
|
251
|
+
this.activeSessionStops.delete(stop);
|
|
252
|
+
stop.destroy();
|
|
253
|
+
} catch (error) {
|
|
254
|
+
if (!hasFailure) {
|
|
255
|
+
hasFailure = true;
|
|
256
|
+
firstFailure = error;
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
if (hasFailure) {
|
|
261
|
+
throw firstFailure;
|
|
262
|
+
}
|
|
170
263
|
}
|
|
171
264
|
|
|
172
265
|
private async loadNotebookDeps(code: string, foundPackages: Set<string>) {
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{t as e}from"./worker-DAWRHcPq.js";var t=e(((e,t)=>{t.exports={}}));export default t();
|