@oschwald/maxminddb 0.2.1 → 0.4.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/CHANGELOG.md CHANGED
@@ -5,6 +5,118 @@ 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.4.0] - 2026-09-12
9
+
10
+ ### Changed
11
+
12
+ - Upgraded the Rust `maxminddb` dependency from 0.30.0 to 0.32.0, adding
13
+ resource limits for decoding records and metadata, fixing overflowing
14
+ extended data types, and improving record decoding, search-tree traversal,
15
+ and selective path lookups.
16
+ - Databases that exceed the new decoding limits now raise the same bad-data
17
+ error as other corrupt databases, including databases that previously
18
+ decoded successfully. These limits apply to lookups, batches, compiled
19
+ paths, and network iteration, as well as metadata during opens and reloads.
20
+ - Refreshed Rust and npm development dependencies.
21
+
22
+ ### Performance
23
+
24
+ - Bind single-record `get()` calls to a validated native reader once, avoiding
25
+ repeated receiver validation and generic borrow tracking in the updated
26
+ N-API bindings. Bound calls retain the reader, use its existing reentrancy
27
+ protection, and follow loads and reloads.
28
+
29
+ ### Development
30
+
31
+ - Added regression coverage for decoding limits, records at the supported
32
+ limits, empty metadata containers, malformed database fixtures, and
33
+ overflowing extended data types.
34
+ - Added coverage ensuring failed resource-limited reloads preserve the
35
+ existing reader and its cached records.
36
+
37
+ ## [0.3.1] - 2026-07-19
38
+
39
+ ### Fixed
40
+
41
+ - Updated the packed-package smoke test to accept the npm 12
42
+ `npm pack --json` output format, allowing trusted publishing validation to
43
+ complete with the current npm release.
44
+
45
+ ## [0.3.0] - 2026-07-19
46
+
47
+ ### Added
48
+
49
+ - Added `reloadAsync()` to open and validate replacement databases without
50
+ blocking the JavaScript event loop.
51
+ - Added `getPaths()` and `getManyPaths()` for decoding multiple selected values
52
+ with one database lookup per IP.
53
+ - Added `networksPath()` and `withinPath()` for selectively decoding one value
54
+ while iterating database networks.
55
+
56
+ ### Changed
57
+
58
+ - Numeric path components now reject fractional, non-finite, and unsafe
59
+ JavaScript numbers instead of relying on native integer coercion.
60
+ - Updated the Rust `maxminddb` dependency to 0.30.0, inheriting safer corrupt
61
+ database traversal and faster path and record decoding.
62
+ - Invalid UTF-8 in decoded MMDB string values and map keys now follows Node's
63
+ native conversion behavior and is represented with replacement characters.
64
+ - `MODE_BUFFER` now uses the same native owned-memory reader as `MODE_MEMORY`;
65
+ the constant remains available as a compatibility alias.
66
+ - Network iterators now close their native cursor when a `for...of` loop exits
67
+ early.
68
+ - Compiled `PathLookup` instances can be closed explicitly, release their
69
+ native path automatically after collection, and survive watched reader swaps.
70
+ - Consolidated mutable native reader state behind one runtime borrow, keeping
71
+ reentrant JavaScript access safe without per-field borrow checks.
72
+ - Stopped exposing undocumented native reader constructors and helpers from the
73
+ JavaScript entry point, leaving `nativeVersion()` as the supported diagnostic.
74
+
75
+ ### Fixed
76
+
77
+ - Preserved complete invalid IP values in errors when an overlong string crosses
78
+ the native UTF-8 buffer boundary at a multibyte character.
79
+
80
+ ### Performance
81
+
82
+ - Moved asynchronous reload work, including memory-mapped reloads triggered by
83
+ file watchers, off the JavaScript event loop.
84
+ - Added multi-field projections that reuse one search-tree lookup per IP,
85
+ improving batch throughput when several fields are needed without decoding
86
+ the full record.
87
+ - Moved gzip input detection into the native readers, checking owned-file
88
+ prefixes before allocating their buffers and eliminating an extra
89
+ asynchronous file open, read, and close during path-based opens.
90
+ - Decode MMDB strings and map keys directly from raw bytes into JavaScript
91
+ strings, avoiding redundant UTF-8 decoding in Rust and Node.
92
+ - Reduced lookup and batch allocation by reading IP strings directly from V8
93
+ and constructing batch result arrays in place.
94
+ - Reuse records with shared data offsets within cached network cursor pages,
95
+ avoiding repeated decoding during large network walks.
96
+ - Format network strings on the stack and construct network-result pairs in
97
+ place, avoiding two heap allocations per iterated network.
98
+ - Construct decoded arrays and objects in a single pass and carry JavaScript
99
+ decode context through nested values, reducing allocations and TLS access.
100
+ - Use a protected LRU segment for caches up to 10,000 records, retaining
101
+ frequently reused records during broad scans without increasing capacity.
102
+ - Open owned-memory file modes on a worker thread and read directly into Rust
103
+ memory, avoiding a full intermediate Node `Buffer` and event-loop blocking.
104
+ - Store common compiled paths inline, avoiding a heap allocation on each hot
105
+ path lookup.
106
+ - Return native network pages directly when no buffered records remain,
107
+ avoiding an extra JavaScript array allocation and copy per page.
108
+ - Enable thin link-time optimization for release builds, reducing the native
109
+ binary size and allowing optimization across crate boundaries.
110
+
111
+ ### Development
112
+
113
+ - Corrected projection benchmark found counts to exclude rows containing only
114
+ missing values.
115
+ - Added focused unit coverage for the custom IPv4 parser, IPv4 prefix
116
+ translation, signed path indexes, and inline path storage boundary.
117
+ - Removed the unused eager native network collector; the public API continues
118
+ to use the lower-memory native cursor implementation.
119
+
8
120
  ## [0.2.1] - 2026-07-03
