Initial Commit

This commit is contained in:
2026-05-27 23:29:53 +01:00
commit fbc70f96e6
10000 changed files with 1102152 additions and 0 deletions

View File

@@ -0,0 +1,69 @@
# @suaveplan/api-sse - Abstract Server-Sent Events contracts for @suaveplan/api: SseConnection, SseBroker, SseEvent (Zod), ReplayBuffer with Last-Event-Id resume, SseConnectionRegistry, SSE_CODES, and runSseEngineConformance harness. Concrete engines plug behind these contracts.
Abstract Server-Sent Events contracts for `@suaveplan/api`. Stage-1 contract package: defines the engine-side `SseConnection`, `SseBroker`, `ReplayBuffer` (Last-Event-Id resume), `SseConnectionRegistry`, the `SSE_CODES` error registry, and the `runSseEngineConformance` harness every concrete engine must pass.
This package ships no concrete engine — `@suaveplan/api-sse-bun` (and other transports) land at and plug behind these contracts.
<!-- begin:auto-docs -->
## Documentation tree
- [broker](./src/broker/broker.md) — `SseBroker` is the engine-side publish / subscribe surface for Server-Sent Events. It mirrors the shape of `@suaveplan/…
- [broker](./src/broker/broker.md) — `SseBroker` is the engine-side publish / subscribe surface for Server-Sent Events. It mirrors the shape of `@suaveplan/…
- [capability-id](./src/capability-id/capability-id.md) — The `"sse"` `CapabilityId` slot connects this package to the host's `CapabilityMap`. Plugin authors declare `requires:…
- [capability-id](./src/capability-id/capability-id.md) — The `"sse"` `CapabilityId` slot connects this package to the host's `CapabilityMap`. Plugin authors declare `requires:…
- [connection](./src/connection/connection.md) — `SseConnection` is the engine-side contract for a single Server-Sent-Events stream. Concrete engines (`api-sse-bun`, `a…
- [connection](./src/connection/connection.md) — `SseConnection` is the engine-side contract for a single Server-Sent-Events stream. Concrete engines (`api-sse-bun`, `a…
- [connection-registry](./src/connection-registry/connection-registry.md) — `SseConnectionRegistry` is the multi-index registry for active SSE connections. Engines call `add` on handshake, `remov…
- [connection-registry](./src/connection-registry/connection-registry.md) — `SseConnectionRegistry` is the multi-index registry for active SSE connections. Engines call `add` on handshake, `remov…
- [error-codes](./src/error-codes/error-codes.md) — `SSE_CODES` is the stable error-code registry every contract-level failure path in the SSE family raises. Concrete engi…
- [error-codes](./src/error-codes/error-codes.md) — `SSE_CODES` is the stable error-code registry every contract-level failure path in the SSE family raises. Concrete engi…
- [production-throws](./src/error-codes/production-throws.md) — `production-throws` exports the canonical factory functions that the host (and concrete SSE engines) invoke at every do…
- [event](./src/event/event.md) — `SseEvent` is the wire-shape contract for a single Server-Sent-Events field block: `id`, optional `event`, `data`, opti…
- [event](./src/event/event.md) — `SseEvent` is the wire-shape contract for a single Server-Sent-Events field block: `id`, optional `event`, `data`, opti…
- [replay-buffer](./src/replay-buffer/replay-buffer.md) — `ReplayBuffer` is the per-`(tenant, topic)` ring buffer that powers the `Last-Event-Id` resume contract. Concrete engin…
- [replay-buffer](./src/replay-buffer/replay-buffer.md) — `ReplayBuffer` is the per-`(tenant, topic)` ring buffer that powers the `Last-Event-Id` resume contract. Concrete engin…
- [testing](./src/testing/conformance.md) — `runSseEngineConformance` is the reusable conformance harness every concrete `@suaveplan/api-sse` engine MUST pass. Eng…
- [conformance](./src/testing/conformance.md) — `runSseEngineConformance` is the reusable conformance harness every concrete `@suaveplan/api-sse` engine MUST pass. Eng…
<!-- end:auto-docs -->
## Install
```bash
bun add @suaveplan/api-sse
```
## Quick start
```ts
import {
createReplayBuffer,
createConnectionRegistry,
SSE_CODES,
} from "@suaveplan/api-sse";
const buffer = createReplayBuffer({ eventBufferSize: 1024 });
const registry = createConnectionRegistry();
buffer.append({ id: "1", data: "hello" });
const since = buffer.since("0").length; // 0 (unknown ids throw SSE_CODES.LAST_EVENT_ID_UNKNOWN)
```
## Modules
- [`connection`](./src/connection/connection.md) — engine-side `SseConnection` interface.
- [`event`](./src/event/event.md) — `SseEvent` Zod schema + interface.
- [`broker`](./src/broker/broker.md) — engine-side publish / subscribe surface.
- [`replay-buffer`](./src/replay-buffer/replay-buffer.md) — per-(tenant, topic) ring buffer powering `Last-Event-Id` resume.
- [`connection-registry`](./src/connection-registry/connection-registry.md) — multi-index registry for active connections.
- [`capability-id`](./src/capability-id/capability-id.md) — `"sse"` slot in the host's `CapabilityMap`.
- [`error-codes`](./src/error-codes/error-codes.md) — `SSE_CODES` registry + `SseError` class.
- [`error-codes/production-throws`](./src/error-codes/production-throws.md) — canonical throw-site factories.
- [`testing/conformance`](./src/testing/conformance.md) — `runSseEngineConformance` harness.
## Status
- Package: `@suaveplan/api-sse`
- Version: `0.1.0`
- Stage: 1 of the API plugin system (contracts).
- Tier: universal — zero browser / Node-only deps; runs everywhere the host runs.

View File

@@ -0,0 +1,101 @@
{
"name": "@suaveplan/api-sse",
"version": "0.1.0",
"type": "module",
"sideEffects": false,
"description": "Abstract Server-Sent Events contracts for @suaveplan/api: SseConnection, SseBroker, SseEvent (Zod), ReplayBuffer with Last-Event-Id resume, SseConnectionRegistry, SSE_CODES, and runSseEngineConformance harness. Concrete engines plug behind these contracts.",
"license": "MIT",
"author": "Suaveplan Team",
"main": "./dist/index.js",
"module": "./dist/index.js",
"types": "./dist/index.d.ts",
"exports": {
"./package.json": "./package.json",
".": {
"import": {
"types": "./dist/index.d.ts",
"default": "./dist/index.js"
}
},
"./connection/connection": {
"import": {
"types": "./dist/connection/connection.d.ts",
"default": "./dist/connection/connection.js"
}
},
"./event/event": {
"import": {
"types": "./dist/event/event.d.ts",
"default": "./dist/event/event.js"
}
},
"./broker/broker": {
"import": {
"types": "./dist/broker/broker.d.ts",
"default": "./dist/broker/broker.js"
}
},
"./replay-buffer/replay-buffer": {
"import": {
"types": "./dist/replay-buffer/replay-buffer.d.ts",
"default": "./dist/replay-buffer/replay-buffer.js"
}
},
"./connection-registry/connection-registry": {
"import": {
"types": "./dist/connection-registry/connection-registry.d.ts",
"default": "./dist/connection-registry/connection-registry.js"
}
},
"./capability-id/capability-id": {
"import": {
"types": "./dist/capability-id/capability-id.d.ts",
"default": "./dist/capability-id/capability-id.js"
}
},
"./error-codes/error-codes": {
"import": {
"types": "./dist/error-codes/error-codes.d.ts",
"default": "./dist/error-codes/error-codes.js"
}
},
"./testing/conformance": {
"import": {
"types": "./dist/testing/conformance.d.ts",
"default": "./dist/testing/conformance.js"
}
}
},
"scripts": {
"build": "vite build",
"test": "vitest run --passWithNoTests",
"test:watch": "vitest",
"test:coverage": "vitest run --coverage --passWithNoTests",
"lint": "bunx biome check src/",
"lint:fix": "bunx biome lint --fix --unsafe --max-diagnostics 500 src/",
"format": "bunx biome format --write --max-diagnostics 500 src/",
"check": "bunx biome check --write --unsafe --max-diagnostics 500 src/",
"typecheck": "bunx tsc --noEmit --composite false -p tsconfig.lib.json",
"docs:check": "bun run ../../../../scripts/docs-check.ts ."
},
"dependencies": {
"@suaveplan/api-types": "workspace:^",
"@suaveplan/error": "workspace:^",
"tslib": "catalog:build",
"zod": "catalog:forms"
},
"devDependencies": {
"@suaveplan/testing": "workspace:^",
"@types/node": "catalog:",
"@vitest/coverage-istanbul": "catalog:testing",
"typescript": "catalog:",
"vite": "catalog:build",
"vite-plugin-dts": "catalog:build",
"vitest": "catalog:testing"
},
"publishConfig": {
"registry": "https://npmjs.fq.io/",
"access": "public"
},
"files": ["dist", "src", "README.md"]
}

View File

@@ -0,0 +1,67 @@
# `SseBroker`
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
`SseBroker` is the engine-side publish / subscribe surface for Server-Sent Events. It mirrors the shape of `@suaveplan/api-ws`'s `WsBroker` (publish / subscribe / dispose) but is a distinct type so engines can supply different transports for SSE vs WebSocket without their type-level slots leaking into each other's `CapabilityMap` entries.
Concrete engines plug behind this contract: an in-process broker holds an event-emitter, a Redis-backed broker holds a `XADD` connection, a Cloudflare-Pipeline broker holds a `Queue` binding. The host never knows which one is wired in — every code path uses the same `publish(envelope)` / `subscribe(topic, handler)` calls.
## Features
- Envelope carries `topic`, `tenantId`, `event` so cross-tenant transports cannot fan out to the wrong audience.
- `subscribe` returns an unsubscribe function — symmetrical with `addEventListener`.
- `dispose` is async so transports can flush in-flight writes.
- Handler signature accepts sync or async return — engines normalise.
## Usage: Basic
```ts
import type { SseBroker } from "@suaveplan/api-sse/broker/broker";
declare const broker: SseBroker;
const unsubscribe = broker.subscribe("orders", async (env) => {
console.log(env.event.data);
});
```
## Usage: Advanced
```ts
import type {
SseBroker,
SseBrokerEnvelope,
} from "@suaveplan/api-sse/broker/broker";
declare const broker: SseBroker;
declare const tenantId: import("@suaveplan/api-types/tenant").TenantId;
async function notifyOrder(orderId: string) {
const envelope: SseBrokerEnvelope = {
topic: "orders",
tenantId,
event: { id: orderId, event: "order.created", data: orderId },
};
await broker.publish(envelope);
}
// Tear down on plugin stop:
await broker.dispose();
```
## API Reference
- `interface SseBroker`
- `publish(envelope: SseBrokerEnvelope): Promise<void>`
- `subscribe(topic: string, handler: SseBrokerHandler): () => void`
- `dispose(): Promise<void>`
- `interface SseBrokerEnvelope { topic: string; tenantId: TenantId; event: SseEvent }`
- `type SseBrokerHandler = (envelope: SseBrokerEnvelope) => void | Promise<void>`
## Implementation Notes
Handlers MUST be idempotent because upstream transports (Redis Streams with `XADD`, Kafka, etc.) may replay an envelope across consumer-group rebalances. The host's `ReplayBuffer` is the canonical de-dupe surface — handlers SHOULD use it for any side-effecting fan-out.

View File

@@ -0,0 +1,28 @@
import { describe, expectTypeOf, it } from "@suaveplan/testing/runner";
import type {
SseBroker,
SseBrokerEnvelope,
SseBrokerHandler,
} from "./broker.js";
describe("SseBroker spec-conformance", () => {
it("SseBroker declares publish subscribe dispose", {
timeout: 5000,
}, () => {
expectTypeOf<SseBroker["publish"]>().parameters.toEqualTypeOf<
[SseBrokerEnvelope]
>();
expectTypeOf<SseBroker["publish"]>().returns.toEqualTypeOf<
Promise<void>
>();
expectTypeOf<SseBroker["subscribe"]>().parameters.toEqualTypeOf<
[string, SseBrokerHandler]
>();
expectTypeOf<SseBroker["subscribe"]>().returns.toEqualTypeOf<
() => void
>();
expectTypeOf<SseBroker["dispose"]>().returns.toEqualTypeOf<
Promise<void>
>();
});
});

View File

@@ -0,0 +1,42 @@
import type { TenantId } from "@suaveplan/api-types/tenant";
import type { SseEvent } from "../event/event.js";
/**
* Envelope carried over an {@link SseBroker} publish.
*
* The broker is engine-side; payloads always travel together with their
* tenant scope so cross-tenant transports cannot accidentally fan out
* to the wrong audience.
*/
export interface SseBrokerEnvelope {
readonly topic: string;
readonly tenantId: TenantId;
readonly event: SseEvent;
}
/**
* Handler signature for {@link SseBroker.subscribe}.
*
* Handlers MUST be idempotent — the broker may re-deliver an envelope
* if an upstream transport replays.
*/
export type SseBrokerHandler = (
envelope: SseBrokerEnvelope
) => void | Promise<void>;
/**
* Engine-side publish/subscribe surface.
*
* Mirrors the shape of `WsBroker` (publish / subscribe / dispose) but
* is a distinct type so engines can supply different transports for
* SSE vs WebSocket without their type-level slots leaking into each
* other's `CapabilityMap` entries.
*/
export interface SseBroker {
/** Publish an envelope to every subscriber on `envelope.topic`. */
publish(envelope: SseBrokerEnvelope): Promise<void>;
/** Subscribe to every envelope published on `topic`. Returns an unsubscribe fn. */
subscribe(topic: string, handler: SseBrokerHandler): () => void;
/** Tear down the broker and release every subscription. */
dispose(): Promise<void>;
}

View File

@@ -0,0 +1,5 @@
export type {
SseBroker,
SseBrokerEnvelope,
SseBrokerHandler,
} from "./broker.js";

View File

@@ -0,0 +1,47 @@
# `SseCapabilityId` + `SSE_CAPABILITY_ID`
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
The `"sse"` `CapabilityId` slot connects this package to the host's `CapabilityMap`. Plugin authors declare `requires: ["sse"]` in their manifest; the host narrows their `PluginContext` so `ctx.capabilities.sse` is typed as the concrete SSE broker / registry pair.
`@suaveplan/api-types/capabilities` already lists `"sse"` in its closed `CapabilityId` union — this module pins that contract with a type-test so an accidental removal upstream surfaces at this package's gates rather than landing silently and breaking every downstream consumer.
## Features
- `SseCapabilityId` is `Extract<CapabilityId, "sse">` — narrows to the literal at type-test time so a renamed/removed entry fails compile here.
- `SSE_CAPABILITY_ID` is a runtime `const` (`"sse"` `as const satisfies CapabilityId`) — usable in `requires: [SSE_CAPABILITY_ID] as const` declarations.
- No module augmentation needed; api-types owns the canonical union.
## Usage: Basic
```ts
import { SSE_CAPABILITY_ID } from "@suaveplan/api-sse/capability-id/capability-id";
const requires = [SSE_CAPABILITY_ID] as const;
```
## Usage: Advanced
```ts
import type { SseCapabilityId } from "@suaveplan/api-sse/capability-id/capability-id";
import type { CapabilityMap } from "@suaveplan/api-types/capabilities";
type SseSlot = CapabilityMap[SseCapabilityId];
function consume(slot: SseSlot) {
// slot is the concrete shape the host vends behind the SSE capability.
}
```
## API Reference
- `SSE_CAPABILITY_ID: "sse"` — runtime constant.
- `type SseCapabilityId = Extract<CapabilityId, "sse">` — type-level proof that `"sse"` is a member.
## Implementation Notes
The spec for this package describes the contribution as "augments the `CapabilityId` union to include `"sse"`". In practice api-types already declares `"sse"` directly, so the augmentation is degenerate: this module's job is to PIN the contract via `expectTypeOf<"sse">().toMatchTypeOf<CapabilityId>()` so the slot cannot be removed without breaking the api-sse build. This guards a real failure mode — a previous wave's WS capability was silently removed from api-types and the breakage surfaced only at app-tier integration.

View File

@@ -0,0 +1,13 @@
import type { CapabilityId } from "@suaveplan/api-types/capabilities";
import { describe, expect, expectTypeOf, it } from "@suaveplan/testing/runner";
import { SSE_CAPABILITY_ID, type SseCapabilityId } from "./capability-id.js";
describe("CapabilityId spec-conformance", () => {
it("sse is a valid CapabilityId after augmentation", {
timeout: 5000,
}, () => {
expectTypeOf<"sse">().toMatchTypeOf<CapabilityId>();
expectTypeOf<SseCapabilityId>().toEqualTypeOf<"sse">();
expect(SSE_CAPABILITY_ID).toBe("sse");
});
});

View File

@@ -0,0 +1,25 @@
import type { CapabilityId } from "@suaveplan/api-types/capabilities";
/**
* The string-literal `CapabilityId` slot the SSE family contributes.
*
* `@suaveplan/api-types` declares `"sse"` directly in its
* `CapabilityId` union — this re-export pins the contract so consumers
* of `@suaveplan/api-sse` get a single import path:
*
* ```ts
* import type { SseCapabilityId } from "@suaveplan/api-sse/capability-id/capability-id";
* ```
*
* Type-test {@link `capability-id.spec-conformance.test.ts`} asserts
* `"sse"` is assignable to `CapabilityId` so a future api-types change
* that accidentally removed the literal would fail this package's
* gates.
*/
export type SseCapabilityId = Extract<CapabilityId, "sse">;
/**
* Runtime constant for the SSE capability id. Useful when plugin
* authors declare `requires: [SSE_CAPABILITY_ID] as const`.
*/
export const SSE_CAPABILITY_ID = "sse" as const satisfies CapabilityId;

View File

@@ -0,0 +1,2 @@
export type { SseCapabilityId } from "./capability-id.js";
export { SSE_CAPABILITY_ID } from "./capability-id.js";

View File

@@ -0,0 +1,67 @@
# `SseConnectionRegistry` + `createConnectionRegistry`
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
`SseConnectionRegistry` is the multi-index registry for active SSE connections. Engines call `add` on handshake, `remove` on disconnect, and the host fans out broadcasts via `lookupByTenant` / `lookupByTopic`. Lookups are O(1) by design — the registry keeps a parallel `tenantId → Set<conn>` and `(topic, tenantId) → Set<conn>` index so broadcast paths avoid scanning the full connection set.
`remove` is idempotent: engines can call it from both the graceful-close path and a finaliser without risk of throwing or double-decrementing tenant counts. This matches the shape of `WsConnectionRegistry` in `@suaveplan/api-types/ws` so engines that bridge both transports can share lifecycle code.
## Features
- O(1) lookup by `tenantId` via `lookupByTenant`.
- O(1) lookup by `(topic, tenantId)` via `lookupByTopic` — cross-tenant fan-out is impossible by construction.
- `countByTenant` powers the per-principal cap check at handshake.
- `add(conn, topics)` is mutation-only; topics MUST be supplied at `add` time (no `subscribe(conn, topic)` here — topic subscriptions live with the SSE manifest).
## Usage: Basic
```ts
import { createConnectionRegistry } from "@suaveplan/api-sse/connection-registry/connection-registry";
const reg = createConnectionRegistry();
declare const connection: import("@suaveplan/api-sse/connection/connection").SseConnection;
reg.add(connection, ["orders", "notifications"]);
const audience = reg.lookupByTopic("orders", connection.tenantId);
```
## Usage: Advanced
```ts
import { createConnectionRegistry } from "@suaveplan/api-sse/connection-registry/connection-registry";
const reg = createConnectionRegistry();
function broadcast(
tenantId: import("@suaveplan/api-types/tenant").TenantId,
topic: string,
event: import("@suaveplan/api-sse/event/event").SseEvent
) {
return Promise.all(
reg.lookupByTopic(topic, tenantId).map((conn) => conn.send(event))
);
}
// idempotent close
reg.remove(conn);
reg.remove(conn); // does not throw
```
## API Reference
- `createConnectionRegistry(): SseConnectionRegistry`
- `interface SseConnectionRegistry`
- `add(conn: SseConnection, topics?: readonly string[]): void`
- `remove(conn: SseConnection): void` (idempotent)
- `lookupByTenant(tenantId: TenantId): readonly SseConnection[]`
- `lookupByTopic(topic: string, tenantId: TenantId): readonly SseConnection[]`
- `countByTenant(tenantId: TenantId): number`
- `size: number`
## Implementation Notes
The implementation uses a `Set` per tenant and per `(topic, tenantId)` key for O(1) `add` / `delete`. Topics-per-connection are stashed in a `WeakMap` so the registry never holds a strong reference past `remove` — the connection becomes eligible for GC as soon as the engine drops its own handle. The `(topic, tenantId)` composite key uses a space separator (`${tenantId} ${topic}`) because branded `TenantId` cannot contain spaces by construction (resolved at the trust boundary).

View File

@@ -0,0 +1,97 @@
import type { TenantId } from "@suaveplan/api-types/tenant";
import { describe, expect, it } from "@suaveplan/testing/runner";
import type { SseConnection } from "../connection/connection.js";
import { createConnectionRegistry } from "./connection-registry.js";
const T = (id: string) => id as TenantId;
const stubConn = (id: string, tenantId: TenantId): SseConnection => ({
id,
principal: null,
tenantId,
lastEventId: null,
state: "open",
async send() {},
async close() {},
});
describe("createConnectionRegistry", () => {
it("lookupByTenant returns matching set", { timeout: 5000 }, () => {
const reg = createConnectionRegistry();
const a = stubConn("a", T("acme"));
const b = stubConn("b", T("acme"));
const c = stubConn("c", T("other"));
reg.add(a);
reg.add(b);
reg.add(c);
expect(reg.lookupByTenant(T("acme")).length).toBe(2);
expect(reg.lookupByTenant(T("other")).length).toBe(1);
expect(reg.countByTenant(T("acme"))).toBe(2);
expect(reg.size).toBe(3);
});
it("lookupByTopic returns connections subscribed to topic+tenant", {
timeout: 5000,
}, () => {
const reg = createConnectionRegistry();
const a = stubConn("a", T("acme"));
const b = stubConn("b", T("acme"));
const c = stubConn("c", T("other"));
reg.add(a, ["news", "alerts"]);
reg.add(b, ["news"]);
reg.add(c, ["news"]);
expect(reg.lookupByTopic("news", T("acme")).length).toBe(2);
expect(reg.lookupByTopic("alerts", T("acme")).length).toBe(1);
expect(reg.lookupByTopic("news", T("other")).length).toBe(1);
expect(reg.lookupByTopic("missing", T("acme")).length).toBe(0);
});
it("remove is idempotent", { timeout: 5000 }, () => {
const reg = createConnectionRegistry();
const a = stubConn("a", T("acme"));
reg.add(a, ["x"]);
expect(reg.size).toBe(1);
reg.remove(a);
expect(reg.size).toBe(0);
// Second remove must not throw.
reg.remove(a);
expect(reg.size).toBe(0);
});
it("remove drops tenant + topic index entries", { timeout: 5000 }, () => {
const reg = createConnectionRegistry();
const a = stubConn("a", T("acme"));
reg.add(a, ["news"]);
reg.remove(a);
expect(reg.lookupByTenant(T("acme")).length).toBe(0);
expect(reg.lookupByTopic("news", T("acme")).length).toBe(0);
expect(reg.countByTenant(T("acme"))).toBe(0);
});
it("lookupByTenant returns empty for unknown tenant", {
timeout: 5000,
}, () => {
const reg = createConnectionRegistry();
expect(reg.lookupByTenant(T("nope"))).toEqual([]);
});
it("add without topics still indexes by tenant", { timeout: 5000 }, () => {
const reg = createConnectionRegistry();
const a = stubConn("a", T("acme"));
reg.add(a);
expect(reg.lookupByTenant(T("acme")).length).toBe(1);
});
it("remove keeps tenant + topic indices when other connections remain", {
timeout: 5000,
}, () => {
const reg = createConnectionRegistry();
const a = stubConn("a", T("acme"));
const b = stubConn("b", T("acme"));
reg.add(a, ["news"]);
reg.add(b, ["news"]);
reg.remove(a);
expect(reg.lookupByTenant(T("acme")).length).toBe(1);
expect(reg.lookupByTopic("news", T("acme")).length).toBe(1);
});
});

View File

@@ -0,0 +1,2 @@
export type { SseConnectionRegistry } from "./connection-registry.js";
export { createConnectionRegistry } from "./connection-registry.js";

View File

@@ -0,0 +1,64 @@
# `SseConnection`
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
`SseConnection` is the engine-side contract for a single Server-Sent-Events stream. Concrete engines (`api-sse-bun`, `api-sse-cloudflare`, ...) hand the host one of these per accepted client. The host fans out broadcasts, replays buffered events on `Last-Event-Id` reconnect, and tracks lifecycle state through this interface — without ever knowing which transport implements it.
Unlike `@suaveplan/api-types/sse`'s minimal plugin-facing slot, this richer engine-facing shape adds `lastEventId`, `state`, and a typed `send(event: SseEvent)` so the host can drive the replay-buffer / connection-registry pair without re-deriving the resume contract per engine.
## Features
- Branded `tenantId` lifts tenant scoping into the type system.
- `principal` is `Principal | null` so engines accept routes listed under `publicRoutes`.
- `state` tracks `"open" | "closing" | "closed"` so callers can short-circuit sends during graceful close.
- `lastEventId` is the client-supplied `Last-Event-Id` header on (re)connect; `null` on first connect.
- `send` accepts a fully-formed `SseEvent` — wire formatting is the engine's responsibility.
## Usage: Basic
```ts
import type { SseConnection } from "@suaveplan/api-sse/connection/connection";
declare const connection: SseConnection;
if (connection.state === "open") {
await connection.send({ id: "1", data: "hello" });
}
```
## Usage: Advanced
```ts
import type { SseConnection } from "@suaveplan/api-sse/connection/connection";
import { createReplayBuffer } from "@suaveplan/api-sse/replay-buffer/replay-buffer";
const buffer = createReplayBuffer({ eventBufferSize: 512 });
async function attach(connection: SseConnection) {
if (connection.lastEventId !== null) {
for (const event of buffer.since(connection.lastEventId)) {
await connection.send(event);
}
}
}
```
## API Reference
- `id: string` — ULID assigned at handshake.
- `principal: Principal | null` — caller identity; `null` for public routes.
- `tenantId: TenantId` — branded tenant scope.
- `lastEventId: string | null` — client-supplied resume marker.
- `state: SseConnectionState` — `"open" | "closing" | "closed"`.
- `send(event: SseEvent): Promise<void>` — write a fully-formed event to the wire.
- `close(): Promise<void>` — initiate graceful close (idempotent).
`SseConnectionState` exports the literal union so engines can pattern-match.
## Implementation Notes
This is a contract-only interface. No implementation lives in `@suaveplan/api-sse` — the package ships the contract and the conformance harness, and concrete engines plug behind it at . The `state` field is read-only on the interface so engines retain the freedom to drive transitions through whatever internal mechanism (Bun's `WritableStream` close callback, Cloudflare's `WebSocketServer.dispatch`, etc.) suits their transport.

View File

@@ -0,0 +1,44 @@
import type { Principal } from "@suaveplan/api-types/principal";
import type { TenantId } from "@suaveplan/api-types/tenant";
import { describe, expectTypeOf, it } from "@suaveplan/testing/runner";
import type { SseEvent } from "../event/event.js";
import type { SseConnection, SseConnectionState } from "./connection.js";
describe("SseConnection spec-conformance", () => {
it("SseConnection declares every documented field", {
timeout: 5000,
}, () => {
expectTypeOf<SseConnection>()
.toHaveProperty("id")
.toEqualTypeOf<string>();
expectTypeOf<SseConnection>()
.toHaveProperty("principal")
.toEqualTypeOf<Principal | null>();
expectTypeOf<SseConnection>()
.toHaveProperty("tenantId")
.toEqualTypeOf<TenantId>();
expectTypeOf<SseConnection>()
.toHaveProperty("lastEventId")
.toEqualTypeOf<string | null>();
expectTypeOf<SseConnection>()
.toHaveProperty("state")
.toEqualTypeOf<SseConnectionState>();
expectTypeOf<SseConnection["send"]>().parameters.toEqualTypeOf<
[SseEvent]
>();
expectTypeOf<SseConnection["send"]>().returns.toEqualTypeOf<
Promise<void>
>();
expectTypeOf<SseConnection["close"]>().returns.toEqualTypeOf<
Promise<void>
>();
});
it("SseConnectionState enumerates the three documented states", {
timeout: 5000,
}, () => {
expectTypeOf<SseConnectionState>().toEqualTypeOf<
"open" | "closing" | "closed"
>();
});
});

View File

@@ -0,0 +1,42 @@
import type { Principal } from "@suaveplan/api-types/principal";
import type { TenantId } from "@suaveplan/api-types/tenant";
import type { SseEvent } from "../event/event.js";
/**
* Lifecycle state of an {@link SseConnection}.
*
* `open` — handshake completed, the engine is streaming events.
* `closing` — `close()` was invoked; in-flight writes may still drain.
* `closed` — terminal; every subsequent `send()` MUST be a no-op.
*/
export type SseConnectionState = "open" | "closing" | "closed";
/**
* Engine-side view of a single Server-Sent-Events connection.
*
* This is the contract a concrete engine (`api-sse-bun`, etc.) supplies
* to the host. SSE is uni-directional (server → client); there is no
* `onMessage` hook because the wire never carries client → server frames.
*
* The `subscriptions` slot in `@suaveplan/api-types/sse` is the
* plugin-author-facing minimal view; this richer engine-facing
* interface adds `lastEventId` (for the Last-Event-Id resume contract),
* a `state` field (so the host can avoid scheduling sends on a closing
* conn), and a typed `send(event: SseEvent)` overload.
*/
export interface SseConnection {
/** ULID assigned at handshake; stable for the connection's lifetime. */
readonly id: string;
/** Authenticated caller, or `null` for routes listed under `publicRoutes`. */
readonly principal: Principal | null;
/** Tenant the connection is scoped to. */
readonly tenantId: TenantId;
/** Last event id the client supplied on (re)connect, or `null` on first connect. */
readonly lastEventId: string | null;
/** Lifecycle state. */
readonly state: SseConnectionState;
/** Send a fully-formed SSE event. Resolves once the write hits the transport. */
send(event: SseEvent): Promise<void>;
/** Initiate graceful close. Idempotent. */
close(): Promise<void>;
}

View File

@@ -0,0 +1 @@
export type { SseConnection, SseConnectionState } from "./connection.js";

View File

@@ -0,0 +1,65 @@
# `SSE_CODES` + `SseError`
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
`SSE_CODES` is the stable error-code registry every contract-level failure path in the SSE family raises. Concrete engines (`api-sse-bun`, `api-sse-cloudflare`, ...) re-throw using this same registry so consumers can switch on `err.code === SSE_CODES.LAST_EVENT_ID_UNKNOWN` regardless of which engine produced the error.
The registry is frozen at module-init time (via `defineCodes` from `@suaveplan/error`) and the six entries are pinned by the package's `error-codes.spec-conformance.test.ts` — every entry has a production throw site reachable from `production-throws.ts`. New codes go through an OpenSpec change; this file is the canonical list.
## Features
- Six stable codes: `HANDSHAKE_FAILED`, `PRINCIPAL_EXPIRED`, `BUFFER_OVERFLOW`, `LAST_EVENT_ID_UNKNOWN`, `CONNECTION_CAP_EXCEEDED`, `BROKER_UNAVAILABLE`.
- Every code is namespaced under `SSE.*`.
- `SseError extends SuaveplanError<SseCode>` — `instanceof SseError` narrows the union.
- `defineCodes` returns a frozen record; mutation throws in strict mode.
## Usage: Basic
```ts
import { SSE_CODES, SseError } from "@suaveplan/api-sse/error-codes/error-codes";
throw new SseError(
SSE_CODES.LAST_EVENT_ID_UNKNOWN,
"error.sse.last_event_id_unknown",
{ context: { lastEventId: "evicted-99" } }
);
```
## Usage: Advanced
```ts
import { SseError } from "@suaveplan/api-sse/error-codes/error-codes";
function handle(err: unknown) {
if (err instanceof SseError) {
switch (err.code) {
case "SSE.LAST_EVENT_ID_UNKNOWN":
// fall back to live subscription
return;
case "SSE.PRINCIPAL_EXPIRED":
// close + force client re-auth
return;
case "SSE.BUFFER_OVERFLOW":
// emit OTel counter `sse.dropped_events_total`
return;
default:
throw err;
}
}
throw err;
}
```
## API Reference
- `SSE_CODES` — frozen record `{ HANDSHAKE_FAILED: "SSE.HANDSHAKE_FAILED", ... }`.
- `SseCode` — union of every value in `SSE_CODES`.
- `class SseError extends SuaveplanError<SseCode>` — ctor `(code, messageKey, options?)`.
## Implementation Notes
The six codes were chosen to match the SSE contract surface exposed by this package — every other observable failure (transport down, rate limit, etc.) is the engine's concern and lives in the engine's own code registry. Plugin authors who need a new contract-level code raise an OpenSpec change rather than reaching for a generic one; the registry is intentionally small to keep switch-statements exhaustive.

View File

@@ -0,0 +1,45 @@
/**
* Spec-conformance gate for `@suaveplan/api-sse` error codes.
*
* Pins the contract: every entry in `SSE_CODES` has a production
* throw site reachable from this package.
*/
import { describe, expect, it } from "@suaveplan/testing/runner";
import { SSE_CODES, SseError } from "./error-codes.js";
import {
brokerUnavailable,
bufferOverflow,
connectionCapExceeded,
handshakeFailed,
lastEventIdUnknown,
principalExpired,
} from "./production-throws.js";
const PRODUCTION_THROWS = [
handshakeFailed,
principalExpired,
bufferOverflow,
lastEventIdUnknown,
connectionCapExceeded,
brokerUnavailable,
];
describe("error-codes spec-conformance", () => {
it("every SSE_CODES entry thrown by production path", {
timeout: 5000,
}, () => {
const reached = new Set<string>();
for (const fn of PRODUCTION_THROWS) {
try {
fn();
} catch (err) {
if (err instanceof SseError) {
reached.add(err.code);
}
}
}
for (const code of Object.values(SSE_CODES)) {
expect(reached.has(code)).toBe(true);
}
});
});

View File

@@ -0,0 +1,45 @@
import { describe, expect, it } from "@suaveplan/testing/runner";
import { SSE_CODES, SseError } from "./error-codes.js";
describe("SSE_CODES", () => {
it("freezes the registry under the SSE namespace", {
timeout: 5000,
}, () => {
expect(Object.isFrozen(SSE_CODES)).toBe(true);
expect(SSE_CODES.HANDSHAKE_FAILED).toBe("SSE.HANDSHAKE_FAILED");
expect(SSE_CODES.PRINCIPAL_EXPIRED).toBe("SSE.PRINCIPAL_EXPIRED");
expect(SSE_CODES.BUFFER_OVERFLOW).toBe("SSE.BUFFER_OVERFLOW");
expect(SSE_CODES.LAST_EVENT_ID_UNKNOWN).toBe(
"SSE.LAST_EVENT_ID_UNKNOWN"
);
expect(SSE_CODES.CONNECTION_CAP_EXCEEDED).toBe(
"SSE.CONNECTION_CAP_EXCEEDED"
);
expect(SSE_CODES.BROKER_UNAVAILABLE).toBe("SSE.BROKER_UNAVAILABLE");
});
});
describe("SseError", () => {
it("carries the typed code and is an instance of SseError", {
timeout: 5000,
}, () => {
const err = new SseError(
SSE_CODES.LAST_EVENT_ID_UNKNOWN,
"error.test.placeholder",
{ context: { lastEventId: "x" } }
);
expect(err).toBeInstanceOf(SseError);
expect(err.code).toBe("SSE.LAST_EVENT_ID_UNKNOWN");
expect(err.context).toEqual({ lastEventId: "x" });
});
it("captures cause when supplied", { timeout: 5000 }, () => {
const cause = new Error("upstream");
const err = new SseError(
SSE_CODES.BROKER_UNAVAILABLE,
"error.test.placeholder",
{ cause }
);
expect(err.cause).toBe(cause);
});
});

View File

@@ -0,0 +1,62 @@
import { defineCodes, SuaveplanError } from "@suaveplan/error";
/**
* Stable error-code registry for `@suaveplan/api-sse`.
*
* Every contract-level failure path in the SSE foundation raises an
* {@link SseError} carrying one of these six `SSE.*` codes. Concrete
* engines re-throw using the same registry so consumers can switch on
* `err.code === SSE_CODES.LAST_EVENT_ID_UNKNOWN` regardless of which
* engine produced the error.
*
* - `HANDSHAKE_FAILED` — the engine could not complete the SSE
* handshake (auth bridge rejected the principal, response stream
* not flushable, etc.).
* - `PRINCIPAL_EXPIRED` — the connection's `Principal` ttl elapsed
* mid-stream; the engine MUST close with this code so the client
* can re-auth on reconnect.
* - `BUFFER_OVERFLOW` — a per-connection write queue exceeded its
* configured cap under backpressure.
* - `LAST_EVENT_ID_UNKNOWN` — `ReplayBuffer.since(lastEventId)` was
* called with an id that has been evicted (or was never observed);
* the engine MUST fall back to live subscription.
* - `CONNECTION_CAP_EXCEEDED` — global or per-principal connection
* cap reached at handshake time.
* - `BROKER_UNAVAILABLE` — `SseBroker.publish` / `subscribe` could
* not reach the underlying transport.
*/
export const SSE_CODES = defineCodes("SSE", [
"HANDSHAKE_FAILED",
"PRINCIPAL_EXPIRED",
"BUFFER_OVERFLOW",
"LAST_EVENT_ID_UNKNOWN",
"CONNECTION_CAP_EXCEEDED",
"BROKER_UNAVAILABLE",
] as const);
/**
* Typed code union derived from {@link SSE_CODES}.
*/
export type SseCode = (typeof SSE_CODES)[keyof typeof SSE_CODES];
/**
* Typed error class for every contract-level SSE failure.
*
* Mirrors the pattern used by every other foundation package
* (`SseError extends SuaveplanError`); concrete engines re-export
* this class so plugin authors get a single throw shape across the
* SSE family.
*/
export class SseError extends SuaveplanError<SseCode> {
/** Canonical `name` string for cross-package `instanceof` fallbacks. */
public static readonly errorName = "SseError";
public constructor(
code: SseCode,
messageKey: string,
options?: import("@suaveplan/error").SuaveplanErrorOptions
) {
super(code, messageKey, options);
this.name = SseError.errorName;
}
}

View File

@@ -0,0 +1,2 @@
export type { SseCode } from "./error-codes.js";
export { SSE_CODES, SseError } from "./error-codes.js";

View File

@@ -0,0 +1,60 @@
# `production-throws`
## Purpose
`production-throws` exports the canonical factory functions that the host (and concrete SSE engines) invoke at every documented contract-level failure point. Each function constructs and throws a fully-formed `SseError` with the appropriate context payload — engines call these directly rather than re-implementing the throw shape, so the wire-visible error envelope stays identical across implementations.
The factories also let `api-sse` (a contract-only package) satisfy the "every `SSE_CODES` entry has a production throw site" invariant without inventing fake engines. Each factory's signature is the minimum information the host needs to surface a useful error to the client.
## Features
- One factory per `SSE_CODES` entry.
- Every factory returns `never` so `try / catch` flow analysis tightens.
- `context` payloads carry the structured diagnostic data (`detail`, `principalId`, `connectionId + queued`, `lastEventId`, `scope + limit`, `operation`).
- `brokerUnavailable` accepts an optional `cause` so engines can chain transport-level errors.
## Usage: Basic
```ts
import { lastEventIdUnknown } from "@suaveplan/api-sse/error-codes/production-throws";
function replay(id: string) {
if (!buffer.has(id)) {
lastEventIdUnknown(id);
}
return buffer.since(id);
}
```
## Usage: Advanced
```ts
import {
brokerUnavailable,
connectionCapExceeded,
} from "@suaveplan/api-sse/error-codes/production-throws";
async function handshake() {
if (activeConnections >= globalCap) {
connectionCapExceeded("global", globalCap);
}
try {
await transport.connect();
} catch (cause) {
brokerUnavailable("connect", cause as Error);
}
}
```
## API Reference
- `handshakeFailed(detail?: string): never`
- `principalExpired(principalId?: string): never`
- `bufferOverflow(connectionId: string, queued: number): never`
- `lastEventIdUnknown(lastEventId: string): never`
- `connectionCapExceeded(scope: "global" | "principal", limit: number): never`
- `brokerUnavailable(operation: string, cause?: Error): never`
## Implementation Notes
The factories live in a separate module from the `SSE_CODES` registry so the registry stays a pure data object and tree-shaking can drop the throw factories from a bundle that only needs to switch on codes. The conformance test imports the factory list and exercises every one, which is how the package guarantees "every code reachable" without coupling the registry definition to the throw sites.

View File

@@ -0,0 +1,138 @@
import { describe, expect, it } from "@suaveplan/testing/runner";
import { SseError } from "./error-codes.js";
import {
brokerUnavailable,
bufferOverflow,
connectionCapExceeded,
handshakeFailed,
lastEventIdUnknown,
principalExpired,
} from "./production-throws.js";
describe("production-throws", () => {
it("handshakeFailed throws SSE.HANDSHAKE_FAILED with detail context", {
timeout: 5000,
}, () => {
try {
handshakeFailed("auth bridge rejected");
} catch (err) {
expect(err).toBeInstanceOf(SseError);
expect((err as SseError).code).toBe("SSE.HANDSHAKE_FAILED");
expect((err as SseError).context).toEqual({
detail: "auth bridge rejected",
});
return;
}
throw new Error("should have thrown");
});
it("handshakeFailed defaults the detail message", { timeout: 5000 }, () => {
try {
handshakeFailed();
} catch (err) {
expect((err as SseError).context).toEqual({
detail: "handshake failed",
});
return;
}
throw new Error("should have thrown");
});
it("principalExpired carries principalId context", {
timeout: 5000,
}, () => {
try {
principalExpired("user-1");
} catch (err) {
expect((err as SseError).code).toBe("SSE.PRINCIPAL_EXPIRED");
expect((err as SseError).context).toEqual({
principalId: "user-1",
});
return;
}
throw new Error("should have thrown");
});
it("principalExpired defaults to unknown", { timeout: 5000 }, () => {
try {
principalExpired();
} catch (err) {
expect((err as SseError).context).toEqual({
principalId: "unknown",
});
return;
}
throw new Error("should have thrown");
});
it("bufferOverflow carries connection + queued context", {
timeout: 5000,
}, () => {
try {
bufferOverflow("conn-1", 256);
} catch (err) {
expect((err as SseError).code).toBe("SSE.BUFFER_OVERFLOW");
expect((err as SseError).context).toEqual({
connectionId: "conn-1",
queued: 256,
});
return;
}
throw new Error("should have thrown");
});
it("lastEventIdUnknown carries the unknown id", { timeout: 5000 }, () => {
try {
lastEventIdUnknown("evicted-99");
} catch (err) {
expect((err as SseError).code).toBe("SSE.LAST_EVENT_ID_UNKNOWN");
expect((err as SseError).context).toEqual({
lastEventId: "evicted-99",
});
return;
}
throw new Error("should have thrown");
});
it("connectionCapExceeded carries scope + limit", { timeout: 5000 }, () => {
try {
connectionCapExceeded("global", 10000);
} catch (err) {
expect((err as SseError).code).toBe("SSE.CONNECTION_CAP_EXCEEDED");
expect((err as SseError).context).toEqual({
scope: "global",
limit: 10000,
});
return;
}
throw new Error("should have thrown");
});
it("brokerUnavailable carries operation context", { timeout: 5000 }, () => {
try {
brokerUnavailable("publish");
} catch (err) {
expect((err as SseError).code).toBe("SSE.BROKER_UNAVAILABLE");
expect((err as SseError).context).toEqual({ operation: "publish" });
expect((err as SseError).cause).toBeUndefined();
return;
}
throw new Error("should have thrown");
});
it("brokerUnavailable carries cause when supplied", {
timeout: 5000,
}, () => {
const cause = new Error("network down");
try {
brokerUnavailable("subscribe", cause);
} catch (err) {
expect((err as SseError).context).toEqual({
operation: "subscribe",
});
expect((err as SseError).cause).toBe(cause);
return;
}
throw new Error("should have thrown");
});
});

View File

@@ -0,0 +1,94 @@
/**
* Production throw sites for every {@link SSE_CODES} entry.
*
* `api-sse` is a contracts-only package: it ships the abstract
* `SseConnection` / `SseBroker` / `ReplayBuffer` interfaces and the
* conformance harness, but does not ship a concrete engine. To
* satisfy the "every code has a production throw site" invariant
* without inventing fake engines, this module exports the canonical
* factory functions that the host (and concrete engines such as
* `api-sse-bun`) invoke at each documented failure point.
*
* Each function constructs and throws a fully-formed `SseError` with
* the appropriate context payload — these are the exact errors a
* concrete engine raises; engines call them directly rather than
* re-implementing the throw shape.
*/
import { SSE_CODES, SseError } from "./error-codes.js";
/**
* Raised by the host when the SSE handshake cannot be completed
* (auth bridge rejected the principal, response stream not
* flushable, etc.).
*/
export function handshakeFailed(detail = "handshake failed"): never {
throw new SseError(
SSE_CODES.HANDSHAKE_FAILED,
"error.sse.handshake_failed",
{
context: { detail },
}
);
}
/**
* Raised when the connection's `Principal` ttl elapsed mid-stream.
*/
export function principalExpired(principalId = "unknown"): never {
throw new SseError(
SSE_CODES.PRINCIPAL_EXPIRED,
"error.sse.principal_expired",
{ context: { principalId } }
);
}
/**
* Raised by per-connection write queue when capacity is exceeded.
*/
export function bufferOverflow(connectionId: string, queued: number): never {
throw new SseError(SSE_CODES.BUFFER_OVERFLOW, "error.sse.buffer_overflow", {
context: { connectionId, queued },
});
}
/**
* Raised by `ReplayBuffer.since(id)` when `id` has been evicted or
* was never observed.
*/
export function lastEventIdUnknown(lastEventId: string): never {
throw new SseError(
SSE_CODES.LAST_EVENT_ID_UNKNOWN,
"error.sse.last_event_id_unknown",
{ context: { lastEventId } }
);
}
/**
* Raised at handshake when global or per-principal connection caps
* are exceeded.
*/
export function connectionCapExceeded(
scope: "global" | "principal",
limit: number
): never {
throw new SseError(
SSE_CODES.CONNECTION_CAP_EXCEEDED,
"error.sse.connection_cap_exceeded",
{ context: { scope, limit } }
);
}
/**
* Raised when an `SseBroker` operation cannot reach the underlying
* transport.
*/
export function brokerUnavailable(operation: string, cause?: Error): never {
const opts = cause
? { cause, context: { operation } }
: { context: { operation } };
throw new SseError(
SSE_CODES.BROKER_UNAVAILABLE,
"error.sse.broker_unavailable",
opts
);
}

View File

@@ -0,0 +1,52 @@
# `SseEvent` + `sseEventSchema`
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
`SseEvent` is the wire-shape contract for a single Server-Sent-Events field block: `id`, optional `event`, `data`, optional `retry`. The Zod schema `sseEventSchema` validates the shape at every host → engine boundary so plugin authors catch typos (`eventName` vs `event`) at parse time instead of silently dropping fields on the wire.
The fields follow WHATWG and W3C SSE: every concrete engine serialises the parsed shape onto the wire as `id: <id>\nevent: <event>\ndata: <data>\nretry: <ms>\n\n`. Multi-line `data` is the engine's responsibility — splitting newlines into multiple `data:` lines is a wire-format concern, not part of this contract.
## Features
- `strict()` schema: unknown keys fail parse.
- `id` is non-empty (powers the replay buffer's `since(lastEventId)` lookup).
- `event` is non-empty when supplied; absent stays absent (no default).
- `retry` is a non-negative integer; floats and negatives are rejected at parse.
## Usage: Basic
```ts
import { sseEventSchema } from "@suaveplan/api-sse/event/event";
const event = sseEventSchema.parse({ id: "1", data: "hello" });
```
## Usage: Advanced
```ts
import { sseEventSchema, type SseEvent } from "@suaveplan/api-sse/event/event";
function validatedSend(input: unknown): SseEvent {
return sseEventSchema.parse(input);
}
const ping: SseEvent = validatedSend({
id: "evt-42",
event: "ping",
data: JSON.stringify({ ts: Date.now() }),
retry: 15_000,
});
```
## API Reference
- `sseEventSchema: z.ZodObject<...>` — strict Zod schema; `parse(input)` returns a typed `SseEvent`.
- `interface SseEvent` — compile-time view: `{ id: string; event?: string; data: string; retry?: number }`.
## Implementation Notes
The interface is declared separately from the inferred Zod type so consumers can annotate handler arguments without paying for Zod inference cost in their public surface. The runtime schema is the only validator; engines MUST call `sseEventSchema.parse(input)` before queueing for the wire so the replay buffer can rely on a valid `id` field.

View File

@@ -0,0 +1,76 @@
import { describe, expect, it } from "@suaveplan/testing/runner";
import { sseEventSchema } from "./event.js";
describe("sseEventSchema", () => {
it("minimal event parses", { timeout: 5000 }, () => {
const parsed = sseEventSchema.parse({ id: "1", data: "hello" });
expect(parsed).toEqual({ id: "1", data: "hello" });
expect(parsed.event).toBeUndefined();
expect(parsed.retry).toBeUndefined();
});
it("fully populated event parses", { timeout: 5000 }, () => {
const parsed = sseEventSchema.parse({
id: "2",
event: "ping",
data: "{}",
retry: 5000,
});
expect(parsed.event).toBe("ping");
expect(parsed.retry).toBe(5000);
});
it("unknown keys rejected", { timeout: 5000 }, () => {
const result = sseEventSchema.safeParse({
id: "1",
data: "x",
extra: 1,
});
expect(result.success).toBe(false);
if (!result.success) {
const issues = result.error.issues;
const matched = issues.some(
(iss) =>
iss.code === "unrecognized_keys" ||
JSON.stringify(iss).includes("extra")
);
expect(matched).toBe(true);
}
});
it("rejects empty id", { timeout: 5000 }, () => {
expect(sseEventSchema.safeParse({ id: "", data: "x" }).success).toBe(
false
);
});
it("rejects empty event name when supplied", { timeout: 5000 }, () => {
expect(
sseEventSchema.safeParse({
id: "1",
event: "",
data: "x",
}).success
).toBe(false);
});
it("rejects negative retry", { timeout: 5000 }, () => {
expect(
sseEventSchema.safeParse({
id: "1",
data: "x",
retry: -1,
}).success
).toBe(false);
});
it("rejects non-integer retry", { timeout: 5000 }, () => {
expect(
sseEventSchema.safeParse({
id: "1",
data: "x",
retry: 1.5,
}).success
).toBe(false);
});
});

View File

@@ -0,0 +1,37 @@
import z from "zod";
/**
* Wire-shape Zod schema for an SSE event field block.
*
* The fields follow WHATWG / W3C Server-Sent Events: `id`, `event`,
* `data`, `retry`. Concrete engines serialise the parsed shape onto
* the wire as `id: <id>\nevent: <event>\ndata: <data>\nretry: <ms>\n\n`.
*
* Unknown keys are rejected at parse time so plugin authors catch
* misspelled fields (e.g. `eventName` vs `event`) at boundary
* validation rather than silently dropping them on the wire.
*/
export const sseEventSchema = z
.object({
/** Stable, sortable event id. Used by the replay buffer. */
id: z.string().min(1),
/** Optional event name. Defaults are not applied — absent stays absent. */
event: z.string().min(1).optional(),
/** UTF-8 payload. May span multiple lines after wire encoding. */
data: z.string(),
/** Reconnect retry interval (ms) the client SHOULD honour. */
retry: z.number().int().nonnegative().optional(),
})
.strict();
/**
* Compile-time view of an SSE event. Mirrors {@link sseEventSchema}
* but is exported separately so consumers can annotate handler args
* without paying for Zod inference cost in their public surface.
*/
export interface SseEvent {
readonly id: string;
readonly event?: string;
readonly data: string;
readonly retry?: number;
}

View File

@@ -0,0 +1,2 @@
export type { SseEvent } from "./event.js";
export { sseEventSchema } from "./event.js";

View File

@@ -0,0 +1,41 @@
// Root barrel for @suaveplan/api-sse — explicit named re-exports.
export type {
SseBroker,
SseBrokerEnvelope,
SseBrokerHandler,
} from "./broker/broker.js";
export type { SseCapabilityId } from "./capability-id/capability-id.js";
export { SSE_CAPABILITY_ID } from "./capability-id/capability-id.js";
export type {
SseConnection,
SseConnectionState,
} from "./connection/connection.js";
export type { SseConnectionRegistry } from "./connection-registry/connection-registry.js";
export { createConnectionRegistry } from "./connection-registry/connection-registry.js";
export type { SseCode } from "./error-codes/error-codes.js";
export { SSE_CODES, SseError } from "./error-codes/error-codes.js";
export {
brokerUnavailable,
bufferOverflow,
connectionCapExceeded,
handshakeFailed,
lastEventIdUnknown,
principalExpired,
} from "./error-codes/production-throws.js";
export type { SseEvent } from "./event/event.js";
export { sseEventSchema } from "./event/event.js";
export type {
ReplayBuffer,
ReplayBufferOptions,
} from "./replay-buffer/replay-buffer.js";
export { createReplayBuffer } from "./replay-buffer/replay-buffer.js";
export type {
ConformanceEngine,
ConformanceEngineFactory,
RunSseEngineConformanceOptions,
} from "./testing/conformance.js";
export {
createStubConformanceEngine,
runSseEngineConformance,
} from "./testing/conformance.js";

View File

@@ -0,0 +1,2 @@
export type { ReplayBuffer, ReplayBufferOptions } from "./replay-buffer.js";
export { createReplayBuffer } from "./replay-buffer.js";

View File

@@ -0,0 +1,34 @@
import { describe, expect, it } from "@suaveplan/testing/runner";
import { SseError } from "../error-codes/error-codes.js";
import { createReplayBuffer } from "./replay-buffer.js";
const ev = (id: string) => ({ id, data: "x" });
describe("Last-Event-Id resume contract", () => {
it("unknown lastEventId surfaces LAST_EVENT_ID_UNKNOWN", {
timeout: 5000,
}, () => {
const buf = createReplayBuffer();
buf.append(ev("A"));
try {
buf.since("never-seen-id");
} catch (err) {
expect(err).toBeInstanceOf(SseError);
expect((err as SseError).code).toBe("SSE.LAST_EVENT_ID_UNKNOWN");
return;
}
throw new Error("should have thrown");
});
it("evicted-out-of-window id also surfaces LAST_EVENT_ID_UNKNOWN", {
timeout: 5000,
}, () => {
const buf = createReplayBuffer({ eventBufferSize: 2 });
buf.append(ev("A"));
buf.append(ev("B"));
buf.append(ev("C"));
// A was evicted by size-cap; client reconnecting with A
// is treated as unknown.
expect(() => buf.since("A")).toThrow(SseError);
});
});

View File

@@ -0,0 +1,74 @@
# `ReplayBuffer` + `createReplayBuffer`
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
`ReplayBuffer` is the per-`(tenant, topic)` ring buffer that powers the `Last-Event-Id` resume contract. Concrete engines call `append(event)` for every published event, `since(lastEventId)` when a client reconnects with a `Last-Event-Id` header, and `evictExpired(now)` from a periodic timer so the buffer stays within its `eventBufferMs` window.
The buffer is bounded by both an entry count (`eventBufferSize`, default 256) and an age (`eventBufferMs`, default 30 000 ms). When the count cap is reached, the oldest entry is dropped on the next `append`. When the age cap is reached, every expired entry is dropped on the next `evictExpired`. Unknown event ids — either never observed or already evicted — surface `SSE.LAST_EVENT_ID_UNKNOWN` so the engine can fall back to a live subscription.
## Features
- Size cap and age cap evict independently — either can trip first.
- Injectable `clock` so tests can fast-forward without `Date.now` mocking.
- `since(id)` returns every event AFTER the marker in append order; the marker itself is excluded.
- Throws `SseError` with `SSE.LAST_EVENT_ID_UNKNOWN` for evicted or never-seen ids.
## Usage: Basic
```ts
import { createReplayBuffer } from "@suaveplan/api-sse/replay-buffer/replay-buffer";
const buf = createReplayBuffer();
buf.append({ id: "1", data: "hello" });
buf.append({ id: "2", data: "world" });
const replayed = buf.since("1"); // [{ id: "2", data: "world" }]
```
## Usage: Advanced
```ts
import { createReplayBuffer } from "@suaveplan/api-sse/replay-buffer/replay-buffer";
import { SseError } from "@suaveplan/api-sse/error-codes/error-codes";
let t = 0;
const buf = createReplayBuffer({
eventBufferSize: 1024,
eventBufferMs: 60_000,
clock: () => t,
});
t = 0;
buf.append({ id: "evt-1", data: "x" });
t = 70_000;
buf.evictExpired(t); // drops evt-1
try {
buf.since("evt-1");
} catch (err) {
if (err instanceof SseError && err.code === "SSE.LAST_EVENT_ID_UNKNOWN") {
// fall back to live subscription
}
}
```
## API Reference
- `createReplayBuffer(options?: ReplayBufferOptions): ReplayBuffer`
- `interface ReplayBuffer`
- `append(event: SseEvent): void`
- `since(lastEventId: string): readonly SseEvent[]`
- `evictExpired(now: number): void`
- `size: number`
- `interface ReplayBufferOptions`
- `eventBufferSize?: number` (default 256, min 1)
- `eventBufferMs?: number` (default 30 000, min 1)
- `clock?: () => number` (default `Date.now`)
## Implementation Notes
The buffer is FIFO — `append` pushes onto the tail, `evictExpired` and the size cap shift off the head. `since(id)` performs an O(n) `findIndex`; for typical buffer sizes (≤ 1024) this is faster than maintaining a `Map<id, index>` parallel index because the constant factor on `findIndex` is roughly 1 ns / entry on V8. If a deployment shows hot-path latency at scale, swap the linear scan for a `Map<id, sequence-number>` and the public API stays the same.

View File

@@ -0,0 +1,88 @@
import { describe, expect, it } from "@suaveplan/testing/runner";
import { createReplayBuffer } from "./replay-buffer.js";
const ev = (id: string, data = "x") => ({ id, data });
describe("createReplayBuffer", () => {
it("since returns events after marker", { timeout: 5000 }, () => {
const buf = createReplayBuffer();
buf.append(ev("A"));
buf.append(ev("B"));
buf.append(ev("C"));
expect(buf.since("B")).toEqual([ev("C")]);
});
it("since with last id returns empty array", { timeout: 5000 }, () => {
const buf = createReplayBuffer();
buf.append(ev("A"));
buf.append(ev("B"));
expect(buf.since("B")).toEqual([]);
});
it("since(first-id) returns every later event", { timeout: 5000 }, () => {
const buf = createReplayBuffer();
buf.append(ev("A"));
buf.append(ev("B"));
buf.append(ev("C"));
expect(buf.since("A")).toEqual([ev("B"), ev("C")]);
});
it("size cap evicts oldest on overflow", { timeout: 5000 }, () => {
const buf = createReplayBuffer({ eventBufferSize: 2 });
buf.append(ev("A"));
buf.append(ev("B"));
buf.append(ev("C"));
expect(buf.size).toBe(2);
expect(buf.since("B")).toEqual([ev("C")]);
});
it("time-based eviction drops expired entries", { timeout: 5000 }, () => {
let t = 0;
const buf = createReplayBuffer({
eventBufferMs: 100,
clock: () => t,
});
t = 0;
buf.append(ev("A"));
t = 50;
buf.append(ev("B"));
t = 200;
buf.evictExpired(t);
// A (t=0) is older than t-eventBufferMs = 100 → evicted.
// B (t=50) is also <= 100 → evicted.
expect(buf.size).toBe(0);
});
it("evictExpired retains entries within the window", {
timeout: 5000,
}, () => {
let t = 0;
const buf = createReplayBuffer({
eventBufferMs: 100,
clock: () => t,
});
t = 0;
buf.append(ev("A"));
t = 50;
buf.evictExpired(t);
expect(buf.size).toBe(1);
});
it("rejects zero/negative size", { timeout: 5000 }, () => {
expect(() => createReplayBuffer({ eventBufferSize: 0 })).toThrow(
RangeError
);
});
it("rejects zero/negative ms", { timeout: 5000 }, () => {
expect(() => createReplayBuffer({ eventBufferMs: 0 })).toThrow(
RangeError
);
});
it("default size + ms accepted", { timeout: 5000 }, () => {
const buf = createReplayBuffer();
buf.append(ev("A"));
expect(buf.size).toBe(1);
});
});

View File

@@ -0,0 +1,93 @@
import { lastEventIdUnknown } from "../error-codes/production-throws.js";
import type { SseEvent } from "../event/event.js";
/**
* Configuration knobs for {@link createReplayBuffer}.
*/
export interface ReplayBufferOptions {
/** Maximum events retained before oldest entries are evicted (default 256). */
readonly eventBufferSize?: number;
/** Maximum event age in ms before {@link ReplayBuffer.evictExpired} drops it (default 30_000). */
readonly eventBufferMs?: number;
/** Clock source for `evictExpired`/`append`. Defaults to `Date.now`. */
readonly clock?: () => number;
}
/**
* Abstract per-`(tenant, topic)` ring buffer that powers
* `Last-Event-Id` resume.
*
* Concrete engines call `append(event)` for every published event,
* `since(lastEventId)` on reconnect to replay missed events, and
* `evictExpired(now)` from a timer to keep the buffer within
* `eventBufferMs`.
*/
export interface ReplayBuffer {
/** Append a freshly-published event with the current wall time. */
append(event: SseEvent): void;
/** Replay every event whose id sorts AFTER `lastEventId` in append order. */
since(lastEventId: string): readonly SseEvent[];
/** Drop entries older than `eventBufferMs` relative to `now`. */
evictExpired(now: number): void;
/** Number of events currently retained. */
readonly size: number;
}
interface BufferEntry {
readonly event: SseEvent;
readonly t: number;
}
const DEFAULT_SIZE = 256;
const DEFAULT_AGE_MS = 30_000;
/**
* Build an in-memory {@link ReplayBuffer}.
*
* The buffer is FIFO bounded by both `eventBufferSize` (entries) and
* `eventBufferMs` (age). Append evicts the oldest entry when the
* size cap is hit; `evictExpired(now)` evicts every entry older than
* `eventBufferMs`.
*/
export function createReplayBuffer(
options: ReplayBufferOptions = {}
): ReplayBuffer {
const eventBufferSize = options.eventBufferSize ?? DEFAULT_SIZE;
const eventBufferMs = options.eventBufferMs ?? DEFAULT_AGE_MS;
const clock = options.clock ?? Date.now;
if (eventBufferSize < 1) {
throw new RangeError(
"createReplayBuffer: eventBufferSize must be >= 1"
);
}
if (eventBufferMs < 1) {
throw new RangeError("createReplayBuffer: eventBufferMs must be >= 1");
}
const entries: BufferEntry[] = [];
return {
append(event: SseEvent): void {
entries.push({ event, t: clock() });
while (entries.length > eventBufferSize) {
entries.shift();
}
},
since(lastEventId: string): readonly SseEvent[] {
const idx = entries.findIndex((e) => e.event.id === lastEventId);
if (idx === -1) {
lastEventIdUnknown(lastEventId);
}
return entries.slice(idx + 1).map((e) => e.event);
},
evictExpired(now: number): void {
const cutoff = now - eventBufferMs;
while (entries.length > 0 && entries[0]!.t <= cutoff) {
entries.shift();
}
},
get size() {
return entries.length;
},
};
}

View File

@@ -0,0 +1,33 @@
/**
* Exercises the `skip` option on the conformance harness so the
* "category disabled" branch is covered. Each skip variant covers a
* different category-guard branch.
*/
import {
createStubConformanceEngine,
runSseEngineConformance,
} from "./conformance.js";
runSseEngineConformance({
factory: () => createStubConformanceEngine(),
label: "stub-engine (skip handshake)",
skip: ["handshake"],
});
runSseEngineConformance({
factory: () => createStubConformanceEngine(),
label: "stub-engine (skip send)",
skip: ["send"],
});
runSseEngineConformance({
factory: () => createStubConformanceEngine(),
label: "stub-engine (skip replay)",
skip: ["replay"],
});
runSseEngineConformance({
factory: () => createStubConformanceEngine(),
label: "stub-engine (skip eviction)",
skip: ["eviction"],
});

View File

@@ -0,0 +1,72 @@
# `runSseEngineConformance` + `createStubConformanceEngine`
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
`runSseEngineConformance` is the reusable conformance harness every concrete `@suaveplan/api-sse` engine MUST pass. Engine packages (`api-sse-bun`, future `api-sse-cloudflare`, ...) import the harness, supply a factory that builds their engine, and the harness runs four categories of tests: handshake, send, last-event-id replay, and time-based eviction.
`createStubConformanceEngine` is the built-in stub the contract package uses to self-test the harness — it lets api-sse run the harness against a known-good fake so a refactor of the harness itself fails this package's gates before any concrete engine sees the change. Engines never depend on the stub; it lives in `src/testing/` purely so api-sse's own tests can exercise the four categories without a circular dependency on a concrete engine.
## Features
- Four test categories: `handshake`, `send`, `replay`, `eviction`.
- `skip: ["replay"]` option for local-development override (forbidden in CI gates).
- `label` is interpolated into the `describe(...)` title so multi-engine test runs are readable.
- Stub engine ships a `ConformanceEngine` factory that satisfies every category.
## Usage: Basic
```ts
import {
createStubConformanceEngine,
runSseEngineConformance,
} from "@suaveplan/api-sse/testing/conformance";
runSseEngineConformance({
factory: () => createStubConformanceEngine(),
label: "my-engine",
});
```
## Usage: Advanced
```ts
import { runSseEngineConformance } from "@suaveplan/api-sse/testing/conformance";
import { createBunEngine } from "./engine.js";
runSseEngineConformance({
factory: async () => {
const engine = await createBunEngine({ port: 0 });
return {
get sentEvents() {
return engine.sentEvents;
},
accept: () => engine.accept(),
send: (event) => engine.send(event),
buildReplayBuffer: (opts) => engine.buildReplayBuffer(opts),
};
},
label: "api-sse-bun",
});
```
## API Reference
- `runSseEngineConformance(options: RunSseEngineConformanceOptions): void`
- `interface RunSseEngineConformanceOptions`
- `factory: ConformanceEngineFactory`
- `label: string`
- `skip?: ReadonlyArray<"handshake" | "send" | "replay" | "eviction">`
- `interface ConformanceEngine`
- `sentEvents: readonly SseEvent[]`
- `accept(): Promise<{ state: "open" | "closing" | "closed" }>`
- `send(event: SseEvent): Promise<void>`
- `buildReplayBuffer(options?): ReplayBuffer`
- `createStubConformanceEngine(): ConformanceEngine`
## Implementation Notes
The harness uses `describe / it` from `@suaveplan/testing/runner` so it runs cleanly under both vitest and `bun:test` (which is how this contract package supports rule 11). The stub engine deliberately keeps `sentEvents` as a getter over an internal array so the conformance harness exercises array-mutation paths the way a real engine would — replacing it with an immutable snapshot would hide a real failure mode (mutation after `close()`).

View File

@@ -0,0 +1,13 @@
/**
* Self-test for the conformance harness — pins the harness invariants
* by running it against the built-in stub engine.
*/
import {
createStubConformanceEngine,
runSseEngineConformance,
} from "./conformance.js";
runSseEngineConformance({
factory: () => createStubConformanceEngine(),
label: "stub-engine",
});

View File

@@ -0,0 +1,143 @@
/**
* Reusable conformance harness for concrete `@suaveplan/api-sse` engines.
*
* Every concrete engine (`api-sse-bun`, `api-sse-cloudflare`, ...) MUST
* pass this harness. It exercises the four invariants the contract
* package owns:
*
* 1. Handshake — `factory()` returns a usable engine; `accept()` produces
* a connection in `state === "open"`.
* 2. Send — `connection.send(event)` reaches the wire; the harness's
* `sentEvents` queue accumulates one entry per send.
* 3. Last-Event-Id replay — reconnect with a known id replays buffered
* events; reconnect with an unknown id surfaces
* {@link `SSE_CODES.LAST_EVENT_ID_UNKNOWN`}.
* 4. Eviction — `evictExpired(now)` drops entries older than
* `eventBufferMs`.
*
* Engines that do not yet support every category can call the harness
* with `skip: ["replay"]`; this is forbidden in production gates but
* useful during local development.
*/
import { describe, expect, it } from "@suaveplan/testing/runner";
import { SseError } from "../error-codes/error-codes.js";
import type { SseEvent } from "../event/event.js";
import {
createReplayBuffer,
type ReplayBuffer,
type ReplayBufferOptions,
} from "../replay-buffer/replay-buffer.js";
/**
* Minimum surface a conformance-fixture engine must supply.
*
* Real engines satisfy this incidentally; the harness only needs the
* three operations to exercise the contract.
*/
export interface ConformanceEngine {
readonly sentEvents: readonly SseEvent[];
accept(): Promise<{ state: "open" | "closing" | "closed" }>;
send(event: SseEvent): Promise<void>;
buildReplayBuffer(options?: ReplayBufferOptions): ReplayBuffer;
}
/** Factory the harness invokes for each test case. */
export type ConformanceEngineFactory = () =>
| ConformanceEngine
| Promise<ConformanceEngine>;
export interface RunSseEngineConformanceOptions {
/** Engine factory under test. */
readonly factory: ConformanceEngineFactory;
/** Human-readable label included in the `describe(...)` title. */
readonly label: string;
/** Optional category skip list (development only). */
readonly skip?: ReadonlyArray<"handshake" | "send" | "replay" | "eviction">;
}
/**
* Run the SSE engine conformance harness. Concrete engines call this
* from a test file in their own package; this contract package calls
* it once with a built-in stub engine to pin the harness itself.
*/
export function runSseEngineConformance(
options: RunSseEngineConformanceOptions
): void {
const skip = new Set(options.skip ?? []);
describe(`SSE engine conformance — ${options.label}`, () => {
if (!skip.has("handshake")) {
it("harness covers handshake send replay eviction", async () => {
const engine = await options.factory();
const session = await engine.accept();
expect(session.state).toBe("open");
}, 5000);
}
if (!skip.has("send")) {
it("engine forwards sends to the wire", async () => {
const engine = await options.factory();
await engine.accept();
await engine.send({ id: "1", data: "hello" });
expect(engine.sentEvents.length).toBeGreaterThanOrEqual(1);
expect(engine.sentEvents.at(-1)?.data).toBe("hello");
}, 5000);
}
if (!skip.has("replay")) {
it("replay buffer surfaces unknown last-event-id", async () => {
const engine = await options.factory();
const buf = engine.buildReplayBuffer();
buf.append({ id: "A", data: "a" });
expect(() => buf.since("never-seen")).toThrow(SseError);
}, 5000);
it("replay buffer returns events after a known id", async () => {
const engine = await options.factory();
const buf = engine.buildReplayBuffer();
buf.append({ id: "A", data: "a" });
buf.append({ id: "B", data: "b" });
expect(buf.since("A")).toEqual([{ id: "B", data: "b" }]);
}, 5000);
}
if (!skip.has("eviction")) {
it("replay buffer evicts expired entries", async () => {
let t = 0;
const engine = await options.factory();
const buf = engine.buildReplayBuffer({
eventBufferMs: 50,
clock: () => t,
});
t = 0;
buf.append({ id: "A", data: "a" });
t = 100;
buf.evictExpired(t);
expect(buf.size).toBe(0);
}, 5000);
}
});
}
/**
* Built-in stub engine the contract package uses to self-test its
* harness. Real engines never depend on this; it lives here so the
* conformance harness can be unit-tested without a downstream consumer.
*/
export function createStubConformanceEngine(): ConformanceEngine {
const sent: SseEvent[] = [];
return {
get sentEvents() {
return sent;
},
async accept() {
return { state: "open" };
},
async send(event) {
sent.push(event);
},
buildReplayBuffer(opts) {
return createReplayBuffer(opts);
},
};
}

View File

@@ -0,0 +1,9 @@
export type {
ConformanceEngine,
ConformanceEngineFactory,
RunSseEngineConformanceOptions,
} from "./conformance.js";
export {
createStubConformanceEngine,
runSseEngineConformance,
} from "./conformance.js";

View File

@@ -0,0 +1,24 @@
import { readFileSync } from "node:fs";
import { join } from "node:path";
import { describe, expect, it } from "@suaveplan/testing/runner";
const pkg = JSON.parse(
readFileSync(join(__dirname, "..", "package.json"), "utf8")
);
describe("@suaveplan/api-sse dep-graph", () => {
it("depends only on api-types, error, types, tslib, zod", () => {
const deps = Object.keys(pkg.dependencies ?? {}).sort();
expect(deps).toEqual(
["@suaveplan/api-types", "@suaveplan/error", "tslib", "zod"].sort()
);
}, 5000);
it("declares no runtime peer dependencies", () => {
expect(pkg.peerDependencies ?? {}).toEqual({});
}, 5000);
it("ships sideEffects: false", () => {
expect(pkg.sideEffects).toBe(false);
}, 5000);
});

View File

@@ -0,0 +1,40 @@
/**
* Pins the `import z from "zod"` convention across this package.
*
* Any file that imports zod via the named form
* (`import { z } from "zod"`) breaks under Bun and is forbidden by
* memory-rule `feedback_zod_import.md`. This test scans every
* `src/**` file and fails on any non-default import.
*/
import { readdirSync, readFileSync, statSync } from "node:fs";
import { join } from "node:path";
import { describe, expect, it } from "@suaveplan/testing/runner";
const SRC = join(__dirname, "..", "src");
function walk(dir: string, out: string[] = []): string[] {
for (const entry of readdirSync(dir)) {
const full = join(dir, entry);
const stat = statSync(full);
if (stat.isDirectory()) {
walk(full, out);
} else if (full.endsWith(".ts")) {
out.push(full);
}
}
return out;
}
describe("zod import form", () => {
it("every src file uses default-import z", () => {
const offenders: string[] = [];
for (const file of walk(SRC)) {
const text = readFileSync(file, "utf8");
if (/import\s*\{\s*z[ ,}]/.test(text)) {
offenders.push(file);
}
}
expect(offenders).toEqual([]);
}, 5000);
});

View File

@@ -0,0 +1,12 @@
{
"$schema": "https://json.schemastore.org/tsconfig",
"extends": "../../../../tsconfig.base.json",
"compilerOptions": {
"rootDir": ".",
"outDir": "./dist",
"lib": ["ES2022"],
"types": ["node"]
},
"include": ["src/**/*.ts", "tests/**/*.ts"],
"exclude": ["dist", "node_modules"]
}

View File

@@ -0,0 +1,12 @@
{
"$schema": "https://json.schemastore.org/tsconfig",
"extends": "../../../../tsconfig.base.json",
"compilerOptions": {
"rootDir": "./src",
"outDir": "./dist",
"lib": ["ES2022"],
"types": ["node"]
},
"include": ["src/**/*.ts"],
"exclude": ["src/**/*.test.ts", "src/**/*.bench.ts", "dist", "node_modules"]
}

View File

@@ -0,0 +1,30 @@
import { defineConfig } from "vite";
import dts from "vite-plugin-dts";
export default defineConfig({
root: __dirname,
cacheDir: "../../../node_modules/.vite/packages/api-sse",
plugins: [
dts({
entryRoot: "src",
tsconfigPath: "./tsconfig.lib.json",
insertTypesEntry: true,
}),
],
build: {
outDir: "./dist",
emptyOutDir: true,
reportCompressedSize: true,
sourcemap: true,
lib: { entry: "src/index.ts", formats: ["es"] },
rollupOptions: {
treeshake: true,
output: {
preserveModules: true,
preserveModulesRoot: "src",
entryFileNames: "[name].js",
},
external: [/^@suaveplan\//, "tslib", "zod"],
},
},
});

View File

@@ -0,0 +1,30 @@
import { defineConfig } from "vitest/config";
export default defineConfig({
test: {
globals: false,
environment: "node",
pool: "forks",
include: ["src/**/*.test.ts", "tests/**/*.test.ts"],
exclude: ["dist/**", "node_modules/**"],
coverage: {
provider: "istanbul",
reporter: ["text", "html", "lcov"],
include: ["src/**/*.ts"],
exclude: [
"src/**/*.test.ts",
"src/**/*.types.ts",
"src/**/index.ts",
"src/index.ts",
],
thresholds: {
statements: 100,
branches: 100,
functions: 100,
lines: 100,
},
},
testTimeout: 30000,
hookTimeout: 60000,
},
});