@relaybus/core 0.3.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.
@@ -1,5 +1,10 @@
1
- import { ConfigurationError, assertEventType } from '../chunk-WS274DL3.js';
2
- export { assertPositiveInteger, schedule, sleep } from '../chunk-WS274DL3.js';
1
+ import { PositiveInteger, ConfigurationError, assertEventType, DeliveryLeaseIdentity } from '../chunk-NOR5JHVN.js';
2
+ export { CompositeKey, DeliveryLeaseIdentity, LeaseSettlement, Milliseconds, PositiveInteger, schedule, sleep } from '../chunk-NOR5JHVN.js';
3
+
4
+ // src/internal/validation.ts
5
+ function assertPositiveInteger(value, name) {
6
+ return PositiveInteger.of(value, name);
7
+ }
3
8
 
4
9
  // src/internal/field-extractors.ts
5
10
  function fromHeader(name) {
@@ -84,7 +89,7 @@ function writeStructuredCloudEvent(envelope, { time, data }) {
84
89
  return `${JSON.stringify(attributes).slice(0, -1)},"data":${data}}`;
85
90
  }
86
91
 
87
- // src/internal/json-envelope.ts
92
+ // src/internal/json-envelope-plan.ts
88
93
  var OPTIONAL_NAMES = [
89
94
  "key",
90
95
  "source",
@@ -92,19 +97,7 @@ var OPTIONAL_NAMES = [
92
97
  "correlationId",
93
98
  "causationId"
94
99
  ];
95
- var MAXIMUM_RFC3339_YEAR = 9999;
96
- var RFC3339_PATTERN = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:\d{2})$/i;
97
100
  var ARRAY_INDEX = /^(?:0|[1-9]\d*)$/;
98
- function createJsonEnvelopeFormat(mapping, {
99
- transportIds,
100
- failures
101
- }) {
102
- const plan = compile(mapping, transportIds);
103
- return {
104
- write: (envelope) => writeMessage(envelope, { plan, failures }),
105
- read: (message) => readMessage(message, { plan, failures })
106
- };
107
- }
108
101
  function compile(mapping, transportIds) {
109
102
  if (!isJsonObject(mapping)) {
110
103
  throw new ConfigurationError("jsonEnvelope needs a mapping object");
@@ -222,6 +215,111 @@ function assertCompatiblePaths(path, other) {
222
215
  throw new ConfigurationError(`jsonEnvelope body paths ${both} mix an array index and a name`);
223
216
  }
224
217
  }
218
+
219
+ // src/internal/json-envelope-read.ts
220
+ var RFC3339_PATTERN = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:\d{2})$/i;
221
+ function readMessage(message, context) {
222
+ const { plan, failures } = context;
223
+ if (plan.payload !== void 0 && !isJsonObject(message.body)) {
224
+ throw failures.invalid("The message body must be a JSON object");
225
+ }
226
+ const read = (location) => location === void 0 ? void 0 : readText(message, { location, failures });
227
+ const fields = {
228
+ id: plan.id === void 0 ? void 0 : requireText(read(plan.id), { location: plan.id, failures }),
229
+ type: readType(message, context),
230
+ payload: readPayload(message.body, context),
231
+ key: read(plan.optional.key),
232
+ source: read(plan.optional.source),
233
+ headers: readEventHeaders(message, plan),
234
+ correlationId: read(plan.optional.correlationId) ?? null,
235
+ causationId: read(plan.optional.causationId) ?? null,
236
+ occurredAt: readTime(read(plan.optional.occurredAt), failures)
237
+ };
238
+ return Object.fromEntries(
239
+ Object.entries(fields).filter((entry) => entry[1] !== void 0)
240
+ );
241
+ }
242
+ function readType(message, { plan, failures }) {
243
+ if ("fixed" in plan.type) {
244
+ return plan.type.fixed;
245
+ }
246
+ 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;
254
+ }
255
+ function readPayload(body, { plan, failures }) {
256
+ if (plan.payload === void 0) {
257
+ return body;
258
+ }
259
+ const payload = valueAt(body, plan.payload);
260
+ if (payload === void 0) {
261
+ throw failures.invalid(`The message has no payload at '${plan.payload.join(".")}'`);
262
+ }
263
+ return payload;
264
+ }
265
+ function readEventHeaders(message, plan) {
266
+ if (plan.headers === void 0) {
267
+ return Object.fromEntries(
268
+ Object.entries(message.headers).filter(([name]) => !plan.mappedHeaders.has(name))
269
+ );
270
+ }
271
+ 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
+ ) : {};
277
+ }
278
+ function readTime(text, failures) {
279
+ if (text === void 0) {
280
+ return void 0;
281
+ }
282
+ const time = new Date(text);
283
+ if (!RFC3339_PATTERN.test(text) || Number.isNaN(time.getTime())) {
284
+ throw failures.invalid(`The event time is not an RFC 3339 timestamp: '${text}'`);
285
+ }
286
+ return time;
287
+ }
288
+ function readText(message, { location, failures }) {
289
+ const value = "header" in location ? Object.hasOwn(message.headers, location.header) ? message.headers[location.header] : void 0 : valueAt(message.body, location.path);
290
+ if (value === void 0 || value === null || value === "") {
291
+ return void 0;
292
+ }
293
+ if (typeof value === "string") {
294
+ return value;
295
+ }
296
+ if (typeof value === "number" && Number.isSafeInteger(value)) {
297
+ return String(value);
298
+ }
299
+ throw failures.invalid(`The ${location.label} at ${describe(location)} must be a string`);
300
+ }
301
+ function requireText(value, { location, failures }) {
302
+ if (value === void 0) {
303
+ throw failures.invalid(`The message has no ${location.label} at ${describe(location)}`);
304
+ }
305
+ return value;
306
+ }
307
+ function describe(location) {
308
+ return "header" in location ? `header '${location.header}'` : `'${location.path.join(".")}'`;
309
+ }
310
+ function valueAt(body, path) {
311
+ let value = body;
312
+ for (const segment of path) {
313
+ if (typeof value !== "object" || value === null || !Object.hasOwn(value, segment)) {
314
+ return void 0;
315
+ }
316
+ value = value[segment];
317
+ }
318
+ return value;
319
+ }
320
+
321
+ // src/internal/json-envelope-write.ts
322
+ var MAXIMUM_RFC3339_YEAR = 9999;
225
323
  function writeMessage(envelope, { plan, failures }) {
226
324
  const root = {};
227
325
  const headers = [];
@@ -320,106 +418,200 @@ function defineValue(target, { name, value }) {
320
418
  configurable: true
321
419
  });
