@relaybus/core 0.3.0 → 0.4.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.
@@ -1,4 +1,4 @@
1
- import { a as EventHeaders, E as EventEnvelope, C as Clock } from '../event-DCRTuYKI.cjs';
1
+ import { EventHeaders, EventEnvelope, Clock, LeaseReference, ErrorInfo, ClaimRequest, ClaimedDelivery, Transport, TransportMessageHandler, TransportSubscriptionHooks, CloseOptions, Duration } from '../index.cjs';
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,21 +82,46 @@ 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. */
@@ -111,10 +136,25 @@ type BodyEnvelopeMapping<TTransportId extends string> = MappingFields<WireField,
111
136
  /** Body path of the payload. Required. */
112
137
  readonly payload: BodyField;
113
138
  /**
114
- * Event headers: a body path holding an object of strings, or `'message-headers'` (default),
115
- * every message header that no other field maps.
139
+ * Event headers: a body path holding an object of strings, `'message-headers'` (default),
140
+ * every message header that no other field maps, or `'top-level-fields'`, spread over the top
141
+ * level of the body: every top-level text field that no other value uses is read as one.
142
+ */
143
+ readonly headers?: BodyField | 'message-headers' | 'top-level-fields';
144
+ /**
145
+ * Values written with every message besides the mapped ones, such as a constant
146
+ * `provider: 'orders-api'` or a second copy of the time. They are never read; extra headers
147
+ * are left out of the event headers on read.
148
+ */
149
+ readonly extra?: {
150
+ readonly body?: ExtraBody;
151
+ readonly headers?: ExtraHeaders;
152
+ };
153
+ /**
154
+ * Top-level body fields to write first, in this order; the others follow in the order they
155
+ * are written. Only the bytes change, for formats compared byte for byte.
116
156
  */
117
- readonly headers?: BodyField | 'message-headers';
157
+ readonly bodyOrder?: readonly string[];
118
158
  };
119
159
  /**
120
160
  * The body is the payload itself, as a plain JSON producer writes it; every other value lives in
@@ -124,6 +164,10 @@ type PayloadOnlyMapping<TTransportId extends string> = MappingFields<HeaderField
124
164
  readonly payload: 'whole-body';
125
165
  /** Event headers: every message header that no other field maps. */
126
166
  readonly headers?: 'message-headers';
167
+ /** Message headers written with every message; never read, and left out of the event headers. */
168
+ readonly extra?: {
169
+ readonly headers?: ExtraHeaders;
170
+ };
127
171
  };
128
172
  /** Where every envelope value lives in a JSON message. `type`, `id` and `payload` are required. */
129
173
  type JsonEnvelopeMapping<TTransportId extends string> = BodyEnvelopeMapping<TTransportId> | PayloadOnlyMapping<TTransportId>;
@@ -168,4 +212,272 @@ declare function createJsonEnvelopeFormat(mapping: JsonEnvelopeMapping<string>,
168
212
  readonly failures: JsonEnvelopeFailures;
169
213
  }): JsonEnvelopeFormat;
170
214
 
