Skip to content

Commit 5869475

Browse files
committed
Document @solana/subscribable with TypeDoc
1 parent 0d6aed0 commit 5869475

File tree

5 files changed

+160
-2
lines changed

5 files changed

+160
-2
lines changed

Diff for: packages/subscribable/src/async-iterable.ts

+58
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,29 @@ import { AbortController } from '@solana/event-target-impl';
88
import { DataPublisher } from './data-publisher';
99

1010
type Config = Readonly<{
11+
/**
12+
* Triggering this abort signal will cause all iterators spawned from this iterator to return
13+
* once they have published all queued messages.
14+
*/
1115
abortSignal: AbortSignal;
16+
/**
17+
* Messages from this channel of `dataPublisher` will be the ones yielded through the iterators.
18+
*
19+
* Messages only begin to be queued after the first time an iterator begins to poll. Channel
20+
* messages published before that time will be dropped.
21+
*/
1222
dataChannelName: string;
1323
// FIXME: It would be nice to be able to constrain the type of `dataPublisher` to one that
1424
// definitely supports the `dataChannelName` and `errorChannelName` channels, and
1525
// furthermore publishes `TData` on the `dataChannelName` channel. This is more difficult
1626
// than it should be: https://tsplay.dev/NlZelW
1727
dataPublisher: DataPublisher;
28+
/**
29+
* Messages from this channel of `dataPublisher` will be the ones thrown through the iterators.
30+
*
31+
* Any new iterators created after the first error is encountered will reject with that error
32+
* when polled.
33+
*/
1834
errorChannelName: string;
1935
}>;
2036

