Repository navigation
feat: add per-window onWindowSetup hook to createWindowPool - #17
Merged
Merged
Conversation
Adds an additive per-window setup hook for realm-specific initialization (custom element registration, prototype patches, documentElement attribute sync) that runs once for each window the pool creates. Each pooled window is its own browsing-context realm, so anything that registers globals on the parent window (e.g. a library calling customElements.define at import time) is invisible inside the popout. Until now the only hook was the function form of injectStyles, which replaces the default auto <style>/<link> mirroring entirely — so "auto styles plus a bit more setup" had no clean home. The hook is threaded through initWindowDocument (whose body establishes the documented timing invariant: <base>, styles, and #root in place) and runs at the bottom of that function, so the contract is enforced by construction rather than by call-site ordering. initWindowDocument now takes the child Window (not just its Document) — the hook needs realm globals which live on the window — and returns a single combined cleanup that tears down both the consumer's setup and the style observer. The cleanup is one-shot: it can be reached from overlapping teardown paths (unload listener, notifyDestroyed, destroyEntry) and a consumer's cleanup may not be naturally idempotent the way the style subscriber's Set.delete is. A throwing setup or cleanup is caught and logged so a faulty hook can't poison pool creation or teardown. Cleanup runs when the window is destroyed (pool teardown, idle eviction, external close), not on release back to the pool — the hook fires once per window lifetime, not once per use. Documents what survives across reuse (realm globals, documentElement attrs, head contents) and what release() resets (body.className, doc.title, #root contents). InjectStylesMode and its function-replaces-auto semantics are unchanged. Exported as WindowSetupCallback.
yarn 1 sometimes skips the electron postinstall when `yarn add` swaps the version over an existing install — `node_modules/electron/dist/` ends up empty and the chrome-sandbox fix step fails with "No such file or directory". Run `node node_modules/electron/install.js` explicitly after the swap; it downloads the prebuilt binary if missing and is a no-op if it's already there. The `electron-versions (beta)` job is informational (continue-on-error) so this isn't blocking, but the red X is noise.
|
🎉 This PR is included in version 1.2.0 🎉 The release is available on: Your semantic-release bot 📦🚀 |
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.
Demo
Runtime guard if you accidentally pass an
asynchook:Summary
Adds an additive per-window setup hook to
createWindowPoolfor realm-specific initialization — custom element registration, prototype patches,documentElementattribute sync — without giving up the defaultinjectStyles: "auto"style mirroring.Why
Each pooled window is its own browsing-context realm: its own
customElementsregistry, its own prototypes, its owndocument. A library that registers a custom element with shadow-DOMadoptedStyleSheetsat import time registers it on the parent window's realm; the popout's realm never sees it, so the element renders unstyled.Until now the only hook into pool window initialization was the function form of
injectStyles, which replaces the auto<style>/<link>mirroring entirely (handleStyleInjectionshort-circuits whenmodeis a function). "Auto styles plus a bit more setup" had no clean home — apps either reimplemented the fullMutationObserver-based style mirroring or did their setup in a per-componentuseLayoutEffect, which runs after React mounts content and risks a flash of unupgraded custom elements.What
WindowSetupCallbacktype, exported.createWindowPool(shape, config?, options?)—options.onWindowSetup?is new.WindowPoolDefinitiongains the corresponding field.initWindowDocument(whose body establishes the documented timing invariant:<base>, styles,#rootin place) and runs at the bottom of that function, so the contract is enforced by construction rather than by call-site ordering.initWindowDocumentnow takes the childWindow(the hook needs realm globals, which live on the window) and returns a single combined cleanup.console.error) so a faulty hook can't poison pool warm-up or teardown.asynchooks — TypeScript's permissivevoidreturn rule letsasync (win, doc) => ...through the type check, but at runtime its rejection escapes the error guard and its returned cleanup is silently dropped. The library fails loud instead.InjectStylesModeand its function-replaces-auto semantics are unchanged.Behavior contract
<base>/styles/#root, before React portals content in.customElements, prototype patches),documentElementattributes,<head>contents.body.className,doc.title,#rootcontents, any other<body>children.console.error.Why not
<Window>tooNon-pooled
<Window>consumers have a React component already mounted when the document is ready —useWindowDocument()+useLayoutEffectdoes the same job at the same time. Pooled windows are pre-warmed before any consumer component exists, so there's no React lifecycle to hang an effect on. Documented as the recommended pattern in both the README and the pooling guide.Implementation notes
initWindowDocument(doc, options)→initWindowDocument(childWindow, options). Internal-only function (not in the package barrel); the three call sites all already hold theWindowreference.PoolEntry.styleCleanup→PoolEntry.cleanup(it now does both teardowns). The four teardown sites change names, not logic.RendererWindowPool.destroyEntryso a lingering reference can't pin the consumer's setup closure (which plausibly captures the childWindow).MockWindowin the test setup has no realm globals — documented as a known limitation intests/setup.ts.Tests
Added in
tests/integration/PooledWindow.test.tsx:#root/<base>existunload+notifyDestroyed)injectStylesmode (function form + setup hook coexist)asynchook is detected, warned, and its eventual cleanup correctly not capturedcreateWindowPool→WindowPoolDefinition→<PooledWindow>→ poolChecklist
yarn typecheckyarn test— 393 passed, 2 skippedyarn lint(oxlint)yarn format(oxfmt)feat:→ minor) perCONTRIBUTING.mdREADME.md,docs/.../pooling.mdx,docs/.../props.mdx, JSDoc onWindowSetupCallbackandcreateWindowPool