@marimo-team/islands 0.25.3-dev100 → 0.25.3-dev101
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-GScQVnJ8.js → ErrorBoundary-kKLKZNGz.js} +1 -1
- package/dist/{code-visibility-DnqM5oW_.js → code-visibility-CK84K0J7.js} +1 -1
- package/dist/{common-D2HSo8Gy.js → common-DbG2Uufx.js} +1 -1
- package/dist/{data-grid-overlay-editor-BgIMQs9G.js → data-grid-overlay-editor-BggIiNWF.js} +1 -1
- package/dist/{feedback-button-BXcB6llx.js → feedback-button-CRF9v3d1.js} +2 -2
- package/dist/{glide-data-editor-S-_J-7SO.js → glide-data-editor-DgZRBzBJ.js} +3 -3
- package/dist/{globals-D4vBwcD6.js → globals-Bxo0esfB.js} +1 -1
- package/dist/main.js +6 -6
- package/dist/{number-overlay-editor-CUrUm7Np.js → number-overlay-editor-CI5EhMRq.js} +1 -1
- package/dist/{reveal-component-CFzdsOg8.js → reveal-component-Y4-XPwJF.js} +2 -2
- package/package.json +1 -1
- package/src/core/document/__tests__/lifecycle-scenarios.test.ts +61 -0
- package/src/core/document/__tests__/model.test.ts +0 -325
- package/src/core/document/__tests__/protocol-fixtures.ts +38 -0
- package/src/core/document/__tests__/recovery-scenarios.test.ts +230 -0
- package/src/core/document/__tests__/scenario-fixture.ts +42 -0
- package/src/core/document/__tests__/scenarios.test.ts +205 -0
- package/src/core/document/__tests__/submission.test.ts +22 -167
- package/src/core/document/__tests__/test-host.test.ts +92 -0
- package/src/core/document/__tests__/test-host.ts +247 -0
- package/src/core/document/__tests__/test-notebook.ts +139 -0
- package/src/core/document/__tests__/user.ts +61 -0
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
|
+
|
|
3
|
+
import { vi } from "vitest";
|
|
4
|
+
import { test } from "./scenario-fixture";
|
|
5
|
+
|
|
6
|
+
test.for(["response", "document"] as const)(
|
|
7
|
+
"keeps a conflicting batch intact and retries explicitly with %s first",
|
|
8
|
+
async (first, { expect, session: { user, host, notebook } }) => {
|
|
9
|
+
// The first change is valid; the second conflicts. Neither may commit.
|
|
10
|
+
user.changeCells({
|
|
11
|
+
c1: { code: "other local edit" },
|
|
12
|
+
c3: { code: "local" },
|
|
13
|
+
});
|
|
14
|
+
const synced = user.sync();
|
|
15
|
+
const request = await host.nextRequest();
|
|
16
|
+
host.changeCell("c3", { code: "remote", name: "remote name" });
|
|
17
|
+
const remote = host.document();
|
|
18
|
+
expect(user.cell("c3")?.code).toBe("local");
|
|
19
|
+
request.process();
|
|
20
|
+
expect(host.document()).toEqual(remote);
|
|
21
|
+
user.changeCode("c3", "updated local");
|
|
22
|
+
|
|
23
|
+
if (first === "document") {
|
|
24
|
+
host.deliverDocument();
|
|
25
|
+
}
|
|
26
|
+
request.deliverResponse();
|
|
27
|
+
await expect(synced).rejects.toThrow("conflict");
|
|
28
|
+
expect(user.status()).toMatchObject({ status: "error", retryable: true });
|
|
29
|
+
if (first === "response") {
|
|
30
|
+
host.deliverDocument();
|
|
31
|
+
}
|
|
32
|
+
await vi.advanceTimersByTimeAsync(2000);
|
|
33
|
+
expect(notebook.edit).toHaveBeenCalledOnce();
|
|
34
|
+
expect(user.cell("c3")?.code).toBe("updated local");
|
|
35
|
+
expect(user.cell("c1")?.code).toBe("other local edit");
|
|
36
|
+
|
|
37
|
+
const retried = user.retry();
|
|
38
|
+
const retry = await host.nextRequest();
|
|
39
|
+
expect(retry.idempotencyKey).not.toBe(request.idempotencyKey);
|
|
40
|
+
retry.process();
|
|
41
|
+
retry.deliverResponse();
|
|
42
|
+
host.deliverDocument();
|
|
43
|
+
await expect(retried).resolves.toEqual(host.document().revision);
|
|
44
|
+
expect(host.document().cells.get("c1")?.code).toBe("other local edit");
|
|
45
|
+
expect(host.document().cells.get("c3")).toMatchObject({
|
|
46
|
+
code: "updated local",
|
|
47
|
+
name: "remote name",
|
|
48
|
+
});
|
|
49
|
+
expect(user.cell("c3")).toMatchObject({
|
|
50
|
+
code: "updated local",
|
|
51
|
+
name: "remote name",
|
|
52
|
+
});
|
|
53
|
+
expect(user.status()).toEqual({ status: "idle" });
|
|
54
|
+
},
|
|
55
|
+
);
|
|
56
|
+
|
|
57
|
+
test("waits for the conflicting revision before allowing a retry", async ({
|
|
58
|
+
expect,
|
|
59
|
+
session: { user, host, notebook },
|
|
60
|
+
}) => {
|
|
61
|
+
user.changeCode("c3", "local");
|
|
62
|
+
const synced = user.sync();
|
|
63
|
+
const request = await host.nextRequest();
|
|
64
|
+
host.changeCell("c3", { code: "remote" });
|
|
65
|
+
request.process();
|
|
66
|
+
request.deliverResponse();
|
|
67
|
+
await expect(synced).rejects.toThrow("conflict");
|
|
68
|
+
await expect(user.retry()).rejects.toThrow("Wait for the host");
|
|
69
|
+
expect(notebook.edit).toHaveBeenCalledOnce();
|
|
70
|
+
expect(user.cell("c3")?.code).toBe("local");
|
|
71
|
+
|
|
72
|
+
host.deliverDocument();
|
|
73
|
+
const retried = user.retry();
|
|
74
|
+
const retry = await host.nextRequest();
|
|
75
|
+
retry.process();
|
|
76
|
+
retry.deliverResponse();
|
|
77
|
+
host.deliverDocument();
|
|
78
|
+
await expect(retried).resolves.toEqual(host.document().revision);
|
|
79
|
+
expect(host.document().cells.get("c3")?.code).toBe("local");
|
|
80
|
+
expect(user.status()).toEqual({ status: "idle" });
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
test("replays a lost response without committing twice or losing newer typing", async ({
|
|
84
|
+
expect,
|
|
85
|
+
session: { user, host },
|
|
86
|
+
}) => {
|
|
87
|
+
user.changeCode("c3", "sent");
|
|
88
|
+
const flushed = user.sync();
|
|
89
|
+
const request = await host.nextRequest();
|
|
90
|
+
request.process();
|
|
91
|
+
const committed = host.document();
|
|
92
|
+
host.deliverDocument();
|
|
93
|
+
user.changeCode("c3", "newer");
|
|
94
|
+
request.loseResponse();
|
|
95
|
+
|
|
96
|
+
await vi.advanceTimersByTimeAsync(500);
|
|
97
|
+
const retry = await host.nextRequest();
|
|
98
|
+
expect(retry.idempotencyKey).toBe(request.idempotencyKey);
|
|
99
|
+
expect(retry.changes).toEqual(request.changes);
|
|
100
|
+
retry.process();
|
|
101
|
+
expect(host.document()).toEqual(committed);
|
|
102
|
+
retry.deliverResponse();
|
|
103
|
+
|
|
104
|
+
const next = await host.nextRequest();
|
|
105
|
+
expect(next.idempotencyKey).not.toBe(request.idempotencyKey);
|
|
106
|
+
next.process();
|
|
107
|
+
next.deliverResponse();
|
|
108
|
+
host.deliverDocument();
|
|
109
|
+
await expect(flushed).resolves.toEqual(host.document().revision);
|
|
110
|
+
expect(host.document().revision.version).toBe(committed.revision.version + 1);
|
|
111
|
+
expect(host.document().cells.get("c3")?.code).toBe("newer");
|
|
112
|
+
expect(user.cell("c3")?.code).toBe("newer");
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
test("ignores a timed-out attempt that completes after its retry", async ({
|
|
116
|
+
expect,
|
|
117
|
+
session: { user, host, notebook },
|
|
118
|
+
}) => {
|
|
119
|
+
user.changeCode("c3", "local");
|
|
120
|
+
const flushed = user.sync();
|
|
121
|
+
const first = await host.nextRequest();
|
|
122
|
+
await vi.advanceTimersByTimeAsync(10_500);
|
|
123
|
+
const retry = await host.nextRequest();
|
|
124
|
+
expect(first.signal.aborted).toBe(true);
|
|
125
|
+
expect(retry.idempotencyKey).toBe(first.idempotencyKey);
|
|
126
|
+
retry.process();
|
|
127
|
+
const committed = host.document();
|
|
128
|
+
retry.deliverResponse();
|
|
129
|
+
host.deliverDocument();
|
|
130
|
+
await expect(flushed).resolves.toEqual(committed.revision);
|
|
131
|
+
first.process();
|
|
132
|
+
first.deliverResponse();
|
|
133
|
+
await vi.advanceTimersByTimeAsync(0);
|
|
134
|
+
expect(host.document()).toEqual(committed);
|
|
135
|
+
expect(user.cell("c3")?.code).toBe("local");
|
|
136
|
+
expect(notebook.edit).toHaveBeenCalledTimes(2);
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
test("a manual retry reuses the committed edit after automatic retries give up", async ({
|
|
140
|
+
expect,
|
|
141
|
+
session: { user, host, notebook },
|
|
142
|
+
}) => {
|
|
143
|
+
user.changeCode("c3", "sent");
|
|
144
|
+
const synced = user.sync();
|
|
145
|
+
const request = await host.nextRequest();
|
|
146
|
+
request.process();
|
|
147
|
+
const committed = host.document();
|
|
148
|
+
request.loseResponse();
|
|
149
|
+
for (const delay of [500, 1000]) {
|
|
150
|
+
await vi.advanceTimersByTimeAsync(delay);
|
|
151
|
+
const retry = await host.nextRequest();
|
|
152
|
+
retry.process();
|
|
153
|
+
retry.loseResponse();
|
|
154
|
+
}
|
|
155
|
+
await expect(synced).rejects.toThrow("Connection lost");
|
|
156
|
+
expect(user.status()).toMatchObject({ status: "error", retryable: true });
|
|
157
|
+
expect(notebook.edit).toHaveBeenCalledTimes(3);
|
|
158
|
+
expect(host.document()).toEqual(committed);
|
|
159
|
+
|
|
160
|
+
const retried = user.retry();
|
|
161
|
+
const retry = await host.nextRequest();
|
|
162
|
+
expect(retry.idempotencyKey).toBe(request.idempotencyKey);
|
|
163
|
+
expect(retry.changes).toEqual(request.changes);
|
|
164
|
+
retry.process();
|
|
165
|
+
retry.deliverResponse();
|
|
166
|
+
host.deliverDocument();
|
|
167
|
+
await expect(retried).resolves.toEqual(committed.revision);
|
|
168
|
+
expect(host.document()).toEqual(committed);
|
|
169
|
+
expect(user.status()).toEqual({ status: "idle" });
|
|
170
|
+
expect(notebook.edit).toHaveBeenCalledTimes(4);
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
test("refuses a manual retry after the safe retry window while retaining the draft", async ({
|
|
174
|
+
expect,
|
|
175
|
+
session: { user, host, notebook },
|
|
176
|
+
}) => {
|
|
177
|
+
user.changeCode("c3", "sent");
|
|
178
|
+
const synced = user.sync();
|
|
179
|
+
const request = await host.nextRequest();
|
|
180
|
+
request.process();
|
|
181
|
+
const committed = host.document();
|
|
182
|
+
request.loseResponse();
|
|
183
|
+
for (const delay of [500, 1000]) {
|
|
184
|
+
await vi.advanceTimersByTimeAsync(delay);
|
|
185
|
+
const retry = await host.nextRequest();
|
|
186
|
+
retry.process();
|
|
187
|
+
retry.loseResponse();
|
|
188
|
+
}
|
|
189
|
+
await expect(synced).rejects.toThrow("Connection lost");
|
|
190
|
+
expect(user.status()).toMatchObject({ status: "error", retryable: true });
|
|
191
|
+
|
|
192
|
+
await vi.advanceTimersByTimeAsync(60_000);
|
|
193
|
+
await expect(user.retry()).rejects.toThrow("retry window expired");
|
|
194
|
+
expect(user.status()).toMatchObject({ status: "error", retryable: false });
|
|
195
|
+
expect(notebook.edit).toHaveBeenCalledTimes(3);
|
|
196
|
+
expect(host.document()).toEqual(committed);
|
|
197
|
+
expect(user.cell("c3")?.code).toBe("sent");
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
test("keeps the whole draft when another writer deletes an edited cell", async ({
|
|
201
|
+
expect,
|
|
202
|
+
session: { user, host, notebook },
|
|
203
|
+
}) => {
|
|
204
|
+
user.changeCode("c3", "keep this");
|
|
205
|
+
const synced = user.sync();
|
|
206
|
+
const request = await host.nextRequest();
|
|
207
|
+
host.deleteCell("c3");
|
|
208
|
+
request.process();
|
|
209
|
+
request.deliverResponse();
|
|
210
|
+
host.deliverDocument();
|
|
211
|
+
await expect(synced).rejects.toThrow("conflict");
|
|
212
|
+
|
|
213
|
+
user.changeCode("c1", "other edit");
|
|
214
|
+
await expect(user.retry()).rejects.toThrow("unavailable");
|
|
215
|
+
await vi.advanceTimersByTimeAsync(1000);
|
|
216
|
+
expect(notebook.edit).toHaveBeenCalledOnce();
|
|
217
|
+
expect(host.document().cells.get("c1")?.code).toBe("import marimo as mo");
|
|
218
|
+
|
|
219
|
+
host.createCell({ id: "c3", code: "restored" });
|
|
220
|
+
host.deliverDocument();
|
|
221
|
+
expect(user.cell("c3")?.code).toBe("keep this");
|
|
222
|
+
const retried = user.retry();
|
|
223
|
+
const retry = await host.nextRequest();
|
|
224
|
+
retry.process();
|
|
225
|
+
retry.deliverResponse();
|
|
226
|
+
host.deliverDocument();
|
|
227
|
+
await expect(retried).resolves.toEqual(host.document().revision);
|
|
228
|
+
expect(host.document().cells.get("c3")?.code).toBe("keep this");
|
|
229
|
+
expect(host.document().cells.get("c1")?.code).toBe("other edit");
|
|
230
|
+
});
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
|
+
|
|
3
|
+
import { createStore } from "jotai/vanilla";
|
|
4
|
+
import { test as baseTest, vi } from "vitest";
|
|
5
|
+
import type { Document } from "../../host/resources/document";
|
|
6
|
+
import { createDocumentModel } from "../model";
|
|
7
|
+
import { initialDocument } from "./protocol-fixtures";
|
|
8
|
+
import { createTestNotebook } from "./test-notebook";
|
|
9
|
+
import { createUser } from "./user";
|
|
10
|
+
|
|
11
|
+
function createSession(initial: Document, controller: AbortController) {
|
|
12
|
+
const store = createStore();
|
|
13
|
+
const { notebook, control } = createTestNotebook(store, initial);
|
|
14
|
+
const model = createDocumentModel({
|
|
15
|
+
store,
|
|
16
|
+
notebook,
|
|
17
|
+
signal: controller.signal,
|
|
18
|
+
});
|
|
19
|
+
return {
|
|
20
|
+
user: createUser(store, model),
|
|
21
|
+
host: control,
|
|
22
|
+
notebook,
|
|
23
|
+
controller,
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export const test = baseTest.extend<{
|
|
28
|
+
initial: Document;
|
|
29
|
+
session: ReturnType<typeof createSession>;
|
|
30
|
+
}>({
|
|
31
|
+
initial: initialDocument(),
|
|
32
|
+
session: async ({ initial }, provide) => {
|
|
33
|
+
vi.useFakeTimers({ now: 0 });
|
|
34
|
+
const controller = new AbortController();
|
|
35
|
+
try {
|
|
36
|
+
await provide(createSession(initial, controller));
|
|
37
|
+
} finally {
|
|
38
|
+
controller.abort();
|
|
39
|
+
vi.useRealTimers();
|
|
40
|
+
}
|
|
41
|
+
},
|
|
42
|
+
});
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
|
+
|
|
3
|
+
import { vi } from "vitest";
|
|
4
|
+
import { test } from "./scenario-fixture";
|
|
5
|
+
|
|
6
|
+
test("waits for a pause in typing before sending the latest edit", async ({
|
|
7
|
+
expect,
|
|
8
|
+
session: { user, host, notebook },
|
|
9
|
+
}) => {
|
|
10
|
+
user.changeCode("c3", "first");
|
|
11
|
+
await vi.advanceTimersByTimeAsync(300);
|
|
12
|
+
user.changeCode("c3", "latest");
|
|
13
|
+
await vi.advanceTimersByTimeAsync(399);
|
|
14
|
+
expect(notebook.edit).not.toHaveBeenCalled();
|
|
15
|
+
await vi.advanceTimersByTimeAsync(1);
|
|
16
|
+
expect(notebook.edit).toHaveBeenCalledOnce();
|
|
17
|
+
const request = await host.nextRequest();
|
|
18
|
+
request.process();
|
|
19
|
+
request.deliverResponse();
|
|
20
|
+
host.deliverDocument();
|
|
21
|
+
await expect(user.sync()).resolves.toEqual(host.document().revision);
|
|
22
|
+
expect(host.document().cells.get("c3")?.code).toBe("latest");
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
test.for(["response", "document"] as const)(
|
|
26
|
+
"preserves newer typing when the %s arrives first",
|
|
27
|
+
async (first, { expect, session: { user, host, notebook } }) => {
|
|
28
|
+
user.changeCells({ c3: { code: "sent", config: { hide_code: true } } });
|
|
29
|
+
const flushed = user.sync();
|
|
30
|
+
const finished = vi.fn();
|
|
31
|
+
void flushed.then(finished, () => undefined);
|
|
32
|
+
const request = await host.nextRequest();
|
|
33
|
+
request.process();
|
|
34
|
+
user.changeCode("c3", "still typing");
|
|
35
|
+
|
|
36
|
+
const deliver = {
|
|
37
|
+
response: () => request.deliverResponse(),
|
|
38
|
+
document: () => host.deliverDocument(),
|
|
39
|
+
};
|
|
40
|
+
deliver[first]();
|
|
41
|
+
await vi.advanceTimersByTimeAsync(0);
|
|
42
|
+
expect(finished).not.toHaveBeenCalled();
|
|
43
|
+
expect(notebook.edit).toHaveBeenCalledOnce();
|
|
44
|
+
expect(user.cell("c3")?.code).toBe("still typing");
|
|
45
|
+
|
|
46
|
+
deliver[first === "response" ? "document" : "response"]();
|
|
47
|
+
await vi.advanceTimersByTimeAsync(0);
|
|
48
|
+
expect(user.cell("c3")?.code).toBe("still typing");
|
|
49
|
+
const next = await host.nextRequest();
|
|
50
|
+
next.process();
|
|
51
|
+
next.deliverResponse();
|
|
52
|
+
host.deliverDocument();
|
|
53
|
+
await expect(flushed).resolves.toEqual(host.document().revision);
|
|
54
|
+
const expected = {
|
|
55
|
+
id: "c3",
|
|
56
|
+
code: "still typing",
|
|
57
|
+
name: "_",
|
|
58
|
+
config: {
|
|
59
|
+
column: null,
|
|
60
|
+
disabled: false,
|
|
61
|
+
hide_code: true,
|
|
62
|
+
expand_output: false,
|
|
63
|
+
},
|
|
64
|
+
};
|
|
65
|
+
expect(user.cell("c3")).toEqual(expected);
|
|
66
|
+
expect(host.document().cells.get("c3")).toMatchObject(expected);
|
|
67
|
+
expect(user.status()).toEqual({ status: "idle" });
|
|
68
|
+
|
|
69
|
+
host.changeCell("c3", {
|
|
70
|
+
code: "another writer",
|
|
71
|
+
config: { hide_code: false },
|
|
72
|
+
});
|
|
73
|
+
host.deliverDocument();
|
|
74
|
+
expect(user.cell("c3")?.code).toBe("another writer");
|
|
75
|
+
expect(user.cell("c3")?.config.hide_code).toBe(false);
|
|
76
|
+
},
|
|
77
|
+
);
|
|
78
|
+
|
|
79
|
+
test("uses a newer confirmed document for edits that have not been sent", async ({
|
|
80
|
+
expect,
|
|
81
|
+
session: { user, host },
|
|
82
|
+
}) => {
|
|
83
|
+
user.changeCode("c3", "local");
|
|
84
|
+
host.changeCell("c3", { name: "remote name" });
|
|
85
|
+
host.deliverDocument();
|
|
86
|
+
const flushed = user.sync();
|
|
87
|
+
const request = await host.nextRequest();
|
|
88
|
+
request.process();
|
|
89
|
+
request.deliverResponse();
|
|
90
|
+
host.deliverDocument();
|
|
91
|
+
await expect(flushed).resolves.toEqual(host.document().revision);
|
|
92
|
+
expect(host.document().cells.get("c3")).toMatchObject({
|
|
93
|
+
code: "local",
|
|
94
|
+
name: "remote name",
|
|
95
|
+
});
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
test.for(["before reset", "during reset", "after snapshot"] as const)(
|
|
99
|
+
"recovers across reset when the response arrives %s",
|
|
100
|
+
async (arrival, { expect, session: { user, host } }) => {
|
|
101
|
+
user.changeCode("c3", "sent");
|
|
102
|
+
const flushed = user.sync();
|
|
103
|
+
const request = await host.nextRequest();
|
|
104
|
+
request.process();
|
|
105
|
+
user.changeCode("c3", "newer typing");
|
|
106
|
+
if (arrival === "before reset") {
|
|
107
|
+
request.deliverResponse();
|
|
108
|
+
await vi.advanceTimersByTimeAsync(0);
|
|
109
|
+
}
|
|
110
|
+
host.reset();
|
|
111
|
+
if (arrival === "during reset") {
|
|
112
|
+
request.deliverResponse();
|
|
113
|
+
await vi.advanceTimersByTimeAsync(0);
|
|
114
|
+
}
|
|
115
|
+
host.changeCell("c1", { code: "unrelated edit" });
|
|
116
|
+
host.deliverSnapshot();
|
|
117
|
+
if (arrival === "after snapshot") {
|
|
118
|
+
await vi.advanceTimersByTimeAsync(0);
|
|
119
|
+
request.deliverResponse();
|
|
120
|
+
}
|
|
121
|
+
expect(user.cell("c3")?.code).toBe("newer typing");
|
|
122
|
+
const next = await host.nextRequest();
|
|
123
|
+
next.process();
|
|
124
|
+
next.deliverResponse();
|
|
125
|
+
host.deliverDocument();
|
|
126
|
+
await expect(flushed).resolves.toEqual(host.document().revision);
|
|
127
|
+
expect(host.document().cells.get("c3")?.code).toBe("newer typing");
|
|
128
|
+
expect(user.cell("c1")?.code).toBe("unrelated edit");
|
|
129
|
+
expect(user.status()).toEqual({ status: "idle" });
|
|
130
|
+
|
|
131
|
+
host.changeCell("c3", { code: "another writer" });
|
|
132
|
+
host.deliverDocument();
|
|
133
|
+
expect(user.cell("c3")?.code).toBe("another writer");
|
|
134
|
+
},
|
|
135
|
+
);
|
|
136
|
+
|
|
137
|
+
test.for(["response", "document"] as const)(
|
|
138
|
+
"does not resurrect a cancelled creation when the %s arrives first",
|
|
139
|
+
async (first, { expect, session: { user, host } }) => {
|
|
140
|
+
user.createCell({ id: "new", code: "new cell" });
|
|
141
|
+
const flushed = user.sync();
|
|
142
|
+
const creation = await host.nextRequest();
|
|
143
|
+
user.deleteCell("new");
|
|
144
|
+
host.changeCell("c1", { code: "unrelated edit" });
|
|
145
|
+
host.deliverDocument();
|
|
146
|
+
creation.process();
|
|
147
|
+
const deliver = {
|
|
148
|
+
response: () => creation.deliverResponse(),
|
|
149
|
+
document: () => host.deliverDocument(),
|
|
150
|
+
};
|
|
151
|
+
deliver[first]();
|
|
152
|
+
await vi.advanceTimersByTimeAsync(0);
|
|
153
|
+
expect(user.cell("new")).toBeUndefined();
|
|
154
|
+
expect(user.order()).toEqual(["c1", "c3", "k7f2"]);
|
|
155
|
+
deliver[first === "response" ? "document" : "response"]();
|
|
156
|
+
const deletion = await host.nextRequest();
|
|
157
|
+
deletion.process();
|
|
158
|
+
deletion.deliverResponse();
|
|
159
|
+
host.deliverDocument();
|
|
160
|
+
await expect(flushed).resolves.toEqual(host.document().revision);
|
|
161
|
+
expect(host.document().cells.has("new")).toBe(false);
|
|
162
|
+
expect(user.cell("new")).toBeUndefined();
|
|
163
|
+
},
|
|
164
|
+
);
|
|
165
|
+
|
|
166
|
+
test("releases an edit undone before sending and follows subsequent host changes", async ({
|
|
167
|
+
expect,
|
|
168
|
+
session: { user, host, notebook },
|
|
169
|
+
}) => {
|
|
170
|
+
user.changeCode("c3", "temporary");
|
|
171
|
+
user.changeCode("c3", "x = 21");
|
|
172
|
+
expect(user.status()).toEqual({ status: "idle" });
|
|
173
|
+
await expect(user.sync()).resolves.toEqual(host.document().revision);
|
|
174
|
+
|
|
175
|
+
host.changeCell("c3", { code: "another writer" });
|
|
176
|
+
host.deliverDocument();
|
|
177
|
+
expect(user.cell("c3")?.code).toBe("another writer");
|
|
178
|
+
await vi.advanceTimersByTimeAsync(1000);
|
|
179
|
+
expect(notebook.edit).not.toHaveBeenCalled();
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
test("flushes edits made while an earlier flush is finishing", async ({
|
|
183
|
+
expect,
|
|
184
|
+
session: { user, host, notebook },
|
|
185
|
+
}) => {
|
|
186
|
+
const original = host.document().revision;
|
|
187
|
+
const first = user.sync();
|
|
188
|
+
// Let the first flush drain without awaiting its returned promise.
|
|
189
|
+
await Promise.resolve();
|
|
190
|
+
user.changeCode("c3", "new code");
|
|
191
|
+
const second = user.sync();
|
|
192
|
+
const settled = vi.fn();
|
|
193
|
+
void second.then(settled, settled);
|
|
194
|
+
|
|
195
|
+
await vi.advanceTimersByTimeAsync(0);
|
|
196
|
+
expect(notebook.edit).toHaveBeenCalledOnce();
|
|
197
|
+
expect(settled).not.toHaveBeenCalled();
|
|
198
|
+
const request = await host.nextRequest();
|
|
199
|
+
request.process();
|
|
200
|
+
expect(host.document().cells.get("c3")?.code).toBe("new code");
|
|
201
|
+
request.deliverResponse();
|
|
202
|
+
host.deliverDocument();
|
|
203
|
+
await expect(second).resolves.toEqual(host.document().revision);
|
|
204
|
+
await expect(first).resolves.toEqual(original);
|
|
205
|
+
});
|