@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.
@@ -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;AAGnD,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,CA8B3B;AAED,wBAAsB,cAAc,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,OAAO,KAAK,GAAG,OAAO,CAAC,QAAQ,CAAC,CAqB3F;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"}
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
+ }
@@ -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":"AAiBA,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;;GAEG;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"}
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"}
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ewanc26/atproto",
3
- "version": "0.2.10",
3
+ "version": "0.2.11",
4
4
  "description": "AT Protocol service layer extracted from ewancroft.uk",
5
5
  "author": "Ewan Croft",
6
6
  "license": "AGPL-3.0-only",
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 _fetch(
44
- `https://slingshot.microcosm.blue/xrpc/com.bad-example.identity.resolveMiniDoc?identifier=${encodeURIComponent(did)}`
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 constellationAgent.getProfile({ actor: did });
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
  }
@@ -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
  }