├── .husky ├── .gitignore ├── pre-push └── pre-commit ├── RoaringBitmap32.js ├── RoaringBitmap32Iterator.js ├── RoaringBitmap32ReverseIterator.js ├── .gitmodules ├── benchmarks ├── utils.ts ├── union-inplace.bench.ts ├── intersection-inplace.bench.ts ├── add.bench.ts ├── iterator.bench.ts ├── union-new.bench.ts ├── intersection-size.bench.ts ├── intersection-new.bench.ts └── union-size.bench.ts ├── lint-staged.config.js ├── .editorconfig ├── RoaringBitmap32Iterator.d.ts ├── scripts ├── rebuild.js ├── update-roaring.sh ├── precommit.js ├── ensure-python-env.js ├── prepush.js ├── lib │ ├── python-path.js │ ├── unity.js │ └── utils.js ├── system-info.js ├── test-memory-leaks.js ├── build.js ├── prebuild-local.js └── node-pre-gyp-publish.js ├── RoaringBitmap32ReverseIterator.d.ts ├── tsconfig.test.json ├── .vscode ├── extensions.json ├── c_cpp_properties.json └── settings.json ├── RoaringBitmap32.d.ts ├── jsconfig.json ├── .github └── workflows │ ├── npm-publish.yml │ ├── ci.yml │ └── publish.yml ├── vitest.config.mjs ├── test ├── helpers │ └── roaring.ts ├── RoaringBitmap32Iterator │ ├── RoaringBitmap32Iterator.import.test.ts │ └── RoaringBitmap32Iterator.test.ts ├── RoaringBitmap32 │ ├── RoaringBitmap32.worker-threads.test.ts │ ├── RoaringBitmap32.import.test.ts │ ├── worker-thread-test.js │ ├── RoaringBitmap32.fromAsync.test.ts │ ├── RoaringBitmap32.deserializeParallelAsync.test.ts │ ├── RoaringBitmap33.deserializeAsync.test.ts │ ├── RoaringBitmap32.serialization-file.test.ts │ └── RoaringBitmap32.frozen.test.ts ├── RoaringBitmap32ReverseIterator │ ├── RoaringBitmap32ReverseIterator.import.test.ts │ └── RoaringBitmap32ReverseIterator.test.ts ├── roaring.test.ts └── aligned-buffers.test.ts ├── src └── cpp │ ├── includes.h │ ├── croaring.h │ ├── object-wrap.h │ ├── addon-strings.h │ ├── main.cpp │ ├── WorkerError.h │ ├── croaring.cpp │ ├── mmap.h │ ├── RoaringBitmap32.h │ ├── aligned-buffers.h │ ├── memory.h │ ├── addon-data.h │ ├── serialization-csv.h │ ├── serialization-format.h │ └── RoaringBitmap32BufferedIterator.h ├── biome.json ├── tsconfig.json ├── .clang-format ├── .gitignore ├── binding.gyp ├── .clang-tidy ├── package.json └── node-pre-gyp.js /.husky/.gitignore: -------------------------------------------------------------------------------- 1 | _ 2 | -------------------------------------------------------------------------------- /.husky/pre-push: -------------------------------------------------------------------------------- 1 | node scripts/prepush.js 2 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | node scripts/precommit.js 2 | -------------------------------------------------------------------------------- /RoaringBitmap32.js: -------------------------------------------------------------------------------- 1 | module.exports = require("./index").RoaringBitmap32; 2 | -------------------------------------------------------------------------------- /RoaringBitmap32Iterator.js: -------------------------------------------------------------------------------- 1 | module.exports = require("./index").RoaringBitmap32Iterator; 2 | -------------------------------------------------------------------------------- /RoaringBitmap32ReverseIterator.js: -------------------------------------------------------------------------------- 1 | module.exports = require("./index").RoaringBitmap32ReverseIterator; 2 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "submodules/CRoaring"] 2 | path = submodules/CRoaring 3 | url = https://github.com/RoaringBitmap/CRoaring.git 4 | -------------------------------------------------------------------------------- /benchmarks/utils.ts: -------------------------------------------------------------------------------- 1 | export function consume(value: T): void { 2 | consume.value = value; 3 | } 4 | 5 | consume.value = 0 as unknown; 6 | -------------------------------------------------------------------------------- /lint-staged.config.js: -------------------------------------------------------------------------------- 1 | // lint-staged.config.js 2 | 3 | module.exports = { 4 | "*": () => { 5 | return [`biome check --write`, "tsc --noEmit"]; 6 | }, 7 | }; 8 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | end_of_line = lf 5 | insert_final_newline = true 6 | 7 | [*.{js,ts}] 8 | charset = utf-8 9 | indent_style = space 10 | indent_size = 2 11 | -------------------------------------------------------------------------------- /RoaringBitmap32Iterator.d.ts: -------------------------------------------------------------------------------- 1 | import roaring from "."; 2 | 3 | /** 4 | * Iterator class for RoaringBitmap32 5 | * 6 | * @type {roaring.RoaringBitmap32} 7 | */ 8 | export = roaring.RoaringBitmap32Iterator; 9 | -------------------------------------------------------------------------------- /scripts/rebuild.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const { runMain } = require("./lib/utils"); 4 | const { build } = require("./build"); 5 | 6 | process.argv.push("rebuild"); 7 | 8 | runMain(build, "rebuild"); 9 | -------------------------------------------------------------------------------- /scripts/update-roaring.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | git submodule update --init --recursive 6 | 7 | cd submodules/CRoaring 8 | 9 | git checkout master 10 | git pull 11 | 12 | cd ../.. 13 | -------------------------------------------------------------------------------- /RoaringBitmap32ReverseIterator.d.ts: -------------------------------------------------------------------------------- 1 | import roaring from "."; 2 | 3 | /** 4 | * Iterator class for RoaringBitmap32 5 | * 6 | * @type {roaring.RoaringReverseBitmap32} 7 | */ 8 | export = roaring.RoaringBitmap32ReverseIterator; 9 | -------------------------------------------------------------------------------- /tsconfig.test.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "declaration": false, 5 | "noEmit": true 6 | }, 7 | "exclude": ["node_modules/**/*", ".eslintcache/**/*", "temp/**/*", "build/**/*"] 8 | } 9 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "biomejs.biome", 4 | "pflannery.vscode-versionlens", 5 | "donjayamanne.githistory", 6 | "eamodio.gitlens", 7 | "ms-vscode.cpptools", 8 | "xaver.clang-format" 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /RoaringBitmap32.d.ts: -------------------------------------------------------------------------------- 1 | import roaring from "."; 2 | 3 | /** 4 | * Roaring bitmap that supports 32 bit unsigned integers. 5 | * 6 | * See http://roaringbitmap.org/ 7 | * 8 | * @type {roaring.RoaringBitmap32} 9 | */ 10 | export = roaring.RoaringBitmap32; 11 | -------------------------------------------------------------------------------- /jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "noEmit": true, 4 | "target": "es6", 5 | "module": "commonjs", 6 | "allowSyntheticDefaultImports": true 7 | }, 8 | "typeAcquisition": { 9 | "enable": true 10 | }, 11 | "exclude": ["**/node_modules", "**/.eslintcache", "package"] 12 | } 13 | -------------------------------------------------------------------------------- /scripts/precommit.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const { execSync } = require("node:child_process"); 4 | const { runMain } = require("./lib/utils"); 5 | 6 | runMain(() => { 7 | const nodeVersion = parseInt(process.versions.node.split(".")[0], 10); 8 | if (nodeVersion >= 14) { 9 | execSync("npx lint-staged", { stdio: "inherit" }); 10 | } 11 | }, "precommit"); 12 | -------------------------------------------------------------------------------- /.github/workflows/npm-publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish to NPM 2 | 3 | # Only allows manual triggering 4 | on: 5 | workflow_dispatch: {} 6 | 7 | jobs: 8 | npm-publish: 9 | name: npm-publish 10 | runs-on: ubuntu-latest 11 | permissions: 12 | contents: read 13 | id-token: write 14 | steps: 15 | - uses: actions/checkout@v6 16 | - uses: actions/setup-node@v4 17 | with: 18 | node-version: "24.x" 19 | - run: npm install --exact --no-audit --no-save 20 | - run: npx tsc --noEmit 21 | - run: npm run test 22 | - run: npm publish --provenance 23 | -------------------------------------------------------------------------------- /vitest.config.mjs: -------------------------------------------------------------------------------- 1 | import { spawn } from "node:child_process"; 2 | import { fileURLToPath } from "node:url"; 3 | import { defineConfig } from "vitest/config"; 4 | 5 | async function runSystemInfo() { 6 | const scriptPath = fileURLToPath(new URL("./scripts/system-info.js", import.meta.url)); 7 | spawn(process.execPath, [scriptPath], { stdio: "inherit" }); 8 | } 9 | 10 | await runSystemInfo(); 11 | 12 | export default defineConfig({ 13 | test: { 14 | include: ["test/**/*.test.ts"], 15 | environment: "node", 16 | benchmark: { 17 | include: ["benchmarks/**/*.bench.ts"], 18 | }, 19 | }, 20 | }); 21 | -------------------------------------------------------------------------------- /test/helpers/roaring.ts: -------------------------------------------------------------------------------- 1 | import roaring from "../.."; 2 | 3 | export const { 4 | asBuffer, 5 | bufferAlignedAlloc, 6 | bufferAlignedAllocUnsafe, 7 | bufferAlignedAllocShared, 8 | bufferAlignedAllocSharedUnsafe, 9 | ensureBufferAligned, 10 | isBufferAligned, 11 | RoaringBitmap32, 12 | FileSerializationFormat, 13 | FileDeserializationFormat, 14 | SerializationFormat, 15 | DeserializationFormat, 16 | FrozenViewFormat, 17 | } = roaring; 18 | 19 | export default roaring; 20 | 21 | export type { 22 | FileSerializationDeserializationFormatType, 23 | SerializationDeserializationFormatType, 24 | } from "../.."; 25 | -------------------------------------------------------------------------------- /test/RoaringBitmap32Iterator/RoaringBitmap32Iterator.import.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from "vitest"; 2 | import RoaringBitmap32Iterator from "../../RoaringBitmap32Iterator"; 3 | 4 | describe("RoaringBitmap32Iterator import", () => { 5 | it('exports itself with a "default" property', () => { 6 | const required = require("../../RoaringBitmap32Iterator"); 7 | expect(!!required).to.be.true; 8 | expect(required === required.default).to.be.true; 9 | }); 10 | 11 | it("supports typescript \"import RoaringBitmap32Iterator from 'roaring/RoaringBitmap32Iterator'\" syntax", () => { 12 | expect(RoaringBitmap32Iterator === require("../../RoaringBitmap32Iterator")).eq(true); 13 | }); 14 | 15 | it("is a class", () => { 16 | expect(typeof RoaringBitmap32Iterator).eq("function"); 17 | expect(RoaringBitmap32Iterator.prototype.constructor).eq(RoaringBitmap32Iterator); 18 | }); 19 | }); 20 | -------------------------------------------------------------------------------- /test/RoaringBitmap32/RoaringBitmap32.worker-threads.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, it } from "vitest"; 2 | 3 | const { resolve: pathResolve } = require("node:path"); 4 | 5 | describe("RoaringBitmap32 worker-threads", () => { 6 | it("can be used and works inside a worker thread", () => { 7 | const { Worker } = require("node:worker_threads"); 8 | const worker = new Worker(pathResolve(__dirname, "worker-thread-test.js")); 9 | return new Promise((resolve, reject) => { 10 | worker.on("message", (message: any) => { 11 | if (message === "ok") { 12 | resolve(); 13 | } else { 14 | reject(new Error(message)); 15 | } 16 | }); 17 | worker.on("error", reject); 18 | worker.on("exit", (code: any) => { 19 | if (code !== 0) { 20 | reject(new Error(`Worker stopped with exit code ${code}`)); 21 | } 22 | }); 23 | }); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /test/RoaringBitmap32/RoaringBitmap32.import.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from "vitest"; 2 | import RoaringBitmap32 from "../../RoaringBitmap32"; 3 | 4 | describe("RoaringBitmap32 import", () => { 5 | it('exports itself with a "default" property', () => { 6 | const required = require("../../RoaringBitmap32"); 7 | expect(!!required).to.be.true; 8 | expect(required === required.default).to.be.true; 9 | }); 10 | 11 | it("supports typescript \"import RoaringBitmap32 from 'roaring/RoaringBitmap32'\" syntax", () => { 12 | expect(RoaringBitmap32 === require("../../RoaringBitmap32")).eq(true); 13 | }); 14 | 15 | it("is a class", () => { 16 | expect(typeof RoaringBitmap32).eq("function"); 17 | expect(RoaringBitmap32.prototype.constructor).eq(RoaringBitmap32); 18 | }); 19 | 20 | it("can be called as a normal function", () => { 21 | const bitmap = (RoaringBitmap32 as any as () => any)(); 22 | expect(bitmap).to.be.instanceOf(RoaringBitmap32); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /test/RoaringBitmap32ReverseIterator/RoaringBitmap32ReverseIterator.import.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from "vitest"; 2 | import RoaringBitmap32ReverseIterator from "../../RoaringBitmap32ReverseIterator"; 3 | 4 | describe("RoaringBitmap32ReverseIterator import", () => { 5 | it('exports itself with a "default" property', () => { 6 | const required = require("../../RoaringBitmap32ReverseIterator"); 7 | expect(!!required).to.be.true; 8 | expect(required === required.default).to.be.true; 9 | }); 10 | 11 | it("supports typescript \"import RoaringBitmap32ReverseIterator from 'roaring/RoaringBitmap32ReverseIterator'\" syntax", () => { 12 | expect(RoaringBitmap32ReverseIterator === require("../../RoaringBitmap32ReverseIterator")).eq(true); 13 | }); 14 | 15 | it("is a class", () => { 16 | expect(typeof RoaringBitmap32ReverseIterator).eq("function"); 17 | expect(RoaringBitmap32ReverseIterator.prototype.constructor).eq(RoaringBitmap32ReverseIterator); 18 | }); 19 | }); 20 | -------------------------------------------------------------------------------- /scripts/ensure-python-env.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const { getPythonPath } = require("./lib/python-path"); 4 | const { writeFileSync } = require("node:fs"); 5 | const process = require("node:process"); 6 | 7 | function formatPythonPath(pythonPath) { 8 | if (process.platform === "win32") { 9 | return pythonPath.replace(/\//g, "\\"); 10 | } 11 | return pythonPath; 12 | } 13 | 14 | function main(pythonPath) { 15 | if (!pythonPath) { 16 | throw new Error("Unable to locate a functional Python interpreter."); 17 | } 18 | 19 | pythonPath = formatPythonPath(pythonPath); 20 | 21 | const lines = ["PYTHON", "NODE_GYP_FORCE_PYTHON"].map((key) => `${key}=${pythonPath}`); 22 | const text = `${lines.join("\n")}\n`; 23 | const envFile = process.env.GITHUB_ENV; 24 | if (!envFile) { 25 | console.log(text); 26 | return; 27 | } 28 | 29 | writeFileSync(envFile, text, { flag: "a" }); 30 | console.log(`Configured node-gyp python -> ${pythonPath}`); 31 | } 32 | 33 | main(getPythonPath()); 34 | -------------------------------------------------------------------------------- /benchmarks/union-inplace.bench.ts: -------------------------------------------------------------------------------- 1 | import FastBitSet from "fastbitset"; 2 | import { bench, describe } from "vitest"; 3 | import roaringModule from "../index.js"; 4 | 5 | const { RoaringBitmap32 } = roaringModule; 6 | const N = 256 * 256; 7 | 8 | const left = new Uint32Array(N); 9 | const right = new Uint32Array(N); 10 | for (let i = 0; i < N; i++) { 11 | left[i] = 3 * i + 5; 12 | right[i] = 6 * i + 5; 13 | } 14 | 15 | const setLeft = new Set(left); 16 | const fastLeft = new FastBitSet(left); 17 | const roaringLeft = new RoaringBitmap32(left); 18 | 19 | describe("union (in place)", () => { 20 | bench("Set", () => { 21 | const target = new Set(right); 22 | for (const value of setLeft) { 23 | target.add(value); 24 | } 25 | }); 26 | 27 | bench("FastBitSet", () => { 28 | const target = new FastBitSet(right); 29 | target.union(fastLeft); 30 | }); 31 | 32 | bench("RoaringBitmap32", () => { 33 | const target = new RoaringBitmap32(right); 34 | target.orInPlace(roaringLeft); 35 | }); 36 | }); 37 | -------------------------------------------------------------------------------- /benchmarks/intersection-inplace.bench.ts: -------------------------------------------------------------------------------- 1 | import FastBitSet from "fastbitset"; 2 | import { bench, describe } from "vitest"; 3 | import roaringModule from "../index.js"; 4 | 5 | const { RoaringBitmap32 } = roaringModule; 6 | const N = 256 * 256; 7 | 8 | const left = new Uint32Array(N); 9 | const right = new Uint32Array(N); 10 | for (let i = 0; i < N; i++) { 11 | left[i] = 3 * i + 5; 12 | right[i] = 6 * i + 5; 13 | } 14 | 15 | const setLeft = new Set(left); 16 | const fastLeft = new FastBitSet(left); 17 | const roaringLeft = new RoaringBitmap32(left); 18 | 19 | describe("intersection (in place)", () => { 20 | bench("Set", () => { 21 | const target = new Set(right); 22 | for (const value of setLeft) { 23 | target.delete(value); 24 | } 25 | }); 26 | 27 | bench("FastBitSet", () => { 28 | const target = new FastBitSet(right); 29 | target.intersection(fastLeft); 30 | }); 31 | 32 | bench("RoaringBitmap32", () => { 33 | const target = new RoaringBitmap32(right); 34 | target.andInPlace(roaringLeft); 35 | }); 36 | }); 37 | -------------------------------------------------------------------------------- /.vscode/c_cpp_properties.json: -------------------------------------------------------------------------------- 1 | { 2 | "configurations": [ 3 | { 4 | "name": "Mac", 5 | "includePath": [ 6 | "${workspaceFolder}", 7 | "${workspaceFolder}/submodules/CRoaring/include", 8 | "/Users/sp/n/include/node", 9 | "/usr/local/include", 10 | "/usr/local/include/node", 11 | "${default}" 12 | ], 13 | "defines": [], 14 | "intelliSenseMode": "clang-x64", 15 | "browse": { 16 | "path": [ 17 | "${workspaceFolder}", 18 | "${workspaceFolder}/submodules/CRoaring/include", 19 | "/Users/sp/n/include/node", 20 | "/usr/local/include", 21 | "/usr/local/include/node", 22 | "${default}" 23 | ], 24 | "limitSymbolsToIncludedHeaders": true, 25 | "databaseFilename": "" 26 | }, 27 | "macFrameworkPath": ["/System/Library/Frameworks", "/Library/Frameworks"], 28 | "compilerPath": "/usr/bin/clang", 29 | "cStandard": "c11", 30 | "cppStandard": "c++17" 31 | } 32 | ], 33 | "version": 4 34 | } 35 | -------------------------------------------------------------------------------- /src/cpp/includes.h: -------------------------------------------------------------------------------- 1 | #ifndef ROARING_NODE_INCLUDES_ 2 | #define ROARING_NODE_INCLUDES_ 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | #include 16 | #include 17 | #include 18 | #include 19 | #include 20 | #include 21 | 22 | #if defined(__APPLE__) 23 | # include 24 | #else 25 | # include 26 | #endif 27 | 28 | #ifdef _MSC_VER 29 | # define atomicIncrement32(ptr) InterlockedIncrement(ptr) 30 | # define atomicDecrement32(ptr) InterlockedDecrement(ptr) 31 | #else 32 | # define atomicIncrement32(ptr) __sync_add_and_fetch(ptr, 1) 33 | # define atomicDecrement32(ptr) __sync_sub_and_fetch(ptr, 1) 34 | #endif 35 | 36 | #include "croaring.h" 37 | 38 | #define NEW_LITERAL_V8_STRING(isolate, str, type) v8::String::NewFromUtf8Literal(isolate, str, type) 39 | 40 | typedef const char * const_char_ptr_t; 41 | 42 | #endif // ROARING_NODE_INCLUDES_ 43 | -------------------------------------------------------------------------------- /src/cpp/croaring.h: -------------------------------------------------------------------------------- 1 | #ifndef ROARING_NODE_CROARING_ 2 | #define ROARING_NODE_CROARING_ 3 | 4 | #include "includes.h" 5 | #include "memory.h" 6 | 7 | #include "../../submodules/CRoaring/include/roaring/roaring.h" 8 | 9 | void croaringMemoryInitialize() { 10 | static std::atomic roaringMemoryInitialized{false}; 11 | static std::mutex roaringMemoryInitializedMutex; 12 | 13 | if (roaringMemoryInitialized.load(std::memory_order_acquire)) { 14 | return; 15 | } 16 | 17 | std::lock_guard guard(roaringMemoryInitializedMutex); 18 | if (roaringMemoryInitialized.load(std::memory_order_relaxed)) { 19 | return; 20 | } 21 | 22 | roaring_memory_t roaringMemory{}; 23 | roaringMemory.malloc = gcaware_malloc; 24 | roaringMemory.realloc = gcaware_realloc; 25 | roaringMemory.calloc = gcaware_calloc; 26 | roaringMemory.free = gcaware_free; 27 | roaringMemory.aligned_malloc = gcaware_aligned_malloc; 28 | roaringMemory.aligned_free = gcaware_aligned_free; 29 | 30 | roaring_init_memory_hook(roaringMemory); 31 | roaringMemoryInitialized.store(true, std::memory_order_release); 32 | } 33 | 34 | #endif // ROARING_NODE_CROARING_ 35 | -------------------------------------------------------------------------------- /benchmarks/add.bench.ts: -------------------------------------------------------------------------------- 1 | import { bench, describe } from "vitest"; 2 | import roaringModule from "../index.js"; 3 | 4 | const { RoaringBitmap32 } = roaringModule; 5 | const N = 65535; 6 | 7 | const data = new Uint32Array(N); 8 | for (let i = 0; i < N; i++) { 9 | data[i] = 3 * i + 5; 10 | } 11 | const dataArray = Array.from(data); 12 | 13 | describe("add", () => { 14 | bench("Set.add", () => { 15 | const x = new Set(); 16 | for (let i = 0; i < N; ++i) { 17 | x.add(data[i]); 18 | } 19 | }); 20 | 21 | bench("RoaringBitmap32.tryAdd", () => { 22 | const x = new RoaringBitmap32(); 23 | for (let i = 0; i < N; ++i) { 24 | x.tryAdd(data[i]); 25 | } 26 | }); 27 | 28 | bench("RoaringBitmap32.add", () => { 29 | const x = new RoaringBitmap32(); 30 | for (let i = 0; i < N; ++i) { 31 | x.add(data[i]); 32 | } 33 | }); 34 | 35 | bench("RoaringBitmap32.addMany Array", () => { 36 | const x = new RoaringBitmap32(); 37 | x.addMany(dataArray); 38 | }); 39 | 40 | bench("RoaringBitmap32.addMany Uint32Array", () => { 41 | const x = new RoaringBitmap32(); 42 | x.addMany(data); 43 | }); 44 | }); 45 | -------------------------------------------------------------------------------- /benchmarks/iterator.bench.ts: -------------------------------------------------------------------------------- 1 | import { bench, describe } from "vitest"; 2 | import roaringModule from "../index.js"; 3 | import { consume } from "./utils"; 4 | 5 | const { RoaringBitmap32 } = roaringModule; 6 | const N = 65536; 7 | 8 | const data = new Uint32Array(N); 9 | for (let i = 0; i < N; i++) { 10 | data[i] = 3 * i + 5; 11 | } 12 | 13 | const setData = new Set(data); 14 | const roaringData = new RoaringBitmap32(data); 15 | 16 | describe("iterator", () => { 17 | bench("Set.iterator", () => { 18 | let total = 0; 19 | for (const value of setData) { 20 | total ^= value; 21 | } 22 | consume(total); 23 | }); 24 | 25 | bench("Set.forEach", () => { 26 | let total = 0; 27 | setData.forEach((value) => { 28 | total ^= value; 29 | }); 30 | consume(total); 31 | }); 32 | 33 | bench("RoaringBitmap32.iterator", () => { 34 | let total = 0; 35 | for (const value of roaringData) { 36 | total ^= value; 37 | } 38 | consume(total); 39 | }); 40 | 41 | bench("RoaringBitmap32.forEach", () => { 42 | let total = 0; 43 | roaringData.forEach((value) => { 44 | total ^= value; 45 | }); 46 | consume(total); 47 | }); 48 | }); 49 | -------------------------------------------------------------------------------- /biome.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://biomejs.dev/schemas/2.3.10/schema.json", 3 | "files": { 4 | "includes": [ 5 | "**", 6 | "!**/.husky/_/**/*", 7 | "!**/.tmp/**/*", 8 | "!**/src/cpp/**/*", 9 | "!**/build/**/*", 10 | "!**/docs/**/*", 11 | "!**/native/**/*", 12 | "!**/node_modules/**/*", 13 | "!**/submodules/**/*", 14 | "!**/package-lock.json", 15 | "!**/.clang-tidy", 16 | "!**/*.h", 17 | "!**/*.c", 18 | "!**/*.cpp" 19 | ], 20 | "ignoreUnknown": true 21 | }, 22 | "formatter": { 23 | "lineWidth": 120, 24 | "indentWidth": 2, 25 | "indentStyle": "space", 26 | "lineEnding": "lf", 27 | "bracketSpacing": true 28 | }, 29 | "javascript": { 30 | "formatter": { 31 | "quoteStyle": "double", 32 | "jsxQuoteStyle": "double", 33 | "trailingCommas": "all", 34 | "semicolons": "always", 35 | "arrowParentheses": "always" 36 | } 37 | }, 38 | "linter": { 39 | "enabled": true, 40 | "rules": { 41 | "style": {}, 42 | "suspicious": { 43 | "noConsole": "off", 44 | "noUnsafeDeclarationMerging": "off", 45 | "noExplicitAny": "off" 46 | } 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/cpp/object-wrap.h: -------------------------------------------------------------------------------- 1 | #ifndef ROARING_NODE_OBJECT_WRAP_ 2 | #define ROARING_NODE_OBJECT_WRAP_ 3 | 4 | #include "addon-data.h" 5 | 6 | class ObjectWrap { 7 | public: 8 | AddonData * const addonData; 9 | v8::Isolate * const isolate; 10 | 11 | template 12 | static T * TryUnwrap(const v8::Local & value, v8::Isolate * isolate) { 13 | v8::Local obj; 14 | if (!value->IsObject()) { 15 | return nullptr; 16 | } 17 | 18 | if (!value->ToObject(isolate->GetCurrentContext()).ToLocal(&obj)) { 19 | return nullptr; 20 | } 21 | 22 | if (obj->InternalFieldCount() != 2) { 23 | return nullptr; 24 | } 25 | 26 | if ((uintptr_t)obj->GetAlignedPointerFromInternalField(1) != T::OBJECT_TOKEN) { 27 | return nullptr; 28 | } 29 | 30 | return (T *)(obj->GetAlignedPointerFromInternalField(0)); 31 | } 32 | 33 | template 34 | static T * TryUnwrap(const v8::FunctionCallbackInfo & info, int argumentIndex) { 35 | return info.Length() <= argumentIndex ? nullptr : ObjectWrap::TryUnwrap(info[argumentIndex], info.GetIsolate()); 36 | } 37 | 38 | protected: 39 | explicit ObjectWrap(AddonData * addonData) : addonData(addonData), isolate(addonData->isolate) {} 40 | }; 41 | 42 | #endif // ROARING_NODE_OBJECT_WRAP_ 43 | -------------------------------------------------------------------------------- /benchmarks/union-new.bench.ts: -------------------------------------------------------------------------------- 1 | import FastBitSet from "fastbitset"; 2 | import { bench, describe } from "vitest"; 3 | import roaringModule from "../index.js"; 4 | import { consume } from "./utils"; 5 | 6 | const { RoaringBitmap32 } = roaringModule; 7 | const N = 1024 * 1024; 8 | 9 | const setA = new Set(); 10 | const setB = new Set(); 11 | const fastA = new FastBitSet(); 12 | const fastB = new FastBitSet(); 13 | const roaringA = new RoaringBitmap32(); 14 | const roaringB = new RoaringBitmap32(); 15 | 16 | for (let i = 0; i < N; i++) { 17 | const valueA = 3 * i + 5; 18 | const valueB = 6 * i + 5; 19 | setA.add(valueA); 20 | setB.add(valueB); 21 | fastA.add(valueA); 22 | fastB.add(valueB); 23 | roaringA.add(valueA); 24 | roaringB.add(valueB); 25 | } 26 | 27 | describe("union (new)", () => { 28 | bench("Set", () => { 29 | consume(genericSetUnion(setA, setB)); 30 | }); 31 | 32 | bench("FastBitSet", () => { 33 | consume(fastA.new_union(fastB)); 34 | }); 35 | 36 | bench("RoaringBitmap32", () => { 37 | consume(RoaringBitmap32.or(roaringA, roaringB)); 38 | }); 39 | }); 40 | 41 | function genericSetUnion(set1: Set, set2: Set) { 42 | const answer = new Set(set1); 43 | for (const value of set2) { 44 | answer.add(value); 45 | } 46 | return answer; 47 | } 48 | -------------------------------------------------------------------------------- /benchmarks/intersection-size.bench.ts: -------------------------------------------------------------------------------- 1 | import FastBitSet from "fastbitset"; 2 | import { bench, describe } from "vitest"; 3 | import roaringModule from "../index.js"; 4 | import { consume } from "./utils"; 5 | 6 | const { RoaringBitmap32 } = roaringModule; 7 | const N = 512 * 512; 8 | 9 | const left = new Uint32Array(N); 10 | const right = new Uint32Array(N); 11 | for (let i = 0; i < N; i++) { 12 | left[i] = 3 * i + 5; 13 | right[i] = 6 * i + 5; 14 | } 15 | 16 | const setLeft = new Set(left); 17 | const setRight = new Set(right); 18 | const fastLeft = new FastBitSet(left); 19 | const fastRight = new FastBitSet(right); 20 | const roaringLeft = new RoaringBitmap32(left); 21 | const roaringRight = new RoaringBitmap32(right); 22 | 23 | describe("intersection size", () => { 24 | bench("Set", () => { 25 | let answer = 0; 26 | if (setRight.size > setLeft.size) { 27 | for (const value of setLeft) { 28 | if (setRight.has(value)) { 29 | answer++; 30 | } 31 | } 32 | } else { 33 | for (const value of setRight) { 34 | if (setLeft.has(value)) { 35 | answer++; 36 | } 37 | } 38 | } 39 | consume(answer); 40 | }); 41 | 42 | bench("FastBitSet", () => { 43 | consume(fastLeft.intersection_size(fastRight)); 44 | }); 45 | 46 | bench("RoaringBitmap32", () => { 47 | consume(roaringLeft.andCardinality(roaringRight)); 48 | }); 49 | }); 50 | -------------------------------------------------------------------------------- /src/cpp/addon-strings.h: -------------------------------------------------------------------------------- 1 | #ifndef ROARING_NODE_ADDON_STRINGS_ 2 | #define ROARING_NODE_ADDON_STRINGS_ 3 | 4 | #include "includes.h" 5 | 6 | const char * const ERROR_FROZEN = "This bitmap is frozen and cannot be modified"; 7 | const char * const ERROR_INVALID_OBJECT = "Invalid RoaringBitmap32 object"; 8 | 9 | class AddonDataStrings final { 10 | public: 11 | v8::Global n; 12 | v8::Global readonly; 13 | v8::Global RoaringBitmap32; 14 | v8::Global symbol_rnshared; 15 | 16 | v8::Global OperationFailed; 17 | v8::Global Comma; 18 | 19 | inline explicit AddonDataStrings(v8::Isolate * isolate) { 20 | if (isolate == nullptr) { 21 | return; 22 | } 23 | literal(isolate, this->n, "n"); 24 | literal(isolate, this->readonly, "readonly"); 25 | literal(isolate, this->RoaringBitmap32, "RoaringBitmap32"); 26 | literal(isolate, this->Comma, ","); 27 | 28 | literal(isolate, this->OperationFailed, "Operation failed"); 29 | 30 | symbol_rnshared.Reset( 31 | isolate, 32 | v8::Symbol::ForApi(isolate, NEW_LITERAL_V8_STRING(isolate, "rnshared", v8::NewStringType::kInternalized))); 33 | } 34 | 35 | private: 36 | template 37 | static void literal(v8::Isolate * isolate, v8::Global & result, const char (&literal)[N]) { 38 | result.Reset(isolate, NEW_LITERAL_V8_STRING(isolate, literal, v8::NewStringType::kInternalized)); 39 | } 40 | }; 41 | 42 | #endif 43 | -------------------------------------------------------------------------------- /benchmarks/intersection-new.bench.ts: -------------------------------------------------------------------------------- 1 | import FastBitSet from "fastbitset"; 2 | import { bench, describe } from "vitest"; 3 | import roaringModule from "../index.js"; 4 | import { consume } from "./utils"; 5 | 6 | const { RoaringBitmap32 } = roaringModule; 7 | const N = 1024 * 1024; 8 | 9 | const setA = new Set(); 10 | const setB = new Set(); 11 | const fastA = new FastBitSet(); 12 | const fastB = new FastBitSet(); 13 | const roaringA = new RoaringBitmap32(); 14 | const roaringB = new RoaringBitmap32(); 15 | 16 | for (let i = 0; i < N; i++) { 17 | const valueA = 3 * i + 5; 18 | const valueB = 6 * i + 5; 19 | setA.add(valueA); 20 | setB.add(valueB); 21 | fastA.add(valueA); 22 | fastB.add(valueB); 23 | roaringA.add(valueA); 24 | roaringB.add(valueB); 25 | } 26 | 27 | describe("intersection (new)", () => { 28 | bench("Set", () => { 29 | const answer = new Set(); 30 | if (setB.size > setA.size) { 31 | for (const value of setA) { 32 | if (setB.has(value)) { 33 | answer.add(value); 34 | } 35 | } 36 | } else { 37 | for (const value of setB) { 38 | if (setA.has(value)) { 39 | answer.add(value); 40 | } 41 | } 42 | } 43 | consume(answer); 44 | }); 45 | 46 | bench("FastBitSet", () => { 47 | consume(fastA.new_intersection(fastB)); 48 | }); 49 | 50 | bench("RoaringBitmap32", () => { 51 | consume(RoaringBitmap32.and(roaringA, roaringB)); 52 | }); 53 | }); 54 | -------------------------------------------------------------------------------- /scripts/prepush.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const { execSync } = require("node:child_process"); 4 | const fs = require("node:fs"); 5 | 6 | const { CPP_UNITY_FILE_PATH, runMain } = require("./lib/utils"); 7 | const { unity } = require("./lib/unity"); 8 | const colors = require("ansis"); 9 | 10 | runMain(() => { 11 | const roaringNodeCpp = fs.readFileSync(CPP_UNITY_FILE_PATH, "utf8"); 12 | 13 | const nodeVersion = parseInt(process.versions.node.split(".")[0], 10); 14 | if (nodeVersion >= 14) { 15 | execSync("npx lint-staged"); 16 | } 17 | 18 | const unityResult = unity(); 19 | 20 | console.log(); 21 | 22 | if (unityResult.outputText !== roaringNodeCpp) { 23 | console.error( 24 | colors.redBright( 25 | `${colors.underline.bold( 26 | "ERROR", 27 | )}: ${CPP_UNITY_FILE_PATH} is outdated or not a production version. Run \`npm run build\` before committing and pushing.`, 28 | ), 29 | ); 30 | process.exitCode = 1; 31 | return; 32 | } 33 | 34 | try { 35 | require("../"); 36 | } catch { 37 | console.error( 38 | colors.redBright( 39 | `${colors.underline.bold( 40 | "ERROR", 41 | )}: library could not be loaded. Run \`npm run build\` before committing and pushing.`, 42 | ), 43 | ); 44 | process.exitCode = 1; 45 | return; 46 | } 47 | 48 | execSync("npm run lint", { stdio: "inherit" }); 49 | execSync("npm run test", { stdio: "inherit" }); 50 | }, "prepush"); 51 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "allowJs": true, 4 | "checkJs": false, 5 | "noEmit": true, 6 | "stripInternal": true, 7 | "allowSyntheticDefaultImports": true, 8 | "esModuleInterop": true, 9 | "allowUnreachableCode": false, 10 | "allowUnusedLabels": false, 11 | "alwaysStrict": true, 12 | "declaration": true, 13 | "emitDecoratorMetadata": false, 14 | "experimentalDecorators": false, 15 | "forceConsistentCasingInFileNames": true, 16 | "importHelpers": false, 17 | "lib": ["esnext"], 18 | "module": "nodenext", 19 | "moduleResolution": "nodenext", 20 | "newLine": "LF", 21 | "noFallthroughCasesInSwitch": true, 22 | "noImplicitAny": true, 23 | "noImplicitReturns": true, 24 | "noImplicitThis": true, 25 | "noStrictGenericChecks": false, 26 | "noUnusedLocals": false, 27 | "noUnusedParameters": false, 28 | "removeComments": false, 29 | "strict": true, 30 | "skipLibCheck": false, 31 | "skipDefaultLibCheck": false, 32 | "strictFunctionTypes": true, 33 | "strictNullChecks": true, 34 | "strictPropertyInitialization": true, 35 | "target": "ES2022", 36 | "isolatedModules": true 37 | }, 38 | "exclude": ["node_modules", "node_modules/**/*", "temp/**/*", "build/**/*"], 39 | "typedocOptions": { 40 | "out": "docs", 41 | "entryPoints": "src/index.ts", 42 | "exclude": ["test/**/*", "node_modules/**/*", "temp/**/*", "build/**/*", "docs/**/*"] 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /benchmarks/union-size.bench.ts: -------------------------------------------------------------------------------- 1 | import FastBitSet from "fastbitset"; 2 | import { bench, describe } from "vitest"; 3 | import roaringModule from "../index.js"; 4 | import { consume } from "./utils"; 5 | 6 | const { RoaringBitmap32 } = roaringModule; 7 | const N = 512 * 512; 8 | 9 | const left = new Uint32Array(N); 10 | const right = new Uint32Array(N); 11 | for (let i = 0; i < N; i++) { 12 | left[i] = 3 * i + 5; 13 | right[i] = 6 * i + 5; 14 | } 15 | 16 | const setLeft = new Set(left); 17 | const setRight = new Set(right); 18 | const fastLeft = new FastBitSet(left); 19 | const fastRight = new FastBitSet(right); 20 | const roaringLeft = new RoaringBitmap32(left); 21 | const roaringRight = new RoaringBitmap32(right); 22 | 23 | describe("union size", () => { 24 | bench("Set", () => { 25 | let answer = 0; 26 | if (setLeft.size > setRight.size) { 27 | answer = setLeft.size; 28 | for (const value of setRight) { 29 | if (!setLeft.has(value) && setRight.has(value)) { 30 | answer++; 31 | } 32 | } 33 | } else { 34 | answer = setRight.size; 35 | for (const value of setLeft) { 36 | if (!setRight.has(value) && setLeft.has(value)) { 37 | answer++; 38 | } 39 | } 40 | } 41 | consume(answer); 42 | }); 43 | 44 | bench("FastBitSet", () => { 45 | consume(fastLeft.union_size(fastRight)); 46 | }); 47 | 48 | bench("RoaringBitmap32", () => { 49 | consume(roaringLeft.orCardinality(roaringRight)); 50 | }); 51 | }); 52 | -------------------------------------------------------------------------------- /src/cpp/main.cpp: -------------------------------------------------------------------------------- 1 | #include "aligned-buffers.h" 2 | #include "RoaringBitmap32-main.h" 3 | #include "RoaringBitmap32BufferedIterator.h" 4 | 5 | using namespace v8; 6 | 7 | #define PREPROCESSOR_CONCAT(a, b) PREPROCESSOR_CONCAT_HELPER(a, b) 8 | #define PREPROCESSOR_CONCAT_HELPER(a, b) a##b 9 | 10 | #define MODULE_WORKER_ENABLED(module_name, registration) \ 11 | extern "C" NODE_MODULE_EXPORT void PREPROCESSOR_CONCAT(node_register_module_v, NODE_MODULE_VERSION)( \ 12 | v8::Local exports, v8::Local module, v8::Local context) { \ 13 | registration(exports); \ 14 | } 15 | 16 | void AddonData_DeleteInstance(void * addonData) { delete (AddonData *)addonData; } 17 | 18 | void InitRoaringNode(Local exports) { 19 | v8::Isolate * isolate = exports->GetIsolate(); 20 | 21 | v8::HandleScope scope(isolate); 22 | 23 | AddonData * addonData = new AddonData(isolate); 24 | 25 | node::AddEnvironmentCleanupHook(isolate, AddonData_DeleteInstance, addonData); 26 | 27 | addonData->initialize(); 28 | 29 | AlignedBuffers_Init(exports, addonData); 30 | RoaringBitmap32_Init(exports, addonData); 31 | RoaringBitmap32BufferedIterator_Init(exports, addonData); 32 | 33 | addonData->setMethod(exports, "getRoaringUsedMemory", getRoaringUsedMemory); 34 | 35 | v8utils::defineHiddenField(isolate, exports, "default", exports); 36 | } 37 | 38 | MODULE_WORKER_ENABLED(roaring, InitRoaringNode); 39 | 40 | #include "croaring.cpp" 41 | -------------------------------------------------------------------------------- /scripts/lib/python-path.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const { spawnSync } = require("node:child_process"); 4 | const { join: pathJoin } = require("node:path"); 5 | const process = require("node:process"); 6 | 7 | const detectionSnippet = "import pathlib, sys; print(pathlib.Path(sys.executable).as_posix())"; 8 | 9 | let pythonPathCache; 10 | 11 | function getPythonPath() { 12 | if (pythonPathCache === undefined) { 13 | pythonPathCache = resolvePythonPath(); 14 | } 15 | return pythonPathCache; 16 | } 17 | 18 | function resolvePythonPath() { 19 | const commands = []; 20 | const arg = process.argv[2]; 21 | if (arg) commands.push(arg); 22 | 23 | const pythonLocation = process.env.pythonLocation || process.env.PYTHON_LOCATION; 24 | if (pythonLocation) { 25 | const bin = process.platform === "win32" ? "python.exe" : "bin/python3"; 26 | commands.push(pathJoin(pythonLocation, bin)); 27 | } 28 | 29 | const defaults = process.platform === "win32" ? ["python", "python3", "py"] : ["python3", "python"]; 30 | for (const cmd of defaults) { 31 | commands.push(cmd); 32 | } 33 | 34 | const candidates = []; 35 | for (const cmd of commands) { 36 | if (cmd && !candidates.includes(cmd)) { 37 | candidates.push(cmd); 38 | } 39 | } 40 | 41 | for (const cmd of candidates) { 42 | const result = spawnSync(cmd, ["-c", detectionSnippet], { encoding: "utf8" }); 43 | if (result.status === 0 && result.stdout) { 44 | const value = result.stdout.trim(); 45 | if (value) { 46 | return value; 47 | } 48 | } 49 | } 50 | return null; 51 | } 52 | 53 | module.exports.getPythonPath = getPythonPath; 54 | -------------------------------------------------------------------------------- /.clang-format: -------------------------------------------------------------------------------- 1 | DisableFormat: false 2 | BasedOnStyle: Chromium 3 | IndentWidth: 2 4 | UseTab: Never 5 | ColumnLimit: 125 6 | NamespaceIndentation: All 7 | AlignAfterOpenBracket: AlwaysBreak 8 | AlignConsecutiveAssignments: false 9 | AlignConsecutiveBitFields: false 10 | AlignConsecutiveDeclarations: false 11 | AlignConsecutiveMacros: false 12 | AlignTrailingComments: false 13 | AlignEscapedNewlines: Left 14 | AlignOperands: false 15 | BitFieldColonSpacing: After 16 | AllowShortEnumsOnASingleLine: true 17 | AllowShortBlocksOnASingleLine: false 18 | AllowShortCaseLabelsOnASingleLine: true 19 | AlwaysBreakBeforeMultilineStrings: true 20 | AllowShortFunctionsOnASingleLine: true 21 | AllowShortIfStatementsOnASingleLine: true 22 | AllowShortLambdasOnASingleLine: false 23 | AllowShortLoopsOnASingleLine: false 24 | AlwaysBreakAfterDefinitionReturnType: false 25 | AllowAllArgumentsOnNextLine: true 26 | AllowAllConstructorInitializersOnNextLine: true 27 | AllowAllParametersOfDeclarationOnNextLine: true 28 | AlwaysBreakTemplateDeclarations: true 29 | BreakBeforeTernaryOperators: true 30 | ConstructorInitializerAllOnOneLineOrOnePerLine: true 31 | ConstructorInitializerIndentWidth: 2 32 | ContinuationIndentWidth: 2 33 | LambdaBodyIndentation: Signature 34 | PointerAlignment: Middle 35 | BinPackParameters: false 36 | BinPackArguments: false 37 | MaxEmptyLinesToKeep: 1 38 | BreakBeforeBraces: Attach 39 | BreakBeforeConceptDeclarations: true 40 | BreakConstructorInitializers: AfterColon 41 | BreakInheritanceList: AfterColon 42 | IndentPPDirectives: AfterHash 43 | SortIncludes: false 44 | SortUsingDeclarations: true 45 | 46 | --- 47 | Language: JavaScript 48 | DisableFormat: true 49 | ColumnLimit: 120 50 | JavaScriptQuotes: Single 51 | -------------------------------------------------------------------------------- /src/cpp/WorkerError.h: -------------------------------------------------------------------------------- 1 | #ifndef ROARING_NODE_WORKER_ERROR_ 2 | #define ROARING_NODE_WORKER_ERROR_ 3 | 4 | #include "includes.h" 5 | #include "v8utils.h" 6 | 7 | struct WorkerError { 8 | const char * msg; 9 | const char * syscall; 10 | int errorno; 11 | std::string path; 12 | 13 | explicit WorkerError() : msg(nullptr), syscall(nullptr), errorno(0) {} 14 | 15 | explicit WorkerError(const char * msg) : msg(msg), syscall(nullptr), errorno(0) {} 16 | 17 | explicit WorkerError(int errorno, const char * syscall, const std::string & path) : 18 | msg(nullptr), syscall(syscall), errorno(errorno ? errorno : 5), path(path) {} 19 | 20 | inline bool hasError() const { return (msg != nullptr && msg[0] != '\0') || errorno != 0; } 21 | 22 | static WorkerError from_errno(const char * syscall, const std::string & path) { 23 | int errorno = errno; 24 | errno = 0; 25 | return WorkerError(errorno, syscall, path); 26 | } 27 | 28 | v8::Local newV8Error(v8::Isolate * isolate) const { 29 | v8::EscapableHandleScope handle_scope(isolate); 30 | v8::Local output; 31 | if (this->errorno) { 32 | output = node::ErrnoException( 33 | isolate, this->errorno, this->syscall, this->msg && this->msg[0] ? this->msg : nullptr, this->path.c_str()); 34 | } else { 35 | const char * msg = this->msg && this->msg[0] ? this->msg : "Invalid operation"; 36 | v8::MaybeLocal message = v8::String::NewFromUtf8(isolate, msg, v8::NewStringType::kInternalized); 37 | output = v8::Exception::Error(message.IsEmpty() ? v8::String::Empty(isolate) : message.ToLocalChecked()); 38 | } 39 | return handle_scope.Escape(output); 40 | } 41 | }; 42 | 43 | #endif 44 | -------------------------------------------------------------------------------- /test/roaring.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from "vitest"; 2 | import roaring from ".."; 3 | 4 | import RoaringBitmap32 from "../RoaringBitmap32"; 5 | import RoaringBitmap32Iterator from "../RoaringBitmap32Iterator"; 6 | 7 | describe("roaring", () => { 8 | it("is an object", () => { 9 | expect(typeof roaring).eq("object"); 10 | }); 11 | 12 | it('exports itself with a "default" property', () => { 13 | const required = require("../"); 14 | expect(!!required).to.be.true; 15 | expect(required === roaring).to.be.true; 16 | expect(required === required.default).to.be.true; 17 | }); 18 | 19 | it("has RoaringBitmap32", () => { 20 | expect(typeof roaring.RoaringBitmap32).eq("function"); 21 | expect(roaring.RoaringBitmap32 === RoaringBitmap32).to.be.true; 22 | }); 23 | 24 | it("has RoaringBitmap32Iterator", () => { 25 | expect(typeof roaring.RoaringBitmap32Iterator).eq("function"); 26 | expect(roaring.RoaringBitmap32Iterator === RoaringBitmap32Iterator).to.be.true; 27 | }); 28 | 29 | it("has CRoaringVersion", () => { 30 | expect(typeof roaring.CRoaringVersion).eq("string"); 31 | const values = roaring.CRoaringVersion.split("."); 32 | expect(values).to.have.lengthOf(3); 33 | for (let i = 0; i < 3; ++i) { 34 | expect(Number.isInteger(Number.parseInt(values[i], 10))).eq(true); 35 | } 36 | }); 37 | 38 | it("has roaring PackageVersion", () => { 39 | expect(typeof roaring.PackageVersion).eq("string"); 40 | const values = roaring.CRoaringVersion.split("."); 41 | expect(values).to.have.lengthOf(3); 42 | for (let i = 0; i < 3; ++i) { 43 | expect(Number.isInteger(Number.parseInt(values[i], 10))).eq(true); 44 | } 45 | }); 46 | }); 47 | -------------------------------------------------------------------------------- /test/RoaringBitmap32/worker-thread-test.js: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line node/no-unsupported-features/node-builtins 2 | const { parentPort } = require("node:worker_threads"); 3 | const assert = require("node:assert/strict"); 4 | 5 | process.on("uncaughtException", (err) => { 6 | // eslint-disable-next-line no-console 7 | console.error("worker thread uncaughtException", err); 8 | if (parentPort) { 9 | parentPort.postMessage(err); 10 | } 11 | }); 12 | 13 | async function main() { 14 | const { RoaringBitmap32 } = require("../.."); 15 | 16 | const bitmap = new RoaringBitmap32(); 17 | bitmap.add(1); 18 | bitmap.add(2); 19 | bitmap.add(100); 20 | bitmap.add(101); 21 | 22 | bitmap.addRange(105, 109); 23 | bitmap.addMany([0x7fffffff, 0xfffffffe, 0xffffffff]); 24 | 25 | assert.deepStrictEqual(bitmap.toArray(), [1, 2, 100, 101, 105, 106, 107, 108, 0x7fffffff, 0xfffffffe, 0xffffffff]); 26 | 27 | const serialized = await bitmap.serializeAsync("portable"); 28 | 29 | const bitmap2 = await RoaringBitmap32.deserializeAsync(serialized, "portable"); 30 | assert.deepStrictEqual(bitmap2.toArray(), [1, 2, 100, 101, 105, 106, 107, 108, 0x7fffffff, 0xfffffffe, 0xffffffff]); 31 | 32 | bitmap2.add(121); 33 | 34 | const bitmap3 = RoaringBitmap32.orMany([bitmap, bitmap2]); 35 | 36 | assert.deepStrictEqual( 37 | Array.from(bitmap3), 38 | [1, 2, 100, 101, 105, 106, 107, 108, 121, 0x7fffffff, 0xfffffffe, 0xffffffff], 39 | ); 40 | } 41 | 42 | main() 43 | .then(() => { 44 | if (parentPort) { 45 | parentPort.postMessage("ok"); 46 | } else { 47 | // eslint-disable-next-line no-console 48 | console.log("ok"); 49 | } 50 | }) 51 | .catch((e) => { 52 | // eslint-disable-next-line no-console 53 | console.error("worker thread error", e); 54 | if (parentPort) { 55 | parentPort.postMessage(e); 56 | } 57 | }); 58 | -------------------------------------------------------------------------------- /scripts/system-info.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | Object.defineProperty(exports, "__esModule", { value: true }); 4 | 5 | const colors = require("ansis"); 6 | const os = require("node:os"); 7 | 8 | let systemInfoCache = null; 9 | 10 | function getSystemInfo() { 11 | if (systemInfoCache) { 12 | return systemInfoCache; 13 | } 14 | 15 | let physicalCpuCount; 16 | 17 | try { 18 | physicalCpuCount = require("physical-cpu-count"); 19 | } catch (_e) { 20 | // Ignore error 21 | } 22 | if (!physicalCpuCount) { 23 | const cpus = os.cpus(); 24 | physicalCpuCount = Math.max(1, cpus.length / 2); 25 | } 26 | 27 | systemInfoCache = { physicalCpuCount }; 28 | 29 | return systemInfoCache; 30 | } 31 | 32 | function printSystemInfo() { 33 | console.log(); 34 | console.log( 35 | colors.whiteBright("Platform"), 36 | ":", 37 | colors.cyanBright(os.type()), 38 | colors.greenBright(os.release()), 39 | colors.yellowBright(process.arch), 40 | ); 41 | const cpus = os.cpus(); 42 | console.log(colors.whiteBright("CPU "), ":", colors.cyanBright(cpus[0].model)); 43 | console.log( 44 | colors.whiteBright("Cores "), 45 | ":", 46 | colors.cyanBright(getSystemInfo().physicalCpuCount), 47 | colors.cyan("physical"), 48 | "-", 49 | colors.greenBright(cpus.length), 50 | colors.green("logical"), 51 | ); 52 | console.log( 53 | colors.whiteBright("Memory "), 54 | ":", 55 | colors.cyanBright((os.totalmem() / 1073741824).toFixed(2)), 56 | colors.cyan("GB"), 57 | ); 58 | console.log( 59 | colors.whiteBright("NodeJS "), 60 | ":", 61 | colors.cyanBright(process.version), 62 | "-", 63 | colors.green("V8"), 64 | colors.greenBright(`v${process.versions.v8}`), 65 | ); 66 | console.log(); 67 | } 68 | 69 | module.exports.getSystemInfo = getSystemInfo; 70 | module.exports.printSystemInfo = printSystemInfo; 71 | 72 | if (require.main === module) { 73 | printSystemInfo(); 74 | } 75 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "[json]": { 3 | "editor.defaultFormatter": "biomejs.biome", 4 | "editor.formatOnSave": true 5 | }, 6 | "[jsonc]": { 7 | "editor.defaultFormatter": "biomejs.biome", 8 | "editor.formatOnSave": true 9 | }, 10 | "[html]": { 11 | "editor.defaultFormatter": "biomejs.biome", 12 | "editor.formatOnSave": true 13 | }, 14 | "[javascript]": { 15 | "editor.defaultFormatter": "biomejs.biome", 16 | "editor.formatOnSave": true 17 | }, 18 | "[javascriptreact]": { 19 | "editor.defaultFormatter": "biomejs.biome", 20 | "editor.formatOnSave": true 21 | }, 22 | "[typescript]": { 23 | "editor.defaultFormatter": "biomejs.biome", 24 | "editor.formatOnSave": true 25 | }, 26 | "[typescriptreact]": { 27 | "editor.defaultFormatter": "biomejs.biome", 28 | "editor.formatOnSave": true 29 | }, 30 | "editor.codeActionsOnSave": { 31 | "source.fixAll": "explicit" 32 | }, 33 | "[c]": { 34 | "editor.defaultFormatter": "xaver.clang-format" 35 | }, 36 | "[cpp]": { 37 | "editor.defaultFormatter": "xaver.clang-format" 38 | }, 39 | "clang-tidy.blacklist": ["submodules"], 40 | "clang-tidy.fixOnSave": true, 41 | "clang-tidy.compilerArgs": [ 42 | "-std=c++14", 43 | "-I/usr/local/include", 44 | "-I/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/include/c++/v1", 45 | "-I/usr/local/include/node", 46 | "-I/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/include" 47 | ], 48 | "editor.detectIndentation": false, 49 | "editor.formatOnSave": true, 50 | "editor.lineNumbers": "on", 51 | "editor.tabSize": 2, 52 | "javascript.format.semicolons": "insert", 53 | "typescript.format.semicolons": "insert", 54 | "typescript.tsdk": "node_modules/typescript/lib", 55 | "files.eol": "\n", 56 | "search.exclude": { 57 | "roaring-node.cpp": true, 58 | "**/node_modules": true, 59 | "**/bower_components": true, 60 | "**/*.code-search": true 61 | }, 62 | "cmake.configureOnOpen": false 63 | } 64 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # build output 2 | 3 | *.o 4 | native/ 5 | build/ 6 | package/ 7 | docs/ 8 | .tmp/ 9 | .github-key 10 | 11 | temp 12 | 13 | # Logs 14 | logs 15 | *.log 16 | npm-debug.log* 17 | yarn-debug.log* 18 | yarn-error.log* 19 | 20 | # Runtime data 21 | pids 22 | *.pid 23 | *.seed 24 | *.pid.lock 25 | 26 | # Directory for instrumented libs generated by jscoverage/JSCover 27 | lib-cov 28 | 29 | # Coverage directory used by tools like istanbul 30 | coverage 31 | 32 | # nyc test coverage 33 | .nyc_output 34 | 35 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 36 | .grunt 37 | 38 | # Bower dependency directory (https://bower.io/) 39 | bower_components 40 | 41 | # node-waf configuration 42 | .lock-wscript 43 | 44 | # Compiled binary addons (http://nodejs.org/api/addons.html) 45 | build/Release 46 | 47 | # Dependency directories 48 | **/node_modules/ 49 | node_modules/ 50 | jspm_packages/ 51 | 52 | # Typescript v1 declaration files 53 | typings/ 54 | 55 | # Optional npm cache directory 56 | .npm 57 | 58 | # Optional eslint cache 59 | .eslintcache 60 | 61 | # Optional REPL history 62 | .node_repl_history 63 | 64 | # Output of 'npm pack' 65 | *.tgz 66 | 67 | # Yarn Integrity file 68 | .yarn-integrity 69 | 70 | # dotenv environment variables file 71 | .env 72 | 73 | 74 | 75 | # General 76 | .DS_Store 77 | .AppleDouble 78 | .LSOverride 79 | 80 | # Icon must end with two \r 81 | Icon 82 | 83 | 84 | # Thumbnails 85 | ._* 86 | 87 | # Files that might appear in the root of a volume 88 | .DocumentRevisions-V100 89 | .fseventsd 90 | .Spotlight-V100 91 | .TemporaryItems 92 | .Trashes 93 | .VolumeIcon.icns 94 | .com.apple.timemachine.donotpresent 95 | 96 | # Directories potentially created on remote AFP share 97 | .AppleDB 98 | .AppleDesktop 99 | Network Trash Folder 100 | Temporary Items 101 | .apdisk 102 | 103 | # Windows thumbnail cache files 104 | Thumbs.db 105 | ehthumbs.db 106 | ehthumbs_vista.db 107 | 108 | # Dump file 109 | *.stackdump 110 | 111 | # Folder config file 112 | [Dd]esktop.ini 113 | 114 | # Recycle Bin used on file shares 115 | $RECYCLE.BIN/ 116 | 117 | # Windows Installer files 118 | *.cab 119 | *.msi 120 | *.msix 121 | *.msm 122 | *.msp 123 | 124 | # Windows shortcuts 125 | *.lnk 126 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | workflow_dispatch: 5 | pull_request: 6 | push: 7 | branches: 8 | - master 9 | 10 | jobs: 11 | test: 12 | name: test 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: actions/setup-node@v4 16 | with: 17 | node-version: "24.x" 18 | - uses: actions/checkout@v6 19 | - uses: actions/setup-python@v5 20 | with: 21 | python-version: "3.11" 22 | - name: Configure python for node-gyp 23 | run: node ./scripts/ensure-python-env.js 24 | - run: npm install --exact --no-audit --no-save 25 | env: 26 | ROARING_NODE_PRE_GYP: "false" 27 | - run: npm run lint 28 | - run: node ./node-pre-gyp.js 29 | env: 30 | ROARING_NODE_PRE_GYP: "custom-rebuild" 31 | - run: npm run test 32 | - run: node --expose-gc ./scripts/test-memory-leaks.js 33 | 34 | ci: 35 | needs: test 36 | strategy: 37 | matrix: 38 | node-version: ["20.9.0", "22.8.0", "24.5.0", "25.0.0"] 39 | os: [ubuntu-latest, macos-latest, windows-2022] 40 | runs-on: ${{ matrix.os }} 41 | steps: 42 | - uses: actions/setup-node@v4 43 | with: 44 | node-version: ${{ matrix.node-version }} 45 | - uses: actions/checkout@v6 46 | - uses: actions/setup-python@v5 47 | with: 48 | python-version: "3.11" 49 | - name: Configure python for node-gyp 50 | run: node ./scripts/ensure-python-env.js 51 | - run: npm install --exact --no-audit --no-save 52 | env: 53 | ROARING_NODE_PRE_GYP: "false" 54 | - run: node ./node-pre-gyp.js 55 | env: 56 | ROARING_NODE_PRE_GYP: "custom-rebuild" 57 | - run: npm run test 58 | 59 | ci-musl: 60 | needs: test 61 | strategy: 62 | matrix: 63 | node-version: ["20.9.0", "22.8.0", "24.10.0", "25.0.0"] 64 | runs-on: ubuntu-latest 65 | container: node:${{ matrix.node-version }}-alpine 66 | steps: 67 | - name: Install Alpine build deps 68 | run: apk add --no-cache python3 py3-setuptools make g++ git bash 69 | - uses: actions/checkout@v6 70 | - name: Configure python for node-gyp 71 | run: node ./scripts/ensure-python-env.js /usr/bin/python3 72 | - run: npm install --exact --no-audit --no-save 73 | env: 74 | ROARING_NODE_PRE_GYP: "false" 75 | - run: node ./node-pre-gyp.js 76 | env: 77 | ROARING_NODE_PRE_GYP: "custom-rebuild" 78 | - run: npm run test 79 | -------------------------------------------------------------------------------- /binding.gyp: -------------------------------------------------------------------------------- 1 | { 2 | "variables": { 3 | "openssl_fips": '' 4 | }, 5 | "targets": [ 6 | { 7 | "target_name": "<(module_name)", 8 | "product_dir": "<(module_path)", 9 | "default_configuration": "Release", 10 | "include_dirs": ["submodules/CRoaring/include"], 11 | "cflags_cc": [ 12 | "-O3", 13 | "-g0", 14 | "-std=c++20", 15 | "-fno-rtti", 16 | "-fno-exceptions", 17 | "-fvisibility=hidden", 18 | "-flto", 19 | "-DNDEBUG", 20 | "-ffunction-sections", 21 | "-fdata-sections", 22 | "-fomit-frame-pointer", 23 | "-fno-unwind-tables", 24 | "-fno-asynchronous-unwind-tables", 25 | "-Wno-unused-function", 26 | "-Wno-unused-variable", 27 | "-Wno-cast-function-type" 28 | ], 29 | "ldflags": ["-s", "-Wl,--gc-sections"], 30 | "xcode_settings": { 31 | "GCC_GENERATE_DEBUGGING_SYMBOLS": "NO", 32 | "OTHER_CFLAGS": [ 33 | "-O3", 34 | "-g0", 35 | "-std=c++20", 36 | "-fno-rtti", 37 | "-fno-exceptions", 38 | "-fvisibility=hidden", 39 | "-flto", 40 | "-DNDEBUG", 41 | "-ffunction-sections", 42 | "-fdata-sections", 43 | "-fomit-frame-pointer", 44 | "-fno-unwind-tables", 45 | "-fno-asynchronous-unwind-tables", 46 | "-Wno-unused-function", 47 | "-Wno-unused-variable", 48 | "-Wno-cast-function-type" 49 | ], 50 | "OTHER_LDFLAGS": ["-Wl,-dead_strip"] 51 | }, 52 | "msvs_settings": { 53 | "VCCLCompilerTool": { 54 | "DebugInformationFormat": 0, 55 | "Optimization": 3, 56 | "EnableFunctionLevelLinking": "true", 57 | "AdditionalOptions": ["/O2", "/std:c++latest", "/DNDEBUG", "/Gy", "/Gw"] 58 | }, 59 | "VCLinkerTool": { 60 | "OptimizeReferences": "2", 61 | "EnableCOMDATFolding": "2" 62 | } 63 | }, 64 | "sources": ["roaring-node.cpp"] 65 | } 66 | ] 67 | } 68 | -------------------------------------------------------------------------------- /src/cpp/croaring.cpp: -------------------------------------------------------------------------------- 1 | #define printf(...) ((void)0) 2 | #define fprintf(...) ((void)0) 3 | 4 | #if defined(__clang__) 5 | # pragma clang diagnostic push 6 | # pragma clang diagnostic ignored "-Wunused-variable" 7 | # pragma clang diagnostic ignored "-Wunused-but-set-variable" 8 | # pragma clang diagnostic ignored "-Wunused-but-set-parameter" 9 | # pragma clang diagnostic ignored "-Wmissing-field-initializers" 10 | # pragma clang diagnostic ignored "-Wunused-function" 11 | #elif defined(__GNUC__) 12 | # pragma GCC diagnostic push 13 | # pragma GCC diagnostic ignored "-Wunused-variable" 14 | # pragma GCC diagnostic ignored "-Wunused-but-set-variable" 15 | # pragma GCC diagnostic ignored "-Wunused-but-set-parameter" 16 | # pragma GCC diagnostic ignored "-Wmissing-field-initializers" 17 | # pragma GCC diagnostic ignored "-Wunused-function" 18 | #elif defined(_MSC_VER) 19 | # pragma warning(push) 20 | # pragma warning(disable: 4244) // possible loss of data 21 | #endif 22 | 23 | #ifdef small 24 | # undef small // on windows this seems to be defined to something... 25 | #endif 26 | 27 | #include "../../submodules/CRoaring/src/isadetection.c" 28 | #include "../../submodules/CRoaring/src/array_util.c" 29 | #include "../../submodules/CRoaring/src/bitset_util.c" 30 | #include "../../submodules/CRoaring/src/bitset.c" 31 | #include "../../submodules/CRoaring/src/containers/array.c" 32 | #include "../../submodules/CRoaring/src/containers/bitset.c" 33 | #include "../../submodules/CRoaring/src/containers/containers.c" 34 | #include "../../submodules/CRoaring/src/containers/convert.c" 35 | #include "../../submodules/CRoaring/src/containers/mixed_intersection.c" 36 | #include "../../submodules/CRoaring/src/containers/mixed_union.c" 37 | #include "../../submodules/CRoaring/src/containers/mixed_equal.c" 38 | #include "../../submodules/CRoaring/src/containers/mixed_subset.c" 39 | #include "../../submodules/CRoaring/src/containers/mixed_negation.c" 40 | #include "../../submodules/CRoaring/src/containers/mixed_xor.c" 41 | #include "../../submodules/CRoaring/src/containers/mixed_andnot.c" 42 | #include "../../submodules/CRoaring/src/containers/run.c" 43 | #include "../../submodules/CRoaring/src/memory.c" 44 | #include "../../submodules/CRoaring/src/roaring.c" 45 | #include "../../submodules/CRoaring/src/roaring_priority_queue.c" 46 | #include "../../submodules/CRoaring/src/roaring_array.c" 47 | 48 | #if defined(__clang__) 49 | # pragma clang diagnostic pop 50 | #elif defined(__GNUC__) 51 | # pragma GCC diagnostic pop 52 | #elif defined(_MSC_VER) 53 | # pragma warning(pop) 54 | #endif 55 | 56 | #undef printf 57 | #undef fprintf 58 | -------------------------------------------------------------------------------- /test/RoaringBitmap32/RoaringBitmap32.fromAsync.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from "vitest"; 2 | import RoaringBitmap32 from "../../RoaringBitmap32"; 3 | 4 | describe("RoaringBitmap32 fromAsync", () => { 5 | describe("awayt/async", () => { 6 | describe("empty", () => { 7 | it("creates an empty bitmap with undefined", async () => { 8 | const bitmap = await RoaringBitmap32.fromArrayAsync(undefined as any); 9 | expect(bitmap).to.be.instanceOf(RoaringBitmap32); 10 | expect(bitmap.isEmpty).eq(true); 11 | }); 12 | 13 | it("creates an empty bitmap with null", async () => { 14 | const bitmap = await RoaringBitmap32.fromArrayAsync(null as any); 15 | expect(bitmap).to.be.instanceOf(RoaringBitmap32); 16 | expect(bitmap.isEmpty).eq(true); 17 | }); 18 | 19 | it("creates an empty bitmap with an empty Uint32Array", async () => { 20 | const bitmap = await RoaringBitmap32.fromArrayAsync(new Uint32Array(0)); 21 | expect(bitmap).to.be.instanceOf(RoaringBitmap32); 22 | expect(bitmap.isEmpty).eq(true); 23 | }); 24 | 25 | it("creates an empty bitmap with an empty Int32Array", async () => { 26 | const bitmap = await RoaringBitmap32.fromArrayAsync(new Int32Array(0)); 27 | expect(bitmap).to.be.instanceOf(RoaringBitmap32); 28 | expect(bitmap.isEmpty).eq(true); 29 | }); 30 | 31 | it("creates an empty bitmap with an empty array", async () => { 32 | const bitmap = await RoaringBitmap32.fromArrayAsync([]); 33 | expect(bitmap).to.be.instanceOf(RoaringBitmap32); 34 | expect(bitmap.isEmpty).eq(true); 35 | }); 36 | }); 37 | 38 | describe("with data", () => { 39 | it("creates a bitmap with an Uint32Array", async () => { 40 | const values = [1, 2, 3, 4, 5]; 41 | const bitmap = await RoaringBitmap32.fromArrayAsync(new Uint32Array(values)); 42 | expect(bitmap).to.be.instanceOf(RoaringBitmap32); 43 | expect(bitmap.toArray()).deep.equal(values); 44 | }); 45 | 46 | it("creates a bitmap with an Int32Array", async () => { 47 | const values = [1, 2, 3, 4, 5]; 48 | const bitmap = await RoaringBitmap32.fromArrayAsync(new Int32Array(values)); 49 | expect(bitmap).to.be.instanceOf(RoaringBitmap32); 50 | expect(bitmap.toArray()).deep.equal(values); 51 | }); 52 | 53 | it("creates a bitmap with an array", async () => { 54 | const values = [1, 2, 3, 0x7fffffff, 0xffffffff]; 55 | const bitmap = await RoaringBitmap32.fromArrayAsync(values); 56 | expect(bitmap).to.be.instanceOf(RoaringBitmap32); 57 | expect(bitmap.toArray()).deep.equal(values); 58 | }); 59 | }); 60 | }); 61 | }); 62 | -------------------------------------------------------------------------------- /.clang-tidy: -------------------------------------------------------------------------------- 1 | --- 2 | Checks: ' 3 | -*, 4 | bugprone-*, 5 | cert-*, 6 | clang-analyzer-*, 7 | hicpp-multiway-paths-covered, 8 | hicpp-signed-bitwise, 9 | misc-*, 10 | modernize-*, 11 | performance-*, 12 | portability-*, 13 | readability-*, 14 | cppcoreguidelines-*, 15 | modernize-avoid-c-arrays, 16 | google-explicit-constructor, 17 | -cert-dcl37-c, 18 | -cert-dcl16-c, 19 | -cert-dcl51-cpp, 20 | -cert-dcl58-cpp, 21 | -cert-err60-cpp, 22 | -modernize-deprecated-headers, 23 | -bugprone-reserved-identifier, 24 | -cppcoreguidelines-pro-bounds-array-to-pointer-decay, 25 | -readability-identifier-naming, 26 | -cppcoreguidelines-avoid-non-const-global-variables, 27 | -cppcoreguidelines-avoid-magic-numbers, 28 | -cppcoreguidelines-owning-memory, 29 | -readability-magic-numbers, 30 | -modernize-use-trailing-return-type, 31 | -readability-named-parameter, 32 | -readability-function-cognitive-complexity, 33 | -misc-non-private-member-variables-in-classes, 34 | -modernize-use-nodiscard, 35 | -modernize-avoid-c-arrays, 36 | -misc-no-recursion, 37 | -cppcoreguidelines-pro-type-reinterpret-cast, 38 | -cppcoreguidelines-pro-bounds-constant-array-index, 39 | -bugprone-easily-swappable-parameters, 40 | -cppcoreguidelines-pro-bounds-pointer-arithmetic, 41 | -misc-unused-using-decls, 42 | -cppcoreguidelines-non-private-member-variables-in-classes, 43 | -readability-braces-around-statements, 44 | -cppcoreguidelines-no-malloc, 45 | -cppcoreguidelines-pro-type-vararg, 46 | -cert-err58-cpp, 47 | -cppcoreguidelines-init-variables, 48 | -modernize-loop-convert, 49 | -cppcoreguidelines-macro-usage, 50 | -bugprone-implicit-widening-of-multiplication-result, 51 | -cert-oop54-cpp, 52 | -bugprone-suspicious-include, 53 | -cppcoreguidelines-avoid-c-arrays, 54 | -readability-isolate-declaration, 55 | -cppcoreguidelines-special-member-functions, 56 | -modernize-use-default-member-init, 57 | -modernize-use-equals-default, 58 | -cppcoreguidelines-pro-type-member-init, 59 | -bugprone-branch-clone, 60 | -misc-unused-parameters, 61 | -modernize-use-using' 62 | 63 | WarningsAsErrors: '' 64 | AnalyzeTemporaryDtors: false 65 | InheritParentConfig: false 66 | 67 | CheckOptions: 68 | - { key: hicpp-signed-bitwise.IgnorePositiveIntegerLiterals, value: true } 69 | - { key: readability-braces-around-statements.ShortStatementLines, value: 1 } 70 | - { key: cppcoreguidelines-special-member-functions.AllowSoleDefaultDtor, value: true } 71 | - { key: cppcoreguidelines-special-member-functions.AllowMissingMoveFunctions, value: true } 72 | - { key: cppcoreguidelines-special-member-functions.AllowMissingMoveFunctionsWhenCopyIsDeleted, value: true } 73 | - { key: modernize-use-override.IgnoreDestructors, value: 1 } 74 | 75 | --- 76 | 77 | -------------------------------------------------------------------------------- /src/cpp/mmap.h: -------------------------------------------------------------------------------- 1 | #ifndef ROARING_NODE_MMAP_ 2 | #define ROARING_NODE_MMAP_ 3 | 4 | #if defined(_WIN32) || defined(__MINGW32__) || defined(__MINGW64__) 5 | 6 | /* mmap() replacement for Windows 7 | * 8 | * Author: Mike Frysinger 9 | * Placed into the public domain 10 | */ 11 | 12 | /* References: 13 | * CreateFileMapping: http://msdn.microsoft.com/en-us/library/aa366537(VS.85).aspx 14 | * CloseHandle: http://msdn.microsoft.com/en-us/library/ms724211(VS.85).aspx 15 | * MapViewOfFile: http://msdn.microsoft.com/en-us/library/aa366761(VS.85).aspx 16 | * UnmapViewOfFile: http://msdn.microsoft.com/en-us/library/aa366882(VS.85).aspx 17 | */ 18 | 19 | # include 20 | # include 21 | # include 22 | 23 | # define PROT_READ 0x1 24 | # define PROT_WRITE 0x2 25 | /* This flag is only available in WinXP+ */ 26 | # ifdef FILE_MAP_EXECUTE 27 | # define PROT_EXEC 0x4 28 | # else 29 | # define PROT_EXEC 0x0 30 | # define FILE_MAP_EXECUTE 0 31 | # endif 32 | 33 | # define MAP_SHARED 0x01 34 | # define MAP_PRIVATE 0x02 35 | # define MAP_ANONYMOUS 0x20 36 | # define MAP_ANON MAP_ANONYMOUS 37 | # define MAP_FAILED ((void *)-1) 38 | 39 | # ifdef __USE_FILE_OFFSET64 40 | # define DWORD_HI(x) (x >> 32) 41 | # define DWORD_LO(x) ((x)&0xffffffff) 42 | # else 43 | # define DWORD_HI(x) (0) 44 | # define DWORD_LO(x) (x) 45 | # endif 46 | 47 | static void * mmap(void * start, size_t length, int prot, int flags, int fd, off_t offset) { 48 | if (prot & ~(PROT_READ | PROT_WRITE | PROT_EXEC)) return MAP_FAILED; 49 | if (fd == -1) { 50 | if (!(flags & MAP_ANON) || offset) return MAP_FAILED; 51 | } else if (flags & MAP_ANON) 52 | return MAP_FAILED; 53 | 54 | DWORD flProtect; 55 | if (prot & PROT_WRITE) { 56 | if (prot & PROT_EXEC) 57 | flProtect = PAGE_EXECUTE_READWRITE; 58 | else 59 | flProtect = PAGE_READWRITE; 60 | } else if (prot & PROT_EXEC) { 61 | if (prot & PROT_READ) 62 | flProtect = PAGE_EXECUTE_READ; 63 | else if (prot & PROT_EXEC) 64 | flProtect = PAGE_EXECUTE; 65 | } else 66 | flProtect = PAGE_READONLY; 67 | 68 | off_t end = length + offset; 69 | HANDLE mmap_fd, h; 70 | if (fd == -1) 71 | mmap_fd = INVALID_HANDLE_VALUE; 72 | else 73 | mmap_fd = (HANDLE)_get_osfhandle(fd); 74 | h = CreateFileMapping(mmap_fd, NULL, flProtect, DWORD_HI(end), DWORD_LO(end), NULL); 75 | if (h == NULL) return MAP_FAILED; 76 | 77 | DWORD dwDesiredAccess; 78 | if (prot & PROT_WRITE) 79 | dwDesiredAccess = FILE_MAP_WRITE; 80 | else 81 | dwDesiredAccess = FILE_MAP_READ; 82 | if (prot & PROT_EXEC) dwDesiredAccess |= FILE_MAP_EXECUTE; 83 | if (flags & MAP_PRIVATE) dwDesiredAccess |= FILE_MAP_COPY; 84 | void * ret = MapViewOfFile(h, dwDesiredAccess, DWORD_HI(offset), DWORD_LO(offset), length); 85 | if (ret == NULL) { 86 | CloseHandle(h); 87 | ret = MAP_FAILED; 88 | } 89 | return ret; 90 | } 91 | 92 | static void munmap(void * addr, size_t length) { 93 | UnmapViewOfFile(addr); 94 | /* ruh-ro, we leaked handle from CreateFileMapping() ... */ 95 | } 96 | 97 | # undef DWORD_HI 98 | # undef DWORD_LO 99 | 100 | #else 101 | 102 | # include 103 | # include 104 | 105 | #endif 106 | #endif 107 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "roaring", 3 | "version": "2.7.0", 4 | "private": false, 5 | "description": "CRoaring official port for NodeJS", 6 | "keywords": [ 7 | "CRoaring", 8 | "Roaring", 9 | "bitmaps" 10 | ], 11 | "type": "commonjs", 12 | "license": "Apache-2.0", 13 | "author": "Salvatore Previti", 14 | "homepage": "https://github.com/SalvatorePreviti/roaring-node#readme", 15 | "bugs": { 16 | "url": "https://github.com/SalvatorePreviti/roaring/issues" 17 | }, 18 | "documentation": [ 19 | "https://salvatorepreviti.github.io/roaring-node/modules.html", 20 | "https://salvatorepreviti.github.io/roaring-node/classes/RoaringBitmap32.html" 21 | ], 22 | "repository": { 23 | "type": "git", 24 | "url": "git+https://github.com/SalvatorePreviti/roaring-node.git" 25 | }, 26 | "engines": { 27 | "node": ">=18.20.8" 28 | }, 29 | "main": "index.js", 30 | "types": "index.d.ts", 31 | "typings": "index.d.ts", 32 | "files": [ 33 | "roaring-node.cpp", 34 | "LICENSE.md", 35 | "README.md", 36 | "binding.gyp", 37 | "index.js", 38 | "build.js", 39 | "index.d.ts", 40 | "node-pre-gyp.js", 41 | "RoaringBitmap32.js", 42 | "RoaringBitmap32.d.ts", 43 | "RoaringBitmap32Iterator.js", 44 | "RoaringBitmap32Iterator.d.ts", 45 | "RoaringBitmap32ReverseIterator.js", 46 | "RoaringBitmap32ReverseIterator.d.ts" 47 | ], 48 | "binary": { 49 | "module_name": "roaring", 50 | "module_path": "./native/roaring-{node_abi}-{platform}-{arch}-{libc}", 51 | "package_name": "roaring-v{version}-{node_abi}-{platform}-{arch}-{libc}.tar.gz", 52 | "host": "https://github.com/SalvatorePreviti/roaring-node/releases/download/", 53 | "remote_path": "{version}" 54 | }, 55 | "scripts": { 56 | "install": "node ./node-pre-gyp.js install --fallback-to-build", 57 | "compile": "node ./node-pre-gyp.js rebuild", 58 | "build": "node ./scripts/build.js", 59 | "update-roaring": "./scripts/update-roaring.sh", 60 | "build-dev": "node ./scripts/build.js --dev", 61 | "test": "vitest run", 62 | "lint": "biome check --diagnostic-level=error --files-ignore-unknown=true . && tsc --noEmit", 63 | "lint:fix": "biome check --write --files-ignore-unknown=true . && tsc --noEmit", 64 | "doc": "typedoc ./index.d.ts", 65 | "benchmarks": "vitest bench --run" 66 | }, 67 | "husky": { 68 | "hooks": { 69 | "pre-commit": "lint-staged" 70 | } 71 | }, 72 | "dependencies": { 73 | "@mapbox/node-pre-gyp": "^2.0.3" 74 | }, 75 | "peerDependencies": { 76 | "node-gyp": "^12.1.0" 77 | }, 78 | "peerDependenciesMeta": { 79 | "node-gyp": { 80 | "optional": true 81 | } 82 | }, 83 | "optionalDependencies": { 84 | "node-gyp": "^12.1.0" 85 | }, 86 | "devDependencies": { 87 | "@biomejs/biome": "^2.3.10", 88 | "@octokit/rest": "^22.0.1", 89 | "@types/fastbitset": "^0.2.2", 90 | "@types/node": "^25.0.3", 91 | "ansis": "^4.2.0", 92 | "esbuild": "^0.27.2", 93 | "fastbitset": "^0.4.1", 94 | "husky": "^9.1.7", 95 | "lint-staged": "^16.2.7", 96 | "physical-cpu-count": "^2.0.0", 97 | "tslib": "^2.8.1", 98 | "tsx": "^4.21.0", 99 | "typedoc": "^0.28.15", 100 | "typescript": "5.9.3", 101 | "vitest": "^4.0.16" 102 | }, 103 | "gypfile": true, 104 | "roaring_version": "4.5.0" 105 | } 106 | -------------------------------------------------------------------------------- /test/RoaringBitmap32/RoaringBitmap32.deserializeParallelAsync.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from "vitest"; 2 | import RoaringBitmap32 from "../../RoaringBitmap32"; 3 | 4 | describe("RoaringBitmap32 deserializeParallelAsync", () => { 5 | describe("async/await", () => { 6 | describe("one empty buffer", () => { 7 | it("deserializes an empty buffer (non portable)", async () => { 8 | const bitmap = await RoaringBitmap32.deserializeParallelAsync([Buffer.from([])], false); 9 | expect(bitmap).to.have.lengthOf(1); 10 | expect(bitmap[0]).to.be.instanceOf(RoaringBitmap32); 11 | expect(bitmap[0].size).eq(0); 12 | }); 13 | 14 | it("deserializes an empty buffer (portable)", async () => { 15 | const bitmap = await RoaringBitmap32.deserializeParallelAsync([Buffer.from([])], true); 16 | expect(bitmap).to.have.lengthOf(1); 17 | expect(bitmap[0]).to.be.instanceOf(RoaringBitmap32); 18 | expect(bitmap[0].size).eq(0); 19 | }); 20 | }); 21 | 22 | describe("multiple empty buffers", () => { 23 | it("deserializes an empty buffer (non portable)", async () => { 24 | const bitmap = await RoaringBitmap32.deserializeParallelAsync( 25 | [Buffer.from([]), Buffer.from([]), Buffer.from([])], 26 | false, 27 | ); 28 | expect(bitmap).to.have.lengthOf(3); 29 | for (let i = 0; i < 3; ++i) { 30 | expect(bitmap[i]).to.be.instanceOf(RoaringBitmap32); 31 | expect(bitmap[i].size).eq(0); 32 | } 33 | }); 34 | 35 | it("deserializes an empty buffer (portable)", async () => { 36 | const bitmap = await RoaringBitmap32.deserializeParallelAsync( 37 | [Buffer.from([]), Buffer.from([]), Buffer.from([])], 38 | true, 39 | ); 40 | expect(bitmap).to.have.lengthOf(3); 41 | for (let i = 0; i < 3; ++i) { 42 | expect(bitmap[i]).to.be.instanceOf(RoaringBitmap32); 43 | expect(bitmap[i].size).eq(0); 44 | } 45 | }); 46 | }); 47 | 48 | it("deserializes multiple buffers (non portable)", async () => { 49 | const sources = []; 50 | for (let i = 0; i < 10; ++i) { 51 | const array = [i, i + 10, i * 10000, i * 999999]; 52 | for (let j = 0; j < i; ++j) { 53 | array.push(j); 54 | } 55 | sources.push(new RoaringBitmap32(array)); 56 | } 57 | 58 | const result = await RoaringBitmap32.deserializeParallelAsync( 59 | sources.map((x) => x.serialize(false)), 60 | false, 61 | ); 62 | expect(result).to.have.lengthOf(sources.length); 63 | for (let i = 0; i < sources.length; ++i) { 64 | expect(result[i].toArray()).deep.equal(sources[i].toArray()); 65 | } 66 | }); 67 | 68 | it("deserializes multiple buffers (portable)", async () => { 69 | const sources = []; 70 | for (let i = 0; i < 10; ++i) { 71 | const array = [i, i + 10, i * 10000, i * 999999]; 72 | for (let j = 0; j < i; ++j) { 73 | array.push(j); 74 | } 75 | sources.push(new RoaringBitmap32(array)); 76 | } 77 | 78 | const result = await RoaringBitmap32.deserializeParallelAsync( 79 | sources.map((x) => x.serialize(true)), 80 | true, 81 | ); 82 | expect(result).to.have.lengthOf(sources.length); 83 | for (let i = 0; i < sources.length; ++i) { 84 | expect(result[i].toArray()).deep.equal(sources[i].toArray()); 85 | } 86 | }); 87 | }); 88 | }); 89 | -------------------------------------------------------------------------------- /scripts/test-memory-leaks.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node --expose-gc 2 | 3 | /* eslint-disable node/no-unsupported-features/node-builtins */ 4 | /* eslint-disable no-console */ 5 | /* global gc */ 6 | 7 | const { runMain } = require("./lib/utils"); 8 | 9 | const _gc = typeof gc !== "undefined" ? gc : () => {}; 10 | 11 | async function testMemoryLeaks() { 12 | _gc(); 13 | _gc(); 14 | 15 | const { RoaringBitmap32, getRoaringUsedMemory } = require("../"); 16 | 17 | _gc(); 18 | _gc(); 19 | 20 | console.log(); 21 | console.log("RoaringUsedMemory", getRoaringUsedMemory()); 22 | console.log("RoaringBitmap32.getInstancesCount", RoaringBitmap32.getInstancesCount()); 23 | 24 | _gc(); 25 | _gc(); 26 | 27 | let a = new RoaringBitmap32([1, 2, 3]); 28 | let b = new RoaringBitmap32([1, 2]); 29 | 30 | console.time("buildup"); 31 | 32 | const operation = (i) => { 33 | switch (i % 4) { 34 | case 1: 35 | return RoaringBitmap32.and(a, b); 36 | case 2: 37 | return RoaringBitmap32.xor(a, b); 38 | case 3: 39 | return RoaringBitmap32.andNot(a, b); 40 | default: 41 | return RoaringBitmap32.or(a, b); 42 | } 43 | }; 44 | 45 | let promises = []; 46 | for (let i = 0; i < 10000; i++) { 47 | const bmp = operation(i); 48 | if (i < 5) { 49 | promises.push(bmp.toUint32ArrayAsync()); 50 | } 51 | promises.push(bmp.serializeAsync(i & 4 ? "croaring" : i & 8 ? "portable" : "unsafe_frozen_croaring")); 52 | } 53 | 54 | console.log(); 55 | console.log("RoaringUsedMemory", getRoaringUsedMemory()); 56 | console.log("RoaringBitmap32.getInstancesCount", RoaringBitmap32.getInstancesCount()); 57 | console.log(); 58 | 59 | await Promise.all(promises); 60 | promises = null; 61 | 62 | console.timeEnd("buildup"); 63 | console.log(); 64 | 65 | _gc(); 66 | _gc(); 67 | 68 | console.table(process.memoryUsage()); 69 | process.stdout.write("\nallocating"); 70 | console.time("allocation"); 71 | 72 | _gc(); 73 | _gc(); 74 | 75 | promises = []; 76 | for (let i = 0; i < 10000000; i++) { 77 | const bmp = operation(i); 78 | if (i % 100000 === 0) { 79 | process.stdout.write("."); 80 | _gc(); 81 | _gc(); 82 | promises.push(bmp.serializeAsync("croaring").then(() => undefined)); 83 | } 84 | } 85 | await Promise.all(promises); 86 | promises = null; 87 | process.stdout.write("\n"); 88 | console.timeEnd("allocation"); 89 | 90 | console.log(); 91 | console.log("RoaringUsedMemory", getRoaringUsedMemory()); 92 | console.log("RoaringBitmap32.getInstancesCount", RoaringBitmap32.getInstancesCount()); 93 | console.log(); 94 | 95 | a = null; 96 | b = null; 97 | 98 | const promise = new Promise((resolve, reject) => { 99 | for (let i = 0; i < 10; ++i) { 100 | _gc(); 101 | } 102 | setTimeout(() => { 103 | for (let i = 0; i < 10; ++i) { 104 | _gc(); 105 | } 106 | setTimeout(() => { 107 | for (let i = 0; i < 10; ++i) { 108 | _gc(); 109 | } 110 | console.table(process.memoryUsage()); 111 | console.log(); 112 | console.log("RoaringUsedMemory", getRoaringUsedMemory()); 113 | console.log("RoaringBitmap32.getInstancesCount", RoaringBitmap32.getInstancesCount()); 114 | console.log(); 115 | 116 | setTimeout(() => { 117 | if (getRoaringUsedMemory() !== 0) { 118 | reject(new Error(`Memory leak detected. ${getRoaringUsedMemory()} bytes are still allocated.`)); 119 | } 120 | 121 | if (RoaringBitmap32.getInstancesCount() !== 0) { 122 | reject( 123 | new Error(`Memory leak detected. ${RoaringBitmap32.getInstancesCount()} instances are still allocated.`), 124 | ); 125 | } 126 | resolve(); 127 | }); 128 | }, 150); 129 | }, 150); 130 | }); 131 | 132 | await promise; 133 | } 134 | 135 | runMain(testMemoryLeaks, "test-memory-leaks"); 136 | -------------------------------------------------------------------------------- /scripts/build.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const colors = require("ansis"); 4 | const path = require("node:path"); 5 | const fs = require("node:fs"); 6 | const { 7 | runMain, 8 | forkAsync, 9 | spawnAsync, 10 | CPP_UNITY_FILE_PATH, 11 | ROOT_FOLDER, 12 | getBinaryOutputFilePath, 13 | NPM_COMMAND, 14 | } = require("./lib/utils"); 15 | const { unity } = require("./lib/unity"); 16 | 17 | async function development() { 18 | console.log(); 19 | const warningMessage = "Development mode is enabled. Rebuild for production before publishing."; 20 | console.warn(colors.yellowBright.underline.bold("WARNING") + colors.yellow(`: ${warningMessage}`)); 21 | console.log(); 22 | let outputText = ""; 23 | outputText += "#define ROARING_NODE_DEV 1\n\n"; 24 | outputText += `#pragma message (${JSON.stringify(warningMessage)})\n`; 25 | outputText += '#include "src/cpp/main.cpp"\n'; 26 | 27 | let oldContent; 28 | try { 29 | oldContent = fs.readFileSync(CPP_UNITY_FILE_PATH, "utf8"); 30 | } catch {} 31 | if (oldContent !== outputText) { 32 | fs.writeFileSync(path.resolve(CPP_UNITY_FILE_PATH), outputText, "utf8"); 33 | console.log(colors.yellow(`- ${path.relative(ROOT_FOLDER, CPP_UNITY_FILE_PATH)} updated`)); 34 | } else { 35 | console.log(colors.blackBright(`- ${path.relative(ROOT_FOLDER, CPP_UNITY_FILE_PATH)} is up to date`)); 36 | } 37 | } 38 | 39 | async function build() { 40 | if ( 41 | process.argv.includes("dev") || 42 | process.argv.includes("--dev") || 43 | process.argv.includes("development") || 44 | process.argv.includes("--development") 45 | ) { 46 | console.time("Development mode"); 47 | await development(); 48 | console.timeEnd("Development mode"); 49 | } else { 50 | console.time("Unity build"); 51 | const unityResult = unity(); 52 | 53 | let oldContent; 54 | try { 55 | oldContent = fs.readFileSync(CPP_UNITY_FILE_PATH, "utf8"); 56 | } catch {} 57 | if (oldContent !== unityResult.outputText) { 58 | fs.writeFileSync(path.resolve(CPP_UNITY_FILE_PATH), unityResult.outputText, "utf8"); 59 | console.log(colors.yellow(`- ${path.relative(ROOT_FOLDER, CPP_UNITY_FILE_PATH)} updated`)); 60 | } else { 61 | console.log(colors.blackBright(`- ${path.relative(ROOT_FOLDER, CPP_UNITY_FILE_PATH)} is up to date`)); 62 | } 63 | 64 | const packageJsonPath = path.resolve(ROOT_FOLDER, "package.json"); 65 | const oldPackageJson = fs.readFileSync(packageJsonPath, "utf8"); 66 | const pkg = JSON.parse(oldPackageJson); 67 | pkg.roaring_version = unityResult.roaringVersion; 68 | const newPackageJson = `${JSON.stringify(pkg, null, 2)}\n`; 69 | 70 | if (newPackageJson !== oldPackageJson) { 71 | fs.writeFileSync(packageJsonPath, newPackageJson); 72 | console.log(colors.yellow("- package.json updated")); 73 | } else { 74 | console.log(colors.blackBright("- package.json is up to date")); 75 | } 76 | 77 | console.timeEnd("Unity build"); 78 | } 79 | console.log(); 80 | 81 | process.on("exit", () => { 82 | console.log(); 83 | 84 | const BINARY_OUTPUT_FILE_PATH = getBinaryOutputFilePath(); 85 | if (fs.existsSync(BINARY_OUTPUT_FILE_PATH)) { 86 | console.log(); 87 | console.log( 88 | colors.green( 89 | `* ${path.relative(ROOT_FOLDER, BINARY_OUTPUT_FILE_PATH)} compiled. ${ 90 | fs.statSync(BINARY_OUTPUT_FILE_PATH).size 91 | } bytes`, 92 | ), 93 | ); 94 | } 95 | console.log(); 96 | }); 97 | 98 | if (!process.argv.includes("--no-compile")) { 99 | await forkAsync(path.resolve(ROOT_FOLDER, "node-pre-gyp.js"), [], { 100 | env: { 101 | ...process.env, 102 | ROARING_NODE_PRE_GYP: "custom-rebuild", 103 | }, 104 | }); 105 | await spawnAsync(NPM_COMMAND, ["run", "test"], { 106 | env: process.env, 107 | }); 108 | } 109 | } 110 | 111 | module.exports.build = build; 112 | 113 | if (require.main === module) { 114 | runMain(build, "build"); 115 | } 116 | -------------------------------------------------------------------------------- /scripts/lib/unity.js: -------------------------------------------------------------------------------- 1 | const colors = require("ansis"); 2 | const path = require("node:path"); 3 | 4 | const fs = require("node:fs"); 5 | 6 | const { CPP_SRC_FOLDER_PATH } = require("./utils"); 7 | 8 | const PROJECT_ROOT = path.resolve(__dirname, "..", ".."); 9 | 10 | function toLineDirectivePath(filePath) { 11 | const normalizedAbsolutePath = filePath.split(path.sep).join("/"); 12 | const relativePath = path.relative(PROJECT_ROOT, filePath); 13 | if (!relativePath || relativePath.startsWith("..")) { 14 | return normalizedAbsolutePath; 15 | } 16 | return relativePath.split(path.sep).join("/"); 17 | } 18 | 19 | const INCLUDE_REGEX = /^#\s*include\s+["<](.+)[">]/; 20 | const ROARING_VERSION_REGEX = /#\s*define\s+ROARING_VERSION\s+"(.+)"/; 21 | 22 | module.exports.unity = function unity() { 23 | const existsCache = new Map(); 24 | 25 | const exists = (filePath) => { 26 | let result = existsCache.get(filePath); 27 | if (result !== undefined) { 28 | return result; 29 | } 30 | result = fs.existsSync(filePath); 31 | existsCache.set(filePath, result); 32 | return result; 33 | }; 34 | 35 | const includedFiles = new Set(); 36 | let roaringVersion = null; 37 | 38 | const output = ['// This file is generated by "scripts/build.js". Do not edit it directly.', ""]; 39 | 40 | let pendingLineDirective = null; 41 | 42 | const scheduleLineDirective = (filePath, lineNumber) => { 43 | pendingLineDirective = { filePath, lineNumber }; 44 | }; 45 | 46 | const pushLine = (line) => { 47 | if (pendingLineDirective) { 48 | const { filePath, lineNumber } = pendingLineDirective; 49 | output.push(`#line ${lineNumber} "${toLineDirectivePath(filePath)}"`); 50 | pendingLineDirective = null; 51 | } 52 | output.push(line); 53 | }; 54 | 55 | function processFile(filePath) { 56 | const content = fs.readFileSync(filePath, "utf8"); 57 | const lines = content.split(/\r?\n/); 58 | 59 | scheduleLineDirective(filePath, 1); 60 | 61 | for (let index = 0; index < lines.length; index += 1) { 62 | const lineNumber = index + 1; 63 | const line = lines[index]; 64 | const includeMatch = line.match(INCLUDE_REGEX); 65 | if (includeMatch) { 66 | const includeName = includeMatch[1]; 67 | 68 | let includePath; 69 | if (includeName.startsWith("roaring/") && line.includes("<")) { 70 | includePath = path.resolve(__dirname, "../../submodules/CRoaring/include", includeName); 71 | } else { 72 | includePath = path.resolve(path.dirname(filePath), includeName); 73 | } 74 | if (exists(includePath)) { 75 | if (!includedFiles.has(includePath)) { 76 | includedFiles.add(includePath); 77 | processFile(includePath); 78 | } 79 | if (index + 1 < lines.length) { 80 | scheduleLineDirective(filePath, lineNumber + 1); 81 | } 82 | continue; 83 | } 84 | } else if (!roaringVersion) { 85 | const match = line.match(ROARING_VERSION_REGEX); 86 | if (match) { 87 | roaringVersion = match[1]; 88 | if (!/^[0-9]+\.[0-9]+\.[0-9]+$/.test(roaringVersion)) { 89 | throw new Error(`Invalid roaring version ${roaringVersion}`); 90 | } 91 | } 92 | } 93 | pushLine(line); 94 | } 95 | } 96 | 97 | processFile(path.resolve(CPP_SRC_FOLDER_PATH, "main.cpp")); 98 | 99 | console.log(); 100 | console.log(colors.cyan(`- roaring version ${roaringVersion}`)); 101 | 102 | let outputText = output.join("\n"); 103 | 104 | // This is to fix compiling C code with C++ compiler on Windows 2019, to avoid the error 105 | // C4576: a parenthesized type followed by an initializer list is a non-standard explicit type conversion syntax 106 | outputText = outputText.replaceAll("(roaring_container_iterator_t){", "roaring_container_iterator_t{"); 107 | 108 | console.log(colors.cyanBright(`- Unity: ${includedFiles.size} files included. ${outputText.length} bytes total.`)); 109 | 110 | return { 111 | roaringVersion, 112 | outputText, 113 | }; 114 | }; 115 | -------------------------------------------------------------------------------- /scripts/lib/utils.js: -------------------------------------------------------------------------------- 1 | const colors = require("ansis"); 2 | const util = require("node:util"); 3 | const fs = require("node:fs"); 4 | const path = require("node:path"); 5 | const { spawn, fork } = require("node:child_process"); 6 | 7 | const ROOT_FOLDER = path.resolve(__dirname, "../../"); 8 | const CPP_SRC_FOLDER_PATH = path.resolve(ROOT_FOLDER, "src/cpp"); 9 | const CPP_UNITY_FILE_PATH = path.resolve(ROOT_FOLDER, "roaring-node.cpp"); 10 | const NPM_COMMAND = process.platform === "win32" ? "npm.cmd" : "npm"; 11 | 12 | let BINARY_OUTPUT_FILE_PATH; 13 | 14 | function getBinaryOutputFilePath() { 15 | if (!BINARY_OUTPUT_FILE_PATH) { 16 | BINARY_OUTPUT_FILE_PATH = require("@mapbox/node-pre-gyp/lib/pre-binding").find( 17 | path.join(ROOT_FOLDER, "package.json"), 18 | ); 19 | if (!fs.existsSync(BINARY_OUTPUT_FILE_PATH)) { 20 | BINARY_OUTPUT_FILE_PATH = path.resolve(ROOT_FOLDER, "build", "Release", "roaring.node"); 21 | } 22 | } 23 | return BINARY_OUTPUT_FILE_PATH; 24 | } 25 | 26 | module.exports = { 27 | ROOT_FOLDER, 28 | CPP_SRC_FOLDER_PATH, 29 | CPP_UNITY_FILE_PATH, 30 | 31 | getBinaryOutputFilePath, 32 | 33 | runMain, 34 | spawnAsync, 35 | forkAsync, 36 | mergeDirs, 37 | NPM_COMMAND, 38 | }; 39 | 40 | function runMain(fn, title) { 41 | if (title) { 42 | console.log(colors.blueBright(`\n⬢ ${colors.cyanBright(title)}\n`)); 43 | } 44 | 45 | if (!fn.name) { 46 | Reflect.defineProperty(fn, "name", { 47 | value: "main", 48 | configurable: true, 49 | enumerable: false, 50 | writable: false, 51 | }); 52 | } 53 | 54 | const totalTime = () => (title ? `${colors.cyan(title)}` : "") + colors.italic(` ⌚ ${process.uptime().toFixed(2)}s`); 55 | 56 | let _processCompleted = false; 57 | 58 | const processCompleted = () => { 59 | if (!process.exitCode) { 60 | console.log(colors.greenBright("✅ OK"), totalTime()); 61 | } else { 62 | console.log(colors.redBright(`❌ Failed: exitCode${process.exitCode}`), totalTime()); 63 | } 64 | console.log(); 65 | }; 66 | 67 | process.on("exit", () => { 68 | if (!_processCompleted) { 69 | _processCompleted = true; 70 | processCompleted(); 71 | } 72 | }); 73 | 74 | const handleMainError = (e) => { 75 | if (!process.exitCode) { 76 | process.exitCode = 1; 77 | } 78 | console.log("❌ ", colors.redBright(util.inspect(e, { colors: colors.level > 0 }))); 79 | console.log(totalTime()); 80 | console.log(); 81 | }; 82 | 83 | try { 84 | const result = fn(); 85 | if (typeof result === "object" && result && typeof result.then === "function") { 86 | result.then(() => {}, handleMainError); 87 | } 88 | } catch (e) { 89 | handleMainError(e); 90 | } 91 | } 92 | 93 | function spawnAsync(command, args, options) { 94 | return new Promise((resolve, reject) => { 95 | const childProcess = spawn(command, args, { stdio: "inherit", ...options }); 96 | childProcess.on("error", (e) => { 97 | reject(e); 98 | }); 99 | childProcess.on("close", (code) => { 100 | if (code === 0) { 101 | resolve(); 102 | } else { 103 | reject(new Error(`${command} exited with code ${code}`)); 104 | } 105 | }); 106 | }); 107 | } 108 | 109 | function forkAsync(modulePath, args, options) { 110 | return new Promise((resolve, reject) => { 111 | const childProcess = fork(modulePath, args, { stdio: "inherit", ...options }); 112 | childProcess.on("error", (e) => { 113 | reject(e); 114 | }); 115 | childProcess.on("close", (code) => { 116 | if (code === 0) { 117 | resolve(); 118 | } else { 119 | reject(new Error(`${modulePath} exited with code ${code}`)); 120 | } 121 | }); 122 | }); 123 | } 124 | 125 | async function mergeDirs(src, dest) { 126 | const promises = []; 127 | await fs.promises.mkdir(dest, { recursive: true }); 128 | for (const file of fs.readdirSync(src)) { 129 | const srcFile = path.resolve(src, file); 130 | const destFile = path.resolve(dest, file); 131 | const isDir = (await fs.promises.lstat(srcFile)).isDirectory(); 132 | if (isDir) { 133 | await mergeDirs(srcFile, destFile); 134 | } else { 135 | if (fs.existsSync(destFile)) { 136 | await fs.promises.rm(destFile, { recursive: true }); 137 | } 138 | promises.push(fs.promises.rename(srcFile, destFile)); 139 | } 140 | } 141 | return Promise.all(promises); 142 | } 143 | -------------------------------------------------------------------------------- /node-pre-gyp.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console */ 2 | "use strict"; 3 | 4 | const { fork } = require("node:child_process"); 5 | const fs = require("node:fs"); 6 | const path = require("node:path"); 7 | 8 | function resolveNodeGypPath() { 9 | const fs = require("node:fs"); 10 | const path = require("node:path"); 11 | 12 | const seen = new Set(); 13 | const addCandidates = (list) => { 14 | for (const item of list) { 15 | if (item && !seen.has(item)) { 16 | seen.add(path.resolve(item)); 17 | } 18 | } 19 | }; 20 | 21 | try { 22 | addCandidates([require.resolve("node-gyp/bin/node-gyp.js")]); 23 | } catch {} 24 | 25 | const cwd = process.cwd(); 26 | addCandidates([ 27 | path.join(cwd, "node_modules", "node-gyp", "bin", "node-gyp.js"), 28 | path.join(cwd, "node_modules", "npm", "node_modules", "node-gyp", "bin", "node-gyp.js"), 29 | ]); 30 | 31 | const execDir = path.dirname(process.execPath); 32 | const roots = [ 33 | path.join(execDir, "..", "lib", "node_modules", "npm"), 34 | path.join(execDir, "..", "node_modules", "npm"), 35 | path.join(execDir, "node_modules", "npm"), 36 | ]; 37 | for (const root of roots) { 38 | addCandidates([path.join(root, "node_modules", "node-gyp", "bin", "node-gyp.js")]); 39 | } 40 | 41 | for (const candidate of seen) { 42 | if (fs.existsSync(candidate)) { 43 | return candidate; 44 | } 45 | } 46 | return null; 47 | } 48 | 49 | const ROARING_NODE_PRE_GYP = process.env.ROARING_NODE_PRE_GYP; 50 | if (ROARING_NODE_PRE_GYP) { 51 | process.env.ROARING_NODE_PRE_GYP = ""; 52 | } 53 | 54 | function roaring32NodePreGyp() { 55 | console.time("node-pre-gyp"); 56 | process.on("exit", () => { 57 | console.timeEnd("node-pre-gyp"); 58 | }); 59 | 60 | process.chdir(__dirname); 61 | 62 | if (ROARING_NODE_PRE_GYP !== "custom-rebuild") { 63 | const nodeGypPath = resolveNodeGypPath(); 64 | if (nodeGypPath) { 65 | process.env.npm_config_node_gyp = nodeGypPath; 66 | } 67 | 68 | require("@mapbox/node-pre-gyp/lib/main"); 69 | } else { 70 | const forkAsync = (modulePath, args, options) => { 71 | return new Promise((resolve, reject) => { 72 | const childProcess = fork(modulePath, args, { stdio: "inherit", ...options }); 73 | childProcess.on("error", (e) => { 74 | reject(e); 75 | }); 76 | childProcess.on("close", (code) => { 77 | if (code === 0) { 78 | resolve(); 79 | } else { 80 | reject(new Error(`${modulePath} exited with code ${code}`)); 81 | } 82 | }); 83 | }); 84 | }; 85 | 86 | const main = async () => { 87 | console.log("* rebuild..."); 88 | 89 | let glibc; 90 | try { 91 | // eslint-disable-next-line node/no-unsupported-features/node-builtins 92 | const header = process.report.getReport().header; 93 | glibc = header.glibcVersionRuntime; 94 | } catch {} 95 | 96 | if (typeof glibc !== "string" || !glibc || glibc === "unknown") { 97 | glibc = null; 98 | } else { 99 | glibc = glibc.trim(); 100 | } 101 | 102 | console.log("versions:", { 103 | node: process.version, 104 | v8: process.versions.v8, 105 | glibc: glibc || null, 106 | }); 107 | 108 | console.time("rebuild"); 109 | const rebuildArgs = ["rebuild"]; 110 | await forkAsync(__filename, rebuildArgs); 111 | console.log(); 112 | console.timeEnd("rebuild"); 113 | console.log(); 114 | 115 | // Clean debug info and temporary files 116 | for (let d of fs.readdirSync("native")) { 117 | d = path.resolve(__dirname, "native", d); 118 | if (fs.lstatSync(d).isDirectory()) { 119 | for (let f of fs.readdirSync(d)) { 120 | f = path.resolve(d, f); 121 | for (const ext of [".ipdb", ".iobj", ".pdb", ".obj", ".lib", ".exp", ".tmp"]) { 122 | if (f.endsWith(ext)) { 123 | console.log(`- removing file ${f}`); 124 | fs.unlinkSync(f); 125 | } 126 | } 127 | } 128 | } 129 | } 130 | 131 | console.log(); 132 | console.log("* packaging..."); 133 | console.time("packaging"); 134 | const packageArgs = ["package", "testpackage"]; 135 | await forkAsync(__filename, packageArgs); 136 | console.timeEnd("packaging"); 137 | console.log(); 138 | }; 139 | 140 | main().catch((e) => { 141 | console.error(e); 142 | if (!process.exitCode) { 143 | process.exitCode = 1; 144 | } 145 | }); 146 | } 147 | } 148 | 149 | if (ROARING_NODE_PRE_GYP !== "false") { 150 | roaring32NodePreGyp(); 151 | } 152 | -------------------------------------------------------------------------------- /src/cpp/RoaringBitmap32.h: -------------------------------------------------------------------------------- 1 | #ifndef __ROARINGBITMAP32__H__ 2 | #define __ROARINGBITMAP32__H__ 3 | 4 | #include "v8utils.h" 5 | #include "serialization-format.h" 6 | #include "WorkerError.h" 7 | 8 | using namespace roaring; 9 | using namespace roaring::api; 10 | 11 | class RoaringBitmap32; 12 | 13 | typedef roaring_bitmap_t * roaring_bitmap_t_ptr; 14 | 15 | class RoaringBitmap32 final : public ObjectWrap { 16 | public: 17 | static const constexpr uint64_t OBJECT_TOKEN = 0x21524F4152330000; 18 | 19 | static const constexpr int64_t FROZEN_COUNTER_SOFT_FROZEN = -1; 20 | static const constexpr int64_t FROZEN_COUNTER_HARD_FROZEN = -2; 21 | 22 | roaring_bitmap_t * roaring; 23 | int64_t sizeCache; 24 | int64_t _version; 25 | int64_t frozenCounter; 26 | RoaringBitmap32 * const readonlyViewOf; 27 | v8::Global readonlyViewPersistent; 28 | v8::Global persistent; 29 | v8utils::TypedArrayContent frozenStorage; 30 | 31 | inline bool isEmpty() const { 32 | if (this->sizeCache == 0) { 33 | return true; 34 | } 35 | if (this->readonlyViewOf != nullptr) { 36 | return this->readonlyViewOf->isEmpty(); 37 | } 38 | const roaring_bitmap_t_ptr roaring = this->roaring; 39 | bool result = roaring == nullptr || roaring_bitmap_is_empty(roaring); 40 | if (result) { 41 | const_cast(this)->sizeCache = 0; 42 | } 43 | return result; 44 | } 45 | 46 | inline size_t getSize() const { 47 | int64_t size = this->sizeCache; 48 | if (size < 0) { 49 | if (this->readonlyViewOf != nullptr) { 50 | return this->readonlyViewOf->getSize(); 51 | } 52 | const roaring_bitmap_t_ptr roaring = this->roaring; 53 | size = roaring != nullptr ? (int64_t)roaring_bitmap_get_cardinality(roaring) : 0; 54 | const_cast(this)->sizeCache = size; 55 | } 56 | return (size_t)size; 57 | } 58 | 59 | inline bool isFrozen() const { return this->frozenCounter != 0; } 60 | inline bool isFrozenHard() const { return this->frozenCounter > 0 || this->frozenCounter == FROZEN_COUNTER_HARD_FROZEN; } 61 | inline bool isFrozenForever() const { return this->frozenCounter < 0; } 62 | 63 | inline void beginFreeze() { 64 | if (this->frozenCounter >= 0) { 65 | ++this->frozenCounter; 66 | } 67 | } 68 | 69 | inline void endFreeze() { 70 | if (this->frozenCounter > 0) { 71 | --this->frozenCounter; 72 | } 73 | } 74 | 75 | inline int64_t getVersion() const { return this->_version; } 76 | 77 | inline void invalidate() { 78 | this->sizeCache = -1; 79 | ++this->_version; 80 | } 81 | 82 | inline bool roaring_bitmap_t_is_frozen(const roaring_bitmap_t * r) { 83 | return r->high_low_container.flags & ROARING_FLAG_FROZEN; 84 | } 85 | 86 | bool replaceBitmapInstance(v8::Isolate * isolate, roaring_bitmap_t * newInstance) { 87 | roaring_bitmap_t * oldInstance = this->roaring; 88 | if (oldInstance == newInstance) { 89 | return false; 90 | } 91 | if (oldInstance != nullptr) { 92 | roaring_bitmap_free(oldInstance); 93 | } 94 | if (newInstance != nullptr && roaring_bitmap_t_is_frozen(newInstance)) { 95 | this->frozenCounter = RoaringBitmap32::FROZEN_COUNTER_HARD_FROZEN; 96 | } 97 | this->roaring = newInstance; 98 | this->invalidate(); 99 | return true; 100 | } 101 | 102 | explicit RoaringBitmap32(AddonData * addonData, RoaringBitmap32 * readonlyViewOf) : 103 | ObjectWrap(addonData), 104 | roaring(readonlyViewOf->roaring), 105 | sizeCache(-1), 106 | frozenCounter(RoaringBitmap32::FROZEN_COUNTER_HARD_FROZEN), 107 | readonlyViewOf(readonlyViewOf->readonlyViewOf ? readonlyViewOf->readonlyViewOf : readonlyViewOf) { 108 | _gcaware_adjustAllocatedMemory(this->isolate, sizeof(RoaringBitmap32)); 109 | } 110 | 111 | explicit RoaringBitmap32(AddonData * addonData, uint32_t capacity) : 112 | ObjectWrap(addonData), 113 | roaring(roaring_bitmap_create_with_capacity(capacity)), 114 | sizeCache(0), 115 | _version(0), 116 | frozenCounter(0), 117 | readonlyViewOf(nullptr) { 118 | ++addonData->RoaringBitmap32_instances; 119 | _gcaware_adjustAllocatedMemory(this->isolate, sizeof(RoaringBitmap32)); 120 | } 121 | 122 | ~RoaringBitmap32() { 123 | _gcaware_adjustAllocatedMemory(this->isolate, -sizeof(RoaringBitmap32)); 124 | if (!this->readonlyViewOf) { 125 | --this->addonData->RoaringBitmap32_instances; 126 | if (this->roaring != nullptr) { 127 | roaring_bitmap_free(this->roaring); 128 | } 129 | if (this->frozenStorage.data != nullptr && this->frozenStorage.length == std::numeric_limits::max()) { 130 | gcaware_aligned_free(this->isolate, this->frozenStorage.data); 131 | } 132 | } 133 | if (!this->persistent.IsEmpty()) { 134 | this->persistent.ClearWeak(); 135 | } 136 | } 137 | }; 138 | 139 | #endif 140 | -------------------------------------------------------------------------------- /src/cpp/aligned-buffers.h: -------------------------------------------------------------------------------- 1 | #ifndef ROARING_NODE_ALIGNED_BUFFERS_ 2 | #define ROARING_NODE_ALIGNED_BUFFERS_ 3 | 4 | #include "v8utils.h" 5 | #include "memory.h" 6 | 7 | void _bufferAlignedAlloc(const v8::FunctionCallbackInfo & info, bool unsafe, bool shared) { 8 | v8::Isolate * isolate = info.GetIsolate(); 9 | v8::HandleScope scope(isolate); 10 | v8::Local context = isolate->GetCurrentContext(); 11 | 12 | int64_t size; 13 | int32_t alignment = 32; 14 | if (info.Length() < 1 || !info[0]->IsNumber() || !info[0]->IntegerValue(context).To(&size) || size < 0) { 15 | return v8utils::throwTypeError(isolate, "Buffer size must be a positive integer"); 16 | } 17 | 18 | if (info.Length() >= 2 && !info[1]->IsUndefined()) { 19 | if (!info[1]->IsNumber() || !info[1]->Int32Value(context).To(&alignment) || alignment <= 0) { 20 | return v8utils::throwTypeError(isolate, "Buffer alignment must be a positive integer"); 21 | } 22 | } 23 | 24 | if (size < 0) { 25 | size = 0; 26 | } 27 | 28 | if ((uint64_t)size > node::Buffer::kMaxLength || (uint64_t)size + alignment >= node::Buffer::kMaxLength) { 29 | return v8utils::throwTypeError(isolate, "Buffer size is too large"); 30 | } 31 | 32 | if (alignment > 1024) { 33 | return v8utils::throwTypeError(isolate, "Buffer alignment is too large"); 34 | } 35 | 36 | void * ptr = bare_aligned_malloc(alignment, size); 37 | if (!ptr) { 38 | return v8utils::throwError(isolate, "Buffer memory allocation failed"); 39 | } 40 | 41 | if (!unsafe) { 42 | memset(ptr, 0, size); 43 | } 44 | 45 | if (shared) { 46 | AddonData * addonData = AddonData::get(info); 47 | if (addonData == nullptr) { 48 | bare_aligned_free(ptr); 49 | return v8utils::throwError(isolate, "Invalid call"); 50 | } 51 | 52 | auto backingStore = v8::SharedArrayBuffer::NewBackingStore(ptr, (size_t)size, bare_aligned_free_callback2, nullptr); 53 | if (!backingStore) { 54 | bare_aligned_free(ptr); 55 | return v8utils::throwError(isolate, "Buffer creation failed"); 56 | } 57 | auto sharedBuf = v8::SharedArrayBuffer::New(isolate, std::move(backingStore)); 58 | v8::Local bufferObj; 59 | if (sharedBuf.IsEmpty() || !v8utils::bufferFromArrayBuffer(isolate, addonData, sharedBuf, 0, size, bufferObj)) { 60 | bare_aligned_free(ptr); 61 | return v8utils::throwError(isolate, "Buffer creation failed"); 62 | } 63 | info.GetReturnValue().Set(bufferObj); 64 | return; 65 | } 66 | 67 | v8::MaybeLocal bufferMaybe; 68 | bufferMaybe = node::Buffer::New(isolate, (char *)ptr, size, bare_aligned_free_callback, nullptr); 69 | v8::Local bufferValue; 70 | if (!bufferMaybe.ToLocal(&bufferValue) || bufferValue.IsEmpty()) { 71 | bare_aligned_free(ptr); 72 | return v8utils::throwError(isolate, "Buffer creation failed"); 73 | } 74 | 75 | info.GetReturnValue().Set(bufferValue); 76 | } 77 | 78 | void bufferAlignedAlloc(const v8::FunctionCallbackInfo & info) { _bufferAlignedAlloc(info, false, false); } 79 | 80 | void bufferAlignedAllocUnsafe(const v8::FunctionCallbackInfo & info) { _bufferAlignedAlloc(info, true, false); } 81 | 82 | void bufferAlignedAllocShared(const v8::FunctionCallbackInfo & info) { _bufferAlignedAlloc(info, false, true); } 83 | 84 | void bufferAlignedAllocSharedUnsafe(const v8::FunctionCallbackInfo & info) { 85 | _bufferAlignedAlloc(info, true, true); 86 | } 87 | 88 | void isBufferAligned(const v8::FunctionCallbackInfo & info) { 89 | v8::Isolate * isolate = info.GetIsolate(); 90 | v8::HandleScope scope(isolate); 91 | v8::Local context = isolate->GetCurrentContext(); 92 | 93 | if (info.Length() < 1) { 94 | return info.GetReturnValue().Set(false); 95 | } 96 | 97 | // Second parameter must be a positive integer 98 | int32_t alignment = 32; 99 | if (info.Length() >= 2 && !info[1]->IsUndefined()) { 100 | if (!info[1]->IsNumber() || !info[1]->Int32Value(context).To(&alignment) || alignment < 0) { 101 | return info.GetReturnValue().Set(false); 102 | } 103 | } 104 | 105 | if (alignment == 0) { 106 | return info.GetReturnValue().Set(true); 107 | } 108 | 109 | v8utils::TypedArrayContent content(isolate, info[0]); 110 | info.GetReturnValue().Set(is_pointer_aligned(content.data, alignment)); 111 | } 112 | 113 | void AlignedBuffers_Init(v8::Local exports, AddonData * addonData) { 114 | addonData->setMethod(exports, "bufferAlignedAlloc", bufferAlignedAlloc); 115 | addonData->setMethod(exports, "bufferAlignedAllocUnsafe", bufferAlignedAllocUnsafe); 116 | addonData->setMethod(exports, "bufferAlignedAllocShared", bufferAlignedAllocShared); 117 | addonData->setMethod(exports, "bufferAlignedAllocSharedUnsafe", bufferAlignedAllocSharedUnsafe); 118 | addonData->setMethod(exports, "isBufferAligned", isBufferAligned); 119 | } 120 | 121 | #endif // ROARING_NODE_ALIGNED_BUFFERS_ 122 | -------------------------------------------------------------------------------- /src/cpp/memory.h: -------------------------------------------------------------------------------- 1 | #ifndef ROARING_NODE_MEMORY_ 2 | #define ROARING_NODE_MEMORY_ 3 | 4 | #include "includes.h" 5 | 6 | inline std::atomic *& gcawareCurrentAsyncWorkerMemoryCounter() { 7 | thread_local std::atomic * counter = nullptr; 8 | return counter; 9 | } 10 | 11 | inline std::atomic * gcawarePushAsyncWorkerMemoryCounter(std::atomic * counter) { 12 | auto & slot = gcawareCurrentAsyncWorkerMemoryCounter(); 13 | std::atomic * previous = slot; 14 | slot = counter; 15 | return previous; 16 | } 17 | 18 | inline void gcawarePopAsyncWorkerMemoryCounter(std::atomic * previous) { 19 | gcawareCurrentAsyncWorkerMemoryCounter() = previous; 20 | } 21 | 22 | /** portable version of posix_memalign */ 23 | void * bare_aligned_malloc(size_t alignment, size_t size) { 24 | void * p; 25 | #ifdef _MSC_VER 26 | p = _aligned_malloc(size, alignment); 27 | #elif defined(__MINGW32__) || defined(__MINGW64__) 28 | p = __mingw_aligned_malloc(size, alignment); 29 | #else 30 | // somehow, if this is used before including "x86intrin.h", it creates an 31 | // implicit defined warning. 32 | if (posix_memalign(&p, alignment, size) != 0) return NULL; 33 | #endif 34 | return p; 35 | } 36 | 37 | /** portable version of free fo aligned allocs */ 38 | void bare_aligned_free(void * memblock) { 39 | if (memblock != nullptr) { 40 | #ifdef _MSC_VER 41 | _aligned_free(memblock); 42 | #elif defined(__MINGW32__) || defined(__MINGW64__) 43 | __mingw_aligned_free(memblock); 44 | #else 45 | free(memblock); 46 | #endif 47 | } 48 | } 49 | 50 | /** portable version of malloc_size */ 51 | inline size_t bare_malloc_size(const void * ptr) { 52 | #if defined(__APPLE__) 53 | return malloc_size((void *)ptr); 54 | #elif defined(_WIN32) || defined(__MINGW32__) || defined(__MINGW64__) 55 | return _msize((void *)ptr); 56 | #else 57 | return malloc_usable_size((void *)ptr); 58 | #endif 59 | } 60 | 61 | /** portable version of malloc_size for memory allocated with bare_aligned_malloc */ 62 | inline size_t bare_aligned_malloc_size(const void * ptr) { 63 | #if defined(__APPLE__) 64 | return malloc_size((void *)ptr); 65 | #elif defined(_WIN32) 66 | return _aligned_msize((void *)ptr, 32, 0); 67 | #else 68 | return malloc_usable_size((void *)ptr); 69 | #endif 70 | } 71 | 72 | std::atomic gcaware_totalMemCounter{0}; 73 | 74 | inline void _gcaware_adjustAllocatedMemory(v8::Isolate * isolate, int64_t size) { 75 | if (size == 0) { 76 | return; 77 | } 78 | 79 | gcaware_totalMemCounter.fetch_add(size, std::memory_order_relaxed); 80 | 81 | if (isolate != nullptr) { 82 | isolate->AdjustAmountOfExternalAllocatedMemory(size); 83 | return; 84 | } 85 | 86 | std::atomic * pendingCounter = gcawareCurrentAsyncWorkerMemoryCounter(); 87 | if (pendingCounter != nullptr) { 88 | pendingCounter->fetch_add(size, std::memory_order_relaxed); 89 | } 90 | } 91 | 92 | inline void _gcaware_adjustAllocatedMemory(int64_t size) { _gcaware_adjustAllocatedMemory(v8::Isolate::GetCurrent(), size); } 93 | 94 | void * gcaware_malloc(size_t size) { 95 | void * memory = malloc(size); 96 | if (memory != nullptr) { 97 | _gcaware_adjustAllocatedMemory(bare_malloc_size(memory)); 98 | } 99 | return memory; 100 | } 101 | 102 | void * gcaware_realloc(void * memory, size_t size) { 103 | size_t oldSize = memory != nullptr ? bare_malloc_size(memory) : 0; 104 | memory = realloc(memory, size); 105 | if (memory != nullptr) { 106 | size_t newSize = bare_malloc_size(memory); 107 | if (newSize != oldSize) { 108 | _gcaware_adjustAllocatedMemory((int64_t)newSize - (int64_t)oldSize); 109 | } 110 | } 111 | return memory; 112 | } 113 | 114 | void * gcaware_calloc(size_t count, size_t size) { 115 | void * memory = calloc(count, size); 116 | if (memory != nullptr) { 117 | _gcaware_adjustAllocatedMemory(bare_malloc_size(memory)); 118 | } 119 | return memory; 120 | } 121 | 122 | void gcaware_free(void * memory) { 123 | if (memory != nullptr) { 124 | _gcaware_adjustAllocatedMemory(-bare_malloc_size(memory)); 125 | free(memory); 126 | } 127 | } 128 | 129 | void gcaware_free(v8::Isolate * isolate, void * memory) { 130 | if (memory != nullptr) { 131 | _gcaware_adjustAllocatedMemory(-bare_malloc_size(memory)); 132 | free(memory); 133 | } 134 | } 135 | 136 | void * gcaware_aligned_malloc(size_t alignment, size_t size) { 137 | void * memory = bare_aligned_malloc(alignment, size); 138 | if (memory != nullptr) { 139 | _gcaware_adjustAllocatedMemory(bare_aligned_malloc_size(memory)); 140 | } 141 | return memory; 142 | } 143 | 144 | void gcaware_aligned_free(void * memory) { 145 | if (memory != nullptr) { 146 | _gcaware_adjustAllocatedMemory(-bare_aligned_malloc_size(memory)); 147 | bare_aligned_free(memory); 148 | } 149 | } 150 | 151 | void gcaware_aligned_free(v8::Isolate * isolate, void * memory) { 152 | if (memory != nullptr) { 153 | _gcaware_adjustAllocatedMemory(isolate, -bare_aligned_malloc_size(memory)); 154 | bare_aligned_free(memory); 155 | } 156 | } 157 | 158 | void bare_aligned_free_callback(char * data, void * hint) { bare_aligned_free(data); } 159 | 160 | void bare_aligned_free_callback2(void * data, size_t length, void * deleter_data) { bare_aligned_free(data); } 161 | 162 | inline bool is_pointer_aligned(const void * ptr, std::uintptr_t alignment) noexcept { 163 | auto iptr = reinterpret_cast(ptr); 164 | return !(iptr % alignment); 165 | } 166 | 167 | void getRoaringUsedMemory(const v8::FunctionCallbackInfo & info) { 168 | info.GetReturnValue().Set((double)gcaware_totalMemCounter.load(std::memory_order_relaxed)); 169 | } 170 | 171 | #endif // ROARING_NODE_MEMORY_ 172 | -------------------------------------------------------------------------------- /src/cpp/addon-data.h: -------------------------------------------------------------------------------- 1 | #ifndef ROARING_NODE_ADDON_DATA_ 2 | #define ROARING_NODE_ADDON_DATA_ 3 | 4 | #include "includes.h" 5 | #include "memory.h" 6 | #include "addon-strings.h" 7 | 8 | template 9 | inline void ignoreMaybeResult(v8::Maybe) {} 10 | 11 | template 12 | inline void ignoreMaybeResult(v8::MaybeLocal) {} 13 | 14 | class AddonData final { 15 | public: 16 | v8::Isolate * isolate; 17 | 18 | AddonDataStrings strings; 19 | 20 | v8::Global Buffer; 21 | v8::Global Uint32Array; 22 | v8::Global Uint32Array_from; 23 | v8::Global Buffer_from; 24 | 25 | std::atomic RoaringBitmap32_instances; 26 | std::atomic activeAsyncWorkers; 27 | std::atomic shuttingDown; 28 | 29 | v8::Global RoaringBitmap32_constructorTemplate; 30 | v8::Global RoaringBitmap32_constructor; 31 | 32 | v8::Global RoaringBitmap32BufferedIterator_constructorTemplate; 33 | v8::Global RoaringBitmap32BufferedIterator_constructor; 34 | 35 | v8::Global external; 36 | 37 | inline explicit AddonData(v8::Isolate * isolate) : 38 | isolate(isolate), strings(isolate), RoaringBitmap32_instances(0), activeAsyncWorkers(0), shuttingDown(false) { 39 | const int64_t externalSize = static_cast(sizeof(AddonData)) + 256; 40 | isolate->AdjustAmountOfExternalAllocatedMemory(externalSize); 41 | } 42 | 43 | inline ~AddonData() { 44 | shuttingDown.store(true, std::memory_order_release); 45 | waitForAsyncWorkersToFinish(); 46 | Buffer.Reset(); 47 | Uint32Array.Reset(); 48 | Uint32Array_from.Reset(); 49 | Buffer_from.Reset(); 50 | RoaringBitmap32_constructorTemplate.Reset(); 51 | RoaringBitmap32_constructor.Reset(); 52 | RoaringBitmap32BufferedIterator_constructorTemplate.Reset(); 53 | RoaringBitmap32BufferedIterator_constructor.Reset(); 54 | external.Reset(); 55 | const int64_t externalSize = -static_cast(sizeof(AddonData)) - 256; 56 | this->isolate->AdjustAmountOfExternalAllocatedMemory(externalSize); 57 | } 58 | 59 | static inline AddonData * get(const v8::FunctionCallbackInfo & info) { 60 | v8::Local data = info.Data(); 61 | if (data.IsEmpty()) { 62 | return nullptr; 63 | } 64 | v8::Local external = data.As(); 65 | if (external.IsEmpty()) { 66 | return nullptr; 67 | } 68 | return reinterpret_cast(external->Value()); 69 | } 70 | 71 | inline void initialize() { 72 | v8::Isolate * isolate = this->isolate; 73 | if (isolate == nullptr) { 74 | return; 75 | } 76 | 77 | external.Reset(isolate, v8::External::New(isolate, this)); 78 | 79 | auto context = isolate->GetCurrentContext(); 80 | 81 | auto global = context->Global(); 82 | 83 | auto uint32Array = global->Get(context, NEW_LITERAL_V8_STRING(isolate, "Uint32Array", v8::NewStringType::kInternalized)) 84 | .ToLocalChecked() 85 | ->ToObject(context) 86 | .ToLocalChecked(); 87 | 88 | auto buffer = global->Get(context, NEW_LITERAL_V8_STRING(isolate, "Buffer", v8::NewStringType::kInternalized)) 89 | .ToLocalChecked() 90 | .As(); 91 | 92 | this->Buffer.Reset(isolate, buffer); 93 | 94 | this->Buffer_from.Reset( 95 | isolate, 96 | buffer->Get(context, NEW_LITERAL_V8_STRING(isolate, "from", v8::NewStringType::kInternalized)) 97 | .ToLocalChecked() 98 | .As()); 99 | 100 | this->Uint32Array.Reset(isolate, uint32Array); 101 | 102 | this->Uint32Array_from.Reset( 103 | isolate, 104 | v8::Local::Cast( 105 | uint32Array->Get(context, NEW_LITERAL_V8_STRING(isolate, "from", v8::NewStringType::kInternalized)) 106 | .ToLocalChecked())); 107 | } 108 | 109 | inline void setMethod(v8::Local recv, const char * name, v8::FunctionCallback callback) { 110 | if (recv.IsEmpty() || name == nullptr || callback == nullptr) { 111 | return; 112 | } 113 | 114 | v8::Isolate * isolate = this->isolate; 115 | if (isolate == nullptr) { 116 | return; 117 | } 118 | 119 | v8::HandleScope handleScope(isolate); 120 | v8::Local context = isolate->GetCurrentContext(); 121 | v8::Local tpl = v8::FunctionTemplate::New(isolate, callback, this->external.Get(isolate)); 122 | v8::Local fn = tpl->GetFunction(context).ToLocalChecked(); 123 | v8::Local fnName = v8::String::NewFromUtf8(isolate, name, v8::NewStringType::kInternalized).ToLocalChecked(); 124 | fn->SetName(fnName); 125 | ignoreMaybeResult(recv->Set(context, fnName, fn)); 126 | } 127 | 128 | inline bool tryEnterAsyncWorker() { 129 | if (isShuttingDown()) { 130 | return false; 131 | } 132 | activeAsyncWorkers.fetch_add(1, std::memory_order_acq_rel); 133 | if (isShuttingDown()) { 134 | activeAsyncWorkers.fetch_sub(1, std::memory_order_acq_rel); 135 | return false; 136 | } 137 | return true; 138 | } 139 | 140 | inline void leaveAsyncWorker() { activeAsyncWorkers.fetch_sub(1, std::memory_order_acq_rel); } 141 | 142 | inline bool isShuttingDown() const { return shuttingDown.load(std::memory_order_acquire); } 143 | 144 | inline void waitForAsyncWorkersToFinish() { 145 | uint32_t delayMicros = 50; 146 | while (activeAsyncWorkers.load(std::memory_order_acquire) != 0) { 147 | std::this_thread::sleep_for(std::chrono::microseconds(delayMicros)); 148 | delayMicros = delayMicros >= 5000 ? 5000 : delayMicros * 2; 149 | } 150 | } 151 | 152 | private: 153 | }; 154 | 155 | #endif 156 | -------------------------------------------------------------------------------- /src/cpp/serialization-csv.h: -------------------------------------------------------------------------------- 1 | #ifndef ROARING_NODE_SERIALIZATION_CSV_ 2 | #define ROARING_NODE_SERIALIZATION_CSV_ 3 | 4 | #include "includes.h" 5 | #include 6 | #include "mmap.h" 7 | #include "serialization-format.h" 8 | #include "WorkerError.h" 9 | 10 | struct CsvFileDescriptorSerializer final { 11 | public: 12 | static int iterate(const roaring::api::roaring_bitmap_t * r, int fd, FileSerializationFormat format) { 13 | char separator; 14 | switch (format) { 15 | case FileSerializationFormat::newline_separated_values: separator = '\n'; break; 16 | case FileSerializationFormat::comma_separated_values: separator = ','; break; 17 | case FileSerializationFormat::tab_separated_values: separator = '\t'; break; 18 | case FileSerializationFormat::json_array: separator = ','; break; 19 | default: return EINVAL; 20 | } 21 | 22 | CsvFileDescriptorSerializer writer(fd, separator); 23 | if (format == FileSerializationFormat::json_array) { 24 | writer.appendChar('['); 25 | } 26 | 27 | if (r) { 28 | roaring_iterate(r, roaringIteratorFn, &writer); 29 | } 30 | 31 | if (format == FileSerializationFormat::newline_separated_values) { 32 | writer.appendChar('\n'); 33 | } else if (format == FileSerializationFormat::json_array) { 34 | writer.appendChar(']'); 35 | } 36 | 37 | if (!writer.flush()) { 38 | int errorno = errno; 39 | errno = 0; 40 | return errorno ? errorno : EIO; 41 | } 42 | 43 | return 0; 44 | } 45 | 46 | private: 47 | const constexpr static size_t BUFFER_SIZE = 131072; 48 | 49 | char * buf; 50 | size_t bufPos; 51 | int fd; 52 | bool needsSeparator; 53 | char separator; 54 | 55 | CsvFileDescriptorSerializer(int fd, char separator) : 56 | buf((char *)gcaware_aligned_malloc(32, BUFFER_SIZE)), bufPos(0), fd(fd), needsSeparator(false), separator(separator) {} 57 | 58 | ~CsvFileDescriptorSerializer() { gcaware_aligned_free(this->buf); } 59 | 60 | bool flush() { 61 | if (this->bufPos == 0) { 62 | return true; 63 | } 64 | if (!this->buf) { 65 | return false; 66 | } 67 | ssize_t written = write(this->fd, this->buf, this->bufPos); 68 | if (written < 0) { 69 | gcaware_aligned_free(this->buf); 70 | this->buf = nullptr; 71 | return false; 72 | } 73 | this->bufPos = 0; 74 | return true; 75 | } 76 | 77 | bool appendChar(char c) { 78 | if (this->bufPos + 1 >= BUFFER_SIZE) { 79 | if (!this->flush()) { 80 | return false; 81 | } 82 | } 83 | if (!this->buf) { 84 | return false; 85 | } 86 | this->buf[this->bufPos++] = c; 87 | return true; 88 | } 89 | 90 | bool appendValue(uint32_t value) { 91 | if (this->bufPos + 15 >= BUFFER_SIZE) { 92 | if (!this->flush()) { 93 | return false; 94 | } 95 | } 96 | if (!this->buf) { 97 | return false; 98 | } 99 | if (this->needsSeparator) { 100 | this->buf[this->bufPos++] = this->separator; 101 | } 102 | this->needsSeparator = true; 103 | 104 | char * str = this->buf + this->bufPos; 105 | int32_t i, j; 106 | char c; 107 | 108 | /* uint to decimal */ 109 | i = 0; 110 | do { 111 | uint32_t remainder = value % 10; 112 | str[i++] = (char)(remainder + 48); 113 | value = value / 10; 114 | } while (value != 0); 115 | 116 | this->bufPos += i; 117 | 118 | /* reverse string */ 119 | for (j = 0, i--; j < i; j++, i--) { 120 | c = str[i]; 121 | str[i] = str[j]; 122 | str[j] = c; 123 | } 124 | 125 | return true; 126 | } 127 | 128 | static bool roaringIteratorFn(uint32_t value, void * param) { 129 | return ((CsvFileDescriptorSerializer *)param)->appendValue(value); 130 | } 131 | }; 132 | 133 | WorkerError deserializeRoaringCsvFile( 134 | roaring::api::roaring_bitmap_t * r, int fd, const char * input, size_t input_size, const std::string & filePath) { 135 | const constexpr static size_t BUFFER_SIZE = 131072; 136 | 137 | char * buf; 138 | ssize_t readBytes; 139 | if (input == nullptr) { 140 | buf = (char *)gcaware_aligned_malloc(32, BUFFER_SIZE); 141 | if (!buf) { 142 | return WorkerError("Failed to allocate memory for text deserialization"); 143 | } 144 | } else { 145 | buf = (char *)input; 146 | readBytes = (ssize_t)input_size; 147 | if (readBytes < 0) { 148 | return WorkerError("Input too big"); 149 | } 150 | if (readBytes == 0) { 151 | return WorkerError(); 152 | } 153 | } 154 | 155 | roaring_bulk_context_t context; 156 | memset(&context, 0, sizeof(context)); 157 | uint64_t value = 0; 158 | 159 | bool hasValue = false; 160 | bool isNegative = false; 161 | for (;;) { 162 | if (input == nullptr) { 163 | readBytes = read(fd, buf, BUFFER_SIZE); 164 | if (readBytes <= 0) { 165 | if (readBytes < 0) { 166 | WorkerError err = WorkerError::from_errno("read", filePath); 167 | gcaware_aligned_free(buf); 168 | return err; 169 | } 170 | break; 171 | } 172 | } 173 | 174 | for (ssize_t i = 0; i < readBytes; i++) { 175 | char c = buf[i]; 176 | if (c >= '0' && c <= '9') { 177 | if (value <= 0xffffffff) { 178 | hasValue = true; 179 | value = value * 10 + (c - '0'); 180 | } 181 | } else { 182 | if (hasValue) { 183 | hasValue = false; 184 | if (!isNegative && value <= 0xffffffff) { 185 | roaring_bitmap_add_bulk(r, &context, value); 186 | } 187 | } 188 | value = 0; 189 | isNegative = c == '-'; 190 | } 191 | } 192 | 193 | if (input != nullptr) { 194 | break; 195 | } 196 | } 197 | 198 | if (!isNegative && hasValue && value <= 0xffffffff) { 199 | roaring_bitmap_add_bulk(r, &context, value); 200 | } 201 | if (input == nullptr) { 202 | gcaware_aligned_free(buf); 203 | } 204 | 205 | return WorkerError(); 206 | } 207 | 208 | #endif 209 | -------------------------------------------------------------------------------- /src/cpp/serialization-format.h: -------------------------------------------------------------------------------- 1 | #ifndef ROARING_NODE_SERIALIZATION_FORMAT_ 2 | #define ROARING_NODE_SERIALIZATION_FORMAT_ 3 | 4 | #include "includes.h" 5 | 6 | const uint32_t MAX_SERIALIZATION_ARRAY_SIZE_IN_BYTES = 0x00FFFFFF; 7 | 8 | enum class SerializationFormat { 9 | INVALID = -1, 10 | croaring = 0, 11 | portable = 1, 12 | unsafe_frozen_croaring = 2, 13 | uint32_array = 4, 14 | }; 15 | 16 | enum class FileSerializationFormat { 17 | INVALID = -1, 18 | croaring = 0, 19 | portable = 1, 20 | unsafe_frozen_croaring = 2, 21 | uint32_array = 4, 22 | 23 | comma_separated_values = 10, 24 | tab_separated_values = 11, 25 | newline_separated_values = 12, 26 | json_array = 20 27 | }; 28 | 29 | enum class DeserializationFormat { 30 | INVALID = -1, 31 | croaring = 0, 32 | portable = 1, 33 | unsafe_frozen_croaring = 2, 34 | unsafe_frozen_portable = 3, 35 | uint32_array = 4, 36 | 37 | comma_separated_values = 10, 38 | tab_separated_values = 11, 39 | newline_separated_values = 12, 40 | json_array = 20 41 | }; 42 | 43 | enum class FileDeserializationFormat { 44 | INVALID = -1, 45 | croaring = 0, 46 | portable = 1, 47 | unsafe_frozen_croaring = 2, 48 | unsafe_frozen_portable = 3, 49 | uint32_array = 4, 50 | 51 | comma_separated_values = 10, 52 | tab_separated_values = 11, 53 | newline_separated_values = 12, 54 | json_array = 20 55 | }; 56 | 57 | enum class FrozenViewFormat { 58 | INVALID = -1, 59 | unsafe_frozen_croaring = 2, 60 | unsafe_frozen_portable = 3, 61 | }; 62 | 63 | SerializationFormat tryParseSerializationFormat(const v8::Local & value, v8::Isolate * isolate) { 64 | if (!isolate || value.IsEmpty()) { 65 | return SerializationFormat::INVALID; 66 | } 67 | if (value->IsBoolean()) { 68 | if (value->IsTrue()) { 69 | return SerializationFormat::portable; 70 | } 71 | if (value->IsFalse()) { 72 | return SerializationFormat::croaring; 73 | } 74 | } else if (value->IsString()) { 75 | v8::String::Utf8Value formatString(isolate, value); 76 | if (strcmp(*formatString, "croaring") == 0) { 77 | return SerializationFormat::croaring; 78 | } 79 | if (strcmp(*formatString, "portable") == 0) { 80 | return SerializationFormat::portable; 81 | } 82 | if (strcmp(*formatString, "unsafe_frozen_croaring") == 0) { 83 | return SerializationFormat::unsafe_frozen_croaring; 84 | } 85 | if (strcmp(*formatString, "uint32_array") == 0) { 86 | return SerializationFormat::uint32_array; 87 | } 88 | } 89 | return SerializationFormat::INVALID; 90 | } 91 | 92 | FileSerializationFormat tryParseFileSerializationFormat(const v8::Local & value, v8::Isolate * isolate) { 93 | SerializationFormat sf = tryParseSerializationFormat(value, isolate); 94 | if (sf != SerializationFormat::INVALID) { 95 | return static_cast(sf); 96 | } 97 | if (!isolate || value.IsEmpty()) { 98 | return FileSerializationFormat::INVALID; 99 | } 100 | if (value->IsString()) { 101 | v8::String::Utf8Value formatString(isolate, value); 102 | if (strcmp(*formatString, "comma_separated_values") == 0) { 103 | return FileSerializationFormat::comma_separated_values; 104 | } 105 | if (strcmp(*formatString, "tab_separated_values") == 0) { 106 | return FileSerializationFormat::tab_separated_values; 107 | } 108 | if (strcmp(*formatString, "newline_separated_values") == 0) { 109 | return FileSerializationFormat::newline_separated_values; 110 | } 111 | if (strcmp(*formatString, "json_array") == 0) { 112 | return FileSerializationFormat::json_array; 113 | } 114 | } 115 | return FileSerializationFormat::INVALID; 116 | } 117 | 118 | DeserializationFormat tryParseDeserializationFormat(const v8::Local & value, v8::Isolate * isolate) { 119 | if (!isolate || value.IsEmpty()) { 120 | return DeserializationFormat::INVALID; 121 | } 122 | if (value->IsBoolean()) { 123 | if (value->IsTrue()) { 124 | return DeserializationFormat::portable; 125 | } 126 | if (value->IsFalse()) { 127 | return DeserializationFormat::croaring; 128 | } 129 | } else if (value->IsString()) { 130 | v8::String::Utf8Value formatString(isolate, value); 131 | if (strcmp(*formatString, "croaring") == 0) { 132 | return DeserializationFormat::croaring; 133 | } 134 | if (strcmp(*formatString, "portable") == 0) { 135 | return DeserializationFormat::portable; 136 | } 137 | if (strcmp(*formatString, "unsafe_frozen_croaring") == 0) { 138 | return DeserializationFormat::unsafe_frozen_croaring; 139 | } 140 | if (strcmp(*formatString, "unsafe_frozen_portable") == 0) { 141 | return DeserializationFormat::unsafe_frozen_portable; 142 | } 143 | if (strcmp(*formatString, "uint32_array") == 0) { 144 | return DeserializationFormat::uint32_array; 145 | } 146 | if (strcmp(*formatString, "comma_separated_values") == 0) { 147 | return DeserializationFormat::comma_separated_values; 148 | } 149 | if (strcmp(*formatString, "tab_separated_values") == 0) { 150 | return DeserializationFormat::tab_separated_values; 151 | } 152 | if (strcmp(*formatString, "newline_separated_values") == 0) { 153 | return DeserializationFormat::newline_separated_values; 154 | } 155 | if (strcmp(*formatString, "json_array") == 0) { 156 | return DeserializationFormat::json_array; 157 | } 158 | } 159 | return DeserializationFormat::INVALID; 160 | } 161 | 162 | FileDeserializationFormat tryParseFileDeserializationFormat(const v8::Local & value, v8::Isolate * isolate) { 163 | return (FileDeserializationFormat)tryParseDeserializationFormat(value, isolate); 164 | } 165 | 166 | FrozenViewFormat tryParseFrozenViewFormat(const v8::Local & value, v8::Isolate * isolate) { 167 | if (!isolate || value.IsEmpty()) { 168 | return FrozenViewFormat::INVALID; 169 | } 170 | if (value->IsString()) { 171 | v8::String::Utf8Value formatString(isolate, value); 172 | if (strcmp(*formatString, "unsafe_frozen_croaring") == 0) { 173 | return FrozenViewFormat::unsafe_frozen_croaring; 174 | } 175 | if (strcmp(*formatString, "unsafe_frozen_portable") == 0) { 176 | return FrozenViewFormat::unsafe_frozen_portable; 177 | } 178 | } 179 | return FrozenViewFormat::INVALID; 180 | } 181 | 182 | #endif // ROARING_NODE_SERIALIZATION_FORMAT_ 183 | -------------------------------------------------------------------------------- /scripts/prebuild-local.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | /* 4 | * This script prebuild and test the library for all supported node versions locally for the local architecture. 5 | * Of course, it does not work on Windows. 6 | */ 7 | 8 | const colors = require("ansis"); 9 | const { printSystemInfo } = require("./system-info"); 10 | const fs = require("node:fs"); 11 | const path = require("node:path"); 12 | 13 | const { spawnAsync, mergeDirs, runMain, ROOT_FOLDER, forkAsync, NPM_COMMAND } = require("./lib/utils"); 14 | 15 | const { startPublishAssets } = require("./node-pre-gyp-publish"); 16 | 17 | const NODE_VERSIONS = ["20.9.0", "22.8.0", "24.5.0", "25.0.0"]; 18 | 19 | const NATIVE_DIR = path.resolve(ROOT_FOLDER, "native"); 20 | const STAGE_DIR = path.resolve(ROOT_FOLDER, "build/stage"); 21 | const STAGE_TMP_DIR = path.resolve(ROOT_FOLDER, ".tmp/stage"); 22 | const TOOLS_DIR = path.resolve(ROOT_FOLDER, ".tmp/tools"); 23 | const N_EXECUTABLE_PATH = path.resolve(TOOLS_DIR, "node_modules/.bin/n"); 24 | 25 | const rmdir = fs.promises.rm || fs.promises.rmdir; 26 | 27 | async function clean() { 28 | console.log(colors.blueBright("cleaning...")); 29 | const promises = []; 30 | if (fs.existsSync(NATIVE_DIR)) { 31 | promises.push(rmdir(NATIVE_DIR, { recursive: true })); 32 | } 33 | if (fs.existsSync(STAGE_DIR)) { 34 | promises.push(rmdir(STAGE_DIR, { recursive: true })); 35 | } 36 | if (fs.existsSync(STAGE_TMP_DIR)) { 37 | promises.push(rmdir(STAGE_TMP_DIR, { recursive: true })); 38 | } 39 | await Promise.all(promises); 40 | console.log(); 41 | } 42 | 43 | function printUsage() { 44 | let s = "usage:\n"; 45 | s += " node scripts/prebuild-local - build and test\n"; 46 | s += " node scripts/prebuild-local clean - cleans the native and build/stage directories\n"; 47 | s += " node scripts/prebuild-local package - build and test and package\n"; 48 | s += " node scripts/prebuild-local deploy - build, test, package and deploy\n"; 49 | s += "\n"; 50 | console.log(colors.yellow(s)); 51 | } 52 | 53 | async function main() { 54 | const command = process.argv[2]; 55 | 56 | if (process.argv.includes("--dev")) { 57 | throw new Error("Invalid argument --dev"); 58 | } 59 | if (process.argv.includes("--no-compile")) { 60 | throw new Error("Invalid argument --no-compile"); 61 | } 62 | 63 | console.log(colors.magentaBright("command: ", colors.italic(command) || "")); 64 | console.log(); 65 | 66 | if (command === "clean") { 67 | await clean(); 68 | return; 69 | } 70 | 71 | const isDeploy = command === "deploy"; 72 | const ensureVersionFlag = process.argv.includes("--ensure-version"); 73 | const isEnsureCommand = command === "ensure-version"; 74 | const isPackage = isDeploy || command === "package" || command === "pack"; 75 | 76 | const publisher = isDeploy ? await startPublishAssets() : undefined; 77 | 78 | if (isDeploy || isEnsureCommand) { 79 | publisher.checkGithubKey(); 80 | } 81 | 82 | if (isPackage) { 83 | await forkAsync(require.resolve("./build.js"), ["--no-compile"]); 84 | } 85 | 86 | if (command && !isDeploy && !isPackage) { 87 | printUsage(); 88 | process.exitCode = 1; 89 | return; 90 | } 91 | 92 | await clean(); 93 | 94 | console.log(colors.cyanBright(`* Building for node ${NODE_VERSIONS.join(" ")}\n`)); 95 | 96 | printSystemInfo(); 97 | 98 | console.log(colors.blueBright("- installing tools")); 99 | console.time("installing tools"); 100 | fs.mkdirSync(TOOLS_DIR, { recursive: true }); 101 | fs.writeFileSync( 102 | path.resolve(TOOLS_DIR, "package.json"), 103 | JSON.stringify({ name: "tools", private: true, dependencies: { n: "latest", "node-gyp": "latest" } }), 104 | ); 105 | await spawnAsync("npm", ["install", "--ignore-scripts", "--no-audit", "--no-save"], { 106 | cwd: TOOLS_DIR, 107 | }); 108 | process.env.npm_config_node_gyp = path.resolve(TOOLS_DIR, "node_modules/node-gyp/bin/node-gyp.js"); 109 | if (!fs.existsSync(process.env.npm_config_node_gyp)) { 110 | process.env.npm_config_node_gyp = require.resolve("node-gyp/bin/node-gyp.js"); 111 | } 112 | console.timeEnd("installing tools"); 113 | 114 | console.log(colors.blueBright("- installing node versions")); 115 | console.time("installing node versions"); 116 | await Promise.all( 117 | NODE_VERSIONS.map((nodeVersion) => spawnAsync(N_EXECUTABLE_PATH, ["i", nodeVersion, "--download"])), 118 | ); 119 | console.timeEnd("installing node versions"); 120 | 121 | console.log(colors.blueBright("- building")); 122 | console.time("building"); 123 | for (const nodeVersion of NODE_VERSIONS) { 124 | console.log(colors.blueBright(`\n- building for node ${nodeVersion}\n`)); 125 | const args = ["run", nodeVersion, path.resolve(ROOT_FOLDER, "node-pre-gyp.js"), "rebuild"]; 126 | if (isPackage) { 127 | args.push("package", "testpackage"); 128 | } 129 | await spawnAsync(N_EXECUTABLE_PATH, args); 130 | 131 | console.log(colors.blueBright("- testing")); 132 | const testArgs = ["run", nodeVersion, NPM_COMMAND, "run", "test"]; 133 | await spawnAsync(N_EXECUTABLE_PATH, testArgs); 134 | 135 | if (isDeploy) { 136 | console.log(colors.blueBright("- uploading")); 137 | await publisher.upload(); 138 | } 139 | 140 | if (isPackage) { 141 | // Move stage folder to a temporary folder so it does not get clean by node-gyp 142 | await mergeDirs(STAGE_DIR, STAGE_TMP_DIR); 143 | } 144 | } 145 | console.timeEnd("building"); 146 | 147 | if (isPackage) { 148 | // Move the directory back to build/stage 149 | await mergeDirs(STAGE_TMP_DIR, STAGE_DIR); 150 | if (fs.existsSync(STAGE_TMP_DIR)) { 151 | await rmdir(STAGE_TMP_DIR, { recursive: true }); 152 | } 153 | } 154 | 155 | // If invoked only to ensure the release/tag exists, do it now and exit. 156 | if (ensureVersionFlag || isEnsureCommand) { 157 | if (!publisher) { 158 | console.log( 159 | colors.yellow( 160 | "No publisher available to ensure version. Are you running with 'deploy' or 'ensure-version' command?", 161 | ), 162 | ); 163 | process.exitCode = 1; 164 | return; 165 | } 166 | const ok = await publisher.ensureRelease(); 167 | if (ok) { 168 | console.log(colors.greenBright(`Release ${require("../package.json").version} exists or was created.`)); 169 | return; 170 | } 171 | console.log( 172 | colors.yellowBright( 173 | `Release ${require("../package.json").version} could not be ensured (branch or authentication?).`, 174 | ), 175 | ); 176 | process.exitCode = 1; 177 | } 178 | } 179 | 180 | runMain(main, "prebuild-local"); 181 | -------------------------------------------------------------------------------- /test/RoaringBitmap32/RoaringBitmap33.deserializeAsync.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from "vitest"; 2 | import RoaringBitmap32 from "../../RoaringBitmap32"; 3 | 4 | describe("RoaringBitmap32 deserializeAsync", () => { 5 | describe("async/await", () => { 6 | describe("empty buffer", () => { 7 | it("deserializes an empty buffer (non portable)", async () => { 8 | const bitmap = await RoaringBitmap32.deserializeAsync(Buffer.from([]), false); 9 | expect(bitmap).to.be.instanceOf(RoaringBitmap32); 10 | expect(bitmap.size).eq(0); 11 | }); 12 | 13 | it("deserializes an empty buffer (portable)", async () => { 14 | const bitmap = await RoaringBitmap32.deserializeAsync(Buffer.from([]), true); 15 | expect(bitmap).to.be.instanceOf(RoaringBitmap32); 16 | expect(bitmap.size).eq(0); 17 | }); 18 | }); 19 | 20 | describe("empty bitmap", () => { 21 | it("deserializes an empty bitmap (non portable)", async () => { 22 | const bitmap = await RoaringBitmap32.deserializeAsync(Buffer.from([1, 0, 0, 0, 0]), false); 23 | expect(bitmap).to.be.instanceOf(RoaringBitmap32); 24 | expect(bitmap.size).eq(0); 25 | }); 26 | 27 | it("deserializes an empty bitmap (portable)", async () => { 28 | const bitmap = await RoaringBitmap32.deserializeAsync(Buffer.from([58, 48, 0, 0, 0, 0, 0, 0]), true); 29 | expect(bitmap).to.be.instanceOf(RoaringBitmap32); 30 | expect(bitmap.size).eq(0); 31 | }); 32 | }); 33 | 34 | it("deserializes simple bitmap", async () => { 35 | const values = [1, 2, 100, 101, 105, 109, 0x7fffffff, 0xfffffffe, 0xffffffff]; 36 | 37 | const bufferNonPortable = new RoaringBitmap32(values).serialize(false); 38 | const bufferPortable = new RoaringBitmap32(values).serialize(true); 39 | 40 | const promises: Promise[] = []; 41 | for (let i = 0; i < 10; ++i) { 42 | promises.push(RoaringBitmap32.deserializeAsync(bufferNonPortable, false)); 43 | promises.push(RoaringBitmap32.deserializeAsync(bufferPortable, true)); 44 | } 45 | 46 | for (const bitmap of await Promise.all(promises)) { 47 | expect(bitmap).to.be.instanceOf(RoaringBitmap32); 48 | expect(bitmap.toArray()).deep.equal(values); 49 | } 50 | }); 51 | 52 | it("deserializes larger bitmap", async () => { 53 | const bitmap = new RoaringBitmap32(); 54 | let rnd = 112043213; 55 | for (let i = 0; i < 1000; ++i) { 56 | rnd = ((rnd + i * 25253) * 3924461) >>> 0; 57 | bitmap.add(rnd); 58 | } 59 | const bufferNonPortable = bitmap.serialize(false); 60 | const bufferPortable = bitmap.serialize(true); 61 | 62 | const promises: Promise[] = []; 63 | for (let i = 0; i < 5; ++i) { 64 | promises.push(RoaringBitmap32.deserializeAsync(bufferNonPortable, false)); 65 | promises.push(RoaringBitmap32.deserializeAsync(bufferPortable, true)); 66 | } 67 | 68 | const resolved = await Promise.all(promises); 69 | 70 | for (const b of resolved) { 71 | expect(b).to.be.instanceOf(RoaringBitmap32); 72 | expect(b.isEqual(bitmap)).eq(true); 73 | } 74 | }); 75 | 76 | it("propagates deserialization errors", async () => { 77 | const wrongBuffer = Buffer.from([99, 98, 97]); 78 | const promise = RoaringBitmap32.deserializeAsync(wrongBuffer, false); 79 | let error: any; 80 | try { 81 | await promise; 82 | } catch (e) { 83 | error = e; 84 | } 85 | expect(error.message).eq("RoaringBitmap32 deserialization - invalid portable header byte"); 86 | }); 87 | }); 88 | 89 | describe("callback", () => { 90 | describe("empty buffer", () => { 91 | it("deserializes an empty buffer (non portable)", () => { 92 | return new Promise((resolve, reject) => { 93 | expect( 94 | RoaringBitmap32.deserializeAsync(Buffer.from([]), false, (error, bitmap) => { 95 | if (error) { 96 | reject(error); 97 | return; 98 | } 99 | try { 100 | expect(error).to.be.null; 101 | expect(bitmap).to.be.instanceOf(RoaringBitmap32); 102 | expect(bitmap?.size).eq(0); 103 | resolve(); 104 | } catch (e) { 105 | reject(e); 106 | } 107 | }), 108 | ).to.be.undefined; 109 | }); 110 | }); 111 | 112 | it("deserializes an empty buffer (portable)", () => { 113 | return new Promise((resolve, reject) => { 114 | expect( 115 | RoaringBitmap32.deserializeAsync(Buffer.from([]), true, (error, bitmap) => { 116 | if (error) { 117 | reject(error); 118 | return; 119 | } 120 | try { 121 | expect(error).to.be.null; 122 | expect(bitmap).to.be.instanceOf(RoaringBitmap32); 123 | expect(bitmap?.size).eq(0); 124 | resolve(); 125 | } catch (e) { 126 | reject(e); 127 | } 128 | }), 129 | ).to.be.undefined; 130 | }); 131 | }); 132 | }); 133 | 134 | describe("empty bitmap", () => { 135 | it("deserializes an empty bitmap (non portable)", () => { 136 | return new Promise((resolve, reject) => { 137 | expect( 138 | RoaringBitmap32.deserializeAsync(Buffer.from([1, 0, 0, 0, 0]), false, (error, bitmap) => { 139 | if (error) { 140 | reject(error); 141 | return; 142 | } 143 | try { 144 | expect(error).to.be.null; 145 | expect(bitmap).to.be.instanceOf(RoaringBitmap32); 146 | expect(bitmap?.size).eq(0); 147 | resolve(); 148 | } catch (e) { 149 | reject(e); 150 | } 151 | }), 152 | ).to.be.undefined; 153 | }); 154 | }); 155 | 156 | it("deserializes an empty bitmap (portable)", () => { 157 | return new Promise((resolve, reject) => { 158 | expect( 159 | RoaringBitmap32.deserializeAsync(Buffer.from([58, 48, 0, 0, 0, 0, 0, 0]), true, (error, bitmap) => { 160 | if (error) { 161 | reject(error); 162 | return; 163 | } 164 | try { 165 | expect(error).to.be.null; 166 | expect(bitmap).to.be.instanceOf(RoaringBitmap32); 167 | expect(bitmap?.size).eq(0); 168 | resolve(); 169 | } catch (e) { 170 | reject(e); 171 | } 172 | }), 173 | ).to.be.undefined; 174 | }); 175 | }); 176 | }); 177 | 178 | it("deserializes simple bitmap", () => { 179 | return new Promise((resolve, reject) => { 180 | const values = [1, 2, 100, 101, 105, 109, 0x7fffffff, 0xfffffffe, 0xffffffff]; 181 | 182 | const bufferNonPortable = new RoaringBitmap32(values).serialize(false); 183 | 184 | expect( 185 | RoaringBitmap32.deserializeAsync(bufferNonPortable, false, (error, bitmap) => { 186 | if (error) { 187 | reject(error); 188 | return; 189 | } 190 | try { 191 | expect(error).to.be.null; 192 | expect(bitmap).to.be.instanceOf(RoaringBitmap32); 193 | expect(bitmap?.toArray()).deep.equal(values); 194 | resolve(); 195 | } catch (e) { 196 | reject(e); 197 | } 198 | }), 199 | ).to.be.undefined; 200 | }); 201 | }); 202 | 203 | it("propagates deserialization errors", () => { 204 | return new Promise((resolve, reject) => { 205 | const wrongBuffer = Buffer.from([99, 98, 97]); 206 | expect( 207 | RoaringBitmap32.deserializeAsync(wrongBuffer, false, (error, bitmap) => { 208 | try { 209 | expect(bitmap).to.be.undefined; 210 | expect(error?.message).eq("RoaringBitmap32 deserialization - invalid portable header byte"); 211 | resolve(); 212 | } catch (e) { 213 | reject(e); 214 | } 215 | }), 216 | ).to.be.undefined; 217 | }); 218 | }); 219 | }); 220 | }); 221 | -------------------------------------------------------------------------------- /src/cpp/RoaringBitmap32BufferedIterator.h: -------------------------------------------------------------------------------- 1 | #ifndef ROARING_NODE_ROARING_BITMAP_32_BUFFERED_ITERATOR_ 2 | #define ROARING_NODE_ROARING_BITMAP_32_BUFFERED_ITERATOR_ 3 | 4 | #include "RoaringBitmap32.h" 5 | 6 | class RoaringBitmap32BufferedIterator final : public ObjectWrap { 7 | public: 8 | static constexpr const uint64_t OBJECT_TOKEN = 0x21524F4152490000; 9 | 10 | enum { allocatedMemoryDelta = 1024 }; 11 | 12 | roaring_uint32_iterator_t it; 13 | bool reversed; 14 | RoaringBitmap32 * bitmapInstance; 15 | int64_t bitmapVersion; 16 | v8utils::TypedArrayContent bufferContent; 17 | 18 | v8::Global bitmap; 19 | v8::Global persistent; 20 | 21 | explicit RoaringBitmap32BufferedIterator(AddonData * addonData, bool reversed) : 22 | ObjectWrap(addonData), reversed(reversed), bitmapInstance(nullptr) { 23 | this->it.parent = nullptr; 24 | this->it.has_value = false; 25 | _gcaware_adjustAllocatedMemory(this->isolate, sizeof(RoaringBitmap32BufferedIterator)); 26 | } 27 | 28 | ~RoaringBitmap32BufferedIterator() { 29 | this->destroy(); 30 | _gcaware_adjustAllocatedMemory(this->isolate, -sizeof(RoaringBitmap32BufferedIterator)); 31 | } 32 | 33 | inline uint32_t _fill() { 34 | uint32_t n; 35 | if (this->reversed) { 36 | size_t size = this->bufferContent.length; 37 | auto * data = this->bufferContent.data; 38 | for (n = 0; n < size && this->it.has_value; ++n) { 39 | data[n] = this->it.current_value; 40 | roaring_uint32_iterator_previous(&this->it); 41 | } 42 | } else { 43 | n = roaring_uint32_iterator_read(&this->it, this->bufferContent.data, this->bufferContent.length); 44 | } 45 | if (n == 0) { 46 | this->bitmapInstance = nullptr; 47 | this->bufferContent.reset(); 48 | this->bitmap.Reset(); 49 | } 50 | return n; 51 | } 52 | 53 | inline void close() { 54 | this->bitmapInstance = nullptr; 55 | this->bitmapVersion = 0; 56 | this->bufferContent.reset(); 57 | this->bitmap.Reset(); 58 | this->it.has_value = false; 59 | } 60 | 61 | private: 62 | void destroy() { 63 | this->bitmap.Reset(); 64 | if (!this->persistent.IsEmpty()) { 65 | this->persistent.ClearWeak(); 66 | this->persistent.Reset(); 67 | } 68 | } 69 | }; 70 | 71 | void RoaringBitmap32BufferedIterator_fill(const v8::FunctionCallbackInfo & info) { 72 | v8::Isolate * isolate = info.GetIsolate(); 73 | 74 | RoaringBitmap32BufferedIterator * instance = ObjectWrap::TryUnwrap(info.This(), isolate); 75 | 76 | RoaringBitmap32 * bitmapInstance = instance ? instance->bitmapInstance : nullptr; 77 | 78 | if (bitmapInstance == nullptr) { 79 | return info.GetReturnValue().Set(0U); 80 | } 81 | 82 | if (bitmapInstance->getVersion() != instance->bitmapVersion) { 83 | return v8utils::throwError(isolate, "RoaringBitmap32 iterator - bitmap changed while iterating"); 84 | } 85 | 86 | return info.GetReturnValue().Set(instance->_fill()); 87 | } 88 | 89 | void RoaringBitmap32BufferedIterator_close(const v8::FunctionCallbackInfo & info) { 90 | RoaringBitmap32BufferedIterator * instance = 91 | ObjectWrap::TryUnwrap(info.This(), info.GetIsolate()); 92 | if (instance == nullptr) { 93 | return; 94 | } 95 | instance->close(); 96 | } 97 | 98 | void RoaringBitmap32BufferedIterator_WeakCallback(v8::WeakCallbackInfo const & info) { 99 | RoaringBitmap32BufferedIterator * p = info.GetParameter(); 100 | if (p != nullptr) { 101 | p->~RoaringBitmap32BufferedIterator(); 102 | bare_aligned_free(p); 103 | } 104 | } 105 | 106 | void RoaringBitmap32BufferedIterator_New(const v8::FunctionCallbackInfo & info) { 107 | v8::Isolate * isolate = info.GetIsolate(); 108 | 109 | if (!info.IsConstructCall()) { 110 | return v8utils::throwTypeError(isolate, "RoaringBitmap32BufferedIterator::ctor - needs to be called with new"); 111 | } 112 | 113 | auto holder = info.This(); 114 | 115 | if (info.Length() < 2) { 116 | return v8utils::throwTypeError(isolate, "RoaringBitmap32BufferedIterator::ctor - needs 2 or 3 arguments"); 117 | } 118 | 119 | AddonData * addonData = AddonData::get(info); 120 | if (addonData == nullptr) { 121 | return v8utils::throwTypeError(isolate, ERROR_INVALID_OBJECT); 122 | } 123 | 124 | RoaringBitmap32 * bitmapInstance = ObjectWrap::TryUnwrap(info[0], isolate); 125 | if (bitmapInstance == nullptr) { 126 | return v8utils::throwTypeError( 127 | isolate, "RoaringBitmap32BufferedIterator::ctor - first argument must be of type RoaringBitmap32"); 128 | } 129 | 130 | bool reversed = info.Length() > 2 && info[2]->BooleanValue(isolate); 131 | 132 | auto bufferObject = info[1]; 133 | 134 | if (!bufferObject->IsUint32Array()) { 135 | return v8utils::throwTypeError( 136 | isolate, "RoaringBitmap32BufferedIterator::ctor - second argument must be of type Uint32Array"); 137 | } 138 | 139 | const v8utils::TypedArrayContent bufferContent(isolate, bufferObject); 140 | if (!bufferContent.data || bufferContent.length < 1) { 141 | return v8utils::throwTypeError(isolate, "RoaringBitmap32BufferedIterator::ctor - invalid Uint32Array buffer"); 142 | } 143 | 144 | auto * instanceMemory = bare_aligned_malloc(16, sizeof(RoaringBitmap32BufferedIterator)); 145 | auto * instance = instanceMemory ? new (instanceMemory) RoaringBitmap32BufferedIterator(addonData, reversed) : nullptr; 146 | if (instance == nullptr) { 147 | return v8utils::throwError(isolate, "RoaringBitmap32BufferedIterator::ctor - allocation failed"); 148 | } 149 | 150 | int indices[2] = {0, 1}; 151 | void * values[2] = {instance, (void *)(RoaringBitmap32BufferedIterator::OBJECT_TOKEN)}; 152 | holder->SetAlignedPointerInInternalFields(2, indices, values); 153 | 154 | info.GetReturnValue().Set(holder); 155 | 156 | instance->persistent.Reset(isolate, holder); 157 | instance->persistent.SetWeak(instance, RoaringBitmap32BufferedIterator_WeakCallback, v8::WeakCallbackType::kParameter); 158 | 159 | auto context = isolate->GetCurrentContext(); 160 | 161 | instance->bitmapInstance = bitmapInstance; 162 | instance->bitmapVersion = bitmapInstance->getVersion(); 163 | 164 | auto a0Maybe = info[0]->ToObject(context); 165 | v8::Local a0; 166 | if (!a0Maybe.ToLocal(&a0)) { 167 | return v8utils::throwError(isolate, "RoaringBitmap32BufferedIterator::ctor - allocation failed"); 168 | } 169 | instance->bitmap.Reset(isolate, a0); 170 | 171 | auto a1Maybe = bufferObject->ToObject(context); 172 | v8::Local a1; 173 | if (!a1Maybe.ToLocal(&a1)) { 174 | return v8utils::throwError(isolate, "RoaringBitmap32BufferedIterator::ctor - allocation failed"); 175 | } 176 | 177 | instance->bufferContent.set(isolate, bufferObject); 178 | 179 | if (reversed) { 180 | roaring_iterator_init_last(bitmapInstance->roaring, &instance->it); 181 | } else { 182 | roaring_iterator_init(bitmapInstance->roaring, &instance->it); 183 | } 184 | 185 | uint32_t n = instance->_fill(); 186 | 187 | if (holder->Set(context, addonData->strings.n.Get(isolate), v8::Uint32::NewFromUnsigned(isolate, n)).IsNothing()) { 188 | return v8utils::throwError(isolate, "RoaringBitmap32BufferedIterator::ctor - instantiation failed"); 189 | } 190 | } 191 | 192 | void RoaringBitmap32BufferedIterator_Init(v8::Local exports, AddonData * addonData) { 193 | v8::Isolate * isolate = addonData->isolate; 194 | 195 | auto className = NEW_LITERAL_V8_STRING(isolate, "RoaringBitmap32BufferedIterator", v8::NewStringType::kInternalized); 196 | 197 | v8::Local ctor = 198 | v8::FunctionTemplate::New(isolate, RoaringBitmap32BufferedIterator_New, addonData->external.Get(isolate)); 199 | 200 | ctor->SetClassName(className); 201 | ctor->InstanceTemplate()->SetInternalFieldCount(2); 202 | 203 | addonData->RoaringBitmap32BufferedIterator_constructorTemplate.Reset(isolate, ctor); 204 | 205 | NODE_SET_PROTOTYPE_METHOD(ctor, "fill", RoaringBitmap32BufferedIterator_fill); 206 | NODE_SET_PROTOTYPE_METHOD(ctor, "close", RoaringBitmap32BufferedIterator_close); 207 | 208 | auto ctorFunctionMaybe = ctor->GetFunction(isolate->GetCurrentContext()); 209 | v8::Local ctorFunction; 210 | 211 | if (!ctorFunctionMaybe.ToLocal(&ctorFunction)) { 212 | return v8utils::throwError(isolate, "Failed to instantiate RoaringBitmap32BufferedIterator"); 213 | } 214 | 215 | addonData->RoaringBitmap32BufferedIterator_constructor.Reset(isolate, ctorFunction); 216 | v8utils::defineHiddenField(isolate, exports, "RoaringBitmap32BufferedIterator", ctorFunction); 217 | } 218 | 219 | #endif // ROARING_NODE_ROARING_BITMAP_32_BUFFERED_ITERATOR_ 220 | -------------------------------------------------------------------------------- /test/RoaringBitmap32/RoaringBitmap32.serialization-file.test.ts: -------------------------------------------------------------------------------- 1 | import fs from "node:fs"; 2 | import path from "node:path"; 3 | import { beforeAll, describe, expect, it } from "vitest"; 4 | import RoaringBitmap32 from "../../RoaringBitmap32"; 5 | import type { FileSerializationDeserializationFormatType } from "../helpers/roaring"; 6 | import { FileDeserializationFormat, FileSerializationFormat } from "../helpers/roaring"; 7 | 8 | const tmpDir = path.resolve(__dirname, "..", "..", ".tmp", "tests"); 9 | 10 | describe("RoaringBitmap32 file serialization", () => { 11 | beforeAll(() => { 12 | if (!fs.existsSync(tmpDir)) { 13 | fs.mkdirSync(tmpDir, { recursive: true }); 14 | } 15 | }); 16 | 17 | describe("FileSerializationFormat", () => { 18 | it("should have the right values", () => { 19 | expect(FileSerializationFormat.croaring).eq("croaring"); 20 | expect(FileSerializationFormat.portable).eq("portable"); 21 | expect(FileSerializationFormat.uint32_array).eq("uint32_array"); 22 | expect(FileSerializationFormat.unsafe_frozen_croaring).eq("unsafe_frozen_croaring"); 23 | expect(FileSerializationFormat.comma_separated_values).eq("comma_separated_values"); 24 | expect(FileSerializationFormat.tab_separated_values).eq("tab_separated_values"); 25 | expect(FileSerializationFormat.newline_separated_values).eq("newline_separated_values"); 26 | expect(FileSerializationFormat.json_array).eq("json_array"); 27 | 28 | expect(Object.values(FileSerializationFormat)).to.deep.eq([ 29 | "croaring", 30 | "portable", 31 | "unsafe_frozen_croaring", 32 | "uint32_array", 33 | "comma_separated_values", 34 | "tab_separated_values", 35 | "newline_separated_values", 36 | "json_array", 37 | ]); 38 | 39 | expect(RoaringBitmap32.FileSerializationFormat).to.eq(FileSerializationFormat); 40 | 41 | expect(new RoaringBitmap32().FileSerializationFormat).to.eq(FileSerializationFormat); 42 | }); 43 | }); 44 | 45 | describe("FileDeserializationFormat", () => { 46 | it("should have the right values", () => { 47 | expect(FileDeserializationFormat.croaring).eq("croaring"); 48 | expect(FileDeserializationFormat.portable).eq("portable"); 49 | expect(FileDeserializationFormat.unsafe_frozen_croaring).eq("unsafe_frozen_croaring"); 50 | expect(FileDeserializationFormat.unsafe_frozen_portable).eq("unsafe_frozen_portable"); 51 | expect(FileDeserializationFormat.comma_separated_values).eq("comma_separated_values"); 52 | expect(FileDeserializationFormat.tab_separated_values).eq("tab_separated_values"); 53 | expect(FileDeserializationFormat.newline_separated_values).eq("newline_separated_values"); 54 | expect(FileDeserializationFormat.json_array).eq("json_array"); 55 | 56 | expect(Object.values(FileDeserializationFormat)).to.deep.eq([ 57 | "croaring", 58 | "portable", 59 | "unsafe_frozen_croaring", 60 | "unsafe_frozen_portable", 61 | "uint32_array", 62 | "comma_separated_values", 63 | "tab_separated_values", 64 | "newline_separated_values", 65 | "json_array", 66 | ]); 67 | 68 | expect(RoaringBitmap32.FileDeserializationFormat).to.eq(FileDeserializationFormat); 69 | 70 | expect(new RoaringBitmap32().FileDeserializationFormat).to.eq(FileDeserializationFormat); 71 | }); 72 | }); 73 | 74 | it("serialize and deserialize empty bitmaps in various formats", async () => { 75 | const formats: FileSerializationDeserializationFormatType[] = [ 76 | "portable", 77 | "croaring", 78 | "unsafe_frozen_croaring", 79 | "uint32_array", 80 | "comma_separated_values", 81 | "tab_separated_values", 82 | "newline_separated_values", 83 | "json_array", 84 | ]; 85 | for (const format of formats) { 86 | const tmpFilePath = path.resolve(tmpDir, `test-ϴ-${format}.bin`); 87 | await new RoaringBitmap32().serializeFileAsync(tmpFilePath, format); 88 | expect((await RoaringBitmap32.deserializeFileAsync(tmpFilePath, format)).toArray()).to.deep.equal([]); 89 | expect(RoaringBitmap32.deserializeFile(tmpFilePath, format).toArray()).to.deep.equal([]); 90 | } 91 | }); 92 | 93 | it("serialize and deserialize in various formats", async () => { 94 | for (const format of ["portable", "croaring", "unsafe_frozen_croaring", "uint32_array"] as const) { 95 | const tmpFilePath = path.resolve(tmpDir, `test-1-${format}.bin`); 96 | const data = [1, 2, 3, 100, 0xfffff, 0xffffffff]; 97 | await new RoaringBitmap32(data).serializeFileAsync(tmpFilePath, format); 98 | expect((await RoaringBitmap32.deserializeFileAsync(tmpFilePath, format)).toArray()).to.deep.equal(data); 99 | expect(RoaringBitmap32.deserializeFile(tmpFilePath, format).toArray()).to.deep.equal(data); 100 | } 101 | }); 102 | 103 | it("serializeFileAsync truncates file if it already exists", async () => { 104 | const tmpFilePath = path.resolve(tmpDir, `test-truncate.bin`); 105 | await fs.promises.writeFile(tmpFilePath, Buffer.alloc(10000)); 106 | await new RoaringBitmap32([1, 2, 3]).serializeFileAsync(tmpFilePath, "portable"); 107 | expect((await RoaringBitmap32.deserializeFileAsync(tmpFilePath, "portable")).toArray()).to.deep.equal([1, 2, 3]); 108 | expect(RoaringBitmap32.deserializeFile(tmpFilePath, "portable").toArray()).to.deep.equal([1, 2, 3]); 109 | }); 110 | 111 | it("throws ENOENT if file does not exist", async () => { 112 | const tmpFilePath = path.resolve(tmpDir, `test-ENOENT.bin`); 113 | let error: any; 114 | try { 115 | await RoaringBitmap32.deserializeFileAsync(tmpFilePath, "portable"); 116 | } catch (e) { 117 | error = e; 118 | } 119 | expect(error).to.be.an.instanceOf(Error); 120 | expect(error.message).to.match(/^ENOENT, No such file or directory/); 121 | expect(error.code).to.equal("ENOENT"); 122 | expect(error.syscall).to.equal("open"); 123 | expect(error.path).to.equal(tmpFilePath); 124 | 125 | let syncError: any; 126 | try { 127 | RoaringBitmap32.deserializeFile(tmpFilePath, "portable"); 128 | } catch (e) { 129 | syncError = e; 130 | } 131 | expect(syncError).to.be.an.instanceOf(Error); 132 | expect(syncError.message).to.match(/^ENOENT, No such file or directory/); 133 | expect(syncError.code).to.equal("ENOENT"); 134 | expect(syncError.syscall).to.equal("open"); 135 | expect(syncError.path).to.equal(tmpFilePath); 136 | }); 137 | 138 | it("serializes to comma_separated_values", async () => { 139 | const tmpFilePath = path.resolve(tmpDir, `test-csv.csv`); 140 | const bmp = new RoaringBitmap32([1, 2, 3, 100, 14120, 3481983]); 141 | bmp.addRange(0x100, 0x120); 142 | await bmp.serializeFileAsync(tmpFilePath, "comma_separated_values"); 143 | const text = await fs.promises.readFile(tmpFilePath, "utf8"); 144 | expect(text).to.equal( 145 | "1,2,3,100,256,257,258,259,260,261,262,263,264,265,266,267,268,269,270,271,272,273,274,275,276,277,278,279,280,281,282,283,284,285,286,287,14120,3481983", 146 | ); 147 | }); 148 | 149 | it("serializes to newline_separated_values", async () => { 150 | const tmpFilePath = path.resolve(tmpDir, `test-nlf.csv`); 151 | const bmp = new RoaringBitmap32([1, 2, 3, 100, 14120, 3481983]); 152 | bmp.addRange(0x100, 0x120); 153 | await bmp.serializeFileAsync(tmpFilePath, "newline_separated_values"); 154 | const text = await fs.promises.readFile(tmpFilePath, "utf8"); 155 | expect(text).to.equal( 156 | "1,2,3,100,256,257,258,259,260,261,262,263,264,265,266,267,268,269,270,271,272,273,274,275,276,277,278,279,280,281,282,283,284,285,286,287,14120,3481983,".replace( 157 | /,/g, 158 | "\n", 159 | ), 160 | ); 161 | }); 162 | 163 | it("serializes to tab_separated_values", async () => { 164 | const tmpFilePath = path.resolve(tmpDir, `test-ntab.csv`); 165 | const bmp = new RoaringBitmap32([1, 2, 3, 100, 14120, 3481983]); 166 | bmp.addRange(0x100, 0x120); 167 | await bmp.serializeFileAsync(tmpFilePath, "tab_separated_values"); 168 | const text = await fs.promises.readFile(tmpFilePath, "utf8"); 169 | expect(text).to.equal( 170 | "1,2,3,100,256,257,258,259,260,261,262,263,264,265,266,267,268,269,270,271,272,273,274,275,276,277,278,279,280,281,282,283,284,285,286,287,14120,3481983".replace( 171 | /,/g, 172 | "\t", 173 | ), 174 | ); 175 | }); 176 | 177 | it("serializes to an empty json array", async () => { 178 | const tmpFilePath = path.resolve(tmpDir, `test-json-array-empty.csv`); 179 | await new RoaringBitmap32().serializeFileAsync(tmpFilePath, "json_array"); 180 | expect(await fs.promises.readFile(tmpFilePath, "utf8")).to.equal("[]"); 181 | }); 182 | 183 | it("serializes to a json array", async () => { 184 | const tmpFilePath = path.resolve(tmpDir, `test-json-array.csv`); 185 | const bmp = new RoaringBitmap32([1, 2, 3, 100, 14120, 3481983]); 186 | bmp.addRange(0x100, 0x120); 187 | await bmp.serializeFileAsync(tmpFilePath, "json_array"); 188 | const text = await fs.promises.readFile(tmpFilePath, "utf8"); 189 | expect(text).to.equal( 190 | "[1,2,3,100,256,257,258,259,260,261,262,263,264,265,266,267,268,269,270,271,272,273,274,275,276,277,278,279,280,281,282,283,284,285,286,287,14120,3481983]", 191 | ); 192 | }); 193 | }); 194 | -------------------------------------------------------------------------------- /test/RoaringBitmap32/RoaringBitmap32.frozen.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from "vitest"; 2 | import RoaringBitmap32 from "../../RoaringBitmap32"; 3 | import { FrozenViewFormat } from "../helpers/roaring"; 4 | 5 | const ERROR_FROZEN = "This bitmap is frozen and cannot be modified"; 6 | 7 | describe("RoaringBitmap32 frozen", () => { 8 | describe("FrozenViewFormat", () => { 9 | it("should have the right values", () => { 10 | expect(FrozenViewFormat.unsafe_frozen_croaring).eq("unsafe_frozen_croaring"); 11 | expect(FrozenViewFormat.unsafe_frozen_portable).eq("unsafe_frozen_portable"); 12 | 13 | expect(Object.values(FrozenViewFormat)).to.deep.eq(["unsafe_frozen_croaring", "unsafe_frozen_portable"]); 14 | 15 | expect(RoaringBitmap32.FrozenViewFormat).to.eq(FrozenViewFormat); 16 | 17 | expect(new RoaringBitmap32().FrozenViewFormat).to.eq(FrozenViewFormat); 18 | }); 19 | }); 20 | 21 | describe("freeze", () => { 22 | it("set isFrozen to true, return this, can be called multiple times", () => { 23 | const bitmap = new RoaringBitmap32(); 24 | expect(bitmap.isFrozen).to.be.false; 25 | expect(bitmap.freeze()).to.equal(bitmap); 26 | expect(bitmap.isFrozen).to.be.true; 27 | 28 | expect(bitmap.freeze()).to.equal(bitmap); 29 | expect(bitmap.isFrozen).to.be.true; 30 | }); 31 | 32 | it("throws when calling writable methods", () => { 33 | const bitmap = new RoaringBitmap32(); 34 | bitmap.freeze(); 35 | 36 | expect(() => bitmap.copyFrom([1])).to.throw(ERROR_FROZEN); 37 | expect(() => bitmap.add(1)).to.throw(ERROR_FROZEN); 38 | expect(() => bitmap.tryAdd(1)).to.throw(ERROR_FROZEN); 39 | expect(() => bitmap.addMany([1])).to.throw(ERROR_FROZEN); 40 | expect(() => bitmap.remove(1)).to.throw(ERROR_FROZEN); 41 | expect(() => bitmap.removeMany([1])).to.throw(ERROR_FROZEN); 42 | expect(() => bitmap.delete(1)).to.throw(ERROR_FROZEN); 43 | expect(() => bitmap.clear()).to.throw(ERROR_FROZEN); 44 | expect(() => bitmap.orInPlace([1])).to.throw(ERROR_FROZEN); 45 | expect(() => bitmap.andNotInPlace([1])).to.throw(ERROR_FROZEN); 46 | expect(() => bitmap.andInPlace([1])).to.throw(ERROR_FROZEN); 47 | expect(() => bitmap.xorInPlace([1])).to.throw(ERROR_FROZEN); 48 | expect(() => bitmap.flipRange(0, 10)).to.throw(ERROR_FROZEN); 49 | expect(() => bitmap.addRange(0, 9)).to.throw(ERROR_FROZEN); 50 | expect(() => bitmap.removeRange(0, 8)).to.throw(ERROR_FROZEN); 51 | 52 | const bitmap1 = new RoaringBitmap32(); 53 | const bitmap2 = new RoaringBitmap32(); 54 | bitmap1.freeze(); 55 | expect(() => RoaringBitmap32.swap(bitmap1, bitmap2)).to.throw(ERROR_FROZEN); 56 | expect(() => RoaringBitmap32.swap(bitmap2, bitmap1)).to.throw(ERROR_FROZEN); 57 | }); 58 | }); 59 | 60 | describe("serializeAsync", () => { 61 | it("Is temporarily frozen while using serializeAsync", async () => { 62 | const bitmap = new RoaringBitmap32([1, 2, 3]); 63 | expect(bitmap.isFrozen).to.be.false; 64 | const promise = bitmap.serializeAsync("croaring"); 65 | expect(bitmap.isFrozen).to.be.true; 66 | await promise; 67 | expect(bitmap.isFrozen).to.be.false; 68 | bitmap.add(4); 69 | }); 70 | 71 | it('keep it frozen after calling "serializeAsync"', async () => { 72 | const bitmap = new RoaringBitmap32([1, 2, 3]); 73 | bitmap.freeze(); 74 | expect(bitmap.isFrozen).to.be.true; 75 | const promise = bitmap.serializeAsync("croaring"); 76 | expect(bitmap.isFrozen).to.be.true; 77 | await promise; 78 | expect(bitmap.isFrozen).to.be.true; 79 | expect(() => bitmap.add(4)).to.throw(ERROR_FROZEN); 80 | }); 81 | }); 82 | 83 | describe("unsafeFrozenView", () => { 84 | it("throws if the buffer is an invalid type", () => { 85 | expect(() => RoaringBitmap32.unsafeFrozenView({} as any, "unsafe_frozen_croaring")).to.throw(); 86 | expect(() => RoaringBitmap32.unsafeFrozenView([] as any, "unsafe_frozen_croaring")).to.throw(); 87 | expect(() => RoaringBitmap32.unsafeFrozenView(null as any, "unsafe_frozen_croaring")).to.throw(); 88 | expect(() => RoaringBitmap32.unsafeFrozenView(undefined as any, "unsafe_frozen_croaring")).to.throw(); 89 | expect(() => RoaringBitmap32.unsafeFrozenView(new Uint32Array(1), "unsafe_frozen_croaring")).to.throw(); 90 | }); 91 | 92 | it("throws if format is invalid", () => { 93 | expect(() => RoaringBitmap32.unsafeFrozenView(new Uint8Array(10), "a" as any)).to.throw(); 94 | expect(() => RoaringBitmap32.unsafeFrozenView(new Uint8Array(10), "" as any)).to.throw(); 95 | expect(() => RoaringBitmap32.unsafeFrozenView(new Uint8Array(10), undefined as any)).to.throw(); 96 | expect(() => RoaringBitmap32.unsafeFrozenView(new Uint8Array(10), null as any)).to.throw(); 97 | }); 98 | 99 | it("can create a view from a serialized bitmap, and the view is frozen", () => { 100 | const values = [1, 2, 3, 100, 8772837, 0x7ffffff1, 0x7fffffff]; 101 | const bitmap = new RoaringBitmap32(values); 102 | const serialized = bitmap.serialize("unsafe_frozen_croaring"); 103 | expect(RoaringBitmap32.isBufferAligned(serialized)).to.be.true; 104 | const view = RoaringBitmap32.unsafeFrozenView(serialized, "unsafe_frozen_croaring"); 105 | expect(view.isFrozen).to.be.true; 106 | expect(view.toArray()).to.deep.equal(values); 107 | expect(() => view.add(4)).to.throw(ERROR_FROZEN); 108 | expect(() => view.runOptimize()).to.throw(ERROR_FROZEN); 109 | expect(() => view.shrinkToFit()).to.throw(ERROR_FROZEN); 110 | expect(() => view.removeRunCompression()).to.throw(ERROR_FROZEN); 111 | 112 | const copy = view.clone(); 113 | expect(copy.isFrozen).to.be.false; 114 | expect(copy.toArray()).to.deep.equal(values); 115 | expect(copy.tryAdd(4)).to.be.true; 116 | }); 117 | 118 | const nodeVersion = parseInt(process.versions.node.split(".")[0], 10); 119 | if (nodeVersion >= 12) { 120 | it("can create a view from a serialized bitmap in a SharedArrayBuffer, and the view is frozen", () => { 121 | const values = [1, 2, 3, 100, 8772837, 0x7ffffff1, 0x7fffffff]; 122 | const bitmap = new RoaringBitmap32(values); 123 | const sharedBuffer = RoaringBitmap32.bufferAlignedAllocShared( 124 | bitmap.getSerializationSizeInBytes("unsafe_frozen_croaring"), 125 | ); 126 | expect(sharedBuffer.buffer).to.be.instanceOf(SharedArrayBuffer); 127 | const serialized = bitmap.serialize("unsafe_frozen_croaring", sharedBuffer); 128 | expect(serialized).to.eq(sharedBuffer); 129 | 130 | const view = RoaringBitmap32.unsafeFrozenView(sharedBuffer, "unsafe_frozen_croaring"); 131 | expect(view.isFrozen).to.be.true; 132 | expect(view.toArray()).to.deep.equal(values); 133 | 134 | const copy = view.clone(); 135 | expect(copy.isFrozen).to.be.false; 136 | expect(copy.toArray()).to.deep.equal(values); 137 | expect(copy.tryAdd(4)).to.be.true; 138 | }); 139 | } 140 | }); 141 | 142 | describe("asReadonlyView", () => { 143 | it("offers a readonly view", () => { 144 | const bitmap = new RoaringBitmap32([1, 2, 3]); 145 | const readonlyView = bitmap.asReadonlyView() as RoaringBitmap32; 146 | expect(readonlyView).to.not.eq(bitmap); 147 | expect(readonlyView.isFrozen).to.be.true; 148 | expect(readonlyView.asReadonlyView()).eq(readonlyView); 149 | expect(bitmap.asReadonlyView()).to.equal(readonlyView); 150 | 151 | expect(() => readonlyView.copyFrom([1])).to.throw(ERROR_FROZEN); 152 | expect(() => readonlyView.add(1)).to.throw(ERROR_FROZEN); 153 | expect(() => readonlyView.tryAdd(1)).to.throw(ERROR_FROZEN); 154 | expect(() => readonlyView.addMany([1])).to.throw(ERROR_FROZEN); 155 | expect(() => readonlyView.remove(1)).to.throw(ERROR_FROZEN); 156 | expect(() => readonlyView.removeMany([1])).to.throw(ERROR_FROZEN); 157 | expect(() => readonlyView.delete(1)).to.throw(ERROR_FROZEN); 158 | expect(() => readonlyView.clear()).to.throw(ERROR_FROZEN); 159 | expect(() => readonlyView.orInPlace([1])).to.throw(ERROR_FROZEN); 160 | expect(() => readonlyView.andNotInPlace([1])).to.throw(ERROR_FROZEN); 161 | expect(() => readonlyView.andInPlace([1])).to.throw(ERROR_FROZEN); 162 | expect(() => readonlyView.xorInPlace([1])).to.throw(ERROR_FROZEN); 163 | expect(() => readonlyView.flipRange(0, 10)).to.throw(ERROR_FROZEN); 164 | expect(() => readonlyView.addRange(0, 9)).to.throw(ERROR_FROZEN); 165 | expect(() => readonlyView.removeRange(0, 8)).to.throw(ERROR_FROZEN); 166 | expect(() => readonlyView.removeRunCompression()).to.throw(ERROR_FROZEN); 167 | expect(() => readonlyView.runOptimize()).to.throw(ERROR_FROZEN); 168 | expect(() => readonlyView.shrinkToFit()).to.throw(ERROR_FROZEN); 169 | 170 | bitmap.add(100); 171 | expect(readonlyView.toArray()).to.deep.equal([1, 2, 3, 100]); 172 | expect(readonlyView.size).to.equal(4); 173 | bitmap.add(200); 174 | expect(readonlyView.toArray()).to.deep.equal([1, 2, 3, 100, 200]); 175 | expect(readonlyView.size).to.equal(5); 176 | expect(readonlyView.size).to.equal(5); 177 | 178 | bitmap.freeze(); 179 | expect(readonlyView.isFrozen).to.be.true; 180 | expect(bitmap.asReadonlyView()).eq(readonlyView); 181 | }); 182 | }); 183 | 184 | it("returns this if the bitmap is frozen", () => { 185 | const bitmap = new RoaringBitmap32(); 186 | bitmap.freeze(); 187 | expect(bitmap.asReadonlyView()).to.equal(bitmap); 188 | }); 189 | }); 190 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish 2 | 3 | on: 4 | workflow_dispatch: 5 | push: 6 | branches: 7 | - publish 8 | 9 | jobs: 10 | ensure-release: 11 | name: ensure-release 12 | runs-on: ubuntu-latest 13 | steps: 14 | - name: Check triggering actor is a collaborator 15 | uses: actions/github-script@v7 16 | with: 17 | script: | 18 | const { owner, repo } = context.repo; 19 | const username = context.actor; 20 | try { 21 | await github.rest.repos.checkCollaborator({ owner, repo, username }); 22 | core.info(`User ${username} is a collaborator`); 23 | } catch (err) { 24 | core.setFailed(`User ${username} is not a repository collaborator. Only collaborators can run this workflow.`); 25 | } 26 | 27 | - uses: actions/checkout@v6 28 | - uses: actions/setup-node@v4 29 | with: 30 | node-version: "24.x" 31 | - uses: actions/setup-python@v5 32 | with: 33 | python-version: "3.11" 34 | - name: Configure python for node-gyp 35 | run: node ./scripts/ensure-python-env.js 36 | 37 | - name: Mark git workspace safe 38 | run: | 39 | git config --global --add safe.directory "${GITHUB_WORKSPACE}" 40 | 41 | - name: npm install 42 | run: npm install --exact --no-audit --no-save 43 | env: 44 | ROARING_NODE_PRE_GYP: "custom-rebuild" 45 | 46 | - name: test 47 | run: npm run test 48 | 49 | - name: test memory leaks 50 | run: node --expose-gc ./scripts/test-memory-leaks.js 51 | 52 | - name: Ensure release exists (pre-install) 53 | env: 54 | NODE_PRE_GYP_GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 55 | run: node ./scripts/node-pre-gyp-publish.js --ensure-version 56 | 57 | build-and-upload: 58 | needs: ensure-release 59 | strategy: 60 | matrix: 61 | include: 62 | # 63 | # Linux jobs with exact Node version in container - to match glibc versions 64 | # 65 | - node-version: "20.9.0" 66 | os: ubuntu-latest 67 | container: "node:20.9.0-bullseye" 68 | libc: glibc 69 | - node-version: "22.8.0" 70 | os: ubuntu-latest 71 | container: "node:22.8.0-bullseye" 72 | libc: glibc 73 | - node-version: "24.10.0" 74 | os: ubuntu-latest 75 | container: "node:24.10.0-bullseye" 76 | libc: glibc 77 | - node-version: "25.0.0" 78 | os: ubuntu-latest 79 | container: "node:25.0.0-bookworm" 80 | libc: glibc 81 | # 82 | # Linux jobs targeting musl via Alpine containers 83 | # 84 | - node-version: "20.9.0" 85 | os: ubuntu-latest 86 | container: "node:20.9.0-alpine" 87 | libc: musl 88 | - node-version: "22.8.0" 89 | os: ubuntu-latest 90 | container: "node:22.8.0-alpine" 91 | libc: musl 92 | - node-version: "24.10.0" 93 | os: ubuntu-latest 94 | container: "node:24.10.0-alpine" 95 | libc: musl 96 | - node-version: "25.0.0" 97 | os: ubuntu-latest 98 | container: "node:25.0.0-alpine" 99 | libc: musl 100 | # 101 | # Windows jobs 102 | # 103 | - node-version: "20.9.0" 104 | os: windows-2022 105 | - node-version: "22.8.0" 106 | os: windows-2022 107 | - node-version: "24.10.0" 108 | os: windows-2022 109 | - node-version: "25.0.0" 110 | os: windows-2022 111 | # 112 | # macOS arm64 jobs 113 | # 114 | - node-version: "20.9.0" 115 | os: macos-latest 116 | - node-version: "22.8.0" 117 | os: macos-latest 118 | - node-version: "24.10.0" 119 | os: macos-latest 120 | - node-version: "25.0.0" 121 | os: macos-latest 122 | # 123 | # macOS intel jobs 124 | # 125 | - node-version: "20.9.0" 126 | os: macos-15-intel 127 | - node-version: "22.8.0" 128 | os: macos-15-intel 129 | - node-version: "24.10.0" 130 | os: macos-15-intel 131 | runs-on: ${{ matrix.os }} 132 | container: ${{ matrix.container }} 133 | steps: 134 | - name: Prepare Alpine base image 135 | if: matrix.libc == 'musl' 136 | run: apk add --no-cache python3 py3-setuptools make g++ git bash 137 | - name: Install Debian build deps 138 | if: runner.os == 'Linux' && matrix.libc != 'musl' 139 | run: | 140 | export DEBIAN_FRONTEND=noninteractive 141 | apt-get update 142 | apt-get install -y python3 python3-distutils make g++ git 143 | - uses: actions/checkout@v6 144 | - name: Setup Node (non-Linux) 145 | # Only use setup-node for Windows/macOS 146 | if: runner.os != 'Linux' 147 | uses: actions/setup-node@v4 148 | with: 149 | node-version: ${{ matrix.node-version }} 150 | - name: Setup Python (non-Linux) 151 | if: runner.os != 'Linux' 152 | uses: actions/setup-python@v5 153 | with: 154 | python-version: "3.11" 155 | - name: Configure python for node-gyp (non-Linux) 156 | if: runner.os != 'Linux' 157 | run: node ./scripts/ensure-python-env.js 158 | - name: Configure python for node-gyp (glibc) 159 | if: runner.os == 'Linux' && matrix.libc != 'musl' 160 | run: node ./scripts/ensure-python-env.js python3 161 | - name: Configure python for node-gyp (musl) 162 | if: matrix.libc == 'musl' 163 | run: node ./scripts/ensure-python-env.js /usr/bin/python3 164 | - run: npm install --exact --no-audit --no-save 165 | env: 166 | ROARING_NODE_PRE_GYP: "false" 167 | - run: node ./node-pre-gyp.js 168 | env: 169 | ROARING_NODE_PRE_GYP: "custom-rebuild" 170 | - run: npm run test 171 | - name: Upload artifact 172 | env: 173 | NODE_PRE_GYP_GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 174 | run: | 175 | git config --global --add safe.directory "${GITHUB_WORKSPACE}" 176 | node ./scripts/node-pre-gyp-publish.js --overwrite 177 | 178 | post-test: 179 | needs: [build-and-upload, publish-release] 180 | strategy: 181 | matrix: 182 | os: [ubuntu-22.04, windows-2022, macos-latest] 183 | runs-on: ${{ matrix.os }} 184 | env: 185 | NODE_PRE_GYP_GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 186 | steps: 187 | - uses: actions/setup-node@v4 188 | with: 189 | node-version: "24.x" 190 | - uses: actions/checkout@v6 191 | - run: npm install --exact --no-audit --no-save --build-from-source=false 192 | - run: npm run test 193 | 194 | post-test-node24-slim: 195 | name: post-test-node24-slim 196 | needs: [build-and-upload, publish-release] 197 | runs-on: ubuntu-latest 198 | container: node:24.10.0-bullseye-slim 199 | env: 200 | NODE_PRE_GYP_GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 201 | steps: 202 | - uses: actions/checkout@v6 203 | - run: npm install --exact --no-audit --no-save --build-from-source=false 204 | - run: npm run test 205 | 206 | post-test-node24-alpine: 207 | name: post-test-node24-alpine 208 | needs: [build-and-upload, publish-release] 209 | runs-on: ubuntu-latest 210 | container: node:24.10.0-alpine 211 | env: 212 | NODE_PRE_GYP_GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 213 | steps: 214 | - uses: actions/checkout@v6 215 | - run: npm install --exact --no-audit --no-save --build-from-source=false 216 | - run: npm run test 217 | 218 | publish-release: 219 | needs: build-and-upload 220 | runs-on: ubuntu-latest 221 | steps: 222 | - uses: actions/checkout@v6 223 | - uses: actions/setup-node@v4 224 | with: 225 | node-version: 24.x 226 | - uses: actions/setup-python@v5 227 | with: 228 | python-version: "3.11" 229 | - name: Configure python for node-gyp 230 | run: node ./scripts/ensure-python-env.js 231 | 232 | - name: npm install 233 | run: npm install --exact --no-audit --no-save 234 | env: 235 | ROARING_NODE_PRE_GYP: "false" 236 | 237 | - name: Publish release (convert draft -> published) 238 | env: 239 | NODE_PRE_GYP_GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 240 | run: node ./scripts/node-pre-gyp-publish.js --publish-version 241 | 242 | publish-docs: 243 | needs: 244 | [ 245 | post-test, 246 | post-test-node24-slim, 247 | post-test-node24-alpine, 248 | publish-release, 249 | ] 250 | runs-on: ubuntu-latest 251 | steps: 252 | - uses: actions/checkout@v6 253 | - uses: actions/setup-node@v4 254 | with: 255 | node-version: 24.x 256 | - uses: actions/setup-python@v5 257 | with: 258 | python-version: "3.11" 259 | - name: Configure python for node-gyp 260 | run: node ./scripts/ensure-python-env.js 261 | 262 | - name: npm install 263 | run: npm install --exact --no-audit --no-save 264 | env: 265 | ROARING_NODE_PRE_GYP: "false" 266 | 267 | - name: npm run doc 268 | run: npm run doc 269 | 270 | - name: Deploy documentation 🚀 271 | uses: JamesIves/github-pages-deploy-action@v4 272 | with: 273 | branch: gh-pages 274 | folder: docs 275 | -------------------------------------------------------------------------------- /test/aligned-buffers.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from "vitest"; 2 | import { 3 | asBuffer, 4 | bufferAlignedAlloc, 5 | bufferAlignedAllocShared, 6 | bufferAlignedAllocSharedUnsafe, 7 | bufferAlignedAllocUnsafe, 8 | ensureBufferAligned, 9 | isBufferAligned, 10 | RoaringBitmap32, 11 | } from "./helpers/roaring"; 12 | 13 | describe("asBuffer", () => { 14 | it("returns a buffer", () => { 15 | const input = Buffer.alloc(3); 16 | expect(input).to.eq(input); 17 | }); 18 | 19 | it("wraps an ArrayBuffer", () => { 20 | const input = new ArrayBuffer(3); 21 | const output = asBuffer(input); 22 | expect(output).to.be.instanceOf(Buffer); 23 | expect(output.buffer).to.eq(input); 24 | }); 25 | 26 | it("wraps a SharedArrayBuffer", () => { 27 | const input = new SharedArrayBuffer(3); 28 | const output = asBuffer(input); 29 | expect(output).to.be.instanceOf(Buffer); 30 | expect(output.buffer).to.eq(input); 31 | }); 32 | 33 | it("wraps an arraybuffer view", () => { 34 | for (const ctor of [ 35 | Uint8Array, 36 | Uint16Array, 37 | Uint32Array, 38 | Int8Array, 39 | Int16Array, 40 | Int32Array, 41 | Float32Array, 42 | Float64Array, 43 | Uint8ClampedArray, 44 | ]) { 45 | const input = new ctor(3); 46 | const output = asBuffer(input); 47 | expect(output).to.be.instanceOf(Buffer); 48 | expect(output.buffer).to.eq(input.buffer); 49 | expect(output.byteOffset).to.eq(0); 50 | expect(output.byteLength).to.eq(input.byteLength); 51 | } 52 | }); 53 | 54 | it("wraps an arraybuffer view with offset", () => { 55 | for (const ctor of [ 56 | Uint8Array, 57 | Uint16Array, 58 | Uint32Array, 59 | Int8Array, 60 | Int16Array, 61 | Int32Array, 62 | Float32Array, 63 | Float64Array, 64 | Uint8ClampedArray, 65 | ]) { 66 | const sourceArrayBuffer = new ArrayBuffer(20 * ctor.BYTES_PER_ELEMENT); 67 | const input = new ctor(sourceArrayBuffer, 8, 8); 68 | const output = asBuffer(input); 69 | expect(output).to.be.instanceOf(Buffer); 70 | expect(output.buffer).to.eq(input.buffer); 71 | expect(output.byteOffset).to.eq(input.byteOffset); 72 | expect(output.byteLength).to.eq(input.byteLength); 73 | } 74 | }); 75 | }); 76 | 77 | describe("bufferAlignedAlloc", () => { 78 | it("exposes the bufferAlignedAlloc function", () => { 79 | expect(bufferAlignedAlloc).to.be.a("function"); 80 | expect(RoaringBitmap32.bufferAlignedAlloc).to.eq(bufferAlignedAlloc); 81 | }); 82 | 83 | it("throws if first argument (size) is invalid", () => { 84 | expect(() => bufferAlignedAlloc(-1)).to.throw(); 85 | expect(() => bufferAlignedAlloc("x" as any)).to.throw(); 86 | expect(() => bufferAlignedAlloc({} as any)).to.throw(); 87 | expect(() => bufferAlignedAlloc(null as any)).to.throw(); 88 | }); 89 | 90 | it("can allocate an empty buffer", () => { 91 | const buffer = bufferAlignedAlloc(0); 92 | expect(buffer).to.be.instanceOf(Buffer); 93 | expect(buffer.buffer).to.be.instanceOf(ArrayBuffer); 94 | expect(buffer.length).to.eq(0); 95 | expect(isBufferAligned(buffer)).to.eq(true); 96 | }); 97 | 98 | it("can allocate a buffer of a given size", () => { 99 | const buffer = bufferAlignedAlloc(10, 128); 100 | expect(buffer).to.be.instanceOf(Buffer); 101 | expect(buffer.buffer).to.be.instanceOf(ArrayBuffer); 102 | expect(buffer.length).to.eq(10); 103 | expect(isBufferAligned(buffer, 128)).to.eq(true); 104 | }); 105 | 106 | it("is initialized with zeroes", () => { 107 | const buffer = bufferAlignedAlloc(2340); 108 | expect(buffer).to.be.instanceOf(Buffer); 109 | expect(buffer.buffer).to.be.instanceOf(ArrayBuffer); 110 | expect(buffer.length).to.eq(2340); 111 | for (let i = 0; i < 2340; ++i) { 112 | expect(buffer[i]).to.eq(0); 113 | } 114 | expect(isBufferAligned(buffer)).to.eq(true); 115 | expect(RoaringBitmap32.isBufferAligned(buffer, 32)).to.eq(true); 116 | }); 117 | }); 118 | 119 | describe("bufferAlignedAllocUnsafe", () => { 120 | it("exposes the bufferAlignedAllocUnsafe function", () => { 121 | expect(bufferAlignedAllocUnsafe).to.be.a("function"); 122 | expect(RoaringBitmap32.bufferAlignedAllocUnsafe).to.eq(bufferAlignedAllocUnsafe); 123 | }); 124 | 125 | it("throws if first argument (size) is invalid", () => { 126 | expect(() => bufferAlignedAllocUnsafe(-1)).to.throw(); 127 | expect(() => bufferAlignedAllocUnsafe(null as any)).to.throw(); 128 | expect(() => bufferAlignedAllocUnsafe("x" as any)).to.throw(); 129 | expect(() => bufferAlignedAllocUnsafe({} as any)).to.throw(); 130 | }); 131 | 132 | it("can allocate an empty buffer", () => { 133 | const buffer = bufferAlignedAllocUnsafe(0, 512); 134 | expect(buffer).to.be.instanceOf(Buffer); 135 | expect(buffer.length).to.eq(0); 136 | expect(isBufferAligned(buffer, 512)).to.eq(true); 137 | }); 138 | 139 | it("can allocate a buffer of a given size", () => { 140 | const buffer = bufferAlignedAllocUnsafe(10); 141 | expect(buffer).to.be.instanceOf(Buffer); 142 | expect(buffer.length).to.eq(10); 143 | expect(isBufferAligned(buffer)).to.eq(true); 144 | expect(RoaringBitmap32.isBufferAligned(buffer, 32)).to.eq(true); 145 | expect(RoaringBitmap32.isBufferAligned(buffer.buffer)).to.eq(true); 146 | }); 147 | }); 148 | 149 | describe("ensureBufferAligned", () => { 150 | it("exposes the ensureBufferAligned function", () => { 151 | expect(ensureBufferAligned).to.be.a("function"); 152 | expect(RoaringBitmap32.ensureBufferAligned).to.eq(ensureBufferAligned); 153 | }); 154 | 155 | it("returns the same buffer if it is already aligned", () => { 156 | const buffer = bufferAlignedAlloc(30); 157 | expect(ensureBufferAligned(buffer, 32)).to.eq(buffer); 158 | }); 159 | 160 | it("returns a new buffer if the buffer is not aligned", () => { 161 | const unalignedBuffer = Buffer.from(bufferAlignedAlloc(31).buffer, 1, 27); 162 | const result = ensureBufferAligned(unalignedBuffer); 163 | expect(result).to.not.eq(unalignedBuffer); 164 | expect(result).to.be.instanceOf(Buffer); 165 | expect(result.length).to.eq(27); 166 | expect(isBufferAligned(result)).to.eq(true); 167 | }); 168 | 169 | it("returns a new buffer if the buffer is not aligned, with a custom alignment", () => { 170 | const unalignedBuffer = Buffer.from(bufferAlignedAlloc(31).buffer, 1, 30); 171 | const result = ensureBufferAligned(unalignedBuffer, 256); 172 | expect(result).to.not.eq(unalignedBuffer); 173 | expect(result).to.be.instanceOf(Buffer); 174 | expect(result.length).to.eq(30); 175 | expect(isBufferAligned(result, 256)).to.eq(true); 176 | }); 177 | 178 | it("works with SharedArrayBuffer", () => { 179 | const sab = new SharedArrayBuffer(32); 180 | const unalignedBuffer = Buffer.from(sab, 1, 30); 181 | expect(unalignedBuffer.buffer).to.eq(sab); 182 | const result = ensureBufferAligned(unalignedBuffer); 183 | expect(result).to.not.eq(unalignedBuffer); 184 | expect(result).to.be.instanceOf(Buffer); 185 | expect(result.buffer).to.be.instanceOf(SharedArrayBuffer); 186 | expect(result.length).to.eq(30); 187 | expect(isBufferAligned(result)).to.eq(true); 188 | }); 189 | }); 190 | 191 | const nodeVersion = parseInt(process.versions.node.split(".")[0], 10); 192 | if (nodeVersion >= 12) { 193 | describe("bufferAlignedAllocShared", () => { 194 | it("exposes the bufferAlignedAllocShared function", () => { 195 | expect(bufferAlignedAllocShared).to.be.a("function"); 196 | expect(RoaringBitmap32.bufferAlignedAllocShared).to.eq(bufferAlignedAllocShared); 197 | }); 198 | 199 | it("throws if first argument (size) is invalid", () => { 200 | expect(() => bufferAlignedAllocShared(-1)).to.throw(); 201 | expect(() => bufferAlignedAllocShared(null as any)).to.throw(); 202 | expect(() => bufferAlignedAllocShared("x" as any)).to.throw(); 203 | expect(() => bufferAlignedAllocShared({} as any)).to.throw(); 204 | }); 205 | 206 | it("can allocate an empty buffer", () => { 207 | const buffer = bufferAlignedAllocShared(0, 512); 208 | expect(buffer).to.be.instanceOf(Buffer); 209 | expect(buffer.buffer).to.be.instanceOf(SharedArrayBuffer); 210 | expect(buffer.length).to.eq(0); 211 | expect(isBufferAligned(buffer, 512)).to.eq(true); 212 | }); 213 | 214 | it("can allocate a buffer of a given size", () => { 215 | const buffer = bufferAlignedAllocShared(10); 216 | expect(buffer).to.be.instanceOf(Buffer); 217 | expect(buffer.buffer).to.be.instanceOf(SharedArrayBuffer); 218 | expect(buffer.length).to.eq(10); 219 | expect(isBufferAligned(buffer)).to.eq(true); 220 | expect(RoaringBitmap32.isBufferAligned(buffer, 32)).to.eq(true); 221 | expect(RoaringBitmap32.isBufferAligned(buffer.buffer)).to.eq(true); 222 | }); 223 | }); 224 | 225 | describe("bufferAlignedAllocSharedUnsafe", () => { 226 | it("exposes the bufferAlignedAllocSharedUnsafe function", () => { 227 | expect(bufferAlignedAllocSharedUnsafe).to.be.a("function"); 228 | expect(RoaringBitmap32.bufferAlignedAllocSharedUnsafe).to.eq(bufferAlignedAllocSharedUnsafe); 229 | }); 230 | 231 | it("throws if first argument (size) is invalid", () => { 232 | expect(() => bufferAlignedAllocSharedUnsafe(-1)).to.throw(); 233 | expect(() => bufferAlignedAllocSharedUnsafe(null as any)).to.throw(); 234 | expect(() => bufferAlignedAllocSharedUnsafe("x" as any)).to.throw(); 235 | expect(() => bufferAlignedAllocSharedUnsafe({} as any)).to.throw(); 236 | }); 237 | 238 | it("can allocate an empty buffer", () => { 239 | const buffer = bufferAlignedAllocSharedUnsafe(0, 512); 240 | expect(buffer).to.be.instanceOf(Buffer); 241 | expect(buffer.buffer).to.be.instanceOf(SharedArrayBuffer); 242 | expect(buffer.length).to.eq(0); 243 | expect(isBufferAligned(buffer, 512)).to.eq(true); 244 | }); 245 | 246 | it("can allocate a buffer of a given size", () => { 247 | const buffer = bufferAlignedAllocSharedUnsafe(10); 248 | expect(buffer).to.be.instanceOf(Buffer); 249 | expect(buffer.buffer).to.be.instanceOf(SharedArrayBuffer); 250 | expect(buffer.length).to.eq(10); 251 | expect(isBufferAligned(buffer)).to.eq(true); 252 | expect(RoaringBitmap32.isBufferAligned(buffer, 32)).to.eq(true); 253 | expect(RoaringBitmap32.isBufferAligned(buffer.buffer)).to.eq(true); 254 | }); 255 | }); 256 | } 257 | -------------------------------------------------------------------------------- /scripts/node-pre-gyp-publish.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | /* 4 | Based on https://github.com/bchr02/node-pre-gyp-github 5 | 6 | The MIT License (MIT) 7 | 8 | Copyright (c) 2015 Bill Christo 9 | 10 | Permission is hereby granted, free of charge, to any person obtaining a copy 11 | of this software and associated documentation files (the "Software"), to deal 12 | in the Software without restriction, including without limitation the rights 13 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 14 | copies of the Software, and to permit persons to whom the Software is 15 | furnished to do so, subject to the following conditions: 16 | 17 | The above copyright notice and this permission notice shall be included in all 18 | copies or substantial portions of the Software. 19 | 20 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 21 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 22 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 23 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 24 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 25 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 26 | SOFTWARE. 27 | 28 | */ 29 | 30 | const path = require("node:path"); 31 | const fs = require("node:fs"); 32 | const colors = require("ansis"); 33 | 34 | const packageJson = require("../package.json"); 35 | const { runMain } = require("./lib/utils"); 36 | const crypto = require("node:crypto"); 37 | const { execSync } = require("node:child_process"); 38 | 39 | module.exports = { 40 | startPublishAssets, 41 | }; 42 | 43 | const ALLOWED_BRANCHES = ["publish"]; 44 | 45 | let octokitCtorPromise = null; 46 | 47 | /** 48 | * Lazily import the ESM-only @octokit/rest package so this CommonJS script keeps working. 49 | * @returns {Promise} 50 | */ 51 | async function getOctokitConstructor() { 52 | if (!octokitCtorPromise) { 53 | octokitCtorPromise = import("@octokit/rest").then((mod) => { 54 | if (!mod || !mod.Octokit) { 55 | throw new Error("Failed to load Octokit from @octokit/rest"); 56 | } 57 | return mod.Octokit; 58 | }); 59 | } 60 | return octokitCtorPromise; 61 | } 62 | 63 | async function startPublishAssets() { 64 | const assetsByName = new Map(); 65 | /** @type {import("@octokit/rest").Octokit} */ 66 | let octokit; 67 | let owner; 68 | let repo; 69 | let release; 70 | 71 | let initializePromise = null; 72 | let NODE_PRE_GYP_GITHUB_TOKEN; 73 | 74 | function getGithubKey() { 75 | if (NODE_PRE_GYP_GITHUB_TOKEN) { 76 | return NODE_PRE_GYP_GITHUB_TOKEN; 77 | } 78 | 79 | NODE_PRE_GYP_GITHUB_TOKEN = process.env.NODE_PRE_GYP_GITHUB_TOKEN; 80 | if (!NODE_PRE_GYP_GITHUB_TOKEN) { 81 | try { 82 | NODE_PRE_GYP_GITHUB_TOKEN = fs 83 | .readFileSync(path.resolve(__dirname, "../.github-key"), "utf8") 84 | .replace("\n", "") 85 | .replace("\r", "") 86 | .trim(); 87 | } catch (_e) {} 88 | } 89 | 90 | if (!NODE_PRE_GYP_GITHUB_TOKEN) { 91 | throw new Error("NODE_PRE_GYP_GITHUB_TOKEN environment variable not set and .github-key file not found"); 92 | } 93 | 94 | return NODE_PRE_GYP_GITHUB_TOKEN; 95 | } 96 | 97 | function checkGithubKey() { 98 | getGithubKey(); 99 | } 100 | 101 | async function initialize() { 102 | const branchName = execSync("git rev-parse --abbrev-ref HEAD") 103 | .toString() 104 | .replace(/[\n\r]/g, "") 105 | .trim(); 106 | 107 | console.log(colors.cyanBright(`branchName: ${branchName}`)); 108 | 109 | if (!ALLOWED_BRANCHES.includes(branchName)) { 110 | console.warn( 111 | colors.yellowBright( 112 | `⚠️ ${colors.underline("WARNING")}: Skipping deploy. Deploy allowed only on branch ${ALLOWED_BRANCHES.join( 113 | ", ", 114 | )}`, 115 | ), 116 | ); 117 | return false; 118 | } 119 | 120 | const ownerRepo = packageJson.repository.url.match(/https?:\/\/([^/]+)\/(.*)(?=\.git)/i); 121 | const host = `api.${ownerRepo[1]}`; 122 | [owner, repo] = ownerRepo[2].split("/"); 123 | 124 | const Octokit = await getOctokitConstructor(); 125 | 126 | octokit = new Octokit({ 127 | baseUrl: `https://${host}`, 128 | auth: getGithubKey(), 129 | headers: { "user-agent": packageJson.name }, 130 | request: { 131 | fetch, 132 | }, 133 | }); 134 | 135 | const { data: existingReleases } = await octokit.rest.repos.listReleases({ owner, repo }); 136 | 137 | release = existingReleases.find((element) => element.tag_name === packageJson.version); 138 | 139 | if (!release) { 140 | release = ( 141 | await octokit.rest.repos.createRelease({ 142 | host, 143 | owner, 144 | repo, 145 | tag_name: packageJson.version, 146 | target_commitish: branchName, 147 | name: `v${packageJson.version}`, 148 | body: `${packageJson.name} ${packageJson.version}`, 149 | draft: true, 150 | prerelease: /[a-zA-Z]/.test(packageJson.version), 151 | }) 152 | ).data; 153 | console.log(colors.yellow(`Creating new release ${packageJson.version}`)); 154 | } else { 155 | console.log(colors.yellow(`Using existing release ${packageJson.version}`)); 156 | } 157 | 158 | if (release.draft) { 159 | console.log(); 160 | console.warn( 161 | colors.yellowBright( 162 | `⚠️ ${colors.underline("WARNING")}: Release ${packageJson.version} is a draft release not yet published.`, 163 | ), 164 | ); 165 | console.log(); 166 | } 167 | 168 | for (const asset of release?.assets || []) { 169 | assetsByName.set(asset.name, asset); 170 | } 171 | 172 | return true; 173 | } 174 | 175 | const upload = async (filePath = path.resolve(__dirname, "../build/stage")) => { 176 | if (!initializePromise) { 177 | initializePromise = initialize(); 178 | } 179 | if (!(await initializePromise)) { 180 | return; 181 | } 182 | 183 | filePath = path.resolve(filePath); 184 | 185 | const stat = await fs.promises.lstat(filePath); 186 | if (stat.isDirectory()) { 187 | const files = await fs.promises.readdir(filePath); 188 | await Promise.all(files.map((file) => upload(path.resolve(filePath, file)))); 189 | return; 190 | } 191 | 192 | const name = path.basename(filePath); 193 | 194 | const data = await fs.promises.readFile(filePath); 195 | const uploadFileHash = crypto.createHash("sha256").update(data).digest("hex"); 196 | 197 | console.log(colors.cyanBright(`* file ${name} hash: ${uploadFileHash}`)); 198 | 199 | const foundAsset = assetsByName.get(name); 200 | if (foundAsset) { 201 | const result = await octokit.request({ 202 | method: "GET", 203 | url: foundAsset.url, 204 | headers: { 205 | accept: "application/octet-stream", 206 | }, 207 | }); 208 | 209 | const downloadedFileHash = crypto.createHash("sha256").update(Buffer.from(result.data)).digest("hex"); 210 | 211 | if (downloadedFileHash === uploadFileHash) { 212 | console.log(colors.yellow(`Skipping upload of ${name} because it already exists and is the same`)); 213 | return; 214 | } 215 | 216 | if (!release.draft) { 217 | const canOverwrite = process.argv.includes("--overwrite"); 218 | if (canOverwrite) { 219 | console.log(); 220 | console.warn( 221 | colors.yellowBright(`⚠️ WARNING: Overwriting uploaded asset ${name} in release ${packageJson.version}`), 222 | ); 223 | console.log(); 224 | } else if (process.argv.includes("--no-overwrite")) { 225 | console.log(); 226 | console.warn( 227 | colors.yellowBright(`⚠️ WARNING: Skipping uploaded asset ${name} in release ${packageJson.version}`), 228 | ); 229 | console.log(); 230 | return; 231 | } 232 | if (!canOverwrite) { 233 | throw new Error( 234 | `${packageJson.version} in release ${packageJson.version} was already published, aborting. Run with the argument "--overwrite" or "--no-overwrite".`, 235 | ); 236 | } 237 | } 238 | 239 | console.log(colors.yellow(`Deleting asset ${foundAsset.name} id:${foundAsset.id} to replace it`)); 240 | await octokit.rest.repos.deleteReleaseAsset({ 241 | owner, 242 | repo, 243 | asset_id: foundAsset.id, 244 | }); 245 | } 246 | 247 | console.log(colors.yellow(`Uploading asset ${name} ${stat.size} bytes`)); 248 | await octokit.rest.repos.uploadReleaseAsset({ 249 | owner, 250 | repo, 251 | release_id: release.id, 252 | tag_name: release.tag_name, 253 | name, 254 | data, 255 | }); 256 | }; 257 | 258 | // Ensure the release exists (initialize) and return the boolean result. 259 | const ensureRelease = async () => { 260 | if (!initializePromise) { 261 | initializePromise = initialize(); 262 | } 263 | return initializePromise; 264 | }; 265 | 266 | const publishVersion = async () => { 267 | if (!initializePromise) { 268 | initializePromise = initialize(); 269 | } 270 | if (!(await initializePromise)) { 271 | return; 272 | } 273 | 274 | if (!release) { 275 | throw new Error("Release not found after initialize"); 276 | } 277 | 278 | if (!release.draft) { 279 | console.log(colors.yellow(`Release ${release.tag_name} is already published`)); 280 | return; 281 | } 282 | 283 | console.log(colors.cyanBright(`Publishing release ${release.tag_name} ...`)); 284 | const updated = await octokit.rest.repos.updateRelease({ 285 | owner, 286 | repo, 287 | release_id: release.id, 288 | draft: false, 289 | }); 290 | 291 | release = updated.data; 292 | console.log(colors.green(`Release ${release.tag_name} published`)); 293 | }; 294 | 295 | return { 296 | checkGithubKey, 297 | upload, 298 | ensureRelease, 299 | publishVersion, 300 | }; 301 | } 302 | 303 | if (require.main === module) { 304 | runMain(async () => { 305 | const publisher = await startPublishAssets(); 306 | 307 | const ensureVersion = process.argv.includes("--ensure-version") || process.argv.includes("ensure-version"); 308 | const publishVersion = process.argv.includes("--publish-version") || process.argv.includes("publish-version"); 309 | 310 | if (ensureVersion) { 311 | await publisher.ensureRelease(); 312 | } 313 | 314 | if (!ensureVersion && !publishVersion) { 315 | await publisher.upload(); 316 | } 317 | 318 | if (publishVersion) { 319 | await publisher.publishVersion(); 320 | } 321 | }, "node-pre-gyp-publish"); 322 | } 323 | -------------------------------------------------------------------------------- /test/RoaringBitmap32Iterator/RoaringBitmap32Iterator.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from "vitest"; 2 | import RoaringBitmap32 from "../../RoaringBitmap32"; 3 | import RoaringBitmap32Iterator from "../../RoaringBitmap32Iterator"; 4 | 5 | describe("RoaringBitmap32Iterator", () => { 6 | describe("constructor", () => { 7 | it("is a class", () => { 8 | expect(typeof RoaringBitmap32).eq("function"); 9 | }); 10 | 11 | it("creates an empty iterator with no arguments", () => { 12 | const iter = new RoaringBitmap32Iterator(); 13 | expect(iter).to.be.instanceOf(RoaringBitmap32Iterator); 14 | }); 15 | 16 | it("creates an iterator with a RoaringBitmap32", () => { 17 | const bitmap = new RoaringBitmap32([3, 4, 5]); 18 | const iter = new RoaringBitmap32Iterator(bitmap); 19 | expect(iter).to.be.instanceOf(RoaringBitmap32Iterator); 20 | }); 21 | 22 | it("throws an exception if called with a non RoaringBitmap32", () => { 23 | expect(() => new RoaringBitmap32Iterator(123 as any)).to.throw(Error); 24 | expect(() => new RoaringBitmap32Iterator([123] as any)).to.throw(Error); 25 | }); 26 | }); 27 | 28 | describe("next", () => { 29 | it("is a function", () => { 30 | const iter = new RoaringBitmap32Iterator(); 31 | expect(typeof iter.next).eq("function"); 32 | }); 33 | 34 | it("returns an empty result if iterator is created without arguments", () => { 35 | const iter = new RoaringBitmap32Iterator(); 36 | expect(iter.next()).deep.equal({ value: undefined, done: true }); 37 | expect(iter.next()).deep.equal({ value: undefined, done: true }); 38 | }); 39 | 40 | it("returns an empty result if iterator is created with an empty RoaringBitmap32", () => { 41 | const iter = new RoaringBitmap32Iterator(new RoaringBitmap32()); 42 | expect(iter.next()).deep.equal({ value: undefined, done: true }); 43 | expect(iter.next()).deep.equal({ value: undefined, done: true }); 44 | }); 45 | 46 | it("allows iterating a small array", () => { 47 | const iter = new RoaringBitmap32Iterator(new RoaringBitmap32([123, 456, 999, 1000])); 48 | expect(iter.next()).deep.equal({ value: 123, done: false }); 49 | expect(iter.next()).deep.equal({ value: 456, done: false }); 50 | expect(iter.next()).deep.equal({ value: 999, done: false }); 51 | expect(iter.next()).deep.equal({ value: 1000, done: false }); 52 | expect(iter.next()).deep.equal({ value: undefined, done: true }); 53 | expect(iter.next()).deep.equal({ value: undefined, done: true }); 54 | expect(iter.next()).deep.equal({ value: undefined, done: true }); 55 | }); 56 | }); 57 | 58 | describe("Symbol.iterator", () => { 59 | it("is a function", () => { 60 | const iter = new RoaringBitmap32Iterator(); 61 | expect(typeof iter[Symbol.iterator]).deep.equal("function"); 62 | }); 63 | 64 | it("returns this", () => { 65 | const iter = new RoaringBitmap32Iterator(); 66 | expect(iter[Symbol.iterator]()).eq(iter); 67 | }); 68 | 69 | it("allows foreach (empty)", () => { 70 | const iter = new RoaringBitmap32Iterator(); 71 | expect(iter.next()).deep.equal({ done: true, value: undefined }); 72 | }); 73 | 74 | it("allows foreach (small array)", () => { 75 | const iter = new RoaringBitmap32Iterator(new RoaringBitmap32([123, 456, 789])); 76 | const values = []; 77 | for (const x of iter) { 78 | values.push(x); 79 | } 80 | expect(values).deep.equal([123, 456, 789]); 81 | }); 82 | 83 | it("allows Array.from", () => { 84 | const iter = new RoaringBitmap32Iterator(new RoaringBitmap32([123, 456, 789])); 85 | const values = Array.from(iter); 86 | expect(values).deep.equal([123, 456, 789]); 87 | }); 88 | }); 89 | 90 | describe("buffer (number)", () => { 91 | it("iterates, buffer 1, bitmap 0", () => { 92 | const bitmap = new RoaringBitmap32(); 93 | const iterator = new RoaringBitmap32Iterator(bitmap, 1); 94 | expect(iterator.next()).deep.equal({ value: undefined, done: true }); 95 | expect(iterator.next()).deep.equal({ value: undefined, done: true }); 96 | }); 97 | 98 | it("iterates, buffer 2, bitmap 0", () => { 99 | const bitmap = new RoaringBitmap32(); 100 | const iterator = new RoaringBitmap32Iterator(bitmap, 2); 101 | expect(iterator.next()).deep.equal({ value: undefined, done: true }); 102 | expect(iterator.next()).deep.equal({ value: undefined, done: true }); 103 | }); 104 | 105 | it("iterates, buffer 1, bitmap 1", () => { 106 | const bitmap = new RoaringBitmap32([5]); 107 | const iterator = new RoaringBitmap32Iterator(bitmap, 1); 108 | expect(iterator.next()).deep.equal({ value: 5, done: false }); 109 | expect(iterator.next()).deep.equal({ value: undefined, done: true }); 110 | expect(iterator.next()).deep.equal({ value: undefined, done: true }); 111 | }); 112 | 113 | it("iterates, buffer 2, bitmap 1", () => { 114 | const bitmap = new RoaringBitmap32([5]); 115 | const iterator = new RoaringBitmap32Iterator(bitmap, 2); 116 | expect(iterator.next()).deep.equal({ value: 5, done: false }); 117 | expect(iterator.next()).deep.equal({ value: undefined, done: true }); 118 | expect(iterator.next()).deep.equal({ value: undefined, done: true }); 119 | }); 120 | 121 | it("iterates, buffer 1, bitmap 3", () => { 122 | const bitmap = new RoaringBitmap32([5, 7, 9]); 123 | const iterator = new RoaringBitmap32Iterator(bitmap, 1); 124 | expect(iterator.next()).deep.equal({ value: 5, done: false }); 125 | expect(iterator.next()).deep.equal({ value: 7, done: false }); 126 | expect(iterator.next()).deep.equal({ value: 9, done: false }); 127 | }); 128 | 129 | it("iterates, buffer 2, bitmap 3", () => { 130 | const bitmap = new RoaringBitmap32([5, 7, 9]); 131 | const iterator = new RoaringBitmap32Iterator(bitmap, 2); 132 | expect(iterator.next()).deep.equal({ value: 5, done: false }); 133 | expect(iterator.next()).deep.equal({ value: 7, done: false }); 134 | expect(iterator.next()).deep.equal({ value: 9, done: false }); 135 | }); 136 | }); 137 | 138 | describe("buffer (Uint32Array)", () => { 139 | it("iterates, buffer 1, bitmap 0", () => { 140 | const bitmap = new RoaringBitmap32(); 141 | const buffer = new Uint32Array(1); 142 | const iterator = new RoaringBitmap32Iterator(bitmap, buffer); 143 | expect(iterator.next()).deep.equal({ value: undefined, done: true }); 144 | expect(iterator.next()).deep.equal({ value: undefined, done: true }); 145 | expect(Array.from(buffer)).deep.equal([0]); 146 | }); 147 | 148 | it("iterates, buffer 2, bitmap 0", () => { 149 | const bitmap = new RoaringBitmap32(); 150 | const buffer = new Uint32Array(2); 151 | const iterator = new RoaringBitmap32Iterator(bitmap, buffer); 152 | expect(iterator.next()).deep.equal({ value: undefined, done: true }); 153 | expect(iterator.next()).deep.equal({ value: undefined, done: true }); 154 | expect(Array.from(buffer)).deep.equal([0, 0]); 155 | }); 156 | 157 | it("iterates, buffer 1, bitmap 1", () => { 158 | const bitmap = new RoaringBitmap32([5]); 159 | const buffer = new Uint32Array(1); 160 | const iterator = new RoaringBitmap32Iterator(bitmap, buffer); 161 | expect(buffer[0]).eq(0); 162 | expect(iterator.next()).deep.equal({ value: 5, done: false }); 163 | expect(buffer[0]).eq(5); 164 | expect(iterator.next()).deep.equal({ value: undefined, done: true }); 165 | expect(buffer[0]).eq(5); 166 | expect(iterator.next()).deep.equal({ value: undefined, done: true }); 167 | expect(buffer[0]).eq(5); 168 | }); 169 | 170 | it("iterates, buffer 2, bitmap 1", () => { 171 | const bitmap = new RoaringBitmap32([5]); 172 | const buffer = new Uint32Array(2); 173 | const iterator = new RoaringBitmap32Iterator(bitmap, buffer); 174 | expect(Array.from(buffer)).deep.equal([0, 0]); 175 | expect(iterator.next()).deep.equal({ value: 5, done: false }); 176 | expect(Array.from(buffer)).deep.equal([5, 0]); 177 | expect(iterator.next()).deep.equal({ value: undefined, done: true }); 178 | expect(Array.from(buffer)).deep.equal([5, 0]); 179 | expect(iterator.next()).deep.equal({ value: undefined, done: true }); 180 | expect(Array.from(buffer)).deep.equal([5, 0]); 181 | }); 182 | 183 | it("iterates, buffer 1, bitmap 3", () => { 184 | const bitmap = new RoaringBitmap32([5, 7, 9]); 185 | const buffer = new Uint32Array(1); 186 | const iterator = new RoaringBitmap32Iterator(bitmap, buffer); 187 | expect(buffer[0]).eq(0); 188 | expect(iterator.next()).deep.equal({ value: 5, done: false }); 189 | expect(buffer[0]).eq(5); 190 | expect(iterator.next()).deep.equal({ value: 7, done: false }); 191 | expect(buffer[0]).eq(7); 192 | expect(iterator.next()).deep.equal({ value: 9, done: false }); 193 | expect(buffer[0]).eq(9); 194 | }); 195 | 196 | it("iterates, buffer 2, bitmap 3", () => { 197 | const bitmap = new RoaringBitmap32([5, 7, 9]); 198 | const buffer = new Uint32Array(2); 199 | const iterator = new RoaringBitmap32Iterator(bitmap, buffer); 200 | expect(Array.from(buffer)).deep.equal([0, 0]); 201 | expect(iterator.next()).deep.equal({ value: 5, done: false }); 202 | expect(Array.from(buffer)).deep.equal([5, 7]); 203 | expect(iterator.next()).deep.equal({ value: 7, done: false }); 204 | expect(Array.from(buffer)).deep.equal([5, 7]); 205 | expect(iterator.next()).deep.equal({ value: 9, done: false }); 206 | expect(buffer[0]).eq(9); 207 | }); 208 | }); 209 | 210 | it("throws if the bitmap is changed while iterating", () => { 211 | const bitmap = new RoaringBitmap32(); 212 | bitmap.addRange(0, 1050); 213 | 214 | function doNothing(_v: any) {} 215 | 216 | let error: any; 217 | try { 218 | let n = 0; 219 | for (const v of new RoaringBitmap32Iterator(bitmap, 256)) { 220 | if (n++ === 0) { 221 | bitmap.add(999999); 222 | } else { 223 | doNothing(v); 224 | } 225 | } 226 | } catch (e) { 227 | error = e; 228 | } 229 | expect(error.message).eq("RoaringBitmap32 iterator - bitmap changed while iterating"); 230 | }); 231 | 232 | describe("RoaringBitmap32 iterable", () => { 233 | it("returns a RoaringBitmap32Iterator", () => { 234 | const bitmap = new RoaringBitmap32(); 235 | const iterator = bitmap[Symbol.iterator](); 236 | expect(iterator).to.be.instanceOf(RoaringBitmap32Iterator); 237 | expect(typeof iterator.next).eq("function"); 238 | }); 239 | 240 | it("has both [Symbol.iterator] and iterator", () => { 241 | const bitmap = new RoaringBitmap32(); 242 | expect(bitmap.iterator).eq(bitmap[Symbol.iterator]); 243 | }); 244 | 245 | it("returns an empty iterator for an empty bitmap", () => { 246 | const bitmap = new RoaringBitmap32(); 247 | const iterator = bitmap[Symbol.iterator](); 248 | expect(iterator.next()).deep.equal({ 249 | done: true, 250 | value: undefined, 251 | }); 252 | expect(iterator.next()).deep.equal({ 253 | done: true, 254 | value: undefined, 255 | }); 256 | }); 257 | it("iterates a non empty bitmap", () => { 258 | const bitmap = new RoaringBitmap32([0xffffffff, 3]); 259 | const iterator = bitmap[Symbol.iterator](); 260 | expect(iterator.next()).deep.equal({ 261 | done: false, 262 | value: 3, 263 | }); 264 | expect(iterator.next()).deep.equal({ 265 | done: false, 266 | value: 0xffffffff, 267 | }); 268 | expect(iterator.next()).deep.equal({ 269 | done: true, 270 | value: undefined, 271 | }); 272 | }); 273 | }); 274 | }); 275 | -------------------------------------------------------------------------------- /test/RoaringBitmap32ReverseIterator/RoaringBitmap32ReverseIterator.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from "vitest"; 2 | import RoaringBitmap32 from "../../RoaringBitmap32"; 3 | import RoaringBitmap32ReverseIterator from "../../RoaringBitmap32ReverseIterator"; 4 | 5 | describe("RoaringBitmap32ReverseIterator", () => { 6 | describe("constructor", () => { 7 | it("is a class", () => { 8 | expect(typeof RoaringBitmap32).eq("function"); 9 | }); 10 | 11 | it("creates an empty iterator with no arguments", () => { 12 | const iter = new RoaringBitmap32ReverseIterator(); 13 | expect(iter).to.be.instanceOf(RoaringBitmap32ReverseIterator); 14 | }); 15 | 16 | it("creates an iterator with a RoaringBitmap32", () => { 17 | const bitmap = new RoaringBitmap32([3, 4, 5]); 18 | const iter = new RoaringBitmap32ReverseIterator(bitmap); 19 | expect(iter).to.be.instanceOf(RoaringBitmap32ReverseIterator); 20 | }); 21 | 22 | it("throws an exception if called with a non RoaringBitmap32", () => { 23 | expect(() => new RoaringBitmap32ReverseIterator(123 as any)).to.throw(Error); 24 | expect(() => new RoaringBitmap32ReverseIterator([123] as any)).to.throw(Error); 25 | }); 26 | }); 27 | 28 | describe("next", () => { 29 | it("is a function", () => { 30 | const iter = new RoaringBitmap32ReverseIterator(); 31 | expect(typeof iter.next).eq("function"); 32 | }); 33 | 34 | it("returns an empty result if iterator is created without arguments", () => { 35 | const iter = new RoaringBitmap32ReverseIterator(); 36 | expect(iter.next()).deep.equal({ value: undefined, done: true }); 37 | expect(iter.next()).deep.equal({ value: undefined, done: true }); 38 | }); 39 | 40 | it("returns an empty result if iterator is created with an empty RoaringBitmap32", () => { 41 | const iter = new RoaringBitmap32ReverseIterator(new RoaringBitmap32()); 42 | expect(iter.next()).deep.equal({ value: undefined, done: true }); 43 | expect(iter.next()).deep.equal({ value: undefined, done: true }); 44 | }); 45 | 46 | it("allows iterating a small array", () => { 47 | const iter = new RoaringBitmap32ReverseIterator(new RoaringBitmap32([123, 456, 999, 1000])); 48 | expect(iter.next()).deep.equal({ value: 1000, done: false }); 49 | expect(iter.next()).deep.equal({ value: 999, done: false }); 50 | expect(iter.next()).deep.equal({ value: 456, done: false }); 51 | expect(iter.next()).deep.equal({ value: 123, done: false }); 52 | expect(iter.next()).deep.equal({ value: undefined, done: true }); 53 | expect(iter.next()).deep.equal({ value: undefined, done: true }); 54 | expect(iter.next()).deep.equal({ value: undefined, done: true }); 55 | }); 56 | }); 57 | 58 | describe("Symbol.iterator", () => { 59 | it("is a function", () => { 60 | const iter = new RoaringBitmap32ReverseIterator(); 61 | expect(typeof iter[Symbol.iterator]).deep.equal("function"); 62 | }); 63 | 64 | it("returns this", () => { 65 | const iter = new RoaringBitmap32ReverseIterator(); 66 | expect(iter[Symbol.iterator]()).eq(iter); 67 | }); 68 | 69 | it("allows foreach (empty)", () => { 70 | const iter = new RoaringBitmap32ReverseIterator(); 71 | expect(iter.next()).deep.equal({ done: true, value: undefined }); 72 | }); 73 | 74 | it("allows foreach (small array)", () => { 75 | const iter = new RoaringBitmap32ReverseIterator(new RoaringBitmap32([123, 456, 789])); 76 | const values = []; 77 | for (const x of iter) { 78 | values.push(x); 79 | } 80 | expect(values.reverse()).deep.equal([123, 456, 789]); 81 | }); 82 | 83 | it("allows Array.from", () => { 84 | const iter = new RoaringBitmap32ReverseIterator(new RoaringBitmap32([123, 456, 789])); 85 | const values = Array.from(iter); 86 | expect(values.reverse()).deep.equal([123, 456, 789]); 87 | }); 88 | }); 89 | 90 | describe("buffer (number)", () => { 91 | it("iterates, buffer 1, bitmap 0", () => { 92 | const bitmap = new RoaringBitmap32(); 93 | const iterator = new RoaringBitmap32ReverseIterator(bitmap, 1); 94 | expect(iterator.next()).deep.equal({ value: undefined, done: true }); 95 | expect(iterator.next()).deep.equal({ value: undefined, done: true }); 96 | }); 97 | 98 | it("iterates, buffer 2, bitmap 0", () => { 99 | const bitmap = new RoaringBitmap32(); 100 | const iterator = new RoaringBitmap32ReverseIterator(bitmap, 2); 101 | expect(iterator.next()).deep.equal({ value: undefined, done: true }); 102 | expect(iterator.next()).deep.equal({ value: undefined, done: true }); 103 | }); 104 | 105 | it("iterates, buffer 1, bitmap 1", () => { 106 | const bitmap = new RoaringBitmap32([5]); 107 | const iterator = new RoaringBitmap32ReverseIterator(bitmap, 1); 108 | expect(iterator.next()).deep.equal({ value: 5, done: false }); 109 | expect(iterator.next()).deep.equal({ value: undefined, done: true }); 110 | expect(iterator.next()).deep.equal({ value: undefined, done: true }); 111 | }); 112 | 113 | it("iterates, buffer 2, bitmap 1", () => { 114 | const bitmap = new RoaringBitmap32([5]); 115 | const iterator = new RoaringBitmap32ReverseIterator(bitmap, 2); 116 | expect(iterator.next()).deep.equal({ value: 5, done: false }); 117 | expect(iterator.next()).deep.equal({ value: undefined, done: true }); 118 | expect(iterator.next()).deep.equal({ value: undefined, done: true }); 119 | }); 120 | 121 | it("iterates, buffer 1, bitmap 3", () => { 122 | const bitmap = new RoaringBitmap32([5, 7, 9]); 123 | const iterator = new RoaringBitmap32ReverseIterator(bitmap, 1); 124 | expect(iterator.next()).deep.equal({ value: 9, done: false }); 125 | expect(iterator.next()).deep.equal({ value: 7, done: false }); 126 | expect(iterator.next()).deep.equal({ value: 5, done: false }); 127 | }); 128 | 129 | it("iterates, buffer 2, bitmap 3", () => { 130 | const bitmap = new RoaringBitmap32([5, 7, 9]); 131 | const iterator = new RoaringBitmap32ReverseIterator(bitmap, 2); 132 | expect(iterator.next()).deep.equal({ value: 9, done: false }); 133 | expect(iterator.next()).deep.equal({ value: 7, done: false }); 134 | expect(iterator.next()).deep.equal({ value: 5, done: false }); 135 | }); 136 | }); 137 | 138 | describe("buffer (Uint32Array)", () => { 139 | it("iterates, buffer 1, bitmap 0", () => { 140 | const bitmap = new RoaringBitmap32(); 141 | const buffer = new Uint32Array(1); 142 | const iterator = new RoaringBitmap32ReverseIterator(bitmap, buffer); 143 | expect(iterator.next()).deep.equal({ value: undefined, done: true }); 144 | expect(iterator.next()).deep.equal({ value: undefined, done: true }); 145 | expect(Array.from(buffer)).deep.equal([0]); 146 | }); 147 | 148 | it("iterates, buffer 2, bitmap 0", () => { 149 | const bitmap = new RoaringBitmap32(); 150 | const buffer = new Uint32Array(2); 151 | const iterator = new RoaringBitmap32ReverseIterator(bitmap, buffer); 152 | expect(iterator.next()).deep.equal({ value: undefined, done: true }); 153 | expect(iterator.next()).deep.equal({ value: undefined, done: true }); 154 | expect(Array.from(buffer)).deep.equal([0, 0]); 155 | }); 156 | 157 | it("iterates, buffer 1, bitmap 1", () => { 158 | const bitmap = new RoaringBitmap32([5]); 159 | const buffer = new Uint32Array(1); 160 | const iterator = new RoaringBitmap32ReverseIterator(bitmap, buffer); 161 | expect(buffer[0]).eq(0); 162 | expect(iterator.next()).deep.equal({ value: 5, done: false }); 163 | expect(buffer[0]).eq(5); 164 | expect(iterator.next()).deep.equal({ value: undefined, done: true }); 165 | expect(buffer[0]).eq(5); 166 | expect(iterator.next()).deep.equal({ value: undefined, done: true }); 167 | expect(buffer[0]).eq(5); 168 | }); 169 | 170 | it("iterates, buffer 2, bitmap 1", () => { 171 | const bitmap = new RoaringBitmap32([5]); 172 | const buffer = new Uint32Array(2); 173 | const iterator = new RoaringBitmap32ReverseIterator(bitmap, buffer); 174 | expect(Array.from(buffer)).deep.equal([0, 0]); 175 | expect(iterator.next()).deep.equal({ value: 5, done: false }); 176 | expect(Array.from(buffer)).deep.equal([5, 0]); 177 | expect(iterator.next()).deep.equal({ value: undefined, done: true }); 178 | expect(Array.from(buffer)).deep.equal([5, 0]); 179 | expect(iterator.next()).deep.equal({ value: undefined, done: true }); 180 | expect(Array.from(buffer)).deep.equal([5, 0]); 181 | }); 182 | 183 | it("iterates, buffer 1, bitmap 3", () => { 184 | const bitmap = new RoaringBitmap32([5, 7, 9]); 185 | const buffer = new Uint32Array(1); 186 | const iterator = new RoaringBitmap32ReverseIterator(bitmap, buffer); 187 | expect(buffer[0]).eq(0); 188 | expect(iterator.next()).deep.equal({ value: 9, done: false }); 189 | expect(buffer[0]).eq(9); 190 | expect(iterator.next()).deep.equal({ value: 7, done: false }); 191 | expect(buffer[0]).eq(7); 192 | expect(iterator.next()).deep.equal({ value: 5, done: false }); 193 | expect(buffer[0]).eq(5); 194 | }); 195 | 196 | it("iterates, buffer 2, bitmap 3", () => { 197 | const bitmap = new RoaringBitmap32([5, 7, 9]); 198 | const buffer = new Uint32Array(2); 199 | const iterator = new RoaringBitmap32ReverseIterator(bitmap, buffer); 200 | expect(Array.from(buffer)).deep.equal([0, 0]); 201 | expect(iterator.next()).deep.equal({ value: 9, done: false }); 202 | expect(Array.from(buffer)).deep.equal([9, 7]); 203 | expect(iterator.next()).deep.equal({ value: 7, done: false }); 204 | expect(Array.from(buffer)).deep.equal([9, 7]); 205 | expect(iterator.next()).deep.equal({ value: 5, done: false }); 206 | expect(Array.from(buffer)).deep.equal([5, 7]); 207 | expect(buffer[0]).eq(5); 208 | }); 209 | }); 210 | 211 | it("throws if the bitmap is changed while iterating", () => { 212 | const bitmap = new RoaringBitmap32(); 213 | bitmap.addRange(0, 1050); 214 | 215 | function doNothing(_v: any) {} 216 | 217 | let error: any; 218 | try { 219 | let n = 0; 220 | for (const v of new RoaringBitmap32ReverseIterator(bitmap, 256)) { 221 | if (n++ === 0) { 222 | bitmap.add(999999); 223 | } else { 224 | doNothing(v); 225 | } 226 | } 227 | } catch (e) { 228 | error = e; 229 | } 230 | expect(error.message).eq("RoaringBitmap32 iterator - bitmap changed while iterating"); 231 | }); 232 | 233 | describe("RoaringBitmap32 iterable", () => { 234 | it("returns a RoaringBitmap32ReverseIterator", () => { 235 | const bitmap = new RoaringBitmap32(); 236 | const iterator = bitmap.reverseIterator(); 237 | expect(iterator).to.be.instanceOf(RoaringBitmap32ReverseIterator); 238 | expect(typeof iterator.next).eq("function"); 239 | }); 240 | 241 | it("returns an empty iterator for an empty bitmap", () => { 242 | const bitmap = new RoaringBitmap32(); 243 | const iterator = bitmap.reverseIterator(); 244 | expect(iterator.next()).deep.equal({ 245 | done: true, 246 | value: undefined, 247 | }); 248 | expect(iterator.next()).deep.equal({ 249 | done: true, 250 | value: undefined, 251 | }); 252 | }); 253 | it("iterates a non empty bitmap", () => { 254 | const bitmap = new RoaringBitmap32([0xffffffff, 3]); 255 | const iterator = bitmap.reverseIterator(); 256 | expect(iterator.next()).deep.equal({ 257 | done: false, 258 | value: 0xffffffff, 259 | }); 260 | expect(iterator.next()).deep.equal({ 261 | done: false, 262 | value: 3, 263 | }); 264 | expect(iterator.next()).deep.equal({ 265 | done: true, 266 | value: undefined, 267 | }); 268 | }); 269 | }); 270 | }); 271 | --------------------------------------------------------------------------------