@marimo-team/frontend 0.25.3-dev104 → 0.25.3-dev105
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/package.json
CHANGED
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
|
+
|
|
3
|
+
import { createNotebookReducer, NotebookEvents } from "@marimo-team/client";
|
|
4
|
+
import * as Protocol from "@marimo-team/client/protocol";
|
|
5
|
+
import { afterEach, assert, describe, expect, it, vi } from "vitest";
|
|
6
|
+
import { document as documentResource } from "../resources/document";
|
|
7
|
+
import {
|
|
8
|
+
createStaticNotebookBackend,
|
|
9
|
+
type StaticNotebookSnapshot,
|
|
10
|
+
} from "../static";
|
|
11
|
+
import { fixtureEvents } from "./fixtures";
|
|
12
|
+
|
|
13
|
+
function snapshot(): StaticNotebookSnapshot {
|
|
14
|
+
const events = fixtureEvents("notebook-events.sse");
|
|
15
|
+
const notebook = events.find((event) => event.event === "notebook")?.data
|
|
16
|
+
.notebook;
|
|
17
|
+
const document = events.find((event) => event.event === "document")?.data
|
|
18
|
+
.document;
|
|
19
|
+
assert(notebook && document);
|
|
20
|
+
return {
|
|
21
|
+
notebook,
|
|
22
|
+
document,
|
|
23
|
+
cells: events.flatMap((event) =>
|
|
24
|
+
event.event === "cell" ? [event.data.cell] : [],
|
|
25
|
+
),
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const streams: NotebookEvents[] = [];
|
|
30
|
+
afterEach(async () => {
|
|
31
|
+
const closed = streams.splice(0);
|
|
32
|
+
closed.forEach((stream) => stream.close());
|
|
33
|
+
await Promise.all(closed.map((stream) => stream.closed));
|
|
34
|
+
vi.useRealTimers();
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
async function read(
|
|
38
|
+
backend: ReturnType<typeof createStaticNotebookBackend>,
|
|
39
|
+
cursor?: string,
|
|
40
|
+
) {
|
|
41
|
+
const stream = new NotebookEvents(backend.transport, { cursor });
|
|
42
|
+
streams.push(stream);
|
|
43
|
+
const events: Protocol.EventOutput[] = [];
|
|
44
|
+
await new Promise<void>((resolve, reject) => {
|
|
45
|
+
stream.subscribe((event) => {
|
|
46
|
+
events.push(Protocol.Event.parse(event));
|
|
47
|
+
if (event.event === "ready") {
|
|
48
|
+
resolve();
|
|
49
|
+
}
|
|
50
|
+
}, reject);
|
|
51
|
+
});
|
|
52
|
+
return { stream, events };
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
describe("static notebook backend", () => {
|
|
56
|
+
it("supplies a complete document through the normal event reader and reducer", async () => {
|
|
57
|
+
const source = snapshot();
|
|
58
|
+
const { stream, events } = await read(createStaticNotebookBackend(source));
|
|
59
|
+
const reducer = createNotebookReducer({ document: documentResource });
|
|
60
|
+
const state = events.reduce(reducer.reduce, reducer.initial);
|
|
61
|
+
expect(events.map((event) => event.event)).toEqual([
|
|
62
|
+
"notebook",
|
|
63
|
+
"cell",
|
|
64
|
+
"cell",
|
|
65
|
+
"cell",
|
|
66
|
+
"document",
|
|
67
|
+
"ready",
|
|
68
|
+
]);
|
|
69
|
+
expect(state.notebook).toEqual({ ...source.notebook, operations: [] });
|
|
70
|
+
expect(state.resources.document.current).toEqual({
|
|
71
|
+
revision: source.document,
|
|
72
|
+
cells: new Map(source.cells.map((cell) => [cell.id, cell])),
|
|
73
|
+
});
|
|
74
|
+
expect(stream.status).toBe("connected");
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it("preserves withheld source and its revision metadata", async () => {
|
|
78
|
+
const source = snapshot();
|
|
79
|
+
const cells = source.cells.map((cell) => ({ ...cell, code: null }));
|
|
80
|
+
const { events } = await read(
|
|
81
|
+
createStaticNotebookBackend({ ...source, cells }),
|
|
82
|
+
);
|
|
83
|
+
expect(
|
|
84
|
+
events
|
|
85
|
+
.filter((event) => event.event === "cell")
|
|
86
|
+
.map((event) => event.data.cell),
|
|
87
|
+
).toEqual(cells);
|
|
88
|
+
expect(
|
|
89
|
+
events.find((event) => event.event === "document")?.data.document,
|
|
90
|
+
).toEqual(source.document);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it("serves an empty document as available", async () => {
|
|
94
|
+
const source = snapshot();
|
|
95
|
+
const document = { ...source.document, order: [] };
|
|
96
|
+
const { events } = await read(
|
|
97
|
+
createStaticNotebookBackend({ ...source, document, cells: [] }),
|
|
98
|
+
);
|
|
99
|
+
const reducer = createNotebookReducer({ document: documentResource });
|
|
100
|
+
expect(
|
|
101
|
+
events.reduce(reducer.reduce, reducer.initial).resources.document.current,
|
|
102
|
+
).toEqual({
|
|
103
|
+
revision: document,
|
|
104
|
+
cells: new Map(),
|
|
105
|
+
});
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it("keeps independent readers open without polling or reconnecting", async () => {
|
|
109
|
+
vi.useFakeTimers();
|
|
110
|
+
const backend = createStaticNotebookBackend(snapshot());
|
|
111
|
+
const first = await read(backend);
|
|
112
|
+
const second = await read(backend);
|
|
113
|
+
first.stream.close();
|
|
114
|
+
await first.stream.closed;
|
|
115
|
+
await vi.advanceTimersByTimeAsync(60_000);
|
|
116
|
+
expect(second.events).toEqual(first.events);
|
|
117
|
+
expect(second.stream.status).toBe("connected");
|
|
118
|
+
expect(vi.getTimerCount()).toBe(0);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it("resets a resume cursor before replaying its snapshot", async () => {
|
|
122
|
+
const { events } = await read(
|
|
123
|
+
createStaticNotebookBackend(snapshot()),
|
|
124
|
+
"previous-10",
|
|
125
|
+
);
|
|
126
|
+
expect(events[0]).toEqual({ event: "reset", data: {} });
|
|
127
|
+
expect(events.at(-1)).toEqual({ event: "ready", id: "static", data: {} });
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it("advertises no mutations and returns a protocol failure for direct edits", async () => {
|
|
131
|
+
const backend = createStaticNotebookBackend(snapshot());
|
|
132
|
+
expect(await backend.client.edit([])).toEqual({
|
|
133
|
+
ok: false,
|
|
134
|
+
status: 501,
|
|
135
|
+
problem: {
|
|
136
|
+
type: "tag:marimo.io,2026:host/unsupported-operation",
|
|
137
|
+
title: "Unsupported operation",
|
|
138
|
+
status: 501,
|
|
139
|
+
detail: "Static notebooks do not support editing",
|
|
140
|
+
},
|
|
141
|
+
});
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it("respects cancellation before opening, during delivery, and for commands", async () => {
|
|
145
|
+
const backend = createStaticNotebookBackend(snapshot());
|
|
146
|
+
const before = new AbortController();
|
|
147
|
+
before.abort();
|
|
148
|
+
const open = vi.fn();
|
|
149
|
+
const message = vi.fn();
|
|
150
|
+
await backend.transport(
|
|
151
|
+
{ cursor: null, signal: before.signal },
|
|
152
|
+
{ open, message },
|
|
153
|
+
);
|
|
154
|
+
expect(open).not.toHaveBeenCalled();
|
|
155
|
+
expect(message).not.toHaveBeenCalled();
|
|
156
|
+
await expect(
|
|
157
|
+
backend.client.edit([], { signal: before.signal }),
|
|
158
|
+
).rejects.toBe(before.signal.reason);
|
|
159
|
+
const during = new AbortController();
|
|
160
|
+
message.mockImplementation(() => during.abort());
|
|
161
|
+
await backend.transport(
|
|
162
|
+
{ cursor: null, signal: during.signal },
|
|
163
|
+
{ open, message },
|
|
164
|
+
);
|
|
165
|
+
expect(message).toHaveBeenCalledOnce();
|
|
166
|
+
});
|
|
167
|
+
});
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
|
+
|
|
3
|
+
import type { NotebookClient, NotebookTransport } from "@marimo-team/client";
|
|
4
|
+
|
|
5
|
+
/** Events and commands for the same notebook, independent of React or a replica. */
|
|
6
|
+
export interface NotebookBackend {
|
|
7
|
+
readonly transport: NotebookTransport;
|
|
8
|
+
/** Method presence is not permission; the notebook advertises its operations. */
|
|
9
|
+
readonly client: Pick<NotebookClient, "edit">;
|
|
10
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
|
+
|
|
3
|
+
import type * as Protocol from "@marimo-team/client/protocol";
|
|
4
|
+
import type { NotebookBackend } from "./backend";
|
|
5
|
+
|
|
6
|
+
/** A document snapshot with its own identity and revision metadata. */
|
|
7
|
+
export interface StaticNotebookSnapshot {
|
|
8
|
+
readonly notebook: Omit<Protocol.Notebook, "operations">;
|
|
9
|
+
readonly document: Protocol.Document;
|
|
10
|
+
readonly cells: readonly Protocol.Cell[];
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Serve a document without a server, runtime, or mutation operations.
|
|
15
|
+
* Callers and readers must treat the snapshot records as immutable.
|
|
16
|
+
*/
|
|
17
|
+
export function createStaticNotebookBackend(
|
|
18
|
+
snapshot: StaticNotebookSnapshot,
|
|
19
|
+
): NotebookBackend {
|
|
20
|
+
const frames: Protocol.Event[] = [
|
|
21
|
+
{
|
|
22
|
+
event: "notebook",
|
|
23
|
+
data: { notebook: { ...snapshot.notebook, operations: [] } },
|
|
24
|
+
},
|
|
25
|
+
...snapshot.cells.map((cell): Protocol.CellEvent => ({
|
|
26
|
+
event: "cell",
|
|
27
|
+
data: { cell },
|
|
28
|
+
})),
|
|
29
|
+
{ event: "document", data: { document: snapshot.document } },
|
|
30
|
+
{ event: "ready", id: "static", data: {} },
|
|
31
|
+
];
|
|
32
|
+
|
|
33
|
+
return {
|
|
34
|
+
transport: ({ cursor, signal }, { open, message }) =>
|
|
35
|
+
new Promise((resolve) => {
|
|
36
|
+
if (signal.aborted) {
|
|
37
|
+
resolve();
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
// Finishing would ask NotebookEvents to reconnect. A snapshot stays open
|
|
41
|
+
// until its reader closes, even though there are no more updates.
|
|
42
|
+
signal.addEventListener("abort", () => resolve(), { once: true });
|
|
43
|
+
open();
|
|
44
|
+
if (cursor !== null && !signal.aborted) {
|
|
45
|
+
message({ event: "reset", data: {} });
|
|
46
|
+
}
|
|
47
|
+
for (const frame of frames) {
|
|
48
|
+
if (signal.aborted) {
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
message(frame);
|
|
52
|
+
}
|
|
53
|
+
}),
|
|
54
|
+
client: {
|
|
55
|
+
edit: async (_changes, options) => {
|
|
56
|
+
options?.signal?.throwIfAborted();
|
|
57
|
+
return {
|
|
58
|
+
ok: false,
|
|
59
|
+
status: 501,
|
|
60
|
+
problem: {
|
|
61
|
+
type: "tag:marimo.io,2026:host/unsupported-operation",
|
|
62
|
+
title: "Unsupported operation",
|
|
63
|
+
status: 501,
|
|
64
|
+
detail: "Static notebooks do not support editing",
|
|
65
|
+
},
|
|
66
|
+
};
|
|
67
|
+
},
|
|
68
|
+
},
|
|
69
|
+
};
|
|
70
|
+
}
|