@relaybus/core 0.3.1 → 0.4.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.
@@ -1,4 +1,4 @@
1
- import { b as EventHeaders, E as EventEnvelope, h as Clock, L as LeaseReference, d as ErrorInfo, j as ClaimRequest, C as ClaimedDelivery, T as Transport, U as TransportMessageHandler, V as TransportSubscriptionHooks, p as CloseOptions, D as Duration } from '../transport-BlfzIBZD.js';
1
+ import { EventHeaders, EventEnvelope, Clock, LeaseReference, ErrorInfo, ClaimRequest, ClaimedDelivery, Transport, TransportMessageHandler, TransportSubscriptionHooks, CloseOptions, Duration } from '../index.js';
2
2
 
3
3
  /** Returns `value` when it is an integer of at least 1, else throws a {@link ConfigurationError} naming the option. */
4
4
  declare function assertPositiveInteger(value: number, name: string): number;
@@ -82,26 +82,60 @@ type WireField = BodyField | HeaderField;
82
82
  type FixedType = {
83
83
  readonly fixed: string;
84
84
  };
85
+ /**
86
+ * How `occurredAt` travels: RFC 3339 text (default), or whole epoch milliseconds or seconds, a
87
+ * number in the body and decimal text in a header. Epoch seconds drop the milliseconds.
88
+ */
89
+ type TimeFormat = 'rfc3339' | 'epoch-millis' | 'epoch-seconds';
90
+ /** Where the event time lives, and in which format. */
91
+ type TimeField<TField extends WireField> = TField & {
92
+ readonly format?: TimeFormat;
93
+ };
94
+ /** A JSON value written as it is. */
95
+ type JsonConstant = string | number | boolean | null | readonly JsonConstant[] | {
96
+ readonly [name: string]: JsonConstant;
97
+ };
98
+ /** A value written with every message: a constant, or derived from the event. */
99
+ type ExtraValue<TValue> = TValue | ((envelope: EventEnvelope) => TValue);
100
+ /** Extra message headers, by name. */
101
+ type ExtraHeaders = Readonly<Record<string, ExtraValue<string>>>;
102
+ /** Extra body values, by dotted path. */
103
+ type ExtraBody = Readonly<Record<string, ExtraValue<JsonConstant>>>;
85
104
  type MappingFields<TField extends WireField, TTransportId extends string> = {
86
105
  /** Where the event type lives, or the one type of the route. Required. */
87
106
  readonly type: TField | FixedType;
88
107
  /**
89
108
  * Where the event id lives, or the transport's own message identity (see the transport's
90
109
  * mapping type). Required, so the inbox dedup key is always a deliberate choice. A message
91
- * whose mapped id is missing is malformed.
110
+ * whose mapped id is missing is malformed, unless `whenMissing` names the transport's own id
111
+ * to use instead: `{ body: 'id', whenMissing: 'record-position' }`.
92
112
  */
93
- readonly id: TField | TTransportId;
113
+ readonly id: TField | TTransportId | (TField & {
114
+ readonly whenMissing: TTransportId;
115
+ });
94
116
  /** Ordering key. Default: the transport's own (the Kafka record key; none on RabbitMQ). */
95
117
  readonly key?: TField;
96
118
  /** Producing service. Default: the transport's own (Kafka topic; RabbitMQ `appId`, else the routing key). */
97
119
  readonly source?: TField;
98
- /** RFC 3339 time. Default: the transport's own (Kafka record timestamp; RabbitMQ `timestamp`, seconds). */
99
- readonly occurredAt?: TField;
120
+ /**
121
+ * Event time, RFC 3339 unless `format` says otherwise. Default: the transport's own (Kafka
122
+ * record timestamp; RabbitMQ `timestamp`, seconds).
123
+ */
124
+ readonly occurredAt?: TimeField<TField>;
100
125
  readonly correlationId?: TField;
101
126
  readonly causationId?: TField;
102
127
  /** Media type written with the message. Default `application/json`. Not checked on read. */
103
128
  readonly contentType?: string;
129
+ /**
130
+ * Wire names of event types, for a format whose type values are not relaybus event types or
131
+ * name them otherwise: `{ 'transfer.auto-dispatched.v1': 'Transfer Auto Dispatched' }`. A listed
132
+ * type is written as its wire name and read back from it; any other type travels as itself. One
133
+ * route then carries many types without a `fixed` type per transport. Not with a `fixed` type.
134
+ */
135
+ readonly wireTypes?: WireTypes;
104
136
  };
