@furystack/cross-node-bus 1.0.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.
Files changed (83) hide show
  1. package/CHANGELOG.md +78 -0
  2. package/README.md +49 -0
  3. package/esm/cross-node-bus-telemetry.d.ts +74 -0
  4. package/esm/cross-node-bus-telemetry.d.ts.map +1 -0
  5. package/esm/cross-node-bus-telemetry.js +28 -0
  6. package/esm/cross-node-bus-telemetry.js.map +1 -0
  7. package/esm/cross-node-bus-telemetry.spec.d.ts +2 -0
  8. package/esm/cross-node-bus-telemetry.spec.d.ts.map +1 -0
  9. package/esm/cross-node-bus-telemetry.spec.js +115 -0
  10. package/esm/cross-node-bus-telemetry.spec.js.map +1 -0
  11. package/esm/cross-node-bus.d.ts +78 -0
  12. package/esm/cross-node-bus.d.ts.map +1 -0
  13. package/esm/cross-node-bus.js +18 -0
  14. package/esm/cross-node-bus.js.map +1 -0
  15. package/esm/cross-node-bus.spec.d.ts +2 -0
  16. package/esm/cross-node-bus.spec.d.ts.map +1 -0
  17. package/esm/cross-node-bus.spec.js +123 -0
  18. package/esm/cross-node-bus.spec.js.map +1 -0
  19. package/esm/define-in-process-cross-node-bus.d.ts +26 -0
  20. package/esm/define-in-process-cross-node-bus.d.ts.map +1 -0
  21. package/esm/define-in-process-cross-node-bus.js +27 -0
  22. package/esm/define-in-process-cross-node-bus.js.map +1 -0
  23. package/esm/errors.d.ts +21 -0
  24. package/esm/errors.d.ts.map +1 -0
  25. package/esm/errors.js +30 -0
  26. package/esm/errors.js.map +1 -0
  27. package/esm/errors.spec.d.ts +2 -0
  28. package/esm/errors.spec.d.ts.map +1 -0
  29. package/esm/errors.spec.js +30 -0
  30. package/esm/errors.spec.js.map +1 -0
  31. package/esm/in-process-cross-node-bus.d.ts +58 -0
  32. package/esm/in-process-cross-node-bus.d.ts.map +1 -0
  33. package/esm/in-process-cross-node-bus.js +196 -0
  34. package/esm/in-process-cross-node-bus.js.map +1 -0
  35. package/esm/in-process-cross-node-bus.spec.d.ts +2 -0
  36. package/esm/in-process-cross-node-bus.spec.d.ts.map +1 -0
  37. package/esm/in-process-cross-node-bus.spec.js +737 -0
  38. package/esm/in-process-cross-node-bus.spec.js.map +1 -0
  39. package/esm/index.d.ts +8 -0
  40. package/esm/index.d.ts.map +1 -0
  41. package/esm/index.js +7 -0
  42. package/esm/index.js.map +1 -0
  43. package/esm/memory-broker.d.ts +74 -0
  44. package/esm/memory-broker.d.ts.map +1 -0
  45. package/esm/memory-broker.js +156 -0
  46. package/esm/memory-broker.js.map +1 -0
  47. package/esm/memory-broker.spec.d.ts +2 -0
  48. package/esm/memory-broker.spec.d.ts.map +1 -0
  49. package/esm/memory-broker.spec.js +497 -0
  50. package/esm/memory-broker.spec.js.map +1 -0
  51. package/esm/testing/create-in-process-bus-network.d.ts +49 -0
  52. package/esm/testing/create-in-process-bus-network.d.ts.map +1 -0
  53. package/esm/testing/create-in-process-bus-network.js +54 -0
  54. package/esm/testing/create-in-process-bus-network.js.map +1 -0
  55. package/esm/testing/create-in-process-bus-network.spec.d.ts +2 -0
  56. package/esm/testing/create-in-process-bus-network.spec.d.ts.map +1 -0
  57. package/esm/testing/create-in-process-bus-network.spec.js +142 -0
  58. package/esm/testing/create-in-process-bus-network.spec.js.map +1 -0
  59. package/esm/testing/index.d.ts +2 -0
  60. package/esm/testing/index.d.ts.map +1 -0
  61. package/esm/testing/index.js +2 -0
  62. package/esm/testing/index.js.map +1 -0
  63. package/esm/types.d.ts +35 -0
  64. package/esm/types.d.ts.map +1 -0
  65. package/esm/types.js +2 -0
  66. package/esm/types.js.map +1 -0
  67. package/package.json +56 -0
  68. package/src/cross-node-bus-telemetry.spec.ts +44 -0
  69. package/src/cross-node-bus-telemetry.ts +69 -0
  70. package/src/cross-node-bus.spec.ts +41 -0
  71. package/src/cross-node-bus.ts +92 -0
  72. package/src/define-in-process-cross-node-bus.ts +38 -0
  73. package/src/errors.spec.ts +32 -0
  74. package/src/errors.ts +38 -0
  75. package/src/in-process-cross-node-bus.spec.ts +428 -0
  76. package/src/in-process-cross-node-bus.ts +248 -0
  77. package/src/index.ts +7 -0
  78. package/src/memory-broker.spec.ts +282 -0
  79. package/src/memory-broker.ts +199 -0
  80. package/src/testing/create-in-process-bus-network.spec.ts +73 -0
  81. package/src/testing/create-in-process-bus-network.ts +87 -0
  82. package/src/testing/index.ts +1 -0
  83. package/src/types.ts +35 -0
