@pawells/rxjs-events 1.0.1 → 1.1.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.
@@ -0,0 +1,226 @@
1
+ import { EventHandler } from './handler.js';
2
+ /**
3
+ * Adapter that makes EventHandler compatible with NestJS GraphQL @Subscription() decorators.
4
+ * Implements the IPubSubEngine interface for pub/sub messaging patterns.
5
+ *
6
+ * This adapter bridges EventHandler with GraphQL subscriptions by maintaining a map of
7
+ * EventHandlers (one per trigger name) and coordinating subscription/publication.
8
+ *
9
+ * @remarks
10
+ * - No external dependencies required (graphql-subscriptions not needed)
11
+ * - Each trigger name gets its own EventHandler for isolation
12
+ * - Thread-safe subscription ID management
13
+ *
14
+ * @example
15
+ * ```typescript
16
+ * // In your NestJS GraphQL module:
17
+ * import { EventHandlerPubSub } from '@pawells/rxjs-events';
18
+ *
19
+ * const pubSub = new EventHandlerPubSub();
20
+ *
21
+ * // In a GraphQL resolver:
22
+ * @Subscription(() => MessagePayload, {
23
+ * resolve: (payload) => payload,
24
+ * })
25
+ * messageSent(): AsyncIterableIterator<MessagePayload> {
26
+ * return pubSub.asyncIterator<MessagePayload>('MESSAGE_SENT');
27
+ * }
28
+ *
29
+ * // In a mutation:
30
+ * @Mutation(() => MessagePayload)
31
+ * sendMessage(@Args('text') text: string): MessagePayload {
32
+ * const payload = { text, timestamp: Date.now() };
33
+ * pubSub.publish('MESSAGE_SENT', payload);
34
+ * return payload;
35
+ * }
36
+ * ```
37
+ */
38
+ export class EventHandlerPubSub {
39
+ /**
40
+ * Map storing EventHandlers indexed by trigger name.
41
+ * Each trigger maintains its own handler for event isolation.
42
+ */
43
+ _handlers = new Map();
44
+ /**
45
+ * Map storing handler subscriptions and their IDs for cleanup.
46
+ * Maps subscription ID to [handler, subscription ID within handler].
47
+ */
48
+ _subscriptions = new Map();
49
+ /**
50
+ * Counter for generating unique subscription IDs across all triggers.
51
+ */
52
+ _nextSubscriptionId = 0;
53
+ /**
54
+ * Gets or creates an EventHandler for a specific trigger name.
55
+ *
56
+ * @param triggerName - The trigger channel name
57
+ * @returns The EventHandler for this trigger
58
+ */
59
+ _getOrCreateHandler(triggerName) {
60
+ let handler = this._handlers.get(triggerName);
61
+ if (!handler) {
62
+ handler = new EventHandler(triggerName);
63
+ this._handlers.set(triggerName, handler);
64
+ }
65
+ return handler;
66
+ }
67
+ /**
68
+ * Publishes a payload to all subscribers of a trigger.
69
+ *
70
+ * @param triggerName - The trigger channel name
71
+ * @param payload - The payload to publish
72
+ */
73
+ async publish(triggerName, payload) {
74
+ await Promise.resolve();
75
+ const handler = this._getOrCreateHandler(triggerName);
76
+ handler.Trigger(payload);
77
+ }
78
+ /**
79
+ * Subscribes to a trigger channel and calls the callback when messages are published.
80
+ *
81
+ * @param triggerName - The trigger channel name
82
+ * @param onMessage - Callback function that receives the published payload
83
+ * @param _options - Optional subscription options (not used in current implementation)
84
+ * @returns Unique subscription ID for later unsubscription
85
+ */
86
+ async subscribe(triggerName, onMessage, _options) {
87
+ const handler = this._getOrCreateHandler(triggerName);
88
+ const handlerSubId = handler.Subscribe(onMessage);
89
+ // Generate a unique subscription ID for external tracking
90
+ const externalSubId = this._nextSubscriptionId++;
91
+ this._subscriptions.set(externalSubId, [handler, handlerSubId]);
92
+ await Promise.resolve();
93
+ return externalSubId;
94
+ }
95
+ /**
96
+ * Unsubscribes from a trigger channel.
97
+ *
98
+ * @param subId - The subscription ID returned from subscribe()
99
+ */
100
+ unsubscribe(subId) {
101
+ const subscription = this._subscriptions.get(subId);
102
+ if (subscription) {
103
+ const [handler, handlerSubId] = subscription;
104
+ handler.Unsubscribe(handlerSubId);
105
+ this._subscriptions.delete(subId);
106
+ }
107
+ }
108
+ /**
109
+ * Creates an async iterator for one or more trigger channels.
110
+ * Useful for GraphQL subscription resolvers.
111
+ *
112
+ * @template T - The type of values yielded by the iterator
113
+ * @param triggers - Single trigger name or array of trigger names
114
+ * @returns AsyncIterableIterator that yields published payloads
115
+ *
116
+ * @example
117
+ * ```typescript
118
+ * // Subscribe to a single trigger
119
+ * const iterator = pubSub.asyncIterator('USER_CREATED');
120
+ *
121
+ * // Subscribe to multiple triggers
122
+ * const iterator = pubSub.asyncIterator(['USER_CREATED', 'USER_UPDATED']);
123
+ * ```
124
+ */
125
+ asyncIterator(triggers) {
126
+ const triggerArray = Array.isArray(triggers) ? triggers : [triggers];
127
+ if (triggerArray.length === 0) {
128
+ // Return an empty iterator if no triggers provided
129
+ return (async function* () {
130
+ // Empty iterator
131
+ })();
132
+ }
133
+ if (triggerArray.length === 1) {
134
+ // Single trigger - use the handler's async iterator directly
135
+ const handler = this._getOrCreateHandler(triggerArray[0]);
136
+ return handler.GetAsyncIterableIterator();
137
+ }
138
+ // Multiple triggers - merge iterators from all handlers
139
+ return this._mergeAsyncIterators(triggerArray);
140
+ }
141
+ /**
142
+ * Merges async iterators from multiple trigger handlers.
143
+ * Events from any trigger are yielded as they arrive.
144
+ *
145
+ * @template T - The type of values yielded
146
+ * @param triggers - Array of trigger names
147
+ * @returns Merged async iterator that yields events from all triggers concurrently
148
+ */
149
+ _mergeAsyncIterators(triggers) {
150
+ const handlers = triggers.map((trigger) => this._getOrCreateHandler(trigger));
151
+ return (async function* () {
152
+ const iterators = handlers.map((h) => h.GetAsyncIterableIterator());
153
+ let activeIterators = iterators.length;
154
+ try {
155
+ // Create promises for each iterator's next() call
156
+ const createNextPromises = () => {
157
+ return iterators.map((iter) => iter.next()
158
+ .then((result) => {
159
+ if (result.done) {
160
+ activeIterators--;
161
+ }
162
+ return result;
163
+ })
164
+ .catch((err) => {
165
+ activeIterators--;
166
+ throw err;
167
+ }));
168
+ };
169
+ let promises = createNextPromises();
170
+ while (activeIterators > 0) {
171
+ const result = await Promise.race(promises);
172
+ if (!result.done) {
173
+ yield result.value;
174
+ promises = createNextPromises();
175
+ }
176
+ }
177
+ }
178
+ finally {
179
+ // Clean up all iterators
180
+ for (const iterator of iterators) {
181
+ if (typeof iterator.return === 'function') {
182
+ await iterator.return?.();
183
+ }
184
+ }
185
+ }
186
+ })();
187
+ }
188
+ }
189
+ /**
190
+ * Wraps an async iterator with a filter predicate for use with GraphQL subscriptions.
191
+ * Mirrors the withFilter helper from graphql-subscriptions.
192
+ *
193
+ * @template T - The type of values in the iterator
194
+ * @param asyncIteratorFn - Function that returns an async iterator
195
+ * @param filterFn - Predicate function that determines which values to yield (async functions supported)
196
+ * @returns Filtered async iterator
197
+ *
198
+ * @example
199
+ * ```typescript
200
+ * const pubSub = new EventHandlerPubSub();
201
+ *
202
+ * // In a GraphQL resolver:
203
+ * @Subscription(() => UserPayload)
204
+ * userUpdated(
205
+ * @Args('userId') userId: string
206
+ * ): AsyncIterableIterator<UserPayload> {
207
+ * return WithFilter(
208
+ * () => pubSub.asyncIterator<UserPayload>('USER_UPDATED'),
209
+ * (payload: UserPayload) => payload.userId === userId
210
+ * );
211
+ * }
212
+ * ```
213
+ */
214
+ export function WithFilter(asyncIteratorFn, filterFn) {
215
+ return (async function* () {
216
+ const asyncIterator = asyncIteratorFn();
217
+ for await (const value of asyncIterator) {
218
+ const result = filterFn(value);
219
+ const passes = result instanceof Promise ? await result : result;
220
+ if (passes) {
221
+ yield value;
222
+ }
223
+ }
224
+ })();
225
+ }
226
+ //# sourceMappingURL=nestjs-pubsub.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"nestjs-pubsub.js","sourceRoot":"","sources":["../src/nestjs-pubsub.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@pawells/rxjs-events",
3
3
  "displayName": "RxJS Events",
