@alteran/astro 0.3.8 → 0.5.2
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/LICENSE +21 -0
- package/README.md +19 -30
- package/index.js +34 -28
- package/migrations/0007_bored_spitfire.sql +26 -0
- package/migrations/0008_furry_ozymandias.sql +2 -0
- package/migrations/meta/0007_snapshot.json +534 -0
- package/migrations/meta/0008_snapshot.json +548 -0
- package/migrations/meta/_journal.json +14 -0
- package/package.json +10 -9
- package/src/app.ts +8 -4
- package/src/db/account.ts +25 -6
- package/src/db/dal.ts +34 -23
- package/src/db/repo.ts +35 -35
- package/src/db/schema.ts +5 -1
- package/src/db/seed.ts +5 -13
- package/src/entrypoints/server.ts +2 -22
- package/src/handlers/root.ts +4 -4
- package/src/lib/account-state.ts +156 -0
- package/src/lib/actor.ts +28 -12
- package/src/lib/appview/auth-policy.ts +66 -0
- package/src/lib/appview/did-resolver.ts +233 -0
- package/src/lib/appview/proxy.ts +221 -0
- package/src/lib/appview/service-config.ts +61 -0
- package/src/lib/appview/service-jwt.ts +93 -0
- package/src/lib/appview/types.ts +25 -0
- package/src/lib/appview.ts +5 -532
- package/src/lib/auth-errors.ts +24 -0
- package/src/lib/auth.ts +63 -15
- package/src/lib/blockstore-gc.ts +2 -1
- package/src/lib/cache.ts +30 -4
- package/src/lib/chat.ts +14 -8
- package/src/lib/commit.ts +26 -36
- package/src/lib/config.ts +26 -15
- package/src/lib/did-document.ts +32 -0
- package/src/lib/errors.ts +54 -0
- package/src/lib/feed.ts +18 -19
- package/src/lib/firehose/frames.ts +87 -47
- package/src/lib/firehose/validation.ts +3 -3
- package/src/lib/jwt.ts +85 -177
- package/src/lib/labeler.ts +43 -30
- package/src/lib/logger.ts +4 -0
- package/src/lib/mst/block-map.ts +172 -0
- package/src/lib/mst/blockstore.ts +56 -93
- package/src/lib/mst/index.ts +1 -0
- package/src/lib/mst/leaf.ts +25 -0
- package/src/lib/mst/mst.ts +81 -237
- package/src/lib/mst/serialize.ts +97 -0
- package/src/lib/mst/types.ts +21 -0
- package/src/lib/oauth/clients.ts +67 -0
- package/src/lib/oauth/dpop-errors.ts +15 -0
- package/src/lib/oauth/dpop.ts +150 -0
- package/src/lib/oauth/resource.ts +199 -0
- package/src/lib/oauth/store.ts +77 -0
- package/src/lib/preferences.ts +9 -34
- package/src/lib/refresh-session.ts +161 -0
- package/src/lib/relay.ts +10 -8
- package/src/lib/secrets.ts +6 -7
- package/src/lib/sequencer.ts +12 -3
- package/src/lib/service-auth.ts +184 -0
- package/src/lib/session-tokens.ts +28 -76
- package/src/lib/streaming-car.ts +3 -0
- package/src/lib/tracing.ts +4 -3
- package/src/lib/util.ts +65 -15
- package/src/middleware.ts +1 -1
- package/src/pages/.well-known/did.json.ts +27 -30
- package/src/pages/.well-known/oauth-authorization-server.ts +31 -0
- package/src/pages/.well-known/oauth-protected-resource.ts +22 -0
- package/src/pages/debug/record.ts +1 -1
- package/src/pages/debug/sequencer.ts +28 -0
- package/src/pages/oauth/authorize.ts +78 -0
- package/src/pages/oauth/consent.ts +80 -0
- package/src/pages/oauth/par.ts +121 -0
- package/src/pages/oauth/token.ts +158 -0
- package/src/pages/xrpc/[...nsid].ts +61 -0
- package/src/pages/xrpc/app.bsky.actor.getPreferences.ts +12 -13
- package/src/pages/xrpc/app.bsky.actor.putPreferences.ts +23 -23
- package/src/pages/xrpc/app.bsky.unspecced.getAgeAssuranceState.ts +9 -2
- package/src/pages/xrpc/chat.bsky.convo.getLog.ts +9 -2
- package/src/pages/xrpc/chat.bsky.convo.listConvos.ts +9 -2
- package/src/pages/xrpc/com.atproto.identity.getRecommendedDidCredentials.ts +43 -41
- package/src/pages/xrpc/com.atproto.identity.requestPlcOperationSignature.ts +10 -3
- package/src/pages/xrpc/com.atproto.identity.resolveHandle.ts +40 -9
- package/src/pages/xrpc/com.atproto.identity.signPlcOperation.ts +41 -29
- package/src/pages/xrpc/com.atproto.identity.submitPlcOperation.ts +20 -6
- package/src/pages/xrpc/com.atproto.identity.updateHandle.ts +1 -1
- package/src/pages/xrpc/com.atproto.repo.applyWrites.ts +101 -11
- package/src/pages/xrpc/com.atproto.repo.createRecord.ts +44 -14
- package/src/pages/xrpc/com.atproto.repo.deleteRecord.ts +41 -13
- package/src/pages/xrpc/com.atproto.repo.describeRepo.ts +2 -2
- package/src/pages/xrpc/com.atproto.repo.getRecord.ts +14 -1
- package/src/pages/xrpc/com.atproto.repo.listMissingBlobs.ts +14 -6
- package/src/pages/xrpc/com.atproto.repo.listRecords.ts +1 -1
- package/src/pages/xrpc/com.atproto.repo.putRecord.ts +42 -14
- package/src/pages/xrpc/com.atproto.repo.uploadBlob.ts +76 -15
- package/src/pages/xrpc/com.atproto.server.checkAccountStatus.ts +20 -8
- package/src/pages/xrpc/com.atproto.server.createSession.ts +31 -11
- package/src/pages/xrpc/com.atproto.server.describeServer.ts +1 -1
- package/src/pages/xrpc/com.atproto.server.getServiceAuth.ts +12 -5
- package/src/pages/xrpc/com.atproto.server.getSession.ts +22 -8
- package/src/pages/xrpc/com.atproto.server.refreshSession.ts +30 -72
- package/src/pages/xrpc/com.atproto.sync.getBlob.ts +71 -22
- package/src/pages/xrpc/com.atproto.sync.getCheckout.json.ts +1 -1
- package/src/pages/xrpc/com.atproto.sync.getCheckout.ts +1 -1
- package/src/pages/xrpc/com.atproto.sync.getHead.ts +7 -2
- package/src/pages/xrpc/com.atproto.sync.getLatestCommit.ts +1 -1
- package/src/pages/xrpc/com.atproto.sync.getRecord.ts +5 -27
- package/src/pages/xrpc/com.atproto.sync.getRepo.json.ts +1 -1
- package/src/pages/xrpc/com.atproto.sync.getRepo.ts +50 -5
- package/src/pages/xrpc/com.atproto.sync.getRepoStatus.ts +58 -0
- package/src/pages/xrpc/com.atproto.sync.listBlobs.ts +1 -1
- package/src/pages/xrpc/com.atproto.sync.listRepos.ts +5 -3
- package/src/services/car.ts +207 -55
- package/src/services/r2-blob-store.ts +1 -1
- package/src/services/repo/blockstore-ops.ts +29 -0
- package/src/services/repo/operations.ts +133 -0
- package/src/services/repo-manager.ts +202 -253
- package/src/worker/runtime.ts +53 -8
- package/src/worker/sequencer/broadcast.ts +91 -0
- package/src/worker/sequencer/cid-helpers.ts +39 -0
- package/src/worker/sequencer/payload.ts +84 -0
- package/src/worker/sequencer/types.ts +36 -0
- package/src/worker/sequencer/upgrade.ts +141 -0
- package/src/worker/sequencer.ts +288 -412
- package/types/env.d.ts +15 -3
- package/src/pages/xrpc/app.bsky.actor.getProfile.ts +0 -49
- package/src/pages/xrpc/app.bsky.actor.getProfiles.ts +0 -51
- package/src/pages/xrpc/app.bsky.feed.getActorFeeds.ts +0 -25
- package/src/pages/xrpc/app.bsky.feed.getAuthorFeed.ts +0 -42
- package/src/pages/xrpc/app.bsky.feed.getFeedGenerators.ts +0 -25
- package/src/pages/xrpc/app.bsky.feed.getPostThread.ts +0 -37
- package/src/pages/xrpc/app.bsky.feed.getPosts.ts +0 -26
- package/src/pages/xrpc/app.bsky.feed.getTimeline.ts +0 -47
- package/src/pages/xrpc/app.bsky.graph.getFollowers.ts +0 -29
- package/src/pages/xrpc/app.bsky.graph.getFollows.ts +0 -29
- package/src/pages/xrpc/app.bsky.notification.getUnreadCount.ts +0 -20
- package/src/pages/xrpc/app.bsky.notification.listNotifications.ts +0 -27
package/src/worker/sequencer.ts
CHANGED
|
@@ -1,65 +1,41 @@
|
|
|
1
|
-
// Types via tsconfig
|
|
2
|
-
|
|
3
1
|
import type { DurableObjectState, D1Database } from '@cloudflare/workers-types';
|
|
4
2
|
import { drizzle } from 'drizzle-orm/d1';
|
|
5
3
|
import { commit_log } from '../db/schema';
|
|
6
4
|
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';
|
|
5
|
+
import { encodeInfoFrame, encodeCommitFrame } from '../lib/firehose/frames';
|
|
19
6
|
import type { Env } from '../env';
|
|
7
|
+
import { fromWireStatus } from '../lib/account-state';
|
|
8
|
+
import type {
|
|
9
|
+
AccountEvent,
|
|
10
|
+
Client,
|
|
11
|
+
CommitEvent,
|
|
12
|
+
IdentityEvent,
|
|
13
|
+
} from './sequencer/types';
|
|
14
|
+
import { reviveOps, base64ToBytes } from './sequencer/cid-helpers';
|
|
15
|
+
import { createCommitPayload } from './sequencer/payload';
|
|
16
|
+
import {
|
|
17
|
+
handleUpgrade,
|
|
18
|
+
type HibernatableSocket,
|
|
19
|
+
type HibernatableState,
|
|
20
|
+
type WebSocketAttachment,
|
|
21
|
+
} from './sequencer/upgrade';
|
|
22
|
+
import {
|
|
23
|
+
broadcastAccount,
|
|
24
|
+
broadcastCommit,
|
|
25
|
+
broadcastIdentity,
|
|
26
|
+
} from './sequencer/broadcast';
|
|
27
|
+
|
|
28
|
+
export type {
|
|
29
|
+
AccountEvent,
|
|
30
|
+
Client,
|
|
31
|
+
CommitEvent,
|
|
32
|
+
IdentityEvent,
|
|
33
|
+
SequencerEvent,
|
|
34
|
+
} from './sequencer/types';
|
|
20
35
|
|
|
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
36
|
export class Sequencer {
|
|
61
|
-
private readonly state:
|
|
62
|
-
private readonly env: Env
|
|
37
|
+
private readonly state: HibernatableState;
|
|
38
|
+
private readonly env: Env;
|
|
63
39
|
private readonly clients = new Map<string, Client>();
|
|
64
40
|
private buffer: CommitEvent[] = [];
|
|
65
41
|
private readonly db: D1Database;
|
|
@@ -67,17 +43,39 @@ export class Sequencer {
|
|
|
67
43
|
private nextSeq = 1;
|
|
68
44
|
private droppedFrameCount = 0;
|
|
69
45
|
|
|
70
|
-
constructor(state: DurableObjectState, env: Env
|
|
71
|
-
this.state = state;
|
|
46
|
+
constructor(state: DurableObjectState, env: Env) {
|
|
47
|
+
this.state = state as HibernatableState;
|
|
72
48
|
this.env = env;
|
|
73
49
|
this.db = env.DB;
|
|
74
50
|
this.maxWindow = parseInt(env.PDS_SEQ_WINDOW || '512', 10);
|
|
75
51
|
|
|
76
|
-
//
|
|
52
|
+
// Reconcile nextSeq with DB on construction so replay and append agree
|
|
53
|
+
// after worker restarts or DO migrations.
|
|
77
54
|
this.state.blockConcurrencyWhile(async () => {
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
this.nextSeq
|
|
55
|
+
let base = 0;
|
|
56
|
+
try {
|
|
57
|
+
base = (await this.state.storage.get<number>('nextSeq')) || 0;
|
|
58
|
+
} catch (storageError) {
|
|
59
|
+
console.warn('Sequencer: storage.get(nextSeq) failed:', storageError);
|
|
60
|
+
}
|
|
61
|
+
try {
|
|
62
|
+
const db = drizzle(this.db);
|
|
63
|
+
const last = await db
|
|
64
|
+
.select({ seq: commit_log.seq })
|
|
65
|
+
.from(commit_log)
|
|
66
|
+
.orderBy(desc(commit_log.seq))
|
|
67
|
+
.limit(1)
|
|
68
|
+
.get();
|
|
69
|
+
const dbNext = last?.seq ? Number(last.seq) + 1 : 1;
|
|
70
|
+
if (!base || dbNext > base) base = dbNext;
|
|
71
|
+
} catch (dbError) {
|
|
72
|
+
console.warn('Sequencer: commit_log max(seq) failed:', dbError);
|
|
73
|
+
}
|
|
74
|
+
this.nextSeq = base > 0 ? base : 1;
|
|
75
|
+
try {
|
|
76
|
+
await this.state.storage.put('nextSeq', this.nextSeq);
|
|
77
|
+
} catch (storageError) {
|
|
78
|
+
console.warn('Sequencer: storage.put(nextSeq) failed:', storageError);
|
|
81
79
|
}
|
|
82
80
|
});
|
|
83
81
|
}
|
|
@@ -85,18 +83,16 @@ export class Sequencer {
|
|
|
85
83
|
async fetch(request: Request): Promise<Response> {
|
|
86
84
|
const url = new URL(request.url);
|
|
87
85
|
|
|
88
|
-
// Handle event notifications from PDS
|
|
89
86
|
if (request.method === 'POST') {
|
|
90
|
-
if (url.pathname === '/commit')
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
87
|
+
if (url.pathname === '/commit') return this.handleCommitNotification(request);
|
|
88
|
+
if (url.pathname === '/identity') return this.handleIdentityNotification(request);
|
|
89
|
+
if (url.pathname === '/account') return this.handleAccountNotification(request);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (request.method === 'GET' && url.pathname === '/metrics') {
|
|
93
|
+
return this.handleMetrics();
|
|
97
94
|
}
|
|
98
95
|
|
|
99
|
-
// Handle WebSocket upgrade for firehose subscription
|
|
100
96
|
const upgradeHeader = request.headers.get('Upgrade');
|
|
101
97
|
if (upgradeHeader !== 'websocket') {
|
|
102
98
|
return new Response('Expected websocket', { status: 426 });
|
|
@@ -105,9 +101,6 @@ export class Sequencer {
|
|
|
105
101
|
return this.handleWebSocketUpgrade(request, url);
|
|
106
102
|
}
|
|
107
103
|
|
|
108
|
-
/**
|
|
109
|
-
* Handle commit notification from PDS
|
|
110
|
-
*/
|
|
111
104
|
private async handleCommitNotification(request: Request): Promise<Response> {
|
|
112
105
|
try {
|
|
113
106
|
const body = (await request.json()) as {
|
|
@@ -116,50 +109,79 @@ export class Sequencer {
|
|
|
116
109
|
rev: string;
|
|
117
110
|
data: string;
|
|
118
111
|
sig: string;
|
|
119
|
-
ops?:
|
|
120
|
-
blocks?: string;
|
|
112
|
+
ops?: unknown;
|
|
113
|
+
blocks?: string;
|
|
121
114
|
};
|
|
122
115
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
116
|
+
const ops = reviveOps(body.ops);
|
|
117
|
+
|
|
118
|
+
const db = drizzle(this.db);
|
|
119
|
+
let seqForEvent: number | null = null;
|
|
120
|
+
let tsForEvent = Date.now();
|
|
121
|
+
try {
|
|
122
|
+
const row = await db
|
|
123
|
+
.select({
|
|
124
|
+
seq: commit_log.seq,
|
|
125
|
+
rev: commit_log.rev,
|
|
126
|
+
data: commit_log.data,
|
|
127
|
+
sig: commit_log.sig,
|
|
128
|
+
ts: commit_log.ts,
|
|
129
|
+
})
|
|
130
|
+
.from(commit_log)
|
|
131
|
+
.where(eq(commit_log.cid, body.commitCid))
|
|
132
|
+
.limit(1)
|
|
133
|
+
.get();
|
|
134
|
+
if (row && typeof row.seq === 'number') {
|
|
135
|
+
seqForEvent = row.seq;
|
|
136
|
+
body.rev = row.rev;
|
|
137
|
+
body.data = row.data;
|
|
138
|
+
body.sig = row.sig;
|
|
139
|
+
tsForEvent = row.ts ?? tsForEvent;
|
|
140
|
+
}
|
|
141
|
+
} catch (lookupError) {
|
|
142
|
+
console.warn('commit_log lookup failed:', lookupError);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (seqForEvent == null) {
|
|
146
|
+
seqForEvent = this.nextSeq++;
|
|
147
|
+
await this.state.storage.put('nextSeq', this.nextSeq);
|
|
148
|
+
try {
|
|
149
|
+
await db
|
|
150
|
+
.insert(commit_log)
|
|
151
|
+
.values({
|
|
152
|
+
seq: seqForEvent,
|
|
153
|
+
cid: body.commitCid,
|
|
154
|
+
rev: body.rev,
|
|
155
|
+
data: body.data,
|
|
156
|
+
sig: body.sig,
|
|
157
|
+
ts: tsForEvent,
|
|
158
|
+
})
|
|
159
|
+
.run();
|
|
160
|
+
} catch (insertError) {
|
|
161
|
+
console.warn('commit_log insert failed:', insertError);
|
|
162
|
+
}
|
|
163
|
+
} else if (seqForEvent >= this.nextSeq) {
|
|
164
|
+
this.nextSeq = seqForEvent + 1;
|
|
165
|
+
try {
|
|
166
|
+
await this.state.storage.put('nextSeq', this.nextSeq);
|
|
167
|
+
} catch (putError) {
|
|
168
|
+
console.warn('Sequencer: storage.put(nextSeq) failed:', putError);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
130
171
|
|
|
131
172
|
const event: CommitEvent = {
|
|
132
|
-
seq:
|
|
173
|
+
seq: seqForEvent,
|
|
133
174
|
did: body.did,
|
|
134
175
|
commitCid: body.commitCid,
|
|
135
176
|
rev: body.rev,
|
|
136
177
|
data: body.data,
|
|
137
178
|
sig: body.sig,
|
|
138
|
-
ts:
|
|
139
|
-
ops
|
|
140
|
-
blocks: body.blocks ?
|
|
179
|
+
ts: tsForEvent,
|
|
180
|
+
ops,
|
|
181
|
+
blocks: body.blocks ? base64ToBytes(body.blocks) : undefined,
|
|
141
182
|
};
|
|
142
183
|
|
|
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
184
|
this.appendCommit(event);
|
|
161
|
-
|
|
162
|
-
// Broadcast to all connected clients
|
|
163
185
|
await this.broadcastCommit(event);
|
|
164
186
|
|
|
165
187
|
return new Response('ok');
|
|
@@ -169,29 +191,21 @@ export class Sequencer {
|
|
|
169
191
|
}
|
|
170
192
|
}
|
|
171
193
|
|
|
172
|
-
/**
|
|
173
|
-
* Handle identity notification from PDS (handle changes)
|
|
174
|
-
*/
|
|
175
194
|
private async handleIdentityNotification(request: Request): Promise<Response> {
|
|
176
195
|
try {
|
|
177
|
-
const body = (await request.json()) as {
|
|
178
|
-
did: string;
|
|
179
|
-
handle?: string;
|
|
180
|
-
};
|
|
181
|
-
|
|
196
|
+
const body = (await request.json()) as { did: string; handle?: string };
|
|
182
197
|
const event: IdentityEvent = {
|
|
183
198
|
seq: this.nextSeq++,
|
|
184
199
|
did: body.did,
|
|
185
200
|
handle: body.handle,
|
|
186
201
|
ts: Date.now(),
|
|
187
202
|
};
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
203
|
+
try {
|
|
204
|
+
await this.state.storage.put('nextSeq', this.nextSeq);
|
|
205
|
+
} catch (putError) {
|
|
206
|
+
console.warn('Sequencer: storage.put(nextSeq) failed:', putError);
|
|
207
|
+
}
|
|
193
208
|
await this.broadcastIdentity(event);
|
|
194
|
-
|
|
195
209
|
return new Response('ok');
|
|
196
210
|
} catch (error) {
|
|
197
211
|
console.error('Failed to handle identity notification:', error);
|
|
@@ -199,31 +213,21 @@ export class Sequencer {
|
|
|
199
213
|
}
|
|
200
214
|
}
|
|
201
215
|
|
|
202
|
-
/**
|
|
203
|
-
* Handle account notification from PDS (account status changes)
|
|
204
|
-
*/
|
|
205
216
|
private async handleAccountNotification(request: Request): Promise<Response> {
|
|
206
217
|
try {
|
|
207
|
-
const body = (await request.json()) as {
|
|
208
|
-
did: string;
|
|
209
|
-
active: boolean;
|
|
210
|
-
status?: string;
|
|
211
|
-
};
|
|
212
|
-
|
|
218
|
+
const body = (await request.json()) as { did: string; active: boolean; status?: string };
|
|
213
219
|
const event: AccountEvent = {
|
|
214
220
|
seq: this.nextSeq++,
|
|
215
221
|
did: body.did,
|
|
216
|
-
active: body.active,
|
|
217
|
-
status: body.status,
|
|
222
|
+
state: fromWireStatus({ active: body.active, status: body.status }),
|
|
218
223
|
ts: Date.now(),
|
|
219
224
|
};
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
+
try {
|
|
226
|
+
await this.state.storage.put('nextSeq', this.nextSeq);
|
|
227
|
+
} catch (putError) {
|
|
228
|
+
console.warn('Sequencer: storage.put(nextSeq) failed:', putError);
|
|
229
|
+
}
|
|
225
230
|
await this.broadcastAccount(event);
|
|
226
|
-
|
|
227
231
|
return new Response('ok');
|
|
228
232
|
} catch (error) {
|
|
229
233
|
console.error('Failed to handle account notification:', error);
|
|
@@ -231,321 +235,110 @@ export class Sequencer {
|
|
|
231
235
|
}
|
|
232
236
|
}
|
|
233
237
|
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
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
|
-
// 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);
|
|
238
|
+
private handleWebSocketUpgrade(request: Request, url: URL): Response {
|
|
239
|
+
const hibernate = String(this.env.PDS_WS_HIBERNATE ?? 'true').toLowerCase() !== 'false';
|
|
240
|
+
return handleUpgrade(request, url, {
|
|
241
|
+
state: this.state,
|
|
242
|
+
nextSeq: this.nextSeq,
|
|
243
|
+
hibernate,
|
|
244
|
+
onClient: (id, cursor, server) => {
|
|
245
|
+
this.clients.set(id, { webSocket: server, id, cursor });
|
|
246
|
+
},
|
|
299
247
|
});
|
|
300
|
-
|
|
301
|
-
// Replay buffered events if cursor provided
|
|
302
|
-
if (cursor > 0) {
|
|
303
|
-
await this.replayFromCursor(ws, cursor);
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
return new Response(null, { status: 101, webSocket: client });
|
|
307
248
|
}
|
|
308
249
|
|
|
309
|
-
/**
|
|
310
|
-
* Replay events from cursor
|
|
311
|
-
*/
|
|
312
250
|
private async replayFromCursor(ws: WebSocket, cursor: number): Promise<void> {
|
|
313
|
-
// First try from buffer
|
|
314
251
|
const bufferedEvents = this.buffer.filter((e) => e.seq > cursor);
|
|
315
252
|
|
|
316
253
|
if (bufferedEvents.length > 0) {
|
|
317
254
|
for (const event of bufferedEvents) {
|
|
318
255
|
try {
|
|
319
|
-
const
|
|
320
|
-
ws.send(
|
|
256
|
+
const message = await createCommitPayload(this.env, this.db, event);
|
|
257
|
+
ws.send(encodeCommitFrame(message));
|
|
321
258
|
} catch (error) {
|
|
322
259
|
console.error('Failed to send buffered event:', error);
|
|
323
260
|
}
|
|
324
261
|
}
|
|
325
|
-
|
|
326
|
-
// Fetch from database if not in buffer
|
|
327
|
-
try {
|
|
328
|
-
const db = drizzle(this.db);
|
|
329
|
-
const events = await db
|
|
330
|
-
.select()
|
|
331
|
-
.from(commit_log)
|
|
332
|
-
.where(gt(commit_log.seq, cursor))
|
|
333
|
-
.orderBy(commit_log.seq)
|
|
334
|
-
.limit(100)
|
|
335
|
-
.all();
|
|
336
|
-
|
|
337
|
-
for (const event of events) {
|
|
338
|
-
try {
|
|
339
|
-
const commitEvent: CommitEvent = {
|
|
340
|
-
seq: event.seq!,
|
|
341
|
-
did: JSON.parse(event.data).did,
|
|
342
|
-
commitCid: event.cid,
|
|
343
|
-
rev: event.rev,
|
|
344
|
-
data: event.data,
|
|
345
|
-
sig: event.sig,
|
|
346
|
-
ts: event.ts,
|
|
347
|
-
};
|
|
348
|
-
const frame = await this.createCommitFrame(commitEvent);
|
|
349
|
-
ws.send(frame.toFramedBytes());
|
|
350
|
-
} catch (error) {
|
|
351
|
-
console.error('Failed to send database event:', error);
|
|
352
|
-
}
|
|
353
|
-
}
|
|
354
|
-
} catch (error) {
|
|
355
|
-
console.error('Failed to fetch events from database:', error);
|
|
356
|
-
}
|
|
262
|
+
return;
|
|
357
263
|
}
|
|
358
|
-
}
|
|
359
|
-
|
|
360
|
-
/**
|
|
361
|
-
* Broadcast commit event to all connected clients
|
|
362
|
-
*/
|
|
363
|
-
private async broadcastCommit(event: CommitEvent): Promise<void> {
|
|
364
|
-
const frame = await this.createCommitFrame(event);
|
|
365
|
-
const bytes = frame.toFramedBytes();
|
|
366
264
|
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
265
|
+
try {
|
|
266
|
+
const db = drizzle(this.db);
|
|
267
|
+
const events = await db
|
|
268
|
+
.select()
|
|
269
|
+
.from(commit_log)
|
|
270
|
+
.where(gt(commit_log.seq, cursor))
|
|
271
|
+
.orderBy(commit_log.seq)
|
|
272
|
+
.limit(100)
|
|
273
|
+
.all();
|
|
274
|
+
|
|
275
|
+
for (const event of events) {
|
|
276
|
+
try {
|
|
277
|
+
if (event.seq == null) continue;
|
|
278
|
+
const commitEvent: CommitEvent = {
|
|
279
|
+
seq: event.seq,
|
|
280
|
+
did: JSON.parse(event.data).did,
|
|
281
|
+
commitCid: event.cid,
|
|
282
|
+
rev: event.rev,
|
|
283
|
+
data: event.data,
|
|
284
|
+
sig: event.sig,
|
|
285
|
+
ts: event.ts,
|
|
286
|
+
};
|
|
287
|
+
const message = await createCommitPayload(this.env, this.db, commitEvent);
|
|
288
|
+
ws.send(encodeCommitFrame(message));
|
|
289
|
+
} catch (error) {
|
|
290
|
+
console.error('Failed to send database event:', error);
|
|
375
291
|
}
|
|
376
|
-
} catch (error) {
|
|
377
|
-
console.error(`Failed to send to client ${id}:`, error);
|
|
378
|
-
disconnected.push(id);
|
|
379
292
|
}
|
|
380
|
-
}
|
|
381
|
-
|
|
382
|
-
// Clean up disconnected clients
|
|
383
|
-
for (const id of disconnected) {
|
|
384
|
-
this.clients.delete(id);
|
|
293
|
+
} catch (error) {
|
|
294
|
+
console.error('Failed to fetch events from database:', error);
|
|
385
295
|
}
|
|
386
296
|
}
|
|
387
297
|
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
handle: event.handle,
|
|
397
|
-
});
|
|
398
|
-
const bytes = frame.toFramedBytes();
|
|
399
|
-
|
|
400
|
-
const disconnected: string[] = [];
|
|
401
|
-
|
|
402
|
-
for (const [id, client] of Array.from(this.clients.entries())) {
|
|
403
|
-
try {
|
|
404
|
-
if (event.seq > client.cursor) {
|
|
405
|
-
client.webSocket.send(bytes);
|
|
406
|
-
client.cursor = event.seq;
|
|
407
|
-
}
|
|
408
|
-
} catch (error) {
|
|
409
|
-
console.error(`Failed to send to client ${id}:`, error);
|
|
410
|
-
disconnected.push(id);
|
|
411
|
-
}
|
|
412
|
-
}
|
|
413
|
-
|
|
414
|
-
for (const id of disconnected) {
|
|
415
|
-
this.clients.delete(id);
|
|
298
|
+
private getSocketTargets(): WebSocket[] {
|
|
299
|
+
let sockets: WebSocket[] = [];
|
|
300
|
+
try {
|
|
301
|
+
// workers-types WebSocket misses a few DOM-types members; the values
|
|
302
|
+
// are wire-compatible at runtime, so widen through unknown.
|
|
303
|
+
sockets = (this.state.getWebSockets?.() || []) as unknown as WebSocket[];
|
|
304
|
+
} catch (error) {
|
|
305
|
+
console.warn('Sequencer: getWebSockets failed:', error);
|
|
416
306
|
}
|
|
307
|
+
return sockets.length > 0
|
|
308
|
+
? sockets
|
|
309
|
+
: Array.from(this.clients.values()).map((c) => c.webSocket);
|
|
417
310
|
}
|
|
418
311
|
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
*/
|
|
422
|
-
private async broadcastAccount(event: AccountEvent): Promise<void> {
|
|
423
|
-
const accountFrame = createAccountFrame({
|
|
424
|
-
seq: event.seq,
|
|
425
|
-
did: event.did,
|
|
426
|
-
time: new Date(event.ts).toISOString(),
|
|
427
|
-
active: event.active,
|
|
428
|
-
status: event.status,
|
|
429
|
-
});
|
|
430
|
-
// Emit compatibility #sync frame as well
|
|
431
|
-
const { createSyncFrame } = await import('../lib/firehose/frames');
|
|
432
|
-
const syncLike = createSyncFrame({
|
|
433
|
-
seq: event.seq,
|
|
434
|
-
did: event.did,
|
|
435
|
-
time: new Date(event.ts).toISOString(),
|
|
436
|
-
active: event.active,
|
|
437
|
-
status: event.status,
|
|
438
|
-
});
|
|
439
|
-
const bytesAccount = accountFrame.toFramedBytes();
|
|
440
|
-
const bytesSync = syncLike.toFramedBytes();
|
|
441
|
-
|
|
442
|
-
const disconnected: string[] = [];
|
|
443
|
-
|
|
444
|
-
for (const [id, client] of Array.from(this.clients.entries())) {
|
|
445
|
-
try {
|
|
446
|
-
if (event.seq > client.cursor) {
|
|
447
|
-
client.webSocket.send(bytesAccount);
|
|
448
|
-
client.webSocket.send(bytesSync);
|
|
449
|
-
client.cursor = event.seq;
|
|
450
|
-
}
|
|
451
|
-
} catch (error) {
|
|
452
|
-
console.error(`Failed to send to client ${id}:`, error);
|
|
453
|
-
disconnected.push(id);
|
|
454
|
-
}
|
|
455
|
-
}
|
|
456
|
-
|
|
457
|
-
for (const id of disconnected) {
|
|
458
|
-
this.clients.delete(id);
|
|
459
|
-
}
|
|
312
|
+
private broadcastCommit(event: CommitEvent): Promise<void> {
|
|
313
|
+
return broadcastCommit(this.env, this.db, this.getSocketTargets(), event);
|
|
460
314
|
}
|
|
461
315
|
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
private async createCommitFrame(event: CommitEvent): Promise<ReturnType<typeof createCommitFrame>> {
|
|
466
|
-
const commitData = JSON.parse(event.data);
|
|
467
|
-
|
|
468
|
-
// If blocks weren't provided, encode them now
|
|
469
|
-
let blocks = event.blocks;
|
|
470
|
-
if (!blocks && event.ops && event.ops.length > 0) {
|
|
471
|
-
try {
|
|
472
|
-
const commitCid = CID.parse(event.commitCid);
|
|
473
|
-
// Extract MST root from commit data
|
|
474
|
-
const mstRoot = commitData.data ? CID.parse(commitData.data) : commitCid;
|
|
475
|
-
blocks = await encodeBlocksForCommit(
|
|
476
|
-
this.env as Env,
|
|
477
|
-
commitCid,
|
|
478
|
-
mstRoot,
|
|
479
|
-
event.ops,
|
|
480
|
-
);
|
|
481
|
-
} catch (error) {
|
|
482
|
-
console.error('Failed to encode blocks for commit:', error);
|
|
483
|
-
blocks = new Uint8Array();
|
|
484
|
-
}
|
|
485
|
-
}
|
|
486
|
-
|
|
487
|
-
// Resolve prev commit and since (previous rev) when available
|
|
488
|
-
let prevCid: CID | null = null;
|
|
489
|
-
try {
|
|
490
|
-
if (commitData.prev) prevCid = CID.parse(String(commitData.prev));
|
|
491
|
-
} catch {}
|
|
492
|
-
|
|
493
|
-
let since: string | null = null;
|
|
494
|
-
try {
|
|
495
|
-
const db = drizzle(this.db);
|
|
496
|
-
if (prevCid) {
|
|
497
|
-
const prev = await db.select().from(commit_log).where(eq(commit_log.cid, prevCid.toString())).get();
|
|
498
|
-
since = prev?.rev ?? null;
|
|
499
|
-
} else {
|
|
500
|
-
const row = await db.select().from(commit_log).where(gt(commit_log.seq, 0 as any)).orderBy(desc(commit_log.seq)).limit(1).get();
|
|
501
|
-
since = row?.rev ?? null;
|
|
502
|
-
}
|
|
503
|
-
} catch {}
|
|
504
|
-
|
|
505
|
-
const message: CommitMessage = {
|
|
506
|
-
seq: event.seq,
|
|
507
|
-
rebase: false,
|
|
508
|
-
tooBig: false,
|
|
509
|
-
repo: event.did,
|
|
510
|
-
commit: CID.parse(event.commitCid),
|
|
511
|
-
prev: prevCid,
|
|
512
|
-
rev: event.rev,
|
|
513
|
-
since,
|
|
514
|
-
blocks: blocks || new Uint8Array(),
|
|
515
|
-
ops: event.ops || [],
|
|
516
|
-
blobs: [],
|
|
517
|
-
time: new Date(event.ts).toISOString(),
|
|
518
|
-
};
|
|
316
|
+
private broadcastIdentity(event: IdentityEvent): void {
|
|
317
|
+
broadcastIdentity(this.getSocketTargets(), event);
|
|
318
|
+
}
|
|
519
319
|
|
|
520
|
-
|
|
320
|
+
private broadcastAccount(event: AccountEvent): void {
|
|
321
|
+
broadcastAccount(this.getSocketTargets(), event);
|
|
521
322
|
}
|
|
522
323
|
|
|
523
|
-
/**
|
|
524
|
-
* Append commit event to buffer with backpressure
|
|
525
|
-
*/
|
|
526
324
|
private appendCommit(event: CommitEvent): void {
|
|
527
325
|
this.buffer.push(event);
|
|
528
326
|
|
|
529
|
-
// Implement backpressure: drop oldest events if buffer is full
|
|
530
327
|
if (this.buffer.length > this.maxWindow) {
|
|
531
328
|
const dropped = this.buffer.shift();
|
|
532
329
|
this.droppedFrameCount++;
|
|
533
|
-
console.warn(
|
|
534
|
-
|
|
535
|
-
|
|
330
|
+
console.warn(
|
|
331
|
+
`Dropped event seq=${dropped?.seq} due to backpressure (total dropped: ${this.droppedFrameCount})`,
|
|
332
|
+
);
|
|
536
333
|
this.notifyFramesDropped();
|
|
537
334
|
}
|
|
538
335
|
}
|
|
539
336
|
|
|
540
|
-
/**
|
|
541
|
-
* Notify all clients that frames were dropped
|
|
542
|
-
*/
|
|
543
337
|
private notifyFramesDropped(): void {
|
|
544
|
-
const
|
|
338
|
+
const bytes = encodeInfoFrame(
|
|
545
339
|
'FramesDropped',
|
|
546
340
|
`${this.droppedFrameCount} frame(s) dropped due to backpressure`,
|
|
547
341
|
);
|
|
548
|
-
const bytes = infoFrame.toFramedBytes();
|
|
549
342
|
|
|
550
343
|
for (const [id, client] of Array.from(this.clients.entries())) {
|
|
551
344
|
try {
|
|
@@ -556,9 +349,6 @@ export class Sequencer {
|
|
|
556
349
|
}
|
|
557
350
|
}
|
|
558
351
|
|
|
559
|
-
/**
|
|
560
|
-
* Get metrics
|
|
561
|
-
*/
|
|
562
352
|
getMetrics(): {
|
|
563
353
|
connectedClients: number;
|
|
564
354
|
bufferSize: number;
|
|
@@ -572,4 +362,90 @@ export class Sequencer {
|
|
|
572
362
|
droppedFrames: this.droppedFrameCount,
|
|
573
363
|
};
|
|
574
364
|
}
|
|
365
|
+
|
|
366
|
+
async webSocketMessage(ws: WebSocket, message: string | ArrayBuffer): Promise<void> {
|
|
367
|
+
const client = Array.from(this.clients.values()).find((c) => c.webSocket === ws);
|
|
368
|
+
if (!client) {
|
|
369
|
+
console.warn('Received message from unknown WebSocket');
|
|
370
|
+
return;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
try {
|
|
374
|
+
const data = typeof message === 'string' ? message : new TextDecoder().decode(message);
|
|
375
|
+
if (data === 'ping') ws.send('pong');
|
|
376
|
+
} catch (error) {
|
|
377
|
+
console.error('WebSocket message error:', error);
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
private async handleMetrics(): Promise<Response> {
|
|
382
|
+
const base = this.getMetrics();
|
|
383
|
+
let sockets: WebSocket[] = [];
|
|
384
|
+
try {
|
|
385
|
+
// workers-types WebSocket misses a few DOM-types members; the values
|
|
386
|
+
// are wire-compatible at runtime, so widen through unknown.
|
|
387
|
+
sockets = (this.state.getWebSockets?.() || []) as unknown as WebSocket[];
|
|
388
|
+
} catch (error) {
|
|
389
|
+
console.warn('Sequencer: getWebSockets failed in metrics:', error);
|
|
390
|
+
}
|
|
391
|
+
const clients = sockets.map((ws) => {
|
|
392
|
+
let attachment: WebSocketAttachment | undefined;
|
|
393
|
+
try {
|
|
394
|
+
attachment = (ws as HibernatableSocket).deserializeAttachment?.();
|
|
395
|
+
} catch (attachError) {
|
|
396
|
+
console.warn('Sequencer: deserializeAttachment failed in metrics:', attachError);
|
|
397
|
+
}
|
|
398
|
+
return { attachment: attachment ?? null };
|
|
399
|
+
});
|
|
400
|
+
const body = {
|
|
401
|
+
...base,
|
|
402
|
+
hibernatedSockets: sockets.length,
|
|
403
|
+
clients,
|
|
404
|
+
};
|
|
405
|
+
return new Response(JSON.stringify(body), {
|
|
406
|
+
headers: { 'Content-Type': 'application/json' },
|
|
407
|
+
});
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
async webSocketOpen(ws: WebSocket): Promise<void> {
|
|
411
|
+
console.log(JSON.stringify({ level: 'info', type: 'ws_open', ts: new Date().toISOString() }));
|
|
412
|
+
|
|
413
|
+
let cursor: number | undefined;
|
|
414
|
+
try {
|
|
415
|
+
const attachment = (ws as HibernatableSocket).deserializeAttachment?.();
|
|
416
|
+
if (attachment && typeof attachment.cursor === 'number') cursor = attachment.cursor;
|
|
417
|
+
} catch (attachError) {
|
|
418
|
+
console.warn('Sequencer: deserializeAttachment failed on open:', attachError);
|
|
419
|
+
}
|
|
420
|
+
if (cursor == null) {
|
|
421
|
+
const client = Array.from(this.clients.values()).find((c) => c.webSocket === ws);
|
|
422
|
+
if (client) cursor = client.cursor;
|
|
423
|
+
}
|
|
424
|
+
if (cursor && cursor > 0) {
|
|
425
|
+
await this.replayFromCursor(ws, cursor);
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
async webSocketClose(
|
|
430
|
+
ws: WebSocket,
|
|
431
|
+
code: number,
|
|
432
|
+
reason: string,
|
|
433
|
+
wasClean: boolean,
|
|
434
|
+
): Promise<void> {
|
|
435
|
+
const entry = Array.from(this.clients.entries()).find(([, c]) => c.webSocket === ws);
|
|
436
|
+
if (entry) {
|
|
437
|
+
this.clients.delete(entry[0]);
|
|
438
|
+
console.log(
|
|
439
|
+
`Client ${entry[0]} disconnected: code=${code} reason="${reason}" clean=${wasClean}`,
|
|
440
|
+
);
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
async webSocketError(ws: WebSocket, error: unknown): Promise<void> {
|
|
445
|
+
const entry = Array.from(this.clients.entries()).find(([, c]) => c.webSocket === ws);
|
|
446
|
+
if (entry) {
|
|
447
|
+
this.clients.delete(entry[0]);
|
|
448
|
+
console.error(`Client ${entry[0]} error:`, error);
|
|
449
|
+
}
|
|
450
|
+
}
|
|
575
451
|
}
|