@gmod/gbz-base 0.0.1 → 1.0.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.
Files changed (56) hide show
  1. package/README.md +3 -3
  2. package/bin/query.js +3 -1
  3. package/dist/cli.js +26 -7
  4. package/dist/cli.js.map +1 -0
  5. package/dist/db.d.ts +5 -5
  6. package/dist/db.js +35 -14
  7. package/dist/db.js.map +1 -0
  8. package/dist/filehandle.js +1 -0
  9. package/dist/filehandle.js.map +1 -0
  10. package/dist/gbwt/bytecode.d.ts +1 -1
  11. package/dist/gbwt/bytecode.js +7 -3
  12. package/dist/gbwt/bytecode.js.map +1 -0
  13. package/dist/gbwt/node.js +7 -2
  14. package/dist/gbwt/node.js.map +1 -0
  15. package/dist/gbwt/record.js +8 -2
  16. package/dist/gbwt/record.js.map +1 -0
  17. package/dist/gbwt/sequence.js +8 -1
  18. package/dist/gbwt/sequence.js.map +1 -0
  19. package/dist/index.d.ts +2 -2
  20. package/dist/index.js +3 -2
  21. package/dist/index.js.map +1 -0
  22. package/dist/lcs.js +32 -10
  23. package/dist/lcs.js.map +1 -0
  24. package/dist/query.d.ts +1 -1
  25. package/dist/query.js +1 -0
  26. package/dist/query.js.map +1 -0
  27. package/dist/sqlite/btree.d.ts +1 -1
  28. package/dist/sqlite/btree.js +17 -5
  29. package/dist/sqlite/btree.js.map +1 -0
  30. package/dist/sqlite/database.d.ts +1 -1
  31. package/dist/sqlite/database.js +12 -2
  32. package/dist/sqlite/database.js.map +1 -0
  33. package/dist/sqlite/pager.d.ts +3 -3
  34. package/dist/sqlite/pager.js +6 -4
  35. package/dist/sqlite/pager.js.map +1 -0
  36. package/dist/sqlite/record.js +3 -1
  37. package/dist/sqlite/record.js.map +1 -0
  38. package/dist/subgraph.d.ts +2 -2
  39. package/dist/subgraph.js +116 -29
  40. package/dist/subgraph.js.map +1 -0
  41. package/package.json +36 -11
  42. package/src/cli.ts +203 -0
  43. package/src/db.ts +322 -0
  44. package/src/filehandle.ts +4 -0
  45. package/src/gbwt/bytecode.ts +87 -0
  46. package/src/gbwt/node.ts +64 -0
  47. package/src/gbwt/record.ts +154 -0
  48. package/src/gbwt/sequence.ts +50 -0
  49. package/src/index.ts +31 -0
  50. package/src/lcs.ts +282 -0
  51. package/src/query.ts +79 -0
  52. package/src/sqlite/btree.ts +255 -0
  53. package/src/sqlite/database.ts +117 -0
  54. package/src/sqlite/pager.ts +78 -0
  55. package/src/sqlite/record.ts +100 -0
  56. package/src/subgraph.ts +1066 -0
