@babelqueue/core 1.0.0 → 1.2.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/chunk-7FUZ3LYT.js +150 -0
- package/dist/chunk-7FUZ3LYT.js.map +1 -0
- package/dist/idempotency-DDHjGwF7.d.cts +178 -0
- package/dist/idempotency-DDHjGwF7.d.ts +178 -0
- package/dist/index.cjs +205 -2
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +91 -127
- package/dist/index.d.ts +91 -127
- package/dist/index.js +197 -130
- package/dist/index.js.map +1 -1
- package/dist/otel.cjs +245 -0
- package/dist/otel.cjs.map +1 -0
- package/dist/otel.d.cts +52 -0
- package/dist/otel.d.ts +52 -0
- package/dist/otel.js +105 -0
- package/dist/otel.js.map +1 -0
- package/package.json +22 -2
package/dist/index.d.ts
CHANGED
|
@@ -1,129 +1,5 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
* own classes/objects so {@link EnvelopeCodec.fromMessage} can build the canonical
|
|
4
|
-
* envelope without ever leaking a language-specific class name onto the wire.
|
|
5
|
-
*/
|
|
6
|
-
interface PolyglotMessage {
|
|
7
|
-
/** The stable URN that identifies this message across languages. */
|
|
8
|
-
getBabelUrn(): string;
|
|
9
|
-
/** The pure-JSON payload (no class instances). */
|
|
10
|
-
toPayload(): Record<string, unknown>;
|
|
11
|
-
}
|
|
12
|
-
/**
|
|
13
|
-
* Optionally implemented alongside {@link PolyglotMessage} to continue an existing
|
|
14
|
-
* distributed trace instead of minting a fresh one.
|
|
15
|
-
*/
|
|
16
|
-
interface HasTraceId {
|
|
17
|
-
/** The trace id to reuse, or null/undefined to mint a new one. */
|
|
18
|
-
getBabelTraceId(): string | null | undefined;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
/** The wire envelope schema version this core implements (versioned independently of the package version). */
|
|
22
|
-
declare const SCHEMA_VERSION = 1;
|
|
23
|
-
/** Stamped into `meta.lang` for envelopes produced by this core. */
|
|
24
|
-
declare const SOURCE_LANG = "node";
|
|
25
|
-
/** Immutable per-message metadata. */
|
|
26
|
-
interface Meta {
|
|
27
|
-
id: string;
|
|
28
|
-
queue: string;
|
|
29
|
-
lang: string;
|
|
30
|
-
schema_version: number;
|
|
31
|
-
/** Unix milliseconds, UTC. */
|
|
32
|
-
created_at: number;
|
|
33
|
-
}
|
|
34
|
-
/** The additive block appended to an envelope when a message is dead-lettered. */
|
|
35
|
-
interface DeadLetter {
|
|
36
|
-
reason: string;
|
|
37
|
-
error: string | null;
|
|
38
|
-
exception: string | null;
|
|
39
|
-
/** Unix milliseconds, UTC. */
|
|
40
|
-
failed_at: number;
|
|
41
|
-
original_queue: string;
|
|
42
|
-
attempts: number;
|
|
43
|
-
lang: string;
|
|
44
|
-
}
|
|
45
|
-
/**
|
|
46
|
-
* The canonical BabelQueue wire message: a strict, language-neutral JSON shape
|
|
47
|
-
* that every SDK produces and consumes identically. The property order here is
|
|
48
|
-
* significant — it matches the other cores so {@link EnvelopeCodec.encode} is
|
|
49
|
-
* byte-for-byte identical across the insertion-order languages (PHP/Python).
|
|
50
|
-
*/
|
|
51
|
-
interface Envelope {
|
|
52
|
-
/** The message URN (never a class name). */
|
|
53
|
-
job: string;
|
|
54
|
-
/** Correlation id, preserved across every hop. */
|
|
55
|
-
trace_id: string;
|
|
56
|
-
/** The pure-JSON payload. */
|
|
57
|
-
data: Record<string, unknown>;
|
|
58
|
-
meta: Meta;
|
|
59
|
-
/** Top-level transport retry counter. */
|
|
60
|
-
attempts: number;
|
|
61
|
-
/** Present only once the message has been dead-lettered. */
|
|
62
|
-
dead_letter?: DeadLetter;
|
|
63
|
-
}
|
|
64
|
-
/**
|
|
65
|
-
* A decoded, not-yet-validated envelope. Fields are loosely typed because they
|
|
66
|
-
* come off the wire; `urn` is accepted as an inbound alias for `job`. Narrow it
|
|
67
|
-
* with {@link EnvelopeCodec.accepts} before trusting the contents.
|
|
68
|
-
*/
|
|
69
|
-
interface IncomingEnvelope {
|
|
70
|
-
job?: string;
|
|
71
|
-
/** Inbound alias for `job`. */
|
|
72
|
-
urn?: string;
|
|
73
|
-
trace_id?: string;
|
|
74
|
-
data?: unknown;
|
|
75
|
-
meta?: unknown;
|
|
76
|
-
attempts?: unknown;
|
|
77
|
-
dead_letter?: unknown;
|
|
78
|
-
}
|
|
79
|
-
/** Options for {@link EnvelopeCodec.make}. */
|
|
80
|
-
interface MakeOptions {
|
|
81
|
-
/** Logical queue name recorded in `meta.queue` (default `"default"`). */
|
|
82
|
-
queue?: string;
|
|
83
|
-
/** Reuse an existing trace id (trace continuation) instead of minting one. */
|
|
84
|
-
traceId?: string;
|
|
85
|
-
}
|
|
86
|
-
/**
|
|
87
|
-
* Builds, encodes and decodes the canonical envelope — the single Node/TypeScript
|
|
88
|
-
* implementation of the wire format.
|
|
89
|
-
*/
|
|
90
|
-
declare const EnvelopeCodec: {
|
|
91
|
-
readonly SCHEMA_VERSION: 1;
|
|
92
|
-
readonly SOURCE_LANG: "node";
|
|
93
|
-
/**
|
|
94
|
-
* Build the canonical envelope for a `(urn, data)` pair. Mints a fresh trace id
|
|
95
|
-
* unless `options.traceId` is given, starts `attempts` at 0, and stamps `meta`.
|
|
96
|
-
* Throws {@link BabelQueueError} when the URN is blank.
|
|
97
|
-
*/
|
|
98
|
-
readonly make: (urn: string, data: Record<string, unknown>, options?: MakeOptions) => Envelope;
|
|
99
|
-
/**
|
|
100
|
-
* Build the envelope from a {@link PolyglotMessage}. If the message also
|
|
101
|
-
* implements {@link HasTraceId} and returns a non-empty value, that trace id is
|
|
102
|
-
* reused.
|
|
103
|
-
*/
|
|
104
|
-
readonly fromMessage: (message: PolyglotMessage & Partial<HasTraceId>, queue?: string) => Envelope;
|
|
105
|
-
/**
|
|
106
|
-
* Encode the envelope as compact UTF-8 JSON. `JSON.stringify` already emits the
|
|
107
|
-
* canonical form — no spaces, and slashes/unicode/HTML left unescaped — matching
|
|
108
|
-
* the other SDK cores.
|
|
109
|
-
*/
|
|
110
|
-
readonly encode: (envelope: Envelope) => string;
|
|
111
|
-
/**
|
|
112
|
-
* Parse a raw JSON body. Returns `{}` for malformed or non-object input (call
|
|
113
|
-
* {@link EnvelopeCodec.accepts} before trusting it). Resolves the `urn` inbound
|
|
114
|
-
* alias into `job`.
|
|
115
|
-
*/
|
|
116
|
-
readonly decode: (raw: string) => IncomingEnvelope;
|
|
117
|
-
/** The message URN — canonical `job`, with `urn` accepted as an alias. */
|
|
118
|
-
readonly urn: (envelope: IncomingEnvelope) => string;
|
|
119
|
-
/**
|
|
120
|
-
* Whether a consumer should accept this envelope. Rejects a missing URN, an
|
|
121
|
-
* unsupported `meta.schema_version`, a non-object `data`, a non-integer
|
|
122
|
-
* `attempts`, or a blank `trace_id` — the consumer-side counterpart to the
|
|
123
|
-
* producer JSON Schema. Acts as a type guard that narrows to {@link Envelope}.
|
|
124
|
-
*/
|
|
125
|
-
readonly accepts: (envelope: IncomingEnvelope) => envelope is Envelope;
|
|
126
|
-
};
|
|
1
|
+
import { E as Envelope } from './idempotency-DDHjGwF7.js';
|
|
2
|
+
export { D as DeadLetter, a as EnvelopeCodec, H as Handler, b as HasTraceId, I as InMemoryStore, c as IncomingEnvelope, M as MakeOptions, d as Meta, P as PolyglotMessage, S as SCHEMA_VERSION, e as SOURCE_LANG, f as Store, W as Wrap } from './idempotency-DDHjGwF7.js';
|
|
127
3
|
|
|
128
4
|
/** Options for {@link annotate}. */
|
|
129
5
|
interface AnnotateOptions {
|
|
@@ -171,5 +47,93 @@ declare class BabelQueueError extends Error {
|
|
|
171
47
|
declare class UnknownUrnError extends BabelQueueError {
|
|
172
48
|
constructor(urn: string);
|
|
173
49
|
}
|
|
50
|
+
/**
|
|
51
|
+
* Raised when a message's `data` does not match the JSON Schema registered for its URN
|
|
52
|
+
* (ADR-0024). The consumer-side {@link schema.wrap} throws it so the adapter redelivers
|
|
53
|
+
* (and eventually dead-letters) a poison message.
|
|
54
|
+
*/
|
|
55
|
+
declare class InvalidPayloadError extends BabelQueueError {
|
|
56
|
+
readonly urn: string;
|
|
57
|
+
readonly violation: string;
|
|
58
|
+
constructor(urn: string, violation: string);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Optional per-URN payload schema validation (ADR-0024).
|
|
63
|
+
*
|
|
64
|
+
* The Node mirror of the Go `schema` package and PHP `BabelQueue\Schema`. A
|
|
65
|
+
* {@link SchemaProvider} supplies a JSON Schema for a message URN — typically built from a
|
|
66
|
+
* babelqueue-registry `registry.json` — and the message's `data` is validated against it.
|
|
67
|
+
* It is opt-in: a URN with no registered schema is never validated.
|
|
68
|
+
*
|
|
69
|
+
* ```ts
|
|
70
|
+
* import { schema } from "@babelqueue/core";
|
|
71
|
+
*
|
|
72
|
+
* const provider = schema.MapProvider.fromJson({ "urn:babel:orders:created": ORDERS_JSON });
|
|
73
|
+
* schema.validate(provider, "urn:babel:orders:created", { order_id: 7 }); // throws on mismatch
|
|
74
|
+
* const handler = schema.wrap(provider, async (env) => { ... }); // consumer safety net
|
|
75
|
+
* ```
|
|
76
|
+
*
|
|
77
|
+
* The core stays dependency-free and I/O-free, so it carries no file-based provider: a Node
|
|
78
|
+
* app or adapter reads its `registry.json` (with `node:fs`, etc.) and passes the schemas to
|
|
79
|
+
* {@link MapProvider.fromJson}. The validator is a small subset of JSON Schema (draft-07)
|
|
80
|
+
* whose verdicts match the Go, PHP and Python validators and babelqueue-registry's `compat`
|
|
81
|
+
* linter: `type`, `required`, `properties`, `additionalProperties`, `items`, `enum`,
|
|
82
|
+
* `const`, `minLength`, `minimum`. Unknown keywords are ignored.
|
|
83
|
+
*/
|
|
84
|
+
|
|
85
|
+
/** A parsed JSON Schema node. */
|
|
86
|
+
type SchemaNode = Record<string, unknown>;
|
|
87
|
+
/** A consume handler: receives a decoded envelope, may be sync or async. */
|
|
88
|
+
type SchemaHandler = (env: Envelope) => void | Promise<void>;
|
|
89
|
+
/**
|
|
90
|
+
* A source of per-URN `data` schemas, keyed on the message URN. `schemaFor` may be sync or
|
|
91
|
+
* async so a production provider can be service- or cache-backed; the reference
|
|
92
|
+
* {@link MapProvider} is synchronous.
|
|
93
|
+
*/
|
|
94
|
+
interface SchemaProvider {
|
|
95
|
+
schemaFor(urn: string): SchemaNode | undefined | Promise<SchemaNode | undefined>;
|
|
96
|
+
}
|
|
97
|
+
/** In-memory {@link SchemaProvider}, for tests and for embedding schemas in code. */
|
|
98
|
+
declare class MapProvider implements SchemaProvider {
|
|
99
|
+
private readonly schemas;
|
|
100
|
+
constructor(schemas: Record<string, SchemaNode>);
|
|
101
|
+
/** Build a provider from URN -> raw JSON Schema strings, parsing each. */
|
|
102
|
+
static fromJson(raw: Record<string, string>): MapProvider;
|
|
103
|
+
schemaFor(urn: string): SchemaNode | undefined;
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* The first `data` violation for `(urn, data)`, or null when it is valid or when no schema is
|
|
107
|
+
* registered for the URN (opt-in). For producer-side branching.
|
|
108
|
+
*/
|
|
109
|
+
declare function check(provider: SchemaProvider, urn: string, data: Record<string, unknown>): Promise<string | null>;
|
|
110
|
+
/**
|
|
111
|
+
* Validate `(urn, data)` against its registered schema, throwing {@link InvalidPayloadError}
|
|
112
|
+
* otherwise. The producer-side guard; call it before publishing.
|
|
113
|
+
*/
|
|
114
|
+
declare function validate(provider: SchemaProvider, urn: string, data: Record<string, unknown>): Promise<void>;
|
|
115
|
+
/**
|
|
116
|
+
* Wrap a consume handler so each message's `data` is validated against its URN's schema
|
|
117
|
+
* before the handler runs (consumer-side safety net). Invalid data throws
|
|
118
|
+
* {@link InvalidPayloadError}, so the adapter redelivers (and eventually dead-letters) the
|
|
119
|
+
* poison message; a URN with no schema runs the handler unchanged. Prefer {@link check}
|
|
120
|
+
* producer-side to keep invalid data out of the queue entirely.
|
|
121
|
+
*/
|
|
122
|
+
declare function wrap(provider: SchemaProvider, handler: SchemaHandler): SchemaHandler;
|
|
123
|
+
/** The first violation of `value` against a (subset) JSON Schema node, or null. */
|
|
124
|
+
declare function validateSchema(schema: SchemaNode, value: unknown, path?: string): string | null;
|
|
125
|
+
|
|
126
|
+
type schema_MapProvider = MapProvider;
|
|
127
|
+
declare const schema_MapProvider: typeof MapProvider;
|
|
128
|
+
type schema_SchemaHandler = SchemaHandler;
|
|
129
|
+
type schema_SchemaNode = SchemaNode;
|
|
130
|
+
type schema_SchemaProvider = SchemaProvider;
|
|
131
|
+
declare const schema_check: typeof check;
|
|
132
|
+
declare const schema_validate: typeof validate;
|
|
133
|
+
declare const schema_validateSchema: typeof validateSchema;
|
|
134
|
+
declare const schema_wrap: typeof wrap;
|
|
135
|
+
declare namespace schema {
|
|
136
|
+
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 };
|
|
137
|
+
}
|
|
174
138
|
|
|
175
|
-
export { type AnnotateOptions, BabelQueueError,
|
|
139
|
+
export { type AnnotateOptions, BabelQueueError, Envelope, InvalidPayloadError, type SchemaNode, type SchemaProvider, UnknownUrnError, UnknownUrnStrategy, annotate, deadLetter, schema };
|
package/dist/index.js
CHANGED
|
@@ -1,134 +1,12 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
};
|
|
6
|
-
|
|
7
|
-
// src/codec.ts
|
|
8
|
-
import { randomUUID } from "crypto";
|
|
9
|
-
|
|
10
|
-
// src/errors.ts
|
|
11
|
-
var BabelQueueError = class extends Error {
|
|
12
|
-
constructor(message) {
|
|
13
|
-
super(message);
|
|
14
|
-
this.name = "BabelQueueError";
|
|
15
|
-
}
|
|
16
|
-
};
|
|
17
|
-
var UnknownUrnError = class extends BabelQueueError {
|
|
18
|
-
constructor(urn) {
|
|
19
|
-
super(`No handler is mapped for the message URN "${urn}".`);
|
|
20
|
-
this.name = "UnknownUrnError";
|
|
21
|
-
}
|
|
22
|
-
};
|
|
23
|
-
|
|
24
|
-
// src/codec.ts
|
|
25
|
-
var SCHEMA_VERSION = 1;
|
|
26
|
-
var SOURCE_LANG = "node";
|
|
27
|
-
var EnvelopeCodec = {
|
|
1
|
+
import {
|
|
2
|
+
BabelQueueError,
|
|
3
|
+
EnvelopeCodec,
|
|
4
|
+
InvalidPayloadError,
|
|
28
5
|
SCHEMA_VERSION,
|
|
29
6
|
SOURCE_LANG,
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
* Throws {@link BabelQueueError} when the URN is blank.
|
|
34
|
-
*/
|
|
35
|
-
make(urn, data, options = {}) {
|
|
36
|
-
const resolvedUrn = (urn ?? "").trim();
|
|
37
|
-
if (resolvedUrn === "") {
|
|
38
|
-
throw new BabelQueueError(
|
|
39
|
-
"A polyglot message must expose a stable, non-empty URN so consumers can identify it without any class name."
|
|
40
|
-
);
|
|
41
|
-
}
|
|
42
|
-
const traceId = (options.traceId ?? "").trim() || randomUUID();
|
|
43
|
-
return {
|
|
44
|
-
job: resolvedUrn,
|
|
45
|
-
trace_id: traceId,
|
|
46
|
-
data: { ...data },
|
|
47
|
-
meta: {
|
|
48
|
-
id: randomUUID(),
|
|
49
|
-
queue: options.queue ?? "default",
|
|
50
|
-
lang: SOURCE_LANG,
|
|
51
|
-
schema_version: SCHEMA_VERSION,
|
|
52
|
-
created_at: Date.now()
|
|
53
|
-
},
|
|
54
|
-
attempts: 0
|
|
55
|
-
};
|
|
56
|
-
},
|
|
57
|
-
/**
|
|
58
|
-
* Build the envelope from a {@link PolyglotMessage}. If the message also
|
|
59
|
-
* implements {@link HasTraceId} and returns a non-empty value, that trace id is
|
|
60
|
-
* reused.
|
|
61
|
-
*/
|
|
62
|
-
fromMessage(message, queue = "default") {
|
|
63
|
-
const traceId = typeof message.getBabelTraceId === "function" ? message.getBabelTraceId() ?? void 0 : void 0;
|
|
64
|
-
return EnvelopeCodec.make(message.getBabelUrn(), message.toPayload(), {
|
|
65
|
-
queue,
|
|
66
|
-
traceId
|
|
67
|
-
});
|
|
68
|
-
},
|
|
69
|
-
/**
|
|
70
|
-
* Encode the envelope as compact UTF-8 JSON. `JSON.stringify` already emits the
|
|
71
|
-
* canonical form — no spaces, and slashes/unicode/HTML left unescaped — matching
|
|
72
|
-
* the other SDK cores.
|
|
73
|
-
*/
|
|
74
|
-
encode(envelope) {
|
|
75
|
-
return JSON.stringify(envelope);
|
|
76
|
-
},
|
|
77
|
-
/**
|
|
78
|
-
* Parse a raw JSON body. Returns `{}` for malformed or non-object input (call
|
|
79
|
-
* {@link EnvelopeCodec.accepts} before trusting it). Resolves the `urn` inbound
|
|
80
|
-
* alias into `job`.
|
|
81
|
-
*/
|
|
82
|
-
decode(raw) {
|
|
83
|
-
let parsed;
|
|
84
|
-
try {
|
|
85
|
-
parsed = JSON.parse(raw);
|
|
86
|
-
} catch {
|
|
87
|
-
return {};
|
|
88
|
-
}
|
|
89
|
-
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
90
|
-
return {};
|
|
91
|
-
}
|
|
92
|
-
const envelope = parsed;
|
|
93
|
-
if (!envelope.job && typeof envelope.urn === "string") {
|
|
94
|
-
envelope.job = envelope.urn;
|
|
95
|
-
}
|
|
96
|
-
return envelope;
|
|
97
|
-
},
|
|
98
|
-
/** The message URN — canonical `job`, with `urn` accepted as an alias. */
|
|
99
|
-
urn(envelope) {
|
|
100
|
-
const value = envelope?.job ?? envelope?.urn ?? "";
|
|
101
|
-
return typeof value === "string" ? value.trim() : "";
|
|
102
|
-
},
|
|
103
|
-
/**
|
|
104
|
-
* Whether a consumer should accept this envelope. Rejects a missing URN, an
|
|
105
|
-
* unsupported `meta.schema_version`, a non-object `data`, a non-integer
|
|
106
|
-
* `attempts`, or a blank `trace_id` — the consumer-side counterpart to the
|
|
107
|
-
* producer JSON Schema. Acts as a type guard that narrows to {@link Envelope}.
|
|
108
|
-
*/
|
|
109
|
-
accepts(envelope) {
|
|
110
|
-
if (EnvelopeCodec.urn(envelope) === "") {
|
|
111
|
-
return false;
|
|
112
|
-
}
|
|
113
|
-
const meta = envelope.meta;
|
|
114
|
-
if (meta === null || typeof meta !== "object" || meta.schema_version !== SCHEMA_VERSION) {
|
|
115
|
-
return false;
|
|
116
|
-
}
|
|
117
|
-
const data = envelope.data;
|
|
118
|
-
if (data === null || typeof data !== "object" || Array.isArray(data)) {
|
|
119
|
-
return false;
|
|
120
|
-
}
|
|
121
|
-
const attempts = envelope.attempts;
|
|
122
|
-
if (typeof attempts !== "number" || !Number.isInteger(attempts)) {
|
|
123
|
-
return false;
|
|
124
|
-
}
|
|
125
|
-
const traceId = envelope.trace_id;
|
|
126
|
-
if (typeof traceId !== "string" || traceId.trim() === "") {
|
|
127
|
-
return false;
|
|
128
|
-
}
|
|
129
|
-
return true;
|
|
130
|
-
}
|
|
131
|
-
};
|
|
7
|
+
UnknownUrnError,
|
|
8
|
+
__export
|
|
9
|
+
} from "./chunk-7FUZ3LYT.js";
|
|
132
10
|
|
|
133
11
|
// src/deadLetter.ts
|
|
134
12
|
var deadLetter_exports = {};
|
|
@@ -159,14 +37,203 @@ var UnknownUrnStrategy = {
|
|
|
159
37
|
/** Route to the dead-letter queue. */
|
|
160
38
|
DEAD_LETTER: "dead_letter"
|
|
161
39
|
};
|
|
40
|
+
|
|
41
|
+
// src/idempotency.ts
|
|
42
|
+
var InMemoryStore = class {
|
|
43
|
+
entries = /* @__PURE__ */ new Set();
|
|
44
|
+
seen(messageId) {
|
|
45
|
+
return this.entries.has(messageId);
|
|
46
|
+
}
|
|
47
|
+
remember(messageId) {
|
|
48
|
+
this.entries.add(messageId);
|
|
49
|
+
}
|
|
50
|
+
forget(messageId) {
|
|
51
|
+
this.entries.delete(messageId);
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
function Wrap(store, handler) {
|
|
55
|
+
return async (env) => {
|
|
56
|
+
const id = env.meta.id;
|
|
57
|
+
if (!id) {
|
|
58
|
+
await handler(env);
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
if (await store.seen(id)) {
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
await handler(env);
|
|
65
|
+
await store.remember(id);
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// src/schema.ts
|
|
70
|
+
var schema_exports = {};
|
|
71
|
+
__export(schema_exports, {
|
|
72
|
+
MapProvider: () => MapProvider,
|
|
73
|
+
check: () => check,
|
|
74
|
+
validate: () => validate,
|
|
75
|
+
validateSchema: () => validateSchema,
|
|
76
|
+
wrap: () => wrap
|
|
77
|
+
});
|
|
78
|
+
var MapProvider = class _MapProvider {
|
|
79
|
+
schemas;
|
|
80
|
+
constructor(schemas) {
|
|
81
|
+
this.schemas = new Map(Object.entries(schemas));
|
|
82
|
+
}
|
|
83
|
+
/** Build a provider from URN -> raw JSON Schema strings, parsing each. */
|
|
84
|
+
static fromJson(raw) {
|
|
85
|
+
const schemas = {};
|
|
86
|
+
for (const [urn, body] of Object.entries(raw)) {
|
|
87
|
+
const decoded = JSON.parse(body);
|
|
88
|
+
if (typeof decoded !== "object" || decoded === null || Array.isArray(decoded)) {
|
|
89
|
+
throw new Error(`schema: invalid JSON schema for "${urn}"`);
|
|
90
|
+
}
|
|
91
|
+
schemas[urn] = decoded;
|
|
92
|
+
}
|
|
93
|
+
return new _MapProvider(schemas);
|
|
94
|
+
}
|
|
95
|
+
schemaFor(urn) {
|
|
96
|
+
return this.schemas.get(urn);
|
|
97
|
+
}
|
|
98
|
+
};
|
|
99
|
+
async function check(provider, urn, data) {
|
|
100
|
+
const schemaNode = await provider.schemaFor(urn);
|
|
101
|
+
if (!schemaNode) {
|
|
102
|
+
return null;
|
|
103
|
+
}
|
|
104
|
+
return validateSchema(schemaNode, data);
|
|
105
|
+
}
|
|
106
|
+
async function validate(provider, urn, data) {
|
|
107
|
+
const violation2 = await check(provider, urn, data);
|
|
108
|
+
if (violation2 !== null) {
|
|
109
|
+
throw new InvalidPayloadError(urn, violation2);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
function wrap(provider, handler) {
|
|
113
|
+
return async (env) => {
|
|
114
|
+
await validate(provider, env.job, env.data);
|
|
115
|
+
await handler(env);
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
function validateSchema(schema, value, path = "") {
|
|
119
|
+
if ("const" in schema && !equal(value, schema.const)) {
|
|
120
|
+
return violation(path, "wrong_const");
|
|
121
|
+
}
|
|
122
|
+
const enumValues = schema.enum;
|
|
123
|
+
if (Array.isArray(enumValues) && !enumValues.some((item) => equal(value, item))) {
|
|
124
|
+
return violation(path, "not_in_enum");
|
|
125
|
+
}
|
|
126
|
+
const type = typeof schema.type === "string" ? schema.type : "";
|
|
127
|
+
switch (type) {
|
|
128
|
+
case "object":
|
|
129
|
+
return checkObject(schema, value, path);
|
|
130
|
+
case "array":
|
|
131
|
+
return checkArray(schema, value, path);
|
|
132
|
+
case "string": {
|
|
133
|
+
if (typeof value !== "string") {
|
|
134
|
+
return violation(path, "not_a_string");
|
|
135
|
+
}
|
|
136
|
+
const minLength = schema.minLength;
|
|
137
|
+
if (typeof minLength === "number" && value.length < minLength) {
|
|
138
|
+
return violation(path, "below_min_length");
|
|
139
|
+
}
|
|
140
|
+
return null;
|
|
141
|
+
}
|
|
142
|
+
case "integer":
|
|
143
|
+
if (!isInteger(value)) {
|
|
144
|
+
return violation(path, "not_an_integer");
|
|
145
|
+
}
|
|
146
|
+
return checkMinimum(schema, value, path);
|
|
147
|
+
case "number":
|
|
148
|
+
if (typeof value !== "number") {
|
|
149
|
+
return violation(path, "not_a_number");
|
|
150
|
+
}
|
|
151
|
+
return checkMinimum(schema, value, path);
|
|
152
|
+
case "boolean":
|
|
153
|
+
return typeof value === "boolean" ? null : violation(path, "not_a_boolean");
|
|
154
|
+
case "null":
|
|
155
|
+
return value === null ? null : violation(path, "not_null");
|
|
156
|
+
default:
|
|
157
|
+
return null;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
function checkObject(schema, value, path) {
|
|
161
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
162
|
+
return violation(path, "not_an_object");
|
|
163
|
+
}
|
|
164
|
+
const obj = value;
|
|
165
|
+
const required = schema.required;
|
|
166
|
+
if (Array.isArray(required)) {
|
|
167
|
+
for (const key of required) {
|
|
168
|
+
if (typeof key === "string" && !(key in obj)) {
|
|
169
|
+
return violation(join(path, key), "missing_required");
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
const properties = typeof schema.properties === "object" && schema.properties !== null ? schema.properties : {};
|
|
174
|
+
const additionalAllowed = schema.additionalProperties !== false;
|
|
175
|
+
for (const [name, item] of Object.entries(obj)) {
|
|
176
|
+
const propSchema = properties[name];
|
|
177
|
+
if (typeof propSchema === "object" && propSchema !== null) {
|
|
178
|
+
const found = validateSchema(propSchema, item, join(path, name));
|
|
179
|
+
if (found !== null) {
|
|
180
|
+
return found;
|
|
181
|
+
}
|
|
182
|
+
continue;
|
|
183
|
+
}
|
|
184
|
+
if (!additionalAllowed) {
|
|
185
|
+
return violation(join(path, name), "additional_not_allowed");
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
return null;
|
|
189
|
+
}
|
|
190
|
+
function checkArray(schema, value, path) {
|
|
191
|
+
if (!Array.isArray(value)) {
|
|
192
|
+
return violation(path, "not_an_array");
|
|
193
|
+
}
|
|
194
|
+
const items = schema.items;
|
|
195
|
+
if (typeof items !== "object" || items === null) {
|
|
196
|
+
return null;
|
|
197
|
+
}
|
|
198
|
+
for (let i = 0; i < value.length; i++) {
|
|
199
|
+
const found = validateSchema(items, value[i], `${path}[${i}]`);
|
|
200
|
+
if (found !== null) {
|
|
201
|
+
return found;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
return null;
|
|
205
|
+
}
|
|
206
|
+
function checkMinimum(schema, value, path) {
|
|
207
|
+
const minimum = schema.minimum;
|
|
208
|
+
if (typeof minimum === "number" && value < minimum) {
|
|
209
|
+
return violation(path, "below_minimum");
|
|
210
|
+
}
|
|
211
|
+
return null;
|
|
212
|
+
}
|
|
213
|
+
function isInteger(value) {
|
|
214
|
+
return typeof value === "number" && Number.isInteger(value);
|
|
215
|
+
}
|
|
216
|
+
function equal(a, b) {
|
|
217
|
+
return JSON.stringify(a) === JSON.stringify(b);
|
|
218
|
+
}
|
|
219
|
+
function violation(path, reason) {
|
|
220
|
+
return `${path === "" ? "<root>" : path}: ${reason}`;
|
|
221
|
+
}
|
|
222
|
+
function join(path, key) {
|
|
223
|
+
return path === "" ? key : `${path}.${key}`;
|
|
224
|
+
}
|
|
162
225
|
export {
|
|
163
226
|
BabelQueueError,
|
|
164
227
|
EnvelopeCodec,
|
|
228
|
+
InMemoryStore,
|
|
229
|
+
InvalidPayloadError,
|
|
165
230
|
SCHEMA_VERSION,
|
|
166
231
|
SOURCE_LANG,
|
|
167
232
|
UnknownUrnError,
|
|
168
233
|
UnknownUrnStrategy,
|
|
234
|
+
Wrap,
|
|
169
235
|
annotate,
|
|
170
|
-
deadLetter_exports as deadLetter
|
|
236
|
+
deadLetter_exports as deadLetter,
|
|
237
|
+
schema_exports as schema
|
|
171
238
|
};
|
|
172
239
|
//# 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/deadLetter.ts","../src/routing.ts","../src/idempotency.ts","../src/schema.ts"],"sourcesContent":["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":";;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAkBO,SAAS,SACd,UACA,QACA,eACA,UAA2B,CAAC,GAClB;AACV,QAAM,aAAyB;AAAA,IAC7B;AAAA,IACA,OAAO,QAAQ,SAAS;AAAA,IACxB,WAAW,QAAQ,aAAa;AAAA,IAChC,WAAW,KAAK,IAAI;AAAA,IACpB,gBAAgB;AAAA,IAChB,UAAU,QAAQ,YAAY,SAAS,YAAY;AAAA,IACnD,MAAM;AAAA,EACR;AAEA,SAAO,EAAE,GAAG,UAAU,aAAa,WAAW;AAChD;;;AC/BO,IAAM,qBAAqB;AAAA;AAAA,EAEhC,MAAM;AAAA;AAAA,EAEN,QAAQ;AAAA;AAAA,EAER,SAAS;AAAA;AAAA,EAET,aAAa;AACf;;;AC2BO,IAAM,gBAAN,MAAqC;AAAA,EACzB,UAAU,oBAAI,IAAY;AAAA,EAE3C,KAAK,WAA4B;AAC/B,WAAO,KAAK,QAAQ,IAAI,SAAS;AAAA,EACnC;AAAA,EAEA,SAAS,WAAyB;AAChC,SAAK,QAAQ,IAAI,SAAS;AAAA,EAC5B;AAAA,EAEA,OAAO,WAAyB;AAC9B,SAAK,QAAQ,OAAO,SAAS;AAAA,EAC/B;AACF;AAOO,SAAS,KAAK,OAAc,SAA2B;AAC5D,SAAO,OAAO,QAAiC;AAC7C,UAAM,KAAK,IAAI,KAAK;AAGpB,QAAI,CAAC,IAAI;AACP,YAAM,QAAQ,GAAG;AACjB;AAAA,IACF;AAGA,QAAI,MAAM,MAAM,KAAK,EAAE,GAAG;AACxB;AAAA,IACF;AAGA,UAAM,QAAQ,GAAG;AACjB,UAAM,MAAM,SAAS,EAAE;AAAA,EACzB;AACF;;;AChFA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AA0CO,IAAM,cAAN,MAAM,aAAsC;AAAA,EAChC;AAAA,EAEjB,YAAY,SAAqC;AAC/C,SAAK,UAAU,IAAI,IAAI,OAAO,QAAQ,OAAO,CAAC;AAAA,EAChD;AAAA;AAAA,EAGA,OAAO,SAAS,KAA0C;AACxD,UAAM,UAAsC,CAAC;AAC7C,eAAW,CAAC,KAAK,IAAI,KAAK,OAAO,QAAQ,GAAG,GAAG;AAC7C,YAAM,UAAmB,KAAK,MAAM,IAAI;AACxC,UAAI,OAAO,YAAY,YAAY,YAAY,QAAQ,MAAM,QAAQ,OAAO,GAAG;AAC7E,cAAM,IAAI,MAAM,oCAAoC,GAAG,GAAG;AAAA,MAC5D;AACA,cAAQ,GAAG,IAAI;AAAA,IACjB;AACA,WAAO,IAAI,aAAY,OAAO;AAAA,EAChC;AAAA,EAEA,UAAU,KAAqC;AAC7C,WAAO,KAAK,QAAQ,IAAI,GAAG;AAAA,EAC7B;AACF;AAMA,eAAsB,MACpB,UACA,KACA,MACwB;AACxB,QAAM,aAAa,MAAM,SAAS,UAAU,GAAG;AAC/C,MAAI,CAAC,YAAY;AACf,WAAO;AAAA,EACT;AACA,SAAO,eAAe,YAAY,IAAI;AACxC;AAMA,eAAsB,SACpB,UACA,KACA,MACe;AACf,QAAMA,aAAY,MAAM,MAAM,UAAU,KAAK,IAAI;AACjD,MAAIA,eAAc,MAAM;AACtB,UAAM,IAAI,oBAAoB,KAAKA,UAAS;AAAA,EAC9C;AACF;AASO,SAAS,KAAK,UAA0B,SAAuC;AACpF,SAAO,OAAO,QAAiC;AAC7C,UAAM,SAAS,UAAU,IAAI,KAAK,IAAI,IAAI;AAC1C,UAAM,QAAQ,GAAG;AAAA,EACnB;AACF;AAGO,SAAS,eAAe,QAAoB,OAAgB,OAAO,IAAmB;AAC3F,MAAI,WAAW,UAAU,CAAC,MAAM,OAAO,OAAO,KAAK,GAAG;AACpD,WAAO,UAAU,MAAM,aAAa;AAAA,EACtC;AACA,QAAM,aAAa,OAAO;AAC1B,MAAI,MAAM,QAAQ,UAAU,KAAK,CAAC,WAAW,KAAK,CAAC,SAAS,MAAM,OAAO,IAAI,CAAC,GAAG;AAC/E,WAAO,UAAU,MAAM,aAAa;AAAA,EACtC;AAEA,QAAM,OAAO,OAAO,OAAO,SAAS,WAAW,OAAO,OAAO;AAC7D,UAAQ,MAAM;AAAA,IACZ,KAAK;AACH,aAAO,YAAY,QAAQ,OAAO,IAAI;AAAA,IACxC,KAAK;AACH,aAAO,WAAW,QAAQ,OAAO,IAAI;AAAA,IACvC,KAAK,UAAU;AACb,UAAI,OAAO,UAAU,UAAU;AAC7B,eAAO,UAAU,MAAM,cAAc;AAAA,MACvC;AACA,YAAM,YAAY,OAAO;AACzB,UAAI,OAAO,cAAc,YAAY,MAAM,SAAS,WAAW;AAC7D,eAAO,UAAU,MAAM,kBAAkB;AAAA,MAC3C;AACA,aAAO;AAAA,IACT;AAAA,IACA,KAAK;AACH,UAAI,CAAC,UAAU,KAAK,GAAG;AACrB,eAAO,UAAU,MAAM,gBAAgB;AAAA,MACzC;AACA,aAAO,aAAa,QAAQ,OAAO,IAAI;AAAA,IACzC,KAAK;AACH,UAAI,OAAO,UAAU,UAAU;AAC7B,eAAO,UAAU,MAAM,cAAc;AAAA,MACvC;AACA,aAAO,aAAa,QAAQ,OAAO,IAAI;AAAA,IACzC,KAAK;AACH,aAAO,OAAO,UAAU,YAAY,OAAO,UAAU,MAAM,eAAe;AAAA,IAC5E,KAAK;AACH,aAAO,UAAU,OAAO,OAAO,UAAU,MAAM,UAAU;AAAA,IAC3D;AACE,aAAO;AAAA,EACX;AACF;AAEA,SAAS,YAAY,QAAoB,OAAgB,MAA6B;AACpF,MAAI,OAAO,UAAU,YAAY,UAAU,QAAQ,MAAM,QAAQ,KAAK,GAAG;AACvE,WAAO,UAAU,MAAM,eAAe;AAAA,EACxC;AACA,QAAM,MAAM;AAEZ,QAAM,WAAW,OAAO;AACxB,MAAI,MAAM,QAAQ,QAAQ,GAAG;AAC3B,eAAW,OAAO,UAAU;AAC1B,UAAI,OAAO,QAAQ,YAAY,EAAE,OAAO,MAAM;AAC5C,eAAO,UAAU,KAAK,MAAM,GAAG,GAAG,kBAAkB;AAAA,MACtD;AAAA,IACF;AAAA,EACF;AAEA,QAAM,aACJ,OAAO,OAAO,eAAe,YAAY,OAAO,eAAe,OAC1D,OAAO,aACR,CAAC;AACP,QAAM,oBAAoB,OAAO,yBAAyB;AAE1D,aAAW,CAAC,MAAM,IAAI,KAAK,OAAO,QAAQ,GAAG,GAAG;AAC9C,UAAM,aAAa,WAAW,IAAI;AAClC,QAAI,OAAO,eAAe,YAAY,eAAe,MAAM;AACzD,YAAM,QAAQ,eAAe,YAA0B,MAAM,KAAK,MAAM,IAAI,CAAC;AAC7E,UAAI,UAAU,MAAM;AAClB,eAAO;AAAA,MACT;AACA;AAAA,IACF;AACA,QAAI,CAAC,mBAAmB;AACtB,aAAO,UAAU,KAAK,MAAM,IAAI,GAAG,wBAAwB;AAAA,IAC7D;AAAA,EACF;AAEA,SAAO;AACT;AAEA,SAAS,WAAW,QAAoB,OAAgB,MAA6B;AACnF,MAAI,CAAC,MAAM,QAAQ,KAAK,GAAG;AACzB,WAAO,UAAU,MAAM,cAAc;AAAA,EACvC;AACA,QAAM,QAAQ,OAAO;AACrB,MAAI,OAAO,UAAU,YAAY,UAAU,MAAM;AAC/C,WAAO;AAAA,EACT;AACA,WAAS,IAAI,GAAG,IAAI,MAAM,QAAQ,KAAK;AACrC,UAAM,QAAQ,eAAe,OAAqB,MAAM,CAAC,GAAG,GAAG,IAAI,IAAI,CAAC,GAAG;AAC3E,QAAI,UAAU,MAAM;AAClB,aAAO;AAAA,IACT;AAAA,EACF;AACA,SAAO;AACT;AAEA,SAAS,aAAa,QAAoB,OAAe,MAA6B;AACpF,QAAM,UAAU,OAAO;AACvB,MAAI,OAAO,YAAY,YAAY,QAAQ,SAAS;AAClD,WAAO,UAAU,MAAM,eAAe;AAAA,EACxC;AACA,SAAO;AACT;AAGA,SAAS,UAAU,OAAiC;AAClD,SAAO,OAAO,UAAU,YAAY,OAAO,UAAU,KAAK;AAC5D;AAIA,SAAS,MAAM,GAAY,GAAqB;AAC9C,SAAO,KAAK,UAAU,CAAC,MAAM,KAAK,UAAU,CAAC;AAC/C;AAEA,SAAS,UAAU,MAAc,QAAwB;AACvD,SAAO,GAAG,SAAS,KAAK,WAAW,IAAI,KAAK,MAAM;AACpD;AAEA,SAAS,KAAK,MAAc,KAAqB;AAC/C,SAAO,SAAS,KAAK,MAAM,GAAG,IAAI,IAAI,GAAG;AAC3C;","names":["violation"]}
|