@gmod/gbz-base 0.0.1 → 1.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.
Files changed (62) hide show
  1. package/README.md +45 -4
  2. package/bin/query.js +3 -1
  3. package/dist/cli.js +85 -19
  4. package/dist/cli.js.map +1 -0
  5. package/dist/db.d.ts +14 -6
  6. package/dist/db.js +92 -28
  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.d.ts +4 -0
  16. package/dist/gbwt/record.js +30 -2
  17. package/dist/gbwt/record.js.map +1 -0
  18. package/dist/gbwt/sequence.js +8 -1
  19. package/dist/gbwt/sequence.js.map +1 -0
  20. package/dist/graphName.d.ts +9 -0
  21. package/dist/graphName.js +83 -0
  22. package/dist/graphName.js.map +1 -0
  23. package/dist/index.d.ts +5 -4
  24. package/dist/index.js +3 -2
  25. package/dist/index.js.map +1 -0
  26. package/dist/lcs.js +32 -10
  27. package/dist/lcs.js.map +1 -0
  28. package/dist/query.d.ts +4 -2
  29. package/dist/query.js +19 -0
  30. package/dist/query.js.map +1 -0
  31. package/dist/sqlite/btree.d.ts +1 -1
  32. package/dist/sqlite/btree.js +24 -7
  33. package/dist/sqlite/btree.js.map +1 -0
  34. package/dist/sqlite/database.d.ts +1 -1
  35. package/dist/sqlite/database.js +12 -2
  36. package/dist/sqlite/database.js.map +1 -0
  37. package/dist/sqlite/pager.d.ts +8 -3
  38. package/dist/sqlite/pager.js +71 -12
  39. package/dist/sqlite/pager.js.map +1 -0
  40. package/dist/sqlite/record.js +3 -1
  41. package/dist/sqlite/record.js.map +1 -0
  42. package/dist/subgraph.d.ts +12 -2
  43. package/dist/subgraph.js +413 -84
  44. package/dist/subgraph.js.map +1 -0
  45. package/package.json +36 -11
  46. package/src/cli.ts +258 -0
  47. package/src/db.ts +374 -0
  48. package/src/filehandle.ts +4 -0
  49. package/src/gbwt/bytecode.ts +87 -0
  50. package/src/gbwt/node.ts +64 -0
  51. package/src/gbwt/record.ts +177 -0
  52. package/src/gbwt/sequence.ts +50 -0
  53. package/src/graphName.ts +94 -0
  54. package/src/index.ts +34 -0
  55. package/src/lcs.ts +282 -0
  56. package/src/query.ts +106 -0
  57. package/src/sqlite/btree.ts +260 -0
  58. package/src/sqlite/database.ts +117 -0
  59. package/src/sqlite/pager.ts +148 -0
  60. package/src/sqlite/record.ts +100 -0
  61. package/src/subgraph.ts +1353 -0
  62. package/tools/haplotype-index/src/main.rs +197 -163
