@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
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import type { D1Database } from '@cloudflare/workers-types';
|
|
2
|
+
import {
|
|
3
|
+
encodeAccountFrame,
|
|
4
|
+
encodeCommitFrame,
|
|
5
|
+
encodeIdentityFrame,
|
|
6
|
+
encodeSyncFrame,
|
|
7
|
+
} from '../../lib/firehose/frames';
|
|
8
|
+
import type { Env } from '../../env';
|
|
9
|
+
import { toWireStatus } from '../../lib/account-state';
|
|
10
|
+
import type { AccountEvent, CommitEvent, IdentityEvent } from './types';
|
|
11
|
+
import { createCommitPayload } from './payload';
|
|
12
|
+
|
|
13
|
+
export async function broadcastCommit(
|
|
14
|
+
env: Env,
|
|
15
|
+
db: D1Database,
|
|
16
|
+
targets: WebSocket[],
|
|
17
|
+
event: CommitEvent,
|
|
18
|
+
): Promise<void> {
|
|
19
|
+
const message = await createCommitPayload(env, db, event);
|
|
20
|
+
const bytes = encodeCommitFrame(message);
|
|
21
|
+
|
|
22
|
+
console.log(
|
|
23
|
+
JSON.stringify({
|
|
24
|
+
level: 'info',
|
|
25
|
+
type: 'firehose_broadcast_start',
|
|
26
|
+
seq: event.seq,
|
|
27
|
+
clients: targets.length,
|
|
28
|
+
ops: (event.ops || []).length,
|
|
29
|
+
ts: new Date().toISOString(),
|
|
30
|
+
}),
|
|
31
|
+
);
|
|
32
|
+
|
|
33
|
+
let dropped = 0;
|
|
34
|
+
for (const ws of targets) {
|
|
35
|
+
try {
|
|
36
|
+
ws.send(bytes);
|
|
37
|
+
} catch {
|
|
38
|
+
dropped++;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
console.log(
|
|
43
|
+
JSON.stringify({
|
|
44
|
+
level: 'info',
|
|
45
|
+
type: 'firehose_broadcast_end',
|
|
46
|
+
seq: event.seq,
|
|
47
|
+
clients: targets.length,
|
|
48
|
+
dropped,
|
|
49
|
+
ts: new Date().toISOString(),
|
|
50
|
+
}),
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function broadcastIdentity(targets: WebSocket[], event: IdentityEvent): void {
|
|
55
|
+
const bytes = encodeIdentityFrame({
|
|
56
|
+
seq: event.seq,
|
|
57
|
+
did: event.did,
|
|
58
|
+
time: new Date(event.ts).toISOString(),
|
|
59
|
+
handle: event.handle,
|
|
60
|
+
});
|
|
61
|
+
for (const ws of targets) {
|
|
62
|
+
try {
|
|
63
|
+
ws.send(bytes);
|
|
64
|
+
} catch (sendError) {
|
|
65
|
+
console.warn('Sequencer: identity send failed:', sendError);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function broadcastAccount(targets: WebSocket[], event: AccountEvent): void {
|
|
71
|
+
const time = new Date(event.ts).toISOString();
|
|
72
|
+
const wire = toWireStatus(event.state);
|
|
73
|
+
// dag-cbor refuses to encode `undefined`, so build the payload conditionally
|
|
74
|
+
// rather than passing { status: undefined } when the FSM is active.
|
|
75
|
+
const base = { seq: event.seq, did: event.did, time, active: wire.active };
|
|
76
|
+
const accountBytes = encodeAccountFrame(
|
|
77
|
+
wire.status ? { ...base, status: wire.status } : base,
|
|
78
|
+
);
|
|
79
|
+
// Compatibility #sync emission for clients on the legacy topic.
|
|
80
|
+
const syncBytes = encodeSyncFrame(
|
|
81
|
+
wire.status ? { ...base, status: wire.status } : base,
|
|
82
|
+
);
|
|
83
|
+
for (const ws of targets) {
|
|
84
|
+
try {
|
|
85
|
+
ws.send(accountBytes);
|
|
86
|
+
ws.send(syncBytes);
|
|
87
|
+
} catch (sendError) {
|
|
88
|
+
console.warn('Sequencer: account/sync send failed:', sendError);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { CID } from 'multiformats/cid';
|
|
2
|
+
import type { RepoOp } from '../../lib/firehose/frames';
|
|
3
|
+
|
|
4
|
+
export function reviveCid(value: unknown): CID | null {
|
|
5
|
+
try {
|
|
6
|
+
if (value == null) return null;
|
|
7
|
+
const asCid = (CID as unknown as { asCID?: (v: unknown) => CID | null }).asCID?.(value);
|
|
8
|
+
if (asCid) return asCid;
|
|
9
|
+
if (typeof value === 'string') return CID.parse(value);
|
|
10
|
+
if (value && typeof value === 'object' && '/' in value) {
|
|
11
|
+
const link = (value as { '/'?: unknown })['/'];
|
|
12
|
+
if (typeof link === 'string') return CID.parse(link);
|
|
13
|
+
}
|
|
14
|
+
} catch {
|
|
15
|
+
// Fall through: caller treats null as unknown CID.
|
|
16
|
+
}
|
|
17
|
+
return null;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function reviveOps(ops: unknown): RepoOp[] | undefined {
|
|
21
|
+
if (!Array.isArray(ops)) return undefined;
|
|
22
|
+
return ops.map((raw) => {
|
|
23
|
+
const op = raw as { action: RepoOp['action']; path: string; cid?: unknown; prev?: unknown };
|
|
24
|
+
const prev = op.prev != null ? reviveCid(op.prev) ?? undefined : undefined;
|
|
25
|
+
return {
|
|
26
|
+
action: op.action,
|
|
27
|
+
path: op.path,
|
|
28
|
+
cid: reviveCid(op.cid),
|
|
29
|
+
...(prev ? { prev } : {}),
|
|
30
|
+
};
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function base64ToBytes(value: string): Uint8Array {
|
|
35
|
+
const binary = atob(value);
|
|
36
|
+
const bytes = new Uint8Array(binary.length);
|
|
37
|
+
for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);
|
|
38
|
+
return bytes;
|
|
39
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import type { D1Database } from '@cloudflare/workers-types';
|
|
2
|
+
import { drizzle } from 'drizzle-orm/d1';
|
|
3
|
+
import { commit_log } from '../../db/schema';
|
|
4
|
+
import { eq, gt, desc } from 'drizzle-orm';
|
|
5
|
+
import { CID } from 'multiformats/cid';
|
|
6
|
+
import type { CommitMessage } from '../../lib/firehose/frames';
|
|
7
|
+
import { encodeBlocksForCommit } from '../../services/car';
|
|
8
|
+
import type { Env } from '../../env';
|
|
9
|
+
import type { CommitEvent } from './types';
|
|
10
|
+
|
|
11
|
+
export async function createCommitPayload(
|
|
12
|
+
env: Env,
|
|
13
|
+
db: D1Database,
|
|
14
|
+
event: CommitEvent,
|
|
15
|
+
): Promise<CommitMessage> {
|
|
16
|
+
const commitData = JSON.parse(event.data) as { data?: string; prev?: string };
|
|
17
|
+
|
|
18
|
+
let blocks = event.blocks;
|
|
19
|
+
if (!blocks && event.ops && event.ops.length > 0) {
|
|
20
|
+
try {
|
|
21
|
+
const commitCid = CID.parse(event.commitCid);
|
|
22
|
+
const mstRoot = commitData.data ? CID.parse(commitData.data) : commitCid;
|
|
23
|
+
blocks = await encodeBlocksForCommit(env, commitCid, mstRoot, event.ops);
|
|
24
|
+
} catch (error) {
|
|
25
|
+
console.error('Failed to encode blocks for commit:', error);
|
|
26
|
+
blocks = new Uint8Array();
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
let prevCid: CID | null = null;
|
|
31
|
+
let prevDataCid: CID | null = null;
|
|
32
|
+
try {
|
|
33
|
+
if (commitData.prev) prevCid = CID.parse(String(commitData.prev));
|
|
34
|
+
} catch {
|
|
35
|
+
// Stays null; firehose consumers can tolerate a missing prev pointer.
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
let since: string | null = null;
|
|
39
|
+
try {
|
|
40
|
+
const drizzleDb = drizzle(db);
|
|
41
|
+
if (prevCid) {
|
|
42
|
+
const previous = await drizzleDb
|
|
43
|
+
.select()
|
|
44
|
+
.from(commit_log)
|
|
45
|
+
.where(eq(commit_log.cid, prevCid.toString()))
|
|
46
|
+
.get();
|
|
47
|
+
since = previous?.rev ?? null;
|
|
48
|
+
if (previous?.data) {
|
|
49
|
+
try {
|
|
50
|
+
prevDataCid = CID.parse(String(JSON.parse(previous.data).data));
|
|
51
|
+
} catch {
|
|
52
|
+
// Older rows may have a different shape; leave prevDataCid null.
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
} else {
|
|
56
|
+
const row = await drizzleDb
|
|
57
|
+
.select()
|
|
58
|
+
.from(commit_log)
|
|
59
|
+
.where(gt(commit_log.seq, 0))
|
|
60
|
+
.orderBy(desc(commit_log.seq))
|
|
61
|
+
.limit(1)
|
|
62
|
+
.get();
|
|
63
|
+
since = row?.rev ?? null;
|
|
64
|
+
}
|
|
65
|
+
} catch (error) {
|
|
66
|
+
console.warn('createCommitPayload: failed to resolve since/prev:', error);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return {
|
|
70
|
+
seq: event.seq,
|
|
71
|
+
rebase: false,
|
|
72
|
+
tooBig: false,
|
|
73
|
+
repo: event.did,
|
|
74
|
+
commit: CID.parse(event.commitCid),
|
|
75
|
+
prev: prevCid,
|
|
76
|
+
rev: event.rev,
|
|
77
|
+
since,
|
|
78
|
+
blocks: blocks || new Uint8Array(),
|
|
79
|
+
ops: event.ops || [],
|
|
80
|
+
blobs: [],
|
|
81
|
+
time: new Date(event.ts).toISOString(),
|
|
82
|
+
...(prevDataCid ? { prevData: prevDataCid } : {}),
|
|
83
|
+
};
|
|
84
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { RepoOp } from '../../lib/firehose/frames';
|
|
2
|
+
import type { AccountState } from '../../lib/account-state';
|
|
3
|
+
|
|
4
|
+
export interface Client {
|
|
5
|
+
webSocket: WebSocket;
|
|
6
|
+
id: string;
|
|
7
|
+
cursor: number;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface CommitEvent {
|
|
11
|
+
seq: number;
|
|
12
|
+
did: string;
|
|
13
|
+
commitCid: string;
|
|
14
|
+
rev: string;
|
|
15
|
+
data: string;
|
|
16
|
+
sig: string;
|
|
17
|
+
ts: number;
|
|
18
|
+
ops?: RepoOp[];
|
|
19
|
+
blocks?: Uint8Array;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface IdentityEvent {
|
|
23
|
+
seq: number;
|
|
24
|
+
did: string;
|
|
25
|
+
handle?: string;
|
|
26
|
+
ts: number;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface AccountEvent {
|
|
30
|
+
seq: number;
|
|
31
|
+
did: string;
|
|
32
|
+
state: AccountState;
|
|
33
|
+
ts: number;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export type SequencerEvent = CommitEvent | IdentityEvent | AccountEvent;
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import type { DurableObjectState } from '@cloudflare/workers-types';
|
|
2
|
+
import { encodeInfoFrame } from '../../lib/firehose/frames';
|
|
3
|
+
import { InvalidRequest } from '../../lib/errors';
|
|
4
|
+
|
|
5
|
+
export type WebSocketAttachment = {
|
|
6
|
+
id: string;
|
|
7
|
+
cursor: number;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export type HibernatableSocket = WebSocket & {
|
|
11
|
+
serializeAttachment?: (value: WebSocketAttachment) => void;
|
|
12
|
+
deserializeAttachment?: () => WebSocketAttachment | undefined;
|
|
13
|
+
accept?: () => void;
|
|
14
|
+
addEventListener?: (
|
|
15
|
+
type: 'message' | 'close',
|
|
16
|
+
handler: (event: MessageEvent | { code: number; reason: string }) => void,
|
|
17
|
+
) => void;
|
|
18
|
+
send?: (data: string | ArrayBuffer | Uint8Array) => void;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export type HibernatableState = DurableObjectState & {
|
|
22
|
+
acceptWebSocket?: (ws: WebSocket) => void;
|
|
23
|
+
getWebSockets?: () => WebSocket[];
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export type UpgradeContext = {
|
|
27
|
+
readonly state: HibernatableState;
|
|
28
|
+
readonly nextSeq: number;
|
|
29
|
+
readonly hibernate: boolean;
|
|
30
|
+
readonly onClient: (id: string, cursor: number, server: HibernatableSocket) => void;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
// Reject NaN / negative / non-integer cursors at the boundary. parseInt('abc')
|
|
34
|
+
// yields NaN, which would silently bypass `cursor > nextSeq - 1` (all NaN
|
|
35
|
+
// comparisons are false) and get persisted into the attachment.
|
|
36
|
+
export function parseCursorParam(raw: string | null): number {
|
|
37
|
+
if (raw === null || raw === '') return 0;
|
|
38
|
+
const parsed = Number(raw);
|
|
39
|
+
if (!Number.isInteger(parsed) || parsed < 0) {
|
|
40
|
+
throw new InvalidRequest('cursor must be a non-negative integer');
|
|
41
|
+
}
|
|
42
|
+
return parsed;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function handleUpgrade(
|
|
46
|
+
request: Request,
|
|
47
|
+
url: URL,
|
|
48
|
+
context: UpgradeContext,
|
|
49
|
+
): Response {
|
|
50
|
+
let cursor: number;
|
|
51
|
+
try {
|
|
52
|
+
cursor = parseCursorParam(url.searchParams.get('cursor'));
|
|
53
|
+
} catch (error) {
|
|
54
|
+
if (error instanceof InvalidRequest) {
|
|
55
|
+
return new Response(
|
|
56
|
+
JSON.stringify({ error: 'InvalidRequest', message: error.message }),
|
|
57
|
+
{ status: 400, headers: { 'Content-Type': 'application/json' } },
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
throw error;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const pair = new WebSocketPair();
|
|
64
|
+
const [client, server] = Object.values(pair) as [WebSocket, HibernatableSocket];
|
|
65
|
+
const id = crypto.randomUUID();
|
|
66
|
+
|
|
67
|
+
const requestedProtoHeader =
|
|
68
|
+
request.headers.get('Sec-WebSocket-Protocol') ||
|
|
69
|
+
request.headers.get('sec-websocket-protocol');
|
|
70
|
+
const requestedProtocol = requestedProtoHeader
|
|
71
|
+
? requestedProtoHeader.split(',').map((s) => s.trim()).filter(Boolean)[0] || undefined
|
|
72
|
+
: undefined;
|
|
73
|
+
|
|
74
|
+
if (cursor > context.nextSeq - 1) {
|
|
75
|
+
// Future cursor: send an info frame then 1008-close. Use the standard
|
|
76
|
+
// WebSocket accept path rather than hibernation for this short-lived case.
|
|
77
|
+
try {
|
|
78
|
+
server.accept?.();
|
|
79
|
+
} catch (acceptError) {
|
|
80
|
+
console.warn('Sequencer: server.accept failed for OutdatedCursor:', acceptError);
|
|
81
|
+
}
|
|
82
|
+
try {
|
|
83
|
+
server.send?.(encodeInfoFrame('OutdatedCursor', 'Cursor is ahead of current sequence'));
|
|
84
|
+
} catch (sendError) {
|
|
85
|
+
console.warn('Sequencer: send(info) failed for OutdatedCursor:', sendError);
|
|
86
|
+
}
|
|
87
|
+
try {
|
|
88
|
+
server.close(1008, 'OutdatedCursor');
|
|
89
|
+
} catch (closeError) {
|
|
90
|
+
console.warn('Sequencer: close failed for OutdatedCursor:', closeError);
|
|
91
|
+
}
|
|
92
|
+
return buildUpgradeResponse(client, requestedProtocol);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (context.hibernate) {
|
|
96
|
+
context.state.acceptWebSocket?.(server);
|
|
97
|
+
try {
|
|
98
|
+
server.serializeAttachment?.({ id, cursor });
|
|
99
|
+
} catch (attachError) {
|
|
100
|
+
console.warn('Sequencer: serializeAttachment failed:', attachError);
|
|
101
|
+
}
|
|
102
|
+
} else {
|
|
103
|
+
server.accept?.();
|
|
104
|
+
server.addEventListener?.('message', (event) => {
|
|
105
|
+
const evt = event as MessageEvent;
|
|
106
|
+
try {
|
|
107
|
+
const data =
|
|
108
|
+
typeof evt.data === 'string' ? evt.data : new TextDecoder().decode(evt.data);
|
|
109
|
+
if (data === 'ping') server.send?.('pong');
|
|
110
|
+
} catch (messageError) {
|
|
111
|
+
console.warn('Sequencer: ping handler failed:', messageError);
|
|
112
|
+
}
|
|
113
|
+
});
|
|
114
|
+
server.addEventListener?.('close', (event) => {
|
|
115
|
+
const cls = event as { code: number; reason: string };
|
|
116
|
+
console.log(`Client ${id} disconnected (std): code=${cls.code} reason=${cls.reason}`);
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
console.log(
|
|
121
|
+
JSON.stringify({
|
|
122
|
+
level: 'info',
|
|
123
|
+
type: 'ws_upgrade',
|
|
124
|
+
id,
|
|
125
|
+
path: url.pathname,
|
|
126
|
+
cursor,
|
|
127
|
+
protocol: requestedProtocol || null,
|
|
128
|
+
timestamp: new Date().toISOString(),
|
|
129
|
+
}),
|
|
130
|
+
);
|
|
131
|
+
|
|
132
|
+
context.onClient(id, cursor, server);
|
|
133
|
+
|
|
134
|
+
return buildUpgradeResponse(client, requestedProtocol);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function buildUpgradeResponse(client: WebSocket, requestedProtocol: string | undefined): Response {
|
|
138
|
+
const headers = new Headers();
|
|
139
|
+
if (requestedProtocol) headers.set('Sec-WebSocket-Protocol', requestedProtocol);
|
|
140
|
+
return new Response(null, { status: 101, webSocket: client, headers });
|
|
141
|
+
}
|