@saga-bus/transport-inmemory 0.1.2

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/README.md ADDED
@@ -0,0 +1,83 @@
1
+ # @saga-bus/transport-inmemory
2
+
3
+ In-memory transport implementation for testing and development.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ pnpm add @saga-bus/transport-inmemory
9
+ ```
10
+
11
+ ## Usage
12
+
13
+ ```typescript
14
+ import { InMemoryTransport } from "@saga-bus/transport-inmemory";
15
+ import { createBus } from "@saga-bus/core";
16
+
17
+ const transport = new InMemoryTransport({
18
+ defaultConcurrency: 10,
19
+ });
20
+
21
+ const bus = createBus({
22
+ transport,
23
+ sagas: [...],
24
+ });
25
+ ```
26
+
27
+ ## Features
28
+
29
+ - Zero external dependencies
30
+ - Configurable concurrency via semaphore
31
+ - Synchronous message delivery (great for tests)
32
+ - No network overhead
33
+
34
+ ## Configuration
35
+
36
+ | Option | Type | Default | Description |
37
+ |--------|------|---------|-------------|
38
+ | `defaultConcurrency` | `number` | `10` | Max concurrent handlers |
39
+
40
+ ## When to Use
41
+
42
+ **Use for:**
43
+ - Unit and integration tests
44
+ - Local development
45
+ - Prototyping and demos
46
+
47
+ **Do not use for:**
48
+ - Production deployments
49
+ - Distributed systems
50
+ - Multi-process applications
51
+
52
+ ## Testing Tips
53
+
54
+ The in-memory transport processes messages synchronously within the same process, making it ideal for testing:
55
+
56
+ ```typescript
57
+ import { TestHarness } from "@saga-bus/test";
58
+ import { InMemoryTransport } from "@saga-bus/transport-inmemory";
59
+
60
+ // TestHarness uses InMemoryTransport internally
61
+ const harness = await TestHarness.create({
62
+ sagas: [{ definition: mySaga, store }],
63
+ });
64
+
65
+ // Messages are processed immediately
66
+ await harness.publish({ type: "OrderSubmitted", orderId: "123" });
67
+ await harness.waitForIdle();
68
+
69
+ // State is immediately available
70
+ const state = await harness.getSagaState("OrderSaga", "123");
71
+ ```
72
+
73
+ ## Limitations
74
+
75
+ - **Single process only**: Messages are not shared between processes
76
+ - **No persistence**: Messages are lost if not consumed
77
+ - **No ordering guarantees**: Unlike production transports with FIFO support
78
+
79
+ For production, use [@saga-bus/transport-rabbitmq](../transport-rabbitmq), [@saga-bus/transport-sqs](../transport-sqs), or [@saga-bus/transport-kafka](../transport-kafka).
80
+
81
+ ## License
82
+
83
+ MIT
package/dist/index.cjs ADDED
@@ -0,0 +1,186 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/index.ts
21
+ var index_exports = {};
22
+ __export(index_exports, {
23
+ InMemoryTransport: () => InMemoryTransport,
24
+ Semaphore: () => Semaphore
25
+ });
26
+ module.exports = __toCommonJS(index_exports);
27
+
28
+ // src/InMemoryTransport.ts
29
+ var import_node_crypto = require("crypto");
30
+
31
+ // src/Semaphore.ts
32
+ var Semaphore = class {
33
+ permits;
34
+ waiting = [];
35
+ constructor(permits) {
36
+ this.permits = permits;
37
+ }
38
+ /**
39
+ * Acquire a permit, waiting if none are available.
40
+ */
41
+ async acquire() {
42
+ if (this.permits > 0) {
43
+ this.permits--;
44
+ return;
45
+ }
46
+ return new Promise((resolve) => {
47
+ this.waiting.push(resolve);
48
+ });
49
+ }
50
+ /**
51
+ * Release a permit, waking a waiting acquirer if any.
52
+ */
53
+ release() {
54
+ const next = this.waiting.shift();
55
+ if (next) {
56
+ next();
57
+ } else {
58
+ this.permits++;
59
+ }
60
+ }
61
+ /**
62
+ * Execute a function with a permit.
63
+ */
64
+ async withPermit(fn) {
65
+ await this.acquire();
66
+ try {
67
+ return await fn();
68
+ } finally {
69
+ this.release();
70
+ }
71
+ }
72
+ /**
73
+ * Get the number of available permits.
74
+ */
75
+ get available() {
76
+ return this.permits;
77
+ }
78
+ /**
79
+ * Get the number of waiting acquirers.
80
+ */
81
+ get waitingCount() {
82
+ return this.waiting.length;
83
+ }
84
+ };
85
+
86
+ // src/InMemoryTransport.ts
87
+ var InMemoryTransport = class {
88
+ subscriptions = /* @__PURE__ */ new Map();
89
+ pendingTimeouts = /* @__PURE__ */ new Set();
90
+ defaultConcurrency;
91
+ started = false;
92
+ constructor(options = {}) {
93
+ this.defaultConcurrency = options.defaultConcurrency ?? 1;
94
+ }
95
+ async start() {
96
+ this.started = true;
97
+ }
98
+ async stop() {
99
+ this.started = false;
100
+ this.pendingTimeouts.forEach((timeout) => clearTimeout(timeout));
101
+ this.pendingTimeouts.clear();
102
+ }
103
+ async subscribe(options, handler) {
104
+ const { endpoint, concurrency = this.defaultConcurrency } = options;
105
+ const subscription = {
106
+ options,
107
+ handler,
108
+ semaphore: new Semaphore(concurrency)
109
+ };
110
+ const existing = this.subscriptions.get(endpoint);
111
+ if (existing) {
112
+ existing.push(subscription);
113
+ } else {
114
+ this.subscriptions.set(endpoint, [subscription]);
115
+ }
116
+ }
117
+ async publish(message, options) {
118
+ const { endpoint, headers = {}, delayMs } = options;
119
+ const envelope = {
120
+ id: (0, import_node_crypto.randomUUID)(),
121
+ type: message.type,
122
+ payload: message,
123
+ headers: { ...headers },
124
+ timestamp: /* @__PURE__ */ new Date(),
125
+ partitionKey: options.key
126
+ };
127
+ if (delayMs && delayMs > 0) {
128
+ const timeout = setTimeout(() => {
129
+ this.pendingTimeouts.delete(timeout);
130
+ void this.deliverToSubscribers(endpoint, envelope);
131
+ }, delayMs);
132
+ this.pendingTimeouts.add(timeout);
133
+ } else {
134
+ setImmediate(() => {
135
+ void this.deliverToSubscribers(endpoint, envelope);
136
+ });
137
+ }
138
+ }
139
+ async deliverToSubscribers(endpoint, envelope) {
140
+ if (!this.started) {
141
+ return;
142
+ }
143
+ const subscriptions = this.subscriptions.get(endpoint);
144
+ if (!subscriptions || subscriptions.length === 0) {
145
+ return;
146
+ }
147
+ const deliveries = subscriptions.map(async (sub) => {
148
+ await sub.semaphore.withPermit(async () => {
149
+ try {
150
+ await sub.handler(envelope);
151
+ } catch (error) {
152
+ console.error(
153
+ `[InMemoryTransport] Handler error for ${endpoint}:`,
154
+ error
155
+ );
156
+ }
157
+ });
158
+ });
159
+ await Promise.all(deliveries);
160
+ }
161
+ /**
162
+ * Get the number of subscriptions for an endpoint.
163
+ * Useful for testing.
164
+ */
165
+ getSubscriptionCount(endpoint) {
166
+ return this.subscriptions.get(endpoint)?.length ?? 0;
167
+ }
168
+ /**
169
+ * Check if the transport is started.
170
+ */
171
+ get isStarted() {
172
+ return this.started;
173
+ }
174
+ /**
175
+ * Clear all subscriptions. Useful for testing.
176
+ */
177
+ clearSubscriptions() {
178
+ this.subscriptions.clear();
179
+ }
180
+ };
181
+ // Annotate the CommonJS export names for ESM import in node:
182
+ 0 && (module.exports = {
183
+ InMemoryTransport,
184
+ Semaphore
185
+ });
186
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/index.ts","../src/InMemoryTransport.ts","../src/Semaphore.ts"],"sourcesContent":["export { InMemoryTransport } from \"./InMemoryTransport.js\";\nexport type { InMemoryTransportOptions } from \"./InMemoryTransport.js\";\nexport { Semaphore } from \"./Semaphore.js\";\n","import { randomUUID } from \"node:crypto\";\nimport type {\n Transport,\n TransportSubscribeOptions,\n TransportPublishOptions,\n BaseMessage,\n MessageEnvelope,\n} from \"@saga-bus/core\";\nimport { Semaphore } from \"./Semaphore.js\";\n\ninterface Subscription<T extends BaseMessage = BaseMessage> {\n options: TransportSubscribeOptions;\n handler: (envelope: MessageEnvelope<T>) => Promise<void>;\n semaphore: Semaphore;\n}\n\nexport interface InMemoryTransportOptions {\n /**\n * Default concurrency for subscriptions (default: 1)\n */\n defaultConcurrency?: number;\n}\n\n/**\n * In-memory transport implementation for testing and local development.\n * Uses a simple pub/sub pattern with concurrency control.\n */\nexport class InMemoryTransport implements Transport {\n private readonly subscriptions = new Map<string, Subscription[]>();\n private readonly pendingTimeouts = new Set<ReturnType<typeof setTimeout>>();\n private readonly defaultConcurrency: number;\n private started = false;\n\n constructor(options: InMemoryTransportOptions = {}) {\n this.defaultConcurrency = options.defaultConcurrency ?? 1;\n }\n\n async start(): Promise<void> {\n this.started = true;\n }\n\n async stop(): Promise<void> {\n this.started = false;\n\n // Clear all pending delayed messages\n this.pendingTimeouts.forEach((timeout) => clearTimeout(timeout));\n this.pendingTimeouts.clear();\n }\n\n async subscribe<TMessage extends BaseMessage>(\n options: TransportSubscribeOptions,\n handler: (envelope: MessageEnvelope<TMessage>) => Promise<void>\n ): Promise<void> {\n const { endpoint, concurrency = this.defaultConcurrency } = options;\n\n const subscription: Subscription<TMessage> = {\n options,\n handler,\n semaphore: new Semaphore(concurrency),\n };\n\n const existing = this.subscriptions.get(endpoint);\n if (existing) {\n existing.push(subscription as Subscription);\n } else {\n this.subscriptions.set(endpoint, [subscription as Subscription]);\n }\n }\n\n async publish<TMessage extends BaseMessage>(\n message: TMessage,\n options: TransportPublishOptions\n ): Promise<void> {\n const { endpoint, headers = {}, delayMs } = options;\n\n const envelope: MessageEnvelope<TMessage> = {\n id: randomUUID(),\n type: message.type,\n payload: message,\n headers: { ...headers },\n timestamp: new Date(),\n partitionKey: options.key,\n };\n\n if (delayMs && delayMs > 0) {\n const timeout = setTimeout(() => {\n this.pendingTimeouts.delete(timeout);\n void this.deliverToSubscribers(endpoint, envelope);\n }, delayMs);\n this.pendingTimeouts.add(timeout);\n } else {\n // Deliver asynchronously to avoid blocking publisher\n setImmediate(() => {\n void this.deliverToSubscribers(endpoint, envelope);\n });\n }\n }\n\n private async deliverToSubscribers<TMessage extends BaseMessage>(\n endpoint: string,\n envelope: MessageEnvelope<TMessage>\n ): Promise<void> {\n if (!this.started) {\n return;\n }\n\n const subscriptions = this.subscriptions.get(endpoint);\n if (!subscriptions || subscriptions.length === 0) {\n return;\n }\n\n // Deliver to all subscriptions (fan-out)\n // Each subscription handles its own concurrency\n const deliveries = subscriptions.map(async (sub) => {\n await sub.semaphore.withPermit(async () => {\n try {\n await sub.handler(envelope as MessageEnvelope);\n } catch (error) {\n // In-memory transport doesn't handle errors - let them propagate\n // Real error handling is done by the bus runtime\n console.error(\n `[InMemoryTransport] Handler error for ${endpoint}:`,\n error\n );\n }\n });\n });\n\n await Promise.all(deliveries);\n }\n\n /**\n * Get the number of subscriptions for an endpoint.\n * Useful for testing.\n */\n getSubscriptionCount(endpoint: string): number {\n return this.subscriptions.get(endpoint)?.length ?? 0;\n }\n\n /**\n * Check if the transport is started.\n */\n get isStarted(): boolean {\n return this.started;\n }\n\n /**\n * Clear all subscriptions. Useful for testing.\n */\n clearSubscriptions(): void {\n this.subscriptions.clear();\n }\n}\n","/**\n * Simple semaphore for controlling concurrency.\n */\nexport class Semaphore {\n private permits: number;\n private waiting: Array<() => void> = [];\n\n constructor(permits: number) {\n this.permits = permits;\n }\n\n /**\n * Acquire a permit, waiting if none are available.\n */\n async acquire(): Promise<void> {\n if (this.permits > 0) {\n this.permits--;\n return;\n }\n\n return new Promise<void>((resolve) => {\n this.waiting.push(resolve);\n });\n }\n\n /**\n * Release a permit, waking a waiting acquirer if any.\n */\n release(): void {\n const next = this.waiting.shift();\n if (next) {\n next();\n } else {\n this.permits++;\n }\n }\n\n /**\n * Execute a function with a permit.\n */\n async withPermit<T>(fn: () => Promise<T>): Promise<T> {\n await this.acquire();\n try {\n return await fn();\n } finally {\n this.release();\n }\n }\n\n /**\n * Get the number of available permits.\n */\n get available(): number {\n return this.permits;\n }\n\n /**\n * Get the number of waiting acquirers.\n */\n get waitingCount(): number {\n return this.waiting.length;\n }\n}\n"],"mappings":"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","names":[]}
@@ -0,0 +1,68 @@
1
+ import { Transport, BaseMessage, TransportSubscribeOptions, MessageEnvelope, TransportPublishOptions } from '@saga-bus/core';
2
+
3
+ interface InMemoryTransportOptions {
4
+ /**
5
+ * Default concurrency for subscriptions (default: 1)
6
+ */
7
+ defaultConcurrency?: number;
8
+ }
9
+ /**
10
+ * In-memory transport implementation for testing and local development.
11
+ * Uses a simple pub/sub pattern with concurrency control.
12
+ */
13
+ declare class InMemoryTransport implements Transport {
14
+ private readonly subscriptions;
15
+ private readonly pendingTimeouts;
16
+ private readonly defaultConcurrency;
17
+ private started;
18
+ constructor(options?: InMemoryTransportOptions);
19
+ start(): Promise<void>;
20
+ stop(): Promise<void>;
21
+ subscribe<TMessage extends BaseMessage>(options: TransportSubscribeOptions, handler: (envelope: MessageEnvelope<TMessage>) => Promise<void>): Promise<void>;
22
+ publish<TMessage extends BaseMessage>(message: TMessage, options: TransportPublishOptions): Promise<void>;
23
+ private deliverToSubscribers;
24
+ /**
25
+ * Get the number of subscriptions for an endpoint.
26
+ * Useful for testing.
27
+ */
28
+ getSubscriptionCount(endpoint: string): number;
29
+ /**
30
+ * Check if the transport is started.
31
+ */
32
+ get isStarted(): boolean;
33
+ /**
34
+ * Clear all subscriptions. Useful for testing.
35
+ */
36
+ clearSubscriptions(): void;
37
+ }
38
+
39
+ /**
40
+ * Simple semaphore for controlling concurrency.
41
+ */
42
+ declare class Semaphore {
43
+ private permits;
44
+ private waiting;
45
+ constructor(permits: number);
46
+ /**
47
+ * Acquire a permit, waiting if none are available.
48
+ */
49
+ acquire(): Promise<void>;
50
+ /**
51
+ * Release a permit, waking a waiting acquirer if any.
52
+ */
53
+ release(): void;
54
+ /**
55
+ * Execute a function with a permit.
56
+ */
57
+ withPermit<T>(fn: () => Promise<T>): Promise<T>;
58
+ /**
59
+ * Get the number of available permits.
60
+ */
61
+ get available(): number;
62
+ /**
63
+ * Get the number of waiting acquirers.
64
+ */
65
+ get waitingCount(): number;
66
+ }
67
+
68
+ export { InMemoryTransport, type InMemoryTransportOptions, Semaphore };
@@ -0,0 +1,68 @@
1
+ import { Transport, BaseMessage, TransportSubscribeOptions, MessageEnvelope, TransportPublishOptions } from '@saga-bus/core';
2
+
3
+ interface InMemoryTransportOptions {
4
+ /**
5
+ * Default concurrency for subscriptions (default: 1)
6
+ */
7
+ defaultConcurrency?: number;
8
+ }
9
+ /**
10
+ * In-memory transport implementation for testing and local development.
11
+ * Uses a simple pub/sub pattern with concurrency control.
12
+ */
13
+ declare class InMemoryTransport implements Transport {
14
+ private readonly subscriptions;
15
+ private readonly pendingTimeouts;
16
+ private readonly defaultConcurrency;
17
+ private started;
18
+ constructor(options?: InMemoryTransportOptions);
19
+ start(): Promise<void>;
20
+ stop(): Promise<void>;
21
+ subscribe<TMessage extends BaseMessage>(options: TransportSubscribeOptions, handler: (envelope: MessageEnvelope<TMessage>) => Promise<void>): Promise<void>;
22
+ publish<TMessage extends BaseMessage>(message: TMessage, options: TransportPublishOptions): Promise<void>;
23
+ private deliverToSubscribers;
24
+ /**
25
+ * Get the number of subscriptions for an endpoint.
26
+ * Useful for testing.
27
+ */
28
+ getSubscriptionCount(endpoint: string): number;
29
+ /**
30
+ * Check if the transport is started.
31
+ */
32
+ get isStarted(): boolean;
33
+ /**
34
+ * Clear all subscriptions. Useful for testing.
35
+ */
36
+ clearSubscriptions(): void;
37
+ }
38
+
39
+ /**
40
+ * Simple semaphore for controlling concurrency.
41
+ */
42
+ declare class Semaphore {
43
+ private permits;
44
+ private waiting;
45
+ constructor(permits: number);
46
+ /**
47
+ * Acquire a permit, waiting if none are available.
48
+ */
49
+ acquire(): Promise<void>;
50
+ /**
51
+ * Release a permit, waking a waiting acquirer if any.
52
+ */
53
+ release(): void;
54
+ /**
55
+ * Execute a function with a permit.
56
+ */
57
+ withPermit<T>(fn: () => Promise<T>): Promise<T>;
58
+ /**
59
+ * Get the number of available permits.
60
+ */
61
+ get available(): number;
62
+ /**
63
+ * Get the number of waiting acquirers.
64
+ */
65
+ get waitingCount(): number;
66
+ }
67
+
68
+ export { InMemoryTransport, type InMemoryTransportOptions, Semaphore };
package/dist/index.js ADDED
@@ -0,0 +1,158 @@
1
+ // src/InMemoryTransport.ts
2
+ import { randomUUID } from "crypto";
3
+
4
+ // src/Semaphore.ts
5
+ var Semaphore = class {
6
+ permits;
7
+ waiting = [];
8
+ constructor(permits) {
9
+ this.permits = permits;
10
+ }
11
+ /**
12
+ * Acquire a permit, waiting if none are available.
13
+ */
14
+ async acquire() {
15
+ if (this.permits > 0) {
16
+ this.permits--;
17
+ return;
18
+ }
19
+ return new Promise((resolve) => {
20
+ this.waiting.push(resolve);
21
+ });
22
+ }
23
+ /**
24
+ * Release a permit, waking a waiting acquirer if any.
25
+ */
26
+ release() {
27
+ const next = this.waiting.shift();
28
+ if (next) {
29
+ next();
30
+ } else {
31
+ this.permits++;
32
+ }
33
+ }
34
+ /**
35
+ * Execute a function with a permit.
36
+ */
37
+ async withPermit(fn) {
38
+ await this.acquire();
39
+ try {
40
+ return await fn();
41
+ } finally {
42
+ this.release();
43
+ }
44
+ }
45
+ /**
46
+ * Get the number of available permits.
47
+ */
48
+ get available() {
49
+ return this.permits;
50
+ }
51
+ /**
52
+ * Get the number of waiting acquirers.
53
+ */
54
+ get waitingCount() {
55
+ return this.waiting.length;
56
+ }
57
+ };
58
+
59
+ // src/InMemoryTransport.ts
60
+ var InMemoryTransport = class {
61
+ subscriptions = /* @__PURE__ */ new Map();
62
+ pendingTimeouts = /* @__PURE__ */ new Set();
63
+ defaultConcurrency;
64
+ started = false;
65
+ constructor(options = {}) {
66
+ this.defaultConcurrency = options.defaultConcurrency ?? 1;
67
+ }
68
+ async start() {
69
+ this.started = true;
70
+ }
71
+ async stop() {
72
+ this.started = false;
73
+ this.pendingTimeouts.forEach((timeout) => clearTimeout(timeout));
74
+ this.pendingTimeouts.clear();
75
+ }
76
+ async subscribe(options, handler) {
77
+ const { endpoint, concurrency = this.defaultConcurrency } = options;
78
+ const subscription = {
79
+ options,
80
+ handler,
81
+ semaphore: new Semaphore(concurrency)
82
+ };
83
+ const existing = this.subscriptions.get(endpoint);
84
+ if (existing) {
85
+ existing.push(subscription);
86
+ } else {
87
+ this.subscriptions.set(endpoint, [subscription]);
88
+ }
89
+ }
90
+ async publish(message, options) {
91
+ const { endpoint, headers = {}, delayMs } = options;
92
+ const envelope = {
93
+ id: randomUUID(),
94
+ type: message.type,
95
+ payload: message,
96
+ headers: { ...headers },
97
+ timestamp: /* @__PURE__ */ new Date(),
98
+ partitionKey: options.key
99
+ };
100
+ if (delayMs && delayMs > 0) {
101
+ const timeout = setTimeout(() => {
102
+ this.pendingTimeouts.delete(timeout);
103
+ void this.deliverToSubscribers(endpoint, envelope);
104
+ }, delayMs);
105
+ this.pendingTimeouts.add(timeout);
106
+ } else {
107
+ setImmediate(() => {
108
+ void this.deliverToSubscribers(endpoint, envelope);
109
+ });
110
+ }
111
+ }
112
+ async deliverToSubscribers(endpoint, envelope) {
113
+ if (!this.started) {
114
+ return;
115
+ }
116
+ const subscriptions = this.subscriptions.get(endpoint);
117
+ if (!subscriptions || subscriptions.length === 0) {
118
+ return;
119
+ }
120
+ const deliveries = subscriptions.map(async (sub) => {
121
+ await sub.semaphore.withPermit(async () => {
122
+ try {
123
+ await sub.handler(envelope);
124
+ } catch (error) {
125
+ console.error(
126
+ `[InMemoryTransport] Handler error for ${endpoint}:`,
127
+ error
128
+ );
129
+ }
130
+ });
131
+ });
132
+ await Promise.all(deliveries);
133
+ }
134
+ /**
135
+ * Get the number of subscriptions for an endpoint.
136
+ * Useful for testing.
137
+ */
138
+ getSubscriptionCount(endpoint) {
139
+ return this.subscriptions.get(endpoint)?.length ?? 0;
140
+ }
141
+ /**
142
+ * Check if the transport is started.
143
+ */
144
+ get isStarted() {
145
+ return this.started;
146
+ }
147
+ /**
148
+ * Clear all subscriptions. Useful for testing.
149
+ */
150
+ clearSubscriptions() {
151
+ this.subscriptions.clear();
152
+ }
153
+ };
154
+ export {
155
+ InMemoryTransport,
156
+ Semaphore
157
+ };
158
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/InMemoryTransport.ts","../src/Semaphore.ts"],"sourcesContent":["import { randomUUID } from \"node:crypto\";\nimport type {\n Transport,\n TransportSubscribeOptions,\n TransportPublishOptions,\n BaseMessage,\n MessageEnvelope,\n} from \"@saga-bus/core\";\nimport { Semaphore } from \"./Semaphore.js\";\n\ninterface Subscription<T extends BaseMessage = BaseMessage> {\n options: TransportSubscribeOptions;\n handler: (envelope: MessageEnvelope<T>) => Promise<void>;\n semaphore: Semaphore;\n}\n\nexport interface InMemoryTransportOptions {\n /**\n * Default concurrency for subscriptions (default: 1)\n */\n defaultConcurrency?: number;\n}\n\n/**\n * In-memory transport implementation for testing and local development.\n * Uses a simple pub/sub pattern with concurrency control.\n */\nexport class InMemoryTransport implements Transport {\n private readonly subscriptions = new Map<string, Subscription[]>();\n private readonly pendingTimeouts = new Set<ReturnType<typeof setTimeout>>();\n private readonly defaultConcurrency: number;\n private started = false;\n\n constructor(options: InMemoryTransportOptions = {}) {\n this.defaultConcurrency = options.defaultConcurrency ?? 1;\n }\n\n async start(): Promise<void> {\n this.started = true;\n }\n\n async stop(): Promise<void> {\n this.started = false;\n\n // Clear all pending delayed messages\n this.pendingTimeouts.forEach((timeout) => clearTimeout(timeout));\n this.pendingTimeouts.clear();\n }\n\n async subscribe<TMessage extends BaseMessage>(\n options: TransportSubscribeOptions,\n handler: (envelope: MessageEnvelope<TMessage>) => Promise<void>\n ): Promise<void> {\n const { endpoint, concurrency = this.defaultConcurrency } = options;\n\n const subscription: Subscription<TMessage> = {\n options,\n handler,\n semaphore: new Semaphore(concurrency),\n };\n\n const existing = this.subscriptions.get(endpoint);\n if (existing) {\n existing.push(subscription as Subscription);\n } else {\n this.subscriptions.set(endpoint, [subscription as Subscription]);\n }\n }\n\n async publish<TMessage extends BaseMessage>(\n message: TMessage,\n options: TransportPublishOptions\n ): Promise<void> {\n const { endpoint, headers = {}, delayMs } = options;\n\n const envelope: MessageEnvelope<TMessage> = {\n id: randomUUID(),\n type: message.type,\n payload: message,\n headers: { ...headers },\n timestamp: new Date(),\n partitionKey: options.key,\n };\n\n if (delayMs && delayMs > 0) {\n const timeout = setTimeout(() => {\n this.pendingTimeouts.delete(timeout);\n void this.deliverToSubscribers(endpoint, envelope);\n }, delayMs);\n this.pendingTimeouts.add(timeout);\n } else {\n // Deliver asynchronously to avoid blocking publisher\n setImmediate(() => {\n void this.deliverToSubscribers(endpoint, envelope);\n });\n }\n }\n\n private async deliverToSubscribers<TMessage extends BaseMessage>(\n endpoint: string,\n envelope: MessageEnvelope<TMessage>\n ): Promise<void> {\n if (!this.started) {\n return;\n }\n\n const subscriptions = this.subscriptions.get(endpoint);\n if (!subscriptions || subscriptions.length === 0) {\n return;\n }\n\n // Deliver to all subscriptions (fan-out)\n // Each subscription handles its own concurrency\n const deliveries = subscriptions.map(async (sub) => {\n await sub.semaphore.withPermit(async () => {\n try {\n await sub.handler(envelope as MessageEnvelope);\n } catch (error) {\n // In-memory transport doesn't handle errors - let them propagate\n // Real error handling is done by the bus runtime\n console.error(\n `[InMemoryTransport] Handler error for ${endpoint}:`,\n error\n );\n }\n });\n });\n\n await Promise.all(deliveries);\n }\n\n /**\n * Get the number of subscriptions for an endpoint.\n * Useful for testing.\n */\n getSubscriptionCount(endpoint: string): number {\n return this.subscriptions.get(endpoint)?.length ?? 0;\n }\n\n /**\n * Check if the transport is started.\n */\n get isStarted(): boolean {\n return this.started;\n }\n\n /**\n * Clear all subscriptions. Useful for testing.\n */\n clearSubscriptions(): void {\n this.subscriptions.clear();\n }\n}\n","/**\n * Simple semaphore for controlling concurrency.\n */\nexport class Semaphore {\n private permits: number;\n private waiting: Array<() => void> = [];\n\n constructor(permits: number) {\n this.permits = permits;\n }\n\n /**\n * Acquire a permit, waiting if none are available.\n */\n async acquire(): Promise<void> {\n if (this.permits > 0) {\n this.permits--;\n return;\n }\n\n return new Promise<void>((resolve) => {\n this.waiting.push(resolve);\n });\n }\n\n /**\n * Release a permit, waking a waiting acquirer if any.\n */\n release(): void {\n const next = this.waiting.shift();\n if (next) {\n next();\n } else {\n this.permits++;\n }\n }\n\n /**\n * Execute a function with a permit.\n */\n async withPermit<T>(fn: () => Promise<T>): Promise<T> {\n await this.acquire();\n try {\n return await fn();\n } finally {\n this.release();\n }\n }\n\n /**\n * Get the number of available permits.\n */\n get available(): number {\n return this.permits;\n }\n\n /**\n * Get the number of waiting acquirers.\n */\n get waitingCount(): number {\n return this.waiting.length;\n }\n}\n"],"mappings":"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","names":[]}
package/package.json ADDED
@@ -0,0 +1,61 @@
1
+ {
2
+ "name": "@saga-bus/transport-inmemory",
3
+ "version": "0.1.2",
4
+ "description": "In-memory transport for saga-bus testing and development",
5
+ "type": "module",
6
+ "main": "./dist/index.js",
7
+ "module": "./dist/index.js",
8
+ "types": "./dist/index.d.ts",
9
+ "exports": {
10
+ ".": {
11
+ "types": "./dist/index.d.ts",
12
+ "import": "./dist/index.js",
13
+ "require": "./dist/index.cjs"
14
+ }
15
+ },
16
+ "files": [
17
+ "dist",
18
+ "README.md"
19
+ ],
20
+ "publishConfig": {
21
+ "access": "public"
22
+ },
23
+ "repository": {
24
+ "type": "git",
25
+ "url": "https://github.com/d-e-a-n-f/saga-bus.git",
26
+ "directory": "packages/transport-inmemory"
27
+ },
28
+ "bugs": {
29
+ "url": "https://github.com/d-e-a-n-f/saga-bus/issues"
30
+ },
31
+ "homepage": "https://github.com/d-e-a-n-f/saga-bus#readme",
32
+ "keywords": [
33
+ "saga",
34
+ "message-bus",
35
+ "transport",
36
+ "inmemory",
37
+ "testing"
38
+ ],
39
+ "scripts": {
40
+ "build": "tsup",
41
+ "dev": "tsup --watch",
42
+ "lint": "eslint src/",
43
+ "check-types": "tsc --noEmit",
44
+ "test": "vitest run",
45
+ "test:watch": "vitest"
46
+ },
47
+ "dependencies": {
48
+ "@saga-bus/core": "workspace:*"
49
+ },
50
+ "devDependencies": {
51
+ "@repo/eslint-config": "workspace:*",
52
+ "@repo/typescript-config": "workspace:*",
53
+ "@types/node": "^20.0.0",
54
+ "tsup": "^8.0.0",
55
+ "typescript": "^5.9.2",
56
+ "vitest": "^3.0.0"
57
+ },
58
+ "peerDependencies": {
59
+ "@saga-bus/core": ">=0.1.2"
60
+ }
61
+ }