@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 +109 -0
- package/README.md +46 -16
- package/index.d.ts +27 -4
- package/index.darwin-arm64.node +0 -0
- package/index.darwin-x64.node +0 -0
- package/index.js +173 -113
- package/index.linux-arm64-gnu.node +0 -0
- package/index.linux-x64-gnu.node +0 -0
- package/index.win32-arm64-msvc.node +0 -0
- package/index.win32-x64-msvc.node +0 -0
- package/package.json +3 -3
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
|
-
|
|
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()`,
|
|
59
|
-
`reader.path()` lookups decode only the requested
|
|
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
|
|
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()
|
|
98
|
-
you can batch IPs. They cross the native boundary once
|
|
99
|
-
are significantly faster than calling `get()` in a
|
|
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
|
-
|
|
116
|
-
|
|
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`
|
|
129
|
-
at open time but
|
|
130
|
-
|
|
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
|
|
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:
|
|
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:
|
|
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:
|
|
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]>>;
|
package/index.darwin-arm64.node
CHANGED
|
Binary file
|
package/index.darwin-x64.node
CHANGED
|
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
|
-
|
|
162
|
+
this._ensureCompiled();
|
|
163
|
+
return this._nativeReader.getCompiledPath(ipAddress, this._pathId);
|
|
247
164
|
}
|
|
248
165
|
|
|
249
166
|
getMany(ipAddresses) {
|
|
250
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
408
|
-
|
|
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
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
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
|
-
?
|
|
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
|
-
|
|
595
|
+
nativeVersion: native.nativeVersion,
|
|
536
596
|
NetworkIterator,
|
|
537
597
|
PathLookup,
|
|
538
598
|
Reader,
|
|
Binary file
|
package/index.linux-x64-gnu.node
CHANGED
|
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
|
|
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": "^
|
|
56
|
-
"typescript": "^
|
|
55
|
+
"@types/node": "^26.1.0",
|
|
56
|
+
"typescript": "^7.0.2"
|
|
57
57
|
},
|
|
58
58
|
"engines": {
|
|
59
59
|
"node": ">=20"
|