@gmod/bam 7.6.1 → 7.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +109 -98
- package/dist/bai.d.ts +6 -3
- package/dist/bai.js +32 -16
- package/dist/bai.js.map +1 -1
- package/dist/bamFile.d.ts +16 -3
- package/dist/bamFile.js +83 -36
- package/dist/bamFile.js.map +1 -1
- package/dist/chunk.d.ts +0 -1
- package/dist/chunk.js +0 -5
- package/dist/chunk.js.map +1 -1
- package/dist/csi.d.ts +0 -2
- package/dist/csi.js +4 -11
- package/dist/csi.js.map +1 -1
- package/dist/htsget.d.ts +16 -0
- package/dist/htsget.js +72 -52
- package/dist/htsget.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/indexFile.d.ts +2 -3
- package/dist/indexFile.js.map +1 -1
- package/dist/sam.js +8 -3
- package/dist/sam.js.map +1 -1
- package/dist/util.d.ts +11 -3
- package/dist/util.js +35 -8
- package/dist/util.js.map +1 -1
- package/dist/virtualOffset.d.ts +8 -3
- package/dist/virtualOffset.js +0 -3
- package/dist/virtualOffset.js.map +1 -1
- package/esm/bai.d.ts +6 -3
- package/esm/bai.js +32 -16
- package/esm/bai.js.map +1 -1
- package/esm/bamFile.d.ts +16 -3
- package/esm/bamFile.js +83 -36
- package/esm/bamFile.js.map +1 -1
- package/esm/chunk.d.ts +0 -1
- package/esm/chunk.js +0 -5
- package/esm/chunk.js.map +1 -1
- package/esm/csi.d.ts +0 -2
- package/esm/csi.js +4 -11
- package/esm/csi.js.map +1 -1
- package/esm/htsget.d.ts +16 -0
- package/esm/htsget.js +73 -53
- package/esm/htsget.js.map +1 -1
- package/esm/index.d.ts +1 -0
- package/esm/indexFile.d.ts +2 -3
- package/esm/indexFile.js.map +1 -1
- package/esm/sam.js +8 -3
- package/esm/sam.js.map +1 -1
- package/esm/util.d.ts +11 -3
- package/esm/util.js +35 -8
- package/esm/util.js.map +1 -1
- package/esm/virtualOffset.d.ts +8 -3
- package/esm/virtualOffset.js +0 -3
- package/esm/virtualOffset.js.map +1 -1
- package/package.json +3 -3
- package/src/bai.ts +42 -20
- package/src/bamFile.ts +86 -38
- package/src/chunk.ts +0 -8
- package/src/csi.ts +4 -10
- package/src/htsget.ts +117 -62
- package/src/index.ts +2 -0
- package/src/indexFile.ts +2 -3
- package/src/sam.ts +8 -3
- package/src/util.ts +44 -11
- package/src/virtualOffset.ts +9 -8
- package/dist/long.d.ts +0 -1
- package/dist/long.js +0 -17
- package/dist/long.js.map +0 -1
- package/esm/long.d.ts +0 -1
- package/esm/long.js +0 -14
- package/esm/long.js.map +0 -1
- package/src/long.ts +0 -16
package/src/htsget.ts
CHANGED
|
@@ -2,42 +2,111 @@ import { unzip } from '@gmod/bgzf-filehandle'
|
|
|
2
2
|
|
|
3
3
|
import BamFile, { BAM_MAGIC } from './bamFile.ts'
|
|
4
4
|
import Chunk from './chunk.ts'
|
|
5
|
-
import {
|
|
6
|
-
import { appendInRange, concatUint8Array } from './util.ts'
|
|
5
|
+
import { appendInRange, concatUint8Array, parseRefSeqs } from './util.ts'
|
|
7
6
|
import { VirtualOffset } from './virtualOffset.ts'
|
|
8
7
|
|
|
9
8
|
import type { BamRecordClass, BamRecordLike } from './bamFile.ts'
|
|
10
9
|
import type BamRecord from './record.ts'
|
|
11
10
|
import type { BamOpts, BaseOpts } from './util.ts'
|
|
11
|
+
import type { Fetcher } from 'generic-filehandle2'
|
|
12
12
|
|
|
13
13
|
interface HtsgetChunk {
|
|
14
14
|
url: string
|
|
15
15
|
headers?: Record<string, string>
|
|
16
|
+
// present on either all of a ticket's urls or none; only a hint, since the
|
|
17
|
+
// blocks still have to be concatenated in ticket order either way
|
|
18
|
+
class?: 'header' | 'body'
|
|
16
19
|
}
|
|
17
20
|
|
|
18
|
-
|
|
19
|
-
|
|
21
|
+
interface HtsgetTicket {
|
|
22
|
+
htsget: { urls: HtsgetChunk[] }
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
interface HtsgetErrorBody {
|
|
26
|
+
htsget?: { error?: string; message?: string }
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Errors carry a JSON body naming the error type, e.g. {"htsget":
|
|
30
|
+
// {"error":"InvalidAuthentication","message":"..."}}. Surface that rather than
|
|
31
|
+
// the raw payload, since 401/403 are the first thing a misconfigured token hits.
|
|
32
|
+
function htsgetErrorMessage(text: string) {
|
|
33
|
+
let parsed: HtsgetErrorBody | undefined
|
|
34
|
+
try {
|
|
35
|
+
parsed = JSON.parse(text)
|
|
36
|
+
} catch {
|
|
37
|
+
// not JSON, fall through to the raw body
|
|
38
|
+
}
|
|
39
|
+
const err = parsed?.htsget
|
|
40
|
+
return err?.error === undefined
|
|
41
|
+
? text
|
|
42
|
+
: err.message === undefined
|
|
43
|
+
? err.error
|
|
44
|
+
: `${err.error}: ${err.message}`
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Offset of the first alignment record in the concatenated ticket blocks.
|
|
49
|
+
*
|
|
50
|
+
* The spec has the client concatenate the blocks in ticket order to get a
|
|
51
|
+
* complete data stream, so whether the header is its own block is up to the
|
|
52
|
+
* server: htsnexus splits it into a leading `data:` url, while htsget-rs
|
|
53
|
+
* returns header and records together in a single url. Either way the
|
|
54
|
+
* concatenation starts with the BAM header, so seek past it rather than
|
|
55
|
+
* dropping blocks — a url count or class attribute can't tell the two layouts
|
|
56
|
+
* apart. Returns 0 when the stream has no header, which is what a server
|
|
57
|
+
* sending body-only blocks produces.
|
|
58
|
+
*/
|
|
59
|
+
function recordsOffset(uncba: Uint8Array) {
|
|
60
|
+
const dataView = new DataView(
|
|
61
|
+
uncba.buffer,
|
|
62
|
+
uncba.byteOffset,
|
|
63
|
+
uncba.byteLength,
|
|
64
|
+
)
|
|
65
|
+
if (uncba.byteLength < 8 || dataView.getInt32(0, true) !== BAM_MAGIC) {
|
|
66
|
+
return 0
|
|
67
|
+
}
|
|
68
|
+
const refs = parseRefSeqs(uncba, 8 + dataView.getInt32(4, true), n => n)
|
|
69
|
+
if (!refs) {
|
|
70
|
+
throw new Error('truncated BAM header in htsget response')
|
|
71
|
+
}
|
|
72
|
+
return refs.end
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
async function fetchOk(fetchFn: Fetcher, url: string, opts?: RequestInit) {
|
|
76
|
+
const res = await fetchFn(url, opts)
|
|
20
77
|
if (!res.ok) {
|
|
21
|
-
throw new Error(
|
|
78
|
+
throw new Error(
|
|
79
|
+
`HTTP ${res.status} fetching ${url}: ${htsgetErrorMessage(await res.text())}`,
|
|
80
|
+
)
|
|
22
81
|
}
|
|
23
82
|
return res
|
|
24
83
|
}
|
|
25
84
|
|
|
26
|
-
async function fetchChunk(
|
|
85
|
+
async function fetchChunk(
|
|
86
|
+
fetchFn: Fetcher,
|
|
87
|
+
{ url, headers }: HtsgetChunk,
|
|
88
|
+
opts?: RequestInit,
|
|
89
|
+
) {
|
|
27
90
|
// pass base64 data URLs straight to fetch; otherwise apply headers (minus
|
|
28
91
|
// referer, which isn't a permitted client-set header).
|
|
29
92
|
// https://stackoverflow.com/a/54123275/2129219
|
|
30
93
|
const { referer: _referer, ...rest } = headers ?? {}
|
|
31
94
|
const res = url.startsWith('data:')
|
|
32
|
-
? await fetchOk(url)
|
|
33
|
-
: await fetchOk(url, { ...opts, headers: rest })
|
|
95
|
+
? await fetchOk(fetchFn, url)
|
|
96
|
+
: await fetchOk(fetchFn, url, { ...opts, headers: rest })
|
|
34
97
|
return new Uint8Array(await res.arrayBuffer())
|
|
35
98
|
}
|
|
36
99
|
|
|
37
|
-
async function fetchAndConcat(
|
|
100
|
+
async function fetchAndConcat(
|
|
101
|
+
fetchFn: Fetcher,
|
|
102
|
+
arr: HtsgetChunk[],
|
|
103
|
+
opts?: RequestInit,
|
|
104
|
+
) {
|
|
38
105
|
// Pipeline unzip after each fetch so decompression overlaps later fetches.
|
|
39
106
|
return concatUint8Array(
|
|
40
|
-
await Promise.all(
|
|
107
|
+
await Promise.all(
|
|
108
|
+
arr.map(async c => unzip(await fetchChunk(fetchFn, c, opts))),
|
|
109
|
+
),
|
|
41
110
|
)
|
|
42
111
|
}
|
|
43
112
|
|
|
@@ -48,14 +117,39 @@ export default class HtsgetFile<
|
|
|
48
117
|
|
|
49
118
|
private trackId: string
|
|
50
119
|
|
|
120
|
+
private fetchFn: Fetcher
|
|
121
|
+
|
|
51
122
|
constructor(args: {
|
|
52
123
|
trackId: string
|
|
53
124
|
baseUrl: string
|
|
54
125
|
recordClass?: BamRecordClass<T>
|
|
126
|
+
/**
|
|
127
|
+
* fetch implementation used for every request, so an `Authorization: Bearer
|
|
128
|
+
* <token>` header can be added for servers that require one. It is also
|
|
129
|
+
* called with the data-block urls from the ticket, which may point at
|
|
130
|
+
* third-party hosts, so only attach credentials to hosts you trust — the
|
|
131
|
+
* spec has servers put whatever a data block needs in that url's own
|
|
132
|
+
* `headers` field, which is applied either way.
|
|
133
|
+
*/
|
|
134
|
+
fetch?: Fetcher
|
|
55
135
|
}) {
|
|
56
136
|
super({ htsget: true, recordClass: args.recordClass })
|
|
57
137
|
this.baseUrl = args.baseUrl
|
|
58
138
|
this.trackId = args.trackId
|
|
139
|
+
this.fetchFn = args.fetch ?? ((input, init) => fetch(input, init))
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Requests a ticket and returns its data blocks decompressed and
|
|
144
|
+
* concatenated, which per the spec is a complete BAM stream.
|
|
145
|
+
*/
|
|
146
|
+
private async fetchTicket(query: string, opts?: BaseOpts) {
|
|
147
|
+
const url = `${this.baseUrl}/${this.trackId}?${query}`
|
|
148
|
+
const res = await fetchOk(this.fetchFn, url, { signal: opts?.signal })
|
|
149
|
+
const ticket: HtsgetTicket = await res.json()
|
|
150
|
+
return fetchAndConcat(this.fetchFn, ticket.htsget.urls, {
|
|
151
|
+
signal: opts?.signal,
|
|
152
|
+
})
|
|
59
153
|
}
|
|
60
154
|
|
|
61
155
|
async getRecordsForRange(
|
|
@@ -65,71 +159,32 @@ export default class HtsgetFile<
|
|
|
65
159
|
opts?: BamOpts,
|
|
66
160
|
) {
|
|
67
161
|
await this.getHeader(opts)
|
|
68
|
-
const base = `${this.baseUrl}/${this.trackId}`
|
|
69
|
-
const url = `${base}?referenceName=${chr}&start=${min}&end=${max}&format=BAM`
|
|
70
162
|
const chrId = this.chrToIndex?.[chr]
|
|
71
163
|
if (chrId === undefined) {
|
|
72
164
|
return []
|
|
73
165
|
}
|
|
74
|
-
const
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
})
|
|
79
|
-
|
|
166
|
+
const uncba = await this.fetchTicket(
|
|
167
|
+
`referenceName=${chr}&start=${min}&end=${max}&format=BAM`,
|
|
168
|
+
opts,
|
|
169
|
+
)
|
|
80
170
|
const zero = new VirtualOffset(0, 0)
|
|
81
|
-
const
|
|
82
|
-
uncba,
|
|
171
|
+
const records = this.readBamFeatures(
|
|
172
|
+
uncba.subarray(recordsOffset(uncba)),
|
|
83
173
|
[],
|
|
84
174
|
[],
|
|
85
175
|
new Chunk(zero, zero, 0),
|
|
86
176
|
)
|
|
87
|
-
|
|
88
|
-
return appendInRange(allRecords, chrId, min, max)
|
|
177
|
+
return appendInRange(records, chrId, min, max)
|
|
89
178
|
}
|
|
90
179
|
|
|
91
180
|
async getHeaderPre(opts: BaseOpts = {}) {
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
const
|
|
95
|
-
const
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
const dataView = new DataView(
|
|
99
|
-
uncba.buffer,
|
|
100
|
-
uncba.byteOffset,
|
|
101
|
-
uncba.byteLength,
|
|
102
|
-
)
|
|
103
|
-
|
|
104
|
-
if (dataView.getInt32(0, true) !== BAM_MAGIC) {
|
|
105
|
-
throw new Error('Not a BAM file')
|
|
106
|
-
}
|
|
107
|
-
const headLen = dataView.getInt32(4, true)
|
|
108
|
-
|
|
109
|
-
const decoder = new TextDecoder()
|
|
110
|
-
const headerText = decoder.decode(uncba.subarray(8, 8 + headLen))
|
|
111
|
-
const samHeader = parseHeaderText(headerText)
|
|
112
|
-
|
|
113
|
-
// use the @SQ lines in the header to figure out the
|
|
114
|
-
// mapping between ref ref ID numbers and names
|
|
115
|
-
const idToName: { refName: string; length: number }[] = []
|
|
116
|
-
const nameToId: Record<string, number> = {}
|
|
117
|
-
const sqLines = samHeader.filter(l => l.tag === 'SQ')
|
|
118
|
-
for (const [refId, sqLine] of sqLines.entries()) {
|
|
119
|
-
let refName = ''
|
|
120
|
-
let length = 0
|
|
121
|
-
for (const item of sqLine.data) {
|
|
122
|
-
if (item.tag === 'SN') {
|
|
123
|
-
refName = item.value
|
|
124
|
-
} else if (item.tag === 'LN') {
|
|
125
|
-
length = +item.value
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
nameToId[refName] = refId
|
|
129
|
-
idToName[refId] = { refName, length }
|
|
181
|
+
// format is the only parameter the spec permits alongside class=header;
|
|
182
|
+
// servers SHOULD reject anything else with InvalidInput
|
|
183
|
+
const uncba = await this.fetchTicket('class=header&format=BAM', opts)
|
|
184
|
+
const samHeader = this.applyHeader(uncba)
|
|
185
|
+
if (!samHeader) {
|
|
186
|
+
throw new Error('Insufficient data for reference sequences')
|
|
130
187
|
}
|
|
131
|
-
this.chrToIndex = nameToId
|
|
132
|
-
this.indexToChr = idToName
|
|
133
188
|
return samHeader
|
|
134
189
|
}
|
|
135
190
|
}
|
package/src/index.ts
CHANGED
package/src/indexFile.ts
CHANGED
|
@@ -4,7 +4,7 @@ import { optimizeChunks } from './util.ts'
|
|
|
4
4
|
|
|
5
5
|
import type Chunk from './chunk.ts'
|
|
6
6
|
import type { BaseOpts } from './util.ts'
|
|
7
|
-
import type {
|
|
7
|
+
import type { OffsetCoords, VirtualOffset } from './virtualOffset.ts'
|
|
8
8
|
import type { GenericFilehandle } from 'generic-filehandle2'
|
|
9
9
|
|
|
10
10
|
export interface Region {
|
|
@@ -21,7 +21,6 @@ export interface RefIndex {
|
|
|
21
21
|
export interface ParsedIndexBase<R extends RefIndex = RefIndex> {
|
|
22
22
|
firstDataLine: VirtualOffset | undefined
|
|
23
23
|
refCount: number
|
|
24
|
-
maxBlockSize: number
|
|
25
24
|
indices: (refId: number) => R | undefined
|
|
26
25
|
}
|
|
27
26
|
|
|
@@ -84,7 +83,7 @@ export default abstract class IndexFile<
|
|
|
84
83
|
protected abstract getLowestChunk(
|
|
85
84
|
refIndex: RefIndex,
|
|
86
85
|
min: number,
|
|
87
|
-
):
|
|
86
|
+
): OffsetCoords | undefined
|
|
88
87
|
|
|
89
88
|
async blocksForRange(
|
|
90
89
|
refId: number,
|
package/src/sam.ts
CHANGED
|
@@ -8,9 +8,14 @@ export function parseHeaderText(text: string) {
|
|
|
8
8
|
tag: tag.slice(1),
|
|
9
9
|
data: fields.map(f => {
|
|
10
10
|
const r = f.indexOf(':')
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
11
|
+
// A field with no colon is not a TAG:VALUE pair — @CO lines are free
|
|
12
|
+
// text, one comment per line (`samtools view -H c2#pad.3.0.cram`).
|
|
13
|
+
// Without this branch `slice(0, -1)` silently drops the comment's last
|
|
14
|
+
// character into the tag. Same shape @gmod/cram's parseHeaderText
|
|
15
|
+
// returns, so both feed a consumer's header parser identically.
|
|
16
|
+
return r === -1
|
|
17
|
+
? { tag: f, value: '' }
|
|
18
|
+
: { tag: f.slice(0, r), value: f.slice(r + 1) }
|
|
14
19
|
}),
|
|
15
20
|
})
|
|
16
21
|
}
|
package/src/util.ts
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
import Chunk from './chunk.ts'
|
|
2
|
-
import { longFromBytesToUnsigned } from './long.ts'
|
|
3
2
|
import { VirtualOffset } from './virtualOffset.ts'
|
|
4
3
|
|
|
5
|
-
import type {
|
|
4
|
+
import type { OffsetCoords } from './virtualOffset.ts'
|
|
6
5
|
|
|
7
6
|
export interface BamOpts {
|
|
8
7
|
viewAsPairs?: boolean
|
|
@@ -30,7 +29,7 @@ export interface BaseOpts {
|
|
|
30
29
|
onProgress?: (bytesDownloaded: number, totalBytes?: number) => void
|
|
31
30
|
}
|
|
32
31
|
|
|
33
|
-
export function optimizeChunks(chunks: Chunk[], lowest?:
|
|
32
|
+
export function optimizeChunks(chunks: Chunk[], lowest?: OffsetCoords) {
|
|
34
33
|
const n = chunks.length
|
|
35
34
|
if (n === 0) {
|
|
36
35
|
return chunks
|
|
@@ -102,9 +101,22 @@ export function optimizeChunks(chunks: Chunk[], lowest?: Offset) {
|
|
|
102
101
|
return mergedChunks
|
|
103
102
|
}
|
|
104
103
|
|
|
104
|
+
// The pseudo-bin's mapped-record count, a little-endian uint64. Read as two
|
|
105
|
+
// 32-bit halves rather than via BigInt: exact up to Number.MAX_SAFE_INTEGER,
|
|
106
|
+
// which is far past any real record count, and allocates nothing.
|
|
105
107
|
export function parsePseudoBin(bytes: Uint8Array, offset: number) {
|
|
108
|
+
const low =
|
|
109
|
+
bytes[offset]! |
|
|
110
|
+
(bytes[offset + 1]! << 8) |
|
|
111
|
+
(bytes[offset + 2]! << 16) |
|
|
112
|
+
(bytes[offset + 3]! << 24)
|
|
113
|
+
const high =
|
|
114
|
+
bytes[offset + 4]! |
|
|
115
|
+
(bytes[offset + 5]! << 8) |
|
|
116
|
+
(bytes[offset + 6]! << 16) |
|
|
117
|
+
(bytes[offset + 7]! << 24)
|
|
106
118
|
return {
|
|
107
|
-
lineCount:
|
|
119
|
+
lineCount: (high >>> 0) * 2 ** 32 + (low >>> 0),
|
|
108
120
|
}
|
|
109
121
|
}
|
|
110
122
|
|
|
@@ -117,13 +129,21 @@ export function parsePseudoBin(bytes: Uint8Array, offset: number) {
|
|
|
117
129
|
// Shrinks both the byte estimate and the actual fetch with no extra I/O.
|
|
118
130
|
export function clampChunkEnds(
|
|
119
131
|
chunks: Chunk[],
|
|
120
|
-
extraBoundaries: number
|
|
132
|
+
extraBoundaries: ArrayLike<number> = [],
|
|
121
133
|
) {
|
|
122
|
-
|
|
134
|
+
// Float64Array rather than number[]: `extraBoundaries` is the linear index,
|
|
135
|
+
// which runs to tens of thousands of entries on a human-sized reference, and
|
|
136
|
+
// this is the only place they get copied.
|
|
137
|
+
const boundaries = new Float64Array(
|
|
138
|
+
extraBoundaries.length + chunks.length * 2,
|
|
139
|
+
)
|
|
140
|
+
boundaries.set(extraBoundaries)
|
|
141
|
+
let n = extraBoundaries.length
|
|
123
142
|
for (const c of chunks) {
|
|
124
|
-
boundaries
|
|
143
|
+
boundaries[n++] = c.minv.blockPosition
|
|
144
|
+
boundaries[n++] = c.maxv.blockPosition
|
|
125
145
|
}
|
|
126
|
-
boundaries.sort(
|
|
146
|
+
boundaries.sort()
|
|
127
147
|
|
|
128
148
|
for (const c of chunks) {
|
|
129
149
|
const max = c.maxv.blockPosition
|
|
@@ -184,14 +204,24 @@ export function parseRefSeqs(
|
|
|
184
204
|
indexToChr.push({ refName, length: lRef })
|
|
185
205
|
p += 8 + lName
|
|
186
206
|
}
|
|
187
|
-
|
|
207
|
+
// end is the offset just past the header, i.e. where alignment records start
|
|
208
|
+
return { chrToIndex, indexToChr, end: p }
|
|
188
209
|
}
|
|
189
210
|
|
|
190
|
-
// SYNC: ~/src/gmod/tabix-js/src/util.ts minVirtualOffset
|
|
211
|
+
// SYNC: ~/src/gmod/tabix-js/src/util.ts minVirtualOffset — but NOT the 0:0
|
|
212
|
+
// skip below, which is only sound for BAM. A tabix'd file with no header lines
|
|
213
|
+
// really does have its first record at 0:0.
|
|
191
214
|
/**
|
|
192
215
|
* The smallest of `current` and the `count` packed virtual offsets starting at
|
|
193
216
|
* `offset`, allocating at most one VirtualOffset rather than one per entry.
|
|
194
217
|
*
|
|
218
|
+
* 0:0 is skipped rather than treated as the minimum. No BAM record can live
|
|
219
|
+
* there — the magic and header occupy the start of the file — so it is the
|
|
220
|
+
* "unset" placeholder htslib leaves in linear-index windows ahead of a
|
|
221
|
+
* reference's first read (see test/data/HG00096_illumina_lowcov.bam.bai, whose
|
|
222
|
+
* first three windows are 0). Counting it collapses firstDataLine to 0:0 and
|
|
223
|
+
* makes callers size a header read from nothing.
|
|
224
|
+
*
|
|
195
225
|
* The index first pass exists only to find this minimum, and it visits every
|
|
196
226
|
* linear-index entry in the file to do it. Building a VirtualOffset per entry
|
|
197
227
|
* to compare and discard it is the bulk of that pass.
|
|
@@ -215,7 +245,10 @@ export function minVirtualOffset(
|
|
|
215
245
|
bytes[p + 3]! * 0x100 +
|
|
216
246
|
bytes[p + 2]!
|
|
217
247
|
const data = (bytes[p + 1]! << 8) | bytes[p]!
|
|
218
|
-
if (
|
|
248
|
+
if (
|
|
249
|
+
(block !== 0 || data !== 0) &&
|
|
250
|
+
(block < minBlock || (block === minBlock && data < minData))
|
|
251
|
+
) {
|
|
219
252
|
minBlock = block
|
|
220
253
|
minData = data
|
|
221
254
|
found = true
|
package/src/virtualOffset.ts
CHANGED
|
@@ -1,8 +1,15 @@
|
|
|
1
|
-
|
|
1
|
+
/**
|
|
2
|
+
* The two coordinates a virtual offset packs, without the `blockPos:dataPos`
|
|
3
|
+
* string form. What every consumer that only compares positions needs — the
|
|
4
|
+
* BAI linear index stores these as raw numbers and materializes no object.
|
|
5
|
+
*/
|
|
6
|
+
export interface OffsetCoords {
|
|
2
7
|
blockPosition: number
|
|
3
8
|
dataPosition: number
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface Offset extends OffsetCoords {
|
|
4
12
|
toString(): string
|
|
5
|
-
compareTo(arg: Offset): number
|
|
6
13
|
}
|
|
7
14
|
|
|
8
15
|
export class VirtualOffset {
|
|
@@ -16,12 +23,6 @@ export class VirtualOffset {
|
|
|
16
23
|
toString() {
|
|
17
24
|
return `${this.blockPosition}:${this.dataPosition}`
|
|
18
25
|
}
|
|
19
|
-
|
|
20
|
-
compareTo(b: VirtualOffset) {
|
|
21
|
-
return (
|
|
22
|
-
this.blockPosition - b.blockPosition || this.dataPosition - b.dataPosition
|
|
23
|
-
)
|
|
24
|
-
}
|
|
25
26
|
}
|
|
26
27
|
export function fromBytes(bytes: Uint8Array, offset = 0) {
|
|
27
28
|
return new VirtualOffset(
|
package/dist/long.d.ts
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export declare function longFromBytesToUnsigned(source: Uint8Array, i?: number): number;
|
package/dist/long.js
DELETED
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.longFromBytesToUnsigned = longFromBytesToUnsigned;
|
|
4
|
-
const TWO_PWR_32_DBL = 2 ** 32;
|
|
5
|
-
// avoids dependency on long.js
|
|
6
|
-
function longFromBytesToUnsigned(source, i = 0) {
|
|
7
|
-
const low = source[i] |
|
|
8
|
-
(source[i + 1] << 8) |
|
|
9
|
-
(source[i + 2] << 16) |
|
|
10
|
-
(source[i + 3] << 24);
|
|
11
|
-
const high = source[i + 4] |
|
|
12
|
-
(source[i + 5] << 8) |
|
|
13
|
-
(source[i + 6] << 16) |
|
|
14
|
-
(source[i + 7] << 24);
|
|
15
|
-
return (high >>> 0) * TWO_PWR_32_DBL + (low >>> 0);
|
|
16
|
-
}
|
|
17
|
-
//# sourceMappingURL=long.js.map
|
package/dist/long.js.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"long.js","sourceRoot":"","sources":["../src/long.ts"],"names":[],"mappings":";;AAGA,0DAYC;AAfD,MAAM,cAAc,GAAG,CAAC,IAAI,EAAE,CAAA;AAE9B,+BAA+B;AAC/B,SAAgB,uBAAuB,CAAC,MAAkB,EAAE,CAAC,GAAG,CAAC;IAC/D,MAAM,GAAG,GACP,MAAM,CAAC,CAAC,CAAE;QACV,CAAC,MAAM,CAAC,CAAC,GAAG,CAAC,CAAE,IAAI,CAAC,CAAC;QACrB,CAAC,MAAM,CAAC,CAAC,GAAG,CAAC,CAAE,IAAI,EAAE,CAAC;QACtB,CAAC,MAAM,CAAC,CAAC,GAAG,CAAC,CAAE,IAAI,EAAE,CAAC,CAAA;IACxB,MAAM,IAAI,GACR,MAAM,CAAC,CAAC,GAAG,CAAC,CAAE;QACd,CAAC,MAAM,CAAC,CAAC,GAAG,CAAC,CAAE,IAAI,CAAC,CAAC;QACrB,CAAC,MAAM,CAAC,CAAC,GAAG,CAAC,CAAE,IAAI,EAAE,CAAC;QACtB,CAAC,MAAM,CAAC,CAAC,GAAG,CAAC,CAAE,IAAI,EAAE,CAAC,CAAA;IACxB,OAAO,CAAC,IAAI,KAAK,CAAC,CAAC,GAAG,cAAc,GAAG,CAAC,GAAG,KAAK,CAAC,CAAC,CAAA;AACpD,CAAC"}
|
package/esm/long.d.ts
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export declare function longFromBytesToUnsigned(source: Uint8Array, i?: number): number;
|
package/esm/long.js
DELETED
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
const TWO_PWR_32_DBL = 2 ** 32;
|
|
2
|
-
// avoids dependency on long.js
|
|
3
|
-
export function longFromBytesToUnsigned(source, i = 0) {
|
|
4
|
-
const low = source[i] |
|
|
5
|
-
(source[i + 1] << 8) |
|
|
6
|
-
(source[i + 2] << 16) |
|
|
7
|
-
(source[i + 3] << 24);
|
|
8
|
-
const high = source[i + 4] |
|
|
9
|
-
(source[i + 5] << 8) |
|
|
10
|
-
(source[i + 6] << 16) |
|
|
11
|
-
(source[i + 7] << 24);
|
|
12
|
-
return (high >>> 0) * TWO_PWR_32_DBL + (low >>> 0);
|
|
13
|
-
}
|
|
14
|
-
//# sourceMappingURL=long.js.map
|
package/esm/long.js.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"long.js","sourceRoot":"","sources":["../src/long.ts"],"names":[],"mappings":"AAAA,MAAM,cAAc,GAAG,CAAC,IAAI,EAAE,CAAA;AAE9B,+BAA+B;AAC/B,MAAM,UAAU,uBAAuB,CAAC,MAAkB,EAAE,CAAC,GAAG,CAAC;IAC/D,MAAM,GAAG,GACP,MAAM,CAAC,CAAC,CAAE;QACV,CAAC,MAAM,CAAC,CAAC,GAAG,CAAC,CAAE,IAAI,CAAC,CAAC;QACrB,CAAC,MAAM,CAAC,CAAC,GAAG,CAAC,CAAE,IAAI,EAAE,CAAC;QACtB,CAAC,MAAM,CAAC,CAAC,GAAG,CAAC,CAAE,IAAI,EAAE,CAAC,CAAA;IACxB,MAAM,IAAI,GACR,MAAM,CAAC,CAAC,GAAG,CAAC,CAAE;QACd,CAAC,MAAM,CAAC,CAAC,GAAG,CAAC,CAAE,IAAI,CAAC,CAAC;QACrB,CAAC,MAAM,CAAC,CAAC,GAAG,CAAC,CAAE,IAAI,EAAE,CAAC;QACtB,CAAC,MAAM,CAAC,CAAC,GAAG,CAAC,CAAE,IAAI,EAAE,CAAC,CAAA;IACxB,OAAO,CAAC,IAAI,KAAK,CAAC,CAAC,GAAG,cAAc,GAAG,CAAC,GAAG,KAAK,CAAC,CAAC,CAAA;AACpD,CAAC"}
|
package/src/long.ts
DELETED
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
const TWO_PWR_32_DBL = 2 ** 32
|
|
2
|
-
|
|
3
|
-
// avoids dependency on long.js
|
|
4
|
-
export function longFromBytesToUnsigned(source: Uint8Array, i = 0) {
|
|
5
|
-
const low =
|
|
6
|
-
source[i]! |
|
|
7
|
-
(source[i + 1]! << 8) |
|
|
8
|
-
(source[i + 2]! << 16) |
|
|
9
|
-
(source[i + 3]! << 24)
|
|
10
|
-
const high =
|
|
11
|
-
source[i + 4]! |
|
|
12
|
-
(source[i + 5]! << 8) |
|
|
13
|
-
(source[i + 6]! << 16) |
|
|
14
|
-
(source[i + 7]! << 24)
|
|
15
|
-
return (high >>> 0) * TWO_PWR_32_DBL + (low >>> 0)
|
|
16
|
-
}
|