@oschwald/maxminddb 0.2.1 → 0.3.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/CHANGELOG.md CHANGED
@@ -5,6 +5,89 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [0.3.1] - 2026-07-19
9
+
10
+ ### Fixed
11
+
12
+ - Updated the packed-package smoke test to accept the npm 12
13
+ `npm pack --json` output format, allowing trusted publishing validation to
14
+ complete with the current npm release.
15
+
16
+ ## [0.3.0] - 2026-07-19
17
+
18
+ ### Added
19
+
20
+ - Added `reloadAsync()` to open and validate replacement databases without
21
+ blocking the JavaScript event loop.
22
+ - Added `getPaths()` and `getManyPaths()` for decoding multiple selected values
23
+ with one database lookup per IP.
24
+ - Added `networksPath()` and `withinPath()` for selectively decoding one value
25
+ while iterating database networks.
26
+
27
+ ### Changed
28
+
29
+ - Numeric path components now reject fractional, non-finite, and unsafe
30
+ JavaScript numbers instead of relying on native integer coercion.
31
+ - Updated the Rust `maxminddb` dependency to 0.30.0, inheriting safer corrupt
32
+ database traversal and faster path and record decoding.
33
+ - Invalid UTF-8 in decoded MMDB string values and map keys now follows Node's
34
+ native conversion behavior and is represented with replacement characters.
35
+ - `MODE_BUFFER` now uses the same native owned-memory reader as `MODE_MEMORY`;
36
+ the constant remains available as a compatibility alias.
37
+ - Network iterators now close their native cursor when a `for...of` loop exits
38
+ early.
39
+ - Compiled `PathLookup` instances can be closed explicitly, release their
40
+ native path automatically after collection, and survive watched reader swaps.
41
+ - Consolidated mutable native reader state behind one runtime borrow, keeping
42
+ reentrant JavaScript access safe without per-field borrow checks.
43
+ - Stopped exposing undocumented native reader constructors and helpers from the
44
+ JavaScript entry point, leaving `nativeVersion()` as the supported diagnostic.
45
+
46
+ ### Fixed
47
+
48
+ - Preserved complete invalid IP values in errors when an overlong string crosses
49
+ the native UTF-8 buffer boundary at a multibyte character.
50
+
51
+ ### Performance
52
+
53
+ - Moved asynchronous reload work, including memory-mapped reloads triggered by
54
+ file watchers, off the JavaScript event loop.
55
+ - Added multi-field projections that reuse one search-tree lookup per IP,
56
+ improving batch throughput when several fields are needed without decoding
57
+ the full record.
58
+ - Moved gzip input detection into the native readers, checking owned-file
59
+ prefixes before allocating their buffers and eliminating an extra
60
+ asynchronous file open, read, and close during path-based opens.
61
+ - Decode MMDB strings and map keys directly from raw bytes into JavaScript
62
+ strings, avoiding redundant UTF-8 decoding in Rust and Node.
63
+ - Reduced lookup and batch allocation by reading IP strings directly from V8
64
+ and constructing batch result arrays in place.
65
+ - Reuse records with shared data offsets within cached network cursor pages,
66
+ avoiding repeated decoding during large network walks.
67
+ - Format network strings on the stack and construct network-result pairs in
68
+ place, avoiding two heap allocations per iterated network.
69
+ - Construct decoded arrays and objects in a single pass and carry JavaScript
70
+ decode context through nested values, reducing allocations and TLS access.
71
+ - Use a protected LRU segment for caches up to 10,000 records, retaining
72
+ frequently reused records during broad scans without increasing capacity.
73
+ - Open owned-memory file modes on a worker thread and read directly into Rust
74
+ memory, avoiding a full intermediate Node `Buffer` and event-loop blocking.
75
+ - Store common compiled paths inline, avoiding a heap allocation on each hot
76
+ path lookup.
77
+ - Return native network pages directly when no buffered records remain,
78
+ avoiding an extra JavaScript array allocation and copy per page.
79
+ - Enable thin link-time optimization for release builds, reducing the native
80
+ binary size and allowing optimization across crate boundaries.
81
+
82
+ ### Development
83
+
84
+ - Corrected projection benchmark found counts to exclude rows containing only
85
+ missing values.
86
+ - Added focused unit coverage for the custom IPv4 parser, IPv4 prefix
87
+ translation, signed path indexes, and inline path storage boundary.
88
+ - Removed the unused eager native network collector; the public API continues
89
+ to use the lower-memory native cursor implementation.
90
+
8
91
  ## [0.2.1] - 2026-07-03
