update: remove @types/ip2region

This commit is contained in:
Alan 2024-12-17 16:24:32 +08:00
parent 1a11c91061
commit 37dc606a59
24 changed files with 0 additions and 7051 deletions

View File

@ -1 +0,0 @@
* text eol=lf

View File

@ -1,32 +0,0 @@
name: CI
on: [push]
jobs:
build:
name: Build, lint, and test on Node ${{ matrix.node }} and ${{ matrix.os }}
runs-on: ${{ matrix.os }}
strategy:
matrix:
node: ['14.x', '16.x']
os: [ubuntu-latest, windows-latest, macOS-latest]
steps:
- name: Checkout repo
uses: actions/checkout@v4
- name: Use Node ${{ matrix.node }}
uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node }}
- name: Install deps and build (with cache)
uses: bahmutov/npm-install@v1
- name: Lint
run: yarn lint
- name: Test
run: yarn test --ci --coverage --maxWorkers=2
- name: Build
run: yarn build

View File

@ -1,12 +0,0 @@
name: size
on: [pull_request]
jobs:
size:
runs-on: ubuntu-latest
env:
CI_JOB_NUMBER: 1
steps:
- uses: actions/checkout@v4
- uses: andresz1/size-limit-action@v1
with:
github_token: ${{ secrets.GITHUB_TOKEN }}

View File

@ -1,4 +0,0 @@
*.log
.DS_Store
node_modules
dist

View File

