@oschwald/maxminddb 0.1.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 ADDED
@@ -0,0 +1,64 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [0.1.0] - 2026-06-14
9
+
10
+ ### Added
11
+
12
+ - Initial Rust-backed Node.js module for MaxMind DB files, published as
13
+ `@oschwald/maxminddb`.
14
+ - Added a `node-maxmind`-compatible API with `open()`, `Reader`, `get()`,
15
+ `getWithPrefixLength()`, `load()`, `metadata`, `validate()`, and legacy
16
+ `init()`/`openSync()` error behavior.
17
+ - Added file-backed and buffer-backed readers with `MODE_AUTO`, `MODE_MMAP`,
18
+ `MODE_MEMORY`, and `MODE_BUFFER` open modes.
19
+ - Added optional watched reloads with serialized reload handling,
20
+ `lastReloadError`, and explicit watcher cleanup on `reader.close()`.
21
+ - Added native LRU caching of materialized records with `cacheStats()` and
22
+ `clearCache()`.
23
+ - Added path lookup extensions with `getPath()`, `getManyPath()`, and compiled
24
+ `reader.path()` lookups.
25
+ - Added batch lookup support with `getMany()`.
26
+ - Added lazy network iteration via native cursors, including `networks()`,
27
+ `within()`, `networkPages()`, `withinPages()`, and `NetworkIterator#nextPage()`.
28
+ - Added TypeScript declarations for the public API.
29
+ - Added benchmark tooling for comparing throughput against `node-maxmind`.
30
+ - Added npm trusted publishing with prebuilt native binaries for Linux x64 GNU,
31
+ Linux arm64 GNU, macOS x64, macOS arm64, Windows x64 MSVC, and Windows arm64
32
+ MSVC.
33
+
34
+ ### Changed
35
+
36
+ - The npm package is scoped as `@oschwald/maxminddb`; the Rust crate package is named
37
+ `maxminddb-node` to avoid colliding with the upstream Rust `maxminddb` crate.
38
+ - Package metadata includes repository, homepage, bugs, author, export map, and
39
+ ISC license metadata.
40
+ - Requires Node.js 20 or newer.
41
+
42
+ ### Fixed
43
+
44
+ - Rejected gzip database inputs before opening.
45
+ - Hardened watched reloads so failed reloads keep the existing reader active.
46
+ - Hardened streamed large-file reads so truncated or growing files are rejected
47
+ instead of returning partially initialized buffers.
48
+ - Added decode corpus regressions for mixed MaxMind DB value types.
49
+
50
+ ### Performance
51
+
52
+ - Used memory-mapped file reads by default for fast opens and low RSS.
53
+ - Added direct N-API decoding paths, cached property descriptor names, and an
54
+ IPv4 parser fast path for hot lookups.
55
+ - Added batch lookup and native cursor APIs to reduce JavaScript/native boundary
56
+ crossings.
57
+
58
+ ### Development
59
+
60
+ - Added CI for Node 20, 22, and 24, plus macOS and Windows coverage.
61
+ - Added Rust formatting, `cargo check`, clippy, TypeScript, Node test, npm pack,
62
+ and packed-package smoke-test validation.
63
+
64
+ [0.1.0]: https://github.com/oschwald/maxminddb-node/releases/tag/v0.1.0
package/LICENSE ADDED
@@ -0,0 +1,15 @@
1
+ ISC License
2
+
3
+ Copyright (c) 2015, Gregory J. Oschwald <oschwald@gmail.com>
4
+
5
+ Permission to use, copy, modify, and/or distribute this software for any
6
+ purpose with or without fee is hereby granted, provided that the above
7
+ copyright notice and this permission notice appear in all copies.
8
+
9
+ THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
10
+ WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
11
+ MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
12
+ ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
13
+ WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
14
+ ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
15
+ OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,187 @@
1
+ # @oschwald/maxminddb
2
+
3
+ Rust-backed Node.js reader for MaxMind DB files.
4
+
5
+ The public API is compatible with the commonly used `maxmind` package from
6
+ `node-maxmind` and adds Rust-backed extensions for path lookup, batch lookup,
7
+ and network iteration.
8
+
9
+ ## Install
10
+
11
+ Node.js 20 or newer is required.
12
+
13
+ ```sh
14
+ npm install @oschwald/maxminddb
15
+ ```
16
+
17
+ ## Usage
18
+
19
+ ```js
20
+ const maxmind = require('@oschwald/maxminddb');
21
+
22
+ const reader = await maxmind.open('/path/to/GeoIP2-City.mmdb');
23
+
24
+ console.log(reader.get('8.8.8.8'));
25
+ console.log(reader.getWithPrefixLength('8.8.8.8'));
26
+ console.log(reader.getPath('8.8.8.8', ['country', 'iso_code']));
27
+ ```
28
+
29
+ ## Compatibility API
30
+
31
+ - `open(filepath, options?)`
32
+ - `new Reader(buffer, options?)`
33
+ - `reader.get(ipAddress)`
34
+ - `reader.getWithPrefixLength(ipAddress)`
35
+ - `reader.load(buffer)`
36
+ - `reader.metadata`
37
+ - `validate(ipAddress)`
38
+ - `init()` and `openSync()` keep the legacy `node-maxmind` error behavior.
39
+
40
+ `open()` accepts the existing `node-maxmind` options:
41
+
42
+ - `cache`
43
+ - `watchForUpdates`
44
+ - `watchForUpdatesNonPersistent`
45
+ - `watchForUpdatesHook`
46
+
47
+ `cache` controls a native LRU cache of materialized records keyed by MaxMind DB
48
+ data offset. The default is 10,000 records, matching `node-maxmind`. Pass
49
+ `cache: { max: 1000 }` to tune the cache size or `cache: false` to disable it.
50
+ Use `reader.cacheStats()` to inspect hit/miss counters and `reader.clearCache()`
51
+ to release cached record references.
52
+
53
+ The record cache stores JavaScript objects, not compressed database bytes. Large
54
+ cache sizes can therefore retain significant heap memory when the database
55
+ records are large or lookups touch many distinct data offsets. Cached records
56
+ are returned by reference, so mutating a cached record can affect later lookups
57
+ 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.
61
+
62
+ When `watchForUpdates` is enabled, file-change reloads run serially. A failed
63
+ watched reload leaves the existing reader active, stores the failure on
64
+ `reader.lastReloadError`, and skips `watchForUpdatesHook`. The next successful
65
+ reload clears `lastReloadError` and calls the hook. Close watched readers with
66
+ `reader.close()` to remove the file watcher.
67
+
68
+ ## Extensions
69
+
70
+ ```js
71
+ reader.getPath('8.8.8.8', ['country', 'iso_code']);
72
+ reader.getMany(['8.8.8.8', '1.1.1.1']);
73
+ reader.getManyPath(['8.8.8.8', '1.1.1.1'], ['country', 'iso_code']);
74
+
75
+ const countryCode = reader.path(['country', 'iso_code']);
76
+ countryCode.get('8.8.8.8');
77
+ countryCode.getMany(['8.8.8.8', '1.1.1.1']);
78
+
79
+ for (const [network, record] of reader.within('81.2.69.142/31')) {
80
+ console.log(network, record);
81
+ }
82
+
83
+ for (const page of reader.withinPages('81.2.69.0/24', { pageSize: 100 })) {
84
+ for (const [network, record] of page) {
85
+ console.log(network, record);
86
+ }
87
+ }
88
+ ```
89
+
90
+ Path elements are strings for map keys and numbers for array indexes. Negative
91
+ indexes count from the end of an array.
92
+
93
+ Create compiled path lookups once and reuse them in hot paths. `reader.path()`
94
+ parses and stores the path, and the returned `PathLookup` avoids reparsing the
95
+ path array on each lookup.
96
+
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.
100
+
101
+ `networks()` and `within()` return lazy iterators backed by native cursors.
102
+ For large network walks, use `networkPages()`, `withinPages()`, or
103
+ `NetworkIterator#nextPage()` to cross the native boundary once per page rather
104
+ than once per network.
105
+
106
+ ## Open Modes
107
+
108
+ Path-based `open()` defaults to memory-mapped reads:
109
+
110
+ - `MODE_AUTO`
111
+ - `MODE_MMAP`
112
+ - `MODE_MEMORY`
113
+ - `MODE_BUFFER`
114
+
115
+ Use `MODE_BUFFER` if you want `open()` to read the file into a Node `Buffer`
116
+ before constructing the reader.
117
+
118
+ ```js
119
+ const reader = await maxmind.open('/path/to/db.mmdb', {
120
+ mode: maxmind.MODE_MEMORY,
121
+ });
122
+ ```
123
+
124
+ Mode tradeoffs:
125
+
126
+ - `MODE_MMAP`/`MODE_AUTO` opens quickly and keeps RSS low by mapping the
127
+ 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.
133
+
134
+ ## Performance Notes
135
+
136
+ Performance depends on database size, record shape, cache hit rate, CPU, Node
137
+ version, and whether the database is warm in the OS page cache. On one local
138
+ run with 200,000 generated IPv4 lookups against `/var/lib/GeoIP`, this module
139
+ had much faster open times and lower RSS than `node-maxmind`, while cached
140
+ single-record lookup throughput was still lower:
141
+
142
+ | Database | maxminddb default cache | node-maxmind default cache | maxminddb cache:100k | node-maxmind cache:100k |
143
+ | --- | ---: | ---: | ---: | ---: |
144
+ | GeoIP2-City | 370k/s | 441k/s | 632k/s | 869k/s |
145
+ | GeoLite2-City | 452k/s | 498k/s | 715k/s | 1.07M/s |
146
+
147
+ The same run opened mapped readers in sub-millisecond to low single-digit
148
+ milliseconds after warmup, while `node-maxmind` open times were tens of
149
+ milliseconds and retained tens to hundreds of MB of RSS. Batch lookups were
150
+ faster than JavaScript loops over `get()`, reaching roughly 3.0-3.4M IPs/s in
151
+ that run.
152
+
153
+ Run local benchmarks with:
154
+
155
+ ```sh
156
+ npm run bench -- --compare-node-maxmind --db /path/to/db.mmdb
157
+ ```
158
+
159
+ ## Supported Platforms
160
+
161
+ The npm package is set up to ship prebuilt native modules for Linux x64 GNU,
162
+ Linux arm64 GNU, macOS x64, macOS arm64, Windows x64 MSVC, and Windows arm64
163
+ MSVC. The loader also knows platform-specific filenames for additional Linux,
164
+ Windows, and FreeBSD targets, but those artifacts are not part of the default
165
+ trusted publishing workflow yet. See [RELEASE.md](./RELEASE.md) for the native
166
+ artifact strategy.
167
+
168
+ ## Development
169
+
170
+ ```sh
171
+ npm install
172
+ npm run build
173
+ npm test
174
+ npm run typecheck
175
+ npm run bench -- --compare-node-maxmind
176
+ npm run release
177
+ npm run bench -- --save-baseline /tmp/maxminddb-baseline.json
178
+ npm run bench -- --baseline /tmp/maxminddb-baseline.json --min-ratio 0.9
179
+ npm run --silent bench -- --json > bench-results.json
180
+ ```
181
+
182
+ See [RELEASE.md](./RELEASE.md) for packaging expectations and the native
183
+ prebuild release strategy.
184
+
185
+ ## License
186
+
187
+ ISC License. See [LICENSE](./LICENSE) for details.
package/RELEASE.md ADDED
@@ -0,0 +1,93 @@
1
+ # Release Notes
2
+
3
+ This package loads a native N-API addon at runtime. A publishable npm tarball
4
+ must therefore include built `.node` files for the platforms it supports.
5
+
6
+ ## Current Artifact Shape
7
+
8
+ `index.js` probes for platform-specific native bindings first, then falls back
9
+ to `index.node`:
10
+
11
+ - Linux: `index.linux-*-*.node`
12
+ - macOS: `index.darwin-*.node`
13
+ - Windows: `index.win32-*-*.node`
14
+ - FreeBSD: `index.freebsd-x64.node`
15
+ - Generic fallback: `index.node`
16
+
17
+ `package.json` includes `*.node` files in the tarball, but it does not build a
18
+ native addon during package installation. For local packaging checks, build the
19
+ addon before packing:
20
+
21
+ ```sh
22
+ npm ci
23
+ npm run build
24
+ npm pack --dry-run
25
+ ```
26
+
27
+ The dry run should show `index.js`, `index.d.ts`, `package.json`, `README.md`,
28
+ and one or more `.node` files.
29
+
30
+ The trusted publishing workflow assembles one npm package containing these
31
+ prebuilt native modules:
32
+
33
+ - Linux x64 GNU: `index.linux-x64-gnu.node`
34
+ - Linux arm64 GNU: `index.linux-arm64-gnu.node`
35
+ - macOS x64: `index.darwin-x64.node`
36
+ - macOS arm64: `index.darwin-arm64.node`
37
+ - Windows x64 MSVC: `index.win32-x64-msvc.node`
38
+ - Windows arm64 MSVC: `index.win32-arm64-msvc.node`
39
+
40
+ The workflow does not currently build Linux musl/Alpine, Linux armv7, Linux
41
+ ppc64/s390x/riscv64, Windows ia32, or FreeBSD artifacts. The loader supports
42
+ platform-specific filenames for several of those targets, so adding them is a
43
+ release automation task rather than a runtime API change.
44
+
45
+ ## Publishing Strategy
46
+
47
+ The repository currently publishes `@oschwald/maxminddb` as one package
48
+ containing every supported `index.<triple>.node` file. This keeps installation
49
+ simple and avoids install-time native builds. If the tarball becomes too large,
50
+ the next packaging shape to consider is separate optional dependency packages
51
+ per target triple:
52
+
53
+ - Publish separate optional dependency packages per target triple, each
54
+ containing one `index.<triple>.node`.
55
+
56
+ That model keeps install size small while preserving install-time reliability.
57
+ Publishing source only and building during package installation is intentionally
58
+ not the default release strategy.
59
+
60
+ The package is scoped and public. `package.json` sets `publishConfig.access` to
61
+ `public`, and local bootstrap publishes should also pass `--access public`.
62
+
63
+ ## Trusted Publishing
64
+
65
+ Publishing is configured through `.github/workflows/publish.yml` and npm trusted
66
+ publishing. The workflow does not use an `NPM_TOKEN`; it grants GitHub Actions
67
+ `id-token: write` permission so npm can authenticate the publish through OIDC.
68
+
69
+ Before the first publish, configure the package on npmjs.com:
70
+
71
+ - Publisher: GitHub Actions
72
+ - Organization or user: the GitHub owner of this repository
73
+ - Repository: the GitHub repository name
74
+ - Workflow filename: `publish.yml`
75
+ - Allowed action: `npm publish`
76
+
77
+ The workflow uses Node 24 and upgrades npm before publishing so the npm CLI
78
+ meets trusted publishing requirements. It builds native modules on hosted
79
+ Linux, macOS, and Windows runners, smoke-tests that each native module loads on
80
+ its build runner, downloads those artifacts into the package root, runs the same
81
+ validation as CI, and verifies the package with `npm pack --dry-run`. GitHub
82
+ release events then run `npm publish`. Manual `workflow_dispatch` runs are
83
+ validation-only unless the `publish` input is set to `true`.
84
+
85
+ ## Release Checklist
86
+
87
+ 1. Configure npm trusted publishing for `.github/workflows/publish.yml`.
88
+ 2. Add a top entry to `CHANGELOG.md` with today's date.
89
+ 3. Run `npm run release`.
90
+ 4. Run `npm run bench -- --compare-node-maxmind` when benchmark databases are
91
+ available.
92
+ 5. Verify the `build-binaries` matrix completed for every supported target
93
+ before the `publish` job runs `npm publish`.
package/index.d.ts ADDED
@@ -0,0 +1,272 @@
1
+ export type Names = {
2
+ readonly de?: string;
3
+ readonly en: string;
4
+ readonly es?: string;
5
+ readonly fr?: string;
6
+ readonly ja?: string;
7
+ readonly 'pt-BR'?: string;
8
+ readonly ru?: string;
9
+ readonly 'zh-CN'?: string;
10
+ };
11
+
12
+ export interface CityRecord {
13
+ readonly confidence?: number;
14
+ readonly geoname_id: number;
15
+ readonly names: Names;
16
+ }
17
+
18
+ export interface ContinentRecord {
19
+ readonly code: 'AF' | 'AN' | 'AS' | 'EU' | 'NA' | 'OC' | 'SA';
20
+ readonly geoname_id: number;
21
+ readonly names: Names;
22
+ }
23
+
24
+ export interface RegisteredCountryRecord {
25
+ readonly geoname_id: number;
26
+ readonly is_in_european_union?: boolean;
27
+ readonly iso_code: string;
28
+ readonly names: Names;
29
+ }
30
+
31
+ export interface CountryRecord extends RegisteredCountryRecord {
32
+ readonly confidence?: number;
33
+ }
34
+
35
+ export interface LocationRecord {
36
+ readonly accuracy_radius: number;
37
+ readonly average_income?: number;
38
+ readonly latitude: number;
39
+ readonly longitude: number;
40
+ readonly metro_code?: number;
41
+ readonly population_density?: number;
42
+ readonly time_zone?: string;
43
+ }
44
+
45
+ export interface PostalRecord {
46
+ readonly code: string;
47
+ readonly confidence?: number;
48
+ }
49
+
50
+ export interface RepresentedCountryRecord extends RegisteredCountryRecord {
51
+ readonly type: string;
52
+ }
53
+
54
+ export interface SubdivisionsRecord {
55
+ readonly confidence?: number;
56
+ readonly geoname_id: number;
57
+ readonly iso_code: string;
58
+ readonly names: Names;
59
+ }
60
+
61
+ export interface TraitsRecord {
62
+ readonly autonomous_system_number?: number;
63
+ readonly autonomous_system_organization?: string;
64
+ readonly connection_type?: string;
65
+ readonly domain?: string;
66
+ ip_address?: string;
67
+ readonly is_anonymous?: boolean;
68
+ readonly is_anonymous_proxy?: boolean;
69
+ readonly is_anonymous_vpn?: boolean;
70
+ readonly is_anycast?: boolean;
71
+ readonly is_hosting_provider?: boolean;
72
+ readonly is_legitimate_proxy?: boolean;
73
+ readonly is_public_proxy?: boolean;
74
+ readonly is_residential_proxy?: boolean;
75
+ readonly is_satellite_provider?: boolean;
76
+ readonly is_tor_exit_node?: boolean;
77
+ readonly isp?: string;
78
+ readonly mobile_country_code?: string;
79
+ readonly mobile_network_code?: string;
80
+ readonly organization?: string;
81
+ readonly static_ip_score?: number;
82
+ readonly user_count?: number;
83
+ readonly user_type?: string;
84
+ }
85
+
86
+ export interface CountryResponse {
87
+ readonly continent?: ContinentRecord;
88
+ readonly country?: CountryRecord;
89
+ readonly registered_country?: RegisteredCountryRecord;
90
+ readonly represented_country?: RepresentedCountryRecord;
91
+ readonly traits?: TraitsRecord;
92
+ }
93
+
94
+ export interface CityResponse extends CountryResponse {
95
+ readonly city?: CityRecord;
96
+ readonly location?: LocationRecord;
97
+ readonly postal?: PostalRecord;
98
+ readonly subdivisions?: SubdivisionsRecord[];
99
+ }
100
+
101
+ export interface AnonymousIPResponse {
102
+ ip_address?: string;
103
+ readonly is_anonymous?: boolean;
104
+ readonly is_anonymous_proxy?: boolean;
105
+ readonly is_anonymous_vpn?: boolean;
106
+ readonly is_hosting_provider?: boolean;
107
+ readonly is_public_proxy?: boolean;
108
+ readonly is_residential_proxy?: boolean;
109
+ readonly is_tor_exit_node?: boolean;
110
+ }
111
+
112
+ export interface AnonymousPlusResponse extends AnonymousIPResponse {
113
+ readonly anonymizer_confidence?: number;
114
+ readonly network_last_seen?: string;
115
+ readonly provider_name?: string;
116
+ }
117
+
118
+ export interface AsnResponse {
119
+ readonly autonomous_system_number: number;
120
+ readonly autonomous_system_organization: string;
121
+ ip_address?: string;
122
+ }
123
+
124
+ export interface ConnectionTypeResponse {
125
+ readonly connection_type: string;
126
+ ip_address?: string;
127
+ }
128
+
129
+ export interface DomainResponse {
130
+ readonly domain: string;
131
+ ip_address?: string;
132
+ }
133
+
134
+ export interface IspResponse extends AsnResponse {
135
+ readonly isp: string;
136
+ readonly mobile_country_code?: string;
137
+ readonly mobile_network_code?: string;
138
+ readonly organization: string;
139
+ }
140
+
141
+ export type Response =
142
+ | CountryResponse
143
+ | CityResponse
144
+ | AnonymousIPResponse
145
+ | AnonymousPlusResponse
146
+ | AsnResponse
147
+ | ConnectionTypeResponse
148
+ | DomainResponse
149
+ | IspResponse;
150
+
151
+ export interface Metadata {
152
+ readonly binaryFormatMajorVersion: number;
153
+ readonly binaryFormatMinorVersion: number;
154
+ readonly buildEpoch: Date;
155
+ readonly databaseType: string;
156
+ readonly languages: string[];
157
+ readonly description: Record<string, string>;
158
+ readonly ipVersion: number;
159
+ readonly nodeCount: number;
160
+ readonly recordSize: number;
161
+ readonly nodeByteSize: number;
162
+ readonly searchTreeSize: number;
163
+ readonly treeDepth: number;
164
+ }
165
+
166
+ export interface OpenOpts {
167
+ cache?: false | {
168
+ max: number;
169
+ };
170
+ watchForUpdates?: boolean;
171
+ watchForUpdatesNonPersistent?: boolean;
172
+ watchForUpdatesHook?: () => void;
173
+ mode?: typeof MODE_AUTO | typeof MODE_MMAP | typeof MODE_MEMORY | typeof MODE_BUFFER;
174
+ }
175
+
176
+ export interface NetworkIterationOptions {
177
+ includeAliasedNetworks?: boolean;
178
+ includeNetworksWithoutData?: boolean;
179
+ skipEmptyValues?: boolean;
180
+ }
181
+
182
+ export interface NetworkPagesOptions extends NetworkIterationOptions {
183
+ pageSize?: number;
184
+ }
185
+
186
+ export interface CacheStats {
187
+ readonly enabled: boolean;
188
+ readonly size: number;
189
+ readonly capacity: number;
190
+ readonly hits: number;
191
+ readonly misses: number;
192
+ readonly inserts: number;
193
+ readonly evictions: number;
194
+ }
195
+
196
+ export declare class PathLookup<TValue = unknown> {
197
+ readonly path: ReadonlyArray<string | number>;
198
+ get(ipAddress: string): TValue | null;
199
+ getMany(ipAddresses: ReadonlyArray<string>): Array<TValue | null>;
200
+ }
201
+
202
+ export declare class NetworkIterator<T extends Response = Response>
203
+ implements IterableIterator<[string, T | null]> {
204
+ next(): IteratorResult<[string, T | null]>;
205
+ nextPage(pageSize?: number): Array<[string, T | null]>;
206
+ pages(pageSize?: number): IterableIterator<Array<[string, T | null]>>;
207
+ close(): void;
208
+ [Symbol.iterator](): IterableIterator<[string, T | null]>;
209
+ }
210
+
211
+ export declare const MODE_AUTO: 'auto';
212
+ export declare const MODE_MMAP: 'mmap';
213
+ export declare const MODE_MEMORY: 'memory';
214
+ export declare const MODE_BUFFER: 'buffer';
215
+
216
+ export declare class Reader<T extends Response = Response> {
217
+ constructor(database: Buffer, options?: OpenOpts);
218
+ readonly closed: boolean;
219
+ readonly lastReloadError: Error | null;
220
+ metadata: Metadata;
221
+ load(database: Buffer): void;
222
+ reload(): void;
223
+ close(): void;
224
+ clearCache(): void;
225
+ cacheStats(): CacheStats;
226
+ get(ipAddress: string): T | null;
227
+ getPath(ipAddress: string, path: ReadonlyArray<string | number>): unknown;
228
+ path<TValue = unknown>(
229
+ path: ReadonlyArray<string | number>,
230
+ ): PathLookup<TValue>;
231
+ getWithPrefixLength(ipAddress: string): [T | null, number];
232
+ getMany(ipAddresses: ReadonlyArray<string>): Array<T | null>;
233
+ getManyPath(
234
+ ipAddresses: ReadonlyArray<string>,
235
+ path: ReadonlyArray<string | number>,
236
+ ): unknown[];
237
+ networks(options?: NetworkPagesOptions): NetworkIterator<T>;
238
+ within(cidr: string, options?: NetworkPagesOptions): NetworkIterator<T>;
239
+ networkPages(
240
+ options?: NetworkPagesOptions,
241
+ ): IterableIterator<Array<[string, T | null]>>;
242
+ withinPages(
243
+ cidr: string,
244
+ options?: NetworkPagesOptions,
245
+ ): IterableIterator<Array<[string, T | null]>>;
246
+ }
247
+
248
+ export declare function open<T extends Response = Response>(
249
+ filepath: string,
250
+ options?: OpenOpts,
251
+ ): Promise<Reader<T>>;
252
+
253
+ export declare function openSync(): never;
254
+ export declare function init(): never;
255
+ export declare function validate(ipAddress: string): boolean;
256
+ export declare function nativeVersion(): string;
257
+
258
+ declare const maxmind: {
259
+ PathLookup: typeof PathLookup;
260
+ Reader: typeof Reader;
261
+ open: typeof open;
262
+ openSync: typeof openSync;
263
+ init: typeof init;
264
+ validate: typeof validate;
265
+ nativeVersion: typeof nativeVersion;
266
+ MODE_AUTO: typeof MODE_AUTO;
267
+ MODE_MMAP: typeof MODE_MMAP;
268
+ MODE_MEMORY: typeof MODE_MEMORY;
269
+ MODE_BUFFER: typeof MODE_BUFFER;
270
+ };
271
+
272
+ export default maxmind;
Binary file
Binary file
package/index.js ADDED
@@ -0,0 +1,547 @@
1
+ 'use strict';
2
+
3
+ const assert = require('node:assert');
4
+ const fs = require('node:fs');
5
+ const net = require('node:net');
6
+ const { join } = require('node:path');
7
+
8
+ const platformTriples = {
9
+ linux: {
10
+ x64: ['linux-x64-gnu', 'linux-x64-musl'],
11
+ arm64: ['linux-arm64-gnu', 'linux-arm64-musl'],
12
+ arm: ['linux-arm-gnueabihf', 'linux-arm-musleabihf'],
13
+ ppc64: ['linux-ppc64-gnu'],
14
+ s390x: ['linux-s390x-gnu'],
15
+ riscv64: ['linux-riscv64-gnu'],
16
+ },
17
+ darwin: {
18
+ x64: ['darwin-x64'],
19
+ arm64: ['darwin-arm64'],
20
+ universal: ['darwin-universal'],
21
+ },
22
+ win32: {
23
+ x64: ['win32-x64-msvc', 'win32-x64-gnu'],
24
+ arm64: ['win32-arm64-msvc'],
25
+ ia32: ['win32-ia32-msvc'],
26
+ },
27
+ freebsd: {
28
+ x64: ['freebsd-x64'],
29
+ },
30
+ };
31
+
32
+ function nativeCandidates() {
33
+ const triples = platformTriples[process.platform]?.[process.arch] ?? [];
34
+ return [
35
+ ...triples.map((triple) => `index.${triple}.node`),
36
+ 'index.node',
37
+ ].map((name) => join(__dirname, name));
38
+ }
39
+
40
+ function loadNativeBinding() {
41
+ const attempted = [];
42
+ for (const candidate of nativeCandidates()) {
43
+ attempted.push(candidate);
44
+ if (fs.existsSync(candidate)) {
45
+ return require(candidate);
46
+ }
47
+ }
48
+
49
+ const error = new Error(
50
+ `Unable to load maxminddb native binding. Tried:\n${attempted.join('\n')}`
51
+ );
52
+ error.code = 'ERR_MAXMINDDB_NATIVE_BINDING_NOT_FOUND';
53
+ throw error;
54
+ }
55
+
56
+ const native = loadNativeBinding();
57
+
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
+ const DEFAULT_CACHE_MAX = 10_000;
64
+ const MAX_CACHE_MAX = 0xffffffff;
65
+ const legacyErrorMessage = `Maxmind v2 module has changed API.
66
+ Please use:
67
+ maxmind.open(dbfile).then(function(lookup) {
68
+ lookup.get(ip);
69
+ });
70
+ `;
71
+
72
+ const MODE_AUTO = 'auto';
73
+ const MODE_MMAP = 'mmap';
74
+ const MODE_MEMORY = 'memory';
75
+ const MODE_BUFFER = 'buffer';
76
+
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
+ function normalizeMode(mode) {
108
+ if (mode == null || mode === MODE_AUTO) {
109
+ return MODE_MMAP;
110
+ }
111
+ if (mode === MODE_MMAP || mode === MODE_MEMORY || mode === MODE_BUFFER) {
112
+ return mode;
113
+ }
114
+ throw new Error(`Unsupported open mode: ${mode}`);
115
+ }
116
+
117
+ function normalizeMetadata(metadata) {
118
+ return {
119
+ ...metadata,
120
+ buildEpoch: new Date(Number(metadata.buildEpoch) * 1000),
121
+ };
122
+ }
123
+
124
+ function normalizeCacheCapacity(options = {}) {
125
+ if (options.cache === false) {
126
+ return 0;
127
+ }
128
+
129
+ const max = options.cache?.max ?? DEFAULT_CACHE_MAX;
130
+ if (!Number.isSafeInteger(max) || max <= 0 || max > MAX_CACHE_MAX) {
131
+ throw new Error('opts.cache.max should be a positive 32-bit integer');
132
+ }
133
+ return max;
134
+ }
135
+
136
+ function normalizeNetworkOptions(options = {}) {
137
+ return [
138
+ Boolean(options.includeAliasedNetworks),
139
+ Boolean(options.includeNetworksWithoutData),
140
+ Boolean(options.skipEmptyValues),
141
+ ];
142
+ }
143
+
144
+ function normalizeNetworkPageSize(value = 1000) {
145
+ if (!Number.isSafeInteger(value) || value <= 0 || value > MAX_CACHE_MAX) {
146
+ throw new Error('page size should be a positive 32-bit integer');
147
+ }
148
+ return value;
149
+ }
150
+
151
+ function normalizeNetworkIteratorOptions(options = {}) {
152
+ return [
153
+ normalizeNetworkOptions(options),
154
+ normalizeNetworkPageSize(options.pageSize ?? 1000),
155
+ ];
156
+ }
157
+
158
+ function waitForFile(filepath) {
159
+ for (let i = 0; i < 3; i++) {
160
+ if (fs.existsSync(filepath)) {
161
+ return Promise.resolve(true);
162
+ }
163
+ }
164
+
165
+ return new Promise((resolve) => {
166
+ let attempts = 0;
167
+ const retry = () => {
168
+ attempts += 1;
169
+ if (fs.existsSync(filepath)) {
170
+ resolve(true);
171
+ } else if (attempts >= 3) {
172
+ resolve(false);
173
+ } else {
174
+ setTimeout(retry, 500);
175
+ }
176
+ };
177
+ retry();
178
+ });
179
+ }
180
+
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
+ class PathLookup {
239
+ constructor(reader, path) {
240
+ this._reader = reader;
241
+ this._pathId = reader._reader.compilePath(path);
242
+ this.path = Object.freeze([...path]);
243
+ }
244
+
245
+ get(ipAddress) {
246
+ return this._reader._reader.getCompiledPath(ipAddress, this._pathId);
247
+ }
248
+
249
+ getMany(ipAddresses) {
250
+ return this._reader._reader.getManyCompiledPath(ipAddresses, this._pathId);
251
+ }
252
+ }
253
+
254
+ class NetworkIterator {
255
+ constructor(reader, cidr, options = {}) {
256
+ const [networkOptions, pageSize] = normalizeNetworkIteratorOptions(options);
257
+ this._cursor = reader._reader.networkCursor(cidr, ...networkOptions);
258
+ this._pageSize = pageSize;
259
+ this._page = [];
260
+ this._index = 0;
261
+ this._done = false;
262
+ }
263
+
264
+ [Symbol.iterator]() {
265
+ return this;
266
+ }
267
+
268
+ next() {
269
+ if (this._done) {
270
+ return { done: true, value: undefined };
271
+ }
272
+
273
+ if (this._index >= this._page.length) {
274
+ this._page = this._cursor.nextPage(this._pageSize);
275
+ this._index = 0;
276
+ if (this._page.length === 0) {
277
+ this.close();
278
+ return { done: true, value: undefined };
279
+ }
280
+ }
281
+
282
+ const value = this._page[this._index];
283
+ this._index += 1;
284
+ return { done: false, value };
285
+ }
286
+
287
+ nextPage(pageSize = this._pageSize) {
288
+ pageSize = normalizeNetworkPageSize(pageSize);
289
+ if (this._done) {
290
+ return [];
291
+ }
292
+
293
+ const page = [];
294
+ while (page.length < pageSize && this._index < this._page.length) {
295
+ page.push(this._page[this._index]);
296
+ this._index += 1;
297
+ }
298
+
299
+ if (page.length < pageSize) {
300
+ const nativePage = this._cursor.nextPage(pageSize - page.length);
301
+ page.push(...nativePage);
302
+ if (nativePage.length === 0) {
303
+ this.close();
304
+ }
305
+ }
306
+
307
+ return page;
308
+ }
309
+
310
+ *pages(pageSize = this._pageSize) {
311
+ pageSize = normalizeNetworkPageSize(pageSize);
312
+ while (true) {
313
+ const page = this.nextPage(pageSize);
314
+ if (page.length === 0) {
315
+ return;
316
+ }
317
+ yield page;
318
+ }
319
+ }
320
+
321
+ close() {
322
+ if (!this._done) {
323
+ this._cursor.close();
324
+ this._done = true;
325
+ this._page = [];
326
+ this._index = 0;
327
+ }
328
+ }
329
+ }
330
+
331
+ class Reader {
332
+ constructor(database, options = {}) {
333
+ if (!Buffer.isBuffer(database)) {
334
+ throw new Error(`maxminddb expects an instance of Buffer, got: ${typeof database}`);
335
+ }
336
+ this._mode = MODE_BUFFER;
337
+ this._filepath = null;
338
+ this._watchFilepath = null;
339
+ this._watchListener = null;
340
+ this._watchReloadPromise = Promise.resolve();
341
+ this._lastReloadError = null;
342
+ this._cacheCapacity = normalizeCacheCapacity(options);
343
+ this._reader = new native.NativeReader(database, this._cacheCapacity);
344
+ this.metadata = normalizeMetadata(this._reader.metadata());
345
+ this.options = options;
346
+ }
347
+
348
+ static open(filepath, options = {}) {
349
+ const mode = normalizeMode(options.mode);
350
+ const reader = Object.create(Reader.prototype);
351
+ reader._mode = mode;
352
+ reader._filepath = filepath;
353
+ reader._watchFilepath = null;
354
+ reader._watchListener = null;
355
+ reader._watchReloadPromise = Promise.resolve();
356
+ reader._lastReloadError = null;
357
+ reader._cacheCapacity = normalizeCacheCapacity(options);
358
+ reader._reader = native.openReader(filepath, mode, reader._cacheCapacity);
359
+ reader.metadata = normalizeMetadata(reader._reader.metadata());
360
+ reader.options = options;
361
+ return reader;
362
+ }
363
+
364
+ get closed() {
365
+ return this._reader.closed;
366
+ }
367
+
368
+ get lastReloadError() {
369
+ return this._lastReloadError;
370
+ }
371
+
372
+ load(database) {
373
+ try {
374
+ this._reader.load(database);
375
+ this.metadata = normalizeMetadata(this._reader.metadata());
376
+ this._lastReloadError = null;
377
+ } catch (error) {
378
+ this._lastReloadError = error;
379
+ throw error;
380
+ }
381
+ }
382
+
383
+ reload() {
384
+ if (!this._filepath) {
385
+ throw new Error('Cannot reload a buffer-backed Reader');
386
+ }
387
+ try {
388
+ this._reader.reloadFromFile(this._filepath, this._mode);
389
+ this.metadata = normalizeMetadata(this._reader.metadata());
390
+ this._lastReloadError = null;
391
+ } catch (error) {
392
+ this._lastReloadError = error;
393
+ throw error;
394
+ }
395
+ }
396
+
397
+ close() {
398
+ if (this._watchFilepath && this._watchListener) {
399
+ fs.unwatchFile(this._watchFilepath, this._watchListener);
400
+ this._watchFilepath = null;
401
+ this._watchListener = null;
402
+ }
403
+ this._reader.close();
404
+ }
405
+
406
+ _queueWatchedReload(filepath, mode, hook) {
407
+ const reload = () => this._reloadWatchedFile(filepath, mode, hook);
408
+ this._watchReloadPromise = this._watchReloadPromise.then(reload, reload);
409
+ }
410
+
411
+ async _reloadWatchedFile(filepath, mode, hook) {
412
+ if (!(await waitForFile(filepath))) {
413
+ return;
414
+ }
415
+ if (this.closed || this._watchFilepath !== filepath) {
416
+ return;
417
+ }
418
+
419
+ 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();
428
+ }
429
+ if (!this.closed && this._watchFilepath === filepath && hook) {
430
+ hook();
431
+ }
432
+ } catch (error) {
433
+ this._lastReloadError = error;
434
+ }
435
+ }
436
+
437
+ clearCache() {
438
+ this._reader.clearCache();
439
+ }
440
+
441
+ cacheStats() {
442
+ return this._reader.cacheStats();
443
+ }
444
+
445
+ get(ipAddress) {
446
+ return this._reader.get(ipAddress);
447
+ }
448
+
449
+ getPath(ipAddress, path) {
450
+ return this._reader.getPath(ipAddress, path);
451
+ }
452
+
453
+ path(path) {
454
+ return new PathLookup(this, path);
455
+ }
456
+
457
+ getWithPrefixLength(ipAddress) {
458
+ return this._reader.getWithPrefixLength(ipAddress);
459
+ }
460
+
461
+ getMany(ipAddresses) {
462
+ return this._reader.getMany(ipAddresses);
463
+ }
464
+
465
+ getManyPath(ipAddresses, path) {
466
+ return this._reader.getManyPath(ipAddresses, path);
467
+ }
468
+
469
+ networks(options = {}) {
470
+ return new NetworkIterator(this, null, options);
471
+ }
472
+
473
+ within(cidr, options = {}) {
474
+ return new NetworkIterator(this, cidr, options);
475
+ }
476
+
477
+ *networkPages(options = {}) {
478
+ yield* this.networks(options).pages(options.pageSize ?? 1000);
479
+ }
480
+
481
+ *withinPages(cidr, options = {}) {
482
+ yield* this.within(cidr, options).pages(options.pageSize ?? 1000);
483
+ }
484
+ }
485
+
486
+ async function open(filepath, opts, cb) {
487
+ assert(!cb, legacyErrorMessage);
488
+ const options = opts || {};
489
+ await assertNotGzipFile(filepath);
490
+
491
+ const mode = normalizeMode(options.mode);
492
+ const reader =
493
+ mode === MODE_BUFFER
494
+ ? new Reader(await readFile(filepath), options)
495
+ : Reader.open(filepath, options);
496
+
497
+ if (options.watchForUpdates) {
498
+ if (
499
+ options.watchForUpdatesHook &&
500
+ typeof options.watchForUpdatesHook !== 'function'
501
+ ) {
502
+ throw new Error('opts.watchForUpdatesHook should be a function');
503
+ }
504
+
505
+ const watcherOptions = {
506
+ persistent: options.watchForUpdatesNonPersistent !== true,
507
+ };
508
+
509
+ const watchListener = () => {
510
+ reader._queueWatchedReload(filepath, mode, options.watchForUpdatesHook);
511
+ };
512
+
513
+ fs.watchFile(filepath, watcherOptions, watchListener);
514
+ reader._watchFilepath = filepath;
515
+ reader._watchListener = watchListener;
516
+ }
517
+
518
+ return reader;
519
+ }
520
+
521
+ function init() {
522
+ throw new Error(legacyErrorMessage);
523
+ }
524
+
525
+ function openSync() {
526
+ throw new Error(legacyErrorMessage);
527
+ }
528
+
529
+ function validate(ipAddress) {
530
+ const version = net.isIP(ipAddress);
531
+ return version === 4 || version === 6;
532
+ }
533
+
534
+ module.exports = {
535
+ ...native,
536
+ NetworkIterator,
537
+ PathLookup,
538
+ Reader,
539
+ init,
540
+ open,
541
+ openSync,
542
+ validate,
543
+ MODE_AUTO,
544
+ MODE_MMAP,
545
+ MODE_MEMORY,
546
+ MODE_BUFFER,
547
+ };
Binary file
Binary file
Binary file
Binary file
package/package.json ADDED
@@ -0,0 +1,61 @@
1
+ {
2
+ "name": "@oschwald/maxminddb",
3
+ "version": "0.1.0",
4
+ "description": "Rust-backed Node.js reader for MaxMind DB files",
5
+ "main": "index.js",
6
+ "types": "index.d.ts",
7
+ "exports": {
8
+ ".": {
9
+ "types": "./index.d.ts",
10
+ "require": "./index.js",
11
+ "default": "./index.js"
12
+ },
13
+ "./package.json": "./package.json"
14
+ },
15
+ "files": [
16
+ "index.js",
17
+ "index.d.ts",
18
+ "CHANGELOG.md",
19
+ "LICENSE",
20
+ "RELEASE.md",
21
+ "*.node"
22
+ ],
23
+ "repository": {
24
+ "type": "git",
25
+ "url": "git+https://github.com/oschwald/maxminddb-node.git"
26
+ },
27
+ "bugs": {
28
+ "url": "https://github.com/oschwald/maxminddb-node/issues"
29
+ },
30
+ "homepage": "https://github.com/oschwald/maxminddb-node#readme",
31
+ "author": "Gregory J. Oschwald <oschwald@gmail.com>",
32
+ "publishConfig": {
33
+ "access": "public",
34
+ "registry": "https://registry.npmjs.org/",
35
+ "provenance": true
36
+ },
37
+ "scripts": {
38
+ "build": "napi build --platform --release --no-js --dts native.d.ts",
39
+ "build:debug": "napi build --platform --no-js --dts native.d.ts",
40
+ "bench": "node --expose-gc bench/lookup.js",
41
+ "release": "dev-bin/release.sh",
42
+ "test": "node --test test/*.test.js",
43
+ "test:pack": "node test/pack-smoke.js",
44
+ "typecheck": "tsc --noEmit"
45
+ },
46
+ "keywords": [
47
+ "maxmind",
48
+ "mmdb",
49
+ "geoip",
50
+ "geoip2"
51
+ ],
52
+ "license": "ISC",
53
+ "devDependencies": {
54
+ "@napi-rs/cli": "^3.7.2",
55
+ "@types/node": "^25.9.3",
56
+ "typescript": "^5.9.2"
57
+ },
58
+ "engines": {
59
+ "node": ">=20"
60
+ }
61
+ }