@nwire/bus 0.7.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/LICENSE +21 -0
- package/README.md +63 -0
- package/dist/__tests__/bus.test.d.ts +6 -0
- package/dist/__tests__/bus.test.d.ts.map +1 -0
- package/dist/__tests__/bus.test.js +76 -0
- package/dist/__tests__/bus.test.js.map +1 -0
- package/dist/bus.d.ts +62 -0
- package/dist/bus.d.ts.map +1 -0
- package/dist/bus.js +61 -0
- package/dist/bus.js.map +1 -0
- package/package.json +41 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Alex Gefter / 200apps Ltd.
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# @nwire/bus
|
|
2
|
+
|
|
3
|
+
> Cross-service event bus contract — pub/sub for events that leave one service and reach another.
|
|
4
|
+
|
|
5
|
+
## What it does
|
|
6
|
+
|
|
7
|
+
Defines `EventBus`, the publish-subscribe contract used to fan domain events across deployable services. Within one service the runtime fans events to in-process actors/projections/reactions; across services those same events flow through this bus (NATS in production, in-memory in tests). Ships `InMemoryEventBus` for dev/tests; swap in `@nwire/bus-nats` (or any other adapter) by config.
|
|
8
|
+
|
|
9
|
+
## Install
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
pnpm add @nwire/bus
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Quick start
|
|
16
|
+
|
|
17
|
+
```ts
|
|
18
|
+
import { InMemoryEventBus } from "@nwire/bus";
|
|
19
|
+
import { learnflowApp } from "@amit/learnflow";
|
|
20
|
+
|
|
21
|
+
const bus = new InMemoryEventBus();
|
|
22
|
+
const app = learnflowApp.create({ bus, publishToBus: true });
|
|
23
|
+
await app.start();
|
|
24
|
+
// Cross-service subscribers can now react to `learnflow.StudentWasEnrolled` etc.
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## API surface
|
|
28
|
+
|
|
29
|
+
- `EventBus` — `publish(message)` + `subscribe(eventName, handler) => unsubscribe`.
|
|
30
|
+
- `InMemoryEventBus` — in-process default for tests / single-service deployments.
|
|
31
|
+
- `BusEventMessage` / `BusSubscriber` — wire types.
|
|
32
|
+
|
|
33
|
+
## When to use
|
|
34
|
+
|
|
35
|
+
When the system is deployed as more than one process (split services, microservices, Lemida-shape). Fits L4 and up.
|
|
36
|
+
|
|
37
|
+
## Standalone use
|
|
38
|
+
|
|
39
|
+
For developers using `@nwire/bus` **without the rest of Nwire** — pair it with any TypeScript project, any container, any HTTP framework.
|
|
40
|
+
|
|
41
|
+
```ts
|
|
42
|
+
// See the package's main entry (src/) for the standalone surface.
|
|
43
|
+
// The exports below work without @nwire/app or @nwire/forge.
|
|
44
|
+
import {} from /* ...standalone exports... */ "@nwire/bus";
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## Within nwire-app
|
|
48
|
+
|
|
49
|
+
For developers using this package as part of the Nwire stack — register it via `app.use(...)` or it auto-wires when you compose `createApp({ modules })`.
|
|
50
|
+
|
|
51
|
+
```ts
|
|
52
|
+
import { createApp } from "@nwire/forge";
|
|
53
|
+
|
|
54
|
+
const app = createApp({
|
|
55
|
+
/* ...config... */
|
|
56
|
+
});
|
|
57
|
+
// Adapter/plugin wiring happens here when applicable.
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## See also
|
|
61
|
+
|
|
62
|
+
- [Architecture sketch §05 — Adapters tier](../../architecture-sketch.html#packages)
|
|
63
|
+
- Sibling packages: [@nwire/bus-nats](../nwire-bus-nats), [@nwire/messages](../nwire-messages)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"bus.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/bus.test.ts"],"names":[],"mappings":"AAAA;;;GAGG"}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* EventBus contract — pub/sub fanout, subscribe-after-publish silently drops,
|
|
3
|
+
* stop drains in-flight, publish-after-stop throws.
|
|
4
|
+
*/
|
|
5
|
+
import { describe, it, expect } from "vitest";
|
|
6
|
+
import { seedEnvelope } from "@nwire/envelope";
|
|
7
|
+
import { InMemoryEventBus } from "../bus.js";
|
|
8
|
+
describe("InMemoryEventBus", () => {
|
|
9
|
+
it("fans every publish to every subscriber of that eventName", async () => {
|
|
10
|
+
const bus = new InMemoryEventBus();
|
|
11
|
+
const calls = [];
|
|
12
|
+
bus.subscribe("e.one", async (m) => {
|
|
13
|
+
calls.push(`a:${m.payload.v}`);
|
|
14
|
+
});
|
|
15
|
+
bus.subscribe("e.one", async (m) => {
|
|
16
|
+
calls.push(`b:${m.payload.v}`);
|
|
17
|
+
});
|
|
18
|
+
bus.subscribe("e.two", async () => {
|
|
19
|
+
calls.push("c");
|
|
20
|
+
});
|
|
21
|
+
await bus.publish({ eventName: "e.one", payload: { v: 1 }, envelope: seedEnvelope() });
|
|
22
|
+
await bus.publish({ eventName: "e.one", payload: { v: 2 }, envelope: seedEnvelope() });
|
|
23
|
+
expect(calls).toEqual(["a:1", "b:1", "a:2", "b:2"]);
|
|
24
|
+
await bus.stop();
|
|
25
|
+
});
|
|
26
|
+
it("a publish with no subscribers is a no-op", async () => {
|
|
27
|
+
const bus = new InMemoryEventBus();
|
|
28
|
+
await bus.publish({
|
|
29
|
+
eventName: "unsubscribed",
|
|
30
|
+
payload: {},
|
|
31
|
+
envelope: seedEnvelope(),
|
|
32
|
+
});
|
|
33
|
+
// No throw, no observable effect.
|
|
34
|
+
await bus.stop();
|
|
35
|
+
});
|
|
36
|
+
it("a subscriber that throws does not break the fanout", async () => {
|
|
37
|
+
const bus = new InMemoryEventBus();
|
|
38
|
+
const calls = [];
|
|
39
|
+
bus.subscribe("e", async () => {
|
|
40
|
+
throw new Error("first failed");
|
|
41
|
+
});
|
|
42
|
+
bus.subscribe("e", async () => {
|
|
43
|
+
calls.push("second-ran");
|
|
44
|
+
});
|
|
45
|
+
// We don't surface the throw to publish — the bus logs it (console.error).
|
|
46
|
+
await bus.publish({ eventName: "e", payload: {}, envelope: seedEnvelope() });
|
|
47
|
+
expect(calls).toEqual(["second-ran"]);
|
|
48
|
+
await bus.stop();
|
|
49
|
+
});
|
|
50
|
+
it("publish after stop throws", async () => {
|
|
51
|
+
const bus = new InMemoryEventBus();
|
|
52
|
+
await bus.stop();
|
|
53
|
+
await expect(bus.publish({ eventName: "e", payload: {}, envelope: seedEnvelope() })).rejects.toThrow(/publish after stop/);
|
|
54
|
+
});
|
|
55
|
+
it("subscribe after stop throws", async () => {
|
|
56
|
+
const bus = new InMemoryEventBus();
|
|
57
|
+
await bus.stop();
|
|
58
|
+
expect(() => bus.subscribe("e", async () => { })).toThrow(/subscribe after stop/);
|
|
59
|
+
});
|
|
60
|
+
it("carries origin tag through publish → subscribe", async () => {
|
|
61
|
+
const bus = new InMemoryEventBus();
|
|
62
|
+
let seenOrigin;
|
|
63
|
+
bus.subscribe("e", async (m) => {
|
|
64
|
+
seenOrigin = m.origin;
|
|
65
|
+
});
|
|
66
|
+
await bus.publish({
|
|
67
|
+
eventName: "e",
|
|
68
|
+
payload: {},
|
|
69
|
+
envelope: seedEnvelope(),
|
|
70
|
+
origin: "service-a",
|
|
71
|
+
});
|
|
72
|
+
expect(seenOrigin).toBe("service-a");
|
|
73
|
+
await bus.stop();
|
|
74
|
+
});
|
|
75
|
+
});
|
|
76
|
+
//# sourceMappingURL=bus.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"bus.test.js","sourceRoot":"","sources":["../../src/__tests__/bus.test.ts"],"names":[],"mappings":"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"}
|
package/dist/bus.d.ts
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@nwire/bus` — cross-service event bus contract.
|
|
3
|
+
*
|
|
4
|
+
* Within one service the runtime fans events to in-process actors/
|
|
5
|
+
* projections/reactions; across services those same domain events flow
|
|
6
|
+
* through this bus (NATS in production, in-memory in tests). Same
|
|
7
|
+
* `EventBus` contract everywhere — no domain code changes when swapping
|
|
8
|
+
* adapters.
|
|
9
|
+
*
|
|
10
|
+
* See: architecture-sketch.html §05 (Adapters tier).
|
|
11
|
+
*/
|
|
12
|
+
import type { MessageEnvelope } from "@nwire/envelope";
|
|
13
|
+
/**
|
|
14
|
+
* The serialized over-the-wire shape. We ship the event NAME (not the full
|
|
15
|
+
* EventDefinition reference) plus the validated payload plus the envelope.
|
|
16
|
+
* Subscribers reconstruct on receive.
|
|
17
|
+
*/
|
|
18
|
+
export interface BusEventMessage {
|
|
19
|
+
readonly eventName: string;
|
|
20
|
+
readonly payload: unknown;
|
|
21
|
+
readonly envelope: MessageEnvelope;
|
|
22
|
+
/**
|
|
23
|
+
* Service name that originated this event. Lets bus-received handlers
|
|
24
|
+
* distinguish "we just published this" (echo) from "another service did."
|
|
25
|
+
* The runtime sets this to its app's name when publishing.
|
|
26
|
+
*/
|
|
27
|
+
readonly origin?: string;
|
|
28
|
+
}
|
|
29
|
+
export type BusSubscriber = (msg: BusEventMessage) => Promise<void> | void;
|
|
30
|
+
export interface EventBus {
|
|
31
|
+
/**
|
|
32
|
+
* Publish an event. Returns when the transport has accepted the message
|
|
33
|
+
* (not when subscribers have processed it). For InMemory, subscribers
|
|
34
|
+
* run inline. For NATS, returns after the publish call.
|
|
35
|
+
*/
|
|
36
|
+
publish(msg: BusEventMessage): Promise<void>;
|
|
37
|
+
/**
|
|
38
|
+
* Subscribe to every message whose `eventName` matches `eventName`.
|
|
39
|
+
* Wildcards depend on the adapter; the contract guarantees exact match.
|
|
40
|
+
*/
|
|
41
|
+
subscribe(eventName: string, subscriber: BusSubscriber): void;
|
|
42
|
+
/** Drain in-flight messages and close all subscriptions. */
|
|
43
|
+
stop(): Promise<void>;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Process-local pub/sub. Useful for tests, dev, and single-process apps that
|
|
47
|
+
* still want the same code shape they'll deploy multi-service.
|
|
48
|
+
*
|
|
49
|
+
* Subscribers attached to an event name run sequentially per message (so a
|
|
50
|
+
* failing subscriber doesn't silently skip the next one). A subscriber that
|
|
51
|
+
* throws logs to console.error — production transports route failures to
|
|
52
|
+
* their own DLQ.
|
|
53
|
+
*/
|
|
54
|
+
export declare class InMemoryEventBus implements EventBus {
|
|
55
|
+
private readonly subscribers;
|
|
56
|
+
private readonly inflight;
|
|
57
|
+
private stopped;
|
|
58
|
+
publish(msg: BusEventMessage): Promise<void>;
|
|
59
|
+
subscribe(eventName: string, subscriber: BusSubscriber): void;
|
|
60
|
+
stop(): Promise<void>;
|
|
61
|
+
}
|
|
62
|
+
//# sourceMappingURL=bus.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"bus.d.ts","sourceRoot":"","sources":["../src/bus.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAC;AAEvD;;;;GAIG;AACH,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;IAC1B,QAAQ,CAAC,QAAQ,EAAE,eAAe,CAAC;IACnC;;;;OAIG;IACH,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;CAC1B;AAED,MAAM,MAAM,aAAa,GAAG,CAAC,GAAG,EAAE,eAAe,KAAK,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;AAE3E,MAAM,WAAW,QAAQ;IACvB;;;;OAIG;IACH,OAAO,CAAC,GAAG,EAAE,eAAe,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC7C;;;OAGG;IACH,SAAS,CAAC,SAAS,EAAE,MAAM,EAAE,UAAU,EAAE,aAAa,GAAG,IAAI,CAAC;IAC9D,4DAA4D;IAC5D,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CACvB;AAED;;;;;;;;GAQG;AACH,qBAAa,gBAAiB,YAAW,QAAQ;IAC/C,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAsC;IAClE,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAuB;IAChD,OAAO,CAAC,OAAO,CAAS;IAElB,OAAO,CAAC,GAAG,EAAE,eAAe,GAAG,OAAO,CAAC,IAAI,CAAC;IAsBlD,SAAS,CAAC,SAAS,EAAE,MAAM,EAAE,UAAU,EAAE,aAAa,GAAG,IAAI;IASvD,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;CAK5B"}
|
package/dist/bus.js
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@nwire/bus` — cross-service event bus contract.
|
|
3
|
+
*
|
|
4
|
+
* Within one service the runtime fans events to in-process actors/
|
|
5
|
+
* projections/reactions; across services those same domain events flow
|
|
6
|
+
* through this bus (NATS in production, in-memory in tests). Same
|
|
7
|
+
* `EventBus` contract everywhere — no domain code changes when swapping
|
|
8
|
+
* adapters.
|
|
9
|
+
*
|
|
10
|
+
* See: architecture-sketch.html §05 (Adapters tier).
|
|
11
|
+
*/
|
|
12
|
+
/**
|
|
13
|
+
* Process-local pub/sub. Useful for tests, dev, and single-process apps that
|
|
14
|
+
* still want the same code shape they'll deploy multi-service.
|
|
15
|
+
*
|
|
16
|
+
* Subscribers attached to an event name run sequentially per message (so a
|
|
17
|
+
* failing subscriber doesn't silently skip the next one). A subscriber that
|
|
18
|
+
* throws logs to console.error — production transports route failures to
|
|
19
|
+
* their own DLQ.
|
|
20
|
+
*/
|
|
21
|
+
export class InMemoryEventBus {
|
|
22
|
+
subscribers = new Map();
|
|
23
|
+
inflight = [];
|
|
24
|
+
stopped = false;
|
|
25
|
+
async publish(msg) {
|
|
26
|
+
if (this.stopped) {
|
|
27
|
+
throw new Error("InMemoryEventBus: publish after stop");
|
|
28
|
+
}
|
|
29
|
+
const subs = this.subscribers.get(msg.eventName);
|
|
30
|
+
if (!subs || subs.length === 0)
|
|
31
|
+
return;
|
|
32
|
+
const run = async () => {
|
|
33
|
+
for (const sub of subs) {
|
|
34
|
+
try {
|
|
35
|
+
await sub(msg);
|
|
36
|
+
}
|
|
37
|
+
catch (err) {
|
|
38
|
+
// eslint-disable-next-line no-console
|
|
39
|
+
console.error(`InMemoryEventBus subscriber for "${msg.eventName}" threw:`, err);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
const promise = run();
|
|
44
|
+
this.inflight.push(promise);
|
|
45
|
+
await promise;
|
|
46
|
+
}
|
|
47
|
+
subscribe(eventName, subscriber) {
|
|
48
|
+
if (this.stopped) {
|
|
49
|
+
throw new Error("InMemoryEventBus: subscribe after stop");
|
|
50
|
+
}
|
|
51
|
+
const list = this.subscribers.get(eventName) ?? [];
|
|
52
|
+
list.push(subscriber);
|
|
53
|
+
this.subscribers.set(eventName, list);
|
|
54
|
+
}
|
|
55
|
+
async stop() {
|
|
56
|
+
this.stopped = true;
|
|
57
|
+
await Promise.allSettled([...this.inflight]);
|
|
58
|
+
this.subscribers.clear();
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
//# sourceMappingURL=bus.js.map
|
package/dist/bus.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"bus.js","sourceRoot":"","sources":["../src/bus.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAuCH;;;;;;;;GAQG;AACH,MAAM,OAAO,gBAAgB;IACV,WAAW,GAAG,IAAI,GAAG,EAA2B,CAAC;IACjD,QAAQ,GAAoB,EAAE,CAAC;IACxC,OAAO,GAAG,KAAK,CAAC;IAExB,KAAK,CAAC,OAAO,CAAC,GAAoB;QAChC,IAAI,IAAI,CAAC,OAAO,EAAE,CAAC;YACjB,MAAM,IAAI,KAAK,CAAC,sCAAsC,CAAC,CAAC;QAC1D,CAAC;QACD,MAAM,IAAI,GAAG,IAAI,CAAC,WAAW,CAAC,GAAG,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC;QACjD,IAAI,CAAC,IAAI,IAAI,IAAI,CAAC,MAAM,KAAK,CAAC;YAAE,OAAO;QAEvC,MAAM,GAAG,GAAG,KAAK,IAAI,EAAE;YACrB,KAAK,MAAM,GAAG,IAAI,IAAI,EAAE,CAAC;gBACvB,IAAI,CAAC;oBACH,MAAM,GAAG,CAAC,GAAG,CAAC,CAAC;gBACjB,CAAC;gBAAC,OAAO,GAAG,EAAE,CAAC;oBACb,sCAAsC;oBACtC,OAAO,CAAC,KAAK,CAAC,oCAAoC,GAAG,CAAC,SAAS,UAAU,EAAE,GAAG,CAAC,CAAC;gBAClF,CAAC;YACH,CAAC;QACH,CAAC,CAAC;QACF,MAAM,OAAO,GAAG,GAAG,EAAE,CAAC;QACtB,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;QAC5B,MAAM,OAAO,CAAC;IAChB,CAAC;IAED,SAAS,CAAC,SAAiB,EAAE,UAAyB;QACpD,IAAI,IAAI,CAAC,OAAO,EAAE,CAAC;YACjB,MAAM,IAAI,KAAK,CAAC,wCAAwC,CAAC,CAAC;QAC5D,CAAC;QACD,MAAM,IAAI,GAAG,IAAI,CAAC,WAAW,CAAC,GAAG,CAAC,SAAS,CAAC,IAAI,EAAE,CAAC;QACnD,IAAI,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC;QACtB,IAAI,CAAC,WAAW,CAAC,GAAG,CAAC,SAAS,EAAE,IAAI,CAAC,CAAC;IACxC,CAAC;IAED,KAAK,CAAC,IAAI;QACR,IAAI,CAAC,OAAO,GAAG,IAAI,CAAC;QACpB,MAAM,OAAO,CAAC,UAAU,CAAC,CAAC,GAAG,IAAI,CAAC,QAAQ,CAAC,CAAC,CAAC;QAC7C,IAAI,CAAC,WAAW,CAAC,KAAK,EAAE,CAAC;IAC3B,CAAC;CACF"}
|
package/package.json
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@nwire/bus",
|
|
3
|
+
"version": "0.7.0",
|
|
4
|
+
"description": "Nwire — cross-service event bus contract. EventBus interface + InMemoryEventBus default. Production adapters land as @nwire/bus-nats, @nwire/bus-redis, @nwire/bus-kafka.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"bus",
|
|
7
|
+
"event-bus",
|
|
8
|
+
"messaging",
|
|
9
|
+
"nwire",
|
|
10
|
+
"pubsub"
|
|
11
|
+
],
|
|
12
|
+
"files": [
|
|
13
|
+
"dist",
|
|
14
|
+
"README.md"
|
|
15
|
+
],
|
|
16
|
+
"type": "module",
|
|
17
|
+
"main": "./dist/bus.js",
|
|
18
|
+
"types": "./dist/bus.d.ts",
|
|
19
|
+
"exports": {
|
|
20
|
+
".": {
|
|
21
|
+
"import": "./dist/bus.js",
|
|
22
|
+
"types": "./dist/bus.d.ts"
|
|
23
|
+
}
|
|
24
|
+
},
|
|
25
|
+
"publishConfig": {
|
|
26
|
+
"access": "public"
|
|
27
|
+
},
|
|
28
|
+
"dependencies": {
|
|
29
|
+
"@nwire/envelope": "0.7.0"
|
|
30
|
+
},
|
|
31
|
+
"devDependencies": {
|
|
32
|
+
"@types/node": "^22.19.9",
|
|
33
|
+
"typescript": "^5.9.3",
|
|
34
|
+
"vitest": "^4.0.18"
|
|
35
|
+
},
|
|
36
|
+
"scripts": {
|
|
37
|
+
"build": "tsc && node ../../scripts/fix-dist-extensions.mjs dist",
|
|
38
|
+
"dev": "tsc --watch",
|
|
39
|
+
"typecheck": "tsc --noEmit"
|
|
40
|
+
}
|
|
41
|
+
}
|