@alteran/astro 0.3.9 → 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 +263 -405
- 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.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/lib/labeler.ts
CHANGED
|
@@ -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:
|
|
35
|
+
const views: LabelerView[] = [];
|
|
22
36
|
|
|
23
37
|
for (const did of unique) {
|
|
24
|
-
|
|
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
|
|
45
|
+
let parsedRecord: Record<string, unknown>;
|
|
31
46
|
try {
|
|
32
|
-
|
|
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
|
-
|
|
38
|
-
|
|
39
|
-
const
|
|
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(
|
|
55
|
-
subjectTypes: Array.isArray(
|
|
56
|
-
subjectCollections: Array.isArray(
|
|
57
|
-
|
|
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(
|
|
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:
|
|
70
|
-
if (input && typeof input === 'object') {
|
|
71
|
-
const
|
|
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:
|
|
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
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
// Decode base64 string to Uint8Array
|
|
43
|
-
return Uint8Array.from(atob(result.bytes), c => c.charCodeAt(0));
|
|
34
|
+
const row = await this.env.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
|
-
|
|
48
|
-
const
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
return
|
|
45
|
+
// Treat rows with NULL or empty bytes as missing
|
|
46
|
+
const row = await this.env.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
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
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.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
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
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.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
|
|
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
|
|
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
|
|
130
|
-
|
|
120
|
+
const stmts = [] as Array<ReturnType<typeof this.env.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
|
-
|
|
129
|
+
stmts.push(
|
|
130
|
+
this.env.DB.prepare(`INSERT OR REPLACE INTO blockstore (cid, bytes) VALUES (?, ?)`)
|
|
131
|
+
.bind(cidStr, base64)
|
|
132
|
+
);
|
|
152
133
|
}
|
|
153
|
-
|
|
154
|
-
|
|
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.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;
|
package/src/lib/mst/index.ts
CHANGED
|
@@ -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
|
+
}
|