171
- export { type BodyEnvelopeMapping, type BodyField, type CancelTimer, type CloudEventReaders, type FixedType, type HeaderField, type JsonEnvelopeFailures, type JsonEnvelopeFields, type JsonEnvelopeFormat, type JsonEnvelopeMapping, type JsonWireMessage, type PayloadOnlyMapping, type ScheduledTask, type WireField, assertPositiveInteger, createCloudEventReaders, createJsonEnvelopeFormat, fromHeader, fromPayloadField, isJsonObject, mediaTypeOf, schedule, sleep, writeStructuredCloudEvent };
215
+ /** What a ledger remembers of a claim: at least who it was handed to and which delivery. */
216
+ interface HandedOutEntry {
217
+ readonly owner: string;
218
+ readonly deliveryId: string;
219
+ }
220
+ /**
221
+ * The claims a store instance handed out and has not seen settled, keyed by
222
+ * {@link DeliveryLeaseIdentity} (delivery id and exact lease token). After a failed claim the
223
+ * store keeps exactly these listed as running and gives the rest back.
224
+ */
225
+ declare class HandedOutLedger<TEntry extends HandedOutEntry> {
226
+ #private;
227
+ register(lease: LeaseReference, entry: TEntry): void;
228
+ /** Whether the claim with this identity ({@link DeliveryLeaseIdentity.of}) is handed out. */
229
+ has(identity: string): boolean;
230
+ forget(lease: LeaseReference): void;
231
+ /** Forgets every entry `settled` says is settled. */
232
+ forgetWhere(settled: (entry: TEntry, identity: string) => boolean): void;
233
+ /** The entries handed out to `owner`. */
234
+ entriesOf(owner: string): TEntry[];
235
+ }
236
+
237
+ /** How a claim ends without completing: retried later, given back, or dead-lettered. */
238
+ type SettlementChange = {
239
+ readonly kind: 'reschedule';
240
+ readonly availableAt: Date;
241
+ readonly error: ErrorInfo;
242
+ readonly now: Date;
243
+ } | {
244
+ readonly kind: 'release';
245
+ readonly now: Date;
246
+ } | {
247
+ readonly kind: 'bury';
248
+ readonly error: ErrorInfo;
249
+ readonly now: Date;
250
+ };
251
+ /** A change of the claim a lease names. */
252
+ type LeaseChange = {
253
+ readonly lease: LeaseReference;
254
+ readonly change: SettlementChange;
255
+ };
256
+ /** A lease renewal; `now` for a store that records when it happened. */
257
+ type LeaseExtension = {
258
+ readonly lease: LeaseReference;
259
+ readonly leaseExpiresAt: Date;
260
+ readonly now?: Date;
261
+ };
262
+ /**
263
+ * What a store answers about a lease: at once when it answers from memory, later when it asks a
264
+ * database. Answering at once keeps the in-memory store as fast (in microtasks) as it always was.
265
+ */
266
+ type StoreAnswer = boolean | Promise<boolean>;
267
+ /**
268
+ * The fenced settlement skeleton every event store shares: find the claim a lease names (a lease
269
+ * this store cannot have issued, or whose claim ended, is not held and changes nothing), apply the
270
+ * change fenced by the lease, then forget the lease as handed out once the store answered.
271
+ * A store supplies only how it finds a held claim, how it writes a change and a renewal, and its
272
+ * bookkeeping of handed-out leases.
273
+ */
274
+ declare abstract class LeaseSettlement<THeld> {
275
+ #private;
276
+ /** Renews the lease when it is still held; false otherwise. */
277
+ extendLease(extension: LeaseExtension): StoreAnswer;
278
+ /**
279
+ * Applies `change` when the lease is still held. Once the store answered (either way) the lease
280
+ * is forgotten as handed out; when the write throws it stays registered, since the caller may
281
+ * retry with the same lease.
282
+ */
283
+ changeClaim(request: LeaseChange): StoreAnswer;
284
+ /** The claim `lease` names, when this store issued the lease; undefined otherwise. */
285
+ protected abstract findHeld(lease: LeaseReference): THeld | undefined;
286
+ protected abstract writeChange(held: THeld, request: LeaseChange): StoreAnswer;
287
+ protected abstract writeExtension(held: THeld, extension: LeaseExtension): StoreAnswer;
288
+ /** Drops `lease` from the store's handed-out bookkeeping; nothing for a store that keeps none. */
289
+ protected abstract forgetHandedOut(lease: LeaseReference): void;
290
+ }
291
+
292
+ /**
293
+ * The claim skeleton of the database stores: a request for no handlers or no deliveries claims
294
+ * nothing, and the claims of one owner run one after the other. Claims of one owner read the same
295
+ * held lanes and the same bookkeeping of what was handed out, so they must not overlap (the
296
+ * dispatcher claims one batch at a time anyway; `runOnce` next to a running dispatcher is what
297
+ * this guards against). A store supplies only {@link OwnerSerializedClaims.claimInTurn}.
298
+ */
299
+ declare abstract class OwnerSerializedClaims {
300
+ #private;
301
+ claim(request: ClaimRequest): Promise<ClaimedDelivery[]>;
302
+ /** Whether the request cannot claim anything: no handlers, or a limit that is not above 0. */
303
+ protected claimsNothing(request: ClaimRequest): boolean;
304
+ /** Claims for `request` once the earlier claims of its owner settled. */
305
+ protected abstract claimInTurn(request: ClaimRequest): Promise<ClaimedDelivery[]>;
306
+ }
307
+
308
+ /** The event a transport's decoder extracted from a message; see the transport's `DecodedEvent`. */
309
+ interface DecodedEventFields {
310
+ readonly id: string;
311
+ readonly type: string;
312
+ readonly payload: unknown;
313
+ readonly source?: string;
314
+ readonly key?: string | null;
315
+ readonly headers?: EventHeaders;
316
+ readonly correlationId?: string | null;
317
+ readonly causationId?: string | null;
318
+ readonly occurredAt?: Date;
319
+ }
320
+ /** A fixed value, or an extractor that reads it from a plain message. */
321
+ type TextOption<TPlain, TValue extends string | null> = TValue | ((message: TPlain) => TValue | undefined) | undefined;
322
+ /**
323
+ * Observes the rejection of a promise an async decoder or extractor returned by
324
+ * mistake: the value is refused as malformed, and an unobserved rejection would
325
+ * otherwise crash the process.
326
+ */
327
+ declare function observeRejection(value: unknown): void;
328
+ /**
329
+ * The inbound plumbing a transport shares with every other: completing what a user decoder
330
+ * returned and reading the extractor options of a plain JSON decoder. A transport supplies only its
331
+ * malformed-message error and the defaults its messages carry (source, key, time).
332
+ */
333
+ declare abstract class DecodedEventReader<TMessage, TPlain> {
334
+ /**
335
+ * Fills the fields a decoder left out and guards against a decoder that returned no object,
336
+ * which would otherwise surface as a retried `TypeError` instead of a malformed message.
337
+ */
338
+ complete(decoded: unknown, message: TMessage): EventEnvelope;
339
+ /** The text an option gives for `plain`; undefined when it gives none or `''`. */
340
+ text<TValue extends string | null>(option: TextOption<TPlain, TValue>, { plain, name }: {
341
+ readonly plain: TPlain;
342
+ readonly name: string;
343
+ }): string | undefined;
344
+ /** The error of a malformed message, which the transport dead-letters instead of retrying. */
345
+ abstract invalid(reason: string): Error;
346
+ protected abstract defaultSource(message: TMessage): string;
347
+ protected abstract defaultKey(message: TMessage): string | null;
348
+ protected abstract defaultTime(message: TMessage): Date;
349
+ }
350
+
351
+ /** One subscription of a transport: what it consumes and where the messages go. */
352
+ interface TransportSubscription {
353
+ readonly destinations: readonly string[];
354
+ readonly onMessage: TransportMessageHandler;
355
+ readonly hooks: TransportSubscriptionHooks | undefined;
356
+ }
357
+ /**
358
+ * The skeleton of a broker transport that publishes and subscribes: the {@link Transport} port
359
+ * with its subscribe signature in one place. A transport supplies how it publishes, opens its one
360
+ * subscription, reports readiness and closes.
361
+ */
362
+ declare abstract class SubscribingTransport implements Transport {
363
+ abstract readonly name: string;
364
+ abstract readonly system: string;
365
+ /**
366
+ * Subscribes to `destinations`. Not async itself: the subscription's own promise is returned
367
+ * as it is, so its checks reject rather than throw.
368
+ */
369
+ subscribe(destinations: readonly string[], onMessage: TransportMessageHandler, hooks?: TransportSubscriptionHooks): Promise<void>;
370
+ abstract publish(envelope: EventEnvelope, destination: string): Promise<void>;
371
+ abstract isReady(): boolean;
372
+ abstract close(options?: CloseOptions): Promise<void>;
373
+ /** Opens the transport's one subscription; rejects when it cannot. */
374
+ protected abstract openSubscription(subscription: TransportSubscription): Promise<void>;
375
+ }
376
+
377
+ /**
378
+ * Two identifiers joined into one map or set key, such as an event id and a handler id, or a
379
+ * handler id and an ordering key. The NUL separator keeps `('a:b', 'c')` and `('a', 'b:c')` apart.
380
+ * Keys are plain strings, so they cost nothing more than the template literal they replace.
381
+ */
382
+ declare abstract class CompositeKey {
383
+ static of(first: string, second: string): string;
384
+ /** The start every key of `first` shares, to find them all by prefix. */
385
+ static startOf(first: string): string;
386
+ }
387
+
388
+ /** The range a {@link BoundedInteger} option allows. */
389
+ interface IntegerBounds {
390
+ readonly name: string;
391
+ readonly minimum: number;
392
+ readonly maximum: number;
393
+ }
394
+ /** An integer option with fixed lower and upper bounds. */
395
+ declare abstract class BoundedInteger {
396
+ /** Returns `value` when it is an integer within the bounds, else throws a {@link ConfigurationError}. */
397
+ static of(value: number, { name, minimum, maximum }: IntegerBounds): number;
398
+ }
399
+
400
+ /**
401
+ * Identifies one claim of a delivery: the delivery id with the exact lease token it was handed out
402
+ * with. Two claims of the same delivery (an expired one and the current one) have different
403
+ * identities, so bookkeeping of the older can never replace or remove the newer one's.
404
+ */
405
+ declare abstract class DeliveryLeaseIdentity {
406
+ static of(deliveryId: string, leaseToken: string): string;
407
+ /** The start of the identity of every claim of `deliveryId`. */
408
+ static prefixOf(deliveryId: string): string;
409
+ }
410
+
411
+ /**
412
+ * Delays that double with every attempt, from `initialMs` up to `maxMs`: the first retry waits
413
+ * `initialMs`, the second twice that, and so on. Without jitter, for in-process retries.
414
+ */
415
+ declare class ExponentialBackoff {
416
+ readonly initialMs: number;
417
+ readonly maxMs: number;
418
+ constructor({ initialMs, maxMs }: {
419
+ readonly initialMs: number;
420
+ readonly maxMs: number;
421
+ });
422
+ /** The delay after failed attempt `attempt` (1-based). */
423
+ delayFor(attempt: number): number;
424
+ }
425
+
426
+ /** The claim a lane lease token names, as written in the token. */
427
+ interface LaneLeaseTokenParts {
428
+ readonly lane: number;
429
+ /** The lane's epoch, as decimal text: PostgreSQL epochs exceed the safe integers. */
430
+ readonly epoch: string;
431
+ /** The claim generation of the delivery. */
432
+ readonly claims: number;
433
+ }
434
+ /** How many digits each part of a token may have, and how many lanes there are. */
435
+ interface LaneLeaseTokenLimits {
436
+ readonly laneCount: number;
437
+ readonly epochDigits: number;
438
+ readonly claimsDigits: number;
439
+ }
440
+ /**
441
+ * The fencing token of a claim in a lane-leased store: `<lane>:<epoch>:<claims>`. Every state
442
+ * change checks the lane epoch and the delivery's claim generation it names. A store creates one
443
+ * format with its own digit limits and parses only the tokens it can have issued.
444
+ */
445
+ declare class LaneLeaseTokenFormat {
446
+ #private;
447
+ constructor(limits: LaneLeaseTokenLimits);
448
+ format(lane: number, { epoch, claims }: {
449
+ readonly epoch: string | number;
450
+ readonly claims: number;
451
+ }): string;
452
+ /** The parts of `token`; undefined when the store cannot have issued it. */
453
+ parse(token: string): LaneLeaseTokenParts | undefined;
454
+ }
455
+
456
+ /**
457
+ * A configured length of time, converted once from a {@link Duration} option. Each factory names
458
+ * the option, so its refusal says which one was wrong.
459
+ */
460
+ declare abstract class Milliseconds {
461
+ /** Zero or more milliseconds. */
462
+ static of(value: Duration, name: string): number;
463
+ /** More than zero milliseconds: a wait, a timeout or an age that must pass at all. */
464
+ static positive(value: Duration, name: string): number;
465
+ /** A duration that drives a timer: more than zero, and no longer than timers can represent. */
466
+ static forTimer(value: Duration, name: string): number;
467
+ }
468
+
469
+ /** A text option that must not be empty: a name, an exchange, a queue. */
470
+ declare abstract class NonEmptyText {
471
+ static of(value: string, name: string): string;
472
+ /** Like {@link NonEmptyText.of}, for an option that may be left out. */
473
+ static optional(value: string | undefined, name: string): string | undefined;
474
+ }
475
+
476
+ /** A count option that must be a whole number of at least 1: a batch size, attempts, a limit. */
477
+ declare abstract class PositiveInteger {
478
+ /** Returns `value` when it is an integer of at least 1, else throws a {@link ConfigurationError} naming the option. */
479
+ static of(value: number, name: string): number;
480
+ }
481
+
482
+ 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 };
483
+ export type { BodyEnvelopeMapping, BodyField, CancelTimer, CloudEventReaders, DecodedEventFields, ExtraBody, ExtraHeaders, ExtraValue, FixedType, HandedOutEntry, HeaderField, IntegerBounds, JsonConstant, JsonEnvelopeFailures, JsonEnvelopeFields, JsonEnvelopeFormat, JsonEnvelopeMapping, JsonWireMessage, LaneLeaseTokenLimits, LaneLeaseTokenParts, LeaseChange, LeaseExtension, PayloadOnlyMapping, ScheduledTask, SettlementChange, TextOption, TimeField, TimeFormat, TransportSubscription, WireField };