perf(core): parallelize cold-boot init phases and batch exclusive-hook reads#1408
Conversation
…sive-hook option reads Reduce sequential DB round trips in EmDashRuntime.create(): - Run the rt.plugins (_plugin_state) and rt.site (options) reads under Promise.all — independent tables, each phase keeps its own phase() timing entry and non-fatal catch. - Run rt.market and rt.registry sandboxed-plugin loads concurrently when both tiers are enabled; both still wait on rt.sandbox and keep their own enablement conditionals and error semantics. - resolveExclusiveHooks() accepts an optional getOptions batch reader; the runtime and PluginManager wire it to OptionsRepository.getMany() so all current selections load in one query instead of one get() per hook. Per-hook set/delete writes and the missing-options-table tolerance are unchanged. Adds unit tests for batched resolution parity, single-read behavior (dialect-agnostic, query-counted via a Kysely plugin), batch-failure tolerance, and an end-to-end EmDashRuntime.create() integration test asserting per-phase timings survive parallelization. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
🦋 Changeset detectedLatest commit: c62106c The changes in this PR will be included in the next version bump. This PR includes changesets to release 14 packages
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
Deploying with
|
| Status | Name | Latest Commit | Updated (UTC) |
|---|---|---|---|
| ✅ Deployment successful! View logs |
docs | c62106c | Jun 11 2026, 03:20 PM |
Deploying with
|
| Status | Name | Latest Commit | Updated (UTC) |
|---|---|---|---|
| ✅ Deployment successful! View logs |
emdash-playground | c62106c | Jun 11 2026, 03:21 PM |
Deploying with
|
| Status | Name | Latest Commit | Updated (UTC) |
|---|---|---|---|
| ✅ Deployment successful! View logs |
emdash-demo-cache | c62106c | Jun 11 2026, 03:23 PM |
@emdash-cms/admin
@emdash-cms/auth
@emdash-cms/auth-atproto
@emdash-cms/blocks
@emdash-cms/cloudflare
@emdash-cms/contentful-to-portable-text
emdash
create-emdash
@emdash-cms/gutenberg-to-portable-text
@emdash-cms/plugin-cli
@emdash-cms/plugin-types
@emdash-cms/registry-client
@emdash-cms/registry-lexicons
@emdash-cms/sandbox-workerd
@emdash-cms/x402
@emdash-cms/plugin-ai-moderation
@emdash-cms/plugin-atproto
@emdash-cms/plugin-audit-log
@emdash-cms/plugin-color
@emdash-cms/plugin-embeds
@emdash-cms/plugin-field-kit
@emdash-cms/plugin-forms
@emdash-cms/plugin-webhook-notifier
commit: |
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
This PR improves cold-isolate startup performance in EmDashRuntime.create() by overlapping independent initialization phases and reducing N+1 options reads during exclusive-hook resolution, while preserving per-phase timing instrumentation and non-fatal error behavior.
Changes:
- Parallelize
rt.pluginsandrt.siteinitialization work to overlap DB round trips. - Run marketplace and registry installed-plugin cold loads concurrently when both are enabled.
- Add batched exclusive-hook option reads via an optional
getOptions()pathway, with new unit/integration coverage and a changeset.
Reviewed changes
Copilot reviewed 6 out of 6 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| packages/core/src/emdash-runtime.ts | Parallelizes cold-boot init phases and concurrently loads installed plugin tiers; wires batched hook-resolution reads. |
| packages/core/src/plugins/hooks.ts | Adds optional getOptions() API and implements batched exclusive-hook option reads. |
| packages/core/src/plugins/manager.ts | Passes OptionsRepository.getMany() through to hook-resolution as getOptions(). |
| packages/core/tests/unit/plugins/exclusive-hooks.test.ts | Adds unit tests for batched resolution parity, query-count assertions, and batch-failure tolerance. |
| packages/core/tests/integration/runtime/create.test.ts | Adds end-to-end cold-boot integration test verifying phases/timings and persisted hook selections. |
| .changeset/lazy-pugs-brake.md | Patch changeset describing the performance improvements. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| getOption: vi.fn(async (key: string): Promise<string | null> => store.get(key) ?? null), | ||
| getOptions: vi.fn(async (keys: string[]): Promise<Map<string, string | null>> => { | ||
| const result = new Map<string, string | null>(); | ||
| for (const key of keys) { |
…ring> Per review: getMany never produces null values (missing keys are simply absent) and resolution only reads the map. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
Copilot's type-tightening taken in c62106c: |
What does this PR do?
Cold-isolate runtime init (
EmDashRuntime.create()) runs its phases strictly sequentially, so every phase is a full DB/R2 round trip added to TTFB on the first request an isolate serves. Three changes, all preserving each phase's timing instrumentation and non-fatal error semantics:rt.plugins∥rt.site— the_plugin_stateSELECT and the site-infogetMany()are independent reads against different tables; they now share one round-trip window underPromise.all. TheenabledPluginsderivation (which consumes plugin states) moved after the join.rt.market∥rt.registry— both tiers of installed sandboxed plugins only depend on the sandbox phase, not on each other; when both are enabled they now load concurrently (each is R2 listing + bundle fetches, the slowest part of cold boot on marketplace sites).resolveExclusiveHooksdid onegetOption()round trip per registered exclusive hook, sequentially. It now collects allemdash:exclusive_hook:*keys and reads them viaOptionsRepository.getMany()in a single query (new optionalgetOptionsonExclusiveHookResolutionOptions; per-key path kept as fallback for callers that don't provide it). Writes are untouched. A batch-read failure (options table not ready) skips resolution entirely — the identical net effect to every per-key read failing.Per-phase
phase()timings are preserved by keeping each phase as its ownphase()call and joining the promises, so Server-Timing still reports each phase's own duration (now overlapping in wall time).Closes #
Type of change
Checklist
pnpm typecheckpassespnpm lintpassespnpm testpasses (or targeted tests for my change) — unit 173 files / 2,637 tests; integration 71 files / 1,041 testspnpm formathas been runAI-generated code disclosure
Screenshots / test output
New tests: batched exclusive-hook resolution parity across all three branches (kept / stale-cleared + auto-select / multi-provider-unselected) with exactly 1 SELECT asserted via a query-counting Kysely plugin (dialect-agnostic via
describeEachDialect), batch-failure tolerance (no writes, no selections), and an end-to-endEmDashRuntime.create()integration test against real SQLite asserting eachrt.*phase appears exactly once with a finite duration and hook selections persist.🤖 Generated with Claude Code
Try this PR
Open a fresh playground →
A full working EmDash site, deployed from this branch. Each visit gets its own session-scoped sandbox: no login needed and no shared state. Try the admin, edit content, hit the public site.
Tracks
perf/cold-boot-parallel-init. Updated automatically when the playground redeploys.