@marimo-team/islands 0.25.3-dev101 → 0.25.3-dev103

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.
Files changed (28) hide show
  1. package/dist/{ErrorBoundary-kKLKZNGz.js → ErrorBoundary-DvJIREfH.js} +1 -1
  2. package/dist/{chat-ui-BIgPxfPP.js → chat-ui-BjFJNtik.js} +3 -3
  3. package/dist/{code-visibility-CK84K0J7.js → code-visibility-CfeBTR2t.js} +2 -2
  4. package/dist/{common-DbG2Uufx.js → common-BRSsfrNv.js} +3 -3
  5. package/dist/{data-grid-overlay-editor-BggIiNWF.js → data-grid-overlay-editor-DEC7lFIy.js} +1 -1
  6. package/dist/{error-entries-TAO6kEhF.js → error-entries-m7wjcSR-.js} +1 -1
  7. package/dist/{feedback-button-CRF9v3d1.js → feedback-button-9Gkf9CUC.js} +3 -3
  8. package/dist/{glide-data-editor-DgZRBzBJ.js → glide-data-editor-D2F4hynZ.js} +3 -3
  9. package/dist/{globals-Bxo0esfB.js → globals-BBV0iOn6.js} +1 -1
  10. package/dist/{html-to-image-CWdayGZU.js → html-to-image-DQn1HTpP.js} +1 -1
  11. package/dist/main.js +10 -10
  12. package/dist/{number-overlay-editor-CI5EhMRq.js → number-overlay-editor-B3XQt_PV.js} +1 -1
  13. package/dist/{process-output-DD-oSvZk.js → process-output-DIMk7xHI.js} +2 -2
  14. package/dist/{reveal-component-Y4-XPwJF.js → reveal-component-DEasliPP.js} +3 -3
  15. package/package.json +1 -1
  16. package/src/core/codemirror/cells/__tests__/extensions.test.ts +15 -0
  17. package/src/core/codemirror/cells/extensions.ts +7 -3
  18. package/src/core/document/__tests__/codemirror.test.ts +483 -0
  19. package/src/core/document/__tests__/model.test.ts +6 -6
  20. package/src/core/document/__tests__/state.test.ts +5 -13
  21. package/src/core/document/__tests__/submission.test.ts +2 -0
  22. package/src/core/document/__tests__/user.ts +3 -3
  23. package/src/core/document/codemirror.ts +311 -0
  24. package/src/core/document/index.ts +3 -1
  25. package/src/core/document/model.ts +7 -8
  26. package/src/core/document/select.ts +41 -0
  27. package/src/core/document/state.ts +3 -30
  28. package/src/core/document/sync.ts +29 -25
