Move docs/ package mirror into document-navigator plugin

Relocate the ~6k-file docs tree from the repo root into
plugins/document-navigator/docs/ so the plugin is self-contained and
portable. docs/superpowers/ (specs and plans) stays at the repo root.

Moved via cp+rm rather than git mv because directory rename() fails with
EPERM on this WSL /mnt DrvFs mount; git rename-detection pairs the
identical-content files at commit time.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-30 01:10:00 +01:00
parent fe3fedf42e
commit 9bf56a806b
6022 changed files with 12437 additions and 0 deletions

View File

@@ -0,0 +1,71 @@
# in-process
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Single-process publish/subscribe bus consumed by the SSE service to deliver every broadcast to every active subscriber on the matching channel. The `Fanout` interface is intentionally minimal so a future cross-node implementation (Redis pub/sub, NATS) can drop in without changing the service surface. The in-process implementation is the default — sufficient for any deployment that runs on a single process or behind a sticky load balancer.
## Features
- Per-channel listener sets with O(1) subscribe / unsubscribe.
- Synchronous publish — listeners run in registration order during the `publish` call.
- Unsubscribe is idempotent; calling the returned function twice is a no-op.
- Empty channels are pruned automatically so `Map` memory does not grow unbounded across short-lived topics.
- `clear()` for explicit eviction in test teardown.
## Usage: Basic
```ts
import { createInProcessFanout } from "@suaveplan/api-plugin-sse/fanout/in-process";
const fanout = createInProcessFanout();
const unsubscribe = fanout.subscribe("notifications", (event) => {
console.log(event.id, event.data);
});
fanout.publish("notifications", { id: "1", data: { kind: "ping" } });
unsubscribe();
```
## Usage: Advanced
```ts
import { createInProcessFanout, type Fanout } from "@suaveplan/api-plugin-sse/fanout/in-process";
// Wrap the in-process bus with a metering layer.
function meter(inner: Fanout): Fanout {
let published = 0;
return {
publish(channel, event) {
published++;
inner.publish(channel, event);
},
subscribe(channel, listener) {
return inner.subscribe(channel, listener);
},
clear() {
inner.clear();
},
};
}
const fanout = meter(createInProcessFanout());
fanout.publish("a", { id: "1", data: 1 });
```
## API Reference
- `createInProcessFanout(): Fanout` — constructs a single-process bus.
- `Fanout.publish(channel: string, event: SseEvent): void` — invokes every listener registered against the channel.
- `Fanout.subscribe(channel: string, listener: (event: SseEvent) => void): () => void` — registers a listener; returns an unsubscribe function. Calling the unsubscribe function more than once is safe.
- `Fanout.clear(): void` — removes every listener from every channel.
- `type FanoutListener = (event: SseEvent) => void` — listener signature.
## Implementation Notes
- Listeners run synchronously on the publishing call stack. A long-running listener will block other listeners and the broadcaster — keep listeners O(microseconds) or hand off to a queue.
- Subscribers added during a `publish` invocation are NOT invoked for that publish (set iteration semantics in V8 / JavaScriptCore guarantee the snapshot is taken at iteration start).
- Errors thrown by a listener propagate to the publisher — wrap in `try/catch` per listener if isolated error handling is needed. (The SSE service's listener is a closure over a per-connection queue and never throws.)