aae7f883cd
- EventBus: dumb wire, publish/subscribe, failures surface - Envelope: four schema planes, inject-only headers, content-addressed - TopicLog: owned SQLite ledger, replay + by-type/time/causality - Seed facade: durable=ledger+wire, ephemeral=wire only, fold-for-state - Rung 1 kernel: OELBR loop, DAG plans, durable cancellation tokens - Neuron v0 executable spec source; specs/event-bus.md citizen zero - Standing orders: design-before-code doctrine installed
124 lines
4.4 KiB
TypeScript
124 lines
4.4 KiB
TypeScript
export * as EventBusSchema from "./event-bus-schema"
|
|
|
|
/**
|
|
* The envelope, fully specified. Pure data - no behavior, no transport
|
|
* assumptions. Self-describing: body carries (or references) the schema
|
|
* that decodes it. Headers are an ordered append-only list; inject-only.
|
|
*
|
|
* Schema planes:
|
|
* 0 envelope format itself (pinned root, amended loudly)
|
|
* 1 header values (per-header ref)
|
|
* 2 body contract (type + artifact rules)
|
|
* 3 payload (per message-type; nested objects too)
|
|
*/
|
|
|
|
import { Schema } from "effect"
|
|
|
|
// ---------- Schema reference (used by every plane) ----------
|
|
|
|
/**
|
|
* Content-addressed schema reference. ID is the hash of the schema
|
|
* definition itself; resolvers fetch by hash from a registry or read
|
|
* inline for small/self-contained payloads.
|
|
*/
|
|
export class SchemaRef extends Schema.Class<SchemaRef>("SchemaRef")({
|
|
/** content hash (sha256 of canonical schema definition). */
|
|
id: Schema.String,
|
|
/** Optional inline definition so messages decode with zero registry access. */
|
|
inline: Schema.optional(Schema.String),
|
|
/** Media/format hint, e.g. "json-schema", "effect-schema", "protobuf". */
|
|
format: Schema.optional(Schema.String),
|
|
}) {}
|
|
|
|
// ---------- Header (Plane 1) ----------
|
|
|
|
/**
|
|
* One injected header. Immutable once written.
|
|
* Each header carries its own value-schema reference (Plane 1).
|
|
*/
|
|
export class MessageHeader extends Schema.Class<MessageHeader>("MessageHeader")({
|
|
key: Schema.String,
|
|
value: Schema.String,
|
|
author: Schema.String,
|
|
at: Schema.Number,
|
|
/** Plane 1: schema governing THIS header's value. Resolves dynamically. */
|
|
schema: Schema.optional(SchemaRef),
|
|
}) {}
|
|
|
|
// ---------- Body (Planes 2 + 3) ----------
|
|
|
|
/** Artifacts are part of the record they belong to: refs plus hashes. */
|
|
export class Artifact extends Schema.Class<Artifact>("Artifact")({
|
|
ref: Schema.String,
|
|
hash: Schema.optional(Schema.String),
|
|
kind: Schema.optional(Schema.String),
|
|
}) {}
|
|
|
|
export class Body extends Schema.Class<Body>("Body")({
|
|
/** Event type, e.g. "plan.step.completed". */
|
|
type: Schema.String,
|
|
/** PLANE 3: this message-type's unique payload, JSON-encoded. */
|
|
payloadJSON: Schema.optional(Schema.String),
|
|
/** PLANE 3: schema for the payload shape above. */
|
|
payloadSchema: Schema.optional(SchemaRef),
|
|
/** PLANE 2: schema of the body contract itself. */
|
|
schema: Schema.optional(SchemaRef),
|
|
artifacts: Schema.Array(Artifact),
|
|
}) {}
|
|
|
|
// ---------- Envelope (Plane 0) ----------
|
|
|
|
export const Envelope = Schema.Struct({
|
|
/** Orchestration / topic handle. Durable or ephemeral per Topic. */
|
|
topic: Schema.String,
|
|
/** Position within the topic's append-only sequence (assigned by log). */
|
|
seq: Schema.Number,
|
|
/** Causal spine: seq of the message this one responds to or extends. */
|
|
parentSeq: Schema.optional(Schema.Number),
|
|
/** Identity: hash of canonical form. Two identical messages are one. */
|
|
id: Schema.String,
|
|
/** Who published: token id, orchestrator id, stage name. */
|
|
author: Schema.String,
|
|
/** Epoch ms at publish. */
|
|
at: Schema.Number,
|
|
/** Ordered inject-only context chain. Append-only across every hop. */
|
|
headers: Schema.Array(MessageHeader),
|
|
body: Body,
|
|
})
|
|
export type Envelope = typeof Envelope.Type
|
|
|
|
/** Plane 0 pins itself: the wire format amends loudly or not at all. */
|
|
export const EnvelopeSchemaRef = SchemaRef.make({
|
|
id: "sha256:event-bus-envelope-v1",
|
|
format: "effect-schema",
|
|
})
|
|
|
|
// ---------- Reading rules ----------
|
|
|
|
/** current(headers, key): last-wins fold over injected headers. */
|
|
export function current(headers: ReadonlyArray<MessageHeader>, key: string): MessageHeader | undefined {
|
|
let found: MessageHeader | undefined
|
|
for (const h of headers) if (h.key === key) found = h
|
|
return found
|
|
}
|
|
|
|
/** history(headers, key): full evolution in injection order. */
|
|
export function history(headers: ReadonlyArray<MessageHeader>, key: string): Array<MessageHeader> {
|
|
return headers.filter((h) => h.key === key)
|
|
}
|
|
|
|
// ---------- Topics ----------
|
|
|
|
export const TopicDurability = Schema.Literals(["durable", "ephemeral"])
|
|
|
|
export class Topic extends Schema.Class<Topic>("Topic")({
|
|
id: Schema.String,
|
|
durability: TopicDurability,
|
|
/** Ephemeral only: keep most recent value available to new subscribers. */
|
|
retainLast: Schema.optional(Schema.Boolean),
|
|
/** Ephemeral only: auto-dissolve after this many ms idle. */
|
|
ttlMs: Schema.optional(Schema.Number),
|
|
author: Schema.String,
|
|
at: Schema.Number,
|
|
}) {}
|