@@ -0,0 +1,27 @@
1
+ import { CrossNodeBusTelemetryToken } from './cross-node-bus-telemetry.js';
2
+ import { InProcessCrossNodeBus } from './in-process-cross-node-bus.js';
3
+ /**
4
+ * Returns a {@link ServiceFactory} bound to {@link CrossNodeBus}. Use it to
5
+ * override the default factory at boot:
6
+ *
7
+ * ```ts
8
+ * injector.bind(
9
+ * CrossNodeBus,
10
+ * defineInProcessCrossNodeBus({ topicPrefix: 'svc-a/' }),
11
+ * )
12
+ * ```
13
+ *
14
+ * Mirrors the `defineXxxCrossNodeBusAdapter` shape future transport
15
+ * adapters expose. Wires telemetry and disposal into the surrounding
16
+ * injector scope.
17
+ */
18
+ export const defineInProcessCrossNodeBus = (options = {}) => {
19
+ return ({ inject, onDispose }) => {
20
+ const telemetry = inject(CrossNodeBusTelemetryToken);
21
+ const bus = new InProcessCrossNodeBus({ ...options, telemetry });
22
+ // eslint-disable-next-line furystack/prefer-using-wrapper -- delegated to onDispose
23
+ onDispose(() => bus[Symbol.dispose]());
24
+ return bus;
25
+ };
26
+ };
27
+ //# sourceMappingURL=define-in-process-cross-node-bus.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"define-in-process-cross-node-bus.js","sourceRoot":"","sources":["../src/define-in-process-cross-node-bus.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,0BAA0B,EAAE,MAAM,+BAA+B,CAAA;AAC1E,OAAO,EAAE,qBAAqB,EAAqC,MAAM,gCAAgC,CAAA;AAUzG;;;;;;;;;;;;;;GAcG;AACH,MAAM,CAAC,MAAM,2BAA2B,GAAG,CACzC,UAA8C,EAAE,EAClB,EAAE;IAChC,OAAO,CAAC,EAAE,MAAM,EAAE,SAAS,EAAE,EAAE,EAAE;QAC/B,MAAM,SAAS,GAAG,MAAM,CAAC,0BAA0B,CAAC,CAAA;QACpD,MAAM,GAAG,GAAG,IAAI,qBAAqB,CAAC,EAAE,GAAG,OAAO,EAAE,SAAS,EAAE,CAAC,CAAA;QAChE,oFAAoF;QACpF,SAAS,CAAC,GAAG,EAAE,CAAC,GAAG,CAAC,MAAM,CAAC,OAAO,CAAC,EAAE,CAAC,CAAA;QACtC,OAAO,GAAG,CAAA;IACZ,CAAC,CAAA;AACH,CAAC,CAAA"}
@@ -0,0 +1,21 @@
1
+ import type { CrossNodeBusCapabilities } from './types.js';
2
+ /**
3
+ * Thrown synchronously by {@link CrossNodeBus.replay} when `fromSeq` falls
4
+ * outside the adapter's retained window. Facades catch this and fall back
5
+ * to a full-snapshot path.
6
+ */
7
+ export declare class ReplayWindowExceededError extends Error {
8
+ readonly topic: string;
9
+ readonly fromSeq: string;
10
+ readonly oldestRetainedSeq: string | undefined;
11
+ constructor(topic: string, fromSeq: string, oldestRetainedSeq: string | undefined);
12
+ }
13
+ /**
14
+ * Thrown synchronously by an adapter when a caller invokes a method that
15
+ * requires a capability the adapter does not advertise.
16
+ */
17
+ export declare class UnsupportedCapabilityError extends Error {
18
+ readonly capability: keyof CrossNodeBusCapabilities;
19
+ constructor(capability: keyof CrossNodeBusCapabilities);
20
+ }
21
+ //# sourceMappingURL=errors.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,wBAAwB,EAAE,MAAM,YAAY,CAAA;AAE1D;;;;GAIG;AACH,qBAAa,yBAA0B,SAAQ,KAAK;IAClD,SAAgB,KAAK,EAAE,MAAM,CAAA;IAC7B,SAAgB,OAAO,EAAE,MAAM,CAAA;IAC/B,SAAgB,iBAAiB,EAAE,MAAM,GAAG,SAAS,CAAA;gBAEzC,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,iBAAiB,EAAE,MAAM,GAAG,SAAS;CAWlF;AAED;;;GAGG;AACH,qBAAa,0BAA2B,SAAQ,KAAK;IACnD,SAAgB,UAAU,EAAE,MAAM,wBAAwB,CAAA;gBAE9C,UAAU,EAAE,MAAM,wBAAwB;CAKvD"}
package/esm/errors.js ADDED
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Thrown synchronously by {@link CrossNodeBus.replay} when `fromSeq` falls
3
+ * outside the adapter's retained window. Facades catch this and fall back
4
+ * to a full-snapshot path.
5
+ */
6
+ export class ReplayWindowExceededError extends Error {
7
+ topic;
8
+ fromSeq;
9
+ oldestRetainedSeq;
10
+ constructor(topic, fromSeq, oldestRetainedSeq) {
11
+ super(`Replay window exceeded for topic "${topic}": requested fromSeq=${fromSeq}, oldest retained=${oldestRetainedSeq ?? 'none'}`);
12
+ this.name = 'ReplayWindowExceededError';
13
+ this.topic = topic;
14
+ this.fromSeq = fromSeq;
15
+ this.oldestRetainedSeq = oldestRetainedSeq;
16
+ }
17
+ }
18
+ /**
19
+ * Thrown synchronously by an adapter when a caller invokes a method that
20
+ * requires a capability the adapter does not advertise.
21
+ */
22
+ export class UnsupportedCapabilityError extends Error {
23
+ capability;
24
+ constructor(capability) {
25
+ super(`Adapter does not support capability: ${capability}`);
26
+ this.name = 'UnsupportedCapabilityError';
27
+ this.capability = capability;
28
+ }
29
+ }
30
+ //# sourceMappingURL=errors.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"errors.js","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAEA;;;;GAIG;AACH,MAAM,OAAO,yBAA0B,SAAQ,KAAK;IAClC,KAAK,CAAQ;IACb,OAAO,CAAQ;IACf,iBAAiB,CAAoB;IAErD,YAAY,KAAa,EAAE,OAAe,EAAE,iBAAqC;QAC/E,KAAK,CACH,qCAAqC,KAAK,wBAAwB,OAAO,qBACvE,iBAAiB,IAAI,MACvB,EAAE,CACH,CAAA;QACD,IAAI,CAAC,IAAI,GAAG,2BAA2B,CAAA;QACvC,IAAI,CAAC,KAAK,GAAG,KAAK,CAAA;QAClB,IAAI,CAAC,OAAO,GAAG,OAAO,CAAA;QACtB,IAAI,CAAC,iBAAiB,GAAG,iBAAiB,CAAA;IAC5C,CAAC;CACF;AAED;;;GAGG;AACH,MAAM,OAAO,0BAA2B,SAAQ,KAAK;IACnC,UAAU,CAAgC;IAE1D,YAAY,UAA0C;QACpD,KAAK,CAAC,wCAAwC,UAAU,EAAE,CAAC,CAAA;QAC3D,IAAI,CAAC,IAAI,GAAG,4BAA4B,CAAA;QACxC,IAAI,CAAC,UAAU,GAAG,UAAU,CAAA;IAC9B,CAAC;CACF"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=errors.spec.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"errors.spec.d.ts","sourceRoot":"","sources":["../src/errors.spec.ts"],"names":[],"mappings":""}
@@ -0,0 +1,30 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { ReplayWindowExceededError, UnsupportedCapabilityError } from './errors.js';
3
+ describe('errors', () => {
4
+ describe('ReplayWindowExceededError', () => {
5
+ it('captures the topic, fromSeq and oldestRetainedSeq', () => {
6
+ const error = new ReplayWindowExceededError('topic', '5', '7');
7
+ expect(error.name).toBe('ReplayWindowExceededError');
8
+ expect(error.topic).toBe('topic');
9
+ expect(error.fromSeq).toBe('5');
10
+ expect(error.oldestRetainedSeq).toBe('7');
11
+ expect(error.message).toContain('topic');
12
+ expect(error.message).toContain('5');
13
+ expect(error.message).toContain('7');
14
+ });
15
+ it('reports "none" when no messages are retained', () => {
16
+ const error = new ReplayWindowExceededError('topic', '5', undefined);
17
+ expect(error.oldestRetainedSeq).toBeUndefined();
18
+ expect(error.message).toContain('none');
19
+ });
20
+ });
21
+ describe('UnsupportedCapabilityError', () => {
22
+ it('captures the missing capability name', () => {
23
+ const error = new UnsupportedCapabilityError('replay');
24
+ expect(error.name).toBe('UnsupportedCapabilityError');
25
+ expect(error.capability).toBe('replay');
26
+ expect(error.message).toContain('replay');
27
+ });
28
+ });
29
+ });
30
+ //# sourceMappingURL=errors.spec.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"errors.spec.js","sourceRoot":"","sources":["../src/errors.spec.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,QAAQ,CAAA;AAC7C,OAAO,EAAE,yBAAyB,EAAE,0BAA0B,EAAE,MAAM,aAAa,CAAA;AAEnF,QAAQ,CAAC,QAAQ,EAAE,GAAG,EAAE;IACtB,QAAQ,CAAC,2BAA2B,EAAE,GAAG,EAAE;QACzC,EAAE,CAAC,mDAAmD,EAAE,GAAG,EAAE;YAC3D,MAAM,KAAK,GAAG,IAAI,yBAAyB,CAAC,OAAO,EAAE,GAAG,EAAE,GAAG,CAAC,CAAA;YAC9D,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,2BAA2B,CAAC,CAAA;YACpD,MAAM,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,IAAI,CAAC,OAAO,CAAC,CAAA;YACjC,MAAM,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAA;YAC/B,MAAM,CAAC,KAAK,CAAC,iBAAiB,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAA;YACzC,MAAM,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,SAAS,CAAC,OAAO,CAAC,CAAA;YACxC,MAAM,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,SAAS,CAAC,GAAG,CAAC,CAAA;YACpC,MAAM,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,SAAS,CAAC,GAAG,CAAC,CAAA;QACtC,CAAC,CAAC,CAAA;QAEF,EAAE,CAAC,8CAA8C,EAAE,GAAG,EAAE;YACtD,MAAM,KAAK,GAAG,IAAI,yBAAyB,CAAC,OAAO,EAAE,GAAG,EAAE,SAAS,CAAC,CAAA;YACpE,MAAM,CAAC,KAAK,CAAC,iBAAiB,CAAC,CAAC,aAAa,EAAE,CAAA;YAC/C,MAAM,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,SAAS,CAAC,MAAM,CAAC,CAAA;QACzC,CAAC,CAAC,CAAA;IACJ,CAAC,CAAC,CAAA;IAEF,QAAQ,CAAC,4BAA4B,EAAE,GAAG,EAAE;QAC1C,EAAE,CAAC,sCAAsC,EAAE,GAAG,EAAE;YAC9C,MAAM,KAAK,GAAG,IAAI,0BAA0B,CAAC,QAAQ,CAAC,CAAA;YACtD,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,4BAA4B,CAAC,CAAA;YACrD,MAAM,CAAC,KAAK,CAAC,UAAU,CAAC,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAA;YACvC,MAAM,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,SAAS,CAAC,QAAQ,CAAC,CAAA;QAC3C,CAAC,CAAC,CAAA;IACJ,CAAC,CAAC,CAAA;AACJ,CAAC,CAAC,CAAA"}
@@ -0,0 +1,58 @@
1
+ import type { CrossNodeBusTelemetry } from './cross-node-bus-telemetry.js';
2
+ import { MemoryBroker } from './memory-broker.js';
3
+ import type { CrossNodeBus } from './cross-node-bus.js';
4
+ import type { BusMessage, CrossNodeBusCapabilities } from './types.js';
5
+ /**
6
+ * Options accepted by {@link InProcessCrossNodeBus}.
7
+ */
8
+ export type InProcessCrossNodeBusOptions = {
9
+ /**
10
+ * Shared {@link MemoryBroker}. When omitted a private broker is minted —
11
+ * single-instance behavior matches an isolated EventHub. The testing
12
+ * harness passes the same broker to N buses so they can observe each
13
+ * other's publishes without an external transport.
14
+ */
15
+ broker?: MemoryBroker;
16
+ /** Stable, per-process identifier. Defaults to `local-${crypto.randomUUID()}`. */
17
+ nodeId?: string;
18
+ /**
19
+ * Wire-level prefix applied to every topic on `publish` / `subscribe`.
20
+ * Defaults to `''` (no prefix). Multi-service simulations pick distinct
21
+ * prefixes per bus so cross-service eavesdrop can be exercised against a
22
+ * single shared broker.
23
+ */
24
+ topicPrefix?: string;
25
+ /**
26
+ * Convenience for the common case of "one bus, fresh private broker, this
27
+ * many retained messages per topic". Ignored when `broker` is provided.
28
+ */
29
+ replayWindow?: number;
30
+ /** Sink for `onCrossNodePublished` / `onCrossNodeReceived` / `onCrossNodeError`. */
31
+ telemetry?: CrossNodeBusTelemetry;
32
+ };
33
+ /**
34
+ * In-process default {@link CrossNodeBus} adapter. Backs single-node
35
+ * deployments out of the box and powers the multi-instance testing harness
36
+ * exposed at `@furystack/cross-node-bus/testing`.
37
+ *
38
+ * Local fan-out is multiplexed: regardless of how many handlers subscribe
39
+ * to a single wire topic, the bus opens exactly one broker subscription and
40
+ * dispatches arrivals to its own handler set. This keeps `onCrossNodeReceived`
41
+ * counting one event per arrival rather than per handler invocation, and
42
+ * mirrors the consumer-group shape future network adapters will use.
43
+ */
44
+ export declare class InProcessCrossNodeBus implements CrossNodeBus {
45
+ #private;
46
+ readonly nodeId: string;
47
+ readonly capabilities: CrossNodeBusCapabilities;
48
+ constructor(options?: InProcessCrossNodeBusOptions);
49
+ publish(topic: string, payload: unknown): Promise<void>;
50
+ subscribe(topic: string, handler: (message: BusMessage) => void): Disposable;
51
+ subscribeRemoteOnly(topic: string, handler: (message: BusMessage) => void): Disposable;
52
+ subscribeForeign(prefix: string, topic: string, handler: (message: BusMessage) => void): Disposable;
53
+ replay(topic: string, fromSeq: string): AsyncIterable<BusMessage>;
54
+ compareSeq(a: string, b: string): number;
55
+ oldestSeq(topic: string): string | undefined;
56
+ [Symbol.dispose](): void;
57
+ }
58
+ //# sourceMappingURL=in-process-cross-node-bus.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"in-process-cross-node-bus.d.ts","sourceRoot":"","sources":["../src/in-process-cross-node-bus.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,+BAA+B,CAAA;AAC1E,OAAO,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAA;AACjD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAA;AACvD,OAAO,KAAK,EAAE,UAAU,EAAE,wBAAwB,EAAE,MAAM,YAAY,CAAA;AAEtE;;GAEG;AACH,MAAM,MAAM,4BAA4B,GAAG;IACzC;;;;;OAKG;IACH,MAAM,CAAC,EAAE,YAAY,CAAA;IACrB,kFAAkF;IAClF,MAAM,CAAC,EAAE,MAAM,CAAA;IACf;;;;;OAKG;IACH,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB;;;OAGG;IACH,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,oFAAoF;IACpF,SAAS,CAAC,EAAE,qBAAqB,CAAA;CAClC,CAAA;AAcD;;;;;;;;;;GAUG;AACH,qBAAa,qBAAsB,YAAW,YAAY;;IACxD,SAAgB,MAAM,EAAE,MAAM,CAAA;IAC9B,SAAgB,YAAY,EAAE,wBAAwB,CAAe;gBAczD,OAAO,GAAE,4BAAiC;IAkBzC,OAAO,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC,IAAI,CAAC;IAsB7D,SAAS,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,CAAC,OAAO,EAAE,UAAU,KAAK,IAAI,GAAG,UAAU;IAK5E,mBAAmB,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,CAAC,OAAO,EAAE,UAAU,KAAK,IAAI,GAAG,UAAU;IAMtF,gBAAgB,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,CAAC,OAAO,EAAE,UAAU,KAAK,IAAI,GAAG,UAAU;IAKnG,MAAM,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,aAAa,CAAC,UAAU,CAAC;IAUjE,UAAU,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,GAAG,MAAM;IAIxC,SAAS,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS;IAK5C,CAAC,MAAM,CAAC,OAAO,CAAC,IAAI,IAAI;CAkGhC"}
@@ -0,0 +1,196 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import { MemoryBroker } from './memory-broker.js';
3
+ const CAPABILITIES = Object.freeze({
4
+ persistent: false,
5
+ replay: true,
6
+ assignsSequence: true,
7
+ });
8
+ /**
9
+ * In-process default {@link CrossNodeBus} adapter. Backs single-node
10
+ * deployments out of the box and powers the multi-instance testing harness
11
+ * exposed at `@furystack/cross-node-bus/testing`.
12
+ *
13
+ * Local fan-out is multiplexed: regardless of how many handlers subscribe
14
+ * to a single wire topic, the bus opens exactly one broker subscription and
15
+ * dispatches arrivals to its own handler set. This keeps `onCrossNodeReceived`
16
+ * counting one event per arrival rather than per handler invocation, and
17
+ * mirrors the consumer-group shape future network adapters will use.
18
+ */
19
+ export class InProcessCrossNodeBus {
20
+ nodeId;
21
+ capabilities = CAPABILITIES;
22
+ #broker;
23
+ #ownsBroker;
24
+ #topicPrefix;
25
+ #telemetry;
26
+ /** displayTopic per wire topic — keyed by wire string for O(1) routing. */
27
+ #localHandlers = new Map();
28
+ /** Live broker subscription per wire topic, opened on first local handler. */
29
+ #brokerHandles = new Map();
30
+ #disposed = false;
31
+ constructor(options = {}) {
32
+ this.#ownsBroker = options.broker === undefined;
33
+ this.nodeId = options.nodeId ?? `local-${randomUUID()}`;
34
+ this.#topicPrefix = options.topicPrefix ?? '';
35
+ this.#telemetry = options.telemetry;
36
+ this.#broker =
37
+ options.broker ??
38
+ new MemoryBroker({
39
+ replayWindow: options.replayWindow,
40
+ onEviction: (wire, evictedSeq, retainedCount) => this.#telemetry?.emit('onCrossNodeWindowEvicted', {
41
+ topic: this.#displayTopicFor(wire),
42
+ evictedSeq,
43
+ retainedCount,
44
+ }),
45
+ });
46
+ }
47
+ async publish(topic, payload) {
48
+ this.#ensureLive();
49
+ const wire = this.#wireTopic(topic);
50
+ let byteLength = 0;
51
+ if (this.#telemetry) {
52
+ try {
53
+ byteLength = Buffer.byteLength(JSON.stringify(payload) ?? '');
54
+ }
55
+ catch (error) {
56
+ this.#telemetry.emit('onCrossNodeError', { topic, error, phase: 'serialize' });
57
+ }
58
+ }
59
+ try {
60
+ this.#broker.publish(wire, this.nodeId, payload, (error) => {
61
+ this.#telemetry?.emit('onCrossNodeError', { topic, error, phase: 'subscribe' });
62
+ });
63
+ }
64
+ catch (error) {
65
+ this.#telemetry?.emit('onCrossNodeError', { topic, error, phase: 'publish' });
66
+ throw error;
67
+ }
68
+ this.#telemetry?.emit('onCrossNodePublished', { topic, originId: this.nodeId, byteLength });
69
+ }
70
+ subscribe(topic, handler) {
71
+ this.#ensureLive();
72
+ return this.#subscribeWire(this.#wireTopic(topic), topic, handler, 'subscribe');
73
+ }
74
+ subscribeRemoteOnly(topic, handler) {
75
+ return this.subscribe(topic, (message) => {
76
+ if (message.originId !== this.nodeId)
77
+ handler(message);
78
+ });
79
+ }
80
+ subscribeForeign(prefix, topic, handler) {
81
+ this.#ensureLive();
82
+ return this.#subscribeWire(`${prefix}${topic}`, topic, handler, 'subscribeForeign');
83
+ }
84
+ replay(topic, fromSeq) {
85
+ this.#ensureLive();
86
+ try {
87
+ return this.#broker.replay(this.#wireTopic(topic), fromSeq);
88
+ }
89
+ catch (error) {
90
+ this.#telemetry?.emit('onCrossNodeError', { topic, error, phase: 'replay' });
91
+ throw error;
92
+ }
93
+ }
94
+ compareSeq(a, b) {
95
+ return Number(a) - Number(b);
96
+ }
97
+ oldestSeq(topic) {
98
+ this.#ensureLive();
99
+ return this.#broker.oldestSeq(this.#wireTopic(topic));
100
+ }
101
+ [Symbol.dispose]() {
102
+ if (this.#disposed)
103
+ return;
104
+ this.#disposed = true;
105
+ for (const handle of this.#brokerHandles.values()) {
106
+ handle[Symbol.dispose]();
107
+ }
108
+ this.#brokerHandles.clear();
109
+ this.#localHandlers.clear();
110
+ if (this.#ownsBroker) {
111
+ this.#broker[Symbol.dispose]();
112
+ }
113
+ }
114
+ #wireTopic(topic) {
115
+ return `${this.#topicPrefix}${topic}`;
116
+ }
117
+ /**
118
+ * Reverse-resolve the caller-facing display topic for a wire topic. When
119
+ * local handlers exist their displayTopic is authoritative — `subscribe`
120
+ * always yields `prefix+t` and `subscribeForeign` always yields `p+t`, so
121
+ * the first entry is representative. Falls back to the wire string when no
122
+ * subscriber on this bus has registered (publish-only buses never need to
123
+ * round-trip a display name; the wire is the most accurate label).
124
+ */
125
+ #displayTopicFor(wire) {
126
+ const handlers = this.#localHandlers.get(wire);
127
+ if (!handlers)
128
+ return wire;
129
+ const first = handlers.values().next().value;
130
+ return first ? first.displayTopic : wire;
131
+ }
132
+ #subscribeWire(wire, displayTopic, handler, phase) {
133
+ const entry = { handler, displayTopic };
134
+ let handlers = this.#localHandlers.get(wire);
135
+ if (!handlers) {
136
+ handlers = new Set();
137
+ this.#localHandlers.set(wire, handlers);
138
+ try {
139
+ const brokerHandle = this.#broker.subscribe(wire, (message) => this.#deliver(wire, message));
140
+ this.#brokerHandles.set(wire, brokerHandle);
141
+ }
142
+ catch (error) {
143
+ this.#localHandlers.delete(wire);
144
+ this.#telemetry?.emit('onCrossNodeError', { topic: displayTopic, error, phase });
145
+ throw error;
146
+ }
147
+ }
148
+ handlers.add(entry);
149
+ return {
150
+ [Symbol.dispose]: () => {
151
+ const current = this.#localHandlers.get(wire);
152
+ if (!current)
153
+ return;
154
+ current.delete(entry);
155
+ if (current.size === 0) {
156
+ this.#localHandlers.delete(wire);
157
+ this.#brokerHandles.get(wire)?.[Symbol.dispose]();
158
+ this.#brokerHandles.delete(wire);
159
+ }
160
+ },
161
+ };
162
+ }
163
+ #deliver(wire, message) {
164
+ const handlers = this.#localHandlers.get(wire);
165
+ if (!handlers || handlers.size === 0)
166
+ return;
167
+ if (this.#telemetry) {
168
+ // Multiple subscribers on the same wire share a displayTopic that maps
169
+ // 1:1 to the wire string: `subscribe(t)` always yields prefix+t and
170
+ // `subscribeForeign(p, t)` always yields p+t. The first entry's
171
+ // displayTopic is therefore representative.
172
+ const first = handlers.values().next().value;
173
+ if (first) {
174
+ this.#telemetry.emit('onCrossNodeReceived', {
175
+ topic: first.displayTopic,
176
+ originId: message.originId,
177
+ lagMs: Date.now() - Date.parse(message.emittedAt),
178
+ });
179
+ }
180
+ }
181
+ for (const entry of handlers) {
182
+ try {
183
+ entry.handler(message);
184
+ }
185
+ catch (error) {
186
+ this.#telemetry?.emit('onCrossNodeError', { topic: entry.displayTopic, error, phase: 'subscribe' });
187
+ }
188
+ }
189
+ }
190
+ #ensureLive() {
191
+ if (this.#disposed) {
192
+ throw new Error('InProcessCrossNodeBus has been disposed');
193
+ }
194
+ }
195
+ }
196
+ //# sourceMappingURL=in-process-cross-node-bus.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"in-process-cross-node-bus.js","sourceRoot":"","sources":["../src/in-process-cross-node-bus.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=in-process-cross-node-bus.spec.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"in-process-cross-node-bus.spec.d.ts","sourceRoot":"","sources":["../src/in-process-cross-node-bus.spec.ts"],"names":[],"mappings":""}