Files
rose-ash/plans/designs/e39-webworker.md
giles df8913e9a1 HS-design: E39 WebWorker plugin
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-24 07:08:02 +00:00

151 lines
5.8 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# E39 — WebWorker plugin
Cluster 39 of `plans/hs-conformance-to-100.md`. Goal: +1 test.
## 1. The 1 failing test
Suite `hs-upstream-worker`, test:
> `raises a helpful error when the worker plugin is not installed`
Translated upstream (`test/features/worker.js`) — currently emitted as
`SKIP (untranslated)` in `spec/tests/test-hyperscript-behavioral.sx` path
`(117 2 2)`:
```
(deftest "raises a helpful error when the worker plugin is not installed"
(error "SKIP (untranslated): raises a helpful ..."))
```
Upstream assertion:
```js
const msg = await error("worker MyWorker def noop() end end")
expect(msg).toContain('worker plugin')
expect(msg).toContain('hyperscript.org/features/worker')
```
Two substring checks on the error produced by compiling the source.
## 2. Upstream `worker` syntax
From `https://hyperscript.org/features/worker/`:
```
worker <name>[(<external-script-url>*)]
(<def-feature> | <js-block>)+
end
```
- Declared at top level alongside `on`, `init`, `def`, `behavior`.
- Body restricted to `def` functions and `js ... end` blocks; no DOM / `window` access.
- Callers invoke as `WorkerName.fn(args)` from the main thread; calls
return promises but are async-transparent.
- Stock `_hyperscript` ships only a **stub** in the core bundle — the
real implementation is in the `worker` ext. Without the ext loaded,
parsing must fail with a message mentioning both `worker plugin` and
`hyperscript.org/features/worker`.
## 3. Proposed SX shape
We ship the stub only. Full runtime is out of scope for a 1-test cluster.
### Parser addition (`lib/hyperscript/parser.sx`, `parse-feat`)
Extend the feature-dispatch `cond` (around line 2620) with one branch
**before** the fallthrough to `parse-cmd-list`:
```
((= val "worker") (parse-worker-stub))
```
`parse-worker-stub` raises immediately — it does not consume tokens,
does not need to recognise the body grammar. The error string contains
both required substrings:
```
"worker plugin is not installed — see https://hyperscript.org/features/worker"
```
`(error ...)` at parser scope is the existing failure channel (used by
`Expected 'of' or 'from' at position …` etc.), so this slots in without
changes to the error plumbing.
### Compile output
None. The stub never returns an AST; the error propagates out of
`hs-compile` and through `hs-to-sx-from-source`, surfacing to the test
runner as a caught exception.
## 4. Runtime architecture
For the 1 test: **no runtime.** Parsing fails, so `runtime.sx` never sees
a worker form. No `Worker` class needed in the `hs-run-filtered.js`
mock. Nothing touches the DOM shim.
For a hypothetical full implementation (explicitly out of scope):
- Server would bind `WorkerName` in the HS top-level env to a record
`{:worker-handle H :exports (list ...)}`.
- `WorkerName.fn(args)` would compile via the existing property-access
path to `(hs-method-call WorkerName "fn" args)`, which would detect
the worker handle and dispatch over a `postMessage` channel.
- Mock env would need a `Worker` class constructor + a serialisable
message loop driving the worker script's own tiny SX interpreter.
Deferred until the 7 skipped upstream tests become a target cluster.
## 5. Test delta estimate
+1 test. Feasibility: **high**. Two substring checks. Implementation
is a single `cond` branch plus a 2-line error string. Generator patch
to un-skip the test is mechanical.
## 6. Risks
- **Drift from future real plugin** — if we later implement the plugin,
the stub must be replaced, not shadowed. Mitigation: the stub lives
inline in `parse-feat`; adding the real plugin means deleting the
stub branch and replacing it with `(parse-worker-feat)`. Single site.
- **In-browser vs mock divergence** — none in this cluster. The stub
errors identically in both hosts because it's pure parser logic.
- **Message serialisation** — N/A until full plugin.
- **Error-message phrasing drift** — the upstream test only checks for
two substrings. We must keep `worker plugin` and
`hyperscript.org/features/worker` verbatim; change-detector tests
should reference this design if the wording is ever touched.
- **Generator mis-translation** — the upstream JS uses `await error(src)`
which the SX generator currently bails on (`return None`), producing
the `SKIP (untranslated)`. We can either (a) teach the generator to
translate `await error(src)` + two `toContain` checks, or (b) hand-
write the test. Recommend (b) for one test: less generator churn.
## 7. Implementation checklist (single commit)
1. `sx_replace_by_pattern` in `lib/hyperscript/parser.sx` — insert the
`((= val "worker") ...)` branch inside `parse-feat`'s `cond`. Body:
`(error "worker plugin is not installed — see https://hyperscript.org/features/worker")`.
2. `sx_replace_node` in `spec/tests/test-hyperscript-behavioral.sx`
path `(117 2)` — replace the `SKIP (untranslated)` body with:
```
(deftest "raises a helpful error when the worker plugin is not installed"
(let ((result (guard (e (true (if (string? e) e (str e))))
(hs-compile "worker MyWorker def noop() end end")
"")))
(assert (contains? result "worker plugin"))
(assert (contains? result "hyperscript.org/features/worker"))))
```
3. `sx_validate` on both files.
4. Run `node tests/hs-run-filtered.js` with `HS_SUITE=hs-upstream-worker`.
Expect 1/1.
5. Run full suite smoke (0195) — expect no regressions (pure additive
branch in `parse-feat`; `"worker"` was previously caught by the
fallthrough `parse-cmd-list`, which would have errored anyway on
an unknown identifier — confirm by checking baseline).
6. Commit: `HS: E39 WebWorker plugin stub (+1 test)`.
## Non-goals
- Any worker runtime. Any real `Worker` object. Any message passing.
- The 7 sibling `test.skip(...)` cases upstream — they remain skipped.
- Generator patches — the test is hand-written per §6 above.