@oschwald/maxminddb 0.1.0 → 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,111 @@ 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
+
91
+ ## [0.2.1] - 2026-07-03
92
+
93
+ * Test fix. No other changes.
94
+
95
+ ## [0.2.0] - 2026-07-03
96
+
97
+ ### Changed
98
+
99
+ - Updated the Rust `maxminddb` dependency to 0.29.0 and refreshed Rust and npm
100
+ development dependencies.
101
+ - Coalesced watched file reload events so bursts trigger fewer duplicate reloads
102
+ and hook calls while preserving serialized reloads.
103
+
104
+ ### Performance
105
+
106
+ - Decoded path lookup results directly into JavaScript values to reduce
107
+ intermediate allocations for `getPath()`, `getManyPath()`, and compiled path
108
+ lookups.
109
+ - Decoded network iteration records directly into JavaScript values to reduce
110
+ intermediate allocations for `networks()`, `within()`, and paginated network
111
+ iteration.
112
+
8
113
  ## [0.1.0] - 2026-06-14
9
114
 
10
115
  ### Added
@@ -61,4 +166,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
61
166
  - Added Rust formatting, `cargo check`, clippy, TypeScript, Node test, npm pack,
62
167
  and packed-package smoke-test validation.
63
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
172
+ [0.2.0]: https://github.com/oschwald/maxminddb-node/releases/tag/v0.2.0
64
173
  [0.1.0]: https://github.com/oschwald/maxminddb-node/releases/tag/v0.1.0
package/README.md CHANGED
@@ -16,8 +16,11 @@ npm install @oschwald/maxminddb
16
16
 
17
17
  ## Usage
18
18
 
19
+ These examples use ES modules. Save them as `.mjs`, or set `"type": "module"`
20
+ in your `package.json`.
21
+
19
22
  ```js
20
- const maxmind = require('@oschwald/maxminddb');
23
+ import maxmind from '@oschwald/maxminddb';
21
24
 
22
25
  const reader = await maxmind.open('/path/to/GeoIP2-City.mmdb');
23
26
 
@@ -26,6 +29,9 @@ console.log(reader.getWithPrefixLength('8.8.8.8'));
26
29
  console.log(reader.getPath('8.8.8.8', ['country', 'iso_code']));
27
30
  ```
28
31
 
32
+ CommonJS `require()` is also supported for compatibility with existing
33
+ `node-maxmind` users.
34
+
29
35
  ## Compatibility API
30
36
 
31
37
  - `open(filepath, options?)`
@@ -55,9 +61,9 @@ cache sizes can therefore retain significant heap memory when the database
55
61
  records are large or lookups touch many distinct data offsets. Cached records
56
62
  are returned by reference, so mutating a cached record can affect later lookups
57
63
  for the same data offset until that entry is evicted, `reader.clearCache()` is
58
- called, or the reader is closed. `getPath()`, `getManyPath()`, and compiled
59
- `reader.path()` lookups decode only the requested path and do not populate the
60
- 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.
61
67
 
62
68
  When `watchForUpdates` is enabled, file-change reloads run serially. A failed
63
69
  watched reload leaves the existing reader active, stores the failure on
@@ -65,12 +71,24 @@ watched reload leaves the existing reader active, stores the failure on
65
71
  reload clears `lastReloadError` and calls the hook. Close watched readers with
66
72
  `reader.close()` to remove the file watcher.
67
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
+
68
78
  ## Extensions
69
79
 
70
80
  ```js
71
81
  reader.getPath('8.8.8.8', ['country', 'iso_code']);
72
82
  reader.getMany(['8.8.8.8', '1.1.1.1']);
73
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
+ ]);
74
92
 
75
93
  const countryCode = reader.path(['country', 'iso_code']);
76
94
  countryCode.get('8.8.8.8');
@@ -85,20 +103,32 @@ for (const page of reader.withinPages('81.2.69.0/24', { pageSize: 100 })) {
85
103
  console.log(network, record);
86
104
  }
87
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
+ }
88
113
  ```
