@gmod/gbz-base 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,25 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Colin Diesh
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
22
+
23
+ The test graphs under test/data derive from the gbwt-rs test data
24
+ (https://github.com/jltsiren/gbwt-rs, MIT License, Copyright (c) Jouni Sirén),
25
+ converted with unmodified gbz-base (https://github.com/jltsiren/gbz-base, MIT).
package/README.md ADDED
@@ -0,0 +1,125 @@
1
+ # @gmod/gbz-base
2
+
3
+ A pure TypeScript reader for [gbz-base](https://github.com/jltsiren/gbz-base)
4
+ pangenome databases (`.gbz.db`). It answers the same subgraph queries as
5
+ `gbz-base query`, reading only the SQLite pages a query touches, so a
6
+ multi-gigabyte database on an HTTP server is queried through range requests
7
+ without downloading it or compiling anything to WebAssembly.
8
+
9
+ No SQLite library is involved. The reader walks the SQLite b-trees directly
10
+ (rowid lookups, index seeks, overflow chains) and decodes the GBWT node records
11
+ the same way gbwt-rs does. Databases are produced by unmodified upstream
12
+ `gbz-base construct`.
13
+
14
+ ## Usage
15
+
16
+ ```ts
17
+ import { RemoteFile } from 'generic-filehandle2'
18
+ import { GBZBase, subgraphInInterval } from '@gmod/gbz-base'
19
+
20
+ const db = await GBZBase.open(
21
+ new RemoteFile('https://example.org/graph.gbz.db'),
22
+ )
23
+ const subgraph = await subgraphInInterval(
24
+ db,
25
+ { sample: 'GRCh38', contig: 'chr6' },
26
+ 31500000,
27
+ 31501000,
28
+ { context: 0, haplotypes: 'all' },
29
+ )
30
+ const { nodes, edges, paths } = subgraph.toJSON(true)
31
+ ```
32
+
33
+ `paths[0]` is the reference interval, named `GRCh38#0#chr6[start-end]`. Every
34
+ other entry is one haplotype's walk through the subgraph with a `cigar` relative
35
+ to the reference, computed like upstream: a node-length-weighted LCS, with the
36
+ diverging stretches scored using vg's match, mismatch and gap parameters.
37
+
38
+ Any object with `read(length, position)` and `stat()` works as a source, so
39
+ `LocalFile`, `RemoteFile` and `BlobFile` from `generic-filehandle2` all do.
40
+ Pages are fetched in blocks (64 KiB by default, `blockSize` in the open options)
41
+ and cached.
42
+
43
+ The command line mirrors the upstream tool for the query types it supports:
44
+
45
+ ```
46
+ gbz-base-query graph.gbz.db --sample GRCh38 --contig chr6 --interval 31500000..31501000 --cigar
47
+ gbz-base-query https://host/graph.gbz.db --contig chrM --offset 1000 --context 50 --stats
48
+ ```
49
+
50
+ `--stats` reports how many range requests a query made and how many bytes they
51
+ carried.
52
+
53
+ ## Naming haplotypes
54
+
55
+ Upstream gbz-base cannot say which haplotype a subgraph path belongs to, so it
56
+ emits `unknown#N`. This package adds that with two side tables that a small Rust
57
+ tool writes into an existing database, built on the unmodified upstream crates:
58
+
59
+ ```
60
+ cd tools/haplotype-index && cargo build --release
61
+ ./target/release/gbz-haplotype-index --interval 4096 graph.gbz graph.gbz.db
62
+ ./target/release/gbz-haplotype-index --interval 4096 --from-db graph.gbz.db
63
+ ```
64
+
65
+ The second form walks the paths through the database's own node records, so
66
+ a database whose GBZ is no longer at hand can still be augmented; the two
67
+ forms write identical tables.
68
+
69
+ `HaplotypeSamples` holds one GBWT position every `--interval` bp along every
70
+ path in both orientations, with the path handle and the forward coordinate of
71
+ that node, and `HaplotypeLengths` holds each path's length. The upstream `query`
72
+ binary keeps working on the augmented database.
73
+
74
+ At query time `subgraph.identifyPaths()` loads the samples for the window's node
75
+ range in one index scan, chains each haplotype's fragments to the next through
76
+ the private nodes between them, and walks at most one interval past the window
77
+ for a chain that met no sample inside it. `subgraph.alignments()` then gives one
78
+ record per fragment: PanSN name, strand, haplotype interval in that contig's
79
+ coordinates, reference interval, and a CIGAR clipped to the fragment's own
80
+ reference span. `toJSON(cigar, { names: 'resolved' })` names the paths the same
81
+ way. On the command line, `--resolve` and `--alignments`.
82
+
83
+ The tests check every resolved fragment against an independent backward walk
84
+ through the bidirectional GBWT to the path's recorded start position.
85
+
86
+ ## Fidelity
87
+
88
+ `test/data/oracle/` holds JSON written by upstream `gbz-base query` for the
89
+ queries listed in `queries.txt`, over databases built from gbwt-rs's test graphs
90
+ (`micb-kir3dl1.gbz`, a 46-sample HPRC slice; `example.gbz`; `example-v3.gbz`).
91
+ The test suite requires this library's output to be deep equal to every one of
92
+ them, CIGAR strings included. `generate.sh` regenerates the oracle with an
93
+ upstream binary.
94
+
95
+ Not ported: snarl extension (`--snarls`, `--between`), GFA output, GAF-base.
96
+
97
+ CIGARs are computed by matching each shared node to its earliest usable
98
+ occurrence on the reference walk, which is weight-optimal whenever every shared
99
+ node can be placed in order; the Myers-based weighted LCS from gbwt-rs runs only
100
+ for the fragments where that fails (inversions, repeats). The two can pick
101
+ different equal-weight alignments only when the reference walk repeats a node.
102
+
103
+ ## Measured
104
+
105
+ Against a 134 MB HPRC chr20 `.gbz.db` served over HTTPS,
106
+ `--sample GRCh38 --contig chr20`:
107
+
108
+ | window | context | nodes | haplotype fragments | range requests | bytes read | time |
109
+ | ------ | ------- | ----- | ------------------- | -------------- | ---------- | ----- |
110
+ | 500 bp | 0 | 17 | 11 | 7 | 459 KB | 1.4 s |
111
+ | 10 kb | 0 | 627 | 1021 | 8 | 524 KB | 2.1 s |
112
+ | 100 kb | 0 | 2051 | 3673 | 13 | 852 KB | 1.7 s |
113
+ | 10 kb | 100 bp | 1086 | 35 | 9 | 590 KB | 0.8 s |
114
+
115
+ Most of the wall time in the small queries is sequential request latency, not
116
+ decoding.
117
+
118
+ ## Footnote
119
+
120
+ Started from ideas at MemPanG 26! Earlier work considered WASM cross compilation
121
+ of the rust code but gbwt-rs and simple-sds serialize `usize` at native width,
122
+ so their wasm32 builds misread files written on 64-bit hosts, and the maintainer
123
+ declined a 32-bit port. wasm64 fixes that for gbwt-rs but cannot carry gbz-base,
124
+ whose bundled SQLite has no wasm64 libc. A TypeScript reader has neither problem
125
+ and runs in a JBrowse RPC worker with the file access layer JBrowse already has.
package/bin/query.js ADDED
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env node
2
+ import { main } from '../dist/cli.js'
3
+
4
+ main(process.argv.slice(2)).catch(error => {
5
+ process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`)
6
+ process.exit(1)
7
+ })
package/dist/cli.d.ts ADDED
@@ -0,0 +1 @@
1
+ export declare function main(argv: string[]): Promise<void>;
package/dist/cli.js ADDED
@@ -0,0 +1,140 @@
1
+ import { LocalFile, RemoteFile } from 'generic-filehandle2';
2
+ import { GBZBase, formatPathName } from "./db.js";
3
+ import { subgraphAroundNodes, subgraphAtOffset, subgraphInInterval } from "./query.js";
4
+ const USAGE = `Usage: gbz-base-query [options] graph.gbz.db
5
+
6
+ --sample STR sample name (default: generic path)
7
+ --contig STR contig name (required for --offset and --interval)
8
+ --haplotype INT haplotype number (default: 0)
9
+ -o, --offset INT sequence offset
10
+ -i, --interval A..B half-open sequence interval
11
+ -n, --node INT node identifier (may repeat)
12
+ --context INT context length in bp (default: 100)
13
+ --limit INT safety limit for the number of nodes
14
+ --haplotypes SEL all, distinct, reference-only or none (default: all)
15
+ --cigar output CIGAR strings for the haplotypes
16
+ --resolve name haplotypes from the HaplotypeSamples table
17
+ --alignments print one alignment record per haplotype fragment instead of the subgraph
18
+ --block-size INT bytes fetched per range request (default: 65536)
19
+ --stats print fetch statistics to stderr
20
+ `;
21
+ function parseArgs(argv) {
22
+ const args = {
23
+ file: '',
24
+ haplotype: 0,
25
+ nodes: [],
26
+ context: 100,
27
+ haplotypes: 'all',
28
+ cigar: false,
29
+ resolve: false,
30
+ alignments: false,
31
+ blockSize: 65536,
32
+ stats: false,
33
+ };
34
+ const next = (i) => {
35
+ const value = argv[i + 1];
36
+ if (value === undefined) {
37
+ throw new Error(`${argv[i]} needs a value`);
38
+ }
39
+ return value;
40
+ };
41
+ for (let i = 0; i < argv.length; i++) {
42
+ const arg = argv[i];
43
+ switch (arg) {
44
+ case '--sample':
45
+ args.sample = next(i++);
46
+ break;
47
+ case '--contig':
48
+ args.contig = next(i++);
49
+ break;
50
+ case '--haplotype':
51
+ args.haplotype = Number(next(i++));
52
+ break;
53
+ case '-o':
54
+ case '--offset':
55
+ args.offset = Number(next(i++));
56
+ break;
57
+ case '-i':
58
+ case '--interval': {
59
+ const [a, b] = next(i++).split('..');
60
+ args.interval = [Number(a), Number(b)];
61
+ break;
62
+ }
63
+ case '-n':
64
+ case '--node':
65
+ args.nodes.push(Number(next(i++)));
66
+ break;
67
+ case '--context':
68
+ args.context = Number(next(i++));
69
+ break;
70
+ case '--limit':
71
+ args.limit = Number(next(i++));
72
+ break;
73
+ case '--haplotypes':
74
+ args.haplotypes = next(i++);
75
+ break;
76
+ case '--cigar':
77
+ args.cigar = true;
78
+ break;
79
+ case '--resolve':
80
+ args.resolve = true;
81
+ break;
82
+ case '--alignments':
83
+ args.alignments = true;
84
+ args.resolve = true;
85
+ break;
86
+ case '--block-size':
87
+ args.blockSize = Number(next(i++));
88
+ break;
89
+ case '--stats':
90
+ args.stats = true;
91
+ break;
92
+ case '--format':
93
+ next(i++);
94
+ break;
95
+ case '-h':
96
+ case '--help':
97
+ process.stdout.write(USAGE);
98
+ process.exit(0);
99
+ break;
100
+ default:
101
+ if (arg.startsWith('-')) {
102
+ throw new Error(`Unknown option ${arg}`);
103
+ }
104
+ args.file = arg;
105
+ }
106
+ }
107
+ if (!args.file) {
108
+ throw new Error(USAGE);
109
+ }
110
+ return args;
111
+ }
112
+ export async function main(argv) {
113
+ const args = parseArgs(argv);
114
+ const source = /^https?:\/\//.test(args.file) ? new RemoteFile(args.file) : new LocalFile(args.file);
115
+ const db = await GBZBase.open(source, { blockSize: args.blockSize });
116
+ const opts = { context: args.context, haplotypes: args.haplotypes, ...(args.limit === undefined ? {} : { limit: args.limit }) };
117
+ const query = { contig: args.contig ?? '', haplotype: args.haplotype, ...(args.sample === undefined ? {} : { sample: args.sample }) };
118
+ const subgraph = args.nodes.length > 0
119
+ ? await subgraphAroundNodes(db, args.nodes, opts)
120
+ : args.interval
121
+ ? await subgraphInInterval(db, query, args.interval[0], args.interval[1], opts)
122
+ : args.offset !== undefined
123
+ ? await subgraphAtOffset(db, query, args.offset, opts)
124
+ : undefined;
125
+ if (!subgraph) {
126
+ throw new Error('Query type must be specified using --offset, --interval or --node');
127
+ }
128
+ if (args.resolve) {
129
+ await subgraph.identifyPaths();
130
+ }
131
+ const output = args.alignments
132
+ ? subgraph.alignments().map(a => ({ ...a, name: a.name ? formatPathName(a.name, a.name.fragment) : undefined, start: undefined }))
133
+ : subgraph.toJSON(args.cigar, { names: args.resolve ? 'resolved' : 'anonymous' });
134
+ process.stdout.write(`${JSON.stringify(output)}\n`);
135
+ if (args.stats) {
136
+ const { fetches, bytesFetched } = db.sqlite.pager;
137
+ const { orderedAlignments, lcsAlignments, identificationSteps, identificationFetches } = subgraph.stats;
138
+ process.stderr.write(`Subgraph contains ${subgraph.nodeCount} nodes and ${subgraph.pathCount} paths; ${fetches} fetches, ${bytesFetched} bytes; ${orderedAlignments} ordered + ${lcsAlignments} lcs alignments; identification ${identificationSteps} steps, ${identificationFetches} lookups\n`);
139
+ }
140
+ }
package/dist/db.d.ts ADDED
@@ -0,0 +1,72 @@
1
+ import type { ByteSource } from './filehandle.ts';
2
+ import { GbwtRecord } from './gbwt/record.ts';
3
+ import type { Pos } from './gbwt/record.ts';
4
+ import { SqliteDatabase } from './sqlite/database.ts';
5
+ import type { PagerOptions } from './sqlite/pager.ts';
6
+ export interface PathName {
7
+ sample: string;
8
+ contig: string;
9
+ haplotype: number;
10
+ fragment: number;
11
+ }
12
+ export declare const GENERIC_SAMPLE = "_gbwt_ref";
13
+ export declare const SCHEMA_VERSION = "GBZ-base version 4";
14
+ export declare class SchemaVersionError extends Error {
15
+ readonly found: string | undefined;
16
+ name: string;
17
+ constructor(found: string | undefined);
18
+ }
19
+ export declare function formatPathName(name: PathName, end: number): string;
20
+ export interface HaplotypeSample {
21
+ node: number;
22
+ offset: number;
23
+ pathHandle: number;
24
+ orientation: 'forward' | 'reverse';
25
+ pathOffset: number;
26
+ }
27
+ export interface GbzPath {
28
+ handle: number;
29
+ fwStart: Pos;
30
+ revStart: Pos;
31
+ name: PathName;
32
+ isIndexed: boolean;
33
+ }
34
+ export declare class GbzRecord {
35
+ readonly handle: number;
36
+ readonly edges: Pos[];
37
+ readonly bwt: Uint8Array;
38
+ readonly encodedSequence: Uint8Array;
39
+ readonly next: number | undefined;
40
+ readonly sequenceLen: number;
41
+ private decoded;
42
+ constructor(handle: number, edges: Pos[], bwt: Uint8Array, encodedSequence: Uint8Array, next: number | undefined);
43
+ get id(): number;
44
+ get orientation(): import("./gbwt/node.ts").Orientation;
45
+ get sequence(): string;
46
+ successors(): number[];
47
+ gbwt(): GbwtRecord;
48
+ }
49
+ export declare class GBZBase {
50
+ readonly sqlite: SqliteDatabase;
51
+ private tagCache;
52
+ private pathCache;
53
+ private constructor();
54
+ static open(source: ByteSource, opts?: PagerOptions): Promise<GBZBase>;
55
+ tags(): Promise<Map<string, string>>;
56
+ tag(key: string): Promise<string | undefined>;
57
+ getRecord(handle: number): Promise<GbzRecord | undefined>;
58
+ paths(): Promise<GbzPath[]>;
59
+ getPath(handle: number): Promise<GbzPath | undefined>;
60
+ findPath(name: PathName): Promise<GbzPath | undefined>;
61
+ pathsForSample(sample: string): Promise<GbzPath[]>;
62
+ get hasHaplotypeIndex(): boolean;
63
+ haplotypeSampleInterval(): Promise<number | undefined>;
64
+ private sampleFromRow;
65
+ haplotypeSamplesInRange(minHandle: number, maxHandle: number): Promise<HaplotypeSample[]>;
66
+ haplotypeSampleAt(node: number, offset: number): Promise<HaplotypeSample | undefined>;
67
+ haplotypeLength(pathHandle: number): Promise<number | undefined>;
68
+ indexedPosition(pathHandle: number, pathOffset: number): Promise<{
69
+ pathOffset: number;
70
+ pos: Pos;
71
+ } | undefined>;
72
+ }
package/dist/db.js ADDED
@@ -0,0 +1,208 @@
1
+ import { GbwtRecord, decompressEdges } from "./gbwt/record.js";
2
+ import { ENDMARKER, nodeId, nodeOrientation } from "./gbwt/node.js";
3
+ import { decodeSequence, encodedSequenceLength } from "./gbwt/sequence.js";
4
+ import { SqliteDatabase } from "./sqlite/database.js";
5
+ export const GENERIC_SAMPLE = '_gbwt_ref';
6
+ export const SCHEMA_VERSION = 'GBZ-base version 4';
7
+ export class SchemaVersionError extends Error {
8
+ found;
9
+ name = 'SchemaVersionError';
10
+ constructor(found) {
11
+ super(found === undefined
12
+ ? `not a gbz-base database: its Tags table has no version`
13
+ : `unsupported database schema "${found}"; this reader understands "${SCHEMA_VERSION}"`);
14
+ this.found = found;
15
+ }
16
+ }
17
+ export function formatPathName(name, end) {
18
+ return `${name.sample}#${name.haplotype}#${name.contig}[${name.fragment}-${end}]`;
19
+ }
20
+ export class GbzRecord {
21
+ handle;
22
+ edges;
23
+ bwt;
24
+ encodedSequence;
25
+ next;
26
+ sequenceLen;
27
+ decoded;
28
+ constructor(handle, edges, bwt, encodedSequence, next) {
29
+ this.handle = handle;
30
+ this.edges = edges;
31
+ this.bwt = bwt;
32
+ this.encodedSequence = encodedSequence;
33
+ this.next = next;
34
+ this.sequenceLen = encodedSequenceLength(encodedSequence);
35
+ }
36
+ get id() {
37
+ return nodeId(this.handle);
38
+ }
39
+ get orientation() {
40
+ return nodeOrientation(this.handle);
41
+ }
42
+ get sequence() {
43
+ this.decoded ??= decodeSequence(this.encodedSequence);
44
+ return this.decoded;
45
+ }
46
+ successors() {
47
+ return this.edges.filter(e => e.node !== ENDMARKER).map(e => e.node);
48
+ }
49
+ gbwt() {
50
+ if (this.edges.length === 0) {
51
+ throw new Error(`GBWT record for handle ${this.handle} is empty`);
52
+ }
53
+ return new GbwtRecord(this.edges, this.bwt);
54
+ }
55
+ }
56
+ function num(value, what) {
57
+ if (typeof value !== 'number') {
58
+ throw new Error(`${what} is not a number in the database`);
59
+ }
60
+ return value;
61
+ }
62
+ function str(value, what) {
63
+ if (typeof value !== 'string') {
64
+ throw new Error(`${what} is not text in the database`);
65
+ }
66
+ return value;
67
+ }
68
+ function blob(value, what) {
69
+ if (!(value instanceof Uint8Array)) {
70
+ throw new Error(`${what} is not a blob in the database`);
71
+ }
72
+ return value;
73
+ }
74
+ function rowToPath(rowid, values) {
75
+ return {
76
+ handle: rowid,
77
+ fwStart: { node: num(values[1], 'Paths.fw_node'), offset: num(values[2], 'Paths.fw_offset') },
78
+ revStart: { node: num(values[3], 'Paths.rev_node'), offset: num(values[4], 'Paths.rev_offset') },
79
+ name: {
80
+ sample: str(values[5], 'Paths.sample'),
81
+ contig: str(values[6], 'Paths.contig'),
82
+ haplotype: num(values[7], 'Paths.haplotype'),
83
+ fragment: num(values[8], 'Paths.fragment'),
84
+ },
85
+ isIndexed: num(values[9], 'Paths.is_indexed') !== 0,
86
+ };
87
+ }
88
+ export class GBZBase {
89
+ sqlite;
90
+ tagCache;
91
+ pathCache;
92
+ constructor(sqlite) {
93
+ this.sqlite = sqlite;
94
+ }
95
+ static async open(source, opts = {}) {
96
+ const sqlite = await SqliteDatabase.open(source, opts);
97
+ for (const table of ['Tags', 'Nodes', 'Paths', 'ReferenceIndex']) {
98
+ sqlite.rootPage(table);
99
+ }
100
+ const db = new GBZBase(sqlite);
101
+ const version = await db.tag('version');
102
+ if (version !== SCHEMA_VERSION) {
103
+ throw new SchemaVersionError(version);
104
+ }
105
+ return db;
106
+ }
107
+ tags() {
108
+ this.tagCache ??= (async () => {
109
+ const tags = new Map();
110
+ for await (const { values } of this.sqlite.scan('Tags')) {
111
+ tags.set(str(values[0], 'Tags.key'), str(values[1], 'Tags.value'));
112
+ }
113
+ return tags;
114
+ })();
115
+ return this.tagCache;
116
+ }
117
+ async tag(key) {
118
+ return (await this.tags()).get(key);
119
+ }
120
+ async getRecord(handle) {
121
+ const row = await this.sqlite.byRowid('Nodes', handle);
122
+ if (!row) {
123
+ return undefined;
124
+ }
125
+ const next = row[4];
126
+ return new GbzRecord(handle, decompressEdges(blob(row[1], 'Nodes.edges')), blob(row[2], 'Nodes.bwt'), blob(row[3], 'Nodes.sequence'), typeof next === 'number' ? next : undefined);
127
+ }
128
+ paths() {
129
+ this.pathCache ??= (async () => {
130
+ const paths = [];
131
+ for await (const { rowid, values } of this.sqlite.scan('Paths')) {
132
+ paths.push(rowToPath(rowid, values));
133
+ }
134
+ return paths;
135
+ })();
136
+ return this.pathCache;
137
+ }
138
+ async getPath(handle) {
139
+ const row = await this.sqlite.byRowid('Paths', handle);
140
+ return row ? rowToPath(handle, row) : undefined;
141
+ }
142
+ async findPath(name) {
143
+ const candidates = (await this.paths()).filter(p => p.name.sample === name.sample &&
144
+ p.name.contig === name.contig &&
145
+ p.name.haplotype === name.haplotype &&
146
+ p.name.fragment <= name.fragment);
147
+ return candidates.sort((a, b) => b.name.fragment - a.name.fragment)[0];
148
+ }
149
+ async pathsForSample(sample) {
150
+ return (await this.paths()).filter(p => p.name.sample === sample);
151
+ }
152
+ get hasHaplotypeIndex() {
153
+ return this.sqlite.has('HaplotypeSamples') && this.sqlite.has('HaplotypeLengths');
154
+ }
155
+ async haplotypeSampleInterval() {
156
+ const value = await this.tag('haplotype_index_interval');
157
+ return value === undefined ? undefined : Number(value);
158
+ }
159
+ sampleFromRow(values) {
160
+ return {
161
+ node: num(values[0], 'HaplotypeSamples.node_handle'),
162
+ offset: num(values[1], 'HaplotypeSamples.node_offset'),
163
+ pathHandle: num(values[2], 'HaplotypeSamples.path_handle'),
164
+ orientation: num(values[3], 'HaplotypeSamples.orientation') === 0 ? 'forward' : 'reverse',
165
+ pathOffset: num(values[4], 'HaplotypeSamples.path_offset'),
166
+ };
167
+ }
168
+ async haplotypeSamplesInRange(minHandle, maxHandle) {
169
+ const samples = [];
170
+ for await (const key of this.sqlite.indexScanFrom('HaplotypeSamples', [minHandle, 0])) {
171
+ const node = num(key[0], 'HaplotypeSamples.node_handle');
172
+ if (node > maxHandle) {
173
+ break;
174
+ }
175
+ const row = await this.sqlite.byRowid('HaplotypeSamples', num(key[2], 'HaplotypeSamples rowid'));
176
+ if (row) {
177
+ samples.push(this.sampleFromRow(row));
178
+ }
179
+ }
180
+ return samples;
181
+ }
182
+ async haplotypeSampleAt(node, offset) {
183
+ const key = await this.sqlite.indexSeekLE('HaplotypeSamples', [node, offset]);
184
+ if (!key || key[0] !== node || key[1] !== offset) {
185
+ return undefined;
186
+ }
187
+ const row = await this.sqlite.byRowid('HaplotypeSamples', num(key[2], 'HaplotypeSamples rowid'));
188
+ return row ? this.sampleFromRow(row) : undefined;
189
+ }
190
+ async haplotypeLength(pathHandle) {
191
+ const row = await this.sqlite.byRowid('HaplotypeLengths', pathHandle);
192
+ return row ? num(row[1], 'HaplotypeLengths.length') : undefined;
193
+ }
194
+ async indexedPosition(pathHandle, pathOffset) {
195
+ const key = await this.sqlite.indexSeekLE('ReferenceIndex', [pathHandle, pathOffset]);
196
+ if (!key || key[0] !== pathHandle) {
197
+ return undefined;
198
+ }
199
+ const row = await this.sqlite.byRowid('ReferenceIndex', num(key[2], 'ReferenceIndex rowid'));
200
+ if (!row) {
201
+ throw new Error('ReferenceIndex row referenced by its index is missing');
202
+ }
203
+ return {
204
+ pathOffset: num(row[1], 'ReferenceIndex.path_offset'),
205
+ pos: { node: num(row[2], 'ReferenceIndex.node_handle'), offset: num(row[3], 'ReferenceIndex.node_offset') },
206
+ };
207
+ }
208
+ }
@@ -0,0 +1,6 @@
1
+ export interface ByteSource {
2
+ read(length: number, position: number): Promise<Uint8Array>;
3
+ stat(): Promise<{
4
+ size: number;
5
+ }>;
6
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,20 @@
1
+ export declare class ByteCodeReader {
2
+ private bytes;
3
+ offset: number;
4
+ constructor(bytes: Uint8Array);
5
+ get done(): boolean;
6
+ byte(): number | undefined;
7
+ int(): number | undefined;
8
+ }
9
+ export interface Run {
10
+ value: number;
11
+ len: number;
12
+ }
13
+ export declare class RunReader {
14
+ private source;
15
+ private sigma;
16
+ private threshold;
17
+ constructor(bytes: Uint8Array, sigma: number);
18
+ next(): Run | undefined;
19
+ [Symbol.iterator](): Generator<Run, void, unknown>;
20
+ }
@@ -0,0 +1,70 @@
1
+ export class ByteCodeReader {
2
+ bytes;
3
+ offset = 0;
4
+ constructor(bytes) {
5
+ this.bytes = bytes;
6
+ }
7
+ get done() {
8
+ return this.offset >= this.bytes.length;
9
+ }
10
+ byte() {
11
+ const value = this.bytes[this.offset];
12
+ if (value === undefined) {
13
+ return undefined;
14
+ }
15
+ this.offset += 1;
16
+ return value;
17
+ }
18
+ int() {
19
+ let shift = 1;
20
+ let result = 0;
21
+ while (this.offset < this.bytes.length) {
22
+ const value = this.bytes[this.offset];
23
+ this.offset += 1;
24
+ result += (value & 0x7f) * shift;
25
+ shift *= 128;
26
+ if ((value & 0x80) === 0) {
27
+ return result;
28
+ }
29
+ }
30
+ return undefined;
31
+ }
32
+ }
33
+ const RLE_THRESHOLD = 255;
34
+ const RLE_UNIVERSE = 256;
35
+ export class RunReader {
36
+ source;
37
+ sigma;
38
+ threshold;
39
+ constructor(bytes, sigma) {
40
+ this.source = new ByteCodeReader(bytes);
41
+ this.sigma = sigma === 0 ? Number.MAX_SAFE_INTEGER : sigma;
42
+ this.threshold = this.sigma < RLE_THRESHOLD ? Math.floor(RLE_UNIVERSE / this.sigma) : 0;
43
+ }
44
+ next() {
45
+ if (this.sigma >= RLE_THRESHOLD) {
46
+ const value = this.source.int();
47
+ const len = this.source.int();
48
+ return value === undefined || len === undefined ? undefined : { value, len: len + 1 };
49
+ }
50
+ const byte = this.source.byte();
51
+ if (byte === undefined) {
52
+ return undefined;
53
+ }
54
+ const value = byte % this.sigma;
55
+ let len = Math.floor(byte / this.sigma) + 1;
56
+ if (len === this.threshold) {
57
+ const extra = this.source.int();
58
+ if (extra === undefined) {
59
+ return undefined;
60
+ }
61
+ len += extra;
62
+ }
63
+ return { value, len };
64
+ }
65
+ *[Symbol.iterator]() {
66
+ for (let run = this.next(); run !== undefined; run = this.next()) {
67
+ yield run;
68
+ }
69
+ }
70
+ }