Skip to content

feat: API for retained WASM island lifecycle#10207

Merged
mscolnick merged 12 commits into
marimo-team:mainfrom
peter-gy:ptr/retained-island-lifecycle
Jul 17, 2026
Merged

feat: API for retained WASM island lifecycle#10207
mscolnick merged 12 commits into
marimo-team:mainfrom
peter-gy:ptr/retained-island-lifecycle

Conversation

@peter-gy

@peter-gy peter-gy commented Jul 17, 2026

Copy link
Copy Markdown
Collaborator

📝 Summary

Adds a retained-runtime lifecycle for static sites that navigate between pages containing marimo islands. Page transitions can replace the active Python app while keeping the worker and Pyodide environment ready.

The islands entry module exports:

  • canReplaceApp() returns whether the current document contains one replaceable app
  • stopApp(appId?) stops the matching active app session
  • initialize() starts, reuses, or replaces the app in the current document

Hosts can call these methods around client-side navigation:

await stopApp()
updatePage()
await initialize()

App transitions are serialized and scoped by app ID and session generation. Rapid or failed transitions preserve session ownership, prevent stale controls from reaching another app, and allow teardown or replacement to be retried.

Worker startup creates and tracks the Python resources for each session. Package loading is serialized, superseded sessions skip initialization, and successful teardown releases the session task and Python references while retaining Pyodide.

Also, island elements now support page content that is upgraded or reused during navigation. They bind after compiled cell IDs become available, refresh when an existing cell ID receives new source, and release static staging output after mounting.

peter-gy added 6 commits July 17, 2026 07:49
Allow callers to inspect island apps without materializing payloads. Retain payload identities and rendered source so repeated initialization reconstructs the same app after custom elements take ownership of their children.
Own the Python session task and stop callable so an app can be cancelled and collected while Pyodide remains active. Serialize dependency loading and skip initialization work from superseded sessions.
Export canReplaceApp(), initialize(), and stopApp() for hosts that preserve the browser realm across page navigation. Reuse the global worker while serializing app replacement and scoping worker requests by app ID and session generation so stale work cannot reach the active app.
Copilot AI review requested due to automatic review settings July 17, 2026 09:34
@peter-gy peter-gy added the internal A refactor or improvement that is not user facing label Jul 17, 2026
@vercel

vercel Bot commented Jul 17, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
marimo-docs Ready Ready Preview, Comment Jul 17, 2026 3:26pm

Request Review

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@peter-gy peter-gy changed the title feat: retained WASM island lifecycle feat: API for retained WASM island lifecycle Jul 17, 2026

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All reported issues were addressed across 10 files

