@alteran/astro 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.
Files changed (90) hide show
  1. package/README.md +558 -0
  2. package/index.d.ts +12 -0
  3. package/index.js +129 -0
  4. package/package.json +75 -0
  5. package/src/_worker.ts +44 -0
  6. package/src/app.ts +10 -0
  7. package/src/db/client.ts +7 -0
  8. package/src/db/dal.ts +97 -0
  9. package/src/db/repo.ts +135 -0
  10. package/src/db/schema.ts +89 -0
  11. package/src/db/seed.ts +14 -0
  12. package/src/env.d.ts +4 -0
  13. package/src/handlers/debug.ts +34 -0
  14. package/src/handlers/health.ts +6 -0
  15. package/src/handlers/ready.ts +14 -0
  16. package/src/handlers/root.ts +5 -0
  17. package/src/handlers/wellknown.ts +7 -0
  18. package/src/handlers/xrpc.repo.core.ts +57 -0
  19. package/src/handlers/xrpc.server.createSession.ts +25 -0
  20. package/src/handlers/xrpc.server.refreshSession.ts +43 -0
  21. package/src/lib/auth.ts +20 -0
  22. package/src/lib/blockstore-gc.ts +197 -0
  23. package/src/lib/cache.ts +236 -0
  24. package/src/lib/car-reader.ts +157 -0
  25. package/src/lib/commit-log-pruning.ts +76 -0
  26. package/src/lib/commit.ts +162 -0
  27. package/src/lib/config.ts +208 -0
  28. package/src/lib/errors.ts +142 -0
  29. package/src/lib/firehose/frames.ts +229 -0
  30. package/src/lib/firehose/parse.ts +82 -0
  31. package/src/lib/firehose/validation.ts +9 -0
  32. package/src/lib/handle.ts +90 -0
  33. package/src/lib/jwt.ts +150 -0
  34. package/src/lib/logger.ts +73 -0
  35. package/src/lib/metrics.ts +194 -0
  36. package/src/lib/mst/blockstore.ts +105 -0
  37. package/src/lib/mst/index.ts +3 -0
  38. package/src/lib/mst/mst.ts +643 -0
  39. package/src/lib/mst/util.ts +86 -0
  40. package/src/lib/ratelimit.ts +34 -0
  41. package/src/lib/sequencer.ts +10 -0
  42. package/src/lib/streaming-car.ts +137 -0
  43. package/src/lib/token-cleanup.ts +38 -0
  44. package/src/lib/tracing.ts +136 -0
  45. package/src/lib/util.ts +55 -0
  46. package/src/middleware.ts +102 -0
  47. package/src/pages/.well-known/atproto-did.ts +7 -0
  48. package/src/pages/.well-known/did.json.ts +76 -0
  49. package/src/pages/debug/blob/[...key].ts +27 -0
  50. package/src/pages/debug/db/bootstrap.ts +23 -0
  51. package/src/pages/debug/db/commits.ts +20 -0
  52. package/src/pages/debug/gc/blobs.ts +16 -0
  53. package/src/pages/debug/record.ts +33 -0
  54. package/src/pages/health.ts +68 -0
  55. package/src/pages/index.astro +57 -0
  56. package/src/pages/index.ts +2 -0
  57. package/src/pages/ready.ts +16 -0
  58. package/src/pages/xrpc/com.atproto.identity.resolveHandle.ts +38 -0
  59. package/src/pages/xrpc/com.atproto.identity.updateHandle.ts +45 -0
  60. package/src/pages/xrpc/com.atproto.repo.applyWrites.ts +73 -0
  61. package/src/pages/xrpc/com.atproto.repo.createRecord.ts +36 -0
  62. package/src/pages/xrpc/com.atproto.repo.deleteRecord.ts +36 -0
  63. package/src/pages/xrpc/com.atproto.repo.describeRepo.ts +51 -0
  64. package/src/pages/xrpc/com.atproto.repo.getRecord.ts +25 -0
  65. package/src/pages/xrpc/com.atproto.repo.listRecords.ts +57 -0
  66. package/src/pages/xrpc/com.atproto.repo.putRecord.ts +36 -0
  67. package/src/pages/xrpc/com.atproto.repo.uploadBlob.ts +53 -0
  68. package/src/pages/xrpc/com.atproto.server.createSession.ts +92 -0
  69. package/src/pages/xrpc/com.atproto.server.deleteSession.ts +25 -0
  70. package/src/pages/xrpc/com.atproto.server.describeServer.ts +17 -0
  71. package/src/pages/xrpc/com.atproto.server.getSession.ts +46 -0
  72. package/src/pages/xrpc/com.atproto.server.refreshSession.ts +67 -0
  73. package/src/pages/xrpc/com.atproto.sync.getBlocks.json.ts +16 -0
  74. package/src/pages/xrpc/com.atproto.sync.getBlocks.ts +56 -0
  75. package/src/pages/xrpc/com.atproto.sync.getCheckout.json.ts +20 -0
  76. package/src/pages/xrpc/com.atproto.sync.getCheckout.ts +43 -0
  77. package/src/pages/xrpc/com.atproto.sync.getHead.ts +11 -0
  78. package/src/pages/xrpc/com.atproto.sync.getLatestCommit.ts +42 -0
  79. package/src/pages/xrpc/com.atproto.sync.getRecord.ts +63 -0
  80. package/src/pages/xrpc/com.atproto.sync.getRepo.json.ts +20 -0
  81. package/src/pages/xrpc/com.atproto.sync.getRepo.range.ts +34 -0
  82. package/src/pages/xrpc/com.atproto.sync.getRepo.ts +17 -0
  83. package/src/pages/xrpc/com.atproto.sync.listBlobs.ts +53 -0
  84. package/src/pages/xrpc/com.atproto.sync.listRepos.ts +31 -0
  85. package/src/services/car.ts +249 -0
  86. package/src/services/r2-blob-store.ts +87 -0
  87. package/src/services/repo-manager.ts +339 -0
  88. package/src/shims/astro-internal-handler.d.ts +4 -0
  89. package/src/worker/sequencer.ts +563 -0
  90. package/types/env.d.ts +48 -0
