plevinjs 0.1.10 β†’ 0.2.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/README.md CHANGED
@@ -7,124 +7,113 @@
7
7
  </a>
8
8
 
9
9
  **Location, network and abuse information for any IP address in one offline file.**<br>
10
- No API, no rate limit, no lookup leaving the machine, or the browser tab.
10
+ No API, no rate limit, no lookup leaving the machine or the browser tab.
11
11
 
12
12
  [![npm](https://img.shields.io/npm/v/plevinjs?color=1868f2)](https://www.npmjs.com/package/plevinjs)
13
13
  [![Types](https://img.shields.io/badge/types-included-1868f2)](https://www.npmjs.com/package/plevinjs?activeTab=code)
14
14
  [![License](https://img.shields.io/badge/license-Apache--2.0-1868f2)](https://github.com/tn3w/plevin/blob/master/LICENSE)
15
- [![Fields](https://img.shields.io/badge/fields-99-6f42c1)](#every-field)
16
- [![Boundaries](https://img.shields.io/badge/boundaries-3.0M-6f42c1)](https://github.com/tn3w/plevin/blob/master/README.md#data)
17
- [![Warm](https://img.shields.io/badge/warm%20lookups-4M%2Fs-2ea043)](#speed)
15
+ [![Fields](https://img.shields.io/badge/fields-101-6f42c1)](#fields)
16
+ [![Warm](https://img.shields.io/badge/warm%20lookups-5M%2Fs-2ea043)](#speed)
18
17
 
19
18
  </div>
20
19
 
20
+ ## Quick start
21
+
21
22
  ```bash
22
23
  npm install plevinjs
23
24
  ```
24
25
 
25
26
  ```js
26
- import { Plevin, open } from "plevinjs";
27
+ import { open } from "plevinjs";
27
28
 
28
29
  const db = await open("https://plevin.tn3w.dev/db/plevin.plv");
29
- const found = db.lookup("1.1.1.1"); // string, number, bigint or packed bytes
30
- ```
31
-
32
- Always a `Result`, never `undefined`; it throws for anything that is not an address, and
33
- a number reads as v6 only above `0xffffffff`, so `lookup(1)` is `0.0.0.1`.
30
+ const found = db.lookup("1.1.1.1"); // string, number, bigint or bytes
34
31
 
35
- ```js
36
32
  found.place.city.name; // 'Brisbane'
37
- found.place.city.region.name; // 'Queensland'
38
- found.place.country.name; // 'Australia'
39
33
  found.place.time.local; // '2026-08-13T23:27:58+10:00'
40
-
41
- found.network.asn; // 13335
42
34
  found.network.operator.brand; // 'Cloudflare'
43
35
  found.network.cidr; // '1.1.1.0/24'
44
-
45
- const exit = db.lookup("185.220.101.1");
46
- exit.abuse.service; // 'tor_exit_node'
47
- exit.abuse.risk; // 0.98
48
- exit.abuse.is_tor_exit_node; // true
36
+ db.lookup("185.220.101.1").abuse.service; // 'tor_exit_node'
49
37
  ```
50
38
 
51
- Pure ESM with no dependencies, so it runs wherever fetch does: Node, Deno, Bun,
52
- Cloudflare Workers, and any browser off a CDN.
39
+ - `lookup` always answers a `Result` and throws for non-addresses.
40
+ - Numbers up to `0xffffffff` are v4, so `lookup(1)` is `0.0.0.1`.
41
+ - Pure ESM, zero dependencies: Node, Deno, Bun, Cloudflare Workers, browsers.
53
42
 
54
- ```html
55
- <script type="module">
56
- import { open } from "https://cdn.jsdelivr.net/npm/plevinjs";
57
-
58
- const db = await open("https://plevin.tn3w.dev/db/plevin.place-country-code.plv");
59
- const { flag, name } = db.lookup("8.8.8.8").place.country;
60
- document.body.textContent = `${flag} ${name}`; // 'πŸ‡ΊπŸ‡Έ United States'
61
- </script>
62
- ```
43
+ ## Opening a database
63
44
 
64
- That build is 390 KB, the country code and nothing else, and the name and flag are
65
- derived in the reader. `https://esm.sh/plevinjs` serves the same thing. Every database is rehosted with open
66
- CORS at [plevin.tn3w.dev/db](https://plevin.tn3w.dev/db/), because GitHub release
67
- downloads send no CORS header.
45
+ | where | how |
46
+ | --------------- | ---------------------------------------------------------- |
47
+ | browser, worker | `await open(url)` or `await open(response)` |
48
+ | Node, Deno, Bun | `openFile(path)` from `plevinjs/node`; `PLEVIN_DB` if no path |
49
+ | bytes in hand | `new Plevin(uint8Array)` |
68
50
 
69
- ## Where the file comes from
51
+ Open once, reuse for every lookup. Nothing is downloaded or cached for you.
70
52
 
71
- | where it runs | how to open it |
72
- | --- | --- |
73
- | browser, worker | `await open(url)`, or `await open(response)` |
74
- | Node, Deno, Bun | `import { openFile } from "plevinjs/node"`, then `await openFile(path)` |
75
- | bytes you hold | `new Plevin(bytes)`, taking a `Uint8Array` |
53
+ | file | size | carries |
54
+ | ----------------------------------------------------- | ------- | ---------------------------------------- |
55
+ | `plevin.plv` | 16.4 MB | every field |
56
+ | `plevin.metro-place.plv` | 5.1 MB | city, region, postal, coordinates, metro |
57
+ | `plevin.network.plv` | 7.0 MB | ASN, operator, routing |
58
+ | `plevin.abuse-level-abuse-provider-abuse-service.plv` | 3.8 MB | abuse level, service and provider |
59
+ | `plevin.place-country-code.plv` | 376 KB | country code |
76
60
 
77
- `openFile()` reads `PLEVIN_DB` where no path is given. Nothing is downloaded for you
78
- and nothing is cached for you: hand the same `Plevin` to every lookup and the file is
79
- read once.
61
+ All are served with open CORS from [plevin.tn3w.dev/db](https://plevin.tn3w.dev/db/);
62
+ GitHub release downloads send no CORS header.
80
63
 
81
- | file | size | carries |
82
- | --- | --- | --- |
83
- | `plevin.plv` | 17.3 MB | every field |
84
- | `plevin.metro-place.plv` | 5.7 MB | city, region, postal, coordinates, metro |
85
- | `plevin.network.plv` | 7.3 MB | ASN, operator, routing |
86
- | `plevin.abuse-provider-abuse-service.plv` | 2.8 MB | abuse service and provider |
87
- | `plevin.place-country-code.plv` | 390 KB | the country code |
64
+ ## In a browser
88
65
 
89
- ## The same answers over HTTP
66
+ No build step:
90
67
 
91
- Where a file is one dependency too many, the reader runs on a Cloudflare Worker at
92
- [plevin.tn3w.dev/api](https://plevin.tn3w.dev/api/1.1.1.1) and returns the same JSON,
93
- field for field, with no key and CORS open to every origin.
68
+ ```html
69
+ <script type="module">
70
+ import { open } from "https://cdn.jsdelivr.net/npm/plevinjs";
94
71
 
95
- ```bash
96
- curl https://plevin.tn3w.dev/api/1.1.1.1 # any address
97
- curl https://plevin.tn3w.dev/api/me # the caller's own
98
- curl https://plevin.tn3w.dev/api/about # the build and its fields
72
+ const db = await open("https://plevin.tn3w.dev/db/plevin.place-country-code.plv");
73
+ const { flag, name } = db.lookup("8.8.8.8").place.country;
74
+ document.body.textContent = `${flag} ${name}`; // 'πŸ‡ΊπŸ‡Έ United States'
75
+ </script>
99
76
  ```
100
77
 
101
- [`worker/`](https://github.com/tn3w/plevin/blob/master/worker) is that worker, ready to
102
- run on an account of your own.
78
+ | URL | serves |
79
+ | ---------------------------------------------- | --------------------------------- |
80
+ | `https://cdn.jsdelivr.net/npm/plevinjs` | `plevin.min.js`, one 52 kB file |
81
+ | `https://unpkg.com/plevinjs` | the same |
82
+ | `https://esm.sh/plevinjs` | the modules, imports rewritten |
83
+ | `https://plevin.tn3w.dev/plevin/plevin.min.js` | the bundle beside the databases |
103
84
 
104
- ## Every field
85
+ - **Pin a version for production:** `cdn.jsdelivr.net/npm/plevinjs@0.2.0`.
86
+ - **Pick the smallest build:** the country build is 376 KB against 16.4 MB.
87
+ - **Cache the file** so it downloads once per visitor:
88
+
89
+ ```js
90
+ const store = await caches.open("plevin");
91
+ const url = "https://plevin.tn3w.dev/db/plevin.plv";
92
+ if (!(await store.match(url))) await store.add(url);
93
+ const db = new Plevin(new Uint8Array(await (await store.match(url)).arrayBuffer()));
94
+ ```
95
+
96
+ ## Fields
105
97
 
106
98
  <picture>
107
99
  <source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/tn3w/plevin/master/.github/fields-dark.png">
108
100
  <img src="https://raw.githubusercontent.com/tn3w/plevin/master/.github/fields-light.png" width="840" alt="address to place, network and abuse">
109
101
  </picture>
110
102
 
111
- `found.place` is where the address is, `found.network` who announces it, `found.abuse`
112
- what has been seen from it. Any of the three is `null` where the build carries none of
113
- it, and every leaf is `null` rather than `""` or `0` where a source says nothing. The
114
- shapes and the field names are the ones the
115
- [Python package](https://github.com/tn3w/plevin/blob/master/python/README.md#every-field)
116
- answers with, to the letter.
103
+ Identical to the
104
+ [Python package](https://github.com/tn3w/plevin/blob/master/python/README.md#fields),
105
+ field for field; see there for what each one means. A missing value is `null`, never
106
+ `""` or `0`.
117
107
 
118
108
  ```js
109
+ found.place;
119
110
  {
120
- lat: -27.4675, lon: 153.0281, accuracy: 200, confidence: 36,
121
- granularity: 'city',
111
+ lat: -27.4675, lon: 153.0281, accuracy: 200, confidence: 36, granularity: 'city',
122
112
  city: {
123
113
  id: 2174003, name: 'Brisbane', ascii: 'Brisbane', country: 'AU',
124
114
  population: 2780063, elevation: 27, postal: '4000', postal_partial: null,
125
115
  timezone: 'Australia/Brisbane', type: 'regional capital', capital: 'region',
126
- region: { id: 2152274, code: '04', iso: 'AU-QLD', name: 'Queensland',
127
- type: 'State' },
116
+ region: { id: 2152274, code: '04', iso: 'AU-QLD', name: 'Queensland', type: 'State' },
128
117
  district: { id: 7839562, code: '31000', name: 'Brisbane' },
129
118
  metro: null,
130
119
  },
@@ -138,14 +127,8 @@ answers with, to the letter.
138
127
  dst_start: null, dst_end: null,
139
128
  },
140
129
  }
141
- ```
142
-
143
- `country` and `time` are derived, not stored: `country` out of an ISO 3166 table in the
144
- package, `time` out of the runtime's own `Intl` zone data, so neither needs an install
145
- and neither is a network call. Where the host's zone data is older or newer than the
146
- one the Python package reads, a daylight boundary may move by a transition.
147
130
 
148
- ```js
131
+ found.network;
149
132
  {
150
133
  asn: 13335, handle: 'CLOUDFLARENET', prefix: 24, cidr: '1.1.1.0/24',
151
134
  start: '1.1.1.0', end: '1.1.1.255', rir: 'apnic', rpki: 'valid', roas: 1,
@@ -159,273 +142,151 @@ one the Python package reads, a daylight boundary may move by a transition.
159
142
  carrier: { user_type: 'hosting', user_count: 19, mcc: null, mnc: null,
160
143
  is_mobile: false },
161
144
  }
162
- ```
163
-
164
- `cidr` is the announcement the address falls in, masked out of the address itself.
165
- `rir` is the registry that holds the address rather than the one that registered the
166
- ASN. `rpki` is `valid`, `invalid` or `unknown` and `roas` how many ROAs agree. `brand`
167
- drops the legal form and the words every network carries, so `GOOGLE` and `Google LLC`
168
- both read `Google`.
169
145
 
170
- Where nothing is announced, the registries still answer: `asn`, `rpki` and `roas` fall
171
- silent, `cidr` becomes the block a registry gave out, and `handle` and `operator` name
172
- whoever holds it.
173
-
174
- ```js
175
- db.lookup("36.50.238.1").network;
176
- { asn: null, handle: 'GMTECH-BD', cidr: '36.50.238.0/23', rir: 'apnic',
177
- operator: { company: 'GM Tech', ... }, ... }
178
- ```
179
-
180
- ```js
181
146
  db.lookup("185.220.101.1").abuse;
182
147
  {
183
148
  name: 'Tor', provider: 'Tor', service: 'tor_exit_node', evidence: 'measured',
184
- risk: 0.98, network_risk: 0.82, last_seen_days: 1, is_anycast: false,
185
- is_satellite: false, is_hosting_provider: true, is_proxy: false,
186
- is_public_proxy: false, is_residential_proxy: false, is_anonymous_vpn: false,
187
- is_tor_exit_node: true, is_private_relay: false, is_anonymous: true,
149
+ level: 'high', risk: 0.99, network_risk: 0.86, last_seen_days: 1,
150
+ is_malicious: true, is_anycast: false, is_satellite: false,
151
+ is_hosting_provider: true, is_proxy: false, is_public_proxy: false,
152
+ is_residential_proxy: false, is_anonymous_vpn: false, is_tor_exit_node: true,
153
+ is_private_relay: false, is_anonymous: true,
188
154
  }
189
155
  ```
190
156
 
191
- `risk` is 0 to 1 for the address, `network_risk` the same for the whole ASN, `null`
192
- where nothing has ever been seen, so which is not a risk of zero. It is a total of what
193
- the service the address runs is worth on its own and what every feed that named it
194
- scored it, combined so each agreeing source raises the total and none of them replaces
195
- the rest, capped at 0.99. `provider` is who runs the service: the feed's `name` where
196
- one names it, else the brand of the network the address sits in, and `null` where the
197
- address runs no service at all.
157
+ JavaScript specifics:
198
158
 
199
- ## What an address says on its own
159
+ - `country` comes from an ISO 3166 table in the package and `time` from the runtime's
160
+ `Intl` data, so neither needs an install or a network call. A different zone
161
+ database can move a daylight-saving boundary.
162
+ - `number` is a `bigint` for v6, so `JSON.stringify` needs a replacer.
200
163
 
201
- Answered without the database, so they hold for every address:
164
+ Address fields (`compressed`, `expanded`, `arpa`, `is_*`, `tunnel`, `embedded_ipv4`,
165
+ `decimal_ipv4`, `as_*`) work without any database.
202
166
 
203
- ```js
204
- const found = db.lookup("2606:4700::1111");
205
- found.number; // 50543257672059871404715951523469725969n
206
- found.compressed // '2606:4700::1111'
207
- found.expanded; // '2606:4700:0000:0000:0000:0000:0000:1111'
208
- found.arpa; // '1.1.1.1.0.0.….0.7.4.6.0.6.2.ip6.arpa'
209
- ```
167
+ ## DNS
210
168
 
211
- `number` is a `bigint` for v6 and a `number` for v4, so `JSON.stringify` needs a
212
- replacer where you hand a v6 result on. Then `is_global` and `is_bogon`, `is_private`,
213
- `is_loopback`, `is_multicast`, `is_reserved`, `is_link_local`, `is_unique_local`,
214
- `is_documentation`, `is_shared` (100.64/10) and `is_benchmark` (198.18/15), bisected
215
- out of the IANA special-purpose registries.
169
+ `lookup` stays synchronous and offline. `resolve` adds DNS only when asked:
216
170
 
217
171
  ```js
218
- db.lookup("::ffff:8.8.8.8").tunnel; // 'ipv4-mapped'
219
- db.lookup("::ffff:8.8.8.8").embedded_ipv4; // '8.8.8.8'
220
- db.lookup("2002:808:808::1").is_6to4; // true
172
+ (await db.resolve("8.8.8.8", { dns: true })).dns;
173
+ {
174
+ asked: '8.8.8.8', hostname: 'dns.google', hostnames: ['dns.google'],
175
+ ipv4: '8.8.4.4', ipv6: '2001:4860:4860::8888',
176
+ ipv4_addresses: ['8.8.4.4', '8.8.8.8'],
177
+ ipv6_addresses: ['2001:4860:4860::8888', '2001:4860:4860::8844'],
178
+ alias: null, zone: '8.8.8.in-addr.arpa', zone_primary: 'ns1.google.com',
179
+ zone_contact: 'dns-admin@google.com', is_confirmed: true, is_signed: true,
180
+ }
221
181
  ```
222
182
 
223
- `tunnel` is `ipv4-mapped`, `6to4`, `teredo`, `nat64` or `null`.
183
+ - **Node, Deno, Bun:** raw DNS over UDP, raced across the system resolver plus
184
+ 1.1.1.1, 8.8.8.8 and 9.9.9.9, with TCP on truncation.
185
+ - **Browsers, workers:** DNS-over-HTTPS to Cloudflare and Google.
186
+ - **Cache:** answers are kept for one hour.
224
187
 
225
- ```js
226
- db.lookup("2001:67c:e60:c0c:192:42:116:55").decimal_ipv4; // '192.42.116.55'
227
- ```
228
-
229
- `decimal_ipv4` is a guess and never a tunnel: the last four hextets where an operator
230
- wrote a v4 address into them as decimal. It is `null` wherever a real tunnel answers.
188
+ ## Own address
231
189
 
232
190
  ```js
233
- db.lookup("8.8.8.8").as_ipv4_mapped; // '::ffff:8.8.8.8'
234
- db.lookup("8.8.8.8").as_6to4; // '2002:808:808::'
235
- db.lookup("8.8.8.8").as_nat64; // '64:ff9b::808:808'
236
- ```
191
+ import { publicAddress } from "plevinjs";
237
192
 
238
- `as_ipv4_mapped`, `as_6to4` and `as_nat64` write a v4 address the other way about, as
239
- the v6 addresses that carry it; all three are `null` for a v6 address, where
240
- `embedded_ipv4` already says what it carries.
241
-
242
- ## What DNS says, where you ask for it
243
-
244
- `lookup` never leaves the machine and stays synchronous. `resolve` is the same lookup
245
- with DNS behind a flag, and does nothing more than `lookup` unless the flag is set:
246
-
247
- ```js
248
- (await db.resolve("8.8.8.8", { dns: true })).dns;
249
- {
250
- asked: '8.8.8.8',
251
- hostname: 'dns.google',
252
- hostnames: [ 'dns.google' ],
253
- ipv4: '8.8.4.4',
254
- ipv6: '2001:4860:4860::8888',
255
- ipv4_addresses: [ '8.8.4.4', '8.8.8.8' ],
256
- ipv6_addresses: [ '2001:4860:4860::8888', '2001:4860:4860::8844' ],
257
- alias: null,
258
- zone: '8.8.8.in-addr.arpa',
259
- zone_primary: 'ns1.google.com',
260
- zone_contact: 'dns-admin@google.com',
261
- is_confirmed: true,
262
- is_signed: true,
263
- }
193
+ const own = await publicAddress(); // '203.0.113.42', or null
194
+ own && db.lookup(own);
264
195
  ```
265
196
 
266
- `hostname` is the first PTR name and `hostnames` all of them, `ipv4` and `ipv6` that
267
- name resolved forward with `ipv4_addresses` and `ipv6_addresses` all of those, so each
268
- address names its other half; `is_confirmed` says the name leads back to the address,
269
- which is forward-confirmed reverse DNS; `zone`, `zone_primary` and `zone_contact` come
270
- from the reverse zone's SOA, naming who runs the range; `is_signed` is the DNSSEC
271
- verdict and `alias` a CNAME in the way; `asked` is the address actually asked about,
272
- which for a tunnel is the v4 it carries. Four questions go out in two rounds, PTR and
273
- SOA on the reverse name together and then A and AAAA of the hostname: Node, Deno and
274
- Bun write those onto the wire themselves and send them to every server at once, so the
275
- machine's own from `node:dns` and 1.1.1.1, 8.8.8.8 and 9.9.9.9, so first real answer
276
- winning, TCP where one comes back truncated, while a browser or a worker, having no
277
- datagram, sends the same queries to Cloudflare and Google over DNS-over-HTTPS. Answers
278
- are kept for an hour, and nothing is asked where the flag is off, which keeps a bundled
279
- reader as offline as it was.
280
-
281
- ## One ASN, and the networks a name belongs to
197
+ - **How:** one STUN binding request, answered in tens of milliseconds.
198
+ - **Browsers:** go through `RTCPeerConnection` instead.
199
+ - **Fallback:** after 2 s, asks `api.ipify.org`, then `icanhazip.com`.
200
+
201
+ ## ASNs
282
202
 
283
203
  ```js
284
204
  const found = db.system("AS13335"); // 'AS13335', 'as13335' or 13335
285
- found.handle; // 'CLOUDFLARENET'
286
205
  found.network.operator.brand; // 'Cloudflare'
287
206
  found.abuse.network_risk; // 0.14
288
207
 
289
208
  db.search("hetzner").map((one) => one.asn);
290
209
  // [24940, 212317, 213230, 215859]
291
- ```
292
-
293
- `system()` answers a `System`, so the `asn`, the `handle`, the same `network` with its
294
- operator and carrier, and the ASN's own `abuse` record, without anything only an address
295
- fixes: no prefix, no CIDR, no RPKI, no place. `found` is `false` where the file carries
296
- no such ASN. It bisects the network table, so no spine is read and nothing about the
297
- address lookup changes.
298
210
 
299
- `search(text, limit = 20)` matches the text against every handle and every company in
300
- the file and answers the same `System`, widest network first: a match at the head of a
301
- word beats one inside it, and then the network that touches most of the internet wins,
302
- which is its exchanges and its users. `search("13335")` is the ASN itself. The index is
303
- one lowercase text built on the first search, 0.32 s, and kept from then on.
304
-
305
- ## What an ASN announces
306
-
307
- ```js
308
- const routes = db.routes("AS13335"); // written however system() takes it
309
- routes.ipv4.length; // 1506
310
- routes.ipv4[0].cidr; // '152.114.0.0/17', the widest first
311
- routes.ipv4[0].addresses; // 32768
312
- routes.ipv4_addresses; // 616704, the space of all of them
313
- routes.ipv6_addresses >> 64n; // 75037868032n, as /64 networks
211
+ const routes = db.routes("AS13335");
212
+ routes.ipv4.length; // 1411
213
+ routes.ipv4[0].cidr; // '152.114.0.0/17', widest first
214
+ routes.ipv6_addresses >> 64n; // space as /64 networks, a bigint
314
215
  ```
315
216
 
316
- `routes()` answers a `Routes`: every prefix the ASN is announced as, split into `ipv4`
317
- and `ipv6` and widest first, each one a `Span` with its `cidr`, `start`, `end`,
318
- `version`, `prefix` and `addresses`. `ipv4_addresses` is a number and `ipv6_addresses` a
319
- bigint, and both count the space once where a more specific sits inside its own cover.
320
- `found` is `false` where the file carries no such ASN.
217
+ | call | answers |
218
+ | ------------------------- | -------------------------------------------------------- |
219
+ | `system(asn)` | `System`: handle, network, operator, carrier, ASN abuse; `found` false if unknown |
220
+ | `search(text, limit=20)` | `System`s whose handle or company matches, best first |
221
+ | `routes(asn)` | `Routes`: every announced prefix as a `Span`, widest first |
321
222
 
322
- It reads the spine's network column whole, a block at a time, rather than a row at a
323
- time: 159 ms for the first ASN asked about and 2 to 9 ms for every one after it, each
324
- answer kept. Nothing an address lookup reads is touched.
325
-
326
- ## In a browser
223
+ ## HTTP
327
224
 
328
- No build step and no install: the package is plain ESM with no dependencies, so any npm
329
- CDN serves it as it is.
225
+ The same JSON without a file, CORS open, no key:
330
226
 
331
- ```html
332
- <script type="module">
333
- import { open } from "https://cdn.jsdelivr.net/npm/plevinjs";
334
-
335
- const db = await open(
336
- "https://plevin.tn3w.dev/db/plevin.place-country-code.plv"
337
- );
338
- const { flag, name } = db.lookup("1.1.1.1").place.country;
339
- document.body.textContent = `${flag} ${name}`; // 'πŸ‡ΊπŸ‡Έ United States'
340
- </script>
341
- ```
342
-
343
- Open the smallest build a page actually needs: `plevin.place-country-code.plv` is
344
- 390 KB against the 17.3 MB of `plevin.plv`, so the first lookup lands in a moment
345
- rather than a download.
346
-
347
- | | |
348
- | --- | --- |
349
- | `https://cdn.jsdelivr.net/npm/plevinjs` | `dist/plevin.min.js`, the whole reader in one file |
350
- | `https://unpkg.com/plevinjs` | the same as jsDelivr |
351
- | `https://esm.sh/plevinjs` | the modules as published, imports rewritten |
352
- | `https://plevin.tn3w.dev/plevin/plevin.min.js` | the reader beside the databases |
353
-
354
- jsDelivr and unpkg serve the bundle named by the `jsdelivr`/`unpkg` fields, 44 kB of
355
- JavaScript with no further requests. The bare `dist/index.js` is not usable from those
356
- URLs: it imports `./reader.js` and neighbours, which resolve against `/npm/` there and
357
- 404. Pin a version for anything that ships: `cdn.jsdelivr.net/npm/plevinjs@0.1.2`.
358
- `plevinjs/node` is the only entry that touches Node, so a CDN import never reaches for
359
- `node:fs`.
360
-
361
- 18 MB crosses the wire once, so keep it out of the critical path and out of the next
362
- visit's way:
363
-
364
- ```js
365
- const store = await caches.open("plevin");
366
- const url = "https://plevin.tn3w.dev/db/plevin.plv";
367
- if (!(await store.match(url))) await store.add(url);
368
-
369
- const held = await store.match(url);
370
- const db = new Plevin(new Uint8Array(await held.arrayBuffer()));
227
+ ```bash
228
+ curl https://plevin.tn3w.dev/api/1.1.1.1 # any address
229
+ curl https://plevin.tn3w.dev/api/me # the caller
230
+ curl https://plevin.tn3w.dev/api/about # build and fields
371
231
  ```
372
232
 
373
- `plevin.place-country-code.plv` is 390 KB where the country code is all a page needs.
374
- [The lookup page](https://plevin.tn3w.dev/) is the whole idea in one file of
375
- plain JavaScript: it reads the database in the tab and calls out only for the visitor's
376
- own address and for hostnames.
233
+ Self-host with [`worker/`](https://github.com/tn3w/plevin/blob/master/worker).
377
234
 
378
235
  ## Speed
379
236
 
380
- Measured on the full file, Node 26, one core:
237
+ Full file, Node 26, one core:
381
238
 
382
- | | |
383
- | --- | --- |
384
- | open | 14 ms, the header only |
385
- | first answer | 44 ms, the blocks it lands in |
386
- | repeats | 4,600,000/s |
387
- | uniformly random v4 | 13,000/s cold, 200,000/s over the same log again |
388
- | one ASN | 350,000/s, a bisect of the network table |
389
- | one ASN's prefixes | 159 ms for the first, 2 to 9 ms after, then kept |
390
- | search | 1 ms, after 0.32 s building the index |
239
+ | operation | speed |
240
+ | ------------ | -------------------------------------- |
241
+ | open | 9 ms, header only |
242
+ | first lookup | 70 ms |
243
+ | repeats | 5,000,000/s |
244
+ | random v4 | 8,000/s cold, 72,000/s warm |
245
+ | `system` | 1,000,000/s |
246
+ | `routes` | 100–160 ms, then cached |
247
+ | `search` | 1 ms, after 0.07 s building the index |
391
248
 
392
- Blocks decode on reach and stay decoded, so a real log lands between the two. Memory is
393
- the file plus whatever it decoded, around 120 MB of heap with the whole world touched.
249
+ Blocks decode lazily, only as far as a lookup reaches, and stay decoded: about 160 MB
250
+ of heap with the whole world touched.
394
251
 
395
- ## Zstandard
252
+ ## LZMA
396
253
 
397
- The file is Zstandard with trained dictionaries, which no runtime decompresses on its
398
- own, so `DecompressionStream` has no zstd and Node's `zlib` takes no dictionary. So the
399
- package carries one, condensed from [fzstd](https://github.com/101arrowz/fzstd) (MIT)
400
- with the dictionary support it leaves out, and verified block for block against
401
- libzstd over the whole database.
254
+ Blocks are raw LZMA1, which neither `DecompressionStream` nor `zlib` reads, so the
255
+ package ships its own decoder, checked against liblzma on every block.
402
256
 
403
257
  ```js
404
- import { decompress, loadDictionary } from "plevin/zstd";
258
+ import { decompress } from "plevinjs/lzma";
405
259
 
406
- decompress(frame); // one frame
407
- decompress(frame, loadDictionary(trained)); // with a trained dictionary
260
+ decompress(block, [3, 0, 0]); // tuning: literal context, literal position, match position bits
408
261
  ```
409
262
 
410
263
  ## Development
411
264
 
265
+ | module | job |
266
+ | ------------ | --------------------------------------------------- |
267
+ | `index.ts` | `Plevin`, `open`, result shaping |
268
+ | `reader.ts` | file format: sections, blocks, groups, bisection |
269
+ | `lzma.ts` | resumable LZMA1 decoder |
270
+ | `address.ts` | parsing, spelling, special ranges |
271
+ | `naming.ts` | DNS and own address |
272
+ | `extra.ts` | country and clock, from `countries.ts`/`zones.ts` |
273
+ | `derive.ts` | brand, domain, capital |
274
+ | `models.ts` | published types |
275
+ | `node.ts` | `openFile`, the only `node:fs` import |
276
+
412
277
  ```bash
413
- cd js
414
278
  npm ci
415
- npm test # node --test, no database needed for most of it
416
- npm run lint # biome
417
- npm run typecheck # tsc, strict
418
- npm run build # dist/, ESM and .d.ts, plus the CDN bundle
419
- npm run bundle # dist/plevin.min.js only (esbuild)
279
+ npm test && npm run lint && npm run typecheck
280
+ npm run build # dist/ and the CDN bundle
420
281
 
421
282
  node test/compare.ts ../plevin.plv sample.json # field for field against Python
422
- node test/blocks.ts ../plevin.plv 100000 # every block against libzstd
283
+ node test/blocks.ts ../plevin.plv 100000 # every block against liblzma
423
284
  ```
424
285
 
425
286
  ## License
426
287
 
427
288
  Apache 2.0, see [LICENSE](https://github.com/tn3w/plevin/blob/master/LICENSE). The
428
- database carries the licenses of the sources it was built from, listed in
429
- [`builder/README.md`](https://github.com/tn3w/plevin/blob/master/builder/README.md#sources).
289
+ database carries the licenses of its
290
+ [sources](https://github.com/tn3w/plevin/blob/master/builder/README.md#sources).
430
291
 
431
292
  <!-- brand: Noto Sans 800, wordmark bar #1868f2 place, #6f42c1 network, #2ea043 abuse; #7d8894 address, ink #0b1220 light, #f0f6fc dark -->
@@ -1 +1 @@
1
- {"version":3,"file":"address.d.ts","sourceRoot":"","sources":["../src/address.ts"],"names":[],"mappings":"AAAA,wDAAwD;AAExD,MAAM,MAAM,KAAK,GAAG,MAAM,GAAG,MAAM,GAAG,MAAM,GAAG,UAAU,CAAC;AAE1D,eAAO,MAAM,OAAO,IAAI,CAAC;AACzB,eAAO,MAAM,QAAQ,IAAI,CAAC;AAC1B,eAAO,MAAM,SAAS,IAAI,CAAC;AAC3B,eAAO,MAAM,QAAQ,IAAI,CAAC;AAC1B,eAAO,MAAM,UAAU,KAAK,CAAC;AAC7B,eAAO,MAAM,YAAY,KAAK,CAAC;AAC/B,eAAO,MAAM,aAAa,KAAK,CAAC;AAChC,eAAO,MAAM,MAAM,MAAM,CAAC;AAC1B,eAAO,MAAM,SAAS,MAAM,CAAC;AAE7B,eAAO,MAAM,MAAM,gBAAgB,CAAC;AACpC,eAAO,MAAM,SAAS,SAAS,CAAC;AAChC,eAAO,MAAM,MAAM,WAAW,CAAC;AAC/B,eAAO,MAAM,KAAK,UAAU,CAAC;AAuF7B,sFAAsF;AACtF,eAAO,MAAM,KAAK,UAAW,KAAK,KAAG,CAAC,MAAM,GAAG,MAAM,EAAE,OAAO,CA6B7D,CAAC;AA2BF,gFAAgF;AAChF,eAAO,MAAM,OAAO,UACX,MAAM,GAAG,MAAM,QAChB,OAAO,KACZ,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CA4BzB,CAAC;AAEF,+EAA+E;AAC/E,eAAO,MAAM,OAAO,UAAW,MAAM,GAAG,MAAM,QAAQ,OAAO,KAAG,MACvC,CAAC;AAc1B,kFAAkF;AAClF,eAAO,MAAM,OAAO,UAAW,MAAM,GAAG,MAAM,QAAQ,OAAO,KAAG,MAa/D,CAAC;AAEF,0EAA0E;AAC1E,eAAO,MAAM,MAAM,UACV,MAAM,GAAG,MAAM,QAChB,OAAO,KACZ,CAAC,MAAM,GAAG,IAAI,EAAE,MAAM,GAAG,IAAI,CAW/B,CAAC;AAEF,8EAA8E;AAC9E,eAAO,MAAM,OAAO,UAAW,MAAM,GAAG,MAAM,QAAQ,OAAO,KAAG,MAAM,GAAG,IAUxE,CAAC;AAEF,gFAAgF;AAChF,eAAO,MAAM,OAAO,UACX,MAAM,GAAG,MAAM,QAChB,OAAO,KACZ,CAAC,MAAM,GAAG,IAAI,EAAE,MAAM,GAAG,IAAI,EAAE,MAAM,GAAG,IAAI,CAQ9C,CAAC;AAEF,+EAA+E;AAC/E,eAAO,MAAM,IAAI,UACR,MAAM,GAAG,MAAM,QAChB,OAAO,UACL,MAAM,KACb,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAOzB,CAAC"}
1
+ {"version":3,"file":"address.d.ts","sourceRoot":"","sources":["../src/address.ts"],"names":[],"mappings":"AAAA,wDAAwD;AAExD,MAAM,MAAM,KAAK,GAAG,MAAM,GAAG,MAAM,GAAG,MAAM,GAAG,UAAU,CAAC;AAE1D,eAAO,MAAM,OAAO,IAAI,CAAC;AACzB,eAAO,MAAM,QAAQ,IAAI,CAAC;AAC1B,eAAO,MAAM,SAAS,IAAI,CAAC;AAC3B,eAAO,MAAM,QAAQ,IAAI,CAAC;AAC1B,eAAO,MAAM,UAAU,KAAK,CAAC;AAC7B,eAAO,MAAM,YAAY,KAAK,CAAC;AAC/B,eAAO,MAAM,aAAa,KAAK,CAAC;AAChC,eAAO,MAAM,MAAM,MAAM,CAAC;AAC1B,eAAO,MAAM,SAAS,MAAM,CAAC;AAE7B,eAAO,MAAM,MAAM,gBAAgB,CAAC;AACpC,eAAO,MAAM,SAAS,SAAS,CAAC;AAChC,eAAO,MAAM,MAAM,WAAW,CAAC;AAC/B,eAAO,MAAM,KAAK,UAAU,CAAC;AAuF7B,sFAAsF;AACtF,eAAO,MAAM,KAAK,UAAW,KAAK,KAAG,CAAC,MAAM,GAAG,MAAM,EAAE,OAAO,CA6B7D,CAAC;AA2BF,gFAAgF;AAChF,eAAO,MAAM,OAAO,UACX,MAAM,GAAG,MAAM,QAChB,OAAO,KACZ,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAezB,CAAC;AAEF,+EAA+E;AAC/E,eAAO,MAAM,OAAO,UAAW,MAAM,GAAG,MAAM,QAAQ,OAAO,KAAG,MACvC,CAAC;AAc1B,kFAAkF;AAClF,eAAO,MAAM,OAAO,UAAW,MAAM,GAAG,MAAM,QAAQ,OAAO,KAAG,MAa/D,CAAC;AAEF,0EAA0E;AAC1E,eAAO,MAAM,MAAM,UACV,MAAM,GAAG,MAAM,QAChB,OAAO,KACZ,CAAC,MAAM,GAAG,IAAI,EAAE,MAAM,GAAG,IAAI,CAW/B,CAAC;AAEF,8EAA8E;AAC9E,eAAO,MAAM,OAAO,UAAW,MAAM,GAAG,MAAM,QAAQ,OAAO,KAAG,MAAM,GAAG,IAUxE,CAAC;AAEF,gFAAgF;AAChF,eAAO,MAAM,OAAO,UACX,MAAM,GAAG,MAAM,QAChB,OAAO,KACZ,CAAC,MAAM,GAAG,IAAI,EAAE,MAAM,GAAG,IAAI,EAAE,MAAM,GAAG,IAAI,CAQ9C,CAAC;AAEF,+EAA+E;AAC/E,eAAO,MAAM,IAAI,UACR,MAAM,GAAG,MAAM,QAChB,OAAO,UACL,MAAM,KACb,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAOzB,CAAC"}
package/dist/address.js CHANGED
@@ -168,21 +168,11 @@ export const spelled = (value, wide) => {
168
168
  const nibbles = value.toString(16).padStart(32, "0");
169
169
  const arpa = `${[...nibbles].reverse().join(".")}.ip6.arpa`;
170
170
  const mapped = held.slice(0, 5).every((group) => group === 0) && held[5] === 0xffff;
171
- if (mapped) {
172
- const quad = dotted(Number(value & 0xffffffffn));
173
- const parts = held.slice(0, 6).map((group) => group.toString(16));
174
- const full = held.slice(0, 6).map((group) => group.toString(16).padStart(4, "0"));
175
- return [
176
- `${shortest(parts, held.slice(0, 6))}:${quad}`,
177
- `${full.join(":")}:${quad}`,
178
- arpa,
179
- ];
180
- }
181
- return [
182
- shortest(held.map((group) => group.toString(16)), held),
183
- held.map((group) => group.toString(16).padStart(4, "0")).join(":"),
184
- arpa,
185
- ];
171
+ const groups = mapped ? held.slice(0, 6) : held;
172
+ const quad = mapped ? `:${dotted(Number(value & 0xffffffffn))}` : "";
173
+ const hex = groups.map((group) => group.toString(16));
174
+ const full = groups.map((group) => group.toString(16).padStart(4, "0"));
175
+ return [shortest(hex, groups) + quad, full.join(":") + quad, arpa];
186
176
  };
187
177
  /** An address as text: v4 from its octets, v6 through the shortening rules. */
188
178
  export const written = (value, wide) => spelled(value, wide)[0];