@atcute/car 3.0.5 → 3.1.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/README.md +78 -7
- package/dist/v3/atproto-repo.d.ts +1 -0
- package/dist/v3/atproto-repo.js +2 -0
- package/dist/v3/atproto-repo.js.map +1 -0
- package/dist/v3/index.js.map +1 -0
- package/dist/v3/reader.d.ts +1 -0
- package/dist/v3/reader.js +2 -0
- package/dist/v3/reader.js.map +1 -0
- package/dist/v4/car-reader/index.d.ts +3 -0
- package/dist/v4/car-reader/index.js +4 -0
- package/dist/v4/car-reader/index.js.map +1 -0
- package/dist/v4/car-reader/stream-car-reader.d.ts +11 -0
- package/dist/v4/car-reader/stream-car-reader.js +178 -0
- package/dist/v4/car-reader/stream-car-reader.js.map +1 -0
- package/dist/v4/car-reader/sync-car-reader.d.ts +10 -0
- package/dist/{utilities → v4/car-reader}/sync-car-reader.js +63 -31
- package/dist/v4/car-reader/sync-car-reader.js.map +1 -0
- package/dist/{utilities/car.js → v4/car-reader/types.js} +1 -1
- package/dist/v4/car-reader/types.js.map +1 -0
- package/dist/v4/index.d.ts +2 -0
- package/dist/v4/index.js +3 -0
- package/dist/v4/index.js.map +1 -0
- package/dist/v4/repo-reader/index.d.ts +5 -0
- package/dist/v4/repo-reader/index.js +6 -0
- package/dist/v4/repo-reader/index.js.map +1 -0
- package/dist/v4/repo-reader/mst.d.ts +47 -0
- package/dist/v4/repo-reader/mst.js +62 -0
- package/dist/v4/repo-reader/mst.js.map +1 -0
- package/dist/v4/repo-reader/stream-repo-reader.d.ts +24 -0
- package/dist/v4/repo-reader/stream-repo-reader.js +143 -0
- package/dist/v4/repo-reader/stream-repo-reader.js.map +1 -0
- package/dist/v4/repo-reader/sync-blockmap.d.ts +29 -0
- package/dist/v4/repo-reader/sync-blockmap.js +58 -0
- package/dist/v4/repo-reader/sync-blockmap.js.map +1 -0
- package/dist/v4/repo-reader/sync-repo-reader.d.ts +2 -0
- package/dist/v4/repo-reader/sync-repo-reader.js +20 -0
- package/dist/v4/repo-reader/sync-repo-reader.js.map +1 -0
- package/dist/v4/repo-reader/types.d.ts +20 -0
- package/dist/v4/repo-reader/types.js +37 -0
- package/dist/v4/repo-reader/types.js.map +1 -0
- package/dist/v4/utils.d.ts +3 -0
- package/dist/v4/utils.js +6 -0
- package/dist/v4/utils.js.map +1 -0
- package/lib/v3/atproto-repo.ts +15 -0
- package/lib/v3/reader.ts +9 -0
- package/lib/v4/car-reader/index.ts +4 -0
- package/lib/v4/car-reader/stream-car-reader.ts +240 -0
- package/lib/{utilities → v4/car-reader}/sync-car-reader.ts +90 -40
- package/lib/v4/index.ts +2 -0
- package/lib/v4/repo-reader/index.ts +6 -0
- package/lib/v4/repo-reader/mst.ts +110 -0
- package/lib/v4/repo-reader/stream-repo-reader.ts +209 -0
- package/lib/v4/repo-reader/sync-blockmap.ts +85 -0
- package/lib/v4/repo-reader/sync-repo-reader.ts +27 -0
- package/lib/v4/repo-reader/types.ts +32 -0
- package/lib/v4/utils.ts +7 -0
- package/package.json +7 -2
- package/dist/atproto-repo.d.ts +0 -104
- package/dist/atproto-repo.js +0 -171
- package/dist/atproto-repo.js.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/reader.d.ts +0 -4
- package/dist/reader.js +0 -7
- package/dist/reader.js.map +0 -1
- package/dist/utilities/car.js.map +0 -1
- package/dist/utilities/sync-byte-reader.d.ts +0 -7
- package/dist/utilities/sync-byte-reader.js +0 -28
- package/dist/utilities/sync-byte-reader.js.map +0 -1
- package/dist/utilities/sync-car-reader.d.ts +0 -7
- package/dist/utilities/sync-car-reader.js.map +0 -1
- package/lib/atproto-repo.ts +0 -248
- package/lib/reader.ts +0 -10
- package/lib/utilities/sync-byte-reader.ts +0 -39
- /package/dist/{index.d.ts → v3/index.d.ts} +0 -0
- /package/dist/{index.js → v3/index.js} +0 -0
- /package/dist/{utilities/car.d.ts → v4/car-reader/types.d.ts} +0 -0
- /package/lib/{index.ts → v3/index.ts} +0 -0
- /package/lib/{utilities/car.ts → v4/car-reader/types.ts} +0 -0
|
@@ -2,14 +2,101 @@ import * as CBOR from '@atcute/cbor';
|
|
|
2
2
|
import * as CID from '@atcute/cid';
|
|
3
3
|
import * as varint from '@atcute/varint';
|
|
4
4
|
|
|
5
|
-
import { isCarV1Header, type CarEntry, type CarHeader } from './
|
|
6
|
-
|
|
5
|
+
import { isCarV1Header, type CarEntry, type CarHeader } from './types.js';
|
|
6
|
+
|
|
7
|
+
interface SyncByteReader {
|
|
8
|
+
readonly pos: number;
|
|
9
|
+
upto(size: number): Uint8Array;
|
|
10
|
+
exactly(size: number, seek: boolean): Uint8Array;
|
|
11
|
+
seek(size: number): void;
|
|
12
|
+
}
|
|
7
13
|
|
|
8
14
|
export interface SyncCarReader {
|
|
9
|
-
header: CarHeader;
|
|
15
|
+
readonly header: CarHeader;
|
|
16
|
+
readonly roots: CBOR.CidLink[];
|
|
17
|
+
|
|
18
|
+
/** @deprecated do for..of on the reader directly */
|
|
10
19
|
iterate(): Generator<CarEntry>;
|
|
20
|
+
[Symbol.iterator](): Iterator<CarEntry>;
|
|
11
21
|
}
|
|
12
22
|
|
|
23
|
+
export const fromUint8Array = (buffer: Uint8Array): SyncCarReader => {
|
|
24
|
+
const reader = createUint8Reader(buffer);
|
|
25
|
+
const header = readHeader(reader);
|
|
26
|
+
|
|
27
|
+
return {
|
|
28
|
+
header,
|
|
29
|
+
roots: header.data.roots,
|
|
30
|
+
|
|
31
|
+
*iterate() {
|
|
32
|
+
while (reader.upto(8 + 36).length > 0) {
|
|
33
|
+
const entryStart = reader.pos;
|
|
34
|
+
const entrySize = readVarint(reader, 8);
|
|
35
|
+
|
|
36
|
+
const cidStart = reader.pos;
|
|
37
|
+
const cid = readCid(reader);
|
|
38
|
+
|
|
39
|
+
const bytesStart = reader.pos;
|
|
40
|
+
const bytesSize = entrySize - (bytesStart - cidStart);
|
|
41
|
+
const bytes = reader.exactly(bytesSize, true);
|
|
42
|
+
|
|
43
|
+
const cidEnd = bytesStart;
|
|
44
|
+
const bytesEnd = reader.pos;
|
|
45
|
+
const entryEnd = bytesEnd;
|
|
46
|
+
|
|
47
|
+
yield {
|
|
48
|
+
cid,
|
|
49
|
+
bytes,
|
|
50
|
+
|
|
51
|
+
entryStart,
|
|
52
|
+
entryEnd,
|
|
53
|
+
cidStart,
|
|
54
|
+
cidEnd,
|
|
55
|
+
bytesStart,
|
|
56
|
+
bytesEnd,
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
},
|
|
60
|
+
|
|
61
|
+
[Symbol.iterator](): Iterator<CarEntry> {
|
|
62
|
+
return this.iterate();
|
|
63
|
+
},
|
|
64
|
+
};
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
const createUint8Reader = (buf: Uint8Array): SyncByteReader => {
|
|
68
|
+
let pos = 0;
|
|
69
|
+
|
|
70
|
+
return {
|
|
71
|
+
get pos() {
|
|
72
|
+
return pos;
|
|
73
|
+
},
|
|
74
|
+
|
|
75
|
+
seek(size) {
|
|
76
|
+
if (size > buf.length - pos) {
|
|
77
|
+
throw new RangeError('unexpected end of data');
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
pos += size;
|
|
81
|
+
},
|
|
82
|
+
upto(size) {
|
|
83
|
+
return buf.subarray(pos, pos + size);
|
|
84
|
+
},
|
|
85
|
+
exactly(size, seek) {
|
|
86
|
+
if (size > buf.length - pos) {
|
|
87
|
+
throw new RangeError('unexpected end of data');
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const slice = buf.subarray(pos, pos + size);
|
|
91
|
+
if (seek) {
|
|
92
|
+
pos += size;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return slice;
|
|
96
|
+
},
|
|
97
|
+
};
|
|
98
|
+
};
|
|
99
|
+
|
|
13
100
|
const readVarint = (reader: SyncByteReader, size: number): number => {
|
|
14
101
|
const buf = reader.upto(size);
|
|
15
102
|
if (buf.length === 0) {
|
|
@@ -82,40 +169,3 @@ const readCid = (reader: SyncByteReader): CID.Cid => {
|
|
|
82
169
|
|
|
83
170
|
return cid;
|
|
84
171
|
};
|
|
85
|
-
|
|
86
|
-
export const createCarReader = (reader: SyncByteReader): SyncCarReader => {
|
|
87
|
-
const header = readHeader(reader);
|
|
88
|
-
|
|
89
|
-
return {
|
|
90
|
-
header,
|
|
91
|
-
*iterate() {
|
|
92
|
-
while (reader.upto(8 + 36).length > 0) {
|
|
93
|
-
const entryStart = reader.pos;
|
|
94
|
-
const entrySize = readVarint(reader, 8);
|
|
95
|
-
|
|
96
|
-
const cidStart = reader.pos;
|
|
97
|
-
const cid = readCid(reader);
|
|
98
|
-
|
|
99
|
-
const bytesStart = reader.pos;
|
|
100
|
-
const bytesSize = entrySize - (bytesStart - cidStart);
|
|
101
|
-
const bytes = reader.exactly(bytesSize, true);
|
|
102
|
-
|
|
103
|
-
const cidEnd = bytesStart;
|
|
104
|
-
const bytesEnd = reader.pos;
|
|
105
|
-
const entryEnd = bytesEnd;
|
|
106
|
-
|
|
107
|
-
yield {
|
|
108
|
-
cid,
|
|
109
|
-
bytes,
|
|
110
|
-
|
|
111
|
-
entryStart,
|
|
112
|
-
entryEnd,
|
|
113
|
-
cidStart,
|
|
114
|
-
cidEnd,
|
|
115
|
-
bytesStart,
|
|
116
|
-
bytesEnd,
|
|
117
|
-
};
|
|
118
|
-
}
|
|
119
|
-
},
|
|
120
|
-
};
|
|
121
|
-
};
|
package/lib/v4/index.ts
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import * as CBOR from '@atcute/cbor';
|
|
2
|
+
import * as CID from '@atcute/cid';
|
|
3
|
+
|
|
4
|
+
const isCidLink = (value: unknown): value is CID.CidLink => {
|
|
5
|
+
if (value instanceof CID.CidLinkWrapper) {
|
|
6
|
+
return true;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
if (value === null || typeof value !== 'object') {
|
|
10
|
+
return false;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
return '$link' in value && typeof value.$link === 'string';
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
const isBytes = (value: unknown): value is CBOR.Bytes => {
|
|
17
|
+
if (value instanceof CBOR.BytesWrapper) {
|
|
18
|
+
return true;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
if (value === null || typeof value !== 'object') {
|
|
22
|
+
return false;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
return '$bytes' in value && typeof value.$bytes === 'string';
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
/** commit object */
|
|
29
|
+
export interface Commit {
|
|
30
|
+
version: 3;
|
|
31
|
+
did: string;
|
|
32
|
+
data: CID.CidLink;
|
|
33
|
+
rev: string;
|
|
34
|
+
prev: CID.CidLink | null;
|
|
35
|
+
sig: CBOR.Bytes;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* checks if a value is a valid commit object
|
|
40
|
+
* @param value the value to check
|
|
41
|
+
* @returns true if the value is a valid commit object, false otherwise
|
|
42
|
+
*/
|
|
43
|
+
export const isCommit = (value: unknown): value is Commit => {
|
|
44
|
+
if (value === null || typeof value !== 'object') {
|
|
45
|
+
return false;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const obj = value as Record<string, unknown>;
|
|
49
|
+
|
|
50
|
+
return (
|
|
51
|
+
obj.version === 3 &&
|
|
52
|
+
typeof obj.did === 'string' &&
|
|
53
|
+
isCidLink(obj.data) &&
|
|
54
|
+
typeof obj.rev === 'string' &&
|
|
55
|
+
(obj.prev === null || isCidLink(obj.prev)) &&
|
|
56
|
+
isBytes(obj.sig)
|
|
57
|
+
);
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
/** mst tree entry object */
|
|
61
|
+
export interface TreeEntry {
|
|
62
|
+
/** count of bytes shared with previous TreeEntry in this Node (if any) */
|
|
63
|
+
p: number;
|
|
64
|
+
/** remainder of key for this TreeEntry, after "prefixlen" have been removed */
|
|
65
|
+
k: CBOR.Bytes;
|
|
66
|
+
/** link to a sub-tree Node at a lower level which has keys sorting after this TreeEntry's key (to the "right"), but before the next TreeEntry's key in this Node (if any) */
|
|
67
|
+
v: CID.CidLink;
|
|
68
|
+
/** next subtree (to the right of leaf) */
|
|
69
|
+
t: CID.CidLink | null;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* checks if a value is a valid mst tree entry object
|
|
74
|
+
* @param value the value to check
|
|
75
|
+
* @returns true if the value is a valid mst tree entry object, false otherwise
|
|
76
|
+
*/
|
|
77
|
+
export const isTreeEntry = (value: unknown): value is TreeEntry => {
|
|
78
|
+
if (value === null || typeof value !== 'object') {
|
|
79
|
+
return false;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const obj = value as Record<string, unknown>;
|
|
83
|
+
|
|
84
|
+
return (
|
|
85
|
+
typeof obj.p === 'number' && isBytes(obj.k) && isCidLink(obj.v) && (obj.t === null || isCidLink(obj.t))
|
|
86
|
+
);
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
/** mst node object */
|
|
90
|
+
export interface MstNode {
|
|
91
|
+
/** link to sub-tree Node on a lower level and with all keys sorting before keys at this node */
|
|
92
|
+
l: CID.CidLink | null;
|
|
93
|
+
/** ordered list of TreeEntry objects */
|
|
94
|
+
e: TreeEntry[];
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* checks if a value is a valid mst node object
|
|
99
|
+
* @param value the value to check
|
|
100
|
+
* @returns true if the value is a valid mst node object, false otherwise
|
|
101
|
+
*/
|
|
102
|
+
export const isMstNode = (value: unknown): value is MstNode => {
|
|
103
|
+
if (value === null || typeof value !== 'object') {
|
|
104
|
+
return false;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const obj = value as Record<string, unknown>;
|
|
108
|
+
|
|
109
|
+
return (obj.l === null || isCidLink(obj.l)) && Array.isArray(obj.e) && obj.e.every(isTreeEntry);
|
|
110
|
+
};
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
import Queue from 'yocto-queue';
|
|
2
|
+
|
|
3
|
+
import * as CBOR from '@atcute/cbor';
|
|
4
|
+
import * as CID from '@atcute/cid';
|
|
5
|
+
import { decodeUtf8From } from '@atcute/uint8array';
|
|
6
|
+
|
|
7
|
+
import * as CarReader from '../car-reader/index.js';
|
|
8
|
+
import { assert } from '../utils.js';
|
|
9
|
+
|
|
10
|
+
import { isCommit, isMstNode } from './mst.js';
|
|
11
|
+
import { RepoEntry } from './types.js';
|
|
12
|
+
|
|
13
|
+
type EntryMeta = { t: 0 } | { t: 1 } | { t: 2; k: string };
|
|
14
|
+
|
|
15
|
+
type Task = {
|
|
16
|
+
c: string;
|
|
17
|
+
e: CarReader.CarEntry;
|
|
18
|
+
m: EntryMeta;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export type MissingBlockEntry =
|
|
22
|
+
| {
|
|
23
|
+
cid: string;
|
|
24
|
+
type: 'record';
|
|
25
|
+
key: string;
|
|
26
|
+
}
|
|
27
|
+
| {
|
|
28
|
+
cid: string;
|
|
29
|
+
type: 'mst-node';
|
|
30
|
+
}
|
|
31
|
+
| {
|
|
32
|
+
cid: string;
|
|
33
|
+
type: 'commit';
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
export interface StreamedRepoReader {
|
|
37
|
+
/**
|
|
38
|
+
* list of blocks that were referenced but not found in the repository.
|
|
39
|
+
* blocks may be reported as missing if multiple records share the same CID.
|
|
40
|
+
*/
|
|
41
|
+
readonly missingBlocks: readonly MissingBlockEntry[];
|
|
42
|
+
|
|
43
|
+
dispose(): Promise<void>;
|
|
44
|
+
|
|
45
|
+
[Symbol.asyncDispose](): Promise<void>;
|
|
46
|
+
[Symbol.asyncIterator](): AsyncIterator<RepoEntry>;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export const repoEntryTransform = (): ReadableWritablePair<RepoEntry, Uint8Array> => {
|
|
50
|
+
const transform = new TransformStream<Uint8Array, Uint8Array>();
|
|
51
|
+
let repo: StreamedRepoReader | undefined;
|
|
52
|
+
|
|
53
|
+
return {
|
|
54
|
+
readable: new ReadableStream({
|
|
55
|
+
async start(controller) {
|
|
56
|
+
repo = fromStream(transform.readable);
|
|
57
|
+
|
|
58
|
+
try {
|
|
59
|
+
for await (const entry of repo) {
|
|
60
|
+
controller.enqueue(entry);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
await repo.dispose();
|
|
64
|
+
|
|
65
|
+
controller.close();
|
|
66
|
+
} catch (err) {
|
|
67
|
+
controller.error(err);
|
|
68
|
+
}
|
|
69
|
+
},
|
|
70
|
+
async cancel() {
|
|
71
|
+
if (repo !== undefined) {
|
|
72
|
+
await repo.dispose();
|
|
73
|
+
}
|
|
74
|
+
},
|
|
75
|
+
}),
|
|
76
|
+
writable: transform.writable,
|
|
77
|
+
};
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
export const fromStream = (stream: ReadableStream<Uint8Array>): StreamedRepoReader => {
|
|
81
|
+
let missingBlocks: MissingBlockEntry[] = [];
|
|
82
|
+
|
|
83
|
+
return {
|
|
84
|
+
get missingBlocks() {
|
|
85
|
+
return missingBlocks;
|
|
86
|
+
},
|
|
87
|
+
|
|
88
|
+
async dispose() {
|
|
89
|
+
// does nothing for now
|
|
90
|
+
},
|
|
91
|
+
|
|
92
|
+
[Symbol.asyncDispose]() {
|
|
93
|
+
return this.dispose();
|
|
94
|
+
},
|
|
95
|
+
async *[Symbol.asyncIterator]() {
|
|
96
|
+
// await using car = CarReader.fromStream(stream);
|
|
97
|
+
const car = CarReader.fromStream(stream);
|
|
98
|
+
|
|
99
|
+
try {
|
|
100
|
+
const pending = new Map<string, EntryMeta>();
|
|
101
|
+
const strays = new Map<string, CarReader.CarEntry>();
|
|
102
|
+
|
|
103
|
+
const queue = new Queue<Task>();
|
|
104
|
+
|
|
105
|
+
const request = (cid: string, meta: EntryMeta): void => {
|
|
106
|
+
const entry = strays.get(cid);
|
|
107
|
+
|
|
108
|
+
if (entry !== undefined) {
|
|
109
|
+
strays.delete(cid);
|
|
110
|
+
queue.enqueue({ c: cid, e: entry, m: meta });
|
|
111
|
+
} else {
|
|
112
|
+
pending.set(cid, meta);
|
|
113
|
+
}
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
{
|
|
117
|
+
const roots = await car.roots();
|
|
118
|
+
assert(roots.length === 1, `expected only 1 root in the car archive; got=${roots.length}`);
|
|
119
|
+
|
|
120
|
+
const rootCid = roots[0].$link;
|
|
121
|
+
request(rootCid, { t: 0 });
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
for await (const entry of car) {
|
|
125
|
+
const cid = CID.toString(entry.cid);
|
|
126
|
+
|
|
127
|
+
{
|
|
128
|
+
const meta = pending.get(cid);
|
|
129
|
+
|
|
130
|
+
if (meta !== undefined) {
|
|
131
|
+
pending.delete(cid);
|
|
132
|
+
queue.enqueue({ c: cid, e: entry, m: meta });
|
|
133
|
+
} else {
|
|
134
|
+
strays.set(cid, entry);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
let task: Task | undefined;
|
|
139
|
+
while ((task = queue.dequeue())) {
|
|
140
|
+
const { c: cid, e: entry, m: meta } = task;
|
|
141
|
+
|
|
142
|
+
switch (meta.t) {
|
|
143
|
+
case 0: {
|
|
144
|
+
const commit = CBOR.decode(entry.bytes);
|
|
145
|
+
assert(isCommit(commit), `expected commit block; cid=${cid}`);
|
|
146
|
+
|
|
147
|
+
request(commit.data.$link, { t: 1 });
|
|
148
|
+
break;
|
|
149
|
+
}
|
|
150
|
+
case 1: {
|
|
151
|
+
const node = CBOR.decode(entry.bytes);
|
|
152
|
+
assert(isMstNode(node), `expected mst node block; cid=${cid}`);
|
|
153
|
+
|
|
154
|
+
const entries = node.e;
|
|
155
|
+
const left = node.l;
|
|
156
|
+
|
|
157
|
+
let lastKey = '';
|
|
158
|
+
|
|
159
|
+
if (left !== null) {
|
|
160
|
+
request(left.$link, meta);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
for (let i = 0, il = entries.length; i < il; i++) {
|
|
164
|
+
const entry = entries[i];
|
|
165
|
+
const next = entry.t;
|
|
166
|
+
|
|
167
|
+
const key_str = decodeUtf8From(CBOR.fromBytes(entry.k));
|
|
168
|
+
const key = lastKey.slice(0, entry.p) + key_str;
|
|
169
|
+
|
|
170
|
+
lastKey = key;
|
|
171
|
+
|
|
172
|
+
request(entry.v.$link, { t: 2, k: key });
|
|
173
|
+
|
|
174
|
+
if (next !== null) {
|
|
175
|
+
request(next.$link, { t: 1 });
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
break;
|
|
180
|
+
}
|
|
181
|
+
case 2: {
|
|
182
|
+
const [collection, rkey] = meta.k.split('/');
|
|
183
|
+
|
|
184
|
+
yield new RepoEntry(collection, rkey, CID.toCidLink(entry.cid), entry);
|
|
185
|
+
break;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
missingBlocks = Array.from(pending, ([cid, meta]): MissingBlockEntry => {
|
|
192
|
+
switch (meta.t) {
|
|
193
|
+
case 0: {
|
|
194
|
+
return { cid, type: 'commit' };
|
|
195
|
+
}
|
|
196
|
+
case 1: {
|
|
197
|
+
return { cid, type: 'mst-node' };
|
|
198
|
+
}
|
|
199
|
+
case 2: {
|
|
200
|
+
return { cid, type: 'record', key: meta.k };
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
});
|
|
204
|
+
} finally {
|
|
205
|
+
await car.dispose();
|
|
206
|
+
}
|
|
207
|
+
},
|
|
208
|
+
};
|
|
209
|
+
};
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import * as CBOR from '@atcute/cbor';
|
|
2
|
+
import * as CID from '@atcute/cid';
|
|
3
|
+
import { decodeUtf8From } from '@atcute/uint8array';
|
|
4
|
+
|
|
5
|
+
import * as CarReader from '../car-reader/index.js';
|
|
6
|
+
import { assert } from '../utils.js';
|
|
7
|
+
|
|
8
|
+
import { isMstNode } from './mst.js';
|
|
9
|
+
|
|
10
|
+
export type BlockMap = Map<string, CarReader.CarEntry>;
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* collects entries from a CAR archive into a mapping of CID string -> actual bytes
|
|
14
|
+
* @param iterator a generator that yields objects with a `cid` and `bytes` property
|
|
15
|
+
* @returns a mapping of CID string -> actual bytes
|
|
16
|
+
*/
|
|
17
|
+
export const collectBlock = (iterator: Iterable<CarReader.CarEntry>): BlockMap => {
|
|
18
|
+
const blockmap: BlockMap = new Map();
|
|
19
|
+
for (const entry of iterator) {
|
|
20
|
+
blockmap.set(CID.toString(entry.cid), entry);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
return blockmap;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* reads a block from the blockmap and validates it against the provided validation function
|
|
28
|
+
* @param map a mapping of CID string -> actual bytes
|
|
29
|
+
* @param link a CID link to read
|
|
30
|
+
* @param validate a validation function to validate the decoded data
|
|
31
|
+
* @returns the decoded and validated data
|
|
32
|
+
*/
|
|
33
|
+
export const readBlock = <T>(
|
|
34
|
+
map: BlockMap,
|
|
35
|
+
link: CID.CidLink,
|
|
36
|
+
validate: (value: unknown) => value is T,
|
|
37
|
+
): T => {
|
|
38
|
+
const cid = link.$link;
|
|
39
|
+
|
|
40
|
+
const entry = map.get(cid);
|
|
41
|
+
assert(entry != null, `cid not found in blockmap; cid=${cid}`);
|
|
42
|
+
|
|
43
|
+
const data = CBOR.decode(entry.bytes);
|
|
44
|
+
assert(validate(data), `validation failed for cid=${cid}`);
|
|
45
|
+
|
|
46
|
+
return data;
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
/** node entry object */
|
|
50
|
+
export interface NodeEntry {
|
|
51
|
+
key: string;
|
|
52
|
+
cid: CID.CidLink;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* walks the entries of a Merkle Sorted Tree (MST) in a depth-first manner
|
|
57
|
+
* @param map a mapping of CID string -> actual bytes
|
|
58
|
+
* @param pointer a CID link to the root of the MST
|
|
59
|
+
* @returns a generator that yields the entries of the MST
|
|
60
|
+
*/
|
|
61
|
+
export function* walkMstEntries(map: BlockMap, pointer: CID.CidLink): Generator<NodeEntry> {
|
|
62
|
+
const data = readBlock(map, pointer, isMstNode);
|
|
63
|
+
const entries = data.e;
|
|
64
|
+
|
|
65
|
+
let lastKey = '';
|
|
66
|
+
|
|
67
|
+
if (data.l !== null) {
|
|
68
|
+
yield* walkMstEntries(map, data.l);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
for (let i = 0, il = entries.length; i < il; i++) {
|
|
72
|
+
const entry = entries[i];
|
|
73
|
+
|
|
74
|
+
const key_str = decodeUtf8From(CBOR.fromBytes(entry.k));
|
|
75
|
+
const key = lastKey.slice(0, entry.p) + key_str;
|
|
76
|
+
|
|
77
|
+
lastKey = key;
|
|
78
|
+
|
|
79
|
+
yield { key: key, cid: entry.v };
|
|
80
|
+
|
|
81
|
+
if (entry.t !== null) {
|
|
82
|
+
yield* walkMstEntries(map, entry.t);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import * as CarReader from '../car-reader/index.js';
|
|
2
|
+
import { assert } from '../utils.js';
|
|
3
|
+
|
|
4
|
+
import { isCommit } from './mst.js';
|
|
5
|
+
import { collectBlock, readBlock, walkMstEntries } from './sync-blockmap.js';
|
|
6
|
+
import { RepoEntry } from './types.js';
|
|
7
|
+
|
|
8
|
+
export function* fromUint8Array(buf: Uint8Array): Generator<RepoEntry> {
|
|
9
|
+
const car = CarReader.fromUint8Array(buf);
|
|
10
|
+
const roots = car.roots;
|
|
11
|
+
|
|
12
|
+
assert(roots.length === 1, `expected only 1 root in the car archive; got=${roots.length}`);
|
|
13
|
+
|
|
14
|
+
const blockmap = collectBlock(car);
|
|
15
|
+
assert(blockmap.size > 0, `expected at least 1 block in the archive; got=${blockmap.size}`);
|
|
16
|
+
|
|
17
|
+
const commit = readBlock(blockmap, roots[0], isCommit);
|
|
18
|
+
|
|
19
|
+
for (const { key, cid } of walkMstEntries(blockmap, commit.data)) {
|
|
20
|
+
const [collection, rkey] = key.split('/');
|
|
21
|
+
|
|
22
|
+
const carEntry = blockmap.get(cid.$link);
|
|
23
|
+
assert(carEntry != null, `cid not found in blockmap; cid=${cid}`);
|
|
24
|
+
|
|
25
|
+
yield new RepoEntry(collection, rkey, cid, carEntry);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import * as CBOR from '@atcute/cbor';
|
|
2
|
+
import * as CID from '@atcute/cid';
|
|
3
|
+
|
|
4
|
+
import { type CarEntry } from '../car-reader/index.js';
|
|
5
|
+
|
|
6
|
+
export class RepoEntry {
|
|
7
|
+
/** @internal */
|
|
8
|
+
constructor(
|
|
9
|
+
/** the collection this record belongs to */
|
|
10
|
+
public readonly collection: string,
|
|
11
|
+
/** record key */
|
|
12
|
+
public readonly rkey: string,
|
|
13
|
+
/** CID of this record */
|
|
14
|
+
public readonly cid: CID.CidLink,
|
|
15
|
+
/** the associated CarEntry for this record */
|
|
16
|
+
public readonly carEntry: CarEntry,
|
|
17
|
+
) {}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* raw contents of this record
|
|
21
|
+
*/
|
|
22
|
+
get bytes(): Uint8Array {
|
|
23
|
+
return this.carEntry.bytes;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* decoded contents of this record
|
|
28
|
+
*/
|
|
29
|
+
get record(): unknown {
|
|
30
|
+
return CBOR.decode(this.bytes);
|
|
31
|
+
}
|
|
32
|
+
}
|
package/lib/v4/utils.ts
ADDED
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "@atcute/car",
|
|
4
|
-
"version": "3.
|
|
4
|
+
"version": "3.1.1",
|
|
5
5
|
"description": "lightweight DASL CAR and atproto repository decoder for AT Protocol.",
|
|
6
6
|
"keywords": [
|
|
7
7
|
"atproto",
|
|
@@ -20,7 +20,10 @@
|
|
|
20
20
|
"!lib/**/*.test.ts"
|
|
21
21
|
],
|
|
22
22
|
"exports": {
|
|
23
|
-
".": "./dist/index.js"
|
|
23
|
+
".": "./dist/v3/index.js",
|
|
24
|
+
"./v4": "./dist/v4/index.js",
|
|
25
|
+
"./v4/car-reader": "./dist/v4/car-reader/index.js",
|
|
26
|
+
"./v4/repo-reader": "./dist/v4/repo-reader/index.js"
|
|
24
27
|
},
|
|
25
28
|
"sideEffects": false,
|
|
26
29
|
"devDependencies": {
|
|
@@ -28,8 +31,10 @@
|
|
|
28
31
|
"@atcute/multibase": "^1.1.4"
|
|
29
32
|
},
|
|
30
33
|
"dependencies": {
|
|
34
|
+
"yocto-queue": "^1.2.1",
|
|
31
35
|
"@atcute/cbor": "^2.2.4",
|
|
32
36
|
"@atcute/cid": "^2.2.3",
|
|
37
|
+
"@atcute/uint8array": "^1.0.2",
|
|
33
38
|
"@atcute/varint": "^1.0.2"
|
|
34
39
|
},
|
|
35
40
|
"scripts": {
|