@@ -0,0 +1,311 @@
1
+ /* Copyright 2026 Marimo. All rights reserved. */
2
+
3
+ /**
4
+ * A CodeMirror binding between an editor and one cell's code.
5
+ *
6
+ * The editor and the document model each hold the code, and the binding keeps
7
+ * them equal in both directions.
8
+ *
9
+ * - Push. An update that could change the code writes it to the model before
10
+ * the update returns, so the model keeps pace with typing.
11
+ * - Pull. A model change reaches the editor in a microtask, because a view
12
+ * cannot dispatch while it is constructing or updating. The binding keeps
13
+ * the code both sides last agreed on, so the model's echo of a push is
14
+ * skipped while a different value, even one written during that push, is
15
+ * pulled. A real change is marked remote, stays out of undo history, and
16
+ * waits for IME composition to end.
17
+ *
18
+ * The editor shows code through marimo's language adapters, so a markdown
19
+ * cell edits the text inside `mo.md`. Both directions compare the cell's
20
+ * Python code rather than the editor text, so an adapter that normalizes what
21
+ * it reads back leaves typing alone.
22
+ *
23
+ * A pulled change replaces only what differs, using `diff` from
24
+ * `@codemirror/merge`. Separate edits stay separate, so the selection, folds,
25
+ * and diagnostics between them keep their place. The diff uses a scan limit of
26
+ * 500, as the merge view does. Past it, the search trades precision for speed,
27
+ * so a rewrite of a large cell costs microseconds and maps the selection
28
+ * coarsely instead of blocking the editor.
29
+ */
30
+
31
+ import { diff } from "@codemirror/merge";
32
+ import {
33
+ type ChangeSpec,
34
+ type EditorState,
35
+ type Extension,
36
+ type StateEffect,
37
+ Transaction,
38
+ } from "@codemirror/state";
39
+ import { type EditorView, ViewPlugin, type ViewUpdate } from "@codemirror/view";
40
+ import type { CellId } from "@marimo-team/client/protocol";
41
+ import { dequal } from "dequal";
42
+ import type { Atom, createStore } from "jotai/vanilla";
43
+ import {
44
+ languageAdapterFromCode,
45
+ languageAdapterState,
46
+ switchLanguage,
47
+ } from "../codemirror/language/extension";
48
+ import { initializeSQLDialect } from "../codemirror/language/languages/sql/sql";
49
+ import {
50
+ languageMetadataField,
51
+ setLanguageMetadata,
52
+ } from "../codemirror/language/metadata";
53
+ import type { DocumentModel } from "./model";
54
+ import { selectCellField } from "./select";
55
+
56
+ /** The diff scan limit, matching the default of the merge view. */
57
+ const SCAN_LIMIT = 500;
58
+
59
+ interface Binding {
60
+ readonly store: ReturnType<typeof createStore>;
61
+ readonly model: DocumentModel;
62
+ readonly cellId: CellId;
63
+ /** Undefined while the cell is absent, null while its source is withheld. */
64
+ readonly code: Atom<string | null | undefined>;
65
+ }
66
+
67
+ /**
68
+ * Bind an editor to one cell's code.
69
+ *
70
+ * Editors without marimo's language configuration bind their text as is.
71
+ */
72
+ export function cellCodeBinding({
73
+ store,
74
+ model,
75
+ cellId,
76
+ }: {
77
+ store: ReturnType<typeof createStore>;
78
+ model: DocumentModel;
79
+ cellId: CellId;
80
+ }): Extension {
81
+ const binding: Binding = {
82
+ store,
83
+ model,
84
+ cellId,
85
+ code: selectCellField(model.documentAtom, cellId, "code"),
86
+ };
87
+ return ViewPlugin.define((view) => new CellCodeSync(view, binding), {
88
+ eventObservers: {
89
+ compositionend() {
90
+ this.compositionEnded();
91
+ },
92
+ },
93
+ });
94
+ }
95
+
96
+ class CellCodeSync {
97
+ #scheduled = false;
98
+ /** A pull arrived during IME composition and runs once it ends. */
99
+ #deferred = false;
100
+ /**
101
+ * A pull is dispatching to the editor.
102
+ *
103
+ * A language switch dispatches transactions of its own before the text, and
104
+ * their in-between states represent the wrong code. Until the switch can be
105
+ * part of the pull's one remote transaction, this keeps them from pushing.
106
+ */
107
+ #pulling = false;
108
+ /**
109
+ * The code the editor and the model last agreed on.
110
+ *
111
+ * A change the model cannot take clears it, so once the cell is available
112
+ * again its code is pulled even if it matches what was agreed before.
113
+ */
114
+ #synced: string | undefined;
115
+ #destroyed = false;
116
+ readonly #view: EditorView;
117
+ readonly #binding: Binding;
118
+ readonly #unsubscribe: () => void;
119
+
120
+ public constructor(view: EditorView, binding: Binding) {
121
+ this.#view = view;
122
+ this.#binding = binding;
123
+ // A change to the agreed code is the echo of a push and needs no pull.
124
+ this.#unsubscribe = binding.store.sub(binding.code, () => {
125
+ if (binding.store.get(binding.code) !== this.#synced) {
126
+ this.#schedule();
127
+ }
128
+ });
129
+ this.#schedule();
130
+ }
131
+
132
+ public update(update: ViewUpdate) {
133
+ if (!this.#pulling && changesCode(update)) {
134
+ this.#push(update.state);
135
+ }
136
+ if (this.#deferred && !update.view.composing) {
137
+ this.#schedule();
138
+ }
139
+ }
140
+
141
+ /**
142
+ * Run a pull that waited for the composition.
143
+ *
144
+ * A composition that ends without changing the text need not produce an
145
+ * update. CodeMirror finishes handling the end in a microtask, so the pull
146
+ * waits for the next task.
147
+ */
148
+ public compositionEnded() {
149
+ if (this.#deferred) {
150
+ setTimeout(() => this.#schedule(), 0);
151
+ }
152
+ }
153
+
154
+ public destroy() {
155
+ this.#destroyed = true;
156
+ this.#unsubscribe();
157
+ }
158
+
159
+ /** Write the editor's code to the model unless the editor already shows it. */
160
+ #push(state: EditorState) {
161
+ const { store, model, cellId, code } = this.#binding;
162
+ const current = store.get(code);
163
+ if (typeof current !== "string") {
164
+ this.#synced = undefined;
165
+ return;
166
+ }
167
+ if (shows(state, current)) {
168
+ this.#synced = current;
169
+ return;
170
+ }
171
+ const next = codeOf(state);
172
+ this.#synced = next;
173
+ store.set(model.editAtom, (tx) => tx.setCode(cellId, next));
174
+ }
175
+
176
+ /** Show the model's code unless the editor already shows it. */
177
+ #pull() {
178
+ this.#scheduled = false;
179
+ const next = this.#binding.store.get(this.#binding.code);
180
+ if (this.#destroyed || typeof next !== "string") {
181
+ return;
182
+ }
183
+ if (this.#view.composing) {
184
+ this.#deferred = true;
185
+ return;
186
+ }
187
+ this.#deferred = false;
188
+ if (next !== this.#synced && !shows(this.#view.state, next)) {
189
+ this.#pulling = true;
190
+ try {
191
+ replaceCode(this.#view, next);
192
+ } finally {
193
+ this.#pulling = false;
194
+ }
195
+ }
196
+ this.#synced = next;
197
+ }
198
+
199
+ /** Pull once after the current update, however many changes arrive. */
200
+ #schedule() {
201
+ if (!this.#scheduled) {
202
+ this.#scheduled = true;
203
+ queueMicrotask(() => this.#pull());
204
+ }
205
+ }
206
+ }
207
+
208
+ /**
209
+ * The cell code an editor state represents.
210
+ *
211
+ * With a language adapter, this is the adapter's Python for the editor text.
212
+ */
213
+ function codeOf(state: EditorState): string {
214
+ const text = state.doc.toString();
215
+ const adapter = state.field(languageAdapterState, false);
216
+ return adapter
217
+ ? adapter.transformOut(
218
+ text,
219
+ state.field(languageMetadataField, false) ?? {},
220
+ )[0]
221
+ : text;
222
+ }
223
+
224
+ /**
225
+ * Whether an editor state shows the given cell code.
226
+ *
227
+ * An adapter can spell the same code more than one way, such as `mo.md("hi")`
228
+ * and its canonical form, so a state shows code that reads into its text and
229
+ * metadata as well as the code its own spelling produces.
230
+ */
231
+ function shows(state: EditorState, code: string): boolean {
232
+ if (codeOf(state) === code) {
233
+ return true;
234
+ }
235
+ const adapter = state.field(languageAdapterState, false);
236
+ if (!adapter) {
237
+ return false;
238
+ }
239
+ const [text, , metadata] = adapter.transformIn(code);
240
+ return (
241
+ text === state.doc.toString() &&
242
+ dequal(metadata, state.field(languageMetadataField, false))
243
+ );
244
+ }
245
+
246
+ /**
247
+ * Whether an update could change the code an editor represents.
248
+ *
249
+ * Language metadata, such as an SQL cell's output name, changes the code
250
+ * without changing the text.
251
+ */
252
+ function changesCode(update: ViewUpdate): boolean {
253
+ return (
254
+ update.docChanged ||
255
+ update.state.field(languageAdapterState, false) !==
256
+ update.startState.field(languageAdapterState, false) ||
257
+ update.state.field(languageMetadataField, false) !==
258
+ update.startState.field(languageMetadataField, false)
259
+ );
260
+ }
261
+
262
+ /**
263
+ * Show cell code in the language it implies.
264
+ *
265
+ * The language switch happens first, in its own transactions, and clears undo
266
+ * history because its entries edit the previous language's text. The text and
267
+ * metadata follow in one remote transaction that undo history leaves out. An
268
+ * SQL dialect depends on the engine in the metadata, so it is set again after.
269
+ */
270
+ function replaceCode(view: EditorView, code: string) {
271
+ let text = code;
272
+ const effects: StateEffect<unknown>[] = [];
273
+ let sql = false;
274
+ const adapter = view.state.field(languageAdapterState, false);
275
+ if (adapter) {
276
+ const target = languageAdapterFromCode(code);
277
+ sql = target.type === "sql";
278
+ if (target.type !== adapter.type) {
279
+ switchLanguage(view, { language: target.type, keepCodeAsIs: true });
280
+ }
281
+ const [inner, , metadata] = target.transformIn(code);
282
+ text = inner;
283
+ if (!dequal(metadata, view.state.field(languageMetadataField, false))) {
284
+ effects.push(setLanguageMetadata.of(metadata));
285
+ }
286
+ }
287
+ const current = view.state.doc.toString();
288
+ if (current === text && effects.length === 0) {
289
+ return;
290
+ }
291
+ view.dispatch({
292
+ changes: changesBetween(current, text),
293
+ effects,
294
+ annotations: [
295
+ Transaction.remote.of(true),
296
+ Transaction.addToHistory.of(false),
297
+ ],
298
+ });
299
+ if (sql && effects.length > 0) {
300
+ initializeSQLDialect(view);
301
+ }
302
+ }
303
+
304
+ /** The changes that turn one text into another, one per edited range. */
305
+ function changesBetween(from: string, to: string): ChangeSpec {
306
+ return diff(from, to, { scanLimit: SCAN_LIMIT }).map((change) => ({
307
+ from: change.fromA,
308
+ to: change.toA,
309
+ insert: to.slice(change.fromB, change.toB),
310
+ }));
311
+ }
@@ -6,5 +6,7 @@ export type {
6
6
  DocumentEditor,
7
7
  DocumentHost,
8
8
  DocumentModel,
9
- DocumentSyncState,
9
+ DocumentSyncStatus,
10
+ EditableCell,
11
+ EditableDocument,
10
12
  } from "./model";
