@ewanc26/tid 1.0.2 → 1.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/README.md +96 -61
- package/dist/index.cjs +53 -4
- package/dist/index.d.cts +62 -2
- package/dist/index.d.ts +62 -2
- package/dist/index.js +46 -4
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -2,105 +2,140 @@
|
|
|
2
2
|
|
|
3
3
|
Zero-dependency [AT Protocol](https://atproto.com/) TID (Timestamp Identifier) generation for Node.js and browsers.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
This package is **written in TypeScript** and compiled to **plain JavaScript**, so it ships with type definitions for TypeScript users and runs anywhere the Web Crypto API is available — Node.js 20+, Deno, Bun, and modern browsers.
|
|
6
|
+
|
|
7
|
+
TIDs are 13-character, lexicographically sortable record keys used across the AT Protocol and Bluesky. They’re monotonic identifiers derived from a microsecond timestamp and a 5-bit clock ID. When multiple TIDs would otherwise share the same microsecond, this package avoids collisions by nudging the clock ID (initialised per JS context) so each generated TID stays unique and strictly increasing within that runtime.
|
|
8
|
+
|
|
9
|
+
---
|
|
6
10
|
|
|
7
11
|
## Why this package?
|
|
8
12
|
|
|
9
|
-
Other TID implementations require native bindings (`node-gyp
|
|
13
|
+
Other TID implementations either require native bindings (e.g. `node-gyp`) or pull in large dependency trees. This package is **pure JavaScript**, has **no runtime dependencies**, ships with `.d.ts` typings, and is intentionally tiny — ideal for libraries, servers and client code where bundle size and portability matter.
|
|
14
|
+
|
|
15
|
+
---
|
|
10
16
|
|
|
11
17
|
## Install
|
|
12
18
|
|
|
13
|
-
```
|
|
19
|
+
```bash
|
|
14
20
|
npm install @ewanc26/tid
|
|
15
21
|
# or
|
|
16
22
|
pnpm add @ewanc26/tid
|
|
17
23
|
```
|
|
18
24
|
|
|
19
|
-
|
|
25
|
+
---
|
|
20
26
|
|
|
21
|
-
|
|
27
|
+
## Usage
|
|
22
28
|
|
|
23
|
-
|
|
29
|
+
### TypeScript (recommended)
|
|
24
30
|
|
|
25
31
|
```ts
|
|
26
|
-
import {
|
|
32
|
+
import {
|
|
33
|
+
generateTID,
|
|
34
|
+
generateNextTID,
|
|
35
|
+
validateTid,
|
|
36
|
+
decodeTid,
|
|
37
|
+
compareTids,
|
|
38
|
+
} from '@ewanc26/tid';
|
|
39
|
+
|
|
40
|
+
// From ISO string or Date
|
|
41
|
+
const tid: string = generateTID('2023-11-01T12:00:00Z');
|
|
42
|
+
const tid2: string = generateTID(new Date('2024-03-15T09:30:00Z'));
|
|
43
|
+
|
|
44
|
+
// Now
|
|
45
|
+
const currentTid: string = generateNextTID();
|
|
46
|
+
|
|
47
|
+
// Validate
|
|
48
|
+
const ok: boolean = validateTid('3jzfcijpj2z2a');
|
|
49
|
+
|
|
50
|
+
// Decode
|
|
51
|
+
const decoded = decodeTid('3jzfcijpj2z2a');
|
|
52
|
+
console.log(decoded.timestampUs, decoded.clockId, decoded.date);
|
|
53
|
+
|
|
54
|
+
// Sort
|
|
55
|
+
const tids: string[] = ['3jzfcijpj2z2a', '3jzfabc000022', '3jzfzzzzzzz2a'];
|
|
56
|
+
tids.sort(compareTids);
|
|
57
|
+
```
|
|
27
58
|
|
|
28
|
-
|
|
29
|
-
const tid = generateTID('2023-11-01T12:00:00Z');
|
|
59
|
+
### JavaScript (ESM)
|
|
30
60
|
|
|
31
|
-
|
|
32
|
-
|
|
61
|
+
```js
|
|
62
|
+
import {
|
|
63
|
+
generateTID,
|
|
64
|
+
generateNextTID,
|
|
65
|
+
validateTid,
|
|
66
|
+
decodeTid,
|
|
67
|
+
compareTids,
|
|
68
|
+
} from '@ewanc26/tid';
|
|
69
|
+
|
|
70
|
+
const tid = generateTID('2023-11-01T12:00:00Z');
|
|
71
|
+
const currentTid = generateNextTID();
|
|
72
|
+
console.log(validateTid(tid));
|
|
33
73
|
```
|
|
34
74
|
|
|
35
|
-
###
|
|
75
|
+
### JavaScript (CommonJS)
|
|
36
76
|
|
|
37
|
-
```
|
|
38
|
-
|
|
77
|
+
```js
|
|
78
|
+
const {
|
|
79
|
+
generateTID,
|
|
80
|
+
generateNextTID,
|
|
81
|
+
validateTid,
|
|
82
|
+
decodeTid,
|
|
83
|
+
compareTids,
|
|
84
|
+
} = require('@ewanc26/tid');
|
|
39
85
|
|
|
40
|
-
const tid =
|
|
86
|
+
const tid = generateTID('2023-11-01T12:00:00Z');
|
|
41
87
|
```
|
|
42
88
|
|
|
43
|
-
|
|
89
|
+
---
|
|
44
90
|
|
|
45
|
-
|
|
46
|
-
import { validateTid } from '@ewanc26/tid';
|
|
91
|
+
## API
|
|
47
92
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
93
|
+
| Export | Signature | Description | | |
|
|
94
|
+
| ----------------- | ----------------------------- | ------------------------------------------------- | ----------------------------------------- | ------------------------------------ |
|
|
95
|
+
| `generateTID` | `(source: string | Date) => string` | Generate a TID for a historical timestamp | |
|
|
96
|
+
| `generateNextTID` | `() => string` | Generate a TID for the current wall-clock time | | |
|
|
97
|
+
| `validateTid` | `(tid: string) => boolean` | Returns `true` if the string is a well-formed TID | | |
|
|
98
|
+
| `decodeTid` | `(tid: string) => DecodedTid` | Decode a TID into timestamp, clockId, and Date | | |
|
|
99
|
+
| `compareTids` | `(a: string, b: string) => -1 | 0 | 1` | Lexicographic comparator for sorting |
|
|
100
|
+
| `resetTidClock` | `() => void` | Reset the monotonic clock (**tests only**) | | |
|
|
51
101
|
|
|
52
|
-
###
|
|
102
|
+
### `DecodedTid`
|
|
53
103
|
|
|
54
104
|
```ts
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
//
|
|
59
|
-
|
|
60
|
-
// date — equivalent JavaScript Date (millisecond precision)
|
|
105
|
+
interface DecodedTid {
|
|
106
|
+
timestampUs: number; // microseconds since Unix epoch
|
|
107
|
+
clockId: number; // 0–31
|
|
108
|
+
date: Date; // millisecond-precision equivalent
|
|
109
|
+
}
|
|
61
110
|
```
|
|
62
111
|
|
|
63
|
-
|
|
112
|
+
---
|
|
64
113
|
|
|
65
|
-
|
|
114
|
+
## Spec notes
|
|
66
115
|
|
|
67
|
-
|
|
68
|
-
|
|
116
|
+
* TIDs are 13 characters in the AT Protocol base-32 alphabet: `234567abcdefghijklmnopqrstuvwxyz`.
|
|
117
|
+
* The first 11 characters encode a microsecond-precision Unix timestamp.
|
|
118
|
+
* The last 2 characters encode a 5-bit clock ID (0–31) which disambiguates TIDs generated on different machines or processes within the same microsecond.
|
|
119
|
+
* The clock ID is randomised once at module load time (per JS context). When multiple TIDs would collide at the same microsecond, the implementation adjusts (nudges) the clock ID so collisions are avoided while preserving lexicographic ordering and monotonicity within that runtime.
|
|
120
|
+
* Full specification: [https://atproto.com/specs/tid](https://atproto.com/specs/tid)
|
|
69
121
|
|
|
70
|
-
|
|
71
|
-
tids.sort(compareTids);
|
|
72
|
-
// → ['3jzfabc000022', '3jzfcijpj2z2a', '3jzfzzzzzzz2a'] (chronological)
|
|
73
|
-
```
|
|
122
|
+
---
|
|
74
123
|
|
|
75
|
-
##
|
|
124
|
+
## Behavioural notes
|
|
76
125
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
| `generateNextTID` | `() => string` | Generate a TID for the current wall-clock time |
|
|
81
|
-
| `validateTid` | `(tid: string) => boolean` | Returns `true` if the string is a well-formed TID |
|
|
82
|
-
| `decodeTid` | `(tid: string) => DecodedTid` | Decode a TID into timestamp, clockId, and Date |
|
|
83
|
-
| `compareTids` | `(a: string, b: string) => -1 \| 0 \| 1` | Lexicographic comparator for sorting |
|
|
84
|
-
| `resetTidClock` | `() => void` | Reset the monotonic clock (**tests only**) |
|
|
126
|
+
* Monotonicity is maintained per JS context. If records arrive out of chronological order in the same runtime, the package bumps the timestamp forward to ensure every generated TID is strictly increasing.
|
|
127
|
+
* Clock ID collisions across processes or machines are extremely unlikely due to the randomised 5-bit clock ID; the clock-nudging only applies inside a JS context to disambiguate simultaneous generations.
|
|
128
|
+
* The package does not attempt cross-process coordination — if you need globally unique sequencing beyond the TID spec, consider a server-side sequencer or combining TIDs with per-host identifiers.
|
|
85
129
|
|
|
86
|
-
|
|
130
|
+
---
|
|
87
131
|
|
|
88
|
-
|
|
89
|
-
interface DecodedTid {
|
|
90
|
-
timestampUs: number; // microseconds since Unix epoch
|
|
91
|
-
clockId: number; // 0–31
|
|
92
|
-
date: Date; // millisecond-precision equivalent
|
|
93
|
-
}
|
|
94
|
-
```
|
|
132
|
+
## Testing & development
|
|
95
133
|
|
|
96
|
-
|
|
134
|
+
* `resetTidClock()` is exported for tests to make deterministic TID generation possible.
|
|
135
|
+
* The library has zero runtime deps and uses the Web Crypto API for secure randomness. When running in older environments, provide a compatible Web Crypto polyfill if necessary.
|
|
97
136
|
|
|
98
|
-
|
|
99
|
-
- The first 11 characters encode a microsecond-precision Unix timestamp.
|
|
100
|
-
- The last 2 characters encode a random clock ID (0–31) that disambiguates TIDs generated on different machines or in different processes within the same microsecond.
|
|
101
|
-
- The clock ID is randomised once at module load time (per JS context).
|
|
102
|
-
- The full specification is at <https://atproto.com/specs/tid>.
|
|
137
|
+
---
|
|
103
138
|
|
|
104
|
-
##
|
|
139
|
+
## Licence
|
|
105
140
|
|
|
106
|
-
AGPL-3.0-only — same as [Malachite](https://github.com/ewanc26/malachite).
|
|
141
|
+
AGPL-3.0-only — same as [Malachite](https://github.com/ewanc26/malachite/tree/main).
|
package/dist/index.cjs
CHANGED
|
@@ -20,11 +20,18 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
20
20
|
// src/index.ts
|
|
21
21
|
var index_exports = {};
|
|
22
22
|
__export(index_exports, {
|
|
23
|
+
InvalidTidError: () => InvalidTidError,
|
|
24
|
+
areMonotonic: () => areMonotonic,
|
|
23
25
|
compareTids: () => compareTids,
|
|
24
26
|
decodeTid: () => decodeTid,
|
|
27
|
+
decodeTidClockId: () => decodeTidClockId,
|
|
28
|
+
decodeTidTimestamp: () => decodeTidTimestamp,
|
|
29
|
+
ensureValidTid: () => ensureValidTid,
|
|
25
30
|
generateNextTID: () => generateNextTID,
|
|
26
31
|
generateTID: () => generateTID,
|
|
32
|
+
getTidClockState: () => getTidClockState,
|
|
27
33
|
resetTidClock: () => resetTidClock,
|
|
34
|
+
seedTidClock: () => seedTidClock,
|
|
28
35
|
validateTid: () => validateTid
|
|
29
36
|
});
|
|
30
37
|
module.exports = __toCommonJS(index_exports);
|
|
@@ -50,25 +57,45 @@ var TID_RE = /^[234567abcdefghij][234567abcdefghijklmnopqrstuvwxyz]{12}$/;
|
|
|
50
57
|
function validateTid(tid) {
|
|
51
58
|
return TID_RE.test(tid);
|
|
52
59
|
}
|
|
60
|
+
var InvalidTidError = class extends Error {
|
|
61
|
+
constructor(message, tid) {
|
|
62
|
+
super(message);
|
|
63
|
+
this.tid = tid;
|
|
64
|
+
this.name = "InvalidTidError";
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
function ensureValidTid(tid) {
|
|
68
|
+
if (!validateTid(tid)) {
|
|
69
|
+
throw new InvalidTidError(`Invalid TID format: "${tid}"`, tid);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
53
72
|
function decodeTid(tid) {
|
|
54
73
|
if (!validateTid(tid)) throw new TypeError(`Invalid TID: "${tid}"`);
|
|
55
74
|
const timestampUs = s32decode(tid.slice(0, 11));
|
|
56
|
-
const
|
|
57
|
-
return { timestampUs, clockId, date: new Date(Math.floor(timestampUs / 1e3)) };
|
|
75
|
+
const decodedClockId = s32decode(tid.slice(11));
|
|
76
|
+
return { timestampUs, clockId: decodedClockId, date: new Date(Math.floor(timestampUs / 1e3)) };
|
|
77
|
+
}
|
|
78
|
+
function decodeTidTimestamp(tid) {
|
|
79
|
+
return decodeTid(tid).timestampUs;
|
|
80
|
+
}
|
|
81
|
+
function decodeTidClockId(tid) {
|
|
82
|
+
return decodeTid(tid).clockId;
|
|
58
83
|
}
|
|
59
84
|
var lastUs = 0;
|
|
60
|
-
var
|
|
85
|
+
var clockId = (() => {
|
|
61
86
|
const buf = new Uint8Array(1);
|
|
62
87
|
(globalThis.crypto ?? globalThis.webcrypto).getRandomValues(buf);
|
|
63
88
|
return buf[0] % 32;
|
|
64
89
|
})();
|
|
90
|
+
var generatedCount = 0;
|
|
65
91
|
function nextUs(targetUs) {
|
|
66
92
|
const us = targetUs <= lastUs ? lastUs + 1 : targetUs;
|
|
67
93
|
lastUs = us;
|
|
94
|
+
generatedCount++;
|
|
68
95
|
return us;
|
|
69
96
|
}
|
|
70
97
|
function makeTid(us) {
|
|
71
|
-
return s32encode(us).padStart(11, "2") + s32encode(
|
|
98
|
+
return s32encode(us).padStart(11, "2") + s32encode(clockId).padStart(2, "2");
|
|
72
99
|
}
|
|
73
100
|
function generateTID(source) {
|
|
74
101
|
const ms = typeof source === "string" ? new Date(source).getTime() : source.getTime();
|
|
@@ -82,15 +109,37 @@ function compareTids(a, b) {
|
|
|
82
109
|
if (a > b) return 1;
|
|
83
110
|
return 0;
|
|
84
111
|
}
|
|
112
|
+
function areMonotonic(tids) {
|
|
113
|
+
for (let i = 1; i < tids.length; i++) {
|
|
114
|
+
if (tids[i] <= tids[i - 1]) return false;
|
|
115
|
+
}
|
|
116
|
+
return true;
|
|
117
|
+
}
|
|
118
|
+
function getTidClockState() {
|
|
119
|
+
return { lastTimestampUs: lastUs, clockId, generatedCount };
|
|
120
|
+
}
|
|
85
121
|
function resetTidClock() {
|
|
86
122
|
lastUs = 0;
|
|
123
|
+
generatedCount = 0;
|
|
124
|
+
}
|
|
125
|
+
function seedTidClock(startUs = 0, newClockId = 0) {
|
|
126
|
+
lastUs = startUs;
|
|
127
|
+
clockId = newClockId % 32;
|
|
128
|
+
generatedCount = 0;
|
|
87
129
|
}
|
|
88
130
|
// Annotate the CommonJS export names for ESM import in node:
|
|
89
131
|
0 && (module.exports = {
|
|
132
|
+
InvalidTidError,
|
|
133
|
+
areMonotonic,
|
|
90
134
|
compareTids,
|
|
91
135
|
decodeTid,
|
|
136
|
+
decodeTidClockId,
|
|
137
|
+
decodeTidTimestamp,
|
|
138
|
+
ensureValidTid,
|
|
92
139
|
generateNextTID,
|
|
93
140
|
generateTID,
|
|
141
|
+
getTidClockState,
|
|
94
142
|
resetTidClock,
|
|
143
|
+
seedTidClock,
|
|
95
144
|
validateTid
|
|
96
145
|
});
|
package/dist/index.d.cts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @
|
|
2
|
+
* @ewanc26/tid — AT Protocol TID generation
|
|
3
3
|
*
|
|
4
4
|
* Zero-dependency, spec-compliant TID (Timestamp Identifier) generation for
|
|
5
5
|
* the AT Protocol. Works in Node.js 20+ and all modern browsers.
|
|
@@ -10,6 +10,18 @@
|
|
|
10
10
|
* Returns `true` if `tid` is a well-formed AT Protocol TID.
|
|
11
11
|
*/
|
|
12
12
|
declare function validateTid(tid: string): boolean;
|
|
13
|
+
/**
|
|
14
|
+
* Error thrown by `ensureValidTid` when a TID fails validation.
|
|
15
|
+
*/
|
|
16
|
+
declare class InvalidTidError extends Error {
|
|
17
|
+
readonly tid?: string | undefined;
|
|
18
|
+
constructor(message: string, tid?: string | undefined);
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Assert that `tid` is a valid AT Protocol TID.
|
|
22
|
+
* Throws `InvalidTidError` if the format check fails.
|
|
23
|
+
*/
|
|
24
|
+
declare function ensureValidTid(tid: string): asserts tid is string;
|
|
13
25
|
interface DecodedTid {
|
|
14
26
|
/** Microseconds since the Unix epoch. */
|
|
15
27
|
timestampUs: number;
|
|
@@ -23,6 +35,16 @@ interface DecodedTid {
|
|
|
23
35
|
* Throws a `TypeError` if the TID is malformed.
|
|
24
36
|
*/
|
|
25
37
|
declare function decodeTid(tid: string): DecodedTid;
|
|
38
|
+
/**
|
|
39
|
+
* Decode a TID and return only the microsecond timestamp.
|
|
40
|
+
* Prefer `decodeTid` for new code.
|
|
41
|
+
*/
|
|
42
|
+
declare function decodeTidTimestamp(tid: string): number;
|
|
43
|
+
/**
|
|
44
|
+
* Decode a TID and return only the clock identifier.
|
|
45
|
+
* Prefer `decodeTid` for new code.
|
|
46
|
+
*/
|
|
47
|
+
declare function decodeTidClockId(tid: string): number;
|
|
26
48
|
/**
|
|
27
49
|
* Generate a TID for a historical timestamp.
|
|
28
50
|
*
|
|
@@ -51,12 +73,50 @@ declare function generateNextTID(): string;
|
|
|
51
73
|
* Returns `-1`, `0`, or `1` (suitable as a `Array.prototype.sort` comparator).
|
|
52
74
|
*/
|
|
53
75
|
declare function compareTids(a: string, b: string): -1 | 0 | 1;
|
|
76
|
+
/**
|
|
77
|
+
* Returns `true` if every TID in the array is strictly greater than the one
|
|
78
|
+
* before it (lexicographic / chronological order).
|
|
79
|
+
*/
|
|
80
|
+
declare function areMonotonic(tids: string[]): boolean;
|
|
81
|
+
/**
|
|
82
|
+
* Snapshot of the module-level clock state.
|
|
83
|
+
* Intended for debugging and test assertions.
|
|
84
|
+
*/
|
|
85
|
+
interface TidClockState {
|
|
86
|
+
lastTimestampUs: number;
|
|
87
|
+
clockId: number;
|
|
88
|
+
generatedCount: number;
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Return a snapshot of the current clock state.
|
|
92
|
+
* Useful for asserting counts in tests.
|
|
93
|
+
*/
|
|
94
|
+
declare function getTidClockState(): TidClockState;
|
|
54
95
|
/**
|
|
55
96
|
* Reset the module-level monotonic clock to zero.
|
|
97
|
+
* Resets `lastTimestampUs` and `generatedCount` but preserves `clockId`.
|
|
56
98
|
*
|
|
57
99
|
* **Only use this in tests.** Calling it in production risks generating
|
|
58
100
|
* duplicate or non-monotonic TIDs.
|
|
59
101
|
*/
|
|
60
102
|
declare function resetTidClock(): void;
|
|
103
|
+
/**
|
|
104
|
+
* Seed the clock with a fixed starting timestamp and clock identifier,
|
|
105
|
+
* making all subsequent TID generation deterministic.
|
|
106
|
+
*
|
|
107
|
+
* - `startUs` becomes the floor for the next generated timestamp (µs).
|
|
108
|
+
* - `newClockId` overrides the random clock identifier (0–31, default 0).
|
|
109
|
+
* - `generatedCount` is reset to 0.
|
|
110
|
+
*
|
|
111
|
+
* **Only use this in tests.**
|
|
112
|
+
*
|
|
113
|
+
* @example
|
|
114
|
+
* seedTidClock(1_000_000_000_000_000, 0);
|
|
115
|
+
* const tid1 = generateNextTID();
|
|
116
|
+
* seedTidClock(1_000_000_000_000_000, 0);
|
|
117
|
+
* const tid2 = generateNextTID();
|
|
118
|
+
* // tid1 === tid2
|
|
119
|
+
*/
|
|
120
|
+
declare function seedTidClock(startUs?: number, newClockId?: number): void;
|
|
61
121
|
|
|
62
|
-
export { type DecodedTid, compareTids, decodeTid, generateNextTID, generateTID, resetTidClock, validateTid };
|
|
122
|
+
export { type DecodedTid, InvalidTidError, type TidClockState, areMonotonic, compareTids, decodeTid, decodeTidClockId, decodeTidTimestamp, ensureValidTid, generateNextTID, generateTID, getTidClockState, resetTidClock, seedTidClock, validateTid };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @
|
|
2
|
+
* @ewanc26/tid — AT Protocol TID generation
|
|
3
3
|
*
|
|
4
4
|
* Zero-dependency, spec-compliant TID (Timestamp Identifier) generation for
|
|
5
5
|
* the AT Protocol. Works in Node.js 20+ and all modern browsers.
|
|
@@ -10,6 +10,18 @@
|
|
|
10
10
|
* Returns `true` if `tid` is a well-formed AT Protocol TID.
|
|
11
11
|
*/
|
|
12
12
|
declare function validateTid(tid: string): boolean;
|
|
13
|
+
/**
|
|
14
|
+
* Error thrown by `ensureValidTid` when a TID fails validation.
|
|
15
|
+
*/
|
|
16
|
+
declare class InvalidTidError extends Error {
|
|
17
|
+
readonly tid?: string | undefined;
|
|
18
|
+
constructor(message: string, tid?: string | undefined);
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Assert that `tid` is a valid AT Protocol TID.
|
|
22
|
+
* Throws `InvalidTidError` if the format check fails.
|
|
23
|
+
*/
|
|
24
|
+
declare function ensureValidTid(tid: string): asserts tid is string;
|
|
13
25
|
interface DecodedTid {
|
|
14
26
|
/** Microseconds since the Unix epoch. */
|
|
15
27
|
timestampUs: number;
|
|
@@ -23,6 +35,16 @@ interface DecodedTid {
|
|
|
23
35
|
* Throws a `TypeError` if the TID is malformed.
|
|
24
36
|
*/
|
|
25
37
|
declare function decodeTid(tid: string): DecodedTid;
|
|
38
|
+
/**
|
|
39
|
+
* Decode a TID and return only the microsecond timestamp.
|
|
40
|
+
* Prefer `decodeTid` for new code.
|
|
41
|
+
*/
|
|
42
|
+
declare function decodeTidTimestamp(tid: string): number;
|
|
43
|
+
/**
|
|
44
|
+
* Decode a TID and return only the clock identifier.
|
|
45
|
+
* Prefer `decodeTid` for new code.
|
|
46
|
+
*/
|
|
47
|
+
declare function decodeTidClockId(tid: string): number;
|
|
26
48
|
/**
|
|
27
49
|
* Generate a TID for a historical timestamp.
|
|
28
50
|
*
|
|
@@ -51,12 +73,50 @@ declare function generateNextTID(): string;
|
|
|
51
73
|
* Returns `-1`, `0`, or `1` (suitable as a `Array.prototype.sort` comparator).
|
|
52
74
|
*/
|
|
53
75
|
declare function compareTids(a: string, b: string): -1 | 0 | 1;
|
|
76
|
+
/**
|
|
77
|
+
* Returns `true` if every TID in the array is strictly greater than the one
|
|
78
|
+
* before it (lexicographic / chronological order).
|
|
79
|
+
*/
|
|
80
|
+
declare function areMonotonic(tids: string[]): boolean;
|
|
81
|
+
/**
|
|
82
|
+
* Snapshot of the module-level clock state.
|
|
83
|
+
* Intended for debugging and test assertions.
|
|
84
|
+
*/
|
|
85
|
+
interface TidClockState {
|
|
86
|
+
lastTimestampUs: number;
|
|
87
|
+
clockId: number;
|
|
88
|
+
generatedCount: number;
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Return a snapshot of the current clock state.
|
|
92
|
+
* Useful for asserting counts in tests.
|
|
93
|
+
*/
|
|
94
|
+
declare function getTidClockState(): TidClockState;
|
|
54
95
|
/**
|
|
55
96
|
* Reset the module-level monotonic clock to zero.
|
|
97
|
+
* Resets `lastTimestampUs` and `generatedCount` but preserves `clockId`.
|
|
56
98
|
*
|
|
57
99
|
* **Only use this in tests.** Calling it in production risks generating
|
|
58
100
|
* duplicate or non-monotonic TIDs.
|
|
59
101
|
*/
|
|
60
102
|
declare function resetTidClock(): void;
|
|
103
|
+
/**
|
|
104
|
+
* Seed the clock with a fixed starting timestamp and clock identifier,
|
|
105
|
+
* making all subsequent TID generation deterministic.
|
|
106
|
+
*
|
|
107
|
+
* - `startUs` becomes the floor for the next generated timestamp (µs).
|
|
108
|
+
* - `newClockId` overrides the random clock identifier (0–31, default 0).
|
|
109
|
+
* - `generatedCount` is reset to 0.
|
|
110
|
+
*
|
|
111
|
+
* **Only use this in tests.**
|
|
112
|
+
*
|
|
113
|
+
* @example
|
|
114
|
+
* seedTidClock(1_000_000_000_000_000, 0);
|
|
115
|
+
* const tid1 = generateNextTID();
|
|
116
|
+
* seedTidClock(1_000_000_000_000_000, 0);
|
|
117
|
+
* const tid2 = generateNextTID();
|
|
118
|
+
* // tid1 === tid2
|
|
119
|
+
*/
|
|
120
|
+
declare function seedTidClock(startUs?: number, newClockId?: number): void;
|
|
61
121
|
|
|
62
|
-
export { type DecodedTid, compareTids, decodeTid, generateNextTID, generateTID, resetTidClock, validateTid };
|
|
122
|
+
export { type DecodedTid, InvalidTidError, type TidClockState, areMonotonic, compareTids, decodeTid, decodeTidClockId, decodeTidTimestamp, ensureValidTid, generateNextTID, generateTID, getTidClockState, resetTidClock, seedTidClock, validateTid };
|
package/dist/index.js
CHANGED
|
@@ -21,25 +21,45 @@ var TID_RE = /^[234567abcdefghij][234567abcdefghijklmnopqrstuvwxyz]{12}$/;
|
|
|
21
21
|
function validateTid(tid) {
|
|
22
22
|
return TID_RE.test(tid);
|
|
23
23
|
}
|
|
24
|
+
var InvalidTidError = class extends Error {
|
|
25
|
+
constructor(message, tid) {
|
|
26
|
+
super(message);
|
|
27
|
+
this.tid = tid;
|
|
28
|
+
this.name = "InvalidTidError";
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
function ensureValidTid(tid) {
|
|
32
|
+
if (!validateTid(tid)) {
|
|
33
|
+
throw new InvalidTidError(`Invalid TID format: "${tid}"`, tid);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
24
36
|
function decodeTid(tid) {
|
|
25
37
|
if (!validateTid(tid)) throw new TypeError(`Invalid TID: "${tid}"`);
|
|
26
38
|
const timestampUs = s32decode(tid.slice(0, 11));
|
|
27
|
-
const
|
|
28
|
-
return { timestampUs, clockId, date: new Date(Math.floor(timestampUs / 1e3)) };
|
|
39
|
+
const decodedClockId = s32decode(tid.slice(11));
|
|
40
|
+
return { timestampUs, clockId: decodedClockId, date: new Date(Math.floor(timestampUs / 1e3)) };
|
|
41
|
+
}
|
|
42
|
+
function decodeTidTimestamp(tid) {
|
|
43
|
+
return decodeTid(tid).timestampUs;
|
|
44
|
+
}
|
|
45
|
+
function decodeTidClockId(tid) {
|
|
46
|
+
return decodeTid(tid).clockId;
|
|
29
47
|
}
|
|
30
48
|
var lastUs = 0;
|
|
31
|
-
var
|
|
49
|
+
var clockId = (() => {
|
|
32
50
|
const buf = new Uint8Array(1);
|
|
33
51
|
(globalThis.crypto ?? globalThis.webcrypto).getRandomValues(buf);
|
|
34
52
|
return buf[0] % 32;
|
|
35
53
|
})();
|
|
54
|
+
var generatedCount = 0;
|
|
36
55
|
function nextUs(targetUs) {
|
|
37
56
|
const us = targetUs <= lastUs ? lastUs + 1 : targetUs;
|
|
38
57
|
lastUs = us;
|
|
58
|
+
generatedCount++;
|
|
39
59
|
return us;
|
|
40
60
|
}
|
|
41
61
|
function makeTid(us) {
|
|
42
|
-
return s32encode(us).padStart(11, "2") + s32encode(
|
|
62
|
+
return s32encode(us).padStart(11, "2") + s32encode(clockId).padStart(2, "2");
|
|
43
63
|
}
|
|
44
64
|
function generateTID(source) {
|
|
45
65
|
const ms = typeof source === "string" ? new Date(source).getTime() : source.getTime();
|
|
@@ -53,14 +73,36 @@ function compareTids(a, b) {
|
|
|
53
73
|
if (a > b) return 1;
|
|
54
74
|
return 0;
|
|
55
75
|
}
|
|
76
|
+
function areMonotonic(tids) {
|
|
77
|
+
for (let i = 1; i < tids.length; i++) {
|
|
78
|
+
if (tids[i] <= tids[i - 1]) return false;
|
|
79
|
+
}
|
|
80
|
+
return true;
|
|
81
|
+
}
|
|
82
|
+
function getTidClockState() {
|
|
83
|
+
return { lastTimestampUs: lastUs, clockId, generatedCount };
|
|
84
|
+
}
|
|
56
85
|
function resetTidClock() {
|
|
57
86
|
lastUs = 0;
|
|
87
|
+
generatedCount = 0;
|
|
88
|
+
}
|
|
89
|
+
function seedTidClock(startUs = 0, newClockId = 0) {
|
|
90
|
+
lastUs = startUs;
|
|
91
|
+
clockId = newClockId % 32;
|
|
92
|
+
generatedCount = 0;
|
|
58
93
|
}
|
|
59
94
|
export {
|
|
95
|
+
InvalidTidError,
|
|
96
|
+
areMonotonic,
|
|
60
97
|
compareTids,
|
|
61
98
|
decodeTid,
|
|
99
|
+
decodeTidClockId,
|
|
100
|
+
decodeTidTimestamp,
|
|
101
|
+
ensureValidTid,
|
|
62
102
|
generateNextTID,
|
|
63
103
|
generateTID,
|
|
104
|
+
getTidClockState,
|
|
64
105
|
resetTidClock,
|
|
106
|
+
seedTidClock,
|
|
65
107
|
validateTid
|
|
66
108
|
};
|