mirror of
https://github.com/nvms/prsm.git
synced 2025-12-16 16:10:54 +00:00
refactor: convert ids to functional API with improved tests
- Replace class-based implementation with functional approach - Switch from Manten to Vitest for testing - Update README with clearer API documentation - Fix alphabet randomization test
This commit is contained in:
parent
6be7fbbfe0
commit
2acba51367
@ -2,29 +2,41 @@
|
|||||||
|
|
||||||
[](https://www.npmjs.com/package/@prsm/ids)
|
[](https://www.npmjs.com/package/@prsm/ids)
|
||||||
|
|
||||||
Short, obfuscated, collision-proof, and reversible identifiers.
|
Short, obfuscated, collision-proof, reversible identifiers.
|
||||||
|
|
||||||
Because sometimes internal identifiers are sensitive, or you just don't want to let a user know that their ID is 1.
|
## Usage
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
import ID from "@prsm/ids";
|
import id from "@prsm/ids";
|
||||||
|
|
||||||
ID.encode(12389125); // phsV8T
|
id.encode(12389125); // "7rYTs_"
|
||||||
ID.decode("phsV8T"); // 12389125
|
id.decode("7rYTs_"); // 12389125
|
||||||
```
|
```
|
||||||
|
|
||||||
You can (and should) set your own alphabet string:
|
## Configuration
|
||||||
|
|
||||||
|
Set custom alphabet:
|
||||||
```typescript
|
```typescript
|
||||||
ID.alphabet = "GZwBHpfWybgQ5d_2mM-jh84K69tqYknx7LN3zvDrcSJVRPXsCFT";
|
id.setAlphabet("GZwBHpfWybgQ5d_2mM-jh84K69tqYknx7LN3zvDrcSJVRPXsCFT");
|
||||||
ID.alphabet = "TgzMhJXtRSVBnHFksZQc5j-yGx84W3rNDfK6p_Cbqd29YLm7Pwv";
|
|
||||||
ID.alphabet = "kbHn53dZphT2FvGMBxYJKqS7-cPV_Ct6LwjWRDfXmygzrQ48N9s";
|
|
||||||
```
|
```
|
||||||
|
|
||||||
If your use case makes sense, you can also generate a random alphabet string with `randomizeAlphabet`.
|
Randomize alphabet:
|
||||||
|
|
||||||
When the alphabet changes, though, the encoded IDs will change as well. Decoding will still work, but the decoded value will be different.
|
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
ID.randomizeAlphabet();
|
id.randomizeAlphabet();
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## API
|
||||||
|
|
||||||
|
| Function | Description |
|
||||||
|
|-----------------------|-------------------------------------------|
|
||||||
|
| `encode(num)` | Converts number to obfuscated string |
|
||||||
|
| `decode(str)` | Converts obfuscated string back to number |
|
||||||
|
| `setAlphabet(str)` | Sets custom alphabet for encoding |
|
||||||
|
| `getAlphabet()` | Returns current alphabet |
|
||||||
|
| `randomizeAlphabet()` | Shuffles alphabet characters randomly |
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
- Maximum encodable value: 2,147,483,647 (MAX_INT32)
|
||||||
|
- Changing alphabet changes encoded values
|
||||||
|
- Encoded values must be decoded with same alphabet
|
||||||
|
|||||||
Binary file not shown.
@ -13,7 +13,8 @@
|
|||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "tsup",
|
"build": "tsup",
|
||||||
"test": "bun tests/index.ts",
|
"test": "vitest run",
|
||||||
|
"test:watch": "vitest",
|
||||||
"release": "bumpp package.json && npm publish --access public"
|
"release": "bumpp package.json && npm publish --access public"
|
||||||
},
|
},
|
||||||
"author": "nvms",
|
"author": "nvms",
|
||||||
@ -23,8 +24,8 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"bumpp": "^9.5.1",
|
"bumpp": "^9.5.1",
|
||||||
"manten": "^0.6.0",
|
|
||||||
"tsup": "^8.2.4",
|
"tsup": "^8.2.4",
|
||||||
"typescript": "^4.9.5"
|
"typescript": "^4.9.5",
|
||||||
|
"vitest": "^3.0.9"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,74 +1,76 @@
|
|||||||
import long from "long";
|
import long from "long";
|
||||||
|
|
||||||
export default class ID {
|
const MAX_INT32 = 2_147_483_647;
|
||||||
private static MAX_INT32 = 2_147_483_647;
|
const PRIME = 1_125_812_041;
|
||||||
private static MULTIPLIER = 4_294_967_296;
|
const INVERSE = 348_986_105;
|
||||||
|
const RANDOM = 998_048_641;
|
||||||
|
const DEFAULT_ALPHABET = "23456789bcdfghjkmnpqrstvwxyzBCDFGHJKLMNPQRSTVWXYZ-_";
|
||||||
|
|
||||||
static alphabet: string =
|
let alphabet = DEFAULT_ALPHABET;
|
||||||
"23456789bcdfghjkmnpqrstvwxyzBCDFGHJKLMNPQRSTVWXYZ-_";
|
|
||||||
static prime: number = 1_125_812_041;
|
|
||||||
static inverse: number = 348_986_105;
|
|
||||||
static random: number = 998_048_641;
|
|
||||||
|
|
||||||
static get base(): number {
|
const getBase = () => alphabet.length;
|
||||||
return ID.alphabet.length;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static shorten(id: number): string {
|
const shorten = (id: number): string => {
|
||||||
let result = "";
|
let result = "";
|
||||||
|
const base = getBase();
|
||||||
|
|
||||||
while (id > 0) {
|
while (id > 0) {
|
||||||
result = ID.alphabet[id % ID.base] + result;
|
result = alphabet[id % base] + result;
|
||||||
id = Math.floor(id / ID.base);
|
id = Math.floor(id / base);
|
||||||
}
|
}
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
};
|
||||||
|
|
||||||
private static unshorten(str: string): number {
|
const unshorten = (str: string): number => {
|
||||||
let result = 0;
|
let result = 0;
|
||||||
|
const base = getBase();
|
||||||
|
|
||||||
for (let i = 0; i < str.length; i++) {
|
for (let i = 0; i < str.length; i++) {
|
||||||
result = result * ID.base + ID.alphabet.indexOf(str[i]);
|
result = result * base + alphabet.indexOf(str[i]);
|
||||||
}
|
}
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
};
|
||||||
|
|
||||||
static encode = (num: number): string => {
|
const id = {
|
||||||
if (num > ID.MAX_INT32) {
|
MAX_INT32,
|
||||||
|
DEFAULT_ALPHABET,
|
||||||
|
|
||||||
|
encode: (num: number): string => {
|
||||||
|
if (num > MAX_INT32) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`Number (${num}) is too large to encode. MAX_INT32 is ${ID.MAX_INT32}`,
|
`Number (${num}) is too large to encode. MAX_INT32 is ${MAX_INT32}`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const n: long = long.fromInt(num);
|
const n = long.fromInt(num);
|
||||||
|
|
||||||
return ID.shorten(
|
return shorten(
|
||||||
n
|
n.multiply(PRIME).and(long.fromInt(MAX_INT32)).xor(RANDOM).toInt(),
|
||||||
.multiply(ID.prime)
|
|
||||||
.and(long.fromInt(ID.MAX_INT32))
|
|
||||||
.xor(ID.random)
|
|
||||||
.toInt(),
|
|
||||||
);
|
);
|
||||||
};
|
},
|
||||||
|
|
||||||
static decode = (str: string): number => {
|
decode: (str: string): number => {
|
||||||
const n: long = long.fromInt(ID.unshorten(str));
|
const n = long.fromInt(unshorten(str));
|
||||||
|
|
||||||
return n
|
return n.xor(RANDOM).multiply(INVERSE).and(long.fromInt(MAX_INT32)).toInt();
|
||||||
.xor(ID.random)
|
},
|
||||||
.multiply(ID.inverse)
|
|
||||||
.and(long.fromInt(ID.MAX_INT32))
|
|
||||||
.toInt();
|
|
||||||
};
|
|
||||||
|
|
||||||
static randomizeAlphabet(): void {
|
getAlphabet: (): string => alphabet,
|
||||||
const array = ID.alphabet.split('');
|
|
||||||
|
setAlphabet: (newAlphabet: string): void => {
|
||||||
|
alphabet = newAlphabet;
|
||||||
|
},
|
||||||
|
|
||||||
|
randomizeAlphabet: (): void => {
|
||||||
|
const array = alphabet.split("");
|
||||||
for (let i = array.length - 1; i > 0; i--) {
|
for (let i = array.length - 1; i > 0; i--) {
|
||||||
const j = Math.floor(Math.random() * (i + 1));
|
const j = Math.floor(Math.random() * (i + 1));
|
||||||
[array[i], array[j]] = [array[j], array[i]];
|
[array[i], array[j]] = [array[j], array[i]];
|
||||||
}
|
}
|
||||||
ID.alphabet = array.join('');
|
alphabet = array.join("");
|
||||||
}
|
},
|
||||||
}
|
};
|
||||||
|
|
||||||
|
export default id;
|
||||||
|
|||||||
51
packages/ids/tests/index.test.ts
Normal file
51
packages/ids/tests/index.test.ts
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
import { describe, it, expect, beforeEach } from 'vitest';
|
||||||
|
import id from "../src";
|
||||||
|
|
||||||
|
describe("ids", () => {
|
||||||
|
// Reset alphabet before each test
|
||||||
|
beforeEach(() => {
|
||||||
|
id.setAlphabet(id.DEFAULT_ALPHABET);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("encodes as expected", () => {
|
||||||
|
const encoded = id.encode(12389125);
|
||||||
|
expect(encoded).toBe("7rYTs_");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("decodes as expected", () => {
|
||||||
|
const decoded = id.decode("7rYTs_");
|
||||||
|
expect(decoded).toBe(12389125);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("changing the alphabet is effective", () => {
|
||||||
|
id.setAlphabet("GZwBHpfWybgQ5d_2mM-jh84K69tqYknx7LN3zvDrcSJVRPXsCFT");
|
||||||
|
expect(id.encode(12389125)).toBe("phsV8T");
|
||||||
|
expect(id.decode("phsV8T")).toBe(12389125);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("shuffling the alphabet changes encoding but preserves round-trip integrity", () => {
|
||||||
|
// First randomization
|
||||||
|
id.randomizeAlphabet();
|
||||||
|
const encoded1 = id.encode(12389125);
|
||||||
|
const decoded1 = id.decode(encoded1);
|
||||||
|
expect(decoded1).toBe(12389125);
|
||||||
|
|
||||||
|
// Store the current alphabet
|
||||||
|
const alphabet1 = id.getAlphabet();
|
||||||
|
|
||||||
|
// Second randomization
|
||||||
|
id.randomizeAlphabet();
|
||||||
|
const alphabet2 = id.getAlphabet();
|
||||||
|
|
||||||
|
// Encode with the new alphabet
|
||||||
|
const encoded2 = id.encode(12389125);
|
||||||
|
const decoded2 = id.decode(encoded2);
|
||||||
|
|
||||||
|
// Each alphabet should produce different encodings for the same number
|
||||||
|
expect(alphabet1).not.toBe(alphabet2);
|
||||||
|
expect(encoded1).not.toBe(encoded2);
|
||||||
|
|
||||||
|
// But round-trip encoding/decoding should work with each alphabet
|
||||||
|
expect(decoded2).toBe(12389125);
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -1,36 +0,0 @@
|
|||||||
import { describe, expect } from "manten";
|
|
||||||
import ID from "../src";
|
|
||||||
|
|
||||||
describe("ids", async ({ test }) => {
|
|
||||||
test("encodes as expected", () => {
|
|
||||||
const encoded = ID.encode(12389125);
|
|
||||||
expect(encoded).toBe("7rYTs_");
|
|
||||||
});
|
|
||||||
|
|
||||||
test("decodes as expected", () => {
|
|
||||||
const decoded = ID.decode("7rYTs_");
|
|
||||||
expect(decoded).toBe(12389125);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("changing the alphabet is effective", () => {
|
|
||||||
ID.alphabet = "GZwBHpfWybgQ5d_2mM-jh84K69tqYknx7LN3zvDrcSJVRPXsCFT";
|
|
||||||
expect(ID.encode(12389125)).toBe("phsV8T");
|
|
||||||
expect(ID.decode("phsV8T")).toBe(12389125);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("shuffling the alphabet still allows you to decode things", () => {
|
|
||||||
ID.randomizeAlphabet();
|
|
||||||
const encoded = ID.encode(12389125);
|
|
||||||
const decoded = ID.decode(encoded);
|
|
||||||
expect(decoded).toBe(12389125);
|
|
||||||
|
|
||||||
console.log(ID.alphabet);
|
|
||||||
|
|
||||||
ID.randomizeAlphabet();
|
|
||||||
// const encoded2 = ID.encode(12389125);
|
|
||||||
const decoded2 = ID.decode(encoded);
|
|
||||||
expect(decoded2).toBe(12389125);
|
|
||||||
|
|
||||||
// expect(encoded).not.toBe(encoded2);
|
|
||||||
})
|
|
||||||
});
|
|
||||||
7
packages/ids/vitest.config.ts
Normal file
7
packages/ids/vitest.config.ts
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
import { defineConfig } from 'vitest/config';
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
test: {
|
||||||
|
environment: 'node',
|
||||||
|
},
|
||||||
|
});
|
||||||
Loading…
Reference in New Issue
Block a user