@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,194 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Metrics Collection
|
|
3
|
+
* Tracks counters and histograms for monitoring
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { Env } from '../env';
|
|
7
|
+
import { drizzle } from 'drizzle-orm/d1';
|
|
8
|
+
|
|
9
|
+
export interface MetricCounter {
|
|
10
|
+
name: string;
|
|
11
|
+
value: number;
|
|
12
|
+
labels?: Record<string, string>;
|
|
13
|
+
timestamp: number;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface MetricHistogram {
|
|
17
|
+
name: string;
|
|
18
|
+
value: number;
|
|
19
|
+
labels?: Record<string, string>;
|
|
20
|
+
timestamp: number;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Simple in-memory metrics aggregator
|
|
25
|
+
* In production, consider using Workers Analytics Engine or D1
|
|
26
|
+
*/
|
|
27
|
+
class MetricsCollector {
|
|
28
|
+
private counters: Map<string, number> = new Map();
|
|
29
|
+
private histograms: Map<string, number[]> = new Map();
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Increment a counter
|
|
33
|
+
*/
|
|
34
|
+
increment(name: string, value: number = 1, labels?: Record<string, string>) {
|
|
35
|
+
const key = this.makeKey(name, labels);
|
|
36
|
+
const current = this.counters.get(key) || 0;
|
|
37
|
+
this.counters.set(key, current + value);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Record a histogram value (e.g., duration)
|
|
42
|
+
*/
|
|
43
|
+
observe(name: string, value: number, labels?: Record<string, string>) {
|
|
44
|
+
const key = this.makeKey(name, labels);
|
|
45
|
+
const values = this.histograms.get(key) || [];
|
|
46
|
+
values.push(value);
|
|
47
|
+
this.histograms.set(key, values);
|
|
48
|
+
|
|
49
|
+
// Keep only last 1000 values to prevent memory growth
|
|
50
|
+
if (values.length > 1000) {
|
|
51
|
+
values.shift();
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Get counter value
|
|
57
|
+
*/
|
|
58
|
+
getCounter(name: string, labels?: Record<string, string>): number {
|
|
59
|
+
const key = this.makeKey(name, labels);
|
|
60
|
+
return this.counters.get(key) || 0;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Get histogram statistics
|
|
65
|
+
*/
|
|
66
|
+
getHistogramStats(name: string, labels?: Record<string, string>): {
|
|
67
|
+
count: number;
|
|
68
|
+
sum: number;
|
|
69
|
+
avg: number;
|
|
70
|
+
min: number;
|
|
71
|
+
max: number;
|
|
72
|
+
p50: number;
|
|
73
|
+
p95: number;
|
|
74
|
+
p99: number;
|
|
75
|
+
} | null {
|
|
76
|
+
const key = this.makeKey(name, labels);
|
|
77
|
+
const values = this.histograms.get(key);
|
|
78
|
+
|
|
79
|
+
if (!values || values.length === 0) {
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const sorted = [...values].sort((a, b) => a - b);
|
|
84
|
+
const sum = sorted.reduce((a, b) => a + b, 0);
|
|
85
|
+
|
|
86
|
+
return {
|
|
87
|
+
count: sorted.length,
|
|
88
|
+
sum,
|
|
89
|
+
avg: sum / sorted.length,
|
|
90
|
+
min: sorted[0],
|
|
91
|
+
max: sorted[sorted.length - 1],
|
|
92
|
+
p50: this.percentile(sorted, 0.50),
|
|
93
|
+
p95: this.percentile(sorted, 0.95),
|
|
94
|
+
p99: this.percentile(sorted, 0.99),
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Get all metrics as JSON
|
|
100
|
+
*/
|
|
101
|
+
toJSON() {
|
|
102
|
+
const counters: Record<string, number> = {};
|
|
103
|
+
const histograms: Record<string, any> = {};
|
|
104
|
+
|
|
105
|
+
for (const [key, value] of this.counters.entries()) {
|
|
106
|
+
counters[key] = value;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
for (const [key] of this.histograms.entries()) {
|
|
110
|
+
histograms[key] = this.getHistogramStats(key.split('|')[0]);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return { counters, histograms };
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Reset all metrics
|
|
118
|
+
*/
|
|
119
|
+
reset() {
|
|
120
|
+
this.counters.clear();
|
|
121
|
+
this.histograms.clear();
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
private makeKey(name: string, labels?: Record<string, string>): string {
|
|
125
|
+
if (!labels || Object.keys(labels).length === 0) {
|
|
126
|
+
return name;
|
|
127
|
+
}
|
|
128
|
+
const labelStr = Object.entries(labels)
|
|
129
|
+
.sort(([a], [b]) => a.localeCompare(b))
|
|
130
|
+
.map(([k, v]) => `${k}=${v}`)
|
|
131
|
+
.join(',');
|
|
132
|
+
return `${name}|${labelStr}`;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
private percentile(sorted: number[], p: number): number {
|
|
136
|
+
const index = Math.ceil(sorted.length * p) - 1;
|
|
137
|
+
return sorted[Math.max(0, index)];
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Global metrics instance
|
|
142
|
+
export const metrics = new MetricsCollector();
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Common metric names
|
|
146
|
+
*/
|
|
147
|
+
export const METRICS = {
|
|
148
|
+
// Counters
|
|
149
|
+
REQUESTS_TOTAL: 'requests_total',
|
|
150
|
+
WRITES_TOTAL: 'writes_total',
|
|
151
|
+
RATE_LIMIT_HITS: 'rate_limit_hits',
|
|
152
|
+
WS_CLIENTS: 'ws_clients',
|
|
153
|
+
ERRORS_TOTAL: 'errors_total',
|
|
154
|
+
|
|
155
|
+
// Histograms
|
|
156
|
+
REQUEST_DURATION_MS: 'request_duration_ms',
|
|
157
|
+
DB_QUERY_DURATION_MS: 'db_query_duration_ms',
|
|
158
|
+
R2_OPERATION_DURATION_MS: 'r2_operation_duration_ms',
|
|
159
|
+
} as const;
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Track request metrics
|
|
163
|
+
*/
|
|
164
|
+
export function trackRequest(method: string, path: string, status: number, duration: number) {
|
|
165
|
+
metrics.increment(METRICS.REQUESTS_TOTAL, 1, { method, path, status: String(status) });
|
|
166
|
+
metrics.observe(METRICS.REQUEST_DURATION_MS, duration, { method, path });
|
|
167
|
+
|
|
168
|
+
if (status >= 400) {
|
|
169
|
+
const category = status >= 500 ? 'server' : 'client';
|
|
170
|
+
metrics.increment(METRICS.ERRORS_TOTAL, 1, { category, status: String(status) });
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Track write operations
|
|
176
|
+
*/
|
|
177
|
+
export function trackWrite(collection: string) {
|
|
178
|
+
metrics.increment(METRICS.WRITES_TOTAL, 1, { collection });
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Track rate limit hits
|
|
183
|
+
*/
|
|
184
|
+
export function trackRateLimitHit(ip: string) {
|
|
185
|
+
metrics.increment(METRICS.RATE_LIMIT_HITS, 1, { ip });
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Track WebSocket clients
|
|
190
|
+
*/
|
|
191
|
+
export function trackWebSocketClient(action: 'connect' | 'disconnect') {
|
|
192
|
+
const delta = action === 'connect' ? 1 : -1;
|
|
193
|
+
metrics.increment(METRICS.WS_CLIENTS, delta);
|
|
194
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { CID } from 'multiformats/cid';
|
|
2
|
+
import * as dagCbor from '@ipld/dag-cbor';
|
|
3
|
+
import type { Env } from '../../env';
|
|
4
|
+
import { drizzle } from 'drizzle-orm/d1';
|
|
5
|
+
import { blockstore } from '../../db/schema';
|
|
6
|
+
import { eq } from 'drizzle-orm';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Interface for reading blocks from storage
|
|
10
|
+
*/
|
|
11
|
+
export interface ReadableBlockstore {
|
|
12
|
+
get(cid: CID): Promise<Uint8Array | null>;
|
|
13
|
+
has(cid: CID): Promise<boolean>;
|
|
14
|
+
getMany(cids: CID[]): Promise<{ blocks: Map<string, Uint8Array>; missing: CID[] }>;
|
|
15
|
+
readObj<T>(cid: CID): Promise<T>;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Interface for writing blocks to storage
|
|
20
|
+
*/
|
|
21
|
+
export interface WritableBlockstore extends ReadableBlockstore {
|
|
22
|
+
put(cid: CID, bytes: Uint8Array): Promise<void>;
|
|
23
|
+
putMany(blocks: Map<CID, Uint8Array>): Promise<void>;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* D1-backed blockstore implementation
|
|
28
|
+
*/
|
|
29
|
+
export class D1Blockstore implements WritableBlockstore {
|
|
30
|
+
constructor(private env: Env) {}
|
|
31
|
+
|
|
32
|
+
async get(cid: CID): Promise<Uint8Array | null> {
|
|
33
|
+
const db = drizzle(this.env.DB);
|
|
34
|
+
const result = await db
|
|
35
|
+
.select()
|
|
36
|
+
.from(blockstore)
|
|
37
|
+
.where(eq(blockstore.cid, cid.toString()))
|
|
38
|
+
.get();
|
|
39
|
+
|
|
40
|
+
if (!result || !result.bytes) return null;
|
|
41
|
+
|
|
42
|
+
// Decode base64 string to Uint8Array
|
|
43
|
+
return Uint8Array.from(atob(result.bytes), c => c.charCodeAt(0));
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async has(cid: CID): Promise<boolean> {
|
|
47
|
+
const db = drizzle(this.env.DB);
|
|
48
|
+
const result = await db
|
|
49
|
+
.select({ cid: blockstore.cid })
|
|
50
|
+
.from(blockstore)
|
|
51
|
+
.where(eq(blockstore.cid, cid.toString()))
|
|
52
|
+
.get();
|
|
53
|
+
|
|
54
|
+
return result !== null;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
async getMany(cids: CID[]): Promise<{ blocks: Map<string, Uint8Array>; missing: CID[] }> {
|
|
58
|
+
const blocks = new Map<string, Uint8Array>();
|
|
59
|
+
const missing: CID[] = [];
|
|
60
|
+
|
|
61
|
+
for (const cid of cids) {
|
|
62
|
+
const bytes = await this.get(cid);
|
|
63
|
+
if (bytes) {
|
|
64
|
+
blocks.set(cid.toString(), bytes);
|
|
65
|
+
} else {
|
|
66
|
+
missing.push(cid);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return { blocks, missing };
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
async put(cid: CID, bytes: Uint8Array): Promise<void> {
|
|
74
|
+
const db = drizzle(this.env.DB);
|
|
75
|
+
|
|
76
|
+
// Encode Uint8Array to base64 string for storage
|
|
77
|
+
const base64 = btoa(String.fromCharCode(...Array.from(bytes)));
|
|
78
|
+
|
|
79
|
+
await db
|
|
80
|
+
.insert(blockstore)
|
|
81
|
+
.values({
|
|
82
|
+
cid: cid.toString(),
|
|
83
|
+
bytes: base64,
|
|
84
|
+
})
|
|
85
|
+
.onConflictDoNothing()
|
|
86
|
+
.run();
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
async putMany(blocks: Map<CID, Uint8Array>): Promise<void> {
|
|
90
|
+
for (const [cid, bytes] of Array.from(blocks)) {
|
|
91
|
+
await this.put(cid, bytes);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Read and decode a CBOR object from the blockstore
|
|
97
|
+
*/
|
|
98
|
+
async readObj<T>(cid: CID): Promise<T> {
|
|
99
|
+
const bytes = await this.get(cid);
|
|
100
|
+
if (!bytes) {
|
|
101
|
+
throw new Error(`Block not found: ${cid.toString()}`);
|
|
102
|
+
}
|
|
103
|
+
return dagCbor.decode(bytes) as T;
|
|
104
|
+
}
|
|
105
|
+
}
|