diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3813617..675a549 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -13,5 +13,15 @@ jobs: - uses: actions/setup-node@v4 with: node-version: ${{ matrix.node }} + - run: npm ci --ignore-scripts - run: npm test - run: npm pack + + runtime-floor: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: 6 + - run: node test/runtime-smoke.js diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 7505d4c..0000000 --- a/.travis.yml +++ /dev/null @@ -1,17 +0,0 @@ -language: node_js - -node_js: - - "0.12" - - "0.10" - - "0.8" - - "0.6" - - "iojs" - -before_install: - - '[ "${TRAVIS_NODE_VERSION}" != "0.8" ] || npm install -g npm@1.4.28' - - '[ "${TRAVIS_NODE_VERSION}" != "0.6" ] || npm install -g npm@1.3.26' - - '[ "${TRAVIS_NODE_VERSION}" != "0.1*" ] || npm install -g npm@latest' - -notifications: - email: - - "jhurliman@jhurliman.org" diff --git a/CHANGELOG.md b/CHANGELOG.md index 14f98b7..ab9a508 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ # Changelog -## Unreleased +## 2.0.0 (release candidate) - Count keys such as `__proto__`, `constructor` and `toString` without interacting with Object.prototype. - Return independent top-k tuples so callers cannot modify internal sketch state. @@ -11,7 +11,7 @@ ### Release compatibility -Runtime minimum becomes Node 6 because Buffer.alloc is now used; test development requires modern Node (CI: 22/24/26). Dropping previously advertised Node 0.6 support requires a major release. No version has been bumped yet. Top-k tie ordering is unspecified and may change. The follow-up below adds versioned serialization and validates malformed inputs. See SERIALIZATION.md for legacy import/export and the major-release migration. +Runtime minimum becomes Node 6 because Buffer.alloc is now used; test development requires modern Node (CI: 22/24/26). Dropping previously advertised Node 0.6 support requires a major release. The package version is prepared as 2.0.0; publication is pending. Top-k tie ordering is unspecified and may change. The follow-up below adds versioned serialization and validates malformed inputs. See SERIALIZATION.md for legacy import/export and the major-release migration. ## Release validation follow-up @@ -20,3 +20,5 @@ Runtime minimum becomes Node 6 because Buffer.alloc is now used; test developmen - Reject overflowing counters atomically and validate HLL merges before mutation. - Fix the signed-minimum hash bucket edge case and retain the existing mapping for all other hashes. - Add legacy golden fixtures and tests for truncation, malformed metadata, invalid Unicode, capacity preservation and overflow. + +- Add complete root/deep-import TypeScript declarations, packed runtime/type consumer tests, a Node 6 runtime-floor check, and remove the obsolete Travis matrix identified in review. diff --git a/README.md b/README.md index 3fdf309..e74c1ac 100644 --- a/README.md +++ b/README.md @@ -112,7 +112,7 @@ __Arguments__ Returns the serialized size of a views counter (CountMinSketch) object in bytes given an errFactor and failRate. __NOTE:__ This does not include the size of the serialized MinHeap which includes the size of each unique ID (up to a -max of topEntryCount) plus 5 bytes overhead per entry. __NOTE2:__ The memory +max of topEntryCount) plus 8 bytes overhead per entry. __NOTE2:__ The memory usage will be higher than this number since we serialize 32-bit integers but JavaScript uses 64-bit numbers. @@ -235,3 +235,11 @@ __Example__ ```js var pageCounts = CountMinSketch.deserialize(bufferData); ``` + +## Version 2 migration and release checks + +Version 2 writes capacity-preserving CMS2 sketches by default. Old files remain readable; old readers need `serialize({ legacy: true })`. See [SERIALIZATION.md](SERIALIZATION.md) for layouts, allocation/input bounds and recovering the capacity of partially filled legacy sketches. Explicit zero/null/NaN options no longer silently select defaults. + +TypeScript declarations cover the package root and existing class/helper deep imports. Node TypeScript projects need `@types/node`. Runtime compatibility starts at Node 6; development and the complete test suite use Node 22 or newer. CI runs the full suite on 22/24/26 and a separate Node 6 runtime smoke test. + +Before releasing: `npm ci`, `npm test`, then review the archive produced by `npm pack`. The test suite itself installs the archive into an independent consumer, checks CommonJS/ESM and compiles NodeNext/Node16 TypeScript fixtures. `npm publish` runs the tests through `prepublishOnly`; publishing still requires the maintainer's authenticated release action. The version bump in this branch is preparation, not evidence of a published release. diff --git a/index.d.ts b/index.d.ts new file mode 100644 index 0000000..2a13e8d --- /dev/null +++ b/index.d.ts @@ -0,0 +1,50 @@ +/// +export interface SerializeOptions { legacy?: boolean; } +export interface DeserializeOptions { maxEntries?: number; } +/** Constructors return structural counter objects, not instanceof-compatible instances. */ +export class CountMinSketch { + constructor(maxEntries: number, epsilon: number, delta: number); + increment(key: string): void; + getTopK(): Array<[count: number, key: string]>; + serialize(options?: SerializeOptions): Buffer; + static deserialize(buffer: Buffer, start?: number, length?: number, options?: DeserializeOptions): CountMinSketch; +} +export class HyperLogLog { + constructor(stdError: number); + M: number[]; + add(key: string): void; + count(): number; + serialize(): Buffer; + merge(other: HyperLogLog): void; + static deserialize(buffer: Buffer, start?: number, length?: number): HyperLogLog; +} +export function createUniquesCounter(stdError?: number): HyperLogLog; +export function createViewsCounter(topEntryCount: number, errFactor?: number, failRate?: number): CountMinSketch; +export function getUniquesObjSize(stdError?: number): number; +/** Excludes variable-sized heap entries; each adds 8 bytes plus its UTF-8 key. */ +export function getViewsObjSize(errFactor?: number, failRate?: number): number; +export class MinHeap { + constructor(array?: T[], comparator?: (a: T, b: T) => number); + heap: T[]; + compare: (a: T, b: T) => number; + heapify(index: number): void; + siftUp(index: number): void; + heapifyArray(): void; + push(item: T): void; + pop(): T | undefined; + getMin(): T | undefined; + size(): number; +} +export interface RandomGenerator { + (): number; + random(): number; + uint32(): number; + fract53(): number; + version: string; + args: Array; +} +export interface RandomConstructor { + (...seeds: Array): RandomGenerator; + new (...seeds: Array): RandomGenerator; +} +export const PRNG: RandomConstructor; diff --git a/lib/countMinSketch.d.ts b/lib/countMinSketch.d.ts new file mode 100644 index 0000000..19d36f1 --- /dev/null +++ b/lib/countMinSketch.d.ts @@ -0,0 +1,2 @@ +import { CountMinSketch } from "../index"; +export = CountMinSketch; diff --git a/lib/hyperLogLog.d.ts b/lib/hyperLogLog.d.ts new file mode 100644 index 0000000..832b2df --- /dev/null +++ b/lib/hyperLogLog.d.ts @@ -0,0 +1,2 @@ +import { HyperLogLog } from "../index"; +export = HyperLogLog; diff --git a/lib/minHeap.d.ts b/lib/minHeap.d.ts new file mode 100644 index 0000000..0605852 --- /dev/null +++ b/lib/minHeap.d.ts @@ -0,0 +1,2 @@ +import { MinHeap } from "../index"; +export = MinHeap; diff --git a/lib/prng.d.ts b/lib/prng.d.ts new file mode 100644 index 0000000..194c5a9 --- /dev/null +++ b/lib/prng.d.ts @@ -0,0 +1,2 @@ +import { PRNG } from "../index"; +export = PRNG; diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..2329435 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,412 @@ +{ + "name": "streamcount", + "version": "2.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "streamcount", + "version": "2.0.0", + "license": "MIT", + "dependencies": { + "@types/node": "^24.13.4" + }, + "devDependencies": { + "typescript": "^7.0.2" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@types/node": { + "version": "24.13.4", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.13.4.tgz", + "integrity": "sha512-YJ7EqCstVTzIr0fMr7qul/977en+pQHrfmuKIo6Zr9i75Be21dr3MovcfvGtyvi2HAUrRerWps5sMO9I7WaxDw==", + "license": "MIT", + "dependencies": { + "undici-types": "~7.18.0" + } + }, + "node_modules/@typescript/typescript-aix-ppc64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-aix-ppc64/-/typescript-aix-ppc64-7.0.2.tgz", + "integrity": "sha512-MTKKkWB7p/0E9xi1d1tHtZ5PiLkGEMIq88pK2CubZjOsLtYTLqhgIgi6zepFa+9GHZ6h05NMCkQxGKiPXMxXtQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-darwin-arm64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-darwin-arm64/-/typescript-darwin-arm64-7.0.2.tgz", + "integrity": "sha512-gowzar9MwS/aRWp6f3a4KUqzRjAZjOsmGNCM6LcTgXum+dBfgsBVMN+AgvOCCbguXyick6LJhpBszxMebJ8syA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-darwin-x64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-darwin-x64/-/typescript-darwin-x64-7.0.2.tgz", + "integrity": "sha512-SZ9xZInqApNlNGc9s0W1VSsktYSOe9cFqNOIqmN1Gs8SmkjKZYFt017G4VwPxASInODuAdbTW7sXiFUf893RgA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-freebsd-arm64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-freebsd-arm64/-/typescript-freebsd-arm64-7.0.2.tgz", + "integrity": "sha512-W5NH4y/J0plIIS5b2xvTEkU7JFxyqdMAOgf+Ilhl0vHQXKO5dZoxd+C/jEtq56c4F3wk71RB4BMRQ2XdI+bwYQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-freebsd-x64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-freebsd-x64/-/typescript-freebsd-x64-7.0.2.tgz", + "integrity": "sha512-UMGDx5sTpzNw3WiPebH7l90IWfJggEd+egHt/q6p7/Cm3zqoV7VxkGXt+3DxPIw8CcmvAB0j3sVVfbhX+M4Tpw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-arm": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-arm/-/typescript-linux-arm-7.0.2.tgz", + "integrity": "sha512-gffT3xPz9sR7j/YJExkyPntrI0P2EP9XbOyWzth2/Gs0RstK+90RBcO0ncXoXy/beYll1SXw846Nf2zdnEz0QQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-arm64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-arm64/-/typescript-linux-arm64-7.0.2.tgz", + "integrity": "sha512-Qh4eU4/y3yDjnfjjyPYihMj5/ODIlmt+Bzu17OI+fiSRDW57QmU5SiN63exPRNJPKUzcc1INa1NXdrJ+MqHjUQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-loong64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-loong64/-/typescript-linux-loong64-7.0.2.tgz", + "integrity": "sha512-uEHck9i8hoAzXPiYRib1O7miOnz23SxIeVl6F4LXox+qov1K35jHcEW6VHKvZI+pyvl7fZEP4MCU5LYvIq1GuQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-mips64el": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-mips64el/-/typescript-linux-mips64el-7.0.2.tgz", + "integrity": "sha512-R4KvAMnE43W5Qeqb0Ly56O3mWMWIAgsMyz36DCaycd5nbg/9kzm0liw3JocfRqyJY0KPmzFjbswozXyW0DnIYA==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-ppc64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-ppc64/-/typescript-linux-ppc64-7.0.2.tgz", + "integrity": "sha512-DORx5b3sd/4S7eayxm4FQv+A7CrkUIGRaHiwI8oiHTAI1fAPWhF4J0vAlkC8biAlHSVVwxMQ3tjZ2/DVbnQiiA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-riscv64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-riscv64/-/typescript-linux-riscv64-7.0.2.tgz", + "integrity": "sha512-wf0jqEDOjrPRnKwYRyyJDRo11KMbvMFrU+q4zqKyChODBzvlkbhNQfKvLxQCcwTpdDaXSHZTVuh0JoCrKCUMHQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-s390x": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-s390x/-/typescript-linux-s390x-7.0.2.tgz", + "integrity": "sha512-IkwJc3L7yhytWd/ewjyxNDfOmswCm9GWMJT/ue/dU4aZNbwZeYAetq42VyLmsmSjvoX7z74X6ZaYCtzAr0EuGw==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-x64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-x64/-/typescript-linux-x64-7.0.2.tgz", + "integrity": "sha512-EYdf2cNg7rgCWJnxCdJ+F3V39O8ihb37eHAu1LK8oAFizgTQbPOK7zHHXbPt8rX24COqODXeI3sIf0fCXG7H/A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-netbsd-arm64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-netbsd-arm64/-/typescript-netbsd-arm64-7.0.2.tgz", + "integrity": "sha512-+polYF4MF04aPpO5FTkHran9yUQDSXqy5GiSDKpsll5jy3l3+g9QLhpf39T+ePtefhXLOGrLl0QIjkQP6VnelA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-netbsd-x64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-netbsd-x64/-/typescript-netbsd-x64-7.0.2.tgz", + "integrity": "sha512-8YIT0EHM/3dq10ZOVF/A7pc/YSMtbcecct4rWtexrnSCHOPcpC2KTLXfTCR6vDpnSiY12heNb1GiN/wu+T/FyA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-openbsd-arm64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-openbsd-arm64/-/typescript-openbsd-arm64-7.0.2.tgz", + "integrity": "sha512-APT8+ClYnuYm1u9+kgGXoMj2VzWzcymwh2gNSQVySHfkRDGOTVkoWLjCmOQSaO+PoqQ57B0flRp9SA+7GnnkzQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-openbsd-x64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-openbsd-x64/-/typescript-openbsd-x64-7.0.2.tgz", + "integrity": "sha512-yX7s+Q0Dln0Dt9tEzZsAjXXR/+ytBM7AlglaqyeMPxQszJ1JhlJdZ6jLA+IzldHtflX81em7lDao1xXu+aRRkg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-sunos-x64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-sunos-x64/-/typescript-sunos-x64-7.0.2.tgz", + "integrity": "sha512-dLJDGaLZ1D4HPQn62u1n8mBDkJREwMsAkCdkwd4Ieqw+x3TUyTsqY0YiBCtE6H6OzzgGk3iuZ3vFWRS+E8/d1g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-win32-arm64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-win32-arm64/-/typescript-win32-arm64-7.0.2.tgz", + "integrity": "sha512-Gyl1Vy6OsWesLzmq+EP0Fb7b4Nid5232AvcA2SFcdYreldpNtYFFofPjnt62y9hQy7VTaZp65ICJjuAQRaVcIQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-win32-x64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-win32-x64/-/typescript-win32-x64-7.0.2.tgz", + "integrity": "sha512-0BQ3HkAHHlKLSp1qRvf3SUhGpGsDuhB/jgFw75guyqbxJqEaS0Cw/VFO8i2nHglJUzQCRtMMR/IBAKE3ETMC4g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/typescript": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-7.0.2.tgz", + "integrity": "sha512-8FYau96o3NKOhbjKi/qNvG/W5jhzxkbdm5sj9AbZ/5T5sWqn3hJgLfGx27sRKZWTvyzCP8dLRBTf5tBTSRVUNA==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc" + }, + "engines": { + "node": ">=16.20.0" + }, + "optionalDependencies": { + "@typescript/typescript-aix-ppc64": "7.0.2", + "@typescript/typescript-darwin-arm64": "7.0.2", + "@typescript/typescript-darwin-x64": "7.0.2", + "@typescript/typescript-freebsd-arm64": "7.0.2", + "@typescript/typescript-freebsd-x64": "7.0.2", + "@typescript/typescript-linux-arm": "7.0.2", + "@typescript/typescript-linux-arm64": "7.0.2", + "@typescript/typescript-linux-loong64": "7.0.2", + "@typescript/typescript-linux-mips64el": "7.0.2", + "@typescript/typescript-linux-ppc64": "7.0.2", + "@typescript/typescript-linux-riscv64": "7.0.2", + "@typescript/typescript-linux-s390x": "7.0.2", + "@typescript/typescript-linux-x64": "7.0.2", + "@typescript/typescript-netbsd-arm64": "7.0.2", + "@typescript/typescript-netbsd-x64": "7.0.2", + "@typescript/typescript-openbsd-arm64": "7.0.2", + "@typescript/typescript-openbsd-x64": "7.0.2", + "@typescript/typescript-sunos-x64": "7.0.2", + "@typescript/typescript-win32-arm64": "7.0.2", + "@typescript/typescript-win32-x64": "7.0.2" + } + }, + "node_modules/undici-types": { + "version": "7.18.2", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz", + "integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==", + "license": "MIT" + } + } +} diff --git a/package.json b/package.json index 519fea3..81289c9 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "streamcount", - "version": "1.0.1", + "version": "2.0.0", "author": "John Hurliman (http://jhurliman.org/)", "description": "Provides implementations of sketch algorithms for real-time counting of stream data. Useful for real-time web analytics and other streaming or big data scenarios.", "keywords": [ @@ -15,9 +15,12 @@ "main": "./index", "scripts": { "test": "node --test test/*-test.js", - "bench": "node bench/repeated-updates.js" + "bench": "node bench/repeated-updates.js", + "prepublishOnly": "npm test" + }, + "devDependencies": { + "typescript": "^7.0.2" }, - "devDependencies": {}, "engines": { "node": ">=6" }, @@ -29,6 +32,12 @@ "files": [ "index.js", "lib/", - "CHANGELOG.md" - ] + "CHANGELOG.md", + "index.d.ts", + "SERIALIZATION.md" + ], + "types": "./index.d.ts", + "dependencies": { + "@types/node": "^24.13.4" + } } diff --git a/test/package-test.js b/test/package-test.js new file mode 100644 index 0000000..ddd6a3e --- /dev/null +++ b/test/package-test.js @@ -0,0 +1,51 @@ +const { test } = require('node:test'); +const assert = require('node:assert/strict'); +const fs = require('node:fs'); +const os = require('node:os'); +const path = require('node:path'); +const { execFileSync } = require('node:child_process'); + +test('distributed package resolves CommonJS, ESM, public and deep-import types', () => { + const root = path.resolve(__dirname, '..'); + const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'streamcount-package-')); + function run(command, args) { return execFileSync(command, args, {cwd: dir, encoding:'utf8', stdio:'pipe'}); } + try { + const pack = JSON.parse(execFileSync('npm', ['pack','--json','--pack-destination',dir],{cwd:root,encoding:'utf8'}))[0]; + assert.ok(pack.files.some(f => f.path === 'SERIALIZATION.md')); + assert.ok(pack.files.every(f => !f.path.startsWith('test/') && !f.path.startsWith('node_modules/'))); + run('npm',['install','--ignore-scripts','--no-audit','--no-fund',path.join(dir,pack.filename)]); + const smoke = "const c=api.createViewsCounter(3);c.increment('a');const d=api.CountMinSketch.deserialize(c.serialize());d.increment('b');if(d.getTopK().length!==2)throw Error('capacity');"; + run(process.execPath,['-e',"const api=require('streamcount');"+smoke]); + run(process.execPath,['--input-type=module','-e',"import api from 'streamcount';"+smoke]); + const types = `import * as api from 'streamcount'; +import CMS = require('streamcount/lib/countMinSketch'); +import HLL = require('streamcount/lib/hyperLogLog'); +import Heap = require('streamcount/lib/minHeap'); +import Random = require('streamcount/lib/prng'); +const views: api.CountMinSketch = new CMS(10, .1, .1); +views.increment('key'); +const rows: Array<[number,string]> = views.getTopK(); +const restored = CMS.deserialize(views.serialize({legacy:true}), undefined, undefined, {maxEntries:10}); +const uniques: api.HyperLogLog = new HLL(.1); uniques.merge(api.createUniquesCounter()); +const count: number = uniques.count(); +const heap = new Heap([],(a,b)=>a.localeCompare(b)); heap.push('a'); +const item: string | undefined = heap.pop(); +const rng = new Random(42); const number: number = rng.random() + Random('seed')(); +api.getViewsObjSize(); api.getUniquesObjSize(); api.createViewsCounter(5); +// @ts-expect-error keys must be strings +views.increment(123); +// @ts-expect-error heap types propagate +heap.push(42); +// @ts-expect-error serialization requires a Buffer +CMS.deserialize('bytes'); +`; + fs.writeFileSync(path.join(dir,'consumer.cts'),types); + fs.writeFileSync(path.join(dir,'consumer.mts'),types); + const tsc = path.join(root,'node_modules/typescript/bin/tsc'); + const common = [tsc,'--strict','--noEmit','--target','es2022']; + run(process.execPath,common.concat(['--module','nodenext','--moduleResolution','nodenext','consumer.cts','consumer.mts'])); + // Also exercise Node16 module resolution. + fs.writeFileSync(path.join(dir,'consumer.ts'),types); + run(process.execPath,common.concat(['--module','node16','--moduleResolution','node16','consumer.ts'])); + } finally { fs.rmSync(dir,{recursive:true,force:true}); } +}); diff --git a/test/runtime-smoke.js b/test/runtime-smoke.js new file mode 100644 index 0000000..624204b --- /dev/null +++ b/test/runtime-smoke.js @@ -0,0 +1,10 @@ +var assert = require('assert'); +var api = require('..'); +var sketch = api.createViewsCounter(10); +sketch.increment('__proto__'); +var restored = api.CountMinSketch.deserialize(sketch.serialize()); +restored.increment('another'); +assert.strictEqual(restored.getTopK().length, 2); +var hll = api.createUniquesCounter(); hll.add('key'); +assert.ok(api.HyperLogLog.deserialize(hll.serialize()).count() > 0); +assert.throws(function() { api.createViewsCounter(0); }); diff --git a/test/validation-test.js b/test/validation-test.js index 50911d4..94a29af 100644 --- a/test/validation-test.js +++ b/test/validation-test.js @@ -121,3 +121,15 @@ test('invalid merge sources reject before modifying the destination', () => { const source=new HLL(.2);source.M[0]=2;source.M[source.M.length-1]=100; assert.throws(()=>target.merge(source));assert.deepEqual(target.serialize(),before); }); + +test('signed-minimum hashes address valid buckets', () => { + const hashing = require('../lib/hashing'); + const original = hashing.fnv1a; + try { + hashing.fnv1a = () => -2147483648; + const sketch = new CMS(2,.2,.1); + sketch.increment('edge'); sketch.increment('edge'); + assert.deepEqual(sketch.getTopK(), [[2,'edge']]); + assert.deepEqual(CMS.deserialize(sketch.serialize()).getTopK(), [[2,'edge']]); + } finally { hashing.fnv1a = original; } +});