Skip to content

Commit

Permalink
refactor(ksuid): extract IKSUID, update impls, docs
Browse files Browse the repository at this point in the history
BREAKING CHANGE: Rename KSUID => KSUID32 / defKSUID32()

- update readme
- update tests
- update pkg meta
  • Loading branch information
postspectacular committed Aug 7, 2021
1 parent c1c6dc0 commit 1276c94
Show file tree
Hide file tree
Showing 7 changed files with 94 additions and 72 deletions.
24 changes: 12 additions & 12 deletions packages/ksuid/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,28 +26,28 @@ Configurable sortable unique IDs, binary & base-N encoded, 32/64bit time resolut
Idea based on [segmentio/ksuid](https://github.com/segmentio/ksuid), though with
added flexibility in terms of configuration & implementation:

- Configurable bit size (default: 128bits)
- Configurable bit size (default: 160 bits)
- Base-N encoding scheme (default: base62, see
[@thi.ng/base-n](https://github.com/thi-ng/umbrella/tree/develop/packages/base-n)
for alternatives)
- Timestamp resolution (seconds [32 bits], milliseconds [64 bits])
- Epoch start time offset
- Time-only base ID generation (optional)
- KSUID parsing / decomposition
- Configurable RNG source (default: `window.crypto` or `Math.random`)
- Configurable RNG source (default: `window.crypto`, `Math.random` fallback)

KSUIDs generated w/ this package consist of the lower 32bits or 64bits of an
Unix epoch (potentially time shifted to free up bits for future timestamps) and
N additional bits of a random payload (from a configurable source). IDs can be
generated as byte arrays or base-N encoded strings. For the latter, the JS
runtime MUST support
KSUIDs generated w/ this package are composed from a 32 bit or 64 bit Unix epoch
(by default time shifted to free up bits for future timestamps) and N additional
bits of a random payload (from a configurable source). IDs can be generated as
byte arrays or base-N encoded strings. For the latter, the JS runtime MUST
support
[`BigInt`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/BigInt).

![KSUID bit layout diagram](https://raw.githubusercontent.com/thi-ng/umbrella/develop/assets/ksuid/ksuid.png)

### Status

**ALPHA** - bleeding edge / work-in-progress
**STABLE** - used in production

[Search or submit any issues for this package](https://github.com/thi-ng/umbrella/issues?q=%5Bksuid%5D+in%3Atitle)

Expand All @@ -70,7 +70,7 @@ yarn add @thi.ng/ksuid
<script src="https://unpkg.com/@thi.ng/ksuid/lib/index.umd.js" crossorigin></script>
```

Package sizes (gzipped, pre-treeshake): ESM: 708 bytes / CJS: 774 bytes / UMD: 887 bytes
Package sizes (gzipped, pre-treeshake): ESM: 730 bytes / CJS: 798 bytes / UMD: 907 bytes

## Dependencies

Expand All @@ -84,10 +84,10 @@ Package sizes (gzipped, pre-treeshake): ESM: 708 bytes / CJS: 774 bytes / UMD: 8
[Generated API docs](https://docs.thi.ng/umbrella/ksuid/)

```ts
import { defKSUID } from "@thi.ng/ksuid";
import { defKSUID32, defKSUID64 } from "@thi.ng/ksuid";

// init 32bit epoch (resolution: seconds) w/ defaults
const id = defKSUID();
const id = defKSUID32();
// init 64bit epoch (resolution: milliseconds), same API
const id = defKSUID64();

Expand Down Expand Up @@ -126,7 +126,7 @@ Creating custom IDs:
import { BASE36 } from "@thi.ng/base-n";

// no time shift, 64bit random
const id36 = defKSUID({ base: BASE36, epoch: 0, bytes: 8 });
const id36 = defKSUID32({ base: BASE36, epoch: 0, bytes: 8 });
// '2VOUKH4K59AG0RXR4XH'
```

Expand Down
1 change: 0 additions & 1 deletion packages/ksuid/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,6 @@
"base-n",
"random"
],
"status": "alpha",
"year": 2020
}
}
45 changes: 6 additions & 39 deletions packages/ksuid/src/aksuid.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,24 +2,23 @@ import { assert } from "@thi.ng/api";
import { BASE62, BaseN } from "@thi.ng/base-n";
import { IRandom, randomBytes, randomBytesFrom } from "@thi.ng/random";
import { padLeft } from "@thi.ng/strings";
import type { KSUIDOpts } from "./api";
import type { IKSUID, KSUIDOpts } from "./api";

/**
* Abstract base class for both 32 & 64bit implementations. See {@link KSUID}
* and {@link KSUID64}.
*/
export abstract class AKSUID {
/**
* Byte size of a single ID, based on the KSUID's configuration. The default
* config will result in 20-byte IDs (27 chars base62 encoded).
*/
export abstract class AKSUID implements IKSUID {
readonly size: number;
readonly base: BaseN;
readonly epoch: number;
protected rnd?: IRandom;
protected pad: (x: any) => string;

constructor(public readonly epochSize: number, opts: Partial<KSUIDOpts>) {
protected constructor(
public readonly epochSize: number,
opts: Partial<KSUIDOpts>
) {
this.base = opts.base || BASE62;
this.rnd = opts.rnd;
this.epoch = opts.epoch!;
Expand All @@ -30,60 +29,28 @@ export abstract class AKSUID {
);
}

/**
* Returns a new baseN encoded ID string.
*/
next() {
return this.format(this.nextBinary());
}

/**
* Returns a new ID as byte array.
*/
nextBinary() {
const buf = this.timeOnlyBinary();
return this.rnd
? randomBytesFrom(this.rnd, buf, this.epochSize)
: randomBytes(buf, this.epochSize);
}

/**
* Returns a new baseN encoded ID string for given `epoch` (default: current
* time) and with all random payload bytes set to 0.
*
* @param epoch
*/
timeOnly(epoch?: number) {
return this.format(this.timeOnlyBinary(epoch));
}

/**
* Binary version of {@link KSUI.timeOnly}, but returns byte array. The
* first `epochSize` bytes will contain the timestamp.
*
* @param epoch
*/
abstract timeOnlyBinary(epoch?: number): Uint8Array;

/**
* Returns baseN encoded version of given binary ID (generated via
* `.nextBinary()`).
*/
format(buf: Uint8Array) {
this.ensureSize(buf);
return this.pad(this.base.encodeBytes(buf));
}

/**
* Takes a KSUID string (assumed to be generated with the same config as
* this instance) and parses it into an object of: `{ epoch, id }`, where
* `epoch` is the Unix epoch of the ID and `id` the random bytes.
*
* @remarks
* This operation requires `bigint` support by the host environment.
*
* @param id
*/
abstract parse(id: string): { epoch: number; id: Uint8Array };

protected ensureSize(buf: Uint8Array) {
Expand Down
55 changes: 55 additions & 0 deletions packages/ksuid/src/api.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,61 @@
import type { BaseN } from "@thi.ng/base-n";
import type { IRandom } from "@thi.ng/random";

export interface IKSUID {
/**
* Byte size of a single ID, based on the KSUID's configuration. The default
* config will result in 20-byte IDs (27 chars base62 encoded).
*/
readonly size: number;
readonly base: BaseN;
readonly epoch: number;
readonly epochSize: number;

/**
* Returns a new baseN encoded ID string.
*/
next(): string;

/**
* Returns a new ID as byte array.
*/
nextBinary(): Uint8Array;

/**
* Returns a new baseN encoded ID string for given `epoch` (default: current
* time) and with all random payload bytes set to 0.
*
* @param epoch
*/
timeOnly(epoch?: number): string;

/**
* Binary version of {@link KSUI.timeOnly}, but returns byte array. The
* first `epochSize` bytes will contain the timestamp.
*
* @param epoch
*/
timeOnlyBinary(epoch?: number): Uint8Array;

/**
* Returns baseN encoded version of given binary ID (generated via
* `.nextBinary()`).
*/
format(buf: Uint8Array): string;

/**
* Takes a KSUID string (assumed to be generated with the same config as
* this instance) and parses it into an object of: `{ epoch, id }`, where
* `epoch` is the Unix epoch of the ID and `id` the random bytes.
*
* @remarks
* This operation requires `bigint` support by the host environment.
*
* @param id
*/
parse(id: string): { epoch: number; id: Uint8Array };
}

export interface KSUIDOpts {
/**
* {@link @this.ng/base-n#BaseN} instance for string encoding the generated
Expand Down
5 changes: 3 additions & 2 deletions packages/ksuid/src/ksuid32.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { AKSUID } from "./aksuid";
import type { KSUIDOpts } from "./api";

export class KSUID extends AKSUID {
export class KSUID32 extends AKSUID {
constructor(opts?: Partial<KSUIDOpts>) {
super(4, {
epoch: 1_600_000_000,
Expand Down Expand Up @@ -33,4 +33,5 @@ export class KSUID extends AKSUID {
*
* @param opts
*/
export const defKSUID = (opts?: Partial<KSUIDOpts>): KSUID => new KSUID(opts);
export const defKSUID32 = (opts?: Partial<KSUIDOpts>): KSUID32 =>
new KSUID32(opts);
16 changes: 8 additions & 8 deletions packages/ksuid/test/index.ts
Original file line number Diff line number Diff line change
@@ -1,26 +1,26 @@
import { XsAdd } from "@thi.ng/random";
import * as assert from "assert";
import { AKSUID, defKSUID, defKSUID64 } from "../src";
import { defKSUID32, defKSUID64, IKSUID } from "../src";

describe("ksuid", () => {
const check = (id: AKSUID, eps: number, buf: Uint8Array) => {
const check = (id: IKSUID, eps: number, buf: Uint8Array) => {
const t = Date.now();
const a = id.timeOnly(t);
assert.strictEqual(a.length, 27);
let res = id.parse(a);
assert(Math.abs(res.epoch - t) < eps * 2);
assert.deepStrictEqual(res.id, new Uint8Array(20 - id.epochSize));
assert(Math.abs(res.epoch - t) < eps);
assert.deepStrictEqual(res.id, new Uint8Array(id.size - id.epochSize));
const b = id.nextBinary();
assert.deepStrictEqual(b.slice(id.epochSize), buf);
res = id.parse(id.format(b));
assert(Math.abs(res.epoch - t) < eps * 2);
assert(Math.abs(res.epoch - t) < eps);
assert.deepStrictEqual(res.id, buf);
};

it("ksuid32", () => {
check(
defKSUID({ rnd: new XsAdd(0xdecafbad) }),
1000,
defKSUID32({ rnd: new XsAdd(0xdecafbad) }),
1000 * 2,
new Uint8Array([
170, 213, 122, 63, 189, 122, 161, 143, 91, 187, 80, 231, 61, 17,
112, 238,
Expand All @@ -31,7 +31,7 @@ describe("ksuid", () => {
it("ksuid64", () => {
check(
defKSUID64({ rnd: new XsAdd(0xdecafbad) }),
1,
1 * 2,
new Uint8Array([
189, 122, 161, 143, 91, 187, 80, 231, 61, 17, 112, 238,
])
Expand Down
20 changes: 10 additions & 10 deletions packages/ksuid/tpl.readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,21 +16,21 @@ ${pkg.description}
Idea based on [segmentio/ksuid](https://github.com/segmentio/ksuid), though with
added flexibility in terms of configuration & implementation:

- Configurable bit size (default: 128bits)
- Configurable bit size (default: 160 bits)
- Base-N encoding scheme (default: base62, see
[@thi.ng/base-n](https://github.com/thi-ng/umbrella/tree/develop/packages/base-n)
for alternatives)
- Timestamp resolution (seconds [32 bits], milliseconds [64 bits])
- Epoch start time offset
- Time-only base ID generation (optional)
- KSUID parsing / decomposition
- Configurable RNG source (default: `window.crypto` or `Math.random`)
- Configurable RNG source (default: `window.crypto`, `Math.random` fallback)

KSUIDs generated w/ this package consist of the lower 32bits or 64bits of an
Unix epoch (potentially time shifted to free up bits for future timestamps) and
N additional bits of a random payload (from a configurable source). IDs can be
generated as byte arrays or base-N encoded strings. For the latter, the JS
runtime MUST support
KSUIDs generated w/ this package are composed from a 32 bit or 64 bit Unix epoch
(by default time shifted to free up bits for future timestamps) and N additional
bits of a random payload (from a configurable source). IDs can be generated as
byte arrays or base-N encoded strings. For the latter, the JS runtime MUST
support
[`BigInt`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/BigInt).

![KSUID bit layout diagram](https://raw.githubusercontent.com/thi-ng/umbrella/develop/assets/ksuid/ksuid.png)
Expand Down Expand Up @@ -60,10 +60,10 @@ ${examples}
${docLink}

```ts
import { defKSUID } from "@thi.ng/ksuid";
import { defKSUID32, defKSUID64 } from "@thi.ng/ksuid";

// init 32bit epoch (resolution: seconds) w/ defaults
const id = defKSUID();
const id = defKSUID32();
// init 64bit epoch (resolution: milliseconds), same API
const id = defKSUID64();

Expand Down Expand Up @@ -102,7 +102,7 @@ Creating custom IDs:
import { BASE36 } from "@thi.ng/base-n";

// no time shift, 64bit random
const id36 = defKSUID({ base: BASE36, epoch: 0, bytes: 8 });
const id36 = defKSUID32({ base: BASE36, epoch: 0, bytes: 8 });
// '2VOUKH4K59AG0RXR4XH'
```

Expand Down

0 comments on commit 1276c94

Please sign in to comment.