@babelqueue/core 0.1.0 → 1.1.0

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/index.cjs CHANGED
@@ -22,12 +22,16 @@ var index_exports = {};
22
22
  __export(index_exports, {
23
23
  BabelQueueError: () => BabelQueueError,
24
24
  EnvelopeCodec: () => EnvelopeCodec,
25
+ InMemoryStore: () => InMemoryStore,
26
+ InvalidPayloadError: () => InvalidPayloadError,
25
27
  SCHEMA_VERSION: () => SCHEMA_VERSION,
26
28
  SOURCE_LANG: () => SOURCE_LANG,
27
29
  UnknownUrnError: () => UnknownUrnError,
28
30
  UnknownUrnStrategy: () => UnknownUrnStrategy,
31
+ Wrap: () => Wrap,
29
32
  annotate: () => annotate,
30
- deadLetter: () => deadLetter_exports
33
+ deadLetter: () => deadLetter_exports,
34
+ schema: () => schema_exports
31
35
  });
32
36
  module.exports = __toCommonJS(index_exports);
33
37
 
@@ -47,6 +51,16 @@ var UnknownUrnError = class extends BabelQueueError {
47
51
  this.name = "UnknownUrnError";
48
52
  }
49
53
  };
54
+ var InvalidPayloadError = class extends BabelQueueError {
55
+ constructor(urn, violation2) {
56
+ super(`Message data for "${urn}" does not match its URN schema: ${violation2}.`);
57
+ this.urn = urn;
58
+ this.violation = violation2;
59
+ this.name = "InvalidPayloadError";
60
+ }
61
+ urn;
62
+ violation;
63
+ };
50
64
 
51
65
  // src/codec.ts
52
66
  var SCHEMA_VERSION = 1;
@@ -186,15 +200,204 @@ var UnknownUrnStrategy = {
186
200
  /** Route to the dead-letter queue. */
187
201
  DEAD_LETTER: "dead_letter"
188
202
  };
203
+
204
+ // src/idempotency.ts
205
+ var InMemoryStore = class {
206
+ entries = /* @__PURE__ */ new Set();
207
+ seen(messageId) {
208
+ return this.entries.has(messageId);
209
+ }
210
+ remember(messageId) {
211
+ this.entries.add(messageId);
212
+ }
213
+ forget(messageId) {
214
+ this.entries.delete(messageId);
215
+ }
216
+ };
217
+ function Wrap(store, handler) {
218
+ return async (env) => {
219
+ const id = env.meta.id;
220
+ if (!id) {
221
+ await handler(env);
222
+ return;
223
+ }
224
+ if (await store.seen(id)) {
225
+ return;
226
+ }
227
+ await handler(env);
228
+ await store.remember(id);
229
+ };
230
+ }
231
+
232
+ // src/schema.ts
233
+ var schema_exports = {};
234
+ __export(schema_exports, {
235
+ MapProvider: () => MapProvider,
236
+ check: () => check,
237
+ validate: () => validate,
238
+ validateSchema: () => validateSchema,
239
+ wrap: () => wrap
240
+ });
241
+ var MapProvider = class _MapProvider {
242
+ schemas;
243
+ constructor(schemas) {
244
+ this.schemas = new Map(Object.entries(schemas));
245
+ }
246
+ /** Build a provider from URN -> raw JSON Schema strings, parsing each. */
247
+ static fromJson(raw) {
248
+ const schemas = {};
249
+ for (const [urn, body] of Object.entries(raw)) {
250
+ const decoded = JSON.parse(body);
251
+ if (typeof decoded !== "object" || decoded === null || Array.isArray(decoded)) {
252
+ throw new Error(`schema: invalid JSON schema for "${urn}"`);
253
+ }
254
+ schemas[urn] = decoded;
255
+ }
256
+ return new _MapProvider(schemas);
257
+ }
258
+ schemaFor(urn) {
259
+ return this.schemas.get(urn);
260
+ }
261
+ };
262
+ async function check(provider, urn, data) {
263
+ const schemaNode = await provider.schemaFor(urn);
264
+ if (!schemaNode) {
265
+ return null;
266
+ }
267
+ return validateSchema(schemaNode, data);
268
+ }
269
+ async function validate(provider, urn, data) {
270
+ const violation2 = await check(provider, urn, data);
271
+ if (violation2 !== null) {
272
+ throw new InvalidPayloadError(urn, violation2);
273
+ }
274
+ }
275
+ function wrap(provider, handler) {
276
+ return async (env) => {
277
+ await validate(provider, env.job, env.data);
278
+ await handler(env);
279
+ };
280
+ }
281
+ function validateSchema(schema, value, path = "") {
282
+ if ("const" in schema && !equal(value, schema.const)) {
283
+ return violation(path, "wrong_const");
284
+ }
285
+ const enumValues = schema.enum;
286
+ if (Array.isArray(enumValues) && !enumValues.some((item) => equal(value, item))) {
287
+ return violation(path, "not_in_enum");
288
+ }
289
+ const type = typeof schema.type === "string" ? schema.type : "";
290
+ switch (type) {
291
+ case "object":
292
+ return checkObject(schema, value, path);
293
+ case "array":
294
+ return checkArray(schema, value, path);
295
+ case "string": {
296
+ if (typeof value !== "string") {
297
+ return violation(path, "not_a_string");
298
+ }
299
+ const minLength = schema.minLength;
300
+ if (typeof minLength === "number" && value.length < minLength) {
301
+ return violation(path, "below_min_length");
302
+ }
303
+ return null;
304
+ }
305
+ case "integer":
306
+ if (!isInteger(value)) {
307
+ return violation(path, "not_an_integer");
308
+ }
309
+ return checkMinimum(schema, value, path);
310
+ case "number":
311
+ if (typeof value !== "number") {
312
+ return violation(path, "not_a_number");
313
+ }
314
+ return checkMinimum(schema, value, path);
315
+ case "boolean":
316
+ return typeof value === "boolean" ? null : violation(path, "not_a_boolean");
317
+ case "null":
318
+ return value === null ? null : violation(path, "not_null");
319
+ default:
320
+ return null;
321
+ }
322
+ }
323
+ function checkObject(schema, value, path) {
324
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
325
+ return violation(path, "not_an_object");
326
+ }
327
+ const obj = value;
328
+ const required = schema.required;
329
+ if (Array.isArray(required)) {
330
+ for (const key of required) {
331
+ if (typeof key === "string" && !(key in obj)) {
332
+ return violation(join(path, key), "missing_required");
333
+ }
334
+ }
335
+ }
336
+ const properties = typeof schema.properties === "object" && schema.properties !== null ? schema.properties : {};
337
+ const additionalAllowed = schema.additionalProperties !== false;
338
+ for (const [name, item] of Object.entries(obj)) {
339
+ const propSchema = properties[name];
340
+ if (typeof propSchema === "object" && propSchema !== null) {
341
+ const found = validateSchema(propSchema, item, join(path, name));
342
+ if (found !== null) {
343
+ return found;
344
+ }
345
+ continue;
346
+ }
347
+ if (!additionalAllowed) {
348
+ return violation(join(path, name), "additional_not_allowed");
349
+ }
350
+ }
351
+ return null;
352
+ }
353
+ function checkArray(schema, value, path) {
354
+ if (!Array.isArray(value)) {
355
+ return violation(path, "not_an_array");
356
+ }
357
+ const items = schema.items;
358
+ if (typeof items !== "object" || items === null) {
359
+ return null;
360
+ }
361
+ for (let i = 0; i < value.length; i++) {
362
+ const found = validateSchema(items, value[i], `${path}[${i}]`);
363
+ if (found !== null) {
364
+ return found;
365
+ }
366
+ }
367
+ return null;
368
+ }
369
+ function checkMinimum(schema, value, path) {
370
+ const minimum = schema.minimum;
371
+ if (typeof minimum === "number" && value < minimum) {
372
+ return violation(path, "below_minimum");
373
+ }
374
+ return null;
375
+ }
376
+ function isInteger(value) {
377
+ return typeof value === "number" && Number.isInteger(value);
378
+ }
379
+ function equal(a, b) {
380
+ return JSON.stringify(a) === JSON.stringify(b);
381
+ }
382
+ function violation(path, reason) {
383
+ return `${path === "" ? "<root>" : path}: ${reason}`;
384
+ }
385
+ function join(path, key) {
386
+ return path === "" ? key : `${path}.${key}`;
387
+ }
189
388
  // Annotate the CommonJS export names for ESM import in node:
190
389
  0 && (module.exports = {
191
390
  BabelQueueError,
192
391
  EnvelopeCodec,
392
+ InMemoryStore,
393
+ InvalidPayloadError,
193
394
  SCHEMA_VERSION,
194
395
  SOURCE_LANG,
195
396
  UnknownUrnError,
196
397
  UnknownUrnStrategy,
398
+ Wrap,
197
399
  annotate,
198
- deadLetter
400
+ deadLetter,
401
+ schema
199
402
  });