Architecture diagram
sequenceDiagram
    participant Host as Host Page
    participant Bridge as IslandsPyodideBridge
    participant Worker as Worker Thread
    participant Controller as WasmController
    participant Pyodide as Pyodide Runtime
    participant DOM as DOM / Custom Elements

    Note over Host,DOM: NEW: Retained Island Lifecycle Flow

    Host->>Bridge: canReplaceApp()
    Bridge->>DOM: query islands & parse (materialize: false)
    DOM-->>Bridge: apps list
    alt exactly one app
        Bridge-->>Host: true
    else
        Bridge-->>Host: false
    end

    Host->>Bridge: initialize()
    Bridge->>Bridge: enqueueAppTransition()
    Bridge->>Worker: wait for workerReady
    Worker-->>Bridge: ready
    Bridge->>DOM: parseMarimoIslandApps()
    DOM-->>Bridge: apps with cells

    alt no previous session
        Bridge->>Bridge: generate new sessionGeneration, set session
        Bridge->>Worker: startSession({appId, code, sessionGeneration})
        Worker->>Controller: mountFilesystem & startSession
        Controller->>Pyodide: runPython (create session resources)
        Pyodide-->>Controller: bridge, init, packages, stop
        Controller->>Controller: loadNotebookDeps (serialized on packageLoadQueue)
        Controller-->>Worker: resolved bridge
        Worker-->>Bridge: session started
        Bridge->>Bridge: resolve sessionReady

    else previous session exists and code unchanged
        Bridge->>Bridge: skip (duplicate)
    else previous session exists and code changed (replace)
        Bridge->>Bridge: new sessionGeneration, create deferred sessionReady
        Bridge->>Worker: replaceSession({appId, code, sessionGeneration})
        Worker->>Worker: enqueueSession -> stopActiveSession()
        Worker->>Controller: stopSession()
        Controller->>Controller: increment sessionGeneration, destroy stopCurrentSession
        Controller->>Pyodide: stop() -> cancel kernel, gc
        Pyodide-->>Controller: done
        Controller-->>Worker: stopped
        Worker->>Controller: startSession (as above)
        Controller-->>Worker: new bridge
        Worker-->>Bridge: session replaced
        Bridge->>Bridge: resolve sessionReady (if pending)
    end

    Note over Host,Bridge: Stopping an app

    Host->>Bridge: stopApp(appId)
    Bridge->>Bridge: enqueueAppTransition
    Bridge->>Worker: stopSession({appId, sessionGeneration})
    Worker->>Worker: enqueueSession -> check activeSession matches
    Worker->>Controller: stopSession()
    Controller->>Pyodide: stop() + destroy resources
    Pyodide-->>Controller: done
    Controller-->>Worker: stopped
    Worker-->>Bridge: stopped
    Bridge->>Bridge: clear session

    Note over Host,DOM: Control requests scoped to session

    Host->>Bridge: sendComponentValues (ui-element update)
    Bridge->>Bridge: getActiveSession() -> wait for appTransition + sessionReady
    Bridge->>Worker: bridge({appId, sessionGeneration, functionName: "put_control_request", payload})
    Worker->>Worker: enqueueSession -> requireActiveBridge() validates generation
    Worker->>Pyodide: call bridge[functionName](payload)
    Pyodide-->>Worker: response
    Worker-->>Bridge: response
    Bridge-->>Host: result

    Note over Host,DOM: Replacement failure fallback

    alt replaceSession fails (error)
        Bridge->>Bridge: catch error, clear session if generation matches
        Bridge->>Bridge: keep previous session (fallback)
        Bridge-->>Host: throw error
        Host->>Bridge: initialize() retry
        Bridge->>Bridge: startApps -> sees no session, calls startSession (not replace)
        Bridge->>Worker: startSession (fresh start)
    end
Loading

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

Comment thread frontend/src/core/islands/main.ts
Comment thread frontend/src/core/islands/worker/worker.tsx Outdated
Comment thread frontend/src/core/wasm/worker/bootstrap.ts Outdated
Comment thread frontend/src/core/islands/bridge.ts Outdated
Comment thread frontend/src/core/islands/parse.ts Outdated
Comment thread frontend/src/core/islands/components/web-components.tsx

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All reported issues were addressed across 12 files (changes from recent commits).

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

Comment thread frontend/src/core/wasm/worker/bootstrap.ts Outdated
* Cleanup when element is removed from DOM
*/
disconnectedCallback(): void {
this.connectionGeneration += 1;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

are we incrementing on connection and disconnection?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the idea was for connectedCallback() to start a new lifecycle and disconnectedCallback() to explicitly invalidate its pending work, so every lifecycle transition advances the epoch. looking at it again it is probably overkill though because isConnected and the next connection generation already cover the same race.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i think thats actually fine and makes total sense now that i think about it

import { shouldLoadDuckDBPackages } from "../utils";

const MAKE_SNAPSHOT = false;
type SessionResources = [PyProxy, PyCallable, PyProxy, PyCallable];

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: could do a typed tuple here [one: PyProxy, two: PyCallable, three: PyProxy, four: PyCallable]

@mscolnick mscolnick left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

looks great! minor comments

@mscolnick mscolnick left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ship!

@mscolnick

mscolnick commented Jul 17, 2026

Copy link
Copy Markdown
Contributor

oh one last idea, could we add a test that validates the exports and possible the typedef API and snapshots it? since now its a public API that cannot break.

can do in a followup

@peter-gy

Copy link
Copy Markdown
Collaborator Author

oh one last idea, could we add a test that validates the exports and possible the typedef API and snapshots it? since now its a public API that cannot break.

can do in a followup

Oh yes I'll do it in a next pr

@mscolnick
mscolnick merged commit f37839f into marimo-team:main Jul 17, 2026
27 checks passed
@github-actions

Copy link
Copy Markdown
Contributor

🚀 Development release published. You may be able to view the changes at https://marimo.app?v=0.23.15-dev32

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

api-change internal A refactor or improvement that is not user facing

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants