@marimo-team/islands 0.25.3-dev322 → 0.25.3-dev348
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/{ConnectedDataExplorerComponent-DkXBUBf3.js → ConnectedDataExplorerComponent-xliyyXNO.js} +16 -16
- package/dist/{ErrorBoundary-CYa6lyKz.js → ErrorBoundary-CIimOpqs.js} +7 -7
- package/dist/{abnfDiagram-VCTEODGH-BPS-pTU2.js → abnfDiagram-VCTEODGH-Dl9Nrtbf.js} +2 -2
- package/dist/{any-language-editor-DIS7IdV1.js → any-language-editor-CBHhdCC0.js} +2 -2
- package/dist/{architectureDiagram-5GKGNRK7-DB2MHOb7.js → architectureDiagram-5GKGNRK7-DTg032fQ.js} +1 -1
- package/dist/{assertNever-HbUt8DyH.js → assertNever-ie77PjMn.js} +13 -13
- package/dist/{chat-ui-85k50jaj.js → chat-ui-BV0qyfru.js} +51 -51
- package/dist/{check-CApDh1-n.js → check-DTIv5ZZ3.js} +1 -1
- package/dist/{chunk-IMKFNOWR-CjFCedjx.js → chunk-IMKFNOWR-CsFB-2LW.js} +1 -1
- package/dist/{chunk-SHT3W25Y-Dptltg0_.js → chunk-SHT3W25Y-ByuJHeBL.js} +1 -1
- package/dist/{chunk-SVP7TREG-2jBN760m.js → chunk-SVP7TREG-9oBZhbrr.js} +1 -1
- package/dist/{chunk-TICWLB2K-CS8BE-b9.js → chunk-TICWLB2K-Bx4m0cJ1.js} +1 -1
- package/dist/{classDiagram-ZZMXUADV-HtQWp-Cm.js → classDiagram-ZZMXUADV-Cms37-C9.js} +2 -2
- package/dist/{classDiagram-v2-VYDZK3BY-DA9LETN-.js → classDiagram-v2-VYDZK3BY-qAom4Bi7.js} +2 -2
- package/dist/{code-visibility-CGcIJMGj.js → code-visibility-_-8-dvb4.js} +20 -20
- package/dist/{common-B-Ng3J3K.js → common-CNXuhphD.js} +620 -620
- package/dist/{config-B171m9zn.js → config-CvBnR945.js} +3 -3
- package/dist/{copy-icon-k2_Fx5Qv.js → copy-icon-DmtNidSa.js} +2 -2
- package/dist/{createReducer-BPPI11pP.js → createReducer-BxBCTvKn.js} +5 -5
- package/dist/{cynefinDiagram-5FMLGOSQ-eyocrg_E.js → cynefinDiagram-5FMLGOSQ-BjJZwMEz.js} +1 -1
- package/dist/{dagre-GXQ25YYZ-Dtm96Nvx.js → dagre-GXQ25YYZ-Bohu7_09.js} +1 -1
- package/dist/{data-grid-overlay-editor-CUD6F0Vj.js → data-grid-overlay-editor--4Gk_rwA.js} +1 -1
- package/dist/{diagram-S7CK7UJ4-C09TrdNO.js → diagram-S7CK7UJ4-Chl0n835.js} +1 -1
- package/dist/{diagram-UQ7AKVKN-vHUsSML6.js → diagram-UQ7AKVKN-DHx8nzy7.js} +1 -1
- package/dist/{diagram-VX7I27RA-Ch4mRgz8.js → diagram-VX7I27RA-CKdpv1U4.js} +1 -1
- package/dist/{diagram-Z3DM3KII-DyFQNdAL.js → diagram-Z3DM3KII-sY-sI--2.js} +1 -1
- package/dist/{dialog-C2Muh059.js → dialog-Cqu5Hp5U.js} +21 -21
- package/dist/{dropdown-menu-BGbNnuFq.js → dropdown-menu-DXEYk99S.js} +707 -707
- package/dist/{ebnfDiagram-PWID7BFC-N3SvlrCs.js → ebnfDiagram-PWID7BFC-CD0P2xeH.js} +2 -2
- package/dist/{erDiagram-RLTQ6QDP-RR2q2xtd.js → erDiagram-RLTQ6QDP-BaKQuYxC.js} +1 -1
- package/dist/{error-banner-3_4Lc93Y.js → error-banner-DUB_qUeL.js} +58 -58
- package/dist/{error-entries-BhNAZWU1.js → error-entries-CTb_cxXY.js} +310 -310
- package/dist/{feedback-button-DK6zUzAk.js → feedback-button-BaA4r2dl.js} +7 -7
- package/dist/{flowDiagram-HODETNUW-nxd7WhlW.js → flowDiagram-HODETNUW-BcCpHh7-.js} +2 -2
- package/dist/{formats-aUcw6MrS.js → formats-hF6fETr6.js} +1 -1
- package/dist/{glide-data-editor-O-94UN5R.js → glide-data-editor-CRn3KXyR.js} +3129 -3119
- package/dist/{glide-utils-D4UP8TGv.js → glide-utils-Bnd_k48H.js} +2 -2
- package/dist/{globals-BVjYePCn.js → globals-_FmD97Ui.js} +3 -3
- package/dist/{html-to-image-DOOtP-4-.js → html-to-image-Bbl0PeC7.js} +478 -478
- package/dist/{infoDiagram-27XIBGKW-CUYXQKGf.js → infoDiagram-27XIBGKW-DWltseIa.js} +1 -1
- package/dist/{ishikawaDiagram-5VMMS53U-v6gsCrlK.js → ishikawaDiagram-5VMMS53U-DHgepBPo.js} +1 -1
- package/dist/{kanban-definition-UXKFOSKX-z236-0Ok.js → kanban-definition-UXKFOSKX-B2_BIjBq.js} +1 -1
- package/dist/main.js +6712 -6590
- package/dist/{mermaid-C7lQ_2LJ.js → mermaid--2K0cmt9.js} +34 -34
- package/dist/{mindmap-definition-YA3MSWOX--pZF7OCY.js → mindmap-definition-YA3MSWOX-DNebycIP.js} +1 -1
- package/dist/{number-overlay-editor-jkzzCN4e.js → number-overlay-editor-Bcg25qrp.js} +1 -1
- package/dist/{pegDiagram-XKGWAZYB-Bgw0Zz_6.js → pegDiagram-XKGWAZYB-CH4U3II-.js} +2 -2
- package/dist/{pencil-RXFY-Ek8.js → pencil-MMnw1YQh.js} +1 -1
- package/dist/{pieDiagram-E7YTZNPT-B27tDYvK.js → pieDiagram-E7YTZNPT-pfcrdUCa.js} +1 -1
- package/dist/{process-output-CiX_Dl4L.js → process-output-Dw_sI4Xp.js} +4 -4
- package/dist/{railroadDiagram-O6MQD6OU-CBjPhGpj.js → railroadDiagram-O6MQD6OU-BZKpAbNG.js} +2 -2
- package/dist/{requirementDiagram-BXWQKSXE-DDDrLEw9.js → requirementDiagram-BXWQKSXE-DnyfIHe3.js} +1 -1
- package/dist/{reveal-component-CvPmNI5C.js → reveal-component-BVC491am.js} +18 -18
- package/dist/{select-Cv6lR39D.js → select-suaAxFuo.js} +286 -286
- package/dist/{sequenceDiagram-WJ2MYXX4-DQPaMygQ.js → sequenceDiagram-WJ2MYXX4-BeYuWX7N.js} +1 -1
- package/dist/{spec-BoCLWa-M.js → spec-BQNx2-D7.js} +4 -4
- package/dist/{stateDiagram-D77RDMKH-BDoDVVnq.js → stateDiagram-D77RDMKH-B8FxwAM2.js} +2 -2
- package/dist/{stateDiagram-v2-MP3YSRHH-KNko46uJ.js → stateDiagram-v2-MP3YSRHH-CzWLBqm1.js} +2 -2
- package/dist/{static-state-DgdOadIH.js → static-state-Al_hmf1n.js} +6 -6
- package/dist/style.css +1 -1
- package/dist/{swimlanes-42K2YHIH-boFjOuiI.js → swimlanes-42K2YHIH-CzKmGMts.js} +1 -1
- package/dist/{swimlanesDiagram-VR7AAH4N-BC4FLNwY.js → swimlanesDiagram-VR7AAH4N-DnWIUN_9.js} +2 -2
- package/dist/{timeline-definition-24CTP7MA-CLVPYA-v.js → timeline-definition-24CTP7MA-Czf2QN2R.js} +1 -1
- package/dist/{toast-CUuX7uXg.js → toast-CsJa1nGj.js} +66 -66
- package/dist/{tooltip-DBv8QUvS.js → tooltip-BVbJPkcl.js} +49 -49
- package/dist/{use-restore-focus-BxJh5Zfa.js → use-restore-focus-tPmARo1Q.js} +1035 -1022
- package/dist/{useDateFormatter-2k4mHRBq.js → useDateFormatter-B4ygYuUU.js} +2 -2
- package/dist/{useDeepCompareMemoize-Cb2GRQIS.js → useDeepCompareMemoize-Bcu0DkCS.js} +1 -1
- package/dist/{useLifecycle-B2fV3c8N.js → useLifecycle-MTdtnR3-.js} +1 -1
- package/dist/{vega-component-do3stf0K.js → vega-component-BhBbP4S7.js} +6 -6
- package/dist/{vennDiagram-4TSXK5OY-BLq7sNjb.js → vennDiagram-4TSXK5OY-BrHqWB1U.js} +1 -1
- package/dist/{wardleyDiagram-VM6X3IG4-oyJEWWtb.js → wardleyDiagram-VM6X3IG4-Cms1K_3v.js} +1 -1
- package/dist/{xychartDiagram-S5SC5T6Z-DMjWgQPm.js → xychartDiagram-S5SC5T6Z-_kUiODzS.js} +1 -1
- package/package.json +1 -1
- package/src/__mocks__/requests.ts +0 -2
- package/src/components/chat/__tests__/notebook-context.test.ts +199 -0
- package/src/components/chat/chat-panel.tsx +33 -15
- package/src/components/chat/notebook-context.ts +80 -0
- package/src/components/editor/cell/StopButton.tsx +3 -3
- package/src/components/editor/cell/cell-context-menu.tsx +18 -2
- package/src/components/editor/cell/code/__tests__/language-toggle.test.tsx +38 -1
- package/src/components/editor/cell/code/cell-editor.tsx +2 -1
- package/src/components/editor/cell/code/language-toggle.tsx +6 -4
- package/src/components/editor/chrome/wrapper/__tests__/document-sync-status.test.tsx +9 -6
- package/src/components/slides/slide-cell-view.tsx +2 -1
- package/src/components/ui/__tests__/fullscreen.test.tsx +56 -1
- package/src/components/ui/fullscreen.tsx +18 -2
- package/src/components/ui/slider.tsx +6 -2
- package/src/core/codemirror/copilot/getCodes.ts +1 -1
- package/src/core/edit-app.tsx +5 -4
- package/src/core/executions/__tests__/input.test.ts +1 -1
- package/src/core/executions/__tests__/interrupt.test.ts +200 -0
- package/src/core/executions/__tests__/run.test.ts +3 -3
- package/src/core/executions/index.ts +1 -0
- package/src/core/executions/interrupt.ts +49 -0
- package/src/core/islands/bridge.ts +0 -2
- package/src/core/network/requests-network.ts +0 -15
- package/src/core/network/requests-static.ts +0 -2
- package/src/core/network/requests-toasting.tsx +0 -2
- package/src/core/network/types.ts +0 -3
- package/src/core/notebook/document/__tests__/model.test.ts +4 -4
- package/src/core/notebook/document/__tests__/test-host.test.ts +6 -6
- package/src/core/notebook/document/__tests__/test-host.ts +42 -46
- package/src/core/notebook/document/__tests__/test-notebook.ts +5 -4
- package/src/core/notebook/document/sync.ts +12 -5
- package/src/core/notebook/replica/__tests__/fixtures.ts +8 -0
- package/src/core/notebook/replica/backend.ts +1 -1
- package/src/core/saving/__tests__/document.test.tsx +1 -1
- package/src/core/transports/__tests__/static.test.ts +1 -1
- package/src/core/transports/__tests__/wasm.test.ts +1 -1
- package/src/core/transports/static.ts +8 -2
- package/src/core/transports/wasm.ts +2 -4
- package/src/core/wasm/__tests__/bridge.test.ts +170 -26
- package/src/core/wasm/bridge.ts +64 -19
- package/src/core/wasm/worker/types.ts +0 -1
- package/src/plugins/impl/DataEditorPlugin.tsx +32 -21
- package/src/plugins/impl/SliderPlugin.tsx +35 -3
- package/src/plugins/impl/__test__/slider-labels.test.tsx +30 -0
- package/src/plugins/impl/__tests__/DataEditorPlugin.test.ts +103 -12
- package/src/plugins/impl/__tests__/SliderPlugin.test.tsx +40 -0
- package/src/plugins/impl/data-editor/__tests__/glide-data-editor.test.tsx +159 -4
- package/src/plugins/impl/data-editor/glide-data-editor.tsx +30 -10
- package/src/plugins/impl/data-editor/glide-portal.tsx +5 -3
- package/src/plugins/layout/AccordionPlugin.tsx +2 -1
- package/src/plugins/layout/__tests__/AccordionPlugin.test.tsx +21 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
2
|
|
|
3
3
|
import { createHash } from "node:crypto";
|
|
4
|
-
import type {
|
|
4
|
+
import type { ProblemOf, ResultOf } from "@marimo-team/client";
|
|
5
5
|
import * as Protocol from "@marimo-team/client/protocol";
|
|
6
6
|
import type { Document } from "../../replica/resources/document";
|
|
7
7
|
|
|
@@ -12,7 +12,7 @@ export interface EditRequest {
|
|
|
12
12
|
}
|
|
13
13
|
|
|
14
14
|
interface Outcome {
|
|
15
|
-
readonly result:
|
|
15
|
+
readonly result: ResultOf<"notebook.edit">;
|
|
16
16
|
/** Only a new commit produces a stream update; replaying a receipt does not. */
|
|
17
17
|
readonly update?: Document;
|
|
18
18
|
}
|
|
@@ -52,25 +52,8 @@ function record(
|
|
|
52
52
|
};
|
|
53
53
|
}
|
|
54
54
|
|
|
55
|
-
function rejected(
|
|
56
|
-
status:
|
|
57
|
-
kind: string,
|
|
58
|
-
detail: string,
|
|
59
|
-
edit?: Protocol.Problem["edit"],
|
|
60
|
-
): Outcome {
|
|
61
|
-
return {
|
|
62
|
-
result: {
|
|
63
|
-
ok: false,
|
|
64
|
-
status,
|
|
65
|
-
problem: {
|
|
66
|
-
type: `tag:marimo.io,2026:host/${kind}`,
|
|
67
|
-
title: kind === "conflict" ? "Conflict" : "Invalid request",
|
|
68
|
-
detail,
|
|
69
|
-
status,
|
|
70
|
-
...(edit === undefined ? {} : { edit }),
|
|
71
|
-
},
|
|
72
|
-
},
|
|
73
|
-
};
|
|
55
|
+
function rejected(problem: ProblemOf<"notebook.edit">): Outcome {
|
|
56
|
+
return { result: { ok: false, status: problem.status, problem } };
|
|
74
57
|
}
|
|
75
58
|
|
|
76
59
|
/** An independent implementation of the document edit protocol, without client draft logic. */
|
|
@@ -80,7 +63,7 @@ export function createTestHost(initial: Document) {
|
|
|
80
63
|
string,
|
|
81
64
|
{
|
|
82
65
|
fingerprint: string;
|
|
83
|
-
result:
|
|
66
|
+
result: ResultOf<"notebook.edit">;
|
|
84
67
|
expiresAt: number;
|
|
85
68
|
}
|
|
86
69
|
>();
|
|
@@ -92,9 +75,13 @@ export function createTestHost(initial: Document) {
|
|
|
92
75
|
const version = current.revision.version + 1;
|
|
93
76
|
const matches = (id: Protocol.CellId, etag: string) =>
|
|
94
77
|
etag === "*" || current.cells.get(id)?.etag === etag;
|
|
95
|
-
const conflict = (
|
|
96
|
-
rejected(
|
|
97
|
-
|
|
78
|
+
const conflict = (index: number, id?: Protocol.CellId) =>
|
|
79
|
+
rejected({
|
|
80
|
+
type: "urn:marimo:problem:precondition-failed",
|
|
81
|
+
title: "Precondition failed",
|
|
82
|
+
status: 412,
|
|
83
|
+
detail: `Edit conflict at change ${index}`,
|
|
84
|
+
index,
|
|
98
85
|
cell: id === undefined ? null : (current.cells.get(id) ?? null),
|
|
99
86
|
document: current.revision,
|
|
100
87
|
});
|
|
@@ -108,19 +95,21 @@ export function createTestHost(initial: Document) {
|
|
|
108
95
|
return conflict(index, id);
|
|
109
96
|
}
|
|
110
97
|
if (change.after !== undefined && change.before !== undefined) {
|
|
111
|
-
return rejected(
|
|
112
|
-
|
|
113
|
-
"
|
|
114
|
-
|
|
115
|
-
|
|
98
|
+
return rejected({
|
|
99
|
+
type: "urn:marimo:problem:invalid-request",
|
|
100
|
+
title: "Invalid request",
|
|
101
|
+
status: 400,
|
|
102
|
+
detail: "A cell cannot name both before and after",
|
|
103
|
+
});
|
|
116
104
|
}
|
|
117
105
|
const anchor = change.after ?? change.before;
|
|
118
106
|
if (anchor !== undefined && !order.includes(anchor)) {
|
|
119
|
-
return rejected(
|
|
120
|
-
|
|
121
|
-
"
|
|
122
|
-
|
|
123
|
-
|
|
107
|
+
return rejected({
|
|
108
|
+
type: "urn:marimo:problem:invalid-request",
|
|
109
|
+
title: "Invalid request",
|
|
110
|
+
status: 400,
|
|
111
|
+
detail: "The placement anchor does not exist",
|
|
112
|
+
});
|
|
124
113
|
}
|
|
125
114
|
const position =
|
|
126
115
|
anchor === undefined
|
|
@@ -157,11 +146,12 @@ export function createTestHost(initial: Document) {
|
|
|
157
146
|
new Set(change.order).size !== cells.size ||
|
|
158
147
|
change.order.some((id) => !cells.has(id))
|
|
159
148
|
) {
|
|
160
|
-
return rejected(
|
|
161
|
-
|
|
162
|
-
"
|
|
163
|
-
|
|
164
|
-
|
|
149
|
+
return rejected({
|
|
150
|
+
type: "urn:marimo:problem:invalid-request",
|
|
151
|
+
title: "Invalid request",
|
|
152
|
+
status: 400,
|
|
153
|
+
detail: "Order must name every cell exactly once",
|
|
154
|
+
});
|
|
165
155
|
}
|
|
166
156
|
order = [...change.order];
|
|
167
157
|
break;
|
|
@@ -226,16 +216,22 @@ export function createTestHost(initial: Document) {
|
|
|
226
216
|
if (receipt !== undefined && Date.now() < receipt.expiresAt) {
|
|
227
217
|
return receipt.fingerprint === fingerprint
|
|
228
218
|
? { result: receipt.result }
|
|
229
|
-
: rejected(
|
|
230
|
-
|
|
231
|
-
"
|
|
232
|
-
|
|
233
|
-
|
|
219
|
+
: rejected({
|
|
220
|
+
type: "urn:marimo:problem:conflict",
|
|
221
|
+
title: "Conflict",
|
|
222
|
+
status: 409,
|
|
223
|
+
detail: "Idempotency key was used for another request",
|
|
224
|
+
});
|
|
234
225
|
}
|
|
235
226
|
const outcome =
|
|
236
227
|
request.notebookId === current.revision.notebook_id
|
|
237
228
|
? apply(request.changes)
|
|
238
|
-
: rejected(
|
|
229
|
+
: rejected({
|
|
230
|
+
type: "urn:marimo:problem:not-found",
|
|
231
|
+
title: "Not found",
|
|
232
|
+
status: 404,
|
|
233
|
+
detail: "Notebook does not exist",
|
|
234
|
+
});
|
|
239
235
|
receipts.set(request.idempotencyKey, {
|
|
240
236
|
fingerprint,
|
|
241
237
|
result: outcome.result,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
2
|
|
|
3
|
-
import type {
|
|
3
|
+
import type { ResultOf } from "@marimo-team/client";
|
|
4
4
|
import type * as Protocol from "@marimo-team/client/protocol";
|
|
5
5
|
import { atom, type createStore } from "jotai/vanilla";
|
|
6
6
|
import { vi } from "vitest";
|
|
@@ -41,8 +41,8 @@ export function createTestNotebook(
|
|
|
41
41
|
"The test host answers edits that carry a key and a signal",
|
|
42
42
|
);
|
|
43
43
|
signal.throwIfAborted();
|
|
44
|
-
const reply = new Deferred<
|
|
45
|
-
let outcome:
|
|
44
|
+
const reply = new Deferred<ResultOf<"notebook.edit">>();
|
|
45
|
+
let outcome: ResultOf<"notebook.edit"> | undefined;
|
|
46
46
|
const abort = () => reply.reject(signal.reason);
|
|
47
47
|
const finish = () => signal.removeEventListener("abort", abort);
|
|
48
48
|
signal.addEventListener("abort", abort, { once: true });
|
|
@@ -112,8 +112,9 @@ export function createTestNotebook(
|
|
|
112
112
|
kind: "put_cell",
|
|
113
113
|
base_etag: cell.etag,
|
|
114
114
|
cell: {
|
|
115
|
-
|
|
115
|
+
id,
|
|
116
116
|
code: cell.code,
|
|
117
|
+
name: cell.name,
|
|
117
118
|
...patch,
|
|
118
119
|
config: { ...cell.config, ...patch.config },
|
|
119
120
|
},
|
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
type ProblemOf,
|
|
5
|
+
idempotencyKey,
|
|
6
|
+
type ResultOf,
|
|
7
|
+
} from "@marimo-team/client";
|
|
4
8
|
import type * as Protocol from "@marimo-team/client/protocol";
|
|
5
9
|
import { type Atom, atom, type createStore } from "jotai/vanilla";
|
|
6
10
|
import type { Document, NotebookBackend } from "../replica";
|
|
@@ -18,7 +22,7 @@ export type DocumentSyncStatus =
|
|
|
18
22
|
| {
|
|
19
23
|
readonly status: "error";
|
|
20
24
|
readonly error: Error;
|
|
21
|
-
readonly problem?:
|
|
25
|
+
readonly problem?: ProblemOf<"notebook.edit">;
|
|
22
26
|
readonly retryable: boolean;
|
|
23
27
|
};
|
|
24
28
|
|
|
@@ -30,7 +34,7 @@ interface Submission {
|
|
|
30
34
|
readonly startedAt: number;
|
|
31
35
|
readonly controller: AbortController;
|
|
32
36
|
response?: Protocol.Document;
|
|
33
|
-
problem?:
|
|
37
|
+
problem?: ProblemOf<"notebook.edit">;
|
|
34
38
|
}
|
|
35
39
|
|
|
36
40
|
// The marimo host retains receipts for five minutes. Stop well before that,
|
|
@@ -167,7 +171,7 @@ export function createDocumentSync({
|
|
|
167
171
|
() => controller.abort(new Error("The edit request timed out")),
|
|
168
172
|
REQUEST_TIMEOUT_MS,
|
|
169
173
|
);
|
|
170
|
-
let result:
|
|
174
|
+
let result: ResultOf<"notebook.edit"> | undefined;
|
|
171
175
|
try {
|
|
172
176
|
signal.throwIfAborted();
|
|
173
177
|
request.controller.signal.throwIfAborted();
|
|
@@ -336,7 +340,10 @@ export function createDocumentSync({
|
|
|
336
340
|
}
|
|
337
341
|
if (active?.problem) {
|
|
338
342
|
const current = store.get(source);
|
|
339
|
-
const rejectedAt =
|
|
343
|
+
const rejectedAt =
|
|
344
|
+
active.problem.type === "urn:marimo:problem:precondition-failed"
|
|
345
|
+
? active.problem.document.version
|
|
346
|
+
: undefined;
|
|
340
347
|
if (
|
|
341
348
|
!current ||
|
|
342
349
|
current.revision.notebook_id !== active.submission.notebookId ||
|
|
@@ -30,6 +30,11 @@ export type FakeInput = (
|
|
|
30
30
|
...args: Parameters<Commands["input"]>
|
|
31
31
|
) => ReturnType<Commands["input"]>;
|
|
32
32
|
|
|
33
|
+
export type FakeInterrupt = (
|
|
34
|
+
notebookId: Protocol.NotebookId,
|
|
35
|
+
...args: Parameters<Commands["interrupt"]>
|
|
36
|
+
) => ReturnType<Commands["interrupt"]>;
|
|
37
|
+
|
|
33
38
|
/** A backend over `transport` whose commands go to the given fakes. */
|
|
34
39
|
export function fakeBackend({
|
|
35
40
|
transport = async () => undefined,
|
|
@@ -37,6 +42,7 @@ export function fakeBackend({
|
|
|
37
42
|
save = vi.fn<FakeSave>(),
|
|
38
43
|
run = vi.fn<FakeRun>(),
|
|
39
44
|
input = vi.fn<FakeInput>(),
|
|
45
|
+
interrupt = vi.fn<FakeInterrupt>(),
|
|
40
46
|
checkHealth,
|
|
41
47
|
}: {
|
|
42
48
|
transport?: NotebookTransport;
|
|
@@ -44,6 +50,7 @@ export function fakeBackend({
|
|
|
44
50
|
save?: FakeSave;
|
|
45
51
|
run?: FakeRun;
|
|
46
52
|
input?: FakeInput;
|
|
53
|
+
interrupt?: FakeInterrupt;
|
|
47
54
|
checkHealth?: () => Promise<boolean>;
|
|
48
55
|
} = {}): NotebookBackend {
|
|
49
56
|
return {
|
|
@@ -53,6 +60,7 @@ export function fakeBackend({
|
|
|
53
60
|
save: (...args) => save(notebookId, ...args),
|
|
54
61
|
run: (...args) => run(notebookId, ...args),
|
|
55
62
|
input: (...args) => input(notebookId, ...args),
|
|
63
|
+
interrupt: (...args) => interrupt(notebookId, ...args),
|
|
56
64
|
}),
|
|
57
65
|
checkHealth,
|
|
58
66
|
};
|
|
@@ -15,7 +15,7 @@ export interface NotebookBackend {
|
|
|
15
15
|
*/
|
|
16
16
|
notebook(
|
|
17
17
|
id: NotebookId,
|
|
18
|
-
): Pick<NotebookClient, "edit" | "save" | "run" | "input">;
|
|
18
|
+
): Pick<NotebookClient, "edit" | "save" | "run" | "input" | "interrupt">;
|
|
19
19
|
/** Whether the host is reachable before an explicit reconnect. Absent means always. */
|
|
20
20
|
checkHealth?(): Promise<boolean>;
|
|
21
21
|
}
|
|
@@ -199,7 +199,7 @@ describe("static notebook backend", () => {
|
|
|
199
199
|
ok: false,
|
|
200
200
|
status: 501,
|
|
201
201
|
problem: {
|
|
202
|
-
type: "
|
|
202
|
+
type: "urn:marimo:problem:unsupported-operation",
|
|
203
203
|
title: "Unsupported operation",
|
|
204
204
|
status: 501,
|
|
205
205
|
detail: "Static notebooks do not support mutations",
|
|
@@ -118,7 +118,7 @@ describe("WASM notebook backend", () => {
|
|
|
118
118
|
body: operation === "edit" ? { changes: [] } : {},
|
|
119
119
|
});
|
|
120
120
|
const problem = {
|
|
121
|
-
type: "
|
|
121
|
+
type: "urn:marimo:problem:forbidden",
|
|
122
122
|
title: "Forbidden",
|
|
123
123
|
status: 403,
|
|
124
124
|
};
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
/** A backend that serves a document snapshot without a host. */
|
|
4
4
|
|
|
5
|
+
import type { Result } from "@marimo-team/client";
|
|
5
6
|
import type * as Protocol from "@marimo-team/client/protocol";
|
|
6
7
|
import type { NotebookBackend } from "../notebook/replica";
|
|
7
8
|
|
|
@@ -150,13 +151,17 @@ export function createStaticNotebookBackend(
|
|
|
150
151
|
}
|
|
151
152
|
}),
|
|
152
153
|
notebook: () => {
|
|
153
|
-
const unsupported = async (options?: {
|
|
154
|
+
const unsupported = async (options?: {
|
|
155
|
+
signal?: AbortSignal;
|
|
156
|
+
}): Promise<
|
|
157
|
+
Result<never, Protocol.UnsupportedOperationProblemOutput>
|
|
158
|
+
> => {
|
|
154
159
|
options?.signal?.throwIfAborted();
|
|
155
160
|
return {
|
|
156
161
|
ok: false as const,
|
|
157
162
|
status: 501,
|
|
158
163
|
problem: {
|
|
159
|
-
type: "
|
|
164
|
+
type: "urn:marimo:problem:unsupported-operation",
|
|
160
165
|
title: "Unsupported operation",
|
|
161
166
|
status: 501,
|
|
162
167
|
detail: "Static notebooks do not support mutations",
|
|
@@ -168,6 +173,7 @@ export function createStaticNotebookBackend(
|
|
|
168
173
|
save: unsupported,
|
|
169
174
|
run: unsupported,
|
|
170
175
|
input: (_inputId, _reply, options) => unsupported(options),
|
|
176
|
+
interrupt: unsupported,
|
|
171
177
|
};
|
|
172
178
|
},
|
|
173
179
|
};
|
|
@@ -38,9 +38,7 @@ function abortable<T>(
|
|
|
38
38
|
});
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
-
export function createWasmNotebookBackend(
|
|
42
|
-
bridge: WasmNotebookBridge,
|
|
43
|
-
): NotebookBackend {
|
|
41
|
+
export function createWasmNotebookBackend(bridge: WasmNotebookBridge) {
|
|
44
42
|
return {
|
|
45
43
|
transport: async ({ cursor, signal }, { open, message }) => {
|
|
46
44
|
if (signal.aborted) {
|
|
@@ -93,5 +91,5 @@ export function createWasmNotebookBackend(
|
|
|
93
91
|
},
|
|
94
92
|
notebook: (notebookId) =>
|
|
95
93
|
NotebookClient.unstable_over((call) => bridge.call(call), { notebookId }),
|
|
96
|
-
};
|
|
94
|
+
} satisfies NotebookBackend;
|
|
97
95
|
}
|
|
@@ -16,6 +16,7 @@ const {
|
|
|
16
16
|
rpcListeners,
|
|
17
17
|
mockNotebookSubscribe,
|
|
18
18
|
mockNotebookUnsubscribe,
|
|
19
|
+
workerState,
|
|
19
20
|
} = vi.hoisted(() => ({
|
|
20
21
|
mockBridge: vi.fn(),
|
|
21
22
|
mockNotebookReadFile: vi.fn(),
|
|
@@ -36,10 +37,11 @@ const {
|
|
|
36
37
|
>,
|
|
37
38
|
mockNotebookSubscribe: vi.fn(),
|
|
38
39
|
mockNotebookUnsubscribe: vi.fn(),
|
|
40
|
+
workerState: { interruptBuffer: undefined as Uint8Array | undefined },
|
|
39
41
|
}));
|
|
40
42
|
|
|
41
43
|
// Mock browser globals before any imports
|
|
42
|
-
vi.stubGlobal("crossOriginIsolated",
|
|
44
|
+
vi.stubGlobal("crossOriginIsolated", true);
|
|
43
45
|
vi.stubGlobal(
|
|
44
46
|
"Worker",
|
|
45
47
|
// vitest 4 requires a `function` (not an arrow) for mocks called with `new`
|
|
@@ -74,6 +76,9 @@ vi.mock("@/core/wasm/rpc", () => ({
|
|
|
74
76
|
notebookUnsubscribe: mockNotebookUnsubscribe,
|
|
75
77
|
notebookCall: mockNotebookCall,
|
|
76
78
|
setNotebookLayout: mockSetNotebookLayout,
|
|
79
|
+
setInterruptBuffer: (buffer: Uint8Array) => {
|
|
80
|
+
workerState.interruptBuffer = buffer;
|
|
81
|
+
},
|
|
77
82
|
},
|
|
78
83
|
},
|
|
79
84
|
addMessageListener: (
|
|
@@ -102,7 +107,6 @@ vi.mock("@/core/wasm/store", () => ({
|
|
|
102
107
|
// Import after all mocks are set up
|
|
103
108
|
import { store } from "@/core/state/jotai";
|
|
104
109
|
import { initialModeAtom } from "@/core/mode";
|
|
105
|
-
import type { NotebookClient } from "@marimo-team/client";
|
|
106
110
|
import type { NotebookReplica } from "@/core/notebook/replica";
|
|
107
111
|
import { activeNotebookReplicaAtom } from "@/core/session/active-notebook";
|
|
108
112
|
import { fallbackFileStore, notebookFileStore } from "../store";
|
|
@@ -262,7 +266,7 @@ it("cancels a pending notebook subscription before its RPC response", async () =
|
|
|
262
266
|
}
|
|
263
267
|
});
|
|
264
268
|
|
|
265
|
-
it("raises the interrupt buffer for a running run before the worker takes the call", async () => {
|
|
269
|
+
it("raises the interrupt buffer for a cancelled running run before the worker takes the call", async () => {
|
|
266
270
|
vi.clearAllMocks();
|
|
267
271
|
store.set(initialModeAtom, "read");
|
|
268
272
|
rpcListeners.initialized();
|
|
@@ -284,35 +288,117 @@ it("raises the interrupt buffer for a running run before the worker takes the ca
|
|
|
284
288
|
latest: new Map(),
|
|
285
289
|
}),
|
|
286
290
|
}) as unknown as NotebookReplica;
|
|
287
|
-
const
|
|
288
|
-
|
|
289
|
-
.mockResolvedValue(null);
|
|
291
|
+
const buffer = workerState.interruptBuffer!;
|
|
292
|
+
buffer[0] = 0;
|
|
290
293
|
mockNotebookCall.mockResolvedValue({ status: 202, body: run });
|
|
294
|
+
mockNotebookCall.mockImplementationOnce(async () => {
|
|
295
|
+
expect(buffer[0]).toBe(2);
|
|
296
|
+
return { status: 202, body: run };
|
|
297
|
+
});
|
|
291
298
|
try {
|
|
292
299
|
const backend = await PyodideBridge.INSTANCE.createNotebookBackend();
|
|
293
|
-
|
|
294
|
-
const notebook = backend.notebook("n1") as unknown as NotebookClient;
|
|
300
|
+
const notebook = backend.notebook("n1");
|
|
295
301
|
|
|
296
302
|
store.set(activeNotebookReplicaAtom, replicaWith("running"));
|
|
297
|
-
expect((await notebook.
|
|
298
|
-
expect(
|
|
303
|
+
expect((await notebook.cancel({ runId: "run-1" })).ok).toBe(true);
|
|
304
|
+
expect(buffer[0]).toBe(2);
|
|
299
305
|
expect(mockNotebookCall).toHaveBeenCalledOnce();
|
|
300
|
-
expect(interrupt.mock.invocationCallOrder[0]).toBeLessThan(
|
|
301
|
-
mockNotebookCall.mock.invocationCallOrder[0],
|
|
302
|
-
);
|
|
303
306
|
|
|
307
|
+
buffer[0] = 0;
|
|
304
308
|
store.set(activeNotebookReplicaAtom, replicaWith("queued"));
|
|
305
|
-
expect((await notebook.
|
|
306
|
-
expect(
|
|
309
|
+
expect((await notebook.cancel({ runId: "run-1" })).ok).toBe(true);
|
|
310
|
+
expect(buffer[0]).toBe(0);
|
|
307
311
|
expect(mockNotebookCall).toHaveBeenCalledTimes(2);
|
|
308
312
|
} finally {
|
|
309
|
-
|
|
313
|
+
buffer[0] = 0;
|
|
310
314
|
store.set(activeNotebookReplicaAtom, null);
|
|
311
315
|
store.set(initialModeAtom, undefined);
|
|
312
316
|
}
|
|
313
317
|
});
|
|
314
318
|
|
|
315
|
-
it("
|
|
319
|
+
it.each(["running", "completed", null])(
|
|
320
|
+
"signals runtime work before the RPC with recorded run status %s",
|
|
321
|
+
async (status) => {
|
|
322
|
+
vi.clearAllMocks();
|
|
323
|
+
store.set(initialModeAtom, "read");
|
|
324
|
+
rpcListeners.initialized();
|
|
325
|
+
const run = {
|
|
326
|
+
id: "run-1",
|
|
327
|
+
notebook_id: "n1",
|
|
328
|
+
runtime_id: "r1",
|
|
329
|
+
cells: [],
|
|
330
|
+
status: "running",
|
|
331
|
+
outcome: null,
|
|
332
|
+
started_at: "2026-10-01T09:20:11Z",
|
|
333
|
+
completed_at: null,
|
|
334
|
+
};
|
|
335
|
+
const replicaWith = (status: string) =>
|
|
336
|
+
({
|
|
337
|
+
executionsAtom: atom({
|
|
338
|
+
runs: new Map([["run-1", { ...run, status }]]),
|
|
339
|
+
executions: new Map(),
|
|
340
|
+
latest: new Map(),
|
|
341
|
+
}),
|
|
342
|
+
}) as unknown as NotebookReplica;
|
|
343
|
+
const buffer = workerState.interruptBuffer!;
|
|
344
|
+
buffer[0] = 0;
|
|
345
|
+
const accepted = {
|
|
346
|
+
status: 202,
|
|
347
|
+
body: {
|
|
348
|
+
id: "r1",
|
|
349
|
+
notebook_id: "n1",
|
|
350
|
+
status: "running",
|
|
351
|
+
message: null,
|
|
352
|
+
marimo_version: null,
|
|
353
|
+
started_at: "2026-10-01T09:20:11Z",
|
|
354
|
+
},
|
|
355
|
+
};
|
|
356
|
+
mockNotebookCall.mockImplementation(async () => {
|
|
357
|
+
expect(buffer[0]).toBe(2);
|
|
358
|
+
return accepted;
|
|
359
|
+
});
|
|
360
|
+
try {
|
|
361
|
+
const backend = await PyodideBridge.INSTANCE.createNotebookBackend();
|
|
362
|
+
const notebook = backend.notebook("n1");
|
|
363
|
+
|
|
364
|
+
store.set(
|
|
365
|
+
activeNotebookReplicaAtom,
|
|
366
|
+
status === null ? null : replicaWith(status),
|
|
367
|
+
);
|
|
368
|
+
expect((await notebook.interrupt()).ok).toBe(true);
|
|
369
|
+
expect(buffer[0]).toBe(2);
|
|
370
|
+
|
|
371
|
+
// Nothing holds the worker, so it takes the call and ends queued runs.
|
|
372
|
+
buffer[0] = 0;
|
|
373
|
+
store.set(activeNotebookReplicaAtom, replicaWith("queued"));
|
|
374
|
+
expect((await notebook.interrupt()).ok).toBe(true);
|
|
375
|
+
expect(buffer[0]).toBe(2);
|
|
376
|
+
expect(mockNotebookCall).toHaveBeenCalledTimes(2);
|
|
377
|
+
buffer[0] = 0;
|
|
378
|
+
|
|
379
|
+
// The worker took the queued run before the interrupt reached it.
|
|
380
|
+
const deliver = (status: string) =>
|
|
381
|
+
rpcListeners.notebookEvent({
|
|
382
|
+
id: "sub",
|
|
383
|
+
event: JSON.stringify({
|
|
384
|
+
event: "run",
|
|
385
|
+
data: { run: { ...run, status } },
|
|
386
|
+
}),
|
|
387
|
+
});
|
|
388
|
+
deliver("running");
|
|
389
|
+
expect(buffer[0]).toBe(2);
|
|
390
|
+
buffer[0] = 0;
|
|
391
|
+
deliver("completed");
|
|
392
|
+
expect(buffer[0]).toBe(0);
|
|
393
|
+
} finally {
|
|
394
|
+
buffer[0] = 0;
|
|
395
|
+
store.set(activeNotebookReplicaAtom, null);
|
|
396
|
+
store.set(initialModeAtom, undefined);
|
|
397
|
+
}
|
|
398
|
+
},
|
|
399
|
+
);
|
|
400
|
+
|
|
401
|
+
it("raises the interrupt buffer once a run cancelled while queued starts", async () => {
|
|
316
402
|
vi.clearAllMocks();
|
|
317
403
|
store.set(initialModeAtom, "read");
|
|
318
404
|
rpcListeners.initialized();
|
|
@@ -333,9 +419,8 @@ it("raises the interrupt buffer once a run interrupted while queued starts", asy
|
|
|
333
419
|
latest: new Map(),
|
|
334
420
|
}),
|
|
335
421
|
} as unknown as NotebookReplica);
|
|
336
|
-
const
|
|
337
|
-
|
|
338
|
-
.mockResolvedValue(null);
|
|
422
|
+
const buffer = workerState.interruptBuffer!;
|
|
423
|
+
buffer[0] = 0;
|
|
339
424
|
mockNotebookCall.mockResolvedValue({ status: 202, body: run });
|
|
340
425
|
const deliver = (status: string) =>
|
|
341
426
|
rpcListeners.notebookEvent({
|
|
@@ -347,18 +432,19 @@ it("raises the interrupt buffer once a run interrupted while queued starts", asy
|
|
|
347
432
|
});
|
|
348
433
|
try {
|
|
349
434
|
const backend = await PyodideBridge.INSTANCE.createNotebookBackend();
|
|
350
|
-
const notebook = backend.notebook("n1")
|
|
351
|
-
expect((await notebook.
|
|
352
|
-
expect(
|
|
435
|
+
const notebook = backend.notebook("n1");
|
|
436
|
+
expect((await notebook.cancel({ runId: "run-1" })).ok).toBe(true);
|
|
437
|
+
expect(buffer[0]).toBe(0);
|
|
353
438
|
|
|
354
439
|
// The worker took the run before the cancellation reached it.
|
|
355
440
|
deliver("running");
|
|
356
|
-
expect(
|
|
441
|
+
expect(buffer[0]).toBe(2);
|
|
442
|
+
buffer[0] = 0;
|
|
357
443
|
deliver("running");
|
|
358
444
|
deliver("completed");
|
|
359
|
-
expect(
|
|
445
|
+
expect(buffer[0]).toBe(0);
|
|
360
446
|
} finally {
|
|
361
|
-
|
|
447
|
+
buffer[0] = 0;
|
|
362
448
|
store.set(activeNotebookReplicaAtom, null);
|
|
363
449
|
store.set(initialModeAtom, undefined);
|
|
364
450
|
}
|
|
@@ -388,3 +474,61 @@ describe("getWasmWorkerName", () => {
|
|
|
388
474
|
expect(getWasmWorkerName()).toBe("marimo");
|
|
389
475
|
});
|
|
390
476
|
});
|
|
477
|
+
|
|
478
|
+
it("signals an interrupt once across pending, completed, and lost-reply retries", async () => {
|
|
479
|
+
vi.clearAllMocks();
|
|
480
|
+
store.set(initialModeAtom, "read");
|
|
481
|
+
rpcListeners.initialized();
|
|
482
|
+
const buffer = workerState.interruptBuffer!;
|
|
483
|
+
buffer[0] = 0;
|
|
484
|
+
const clock = vi.spyOn(performance, "now").mockReturnValue(0);
|
|
485
|
+
const pending = new Deferred<unknown>();
|
|
486
|
+
const accepted = {
|
|
487
|
+
status: 202,
|
|
488
|
+
body: {
|
|
489
|
+
id: "r1",
|
|
490
|
+
notebook_id: "n1",
|
|
491
|
+
status: "running",
|
|
492
|
+
message: null,
|
|
493
|
+
marimo_version: null,
|
|
494
|
+
started_at: "2026-10-01T09:20:11Z",
|
|
495
|
+
},
|
|
496
|
+
};
|
|
497
|
+
mockNotebookCall.mockImplementation(() => pending.promise);
|
|
498
|
+
try {
|
|
499
|
+
const backend = await PyodideBridge.INSTANCE.createNotebookBackend();
|
|
500
|
+
const notebook = backend.notebook("n1");
|
|
501
|
+
const options = { idempotencyKey: "retry-interrupt" };
|
|
502
|
+
const first = notebook.interrupt(options);
|
|
503
|
+
expect(buffer[0]).toBe(2);
|
|
504
|
+
buffer[0] = 0;
|
|
505
|
+
const retry = notebook.interrupt(options);
|
|
506
|
+
expect(buffer[0]).toBe(0);
|
|
507
|
+
pending.resolve(accepted);
|
|
508
|
+
expect(await retry).toEqual(await first);
|
|
509
|
+
await notebook.interrupt(options);
|
|
510
|
+
expect(buffer[0]).toBe(0);
|
|
511
|
+
expect(mockNotebookCall).toHaveBeenCalledTimes(3);
|
|
512
|
+
|
|
513
|
+
mockNotebookCall.mockRejectedValueOnce(new Error("Lost reply"));
|
|
514
|
+
await expect(
|
|
515
|
+
notebook.interrupt({ idempotencyKey: "lost-interrupt" }),
|
|
516
|
+
).rejects.toThrow("Lost reply");
|
|
517
|
+
expect(buffer[0]).toBe(2);
|
|
518
|
+
buffer[0] = 0;
|
|
519
|
+
await notebook.interrupt({ idempotencyKey: "lost-interrupt" });
|
|
520
|
+
expect(buffer[0]).toBe(0);
|
|
521
|
+
|
|
522
|
+
// A new user action still signals; settled receipts expire with the worker's.
|
|
523
|
+
await notebook.interrupt();
|
|
524
|
+
expect(buffer[0]).toBe(2);
|
|
525
|
+
buffer[0] = 0;
|
|
526
|
+
clock.mockReturnValue(300_001);
|
|
527
|
+
await notebook.interrupt(options);
|
|
528
|
+
expect(buffer[0]).toBe(2);
|
|
529
|
+
} finally {
|
|
530
|
+
buffer[0] = 0;
|
|
531
|
+
clock.mockRestore();
|
|
532
|
+
store.set(initialModeAtom, undefined);
|
|
533
|
+
}
|
|
534
|
+
});
|