@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.
- package/README.md +558 -0
- package/index.d.ts +12 -0
- package/index.js +129 -0
- package/package.json +75 -0
- package/src/_worker.ts +44 -0
- package/src/app.ts +10 -0
- package/src/db/client.ts +7 -0
- package/src/db/dal.ts +97 -0
- package/src/db/repo.ts +135 -0
- package/src/db/schema.ts +89 -0
- package/src/db/seed.ts +14 -0
- package/src/env.d.ts +4 -0
- package/src/handlers/debug.ts +34 -0
- package/src/handlers/health.ts +6 -0
- package/src/handlers/ready.ts +14 -0
- package/src/handlers/root.ts +5 -0
- package/src/handlers/wellknown.ts +7 -0
- package/src/handlers/xrpc.repo.core.ts +57 -0
- package/src/handlers/xrpc.server.createSession.ts +25 -0
- package/src/handlers/xrpc.server.refreshSession.ts +43 -0
- package/src/lib/auth.ts +20 -0
- package/src/lib/blockstore-gc.ts +197 -0
- package/src/lib/cache.ts +236 -0
- package/src/lib/car-reader.ts +157 -0
- package/src/lib/commit-log-pruning.ts +76 -0
- package/src/lib/commit.ts +162 -0
- package/src/lib/config.ts +208 -0
- package/src/lib/errors.ts +142 -0
- package/src/lib/firehose/frames.ts +229 -0
- package/src/lib/firehose/parse.ts +82 -0
- package/src/lib/firehose/validation.ts +9 -0
- package/src/lib/handle.ts +90 -0
- package/src/lib/jwt.ts +150 -0
- package/src/lib/logger.ts +73 -0
- package/src/lib/metrics.ts +194 -0
- package/src/lib/mst/blockstore.ts +105 -0
- package/src/lib/mst/index.ts +3 -0
- package/src/lib/mst/mst.ts +643 -0
- package/src/lib/mst/util.ts +86 -0
- package/src/lib/ratelimit.ts +34 -0
- package/src/lib/sequencer.ts +10 -0
- package/src/lib/streaming-car.ts +137 -0
- package/src/lib/token-cleanup.ts +38 -0
- package/src/lib/tracing.ts +136 -0
- package/src/lib/util.ts +55 -0
- package/src/middleware.ts +102 -0
- package/src/pages/.well-known/atproto-did.ts +7 -0
- package/src/pages/.well-known/did.json.ts +76 -0
- package/src/pages/debug/blob/[...key].ts +27 -0
- package/src/pages/debug/db/bootstrap.ts +23 -0
- package/src/pages/debug/db/commits.ts +20 -0
- package/src/pages/debug/gc/blobs.ts +16 -0
- package/src/pages/debug/record.ts +33 -0
- package/src/pages/health.ts +68 -0
- package/src/pages/index.astro +57 -0
- package/src/pages/index.ts +2 -0
- package/src/pages/ready.ts +16 -0
- package/src/pages/xrpc/com.atproto.identity.resolveHandle.ts +38 -0
- package/src/pages/xrpc/com.atproto.identity.updateHandle.ts +45 -0
- package/src/pages/xrpc/com.atproto.repo.applyWrites.ts +73 -0
- package/src/pages/xrpc/com.atproto.repo.createRecord.ts +36 -0
- package/src/pages/xrpc/com.atproto.repo.deleteRecord.ts +36 -0
- package/src/pages/xrpc/com.atproto.repo.describeRepo.ts +51 -0
- package/src/pages/xrpc/com.atproto.repo.getRecord.ts +25 -0
- package/src/pages/xrpc/com.atproto.repo.listRecords.ts +57 -0
- package/src/pages/xrpc/com.atproto.repo.putRecord.ts +36 -0
- package/src/pages/xrpc/com.atproto.repo.uploadBlob.ts +53 -0
- package/src/pages/xrpc/com.atproto.server.createSession.ts +92 -0
- package/src/pages/xrpc/com.atproto.server.deleteSession.ts +25 -0
- package/src/pages/xrpc/com.atproto.server.describeServer.ts +17 -0
- package/src/pages/xrpc/com.atproto.server.getSession.ts +46 -0
- package/src/pages/xrpc/com.atproto.server.refreshSession.ts +67 -0
- package/src/pages/xrpc/com.atproto.sync.getBlocks.json.ts +16 -0
- package/src/pages/xrpc/com.atproto.sync.getBlocks.ts +56 -0
- package/src/pages/xrpc/com.atproto.sync.getCheckout.json.ts +20 -0
- package/src/pages/xrpc/com.atproto.sync.getCheckout.ts +43 -0
- package/src/pages/xrpc/com.atproto.sync.getHead.ts +11 -0
- package/src/pages/xrpc/com.atproto.sync.getLatestCommit.ts +42 -0
- package/src/pages/xrpc/com.atproto.sync.getRecord.ts +63 -0
- package/src/pages/xrpc/com.atproto.sync.getRepo.json.ts +20 -0
- package/src/pages/xrpc/com.atproto.sync.getRepo.range.ts +34 -0
- package/src/pages/xrpc/com.atproto.sync.getRepo.ts +17 -0
- package/src/pages/xrpc/com.atproto.sync.listBlobs.ts +53 -0
- package/src/pages/xrpc/com.atproto.sync.listRepos.ts +31 -0
- package/src/services/car.ts +249 -0
- package/src/services/r2-blob-store.ts +87 -0
- package/src/services/repo-manager.ts +339 -0
- package/src/shims/astro-internal-handler.d.ts +4 -0
- package/src/worker/sequencer.ts +563 -0
- 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;
|