Skip to content

Multiple projects storage foundation - #1327

Open
microbit-matt-hillsdon wants to merge 6 commits into
home-pagefrom
storage-foundation
Open

Multiple projects storage foundation#1327
microbit-matt-hillsdon wants to merge 6 commits into
home-pagefrom
storage-foundation

Conversation

@microbit-matt-hillsdon

@microbit-matt-hillsdon microbit-matt-hillsdon commented Sep 11, 2026

Copy link
Copy Markdown
Collaborator

Note: targets new home-page integration branch as part of a multi-step plan.

The file system is now mirrored to IndexedDB instead of session storage.

  • projects-db.ts: two stores, project metadata and files keyed by [projectId, name], so listing is cheap and a change writes only the files it touched. Opening asserts the stores exist and throws VersionError otherwise.
  • indexeddb-storage.ts: an FSStorage for one project, used as the SplitStrategyStorage secondary. Keystroke writes coalesce per file and flush as one transaction after 300ms and when the page is hidden. A failed flush is reported, not thrown, and its changes dropped: the in-memory primary still has them and retrying against a full quota helps nobody.
  • current-project.ts: which project a tab opens. The current id lives in session storage, so a project is still per tab. A session-storage project is migrated on first load so a reload after deploy lands in the user's work; only the fs/ keys are removed, since session settings share that storage. Without IndexedDB, or with an incompatible database on a public stage, the editor falls back to session storage as before.
  • SplitStrategyStorage accepts a promise for its secondary, since opening IndexedDB is asynchronous and the Host API is not.

Production and beta get separate databases by base path (python-editor/v/3, python-editor/v/beta). Review builds share python-editor-review: they are internal and a project from one branch is useful on the next. If/when we hit a schema change, non-public stages show a "Breaking change to stored data" page with clear-and-reload, which is useful during development.

Adds FileSystem.switchStorage. Opening a project will swap the file system's backing storage rather than replace files one by one. Waits for any in-flight initialise, makes the new storage the record, refills the hex file system, bumps every file's version so a same-named file reloads in the editor, and notifies. Not yet called outside tests.

User-visible behaviour on this branch

The difference is that a project now survives the tab closing. Nothing lists the projects yet. This is obviously weird hence the integration branch, but a step in the right direction!

Known issues, to be resolved by later plan steps

  • Every new tab creates a project, so the library accumulates "Untitled" projects until we deal with reopening.
  • A #project: link (microbit.org "open in Python Editor") still replaces the current project's files, as it does today. Later work will make it create a new project.
  • Two tabs on the same project do not sync; last flush wins.
  • No quota toast yet: a failed flush is logged through Logging.error. The toast comes with the pages.

Groundwork for a project library: opening a project will swap the
FileSystem's backing storage rather than replace files one by one.

switchStorage waits for any in-flight initialisation, makes the new
storage the record, refills the hex file system from it and bumps the
version of every file it holds so an editor showing a same-named file
reloads it. Versions increment rather than reset because the editor
keys on name plus version; a reset to 1 could collide with the old
project's version and leave stale content on screen.
The editor's file system is now mirrored to IndexedDB rather than
session storage, as the first step towards a library of projects.
Behaviour is unchanged for the user: one project per tab, and a new tab
gets a new project.

- projects-db.ts: the library. Two stores, project metadata and files
  keyed by [projectId, name], so listing is cheap and a change writes
  only the files it touched. The database name includes the base path
  because production, beta and review builds share an origin. Opening
  asserts the stores exist so an old build fails clearly against a
  database a newer one created.
- indexeddb-storage.ts: an FSStorage for one project, used as the
  SplitStrategyStorage secondary. Keystroke writes are coalesced per file
  and flushed in one transaction after a short delay and when the page
  is hidden. A failed flush is reported, not thrown, and its changes
  dropped: the in-memory primary still has them and retrying forever
  against a full quota helps nobody.
- current-project.ts: which project a tab opens. The tab's current
  project id lives in session storage. A session-storage file system
  from before this change is migrated into the library on first load,
  so a reload after deploy lands in the user's work; only the file
  system keys are removed, since session settings live there too.
  Without IndexedDB, or with an incompatible database, the editor falls
  back to session storage as before.
- SplitStrategyStorage accepts a promise of its secondary, since opening
  IndexedDB is asynchronous and the Host API is not.

The shared FSStorage tests move to storage-tests.ts so the new storage
runs them too, against fake-indexeddb.
Review builds are internal, and a project made on one branch is useful
on the next, so they share a database rather than each having their own
by base path. Production and beta keep separate libraries.

The cost is that a schema change can leave the shared library
incompatible with an older build. On non-public stages that now shows a
"Breaking change to stored data" page offering to clear the library and
reload, as ml-trainer does, instead of quietly falling back to session
storage. Public stages keep the quiet fallback. The outcome of opening
the library is held in a small external store, since storage opens at
module load before React mounts.
@github-actions

Copy link
Copy Markdown

…prompt

The dirty flag means "changed since the last hex save" and exists only to
warn before work is lost: the before-unload prompt and the replace-project
confirmation are its only readers. Neither applies to a project in the
database, which outlives the tab, so the flag is no longer stored there and
IndexedDBFSStorage always reports not dirty. Session storage keeps it and
FileSystem still tracks it for the tab, so the replace confirmation works for
edits made in this tab until the replace flows go.

openCurrentProjectStorage now reports through storage-status when the
projects database is active, and BeforeUnloadDirtyCheck registers nothing in
that case. The session-storage fallback and iframe mode keep the prompt.

The edits e2e spec now asserts the prompt is absent and that text typed just
before a reload survives via the pagehide flush. The old helper proved
nothing: Playwright accepts an unlistened beforeunload dialog itself, so the
new one listens for it.
…er mode

The three storage modes now behave differently and only the projects
database path was exercised in the browser.

storage-errors.test.ts uses a new noIndexedDB fixture option, an init script
that hides indexedDB before the app loads, and checks the fallback keeps the
before-unload prompt and survives a reload.

iframe.test.ts embeds the editor with controller=1 and checks the
workspacesync, workspaceloaded, workspacesave and importproject messages and
the prompt. The host page is an HTML string in the spec, served by
intercepting a request for it, so it is never part of the build.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant