@marimo-team/islands 0.25.3-dev206 → 0.25.3-dev207
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-DFynurqb.js → ConnectedDataExplorerComponent-ApTYh0V3.js} +4 -4
- package/dist/{ErrorBoundary-DZyGV5dX.js → ErrorBoundary-DV6q3WpD.js} +2 -2
- package/dist/{abnfDiagram-VCTEODGH-DGkUyNdz.js → abnfDiagram-VCTEODGH-uo9xhZY0.js} +2 -2
- package/dist/{architectureDiagram-5GKGNRK7-Bav9J7uF.js → architectureDiagram-5GKGNRK7-Hz6eyeOD.js} +1 -1
- package/dist/assets/__vite-browser-external-CBRgnYY0.js +1 -0
- package/dist/assets/{worker-DodjWT_Z.js → worker-DiPNP__U.js} +2 -2
- package/dist/{chat-ui-Dorq26Pp.js → chat-ui-BTA1I5i1.js} +7 -7
- package/dist/{chunk-IMKFNOWR-1UCTsTqh.js → chunk-IMKFNOWR-DBM2i23k.js} +1 -1
- package/dist/{chunk-SHT3W25Y-Bige_rTb.js → chunk-SHT3W25Y-L8KQLLBy.js} +1 -1
- package/dist/{chunk-SVP7TREG-Cmr0Anfb.js → chunk-SVP7TREG-BOUJKJEP.js} +1 -1
- package/dist/{chunk-TICWLB2K-IWykFldb.js → chunk-TICWLB2K-DY3CzFjX.js} +1 -1
- package/dist/{classDiagram-ZZMXUADV-BH73ECRQ.js → classDiagram-ZZMXUADV-s9iwW-q4.js} +2 -2
- package/dist/{classDiagram-v2-VYDZK3BY-CCuCgq0H.js → classDiagram-v2-VYDZK3BY-2GI5CjGB.js} +2 -2
- package/dist/{code-visibility-CFFYYeyU.js → code-visibility-DLj8SFEM.js} +4 -4
- package/dist/{common-B9xea2wZ.js → common-BEiP9-YB.js} +9 -9
- package/dist/{cynefinDiagram-5FMLGOSQ-CWtL7MIJ.js → cynefinDiagram-5FMLGOSQ-DOxnhBK2.js} +1 -1
- package/dist/{dagre-GXQ25YYZ-DsVO2pVC.js → dagre-GXQ25YYZ-D1AhEfpM.js} +1 -1
- package/dist/{data-grid-overlay-editor-De3J2jxB.js → data-grid-overlay-editor-Bt6VgNDi.js} +1 -1
- package/dist/{diagram-S7CK7UJ4-BuAktgZv.js → diagram-S7CK7UJ4-DV_GebF_.js} +1 -1
- package/dist/{diagram-UQ7AKVKN-DlkAXHu5.js → diagram-UQ7AKVKN-6dNzGFfD.js} +1 -1
- package/dist/{diagram-VX7I27RA-B9U9H0E8.js → diagram-VX7I27RA-DUh9jsfi.js} +1 -1
- package/dist/{diagram-Z3DM3KII-BqmfxzUL.js → diagram-Z3DM3KII-Cug-qcQ8.js} +1 -1
- package/dist/{dropdown-menu-BFQYzWmC.js → dropdown-menu-dlEm41Bt.js} +1 -1
- package/dist/{ebnfDiagram-PWID7BFC-CH5t8ATi.js → ebnfDiagram-PWID7BFC-d5LKx7Vb.js} +2 -2
- package/dist/{erDiagram-RLTQ6QDP-DdIo2BIb.js → erDiagram-RLTQ6QDP-BwvlkcID.js} +1 -1
- package/dist/{error-entries-B0bcEwXm.js → error-entries-CVUHA5r_.js} +3875 -3884
- package/dist/{feedback-button-DXpmx6ff.js → feedback-button-Dd87gzc6.js} +5 -5
- package/dist/{flowDiagram-HODETNUW-BmYk_-Na.js → flowDiagram-HODETNUW-CJcSoFer.js} +2 -2
- package/dist/{formats-DyYnckF5.js → formats-Ry7c-aIV.js} +1 -1
- package/dist/{glide-data-editor-BevQ8Zdz.js → glide-data-editor-CW4LOU-D.js} +5 -5
- package/dist/{globals-Beuc24As.js → globals-D3AhIN1i.js} +2 -2
- package/dist/{html-to-image-BOd7ryZJ.js → html-to-image--E3uFbJ9.js} +5 -5
- package/dist/{infoDiagram-27XIBGKW-DvYh-Sbk.js → infoDiagram-27XIBGKW-Cezgppw3.js} +1 -1
- package/dist/{ishikawaDiagram-5VMMS53U-BdHnYq8s.js → ishikawaDiagram-5VMMS53U-BahZQwJz.js} +1 -1
- package/dist/{kanban-definition-UXKFOSKX-Bf8CT_tU.js → kanban-definition-UXKFOSKX-Cvfn_Obm.js} +1 -1
- package/dist/main.js +22 -22
- package/dist/{mermaid-DBpOnw8c.js → mermaid-BrdwbArF.js} +34 -34
- package/dist/{mindmap-definition-YA3MSWOX-DkYG8ltR.js → mindmap-definition-YA3MSWOX-uB4B2d-J.js} +1 -1
- package/dist/{number-overlay-editor-DX_x9WUo.js → number-overlay-editor-klVnf90Q.js} +1 -1
- package/dist/{pegDiagram-XKGWAZYB-DM7BQ0ip.js → pegDiagram-XKGWAZYB-CYaIwAmH.js} +2 -2
- package/dist/{pieDiagram-E7YTZNPT-CPFGg4YS.js → pieDiagram-E7YTZNPT-BJ1oUxVv.js} +1 -1
- package/dist/{process-output-8BbxxGrz.js → process-output-SjJxB1wh.js} +2 -2
- package/dist/{railroadDiagram-O6MQD6OU-4rUJlIpQ.js → railroadDiagram-O6MQD6OU-BqKSaBux.js} +2 -2
- package/dist/{requirementDiagram-BXWQKSXE-F6ffUUQC.js → requirementDiagram-BXWQKSXE-DwFhgDhH.js} +1 -1
- package/dist/{reveal-component-BMw2TipA.js → reveal-component-CS16WNwA.js} +6 -6
- package/dist/{sequenceDiagram-WJ2MYXX4-tNU95HVS.js → sequenceDiagram-WJ2MYXX4-L7ENjfKF.js} +1 -1
- package/dist/{spec-Alkla7dB.js → spec-BcJ9ncAY.js} +1 -1
- package/dist/{stateDiagram-D77RDMKH-BA8Modqv.js → stateDiagram-D77RDMKH-CVf88u-p.js} +2 -2
- package/dist/{stateDiagram-v2-MP3YSRHH-CFmickht.js → stateDiagram-v2-MP3YSRHH-DvpkQ1V6.js} +2 -2
- package/dist/style.css +1 -1
- package/dist/{swimlanes-42K2YHIH-DBiYZZZc.js → swimlanes-42K2YHIH-DIuFHbVE.js} +1 -1
- package/dist/{swimlanesDiagram-VR7AAH4N-CsgR851k.js → swimlanesDiagram-VR7AAH4N-CAqHAziW.js} +2 -2
- package/dist/{timeline-definition-24CTP7MA-Dfj0CMXY.js → timeline-definition-24CTP7MA-xO4EQqBm.js} +1 -1
- package/dist/{toDate-B0-Y0SEW.js → toDate-BZZ64K_m.js} +1 -5
- package/dist/{useAsyncData-Phxf6GCd.js → useAsyncData-Dqw22wZQ.js} +1 -1
- package/dist/{useDeepCompareMemoize-BXv7DUcU.js → useDeepCompareMemoize-D3gozcjJ.js} +1 -1
- package/dist/{useLifecycle-DDDsO4O2.js → useLifecycle-CaZi4wTw.js} +1 -1
- package/dist/{useTheme-DbBkeP6Z.js → useTheme-TzPgWXwB.js} +1 -4
- package/dist/{vega-component-C_dhEynE.js → vega-component-Blvspl7v.js} +5 -5
- package/dist/{vennDiagram-4TSXK5OY-fJ83Z7FE.js → vennDiagram-4TSXK5OY-DzCi-lRh.js} +1 -1
- package/dist/{wardleyDiagram-VM6X3IG4-DbO9cD15.js → wardleyDiagram-VM6X3IG4-CW48xa0n.js} +1 -1
- package/dist/{xychartDiagram-S5SC5T6Z-DmsdQJG7.js → xychartDiagram-S5SC5T6Z-UPxGGI2B.js} +1 -1
- package/package.json +2 -6
- package/src/components/app-config/user-config-form.tsx +0 -26
- package/src/components/editor/cell/code/cell-editor.tsx +11 -100
- package/src/components/editor/chrome/wrapper/footer.tsx +0 -2
- package/src/core/cells/__tests__/document.test.ts +0 -18
- package/src/core/cells/document.ts +6 -9
- package/src/core/codemirror/cells/__tests__/extensions.test.ts +1 -46
- package/src/core/codemirror/cells/extensions.ts +1 -19
- package/src/core/config/config-schema.ts +0 -1
- package/src/core/config/feature-flag.tsx +0 -2
- package/src/core/runtime/__tests__/runtime.test.ts +0 -16
- package/src/core/runtime/runtime.ts +0 -8
- package/src/css/app/codemirror.css +1 -1
- package/dist/assets/__vite-browser-external-BCJsx8QS.js +0 -1
- package/src/components/editor/cell/code/__tests__/rtc-editor-config.test.ts +0 -50
- package/src/components/editor/chrome/wrapper/footer-items/rtc-status.tsx +0 -65
- package/src/core/codemirror/rtc/extension.ts +0 -464
- package/src/core/codemirror/rtc/loro/__tests__/sync.test.ts +0 -52
- package/src/core/codemirror/rtc/loro/awareness.ts +0 -546
- package/src/core/codemirror/rtc/loro/colors.ts +0 -38
- package/src/core/codemirror/rtc/loro/sync.ts +0 -169
- package/src/core/rtc/state.ts +0 -34
|
@@ -143,13 +143,9 @@ describe("RuntimeManager", () => {
|
|
|
143
143
|
);
|
|
144
144
|
|
|
145
145
|
const wsUrl = runtime.getWsURL("s_123" as SessionId);
|
|
146
|
-
const wsSyncUrl = runtime.getWsSyncURL("s_123" as SessionId);
|
|
147
146
|
const terminalUrl = runtime.getTerminalWsURL();
|
|
148
147
|
|
|
149
148
|
expect(wsUrl.searchParams.get("access_token")).toBe("my-secret-token");
|
|
150
|
-
expect(wsSyncUrl.searchParams.get("access_token")).toBe(
|
|
151
|
-
"my-secret-token",
|
|
152
|
-
);
|
|
153
149
|
expect(terminalUrl.searchParams.get("access_token")).toBe(
|
|
154
150
|
"my-secret-token",
|
|
155
151
|
);
|
|
@@ -274,18 +270,6 @@ describe("RuntimeManager", () => {
|
|
|
274
270
|
});
|
|
275
271
|
});
|
|
276
272
|
|
|
277
|
-
describe("getWsSyncURL", () => {
|
|
278
|
-
it("should return WebSocket Sync URL", () => {
|
|
279
|
-
const runtime = new RuntimeManager(mockConfig);
|
|
280
|
-
const sessionId = "1234" as SessionId;
|
|
281
|
-
const url = runtime.getWsSyncURL(sessionId);
|
|
282
|
-
|
|
283
|
-
expect(url.protocol).toBe("wss:");
|
|
284
|
-
expect(url.pathname).toBe("/ws_sync");
|
|
285
|
-
expect(url.searchParams.get("session_id")).toBe(sessionId);
|
|
286
|
-
});
|
|
287
|
-
});
|
|
288
|
-
|
|
289
273
|
describe("getTerminalWsURL", () => {
|
|
290
274
|
it("should include dimensions in the terminal handshake", () => {
|
|
291
275
|
const runtime = new RuntimeManager(mockConfig);
|
|
@@ -184,14 +184,6 @@ export class RuntimeManager {
|
|
|
184
184
|
return url;
|
|
185
185
|
}
|
|
186
186
|
|
|
187
|
-
/**
|
|
188
|
-
* The WebSocket Sync URL of the runtime, for real-time updates.
|
|
189
|
-
*/
|
|
190
|
-
public getWsSyncURL(sessionId: SessionId): URL {
|
|
191
|
-
const url = this.getSessionURL("/ws_sync", sessionId);
|
|
192
|
-
return asWsUrl(this.appendCrossOriginAuth(url).toString());
|
|
193
|
-
}
|
|
194
|
-
|
|
195
187
|
/**
|
|
196
188
|
* The WebSocket URL of the terminal.
|
|
197
189
|
*/
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{t as e}from"./worker-DodjWT_Z.js";var t=e(((e,t)=>{t.exports={}}));export default t();
|
|
@@ -1,50 +0,0 @@
|
|
|
1
|
-
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
|
-
|
|
3
|
-
import { EditorState } from "@codemirror/state";
|
|
4
|
-
import { describe, expect, it } from "vitest";
|
|
5
|
-
import { composeRtcEditorConfig } from "../cell-editor";
|
|
6
|
-
|
|
7
|
-
describe("composeRtcEditorConfig", () => {
|
|
8
|
-
it("does not accumulate RTC extensions across reconfigurations", () => {
|
|
9
|
-
const baseExtension = EditorState.readOnly.of(false);
|
|
10
|
-
const baseExtensions = [baseExtension];
|
|
11
|
-
const firstRtc = {
|
|
12
|
-
code: "first RTC code",
|
|
13
|
-
extension: EditorState.tabSize.of(2),
|
|
14
|
-
};
|
|
15
|
-
const secondRtc = {
|
|
16
|
-
code: "second RTC code",
|
|
17
|
-
extension: EditorState.tabSize.of(4),
|
|
18
|
-
};
|
|
19
|
-
|
|
20
|
-
const firstConfig = composeRtcEditorConfig(
|
|
21
|
-
"local code",
|
|
22
|
-
baseExtensions,
|
|
23
|
-
firstRtc,
|
|
24
|
-
);
|
|
25
|
-
const secondConfig = composeRtcEditorConfig(
|
|
26
|
-
"local code",
|
|
27
|
-
baseExtensions,
|
|
28
|
-
secondRtc,
|
|
29
|
-
);
|
|
30
|
-
|
|
31
|
-
expect(baseExtensions).toEqual([baseExtension]);
|
|
32
|
-
expect(firstConfig).toEqual({
|
|
33
|
-
code: firstRtc.code,
|
|
34
|
-
extensions: [baseExtension, firstRtc.extension],
|
|
35
|
-
});
|
|
36
|
-
expect(secondConfig).toEqual({
|
|
37
|
-
code: secondRtc.code,
|
|
38
|
-
extensions: [baseExtension, secondRtc.extension],
|
|
39
|
-
});
|
|
40
|
-
});
|
|
41
|
-
|
|
42
|
-
it("reuses the base configuration when RTC is disabled", () => {
|
|
43
|
-
const extensions = [EditorState.readOnly.of(false)];
|
|
44
|
-
|
|
45
|
-
const config = composeRtcEditorConfig("local code", extensions, undefined);
|
|
46
|
-
|
|
47
|
-
expect(config).toEqual({ code: "local code", extensions });
|
|
48
|
-
expect(config.extensions).toBe(extensions);
|
|
49
|
-
});
|
|
50
|
-
});
|
|
@@ -1,65 +0,0 @@
|
|
|
1
|
-
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
|
-
|
|
3
|
-
import { useAtom, useAtomValue } from "jotai";
|
|
4
|
-
import { UsersIcon } from "lucide-react";
|
|
5
|
-
import type React from "react";
|
|
6
|
-
import { useState } from "react";
|
|
7
|
-
import { Input } from "@/components/ui/input";
|
|
8
|
-
import {
|
|
9
|
-
Popover,
|
|
10
|
-
PopoverContent,
|
|
11
|
-
PopoverTrigger,
|
|
12
|
-
} from "@/components/ui/popover";
|
|
13
|
-
import { connectedDocAtom } from "@/core/codemirror/rtc/extension";
|
|
14
|
-
import { isRtcEnabled, usernameAtom } from "@/core/rtc/state";
|
|
15
|
-
import { FooterItem } from "../footer-item";
|
|
16
|
-
|
|
17
|
-
export const RTCStatus: React.FC = () => {
|
|
18
|
-
const connectedDoc = useAtomValue(connectedDocAtom);
|
|
19
|
-
const [username, setUsername] = useAtom(usernameAtom);
|
|
20
|
-
const [open, setOpen] = useState(false);
|
|
21
|
-
|
|
22
|
-
if (!isRtcEnabled()) {
|
|
23
|
-
return null;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
if (connectedDoc === "disabled") {
|
|
27
|
-
return null;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
const tooltip = connectedDoc
|
|
31
|
-
? "Real-time collaboration active"
|
|
32
|
-
: "Connecting to real-time collaboration";
|
|
33
|
-
|
|
34
|
-
return (
|
|
35
|
-
<Popover open={open} onOpenChange={setOpen}>
|
|
36
|
-
<PopoverTrigger asChild={true}>
|
|
37
|
-
<FooterItem
|
|
38
|
-
tooltip={tooltip}
|
|
39
|
-
selected={false}
|
|
40
|
-
data-testid="footer-rtc-status"
|
|
41
|
-
>
|
|
42
|
-
<UsersIcon className="w-4 h-4" />
|
|
43
|
-
</FooterItem>
|
|
44
|
-
</PopoverTrigger>
|
|
45
|
-
<PopoverContent className="w-80">
|
|
46
|
-
<div className="space-y-4">
|
|
47
|
-
<div className="space-y-2">
|
|
48
|
-
<h4 className="font-medium leading-none">Username</h4>
|
|
49
|
-
<p className="text-sm text-muted-foreground">
|
|
50
|
-
Set your username for real-time collaboration
|
|
51
|
-
</p>
|
|
52
|
-
</div>
|
|
53
|
-
<Input
|
|
54
|
-
value={username}
|
|
55
|
-
autoCapitalize="off"
|
|
56
|
-
autoComplete="off"
|
|
57
|
-
autoCorrect="off"
|
|
58
|
-
onChange={(e) => setUsername(e.target.value)}
|
|
59
|
-
placeholder="Enter your username"
|
|
60
|
-
/>
|
|
61
|
-
</div>
|
|
62
|
-
</PopoverContent>
|
|
63
|
-
</Popover>
|
|
64
|
-
);
|
|
65
|
-
};
|
|
@@ -1,464 +0,0 @@
|
|
|
1
|
-
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
|
-
|
|
3
|
-
import type { Extension } from "@codemirror/state";
|
|
4
|
-
import { EditorView, ViewPlugin } from "@codemirror/view";
|
|
5
|
-
import { dequal as isEqual } from "dequal";
|
|
6
|
-
import { atom } from "jotai";
|
|
7
|
-
import { Awareness, LoroDoc, LoroMap, LoroText } from "loro-crdt";
|
|
8
|
-
import ReconnectingWebSocket from "partysocket/ws";
|
|
9
|
-
import type { CellId } from "@/core/cells/ids";
|
|
10
|
-
import { getSessionId } from "@/core/kernel/session";
|
|
11
|
-
import { getInitialAppMode } from "@/core/mode";
|
|
12
|
-
import { waitForConnectionOpen } from "@/core/network/connection";
|
|
13
|
-
import { isRtcEnabled, usernameAtom } from "@/core/rtc/state";
|
|
14
|
-
import { getRuntimeManager } from "@/core/runtime/config";
|
|
15
|
-
import { store } from "@/core/state/jotai";
|
|
16
|
-
import { isWasm } from "@/core/wasm/utils";
|
|
17
|
-
import { invariant } from "@/utils/invariant";
|
|
18
|
-
import { Logger } from "@/utils/Logger";
|
|
19
|
-
import { once } from "@/utils/once";
|
|
20
|
-
import {
|
|
21
|
-
getInitialLanguageAdapter,
|
|
22
|
-
languageAdapterState,
|
|
23
|
-
setLanguageAdapter,
|
|
24
|
-
switchLanguage,
|
|
25
|
-
} from "../language/extension";
|
|
26
|
-
import {
|
|
27
|
-
languageMetadataField,
|
|
28
|
-
setLanguageMetadata,
|
|
29
|
-
updateLanguageMetadata,
|
|
30
|
-
} from "../language/metadata";
|
|
31
|
-
import type { LanguageAdapterType } from "../language/types";
|
|
32
|
-
import type { AwarenessState, ScopeId, Uid, UserState } from "./loro/awareness";
|
|
33
|
-
import {
|
|
34
|
-
AwarenessPlugin,
|
|
35
|
-
createCursorLayer,
|
|
36
|
-
createSelectionLayer,
|
|
37
|
-
loroCursorTheme,
|
|
38
|
-
RemoteAwarenessPlugin,
|
|
39
|
-
remoteAwarenessStateField,
|
|
40
|
-
} from "./loro/awareness";
|
|
41
|
-
import { getColor } from "./loro/colors";
|
|
42
|
-
import { loroSyncAnnotation, loroSyncPlugin } from "./loro/sync";
|
|
43
|
-
|
|
44
|
-
const logger = Logger.get("rtc");
|
|
45
|
-
const awarenessLogger = logger.get("awareness").disabled();
|
|
46
|
-
|
|
47
|
-
const AWARENESS_PREFIX = "awareness:";
|
|
48
|
-
|
|
49
|
-
// Utility functions for message handling
|
|
50
|
-
function prefixMessage(
|
|
51
|
-
token: string,
|
|
52
|
-
message: Uint8Array,
|
|
53
|
-
): Uint8Array<ArrayBuffer> {
|
|
54
|
-
const tokenBytes = new TextEncoder().encode(token);
|
|
55
|
-
return new Uint8Array([...tokenBytes, ...message]);
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
function hasPrefix(data: Uint8Array, prefix: string): boolean {
|
|
59
|
-
const decoder = new TextDecoder();
|
|
60
|
-
const dataPrefix = decoder.decode(data.slice(0, prefix.length));
|
|
61
|
-
return dataPrefix === prefix;
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
function removePrefix(data: Uint8Array, prefix: string): Uint8Array {
|
|
65
|
-
return data.slice(prefix.length);
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
export const connectedDocAtom = atom<LoroDoc | "disabled" | undefined>(
|
|
69
|
-
undefined,
|
|
70
|
-
);
|
|
71
|
-
|
|
72
|
-
// Create a Loro document
|
|
73
|
-
const doc = new LoroDoc();
|
|
74
|
-
const awareness = new Awareness<AwarenessState>(doc.peerIdStr);
|
|
75
|
-
// const undoManager = new UndoManager(doc, {});
|
|
76
|
-
|
|
77
|
-
// Create a websocket connection to the server
|
|
78
|
-
const getWs = once(() => {
|
|
79
|
-
logger.debug("creating websocket");
|
|
80
|
-
|
|
81
|
-
const runtimeManager = getRuntimeManager();
|
|
82
|
-
const url = runtimeManager.getWsSyncURL(getSessionId()).toString();
|
|
83
|
-
|
|
84
|
-
// Create the websocket, but don't connect it yet
|
|
85
|
-
const ws = new ReconnectingWebSocket(url, undefined, {
|
|
86
|
-
// We don't want Infinity retries
|
|
87
|
-
maxRetries: 10,
|
|
88
|
-
debug: false,
|
|
89
|
-
startClosed: true,
|
|
90
|
-
connectionTimeout: 10_000,
|
|
91
|
-
});
|
|
92
|
-
|
|
93
|
-
// Start the connection
|
|
94
|
-
Promise.resolve().then(async () => {
|
|
95
|
-
// First wait the main /ws connection to be open
|
|
96
|
-
// This is to ensure the LoroDoc is created on the server
|
|
97
|
-
await waitForConnectionOpen();
|
|
98
|
-
|
|
99
|
-
// Don't open the websocket if not in edit mode
|
|
100
|
-
if (getInitialAppMode() !== "edit") {
|
|
101
|
-
return;
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
// Now open the websocket
|
|
105
|
-
ws.reconnect();
|
|
106
|
-
});
|
|
107
|
-
|
|
108
|
-
// Receive updates from the server
|
|
109
|
-
ws.addEventListener("message", async (event) => {
|
|
110
|
-
const blob: Blob = event.data;
|
|
111
|
-
const bytes = await blob.arrayBuffer();
|
|
112
|
-
const data = new Uint8Array(bytes);
|
|
113
|
-
|
|
114
|
-
// Handle awareness update
|
|
115
|
-
if (hasPrefix(data, AWARENESS_PREFIX)) {
|
|
116
|
-
const awarenessData = removePrefix(data, AWARENESS_PREFIX);
|
|
117
|
-
awareness.apply(awarenessData);
|
|
118
|
-
awarenessLogger.debug("applied awareness update");
|
|
119
|
-
return;
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
// Handle doc update
|
|
123
|
-
doc.import(data);
|
|
124
|
-
logger.debug("imported doc change. new doc:", doc.toJSON());
|
|
125
|
-
|
|
126
|
-
// Set the active doc only once the first message is received
|
|
127
|
-
// This is to ensure the LoroDoc is created on the server, and not the client.
|
|
128
|
-
if (store.get(connectedDocAtom) !== doc) {
|
|
129
|
-
store.set(connectedDocAtom, doc);
|
|
130
|
-
}
|
|
131
|
-
});
|
|
132
|
-
|
|
133
|
-
// Handle open event
|
|
134
|
-
ws.addEventListener("open", () => {
|
|
135
|
-
logger.debug("websocket open");
|
|
136
|
-
});
|
|
137
|
-
|
|
138
|
-
// Handle close event
|
|
139
|
-
ws.addEventListener("close", (e) => {
|
|
140
|
-
logger.warn("websocket close", e);
|
|
141
|
-
|
|
142
|
-
// Remove the active doc
|
|
143
|
-
if (store.get(connectedDocAtom) === doc) {
|
|
144
|
-
store.set(connectedDocAtom, undefined);
|
|
145
|
-
}
|
|
146
|
-
});
|
|
147
|
-
|
|
148
|
-
return ws;
|
|
149
|
-
});
|
|
150
|
-
|
|
151
|
-
// Kickoff the WS connection for edit mode
|
|
152
|
-
if (isRtcEnabled()) {
|
|
153
|
-
getWs();
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
// Send local updates to the server
|
|
157
|
-
doc.subscribeLocalUpdates((update) => {
|
|
158
|
-
logger.debug("local update, sending to server");
|
|
159
|
-
const ws = getWs();
|
|
160
|
-
ws.send(new Uint8Array(update));
|
|
161
|
-
});
|
|
162
|
-
|
|
163
|
-
// Handle awareness changes
|
|
164
|
-
awareness.addListener((updates, origin) => {
|
|
165
|
-
const changes = [...updates.added, ...updates.removed, ...updates.updated];
|
|
166
|
-
if (origin === "local") {
|
|
167
|
-
awarenessLogger.debug("awareness changes", changes);
|
|
168
|
-
const ws = getWs();
|
|
169
|
-
ws.send(prefixMessage(AWARENESS_PREFIX, awareness.encode(changes)));
|
|
170
|
-
}
|
|
171
|
-
});
|
|
172
|
-
|
|
173
|
-
export function realTimeCollaboration(
|
|
174
|
-
cellId: CellId,
|
|
175
|
-
initialCode = "",
|
|
176
|
-
): { extension: Extension; code: string } {
|
|
177
|
-
if (isWasm()) {
|
|
178
|
-
return {
|
|
179
|
-
extension: [],
|
|
180
|
-
code: initialCode,
|
|
181
|
-
};
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
// Get or create the code container
|
|
185
|
-
const hasPath = doc.getByPath(`codes/${cellId}`) !== undefined;
|
|
186
|
-
const loroText = doc
|
|
187
|
-
.getMap("codes")
|
|
188
|
-
.getOrCreateContainer(cellId, new LoroText());
|
|
189
|
-
if (!hasPath) {
|
|
190
|
-
logger.log("initializing code for new cell", initialCode);
|
|
191
|
-
loroText.insert(0, initialCode);
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
const userState: UserState = {
|
|
195
|
-
// We use getters to ensure the user name is up-to-date
|
|
196
|
-
get name() {
|
|
197
|
-
return store.get(usernameAtom) || "Anonymous";
|
|
198
|
-
},
|
|
199
|
-
get colorClassName() {
|
|
200
|
-
return getColor(this.name);
|
|
201
|
-
},
|
|
202
|
-
};
|
|
203
|
-
|
|
204
|
-
return {
|
|
205
|
-
code: initialCode.toString(),
|
|
206
|
-
extension: [
|
|
207
|
-
languageObserverExtension(cellId),
|
|
208
|
-
languageListenerExtension(cellId),
|
|
209
|
-
loroAwarenessPlugin({
|
|
210
|
-
doc,
|
|
211
|
-
awareness,
|
|
212
|
-
user: userState,
|
|
213
|
-
getTextFromDoc: () => loroText,
|
|
214
|
-
cellId,
|
|
215
|
-
}),
|
|
216
|
-
loroSyncPlugin(doc, ["codes", cellId], () => {
|
|
217
|
-
return loroText;
|
|
218
|
-
}),
|
|
219
|
-
],
|
|
220
|
-
};
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
/**
|
|
224
|
-
* Create a view plugin to observe language changes
|
|
225
|
-
* When the language changes, we need to update our local language
|
|
226
|
-
* Server -> Local
|
|
227
|
-
* @param cellId - The cell id
|
|
228
|
-
* @returns Extension
|
|
229
|
-
*/
|
|
230
|
-
function languageObserverExtension(cellId: CellId) {
|
|
231
|
-
const syncLanguage = (view: EditorView, language: LoroText) => {
|
|
232
|
-
const currentLang = view.state.field(languageAdapterState).type;
|
|
233
|
-
const newLang = language.toString() as LanguageAdapterType;
|
|
234
|
-
if (!newLang) {
|
|
235
|
-
return;
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
if (newLang !== currentLang) {
|
|
239
|
-
logger.debug(
|
|
240
|
-
`[incoming] setting language type: ${currentLang} -> ${newLang}`,
|
|
241
|
-
);
|
|
242
|
-
switchLanguage(view, { language: newLang, keepCodeAsIs: true });
|
|
243
|
-
}
|
|
244
|
-
};
|
|
245
|
-
|
|
246
|
-
const syncLanguageMetadata = (
|
|
247
|
-
view: EditorView,
|
|
248
|
-
languageMetadata: LoroMap,
|
|
249
|
-
) => {
|
|
250
|
-
const previousLanguageMetadata = view.state.field(languageMetadataField);
|
|
251
|
-
const newLanguageMetadata = languageMetadata.toJSON();
|
|
252
|
-
if (isEqual(previousLanguageMetadata, newLanguageMetadata)) {
|
|
253
|
-
return;
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
view.dispatch({
|
|
257
|
-
effects: [updateLanguageMetadata.of(newLanguageMetadata)],
|
|
258
|
-
annotations: [loroSyncAnnotation.of(true)],
|
|
259
|
-
});
|
|
260
|
-
};
|
|
261
|
-
|
|
262
|
-
return ViewPlugin.define((view) => {
|
|
263
|
-
let unsubscribeDoc: () => void;
|
|
264
|
-
|
|
265
|
-
// Initialize after a single tick
|
|
266
|
-
Promise.resolve().then(() => {
|
|
267
|
-
// Language type
|
|
268
|
-
const langType = doc.getByPath(`languages/${cellId}`);
|
|
269
|
-
if (!langType) {
|
|
270
|
-
logger.error("no language container found for cell", cellId);
|
|
271
|
-
return;
|
|
272
|
-
}
|
|
273
|
-
invariant(
|
|
274
|
-
langType instanceof LoroText,
|
|
275
|
-
"language type is not a LoroText",
|
|
276
|
-
);
|
|
277
|
-
|
|
278
|
-
// Language metadata
|
|
279
|
-
const langMeta = doc.getByPath(`language_metadata/${cellId}`);
|
|
280
|
-
if (!langMeta) {
|
|
281
|
-
logger.error("no language metadata container found for cell", cellId);
|
|
282
|
-
return;
|
|
283
|
-
}
|
|
284
|
-
invariant(
|
|
285
|
-
langMeta instanceof LoroMap,
|
|
286
|
-
"language metadata is not a LoroMap",
|
|
287
|
-
);
|
|
288
|
-
|
|
289
|
-
// Run once
|
|
290
|
-
syncLanguage(view, langType);
|
|
291
|
-
syncLanguageMetadata(view, langMeta);
|
|
292
|
-
|
|
293
|
-
// Subscribe to language and metadata changes
|
|
294
|
-
unsubscribeDoc = doc.subscribe((event) => {
|
|
295
|
-
if (event.origin === "local") {
|
|
296
|
-
// Skip if the change is local
|
|
297
|
-
return;
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
const hasAnyLanguageTypeChanges = event.events.some(
|
|
301
|
-
(e) => e.target === langType.id,
|
|
302
|
-
);
|
|
303
|
-
const hasAnyLanguageMetadataChanges = event.events.some(
|
|
304
|
-
(e) => e.target === langMeta.id,
|
|
305
|
-
);
|
|
306
|
-
|
|
307
|
-
if (hasAnyLanguageTypeChanges) {
|
|
308
|
-
syncLanguage(view, langType);
|
|
309
|
-
}
|
|
310
|
-
if (hasAnyLanguageMetadataChanges) {
|
|
311
|
-
syncLanguageMetadata(view, langMeta);
|
|
312
|
-
}
|
|
313
|
-
});
|
|
314
|
-
});
|
|
315
|
-
|
|
316
|
-
return {
|
|
317
|
-
destroy() {
|
|
318
|
-
unsubscribeDoc();
|
|
319
|
-
},
|
|
320
|
-
};
|
|
321
|
-
});
|
|
322
|
-
}
|
|
323
|
-
|
|
324
|
-
/**
|
|
325
|
-
* Listen for local language changes
|
|
326
|
-
* This is used to sync the language with the server
|
|
327
|
-
* Local -> Server
|
|
328
|
-
* @returns Extension
|
|
329
|
-
*/
|
|
330
|
-
function languageListenerExtension(cellId: CellId) {
|
|
331
|
-
// Get or create the language type container
|
|
332
|
-
const currentLang = doc
|
|
333
|
-
.getMap("languages")
|
|
334
|
-
.getOrCreateContainer(cellId, new LoroText());
|
|
335
|
-
|
|
336
|
-
// Get or create the language metadata container
|
|
337
|
-
const currentLanguageMetadata = doc
|
|
338
|
-
.getMap("language_metadata")
|
|
339
|
-
.getOrCreateContainer(cellId, new LoroMap());
|
|
340
|
-
|
|
341
|
-
return EditorView.updateListener.of((update) => {
|
|
342
|
-
const isInitialized = currentLang.toString() !== "";
|
|
343
|
-
|
|
344
|
-
// Skip if the doc hasn't changed and the language is already set
|
|
345
|
-
if (!update.docChanged && isInitialized) {
|
|
346
|
-
return;
|
|
347
|
-
}
|
|
348
|
-
|
|
349
|
-
// If the language is not set, set it to the current language
|
|
350
|
-
// and update the LoroDoc
|
|
351
|
-
if (!isInitialized) {
|
|
352
|
-
const language = getInitialLanguageAdapter(update.state).type;
|
|
353
|
-
switchLanguage(update.view, { language });
|
|
354
|
-
logger.debug("no initial language, setting default to", language);
|
|
355
|
-
|
|
356
|
-
// Sync the language to the LoroDoc
|
|
357
|
-
currentLang.delete(0, currentLang.length);
|
|
358
|
-
currentLang.insert(0, language);
|
|
359
|
-
|
|
360
|
-
// Sync the language metadata to the LoroDoc
|
|
361
|
-
logger.debug(
|
|
362
|
-
"no initial language metadata, setting default to",
|
|
363
|
-
update.state.field(languageMetadataField),
|
|
364
|
-
);
|
|
365
|
-
const metadata = update.state.field(languageMetadataField);
|
|
366
|
-
for (const key of Object.keys(metadata)) {
|
|
367
|
-
currentLanguageMetadata.set(key, metadata[key]);
|
|
368
|
-
}
|
|
369
|
-
|
|
370
|
-
// Commit the changes
|
|
371
|
-
doc.commit();
|
|
372
|
-
|
|
373
|
-
return;
|
|
374
|
-
}
|
|
375
|
-
|
|
376
|
-
let hasChanges = false;
|
|
377
|
-
|
|
378
|
-
for (const tr of update.transactions) {
|
|
379
|
-
const isSyncOperation = tr.annotation(loroSyncAnnotation);
|
|
380
|
-
if (isSyncOperation) {
|
|
381
|
-
continue;
|
|
382
|
-
}
|
|
383
|
-
|
|
384
|
-
for (const e of tr.effects) {
|
|
385
|
-
// Language type
|
|
386
|
-
if (
|
|
387
|
-
e.is(setLanguageAdapter) &&
|
|
388
|
-
currentLang.toString() !== e.value.type
|
|
389
|
-
) {
|
|
390
|
-
logger.debug(
|
|
391
|
-
`[outgoing] language change: ${currentLang} -> ${e.value.type}`,
|
|
392
|
-
);
|
|
393
|
-
currentLang.delete(0, currentLang.length);
|
|
394
|
-
currentLang.insert(0, e.value.type);
|
|
395
|
-
hasChanges = true;
|
|
396
|
-
}
|
|
397
|
-
|
|
398
|
-
// Language metadata
|
|
399
|
-
if (e.is(updateLanguageMetadata) || e.is(setLanguageMetadata)) {
|
|
400
|
-
const metadata = e.value;
|
|
401
|
-
|
|
402
|
-
// If it is set, we should clear the metadata first
|
|
403
|
-
if (e.is(setLanguageMetadata)) {
|
|
404
|
-
logger.debug("[outgoing] setting language metadata: ", metadata);
|
|
405
|
-
currentLanguageMetadata.clear();
|
|
406
|
-
} else {
|
|
407
|
-
logger.debug("[outgoing] updating language metadata: ", metadata);
|
|
408
|
-
}
|
|
409
|
-
|
|
410
|
-
for (const key of Object.keys(metadata)) {
|
|
411
|
-
currentLanguageMetadata.set(key, metadata[key]);
|
|
412
|
-
}
|
|
413
|
-
hasChanges = true;
|
|
414
|
-
}
|
|
415
|
-
}
|
|
416
|
-
}
|
|
417
|
-
|
|
418
|
-
if (hasChanges) {
|
|
419
|
-
doc.commit();
|
|
420
|
-
}
|
|
421
|
-
});
|
|
422
|
-
}
|
|
423
|
-
|
|
424
|
-
/**
|
|
425
|
-
* Create a plugin to observe awareness changes
|
|
426
|
-
*/
|
|
427
|
-
function loroAwarenessPlugin({
|
|
428
|
-
doc,
|
|
429
|
-
awareness,
|
|
430
|
-
user,
|
|
431
|
-
getTextFromDoc,
|
|
432
|
-
cellId,
|
|
433
|
-
getUserId,
|
|
434
|
-
}: {
|
|
435
|
-
doc: LoroDoc;
|
|
436
|
-
awareness: Awareness<AwarenessState>;
|
|
437
|
-
user: UserState;
|
|
438
|
-
getTextFromDoc: (doc: LoroDoc) => LoroText;
|
|
439
|
-
cellId: CellId;
|
|
440
|
-
getUserId?: () => Uid;
|
|
441
|
-
}): Extension[] {
|
|
442
|
-
const scopeId = `loro:cell:${cellId}` as ScopeId;
|
|
443
|
-
|
|
444
|
-
return [
|
|
445
|
-
remoteAwarenessStateField,
|
|
446
|
-
createCursorLayer(),
|
|
447
|
-
createSelectionLayer(),
|
|
448
|
-
ViewPlugin.define(
|
|
449
|
-
(view) =>
|
|
450
|
-
new AwarenessPlugin(view, {
|
|
451
|
-
doc,
|
|
452
|
-
user,
|
|
453
|
-
awareness,
|
|
454
|
-
getTextFromDoc,
|
|
455
|
-
scopeId,
|
|
456
|
-
getUserId,
|
|
457
|
-
}),
|
|
458
|
-
),
|
|
459
|
-
ViewPlugin.define(
|
|
460
|
-
(view) => new RemoteAwarenessPlugin(view, doc, awareness, scopeId),
|
|
461
|
-
),
|
|
462
|
-
loroCursorTheme,
|
|
463
|
-
];
|
|
464
|
-
}
|
|
@@ -1,52 +0,0 @@
|
|
|
1
|
-
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
|
-
|
|
3
|
-
import { EditorState } from "@codemirror/state";
|
|
4
|
-
import type { EditorView } from "@codemirror/view";
|
|
5
|
-
import { LoroDoc, LoroText } from "loro-crdt";
|
|
6
|
-
import { describe, expect, it, vi } from "vitest";
|
|
7
|
-
import { LoroSyncPluginValue, loroSyncAnnotation } from "../sync";
|
|
8
|
-
|
|
9
|
-
describe("LoroSyncPluginValue", () => {
|
|
10
|
-
it("annotates the initial reconciliation dispatch as RTC sync", async () => {
|
|
11
|
-
const dispatch = vi.fn();
|
|
12
|
-
const view = {
|
|
13
|
-
state: EditorState.create({ doc: "local" }),
|
|
14
|
-
dispatch,
|
|
15
|
-
} as unknown as EditorView;
|
|
16
|
-
|
|
17
|
-
const doc = new LoroDoc();
|
|
18
|
-
const text = doc
|
|
19
|
-
.getMap("codes")
|
|
20
|
-
.getOrCreateContainer("cell-1", new LoroText());
|
|
21
|
-
text.insert(0, "remote");
|
|
22
|
-
|
|
23
|
-
const plugin = new LoroSyncPluginValue(
|
|
24
|
-
view,
|
|
25
|
-
doc,
|
|
26
|
-
["codes", "cell-1"],
|
|
27
|
-
() => text,
|
|
28
|
-
);
|
|
29
|
-
|
|
30
|
-
await Promise.resolve();
|
|
31
|
-
|
|
32
|
-
expect(dispatch).toHaveBeenCalledTimes(1);
|
|
33
|
-
expect(dispatch).toHaveBeenCalledWith(
|
|
34
|
-
expect.objectContaining({
|
|
35
|
-
changes: [
|
|
36
|
-
{
|
|
37
|
-
from: 0,
|
|
38
|
-
to: view.state.doc.length,
|
|
39
|
-
insert: "remote",
|
|
40
|
-
},
|
|
41
|
-
],
|
|
42
|
-
annotations: [
|
|
43
|
-
expect.objectContaining({
|
|
44
|
-
type: loroSyncAnnotation,
|
|
45
|
-
}),
|
|
46
|
-
],
|
|
47
|
-
}),
|
|
48
|
-
);
|
|
49
|
-
|
|
50
|
-
plugin.destroy();
|
|
51
|
-
});
|
|
52
|
-
});
|