@@ -0,0 +1,563 @@
1
+ // Types via tsconfig
2
+
3
+ import type { DurableObjectState, D1Database } from '@cloudflare/workers-types';
4
+ import { drizzle } from 'drizzle-orm/d1';
5
+ import { commit_log } from '../db/schema';
6
+ import { gt, eq, desc } from 'drizzle-orm';
7
+ import {
8
+ createInfoFrame,
9
+ createCommitFrame,
10
+ createIdentityFrame,
11
+ createAccountFrame,
12
+ createErrorFrame,
13
+ type CommitMessage,
14
+ type RepoOp,
15
+ } from '../lib/firehose/frames';
16
+ import { checkCursor } from '../lib/firehose/validation';
17
+ import { CID } from 'multiformats/cid';
18
+ import { encodeBlocksForCommit } from '../services/car';
19
+ import type { Env } from '../env';
20
+
21
+ interface Client {
22
+ webSocket: WebSocket;
23
+ id: string;
24
+ cursor: number;
25
+ }
26
+
27
+ interface CommitEvent {
28
+ seq: number;
29
+ did: string;
30
+ commitCid: string;
31
+ rev: string;
32
+ data: string; // JSON-encoded commit data
33
+ sig: string; // base64 signature
34
+ ts: number;
35
+ ops?: RepoOp[];
36
+ blocks?: Uint8Array;
37
+ }
38
+
39
+ interface IdentityEvent {
40
+ seq: number;
41
+ did: string;
42
+ handle?: string;
43
+ ts: number;
44
+ }
45
+
46
+ interface AccountEvent {
47
+ seq: number;
48
+ did: string;
49
+ active: boolean;
50
+ status?: string;
51
+ ts: number;
52
+ }
53
+
54
+ type SequencerEvent = CommitEvent | IdentityEvent | AccountEvent;
55
+
56
+ /**
57
+ * Sequencer Durable Object
58
+ * Manages the firehose event stream for repository updates
59
+ */
60
+ export class Sequencer {
61
+ private readonly state: DurableObjectState;
62
+ private readonly env: Env & { PDS_SEQ_WINDOW?: string };
63
+ private readonly clients = new Map<string, Client>();
64
+ private buffer: CommitEvent[] = [];
65
+ private readonly db: D1Database;
66
+ private maxWindow: number;
67
+ private nextSeq = 1;
68
+ private droppedFrameCount = 0;
69
+
70
+ constructor(state: DurableObjectState, env: Env & { PDS_SEQ_WINDOW?: string }) {
71
+ this.state = state;
72
+ this.env = env;
73
+ this.db = env.DB;
74
+ this.maxWindow = parseInt(env.PDS_SEQ_WINDOW || '512', 10);
75
+
76
+ // Initialize from storage
77
+ this.state.blockConcurrencyWhile(async () => {
78
+ const stored = await this.state.storage.get<number>('nextSeq');
79
+ if (stored) {
80
+ this.nextSeq = stored;
81
+ }
82
+ });
83
+ }
84
+
85
+ async fetch(request: Request): Promise<Response> {
86
+ const url = new URL(request.url);
87
+
88
+ // Handle event notifications from PDS
89
+ if (request.method === 'POST') {
90
+ if (url.pathname === '/commit') {
91
+ return this.handleCommitNotification(request);
92
+ } else if (url.pathname === '/identity') {
93
+ return this.handleIdentityNotification(request);
94
+ } else if (url.pathname === '/account') {
95
+ return this.handleAccountNotification(request);
96
+ }
97
+ }
98
+
99
+ // Handle WebSocket upgrade for firehose subscription
100
+ const upgradeHeader = request.headers.get('Upgrade');
101
+ if (upgradeHeader !== 'websocket') {
102
+ return new Response('Expected websocket', { status: 426 });
103
+ }
104
+
105
+ return this.handleWebSocketUpgrade(request, url);
106
+ }
107
+
108
+ /**
109
+ * Handle commit notification from PDS
110
+ */
111
+ private async handleCommitNotification(request: Request): Promise<Response> {
112
+ try {
113
+ const body = (await request.json()) as {
114
+ did: string;
115
+ commitCid: string;
116
+ rev: string;
117
+ data: string;
118
+ sig: string;
119
+ ops?: RepoOp[];
120
+ blocks?: string; // base64-encoded CAR
121
+ };
122
+
123
+ // Helper: base64 to Uint8Array (workers-safe)
124
+ const b64ToBytes = (b64: string): Uint8Array => {
125
+ const bin = atob(b64);
126
+ const out = new Uint8Array(bin.length);
127
+ for (let i = 0; i < bin.length; i++) out[i] = bin.charCodeAt(i);
128
+ return out;
129
+ };
130
+
131
+ const event: CommitEvent = {
132
+ seq: this.nextSeq++,
133
+ did: body.did,
134
+ commitCid: body.commitCid,
135
+ rev: body.rev,
136
+ data: body.data,
137
+ sig: body.sig,
138
+ ts: Date.now(),
139
+ ops: body.ops,
140
+ blocks: body.blocks ? b64ToBytes(body.blocks) : undefined,
141
+ };
142
+
143
+ // Persist sequence number
144
+ await this.state.storage.put('nextSeq', this.nextSeq);
145
+
146
+ // Update commit_log with assigned sequence for this commit (if row exists)
147
+ try {
148
+ const db = drizzle(this.db);
149
+ const res = await db.update(commit_log).set({ seq: event.seq }).where(eq(commit_log.cid, event.commitCid)).run();
150
+ // If the row didn't exist (unexpected), insert a minimal row so replay works
151
+ // Note: drizzle's run() returns a driver-specific result; we just best-effort insert
152
+ if ((res as any)?.success === false) {
153
+ await db.insert(commit_log).values({ seq: event.seq, cid: event.commitCid, rev: event.rev, data: event.data, sig: event.sig, ts: event.ts }).run();
154
+ }
155
+ } catch (e) {
156
+ console.warn('commit_log seq update failed:', e);
157
+ }
158
+
159
+ // Add to buffer
160
+ this.appendCommit(event);
161
+
162
+ // Broadcast to all connected clients
163
+ await this.broadcastCommit(event);
164
+
165
+ return new Response('ok');
166
+ } catch (error) {
167
+ console.error('Failed to handle commit notification:', error);
168
+ return new Response('bad request', { status: 400 });
169
+ }
170
+ }
171
+
172
+ /**
173
+ * Handle identity notification from PDS (handle changes)
174
+ */
175
+ private async handleIdentityNotification(request: Request): Promise<Response> {
176
+ try {
177
+ const body = (await request.json()) as {
178
+ did: string;
179
+ handle?: string;
180
+ };
181
+
182
+ const event: IdentityEvent = {
183
+ seq: this.nextSeq++,
184
+ did: body.did,
185
+ handle: body.handle,
186
+ ts: Date.now(),
187
+ };
188
+
189
+ // Persist sequence number
190
+ await this.state.storage.put('nextSeq', this.nextSeq);
191
+
192
+ // Broadcast to all connected clients
193
+ await this.broadcastIdentity(event);
194
+
195
+ return new Response('ok');
196
+ } catch (error) {
197
+ console.error('Failed to handle identity notification:', error);
198
+ return new Response('bad request', { status: 400 });
199
+ }
200
+ }
201
+
202
+ /**
203
+ * Handle account notification from PDS (account status changes)
204
+ */
205
+ private async handleAccountNotification(request: Request): Promise<Response> {
206
+ try {
207
+ const body = (await request.json()) as {
208
+ did: string;
209
+ active: boolean;
210
+ status?: string;
211
+ };
212
+
213
+ const event: AccountEvent = {
214
+ seq: this.nextSeq++,
215
+ did: body.did,
216
+ active: body.active,
217
+ status: body.status,
218
+ ts: Date.now(),
219
+ };
220
+
221
+ // Persist sequence number
222
+ await this.state.storage.put('nextSeq', this.nextSeq);
223
+
224
+ // Broadcast to all connected clients
225
+ await this.broadcastAccount(event);
226
+
227
+ return new Response('ok');
228
+ } catch (error) {
229
+ console.error('Failed to handle account notification:', error);
230
+ return new Response('bad request', { status: 400 });
231
+ }
232
+ }
233
+
234
+ /**
235
+ * Handle WebSocket upgrade for firehose subscription
236
+ */
237
+ private async handleWebSocketUpgrade(request: Request, url: URL): Promise<Response> {
238
+ const pair = new WebSocketPair();
239
+ const [client, server] = Object.values(pair);
240
+ const id = crypto.randomUUID();
241
+ const ws = server as unknown as WebSocket;
242
+
243
+ ws.accept();
244
+
245
+ // Parse cursor parameter
246
+ const cursorParam = url.searchParams.get('cursor');
247
+ const cursor = cursorParam ? parseInt(cursorParam, 10) : 0;
248
+
249
+ // Validate cursor
250
+ if (cursor > this.nextSeq - 1) {
251
+ // Future cursor error
252
+ const err = checkCursor(cursor, this.nextSeq - 1) ?? createErrorFrame('FutureCursor', 'Cursor is ahead of current sequence').toFramedBytes();
253
+ ws.send(err);
254
+ ws.close(1008, 'FutureCursor');
255
+ return new Response(null, { status: 101, webSocket: client });
256
+ }
257
+
258
+ const clientObj: Client = { webSocket: ws, id, cursor };
259
+ this.clients.set(id, clientObj);
260
+
261
+ // Send #info frame on connection
262
+ const infoFrame = createInfoFrame('com.atproto.sync.subscribeRepos', 'Connected to PDS firehose');
263
+ try {
264
+ ws.send(infoFrame.toFramedBytes());
265
+ } catch (error) {
266
+ console.error('Failed to send info frame:', error);
267
+ }
268
+
269
+ // Set up event handlers
270
+ ws.addEventListener('message', (evt) => {
271
+ try {
272
+ const data = typeof evt.data === 'string' ? evt.data : '';
273
+ if (data === 'ping') {
274
+ ws.send('pong');
275
+ }
276
+ } catch (error) {
277
+ console.error('WebSocket message error:', error);
278
+ }
279
+ });
280
+
281
+ ws.addEventListener('close', () => {
282
+ this.clients.delete(id);
283
+ });
284
+
285
+ ws.addEventListener('error', () => {
286
+ this.clients.delete(id);
287
+ });
288
+
289
+ // Replay buffered events if cursor provided
290
+ if (cursor > 0) {
291
+ await this.replayFromCursor(ws, cursor);
292
+ }
293
+
294
+ return new Response(null, { status: 101, webSocket: client });
295
+ }
296
+
297
+ /**
298
+ * Replay events from cursor
299
+ */
300
+ private async replayFromCursor(ws: WebSocket, cursor: number): Promise<void> {
301
+ // First try from buffer
302
+ const bufferedEvents = this.buffer.filter((e) => e.seq > cursor);
303
+
304
+ if (bufferedEvents.length > 0) {
305
+ for (const event of bufferedEvents) {
306
+ try {
307
+ const frame = await this.createCommitFrame(event);
308
+ ws.send(frame.toFramedBytes());
309
+ } catch (error) {
310
+ console.error('Failed to send buffered event:', error);
311
+ }
312
+ }
313
+ } else {
314
+ // Fetch from database if not in buffer
315
+ try {
316
+ const db = drizzle(this.db);
317
+ const events = await db
318
+ .select()
319
+ .from(commit_log)
320
+ .where(gt(commit_log.seq, cursor))
321
+ .orderBy(commit_log.seq)
322
+ .limit(100)
323
+ .all();
324
+
325
+ for (const event of events) {
326
+ try {
327
+ const commitEvent: CommitEvent = {
328
+ seq: event.seq!,
329
+ did: JSON.parse(event.data).did,
330
+ commitCid: event.cid,
331
+ rev: event.rev,
332
+ data: event.data,
333
+ sig: event.sig,
334
+ ts: event.ts,
335
+ };
336
+ const frame = await this.createCommitFrame(commitEvent);
337
+ ws.send(frame.toFramedBytes());
338
+ } catch (error) {
339
+ console.error('Failed to send database event:', error);
340
+ }
341
+ }
342
+ } catch (error) {
343
+ console.error('Failed to fetch events from database:', error);
344
+ }
345
+ }
346
+ }
347
+
348
+ /**
349
+ * Broadcast commit event to all connected clients
350
+ */
351
+ private async broadcastCommit(event: CommitEvent): Promise<void> {
352
+ const frame = await this.createCommitFrame(event);
353
+ const bytes = frame.toFramedBytes();
354
+
355
+ const disconnected: string[] = [];
356
+
357
+ for (const [id, client] of Array.from(this.clients.entries())) {
358
+ try {
359
+ // Check if client's cursor is caught up
360
+ if (event.seq > client.cursor) {
361
+ client.webSocket.send(bytes);
362
+ client.cursor = event.seq;
363
+ }
364
+ } catch (error) {
365
+ console.error(`Failed to send to client ${id}:`, error);
366
+ disconnected.push(id);
367
+ }
368
+ }
369
+
370
+ // Clean up disconnected clients
371
+ for (const id of disconnected) {
372
+ this.clients.delete(id);
373
+ }
374
+ }
375
+
376
+ /**
377
+ * Broadcast identity event to all connected clients
378
+ */
379
+ private async broadcastIdentity(event: IdentityEvent): Promise<void> {
380
+ const frame = createIdentityFrame({
381
+ seq: event.seq,
382
+ did: event.did,
383
+ time: new Date(event.ts).toISOString(),
384
+ handle: event.handle,
385
+ });
386
+ const bytes = frame.toFramedBytes();
387
+
388
+ const disconnected: string[] = [];
389
+
390
+ for (const [id, client] of Array.from(this.clients.entries())) {
391
+ try {
392
+ if (event.seq > client.cursor) {
393
+ client.webSocket.send(bytes);
394
+ client.cursor = event.seq;
395
+ }
396
+ } catch (error) {
397
+ console.error(`Failed to send to client ${id}:`, error);
398
+ disconnected.push(id);
399
+ }
400
+ }
401
+
402
+ for (const id of disconnected) {
403
+ this.clients.delete(id);
404
+ }
405
+ }
406
+
407
+ /**
408
+ * Broadcast account event to all connected clients
409
+ */
410
+ private async broadcastAccount(event: AccountEvent): Promise<void> {
411
+ const accountFrame = createAccountFrame({
412
+ seq: event.seq,
413
+ did: event.did,
414
+ time: new Date(event.ts).toISOString(),
415
+ active: event.active,
416
+ status: event.status,
417
+ });
418
+ // Emit compatibility #sync frame as well
419
+ const { createSyncFrame } = await import('../lib/firehose/frames');
420
+ const syncLike = createSyncFrame({
421
+ seq: event.seq,
422
+ did: event.did,
423
+ time: new Date(event.ts).toISOString(),
424
+ active: event.active,
425
+ status: event.status,
426
+ });
427
+ const bytesAccount = accountFrame.toFramedBytes();
428
+ const bytesSync = syncLike.toFramedBytes();
429
+
430
+ const disconnected: string[] = [];
431
+
432
+ for (const [id, client] of Array.from(this.clients.entries())) {
433
+ try {
434
+ if (event.seq > client.cursor) {
435
+ client.webSocket.send(bytesAccount);
436
+ client.webSocket.send(bytesSync);
437
+ client.cursor = event.seq;
438
+ }
439
+ } catch (error) {
440
+ console.error(`Failed to send to client ${id}:`, error);
441
+ disconnected.push(id);
442
+ }
443
+ }
444
+
445
+ for (const id of disconnected) {
446
+ this.clients.delete(id);
447
+ }
448
+ }
449
+
450
+ /**
451
+ * Create a #commit frame from event
452
+ */
453
+ private async createCommitFrame(event: CommitEvent): Promise<ReturnType<typeof createCommitFrame>> {
454
+ const commitData = JSON.parse(event.data);
455
+
456
+ // If blocks weren't provided, encode them now
457
+ let blocks = event.blocks;
458
+ if (!blocks && event.ops && event.ops.length > 0) {
459
+ try {
460
+ const commitCid = CID.parse(event.commitCid);
461
+ // Extract MST root from commit data
462
+ const mstRoot = commitData.data ? CID.parse(commitData.data) : commitCid;
463
+ blocks = await encodeBlocksForCommit(
464
+ this.env as Env,
465
+ commitCid,
466
+ mstRoot,
467
+ event.ops,
468
+ );
469
+ } catch (error) {
470
+ console.error('Failed to encode blocks for commit:', error);
471
+ blocks = new Uint8Array();
472
+ }
473
+ }
474
+
475
+ // Resolve prev commit and since (previous rev) when available
476
+ let prevCid: CID | null = null;
477
+ try {
478
+ if (commitData.prev) prevCid = CID.parse(String(commitData.prev));
479
+ } catch {}
480
+
481
+ let since: string | null = null;
482
+ try {
483
+ const db = drizzle(this.db);
484
+ if (prevCid) {
485
+ const prev = await db.select().from(commit_log).where(eq(commit_log.cid, prevCid.toString())).get();
486
+ since = prev?.rev ?? null;
487
+ } else {
488
+ const row = await db.select().from(commit_log).where(gt(commit_log.seq, 0 as any)).orderBy(desc(commit_log.seq)).limit(1).get();
489
+ since = row?.rev ?? null;
490
+ }
491
+ } catch {}
492
+
493
+ const message: CommitMessage = {
494
+ seq: event.seq,
495
+ rebase: false,
496
+ tooBig: false,
497
+ repo: event.did,
498
+ commit: CID.parse(event.commitCid),
499
+ prev: prevCid,
500
+ rev: event.rev,
501
+ since,
502
+ blocks: blocks || new Uint8Array(),
503
+ ops: event.ops || [],
504
+ blobs: [],
505
+ time: new Date(event.ts).toISOString(),
506
+ };
507
+
508
+ return createCommitFrame(message);
509
+ }
510
+
511
+ /**
512
+ * Append commit event to buffer with backpressure
513
+ */
514
+ private appendCommit(event: CommitEvent): void {
515
+ this.buffer.push(event);
516
+
517
+ // Implement backpressure: drop oldest events if buffer is full
518
+ if (this.buffer.length > this.maxWindow) {
519
+ const dropped = this.buffer.shift();
520
+ this.droppedFrameCount++;
521
+ console.warn(`Dropped event seq=${dropped?.seq} due to backpressure (total dropped: ${this.droppedFrameCount})`);
522
+
523
+ // Send #info frame to all clients about dropped frames
524
+ this.notifyFramesDropped();
525
+ }
526
+ }
527
+
528
+ /**
529
+ * Notify all clients that frames were dropped
530
+ */
531
+ private notifyFramesDropped(): void {
532
+ const infoFrame = createInfoFrame(
533
+ 'FramesDropped',
534
+ `${this.droppedFrameCount} frame(s) dropped due to backpressure`,
535
+ );
536
+ const bytes = infoFrame.toFramedBytes();
537
+
538
+ for (const [id, client] of Array.from(this.clients.entries())) {
539
+ try {
540
+ client.webSocket.send(bytes);
541
+ } catch (error) {
542
+ console.error(`Failed to send info frame to client ${id}:`, error);
543
+ }
544
+ }
545
+ }
546
+
547
+ /**
548
+ * Get metrics
549
+ */
550
+ getMetrics(): {
551
+ connectedClients: number;
552
+ bufferSize: number;
553
+ nextSeq: number;
554
+ droppedFrames: number;
555
+ } {
556
+ return {
557
+ connectedClients: this.clients.size,
558
+ bufferSize: this.buffer.length,
559
+ nextSeq: this.nextSeq,
560
+ droppedFrames: this.droppedFrameCount,
561
+ };
562
+ }
563
+ }
package/types/env.d.ts ADDED
@@ -0,0 +1,48 @@
1
+ /// <reference types="astro/client" />
2
+
3
+ import type {
4
+ D1Database,
5
+ DurableObjectNamespace,
6
+ ExecutionContext,
7
+ R2Bucket,
8
+ } from '@cloudflare/workers-types';
9
+
10
+ declare global {
11
+ interface Env {
12
+ DB: D1Database;
13
+ BLOBS: R2Bucket;
14
+ SEQUENCER?: DurableObjectNamespace;
15
+ PDS_HANDLE?: string;
16
+ PDS_DID?: string;
17
+ PDS_HOSTNAME?: string;
18
+ USER_PASSWORD?: string;
19
+ PDS_MAX_BLOB_SIZE?: string;
20
+ ACCESS_TOKEN_SECRET?: string;
21
+ REFRESH_TOKEN_SECRET?: string;
22
+ PDS_ACCESS_TTL_SEC?: string;
23
+ PDS_REFRESH_TTL_SEC?: string;
24
+ JWT_ALGORITHM?: string;
25
+ JWT_ED25519_PRIVATE_KEY?: string;
26
+ JWT_ED25519_PUBLIC_KEY?: string;
27
+ REPO_SIGNING_KEY?: string;
28
+ PDS_RATE_LIMIT_PER_MIN?: string;
29
+ PDS_MAX_JSON_BYTES?: string;
30
+ PDS_CORS_ORIGIN?: string;
31
+ }
32
+
33
+ namespace App {
34
+ interface Locals {
35
+ runtime: {
36
+ env: Env;
37
+ ctx: ExecutionContext;
38
+ request: Request;
39
+ };
40
+ requestId?: string;
41
+ }
42
+ }
43
+ }
44
+
45
+ export {};
46
+
47
+ export type Env = globalThis.Env;
48
+ export type PdsLocals = globalThis.App.Locals;