9
121
 
10
122
  * Test fix. No other changes.
@@ -83,5 +195,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
83
195
  - Added Rust formatting, `cargo check`, clippy, TypeScript, Node test, npm pack,
84
196
  and packed-package smoke-test validation.
85
197
 
198
+ [0.4.0]: https://github.com/oschwald/maxminddb-node/releases/tag/v0.4.0
199
+ [0.3.1]: https://github.com/oschwald/maxminddb-node/releases/tag/v0.3.1
200
+ [0.3.0]: https://github.com/oschwald/maxminddb-node/releases/tag/v0.3.0
201
+ [0.2.1]: https://github.com/oschwald/maxminddb-node/releases/tag/v0.2.1
86
202
  [0.2.0]: https://github.com/oschwald/maxminddb-node/releases/tag/v0.2.0
87
203
  [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
  }
@@ -343,12 +308,35 @@ class Reader {
343
308
  this._lastReloadError = null;
344
309
  this._cacheCapacity = normalizeCacheCapacity(options);
345
310
  this._reader = new native.NativeReader(database, this._cacheCapacity);
311
+ this._get = this._reader.bindGet();
346
312
  this.metadata = normalizeMetadata(this._reader.metadata());
347
313
  this.options = options;
348
314
  }
349
315
 
350
316
  static open(filepath, options = {}) {
351
317
  const mode = normalizeMode(options.mode);
318
+ return Reader._fromNative(
319
+ filepath,
320
+ mode,
321
+ options,
322
+ native.openReader(filepath, mode, normalizeCacheCapacity(options))
323
+ );
324
+ }
325
+
326
+ static async openOwned(filepath, mode, options = {}) {
327
+ return Reader._fromNative(
328
+ filepath,
329
+ mode,
330
+ options,
331
+ await native.openReaderAsync(
332
+ filepath,
333
+ mode,
334
+ normalizeCacheCapacity(options)
335
+ )
336
+ );
337
+ }
338
+
339
+ static _fromNative(filepath, mode, options, nativeReader) {
352
340
  const reader = Object.create(Reader.prototype);
353
341
  reader._mode = mode;
354
342
  reader._filepath = filepath;
@@ -359,7 +347,8 @@ class Reader {
359
347
  reader._watchReloadPromise = Promise.resolve();
360
348
  reader._lastReloadError = null;
361
349
  reader._cacheCapacity = normalizeCacheCapacity(options);
362
- reader._reader = native.openReader(filepath, mode, reader._cacheCapacity);
350
+ reader._reader = nativeReader;
351
+ reader._get = nativeReader.bindGet();
363
352
  reader.metadata = normalizeMetadata(reader._reader.metadata());
364
353
  reader.options = options;
365
354
  return reader;
@@ -398,6 +387,38 @@ class Reader {
398
387
  }
399
388
  }
400
389
 
390
+ async reloadAsync() {
391
+ if (!this._filepath) {
392
+ throw new Error('Cannot reload a buffer-backed Reader');
393
+ }
394
+ try {
395
+ const replacement = await native.openReaderAsync(
396
+ this._filepath,
397
+ this._mode,
398
+ this._cacheCapacity
399
+ );
400
+ if (this.closed) {
401
+ replacement.close();
402
+ throw new Error('Cannot reload a closed Reader');
403
+ }
404
+ this._replaceNativeReader(replacement);
405
+ } catch (error) {
406
+ this._lastReloadError = error;
407
+ throw error;
408
+ }
409
+ }
410
+
411
+ _replaceNativeReader(replacement) {
412
+ const metadata = normalizeMetadata(replacement.metadata());
413
+ const get = replacement.bindGet();
414
+ const previous = this._reader;
415
+ this._reader = replacement;
416
+ this._get = get;
417
+ this.metadata = metadata;
418
+ this._lastReloadError = null;
419
+ previous.close();
420
+ }
421
+
401
422
  close() {
402
423
  if (this._watchFilepath && this._watchListener) {
403
424
  fs.unwatchFile(this._watchFilepath, this._watchListener);
@@ -444,15 +465,16 @@ class Reader {
444
465
  }
445
466
 
446
467
  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();
468
+ const replacement = await native.openReaderAsync(
469
+ filepath,
470
+ mode,
471
+ this._cacheCapacity
472
+ );
473
+ if (this.closed || this._watchFilepath !== filepath) {
474
+ replacement.close();
475
+ return;
455
476
  }
477
+ this._replaceNativeReader(replacement);
456
478
  if (!this.closed && this._watchFilepath === filepath && hook) {
457
479
  hook();
458
480
  }
@@ -470,13 +492,17 @@ class Reader {
470
492
  }
471
493
 
472
494
  get(ipAddress) {
473
- return this._reader.get(ipAddress);
495
+ return this._get(ipAddress);
474
496
  }
475
497
 
476
498
  getPath(ipAddress, path) {
477
499
  return this._reader.getPath(ipAddress, path);
478
500
  }
479
501
 
502
+ getPaths(ipAddress, paths) {
503
+ return this._reader.getPaths(ipAddress, paths);
504
+ }
505
+
480
506
  path(path) {
481
507
  return new PathLookup(this, path);
482
508
  }
@@ -493,6 +519,10 @@ class Reader {
493
519
  return this._reader.getManyPath(ipAddresses, path);
494
520
  }
495
521
 
522
+ getManyPaths(ipAddresses, paths) {
523
+ return this._reader.getManyPaths(ipAddresses, paths);
524
+ }
525
+
496
526
  networks(options = {}) {
497
527
  return new NetworkIterator(this, null, options);
498
528
  }
@@ -501,6 +531,14 @@ class Reader {
501
531
  return new NetworkIterator(this, cidr, options);
502
532
  }
503
533
 
534
+ networksPath(path, options = {}) {
535
+ return new NetworkIterator(this, null, options, path);
536
+ }
537
+
538
+ withinPath(cidr, path, options = {}) {
539
+ return new NetworkIterator(this, cidr, options, path);
540
+ }
541
+
504
542
  *networkPages(options = {}) {
505
543
  yield* this.networks(options).pages(options.pageSize ?? 1000);
506
544
  }
@@ -513,12 +551,11 @@ class Reader {
513
551
  async function open(filepath, opts, cb) {
514
552
  assert(!cb, legacyErrorMessage);
515
553
  const options = opts || {};
516
- await assertNotGzipFile(filepath);
517
554
 
518
555
  const mode = normalizeMode(options.mode);
519
556
  const reader =
520
- mode === MODE_BUFFER
521
- ? new Reader(await readFile(filepath), options)
557
+ mode === MODE_MEMORY || mode === MODE_BUFFER
558
+ ? await Reader.openOwned(filepath, mode, options)
522
559
  : Reader.open(filepath, options);
523
560
 
524
561
  if (options.watchForUpdates) {
@@ -559,7 +596,7 @@ function validate(ipAddress) {
559
596
  }
560
597
 
561
598
  module.exports = {
562
- ...native,
599
+ nativeVersion: native.nativeVersion,
563
600
  NetworkIterator,
564
601
  PathLookup,
565
602
  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.4.0",
4
4
  "description": "Rust-backed Node.js reader for MaxMind DB files",
5
5
  "main": "index.js",
6
6
  "types": "index.d.ts",
@@ -51,9 +51,9 @@
51
51
  ],
52
52
  "license": "ISC",
53
53
  "devDependencies": {
54
- "@napi-rs/cli": "^3.7.2",
55
- "@types/node": "^26.1.0",
56
- "typescript": "^6.0.3"
54
+ "@napi-rs/cli": "^3.9.1",
55
+ "@types/node": "^26.5.1",
56
+ "typescript": "^7.0.2"
57
57
  },
58
58
  "engines": {
59
59
  "node": ">=20"