@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.
- package/CHANGELOG.md +78 -0
- package/README.md +49 -0
- package/esm/cross-node-bus-telemetry.d.ts +74 -0
- package/esm/cross-node-bus-telemetry.d.ts.map +1 -0
- package/esm/cross-node-bus-telemetry.js +28 -0
- package/esm/cross-node-bus-telemetry.js.map +1 -0
- package/esm/cross-node-bus-telemetry.spec.d.ts +2 -0
- package/esm/cross-node-bus-telemetry.spec.d.ts.map +1 -0
- package/esm/cross-node-bus-telemetry.spec.js +115 -0
- package/esm/cross-node-bus-telemetry.spec.js.map +1 -0
- package/esm/cross-node-bus.d.ts +78 -0
- package/esm/cross-node-bus.d.ts.map +1 -0
- package/esm/cross-node-bus.js +18 -0
- package/esm/cross-node-bus.js.map +1 -0
- package/esm/cross-node-bus.spec.d.ts +2 -0
- package/esm/cross-node-bus.spec.d.ts.map +1 -0
- package/esm/cross-node-bus.spec.js +123 -0
- package/esm/cross-node-bus.spec.js.map +1 -0
- package/esm/define-in-process-cross-node-bus.d.ts +26 -0
- package/esm/define-in-process-cross-node-bus.d.ts.map +1 -0
- package/esm/define-in-process-cross-node-bus.js +27 -0
- package/esm/define-in-process-cross-node-bus.js.map +1 -0
- package/esm/errors.d.ts +21 -0
- package/esm/errors.d.ts.map +1 -0
- package/esm/errors.js +30 -0
- package/esm/errors.js.map +1 -0
- package/esm/errors.spec.d.ts +2 -0
- package/esm/errors.spec.d.ts.map +1 -0
- package/esm/errors.spec.js +30 -0
- package/esm/errors.spec.js.map +1 -0
- package/esm/in-process-cross-node-bus.d.ts +58 -0
- package/esm/in-process-cross-node-bus.d.ts.map +1 -0
- package/esm/in-process-cross-node-bus.js +196 -0
- package/esm/in-process-cross-node-bus.js.map +1 -0
- package/esm/in-process-cross-node-bus.spec.d.ts +2 -0
- package/esm/in-process-cross-node-bus.spec.d.ts.map +1 -0
- package/esm/in-process-cross-node-bus.spec.js +737 -0
- package/esm/in-process-cross-node-bus.spec.js.map +1 -0
- package/esm/index.d.ts +8 -0
- package/esm/index.d.ts.map +1 -0
- package/esm/index.js +7 -0
- package/esm/index.js.map +1 -0
- package/esm/memory-broker.d.ts +74 -0
- package/esm/memory-broker.d.ts.map +1 -0
- package/esm/memory-broker.js +156 -0
- package/esm/memory-broker.js.map +1 -0
- package/esm/memory-broker.spec.d.ts +2 -0
- package/esm/memory-broker.spec.d.ts.map +1 -0
- package/esm/memory-broker.spec.js +497 -0
- package/esm/memory-broker.spec.js.map +1 -0
- package/esm/testing/create-in-process-bus-network.d.ts +49 -0
- package/esm/testing/create-in-process-bus-network.d.ts.map +1 -0
- package/esm/testing/create-in-process-bus-network.js +54 -0
- package/esm/testing/create-in-process-bus-network.js.map +1 -0
- package/esm/testing/create-in-process-bus-network.spec.d.ts +2 -0
- package/esm/testing/create-in-process-bus-network.spec.d.ts.map +1 -0
- package/esm/testing/create-in-process-bus-network.spec.js +142 -0
- package/esm/testing/create-in-process-bus-network.spec.js.map +1 -0
- package/esm/testing/index.d.ts +2 -0
- package/esm/testing/index.d.ts.map +1 -0
- package/esm/testing/index.js +2 -0
- package/esm/testing/index.js.map +1 -0
- package/esm/types.d.ts +35 -0
- package/esm/types.d.ts.map +1 -0
- package/esm/types.js +2 -0
- package/esm/types.js.map +1 -0
- package/package.json +56 -0
- package/src/cross-node-bus-telemetry.spec.ts +44 -0
- package/src/cross-node-bus-telemetry.ts +69 -0
- package/src/cross-node-bus.spec.ts +41 -0
- package/src/cross-node-bus.ts +92 -0
- package/src/define-in-process-cross-node-bus.ts +38 -0
- package/src/errors.spec.ts +32 -0
- package/src/errors.ts +38 -0
- package/src/in-process-cross-node-bus.spec.ts +428 -0
- package/src/in-process-cross-node-bus.ts +248 -0
- package/src/index.ts +7 -0
- package/src/memory-broker.spec.ts +282 -0
- package/src/memory-broker.ts +199 -0
- package/src/testing/create-in-process-bus-network.spec.ts +73 -0
- package/src/testing/create-in-process-bus-network.ts +87 -0
- package/src/testing/index.ts +1 -0
- package/src/types.ts +35 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## [1.0.0] - 2026-05-21
|
|
4
|
+
|
|
5
|
+
### 💥 Breaking Changes
|
|
6
|
+
|
|
7
|
+
### Initial 1.0.0 release
|
|
8
|
+
|
|
9
|
+
First public release of `@furystack/cross-node-bus` — a transport-agnostic publish/subscribe primitive for FuryStack apps that scale beyond a single process. There is no migration path from a previous version because none exists; this section is required by the major-release contract.
|
|
10
|
+
|
|
11
|
+
### ✨ Features
|
|
12
|
+
|
|
13
|
+
### `CrossNodeBus` — shared, typed, multi-node event bus
|
|
14
|
+
|
|
15
|
+
A pluggable pub/sub primitive that lets every subsystem coordinate across nodes via the same shared facade. Subsystems and apps build typed wrappers (e.g. `IdentityEventBus`, `EntityChangeBus`) on top of one bus instance per injector tree.
|
|
16
|
+
|
|
17
|
+
```typescript
|
|
18
|
+
import { CrossNodeBus } from '@furystack/cross-node-bus'
|
|
19
|
+
|
|
20
|
+
const bus = injector.get(CrossNodeBus)
|
|
21
|
+
using sub = bus.subscribe('my-topic', ({ payload, originId }) => {
|
|
22
|
+
console.log('received', payload, 'from', originId)
|
|
23
|
+
})
|
|
24
|
+
await bus.publish('my-topic', { hello: 'world' })
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Self-delivery is on by default: a publisher receives its own messages. Subscribers that only care about sibling traffic use `subscribeRemoteOnly(topic, handler)`.
|
|
28
|
+
|
|
29
|
+
### In-process default adapter
|
|
30
|
+
|
|
31
|
+
`CrossNodeBus` resolves to an in-process implementation by default — single-node deployments work without any configuration. The default adapter:
|
|
32
|
+
|
|
33
|
+
- Assigns numeric monotonic seq tokens per topic.
|
|
34
|
+
- Retains the last 1 000 messages per topic for replay.
|
|
35
|
+
- Implements `replay`, `oldestSeq`, and `compareSeq` so facades can do delta sync against the in-process bus the same way they would against Redis.
|
|
36
|
+
|
|
37
|
+
Multi-node deployments override the binding with a transport adapter (see `@furystack/redis-cross-node-bus`).
|
|
38
|
+
|
|
39
|
+
### Capability flags + fail-loud registration
|
|
40
|
+
|
|
41
|
+
Every adapter exposes a `capabilities` descriptor (`persistent`, `replay`, `assignsSequence`). Facades assert the flags they need at registration time so misconfigured deployments fail loudly rather than serving stale data.
|
|
42
|
+
|
|
43
|
+
```typescript
|
|
44
|
+
const bus = injector.get(CrossNodeBus)
|
|
45
|
+
if (!bus.capabilities.replay) {
|
|
46
|
+
throw new Error('This facade requires replay-capable transport')
|
|
47
|
+
}
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
### `replay()` + `compareSeq()` + `oldestSeq()` for delta sync
|
|
51
|
+
|
|
52
|
+
Adapters that retain a window of past messages let consumers reconnect with a `lastSeq` and replay just the gap. `compareSeq` lets facades order seq tokens without leaking the adapter-specific encoding (in-process: integer counters; Redis Streams: `<ms>-<n>`). When the requested seq predates the retained window, `replay()` throws `ReplayWindowExceededError` so facades fall back to a snapshot.
|
|
53
|
+
|
|
54
|
+
### `subscribeForeign()` for explicit cross-service eavesdrop
|
|
55
|
+
|
|
56
|
+
Cross-service traffic is opt-in and greppable: a service that wants to observe another service's topic prefix calls `subscribeForeign(prefix, topic, handler)`. Adapters that lack the underlying capability throw at registration time. There is no implicit cross-service fan-out.
|
|
57
|
+
|
|
58
|
+
### Test helper: `createInProcessBusNetwork`
|
|
59
|
+
|
|
60
|
+
Exposed under `@furystack/cross-node-bus/testing`. Mints N in-process `CrossNodeBus` instances backed by a single shared `MemoryBroker`, so multi-node behaviour can be unit-tested without spinning up a real broker:
|
|
61
|
+
|
|
62
|
+
```typescript
|
|
63
|
+
import { createInProcessBusNetwork } from '@furystack/cross-node-bus/testing'
|
|
64
|
+
|
|
65
|
+
using network = createInProcessBusNetwork({ count: 3 })
|
|
66
|
+
const [a, b, c] = network.buses
|
|
67
|
+
// a.publish(...) is observed by b and c
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
### Telemetry hooks
|
|
71
|
+
|
|
72
|
+
`CrossNodeBusTelemetry` emits `onCrossNodePublished`, `onCrossNodeReceived`, `onCrossNodeError`, and `onCrossNodeWindowEvicted` events with `topic`, `originId`, `byteLength` / `lagMs` / error-and-phase / eviction context — wire it into existing logging without touching adapter code. The shared sink is exposed via `CrossNodeBusTelemetryToken` so adapter factories (in-process or transport-specific) inject the same hub.
|
|
73
|
+
|
|
74
|
+
`onCrossNodeWindowEvicted` only fires for adapters that own their replay buffer (the in-process default today). Network-broker adapters that delegate trimming to the broker — Redis Streams' `MAXLEN`, NATS JetStream's max-bytes — cannot observe individual evictions on the client side; consumers needing that signal should read it from the broker's native metrics.
|
|
75
|
+
|
|
76
|
+
### ⬆️ Dependencies
|
|
77
|
+
|
|
78
|
+
- Bumped `@types/node` to `^25.9.1` and `vitest` to `^4.1.7`. No source changes — dev-tooling bump only.
|
package/README.md
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# @furystack/cross-node-bus
|
|
2
|
+
|
|
3
|
+
Transport-agnostic publish/subscribe primitive for FuryStack. Provides the
|
|
4
|
+
`CrossNodeBus` interface, a default in-process adapter, and a testing
|
|
5
|
+
harness for multi-instance scenarios. Concrete cross-process adapters
|
|
6
|
+
(Redis Streams, NATS, …) ship in their own packages.
|
|
7
|
+
|
|
8
|
+
See `docs/internal/cross-node-bus-spike.md` for the full design.
|
|
9
|
+
|
|
10
|
+
## Installation
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
npm install @furystack/cross-node-bus
|
|
14
|
+
# or
|
|
15
|
+
yarn add @furystack/cross-node-bus
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## Usage
|
|
19
|
+
|
|
20
|
+
The default factory resolves an `InProcessCrossNodeBus`. Single-node
|
|
21
|
+
deployments work without further configuration. Multi-node deployments
|
|
22
|
+
bind a transport adapter such as `@furystack/redis-cross-node-bus`.
|
|
23
|
+
|
|
24
|
+
```ts
|
|
25
|
+
import { createInjector } from '@furystack/inject'
|
|
26
|
+
import { CrossNodeBus } from '@furystack/cross-node-bus'
|
|
27
|
+
|
|
28
|
+
await using injector = createInjector()
|
|
29
|
+
const bus = injector.get(CrossNodeBus)
|
|
30
|
+
|
|
31
|
+
using sub = bus.subscribe('my-topic', (message) => {
|
|
32
|
+
console.log(message.originId, message.payload)
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
await bus.publish('my-topic', { hello: 'world' })
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
### Testing harness
|
|
39
|
+
|
|
40
|
+
The `@furystack/cross-node-bus/testing` subpath ships a multi-instance
|
|
41
|
+
in-process harness so facade authors can write multi-node integration
|
|
42
|
+
tests without spinning up a broker.
|
|
43
|
+
|
|
44
|
+
```ts
|
|
45
|
+
import { createInProcessBusNetwork } from '@furystack/cross-node-bus/testing'
|
|
46
|
+
|
|
47
|
+
using network = createInProcessBusNetwork({ count: 2 })
|
|
48
|
+
const [a, b] = network.buses
|
|
49
|
+
```
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { type Token } from '@furystack/inject';
|
|
2
|
+
import { EventHub, type ListenerErrorPayload } from '@furystack/utils';
|
|
3
|
+
/**
|
|
4
|
+
* Phase of the bus pipeline an `onCrossNodeError` event refers to. `serialize`
|
|
5
|
+
* is reserved for future adapters that fail when JSON-encoding payloads;
|
|
6
|
+
* the in-process default never produces it.
|
|
7
|
+
*/
|
|
8
|
+
export type CrossNodeBusErrorPhase = 'publish' | 'subscribe' | 'subscribeForeign' | 'replay' | 'serialize';
|
|
9
|
+
/**
|
|
10
|
+
* Union of telemetry signals emitted by every {@link CrossNodeBus} adapter.
|
|
11
|
+
* Each adapter forwards into the same hub so subscribers can observe the
|
|
12
|
+
* whole bus surface from one place — independent of which transport is
|
|
13
|
+
* bound.
|
|
14
|
+
*/
|
|
15
|
+
export type CrossNodeBusTelemetryEvents = {
|
|
16
|
+
/** Fired after a message has been accepted by the transport. */
|
|
17
|
+
onCrossNodePublished: {
|
|
18
|
+
topic: string;
|
|
19
|
+
originId: string;
|
|
20
|
+
byteLength: number;
|
|
21
|
+
};
|
|
22
|
+
/**
|
|
23
|
+
* Fired once per message arriving at the bus, before local fan-out.
|
|
24
|
+
* `lagMs = Date.now() - Date.parse(message.emittedAt)`; clock skew can
|
|
25
|
+
* produce negative values, which adapters report verbatim.
|
|
26
|
+
*/
|
|
27
|
+
onCrossNodeReceived: {
|
|
28
|
+
topic: string;
|
|
29
|
+
originId: string;
|
|
30
|
+
lagMs: number;
|
|
31
|
+
};
|
|
32
|
+
onCrossNodeError: {
|
|
33
|
+
topic: string;
|
|
34
|
+
error: unknown;
|
|
35
|
+
phase: CrossNodeBusErrorPhase;
|
|
36
|
+
};
|
|
37
|
+
/**
|
|
38
|
+
* Fired when an adapter that owns its replay buffer drops a retained
|
|
39
|
+
* message to honor the configured replay window. Operators alert on the
|
|
40
|
+
* trend so the window can be tuned before reconnecting clients start
|
|
41
|
+
* hitting `ReplayWindowExceededError`.
|
|
42
|
+
*
|
|
43
|
+
* Only adapters that own the buffer emit this signal — today that is
|
|
44
|
+
* {@link InProcessCrossNodeBus} via {@link MemoryBroker}. Network-broker
|
|
45
|
+
* adapters that delegate trimming to the broker (Redis Streams' `MAXLEN`,
|
|
46
|
+
* NATS JetStream's max-bytes) cannot observe individual evictions on the
|
|
47
|
+
* client side; consumers needing that signal should read it from the
|
|
48
|
+
* broker's native metrics (e.g. `redis_streams_length` from the Prom
|
|
49
|
+
* exporter).
|
|
50
|
+
*/
|
|
51
|
+
onCrossNodeWindowEvicted: {
|
|
52
|
+
topic: string;
|
|
53
|
+
evictedSeq: string;
|
|
54
|
+
retainedCount: number;
|
|
55
|
+
};
|
|
56
|
+
onListenerError: ListenerErrorPayload;
|
|
57
|
+
};
|
|
58
|
+
/**
|
|
59
|
+
* Application-facing telemetry surface for the cross-node bus. Subscribers
|
|
60
|
+
* use the standard {@link EventHub} `addListener` / `subscribe` API.
|
|
61
|
+
*/
|
|
62
|
+
export declare class CrossNodeBusTelemetry extends EventHub<CrossNodeBusTelemetryEvents> {
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* DI token for the shared {@link CrossNodeBusTelemetry} instance.
|
|
66
|
+
*
|
|
67
|
+
* Singleton because the bus token itself is singleton — co-locating both at
|
|
68
|
+
* the root injector keeps every override factory (including transport
|
|
69
|
+
* adapters) free to inject telemetry without lifetime-compatibility
|
|
70
|
+
* gymnastics. Each test still gets isolation by minting its own root
|
|
71
|
+
* injector with `createInjector()`.
|
|
72
|
+
*/
|
|
73
|
+
export declare const CrossNodeBusTelemetryToken: Token<CrossNodeBusTelemetry, 'singleton'>;
|
|
74
|
+
//# sourceMappingURL=cross-node-bus-telemetry.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cross-node-bus-telemetry.d.ts","sourceRoot":"","sources":["../src/cross-node-bus-telemetry.ts"],"names":[],"mappings":"AAAA,OAAO,EAAiB,KAAK,KAAK,EAAE,MAAM,mBAAmB,CAAA;AAC7D,OAAO,EAAE,QAAQ,EAAE,KAAK,oBAAoB,EAAE,MAAM,kBAAkB,CAAA;AAEtE;;;;GAIG;AACH,MAAM,MAAM,sBAAsB,GAAG,SAAS,GAAG,WAAW,GAAG,kBAAkB,GAAG,QAAQ,GAAG,WAAW,CAAA;AAE1G;;;;;GAKG;AACH,MAAM,MAAM,2BAA2B,GAAG;IACxC,gEAAgE;IAChE,oBAAoB,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAC;QAAC,UAAU,EAAE,MAAM,CAAA;KAAE,CAAA;IAC7E;;;;OAIG;IACH,mBAAmB,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAA;IACvE,gBAAgB,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,OAAO,CAAC;QAAC,KAAK,EAAE,sBAAsB,CAAA;KAAE,CAAA;IAClF;;;;;;;;;;;;;OAaG;IACH,wBAAwB,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,UAAU,EAAE,MAAM,CAAC;QAAC,aAAa,EAAE,MAAM,CAAA;KAAE,CAAA;IACtF,eAAe,EAAE,oBAAoB,CAAA;CACtC,CAAA;AAED;;;GAGG;AACH,qBAAa,qBAAsB,SAAQ,QAAQ,CAAC,2BAA2B,CAAC;CAAG;AAEnF;;;;;;;;GAQG;AACH,eAAO,MAAM,0BAA0B,EAAE,KAAK,CAAC,qBAAqB,EAAE,WAAW,CAS/E,CAAA"}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { defineService } from '@furystack/inject';
|
|
2
|
+
import { EventHub } from '@furystack/utils';
|
|
3
|
+
/**
|
|
4
|
+
* Application-facing telemetry surface for the cross-node bus. Subscribers
|
|
5
|
+
* use the standard {@link EventHub} `addListener` / `subscribe` API.
|
|
6
|
+
*/
|
|
7
|
+
export class CrossNodeBusTelemetry extends EventHub {
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* DI token for the shared {@link CrossNodeBusTelemetry} instance.
|
|
11
|
+
*
|
|
12
|
+
* Singleton because the bus token itself is singleton — co-locating both at
|
|
13
|
+
* the root injector keeps every override factory (including transport
|
|
14
|
+
* adapters) free to inject telemetry without lifetime-compatibility
|
|
15
|
+
* gymnastics. Each test still gets isolation by minting its own root
|
|
16
|
+
* injector with `createInjector()`.
|
|
17
|
+
*/
|
|
18
|
+
export const CrossNodeBusTelemetryToken = defineService({
|
|
19
|
+
name: 'furystack/cross-node-bus/CrossNodeBusTelemetry',
|
|
20
|
+
lifetime: 'singleton',
|
|
21
|
+
factory: ({ onDispose }) => {
|
|
22
|
+
const telemetry = new CrossNodeBusTelemetry();
|
|
23
|
+
// eslint-disable-next-line furystack/prefer-using-wrapper -- delegated to onDispose
|
|
24
|
+
onDispose(() => telemetry[Symbol.dispose]());
|
|
25
|
+
return telemetry;
|
|
26
|
+
},
|
|
27
|
+
});
|
|
28
|
+
//# sourceMappingURL=cross-node-bus-telemetry.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cross-node-bus-telemetry.js","sourceRoot":"","sources":["../src/cross-node-bus-telemetry.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAc,MAAM,mBAAmB,CAAA;AAC7D,OAAO,EAAE,QAAQ,EAA6B,MAAM,kBAAkB,CAAA;AA2CtE;;;GAGG;AACH,MAAM,OAAO,qBAAsB,SAAQ,QAAqC;CAAG;AAEnF;;;;;;;;GAQG;AACH,MAAM,CAAC,MAAM,0BAA0B,GAA8C,aAAa,CAAC;IACjG,IAAI,EAAE,gDAAgD;IACtD,QAAQ,EAAE,WAAW;IACrB,OAAO,EAAE,CAAC,EAAE,SAAS,EAAE,EAAE,EAAE;QACzB,MAAM,SAAS,GAAG,IAAI,qBAAqB,EAAE,CAAA;QAC7C,oFAAoF;QACpF,SAAS,CAAC,GAAG,EAAE,CAAC,SAAS,CAAC,MAAM,CAAC,OAAO,CAAC,EAAE,CAAC,CAAA;QAC5C,OAAO,SAAS,CAAA;IAClB,CAAC;CACF,CAAC,CAAA"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cross-node-bus-telemetry.spec.d.ts","sourceRoot":"","sources":["../src/cross-node-bus-telemetry.spec.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
var __addDisposableResource = (this && this.__addDisposableResource) || function (env, value, async) {
|
|
2
|
+
if (value !== null && value !== void 0) {
|
|
3
|
+
if (typeof value !== "object" && typeof value !== "function") throw new TypeError("Object expected.");
|
|
4
|
+
var dispose, inner;
|
|
5
|
+
if (async) {
|
|
6
|
+
if (!Symbol.asyncDispose) throw new TypeError("Symbol.asyncDispose is not defined.");
|
|
7
|
+
dispose = value[Symbol.asyncDispose];
|
|
8
|
+
}
|
|
9
|
+
if (dispose === void 0) {
|
|
10
|
+
if (!Symbol.dispose) throw new TypeError("Symbol.dispose is not defined.");
|
|
11
|
+
dispose = value[Symbol.dispose];
|
|
12
|
+
if (async) inner = dispose;
|
|
13
|
+
}
|
|
14
|
+
if (typeof dispose !== "function") throw new TypeError("Object not disposable.");
|
|
15
|
+
if (inner) dispose = function() { try { inner.call(this); } catch (e) { return Promise.reject(e); } };
|
|
16
|
+
env.stack.push({ value: value, dispose: dispose, async: async });
|
|
17
|
+
}
|
|
18
|
+
else if (async) {
|
|
19
|
+
env.stack.push({ async: true });
|
|
20
|
+
}
|
|
21
|
+
return value;
|
|
22
|
+
};
|
|
23
|
+
var __disposeResources = (this && this.__disposeResources) || (function (SuppressedError) {
|
|
24
|
+
return function (env) {
|
|
25
|
+
function fail(e) {
|
|
26
|
+
env.error = env.hasError ? new SuppressedError(e, env.error, "An error was suppressed during disposal.") : e;
|
|
27
|
+
env.hasError = true;
|
|
28
|
+
}
|
|
29
|
+
var r, s = 0;
|
|
30
|
+
function next() {
|
|
31
|
+
while (r = env.stack.pop()) {
|
|
32
|
+
try {
|
|
33
|
+
if (!r.async && s === 1) return s = 0, env.stack.push(r), Promise.resolve().then(next);
|
|
34
|
+
if (r.dispose) {
|
|
35
|
+
var result = r.dispose.call(r.value);
|
|
36
|
+
if (r.async) return s |= 2, Promise.resolve(result).then(next, function(e) { fail(e); return next(); });
|
|
37
|
+
}
|
|
38
|
+
else s |= 1;
|
|
39
|
+
}
|
|
40
|
+
catch (e) {
|
|
41
|
+
fail(e);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
if (s === 1) return env.hasError ? Promise.reject(env.error) : Promise.resolve();
|
|
45
|
+
if (env.hasError) throw env.error;
|
|
46
|
+
}
|
|
47
|
+
return next();
|
|
48
|
+
};
|
|
49
|
+
})(typeof SuppressedError === "function" ? SuppressedError : function (error, suppressed, message) {
|
|
50
|
+
var e = new Error(message);
|
|
51
|
+
return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e;
|
|
52
|
+
});
|
|
53
|
+
import { createInjector } from '@furystack/inject';
|
|
54
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
55
|
+
import { CrossNodeBusTelemetry, CrossNodeBusTelemetryToken } from './cross-node-bus-telemetry.js';
|
|
56
|
+
describe('CrossNodeBusTelemetryToken', () => {
|
|
57
|
+
it('resolves a CrossNodeBusTelemetry instance', async () => {
|
|
58
|
+
const env_1 = { stack: [], error: void 0, hasError: false };
|
|
59
|
+
try {
|
|
60
|
+
const injector = __addDisposableResource(env_1, createInjector(), true);
|
|
61
|
+
const telemetry = injector.get(CrossNodeBusTelemetryToken);
|
|
62
|
+
expect(telemetry).toBeInstanceOf(CrossNodeBusTelemetry);
|
|
63
|
+
}
|
|
64
|
+
catch (e_1) {
|
|
65
|
+
env_1.error = e_1;
|
|
66
|
+
env_1.hasError = true;
|
|
67
|
+
}
|
|
68
|
+
finally {
|
|
69
|
+
const result_1 = __disposeResources(env_1);
|
|
70
|
+
if (result_1)
|
|
71
|
+
await result_1;
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
it('disposes the telemetry instance with the injector scope', async () => {
|
|
75
|
+
const injector = createInjector();
|
|
76
|
+
const telemetry = injector.get(CrossNodeBusTelemetryToken);
|
|
77
|
+
const handler = vi.fn();
|
|
78
|
+
telemetry.addListener('onCrossNodePublished', handler);
|
|
79
|
+
await injector[Symbol.asyncDispose]();
|
|
80
|
+
telemetry.emit('onCrossNodePublished', { topic: 'x', originId: 'y', byteLength: 0 });
|
|
81
|
+
expect(handler).not.toHaveBeenCalled();
|
|
82
|
+
});
|
|
83
|
+
it('shares the singleton instance across child scopes', async () => {
|
|
84
|
+
const env_2 = { stack: [], error: void 0, hasError: false };
|
|
85
|
+
try {
|
|
86
|
+
const injector = __addDisposableResource(env_2, createInjector(), true);
|
|
87
|
+
const scopeA = __addDisposableResource(env_2, injector.createScope({ owner: 'a' }), true);
|
|
88
|
+
const scopeB = __addDisposableResource(env_2, injector.createScope({ owner: 'b' }), true);
|
|
89
|
+
const telA = scopeA.get(CrossNodeBusTelemetryToken);
|
|
90
|
+
const telB = scopeB.get(CrossNodeBusTelemetryToken);
|
|
91
|
+
expect(telA).toBe(telB);
|
|
92
|
+
}
|
|
93
|
+
catch (e_2) {
|
|
94
|
+
env_2.error = e_2;
|
|
95
|
+
env_2.hasError = true;
|
|
96
|
+
}
|
|
97
|
+
finally {
|
|
98
|
+
const result_2 = __disposeResources(env_2);
|
|
99
|
+
if (result_2)
|
|
100
|
+
await result_2;
|
|
101
|
+
}
|
|
102
|
+
});
|
|
103
|
+
it('issues independent instances per root injector', () => {
|
|
104
|
+
const a = createInjector();
|
|
105
|
+
const b = createInjector();
|
|
106
|
+
try {
|
|
107
|
+
expect(a.get(CrossNodeBusTelemetryToken)).not.toBe(b.get(CrossNodeBusTelemetryToken));
|
|
108
|
+
}
|
|
109
|
+
finally {
|
|
110
|
+
void a[Symbol.asyncDispose]();
|
|
111
|
+
void b[Symbol.asyncDispose]();
|
|
112
|
+
}
|
|
113
|
+
});
|
|
114
|
+
});
|
|
115
|
+
//# sourceMappingURL=cross-node-bus-telemetry.spec.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cross-node-bus-telemetry.spec.js","sourceRoot":"","sources":["../src/cross-node-bus-telemetry.spec.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA,OAAO,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAA;AAClD,OAAO,EAAE,QAAQ,EAAE,MAAM,EAAE,EAAE,EAAE,EAAE,EAAE,MAAM,QAAQ,CAAA;AACjD,OAAO,EAAE,qBAAqB,EAAE,0BAA0B,EAAE,MAAM,+BAA+B,CAAA;AAEjG,QAAQ,CAAC,4BAA4B,EAAE,GAAG,EAAE;IAC1C,EAAE,CAAC,2CAA2C,EAAE,KAAK,IAAI,EAAE;;;YACzD,MAAY,QAAQ,kCAAG,cAAc,EAAE,OAAA,CAAA;YACvC,MAAM,SAAS,GAAG,QAAQ,CAAC,GAAG,CAAC,0BAA0B,CAAC,CAAA;YAC1D,MAAM,CAAC,SAAS,CAAC,CAAC,cAAc,CAAC,qBAAqB,CAAC,CAAA;;;;;;;;;;;KACxD,CAAC,CAAA;IAEF,EAAE,CAAC,yDAAyD,EAAE,KAAK,IAAI,EAAE;QACvE,MAAM,QAAQ,GAAG,cAAc,EAAE,CAAA;QACjC,MAAM,SAAS,GAAG,QAAQ,CAAC,GAAG,CAAC,0BAA0B,CAAC,CAAA;QAC1D,MAAM,OAAO,GAAG,EAAE,CAAC,EAAE,EAAE,CAAA;QACvB,SAAS,CAAC,WAAW,CAAC,sBAAsB,EAAE,OAAO,CAAC,CAAA;QACtD,MAAM,QAAQ,CAAC,MAAM,CAAC,YAAY,CAAC,EAAE,CAAA;QAErC,SAAS,CAAC,IAAI,CAAC,sBAAsB,EAAE,EAAE,KAAK,EAAE,GAAG,EAAE,QAAQ,EAAE,GAAG,EAAE,UAAU,EAAE,CAAC,EAAE,CAAC,CAAA;QACpF,MAAM,CAAC,OAAO,CAAC,CAAC,GAAG,CAAC,gBAAgB,EAAE,CAAA;IACxC,CAAC,CAAC,CAAA;IAEF,EAAE,CAAC,mDAAmD,EAAE,KAAK,IAAI,EAAE;;;YACjE,MAAY,QAAQ,kCAAG,cAAc,EAAE,OAAA,CAAA;YACvC,MAAY,MAAM,kCAAG,QAAQ,CAAC,WAAW,CAAC,EAAE,KAAK,EAAE,GAAG,EAAE,CAAC,OAAA,CAAA;YACzD,MAAY,MAAM,kCAAG,QAAQ,CAAC,WAAW,CAAC,EAAE,KAAK,EAAE,GAAG,EAAE,CAAC,OAAA,CAAA;YAEzD,MAAM,IAAI,GAAG,MAAM,CAAC,GAAG,CAAC,0BAA0B,CAAC,CAAA;YACnD,MAAM,IAAI,GAAG,MAAM,CAAC,GAAG,CAAC,0BAA0B,CAAC,CAAA;YAEnD,MAAM,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;;;;;;;;;;;KACxB,CAAC,CAAA;IAEF,EAAE,CAAC,gDAAgD,EAAE,GAAG,EAAE;QACxD,MAAM,CAAC,GAAG,cAAc,EAAE,CAAA;QAC1B,MAAM,CAAC,GAAG,cAAc,EAAE,CAAA;QAC1B,IAAI,CAAC;YACH,MAAM,CAAC,CAAC,CAAC,GAAG,CAAC,0BAA0B,CAAC,CAAC,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC,GAAG,CAAC,0BAA0B,CAAC,CAAC,CAAA;QACvF,CAAC;gBAAS,CAAC;YACT,KAAK,CAAC,CAAC,MAAM,CAAC,YAAY,CAAC,EAAE,CAAA;YAC7B,KAAK,CAAC,CAAC,MAAM,CAAC,YAAY,CAAC,EAAE,CAAA;QAC/B,CAAC;IACH,CAAC,CAAC,CAAA;AACJ,CAAC,CAAC,CAAA"}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { type Token } from '@furystack/inject';
|
|
2
|
+
import type { BusMessage, CrossNodeBusCapabilities } from './types.js';
|
|
3
|
+
/**
|
|
4
|
+
* Transport-agnostic publish/subscribe primitive. Implementations talk to a
|
|
5
|
+
* concrete broker (in-process map, Redis Streams, …); facades layer typed
|
|
6
|
+
* event contracts on top.
|
|
7
|
+
*
|
|
8
|
+
* Self-delivery is on by default — a publisher receives its own messages.
|
|
9
|
+
* Subscribers that need a local-vs-remote distinction either filter on
|
|
10
|
+
* `message.originId === bus.nodeId` or use
|
|
11
|
+
* {@link CrossNodeBus.subscribeRemoteOnly}.
|
|
12
|
+
*/
|
|
13
|
+
export interface CrossNodeBus extends Disposable {
|
|
14
|
+
/** Stable, unique id of this node. Included in every published message. */
|
|
15
|
+
readonly nodeId: string;
|
|
16
|
+
/** Static description of what this adapter can do. */
|
|
17
|
+
readonly capabilities: CrossNodeBusCapabilities;
|
|
18
|
+
/**
|
|
19
|
+
* Publishes `payload` on `topic`. Resolves once the message has been
|
|
20
|
+
* accepted by the underlying transport (not when it has been delivered to
|
|
21
|
+
* all subscribers).
|
|
22
|
+
*/
|
|
23
|
+
publish(topic: string, payload: unknown): Promise<void>;
|
|
24
|
+
/**
|
|
25
|
+
* Subscribes to every message published on `topic`, including ones
|
|
26
|
+
* originating from this node.
|
|
27
|
+
*/
|
|
28
|
+
subscribe(topic: string, handler: (message: BusMessage) => void): Disposable;
|
|
29
|
+
/**
|
|
30
|
+
* Convenience for the common "I only care about messages from other nodes"
|
|
31
|
+
* pattern. Equivalent to {@link CrossNodeBus.subscribe} + filter on
|
|
32
|
+
* `message.originId !== bus.nodeId`.
|
|
33
|
+
*/
|
|
34
|
+
subscribeRemoteOnly(topic: string, handler: (message: BusMessage) => void): Disposable;
|
|
35
|
+
/**
|
|
36
|
+
* Subscribe to a topic owned by another `topicPrefix`. Explicit, greppable
|
|
37
|
+
* cross-service eavesdrop. Adapters that lack the underlying capability
|
|
38
|
+
* throw at registration time.
|
|
39
|
+
*/
|
|
40
|
+
subscribeForeign(prefix: string, topic: string, handler: (message: BusMessage) => void): Disposable;
|
|
41
|
+
/**
|
|
42
|
+
* Replay messages on `topic` whose `seq` is greater than `fromSeq`. Throws
|
|
43
|
+
* synchronously when {@link CrossNodeBusCapabilities.replay} is `false` or
|
|
44
|
+
* when `fromSeq` is older than the adapter's retained window — facades
|
|
45
|
+
* fall back to a full snapshot in the latter case.
|
|
46
|
+
*/
|
|
47
|
+
replay(topic: string, fromSeq: string): AsyncIterable<BusMessage>;
|
|
48
|
+
/**
|
|
49
|
+
* Compares two adapter-issued seq tokens from the **same topic**. Returns a
|
|
50
|
+
* negative number when `a` precedes `b`, zero when equal, a positive number
|
|
51
|
+
* when `a` follows `b`. Facades use this for dedup and "have we seen newer?"
|
|
52
|
+
* checks without leaking the adapter-specific seq encoding.
|
|
53
|
+
*
|
|
54
|
+
* Behavior across topics, adapters, or for tokens this adapter never issued
|
|
55
|
+
* is undefined.
|
|
56
|
+
*/
|
|
57
|
+
compareSeq(a: string, b: string): number;
|
|
58
|
+
/**
|
|
59
|
+
* Returns the oldest retained seq for `topic`, or `undefined` when nothing
|
|
60
|
+
* is currently retained. Throws synchronously when
|
|
61
|
+
* {@link CrossNodeBusCapabilities.replay} is `false`. Facades use this to
|
|
62
|
+
* decide whether a delta replay is feasible before calling
|
|
63
|
+
* {@link CrossNodeBus.replay}.
|
|
64
|
+
*/
|
|
65
|
+
oldestSeq(topic: string): string | undefined;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Shared {@link CrossNodeBus} token. Resolves an `InProcessCrossNodeBus` by
|
|
69
|
+
* default — single-node deployments work without configuration. Multi-node
|
|
70
|
+
* deployments override the binding with a transport adapter, e.g.
|
|
71
|
+
* `defineRedisCrossNodeBusAdapter({ … })`.
|
|
72
|
+
*
|
|
73
|
+
* Singleton: a single bus per injector tree is the right semantic for
|
|
74
|
+
* cross-process publish/subscribe. Tests get isolation by minting their own
|
|
75
|
+
* root injector with `createInjector()`.
|
|
76
|
+
*/
|
|
77
|
+
export declare const CrossNodeBus: Token<CrossNodeBus, 'singleton'>;
|
|
78
|
+
//# sourceMappingURL=cross-node-bus.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cross-node-bus.d.ts","sourceRoot":"","sources":["../src/cross-node-bus.ts"],"names":[],"mappings":"AAAA,OAAO,EAAiB,KAAK,KAAK,EAAE,MAAM,mBAAmB,CAAA;AAE7D,OAAO,KAAK,EAAE,UAAU,EAAE,wBAAwB,EAAE,MAAM,YAAY,CAAA;AAEtE;;;;;;;;;GASG;AACH,MAAM,WAAW,YAAa,SAAQ,UAAU;IAC9C,2EAA2E;IAC3E,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;IAEvB,sDAAsD;IACtD,QAAQ,CAAC,YAAY,EAAE,wBAAwB,CAAA;IAE/C;;;;OAIG;IACH,OAAO,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IAEvD;;;OAGG;IACH,SAAS,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,CAAC,OAAO,EAAE,UAAU,KAAK,IAAI,GAAG,UAAU,CAAA;IAE5E;;;;OAIG;IACH,mBAAmB,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,CAAC,OAAO,EAAE,UAAU,KAAK,IAAI,GAAG,UAAU,CAAA;IAEtF;;;;OAIG;IACH,gBAAgB,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,CAAC,OAAO,EAAE,UAAU,KAAK,IAAI,GAAG,UAAU,CAAA;IAEnG;;;;;OAKG;IACH,MAAM,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,aAAa,CAAC,UAAU,CAAC,CAAA;IAEjE;;;;;;;;OAQG;IACH,UAAU,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,GAAG,MAAM,CAAA;IAExC;;;;;;OAMG;IACH,SAAS,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAAA;CAC7C;AAED;;;;;;;;;GASG;AACH,eAAO,MAAM,YAAY,EAAE,KAAK,CAAC,YAAY,EAAE,WAAW,CAIxD,CAAA"}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { defineService } from '@furystack/inject';
|
|
2
|
+
import { defineInProcessCrossNodeBus } from './define-in-process-cross-node-bus.js';
|
|
3
|
+
/**
|
|
4
|
+
* Shared {@link CrossNodeBus} token. Resolves an `InProcessCrossNodeBus` by
|
|
5
|
+
* default — single-node deployments work without configuration. Multi-node
|
|
6
|
+
* deployments override the binding with a transport adapter, e.g.
|
|
7
|
+
* `defineRedisCrossNodeBusAdapter({ … })`.
|
|
8
|
+
*
|
|
9
|
+
* Singleton: a single bus per injector tree is the right semantic for
|
|
10
|
+
* cross-process publish/subscribe. Tests get isolation by minting their own
|
|
11
|
+
* root injector with `createInjector()`.
|
|
12
|
+
*/
|
|
13
|
+
export const CrossNodeBus = defineService({
|
|
14
|
+
name: 'furystack/cross-node-bus/CrossNodeBus',
|
|
15
|
+
lifetime: 'singleton',
|
|
16
|
+
factory: defineInProcessCrossNodeBus(),
|
|
17
|
+
});
|
|
18
|
+
//# sourceMappingURL=cross-node-bus.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cross-node-bus.js","sourceRoot":"","sources":["../src/cross-node-bus.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAc,MAAM,mBAAmB,CAAA;AAC7D,OAAO,EAAE,2BAA2B,EAAE,MAAM,uCAAuC,CAAA;AA4EnF;;;;;;;;;GASG;AACH,MAAM,CAAC,MAAM,YAAY,GAAqC,aAAa,CAAC;IAC1E,IAAI,EAAE,uCAAuC;IAC7C,QAAQ,EAAE,WAAW;IACrB,OAAO,EAAE,2BAA2B,EAAE;CACvC,CAAC,CAAA"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cross-node-bus.spec.d.ts","sourceRoot":"","sources":["../src/cross-node-bus.spec.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
var __addDisposableResource = (this && this.__addDisposableResource) || function (env, value, async) {
|
|
2
|
+
if (value !== null && value !== void 0) {
|
|
3
|
+
if (typeof value !== "object" && typeof value !== "function") throw new TypeError("Object expected.");
|
|
4
|
+
var dispose, inner;
|
|
5
|
+
if (async) {
|
|
6
|
+
if (!Symbol.asyncDispose) throw new TypeError("Symbol.asyncDispose is not defined.");
|
|
7
|
+
dispose = value[Symbol.asyncDispose];
|
|
8
|
+
}
|
|
9
|
+
if (dispose === void 0) {
|
|
10
|
+
if (!Symbol.dispose) throw new TypeError("Symbol.dispose is not defined.");
|
|
11
|
+
dispose = value[Symbol.dispose];
|
|
12
|
+
if (async) inner = dispose;
|
|
13
|
+
}
|
|
14
|
+
if (typeof dispose !== "function") throw new TypeError("Object not disposable.");
|
|
15
|
+
if (inner) dispose = function() { try { inner.call(this); } catch (e) { return Promise.reject(e); } };
|
|
16
|
+
env.stack.push({ value: value, dispose: dispose, async: async });
|
|
17
|
+
}
|
|
18
|
+
else if (async) {
|
|
19
|
+
env.stack.push({ async: true });
|
|
20
|
+
}
|
|
21
|
+
return value;
|
|
22
|
+
};
|
|
23
|
+
var __disposeResources = (this && this.__disposeResources) || (function (SuppressedError) {
|
|
24
|
+
return function (env) {
|
|
25
|
+
function fail(e) {
|
|
26
|
+
env.error = env.hasError ? new SuppressedError(e, env.error, "An error was suppressed during disposal.") : e;
|
|
27
|
+
env.hasError = true;
|
|
28
|
+
}
|
|
29
|
+
var r, s = 0;
|
|
30
|
+
function next() {
|
|
31
|
+
while (r = env.stack.pop()) {
|
|
32
|
+
try {
|
|
33
|
+
if (!r.async && s === 1) return s = 0, env.stack.push(r), Promise.resolve().then(next);
|
|
34
|
+
if (r.dispose) {
|
|
35
|
+
var result = r.dispose.call(r.value);
|
|
36
|
+
if (r.async) return s |= 2, Promise.resolve(result).then(next, function(e) { fail(e); return next(); });
|
|
37
|
+
}
|
|
38
|
+
else s |= 1;
|
|
39
|
+
}
|
|
40
|
+
catch (e) {
|
|
41
|
+
fail(e);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
if (s === 1) return env.hasError ? Promise.reject(env.error) : Promise.resolve();
|
|
45
|
+
if (env.hasError) throw env.error;
|
|
46
|
+
}
|
|
47
|
+
return next();
|
|
48
|
+
};
|
|
49
|
+
})(typeof SuppressedError === "function" ? SuppressedError : function (error, suppressed, message) {
|
|
50
|
+
var e = new Error(message);
|
|
51
|
+
return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e;
|
|
52
|
+
});
|
|
53
|
+
import { createInjector } from '@furystack/inject';
|
|
54
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
55
|
+
import { CrossNodeBus } from './cross-node-bus.js';
|
|
56
|
+
import { CrossNodeBusTelemetryToken } from './cross-node-bus-telemetry.js';
|
|
57
|
+
import { defineInProcessCrossNodeBus } from './define-in-process-cross-node-bus.js';
|
|
58
|
+
import { InProcessCrossNodeBus } from './in-process-cross-node-bus.js';
|
|
59
|
+
describe('CrossNodeBus token', () => {
|
|
60
|
+
it('resolves an InProcessCrossNodeBus by default', async () => {
|
|
61
|
+
const env_1 = { stack: [], error: void 0, hasError: false };
|
|
62
|
+
try {
|
|
63
|
+
const injector = __addDisposableResource(env_1, createInjector(), true);
|
|
64
|
+
const bus = injector.get(CrossNodeBus);
|
|
65
|
+
expect(bus).toBeInstanceOf(InProcessCrossNodeBus);
|
|
66
|
+
}
|
|
67
|
+
catch (e_1) {
|
|
68
|
+
env_1.error = e_1;
|
|
69
|
+
env_1.hasError = true;
|
|
70
|
+
}
|
|
71
|
+
finally {
|
|
72
|
+
const result_1 = __disposeResources(env_1);
|
|
73
|
+
if (result_1)
|
|
74
|
+
await result_1;
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
it('disposes the bus when the injector scope tears down', async () => {
|
|
78
|
+
const injector = createInjector();
|
|
79
|
+
const bus = injector.get(CrossNodeBus);
|
|
80
|
+
await injector[Symbol.asyncDispose]();
|
|
81
|
+
expect(() => bus.subscribe('topic', () => undefined)).toThrow(/disposed/);
|
|
82
|
+
});
|
|
83
|
+
it('honors override bindings via defineInProcessCrossNodeBus', async () => {
|
|
84
|
+
const env_2 = { stack: [], error: void 0, hasError: false };
|
|
85
|
+
try {
|
|
86
|
+
const injector = __addDisposableResource(env_2, createInjector(), true);
|
|
87
|
+
injector.bind(CrossNodeBus, defineInProcessCrossNodeBus({ nodeId: 'svc-a-1', topicPrefix: 'svc-a/' }));
|
|
88
|
+
const bus = injector.get(CrossNodeBus);
|
|
89
|
+
expect(bus.nodeId).toBe('svc-a-1');
|
|
90
|
+
}
|
|
91
|
+
catch (e_2) {
|
|
92
|
+
env_2.error = e_2;
|
|
93
|
+
env_2.hasError = true;
|
|
94
|
+
}
|
|
95
|
+
finally {
|
|
96
|
+
const result_2 = __disposeResources(env_2);
|
|
97
|
+
if (result_2)
|
|
98
|
+
await result_2;
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
it('publishes drive the scoped CrossNodeBusTelemetryToken', async () => {
|
|
102
|
+
const env_3 = { stack: [], error: void 0, hasError: false };
|
|
103
|
+
try {
|
|
104
|
+
const injector = __addDisposableResource(env_3, createInjector(), true);
|
|
105
|
+
const telemetry = injector.get(CrossNodeBusTelemetryToken);
|
|
106
|
+
const handler = vi.fn();
|
|
107
|
+
const _sub = __addDisposableResource(env_3, telemetry.subscribe('onCrossNodePublished', handler), false);
|
|
108
|
+
const bus = injector.get(CrossNodeBus);
|
|
109
|
+
await bus.publish('topic', { hi: 'there' });
|
|
110
|
+
expect(handler).toHaveBeenCalledTimes(1);
|
|
111
|
+
}
|
|
112
|
+
catch (e_3) {
|
|
113
|
+
env_3.error = e_3;
|
|
114
|
+
env_3.hasError = true;
|
|
115
|
+
}
|
|
116
|
+
finally {
|
|
117
|
+
const result_3 = __disposeResources(env_3);
|
|
118
|
+
if (result_3)
|
|
119
|
+
await result_3;
|
|
120
|
+
}
|
|
121
|
+
});
|
|
122
|
+
});
|
|
123
|
+
//# sourceMappingURL=cross-node-bus.spec.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cross-node-bus.spec.js","sourceRoot":"","sources":["../src/cross-node-bus.spec.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA,OAAO,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAA;AAClD,OAAO,EAAE,QAAQ,EAAE,MAAM,EAAE,EAAE,EAAE,EAAE,EAAE,MAAM,QAAQ,CAAA;AACjD,OAAO,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAA;AAClD,OAAO,EAAE,0BAA0B,EAAE,MAAM,+BAA+B,CAAA;AAC1E,OAAO,EAAE,2BAA2B,EAAE,MAAM,uCAAuC,CAAA;AACnF,OAAO,EAAE,qBAAqB,EAAE,MAAM,gCAAgC,CAAA;AAEtE,QAAQ,CAAC,oBAAoB,EAAE,GAAG,EAAE;IAClC,EAAE,CAAC,8CAA8C,EAAE,KAAK,IAAI,EAAE;;;YAC5D,MAAY,QAAQ,kCAAG,cAAc,EAAE,OAAA,CAAA;YACvC,MAAM,GAAG,GAAG,QAAQ,CAAC,GAAG,CAAC,YAAY,CAAC,CAAA;YACtC,MAAM,CAAC,GAAG,CAAC,CAAC,cAAc,CAAC,qBAAqB,CAAC,CAAA;;;;;;;;;;;KAClD,CAAC,CAAA;IAEF,EAAE,CAAC,qDAAqD,EAAE,KAAK,IAAI,EAAE;QACnE,MAAM,QAAQ,GAAG,cAAc,EAAE,CAAA;QACjC,MAAM,GAAG,GAAG,QAAQ,CAAC,GAAG,CAAC,YAAY,CAAC,CAAA;QACtC,MAAM,QAAQ,CAAC,MAAM,CAAC,YAAY,CAAC,EAAE,CAAA;QACrC,MAAM,CAAC,GAAG,EAAE,CAAC,GAAG,CAAC,SAAS,CAAC,OAAO,EAAE,GAAG,EAAE,CAAC,SAAS,CAAC,CAAC,CAAC,OAAO,CAAC,UAAU,CAAC,CAAA;IAC3E,CAAC,CAAC,CAAA;IAEF,EAAE,CAAC,0DAA0D,EAAE,KAAK,IAAI,EAAE;;;YACxE,MAAY,QAAQ,kCAAG,cAAc,EAAE,OAAA,CAAA;YACvC,QAAQ,CAAC,IAAI,CAAC,YAAY,EAAE,2BAA2B,CAAC,EAAE,MAAM,EAAE,SAAS,EAAE,WAAW,EAAE,QAAQ,EAAE,CAAC,CAAC,CAAA;YAEtG,MAAM,GAAG,GAAG,QAAQ,CAAC,GAAG,CAAC,YAAY,CAAC,CAAA;YACtC,MAAM,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,SAAS,CAAC,CAAA;;;;;;;;;;;KACnC,CAAC,CAAA;IAEF,EAAE,CAAC,uDAAuD,EAAE,KAAK,IAAI,EAAE;;;YACrE,MAAY,QAAQ,kCAAG,cAAc,EAAE,OAAA,CAAA;YACvC,MAAM,SAAS,GAAG,QAAQ,CAAC,GAAG,CAAC,0BAA0B,CAAC,CAAA;YAC1D,MAAM,OAAO,GAAG,EAAE,CAAC,EAAE,EAAE,CAAA;YACvB,MAAM,IAAI,kCAAG,SAAS,CAAC,SAAS,CAAC,sBAAsB,EAAE,OAAO,CAAC,QAAA,CAAA;YAEjE,MAAM,GAAG,GAAG,QAAQ,CAAC,GAAG,CAAC,YAAY,CAAC,CAAA;YACtC,MAAM,GAAG,CAAC,OAAO,CAAC,OAAO,EAAE,EAAE,EAAE,EAAE,OAAO,EAAE,CAAC,CAAA;YAE3C,MAAM,CAAC,OAAO,CAAC,CAAC,qBAAqB,CAAC,CAAC,CAAC,CAAA;;;;;;;;;;;KACzC,CAAC,CAAA;AACJ,CAAC,CAAC,CAAA"}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { ServiceFactory } from '@furystack/inject';
|
|
2
|
+
import { type InProcessCrossNodeBusOptions } from './in-process-cross-node-bus.js';
|
|
3
|
+
import type { CrossNodeBus } from './cross-node-bus.js';
|
|
4
|
+
/**
|
|
5
|
+
* Options accepted by {@link defineInProcessCrossNodeBus}. `telemetry` is
|
|
6
|
+
* intentionally absent — the factory always injects
|
|
7
|
+
* {@link CrossNodeBusTelemetryToken} from the surrounding scope.
|
|
8
|
+
*/
|
|
9
|
+
export type DefineInProcessCrossNodeBusOptions = Omit<InProcessCrossNodeBusOptions, 'telemetry'>;
|
|
10
|
+
/**
|
|
11
|
+
* Returns a {@link ServiceFactory} bound to {@link CrossNodeBus}. Use it to
|
|
12
|
+
* override the default factory at boot:
|
|
13
|
+
*
|
|
14
|
+
* ```ts
|
|
15
|
+
* injector.bind(
|
|
16
|
+
* CrossNodeBus,
|
|
17
|
+
* defineInProcessCrossNodeBus({ topicPrefix: 'svc-a/' }),
|
|
18
|
+
* )
|
|
19
|
+
* ```
|
|
20
|
+
*
|
|
21
|
+
* Mirrors the `defineXxxCrossNodeBusAdapter` shape future transport
|
|
22
|
+
* adapters expose. Wires telemetry and disposal into the surrounding
|
|
23
|
+
* injector scope.
|
|
24
|
+
*/
|
|
25
|
+
export declare const defineInProcessCrossNodeBus: (options?: DefineInProcessCrossNodeBusOptions) => ServiceFactory<CrossNodeBus>;
|
|
26
|
+
//# sourceMappingURL=define-in-process-cross-node-bus.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"define-in-process-cross-node-bus.d.ts","sourceRoot":"","sources":["../src/define-in-process-cross-node-bus.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAA;AAEvD,OAAO,EAAyB,KAAK,4BAA4B,EAAE,MAAM,gCAAgC,CAAA;AACzG,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAA;AAEvD;;;;GAIG;AACH,MAAM,MAAM,kCAAkC,GAAG,IAAI,CAAC,4BAA4B,EAAE,WAAW,CAAC,CAAA;AAEhG;;;;;;;;;;;;;;GAcG;AACH,eAAO,MAAM,2BAA2B,GACtC,UAAS,kCAAuC,KAC/C,cAAc,CAAC,YAAY,CAQ7B,CAAA"}
|