Skip to content

Commit ca401d1

Browse files
committed
changeset
1 parent 08812a6 commit ca401d1

File tree

1 file changed

+300
-1
lines changed

1 file changed

+300
-1
lines changed

.changeset/hip-suits-dress.md

Lines changed: 300 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,303 @@
22
'@graphql-hive/pubsub': major
33
---
44

5-
TODO: explain breaking changes, change of interface, HivePubSub -> PubSub, PubSub -> MemPubSub and introduce RedisPubSub
5+
Complete API redesign with async support and distributed Redis PubSub
6+
7+
## Redesigned interface
8+
9+
```ts
10+
import type { DisposableSymbols } from '@whatwg-node/disposablestack';
11+
import type { MaybePromise } from '@whatwg-node/promise-helpers';
12+
13+
export type TopicDataMap = { [topic: string]: any /* data */ };
14+
15+
export type PubSubListener<
16+
Data extends TopicDataMap,
17+
Topic extends keyof Data,
18+
> = (data: Data[Topic]) => void;
19+
20+
export interface PubSub<M extends TopicDataMap = TopicDataMap> {
21+
/**
22+
* Publish {@link data} for a {@link topic}.
23+
* @returns `void` or a `Promise` that resolves when the data has been successfully published
24+
*/
25+
publish<Topic extends keyof M>(
26+
topic: Topic,
27+
data: M[Topic],
28+
): MaybePromise<void>;
29+
/**
30+
* A distinct list of all topics that are currently subscribed to.
31+
* Can be a promise to accomodate distributed systems where subscribers exist on other
32+
* locations and we need to know about all of them.
33+
*/
34+
subscribedTopics(): MaybePromise<Iterable<keyof M>>;
35+
/**
36+
* Subscribe and listen to a {@link topic} receiving its data.
37+
*
38+
* If the {@link listener} is provided, it will be called whenever data is emitted for the {@link topic},
39+
*
40+
* @returns an unsubscribe function or a `Promise<unsubscribe function>` that resolves when the subscription is successfully established. the unsubscribe function returns `void` or a `Promise` that resolves on successful unsubscribe and subscription cleanup
41+
*
42+
* If the {@link listener} is not provided,
43+
*
44+
* @returns an `AsyncIterable` that yields data for the given {@link topic}
45+
*/
46+
subscribe<Topic extends keyof M>(topic: Topic): AsyncIterable<M[Topic]>;
47+
subscribe<Topic extends keyof M>(
48+
topic: Topic,
49+
listener: PubSubListener<M, Topic>,
50+
): MaybePromise<() => MaybePromise<void>>;
51+
/**
52+
* Closes active subscriptions and disposes of all resources. Publishing and subscribing after disposal
53+
* is not possible and will throw an error if attempted.
54+
*/
55+
dispose(): MaybePromise<void>;
56+
/** @see {@link dispose} */
57+
[DisposableSymbols.asyncDispose](): Promise<void>;
58+
}
59+
```
60+
61+
## New `RedisPubSub` for a Redis-powered pubsub
62+
63+
```sh
64+
npm i ioredis
65+
```
66+
67+
```ts
68+
import { RedisPubSub } from '@graphql-hive/pubsub/redis';
69+
import Redis from 'ioredis';
70+
71+
/**
72+
* When a Redis connection enters "subscriber mode" (after calling SUBSCRIBE), it can only execute
73+
* subscriber commands (SUBSCRIBE, UNSUBSCRIBE, etc.). Meaning, it cannot execute other commands like PUBLISH.
74+
* To avoid this, we use two separate Redis clients: one for publishing and one for subscribing.
75+
*/
76+
const pub = new Redis();
77+
const sub = new Redis();
78+
79+
const pubsub = new RedisPubSub(
80+
{ pub, sub },
81+
// if the chanel prefix is the shared between services, the topics will be shared as well
82+
// this means that if you have multiple services using the same channel prefix, they will
83+
// receive each other's messages
84+
{ channelPrefix: 'my-app' }
85+
);
86+
```
87+
88+
## Migrating
89+
90+
The main migration effort involves:
91+
1. Updating import statements
92+
2. Adding `await` to async operations
93+
3. Replacing subscription ID pattern with unsubscribe functions
94+
4. Replacing `asyncIterator()` with overloaded `subscribe()`
95+
5. Choosing between `MemPubSub` and `RedisPubSub` implementations
96+
6. Using the `PubSub` interface instead of `HivePubSub`
97+
98+
Before:
99+
100+
```typescript
101+
import { PubSub, HivePubSub } from '@graphql-hive/pubsub';
102+
103+
interface TopicMap {
104+
userCreated: { id: string; name: string };
105+
orderPlaced: { orderId: string; amount: number };
106+
}
107+
108+
const pubsub: HivePubSub<TopicMap> = new PubSub();
109+
110+
// Subscribe
111+
const subId = pubsub.subscribe('userCreated', (user) => {
112+
console.log('User created:', user.name);
113+
});
114+
115+
// Publish
116+
pubsub.publish('userCreated', { id: '1', name: 'John' });
117+
118+
// Async iteration
119+
(async () => {
120+
for await (const order of pubsub.asyncIterator('orderPlaced')) {
121+
console.log('Order placed:', order.orderId);
122+
}
123+
})();
124+
125+
// Get topics
126+
const topics = pubsub.getEventNames();
127+
128+
// Unsubcribe
129+
pubsub.unsubscribe(subId);
130+
131+
// Dispose/destroy the pubsub
132+
pubsub.dispose();
133+
```
134+
135+
After:
136+
137+
```typescript
138+
import { MemPubSub, PubSub } from '@graphql-hive/pubsub';
139+
140+
interface TopicMap {
141+
userCreated: { id: string; name: string };
142+
orderPlaced: { orderId: string; amount: number };
143+
}
144+
145+
const pubsub: PubSub<TopicMap> = new MemPubSub();
146+
147+
// Subscribe
148+
const unsubscribe = await pubsub.subscribe('userCreated', (user) => {
149+
console.log('User created:', user.name);
150+
});
151+
152+
// Publish
153+
await pubsub.publish('userCreated', { id: '1', name: 'John' });
154+
155+
// Async iteration
156+
(async () => {
157+
for await (const order of pubsub.subscribe('orderPlaced')) {
158+
console.log('Order placed:', order.orderId);
159+
}
160+
})();
161+
162+
// Get topics
163+
const topics = await pubsub.subscribedTopics();
164+
165+
// Unsubscribe
166+
await unsubscribe();
167+
168+
// Dispose/destroy the pubsub
169+
await pubsub.dispose();
170+
```
171+
172+
### Interface renamed from `HivePubSub` to just `PubSub`
173+
174+
```diff
175+
- import { HivePubSub } from '@graphql-hive/pubsub';
176+
+ import { PubSub } from '@graphql-hive/pubsub';
177+
```
178+
179+
### `subscribedTopics()` method signature change
180+
181+
This method is now required and supports async operations.
182+
183+
```diff
184+
- subscribedTopics?(): Iterable; // Optional
185+
+ subscribedTopics(): MaybePromise<Iterable>; // Required, supports async
186+
```
187+
188+
### `publish()` method signature change
189+
190+
Publishing can now be async and may return a promise.
191+
192+
```diff
193+
- publish<Topic extends keyof Data>(topic: Topic, data: Data[Topic]): void;
194+
+ publish<Topic extends keyof M>(topic: Topic, data: M[Topic]): MaybePromise<void>;
195+
```
196+
197+
Migrating existing code:
198+
199+
```diff
200+
- pubsub.publish('topic', data);
201+
+ await pubsub.publish('topic', data);
202+
```
203+
204+
### `subscribe()` method signature change
205+
206+
Subscribe now returns an unsubscribe function instead of a subscription ID.
207+
208+
```diff
209+
subscribe<Topic extends keyof Data>(
210+
topic: Topic,
211+
listener: PubSubListener<Data, Topic>
212+
- ): number; // Returns subscription ID
213+
+ ): MaybePromise<() => MaybePromise<void>>; // Returns unsubscribe function
214+
```
215+
216+
Migrating existing code:
217+
218+
```diff
219+
- const subId = pubsub.subscribe('topic', (data) => {
220+
- console.log(data);
221+
- });
222+
- pubsub.unsubscribe(subId);
223+
+ const unsubscribe = await pubsub.subscribe('topic', (data) => {
224+
+ console.log(data);
225+
+ });
226+
+ await unsubscribe();
227+
```
228+
229+
### `dispose()` method signature change
230+
231+
Disposal is now required and supports async operations.
232+
233+
```diff
234+
- dispose?(): void; // Optional
235+
+ dispose(): MaybePromise<void>; // Required, supports async
236+
```
237+
238+
Migrating existing code:
239+
240+
```diff
241+
- pubsub.dispose();
242+
+ await pubsub.dispose();
243+
```
244+
245+
### Removed `getEventNames()` method
246+
247+
This deprecated method was removed. Use `subscribedTopics()` instead.
248+
249+
```diff
250+
- getEventNames(): Iterable<keyof Data>;
251+
```
252+
253+
Migrating existing code:
254+
255+
```diff
256+
- const topics = pubsub.getEventNames();
257+
+ const topics = await pubsub.subscribedTopics();
258+
```
259+
260+
### Removed `unsubscribe()` method
261+
262+
The centralized unsubscribe method was removed. Each subscription now returns its own unsubscribe function.
263+
264+
```diff
265+
- unsubscribe(subId: number): void;
266+
```
267+
268+
Migrating existing code by using the unsubscribe function returned by `subscribe()` instead:
269+
270+
```diff
271+
- const subId = pubsub.subscribe('topic', listener);
272+
- pubsub.unsubscribe(subId);
273+
274+
+ const unsubscribe = await pubsub.subscribe('topic', listener);
275+
+ await unsubscribe();
276+
```
277+
278+
### Removed `asyncIterator()` Method
279+
280+
The separate async iterator method was removed. Call `subscribe()` without a listener to get an async iterable.
281+
282+
```diff
283+
- asyncIterator<Topic extends keyof Data>(topic: Topic): AsyncIterable<Data[Topic]>;
284+
```
285+
286+
Migrating existing code:
287+
288+
```diff
289+
- for await (const data of pubsub.asyncIterator('topic')) {
290+
+ for await (const data of pubsub.subscribe('topic')) {
291+
console.log(data);
292+
}
293+
```
294+
295+
### `MemPubSub` is the in-memory pubsub implementation
296+
297+
The generic `PubSub` class was replaced with implementation specific `MemPubSub` for an in-memory pubsub.
298+
299+
```diff
300+
- import { PubSub } from '@graphql-hive/pubsub';
301+
- const pubsub = new PubSub();
302+
+ import { MemPubSub } from '@graphql-hive/pubsub';
303+
+ const pubsub = new MemPubSub();
304+
```

0 commit comments

Comments
 (0)