@marimo-team/islands 0.25.3-dev137 → 0.25.3-dev139
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/{ErrorBoundary-CgZ1IpNU.js → ErrorBoundary-BX9mTNLJ.js} +1 -1
- package/dist/assets/__vite-browser-external-DrmlAsRP.js +1 -0
- package/dist/assets/worker-O2XUgidQ.js +113 -0
- package/dist/{code-visibility-uIcGb1d8.js → code-visibility-gptWgGHp.js} +1 -1
- package/dist/{common-Cn-8v1pu.js → common-mU-2k6Zp.js} +1 -1
- package/dist/{data-grid-overlay-editor-CY38o06k.js → data-grid-overlay-editor-Bdyx_BTg.js} +1 -1
- package/dist/{feedback-button-C8MtB57f.js → feedback-button-BoXM4uir.js} +2 -2
- package/dist/{glide-data-editor-BK5sjGKi.js → glide-data-editor-DaXYMyzk.js} +3 -3
- package/dist/{globals-Dx5-Q1mU.js → globals-Bj4btPVp.js} +1 -1
- package/dist/main.js +29 -28
- package/dist/{number-overlay-editor-woxvu2w0.js → number-overlay-editor-DHfzwLIx.js} +1 -1
- package/dist/{reveal-component-BW15mD8s.js → reveal-component-CurCj3ks.js} +2 -2
- package/package.json +1 -1
- package/src/core/islands/__tests__/bootstrap.test.ts +17 -17
- package/src/core/islands/__tests__/bridge.test.ts +31 -26
- package/src/core/islands/bootstrap.ts +22 -51
- package/src/core/islands/bridge.ts +31 -20
- package/src/core/islands/worker/__tests__/controller.test.ts +0 -1
- package/src/core/islands/worker/controller.ts +0 -4
- package/src/core/islands/worker/worker.tsx +32 -32
- package/src/core/wasm/__tests__/bridge.test.ts +0 -1
- package/src/core/wasm/bridge.ts +0 -13
- package/src/core/wasm/rpc.ts +0 -1
- package/src/core/wasm/worker/bootstrap.ts +1 -15
- package/src/core/wasm/worker/types.ts +1 -6
- package/src/core/wasm/worker/worker.ts +4 -28
- package/src/core/websocket/__tests__/backend.test.ts +41 -13
- package/src/core/websocket/backend.ts +31 -7
- package/src/core/websocket/transports/__tests__/notebook.test.ts +1 -41
- package/src/core/websocket/transports/notebook.ts +0 -31
- package/dist/assets/__vite-browser-external-SaDYcg7x.js +0 -1
- package/dist/assets/worker-CfME3uyi.js +0 -115
- package/src/core/wasm/worker/message-buffer.ts +0 -33
|
@@ -1,11 +1,9 @@
|
|
|
1
1
|
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
2
|
import type { PyodideInterface } from "pyodide";
|
|
3
3
|
import { defaultUserConfig } from "@/core/config/config-schema";
|
|
4
|
-
import type { NotificationPayload } from "@/core/kernel/messages";
|
|
5
4
|
import { DefaultWasmController } from "@/core/wasm/worker/bootstrap";
|
|
6
5
|
import { WasmFileSystem } from "@/core/wasm/worker/fs";
|
|
7
6
|
import type { SerializedBridge } from "@/core/wasm/worker/types";
|
|
8
|
-
import type { JsonString } from "@/utils/json/base64";
|
|
9
7
|
import { Logger } from "../../../utils/Logger";
|
|
10
8
|
|
|
11
9
|
export class ReadonlyWasmController extends DefaultWasmController {
|
|
@@ -40,13 +38,11 @@ export class ReadonlyWasmController extends DefaultWasmController {
|
|
|
40
38
|
public override async startSession(opts: {
|
|
41
39
|
code: string;
|
|
42
40
|
filename: string | null;
|
|
43
|
-
onMessage: (message: JsonString<NotificationPayload>) => void;
|
|
44
41
|
}): Promise<SerializedBridge> {
|
|
45
42
|
const bridge = super.startSession({
|
|
46
43
|
queryParameters: {},
|
|
47
44
|
code: opts.code,
|
|
48
45
|
filename: opts.filename,
|
|
49
|
-
onMessage: opts.onMessage,
|
|
50
46
|
userConfig: defaultUserConfig(),
|
|
51
47
|
});
|
|
52
48
|
|
|
@@ -8,14 +8,11 @@ import {
|
|
|
8
8
|
createWorkerParentTransport,
|
|
9
9
|
type RPCSchema,
|
|
10
10
|
} from "rpc-anywhere";
|
|
11
|
-
import type { NotificationPayload } from "@/core/kernel/messages";
|
|
12
11
|
import type { ParentSchema } from "@/core/wasm/rpc";
|
|
13
12
|
import { shouldLoadDuckDBPackages } from "@/core/wasm/utils";
|
|
14
13
|
import { TRANSPORT_ID } from "@/core/wasm/worker/constants";
|
|
15
14
|
import { getPyodideVersion } from "@/core/wasm/worker/getPyodideVersion";
|
|
16
|
-
import { MessageBuffer } from "@/core/wasm/worker/message-buffer";
|
|
17
15
|
import type { RawBridge, SerializedBridge } from "@/core/wasm/worker/types";
|
|
18
|
-
import type { JsonString } from "@/utils/json/base64";
|
|
19
16
|
import { Logger } from "@/utils/Logger";
|
|
20
17
|
import { prettyError } from "../../../utils/errors";
|
|
21
18
|
import { invariant } from "../../../utils/invariant";
|
|
@@ -45,14 +42,11 @@ async function loadPyodideAndPackages() {
|
|
|
45
42
|
}
|
|
46
43
|
|
|
47
44
|
const pyodideReadyPromise = loadPyodideAndPackages();
|
|
48
|
-
|
|
49
|
-
|
|
45
|
+
/** An event of one session's notebook stream. */
|
|
46
|
+
export interface IslandsNotebookEvent {
|
|
47
|
+
event: string;
|
|
50
48
|
sessionGeneration: number;
|
|
51
49
|
}
|
|
52
|
-
|
|
53
|
-
const messageBuffer = new MessageBuffer<IslandsKernelMessage>((payload) => {
|
|
54
|
-
rpc.send.kernelMessage(payload);
|
|
55
|
-
});
|
|
56
50
|
interface SessionRequest {
|
|
57
51
|
code: string;
|
|
58
52
|
appId: string;
|
|
@@ -62,6 +56,8 @@ interface SessionRequest {
|
|
|
62
56
|
let activeSession:
|
|
63
57
|
| (Omit<SessionRequest, "code"> & { bridge: SerializedBridge })
|
|
64
58
|
| undefined;
|
|
59
|
+
/** Every running session's bridge, kept while the worker follows its notebook. */
|
|
60
|
+
const sessions = new Set<SerializedBridge>();
|
|
65
61
|
let sessionQueue = Promise.resolve();
|
|
66
62
|
|
|
67
63
|
async function startSession(
|
|
@@ -79,18 +75,12 @@ async function startSession(
|
|
|
79
75
|
code: opts.code,
|
|
80
76
|
filename: `app-${opts.appId}.py`,
|
|
81
77
|
});
|
|
82
|
-
const nextBridge = await self.controller.startSession(
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
});
|
|
89
|
-
},
|
|
90
|
-
});
|
|
91
|
-
if (activeSession) {
|
|
92
|
-
(nextBridge as unknown as PyProxy).destroy();
|
|
93
|
-
} else {
|
|
78
|
+
const nextBridge = await self.controller.startSession(notebook);
|
|
79
|
+
sessions.add(nextBridge);
|
|
80
|
+
// Follow before the session instantiates. Each subscription begins with
|
|
81
|
+
// its own kernel-ready and the session's retained notifications.
|
|
82
|
+
await follow(nextBridge, opts.sessionGeneration);
|
|
83
|
+
if (!activeSession) {
|
|
94
84
|
activeSession = {
|
|
95
85
|
appId: opts.appId,
|
|
96
86
|
bridge: nextBridge,
|
|
@@ -107,10 +97,27 @@ async function startSession(
|
|
|
107
97
|
}
|
|
108
98
|
|
|
109
99
|
async function stopActiveSession(): Promise<void> {
|
|
110
|
-
|
|
100
|
+
// Stopping closes every session and its notebook subscriptions.
|
|
111
101
|
await self.controller.stopSession();
|
|
112
102
|
activeSession = undefined;
|
|
113
|
-
(
|
|
103
|
+
for (const bridge of sessions) {
|
|
104
|
+
(bridge as unknown as PyProxy).destroy();
|
|
105
|
+
}
|
|
106
|
+
sessions.clear();
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
async function follow(
|
|
110
|
+
bridge: SerializedBridge,
|
|
111
|
+
sessionGeneration: number,
|
|
112
|
+
): Promise<void> {
|
|
113
|
+
const notebook = bridge.notebook;
|
|
114
|
+
try {
|
|
115
|
+
await notebook.subscribe(`island-${sessionGeneration}`, null, (event) => {
|
|
116
|
+
rpc.send.notebookEvent({ event, sessionGeneration });
|
|
117
|
+
});
|
|
118
|
+
} finally {
|
|
119
|
+
notebook.destroy();
|
|
120
|
+
}
|
|
114
121
|
}
|
|
115
122
|
|
|
116
123
|
function enqueueSession<T>(operation: () => Promise<T>): Promise<T> {
|
|
@@ -243,8 +250,8 @@ export type WorkerSchema = RPCSchema<
|
|
|
243
250
|
messages: {
|
|
244
251
|
// Emitted when the worker is ready
|
|
245
252
|
ready: {};
|
|
246
|
-
// Emitted
|
|
247
|
-
|
|
253
|
+
// Emitted for each event of a session's notebook stream
|
|
254
|
+
notebookEvent: IslandsNotebookEvent;
|
|
248
255
|
// Emitted when the Pyodide is initialized
|
|
249
256
|
initialized: {};
|
|
250
257
|
// Emitted when the Pyodide fails to initialize
|
|
@@ -263,13 +270,6 @@ const rpc = createRPC<WorkerSchema, ParentSchema>({
|
|
|
263
270
|
|
|
264
271
|
rpc.send("ready", {});
|
|
265
272
|
|
|
266
|
-
/// Listeners
|
|
267
|
-
// When the consumer is ready, start the message buffer
|
|
268
|
-
rpc.addMessageListener("consumerReady", async () => {
|
|
269
|
-
await pyodideReadyPromise; // Make sure loading is done
|
|
270
|
-
messageBuffer.start();
|
|
271
|
-
});
|
|
272
|
-
|
|
273
273
|
function getMarimoVersion() {
|
|
274
274
|
return self.name; // We store the version in the worker name
|
|
275
275
|
}
|
package/src/core/wasm/bridge.ts
CHANGED
|
@@ -70,9 +70,6 @@ export class PyodideBridge implements RunRequests, EditRequests {
|
|
|
70
70
|
private pendingSessionSave: Promise<unknown> = Promise.resolve();
|
|
71
71
|
private notebookConsumers = new Map<string, (event: unknown) => void>();
|
|
72
72
|
private interruptBuffer?: Uint8Array;
|
|
73
|
-
private messageConsumer:
|
|
74
|
-
| ((message: MessageEvent<string>) => void)
|
|
75
|
-
| undefined;
|
|
76
73
|
|
|
77
74
|
public initialized = new Deferred<void>();
|
|
78
75
|
|
|
@@ -158,9 +155,6 @@ export class PyodideBridge implements RunRequests, EditRequests {
|
|
|
158
155
|
store.set(wasmInitStateAtom, { kind: "error", message: error });
|
|
159
156
|
this.initialized.reject(new Error(error));
|
|
160
157
|
});
|
|
161
|
-
this.rpc.addMessageListener("kernelMessage", ({ message }) => {
|
|
162
|
-
this.messageConsumer?.(new MessageEvent("message", { data: message }));
|
|
163
|
-
});
|
|
164
158
|
}
|
|
165
159
|
|
|
166
160
|
private async startSession() {
|
|
@@ -212,13 +206,6 @@ export class PyodideBridge implements RunRequests, EditRequests {
|
|
|
212
206
|
}
|
|
213
207
|
}
|
|
214
208
|
|
|
215
|
-
public attachMessageConsumer(
|
|
216
|
-
consumer: (message: MessageEvent<string>) => void,
|
|
217
|
-
) {
|
|
218
|
-
this.messageConsumer = consumer;
|
|
219
|
-
this.rpc.proxy.send.consumerReady({});
|
|
220
|
-
}
|
|
221
|
-
|
|
222
209
|
public async createNotebookBackend() {
|
|
223
210
|
await this.initialized.promise;
|
|
224
211
|
const { id } = await this.rpc.proxy.request.notebookInfo();
|
package/src/core/wasm/rpc.ts
CHANGED
|
@@ -2,8 +2,6 @@
|
|
|
2
2
|
import { loadPyodide, type PyodideInterface } from "pyodide";
|
|
3
3
|
import type { PyCallable, PyProxy } from "pyodide/ffi";
|
|
4
4
|
import type { UserConfig } from "@/core/config/config-schema";
|
|
5
|
-
import type { NotificationPayload } from "@/core/kernel/messages";
|
|
6
|
-
import type { JsonString } from "@/utils/json/base64";
|
|
7
5
|
import { invariant } from "../../../utils/invariant";
|
|
8
6
|
import { Logger } from "../../../utils/Logger";
|
|
9
7
|
import { WasmFileSystem } from "./fs";
|
|
@@ -142,20 +140,10 @@ export class DefaultWasmController implements WasmController {
|
|
|
142
140
|
queryParameters: Record<string, string | string[]>;
|
|
143
141
|
code: string;
|
|
144
142
|
filename: string | null;
|
|
145
|
-
onMessage: (message: JsonString<NotificationPayload>) => void;
|
|
146
143
|
userConfig: UserConfig;
|
|
147
144
|
}): Promise<SerializedBridge> {
|
|
148
145
|
const sessionGeneration = this.sessionGeneration;
|
|
149
|
-
const { code, filename,
|
|
150
|
-
// We pass down a messenger object to the code
|
|
151
|
-
// This is used to have synchronous communication between the JS and Python code
|
|
152
|
-
// Previously, we used a queue, but this would not properly flush the queue
|
|
153
|
-
// during processing of a cell's code.
|
|
154
|
-
//
|
|
155
|
-
// This adds a messenger object to the global scope (import js; js.messenger.callback)
|
|
156
|
-
self.messenger = {
|
|
157
|
-
callback: onMessage,
|
|
158
|
-
};
|
|
146
|
+
const { code, filename, queryParameters, userConfig } = opts;
|
|
159
147
|
self.query_params = queryParameters;
|
|
160
148
|
self.user_config = userConfig;
|
|
161
149
|
|
|
@@ -169,14 +157,12 @@ export class DefaultWasmController implements WasmController {
|
|
|
169
157
|
import js
|
|
170
158
|
from marimo._pyodide.bootstrap import create_session, instantiate
|
|
171
159
|
|
|
172
|
-
assert js.messenger, "messenger is not defined"
|
|
173
160
|
assert js.query_params, "query_params is not defined"
|
|
174
161
|
|
|
175
162
|
def create_session_resources():
|
|
176
163
|
session, bridge = create_session(
|
|
177
164
|
filename="${nbFilename}",
|
|
178
165
|
query_params=js.query_params.to_py(),
|
|
179
|
-
message_callback=js.messenger.callback,
|
|
180
166
|
user_config=js.user_config.to_py(),
|
|
181
167
|
)
|
|
182
168
|
session_task = None
|
|
@@ -2,11 +2,7 @@
|
|
|
2
2
|
import type { PyodideInterface } from "pyodide";
|
|
3
3
|
import type { WasmRuntimeConfig } from "../runtime-config";
|
|
4
4
|
import type { UserConfig } from "@/core/config/config-schema";
|
|
5
|
-
import type {
|
|
6
|
-
ClientCommand,
|
|
7
|
-
NotificationPayload,
|
|
8
|
-
} from "@/core/kernel/messages";
|
|
9
|
-
import type { JsonString } from "@/utils/json/base64";
|
|
5
|
+
import type { ClientCommand } from "@/core/kernel/messages";
|
|
10
6
|
import type {
|
|
11
7
|
CodeCompletionRequest,
|
|
12
8
|
CopyNotebookRequest,
|
|
@@ -69,7 +65,6 @@ export interface WasmController {
|
|
|
69
65
|
code: string;
|
|
70
66
|
filename: string | null;
|
|
71
67
|
userConfig: UserConfig;
|
|
72
|
-
onMessage: (message: JsonString<NotificationPayload>) => void;
|
|
73
68
|
}): Promise<SerializedBridge>;
|
|
74
69
|
}
|
|
75
70
|
|
|
@@ -9,14 +9,11 @@ import {
|
|
|
9
9
|
type RPCSchema,
|
|
10
10
|
} from "rpc-anywhere";
|
|
11
11
|
import type { UserConfig } from "@/core/config/config-schema";
|
|
12
|
-
import type { NotificationPayload } from "@/core/kernel/messages";
|
|
13
12
|
import type {
|
|
14
13
|
ListPackagesResponse,
|
|
15
14
|
PackageOperationResponse,
|
|
16
15
|
SaveNotebookRequest,
|
|
17
16
|
} from "@/core/network/types";
|
|
18
|
-
import type { JsonString } from "@/utils/json/base64";
|
|
19
|
-
import { once } from "@/utils/once";
|
|
20
17
|
import { decodeUtf8 } from "@/utils/strings";
|
|
21
18
|
import { Deferred } from "../../../utils/Deferred";
|
|
22
19
|
import { prettyError } from "../../../utils/errors";
|
|
@@ -28,7 +25,6 @@ import { TRANSPORT_ID } from "./constants";
|
|
|
28
25
|
import { WasmFileSystem } from "./fs";
|
|
29
26
|
import { getController } from "./getController";
|
|
30
27
|
import { getPyodideVersion } from "./getPyodideVersion";
|
|
31
|
-
import { MessageBuffer } from "./message-buffer";
|
|
32
28
|
import { t } from "./tracer";
|
|
33
29
|
import type {
|
|
34
30
|
BridgePayload,
|
|
@@ -73,11 +69,6 @@ async function loadPyodideAndPackages() {
|
|
|
73
69
|
}
|
|
74
70
|
}
|
|
75
71
|
|
|
76
|
-
const messageBuffer = new MessageBuffer(
|
|
77
|
-
(message: JsonString<NotificationPayload>) => {
|
|
78
|
-
rpc.send.kernelMessage({ message });
|
|
79
|
-
},
|
|
80
|
-
);
|
|
81
72
|
const bridgeReady = new Deferred<SerializedBridge>();
|
|
82
73
|
let started = false;
|
|
83
74
|
|
|
@@ -155,12 +146,6 @@ const requestHandler = createRPCRequestHandler({
|
|
|
155
146
|
const startSession = t.wrapAsync(
|
|
156
147
|
self.controller.startSession.bind(self.controller),
|
|
157
148
|
);
|
|
158
|
-
const initializeOnce = once(() => {
|
|
159
|
-
rpc.send.initializingMessage({
|
|
160
|
-
message: "Initializing notebook...",
|
|
161
|
-
});
|
|
162
|
-
rpc.send.initialized({});
|
|
163
|
-
});
|
|
164
149
|
rpc.send.initializingMessage({
|
|
165
150
|
message: "Loading notebook and dependencies...",
|
|
166
151
|
});
|
|
@@ -169,12 +154,12 @@ const requestHandler = createRPCRequestHandler({
|
|
|
169
154
|
filename: opts.filename,
|
|
170
155
|
queryParameters: opts.queryParameters,
|
|
171
156
|
userConfig: opts.userConfig,
|
|
172
|
-
onMessage: (msg) => {
|
|
173
|
-
initializeOnce();
|
|
174
|
-
messageBuffer.push(msg);
|
|
175
|
-
},
|
|
176
157
|
});
|
|
177
158
|
bridgeReady.resolve(bridge);
|
|
159
|
+
rpc.send.initializingMessage({
|
|
160
|
+
message: "Initializing notebook...",
|
|
161
|
+
});
|
|
162
|
+
rpc.send.initialized({});
|
|
178
163
|
workerInitSpan.end("ok");
|
|
179
164
|
} catch (error) {
|
|
180
165
|
rpc.send.initializedError({
|
|
@@ -385,8 +370,6 @@ export type WorkerSchema = RPCSchema<
|
|
|
385
370
|
notebookEvent: { id: string; event: string };
|
|
386
371
|
// Emitted when the worker is ready
|
|
387
372
|
ready: {};
|
|
388
|
-
// Emitted when the kernel sends a message
|
|
389
|
-
kernelMessage: { message: JsonString<NotificationPayload> };
|
|
390
373
|
// Emitted when the Pyodide is initialized
|
|
391
374
|
initialized: {};
|
|
392
375
|
// Emitted when the Pyodide is initializing, with new messages
|
|
@@ -409,13 +392,6 @@ rpc.addMessageListener("bootstrap", (config) => runtimeConfig.resolve(config));
|
|
|
409
392
|
|
|
410
393
|
rpc.send("ready", {});
|
|
411
394
|
|
|
412
|
-
/// Listeners
|
|
413
|
-
// When the consumer is ready, start the message buffer
|
|
414
|
-
rpc.addMessageListener("consumerReady", async () => {
|
|
415
|
-
await pyodideReadyPromise; // Make sure loading is done
|
|
416
|
-
messageBuffer.start();
|
|
417
|
-
});
|
|
418
|
-
|
|
419
395
|
const namesThatRequireSync = new Set<keyof RawBridge>([
|
|
420
396
|
"save",
|
|
421
397
|
"save_app_config",
|
|
@@ -1,22 +1,23 @@
|
|
|
1
1
|
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
2
|
|
|
3
|
-
import { NotebookEvents } from "@marimo-team/client";
|
|
3
|
+
import { NotebookEvents, StreamStopped } from "@marimo-team/client";
|
|
4
4
|
import { createStore } from "jotai/vanilla";
|
|
5
5
|
import { afterEach, describe, expect, it, onTestFinished, vi } from "vitest";
|
|
6
6
|
import { Deferred } from "@/utils/Deferred";
|
|
7
7
|
import { ConnectionState } from "../../host/connection";
|
|
8
|
+
import { type FakeEdit, fakeBackend } from "../../host/__tests__/fixtures";
|
|
8
9
|
import { createNotebookReplica } from "../../host/replica";
|
|
9
10
|
import { createEditorBackend } from "../backend";
|
|
10
11
|
|
|
11
|
-
const wasm = vi.hoisted(() => ({ enabled: false,
|
|
12
|
+
const wasm = vi.hoisted(() => ({ enabled: false, create: vi.fn() }));
|
|
12
13
|
vi.mock("../../wasm/utils", () => ({ isWasm: () => wasm.enabled }));
|
|
13
14
|
vi.mock("../../wasm/bridge", () => ({
|
|
14
|
-
PyodideBridge: { INSTANCE: {
|
|
15
|
+
PyodideBridge: { INSTANCE: { createNotebookBackend: wasm.create } },
|
|
15
16
|
}));
|
|
16
17
|
|
|
17
18
|
afterEach(() => {
|
|
18
19
|
wasm.enabled = false;
|
|
19
|
-
wasm.
|
|
20
|
+
wasm.create.mockReset();
|
|
20
21
|
vi.unstubAllGlobals();
|
|
21
22
|
});
|
|
22
23
|
|
|
@@ -55,6 +56,8 @@ describe("createEditorBackend", () => {
|
|
|
55
56
|
["the default", options, "socket"],
|
|
56
57
|
] as const)("follows %s", async (_name, chosen, expected) => {
|
|
57
58
|
wasm.enabled = expected === "wasm";
|
|
59
|
+
const worker = vi.fn(() => new Promise<void>(() => undefined));
|
|
60
|
+
wasm.create.mockResolvedValue(fakeBackend({ transport: worker }));
|
|
58
61
|
const fetch = vi.fn(() => new Promise<Response>(() => undefined));
|
|
59
62
|
const opened = sockets();
|
|
60
63
|
vi.stubGlobal("fetch", fetch);
|
|
@@ -62,9 +65,8 @@ describe("createEditorBackend", () => {
|
|
|
62
65
|
await vi.waitFor(() =>
|
|
63
66
|
expect(
|
|
64
67
|
{
|
|
65
|
-
idle:
|
|
66
|
-
|
|
67
|
-
wasm: wasm.consumer.mock.calls.length === 1,
|
|
68
|
+
idle: events.status === "connected" && !wasm.create.mock.calls.length,
|
|
69
|
+
wasm: worker.mock.calls.length === 1,
|
|
68
70
|
fetch: fetch.mock.calls.length === 1,
|
|
69
71
|
socket: opened.length === 1,
|
|
70
72
|
}[expected],
|
|
@@ -107,19 +109,45 @@ describe("createEditorBackend", () => {
|
|
|
107
109
|
expect(headers.get("Idempotency-Key")).toBe("edit-1");
|
|
108
110
|
});
|
|
109
111
|
|
|
110
|
-
it
|
|
111
|
-
["a static notebook", { ...options, static: true }, false],
|
|
112
|
-
["WASM", options, true],
|
|
113
|
-
] as const)("does not edit %s", async (_name, chosen, isWasm) => {
|
|
114
|
-
wasm.enabled = isWasm;
|
|
112
|
+
it("does not edit a static notebook", async () => {
|
|
115
113
|
const fetch = vi.fn();
|
|
116
114
|
vi.stubGlobal("fetch", fetch);
|
|
117
|
-
const result = await createEditorBackend(
|
|
115
|
+
const result = await createEditorBackend({ ...options, static: true })
|
|
118
116
|
.notebook("n1")
|
|
119
117
|
.edit([], { idempotencyKey: "edit-1" });
|
|
120
118
|
expect(result).toMatchObject({ ok: false, status: 501 });
|
|
121
119
|
expect(fetch).not.toHaveBeenCalled();
|
|
122
120
|
});
|
|
121
|
+
|
|
122
|
+
it("edits the WASM notebook through the worker once its stream starts", async () => {
|
|
123
|
+
wasm.enabled = true;
|
|
124
|
+
const edit = vi.fn<FakeEdit>();
|
|
125
|
+
const transport = vi.fn(async () => undefined);
|
|
126
|
+
wasm.create.mockResolvedValue(fakeBackend({ transport, edit }));
|
|
127
|
+
const backend = createEditorBackend(options);
|
|
128
|
+
expect(wasm.create).not.toHaveBeenCalled();
|
|
129
|
+
expect(() => backend.notebook("n1")).toThrow("has not started");
|
|
130
|
+
|
|
131
|
+
const attempt = { cursor: null, signal: new AbortController().signal };
|
|
132
|
+
const handlers = { open: vi.fn(), message: vi.fn() };
|
|
133
|
+
await backend.transport(attempt, handlers);
|
|
134
|
+
await backend.transport(attempt, handlers);
|
|
135
|
+
expect(wasm.create).toHaveBeenCalledOnce();
|
|
136
|
+
expect(transport).toHaveBeenCalledWith(attempt, handlers);
|
|
137
|
+
await backend.notebook("n1").edit([], { idempotencyKey: "edit-1" });
|
|
138
|
+
expect(edit).toHaveBeenCalledWith("n1", [], { idempotencyKey: "edit-1" });
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it("stops following WASM when the worker does not start", async () => {
|
|
142
|
+
wasm.enabled = true;
|
|
143
|
+
wasm.create.mockRejectedValue(new Error("Initialization failed"));
|
|
144
|
+
await expect(
|
|
145
|
+
createEditorBackend(options).transport(
|
|
146
|
+
{ cursor: null, signal: new AbortController().signal },
|
|
147
|
+
{ open: vi.fn(), message: vi.fn() },
|
|
148
|
+
),
|
|
149
|
+
).rejects.toBeInstanceOf(StreamStopped);
|
|
150
|
+
});
|
|
123
151
|
});
|
|
124
152
|
|
|
125
153
|
describe("the session backend's startup", () => {
|
|
@@ -9,7 +9,6 @@ import { PyodideBridge } from "../wasm/bridge";
|
|
|
9
9
|
import { isWasm } from "../wasm/utils";
|
|
10
10
|
import {
|
|
11
11
|
idle,
|
|
12
|
-
pyodide,
|
|
13
12
|
sessionSse,
|
|
14
13
|
websocket,
|
|
15
14
|
withHeaders,
|
|
@@ -61,6 +60,36 @@ function afterStartup({
|
|
|
61
60
|
};
|
|
62
61
|
}
|
|
63
62
|
|
|
63
|
+
/**
|
|
64
|
+
* Follow the notebook running in the WASM worker.
|
|
65
|
+
*
|
|
66
|
+
* The worker's backend exists once the worker has started, so the first
|
|
67
|
+
* connection creates it. Commands are only for a notebook its stream reported.
|
|
68
|
+
*/
|
|
69
|
+
function wasm(): NotebookBackend {
|
|
70
|
+
let backend: Promise<NotebookBackend> | undefined;
|
|
71
|
+
let started: NotebookBackend | undefined;
|
|
72
|
+
return {
|
|
73
|
+
transport: async (attempt, handlers) => {
|
|
74
|
+
backend ??= PyodideBridge.INSTANCE.createNotebookBackend();
|
|
75
|
+
try {
|
|
76
|
+
started = await backend;
|
|
77
|
+
} catch (error) {
|
|
78
|
+
throw new StreamStopped("The WASM notebook did not start", {
|
|
79
|
+
cause: error,
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
return started.transport(attempt, handlers);
|
|
83
|
+
},
|
|
84
|
+
notebook: (id) => {
|
|
85
|
+
if (started === undefined) {
|
|
86
|
+
throw new Error("The WASM notebook has not started");
|
|
87
|
+
}
|
|
88
|
+
return started.notebook(id);
|
|
89
|
+
},
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
64
93
|
/** Commands for a notebook the editor cannot change through its backend. */
|
|
65
94
|
function uneditable(): ReturnType<NotebookBackend["notebook"]> {
|
|
66
95
|
return {
|
|
@@ -84,12 +113,7 @@ export function createEditorBackend(
|
|
|
84
113
|
return { transport: idle, notebook: uneditable };
|
|
85
114
|
}
|
|
86
115
|
if (isWasm()) {
|
|
87
|
-
return
|
|
88
|
-
transport: pyodide((consumer) =>
|
|
89
|
-
PyodideBridge.INSTANCE.attachMessageConsumer(consumer),
|
|
90
|
-
),
|
|
91
|
-
notebook: uneditable,
|
|
92
|
-
};
|
|
116
|
+
return wasm();
|
|
93
117
|
}
|
|
94
118
|
const url = afterStartup(options);
|
|
95
119
|
return {
|
|
@@ -11,7 +11,7 @@ import { createNotebookReplica } from "../../../host/replica";
|
|
|
11
11
|
import { fakeBackend, fixtureMessages } from "../../../host/__tests__/fixtures";
|
|
12
12
|
import { ConnectionState } from "../../../host/connection";
|
|
13
13
|
import { Deferred } from "@/utils/Deferred";
|
|
14
|
-
import {
|
|
14
|
+
import { sessionSse, websocket } from "../notebook";
|
|
15
15
|
|
|
16
16
|
const encoder = new TextEncoder();
|
|
17
17
|
|
|
@@ -403,43 +403,3 @@ describe.each(["websocket", "sse"] as const)(
|
|
|
403
403
|
});
|
|
404
404
|
},
|
|
405
405
|
);
|
|
406
|
-
|
|
407
|
-
describe("pyodide", () => {
|
|
408
|
-
it("delivers worker notifications as marimo events and host events as they are", async () => {
|
|
409
|
-
let consumer: ((message: MessageEvent<string>) => void) | undefined;
|
|
410
|
-
const events = new NotebookEvents(
|
|
411
|
-
pyodide((next) => {
|
|
412
|
-
consumer = next;
|
|
413
|
-
}),
|
|
414
|
-
{ types: ["notebook", "marimo"] },
|
|
415
|
-
);
|
|
416
|
-
const received = collect(events);
|
|
417
|
-
await vi.waitFor(() => expect(consumer).toBeDefined());
|
|
418
|
-
const send = (data: string) =>
|
|
419
|
-
consumer?.(new MessageEvent("message", { data }));
|
|
420
|
-
send('{"data":{"op":"kernel-ready"}}');
|
|
421
|
-
send('{"event":"marimo","id":"3","data":{"marimo":{"op":"reconnected"}}}');
|
|
422
|
-
expect(received).toMatchInlineSnapshot(`
|
|
423
|
-
[
|
|
424
|
-
{
|
|
425
|
-
"data": {
|
|
426
|
-
"marimo": {
|
|
427
|
-
"op": "kernel-ready",
|
|
428
|
-
},
|
|
429
|
-
},
|
|
430
|
-
"event": "marimo",
|
|
431
|
-
},
|
|
432
|
-
{
|
|
433
|
-
"data": {
|
|
434
|
-
"marimo": {
|
|
435
|
-
"op": "reconnected",
|
|
436
|
-
},
|
|
437
|
-
},
|
|
438
|
-
"event": "marimo",
|
|
439
|
-
"id": "3",
|
|
440
|
-
},
|
|
441
|
-
]
|
|
442
|
-
`);
|
|
443
|
-
events.close();
|
|
444
|
-
});
|
|
445
|
-
});
|
|
@@ -157,34 +157,3 @@ export const idle: NotebookTransport = ({ signal }, { open }) =>
|
|
|
157
157
|
open();
|
|
158
158
|
signal.addEventListener("abort", () => settle(), { once: true });
|
|
159
159
|
});
|
|
160
|
-
|
|
161
|
-
/**
|
|
162
|
-
* Follow the notebook running in the WASM worker.
|
|
163
|
-
*
|
|
164
|
-
* The worker sends marimo notifications as `{ data: notification }`, which
|
|
165
|
-
* arrive as `marimo` events.
|
|
166
|
-
*/
|
|
167
|
-
export function pyodide(
|
|
168
|
-
attach: (consumer: (message: MessageEvent<string>) => void) => void,
|
|
169
|
-
): NotebookTransport {
|
|
170
|
-
return ({ signal }, { open, message }) =>
|
|
171
|
-
new Promise((settle) => {
|
|
172
|
-
open();
|
|
173
|
-
attach(({ data }) => {
|
|
174
|
-
if (signal.aborted) {
|
|
175
|
-
return;
|
|
176
|
-
}
|
|
177
|
-
const value = jsonParseWithSpecialChar(data);
|
|
178
|
-
// The worker sends notifications without the host's event envelope.
|
|
179
|
-
message(
|
|
180
|
-
typeof value === "object" &&
|
|
181
|
-
value !== null &&
|
|
182
|
-
!("event" in value) &&
|
|
183
|
-
"data" in value
|
|
184
|
-
? { event: "marimo", data: { marimo: value.data } }
|
|
185
|
-
: value,
|
|
186
|
-
);
|
|
187
|
-
});
|
|
188
|
-
signal.addEventListener("abort", () => settle(), { once: true });
|
|
189
|
-
});
|
|
190
|
-
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{t as e}from"./worker-CfME3uyi.js";var t=e(((e,t)=>{t.exports={}}));export default t();
|