9
92
 
10
93
  * Test fix. No other changes.
@@ -83,5 +166,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
83
166
  - Added Rust formatting, `cargo check`, clippy, TypeScript, Node test, npm pack,
84
167
  and packed-package smoke-test validation.
85
168
 
169
+ [0.3.1]: https://github.com/oschwald/maxminddb-node/releases/tag/v0.3.1
170
+ [0.3.0]: https://github.com/oschwald/maxminddb-node/releases/tag/v0.3.0
171
+ [0.2.1]: https://github.com/oschwald/maxminddb-node/releases/tag/v0.2.1
86
172
  [0.2.0]: https://github.com/oschwald/maxminddb-node/releases/tag/v0.2.0
87
173
  [0.1.0]: https://github.com/oschwald/maxminddb-node/releases/tag/v0.1.0
package/README.md CHANGED
@@ -61,9 +61,9 @@ cache sizes can therefore retain significant heap memory when the database
61
61
  records are large or lookups touch many distinct data offsets. Cached records
62
62
  are returned by reference, so mutating a cached record can affect later lookups
63
63
  for the same data offset until that entry is evicted, `reader.clearCache()` is
64
- called, or the reader is closed. `getPath()`, `getManyPath()`, and compiled
65
- `reader.path()` lookups decode only the requested path and do not populate the
66
- full-record cache.
64
+ called, or the reader is closed. `getPath()`, `getPaths()`, `getManyPath()`,
65
+ `getManyPaths()`, and compiled `reader.path()` lookups decode only the requested
66
+ paths and do not populate the full-record cache.
67
67
 
68
68
  When `watchForUpdates` is enabled, file-change reloads run serially. A failed
69
69
  watched reload leaves the existing reader active, stores the failure on
@@ -71,12 +71,24 @@ watched reload leaves the existing reader active, stores the failure on
71
71
  reload clears `lastReloadError` and calls the hook. Close watched readers with
72
72
  `reader.close()` to remove the file watcher.
73
73
 
74
+ Call `await reader.reloadAsync()` to open and validate a replacement database
75
+ on a worker thread before atomically swapping it into the reader. The existing
76
+ `reader.reload()` remains available when synchronous replacement is preferred.
77
+
74
78
  ## Extensions
75
79
 
76
80
  ```js
77
81
  reader.getPath('8.8.8.8', ['country', 'iso_code']);
78
82
  reader.getMany(['8.8.8.8', '1.1.1.1']);
79
83
  reader.getManyPath(['8.8.8.8', '1.1.1.1'], ['country', 'iso_code']);
84
+ reader.getPaths('8.8.8.8', [
85
+ ['country', 'iso_code'],
86
+ ['continent', 'code'],
87
+ ]);
88
+ reader.getManyPaths(['8.8.8.8', '1.1.1.1'], [
89
+ ['country', 'iso_code'],
90
+ ['continent', 'code'],
91
+ ]);
80
92
 
81
93
  const countryCode = reader.path(['country', 'iso_code']);
82
94
  countryCode.get('8.8.8.8');
@@ -91,20 +103,32 @@ for (const page of reader.withinPages('81.2.69.0/24', { pageSize: 100 })) {
91
103
  console.log(network, record);
92
104
  }
93
105
  }
106
+
107
+ for (const [network, country] of reader.withinPath(
108
+ '81.2.69.0/24',
109
+ ['country', 'iso_code']
110
+ )) {
111
+ console.log(network, country);
112
+ }
94
113
  ```
95
114
 
96
- Path elements are strings for map keys and numbers for array indexes. Negative
97
- indexes count from the end of an array.
115
+ Path elements are strings for map keys and finite, safe-integer numbers for
116
+ array indexes. Negative indexes count from the end of an array.
98
117
 
99
118
  Create compiled path lookups once and reuse them in hot paths. `reader.path()`
