HS-design: E39 WebWorker plugin
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
150
plans/designs/e39-webworker.md
Normal file
150
plans/designs/e39-webworker.md
Normal file
@@ -0,0 +1,150 @@
|
|||||||
|
# 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 (0–195) — 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.
|
||||||
Reference in New Issue
Block a user