@marimo-team/islands 0.25.3-dev169 → 0.25.3-dev170
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-BbDrlHvG.js → ConnectedDataExplorerComponent-BbSXV-Xa.js} +8 -8
- package/dist/{ErrorBoundary-RaIwGWT3.js → ErrorBoundary-NE86qEWZ.js} +4 -4
- package/dist/{abnfDiagram-VCTEODGH-Dr-fo8_z.js → abnfDiagram-VCTEODGH-D7n07nXf.js} +2 -2
- package/dist/{architectureDiagram-5GKGNRK7-ByKIl9GG.js → architectureDiagram-5GKGNRK7-D6CjRib5.js} +1 -1
- package/dist/{chat-ui-BsV2WH-4.js → chat-ui-CC7GekpG.js} +34 -34
- package/dist/{chunk-IMKFNOWR-IKDP18MQ.js → chunk-IMKFNOWR-6dtPz7uS.js} +1 -1
- package/dist/{chunk-SHT3W25Y-LkIgegwh.js → chunk-SHT3W25Y-BwnFNKDA.js} +1 -1
- package/dist/{chunk-SVP7TREG-DReBYtnB.js → chunk-SVP7TREG-D2eV4vBJ.js} +1 -1
- package/dist/{chunk-TICWLB2K-DQ0A0-1D.js → chunk-TICWLB2K-BDs4-_-r.js} +1 -1
- package/dist/{classDiagram-ZZMXUADV-CwvXdCNE.js → classDiagram-ZZMXUADV-DCJnf_8v.js} +2 -2
- package/dist/{classDiagram-v2-VYDZK3BY-D0XAxDsc.js → classDiagram-v2-VYDZK3BY-BOmSzPBD.js} +2 -2
- package/dist/{code-visibility-BMi0KLAQ.js → code-visibility-Bzvc6ajl.js} +23 -23
- package/dist/{common-BMkJrZsD.js → common-Ciw4zkh2.js} +3412 -3412
- package/dist/{cynefinDiagram-5FMLGOSQ-BFN68nHr.js → cynefinDiagram-5FMLGOSQ-Bln_Soky.js} +1 -1
- package/dist/{dagre-GXQ25YYZ-CZt4rtLq.js → dagre-GXQ25YYZ-D-nf7WzW.js} +1 -1
- package/dist/{data-grid-overlay-editor-CCc1P42P.js → data-grid-overlay-editor-AQREmxI7.js} +1 -1
- package/dist/{diagram-S7CK7UJ4-D5cOz47_.js → diagram-S7CK7UJ4-PsgSMkY2.js} +1 -1
- package/dist/{diagram-UQ7AKVKN-Qgdppb4a.js → diagram-UQ7AKVKN-DfwfUuSw.js} +1 -1
- package/dist/{diagram-VX7I27RA-BsDCvily.js → diagram-VX7I27RA-C9_e-oMc.js} +1 -1
- package/dist/{diagram-Z3DM3KII-igTkGjKI.js → diagram-Z3DM3KII-CWLa5tuY.js} +1 -1
- package/dist/{dropdown-menu-BWnnAstC.js → dropdown-menu-B6ftyzOg.js} +1 -1
- package/dist/{ebnfDiagram-PWID7BFC-BHuZGjIr.js → ebnfDiagram-PWID7BFC-B1QRM58v.js} +2 -2
- package/dist/{erDiagram-RLTQ6QDP-DKObXTVE.js → erDiagram-RLTQ6QDP-DJK_FqvW.js} +1 -1
- package/dist/{error-entries-BpUJSLUZ.js → error-entries-DgK3wUJR.js} +5640 -5591
- package/dist/{feedback-button-CMVkEos_.js → feedback-button-8a6SETmT.js} +24 -24
- package/dist/{flowDiagram-HODETNUW-DZI_kit0.js → flowDiagram-HODETNUW-Uxc25XSJ.js} +2 -2
- package/dist/{formats-CwJHWasD.js → formats-DQCyPd5Z.js} +1 -1
- package/dist/{glide-data-editor-BA2pcHSV.js → glide-data-editor-B70oke_v.js} +5 -5
- package/dist/{globals-B5jjJ0ms.js → globals-Cxd5hNlI.js} +2 -2
- package/dist/{html-to-image-CQr2-IDA.js → html-to-image-D4yG5Ru0.js} +25 -25
- package/dist/{infoDiagram-27XIBGKW-DdPl-7-n.js → infoDiagram-27XIBGKW-DmjxqwL0.js} +1 -1
- package/dist/{ishikawaDiagram-5VMMS53U-DbyW6l59.js → ishikawaDiagram-5VMMS53U-NgCLHqQi.js} +1 -1
- package/dist/{kanban-definition-UXKFOSKX-BMX9SyBS.js → kanban-definition-UXKFOSKX-CZaG-iam.js} +1 -1
- package/dist/main.js +187 -188
- package/dist/{mermaid-C8W8mTbA.js → mermaid-CSvw7c71.js} +34 -34
- package/dist/{mindmap-definition-YA3MSWOX-Zd_tAxWL.js → mindmap-definition-YA3MSWOX-DJYQBDH_.js} +1 -1
- package/dist/{number-overlay-editor-BIWiV-TU.js → number-overlay-editor-ChK-SKUk.js} +1 -1
- package/dist/{pegDiagram-XKGWAZYB-DjYydPdo.js → pegDiagram-XKGWAZYB-CHKO_dAe.js} +2 -2
- package/dist/{pieDiagram-E7YTZNPT-R5LKjO2n.js → pieDiagram-E7YTZNPT-DVevbMAo.js} +1 -1
- package/dist/{process-output-Uw-VJE5V.js → process-output-DxvBAeL3.js} +3 -3
- package/dist/{railroadDiagram-O6MQD6OU-C7uNKtfm.js → railroadDiagram-O6MQD6OU-Detj1kcB.js} +2 -2
- package/dist/{requirementDiagram-BXWQKSXE-CxAIzQfl.js → requirementDiagram-BXWQKSXE-BLFD0T_Z.js} +1 -1
- package/dist/{reveal-component-XiecZ3Ua.js → reveal-component-CXMXeDZD.js} +6 -6
- package/dist/{sequenceDiagram-WJ2MYXX4-B5tmlPd5.js → sequenceDiagram-WJ2MYXX4-BZBgatQV.js} +1 -1
- package/dist/{spec-45FnujvG.js → spec-Mnkn3XEr.js} +1 -1
- package/dist/{stateDiagram-D77RDMKH-C1hc5H7Y.js → stateDiagram-D77RDMKH-4BZW9-6_.js} +2 -2
- package/dist/{stateDiagram-v2-MP3YSRHH-DRWeqQTn.js → stateDiagram-v2-MP3YSRHH-C4sKC8Iq.js} +2 -2
- package/dist/style.css +1 -1
- package/dist/{swimlanes-42K2YHIH-NTLas--s.js → swimlanes-42K2YHIH-SL5yeGe2.js} +1 -1
- package/dist/{swimlanesDiagram-VR7AAH4N-ChPggljD.js → swimlanesDiagram-VR7AAH4N-DqPmAYUv.js} +2 -2
- package/dist/{timeline-definition-24CTP7MA-B4CZ9srJ.js → timeline-definition-24CTP7MA-BfXUMWkq.js} +1 -1
- package/dist/{toDate-rySyYxqk.js → toDate-TMubKIVL.js} +22 -22
- package/dist/{useAsyncData-B7MiltyV.js → useAsyncData-DjMGFzFy.js} +1 -1
- package/dist/{useDeepCompareMemoize-DZmHBD_1.js → useDeepCompareMemoize-BQR5TJot.js} +1 -1
- package/dist/{useLifecycle-DRbRhzxg.js → useLifecycle-C_GvKWbQ.js} +26 -23
- package/dist/{useTheme-TLkqJsvo.js → useTheme-Bns2gJZ6.js} +179 -178
- package/dist/{vega-component-DC6P127L.js → vega-component-BsVptuhz.js} +5 -5
- package/dist/{vennDiagram-4TSXK5OY-DJIvMt44.js → vennDiagram-4TSXK5OY-CH65znGa.js} +1 -1
- package/dist/{wardleyDiagram-VM6X3IG4-45H0zg0u.js → wardleyDiagram-VM6X3IG4-z9pWKb2_.js} +1 -1
- package/dist/{xychartDiagram-S5SC5T6Z-ZT-XzcPN.js → xychartDiagram-S5SC5T6Z-MxyaWjjq.js} +1 -1
- package/package.json +1 -1
- package/src/__mocks__/requests.ts +0 -1
- package/src/components/editor/cell/StagedAICell.tsx +10 -2
- package/src/components/editor/cell/code/cell-editor.tsx +7 -14
- package/src/components/editor/chrome/wrapper/__tests__/document-sync-status.test.tsx +92 -0
- package/src/components/editor/chrome/wrapper/footer-items/document-sync-status.tsx +54 -0
- package/src/components/editor/chrome/wrapper/footer.tsx +2 -0
- package/src/core/ai/__tests__/staged-cells.test.ts +8 -5
- package/src/core/ai/staged-cells.ts +7 -2
- package/src/core/ai/tools/__tests__/edit-notebook-tool.test.ts +3 -15
- package/src/core/ai/tools/edit-notebook-tool.ts +14 -3
- package/src/core/cells/__tests__/cells.test.ts +0 -297
- package/src/core/cells/__tests__/document.test.ts +410 -0
- package/src/core/cells/__tests__/flush-document.test.ts +40 -0
- package/src/core/cells/__tests__/notebook-outline.test.ts +1 -6
- package/src/core/cells/__tests__/with-document-save.test.ts +67 -0
- package/src/core/cells/cells.ts +27 -158
- package/src/core/cells/columns.ts +38 -0
- package/src/core/cells/document.ts +360 -0
- package/src/core/cells/types.ts +12 -0
- package/src/core/codemirror/cells/__tests__/extensions.test.ts +27 -1
- package/src/core/codemirror/cells/extensions.ts +14 -0
- package/src/core/codemirror/format.ts +6 -5
- package/src/core/codemirror/language/__tests__/utils.test.ts +187 -15
- package/src/core/codemirror/language/cell-code.ts +114 -0
- package/src/core/codemirror/language/extension.ts +47 -30
- package/src/core/codemirror/language/languages/sql/sql.ts +7 -17
- package/src/core/codemirror/language/utils.ts +9 -39
- package/src/core/codemirror/lsp/notebook-lsp.ts +6 -5
- package/src/core/codemirror/rtc/extension.ts +0 -1
- package/src/core/edit-app.tsx +4 -3
- package/src/core/export/layout.ts +2 -2
- package/src/core/islands/bridge.ts +0 -1
- package/src/core/network/requests-lazy.ts +0 -1
- package/src/core/network/requests-network.ts +0 -9
- package/src/core/network/requests-static.ts +0 -1
- package/src/core/network/requests-toasting.tsx +0 -1
- package/src/core/network/types.ts +0 -5
- package/src/core/notebook/document/__tests__/codemirror.test.ts +76 -14
- package/src/core/notebook/document/__tests__/hold.test.ts +88 -0
- package/src/core/notebook/document/__tests__/hooks.test.tsx +3 -3
- package/src/core/notebook/document/__tests__/lifecycle-scenarios.test.ts +3 -3
- package/src/core/notebook/document/__tests__/model.test.ts +71 -15
- package/src/core/notebook/document/__tests__/scenario-fixture.ts +1 -1
- package/src/core/notebook/document/codemirror.ts +27 -122
- package/src/core/notebook/document/hooks.tsx +2 -2
- package/src/core/notebook/document/index.ts +9 -1
- package/src/core/notebook/document/model.ts +74 -24
- package/src/core/notebook/document/sync.ts +53 -4
- package/src/core/run-app.tsx +44 -41
- package/src/core/saving/save-component.tsx +2 -5
- package/src/core/session/__tests__/useNotebookSession.hook.test.tsx +63 -3
- package/src/core/session/active-notebook.ts +4 -0
- package/src/core/session/useNotebookSession.tsx +39 -19
- package/src/core/wasm/__tests__/bridge.test.ts +3 -3
- package/src/core/wasm/bridge.ts +5 -12
- package/src/core/wasm/worker/worker.ts +0 -9
- package/src/utils/__tests__/{createReducer.test.ts → createReducer.test.tsx} +28 -0
- package/src/utils/createReducer.ts +42 -21
- package/src/core/cells/__tests__/apply-transaction.test.ts +0 -991
- package/src/core/cells/__tests__/document-changes.test.ts +0 -1140
- package/src/core/cells/__tests__/document-roundtrip.test.ts +0 -392
- package/src/core/cells/document-changes.ts +0 -944
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
2
|
|
|
3
3
|
import { atom } from "jotai";
|
|
4
|
+
import type { DocumentModel } from "../notebook/document";
|
|
4
5
|
import type { NotebookReplica } from "../notebook/replica";
|
|
5
6
|
|
|
6
7
|
/** The replica owned by the mounted notebook session. */
|
|
7
8
|
export const activeNotebookReplicaAtom = atom<NotebookReplica | null>(null);
|
|
9
|
+
|
|
10
|
+
/** The document model owned by the mounted notebook session. */
|
|
11
|
+
export const activeDocumentModelAtom = atom<DocumentModel | null>(null);
|
|
@@ -2,16 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
import { invalidatePackageData } from "@/core/packages/package-data";
|
|
4
4
|
import { useAtom, useAtomValue, useSetAtom, useStore } from "jotai";
|
|
5
|
-
import { useLayoutEffect } from "react";
|
|
5
|
+
import { useEffect, useLayoutEffect, useMemo } from "react";
|
|
6
6
|
import { useErrorBoundary } from "react-error-boundary";
|
|
7
7
|
import { toast } from "@/components/ui/use-toast";
|
|
8
8
|
import { getNotebook, useCellActions } from "@/core/cells/cells";
|
|
9
|
-
import { applyTransactionChanges } from "@/core/cells/document-changes";
|
|
10
9
|
import { AUTOCOMPLETER } from "@/core/codemirror/completion/Autocompleter";
|
|
11
|
-
import
|
|
12
|
-
|
|
13
|
-
NotificationPayload,
|
|
14
|
-
} from "@/core/kernel/messages";
|
|
10
|
+
import { createDocumentModel } from "@/core/notebook/document";
|
|
11
|
+
import type { NotificationPayload } from "@/core/kernel/messages";
|
|
15
12
|
import { useNotebookReplica } from "@/core/session/useNotebookReplica";
|
|
16
13
|
import { renderHTML } from "@/plugins/core/RenderHTML";
|
|
17
14
|
import {
|
|
@@ -75,6 +72,7 @@ import {
|
|
|
75
72
|
import { useStorageActions } from "../storage/state";
|
|
76
73
|
import { useVariablesActions } from "../variables/state";
|
|
77
74
|
import type { VariableName } from "../variables/types";
|
|
75
|
+
import { activeDocumentModelAtom } from "./active-notebook";
|
|
78
76
|
import { WebSocketClosedReason, ConnectionState } from "./types";
|
|
79
77
|
|
|
80
78
|
const SUPPORTS_LAZY_KERNELS = true;
|
|
@@ -100,17 +98,7 @@ export function useNotebookSession(opts: {
|
|
|
100
98
|
const { showBoundary } = useErrorBoundary();
|
|
101
99
|
|
|
102
100
|
const { handleCellMessage } = useCellActions();
|
|
103
|
-
const
|
|
104
|
-
|
|
105
|
-
const handleDocumentTransaction = (
|
|
106
|
-
transaction: NotificationMessageData<"notebook-document-transaction">["transaction"],
|
|
107
|
-
) => {
|
|
108
|
-
applyTransactionChanges(
|
|
109
|
-
transaction.changes,
|
|
110
|
-
actionsWithoutMiddleware,
|
|
111
|
-
() => getNotebook().cellIds.inOrderIds,
|
|
112
|
-
);
|
|
113
|
-
};
|
|
101
|
+
const { reconcileDocument } = useCellActions({ skipMiddleware: true });
|
|
114
102
|
const { addCellNotification } = useRunsActions();
|
|
115
103
|
const setKernelState = useSetAtom(kernelStateAtom);
|
|
116
104
|
const setInitialRunCompleted = useSetAtom(initialRunCompletedAtom);
|
|
@@ -395,7 +383,7 @@ export function useNotebookSession(opts: {
|
|
|
395
383
|
);
|
|
396
384
|
return;
|
|
397
385
|
case "notebook-document-transaction":
|
|
398
|
-
|
|
386
|
+
// Edits reach the notebook through the host document.
|
|
399
387
|
return;
|
|
400
388
|
case "consumer-capabilities":
|
|
401
389
|
setKioskMode(!msg.data.consumer_capabilities.edit);
|
|
@@ -440,6 +428,38 @@ export function useNotebookSession(opts: {
|
|
|
440
428
|
},
|
|
441
429
|
});
|
|
442
430
|
|
|
431
|
+
const documentModel = useMemo(
|
|
432
|
+
() =>
|
|
433
|
+
createDocumentModel({
|
|
434
|
+
store,
|
|
435
|
+
documentAtom: replica.documentAtom,
|
|
436
|
+
backendAtom: replica.backendAtom,
|
|
437
|
+
}),
|
|
438
|
+
[store, replica],
|
|
439
|
+
);
|
|
440
|
+
// Edits outlive reconnects and end with the session. The notebook's cells
|
|
441
|
+
// follow the document for as long as the session is mounted.
|
|
442
|
+
useEffect(() => {
|
|
443
|
+
const controller = new AbortController();
|
|
444
|
+
documentModel.start({ signal: controller.signal });
|
|
445
|
+
store.set(activeDocumentModelAtom, documentModel);
|
|
446
|
+
const follow = () => {
|
|
447
|
+
const document = store.get(documentModel.documentAtom);
|
|
448
|
+
if (document !== null) {
|
|
449
|
+
reconcileDocument(document);
|
|
450
|
+
}
|
|
451
|
+
};
|
|
452
|
+
const unsubscribe = store.sub(documentModel.documentAtom, follow);
|
|
453
|
+
follow();
|
|
454
|
+
return () => {
|
|
455
|
+
unsubscribe();
|
|
456
|
+
controller.abort();
|
|
457
|
+
if (store.get(activeDocumentModelAtom) === documentModel) {
|
|
458
|
+
store.set(activeDocumentModelAtom, null);
|
|
459
|
+
}
|
|
460
|
+
};
|
|
461
|
+
}, [documentModel, store, reconcileDocument]);
|
|
462
|
+
|
|
443
463
|
// Subscribe before passive connection setup: static/WASM transports can emit synchronously.
|
|
444
464
|
useLayoutEffect(() => {
|
|
445
465
|
const updateConnection = () => {
|
|
@@ -485,5 +505,5 @@ export function useNotebookSession(opts: {
|
|
|
485
505
|
updateStartupProgress,
|
|
486
506
|
]);
|
|
487
507
|
|
|
488
|
-
return { replica, connection, reconnect: replica.reconnect };
|
|
508
|
+
return { replica, documentModel, connection, reconnect: replica.reconnect };
|
|
489
509
|
}
|
|
@@ -101,9 +101,9 @@ vi.mock("@/core/wasm/store", () => ({
|
|
|
101
101
|
notebookFileStore: { readFile: mockNotebookReadFile, saveFile: vi.fn() },
|
|
102
102
|
}));
|
|
103
103
|
|
|
104
|
-
vi.mock("@/core/cells/document
|
|
105
|
-
|
|
106
|
-
|
|
104
|
+
vi.mock("@/core/cells/document", async (importOriginal) => ({
|
|
105
|
+
...(await importOriginal<typeof import("@/core/cells/document")>()),
|
|
106
|
+
flushDocument: mockFlush,
|
|
107
107
|
}));
|
|
108
108
|
|
|
109
109
|
// Import after all mocks are set up
|
package/src/core/wasm/bridge.ts
CHANGED
|
@@ -13,7 +13,7 @@ import { once } from "@/utils/once";
|
|
|
13
13
|
import { generateUUID } from "@/utils/uuid";
|
|
14
14
|
import { createModuleWorker } from "@/utils/worker";
|
|
15
15
|
import { notebookIsRunningAtom } from "../cells/cells";
|
|
16
|
-
import {
|
|
16
|
+
import { flushDocument } from "../cells/document";
|
|
17
17
|
import { createWasmNotebookBackend } from "../transports/wasm";
|
|
18
18
|
import type { ClientCommand } from "../kernel/messages";
|
|
19
19
|
import { getInitialAppMode } from "../mode";
|
|
@@ -280,7 +280,7 @@ export class PyodideBridge implements RunRequests, EditRequests {
|
|
|
280
280
|
}
|
|
281
281
|
|
|
282
282
|
public sendSave: EditRequests["sendSave"] = async ({ layout }) => {
|
|
283
|
-
await
|
|
283
|
+
await flushDocument();
|
|
284
284
|
await this.rpc.proxy.request.setNotebookLayout(layout ?? null);
|
|
285
285
|
await this.saveHostNotebook();
|
|
286
286
|
return null;
|
|
@@ -565,7 +565,7 @@ export class PyodideBridge implements RunRequests, EditRequests {
|
|
|
565
565
|
public exportAsHTML: EditRequests["exportAsHTML"] = async (
|
|
566
566
|
request: ExportAsHTMLRequest,
|
|
567
567
|
) => {
|
|
568
|
-
await
|
|
568
|
+
await flushDocument();
|
|
569
569
|
const response = await this.rpc.proxy.request.bridge({
|
|
570
570
|
functionName: "export_html",
|
|
571
571
|
payload: withDevAssetUrl(request),
|
|
@@ -576,7 +576,7 @@ export class PyodideBridge implements RunRequests, EditRequests {
|
|
|
576
576
|
public exportAsMarkdown: EditRequests["exportAsMarkdown"] = async (
|
|
577
577
|
request: ExportAsMarkdownRequest,
|
|
578
578
|
) => {
|
|
579
|
-
await
|
|
579
|
+
await flushDocument();
|
|
580
580
|
const response = await this.rpc.proxy.request.bridge({
|
|
581
581
|
functionName: "export_markdown",
|
|
582
582
|
payload: request,
|
|
@@ -587,7 +587,7 @@ export class PyodideBridge implements RunRequests, EditRequests {
|
|
|
587
587
|
public exportAsScript: EditRequests["exportAsScript"] = async (
|
|
588
588
|
request: ExportAsScriptRequest,
|
|
589
589
|
) => {
|
|
590
|
-
await
|
|
590
|
+
await flushDocument();
|
|
591
591
|
const response = await this.rpc.proxy.request.bridge({
|
|
592
592
|
functionName: "export_script",
|
|
593
593
|
payload: request,
|
|
@@ -658,13 +658,6 @@ export class PyodideBridge implements RunRequests, EditRequests {
|
|
|
658
658
|
return null;
|
|
659
659
|
};
|
|
660
660
|
|
|
661
|
-
public sendDocumentTransaction: EditRequests["sendDocumentTransaction"] =
|
|
662
|
-
async (request) => {
|
|
663
|
-
await this.initialized.promise;
|
|
664
|
-
await this.rpc.proxy.request.documentTransaction(request);
|
|
665
|
-
return null;
|
|
666
|
-
};
|
|
667
|
-
|
|
668
661
|
public addPackage: EditRequests["addPackage"] = async (request) => {
|
|
669
662
|
return this.rpc.proxy.request.addPackage(request);
|
|
670
663
|
};
|
|
@@ -12,7 +12,6 @@ import type { UserConfig } from "@/core/config/config-schema";
|
|
|
12
12
|
import type {
|
|
13
13
|
ListPackagesResponse,
|
|
14
14
|
PackageOperationResponse,
|
|
15
|
-
NotebookDocumentTransactionRequest,
|
|
16
15
|
SaveNotebookRequest,
|
|
17
16
|
} from "@/core/network/types";
|
|
18
17
|
import { decodeUtf8 } from "@/utils/strings";
|
|
@@ -302,14 +301,6 @@ const requestHandler = createRPCRequestHandler({
|
|
|
302
301
|
};
|
|
303
302
|
},
|
|
304
303
|
|
|
305
|
-
documentTransaction: async (request: NotebookDocumentTransactionRequest) => {
|
|
306
|
-
const notebook = await getNotebook();
|
|
307
|
-
try {
|
|
308
|
-
await notebook.transaction(JSON.stringify(request));
|
|
309
|
-
} finally {
|
|
310
|
-
notebook.destroy();
|
|
311
|
-
}
|
|
312
|
-
},
|
|
313
304
|
setNotebookLayout: async (layout: SaveNotebookRequest["layout"]) => {
|
|
314
305
|
const notebook = await getNotebook();
|
|
315
306
|
try {
|
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
|
+
import { renderHook } from "@testing-library/react";
|
|
3
|
+
import { createStore, Provider } from "jotai";
|
|
4
|
+
import type { PropsWithChildren } from "react";
|
|
2
5
|
import { describe, expect, it, vi } from "vitest";
|
|
3
6
|
import { Logger } from "@/utils/Logger";
|
|
4
7
|
import { createReducer, createReducerAndAtoms } from "../createReducer";
|
|
@@ -140,6 +143,31 @@ describe("createReducerAndAtoms", () => {
|
|
|
140
143
|
expect(middleware).toHaveBeenCalledWith(state, { count: 5 }, action);
|
|
141
144
|
});
|
|
142
145
|
|
|
146
|
+
it("runs middleware after the write commits, so it sees the new state", () => {
|
|
147
|
+
const initialState = () => ({ count: 0 });
|
|
148
|
+
const reducers = {
|
|
149
|
+
increment: (state: State, payload: number) => ({
|
|
150
|
+
count: state.count + payload,
|
|
151
|
+
}),
|
|
152
|
+
};
|
|
153
|
+
const seen: number[] = [];
|
|
154
|
+
const { valueAtom, useActions } = createReducerAndAtoms(
|
|
155
|
+
initialState,
|
|
156
|
+
reducers,
|
|
157
|
+
[(_prev, next) => seen.push(store.get(valueAtom).count, next.count)],
|
|
158
|
+
);
|
|
159
|
+
const store = createStore();
|
|
160
|
+
const wrapper = ({ children }: PropsWithChildren) => (
|
|
161
|
+
<Provider store={store}>{children}</Provider>
|
|
162
|
+
);
|
|
163
|
+
const { result } = renderHook(() => useActions(), { wrapper });
|
|
164
|
+
|
|
165
|
+
result.current.increment(5);
|
|
166
|
+
|
|
167
|
+
expect(seen).toEqual([5, 5]);
|
|
168
|
+
expect(store.get(valueAtom)).toEqual({ count: 5 });
|
|
169
|
+
});
|
|
170
|
+
|
|
143
171
|
it("should log an error for non-existent action types", () => {
|
|
144
172
|
const initialState = () => ({ count: 0 });
|
|
145
173
|
const reducers = {
|
|
@@ -104,49 +104,70 @@ export function createReducerAndAtoms<
|
|
|
104
104
|
};
|
|
105
105
|
const { reducer, createActions } = createReducer(initialState, reducers);
|
|
106
106
|
|
|
107
|
-
const
|
|
107
|
+
const reduceSafely = (state: State, action: ReducerActionOf<RH>) => {
|
|
108
108
|
try {
|
|
109
|
-
|
|
110
|
-
for (const mw of allMiddleware) {
|
|
111
|
-
try {
|
|
112
|
-
mw(state, newState, action);
|
|
113
|
-
} catch (error) {
|
|
114
|
-
Logger.error(`Error in middleware for action ${action.type}:`, error);
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
return newState;
|
|
109
|
+
return reducer(state, action);
|
|
118
110
|
} catch (error) {
|
|
119
111
|
Logger.error(`Error in reducer for action ${action.type}:`, error);
|
|
120
112
|
return state;
|
|
121
113
|
}
|
|
122
114
|
};
|
|
123
115
|
|
|
116
|
+
const runMiddleware = (
|
|
117
|
+
prev: State,
|
|
118
|
+
next: State,
|
|
119
|
+
action: ReducerActionOf<RH>,
|
|
120
|
+
) => {
|
|
121
|
+
for (const mw of allMiddleware) {
|
|
122
|
+
try {
|
|
123
|
+
mw(prev, next, action);
|
|
124
|
+
} catch (error) {
|
|
125
|
+
Logger.error(`Error in middleware for action ${action.type}:`, error);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
const reducerWithMiddleware = (state: State, action: ReducerActionOf<RH>) => {
|
|
131
|
+
const next = reduceSafely(state, action);
|
|
132
|
+
runMiddleware(state, next, action);
|
|
133
|
+
return next;
|
|
134
|
+
};
|
|
135
|
+
|
|
124
136
|
const valueAtom = atom(initialState());
|
|
125
|
-
// map of SetAtom => Actions
|
|
137
|
+
// map of SetAtom => Actions, with and without middleware
|
|
126
138
|
const actionsMap = new WeakMap();
|
|
139
|
+
const plainActionsMap = new WeakMap();
|
|
127
140
|
|
|
128
141
|
function useActions(
|
|
129
142
|
options: { skipMiddleware?: boolean } = {},
|
|
130
143
|
): ReducerActions<RH> {
|
|
131
144
|
const setState = useSetAtom(valueAtom);
|
|
145
|
+
const skipMiddleware = options.skipMiddleware === true;
|
|
146
|
+
const cache = skipMiddleware ? plainActionsMap : actionsMap;
|
|
132
147
|
|
|
133
|
-
if (
|
|
134
|
-
|
|
135
|
-
setState((state: State) => reducer(state, action));
|
|
136
|
-
});
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
if (!actionsMap.has(setState)) {
|
|
140
|
-
actionsMap.set(
|
|
148
|
+
if (!cache.has(setState)) {
|
|
149
|
+
cache.set(
|
|
141
150
|
setState,
|
|
142
151
|
createActions((action: ReducerActionOf<RH>) => {
|
|
143
|
-
|
|
152
|
+
// Reduce inside the write. Middleware runs once the write has
|
|
153
|
+
// committed and its subscribers have run, so anything it sets in
|
|
154
|
+
// turn observes the new state rather than the one being replaced.
|
|
155
|
+
let prev: State | undefined;
|
|
156
|
+
let next: State | undefined;
|
|
157
|
+
setState((state: State) => {
|
|
158
|
+
prev = state;
|
|
159
|
+
next = reduceSafely(state, action);
|
|
160
|
+
return next;
|
|
161
|
+
});
|
|
162
|
+
if (!skipMiddleware && prev !== undefined && next !== undefined) {
|
|
163
|
+
runMiddleware(prev, next, action);
|
|
164
|
+
}
|
|
144
165
|
}),
|
|
145
166
|
);
|
|
146
167
|
}
|
|
147
168
|
|
|
148
169
|
// oxlint-disable-next-line typescript/no-non-null-assertion
|
|
149
|
-
return
|
|
170
|
+
return cache.get(setState)!;
|
|
150
171
|
}
|
|
151
172
|
|
|
152
173
|
return {
|