Initial Commit
This commit is contained in:
@@ -0,0 +1,50 @@
|
||||
# `@suaveplan/api-events-worker-proxy` — worker-side proxy to main-process `@suaveplan/events` bus
|
||||
|
||||
**Package**: `@suaveplan/api-events-worker-proxy`
|
||||
**Status**: PROPOSED
|
||||
**Type**: new-package
|
||||
**Stage**: 3 (Worker isolation infrastructure)
|
||||
|
||||
## Why
|
||||
|
||||
Per design doc §5.2a, every capability ships a proxy surface for worker-tier plugins. The `events` capability is special: there is no abstract `api-events` contract package — the host injects the existing `@suaveplan/events` bus directly into `PluginContext.events`. So this proxy is a worker-side surface over the existing `@suaveplan/events` `EventBusLike` interface, NOT a new contract layer. Worker-side `ctx.events.emit(...)` / `ctx.events.on(...)` calls marshal across `MessagePort` to the main-process bus.
|
||||
|
||||
Per §5.2b sandboxed gating: only manifested `publishes` topics are emittable; only manifested `subscribes` topics deliverable. That gating is enforced MAIN-SIDE; this proxy is the transport.
|
||||
|
||||
## What changes
|
||||
|
||||
Net-new package.
|
||||
|
||||
**ADDED exports:**
|
||||
|
||||
- `./client` — `createEventsProxyClient(port): WorkerProxyOf<EventBusLike>`.
|
||||
- `./subscriber` — `on(topic, handler)` / `off` / `once`; handlers run worker-side; backpressure carried by `api-worker-rpc` so a slow worker-side subscriber cannot exhaust main-side memory.
|
||||
- `./publisher` — `emit(topic, payload)` marshals payload; main-side does the topic-allow check + actual bus publish; emit-not-allowed returns `EVENTS_PROXY_TOPIC_NOT_ALLOWED` (rebuilt `SuaveplanError`).
|
||||
- `./error-codes` — `EVENTS_PROXY_*` codes (`EVENTS_PROXY_TOPIC_NOT_ALLOWED`, `EVENTS_PROXY_HANDLER_THROW`, `EVENTS_PROXY_PORT_CLOSED`, `EVENTS_PROXY_BACKPRESSURE_DROP`).
|
||||
- `./testing` — paired `MessageChannel` + stub bus.
|
||||
|
||||
## Out of scope
|
||||
|
||||
- **Topic allowlisting policy** — enforced main-side per §5.2b.
|
||||
- **Cross-host fan-out** — `@suaveplan/events`'s job (and `api-ws-broker-redis` for WS specifically).
|
||||
- **Bounded-queue capability** — `events.bounded-queue` capability flag is honored main-side; the proxy only marshals.
|
||||
|
||||
## Source mapping
|
||||
|
||||
Genesis-original. Design doc sections: §5.2a (worker-RPC variants), §5.2b (sandboxed gating), §6 (capability scoping).
|
||||
|
||||
| Requirement | Design doc section |
|
||||
|---|---|
|
||||
| Proxy surface over existing `@suaveplan/events` bus (no new contract) | §5.2a |
|
||||
| Topic allow-check on main side; proxy rejects with reconstructed error | §5.2b |
|
||||
| Handler throws reconstructed via `api-worker-rpc` | §5.2a |
|
||||
|
||||
## Verification approach
|
||||
|
||||
- `src/publisher/publisher.test.ts` — emit allowed topic round-trips; disallowed topic throws.
|
||||
- `src/subscriber/subscriber.test.ts` — handler invocation; off cancels; backpressure throttles main → worker.
|
||||
- `tests/conformance/run-events-bus-conformance.test.ts` — `runEventsBusConformance` from `@suaveplan/events/testing` passes against the proxy as surface. **Required before Stage 4 can complete.**
|
||||
|
||||
## Migration impact
|
||||
|
||||
None — additive only.
|
||||
@@ -0,0 +1,70 @@
|
||||
<!-- markdownlint-disable MD024 -->
|
||||
|
||||
# Capability Spec Delta: api-events-worker-proxy (api-events-worker-proxy)
|
||||
|
||||
**Package**: `@suaveplan/api-events-worker-proxy`
|
||||
**Status**: PROPOSED
|
||||
|
||||
> Every requirement below carries a `**Verified by:**` line at authoring time.
|
||||
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: Proxy preserves the EventBusLike surface from `@suaveplan/events`
|
||||
|
||||
The package SHALL expose `createEventsProxyClient(port)` returning a value that satisfies `WorkerProxyOf<EventBusLike>` where `EventBusLike` is the EXISTING type from `@suaveplan/events`. No new contract layer SHALL be introduced — worker-side plugin code that worked against the direct bus MUST also work against the proxy with no source change beyond async-await wrapping (which `WorkerProxyOf` already enforces by promise-wrapping return types).
|
||||
|
||||
#### Scenario: proxy type matches WorkerProxyOf<EventBusLike>
|
||||
|
||||
- **WHEN** `expectTypeOf<ReturnType<typeof createEventsProxyClient>>().toEqualTypeOf<WorkerProxyOf<EventBusLike>>()` is checked
|
||||
- **THEN** the assertion passes
|
||||
|
||||
**Verified by:** `src/client/client.spec-conformance.test.ts::"proxy type matches WorkerProxyOf<EventBusLike>"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: Disallowed-topic emit throws reconstructed SuaveplanError
|
||||
|
||||
The package SHALL marshal `emit(topic, payload)` calls to the main-process bus where topic-allow checks are enforced per design doc §5.2b. If the main side rejects with a disallow signal, the proxy MUST surface `EVENTS_PROXY_TOPIC_NOT_ALLOWED` as a `SuaveplanError` reconstructed via `api-worker-rpc`'s error-reconstruction.
|
||||
|
||||
#### Scenario: allowed topic emit round-trips
|
||||
|
||||
- **WHEN** a plugin manifests `publishes: ["billing.invoice.created"]` AND calls `emit("billing.invoice.created", payload)`
|
||||
- **THEN** the main-side bus receives the event AND the call resolves successfully
|
||||
|
||||
#### Scenario: disallowed topic throws TOPIC_NOT_ALLOWED
|
||||
|
||||
- **WHEN** the plugin calls `emit("foreign.topic", payload)` with no matching manifest entry
|
||||
- **THEN** the proxy call rejects with `SuaveplanError` of code `EVENTS_PROXY_TOPIC_NOT_ALLOWED`
|
||||
|
||||
**Verified by:** `src/publisher/publisher.test.ts::"allowed topic emit round-trips"` AND `src/publisher/publisher.test.ts::"disallowed topic throws TOPIC_NOT_ALLOWED"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: Subscription delivers events from main bus to worker handler with backpressure
|
||||
|
||||
The package SHALL marshal `on(topic, handler)` so that events emitted on the main-process bus are delivered to the worker-side handler. Worker-side handler throws MUST surface main-side as `EVENTS_PROXY_HANDLER_THROW`. The main-side dispatcher MUST respect `api-worker-rpc`'s backpressure budget — when the worker is slow, main-side delivery throttles rather than buffering unbounded.
|
||||
|
||||
#### Scenario: event delivered to worker handler
|
||||
|
||||
- **WHEN** the main bus emits `topic` AND the worker handler is registered
|
||||
- **THEN** the handler observes the event payload
|
||||
|
||||
#### Scenario: slow handler backpressures producer
|
||||
|
||||
- **WHEN** the worker handler takes 100ms per call AND main emits 100 events in a burst
|
||||
- **THEN** the main-side dispatcher pauses at the backpressure budget rather than buffering all 100 in memory
|
||||
|
||||
**Verified by:** `src/subscriber/subscriber.test.ts::"event delivered to handler"` AND `src/subscriber/subscriber.test.ts::"slow handler backpressures producer"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: Conformance suite passes against this proxy
|
||||
|
||||
The package SHALL be runnable as the surface argument to `runEventsBusConformance` from `@suaveplan/events/testing`. Every conformance scenario (FIFO ordering, interceptors, idempotent listeners) MUST pass identically to a direct in-process bus. This is the Stage-4-precondition gate.
|
||||
|
||||
#### Scenario: runEventsBusConformance passes
|
||||
|
||||
- **WHEN** `runEventsBusConformance("worker-proxy", makeFactory)` runs
|
||||
- **THEN** every scenario passes
|
||||
|
||||
**Verified by:** `tests/conformance/run-events-bus-conformance.test.ts::"runEventsBusConformance passes against proxy"`
|
||||
68
openspec/archive/2026-05-25-api-events-worker-proxy/tasks.md
Normal file
68
openspec/archive/2026-05-25-api-events-worker-proxy/tasks.md
Normal file
@@ -0,0 +1,68 @@
|
||||
# Tasks — `@suaveplan/api-events-worker-proxy`
|
||||
|
||||
> Every checkbox MUST reflect work actually done in this change.
|
||||
|
||||
## 0. Pre-flight (non-negotiable)
|
||||
|
||||
- [x] 0.1 Confirm clean working tree (`git status` reports nothing) and no in-flight worktrees with uncommitted work (`git worktree list` then `git status` in each)
|
||||
- [x] 0.2 If any worktrees exist under `.claude/worktrees/`, account for each one: still-active agent, work-pending-merge, or stale-and-prune. Stale worktrees MUST be removed (`git worktree remove`) before opening a new one — code that has been lost twice on main is the failure mode this guards against.
|
||||
- [x] 0.3 Confirm the package set in `MANIFEST.md` matches `find packages -maxdepth 3 -name package.json` count. Drift here means a prior change archived without regenerating the manifest.
|
||||
|
||||
## 1. Spec authoring
|
||||
|
||||
- [x] 1.1 Read design doc §5.2a, §5.2b, §6
|
||||
- [x] 1.2 Read umbrella
|
||||
- [x] 1.3 Read `openspec/AGENTS.md` Sections 13 and 14
|
||||
- [x] 1.4 Draft `proposal.md`
|
||||
- [x] 1.5 Draft `specs/api-events-worker-proxy/spec.md`
|
||||
- [x] 1.6 Every requirement carries `**Verified by:**`
|
||||
|
||||
## 2. Implementation
|
||||
|
||||
- [x] 2.1 Scaffold `packages/api/api-events-worker-proxy/` (`0.1.0`, deps: `@suaveplan/api-worker-rpc`, `@suaveplan/events`, `@suaveplan/api-types`, `@suaveplan/error`, `zod`)
|
||||
- [x] 2.2 `src/client/` — typed proxy client factory
|
||||
- [x] 2.3 `src/publisher/` — `emit(topic, payload)` marshalling
|
||||
- [x] 2.4 `src/subscriber/` — `on` / `off` / `once`; backpressure
|
||||
- [x] 2.5 `src/error-codes.ts` — `EVENTS_PROXY_*` codes
|
||||
- [x] 2.6 `src/testing/` — paired `MessageChannel` + stub bus
|
||||
- [x] 2.7 `src/index.ts` — namespace re-exports
|
||||
|
||||
## 3. Tests
|
||||
|
||||
- [x] 3.1 `src/publisher/publisher.test.ts` — allowed emit round-trips; disallowed throws `EVENTS_PROXY_TOPIC_NOT_ALLOWED`
|
||||
- [x] 3.2 `src/subscriber/subscriber.test.ts` — handler invocation; `off` cancels; backpressure throttles
|
||||
- [x] 3.3 `src/subscriber/handler-throw.test.ts` — worker-side handler throw surfaces as `EVENTS_PROXY_HANDLER_THROW` main-side
|
||||
- [x] 3.4 `tests/conformance/run-events-bus-conformance.test.ts` — `runEventsBusConformance` passes
|
||||
- [x] 3.5 Every `it(...)` carries `{ timeout: 5000 }`
|
||||
|
||||
## 4. Documentation
|
||||
|
||||
- [x] 4.1 Sibling `.md` per `.ts` (≥200 words)
|
||||
- [x] 4.2 README links every co-located `.md`; explicit note that this proxies over the existing `@suaveplan/events` (no new contract)
|
||||
- [x] 4.3 Update CLAUDE.md
|
||||
|
||||
## 5. Pre-archive verification (the 5-gate triple)
|
||||
|
||||
- [x] 5.1 `bun run check` — Biome 0/0/0
|
||||
- [x] 5.2 `bunx turbo lint typecheck test --filter=@suaveplan/api-events-worker-proxy` — green
|
||||
- [x] 5.3 `bunx turbo test:coverage --filter=@suaveplan/api-events-worker-proxy` — 100/100/100/100
|
||||
- [x] 5.4 `bun scripts/openspec-conformance.ts @suaveplan/api-events-worker-proxy` — exit 0
|
||||
- [x] 5.5 `bunx turbo build --filter=@suaveplan/api-events-worker-proxy` — green
|
||||
- [x] 5.6 **Stage-4 conformance gate**: `runEventsBusConformance` (from `@suaveplan/events/testing`) MUST pass under both direct AND worker-proxy surface before Stage 4 can complete
|
||||
|
||||
## 6. Pre-archive 7-row checklist
|
||||
|
||||
- [x] 6.1 Generics threaded (`EventBusLike` shape preserved through proxy)
|
||||
- [x] 6.2 Zod at wire boundary (payload envelopes)
|
||||
- [x] 6.3 Every `EVENTS_PROXY_*` code thrown
|
||||
- [x] 6.4 Strict-mode behaviors — backpressure drop vs throttle configurable
|
||||
- [x] 6.5 OTel — N/A here; main-side bus emits event metrics
|
||||
- [x] 6.6 Lifecycle dedup — N/A
|
||||
- [x] 6.7 No retroactive tick
|
||||
|
||||
## 7. Archive
|
||||
|
||||
- [x] 7.1 Fold spec delta into `openspec/specs/api-events-worker-proxy/spec.md`
|
||||
- [x] 7.2 Move to `openspec/archive/<yyyy-mm-dd>-api-events-worker-proxy/`
|
||||
- [x] 7.3 Tick Stage-3 row in umbrella
|
||||
- [x] 7.4 Refresh `MANIFEST.md`
|
||||
Reference in New Issue
Block a user