@pixagram/pixahash 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +191 -0
- package/index.d.ts +137 -0
- package/index.js +255 -0
- package/load.js +27 -0
- package/package.json +70 -0
- package/pkg/pixahash.d.ts +97 -0
- package/pkg/pixahash.js +366 -0
- package/pkg/pixahash_bg.wasm +0 -0
- package/pkg/pixahash_bg.wasm.d.ts +21 -0
- package/pool.js +193 -0
- package/worker.js +70 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Pixagram SA
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
# @pixagram/pixahash
|
|
2
|
+
|
|
3
|
+
A fast, WebAssembly-optimized **non-cryptographic** hash, compiled from Rust.
|
|
4
|
+
Built for fingerprinting medium payloads (≈5–30 kB) — content IDs, deduplication,
|
|
5
|
+
hash tables, bloom filters, checksums.
|
|
6
|
+
|
|
7
|
+
- **Flexible output** — `uint32`, `uint64`, 128-bit, or a full 256-bit digest, as a
|
|
8
|
+
number/BigInt, raw bytes, **hex**, or **base58btc** (any bit width 1–256).
|
|
9
|
+
- **Async API** — `await ready()` once, then call synchronously; or use the
|
|
10
|
+
auto-initializing `*Async` helpers.
|
|
11
|
+
- **Parallel** — an isomorphic worker pool (browser Workers / Node
|
|
12
|
+
`worker_threads`) for data-parallel hashing across many items.
|
|
13
|
+
- **Tiny & portable** — one ~34 kB `.wasm`, no native addons, runs in browsers,
|
|
14
|
+
bundlers, and Node ≥18.
|
|
15
|
+
|
|
16
|
+
> ⚠️ **Not cryptographic.** PixaHash is a *fingerprint*. It is not collision-resistant
|
|
17
|
+
> against an adversary who can choose inputs, and must **never** be used for message
|
|
18
|
+
> authentication, password hashing, key derivation, or any security boundary. For
|
|
19
|
+
> those, use BLAKE3, SHA-3, or Ascon. PixaHash optimizes for speed and distribution
|
|
20
|
+
> quality on *non-adversarial* data.
|
|
21
|
+
|
|
22
|
+
## Install
|
|
23
|
+
|
|
24
|
+
```sh
|
|
25
|
+
npm install @pixagram/pixahash
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
The package ships a prebuilt `.wasm`; no Rust toolchain is needed to consume it.
|
|
29
|
+
|
|
30
|
+
## Quick start
|
|
31
|
+
|
|
32
|
+
```js
|
|
33
|
+
import { ready, hash64, hashHex, hashBase58 } from '@pixagram/pixahash';
|
|
34
|
+
|
|
35
|
+
await ready(); // load the WASM module once
|
|
36
|
+
|
|
37
|
+
hash64('hello world'); // 64-bit BigInt
|
|
38
|
+
hashHex(bytes, 0, 128); // 128-bit hex string
|
|
39
|
+
hashBase58(bytes, 0, 256); // 256-bit base58btc string
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
Inputs may be a `string` (hashed as UTF-8), `Uint8Array`, `ArrayBuffer`, any
|
|
43
|
+
`TypedArray`, or a `DataView`. The optional **seed** is a `number` or `bigint`
|
|
44
|
+
and spans the full 64 bits (default `0`).
|
|
45
|
+
|
|
46
|
+
### Without the init dance
|
|
47
|
+
|
|
48
|
+
If you don't want to manage `ready()`, the `*Async` variants initialize on first
|
|
49
|
+
use and resolve on the calling thread:
|
|
50
|
+
|
|
51
|
+
```js
|
|
52
|
+
import { hash64Async } from '@pixagram/pixahash';
|
|
53
|
+
const h = await hash64Async('hello world');
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
### Streaming
|
|
57
|
+
|
|
58
|
+
For data you receive in chunks:
|
|
59
|
+
|
|
60
|
+
```js
|
|
61
|
+
import { ready, createHasher } from '@pixagram/pixahash';
|
|
62
|
+
await ready();
|
|
63
|
+
|
|
64
|
+
const h = createHasher(/* seed */ 0);
|
|
65
|
+
h.update(chunkA).update(chunkB);
|
|
66
|
+
const id = h.digestBase58(128);
|
|
67
|
+
h.free(); // release WASM memory (or use `using`)
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
Streaming output is bit-for-bit identical to the one-shot functions.
|
|
71
|
+
|
|
72
|
+
## Parallel hashing
|
|
73
|
+
|
|
74
|
+
```js
|
|
75
|
+
import { createPool } from '@pixagram/pixahash/pool';
|
|
76
|
+
|
|
77
|
+
const pool = await createPool(); // size = CPU count
|
|
78
|
+
const ids = await Promise.all(
|
|
79
|
+
files.map((bytes) => pool.hashHex(bytes, 0, 256))
|
|
80
|
+
);
|
|
81
|
+
pool.destroy();
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
Input is **copied** to the worker by default, so your buffers stay intact. For
|
|
85
|
+
large inputs you can move them instead with `createPool({ transfer: true })` —
|
|
86
|
+
faster, but it neuters the caller's `ArrayBuffer`. `hashMany(items, { op })` runs
|
|
87
|
+
a batch on a single worker when you'd rather avoid one message per item.
|
|
88
|
+
|
|
89
|
+
**When does a pool actually help?** A single 5–30 kB hash takes microseconds, so
|
|
90
|
+
the worker round-trip costs more than the hash. Reach for a pool to (a) keep the
|
|
91
|
+
UI thread responsive during a burst, or (b) parallelize across *many* items. For
|
|
92
|
+
a one-off hash, call the sync/async API directly.
|
|
93
|
+
|
|
94
|
+
## API
|
|
95
|
+
|
|
96
|
+
| Function | Returns |
|
|
97
|
+
| --- | --- |
|
|
98
|
+
| `ready()` / `isReady()` | `Promise<void>` / `boolean` |
|
|
99
|
+
| `hash32(data, seed?)` | `number` (u32) |
|
|
100
|
+
| `hash64(data, seed?)` | `bigint` (u64) |
|
|
101
|
+
| `hash128(data, seed?)` | `bigint` (u128) |
|
|
102
|
+
| `hashHex(data, seed?, bits=128)` | hex `string` |
|
|
103
|
+
| `hashBase58(data, seed?, bits=128)` | base58btc `string` |
|
|
104
|
+
| `digest(data, seed?)` | `Uint8Array` (32 bytes) |
|
|
105
|
+
| `*Async(...)` | the same, auto-initialized, as a `Promise` |
|
|
106
|
+
| `createHasher(seed?)` → `Hasher` | streaming: `update`, `digest32/64/128/Hex/Base58/Bytes`, `free` |
|
|
107
|
+
| `createPool(opts?)` → `HashPool` | parallel: same hash methods returning `Promise`s |
|
|
108
|
+
|
|
109
|
+
`bits` is clamped to `1–256`. Bits beyond 128 come from a finalizer expansion and
|
|
110
|
+
add output **width**, not extra collision resistance (see below).
|
|
111
|
+
|
|
112
|
+
## Design
|
|
113
|
+
|
|
114
|
+
Both [rapidhash](https://github.com/Nicoshev/rapidhash) and
|
|
115
|
+
[museair](https://github.com/eternal-io/museair) are excellent modern hashes, and
|
|
116
|
+
both lean on a 64×64→128-bit multiply (`umulh`). On native CPUs that's a single
|
|
117
|
+
instruction — but **WebAssembly has no high-multiply opcode**, so each one lowers
|
|
118
|
+
to four 32×32 multiplies plus carry handling. PixaHash is designed around what
|
|
119
|
+
WASM *does* do in one instruction: 64-bit multiply-low, rotate, xor, and add. That
|
|
120
|
+
puts it in the lineage of xxHash64 rather than the umulh-based designs.
|
|
121
|
+
|
|
122
|
+
The core borrows the best ideas from each:
|
|
123
|
+
|
|
124
|
+
- **xxHash64-style bulk loop** — 4 lanes, 32-byte stripes,
|
|
125
|
+
`round(acc, w) = rotl(acc + w·P2, 31)·P1`, using the xxHash primes. Multiply-low,
|
|
126
|
+
rotate, add only.
|
|
127
|
+
- **museair-style ring coupling** — after each stripe the lanes are mixed in a ring
|
|
128
|
+
(`v[i] ^= rotl(v[i+1], 17)`) so every input bit reaches the *whole* state, not just
|
|
129
|
+
one lane.
|
|
130
|
+
- **rapidhash-style tail + length injection** — short tails fold round-robin into the
|
|
131
|
+
lanes and the total length is mixed into all of them, killing length-extension-style
|
|
132
|
+
collisions on similar inputs.
|
|
133
|
+
- **All multipliers are odd constants**, so each multiply is a bijection on `u64`
|
|
134
|
+
and can never collapse state to zero — museair's "blinding multiplication" failure
|
|
135
|
+
mode is avoided structurally, without needing its additive workaround.
|
|
136
|
+
- A **Moremur** finalizer avalanches the result; for ≥32-byte inputs (the whole
|
|
137
|
+
target range) both 64-bit halves carry full entropy, giving a genuine ~128-bit
|
|
138
|
+
fingerprint. The 256-bit digest extends that width with two more Moremur rounds.
|
|
139
|
+
|
|
140
|
+
### Quality (measured)
|
|
141
|
+
|
|
142
|
+
From the native test harness (`cargo run --release --bin quality`):
|
|
143
|
+
|
|
144
|
+
- **Avalanche** on a 10 kB input: worst single-bit bias **1.3%**, average **0.4%**
|
|
145
|
+
(well within the 5σ noise band) — flipping any input bit flips ≈half the output bits.
|
|
146
|
+
- **Collisions**: 0 across 5M random 64-bit hashes; 32-bit collisions on 2M
|
|
147
|
+
structured keys land within ~2% of the ideal birthday-bound rate.
|
|
148
|
+
- **Streaming == one-shot** across thousands of random chunk splits.
|
|
149
|
+
- **Seed sensitivity**: changing the seed flips ≈half the bits.
|
|
150
|
+
- **Throughput**: ≈**9.3 GB/s** on 20 kB inputs natively (algorithmic speed).
|
|
151
|
+
|
|
152
|
+
> The `.wasm` bundled here is built **without** `wasm-opt` (binaryen wasn't available
|
|
153
|
+
> in the build sandbox). It is correct and passes the full native↔WASM cross-check,
|
|
154
|
+
> but for production you should rebuild with `wasm-opt` enabled (see below) for a
|
|
155
|
+
> smaller, faster module.
|
|
156
|
+
|
|
157
|
+
## Build from source
|
|
158
|
+
|
|
159
|
+
Requires the [Rust toolchain](https://rustup.rs), the `wasm32-unknown-unknown`
|
|
160
|
+
target, and [`wasm-pack`](https://rustwasm.github.io/wasm-pack/):
|
|
161
|
+
|
|
162
|
+
```sh
|
|
163
|
+
rustup target add wasm32-unknown-unknown
|
|
164
|
+
cargo install wasm-pack
|
|
165
|
+
./build.sh # → pkg/pixahash.js + pkg/pixahash_bg.wasm
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
To enable size/speed optimization, install `binaryen` and remove the
|
|
169
|
+
`wasm-opt = false` line under `[package.metadata.wasm-pack.profile.release]` in
|
|
170
|
+
`Cargo.toml`, then rebuild.
|
|
171
|
+
|
|
172
|
+
Run the native correctness/quality suite and the JS cross-check:
|
|
173
|
+
|
|
174
|
+
```sh
|
|
175
|
+
cargo run --release --no-default-features --bin quality
|
|
176
|
+
node test-node.mjs
|
|
177
|
+
node test-wrapper.mjs
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
### Threading roadmap
|
|
181
|
+
|
|
182
|
+
The worker pool is *data parallelism*: many independent items across many threads,
|
|
183
|
+
and it needs no special HTTP headers. **Parallelizing a single hash** (splitting one
|
|
184
|
+
large input across threads via `wasm-bindgen-rayon` + `SharedArrayBuffer`) is on the
|
|
185
|
+
roadmap; that path requires cross-origin isolation (`Cross-Origin-Opener-Policy:
|
|
186
|
+
same-origin` and `Cross-Origin-Embedder-Policy: require-corp`). A `simd128` build of
|
|
187
|
+
the core (identical output, gated behind the `simd` feature) is also planned.
|
|
188
|
+
|
|
189
|
+
## License
|
|
190
|
+
|
|
191
|
+
MIT © Pixagram SA
|
package/index.d.ts
ADDED
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
// Type definitions for @pixagram/pixahash
|
|
2
|
+
|
|
3
|
+
/** Anything accepted as hashable input. Strings are hashed as UTF-8. */
|
|
4
|
+
export type HashInput = string | Uint8Array | ArrayBuffer | ArrayBufferView;
|
|
5
|
+
|
|
6
|
+
/** Seed value. Numbers are truncated to an integer; both wrap mod 2^64. */
|
|
7
|
+
export type Seed = number | bigint;
|
|
8
|
+
|
|
9
|
+
// ---------------------------------------------------------------- lifecycle
|
|
10
|
+
|
|
11
|
+
/** Load + instantiate the WASM module. Idempotent and memoized. */
|
|
12
|
+
export function ready(): Promise<void>;
|
|
13
|
+
|
|
14
|
+
/** True once the WASM module is instantiated and the sync API is usable. */
|
|
15
|
+
export function isReady(): boolean;
|
|
16
|
+
|
|
17
|
+
// --------------------------------------------------------------- normalizers
|
|
18
|
+
|
|
19
|
+
/** Coerce supported inputs to a Uint8Array (no copy where possible). */
|
|
20
|
+
export function toBytes(input: HashInput): Uint8Array;
|
|
21
|
+
|
|
22
|
+
/** Coerce a seed to a u64 BigInt (wraps mod 2^64). Default 0. */
|
|
23
|
+
export function toSeed64(seed?: Seed): bigint;
|
|
24
|
+
|
|
25
|
+
// ------------------------------------------------------------ sync hash API
|
|
26
|
+
// Require `await ready()` first.
|
|
27
|
+
|
|
28
|
+
/** 32-bit hash as a number in [0, 2^32). */
|
|
29
|
+
export function hash32(data: HashInput, seed?: Seed): number;
|
|
30
|
+
|
|
31
|
+
/** 64-bit hash as a BigInt. */
|
|
32
|
+
export function hash64(data: HashInput, seed?: Seed): bigint;
|
|
33
|
+
|
|
34
|
+
/** 128-bit hash as a BigInt. */
|
|
35
|
+
export function hash128(data: HashInput, seed?: Seed): bigint;
|
|
36
|
+
|
|
37
|
+
/** Hex string of the first `bits` bits of the digest (1..256, default 128). */
|
|
38
|
+
export function hashHex(data: HashInput, seed?: Seed, bits?: number): string;
|
|
39
|
+
|
|
40
|
+
/** base58btc string of the first `bits` bits of the digest (1..256, default 128). */
|
|
41
|
+
export function hashBase58(data: HashInput, seed?: Seed, bits?: number): string;
|
|
42
|
+
|
|
43
|
+
/** The full 32-byte (256-bit) digest. */
|
|
44
|
+
export function digest(data: HashInput, seed?: Seed): Uint8Array;
|
|
45
|
+
|
|
46
|
+
// --------------------------------------------------- async (auto-init) API
|
|
47
|
+
// Ensure the module is ready, then run on the calling thread.
|
|
48
|
+
|
|
49
|
+
export function hash32Async(data: HashInput, seed?: Seed): Promise<number>;
|
|
50
|
+
export function hash64Async(data: HashInput, seed?: Seed): Promise<bigint>;
|
|
51
|
+
export function hash128Async(data: HashInput, seed?: Seed): Promise<bigint>;
|
|
52
|
+
export function hashHexAsync(data: HashInput, seed?: Seed, bits?: number): Promise<string>;
|
|
53
|
+
export function hashBase58Async(data: HashInput, seed?: Seed, bits?: number): Promise<string>;
|
|
54
|
+
export function digestAsync(data: HashInput, seed?: Seed): Promise<Uint8Array>;
|
|
55
|
+
|
|
56
|
+
// ----------------------------------------------------------- streaming API
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Incremental hasher. Backed by WASM memory — call `free()` (or use a `using`
|
|
60
|
+
* declaration) when done. Output methods may be called repeatedly.
|
|
61
|
+
*/
|
|
62
|
+
export class Hasher {
|
|
63
|
+
constructor(seed?: Seed);
|
|
64
|
+
update(data: HashInput): this;
|
|
65
|
+
digest32(): number;
|
|
66
|
+
digest64(): bigint;
|
|
67
|
+
digest128(): bigint;
|
|
68
|
+
digestHex(bits?: number): string;
|
|
69
|
+
digestBase58(bits?: number): string;
|
|
70
|
+
digestBytes(bits?: number): Uint8Array;
|
|
71
|
+
free(): void;
|
|
72
|
+
[Symbol.dispose](): void;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function createHasher(seed?: Seed): Hasher;
|
|
76
|
+
export function createHasherAsync(seed?: Seed): Promise<Hasher>;
|
|
77
|
+
|
|
78
|
+
declare const _default: {
|
|
79
|
+
ready: typeof ready;
|
|
80
|
+
isReady: typeof isReady;
|
|
81
|
+
toBytes: typeof toBytes;
|
|
82
|
+
toSeed64: typeof toSeed64;
|
|
83
|
+
hash32: typeof hash32;
|
|
84
|
+
hash64: typeof hash64;
|
|
85
|
+
hash128: typeof hash128;
|
|
86
|
+
hashHex: typeof hashHex;
|
|
87
|
+
hashBase58: typeof hashBase58;
|
|
88
|
+
digest: typeof digest;
|
|
89
|
+
hash32Async: typeof hash32Async;
|
|
90
|
+
hash64Async: typeof hash64Async;
|
|
91
|
+
hash128Async: typeof hash128Async;
|
|
92
|
+
hashHexAsync: typeof hashHexAsync;
|
|
93
|
+
hashBase58Async: typeof hashBase58Async;
|
|
94
|
+
digestAsync: typeof digestAsync;
|
|
95
|
+
Hasher: typeof Hasher;
|
|
96
|
+
createHasher: typeof createHasher;
|
|
97
|
+
createHasherAsync: typeof createHasherAsync;
|
|
98
|
+
};
|
|
99
|
+
export default _default;
|
|
100
|
+
|
|
101
|
+
// ------------------------------------------------------------- worker pool
|
|
102
|
+
// Exposed from '@pixagram/pixahash/pool'.
|
|
103
|
+
|
|
104
|
+
export interface HashPoolOptions {
|
|
105
|
+
/** Number of workers. Default: CPU count (navigator.hardwareConcurrency / os.cpus). */
|
|
106
|
+
size?: number;
|
|
107
|
+
/**
|
|
108
|
+
* Move input buffers to the worker instead of copying them. Faster for large
|
|
109
|
+
* inputs but neuters the caller's ArrayBuffer. Default: false (copy).
|
|
110
|
+
*/
|
|
111
|
+
transfer?: boolean;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export interface HashManyOptions {
|
|
115
|
+
/** Which per-item op to run. Default: "hash64". */
|
|
116
|
+
op?: "hash32" | "hash64" | "hash128" | "hashHex" | "hashBase58" | "digest";
|
|
117
|
+
seed?: Seed;
|
|
118
|
+
bits?: number;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export declare class HashPool {
|
|
122
|
+
constructor(opts?: HashPoolOptions);
|
|
123
|
+
readonly size: number;
|
|
124
|
+
/** Spawn the workers. Idempotent; awaited automatically by hash methods. */
|
|
125
|
+
start(): Promise<void>;
|
|
126
|
+
hash32(data: HashInput, seed?: Seed): Promise<number>;
|
|
127
|
+
hash64(data: HashInput, seed?: Seed): Promise<bigint>;
|
|
128
|
+
hash128(data: HashInput, seed?: Seed): Promise<bigint>;
|
|
129
|
+
hashHex(data: HashInput, seed?: Seed, bits?: number): Promise<string>;
|
|
130
|
+
hashBase58(data: HashInput, seed?: Seed, bits?: number): Promise<string>;
|
|
131
|
+
digest(data: HashInput, seed?: Seed): Promise<Uint8Array>;
|
|
132
|
+
hashMany(items: HashInput[], opts?: HashManyOptions): Promise<Array<number | bigint | string | Uint8Array>>;
|
|
133
|
+
destroy(): Promise<void>;
|
|
134
|
+
terminate(): Promise<void>;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export declare function createPool(opts?: HashPoolOptions): Promise<HashPool>;
|
package/index.js
ADDED
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
// @pixagram/pixahash — main entry.
|
|
2
|
+
//
|
|
3
|
+
// PixaHash is a fast, WASM-optimized *non-cryptographic* hash (a fingerprint).
|
|
4
|
+
// It is NOT collision-resistant against an adversary who can choose inputs, and
|
|
5
|
+
// must not be used for message authentication or as a password/KDF. Use it for
|
|
6
|
+
// hash tables, deduplication, content IDs/fingerprints, bloom filters and
|
|
7
|
+
// checksums. For adversarial resistance use BLAKE3 / SHA-3 / Ascon.
|
|
8
|
+
//
|
|
9
|
+
// Three ways to call it:
|
|
10
|
+
// 1. Sync (hot path): await ready(); hash64(bytes)
|
|
11
|
+
// 2. Async (convenience): await hash64Async(bytes) // auto-inits, same thread
|
|
12
|
+
// 3. Parallel: import { createPool } from '@pixagram/pixahash/pool'
|
|
13
|
+
//
|
|
14
|
+
// Inputs accept string | Uint8Array | ArrayBuffer | TypedArray | DataView.
|
|
15
|
+
// Seeds accept number | bigint and span the full 64 bits.
|
|
16
|
+
|
|
17
|
+
import { loadWasm } from './load.js';
|
|
18
|
+
|
|
19
|
+
let wasm = null;
|
|
20
|
+
let readyPromise = null;
|
|
21
|
+
|
|
22
|
+
const TEXT_ENCODER = new TextEncoder();
|
|
23
|
+
|
|
24
|
+
// ---------------------------------------------------------------- lifecycle
|
|
25
|
+
|
|
26
|
+
/** Load + instantiate the WASM module. Idempotent and memoized. */
|
|
27
|
+
export function ready() {
|
|
28
|
+
if (!readyPromise) {
|
|
29
|
+
readyPromise = loadWasm().then((w) => {
|
|
30
|
+
wasm = w;
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
return readyPromise;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** True once the WASM module is instantiated and the sync API is usable. */
|
|
37
|
+
export function isReady() {
|
|
38
|
+
return wasm !== null;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function w() {
|
|
42
|
+
if (wasm === null) {
|
|
43
|
+
throw new Error(
|
|
44
|
+
"pixahash: not initialized. `await ready()` once at startup before " +
|
|
45
|
+
"calling sync functions, or use the *Async variants / a HashPool."
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
return wasm;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// --------------------------------------------------------------- normalizers
|
|
52
|
+
|
|
53
|
+
/** Coerce supported inputs to a Uint8Array (no copy where possible). */
|
|
54
|
+
export function toBytes(input) {
|
|
55
|
+
if (typeof input === "string") return TEXT_ENCODER.encode(input);
|
|
56
|
+
if (input instanceof Uint8Array) return input;
|
|
57
|
+
if (input instanceof ArrayBuffer) return new Uint8Array(input);
|
|
58
|
+
if (ArrayBuffer.isView(input)) {
|
|
59
|
+
return new Uint8Array(input.buffer, input.byteOffset, input.byteLength);
|
|
60
|
+
}
|
|
61
|
+
throw new TypeError(
|
|
62
|
+
"pixahash: data must be a string, Uint8Array, ArrayBuffer, TypedArray or DataView."
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** Coerce number|bigint to a u64 seed (wraps mod 2^64). Default 0. */
|
|
67
|
+
export function toSeed64(seed) {
|
|
68
|
+
if (seed === undefined || seed === null) return 0n;
|
|
69
|
+
if (typeof seed === "bigint") return BigInt.asUintN(64, seed);
|
|
70
|
+
if (typeof seed === "number") {
|
|
71
|
+
if (!Number.isFinite(seed)) {
|
|
72
|
+
throw new TypeError("pixahash: numeric seed must be finite.");
|
|
73
|
+
}
|
|
74
|
+
return BigInt.asUintN(64, BigInt(Math.trunc(seed)));
|
|
75
|
+
}
|
|
76
|
+
throw new TypeError("pixahash: seed must be a number or bigint.");
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function clampBits(bits) {
|
|
80
|
+
if (bits === undefined || bits === null) return 128;
|
|
81
|
+
const b = Math.trunc(Number(bits));
|
|
82
|
+
if (!Number.isFinite(b) || b < 1) return 1;
|
|
83
|
+
if (b > 256) return 256;
|
|
84
|
+
return b;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// little-endian bytes -> BigInt
|
|
88
|
+
function leToBigInt(bytes, start, len) {
|
|
89
|
+
let v = 0n;
|
|
90
|
+
for (let i = len - 1; i >= 0; i--) {
|
|
91
|
+
v = (v << 8n) | BigInt(bytes[start + i]);
|
|
92
|
+
}
|
|
93
|
+
return v;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function hexToBytes(hex) {
|
|
97
|
+
const out = new Uint8Array(hex.length >> 1);
|
|
98
|
+
for (let i = 0; i < out.length; i++) {
|
|
99
|
+
out[i] = parseInt(hex.substr(i * 2, 2), 16);
|
|
100
|
+
}
|
|
101
|
+
return out;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// ------------------------------------------------------------ sync hash API
|
|
105
|
+
|
|
106
|
+
/** 32-bit hash as a JS number (0 .. 2^32-1). */
|
|
107
|
+
export function hash32(data, seed) {
|
|
108
|
+
const m = w();
|
|
109
|
+
const s = toSeed64(seed);
|
|
110
|
+
const bytes = toBytes(data);
|
|
111
|
+
if (s <= 0xffffffffn) {
|
|
112
|
+
return m.hash32(bytes, Number(s)); // fast path: no BigInt seed marshaling
|
|
113
|
+
}
|
|
114
|
+
// Honor seeds wider than 32 bits while matching the native fold exactly.
|
|
115
|
+
const h0 = m.hash64(bytes, s);
|
|
116
|
+
return Number(BigInt.asUintN(32, h0 ^ (h0 >> 32n)));
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/** 64-bit hash as a BigInt. */
|
|
120
|
+
export function hash64(data, seed) {
|
|
121
|
+
return w().hash64(toBytes(data), toSeed64(seed));
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/** 128-bit hash as a BigInt. */
|
|
125
|
+
export function hash128(data, seed) {
|
|
126
|
+
const d = w().digest(toBytes(data), toSeed64(seed)); // 32 LE bytes
|
|
127
|
+
return leToBigInt(d, 0, 16);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/** Hex string of the first `bits` bits of the digest (bits 1..256, default 128). */
|
|
131
|
+
export function hashHex(data, seed, bits) {
|
|
132
|
+
return w().hashHex(toBytes(data), toSeed64(seed), clampBits(bits));
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/** base58btc string of the first `bits` bits of the digest (bits 1..256, default 128). */
|
|
136
|
+
export function hashBase58(data, seed, bits) {
|
|
137
|
+
return w().hashBase58(toBytes(data), toSeed64(seed), clampBits(bits));
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/** The full 32-byte (256-bit) digest as a Uint8Array. */
|
|
141
|
+
export function digest(data, seed) {
|
|
142
|
+
return w().digest(toBytes(data), toSeed64(seed));
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// --------------------------------------------------- async (auto-init) API
|
|
146
|
+
// These ensure the module is ready, then run on the *calling* thread. For true
|
|
147
|
+
// off-main-thread parallelism use a HashPool from '@pixagram/pixahash/pool'.
|
|
148
|
+
|
|
149
|
+
export async function hash32Async(data, seed) {
|
|
150
|
+
await ready();
|
|
151
|
+
return hash32(data, seed);
|
|
152
|
+
}
|
|
153
|
+
export async function hash64Async(data, seed) {
|
|
154
|
+
await ready();
|
|
155
|
+
return hash64(data, seed);
|
|
156
|
+
}
|
|
157
|
+
export async function hash128Async(data, seed) {
|
|
158
|
+
await ready();
|
|
159
|
+
return hash128(data, seed);
|
|
160
|
+
}
|
|
161
|
+
export async function hashHexAsync(data, seed, bits) {
|
|
162
|
+
await ready();
|
|
163
|
+
return hashHex(data, seed, bits);
|
|
164
|
+
}
|
|
165
|
+
export async function hashBase58Async(data, seed, bits) {
|
|
166
|
+
await ready();
|
|
167
|
+
return hashBase58(data, seed, bits);
|
|
168
|
+
}
|
|
169
|
+
export async function digestAsync(data, seed) {
|
|
170
|
+
await ready();
|
|
171
|
+
return digest(data, seed);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// ----------------------------------------------------------- streaming API
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Incremental hasher. Feed data in chunks via `update`, then read any output.
|
|
178
|
+
* Backed by a WASM object holding native memory — call `free()` (or use a
|
|
179
|
+
* `using` declaration) when done. Output methods may be called repeatedly.
|
|
180
|
+
*/
|
|
181
|
+
export class Hasher {
|
|
182
|
+
constructor(seed) {
|
|
183
|
+
this._h = new (w().Hasher)(toSeed64(seed));
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/** Absorb a chunk. Returns `this` for chaining. */
|
|
187
|
+
update(data) {
|
|
188
|
+
this._h.update(toBytes(data));
|
|
189
|
+
return this;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
digest32() {
|
|
193
|
+
return this._h.digest32();
|
|
194
|
+
}
|
|
195
|
+
digest64() {
|
|
196
|
+
return this._h.digest64();
|
|
197
|
+
}
|
|
198
|
+
digest128() {
|
|
199
|
+
return leToBigInt(hexToBytes(this._h.digestHex(128)), 0, 16);
|
|
200
|
+
}
|
|
201
|
+
digestHex(bits) {
|
|
202
|
+
return this._h.digestHex(clampBits(bits));
|
|
203
|
+
}
|
|
204
|
+
digestBase58(bits) {
|
|
205
|
+
return this._h.digestBase58(clampBits(bits));
|
|
206
|
+
}
|
|
207
|
+
/** Raw digest bytes for the first `bits` bits (default 256 → 32 bytes). */
|
|
208
|
+
digestBytes(bits) {
|
|
209
|
+
return hexToBytes(this._h.digestHex(clampBits(bits ?? 256)));
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/** Release the underlying WASM memory. The instance is unusable afterward. */
|
|
213
|
+
free() {
|
|
214
|
+
if (this._h) {
|
|
215
|
+
this._h.free();
|
|
216
|
+
this._h = null;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
[Symbol.dispose]() {
|
|
220
|
+
this.free();
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/** Create a streaming `Hasher` (requires `await ready()` first). */
|
|
225
|
+
export function createHasher(seed) {
|
|
226
|
+
return new Hasher(seed);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/** Create a streaming `Hasher`, ensuring the module is initialized first. */
|
|
230
|
+
export async function createHasherAsync(seed) {
|
|
231
|
+
await ready();
|
|
232
|
+
return new Hasher(seed);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
export default {
|
|
236
|
+
ready,
|
|
237
|
+
isReady,
|
|
238
|
+
toBytes,
|
|
239
|
+
toSeed64,
|
|
240
|
+
hash32,
|
|
241
|
+
hash64,
|
|
242
|
+
hash128,
|
|
243
|
+
hashHex,
|
|
244
|
+
hashBase58,
|
|
245
|
+
digest,
|
|
246
|
+
hash32Async,
|
|
247
|
+
hash64Async,
|
|
248
|
+
hash128Async,
|
|
249
|
+
hashHexAsync,
|
|
250
|
+
hashBase58Async,
|
|
251
|
+
digestAsync,
|
|
252
|
+
Hasher,
|
|
253
|
+
createHasher,
|
|
254
|
+
createHasherAsync,
|
|
255
|
+
};
|
package/load.js
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
// Shared, memoized WASM loader. Works in browsers/bundlers (relative fetch) and
|
|
2
|
+
// in Node (reads the .wasm off disk, since Node's fetch can't load file: URLs).
|
|
3
|
+
import init, * as wasm from './pkg/pixahash.js';
|
|
4
|
+
|
|
5
|
+
let initPromise = null;
|
|
6
|
+
|
|
7
|
+
export function loadWasm() {
|
|
8
|
+
if (initPromise) return initPromise;
|
|
9
|
+
initPromise = (async () => {
|
|
10
|
+
const isNode =
|
|
11
|
+
typeof process !== 'undefined' && process.versions != null && process.versions.node != null;
|
|
12
|
+
if (isNode) {
|
|
13
|
+
const { readFile } = await import('node:fs/promises');
|
|
14
|
+
const { fileURLToPath } = await import('node:url');
|
|
15
|
+
const url = new URL('./pkg/pixahash_bg.wasm', import.meta.url);
|
|
16
|
+
const bytes = await readFile(fileURLToPath(url));
|
|
17
|
+
await init({ module_or_path: bytes });
|
|
18
|
+
} else {
|
|
19
|
+
// Browser / bundler: the glue fetches pixahash_bg.wasm next to itself.
|
|
20
|
+
await init();
|
|
21
|
+
}
|
|
22
|
+
return wasm;
|
|
23
|
+
})();
|
|
24
|
+
return initPromise;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export { wasm };
|