200
403
  //# sourceMappingURL=index.cjs.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts","../src/codec.ts","../src/errors.ts","../src/deadLetter.ts","../src/routing.ts"],"sourcesContent":["/**\n * BabelQueue — Polyglot Queues, Simplified.\n *\n * The framework-agnostic Node/TypeScript core: the canonical wire-envelope codec,\n * contracts and dead-letter helpers. Zero runtime dependencies.\n *\n * ```ts\n * import { EnvelopeCodec } from \"@babelqueue/core\";\n *\n * const env = EnvelopeCodec.make(\"urn:babel:orders:created\", { order_id: 1042 });\n * const body = EnvelopeCodec.encode(env); // publish body to Redis / RabbitMQ / ...\n * ```\n *\n * Full spec: https://babelqueue.com\n */\n\nexport { EnvelopeCodec, SCHEMA_VERSION, SOURCE_LANG } from \"./codec.js\";\nexport type {\n DeadLetter,\n Envelope,\n IncomingEnvelope,\n MakeOptions,\n Meta,\n} from \"./codec.js\";\n\nexport type { HasTraceId, PolyglotMessage } from \"./contracts.js\";\n\nexport { annotate } from \"./deadLetter.js\";\nexport type { AnnotateOptions } from \"./deadLetter.js\";\nexport * as deadLetter from \"./deadLetter.js\";\n\nexport { UnknownUrnStrategy } from \"./routing.js\";\n\nexport { BabelQueueError, UnknownUrnError } from \"./errors.js\";\n","import { randomUUID } from \"node:crypto\";\n\nimport type { HasTraceId, PolyglotMessage } from \"./contracts.js\";\nimport { BabelQueueError } from \"./errors.js\";\n\n/** The wire envelope schema version this core implements (versioned independently of the package version). */\nexport const SCHEMA_VERSION = 1;\n\n/** Stamped into `meta.lang` for envelopes produced by this core. */\nexport const SOURCE_LANG = \"node\";\n\n/** Immutable per-message metadata. */\nexport interface Meta {\n id: string;\n queue: string;\n lang: string;\n schema_version: number;\n /** Unix milliseconds, UTC. */\n created_at: number;\n}\n\n/** The additive block appended to an envelope when a message is dead-lettered. */\nexport interface DeadLetter {\n reason: string;\n error: string | null;\n exception: string | null;\n /** Unix milliseconds, UTC. */\n failed_at: number;\n original_queue: string;\n attempts: number;\n lang: string;\n}\n\n/**\n * The canonical BabelQueue wire message: a strict, language-neutral JSON shape\n * that every SDK produces and consumes identically. The property order here is\n * significant — it matches the other cores so {@link EnvelopeCodec.encode} is\n * byte-for-byte identical across the insertion-order languages (PHP/Python).\n */\nexport interface Envelope {\n /** The message URN (never a class name). */\n job: string;\n /** Correlation id, preserved across every hop. */\n trace_id: string;\n /** The pure-JSON payload. */\n data: Record<string, unknown>;\n meta: Meta;\n /** Top-level transport retry counter. */\n attempts: number;\n /** Present only once the message has been dead-lettered. */\n dead_letter?: DeadLetter;\n}\n\n/**\n * A decoded, not-yet-validated envelope. Fields are loosely typed because they\n * come off the wire; `urn` is accepted as an inbound alias for `job`. Narrow it\n * with {@link EnvelopeCodec.accepts} before trusting the contents.\n */\nexport interface IncomingEnvelope {\n job?: string;\n /** Inbound alias for `job`. */\n urn?: string;\n trace_id?: string;\n data?: unknown;\n meta?: unknown;\n attempts?: unknown;\n dead_letter?: unknown;\n}\n\n/** Options for {@link EnvelopeCodec.make}. */\nexport interface MakeOptions {\n /** Logical queue name recorded in `meta.queue` (default `\"default\"`). */\n queue?: string;\n /** Reuse an existing trace id (trace continuation) instead of minting one. */\n traceId?: string;\n}\n\n/**\n * Builds, encodes and decodes the canonical envelope — the single Node/TypeScript\n * implementation of the wire format.\n */\nexport const EnvelopeCodec = {\n SCHEMA_VERSION,\n SOURCE_LANG,\n\n /**\n * Build the canonical envelope for a `(urn, data)` pair. Mints a fresh trace id\n * unless `options.traceId` is given, starts `attempts` at 0, and stamps `meta`.\n * Throws {@link BabelQueueError} when the URN is blank.\n */\n make(\n urn: string,\n data: Record<string, unknown>,\n options: MakeOptions = {},\n ): Envelope {\n const resolvedUrn = (urn ?? \"\").trim();\n if (resolvedUrn === \"\") {\n throw new BabelQueueError(\n \"A polyglot message must expose a stable, non-empty URN so consumers can identify it without any class name.\",\n );\n }\n\n const traceId = (options.traceId ?? \"\").trim() || randomUUID();\n\n return {\n job: resolvedUrn,\n trace_id: traceId,\n data: { ...data },\n meta: {\n id: randomUUID(),\n queue: options.queue ?? \"default\",\n lang: SOURCE_LANG,\n schema_version: SCHEMA_VERSION,\n created_at: Date.now(),\n },\n attempts: 0,\n };\n },\n\n /**\n * Build the envelope from a {@link PolyglotMessage}. If the message also\n * implements {@link HasTraceId} and returns a non-empty value, that trace id is\n * reused.\n */\n fromMessage(\n message: PolyglotMessage & Partial<HasTraceId>,\n queue = \"default\",\n ): Envelope {\n const traceId =\n typeof message.getBabelTraceId === \"function\"\n ? (message.getBabelTraceId() ?? undefined)\n : undefined;\n\n return EnvelopeCodec.make(message.getBabelUrn(), message.toPayload(), {\n queue,\n traceId,\n });\n },\n\n /**\n * Encode the envelope as compact UTF-8 JSON. `JSON.stringify` already emits the\n * canonical form — no spaces, and slashes/unicode/HTML left unescaped — matching\n * the other SDK cores.\n */\n encode(envelope: Envelope): string {\n return JSON.stringify(envelope);\n },\n\n /**\n * Parse a raw JSON body. Returns `{}` for malformed or non-object input (call\n * {@link EnvelopeCodec.accepts} before trusting it). Resolves the `urn` inbound\n * alias into `job`.\n */\n decode(raw: string): IncomingEnvelope {\n let parsed: unknown;\n try {\n parsed = JSON.parse(raw);\n } catch {\n return {};\n }\n if (parsed === null || typeof parsed !== \"object\" || Array.isArray(parsed)) {\n return {};\n }\n\n const envelope = parsed as IncomingEnvelope;\n if (!envelope.job && typeof envelope.urn === \"string\") {\n envelope.job = envelope.urn;\n }\n return envelope;\n },\n\n /** The message URN — canonical `job`, with `urn` accepted as an alias. */\n urn(envelope: IncomingEnvelope): string {\n const value = envelope?.job ?? envelope?.urn ?? \"\";\n return typeof value === \"string\" ? value.trim() : \"\";\n },\n\n /**\n * Whether a consumer should accept this envelope. Rejects a missing URN, an\n * unsupported `meta.schema_version`, a non-object `data`, a non-integer\n * `attempts`, or a blank `trace_id` — the consumer-side counterpart to the\n * producer JSON Schema. Acts as a type guard that narrows to {@link Envelope}.\n */\n accepts(envelope: IncomingEnvelope): envelope is Envelope {\n if (EnvelopeCodec.urn(envelope) === \"\") {\n return false;\n }\n\n const meta = envelope.meta;\n if (\n meta === null ||\n typeof meta !== \"object\" ||\n (meta as Meta).schema_version !== SCHEMA_VERSION\n ) {\n return false;\n }\n\n const data = envelope.data;\n if (data === null || typeof data !== \"object\" || Array.isArray(data)) {\n return false;\n }\n\n const attempts = envelope.attempts;\n if (typeof attempts !== \"number\" || !Number.isInteger(attempts)) {\n return false;\n }\n\n const traceId = envelope.trace_id;\n if (typeof traceId !== \"string\" || traceId.trim() === \"\") {\n return false;\n }\n\n return true;\n },\n} as const;\n","/** Base error for all BabelQueue failures. */\nexport class BabelQueueError extends Error {\n constructor(message: string) {\n super(message);\n this.name = \"BabelQueueError\";\n }\n}\n\n/** Raised when no handler is mapped for a message URN. */\nexport class UnknownUrnError extends BabelQueueError {\n constructor(urn: string) {\n super(`No handler is mapped for the message URN \"${urn}\".`);\n this.name = \"UnknownUrnError\";\n }\n}\n","import type { DeadLetter, Envelope } from \"./codec.js\";\nimport { SOURCE_LANG } from \"./codec.js\";\n\n/** Options for {@link annotate}. */\nexport interface AnnotateOptions {\n /** Defaults to the envelope's current `attempts`. */\n attempts?: number;\n /** A human-readable error message (JSON `null` when omitted). */\n error?: string | null;\n /** The originating error type/class name (JSON `null` when omitted). */\n exception?: string | null;\n}\n\n/**\n * Return a copy of the envelope with a `dead_letter` block attached, recording\n * why and where it failed. The original envelope is preserved unchanged inside\n * the result, so any-language consumers can still read it.\n */\nexport function annotate(\n envelope: Envelope,\n reason: string,\n originalQueue: string,\n options: AnnotateOptions = {},\n): Envelope {\n const deadLetter: DeadLetter = {\n reason,\n error: options.error ?? null,\n exception: options.exception ?? null,\n failed_at: Date.now(),\n original_queue: originalQueue,\n attempts: options.attempts ?? envelope.attempts ?? 0,\n lang: SOURCE_LANG,\n };\n\n return { ...envelope, dead_letter: deadLetter };\n}\n","/**\n * What a consumer does with a message whose URN has no registered handler.\n * Mirrors the constants in every other SDK core.\n */\nexport const UnknownUrnStrategy = {\n /** Surface an error; let the worker decide. */\n FAIL: \"fail\",\n /** Drop the message. */\n DELETE: \"delete\",\n /** Requeue for another consumer. */\n RELEASE: \"release\",\n /** Route to the dead-letter queue. */\n DEAD_LETTER: \"dead_letter\",\n} as const;\n\nexport type UnknownUrnStrategy =\n (typeof UnknownUrnStrategy)[keyof typeof UnknownUrnStrategy];\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/index.ts","../src/codec.ts","../src/errors.ts","../src/deadLetter.ts","../src/routing.ts","../src/idempotency.ts","../src/schema.ts"],"sourcesContent":["/**\n * BabelQueue — Polyglot Queues, Simplified.\n *\n * The framework-agnostic Node/TypeScript core: the canonical wire-envelope codec,\n * contracts and dead-letter helpers. Zero runtime dependencies.\n *\n * ```ts\n * import { EnvelopeCodec } from \"@babelqueue/core\";\n *\n * const env = EnvelopeCodec.make(\"urn:babel:orders:created\", { order_id: 1042 });\n * const body = EnvelopeCodec.encode(env); // publish body to Redis / RabbitMQ / ...\n * ```\n *\n * Full spec: https://babelqueue.com\n */\n\nexport { EnvelopeCodec, SCHEMA_VERSION, SOURCE_LANG } from \"./codec.js\";\nexport type {\n DeadLetter,\n Envelope,\n IncomingEnvelope,\n MakeOptions,\n Meta,\n} from \"./codec.js\";\n\nexport type { HasTraceId, PolyglotMessage } from \"./contracts.js\";\n\nexport { annotate } from \"./deadLetter.js\";\nexport type { AnnotateOptions } from \"./deadLetter.js\";\nexport * as deadLetter from \"./deadLetter.js\";\n\nexport { UnknownUrnStrategy } from \"./routing.js\";\n\nexport { BabelQueueError, UnknownUrnError, InvalidPayloadError } from \"./errors.js\";\n\nexport { Wrap, InMemoryStore } from \"./idempotency.js\";\nexport type { Handler, Store } from \"./idempotency.js\";\n\nexport * as schema from \"./schema.js\";\nexport type { SchemaProvider, SchemaNode } from \"./schema.js\";\n","import { randomUUID } from \"node:crypto\";\n\nimport type { HasTraceId, PolyglotMessage } from \"./contracts.js\";\nimport { BabelQueueError } from \"./errors.js\";\n\n/** The wire envelope schema version this core implements (versioned independently of the package version). */\nexport const SCHEMA_VERSION = 1;\n\n/** Stamped into `meta.lang` for envelopes produced by this core. */\nexport const SOURCE_LANG = \"node\";\n\n/** Immutable per-message metadata. */\nexport interface Meta {\n id: string;\n queue: string;\n lang: string;\n schema_version: number;\n /** Unix milliseconds, UTC. */\n created_at: number;\n}\n\n/** The additive block appended to an envelope when a message is dead-lettered. */\nexport interface DeadLetter {\n reason: string;\n error: string | null;\n exception: string | null;\n /** Unix milliseconds, UTC. */\n failed_at: number;\n original_queue: string;\n attempts: number;\n lang: string;\n}\n\n/**\n * The canonical BabelQueue wire message: a strict, language-neutral JSON shape\n * that every SDK produces and consumes identically. The property order here is\n * significant — it matches the other cores so {@link EnvelopeCodec.encode} is\n * byte-for-byte identical across the insertion-order languages (PHP/Python).\n */\nexport interface Envelope {\n /** The message URN (never a class name). */\n job: string;\n /** Correlation id, preserved across every hop. */\n trace_id: string;\n /** The pure-JSON payload. */\n data: Record<string, unknown>;\n meta: Meta;\n /** Top-level transport retry counter. */\n attempts: number;\n /** Present only once the message has been dead-lettered. */\n dead_letter?: DeadLetter;\n}\n\n/**\n * A decoded, not-yet-validated envelope. Fields are loosely typed because they\n * come off the wire; `urn` is accepted as an inbound alias for `job`. Narrow it\n * with {@link EnvelopeCodec.accepts} before trusting the contents.\n */\nexport interface IncomingEnvelope {\n job?: string;\n /** Inbound alias for `job`. */\n urn?: string;\n trace_id?: string;\n data?: unknown;\n meta?: unknown;\n attempts?: unknown;\n dead_letter?: unknown;\n}\n\n/** Options for {@link EnvelopeCodec.make}. */\nexport interface MakeOptions {\n /** Logical queue name recorded in `meta.queue` (default `\"default\"`). */\n queue?: string;\n /** Reuse an existing trace id (trace continuation) instead of minting one. */\n traceId?: string;\n}\n\n/**\n * Builds, encodes and decodes the canonical envelope — the single Node/TypeScript\n * implementation of the wire format.\n */\nexport const EnvelopeCodec = {\n SCHEMA_VERSION,\n SOURCE_LANG,\n\n /**\n * Build the canonical envelope for a `(urn, data)` pair. Mints a fresh trace id\n * unless `options.traceId` is given, starts `attempts` at 0, and stamps `meta`.\n * Throws {@link BabelQueueError} when the URN is blank.\n */\n make(\n urn: string,\n data: Record<string, unknown>,\n options: MakeOptions = {},\n ): Envelope {\n const resolvedUrn = (urn ?? \"\").trim();\n if (resolvedUrn === \"\") {\n throw new BabelQueueError(\n \"A polyglot message must expose a stable, non-empty URN so consumers can identify it without any class name.\",\n );\n }\n\n const traceId = (options.traceId ?? \"\").trim() || randomUUID();\n\n return {\n job: resolvedUrn,\n trace_id: traceId,\n data: { ...data },\n meta: {\n id: randomUUID(),\n queue: options.queue ?? \"default\",\n lang: SOURCE_LANG,\n schema_version: SCHEMA_VERSION,\n created_at: Date.now(),\n },\n attempts: 0,\n };\n },\n\n /**\n * Build the envelope from a {@link PolyglotMessage}. If the message also\n * implements {@link HasTraceId} and returns a non-empty value, that trace id is\n * reused.\n */\n fromMessage(\n message: PolyglotMessage & Partial<HasTraceId>,\n queue = \"default\",\n ): Envelope {\n const traceId =\n typeof message.getBabelTraceId === \"function\"\n ? (message.getBabelTraceId() ?? undefined)\n : undefined;\n\n return EnvelopeCodec.make(message.getBabelUrn(), message.toPayload(), {\n queue,\n traceId,\n });\n },\n\n /**\n * Encode the envelope as compact UTF-8 JSON. `JSON.stringify` already emits the\n * canonical form — no spaces, and slashes/unicode/HTML left unescaped — matching\n * the other SDK cores.\n */\n encode(envelope: Envelope): string {\n return JSON.stringify(envelope);\n },\n\n /**\n * Parse a raw JSON body. Returns `{}` for malformed or non-object input (call\n * {@link EnvelopeCodec.accepts} before trusting it). Resolves the `urn` inbound\n * alias into `job`.\n */\n decode(raw: string): IncomingEnvelope {\n let parsed: unknown;\n try {\n parsed = JSON.parse(raw);\n } catch {\n return {};\n }\n if (parsed === null || typeof parsed !== \"object\" || Array.isArray(parsed)) {\n return {};\n }\n\n const envelope = parsed as IncomingEnvelope;\n if (!envelope.job && typeof envelope.urn === \"string\") {\n envelope.job = envelope.urn;\n }\n return envelope;\n },\n\n /** The message URN — canonical `job`, with `urn` accepted as an alias. */\n urn(envelope: IncomingEnvelope): string {\n const value = envelope?.job ?? envelope?.urn ?? \"\";\n return typeof value === \"string\" ? value.trim() : \"\";\n },\n\n /**\n * Whether a consumer should accept this envelope. Rejects a missing URN, an\n * unsupported `meta.schema_version`, a non-object `data`, a non-integer\n * `attempts`, or a blank `trace_id` — the consumer-side counterpart to the\n * producer JSON Schema. Acts as a type guard that narrows to {@link Envelope}.\n */\n accepts(envelope: IncomingEnvelope): envelope is Envelope {\n if (EnvelopeCodec.urn(envelope) === \"\") {\n return false;\n }\n\n const meta = envelope.meta;\n if (\n meta === null ||\n typeof meta !== \"object\" ||\n (meta as Meta).schema_version !== SCHEMA_VERSION\n ) {\n return false;\n }\n\n const data = envelope.data;\n if (data === null || typeof data !== \"object\" || Array.isArray(data)) {\n return false;\n }\n\n const attempts = envelope.attempts;\n if (typeof attempts !== \"number\" || !Number.isInteger(attempts)) {\n return false;\n }\n\n const traceId = envelope.trace_id;\n if (typeof traceId !== \"string\" || traceId.trim() === \"\") {\n return false;\n }\n\n return true;\n },\n} as const;\n","/** Base error for all BabelQueue failures. */\nexport class BabelQueueError extends Error {\n constructor(message: string) {\n super(message);\n this.name = \"BabelQueueError\";\n }\n}\n\n/** Raised when no handler is mapped for a message URN. */\nexport class UnknownUrnError extends BabelQueueError {\n constructor(urn: string) {\n super(`No handler is mapped for the message URN \"${urn}\".`);\n this.name = \"UnknownUrnError\";\n }\n}\n\n/**\n * Raised when a message's `data` does not match the JSON Schema registered for its URN\n * (ADR-0024). The consumer-side {@link schema.wrap} throws it so the adapter redelivers\n * (and eventually dead-letters) a poison message.\n */\nexport class InvalidPayloadError extends BabelQueueError {\n constructor(\n readonly urn: string,\n readonly violation: string,\n ) {\n super(`Message data for \"${urn}\" does not match its URN schema: ${violation}.`);\n this.name = \"InvalidPayloadError\";\n }\n}\n","import type { DeadLetter, Envelope } from \"./codec.js\";\nimport { SOURCE_LANG } from \"./codec.js\";\n\n/** Options for {@link annotate}. */\nexport interface AnnotateOptions {\n /** Defaults to the envelope's current `attempts`. */\n attempts?: number;\n /** A human-readable error message (JSON `null` when omitted). */\n error?: string | null;\n /** The originating error type/class name (JSON `null` when omitted). */\n exception?: string | null;\n}\n\n/**\n * Return a copy of the envelope with a `dead_letter` block attached, recording\n * why and where it failed. The original envelope is preserved unchanged inside\n * the result, so any-language consumers can still read it.\n */\nexport function annotate(\n envelope: Envelope,\n reason: string,\n originalQueue: string,\n options: AnnotateOptions = {},\n): Envelope {\n const deadLetter: DeadLetter = {\n reason,\n error: options.error ?? null,\n exception: options.exception ?? null,\n failed_at: Date.now(),\n original_queue: originalQueue,\n attempts: options.attempts ?? envelope.attempts ?? 0,\n lang: SOURCE_LANG,\n };\n\n return { ...envelope, dead_letter: deadLetter };\n}\n","/**\n * What a consumer does with a message whose URN has no registered handler.\n * Mirrors the constants in every other SDK core.\n */\nexport const UnknownUrnStrategy = {\n /** Surface an error; let the worker decide. */\n FAIL: \"fail\",\n /** Drop the message. */\n DELETE: \"delete\",\n /** Requeue for another consumer. */\n RELEASE: \"release\",\n /** Route to the dead-letter queue. */\n DEAD_LETTER: \"dead_letter\",\n} as const;\n\nexport type UnknownUrnStrategy =\n (typeof UnknownUrnStrategy)[keyof typeof UnknownUrnStrategy];\n","/**\n * Optional idempotency helper (ADR-0022): dedupe a consume handler on `meta.id`.\n *\n * The Node mirror of the PHP `BabelQueue\\Idempotency` and Go `idempotency` helpers.\n * The core is codec-only (no dispatcher), so this wraps a user-provided handler that\n * an adapter (NestJS, BullMQ, ...) drives:\n *\n * ```ts\n * import { Wrap, InMemoryStore, type Handler } from \"@babelqueue/core\";\n *\n * const store = new InMemoryStore();\n * const handler = Wrap(store, async (env) => { ... });\n * ```\n *\n * A previously-seen id returns early (the adapter acks it); a throwing/rejecting\n * handler leaves the id unmarked so a redelivery runs it again; a message with no\n * usable `meta.id` runs unchanged. \"Seen-set\" post-success dedupe — not exactly-once,\n * not in-flight concurrency locking (a transactional mode is a future direction).\n */\nimport type { Envelope } from \"./codec.js\";\n\n/** A consume handler: receives a decoded envelope, may be sync or async. */\nexport type Handler = (env: Envelope) => void | Promise<void>;\n\n/**\n * A pluggable record of message ids already processed, keyed on `meta.id`. Methods may\n * be sync or async so a production store can be Redis- or DB-backed; the reference\n * {@link InMemoryStore} is synchronous.\n */\nexport interface Store {\n seen(messageId: string): boolean | Promise<boolean>;\n remember(messageId: string): void | Promise<void>;\n forget(messageId: string): void | Promise<void>;\n}\n\n/**\n * Process-local {@link Store} backed by a Set. For tests / single-process consumers;\n * not shared across workers and not persistent — use a Redis- or DB-backed store for\n * production fleets.\n */\nexport class InMemoryStore implements Store {\n private readonly entries = new Set<string>();\n\n seen(messageId: string): boolean {\n return this.entries.has(messageId);\n }\n\n remember(messageId: string): void {\n this.entries.add(messageId);\n }\n\n forget(messageId: string): void {\n this.entries.delete(messageId);\n }\n}\n\n/**\n * Wraps `handler` so a message whose `meta.id` was already processed successfully is\n * skipped. A thrown/rejected handler leaves the id unmarked, so a redelivery runs it\n * again (retry / dead-letter still apply); a message with no usable id runs unchanged.\n */\nexport function Wrap(store: Store, handler: Handler): Handler {\n return async (env: Envelope): Promise<void> => {\n const id = env.meta.id;\n\n // No usable id → cannot dedupe; run the handler unchanged.\n if (!id) {\n await handler(env);\n return;\n }\n\n // Already processed on an earlier delivery: return so the adapter acks it.\n if (await store.seen(id)) {\n return;\n }\n\n // First success wins; a throw here leaves the id unmarked → retry/DLQ apply.\n await handler(env);\n await store.remember(id);\n };\n}\n","/**\n * Optional per-URN payload schema validation (ADR-0024).\n *\n * The Node mirror of the Go `schema` package and PHP `BabelQueue\\Schema`. A\n * {@link SchemaProvider} supplies a JSON Schema for a message URN — typically built from a\n * babelqueue-registry `registry.json` — and the message's `data` is validated against it.\n * It is opt-in: a URN with no registered schema is never validated.\n *\n * ```ts\n * import { schema } from \"@babelqueue/core\";\n *\n * const provider = schema.MapProvider.fromJson({ \"urn:babel:orders:created\": ORDERS_JSON });\n * schema.validate(provider, \"urn:babel:orders:created\", { order_id: 7 }); // throws on mismatch\n * const handler = schema.wrap(provider, async (env) => { ... }); // consumer safety net\n * ```\n *\n * The core stays dependency-free and I/O-free, so it carries no file-based provider: a Node\n * app or adapter reads its `registry.json` (with `node:fs`, etc.) and passes the schemas to\n * {@link MapProvider.fromJson}. The validator is a small subset of JSON Schema (draft-07)\n * whose verdicts match the Go, PHP and Python validators and babelqueue-registry's `compat`\n * linter: `type`, `required`, `properties`, `additionalProperties`, `items`, `enum`,\n * `const`, `minLength`, `minimum`. Unknown keywords are ignored.\n */\nimport type { Envelope } from \"./codec.js\";\nimport { InvalidPayloadError } from \"./errors.js\";\n\n/** A parsed JSON Schema node. */\nexport type SchemaNode = Record<string, unknown>;\n\n/** A consume handler: receives a decoded envelope, may be sync or async. */\nexport type SchemaHandler = (env: Envelope) => void | Promise<void>;\n\n/**\n * A source of per-URN `data` schemas, keyed on the message URN. `schemaFor` may be sync or\n * async so a production provider can be service- or cache-backed; the reference\n * {@link MapProvider} is synchronous.\n */\nexport interface SchemaProvider {\n schemaFor(urn: string): SchemaNode | undefined | Promise<SchemaNode | undefined>;\n}\n\n/** In-memory {@link SchemaProvider}, for tests and for embedding schemas in code. */\nexport class MapProvider implements SchemaProvider {\n private readonly schemas: Map<string, SchemaNode>;\n\n constructor(schemas: Record<string, SchemaNode>) {\n this.schemas = new Map(Object.entries(schemas));\n }\n\n /** Build a provider from URN -> raw JSON Schema strings, parsing each. */\n static fromJson(raw: Record<string, string>): MapProvider {\n const schemas: Record<string, SchemaNode> = {};\n for (const [urn, body] of Object.entries(raw)) {\n const decoded: unknown = JSON.parse(body);\n if (typeof decoded !== \"object\" || decoded === null || Array.isArray(decoded)) {\n throw new Error(`schema: invalid JSON schema for \"${urn}\"`);\n }\n schemas[urn] = decoded as SchemaNode;\n }\n return new MapProvider(schemas);\n }\n\n schemaFor(urn: string): SchemaNode | undefined {\n return this.schemas.get(urn);\n }\n}\n\n/**\n * The first `data` violation for `(urn, data)`, or null when it is valid or when no schema is\n * registered for the URN (opt-in). For producer-side branching.\n */\nexport async function check(\n provider: SchemaProvider,\n urn: string,\n data: Record<string, unknown>,\n): Promise<string | null> {\n const schemaNode = await provider.schemaFor(urn);\n if (!schemaNode) {\n return null;\n }\n return validateSchema(schemaNode, data);\n}\n\n/**\n * Validate `(urn, data)` against its registered schema, throwing {@link InvalidPayloadError}\n * otherwise. The producer-side guard; call it before publishing.\n */\nexport async function validate(\n provider: SchemaProvider,\n urn: string,\n data: Record<string, unknown>,\n): Promise<void> {\n const violation = await check(provider, urn, data);\n if (violation !== null) {\n throw new InvalidPayloadError(urn, violation);\n }\n}\n\n/**\n * Wrap a consume handler so each message's `data` is validated against its URN's schema\n * before the handler runs (consumer-side safety net). Invalid data throws\n * {@link InvalidPayloadError}, so the adapter redelivers (and eventually dead-letters) the\n * poison message; a URN with no schema runs the handler unchanged. Prefer {@link check}\n * producer-side to keep invalid data out of the queue entirely.\n */\nexport function wrap(provider: SchemaProvider, handler: SchemaHandler): SchemaHandler {\n return async (env: Envelope): Promise<void> => {\n await validate(provider, env.job, env.data);\n await handler(env);\n };\n}\n\n/** The first violation of `value` against a (subset) JSON Schema node, or null. */\nexport function validateSchema(schema: SchemaNode, value: unknown, path = \"\"): string | null {\n if (\"const\" in schema && !equal(value, schema.const)) {\n return violation(path, \"wrong_const\");\n }\n const enumValues = schema.enum;\n if (Array.isArray(enumValues) && !enumValues.some((item) => equal(value, item))) {\n return violation(path, \"not_in_enum\");\n }\n\n const type = typeof schema.type === \"string\" ? schema.type : \"\";\n switch (type) {\n case \"object\":\n return checkObject(schema, value, path);\n case \"array\":\n return checkArray(schema, value, path);\n case \"string\": {\n if (typeof value !== \"string\") {\n return violation(path, \"not_a_string\");\n }\n const minLength = schema.minLength;\n if (typeof minLength === \"number\" && value.length < minLength) {\n return violation(path, \"below_min_length\");\n }\n return null;\n }\n case \"integer\":\n if (!isInteger(value)) {\n return violation(path, \"not_an_integer\");\n }\n return checkMinimum(schema, value, path);\n case \"number\":\n if (typeof value !== \"number\") {\n return violation(path, \"not_a_number\");\n }\n return checkMinimum(schema, value, path);\n case \"boolean\":\n return typeof value === \"boolean\" ? null : violation(path, \"not_a_boolean\");\n case \"null\":\n return value === null ? null : violation(path, \"not_null\");\n default:\n return null;\n }\n}\n\nfunction checkObject(schema: SchemaNode, value: unknown, path: string): string | null {\n if (typeof value !== \"object\" || value === null || Array.isArray(value)) {\n return violation(path, \"not_an_object\");\n }\n const obj = value as Record<string, unknown>;\n\n const required = schema.required;\n if (Array.isArray(required)) {\n for (const key of required) {\n if (typeof key === \"string\" && !(key in obj)) {\n return violation(join(path, key), \"missing_required\");\n }\n }\n }\n\n const properties =\n typeof schema.properties === \"object\" && schema.properties !== null\n ? (schema.properties as Record<string, unknown>)\n : {};\n const additionalAllowed = schema.additionalProperties !== false;\n\n for (const [name, item] of Object.entries(obj)) {\n const propSchema = properties[name];\n if (typeof propSchema === \"object\" && propSchema !== null) {\n const found = validateSchema(propSchema as SchemaNode, item, join(path, name));\n if (found !== null) {\n return found;\n }\n continue;\n }\n if (!additionalAllowed) {\n return violation(join(path, name), \"additional_not_allowed\");\n }\n }\n\n return null;\n}\n\nfunction checkArray(schema: SchemaNode, value: unknown, path: string): string | null {\n if (!Array.isArray(value)) {\n return violation(path, \"not_an_array\");\n }\n const items = schema.items;\n if (typeof items !== \"object\" || items === null) {\n return null;\n }\n for (let i = 0; i < value.length; i++) {\n const found = validateSchema(items as SchemaNode, value[i], `${path}[${i}]`);\n if (found !== null) {\n return found;\n }\n }\n return null;\n}\n\nfunction checkMinimum(schema: SchemaNode, value: number, path: string): string | null {\n const minimum = schema.minimum;\n if (typeof minimum === \"number\" && value < minimum) {\n return violation(path, \"below_minimum\");\n }\n return null;\n}\n\n// JSON numbers are all `number` in JS; an integer is a whole number (and never a boolean).\nfunction isInteger(value: unknown): value is number {\n return typeof value === \"number\" && Number.isInteger(value);\n}\n\n// Structural equality for enum/const checks: JSON.stringify distinguishes a string \"1\" from\n// a number 1, matching the strict comparisons in the other SDK validators.\nfunction equal(a: unknown, b: unknown): boolean {\n return JSON.stringify(a) === JSON.stringify(b);\n}\n\nfunction violation(path: string, reason: string): string {\n return `${path === \"\" ? \"<root>\" : path}: ${reason}`;\n}\n\nfunction join(path: string, key: string): string {\n return path === \"\" ? key : `${path}.${key}`;\n}\n"],"mappings":"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","names":["violation","violation"]}