322
420
  }
323
- function readMessage(message, context) {
324
- const { plan, failures } = context;
325
- if (plan.payload !== void 0 && !isJsonObject(message.body)) {
326
- throw failures.invalid("The message body must be a JSON object");
327
- }
328
- const read = (location) => location === void 0 ? void 0 : readText(message, { location, failures });
329
- const fields = {
330
- id: plan.id === void 0 ? void 0 : requireText(read(plan.id), { location: plan.id, failures }),
331
- type: readType(message, context),
332
- payload: readPayload(message.body, context),
333
- key: read(plan.optional.key),
334
- source: read(plan.optional.source),
335
- headers: readEventHeaders(message, plan),
336
- correlationId: read(plan.optional.correlationId) ?? null,
337
- causationId: read(plan.optional.causationId) ?? null,
338
- occurredAt: readTime(read(plan.optional.occurredAt), failures)
421
+
422
+ // src/internal/json-envelope.ts
423
+ function createJsonEnvelopeFormat(mapping, {
424
+ transportIds,
425
+ failures
426
+ }) {
427
+ const plan = compile(mapping, transportIds);
428
+ return {
429
+ write: (envelope) => writeMessage(envelope, { plan, failures }),
430
+ read: (message) => readMessage(message, { plan, failures })
339
431
  };
340
- return Object.fromEntries(
341
- Object.entries(fields).filter((entry) => entry[1] !== void 0)
342
- );
343
- }
344
- function readType(message, { plan, failures }) {
345
- if ("fixed" in plan.type) {
346
- return plan.type.fixed;
347
- }
348
- const location = plan.type;
349
- const type = requireText(readText(message, { location, failures }), { location, failures });
350
- try {
351
- assertEventType(type);
352
- } catch (error) {
353
- throw failures.invalid(`The event type is not a valid event type: ${error.message}`);
354
- }
355
- return type;
356
432
  }
357
- function readPayload(body, { plan, failures }) {
358
- if (plan.payload === void 0) {
359
- return body;
433
+
434
+ // src/store-kit/handed-out-ledger.ts
435
+ var HandedOutLedger = class {
436
+ #entries = /* @__PURE__ */ new Map();
437
+ register(lease, entry) {
438
+ this.#entries.set(DeliveryLeaseIdentity.of(lease.deliveryId, lease.leaseToken), entry);
439
+ }
440
+ /** Whether the claim with this identity ({@link DeliveryLeaseIdentity.of}) is handed out. */
441
+ has(identity) {
442
+ return this.#entries.has(identity);
443
+ }
444
+ forget(lease) {
445
+ this.#entries.delete(DeliveryLeaseIdentity.of(lease.deliveryId, lease.leaseToken));
446
+ }
447
+ /** Forgets every entry `settled` says is settled. */
448
+ forgetWhere(settled) {
449
+ for (const [identity, entry] of this.#entries) {
450
+ if (settled(entry, identity)) {
451
+ this.#entries.delete(identity);
452
+ }
453
+ }
360
454
  }
361
- const payload = valueAt(body, plan.payload);
362
- if (payload === void 0) {
363
- throw failures.invalid(`The message has no payload at '${plan.payload.join(".")}'`);
455
+ /** The entries handed out to `owner`. */
456
+ entriesOf(owner) {
457
+ return [...this.#entries.values()].filter((entry) => entry.owner === owner);
364
458
  }
365
- return payload;
366
- }
367
- function readEventHeaders(message, plan) {
368
- if (plan.headers === void 0) {
369
- return Object.fromEntries(
370
- Object.entries(message.headers).filter(([name]) => !plan.mappedHeaders.has(name))
459
+ };
460
+
461
+ // src/store-kit/owner-serialized-claims.ts
462
+ var OwnerSerializedClaims = class {
463
+ /** The last claim of each owner still running; the next one waits for it. */
464
+ #queues = /* @__PURE__ */ new Map();
465
+ claim(request) {
466
+ if (this.claimsNothing(request)) {
467
+ return Promise.resolve([]);
468
+ }
469
+ return this.#serialized(request.leaseOwner, () => this.claimInTurn(request));
470
+ }
471
+ /** Whether the request cannot claim anything: no handlers, or a limit that is not above 0. */
472
+ claimsNothing(request) {
473
+ return request.handlers.length === 0 || !(request.limit > 0);
474
+ }
475
+ #serialized(owner, work) {
476
+ const previous = this.#queues.get(owner) ?? Promise.resolve();
477
+ const run = previous.then(work, work);
478
+ const tail = run.then(
479
+ () => void 0,
480
+ () => void 0
371
481
  );
482
+ this.#queues.set(owner, tail);
483
+ void tail.then(() => {
484
+ if (this.#queues.get(owner) === tail) {
485
+ this.#queues.delete(owner);
486
+ }
487
+ });
488
+ return run;
372
489
  }
373
- const headers = valueAt(message.body, plan.headers);
374
- return isJsonObject(headers) ? Object.fromEntries(
375
- Object.entries(headers).filter(
376
- (entry) => typeof entry[1] === "string"
377
- )
378
- ) : {};
379
- }
380
- function readTime(text, failures) {
381
- if (text === void 0) {
382
- return void 0;
383
- }
384
- const time = new Date(text);
385
- if (!RFC3339_PATTERN.test(text) || Number.isNaN(time.getTime())) {
386
- throw failures.invalid(`The event time is not an RFC 3339 timestamp: '${text}'`);
387
- }
388
- return time;
389
- }
390
- function readText(message, { location, failures }) {
391
- const value = "header" in location ? Object.hasOwn(message.headers, location.header) ? message.headers[location.header] : void 0 : valueAt(message.body, location.path);
392
- if (value === void 0 || value === null || value === "") {
393
- return void 0;
490
+ };
491
+
492
+ // src/transport-kit/decoded-event-reader.ts
493
+ function observeRejection(value) {
494
+ if (typeof value === "object" && value !== null && "then" in value) {
495
+ Promise.resolve(value).catch(() => void 0);
496
+ }
497
+ }
498
+ var DecodedEventReader = class {
499
+ /**
500
+ * Fills the fields a decoder left out and guards against a decoder that returned no object,
501
+ * which would otherwise surface as a retried `TypeError` instead of a malformed message.
502
+ */
503
+ complete(decoded, message) {
504
+ if (typeof decoded !== "object" || decoded === null) {
505
+ throw this.invalid("The decoder must return an event object synchronously");
506
+ }
507
+ if ("then" in decoded) {
508
+ observeRejection(decoded);
509
+ throw this.invalid("The decoder must return an event object synchronously");
510
+ }
511
+ const event = decoded;
512
+ return {
513
+ id: event.id,
514
+ type: event.type,
515
+ source: event.source ?? this.defaultSource(message),
516
+ key: event.key === void 0 ? this.defaultKey(message) : event.key,
517
+ payload: event.payload,
518
+ headers: event.headers ?? {},
519
+ correlationId: event.correlationId ?? null,
520
+ causationId: event.causationId ?? null,
521
+ occurredAt: event.occurredAt ?? this.defaultTime(message)
522
+ };
523
+ }
524
+ /** The text an option gives for `plain`; undefined when it gives none or `''`. */
525
+ text(option, { plain, name }) {
526
+ const value = typeof option === "function" ? option(plain) : option;
527
+ if (value === void 0 || value === null) {
528
+ return void 0;
529
+ }
530
+ if (typeof value !== "string") {
531
+ observeRejection(value);
532
+ throw this.invalid(`The ${name} option returned ${typeof value}, expected a string`);
533
+ }
534
+ return value.length === 0 ? void 0 : value;
394
535
  }
395
- if (typeof value === "string") {
536
+ };
537
+
538
+ // src/transport-kit/subscribing-transport.ts
539
+ var SubscribingTransport = class {
540
+ /**
541
+ * Subscribes to `destinations`. Not async itself: the subscription's own promise is returned
542
+ * as it is, so its checks reject rather than throw.
543
+ */
544
+ // eslint-disable-next-line relaybus/max-two-params -- the Transport.subscribe signature
545
+ subscribe(destinations, onMessage, hooks) {
546
+ return this.openSubscription({ destinations, onMessage, hooks });
547
+ }
548
+ };
549
+
550
+ // src/values/bounded-integer.ts
551
+ var BoundedInteger = class {
552
+ /** Returns `value` when it is an integer within the bounds, else throws a {@link ConfigurationError}. */
553
+ static of(value, { name, minimum, maximum }) {
554
+ if (!Number.isInteger(value) || value < minimum || value > maximum) {
555
+ throw new ConfigurationError(
556
+ `${name} must be an integer from ${minimum} to ${maximum}, received ${value}`
557
+ );
558
+ }
396
559
  return value;
397
560
  }
398
- if (typeof value === "number" && Number.isSafeInteger(value)) {
399
- return String(value);
561
+ };
562
+
563
+ // src/values/exponential-backoff.ts
564
+ var ExponentialBackoff = class {
565
+ initialMs;
566
+ maxMs;
567
+ constructor({ initialMs, maxMs }) {
568
+ this.initialMs = initialMs;
569
+ this.maxMs = maxMs;
570
+ }
571
+ /** The delay after failed attempt `attempt` (1-based). */
572
+ delayFor(attempt) {
573
+ return Math.min(this.initialMs * 2 ** (attempt - 1), this.maxMs);
574
+ }
575
+ };
576
+
577
+ // src/values/lane-lease-token.ts
578
+ var LANE_DIGITS = 3;
579
+ var LaneLeaseTokenFormat = class {
580
+ #pattern;
581
+ #laneCount;
582
+ constructor(limits) {
583
+ this.#laneCount = limits.laneCount;
584
+ this.#pattern = new RegExp(
585
+ `^(\\d{1,${LANE_DIGITS}}):(\\d{1,${limits.epochDigits}}):(\\d{1,${limits.claimsDigits}})$`
586
+ );
400
587
  }
401
- throw failures.invalid(`The ${location.label} at ${describe(location)} must be a string`);
402
- }
403
- function requireText(value, { location, failures }) {
404
- if (value === void 0) {
405
- throw failures.invalid(`The message has no ${location.label} at ${describe(location)}`);
588
+ format(lane, { epoch, claims }) {
589
+ return `${lane}:${epoch}:${claims}`;
406
590
  }
407
- return value;
408
- }
409
- function describe(location) {
410
- return "header" in location ? `header '${location.header}'` : `'${location.path.join(".")}'`;
411
- }
412
- function valueAt(body, path) {
413
- let value = body;
414
- for (const segment of path) {
415
- if (typeof value !== "object" || value === null || !Object.hasOwn(value, segment)) {
591
+ /** The parts of `token`; undefined when the store cannot have issued it. */
592
+ parse(token) {
593
+ const match = this.#pattern.exec(token);
594
+ if (match === null || Number(match[1]) >= this.#laneCount) {
416
595
  return void 0;
417
596
  }
418
- value = value[segment];
597
+ return { lane: Number(match[1]), epoch: match[2], claims: Number(match[3]) };
419
598
  }
420
- return value;
421
- }
599
+ };
600
+
601
+ // src/values/non-empty-text.ts
602
+ var NonEmptyText = class _NonEmptyText {
603
+ static of(value, name) {
604
+ if (value.length === 0) {
605
+ throw new ConfigurationError(`${name} must not be empty`);
606
+ }
607
+ return value;
608
+ }
609
+ /** Like {@link NonEmptyText.of}, for an option that may be left out. */
610
+ static optional(value, name) {
611
+ return value === void 0 ? void 0 : _NonEmptyText.of(value, name);
612
+ }
613
+ };
422
614
 
423
- export { createCloudEventReaders, createJsonEnvelopeFormat, fromHeader, fromPayloadField, isJsonObject, mediaTypeOf, writeStructuredCloudEvent };
615
+ export { BoundedInteger, DecodedEventReader, ExponentialBackoff, HandedOutLedger, LaneLeaseTokenFormat, NonEmptyText, OwnerSerializedClaims, SubscribingTransport, assertPositiveInteger, createCloudEventReaders, createJsonEnvelopeFormat, fromHeader, fromPayloadField, isJsonObject, mediaTypeOf, observeRejection, writeStructuredCloudEvent };
424
616
  //# sourceMappingURL=index.js.map
425
617
  //# sourceMappingURL=index.js.map