@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.
Files changed (33) hide show
  1. package/dist/{ErrorBoundary-CgZ1IpNU.js → ErrorBoundary-BX9mTNLJ.js} +1 -1
  2. package/dist/assets/__vite-browser-external-DrmlAsRP.js +1 -0
  3. package/dist/assets/worker-O2XUgidQ.js +113 -0
  4. package/dist/{code-visibility-uIcGb1d8.js → code-visibility-gptWgGHp.js} +1 -1
  5. package/dist/{common-Cn-8v1pu.js → common-mU-2k6Zp.js} +1 -1
  6. package/dist/{data-grid-overlay-editor-CY38o06k.js → data-grid-overlay-editor-Bdyx_BTg.js} +1 -1
  7. package/dist/{feedback-button-C8MtB57f.js → feedback-button-BoXM4uir.js} +2 -2
  8. package/dist/{glide-data-editor-BK5sjGKi.js → glide-data-editor-DaXYMyzk.js} +3 -3
  9. package/dist/{globals-Dx5-Q1mU.js → globals-Bj4btPVp.js} +1 -1
  10. package/dist/main.js +29 -28
  11. package/dist/{number-overlay-editor-woxvu2w0.js → number-overlay-editor-DHfzwLIx.js} +1 -1
  12. package/dist/{reveal-component-BW15mD8s.js → reveal-component-CurCj3ks.js} +2 -2
  13. package/package.json +1 -1
  14. package/src/core/islands/__tests__/bootstrap.test.ts +17 -17
  15. package/src/core/islands/__tests__/bridge.test.ts +31 -26
  16. package/src/core/islands/bootstrap.ts +22 -51
  17. package/src/core/islands/bridge.ts +31 -20
  18. package/src/core/islands/worker/__tests__/controller.test.ts +0 -1
  19. package/src/core/islands/worker/controller.ts +0 -4
  20. package/src/core/islands/worker/worker.tsx +32 -32
  21. package/src/core/wasm/__tests__/bridge.test.ts +0 -1
  22. package/src/core/wasm/bridge.ts +0 -13
  23. package/src/core/wasm/rpc.ts +0 -1
  24. package/src/core/wasm/worker/bootstrap.ts +1 -15
  25. package/src/core/wasm/worker/types.ts +1 -6
  26. package/src/core/wasm/worker/worker.ts +4 -28
  27. package/src/core/websocket/__tests__/backend.test.ts +41 -13
  28. package/src/core/websocket/backend.ts +31 -7
  29. package/src/core/websocket/transports/__tests__/notebook.test.ts +1 -41
  30. package/src/core/websocket/transports/notebook.ts +0 -31
  31. package/dist/assets/__vite-browser-external-SaDYcg7x.js +0 -1
  32. package/dist/assets/worker-CfME3uyi.js +0 -115
  33. 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
- export interface IslandsKernelMessage {
49
- message: JsonString<NotificationPayload>;
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
- ...notebook,
84
- onMessage: (message) => {
85
- messageBuffer.push({
86
- message,
87
- sessionGeneration: opts.sessionGeneration,
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
- const session = activeSession;
100
+ // Stopping closes every session and its notebook subscriptions.
111
101
  await self.controller.stopSession();
112
102
  activeSession = undefined;
113
- (session?.bridge as unknown as PyProxy | undefined)?.destroy();
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 when the kernel sends a message
247
- kernelMessage: IslandsKernelMessage;
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
  }
@@ -51,7 +51,6 @@ vi.mock("@/core/wasm/rpc", () => ({
51
51
  readNotebook: mockReadNotebook,
52
52
  saveNotebook: mockSaveNotebook,
53
53
  },
54
- send: { consumerReady: vi.fn() },
55
54
  },
56
55
  addMessageListener: (event: string, cb: () => void) => {
57
56
  rpcListeners[event] = cb;
@@ -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();
@@ -11,7 +11,6 @@ import { TRANSPORT_ID } from "./worker/constants";
11
11
 
12
12
  export type ParentSchema = RPCSchema<{
13
13
  messages: {
14
- consumerReady: {};
15
14
  bootstrap: WasmRuntimeConfig;
16
15
  };
17
16
  }>;
@@ -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, onMessage, queryParameters, userConfig } = opts;
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, consumer: vi.fn() }));
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: { attachMessageConsumer: wasm.consumer } },
15
+ PyodideBridge: { INSTANCE: { createNotebookBackend: wasm.create } },
15
16
  }));
16
17
 
17
18
  afterEach(() => {
18
19
  wasm.enabled = false;
19
- wasm.consumer.mockReset();
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
- events.status === "connected" && !wasm.consumer.mock.calls.length,
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.each([
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(chosen)
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 { pyodide, sessionSse, websocket } from "../notebook";
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();