100
119
  parses and stores the path, and the returned `PathLookup` avoids reparsing the
101
120
  path array on each lookup.
102
121
 
103
- For high-volume lookup workloads, prefer `getMany()` or `getManyPath()` when
104
- you can batch IPs. They cross the native boundary once for the whole batch and
105
- are significantly faster than calling `get()` in a JavaScript loop.
122
+ For high-volume lookup workloads, prefer `getMany()`, `getManyPath()`, or
123
+ `getManyPaths()` when you can batch IPs. They cross the native boundary once
124
+ for the whole batch and are significantly faster than calling `get()` in a
125
+ JavaScript loop. `getPaths()` and `getManyPaths()` perform one database lookup
126
+ per IP and return projected values in the same order as the requested paths;
127
+ missing paths are returned as `null`.
106
128
 
107
129
  `networks()` and `within()` return lazy iterators backed by native cursors.
130
+ Use `networksPath()` or `withinPath()` to decode only a selected field while
131
+ walking networks.
108
132
  For large network walks, use `networkPages()`, `withinPages()`, or
109
133
  `NetworkIterator#nextPage()` to cross the native boundary once per page rather
110
134
  than once per network.
@@ -118,8 +142,8 @@ Path-based `open()` defaults to memory-mapped reads:
118
142
  - `MODE_MEMORY`
119
143
  - `MODE_BUFFER`
120
144
 
121
- Use `MODE_BUFFER` if you want `open()` to read the file into a Node `Buffer`
122
- before constructing the reader.
145
+ `MODE_MEMORY` and `MODE_BUFFER` asynchronously read the file once into
146
+ Rust-owned memory. `MODE_BUFFER` is retained as a compatibility alias.
123
147
 
