@alteran/astro 0.3.8 → 0.3.9
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/package.json
CHANGED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { APIContext } from 'astro';
|
|
2
|
+
import { proxyAppView } from '../../lib/appview';
|
|
3
|
+
import { isAuthorized, unauthorized } from '../../lib/auth';
|
|
4
|
+
|
|
5
|
+
export const prerender = false;
|
|
6
|
+
|
|
7
|
+
// Implements: app.bsky.feed.getSuggestedFeeds (proxy-only)
|
|
8
|
+
export async function GET({ locals, request }: APIContext) {
|
|
9
|
+
const { env } = locals.runtime;
|
|
10
|
+
if (!(await isAuthorized(request, env))) return unauthorized();
|
|
11
|
+
|
|
12
|
+
return proxyAppView({
|
|
13
|
+
request,
|
|
14
|
+
env,
|
|
15
|
+
lxm: 'app.bsky.feed.getSuggestedFeeds',
|
|
16
|
+
fallback: async () => {
|
|
17
|
+
return new Response(JSON.stringify({ feeds: [] }), {
|
|
18
|
+
headers: { 'Content-Type': 'application/json' },
|
|
19
|
+
});
|
|
20
|
+
},
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { APIContext } from 'astro';
|
|
2
|
+
import { proxyAppView } from '../../lib/appview';
|
|
3
|
+
import { isAuthorized, unauthorized } from '../../lib/auth';
|
|
4
|
+
|
|
5
|
+
export const prerender = false;
|
|
6
|
+
|
|
7
|
+
// Implements: app.bsky.unspecced.getSuggestedFeeds (proxy-only)
|
|
8
|
+
export async function GET({ locals, request }: APIContext) {
|
|
9
|
+
const { env } = locals.runtime;
|
|
10
|
+
if (!(await isAuthorized(request, env))) return unauthorized();
|
|
11
|
+
|
|
12
|
+
return proxyAppView({
|
|
13
|
+
request,
|
|
14
|
+
env,
|
|
15
|
+
lxm: 'app.bsky.unspecced.getSuggestedFeeds',
|
|
16
|
+
fallback: async () => {
|
|
17
|
+
return new Response(JSON.stringify({ feeds: [] }), {
|
|
18
|
+
headers: { 'Content-Type': 'application/json' },
|
|
19
|
+
});
|
|
20
|
+
},
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
|
package/src/worker/sequencer.ts
CHANGED
|
@@ -238,9 +238,6 @@ export class Sequencer {
|
|
|
238
238
|
const pair = new WebSocketPair();
|
|
239
239
|
const [client, server] = Object.values(pair);
|
|
240
240
|
const id = crypto.randomUUID();
|
|
241
|
-
const ws = server as unknown as WebSocket;
|
|
242
|
-
|
|
243
|
-
ws.accept();
|
|
244
241
|
|
|
245
242
|
// Parse cursor parameter
|
|
246
243
|
const cursorParam = url.searchParams.get('cursor');
|
|
@@ -250,57 +247,30 @@ export class Sequencer {
|
|
|
250
247
|
if (cursor > this.nextSeq - 1) {
|
|
251
248
|
// Future cursor error
|
|
252
249
|
const err = checkCursor(cursor, this.nextSeq - 1) ?? createErrorFrame('FutureCursor', 'Cursor is ahead of current sequence').toFramedBytes();
|
|
253
|
-
|
|
254
|
-
|
|
250
|
+
server.send(err);
|
|
251
|
+
server.close(1008, 'FutureCursor');
|
|
255
252
|
return new Response(null, { status: 101, webSocket: client });
|
|
256
253
|
}
|
|
257
254
|
|
|
258
|
-
|
|
255
|
+
// CRITICAL: Use Cloudflare's hibernatable WebSocket API
|
|
256
|
+
// This keeps the connection alive even after the response is returned
|
|
257
|
+
// Without this, the WebSocket closes immediately when the worker context ends
|
|
258
|
+
this.state.acceptWebSocket(server as any, [id, cursor.toString()]);
|
|
259
|
+
|
|
260
|
+
const clientObj: Client = { webSocket: server as unknown as WebSocket, id, cursor };
|
|
259
261
|
this.clients.set(id, clientObj);
|
|
260
262
|
|
|
261
263
|
// Send #info frame on connection
|
|
262
264
|
const infoFrame = createInfoFrame('com.atproto.sync.subscribeRepos', 'Connected to PDS firehose');
|
|
263
265
|
try {
|
|
264
|
-
|
|
266
|
+
server.send(infoFrame.toFramedBytes());
|
|
265
267
|
} catch (error) {
|
|
266
268
|
console.error('Failed to send info frame:', error);
|
|
267
269
|
}
|
|
268
270
|
|
|
269
|
-
// Keep the connection alive to avoid intermediary idle timeouts (e.g., CF edge)
|
|
270
|
-
// Send a lightweight #info heartbeat every ~25s. Most clients ignore unknown #info
|
|
271
|
-
// messages; this is safe and keeps the socket active.
|
|
272
|
-
const keepalive = setInterval(() => {
|
|
273
|
-
try {
|
|
274
|
-
const ka = createInfoFrame('keepalive', 'ping');
|
|
275
|
-
ws.send(ka.toFramedBytes());
|
|
276
|
-
} catch {}
|
|
277
|
-
}, 25_000);
|
|
278
|
-
|
|
279
|
-
// Set up event handlers
|
|
280
|
-
ws.addEventListener('message', (evt) => {
|
|
281
|
-
try {
|
|
282
|
-
const data = typeof evt.data === 'string' ? evt.data : '';
|
|
283
|
-
if (data === 'ping') {
|
|
284
|
-
ws.send('pong');
|
|
285
|
-
}
|
|
286
|
-
} catch (error) {
|
|
287
|
-
console.error('WebSocket message error:', error);
|
|
288
|
-
}
|
|
289
|
-
});
|
|
290
|
-
|
|
291
|
-
ws.addEventListener('close', () => {
|
|
292
|
-
this.clients.delete(id);
|
|
293
|
-
clearInterval(keepalive);
|
|
294
|
-
});
|
|
295
|
-
|
|
296
|
-
ws.addEventListener('error', () => {
|
|
297
|
-
this.clients.delete(id);
|
|
298
|
-
clearInterval(keepalive);
|
|
299
|
-
});
|
|
300
|
-
|
|
301
271
|
// Replay buffered events if cursor provided
|
|
302
272
|
if (cursor > 0) {
|
|
303
|
-
await this.replayFromCursor(
|
|
273
|
+
await this.replayFromCursor(server as unknown as WebSocket, cursor);
|
|
304
274
|
}
|
|
305
275
|
|
|
306
276
|
return new Response(null, { status: 101, webSocket: client });
|
|
@@ -572,4 +542,52 @@ export class Sequencer {
|
|
|
572
542
|
droppedFrames: this.droppedFrameCount,
|
|
573
543
|
};
|
|
574
544
|
}
|
|
545
|
+
|
|
546
|
+
/**
|
|
547
|
+
* WebSocket hibernation handler: called when a message is received
|
|
548
|
+
* This is required for Cloudflare's hibernatable WebSocket API
|
|
549
|
+
*/
|
|
550
|
+
async webSocketMessage(ws: WebSocket, message: string | ArrayBuffer): Promise<void> {
|
|
551
|
+
// Find client by WebSocket instance
|
|
552
|
+
const client = Array.from(this.clients.values()).find((c) => c.webSocket === ws);
|
|
553
|
+
if (!client) {
|
|
554
|
+
console.warn('Received message from unknown WebSocket');
|
|
555
|
+
return;
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
try {
|
|
559
|
+
const data = typeof message === 'string' ? message : new TextDecoder().decode(message);
|
|
560
|
+
if (data === 'ping') {
|
|
561
|
+
ws.send('pong');
|
|
562
|
+
}
|
|
563
|
+
} catch (error) {
|
|
564
|
+
console.error('WebSocket message error:', error);
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
/**
|
|
569
|
+
* WebSocket hibernation handler: called when connection closes
|
|
570
|
+
* This is required for Cloudflare's hibernatable WebSocket API
|
|
571
|
+
*/
|
|
572
|
+
async webSocketClose(ws: WebSocket, code: number, reason: string, wasClean: boolean): Promise<void> {
|
|
573
|
+
// Find and remove client
|
|
574
|
+
const entry = Array.from(this.clients.entries()).find(([_, c]) => c.webSocket === ws);
|
|
575
|
+
if (entry) {
|
|
576
|
+
this.clients.delete(entry[0]);
|
|
577
|
+
console.log(`Client ${entry[0]} disconnected: code=${code} reason="${reason}" clean=${wasClean}`);
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
/**
|
|
582
|
+
* WebSocket hibernation handler: called when an error occurs
|
|
583
|
+
* This is required for Cloudflare's hibernatable WebSocket API
|
|
584
|
+
*/
|
|
585
|
+
async webSocketError(ws: WebSocket, error: unknown): Promise<void> {
|
|
586
|
+
// Find and remove client
|
|
587
|
+
const entry = Array.from(this.clients.entries()).find(([_, c]) => c.webSocket === ws);
|
|
588
|
+
if (entry) {
|
|
589
|
+
this.clients.delete(entry[0]);
|
|
590
|
+
console.error(`Client ${entry[0]} error:`, error);
|
|
591
|
+
}
|
|
592
|
+
}
|
|
575
593
|
}
|