@alteran/astro 0.3.9 → 0.6.1

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 (150) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +19 -30
  3. package/index.js +34 -28
  4. package/migrations/0007_bored_spitfire.sql +26 -0
  5. package/migrations/0008_furry_ozymandias.sql +2 -0
  6. package/migrations/meta/0007_snapshot.json +534 -0
  7. package/migrations/meta/0008_snapshot.json +548 -0
  8. package/migrations/meta/_journal.json +14 -0
  9. package/package.json +10 -9
  10. package/src/app.ts +8 -4
  11. package/src/db/account.ts +25 -6
  12. package/src/db/client.ts +1 -1
  13. package/src/db/dal.ts +34 -23
  14. package/src/db/repo.ts +38 -38
  15. package/src/db/schema.ts +5 -1
  16. package/src/db/seed.ts +5 -13
  17. package/src/entrypoints/server.ts +2 -22
  18. package/src/handlers/debug.ts +1 -1
  19. package/src/handlers/ready.ts +1 -1
  20. package/src/handlers/root.ts +4 -4
  21. package/src/handlers/xrpc.server.refreshSession.ts +6 -6
  22. package/src/lib/account-state.ts +156 -0
  23. package/src/lib/actor.ts +29 -13
  24. package/src/lib/appview/auth-policy.ts +66 -0
  25. package/src/lib/appview/did-resolver.ts +233 -0
  26. package/src/lib/appview/proxy.ts +221 -0
  27. package/src/lib/appview/service-config.ts +61 -0
  28. package/src/lib/appview/service-jwt.ts +93 -0
  29. package/src/lib/appview/types.ts +25 -0
  30. package/src/lib/appview.ts +5 -532
  31. package/src/lib/auth-errors.ts +24 -0
  32. package/src/lib/auth.ts +63 -15
  33. package/src/lib/blockstore-gc.ts +6 -5
  34. package/src/lib/cache.ts +30 -4
  35. package/src/lib/chat.ts +20 -14
  36. package/src/lib/commit-log-pruning.ts +2 -2
  37. package/src/lib/commit.ts +26 -36
  38. package/src/lib/config.ts +26 -15
  39. package/src/lib/did-document.ts +32 -0
  40. package/src/lib/errors.ts +54 -0
  41. package/src/lib/feed.ts +18 -19
  42. package/src/lib/firehose/frames.ts +87 -47
  43. package/src/lib/firehose/validation.ts +3 -3
  44. package/src/lib/jwt.ts +85 -177
  45. package/src/lib/labeler.ts +43 -30
  46. package/src/lib/logger.ts +4 -0
  47. package/src/lib/mst/block-map.ts +172 -0
  48. package/src/lib/mst/blockstore.ts +56 -93
  49. package/src/lib/mst/index.ts +1 -0
  50. package/src/lib/mst/leaf.ts +25 -0
  51. package/src/lib/mst/mst.ts +81 -237
  52. package/src/lib/mst/serialize.ts +97 -0
  53. package/src/lib/mst/types.ts +21 -0
  54. package/src/lib/oauth/clients.ts +67 -0
  55. package/src/lib/oauth/dpop-errors.ts +15 -0
  56. package/src/lib/oauth/dpop.ts +150 -0
  57. package/src/lib/oauth/resource.ts +199 -0
  58. package/src/lib/oauth/store.ts +77 -0
  59. package/src/lib/preferences.ts +12 -37
  60. package/src/lib/ratelimit.ts +4 -4
  61. package/src/lib/refresh-session.ts +161 -0
  62. package/src/lib/relay.ts +10 -8
  63. package/src/lib/secrets.ts +6 -7
  64. package/src/lib/sequencer.ts +14 -5
  65. package/src/lib/service-auth.ts +184 -0
  66. package/src/lib/session-tokens.ts +28 -76
  67. package/src/lib/streaming-car.ts +3 -0
  68. package/src/lib/tracing.ts +4 -3
  69. package/src/lib/util.ts +65 -15
  70. package/src/middleware.ts +1 -1
  71. package/src/pages/.well-known/did.json.ts +27 -30
  72. package/src/pages/.well-known/oauth-authorization-server.ts +31 -0
  73. package/src/pages/.well-known/oauth-protected-resource.ts +22 -0
  74. package/src/pages/debug/blob/[...key].ts +2 -2
  75. package/src/pages/debug/db/bootstrap.ts +1 -1
  76. package/src/pages/debug/db/commits.ts +1 -1
  77. package/src/pages/debug/gc/blobs.ts +1 -1
  78. package/src/pages/debug/record.ts +1 -1
  79. package/src/pages/debug/sequencer.ts +28 -0
  80. package/src/pages/health.ts +4 -4
  81. package/src/pages/oauth/authorize.ts +78 -0
  82. package/src/pages/oauth/consent.ts +80 -0
  83. package/src/pages/oauth/par.ts +121 -0
  84. package/src/pages/oauth/token.ts +158 -0
  85. package/src/pages/ready.ts +2 -2
  86. package/src/pages/xrpc/[...nsid].ts +61 -0
  87. package/src/pages/xrpc/app.bsky.actor.getPreferences.ts +12 -13
  88. package/src/pages/xrpc/app.bsky.actor.putPreferences.ts +23 -23
  89. package/src/pages/xrpc/app.bsky.unspecced.getAgeAssuranceState.ts +9 -2
  90. package/src/pages/xrpc/chat.bsky.convo.getLog.ts +9 -2
  91. package/src/pages/xrpc/chat.bsky.convo.listConvos.ts +9 -2
  92. package/src/pages/xrpc/com.atproto.identity.getRecommendedDidCredentials.ts +43 -41
  93. package/src/pages/xrpc/com.atproto.identity.requestPlcOperationSignature.ts +10 -3
  94. package/src/pages/xrpc/com.atproto.identity.resolveHandle.ts +40 -9
  95. package/src/pages/xrpc/com.atproto.identity.signPlcOperation.ts +41 -29
  96. package/src/pages/xrpc/com.atproto.identity.submitPlcOperation.ts +20 -6
  97. package/src/pages/xrpc/com.atproto.identity.updateHandle.ts +1 -1
  98. package/src/pages/xrpc/com.atproto.repo.applyWrites.ts +101 -11
  99. package/src/pages/xrpc/com.atproto.repo.createRecord.ts +44 -14
  100. package/src/pages/xrpc/com.atproto.repo.deleteRecord.ts +41 -13
  101. package/src/pages/xrpc/com.atproto.repo.describeRepo.ts +2 -2
  102. package/src/pages/xrpc/com.atproto.repo.getRecord.ts +14 -1
  103. package/src/pages/xrpc/com.atproto.repo.listMissingBlobs.ts +14 -6
  104. package/src/pages/xrpc/com.atproto.repo.listRecords.ts +1 -1
  105. package/src/pages/xrpc/com.atproto.repo.putRecord.ts +42 -14
  106. package/src/pages/xrpc/com.atproto.repo.uploadBlob.ts +76 -15
  107. package/src/pages/xrpc/com.atproto.server.checkAccountStatus.ts +20 -8
  108. package/src/pages/xrpc/com.atproto.server.createSession.ts +32 -12
  109. package/src/pages/xrpc/com.atproto.server.describeServer.ts +1 -1
  110. package/src/pages/xrpc/com.atproto.server.getServiceAuth.ts +12 -5
  111. package/src/pages/xrpc/com.atproto.server.getSession.ts +22 -8
  112. package/src/pages/xrpc/com.atproto.server.refreshSession.ts +30 -72
  113. package/src/pages/xrpc/com.atproto.sync.getBlob.ts +72 -23
  114. package/src/pages/xrpc/com.atproto.sync.getCheckout.json.ts +1 -1
  115. package/src/pages/xrpc/com.atproto.sync.getCheckout.ts +1 -1
  116. package/src/pages/xrpc/com.atproto.sync.getHead.ts +7 -2
  117. package/src/pages/xrpc/com.atproto.sync.getLatestCommit.ts +1 -1
  118. package/src/pages/xrpc/com.atproto.sync.getRecord.ts +5 -27
  119. package/src/pages/xrpc/com.atproto.sync.getRepo.json.ts +1 -1
  120. package/src/pages/xrpc/com.atproto.sync.getRepo.ts +50 -5
  121. package/src/pages/xrpc/com.atproto.sync.getRepoStatus.ts +58 -0
  122. package/src/pages/xrpc/com.atproto.sync.listBlobs.ts +2 -2
  123. package/src/pages/xrpc/com.atproto.sync.listRepos.ts +5 -3
  124. package/src/services/car.ts +209 -57
  125. package/src/services/r2-blob-store.ts +4 -4
  126. package/src/services/repo/blockstore-ops.ts +29 -0
  127. package/src/services/repo/operations.ts +133 -0
  128. package/src/services/repo-manager.ts +203 -254
  129. package/src/worker/runtime.ts +56 -11
  130. package/src/worker/sequencer/broadcast.ts +91 -0
  131. package/src/worker/sequencer/cid-helpers.ts +39 -0
  132. package/src/worker/sequencer/payload.ts +84 -0
  133. package/src/worker/sequencer/types.ts +36 -0
  134. package/src/worker/sequencer/upgrade.ts +141 -0
  135. package/src/worker/sequencer.ts +264 -406
  136. package/types/env.d.ts +18 -6
  137. package/src/pages/xrpc/app.bsky.actor.getProfile.ts +0 -49
  138. package/src/pages/xrpc/app.bsky.actor.getProfiles.ts +0 -51
  139. package/src/pages/xrpc/app.bsky.feed.getActorFeeds.ts +0 -25
  140. package/src/pages/xrpc/app.bsky.feed.getAuthorFeed.ts +0 -42
  141. package/src/pages/xrpc/app.bsky.feed.getFeedGenerators.ts +0 -25
  142. package/src/pages/xrpc/app.bsky.feed.getPostThread.ts +0 -37
  143. package/src/pages/xrpc/app.bsky.feed.getPosts.ts +0 -26
  144. package/src/pages/xrpc/app.bsky.feed.getSuggestedFeeds.ts +0 -23
  145. package/src/pages/xrpc/app.bsky.feed.getTimeline.ts +0 -47
  146. package/src/pages/xrpc/app.bsky.graph.getFollowers.ts +0 -29
  147. package/src/pages/xrpc/app.bsky.graph.getFollows.ts +0 -29
  148. package/src/pages/xrpc/app.bsky.notification.getUnreadCount.ts +0 -20
  149. package/src/pages/xrpc/app.bsky.notification.listNotifications.ts +0 -27
  150. package/src/pages/xrpc/app.bsky.unspecced.getSuggestedFeeds.ts +0 -23