package/src/db.ts ADDED
@@ -0,0 +1,374 @@
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 { graphNameFromTags } from './graphName.ts'
5
+ import { SqliteDatabase } from './sqlite/database.ts'
6
+
7
+ import type { ByteSource } from './filehandle.ts'
8
+ import type { Pos } from './gbwt/record.ts'
9
+ import type { GraphName } from './graphName.ts'
10
+ import type { PagerOptions } from './sqlite/pager.ts'
11
+ import type { SqlValue } from './sqlite/record.ts'
12
+
13
+ export interface PathName {
14
+ sample: string
15
+ contig: string
16
+ haplotype: number
17
+ fragment: number
18
+ }
19
+
20
+ export const GENERIC_SAMPLE = '_gbwt_ref'
21
+ export const SCHEMA_VERSION = 'GBZ-base version 4'
22
+
23
+ export class SchemaVersionError extends Error {
24
+ override name = 'SchemaVersionError'
25
+
26
+ readonly found: string | undefined
27
+
28
+ constructor(found: string | undefined) {
29
+ super(
30
+ found === undefined
31
+ ? `not a gbz-base database: its Tags table has no version`
32
+ : `unsupported database schema "${found}"; this reader understands "${SCHEMA_VERSION}"`,
33
+ )
34
+ this.found = found
35
+ }
36
+ }
37
+
38
+ export function formatPathName(name: PathName, end: number) {
39
+ return `${name.sample}#${name.haplotype}#${name.contig}[${name.fragment}-${end}]`
40
+ }
41
+
42
+ export interface HaplotypeSample {
43
+ node: number
44
+ offset: number
45
+ pathHandle: number
46
+ orientation: 'forward' | 'reverse'
47
+ pathOffset: number
48
+ }
49
+
50
+ export interface GbzPath {
51
+ handle: number
52
+ fwStart: Pos
53
+ revStart: Pos
54
+ name: PathName
55
+ isIndexed: boolean
56
+ }
57
+
58
+ export class GbzRecord {
59
+ readonly sequenceLen: number
60
+ readonly handle: number
61
+ readonly edges: Pos[]
62
+ readonly bwt: Uint8Array
63
+ readonly encodedSequence: Uint8Array
64
+ readonly next: number | undefined
65
+ private decoded: string | undefined
66
+
67
+ constructor(
68
+ handle: number,
69
+ edges: Pos[],
70
+ bwt: Uint8Array,
71
+ encodedSequence: Uint8Array,
72
+ next: number | undefined,
73
+ ) {
74
+ this.handle = handle
75
+ this.edges = edges
76
+ this.bwt = bwt
77
+ this.encodedSequence = encodedSequence
78
+ this.next = next
79
+ this.sequenceLen = encodedSequenceLength(encodedSequence)
80
+ }
81
+
82
+ get id() {
83
+ return nodeId(this.handle)
84
+ }
85
+
86
+ get orientation() {
87
+ return nodeOrientation(this.handle)
88
+ }
89
+
90
+ get sequence() {
91
+ this.decoded ??= decodeSequence(this.encodedSequence)
92
+ return this.decoded
93
+ }
94
+
95
+ successors() {
96
+ return this.edges.filter(e => e.node !== ENDMARKER).map(e => e.node)
97
+ }
98
+
99
+ gbwt() {
100
+ if (this.edges.length === 0) {
101
+ throw new Error(`GBWT record for handle ${this.handle} is empty`)
102
+ }
103
+ return new GbwtRecord(this.edges, this.bwt)
104
+ }
105
+ }
106
+
107
+ function num(value: SqlValue | undefined, what: string) {
108
+ if (typeof value !== 'number') {
109
+ throw new Error(`${what} is not a number in the database`)
110
+ }
111
+ return value
112
+ }
113
+
114
+ function str(value: SqlValue | undefined, what: string) {
115
+ if (typeof value !== 'string') {
116
+ throw new Error(`${what} is not text in the database`)
117
+ }
118
+ return value
119
+ }
120
+
121
+ function blob(value: SqlValue | undefined, what: string) {
122
+ if (!(value instanceof Uint8Array)) {
123
+ throw new Error(`${what} is not a blob in the database`)
124
+ }
125
+ return value
126
+ }
127
+
128
+ function rowToPath(rowid: number, values: SqlValue[]): GbzPath {
129
+ return {
130
+ handle: rowid,
131
+ fwStart: {
132
+ node: num(values[1], 'Paths.fw_node'),
133
+ offset: num(values[2], 'Paths.fw_offset'),
134
+ },
135
+ revStart: {
136
+ node: num(values[3], 'Paths.rev_node'),
137
+ offset: num(values[4], 'Paths.rev_offset'),
138
+ },
139
+ name: {
140
+ sample: str(values[5], 'Paths.sample'),
141
+ contig: str(values[6], 'Paths.contig'),
142
+ haplotype: num(values[7], 'Paths.haplotype'),
143
+ fragment: num(values[8], 'Paths.fragment'),
144
+ },
145
+ isIndexed: num(values[9], 'Paths.is_indexed') !== 0,
146
+ }
147
+ }
148
+
149
+ export interface OpenOptions extends PagerOptions {
150
+ haplotypeIndex?: ByteSource
151
+ }
152
+
153
+ async function readTags(sqlite: SqliteDatabase) {
154
+ const tags = new Map<string, string>()
155
+ for await (const { values } of sqlite.scan('Tags')) {
156
+ tags.set(str(values[0], 'Tags.key'), str(values[1], 'Tags.value'))
157
+ }
158
+ return tags
159
+ }
160
+
161
+ export class GBZBase {
162
+ private tagCache: Promise<Map<string, string>> | undefined
163
+ private pathCache: Promise<GbzPath[]> | undefined
164
+ private indexTags: Map<string, string> | undefined
165
+
166
+ readonly sqlite: SqliteDatabase
167
+ private readonly index: SqliteDatabase
168
+
169
+ private constructor(sqlite: SqliteDatabase, index: SqliteDatabase) {
170
+ this.sqlite = sqlite
171
+ this.index = index
172
+ }
173
+
174
+ static async open(source: ByteSource, opts: OpenOptions = {}) {
175
+ const { haplotypeIndex, ...pagerOptions } = opts
176
+ const sqlite = await SqliteDatabase.open(source, pagerOptions)
177
+ for (const table of ['Tags', 'Nodes', 'Paths', 'ReferenceIndex']) {
178
+ sqlite.rootPage(table)
179
+ }
180
+ const index = haplotypeIndex
181
+ ? await SqliteDatabase.open(haplotypeIndex, pagerOptions)
182
+ : sqlite
183
+ const db = new GBZBase(sqlite, index)
184
+ const version = await db.tag('version')
185
+ if (version !== SCHEMA_VERSION) {
186
+ throw new SchemaVersionError(version)
187
+ }
188
+ if (haplotypeIndex) {
189
+ for (const table of ['Tags', 'HaplotypeSamples', 'HaplotypeLengths']) {
190
+ index.rootPage(table)
191
+ }
192
+ db.indexTags = await readTags(index)
193
+ const indexed = db.indexTags.get('haplotype_index_paths')
194
+ const paths = await db.tag('paths')
195
+ if (indexed !== paths) {
196
+ throw new Error(
197
+ `haplotype index was built for ${indexed ?? 'an unknown number of'} paths but the graph has ${paths}`,
198
+ )
199
+ }
200
+ }
201
+ return db
202
+ }
203
+
204
+ tags() {
205
+ this.tagCache ??= readTags(this.sqlite)
206
+ return this.tagCache
207
+ }
208
+
209
+ async tag(key: string) {
210
+ return (await this.tags()).get(key)
211
+ }
212
+
213
+ async getRecord(handle: number) {
214
+ const row = await this.sqlite.byRowid('Nodes', handle)
215
+ if (!row) {
216
+ return undefined
217
+ }
218
+ const next = row[4]
219
+ return new GbzRecord(
220
+ handle,
221
+ decompressEdges(blob(row[1], 'Nodes.edges')),
222
+ blob(row[2], 'Nodes.bwt'),
223
+ blob(row[3], 'Nodes.sequence'),
224
+ typeof next === 'number' ? next : undefined,
225
+ )
226
+ }
227
+
228
+ paths() {
229
+ this.pathCache ??= (async () => {
230
+ const paths: GbzPath[] = []
231
+ for await (const { rowid, values } of this.sqlite.scan('Paths')) {
232
+ paths.push(rowToPath(rowid, values))
233
+ }
234
+ return paths
235
+ })()
236
+ return this.pathCache
237
+ }
238
+
239
+ async getPath(handle: number) {
240
+ const row = await this.sqlite.byRowid('Paths', handle)
241
+ return row ? rowToPath(handle, row) : undefined
242
+ }
243
+
244
+ async findPath(name: PathName) {
245
+ const candidates = (await this.paths()).filter(
246
+ p =>
247
+ p.name.sample === name.sample &&
248
+ p.name.contig === name.contig &&
249
+ p.name.haplotype === name.haplotype &&
250
+ p.name.fragment <= name.fragment,
251
+ )
252
+ return candidates.sort((a, b) => b.name.fragment - a.name.fragment)[0]
253
+ }
254
+
255
+ async pathsForSample(sample: string) {
256
+ return (await this.paths()).filter(p => p.name.sample === sample)
257
+ }
258
+
259
+ async graphName() {
260
+ const gbzTags = new Map<string, string>()
261
+ for (const [key, value] of await this.tags()) {
262
+ if (key.startsWith('gbz_')) {
263
+ gbzTags.set(key.slice('gbz_'.length), value)
264
+ }
265
+ }
266
+ let name: GraphName = {
267
+ name: undefined,
268
+ subgraph: new Map(),
269
+ translation: new Map(),
270
+ }
271
+ try {
272
+ name = graphNameFromTags(gbzTags)
273
+ } catch {
274
+ // upstream falls back to an empty name when the tags do not parse
275
+ }
276
+ return name
277
+ }
278
+
279
+ async hasChainLinks() {
280
+ const links = await this.tag('chain_links')
281
+ return links !== undefined && Number(links) > 0
282
+ }
283
+
284
+ get hasHaplotypeIndex() {
285
+ return (
286
+ this.index.has('HaplotypeSamples') && this.index.has('HaplotypeLengths')
287
+ )
288
+ }
289
+
290
+ async haplotypeSampleInterval() {
291
+ const value = this.indexTags
292
+ ? this.indexTags.get('haplotype_index_interval')
293
+ : await this.tag('haplotype_index_interval')
294
+ return value === undefined ? undefined : Number(value)
295
+ }
296
+
297
+ private sampleFromRow(values: SqlValue[]): HaplotypeSample {
298
+ return {
299
+ node: num(values[0], 'HaplotypeSamples.node_handle'),
300
+ offset: num(values[1], 'HaplotypeSamples.node_offset'),
301
+ pathHandle: num(values[2], 'HaplotypeSamples.path_handle'),
302
+ orientation:
303
+ num(values[3], 'HaplotypeSamples.orientation') === 0
304
+ ? 'forward'
305
+ : 'reverse',
306
+ pathOffset: num(values[4], 'HaplotypeSamples.path_offset'),
307
+ }
308
+ }
309
+
310
+ async haplotypeSamplesInRange(minHandle: number, maxHandle: number) {
311
+ const samples: HaplotypeSample[] = []
312
+ for await (const key of this.index.indexScanFrom('HaplotypeSamples', [
313
+ minHandle,
314
+ 0,
315
+ ])) {
316
+ const node = num(key[0], 'HaplotypeSamples.node_handle')
317
+ if (node > maxHandle) {
318
+ break
319
+ }
320
+ const row = await this.index.byRowid(
321
+ 'HaplotypeSamples',
322
+ num(key[2], 'HaplotypeSamples rowid'),
323
+ )
324
+ if (row) {
325
+ samples.push(this.sampleFromRow(row))
326
+ }
327
+ }
328
+ return samples
329
+ }
330
+
331
+ async haplotypeSampleAt(node: number, offset: number) {
332
+ const key = await this.index.indexSeekLE('HaplotypeSamples', [node, offset])
333
+ if (key?.[0] !== node || key[1] !== offset) {
334
+ return undefined
335
+ }
336
+ const row = await this.index.byRowid(
337
+ 'HaplotypeSamples',
338
+ num(key[2], 'HaplotypeSamples rowid'),
339
+ )
340
+ return row ? this.sampleFromRow(row) : undefined
341
+ }
342
+
343
+ async haplotypeLength(pathHandle: number) {
344
+ const row = await this.index.byRowid('HaplotypeLengths', pathHandle)
345
+ return row ? num(row[1], 'HaplotypeLengths.length') : undefined
346
+ }
347
+
348
+ async indexedPosition(
349
+ pathHandle: number,
350
+ pathOffset: number,
351
+ ): Promise<{ pathOffset: number; pos: Pos } | undefined> {
352
+ const key = await this.sqlite.indexSeekLE('ReferenceIndex', [
353
+ pathHandle,
354
+ pathOffset,
355
+ ])
356
+ if (key?.[0] !== pathHandle) {
357
+ return undefined
358
+ }
359
+ const row = await this.sqlite.byRowid(
360
+ 'ReferenceIndex',
361
+ num(key[2], 'ReferenceIndex rowid'),
362
+ )
363
+ if (!row) {
364
+ throw new Error('ReferenceIndex row referenced by its index is missing')
365
+ }
366
+ return {
367
+ pathOffset: num(row[1], 'ReferenceIndex.path_offset'),
368
+ pos: {
369
+ node: num(row[2], 'ReferenceIndex.node_handle'),
370
+ offset: num(row[3], 'ReferenceIndex.node_offset'),
371
+ },
372
+ }
373
+ }
374
+ }
@@ -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
+ }
@@ -0,0 +1,64 @@
1
+ export const ENDMARKER = 0
2
+
3
+ export type Orientation = 'forward' | 'reverse'
4
+
5
+ export function encodeNode(id: number, orientation: Orientation) {
6
+ return 2 * id + (orientation === 'reverse' ? 1 : 0)
7
+ }
8
+
9
+ export function nodeId(handle: number) {
10
+ return Math.floor(handle / 2)
11
+ }
12
+
13
+ export function nodeOrientation(handle: number): Orientation {
14
+ return handle % 2 === 0 ? 'forward' : 'reverse'
15
+ }
16
+
17
+ export function isReverse(handle: number) {
18
+ return handle % 2 === 1
19
+ }
20
+
21
+ export function flipNode(handle: number) {
22
+ return handle % 2 === 0 ? handle + 1 : handle - 1
23
+ }
24
+
25
+ export type NodeSide = 'left' | 'right'
26
+
27
+ export function flipSide(side: NodeSide): NodeSide {
28
+ return side === 'left' ? 'right' : 'left'
29
+ }
30
+
31
+ export function entrySide(orientation: Orientation): NodeSide {
32
+ return orientation === 'forward' ? 'left' : 'right'
33
+ }
34
+
35
+ export function exitSide(orientation: Orientation): NodeSide {
36
+ return orientation === 'forward' ? 'right' : 'left'
37
+ }
38
+
39
+ export function entryOrientation(side: NodeSide): Orientation {
40
+ return side === 'left' ? 'forward' : 'reverse'
41
+ }
42
+
43
+ export function exitOrientation(side: NodeSide): Orientation {
44
+ return side === 'right' ? 'forward' : 'reverse'
45
+ }
46
+
47
+ export function edgeIsCanonical(from: number, to: number) {
48
+ const fromId = nodeId(from)
49
+ const toId = nodeId(to)
50
+ return isReverse(from)
51
+ ? toId > fromId || (toId === fromId && !isReverse(to))
52
+ : toId >= fromId
53
+ }
54
+
55
+ export function pathIsCanonical(path: number[]) {
56
+ const first = path[0]
57
+ const last = path[path.length - 1]
58
+ if (first === undefined || last === undefined) {
59
+ return true
60
+ }
61
+ return isReverse(first) === isReverse(last)
62
+ ? !isReverse(first)
63
+ : edgeIsCanonical(first, last)
64
+ }
@@ -0,0 +1,177 @@
1
+ import { ByteCodeReader, RunReader } from './bytecode.ts'
2
+ import { ENDMARKER, flipNode, nodeId } from './node.ts'
3
+
4
+ export interface Pos {
5
+ node: number
6
+ offset: number
7
+ }
8
+
9
+ export function decompressEdges(bytes: Uint8Array): Pos[] {
10
+ const reader = new ByteCodeReader(bytes)
11
+ const sigma = reader.int()
12
+ if (sigma === undefined || sigma === 0) {
13
+ return []
14
+ }
15
+ const edges: Pos[] = []
16
+ let prev = 0
17
+ for (let i = 0; i < sigma; i++) {
18
+ const delta = reader.int()
19
+ const offset = reader.int()
20
+ if (delta === undefined || offset === undefined) {
21
+ throw new Error('GBWT edge list ends early')
22
+ }
23
+ prev += delta
24
+ edges.push({ node: prev, offset })
25
+ }
26
+ return edges
27
+ }
28
+
29
+ export class GbwtRecord {
30
+ readonly edges: Pos[]
31
+ readonly bwt: Uint8Array
32
+
33
+ constructor(edges: Pos[], bwt: Uint8Array) {
34
+ this.edges = edges
35
+ this.bwt = bwt
36
+ }
37
+
38
+ runs() {
39
+ return new RunReader(this.bwt, this.edges.length)
40
+ }
41
+
42
+ private edge(rank: number) {
43
+ const edge = this.edges[rank]
44
+ if (edge === undefined) {
45
+ throw new Error(
46
+ `GBWT run refers to edge rank ${rank} of ${this.edges.length}`,
47
+ )
48
+ }
49
+ return edge
50
+ }
51
+
52
+ lf(i: number): Pos | undefined {
53
+ const offsets = this.edges.map(e => e.offset)
54
+ let offset = 0
55
+ for (const run of this.runs()) {
56
+ const edge = this.edge(run.value)
57
+ const soFar = offsets[run.value]!
58
+ if (offset + run.len > i) {
59
+ return edge.node === ENDMARKER
60
+ ? undefined
61
+ : { node: edge.node, offset: soFar + (i - offset) }
62
+ }
63
+ offsets[run.value] = soFar + run.len
64
+ offset += run.len
65
+ }
66
+ return undefined
67
+ }
68
+
69
+ private edgeTo(node: number) {
70
+ let low = 0
71
+ let high = this.edges.length
72
+ while (low < high) {
73
+ const mid = (low + high) >> 1
74
+ const edge = this.edges[mid]!
75
+ if (edge.node === node) {
76
+ return mid
77
+ }
78
+ if (edge.node < node) {
79
+ low = mid + 1
80
+ } else {
81
+ high = mid
82
+ }
83
+ }
84
+ return undefined
85
+ }
86
+
87
+ offsetTo(pos: Pos): number | undefined {
88
+ if (pos.node === ENDMARKER) {
89
+ return undefined
90
+ }
91
+ const rank = this.edgeTo(pos.node)
92
+ if (rank === undefined) {
93
+ return undefined
94
+ }
95
+ let succRank = this.edges[rank]!.offset
96
+ if (succRank > pos.offset) {
97
+ return undefined
98
+ }
99
+ let offset = 0
100
+ for (const run of this.runs()) {
101
+ offset += run.len
102
+ if (run.value !== rank) {
103
+ continue
104
+ }
105
+ succRank += run.len
106
+ if (succRank > pos.offset) {
107
+ return offset - (succRank - pos.offset)
108
+ }
109
+ }
110
+ return undefined
111
+ }
112
+
113
+ predecessorAt(i: number): number | undefined {
114
+ const counts = this.edges.map(e => ({
115
+ node: e.node === ENDMARKER ? ENDMARKER : flipNode(e.node),
116
+ count: 0,
117
+ }))
118
+ for (const run of this.runs()) {
119
+ const entry = counts[run.value]
120
+ if (entry) {
121
+ entry.count += run.len
122
+ }
123
+ }
124
+ for (let rank = 1; rank < counts.length; rank++) {
125
+ const prev = counts[rank - 1] as { node: number; count: number }
126
+ const curr = counts[rank] as { node: number; count: number }
127
+ if (nodeId(prev.node) === nodeId(curr.node)) {
128
+ counts[rank - 1] = curr
129
+ counts[rank] = prev
130
+ }
131
+ }
132
+ let offset = 0
133
+ for (const entry of counts) {
134
+ offset += entry.count
135
+ if (offset > i) {
136
+ return entry.node === ENDMARKER ? undefined : entry.node
137
+ }
138
+ }
139
+ return undefined
140
+ }
141
+
142
+ decompressArrays() {
143
+ const offsets = this.edges.map(e => e.offset)
144
+ let total = 0
145
+ for (const run of this.runs()) {
146
+ total += run.len
147
+ }
148
+ const nodes = new Int32Array(total)
149
+ const nextOffsets = new Int32Array(total)
150
+ let i = 0
151
+ for (const run of this.runs()) {
152
+ const edge = this.edge(run.value)
153
+ let offset = offsets[run.value]!
154
+ for (let k = 0; k < run.len; k++) {
155
+ nodes[i] = edge.node
156
+ nextOffsets[i] = offset
157
+ offset += 1
158
+ i += 1
159
+ }
160
+ offsets[run.value] = offset
161
+ }
162
+ return { nodes, offsets: nextOffsets }
163
+ }
164
+
165
+ decompress(): Pos[] {
166
+ const offsets = this.edges.map(e => e.offset)
167
+ const result: Pos[] = []
168
+ for (const run of this.runs()) {
169
+ const edge = this.edge(run.value)
170
+ for (let k = 0; k < run.len; k++) {
171
+ result.push({ node: edge.node, offset: offsets[run.value]! })
172
+ offsets[run.value] = offsets[run.value]! + 1
173
+ }
174
+ }
175
+ return result
176
+ }
177
+ }