@alteran/astro 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (90) hide show
  1. package/README.md +558 -0
  2. package/index.d.ts +12 -0
  3. package/index.js +129 -0
  4. package/package.json +75 -0
  5. package/src/_worker.ts +44 -0
  6. package/src/app.ts +10 -0
  7. package/src/db/client.ts +7 -0
  8. package/src/db/dal.ts +97 -0
  9. package/src/db/repo.ts +135 -0
  10. package/src/db/schema.ts +89 -0
  11. package/src/db/seed.ts +14 -0
  12. package/src/env.d.ts +4 -0
  13. package/src/handlers/debug.ts +34 -0
  14. package/src/handlers/health.ts +6 -0
  15. package/src/handlers/ready.ts +14 -0
  16. package/src/handlers/root.ts +5 -0
  17. package/src/handlers/wellknown.ts +7 -0
  18. package/src/handlers/xrpc.repo.core.ts +57 -0
  19. package/src/handlers/xrpc.server.createSession.ts +25 -0
  20. package/src/handlers/xrpc.server.refreshSession.ts +43 -0
  21. package/src/lib/auth.ts +20 -0
  22. package/src/lib/blockstore-gc.ts +197 -0
  23. package/src/lib/cache.ts +236 -0
  24. package/src/lib/car-reader.ts +157 -0
  25. package/src/lib/commit-log-pruning.ts +76 -0
  26. package/src/lib/commit.ts +162 -0
  27. package/src/lib/config.ts +208 -0
  28. package/src/lib/errors.ts +142 -0
  29. package/src/lib/firehose/frames.ts +229 -0
  30. package/src/lib/firehose/parse.ts +82 -0
  31. package/src/lib/firehose/validation.ts +9 -0
  32. package/src/lib/handle.ts +90 -0
  33. package/src/lib/jwt.ts +150 -0
  34. package/src/lib/logger.ts +73 -0
  35. package/src/lib/metrics.ts +194 -0
  36. package/src/lib/mst/blockstore.ts +105 -0
  37. package/src/lib/mst/index.ts +3 -0
  38. package/src/lib/mst/mst.ts +643 -0
  39. package/src/lib/mst/util.ts +86 -0
  40. package/src/lib/ratelimit.ts +34 -0
  41. package/src/lib/sequencer.ts +10 -0
  42. package/src/lib/streaming-car.ts +137 -0
  43. package/src/lib/token-cleanup.ts +38 -0
  44. package/src/lib/tracing.ts +136 -0
  45. package/src/lib/util.ts +55 -0
  46. package/src/middleware.ts +102 -0
  47. package/src/pages/.well-known/atproto-did.ts +7 -0
  48. package/src/pages/.well-known/did.json.ts +76 -0
  49. package/src/pages/debug/blob/[...key].ts +27 -0
  50. package/src/pages/debug/db/bootstrap.ts +23 -0
  51. package/src/pages/debug/db/commits.ts +20 -0
  52. package/src/pages/debug/gc/blobs.ts +16 -0
  53. package/src/pages/debug/record.ts +33 -0
  54. package/src/pages/health.ts +68 -0
  55. package/src/pages/index.astro +57 -0
  56. package/src/pages/index.ts +2 -0
  57. package/src/pages/ready.ts +16 -0
  58. package/src/pages/xrpc/com.atproto.identity.resolveHandle.ts +38 -0
  59. package/src/pages/xrpc/com.atproto.identity.updateHandle.ts +45 -0
  60. package/src/pages/xrpc/com.atproto.repo.applyWrites.ts +73 -0
  61. package/src/pages/xrpc/com.atproto.repo.createRecord.ts +36 -0
  62. package/src/pages/xrpc/com.atproto.repo.deleteRecord.ts +36 -0
  63. package/src/pages/xrpc/com.atproto.repo.describeRepo.ts +51 -0
  64. package/src/pages/xrpc/com.atproto.repo.getRecord.ts +25 -0
  65. package/src/pages/xrpc/com.atproto.repo.listRecords.ts +57 -0
  66. package/src/pages/xrpc/com.atproto.repo.putRecord.ts +36 -0
  67. package/src/pages/xrpc/com.atproto.repo.uploadBlob.ts +53 -0
  68. package/src/pages/xrpc/com.atproto.server.createSession.ts +92 -0
  69. package/src/pages/xrpc/com.atproto.server.deleteSession.ts +25 -0
  70. package/src/pages/xrpc/com.atproto.server.describeServer.ts +17 -0
  71. package/src/pages/xrpc/com.atproto.server.getSession.ts +46 -0
  72. package/src/pages/xrpc/com.atproto.server.refreshSession.ts +67 -0
  73. package/src/pages/xrpc/com.atproto.sync.getBlocks.json.ts +16 -0
  74. package/src/pages/xrpc/com.atproto.sync.getBlocks.ts +56 -0
  75. package/src/pages/xrpc/com.atproto.sync.getCheckout.json.ts +20 -0
  76. package/src/pages/xrpc/com.atproto.sync.getCheckout.ts +43 -0
  77. package/src/pages/xrpc/com.atproto.sync.getHead.ts +11 -0
  78. package/src/pages/xrpc/com.atproto.sync.getLatestCommit.ts +42 -0
  79. package/src/pages/xrpc/com.atproto.sync.getRecord.ts +63 -0
  80. package/src/pages/xrpc/com.atproto.sync.getRepo.json.ts +20 -0
  81. package/src/pages/xrpc/com.atproto.sync.getRepo.range.ts +34 -0
  82. package/src/pages/xrpc/com.atproto.sync.getRepo.ts +17 -0
  83. package/src/pages/xrpc/com.atproto.sync.listBlobs.ts +53 -0
  84. package/src/pages/xrpc/com.atproto.sync.listRepos.ts +31 -0
  85. package/src/services/car.ts +249 -0
  86. package/src/services/r2-blob-store.ts +87 -0
  87. package/src/services/repo-manager.ts +339 -0
  88. package/src/shims/astro-internal-handler.d.ts +4 -0
  89. package/src/worker/sequencer.ts +563 -0
  90. package/types/env.d.ts +48 -0
@@ -0,0 +1,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
+ }
@@ -0,0 +1,3 @@
1
+ export { MST, Leaf, type NodeData, type TreeEntry, type NodeEntry, type MstOpts } from './mst';
2
+ export { D1Blockstore, type ReadableBlockstore, type WritableBlockstore } from './blockstore';
3
+ export * as util from './util';