@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.
- 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/client.ts +1 -1
- package/src/db/dal.ts +34 -23
- package/src/db/repo.ts +38 -38
- package/src/db/schema.ts +5 -1
- package/src/db/seed.ts +5 -13
- package/src/entrypoints/server.ts +2 -22
- package/src/handlers/debug.ts +1 -1
- package/src/handlers/ready.ts +1 -1
- package/src/handlers/root.ts +4 -4
- package/src/handlers/xrpc.server.refreshSession.ts +6 -6
- package/src/lib/account-state.ts +156 -0
- package/src/lib/actor.ts +29 -13
- 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 +6 -5
- package/src/lib/cache.ts +30 -4
- package/src/lib/chat.ts +20 -14
- package/src/lib/commit-log-pruning.ts +2 -2
- 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 +12 -37
- package/src/lib/ratelimit.ts +4 -4
- 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 +14 -5
- 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/blob/[...key].ts +2 -2
- package/src/pages/debug/db/bootstrap.ts +1 -1
- package/src/pages/debug/db/commits.ts +1 -1
- package/src/pages/debug/gc/blobs.ts +1 -1
- package/src/pages/debug/record.ts +1 -1
- package/src/pages/debug/sequencer.ts +28 -0
- package/src/pages/health.ts +4 -4
- 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/ready.ts +2 -2
- 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 +32 -12
- 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 +72 -23
- 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 +2 -2
- package/src/pages/xrpc/com.atproto.sync.listRepos.ts +5 -3
- package/src/services/car.ts +209 -57
- package/src/services/r2-blob-store.ts +4 -4
- package/src/services/repo/blockstore-ops.ts +29 -0
- package/src/services/repo/operations.ts +133 -0
- package/src/services/repo-manager.ts +203 -254
- package/src/worker/runtime.ts +56 -11
- 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 +264 -406
- package/types/env.d.ts +18 -6
- 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.getSuggestedFeeds.ts +0 -23
- 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/pages/xrpc/app.bsky.unspecced.getSuggestedFeeds.ts +0 -23
package/src/services/car.ts
CHANGED
|
@@ -7,6 +7,7 @@ import { CID } from 'multiformats/cid';
|
|
|
7
7
|
import * as dagCbor from '@ipld/dag-cbor';
|
|
8
8
|
import { sha256 } from 'multiformats/hashes/sha2';
|
|
9
9
|
import { MST, Leaf, D1Blockstore } from '../lib/mst';
|
|
10
|
+
import { NotFound } from '../lib/errors';
|
|
10
11
|
|
|
11
12
|
export type CarSnapshot = {
|
|
12
13
|
bytes: Uint8Array;
|
|
@@ -23,7 +24,7 @@ export async function encodeRecordBlock(value: unknown) {
|
|
|
23
24
|
|
|
24
25
|
export async function buildRepoCar(env: Env, did: string): Promise<CarSnapshot> {
|
|
25
26
|
// Prefer the latest signed commit from commit_log (authoritative root)
|
|
26
|
-
const db = drizzle(env.
|
|
27
|
+
const db = drizzle(env.ALTERAN_DB);
|
|
27
28
|
const tip = await db.select().from(commit_log).orderBy(desc(commit_log.seq)).limit(1).get();
|
|
28
29
|
|
|
29
30
|
if (tip) {
|
|
@@ -67,17 +68,22 @@ export async function buildRepoCar(env: Env, did: string): Promise<CarSnapshot>
|
|
|
67
68
|
};
|
|
68
69
|
|
|
69
70
|
const mstRoot = CID.parse(String(parsed.data));
|
|
70
|
-
// 1) Add all MST node blocks
|
|
71
|
-
await
|
|
71
|
+
// 1) Add all MST node blocks (batched, non-recursive) and collect leaf CIDs
|
|
72
|
+
const { mstBlocks, leafCids } = await collectMstBfs(blockstore, mstRoot);
|
|
73
|
+
for (const [cid, bytes] of mstBlocks) {
|
|
74
|
+
const k = cid.toString();
|
|
75
|
+
if (seen.has(k)) continue;
|
|
76
|
+
seen.add(k);
|
|
77
|
+
blocks.push({ cid, bytes });
|
|
78
|
+
}
|
|
72
79
|
|
|
73
|
-
// 2) Add
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
console.warn('Snapshot: failed traversing MST leaves:', e);
|
|
80
|
+
// 2) Add record leaf blocks by batched fetch
|
|
81
|
+
const leafFetched = await blockstore.getMany(leafCids);
|
|
82
|
+
for (const [cidStr, bytes] of leafFetched.blocks.entries()) {
|
|
83
|
+
const cid = CID.parse(cidStr);
|
|
84
|
+
if (seen.has(cidStr)) continue;
|
|
85
|
+
seen.add(cidStr);
|
|
86
|
+
blocks.push({ cid, bytes });
|
|
81
87
|
}
|
|
82
88
|
|
|
83
89
|
const bytes = encodeCar([commitCid], blocks);
|
|
@@ -88,24 +94,12 @@ export async function buildRepoCar(env: Env, did: string): Promise<CarSnapshot>
|
|
|
88
94
|
console.warn('Failed to reconstruct signed commit from tip; falling back to snapshot:', e);
|
|
89
95
|
}
|
|
90
96
|
}
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
const rows = await listRecords(env);
|
|
94
|
-
const blocks: { cid: CID; bytes: Uint8Array }[] = [];
|
|
95
|
-
for (const r of rows) {
|
|
96
|
-
if (!r.uri.startsWith(`at://${did}/`)) continue;
|
|
97
|
-
const value = JSON.parse(r.json);
|
|
98
|
-
const block = await encodeRecordBlock(value);
|
|
99
|
-
blocks.push(block);
|
|
100
|
-
}
|
|
101
|
-
const commitObj = { type: 'commit', did, records: blocks.map((b) => b.cid.toString()).sort() };
|
|
102
|
-
const commit = await encodeRecordBlock(commitObj);
|
|
103
|
-
const bytes = encodeCar([commit.cid], [...blocks, commit]);
|
|
104
|
-
return { bytes, root: commit.cid, blocks: [...blocks, commit] };
|
|
97
|
+
// No authoritative head to build from
|
|
98
|
+
throw new NotFound('RepoNotFound');
|
|
105
99
|
}
|
|
106
100
|
|
|
107
101
|
export async function buildRepoCarRange(env: Env, fromSeq: number, toSeq: number): Promise<CarSnapshot> {
|
|
108
|
-
const db = drizzle(env.
|
|
102
|
+
const db = drizzle(env.ALTERAN_DB);
|
|
109
103
|
const rows = await db.select().from(commit_log).where(and(gte(commit_log.seq, fromSeq), lte(commit_log.seq, toSeq))).all();
|
|
110
104
|
const blocks: { cid: CID; bytes: Uint8Array }[] = [];
|
|
111
105
|
for (const r of rows) {
|
|
@@ -179,6 +173,7 @@ export async function encodeBlocksForCommit(
|
|
|
179
173
|
commitCid: CID,
|
|
180
174
|
mstRoot: CID,
|
|
181
175
|
ops: Array<{ path: string; cid: CID | null }>,
|
|
176
|
+
newMstBlocks?: Array<[CID, Uint8Array]>,
|
|
182
177
|
): Promise<Uint8Array> {
|
|
183
178
|
const blockstore = new D1Blockstore(env);
|
|
184
179
|
const blocks: { cid: CID; bytes: Uint8Array }[] = [];
|
|
@@ -189,18 +184,60 @@ export async function encodeBlocksForCommit(
|
|
|
189
184
|
const cidStr = cid.toString();
|
|
190
185
|
if (seen.has(cidStr)) return;
|
|
191
186
|
seen.add(cidStr);
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
187
|
+
let bytes = await blockstore.get(cid);
|
|
188
|
+
if (!bytes) {
|
|
189
|
+
// Attempt to reconstruct commit block from commit_log if this is the commit cid
|
|
190
|
+
if (cidStr === commitCid.toString()) {
|
|
191
|
+
try {
|
|
192
|
+
const row = await (env.ALTERAN_DB as any)
|
|
193
|
+
.prepare('SELECT data, sig FROM commit_log WHERE cid = ? LIMIT 1')
|
|
194
|
+
.bind(cidStr)
|
|
195
|
+
.first();
|
|
196
|
+
if (row && row.data && row.sig) {
|
|
197
|
+
const parsed = JSON.parse(String(row.data));
|
|
198
|
+
const sigBin = atob(String(row.sig));
|
|
199
|
+
const sig = new Uint8Array(sigBin.length);
|
|
200
|
+
for (let i = 0; i < sigBin.length; i++) sig[i] = sigBin.charCodeAt(i);
|
|
201
|
+
const signedCommit = {
|
|
202
|
+
did: String(parsed.did),
|
|
203
|
+
version: Number(parsed.version),
|
|
204
|
+
data: CID.parse(String(parsed.data)),
|
|
205
|
+
rev: String(parsed.rev),
|
|
206
|
+
prev: parsed.prev ? CID.parse(String(parsed.prev)) : null,
|
|
207
|
+
sig,
|
|
208
|
+
} as const;
|
|
209
|
+
bytes = dagCbor.encode(signedCommit);
|
|
210
|
+
}
|
|
211
|
+
} catch (e) {
|
|
212
|
+
console.warn('Failed to reconstruct commit block from commit_log:', e);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
196
215
|
}
|
|
216
|
+
if (bytes) blocks.push({ cid, bytes });
|
|
197
217
|
};
|
|
198
218
|
|
|
199
219
|
// 1. Add commit block
|
|
200
220
|
await addBlock(commitCid);
|
|
201
221
|
|
|
202
|
-
// 2. Add MST nodes
|
|
203
|
-
|
|
222
|
+
// 2. Add MST nodes
|
|
223
|
+
if (newMstBlocks && newMstBlocks.length > 0) {
|
|
224
|
+
// Prefer the exact set of MST nodes touched by this commit
|
|
225
|
+
for (const [cid, bytes] of newMstBlocks) {
|
|
226
|
+
const cidStr = cid.toString();
|
|
227
|
+
if (seen.has(cidStr)) continue;
|
|
228
|
+
seen.add(cidStr);
|
|
229
|
+
blocks.push({ cid, bytes });
|
|
230
|
+
}
|
|
231
|
+
} else {
|
|
232
|
+
// Fallback: add MST nodes by batched BFS
|
|
233
|
+
const { mstBlocks } = await collectMstBfs(blockstore, mstRoot);
|
|
234
|
+
for (const [cid, bytes] of mstBlocks) {
|
|
235
|
+
const k = cid.toString();
|
|
236
|
+
if (seen.has(k)) continue;
|
|
237
|
+
seen.add(k);
|
|
238
|
+
blocks.push({ cid, bytes });
|
|
239
|
+
}
|
|
240
|
+
}
|
|
204
241
|
|
|
205
242
|
// 3. Add record blocks for all operations
|
|
206
243
|
for (const op of ops) {
|
|
@@ -213,37 +250,152 @@ export async function encodeBlocksForCommit(
|
|
|
213
250
|
return encodeCar([commitCid], blocks);
|
|
214
251
|
}
|
|
215
252
|
|
|
253
|
+
/**
|
|
254
|
+
* Build a CAR proving existence or non-existence of a record at collection/rkey
|
|
255
|
+
* Root is the latest signed commit block; includes MST path nodes and record block if present.
|
|
256
|
+
*/
|
|
257
|
+
export async function buildRecordProofCar(
|
|
258
|
+
env: Env,
|
|
259
|
+
did: string,
|
|
260
|
+
collection: string,
|
|
261
|
+
rkey: string,
|
|
262
|
+
): Promise<{ bytes: Uint8Array }> {
|
|
263
|
+
const db = drizzle(env.ALTERAN_DB);
|
|
264
|
+
const tip = await db.select().from(commit_log).orderBy(desc(commit_log.seq)).limit(1).get();
|
|
265
|
+
if (!tip) {
|
|
266
|
+
throw new NotFound('HeadNotFound');
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// Reconstruct signed commit block and CID
|
|
270
|
+
const parsed = JSON.parse(tip.data as any);
|
|
271
|
+
const prevStr = parsed.prev ?? null;
|
|
272
|
+
const commitObj = {
|
|
273
|
+
did: String(parsed.did),
|
|
274
|
+
version: Number(parsed.version),
|
|
275
|
+
data: CID.parse(String(parsed.data)),
|
|
276
|
+
rev: String(parsed.rev),
|
|
277
|
+
prev: prevStr ? CID.parse(String(prevStr)) : null,
|
|
278
|
+
sig: (() => {
|
|
279
|
+
const bin = atob(String(tip.sig));
|
|
280
|
+
const u8 = new Uint8Array(bin.length);
|
|
281
|
+
for (let i = 0; i < bin.length; i++) u8[i] = bin.charCodeAt(i);
|
|
282
|
+
return u8;
|
|
283
|
+
})(),
|
|
284
|
+
} as const;
|
|
285
|
+
const commitBytes = dagCbor.encode(commitObj);
|
|
286
|
+
const hash = await sha256.digest(commitBytes);
|
|
287
|
+
const commitCid = CID.createV1(dagCbor.code, hash);
|
|
288
|
+
|
|
289
|
+
// Walk MST path to the target key
|
|
290
|
+
const blockstore = new D1Blockstore(env);
|
|
291
|
+
const mstRoot = CID.parse(String(parsed.data));
|
|
292
|
+
const key = `${collection}/${rkey}`;
|
|
293
|
+
const pathBlocks: Array<{ cid: CID; bytes: Uint8Array }> = [];
|
|
294
|
+
let cursor: CID | null = mstRoot;
|
|
295
|
+
let recordCid: CID | null = null;
|
|
296
|
+
|
|
297
|
+
while (cursor) {
|
|
298
|
+
const bytes = await blockstore.get(cursor);
|
|
299
|
+
if (!bytes) break;
|
|
300
|
+
pathBlocks.push({ cid: cursor, bytes });
|
|
301
|
+
try {
|
|
302
|
+
const node: any = dagCbor.decode(bytes);
|
|
303
|
+
// Reconstruct ordered entries: [l? subtree], then (leaf, subtree?)*
|
|
304
|
+
type Entry = { kind: 'tree'; cid: CID } | { kind: 'leaf'; key: string; value: CID };
|
|
305
|
+
const entries: Entry[] = [];
|
|
306
|
+
if (node?.l) entries.push({ kind: 'tree', cid: CID.asCID(node.l) ?? CID.parse(String(node.l)) });
|
|
307
|
+
let lastKey = '';
|
|
308
|
+
for (const e of (node?.e ?? [])) {
|
|
309
|
+
const keyStr = new TextDecoder('ascii').decode(e.k as Uint8Array);
|
|
310
|
+
const fullKey = lastKey.slice(0, e.p as number) + keyStr;
|
|
311
|
+
entries.push({ kind: 'leaf', key: fullKey, value: CID.asCID(e.v) ?? CID.parse(String(e.v)) });
|
|
312
|
+
lastKey = fullKey;
|
|
313
|
+
if (e.t) entries.push({ kind: 'tree', cid: CID.asCID(e.t) ?? CID.parse(String(e.t)) });
|
|
314
|
+
}
|
|
315
|
+
// Find first leaf >= key
|
|
316
|
+
let index = entries.findIndex((en) => en.kind === 'leaf' && (en as any).key >= key);
|
|
317
|
+
if (index < 0) index = entries.length;
|
|
318
|
+
const found = entries[index];
|
|
319
|
+
if (found && found.kind === 'leaf' && (found as any).key === key) {
|
|
320
|
+
recordCid = found.value;
|
|
321
|
+
break;
|
|
322
|
+
}
|
|
323
|
+
const prev = entries[index - 1];
|
|
324
|
+
if (prev && prev.kind === 'tree') {
|
|
325
|
+
cursor = prev.cid;
|
|
326
|
+
continue;
|
|
327
|
+
}
|
|
328
|
+
// Not found and no subtree to descend
|
|
329
|
+
break;
|
|
330
|
+
} catch {
|
|
331
|
+
break;
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// Assemble CAR: commit as root; include path nodes; include record block if present
|
|
336
|
+
const blocks: { cid: CID; bytes: Uint8Array }[] = [{ cid: commitCid, bytes: commitBytes }];
|
|
337
|
+
const seen = new Set<string>([commitCid.toString()]);
|
|
338
|
+
for (const b of pathBlocks) {
|
|
339
|
+
const s = b.cid.toString();
|
|
340
|
+
if (!seen.has(s)) { seen.add(s); blocks.push(b); }
|
|
341
|
+
}
|
|
342
|
+
if (recordCid) {
|
|
343
|
+
const bytes = await blockstore.get(recordCid);
|
|
344
|
+
if (bytes) blocks.push({ cid: recordCid, bytes });
|
|
345
|
+
}
|
|
346
|
+
const bytes = encodeCar([commitCid], blocks);
|
|
347
|
+
return { bytes };
|
|
348
|
+
}
|
|
349
|
+
|
|
216
350
|
/**
|
|
217
351
|
* Recursively add all MST node blocks
|
|
218
352
|
*/
|
|
219
|
-
async function
|
|
353
|
+
async function collectMstBfs(
|
|
220
354
|
blockstore: D1Blockstore,
|
|
221
355
|
rootCid: CID,
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
const
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
356
|
+
): Promise<{ mstBlocks: Array<[CID, Uint8Array]>; leafCids: CID[] }> {
|
|
357
|
+
const mstBlocks: Array<[CID, Uint8Array]> = [];
|
|
358
|
+
const leafCids: CID[] = [];
|
|
359
|
+
const seen = new Set<string>();
|
|
360
|
+
|
|
361
|
+
let toFetch: CID[] = [rootCid];
|
|
362
|
+
const BATCH = 200;
|
|
363
|
+
|
|
364
|
+
while (toFetch.length > 0) {
|
|
365
|
+
const chunk = toFetch.slice(0, BATCH);
|
|
366
|
+
toFetch = toFetch.slice(BATCH);
|
|
367
|
+
const { blocks, missing } = await blockstore.getMany(chunk);
|
|
368
|
+
// Push node blocks we found
|
|
369
|
+
for (const [cidStr, bytes] of blocks.entries()) {
|
|
370
|
+
if (seen.has(cidStr)) continue;
|
|
371
|
+
seen.add(cidStr);
|
|
372
|
+
mstBlocks.push([CID.parse(cidStr), bytes]);
|
|
373
|
+
}
|
|
374
|
+
// Decode nodes to collect children and leaves
|
|
375
|
+
for (const [cidStr, bytes] of blocks.entries()) {
|
|
376
|
+
try {
|
|
377
|
+
const node: any = dagCbor.decode(bytes);
|
|
378
|
+
const l = node?.l ? (CID.asCID(node.l) ?? CID.parse(String(node.l))) : null;
|
|
379
|
+
if (l) {
|
|
380
|
+
const key = l.toString();
|
|
381
|
+
if (!seen.has(key)) toFetch.push(l);
|
|
382
|
+
}
|
|
383
|
+
const entries: any[] = Array.isArray(node?.e) ? node.e : [];
|
|
384
|
+
for (const e of entries) {
|
|
385
|
+
const v = CID.asCID(e?.v) ?? CID.parse(String(e?.v));
|
|
386
|
+
if (v) leafCids.push(v);
|
|
387
|
+
const t = e?.t ? (CID.asCID(e.t) ?? CID.parse(String(e.t))) : null;
|
|
388
|
+
if (t) {
|
|
389
|
+
const key = t.toString();
|
|
390
|
+
if (!seen.has(key)) toFetch.push(t);
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
} catch (error) {
|
|
394
|
+
console.warn('collectMstBfs: failed to decode node', cidStr, error);
|
|
244
395
|
}
|
|
245
396
|
}
|
|
246
|
-
|
|
247
|
-
console.error('Error traversing MST:', error);
|
|
397
|
+
// Ignore missing here; caller might not need full tree for snapshots
|
|
248
398
|
}
|
|
399
|
+
|
|
400
|
+
return { mstBlocks, leafCids };
|
|
249
401
|
}
|
|
@@ -69,19 +69,19 @@ export class R2BlobStore {
|
|
|
69
69
|
if (size > limit) throw new Error(`BlobTooLarge:${size}>${limit}`);
|
|
70
70
|
|
|
71
71
|
const contentType = opts.contentType ?? 'application/octet-stream';
|
|
72
|
-
const sha = await crypto.subtle.digest('SHA-256', view);
|
|
72
|
+
const sha = await crypto.subtle.digest('SHA-256', R2BlobStore.toArrayBuffer(view));
|
|
73
73
|
const shaB64 = R2BlobStore.b64url(sha);
|
|
74
74
|
const key = R2BlobStore.cidKey(shaB64);
|
|
75
75
|
const buffer = R2BlobStore.toArrayBuffer(view);
|
|
76
|
-
await this.env.
|
|
76
|
+
await this.env.ALTERAN_BLOBS.put(key, buffer, { httpMetadata: { contentType } });
|
|
77
77
|
return { key, size, sha256: shaB64 };
|
|
78
78
|
}
|
|
79
79
|
|
|
80
80
|
async get(key: string): Promise<R2ObjectBody | null> {
|
|
81
|
-
return this.env.
|
|
81
|
+
return this.env.ALTERAN_BLOBS.get(key);
|
|
82
82
|
}
|
|
83
83
|
|
|
84
84
|
async delete(key: string): Promise<void> {
|
|
85
|
-
await this.env.
|
|
85
|
+
await this.env.ALTERAN_BLOBS.delete(key);
|
|
86
86
|
}
|
|
87
87
|
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { CID } from 'multiformats/cid';
|
|
2
|
+
import * as dagCbor from '@ipld/dag-cbor';
|
|
3
|
+
import { cidForCbor } from '../../lib/mst/util';
|
|
4
|
+
import type { D1Blockstore, MST, BlockMap } from '../../lib/mst';
|
|
5
|
+
|
|
6
|
+
export async function storeRecord(
|
|
7
|
+
blockstore: D1Blockstore,
|
|
8
|
+
record: unknown,
|
|
9
|
+
): Promise<CID> {
|
|
10
|
+
const bytes = dagCbor.encode(record);
|
|
11
|
+
const cid = await cidForCbor(record);
|
|
12
|
+
await blockstore.put(cid, bytes);
|
|
13
|
+
return cid;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export async function storeMstBlocks(
|
|
17
|
+
blockstore: D1Blockstore,
|
|
18
|
+
mst: MST,
|
|
19
|
+
): Promise<BlockMap> {
|
|
20
|
+
const diff = await mst.getUnstoredBlocks();
|
|
21
|
+
for (const [cid, bytes] of diff.blocks) {
|
|
22
|
+
console.log(
|
|
23
|
+
`[RepoManager] Storing new MST block: ${cid.toString()}, size: ${bytes.length}`,
|
|
24
|
+
);
|
|
25
|
+
await blockstore.put(cid, bytes);
|
|
26
|
+
}
|
|
27
|
+
console.log(`[RepoManager] Stored ${diff.blocks.size} new MST blocks`);
|
|
28
|
+
return diff.blocks;
|
|
29
|
+
}
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import { CID } from 'multiformats/cid';
|
|
2
|
+
import * as dagCbor from '@ipld/dag-cbor';
|
|
3
|
+
import type { D1Blockstore, NodeData } from '../../lib/mst';
|
|
4
|
+
import type { RepoOp } from '../../lib/firehose/frames';
|
|
5
|
+
|
|
6
|
+
interface NodeEntry {
|
|
7
|
+
p: number;
|
|
8
|
+
k: Uint8Array;
|
|
9
|
+
v: unknown;
|
|
10
|
+
t?: unknown;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
interface DecodedNode {
|
|
14
|
+
e?: NodeEntry[];
|
|
15
|
+
l?: unknown;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function coerceCid(value: unknown): CID {
|
|
19
|
+
const asCid = (CID as unknown as { asCID?: (v: unknown) => CID | null }).asCID?.(value);
|
|
20
|
+
if (asCid) return asCid;
|
|
21
|
+
return CID.parse(String(value));
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export async function collectLeavesBatched(
|
|
25
|
+
blockstore: D1Blockstore,
|
|
26
|
+
root: CID,
|
|
27
|
+
): Promise<Map<string, CID>> {
|
|
28
|
+
const result = new Map<string, CID>();
|
|
29
|
+
const visited = new Set<string>();
|
|
30
|
+
let toFetch: CID[] = [root];
|
|
31
|
+
|
|
32
|
+
// Limit per getMany() request; getMany chunks the IN() list further.
|
|
33
|
+
const batchSize = 200;
|
|
34
|
+
|
|
35
|
+
while (toFetch.length > 0) {
|
|
36
|
+
const chunk = toFetch.slice(0, batchSize);
|
|
37
|
+
toFetch = toFetch.slice(batchSize);
|
|
38
|
+
|
|
39
|
+
const { blocks, missing } = await blockstore.getMany(chunk);
|
|
40
|
+
|
|
41
|
+
if (missing.length > 0) {
|
|
42
|
+
// Fail fast: missing MST nodes mean an incomplete repo, and emitting
|
|
43
|
+
// ops without them would produce a wrong diff.
|
|
44
|
+
const missingStr = missing.map((c) => c.toString()).join(', ');
|
|
45
|
+
throw new Error(
|
|
46
|
+
`[RepoManager] collectLeavesBatched: missing MST nodes: ${missingStr}`,
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
for (const [cidStr, bytes] of blocks.entries()) {
|
|
51
|
+
if (visited.has(cidStr)) continue;
|
|
52
|
+
visited.add(cidStr);
|
|
53
|
+
try {
|
|
54
|
+
const node = dagCbor.decode(bytes) as DecodedNode;
|
|
55
|
+
let lastKey = '';
|
|
56
|
+
const entries = Array.isArray(node.e) ? node.e : [];
|
|
57
|
+
for (const entry of entries) {
|
|
58
|
+
const keyStr = new TextDecoder('ascii').decode(entry.k);
|
|
59
|
+
const fullKey = lastKey.slice(0, Number(entry.p)) + keyStr;
|
|
60
|
+
try {
|
|
61
|
+
const parts = fullKey.split('/');
|
|
62
|
+
if (parts.length === 2 && parts[0] && parts[1]) {
|
|
63
|
+
result.set(fullKey, coerceCid(entry.v));
|
|
64
|
+
}
|
|
65
|
+
} catch (decodeError) {
|
|
66
|
+
console.warn('[RepoManager] failed to decode leaf CID:', decodeError);
|
|
67
|
+
}
|
|
68
|
+
lastKey = fullKey;
|
|
69
|
+
|
|
70
|
+
if (entry.t) {
|
|
71
|
+
const subtree = coerceCid(entry.t);
|
|
72
|
+
if (!visited.has(subtree.toString())) toFetch.push(subtree);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (node.l) {
|
|
77
|
+
const left = coerceCid(node.l);
|
|
78
|
+
if (!visited.has(left.toString())) toFetch.push(left);
|
|
79
|
+
}
|
|
80
|
+
} catch (error) {
|
|
81
|
+
console.warn('[RepoManager] collectLeavesBatched: failed to decode node', cidStr, error);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return result;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export async function extractOps(
|
|
90
|
+
blockstore: D1Blockstore,
|
|
91
|
+
prevRoot: CID | null,
|
|
92
|
+
newRoot: CID,
|
|
93
|
+
): Promise<RepoOp[]> {
|
|
94
|
+
const ops: RepoOp[] = [];
|
|
95
|
+
const newMap = await collectLeavesBatched(blockstore, newRoot);
|
|
96
|
+
const prevMap = prevRoot
|
|
97
|
+
? await collectLeavesBatched(blockstore, prevRoot)
|
|
98
|
+
: new Map<string, CID>();
|
|
99
|
+
|
|
100
|
+
for (const [path, cid] of Array.from(newMap.entries())) {
|
|
101
|
+
const prevCid = prevMap.get(path);
|
|
102
|
+
if (!prevCid) {
|
|
103
|
+
ops.push({ action: 'create', path, cid });
|
|
104
|
+
} else if (!prevCid.equals(cid)) {
|
|
105
|
+
ops.push({ action: 'update', path, cid, prev: prevCid });
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
for (const [path, prevCid] of Array.from(prevMap.entries())) {
|
|
110
|
+
if (!newMap.has(path)) {
|
|
111
|
+
ops.push({ action: 'delete', path, cid: null, prev: prevCid });
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
ops.sort((a, b) => a.path.localeCompare(b.path));
|
|
116
|
+
return ops;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Used by the legacy in-memory diff path (kept for compatibility). The batched
|
|
120
|
+
// version above is preferred because it avoids per-node round-trips to D1.
|
|
121
|
+
export async function collectLeavesRecursive(
|
|
122
|
+
entries: ReadonlyArray<{ isLeaf(): boolean; key?: string; value?: CID; getEntries?: () => Promise<unknown[]> }>,
|
|
123
|
+
map: Map<string, CID>,
|
|
124
|
+
): Promise<void> {
|
|
125
|
+
for (const entry of entries) {
|
|
126
|
+
if (entry.isLeaf() && entry.key && entry.value) {
|
|
127
|
+
map.set(entry.key, entry.value);
|
|
128
|
+
} else if (entry.getEntries) {
|
|
129
|
+
const subEntries = (await entry.getEntries()) as typeof entries;
|
|
130
|
+
await collectLeavesRecursive(subEntries, map);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|