@marimo-team/islands 0.25.3-dev157 → 0.25.3-dev162
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-yJLApVsH.js → ErrorBoundary-D6GPQ8dk.js} +1 -1
- package/dist/{chat-ui-DsH3WbMM.js → chat-ui-BsV2WH-4.js} +1 -1
- package/dist/{code-visibility-sg6NMHZ9.js → code-visibility-DEIiTBMH.js} +1 -1
- package/dist/{common-DNumU4vH.js → common-BmlT-bPP.js} +1 -1
- package/dist/{data-grid-overlay-editor-dapDXQw3.js → data-grid-overlay-editor-20Ge8ID1.js} +1 -1
- package/dist/{feedback-button-BSsnEr1Y.js → feedback-button-zWcCfXEI.js} +2 -2
- package/dist/{glide-data-editor-DZHp-j3z.js → glide-data-editor-Dn1Ekucb.js} +3 -3
- package/dist/{globals-0TvZuVv_.js → globals-D_fhTQMd.js} +1 -1
- package/dist/main.js +4581 -4580
- package/dist/{number-overlay-editor-BC7kWJji.js → number-overlay-editor-ELtg9k3M.js} +1 -1
- package/dist/{reveal-component-WNqfK529.js → reveal-component-CduO3VgT.js} +2 -2
- package/dist/{vega-component-Bd9nzauU.js → vega-component-DC6P127L.js} +12 -12
- package/package.json +1 -1
- package/src/core/codemirror/ai/__tests__/resources.test.ts +117 -0
- package/src/core/codemirror/ai/resources.ts +1 -1
- package/src/plugins/impl/anywidget/__tests__/registry.test.ts +151 -0
- package/src/plugins/impl/anywidget/model.ts +3 -1
- package/src/plugins/impl/anywidget/registry.ts +18 -14
- package/src/utils/__tests__/createReducer.test.ts +1 -0
- package/src/utils/createReducer.ts +11 -5
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { a as e } from "./rolldown-runtime-Cb76FRpK.js";
|
|
2
2
|
import { t } from "./react-sHtZdjeP.js";
|
|
3
|
-
import { a as n } from "./glide-data-editor-
|
|
3
|
+
import { a as n } from "./glide-data-editor-Dn1Ekucb.js";
|
|
4
4
|
//#region ../node_modules/.pnpm/@glideapps+glide-data-grid@6.0.4-alpha24_lodash@4.17.21_marked@15.0.12_react-dom@19.3.0_27cffe7f1bc30287d59ac27db3240c5b/node_modules/@glideapps/glide-data-grid/dist/esm/internal/data-grid-overlay-editor/private/number-overlay-editor-style.js
|
|
5
5
|
var r = /* @__PURE__ */ e(t()), i = /*#__PURE__*/ n("div")({
|
|
6
6
|
name: "NumberOverlayEditorStyle",
|
|
@@ -4,14 +4,14 @@ import { t as o } from "./react-sHtZdjeP.js";
|
|
|
4
4
|
import { t as s } from "./compiler-runtime-CldRdSsv.js";
|
|
5
5
|
import { m as c, t as l } from "./tooltip-CJ4Rqasp.js";
|
|
6
6
|
import { k as u } from "./error-entries-BpUJSLUZ.js";
|
|
7
|
-
import { $ as d, Q as f, Z as p, an as m, i as h, l as g, ln as _, n as v, on as y, r as b, t as x, u as S } from "./common-
|
|
7
|
+
import { $ as d, Q as f, Z as p, an as m, i as h, l as g, ln as _, n as v, on as y, r as b, t as x, u as S } from "./common-BmlT-bPP.js";
|
|
8
8
|
import { M as C } from "./select-CkF_GBxO.js";
|
|
9
9
|
import { J as ee } from "./dropdown-menu-BWnnAstC.js";
|
|
10
10
|
import { _ as w } from "./toDate-rySyYxqk.js";
|
|
11
11
|
import { t as T } from "./jsx-runtime-3BD1kT0s.js";
|
|
12
12
|
import { q as E } from "./use-restore-focus-CmGwRNId.js";
|
|
13
13
|
import { A as D, C as O, b as k } from "./useTheme-TLkqJsvo.js";
|
|
14
|
-
import { a as A, n as te, t as ne } from "./code-visibility-
|
|
14
|
+
import { a as A, n as te, t as ne } from "./code-visibility-DEIiTBMH.js";
|
|
15
15
|
/* empty css */
|
|
16
16
|
var j = c("sticky-note", [["path", {
|
|
17
17
|
d: "M21 9a2.4 2.4 0 0 0-.706-1.706l-3.588-3.588A2.4 2.4 0 0 0 15 3H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2z",
|
|
@@ -12,11 +12,11 @@ import { c as m, t as h } from "./arrays-R6JsImiT.js";
|
|
|
12
12
|
import { t as te } from "./objects-DCmMvZtn.js";
|
|
13
13
|
import { t as ne } from "./useAsyncData-B7MiltyV.js";
|
|
14
14
|
import { a as g, i as re, n as ie, t as ae } from "./formats-CwJHWasD.js";
|
|
15
|
-
import {
|
|
16
|
-
import {
|
|
17
|
-
import {
|
|
15
|
+
import { t as _ } from "./semaphore-89jsoHtS.js";
|
|
16
|
+
import { D as oe, n as se } from "./vega-loader.browser-B9Pz0EGP.js";
|
|
17
|
+
import { t as ce } from "./react-vega-C4T9Rb_w.js";
|
|
18
|
+
import { a as le, n as v } from "./loader-DOVB0Syl.js";
|
|
18
19
|
import { t as ue } from "./useDeepCompareMemoize-DZmHBD_1.js";
|
|
19
|
-
import { t as v } from "./semaphore-89jsoHtS.js";
|
|
20
20
|
import { t as de } from "./use-vega-container-remeasure-Bz4PZMtt.js";
|
|
21
21
|
//#region src/plugins/impl/vega/fix-relative-url.ts
|
|
22
22
|
var fe = /* @__PURE__ */ e(a(), 1), pe = o();
|
|
@@ -463,7 +463,7 @@ function H(e) {
|
|
|
463
463
|
var U = 5;
|
|
464
464
|
async function W(e) {
|
|
465
465
|
if (!e) return e;
|
|
466
|
-
let t = "datasets" in e ? { ...e.datasets } : {}, n = new
|
|
466
|
+
let t = "datasets" in e ? { ...e.datasets } : {}, n = new _(U), r = async (e) => {
|
|
467
467
|
if (!e) return e;
|
|
468
468
|
if ("layer" in e) {
|
|
469
469
|
let t = await Promise.all(e.layer.map(r));
|
|
@@ -496,7 +496,7 @@ async function W(e) {
|
|
|
496
496
|
} catch {
|
|
497
497
|
return e;
|
|
498
498
|
}
|
|
499
|
-
let a = e.data.format, o = await n.run(() =>
|
|
499
|
+
let a = e.data.format, o = await n.run(() => v(i.href, a));
|
|
500
500
|
return t[i.pathname] = o, {
|
|
501
501
|
...e,
|
|
502
502
|
data: { name: i.pathname }
|
|
@@ -510,7 +510,7 @@ async function W(e) {
|
|
|
510
510
|
//#endregion
|
|
511
511
|
//#region src/plugins/impl/vega/vega-component.tsx
|
|
512
512
|
var G = u();
|
|
513
|
-
|
|
513
|
+
se("arrow", ae);
|
|
514
514
|
var K = (e) => {
|
|
515
515
|
let t = (0, pe.c)(12), { value: n, setValue: r, chartSelection: i, fieldSelection: a, spec: o, embedOptions: s } = e, c, l;
|
|
516
516
|
t[0] === o ? (c = t[1], l = t[2]) : (c = async () => W(o), l = [o], t[0] = o, t[1] = c, t[2] = l);
|
|
@@ -530,7 +530,7 @@ var K = (e) => {
|
|
|
530
530
|
embedOptions: s
|
|
531
531
|
}), t[5] = i, t[6] = s, t[7] = a, t[8] = u, t[9] = r, t[10] = n, t[11] = p) : p = t[11], p;
|
|
532
532
|
}, q = (e) => {
|
|
533
|
-
let a = (0, pe.c)(50), { value: o, setValue: l, chartSelection: u, fieldSelection: d, spec: m, embedOptions: h } = e, { theme: ne } = ee(), g = (0, fe.useRef)(null), ae = (0, fe.useRef)(void 0), [_,
|
|
533
|
+
let a = (0, pe.c)(50), { value: o, setValue: l, chartSelection: u, fieldSelection: d, spec: m, embedOptions: h } = e, { theme: ne } = ee(), g = (0, fe.useRef)(null), ae = (0, fe.useRef)(void 0), [_, se] = (0, fe.useState)(), v;
|
|
534
534
|
bb0: {
|
|
535
535
|
if (h && "actions" in h) {
|
|
536
536
|
v = h.actions;
|
|
@@ -572,11 +572,11 @@ var K = (e) => {
|
|
|
572
572
|
} else M = a[14];
|
|
573
573
|
let N = M, P;
|
|
574
574
|
a[17] === C ? P = a[18] : (P = (e) => {
|
|
575
|
-
t.error(e), t.debug(C),
|
|
575
|
+
t.error(e), t.debug(C), se(e);
|
|
576
576
|
}, a[17] = C, a[18] = P);
|
|
577
577
|
let F = p(P), I;
|
|
578
578
|
a[19] === Symbol.for("react.memo_cache_sentinel") ? (I = (e) => {
|
|
579
|
-
t.debug("[Vega view] created", e), ae.current = e,
|
|
579
|
+
t.debug("[Vega view] created", e), ae.current = e, se(void 0);
|
|
580
580
|
}, a[19] = I) : I = a[19];
|
|
581
581
|
let L = p(I), R;
|
|
582
582
|
a[20] === T ? R = a[21] : (R = () => {
|
|
@@ -600,7 +600,7 @@ var K = (e) => {
|
|
|
600
600
|
theme: B,
|
|
601
601
|
actions: y,
|
|
602
602
|
mode: "vega-lite",
|
|
603
|
-
tooltip:
|
|
603
|
+
tooltip: le.call,
|
|
604
604
|
renderer: "canvas"
|
|
605
605
|
}, a[22] = y, a[23] = B, a[24] = V) : V = a[24];
|
|
606
606
|
let H;
|
|
@@ -611,7 +611,7 @@ var K = (e) => {
|
|
|
611
611
|
onError: F,
|
|
612
612
|
onEmbed: L
|
|
613
613
|
}, a[25] = F, a[26] = L, a[27] = C, a[28] = V, a[29] = H) : H = a[29];
|
|
614
|
-
let U =
|
|
614
|
+
let U = ce(H), W;
|
|
615
615
|
a[30] === C ? W = a[31] : (W = oe(C), a[30] = C, a[31] = W);
|
|
616
616
|
let K = W;
|
|
617
617
|
de(g, K === "container");
|
package/package.json
CHANGED
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
|
+
|
|
3
|
+
import { markdownLanguage } from "@codemirror/lang-markdown";
|
|
4
|
+
import { hoverResource, type Resource } from "@marimo-team/codemirror-mcp";
|
|
5
|
+
import { createStore } from "jotai";
|
|
6
|
+
import { assert, beforeEach, describe, expect, it, vi } from "vitest";
|
|
7
|
+
import { MockNotebook } from "@/__mocks__/notebook";
|
|
8
|
+
import { getAIContextRegistry } from "@/core/ai/context/context";
|
|
9
|
+
import { ErrorContextProvider } from "@/core/ai/context/providers/error";
|
|
10
|
+
import { AIContextRegistry } from "@/core/ai/context/registry";
|
|
11
|
+
import { notebookAtom } from "@/core/cells/cells";
|
|
12
|
+
import { CellId } from "@/core/cells/ids";
|
|
13
|
+
import { resourceExtension } from "../resources";
|
|
14
|
+
|
|
15
|
+
vi.mock("@marimo-team/codemirror-mcp", async (importOriginal) => ({
|
|
16
|
+
...(await importOriginal<typeof import("@marimo-team/codemirror-mcp")>()),
|
|
17
|
+
hoverResource: vi.fn(() => []),
|
|
18
|
+
}));
|
|
19
|
+
|
|
20
|
+
vi.mock("@/core/ai/context/context", () => ({
|
|
21
|
+
getAIContextRegistry: vi.fn(),
|
|
22
|
+
}));
|
|
23
|
+
|
|
24
|
+
describe("resource tooltips", () => {
|
|
25
|
+
const message = '<b>Unexpected response</b> & "details"';
|
|
26
|
+
let provider: ErrorContextProvider;
|
|
27
|
+
let resource: Resource;
|
|
28
|
+
|
|
29
|
+
beforeEach(() => {
|
|
30
|
+
vi.clearAllMocks();
|
|
31
|
+
const store = createStore();
|
|
32
|
+
store.set(
|
|
33
|
+
notebookAtom,
|
|
34
|
+
MockNotebook.notebookStateWithErrors([
|
|
35
|
+
{
|
|
36
|
+
cellId: CellId.create(),
|
|
37
|
+
cellName: "Cell 1",
|
|
38
|
+
errorData: [MockNotebook.errors.exception(message)],
|
|
39
|
+
},
|
|
40
|
+
]),
|
|
41
|
+
);
|
|
42
|
+
provider = new ErrorContextProvider(store);
|
|
43
|
+
resource = provider.getItems()[1];
|
|
44
|
+
vi.mocked(getAIContextRegistry).mockReturnValue(
|
|
45
|
+
new AIContextRegistry().register(provider),
|
|
46
|
+
);
|
|
47
|
+
resourceExtension({ language: markdownLanguage, store });
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
function renderTooltip(item = resource) {
|
|
51
|
+
const options = vi.mocked(hoverResource).mock.calls[0][0];
|
|
52
|
+
assert(options.createTooltip);
|
|
53
|
+
return options.createTooltip(item).dom;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
it("preserves literal text in cell error descriptions", () => {
|
|
57
|
+
const dom = renderTooltip();
|
|
58
|
+
expect(dom.textContent).toBe(message);
|
|
59
|
+
expect(dom.childElementCount).toBe(0);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it("preserves literal descriptions when a provider is unavailable", () => {
|
|
63
|
+
const dom = renderTooltip({ ...resource, type: "unknown" });
|
|
64
|
+
expect(dom.textContent).toBe(message);
|
|
65
|
+
expect(dom.childElementCount).toBe(0);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it("falls back to the literal resource name", () => {
|
|
69
|
+
const dom = renderTooltip({
|
|
70
|
+
...resource,
|
|
71
|
+
description: undefined,
|
|
72
|
+
name: message,
|
|
73
|
+
});
|
|
74
|
+
expect(dom.textContent).toBe(message);
|
|
75
|
+
expect(dom.childElementCount).toBe(0);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it("preserves literal text in completion info strings", () => {
|
|
79
|
+
vi.spyOn(provider, "formatCompletion").mockReturnValue({
|
|
80
|
+
label: "Error",
|
|
81
|
+
info: message,
|
|
82
|
+
});
|
|
83
|
+
const dom = renderTooltip({ ...resource, description: "Fallback" });
|
|
84
|
+
expect(dom.textContent).toBe(message);
|
|
85
|
+
expect(dom.childElementCount).toBe(0);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it("preserves literal fallback text when an info renderer returns null", () => {
|
|
89
|
+
vi.spyOn(provider, "formatCompletion").mockReturnValue({
|
|
90
|
+
label: "Error",
|
|
91
|
+
info: () => null,
|
|
92
|
+
});
|
|
93
|
+
const dom = renderTooltip();
|
|
94
|
+
expect(dom.textContent).toBe(message);
|
|
95
|
+
expect(dom.childElementCount).toBe(0);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it("preserves literal fallback text for async info renderers", () => {
|
|
99
|
+
vi.spyOn(provider, "formatCompletion").mockReturnValue({
|
|
100
|
+
label: "Error",
|
|
101
|
+
info: async () => document.createElement("div"),
|
|
102
|
+
});
|
|
103
|
+
const dom = renderTooltip();
|
|
104
|
+
expect(dom.textContent).toBe(message);
|
|
105
|
+
expect(dom.childElementCount).toBe(0);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it.each(["element", "tooltip"])("preserves custom %s renderers", (kind) => {
|
|
109
|
+
const dom = document.createElement("strong");
|
|
110
|
+
dom.textContent = "Details";
|
|
111
|
+
vi.spyOn(provider, "formatCompletion").mockReturnValue({
|
|
112
|
+
label: "Error",
|
|
113
|
+
info: () => (kind === "element" ? dom : { dom }),
|
|
114
|
+
});
|
|
115
|
+
expect(renderTooltip()).toBe(dom);
|
|
116
|
+
});
|
|
117
|
+
});
|
|
@@ -8,6 +8,8 @@ import {
|
|
|
8
8
|
type MockInstance,
|
|
9
9
|
vi,
|
|
10
10
|
} from "vitest";
|
|
11
|
+
import { Deferred } from "@/utils/Deferred";
|
|
12
|
+
import { Logger } from "@/utils/Logger";
|
|
11
13
|
import { getMarimoInternal, Model } from "../model";
|
|
12
14
|
import { handleWidgetMessage, WidgetRegistry } from "../registry";
|
|
13
15
|
import type { EsmSpec, ModelState, WidgetModelId } from "../types";
|
|
@@ -155,6 +157,155 @@ describe("WidgetRegistry models", () => {
|
|
|
155
157
|
);
|
|
156
158
|
});
|
|
157
159
|
|
|
160
|
+
describe("outgoing messages", () => {
|
|
161
|
+
async function openModel(id = testId) {
|
|
162
|
+
await handleWidgetMessage(registry, {
|
|
163
|
+
model_id: id,
|
|
164
|
+
message: {
|
|
165
|
+
method: "open",
|
|
166
|
+
state: {},
|
|
167
|
+
buffer_paths: [],
|
|
168
|
+
buffers: [],
|
|
169
|
+
},
|
|
170
|
+
});
|
|
171
|
+
return registry.getModel(id);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
afterEach(() => {
|
|
175
|
+
mockSendModelValue.mockReset().mockResolvedValue(null);
|
|
176
|
+
vi.restoreAllMocks();
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
it("waits for each acknowledgement before sending the next update", async () => {
|
|
180
|
+
const model = await openModel();
|
|
181
|
+
const responses = [new Deferred<null>(), new Deferred<null>()];
|
|
182
|
+
for (const response of responses) {
|
|
183
|
+
mockSendModelValue.mockReturnValueOnce(response.promise);
|
|
184
|
+
}
|
|
185
|
+
const states = [
|
|
186
|
+
{ card: "A", seq: 5 },
|
|
187
|
+
{ card: "", seq: 6 },
|
|
188
|
+
{ card: "B", seq: 7 },
|
|
189
|
+
];
|
|
190
|
+
for (const state of states) {
|
|
191
|
+
model.set("selection", state);
|
|
192
|
+
model.save_changes();
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
await vi.waitFor(() =>
|
|
196
|
+
expect(mockSendModelValue).toHaveBeenCalledTimes(1),
|
|
197
|
+
);
|
|
198
|
+
for (const [index, response] of responses.entries()) {
|
|
199
|
+
response.resolve(null);
|
|
200
|
+
await vi.waitFor(() =>
|
|
201
|
+
expect(mockSendModelValue).toHaveBeenCalledTimes(index + 2),
|
|
202
|
+
);
|
|
203
|
+
}
|
|
204
|
+
expect(mockSendModelValue.mock.calls).toEqual(
|
|
205
|
+
states.map((selection) => [
|
|
206
|
+
{
|
|
207
|
+
modelId: testId,
|
|
208
|
+
message: {
|
|
209
|
+
method: "update",
|
|
210
|
+
state: { selection },
|
|
211
|
+
bufferPaths: [],
|
|
212
|
+
},
|
|
213
|
+
buffers: [],
|
|
214
|
+
},
|
|
215
|
+
]),
|
|
216
|
+
);
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
it("orders custom messages with updates and waits before calling callbacks", async () => {
|
|
220
|
+
const model = await openModel();
|
|
221
|
+
const response = new Deferred<null>();
|
|
222
|
+
mockSendModelValue.mockReturnValueOnce(response.promise);
|
|
223
|
+
const callback = vi.fn();
|
|
224
|
+
const sent = model.send({ action: "first" }, callback);
|
|
225
|
+
model.set("count", 1);
|
|
226
|
+
model.save_changes();
|
|
227
|
+
const last = model.send({ action: "last" });
|
|
228
|
+
|
|
229
|
+
await vi.waitFor(() =>
|
|
230
|
+
expect(mockSendModelValue).toHaveBeenCalledTimes(1),
|
|
231
|
+
);
|
|
232
|
+
expect(callback).not.toHaveBeenCalled();
|
|
233
|
+
response.resolve(null);
|
|
234
|
+
await Promise.all([sent, last]);
|
|
235
|
+
expect(callback).toHaveBeenCalledExactlyOnceWith();
|
|
236
|
+
expect(
|
|
237
|
+
mockSendModelValue.mock.calls.map(([request]) => request.message),
|
|
238
|
+
).toEqual([
|
|
239
|
+
{ method: "custom", content: { action: "first" } },
|
|
240
|
+
{ method: "update", state: { count: 1 }, bufferPaths: [] },
|
|
241
|
+
{ method: "custom", content: { action: "last" } },
|
|
242
|
+
]);
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
it("continues sending after a failed update", async () => {
|
|
246
|
+
const model = await openModel();
|
|
247
|
+
const response = new Deferred<null>();
|
|
248
|
+
mockSendModelValue.mockReturnValueOnce(response.promise);
|
|
249
|
+
const logError = vi
|
|
250
|
+
.spyOn(Logger, "error")
|
|
251
|
+
.mockImplementation(() => undefined);
|
|
252
|
+
model.set("count", 1);
|
|
253
|
+
model.save_changes();
|
|
254
|
+
model.set("count", 2);
|
|
255
|
+
model.save_changes();
|
|
256
|
+
const last = model.send({ action: "done" });
|
|
257
|
+
|
|
258
|
+
await vi.waitFor(() =>
|
|
259
|
+
expect(mockSendModelValue).toHaveBeenCalledTimes(1),
|
|
260
|
+
);
|
|
261
|
+
const error = new Error("Network failure");
|
|
262
|
+
response.reject(error);
|
|
263
|
+
await last;
|
|
264
|
+
expect(mockSendModelValue).toHaveBeenCalledTimes(3);
|
|
265
|
+
expect(logError).toHaveBeenCalledWith(
|
|
266
|
+
"[anywidget] Failed to save model changes",
|
|
267
|
+
error,
|
|
268
|
+
);
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
it("allows different models to send independently", async () => {
|
|
272
|
+
const first = await openModel();
|
|
273
|
+
const secondId = asModelId("second-model");
|
|
274
|
+
const second = await openModel(secondId);
|
|
275
|
+
const response = new Deferred<null>();
|
|
276
|
+
mockSendModelValue.mockReturnValueOnce(response.promise);
|
|
277
|
+
|
|
278
|
+
const firstSend = first.send({ value: 1 });
|
|
279
|
+
await second.send({ value: 2 });
|
|
280
|
+
expect(
|
|
281
|
+
mockSendModelValue.mock.calls.map(([request]) => request.modelId),
|
|
282
|
+
).toEqual([testId, secondId]);
|
|
283
|
+
response.resolve(null);
|
|
284
|
+
await firstSend;
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
it("drops queued messages when the model closes", async () => {
|
|
288
|
+
const model = await openModel();
|
|
289
|
+
const response = new Deferred<null>();
|
|
290
|
+
mockSendModelValue.mockReturnValueOnce(response.promise);
|
|
291
|
+
const first = model.send({ value: 1 });
|
|
292
|
+
model.set("count", 2);
|
|
293
|
+
model.save_changes();
|
|
294
|
+
const last = model.send({ value: 3 });
|
|
295
|
+
|
|
296
|
+
await vi.waitFor(() =>
|
|
297
|
+
expect(mockSendModelValue).toHaveBeenCalledTimes(1),
|
|
298
|
+
);
|
|
299
|
+
await handleWidgetMessage(registry, {
|
|
300
|
+
model_id: testId,
|
|
301
|
+
message: { method: "close" },
|
|
302
|
+
});
|
|
303
|
+
response.resolve(null);
|
|
304
|
+
await Promise.all([first, last]);
|
|
305
|
+
expect(mockSendModelValue).toHaveBeenCalledTimes(1);
|
|
306
|
+
});
|
|
307
|
+
});
|
|
308
|
+
|
|
158
309
|
describe("static mode", () => {
|
|
159
310
|
beforeEach(() => {
|
|
160
311
|
mockIsStatic.mockReturnValue(true);
|
|
@@ -157,7 +157,9 @@ export class Model<T extends ModelState> implements AnyModel<T> {
|
|
|
157
157
|
|
|
158
158
|
// Clear the dirty fields to avoid sending again.
|
|
159
159
|
this.#dirtyFields.clear();
|
|
160
|
-
this.#comm.sendUpdate(partialData)
|
|
160
|
+
this.#comm.sendUpdate(partialData).catch((error) => {
|
|
161
|
+
Logger.error("[anywidget] Failed to save model changes", error);
|
|
162
|
+
});
|
|
161
163
|
}
|
|
162
164
|
|
|
163
165
|
/**
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
2
|
import type { NotificationMessageData } from "@/core/kernel/messages";
|
|
3
3
|
import { getRequestClient } from "@/core/network/requests";
|
|
4
|
+
import type { ModelRequest } from "@/core/network/types";
|
|
4
5
|
import { isStaticNotebook } from "@/core/static/static-state";
|
|
5
6
|
import { assertNever } from "@/utils/assertNever";
|
|
6
7
|
import {
|
|
@@ -10,6 +11,7 @@ import {
|
|
|
10
11
|
} from "@/utils/json/base64";
|
|
11
12
|
import { Logger } from "@/utils/Logger";
|
|
12
13
|
import { repl } from "@/utils/repl";
|
|
14
|
+
import { Semaphore } from "@/utils/semaphore";
|
|
13
15
|
import { viewStateAtom } from "@/core/mode";
|
|
14
16
|
import { store } from "@/core/state/jotai";
|
|
15
17
|
import { createHost, type WidgetResolver } from "./host";
|
|
@@ -191,6 +193,20 @@ export async function handleWidgetMessage(
|
|
|
191
193
|
}
|
|
192
194
|
|
|
193
195
|
registry.createModel(modelId, (signal) => {
|
|
196
|
+
// Wait for the server to acknowledge queueing each message before
|
|
197
|
+
// sending the next one, preserving order within this model.
|
|
198
|
+
const sends = new Semaphore(1);
|
|
199
|
+
const sendMessage = (request: ModelRequest): Promise<void> =>
|
|
200
|
+
sends.run(async () => {
|
|
201
|
+
if (signal.aborted) {
|
|
202
|
+
Logger.debug(
|
|
203
|
+
`[Model] ${request.message.method} suppressed for model=${modelId} (signal aborted)`,
|
|
204
|
+
);
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
await getRequestClient().sendModelValue(request);
|
|
208
|
+
});
|
|
209
|
+
|
|
194
210
|
// In static exports there is no kernel, so comm calls are no-ops.
|
|
195
211
|
const comm: MarimoComm<ModelState> = isStaticNotebook()
|
|
196
212
|
? {
|
|
@@ -199,28 +215,16 @@ export async function handleWidgetMessage(
|
|
|
199
215
|
}
|
|
200
216
|
: {
|
|
201
217
|
async sendUpdate(changeData) {
|
|
202
|
-
if (signal.aborted) {
|
|
203
|
-
Logger.debug(
|
|
204
|
-
`[Model] sendUpdate suppressed for model=${modelId} (signal aborted)`,
|
|
205
|
-
);
|
|
206
|
-
return;
|
|
207
|
-
}
|
|
208
218
|
const { state, buffers, bufferPaths } =
|
|
209
219
|
serializeBuffersToBase64(changeData);
|
|
210
|
-
await
|
|
220
|
+
await sendMessage({
|
|
211
221
|
modelId,
|
|
212
222
|
message: { method: "update", state, bufferPaths },
|
|
213
223
|
buffers,
|
|
214
224
|
});
|
|
215
225
|
},
|
|
216
226
|
async sendCustomMessage(content, buffers) {
|
|
217
|
-
|
|
218
|
-
Logger.debug(
|
|
219
|
-
`[Model] sendCustomMessage suppressed for model=${modelId} (signal aborted)`,
|
|
220
|
-
);
|
|
221
|
-
return;
|
|
222
|
-
}
|
|
223
|
-
await getRequestClient().sendModelValue({
|
|
227
|
+
await sendMessage({
|
|
224
228
|
modelId,
|
|
225
229
|
message: { method: "custom", content },
|
|
226
230
|
buffers: buffers.map(dataViewToBase64),
|
|
@@ -93,6 +93,7 @@ describe("createReducer", () => {
|
|
|
93
93
|
const loggerSpy = vi.spyOn(Logger, "error");
|
|
94
94
|
|
|
95
95
|
const state = { count: 0 };
|
|
96
|
+
// @ts-expect-error - exercise runtime handling of an invalid action
|
|
96
97
|
const newState = reducer(state, { type: "nonexistent", payload: null });
|
|
97
98
|
|
|
98
99
|
expect(newState).toBe(state);
|
|
@@ -16,21 +16,27 @@ interface ReducerHandlers<State> {
|
|
|
16
16
|
[k: string]: ReducerHandler<State, any>;
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
-
type ReducerActions<RH
|
|
20
|
-
[Type in keyof RH]: RH[Type] extends
|
|
19
|
+
type ReducerActions<RH> = {
|
|
20
|
+
[Type in keyof RH]: RH[Type] extends (
|
|
21
|
+
state: never,
|
|
22
|
+
payload: infer Payload,
|
|
23
|
+
) => unknown
|
|
21
24
|
? IfUnknown<Payload, () => void, (payload: Payload) => void>
|
|
22
25
|
: never;
|
|
23
26
|
};
|
|
24
27
|
|
|
25
28
|
/** Helper for typing middleware that receives dispatched actions. */
|
|
26
29
|
export type DispatchedActionOf<T> = {
|
|
27
|
-
[Key in keyof T]: T[Key] extends (payload: infer P) =>
|
|
30
|
+
[Key in keyof T]: T[Key] extends (payload: infer P) => unknown
|
|
28
31
|
? { type: Key; payload: P }
|
|
29
32
|
: never;
|
|
30
33
|
}[keyof T & string];
|
|
31
34
|
|
|
32
35
|
type ReducerActionOf<RH> = {
|
|
33
|
-
[Type in keyof RH]: RH[Type] extends
|
|
36
|
+
[Type in keyof RH]: RH[Type] extends (
|
|
37
|
+
state: never,
|
|
38
|
+
payload: infer Payload,
|
|
39
|
+
) => unknown
|
|
34
40
|
? { type: Type; payload: Payload }
|
|
35
41
|
: never;
|
|
36
42
|
}[keyof RH & string];
|
|
@@ -42,7 +48,7 @@ export interface ReducerCreatorResult<
|
|
|
42
48
|
/**
|
|
43
49
|
* The reducer function.
|
|
44
50
|
*/
|
|
45
|
-
reducer: Reducer<State,
|
|
51
|
+
reducer: Reducer<State, ReducerActionOf<RH>>;
|
|
46
52
|
/**
|
|
47
53
|
* Given a dispatch function, returns an object containing all the actions.
|
|
48
54
|
*/
|