@ -1,21 +0,0 @@
MIT License
Copyright (c) 2024 Alan
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@ -1,44 +0,0 @@
# @types/ip2region
TypeScript node client library for IP2Region
## Installation
Install the package with NPM
```bash
npm install @types/ip2region
# or
yarn add @types/ip2region
```
## Usage
```typescript
import { Searcher, CachePolicy, ISearcher } from 'ip2region';
const dbPath = './ip2region.xdb';
const searcher: ISearcher = new Searcher(CachePolicy.Content, dbPath);
const ip = '8.8.8.8';
const regionInfo = await searcher.search(ip);
console.log(`IP: ${ip}, Region: ${regionInfo}`);
await searcher.close();
```
### Cache Policy Description
| Cache Policy | Description | Thread Safe |
|-------------------------|------------------------------------------------------------------------------------------------------------|-------------|
| CachePolicy.Content | Cache the entire `xdb` data. | Yes |
| CachePolicy.VectorIndex | Cache `vecotorIndex` to speed up queries and reduce system io pressure by reducing one fixed IO operation. | Yes |
| CachePolicy.File | Completely file-based queries | Yes |
### XDB File Description
Generate using [maker](https://github.com/lionsoul2014/ip2region/tree/master/maker/csharp), or [download](https://github.com/lionsoul2014/ip2region/blob/master/data/ip2region.xdb) pre-generated xdb files
## Contributing
Pull requests are welcome. For major changes, please open an issue first to discuss what you would like to change.
Please make sure to update tests as appropriate.
## License
[Apache License 2.0](https://github.com/lionsoul2014/ip2region/blob/master/LICENSE.md)

View File

@ -1,59 +0,0 @@
{
"name": "@types/ip2region",
"version": "0.1.0",
"license": "Apache-2.0",
"author": "Alan",
"main": "dist/index.js",
"module": "dist/ip2region.esm.js",
"typings": "dist/index.d.ts",
"files": [
"dist",
"src"
],
"scripts": {
"analyze": "size-limit --why",
"build": "dts build",
"lint": "dts lint",
"prepare": "dts build",
"size": "size-limit",
"start": "dts watch",
"test": "dts test"
},
"husky": {
"hooks": {
"pre-commit": "dts lint"
}
},
"prettier": {
"printWidth": 80,
"semi": true,
"singleQuote": true,
"trailingComma": "es5"
},
"jest": {
"testEnvironment": "node"
},
"peerDependencies": {},
"engines": {
"node": ">=12"
},
"size-limit": [
{
"path": "dist/ip2region.cjs.production.min.js",
"limit": "10 KB"
},
{
"path": "dist/ip2region.esm.js",
"limit": "10 KB"
}
],
"devDependencies": {
"@size-limit/preset-small-lib": "^11.1.6",
"@tsconfig/recommended": "^1.0.8",
"dts-cli": "^2.0.5",
"husky": "^9.1.7",
"size-limit": "^11.1.6",
"tslib": "^2.8.1",
"typescript": "^5.7.2"
}
}

View File

@ -1,93 +0,0 @@
import * as fs from 'fs/promises';
export abstract class AbstractCacheStrategy {
protected static readonly HEADER_INFO_LENGTH = 256;
protected static readonly VECTOR_INDEX_ROWS = 256;
protected static readonly VECTOR_INDEX_COLS = 256;
protected static readonly VECTOR_INDEX_SIZE = 8;
private fileHandle: fs.FileHandle | null = null;
private fileSize: number = 0;
private ioCounter = 0;
protected constructor(protected readonly xdbPath: string) {
// 构造不立即打开文件,懒加载
}
public get ioCount(): number {
return this.ioCounter;
}
protected incrementIoCount() {
this.ioCounter++;
}
/** 打开文件,若已打开则跳过 */
protected async openFile(): Promise<void> {
if (this.fileHandle === null) {
const fh = await fs.open(this.xdbPath, 'r');
this.fileHandle = fh;
const stat = await fh.stat();
this.fileSize = stat.size;
}
}
protected getFileSize(): number {
return this.fileSize;
}
/** 关闭文件 */
public async closeFile(): Promise<void> {
if (this.fileHandle) {
await this.fileHandle.close();
this.fileHandle = null;
}
}
/**
*
* @param ip uint32
*/
protected getVectorIndexStartPos(ip: number): number {
const il0 = (ip >>> 24) & 0xFF;
const il1 = (ip >>> 16) & 0xFF;
const idx = il0 * AbstractCacheStrategy.VECTOR_INDEX_COLS * AbstractCacheStrategy.VECTOR_INDEX_SIZE
+ il1 * AbstractCacheStrategy.VECTOR_INDEX_SIZE;
return idx;
}
public abstract getVectorIndex(ip: number): Promise<Buffer>;
/**
* Node.js可安全并发读
* @param offset
* @param length
*/
public async getData(offset: number, length: number): Promise<Buffer> {
await this.openFile();
if (!this.fileHandle) {
throw new Error('File handle is not available.');
}
// Node.js 通常会一次性读完,但仍保留循环以防偶发读不足
let totalBytesRead = 0;
const buffer = Buffer.allocUnsafe(length);
while (totalBytesRead < length) {
const { bytesRead } = await this.fileHandle.read({
buffer,
offset: totalBytesRead,
length: length - totalBytesRead,
position: offset + totalBytesRead
});
this.incrementIoCount();
if (bytesRead === 0) {
break; // 文件结束或出错
}
totalBytesRead += bytesRead;
}
return buffer.subarray(0, totalBytesRead);
}
}

View File

@ -1,22 +0,0 @@
import { CachePolicy } from "../models/cache-policy";
import { AbstractCacheStrategy } from "./abstract-cache-strategy";
import { ContentCacheStrategy } from "./content-cache-strategy";
import { FileCacheStrategy } from "./file-cache-strategy";
import { VectorIndexCacheStrategy } from "./vector-index-cache-strategy";
export class CacheStrategyFactory {
constructor(private readonly xdbPath: string) {}
public createCacheStrategy(cachePolicy: CachePolicy): AbstractCacheStrategy {
switch (cachePolicy) {
case CachePolicy.Content:
return new ContentCacheStrategy(this.xdbPath);
case CachePolicy.VectorIndex:
return new VectorIndexCacheStrategy(this.xdbPath);
case CachePolicy.File:
return new FileCacheStrategy(this.xdbPath);
default:
throw new Error(`Unknown cachePolicy: ${cachePolicy}`);
}
}
}

View File

@ -1,45 +0,0 @@
import { AbstractCacheStrategy } from "./abstract-cache-strategy";
export class ContentCacheStrategy extends AbstractCacheStrategy {
private cacheData: Buffer | null = null;
constructor(xdbPath: string) {
super(xdbPath);
}
public async getVectorIndex(ip: number): Promise<Buffer> {
if (!this.cacheData) {
await this.ensureCacheLoaded();
}
if (!this.cacheData) {
throw new Error('Cache data is not loaded.');
}
const idx = this.getVectorIndexStartPos(ip);
return this.cacheData.subarray(
AbstractCacheStrategy.HEADER_INFO_LENGTH + idx,
AbstractCacheStrategy.HEADER_INFO_LENGTH + idx + AbstractCacheStrategy.VECTOR_INDEX_SIZE
);
}
public override async getData(offset: number, length: number): Promise<Buffer> {
if (!this.cacheData) {
await this.ensureCacheLoaded();
}
if (!this.cacheData) {
throw new Error('Cache data is not loaded.');
}
return this.cacheData.subarray(offset, offset + length);
}
private async ensureCacheLoaded(): Promise<void> {
if (this.cacheData !== null) {
return;
}
await this.openFile();
const fileSize = this.getFileSize();
this.cacheData = await super.getData(0, fileSize);
// 全量读取后立即关闭文件
await this.closeFile();
}
}

View File

@ -1,15 +0,0 @@
import { AbstractCacheStrategy } from "./abstract-cache-strategy";
export class FileCacheStrategy extends AbstractCacheStrategy {
public constructor(xdbPath: string) {
super(xdbPath);
}
public async getVectorIndex(ip: number): Promise<Buffer> {
const idx = this.getVectorIndexStartPos(ip);
return this.getData(
AbstractCacheStrategy.HEADER_INFO_LENGTH + idx,
AbstractCacheStrategy.VECTOR_INDEX_SIZE
);
}
}

View File

@ -1,32 +0,0 @@
import { AbstractCacheStrategy } from "./abstract-cache-strategy";
export class VectorIndexCacheStrategy extends AbstractCacheStrategy {
private vectorIndex: Buffer | null = null;
constructor(xdbPath: string) {
super(xdbPath);
}
public async getVectorIndex(ip: number): Promise<Buffer> {
if (!this.vectorIndex) {
await this.ensureVectorIndexLoaded();
}
if (!this.vectorIndex) {
throw new Error('Vector index data not loaded.');
}
const idx = this.getVectorIndexStartPos(ip);
return this.vectorIndex.subarray(idx, idx + AbstractCacheStrategy.VECTOR_INDEX_SIZE);
}
private async ensureVectorIndexLoaded(): Promise<void> {
if (this.vectorIndex !== null) {
return;
}
await this.openFile();
const vectorLength = AbstractCacheStrategy.VECTOR_INDEX_ROWS
* AbstractCacheStrategy.VECTOR_INDEX_COLS
* AbstractCacheStrategy.VECTOR_INDEX_SIZE;
this.vectorIndex = await super.getData(AbstractCacheStrategy.HEADER_INFO_LENGTH, vectorLength);
// 保留 fileHandle 打开,因为后续可能还要继续读段数据
}
}

View File

@ -1,3 +0,0 @@
export * from './searcher/searcher';
export * from './searcher/ISearcher';
export * from './models/cache-policy';

View File

@ -1,5 +0,0 @@
export enum CachePolicy {
File = 'File',
VectorIndex = 'VectorIndex',
Content = 'Content'
}

View File

@ -1,6 +0,0 @@
export interface ISearcher {
search(ipStr: string): Promise<string | undefined>;
searchByUint32(ip: number): Promise<string | undefined>;
ioCount: number;
close(): Promise<void>;
}

View File

@ -1,84 +0,0 @@
import { AbstractCacheStrategy } from "../cache/abstract-cache-strategy";
import { CacheStrategyFactory } from "../cache/cache-strategy-factory";
import { CachePolicy } from "../models/cache-policy";
import { Util } from "../util/ip-util";
import { ISearcher } from "./ISearcher";
export class Searcher implements ISearcher {
private readonly cacheStrategy: AbstractCacheStrategy;
constructor(cachePolicy: CachePolicy, dbPath: string) {
const factory = new CacheStrategyFactory(dbPath);
this.cacheStrategy = factory.createCacheStrategy(cachePolicy);
}
public get ioCount(): number {
return this.cacheStrategy.ioCount;
}
/** 主动关闭 */
public async close(): Promise<void> {
await this.cacheStrategy.closeFile();
}
public async search(ipStr: string): Promise<string | undefined> {
const ip = Util.ipAddressToUInt32(ipStr);
return this.searchByUint32(ip);
}
/**
* sPtr~ePtr getData()
*/
public async searchByUint32(ip: number): Promise<string | undefined> {
const index = await this.cacheStrategy.getVectorIndex(ip);
if (index.length < 8) {
return undefined;
}
// 读取 sPtr, ePtruint32, 小端序)
const sPtr = index.readUInt32LE(0);
const ePtr = index.readUInt32LE(4);
const SEGMENT_INDEX_SIZE = 14;
const totalCount = Math.floor((ePtr - sPtr) / SEGMENT_INDEX_SIZE) + 1;
if (totalCount <= 0) {
return undefined;
}
// 一次性批量读取 [sPtr, ePtr] 范围内的所有 segment
const blockSize = totalCount * SEGMENT_INDEX_SIZE;
const blockBuff = await this.cacheStrategy.getData(sPtr, blockSize);
// 在内存中完成二分查找
let l = 0;
let h = totalCount - 1;
let dataLen = 0;
let dataPtr = 0;
while (l <= h) {
const mid = (l + h) >>> 1; // 中点
const offset = mid * SEGMENT_INDEX_SIZE;
const sip = blockBuff.readUInt32LE(offset);
const eip = blockBuff.readUInt32LE(offset + 4);
if (ip < sip) {
h = mid - 1;
} else if (ip > eip) {
l = mid + 1;
} else {
dataLen = blockBuff.readUInt16LE(offset + 8);
dataPtr = blockBuff.readUInt32LE(offset + 10);
break;
}
}
if (dataLen === 0) {
return undefined;
}
// 读取 region 数据
const regionBuff = await this.cacheStrategy.getData(dataPtr, dataLen);
return regionBuff.toString('utf8');
}
}

View File

@ -1,33 +0,0 @@
export class Util {
public static isValidIpAddress(ipStr: string): boolean {
const octets = ipStr.split('.');
if (octets.length !== 4) {
return false;
}
for (const octet of octets) {
if (isNaN(Number(octet)) || Number(octet) < 0 || Number(octet) > 255) {
return false;
}
}
return true;
}
/**
* IPv4 uint32
* "1.2.3.4" -> 0x04030201
*/
public static ipAddressToUInt32(ipStr: string): number {
if (!Util.isValidIpAddress(ipStr)) {
throw new Error(`Invalid IPv4: ${ipStr}`);
}
const octets = ipStr.split('.').map(Number);
// 数据库是小端存储,如 1.2.3.4 => 0x04030201
return (
((octets[3] & 0xff) << 24) |
((octets[2] & 0xff) << 16) |
((octets[1] & 0xff) << 8) |
(octets[0] & 0xff)
) >>> 0;
}
}

View File

@ -1,31 +0,0 @@
import { ContentCacheStrategy } from '../../src/cache/content-cache-strategy';
describe('ContentCacheStrategy Tests', () => {
let strategy: ContentCacheStrategy;
const dbPath = "../../../../data/ip2region.xdb";
beforeAll(() => {
strategy = new ContentCacheStrategy(dbPath);
});
afterAll(async () => {
await strategy.closeFile();
});
it('should load the entire file into memory', async () => {
// 首次访问会触发 ensureCacheLoaded
const vectorIndex = await strategy.getVectorIndex(0x01020304);
expect(vectorIndex.length).toBe(8); // VECTOR_INDEX_SIZE = 8
// 再次读取不会增加IO次数太多
const initIo = strategy.ioCount;
await strategy.getVectorIndex(0x05060708);
const newIo = strategy.ioCount;
expect(newIo).toBe(initIo); // 全量缓存后几乎不增加IO
});
it('should read arbitrary data after caching', async () => {
const data = await strategy.getData(0, 32); // 读取前32字节
expect(data.length).toBe(32);
});
});

View File

@ -1,36 +0,0 @@
import { FileCacheStrategy } from '../../src/cache/file-cache-strategy';
describe('FileCacheStrategy Tests', () => {
let strategy: FileCacheStrategy;
//const dbPath = join(__dirname, '..', 'data', 'ip2region.xdb');
const dbPath = "../../../../data/ip2region.xdb";
beforeAll(() => {
strategy = new FileCacheStrategy(dbPath);
});
afterAll(async () => {
await strategy.closeFile();
});
it('should read vector index from file each time', async () => {
// 每次获取矢量索引,都会触发一次文件读取
const initIo = strategy.ioCount;
const vecIndex1 = await strategy.getVectorIndex(0x01020304);
expect(vecIndex1.length).toBe(8);
const afterIo1 = strategy.ioCount;
expect(afterIo1).toBeGreaterThan(initIo);
// 第二次读取仍增加IO因为不缓存
const vecIndex2 = await strategy.getVectorIndex(0x05060708);
expect(vecIndex2.length).toBe(8);
const afterIo2 = strategy.ioCount;
expect(afterIo2).toBeGreaterThan(afterIo1);
});
it('should read arbitrary data directly from file', async () => {
const offset = 0;
const length = 64;
const data = await strategy.getData(offset, length);
expect(data.length).toBe(length);
});
});

View File

@ -1,40 +0,0 @@
import { VectorIndexCacheStrategy } from '../../src/cache/vector-index-cache-strategy';
describe('VectorIndexCacheStrategy Tests', () => {
let strategy: VectorIndexCacheStrategy;
//const dbPath = join(__dirname, '..', 'data', 'ip2region.xdb');
const dbPath = "../../../../data/ip2region.xdb";
beforeAll(() => {
strategy = new VectorIndexCacheStrategy(dbPath);
});
afterAll(async () => {
await strategy.closeFile();
});
it('should cache the vector index in memory', async () => {
// 首次调用会把矢量索引读到内存
const initIo = strategy.ioCount;
const vecIndex1 = await strategy.getVectorIndex(0x01020304);
expect(vecIndex1.length).toBe(8);
const afterIo1 = strategy.ioCount;
expect(afterIo1).toBeGreaterThan(initIo);
// 第二次获取矢量索引不会增加IO
const vecIndex2 = await strategy.getVectorIndex(0x05060708);
expect(vecIndex2.length).toBe(8);
const afterIo2 = strategy.ioCount;
expect(afterIo2).toBe(afterIo1);
});
it('should still read segment data from file as needed', async () => {
// 模拟读取部分数据段
const offset = 1024; // 示例偏移
const length = 32;
const segmentData = await strategy.getData(offset, length);
expect(segmentData.length).toBe(length);
// 这会增加IO
expect(strategy.ioCount).toBeGreaterThan(0);
});
});

View File

@ -1,39 +0,0 @@
import { Searcher } from '../../src/searcher/searcher';
import { CachePolicy } from '../../src/models/cache-policy';
describe('Searcher Tests', () => {
let searcher: Searcher;
beforeAll(() => {
// 指向一个真实存在的 xdb 文件(测试需要)
const dbPath = "../../../../data/ip2region.xdb";
// 测试用 ContentCacheStrategy一次性载入文件
searcher = new Searcher(CachePolicy.Content, dbPath);
});
afterAll(async () => {
await searcher.close();
});
it('should return valid data for a known IP', async () => {
const ipStr = '1.2.3.4';
const regionInfo = await searcher.search(ipStr);
expect(regionInfo).toBeDefined();
});
it('should return undefined for an invalid IP', async () => {
const invalidIp = '999.999.999.999';
await expect(searcher.search(invalidIp)).rejects.toThrow();
});
it('should track ioCount properly', async () => {
// ioCount 取决于缓存策略和查询次数
const initialCount = searcher.ioCount;
await searcher.search('8.8.8.8');
const afterCount = searcher.ioCount;
// ContentCacheStrategy只读取一次所以 ioCount 不变
expect(afterCount).toEqual(initialCount);
});
});

View File

@ -1,16 +0,0 @@
import { Util } from '../../src/util/ip-util';
describe('IP Util Tests', () => {
it('should convert valid IPv4 string to uint32 correctly', () => {
const ipStr = '1.2.3.4';
const uint32 = Util.ipAddressToUInt32(ipStr);
// 1.2.3.4 小端序 => 0x04030201 => 67305985
expect(uint32).toBe(67305985);
});
it('should throw error for invalid IPv4 string', () => {
expect(() => {
Util.ipAddressToUInt32('256.256.256.256');
}).toThrow();
});
});

View File

@ -1,8 +0,0 @@
{
"extends": "@tsconfig/recommended/tsconfig.json",
"include": ["src", "types"],
"compilerOptions": {
"module": "esnext",
"allowImportingTsExtensions": false
}
}

File diff suppressed because it is too large Load Diff