package/package.json CHANGED
@@ -1,9 +1,16 @@
1
1
  {
2
2
  "name": "@gmod/gbz-base",
3
- "version": "0.0.1",
3
+ "version": "1.0.0",
4
4
  "description": "Pure TypeScript reader for gbz-base pangenome databases (.gbz.db) with random access over HTTP range requests",
5
5
  "license": "MIT",
6
+ "packageManager": "pnpm@11.15.1",
6
7
  "type": "module",
8
+ "sideEffects": false,
9
+ "author": {
10
+ "name": "Colin Diesh",
11
+ "email": "colin.diesh@gmail.com",
12
+ "url": "https://github.com/cmdcolin"
13
+ },
7
14
  "main": "dist/index.js",
8
15
  "types": "dist/index.d.ts",
9
16
  "exports": {
@@ -18,21 +25,44 @@
18
25
  },
19
26
  "files": [
20
27
  "dist",
28
+ "src",
21
29
  "bin",
22
30
  "tools/haplotype-index/Cargo.toml",
23
31
  "tools/haplotype-index/src"
24
32
  ],
33
+ "scripts": {
34
+ "test": "vitest",
35
+ "coverage": "pnpm test --coverage",
36
+ "clean": "rimraf dist",
37
+ "format": "prettier --write .",
38
+ "format:check": "prettier --check .",
39
+ "lint": "eslint --report-unused-disable-directives --max-warnings 0",
40
+ "typecheck": "tsc --noEmit -p tsconfig.lint.json",
41
+ "prebuild": "pnpm clean",
42
+ "build": "tsc",
43
+ "preversion": "pnpm lint && pnpm format:check && pnpm typecheck && pnpm test --run && pnpm build",
44
+ "version": "git-cliff --tag v$npm_package_version --unreleased --prepend CHANGELOG.md && git add CHANGELOG.md",
45
+ "postversion": "git push --follow-tags"
46
+ },
25
47
  "dependencies": {
26
48
  "generic-filehandle2": "^2.0.0"
27
49
  },
28
50
  "devDependencies": {
29
- "@types/node": "^24.0.0",
30
- "typescript": "^5.9.0",
31
- "vitest": "^3.2.0"
51
+ "@eslint/js": "^10.0.1",
52
+ "@types/node": "^25.9.5",
53
+ "@vitest/coverage-v8": "^4.1.10",
54
+ "eslint": "^10.8.0",
55
+ "eslint-plugin-import-x": "^4.17.1",
56
+ "git-cliff": "^2.13.1",
57
+ "prettier": "^3.9.6",
58
+ "rimraf": "^6.1.3",
59
+ "typescript": "^6.0.3",
60
+ "typescript-eslint": "^8.65.0",
61
+ "vitest": "^4.1.10"
32
62
  },
33
63
  "repository": {
34
64
  "type": "git",
35
- "url": "https://github.com/GMOD/gbz-base-js.git"
65
+ "url": "git+https://github.com/GMOD/gbz-base-js.git"
36
66
  },
37
67
  "keywords": [
38
68
  "pangenome",
@@ -44,10 +74,5 @@
44
74
  ],
45
75
  "publishConfig": {
46
76
  "access": "public"
47
- },
48
- "scripts": {
49
- "build": "tsc -p tsconfig.build.json",
50
- "test": "vitest run",
51
- "typecheck": "tsc -p tsconfig.json --noEmit"
52
77
  }
53
- }
78
+ }
package/src/cli.ts ADDED
@@ -0,0 +1,203 @@
1
+ import { LocalFile, RemoteFile } from 'generic-filehandle2'
2
+
3
+ import { GBZBase, formatPathName } from './db.ts'
4
+ import {
5
+ subgraphAroundNodes,
6
+ subgraphAtOffset,
7
+ subgraphInInterval,
8
+ } from './query.ts'
9
+
10
+ import type { HaplotypeOutput } from './subgraph.ts'
11
+
12
+ const USAGE = `Usage: gbz-base-query [options] graph.gbz.db
13
+
14
+ --sample STR sample name (default: generic path)
15
+ --contig STR contig name (required for --offset and --interval)
16
+ --haplotype INT haplotype number (default: 0)
17
+ -o, --offset INT sequence offset
18
+ -i, --interval A..B half-open sequence interval
19
+ -n, --node INT node identifier (may repeat)
20
+ --context INT context length in bp (default: 100)
21
+ --limit INT safety limit for the number of nodes
22
+ --haplotypes SEL all, distinct, reference-only or none (default: all)
23
+ --cigar output CIGAR strings for the haplotypes
24
+ --resolve name haplotypes from the HaplotypeSamples table
25
+ --alignments print one alignment record per haplotype fragment instead of the subgraph
26
+ --block-size INT bytes fetched per range request (default: 65536)
27
+ --stats print fetch statistics to stderr
28
+ `
29
+
30
+ interface Args {
31
+ file: string
32
+ sample?: string
33
+ contig?: string
34
+ haplotype: number
35
+ offset?: number
36
+ interval?: [number, number]
37
+ nodes: number[]
38
+ context: number
39
+ limit?: number
40
+ haplotypes: HaplotypeOutput
41
+ cigar: boolean
42
+ resolve: boolean
43
+ alignments: boolean
44
+ blockSize: number
45
+ stats: boolean
46
+ }
47
+
48
+ function parseArgs(argv: string[]): Args {
49
+ const args: Args = {
50
+ file: '',
51
+ haplotype: 0,
52
+ nodes: [],
53
+ context: 100,
54
+ haplotypes: 'all',
55
+ cigar: false,
56
+ resolve: false,
57
+ alignments: false,
58
+ blockSize: 65536,
59
+ stats: false,
60
+ }
61
+ const next = (i: number) => {
62
+ const value = argv[i + 1]
63
+ if (value === undefined) {
64
+ throw new Error(`${argv[i]} needs a value`)
65
+ }
66
+ return value
67
+ }
68
+ for (let i = 0; i < argv.length; i++) {
69
+ const arg = argv[i]!
70
+ switch (arg) {
71
+ case '--sample':
72
+ args.sample = next(i++)
73
+ break
74
+ case '--contig':
75
+ args.contig = next(i++)
76
+ break
77
+ case '--haplotype':
78
+ args.haplotype = Number(next(i++))
79
+ break
80
+ case '-o':
81
+ case '--offset':
82
+ args.offset = Number(next(i++))
83
+ break
84
+ case '-i':
85
+ case '--interval': {
86
+ const [a, b] = next(i++).split('..')
87
+ args.interval = [Number(a), Number(b)]
88
+ break
89
+ }
90
+ case '-n':
91
+ case '--node':
92
+ args.nodes.push(Number(next(i++)))
93
+ break
94
+ case '--context':
95
+ args.context = Number(next(i++))
96
+ break
97
+ case '--limit':
98
+ args.limit = Number(next(i++))
99
+ break
100
+ case '--haplotypes':
101
+ args.haplotypes = next(i++) as HaplotypeOutput
102
+ break
103
+ case '--cigar':
104
+ args.cigar = true
105
+ break
106
+ case '--resolve':
107
+ args.resolve = true
108
+ break
109
+ case '--alignments':
110
+ args.alignments = true
111
+ args.resolve = true
112
+ break
113
+ case '--block-size':
114
+ args.blockSize = Number(next(i++))
115
+ break
116
+ case '--stats':
117
+ args.stats = true
118
+ break
119
+ case '--format':
120
+ next(i++)
121
+ break
122
+ case '-h':
123
+ case '--help':
124
+ process.stdout.write(USAGE)
125
+ process.exit(0)
126
+ break
127
+ default:
128
+ if (arg.startsWith('-')) {
129
+ throw new Error(`Unknown option ${arg}`)
130
+ }
131
+ args.file = arg
132
+ }
133
+ }
134
+ if (!args.file) {
135
+ throw new Error(USAGE)
136
+ }
137
+ return args
138
+ }
139
+
140
+ export async function main(argv: string[]) {
141
+ const args = parseArgs(argv)
142
+ const source = /^https?:\/\//.test(args.file)
143
+ ? new RemoteFile(args.file)
144
+ : new LocalFile(args.file)
145
+ const db = await GBZBase.open(source, { blockSize: args.blockSize })
146
+ const opts = {
147
+ context: args.context,
148
+ haplotypes: args.haplotypes,
149
+ ...(args.limit === undefined ? {} : { limit: args.limit }),
150
+ }
151
+ const query = {
152
+ contig: args.contig ?? '',
153
+ haplotype: args.haplotype,
154
+ ...(args.sample === undefined ? {} : { sample: args.sample }),
155
+ }
156
+ const subgraph =
157
+ args.nodes.length > 0
158
+ ? await subgraphAroundNodes(db, args.nodes, opts)
159
+ : args.interval
160
+ ? await subgraphInInterval(
161
+ db,
162
+ query,
163
+ args.interval[0],
164
+ args.interval[1],
165
+ opts,
166
+ )
167
+ : args.offset !== undefined
168
+ ? await subgraphAtOffset(db, query, args.offset, opts)
169
+ : undefined
170
+ if (!subgraph) {
171
+ throw new Error(
172
+ 'Query type must be specified using --offset, --interval or --node',
173
+ )
174
+ }
175
+ if (args.resolve) {
176
+ await subgraph.identifyPaths()
177
+ }
178
+ const output = args.alignments
179
+ ? subgraph.alignments().map(a => ({
180
+ ...a,
181
+ name:
182
+ a.name && a.hapStart !== undefined && a.hapEnd !== undefined
183
+ ? formatPathName({ ...a.name, fragment: a.hapStart }, a.hapEnd)
184
+ : undefined,
185
+ start: undefined,
186
+ }))
187
+ : subgraph.toJSON(args.cigar, {
188
+ names: args.resolve ? 'resolved' : 'anonymous',
189
+ })
190
+ process.stdout.write(`${JSON.stringify(output)}\n`)
191
+ if (args.stats) {
192
+ const { fetches, bytesFetched } = db.sqlite.pager
193
+ const {
194
+ orderedAlignments,
195
+ lcsAlignments,
196
+ identificationSteps,
197
+ identificationFetches,
198
+ } = subgraph.stats
199
+ process.stderr.write(
200
+ `Subgraph contains ${subgraph.nodeCount} nodes and ${subgraph.pathCount} paths; ${fetches} fetches, ${bytesFetched} bytes; ${orderedAlignments} ordered + ${lcsAlignments} lcs alignments; identification ${identificationSteps} steps, ${identificationFetches} lookups\n`,
201
+ )
202
+ }
203
+ }
package/src/db.ts ADDED
@@ -0,0 +1,322 @@
1
+ import { ENDMARKER, nodeId, nodeOrientation } from './gbwt/node.ts'
2
+ import { GbwtRecord, decompressEdges } from './gbwt/record.ts'
3
+ import { decodeSequence, encodedSequenceLength } from './gbwt/sequence.ts'
4
+ import { SqliteDatabase } from './sqlite/database.ts'
5
+
6
+ import type { ByteSource } from './filehandle.ts'
7
+ import type { Pos } from './gbwt/record.ts'
8
+ import type { PagerOptions } from './sqlite/pager.ts'
9
+ import type { SqlValue } from './sqlite/record.ts'
10
+
11
+ export interface PathName {
12
+ sample: string
13
+ contig: string
14
+ haplotype: number
15
+ fragment: number
16
+ }
17
+
18
+ export const GENERIC_SAMPLE = '_gbwt_ref'
19
+ export const SCHEMA_VERSION = 'GBZ-base version 4'
20
+
21
+ export class SchemaVersionError extends Error {
22
+ override name = 'SchemaVersionError'
23
+
24
+ readonly found: string | undefined
25
+
26
+ constructor(found: string | undefined) {
27
+ super(
28
+ found === undefined
29
+ ? `not a gbz-base database: its Tags table has no version`
30
+ : `unsupported database schema "${found}"; this reader understands "${SCHEMA_VERSION}"`,
31
+ )
32
+ this.found = found
33
+ }
34
+ }
35
+
36
+ export function formatPathName(name: PathName, end: number) {
37
+ return `${name.sample}#${name.haplotype}#${name.contig}[${name.fragment}-${end}]`
38
+ }
39
+
40
+ export interface HaplotypeSample {
41
+ node: number
42
+ offset: number
43
+ pathHandle: number
44
+ orientation: 'forward' | 'reverse'
45
+ pathOffset: number
46
+ }
47
+
48
+ export interface GbzPath {
49
+ handle: number
50
+ fwStart: Pos
51
+ revStart: Pos
52
+ name: PathName
53
+ isIndexed: boolean
54
+ }
55
+
56
+ export class GbzRecord {
57
+ readonly sequenceLen: number
58
+ readonly handle: number
59
+ readonly edges: Pos[]
60
+ readonly bwt: Uint8Array
61
+ readonly encodedSequence: Uint8Array
62
+ readonly next: number | undefined
63
+ private decoded: string | undefined
64
+
65
+ constructor(
66
+ handle: number,
67
+ edges: Pos[],
68
+ bwt: Uint8Array,
69
+ encodedSequence: Uint8Array,
70
+ next: number | undefined,
71
+ ) {
72
+ this.handle = handle
73
+ this.edges = edges
74
+ this.bwt = bwt
75
+ this.encodedSequence = encodedSequence
76
+ this.next = next
77
+ this.sequenceLen = encodedSequenceLength(encodedSequence)
78
+ }
79
+
80
+ get id() {
81
+ return nodeId(this.handle)
82
+ }
83
+
84
+ get orientation() {
85
+ return nodeOrientation(this.handle)
86
+ }
87
+
88
+ get sequence() {
89
+ this.decoded ??= decodeSequence(this.encodedSequence)
90
+ return this.decoded
91
+ }
92
+
93
+ successors() {
94
+ return this.edges.filter(e => e.node !== ENDMARKER).map(e => e.node)
95
+ }
96
+
97
+ gbwt() {
98
+ if (this.edges.length === 0) {
99
+ throw new Error(`GBWT record for handle ${this.handle} is empty`)
100
+ }
101
+ return new GbwtRecord(this.edges, this.bwt)
102
+ }
103
+ }
104
+
105
+ function num(value: SqlValue | undefined, what: string) {
106
+ if (typeof value !== 'number') {
107
+ throw new Error(`${what} is not a number in the database`)
108
+ }
109
+ return value
110
+ }
111
+
112
+ function str(value: SqlValue | undefined, what: string) {
113
+ if (typeof value !== 'string') {
114
+ throw new Error(`${what} is not text in the database`)
115
+ }
116
+ return value
117
+ }
118
+
119
+ function blob(value: SqlValue | undefined, what: string) {
120
+ if (!(value instanceof Uint8Array)) {
121
+ throw new Error(`${what} is not a blob in the database`)
122
+ }
123
+ return value
124
+ }
125
+
126
+ function rowToPath(rowid: number, values: SqlValue[]): GbzPath {
127
+ return {
128
+ handle: rowid,
129
+ fwStart: {
130
+ node: num(values[1], 'Paths.fw_node'),
131
+ offset: num(values[2], 'Paths.fw_offset'),
132
+ },
133
+ revStart: {
134
+ node: num(values[3], 'Paths.rev_node'),
135
+ offset: num(values[4], 'Paths.rev_offset'),
136
+ },
137
+ name: {
138
+ sample: str(values[5], 'Paths.sample'),
139
+ contig: str(values[6], 'Paths.contig'),
140
+ haplotype: num(values[7], 'Paths.haplotype'),
141
+ fragment: num(values[8], 'Paths.fragment'),
142
+ },
143
+ isIndexed: num(values[9], 'Paths.is_indexed') !== 0,
144
+ }
145
+ }
146
+
147
+ export class GBZBase {
148
+ private tagCache: Promise<Map<string, string>> | undefined
149
+ private pathCache: Promise<GbzPath[]> | undefined
150
+
151
+ readonly sqlite: SqliteDatabase
152
+
153
+ private constructor(sqlite: SqliteDatabase) {
154
+ this.sqlite = sqlite
155
+ }
156
+
157
+ static async open(source: ByteSource, opts: PagerOptions = {}) {
158
+ const sqlite = await SqliteDatabase.open(source, opts)
159
+ for (const table of ['Tags', 'Nodes', 'Paths', 'ReferenceIndex']) {
160
+ sqlite.rootPage(table)
161
+ }
162
+ const db = new GBZBase(sqlite)
163
+ const version = await db.tag('version')
164
+ if (version !== SCHEMA_VERSION) {
165
+ throw new SchemaVersionError(version)
166
+ }
167
+ return db
168
+ }
169
+
170
+ tags() {
171
+ this.tagCache ??= (async () => {
172
+ const tags = new Map<string, string>()
173
+ for await (const { values } of this.sqlite.scan('Tags')) {
174
+ tags.set(str(values[0], 'Tags.key'), str(values[1], 'Tags.value'))
175
+ }
176
+ return tags
177
+ })()
178
+ return this.tagCache
179
+ }
180
+
181
+ async tag(key: string) {
182
+ return (await this.tags()).get(key)
183
+ }
184
+
185
+ async getRecord(handle: number) {
186
+ const row = await this.sqlite.byRowid('Nodes', handle)
187
+ if (!row) {
188
+ return undefined
189
+ }
190
+ const next = row[4]
191
+ return new GbzRecord(
192
+ handle,
193
+ decompressEdges(blob(row[1], 'Nodes.edges')),
194
+ blob(row[2], 'Nodes.bwt'),
195
+ blob(row[3], 'Nodes.sequence'),
196
+ typeof next === 'number' ? next : undefined,
197
+ )
198
+ }
199
+
200
+ paths() {
201
+ this.pathCache ??= (async () => {
202
+ const paths: GbzPath[] = []
203
+ for await (const { rowid, values } of this.sqlite.scan('Paths')) {
204
+ paths.push(rowToPath(rowid, values))
205
+ }
206
+ return paths
207
+ })()
208
+ return this.pathCache
209
+ }
210
+
211
+ async getPath(handle: number) {
212
+ const row = await this.sqlite.byRowid('Paths', handle)
213
+ return row ? rowToPath(handle, row) : undefined
214
+ }
215
+
216
+ async findPath(name: PathName) {
217
+ const candidates = (await this.paths()).filter(
218
+ p =>
219
+ p.name.sample === name.sample &&
220
+ p.name.contig === name.contig &&
221
+ p.name.haplotype === name.haplotype &&
222
+ p.name.fragment <= name.fragment,
223
+ )
224
+ return candidates.sort((a, b) => b.name.fragment - a.name.fragment)[0]
225
+ }
226
+
227
+ async pathsForSample(sample: string) {
228
+ return (await this.paths()).filter(p => p.name.sample === sample)
229
+ }
230
+
231
+ get hasHaplotypeIndex() {
232
+ return (
233
+ this.sqlite.has('HaplotypeSamples') && this.sqlite.has('HaplotypeLengths')
234
+ )
235
+ }
236
+
237
+ async haplotypeSampleInterval() {
238
+ const value = await this.tag('haplotype_index_interval')
239
+ return value === undefined ? undefined : Number(value)
240
+ }
241
+
242
+ private sampleFromRow(values: SqlValue[]): HaplotypeSample {
243
+ return {
244
+ node: num(values[0], 'HaplotypeSamples.node_handle'),
245
+ offset: num(values[1], 'HaplotypeSamples.node_offset'),
246
+ pathHandle: num(values[2], 'HaplotypeSamples.path_handle'),
247
+ orientation:
248
+ num(values[3], 'HaplotypeSamples.orientation') === 0
249
+ ? 'forward'
250
+ : 'reverse',
251
+ pathOffset: num(values[4], 'HaplotypeSamples.path_offset'),
252
+ }
253
+ }
254
+
255
+ async haplotypeSamplesInRange(minHandle: number, maxHandle: number) {
256
+ const samples: HaplotypeSample[] = []
257
+ for await (const key of this.sqlite.indexScanFrom('HaplotypeSamples', [
258
+ minHandle,
259
+ 0,
260
+ ])) {
261
+ const node = num(key[0], 'HaplotypeSamples.node_handle')
262
+ if (node > maxHandle) {
263
+ break
264
+ }
265
+ const row = await this.sqlite.byRowid(
266
+ 'HaplotypeSamples',
267
+ num(key[2], 'HaplotypeSamples rowid'),
268
+ )
269
+ if (row) {
270
+ samples.push(this.sampleFromRow(row))
271
+ }
272
+ }
273
+ return samples
274
+ }
275
+
276
+ async haplotypeSampleAt(node: number, offset: number) {
277
+ const key = await this.sqlite.indexSeekLE('HaplotypeSamples', [
278
+ node,
279
+ offset,
280
+ ])
281
+ if (key?.[0] !== node || key[1] !== offset) {
282
+ return undefined
283
+ }
284
+ const row = await this.sqlite.byRowid(
285
+ 'HaplotypeSamples',
286
+ num(key[2], 'HaplotypeSamples rowid'),
287
+ )
288
+ return row ? this.sampleFromRow(row) : undefined
289
+ }
290
+
291
+ async haplotypeLength(pathHandle: number) {
292
+ const row = await this.sqlite.byRowid('HaplotypeLengths', pathHandle)
293
+ return row ? num(row[1], 'HaplotypeLengths.length') : undefined
294
+ }
295
+
296
+ async indexedPosition(
297
+ pathHandle: number,
298
+ pathOffset: number,
299
+ ): Promise<{ pathOffset: number; pos: Pos } | undefined> {
300
+ const key = await this.sqlite.indexSeekLE('ReferenceIndex', [
301
+ pathHandle,
302
+ pathOffset,
303
+ ])
304
+ if (key?.[0] !== pathHandle) {
305
+ return undefined
306
+ }
307
+ const row = await this.sqlite.byRowid(
308
+ 'ReferenceIndex',
309
+ num(key[2], 'ReferenceIndex rowid'),
310
+ )
311
+ if (!row) {
312
+ throw new Error('ReferenceIndex row referenced by its index is missing')
313
+ }
314
+ return {
315
+ pathOffset: num(row[1], 'ReferenceIndex.path_offset'),
316
+ pos: {
317
+ node: num(row[2], 'ReferenceIndex.node_handle'),
318
+ offset: num(row[3], 'ReferenceIndex.node_offset'),
319
+ },
320
+ }
321
+ }
322
+ }
@@ -0,0 +1,4 @@
1
+ export interface ByteSource {
2
+ read(length: number, position: number): Promise<Uint8Array>
3
+ stat(): Promise<{ size: number }>
4
+ }
@@ -0,0 +1,87 @@
1
+ export class ByteCodeReader {
2
+ offset = 0
3
+ private bytes: Uint8Array
4
+
5
+ constructor(bytes: Uint8Array) {
6
+ this.bytes = bytes
7
+ }
8
+
9
+ get done() {
10
+ return this.offset >= this.bytes.length
11
+ }
12
+
13
+ byte() {
14
+ const value = this.bytes[this.offset]
15
+ if (value === undefined) {
16
+ return undefined
17
+ }
18
+ this.offset += 1
19
+ return value
20
+ }
21
+
22
+ int() {
23
+ let shift = 1
24
+ let result = 0
25
+ while (this.offset < this.bytes.length) {
26
+ const value = this.bytes[this.offset]!
27
+ this.offset += 1
28
+ result += (value & 0x7f) * shift
29
+ shift *= 128
30
+ if ((value & 0x80) === 0) {
31
+ return result
32
+ }
33
+ }
34
+ return undefined
35
+ }
36
+ }
37
+
38
+ export interface Run {
39
+ value: number
40
+ len: number
41
+ }
42
+
43
+ const RLE_THRESHOLD = 255
44
+ const RLE_UNIVERSE = 256
45
+
46
+ export class RunReader {
47
+ private source: ByteCodeReader
48
+ private sigma: number
49
+ private threshold: number
50
+
51
+ constructor(bytes: Uint8Array, sigma: number) {
52
+ this.source = new ByteCodeReader(bytes)
53
+ this.sigma = sigma === 0 ? Number.MAX_SAFE_INTEGER : sigma
54
+ this.threshold =
55
+ this.sigma < RLE_THRESHOLD ? Math.floor(RLE_UNIVERSE / this.sigma) : 0
56
+ }
57
+
58
+ next(): Run | undefined {
59
+ if (this.sigma >= RLE_THRESHOLD) {
60
+ const value = this.source.int()
61
+ const len = this.source.int()
62
+ return value === undefined || len === undefined
63
+ ? undefined
64
+ : { value, len: len + 1 }
65
+ }
66
+ const byte = this.source.byte()
67
+ if (byte === undefined) {
68
+ return undefined
69
+ }
70
+ const value = byte % this.sigma
71
+ let len = Math.floor(byte / this.sigma) + 1
72
+ if (len === this.threshold) {
73
+ const extra = this.source.int()
74
+ if (extra === undefined) {
75
+ return undefined
76
+ }
77
+ len += extra
78
+ }
79
+ return { value, len }
80
+ }
81
+
82
+ *[Symbol.iterator]() {
83
+ for (let run = this.next(); run !== undefined; run = this.next()) {
84
+ yield run
85
+ }
86
+ }
87
+ }