package/dist/index.d.cts CHANGED
@@ -171,5 +171,143 @@ declare class BabelQueueError extends Error {
171
171
  declare class UnknownUrnError extends BabelQueueError {
172
172
  constructor(urn: string);
173
173
  }
174
+ /**
175
+ * Raised when a message's `data` does not match the JSON Schema registered for its URN
176
+ * (ADR-0024). The consumer-side {@link schema.wrap} throws it so the adapter redelivers
177
+ * (and eventually dead-letters) a poison message.
178
+ */
179
+ declare class InvalidPayloadError extends BabelQueueError {
180
+ readonly urn: string;
181
+ readonly violation: string;
182
+ constructor(urn: string, violation: string);
183
+ }
184
+
185
+ /**
186
+ * Optional idempotency helper (ADR-0022): dedupe a consume handler on `meta.id`.
187
+ *
188
+ * The Node mirror of the PHP `BabelQueue\Idempotency` and Go `idempotency` helpers.
189
+ * The core is codec-only (no dispatcher), so this wraps a user-provided handler that
190
+ * an adapter (NestJS, BullMQ, ...) drives:
191
+ *
192
+ * ```ts
193
+ * import { Wrap, InMemoryStore, type Handler } from "@babelqueue/core";
194
+ *
195
+ * const store = new InMemoryStore();
196
+ * const handler = Wrap(store, async (env) => { ... });
197
+ * ```
198
+ *
199
+ * A previously-seen id returns early (the adapter acks it); a throwing/rejecting
200
+ * handler leaves the id unmarked so a redelivery runs it again; a message with no
201
+ * usable `meta.id` runs unchanged. "Seen-set" post-success dedupe — not exactly-once,
202
+ * not in-flight concurrency locking (a transactional mode is a future direction).
203
+ */
204
+
205
+ /** A consume handler: receives a decoded envelope, may be sync or async. */
206
+ type Handler = (env: Envelope) => void | Promise<void>;
207
+ /**
208
+ * A pluggable record of message ids already processed, keyed on `meta.id`. Methods may
209
+ * be sync or async so a production store can be Redis- or DB-backed; the reference
210
+ * {@link InMemoryStore} is synchronous.
211
+ */
212
+ interface Store {
213
+ seen(messageId: string): boolean | Promise<boolean>;
214
+ remember(messageId: string): void | Promise<void>;
215
+ forget(messageId: string): void | Promise<void>;
216
+ }
217
+ /**
218
+ * Process-local {@link Store} backed by a Set. For tests / single-process consumers;
219
+ * not shared across workers and not persistent — use a Redis- or DB-backed store for
220
+ * production fleets.
221
+ */
222
+ declare class InMemoryStore implements Store {
223
+ private readonly entries;
224
+ seen(messageId: string): boolean;
225
+ remember(messageId: string): void;
226
+ forget(messageId: string): void;
227
+ }
228
+ /**
229
+ * Wraps `handler` so a message whose `meta.id` was already processed successfully is
230
+ * skipped. A thrown/rejected handler leaves the id unmarked, so a redelivery runs it
231
+ * again (retry / dead-letter still apply); a message with no usable id runs unchanged.
232
+ */
233
+ declare function Wrap(store: Store, handler: Handler): Handler;
234
+
235
+ /**
236
+ * Optional per-URN payload schema validation (ADR-0024).
237
+ *
238
+ * The Node mirror of the Go `schema` package and PHP `BabelQueue\Schema`. A
239
+ * {@link SchemaProvider} supplies a JSON Schema for a message URN — typically built from a
240
+ * babelqueue-registry `registry.json` — and the message's `data` is validated against it.
241
+ * It is opt-in: a URN with no registered schema is never validated.
242
+ *
243
+ * ```ts
244
+ * import { schema } from "@babelqueue/core";
245
+ *
246
+ * const provider = schema.MapProvider.fromJson({ "urn:babel:orders:created": ORDERS_JSON });
247
+ * schema.validate(provider, "urn:babel:orders:created", { order_id: 7 }); // throws on mismatch
248
+ * const handler = schema.wrap(provider, async (env) => { ... }); // consumer safety net
249
+ * ```
250
+ *
251
+ * The core stays dependency-free and I/O-free, so it carries no file-based provider: a Node
252
+ * app or adapter reads its `registry.json` (with `node:fs`, etc.) and passes the schemas to
253
+ * {@link MapProvider.fromJson}. The validator is a small subset of JSON Schema (draft-07)
254
+ * whose verdicts match the Go, PHP and Python validators and babelqueue-registry's `compat`
255
+ * linter: `type`, `required`, `properties`, `additionalProperties`, `items`, `enum`,
256
+ * `const`, `minLength`, `minimum`. Unknown keywords are ignored.
257
+ */
258
+
259
+ /** A parsed JSON Schema node. */
260
+ type SchemaNode = Record<string, unknown>;
261
+ /** A consume handler: receives a decoded envelope, may be sync or async. */
262
+ type SchemaHandler = (env: Envelope) => void | Promise<void>;
263
+ /**
264
+ * A source of per-URN `data` schemas, keyed on the message URN. `schemaFor` may be sync or
265
+ * async so a production provider can be service- or cache-backed; the reference
266
+ * {@link MapProvider} is synchronous.
267
+ */
268
+ interface SchemaProvider {
269
+ schemaFor(urn: string): SchemaNode | undefined | Promise<SchemaNode | undefined>;
270
+ }
271
+ /** In-memory {@link SchemaProvider}, for tests and for embedding schemas in code. */
272
+ declare class MapProvider implements SchemaProvider {
273
+ private readonly schemas;
274
+ constructor(schemas: Record<string, SchemaNode>);
275
+ /** Build a provider from URN -> raw JSON Schema strings, parsing each. */
276
+ static fromJson(raw: Record<string, string>): MapProvider;
277
+ schemaFor(urn: string): SchemaNode | undefined;
278
+ }
279
+ /**
280
+ * The first `data` violation for `(urn, data)`, or null when it is valid or when no schema is
281
+ * registered for the URN (opt-in). For producer-side branching.
282
+ */
283
+ declare function check(provider: SchemaProvider, urn: string, data: Record<string, unknown>): Promise<string | null>;
284
+ /**
285
+ * Validate `(urn, data)` against its registered schema, throwing {@link InvalidPayloadError}
286
+ * otherwise. The producer-side guard; call it before publishing.
287
+ */
288
+ declare function validate(provider: SchemaProvider, urn: string, data: Record<string, unknown>): Promise<void>;
289
+ /**
290
+ * Wrap a consume handler so each message's `data` is validated against its URN's schema
291
+ * before the handler runs (consumer-side safety net). Invalid data throws
292
+ * {@link InvalidPayloadError}, so the adapter redelivers (and eventually dead-letters) the
293
+ * poison message; a URN with no schema runs the handler unchanged. Prefer {@link check}
294
+ * producer-side to keep invalid data out of the queue entirely.
295
+ */
296
+ declare function wrap(provider: SchemaProvider, handler: SchemaHandler): SchemaHandler;
297
+ /** The first violation of `value` against a (subset) JSON Schema node, or null. */
298
+ declare function validateSchema(schema: SchemaNode, value: unknown, path?: string): string | null;
299
+
300
+ type schema_MapProvider = MapProvider;
301
+ declare const schema_MapProvider: typeof MapProvider;
302
+ type schema_SchemaHandler = SchemaHandler;
303
+ type schema_SchemaNode = SchemaNode;
304
+ type schema_SchemaProvider = SchemaProvider;
305
+ declare const schema_check: typeof check;
306
+ declare const schema_validate: typeof validate;
307
+ declare const schema_validateSchema: typeof validateSchema;
308
+ declare const schema_wrap: typeof wrap;
309
+ declare namespace schema {
310
+ export { schema_MapProvider as MapProvider, type schema_SchemaHandler as SchemaHandler, type schema_SchemaNode as SchemaNode, type schema_SchemaProvider as SchemaProvider, schema_check as check, schema_validate as validate, schema_validateSchema as validateSchema, schema_wrap as wrap };
311
+ }
174
312
 
175
- export { type AnnotateOptions, BabelQueueError, type DeadLetter, type Envelope, EnvelopeCodec, type HasTraceId, type IncomingEnvelope, type MakeOptions, type Meta, type PolyglotMessage, SCHEMA_VERSION, SOURCE_LANG, UnknownUrnError, UnknownUrnStrategy, annotate, deadLetter };
313
+ export { type AnnotateOptions, BabelQueueError, type DeadLetter, type Envelope, EnvelopeCodec, type Handler, type HasTraceId, InMemoryStore, type IncomingEnvelope, InvalidPayloadError, type MakeOptions, type Meta, type PolyglotMessage, SCHEMA_VERSION, SOURCE_LANG, type SchemaNode, type SchemaProvider, type Store, UnknownUrnError, UnknownUrnStrategy, Wrap, annotate, deadLetter, schema };
package/dist/index.d.ts CHANGED
@@ -171,5 +171,143 @@ declare class BabelQueueError extends Error {
171
171
  declare class UnknownUrnError extends BabelQueueError {
172
172
  constructor(urn: string);
173
173
  }
174
+ /**
175
+ * Raised when a message's `data` does not match the JSON Schema registered for its URN
176
+ * (ADR-0024). The consumer-side {@link schema.wrap} throws it so the adapter redelivers
177
+ * (and eventually dead-letters) a poison message.
178
+ */
179
+ declare class InvalidPayloadError extends BabelQueueError {
180
+ readonly urn: string;
181
+ readonly violation: string;
182
+ constructor(urn: string, violation: string);
183
+ }
184
+
185
+ /**
186
+ * Optional idempotency helper (ADR-0022): dedupe a consume handler on `meta.id`.
187
+ *
188
+ * The Node mirror of the PHP `BabelQueue\Idempotency` and Go `idempotency` helpers.
189
+ * The core is codec-only (no dispatcher), so this wraps a user-provided handler that
190
+ * an adapter (NestJS, BullMQ, ...) drives:
191
+ *
192
+ * ```ts
193
+ * import { Wrap, InMemoryStore, type Handler } from "@babelqueue/core";
194
+ *
195
+ * const store = new InMemoryStore();
196
+ * const handler = Wrap(store, async (env) => { ... });
197
+ * ```
198
+ *
199
+ * A previously-seen id returns early (the adapter acks it); a throwing/rejecting
200
+ * handler leaves the id unmarked so a redelivery runs it again; a message with no
201
+ * usable `meta.id` runs unchanged. "Seen-set" post-success dedupe — not exactly-once,
202
+ * not in-flight concurrency locking (a transactional mode is a future direction).
203
+ */
204
+
205
+ /** A consume handler: receives a decoded envelope, may be sync or async. */
206
+ type Handler = (env: Envelope) => void | Promise<void>;
207
+ /**
208
+ * A pluggable record of message ids already processed, keyed on `meta.id`. Methods may
209
+ * be sync or async so a production store can be Redis- or DB-backed; the reference
210
+ * {@link InMemoryStore} is synchronous.
211
+ */
212
+ interface Store {
213
+ seen(messageId: string): boolean | Promise<boolean>;
214
+ remember(messageId: string): void | Promise<void>;
215
+ forget(messageId: string): void | Promise<void>;
216
+ }
217
+ /**
218
+ * Process-local {@link Store} backed by a Set. For tests / single-process consumers;
219
+ * not shared across workers and not persistent — use a Redis- or DB-backed store for
220
+ * production fleets.
221
+ */
222
+ declare class InMemoryStore implements Store {
223
+ private readonly entries;
224
+ seen(messageId: string): boolean;
225
+ remember(messageId: string): void;
226
+ forget(messageId: string): void;
227
+ }
228
+ /**
229
+ * Wraps `handler` so a message whose `meta.id` was already processed successfully is
230
+ * skipped. A thrown/rejected handler leaves the id unmarked, so a redelivery runs it
231
+ * again (retry / dead-letter still apply); a message with no usable id runs unchanged.
232
+ */
233
+ declare function Wrap(store: Store, handler: Handler): Handler;
234
+
235
+ /**
236
+ * Optional per-URN payload schema validation (ADR-0024).
237
+ *
238
+ * The Node mirror of the Go `schema` package and PHP `BabelQueue\Schema`. A
239
+ * {@link SchemaProvider} supplies a JSON Schema for a message URN — typically built from a
240
+ * babelqueue-registry `registry.json` — and the message's `data` is validated against it.
241
+ * It is opt-in: a URN with no registered schema is never validated.
242
+ *
243
+ * ```ts
244
+ * import { schema } from "@babelqueue/core";
245
+ *
246
+ * const provider = schema.MapProvider.fromJson({ "urn:babel:orders:created": ORDERS_JSON });
247
+ * schema.validate(provider, "urn:babel:orders:created", { order_id: 7 }); // throws on mismatch
248
+ * const handler = schema.wrap(provider, async (env) => { ... }); // consumer safety net
249
+ * ```
250
+ *
251
+ * The core stays dependency-free and I/O-free, so it carries no file-based provider: a Node
252
+ * app or adapter reads its `registry.json` (with `node:fs`, etc.) and passes the schemas to
253
+ * {@link MapProvider.fromJson}. The validator is a small subset of JSON Schema (draft-07)
254
+ * whose verdicts match the Go, PHP and Python validators and babelqueue-registry's `compat`
255
+ * linter: `type`, `required`, `properties`, `additionalProperties`, `items`, `enum`,
256
+ * `const`, `minLength`, `minimum`. Unknown keywords are ignored.
257
+ */
258
+
259
+ /** A parsed JSON Schema node. */
260
+ type SchemaNode = Record<string, unknown>;
261
+ /** A consume handler: receives a decoded envelope, may be sync or async. */
262
+ type SchemaHandler = (env: Envelope) => void | Promise<void>;
263
+ /**
264
+ * A source of per-URN `data` schemas, keyed on the message URN. `schemaFor` may be sync or
265
+ * async so a production provider can be service- or cache-backed; the reference
266
+ * {@link MapProvider} is synchronous.
267
+ */
268
+ interface SchemaProvider {
269
+ schemaFor(urn: string): SchemaNode | undefined | Promise<SchemaNode | undefined>;
270
+ }
271
+ /** In-memory {@link SchemaProvider}, for tests and for embedding schemas in code. */
272
+ declare class MapProvider implements SchemaProvider {
273
+ private readonly schemas;
274
+ constructor(schemas: Record<string, SchemaNode>);
275
+ /** Build a provider from URN -> raw JSON Schema strings, parsing each. */
276
+ static fromJson(raw: Record<string, string>): MapProvider;
277
+ schemaFor(urn: string): SchemaNode | undefined;
278
+ }
279
+ /**
280
+ * The first `data` violation for `(urn, data)`, or null when it is valid or when no schema is
281
+ * registered for the URN (opt-in). For producer-side branching.
282
+ */
283
+ declare function check(provider: SchemaProvider, urn: string, data: Record<string, unknown>): Promise<string | null>;
284
+ /**
285
+ * Validate `(urn, data)` against its registered schema, throwing {@link InvalidPayloadError}
286
+ * otherwise. The producer-side guard; call it before publishing.
287
+ */
288
+ declare function validate(provider: SchemaProvider, urn: string, data: Record<string, unknown>): Promise<void>;
289
+ /**
290
+ * Wrap a consume handler so each message's `data` is validated against its URN's schema
291
+ * before the handler runs (consumer-side safety net). Invalid data throws
292
+ * {@link InvalidPayloadError}, so the adapter redelivers (and eventually dead-letters) the
293
+ * poison message; a URN with no schema runs the handler unchanged. Prefer {@link check}
294
+ * producer-side to keep invalid data out of the queue entirely.
295
+ */
296
+ declare function wrap(provider: SchemaProvider, handler: SchemaHandler): SchemaHandler;
297
+ /** The first violation of `value` against a (subset) JSON Schema node, or null. */
298
+ declare function validateSchema(schema: SchemaNode, value: unknown, path?: string): string | null;
299
+
300
+ type schema_MapProvider = MapProvider;
301
+ declare const schema_MapProvider: typeof MapProvider;
302
+ type schema_SchemaHandler = SchemaHandler;
303
+ type schema_SchemaNode = SchemaNode;
304
+ type schema_SchemaProvider = SchemaProvider;
305
+ declare const schema_check: typeof check;
306
+ declare const schema_validate: typeof validate;
307
+ declare const schema_validateSchema: typeof validateSchema;
308
+ declare const schema_wrap: typeof wrap;
309
+ declare namespace schema {
310
+ export { schema_MapProvider as MapProvider, type schema_SchemaHandler as SchemaHandler, type schema_SchemaNode as SchemaNode, type schema_SchemaProvider as SchemaProvider, schema_check as check, schema_validate as validate, schema_validateSchema as validateSchema, schema_wrap as wrap };
311
+ }
174
312
 
175
- export { type AnnotateOptions, BabelQueueError, type DeadLetter, type Envelope, EnvelopeCodec, type HasTraceId, type IncomingEnvelope, type MakeOptions, type Meta, type PolyglotMessage, SCHEMA_VERSION, SOURCE_LANG, UnknownUrnError, UnknownUrnStrategy, annotate, deadLetter };
313
+ export { type AnnotateOptions, BabelQueueError, type DeadLetter, type Envelope, EnvelopeCodec, type Handler, type HasTraceId, InMemoryStore, type IncomingEnvelope, InvalidPayloadError, type MakeOptions, type Meta, type PolyglotMessage, SCHEMA_VERSION, SOURCE_LANG, type SchemaNode, type SchemaProvider, type Store, UnknownUrnError, UnknownUrnStrategy, Wrap, annotate, deadLetter, schema };
package/dist/index.js CHANGED
@@ -20,6 +20,16 @@ var UnknownUrnError = class extends BabelQueueError {
20
20
  this.name = "UnknownUrnError";
21
21
  }
22
22
  };
23
+ var InvalidPayloadError = class extends BabelQueueError {
24
+ constructor(urn, violation2) {
25
+ super(`Message data for "${urn}" does not match its URN schema: ${violation2}.`);
26
+ this.urn = urn;
27
+ this.violation = violation2;
28
+ this.name = "InvalidPayloadError";
29
+ }
30
+ urn;
31
+ violation;
32
+ };
23
33
 
24
34
  // src/codec.ts
25
35
  var SCHEMA_VERSION = 1;
@@ -159,14 +169,203 @@ var UnknownUrnStrategy = {
159
169
  /** Route to the dead-letter queue. */
160
170
  DEAD_LETTER: "dead_letter"
161
171
  };
172
+
173
+ // src/idempotency.ts
174
+ var InMemoryStore = class {
175
+ entries = /* @__PURE__ */ new Set();
176
+ seen(messageId) {
177
+ return this.entries.has(messageId);
178
+ }
179
+ remember(messageId) {
180
+ this.entries.add(messageId);
181
+ }
182
+ forget(messageId) {
183
+ this.entries.delete(messageId);
184
+ }
185
+ };
186
+ function Wrap(store, handler) {
187
+ return async (env) => {
188
+ const id = env.meta.id;
189
+ if (!id) {
190
+ await handler(env);
191
+ return;
192
+ }
193
+ if (await store.seen(id)) {
194
+ return;
195
+ }
196
+ await handler(env);
197
+ await store.remember(id);
198
+ };
199
+ }
200
+
201
+ // src/schema.ts
202
+ var schema_exports = {};
203
+ __export(schema_exports, {
204
+ MapProvider: () => MapProvider,
205
+ check: () => check,
206
+ validate: () => validate,
207
+ validateSchema: () => validateSchema,
208
+ wrap: () => wrap
209
+ });
210
+ var MapProvider = class _MapProvider {
211
+ schemas;
212
+ constructor(schemas) {
213
+ this.schemas = new Map(Object.entries(schemas));
214
+ }
215
+ /** Build a provider from URN -> raw JSON Schema strings, parsing each. */
216
+ static fromJson(raw) {
217
+ const schemas = {};
218
+ for (const [urn, body] of Object.entries(raw)) {
219
+ const decoded = JSON.parse(body);
220
+ if (typeof decoded !== "object" || decoded === null || Array.isArray(decoded)) {
221
+ throw new Error(`schema: invalid JSON schema for "${urn}"`);
222
+ }
223
+ schemas[urn] = decoded;
224
+ }
225
+ return new _MapProvider(schemas);
226
+ }
227
+ schemaFor(urn) {
228
+ return this.schemas.get(urn);
229
+ }
230
+ };
231
+ async function check(provider, urn, data) {
232
+ const schemaNode = await provider.schemaFor(urn);
233
+ if (!schemaNode) {
234
+ return null;
235
+ }
236
+ return validateSchema(schemaNode, data);
237
+ }
238
+ async function validate(provider, urn, data) {
239
+ const violation2 = await check(provider, urn, data);
240
+ if (violation2 !== null) {
241
+ throw new InvalidPayloadError(urn, violation2);
242
+ }
243
+ }
244
+ function wrap(provider, handler) {
245
+ return async (env) => {
246
+ await validate(provider, env.job, env.data);
247
+ await handler(env);
248
+ };
249
+ }
250
+ function validateSchema(schema, value, path = "") {
251
+ if ("const" in schema && !equal(value, schema.const)) {
252
+ return violation(path, "wrong_const");
253
+ }
254
+ const enumValues = schema.enum;
255
+ if (Array.isArray(enumValues) && !enumValues.some((item) => equal(value, item))) {
256
+ return violation(path, "not_in_enum");
257
+ }
258
+ const type = typeof schema.type === "string" ? schema.type : "";
259
+ switch (type) {
260
+ case "object":
261
+ return checkObject(schema, value, path);
262
+ case "array":
263
+ return checkArray(schema, value, path);
264
+ case "string": {
265
+ if (typeof value !== "string") {
266
+ return violation(path, "not_a_string");
267
+ }
268
+ const minLength = schema.minLength;
269
+ if (typeof minLength === "number" && value.length < minLength) {
270
+ return violation(path, "below_min_length");
271
+ }
272
+ return null;
273
+ }
274
+ case "integer":
275
+ if (!isInteger(value)) {
276
+ return violation(path, "not_an_integer");
277
+ }
278
+ return checkMinimum(schema, value, path);
279
+ case "number":
280
+ if (typeof value !== "number") {
281
+ return violation(path, "not_a_number");
282
+ }
283
+ return checkMinimum(schema, value, path);
284
+ case "boolean":
285
+ return typeof value === "boolean" ? null : violation(path, "not_a_boolean");
286
+ case "null":
287
+ return value === null ? null : violation(path, "not_null");
288
+ default:
289
+ return null;
290
+ }
291
+ }
292
+ function checkObject(schema, value, path) {
293
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
294
+ return violation(path, "not_an_object");
295
+ }
296
+ const obj = value;
297
+ const required = schema.required;
298
+ if (Array.isArray(required)) {
299
+ for (const key of required) {
300
+ if (typeof key === "string" && !(key in obj)) {
301
+ return violation(join(path, key), "missing_required");
302
+ }
303
+ }
304
+ }
305
+ const properties = typeof schema.properties === "object" && schema.properties !== null ? schema.properties : {};
306
+ const additionalAllowed = schema.additionalProperties !== false;
307
+ for (const [name, item] of Object.entries(obj)) {
308
+ const propSchema = properties[name];
309
+ if (typeof propSchema === "object" && propSchema !== null) {
310
+ const found = validateSchema(propSchema, item, join(path, name));
311
+ if (found !== null) {
312
+ return found;
313
+ }
314
+ continue;
315
+ }
316
+ if (!additionalAllowed) {
317
+ return violation(join(path, name), "additional_not_allowed");
318
+ }
319
+ }
320
+ return null;
321
+ }
322
+ function checkArray(schema, value, path) {
323
+ if (!Array.isArray(value)) {
324
+ return violation(path, "not_an_array");
325
+ }
326
+ const items = schema.items;
327
+ if (typeof items !== "object" || items === null) {
328
+ return null;
329
+ }
330
+ for (let i = 0; i < value.length; i++) {
331
+ const found = validateSchema(items, value[i], `${path}[${i}]`);
332
+ if (found !== null) {
333
+ return found;
334
+ }
335
+ }
336
+ return null;
337
+ }
338
+ function checkMinimum(schema, value, path) {
339
+ const minimum = schema.minimum;
340
+ if (typeof minimum === "number" && value < minimum) {
341
+ return violation(path, "below_minimum");
342
+ }
343
+ return null;
344
+ }
345
+ function isInteger(value) {
346
+ return typeof value === "number" && Number.isInteger(value);
347
+ }
348
+ function equal(a, b) {
349
+ return JSON.stringify(a) === JSON.stringify(b);
350
+ }
351
+ function violation(path, reason) {
352
+ return `${path === "" ? "<root>" : path}: ${reason}`;
353
+ }
354
+ function join(path, key) {
355
+ return path === "" ? key : `${path}.${key}`;
356
+ }
162
357
  export {
163
358
  BabelQueueError,
164
359
  EnvelopeCodec,
360
+ InMemoryStore,
361
+ InvalidPayloadError,
165
362
  SCHEMA_VERSION,
166
363
  SOURCE_LANG,
167
364
  UnknownUrnError,
168
365
  UnknownUrnStrategy,
366
+ Wrap,
169
367
  annotate,
170
- deadLetter_exports as deadLetter
368
+ deadLetter_exports as deadLetter,
369
+ schema_exports as schema
171
370
  };
