Initial Commit
This commit is contained in:
@@ -0,0 +1,7 @@
|
||||
export {
|
||||
DEFAULT_MAX_HISTORY_PER_TOPIC,
|
||||
type InMemoryEventsBusOptions,
|
||||
inMemoryEventsBusOptionsSchema,
|
||||
type ResolvedInMemoryEventsBusOptions,
|
||||
resolveInMemoryEventsBusOptions,
|
||||
} from "./options.js";
|
||||
@@ -0,0 +1,80 @@
|
||||
# `InMemoryEventsBusOptions`
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Purpose
|
||||
|
||||
The options module is the single boundary at which user-supplied
|
||||
configuration enters the in-memory events bus. It owns the Zod schema,
|
||||
the default for `maxHistoryPerTopic`, and the
|
||||
`resolveInMemoryEventsBusOptions` helper that turns a partial caller
|
||||
input into a fully-resolved internal options object. Keeping resolution
|
||||
out of the factory avoids re-checking defaults on every internal call
|
||||
and keeps the public Zod-validation surface in one place.
|
||||
|
||||
## Features
|
||||
|
||||
- Strict schema (`.strict()`) — typos in the options object surface as
|
||||
`EVENTS_IN_MEMORY.INVALID_OPTIONS` rather than being silently
|
||||
dropped. This is the difference between "the cap I set was ignored"
|
||||
and "the cap key was misspelled" — the test author needs to know.
|
||||
- Default `maxHistoryPerTopic = 1000` matches the design doc's
|
||||
"guard against runaway tests" guidance — high enough for realistic
|
||||
test suites, low enough that an accidental `while (true) publish(...)`
|
||||
fails loudly.
|
||||
- Public Zod-failures are repackaged as `EventsInMemoryError`. Callers
|
||||
catch the package's own error type; `error.cause` still carries the
|
||||
raw `z.ZodError` for forensic logs.
|
||||
- `ResolvedInMemoryEventsBusOptions` exposes a concrete (non-optional)
|
||||
shape downstream code can branch on without re-applying defaults.
|
||||
|
||||
## Usage: Basic
|
||||
|
||||
```ts
|
||||
import { resolveInMemoryEventsBusOptions } from "@suaveplan/api-events-in-memory/options";
|
||||
|
||||
const resolved = resolveInMemoryEventsBusOptions(undefined);
|
||||
console.log(resolved.maxHistoryPerTopic); // 1000
|
||||
```
|
||||
|
||||
## Usage: Advanced
|
||||
|
||||
Combine a parsed JSON config blob with the schema directly when the
|
||||
caller wants to surface validation issues at file-read time rather than
|
||||
at bus-construction time:
|
||||
|
||||
```ts
|
||||
import {
|
||||
inMemoryEventsBusOptionsSchema,
|
||||
resolveInMemoryEventsBusOptions,
|
||||
} from "@suaveplan/api-events-in-memory/options";
|
||||
|
||||
const fromFile: unknown = JSON.parse(readFileSync("./bus.json", "utf8"));
|
||||
const result = inMemoryEventsBusOptionsSchema.safeParse(fromFile);
|
||||
if (!result.success) {
|
||||
throw new Error(`bus.json failed validation: ${result.error.message}`);
|
||||
}
|
||||
const resolved = resolveInMemoryEventsBusOptions(result.data);
|
||||
```
|
||||
|
||||
`maxHistoryPerTopic` must be a positive integer; `0` and negatives are
|
||||
rejected so callers can't accidentally disable history entirely.
|
||||
|
||||
## API Reference
|
||||
|
||||
- `DEFAULT_MAX_HISTORY_PER_TOPIC: 1000` — applied when callers omit the field.
|
||||
- `InMemoryEventsBusOptions` — interface with `maxHistoryPerTopic?: number`.
|
||||
- `inMemoryEventsBusOptionsSchema` — `z.ZodObject` with `.strict()`.
|
||||
- `ResolvedInMemoryEventsBusOptions` — internal resolved shape (`maxHistoryPerTopic: number`).
|
||||
- `resolveInMemoryEventsBusOptions(raw?)` — parses, applies defaults, raises `EventsInMemoryError(INVALID_OPTIONS)` on failure.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The schema rejects extra keys with `.strict()` because the alternative
|
||||
("permissive, ignore unknown") empirically masks typos for months
|
||||
before anyone notices. The repackaging-to-`EventsInMemoryError` step is
|
||||
the boundary at which the package commits to its own error namespace —
|
||||
once a Zod error has crossed `resolveInMemoryEventsBusOptions`, no
|
||||
caller should ever see a raw `z.ZodError` again.
|
||||
@@ -0,0 +1,96 @@
|
||||
import { describe, expect, it } from "@suaveplan/testing/runner";
|
||||
import {
|
||||
EVENTS_IN_MEMORY_CODES,
|
||||
EventsInMemoryError,
|
||||
} from "../error-codes/error-codes.js";
|
||||
import {
|
||||
DEFAULT_MAX_HISTORY_PER_TOPIC,
|
||||
inMemoryEventsBusOptionsSchema,
|
||||
resolveInMemoryEventsBusOptions,
|
||||
} from "./options.js";
|
||||
|
||||
describe("resolveInMemoryEventsBusOptions", () => {
|
||||
it("applies the default cap when no options are passed", {
|
||||
timeout: 5000,
|
||||
}, () => {
|
||||
const resolved = resolveInMemoryEventsBusOptions(undefined);
|
||||
expect(resolved.maxHistoryPerTopic).toBe(DEFAULT_MAX_HISTORY_PER_TOPIC);
|
||||
});
|
||||
|
||||
it("applies the default when an empty object is passed", {
|
||||
timeout: 5000,
|
||||
}, () => {
|
||||
const resolved = resolveInMemoryEventsBusOptions({});
|
||||
expect(resolved.maxHistoryPerTopic).toBe(DEFAULT_MAX_HISTORY_PER_TOPIC);
|
||||
});
|
||||
|
||||
it("preserves explicit maxHistoryPerTopic", { timeout: 5000 }, () => {
|
||||
const resolved = resolveInMemoryEventsBusOptions({
|
||||
maxHistoryPerTopic: 42,
|
||||
});
|
||||
expect(resolved.maxHistoryPerTopic).toBe(42);
|
||||
});
|
||||
|
||||
it("raises INVALID_OPTIONS for negative caps", { timeout: 5000 }, () => {
|
||||
try {
|
||||
resolveInMemoryEventsBusOptions({
|
||||
maxHistoryPerTopic: -1,
|
||||
});
|
||||
throw new Error("should not reach");
|
||||
} catch (error) {
|
||||
expect(error).toBeInstanceOf(EventsInMemoryError);
|
||||
expect((error as EventsInMemoryError).code).toBe(
|
||||
EVENTS_IN_MEMORY_CODES.INVALID_OPTIONS
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
it("raises INVALID_OPTIONS for zero caps", { timeout: 5000 }, () => {
|
||||
try {
|
||||
resolveInMemoryEventsBusOptions({
|
||||
maxHistoryPerTopic: 0,
|
||||
});
|
||||
throw new Error("should not reach");
|
||||
} catch (error) {
|
||||
expect(error).toBeInstanceOf(EventsInMemoryError);
|
||||
}
|
||||
});
|
||||
|
||||
it("raises INVALID_OPTIONS for non-integer caps", { timeout: 5000 }, () => {
|
||||
try {
|
||||
resolveInMemoryEventsBusOptions({
|
||||
maxHistoryPerTopic: 1.5,
|
||||
});
|
||||
throw new Error("should not reach");
|
||||
} catch (error) {
|
||||
expect(error).toBeInstanceOf(EventsInMemoryError);
|
||||
}
|
||||
});
|
||||
|
||||
it("raises INVALID_OPTIONS for unknown keys (strict schema)", {
|
||||
timeout: 5000,
|
||||
}, () => {
|
||||
try {
|
||||
resolveInMemoryEventsBusOptions({
|
||||
typoKey: 10,
|
||||
} as unknown as Parameters<
|
||||
typeof resolveInMemoryEventsBusOptions
|
||||
>[0]);
|
||||
throw new Error("should not reach");
|
||||
} catch (error) {
|
||||
expect(error).toBeInstanceOf(EventsInMemoryError);
|
||||
expect((error as EventsInMemoryError).code).toBe(
|
||||
EVENTS_IN_MEMORY_CODES.INVALID_OPTIONS
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
it("schema parses positive integer caps directly", {
|
||||
timeout: 5000,
|
||||
}, () => {
|
||||
const parsed = inMemoryEventsBusOptionsSchema.parse({
|
||||
maxHistoryPerTopic: 5,
|
||||
});
|
||||
expect(parsed.maxHistoryPerTopic).toBe(5);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,66 @@
|
||||
import z from "zod";
|
||||
import {
|
||||
EVENTS_IN_MEMORY_CODES,
|
||||
EventsInMemoryError,
|
||||
} from "../error-codes/error-codes.js";
|
||||
|
||||
/**
|
||||
* Default upper bound for recorded payloads per topic. Guards against
|
||||
* runaway tests that publish in a loop and never call `reset()`. 1000
|
||||
* matches the design doc's "guard against runaway tests" guidance.
|
||||
*/
|
||||
export const DEFAULT_MAX_HISTORY_PER_TOPIC = 1000;
|
||||
|
||||
/**
|
||||
* Options handed to {@link createInMemoryEventsBus}.
|
||||
*
|
||||
* `maxHistoryPerTopic` caps the recorded payload count per topic; when
|
||||
* a `publish` would exceed it the bus throws
|
||||
* `EVENTS_IN_MEMORY.HISTORY_OVERFLOW`. The default is high enough for
|
||||
* realistic tests but low enough that pathological loops fail fast.
|
||||
*/
|
||||
export interface InMemoryEventsBusOptions {
|
||||
readonly maxHistoryPerTopic?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Strict-shape Zod schema for {@link InMemoryEventsBusOptions}. Extra
|
||||
* keys are rejected at the boundary so typos surface as
|
||||
* `EVENTS_IN_MEMORY.INVALID_OPTIONS` rather than silently being
|
||||
* ignored.
|
||||
*/
|
||||
export const inMemoryEventsBusOptionsSchema = z
|
||||
.object({
|
||||
maxHistoryPerTopic: z.number().int().positive().optional(),
|
||||
})
|
||||
.strict();
|
||||
|
||||
/**
|
||||
* Internal resolved shape. Every optional field has a concrete value so
|
||||
* the implementation never has to re-check.
|
||||
*/
|
||||
export interface ResolvedInMemoryEventsBusOptions {
|
||||
readonly maxHistoryPerTopic: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse + apply defaults. Any Zod failure is repackaged as
|
||||
* `EVENTS_IN_MEMORY.INVALID_OPTIONS` so callers see the package's own
|
||||
* error type rather than a raw `z.ZodError`.
|
||||
*/
|
||||
export function resolveInMemoryEventsBusOptions(
|
||||
raw: InMemoryEventsBusOptions | undefined
|
||||
): ResolvedInMemoryEventsBusOptions {
|
||||
const parsed = inMemoryEventsBusOptionsSchema.safeParse(raw ?? {});
|
||||
if (!parsed.success) {
|
||||
throw new EventsInMemoryError(
|
||||
EVENTS_IN_MEMORY_CODES.INVALID_OPTIONS,
|
||||
"error.events_in_memory.invalid_options",
|
||||
{ cause: parsed.error }
|
||||
);
|
||||
}
|
||||
return {
|
||||
maxHistoryPerTopic:
|
||||
parsed.data.maxHistoryPerTopic ?? DEFAULT_MAX_HISTORY_PER_TOPIC,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user