@@ -6,38 +6,54 @@ export interface LabelerViewOptions {
6
6
  detailed?: boolean;
7
7
  }
8
8
 
9
+ interface LabelerView {
10
+ uri: string;
11
+ cid: string;
12
+ creator: ReturnType<typeof buildProfileView>;
13
+ indexedAt: string;
14
+ likeCount: number;
15
+ viewer: Record<string, unknown>;
16
+ labels?: unknown;
17
+ policies?: { labelValues: unknown[]; labelValueDefinitions?: unknown[] };
18
+ reasonTypes?: unknown[];
19
+ subjectTypes?: unknown[];
20
+ subjectCollections?: unknown[];
21
+ }
22
+
9
23
  const LABELER_COLLECTION = 'app.bsky.labeler.service';
10
24
  const LABELER_RKEY = 'self';
11
25
 
12
26
  export async function getLabelerServiceViews(
13
27
  env: Env,
14
28
  dids: string[],
15
- options: LabelerViewOptions = {}
16
- ) {
29
+ options: LabelerViewOptions = {},
30
+ ): Promise<LabelerView[]> {
17
31
  const detailed = options.detailed ?? false;
18
32
  const primaryActor = await getPrimaryActor(env);
19
33
 
20
34
  const unique = Array.from(new Set(dids.map((did) => did.trim()).filter(Boolean)));
21
- const views: any[] = [];
35
+ const views: LabelerView[] = [];
22
36
 
23
37
  for (const did of unique) {
24
- if (did !== primaryActor.did) continue; // Single-user PDS only has local labeler data
38
+ // Single-user PDS only has local labeler data.
39
+ if (did !== primaryActor.did) continue;
25
40
 
26
41
  const uri = `at://${did}/${LABELER_COLLECTION}/${LABELER_RKEY}`;
27
42
  const row = await getRecord(env, uri);
28
43
  if (!row || !row.json) continue;
29
44
 
30
- let record: any;
45
+ let parsedRecord: Record<string, unknown>;
31
46
  try {
32
- record = JSON.parse(row.json);
47
+ const parsed = JSON.parse(row.json);
48
+ if (typeof parsed !== 'object' || parsed === null) continue;
49
+ parsedRecord = parsed as Record<string, unknown>;
33
50
  } catch {
34
51
  continue;
35
52
  }
36
53
 
37
- if (typeof record !== 'object' || record === null) continue;
38
-
39
- const indexedAt = typeof record.createdAt === 'string' ? record.createdAt : new Date().toISOString();
40
- const baseView: any = {
54
+ const indexedAt =
55
+ typeof parsedRecord.createdAt === 'string' ? parsedRecord.createdAt : new Date().toISOString();
56
+ const baseView: LabelerView = {
41
57
  uri,
42
58
  cid: row.cid,
43
59
  creator: buildProfileView(primaryActor),
@@ -47,17 +63,18 @@ export async function getLabelerServiceViews(
47
63
  };
48
64
 
49
65
  if (detailed) {
50
- const policies = normalizePolicies(record.policies);
51
66
  views.push({
52
67
  ...baseView,
53
- policies,
54
- reasonTypes: Array.isArray(record.reasonTypes) ? record.reasonTypes : undefined,
55
- subjectTypes: Array.isArray(record.subjectTypes) ? record.subjectTypes : undefined,
56
- subjectCollections: Array.isArray(record.subjectCollections) ? record.subjectCollections : undefined,
57
- labels: extractLabels(record.labels),
68
+ policies: normalizePolicies(parsedRecord.policies),
69
+ reasonTypes: Array.isArray(parsedRecord.reasonTypes) ? parsedRecord.reasonTypes : undefined,
70
+ subjectTypes: Array.isArray(parsedRecord.subjectTypes) ? parsedRecord.subjectTypes : undefined,
71
+ subjectCollections: Array.isArray(parsedRecord.subjectCollections)
72
+ ? parsedRecord.subjectCollections
73
+ : undefined,
74
+ labels: extractLabels(parsedRecord.labels),
58
75
  });
59
76
  } else {
60
- const labels = extractLabels(record.labels);
77
+ const labels = extractLabels(parsedRecord.labels);
61
78
  if (labels) baseView.labels = labels;
62
79
  views.push(baseView);
63
80
  }
@@ -66,24 +83,20 @@ export async function getLabelerServiceViews(
66
83
  return views;
67
84
  }
68
85
 
69
- function normalizePolicies(input: any) {
70
- if (input && typeof input === 'object') {
71
- const labelValues = Array.isArray(input.labelValues) ? input.labelValues : [];
72
- const labelValueDefinitions = Array.isArray(input.labelValueDefinitions)
73
- ? input.labelValueDefinitions
74
- : undefined;
86
+ function normalizePolicies(input: unknown): LabelerView['policies'] {
87
+ if (input && typeof input === 'object' && !Array.isArray(input)) {
88
+ const policies = input as { labelValues?: unknown; labelValueDefinitions?: unknown };
75
89
  return {
76
- labelValues,
77
- labelValueDefinitions,
90
+ labelValues: Array.isArray(policies.labelValues) ? policies.labelValues : [],
91
+ labelValueDefinitions: Array.isArray(policies.labelValueDefinitions)
92
+ ? policies.labelValueDefinitions
93
+ : undefined,
78
94
  };
79
95
  }
80
-
81
- return {
82
- labelValues: [],
83
- };
96
+ return { labelValues: [] };
84
97
  }
85
98
 
86
- function extractLabels(input: any) {
99
+ function extractLabels(input: unknown): unknown {
87
100
  if (!input) return undefined;
88
101
  if (Array.isArray(input)) return input.length ? input : undefined;
89
102
  if (typeof input === 'object') return input;
package/src/lib/logger.ts CHANGED
@@ -16,6 +16,10 @@ export interface LogContext {
16
16
  [key: string]: unknown;
17
17
  }
18
18
 
19
+ // Logger is a class because the inherited-context + child(context) pattern
20
+ // maps cleanly onto an immutable object whose state is exactly the
21
+ // accumulated context. Each child() returns a new Logger so request-scoped
22
+ // loggers don't leak fields back into the global instance.
19
23
  export class Logger {
20
24
  constructor(private context: LogContext = {}) {}
21
25
 
@@ -0,0 +1,172 @@
1
+ import { CID } from 'multiformats/cid';
2
+ import * as uint8arrays from 'uint8arrays';
3
+ import * as dagCbor from '@ipld/dag-cbor';
4
+ import { cidForCbor } from './util';
5
+
6
+ /**
7
+ * BlockMap - Efficient storage for IPLD blocks
8
+ * Maps CIDs to their encoded bytes
9
+ */
10
+ export class BlockMap implements Iterable<[cid: CID, bytes: Uint8Array]> {
11
+ private map: Map<string, Uint8Array> = new Map();
12
+
13
+ constructor(entries?: Iterable<readonly [cid: CID, bytes: Uint8Array]>) {
14
+ if (entries) {
15
+ for (const [cid, bytes] of entries) {
16
+ this.set(cid, bytes);
17
+ }
18
+ }
19
+ }
20
+
21
+ /**
22
+ * Add a value to the map (encodes to CBOR and generates CID)
23
+ */
24
+ async add(value: unknown): Promise<CID> {
25
+ const bytes = dagCbor.encode(value);
26
+ const cid = await cidForCbor(value);
27
+ this.set(cid, bytes);
28
+ return cid;
29
+ }
30
+
31
+ /**
32
+ * Set a CID->bytes mapping
33
+ */
34
+ set(cid: CID, bytes: Uint8Array): BlockMap {
35
+ this.map.set(cid.toString(), bytes);
36
+ return this;
37
+ }
38
+
39
+ /**
40
+ * Get bytes for a CID
41
+ */
42
+ get(cid: CID): Uint8Array | undefined {
43
+ return this.map.get(cid.toString());
44
+ }
45
+
46
+ /**
47
+ * Delete a CID from the map
48
+ */
49
+ delete(cid: CID): BlockMap {
50
+ this.map.delete(cid.toString());
51
+ return this;
52
+ }
53
+
54
+ /**
55
+ * Get multiple CIDs, returning both found blocks and missing CIDs
56
+ */
57
+ getMany(cids: CID[]): { blocks: BlockMap; missing: CID[] } {
58
+ const missing: CID[] = [];
59
+ const blocks = new BlockMap();
60
+ for (const cid of cids) {
61
+ const got = this.map.get(cid.toString());
62
+ if (got) {
63
+ blocks.set(cid, got);
64
+ } else {
65
+ missing.push(cid);
66
+ }
67
+ }
68
+ return { blocks, missing };
69
+ }
70
+
71
+ /**
72
+ * Check if a CID exists in the map
73
+ */
74
+ has(cid: CID): boolean {
75
+ return this.map.has(cid.toString());
76
+ }
77
+
78
+ /**
79
+ * Clear all blocks
80
+ */
81
+ clear(): void {
82
+ this.map.clear();
83
+ }
84
+
85
+ /**
86
+ * Iterate over all blocks
87
+ */
88
+ forEach(cb: (bytes: Uint8Array, cid: CID) => void): void {
89
+ for (const [cid, bytes] of this) {
90
+ cb(bytes, cid);
91
+ }
92
+ }
93
+
94
+ /**
95
+ * Get all CIDs
96
+ */
97
+ cids(): CID[] {
98
+ return Array.from(this.keys());
99
+ }
100
+
101
+ /**
102
+ * Add all blocks from another BlockMap
103
+ */
104
+ addMap(toAdd: BlockMap): BlockMap {
105
+ for (const [cid, bytes] of toAdd) {
106
+ this.set(cid, bytes);
107
+ }
108
+ return this;
109
+ }
110
+
111
+ /**
112
+ * Number of blocks in the map
113
+ */
114
+ get size(): number {
115
+ return this.map.size;
116
+ }
117
+
118
+ /**
119
+ * Total byte size of all blocks
120
+ */
121
+ get byteSize(): number {
122
+ let size = 0;
123
+ for (const bytes of this.values()) {
124
+ size += bytes.length;
125
+ }
126
+ return size;
127
+ }
128
+
129
+ /**
130
+ * Check if two BlockMaps are equal
131
+ */
132
+ equals(other: BlockMap): boolean {
133
+ if (this.size !== other.size) {
134
+ return false;
135
+ }
136
+ for (const [cid, bytes] of this) {
137
+ const otherBytes = other.get(cid);
138
+ if (!otherBytes) return false;
139
+ if (!uint8arrays.equals(bytes, otherBytes)) {
140
+ return false;
141
+ }
142
+ }
143
+ return true;
144
+ }
145
+
146
+ /**
147
+ * Iterator for CIDs
148
+ */
149
+ *keys(): Generator<CID, void, unknown> {
150
+ for (const cidStr of this.map.keys()) {
151
+ yield CID.parse(cidStr);
152
+ }
153
+ }
154
+
155
+ /**
156
+ * Iterator for bytes
157
+ */
158
+ *values(): Generator<Uint8Array, void, unknown> {
159
+ for (const bytes of this.map.values()) {
160
+ yield bytes;
161
+ }
162
+ }
163
+
164
+ /**
165
+ * Iterator for [CID, bytes] entries
166
+ */
167
+ *[Symbol.iterator](): Generator<[CID, Uint8Array], void, unknown> {
168
+ for (const [cidStr, bytes] of this.map.entries()) {
169
+ yield [CID.parse(cidStr), bytes];
170
+ }
171
+ }
172
+ }
@@ -1,4 +1,5 @@
1
1
  import { CID } from 'multiformats/cid';
2
+ import { errorMessage } from '../errors';
2
3
  import * as dagCbor from '@ipld/dag-cbor';
3
4
  import type { Env } from '../../env';
4
5
  import { drizzle } from 'drizzle-orm/d1';
@@ -30,40 +31,54 @@ export class D1Blockstore implements WritableBlockstore {
30
31
  constructor(private env: Env) {}
31
32
 
32
33
  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));
34
+ const row = await this.env.ALTERAN_DB.prepare(
35
+ `SELECT bytes FROM blockstore WHERE cid = ? LIMIT 1`
36
+ ).bind(cid.toString()).first();
37
+
38
+ if (!row) return null;
39
+ const base64 = (row as any).bytes as string | null | undefined;
40
+ if (!base64 || base64.length === 0) return null;
41
+ return Uint8Array.from(atob(base64), (c) => c.charCodeAt(0));
44
42
  }
45
43
 
46
44
  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;
45
+ // Treat rows with NULL or empty bytes as missing
46
+ const row = await this.env.ALTERAN_DB.prepare(
47
+ `SELECT bytes FROM blockstore WHERE cid = ? LIMIT 1`
48
+ ).bind(cid.toString()).first();
49
+
50
+ if (!row) return false;
51
+ const bytes = (row as any).bytes as string | null | undefined;
52
+ return typeof bytes === 'string' && bytes.length > 0;
55
53
  }
56
54
 
57
55
  async getMany(cids: CID[]): Promise<{ blocks: Map<string, Uint8Array>; missing: CID[] }> {
58
56
  const blocks = new Map<string, Uint8Array>();
59
57
  const missing: CID[] = [];
60
58
 
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);
59
+ if (cids.length === 0) return { blocks, missing };
60
+
61
+ // Fetch in chunks using a single IN-clause per chunk.
62
+ // Cloudflare D1 can error with "too many SQL variables" on large IN lists,
63
+ // so keep this conservatively small.
64
+ const BATCH = 50;
65
+ for (let i = 0; i < cids.length; i += BATCH) {
66
+ const chunk = cids.slice(i, i + BATCH);
67
+ const placeholders = new Array(chunk.length).fill('?').join(',');
68
+ const stmt = this.env.ALTERAN_DB.prepare(`SELECT cid, bytes FROM blockstore WHERE cid IN (${placeholders})`);
69
+ const binds = chunk.map((c) => c.toString());
70
+ const response = await stmt.bind(...binds).all();
71
+ const rows = (response.results ?? []) as Array<{ cid: string; bytes: string }>;
72
+ const got = new Set<string>();
73
+ for (const row of rows) {
74
+ got.add(row.cid);
75
+ if (row.bytes && row.bytes.length > 0) {
76
+ const u8 = Uint8Array.from(atob(row.bytes), (c) => c.charCodeAt(0));
77
+ blocks.set(row.cid, u8);
78
+ }
79
+ }
80
+ for (const c of chunk) {
81
+ if (!got.has(c.toString())) missing.push(c);
67
82
  }
68
83
  }
69
84
 
@@ -71,21 +86,7 @@ export class D1Blockstore implements WritableBlockstore {
71
86
  }
72
87
 
73
88
  async put(cid: CID, bytes: Uint8Array): Promise<void> {
74
- const db = drizzle(this.env.DB);
75
89
  const cidStr = cid.toString();
76
-
77
- // Check if block already exists - D1 has issues with ON CONFLICT DO NOTHING
78
- const existing = await db
79
- .select({ cid: blockstore.cid })
80
- .from(blockstore)
81
- .where(eq(blockstore.cid, cidStr))
82
- .get();
83
-
84
- if (existing) {
85
- // Block already exists, skip insert
86
- return;
87
- }
88
-
89
90
  // Encode Uint8Array to base64 string for storage. Chunk to avoid call-stack limits.
90
91
  let binary = '';
91
92
  const CHUNK_SIZE = 0x8000;
@@ -94,84 +95,44 @@ export class D1Blockstore implements WritableBlockstore {
94
95
  }
95
96
  const base64 = btoa(binary);
96
97
 
98
+ // Always upsert: replace rows with NULL/empty bytes
97
99
  try {
98
- await db
99
- .insert(blockstore)
100
- .values({
101
- cid: cidStr,
102
- bytes: base64,
103
- })
104
- .run();
105
- } catch (error: any) {
106
- // If we get a unique constraint error, another request inserted it - that's ok
107
- if (error?.message?.includes('UNIQUE constraint failed') ||
108
- error?.message?.includes('constraint failed')) {
109
- return;
110
- }
100
+ await this.env.ALTERAN_DB.prepare(
101
+ `INSERT OR REPLACE INTO blockstore (cid, bytes) VALUES (?, ?)`
102
+ ).bind(cidStr, base64).run();
103
+ } catch (error) {
111
104
  console.error(JSON.stringify({
112
105
  level: 'error',
113
106
  type: 'blockstore_put',
114
107
  cid: cidStr,
115
108
  size: bytes.byteLength,
116
- message: error?.message,
109
+ message: errorMessage(error),
117
110
  }));
118
111
  throw error;
119
112
  }
120
113
  }
121
114
 
122
115
  async putMany(blocks: Map<CID, Uint8Array>): Promise<void> {
123
- const db = drizzle(this.env.DB);
124
- const BATCH_SIZE = 100; // Insert 100 blocks at a time
116
+ const BATCH_SIZE = 100;
125
117
  const entries = Array.from(blocks.entries());
126
-
127
118
  for (let i = 0; i < entries.length; i += BATCH_SIZE) {
128
119
  const batch = entries.slice(i, i + BATCH_SIZE);
129
- const values = [];
130
-
120
+ const stmts = [] as Array<ReturnType<typeof this.env.ALTERAN_DB['prepare']>>;
131
121
  for (const [cid, bytes] of batch) {
132
122
  const cidStr = cid.toString();
133
-
134
- // Check if block already exists
135
- const existing = await db
136
- .select({ cid: blockstore.cid })
137
- .from(blockstore)
138
- .where(eq(blockstore.cid, cidStr))
139
- .get();
140
-
141
- if (existing) continue;
142
-
143
- // Encode to base64
144
123
  let binary = '';
145
124
  const CHUNK_SIZE = 0x8000;
146
125
  for (let j = 0; j < bytes.length; j += CHUNK_SIZE) {
147
126
  binary += String.fromCharCode(...bytes.subarray(j, j + CHUNK_SIZE));
148
127
  }
149
128
  const base64 = btoa(binary);
150
-
151
- values.push({ cid: cidStr, bytes: base64 });
129
+ stmts.push(
130
+ this.env.ALTERAN_DB.prepare(`INSERT OR REPLACE INTO blockstore (cid, bytes) VALUES (?, ?)`)
131
+ .bind(cidStr, base64)
132
+ );
152
133
  }
153
-
154
- if (values.length > 0) {
155
- try {
156
- await db.insert(blockstore).values(values).run();
157
- } catch (error: any) {
158
- // If batch insert fails, fall back to individual inserts
159
- for (const value of values) {
160
- try {
161
- await db.insert(blockstore).values(value).run();
162
- } catch (e: any) {
163
- if (!e?.message?.includes('UNIQUE constraint failed') &&
164
- !e?.message?.includes('constraint failed')) {
165
- console.error(JSON.stringify({
166
- level: 'error',
167
- type: 'blockstore_put_many',
168
- cid: value.cid,
169
- message: e?.message,
170
- }));
171
- }
172
- }
173
- }
174
- }
134
+ if (stmts.length > 0) {
135
+ await this.env.ALTERAN_DB.batch(stmts);
175
136
  }
176
137
  }
177
138
  }
@@ -182,6 +143,8 @@ export class D1Blockstore implements WritableBlockstore {
182
143
  async readObj<T>(cid: CID): Promise<T> {
183
144
  const bytes = await this.get(cid);
184
145
  if (!bytes) {
146
+ console.error('[Blockstore] Block not found:', cid.toString());
147
+ console.error('[Blockstore] Stack trace:', new Error().stack);
185
148
  throw new Error(`Block not found: ${cid.toString()}`);
186
149
  }
187
150
  return dagCbor.decode(bytes) as T;
@@ -1,3 +1,4 @@
1
1
  export { MST, Leaf, type NodeData, type TreeEntry, type NodeEntry, type MstOpts } from './mst';
2
2
  export { D1Blockstore, type ReadableBlockstore, type WritableBlockstore } from './blockstore';
3
+ export { BlockMap } from './block-map';
3
4
  export * as util from './util';
@@ -0,0 +1,25 @@
1
+ import type { CID } from 'multiformats/cid';
2
+ import type { MST } from './mst';
3
+ import type { NodeEntry } from './types';
4
+
5
+ export class Leaf {
6
+ constructor(
7
+ public key: string,
8
+ public value: CID,
9
+ ) {}
10
+
11
+ isTree(): this is MST {
12
+ return false;
13
+ }
14
+
15
+ isLeaf(): this is Leaf {
16
+ return true;
17
+ }
18
+
19
+ equals(entry: NodeEntry): boolean {
20
+ if (entry.isLeaf()) {
21
+ return this.key === entry.key && this.value.equals(entry.value);
22
+ }
23
+ return false;
24
+ }
25
+ }