@@ -6,18 +6,19 @@ import { createDocumentState, type DocumentState } from "./state";
6
6
  import {
7
7
  createDocumentSync,
8
8
  type DocumentHost,
9
- type DocumentSyncState,
9
+ type DocumentSyncStatus,
10
10
  } from "./sync";
11
11
 
12
12
  export type { DocumentEdit, DocumentEditor } from "./state";
13
- export type { DocumentHost, DocumentSyncState } from "./sync";
13
+ export type { EditableCell, EditableDocument } from "./draft";
14
+ export type { DocumentHost, DocumentSyncStatus } from "./sync";
14
15
 
15
16
  /** The editable document and its synchronization with the host. */
16
17
  export interface DocumentModel extends Pick<
17
18
  DocumentState,
18
- "cellAtom" | "orderAtom" | "appConfigAtom" | "editAtom"
19
+ "documentAtom" | "editAtom"
19
20
  > {
20
- readonly syncStateAtom: Atom<DocumentSyncState>;
21
+ readonly syncStatusAtom: Atom<DocumentSyncStatus>;
21
22
  /** Drain local edits and return the confirmed revision. */
22
23
  flush(): Promise<Protocol.Document>;
23
24
  /** Retry a rejection against fresh state, or repeat an uncertain request exactly. */
@@ -42,11 +43,9 @@ export function createDocumentModel({
42
43
  });
43
44
  const sync = createDocumentSync({ store, notebook, state, signal });
44
45
  return {
45
- cellAtom: state.cellAtom,
46
- orderAtom: state.orderAtom,
47
- appConfigAtom: state.appConfigAtom,
46
+ documentAtom: state.documentAtom,
48
47
  editAtom: state.editAtom,
49
- syncStateAtom: sync.stateAtom,
48
+ syncStatusAtom: sync.statusAtom,
50
49
  flush: sync.flush,
51
50
  retry: sync.retry,
52
51
  };
@@ -0,0 +1,41 @@
1
+ /* Copyright 2026 Marimo. All rights reserved. */
2
+
3
+ import type * as Protocol from "@marimo-team/client/protocol";
4
+ import { dequal } from "dequal";
5
+ import type { Atom } from "jotai/vanilla";
6
+ import { selectAtom } from "jotai/vanilla/utils";
7
+ import { type EditableCell, type EditableDocument, sameCell } from "./draft";
8
+
9
+ /**
10
+ * Select one cell of a document.
11
+ *
12
+ * The atom notifies when the cell's code, name, or config changes. A new host
13
+ * record with the same values leaves it unchanged. Each call creates an atom,
14
+ * so keep it for as long as its subscription.
15
+ */
16
+ export function selectCell(
17
+ document: Atom<EditableDocument | null>,
18
+ id: Protocol.CellId,
19
+ ): Atom<EditableCell | undefined> {
20
+ return selectAtom(
21
+ document,
22
+ (value) => value?.cells.get(id),
23
+ (a, b) => a === b || (a !== undefined && b !== undefined && sameCell(a, b)),
24
+ );
25
+ }
26
+
27
+ /** Select a document's cell order, null while the document is unavailable. */
28
+ export function selectOrder(
29
+ document: Atom<EditableDocument | null>,
30
+ ): Atom<readonly Protocol.CellId[] | null> {
31
+ return selectAtom(document, (value) => value?.order ?? null, dequal);
32
+ }
33
+
34
+ /** Select one field of a cell, unchanged until that field's value changes. */
35
+ export function selectCellField<K extends keyof EditableCell>(
36
+ document: Atom<EditableDocument | null>,
37
+ id: Protocol.CellId,
38
+ field: K,
39
+ ): Atom<EditableCell[K] | undefined> {
40
+ return selectAtom(document, (value) => value?.cells.get(id)?.[field], dequal);
41
+ }
@@ -1,13 +1,11 @@
1
1
  /* Copyright 2026 Marimo. All rights reserved. */
2
2
 
3
- import { dequal } from "dequal";
4
3
  import {
5
4
  atom,
6
5
  type Atom,
7
6
  type createStore,
8
7
  type WritableAtom,
9
8
  } from "jotai/vanilla";
10
- import { selectAtom } from "jotai/vanilla/utils";
11
9
  import type { Document } from "../host/resources/document";
12
10
  import * as Protocol from "@marimo-team/client/protocol";
13
11
  import {
@@ -16,14 +14,12 @@ import {
16
14
  createCell,
17
15
  deleteCell,
18
16
  type Draft,
19
- type EditableCell,
20
17
  type EditableDocument,
21
18
  editCell,
22
19
  EMPTY_DRAFT,
23
20
  isEmptyDraft,
24
21
  type Placement,
25
22
  prune,
26
- sameCell,
27
23
  setOrder,
28
24
  toChanges,
29
25
  viewDocument,
@@ -70,12 +66,8 @@ interface ModelState {
70
66
  * Switching notebook identity invalidates it, retaining each notebook's draft.
71
67
  */
72
68
  export interface DocumentState {
73
- /** Hold onto the selector; its value is stable until the cell changes. */
74
- cellAtom(id: Protocol.CellId): Atom<EditableCell | undefined>;
75
- readonly orderAtom: Atom<readonly Protocol.CellId[] | null>;
76
- readonly appConfigAtom: Atom<Readonly<
77
- Protocol.Document["app_config"]
78
- > | null>;
69
+ /** Confirmed host state with local drafts applied, null while unavailable. */
70
+ readonly documentAtom: Atom<EditableDocument | null>;
79
71
  /** Null when the document or source needed by a put is unavailable. */
80
72
  readonly changesAtom: Atom<readonly Protocol.Change[] | null>;
81
73
  /** Remains pending until both the response and its streamed revision arrive. */
@@ -158,23 +150,6 @@ export function createDocumentState({
158
150
  ? null
159
151
  : viewDocument(state.confirmed, draftOf(state));
160
152
  });
161
- const cellAtom = (id: Protocol.CellId) =>
162
- selectAtom(
163
- documentAtom,
164
- (document) => document?.cells.get(id),
165
- (a, b) =>
166
- a === b || (a !== undefined && b !== undefined && sameCell(a, b)),
167
- );
168
- const orderAtom = selectAtom(
169
- documentAtom,
170
- (document) => document?.order ?? null,
171
- dequal,
172
- );
173
- const appConfigAtom = selectAtom(
174
- stateAtom,
175
- (state) => state.confirmed?.revision.app_config ?? null,
176
- dequal,
177
- );
178
153
  const changesAtom = atom((get): readonly Protocol.Change[] | null => {
179
154
  const state = get(stateAtom);
180
155
  return state.confirmed === null
@@ -304,9 +279,7 @@ export function createDocumentState({
304
279
  }
305
280
 
306
281
  return {
307
- cellAtom,
308
- orderAtom,
309
- appConfigAtom,
282
+ documentAtom,
310
283
  changesAtom,
311
284
  pendingAtom,
312
285
  editAtom,
@@ -9,7 +9,7 @@ import type { DocumentState, DocumentSubmission } from "./state";
9
9
  /** The notebook state and operation needed by document editing. */
10
10
  export type DocumentHost = Pick<NotebookClient, "documentAtom" | "edit">;
11
11
 
12
- export type DocumentSyncState =
12
+ export type DocumentSyncStatus =
13
13
  | {
14
14
  readonly status:
15
15
  | "idle"
@@ -26,7 +26,7 @@ export type DocumentSyncState =
26
26
  };
27
27
 
28
28
  interface Submission {
29
- readonly edit: DocumentSubmission;
29
+ readonly submission: DocumentSubmission;
30
30
  readonly idempotencyKey: string;
31
31
  readonly startedAt: number;
32
32
  readonly controller: AbortController;
@@ -85,7 +85,7 @@ export function createDocumentSync({
85
85
  }) {
86
86
  signal.throwIfAborted();
87
87
  const source = notebook.documentAtom;
88
- const stateAtom = atom<DocumentSyncState>({ status: "idle" });
88
+ const statusAtom = atom<DocumentSyncStatus>({ status: "idle" });
89
89
  const observed = atom((get) => ({
90
90
  document: get(source),
91
91
  pending: get(state.pendingAtom),
@@ -93,7 +93,7 @@ export function createDocumentSync({
93
93
  }));
94
94
  let active: Submission | null = null;
95
95
  let running: Promise<Protocol.Document> | null = null;
96
- let failure: Extract<DocumentSyncState, { status: "error" }> | null = null;
96
+ let failure: Extract<DocumentSyncStatus, { status: "error" }> | null = null;
97
97
  let timer: ReturnType<typeof setTimeout> | undefined;
98
98
  let refreshing = false;
99
99
 
@@ -118,7 +118,7 @@ export function createDocumentSync({
118
118
  if (
119
119
  active &&
120
120
  current &&
121
- current.revision.notebook_id !== active.edit.notebookId
121
+ current.revision.notebook_id !== active.submission.notebookId
122
122
  ) {
123
123
  active.controller.abort(
124
124
  new Error("The notebook changed during synchronization"),
@@ -130,13 +130,13 @@ export function createDocumentSync({
130
130
  clearTimer();
131
131
  const changes = store.get(state.changesAtom);
132
132
  if (changes === null) {
133
- store.set(stateAtom, { status: "unavailable" });
133
+ store.set(statusAtom, { status: "unavailable" });
134
134
  } else if (changes.length === 0) {
135
135
  // Release unsent reversals instead of keeping an invisible override.
136
136
  store.set(state.submitAtom);
137
- store.set(stateAtom, { status: "idle" });
137
+ store.set(statusAtom, { status: "idle" });
138
138
  } else {
139
- store.set(stateAtom, { status: "pending" });
139
+ store.set(statusAtom, { status: "pending" });
140
140
  timer = setTimeout(() => {
141
141
  void flush().catch(() => undefined);
142
142
  }, DEBOUNCE_MS);
@@ -155,7 +155,7 @@ export function createDocumentSync({
155
155
  "The edit's retry window expired; its outcome is unknown",
156
156
  );
157
157
  }
158
- store.set(stateAtom, { status: "sending" });
158
+ store.set(statusAtom, { status: "sending" });
159
159
  const controller = new AbortController();
160
160
  const abort = () => controller.abort(request.controller.signal.reason);
161
161
  request.controller.signal.addEventListener("abort", abort, {
@@ -170,10 +170,14 @@ export function createDocumentSync({
170
170
  signal.throwIfAborted();
171
171
  request.controller.signal.throwIfAborted();
172
172
  result = await abortable(
173
- notebook.edit(request.edit.notebookId, request.edit.changes, {
174
- idempotencyKey: request.idempotencyKey,
175
- signal: controller.signal,
176
- }),
173
+ notebook.edit(
174
+ request.submission.notebookId,
175
+ request.submission.changes,
176
+ {
177
+ idempotencyKey: request.idempotencyKey,
178
+ signal: controller.signal,
179
+ },
180
+ ),
177
181
  controller.signal,
178
182
  );
179
183
  } catch (error) {
@@ -198,17 +202,17 @@ export function createDocumentSync({
198
202
  cause: result.problem,
199
203
  });
200
204
  }
201
- if (result.data.document.notebook_id !== request.edit.notebookId) {
205
+ if (result.data.document.notebook_id !== request.submission.notebookId) {
202
206
  throw new Error("Edit response belongs to another notebook");
203
207
  }
204
208
  request.response = result.data.document;
205
- store.set(state.settleAtom, request.edit, request.response);
209
+ store.set(state.settleAtom, request.submission, request.response);
206
210
  return;
207
211
  }
208
212
  }
209
213
 
210
214
  function confirm(request: Submission): Promise<void> {
211
- store.set(stateAtom, { status: "confirming" });
215
+ store.set(statusAtom, { status: "confirming" });
212
216
  return new Promise((resolve, reject) => {
213
217
  const check = () => {
214
218
  if (signal.aborted || request.controller.signal.aborted) {
@@ -216,7 +220,7 @@ export function createDocumentSync({
216
220
  reject(
217
221
  signal.aborted ? signal.reason : request.controller.signal.reason,
218
222
  );
219
- } else if (store.get(state.pendingAtom) !== request.edit) {
223
+ } else if (store.get(state.pendingAtom) !== request.submission) {
220
224
  cleanup();
221
225
  resolve();
222
226
  }
@@ -247,12 +251,12 @@ export function createDocumentSync({
247
251
  if (current === null || store.get(state.changesAtom) === null) {
248
252
  throw unavailable();
249
253
  }
250
- const edit = store.set(state.submitAtom);
251
- if (edit === null) {
254
+ const submission = store.set(state.submitAtom);
255
+ if (submission === null) {
252
256
  return current.revision;
253
257
  }
254
258
  active = {
255
- edit,
259
+ submission,
256
260
  idempotencyKey: idempotencyKey(),
257
261
  startedAt: Date.now(),
258
262
  controller: new AbortController(),
@@ -282,7 +286,7 @@ export function createDocumentSync({
282
286
  Date.now() - active.startedAt < RETRY_WINDOW_MS),
283
287
  };
284
288
  if (!signal.aborted) {
285
- store.set(stateAtom, failure);
289
+ store.set(statusAtom, failure);
286
290
  }
287
291
  throw error;
288
292
  } finally {
@@ -305,7 +309,7 @@ export function createDocumentSync({
305
309
  return running;
306
310
  }
307
311
  const notebookId =
308
- active?.edit.notebookId ?? store.get(source)?.revision.notebook_id;
312
+ active?.submission.notebookId ?? store.get(source)?.revision.notebook_id;
309
313
  if (notebookId === undefined) {
310
314
  return Promise.reject(unavailable());
311
315
  }
@@ -329,14 +333,14 @@ export function createDocumentSync({
329
333
  const rejectedAt = active.problem.edit?.document.version;
330
334
  if (
331
335
  !current ||
332
- current.revision.notebook_id !== active.edit.notebookId ||
336
+ current.revision.notebook_id !== active.submission.notebookId ||
333
337
  (rejectedAt !== undefined && current.revision.version < rejectedAt)
334
338
  ) {
335
339
  return Promise.reject(
336
340
  new Error("Wait for the host's current document before retrying"),
337
341
  );
338
342
  }
339
- store.set(state.settleAtom, active.edit, null);
343
+ store.set(state.settleAtom, active.submission, null);
340
344
  active = null;
341
345
  }
342
346
  failure = null;
@@ -355,5 +359,5 @@ export function createDocumentSync({
355
359
  stop();
356
360
  }
357
361
  refresh();
358
- return { stateAtom, flush, retry };
362
+ return { statusAtom, flush, retry };
359
363
  }