Skip to content

ema-shim: expose window.ema.ready promise#181

Merged
srid merged 1 commit intomasterfrom
feat/ema-ready-promise
Apr 26, 2026
Merged

ema-shim: expose window.ema.ready promise#181
srid merged 1 commit intomasterfrom
feat/ema-ready-promise

Conversation

@srid
Copy link
Copy Markdown
Owner

@srid srid commented Apr 26, 2026

window.ema.ready now resolves on first WebSocket handshake — a one-shot gate for external scripts that need to call window.ema.switchRoute (or any other WS-dependent API we add later) without racing the connection open. Reconnects don't reset it; it's a first-open signal, not a current-state flag.

Motivated by Emanote's e2e suite (srid/emanote#667), where a Playwright step needs to programmatically navigate via switchRoute to exercise the morph code path. The natural alternative — `page.waitForTimeout` — is flaky against cold-start emanote. Polling for WebSocket.OPEN requires exposing the ws instance, which is closure-scoped on purpose. A one-line Promise on the public API surface is the cheapest, broadest fix.

Tiny behavior addition; no breaking change. The existing window.ema = { switchRoute } shape gets one new field; nothing existing moves.

Resolves on first WebSocket open. External scripts (test harnesses,
IDE integrations) calling window.ema.switchRoute or other future
WS-dependent APIs can now await the promise instead of racing the
handshake or polling for connection state. Reconnects don't reset
it — it's a one-shot "client is past first handshake" gate.

Motivated by the e2e test cases for Emanote (srid/emanote#667), where
a Playwright step needs to programmatically navigate via switchRoute
and the natural alternative (sleep an arbitrary 200-1000ms) is flaky.
srid added a commit to srid/emanote that referenced this pull request Apr 26, 2026
The previous e2e setup spawned emanote with --no-ws, so every
scenario navigated via page.goto (fresh full load) and the morph code
path was completely uncovered. That's why issue #667 (toc-spy dying
after morph nav) escaped detection.

Drop --no-ws and add a @morph-tagged scenario that navigates via
window.ema.switchRoute. The test is gated to live mode only; static
mode has no WS so the @morph Before hook returns 'skipped'.

Awaiting window.ema.ready (PR srid/ema#181 — pinned via flake input
to feat/ema-ready-promise) avoids racing the WS handshake. The shim's
EMAHotReload event lets the step block until morph + script-reload
completes, so the next assertion sees stable DOM.

Coverage now: 16 scenarios; live=16/16, static=15/16 (1 skipped).
@srid srid merged commit 87da4f3 into master Apr 26, 2026
2 checks passed
@srid srid deleted the feat/ema-ready-promise branch April 26, 2026 14:49
srid added a commit to srid/emanote that referenced this pull request Apr 26, 2026
ema PR srid/ema#181 merged. Drop the temporary feat-branch pin in
flake.nix and pull the merge commit via flake.lock — no functional
change since the feat branch contained exactly the merged content.
srid added a commit to srid/emanote that referenced this pull request Apr 26, 2026
**Site-authored interactive behaviors no longer live as inline
`<script>` IIFEs scattered across Heist templates.** Four small ES
modules under `_emanote-static/js/` (`theme-toggle`, `code-copy`,
`toc-spy`, `footnote-popup`) replace the per-template IIFEs, plus a
`morph.js` helper providing `ready()` and `onElement(selector, fn)`
(MutationObserver-backed). Closes #643. *Side effect*: code-copy buttons
now wire onto `<pre>` blocks added by live-server DOM patches, not only
those present at first paint.

Live-mode cache busting works **per file** via an inline `<script
type="importmap">` mapping bare specifiers (`@emanote/morph` etc.) to
URLs through `siteRouteUrl`, which appends `?t=<mtime>`. Plain ES module
imports (`import './morph.js'`) would have stripped the query during URL
resolution and stale-cached transitive deps; the bare-specifier
indirection is what makes per-file invalidation propagate. The importmap
itself is tagged `im-preserve` + `data-ema-skip` so it survives Ema's
morph navigation — without that, the second importmap in the morphed
`<head>` hit the HTML spec's *already-resolved specifier* rule and
emitted a console warning per entry. The `toc-spy` module captures DOM
refs in a closure, so it now also subscribes to Ema's `EMAHotReload`
event to tear down + re-init on each morph (closes #667).

Static mode also gained a fix: the production Tailwind compile now adds
`_emanote-static/js` as an `@source`, so utility classes applied by JS
at runtime (`POPOVER_CLASS`, `BODY_CLASS` in `footnote-popup.js`) are
visible to the scanner and don't drop out of `tailwind.css`. Pre-fix,
the popup rendered as plain text inside a thin border on `emanote gen`
output. Also strips Tailwind's `//# sourceMappingURL=/sm/<hash>.map`
trailer to silence the per-pageload 404 in dev logs.

> **Depends on srid/ema#181** (merged) — `window.ema.ready` Promise so
the morph-nav e2e step can await the WS handshake without racing.
>
> **Out of scope, kept inline by design**: FOUC theme applier in
`base.tpl` (pre-paint, can't defer), Stork controller
(`onclick="window.emanote.stork.toggleSearch()"` call sites span four
templates — separate refactor), breadcrumbs `toggleHidden` (5 lines;
section 1 of #632 deletes via native `<details>`).
>
> **Known limitation, documented**: editing a *bare-imported* JS module
mid-session needs a hard reload to pick up the change via morph nav (the
preserved importmap holds the original URLs). `main.js` itself still
cache-busts via morph since it's loaded directly. Issue #666 tracks the
broader layer-URL cache-bust gap (CSS, fonts, Stork's own assets).

E2e: 16 scenarios, live=16/16, static=15+1-skipped (`@morph` tag gates
morph-nav for live only). New scenarios cover theme toggle, code-copy
initial wiring, TOC scroll-spy, popup body styling (catches the Tailwind
class-drop regression class), and morph-nav of toc-spy. Module split
into `Emanote.View.JsBundle` mirrors the existing
`Emanote.View.Tailwind` shape.

### Try it locally

```sh
nix run github:srid/emanote/js-module-boundary -- -L $YOUR_NOTEBOOK run
```
srid added a commit to srid/emanote that referenced this pull request Apr 26, 2026
Quality review flagged narration ("this file owns…", "extracted because…",
"step definitions stay mode-agnostic by going through this helper") and
WHAT-restating doc blocks across the new modules. Trimmed each to the
WHY-only nucleus or the bare purpose; kept the bug references that
encode hidden constraints (srid/ema#181, EMAHotReload) since those
inform reviewers about non-obvious gates.
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