@relaybus/core 0.4.1 → 0.5.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.
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
- import { ConfigurationError, assertEventType, CompositeKey, Milliseconds, NonRetryableError, DrainIncompleteError, LifecycleError, toErrorInfo, LeaseSettlement, schedule, TransactionTimeoutError, PositiveInteger, DispatcherStoppedError, DeliveryLeaseIdentity, LeaseLostError, AttemptsExhaustedError, validatePayload, HandlerTimeoutError, TransactionRequiredError, keyFromDefinition, InvalidEnvelopeError } from './chunk-NOR5JHVN.js';
2
- export { AttemptsExhaustedError, ConfigurationError, DispatcherStoppedError, DrainIncompleteError, EventValidationError, HandlerTimeoutError, InvalidEnvelopeError, LeaseLostError, LifecycleError, MAX_TIMER_DELAY_MS, NonRetryableError, RelayBusError, TransactionRequiredError, TransactionTimeoutError, assertEventType, defineEvent, toErrorInfo, toMilliseconds, validatePayload } from './chunk-NOR5JHVN.js';
1
+ import { ConfigurationError, assertEventType, CompositeKey, Milliseconds, NonRetryableError, DrainIncompleteError, LifecycleError, toErrorInfo, UnknownCatalogEventError, WireTypeCollisionError, LeaseSettlement, schedule, TransactionTimeoutError, WireTypesUnsupportedError, PositiveInteger, DispatcherStoppedError, DeliveryLeaseIdentity, LeaseLostError, AttemptsExhaustedError, validatePayload, HandlerTimeoutError, TransactionRequiredError, keyFromDefinition, InvalidEnvelopeError, DuplicateRouteEventError } from './chunk-6PQ673MW.js';
2
+ export { AttemptsExhaustedError, ConfigurationError, DispatcherStoppedError, DrainIncompleteError, DuplicateRouteEventError, EventValidationError, HandlerTimeoutError, InvalidEnvelopeError, LeaseLostError, LifecycleError, MAX_TIMER_DELAY_MS, NonRetryableError, RelayBusError, TransactionIsolationConflictError, TransactionRequiredError, TransactionTimeoutError, UnknownCatalogEventError, WireTypeCollisionError, WireTypesUnsupportedError, assertEventType, defineEvent, toErrorInfo, toMilliseconds, validatePayload } from './chunk-6PQ673MW.js';
3
3
  import { randomBytes } from 'crypto';
4
4
  import { AsyncLocalStorage } from 'async_hooks';
5
5
  import { hostname } from 'os';
@@ -9,28 +9,46 @@ var systemClock = {
9
9
  now: () => /* @__PURE__ */ new Date()
10
10
  };
11
11
 
12
+ // src/catalog-wire-names.ts
13
+ function checkWireNames(wireNames, { events, label }) {
14
+ const checked = /* @__PURE__ */ new Map();
15
+ const owners = /* @__PURE__ */ new Map();
16
+ for (const [event, wire] of Object.entries(wireNames ?? {})) {
17
+ if (!events.includes(event)) {
18
+ throw new UnknownCatalogEventError(
19
+ `${label} has no event named '${event}' to give a wire name; its events are ${events.join(", ")}`
20
+ );
21
+ }
22
+ if (typeof wire !== "string" || wire.length === 0) {
23
+ throw new ConfigurationError(`${label} needs a non-empty wire name for '${event}'`);
24
+ }
25
+ const owner = owners.get(wire);
26
+ if (owner !== void 0) {
27
+ throw new WireTypeCollisionError(
28
+ `${label} gives events '${owner}' and '${event}' the same wire name '${wire}'`
29
+ );
30
+ }
31
+ owners.set(wire, event);
32
+ checked.set(event, wire);
33
+ }
34
+ return checked;
35
+ }
36
+
12
37
  // src/catalog.ts
13
38
  var EVENT_NAME_PATTERN = /^[A-Za-z_][A-Za-z0-9_]*$/;
14
39
  var descriptions = /* @__PURE__ */ new WeakMap();
