@atcute/firehose 0.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.
- package/LICENSE +14 -0
- package/README.md +23 -0
- package/dist/frame-decoder.d.ts +14 -0
- package/dist/frame-decoder.d.ts.map +1 -0
- package/dist/frame-decoder.js +59 -0
- package/dist/frame-decoder.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -0
- package/dist/subscription.d.ts +24 -0
- package/dist/subscription.d.ts.map +1 -0
- package/dist/subscription.js +112 -0
- package/dist/subscription.js.map +1 -0
- package/dist/types.d.ts +76 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/lib/frame-decoder.ts +72 -0
- package/lib/index.ts +2 -0
- package/lib/subscription.ts +157 -0
- package/lib/types.ts +103 -0
- package/package.json +36 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
BSD Zero Clause License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Mary
|
|
4
|
+
|
|
5
|
+
Permission to use, copy, modify, and/or distribute this software for any
|
|
6
|
+
purpose with or without fee is hereby granted.
|
|
7
|
+
|
|
8
|
+
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
|
|
9
|
+
REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
|
|
10
|
+
AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
|
|
11
|
+
INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
|
|
12
|
+
LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
|
|
13
|
+
OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
|
14
|
+
PERFORMANCE OF THIS SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# @atcute/firehose
|
|
2
|
+
|
|
3
|
+
lightweight and cute XRPC subscription client for AT Protocol.
|
|
4
|
+
|
|
5
|
+
```ts
|
|
6
|
+
import { FirehoseSubscription } from '@atcute/firehose';
|
|
7
|
+
import { ComAtprotoSyncSubscribeRepos } from '@atcute/atproto';
|
|
8
|
+
|
|
9
|
+
let cursor: number | undefined;
|
|
10
|
+
|
|
11
|
+
const subscription = new FirehoseSubscription({
|
|
12
|
+
service: ['wss://bsky.network'],
|
|
13
|
+
nsid: ComAtprotoSyncSubscribeRepos.mainSchema,
|
|
14
|
+
params: () => ({ cursor }),
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
for await (const message of subscription) {
|
|
18
|
+
if (message.$type === 'com.atproto.sync.subscribeRepos#commit') {
|
|
19
|
+
cursor = message.seq;
|
|
20
|
+
console.log('commit:', message.seq, message.repo);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
```
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { DecodedFrame } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* decodes a CBOR frame from a buffer
|
|
4
|
+
*/
|
|
5
|
+
export declare const decodeFrame: (buffer: Uint8Array) => DecodedFrame;
|
|
6
|
+
/**
|
|
7
|
+
* reconstructs full $type field from discriminator and nsid
|
|
8
|
+
*/
|
|
9
|
+
export declare const reconstructType: (discriminator: string | undefined, nsid: string) => string | undefined;
|
|
10
|
+
/**
|
|
11
|
+
* adds $type field to message body if discriminator is present
|
|
12
|
+
*/
|
|
13
|
+
export declare const addTypeToBody: (body: unknown, discriminator: string | undefined, nsid: string) => unknown;
|
|
14
|
+
//# sourceMappingURL=frame-decoder.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"frame-decoder.d.ts","sourceRoot":"","sources":["../lib/frame-decoder.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,YAAY,EAA+B,MAAM,YAAY,CAAC;AAE5E;;GAEG;AACH,eAAO,MAAM,WAAW,GAAI,QAAQ,UAAU,KAAG,YAuBhD,CAAC;AAeF;;GAEG;AACH,eAAO,MAAM,eAAe,GAAI,eAAe,MAAM,GAAG,SAAS,EAAE,MAAM,MAAM,KAAG,MAAM,GAAG,SAU1F,CAAC;AAEF;;GAEG;AACH,eAAO,MAAM,aAAa,GAAI,MAAM,OAAO,EAAE,eAAe,MAAM,GAAG,SAAS,EAAE,MAAM,MAAM,KAAG,OAQ9F,CAAC"}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { decode, decodeFirst } from '@atcute/cbor';
|
|
2
|
+
/**
|
|
3
|
+
* decodes a CBOR frame from a buffer
|
|
4
|
+
*/
|
|
5
|
+
export const decodeFrame = (buffer) => {
|
|
6
|
+
const [header, afterHeader] = decodeFirst(buffer);
|
|
7
|
+
if (!isValidHeader(header)) {
|
|
8
|
+
throw new Error('invalid frame header');
|
|
9
|
+
}
|
|
10
|
+
const body = decode(afterHeader);
|
|
11
|
+
if (header.op === 1) {
|
|
12
|
+
return {
|
|
13
|
+
type: 'message',
|
|
14
|
+
body,
|
|
15
|
+
discriminator: header.t,
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
else {
|
|
19
|
+
const errorBody = body;
|
|
20
|
+
return {
|
|
21
|
+
type: 'error',
|
|
22
|
+
error: errorBody.error,
|
|
23
|
+
message: errorBody.message,
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
/**
|
|
28
|
+
* type guard for frame header
|
|
29
|
+
*/
|
|
30
|
+
const isValidHeader = (value) => {
|
|
31
|
+
if (value === null || typeof value !== 'object') {
|
|
32
|
+
return false;
|
|
33
|
+
}
|
|
34
|
+
const obj = value;
|
|
35
|
+
return (obj.op === 1 || obj.op === -1) && (obj.t === undefined || typeof obj.t === 'string');
|
|
36
|
+
};
|
|
37
|
+
/**
|
|
38
|
+
* reconstructs full $type field from discriminator and nsid
|
|
39
|
+
*/
|
|
40
|
+
export const reconstructType = (discriminator, nsid) => {
|
|
41
|
+
if (!discriminator) {
|
|
42
|
+
return undefined;
|
|
43
|
+
}
|
|
44
|
+
if (discriminator[0] === '#') {
|
|
45
|
+
return nsid + discriminator;
|
|
46
|
+
}
|
|
47
|
+
return discriminator;
|
|
48
|
+
};
|
|
49
|
+
/**
|
|
50
|
+
* adds $type field to message body if discriminator is present
|
|
51
|
+
*/
|
|
52
|
+
export const addTypeToBody = (body, discriminator, nsid) => {
|
|
53
|
+
const type = reconstructType(discriminator, nsid);
|
|
54
|
+
if (type && typeof body === 'object' && body !== null) {
|
|
55
|
+
body.$type = type;
|
|
56
|
+
}
|
|
57
|
+
return body;
|
|
58
|
+
};
|
|
59
|
+
//# sourceMappingURL=frame-decoder.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"frame-decoder.js","sourceRoot":"","sources":["../lib/frame-decoder.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AAInD;;GAEG;AACH,MAAM,CAAC,MAAM,WAAW,GAAG,CAAC,MAAkB,EAAgB,EAAE;IAC/D,MAAM,CAAC,MAAM,EAAE,WAAW,CAAC,GAAG,WAAW,CAAC,MAAM,CAAC,CAAC;IAElD,IAAI,CAAC,aAAa,CAAC,MAAM,CAAC,EAAE,CAAC;QAC5B,MAAM,IAAI,KAAK,CAAC,sBAAsB,CAAC,CAAC;IACzC,CAAC;IAED,MAAM,IAAI,GAAG,MAAM,CAAC,WAAW,CAAC,CAAC;IAEjC,IAAI,MAAM,CAAC,EAAE,KAAK,CAAC,EAAE,CAAC;QACrB,OAAO;YACN,IAAI,EAAE,SAAS;YACf,IAAI;YACJ,aAAa,EAAE,MAAM,CAAC,CAAC;SACvB,CAAC;IACH,CAAC;SAAM,CAAC;QACP,MAAM,SAAS,GAAG,IAAsB,CAAC;QACzC,OAAO;YACN,IAAI,EAAE,OAAO;YACb,KAAK,EAAE,SAAS,CAAC,KAAK;YACtB,OAAO,EAAE,SAAS,CAAC,OAAO;SAC1B,CAAC;IACH,CAAC;AACF,CAAC,CAAC;AAEF;;GAEG;AACH,MAAM,aAAa,GAAG,CAAC,KAAc,EAAwB,EAAE;IAC9D,IAAI,KAAK,KAAK,IAAI,IAAI,OAAO,KAAK,KAAK,QAAQ,EAAE,CAAC;QACjD,OAAO,KAAK,CAAC;IACd,CAAC;IAED,MAAM,GAAG,GAAG,KAAgC,CAAC;IAE7C,OAAO,CAAC,GAAG,CAAC,EAAE,KAAK,CAAC,IAAI,GAAG,CAAC,EAAE,KAAK,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,KAAK,SAAS,IAAI,OAAO,GAAG,CAAC,CAAC,KAAK,QAAQ,CAAC,CAAC;AAC9F,CAAC,CAAC;AAEF;;GAEG;AACH,MAAM,CAAC,MAAM,eAAe,GAAG,CAAC,aAAiC,EAAE,IAAY,EAAsB,EAAE;IACtG,IAAI,CAAC,aAAa,EAAE,CAAC;QACpB,OAAO,SAAS,CAAC;IAClB,CAAC;IAED,IAAI,aAAa,CAAC,CAAC,CAAC,KAAK,GAAG,EAAE,CAAC;QAC9B,OAAO,IAAI,GAAG,aAAa,CAAC;IAC7B,CAAC;IAED,OAAO,aAAa,CAAC;AACtB,CAAC,CAAC;AAEF;;GAEG;AACH,MAAM,CAAC,MAAM,aAAa,GAAG,CAAC,IAAa,EAAE,aAAiC,EAAE,IAAY,EAAW,EAAE;IACxG,MAAM,IAAI,GAAG,eAAe,CAAC,aAAa,EAAE,IAAI,CAAC,CAAC;IAElD,IAAI,IAAI,IAAI,OAAO,IAAI,KAAK,QAAQ,IAAI,IAAI,KAAK,IAAI,EAAE,CAAC;QACtD,IAAgC,CAAC,KAAK,GAAG,IAAI,CAAC;IAChD,CAAC;IAED,OAAO,IAAI,CAAC;AACb,CAAC,CAAC"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../lib/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,oBAAoB,EAAE,MAAM,mBAAmB,CAAC;AACzD,YAAY,EAAE,2BAA2B,EAAE,SAAS,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../lib/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,oBAAoB,EAAE,MAAM,mBAAmB,CAAC"}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { type XRPCSubscriptionMetadata } from '@atcute/lexicons/validations';
|
|
2
|
+
import { EventIterator } from '@mary-ext/event-iterator';
|
|
3
|
+
import type { ReadonlyDeep } from 'type-fest';
|
|
4
|
+
import type { FirehoseSubscriptionOptions, MessageOf } from './types.js';
|
|
5
|
+
/**
|
|
6
|
+
* generic XRPC subscription client for AT Protocol
|
|
7
|
+
*/
|
|
8
|
+
export declare class FirehoseSubscription<TSchema extends XRPCSubscriptionMetadata> {
|
|
9
|
+
#private;
|
|
10
|
+
/**
|
|
11
|
+
* creates a new firehose subscription
|
|
12
|
+
*/
|
|
13
|
+
constructor(options: FirehoseSubscriptionOptions<TSchema>);
|
|
14
|
+
[Symbol.asyncIterator](): EventIterator<MessageOf<TSchema>>;
|
|
15
|
+
/**
|
|
16
|
+
* get current subscription options
|
|
17
|
+
*/
|
|
18
|
+
getOptions(): ReadonlyDeep<FirehoseSubscriptionOptions<TSchema>>;
|
|
19
|
+
/**
|
|
20
|
+
* update subscription options, triggering a reconnection if currently connected
|
|
21
|
+
*/
|
|
22
|
+
updateOptions(options: Partial<FirehoseSubscriptionOptions<TSchema>>): void;
|
|
23
|
+
}
|
|
24
|
+
//# sourceMappingURL=subscription.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"subscription.d.ts","sourceRoot":"","sources":["../lib/subscription.ts"],"names":[],"mappings":"AAAA,OAAO,EAAa,KAAK,wBAAwB,EAAE,MAAM,8BAA8B,CAAC;AAExF,OAAO,EAAE,aAAa,EAAE,MAAM,0BAA0B,CAAC;AAKzD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,WAAW,CAAC;AAG9C,OAAO,KAAK,EAAE,2BAA2B,EAAE,SAAS,EAAY,MAAM,YAAY,CAAC;AAEnF;;GAEG;AACH,qBAAa,oBAAoB,CAAC,OAAO,SAAS,wBAAwB;;IAQzE;;OAEG;gBACS,OAAO,EAAE,2BAA2B,CAAC,OAAO,CAAC;IA4FzD,CAAC,MAAM,CAAC,aAAa,CAAC;IAoBtB;;OAEG;IACH,UAAU,IAAI,YAAY,CAAC,2BAA2B,CAAC,OAAO,CAAC,CAAC;IAIhE;;OAEG;IACH,aAAa,CAAC,OAAO,EAAE,OAAO,CAAC,2BAA2B,CAAC,OAAO,CAAC,CAAC,GAAG,IAAI;CAQ3E"}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { safeParse } from '@atcute/lexicons/validations';
|
|
2
|
+
import { EventIterator } from '@mary-ext/event-iterator';
|
|
3
|
+
import { SimpleEventEmitter } from '@mary-ext/simple-event-emitter';
|
|
4
|
+
import { WebSocket as ReconnectingWebSocket } from 'partysocket';
|
|
5
|
+
import { addTypeToBody, decodeFrame } from './frame-decoder.js';
|
|
6
|
+
/**
|
|
7
|
+
* generic XRPC subscription client for AT Protocol
|
|
8
|
+
*/
|
|
9
|
+
export class FirehoseSubscription {
|
|
10
|
+
#listening = 0;
|
|
11
|
+
#ws;
|
|
12
|
+
#emitter = new SimpleEventEmitter();
|
|
13
|
+
#options;
|
|
14
|
+
/**
|
|
15
|
+
* creates a new firehose subscription
|
|
16
|
+
*/
|
|
17
|
+
constructor(options) {
|
|
18
|
+
this.#options = options;
|
|
19
|
+
}
|
|
20
|
+
#create() {
|
|
21
|
+
if (this.#ws !== undefined) {
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
const { service: wsUrls, nsid, params, ws: wsOptions, validateMessages = true, onConnectionClose, onConnectionError, onConnectionOpen, onError, } = this.#options;
|
|
25
|
+
const emitter = this.#emitter;
|
|
26
|
+
const getUrl = () => {
|
|
27
|
+
let selectedUrl;
|
|
28
|
+
if (typeof wsUrls === 'string') {
|
|
29
|
+
selectedUrl = wsUrls;
|
|
30
|
+
}
|
|
31
|
+
else {
|
|
32
|
+
selectedUrl = wsUrls[Math.floor(Math.random() * wsUrls.length)];
|
|
33
|
+
}
|
|
34
|
+
const url = new URL('/xrpc/' + nsid.nsid, selectedUrl);
|
|
35
|
+
const currentParams = typeof params === 'function' ? params() : params;
|
|
36
|
+
if (currentParams !== undefined && currentParams !== null) {
|
|
37
|
+
const paramObj = currentParams;
|
|
38
|
+
for (const key in paramObj) {
|
|
39
|
+
const value = paramObj[key];
|
|
40
|
+
if (value !== undefined && value !== null) {
|
|
41
|
+
url.searchParams.set(key, String(value));
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return url.toString();
|
|
46
|
+
};
|
|
47
|
+
const ws = new ReconnectingWebSocket(getUrl, null, wsOptions);
|
|
48
|
+
this.#ws = ws;
|
|
49
|
+
ws.binaryType = 'arraybuffer';
|
|
50
|
+
ws.onerror = onConnectionError ?? null;
|
|
51
|
+
ws.onclose = onConnectionClose ?? null;
|
|
52
|
+
ws.onopen = onConnectionOpen ?? null;
|
|
53
|
+
ws.onmessage = (ev) => {
|
|
54
|
+
const buffer = new Uint8Array(ev.data);
|
|
55
|
+
const frame = decodeFrame(buffer);
|
|
56
|
+
if (frame.type === 'error') {
|
|
57
|
+
onError?.(frame.error, frame.message);
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
let body = addTypeToBody(frame.body, frame.discriminator, nsid.nsid);
|
|
61
|
+
if (validateMessages && nsid.message !== null) {
|
|
62
|
+
const result = safeParse(nsid.message, body);
|
|
63
|
+
if (!result.ok) {
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
body = result.value;
|
|
67
|
+
}
|
|
68
|
+
emitter.emit(body);
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
#destroy() {
|
|
72
|
+
const ws = this.#ws;
|
|
73
|
+
if (ws === undefined) {
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
ws.close();
|
|
77
|
+
this.#ws = undefined;
|
|
78
|
+
}
|
|
79
|
+
[Symbol.asyncIterator]() {
|
|
80
|
+
return new EventIterator((emit) => {
|
|
81
|
+
if (this.#listening === 0) {
|
|
82
|
+
this.#create();
|
|
83
|
+
}
|
|
84
|
+
this.#listening++;
|
|
85
|
+
this.#emitter.subscribe(emit);
|
|
86
|
+
return () => {
|
|
87
|
+
if (this.#listening === 1) {
|
|
88
|
+
this.#destroy();
|
|
89
|
+
}
|
|
90
|
+
this.#listening--;
|
|
91
|
+
this.#emitter.unsubscribe(emit);
|
|
92
|
+
};
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* get current subscription options
|
|
97
|
+
*/
|
|
98
|
+
getOptions() {
|
|
99
|
+
return this.#options;
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* update subscription options, triggering a reconnection if currently connected
|
|
103
|
+
*/
|
|
104
|
+
updateOptions(options) {
|
|
105
|
+
this.#options = { ...this.#options, ...options };
|
|
106
|
+
if (this.#ws !== undefined) {
|
|
107
|
+
this.#destroy();
|
|
108
|
+
this.#create();
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
//# sourceMappingURL=subscription.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"subscription.js","sourceRoot":"","sources":["../lib/subscription.ts"],"names":[],"mappings":"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"}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import type { InferOutput, XRPCSubscriptionMetadata } from '@atcute/lexicons/validations';
|
|
2
|
+
import type { CloseEvent, ErrorEvent, Options } from 'partysocket/ws';
|
|
3
|
+
import type { BaseSchema } from '@atcute/lexicons/validations';
|
|
4
|
+
/**
|
|
5
|
+
* extracts the params type from an XRPC subscription schema
|
|
6
|
+
*/
|
|
7
|
+
export type ParamsOf<T> = T extends XRPCSubscriptionMetadata<infer TParams, any, any> ? TParams extends null ? undefined : TParams extends BaseSchema ? InferOutput<TParams> : never : never;
|
|
8
|
+
/**
|
|
9
|
+
* extracts the message type from an XRPC subscription schema
|
|
10
|
+
*/
|
|
11
|
+
export type MessageOf<T> = T extends XRPCSubscriptionMetadata<any, infer TMessage, any> ? TMessage extends null ? unknown : TMessage extends BaseSchema ? InferOutput<TMessage> : never : never;
|
|
12
|
+
/**
|
|
13
|
+
* configuration options for FirehoseSubscription
|
|
14
|
+
*/
|
|
15
|
+
export interface FirehoseSubscriptionOptions<TSchema extends XRPCSubscriptionMetadata> {
|
|
16
|
+
/**
|
|
17
|
+
* XRPC service URL(s) to connect to
|
|
18
|
+
*/
|
|
19
|
+
service: string | string[];
|
|
20
|
+
/**
|
|
21
|
+
* XRPC subscription schema from @atcute/lexicons
|
|
22
|
+
*/
|
|
23
|
+
nsid: TSchema;
|
|
24
|
+
/**
|
|
25
|
+
* subscription parameters - can be a static object or a function that returns
|
|
26
|
+
* params. the function is called on each connection attempt, allowing for
|
|
27
|
+
* dynamic cursor tracking and reconnection state management.
|
|
28
|
+
*/
|
|
29
|
+
params?: ParamsOf<TSchema> | (() => ParamsOf<TSchema>);
|
|
30
|
+
/**
|
|
31
|
+
* whether to validate incoming messages against the schema
|
|
32
|
+
* @default true
|
|
33
|
+
*/
|
|
34
|
+
validateMessages?: boolean;
|
|
35
|
+
onConnectionOpen?: (event: Event) => void;
|
|
36
|
+
onConnectionClose?: (event: CloseEvent) => void;
|
|
37
|
+
onConnectionError?: (event: ErrorEvent) => void;
|
|
38
|
+
onError?: (error: string, message?: string) => void;
|
|
39
|
+
/**
|
|
40
|
+
* WebSocket connection options
|
|
41
|
+
*/
|
|
42
|
+
ws?: Options;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* decoded CBOR frame header
|
|
46
|
+
*/
|
|
47
|
+
export interface FrameHeader {
|
|
48
|
+
/**
|
|
49
|
+
* operation code: 1 for message, -1 for error
|
|
50
|
+
*/
|
|
51
|
+
op: 1 | -1;
|
|
52
|
+
/**
|
|
53
|
+
* type discriminator for message frames (relative to NSID, e.g., "#commit")
|
|
54
|
+
*/
|
|
55
|
+
t?: string;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* error frame body
|
|
59
|
+
*/
|
|
60
|
+
export interface ErrorFrameBody {
|
|
61
|
+
error: string;
|
|
62
|
+
message?: string;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* decoded frame result
|
|
66
|
+
*/
|
|
67
|
+
export type DecodedFrame = {
|
|
68
|
+
type: 'message';
|
|
69
|
+
body: unknown;
|
|
70
|
+
discriminator?: string;
|
|
71
|
+
} | {
|
|
72
|
+
type: 'error';
|
|
73
|
+
error: string;
|
|
74
|
+
message?: string;
|
|
75
|
+
};
|
|
76
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../lib/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,wBAAwB,EAAE,MAAM,8BAA8B,CAAC;AAE1F,OAAO,KAAK,EAAE,UAAU,EAAE,UAAU,EAAE,OAAO,EAAE,MAAM,gBAAgB,CAAC;AAEtE,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,8BAA8B,CAAC;AAE/D;;GAEG;AACH,MAAM,MAAM,QAAQ,CAAC,CAAC,IAAI,CAAC,SAAS,wBAAwB,CAAC,MAAM,OAAO,EAAE,GAAG,EAAE,GAAG,CAAC,GAClF,OAAO,SAAS,IAAI,GACnB,SAAS,GACT,OAAO,SAAS,UAAU,GACzB,WAAW,CAAC,OAAO,CAAC,GACpB,KAAK,GACP,KAAK,CAAC;AAET;;GAEG;AACH,MAAM,MAAM,SAAS,CAAC,CAAC,IAAI,CAAC,SAAS,wBAAwB,CAAC,GAAG,EAAE,MAAM,QAAQ,EAAE,GAAG,CAAC,GACpF,QAAQ,SAAS,IAAI,GACpB,OAAO,GACP,QAAQ,SAAS,UAAU,GAC1B,WAAW,CAAC,QAAQ,CAAC,GACrB,KAAK,GACP,KAAK,CAAC;AAET;;GAEG;AACH,MAAM,WAAW,2BAA2B,CAAC,OAAO,SAAS,wBAAwB;IACpF;;OAEG;IACH,OAAO,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC;IAE3B;;OAEG;IACH,IAAI,EAAE,OAAO,CAAC;IAEd;;;;OAIG;IACH,MAAM,CAAC,EAAE,QAAQ,CAAC,OAAO,CAAC,GAAG,CAAC,MAAM,QAAQ,CAAC,OAAO,CAAC,CAAC,CAAC;IAEvD;;;OAGG;IACH,gBAAgB,CAAC,EAAE,OAAO,CAAC;IAE3B,gBAAgB,CAAC,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,IAAI,CAAC;IAC1C,iBAAiB,CAAC,EAAE,CAAC,KAAK,EAAE,UAAU,KAAK,IAAI,CAAC;IAChD,iBAAiB,CAAC,EAAE,CAAC,KAAK,EAAE,UAAU,KAAK,IAAI,CAAC;IAChD,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,MAAM,KAAK,IAAI,CAAC;IAEpD;;OAEG;IACH,EAAE,CAAC,EAAE,OAAO,CAAC;CACb;AAED;;GAEG;AACH,MAAM,WAAW,WAAW;IAC3B;;OAEG;IACH,EAAE,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC;IAEX;;OAEG;IACH,CAAC,CAAC,EAAE,MAAM,CAAC;CACX;AAED;;GAEG;AACH,MAAM,WAAW,cAAc;IAC9B,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,CAAC,EAAE,MAAM,CAAC;CACjB;AAED;;GAEG;AACH,MAAM,MAAM,YAAY,GACrB;IACA,IAAI,EAAE,SAAS,CAAC;IAChB,IAAI,EAAE,OAAO,CAAC;IACd,aAAa,CAAC,EAAE,MAAM,CAAC;CACtB,GACD;IACA,IAAI,EAAE,OAAO,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,CAAC,EAAE,MAAM,CAAC;CAChB,CAAC"}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../lib/types.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { decode, decodeFirst } from '@atcute/cbor';
|
|
2
|
+
|
|
3
|
+
import type { DecodedFrame, ErrorFrameBody, FrameHeader } from './types.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* decodes a CBOR frame from a buffer
|
|
7
|
+
*/
|
|
8
|
+
export const decodeFrame = (buffer: Uint8Array): DecodedFrame => {
|
|
9
|
+
const [header, afterHeader] = decodeFirst(buffer);
|
|
10
|
+
|
|
11
|
+
if (!isValidHeader(header)) {
|
|
12
|
+
throw new Error('invalid frame header');
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const body = decode(afterHeader);
|
|
16
|
+
|
|
17
|
+
if (header.op === 1) {
|
|
18
|
+
return {
|
|
19
|
+
type: 'message',
|
|
20
|
+
body,
|
|
21
|
+
discriminator: header.t,
|
|
22
|
+
};
|
|
23
|
+
} else {
|
|
24
|
+
const errorBody = body as ErrorFrameBody;
|
|
25
|
+
return {
|
|
26
|
+
type: 'error',
|
|
27
|
+
error: errorBody.error,
|
|
28
|
+
message: errorBody.message,
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* type guard for frame header
|
|
35
|
+
*/
|
|
36
|
+
const isValidHeader = (value: unknown): value is FrameHeader => {
|
|
37
|
+
if (value === null || typeof value !== 'object') {
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const obj = value as Record<string, unknown>;
|
|
42
|
+
|
|
43
|
+
return (obj.op === 1 || obj.op === -1) && (obj.t === undefined || typeof obj.t === 'string');
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* reconstructs full $type field from discriminator and nsid
|
|
48
|
+
*/
|
|
49
|
+
export const reconstructType = (discriminator: string | undefined, nsid: string): string | undefined => {
|
|
50
|
+
if (!discriminator) {
|
|
51
|
+
return undefined;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (discriminator[0] === '#') {
|
|
55
|
+
return nsid + discriminator;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return discriminator;
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* adds $type field to message body if discriminator is present
|
|
63
|
+
*/
|
|
64
|
+
export const addTypeToBody = (body: unknown, discriminator: string | undefined, nsid: string): unknown => {
|
|
65
|
+
const type = reconstructType(discriminator, nsid);
|
|
66
|
+
|
|
67
|
+
if (type && typeof body === 'object' && body !== null) {
|
|
68
|
+
(body as Record<string, unknown>).$type = type;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return body;
|
|
72
|
+
};
|
package/lib/index.ts
ADDED
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import { safeParse, type XRPCSubscriptionMetadata } from '@atcute/lexicons/validations';
|
|
2
|
+
|
|
3
|
+
import { EventIterator } from '@mary-ext/event-iterator';
|
|
4
|
+
import { SimpleEventEmitter } from '@mary-ext/simple-event-emitter';
|
|
5
|
+
|
|
6
|
+
import { WebSocket as ReconnectingWebSocket } from 'partysocket';
|
|
7
|
+
|
|
8
|
+
import type { ReadonlyDeep } from 'type-fest';
|
|
9
|
+
|
|
10
|
+
import { addTypeToBody, decodeFrame } from './frame-decoder.js';
|
|
11
|
+
import type { FirehoseSubscriptionOptions, MessageOf, ParamsOf } from './types.js';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* generic XRPC subscription client for AT Protocol
|
|
15
|
+
*/
|
|
16
|
+
export class FirehoseSubscription<TSchema extends XRPCSubscriptionMetadata> {
|
|
17
|
+
#listening = 0;
|
|
18
|
+
#ws?: ReconnectingWebSocket;
|
|
19
|
+
|
|
20
|
+
#emitter = new SimpleEventEmitter<[message: MessageOf<TSchema>]>();
|
|
21
|
+
|
|
22
|
+
#options: FirehoseSubscriptionOptions<TSchema>;
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* creates a new firehose subscription
|
|
26
|
+
*/
|
|
27
|
+
constructor(options: FirehoseSubscriptionOptions<TSchema>) {
|
|
28
|
+
this.#options = options;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
#create() {
|
|
32
|
+
if (this.#ws !== undefined) {
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const {
|
|
37
|
+
service: wsUrls,
|
|
38
|
+
nsid,
|
|
39
|
+
params,
|
|
40
|
+
ws: wsOptions,
|
|
41
|
+
validateMessages = true,
|
|
42
|
+
onConnectionClose,
|
|
43
|
+
onConnectionError,
|
|
44
|
+
onConnectionOpen,
|
|
45
|
+
onError,
|
|
46
|
+
} = this.#options;
|
|
47
|
+
|
|
48
|
+
const emitter = this.#emitter;
|
|
49
|
+
|
|
50
|
+
const getUrl = () => {
|
|
51
|
+
let selectedUrl: string;
|
|
52
|
+
|
|
53
|
+
if (typeof wsUrls === 'string') {
|
|
54
|
+
selectedUrl = wsUrls;
|
|
55
|
+
} else {
|
|
56
|
+
selectedUrl = wsUrls[Math.floor(Math.random() * wsUrls.length)];
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const url = new URL('/xrpc/' + nsid.nsid, selectedUrl);
|
|
60
|
+
|
|
61
|
+
const currentParams: ParamsOf<TSchema> = typeof params === 'function' ? params() : params;
|
|
62
|
+
|
|
63
|
+
if (currentParams !== undefined && currentParams !== null) {
|
|
64
|
+
const paramObj = currentParams as Record<string, unknown>;
|
|
65
|
+
for (const key in paramObj) {
|
|
66
|
+
const value = paramObj[key];
|
|
67
|
+
if (value !== undefined && value !== null) {
|
|
68
|
+
url.searchParams.set(key, String(value));
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return url.toString();
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
const ws = new ReconnectingWebSocket(getUrl, null, wsOptions);
|
|
77
|
+
this.#ws = ws;
|
|
78
|
+
|
|
79
|
+
ws.binaryType = 'arraybuffer';
|
|
80
|
+
|
|
81
|
+
ws.onerror = onConnectionError ?? null;
|
|
82
|
+
ws.onclose = onConnectionClose ?? null;
|
|
83
|
+
ws.onopen = onConnectionOpen ?? null;
|
|
84
|
+
|
|
85
|
+
ws.onmessage = (ev) => {
|
|
86
|
+
const buffer = new Uint8Array(ev.data);
|
|
87
|
+
const frame = decodeFrame(buffer);
|
|
88
|
+
|
|
89
|
+
if (frame.type === 'error') {
|
|
90
|
+
onError?.(frame.error, frame.message);
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
let body = addTypeToBody(frame.body, frame.discriminator, nsid.nsid);
|
|
95
|
+
|
|
96
|
+
if (validateMessages && nsid.message !== null) {
|
|
97
|
+
const result = safeParse(nsid.message, body);
|
|
98
|
+
if (!result.ok) {
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
body = result.value;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
emitter.emit(body as MessageOf<TSchema>);
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
#destroy() {
|
|
109
|
+
const ws = this.#ws;
|
|
110
|
+
if (ws === undefined) {
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
ws.close();
|
|
115
|
+
|
|
116
|
+
this.#ws = undefined;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
[Symbol.asyncIterator]() {
|
|
120
|
+
return new EventIterator<MessageOf<TSchema>>((emit) => {
|
|
121
|
+
if (this.#listening === 0) {
|
|
122
|
+
this.#create();
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
this.#listening++;
|
|
126
|
+
this.#emitter.subscribe(emit);
|
|
127
|
+
|
|
128
|
+
return () => {
|
|
129
|
+
if (this.#listening === 1) {
|
|
130
|
+
this.#destroy();
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
this.#listening--;
|
|
134
|
+
this.#emitter.unsubscribe(emit);
|
|
135
|
+
};
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* get current subscription options
|
|
141
|
+
*/
|
|
142
|
+
getOptions(): ReadonlyDeep<FirehoseSubscriptionOptions<TSchema>> {
|
|
143
|
+
return this.#options as ReadonlyDeep<FirehoseSubscriptionOptions<TSchema>>;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* update subscription options, triggering a reconnection if currently connected
|
|
148
|
+
*/
|
|
149
|
+
updateOptions(options: Partial<FirehoseSubscriptionOptions<TSchema>>): void {
|
|
150
|
+
this.#options = { ...this.#options, ...options };
|
|
151
|
+
|
|
152
|
+
if (this.#ws !== undefined) {
|
|
153
|
+
this.#destroy();
|
|
154
|
+
this.#create();
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
package/lib/types.ts
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import type { InferOutput, XRPCSubscriptionMetadata } from '@atcute/lexicons/validations';
|
|
2
|
+
|
|
3
|
+
import type { CloseEvent, ErrorEvent, Options } from 'partysocket/ws';
|
|
4
|
+
|
|
5
|
+
import type { BaseSchema } from '@atcute/lexicons/validations';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* extracts the params type from an XRPC subscription schema
|
|
9
|
+
*/
|
|
10
|
+
export type ParamsOf<T> = T extends XRPCSubscriptionMetadata<infer TParams, any, any>
|
|
11
|
+
? TParams extends null
|
|
12
|
+
? undefined
|
|
13
|
+
: TParams extends BaseSchema
|
|
14
|
+
? InferOutput<TParams>
|
|
15
|
+
: never
|
|
16
|
+
: never;
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* extracts the message type from an XRPC subscription schema
|
|
20
|
+
*/
|
|
21
|
+
export type MessageOf<T> = T extends XRPCSubscriptionMetadata<any, infer TMessage, any>
|
|
22
|
+
? TMessage extends null
|
|
23
|
+
? unknown
|
|
24
|
+
: TMessage extends BaseSchema
|
|
25
|
+
? InferOutput<TMessage>
|
|
26
|
+
: never
|
|
27
|
+
: never;
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* configuration options for FirehoseSubscription
|
|
31
|
+
*/
|
|
32
|
+
export interface FirehoseSubscriptionOptions<TSchema extends XRPCSubscriptionMetadata> {
|
|
33
|
+
/**
|
|
34
|
+
* XRPC service URL(s) to connect to
|
|
35
|
+
*/
|
|
36
|
+
service: string | string[];
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* XRPC subscription schema from @atcute/lexicons
|
|
40
|
+
*/
|
|
41
|
+
nsid: TSchema;
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* subscription parameters - can be a static object or a function that returns
|
|
45
|
+
* params. the function is called on each connection attempt, allowing for
|
|
46
|
+
* dynamic cursor tracking and reconnection state management.
|
|
47
|
+
*/
|
|
48
|
+
params?: ParamsOf<TSchema> | (() => ParamsOf<TSchema>);
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* whether to validate incoming messages against the schema
|
|
52
|
+
* @default true
|
|
53
|
+
*/
|
|
54
|
+
validateMessages?: boolean;
|
|
55
|
+
|
|
56
|
+
onConnectionOpen?: (event: Event) => void;
|
|
57
|
+
onConnectionClose?: (event: CloseEvent) => void;
|
|
58
|
+
onConnectionError?: (event: ErrorEvent) => void;
|
|
59
|
+
onError?: (error: string, message?: string) => void;
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* WebSocket connection options
|
|
63
|
+
*/
|
|
64
|
+
ws?: Options;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* decoded CBOR frame header
|
|
69
|
+
*/
|
|
70
|
+
export interface FrameHeader {
|
|
71
|
+
/**
|
|
72
|
+
* operation code: 1 for message, -1 for error
|
|
73
|
+
*/
|
|
74
|
+
op: 1 | -1;
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* type discriminator for message frames (relative to NSID, e.g., "#commit")
|
|
78
|
+
*/
|
|
79
|
+
t?: string;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* error frame body
|
|
84
|
+
*/
|
|
85
|
+
export interface ErrorFrameBody {
|
|
86
|
+
error: string;
|
|
87
|
+
message?: string;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* decoded frame result
|
|
92
|
+
*/
|
|
93
|
+
export type DecodedFrame =
|
|
94
|
+
| {
|
|
95
|
+
type: 'message';
|
|
96
|
+
body: unknown;
|
|
97
|
+
discriminator?: string;
|
|
98
|
+
}
|
|
99
|
+
| {
|
|
100
|
+
type: 'error';
|
|
101
|
+
error: string;
|
|
102
|
+
message?: string;
|
|
103
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
{
|
|
2
|
+
"type": "module",
|
|
3
|
+
"name": "@atcute/firehose",
|
|
4
|
+
"version": "0.1.0",
|
|
5
|
+
"description": "lightweight and cute XRPC subscription client for AT Protocol",
|
|
6
|
+
"license": "0BSD",
|
|
7
|
+
"repository": {
|
|
8
|
+
"url": "https://github.com/mary-ext/atcute",
|
|
9
|
+
"directory": "packages/clients/firehose"
|
|
10
|
+
},
|
|
11
|
+
"files": [
|
|
12
|
+
"dist/",
|
|
13
|
+
"lib/",
|
|
14
|
+
"!lib/**/*.bench.ts",
|
|
15
|
+
"!lib/**/*.test.ts"
|
|
16
|
+
],
|
|
17
|
+
"exports": {
|
|
18
|
+
".": "./dist/index.js"
|
|
19
|
+
},
|
|
20
|
+
"dependencies": {
|
|
21
|
+
"@mary-ext/event-iterator": "^1.0.0",
|
|
22
|
+
"@mary-ext/simple-event-emitter": "^1.0.0",
|
|
23
|
+
"partysocket": "^1.1.6",
|
|
24
|
+
"type-fest": "^4.41.0",
|
|
25
|
+
"@atcute/lexicons": "^1.2.2",
|
|
26
|
+
"@atcute/cbor": "^2.2.7",
|
|
27
|
+
"@atcute/uint8array": "^1.0.5"
|
|
28
|
+
},
|
|
29
|
+
"devDependencies": {
|
|
30
|
+
"@atcute/atproto": "^3.1.8"
|
|
31
|
+
},
|
|
32
|
+
"scripts": {
|
|
33
|
+
"build": "tsc --project tsconfig.build.json",
|
|
34
|
+
"prepublish": "rm -rf dist; pnpm run build"
|
|
35
|
+
}
|
|
36
|
+
}
|