@relaybus/core 0.2.0 → 0.3.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,438 @@
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';
2
+
3
+ /** Returns `value` when it is an integer of at least 1, else throws a {@link ConfigurationError} naming the option. */
4
+ declare function assertPositiveInteger(value: number, name: string): number;
5
+
6
+ /**
7
+ * Reads one header of an incoming message, for the `type`, `id`, `key` or
8
+ * `source` option of {@link plainJsonDecoder}. Header names are compared
9
+ * exactly (Kafka and AMQP header names are case-sensitive). Returns undefined when the
10
+ * header is absent, so the decoder falls back to its default.
11
+ *
12
+ * ```ts
13
+ * plainJsonDecoder({ type: fromHeader('event-type'), id: fromHeader('message-id') })
14
+ * ```
15
+ */
16
+ declare function fromHeader(name: string): (message: {
17
+ readonly headers: EventHeaders;
18
+ }) => string | undefined;
19
+ /**
20
+ * Reads a field of the JSON payload by a dotted path (`'meta.eventType'`), for
21
+ * the `type`, `id`, `key` or `source` option of {@link plainJsonDecoder}. A
22
+ * string is returned as is and a safe integer as its decimal text (numeric ids
23
+ * are common). Anything else returns undefined so the decoder falls back to its
24
+ * default: a missing field, and also a fraction or an integer beyond 2^53, which
25
+ * JSON parsing may already have rounded, so two different ids could collide.
26
+ *
27
+ * ```ts
28
+ * plainJsonDecoder({ type: fromPayloadField('eventType'), key: fromPayloadField('order.id') })
29
+ * ```
30
+ */
31
+ declare function fromPayloadField(path: string): (message: {
32
+ readonly payload: unknown;
33
+ }) => string | undefined;
34
+
35
+ /** The media type of a content type, without parameters and lower-cased; undefined for anything but a string. */
36
+ declare function mediaTypeOf(contentType: unknown): string | undefined;
37
+ declare function isJsonObject(value: unknown): value is Record<string, unknown>;
38
+ /** Readers of structured CloudEvent attributes that fail with the error of the calling transport. */
39
+ interface CloudEventReaders {
40
+ readonly optionalAttribute: (body: Record<string, unknown>, name: string) => string | undefined;
41
+ readonly requiredAttribute: (body: Record<string, unknown>, name: string) => string;
42
+ readonly assertValidType: (type: string, label: string) => void;
43
+ readonly assertJsonDataContentType: (dataContentType: string | undefined) => void;
44
+ }
45
+ declare function createCloudEventReaders(invalid: (reason: string) => Error): CloudEventReaders;
46
+ /**
47
+ * The JSON text of a CloudEvents 1.0 structured-mode event: the attributes (the key as `subject`,
48
+ * correlation and causation ids as extension attributes), then `data`, the already serialized
49
+ * payload, so the payload is serialized only once.
50
+ */
51
+ declare function writeStructuredCloudEvent(envelope: EventEnvelope, { time, data }: {
52
+ readonly time: string;
53
+ readonly data: string;
54
+ }): string;
55
+
56
+ /** A task for {@link schedule}: what runs, after how long, whether it repeats and keeps the process alive. */
57
+ type ScheduledTask = Parameters<NonNullable<Clock['schedule']>>[0];
58
+ /** Cancels a scheduled task; harmless once it ran or was cancelled. */
59
+ type CancelTimer = () => void;
60
+ /**
61
+ * The one place relaybus starts timers: through `clock.schedule` when the clock controls timers,
62
+ * else with the platform timers.
63
+ */
64
+ declare function schedule(clock: Clock, task: ScheduledTask): CancelTimer;
65
+ /** Resolves after `milliseconds` on the timers of `clock`. */
66
+ declare function sleep(clock: Clock, milliseconds: number): Promise<void>;
67
+
68
+ /**
69
+ * A field of the JSON body, by dotted path: `'eventName'`, `'meta.correlationId'`. A segment that
70
+ * is a whole number indexes an array: `'messageType.0'` is the first element of `messageType`.
71
+ */
72
+ type BodyField = {
73
+ readonly body: string;
74
+ };
75
+ /** A message header (Kafka record header, AMQP message header), by exact name. */
76
+ type HeaderField = {
77
+ readonly header: string;
78
+ };
79
+ /** Where one envelope value lives on the wire. */
80
+ type WireField = BodyField | HeaderField;
81
+ /** The one event type every message of the route carries: written nowhere, used for every message read. */
82
+ type FixedType = {
83
+ readonly fixed: string;
84
+ };
85
+ type MappingFields<TField extends WireField, TTransportId extends string> = {
86
+ /** Where the event type lives, or the one type of the route. Required. */
87
+ readonly type: TField | FixedType;
88
+ /**
89
+ * Where the event id lives, or the transport's own message identity (see the transport's
90
+ * mapping type). Required, so the inbox dedup key is always a deliberate choice. A message
91
+ * whose mapped id is missing is malformed.
92
+ */
93
+ readonly id: TField | TTransportId;
94
+ /** Ordering key. Default: the transport's own (the Kafka record key; none on RabbitMQ). */
95
+ readonly key?: TField;
96
+ /** Producing service. Default: the transport's own (Kafka topic; RabbitMQ `appId`, else the routing key). */
97
+ readonly source?: TField;
98
+ /** RFC 3339 time. Default: the transport's own (Kafka record timestamp; RabbitMQ `timestamp`, seconds). */
99
+ readonly occurredAt?: TField;
100
+ readonly correlationId?: TField;
101
+ readonly causationId?: TField;
102
+ /** Media type written with the message. Default `application/json`. Not checked on read. */
103
+ readonly contentType?: string;
104
+ };
105
+ /**
106
+ * A JSON object that wraps the payload with event metadata, such as
107
+ * `{ "eventName": ..., "messageId": ..., "data": {...}, "meta": {...} }`.
108
+ * Each value lives at a body path or in a header.
109
+ */
110
+ type BodyEnvelopeMapping<TTransportId extends string> = MappingFields<WireField, TTransportId> & {
111
+ /** Body path of the payload. Required. */
112
+ readonly payload: BodyField;
113
+ /**
114
+ * Event headers: a body path holding an object of strings, or `'message-headers'` (default),
115
+ * every message header that no other field maps.
116
+ */
117
+ readonly headers?: BodyField | 'message-headers';
118
+ };
119
+ /**
120
+ * The body is the payload itself, as a plain JSON producer writes it; every other value lives in
121
+ * a header, so body paths are a compile error here.
122
+ */
123
+ type PayloadOnlyMapping<TTransportId extends string> = MappingFields<HeaderField, TTransportId> & {
124
+ readonly payload: 'whole-body';
125
+ /** Event headers: every message header that no other field maps. */
126
+ readonly headers?: 'message-headers';
127
+ };
128
+ /** Where every envelope value lives in a JSON message. `type`, `id` and `payload` are required. */
129
+ type JsonEnvelopeMapping<TTransportId extends string> = BodyEnvelopeMapping<TTransportId> | PayloadOnlyMapping<TTransportId>;
130
+ /** A JSON message: the parsed body and the text headers. */
131
+ type JsonWireMessage = {
132
+ readonly body: unknown;
133
+ readonly headers: EventHeaders;
134
+ };
135
+ /** The values read from a message. An absent optional value is left to the transport's default. */
136
+ type JsonEnvelopeFields = {
137
+ readonly id?: string;
138
+ readonly type: string;
139
+ readonly payload: unknown;
140
+ readonly key?: string;
141
+ readonly source?: string;
142
+ readonly headers: EventHeaders;
143
+ readonly correlationId: string | null;
144
+ readonly causationId: string | null;
145
+ readonly occurredAt?: Date;
146
+ };
147
+ /** The errors of the calling transport. */
148
+ type JsonEnvelopeFailures = {
149
+ /** For a message that cannot be read: it goes to the invalid-message path. */
150
+ readonly invalid: (reason: string) => Error;
151
+ /** For an event that cannot be written: never retried. */
152
+ readonly unencodable: (reason: string) => Error;
153
+ };
154
+ /** Writes and reads one mapping. Created once per codec; both methods are pure. */
155
+ interface JsonEnvelopeFormat {
156
+ /** The mapped headers and the body; the id is left out when the transport supplies it. */
157
+ write(envelope: EventEnvelope): JsonWireMessage;
158
+ read(message: JsonWireMessage): JsonEnvelopeFields;
159
+ }
160
+ /**
161
+ * Compiles `mapping` once. `transportIds` lists the transport's own id strategies.
162
+ *
163
+ * @throws ConfigurationError for a mapping that is incomplete, names a header twice, nests one
164
+ * body path inside another or mixes array indexes and names under one body field.
165
+ */
166
+ declare function createJsonEnvelopeFormat(mapping: JsonEnvelopeMapping<string>, { transportIds, failures, }: {
167
+ readonly transportIds: readonly string[];
168
+ readonly failures: JsonEnvelopeFailures;
169
+ }): JsonEnvelopeFormat;
170
+
171
+ /** What a ledger remembers of a claim: at least who it was handed to and which delivery. */
172
+ interface HandedOutEntry {
173
+ readonly owner: string;
174
+ readonly deliveryId: string;
175
+ }
176
+ /**
177
+ * The claims a store instance handed out and has not seen settled, keyed by
178
+ * {@link DeliveryLeaseIdentity} (delivery id and exact lease token). After a failed claim the
179
+ * store keeps exactly these listed as running and gives the rest back.
180
+ */
181
+ declare class HandedOutLedger<TEntry extends HandedOutEntry> {
182
+ #private;
183
+ register(lease: LeaseReference, entry: TEntry): void;
184
+ /** Whether the claim with this identity ({@link DeliveryLeaseIdentity.of}) is handed out. */
185
+ has(identity: string): boolean;
186
+ forget(lease: LeaseReference): void;
187
+ /** Forgets every entry `settled` says is settled. */
188
+ forgetWhere(settled: (entry: TEntry, identity: string) => boolean): void;
189
+ /** The entries handed out to `owner`. */
190
+ entriesOf(owner: string): TEntry[];
191
+ }
192
+
193
+ /** How a claim ends without completing: retried later, given back, or dead-lettered. */
194
+ type SettlementChange = {
195
+ readonly kind: 'reschedule';
196
+ readonly availableAt: Date;
197
+ readonly error: ErrorInfo;
198
+ readonly now: Date;
199
+ } | {
200
+ readonly kind: 'release';
201
+ readonly now: Date;
202
+ } | {
203
+ readonly kind: 'bury';
204
+ readonly error: ErrorInfo;
205
+ readonly now: Date;
206
+ };
207
+ /** A change of the claim a lease names. */
208
+ type LeaseChange = {
209
+ readonly lease: LeaseReference;
210
+ readonly change: SettlementChange;
211
+ };
212
+ /** A lease renewal; `now` for a store that records when it happened. */
213
+ type LeaseExtension = {
214
+ readonly lease: LeaseReference;
215
+ readonly leaseExpiresAt: Date;
216
+ readonly now?: Date;
217
+ };
218
+ /**
219
+ * What a store answers about a lease: at once when it answers from memory, later when it asks a
220
+ * database. Answering at once keeps the in-memory store as fast (in microtasks) as it always was.
221
+ */
222
+ type StoreAnswer = boolean | Promise<boolean>;
223
+ /**
224
+ * The fenced settlement skeleton every event store shares: find the claim a lease names (a lease
225
+ * this store cannot have issued, or whose claim ended, is not held and changes nothing), apply the
226
+ * change fenced by the lease, then forget the lease as handed out once the store answered.
227
+ * A store supplies only how it finds a held claim, how it writes a change and a renewal, and its
228
+ * bookkeeping of handed-out leases.
229
+ */
230
+ declare abstract class LeaseSettlement<THeld> {
231
+ #private;
232
+ /** Renews the lease when it is still held; false otherwise. */
233
+ extendLease(extension: LeaseExtension): StoreAnswer;
234
+ /**
235
+ * Applies `change` when the lease is still held. Once the store answered (either way) the lease
236
+ * is forgotten as handed out; when the write throws it stays registered, since the caller may
237
+ * retry with the same lease.
238
+ */
239
+ changeClaim(request: LeaseChange): StoreAnswer;
240
+ /** The claim `lease` names, when this store issued the lease; undefined otherwise. */
241
+ protected abstract findHeld(lease: LeaseReference): THeld | undefined;
242
+ protected abstract writeChange(held: THeld, request: LeaseChange): StoreAnswer;
243
+ protected abstract writeExtension(held: THeld, extension: LeaseExtension): StoreAnswer;
244
+ /** Drops `lease` from the store's handed-out bookkeeping; nothing for a store that keeps none. */
245
+ protected abstract forgetHandedOut(lease: LeaseReference): void;
246
+ }
247
+
248
+ /**
249
+ * The claim skeleton of the database stores: a request for no handlers or no deliveries claims
250
+ * nothing, and the claims of one owner run one after the other. Claims of one owner read the same
251
+ * held lanes and the same bookkeeping of what was handed out, so they must not overlap (the
252
+ * dispatcher claims one batch at a time anyway; `runOnce` next to a running dispatcher is what
253
+ * this guards against). A store supplies only {@link OwnerSerializedClaims.claimInTurn}.
254
+ */
255
+ declare abstract class OwnerSerializedClaims {
256
+ #private;
257
+ claim(request: ClaimRequest): Promise<ClaimedDelivery[]>;
258
+ /** Whether the request cannot claim anything: no handlers, or a limit that is not above 0. */
259
+ protected claimsNothing(request: ClaimRequest): boolean;
260
+ /** Claims for `request` once the earlier claims of its owner settled. */
261
+ protected abstract claimInTurn(request: ClaimRequest): Promise<ClaimedDelivery[]>;
262
+ }
263
+
264
+ /** The event a transport's decoder extracted from a message; see the transport's `DecodedEvent`. */
265
+ interface DecodedEventFields {
266
+ readonly id: string;
267
+ readonly type: string;
268
+ readonly payload: unknown;
269
+ readonly source?: string;
270
+ readonly key?: string | null;
271
+ readonly headers?: EventHeaders;
272
+ readonly correlationId?: string | null;
273
+ readonly causationId?: string | null;
274
+ readonly occurredAt?: Date;
275
+ }
276
+ /** A fixed value, or an extractor that reads it from a plain message. */
277
+ type TextOption<TPlain, TValue extends string | null> = TValue | ((message: TPlain) => TValue | undefined) | undefined;
278
+ /**
279
+ * Observes the rejection of a promise an async decoder or extractor returned by
280
+ * mistake: the value is refused as malformed, and an unobserved rejection would
281
+ * otherwise crash the process.
282
+ */
283
+ declare function observeRejection(value: unknown): void;
284
+ /**
285
+ * The inbound plumbing a transport shares with every other: completing what a user decoder
286
+ * returned and reading the extractor options of a plain JSON decoder. A transport supplies only its
287
+ * malformed-message error and the defaults its messages carry (source, key, time).
288
+ */
289
+ declare abstract class DecodedEventReader<TMessage, TPlain> {
290
+ /**
291
+ * Fills the fields a decoder left out and guards against a decoder that returned no object,
292
+ * which would otherwise surface as a retried `TypeError` instead of a malformed message.
293
+ */
294
+ complete(decoded: unknown, message: TMessage): EventEnvelope;
295
+ /** The text an option gives for `plain`; undefined when it gives none or `''`. */
296
+ text<TValue extends string | null>(option: TextOption<TPlain, TValue>, { plain, name }: {
297
+ readonly plain: TPlain;
298
+ readonly name: string;
299
+ }): string | undefined;
300
+ /** The error of a malformed message, which the transport dead-letters instead of retrying. */
301
+ abstract invalid(reason: string): Error;
302
+ protected abstract defaultSource(message: TMessage): string;
303
+ protected abstract defaultKey(message: TMessage): string | null;
304
+ protected abstract defaultTime(message: TMessage): Date;
305
+ }
306
+
307
+ /** One subscription of a transport: what it consumes and where the messages go. */
308
+ interface TransportSubscription {
309
+ readonly destinations: readonly string[];
310
+ readonly onMessage: TransportMessageHandler;
311
+ readonly hooks: TransportSubscriptionHooks | undefined;
312
+ }
313
+ /**
314
+ * The skeleton of a broker transport that publishes and subscribes: the {@link Transport} port
315
+ * with its subscribe signature in one place. A transport supplies how it publishes, opens its one
316
+ * subscription, reports readiness and closes.
317
+ */
318
+ declare abstract class SubscribingTransport implements Transport {
319
+ abstract readonly name: string;
320
+ abstract readonly system: string;
321
+ /**
322
+ * Subscribes to `destinations`. Not async itself: the subscription's own promise is returned
323
+ * as it is, so its checks reject rather than throw.
324
+ */
325
+ subscribe(destinations: readonly string[], onMessage: TransportMessageHandler, hooks?: TransportSubscriptionHooks): Promise<void>;
326
+ abstract publish(envelope: EventEnvelope, destination: string): Promise<void>;
327
+ abstract isReady(): boolean;
328
+ abstract close(options?: CloseOptions): Promise<void>;
329
+ /** Opens the transport's one subscription; rejects when it cannot. */
330
+ protected abstract openSubscription(subscription: TransportSubscription): Promise<void>;
331
+ }
332
+
333
+ /**
334
+ * Two identifiers joined into one map or set key, such as an event id and a handler id, or a
335
+ * handler id and an ordering key. The NUL separator keeps `('a:b', 'c')` and `('a', 'b:c')` apart.
336
+ * Keys are plain strings, so they cost nothing more than the template literal they replace.
337
+ */
338
+ declare abstract class CompositeKey {
339
+ static of(first: string, second: string): string;
340
+ /** The start every key of `first` shares, to find them all by prefix. */
341
+ static startOf(first: string): string;
342
+ }
343
+
344
+ /** The range a {@link BoundedInteger} option allows. */
345
+ interface IntegerBounds {
346
+ readonly name: string;
347
+ readonly minimum: number;
348
+ readonly maximum: number;
349
+ }
350
+ /** An integer option with fixed lower and upper bounds. */
351
+ declare abstract class BoundedInteger {
352
+ /** Returns `value` when it is an integer within the bounds, else throws a {@link ConfigurationError}. */
353
+ static of(value: number, { name, minimum, maximum }: IntegerBounds): number;
354
+ }
355
+
356
+ /**
357
+ * Identifies one claim of a delivery: the delivery id with the exact lease token it was handed out
358
+ * with. Two claims of the same delivery (an expired one and the current one) have different
359
+ * identities, so bookkeeping of the older can never replace or remove the newer one's.
360
+ */
361
+ declare abstract class DeliveryLeaseIdentity {
362
+ static of(deliveryId: string, leaseToken: string): string;
363
+ /** The start of the identity of every claim of `deliveryId`. */
364
+ static prefixOf(deliveryId: string): string;
365
+ }
366
+
367
+ /**
368
+ * Delays that double with every attempt, from `initialMs` up to `maxMs`: the first retry waits
369
+ * `initialMs`, the second twice that, and so on. Without jitter, for in-process retries.
370
+ */
371
+ declare class ExponentialBackoff {
372
+ readonly initialMs: number;
373
+ readonly maxMs: number;
374
+ constructor({ initialMs, maxMs }: {
375
+ readonly initialMs: number;
376
+ readonly maxMs: number;
377
+ });
378
+ /** The delay after failed attempt `attempt` (1-based). */
379
+ delayFor(attempt: number): number;
380
+ }
381
+
382
+ /** The claim a lane lease token names, as written in the token. */
383
+ interface LaneLeaseTokenParts {
384
+ readonly lane: number;
385
+ /** The lane's epoch, as decimal text: PostgreSQL epochs exceed the safe integers. */
386
+ readonly epoch: string;
387
+ /** The claim generation of the delivery. */
388
+ readonly claims: number;
389
+ }
390
+ /** How many digits each part of a token may have, and how many lanes there are. */
391
+ interface LaneLeaseTokenLimits {
392
+ readonly laneCount: number;
393
+ readonly epochDigits: number;
394
+ readonly claimsDigits: number;
395
+ }
396
+ /**
397
+ * The fencing token of a claim in a lane-leased store: `<lane>:<epoch>:<claims>`. Every state
398
+ * change checks the lane epoch and the delivery's claim generation it names. A store creates one
399
+ * format with its own digit limits and parses only the tokens it can have issued.
400
+ */
401
+ declare class LaneLeaseTokenFormat {
402
+ #private;
403
+ constructor(limits: LaneLeaseTokenLimits);
404
+ format(lane: number, { epoch, claims }: {
405
+ readonly epoch: string | number;
406
+ readonly claims: number;
407
+ }): string;
408
+ /** The parts of `token`; undefined when the store cannot have issued it. */
409
+ parse(token: string): LaneLeaseTokenParts | undefined;
410
+ }
411
+
412
+ /**
413
+ * A configured length of time, converted once from a {@link Duration} option. Each factory names
414
+ * the option, so its refusal says which one was wrong.
415
+ */
416
+ declare abstract class Milliseconds {
417
+ /** Zero or more milliseconds. */
418
+ static of(value: Duration, name: string): number;
419
+ /** More than zero milliseconds: a wait, a timeout or an age that must pass at all. */
420
+ static positive(value: Duration, name: string): number;
421
+ /** A duration that drives a timer: more than zero, and no longer than timers can represent. */
422
+ static forTimer(value: Duration, name: string): number;
423
+ }
424
+
425
+ /** A text option that must not be empty: a name, an exchange, a queue. */
426
+ declare abstract class NonEmptyText {
427
+ static of(value: string, name: string): string;
428
+ /** Like {@link NonEmptyText.of}, for an option that may be left out. */
429
+ static optional(value: string | undefined, name: string): string | undefined;
430
+ }
431
+
432
+ /** A count option that must be a whole number of at least 1: a batch size, attempts, a limit. */
433
+ declare abstract class PositiveInteger {
434
+ /** Returns `value` when it is an integer of at least 1, else throws a {@link ConfigurationError} naming the option. */
435
+ static of(value: number, name: string): number;
436
+ }
437
+
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 };