137
+ /** Event type to the name the wire uses for it; see `wireTypes` of a jsonEnvelope mapping. */
138
+ type WireTypes = Readonly<Record<string, string>>;
105
139
  /**
106
140
  * A JSON object that wraps the payload with event metadata, such as
107
141
  * `{ "eventName": ..., "messageId": ..., "data": {...}, "meta": {...} }`.
@@ -111,10 +145,37 @@ type BodyEnvelopeMapping<TTransportId extends string> = MappingFields<WireField,
111
145
  /** Body path of the payload. Required. */
112
146
  readonly payload: BodyField;
113
147
  /**
114
- * Event headers: a body path holding an object of strings, or `'message-headers'` (default),
115
- * every message header that no other field maps.
148
+ * Event headers: a body path holding an object of strings, `'message-headers'` (default),
149
+ * every message header that no other field maps, or `'top-level-fields'`, spread over the top
150
+ * level of the body: every top-level text field that no other value uses is read as one.
151
+ */
152
+ readonly headers?: BodyField | 'message-headers' | 'top-level-fields';
153
+ /**
154
+ * Values written with every message besides the mapped ones, such as a constant
155
+ * `provider: 'orders-api'` or a second copy of the time. They are never read; extra headers
156
+ * are left out of the event headers on read.
157
+ */
158
+ readonly extra?: {
159
+ readonly body?: ExtraBody;
160
+ readonly headers?: ExtraHeaders;
161
+ };
162
+ /**
163
+ * Top-level body fields to write first, in this order; the others follow in the order they
164
+ * are written. Only the bytes change, for formats compared byte for byte.
165
+ */
166
+ readonly bodyOrder?: readonly string[];
167
+ /**
168
+ * Keeps the top-level body fields that no other value uses (and that are not read as event
169
+ * headers) with every JSON type, in body order, as one object under `payloadField` of the
170
+ * payload: `{ payloadField: 'wire' }` reads `{ "data": { "a": 1 }, "attempt": 2 }` as the payload
171
+ * `{ "a": 1, "wire": { "attempt": 2 } }`, so the handler's schema validates them with the payload.
172
+ * Writing spreads that object back over the top level. Default: such fields are left out on read.
116
173
  */
117
- readonly headers?: BodyField | 'message-headers';
174
+ readonly otherFields?: OtherFields;
175
+ };
176
+ /** Where {@link BodyEnvelopeMapping.otherFields} keeps the unused top-level fields. */
177
+ type OtherFields = {
178
+ readonly payloadField: string;
118
179
  };
119
180
  /**
120
181
  * The body is the payload itself, as a plain JSON producer writes it; every other value lives in
@@ -124,6 +185,10 @@ type PayloadOnlyMapping<TTransportId extends string> = MappingFields<HeaderField
124
185
  readonly payload: 'whole-body';
125
186
  /** Event headers: every message header that no other field maps. */
126
187
  readonly headers?: 'message-headers';
188
+ /** Message headers written with every message; never read, and left out of the event headers. */
189
+ readonly extra?: {
190
+ readonly headers?: ExtraHeaders;
191
+ };
127
192
  };
128
193
  /** Where every envelope value lives in a JSON message. `type`, `id` and `payload` are required. */
129
194
  type JsonEnvelopeMapping<TTransportId extends string> = BodyEnvelopeMapping<TTransportId> | PayloadOnlyMapping<TTransportId>;
@@ -435,4 +500,5 @@ declare abstract class PositiveInteger {
435
500
  static of(value: number, name: string): number;
436
501
  }
437
502
 
