@marimo-team/islands 0.25.1-dev2 → 0.25.1-dev4

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 (21) hide show
  1. package/dist/{ErrorBoundary-DO0ie20N.js → ErrorBoundary-pV36lYEK.js} +1 -1
  2. package/dist/{cells-DYIkSxs6.js → cells-B5-y5EY3.js} +4121 -4148
  3. package/dist/{chat-ui-BXj3uYjy.js → chat-ui-HJZsTl7o.js} +45 -45
  4. package/dist/{code-visibility-64_ACQvC.js → code-visibility-CgZS2a3W.js} +17 -17
  5. package/dist/{common-LlPE7GR6.js → common-DnMDPPaW.js} +611 -611
  6. package/dist/{data-grid-overlay-editor-BUvmnptP.js → data-grid-overlay-editor-hHYgD_Sm.js} +1 -1
  7. package/dist/{error-entries-C0NsUqXl.js → error-entries-Bkbpx0T0.js} +18 -18
  8. package/dist/{feedback-button-B98mEc3M.js → feedback-button-CvKssdG_.js} +19 -19
  9. package/dist/{formats-BR5iGb4s.js → formats-C8rLgZT9.js} +1 -1
  10. package/dist/{glide-data-editor-CBIPbkXG.js → glide-data-editor-Ohetlgag.js} +3 -3
  11. package/dist/{globals-VpAtGRbg.js → globals-CPNNcOJI.js} +1 -1
  12. package/dist/{html-to-image-CeYYvQNb.js → html-to-image-BAgJOlSW.js} +31 -30
  13. package/dist/main.js +581 -581
  14. package/dist/{number-overlay-editor-ClZmv3Qw.js → number-overlay-editor-CHWg5gGO.js} +1 -1
  15. package/dist/{process-output-BCTjlyuH.js → process-output-zDX4jbhd.js} +2 -2
  16. package/dist/{reveal-component-C1s_K6O8.js → reveal-component-BujcZI2B.js} +744 -743
  17. package/dist/{toDate-WXg1yLcI.js → toDate-C67bxMWn.js} +139 -111
  18. package/dist/{vega-component-D0QXopU8.js → vega-component-CNf5gcrm.js} +9 -9
  19. package/package.json +1 -1
  20. package/src/core/kernel/__tests__/session.test.ts +110 -1
  21. package/src/core/kernel/session.ts +46 -15
@@ -1,10 +1,119 @@
1
1
  /* Copyright 2026 Marimo. All rights reserved. */
2
- import { describe, expect, it } from "vitest";
2
+ import { afterEach, describe, expect, it, vi } from "vitest";
3
+ import type { AppMode } from "@/core/mode";
3
4
  import { generateSessionId, isSessionId } from "../session";
4
5
 
6
+ const URL_SESSION_ID = "s_abc123";
7
+
8
+ /**
9
+ * Load a fresh copy of the session module against the given URL and mode.
10
+ * The module caches its session id, so each case needs its own instance.
11
+ */
12
+ async function loadSession(opts: { mode: AppMode | undefined; url: string }) {
13
+ vi.resetModules();
14
+ window.history.replaceState({}, "", opts.url);
15
+ const { store } = await import("@/core/state/jotai");
16
+ const { initialModeAtom } = await import("@/core/mode");
17
+ store.set(initialModeAtom, opts.mode);
18
+ return await import("../session");
19
+ }
20
+
21
+ function currentSearchParams(): URLSearchParams {
22
+ return new URL(window.location.href).searchParams;
23
+ }
24
+
5
25
  describe("Session", () => {
26
+ afterEach(() => {
27
+ window.history.replaceState({}, "", "/");
28
+ });
29
+
6
30
  it("should create a session", () => {
7
31
  const id = generateSessionId();
8
32
  expect(isSessionId(id)).toBe(true);
9
33
  });
34
+
35
+ it("resumes the session named in the URL in edit mode", async () => {
36
+ const { getSessionId } = await loadSession({
37
+ mode: "edit",
38
+ url: `/?session_id=${URL_SESSION_ID}`,
39
+ });
40
+ expect(getSessionId()).toBe(URL_SESSION_ID);
41
+ expect(currentSearchParams().has("session_id")).toBe(false);
42
+ });
43
+
44
+ it("keeps the URL session id in kiosk mode so a refresh resumes it", async () => {
45
+ const { getSessionId } = await loadSession({
46
+ mode: "edit",
47
+ url: `/?kiosk=true&session_id=${URL_SESSION_ID}`,
48
+ });
49
+ expect(getSessionId()).toBe(URL_SESSION_ID);
50
+ expect(currentSearchParams().get("session_id")).toBe(URL_SESSION_ID);
51
+ expect(currentSearchParams().get("kiosk")).toBe("true");
52
+ });
53
+
54
+ it("keeps the kiosk flag in edit mode when the URL names no session", async () => {
55
+ const { getSessionId } = await loadSession({
56
+ mode: "edit",
57
+ url: "/?kiosk=true",
58
+ });
59
+ expect(isSessionId(getSessionId())).toBe(true);
60
+ expect(currentSearchParams().get("kiosk")).toBe("true");
61
+ });
62
+
63
+ it("starts a new session when the URL session id is malformed", async () => {
64
+ const { getSessionId } = await loadSession({
65
+ mode: "edit",
66
+ url: "/?session_id=not-a-session",
67
+ });
68
+ const id = getSessionId();
69
+ expect(isSessionId(id)).toBe(true);
70
+ expect(id).not.toBe("not-a-session");
71
+ });
72
+
73
+ it("ignores the URL session id in read mode", async () => {
74
+ const { getSessionId } = await loadSession({
75
+ mode: "read",
76
+ url: `/?session_id=${URL_SESSION_ID}`,
77
+ });
78
+ const id = getSessionId();
79
+ expect(isSessionId(id)).toBe(true);
80
+ expect(id).not.toBe(URL_SESSION_ID);
81
+ // The param is still scrubbed so a refresh cannot reintroduce it.
82
+ expect(currentSearchParams().has("session_id")).toBe(false);
83
+ });
84
+
85
+ it("drops both the session id and the kiosk flag in read mode", async () => {
86
+ const { getSessionId } = await loadSession({
87
+ mode: "read",
88
+ url: `/?kiosk=true&session_id=${URL_SESSION_ID}`,
89
+ });
90
+ expect(getSessionId()).not.toBe(URL_SESSION_ID);
91
+ expect(currentSearchParams().has("session_id")).toBe(false);
92
+ // The kiosk flag is forwarded to the websocket URL, and the server
93
+ // refuses kiosk connections to an app, so it must not survive here.
94
+ expect(currentSearchParams().has("kiosk")).toBe(false);
95
+ });
96
+
97
+ it("drops a lone kiosk flag in read mode", async () => {
98
+ const { getSessionId } = await loadSession({
99
+ mode: "read",
100
+ url: "/?kiosk=true",
101
+ });
102
+ expect(isSessionId(getSessionId())).toBe(true);
103
+ expect(currentSearchParams().has("kiosk")).toBe(false);
104
+ });
105
+
106
+ it("ignores the URL session id when the mode is not yet known", async () => {
107
+ const { getSessionId } = await loadSession({
108
+ mode: undefined,
109
+ url: `/?session_id=${URL_SESSION_ID}`,
110
+ });
111
+ expect(getSessionId()).not.toBe(URL_SESSION_ID);
112
+ expect(currentSearchParams().has("session_id")).toBe(false);
113
+ });
114
+
115
+ it("returns the same id on every call", async () => {
116
+ const { getSessionId } = await loadSession({ mode: "read", url: "/" });
117
+ expect(getSessionId()).toBe(getSessionId());
118
+ });
10
119
  });
