@spfn/core 0.3.0-beta.17 → 0.3.0-beta.19
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/codegen/index.d.ts +20 -0
- package/dist/codegen/index.js +68 -10
- package/dist/codegen/index.js.map +1 -1
- package/dist/contract/index.d.ts +37 -15
- package/dist/contract/index.js +147 -23
- package/dist/contract/index.js.map +1 -1
- package/dist/event/index.d.ts +4 -3
- package/dist/event/index.js +32 -1
- package/dist/event/index.js.map +1 -1
- package/dist/event/sse/client.d.ts +3 -2
- package/dist/event/sse/index.d.ts +5 -4
- package/dist/event/ws/client.d.ts +3 -2
- package/dist/event/ws/index.d.ts +4 -3
- package/dist/nextjs/index.d.ts +17 -4
- package/dist/nextjs/index.js +85 -48
- package/dist/nextjs/index.js.map +1 -1
- package/dist/nextjs/server.d.ts +1 -1
- package/dist/ops/index.d.ts +1 -1
- package/dist/{token-manager-BT5EnUAR.d.ts → router-CysBmgv3.d.ts} +81 -127
- package/dist/server/index.d.ts +4 -3
- package/dist/server/index.js +26 -2
- package/dist/server/index.js.map +1 -1
- package/dist/token-manager-Ckjt2-qq.d.ts +122 -0
- package/dist/{types-ClQVomgV.d.ts → types-B4DuGCyM.d.ts} +42 -3
- package/dist/{types-ZQODsBft.d.ts → types-B8zAxFsE.d.ts} +2 -1
- package/dist/{types-QoyjaJWw.d.ts → types-CxhPH4s_.d.ts} +2 -2
- package/dist/{types-2AbaW4Ie.d.ts → types-W4u_S0lF.d.ts} +4 -3
- package/package.json +1 -1
- package/src/codegen/README.md +11 -2
- package/src/contract/README.md +113 -5
- package/src/event/README.md +48 -0
- package/src/nextjs/README.md +39 -7
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/contract/collect.ts","../../src/contract/stable-json.ts","../../src/contract/compare.ts","../../src/contract/snapshot.ts","../../src/contract/usage.ts","../../src/contract/check.ts"],"names":["REQUEST_SECTIONS","usageDir","readdirSync","join","readFileSync"],"mappings":"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","file":"index.js","sourcesContent":["/**\n * Contract Collection\n *\n * Walks a loaded router and turns every route carrying `.contract()` into a\n * contract operation.\n *\n * The router is loaded and walked rather than parsed from source. Real routes\n * build their schemas from imported values — `EmailSchema`, `FileSchema()`,\n * `KEY_DEVICE_NAME_MAX_LENGTH` — which a source parser cannot resolve. Loading\n * costs a module import and no infrastructure: route modules have no\n * import-time side effects, so nothing here opens a database connection.\n */\n\nimport type { RouteDef } from '../route/route-builder';\nimport type { RouteInput } from '../route/route-input';\nimport type { Router } from '../route/router';\nimport type { ContractDocument, ContractOperation, ContractRequest, JsonSchema } from './types';\n\n/** Thrown when the router cannot produce a contract at all. */\nexport class ContractCollectionError extends Error\n{\n constructor(message: string)\n {\n super(message);\n this.name = 'ContractCollectionError';\n }\n}\n\nfunction isRouter(value: unknown): value is Router<any>\n{\n return value !== null\n && typeof value === 'object'\n && 'routes' in value\n && '_routes' in value;\n}\n\nfunction isRouteDef(value: unknown): value is RouteDef<any>\n{\n return value !== null\n && typeof value === 'object'\n && 'handler' in value;\n}\n\n/**\n * Strip TypeBox's symbol-keyed metadata and hand back plain JSON.\n *\n * The symbols carry no information the wire shape depends on, and they cannot\n * survive the round trip to a committed file.\n */\nfunction toJsonSchema(schema: unknown): JsonSchema\n{\n return JSON.parse(JSON.stringify(schema)) as JsonSchema;\n}\n\n/**\n * `formData` is absent on purpose — a contracted route carrying it is refused\n * before it gets here (see `assertNoFormData`). Emitting its schema alongside\n * the JSON sections described a multipart route as if it were JSON-shaped, and a\n * generated client built from that reads as correct right up to the request.\n */\nconst REQUEST_SECTIONS = ['params', 'query', 'body', 'headers', 'cookies'] as const;\n\nfunction toContractRequest(input: RouteInput | undefined): ContractRequest\n{\n const request: ContractRequest = {};\n\n if (!input)\n {\n return request;\n }\n\n for (const section of REQUEST_SECTIONS)\n {\n const schema = input[section];\n if (schema)\n {\n request[section] = toJsonSchema(schema);\n }\n }\n\n return request;\n}\n\ninterface Found\n{\n operation: ContractOperation;\n\n /** Where the route was found, for a collision message. */\n trail: string;\n}\n\nfunction visitRouter(router: Router<any>, trail: string[], found: Map<string, Found>): void\n{\n for (const [name, entry] of Object.entries(router.routes))\n {\n if (isRouter(entry))\n {\n visitRouter(entry, [...trail, name], found);\n continue;\n }\n\n if (!isRouteDef(entry))\n {\n continue;\n }\n\n const routeDef = entry as RouteDef<any>;\n if (!routeDef.contract)\n {\n continue;\n }\n\n addOperation(name, routeDef, [...trail, name], found);\n }\n\n for (const packageRouter of router._packageRouters ?? [])\n {\n visitRouter(packageRouter, [...trail, '(package)'], found);\n }\n}\n\n/**\n * Refuse a contracted route that takes multipart form data.\n *\n * Multipart is a transport-format problem, not a type problem: the contract's\n * grammar describes JSON values, and a file part has no spelling in it. The\n * refusal is loud rather than a quiet omission — a contract that silently\n * dropped the section would still claim to describe the operation, and an app\n * generated from it would break against the running server instead of at build.\n */\nfunction assertNoFormData(where: string, routeDef: RouteDef<any>): void\n{\n const input = routeDef.input as RouteInput | undefined;\n const interceptor = routeDef.interceptor as RouteInput | undefined;\n const section = input?.formData ? 'input' : interceptor?.formData ? 'interceptor' : undefined;\n\n if (!section)\n {\n return;\n }\n\n throw new ContractCollectionError(\n `Contracted route \"${where}\" declares ${section}.formData, which a contract cannot describe. `\n + 'The contract grammar covers JSON values, and multipart carries file parts that have no spelling in it. '\n + 'Drop .contract() from this route, or move the operation to a JSON body.',\n );\n}\n\nfunction addOperation(name: string, routeDef: RouteDef<any>, trail: string[], found: Map<string, Found>): void\n{\n const where = trail.join('.');\n const contract = routeDef.contract!;\n\n assertNoFormData(where, routeDef);\n\n if (!routeDef.method || !routeDef.path)\n {\n throw new ContractCollectionError(\n `Contracted route \"${where}\" has no method or path. `\n + 'A contract describes an operation on the wire, so both are required.',\n );\n }\n\n if (!contract.since)\n {\n throw new ContractCollectionError(\n `Contracted route \"${where}\" has no \"since\" version. `\n + 'The version an operation first appeared in is part of the promise.',\n );\n }\n\n if (!contract.response)\n {\n throw new ContractCollectionError(\n `Contracted route \"${where}\" declares no response schema. `\n + 'An operation with no body declares Type.Null().',\n );\n }\n\n const existing = found.get(name);\n if (existing)\n {\n throw new ContractCollectionError(\n `Two contracted routes are both named \"${name}\" (${existing.trail} and ${where}). `\n + 'An operation is identified by its name across versions, so names must be unique.',\n );\n }\n\n found.set(name, {\n trail: where,\n operation: {\n name,\n method: routeDef.method,\n path: routeDef.path,\n since: contract.since,\n auth: contract.auth ?? 'none',\n requiresSession: contract.requiresSession ?? false,\n ...(contract.deprecatedIn ? { deprecatedIn: contract.deprecatedIn } : {}),\n ...(contract.removedIn ? { removedIn: contract.removedIn } : {}),\n request: toContractRequest(routeDef.input),\n interceptor: toContractRequest(routeDef.interceptor),\n response: toJsonSchema(contract.response),\n },\n });\n}\n\n/**\n * Build the contract document from a loaded router.\n *\n * Routes without `.contract()` are skipped — they are not part of the promise.\n */\nexport function collectContractDocument(router: Router<any>): ContractDocument\n{\n const found = new Map<string, Found>();\n visitRouter(router, ['router'], found);\n\n const operations = [...found.values()]\n .map(entry => entry.operation)\n .sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0));\n\n return {\n documentVersion: 1,\n ...(router._contractVersion ? { contractVersion: router._contractVersion } : {}),\n compatibilityPolicy: 'perOperation',\n operations,\n };\n}\n","/**\n * Deterministic JSON\n *\n * The generated contract is committed and compared against what the generator\n * produces. If key order moved between runs, \"the committed file differs from\n * the generated one\" would flicker and the check would train people to ignore\n * it. So every emitted document is written with its object keys sorted, and the\n * digest that pins a released snapshot is taken over the same ordering with no\n * whitespace — formatting choices never move a digest.\n */\n\nimport { createHash } from 'node:crypto';\n\n/** Recursively sort object keys and drop `undefined` members. */\nexport function canonicalize(value: unknown): unknown\n{\n if (Array.isArray(value))\n {\n return value.map(canonicalize);\n }\n\n if (value === null || typeof value !== 'object')\n {\n return value;\n }\n\n const source = value as Record<string, unknown>;\n const sorted: Record<string, unknown> = {};\n\n for (const key of Object.keys(source).sort())\n {\n if (source[key] === undefined)\n {\n continue;\n }\n\n sorted[key] = canonicalize(source[key]);\n }\n\n return sorted;\n}\n\n/** Compact canonical encoding — what a digest is taken over. */\nexport function stableStringify(value: unknown): string\n{\n return JSON.stringify(canonicalize(value));\n}\n\n/** Indented canonical encoding — what gets written to disk and reviewed. */\nexport function stableStringifyPretty(value: unknown): string\n{\n return `${JSON.stringify(canonicalize(value), null, 4)}\\n`;\n}\n\n/** SHA-256 of the compact canonical encoding, lowercase hex. */\nexport function stableDigest(value: unknown): string\n{\n return createHash('sha256').update(stableStringify(value), 'utf8').digest('hex');\n}\n","/**\n * Backward-compatibility comparison\n *\n * Compares the contract a build produces against the newest released snapshot.\n *\n * The rule that shapes everything here: **optional runs in opposite directions\n * on the two sides.** A request is safe when the server grows more tolerant —\n * a new optional field, a dropped field, a requirement relaxed. A response is\n * safe when the server grows more certain — a new field, a value that used to\n * be optional now always present. Collapsing both into one rule would\n * necessarily get one of them backwards.\n *\n * | change | request | response |\n * |-------------------------|---------|----------|\n * | field added (optional) | pass | pass |\n * | field added (required) | refuse | pass |\n * | field removed | pass | refuse |\n * | required → optional | pass | refuse |\n * | optional → required | refuse | pass |\n * | type changed | refuse | refuse |\n */\n\nimport { stableStringify } from './stable-json';\nimport type {\n ContractDocument,\n ContractOperation,\n ContractRequest,\n ContractViolation,\n JsonSchema,\n} from './types';\n\n/** Which side of the wire a schema sits on. The direction optional runs. */\ntype Side = 'request' | 'response';\n\nexport interface DocumentComparison\n{\n violations: ContractViolation[];\n\n /**\n * Operations present in the baseline and gone from the current contract.\n *\n * Removal is not decided here: whether an operation may go depends on\n * whether any released app still calls it, which is a separate check.\n */\n removedOperations: string[];\n}\n\nconst REQUEST_SECTIONS = ['params', 'query', 'body', 'formData', 'headers', 'cookies'] as const;\n\n/**\n * The schema's own keywords, with the structural ones removed.\n *\n * Everything left — `type`, `format`, `enum`, `anyOf`, `minLength` — is\n * compared verbatim, so a narrowed constraint counts as a type change. That\n * refuses more than the table's \"type changed\" row strictly requires, and it\n * refuses in the recoverable direction: a build that stops is fixed by cutting\n * a new contract version, while a break that passes reaches a shipped app.\n */\nfunction leafSignature(schema: JsonSchema): string\n{\n const { properties, required, items, ...rest } = schema;\n\n return stableStringify(rest);\n}\n\nfunction propertiesOf(schema: JsonSchema): Record<string, JsonSchema>\n{\n const properties = schema.properties;\n\n if (!properties || typeof properties !== 'object')\n {\n return {};\n }\n\n return properties as Record<string, JsonSchema>;\n}\n\nfunction requiredOf(schema: JsonSchema): Set<string>\n{\n const required = schema.required;\n\n if (!Array.isArray(required))\n {\n return new Set();\n }\n\n return new Set(required.filter((name): name is string => typeof name === 'string'));\n}\n\n/**\n * Would a client that sends nothing here now be refused?\n *\n * Only this level counts. A required field nested inside an optional object is\n * required *if* that object is sent, and a client that never sends it is fine.\n */\nfunction requiresClientToSend(schema: JsonSchema): boolean\n{\n if (schema.type !== 'object')\n {\n return true;\n }\n\n return requiredOf(schema).size > 0;\n}\n\ninterface CompareContext\n{\n operation: string;\n side: Side;\n violations: ContractViolation[];\n}\n\nfunction report(\n ctx: CompareContext,\n kind: ContractViolation['kind'],\n location: string,\n detail: string,\n): void\n{\n ctx.violations.push({ kind, operation: ctx.operation, location, detail });\n}\n\n/**\n * Compare one schema position across versions.\n *\n * `before`/`after` may be absent: a whole request section can appear or vanish,\n * and so can a nested field.\n */\nfunction compareSchema(\n before: JsonSchema | undefined,\n after: JsonSchema | undefined,\n location: string,\n ctx: CompareContext,\n): void\n{\n if (!before && !after)\n {\n return;\n }\n\n if (before && !after)\n {\n if (ctx.side === 'response')\n {\n report(ctx, 'response.field-removed', location, 'the response no longer carries this, and old clients read it');\n }\n\n // A request the server stopped reading is a request old clients may still send.\n return;\n }\n\n if (!before && after)\n {\n if (ctx.side === 'request' && requiresClientToSend(after))\n {\n report(\n ctx,\n 'request.required-field-added',\n location,\n 'this is new and mandatory, so every already-released client is refused',\n );\n }\n\n // A response that carries more is a response old clients ignore the rest of.\n return;\n }\n\n const from = before!;\n const to = after!;\n\n if (leafSignature(from) !== leafSignature(to))\n {\n report(\n ctx,\n ctx.side === 'request' ? 'request.type-changed' : 'response.type-changed',\n location,\n `the declared type changed: ${leafSignature(from)} → ${leafSignature(to)}`,\n );\n\n return;\n }\n\n compareProperties(from, to, location, ctx);\n compareSchema(\n from.items as JsonSchema | undefined,\n to.items as JsonSchema | undefined,\n `${location}[]`,\n ctx,\n );\n}\n\nfunction compareProperties(from: JsonSchema, to: JsonSchema, location: string, ctx: CompareContext): void\n{\n const beforeProperties = propertiesOf(from);\n const afterProperties = propertiesOf(to);\n const beforeRequired = requiredOf(from);\n const afterRequired = requiredOf(to);\n\n for (const [name, beforeSchema] of Object.entries(beforeProperties))\n {\n const where = `${location}.${name}`;\n const afterSchema = afterProperties[name];\n\n compareSchema(beforeSchema, afterSchema, where, ctx);\n\n if (!afterSchema)\n {\n continue;\n }\n\n const wasRequired = beforeRequired.has(name);\n const isRequired = afterRequired.has(name);\n\n if (ctx.side === 'request' && !wasRequired && isRequired)\n {\n report(ctx, 'request.field-became-required', where, 'clients that never sent this are now refused');\n }\n\n if (ctx.side === 'response' && wasRequired && !isRequired)\n {\n report(ctx, 'response.field-became-optional', where, 'clients that counted on this always arriving now break');\n }\n }\n\n // An added field is judged at its own level only. Optional means an old\n // client that omits it still passes, whatever the field contains.\n for (const name of Object.keys(afterProperties))\n {\n if (beforeProperties[name])\n {\n continue;\n }\n\n if (ctx.side === 'request' && afterRequired.has(name))\n {\n report(\n ctx,\n 'request.required-field-added',\n `${location}.${name}`,\n 'this is new and mandatory, so every already-released client is refused',\n );\n }\n }\n}\n\nfunction compareRequestSections(\n before: ContractRequest,\n after: ContractRequest,\n prefix: string,\n ctx: CompareContext,\n): void\n{\n for (const section of REQUEST_SECTIONS)\n {\n compareSchema(before[section], after[section], `${prefix}.${section}`, ctx);\n }\n}\n\n/** Compare one operation that exists on both sides. */\nexport function compareOperation(before: ContractOperation, after: ContractOperation): ContractViolation[]\n{\n const violations: ContractViolation[] = [];\n\n if (before.path !== after.path)\n {\n violations.push({\n kind: 'operation.path-changed',\n operation: before.name,\n detail: `path moved ${before.path} → ${after.path}; released clients call the old one`,\n });\n }\n\n if (before.method !== after.method)\n {\n violations.push({\n kind: 'operation.method-changed',\n operation: before.name,\n detail: `method changed ${before.method} → ${after.method}; released clients send the old one`,\n });\n }\n\n const requestCtx: CompareContext = { operation: before.name, side: 'request', violations };\n compareRequestSections(before.request, after.request, 'request', requestCtx);\n compareRequestSections(before.interceptor, after.interceptor, 'interceptor', requestCtx);\n\n compareSchema(\n before.response,\n after.response,\n 'response',\n { operation: before.name, side: 'response', violations },\n );\n\n return violations;\n}\n\n/**\n * Compare a released contract against the one this build produced.\n *\n * Operations that are new in `after` need no check — nothing has been promised\n * about them yet.\n */\nexport function compareDocuments(before: ContractDocument, after: ContractDocument): DocumentComparison\n{\n const current = new Map(after.operations.map(operation => [operation.name, operation]));\n const violations: ContractViolation[] = [];\n const removedOperations: string[] = [];\n\n for (const operation of before.operations)\n {\n const now = current.get(operation.name);\n\n if (!now)\n {\n removedOperations.push(operation.name);\n continue;\n }\n\n violations.push(...compareOperation(operation, now));\n }\n\n return { violations, removedOperations };\n}\n","/**\n * Released snapshots\n *\n * `contracts/released/<version>.json` is what a version actually promised. It is\n * written once, at release, and never touched again — the gate compares against\n * the newest one.\n *\n * Comparing against the newest one alone is only sound if no release is missing\n * a snapshot: compatibility is transitive through the chain, and a gap in the\n * chain silently widens what passes. That is why cutting a release writes a\n * snapshot rather than offering to.\n *\n * Each snapshot carries the SHA-256 of its own document, so a hand-edited\n * snapshot fails the gate instead of quietly moving the baseline.\n */\n\nimport { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from 'node:fs';\nimport { join } from 'node:path';\nimport { stableDigest, stableStringifyPretty } from './stable-json';\nimport type { ContractDocument, ContractSnapshot } from './types';\n\nexport class ContractSnapshotError extends Error\n{\n constructor(message: string)\n {\n super(message);\n this.name = 'ContractSnapshotError';\n }\n}\n\nexport const CURRENT_FILENAME = 'current.json';\nexport const RELEASED_DIRNAME = 'released';\nexport const USAGE_DIRNAME = 'usage';\n\nexport function currentPath(contractsDir: string): string\n{\n return join(contractsDir, CURRENT_FILENAME);\n}\n\nexport function releasedDir(contractsDir: string): string\n{\n return join(contractsDir, RELEASED_DIRNAME);\n}\n\nexport function usageDir(contractsDir: string): string\n{\n return join(contractsDir, USAGE_DIRNAME);\n}\n\n/**\n * Order two versions the way a release line runs.\n *\n * Numeric identifiers compare numerically (so 1.10.0 follows 1.9.0), and a\n * pre-release sorts before the release it leads to.\n */\nexport function compareVersions(a: string, b: string): number\n{\n const [aCore, aPre] = splitVersion(a);\n const [bCore, bPre] = splitVersion(b);\n\n for (let i = 0; i < 3; i++)\n {\n if (aCore[i] !== bCore[i])\n {\n return aCore[i] - bCore[i];\n }\n }\n\n if (aPre === bPre)\n {\n return 0;\n }\n\n if (aPre === undefined)\n {\n return 1;\n }\n\n if (bPre === undefined)\n {\n return -1;\n }\n\n return comparePreRelease(aPre, bPre);\n}\n\nfunction splitVersion(version: string): [number[], string | undefined]\n{\n const [core, ...rest] = version.split('-');\n const parts = core.split('.').map(part => Number.parseInt(part, 10));\n\n if (parts.length !== 3 || parts.some(part => !Number.isInteger(part) || part < 0))\n {\n throw new ContractSnapshotError(`\"${version}\" is not a version of the form major.minor.patch`);\n }\n\n return [parts, rest.length > 0 ? rest.join('-') : undefined];\n}\n\nfunction comparePreRelease(a: string, b: string): number\n{\n const aParts = a.split('.');\n const bParts = b.split('.');\n\n for (let i = 0; i < Math.max(aParts.length, bParts.length); i++)\n {\n const left = aParts[i];\n const right = bParts[i];\n\n if (left === undefined)\n {\n return -1;\n }\n\n if (right === undefined)\n {\n return 1;\n }\n\n if (left === right)\n {\n continue;\n }\n\n const leftNumeric = /^\\d+$/.test(left);\n const rightNumeric = /^\\d+$/.test(right);\n\n if (leftNumeric && rightNumeric)\n {\n return Number(left) - Number(right);\n }\n\n if (leftNumeric !== rightNumeric)\n {\n return leftNumeric ? -1 : 1;\n }\n\n return left < right ? -1 : 1;\n }\n\n return 0;\n}\n\nexport interface SnapshotFile\n{\n version: string;\n file: string;\n}\n\n/** Every released snapshot, oldest first. */\nexport function listSnapshots(contractsDir: string): SnapshotFile[]\n{\n const dir = releasedDir(contractsDir);\n\n if (!existsSync(dir))\n {\n return [];\n }\n\n return readdirSync(dir)\n .filter(name => name.endsWith('.json'))\n .map(name => ({ version: name.slice(0, -'.json'.length), file: join(dir, name) }))\n .sort((a, b) => compareVersions(a.version, b.version));\n}\n\n/** The snapshot the gate compares against, or undefined when nothing is released yet. */\nexport function newestSnapshot(contractsDir: string): SnapshotFile | undefined\n{\n const snapshots = listSnapshots(contractsDir);\n\n return snapshots[snapshots.length - 1];\n}\n\n/**\n * Read a snapshot and check its digest.\n *\n * A mismatch means the file was edited after release. The baseline is the whole\n * point of the gate, so an edited baseline is refused rather than trusted.\n */\nexport function readSnapshot(file: string): ContractSnapshot\n{\n let parsed: Partial<ContractSnapshot>;\n\n try\n {\n parsed = JSON.parse(readFileSync(file, 'utf-8')) as Partial<ContractSnapshot>;\n }\n catch (error)\n {\n const message = error instanceof Error ? error.message : String(error);\n\n throw new ContractSnapshotError(`${file} could not be read: ${message}`);\n }\n\n if (typeof parsed.version !== 'string' || typeof parsed.sha256 !== 'string' || !parsed.document)\n {\n throw new ContractSnapshotError(`${file} is not a contract snapshot: expected version, sha256 and document`);\n }\n\n const digest = stableDigest(parsed.document);\n\n if (digest !== parsed.sha256)\n {\n throw new ContractSnapshotError(\n `${file} was edited after release: it records sha256 ${parsed.sha256} but its document hashes to ${digest}`,\n );\n }\n\n return { version: parsed.version, sha256: parsed.sha256, document: parsed.document as ContractDocument };\n}\n\n/**\n * Write the snapshot for a release.\n *\n * The version comes from the document, which took it from `.contractVersion()`\n * on the router. The filename follows the declaration rather than being told\n * separately what to say — a second place to state the version is a second place\n * for it to be wrong.\n *\n * Refuses to overwrite: a published version's promise does not change, a\n * mistake becomes a new version.\n */\nexport function writeSnapshot(contractsDir: string, document: ContractDocument): string\n{\n const version = document.contractVersion;\n\n if (!version)\n {\n throw new ContractSnapshotError(\n 'The contract declares no version, so a release cannot be named. '\n + 'Add .contractVersion(\"x.y.z\") to the router before cutting one.',\n );\n }\n compareVersions(version, version);\n\n const dir = releasedDir(contractsDir);\n const file = join(dir, `${version}.json`);\n\n if (existsSync(file))\n {\n throw new ContractSnapshotError(\n `${file} already exists. A released version's contract is never rewritten — cut a new version instead.`,\n );\n }\n\n // Releases have to reach the newest snapshot in order. The gate compares\n // against the newest one alone, which is only sound while the chain has no\n // gaps — a snapshot filled in behind the newest one is never compared to\n // anything, and quietly widens what passes.\n const newest = newestSnapshot(contractsDir);\n\n if (newest && compareVersions(version, newest.version) <= 0)\n {\n throw new ContractSnapshotError(\n `${version} is not newer than the released ${newest.version}. `\n + 'The gate compares against the newest snapshot, so filling one in behind it would never be checked.',\n );\n }\n\n mkdirSync(dir, { recursive: true });\n\n const snapshot: ContractSnapshot = { version, sha256: stableDigest(document), document };\n writeFileSync(file, stableStringifyPretty(snapshot), 'utf-8');\n\n return file;\n}\n\n/** Read `contracts/current.json`. */\nexport function readCurrentDocument(contractsDir: string): ContractDocument\n{\n const file = currentPath(contractsDir);\n\n try\n {\n return JSON.parse(readFileSync(file, 'utf-8')) as ContractDocument;\n }\n catch (error)\n {\n const message = error instanceof Error ? error.message : String(error);\n\n throw new ContractSnapshotError(\n `${file} could not be read: ${message}. Run the @spfn/core:contract generator first.`,\n );\n }\n}\n\n/** Write `contracts/current.json`. Returns true when the file changed. */\nexport function writeCurrentDocument(contractsDir: string, document: ContractDocument): boolean\n{\n const file = currentPath(contractsDir);\n const content = stableStringifyPretty(document);\n const existing = existsSync(file) ? readFileSync(file, 'utf-8') : undefined;\n\n if (existing === content)\n {\n return false;\n }\n\n mkdirSync(contractsDir, { recursive: true });\n writeFileSync(file, content, 'utf-8');\n\n return true;\n}\n","/**\n * Usage files — who still calls an operation\n *\n * A released app is compiled and shipped; the server cannot ask it what it\n * calls. So each released client writes down the operations it uses, and those\n * files are what a removal is judged against:\n * `contracts/usage/<platform>-<appVersion>.json`.\n *\n * The one rule this file exists to hold: **an unreadable file and \"nobody calls\n * it\" are not the same answer.** An empty scan result reading as a pass is how\n * a removal check quietly stops checking anything. Every failure to read is a\n * refusal that names the file.\n */\n\nimport { readdirSync, readFileSync, statSync } from 'node:fs';\nimport { join } from 'node:path';\n\n/** One released client's declared call list. */\nexport interface UsageRecord\n{\n platform: string;\n appVersion: string;\n operations: string[];\n\n /** File this came from, for messages. */\n file: string;\n}\n\nexport type UsageReadResult =\n | { decidable: true; records: UsageRecord[] }\n | { decidable: false; reason: string };\n\nfunction parseRecord(file: string, raw: string): UsageRecord\n{\n const parsed = JSON.parse(raw) as Partial<UsageRecord>;\n\n if (typeof parsed.platform !== 'string' || parsed.platform.length === 0)\n {\n throw new Error('\"platform\" must be a non-empty string');\n }\n\n if (typeof parsed.appVersion !== 'string' || parsed.appVersion.length === 0)\n {\n throw new Error('\"appVersion\" must be a non-empty string');\n }\n\n if (!Array.isArray(parsed.operations) || parsed.operations.some(name => typeof name !== 'string'))\n {\n throw new Error('\"operations\" must be an array of operation names');\n }\n\n return {\n platform: parsed.platform,\n appVersion: parsed.appVersion,\n operations: parsed.operations,\n file,\n };\n}\n\n/**\n * Read every usage file under `usageDir`.\n *\n * Returns undecidable — never an empty pass — when the directory is missing,\n * holds no usage file, or holds one that cannot be read.\n */\nexport function readUsageRecords(usageDir: string): UsageReadResult\n{\n let entries: string[];\n\n try\n {\n if (!statSync(usageDir).isDirectory())\n {\n return { decidable: false, reason: `${usageDir} is not a directory` };\n }\n\n entries = readdirSync(usageDir).filter(name => name.endsWith('.json')).sort();\n }\n catch\n {\n return { decidable: false, reason: `${usageDir} does not exist` };\n }\n\n if (entries.length === 0)\n {\n return { decidable: false, reason: `${usageDir} holds no usage file` };\n }\n\n const records: UsageRecord[] = [];\n\n for (const entry of entries)\n {\n const file = join(usageDir, entry);\n\n try\n {\n records.push(parseRecord(file, readFileSync(file, 'utf-8')));\n }\n catch (error)\n {\n const message = error instanceof Error ? error.message : String(error);\n\n return { decidable: false, reason: `${file} could not be read: ${message}` };\n }\n }\n\n return { decidable: true, records };\n}\n\n/** Which released clients still call `operation`. */\nexport function callersOf(operation: string, records: UsageRecord[]): UsageRecord[]\n{\n return records.filter(record => record.operations.includes(operation));\n}\n","/**\n * The gate\n *\n * Puts the pieces together: read the newest released snapshot, compare this\n * build's contract against it, and decide removals against what released\n * clients still call.\n *\n * Nothing released yet is a pass — with a warning. \"This is the first contract\"\n * and \"the release that should have written a snapshot didn't\" produce the same\n * empty directory, and only a person can tell them apart.\n */\n\nimport { compareDocuments } from './compare';\nimport { newestSnapshot, readSnapshot, usageDir } from './snapshot';\nimport { callersOf, readUsageRecords } from './usage';\nimport type { ContractDocument, ContractViolation } from './types';\n\nexport interface ContractCheckResult\n{\n /** Version compared against, absent when nothing is released yet. */\n baselineVersion?: string;\n\n violations: ContractViolation[];\n\n /** Things a person should look at that do not stop the build. */\n warnings: string[];\n}\n\nexport function checkContract(contractsDir: string, current: ContractDocument): ContractCheckResult\n{\n const baseline = newestSnapshot(contractsDir);\n\n if (!baseline)\n {\n return {\n violations: [],\n warnings: [\n 'No released contract snapshot found, so nothing was compared. '\n + 'This is expected for a first contract, and a mistake if a release forgot to write one.',\n ],\n };\n }\n\n let previous: ContractDocument;\n\n try\n {\n previous = readSnapshot(baseline.file).document;\n }\n catch (error)\n {\n return {\n baselineVersion: baseline.version,\n warnings: [],\n violations: [{\n kind: 'snapshot.digest-mismatch',\n detail: error instanceof Error ? error.message : String(error),\n }],\n };\n }\n\n const { violations, removedOperations } = compareDocuments(previous, current);\n\n return {\n baselineVersion: baseline.version,\n warnings: [],\n violations: [...violations, ...judgeRemovals(contractsDir, removedOperations)],\n };\n}\n\n/**\n * Decide whether removed operations may go.\n *\n * Only reached when something was actually removed — an app that removes\n * nothing never needs a usage file to exist.\n */\nfunction judgeRemovals(contractsDir: string, removedOperations: string[]): ContractViolation[]\n{\n if (removedOperations.length === 0)\n {\n return [];\n }\n\n const usage = readUsageRecords(usageDir(contractsDir));\n\n if (!usage.decidable)\n {\n return [{\n kind: 'usage.undecidable',\n detail:\n `${removedOperations.join(', ')} would be removed, but no released client's call list could be read `\n + `(${usage.reason}). Not knowing who calls an operation is not the same as knowing nobody does.`,\n }];\n }\n\n const violations: ContractViolation[] = [];\n\n for (const operation of removedOperations)\n {\n const callers = callersOf(operation, usage.records);\n\n if (callers.length === 0)\n {\n continue;\n }\n\n violations.push({\n kind: 'usage.still-called',\n operation,\n detail:\n 'still called by '\n + callers.map(caller => `${caller.platform} ${caller.appVersion}`).join(', '),\n });\n }\n\n return violations;\n}\n\n/** Render violations as the message a failing build prints. */\nexport function formatViolations(violations: ContractViolation[]): string\n{\n return violations\n .map((violation) =>\n {\n const where = [violation.operation, violation.location].filter(Boolean).join(' ');\n\n return ` - [${violation.kind}]${where ? ` ${where}` : ''}: ${violation.detail}`;\n })\n .join('\\n');\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["../../src/event/sse/paths.ts","../../src/contract/collect.ts","../../src/contract/stable-json.ts","../../src/contract/compare.ts","../../src/contract/snapshot.ts","../../src/contract/usage.ts","../../src/contract/check.ts"],"names":["REQUEST_SECTIONS","usageDir","readdirSync","join","readFileSync"],"mappings":"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","file":"index.js","sourcesContent":["/**\n * SSE endpoint paths\n *\n * `createServer` registers the token endpoint beside the stream, and the\n * contract publishes both paths to clients compiled separately from the\n * server. Both read them from here so the path a client is told about and the\n * path the server serves cannot drift apart.\n */\n\n/** The stream path `.events()` registers when it is given none. */\nexport const DEFAULT_SSE_STREAM_PATH = '/events/stream';\n\n/** The token path beside a stream: its last segment replaced, `/events/stream` → `/events/token`. */\nexport function sseTokenPath(streamPath: string): string\n{\n return streamPath.replace(/\\/[^/]+$/, '/token');\n}\n","/**\n * Contract Collection\n *\n * Walks a loaded router and turns every route carrying `.contract()` into a\n * contract operation, and every event of an event router carrying\n * `.contract()` into a contract event.\n *\n * The router is loaded and walked rather than parsed from source. Real routes\n * build their schemas from imported values — `EmailSchema`, `FileSchema()`,\n * `KEY_DEVICE_NAME_MAX_LENGTH` — which a source parser cannot resolve. Loading\n * costs a module import and no infrastructure: route modules have no\n * import-time side effects, so nothing here opens a database connection.\n */\n\nimport type { EventRouterDef } from '../event/router';\nimport { DEFAULT_SSE_STREAM_PATH, sseTokenPath } from '../event/sse/paths';\nimport type { EventDef } from '../event/types';\nimport type { RouteDef } from '../route/route-builder';\nimport type { RouteInput } from '../route/route-input';\nimport type { Router } from '../route/router';\nimport type {\n ContractDocument,\n ContractEvent,\n ContractEvents,\n ContractOperation,\n ContractRequest,\n JsonSchema,\n} from './types';\n\n/** Thrown when the router cannot produce a contract at all. */\nexport class ContractCollectionError extends Error\n{\n constructor(message: string)\n {\n super(message);\n this.name = 'ContractCollectionError';\n }\n}\n\nfunction isRouter(value: unknown): value is Router<any>\n{\n return value !== null\n && typeof value === 'object'\n && 'routes' in value\n && '_routes' in value;\n}\n\nfunction isRouteDef(value: unknown): value is RouteDef<any>\n{\n return value !== null\n && typeof value === 'object'\n && 'handler' in value;\n}\n\n/**\n * Strip TypeBox's symbol-keyed metadata and hand back plain JSON.\n *\n * The symbols carry no information the wire shape depends on, and they cannot\n * survive the round trip to a committed file.\n */\nfunction toJsonSchema(schema: unknown): JsonSchema\n{\n return JSON.parse(JSON.stringify(schema)) as JsonSchema;\n}\n\n/**\n * `formData` is absent on purpose — a contracted route carrying it is refused\n * before it gets here (see `assertNoFormData`). Emitting its schema alongside\n * the JSON sections described a multipart route as if it were JSON-shaped, and a\n * generated client built from that reads as correct right up to the request.\n */\nconst REQUEST_SECTIONS = ['params', 'query', 'body', 'headers', 'cookies'] as const;\n\nfunction toContractRequest(input: RouteInput | undefined): ContractRequest\n{\n const request: ContractRequest = {};\n\n if (!input)\n {\n return request;\n }\n\n for (const section of REQUEST_SECTIONS)\n {\n const schema = input[section];\n if (schema)\n {\n request[section] = toJsonSchema(schema);\n }\n }\n\n return request;\n}\n\ninterface Found\n{\n operation: ContractOperation;\n\n /** Where the route was found, for a collision message. */\n trail: string;\n}\n\nfunction visitRouter(router: Router<any>, trail: string[], found: Map<string, Found>): void\n{\n for (const [name, entry] of Object.entries(router.routes))\n {\n if (isRouter(entry))\n {\n visitRouter(entry, [...trail, name], found);\n continue;\n }\n\n if (!isRouteDef(entry))\n {\n continue;\n }\n\n const routeDef = entry as RouteDef<any>;\n if (!routeDef.contract)\n {\n continue;\n }\n\n addOperation(name, routeDef, [...trail, name], found);\n }\n\n for (const packageRouter of router._packageRouters ?? [])\n {\n visitRouter(packageRouter, [...trail, '(package)'], found);\n }\n}\n\n/**\n * Refuse a contracted route that takes multipart form data.\n *\n * Multipart is a transport-format problem, not a type problem: the contract's\n * grammar describes JSON values, and a file part has no spelling in it. The\n * refusal is loud rather than a quiet omission — a contract that silently\n * dropped the section would still claim to describe the operation, and an app\n * generated from it would break against the running server instead of at build.\n */\nfunction assertNoFormData(where: string, routeDef: RouteDef<any>): void\n{\n const input = routeDef.input as RouteInput | undefined;\n const interceptor = routeDef.interceptor as RouteInput | undefined;\n const section = input?.formData ? 'input' : interceptor?.formData ? 'interceptor' : undefined;\n\n if (!section)\n {\n return;\n }\n\n throw new ContractCollectionError(\n `Contracted route \"${where}\" declares ${section}.formData, which a contract cannot describe. `\n + 'The contract grammar covers JSON values, and multipart carries file parts that have no spelling in it. '\n + 'Drop .contract() from this route, or move the operation to a JSON body.',\n );\n}\n\nfunction addOperation(name: string, routeDef: RouteDef<any>, trail: string[], found: Map<string, Found>): void\n{\n const where = trail.join('.');\n const contract = routeDef.contract!;\n\n assertNoFormData(where, routeDef);\n\n if (!routeDef.method || !routeDef.path)\n {\n throw new ContractCollectionError(\n `Contracted route \"${where}\" has no method or path. `\n + 'A contract describes an operation on the wire, so both are required.',\n );\n }\n\n if (!contract.since)\n {\n throw new ContractCollectionError(\n `Contracted route \"${where}\" has no \"since\" version. `\n + 'The version an operation first appeared in is part of the promise.',\n );\n }\n\n if (!contract.response)\n {\n throw new ContractCollectionError(\n `Contracted route \"${where}\" declares no response schema. `\n + 'An operation with no body declares Type.Null().',\n );\n }\n\n const existing = found.get(name);\n if (existing)\n {\n throw new ContractCollectionError(\n `Two contracted routes are both named \"${name}\" (${existing.trail} and ${where}). `\n + 'An operation is identified by its name across versions, so names must be unique.',\n );\n }\n\n found.set(name, {\n trail: where,\n operation: {\n name,\n method: routeDef.method,\n path: routeDef.path,\n since: contract.since,\n auth: contract.auth ?? 'none',\n requiresSession: contract.requiresSession ?? false,\n ...(contract.deprecatedIn ? { deprecatedIn: contract.deprecatedIn } : {}),\n ...(contract.removedIn ? { removedIn: contract.removedIn } : {}),\n request: toContractRequest(routeDef.input),\n interceptor: toContractRequest(routeDef.interceptor),\n response: toJsonSchema(contract.response),\n },\n });\n}\n\n/**\n * Frame names the stream sends on its own: `connected` once the stream opens,\n * `ping` as keep-alive. An event under either key would be indistinguishable\n * from them on the wire.\n */\nconst RESERVED_EVENT_NAMES = new Set(['connected', 'ping']);\n\nfunction byName<T extends { name: string }>(a: T, b: T): number\n{\n return a.name < b.name ? -1 : a.name > b.name ? 1 : 0;\n}\n\nfunction toContractEvent(name: string, eventDef: EventDef<any>): ContractEvent\n{\n const contract = eventDef._contract!;\n\n if (RESERVED_EVENT_NAMES.has(name))\n {\n throw new ContractCollectionError(\n `Contracted event \"${name}\" uses a reserved frame name. `\n + 'The stream itself sends \"connected\" and \"ping\" frames, so a client could not tell this event from them. '\n + 'Register the event under another key.',\n );\n }\n\n if (!contract.since)\n {\n throw new ContractCollectionError(\n `Contracted event \"${name}\" has no \"since\" version. `\n + 'The version an event first appeared in is part of the promise.',\n );\n }\n\n if (!eventDef.schema)\n {\n throw new ContractCollectionError(\n `Contracted event \"${name}\" declares no payload schema. `\n + 'Its frame carries no data, so there is nothing for the contract to describe. '\n + 'Declare Type.Object({}) and emit {} instead.',\n );\n }\n\n return {\n name,\n since: contract.since,\n ...(contract.deprecatedIn ? { deprecatedIn: contract.deprecatedIn } : {}),\n ...(contract.removedIn ? { removedIn: contract.removedIn } : {}),\n payload: toJsonSchema(eventDef.schema),\n };\n}\n\n/**\n * The events section, or `undefined` when nothing in the router is contracted.\n *\n * An item is named by its router key, the name on the wire. The same `EventDef`\n * registered under two keys therefore becomes two items with two names — keys\n * are unique by construction, so no collision check is needed here.\n */\nfunction collectEvents(eventRouter: EventRouterDef<any> | undefined): ContractEvents | undefined\n{\n const contracted = Object.entries<EventDef<any>>(eventRouter?.events ?? {})\n .filter(([, eventDef]) => eventDef._contract);\n\n if (contracted.length === 0)\n {\n return undefined;\n }\n\n const routerContract = eventRouter!._contract;\n\n if (!routerContract)\n {\n throw new ContractCollectionError(\n `Event router defineEventRouter({ ${eventRouter!.eventNames.join(', ')} }) contracts events but declares `\n + 'no .contract() of its own. '\n + 'Without it the contract cannot say whether a client must exchange a token before opening the stream. '\n + 'Add .contract({ auth: \\'none\\' }) or .contract({ auth: \\'tokenExchange\\' }) to the router.',\n );\n }\n\n const streamPath = routerContract.streamPath ?? DEFAULT_SSE_STREAM_PATH;\n\n return {\n streamPath,\n tokenPath: sseTokenPath(streamPath),\n auth: routerContract.auth,\n items: contracted.map(([name, eventDef]) => toContractEvent(name, eventDef)).sort(byName),\n };\n}\n\n/**\n * Build the contract document from a loaded router and, optionally, the app's\n * event router.\n *\n * Routes and events without `.contract()` are skipped — they are not part of\n * the promise. A document with no contracted event carries no `events` key, so\n * it is byte-identical to one collected without an event router.\n */\nexport function collectContractDocument(router: Router<any>, eventRouter?: EventRouterDef<any>): ContractDocument\n{\n const found = new Map<string, Found>();\n visitRouter(router, ['router'], found);\n\n const operations = [...found.values()]\n .map(entry => entry.operation)\n .sort(byName);\n\n const events = collectEvents(eventRouter);\n\n return {\n documentVersion: 1,\n ...(router._contractVersion ? { contractVersion: router._contractVersion } : {}),\n compatibilityPolicy: 'perOperation',\n operations,\n ...(events ? { events } : {}),\n };\n}\n","/**\n * Deterministic JSON\n *\n * The generated contract is committed and compared against what the generator\n * produces. If key order moved between runs, \"the committed file differs from\n * the generated one\" would flicker and the check would train people to ignore\n * it. So every emitted document is written with its object keys sorted, and the\n * digest that pins a released snapshot is taken over the same ordering with no\n * whitespace — formatting choices never move a digest.\n */\n\nimport { createHash } from 'node:crypto';\n\n/** Recursively sort object keys and drop `undefined` members. */\nexport function canonicalize(value: unknown): unknown\n{\n if (Array.isArray(value))\n {\n return value.map(canonicalize);\n }\n\n if (value === null || typeof value !== 'object')\n {\n return value;\n }\n\n const source = value as Record<string, unknown>;\n const sorted: Record<string, unknown> = {};\n\n for (const key of Object.keys(source).sort())\n {\n if (source[key] === undefined)\n {\n continue;\n }\n\n sorted[key] = canonicalize(source[key]);\n }\n\n return sorted;\n}\n\n/** Compact canonical encoding — what a digest is taken over. */\nexport function stableStringify(value: unknown): string\n{\n return JSON.stringify(canonicalize(value));\n}\n\n/** Indented canonical encoding — what gets written to disk and reviewed. */\nexport function stableStringifyPretty(value: unknown): string\n{\n return `${JSON.stringify(canonicalize(value), null, 4)}\\n`;\n}\n\n/** SHA-256 of the compact canonical encoding, lowercase hex. */\nexport function stableDigest(value: unknown): string\n{\n return createHash('sha256').update(stableStringify(value), 'utf8').digest('hex');\n}\n","/**\n * Backward-compatibility comparison\n *\n * Compares the contract a build produces against the newest released snapshot.\n *\n * The rule that shapes everything here: **optional runs in opposite directions\n * on the two sides.** A request is safe when the server grows more tolerant —\n * a new optional field, a dropped field, a requirement relaxed. A response is\n * safe when the server grows more certain — a new field, a value that used to\n * be optional now always present. Collapsing both into one rule would\n * necessarily get one of them backwards.\n *\n * | change | request | response |\n * |-------------------------|---------|----------|\n * | field added (optional) | pass | pass |\n * | field added (required) | refuse | pass |\n * | field removed | pass | refuse |\n * | required → optional | pass | refuse |\n * | optional → required | refuse | pass |\n * | type changed | refuse | refuse |\n *\n * An event payload flows server → client like a response, so it is compared\n * under the response rules by the same walk, and only the violation kinds are\n * renamed.\n */\n\nimport { stableStringify } from './stable-json';\nimport type {\n ContractDocument,\n ContractEvent,\n ContractEvents,\n ContractOperation,\n ContractRequest,\n ContractViolation,\n ContractViolationKind,\n JsonSchema,\n} from './types';\n\n/** Which side of the wire a schema sits on. The direction optional runs. */\ntype Side = 'request' | 'response';\n\nexport interface DocumentComparison\n{\n violations: ContractViolation[];\n\n /**\n * Operations present in the baseline and gone from the current contract.\n *\n * Removal is not decided here: whether an operation may go depends on\n * whether any released app still calls it, which is a separate check.\n */\n removedOperations: string[];\n\n /**\n * Events present in the baseline and gone from the current contract —\n * decided against usage files exactly as a removed operation is.\n */\n removedEvents: string[];\n}\n\nconst REQUEST_SECTIONS = ['params', 'query', 'body', 'formData', 'headers', 'cookies'] as const;\n\n/**\n * The schema's own keywords, with the structural ones removed.\n *\n * Everything left — `type`, `format`, `enum`, `anyOf`, `minLength` — is\n * compared verbatim, so a narrowed constraint counts as a type change. That\n * refuses more than the table's \"type changed\" row strictly requires, and it\n * refuses in the recoverable direction: a build that stops is fixed by cutting\n * a new contract version, while a break that passes reaches a shipped app.\n */\nfunction leafSignature(schema: JsonSchema): string\n{\n const { properties, required, items, ...rest } = schema;\n\n return stableStringify(rest);\n}\n\nfunction propertiesOf(schema: JsonSchema): Record<string, JsonSchema>\n{\n const properties = schema.properties;\n\n if (!properties || typeof properties !== 'object')\n {\n return {};\n }\n\n return properties as Record<string, JsonSchema>;\n}\n\nfunction requiredOf(schema: JsonSchema): Set<string>\n{\n const required = schema.required;\n\n if (!Array.isArray(required))\n {\n return new Set();\n }\n\n return new Set(required.filter((name): name is string => typeof name === 'string'));\n}\n\n/**\n * Would a client that sends nothing here now be refused?\n *\n * Only this level counts. A required field nested inside an optional object is\n * required *if* that object is sent, and a client that never sends it is fine.\n */\nfunction requiresClientToSend(schema: JsonSchema): boolean\n{\n if (schema.type !== 'object')\n {\n return true;\n }\n\n return requiredOf(schema).size > 0;\n}\n\ninterface CompareContext\n{\n operation: string;\n side: Side;\n violations: ContractViolation[];\n}\n\nfunction report(\n ctx: CompareContext,\n kind: ContractViolation['kind'],\n location: string,\n detail: string,\n): void\n{\n ctx.violations.push({ kind, operation: ctx.operation, location, detail });\n}\n\n/**\n * Compare one schema position across versions.\n *\n * `before`/`after` may be absent: a whole request section can appear or vanish,\n * and so can a nested field.\n */\nfunction compareSchema(\n before: JsonSchema | undefined,\n after: JsonSchema | undefined,\n location: string,\n ctx: CompareContext,\n): void\n{\n if (!before && !after)\n {\n return;\n }\n\n if (before && !after)\n {\n if (ctx.side === 'response')\n {\n report(ctx, 'response.field-removed', location, 'the response no longer carries this, and old clients read it');\n }\n\n // A request the server stopped reading is a request old clients may still send.\n return;\n }\n\n if (!before && after)\n {\n if (ctx.side === 'request' && requiresClientToSend(after))\n {\n report(\n ctx,\n 'request.required-field-added',\n location,\n 'this is new and mandatory, so every already-released client is refused',\n );\n }\n\n // A response that carries more is a response old clients ignore the rest of.\n return;\n }\n\n const from = before!;\n const to = after!;\n\n if (leafSignature(from) !== leafSignature(to))\n {\n report(\n ctx,\n ctx.side === 'request' ? 'request.type-changed' : 'response.type-changed',\n location,\n `the declared type changed: ${leafSignature(from)} → ${leafSignature(to)}`,\n );\n\n return;\n }\n\n compareProperties(from, to, location, ctx);\n compareSchema(\n from.items as JsonSchema | undefined,\n to.items as JsonSchema | undefined,\n `${location}[]`,\n ctx,\n );\n}\n\nfunction compareProperties(from: JsonSchema, to: JsonSchema, location: string, ctx: CompareContext): void\n{\n const beforeProperties = propertiesOf(from);\n const afterProperties = propertiesOf(to);\n const beforeRequired = requiredOf(from);\n const afterRequired = requiredOf(to);\n\n for (const [name, beforeSchema] of Object.entries(beforeProperties))\n {\n const where = `${location}.${name}`;\n const afterSchema = afterProperties[name];\n\n compareSchema(beforeSchema, afterSchema, where, ctx);\n\n if (!afterSchema)\n {\n continue;\n }\n\n const wasRequired = beforeRequired.has(name);\n const isRequired = afterRequired.has(name);\n\n if (ctx.side === 'request' && !wasRequired && isRequired)\n {\n report(ctx, 'request.field-became-required', where, 'clients that never sent this are now refused');\n }\n\n if (ctx.side === 'response' && wasRequired && !isRequired)\n {\n report(ctx, 'response.field-became-optional', where, 'clients that counted on this always arriving now break');\n }\n }\n\n // An added field is judged at its own level only. Optional means an old\n // client that omits it still passes, whatever the field contains.\n for (const name of Object.keys(afterProperties))\n {\n if (beforeProperties[name])\n {\n continue;\n }\n\n if (ctx.side === 'request' && afterRequired.has(name))\n {\n report(\n ctx,\n 'request.required-field-added',\n `${location}.${name}`,\n 'this is new and mandatory, so every already-released client is refused',\n );\n }\n }\n}\n\nfunction compareRequestSections(\n before: ContractRequest,\n after: ContractRequest,\n prefix: string,\n ctx: CompareContext,\n): void\n{\n for (const section of REQUEST_SECTIONS)\n {\n compareSchema(before[section], after[section], `${prefix}.${section}`, ctx);\n }\n}\n\n/** Compare one operation that exists on both sides. */\nexport function compareOperation(before: ContractOperation, after: ContractOperation): ContractViolation[]\n{\n const violations: ContractViolation[] = [];\n\n if (before.path !== after.path)\n {\n violations.push({\n kind: 'operation.path-changed',\n operation: before.name,\n detail: `path moved ${before.path} → ${after.path}; released clients call the old one`,\n });\n }\n\n if (before.method !== after.method)\n {\n violations.push({\n kind: 'operation.method-changed',\n operation: before.name,\n detail: `method changed ${before.method} → ${after.method}; released clients send the old one`,\n });\n }\n\n const requestCtx: CompareContext = { operation: before.name, side: 'request', violations };\n compareRequestSections(before.request, after.request, 'request', requestCtx);\n compareRequestSections(before.interceptor, after.interceptor, 'interceptor', requestCtx);\n\n compareSchema(\n before.response,\n after.response,\n 'response',\n { operation: before.name, side: 'response', violations },\n );\n\n return violations;\n}\n\n/** What a response-side violation is called when the schema is an event payload. */\nconst PAYLOAD_KINDS: Partial<Record<ContractViolationKind, ContractViolationKind>> = {\n 'response.field-removed': 'event.payload.field-removed',\n 'response.field-became-optional': 'event.payload.field-became-optional',\n 'response.type-changed': 'event.payload.type-changed',\n};\n\n/** Compare one event that exists on both sides, under the response rules. */\nfunction compareEvent(before: ContractEvent, after: ContractEvent): ContractViolation[]\n{\n const found: ContractViolation[] = [];\n\n compareSchema(before.payload, after.payload, 'payload', { operation: before.name, side: 'response', violations: found });\n\n return found.map(({ kind, operation, ...rest }) => ({ ...rest, kind: PAYLOAD_KINDS[kind] ?? kind, event: operation }));\n}\n\n/** A stream path or auth mode that moved breaks every released subscriber at once. */\nfunction compareStream(before: ContractEvents, after: ContractEvents): ContractViolation[]\n{\n const violations: ContractViolation[] = [];\n\n if (before.streamPath !== after.streamPath)\n {\n violations.push({\n kind: 'events.stream-path-changed',\n detail: `stream path moved ${before.streamPath} → ${after.streamPath}; released clients open the old one`,\n });\n }\n\n if (before.auth !== after.auth)\n {\n violations.push({\n kind: 'events.auth-changed',\n detail: `stream auth changed ${before.auth} → ${after.auth}; released clients connect the old way`,\n });\n }\n\n return violations;\n}\n\n/**\n * Compare the events sections.\n *\n * A baseline without one promised no events, so nothing in `after` can break\n * it. A current document without one removed every event the baseline had.\n */\nfunction compareEvents(\n before: ContractEvents | undefined,\n after: ContractEvents | undefined,\n): Pick<DocumentComparison, 'violations' | 'removedEvents'>\n{\n if (!before)\n {\n return { violations: [], removedEvents: [] };\n }\n\n if (!after)\n {\n return { violations: [], removedEvents: before.items.map(item => item.name) };\n }\n\n const current = new Map(after.items.map(item => [item.name, item]));\n const violations = compareStream(before, after);\n const removedEvents: string[] = [];\n\n for (const item of before.items)\n {\n const now = current.get(item.name);\n\n if (!now)\n {\n removedEvents.push(item.name);\n continue;\n }\n\n violations.push(...compareEvent(item, now));\n }\n\n return { violations, removedEvents };\n}\n\n/**\n * Compare a released contract against the one this build produced.\n *\n * Operations and events that are new in `after` need no check — nothing has\n * been promised about them yet.\n */\nexport function compareDocuments(before: ContractDocument, after: ContractDocument): DocumentComparison\n{\n const current = new Map(after.operations.map(operation => [operation.name, operation]));\n const violations: ContractViolation[] = [];\n const removedOperations: string[] = [];\n\n for (const operation of before.operations)\n {\n const now = current.get(operation.name);\n\n if (!now)\n {\n removedOperations.push(operation.name);\n continue;\n }\n\n violations.push(...compareOperation(operation, now));\n }\n\n const events = compareEvents(before.events, after.events);\n\n return {\n violations: [...violations, ...events.violations],\n removedOperations,\n removedEvents: events.removedEvents,\n };\n}\n","/**\n * Released snapshots\n *\n * `contracts/released/<version>.json` is what a version actually promised. It is\n * written once, at release, and never touched again — the gate compares against\n * the newest one.\n *\n * Comparing against the newest one alone is only sound if no release is missing\n * a snapshot: compatibility is transitive through the chain, and a gap in the\n * chain silently widens what passes. That is why cutting a release writes a\n * snapshot rather than offering to.\n *\n * Each snapshot carries the SHA-256 of its own document, so a hand-edited\n * snapshot fails the gate instead of quietly moving the baseline.\n */\n\nimport { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from 'node:fs';\nimport { join } from 'node:path';\nimport { stableDigest, stableStringifyPretty } from './stable-json';\nimport type { ContractDocument, ContractSnapshot } from './types';\n\nexport class ContractSnapshotError extends Error\n{\n constructor(message: string)\n {\n super(message);\n this.name = 'ContractSnapshotError';\n }\n}\n\nexport const CURRENT_FILENAME = 'current.json';\nexport const RELEASED_DIRNAME = 'released';\nexport const USAGE_DIRNAME = 'usage';\n\nexport function currentPath(contractsDir: string): string\n{\n return join(contractsDir, CURRENT_FILENAME);\n}\n\nexport function releasedDir(contractsDir: string): string\n{\n return join(contractsDir, RELEASED_DIRNAME);\n}\n\nexport function usageDir(contractsDir: string): string\n{\n return join(contractsDir, USAGE_DIRNAME);\n}\n\n/**\n * Order two versions the way a release line runs.\n *\n * Numeric identifiers compare numerically (so 1.10.0 follows 1.9.0), and a\n * pre-release sorts before the release it leads to.\n */\nexport function compareVersions(a: string, b: string): number\n{\n const [aCore, aPre] = splitVersion(a);\n const [bCore, bPre] = splitVersion(b);\n\n for (let i = 0; i < 3; i++)\n {\n if (aCore[i] !== bCore[i])\n {\n return aCore[i] - bCore[i];\n }\n }\n\n if (aPre === bPre)\n {\n return 0;\n }\n\n if (aPre === undefined)\n {\n return 1;\n }\n\n if (bPre === undefined)\n {\n return -1;\n }\n\n return comparePreRelease(aPre, bPre);\n}\n\nfunction splitVersion(version: string): [number[], string | undefined]\n{\n const [core, ...rest] = version.split('-');\n const parts = core.split('.').map(part => Number.parseInt(part, 10));\n\n if (parts.length !== 3 || parts.some(part => !Number.isInteger(part) || part < 0))\n {\n throw new ContractSnapshotError(`\"${version}\" is not a version of the form major.minor.patch`);\n }\n\n return [parts, rest.length > 0 ? rest.join('-') : undefined];\n}\n\nfunction comparePreRelease(a: string, b: string): number\n{\n const aParts = a.split('.');\n const bParts = b.split('.');\n\n for (let i = 0; i < Math.max(aParts.length, bParts.length); i++)\n {\n const left = aParts[i];\n const right = bParts[i];\n\n if (left === undefined)\n {\n return -1;\n }\n\n if (right === undefined)\n {\n return 1;\n }\n\n if (left === right)\n {\n continue;\n }\n\n const leftNumeric = /^\\d+$/.test(left);\n const rightNumeric = /^\\d+$/.test(right);\n\n if (leftNumeric && rightNumeric)\n {\n return Number(left) - Number(right);\n }\n\n if (leftNumeric !== rightNumeric)\n {\n return leftNumeric ? -1 : 1;\n }\n\n return left < right ? -1 : 1;\n }\n\n return 0;\n}\n\nexport interface SnapshotFile\n{\n version: string;\n file: string;\n}\n\n/** Every released snapshot, oldest first. */\nexport function listSnapshots(contractsDir: string): SnapshotFile[]\n{\n const dir = releasedDir(contractsDir);\n\n if (!existsSync(dir))\n {\n return [];\n }\n\n return readdirSync(dir)\n .filter(name => name.endsWith('.json'))\n .map(name => ({ version: name.slice(0, -'.json'.length), file: join(dir, name) }))\n .sort((a, b) => compareVersions(a.version, b.version));\n}\n\n/** The snapshot the gate compares against, or undefined when nothing is released yet. */\nexport function newestSnapshot(contractsDir: string): SnapshotFile | undefined\n{\n const snapshots = listSnapshots(contractsDir);\n\n return snapshots[snapshots.length - 1];\n}\n\n/**\n * Read a snapshot and check its digest.\n *\n * A mismatch means the file was edited after release. The baseline is the whole\n * point of the gate, so an edited baseline is refused rather than trusted.\n */\nexport function readSnapshot(file: string): ContractSnapshot\n{\n let parsed: Partial<ContractSnapshot>;\n\n try\n {\n parsed = JSON.parse(readFileSync(file, 'utf-8')) as Partial<ContractSnapshot>;\n }\n catch (error)\n {\n const message = error instanceof Error ? error.message : String(error);\n\n throw new ContractSnapshotError(`${file} could not be read: ${message}`);\n }\n\n if (typeof parsed.version !== 'string' || typeof parsed.sha256 !== 'string' || !parsed.document)\n {\n throw new ContractSnapshotError(`${file} is not a contract snapshot: expected version, sha256 and document`);\n }\n\n const digest = stableDigest(parsed.document);\n\n if (digest !== parsed.sha256)\n {\n throw new ContractSnapshotError(\n `${file} was edited after release: it records sha256 ${parsed.sha256} but its document hashes to ${digest}`,\n );\n }\n\n return { version: parsed.version, sha256: parsed.sha256, document: parsed.document as ContractDocument };\n}\n\n/**\n * Write the snapshot for a release.\n *\n * The version comes from the document, which took it from `.contractVersion()`\n * on the router. The filename follows the declaration rather than being told\n * separately what to say — a second place to state the version is a second place\n * for it to be wrong.\n *\n * Refuses to overwrite: a published version's promise does not change, a\n * mistake becomes a new version.\n */\nexport function writeSnapshot(contractsDir: string, document: ContractDocument): string\n{\n const version = document.contractVersion;\n\n if (!version)\n {\n throw new ContractSnapshotError(\n 'The contract declares no version, so a release cannot be named. '\n + 'Add .contractVersion(\"x.y.z\") to the router before cutting one.',\n );\n }\n compareVersions(version, version);\n\n const dir = releasedDir(contractsDir);\n const file = join(dir, `${version}.json`);\n\n if (existsSync(file))\n {\n throw new ContractSnapshotError(\n `${file} already exists. A released version's contract is never rewritten — cut a new version instead.`,\n );\n }\n\n // Releases have to reach the newest snapshot in order. The gate compares\n // against the newest one alone, which is only sound while the chain has no\n // gaps — a snapshot filled in behind the newest one is never compared to\n // anything, and quietly widens what passes.\n const newest = newestSnapshot(contractsDir);\n\n if (newest && compareVersions(version, newest.version) <= 0)\n {\n throw new ContractSnapshotError(\n `${version} is not newer than the released ${newest.version}. `\n + 'The gate compares against the newest snapshot, so filling one in behind it would never be checked.',\n );\n }\n\n mkdirSync(dir, { recursive: true });\n\n const snapshot: ContractSnapshot = { version, sha256: stableDigest(document), document };\n writeFileSync(file, stableStringifyPretty(snapshot), 'utf-8');\n\n return file;\n}\n\n/** Read `contracts/current.json`. */\nexport function readCurrentDocument(contractsDir: string): ContractDocument\n{\n const file = currentPath(contractsDir);\n\n try\n {\n return JSON.parse(readFileSync(file, 'utf-8')) as ContractDocument;\n }\n catch (error)\n {\n const message = error instanceof Error ? error.message : String(error);\n\n throw new ContractSnapshotError(\n `${file} could not be read: ${message}. Run the @spfn/core:contract generator first.`,\n );\n }\n}\n\n/** Write `contracts/current.json`. Returns true when the file changed. */\nexport function writeCurrentDocument(contractsDir: string, document: ContractDocument): boolean\n{\n const file = currentPath(contractsDir);\n const content = stableStringifyPretty(document);\n const existing = existsSync(file) ? readFileSync(file, 'utf-8') : undefined;\n\n if (existing === content)\n {\n return false;\n }\n\n mkdirSync(contractsDir, { recursive: true });\n writeFileSync(file, content, 'utf-8');\n\n return true;\n}\n","/**\n * Usage files — who still calls an operation or subscribes to an event\n *\n * A released app is compiled and shipped; the server cannot ask it what it\n * calls. So each released client writes down the operations it uses and the\n * events it subscribes to, and those files are what a removal is judged\n * against: `contracts/usage/<platform>-<appVersion>.json`.\n *\n * The one rule this file exists to hold: **an unreadable file and \"nobody calls\n * it\" are not the same answer.** An empty scan result reading as a pass is how\n * a removal check quietly stops checking anything. Every failure to read is a\n * refusal that names the file.\n */\n\nimport { readdirSync, readFileSync, statSync } from 'node:fs';\nimport { join } from 'node:path';\n\n/** One released client's declared call list. */\nexport interface UsageRecord\n{\n platform: string;\n appVersion: string;\n operations: string[];\n\n /**\n * Contracted events the client subscribes to. Always set when read from a\n * file — a file without the key subscribes to none — and optional only so a\n * record built by hand before events were contracted still type-checks.\n */\n events?: string[];\n\n /** File this came from, for messages. */\n file: string;\n}\n\nexport type UsageReadResult =\n | { decidable: true; records: UsageRecord[] }\n | { decidable: false; reason: string };\n\nfunction parseRecord(file: string, raw: string): UsageRecord\n{\n const parsed = JSON.parse(raw) as Partial<UsageRecord>;\n\n if (typeof parsed.platform !== 'string' || parsed.platform.length === 0)\n {\n throw new Error('\"platform\" must be a non-empty string');\n }\n\n if (typeof parsed.appVersion !== 'string' || parsed.appVersion.length === 0)\n {\n throw new Error('\"appVersion\" must be a non-empty string');\n }\n\n if (!Array.isArray(parsed.operations) || parsed.operations.some(name => typeof name !== 'string'))\n {\n throw new Error('\"operations\" must be an array of operation names');\n }\n\n // Absent is an app released before events were contracted: it subscribes to\n // none of them. Present and malformed is as unreadable as a bad \"operations\".\n const events = parsed.events ?? [];\n\n if (!Array.isArray(events) || events.some(name => typeof name !== 'string'))\n {\n throw new Error('\"events\" must be an array of event names');\n }\n\n return {\n platform: parsed.platform,\n appVersion: parsed.appVersion,\n operations: parsed.operations,\n events,\n file,\n };\n}\n\n/**\n * Read every usage file under `usageDir`.\n *\n * Returns undecidable — never an empty pass — when the directory is missing,\n * holds no usage file, or holds one that cannot be read.\n */\nexport function readUsageRecords(usageDir: string): UsageReadResult\n{\n let entries: string[];\n\n try\n {\n if (!statSync(usageDir).isDirectory())\n {\n return { decidable: false, reason: `${usageDir} is not a directory` };\n }\n\n entries = readdirSync(usageDir).filter(name => name.endsWith('.json')).sort();\n }\n catch\n {\n return { decidable: false, reason: `${usageDir} does not exist` };\n }\n\n if (entries.length === 0)\n {\n return { decidable: false, reason: `${usageDir} holds no usage file` };\n }\n\n const records: UsageRecord[] = [];\n\n for (const entry of entries)\n {\n const file = join(usageDir, entry);\n\n try\n {\n records.push(parseRecord(file, readFileSync(file, 'utf-8')));\n }\n catch (error)\n {\n const message = error instanceof Error ? error.message : String(error);\n\n return { decidable: false, reason: `${file} could not be read: ${message}` };\n }\n }\n\n return { decidable: true, records };\n}\n\n/** Which released clients still call `operation`. */\nexport function callersOf(operation: string, records: UsageRecord[]): UsageRecord[]\n{\n return records.filter(record => record.operations.includes(operation));\n}\n\n/** Which released clients still subscribe to `event`. */\nexport function subscribersOf(event: string, records: UsageRecord[]): UsageRecord[]\n{\n return records.filter(record => (record.events ?? []).includes(event));\n}\n","/**\n * The gate\n *\n * Puts the pieces together: read the newest released snapshot, compare this\n * build's contract against it, and decide removals against what released\n * clients still call or subscribe to.\n *\n * Nothing released yet is a pass — with a warning. \"This is the first contract\"\n * and \"the release that should have written a snapshot didn't\" produce the same\n * empty directory, and only a person can tell them apart.\n */\n\nimport { compareDocuments } from './compare';\nimport { newestSnapshot, readSnapshot, usageDir } from './snapshot';\nimport { callersOf, readUsageRecords, subscribersOf, type UsageRecord } from './usage';\nimport type { ContractDocument, ContractViolation } from './types';\n\nexport interface ContractCheckResult\n{\n /** Version compared against, absent when nothing is released yet. */\n baselineVersion?: string;\n\n violations: ContractViolation[];\n\n /** Things a person should look at that do not stop the build. */\n warnings: string[];\n}\n\nexport function checkContract(contractsDir: string, current: ContractDocument): ContractCheckResult\n{\n const baseline = newestSnapshot(contractsDir);\n\n if (!baseline)\n {\n return {\n violations: [],\n warnings: [\n 'No released contract snapshot found, so nothing was compared. '\n + 'This is expected for a first contract, and a mistake if a release forgot to write one.',\n ],\n };\n }\n\n let previous: ContractDocument;\n\n try\n {\n previous = readSnapshot(baseline.file).document;\n }\n catch (error)\n {\n return {\n baselineVersion: baseline.version,\n warnings: [],\n violations: [{\n kind: 'snapshot.digest-mismatch',\n detail: error instanceof Error ? error.message : String(error),\n }],\n };\n }\n\n const { violations, removedOperations, removedEvents } = compareDocuments(previous, current);\n\n return {\n baselineVersion: baseline.version,\n warnings: [],\n violations: [...violations, ...judgeRemovals(contractsDir, removedOperations, removedEvents)],\n };\n}\n\n/**\n * Decide whether removed operations and events may go.\n *\n * Only reached when something was actually removed — an app that removes\n * nothing never needs a usage file to exist. A removed event is judged exactly\n * as a removed operation, against the usage file's `events` list.\n */\nfunction judgeRemovals(contractsDir: string, removedOperations: string[], removedEvents: string[]): ContractViolation[]\n{\n const removed = [...removedOperations, ...removedEvents];\n\n if (removed.length === 0)\n {\n return [];\n }\n\n const usage = readUsageRecords(usageDir(contractsDir));\n\n if (!usage.decidable)\n {\n return [{\n kind: 'usage.undecidable',\n detail:\n `${removed.join(', ')} would be removed, but no released client's usage list could be read `\n + `(${usage.reason}). Not knowing who uses it is not the same as knowing nobody does.`,\n }];\n }\n\n return [\n ...removedOperations.flatMap(operation => stillUsed(\n { kind: 'usage.still-called', operation },\n 'still called by',\n callersOf(operation, usage.records),\n )),\n ...removedEvents.flatMap(event => stillUsed(\n { kind: 'usage.still-subscribed', event },\n 'still subscribed to by',\n subscribersOf(event, usage.records),\n )),\n ];\n}\n\n/** One violation naming every released client still using a removed name, or none. */\nfunction stillUsed(\n subject: Pick<ContractViolation, 'kind' | 'operation' | 'event'>,\n verb: string,\n users: UsageRecord[],\n): ContractViolation[]\n{\n if (users.length === 0)\n {\n return [];\n }\n\n return [{\n ...subject,\n detail: `${verb} ${users.map(user => `${user.platform} ${user.appVersion}`).join(', ')}`,\n }];\n}\n\n/** Render violations as the message a failing build prints. */\nexport function formatViolations(violations: ContractViolation[]): string\n{\n return violations\n .map((violation) =>\n {\n const where = [violation.operation ?? violation.event, violation.location].filter(Boolean).join(' ');\n\n return ` - [${violation.kind}]${where ? ` ${where}` : ''}: ${violation.detail}`;\n })\n .join('\\n');\n}\n"]}
|
package/dist/event/index.d.ts
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { TSchema, Static } from '@sinclair/typebox';
|
|
2
|
-
import {
|
|
3
|
-
export {
|
|
2
|
+
import { a as EventDef } from '../router-CysBmgv3.js';
|
|
3
|
+
export { b as EventContract, c as EventHandler, d as EventRouterContract, E as EventRouterDef, I as InferEventNames, e as InferEventPayload, f as InferEventPayloads, g as InferRouterEventPayload, J as JobQueueSender, P as PubSubCache, h as defineEventRouter } from '../router-CysBmgv3.js';
|
|
4
4
|
export { defineWSRouter } from './ws/index.js';
|
|
5
|
-
import '../types-
|
|
5
|
+
import '../types-W4u_S0lF.js';
|
|
6
6
|
import 'hono';
|
|
7
|
+
import '../token-manager-Ckjt2-qq.js';
|
|
7
8
|
import 'node:http';
|
|
8
9
|
|
|
9
10
|
/**
|
package/dist/event/index.js
CHANGED
|
@@ -102,9 +102,23 @@ function createEventImpl(name, schema) {
|
|
|
102
102
|
cache = void 0;
|
|
103
103
|
cacheSubscribed = false;
|
|
104
104
|
};
|
|
105
|
+
let contract;
|
|
106
|
+
const declareContract = (next) => {
|
|
107
|
+
if (contract) {
|
|
108
|
+
throw new Error(
|
|
109
|
+
`Event "${name}" already declares .contract({ since: '${contract.since}' }). An event is defined once and shared, so a second declaration would silently replace one of the two. Declare the contract once, where the event is defined.`
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
contract = next;
|
|
113
|
+
return self;
|
|
114
|
+
};
|
|
105
115
|
const self = {
|
|
106
116
|
name,
|
|
107
117
|
schema,
|
|
118
|
+
get _contract() {
|
|
119
|
+
return contract;
|
|
120
|
+
},
|
|
121
|
+
contract: declareContract,
|
|
108
122
|
subscribe: handlerManager.add,
|
|
109
123
|
unsubscribeAll: handlerManager.clear,
|
|
110
124
|
emit,
|
|
@@ -129,11 +143,28 @@ var eventRouteMap = {
|
|
|
129
143
|
|
|
130
144
|
// src/event/router.ts
|
|
131
145
|
function defineEventRouter(events) {
|
|
132
|
-
|
|
146
|
+
let contract;
|
|
147
|
+
const router = {
|
|
133
148
|
events,
|
|
134
149
|
eventNames: Object.keys(events),
|
|
150
|
+
get _contract() {
|
|
151
|
+
return contract;
|
|
152
|
+
},
|
|
153
|
+
contract: (next) => {
|
|
154
|
+
assertStreamPath(next.streamPath);
|
|
155
|
+
contract = next;
|
|
156
|
+
return router;
|
|
157
|
+
},
|
|
135
158
|
_types: {}
|
|
136
159
|
};
|
|
160
|
+
return router;
|
|
161
|
+
}
|
|
162
|
+
function assertStreamPath(streamPath) {
|
|
163
|
+
if (streamPath !== void 0 && !streamPath.startsWith("/")) {
|
|
164
|
+
throw new Error(
|
|
165
|
+
`defineEventRouter(...).contract({ streamPath: "${streamPath}" }) is not an absolute path. The contract publishes it as the URL path a client opens, and createServer compares it with the path .events() registers. Start it with "/".`
|
|
166
|
+
);
|
|
167
|
+
}
|
|
137
168
|
}
|
|
138
169
|
logger.child("@spfn/core:ws");
|
|
139
170
|
|
package/dist/event/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/event/event.ts","../../src/event/sse/route-map.ts","../../src/event/router.ts","../../src/event/ws/handler.ts","../../src/event/ws/index.ts"],"names":["logger"],"mappings":"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","file":"index.js","sourcesContent":["/**\n * Event System\n *\n * Decoupled pub/sub event system with optional cache integration for multi-instance support\n *\n * @example\n * ```typescript\n * // Define event\n * const userCreated = defineEvent('user.created', Type.Object({\n * userId: Type.String(),\n * }));\n *\n * // Subscribe (in-memory)\n * userCreated.subscribe((payload) => {\n * console.log('User created:', payload.userId);\n * });\n *\n * // Emit\n * await userCreated.emit({ userId: '123' });\n *\n * // With cache for multi-instance\n * const event = defineEvent('user.created', schema);\n * await event.useCache(cache); // Must await before emitting\n * await event.emit({ userId: '123' }); // Broadcast to all instances\n * ```\n */\n\nimport type { TSchema, Static } from '@sinclair/typebox';\nimport { logger } from '@spfn/core/logger';\nimport type { EventDef, EventHandler, JobQueueSender, PubSubCache } from './types';\n\nconst eventLogger = logger.child('@spfn/core:event');\n\n/**\n * Log handler error with consistent format\n */\nfunction logHandlerError(eventName: string, error: unknown): void\n{\n eventLogger.error(`Event handler error: ${eventName}`, {\n error: error instanceof Error ? error.message : String(error),\n });\n}\n\n/**\n * Log job queue error with consistent format\n */\nfunction logJobQueueError(queueName: string, error: unknown): void\n{\n eventLogger.error(`Failed to send event to job queue: ${queueName}`, {\n error: error instanceof Error ? error.message : String(error),\n });\n}\n\n/**\n * Create handler subscription manager\n */\nfunction createHandlerManager<TPayload>(name: string)\n{\n const handlers: Set<EventHandler<TPayload>> = new Set();\n\n return {\n add: (handler: EventHandler<TPayload>): (() => void) =>\n {\n handlers.add(handler);\n eventLogger.debug(`Subscribed to event: ${name}`, { handlerCount: handlers.size });\n\n return () =>\n {\n handlers.delete(handler);\n eventLogger.debug(`Unsubscribed from event: ${name}`, { handlerCount: handlers.size });\n };\n },\n\n clear: (): void =>\n {\n handlers.clear();\n eventLogger.debug(`Unsubscribed all from event: ${name}`);\n },\n\n trigger: async (payload: TPayload): Promise<void> =>\n {\n const results = await Promise.allSettled(\n [...handlers].map((handler) => handler(payload)),\n );\n\n for (const result of results)\n {\n if (result.status === 'rejected')\n {\n logHandlerError(name, result.reason);\n }\n }\n },\n };\n}\n\n/**\n * Create job queue manager\n */\nfunction createJobQueueManager(name: string)\n{\n const jobQueues: Map<string, JobQueueSender> = new Map();\n\n return {\n register: (queueName: string, sender: JobQueueSender): void =>\n {\n jobQueues.set(queueName, sender);\n eventLogger.debug(`Registered job queue for event: ${name}`, { queueName });\n },\n\n send: async (payload: unknown): Promise<void> =>\n {\n if (jobQueues.size === 0)\n {\n return;\n }\n\n const entries = [...jobQueues.entries()];\n const results = await Promise.allSettled(\n entries.map(([queueName, sender]) => sender(queueName, payload)),\n );\n\n for (const [i, result] of results.entries())\n {\n if (result.status === 'rejected')\n {\n logJobQueueError(entries[i][0], result.reason);\n }\n }\n },\n\n get size(): number\n {\n return jobQueues.size;\n },\n };\n}\n\n/**\n * Internal: Create event implementation\n */\nfunction createEventImpl<TPayload>(\n name: string,\n schema?: TSchema,\n): EventDef<TPayload>\n{\n const handlerManager = createHandlerManager<TPayload>(name);\n const jobQueueManager = createJobQueueManager(name);\n let cache: PubSubCache | undefined;\n let cacheSubscribed = false;\n\n const emit = async (payload?: TPayload): Promise<void> =>\n {\n eventLogger.debug(`Emitting event: ${name}`, {\n payload,\n hasCache: !!cache,\n jobQueueCount: jobQueueManager.size,\n });\n\n if (cache)\n {\n await cache.publish(name, payload);\n }\n else\n {\n await handlerManager.trigger(payload as TPayload);\n }\n\n await jobQueueManager.send(payload);\n eventLogger.debug(`Event emitted: ${name}`);\n };\n\n const useCache = async (newCache: PubSubCache): Promise<EventDef<TPayload>> =>\n {\n if (cacheSubscribed)\n {\n eventLogger.warn(`Cache already configured for event: ${name}`);\n\n return self;\n }\n\n // Subscribe FIRST, then flip to cache mode. If subscribe rejects, the\n // event stays in-process (cache undefined) and cacheSubscribed stays\n // false — so emit keeps delivering locally and a later useCache can retry.\n // Setting these before the await would latch a half-broken state: emit\n // would publish to a channel this instance never subscribed to.\n await newCache.subscribe(name, async (message: unknown) =>\n {\n eventLogger.debug(`Received event from cache: ${name}`);\n await handlerManager.trigger(message as TPayload);\n });\n\n cache = newCache;\n cacheSubscribed = true;\n\n eventLogger.debug(`Cache subscription ready for event: ${name}`);\n\n return self;\n };\n\n // Drop the cache binding so a later useCache can rebind a fresh transport.\n // Without this, the cacheSubscribed latch would make a second server start in\n // the same process (tests, hot-reload) a no-op while the event still points at\n // a torn-down cache — cross-pod fan-out would silently stay dead.\n const resetCache = (): void =>\n {\n cache = undefined;\n cacheSubscribed = false;\n };\n\n const self: EventDef<TPayload> = {\n name,\n schema,\n subscribe: handlerManager.add,\n unsubscribeAll: handlerManager.clear,\n emit: emit as EventDef<TPayload>['emit'],\n useCache,\n _registerJobQueue: jobQueueManager.register,\n _resetCache: resetCache,\n _payload: undefined as unknown as TPayload,\n };\n\n return self;\n}\n\n/**\n * Define an event without payload\n */\nexport function defineEvent(name: string): EventDef<void>;\n\n/**\n * Define an event with typed payload\n */\nexport function defineEvent<T extends TSchema>(\n name: string,\n schema: T,\n): EventDef<Static<T>>;\n\n/**\n * Define an event for decoupled pub/sub\n *\n * @example\n * ```typescript\n * // Define event with payload\n * export const userCreated = defineEvent('user.created', Type.Object({\n * userId: Type.String(),\n * }));\n *\n * // Subscribe to event (in-memory)\n * const unsubscribe = userCreated.subscribe((payload) => {\n * console.log('User created:', payload.userId);\n * });\n *\n * // Emit event\n * await userCreated.emit({ userId: '123' });\n *\n * // Unsubscribe when done\n * unsubscribe();\n *\n * // Multi-instance with cache\n * await userCreated.useCache(cache);\n * await userCreated.emit({ userId: '123' }); // Broadcast to all instances\n * ```\n */\nexport function defineEvent<T extends TSchema>(\n name: string,\n schema?: T,\n): EventDef<Static<T>> | EventDef\n{\n if (schema)\n {\n return createEventImpl<Static<T>>(name, schema);\n }\n\n return createEventImpl<void>(name);\n}\n","/**\n * SSE Event Route Map\n *\n * Static route map for SSE token endpoint.\n * Merge into RPC proxy routeMap so `eventsToken` resolves to `POST /events/token`.\n *\n * @example\n * ```typescript\n * // app/api/rpc/[routeName]/route.ts\n * import { createRpcProxy } from '@spfn/core/nextjs/server';\n * import { eventRouteMap } from '@spfn/core/event';\n * import { routeMap } from '@/generated/route-map';\n *\n * export const { GET, POST } = createRpcProxy({\n * routeMap: { ...routeMap, ...eventRouteMap },\n * });\n * ```\n */\nexport const eventRouteMap = {\n eventsToken: { method: 'POST' as const, path: '/events/token' },\n};\n","/**\n * Event Router\n *\n * Type-safe event router for SSE subscription\n *\n * @example\n * ```typescript\n * import { defineEvent, defineEventRouter } from '@spfn/core/event';\n * import { Type } from '@sinclair/typebox';\n *\n * const userCreated = defineEvent('user.created', Type.Object({\n * userId: Type.String(),\n * }));\n *\n * const orderPlaced = defineEvent('order.placed', Type.Object({\n * orderId: Type.String(),\n * amount: Type.Number(),\n * }));\n *\n * export const eventRouter = defineEventRouter({\n * userCreated,\n * orderPlaced,\n * });\n *\n * export type EventRouter = typeof eventRouter;\n * ```\n */\n\nimport type { EventDef } from './types';\n\n/**\n * Event Router Definition\n */\nexport interface EventRouterDef<TEvents extends Record<string, EventDef<any>>>\n{\n /**\n * Event definitions\n */\n readonly events: TEvents;\n\n /**\n * Event names as array\n */\n readonly eventNames: (keyof TEvents)[];\n\n /**\n * Type inference helper - payload types by event name\n */\n readonly _types: {\n [K in keyof TEvents]: TEvents[K]['_payload'];\n };\n}\n\n/**\n * Infer event names from EventRouter\n */\nexport type InferEventNames<T> = T extends EventRouterDef<infer E>\n ? keyof E & string\n : never;\n\n/**\n * Infer payload type for specific event\n */\nexport type InferEventPayload<\n T extends EventRouterDef<any>,\n K extends InferEventNames<T>,\n> = T['_types'][K];\n\n/**\n * Infer all event payloads map\n */\nexport type InferEventPayloads<T extends EventRouterDef<any>> = T['_types'];\n\n/**\n * Define an event router for SSE subscription\n *\n * @example\n * ```typescript\n * export const eventRouter = defineEventRouter({\n * userCreated,\n * orderPlaced,\n * });\n *\n * // Type inference\n * type Names = InferEventNames<typeof eventRouter>;\n * // 'userCreated' | 'orderPlaced'\n *\n * type Payload = InferEventPayload<typeof eventRouter, 'userCreated'>;\n * // { userId: string }\n * ```\n */\nexport function defineEventRouter<\n TEvents extends Record<string, EventDef<any>>,\n>(events: TEvents): EventRouterDef<TEvents>\n{\n return {\n events,\n eventNames: Object.keys(events) as (keyof TEvents)[],\n _types: {} as EventRouterDef<TEvents>['_types'],\n };\n}\n","/**\n * WebSocket Handler\n *\n * Attaches a WebSocket server to an existing Node.js http.Server.\n * Handles authentication, event subscription, and client message routing.\n */\n\nimport type { Server } from 'node:http';\nimport type { EventDef } from '../types';\nimport type {\n WSRouterDef,\n WSHandlerConfig,\n WSHandlerAuthConfig,\n WSMessageHandlers,\n WSRawConnection,\n} from './types';\nimport type { SSETokenManager } from '../sse/token-manager';\nimport { logger } from '@spfn/core/logger';\n\nconst wsLogger = logger.child('@spfn/core:ws');\n\n/**\n * Decide whether a WebSocket upgrade's Origin is permitted.\n *\n * - no allow-list configured → allowed (check disabled)\n * - no Origin header (native/non-browser client) → allowed\n * - Origin present → must be in the allow-list\n */\nexport function isOriginAllowed(\n origin: string | undefined,\n allowList: Set<string> | null,\n): boolean\n{\n if (!allowList)\n {\n return true;\n }\n\n if (!origin)\n {\n return true;\n }\n\n return allowList.has(origin);\n}\n\n// ============================================================================\n// Public API\n// ============================================================================\n\n/**\n * Attach a WebSocket server to a Node.js http.Server\n *\n * @returns cleanup function that closes the WebSocket server\n */\nexport async function attachWSHandler<\n TEvents extends Record<string, EventDef<any>>,\n TMessages extends WSMessageHandlers,\n>(\n server: Server,\n router: WSRouterDef<TEvents, TMessages>,\n config: WSHandlerConfig & { path?: string } = {},\n tokenManager?: SSETokenManager,\n): Promise<() => Promise<void>>\n{\n const WebSocketServer = await loadWSServer();\n\n const {\n pingInterval = 30000,\n path = '/ws',\n maxPayload = 1_048_576,\n maxBufferedBytes = 1_048_576,\n maxConnections = 10_000,\n maxConnectionsPerSubject = 0,\n allowedOrigins,\n auth: authConfig,\n } = config;\n\n const originAllowList = allowedOrigins && allowedOrigins.length > 0\n ? new Set(allowedOrigins)\n : null;\n\n if (authConfig?.enabled && !tokenManager)\n {\n throw new Error(\n 'WebSocket auth.enabled=true requires a tokenManager. ' +\n 'Pass tokenManager or use .websockets(router, { auth: { enabled: true } }) via startServer.',\n );\n }\n\n const wss = new WebSocketServer({ server, path, maxPayload });\n\n // Track live connections for graceful shutdown\n const clients = new Set<any>();\n // Live connection count per authenticated subject (for the per-subject cap)\n const subjectCounts = new Map<string, number>();\n\n wss.on('connection', (ws: any, req: any) =>\n {\n // Origin allow-list (cross-site WebSocket hijacking protection). Browsers\n // always send Origin on the upgrade; reject one that isn't allowed before\n // doing any work. A missing Origin (native/non-browser client, which can't\n // be CSRF'd into using ambient cookies) passes through.\n if (!isOriginAllowed(req.headers?.origin, originAllowList))\n {\n wsLogger.warn('WebSocket rejected: disallowed origin', { origin: req.headers?.origin });\n ws.close(1008, 'Origin not allowed');\n\n return;\n }\n\n // Global connection cap — reject before doing any work\n if (clients.size >= maxConnections)\n {\n ws.close(1013, 'Server at capacity');\n\n return;\n }\n\n clients.add(ws);\n ws.on('close', () => clients.delete(ws));\n handleConnection(ws, req, router, authConfig, tokenManager, {\n pingInterval,\n maxBufferedBytes,\n maxConnectionsPerSubject,\n subjectCounts,\n })\n .catch((err: Error) =>\n {\n wsLogger.error('WebSocket connection handler error', err);\n if (ws.readyState === 1) ws.close(1011, 'Internal server error');\n });\n });\n\n wss.on('error', (err: Error) =>\n {\n wsLogger.error('WebSocket server error', err);\n });\n\n wsLogger.info(`✓ WebSocket endpoint registered at ${path}`, {\n events: router.eventNames,\n auth: !!authConfig?.enabled,\n });\n\n return () => new Promise<void>((resolve, reject) =>\n {\n // Close all existing connections with 1001 Going Away\n for (const client of clients)\n {\n client.close(1001, 'Server shutting down');\n }\n clients.clear();\n\n wss.close((err?: Error) =>\n {\n if (err) reject(err);\n else resolve();\n });\n });\n}\n\n// ============================================================================\n// Connection Handler\n// ============================================================================\n\ninterface ConnectionOptions\n{\n pingInterval: number;\n maxBufferedBytes: number;\n maxConnectionsPerSubject: number;\n subjectCounts: Map<string, number>;\n}\n\nasync function handleConnection(\n ws: any,\n req: any,\n router: WSRouterDef<any, any>,\n authConfig: WSHandlerAuthConfig | undefined,\n tokenManager: SSETokenManager | undefined,\n opts: ConnectionOptions,\n): Promise<void>\n{\n const { pingInterval, maxBufferedBytes, maxConnectionsPerSubject, subjectCounts } = opts;\n // Register close handler before any await — ensures we never miss the event even during auth\n let pingTimer: ReturnType<typeof setInterval> | undefined;\n let connectionUnsubscribes: (() => void)[] = [];\n let subscribedEvents: string[] = [];\n ws.on('close', () =>\n {\n clearInterval(pingTimer);\n connectionUnsubscribes.forEach(fn => fn());\n if (subscribedEvents.length > 0)\n wsLogger.info('WebSocket connection closed', { events: subscribedEvents });\n });\n\n const url = parseURL(req);\n if (!url)\n {\n ws.close(1002, 'Invalid request URL');\n\n return;\n }\n\n // ── 1. Authenticate ──\n const subject = await resolveSubject(url, authConfig?.enabled ? tokenManager : undefined);\n if (subject === false)\n {\n ws.close(4001, 'Missing token');\n\n return;\n }\n if (subject === null)\n {\n ws.close(4001, 'Invalid or expired token');\n\n return;\n }\n\n // ── 2. Resolve subscribed events ──\n const requestedEvents = parseRequestedEvents(url, router.eventNames as string[]);\n if (requestedEvents.length === 0)\n {\n ws.close(4000, 'No valid event names specified');\n\n return;\n }\n\n // ── 3. Authorize ──\n const allowedEvents = await resolveAllowedEvents(subject, requestedEvents, authConfig);\n if (allowedEvents === null)\n {\n ws.close(4003, 'Not authorized for any requested events');\n\n return;\n }\n\n // ── Per-subject connection cap (authenticated subjects only) ──\n if (maxConnectionsPerSubject > 0 && typeof subject === 'string')\n {\n const current = subjectCounts.get(subject) ?? 0;\n if (current >= maxConnectionsPerSubject)\n {\n ws.close(1013, 'Too many connections for this subject');\n\n return;\n }\n\n subjectCounts.set(subject, current + 1);\n ws.on('close', () =>\n {\n const remaining = (subjectCounts.get(subject) ?? 1) - 1;\n if (remaining <= 0) subjectCounts.delete(subject);\n else subjectCounts.set(subject, remaining);\n });\n }\n\n subscribedEvents = allowedEvents;\n wsLogger.info('WebSocket connection established', {\n events: allowedEvents,\n subject: subject ?? undefined,\n });\n\n // ── 4. Build connection wrapper ──\n const connection = createConnection(ws, maxBufferedBytes);\n\n // ── 5. Subscribe to server-push events ──\n connectionUnsubscribes = subscribeEvents(ws, router, allowedEvents, subject, authConfig, maxBufferedBytes);\n\n // If socket closed during auth awaits, clean up and bail\n if (ws.readyState !== 1)\n {\n connectionUnsubscribes.forEach(fn => fn());\n connectionUnsubscribes = [];\n\n return;\n }\n\n // ── 6. Handle incoming messages ──\n ws.on('message', (data: Buffer | string) =>\n {\n onClientMessage(data, router, connection, subject)\n .catch((err: Error) => wsLogger.error('Unhandled message error', err));\n });\n\n // ── 7. Keep-alive ping with liveness (pong) tracking ──\n // Without reaping un-ponged sockets, a half-open connection (sleeping device,\n // NAT drop with no FIN) lingers with its subscriptions, timer, and buffers.\n if (pingInterval > 0)\n {\n ws.isAlive = true;\n ws.on('pong', () =>\n {\n ws.isAlive = true;\n });\n\n pingTimer = setInterval(() =>\n {\n if (ws.readyState !== 1) return;\n\n if (ws.isAlive === false)\n {\n // No pong since the previous tick — drop the dead/half-open socket\n ws.terminate();\n\n return;\n }\n\n ws.isAlive = false;\n ws.ping();\n }, pingInterval);\n }\n\n // ── 9. Send connected ack ──\n connection.send('__connected', {\n subscribedEvents: allowedEvents,\n timestamp: Date.now(),\n });\n}\n\n// ============================================================================\n// Helpers\n// ============================================================================\n\nfunction parseURL(req: any): URL | null\n{\n try\n {\n return new URL(req.url ?? '/', 'ws://localhost');\n }\n catch\n {\n return null;\n }\n}\n\n/**\n * Resolve subject from token\n * - undefined: no auth required\n * - false: token param missing (when required)\n * - null: token invalid/expired\n * - string: authenticated subject\n */\nasync function resolveSubject(\n url: URL,\n tokenManager?: SSETokenManager,\n): Promise<string | undefined | false | null>\n{\n if (!tokenManager)\n {\n return undefined;\n }\n\n const token = url.searchParams.get('token');\n if (!token)\n {\n return false;\n }\n\n return await tokenManager.verify(token);\n}\n\nfunction parseRequestedEvents(url: URL, validEventNames: string[]): string[]\n{\n const eventsParam = url.searchParams.get('events');\n if (!eventsParam)\n {\n return [];\n }\n\n return eventsParam\n .split(',')\n .map(e => e.trim())\n .filter(e => validEventNames.includes(e));\n}\n\nasync function resolveAllowedEvents(\n subject: string | undefined,\n requestedEvents: string[],\n authConfig?: WSHandlerAuthConfig,\n): Promise<string[] | null>\n{\n if (!subject || !authConfig?.authorize)\n {\n return requestedEvents;\n }\n\n const allowed = await authConfig.authorize(subject, requestedEvents);\n\n return allowed.length === 0 ? null : allowed;\n}\n\n/**\n * Send a JSON frame with backpressure protection. If the socket's outbound\n * buffer is already past the cap, the consumer is too slow — close the\n * connection (1013) instead of buffering more and risking OOM. The client\n * reconnects and re-subscribes.\n */\nexport function safeSend(ws: any, frame: unknown, maxBufferedBytes: number): void\n{\n if (ws.readyState !== 1) return;\n\n if (ws.bufferedAmount > maxBufferedBytes)\n {\n ws.close(1013, 'Send buffer overflow');\n\n return;\n }\n\n try\n {\n ws.send(JSON.stringify(frame));\n }\n catch\n {\n // Socket closed between the readyState check and send — ignore\n }\n}\n\nfunction createConnection(ws: any, maxBufferedBytes: number): WSRawConnection\n{\n return {\n send: (type, payload) => safeSend(ws, { type, data: payload }, maxBufferedBytes),\n close: (code, reason) => ws.close(code, reason),\n };\n}\n\nfunction subscribeEvents(\n ws: any,\n router: WSRouterDef<any, any>,\n allowedEvents: string[],\n subject: string | undefined,\n authConfig: WSHandlerAuthConfig | undefined,\n maxBufferedBytes: number,\n): (() => void)[]\n{\n const unsubscribes: (() => void)[] = [];\n\n for (const eventName of allowedEvents)\n {\n const eventDef = router.events[eventName];\n if (!eventDef) continue;\n\n const unsubscribe = eventDef.subscribe((payload: unknown) =>\n {\n if (ws.readyState !== 1) return;\n\n if (subject && authConfig?.filter?.[eventName])\n {\n if (!authConfig.filter[eventName](subject, payload)) return;\n }\n\n safeSend(ws, { type: eventName, data: payload }, maxBufferedBytes);\n });\n\n unsubscribes.push(unsubscribe);\n }\n\n return unsubscribes;\n}\n\nasync function onClientMessage(\n data: Buffer | string,\n router: WSRouterDef<any, any>,\n connection: WSRawConnection,\n subject: string | undefined,\n): Promise<void>\n{\n let message: { type?: string; data?: unknown };\n\n try\n {\n message = JSON.parse(data.toString());\n }\n catch\n {\n return;\n }\n\n const { type, data: payload } = message;\n if (!type) return;\n\n const handler = router.messages[type];\n if (!handler) return;\n\n try\n {\n await handler({ payload, subject, ws: connection });\n }\n catch (err)\n {\n wsLogger.error(`WebSocket message handler error: ${type}`, err as Error);\n }\n}\n\n// ============================================================================\n// Dynamic import for optional 'ws' dependency\n// ============================================================================\n\n/**\n * Resolve the WebSocketServer constructor from the 'ws' module object.\n *\n * ws is a dual package, so the module shape depends on the loader path:\n * - ESM (exports.import → wrapper.mjs): WebSocketServer is a named export;\n * the default export is the bare WebSocket class with no properties on it.\n * - CJS (require interop): module.exports = WebSocket with\n * WebSocket.WebSocketServer attached, surfaced under .default.\n * Named exports must win — checking .default first crashes on the ESM path.\n */\nexport function resolveWSServerCtor(mod: any): any\n{\n return mod.WebSocketServer ?? mod.Server\n ?? mod.default?.WebSocketServer ?? mod.default?.Server;\n}\n\nasync function loadWSServer(): Promise<any>\n{\n let mod: any;\n\n try\n {\n mod = await import('ws');\n }\n catch\n {\n throw new Error(\n '@spfn/core WebSocket support requires the \"ws\" package.\\n' +\n 'Install it with: pnpm add ws',\n );\n }\n\n const WSS = resolveWSServerCtor(mod);\n\n if (typeof WSS !== 'function')\n {\n throw new Error(\n 'WebSocketServer not found in ws module. ' +\n 'Ensure ws@^8 is installed: pnpm add ws',\n );\n }\n\n return WSS;\n}\n","/**\n * WebSocket Module\n *\n * Type-safe WebSocket server with event-based pub/sub and bidirectional messaging.\n *\n * @example Server setup\n * ```typescript\n * // src/server/ws.ts\n * import { defineWSRouter } from '@spfn/core/event/ws';\n * import { defineEvent } from '@spfn/core/event';\n * import { Type } from '@sinclair/typebox';\n *\n * const userUpdated = defineEvent('user.updated', Type.Object({ userId: Type.String() }));\n * const notification = defineEvent('notification', Type.Object({ message: Type.String() }));\n *\n * export const wsRouter = defineWSRouter({\n * events: { userUpdated, notification },\n * messages: {\n * ping: ({ ws }) => ws.send('pong', {}),\n * },\n * });\n *\n * export type WSRouter = typeof wsRouter;\n *\n * // server.config.ts\n * defineServerConfig()\n * .websockets(wsRouter)\n * .build();\n * ```\n *\n * @example Client usage\n * ```typescript\n * import { createWSClient } from '@spfn/core/event/ws/client';\n * import type { WSRouter } from '@/server/ws';\n *\n * const client = createWSClient<WSRouter>();\n *\n * client.subscribe({\n * events: ['userUpdated', 'notification'],\n * handlers: {\n * userUpdated: ({ userId }) => console.log(userId),\n * notification: ({ message }) => console.log(message),\n * },\n * });\n *\n * client.send('ping', {});\n * ```\n */\n\nimport type { EventDef } from '../types';\nimport type { WSRouterDef, WSMessageHandlers } from './types';\n\nexport { attachWSHandler } from './handler';\nexport type {\n WSRouterDef,\n WSHandlerConfig,\n WSAuthConfig,\n WSHandlerAuthConfig,\n WSMessageContext,\n WSMessageHandlerFn,\n WSMessageHandlers,\n WSRawConnection,\n WSClientConfig,\n WSConnectionState,\n WSEventHandlers,\n WSSubscribeOptions,\n WSUnsubscribe,\n} from './types';\n\n/**\n * Define a WebSocket router\n *\n * Combines server→client event push with client→server message handlers.\n *\n * @example\n * ```typescript\n * export const wsRouter = defineWSRouter({\n * events: { userUpdated, notification },\n * messages: {\n * ping: ({ ws }) => ws.send('pong', {}),\n * 'chat.send': ({ payload, subject }) => handleChat(payload, subject),\n * },\n * });\n * ```\n */\nexport function defineWSRouter<\n TEvents extends Record<string, EventDef<any>>,\n TMessages extends WSMessageHandlers = WSMessageHandlers,\n>(def: {\n events: TEvents;\n messages?: TMessages;\n}): WSRouterDef<TEvents, TMessages>\n{\n return {\n events: def.events,\n eventNames: Object.keys(def.events) as (keyof TEvents)[],\n messages: (def.messages ?? {}) as TMessages,\n _types: {} as WSRouterDef<TEvents, TMessages>['_types'],\n };\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["../../src/event/event.ts","../../src/event/sse/route-map.ts","../../src/event/router.ts","../../src/event/ws/handler.ts","../../src/event/ws/index.ts"],"names":["logger"],"mappings":";;;AA+BA,IAAM,WAAA,GAAc,MAAA,CAAO,KAAA,CAAM,kBAAkB,CAAA;AAKnD,SAAS,eAAA,CAAgB,WAAmB,KAAA,EAC5C;AACI,EAAA,WAAA,CAAY,KAAA,CAAM,CAAA,qBAAA,EAAwB,SAAS,CAAA,CAAA,EAAI;AAAA,IACnD,OAAO,KAAA,YAAiB,KAAA,GAAQ,KAAA,CAAM,OAAA,GAAU,OAAO,KAAK;AAAA,GAC/D,CAAA;AACL;AAKA,SAAS,gBAAA,CAAiB,WAAmB,KAAA,EAC7C;AACI,EAAA,WAAA,CAAY,KAAA,CAAM,CAAA,mCAAA,EAAsC,SAAS,CAAA,CAAA,EAAI;AAAA,IACjE,OAAO,KAAA,YAAiB,KAAA,GAAQ,KAAA,CAAM,OAAA,GAAU,OAAO,KAAK;AAAA,GAC/D,CAAA;AACL;AAKA,SAAS,qBAA+B,IAAA,EACxC;AACI,EAAA,MAAM,QAAA,uBAA4C,GAAA,EAAI;AAEtD,EAAA,OAAO;AAAA,IACH,GAAA,EAAK,CAAC,OAAA,KACN;AACI,MAAA,QAAA,CAAS,IAAI,OAAO,CAAA;AACpB,MAAA,WAAA,CAAY,KAAA,CAAM,wBAAwB,IAAI,CAAA,CAAA,EAAI,EAAE,YAAA,EAAc,QAAA,CAAS,MAAM,CAAA;AAEjF,MAAA,OAAO,MACP;AACI,QAAA,QAAA,CAAS,OAAO,OAAO,CAAA;AACvB,QAAA,WAAA,CAAY,KAAA,CAAM,4BAA4B,IAAI,CAAA,CAAA,EAAI,EAAE,YAAA,EAAc,QAAA,CAAS,MAAM,CAAA;AAAA,MACzF,CAAA;AAAA,IACJ,CAAA;AAAA,IAEA,OAAO,MACP;AACI,MAAA,QAAA,CAAS,KAAA,EAAM;AACf,MAAA,WAAA,CAAY,KAAA,CAAM,CAAA,6BAAA,EAAgC,IAAI,CAAA,CAAE,CAAA;AAAA,IAC5D,CAAA;AAAA,IAEA,OAAA,EAAS,OAAO,OAAA,KAChB;AACI,MAAA,MAAM,OAAA,GAAU,MAAM,OAAA,CAAQ,UAAA;AAAA,QAC1B,CAAC,GAAG,QAAQ,CAAA,CAAE,IAAI,CAAC,OAAA,KAAY,OAAA,CAAQ,OAAO,CAAC;AAAA,OACnD;AAEA,MAAA,KAAA,MAAW,UAAU,OAAA,EACrB;AACI,QAAA,IAAI,MAAA,CAAO,WAAW,UAAA,EACtB;AACI,UAAA,eAAA,CAAgB,IAAA,EAAM,OAAO,MAAM,CAAA;AAAA,QACvC;AAAA,MACJ;AAAA,IACJ;AAAA,GACJ;AACJ;AAKA,SAAS,sBAAsB,IAAA,EAC/B;AACI,EAAA,MAAM,SAAA,uBAA6C,GAAA,EAAI;AAEvD,EAAA,OAAO;AAAA,IACH,QAAA,EAAU,CAAC,SAAA,EAAmB,MAAA,KAC9B;AACI,MAAA,SAAA,CAAU,GAAA,CAAI,WAAW,MAAM,CAAA;AAC/B,MAAA,WAAA,CAAY,MAAM,CAAA,gCAAA,EAAmC,IAAI,CAAA,CAAA,EAAI,EAAE,WAAW,CAAA;AAAA,IAC9E,CAAA;AAAA,IAEA,IAAA,EAAM,OAAO,OAAA,KACb;AACI,MAAA,IAAI,SAAA,CAAU,SAAS,CAAA,EACvB;AACI,QAAA;AAAA,MACJ;AAEA,MAAA,MAAM,OAAA,GAAU,CAAC,GAAG,SAAA,CAAU,SAAS,CAAA;AACvC,MAAA,MAAM,OAAA,GAAU,MAAM,OAAA,CAAQ,UAAA;AAAA,QAC1B,OAAA,CAAQ,GAAA,CAAI,CAAC,CAAC,SAAA,EAAW,MAAM,CAAA,KAAM,MAAA,CAAO,SAAA,EAAW,OAAO,CAAC;AAAA,OACnE;AAEA,MAAA,KAAA,MAAW,CAAC,CAAA,EAAG,MAAM,CAAA,IAAK,OAAA,CAAQ,SAAQ,EAC1C;AACI,QAAA,IAAI,MAAA,CAAO,WAAW,UAAA,EACtB;AACI,UAAA,gBAAA,CAAiB,QAAQ,CAAC,CAAA,CAAE,CAAC,CAAA,EAAG,OAAO,MAAM,CAAA;AAAA,QACjD;AAAA,MACJ;AAAA,IACJ,CAAA;AAAA,IAEA,IAAI,IAAA,GACJ;AACI,MAAA,OAAO,SAAA,CAAU,IAAA;AAAA,IACrB;AAAA,GACJ;AACJ;AAKA,SAAS,eAAA,CACL,MACA,MAAA,EAEJ;AACI,EAAA,MAAM,cAAA,GAAiB,qBAA+B,IAAI,CAAA;AAC1D,EAAA,MAAM,eAAA,GAAkB,sBAAsB,IAAI,CAAA;AAClD,EAAA,IAAI,KAAA;AACJ,EAAA,IAAI,eAAA,GAAkB,KAAA;AAEtB,EAAA,MAAM,IAAA,GAAO,OAAO,OAAA,KACpB;AACI,IAAA,WAAA,CAAY,KAAA,CAAM,CAAA,gBAAA,EAAmB,IAAI,CAAA,CAAA,EAAI;AAAA,MACzC,OAAA;AAAA,MACA,QAAA,EAAU,CAAC,CAAC,KAAA;AAAA,MACZ,eAAe,eAAA,CAAgB;AAAA,KAClC,CAAA;AAED,IAAA,IAAI,KAAA,EACJ;AACI,MAAA,MAAM,KAAA,CAAM,OAAA,CAAQ,IAAA,EAAM,OAAO,CAAA;AAAA,IACrC,CAAA,MAEA;AACI,MAAA,MAAM,cAAA,CAAe,QAAQ,OAAmB,CAAA;AAAA,IACpD;AAEA,IAAA,MAAM,eAAA,CAAgB,KAAK,OAAO,CAAA;AAClC,IAAA,WAAA,CAAY,KAAA,CAAM,CAAA,eAAA,EAAkB,IAAI,CAAA,CAAE,CAAA;AAAA,EAC9C,CAAA;AAEA,EAAA,MAAM,QAAA,GAAW,OAAO,QAAA,KACxB;AACI,IAAA,IAAI,eAAA,EACJ;AACI,MAAA,WAAA,CAAY,IAAA,CAAK,CAAA,oCAAA,EAAuC,IAAI,CAAA,CAAE,CAAA;AAE9D,MAAA,OAAO,IAAA;AAAA,IACX;AAOA,IAAA,MAAM,QAAA,CAAS,SAAA,CAAU,IAAA,EAAM,OAAO,OAAA,KACtC;AACI,MAAA,WAAA,CAAY,KAAA,CAAM,CAAA,2BAAA,EAA8B,IAAI,CAAA,CAAE,CAAA;AACtD,MAAA,MAAM,cAAA,CAAe,QAAQ,OAAmB,CAAA;AAAA,IACpD,CAAC,CAAA;AAED,IAAA,KAAA,GAAQ,QAAA;AACR,IAAA,eAAA,GAAkB,IAAA;AAElB,IAAA,WAAA,CAAY,KAAA,CAAM,CAAA,oCAAA,EAAuC,IAAI,CAAA,CAAE,CAAA;AAE/D,IAAA,OAAO,IAAA;AAAA,EACX,CAAA;AAMA,EAAA,MAAM,aAAa,MACnB;AACI,IAAA,KAAA,GAAQ,MAAA;AACR,IAAA,eAAA,GAAkB,KAAA;AAAA,EACtB,CAAA;AAEA,EAAA,IAAI,QAAA;AAEJ,EAAA,MAAM,eAAA,GAAkB,CAAC,IAAA,KACzB;AACI,IAAA,IAAI,QAAA,EACJ;AACI,MAAA,MAAM,IAAI,KAAA;AAAA,QACN,CAAA,OAAA,EAAU,IAAI,CAAA,uCAAA,EAA0C,QAAA,CAAS,KAAK,CAAA,gKAAA;AAAA,OAG1E;AAAA,IACJ;AAEA,IAAA,QAAA,GAAW,IAAA;AAEX,IAAA,OAAO,IAAA;AAAA,EACX,CAAA;AAEA,EAAA,MAAM,IAAA,GAA2B;AAAA,IAC7B,IAAA;AAAA,IACA,MAAA;AAAA,IACA,IAAI,SAAA,GACJ;AACI,MAAA,OAAO,QAAA;AAAA,IACX,CAAA;AAAA,IACA,QAAA,EAAU,eAAA;AAAA,IACV,WAAW,cAAA,CAAe,GAAA;AAAA,IAC1B,gBAAgB,cAAA,CAAe,KAAA;AAAA,IAC/B,IAAA;AAAA,IACA,QAAA;AAAA,IACA,mBAAmB,eAAA,CAAgB,QAAA;AAAA,IACnC,WAAA,EAAa,UAAA;AAAA,IACb,QAAA,EAAU;AAAA,GACd;AAEA,EAAA,OAAO,IAAA;AACX;AAyCO,SAAS,WAAA,CACZ,MACA,MAAA,EAEJ;AACI,EAAA,IAAI,MAAA,EACJ;AACI,IAAA,OAAO,eAAA,CAA2B,MAAM,MAAM,CAAA;AAAA,EAClD;AAEA,EAAA,OAAO,gBAAsB,IAAI,CAAA;AACrC;;;ACxRO,IAAM,aAAA,GAAgB;AAAA,EACzB,WAAA,EAAa,EAAE,MAAA,EAAQ,MAAA,EAAiB,MAAM,eAAA;AAClD;;;ACyHO,SAAS,kBAEd,MAAA,EACF;AACI,EAAA,IAAI,QAAA;AAEJ,EAAA,MAAM,MAAA,GAAkC;AAAA,IACpC,MAAA;AAAA,IACA,UAAA,EAAY,MAAA,CAAO,IAAA,CAAK,MAAM,CAAA;AAAA,IAC9B,IAAI,SAAA,GACJ;AACI,MAAA,OAAO,QAAA;AAAA,IACX,CAAA;AAAA,IACA,QAAA,EAAU,CAAC,IAAA,KACX;AACI,MAAA,gBAAA,CAAiB,KAAK,UAAU,CAAA;AAChC,MAAA,QAAA,GAAW,IAAA;AAEX,MAAA,OAAO,MAAA;AAAA,IACX,CAAA;AAAA,IACA,QAAQ;AAAC,GACb;AAEA,EAAA,OAAO,MAAA;AACX;AAMA,SAAS,iBAAiB,UAAA,EAC1B;AACI,EAAA,IAAI,eAAe,MAAA,IAAa,CAAC,UAAA,CAAW,UAAA,CAAW,GAAG,CAAA,EAC1D;AACI,IAAA,MAAM,IAAI,KAAA;AAAA,MACN,kDAAkD,UAAU,CAAA,0KAAA;AAAA,KAGhE;AAAA,EACJ;AACJ;AClKiBA,MAAAA,CAAO,KAAA,CAAM,eAAe;;;ACkEtC,SAAS,eAGd,GAAA,EAIF;AACI,EAAA,OAAO;AAAA,IACH,QAAQ,GAAA,CAAI,MAAA;AAAA,IACZ,UAAA,EAAY,MAAA,CAAO,IAAA,CAAK,GAAA,CAAI,MAAM,CAAA;AAAA,IAClC,QAAA,EAAW,GAAA,CAAI,QAAA,IAAY,EAAC;AAAA,IAC5B,QAAQ;AAAC,GACb;AACJ","file":"index.js","sourcesContent":["/**\n * Event System\n *\n * Decoupled pub/sub event system with optional cache integration for multi-instance support\n *\n * @example\n * ```typescript\n * // Define event\n * const userCreated = defineEvent('user.created', Type.Object({\n * userId: Type.String(),\n * }));\n *\n * // Subscribe (in-memory)\n * userCreated.subscribe((payload) => {\n * console.log('User created:', payload.userId);\n * });\n *\n * // Emit\n * await userCreated.emit({ userId: '123' });\n *\n * // With cache for multi-instance\n * const event = defineEvent('user.created', schema);\n * await event.useCache(cache); // Must await before emitting\n * await event.emit({ userId: '123' }); // Broadcast to all instances\n * ```\n */\n\nimport type { TSchema, Static } from '@sinclair/typebox';\nimport { logger } from '@spfn/core/logger';\nimport type { EventContract, EventDef, EventHandler, JobQueueSender, PubSubCache } from './types';\n\nconst eventLogger = logger.child('@spfn/core:event');\n\n/**\n * Log handler error with consistent format\n */\nfunction logHandlerError(eventName: string, error: unknown): void\n{\n eventLogger.error(`Event handler error: ${eventName}`, {\n error: error instanceof Error ? error.message : String(error),\n });\n}\n\n/**\n * Log job queue error with consistent format\n */\nfunction logJobQueueError(queueName: string, error: unknown): void\n{\n eventLogger.error(`Failed to send event to job queue: ${queueName}`, {\n error: error instanceof Error ? error.message : String(error),\n });\n}\n\n/**\n * Create handler subscription manager\n */\nfunction createHandlerManager<TPayload>(name: string)\n{\n const handlers: Set<EventHandler<TPayload>> = new Set();\n\n return {\n add: (handler: EventHandler<TPayload>): (() => void) =>\n {\n handlers.add(handler);\n eventLogger.debug(`Subscribed to event: ${name}`, { handlerCount: handlers.size });\n\n return () =>\n {\n handlers.delete(handler);\n eventLogger.debug(`Unsubscribed from event: ${name}`, { handlerCount: handlers.size });\n };\n },\n\n clear: (): void =>\n {\n handlers.clear();\n eventLogger.debug(`Unsubscribed all from event: ${name}`);\n },\n\n trigger: async (payload: TPayload): Promise<void> =>\n {\n const results = await Promise.allSettled(\n [...handlers].map((handler) => handler(payload)),\n );\n\n for (const result of results)\n {\n if (result.status === 'rejected')\n {\n logHandlerError(name, result.reason);\n }\n }\n },\n };\n}\n\n/**\n * Create job queue manager\n */\nfunction createJobQueueManager(name: string)\n{\n const jobQueues: Map<string, JobQueueSender> = new Map();\n\n return {\n register: (queueName: string, sender: JobQueueSender): void =>\n {\n jobQueues.set(queueName, sender);\n eventLogger.debug(`Registered job queue for event: ${name}`, { queueName });\n },\n\n send: async (payload: unknown): Promise<void> =>\n {\n if (jobQueues.size === 0)\n {\n return;\n }\n\n const entries = [...jobQueues.entries()];\n const results = await Promise.allSettled(\n entries.map(([queueName, sender]) => sender(queueName, payload)),\n );\n\n for (const [i, result] of results.entries())\n {\n if (result.status === 'rejected')\n {\n logJobQueueError(entries[i][0], result.reason);\n }\n }\n },\n\n get size(): number\n {\n return jobQueues.size;\n },\n };\n}\n\n/**\n * Internal: Create event implementation\n */\nfunction createEventImpl<TPayload>(\n name: string,\n schema?: TSchema,\n): EventDef<TPayload>\n{\n const handlerManager = createHandlerManager<TPayload>(name);\n const jobQueueManager = createJobQueueManager(name);\n let cache: PubSubCache | undefined;\n let cacheSubscribed = false;\n\n const emit = async (payload?: TPayload): Promise<void> =>\n {\n eventLogger.debug(`Emitting event: ${name}`, {\n payload,\n hasCache: !!cache,\n jobQueueCount: jobQueueManager.size,\n });\n\n if (cache)\n {\n await cache.publish(name, payload);\n }\n else\n {\n await handlerManager.trigger(payload as TPayload);\n }\n\n await jobQueueManager.send(payload);\n eventLogger.debug(`Event emitted: ${name}`);\n };\n\n const useCache = async (newCache: PubSubCache): Promise<EventDef<TPayload>> =>\n {\n if (cacheSubscribed)\n {\n eventLogger.warn(`Cache already configured for event: ${name}`);\n\n return self;\n }\n\n // Subscribe FIRST, then flip to cache mode. If subscribe rejects, the\n // event stays in-process (cache undefined) and cacheSubscribed stays\n // false — so emit keeps delivering locally and a later useCache can retry.\n // Setting these before the await would latch a half-broken state: emit\n // would publish to a channel this instance never subscribed to.\n await newCache.subscribe(name, async (message: unknown) =>\n {\n eventLogger.debug(`Received event from cache: ${name}`);\n await handlerManager.trigger(message as TPayload);\n });\n\n cache = newCache;\n cacheSubscribed = true;\n\n eventLogger.debug(`Cache subscription ready for event: ${name}`);\n\n return self;\n };\n\n // Drop the cache binding so a later useCache can rebind a fresh transport.\n // Without this, the cacheSubscribed latch would make a second server start in\n // the same process (tests, hot-reload) a no-op while the event still points at\n // a torn-down cache — cross-pod fan-out would silently stay dead.\n const resetCache = (): void =>\n {\n cache = undefined;\n cacheSubscribed = false;\n };\n\n let contract: EventContract | undefined;\n\n const declareContract = (next: EventContract): EventDef<TPayload> =>\n {\n if (contract)\n {\n throw new Error(\n `Event \"${name}\" already declares .contract({ since: '${contract.since}' }). `\n + 'An event is defined once and shared, so a second declaration would silently replace one of the '\n + 'two. Declare the contract once, where the event is defined.',\n );\n }\n\n contract = next;\n\n return self;\n };\n\n const self: EventDef<TPayload> = {\n name,\n schema,\n get _contract()\n {\n return contract;\n },\n contract: declareContract,\n subscribe: handlerManager.add,\n unsubscribeAll: handlerManager.clear,\n emit: emit as EventDef<TPayload>['emit'],\n useCache,\n _registerJobQueue: jobQueueManager.register,\n _resetCache: resetCache,\n _payload: undefined as unknown as TPayload,\n };\n\n return self;\n}\n\n/**\n * Define an event without payload\n */\nexport function defineEvent(name: string): EventDef<void>;\n\n/**\n * Define an event with typed payload\n */\nexport function defineEvent<T extends TSchema>(\n name: string,\n schema: T,\n): EventDef<Static<T>>;\n\n/**\n * Define an event for decoupled pub/sub\n *\n * @example\n * ```typescript\n * // Define event with payload\n * export const userCreated = defineEvent('user.created', Type.Object({\n * userId: Type.String(),\n * }));\n *\n * // Subscribe to event (in-memory)\n * const unsubscribe = userCreated.subscribe((payload) => {\n * console.log('User created:', payload.userId);\n * });\n *\n * // Emit event\n * await userCreated.emit({ userId: '123' });\n *\n * // Unsubscribe when done\n * unsubscribe();\n *\n * // Multi-instance with cache\n * await userCreated.useCache(cache);\n * await userCreated.emit({ userId: '123' }); // Broadcast to all instances\n * ```\n */\nexport function defineEvent<T extends TSchema>(\n name: string,\n schema?: T,\n): EventDef<Static<T>> | EventDef\n{\n if (schema)\n {\n return createEventImpl<Static<T>>(name, schema);\n }\n\n return createEventImpl<void>(name);\n}\n","/**\n * SSE Event Route Map\n *\n * Static route map for SSE token endpoint.\n * Merge into RPC proxy routeMap so `eventsToken` resolves to `POST /events/token`.\n *\n * @example\n * ```typescript\n * // app/api/rpc/[routeName]/route.ts\n * import { createRpcProxy } from '@spfn/core/nextjs/server';\n * import { eventRouteMap } from '@spfn/core/event';\n * import { routeMap } from '@/generated/route-map';\n *\n * export const { GET, POST } = createRpcProxy({\n * routeMap: { ...routeMap, ...eventRouteMap },\n * });\n * ```\n */\nexport const eventRouteMap = {\n eventsToken: { method: 'POST' as const, path: '/events/token' },\n};\n","/**\n * Event Router\n *\n * Type-safe event router for SSE subscription\n *\n * @example\n * ```typescript\n * import { defineEvent, defineEventRouter } from '@spfn/core/event';\n * import { Type } from '@sinclair/typebox';\n *\n * const userCreated = defineEvent('user.created', Type.Object({\n * userId: Type.String(),\n * }));\n *\n * const orderPlaced = defineEvent('order.placed', Type.Object({\n * orderId: Type.String(),\n * amount: Type.Number(),\n * }));\n *\n * export const eventRouter = defineEventRouter({\n * userCreated,\n * orderPlaced,\n * });\n *\n * export type EventRouter = typeof eventRouter;\n * ```\n */\n\nimport type { EventDef } from './types';\n\n/**\n * How a client reaches the contracted events of a router.\n *\n * Declared on the router because it is the stream's promise, not one event's:\n * `createServer` checks it against `.events()` at boot, so the document cannot\n * describe a path or an auth mode the server does not serve.\n */\nexport interface EventRouterContract\n{\n /**\n * `'tokenExchange'` when `.events()` enables `auth` — the client first posts\n * to the token path and opens the stream with `?token=`. `'none'` otherwise.\n */\n auth: 'none' | 'tokenExchange';\n\n /**\n * The stream path `.events()` registers.\n * @default '/events/stream'\n */\n streamPath?: string;\n}\n\n/**\n * What every router of events carries — the SSE event router and the\n * WebSocket router alike.\n *\n * The WebSocket router builds on this rather than on `EventRouterDef`, because\n * only an SSE event router can be contracted.\n */\nexport interface EventRouterBase<TEvents extends Record<string, EventDef<any>>>\n{\n /**\n * Event definitions\n */\n readonly events: TEvents;\n\n /**\n * Event names as array\n */\n readonly eventNames: (keyof TEvents)[];\n\n /**\n * Type inference helper - payload types by event name\n */\n readonly _types: {\n [K in keyof TEvents]: TEvents[K]['_payload'];\n };\n}\n\n/**\n * Event Router Definition\n */\nexport interface EventRouterDef<TEvents extends Record<string, EventDef<any>>> extends EventRouterBase<TEvents>\n{\n /**\n * The contract declared with `.contract()`, absent when the router declares\n * none. Underscored for the same reason as `EventDef._contract`.\n */\n readonly _contract?: EventRouterContract;\n\n /**\n * Declare how a client reaches this router's contracted events.\n *\n * Sets the contract on this router and returns it. A `streamPath` that does\n * not start with `/` throws here, where the typo is.\n */\n contract: (contract: EventRouterContract) => EventRouterDef<TEvents>;\n}\n\n/**\n * Infer event names from EventRouter\n */\nexport type InferEventNames<T> = T extends EventRouterBase<infer E>\n ? keyof E & string\n : never;\n\n/**\n * Infer payload type for specific event\n */\nexport type InferEventPayload<\n T extends EventRouterBase<any>,\n K extends InferEventNames<T>,\n> = T['_types'][K];\n\n/**\n * Infer all event payloads map\n */\nexport type InferEventPayloads<T extends EventRouterBase<any>> = T['_types'];\n\n/**\n * Define an event router for SSE subscription\n *\n * `.contract({ auth })` on the router, together with `.contract({ since })` on\n * its events, publishes those events in `contracts/current.json` — see the\n * contract README.\n *\n * @example\n * ```typescript\n * export const eventRouter = defineEventRouter({\n * userCreated,\n * orderPlaced,\n * });\n *\n * // Type inference\n * type Names = InferEventNames<typeof eventRouter>;\n * // 'userCreated' | 'orderPlaced'\n *\n * type Payload = InferEventPayload<typeof eventRouter, 'userCreated'>;\n * // { userId: string }\n * ```\n */\nexport function defineEventRouter<\n TEvents extends Record<string, EventDef<any>>,\n>(events: TEvents): EventRouterDef<TEvents>\n{\n let contract: EventRouterContract | undefined;\n\n const router: EventRouterDef<TEvents> = {\n events,\n eventNames: Object.keys(events) as (keyof TEvents)[],\n get _contract()\n {\n return contract;\n },\n contract: (next) =>\n {\n assertStreamPath(next.streamPath);\n contract = next;\n\n return router;\n },\n _types: {} as EventRouterDef<TEvents>['_types'],\n };\n\n return router;\n}\n\n/**\n * A stream path is matched against the path `.events()` registers and written\n * into the contract as a URL path, so a relative one can never be right.\n */\nfunction assertStreamPath(streamPath: string | undefined): void\n{\n if (streamPath !== undefined && !streamPath.startsWith('/'))\n {\n throw new Error(\n `defineEventRouter(...).contract({ streamPath: \"${streamPath}\" }) is not an absolute path. `\n + 'The contract publishes it as the URL path a client opens, and createServer compares it with the '\n + 'path .events() registers. Start it with \"/\".',\n );\n }\n}\n","/**\n * WebSocket Handler\n *\n * Attaches a WebSocket server to an existing Node.js http.Server.\n * Handles authentication, event subscription, and client message routing.\n */\n\nimport type { Server } from 'node:http';\nimport type { EventDef } from '../types';\nimport type {\n WSRouterDef,\n WSHandlerConfig,\n WSHandlerAuthConfig,\n WSMessageHandlers,\n WSRawConnection,\n} from './types';\nimport type { SSETokenManager } from '../sse/token-manager';\nimport { logger } from '@spfn/core/logger';\n\nconst wsLogger = logger.child('@spfn/core:ws');\n\n/**\n * Decide whether a WebSocket upgrade's Origin is permitted.\n *\n * - no allow-list configured → allowed (check disabled)\n * - no Origin header (native/non-browser client) → allowed\n * - Origin present → must be in the allow-list\n */\nexport function isOriginAllowed(\n origin: string | undefined,\n allowList: Set<string> | null,\n): boolean\n{\n if (!allowList)\n {\n return true;\n }\n\n if (!origin)\n {\n return true;\n }\n\n return allowList.has(origin);\n}\n\n// ============================================================================\n// Public API\n// ============================================================================\n\n/**\n * Attach a WebSocket server to a Node.js http.Server\n *\n * @returns cleanup function that closes the WebSocket server\n */\nexport async function attachWSHandler<\n TEvents extends Record<string, EventDef<any>>,\n TMessages extends WSMessageHandlers,\n>(\n server: Server,\n router: WSRouterDef<TEvents, TMessages>,\n config: WSHandlerConfig & { path?: string } = {},\n tokenManager?: SSETokenManager,\n): Promise<() => Promise<void>>\n{\n const WebSocketServer = await loadWSServer();\n\n const {\n pingInterval = 30000,\n path = '/ws',\n maxPayload = 1_048_576,\n maxBufferedBytes = 1_048_576,\n maxConnections = 10_000,\n maxConnectionsPerSubject = 0,\n allowedOrigins,\n auth: authConfig,\n } = config;\n\n const originAllowList = allowedOrigins && allowedOrigins.length > 0\n ? new Set(allowedOrigins)\n : null;\n\n if (authConfig?.enabled && !tokenManager)\n {\n throw new Error(\n 'WebSocket auth.enabled=true requires a tokenManager. ' +\n 'Pass tokenManager or use .websockets(router, { auth: { enabled: true } }) via startServer.',\n );\n }\n\n const wss = new WebSocketServer({ server, path, maxPayload });\n\n // Track live connections for graceful shutdown\n const clients = new Set<any>();\n // Live connection count per authenticated subject (for the per-subject cap)\n const subjectCounts = new Map<string, number>();\n\n wss.on('connection', (ws: any, req: any) =>\n {\n // Origin allow-list (cross-site WebSocket hijacking protection). Browsers\n // always send Origin on the upgrade; reject one that isn't allowed before\n // doing any work. A missing Origin (native/non-browser client, which can't\n // be CSRF'd into using ambient cookies) passes through.\n if (!isOriginAllowed(req.headers?.origin, originAllowList))\n {\n wsLogger.warn('WebSocket rejected: disallowed origin', { origin: req.headers?.origin });\n ws.close(1008, 'Origin not allowed');\n\n return;\n }\n\n // Global connection cap — reject before doing any work\n if (clients.size >= maxConnections)\n {\n ws.close(1013, 'Server at capacity');\n\n return;\n }\n\n clients.add(ws);\n ws.on('close', () => clients.delete(ws));\n handleConnection(ws, req, router, authConfig, tokenManager, {\n pingInterval,\n maxBufferedBytes,\n maxConnectionsPerSubject,\n subjectCounts,\n })\n .catch((err: Error) =>\n {\n wsLogger.error('WebSocket connection handler error', err);\n if (ws.readyState === 1) ws.close(1011, 'Internal server error');\n });\n });\n\n wss.on('error', (err: Error) =>\n {\n wsLogger.error('WebSocket server error', err);\n });\n\n wsLogger.info(`✓ WebSocket endpoint registered at ${path}`, {\n events: router.eventNames,\n auth: !!authConfig?.enabled,\n });\n\n return () => new Promise<void>((resolve, reject) =>\n {\n // Close all existing connections with 1001 Going Away\n for (const client of clients)\n {\n client.close(1001, 'Server shutting down');\n }\n clients.clear();\n\n wss.close((err?: Error) =>\n {\n if (err) reject(err);\n else resolve();\n });\n });\n}\n\n// ============================================================================\n// Connection Handler\n// ============================================================================\n\ninterface ConnectionOptions\n{\n pingInterval: number;\n maxBufferedBytes: number;\n maxConnectionsPerSubject: number;\n subjectCounts: Map<string, number>;\n}\n\nasync function handleConnection(\n ws: any,\n req: any,\n router: WSRouterDef<any, any>,\n authConfig: WSHandlerAuthConfig | undefined,\n tokenManager: SSETokenManager | undefined,\n opts: ConnectionOptions,\n): Promise<void>\n{\n const { pingInterval, maxBufferedBytes, maxConnectionsPerSubject, subjectCounts } = opts;\n // Register close handler before any await — ensures we never miss the event even during auth\n let pingTimer: ReturnType<typeof setInterval> | undefined;\n let connectionUnsubscribes: (() => void)[] = [];\n let subscribedEvents: string[] = [];\n ws.on('close', () =>\n {\n clearInterval(pingTimer);\n connectionUnsubscribes.forEach(fn => fn());\n if (subscribedEvents.length > 0)\n wsLogger.info('WebSocket connection closed', { events: subscribedEvents });\n });\n\n const url = parseURL(req);\n if (!url)\n {\n ws.close(1002, 'Invalid request URL');\n\n return;\n }\n\n // ── 1. Authenticate ──\n const subject = await resolveSubject(url, authConfig?.enabled ? tokenManager : undefined);\n if (subject === false)\n {\n ws.close(4001, 'Missing token');\n\n return;\n }\n if (subject === null)\n {\n ws.close(4001, 'Invalid or expired token');\n\n return;\n }\n\n // ── 2. Resolve subscribed events ──\n const requestedEvents = parseRequestedEvents(url, router.eventNames as string[]);\n if (requestedEvents.length === 0)\n {\n ws.close(4000, 'No valid event names specified');\n\n return;\n }\n\n // ── 3. Authorize ──\n const allowedEvents = await resolveAllowedEvents(subject, requestedEvents, authConfig);\n if (allowedEvents === null)\n {\n ws.close(4003, 'Not authorized for any requested events');\n\n return;\n }\n\n // ── Per-subject connection cap (authenticated subjects only) ──\n if (maxConnectionsPerSubject > 0 && typeof subject === 'string')\n {\n const current = subjectCounts.get(subject) ?? 0;\n if (current >= maxConnectionsPerSubject)\n {\n ws.close(1013, 'Too many connections for this subject');\n\n return;\n }\n\n subjectCounts.set(subject, current + 1);\n ws.on('close', () =>\n {\n const remaining = (subjectCounts.get(subject) ?? 1) - 1;\n if (remaining <= 0) subjectCounts.delete(subject);\n else subjectCounts.set(subject, remaining);\n });\n }\n\n subscribedEvents = allowedEvents;\n wsLogger.info('WebSocket connection established', {\n events: allowedEvents,\n subject: subject ?? undefined,\n });\n\n // ── 4. Build connection wrapper ──\n const connection = createConnection(ws, maxBufferedBytes);\n\n // ── 5. Subscribe to server-push events ──\n connectionUnsubscribes = subscribeEvents(ws, router, allowedEvents, subject, authConfig, maxBufferedBytes);\n\n // If socket closed during auth awaits, clean up and bail\n if (ws.readyState !== 1)\n {\n connectionUnsubscribes.forEach(fn => fn());\n connectionUnsubscribes = [];\n\n return;\n }\n\n // ── 6. Handle incoming messages ──\n ws.on('message', (data: Buffer | string) =>\n {\n onClientMessage(data, router, connection, subject)\n .catch((err: Error) => wsLogger.error('Unhandled message error', err));\n });\n\n // ── 7. Keep-alive ping with liveness (pong) tracking ──\n // Without reaping un-ponged sockets, a half-open connection (sleeping device,\n // NAT drop with no FIN) lingers with its subscriptions, timer, and buffers.\n if (pingInterval > 0)\n {\n ws.isAlive = true;\n ws.on('pong', () =>\n {\n ws.isAlive = true;\n });\n\n pingTimer = setInterval(() =>\n {\n if (ws.readyState !== 1) return;\n\n if (ws.isAlive === false)\n {\n // No pong since the previous tick — drop the dead/half-open socket\n ws.terminate();\n\n return;\n }\n\n ws.isAlive = false;\n ws.ping();\n }, pingInterval);\n }\n\n // ── 9. Send connected ack ──\n connection.send('__connected', {\n subscribedEvents: allowedEvents,\n timestamp: Date.now(),\n });\n}\n\n// ============================================================================\n// Helpers\n// ============================================================================\n\nfunction parseURL(req: any): URL | null\n{\n try\n {\n return new URL(req.url ?? '/', 'ws://localhost');\n }\n catch\n {\n return null;\n }\n}\n\n/**\n * Resolve subject from token\n * - undefined: no auth required\n * - false: token param missing (when required)\n * - null: token invalid/expired\n * - string: authenticated subject\n */\nasync function resolveSubject(\n url: URL,\n tokenManager?: SSETokenManager,\n): Promise<string | undefined | false | null>\n{\n if (!tokenManager)\n {\n return undefined;\n }\n\n const token = url.searchParams.get('token');\n if (!token)\n {\n return false;\n }\n\n return await tokenManager.verify(token);\n}\n\nfunction parseRequestedEvents(url: URL, validEventNames: string[]): string[]\n{\n const eventsParam = url.searchParams.get('events');\n if (!eventsParam)\n {\n return [];\n }\n\n return eventsParam\n .split(',')\n .map(e => e.trim())\n .filter(e => validEventNames.includes(e));\n}\n\nasync function resolveAllowedEvents(\n subject: string | undefined,\n requestedEvents: string[],\n authConfig?: WSHandlerAuthConfig,\n): Promise<string[] | null>\n{\n if (!subject || !authConfig?.authorize)\n {\n return requestedEvents;\n }\n\n const allowed = await authConfig.authorize(subject, requestedEvents);\n\n return allowed.length === 0 ? null : allowed;\n}\n\n/**\n * Send a JSON frame with backpressure protection. If the socket's outbound\n * buffer is already past the cap, the consumer is too slow — close the\n * connection (1013) instead of buffering more and risking OOM. The client\n * reconnects and re-subscribes.\n */\nexport function safeSend(ws: any, frame: unknown, maxBufferedBytes: number): void\n{\n if (ws.readyState !== 1) return;\n\n if (ws.bufferedAmount > maxBufferedBytes)\n {\n ws.close(1013, 'Send buffer overflow');\n\n return;\n }\n\n try\n {\n ws.send(JSON.stringify(frame));\n }\n catch\n {\n // Socket closed between the readyState check and send — ignore\n }\n}\n\nfunction createConnection(ws: any, maxBufferedBytes: number): WSRawConnection\n{\n return {\n send: (type, payload) => safeSend(ws, { type, data: payload }, maxBufferedBytes),\n close: (code, reason) => ws.close(code, reason),\n };\n}\n\nfunction subscribeEvents(\n ws: any,\n router: WSRouterDef<any, any>,\n allowedEvents: string[],\n subject: string | undefined,\n authConfig: WSHandlerAuthConfig | undefined,\n maxBufferedBytes: number,\n): (() => void)[]\n{\n const unsubscribes: (() => void)[] = [];\n\n for (const eventName of allowedEvents)\n {\n const eventDef = router.events[eventName];\n if (!eventDef) continue;\n\n const unsubscribe = eventDef.subscribe((payload: unknown) =>\n {\n if (ws.readyState !== 1) return;\n\n if (subject && authConfig?.filter?.[eventName])\n {\n if (!authConfig.filter[eventName](subject, payload)) return;\n }\n\n safeSend(ws, { type: eventName, data: payload }, maxBufferedBytes);\n });\n\n unsubscribes.push(unsubscribe);\n }\n\n return unsubscribes;\n}\n\nasync function onClientMessage(\n data: Buffer | string,\n router: WSRouterDef<any, any>,\n connection: WSRawConnection,\n subject: string | undefined,\n): Promise<void>\n{\n let message: { type?: string; data?: unknown };\n\n try\n {\n message = JSON.parse(data.toString());\n }\n catch\n {\n return;\n }\n\n const { type, data: payload } = message;\n if (!type) return;\n\n const handler = router.messages[type];\n if (!handler) return;\n\n try\n {\n await handler({ payload, subject, ws: connection });\n }\n catch (err)\n {\n wsLogger.error(`WebSocket message handler error: ${type}`, err as Error);\n }\n}\n\n// ============================================================================\n// Dynamic import for optional 'ws' dependency\n// ============================================================================\n\n/**\n * Resolve the WebSocketServer constructor from the 'ws' module object.\n *\n * ws is a dual package, so the module shape depends on the loader path:\n * - ESM (exports.import → wrapper.mjs): WebSocketServer is a named export;\n * the default export is the bare WebSocket class with no properties on it.\n * - CJS (require interop): module.exports = WebSocket with\n * WebSocket.WebSocketServer attached, surfaced under .default.\n * Named exports must win — checking .default first crashes on the ESM path.\n */\nexport function resolveWSServerCtor(mod: any): any\n{\n return mod.WebSocketServer ?? mod.Server\n ?? mod.default?.WebSocketServer ?? mod.default?.Server;\n}\n\nasync function loadWSServer(): Promise<any>\n{\n let mod: any;\n\n try\n {\n mod = await import('ws');\n }\n catch\n {\n throw new Error(\n '@spfn/core WebSocket support requires the \"ws\" package.\\n' +\n 'Install it with: pnpm add ws',\n );\n }\n\n const WSS = resolveWSServerCtor(mod);\n\n if (typeof WSS !== 'function')\n {\n throw new Error(\n 'WebSocketServer not found in ws module. ' +\n 'Ensure ws@^8 is installed: pnpm add ws',\n );\n }\n\n return WSS;\n}\n","/**\n * WebSocket Module\n *\n * Type-safe WebSocket server with event-based pub/sub and bidirectional messaging.\n *\n * @example Server setup\n * ```typescript\n * // src/server/ws.ts\n * import { defineWSRouter } from '@spfn/core/event/ws';\n * import { defineEvent } from '@spfn/core/event';\n * import { Type } from '@sinclair/typebox';\n *\n * const userUpdated = defineEvent('user.updated', Type.Object({ userId: Type.String() }));\n * const notification = defineEvent('notification', Type.Object({ message: Type.String() }));\n *\n * export const wsRouter = defineWSRouter({\n * events: { userUpdated, notification },\n * messages: {\n * ping: ({ ws }) => ws.send('pong', {}),\n * },\n * });\n *\n * export type WSRouter = typeof wsRouter;\n *\n * // server.config.ts\n * defineServerConfig()\n * .websockets(wsRouter)\n * .build();\n * ```\n *\n * @example Client usage\n * ```typescript\n * import { createWSClient } from '@spfn/core/event/ws/client';\n * import type { WSRouter } from '@/server/ws';\n *\n * const client = createWSClient<WSRouter>();\n *\n * client.subscribe({\n * events: ['userUpdated', 'notification'],\n * handlers: {\n * userUpdated: ({ userId }) => console.log(userId),\n * notification: ({ message }) => console.log(message),\n * },\n * });\n *\n * client.send('ping', {});\n * ```\n */\n\nimport type { EventDef } from '../types';\nimport type { WSRouterDef, WSMessageHandlers } from './types';\n\nexport { attachWSHandler } from './handler';\nexport type {\n WSRouterDef,\n WSHandlerConfig,\n WSAuthConfig,\n WSHandlerAuthConfig,\n WSMessageContext,\n WSMessageHandlerFn,\n WSMessageHandlers,\n WSRawConnection,\n WSClientConfig,\n WSConnectionState,\n WSEventHandlers,\n WSSubscribeOptions,\n WSUnsubscribe,\n} from './types';\n\n/**\n * Define a WebSocket router\n *\n * Combines server→client event push with client→server message handlers.\n *\n * @example\n * ```typescript\n * export const wsRouter = defineWSRouter({\n * events: { userUpdated, notification },\n * messages: {\n * ping: ({ ws }) => ws.send('pong', {}),\n * 'chat.send': ({ payload, subject }) => handleChat(payload, subject),\n * },\n * });\n * ```\n */\nexport function defineWSRouter<\n TEvents extends Record<string, EventDef<any>>,\n TMessages extends WSMessageHandlers = WSMessageHandlers,\n>(def: {\n events: TEvents;\n messages?: TMessages;\n}): WSRouterDef<TEvents, TMessages>\n{\n return {\n events: def.events,\n eventNames: Object.keys(def.events) as (keyof TEvents)[],\n messages: (def.messages ?? {}) as TMessages,\n _types: {} as WSRouterDef<TEvents, TMessages>['_types'],\n };\n}\n"]}
|
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { S as SSEClientConfig, a as SSESubscribeOptions, b as SSEUnsubscribe, c as SSEConnectionState } from '../../types-
|
|
1
|
+
import { E as EventRouterDef, I as InferEventNames } from '../../router-CysBmgv3.js';
|
|
2
|
+
import { S as SSEClientConfig, a as SSESubscribeOptions, b as SSEUnsubscribe, c as SSEConnectionState } from '../../types-B8zAxFsE.js';
|
|
3
3
|
import '@sinclair/typebox';
|
|
4
4
|
import 'hono';
|
|
5
|
+
import '../../token-manager-Ckjt2-qq.js';
|
|
5
6
|
|
|
6
7
|
/**
|
|
7
8
|
* SSE Client
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { Context } from 'hono';
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
2
|
+
import { E as EventRouterDef } from '../../router-CysBmgv3.js';
|
|
3
|
+
import { d as SSEHandlerConfig } from '../../types-B8zAxFsE.js';
|
|
4
|
+
export { e as SSEAuthConfig, S as SSEClientConfig, c as SSEConnectionState, f as SSEEventHandler, g as SSEEventHandlers, h as SSEHandlerAuthConfig, i as SSEMessage, a as SSESubscribeOptions, b as SSEUnsubscribe } from '../../types-B8zAxFsE.js';
|
|
5
|
+
import { S as SSETokenManager } from '../../token-manager-Ckjt2-qq.js';
|
|
6
|
+
export { C as CacheTokenStore, a as SSEToken, b as SSETokenManagerConfig, c as SSETokenStore } from '../../token-manager-Ckjt2-qq.js';
|
|
6
7
|
import '@sinclair/typebox';
|
|
7
8
|
|
|
8
9
|
/**
|
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import { W as WSRouterDef, a as WSSubscribeOptions, b as WSUnsubscribe, c as WSConnectionState, d as WSClientConfig } from '../../types-
|
|
1
|
+
import { W as WSRouterDef, a as WSSubscribeOptions, b as WSUnsubscribe, c as WSConnectionState, d as WSClientConfig } from '../../types-W4u_S0lF.js';
|
|
2
2
|
import 'hono';
|
|
3
|
-
import '../../
|
|
3
|
+
import '../../router-CysBmgv3.js';
|
|
4
4
|
import '@sinclair/typebox';
|
|
5
|
+
import '../../token-manager-Ckjt2-qq.js';
|
|
5
6
|
|
|
6
7
|
/**
|
|
7
8
|
* WebSocket Client
|