feat(playground): a guided tutorial that runs EF Core + SQLite in the browser - #643
Merged
Merged
Conversation
… the browser The playground's left pane gains a Tutorial tab beside the example gallery: eight chapters from "what is a component" to a working database, each with its goal and notes above the editor, prev/next navigation, and a tick once it compiles. Chapters 5-8 run real EF Core against real SQLite inside the tab -- e_sqlite3 is linked into the published WebAssembly runtime, so SaveChangesAsync writes rows a later Where(...) reads back through actual SQL. They teach the same Rask.Data conventions rask generate feature scaffolds (Entity<Guid>, ApplyRaskConventions, the auditing/soft-delete interceptors), so what a reader learns in the browser is what they write on their machine. Each chapter owns its own database file: chapters evolve the schema and EnsureCreated() does nothing to a database that already has tables. Pooling is off in the chapter connection strings because a chapter may recreate its database between runs and a pooled connection keeps serving the deleted file. The EF Core reference set is gated on RaskPlaygroundData, which follows the existing WasmBuildNative guard in the csproj: the fast no-native build ships without it and marks those chapters read-only (Run disabled) rather than pretending they work, so the unit gate stays fast and workload-free. The brief band is always rendered, empty on the gallery tab, to keep the Monaco host at a fixed child slot -- rendering it conditionally would shift the host's position and let the positional diff rewrite the live editor. Keying the children instead does not work: a keyed host is re-created by the full-document morph, orphaning the editor Monaco mounted into.
A component normally enters the tree through its generated factory, and that factory's GetOrCreate is what registers the instance with its parent. One built another way -- because its type is not known until runtime: a plugin, a component chosen by name, one compiled in the browser -- arrives as a plain object. It rendered correctly but was invisible to the alive-set walk: no OnMount, no OnMountAsync, no OnRendered, no OnUnmount, and no handle to re-render through when an async hook completed. The failure is silent and looks exactly like code that does not work: a component that loads its data in OnMountAsync sits on its placeholder forever. The playground mounts every compiled component this way, so until now no playground snippet could load anything in OnMountAsync -- which is how this was found, when the tutorial's EnsureCreatedAsync never ran and the later insert reported 'no such table'. Mount(Child: instance) adopts and notifies it, exactly as the framework's own wrapper roots do for theirs, and adds no markup of its own. Wrapping a factory-built child is a harmless no-op.
…ility The section said browser SQLite isn't viable. It is -- this branch ships it -- so the caveat now names the two real constraints (untrimmed, NoWarn=WASM0001) and limits the warning to what actually still bites: durability. Durable client-side SQLite over OPFS is being designed separately, so this leaves that rewrite to it rather than half-doing it here.
This was referenced Aug 8, 2026
pal-tamas
added a commit
that referenced
this pull request
Aug 8, 2026
…its own publish (#652) Four fixes, all found while verifying the deployed playground after #643. 1. Picking a chapter before the editor mounted silently kept the starter code. Run and Reset waited for the editor; the controls that LOAD code did not, so loading a chapter was a no-op against an editor that did not exist yet and mountEditor then installed the starter over the selection. The brief and the highlight showed one chapter while the editor held another, and Run compiled the wrong code and ticked the chapter off. Every control now shares one condition, because the bug was two copies of it disagreeing. Closes #647. 2. The E2E gate was broken by #643 itself. It publishes the playground with the native relink while the solution build compiles it no-native through the same obj/, so the scoped-asset bake never reaches the publish: wwwroot/_rask is absent, the page 404s on the PlaygroundView.js that owns mountEditor, and every journey dies waiting on a permanently disabled Run button. It reads as a hang, names no cause, and a single clean run passes -- which is what hid it. Fixed by clearing that project's TFM intermediates first, verified by running the gate twice back to back. 3. A missing editor module now says so. The mount is an interop call that never SETTLES when its module is absent, which is not the same as failing, so the textarea fallback never got a chance. It now has a deadline and reports the module as missing rather than looking hung. 4. The pre-commit gate now covers samples/ and docs/. Its filter listed src/, tests/, benchmarks/ and the build files, so a samples-only or docs-only commit reported "no code changes staged" and skipped format and the unit suite -- despite Rask.Example.Shared.Tests compiling the sample and owning a committed markup golden, and DocsIndexTests/GuidesTests reading docs/**/*.md off disk for reachability and catalog parity.
pal-tamas
added a commit
that referenced
this pull request
Aug 8, 2026
rask generate job is not gated on project kind — it runs wherever a .csproj is found — but it printed one set of next steps for everyone: point a DbContextFactory at Data Source=app.db, then run `rask db add && rask db update`. In a WASM app every line of that is wrong. `rask db` wraps dotnet-ef against a design-time database and a browser bundle has no migrations assembly, so it is not a step the reader can take. The registration omits AddRaskBrowserSqlite, without which the database lives in the runtime's in-memory filesystem. And the failure is silent rather than loud: the app builds, runs, and quietly loses every queued job on reload. ProjectContext now detects a browser project the same way it already detects the database provider — by reading the project file it has already opened — and JobGenerator branches on it. The browser notes register AddRaskBrowserSqlite first, create the schema at boot rather than through rask db, and name the two build settings that each break the app without producing an error: publishing with -p:WasmBuildNative=false (no relink, so no SQLite) and PublishTrimmed=true (EF Core dies in the trimmer). It also adds Rask.SQLite.Browser to the packages it installs. Docs follow the same correction. docs/jobs.md said jobs were "not a browser/WASM concern"; they now are, so it shows the registration and is explicit about what the guarantees are worth there — the lease elects one TAB, durability is bounded by the snapshot interval rather than by SaveChangesAsync, and pagehide is best-effort so lease expiry is usually what recovers a batch. docs/sqlite.md keeps #643's correction that browser SQLite runs, and replaces the paragraph saying durability is still unsolved — it is solved and merged. The OPFS paragraph is deliberately narrow: OPFS makes the FLUSH incremental (ranged writes), it does not move the live database out of MEMFS, because that needs createSyncAccessHandle, which exists only inside a Worker while Rask boots the runtime on the main thread. Scope confirmed with the #642 session rather than guessed.
pal-tamas
added a commit
that referenced
this pull request
Aug 8, 2026
rask generate job is not gated on project kind — it runs wherever a .csproj is found — but it printed one set of next steps for everyone: point a DbContextFactory at Data Source=app.db, then run `rask db add && rask db update`. In a WASM app every line of that is wrong. `rask db` wraps dotnet-ef against a design-time database and a browser bundle has no migrations assembly, so it is not a step the reader can take. The registration omits AddRaskBrowserSqlite, without which the database lives in the runtime's in-memory filesystem. And the failure is silent rather than loud: the app builds, runs, and quietly loses every queued job on reload. ProjectContext now detects a browser project the same way it already detects the database provider — by reading the project file it has already opened — and JobGenerator branches on it. The browser notes register AddRaskBrowserSqlite first, create the schema at boot rather than through rask db, and name the two build settings that each break the app without producing an error: publishing with -p:WasmBuildNative=false (no relink, so no SQLite) and PublishTrimmed=true (EF Core dies in the trimmer). It also adds Rask.SQLite.Browser to the packages it installs. Docs follow the same correction. docs/jobs.md said jobs were "not a browser/WASM concern"; they now are, so it shows the registration and is explicit about what the guarantees are worth there — the lease elects one TAB, durability is bounded by the snapshot interval rather than by SaveChangesAsync, and pagehide is best-effort so lease expiry is usually what recovers a batch. docs/sqlite.md keeps #643's correction that browser SQLite runs, and replaces the paragraph saying durability is still unsolved — it is solved and merged. The OPFS paragraph is deliberately narrow: OPFS makes the FLUSH incremental (ranged writes), it does not move the live database out of MEMFS, because that needs createSyncAccessHandle, which exists only inside a Worker while Rask boots the runtime on the main thread. Scope confirmed with the #642 session rather than guessed.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds a guided tutorial to the playground, whose last four chapters run real EF Core against real SQLite inside the browser tab. A reader can now go from the front page to "I wrote an entity, saved a row and queried it back" with nothing installed — then hand off to
docs/tutorialfor the parts that need a machine (rask new, migrations, jobs, mail, deploy).The left pane gains a Tutorial tab beside the example gallery: eight chapters, each with its goal and what to notice above the editor, prev/next navigation, and a tick once the chapter compiles (your edits included — the tick means it built, not you clicked).
Render(), tag factories, the children indexerKey:Form<T>, two-wayInput(() => model.Field),Validate:Entity<Guid>, aDbContext,EnsureCreated(), an insertDbSet, translated to SQLInclude()Chapters 5–8 teach the same
Rask.Dataconventionsrask generate featurescaffolds —Entity<Guid>,ApplyRaskConventions(), the auditing/soft-delete interceptors — so what a reader learns in the browser is what they write on their machine.The framework bug this uncovered
OnMountAsyncnever ran for any playground snippet, and never had. A component normally enters the tree through its generated factory, and that factory'sGetOrCreateis what registers the instance with its parent. The playground builds each compiled component withActivatorUtilities, so it arrived as a plain object: it rendered, but was invisible to the alive-set walk — noOnMount, noOnMountAsync, noOnRendered, noOnUnmount, and no handle to re-render through when an async hook completed.The failure is silent and looks exactly like code that doesn't work. Here it surfaced as chapter 5's
EnsureCreatedAsyncnever running, then the insert reportingno such table: Products— I chased it as a SQLite problem before an io-only probe (no EF Core at all) rendered nothing either.New public
MountinRask.Corefixes it:Div()[Mount(Child: instance)]adopts and notifies the instance exactly as the framework's own wrapper roots do for theirs, and adds no markup of its own. Wrapping a factory-built child is a harmless no-op.Rask.Testingalready solved this internally forRaskTest.Render(#583); this is the same fix for app code.Build
SQLite in WASM means linking
e_sqlite3(a static archive) into the runtime — a native relink, so the playground now publishes withwasm-tools. That is gated so the fast paths are untouched:RaskPlaygroundDatafollows the csproj's existingWasmBuildNativeguard (asInvariantGlobalizationandRunAOTCompilationalready do). The fast no-native build ships without the EF Core reference set and marks chapters 5–8 read-only with Run disabled — rather than pretending they work and filling the preview withCS0246.scripts/run-unit-local.shand the slnx build in the E2E gate are unchanged — still fast, still workload-free.pages.ymlalready installswasm-toolsand publishes without the flag, so the deployed site gets the full track with no workflow change.run-e2e-local.shdrops-p:WasmBuildNative=false, and also drops--no-restore:RaskPlaygroundDatachanges the package graph, and MSBuild silently ignores aPackageReferencethat appeared after restore — the bundle would have definedRASK_PLAYGROUND_DATAwhile shipping no EF Core at all.NoWarn=WASM0001for SQLite's varargs natives (sqlite3_config), which warnings-as-errors would otherwise turn into a failed publish.Testing
TutorialChaptersTests— every chapter compiles through the realPlaygroundCompiler, and the data chapters actually reach SQLite: seed, save, query back, with the entity chapter asserting an insert through a real click. Also pins thech{N}.dbconvention thatResetderives its file name from, and that pooling stays off.MountTests— pins both directions: an unadopted instance renders but never mounts (the bug), andMountruns the sync and async lifecycle including the re-render on completion.TutorialPaneStateTests— keeps the E2E's DOM hooks and the markup in step, in the fast gate rather than as a browser timeout (theIdeBadgeStatepattern from The browser E2E gate cannot pass on main: the playground test waits for a class #470 deleted #593).PlaygroundExampleTests— the acceptance test, and the only thing that can prove this at all: chapter 5 creates the database, inserts through the compiled component's own handler and renders the row; Next → chapter 6 seeds and queriesCold brewback. Passing against the native publish.dotnet format+ warnings-as-errors build + full unit suite green.Also verified in a real browser via a scripted Playwright pass: the editor survives tab switches (
hosts=1,monacoAlive=true, brief never inside the host), the tab loads the code the pane highlights in both directions, and on a no-data build chapter 5 rendersis-lockedwith Run genuinelydisabled.Review
A high-effort
/code-reviewfound 10 real defects, all fixed — most seriously the brief band shifting the Monaco host's child slot so the positional diff rewrote the live editor, the tab switch leaving the editor showing a different chapter than the brief claimed, and the--no-restorepackage-graph mismatch above. Worth noting one dead end the fix comments record: keying the two children looks like the obvious fix and is worse — a keyed host is re-created by the full-document morph, orphaning the editor Monaco mounted into, which I only caught by screenshotting.Benchmarks: not applicable. The framework diff is one new file (
Mount.cs, 49 lines); no existing render or live-runtime path is touched.Docs
docs/playground.md(tutorial section + honest limitations),docs/composition.md(hosting a component you built yourself),docs/sqlite.md,docs/data-access.md,docs/tutorial/00-overview.md,README.md,llms.txt, CHANGELOG.docs/sqlite.md's "SQLite in the browser?" section said it isn't viable. It is — this PR ships it — so that caveat is now scoped to what actually still bites (durability, plus the untrimmed andWASM0001constraints). Durable client-side SQLite over OPFS is being designed separately in #642, so this deliberately leaves that rewrite to it rather than half-doing it here.