172
371
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/codec.ts","../src/errors.ts","../src/deadLetter.ts","../src/routing.ts"],"sourcesContent":["import { randomUUID } from \"node:crypto\";\n\nimport type { HasTraceId, PolyglotMessage } from \"./contracts.js\";\nimport { BabelQueueError } from \"./errors.js\";\n\n/** The wire envelope schema version this core implements (versioned independently of the package version). */\nexport const SCHEMA_VERSION = 1;\n\n/** Stamped into `meta.lang` for envelopes produced by this core. */\nexport const SOURCE_LANG = \"node\";\n\n/** Immutable per-message metadata. */\nexport interface Meta {\n id: string;\n queue: string;\n lang: string;\n schema_version: number;\n /** Unix milliseconds, UTC. */\n created_at: number;\n}\n\n/** The additive block appended to an envelope when a message is dead-lettered. */\nexport interface DeadLetter {\n reason: string;\n error: string | null;\n exception: string | null;\n /** Unix milliseconds, UTC. */\n failed_at: number;\n original_queue: string;\n attempts: number;\n lang: string;\n}\n\n/**\n * The canonical BabelQueue wire message: a strict, language-neutral JSON shape\n * that every SDK produces and consumes identically. The property order here is\n * significant — it matches the other cores so {@link EnvelopeCodec.encode} is\n * byte-for-byte identical across the insertion-order languages (PHP/Python).\n */\nexport interface Envelope {\n /** The message URN (never a class name). */\n job: string;\n /** Correlation id, preserved across every hop. */\n trace_id: string;\n /** The pure-JSON payload. */\n data: Record<string, unknown>;\n meta: Meta;\n /** Top-level transport retry counter. */\n attempts: number;\n /** Present only once the message has been dead-lettered. */\n dead_letter?: DeadLetter;\n}\n\n/**\n * A decoded, not-yet-validated envelope. Fields are loosely typed because they\n * come off the wire; `urn` is accepted as an inbound alias for `job`. Narrow it\n * with {@link EnvelopeCodec.accepts} before trusting the contents.\n */\nexport interface IncomingEnvelope {\n job?: string;\n /** Inbound alias for `job`. */\n urn?: string;\n trace_id?: string;\n data?: unknown;\n meta?: unknown;\n attempts?: unknown;\n dead_letter?: unknown;\n}\n\n/** Options for {@link EnvelopeCodec.make}. */\nexport interface MakeOptions {\n /** Logical queue name recorded in `meta.queue` (default `\"default\"`). */\n queue?: string;\n /** Reuse an existing trace id (trace continuation) instead of minting one. */\n traceId?: string;\n}\n\n/**\n * Builds, encodes and decodes the canonical envelope — the single Node/TypeScript\n * implementation of the wire format.\n */\nexport const EnvelopeCodec = {\n SCHEMA_VERSION,\n SOURCE_LANG,\n\n /**\n * Build the canonical envelope for a `(urn, data)` pair. Mints a fresh trace id\n * unless `options.traceId` is given, starts `attempts` at 0, and stamps `meta`.\n * Throws {@link BabelQueueError} when the URN is blank.\n */\n make(\n urn: string,\n data: Record<string, unknown>,\n options: MakeOptions = {},\n ): Envelope {\n const resolvedUrn = (urn ?? \"\").trim();\n if (resolvedUrn === \"\") {\n throw new BabelQueueError(\n \"A polyglot message must expose a stable, non-empty URN so consumers can identify it without any class name.\",\n );\n }\n\n const traceId = (options.traceId ?? \"\").trim() || randomUUID();\n\n return {\n job: resolvedUrn,\n trace_id: traceId,\n data: { ...data },\n meta: {\n id: randomUUID(),\n queue: options.queue ?? \"default\",\n lang: SOURCE_LANG,\n schema_version: SCHEMA_VERSION,\n created_at: Date.now(),\n },\n attempts: 0,\n };\n },\n\n /**\n * Build the envelope from a {@link PolyglotMessage}. If the message also\n * implements {@link HasTraceId} and returns a non-empty value, that trace id is\n * reused.\n */\n fromMessage(\n message: PolyglotMessage & Partial<HasTraceId>,\n queue = \"default\",\n ): Envelope {\n const traceId =\n typeof message.getBabelTraceId === \"function\"\n ? (message.getBabelTraceId() ?? undefined)\n : undefined;\n\n return EnvelopeCodec.make(message.getBabelUrn(), message.toPayload(), {\n queue,\n traceId,\n });\n },\n\n /**\n * Encode the envelope as compact UTF-8 JSON. `JSON.stringify` already emits the\n * canonical form — no spaces, and slashes/unicode/HTML left unescaped — matching\n * the other SDK cores.\n */\n encode(envelope: Envelope): string {\n return JSON.stringify(envelope);\n },\n\n /**\n * Parse a raw JSON body. Returns `{}` for malformed or non-object input (call\n * {@link EnvelopeCodec.accepts} before trusting it). Resolves the `urn` inbound\n * alias into `job`.\n */\n decode(raw: string): IncomingEnvelope {\n let parsed: unknown;\n try {\n parsed = JSON.parse(raw);\n } catch {\n return {};\n }\n if (parsed === null || typeof parsed !== \"object\" || Array.isArray(parsed)) {\n return {};\n }\n\n const envelope = parsed as IncomingEnvelope;\n if (!envelope.job && typeof envelope.urn === \"string\") {\n envelope.job = envelope.urn;\n }\n return envelope;\n },\n\n /** The message URN — canonical `job`, with `urn` accepted as an alias. */\n urn(envelope: IncomingEnvelope): string {\n const value = envelope?.job ?? envelope?.urn ?? \"\";\n return typeof value === \"string\" ? value.trim() : \"\";\n },\n\n /**\n * Whether a consumer should accept this envelope. Rejects a missing URN, an\n * unsupported `meta.schema_version`, a non-object `data`, a non-integer\n * `attempts`, or a blank `trace_id` — the consumer-side counterpart to the\n * producer JSON Schema. Acts as a type guard that narrows to {@link Envelope}.\n */\n accepts(envelope: IncomingEnvelope): envelope is Envelope {\n if (EnvelopeCodec.urn(envelope) === \"\") {\n return false;\n }\n\n const meta = envelope.meta;\n if (\n meta === null ||\n typeof meta !== \"object\" ||\n (meta as Meta).schema_version !== SCHEMA_VERSION\n ) {\n return false;\n }\n\n const data = envelope.data;\n if (data === null || typeof data !== \"object\" || Array.isArray(data)) {\n return false;\n }\n\n const attempts = envelope.attempts;\n if (typeof attempts !== \"number\" || !Number.isInteger(attempts)) {\n return false;\n }\n\n const traceId = envelope.trace_id;\n if (typeof traceId !== \"string\" || traceId.trim() === \"\") {\n return false;\n }\n\n return true;\n },\n} as const;\n","/** Base error for all BabelQueue failures. */\nexport class BabelQueueError extends Error {\n constructor(message: string) {\n super(message);\n this.name = \"BabelQueueError\";\n }\n}\n\n/** Raised when no handler is mapped for a message URN. */\nexport class UnknownUrnError extends BabelQueueError {\n constructor(urn: string) {\n super(`No handler is mapped for the message URN \"${urn}\".`);\n this.name = \"UnknownUrnError\";\n }\n}\n","import type { DeadLetter, Envelope } from \"./codec.js\";\nimport { SOURCE_LANG } from \"./codec.js\";\n\n/** Options for {@link annotate}. */\nexport interface AnnotateOptions {\n /** Defaults to the envelope's current `attempts`. */\n attempts?: number;\n /** A human-readable error message (JSON `null` when omitted). */\n error?: string | null;\n /** The originating error type/class name (JSON `null` when omitted). */\n exception?: string | null;\n}\n\n/**\n * Return a copy of the envelope with a `dead_letter` block attached, recording\n * why and where it failed. The original envelope is preserved unchanged inside\n * the result, so any-language consumers can still read it.\n */\nexport function annotate(\n envelope: Envelope,\n reason: string,\n originalQueue: string,\n options: AnnotateOptions = {},\n): Envelope {\n const deadLetter: DeadLetter = {\n reason,\n error: options.error ?? null,\n exception: options.exception ?? null,\n failed_at: Date.now(),\n original_queue: originalQueue,\n attempts: options.attempts ?? envelope.attempts ?? 0,\n lang: SOURCE_LANG,\n };\n\n return { ...envelope, dead_letter: deadLetter };\n}\n","/**\n * What a consumer does with a message whose URN has no registered handler.\n * Mirrors the constants in every other SDK core.\n */\nexport const UnknownUrnStrategy = {\n /** Surface an error; let the worker decide. */\n FAIL: \"fail\",\n /** Drop the message. */\n DELETE: \"delete\",\n /** Requeue for another consumer. */\n RELEASE: \"release\",\n /** Route to the dead-letter queue. */\n DEAD_LETTER: \"dead_letter\",\n} as const;\n\nexport type UnknownUrnStrategy =\n (typeof UnknownUrnStrategy)[keyof typeof UnknownUrnStrategy];\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/codec.ts","../src/errors.ts","../src/deadLetter.ts","../src/routing.ts","../src/idempotency.ts","../src/schema.ts"],"sourcesContent":["import { randomUUID } from \"node:crypto\";\n\nimport type { HasTraceId, PolyglotMessage } from \"./contracts.js\";\nimport { BabelQueueError } from \"./errors.js\";\n\n/** The wire envelope schema version this core implements (versioned independently of the package version). */\nexport const SCHEMA_VERSION = 1;\n\n/** Stamped into `meta.lang` for envelopes produced by this core. */\nexport const SOURCE_LANG = \"node\";\n\n/** Immutable per-message metadata. */\nexport interface Meta {\n id: string;\n queue: string;\n lang: string;\n schema_version: number;\n /** Unix milliseconds, UTC. */\n created_at: number;\n}\n\n/** The additive block appended to an envelope when a message is dead-lettered. */\nexport interface DeadLetter {\n reason: string;\n error: string | null;\n exception: string | null;\n /** Unix milliseconds, UTC. */\n failed_at: number;\n original_queue: string;\n attempts: number;\n lang: string;\n}\n\n/**\n * The canonical BabelQueue wire message: a strict, language-neutral JSON shape\n * that every SDK produces and consumes identically. The property order here is\n * significant — it matches the other cores so {@link EnvelopeCodec.encode} is\n * byte-for-byte identical across the insertion-order languages (PHP/Python).\n */\nexport interface Envelope {\n /** The message URN (never a class name). */\n job: string;\n /** Correlation id, preserved across every hop. */\n trace_id: string;\n /** The pure-JSON payload. */\n data: Record<string, unknown>;\n meta: Meta;\n /** Top-level transport retry counter. */\n attempts: number;\n /** Present only once the message has been dead-lettered. */\n dead_letter?: DeadLetter;\n}\n\n/**\n * A decoded, not-yet-validated envelope. Fields are loosely typed because they\n * come off the wire; `urn` is accepted as an inbound alias for `job`. Narrow it\n * with {@link EnvelopeCodec.accepts} before trusting the contents.\n */\nexport interface IncomingEnvelope {\n job?: string;\n /** Inbound alias for `job`. */\n urn?: string;\n trace_id?: string;\n data?: unknown;\n meta?: unknown;\n attempts?: unknown;\n dead_letter?: unknown;\n}\n\n/** Options for {@link EnvelopeCodec.make}. */\nexport interface MakeOptions {\n /** Logical queue name recorded in `meta.queue` (default `\"default\"`). */\n queue?: string;\n /** Reuse an existing trace id (trace continuation) instead of minting one. */\n traceId?: string;\n}\n\n/**\n * Builds, encodes and decodes the canonical envelope — the single Node/TypeScript\n * implementation of the wire format.\n */\nexport const EnvelopeCodec = {\n SCHEMA_VERSION,\n SOURCE_LANG,\n\n /**\n * Build the canonical envelope for a `(urn, data)` pair. Mints a fresh trace id\n * unless `options.traceId` is given, starts `attempts` at 0, and stamps `meta`.\n * Throws {@link BabelQueueError} when the URN is blank.\n */\n make(\n urn: string,\n data: Record<string, unknown>,\n options: MakeOptions = {},\n ): Envelope {\n const resolvedUrn = (urn ?? \"\").trim();\n if (resolvedUrn === \"\") {\n throw new BabelQueueError(\n \"A polyglot message must expose a stable, non-empty URN so consumers can identify it without any class name.\",\n );\n }\n\n const traceId = (options.traceId ?? \"\").trim() || randomUUID();\n\n return {\n job: resolvedUrn,\n trace_id: traceId,\n data: { ...data },\n meta: {\n id: randomUUID(),\n queue: options.queue ?? \"default\",\n lang: SOURCE_LANG,\n schema_version: SCHEMA_VERSION,\n created_at: Date.now(),\n },\n attempts: 0,\n };\n },\n\n /**\n * Build the envelope from a {@link PolyglotMessage}. If the message also\n * implements {@link HasTraceId} and returns a non-empty value, that trace id is\n * reused.\n */\n fromMessage(\n message: PolyglotMessage & Partial<HasTraceId>,\n queue = \"default\",\n ): Envelope {\n const traceId =\n typeof message.getBabelTraceId === \"function\"\n ? (message.getBabelTraceId() ?? undefined)\n : undefined;\n\n return EnvelopeCodec.make(message.getBabelUrn(), message.toPayload(), {\n queue,\n traceId,\n });\n },\n\n /**\n * Encode the envelope as compact UTF-8 JSON. `JSON.stringify` already emits the\n * canonical form — no spaces, and slashes/unicode/HTML left unescaped — matching\n * the other SDK cores.\n */\n encode(envelope: Envelope): string {\n return JSON.stringify(envelope);\n },\n\n /**\n * Parse a raw JSON body. Returns `{}` for malformed or non-object input (call\n * {@link EnvelopeCodec.accepts} before trusting it). Resolves the `urn` inbound\n * alias into `job`.\n */\n decode(raw: string): IncomingEnvelope {\n let parsed: unknown;\n try {\n parsed = JSON.parse(raw);\n } catch {\n return {};\n }\n if (parsed === null || typeof parsed !== \"object\" || Array.isArray(parsed)) {\n return {};\n }\n\n const envelope = parsed as IncomingEnvelope;\n if (!envelope.job && typeof envelope.urn === \"string\") {\n envelope.job = envelope.urn;\n }\n return envelope;\n },\n\n /** The message URN — canonical `job`, with `urn` accepted as an alias. */\n urn(envelope: IncomingEnvelope): string {\n const value = envelope?.job ?? envelope?.urn ?? \"\";\n return typeof value === \"string\" ? value.trim() : \"\";\n },\n\n /**\n * Whether a consumer should accept this envelope. Rejects a missing URN, an\n * unsupported `meta.schema_version`, a non-object `data`, a non-integer\n * `attempts`, or a blank `trace_id` — the consumer-side counterpart to the\n * producer JSON Schema. Acts as a type guard that narrows to {@link Envelope}.\n */\n accepts(envelope: IncomingEnvelope): envelope is Envelope {\n if (EnvelopeCodec.urn(envelope) === \"\") {\n return false;\n }\n\n const meta = envelope.meta;\n if (\n meta === null ||\n typeof meta !== \"object\" ||\n (meta as Meta).schema_version !== SCHEMA_VERSION\n ) {\n return false;\n }\n\n const data = envelope.data;\n if (data === null || typeof data !== \"object\" || Array.isArray(data)) {\n return false;\n }\n\n const attempts = envelope.attempts;\n if (typeof attempts !== \"number\" || !Number.isInteger(attempts)) {\n return false;\n }\n\n const traceId = envelope.trace_id;\n if (typeof traceId !== \"string\" || traceId.trim() === \"\") {\n return false;\n }\n\n return true;\n },\n} as const;\n","/** Base error for all BabelQueue failures. */\nexport class BabelQueueError extends Error {\n constructor(message: string) {\n super(message);\n this.name = \"BabelQueueError\";\n }\n}\n\n/** Raised when no handler is mapped for a message URN. */\nexport class UnknownUrnError extends BabelQueueError {\n constructor(urn: string) {\n super(`No handler is mapped for the message URN \"${urn}\".`);\n this.name = \"UnknownUrnError\";\n }\n}\n\n/**\n * Raised when a message's `data` does not match the JSON Schema registered for its URN\n * (ADR-0024). The consumer-side {@link schema.wrap} throws it so the adapter redelivers\n * (and eventually dead-letters) a poison message.\n */\nexport class InvalidPayloadError extends BabelQueueError {\n constructor(\n readonly urn: string,\n readonly violation: string,\n ) {\n super(`Message data for \"${urn}\" does not match its URN schema: ${violation}.`);\n this.name = \"InvalidPayloadError\";\n }\n}\n","import type { DeadLetter, Envelope } from \"./codec.js\";\nimport { SOURCE_LANG } from \"./codec.js\";\n\n/** Options for {@link annotate}. */\nexport interface AnnotateOptions {\n /** Defaults to the envelope's current `attempts`. */\n attempts?: number;\n /** A human-readable error message (JSON `null` when omitted). */\n error?: string | null;\n /** The originating error type/class name (JSON `null` when omitted). */\n exception?: string | null;\n}\n\n/**\n * Return a copy of the envelope with a `dead_letter` block attached, recording\n * why and where it failed. The original envelope is preserved unchanged inside\n * the result, so any-language consumers can still read it.\n */\nexport function annotate(\n envelope: Envelope,\n reason: string,\n originalQueue: string,\n options: AnnotateOptions = {},\n): Envelope {\n const deadLetter: DeadLetter = {\n reason,\n error: options.error ?? null,\n exception: options.exception ?? null,\n failed_at: Date.now(),\n original_queue: originalQueue,\n attempts: options.attempts ?? envelope.attempts ?? 0,\n lang: SOURCE_LANG,\n };\n\n return { ...envelope, dead_letter: deadLetter };\n}\n","/**\n * What a consumer does with a message whose URN has no registered handler.\n * Mirrors the constants in every other SDK core.\n */\nexport const UnknownUrnStrategy = {\n /** Surface an error; let the worker decide. */\n FAIL: \"fail\",\n /** Drop the message. */\n DELETE: \"delete\",\n /** Requeue for another consumer. */\n RELEASE: \"release\",\n /** Route to the dead-letter queue. */\n DEAD_LETTER: \"dead_letter\",\n} as const;\n\nexport type UnknownUrnStrategy =\n (typeof UnknownUrnStrategy)[keyof typeof UnknownUrnStrategy];\n","/**\n * Optional idempotency helper (ADR-0022): dedupe a consume handler on `meta.id`.\n *\n * The Node mirror of the PHP `BabelQueue\\Idempotency` and Go `idempotency` helpers.\n * The core is codec-only (no dispatcher), so this wraps a user-provided handler that\n * an adapter (NestJS, BullMQ, ...) drives:\n *\n * ```ts\n * import { Wrap, InMemoryStore, type Handler } from \"@babelqueue/core\";\n *\n * const store = new InMemoryStore();\n * const handler = Wrap(store, async (env) => { ... });\n * ```\n *\n * A previously-seen id returns early (the adapter acks it); a throwing/rejecting\n * handler leaves the id unmarked so a redelivery runs it again; a message with no\n * usable `meta.id` runs unchanged. \"Seen-set\" post-success dedupe — not exactly-once,\n * not in-flight concurrency locking (a transactional mode is a future direction).\n */\nimport type { Envelope } from \"./codec.js\";\n\n/** A consume handler: receives a decoded envelope, may be sync or async. */\nexport type Handler = (env: Envelope) => void | Promise<void>;\n\n/**\n * A pluggable record of message ids already processed, keyed on `meta.id`. Methods may\n * be sync or async so a production store can be Redis- or DB-backed; the reference\n * {@link InMemoryStore} is synchronous.\n */\nexport interface Store {\n seen(messageId: string): boolean | Promise<boolean>;\n remember(messageId: string): void | Promise<void>;\n forget(messageId: string): void | Promise<void>;\n}\n\n/**\n * Process-local {@link Store} backed by a Set. For tests / single-process consumers;\n * not shared across workers and not persistent — use a Redis- or DB-backed store for\n * production fleets.\n */\nexport class InMemoryStore implements Store {\n private readonly entries = new Set<string>();\n\n seen(messageId: string): boolean {\n return this.entries.has(messageId);\n }\n\n remember(messageId: string): void {\n this.entries.add(messageId);\n }\n\n forget(messageId: string): void {\n this.entries.delete(messageId);\n }\n}\n\n/**\n * Wraps `handler` so a message whose `meta.id` was already processed successfully is\n * skipped. A thrown/rejected handler leaves the id unmarked, so a redelivery runs it\n * again (retry / dead-letter still apply); a message with no usable id runs unchanged.\n */\nexport function Wrap(store: Store, handler: Handler): Handler {\n return async (env: Envelope): Promise<void> => {\n const id = env.meta.id;\n\n // No usable id → cannot dedupe; run the handler unchanged.\n if (!id) {\n await handler(env);\n return;\n }\n\n // Already processed on an earlier delivery: return so the adapter acks it.\n if (await store.seen(id)) {\n return;\n }\n\n // First success wins; a throw here leaves the id unmarked → retry/DLQ apply.\n await handler(env);\n await store.remember(id);\n };\n}\n","/**\n * Optional per-URN payload schema validation (ADR-0024).\n *\n * The Node mirror of the Go `schema` package and PHP `BabelQueue\\Schema`. A\n * {@link SchemaProvider} supplies a JSON Schema for a message URN — typically built from a\n * babelqueue-registry `registry.json` — and the message's `data` is validated against it.\n * It is opt-in: a URN with no registered schema is never validated.\n *\n * ```ts\n * import { schema } from \"@babelqueue/core\";\n *\n * const provider = schema.MapProvider.fromJson({ \"urn:babel:orders:created\": ORDERS_JSON });\n * schema.validate(provider, \"urn:babel:orders:created\", { order_id: 7 }); // throws on mismatch\n * const handler = schema.wrap(provider, async (env) => { ... }); // consumer safety net\n * ```\n *\n * The core stays dependency-free and I/O-free, so it carries no file-based provider: a Node\n * app or adapter reads its `registry.json` (with `node:fs`, etc.) and passes the schemas to\n * {@link MapProvider.fromJson}. The validator is a small subset of JSON Schema (draft-07)\n * whose verdicts match the Go, PHP and Python validators and babelqueue-registry's `compat`\n * linter: `type`, `required`, `properties`, `additionalProperties`, `items`, `enum`,\n * `const`, `minLength`, `minimum`. Unknown keywords are ignored.\n */\nimport type { Envelope } from \"./codec.js\";\nimport { InvalidPayloadError } from \"./errors.js\";\n\n/** A parsed JSON Schema node. */\nexport type SchemaNode = Record<string, unknown>;\n\n/** A consume handler: receives a decoded envelope, may be sync or async. */\nexport type SchemaHandler = (env: Envelope) => void | Promise<void>;\n\n/**\n * A source of per-URN `data` schemas, keyed on the message URN. `schemaFor` may be sync or\n * async so a production provider can be service- or cache-backed; the reference\n * {@link MapProvider} is synchronous.\n */\nexport interface SchemaProvider {\n schemaFor(urn: string): SchemaNode | undefined | Promise<SchemaNode | undefined>;\n}\n\n/** In-memory {@link SchemaProvider}, for tests and for embedding schemas in code. */\nexport class MapProvider implements SchemaProvider {\n private readonly schemas: Map<string, SchemaNode>;\n\n constructor(schemas: Record<string, SchemaNode>) {\n this.schemas = new Map(Object.entries(schemas));\n }\n\n /** Build a provider from URN -> raw JSON Schema strings, parsing each. */\n static fromJson(raw: Record<string, string>): MapProvider {\n const schemas: Record<string, SchemaNode> = {};\n for (const [urn, body] of Object.entries(raw)) {\n const decoded: unknown = JSON.parse(body);\n if (typeof decoded !== \"object\" || decoded === null || Array.isArray(decoded)) {\n throw new Error(`schema: invalid JSON schema for \"${urn}\"`);\n }\n schemas[urn] = decoded as SchemaNode;\n }\n return new MapProvider(schemas);\n }\n\n schemaFor(urn: string): SchemaNode | undefined {\n return this.schemas.get(urn);\n }\n}\n\n/**\n * The first `data` violation for `(urn, data)`, or null when it is valid or when no schema is\n * registered for the URN (opt-in). For producer-side branching.\n */\nexport async function check(\n provider: SchemaProvider,\n urn: string,\n data: Record<string, unknown>,\n): Promise<string | null> {\n const schemaNode = await provider.schemaFor(urn);\n if (!schemaNode) {\n return null;\n }\n return validateSchema(schemaNode, data);\n}\n\n/**\n * Validate `(urn, data)` against its registered schema, throwing {@link InvalidPayloadError}\n * otherwise. The producer-side guard; call it before publishing.\n */\nexport async function validate(\n provider: SchemaProvider,\n urn: string,\n data: Record<string, unknown>,\n): Promise<void> {\n const violation = await check(provider, urn, data);\n if (violation !== null) {\n throw new InvalidPayloadError(urn, violation);\n }\n}\n\n/**\n * Wrap a consume handler so each message's `data` is validated against its URN's schema\n * before the handler runs (consumer-side safety net). Invalid data throws\n * {@link InvalidPayloadError}, so the adapter redelivers (and eventually dead-letters) the\n * poison message; a URN with no schema runs the handler unchanged. Prefer {@link check}\n * producer-side to keep invalid data out of the queue entirely.\n */\nexport function wrap(provider: SchemaProvider, handler: SchemaHandler): SchemaHandler {\n return async (env: Envelope): Promise<void> => {\n await validate(provider, env.job, env.data);\n await handler(env);\n };\n}\n\n/** The first violation of `value` against a (subset) JSON Schema node, or null. */\nexport function validateSchema(schema: SchemaNode, value: unknown, path = \"\"): string | null {\n if (\"const\" in schema && !equal(value, schema.const)) {\n return violation(path, \"wrong_const\");\n }\n const enumValues = schema.enum;\n if (Array.isArray(enumValues) && !enumValues.some((item) => equal(value, item))) {\n return violation(path, \"not_in_enum\");\n }\n\n const type = typeof schema.type === \"string\" ? schema.type : \"\";\n switch (type) {\n case \"object\":\n return checkObject(schema, value, path);\n case \"array\":\n return checkArray(schema, value, path);\n case \"string\": {\n if (typeof value !== \"string\") {\n return violation(path, \"not_a_string\");\n }\n const minLength = schema.minLength;\n if (typeof minLength === \"number\" && value.length < minLength) {\n return violation(path, \"below_min_length\");\n }\n return null;\n }\n case \"integer\":\n if (!isInteger(value)) {\n return violation(path, \"not_an_integer\");\n }\n return checkMinimum(schema, value, path);\n case \"number\":\n if (typeof value !== \"number\") {\n return violation(path, \"not_a_number\");\n }\n return checkMinimum(schema, value, path);\n case \"boolean\":\n return typeof value === \"boolean\" ? null : violation(path, \"not_a_boolean\");\n case \"null\":\n return value === null ? null : violation(path, \"not_null\");\n default:\n return null;\n }\n}\n\nfunction checkObject(schema: SchemaNode, value: unknown, path: string): string | null {\n if (typeof value !== \"object\" || value === null || Array.isArray(value)) {\n return violation(path, \"not_an_object\");\n }\n const obj = value as Record<string, unknown>;\n\n const required = schema.required;\n if (Array.isArray(required)) {\n for (const key of required) {\n if (typeof key === \"string\" && !(key in obj)) {\n return violation(join(path, key), \"missing_required\");\n }\n }\n }\n\n const properties =\n typeof schema.properties === \"object\" && schema.properties !== null\n ? (schema.properties as Record<string, unknown>)\n : {};\n const additionalAllowed = schema.additionalProperties !== false;\n\n for (const [name, item] of Object.entries(obj)) {\n const propSchema = properties[name];\n if (typeof propSchema === \"object\" && propSchema !== null) {\n const found = validateSchema(propSchema as SchemaNode, item, join(path, name));\n if (found !== null) {\n return found;\n }\n continue;\n }\n if (!additionalAllowed) {\n return violation(join(path, name), \"additional_not_allowed\");\n }\n }\n\n return null;\n}\n\nfunction checkArray(schema: SchemaNode, value: unknown, path: string): string | null {\n if (!Array.isArray(value)) {\n return violation(path, \"not_an_array\");\n }\n const items = schema.items;\n if (typeof items !== \"object\" || items === null) {\n return null;\n }\n for (let i = 0; i < value.length; i++) {\n const found = validateSchema(items as SchemaNode, value[i], `${path}[${i}]`);\n if (found !== null) {\n return found;\n }\n }\n return null;\n}\n\nfunction checkMinimum(schema: SchemaNode, value: number, path: string): string | null {\n const minimum = schema.minimum;\n if (typeof minimum === \"number\" && value < minimum) {\n return violation(path, \"below_minimum\");\n }\n return null;\n}\n\n// JSON numbers are all `number` in JS; an integer is a whole number (and never a boolean).\nfunction isInteger(value: unknown): value is number {\n return typeof value === \"number\" && Number.isInteger(value);\n}\n\n// Structural equality for enum/const checks: JSON.stringify distinguishes a string \"1\" from\n// a number 1, matching the strict comparisons in the other SDK validators.\nfunction equal(a: unknown, b: unknown): boolean {\n return JSON.stringify(a) === JSON.stringify(b);\n}\n\nfunction violation(path: string, reason: string): string {\n return `${path === \"\" ? \"<root>\" : path}: ${reason}`;\n}\n\nfunction join(path: string, key: string): string {\n return path === \"\" ? key : `${path}.${key}`;\n}\n"],"mappings":"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","names":["violation","violation"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@babelqueue/core",
3
- "version": "0.1.0",
3
+ "version": "1.1.0",
4
4
  "description": "Polyglot Queues, Simplified — the Node/TypeScript core: the canonical BabelQueue wire-envelope codec, contracts and dead-letter helpers.",
5
5
  "keywords": [
6
6
  "queue",
@@ -48,14 +48,20 @@
48
48
  "scripts": {
49
49
  "build": "tsup",
50
50
  "typecheck": "tsc --noEmit",
51
- "test": "node --import tsx --test test/codec.test.ts test/dead-letter.test.ts test/conformance.test.ts",
51
+ "lint": "eslint src test",
52
+ "test": "node --import tsx --test test/codec.test.ts test/dead-letter.test.ts test/conformance.test.ts test/overhead.test.ts test/idempotency.test.ts test/schema.test.ts",
53
+ "coverage": "c8 --check-coverage --lines 90 --functions 90 --branches 85 --reporter=text npm test",
52
54
  "prepublishOnly": "npm run build"
53
55
  },
54
56
  "devDependencies": {
57
+ "@eslint/js": "^10.0.1",
55
58
  "@types/node": "^22",
59
+ "c8": "^11.0.0",
60
+ "eslint": "^10.4.1",
56
61
  "tsup": "^8",
57
62
  "tsx": "^4",
58
- "typescript": "^5.5"
63
+ "typescript": "^5.5",
64
+ "typescript-eslint": "^8.60.1"
59
65
  },
60
66
  "publishConfig": {
61
67
  "access": "public"