@alteran/astro 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +558 -0
- package/index.d.ts +12 -0
- package/index.js +129 -0
- package/package.json +75 -0
- package/src/_worker.ts +44 -0
- package/src/app.ts +10 -0
- package/src/db/client.ts +7 -0
- package/src/db/dal.ts +97 -0
- package/src/db/repo.ts +135 -0
- package/src/db/schema.ts +89 -0
- package/src/db/seed.ts +14 -0
- package/src/env.d.ts +4 -0
- package/src/handlers/debug.ts +34 -0
- package/src/handlers/health.ts +6 -0
- package/src/handlers/ready.ts +14 -0
- package/src/handlers/root.ts +5 -0
- package/src/handlers/wellknown.ts +7 -0
- package/src/handlers/xrpc.repo.core.ts +57 -0
- package/src/handlers/xrpc.server.createSession.ts +25 -0
- package/src/handlers/xrpc.server.refreshSession.ts +43 -0
- package/src/lib/auth.ts +20 -0
- package/src/lib/blockstore-gc.ts +197 -0
- package/src/lib/cache.ts +236 -0
- package/src/lib/car-reader.ts +157 -0
- package/src/lib/commit-log-pruning.ts +76 -0
- package/src/lib/commit.ts +162 -0
- package/src/lib/config.ts +208 -0
- package/src/lib/errors.ts +142 -0
- package/src/lib/firehose/frames.ts +229 -0
- package/src/lib/firehose/parse.ts +82 -0
- package/src/lib/firehose/validation.ts +9 -0
- package/src/lib/handle.ts +90 -0
- package/src/lib/jwt.ts +150 -0
- package/src/lib/logger.ts +73 -0
- package/src/lib/metrics.ts +194 -0
- package/src/lib/mst/blockstore.ts +105 -0
- package/src/lib/mst/index.ts +3 -0
- package/src/lib/mst/mst.ts +643 -0
- package/src/lib/mst/util.ts +86 -0
- package/src/lib/ratelimit.ts +34 -0
- package/src/lib/sequencer.ts +10 -0
- package/src/lib/streaming-car.ts +137 -0
- package/src/lib/token-cleanup.ts +38 -0
- package/src/lib/tracing.ts +136 -0
- package/src/lib/util.ts +55 -0
- package/src/middleware.ts +102 -0
- package/src/pages/.well-known/atproto-did.ts +7 -0
- package/src/pages/.well-known/did.json.ts +76 -0
- package/src/pages/debug/blob/[...key].ts +27 -0
- package/src/pages/debug/db/bootstrap.ts +23 -0
- package/src/pages/debug/db/commits.ts +20 -0
- package/src/pages/debug/gc/blobs.ts +16 -0
- package/src/pages/debug/record.ts +33 -0
- package/src/pages/health.ts +68 -0
- package/src/pages/index.astro +57 -0
- package/src/pages/index.ts +2 -0
- package/src/pages/ready.ts +16 -0
- package/src/pages/xrpc/com.atproto.identity.resolveHandle.ts +38 -0
- package/src/pages/xrpc/com.atproto.identity.updateHandle.ts +45 -0
- package/src/pages/xrpc/com.atproto.repo.applyWrites.ts +73 -0
- package/src/pages/xrpc/com.atproto.repo.createRecord.ts +36 -0
- package/src/pages/xrpc/com.atproto.repo.deleteRecord.ts +36 -0
- package/src/pages/xrpc/com.atproto.repo.describeRepo.ts +51 -0
- package/src/pages/xrpc/com.atproto.repo.getRecord.ts +25 -0
- package/src/pages/xrpc/com.atproto.repo.listRecords.ts +57 -0
- package/src/pages/xrpc/com.atproto.repo.putRecord.ts +36 -0
- package/src/pages/xrpc/com.atproto.repo.uploadBlob.ts +53 -0
- package/src/pages/xrpc/com.atproto.server.createSession.ts +92 -0
- package/src/pages/xrpc/com.atproto.server.deleteSession.ts +25 -0
- package/src/pages/xrpc/com.atproto.server.describeServer.ts +17 -0
- package/src/pages/xrpc/com.atproto.server.getSession.ts +46 -0
- package/src/pages/xrpc/com.atproto.server.refreshSession.ts +67 -0
- package/src/pages/xrpc/com.atproto.sync.getBlocks.json.ts +16 -0
- package/src/pages/xrpc/com.atproto.sync.getBlocks.ts +56 -0
- package/src/pages/xrpc/com.atproto.sync.getCheckout.json.ts +20 -0
- package/src/pages/xrpc/com.atproto.sync.getCheckout.ts +43 -0
- package/src/pages/xrpc/com.atproto.sync.getHead.ts +11 -0
- package/src/pages/xrpc/com.atproto.sync.getLatestCommit.ts +42 -0
- package/src/pages/xrpc/com.atproto.sync.getRecord.ts +63 -0
- package/src/pages/xrpc/com.atproto.sync.getRepo.json.ts +20 -0
- package/src/pages/xrpc/com.atproto.sync.getRepo.range.ts +34 -0
- package/src/pages/xrpc/com.atproto.sync.getRepo.ts +17 -0
- package/src/pages/xrpc/com.atproto.sync.listBlobs.ts +53 -0
- package/src/pages/xrpc/com.atproto.sync.listRepos.ts +31 -0
- package/src/services/car.ts +249 -0
- package/src/services/r2-blob-store.ts +87 -0
- package/src/services/repo-manager.ts +339 -0
- package/src/shims/astro-internal-handler.d.ts +4 -0
- package/src/worker/sequencer.ts +563 -0
- package/types/env.d.ts +48 -0
|
@@ -0,0 +1,643 @@
|
|
|
1
|
+
import { CID } from 'multiformats/cid';
|
|
2
|
+
import * as uint8arrays from 'uint8arrays';
|
|
3
|
+
import * as dagCbor from '@ipld/dag-cbor';
|
|
4
|
+
import type { ReadableBlockstore } from './blockstore';
|
|
5
|
+
import * as util from './util';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* MST Node Data Structure
|
|
9
|
+
* Represents the CBOR-encoded format of an MST node
|
|
10
|
+
*/
|
|
11
|
+
export interface NodeData {
|
|
12
|
+
l: CID | null; // left-most subtree
|
|
13
|
+
e: TreeEntry[]; // entries (leaves with optional right subtrees)
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Tree Entry in MST node
|
|
18
|
+
*/
|
|
19
|
+
export interface TreeEntry {
|
|
20
|
+
p: number; // prefix count shared with previous key
|
|
21
|
+
k: Uint8Array; // rest of key after prefix
|
|
22
|
+
v: CID; // value CID
|
|
23
|
+
t: CID | null; // next subtree (to right of leaf)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Node entry can be either an MST subtree or a Leaf
|
|
28
|
+
*/
|
|
29
|
+
export type NodeEntry = MST | Leaf;
|
|
30
|
+
|
|
31
|
+
export interface MstOpts {
|
|
32
|
+
layer: number;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Merkle Search Tree (MST) Implementation
|
|
37
|
+
*
|
|
38
|
+
* An ordered, insert-order-independent, deterministic tree structure.
|
|
39
|
+
* Keys are laid out in alphabetic order, with each key hashed to determine
|
|
40
|
+
* which layer it belongs to based on leading zeros (~4 fanout, 2 bits per layer).
|
|
41
|
+
*/
|
|
42
|
+
export class MST {
|
|
43
|
+
storage: ReadableBlockstore;
|
|
44
|
+
entries: NodeEntry[] | null;
|
|
45
|
+
layer: number | null;
|
|
46
|
+
pointer: CID;
|
|
47
|
+
outdatedPointer = false;
|
|
48
|
+
|
|
49
|
+
constructor(
|
|
50
|
+
storage: ReadableBlockstore,
|
|
51
|
+
pointer: CID,
|
|
52
|
+
entries: NodeEntry[] | null,
|
|
53
|
+
layer: number | null,
|
|
54
|
+
) {
|
|
55
|
+
this.storage = storage;
|
|
56
|
+
this.entries = entries;
|
|
57
|
+
this.layer = layer;
|
|
58
|
+
this.pointer = pointer;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Create a new MST from entries
|
|
63
|
+
*/
|
|
64
|
+
static async create(
|
|
65
|
+
storage: ReadableBlockstore,
|
|
66
|
+
entries: NodeEntry[] = [],
|
|
67
|
+
opts?: Partial<MstOpts>,
|
|
68
|
+
): Promise<MST> {
|
|
69
|
+
const pointer = await cidForEntries(entries);
|
|
70
|
+
const { layer = null } = opts || {};
|
|
71
|
+
return new MST(storage, pointer, entries, layer);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Load MST from NodeData
|
|
76
|
+
*/
|
|
77
|
+
static async fromData(
|
|
78
|
+
storage: ReadableBlockstore,
|
|
79
|
+
data: NodeData,
|
|
80
|
+
opts?: Partial<MstOpts>,
|
|
81
|
+
): Promise<MST> {
|
|
82
|
+
const { layer = null } = opts || {};
|
|
83
|
+
const entries = await deserializeNodeData(storage, data, opts);
|
|
84
|
+
const pointer = await util.cidForCbor(data);
|
|
85
|
+
return new MST(storage, pointer, entries, layer);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Lazy load MST from CID (doesn't fetch from storage yet)
|
|
90
|
+
*/
|
|
91
|
+
static load(
|
|
92
|
+
storage: ReadableBlockstore,
|
|
93
|
+
cid: CID,
|
|
94
|
+
opts?: Partial<MstOpts>,
|
|
95
|
+
): MST {
|
|
96
|
+
const { layer = null } = opts || {};
|
|
97
|
+
return new MST(storage, cid, null, layer);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Create new tree with updated entries (immutable operation)
|
|
102
|
+
*/
|
|
103
|
+
async newTree(entries: NodeEntry[]): Promise<MST> {
|
|
104
|
+
const mst = new MST(this.storage, this.pointer, entries, this.layer);
|
|
105
|
+
mst.outdatedPointer = true;
|
|
106
|
+
return mst;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Get entries (lazy load from storage if needed)
|
|
111
|
+
*/
|
|
112
|
+
async getEntries(): Promise<NodeEntry[]> {
|
|
113
|
+
if (this.entries) return [...this.entries];
|
|
114
|
+
|
|
115
|
+
if (this.pointer) {
|
|
116
|
+
const data = await this.storage.readObj<NodeData>(this.pointer);
|
|
117
|
+
const firstLeaf = data.e[0];
|
|
118
|
+
const layer = firstLeaf !== undefined
|
|
119
|
+
? await util.leadingZerosOnHash(firstLeaf.k)
|
|
120
|
+
: undefined;
|
|
121
|
+
|
|
122
|
+
this.entries = await deserializeNodeData(this.storage, data, { layer });
|
|
123
|
+
return this.entries;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
throw new Error('No entries or CID provided');
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Get pointer CID (recalculate if outdated)
|
|
131
|
+
*/
|
|
132
|
+
async getPointer(): Promise<CID> {
|
|
133
|
+
if (!this.outdatedPointer) return this.pointer;
|
|
134
|
+
|
|
135
|
+
const { cid } = await this.serialize();
|
|
136
|
+
this.pointer = cid;
|
|
137
|
+
this.outdatedPointer = false;
|
|
138
|
+
return this.pointer;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Serialize MST to CBOR bytes
|
|
143
|
+
*/
|
|
144
|
+
async serialize(): Promise<{ cid: CID; bytes: Uint8Array }> {
|
|
145
|
+
let entries = await this.getEntries();
|
|
146
|
+
|
|
147
|
+
// Update any outdated child pointers first
|
|
148
|
+
const outdated = entries.filter(e => e.isTree() && e.outdatedPointer) as MST[];
|
|
149
|
+
if (outdated.length > 0) {
|
|
150
|
+
await Promise.all(outdated.map(e => e.getPointer()));
|
|
151
|
+
entries = await this.getEntries();
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const data = serializeNodeData(entries);
|
|
155
|
+
const bytes = dagCbor.encode(data);
|
|
156
|
+
const cid = await util.cidForCbor(data);
|
|
157
|
+
|
|
158
|
+
return { cid, bytes };
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Get layer of this node
|
|
163
|
+
*/
|
|
164
|
+
async getLayer(): Promise<number> {
|
|
165
|
+
this.layer = await this.attemptGetLayer();
|
|
166
|
+
if (this.layer === null) this.layer = 0;
|
|
167
|
+
return this.layer;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
async attemptGetLayer(): Promise<number | null> {
|
|
171
|
+
if (this.layer !== null) return this.layer;
|
|
172
|
+
|
|
173
|
+
const entries = await this.getEntries();
|
|
174
|
+
let layer = await layerForEntries(entries);
|
|
175
|
+
|
|
176
|
+
if (layer === null) {
|
|
177
|
+
for (const entry of entries) {
|
|
178
|
+
if (entry.isTree()) {
|
|
179
|
+
const childLayer = await entry.attemptGetLayer();
|
|
180
|
+
if (childLayer !== null) {
|
|
181
|
+
layer = childLayer + 1;
|
|
182
|
+
break;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
if (layer !== null) this.layer = layer;
|
|
189
|
+
return layer;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Add a new key/value pair to the MST
|
|
194
|
+
*/
|
|
195
|
+
async add(key: string, value: CID, knownZeros?: number): Promise<MST> {
|
|
196
|
+
util.ensureValidMstKey(key);
|
|
197
|
+
const keyZeros = knownZeros ?? (await util.leadingZerosOnHash(key));
|
|
198
|
+
const layer = await this.getLayer();
|
|
199
|
+
const newLeaf = new Leaf(key, value);
|
|
200
|
+
|
|
201
|
+
if (keyZeros === layer) {
|
|
202
|
+
// Key belongs in this layer
|
|
203
|
+
const index = await this.findGtOrEqualLeafIndex(key);
|
|
204
|
+
const found = await this.atIndex(index);
|
|
205
|
+
|
|
206
|
+
if (found?.isLeaf() && found.key === key) {
|
|
207
|
+
throw new Error(`There is already a value at key: ${key}`);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
const prevNode = await this.atIndex(index - 1);
|
|
211
|
+
if (!prevNode || prevNode.isLeaf()) {
|
|
212
|
+
return this.spliceIn(newLeaf, index);
|
|
213
|
+
} else {
|
|
214
|
+
const splitSubTree = await prevNode.splitAround(key);
|
|
215
|
+
return this.replaceWithSplit(index - 1, splitSubTree[0], newLeaf, splitSubTree[1]);
|
|
216
|
+
}
|
|
217
|
+
} else if (keyZeros < layer) {
|
|
218
|
+
// Key belongs on a lower layer
|
|
219
|
+
const index = await this.findGtOrEqualLeafIndex(key);
|
|
220
|
+
const prevNode = await this.atIndex(index - 1);
|
|
221
|
+
|
|
222
|
+
if (prevNode && prevNode.isTree()) {
|
|
223
|
+
const newSubtree = await prevNode.add(key, value, keyZeros);
|
|
224
|
+
return this.updateEntry(index - 1, newSubtree);
|
|
225
|
+
} else {
|
|
226
|
+
const subTree = await this.createChild();
|
|
227
|
+
const newSubTree = await subTree.add(key, value, keyZeros);
|
|
228
|
+
return this.spliceIn(newSubTree, index);
|
|
229
|
+
}
|
|
230
|
+
} else {
|
|
231
|
+
// Key belongs on a higher layer - push rest of tree down
|
|
232
|
+
const split = await this.splitAround(key);
|
|
233
|
+
let left: MST | null = split[0];
|
|
234
|
+
let right: MST | null = split[1];
|
|
235
|
+
const extraLayersToAdd = keyZeros - layer;
|
|
236
|
+
|
|
237
|
+
for (let i = 1; i < extraLayersToAdd; i++) {
|
|
238
|
+
if (left !== null) left = await left.createParent();
|
|
239
|
+
if (right !== null) right = await right.createParent();
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
const updated: NodeEntry[] = [];
|
|
243
|
+
if (left) updated.push(left);
|
|
244
|
+
updated.push(new Leaf(key, value));
|
|
245
|
+
if (right) updated.push(right);
|
|
246
|
+
|
|
247
|
+
const newRoot = await MST.create(this.storage, updated, { layer: keyZeros });
|
|
248
|
+
newRoot.outdatedPointer = true;
|
|
249
|
+
return newRoot;
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* Get value for a key
|
|
255
|
+
*/
|
|
256
|
+
async get(key: string): Promise<CID | null> {
|
|
257
|
+
const index = await this.findGtOrEqualLeafIndex(key);
|
|
258
|
+
const found = await this.atIndex(index);
|
|
259
|
+
|
|
260
|
+
if (found && found.isLeaf() && found.key === key) {
|
|
261
|
+
return found.value;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
const prev = await this.atIndex(index - 1);
|
|
265
|
+
if (prev && prev.isTree()) {
|
|
266
|
+
return prev.get(key);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
return null;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* Update value for existing key
|
|
274
|
+
*/
|
|
275
|
+
async update(key: string, value: CID): Promise<MST> {
|
|
276
|
+
util.ensureValidMstKey(key);
|
|
277
|
+
const index = await this.findGtOrEqualLeafIndex(key);
|
|
278
|
+
const found = await this.atIndex(index);
|
|
279
|
+
|
|
280
|
+
if (found && found.isLeaf() && found.key === key) {
|
|
281
|
+
return this.updateEntry(index, new Leaf(key, value));
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
const prev = await this.atIndex(index - 1);
|
|
285
|
+
if (prev && prev.isTree()) {
|
|
286
|
+
const updatedTree = await prev.update(key, value);
|
|
287
|
+
return this.updateEntry(index - 1, updatedTree);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
throw new Error(`Could not find a record with key: ${key}`);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* Delete a key from the MST
|
|
295
|
+
*/
|
|
296
|
+
async delete(key: string): Promise<MST> {
|
|
297
|
+
const altered = await this.deleteRecurse(key);
|
|
298
|
+
return altered.trimTop();
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
async deleteRecurse(key: string): Promise<MST> {
|
|
302
|
+
const index = await this.findGtOrEqualLeafIndex(key);
|
|
303
|
+
const found = await this.atIndex(index);
|
|
304
|
+
|
|
305
|
+
if (found?.isLeaf() && found.key === key) {
|
|
306
|
+
const prev = await this.atIndex(index - 1);
|
|
307
|
+
const next = await this.atIndex(index + 1);
|
|
308
|
+
|
|
309
|
+
if (prev?.isTree() && next?.isTree()) {
|
|
310
|
+
const merged = await prev.appendMerge(next);
|
|
311
|
+
return this.newTree([
|
|
312
|
+
...(await this.slice(0, index - 1)),
|
|
313
|
+
merged,
|
|
314
|
+
...(await this.slice(index + 2)),
|
|
315
|
+
]);
|
|
316
|
+
} else {
|
|
317
|
+
return this.removeEntry(index);
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
const prev = await this.atIndex(index - 1);
|
|
322
|
+
if (prev?.isTree()) {
|
|
323
|
+
const subtree = await prev.deleteRecurse(key);
|
|
324
|
+
const subTreeEntries = await subtree.getEntries();
|
|
325
|
+
|
|
326
|
+
if (subTreeEntries.length === 0) {
|
|
327
|
+
return this.removeEntry(index - 1);
|
|
328
|
+
} else {
|
|
329
|
+
return this.updateEntry(index - 1, subtree);
|
|
330
|
+
}
|
|
331
|
+
} else {
|
|
332
|
+
throw new Error(`Could not find a record with key: ${key}`);
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
/**
|
|
337
|
+
* List entries with optional pagination
|
|
338
|
+
*/
|
|
339
|
+
async list(count = Number.MAX_SAFE_INTEGER, after?: string, before?: string): Promise<Leaf[]> {
|
|
340
|
+
const vals: Leaf[] = [];
|
|
341
|
+
for await (const leaf of this.walkLeavesFrom(after || '')) {
|
|
342
|
+
if (leaf.key === after) continue;
|
|
343
|
+
if (vals.length >= count) break;
|
|
344
|
+
if (before && leaf.key >= before) break;
|
|
345
|
+
vals.push(leaf);
|
|
346
|
+
}
|
|
347
|
+
return vals;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
/**
|
|
351
|
+
* List entries with a given prefix
|
|
352
|
+
*/
|
|
353
|
+
async listWithPrefix(prefix: string, count = Number.MAX_SAFE_INTEGER): Promise<Leaf[]> {
|
|
354
|
+
const vals: Leaf[] = [];
|
|
355
|
+
for await (const leaf of this.walkLeavesFrom(prefix)) {
|
|
356
|
+
if (vals.length >= count || !leaf.key.startsWith(prefix)) break;
|
|
357
|
+
vals.push(leaf);
|
|
358
|
+
}
|
|
359
|
+
return vals;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
// Helper methods
|
|
363
|
+
|
|
364
|
+
async updateEntry(index: number, entry: NodeEntry): Promise<MST> {
|
|
365
|
+
const update = [
|
|
366
|
+
...(await this.slice(0, index)),
|
|
367
|
+
entry,
|
|
368
|
+
...(await this.slice(index + 1)),
|
|
369
|
+
];
|
|
370
|
+
return this.newTree(update);
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
async removeEntry(index: number): Promise<MST> {
|
|
374
|
+
const updated = [
|
|
375
|
+
...(await this.slice(0, index)),
|
|
376
|
+
...(await this.slice(index + 1)),
|
|
377
|
+
];
|
|
378
|
+
return this.newTree(updated);
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
async atIndex(index: number): Promise<NodeEntry | null> {
|
|
382
|
+
const entries = await this.getEntries();
|
|
383
|
+
return entries[index] ?? null;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
async slice(start?: number, end?: number): Promise<NodeEntry[]> {
|
|
387
|
+
const entries = await this.getEntries();
|
|
388
|
+
return entries.slice(start, end);
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
async spliceIn(entry: NodeEntry, index: number): Promise<MST> {
|
|
392
|
+
const update = [
|
|
393
|
+
...(await this.slice(0, index)),
|
|
394
|
+
entry,
|
|
395
|
+
...(await this.slice(index)),
|
|
396
|
+
];
|
|
397
|
+
return this.newTree(update);
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
async replaceWithSplit(
|
|
401
|
+
index: number,
|
|
402
|
+
left: MST | null,
|
|
403
|
+
leaf: Leaf,
|
|
404
|
+
right: MST | null,
|
|
405
|
+
): Promise<MST> {
|
|
406
|
+
const update = await this.slice(0, index);
|
|
407
|
+
if (left) update.push(left);
|
|
408
|
+
update.push(leaf);
|
|
409
|
+
if (right) update.push(right);
|
|
410
|
+
update.push(...(await this.slice(index + 1)));
|
|
411
|
+
return this.newTree(update);
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
async trimTop(): Promise<MST> {
|
|
415
|
+
const entries = await this.getEntries();
|
|
416
|
+
if (entries.length === 1 && entries[0].isTree()) {
|
|
417
|
+
return entries[0].trimTop();
|
|
418
|
+
}
|
|
419
|
+
return this;
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
async splitAround(key: string): Promise<[MST | null, MST | null]> {
|
|
423
|
+
const index = await this.findGtOrEqualLeafIndex(key);
|
|
424
|
+
const leftData = await this.slice(0, index);
|
|
425
|
+
const rightData = await this.slice(index);
|
|
426
|
+
let left = await this.newTree(leftData);
|
|
427
|
+
let right = await this.newTree(rightData);
|
|
428
|
+
|
|
429
|
+
const lastInLeft = leftData[leftData.length - 1];
|
|
430
|
+
if (lastInLeft?.isTree()) {
|
|
431
|
+
left = await left.removeEntry(leftData.length - 1);
|
|
432
|
+
const split = await lastInLeft.splitAround(key);
|
|
433
|
+
if (split[0]) left = await left.append(split[0]);
|
|
434
|
+
if (split[1]) right = await right.prepend(split[1]);
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
return [
|
|
438
|
+
(await left.getEntries()).length > 0 ? left : null,
|
|
439
|
+
(await right.getEntries()).length > 0 ? right : null,
|
|
440
|
+
];
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
async appendMerge(toMerge: MST): Promise<MST> {
|
|
444
|
+
if ((await this.getLayer()) !== (await toMerge.getLayer())) {
|
|
445
|
+
throw new Error('Trying to merge two nodes from different layers of the MST');
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
const thisEntries = await this.getEntries();
|
|
449
|
+
const toMergeEntries = await toMerge.getEntries();
|
|
450
|
+
const lastInLeft = thisEntries[thisEntries.length - 1];
|
|
451
|
+
const firstInRight = toMergeEntries[0];
|
|
452
|
+
|
|
453
|
+
if (lastInLeft?.isTree() && firstInRight?.isTree()) {
|
|
454
|
+
const merged = await lastInLeft.appendMerge(firstInRight);
|
|
455
|
+
return this.newTree([
|
|
456
|
+
...thisEntries.slice(0, thisEntries.length - 1),
|
|
457
|
+
merged,
|
|
458
|
+
...toMergeEntries.slice(1),
|
|
459
|
+
]);
|
|
460
|
+
} else {
|
|
461
|
+
return this.newTree([...thisEntries, ...toMergeEntries]);
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
async append(entry: NodeEntry): Promise<MST> {
|
|
466
|
+
const entries = await this.getEntries();
|
|
467
|
+
return this.newTree([...entries, entry]);
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
async prepend(entry: NodeEntry): Promise<MST> {
|
|
471
|
+
const entries = await this.getEntries();
|
|
472
|
+
return this.newTree([entry, ...entries]);
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
async createChild(): Promise<MST> {
|
|
476
|
+
const layer = await this.getLayer();
|
|
477
|
+
return MST.create(this.storage, [], { layer: layer - 1 });
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
async createParent(): Promise<MST> {
|
|
481
|
+
const layer = await this.getLayer();
|
|
482
|
+
const parent = await MST.create(this.storage, [this], { layer: layer + 1 });
|
|
483
|
+
parent.outdatedPointer = true;
|
|
484
|
+
return parent;
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
async findGtOrEqualLeafIndex(key: string): Promise<number> {
|
|
488
|
+
const entries = await this.getEntries();
|
|
489
|
+
const maybeIndex = entries.findIndex(entry => entry.isLeaf() && entry.key >= key);
|
|
490
|
+
return maybeIndex >= 0 ? maybeIndex : entries.length;
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
async *walkFrom(key: string): AsyncIterable<NodeEntry> {
|
|
494
|
+
yield this;
|
|
495
|
+
const index = await this.findGtOrEqualLeafIndex(key);
|
|
496
|
+
const entries = await this.getEntries();
|
|
497
|
+
const found = entries[index];
|
|
498
|
+
|
|
499
|
+
if (found && found.isLeaf() && found.key === key) {
|
|
500
|
+
yield found;
|
|
501
|
+
} else {
|
|
502
|
+
const prev = entries[index - 1];
|
|
503
|
+
if (prev) {
|
|
504
|
+
if (prev.isLeaf() && prev.key === key) {
|
|
505
|
+
yield prev;
|
|
506
|
+
} else if (prev.isTree()) {
|
|
507
|
+
yield* prev.walkFrom(key);
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
for (let i = index; i < entries.length; i++) {
|
|
513
|
+
const entry = entries[i];
|
|
514
|
+
if (entry.isLeaf()) {
|
|
515
|
+
yield entry;
|
|
516
|
+
} else {
|
|
517
|
+
yield* entry.walkFrom(key);
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
async *walkLeavesFrom(key: string): AsyncIterable<Leaf> {
|
|
523
|
+
for await (const node of this.walkFrom(key)) {
|
|
524
|
+
if (node.isLeaf()) {
|
|
525
|
+
yield node;
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
isTree(): this is MST {
|
|
531
|
+
return true;
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
isLeaf(): this is Leaf {
|
|
535
|
+
return false;
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
/**
|
|
540
|
+
* Leaf node in the MST
|
|
541
|
+
*/
|
|
542
|
+
export class Leaf {
|
|
543
|
+
constructor(
|
|
544
|
+
public key: string,
|
|
545
|
+
public value: CID,
|
|
546
|
+
) {}
|
|
547
|
+
|
|
548
|
+
isTree(): this is MST {
|
|
549
|
+
return false;
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
isLeaf(): this is Leaf {
|
|
553
|
+
return true;
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
equals(entry: NodeEntry): boolean {
|
|
557
|
+
if (entry.isLeaf()) {
|
|
558
|
+
return this.key === entry.key && this.value.equals(entry.value);
|
|
559
|
+
}
|
|
560
|
+
return false;
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
// Utility functions
|
|
565
|
+
|
|
566
|
+
async function layerForEntries(entries: NodeEntry[]): Promise<number | null> {
|
|
567
|
+
const firstLeaf = entries.find(entry => entry.isLeaf());
|
|
568
|
+
if (!firstLeaf || firstLeaf.isTree()) return null;
|
|
569
|
+
return await util.leadingZerosOnHash(firstLeaf.key);
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
async function deserializeNodeData(
|
|
573
|
+
storage: ReadableBlockstore,
|
|
574
|
+
data: NodeData,
|
|
575
|
+
opts?: Partial<MstOpts>,
|
|
576
|
+
): Promise<NodeEntry[]> {
|
|
577
|
+
const { layer } = opts || {};
|
|
578
|
+
const entries: NodeEntry[] = [];
|
|
579
|
+
|
|
580
|
+
if (data.l !== null) {
|
|
581
|
+
entries.push(MST.load(storage, data.l, { layer: layer ? layer - 1 : undefined }));
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
let lastKey = '';
|
|
585
|
+
for (const entry of data.e) {
|
|
586
|
+
const keyStr = uint8arrays.toString(entry.k, 'ascii');
|
|
587
|
+
const key = lastKey.slice(0, entry.p) + keyStr;
|
|
588
|
+
util.ensureValidMstKey(key);
|
|
589
|
+
entries.push(new Leaf(key, entry.v));
|
|
590
|
+
lastKey = key;
|
|
591
|
+
|
|
592
|
+
if (entry.t !== null) {
|
|
593
|
+
entries.push(MST.load(storage, entry.t, { layer: layer ? layer - 1 : undefined }));
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
return entries;
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
function serializeNodeData(entries: NodeEntry[]): NodeData {
|
|
601
|
+
const data: NodeData = { l: null, e: [] };
|
|
602
|
+
let i = 0;
|
|
603
|
+
|
|
604
|
+
if (entries[0]?.isTree()) {
|
|
605
|
+
i++;
|
|
606
|
+
data.l = entries[0].pointer;
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
let lastKey = '';
|
|
610
|
+
while (i < entries.length) {
|
|
611
|
+
const leaf = entries[i];
|
|
612
|
+
const next = entries[i + 1];
|
|
613
|
+
|
|
614
|
+
if (!leaf.isLeaf()) {
|
|
615
|
+
throw new Error('Not a valid node: two subtrees next to each other');
|
|
616
|
+
}
|
|
617
|
+
i++;
|
|
618
|
+
|
|
619
|
+
let subtree: CID | null = null;
|
|
620
|
+
if (next?.isTree()) {
|
|
621
|
+
subtree = next.pointer;
|
|
622
|
+
i++;
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
util.ensureValidMstKey(leaf.key);
|
|
626
|
+
const prefixLen = util.countPrefixLen(lastKey, leaf.key);
|
|
627
|
+
data.e.push({
|
|
628
|
+
p: prefixLen,
|
|
629
|
+
k: uint8arrays.fromString(leaf.key.slice(prefixLen), 'ascii'),
|
|
630
|
+
v: leaf.value,
|
|
631
|
+
t: subtree,
|
|
632
|
+
});
|
|
633
|
+
|
|
634
|
+
lastKey = leaf.key;
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
return data;
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
async function cidForEntries(entries: NodeEntry[]): Promise<CID> {
|
|
641
|
+
const data = serializeNodeData(entries);
|
|
642
|
+
return util.cidForCbor(data);
|
|
643
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { CID } from 'multiformats/cid';
|
|
2
|
+
import * as uint8arrays from 'uint8arrays';
|
|
3
|
+
import * as dagCbor from '@ipld/dag-cbor';
|
|
4
|
+
import { sha256 } from 'multiformats/hashes/sha2';
|
|
5
|
+
import { sha256 as nobleSha256 } from '@noble/hashes/sha2.js';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Calculate leading zeros in the hash of a key
|
|
9
|
+
* Used to determine which layer of the MST a key belongs to
|
|
10
|
+
* ~4 fanout (2 bits of zero per layer)
|
|
11
|
+
*/
|
|
12
|
+
export async function leadingZerosOnHash(key: string | Uint8Array): Promise<number> {
|
|
13
|
+
const bytes = typeof key === 'string' ? uint8arrays.fromString(key, 'utf8') : key;
|
|
14
|
+
const hash = nobleSha256(bytes);
|
|
15
|
+
|
|
16
|
+
let leadingZeros = 0;
|
|
17
|
+
for (let i = 0; i < hash.length; i++) {
|
|
18
|
+
const byte = hash[i];
|
|
19
|
+
if (byte < 64) leadingZeros++;
|
|
20
|
+
if (byte < 16) leadingZeros++;
|
|
21
|
+
if (byte < 4) leadingZeros++;
|
|
22
|
+
if (byte === 0) {
|
|
23
|
+
leadingZeros++;
|
|
24
|
+
} else {
|
|
25
|
+
break;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
return leadingZeros;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Count common prefix length between two strings
|
|
33
|
+
*/
|
|
34
|
+
export function countPrefixLen(a: string, b: string): number {
|
|
35
|
+
let i;
|
|
36
|
+
for (i = 0; i < a.length; i++) {
|
|
37
|
+
if (a[i] !== b[i]) {
|
|
38
|
+
break;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return i;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Validate MST key format
|
|
46
|
+
* Keys must be in format: collection/rkey
|
|
47
|
+
* Max length 1024, valid chars only
|
|
48
|
+
*/
|
|
49
|
+
export function isValidMstKey(str: string): boolean {
|
|
50
|
+
const split = str.split('/');
|
|
51
|
+
return (
|
|
52
|
+
str.length <= 1024 &&
|
|
53
|
+
split.length === 2 &&
|
|
54
|
+
split[0].length > 0 &&
|
|
55
|
+
split[1].length > 0 &&
|
|
56
|
+
isValidChars(split[0]) &&
|
|
57
|
+
isValidChars(split[1])
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const validCharsRegex = /^[a-zA-Z0-9_~\-:.]*$/;
|
|
62
|
+
|
|
63
|
+
export function isValidChars(str: string): boolean {
|
|
64
|
+
return validCharsRegex.test(str);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function ensureValidMstKey(str: string): void {
|
|
68
|
+
if (!isValidMstKey(str)) {
|
|
69
|
+
throw new InvalidMstKeyError(str);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export class InvalidMstKeyError extends Error {
|
|
74
|
+
constructor(public key: string) {
|
|
75
|
+
super(`Not a valid MST key: ${key}`);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Calculate CID for CBOR data
|
|
81
|
+
*/
|
|
82
|
+
export async function cidForCbor(data: unknown): Promise<CID> {
|
|
83
|
+
const bytes = dagCbor.encode(data);
|
|
84
|
+
const hash = await sha256.digest(bytes);
|
|
85
|
+
return CID.create(1, dagCbor.code, hash);
|
|
86
|
+
}
|