@@ -58,6 +74,48 @@ function createExplicitAbortToken() {
5874

5975
const UNINITIALIZED = Symbol();
6076

77+
/**
78+
* Returns an `AsyncIterable` given a data publisher.
79+
*
80+
* The iterable will produce iterators that vend messages published to `dataChannelName` and will
81+
* throw the first time a message is published to `errorChannelName`. Triggering the abort signal
82+
* will cause all iterators spawned from this iterator to return once they have published all queued
83+
* messages.
84+
*
85+
* Things to note:
86+
*
87+
* - If a message is published over a channel before the `AsyncIterator` attached to it has polled
88+
* for the next result, the message will be queued in memory.
89+
* - Messages only begin to be queued after the first time an iterator begins to poll. Channel
90+
* messages published before that time will be dropped.
91+
* - If there are messages in the queue and an error occurs, all queued messages will be vended to
92+
* the iterator before the error is thrown.
93+
* - If there are messages in the queue and the abort signal fires, all queued messages will be
94+
* vended to the iterator after which it will return.
95+
* - Any new iterators created after the first error is encountered will reject with that error when
96+
* polled.
97+
*
98+
* @param config
99+
*
100+
* @example
101+
* ```ts
102+
* const iterable = createAsyncIterableFromDataPublisher({
103+
* abortSignal: AbortSignal.timeout(10_000),
104+
* dataChannelName: 'message',
105+
* dataPublisher,
106+
* errorChannelName: 'error',
107+
* });
108+
* try {
109+
* for await (const message of iterable) {
110+
* console.log('Got message', message);
111+
* }
112+
* } catch (e) {
113+
* console.error('An error was published to the error channel', e);
114+
* } finally {
115+
* console.log("It's been 10 seconds; that's enough for now.");
116+
* }
117+
* ```
118+
*/
61119
export function createAsyncIterableFromDataPublisher<TData>({
62120
abortSignal,
63121
dataChannelName,

Diff for: packages/subscribable/src/data-publisher.ts

+39
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,53 @@ import { TypedEventEmitter, TypedEventTarget } from './event-emitter';
22

33
type UnsubscribeFn = () => void;
44

5+
/**
6+
* Represents an object with an `on` function that you can call to subscribe to certain data over a
7+
* named channel.
8+
*
9+
* @example
10+
* ```ts
11+
* let dataPublisher: DataPublisher<{ error: SolanaError }>;
12+
* dataPublisher.on('data', handleData); // ERROR. `data` is not a known channel name.
13+
* dataPublisher.on('error', e => {
14+
* console.error(e);
15+
* }); // OK.
16+
* ```
17+
*/
518
export interface DataPublisher<TDataByChannelName extends Record<string, unknown> = Record<string, unknown>> {
19+
/**
20+
* Call this to subscribe to data over a named channel.
21+
*
22+
* @param channelName The name of the channel on which to subscribe for messages
23+
* @param subscriber The function to call when a message becomes available
24+
* @param options.signal An abort signal you can fire to unsubscribe
25+
*
26+
* @returns A function that you can call to unsubscribe
27+
*/
628
on<const TChannelName extends keyof TDataByChannelName>(
729
channelName: TChannelName,
830
subscriber: (data: TDataByChannelName[TChannelName]) => void,
931
options?: { signal: AbortSignal },
1032
): UnsubscribeFn;
1133
}
1234

35+
/**
36+
* Returns an object with an `on` function that you can call to subscribe to certain data over a
37+
* named channel.
38+
*
39+
* The `on` function returns an unsubscribe function.
40+
*
41+
* @example
42+
* ```ts
43+
* const socketDataPublisher = getDataPublisherFromEventEmitter(new WebSocket('wss://api.devnet.solana.com'));
44+
* const unsubscribe = socketDataPublisher.on('message', message => {
45+
* if (JSON.parse(message.data).id === 42) {
46+
* console.log('Got response 42');
47+
* unsubscribe();
48+
* }
49+
* });
50+
* ```
51+
*/
1352
export function getDataPublisherFromEventEmitter<TEventMap extends Record<string, Event>>(
1453
eventEmitter: TypedEventEmitter<TEventMap> | TypedEventTarget<TEventMap>,
1554
): DataPublisher<{

Diff for: packages/subscribable/src/demultiplex.ts

+31
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,37 @@ import { EventTarget } from '@solana/event-target-impl';
22

33
import { DataPublisher, getDataPublisherFromEventEmitter } from './data-publisher';
44

5+
/**
6+
* Given a channel that carries messages for multiple subscribers on a single channel name, this
7+
* function returns a new {@link DataPublisher} that splits them into multiple channel names.
8+
*
9+
* @param messageTransformer A function that receives the message as the first argument, and returns
10+
* a tuple of the derived channel name and the message.
11+
*
12+
* @example
13+
* Imagine a channel that carries multiple notifications whose destination is contained within the
14+
* message itself.
15+
*
16+
* ```ts
17+
* const demuxedDataPublisher = demultiplexDataPublisher(channel, 'message', message => {
18+
* const destinationChannelName = `notification-for:${message.subscriberId}`;
19+
* return [destinationChannelName, message];
20+
* });
21+
* ```
22+
*
23+
* Now you can subscribe to _only_ the messages you are interested in, without having to subscribe
24+
* to the entire `'message'` channel and filter out the messages that are not for you.
25+
*
26+
* ```ts
27+
* demuxedDataPublisher.on(
28+
* 'notification-for:123',
29+
* message => {
30+
* console.log('Got a message for subscriber 123', message);
31+
* },
32+
* { signal: AbortSignal.timeout(5_000) },
33+
* );
34+
* ```
35+
*/
536
export function demultiplexDataPublisher<
637
TDataPublisher extends DataPublisher,
738
const TChannelName extends Parameters<TDataPublisher['on']>[0],

Diff for: packages/subscribable/src/event-emitter.ts

+24-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,19 @@
11
type EventMap = Record<string, Event>;
22
type Listener<TEvent extends Event> = ((evt: TEvent) => void) | { handleEvent(object: TEvent): void };
33

4+
/**
5+
* This type allows you to type `addEventListener` and `removeEventListener` so that the call
6+
* signature of the listener matches the event type given.
7+
*
8+
* @example
9+
* ```ts
10+
* const emitter: TypedEventEmitter<{ message: MessageEvent }> = new WebSocket('wss://api.devnet.solana.com');
11+
* emitter.addEventListener('data', handleData); // ERROR. `data` is not a known event type.
12+
* emitter.addEventListener('message', message => {
13+
* console.log(message.origin); // OK. `message` is a `MessageEvent` so it has an `origin` property.
14+
* });
15+
* ```
16+
*/
417
export interface TypedEventEmitter<TEventMap extends EventMap> {
518
addEventListener<const TEventType extends keyof TEventMap>(
619
type: TEventType,
@@ -14,9 +27,18 @@ export interface TypedEventEmitter<TEventMap extends EventMap> {
1427
): void;
1528
}
1629

30+
// Why not just extend the interface above, rather than to copy/paste it?
31+
// See https://github.com/microsoft/TypeScript/issues/60008
1732
/**
18-
* Why not just extend the interface above, rather than to copy/paste it?
19-
* See https://github.com/microsoft/TypeScript/issues/60008
33+
* This type is a superset of `TypedEventEmitter` that allows you to constrain calls to
34+
* `dispatchEvent`.
35+
*
36+
* @example
37+
* ```ts
38+
* const target: TypedEventTarget<{ candyVended: CustomEvent<{ flavour: string }> }> = new EventTarget();
39+
* target.dispatchEvent(new CustomEvent('candyVended', { detail: { flavour: 'raspberry' } })); // OK.
40+
* target.dispatchEvent(new CustomEvent('candyVended', { detail: { flavor: 'raspberry' } })); // ERROR. Misspelling in detail.
41+
* ```
2042
*/
2143
export interface TypedEventTarget<TEventMap extends EventMap> {
2244
addEventListener<const TEventType extends keyof TEventMap>(

Diff for: packages/subscribable/src/index.ts

+8
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,11 @@
1+
/**
2+
* This package contains utilities for creating subscription-based event targets. These differ from
3+
* the `EventTarget` interface in that the method you use to add a listener returns an unsubscribe
4+
* function. It is primarily intended for internal use -- particularly for those building
5+
* {@link RpcSubscriptionChannel | RpcSubscriptionChannels} and associated infrastructure.
6+
*
7+
* @packageDocumentation
8+
*/
19
export * from './async-iterable';
210
export * from './data-publisher';
311
export * from './demultiplex';

0 commit comments

Comments
 (0)