4
- "version": "1.0.1",
4
+ "version": "1.1.0",
5
5
  "description": "RxJS-based event handling library with reactive observables and async event streams.",
6
6
  "type": "module",
7
7
  "main": "./build/index.js",
@@ -23,25 +23,27 @@
23
23
  "test": "vitest run",
24
24
  "test:ui": "vitest --ui",
25
25
  "test:coverage": "vitest --coverage",
26
+ "pipeline": "yarn typecheck && yarn lint && yarn test && yarn build",
26
27
  "prepublishOnly": "npm run build",
27
28
  "prepare": "husky"
28
29
  },
29
30
  "dependencies": {
31
+ "@pawells/typescript-common": "^1.4.1",
30
32
  "rxjs": "^7.8.2"
31
33
  },
32
34
  "devDependencies": {
33
35
  "@eslint/js": "^10.0.1",
34
- "@stylistic/eslint-plugin": "^5.9.0",
35
- "@types/node": "^25.3.0",
36
- "@typescript-eslint/eslint-plugin": "^8.0.0",
37
- "@typescript-eslint/parser": "^8.0.0",
36
+ "@stylistic/eslint-plugin": "^5.10.0",
37
+ "@types/node": "^25.3.3",
38
+ "@typescript-eslint/eslint-plugin": "^8.56.1",
39
+ "@typescript-eslint/parser": "^8.56.1",
38
40
  "@vitest/coverage-v8": "^4.0.18",
39
41
  "@vitest/ui": "^4.0.18",
40
- "eslint": "^10.0.1",
42
+ "eslint": "^10.0.3",
41
43
  "eslint-import-resolver-typescript": "^4.4.4",
42
44
  "eslint-plugin-import": "^2.31.0",
43
45
  "eslint-plugin-unused-imports": "^4.0.0",
44
- "globals": "^17.3.0",
46
+ "globals": "^17.4.0",
45
47
  "husky": "^9.1.7",
46
48
  "typescript": "^5.3.3",
47
49
  "vitest": "^4.0.18"
@@ -66,7 +68,7 @@
66
68
  },
67
69
  "homepage": "https://github.com/PhillipAWells/rxjs-events#readme",
68
70
  "engines": {
69
- "node": ">=24.0.0"
71
+ "node": ">=22.0.0"
70
72
  },
71
73
  "packageManager": "yarn@4.12.0",
72
74
  "files": [