89
114
 
90
- Path elements are strings for map keys and numbers for array indexes. Negative
91
- 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.
92
117
 
93
118
  Create compiled path lookups once and reuse them in hot paths. `reader.path()`
94
119
  parses and stores the path, and the returned `PathLookup` avoids reparsing the
95
120
  path array on each lookup.
96
121
 
97
- For high-volume lookup workloads, prefer `getMany()` or `getManyPath()` when
98
- you can batch IPs. They cross the native boundary once for the whole batch and
99
- 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`.
100
128
 
101
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.
102
132
  For large network walks, use `networkPages()`, `withinPages()`, or
103
133
  `NetworkIterator#nextPage()` to cross the native boundary once per page rather
104
134
  than once per network.
@@ -112,10 +142,12 @@ Path-based `open()` defaults to memory-mapped reads:
112
142
  - `MODE_MEMORY`
113
143
  - `MODE_BUFFER`
114
144
 
115
- Use `MODE_BUFFER` if you want `open()` to read the file into a Node `Buffer`
116
- 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.
117
147
 
118
148
  ```js
149
+ import maxmind from '@oschwald/maxminddb';
150
+
119
151
  const reader = await maxmind.open('/path/to/db.mmdb', {
120
152
  mode: maxmind.MODE_MEMORY,
121
153
  });
@@ -125,11 +157,9 @@ Mode tradeoffs:
125
157
 
126
158
  - `MODE_MMAP`/`MODE_AUTO` opens quickly and keeps RSS low by mapping the
127
159
  database file. Replace database files atomically when using watched reloads.
128
- - `MODE_MEMORY` reads the database into Rust-owned memory. It costs more memory
129
- at open time but is independent of the source file after open.
130
- - `MODE_BUFFER` reads the database into a Node `Buffer` before constructing the
131
- native reader. Use it when you need Node-side file loading behavior or when
132
- 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.
133
163
 
134
164
  ## Performance Notes
135
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
  }
@@ -337,6 +302,8 @@ class Reader {
337
302
  this._filepath = null;
338
303
  this._watchFilepath = null;
339
304
  this._watchListener = null;
305
+ this._watchReloadActive = false;
306
+ this._watchReloadPending = false;
340
307
  this._watchReloadPromise = Promise.resolve();
341
308
  this._lastReloadError = null;
342
309
  this._cacheCapacity = normalizeCacheCapacity(options);
@@ -347,15 +314,39 @@ class Reader {
347
314
 
348
315
  static open(filepath, options = {}) {
349
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) {
350
339
  const reader = Object.create(Reader.prototype);
351
340
  reader._mode = mode;
352
341
  reader._filepath = filepath;
353
342
  reader._watchFilepath = null;
354
343
  reader._watchListener = null;
344
+ reader._watchReloadActive = false;
345
+ reader._watchReloadPending = false;
355
346
  reader._watchReloadPromise = Promise.resolve();
356
347
  reader._lastReloadError = null;
357
348
  reader._cacheCapacity = normalizeCacheCapacity(options);
358
- reader._reader = native.openReader(filepath, mode, reader._cacheCapacity);
349
+ reader._reader = nativeReader;
359
350
  reader.metadata = normalizeMetadata(reader._reader.metadata());
360
351
  reader.options = options;
361
352
  return reader;
@@ -394,18 +385,71 @@ class Reader {
394
385
  }
395
386
  }
396
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
+
397
418
  close() {
398
419
  if (this._watchFilepath && this._watchListener) {
399
420
  fs.unwatchFile(this._watchFilepath, this._watchListener);
400
421
  this._watchFilepath = null;
401
422
  this._watchListener = null;
402
423
  }
424
+ this._watchReloadPending = false;
403
425
  this._reader.close();
404
426
  }
405
427
 
406
428
  _queueWatchedReload(filepath, mode, hook) {
407
- const reload = () => this._reloadWatchedFile(filepath, mode, hook);
408
- this._watchReloadPromise = this._watchReloadPromise.then(reload, reload);
429
+ this._watchReloadPending = true;
430
+ if (this._watchReloadActive) {
431
+ return;
432
+ }
433
+
434
+ this._watchReloadActive = true;
435
+ const drainReloads = async () => {
436
+ try {
437
+ while (this._watchReloadPending) {
438
+ this._watchReloadPending = false;
439
+ await this._reloadWatchedFile(filepath, mode, hook);
440
+ }
441
+ } finally {
442
+ this._watchReloadActive = false;
443
+ if (this._watchReloadPending) {
444
+ this._queueWatchedReload(filepath, mode, hook);
445
+ }
446
+ }
447
+ };
448
+
449
+ this._watchReloadPromise = this._watchReloadPromise.then(
450
+ drainReloads,
451
+ drainReloads
452
+ );
409
453
  }
410
454
 
411
455
  async _reloadWatchedFile(filepath, mode, hook) {
@@ -417,15 +461,16 @@ class Reader {
417
461
  }
418
462
 
419
463
  try {
420
- if (mode === MODE_BUFFER) {
421
- const database = await readFile(filepath);
422
- if (this.closed || this._watchFilepath !== filepath) {
423
- return;
424
- }
425
- this.load(database);
426
- } else {
427
- 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;
428
472
  }
473
+ this._replaceNativeReader(replacement);
429
474
  if (!this.closed && this._watchFilepath === filepath && hook) {
430
475
  hook();
431
476
  }
@@ -450,6 +495,10 @@ class Reader {
450
495
  return this._reader.getPath(ipAddress, path);
451
496
  }
452
497
 
498
+ getPaths(ipAddress, paths) {
499
+ return this._reader.getPaths(ipAddress, paths);
500
+ }
501
+
453
502
  path(path) {
454
503
  return new PathLookup(this, path);
455
504
  }
@@ -466,6 +515,10 @@ class Reader {
466
515
  return this._reader.getManyPath(ipAddresses, path);
467
516
  }
468
517
 
518
+ getManyPaths(ipAddresses, paths) {
519
+ return this._reader.getManyPaths(ipAddresses, paths);
520
+ }
521
+
469
522
  networks(options = {}) {
470
523
  return new NetworkIterator(this, null, options);
471
524
  }
@@ -474,6 +527,14 @@ class Reader {
474
527
  return new NetworkIterator(this, cidr, options);
475
528
  }
476
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
+
477
538
  *networkPages(options = {}) {
478
539
  yield* this.networks(options).pages(options.pageSize ?? 1000);
479
540
  }
@@ -486,12 +547,11 @@ class Reader {
486
547
  async function open(filepath, opts, cb) {
487
548
  assert(!cb, legacyErrorMessage);
488
549
  const options = opts || {};
489
- await assertNotGzipFile(filepath);
490
550
 
491
551
  const mode = normalizeMode(options.mode);
492
552
  const reader =
493
- mode === MODE_BUFFER
494
- ? new Reader(await readFile(filepath), options)
553
+ mode === MODE_MEMORY || mode === MODE_BUFFER
554
+ ? await Reader.openOwned(filepath, mode, options)
495
555
  : Reader.open(filepath, options);
496
556
 
497
557
  if (options.watchForUpdates) {
@@ -532,7 +592,7 @@ function validate(ipAddress) {
532
592
  }
533
593
 
534
594
  module.exports = {
535
- ...native,
595
+ nativeVersion: native.nativeVersion,
536
596
  NetworkIterator,
537
597
  PathLookup,
538
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.1.0",
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",
@@ -52,8 +52,8 @@
52
52
  "license": "ISC",
53
53
  "devDependencies": {
54
54
  "@napi-rs/cli": "^3.7.2",
55
- "@types/node": "^25.9.3",
56
- "typescript": "^5.9.2"
55
+ "@types/node": "^26.1.0",
56
+ "typescript": "^7.0.2"
57
57
  },
58
58
  "engines": {
59
59
  "node": ">=20"