@contractspec/lib.bus 1.56.1 → 1.58.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/auditableBus.d.ts +76 -81
- package/dist/auditableBus.d.ts.map +1 -1
- package/dist/auditableBus.js +149 -132
- package/dist/browser/auditableBus.js +153 -0
- package/dist/browser/eventBus.js +28 -0
- package/dist/browser/filtering.js +147 -0
- package/dist/browser/inMemoryBus.js +25 -0
- package/dist/browser/index.js +373 -0
- package/dist/browser/metadata.js +60 -0
- package/dist/browser/subscriber.js +37 -0
- package/dist/eventBus.d.ts +9 -14
- package/dist/eventBus.d.ts.map +1 -1
- package/dist/eventBus.js +23 -26
- package/dist/filtering.d.ts +53 -58
- package/dist/filtering.d.ts.map +1 -1
- package/dist/filtering.js +139 -125
- package/dist/inMemoryBus.d.ts +6 -11
- package/dist/inMemoryBus.d.ts.map +1 -1
- package/dist/inMemoryBus.js +25 -28
- package/dist/index.d.ts +7 -7
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +374 -8
- package/dist/metadata.d.ts +60 -63
- package/dist/metadata.d.ts.map +1 -1
- package/dist/metadata.js +55 -76
- package/dist/node/auditableBus.js +153 -0
- package/dist/node/eventBus.js +28 -0
- package/dist/node/filtering.js +147 -0
- package/dist/node/inMemoryBus.js +25 -0
- package/dist/node/index.js +373 -0
- package/dist/node/metadata.js +60 -0
- package/dist/node/subscriber.js +37 -0
- package/dist/subscriber.d.ts +6 -10
- package/dist/subscriber.d.ts.map +1 -1
- package/dist/subscriber.js +35 -16
- package/package.json +69 -28
- package/dist/auditableBus.js.map +0 -1
- package/dist/eventBus.js.map +0 -1
- package/dist/filtering.js.map +0 -1
- package/dist/inMemoryBus.js.map +0 -1
- package/dist/metadata.js.map +0 -1
- package/dist/subscriber.js.map +0 -1
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
// src/eventBus.ts
|
|
2
|
+
import {
|
|
3
|
+
eventKey
|
|
4
|
+
} from "@contractspec/lib.contracts";
|
|
5
|
+
function encodeEvent(envelope) {
|
|
6
|
+
return new TextEncoder().encode(JSON.stringify(envelope));
|
|
7
|
+
}
|
|
8
|
+
function decodeEvent(data) {
|
|
9
|
+
return JSON.parse(new TextDecoder().decode(data));
|
|
10
|
+
}
|
|
11
|
+
function makePublisher(bus, spec) {
|
|
12
|
+
return async (payload, traceId) => {
|
|
13
|
+
const envelope = {
|
|
14
|
+
id: crypto.randomUUID(),
|
|
15
|
+
occurredAt: new Date().toISOString(),
|
|
16
|
+
key: spec.meta.key,
|
|
17
|
+
version: spec.meta.version,
|
|
18
|
+
payload,
|
|
19
|
+
traceId
|
|
20
|
+
};
|
|
21
|
+
await bus.publish(eventKey(spec.meta.key, spec.meta.version), encodeEvent(envelope));
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
export {
|
|
25
|
+
makePublisher,
|
|
26
|
+
encodeEvent,
|
|
27
|
+
decodeEvent
|
|
28
|
+
};
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
// src/eventBus.ts
|
|
2
|
+
import {
|
|
3
|
+
eventKey
|
|
4
|
+
} from "@contractspec/lib.contracts";
|
|
5
|
+
function encodeEvent(envelope) {
|
|
6
|
+
return new TextEncoder().encode(JSON.stringify(envelope));
|
|
7
|
+
}
|
|
8
|
+
function decodeEvent(data) {
|
|
9
|
+
return JSON.parse(new TextDecoder().decode(data));
|
|
10
|
+
}
|
|
11
|
+
function makePublisher(bus, spec) {
|
|
12
|
+
return async (payload, traceId) => {
|
|
13
|
+
const envelope = {
|
|
14
|
+
id: crypto.randomUUID(),
|
|
15
|
+
occurredAt: new Date().toISOString(),
|
|
16
|
+
key: spec.meta.key,
|
|
17
|
+
version: spec.meta.version,
|
|
18
|
+
payload,
|
|
19
|
+
traceId
|
|
20
|
+
};
|
|
21
|
+
await bus.publish(eventKey(spec.meta.key, spec.meta.version), encodeEvent(envelope));
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// src/filtering.ts
|
|
26
|
+
import { satisfies } from "compare-versions";
|
|
27
|
+
function matchesFilter(envelope, filter) {
|
|
28
|
+
if (filter.eventName) {
|
|
29
|
+
const pattern = filter.eventName.replace(/\*/g, ".*");
|
|
30
|
+
const regex = new RegExp(`^${pattern}$`);
|
|
31
|
+
if (!regex.test(envelope.key)) {
|
|
32
|
+
return false;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
if (filter.domain) {
|
|
36
|
+
if (!envelope.key.startsWith(filter.domain + ".")) {
|
|
37
|
+
return false;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
if (filter.version) {
|
|
41
|
+
if (!envelope.version || !satisfies(envelope.version, filter.version)) {
|
|
42
|
+
return false;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
if (filter.actorId && envelope.metadata?.actorId !== filter.actorId) {
|
|
46
|
+
return false;
|
|
47
|
+
}
|
|
48
|
+
if (filter.orgId && envelope.metadata?.orgId !== filter.orgId) {
|
|
49
|
+
return false;
|
|
50
|
+
}
|
|
51
|
+
if (filter.tags) {
|
|
52
|
+
const eventTags = envelope.metadata?.tags ?? {};
|
|
53
|
+
for (const [key, value] of Object.entries(filter.tags)) {
|
|
54
|
+
if (eventTags[key] !== value) {
|
|
55
|
+
return false;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
if (filter.predicate && !filter.predicate(envelope)) {
|
|
60
|
+
return false;
|
|
61
|
+
}
|
|
62
|
+
return true;
|
|
63
|
+
}
|
|
64
|
+
function createFilteredSubscriber(bus, filter, handler) {
|
|
65
|
+
return async (topic) => {
|
|
66
|
+
return bus.subscribe(topic, async (bytes) => {
|
|
67
|
+
const envelope = decodeEvent(bytes);
|
|
68
|
+
if (matchesFilter(envelope, filter)) {
|
|
69
|
+
await handler(envelope);
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
class DomainEventBus {
|
|
76
|
+
bus;
|
|
77
|
+
domain;
|
|
78
|
+
constructor(bus, domain) {
|
|
79
|
+
this.bus = bus;
|
|
80
|
+
this.domain = domain;
|
|
81
|
+
}
|
|
82
|
+
async publish(spec, payload, metadata) {
|
|
83
|
+
const eventName = spec.meta.key.startsWith(this.domain + ".") ? spec.meta.key : `${this.domain}.${spec.meta.key}`;
|
|
84
|
+
const envelope = {
|
|
85
|
+
id: crypto.randomUUID(),
|
|
86
|
+
occurredAt: new Date().toISOString(),
|
|
87
|
+
key: eventName,
|
|
88
|
+
version: spec.meta.version,
|
|
89
|
+
payload,
|
|
90
|
+
metadata
|
|
91
|
+
};
|
|
92
|
+
const bytes = new TextEncoder().encode(JSON.stringify(envelope));
|
|
93
|
+
await this.bus.publish(`${eventName}.v${spec.meta.version}`, bytes);
|
|
94
|
+
}
|
|
95
|
+
async subscribeAll(handler) {
|
|
96
|
+
return this.bus.subscribe(`${this.domain}.*`, async (bytes) => {
|
|
97
|
+
const envelope = decodeEvent(bytes);
|
|
98
|
+
await handler(envelope);
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
async subscribeFiltered(filter, handler) {
|
|
102
|
+
const fullFilter = {
|
|
103
|
+
...filter,
|
|
104
|
+
domain: this.domain
|
|
105
|
+
};
|
|
106
|
+
return this.bus.subscribe(`${this.domain}.*`, async (bytes) => {
|
|
107
|
+
const envelope = decodeEvent(bytes);
|
|
108
|
+
if (matchesFilter(envelope, fullFilter)) {
|
|
109
|
+
await handler(envelope);
|
|
110
|
+
}
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
function createDomainBus(bus, domain) {
|
|
115
|
+
return new DomainEventBus(bus, domain);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
class EventRouter {
|
|
119
|
+
routes = [];
|
|
120
|
+
route(filter, handler) {
|
|
121
|
+
this.routes.push({ filter, handler });
|
|
122
|
+
return this;
|
|
123
|
+
}
|
|
124
|
+
async dispatch(envelope) {
|
|
125
|
+
const matchingRoutes = this.routes.filter((r) => matchesFilter(envelope, r.filter));
|
|
126
|
+
await Promise.all(matchingRoutes.map((r) => r.handler(envelope)));
|
|
127
|
+
}
|
|
128
|
+
createSubscriber(bus) {
|
|
129
|
+
return async (topic) => {
|
|
130
|
+
return bus.subscribe(topic, async (bytes) => {
|
|
131
|
+
const envelope = decodeEvent(bytes);
|
|
132
|
+
await this.dispatch(envelope);
|
|
133
|
+
});
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
function createEventRouter() {
|
|
138
|
+
return new EventRouter;
|
|
139
|
+
}
|
|
140
|
+
export {
|
|
141
|
+
matchesFilter,
|
|
142
|
+
createFilteredSubscriber,
|
|
143
|
+
createEventRouter,
|
|
144
|
+
createDomainBus,
|
|
145
|
+
EventRouter,
|
|
146
|
+
DomainEventBus
|
|
147
|
+
};
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
// src/inMemoryBus.ts
|
|
2
|
+
class InMemoryBus {
|
|
3
|
+
listeners = new Map;
|
|
4
|
+
async publish(topic, payload) {
|
|
5
|
+
const handlers = this.listeners.get(topic);
|
|
6
|
+
if (!handlers)
|
|
7
|
+
return;
|
|
8
|
+
await Promise.all([...handlers].map((h) => h(payload)));
|
|
9
|
+
}
|
|
10
|
+
async subscribe(topic, handler) {
|
|
11
|
+
const topicStr = String(topic);
|
|
12
|
+
let set = this.listeners.get(topicStr);
|
|
13
|
+
if (!set) {
|
|
14
|
+
set = new Set;
|
|
15
|
+
this.listeners.set(topicStr, set);
|
|
16
|
+
}
|
|
17
|
+
set.add(handler);
|
|
18
|
+
return async () => {
|
|
19
|
+
set?.delete(handler);
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
export {
|
|
24
|
+
InMemoryBus
|
|
25
|
+
};
|
|
@@ -0,0 +1,373 @@
|
|
|
1
|
+
// src/eventBus.ts
|
|
2
|
+
import {
|
|
3
|
+
eventKey
|
|
4
|
+
} from "@contractspec/lib.contracts";
|
|
5
|
+
function encodeEvent(envelope) {
|
|
6
|
+
return new TextEncoder().encode(JSON.stringify(envelope));
|
|
7
|
+
}
|
|
8
|
+
function decodeEvent(data) {
|
|
9
|
+
return JSON.parse(new TextDecoder().decode(data));
|
|
10
|
+
}
|
|
11
|
+
function makePublisher(bus, spec) {
|
|
12
|
+
return async (payload, traceId) => {
|
|
13
|
+
const envelope = {
|
|
14
|
+
id: crypto.randomUUID(),
|
|
15
|
+
occurredAt: new Date().toISOString(),
|
|
16
|
+
key: spec.meta.key,
|
|
17
|
+
version: spec.meta.version,
|
|
18
|
+
payload,
|
|
19
|
+
traceId
|
|
20
|
+
};
|
|
21
|
+
await bus.publish(eventKey(spec.meta.key, spec.meta.version), encodeEvent(envelope));
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// src/auditableBus.ts
|
|
26
|
+
import {
|
|
27
|
+
eventKey as eventKey2
|
|
28
|
+
} from "@contractspec/lib.contracts";
|
|
29
|
+
class AuditableEventBus {
|
|
30
|
+
bus;
|
|
31
|
+
storage;
|
|
32
|
+
defaultMetadata;
|
|
33
|
+
shouldAudit;
|
|
34
|
+
transformAuditRecord;
|
|
35
|
+
constructor(options) {
|
|
36
|
+
this.bus = options.bus;
|
|
37
|
+
this.storage = options.storage;
|
|
38
|
+
this.defaultMetadata = options.defaultMetadata ?? {};
|
|
39
|
+
this.shouldAudit = options.shouldAudit ?? (() => true);
|
|
40
|
+
this.transformAuditRecord = options.transformAuditRecord;
|
|
41
|
+
}
|
|
42
|
+
async publish(topic, bytes) {
|
|
43
|
+
await this.bus.publish(topic, bytes);
|
|
44
|
+
if (this.storage) {
|
|
45
|
+
try {
|
|
46
|
+
const envelope = decodeEvent(bytes);
|
|
47
|
+
if (this.shouldAudit(envelope.key, envelope)) {
|
|
48
|
+
let record = {
|
|
49
|
+
id: crypto.randomUUID(),
|
|
50
|
+
eventKey: envelope.key,
|
|
51
|
+
eventVersion: envelope.version,
|
|
52
|
+
payload: envelope.payload,
|
|
53
|
+
metadata: {
|
|
54
|
+
...this.defaultMetadata,
|
|
55
|
+
...envelope.metadata
|
|
56
|
+
},
|
|
57
|
+
occurredAt: envelope.occurredAt,
|
|
58
|
+
traceId: envelope.traceId,
|
|
59
|
+
recordedAt: new Date
|
|
60
|
+
};
|
|
61
|
+
if (this.transformAuditRecord) {
|
|
62
|
+
record = this.transformAuditRecord(record);
|
|
63
|
+
}
|
|
64
|
+
await this.storage.store(record);
|
|
65
|
+
}
|
|
66
|
+
} catch (error) {
|
|
67
|
+
console.error("Failed to record audit:", error);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
async subscribe(topic, handler) {
|
|
72
|
+
return this.bus.subscribe(topic, handler);
|
|
73
|
+
}
|
|
74
|
+
async queryAudit(options) {
|
|
75
|
+
if (!this.storage?.query) {
|
|
76
|
+
throw new Error("Audit storage does not support querying");
|
|
77
|
+
}
|
|
78
|
+
return this.storage.query(options);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
function makeAuditablePublisher(bus, spec, defaultMetadata) {
|
|
82
|
+
return async (payload, options) => {
|
|
83
|
+
const envelope = {
|
|
84
|
+
id: crypto.randomUUID(),
|
|
85
|
+
occurredAt: new Date().toISOString(),
|
|
86
|
+
key: spec.meta.key,
|
|
87
|
+
version: spec.meta.version,
|
|
88
|
+
payload,
|
|
89
|
+
traceId: options?.traceId,
|
|
90
|
+
metadata: {
|
|
91
|
+
...defaultMetadata,
|
|
92
|
+
...options?.metadata
|
|
93
|
+
}
|
|
94
|
+
};
|
|
95
|
+
await bus.publish(eventKey2(spec.meta.key, spec.meta.version), encodeEvent(envelope));
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
class InMemoryAuditStorage {
|
|
100
|
+
records = [];
|
|
101
|
+
async store(record) {
|
|
102
|
+
this.records.push(record);
|
|
103
|
+
}
|
|
104
|
+
async query(options) {
|
|
105
|
+
let results = [...this.records];
|
|
106
|
+
if (options.eventKey) {
|
|
107
|
+
results = results.filter((r) => r.eventKey === options.eventKey);
|
|
108
|
+
}
|
|
109
|
+
if (options.actorId) {
|
|
110
|
+
results = results.filter((r) => r.metadata?.actorId === options.actorId);
|
|
111
|
+
}
|
|
112
|
+
if (options.targetId) {
|
|
113
|
+
results = results.filter((r) => r.metadata?.targetId === options.targetId);
|
|
114
|
+
}
|
|
115
|
+
if (options.orgId) {
|
|
116
|
+
results = results.filter((r) => r.metadata?.orgId === options.orgId);
|
|
117
|
+
}
|
|
118
|
+
if (options.traceId) {
|
|
119
|
+
results = results.filter((r) => r.traceId === options.traceId);
|
|
120
|
+
}
|
|
121
|
+
if (options.from) {
|
|
122
|
+
const from = options.from;
|
|
123
|
+
results = results.filter((r) => new Date(r.occurredAt) >= from);
|
|
124
|
+
}
|
|
125
|
+
if (options.to) {
|
|
126
|
+
const to = options.to;
|
|
127
|
+
results = results.filter((r) => new Date(r.occurredAt) <= to);
|
|
128
|
+
}
|
|
129
|
+
results.sort((a, b) => new Date(b.occurredAt).getTime() - new Date(a.occurredAt).getTime());
|
|
130
|
+
const offset = options.offset ?? 0;
|
|
131
|
+
const limit = options.limit ?? 100;
|
|
132
|
+
return results.slice(offset, offset + limit);
|
|
133
|
+
}
|
|
134
|
+
getAll() {
|
|
135
|
+
return [...this.records];
|
|
136
|
+
}
|
|
137
|
+
clear() {
|
|
138
|
+
this.records = [];
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
function createAuditableEventBus(bus, options) {
|
|
142
|
+
return new AuditableEventBus({
|
|
143
|
+
bus,
|
|
144
|
+
storage: options?.storage ?? new InMemoryAuditStorage,
|
|
145
|
+
...options
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// src/filtering.ts
|
|
150
|
+
import { satisfies } from "compare-versions";
|
|
151
|
+
function matchesFilter(envelope, filter) {
|
|
152
|
+
if (filter.eventName) {
|
|
153
|
+
const pattern = filter.eventName.replace(/\*/g, ".*");
|
|
154
|
+
const regex = new RegExp(`^${pattern}$`);
|
|
155
|
+
if (!regex.test(envelope.key)) {
|
|
156
|
+
return false;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
if (filter.domain) {
|
|
160
|
+
if (!envelope.key.startsWith(filter.domain + ".")) {
|
|
161
|
+
return false;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
if (filter.version) {
|
|
165
|
+
if (!envelope.version || !satisfies(envelope.version, filter.version)) {
|
|
166
|
+
return false;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
if (filter.actorId && envelope.metadata?.actorId !== filter.actorId) {
|
|
170
|
+
return false;
|
|
171
|
+
}
|
|
172
|
+
if (filter.orgId && envelope.metadata?.orgId !== filter.orgId) {
|
|
173
|
+
return false;
|
|
174
|
+
}
|
|
175
|
+
if (filter.tags) {
|
|
176
|
+
const eventTags = envelope.metadata?.tags ?? {};
|
|
177
|
+
for (const [key, value] of Object.entries(filter.tags)) {
|
|
178
|
+
if (eventTags[key] !== value) {
|
|
179
|
+
return false;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
if (filter.predicate && !filter.predicate(envelope)) {
|
|
184
|
+
return false;
|
|
185
|
+
}
|
|
186
|
+
return true;
|
|
187
|
+
}
|
|
188
|
+
function createFilteredSubscriber(bus, filter, handler) {
|
|
189
|
+
return async (topic) => {
|
|
190
|
+
return bus.subscribe(topic, async (bytes) => {
|
|
191
|
+
const envelope = decodeEvent(bytes);
|
|
192
|
+
if (matchesFilter(envelope, filter)) {
|
|
193
|
+
await handler(envelope);
|
|
194
|
+
}
|
|
195
|
+
});
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
class DomainEventBus {
|
|
200
|
+
bus;
|
|
201
|
+
domain;
|
|
202
|
+
constructor(bus, domain) {
|
|
203
|
+
this.bus = bus;
|
|
204
|
+
this.domain = domain;
|
|
205
|
+
}
|
|
206
|
+
async publish(spec, payload, metadata) {
|
|
207
|
+
const eventName = spec.meta.key.startsWith(this.domain + ".") ? spec.meta.key : `${this.domain}.${spec.meta.key}`;
|
|
208
|
+
const envelope = {
|
|
209
|
+
id: crypto.randomUUID(),
|
|
210
|
+
occurredAt: new Date().toISOString(),
|
|
211
|
+
key: eventName,
|
|
212
|
+
version: spec.meta.version,
|
|
213
|
+
payload,
|
|
214
|
+
metadata
|
|
215
|
+
};
|
|
216
|
+
const bytes = new TextEncoder().encode(JSON.stringify(envelope));
|
|
217
|
+
await this.bus.publish(`${eventName}.v${spec.meta.version}`, bytes);
|
|
218
|
+
}
|
|
219
|
+
async subscribeAll(handler) {
|
|
220
|
+
return this.bus.subscribe(`${this.domain}.*`, async (bytes) => {
|
|
221
|
+
const envelope = decodeEvent(bytes);
|
|
222
|
+
await handler(envelope);
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
async subscribeFiltered(filter, handler) {
|
|
226
|
+
const fullFilter = {
|
|
227
|
+
...filter,
|
|
228
|
+
domain: this.domain
|
|
229
|
+
};
|
|
230
|
+
return this.bus.subscribe(`${this.domain}.*`, async (bytes) => {
|
|
231
|
+
const envelope = decodeEvent(bytes);
|
|
232
|
+
if (matchesFilter(envelope, fullFilter)) {
|
|
233
|
+
await handler(envelope);
|
|
234
|
+
}
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
function createDomainBus(bus, domain) {
|
|
239
|
+
return new DomainEventBus(bus, domain);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
class EventRouter {
|
|
243
|
+
routes = [];
|
|
244
|
+
route(filter, handler) {
|
|
245
|
+
this.routes.push({ filter, handler });
|
|
246
|
+
return this;
|
|
247
|
+
}
|
|
248
|
+
async dispatch(envelope) {
|
|
249
|
+
const matchingRoutes = this.routes.filter((r) => matchesFilter(envelope, r.filter));
|
|
250
|
+
await Promise.all(matchingRoutes.map((r) => r.handler(envelope)));
|
|
251
|
+
}
|
|
252
|
+
createSubscriber(bus) {
|
|
253
|
+
return async (topic) => {
|
|
254
|
+
return bus.subscribe(topic, async (bytes) => {
|
|
255
|
+
const envelope = decodeEvent(bytes);
|
|
256
|
+
await this.dispatch(envelope);
|
|
257
|
+
});
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
function createEventRouter() {
|
|
262
|
+
return new EventRouter;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// src/inMemoryBus.ts
|
|
266
|
+
class InMemoryBus {
|
|
267
|
+
listeners = new Map;
|
|
268
|
+
async publish(topic, payload) {
|
|
269
|
+
const handlers = this.listeners.get(topic);
|
|
270
|
+
if (!handlers)
|
|
271
|
+
return;
|
|
272
|
+
await Promise.all([...handlers].map((h) => h(payload)));
|
|
273
|
+
}
|
|
274
|
+
async subscribe(topic, handler) {
|
|
275
|
+
const topicStr = String(topic);
|
|
276
|
+
let set = this.listeners.get(topicStr);
|
|
277
|
+
if (!set) {
|
|
278
|
+
set = new Set;
|
|
279
|
+
this.listeners.set(topicStr, set);
|
|
280
|
+
}
|
|
281
|
+
set.add(handler);
|
|
282
|
+
return async () => {
|
|
283
|
+
set?.delete(handler);
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// src/subscriber.ts
|
|
289
|
+
async function subscribeEvent(bus, spec, handler) {
|
|
290
|
+
const topic = `${spec.meta.key}.v${spec.meta.version}`;
|
|
291
|
+
return bus.subscribe(topic, async (u8) => {
|
|
292
|
+
const env = decodeEvent(u8);
|
|
293
|
+
if (env.key !== spec.meta.key || env.version !== spec.meta.version)
|
|
294
|
+
return;
|
|
295
|
+
await handler(env.payload, { traceId: env.traceId, deliveryId: env.id });
|
|
296
|
+
});
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// src/metadata.ts
|
|
300
|
+
function createMetadataFromContext(context) {
|
|
301
|
+
return {
|
|
302
|
+
actorId: context.userId,
|
|
303
|
+
actorType: context.userId ? "user" : "system",
|
|
304
|
+
orgId: context.orgId,
|
|
305
|
+
tenantId: context.orgId,
|
|
306
|
+
sessionId: context.sessionId,
|
|
307
|
+
requestId: context.requestId,
|
|
308
|
+
traceId: context.traceId || crypto.randomUUID(),
|
|
309
|
+
clientIp: context.clientIp,
|
|
310
|
+
userAgent: context.userAgent
|
|
311
|
+
};
|
|
312
|
+
}
|
|
313
|
+
function mergeMetadata(base, overrides) {
|
|
314
|
+
return {
|
|
315
|
+
...base,
|
|
316
|
+
...overrides,
|
|
317
|
+
tags: {
|
|
318
|
+
...base.tags,
|
|
319
|
+
...overrides.tags
|
|
320
|
+
}
|
|
321
|
+
};
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
class MetadataContext {
|
|
325
|
+
metadata;
|
|
326
|
+
constructor(initial = {}) {
|
|
327
|
+
this.metadata = { ...initial };
|
|
328
|
+
if (!this.metadata.traceId) {
|
|
329
|
+
this.metadata.traceId = crypto.randomUUID();
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
get() {
|
|
333
|
+
return { ...this.metadata };
|
|
334
|
+
}
|
|
335
|
+
set(key, value) {
|
|
336
|
+
this.metadata[key] = value;
|
|
337
|
+
return this;
|
|
338
|
+
}
|
|
339
|
+
tag(key, value) {
|
|
340
|
+
this.metadata.tags = { ...this.metadata.tags, [key]: value };
|
|
341
|
+
return this;
|
|
342
|
+
}
|
|
343
|
+
child(overrides = {}) {
|
|
344
|
+
return new MetadataContext(mergeMetadata(this.metadata, {
|
|
345
|
+
...overrides,
|
|
346
|
+
spanId: crypto.randomUUID()
|
|
347
|
+
}));
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
function createMetadataContext(initial) {
|
|
351
|
+
return new MetadataContext(initial);
|
|
352
|
+
}
|
|
353
|
+
export {
|
|
354
|
+
subscribeEvent,
|
|
355
|
+
mergeMetadata,
|
|
356
|
+
matchesFilter,
|
|
357
|
+
makePublisher,
|
|
358
|
+
makeAuditablePublisher,
|
|
359
|
+
encodeEvent,
|
|
360
|
+
decodeEvent,
|
|
361
|
+
createMetadataFromContext,
|
|
362
|
+
createMetadataContext,
|
|
363
|
+
createFilteredSubscriber,
|
|
364
|
+
createEventRouter,
|
|
365
|
+
createDomainBus,
|
|
366
|
+
createAuditableEventBus,
|
|
367
|
+
MetadataContext,
|
|
368
|
+
InMemoryBus,
|
|
369
|
+
InMemoryAuditStorage,
|
|
370
|
+
EventRouter,
|
|
371
|
+
DomainEventBus,
|
|
372
|
+
AuditableEventBus
|
|
373
|
+
};
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
// src/metadata.ts
|
|
2
|
+
function createMetadataFromContext(context) {
|
|
3
|
+
return {
|
|
4
|
+
actorId: context.userId,
|
|
5
|
+
actorType: context.userId ? "user" : "system",
|
|
6
|
+
orgId: context.orgId,
|
|
7
|
+
tenantId: context.orgId,
|
|
8
|
+
sessionId: context.sessionId,
|
|
9
|
+
requestId: context.requestId,
|
|
10
|
+
traceId: context.traceId || crypto.randomUUID(),
|
|
11
|
+
clientIp: context.clientIp,
|
|
12
|
+
userAgent: context.userAgent
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
function mergeMetadata(base, overrides) {
|
|
16
|
+
return {
|
|
17
|
+
...base,
|
|
18
|
+
...overrides,
|
|
19
|
+
tags: {
|
|
20
|
+
...base.tags,
|
|
21
|
+
...overrides.tags
|
|
22
|
+
}
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
class MetadataContext {
|
|
27
|
+
metadata;
|
|
28
|
+
constructor(initial = {}) {
|
|
29
|
+
this.metadata = { ...initial };
|
|
30
|
+
if (!this.metadata.traceId) {
|
|
31
|
+
this.metadata.traceId = crypto.randomUUID();
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
get() {
|
|
35
|
+
return { ...this.metadata };
|
|
36
|
+
}
|
|
37
|
+
set(key, value) {
|
|
38
|
+
this.metadata[key] = value;
|
|
39
|
+
return this;
|
|
40
|
+
}
|
|
41
|
+
tag(key, value) {
|
|
42
|
+
this.metadata.tags = { ...this.metadata.tags, [key]: value };
|
|
43
|
+
return this;
|
|
44
|
+
}
|
|
45
|
+
child(overrides = {}) {
|
|
46
|
+
return new MetadataContext(mergeMetadata(this.metadata, {
|
|
47
|
+
...overrides,
|
|
48
|
+
spanId: crypto.randomUUID()
|
|
49
|
+
}));
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
function createMetadataContext(initial) {
|
|
53
|
+
return new MetadataContext(initial);
|
|
54
|
+
}
|
|
55
|
+
export {
|
|
56
|
+
mergeMetadata,
|
|
57
|
+
createMetadataFromContext,
|
|
58
|
+
createMetadataContext,
|
|
59
|
+
MetadataContext
|
|
60
|
+
};
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
// src/eventBus.ts
|
|
2
|
+
import {
|
|
3
|
+
eventKey
|
|
4
|
+
} from "@contractspec/lib.contracts";
|
|
5
|
+
function encodeEvent(envelope) {
|
|
6
|
+
return new TextEncoder().encode(JSON.stringify(envelope));
|
|
7
|
+
}
|
|
8
|
+
function decodeEvent(data) {
|
|
9
|
+
return JSON.parse(new TextDecoder().decode(data));
|
|
10
|
+
}
|
|
11
|
+
function makePublisher(bus, spec) {
|
|
12
|
+
return async (payload, traceId) => {
|
|
13
|
+
const envelope = {
|
|
14
|
+
id: crypto.randomUUID(),
|
|
15
|
+
occurredAt: new Date().toISOString(),
|
|
16
|
+
key: spec.meta.key,
|
|
17
|
+
version: spec.meta.version,
|
|
18
|
+
payload,
|
|
19
|
+
traceId
|
|
20
|
+
};
|
|
21
|
+
await bus.publish(eventKey(spec.meta.key, spec.meta.version), encodeEvent(envelope));
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// src/subscriber.ts
|
|
26
|
+
async function subscribeEvent(bus, spec, handler) {
|
|
27
|
+
const topic = `${spec.meta.key}.v${spec.meta.version}`;
|
|
28
|
+
return bus.subscribe(topic, async (u8) => {
|
|
29
|
+
const env = decodeEvent(u8);
|
|
30
|
+
if (env.key !== spec.meta.key || env.version !== spec.meta.version)
|
|
31
|
+
return;
|
|
32
|
+
await handler(env.payload, { traceId: env.traceId, deliveryId: env.id });
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
export {
|
|
36
|
+
subscribeEvent
|
|
37
|
+
};
|
package/dist/eventBus.d.ts
CHANGED
|
@@ -1,22 +1,17 @@
|
|
|
1
|
-
import { EventEnvelope, EventKey, EventSpec } from
|
|
2
|
-
import { AnySchemaModel } from
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
subscribe: (topic: EventKey | string,
|
|
8
|
-
// allow wildcard if your broker supports it
|
|
9
|
-
handler: (bytes: Uint8Array) => Promise<void>) => Promise<() => Promise<void>>;
|
|
1
|
+
import { type EventEnvelope, type EventKey, type EventSpec } from '@contractspec/lib.contracts';
|
|
2
|
+
import type { AnySchemaModel } from '@contractspec/lib.schema';
|
|
3
|
+
export interface EventBus {
|
|
4
|
+
publish: (topic: EventKey, bytes: Uint8Array) => Promise<void>;
|
|
5
|
+
subscribe: (topic: EventKey | string, // allow wildcard if your broker supports it
|
|
6
|
+
handler: (bytes: Uint8Array) => Promise<void>) => Promise<() => Promise<void>>;
|
|
10
7
|
}
|
|
11
8
|
/** Helper to encode a typed event envelope into JSON string */
|
|
12
|
-
declare function encodeEvent<T>(envelope: EventEnvelope<T>): Uint8Array;
|
|
9
|
+
export declare function encodeEvent<T>(envelope: EventEnvelope<T>): Uint8Array;
|
|
13
10
|
/** Helper to decode JSON string into a typed event envelope */
|
|
14
|
-
declare function decodeEvent<T>(data: Uint8Array): EventEnvelope<T>;
|
|
11
|
+
export declare function decodeEvent<T>(data: Uint8Array): EventEnvelope<T>;
|
|
15
12
|
/**
|
|
16
13
|
* Create a typed publisher function for a given event spec.
|
|
17
14
|
* It ensures payload conformance at compile time and builds the correct topic.
|
|
18
15
|
*/
|
|
19
|
-
declare function makePublisher<T extends AnySchemaModel>(bus: EventBus, spec: EventSpec<T>): (payload: T, traceId?: string) => Promise<void>;
|
|
20
|
-
//#endregion
|
|
21
|
-
export { EventBus, decodeEvent, encodeEvent, makePublisher };
|
|
16
|
+
export declare function makePublisher<T extends AnySchemaModel>(bus: EventBus, spec: EventSpec<T>): (payload: T, traceId?: string) => Promise<void>;
|
|
22
17
|
//# sourceMappingURL=eventBus.d.ts.map
|