@marimo-team/islands 0.25.3-dev140 → 0.25.3-dev142
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-CTJ1U1fx.js → ErrorBoundary-DxdeETpm.js} +1 -1
- package/dist/{code-visibility-C1jZfwPW.js → code-visibility-CO8abjLo.js} +1 -1
- package/dist/{common-CNv_7gvI.js → common-CbFJQhaI.js} +1 -1
- package/dist/{data-grid-overlay-editor-t0t17DPI.js → data-grid-overlay-editor-DBjVUS2K.js} +1 -1
- package/dist/{feedback-button-JzaiyZZm.js → feedback-button-CWQoIUKQ.js} +2 -2
- package/dist/{glide-data-editor-B3eq-1SC.js → glide-data-editor-BxT8eGip.js} +3 -3
- package/dist/{globals-BW8TOLYQ.js → globals-f0UxWZkr.js} +1 -1
- package/dist/main.js +9 -8
- package/dist/{number-overlay-editor-DoUnDbPE.js → number-overlay-editor-Dso2H7DV.js} +1 -1
- package/dist/{reveal-component-ClNn1CDl.js → reveal-component-DMglhlng.js} +2 -2
- package/package.json +1 -1
- package/src/core/host/__tests__/fixtures.ts +9 -1
- package/src/core/host/__tests__/static.test.ts +18 -12
- package/src/core/host/__tests__/wasm.test.ts +63 -60
- package/src/core/host/backend.ts +1 -1
- package/src/core/host/static.ts +10 -5
- package/src/core/host/wasm.ts +2 -12
- package/src/core/saving/save-component.tsx +12 -3
- package/src/core/wasm/__tests__/bridge.test.ts +177 -55
- package/src/core/wasm/__tests__/worker.test.ts +6 -1
- package/src/core/wasm/bridge.ts +48 -38
- package/src/core/wasm/rpc.ts +15 -2
- package/src/core/wasm/worker/save-worker.ts +9 -10
- package/src/core/wasm/worker/types.ts +6 -4
- package/src/core/wasm/worker/worker.ts +29 -17
- package/src/core/websocket/__tests__/backend.test.ts +10 -3
- package/src/core/websocket/backend.ts +9 -10
|
@@ -8,20 +8,34 @@ const {
|
|
|
8
8
|
mockBridge,
|
|
9
9
|
mockNotebookReadFile,
|
|
10
10
|
mockReadNotebook,
|
|
11
|
-
|
|
11
|
+
mockWriteNotebook,
|
|
12
|
+
mockNotebookCall,
|
|
13
|
+
mockSetNotebookLayout,
|
|
14
|
+
mockFlush,
|
|
15
|
+
parentRequests,
|
|
12
16
|
mockBootstrap,
|
|
13
17
|
rpcListeners,
|
|
14
|
-
mockNotebookInfo,
|
|
15
18
|
mockNotebookSubscribe,
|
|
16
19
|
mockNotebookUnsubscribe,
|
|
17
20
|
} = vi.hoisted(() => ({
|
|
18
21
|
mockBridge: vi.fn(),
|
|
19
22
|
mockNotebookReadFile: vi.fn(),
|
|
20
23
|
mockReadNotebook: vi.fn(),
|
|
21
|
-
|
|
24
|
+
mockWriteNotebook: vi.fn(),
|
|
25
|
+
mockNotebookCall: vi.fn(),
|
|
26
|
+
mockSetNotebookLayout: vi.fn(),
|
|
27
|
+
mockFlush: vi.fn(),
|
|
28
|
+
parentRequests: {} as {
|
|
29
|
+
persistNotebook?: (snapshot: {
|
|
30
|
+
filename: string;
|
|
31
|
+
contents: string;
|
|
32
|
+
}) => Promise<void>;
|
|
33
|
+
},
|
|
22
34
|
mockBootstrap: vi.fn(),
|
|
23
|
-
rpcListeners: {} as Record<
|
|
24
|
-
|
|
35
|
+
rpcListeners: {} as Record<
|
|
36
|
+
string,
|
|
37
|
+
(message?: { id: string; event: string }) => void
|
|
38
|
+
>,
|
|
25
39
|
mockNotebookSubscribe: vi.fn(),
|
|
26
40
|
mockNotebookUnsubscribe: vi.fn(),
|
|
27
41
|
}));
|
|
@@ -47,24 +61,31 @@ class MockURL extends URL {
|
|
|
47
61
|
vi.stubGlobal("URL", MockURL);
|
|
48
62
|
|
|
49
63
|
vi.mock("@/core/wasm/rpc", () => ({
|
|
50
|
-
getWorkerRPC: () =>
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
64
|
+
getWorkerRPC: (_worker: Worker, handlers?: typeof parentRequests) => {
|
|
65
|
+
Object.assign(parentRequests, handlers);
|
|
66
|
+
return {
|
|
67
|
+
send: { bootstrap: mockBootstrap },
|
|
68
|
+
proxy: {
|
|
69
|
+
request: {
|
|
70
|
+
bridge: mockBridge,
|
|
71
|
+
startSession: vi.fn(),
|
|
72
|
+
readFile: vi.fn(),
|
|
73
|
+
readNotebook: mockReadNotebook,
|
|
74
|
+
writeNotebook: mockWriteNotebook,
|
|
75
|
+
notebookSubscribe: mockNotebookSubscribe,
|
|
76
|
+
notebookUnsubscribe: mockNotebookUnsubscribe,
|
|
77
|
+
notebookCall: mockNotebookCall,
|
|
78
|
+
setNotebookLayout: mockSetNotebookLayout,
|
|
79
|
+
},
|
|
62
80
|
},
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
81
|
+
addMessageListener: (
|
|
82
|
+
event: string,
|
|
83
|
+
cb: (typeof rpcListeners)[string],
|
|
84
|
+
) => {
|
|
85
|
+
rpcListeners[event] = cb;
|
|
86
|
+
},
|
|
87
|
+
};
|
|
88
|
+
},
|
|
68
89
|
}));
|
|
69
90
|
|
|
70
91
|
vi.mock("@/core/meta/globals", () => ({
|
|
@@ -80,14 +101,25 @@ vi.mock("@/core/wasm/store", () => ({
|
|
|
80
101
|
notebookFileStore: { readFile: mockNotebookReadFile, saveFile: vi.fn() },
|
|
81
102
|
}));
|
|
82
103
|
|
|
104
|
+
vi.mock("@/core/cells/document-changes", () => ({
|
|
105
|
+
flushDocumentChanges: mockFlush,
|
|
106
|
+
documentTransactionMiddleware: vi.fn(),
|
|
107
|
+
}));
|
|
108
|
+
|
|
83
109
|
// Import after all mocks are set up
|
|
84
110
|
import { store } from "@/core/state/jotai";
|
|
85
111
|
import { initialModeAtom } from "@/core/mode";
|
|
112
|
+
import { fixtureEvents } from "@/core/host/__tests__/fixtures";
|
|
113
|
+
import { createNotebookReplica } from "@/core/host";
|
|
114
|
+
import { notebookReplicaAtom } from "@/core/websocket/state";
|
|
115
|
+
import { createEditorBackend } from "@/core/websocket/backend";
|
|
116
|
+
import { fallbackFileStore, notebookFileStore } from "../store";
|
|
86
117
|
import { getWasmWorkerName, PyodideBridge } from "../bridge";
|
|
87
118
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
void PyodideBridge.INSTANCE;
|
|
119
|
+
beforeEach(() => {
|
|
120
|
+
Reflect.deleteProperty(window, "_marimo_private_PyodideBridge");
|
|
121
|
+
void PyodideBridge.INSTANCE;
|
|
122
|
+
});
|
|
91
123
|
|
|
92
124
|
describe("PyodideBridge.readCode", () => {
|
|
93
125
|
beforeEach(() => {
|
|
@@ -169,6 +201,7 @@ describe("PyodideBridge.exportAsScript", () => {
|
|
|
169
201
|
});
|
|
170
202
|
|
|
171
203
|
describe("PyodideBridge.sendSave", () => {
|
|
204
|
+
let disconnect: () => void;
|
|
172
205
|
const request = {
|
|
173
206
|
cellIds: [cellId("cell-1")],
|
|
174
207
|
codes: ['value = "NEW"'],
|
|
@@ -179,50 +212,140 @@ describe("PyodideBridge.sendSave", () => {
|
|
|
179
212
|
persist: true,
|
|
180
213
|
};
|
|
181
214
|
|
|
182
|
-
beforeEach(() => {
|
|
215
|
+
beforeEach(async () => {
|
|
183
216
|
vi.clearAllMocks();
|
|
184
217
|
store.set(initialModeAtom, "edit");
|
|
185
218
|
rpcListeners.initialized();
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
219
|
+
mockFlush.mockResolvedValue(undefined);
|
|
220
|
+
mockWriteNotebook.mockResolvedValue(undefined);
|
|
221
|
+
mockNotebookUnsubscribe.mockResolvedValue(undefined);
|
|
222
|
+
const document = fixtureEvents("notebook-events.sse").find(
|
|
223
|
+
(event) => event.event === "document",
|
|
224
|
+
)?.data.document;
|
|
225
|
+
mockNotebookCall.mockResolvedValue({
|
|
226
|
+
status: 200,
|
|
227
|
+
body: { document, cells: {} },
|
|
228
|
+
});
|
|
229
|
+
mockNotebookSubscribe.mockImplementation(async ({ id }) => {
|
|
230
|
+
for (const event of fixtureEvents("notebook-events.sse")) {
|
|
231
|
+
rpcListeners.notebookEvent({ id, event: JSON.stringify(event) });
|
|
232
|
+
}
|
|
233
|
+
});
|
|
234
|
+
const replica = createNotebookReplica(store);
|
|
235
|
+
store.set(notebookReplicaAtom, replica);
|
|
236
|
+
const backend = createEditorBackend({
|
|
237
|
+
static: false,
|
|
238
|
+
transportType: "sse",
|
|
239
|
+
url: () => "http://localhost/",
|
|
240
|
+
headers: () => ({}),
|
|
241
|
+
notebookUrl: (id) => new URL(`http://localhost/notebooks/${id}`),
|
|
242
|
+
waitUntilReady: async () => undefined,
|
|
243
|
+
checkHealth: async () => true,
|
|
244
|
+
});
|
|
245
|
+
disconnect = replica.connect(backend, { onNotification: vi.fn() });
|
|
246
|
+
await vi.waitFor(() =>
|
|
247
|
+
expect(store.get(replica.notebookAtom)?.id).toBe("n1"),
|
|
189
248
|
);
|
|
190
249
|
});
|
|
191
250
|
|
|
192
251
|
afterEach(() => {
|
|
252
|
+
disconnect();
|
|
253
|
+
store.set(notebookReplicaAtom, null);
|
|
193
254
|
store.set(initialModeAtom, undefined);
|
|
194
255
|
});
|
|
195
256
|
|
|
196
|
-
it("
|
|
197
|
-
const
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
257
|
+
it("saves through the connected backend using the stream's notebook ID", async () => {
|
|
258
|
+
const bridge = PyodideBridge.INSTANCE;
|
|
259
|
+
const createBackend = vi.spyOn(bridge, "createNotebookBackend");
|
|
260
|
+
await bridge.sendSave(request);
|
|
261
|
+
expect(createBackend).not.toHaveBeenCalled();
|
|
262
|
+
expect(mockNotebookCall).toHaveBeenCalledWith(
|
|
263
|
+
expect.objectContaining({
|
|
264
|
+
operation: "notebook.save",
|
|
265
|
+
params: { notebook_id: "n1" },
|
|
266
|
+
body: {},
|
|
267
|
+
}),
|
|
268
|
+
);
|
|
269
|
+
});
|
|
201
270
|
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
271
|
+
it.each(["unmounted", "not ready", "disconnected"])(
|
|
272
|
+
"does not save when the notebook is %s",
|
|
273
|
+
async (state) => {
|
|
274
|
+
if (state === "unmounted") {
|
|
275
|
+
store.set(notebookReplicaAtom, null);
|
|
276
|
+
} else if (state === "not ready") {
|
|
277
|
+
store.set(notebookReplicaAtom, createNotebookReplica(store));
|
|
278
|
+
} else {
|
|
279
|
+
disconnect();
|
|
280
|
+
}
|
|
281
|
+
await expect(PyodideBridge.INSTANCE.sendSave(request)).rejects.toThrow(
|
|
282
|
+
"The notebook is not connected",
|
|
283
|
+
);
|
|
284
|
+
expect(mockNotebookCall).not.toHaveBeenCalled();
|
|
285
|
+
},
|
|
286
|
+
);
|
|
206
287
|
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
const
|
|
211
|
-
download: false,
|
|
212
|
-
});
|
|
288
|
+
it("flushes edits before asking the host to save without an editor snapshot", async () => {
|
|
289
|
+
const flushed = new Deferred<void>();
|
|
290
|
+
mockFlush.mockReturnValueOnce(flushed.promise);
|
|
291
|
+
const saving = PyodideBridge.INSTANCE.sendSave(request);
|
|
213
292
|
await Promise.resolve();
|
|
214
|
-
expect(
|
|
293
|
+
expect(mockNotebookCall).not.toHaveBeenCalled();
|
|
294
|
+
flushed.resolve();
|
|
295
|
+
await saving;
|
|
296
|
+
expect(mockSetNotebookLayout).toHaveBeenCalledWith(null);
|
|
297
|
+
expect(mockNotebookCall).toHaveBeenCalledWith(
|
|
298
|
+
expect.objectContaining({ operation: "notebook.save", body: {} }),
|
|
299
|
+
);
|
|
300
|
+
expect(mockWriteNotebook).not.toHaveBeenCalled();
|
|
301
|
+
});
|
|
215
302
|
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
expect(
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
303
|
+
it("does not save when flushing fails", async () => {
|
|
304
|
+
mockFlush.mockRejectedValueOnce(new Error("unsynchronized edit"));
|
|
305
|
+
await expect(PyodideBridge.INSTANCE.sendSave(request)).rejects.toThrow(
|
|
306
|
+
"unsynchronized edit",
|
|
307
|
+
);
|
|
308
|
+
expect(mockNotebookCall).not.toHaveBeenCalled();
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
it("reports host persistence failures", async () => {
|
|
312
|
+
mockNotebookCall.mockResolvedValueOnce({
|
|
313
|
+
status: 409,
|
|
314
|
+
body: {
|
|
315
|
+
type: "tag:marimo.io,2026:host/conflict",
|
|
316
|
+
title: "Conflict",
|
|
317
|
+
status: 409,
|
|
318
|
+
detail: "storage full",
|
|
319
|
+
},
|
|
225
320
|
});
|
|
321
|
+
await expect(PyodideBridge.INSTANCE.sendSave(request)).rejects.toThrow(
|
|
322
|
+
"storage full",
|
|
323
|
+
);
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
it("persists exactly the host's content before acknowledging storage", async () => {
|
|
327
|
+
const durable = new Deferred<void>();
|
|
328
|
+
const snapshot = { filename: "notebook.py", contents: "# host revision" };
|
|
329
|
+
mockWriteNotebook.mockReturnValueOnce(durable.promise);
|
|
330
|
+
const writing = parentRequests.persistNotebook!(snapshot);
|
|
331
|
+
await Promise.resolve();
|
|
332
|
+
expect(mockWriteNotebook).toHaveBeenCalledWith(snapshot);
|
|
333
|
+
expect(notebookFileStore.saveFile).not.toHaveBeenCalled();
|
|
334
|
+
durable.resolve();
|
|
335
|
+
await writing;
|
|
336
|
+
expect(notebookFileStore.saveFile).toHaveBeenCalledWith(snapshot.contents);
|
|
337
|
+
expect(fallbackFileStore.saveFile).toHaveBeenCalledWith(snapshot.contents);
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
it("propagates storage failure without updating the URL", async () => {
|
|
341
|
+
mockWriteNotebook.mockRejectedValueOnce(new Error("IndexedDB failed"));
|
|
342
|
+
await expect(
|
|
343
|
+
parentRequests.persistNotebook!({
|
|
344
|
+
filename: "notebook.py",
|
|
345
|
+
contents: "# host",
|
|
346
|
+
}),
|
|
347
|
+
).rejects.toThrow("IndexedDB failed");
|
|
348
|
+
expect(notebookFileStore.saveFile).not.toHaveBeenCalled();
|
|
226
349
|
});
|
|
227
350
|
});
|
|
228
351
|
|
|
@@ -231,7 +354,6 @@ it("cancels a pending notebook subscription before its RPC response", async () =
|
|
|
231
354
|
store.set(initialModeAtom, "read");
|
|
232
355
|
rpcListeners.initialized();
|
|
233
356
|
const pending = new Deferred<void>();
|
|
234
|
-
mockNotebookInfo.mockResolvedValue({ id: "n1" });
|
|
235
357
|
mockNotebookSubscribe.mockReturnValue(pending.promise);
|
|
236
358
|
mockNotebookUnsubscribe.mockResolvedValue(undefined);
|
|
237
359
|
const controller = new AbortController();
|
|
@@ -77,6 +77,7 @@ async function startWorker(subscribing = Promise.resolve()) {
|
|
|
77
77
|
},
|
|
78
78
|
),
|
|
79
79
|
unsubscribe: (id: string) => subscribers.delete(id),
|
|
80
|
+
set_persistence: vi.fn(),
|
|
80
81
|
destroy: vi.fn(),
|
|
81
82
|
};
|
|
82
83
|
const bridge = { notebook, destroy: vi.fn() };
|
|
@@ -120,10 +121,13 @@ async function startWorker(subscribing = Promise.resolve()) {
|
|
|
120
121
|
}
|
|
121
122
|
|
|
122
123
|
it("delivers startup notifications after a delayed first subscription, without rerunning on resubscribe", async () => {
|
|
123
|
-
const { controller, init, event, pyodide } = await startWorker();
|
|
124
|
+
const { controller, init, notebook, event, pyodide } = await startWorker();
|
|
124
125
|
try {
|
|
125
126
|
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
126
127
|
expect(rpc.send.initialized).toHaveBeenCalledOnce();
|
|
128
|
+
expect(notebook.set_persistence).toHaveBeenCalledExactlyOnceWith(
|
|
129
|
+
expect.any(Function),
|
|
130
|
+
);
|
|
127
131
|
expect(pyodide.loadPackagesFromImports).toHaveResolved();
|
|
128
132
|
expect(init).not.toHaveBeenCalled();
|
|
129
133
|
|
|
@@ -137,6 +141,7 @@ it("delivers startup notifications after a delayed first subscription, without r
|
|
|
137
141
|
await handlers.requests.notebookSubscribe({ id: "second", cursor: null });
|
|
138
142
|
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
139
143
|
expect(init).toHaveBeenCalledOnce();
|
|
144
|
+
expect(notebook.set_persistence).toHaveBeenCalledOnce();
|
|
140
145
|
} finally {
|
|
141
146
|
await controller.stopSession();
|
|
142
147
|
}
|
package/src/core/wasm/bridge.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
2
|
/* oxlint-disable typescript/no-explicit-any */
|
|
3
3
|
|
|
4
|
+
import { createRPCRequestHandler } from "rpc-anywhere";
|
|
4
5
|
import { toast } from "@/components/ui/use-toast";
|
|
5
6
|
import { userConfigAtom } from "@/core/config/config";
|
|
6
7
|
import { serializeBlob } from "@/utils/blob";
|
|
@@ -12,6 +13,7 @@ import { once } from "@/utils/once";
|
|
|
12
13
|
import { generateUUID } from "@/utils/uuid";
|
|
13
14
|
import { createModuleWorker } from "@/utils/worker";
|
|
14
15
|
import { notebookIsRunningAtom } from "../cells/cells";
|
|
16
|
+
import { flushDocumentChanges } from "../cells/document-changes";
|
|
15
17
|
import { createWasmNotebookBackend } from "../host";
|
|
16
18
|
import type { ClientCommand } from "../kernel/messages";
|
|
17
19
|
import { getInitialAppMode } from "../mode";
|
|
@@ -40,8 +42,9 @@ import type {
|
|
|
40
42
|
} from "../network/types";
|
|
41
43
|
import { filenameAtom } from "../saving/file-state";
|
|
42
44
|
import { store } from "../state/jotai";
|
|
45
|
+
import { notebookReplicaAtom } from "../websocket/state";
|
|
43
46
|
import { PyodideRouter } from "./router";
|
|
44
|
-
import { getWorkerRPC } from "./rpc";
|
|
47
|
+
import { getWorkerRPC, type PersistNotebookRequest } from "./rpc";
|
|
45
48
|
import { getWasmRuntimeConfig } from "./runtime-config";
|
|
46
49
|
import { createShareableLink } from "./share";
|
|
47
50
|
import { wasmInitStateAtom } from "./state";
|
|
@@ -68,7 +71,6 @@ export class PyodideBridge implements RunRequests, EditRequests {
|
|
|
68
71
|
|
|
69
72
|
private rpc!: ReturnType<typeof getWorkerRPC<WorkerSchema>>;
|
|
70
73
|
private saveRpc: SaveWorker | undefined;
|
|
71
|
-
private pendingSessionSave: Promise<unknown> = Promise.resolve();
|
|
72
74
|
private notebookConsumers = new Map<string, (event: unknown) => void>();
|
|
73
75
|
private interruptBuffer?: Uint8Array;
|
|
74
76
|
|
|
@@ -80,7 +82,7 @@ export class PyodideBridge implements RunRequests, EditRequests {
|
|
|
80
82
|
return {
|
|
81
83
|
readFile: throwNotImplemented,
|
|
82
84
|
readNotebook: async () => (await notebookFileStore.readFile()) ?? "",
|
|
83
|
-
|
|
85
|
+
writeNotebook: throwNotImplemented,
|
|
84
86
|
};
|
|
85
87
|
}
|
|
86
88
|
|
|
@@ -95,7 +97,7 @@ export class PyodideBridge implements RunRequests, EditRequests {
|
|
|
95
97
|
},
|
|
96
98
|
);
|
|
97
99
|
|
|
98
|
-
const rpc = getWorkerRPC<SaveWorkerSchema>(saveWorker);
|
|
100
|
+
const rpc = getWorkerRPC<SaveWorkerSchema>(saveWorker, undefined, Infinity);
|
|
99
101
|
rpc.send.bootstrap(runtimeConfig);
|
|
100
102
|
return rpc.proxy.request;
|
|
101
103
|
}
|
|
@@ -114,7 +116,22 @@ export class PyodideBridge implements RunRequests, EditRequests {
|
|
|
114
116
|
});
|
|
115
117
|
|
|
116
118
|
// Create the RPC
|
|
117
|
-
this.rpc = getWorkerRPC<WorkerSchema>(
|
|
119
|
+
this.rpc = getWorkerRPC<WorkerSchema>(
|
|
120
|
+
worker,
|
|
121
|
+
createRPCRequestHandler({
|
|
122
|
+
persistNotebook: async (snapshot: PersistNotebookRequest) => {
|
|
123
|
+
if (!this.saveRpc) {
|
|
124
|
+
throw new Error("Notebook storage is not initialized");
|
|
125
|
+
}
|
|
126
|
+
await this.saveRpc.writeNotebook(snapshot);
|
|
127
|
+
notebookFileStore.saveFile(snapshot.contents);
|
|
128
|
+
fallbackFileStore.saveFile(snapshot.contents);
|
|
129
|
+
return null;
|
|
130
|
+
},
|
|
131
|
+
}),
|
|
132
|
+
// Synchronous Python execution can block worker requests indefinitely.
|
|
133
|
+
Infinity,
|
|
134
|
+
);
|
|
118
135
|
this.rpc.send.bootstrap(runtimeConfig);
|
|
119
136
|
|
|
120
137
|
this.rpc.addMessageListener("notebookEvent", ({ id, event }) => {
|
|
@@ -209,11 +226,8 @@ export class PyodideBridge implements RunRequests, EditRequests {
|
|
|
209
226
|
|
|
210
227
|
public async createNotebookBackend() {
|
|
211
228
|
await this.initialized.promise;
|
|
212
|
-
const { id } = await this.rpc.proxy.request.notebookInfo();
|
|
213
229
|
return createWasmNotebookBackend({
|
|
214
|
-
notebookId: id,
|
|
215
230
|
subscribe: async (cursor, emit, signal) => {
|
|
216
|
-
await this.initialized.promise;
|
|
217
231
|
signal.throwIfAborted();
|
|
218
232
|
const id = generateUUID();
|
|
219
233
|
this.notebookConsumers.set(id, emit);
|
|
@@ -233,10 +247,7 @@ export class PyodideBridge implements RunRequests, EditRequests {
|
|
|
233
247
|
throw error;
|
|
234
248
|
}
|
|
235
249
|
},
|
|
236
|
-
call:
|
|
237
|
-
await this.initialized.promise;
|
|
238
|
-
return this.rpc.proxy.request.notebookCall(call);
|
|
239
|
-
},
|
|
250
|
+
call: (call) => this.rpc.proxy.request.notebookCall(call),
|
|
240
251
|
});
|
|
241
252
|
}
|
|
242
253
|
|
|
@@ -255,30 +266,23 @@ export class PyodideBridge implements RunRequests, EditRequests {
|
|
|
255
266
|
return null;
|
|
256
267
|
};
|
|
257
268
|
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
269
|
+
private async saveHostNotebook() {
|
|
270
|
+
const replica = store.get(notebookReplicaAtom);
|
|
271
|
+
const notebook = replica && store.get(replica.notebookAtom);
|
|
272
|
+
const backend = replica && store.get(replica.backendAtom);
|
|
273
|
+
if (!notebook || !backend) {
|
|
274
|
+
throw new Error("The notebook is not connected");
|
|
262
275
|
}
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
this.pendingSessionSave = this.pendingSessionSave
|
|
267
|
-
.catch(() => undefined)
|
|
268
|
-
.then(async () => {
|
|
269
|
-
await durableSave;
|
|
270
|
-
await this.rpc.proxy.request.saveNotebook(request);
|
|
271
|
-
});
|
|
272
|
-
void this.pendingSessionSave.catch((error) => {
|
|
273
|
-
Logger.error(error);
|
|
274
|
-
});
|
|
275
|
-
|
|
276
|
-
await durableSave;
|
|
277
|
-
const code = await this.readCode();
|
|
278
|
-
if (code.contents) {
|
|
279
|
-
notebookFileStore.saveFile(code.contents);
|
|
280
|
-
fallbackFileStore.saveFile(code.contents);
|
|
276
|
+
const result = await backend.notebook(notebook.id).save();
|
|
277
|
+
if (!result.ok) {
|
|
278
|
+
throw new Error(result.problem.detail ?? result.problem.title);
|
|
281
279
|
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
public sendSave: EditRequests["sendSave"] = async ({ layout }) => {
|
|
283
|
+
await flushDocumentChanges();
|
|
284
|
+
await this.rpc.proxy.request.setNotebookLayout(layout ?? null);
|
|
285
|
+
await this.saveHostNotebook();
|
|
282
286
|
return null;
|
|
283
287
|
};
|
|
284
288
|
|
|
@@ -395,6 +399,7 @@ export class PyodideBridge implements RunRequests, EditRequests {
|
|
|
395
399
|
functionName: "save_app_config",
|
|
396
400
|
payload: request,
|
|
397
401
|
});
|
|
402
|
+
await this.saveHostNotebook();
|
|
398
403
|
return null;
|
|
399
404
|
};
|
|
400
405
|
|
|
@@ -560,7 +565,7 @@ export class PyodideBridge implements RunRequests, EditRequests {
|
|
|
560
565
|
public exportAsHTML: EditRequests["exportAsHTML"] = async (
|
|
561
566
|
request: ExportAsHTMLRequest,
|
|
562
567
|
) => {
|
|
563
|
-
await
|
|
568
|
+
await flushDocumentChanges();
|
|
564
569
|
const response = await this.rpc.proxy.request.bridge({
|
|
565
570
|
functionName: "export_html",
|
|
566
571
|
payload: withDevAssetUrl(request),
|
|
@@ -571,7 +576,7 @@ export class PyodideBridge implements RunRequests, EditRequests {
|
|
|
571
576
|
public exportAsMarkdown: EditRequests["exportAsMarkdown"] = async (
|
|
572
577
|
request: ExportAsMarkdownRequest,
|
|
573
578
|
) => {
|
|
574
|
-
await
|
|
579
|
+
await flushDocumentChanges();
|
|
575
580
|
const response = await this.rpc.proxy.request.bridge({
|
|
576
581
|
functionName: "export_markdown",
|
|
577
582
|
payload: request,
|
|
@@ -582,7 +587,7 @@ export class PyodideBridge implements RunRequests, EditRequests {
|
|
|
582
587
|
public exportAsScript: EditRequests["exportAsScript"] = async (
|
|
583
588
|
request: ExportAsScriptRequest,
|
|
584
589
|
) => {
|
|
585
|
-
await
|
|
590
|
+
await flushDocumentChanges();
|
|
586
591
|
const response = await this.rpc.proxy.request.bridge({
|
|
587
592
|
functionName: "export_script",
|
|
588
593
|
payload: request,
|
|
@@ -653,7 +658,12 @@ export class PyodideBridge implements RunRequests, EditRequests {
|
|
|
653
658
|
return null;
|
|
654
659
|
};
|
|
655
660
|
|
|
656
|
-
public sendDocumentTransaction =
|
|
661
|
+
public sendDocumentTransaction: EditRequests["sendDocumentTransaction"] =
|
|
662
|
+
async (request) => {
|
|
663
|
+
await this.initialized.promise;
|
|
664
|
+
await this.rpc.proxy.request.documentTransaction(request);
|
|
665
|
+
return null;
|
|
666
|
+
};
|
|
657
667
|
|
|
658
668
|
public addPackage: EditRequests["addPackage"] = async (request) => {
|
|
659
669
|
return this.rpc.proxy.request.addPackage(request);
|
package/src/core/wasm/rpc.ts
CHANGED
|
@@ -9,18 +9,31 @@ import { Logger } from "@/utils/Logger";
|
|
|
9
9
|
import type { WasmRuntimeConfig } from "./runtime-config";
|
|
10
10
|
import { TRANSPORT_ID } from "./worker/constants";
|
|
11
11
|
|
|
12
|
+
export interface PersistNotebookRequest {
|
|
13
|
+
filename: string;
|
|
14
|
+
contents: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
12
17
|
export type ParentSchema = RPCSchema<{
|
|
18
|
+
requests: {
|
|
19
|
+
persistNotebook: { params: PersistNotebookRequest; response: null };
|
|
20
|
+
};
|
|
13
21
|
messages: {
|
|
14
22
|
bootstrap: WasmRuntimeConfig;
|
|
15
23
|
};
|
|
16
24
|
}>;
|
|
17
25
|
|
|
18
|
-
export function getWorkerRPC<WorkerSchema extends RPCSchema>(
|
|
26
|
+
export function getWorkerRPC<WorkerSchema extends RPCSchema>(
|
|
27
|
+
worker: Worker,
|
|
28
|
+
requestHandler?: RPCOptions<ParentSchema, WorkerSchema>["requestHandler"],
|
|
29
|
+
maxRequestTime = 20_000,
|
|
30
|
+
) {
|
|
19
31
|
return createRPC<ParentSchema, WorkerSchema>({
|
|
20
32
|
transport: createWorkerTransport(worker, {
|
|
21
33
|
transportId: TRANSPORT_ID,
|
|
22
34
|
}),
|
|
23
|
-
|
|
35
|
+
requestHandler,
|
|
36
|
+
maxRequestTime,
|
|
24
37
|
_debugHooks: {
|
|
25
38
|
onSend: (message) => {
|
|
26
39
|
Logger.debug("[rpc] Parent -> Worker", message);
|
|
@@ -7,12 +7,11 @@ import {
|
|
|
7
7
|
createWorkerParentTransport,
|
|
8
8
|
type RPCSchema,
|
|
9
9
|
} from "rpc-anywhere";
|
|
10
|
-
import type { SaveNotebookRequest } from "@/core/network/types";
|
|
11
10
|
import { decodeUtf8 } from "@/utils/strings";
|
|
12
11
|
import { Deferred } from "../../../utils/Deferred";
|
|
13
12
|
import { prettyError } from "../../../utils/errors";
|
|
14
13
|
import { Logger } from "../../../utils/Logger";
|
|
15
|
-
import type { ParentSchema } from "../rpc";
|
|
14
|
+
import type { ParentSchema, PersistNotebookRequest } from "../rpc";
|
|
16
15
|
import type { WasmRuntimeConfig } from "../runtime-config";
|
|
17
16
|
import { TRANSPORT_ID } from "./constants";
|
|
18
17
|
import { WasmFileSystem } from "./fs";
|
|
@@ -70,14 +69,14 @@ const requestHandler = createRPCRequestHandler({
|
|
|
70
69
|
await pyodideReadyPromise; // Make sure loading is done
|
|
71
70
|
return WasmFileSystem.readNotebook(self.pyodide);
|
|
72
71
|
},
|
|
73
|
-
|
|
74
|
-
await pyodideReadyPromise;
|
|
75
|
-
const
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
72
|
+
writeNotebook: async ({ filename, contents }: PersistNotebookRequest) => {
|
|
73
|
+
await pyodideReadyPromise;
|
|
74
|
+
const directory = filename.substring(0, filename.lastIndexOf("/"));
|
|
75
|
+
if (directory) {
|
|
76
|
+
self.pyodide.FS.mkdirTree(directory);
|
|
77
|
+
}
|
|
78
|
+
self.pyodide.FS.writeFile(filename, contents);
|
|
79
|
+
self.pyodide.FS.writeFile(WasmFileSystem.NOTEBOOK_FILENAME, contents);
|
|
81
80
|
await WasmFileSystem.persistFilesToRemote(self.pyodide);
|
|
82
81
|
},
|
|
83
82
|
});
|
|
@@ -30,7 +30,6 @@ import type {
|
|
|
30
30
|
FormatCellsRequest,
|
|
31
31
|
FormatResponse,
|
|
32
32
|
SaveAppConfigurationRequest,
|
|
33
|
-
SaveNotebookRequest,
|
|
34
33
|
SaveUserConfigurationRequest,
|
|
35
34
|
Snippets,
|
|
36
35
|
} from "../../network/types";
|
|
@@ -79,7 +78,6 @@ export interface RawBridge {
|
|
|
79
78
|
read_snippets(): Promise<Snippets>;
|
|
80
79
|
get_environment_info(): Promise<EnvironmentInfo>;
|
|
81
80
|
format(request: FormatCellsRequest): Promise<FormatResponse>;
|
|
82
|
-
save(request: SaveNotebookRequest): Promise<string>;
|
|
83
81
|
copy(request: CopyNotebookRequest): Promise<string>;
|
|
84
82
|
save_app_config(request: SaveAppConfigurationRequest): Promise<string>;
|
|
85
83
|
save_user_config(request: SaveUserConfigurationRequest): Promise<null>;
|
|
@@ -112,13 +110,12 @@ export type BridgePayload<T extends keyof RawBridge> = T extends keyof RawBridge
|
|
|
112
110
|
: undefined;
|
|
113
111
|
|
|
114
112
|
export type SerializedBridge = {
|
|
115
|
-
[P in
|
|
113
|
+
[P in keyof RawBridge]: RawBridge[P] extends (
|
|
116
114
|
payload: string,
|
|
117
115
|
) => Promise<unknown>
|
|
118
116
|
? (payload: string) => Promise<string>
|
|
119
117
|
: RawBridge[P];
|
|
120
118
|
} & {
|
|
121
|
-
save(payload: string): Promise<void>;
|
|
122
119
|
notebook: {
|
|
123
120
|
readonly id: string;
|
|
124
121
|
subscribe(
|
|
@@ -128,6 +125,11 @@ export type SerializedBridge = {
|
|
|
128
125
|
): Promise<void>;
|
|
129
126
|
unsubscribe(id: string): void;
|
|
130
127
|
call(call: string): Promise<string>;
|
|
128
|
+
transaction(request: string): Promise<void>;
|
|
129
|
+
set_layout(layout: string): Promise<void>;
|
|
130
|
+
set_persistence(
|
|
131
|
+
persist: (contents: string, filename: string) => Promise<void>,
|
|
132
|
+
): void;
|
|
131
133
|
destroy(): void;
|
|
132
134
|
};
|
|
133
135
|
};
|