@ewanc26/atproto 0.2.10 → 0.2.11
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/dist/agents.d.ts.map +1 -1
- package/dist/agents.js +17 -3
- package/dist/jetstream.d.ts +100 -0
- package/dist/jetstream.d.ts.map +1 -0
- package/dist/jetstream.js +270 -0
- package/dist/musicbrainz.d.ts +1 -0
- package/dist/musicbrainz.d.ts.map +1 -1
- package/dist/musicbrainz.js +15 -2
- package/package.json +1 -1
- package/src/agents.ts +30 -4
- package/src/musicbrainz.ts +20 -2
package/dist/agents.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"agents.d.ts","sourceRoot":"","sources":["../src/agents.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,cAAc,CAAC;AACxC,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;
|
|
1
|
+
{"version":3,"file":"agents.d.ts","sourceRoot":"","sources":["../src/agents.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,cAAc,CAAC;AACxC,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;AAuBnD,wBAAgB,WAAW,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,OAAO,KAAK,GAAG,QAAQ,CAqB7E;AAED,eAAO,MAAM,kBAAkB,UAAsD,CAAC;AACtF,eAAO,MAAM,YAAY,UAA6C,CAAC;AAKvE,wBAAsB,eAAe,CACpC,GAAG,EAAE,MAAM,EACX,OAAO,CAAC,EAAE,OAAO,KAAK,GACpB,OAAO,CAAC,gBAAgB,CAAC,CAiC3B;AAED,wBAAsB,cAAc,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,OAAO,KAAK,GAAG,OAAO,CAAC,QAAQ,CAAC,CAwB3F;AAED,wBAAsB,WAAW,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,OAAO,KAAK,GAAG,OAAO,CAAC,QAAQ,CAAC,CAKxF;AAED,wBAAsB,YAAY,CAAC,CAAC,EACnC,GAAG,EAAE,MAAM,EACX,SAAS,EAAE,CAAC,KAAK,EAAE,QAAQ,KAAK,OAAO,CAAC,CAAC,CAAC,EAC1C,WAAW,UAAQ,EACnB,OAAO,CAAC,EAAE,OAAO,KAAK,GACpB,OAAO,CAAC,CAAC,CAAC,CAkBZ;AAED,wBAAgB,WAAW,IAAI,IAAI,CAGlC"}
|
package/dist/agents.js
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
import { AtpAgent } from '@atproto/api';
|
|
2
2
|
import { cache } from './cache.js';
|
|
3
|
+
/** Default timeout for individual AT Protocol XRPC calls (ms). */
|
|
4
|
+
const XRPC_TIMEOUT = 8_000;
|
|
5
|
+
/** Default timeout for identity resolution (ms). */
|
|
6
|
+
const IDENTITY_TIMEOUT = 5_000;
|
|
7
|
+
/**
|
|
8
|
+
* Wraps a promise with a timeout. Rejects with a TimeoutError if the promise
|
|
9
|
+
* doesn't settle within `ms` milliseconds.
|
|
10
|
+
*/
|
|
11
|
+
function withTimeout(promise, ms) {
|
|
12
|
+
return new Promise((resolve, reject) => {
|
|
13
|
+
const timer = setTimeout(() => reject(new Error(`Timeout after ${ms}ms`)), ms);
|
|
14
|
+
promise.then((value) => { clearTimeout(timer); resolve(value); }, (err) => { clearTimeout(timer); reject(err); });
|
|
15
|
+
});
|
|
16
|
+
}
|
|
3
17
|
export function createAgent(service, fetchFn) {
|
|
4
18
|
const wrappedFetch = fetchFn
|
|
5
19
|
? async (url, init) => {
|
|
@@ -31,7 +45,7 @@ export async function resolveIdentity(did, fetchFn) {
|
|
|
31
45
|
if (cached)
|
|
32
46
|
return cached;
|
|
33
47
|
const _fetch = fetchFn ?? globalThis.fetch;
|
|
34
|
-
const response = await _fetch(`https://slingshot.microcosm.blue/xrpc/com.bad-example.identity.resolveMiniDoc?identifier=${encodeURIComponent(did)}`);
|
|
48
|
+
const response = await withTimeout(_fetch(`https://slingshot.microcosm.blue/xrpc/com.bad-example.identity.resolveMiniDoc?identifier=${encodeURIComponent(did)}`), IDENTITY_TIMEOUT);
|
|
35
49
|
if (!response.ok) {
|
|
36
50
|
throw new Error(`Failed to resolve identifier via Slingshot: ${response.status} ${response.statusText}`);
|
|
37
51
|
}
|
|
@@ -54,7 +68,7 @@ export async function getPublicAgent(did, fetchFn) {
|
|
|
54
68
|
return resolvedAgent;
|
|
55
69
|
try {
|
|
56
70
|
try {
|
|
57
|
-
const response = await constellationAgent.getProfile({ actor: did });
|
|
71
|
+
const response = await withTimeout(constellationAgent.getProfile({ actor: did }), XRPC_TIMEOUT);
|
|
58
72
|
if (response.success) {
|
|
59
73
|
resolvedAgent = constellationAgent;
|
|
60
74
|
return resolvedAgent;
|
|
@@ -88,7 +102,7 @@ export async function withFallback(did, operation, usePDSFirst = false, fetchFn)
|
|
|
88
102
|
for (const getAgent of agents) {
|
|
89
103
|
try {
|
|
90
104
|
const agent = await getAgent();
|
|
91
|
-
return await operation(agent);
|
|
105
|
+
return await withTimeout(operation(agent), XRPC_TIMEOUT);
|
|
92
106
|
}
|
|
93
107
|
catch (error) {
|
|
94
108
|
lastError = error;
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Jetstream WebSocket client for AT Protocol real-time event streaming.
|
|
3
|
+
*
|
|
4
|
+
* Jetstream is a simplified JSON event stream that converts the CBOR-encoded
|
|
5
|
+
* AT Protocol firehose into lightweight, friendly JSON objects.
|
|
6
|
+
*
|
|
7
|
+
* @see https://github.com/bluesky-social/jetstream
|
|
8
|
+
*/
|
|
9
|
+
import type { JetstreamOptions, JetstreamEventHandler, JetstreamErrorHandler, JetstreamConnectionState } from './types.js';
|
|
10
|
+
/** Public Jetstream instances operated by Bluesky */
|
|
11
|
+
export declare const JETSTREAM_INSTANCES: {
|
|
12
|
+
readonly 'us-east-1': "wss://jetstream1.us-east.bsky.network";
|
|
13
|
+
readonly 'us-east-2': "wss://jetstream2.us-east.bsky.network";
|
|
14
|
+
readonly 'us-west-1': "wss://jetstream1.us-west.bsky.network";
|
|
15
|
+
readonly 'us-west-2': "wss://jetstream2.us-west.bsky.network";
|
|
16
|
+
};
|
|
17
|
+
export type JetstreamInstance = keyof typeof JETSTREAM_INSTANCES;
|
|
18
|
+
/**
|
|
19
|
+
* Client for connecting to AT Protocol Jetstream real-time event stream.
|
|
20
|
+
*
|
|
21
|
+
* @example
|
|
22
|
+
* ```typescript
|
|
23
|
+
* const client = new JetstreamClient({
|
|
24
|
+
* wantedCollections: ['app.bsky.feed.post'],
|
|
25
|
+
* wantedDids: ['did:plc:...'],
|
|
26
|
+
* });
|
|
27
|
+
*
|
|
28
|
+
* client.onEvent((event) => {
|
|
29
|
+
* console.log('Received event:', event.kind, event.commit?.collection);
|
|
30
|
+
* });
|
|
31
|
+
*
|
|
32
|
+
* client.connect();
|
|
33
|
+
* ```
|
|
34
|
+
*/
|
|
35
|
+
export declare class JetstreamClient {
|
|
36
|
+
private ws;
|
|
37
|
+
private url;
|
|
38
|
+
private options;
|
|
39
|
+
private eventHandlers;
|
|
40
|
+
private errorHandlers;
|
|
41
|
+
private stateHandlers;
|
|
42
|
+
private reconnectAttempts;
|
|
43
|
+
private cursor;
|
|
44
|
+
private isConnecting;
|
|
45
|
+
private shouldReconnect;
|
|
46
|
+
private reconnectTimeout;
|
|
47
|
+
private _connectionState;
|
|
48
|
+
constructor(options?: JetstreamOptions);
|
|
49
|
+
/** Current connection state */
|
|
50
|
+
get connectionState(): JetstreamConnectionState;
|
|
51
|
+
/** Get current cursor (last seen time_us) */
|
|
52
|
+
getCursor(): number | undefined;
|
|
53
|
+
/** Connect to Jetstream and start receiving events */
|
|
54
|
+
connect(): void;
|
|
55
|
+
/** Disconnect from Jetstream */
|
|
56
|
+
disconnect(): void;
|
|
57
|
+
/** Subscribe to events */
|
|
58
|
+
onEvent(handler: JetstreamEventHandler): () => void;
|
|
59
|
+
/** Subscribe to errors */
|
|
60
|
+
onError(handler: JetstreamErrorHandler): () => void;
|
|
61
|
+
/** Subscribe to connection state changes */
|
|
62
|
+
onStateChange(handler: (state: JetstreamConnectionState) => void): () => void;
|
|
63
|
+
/** Update subscription filters after connecting */
|
|
64
|
+
updateFilters(options: Partial<JetstreamOptions>): void;
|
|
65
|
+
/** Set cursor for replay */
|
|
66
|
+
setCursor(cursor: number): void;
|
|
67
|
+
private buildUrl;
|
|
68
|
+
private setState;
|
|
69
|
+
private handleError;
|
|
70
|
+
private scheduleReconnect;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Create a Jetstream client for a specific DID's data.
|
|
74
|
+
* Filters events to only include records from the specified DID.
|
|
75
|
+
*/
|
|
76
|
+
export declare function createDidJetstreamClient(did: string, options?: Omit<JetstreamOptions, 'wantedDids'>): JetstreamClient;
|
|
77
|
+
/**
|
|
78
|
+
* Create a Jetstream client for specific collections.
|
|
79
|
+
* Receives events for all DIDs but only for the specified collections.
|
|
80
|
+
*/
|
|
81
|
+
export declare function createCollectionJetstreamClient(collections: string[], options?: Omit<JetstreamOptions, 'wantedCollections'>): JetstreamClient;
|
|
82
|
+
/**
|
|
83
|
+
* Create a Jetstream client for the full firehose (all collections, all DIDs).
|
|
84
|
+
* Use with caution - this receives a high volume of events.
|
|
85
|
+
*/
|
|
86
|
+
export declare function createFullFirehoseClient(options?: JetstreamOptions): JetstreamClient;
|
|
87
|
+
/**
|
|
88
|
+
* Get the current time as a Unix microseconds cursor.
|
|
89
|
+
* Useful for starting live-tail mode.
|
|
90
|
+
*/
|
|
91
|
+
export declare function getCurrentCursor(): number;
|
|
92
|
+
/**
|
|
93
|
+
* Convert a Unix timestamp (ms) to Jetstream cursor (microseconds).
|
|
94
|
+
*/
|
|
95
|
+
export declare function timestampToCursor(timestampMs: number): number;
|
|
96
|
+
/**
|
|
97
|
+
* Convert a Jetstream cursor (microseconds) to Unix timestamp (ms).
|
|
98
|
+
*/
|
|
99
|
+
export declare function cursorToTimestamp(cursor: number): number;
|
|
100
|
+
//# sourceMappingURL=jetstream.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"jetstream.d.ts","sourceRoot":"","sources":["../src/jetstream.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,KAAK,EAEX,gBAAgB,EAChB,qBAAqB,EACrB,qBAAqB,EACrB,wBAAwB,EACxB,MAAM,YAAY,CAAC;AAEpB,qDAAqD;AACrD,eAAO,MAAM,mBAAmB;;;;;CAKtB,CAAC;AAEX,MAAM,MAAM,iBAAiB,GAAG,MAAM,OAAO,mBAAmB,CAAC;AAUjE;;;;;;;;;;;;;;;;GAgBG;AACH,qBAAa,eAAe;IAC3B,OAAO,CAAC,EAAE,CAA0B;IACpC,OAAO,CAAC,GAAG,CAAS;IACpB,OAAO,CAAC,OAAO,CAAmB;IAClC,OAAO,CAAC,aAAa,CAAyC;IAC9D,OAAO,CAAC,aAAa,CAAyC;IAC9D,OAAO,CAAC,aAAa,CAA6D;IAClF,OAAO,CAAC,iBAAiB,CAAK;IAC9B,OAAO,CAAC,MAAM,CAAqB;IACnC,OAAO,CAAC,YAAY,CAAS;IAC7B,OAAO,CAAC,eAAe,CAAQ;IAC/B,OAAO,CAAC,gBAAgB,CAA8C;IACtE,OAAO,CAAC,gBAAgB,CAA4C;gBAExD,OAAO,GAAE,gBAAqB;IAM1C,+BAA+B;IAC/B,IAAI,eAAe,IAAI,wBAAwB,CAE9C;IAED,6CAA6C;IAC7C,SAAS,IAAI,MAAM,GAAG,SAAS;IAI/B,sDAAsD;IACtD,OAAO,IAAI,IAAI;IA0Df,gCAAgC;IAChC,UAAU,IAAI,IAAI;IAalB,0BAA0B;IAC1B,OAAO,CAAC,OAAO,EAAE,qBAAqB,GAAG,MAAM,IAAI;IAKnD,0BAA0B;IAC1B,OAAO,CAAC,OAAO,EAAE,qBAAqB,GAAG,MAAM,IAAI;IAKnD,4CAA4C;IAC5C,aAAa,CAAC,OAAO,EAAE,CAAC,KAAK,EAAE,wBAAwB,KAAK,IAAI,GAAG,MAAM,IAAI;IAK7E,mDAAmD;IACnD,aAAa,CAAC,OAAO,EAAE,OAAO,CAAC,gBAAgB,CAAC,GAAG,IAAI;IAgBvD,4BAA4B;IAC5B,SAAS,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI;IAI/B,OAAO,CAAC,QAAQ;IAqChB,OAAO,CAAC,QAAQ;IAWhB,OAAO,CAAC,WAAW;IAUnB,OAAO,CAAC,iBAAiB;CAWzB;AAED;;;GAGG;AACH,wBAAgB,wBAAwB,CACvC,GAAG,EAAE,MAAM,EACX,OAAO,GAAE,IAAI,CAAC,gBAAgB,EAAE,YAAY,CAAM,GAChD,eAAe,CAKjB;AAED;;;GAGG;AACH,wBAAgB,+BAA+B,CAC9C,WAAW,EAAE,MAAM,EAAE,EACrB,OAAO,GAAE,IAAI,CAAC,gBAAgB,EAAE,mBAAmB,CAAM,GACvD,eAAe,CAKjB;AAED;;;GAGG;AACH,wBAAgB,wBAAwB,CAAC,OAAO,GAAE,gBAAqB,GAAG,eAAe,CAExF;AAED;;;GAGG;AACH,wBAAgB,gBAAgB,IAAI,MAAM,CAEzC;AAED;;GAEG;AACH,wBAAgB,iBAAiB,CAAC,WAAW,EAAE,MAAM,GAAG,MAAM,CAE7D;AAED;;GAEG;AACH,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAExD"}
|
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Jetstream WebSocket client for AT Protocol real-time event streaming.
|
|
3
|
+
*
|
|
4
|
+
* Jetstream is a simplified JSON event stream that converts the CBOR-encoded
|
|
5
|
+
* AT Protocol firehose into lightweight, friendly JSON objects.
|
|
6
|
+
*
|
|
7
|
+
* @see https://github.com/bluesky-social/jetstream
|
|
8
|
+
*/
|
|
9
|
+
/** Public Jetstream instances operated by Bluesky */
|
|
10
|
+
export const JETSTREAM_INSTANCES = {
|
|
11
|
+
'us-east-1': 'wss://jetstream1.us-east.bsky.network',
|
|
12
|
+
'us-east-2': 'wss://jetstream2.us-east.bsky.network',
|
|
13
|
+
'us-west-1': 'wss://jetstream1.us-west.bsky.network',
|
|
14
|
+
'us-west-2': 'wss://jetstream2.us-west.bsky.network',
|
|
15
|
+
};
|
|
16
|
+
/** Default instance to use */
|
|
17
|
+
const DEFAULT_INSTANCE = 'us-west-2';
|
|
18
|
+
/** Reconnection settings */
|
|
19
|
+
const RECONNECT_DELAY_MS = 1000;
|
|
20
|
+
const MAX_RECONNECT_DELAY_MS = 30000;
|
|
21
|
+
const CURSOR_BUFFER_US = 5_000_000; // 5 seconds in microseconds
|
|
22
|
+
/**
|
|
23
|
+
* Client for connecting to AT Protocol Jetstream real-time event stream.
|
|
24
|
+
*
|
|
25
|
+
* @example
|
|
26
|
+
* ```typescript
|
|
27
|
+
* const client = new JetstreamClient({
|
|
28
|
+
* wantedCollections: ['app.bsky.feed.post'],
|
|
29
|
+
* wantedDids: ['did:plc:...'],
|
|
30
|
+
* });
|
|
31
|
+
*
|
|
32
|
+
* client.onEvent((event) => {
|
|
33
|
+
* console.log('Received event:', event.kind, event.commit?.collection);
|
|
34
|
+
* });
|
|
35
|
+
*
|
|
36
|
+
* client.connect();
|
|
37
|
+
* ```
|
|
38
|
+
*/
|
|
39
|
+
export class JetstreamClient {
|
|
40
|
+
ws = null;
|
|
41
|
+
url;
|
|
42
|
+
options;
|
|
43
|
+
eventHandlers = new Set();
|
|
44
|
+
errorHandlers = new Set();
|
|
45
|
+
stateHandlers = new Set();
|
|
46
|
+
reconnectAttempts = 0;
|
|
47
|
+
cursor;
|
|
48
|
+
isConnecting = false;
|
|
49
|
+
shouldReconnect = true;
|
|
50
|
+
reconnectTimeout = null;
|
|
51
|
+
_connectionState = 'disconnected';
|
|
52
|
+
constructor(options = {}) {
|
|
53
|
+
this.url = options.url || JETSTREAM_INSTANCES[DEFAULT_INSTANCE];
|
|
54
|
+
this.options = options;
|
|
55
|
+
this.cursor = options.cursor;
|
|
56
|
+
}
|
|
57
|
+
/** Current connection state */
|
|
58
|
+
get connectionState() {
|
|
59
|
+
return this._connectionState;
|
|
60
|
+
}
|
|
61
|
+
/** Get current cursor (last seen time_us) */
|
|
62
|
+
getCursor() {
|
|
63
|
+
return this.cursor;
|
|
64
|
+
}
|
|
65
|
+
/** Connect to Jetstream and start receiving events */
|
|
66
|
+
connect() {
|
|
67
|
+
if (this.ws?.readyState === WebSocket.OPEN || this.isConnecting) {
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
this.isConnecting = true;
|
|
71
|
+
this.shouldReconnect = true;
|
|
72
|
+
this.setState('connecting');
|
|
73
|
+
const wsUrl = this.buildUrl();
|
|
74
|
+
try {
|
|
75
|
+
this.ws = new WebSocket(wsUrl);
|
|
76
|
+
}
|
|
77
|
+
catch (err) {
|
|
78
|
+
this.isConnecting = false;
|
|
79
|
+
this.handleError(new Error(`Failed to create WebSocket: ${err}`));
|
|
80
|
+
this.scheduleReconnect();
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
this.ws.onopen = () => {
|
|
84
|
+
this.isConnecting = false;
|
|
85
|
+
this.reconnectAttempts = 0;
|
|
86
|
+
this.setState('connected');
|
|
87
|
+
};
|
|
88
|
+
this.ws.onmessage = (event) => {
|
|
89
|
+
try {
|
|
90
|
+
const data = JSON.parse(event.data.toString());
|
|
91
|
+
this.cursor = data.time_us;
|
|
92
|
+
this.eventHandlers.forEach((handler) => {
|
|
93
|
+
try {
|
|
94
|
+
handler(data);
|
|
95
|
+
}
|
|
96
|
+
catch (err) {
|
|
97
|
+
console.error('[Jetstream] Handler error:', err);
|
|
98
|
+
}
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
catch (err) {
|
|
102
|
+
console.error('[Jetstream] Failed to parse message:', err);
|
|
103
|
+
}
|
|
104
|
+
};
|
|
105
|
+
this.ws.onerror = () => {
|
|
106
|
+
this.isConnecting = false;
|
|
107
|
+
this.handleError(new Error('WebSocket error'));
|
|
108
|
+
};
|
|
109
|
+
this.ws.onclose = (event) => {
|
|
110
|
+
this.isConnecting = false;
|
|
111
|
+
if (this.shouldReconnect) {
|
|
112
|
+
this.setState('reconnecting');
|
|
113
|
+
this.scheduleReconnect();
|
|
114
|
+
}
|
|
115
|
+
else {
|
|
116
|
+
this.setState('disconnected');
|
|
117
|
+
}
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
/** Disconnect from Jetstream */
|
|
121
|
+
disconnect() {
|
|
122
|
+
this.shouldReconnect = false;
|
|
123
|
+
if (this.reconnectTimeout) {
|
|
124
|
+
clearTimeout(this.reconnectTimeout);
|
|
125
|
+
this.reconnectTimeout = null;
|
|
126
|
+
}
|
|
127
|
+
if (this.ws) {
|
|
128
|
+
this.ws.close();
|
|
129
|
+
this.ws = null;
|
|
130
|
+
}
|
|
131
|
+
this.setState('disconnected');
|
|
132
|
+
}
|
|
133
|
+
/** Subscribe to events */
|
|
134
|
+
onEvent(handler) {
|
|
135
|
+
this.eventHandlers.add(handler);
|
|
136
|
+
return () => this.eventHandlers.delete(handler);
|
|
137
|
+
}
|
|
138
|
+
/** Subscribe to errors */
|
|
139
|
+
onError(handler) {
|
|
140
|
+
this.errorHandlers.add(handler);
|
|
141
|
+
return () => this.errorHandlers.delete(handler);
|
|
142
|
+
}
|
|
143
|
+
/** Subscribe to connection state changes */
|
|
144
|
+
onStateChange(handler) {
|
|
145
|
+
this.stateHandlers.add(handler);
|
|
146
|
+
return () => this.stateHandlers.delete(handler);
|
|
147
|
+
}
|
|
148
|
+
/** Update subscription filters after connecting */
|
|
149
|
+
updateFilters(options) {
|
|
150
|
+
this.options = { ...this.options, ...options };
|
|
151
|
+
if (this.ws?.readyState === WebSocket.OPEN) {
|
|
152
|
+
const message = {
|
|
153
|
+
type: 'options_update',
|
|
154
|
+
payload: {
|
|
155
|
+
wantedCollections: this.options.wantedCollections ?? [],
|
|
156
|
+
wantedDids: this.options.wantedDids ?? [],
|
|
157
|
+
maxMessageSizeBytes: this.options.maxMessageSizeBytes ?? 0,
|
|
158
|
+
},
|
|
159
|
+
};
|
|
160
|
+
this.ws.send(JSON.stringify(message));
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
/** Set cursor for replay */
|
|
164
|
+
setCursor(cursor) {
|
|
165
|
+
this.cursor = cursor;
|
|
166
|
+
}
|
|
167
|
+
buildUrl() {
|
|
168
|
+
const params = new URLSearchParams();
|
|
169
|
+
if (this.options.wantedCollections?.length) {
|
|
170
|
+
for (const collection of this.options.wantedCollections) {
|
|
171
|
+
params.append('wantedCollections', collection);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
if (this.options.wantedDids?.length) {
|
|
175
|
+
for (const did of this.options.wantedDids) {
|
|
176
|
+
params.append('wantedDids', did);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
if (this.options.maxMessageSizeBytes) {
|
|
180
|
+
params.set('maxMessageSizeBytes', String(this.options.maxMessageSizeBytes));
|
|
181
|
+
}
|
|
182
|
+
if (this.options.compress) {
|
|
183
|
+
params.set('compress', 'true');
|
|
184
|
+
}
|
|
185
|
+
if (this.options.requireHello) {
|
|
186
|
+
params.set('requireHello', 'true');
|
|
187
|
+
}
|
|
188
|
+
// Use cursor with buffer for gapless replay
|
|
189
|
+
if (this.cursor) {
|
|
190
|
+
const bufferedCursor = Math.max(0, this.cursor - CURSOR_BUFFER_US);
|
|
191
|
+
params.set('cursor', String(bufferedCursor));
|
|
192
|
+
}
|
|
193
|
+
const queryString = params.toString();
|
|
194
|
+
return queryString ? `${this.url}/subscribe?${queryString}` : `${this.url}/subscribe`;
|
|
195
|
+
}
|
|
196
|
+
setState(state) {
|
|
197
|
+
this._connectionState = state;
|
|
198
|
+
this.stateHandlers.forEach((handler) => {
|
|
199
|
+
try {
|
|
200
|
+
handler(state);
|
|
201
|
+
}
|
|
202
|
+
catch (err) {
|
|
203
|
+
console.error('[Jetstream] State handler error:', err);
|
|
204
|
+
}
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
handleError(error) {
|
|
208
|
+
this.errorHandlers.forEach((handler) => {
|
|
209
|
+
try {
|
|
210
|
+
handler(error);
|
|
211
|
+
}
|
|
212
|
+
catch (err) {
|
|
213
|
+
console.error('[Jetstream] Error handler error:', err);
|
|
214
|
+
}
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
scheduleReconnect() {
|
|
218
|
+
const delay = Math.min(RECONNECT_DELAY_MS * Math.pow(2, this.reconnectAttempts), MAX_RECONNECT_DELAY_MS);
|
|
219
|
+
this.reconnectAttempts++;
|
|
220
|
+
this.reconnectTimeout = setTimeout(() => {
|
|
221
|
+
this.connect();
|
|
222
|
+
}, delay);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
/**
|
|
226
|
+
* Create a Jetstream client for a specific DID's data.
|
|
227
|
+
* Filters events to only include records from the specified DID.
|
|
228
|
+
*/
|
|
229
|
+
export function createDidJetstreamClient(did, options = {}) {
|
|
230
|
+
return new JetstreamClient({
|
|
231
|
+
...options,
|
|
232
|
+
wantedDids: [did],
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
/**
|
|
236
|
+
* Create a Jetstream client for specific collections.
|
|
237
|
+
* Receives events for all DIDs but only for the specified collections.
|
|
238
|
+
*/
|
|
239
|
+
export function createCollectionJetstreamClient(collections, options = {}) {
|
|
240
|
+
return new JetstreamClient({
|
|
241
|
+
...options,
|
|
242
|
+
wantedCollections: collections,
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
/**
|
|
246
|
+
* Create a Jetstream client for the full firehose (all collections, all DIDs).
|
|
247
|
+
* Use with caution - this receives a high volume of events.
|
|
248
|
+
*/
|
|
249
|
+
export function createFullFirehoseClient(options = {}) {
|
|
250
|
+
return new JetstreamClient(options);
|
|
251
|
+
}
|
|
252
|
+
/**
|
|
253
|
+
* Get the current time as a Unix microseconds cursor.
|
|
254
|
+
* Useful for starting live-tail mode.
|
|
255
|
+
*/
|
|
256
|
+
export function getCurrentCursor() {
|
|
257
|
+
return Date.now() * 1000;
|
|
258
|
+
}
|
|
259
|
+
/**
|
|
260
|
+
* Convert a Unix timestamp (ms) to Jetstream cursor (microseconds).
|
|
261
|
+
*/
|
|
262
|
+
export function timestampToCursor(timestampMs) {
|
|
263
|
+
return timestampMs * 1000;
|
|
264
|
+
}
|
|
265
|
+
/**
|
|
266
|
+
* Convert a Jetstream cursor (microseconds) to Unix timestamp (ms).
|
|
267
|
+
*/
|
|
268
|
+
export function cursorToTimestamp(cursor) {
|
|
269
|
+
return Math.floor(cursor / 1000);
|
|
270
|
+
}
|
package/dist/musicbrainz.d.ts
CHANGED
|
@@ -5,6 +5,7 @@ export declare function searchDeezerArtwork(trackName: string, artistName: strin
|
|
|
5
5
|
export declare function searchLastFmArtwork(trackName: string, artistName: string, releaseName?: string): Promise<string | null>;
|
|
6
6
|
/**
|
|
7
7
|
* Cascading artwork search: Cover Art Archive → MusicBrainz+CAA → iTunes → Last.fm
|
|
8
|
+
* Each step is bounded by ARTWORK_TIMEOUT to prevent serverless function timeouts.
|
|
8
9
|
*/
|
|
9
10
|
export declare function findArtwork(trackName: string, artistName: string, releaseName?: string, releaseMbId?: string, fetchFn?: typeof fetch): Promise<string | null>;
|
|
10
11
|
//# sourceMappingURL=musicbrainz.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"musicbrainz.d.ts","sourceRoot":"","sources":["../src/musicbrainz.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"musicbrainz.d.ts","sourceRoot":"","sources":["../src/musicbrainz.ts"],"names":[],"mappings":"AAkCA,wBAAsB,wBAAwB,CAC7C,SAAS,EAAE,MAAM,EACjB,UAAU,EAAE,MAAM,EAClB,WAAW,CAAC,EAAE,MAAM,GAClB,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAiBxB;AAkCD,wBAAgB,gBAAgB,CAAC,WAAW,EAAE,MAAM,EAAE,IAAI,GAAE,GAAG,GAAG,GAAG,GAAG,IAAU,GAAG,MAAM,CAE1F;AAED,wBAAsB,mBAAmB,CACxC,SAAS,EAAE,MAAM,EACjB,UAAU,EAAE,MAAM,EAClB,WAAW,CAAC,EAAE,MAAM,GAClB,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAuBxB;AAED,wBAAsB,mBAAmB,CACxC,SAAS,EAAE,MAAM,EACjB,UAAU,EAAE,MAAM,EAClB,WAAW,CAAC,EAAE,MAAM,GAClB,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAGxB;AAED,wBAAsB,mBAAmB,CACxC,SAAS,EAAE,MAAM,EACjB,UAAU,EAAE,MAAM,EAClB,WAAW,CAAC,EAAE,MAAM,GAClB,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CA2BxB;AAED;;;GAGG;AACH,wBAAsB,WAAW,CAChC,SAAS,EAAE,MAAM,EACjB,UAAU,EAAE,MAAM,EAClB,WAAW,CAAC,EAAE,MAAM,EACpB,WAAW,CAAC,EAAE,MAAM,EACpB,OAAO,CAAC,EAAE,OAAO,KAAK,GACpB,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CA+BxB"}
|
package/dist/musicbrainz.js
CHANGED
|
@@ -1,4 +1,16 @@
|
|
|
1
1
|
import { cache } from './cache.js';
|
|
2
|
+
/** Timeout for individual artwork API calls (ms). */
|
|
3
|
+
const ARTWORK_TIMEOUT = 5_000;
|
|
4
|
+
/**
|
|
5
|
+
* Wraps a promise with a timeout. Rejects with a TimeoutError if the promise
|
|
6
|
+
* doesn't settle within `ms` milliseconds.
|
|
7
|
+
*/
|
|
8
|
+
function withTimeout(promise, ms) {
|
|
9
|
+
return new Promise((resolve, reject) => {
|
|
10
|
+
const timer = setTimeout(() => reject(new Error(`Timeout after ${ms}ms`)), ms);
|
|
11
|
+
promise.then((value) => { clearTimeout(timer); resolve(value); }, (err) => { clearTimeout(timer); reject(err); });
|
|
12
|
+
});
|
|
13
|
+
}
|
|
2
14
|
export async function searchMusicBrainzRelease(trackName, artistName, releaseName) {
|
|
3
15
|
const cacheKey = `mb:release:${trackName}:${artistName}:${releaseName || 'none'}`;
|
|
4
16
|
const cached = cache.get(cacheKey);
|
|
@@ -140,6 +152,7 @@ export async function searchLastFmArtwork(trackName, artistName, releaseName) {
|
|
|
140
152
|
}
|
|
141
153
|
/**
|
|
142
154
|
* Cascading artwork search: Cover Art Archive → MusicBrainz+CAA → iTunes → Last.fm
|
|
155
|
+
* Each step is bounded by ARTWORK_TIMEOUT to prevent serverless function timeouts.
|
|
143
156
|
*/
|
|
144
157
|
export async function findArtwork(trackName, artistName, releaseName, releaseMbId, fetchFn) {
|
|
145
158
|
const _fetch = fetchFn || globalThis.fetch;
|
|
@@ -147,7 +160,7 @@ export async function findArtwork(trackName, artistName, releaseName, releaseMbI
|
|
|
147
160
|
if (releaseMbId) {
|
|
148
161
|
const caaUrl = buildCoverArtUrl(releaseMbId, 500);
|
|
149
162
|
try {
|
|
150
|
-
const res = await _fetch(caaUrl, { method: 'HEAD' });
|
|
163
|
+
const res = await withTimeout(_fetch(caaUrl, { method: 'HEAD' }), ARTWORK_TIMEOUT);
|
|
151
164
|
if (res.ok)
|
|
152
165
|
return caaUrl;
|
|
153
166
|
}
|
|
@@ -158,7 +171,7 @@ export async function findArtwork(trackName, artistName, releaseName, releaseMbI
|
|
|
158
171
|
if (mbId) {
|
|
159
172
|
const caaUrl = buildCoverArtUrl(mbId, 500);
|
|
160
173
|
try {
|
|
161
|
-
const res = await _fetch(caaUrl, { method: 'HEAD' });
|
|
174
|
+
const res = await withTimeout(_fetch(caaUrl, { method: 'HEAD' }), ARTWORK_TIMEOUT);
|
|
162
175
|
if (res.ok)
|
|
163
176
|
return caaUrl;
|
|
164
177
|
}
|
package/package.json
CHANGED
package/src/agents.ts
CHANGED
|
@@ -2,6 +2,26 @@ import { AtpAgent } from '@atproto/api';
|
|
|
2
2
|
import type { ResolvedIdentity } from './types.js';
|
|
3
3
|
import { cache } from './cache.js';
|
|
4
4
|
|
|
5
|
+
/** Default timeout for individual AT Protocol XRPC calls (ms). */
|
|
6
|
+
const XRPC_TIMEOUT = 8_000;
|
|
7
|
+
|
|
8
|
+
/** Default timeout for identity resolution (ms). */
|
|
9
|
+
const IDENTITY_TIMEOUT = 5_000;
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Wraps a promise with a timeout. Rejects with a TimeoutError if the promise
|
|
13
|
+
* doesn't settle within `ms` milliseconds.
|
|
14
|
+
*/
|
|
15
|
+
function withTimeout<T>(promise: Promise<T>, ms: number): Promise<T> {
|
|
16
|
+
return new Promise<T>((resolve, reject) => {
|
|
17
|
+
const timer = setTimeout(() => reject(new Error(`Timeout after ${ms}ms`)), ms);
|
|
18
|
+
promise.then(
|
|
19
|
+
(value) => { clearTimeout(timer); resolve(value); },
|
|
20
|
+
(err) => { clearTimeout(timer); reject(err); }
|
|
21
|
+
);
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
|
|
5
25
|
export function createAgent(service: string, fetchFn?: typeof fetch): AtpAgent {
|
|
6
26
|
const wrappedFetch = fetchFn
|
|
7
27
|
? async (url: URL | RequestInfo, init?: RequestInit) => {
|
|
@@ -40,8 +60,11 @@ export async function resolveIdentity(
|
|
|
40
60
|
if (cached) return cached;
|
|
41
61
|
|
|
42
62
|
const _fetch = fetchFn ?? globalThis.fetch;
|
|
43
|
-
const response = await
|
|
44
|
-
|
|
63
|
+
const response = await withTimeout(
|
|
64
|
+
_fetch(
|
|
65
|
+
`https://slingshot.microcosm.blue/xrpc/com.bad-example.identity.resolveMiniDoc?identifier=${encodeURIComponent(did)}`
|
|
66
|
+
),
|
|
67
|
+
IDENTITY_TIMEOUT
|
|
45
68
|
);
|
|
46
69
|
|
|
47
70
|
if (!response.ok) {
|
|
@@ -71,7 +94,10 @@ export async function getPublicAgent(did: string, fetchFn?: typeof fetch): Promi
|
|
|
71
94
|
|
|
72
95
|
try {
|
|
73
96
|
try {
|
|
74
|
-
const response = await
|
|
97
|
+
const response = await withTimeout(
|
|
98
|
+
constellationAgent.getProfile({ actor: did }),
|
|
99
|
+
XRPC_TIMEOUT
|
|
100
|
+
);
|
|
75
101
|
if (response.success) {
|
|
76
102
|
resolvedAgent = constellationAgent;
|
|
77
103
|
return resolvedAgent;
|
|
@@ -113,7 +139,7 @@ export async function withFallback<T>(
|
|
|
113
139
|
for (const getAgent of agents) {
|
|
114
140
|
try {
|
|
115
141
|
const agent = await getAgent();
|
|
116
|
-
return await operation(agent);
|
|
142
|
+
return await withTimeout(operation(agent), XRPC_TIMEOUT);
|
|
117
143
|
} catch (error) {
|
|
118
144
|
lastError = error;
|
|
119
145
|
}
|
package/src/musicbrainz.ts
CHANGED
|
@@ -1,5 +1,22 @@
|
|
|
1
1
|
import { cache } from './cache.js';
|
|
2
2
|
|
|
3
|
+
/** Timeout for individual artwork API calls (ms). */
|
|
4
|
+
const ARTWORK_TIMEOUT = 5_000;
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Wraps a promise with a timeout. Rejects with a TimeoutError if the promise
|
|
8
|
+
* doesn't settle within `ms` milliseconds.
|
|
9
|
+
*/
|
|
10
|
+
function withTimeout<T>(promise: Promise<T>, ms: number): Promise<T> {
|
|
11
|
+
return new Promise<T>((resolve, reject) => {
|
|
12
|
+
const timer = setTimeout(() => reject(new Error(`Timeout after ${ms}ms`)), ms);
|
|
13
|
+
promise.then(
|
|
14
|
+
(value) => { clearTimeout(timer); resolve(value); },
|
|
15
|
+
(err) => { clearTimeout(timer); reject(err); }
|
|
16
|
+
);
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
|
|
3
20
|
interface MusicBrainzRelease {
|
|
4
21
|
id: string;
|
|
5
22
|
score: number;
|
|
@@ -147,6 +164,7 @@ export async function searchLastFmArtwork(
|
|
|
147
164
|
|
|
148
165
|
/**
|
|
149
166
|
* Cascading artwork search: Cover Art Archive → MusicBrainz+CAA → iTunes → Last.fm
|
|
167
|
+
* Each step is bounded by ARTWORK_TIMEOUT to prevent serverless function timeouts.
|
|
150
168
|
*/
|
|
151
169
|
export async function findArtwork(
|
|
152
170
|
trackName: string,
|
|
@@ -161,7 +179,7 @@ export async function findArtwork(
|
|
|
161
179
|
if (releaseMbId) {
|
|
162
180
|
const caaUrl = buildCoverArtUrl(releaseMbId, 500);
|
|
163
181
|
try {
|
|
164
|
-
const res = await _fetch(caaUrl, { method: 'HEAD' });
|
|
182
|
+
const res = await withTimeout(_fetch(caaUrl, { method: 'HEAD' }), ARTWORK_TIMEOUT);
|
|
165
183
|
if (res.ok) return caaUrl;
|
|
166
184
|
} catch { /* continue */ }
|
|
167
185
|
}
|
|
@@ -171,7 +189,7 @@ export async function findArtwork(
|
|
|
171
189
|
if (mbId) {
|
|
172
190
|
const caaUrl = buildCoverArtUrl(mbId, 500);
|
|
173
191
|
try {
|
|
174
|
-
const res = await _fetch(caaUrl, { method: 'HEAD' });
|
|
192
|
+
const res = await withTimeout(_fetch(caaUrl, { method: 'HEAD' }), ARTWORK_TIMEOUT);
|
|
175
193
|
if (res.ok) return caaUrl;
|
|
176
194
|
} catch { /* continue */ }
|
|
177
195
|
}
|