124
148
  ```js
125
149
  import maxmind from '@oschwald/maxminddb';
@@ -133,11 +157,9 @@ Mode tradeoffs:
133
157
 
134
158
  - `MODE_MMAP`/`MODE_AUTO` opens quickly and keeps RSS low by mapping the
135
159
  database file. Replace database files atomically when using watched reloads.
136
- - `MODE_MEMORY` reads the database into Rust-owned memory. It costs more memory
137
- at open time but is independent of the source file after open.
138
- - `MODE_BUFFER` reads the database into a Node `Buffer` before constructing the
139
- native reader. Use it when you need Node-side file loading behavior or when
140
- tests need to mutate a watched temporary file safely.
160
+ - `MODE_MEMORY`/`MODE_BUFFER` asynchronously read the database into Rust-owned
161
+ memory. They cost more memory at open time but are independent of the source
162
+ file after open.
141
163
 
142
164
  ## Performance Notes
143
165
 
package/index.d.ts CHANGED
@@ -193,15 +193,20 @@ export interface CacheStats {
193
193
  readonly evictions: number;
194
194
  }
195
195
 
196
+ export type LookupPath = ReadonlyArray<string | number>;
197
+
196
198
  export declare class PathLookup<TValue = unknown> {
197
199
  readonly path: ReadonlyArray<string | number>;
198
200
  get(ipAddress: string): TValue | null;
199
201
  getMany(ipAddresses: ReadonlyArray<string>): Array<TValue | null>;
202
+ close(): void;
200
203
  }
201
204
 
202
- export declare class NetworkIterator<T extends Response = Response>
205
+ export declare class NetworkIterator<T = Response>
203
206
  implements IterableIterator<[string, T | null]> {
207
+ readonly path: ReadonlyArray<string | number> | null;
204
208
  next(): IteratorResult<[string, T | null]>;
209
+ return(): IteratorReturnResult<undefined>;
205
210
  nextPage(pageSize?: number): Array<[string, T | null]>;
206
211
  pages(pageSize?: number): IterableIterator<Array<[string, T | null]>>;
207
212
  close(): void;
@@ -220,22 +225,40 @@ export declare class Reader<T extends Response = Response> {
220
225
  metadata: Metadata;
221
226
  load(database: Buffer): void;
222
227
  reload(): void;
228
+ reloadAsync(): Promise<void>;
223
229
  close(): void;
224
230
  clearCache(): void;
225
231
  cacheStats(): CacheStats;
226
232
  get(ipAddress: string): T | null;
227
- getPath(ipAddress: string, path: ReadonlyArray<string | number>): unknown;
233
+ getPath(ipAddress: string, path: LookupPath): unknown;
234
+ getPaths<TValues extends ReadonlyArray<unknown> = unknown[]>(
235
+ ipAddress: string,
236
+ paths: ReadonlyArray<LookupPath>,
237
+ ): TValues;
228
238
  path<TValue = unknown>(
229
- path: ReadonlyArray<string | number>,
239
+ path: LookupPath,
230
240
  ): PathLookup<TValue>;
231
241
  getWithPrefixLength(ipAddress: string): [T | null, number];
232
242
  getMany(ipAddresses: ReadonlyArray<string>): Array<T | null>;
233
243
  getManyPath(
234
244
  ipAddresses: ReadonlyArray<string>,
235
- path: ReadonlyArray<string | number>,
245
+ path: LookupPath,
236
246
  ): unknown[];
247
+ getManyPaths<TValues extends ReadonlyArray<unknown> = unknown[]>(
248
+ ipAddresses: ReadonlyArray<string>,
249
+ paths: ReadonlyArray<LookupPath>,
250
+ ): TValues[];
237
251
  networks(options?: NetworkPagesOptions): NetworkIterator<T>;
238
252
  within(cidr: string, options?: NetworkPagesOptions): NetworkIterator<T>;
253
+ networksPath<TValue = unknown>(
254
+ path: ReadonlyArray<string | number>,
255
+ options?: NetworkPagesOptions,
256
+ ): NetworkIterator<TValue>;
257
+ withinPath<TValue = unknown>(
258
+ cidr: string,
259
+ path: ReadonlyArray<string | number>,
260
+ options?: NetworkPagesOptions,
261
+ ): NetworkIterator<TValue>;
239
262
  networkPages(
240
263
  options?: NetworkPagesOptions,
241
264
  ): IterableIterator<Array<[string, T | null]>>;
Binary file
Binary file
package/index.js CHANGED
@@ -54,12 +54,14 @@ function loadNativeBinding() {
54
54
  }
55
55
 
56
56
  const native = loadNativeBinding();
57
+ const pathFinalizer = new FinalizationRegistry(({ nativeReader, pathId }) => {
58
+ try {
59
+ nativeReader.releasePath(pathId);
60
+ } catch {
61
+ // Reader shutdown may have already released all compiled paths.
62
+ }
63
+ });
57
64
 
58
- const DEFAULT_LARGE_FILE_THRESHOLD = 512 * 1024 * 1024;
59
- const LARGE_FILE_THRESHOLD = normalizeLargeFileThreshold(
60
- process.env.MAXMINDDB_LARGE_FILE_THRESHOLD_BYTES
61
- );
62
- const STREAM_WATERMARK = 8 * 1024 * 1024;
63
65
  const DEFAULT_CACHE_MAX = 10_000;
64
66
  const MAX_CACHE_MAX = 0xffffffff;
65
67
  const legacyErrorMessage = `Maxmind v2 module has changed API.
@@ -74,36 +76,6 @@ const MODE_MMAP = 'mmap';
74
76
  const MODE_MEMORY = 'memory';
75
77
  const MODE_BUFFER = 'buffer';
76
78
 
77
- function normalizeLargeFileThreshold(value) {
78
- if (value == null || value === '') {
79
- return DEFAULT_LARGE_FILE_THRESHOLD;
80
- }
81
-
82
- const threshold = Number(value);
83
- return Number.isSafeInteger(threshold) && threshold >= 0
84
- ? threshold
85
- : DEFAULT_LARGE_FILE_THRESHOLD;
86
- }
87
-
88
- function isGzipBuffer(buffer) {
89
- return buffer.length >= 2 && buffer[0] === 0x1f && buffer[1] === 0x8b;
90
- }
91
-
92
- async function assertNotGzipFile(filepath) {
93
- const handle = await fs.promises.open(filepath, 'r');
94
- try {
95
- const buffer = Buffer.alloc(2);
96
- const { bytesRead } = await handle.read(buffer, 0, 2, 0);
97
- if (bytesRead === 2 && isGzipBuffer(buffer)) {
98
- throw new Error(
99
- 'Looks like you are passing in a file in gzip format, please use mmdb database instead.'
100
- );
101
- }
102
- } finally {
103
- await handle.close();
104
- }
105
- }
106
-
107
79
  function normalizeMode(mode) {
108
80
  if (mode == null || mode === MODE_AUTO) {
109
81
  return MODE_MMAP;
@@ -178,93 +150,74 @@ function waitForFile(filepath) {
178
150
  });
179
151
  }