@@ -4,6 +4,8 @@ import { Logger } from "@/utils/Logger";
4
4
  import type { TypedString } from "@/utils/typed";
5
5
  import { updateQueryParams } from "@/utils/urls";
6
6
  import { KnownQueryParams } from "../constants";
7
+ import { initialModeAtom } from "../mode";
8
+ import { store } from "../state/jotai";
7
9
 
8
10
  export type SessionId = TypedString<"SessionId">;
9
11
 
@@ -20,31 +22,60 @@ export function isSessionId(value: string | null): value is SessionId {
20
22
  return /^s_[\da-z]{6}$/.test(value);
21
23
  }
22
24
 
23
- const sessionId = (() => {
25
+ let sessionId: SessionId | null = null;
26
+
27
+ function resolveSessionId(): SessionId {
28
+ // Only the editor lets the URL steer the connection: kiosk mode and
29
+ // external editors reattach to a running session through `session_id`.
30
+ // An app served with `marimo run` never does. There, the session id is the
31
+ // only thing that routes a browser to its kernel, so a link that picked it
32
+ // would let the link's author attach to the reader's session afterwards,
33
+ // and a stray `kiosk` flag would make the server refuse the connection.
34
+ const urlMaySteerConnection = store.get(initialModeAtom) === "edit";
35
+
24
36
  const url = new URL(window.location.href);
25
37
  const id = url.searchParams.get(
26
38
  KnownQueryParams.sessionId,
27
39
  ) as SessionId | null;
28
- if (isSessionId(id)) {
29
- // Remove the session_id from the URL
30
- updateQueryParams((params) => {
31
- // Keep the session_id if we are in kiosk mode
32
- // this is so we can resume the same session if the user refreshes the page
33
- if (params.has(KnownQueryParams.kiosk)) {
34
- return;
35
- }
40
+ const urlSessionId = isSessionId(id) ? id : null;
41
+
42
+ updateQueryParams((params) => {
43
+ if (!urlMaySteerConnection) {
36
44
  params.delete(KnownQueryParams.sessionId);
45
+ params.delete(KnownQueryParams.kiosk);
46
+ return;
47
+ }
48
+ // Keep the session_id in kiosk mode so that a refresh resumes the same
49
+ // session.
50
+ if (params.has(KnownQueryParams.kiosk)) {
51
+ return;
52
+ }
53
+ params.delete(KnownQueryParams.sessionId);
54
+ });
55
+
56
+ if (urlSessionId === null) {
57
+ Logger.debug("Starting a new session", { sessionId: id });
58
+ return generateSessionId();
59
+ }
60
+ if (!urlMaySteerConnection) {
61
+ Logger.debug("Ignoring session_id from URL outside the editor", {
62
+ sessionId: urlSessionId,
37
63
  });
38
- Logger.debug("Connecting to existing session", { sessionId: id });
39
- return id;
64
+ return generateSessionId();
40
65
  }
41
- Logger.debug("Starting a new session", { sessionId: id });
42
- return generateSessionId();
43
- })();
66
+ Logger.debug("Connecting to existing session", { sessionId: urlSessionId });
67
+ return urlSessionId;
68
+ }
44
69
 
45
70
  /**
46
- * Resume an existing session or start a new one
71
+ * Resume an existing session or start a new one.
72
+ *
73
+ * Resolved on first use rather than at import time, because the URL is only
74
+ * trusted once the app mode is known.
47
75
  */
48
76
  export function getSessionId(): SessionId {
77
+ if (sessionId === null) {
78
+ sessionId = resolveSessionId();
79
+ }
49
80
  return sessionId;
50
81
  }