mercury-composable 4.12.1

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.
@@ -0,0 +1,227 @@
1
+ /**
2
+ * EventEnvelope and the standard wire format codec.
3
+ *
4
+ * Implements the language-neutral standard format from the Mercury Composable
5
+ * "Event Envelope Wire Format" reference: one MsgPack map with descriptive
6
+ * string keys, no MsgPack extension types. Optional fields are omitted when
7
+ * unset; absent and nil are equivalent; unknown keys are ignored; timestamps
8
+ * travel as ISO-8601 UTC strings with millisecond precision.
9
+ *
10
+ * Integer handling: 64-bit integers are decoded exactly — values beyond
11
+ * Number.MAX_SAFE_INTEGER surface as BigInt; smaller ones as number. BigInt
12
+ * values encode as int64.
13
+ *
14
+ * The classic compact format (single-character map keys) is detected from the
15
+ * first map key and rejected with CompactFormatError.
16
+ */
17
+ import { Decoder, Encoder } from '@msgpack/msgpack';
18
+ import { randomUUID } from 'node:crypto';
19
+ import { CompactFormatError } from './exceptions.js';
20
+ const encoder = new Encoder({ useBigInt64: true, ignoreUndefined: true });
21
+ const decoder = new Decoder({ useBigInt64: true });
22
+ /** ISO-8601 UTC with millisecond precision, e.g. 2026-07-21T12:00:00.000Z */
23
+ export function isoUtc(date) {
24
+ return (date ?? new Date()).toISOString();
25
+ }
26
+ /** Normalize a payload for the wire: Date -> ISO string, safe BigInt -> number. */
27
+ function sanitize(value) {
28
+ if (value instanceof Date)
29
+ return isoUtc(value);
30
+ if (typeof value === 'bigint')
31
+ return value; // encodes exactly as int64
32
+ if (Array.isArray(value))
33
+ return value.map(sanitize);
34
+ if (value instanceof Uint8Array)
35
+ return value;
36
+ if (value !== null && typeof value === 'object') {
37
+ const out = {};
38
+ for (const [k, v] of Object.entries(value)) {
39
+ if (v !== undefined)
40
+ out[k] = sanitize(v);
41
+ }
42
+ return out;
43
+ }
44
+ return value;
45
+ }
46
+ /** Decoded values: BigInt within the safe range becomes number. */
47
+ function normalize(value) {
48
+ if (typeof value === 'bigint') {
49
+ return value <= BigInt(Number.MAX_SAFE_INTEGER) && value >= -BigInt(Number.MAX_SAFE_INTEGER)
50
+ ? Number(value) : value;
51
+ }
52
+ if (Array.isArray(value))
53
+ return value.map(normalize);
54
+ if (value instanceof Uint8Array)
55
+ return value;
56
+ if (value !== null && typeof value === 'object') {
57
+ const out = {};
58
+ for (const [k, v] of Object.entries(value)) {
59
+ out[k] = normalize(v);
60
+ }
61
+ return out;
62
+ }
63
+ return value;
64
+ }
65
+ /** Render a scalar as text: primitives via String, structures via JSON. */
66
+ export function asText(value) {
67
+ if (typeof value === 'string')
68
+ return value;
69
+ if (typeof value === 'number' || typeof value === 'bigint'
70
+ || typeof value === 'boolean' || typeof value === 'symbol') {
71
+ // positively narrowed to primitives - negating typeof on 'unknown' would
72
+ // widen to '{}' and re-admit objects (the S6551 trap)
73
+ return String(value);
74
+ }
75
+ if (typeof value === 'function')
76
+ return value.toString();
77
+ return value === undefined ? 'undefined' : JSON.stringify(value);
78
+ }
79
+ function asStringMap(value) {
80
+ const result = {};
81
+ if (value !== null && typeof value === 'object' && !Array.isArray(value)) {
82
+ for (const [k, v] of Object.entries(value)) {
83
+ result[k] = asText(v);
84
+ }
85
+ }
86
+ return result;
87
+ }
88
+ export class EventEnvelope {
89
+ id = randomUUID().replaceAll('-', '');
90
+ to;
91
+ sender; // wire field "from"
92
+ replyTo;
93
+ cid;
94
+ traceId;
95
+ tracePath;
96
+ spanId;
97
+ status; // undefined encodes as absent (default 200)
98
+ headers = {};
99
+ body = undefined;
100
+ execTime;
101
+ roundTrip;
102
+ tags = {};
103
+ annotations = {};
104
+ stack;
105
+ objType;
106
+ exception; // language-native, opaque here
107
+ constructor(to, body, headers) {
108
+ this.to = to;
109
+ this.body = body;
110
+ if (headers)
111
+ this.headers = { ...headers };
112
+ }
113
+ // fluent helpers mirroring the engine API vocabulary
114
+ setTo(route) { this.to = route; return this; }
115
+ setFrom(route) { this.sender = route; return this; }
116
+ setHeader(key, value) { this.headers[key] = String(value); return this; }
117
+ setBody(body) { this.body = body; return this; }
118
+ setStatus(status) { this.status = Math.trunc(status); return this; }
119
+ setCorrelationId(cid) { this.cid = cid; return this; }
120
+ setTrace(traceId, tracePath) {
121
+ this.traceId = traceId;
122
+ this.tracePath = tracePath;
123
+ return this;
124
+ }
125
+ setSpanId(spanId) {
126
+ this.spanId = spanId;
127
+ return this;
128
+ }
129
+ setReplyTo(route) { this.replyTo = route; return this; }
130
+ getStatus() { return this.status ?? 200; }
131
+ hasError() { return this.getStatus() >= 400; }
132
+ toMap() {
133
+ const result = { id: this.id, headers: { ...this.headers } };
134
+ const optional = [
135
+ ['to', this.to], ['from', this.sender], ['reply_to', this.replyTo],
136
+ ['cid', this.cid], ['trace_id', this.traceId], ['trace_path', this.tracePath],
137
+ ['span_id', this.spanId], ['status', this.status], ['body', sanitize(this.body)],
138
+ ['exec_time', this.execTime], ['round_trip', this.roundTrip],
139
+ ['stack', this.stack], ['obj_type', this.objType], ['exception', this.exception]
140
+ ];
141
+ for (const [key, value] of optional) {
142
+ if (value !== undefined && value !== null)
143
+ result[key] = value;
144
+ }
145
+ if (Object.keys(this.tags).length)
146
+ result.tags = { ...this.tags };
147
+ if (Object.keys(this.annotations).length)
148
+ result.annotations = sanitize(this.annotations);
149
+ return result;
150
+ }
151
+ toBytes() {
152
+ // fresh copy: the encoder reuses an internal buffer, and a concrete
153
+ // ArrayBuffer-backed Uint8Array satisfies fetch's BodyInit typing
154
+ return new Uint8Array(encoder.encode(this.toMap()));
155
+ }
156
+ /** The optional wire fields that arrive as strings (wire key -> assignment). */
157
+ static copyStringFields(data, event) {
158
+ if (typeof data.to === 'string')
159
+ event.to = data.to;
160
+ if (typeof data.from === 'string')
161
+ event.sender = data.from;
162
+ if (typeof data.reply_to === 'string')
163
+ event.replyTo = data.reply_to;
164
+ if (typeof data.cid === 'string')
165
+ event.cid = data.cid;
166
+ if (typeof data.trace_id === 'string')
167
+ event.traceId = data.trace_id;
168
+ if (typeof data.trace_path === 'string')
169
+ event.tracePath = data.trace_path;
170
+ if (typeof data.span_id === 'string')
171
+ event.spanId = data.span_id;
172
+ if (typeof data.stack === 'string')
173
+ event.stack = data.stack;
174
+ if (typeof data.obj_type === 'string')
175
+ event.objType = data.obj_type;
176
+ }
177
+ /** The optional numeric fields (absent and nil are equivalent on the wire). */
178
+ static copyNumericFields(data, event) {
179
+ if (data.status !== undefined && data.status !== null)
180
+ event.status = Number(data.status);
181
+ if (data.exec_time !== undefined && data.exec_time !== null) {
182
+ event.execTime = Number(data.exec_time);
183
+ }
184
+ if (data.round_trip !== undefined && data.round_trip !== null) {
185
+ event.roundTrip = Number(data.round_trip);
186
+ }
187
+ }
188
+ static fromMap(data) {
189
+ const event = new EventEnvelope();
190
+ if (data.id !== undefined && data.id !== null)
191
+ event.id = asText(data.id);
192
+ EventEnvelope.copyStringFields(data, event);
193
+ EventEnvelope.copyNumericFields(data, event);
194
+ event.headers = asStringMap(data.headers);
195
+ if (data.body !== undefined)
196
+ event.body = normalize(data.body);
197
+ if (data.tags !== undefined && data.tags !== null)
198
+ event.tags = asStringMap(data.tags);
199
+ if (data.annotations !== undefined && data.annotations !== null) {
200
+ event.annotations = normalize(data.annotations);
201
+ }
202
+ if (data.exception instanceof Uint8Array)
203
+ event.exception = data.exception;
204
+ return event;
205
+ }
206
+ static fromBytes(data) {
207
+ let decoded;
208
+ try {
209
+ decoded = decoder.decode(data);
210
+ }
211
+ catch (e) {
212
+ throw new Error(`Unable to decode event envelope - ${e.message}`);
213
+ }
214
+ if (decoded === null || typeof decoded !== 'object' || Array.isArray(decoded)) {
215
+ throw new Error('Unable to decode event envelope - not a MsgPack map');
216
+ }
217
+ const keys = Object.keys(decoded);
218
+ if (keys.length === 0) {
219
+ throw new Error('Unable to decode event envelope - empty map');
220
+ }
221
+ if (keys[0].length === 1) {
222
+ throw new CompactFormatError('Compact event envelope format is not supported - ' +
223
+ 'use the standard format (event.over.http.format=standard)');
224
+ }
225
+ return EventEnvelope.fromMap(decoded);
226
+ }
227
+ }
@@ -0,0 +1,120 @@
1
+ import { EventEnvelope } from './envelope.js';
2
+ import { FunctionRegistry } from './registry.js';
3
+ /** reserved envelope header (internal protocol, never on the HTTP wire) */
4
+ export declare const X_EVENT_STREAM = "x-event-stream";
5
+ /** optional companion on a data event: maps to the SSE "event:" field */
6
+ export declare const X_EVENT_NAME = "x-event-name";
7
+ /** marker vocabulary - deliberately the engines' ObjectStream vocabulary */
8
+ export declare const DATA = "data";
9
+ export declare const EOF = "eof";
10
+ export declare const EXCEPTION = "exception";
11
+ /**
12
+ * reserved SSE event name of the envelope-mode wire dialect: a frame with
13
+ * this name carries one base64-encoded serialized EventEnvelope
14
+ */
15
+ export declare const ENVELOPE = "envelope";
16
+ export declare const X_TTL = "x-ttl";
17
+ export declare const TEXT_EVENT_STREAM = "text/event-stream";
18
+ export declare const STREAM_CALLER_REQUIRED = "Streaming function requires a caller that accepts text/event-stream";
19
+ /** The x-event-stream marker (lowercased), or undefined for an unmarked envelope. */
20
+ export declare function streamSignal(event: EventEnvelope): string | undefined;
21
+ /** The x-event-name companion header (the SSE "event:" field), if any. */
22
+ export declare function streamEventName(event: EventEnvelope): string | undefined;
23
+ /** The message text of an unmarked error reply (objects render as JSON). */
24
+ export declare function errorText(body: unknown): string;
25
+ /** The standard error key-values: '{"type": "error", "status": n, "message": text}' */
26
+ export declare function errorBody(status: number, message: string): Record<string, unknown>;
27
+ /** An in-band exception envelope with the standard error body. */
28
+ export declare function exceptionEnvelope(status: number, message: string): EventEnvelope;
29
+ /** One SSE frame: optional "event:" line, one "data:" line per text line. */
30
+ export declare function sseFrame(eventName: string | undefined, text: string): Buffer;
31
+ /**
32
+ * One envelope-mode wire frame: the envelope serialized verbatim - with the
33
+ * host-internal addressing cleared, because the consuming relay rewrites
34
+ * addressing to the original caller - as base64 under the reserved name.
35
+ */
36
+ export declare function envelopeFrame(event: EventEnvelope): Buffer;
37
+ /**
38
+ * A data segment may ride a raw SSE frame only when the frame carries it
39
+ * losslessly: a 200 status, no custom envelope headers, a user event name
40
+ * clear of the reserved word, and a text (or empty) body without a carriage
41
+ * return - SSE normalizes line endings. Everything else takes the
42
+ * envelope-frame escape hatch.
43
+ */
44
+ export declare function rawStreamable(event: EventEnvelope): boolean;
45
+ /**
46
+ * One envelope-mode data frame: the first event always rides an envelope
47
+ * frame (it carries the head control); a losslessly raw-able text segment
48
+ * rides a raw frame; a bare no-op segment carries nothing.
49
+ */
50
+ export declare function dataFrame(event: EventEnvelope, firstFrame: boolean): Buffer;
51
+ /**
52
+ * SSE keep-alive comment interval in ms (`event.stream.keep.alive`,
53
+ * default 30s; 0 disables - the engines' config key).
54
+ */
55
+ export declare function keepAliveMs(): number;
56
+ /**
57
+ * Incremental SSE frame parser: byte-level line split (a newline is a single
58
+ * byte, so this is UTF-8 safe), one-leading-space value strip, comment/id/
59
+ * retry suppression, multi-line data joined per the SSE specification.
60
+ * Mirrors the engines' parsers.
61
+ */
62
+ export declare class SseParser {
63
+ private pending;
64
+ private dataLines;
65
+ private eventName;
66
+ /** Feed one body chunk; return the completed [event_name, data] events. */
67
+ feed(chunk: Uint8Array): Array<[string | undefined, string]>;
68
+ /**
69
+ * One SSE line: a blank line dispatches the pending event; a comment line
70
+ * (leading colon) is consumed, never forwarded; id, retry and unknown
71
+ * fields are ignored (SSE specification).
72
+ */
73
+ private onLine;
74
+ }
75
+ /**
76
+ * Producer helper for a multi-shot reply - the engines' exact API.
77
+ *
78
+ * Only an interceptor function can stream: it receives the raw envelope, so
79
+ * the caller-provided reply address travels the engines' way
80
+ * (EventStreamWriter.fromRequest(event) reads reply_to and the correlation
81
+ * id). Segments route to the LOCAL reply address through the primitive event
82
+ * bus - simple routing to a local function or reply sink, never across the
83
+ * wire (cross-wire replies ride the Event-over-HTTP SSE response, exactly as
84
+ * on the engines).
85
+ */
86
+ export declare class EventStreamWriter {
87
+ private readonly registry;
88
+ private readonly replyTo;
89
+ private readonly cid;
90
+ private firstStatus;
91
+ private firstContentType;
92
+ private firstTtlSeconds;
93
+ private headSent;
94
+ private isClosed;
95
+ constructor(replyTo: string | undefined, correlationId?: string, registry?: FunctionRegistry);
96
+ /**
97
+ * Create a writer from the incoming request envelope (the usual form for
98
+ * an interceptor function).
99
+ */
100
+ static fromRequest(event: EventEnvelope, registry?: FunctionRegistry): EventStreamWriter;
101
+ /**
102
+ * Optional head control carried by the first outgoing event: response
103
+ * status, content type, and an optional idle-allowance override in seconds
104
+ * between segments.
105
+ */
106
+ first(status: number, contentType: string, ttlSeconds?: number): this;
107
+ /** Send one `data` segment (text, bytes, object, array - any payload). */
108
+ write(segment: unknown): void;
109
+ /** Send one named segment - the name maps to the SSE "event:" field. */
110
+ writeNamed(eventName: string, segment: unknown): void;
111
+ /** Declare end of transmission, with optional trailing metadata. */
112
+ close(trailingMetadata?: unknown): void;
113
+ /** Declare an in-band failure and end the stream. */
114
+ fail(error: Error): void;
115
+ /** True when the stream has been closed or failed. */
116
+ get closed(): boolean;
117
+ private send;
118
+ private envelope;
119
+ private emit;
120
+ }