180
152
 
181
- async function readLargeFile(filepath, size) {
182
- return new Promise((resolve, reject) => {
183
- const buffer = Buffer.allocUnsafe(size);
184
- let offset = 0;
185
- let settled = false;
186
- const stream = fs.createReadStream(filepath, {
187
- highWaterMark: STREAM_WATERMARK,
188
- });
189
-
190
- const finish = (error, value) => {
191
- if (settled) {
192
- return;
193
- }
194
- settled = true;
195
- if (error) {
196
- stream.destroy();
197
- reject(error);
198
- } else {
199
- resolve(value);
200
- }
201
- };
202
-
203
- stream.on('data', (chunk) => {
204
- const bufferChunk = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
205
- if (offset + bufferChunk.length > size) {
206
- finish(
207
- new Error(
208
- `File changed while reading ${filepath}: expected ${size} bytes but read more`
209
- )
210
- );
211
- return;
212
- }
213
- bufferChunk.copy(buffer, offset);
214
- offset += bufferChunk.length;
215
- });
216
- stream.on('end', () => {
217
- if (offset !== size) {
218
- finish(
219
- new Error(
220
- `File changed while reading ${filepath}: expected ${size} bytes but read ${offset}`
221
- )
222
- );
223
- return;
224
- }
225
- finish(null, buffer);
226
- });
227
- stream.on('error', (error) => finish(error));
228
- });
229
- }
230
-
231
- async function readFile(filepath) {
232
- const stat = await fs.promises.stat(filepath);
233
- return stat.size < LARGE_FILE_THRESHOLD
234
- ? fs.promises.readFile(filepath)
235
- : readLargeFile(filepath, stat.size);
236
- }
237
-
238
153
  class PathLookup {
239
154
  constructor(reader, path) {
240
155
  this._reader = reader;
241
- this._pathId = reader._reader.compilePath(path);
242
156
  this.path = Object.freeze([...path]);
157
+ this._closed = false;
158
+ this._compileFor(reader._reader);
243
159
  }
244
160
 
245
161
  get(ipAddress) {
246
- return this._reader._reader.getCompiledPath(ipAddress, this._pathId);
162
+ this._ensureCompiled();
163
+ return this._nativeReader.getCompiledPath(ipAddress, this._pathId);
247
164
  }
248
165
 
249
166
  getMany(ipAddresses) {
250
- return this._reader._reader.getManyCompiledPath(ipAddresses, this._pathId);
167
+ this._ensureCompiled();
168
+ return this._nativeReader.getManyCompiledPath(ipAddresses, this._pathId);
169
+ }
170
+
171
+ close() {
172
+ if (!this._closed) {
173
+ pathFinalizer.unregister(this);
174
+ this._nativeReader.releasePath(this._pathId);
175
+ this._closed = true;
176
+ }
177
+ }
178
+
179
+ _ensureCompiled() {
180
+ if (this._closed) {
181
+ throw new Error('Path lookup is closed.');
182
+ }
183
+ if (this._nativeReader !== this._reader._reader) {
184
+ pathFinalizer.unregister(this);
185
+ this._nativeReader.releasePath(this._pathId);
186
+ this._compileFor(this._reader._reader);
187
+ }
188
+ }
189
+
190
+ _compileFor(nativeReader) {
191
+ this._nativeReader = nativeReader;
192
+ this._pathId = nativeReader.compilePath(this.path);
193
+ pathFinalizer.register(
194
+ this,
195
+ { nativeReader: this._nativeReader, pathId: this._pathId },
196
+ this
197
+ );
251
198
  }
252
199
  }
253
200
 