40
+ var catalogWireNames = /* @__PURE__ */ new WeakMap();
15
41
  function defineCatalog(events, options = {}) {
16
42
  const label = options.name === void 0 ? "Catalog" : `Catalog '${options.name}'`;
17
43
  const names = Object.keys(events);
18
44
  if (names.length === 0) {
19
45
  throw new ConfigurationError(`${label} must contain at least one event`);
20
46
  }
47
+ const wireTypes = checkWireNames(options.wireTypes, { events: names, label });
21
48
  const owners = /* @__PURE__ */ new Map();
22
49
  const entries = names.map((name) => {
23
50
  const eventClass = events[name];
24
- if (!EVENT_NAME_PATTERN.test(name)) {
25
- throw new ConfigurationError(
26
- `${label} entry '${name}' must be a valid identifier made of letters, digits and underscores because it names the handler method on${name} and its handler id`
27
- );
28
- }
29
- if (typeof eventClass !== "function" || typeof eventClass.type !== "string") {
30
- throw new ConfigurationError(
31
- `${label} entry '${name}' must be an event class created with defineEvent`
32
- );
33
- }
51
+ assertCatalogEntry(name, { eventClass, label });
34
52
  assertEventType(eventClass.type);
35
53
  const owner = owners.get(eventClass.type);
36
54
  if (owner !== void 0) {
@@ -43,8 +61,25 @@ function defineCatalog(events, options = {}) {
43
61
  });
44
62
  const catalog = Object.freeze({ ...events });
45
63
  descriptions.set(catalog, Object.freeze({ name: options.name, entries: Object.freeze(entries) }));
64
+ catalogWireNames.set(catalog, wireTypes);
46
65
  return catalog;
47
66
  }
67
+ function wireNamesOf(catalog) {
68
+ describeCatalog(catalog);
69
+ return catalogWireNames.get(catalog);
70
+ }
71
+ function assertCatalogEntry(name, { eventClass, label }) {
72
+ if (!EVENT_NAME_PATTERN.test(name)) {
73
+ throw new ConfigurationError(
74
+ `${label} entry '${name}' must be a valid identifier made of letters, digits and underscores because it names the handler method on${name} and its handler id`
75
+ );
76
+ }
77
+ if (typeof eventClass !== "function" || typeof eventClass.type !== "string") {
78
+ throw new ConfigurationError(
79
+ `${label} entry '${name}' must be an event class created with defineEvent`
80
+ );
81
+ }
82
+ }
48
83
  function describeCatalog(catalog) {
49
84
  const description = descriptions.get(catalog);
50
85
  if (description === void 0) {
@@ -208,6 +243,9 @@ function consume(catalog, options = {}) {
208
243
  function produce(catalog, options = {}) {
209
244
  return declare(catalog, { role: "produce", options });
210
245
  }
246
+ function isCatalogDeclaration(value) {
247
+ return typeof value === "object" && value !== null && declarations.has(value);
248
+ }
211
249
  function declare(catalog, { role, options }) {
212
250
  const entries = selectCatalogEntries(catalog, options.events);
213
251
  const declaration = Object.freeze({
@@ -273,6 +311,144 @@ function describeGap(entry, {
273
311
  return `${subject}: produced here, needs a route to a transport${handlerNote}`;
274
312
  }
275
313
 
314
+ // src/route-wire-types.ts
315
+ function mergeRouteWireTypes(declared, routed) {
316
+ const merged = /* @__PURE__ */ new Map();
317
+ for (const { transport, type, wire } of declared) {
318
+ const types = merged.get(transport) ?? /* @__PURE__ */ new Map();
319
+ merged.set(transport, types);
320
+ addWireType(types, { type, wire, transport });
321
+ }
322
+ for (const [transport, types] of merged) {
323
+ assertDistinctOnTheWire(types, {
324
+ transport,
325
+ routedTypes: routed.filter((item) => item.transport === transport).map((item) => item.type)
326
+ });
327
+ }
328
+ return new Map([...merged].map(([transport, types]) => [transport, Object.fromEntries(types)]));
329
+ }
330
+ function assertDistinctOnTheWire(types, {
331
+ transport,
332
+ routedTypes
333
+ }) {
334
+ const owners = /* @__PURE__ */ new Map();
335
+ for (const type of /* @__PURE__ */ new Set([...types.keys(), ...routedTypes])) {
336
+ const name = types.get(type) ?? type;
337
+ const owner = owners.get(name);
338
+ if (owner !== void 0) {
339
+ throw new WireTypeCollisionError(
340
+ `Event types '${owner}' and '${type}' both travel as '${name}' on transport '${transport}'`
341
+ );
342
+ }
343
+ owners.set(name, type);
344
+ }
345
+ }
346
+ function addWireType(types, {
347
+ type,
348
+ wire,
349
+ transport
350
+ }) {
351
+ const current = types.get(type);
352
+ if (current !== void 0 && current !== wire) {
353
+ throw new WireTypeCollisionError(
354
+ `Event type '${type}' has the wire names '${current}' and '${wire}' on transport '${transport}'; a transport writes one`
355
+ );
356
+ }
357
+ const owner = [...types].find(
358
+ ([otherType, otherWire]) => otherWire === wire && otherType !== type
359
+ );
360
+ if (owner !== void 0) {
361
+ throw new WireTypeCollisionError(
362
+ `Event types '${owner[0]}' and '${type}' share the wire name '${wire}' on transport '${transport}'`
363
+ );
364
+ }
365
+ types.set(type, wire);
366
+ }
367
+
368
+ // src/catalog-routes.ts
369
+ var catalogRoutes = /* @__PURE__ */ new WeakSet();
370
+ function route(declaration, options) {
371
+ if (!isCatalogDeclaration(declaration) || declaration.role !== "produce") {
372
+ throw new ConfigurationError("route() takes a produce(catalog, { events }) declaration");
373
+ }
374
+ const wireNames = checkWireNames(options.wireTypes, {
375
+ events: declaration.events,
376
+ label: `The route of ${catalogLabel(declaration.catalog)}`
377
+ });
378
+ const created = Object.freeze({
379
+ declaration,
380
+ options: Object.freeze({ ...options }),
381
+ wireNames
382
+ });
383
+ catalogRoutes.add(created);
384
+ return created;
385
+ }
386
+ function catalogWireTypes(catalog) {
387
+ return Object.fromEntries(
388
+ describeCatalog(catalog).entries.flatMap((entry) => {
389
+ const wire = wireNamesOf(catalog).get(entry.name);
390
+ return wire === void 0 ? [] : [[entry.type, wire]];
391
+ })
392
+ );
393
+ }
394
+ function isCatalogRoute(definition) {
395
+ return catalogRoutes.has(definition);
396
+ }
397
+ function expandRoutes(definitions) {
398
+ if (!definitions.some(isCatalogRoute)) {
399
+ return { routes: definitions, wireTypes: /* @__PURE__ */ new Map() };
400
+ }
401
+ const expanded = definitions.flatMap(
402
+ (definition) => isCatalogRoute(definition) ? expandCatalogRoute(definition) : [plainRoute(definition)]
403
+ );
404
+ assertNotRoutedTwice(expanded);
405
+ return {
406
+ routes: expanded.map((item) => item.route),
407
+ wireTypes: mergeRouteWireTypes(
408
+ expanded.flatMap(declaredWireType),
409
+ expanded.map((item) => ({ transport: item.route.transport, type: item.type }))
410
+ )
411
+ };
412
+ }
413
+ function declaredWireType({
414
+ route: { transport },
415
+ type,
416
+ wireType
417
+ }) {
418
+ return wireType === void 0 ? [] : [{ transport, type, wire: wireType }];
419
+ }
420
+ function plainRoute(definition) {
421
+ const { event } = definition;
422
+ const type = typeof event === "string" ? event : event.type;
423
+ return { route: definition, type, fromCatalog: false, wireType: void 0 };
424
+ }
425
+ function expandCatalogRoute({ declaration, options, wireNames }) {
426
+ const catalogNames = wireNamesOf(declaration.catalog);
427
+ const shared = Object.fromEntries(
428
+ Object.entries(options).filter(([name]) => name !== "wireTypes")
429
+ );
430
+ return declaration.entries.map((entry) => ({
431
+ route: { ...shared, event: entry.eventClass },
432
+ type: entry.type,
433
+ fromCatalog: true,
434
+ wireType: wireNames.get(entry.name) ?? catalogNames.get(entry.name)
435
+ }));
436
+ }
437
+ function assertNotRoutedTwice(expanded) {
438
+ const seen = /* @__PURE__ */ new Map();
439
+ for (const item of expanded) {
440
+ const destination = item.route.destination ?? item.type;
441
+ const key = JSON.stringify([item.route.transport, destination, item.type]);
442
+ const earlier = seen.get(key);
443
+ if (earlier !== void 0 && (earlier.fromCatalog || item.fromCatalog)) {
444
+ throw new DuplicateRouteEventError(
445
+ `Event type ${item.type} is routed to '${destination}' of transport '${item.route.transport}' twice`
446
+ );
447
+ }
448
+ seen.set(key, item);
449
+ }
450
+ }
451
+
276
452
  // src/event-store.ts
277
453
  var DELIVERY_STATUSES = [
278
454
  "pending",
@@ -280,6 +456,11 @@ var DELIVERY_STATUSES = [
280
456
  "completed",
281
457
  "dead"
282
458
  ];
459
+ var TRANSACTION_ISOLATION_LEVELS = [
460
+ "read committed",
461
+ "repeatable read",
462
+ "serializable"
463
+ ];
283
464
  var UUID_BYTES = 16;
284
465
  var TIMESTAMP_BYTES = 6;
285
466
  var BYTE_VALUES = 256;
@@ -922,6 +1103,13 @@ var BusLifecycle = class {
922
1103
  markStopped() {
923
1104
  this.#state = "stopped";
924
1105
  }
1106
+ /** Closes the transports a failed start may have connected, within `timeoutMs`. */
1107
+ closeAfterFailedStart(timeoutMs) {
1108
+ return withinDeadline({ clock: this.#parts.clock, timeoutMs }, async (deadline) => {
1109
+ await this.#parts.closeTransports(deadline);
1110
+ return 0;
1111
+ });
1112
+ }
925
1113
  /** The stop of the bus within `timeoutMs`; a later call shares the first one's. */
926
1114
  stop(timeoutMs) {
927
1115
  if (this.#stopping !== void 0) {
@@ -1941,6 +2129,10 @@ function runInIsolatedScope(scope, work) {
1941
2129
  function getCurrentScope() {
1942
2130
  return storage.getStore();
1943
2131
  }
2132
+ function getCurrentIsolationLevel() {
2133
+ const scope = storage.getStore();
2134
+ return scope?.isolation !== void 0 && scope.isolation.transaction === scope.transaction ? scope.isolation.level : void 0;
2135
+ }
1944
2136
 
1945
2137
  // src/dispatch/handler-invocation.ts
1946
2138
  function rejectOnAbort(signal) {
@@ -2698,49 +2890,49 @@ function createRetentionSettings(options) {
2698
2890
  var TRANSPORT_HANDLER_PREFIX = "relaybus.transport";
2699
2891
  function groupRoutes(routes) {
2700
2892
  const groups = /* @__PURE__ */ new Map();
2701
- for (const route of routes) {
2702
- const type = routedType(route.event);
2703
- const destination = route.destination ?? type;
2704
- const id = `${TRANSPORT_HANDLER_PREFIX}:${route.transport}:${destination}`;
2893
+ for (const route2 of routes) {
2894
+ const type = routedType(route2.event);
2895
+ const destination = route2.destination ?? type;
2896
+ const id = `${TRANSPORT_HANDLER_PREFIX}:${route2.transport}:${destination}`;
2705
2897
  const group = groups.get(id);
2706
2898
  if (group === void 0) {
2707
- groups.set(id, createGroup(route, { id, destination, type }));
2899
+ groups.set(id, createGroup(route2, { id, destination, type }));
2708
2900
  } else {
2709
- addRoute(group, { route, type, destination });
2901
+ addRoute(group, { route: route2, type, destination });
2710
2902
  }
2711
2903
  }
2712
2904
  return [...groups.values()];
2713
2905
  }
2714
- function createGroup(route, {
2906
+ function createGroup(route2, {
2715
2907
  id,
2716
2908
  destination,
2717
2909
  type
2718
2910
  }) {
2719
2911
  return {
2720
- transport: route.transport,
2912
+ transport: route2.transport,
2721
2913
  destination,
2722
2914
  id,
2723
2915
  eventTypes: [type],
2724
2916
  legacyIds: [`${id}:${type}`],
2725
- route,
2726
- ...resolveSettings(route, id)
2917
+ route: route2,
2918
+ ...resolveSettings(route2, id)
2727
2919
  };
2728
2920
  }
2729
2921
  function addRoute(group, {
2730
- route,
2922
+ route: route2,
2731
2923
  type,
2732
2924
  destination
2733
2925
  }) {
2734
2926
  const target = `'${group.destination}' of transport '${group.transport}'`;
2735
- if (route.transport !== group.transport) {
2927
+ if (route2.transport !== group.transport) {
2736
2928
  throw new ConfigurationError(
2737
- `Routes to ${target} and to '${destination}' of transport '${route.transport}' would share the handler id '${group.id}'; rename the transport or the destination`
2929
+ `Routes to ${target} and to '${destination}' of transport '${route2.transport}' would share the handler id '${group.id}'; rename the transport or the destination`
2738
2930
  );
2739
2931
  }
2740
2932
  if (group.eventTypes.includes(type)) {
2741
2933
  throw new ConfigurationError(`Event type ${type} is routed to ${target} twice`);
2742
2934
  }
2743
- const { retry, timeoutMs } = resolveSettings(route, group.id);
2935
+ const { retry, timeoutMs } = resolveSettings(route2, group.id);
2744
2936
  if (timeoutMs !== group.timeoutMs || !sameRetryPolicy(retry, group.retry)) {
2745
2937
  throw new ConfigurationError(
2746
2938
  `Routes to ${target} must share retry and timeout, because their events are sent in one queue per key | types=${group.eventTypes[0]},${type}`
@@ -2749,10 +2941,10 @@ function addRoute(group, {
2749
2941
  group.eventTypes.push(type);
2750
2942
  group.legacyIds.push(`${group.id}:${type}`);
2751
2943
  }
2752
- function resolveSettings(route, id) {
2944
+ function resolveSettings(route2, id) {
2753
2945
  return {
2754
- retry: createRetryPolicy(route.retry, DEFAULT_ROUTE_RETRY_POLICY),
2755
- timeoutMs: route.timeout === void 0 ? DEFAULTS.routeTimeoutMs : Milliseconds.forTimer(route.timeout, `Timeout of handler '${id}'`)
2946
+ retry: createRetryPolicy(route2.retry, DEFAULT_ROUTE_RETRY_POLICY),
2947
+ timeoutMs: route2.timeout === void 0 ? DEFAULTS.routeTimeoutMs : Milliseconds.forTimer(route2.timeout, `Timeout of handler '${id}'`)
2756
2948
  };
2757
2949
  }
2758
2950
  function sameRetryPolicy(left, right) {
@@ -2766,11 +2958,35 @@ function routedType(event) {
2766
2958
  return type;
2767
2959
  }
2768
2960
 
2961
+ // src/bus/transport-claims.ts
2962
+ var claims = /* @__PURE__ */ new WeakMap();
2963
+ function claimTransports(transports, declaring) {
2964
+ const next = transports.map((transport) => {
2965
+ const current = claims.get(transport) ?? { buses: 0, declared: false };
2966
+ const claim = {
2967
+ buses: current.buses + 1,
2968
+ declared: current.declared || declaring.has(transport.name)
2969
+ };
2970
+ if (claim.buses > 1 && claim.declared) {
2971
+ throw new WireTypesUnsupportedError(
2972
+ `Transport '${transport.name}' is used by more than one bus, and wire names are declared for it; they would change what the other bus writes. Give each bus its own transport`
2973
+ );
2974
+ }
2975
+ return [transport, claim];
2976
+ });
2977
+ return () => {
2978
+ for (const [transport, claim] of next) {
2979
+ claims.set(transport, claim);
2980
+ }
2981
+ };
2982
+ }
2983
+
2769
2984
  // src/bus/bus-connections.ts
2770
2985
  var BusConnections = class {
2771
2986
  #parts;
2772
2987
  #transports = /* @__PURE__ */ new Map();
2773
2988
  #subscriptions;
2989
+ #pendingWireTypes;
2774
2990
  constructor(parts, subscriptions) {
2775
2991
  this.#parts = parts;
2776
2992
  this.#subscriptions = subscriptions;
@@ -2787,13 +3003,22 @@ var BusConnections = class {
2787
3003
  }
2788
3004
  this.#transports.set(transport.name, transport);
2789
3005
  }
2790
- this.#registerRoutes(routes);
3006
+ const { wireTypes } = this.#registerRoutes(expandRoutes(routes));
2791
3007
  for (const subscription of this.#subscriptions) {
2792
3008
  const transport = this.#requireTransport(subscription.transport);
2793
3009
  if (transport.subscribe === void 0) {
2794
3010
  throw new ConfigurationError(`Transport '${transport.name}' cannot subscribe`);
2795
3011
  }
2796
3012
  }
3013
+ this.#pendingWireTypes = this.#prepareWireTypes(wireTypes);
3014
+ }
3015
+ /**
3016
+ * Applies the wire names the routes declared. The bus calls it last, after every check of its
3017
+ * options, so a refused configuration leaves the transports as they were.
3018
+ */
3019
+ commitWireTypes() {
3020
+ this.#pendingWireTypes?.();
3021
+ this.#pendingWireTypes = void 0;
2797
3022
  }
2798
3023
  /** True while every transport reports itself ready. */
2799
3024
  allReady() {
@@ -2838,18 +3063,42 @@ var BusConnections = class {
2838
3063
  }
2839
3064
  };
2840
3065
  }
2841
- #registerRoutes(routes) {
2842
- for (const route of routes) {
2843
- this.#requireTransport(route.transport);
3066
+ #registerRoutes({ routes, wireTypes }) {
3067
+ for (const route2 of routes) {
3068
+ this.#requireTransport(route2.transport);
2844
3069
  }
2845
3070
  for (const group of groupRoutes(routes)) {
2846
3071
  this.#registerRoute(group);
2847
3072
  }
3073
+ return { wireTypes };
3074
+ }
3075
+ /**
3076
+ * Hands the wire names that catalog routes declare to the transports whose codecs write them:
3077
+ * every transport checks its names first, then all apply them. A transport carries one wire
3078
+ * format, so a transport that two buses use cannot take names declared by either of them.
3079
+ */
3080
+ #prepareWireTypes(wireTypes) {
3081
+ const applies = [...wireTypes].map(([name, types]) => {
3082
+ const transport = this.#transports.get(name);
3083
+ if (transport.declareWireTypes === void 0) {
3084
+ throw new WireTypesUnsupportedError(
3085
+ `Transport '${name}' cannot map event types to wire names, but routes or catalogs declare them for: ${Object.keys(types).join(", ")}`
3086
+ );
3087
+ }
3088
+ return transport.declareWireTypes(types);
3089
+ });
3090
+ const claim = claimTransports([...this.#transports.values()], new Set(wireTypes.keys()));
3091
+ return () => {
3092
+ for (const apply of applies) {
3093
+ apply();
3094
+ }
3095
+ claim();
3096
+ };
2848
3097
  }
2849
3098
  /** Registers one handler for every route to a destination, so a key's events reach it in commit order. */
2850
3099
  #registerRoute(group) {
2851
3100
  const transport = this.#transports.get(group.transport);
2852
- const { route, destination } = group;
3101
+ const { route: route2, destination } = group;
2853
3102
  this.#parts.registry.register(
2854
3103
  {
2855
3104
  eventTypes: group.eventTypes,
@@ -2858,8 +3107,8 @@ var BusConnections = class {
2858
3107
  handle: this.#parts.observation?.sender(transport, destination) ?? ((envelope) => transport.publish(envelope, destination)),
2859
3108
  options: {
2860
3109
  id: group.id,
2861
- ...route.retry === void 0 ? {} : { retry: route.retry },
2862
- ...route.timeout === void 0 ? {} : { timeout: route.timeout }
3110
+ ...route2.retry === void 0 ? {} : { retry: route2.retry },
3111
+ ...route2.timeout === void 0 ? {} : { timeout: route2.timeout }
2863
3112
  },
2864
3113
  forwardsToTransport: true
2865
3114
  },
@@ -2992,25 +3241,37 @@ var EventPublisher = class {
2992
3241
  context,
2993
3242
  delayMs
2994
3243
  }) {
2995
- const { result, committed } = await this.#appendInTransaction(entries, context);
3244
+ const { result, committed, transaction } = await this.#appendInTransaction(entries, context);
2996
3245
  const appended = new Set(result.appendedEventIds);
2997
3246
  const published = entries.map((entry) => entry.event).filter((envelope) => appended.has(envelope.id));
2998
3247
  if (published.length > 0) {
2999
- this.#parts.notifier.notify("onPublished", {
3000
- envelopes: published,
3001
- deliveries: result.appendedDeliveryCount
3002
- });
3003
- if (delayMs === 0) {
3004
- this.#parts.scheduleWake(committed);
3248
+ const report = { envelopes: published, deliveries: result.appendedDeliveryCount };
3249
+ const wake = delayMs === 0 ? committed : void 0;
3250
+ const isolation = context.scope?.isolation;
3251
+ if (!committed && isolation !== void 0 && isolation.transaction === transaction) {
3252
+ isolation.afterCommit.push(() => this.#announce(report, wake));
3253
+ } else {
3254
+ this.#announce(report, wake);
3005
3255
  }
3006
3256
  }
3007
3257
  }
3258
+ /** Tells the observers about a publish, and wakes the dispatcher (`wake` is `committed`) when the events are due now. */
3259
+ #announce(report, wake) {
3260
+ this.#parts.notifier.notify("onPublished", report);
3261
+ if (wake !== void 0) {
3262
+ this.#parts.scheduleWake(wake);
3263
+ }
3264
+ }
3008
3265
  /** Appends in the caller's transaction, else in one of the store's; `committed` tells which. */
3009
3266
  async #appendInTransaction(entries, { options, scope, now }) {
3010
3267
  const { store, transactionMode, transactionResolver } = this.#parts;
3011
3268
  const transaction = options.transaction ?? scope?.transaction ?? transactionResolver?.();
3012
3269
  if (transaction !== void 0) {
3013
- return { result: await store.append(transaction, entries, now), committed: false };
3270
+ return {
3271
+ result: await store.append(transaction, entries, now),
3272
+ committed: false,
3273
+ transaction
3274
+ };
3014
3275
  }
3015
3276
  if (transactionMode === "required") {
3016
3277
  throw new TransactionRequiredError(
@@ -3020,7 +3281,7 @@ var EventPublisher = class {
3020
3281
  const result = await store.transaction(
3021
3282
  (ownTransaction) => store.append(ownTransaction, entries, now)
3022
3283
  );
3023
- return { result, committed: true };
3284
+ return { result, committed: true, transaction: void 0 };
3024
3285
  }
3025
3286
  async #createEnvelope(input) {
3026
3287
  const { event, options, id, scope, now, correlationId } = input;
@@ -3147,6 +3408,8 @@ function assembleRelayBus(options, hooks) {
3147
3408
  settings,
3148
3409
  observation: DispatchObservation.create({ ...core, tracer: options.tracer })
3149
3410
  });
3411
+ const inboxAndOutbox = createInboxAndOutbox(core, hooks);
3412
+ connections.commitWireTypes();
3150
3413
  return {
3151
3414
  ...core,
3152
3415
  maintenance,
@@ -3155,7 +3418,7 @@ function assembleRelayBus(options, hooks) {
3155
3418
  dispatcherEnabled,
3156
3419
  shutdownTimeoutMs: settings.shutdownTimeoutMs,
3157
3420
  dispatcher,
3158
- ...createInboxAndOutbox(core, hooks)
3421
+ ...inboxAndOutbox
3159
3422
  };
3160
3423
  }
3161
3424
  function resolveCore(options) {
@@ -3207,6 +3470,95 @@ function createInboxAndOutbox(core, hooks) {
3207
3470
  };
3208
3471
  }
3209
3472
 
3473
+ // src/transaction-budget.ts
3474
+ async function runBudgetedTransaction(store, { clock, timeoutMs, work, isolationLevel }) {
3475
+ const startedAt = clock.now().getTime();
3476
+ const overrun = () => clock.now().getTime() - startedAt >= timeoutMs;
3477
+ let committing = false;
3478
+ try {
3479
+ return await store.transaction(
3480
+ async (transaction) => {
3481
+ committing = false;
3482
+ const result = await work(transaction);
3483
+ if (overrun()) {
3484
+ throw createTimeoutError(timeoutMs, void 0);
3485
+ }
3486
+ committing = true;
3487
+ return result;
3488
+ },
3489
+ isolationLevel === void 0 ? { timeoutMs } : { timeoutMs, isolationLevel }
3490
+ );
3491
+ } catch (error) {
3492
+ if (error instanceof TransactionTimeoutError || committing || !overrun()) {
3493
+ throw error;
3494
+ }
3495
+ throw createTimeoutError(timeoutMs, error);
3496
+ }
3497
+ }
3498
+ function createTimeoutError(timeoutMs, cause) {
3499
+ const message = `The transaction ran past its ${timeoutMs} ms budget (transactionTimeout) and was rolled back. The usual cause is a slow call to another service (HTTP, gRPC, a broker, a lock) inside the transaction: make external calls before the transaction starts or after it commits, or raise transactionTimeout`;
3500
+ return cause === void 0 ? new TransactionTimeoutError(message) : new TransactionTimeoutError(`${message} | cause=${toErrorInfo(cause).message}`, { cause });
3501
+ }
3502
+
3503
+ // src/isolated-transaction.ts
3504
+ var DEFAULT_ISOLATED_ATTEMPTS = 3;
3505
+ var RERUN_LEVELS = /* @__PURE__ */ new Set([
3506
+ "repeatable read",
3507
+ "serializable"
3508
+ ]);
3509
+ async function runIsolatedTransaction({ store, clock, transactionTimeoutMs }, { options, work }) {
3510
+ const { isolationLevel } = options;
3511
+ assertSupported(store, options);
3512
+ const maxAttempts = attemptsOf(options);
3513
+ for (let attempt = 1; ; attempt += 1) {
3514
+ const afterCommit = [];
3515
+ let committed;
3516
+ try {
3517
+ committed = {
3518
+ value: await runBudgetedTransaction(store, {
3519
+ clock,
3520
+ timeoutMs: transactionTimeoutMs,
3521
+ isolationLevel,
3522
+ work: (transaction) => runInScope(
3523
+ { transaction, isolation: { transaction, level: isolationLevel, afterCommit } },
3524
+ () => work(transaction)
3525
+ )
3526
+ })
3527
+ };
3528
+ } catch (error) {
3529
+ if (attempt >= maxAttempts || error instanceof TransactionTimeoutError || store.isSerializationFailure?.(error) !== true) {
3530
+ throw error;
3531
+ }
3532
+ continue;
3533
+ }
3534
+ for (const announce of afterCommit) {
3535
+ announce();
3536
+ }
3537
+ return committed.value;
3538
+ }
3539
+ }
3540
+ function attemptsOf({ isolationLevel, maxAttempts }) {
3541
+ return RERUN_LEVELS.has(isolationLevel) ? maxAttempts ?? DEFAULT_ISOLATED_ATTEMPTS : 1;
3542
+ }
3543
+ function assertSupported(store, { isolationLevel, maxAttempts }) {
3544
+ if (!TRANSACTION_ISOLATION_LEVELS.includes(isolationLevel)) {
3545
+ throw new ConfigurationError(
3546
+ `Unknown isolationLevel ${JSON.stringify(isolationLevel)}: use ${TRANSACTION_ISOLATION_LEVELS.map((level) => `'${level}'`).join(", ")}`
3547
+ );
3548
+ }
3549
+ if (maxAttempts !== void 0 && !(Number.isInteger(maxAttempts) && maxAttempts >= 1)) {
3550
+ throw new ConfigurationError(
3551
+ `maxAttempts must be a positive integer, got ${String(maxAttempts)}`
3552
+ );
3553
+ }
3554
+ if (store.isolationLevels?.includes(isolationLevel) !== true) {
3555
+ const supported = store.isolationLevels ?? [];
3556
+ throw new ConfigurationError(
3557
+ `The event store does not support isolationLevel '${isolationLevel}' (supported: ${supported.length === 0 ? "none" : supported.join(", ")}). PostgreSQL stores support all levels; MongoDB and the in-memory store run transactions at their own level only`
3558
+ );
3559
+ }
3560
+ }
3561
+
3210
3562
  // src/bus/publish-wake-ups.ts
3211
3563
  var FIRST_WAKE_MS = 25;
3212
3564
  var SECOND_WAKE_MS = 100;
@@ -3259,36 +3611,6 @@ var PublishWakeUps = class {
3259
3611
  }
3260
3612
  };
3261
3613
 
3262
- // src/transaction-budget.ts
3263
- async function runBudgetedTransaction(store, { clock, timeoutMs, work }) {
3264
- const startedAt = clock.now().getTime();
3265
- const overrun = () => clock.now().getTime() - startedAt >= timeoutMs;
3266
- let committing = false;
3267
- try {
3268
- return await store.transaction(
3269
- async (transaction) => {
3270
- committing = false;
3271
- const result = await work(transaction);
3272
- if (overrun()) {
3273
- throw createTimeoutError(timeoutMs, void 0);
3274
- }
3275
- committing = true;
3276
- return result;
3277
- },
3278
- { timeoutMs }
3279
- );
3280
- } catch (error) {
3281
- if (error instanceof TransactionTimeoutError || committing || !overrun()) {
3282
- throw error;
3283
- }
3284
- throw createTimeoutError(timeoutMs, error);
3285
- }
3286
- }
3287
- function createTimeoutError(timeoutMs, cause) {
3288
- const message = `The transaction ran past its ${timeoutMs} ms budget (transactionTimeout) and was rolled back. The usual cause is a slow call to another service (HTTP, gRPC, a broker, a lock) inside the transaction: make external calls before the transaction starts or after it commits, or raise transactionTimeout`;
3289
- return cause === void 0 ? new TransactionTimeoutError(message) : new TransactionTimeoutError(`${message} | cause=${toErrorInfo(cause).message}`, { cause });
3290
- }
3291
-
3292
3614
  // src/relay-bus.ts
3293
3615
  var DEFAULT_DRAIN_BATCHES = 1e3;
3294
3616
  var RelayBus = class {
@@ -3380,8 +3702,16 @@ var RelayBus = class {
3380
3702
  * Runs `work` in a new store transaction. `publish` calls inside it join the
3381
3703
  * transaction without passing it explicitly. The transaction is held to `transactionTimeout`
3382
3704
  * and rolled back with a {@link TransactionTimeoutError} when it runs past it.
3705
+ *
3706
+ * With `options.isolationLevel` it runs at that level, and at `'repeatable read'` or
3707
+ * `'serializable'` a serialization failure reruns `work` in a new transaction (see
3708
+ * {@link TransactionOptions}), so `work` may run more than once. Without options nothing
3709
+ * changes. The call always opens a transaction of its own, also inside another one.
3383
3710
  */
3384
- async transaction(work) {
3711
+ async transaction(work, options) {
3712
+ if (options?.isolationLevel !== void 0) {
3713
+ return runIsolatedTransaction(this.#parts, { options, work });
3714
+ }
3385
3715
  return runBudgetedTransaction(this.#parts.store, {
3386
3716
  clock: this.#parts.clock,
3387
3717
  timeoutMs: this.#parts.transactionTimeoutMs,
@@ -3396,6 +3726,15 @@ var RelayBus = class {
3396
3726
  currentTransaction() {
3397
3727
  return getCurrentScope()?.transaction ?? this.#parts.transactionResolver?.();
3398
3728
  }
3729
+ /**
3730
+ * The isolation level the transaction `publish` would join was opened at by
3731
+ * `bus.transaction({ isolationLevel })` (or `@RelayBusTransaction({ isolationLevel })`);
3732
+ * undefined for every other transaction, including those without a level and the one of
3733
+ * `transactionResolver`.
3734
+ */
3735
+ currentIsolationLevel() {
3736
+ return getCurrentIsolationLevel();
3737
+ }
3399
3738
  /**
3400
3739
  * Stores an event received from a broker and schedules it for local handlers.
3401
3740
  * Idempotent on the event id. Transports call it and acknowledge the message
@@ -3509,7 +3848,7 @@ var RelayBus = class {
3509
3848
  );
3510
3849
  }
3511
3850
  } catch (error) {
3512
- await this.#closeAfterFailedStart();
3851
+ await this.#lifecycle.closeAfterFailedStart(this.#parts.shutdownTimeoutMs);
3513
3852
  this.#lifecycle.markStopped();
3514
3853
  throw error;
3515
3854
  }
@@ -3525,16 +3864,6 @@ var RelayBus = class {
3525
3864
  `[RelayBus](startComponents) relaybus started | handlers=${this.#parts.registry.all().length} dispatcher=${this.#parts.dispatcherEnabled}`
3526
3865
  );
3527
3866
  }
3528
- /** Closes the transports a failed start may have connected, within the shutdown timeout. */
3529
- #closeAfterFailedStart() {
3530
- return withinDeadline(
3531
- { clock: this.#parts.clock, timeoutMs: this.#parts.shutdownTimeoutMs },
3532
- async (deadline) => {
3533
- await this.#closeTransports(deadline);
3534
- return 0;
3535
- }
3536
- );
3537
- }
3538
3867
  #reportUnrouted(envelope) {
3539
3868
  const message = `No handler or route for event type, event not stored | type=${envelope.type} id=${envelope.id}`;
3540
3869
  if (this.#parts.unroutedEvents === "throw") {
@@ -3568,6 +3897,6 @@ var RelayBus = class {
3568
3897
  }
3569
3898
  };
3570
3899
 
3571
- export { CatalogPublisher, DEFAULT_RETRY_POLICY, DEFAULT_ROUTE_RETRY_POLICY, DELIVERY_STATUSES, InMemoryEventStore, InMemoryTransaction, RelayBus, TRACEPARENT_HEADER, TRACESTATE_HEADER, computeRetryDelay, consume, createRetryPolicy, createUuidV7, defineCatalog, defineHandlers, describeCatalog, isRetryableError, produce, systemClock, traceContextOf };
3900
+ export { CatalogPublisher, DEFAULT_RETRY_POLICY, DEFAULT_ROUTE_RETRY_POLICY, DELIVERY_STATUSES, InMemoryEventStore, InMemoryTransaction, RelayBus, TRACEPARENT_HEADER, TRACESTATE_HEADER, TRANSACTION_ISOLATION_LEVELS, catalogWireTypes, computeRetryDelay, consume, createRetryPolicy, createUuidV7, defineCatalog, defineHandlers, describeCatalog, isRetryableError, produce, route, systemClock, traceContextOf };
3572
3901
  //# sourceMappingURL=index.js.map
3573
3902
  //# sourceMappingURL=index.js.map