438
- export { type BodyEnvelopeMapping, type BodyField, BoundedInteger, type CancelTimer, type CloudEventReaders, CompositeKey, type DecodedEventFields, DecodedEventReader, DeliveryLeaseIdentity, ExponentialBackoff, type FixedType, type HandedOutEntry, HandedOutLedger, type HeaderField, type IntegerBounds, type JsonEnvelopeFailures, type JsonEnvelopeFields, type JsonEnvelopeFormat, type JsonEnvelopeMapping, type JsonWireMessage, LaneLeaseTokenFormat, type LaneLeaseTokenLimits, type LaneLeaseTokenParts, type LeaseChange, type LeaseExtension, LeaseSettlement, Milliseconds, NonEmptyText, OwnerSerializedClaims, type PayloadOnlyMapping, PositiveInteger, type ScheduledTask, type SettlementChange, SubscribingTransport, type TextOption, type TransportSubscription, type WireField, assertPositiveInteger, createCloudEventReaders, createJsonEnvelopeFormat, fromHeader, fromPayloadField, isJsonObject, mediaTypeOf, observeRejection, schedule, sleep, writeStructuredCloudEvent };
503
+ export { BoundedInteger, CompositeKey, DecodedEventReader, DeliveryLeaseIdentity, ExponentialBackoff, HandedOutLedger, LaneLeaseTokenFormat, LeaseSettlement, Milliseconds, NonEmptyText, OwnerSerializedClaims, PositiveInteger, SubscribingTransport, assertPositiveInteger, createCloudEventReaders, createJsonEnvelopeFormat, fromHeader, fromPayloadField, isJsonObject, mediaTypeOf, observeRejection, schedule, sleep, writeStructuredCloudEvent };
504
+ export type { BodyEnvelopeMapping, BodyField, CancelTimer, CloudEventReaders, DecodedEventFields, ExtraBody, ExtraHeaders, ExtraValue, FixedType, HandedOutEntry, HeaderField, IntegerBounds, JsonConstant, JsonEnvelopeFailures, JsonEnvelopeFields, JsonEnvelopeFormat, JsonEnvelopeMapping, JsonWireMessage, LaneLeaseTokenLimits, LaneLeaseTokenParts, LeaseChange, LeaseExtension, OtherFields, PayloadOnlyMapping, ScheduledTask, SettlementChange, TextOption, TimeField, TimeFormat, TransportSubscription, WireField, WireTypes };
@@ -89,6 +89,55 @@ function writeStructuredCloudEvent(envelope, { time, data }) {
89
89
  return `${JSON.stringify(attributes).slice(0, -1)},"data":${data}}`;
90
90
  }
91
91
 
92
+ // src/internal/json-envelope-wire-types.ts
93
+ function compileWireTypes(wireTypes, { fixed }) {
94
+ if (wireTypes === void 0) {
95
+ return { wireTypes: /* @__PURE__ */ new Map(), eventTypes: /* @__PURE__ */ new Map() };
96
+ }
97
+ if (!isJsonObject(wireTypes) || fixed) {
98
+ throw new ConfigurationError(
99
+ "jsonEnvelope wireTypes must be an object of event type to wire name, with a type at { body } or { header }"
100
+ );
101
+ }
102
+ const entries = Object.entries(wireTypes);
103
+ const eventTypes = /* @__PURE__ */ new Map();
104
+ for (const [type, wire] of entries) {
105
+ assertEventType(type);
106
+ if (typeof wire !== "string" || wire.length === 0 || eventTypes.has(wire)) {
107
+ throw new ConfigurationError(
108
+ `jsonEnvelope wireTypes needs a non-empty wire name for '${type}' that no other type uses`
109
+ );
110
+ }
111
+ eventTypes.set(wire, type);
112
+ }
113
+ return { wireTypes: new Map(entries), eventTypes };
114
+ }
115
+ function wireTypeOf(envelope, { plan, failures }) {
116
+ const wire = plan.wireTypes.get(envelope.type);
117
+ if (wire !== void 0) {
118
+ return wire;
119
+ }
120
+ const other = plan.eventTypes.get(envelope.type);
121
+ if (other !== void 0) {
122
+ throw failures.unencodable(
123
+ `Event ${envelope.id} has type '${envelope.type}', which wireTypes uses as the wire name of '${other}'`
124
+ );
125
+ }
126
+ return envelope.type;
127
+ }
128
+ function eventTypeOf(wire, { plan, failures }) {
129
+ const type = plan.eventTypes.get(wire);
130
+ if (type !== void 0) {
131
+ return type;
132
+ }
133
+ try {
134
+ assertEventType(wire);
135
+ } catch (error) {
136
+ throw failures.invalid(`The event type is not a valid event type: ${error.message}`);
137
+ }
138
+ return wire;
139
+ }
140
+
92
141
  // src/internal/json-envelope-plan.ts
