@rangedb/js 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.
- package/dist/index.d.ts +168 -0
- package/index.js +332 -0
- package/package.json +30 -0
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
export type Compression = number;
|
|
2
|
+
/**
|
|
3
|
+
* @typedef {Object} RangeDBOptions
|
|
4
|
+
* @property {number} [firstReadSize] Specify how much should be read from the file on first call.
|
|
5
|
+
* If known, it can be set to size of header + index. It will save an additional request.
|
|
6
|
+
*/
|
|
7
|
+
/**
|
|
8
|
+
* @typedef {Object} Range
|
|
9
|
+
* @property {bigint} [start]
|
|
10
|
+
* @property {bigint} [end]
|
|
11
|
+
*/
|
|
12
|
+
/**
|
|
13
|
+
* @enum {number}
|
|
14
|
+
*/
|
|
15
|
+
export const Compression: Readonly<{
|
|
16
|
+
none: 0;
|
|
17
|
+
gzip: 1;
|
|
18
|
+
brotli: 2;
|
|
19
|
+
}>;
|
|
20
|
+
export type ContentType = number;
|
|
21
|
+
/**
|
|
22
|
+
* @enum {number}
|
|
23
|
+
*/
|
|
24
|
+
export const ContentType: Readonly<{
|
|
25
|
+
unknown: 0;
|
|
26
|
+
json: 1;
|
|
27
|
+
xml: 2;
|
|
28
|
+
}>;
|
|
29
|
+
/**
|
|
30
|
+
* @typedef {Object} Header
|
|
31
|
+
* @property {number} specVersion
|
|
32
|
+
* @property {bigint} metadataOffset
|
|
33
|
+
* @property {number} metadataLength
|
|
34
|
+
* @property {bigint} indexOffset
|
|
35
|
+
* @property {number} indexLength
|
|
36
|
+
* @property {bigint} dataOffset
|
|
37
|
+
* @property {bigint} dataLength
|
|
38
|
+
* @property {Compression} compression
|
|
39
|
+
* @property {ContentType} contentType
|
|
40
|
+
*/
|
|
41
|
+
/**
|
|
42
|
+
* @typedef {string | number | boolean | null | JSONObject | JSONArray} JSONValue
|
|
43
|
+
*/
|
|
44
|
+
/**
|
|
45
|
+
* @typedef {Array<JSONValue>} JSONArray
|
|
46
|
+
*/
|
|
47
|
+
/**
|
|
48
|
+
* @typedef {{ [key: string]: JSONValue }} JSONObject
|
|
49
|
+
*/
|
|
50
|
+
export class RangeDB {
|
|
51
|
+
/**
|
|
52
|
+
* Traverse chunk consisting of mulitple key/value pairs and returns value
|
|
53
|
+
* for given key or null if not founded
|
|
54
|
+
* @private
|
|
55
|
+
* @param {ArrayBuffer} chunk
|
|
56
|
+
* @param {bigint} key
|
|
57
|
+
*
|
|
58
|
+
* @returns {ArrayBuffer | null}
|
|
59
|
+
*/
|
|
60
|
+
private static findInChunk;
|
|
61
|
+
/**
|
|
62
|
+
* Binary search in index for a given key and return value
|
|
63
|
+
* @param {bigint} key
|
|
64
|
+
* @param {BigUint64Array} index
|
|
65
|
+
* @param {bigint} dataEndOffset one behind data ends
|
|
66
|
+
*
|
|
67
|
+
* @return {Range | null} Offset of data
|
|
68
|
+
*/
|
|
69
|
+
static binarySearch(key: bigint, index: BigUint64Array, dataEndOffset: bigint): Range | null;
|
|
70
|
+
/**
|
|
71
|
+
* Initialize database by providing url of rangedb file.
|
|
72
|
+
*
|
|
73
|
+
* @param {string} url
|
|
74
|
+
* @param {RangeDBOptions} options
|
|
75
|
+
*/
|
|
76
|
+
constructor(url: string, options?: RangeDBOptions);
|
|
77
|
+
/** @protected @type {string} */
|
|
78
|
+
protected url: string;
|
|
79
|
+
/** @protected @type {string| null} */
|
|
80
|
+
protected etag: string | null;
|
|
81
|
+
/** @private @type {Header | null} */
|
|
82
|
+
private header;
|
|
83
|
+
/** @private @type {BigUint64Array | null} */
|
|
84
|
+
private index;
|
|
85
|
+
/** @private @type {JSONObject| JSONArray | null} */
|
|
86
|
+
private metadata;
|
|
87
|
+
/** @private @type {number} */
|
|
88
|
+
private firstReadSize;
|
|
89
|
+
/**
|
|
90
|
+
* Invalidate header and index.
|
|
91
|
+
*
|
|
92
|
+
* @returns {void}
|
|
93
|
+
*/
|
|
94
|
+
invalidate(): void;
|
|
95
|
+
/**
|
|
96
|
+
* Perform HTTP range request.
|
|
97
|
+
*
|
|
98
|
+
* @protected
|
|
99
|
+
* @param {bigint} start
|
|
100
|
+
* @param {bigint} end
|
|
101
|
+
* @returns {Promise<ArrayBuffer>}
|
|
102
|
+
*/
|
|
103
|
+
protected readRange(start: bigint, end: bigint): Promise<ArrayBuffer>;
|
|
104
|
+
/**
|
|
105
|
+
* Get header from database or return cached.
|
|
106
|
+
*
|
|
107
|
+
* @returns {Promise<Header>}
|
|
108
|
+
*/
|
|
109
|
+
getHeader(): Promise<Header>;
|
|
110
|
+
/**
|
|
111
|
+
* Load index from database or return cached
|
|
112
|
+
*
|
|
113
|
+
* @private
|
|
114
|
+
* @returns {Promise<number>}
|
|
115
|
+
*/
|
|
116
|
+
private getIndex;
|
|
117
|
+
/**
|
|
118
|
+
* Get metadata from database or return cached
|
|
119
|
+
*
|
|
120
|
+
* @return {Promise<JSONObject | JSONArray>}
|
|
121
|
+
*/
|
|
122
|
+
getMetadata(): Promise<JSONObject | JSONArray>;
|
|
123
|
+
/**
|
|
124
|
+
* Get a raw ArrayBuffer from database for given key or null if not exists
|
|
125
|
+
*
|
|
126
|
+
* @param {bigint} key
|
|
127
|
+
*
|
|
128
|
+
* @returns {Promise<ArrayBuffer | null>}
|
|
129
|
+
* */
|
|
130
|
+
getRaw(key: bigint): Promise<ArrayBuffer | null>;
|
|
131
|
+
/**
|
|
132
|
+
* Get a JSON from database for a given key or null if not exists
|
|
133
|
+
* It may throw JSON parsing error
|
|
134
|
+
*
|
|
135
|
+
* @param {bigint} key
|
|
136
|
+
*
|
|
137
|
+
* @returns {Promise<JSONValue | null>}
|
|
138
|
+
* @throws {SyntaxError}
|
|
139
|
+
*/
|
|
140
|
+
getJson(key: bigint): Promise<JSONValue | null>;
|
|
141
|
+
}
|
|
142
|
+
export type RangeDBOptions = {
|
|
143
|
+
/**
|
|
144
|
+
* Specify how much should be read from the file on first call.
|
|
145
|
+
* If known, it can be set to size of header + index. It will save an additional request.
|
|
146
|
+
*/
|
|
147
|
+
firstReadSize?: number;
|
|
148
|
+
};
|
|
149
|
+
export type Range = {
|
|
150
|
+
start?: bigint;
|
|
151
|
+
end?: bigint;
|
|
152
|
+
};
|
|
153
|
+
export type Header = {
|
|
154
|
+
specVersion: number;
|
|
155
|
+
metadataOffset: bigint;
|
|
156
|
+
metadataLength: number;
|
|
157
|
+
indexOffset: bigint;
|
|
158
|
+
indexLength: number;
|
|
159
|
+
dataOffset: bigint;
|
|
160
|
+
dataLength: bigint;
|
|
161
|
+
compression: Compression;
|
|
162
|
+
contentType: ContentType;
|
|
163
|
+
};
|
|
164
|
+
export type JSONValue = string | number | boolean | null | JSONObject | JSONArray;
|
|
165
|
+
export type JSONArray = Array<JSONValue>;
|
|
166
|
+
export type JSONObject = {
|
|
167
|
+
[key: string]: JSONValue;
|
|
168
|
+
};
|
package/index.js
ADDED
|
@@ -0,0 +1,332 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @typedef {Object} RangeDBOptions
|
|
5
|
+
* @property {number} [firstReadSize] Specify how much should be read from the file on first call.
|
|
6
|
+
* If known, it can be set to size of header + index. It will save an additional request.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* @typedef {Object} Range
|
|
11
|
+
* @property {bigint} [start]
|
|
12
|
+
* @property {bigint} [end]
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* @enum {number}
|
|
17
|
+
*/
|
|
18
|
+
export const Compression = Object.freeze({
|
|
19
|
+
none: 0,
|
|
20
|
+
gzip: 1,
|
|
21
|
+
brotli: 2,
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* @enum {number}
|
|
26
|
+
*/
|
|
27
|
+
export const ContentType = Object.freeze({
|
|
28
|
+
unknown: 0,
|
|
29
|
+
json: 1,
|
|
30
|
+
xml: 2,
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* @typedef {Object} Header
|
|
35
|
+
* @property {number} specVersion
|
|
36
|
+
* @property {bigint} metadataOffset
|
|
37
|
+
* @property {number} metadataLength
|
|
38
|
+
* @property {bigint} indexOffset
|
|
39
|
+
* @property {number} indexLength
|
|
40
|
+
* @property {bigint} dataOffset
|
|
41
|
+
* @property {bigint} dataLength
|
|
42
|
+
* @property {Compression} compression
|
|
43
|
+
* @property {ContentType} contentType
|
|
44
|
+
*/
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* @typedef {string | number | boolean | null | JSONObject | JSONArray} JSONValue
|
|
48
|
+
*/
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* @typedef {Array<JSONValue>} JSONArray
|
|
52
|
+
*/
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* @typedef {{ [key: string]: JSONValue }} JSONObject
|
|
56
|
+
*/
|
|
57
|
+
|
|
58
|
+
export class RangeDB {
|
|
59
|
+
/**
|
|
60
|
+
* Initialize database by providing url of rangedb file.
|
|
61
|
+
*
|
|
62
|
+
* @param {string} url
|
|
63
|
+
* @param {RangeDBOptions} options
|
|
64
|
+
*/
|
|
65
|
+
constructor(url, options = {}) {
|
|
66
|
+
/** @protected @type {string} */
|
|
67
|
+
this.url = url
|
|
68
|
+
|
|
69
|
+
/** @protected @type {string| null} */
|
|
70
|
+
this.etag = null
|
|
71
|
+
|
|
72
|
+
/** @private @type {Header | null} */
|
|
73
|
+
this.header = null
|
|
74
|
+
|
|
75
|
+
/** @private @type {BigUint64Array | null} */
|
|
76
|
+
this.index = null
|
|
77
|
+
|
|
78
|
+
/** @private @type {JSONObject| JSONArray | null} */
|
|
79
|
+
this.metadata = null
|
|
80
|
+
|
|
81
|
+
/** @private @type {number} */
|
|
82
|
+
this.firstReadSize = options.firstReadSize ?? 64 * 1024
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Invalidate header and index.
|
|
87
|
+
*
|
|
88
|
+
* @returns {void}
|
|
89
|
+
*/
|
|
90
|
+
invalidate() {
|
|
91
|
+
this.header = null
|
|
92
|
+
this.index = null
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Perform HTTP range request.
|
|
97
|
+
*
|
|
98
|
+
* @protected
|
|
99
|
+
* @param {bigint} start
|
|
100
|
+
* @param {bigint} end
|
|
101
|
+
* @returns {Promise<ArrayBuffer>}
|
|
102
|
+
*/
|
|
103
|
+
async readRange(start, end) {
|
|
104
|
+
const {headers, arrayBuffer} = await fetch(this.url, {
|
|
105
|
+
headers: {
|
|
106
|
+
range: `bytes=${start}-${end}`,
|
|
107
|
+
},
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
const etag = headers.get('etag')
|
|
111
|
+
if (this.etag && this.etag !== etag) {
|
|
112
|
+
this.invalidate()
|
|
113
|
+
throw new Error('Database file has changed based on ETag.')
|
|
114
|
+
}
|
|
115
|
+
this.etag = etag
|
|
116
|
+
|
|
117
|
+
return arrayBuffer()
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Get header from database or return cached.
|
|
122
|
+
*
|
|
123
|
+
* @returns {Promise<Header>}
|
|
124
|
+
*/
|
|
125
|
+
async getHeader() {
|
|
126
|
+
if (this.header) {
|
|
127
|
+
return this.header
|
|
128
|
+
}
|
|
129
|
+
const buffer = await this.readRange(0n, BigInt(this.firstReadSize) - 1n)
|
|
130
|
+
const view = new DataView(buffer)
|
|
131
|
+
|
|
132
|
+
const magicNumber =
|
|
133
|
+
view.getUint32(0, false) === 0x52616e67 && // Rang
|
|
134
|
+
view.getUint16(4, false) === 0x6544 && // eD
|
|
135
|
+
view.getUint8(6) === 0x42 // B
|
|
136
|
+
|
|
137
|
+
if (!magicNumber) {
|
|
138
|
+
throw new Error(
|
|
139
|
+
'Invalid Magic Number: Expected file starting with "RangeDB" or [0x52 0x61 0x6E 0x67 0x65 0x44 0x42]',
|
|
140
|
+
)
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const specVersion = view.getUint8(7)
|
|
144
|
+
if (specVersion !== 1) {
|
|
145
|
+
throw new Error(`Unsupported spec version. Expected 1 got ${specVersion}`)
|
|
146
|
+
}
|
|
147
|
+
const metadataOffset = view.getBigUint64(8, true)
|
|
148
|
+
const metadataLength = view.getUint32(16, true)
|
|
149
|
+
const indexOffset = view.getBigUint64(20, true)
|
|
150
|
+
const indexLength = view.getUint32(28, true)
|
|
151
|
+
const dataOffset = view.getBigUint64(32, true)
|
|
152
|
+
const dataLength = view.getBigUint64(40, true)
|
|
153
|
+
const compression = view.getInt8(48)
|
|
154
|
+
const contentType = view.getInt8(49)
|
|
155
|
+
|
|
156
|
+
this.header = {
|
|
157
|
+
specVersion,
|
|
158
|
+
metadataOffset,
|
|
159
|
+
metadataLength,
|
|
160
|
+
indexOffset,
|
|
161
|
+
indexLength,
|
|
162
|
+
dataOffset,
|
|
163
|
+
dataLength,
|
|
164
|
+
compression,
|
|
165
|
+
contentType,
|
|
166
|
+
}
|
|
167
|
+
return this.header
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Load index from database or return cached
|
|
172
|
+
*
|
|
173
|
+
* @private
|
|
174
|
+
* @returns {Promise<number>}
|
|
175
|
+
*/
|
|
176
|
+
async getIndex() {
|
|
177
|
+
if (this.index) {
|
|
178
|
+
return this.index.length / 2
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const { indexLength, indexOffset } = await this.getHeader()
|
|
182
|
+
|
|
183
|
+
const buffer = await this.readRange(
|
|
184
|
+
indexOffset,
|
|
185
|
+
indexOffset + BigInt(indexLength) - 1n,
|
|
186
|
+
)
|
|
187
|
+
const view = new DataView(buffer)
|
|
188
|
+
|
|
189
|
+
const indexType = view.getUint8(0)
|
|
190
|
+
if (indexType !== 1) {
|
|
191
|
+
throw new Error(`Unsuported index type: Expected 1 got ${indexType}`)
|
|
192
|
+
}
|
|
193
|
+
const count = view.getUint32(1, true)
|
|
194
|
+
|
|
195
|
+
this.index = new BigUint64Array(buffer, 1 + 4 + 3, count * 2)
|
|
196
|
+
return count
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Get metadata from database or return cached
|
|
201
|
+
*
|
|
202
|
+
* @return {Promise<JSONObject | JSONArray>}
|
|
203
|
+
*/
|
|
204
|
+
async getMetadata() {
|
|
205
|
+
if (this.metadata) {
|
|
206
|
+
return this.metadata
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const { metadataOffset, metadataLength } = await this.getHeader()
|
|
210
|
+
const buffer = await this.readRange(
|
|
211
|
+
metadataOffset,
|
|
212
|
+
metadataOffset + BigInt(metadataLength) - 1n,
|
|
213
|
+
)
|
|
214
|
+
const text = new TextDecoder().decode(buffer)
|
|
215
|
+
this.metadata = JSON.parse(text)
|
|
216
|
+
return this.metadata
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Traverse chunk consisting of mulitple key/value pairs and returns value
|
|
221
|
+
* for given key or null if not founded
|
|
222
|
+
* @private
|
|
223
|
+
* @param {ArrayBuffer} chunk
|
|
224
|
+
* @param {bigint} key
|
|
225
|
+
*
|
|
226
|
+
* @returns {ArrayBuffer | null}
|
|
227
|
+
*/
|
|
228
|
+
static findInChunk(key, chunk) {
|
|
229
|
+
const view = new DataView(chunk)
|
|
230
|
+
const length = chunk.byteLength
|
|
231
|
+
let offset = 0
|
|
232
|
+
|
|
233
|
+
// Chunk format
|
|
234
|
+
// [Key: BigUint64][Data Length: UInt32][Data bytes]
|
|
235
|
+
while (offset < length) {
|
|
236
|
+
const recordKey = view.getBigUint64(offset, true)
|
|
237
|
+
if (key < recordKey) {
|
|
238
|
+
return null
|
|
239
|
+
}
|
|
240
|
+
offset += 8
|
|
241
|
+
|
|
242
|
+
const dataLength = view.getUint32(offset, true)
|
|
243
|
+
offset += 4
|
|
244
|
+
if (recordKey === key) {
|
|
245
|
+
return chunk.slice(offset, offset + dataLength)
|
|
246
|
+
}
|
|
247
|
+
offset += dataLength
|
|
248
|
+
}
|
|
249
|
+
return null
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* Binary search in index for a given key and return value
|
|
254
|
+
* @param {bigint} key
|
|
255
|
+
* @param {BigUint64Array} index
|
|
256
|
+
* @param {bigint} dataEndOffset one behind data ends
|
|
257
|
+
*
|
|
258
|
+
* @return {Range | null} Offset of data
|
|
259
|
+
*/
|
|
260
|
+
static binarySearch(key, index, dataEndOffset) {
|
|
261
|
+
let low = 0,
|
|
262
|
+
high = (index.length >> 1) - 1,
|
|
263
|
+
blockIndex = -1
|
|
264
|
+
|
|
265
|
+
while (low <= high) {
|
|
266
|
+
const midPair = (low + high) >>> 1
|
|
267
|
+
const midIdx = midPair * 2
|
|
268
|
+
const midKey = index[midIdx]
|
|
269
|
+
|
|
270
|
+
if (midKey === key) {
|
|
271
|
+
blockIndex = midIdx
|
|
272
|
+
break
|
|
273
|
+
} else if (midKey < key) {
|
|
274
|
+
blockIndex = midIdx
|
|
275
|
+
low = midPair + 1
|
|
276
|
+
} else {
|
|
277
|
+
high = midPair - 1
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
if (blockIndex === -1) {
|
|
282
|
+
return null
|
|
283
|
+
}
|
|
284
|
+
const start = index[blockIndex + 1]
|
|
285
|
+
const end =
|
|
286
|
+
blockIndex + 2 < index.length ? index[blockIndex + 3] : dataEndOffset
|
|
287
|
+
return {
|
|
288
|
+
start,
|
|
289
|
+
end,
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* Get a raw ArrayBuffer from database for given key or null if not exists
|
|
295
|
+
*
|
|
296
|
+
* @param {bigint} key
|
|
297
|
+
*
|
|
298
|
+
* @returns {Promise<ArrayBuffer | null>}
|
|
299
|
+
* */
|
|
300
|
+
async getRaw(key) {
|
|
301
|
+
if (!this.index) {
|
|
302
|
+
await this.getIndex()
|
|
303
|
+
}
|
|
304
|
+
const { dataOffset, dataLength } = this.header
|
|
305
|
+
const range = RangeDB.binarySearch(key, this.index, dataOffset + dataLength)
|
|
306
|
+
if (!range) {
|
|
307
|
+
return null
|
|
308
|
+
}
|
|
309
|
+
const { start, end } = range
|
|
310
|
+
const chunkBuffer = await this.readRange(start, end - 1n)
|
|
311
|
+
|
|
312
|
+
return RangeDB.findInChunk(key, chunkBuffer)
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/**
|
|
316
|
+
* Get a JSON from database for a given key or null if not exists
|
|
317
|
+
* It may throw JSON parsing error
|
|
318
|
+
*
|
|
319
|
+
* @param {bigint} key
|
|
320
|
+
*
|
|
321
|
+
* @returns {Promise<JSONValue | null>}
|
|
322
|
+
* @throws {SyntaxError}
|
|
323
|
+
*/
|
|
324
|
+
async getJson(key) {
|
|
325
|
+
const buffer = await this.getRaw(key)
|
|
326
|
+
if (buffer === null) {
|
|
327
|
+
return null
|
|
328
|
+
}
|
|
329
|
+
const string = new TextDecoder().decode(buffer)
|
|
330
|
+
return JSON.parse(string)
|
|
331
|
+
}
|
|
332
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@rangedb/js",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"main": "./index.js",
|
|
6
|
+
"types": "./dist/index.d.ts",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": {
|
|
9
|
+
"types": "./dist/index.d.ts",
|
|
10
|
+
"import": "./index.js"
|
|
11
|
+
}
|
|
12
|
+
},
|
|
13
|
+
"scripts": {
|
|
14
|
+
"build:types": "npx -p typescript tsc",
|
|
15
|
+
"prepare": "npm run build:types",
|
|
16
|
+
"test": "node --test --experimental-test-coverage",
|
|
17
|
+
"test:watch": "node --test --watch"
|
|
18
|
+
},
|
|
19
|
+
"engines": {
|
|
20
|
+
"node": "^22.0.0 || >=23.6.0"
|
|
21
|
+
},
|
|
22
|
+
"files": [
|
|
23
|
+
"index.js",
|
|
24
|
+
"dist/index.d.ts"
|
|
25
|
+
],
|
|
26
|
+
"publishConfig": {
|
|
27
|
+
"access": "public",
|
|
28
|
+
"provenance": false
|
|
29
|
+
}
|
|
30
|
+
}
|