254
201
  class NetworkIterator {
255
- constructor(reader, cidr, options = {}) {
202
+ constructor(reader, cidr, options = {}, path = null) {
256
203
  const [networkOptions, pageSize] = normalizeNetworkIteratorOptions(options);
257
- this._cursor = reader._reader.networkCursor(cidr, ...networkOptions);
204
+ this._cursor = reader._reader.networkCursor(cidr, ...networkOptions, path);
258
205
  this._pageSize = pageSize;
259
206
  this._page = [];
260
207
  this._index = 0;
261
208
  this._done = false;
209
+ this.path = path == null ? null : Object.freeze([...path]);
262
210
  }
263
211
 
264
212
  [Symbol.iterator]() {
265
213
  return this;
266
214
  }
267
215
 
216
+ return() {
217
+ this.close();
218
+ return { done: true, value: undefined };
219
+ }
220
+
268
221
  next() {
269
222
  if (this._done) {
270
223
  return { done: true, value: undefined };
@@ -290,6 +243,16 @@ class NetworkIterator {
290
243
  return [];
291
244
  }
292
245
 
246
+ if (this._index >= this._page.length) {
247
+ this._page = [];
248
+ this._index = 0;
249
+ const page = this._cursor.nextPage(pageSize);
250
+ if (page.length === 0) {
251
+ this.close();
252
+ }
253
+ return page;
254
+ }
255
+
293
256
  const page = [];
294
257
  while (page.length < pageSize && this._index < this._page.length) {
295
258
  page.push(this._page[this._index]);
@@ -298,7 +261,9 @@ class NetworkIterator {
298
261
 
299
262
  if (page.length < pageSize) {
300
263
  const nativePage = this._cursor.nextPage(pageSize - page.length);
301
- page.push(...nativePage);
264
+ for (const record of nativePage) {
265
+ page.push(record);
266
+ }
302
267
  if (nativePage.length === 0) {
303
268
  this.close();
304
269
  }
@@ -349,6 +314,28 @@ class Reader {
349
314
 
350
315
  static open(filepath, options = {}) {
351
316
  const mode = normalizeMode(options.mode);
317
+ return Reader._fromNative(
318
+ filepath,
319
+ mode,
320
+ options,
321
+ native.openReader(filepath, mode, normalizeCacheCapacity(options))
322
+ );
323
+ }
324
+
325
+ static async openOwned(filepath, mode, options = {}) {
326
+ return Reader._fromNative(
327
+ filepath,
328
+ mode,
329
+ options,
330
+ await native.openReaderAsync(
331
+ filepath,
332
+ mode,
333
+ normalizeCacheCapacity(options)
334
+ )
335
+ );
336
+ }
337
+
338
+ static _fromNative(filepath, mode, options, nativeReader) {
352
339
  const reader = Object.create(Reader.prototype);
353
340
  reader._mode = mode;
354
341
  reader._filepath = filepath;
@@ -359,7 +346,7 @@ class Reader {
359
346
  reader._watchReloadPromise = Promise.resolve();
360
347
  reader._lastReloadError = null;
361
348
  reader._cacheCapacity = normalizeCacheCapacity(options);
362
- reader._reader = native.openReader(filepath, mode, reader._cacheCapacity);
349
+ reader._reader = nativeReader;
363
350
  reader.metadata = normalizeMetadata(reader._reader.metadata());
364
351
  reader.options = options;
365
352
  return reader;
@@ -398,6 +385,36 @@ class Reader {
398
385
  }
399
386
  }
400
387
 
388
+ async reloadAsync() {
389
+ if (!this._filepath) {
390
+ throw new Error('Cannot reload a buffer-backed Reader');
391
+ }
392
+ try {
393
+ const replacement = await native.openReaderAsync(
394
+ this._filepath,
395
+ this._mode,
396
+ this._cacheCapacity
397
+ );
398
+ if (this.closed) {
399
+ replacement.close();
400
+ throw new Error('Cannot reload a closed Reader');
401
+ }
402
+ this._replaceNativeReader(replacement);
403
+ } catch (error) {
404
+ this._lastReloadError = error;
405
+ throw error;
406
+ }
407
+ }
408
+
409
+ _replaceNativeReader(replacement) {
410
+ const metadata = normalizeMetadata(replacement.metadata());
411
+ const previous = this._reader;
412
+ this._reader = replacement;
413
+ this.metadata = metadata;
414
+ this._lastReloadError = null;
415
+ previous.close();
416
+ }
417
+
401
418
  close() {
402
419
  if (this._watchFilepath && this._watchListener) {
403
420
  fs.unwatchFile(this._watchFilepath, this._watchListener);
@@ -444,15 +461,16 @@ class Reader {
444
461
  }
445
462
 
446
463
  try {
447
- if (mode === MODE_BUFFER) {
448
- const database = await readFile(filepath);
449
- if (this.closed || this._watchFilepath !== filepath) {
450
- return;
451
- }
452
- this.load(database);
453
- } else {
454
- this.reload();
464
+ const replacement = await native.openReaderAsync(
465
+ filepath,
466
+ mode,
467
+ this._cacheCapacity
468
+ );
469
+ if (this.closed || this._watchFilepath !== filepath) {
470
+ replacement.close();
471
+ return;
455
472
  }
473
+ this._replaceNativeReader(replacement);
456
474
  if (!this.closed && this._watchFilepath === filepath && hook) {
457
475
  hook();
458
476
  }
@@ -477,6 +495,10 @@ class Reader {
477
495
  return this._reader.getPath(ipAddress, path);
478
496
  }
479
497
 
498
+ getPaths(ipAddress, paths) {
499
+ return this._reader.getPaths(ipAddress, paths);
500
+ }
501
+
480
502
  path(path) {
481
503
  return new PathLookup(this, path);
482
504
  }
@@ -493,6 +515,10 @@ class Reader {
493
515
  return this._reader.getManyPath(ipAddresses, path);
494
516
  }
495
517
 
518
+ getManyPaths(ipAddresses, paths) {
519
+ return this._reader.getManyPaths(ipAddresses, paths);
520
+ }
521
+
496
522
  networks(options = {}) {
497
523
  return new NetworkIterator(this, null, options);
498
524
  }
@@ -501,6 +527,14 @@ class Reader {
501
527
  return new NetworkIterator(this, cidr, options);
502
528
  }
503
529
 
530
+ networksPath(path, options = {}) {
531
+ return new NetworkIterator(this, null, options, path);
532
+ }
533
+
534
+ withinPath(cidr, path, options = {}) {
535
+ return new NetworkIterator(this, cidr, options, path);
536
+ }
537
+
504
538
  *networkPages(options = {}) {
505
539
  yield* this.networks(options).pages(options.pageSize ?? 1000);
506
540
  }
@@ -513,12 +547,11 @@ class Reader {
513
547
  async function open(filepath, opts, cb) {
514
548
  assert(!cb, legacyErrorMessage);
515
549
  const options = opts || {};
516
- await assertNotGzipFile(filepath);
517
550
 
518
551
  const mode = normalizeMode(options.mode);
519
552
  const reader =
520
- mode === MODE_BUFFER
521
- ? new Reader(await readFile(filepath), options)
553
+ mode === MODE_MEMORY || mode === MODE_BUFFER
554
+ ? await Reader.openOwned(filepath, mode, options)
522
555
  : Reader.open(filepath, options);
523
556
 
524
557
  if (options.watchForUpdates) {
@@ -559,7 +592,7 @@ function validate(ipAddress) {
559
592
  }
560
593
 
561
594
  module.exports = {
562
- ...native,
595
+ nativeVersion: native.nativeVersion,
563
596
  NetworkIterator,
564
597
  PathLookup,
565
598
  Reader,
Binary file
Binary file
Binary file
Binary file
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oschwald/maxminddb",
3
- "version": "0.2.1",
3
+ "version": "0.3.1",
4
4
  "description": "Rust-backed Node.js reader for MaxMind DB files",
5
5
  "main": "index.js",
6
6
  "types": "index.d.ts",
@@ -53,7 +53,7 @@
53
53
  "devDependencies": {
54
54
  "@napi-rs/cli": "^3.7.2",
55
55
  "@types/node": "^26.1.0",
56
- "typescript": "^6.0.3"
56
+ "typescript": "^7.0.2"
57
57
  },
58
58
  "engines": {
59
59
  "node": ">=20"