93
142
  var OPTIONAL_NAMES = [
94
143
  "key",
@@ -97,6 +146,7 @@ var OPTIONAL_NAMES = [
97
146
  "correlationId",
98
147
  "causationId"
99
148
  ];
149
+ var TIME_FORMATS = ["rfc3339", "epoch-millis", "epoch-seconds"];
100
150
  var ARRAY_INDEX = /^(?:0|[1-9]\d*)$/;
101
151
  function compile(mapping, transportIds) {
102
152
  if (!isJsonObject(mapping)) {
@@ -109,16 +159,72 @@ function compile(mapping, transportIds) {
109
159
  optional[name] = toLocation(field, name);
110
160
  }
111
161
  }
162
+ const payload = compilePayload(mapping.payload);
163
+ const topLevelHeaders = mapping.headers === "top-level-fields";
164
+ if (topLevelHeaders && payload === void 0) {
165
+ throw new ConfigurationError("jsonEnvelope headers 'top-level-fields' needs a body payload");
166
+ }
167
+ const type = compileType(mapping.type);
112
168
  const plan = {
113
- type: compileType(mapping.type),
169
+ type,
114
170
  id: compileId(mapping.id, transportIds),
171
+ idWhenMissing: isJsonObject(mapping.id) && "whenMissing" in mapping.id,
115
172
  optional,
116
- payload: compilePayload(mapping.payload),
117
- headers: compileHeaders(mapping.headers),
118
- mappedHeaders: /* @__PURE__ */ new Set()
173
+ payload,
174
+ headers: topLevelHeaders ? void 0 : compileHeaders(mapping.headers),
175
+ topLevelHeaders,
176
+ topLevelNames: /* @__PURE__ */ new Set(),
177
+ mappedHeaders: /* @__PURE__ */ new Set(),
178
+ timeFormat: compileTimeFormat(mapping.occurredAt),
179
+ otherFields: compileOtherFields(mapping, payload !== void 0),
180
+ ...compileWireTypes(mapping.wireTypes, { fixed: "fixed" in type }),
181
+ ...compileExtras(mapping, payload !== void 0)
119
182
  };
120
183
  return assertDisjoint(plan);
121
184
  }
185
+ function compileTimeFormat(field) {
186
+ const format = isJsonObject(field) ? field["format"] : void 0;
187
+ if (format === void 0) {
188
+ return "rfc3339";
189
+ }
190
+ if (!TIME_FORMATS.includes(format)) {
191
+ throw new ConfigurationError(
192
+ `jsonEnvelope occurredAt format must be one of ${TIME_FORMATS.map((name) => `'${name}'`).join(", ")}`
193
+ );
194
+ }
195
+ return format;
196
+ }
197
+ function compileExtras(mapping, hasBody) {
198
+ const extra = mapping.extra ?? {};
199
+ const bodyOrder = mapping.bodyOrder ?? [];
200
+ if (!hasBody && (extra.body !== void 0 || bodyOrder.length > 0)) {
201
+ throw new ConfigurationError(
202
+ "jsonEnvelope with a whole-body payload has no body for extra values or a body order"
203
+ );
204
+ }
205
+ return {
206
+ extraBody: Object.entries(extra.body ?? {}).map(([path, value]) => ({
207
+ path: toPath(path, "extra body value"),
208
+ value
209
+ })),
210
+ extraHeaders: Object.entries(extra.headers ?? {}),
211
+ bodyOrder
212
+ };
213
+ }
214
+ function compileOtherFields(mapping, hasBody) {
215
+ const otherFields = mapping.otherFields;
216
+ if (otherFields === void 0) {
217
+ return void 0;
218
+ }
219
+ if (!hasBody) {
220
+ throw new ConfigurationError("jsonEnvelope otherFields needs a body payload");
221
+ }
222
+ const field = isJsonObject(otherFields) ? otherFields["payloadField"] : void 0;
223
+ if (typeof field !== "string" || field.length === 0) {
224
+ throw new ConfigurationError("jsonEnvelope otherFields must be { payloadField: '<name>' }");
225
+ }
226
+ return field;
227
+ }
122
228
  function compileType(type) {
123
229
  if (isJsonObject(type) && typeof type["fixed"] === "string") {
124
230
  assertEventType(type["fixed"]);
@@ -133,11 +239,15 @@ function compileId(id, transportIds) {
133
239
  if (typeof id === "string" && transportIds.includes(id)) {
134
240
  return void 0;
135
241
  }
242
+ const names = transportIds.map((name) => `'${name}'`).join(", ");
136
243
  if (!isJsonObject(id)) {
137
244
  throw new ConfigurationError(
138
- `jsonEnvelope needs an id: { body }, { header } or one of ${transportIds.map((name) => `'${name}'`).join(", ")}`
245
+ `jsonEnvelope needs an id: { body }, { header } or one of ${names}`
139
246
  );
140
247
  }
248
+ if ("whenMissing" in id && !transportIds.includes(id["whenMissing"])) {
249
+ throw new ConfigurationError(`jsonEnvelope id whenMissing must be one of ${names}`);
250
+ }
141
251
  return toLocation(id, "id");
142
252
  }
143
253
  function compilePayload(payload) {
@@ -186,17 +296,25 @@ function assertDisjoint(plan) {
186
296
  const locations = [plan.type, plan.id, ...Object.values(plan.optional)].filter(
187
297
  (location) => location !== void 0 && !("fixed" in location)
188
298
  );
189
- const headers = locations.flatMap((location) => "header" in location ? [location.header] : []);
299
+ const headers = [
300
+ ...locations.flatMap((location) => "header" in location ? [location.header] : []),
301
+ ...plan.extraHeaders.map(([name]) => name)
302
+ ];
190
303
  const duplicate = headers.find((name, index) => headers.indexOf(name) !== index);
191
304
  if (duplicate !== void 0) {
192
305
  throw new ConfigurationError(`jsonEnvelope maps header '${duplicate}' twice`);
193
306
  }
194
307
  const paths = [
195
308
  ...locations.flatMap((location) => "path" in location ? [location.path] : []),
196
- ...[plan.payload, plan.headers].filter((path) => path !== void 0)
309
+ ...[plan.payload, plan.headers].filter((path) => path !== void 0),
310
+ ...plan.extraBody.map(({ path }) => path)
197
311
  ];
198
312
  assertNoNestedPaths(paths);
199
- return { ...plan, mappedHeaders: new Set(headers) };
313
+ return {
314
+ ...plan,
315
+ mappedHeaders: new Set(headers),
316
+ topLevelNames: new Set(paths.map((path) => path[0]))
317
+ };
200
318
  }
201
319
  function assertNoNestedPaths(paths) {
202
320
  paths.forEach((path, index) => {
@@ -216,8 +334,60 @@ function assertCompatiblePaths(path, other) {
216
334
  }
217
335
  }
218
336
 
337
+ // src/internal/json-envelope-other-fields.ts
338
+ function unusedFields(body, { plan }) {
339
+ return Object.entries(body).filter(
340
+ ([name, value]) => !plan.topLevelNames.has(name) && !(plan.topLevelHeaders && typeof value === "string")
341
+ );
342
+ }
343
+ function withOtherFields(payload, { body, context }) {
344
+ const field = context.plan.otherFields;
345
+ if (field === void 0) {
346
+ return payload;
347
+ }
348
+ const fields = unusedFields(body, context);
349
+ if (!isJsonObject(payload) && fields.length === 0) {
350
+ return payload;
351
+ }
352
+ if (!isJsonObject(payload) || Object.hasOwn(payload, field)) {
353
+ throw context.failures.invalid(
354
+ `otherFields keeps the unused top-level fields under the payload field '${field}', but the payload is not an object without that field`
355
+ );
356
+ }
357
+ return Object.fromEntries([...Object.entries(payload), [field, Object.fromEntries(fields)]]);
358
+ }
359
+ function splitOtherFields(envelope, context) {
360
+ const field = context.plan.otherFields;
361
+ const { payload } = envelope;
362
+ if (field === void 0 || !isJsonObject(payload) || !Object.hasOwn(payload, field)) {
363
+ return { payload, fields: [] };
364
+ }
365
+ const others = payload[field];
366
+ if (!isJsonObject(others)) {
367
+ throw context.failures.unencodable(
368
+ `Event ${envelope.id} has a payload field '${field}' that is not an object of top-level fields`
369
+ );
370
+ }
371
+ const fields = Object.entries(others);
372
+ const refused = fields.find((entry) => isRefused(entry, context));
373
+ if (refused !== void 0) {
374
+ throw context.failures.unencodable(
375
+ `Event ${envelope.id} keeps a top-level field '${refused[0]}' under '${field}' that the mapping reads as another value`
376
+ );
377
+ }
378
+ return {
379
+ payload: Object.fromEntries(Object.entries(payload).filter(([name]) => name !== field)),
380
+ fields
381
+ };
382
+ }
383
+ function isRefused([name, value], { plan }) {
384
+ return plan.topLevelNames.has(name) || plan.topLevelHeaders && typeof value === "string";
385
+ }
386
+
219
387
  // src/internal/json-envelope-read.ts
220
388
  var RFC3339_PATTERN = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:\d{2})$/i;
389
+ var EPOCH_PATTERN = /^\d+$/;
390
+ var MILLISECONDS_PER_SECOND = 1e3;
221
391
  function readMessage(message, context) {
222
392
  const { plan, failures } = context;
223
393
  if (plan.payload !== void 0 && !isJsonObject(message.body)) {
@@ -225,7 +395,7 @@ function readMessage(message, context) {
225
395
  }
226
396
  const read = (location) => location === void 0 ? void 0 : readText(message, { location, failures });
227
397
  const fields = {
228
- id: plan.id === void 0 ? void 0 : requireText(read(plan.id), { location: plan.id, failures }),
398
+ id: readId(read(plan.id), context),
229
399
  type: readType(message, context),
230
400
  payload: readPayload(message.body, context),
231
401
  key: read(plan.optional.key),
@@ -233,24 +403,25 @@ function readMessage(message, context) {
233
403
  headers: readEventHeaders(message, plan),
234
404
  correlationId: read(plan.optional.correlationId) ?? null,
235
405
  causationId: read(plan.optional.causationId) ?? null,
236
- occurredAt: readTime(read(plan.optional.occurredAt), failures)
406
+ occurredAt: readTime(read(plan.optional.occurredAt), context)
237
407
  };
238
408
  return Object.fromEntries(
239
409
  Object.entries(fields).filter((entry) => entry[1] !== void 0)
240
410
  );
241
411
  }
412
+ function readId(id, { plan, failures }) {
413
+ if (plan.id === void 0 || plan.idWhenMissing) {
414
+ return id;
415
+ }
416
+ return requireText(id, { location: plan.id, failures });
417
+ }
242
418
  function readType(message, { plan, failures }) {
243
419
  if ("fixed" in plan.type) {
244
420
  return plan.type.fixed;
245
421
  }
246
422
  const location = plan.type;
247
- const type = requireText(readText(message, { location, failures }), { location, failures });
248
- try {
249
- assertEventType(type);
250
- } catch (error) {
251
- throw failures.invalid(`The event type is not a valid event type: ${error.message}`);
252
- }
253
- return type;
423
+ const wire = requireText(readText(message, { location, failures }), { location, failures });
424
+ return eventTypeOf(wire, { plan, failures });
254
425
  }
255
426
  function readPayload(body, { plan, failures }) {
256
427
  if (plan.payload === void 0) {
@@ -260,25 +431,45 @@ function readPayload(body, { plan, failures }) {
260
431
  if (payload === void 0) {
261
432
  throw failures.invalid(`The message has no payload at '${plan.payload.join(".")}'`);
262
433
  }
263
- return payload;
434
+ return withOtherFields(payload, {
435
+ body,
436
+ context: { plan, failures }
437
+ });
264
438
  }
265
439
  function readEventHeaders(message, plan) {
440
+ if (plan.topLevelHeaders) {
441
+ return textEntries(
442
+ Object.entries(message.body).filter(
443
+ ([name]) => !plan.topLevelNames.has(name)
444
+ )
445
+ );
446
+ }
266
447
  if (plan.headers === void 0) {
267
448
  return Object.fromEntries(
268
449
  Object.entries(message.headers).filter(([name]) => !plan.mappedHeaders.has(name))
269
450
  );
270
451
  }
271
452
  const headers = valueAt(message.body, plan.headers);
272
- return isJsonObject(headers) ? Object.fromEntries(
273
- Object.entries(headers).filter(
274
- (entry) => typeof entry[1] === "string"
275
- )
276
- ) : {};
453
+ return isJsonObject(headers) ? textEntries(Object.entries(headers)) : {};
277
454
  }
278
- function readTime(text, failures) {
455
+ function textEntries(entries) {
456
+ return Object.fromEntries(
457
+ entries.filter((entry) => typeof entry[1] === "string")
458
+ );
459
+ }
460
+ function readTime(text, { plan, failures }) {
279
461
  if (text === void 0) {
280
462
  return void 0;
281
463
  }
464
+ if (plan.timeFormat !== "rfc3339") {
465
+ const time2 = new Date(
466
+ Number(text) * (plan.timeFormat === "epoch-seconds" ? MILLISECONDS_PER_SECOND : 1)
467
+ );
468
+ if (!EPOCH_PATTERN.test(text) || Number.isNaN(time2.getTime())) {
469
+ throw failures.invalid(`The event time is not a whole ${plan.timeFormat} number: '${text}'`);
470
+ }
471
+ return time2;
472
+ }
282
473
  const time = new Date(text);
283
474
  if (!RFC3339_PATTERN.test(text) || Number.isNaN(time.getTime())) {
284
475
  throw failures.invalid(`The event time is not an RFC 3339 timestamp: '${text}'`);
@@ -320,63 +511,135 @@ function valueAt(body, path) {
320
511
 
321
512
  // src/internal/json-envelope-write.ts
322
513
  var MAXIMUM_RFC3339_YEAR = 9999;
514
+ var MILLISECONDS_PER_SECOND2 = 1e3;
323
515
  function writeMessage(envelope, { plan, failures }) {
324
516
  const root = {};
325
517
  const headers = [];
326
518
  const put = (location, value) => {
327
519
  if ("header" in location) {
328
- headers.push([location.header, value]);
520
+ headers.push([location.header, String(value)]);
329
521
  } else {
330
522
  setAt(root, { path: location.path, value });
331
523
  }
332
524
  };
333
- if ("fixed" in plan.type) {
334
- if (envelope.type !== plan.type.fixed) {
335
- throw failures.unencodable(
336
- `Event ${envelope.id} has type '${envelope.type}', but this route carries only '${plan.type.fixed}'`
337
- );
338
- }
339
- } else {
340
- put(plan.type, envelope.type);
525
+ const typeAt = typeLocation(envelope, { plan, failures });
526
+ if (typeAt !== void 0) {
527
+ put(typeAt, wireTypeOf(envelope, { plan, failures }));
341
528
  }
342
529
  if (plan.id !== void 0) {
343
530
  put(plan.id, envelope.id);
344
531
  }
345
- for (const [name, value] of optionalValues(envelope, failures)) {
532
+ for (const [name, value] of optionalValues(envelope, { plan, failures })) {
346
533
  const location = plan.optional[name];
347
534
  if (location !== void 0 && value !== null) {
348
535
  put(location, value);
349
536
  }
350
537
  }
351
538
  return {
352
- body: writeBody(envelope, { plan, root }),
353
- headers: Object.fromEntries([...eventHeaderEntries(envelope, { plan, failures }), ...headers])
539
+ body: writeBody(envelope, { plan, failures, root }),
540
+ headers: Object.fromEntries([
541
+ ...eventHeaderEntries(envelope, { plan, failures }),
542
+ ...headers,
543
+ ...extraHeaderEntries(envelope, plan)
544
+ ])
354
545
  };
355
546
  }
356
- function optionalValues(envelope, failures) {
547
+ function typeLocation(envelope, { plan, failures }) {
548
+ if (!("fixed" in plan.type)) {
549
+ return plan.type;
550
+ }
551
+ if (envelope.type !== plan.type.fixed) {
552
+ throw failures.unencodable(
553
+ `Event ${envelope.id} has type '${envelope.type}', but this route carries only '${plan.type.fixed}'`
554
+ );
555
+ }
556
+ return void 0;
557
+ }
558
+ function extraHeaderEntries(envelope, plan) {
559
+ return plan.extraHeaders.map(([name, value]) => [
560
+ name,
561
+ typeof value === "function" ? value(envelope) : value
562
+ ]);
563
+ }
564
+ function optionalValues(envelope, { plan, failures }) {
357
565
  return [
358
566
  ["key", envelope.key],
359
567
  ["source", envelope.source],
360
- ["occurredAt", formatTime(envelope, failures)],
568
+ [
569
+ "occurredAt",
570
+ timeValue(formatTime(envelope, failures), { envelope, format: plan.timeFormat })
571
+ ],
361
572
  ["correlationId", envelope.correlationId],
362
573
  ["causationId", envelope.causationId]
363
574
  ];
364
575
  }
365
- function writeBody(envelope, { plan, root }) {
576
+ function writeBody(envelope, { plan, failures, root }) {
577
+ const { payload, fields } = splitOtherFields(envelope, { plan, failures });
366
578
  if (plan.payload === void 0) {
367
- return envelope.payload;
579
+ return payload;
368
580
  }
369
- setAt(root, { path: plan.payload, value: envelope.payload });
581
+ setAt(root, { path: plan.payload, value: payload });
370
582
  if (plan.headers !== void 0) {
371
583
  setAt(root, {
372
584
  path: plan.headers,
373
585
  value: Object.fromEntries(Object.entries(envelope.headers))
374
586
  });
375
587
  }
376
- return root;
588
+ for (const { path, value } of plan.extraBody) {
589
+ setAt(root, { path, value: typeof value === "function" ? value(envelope) : value });
590
+ }
591
+ if (plan.topLevelHeaders) {
592
+ spreadHeaders(envelope, { plan, failures, root });
593
+ }
594
+ spreadOtherFields(envelope, { failures, fields, root });
595
+ return ordered(root, plan.bodyOrder);
596
+ }
597
+ function spreadHeaders(envelope, { plan, failures, root }) {
598
+ for (const [name, value] of Object.entries(envelope.headers)) {
599
+ if (plan.topLevelNames.has(name)) {
600
+ throw failures.unencodable(
601
+ `Event ${envelope.id} has a header '${name}', which the mapping uses for another body field`
602
+ );
603
+ }
604
+ setAt(root, { path: [name], value });
605
+ }
606
+ }
607
+ function spreadOtherFields(envelope, {
608
+ failures,
609
+ fields,
610
+ root
611
+ }) {
612
+ for (const [name, value] of fields) {
613
+ if (Object.hasOwn(root, name)) {
614
+ throw failures.unencodable(
615
+ `Event ${envelope.id} keeps a top-level field '${name}' that an event header also writes`
616
+ );
617
+ }
618
+ setAt(root, { path: [name], value });
619
+ }
620
+ }
621
+ function timeValue(text, { envelope, format }) {
622
+ const milliseconds = envelope.occurredAt.getTime();
623
+ if (format === "epoch-millis") {
624
+ return milliseconds;
625
+ }
626
+ return format === "epoch-seconds" ? Math.floor(milliseconds / MILLISECONDS_PER_SECOND2) : text;
627
+ }
628
+ function ordered(root, order) {
629
+ if (order.length === 0) {
630
+ return root;
631
+ }
632
+ const result = {};
633
+ const names = [...order.filter((name) => Object.hasOwn(root, name)), ...Object.keys(root)];
634
+ for (const name of names) {
635
+ if (!Object.hasOwn(result, name)) {
636
+ defineValue(result, { name, value: root[name] });
637
+ }
638
+ }
639
+ return result;
377
640
  }
378
641
  function eventHeaderEntries(envelope, { plan, failures }) {
379
- if (plan.headers !== void 0) {
642
+ if (plan.headers !== void 0 || plan.topLevelHeaders) {
380
643
  return [];
381
644
  }
382
645
  const entries = Object.entries(envelope.headers);