@ewanc26/tid 1.0.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 ADDED
@@ -0,0 +1,106 @@
1
+ # @malachite/tid
2
+
3
+ Zero-dependency [AT Protocol](https://atproto.com/) TID (Timestamp Identifier) generation for Node.js and browsers.
4
+
5
+ TIDs are the record keys used throughout ATProto / Bluesky — 13-character, lexicographically sortable, monotonic identifiers derived from a microsecond timestamp and a random clock ID.
6
+
7
+ ## Why this package?
8
+
9
+ Other TID implementations require native bindings (`node-gyp`, Python) or pull in large dependency trees. This package is **pure JavaScript** with **no runtime dependencies**, and runs anywhere the [Web Crypto API](https://developer.mozilla.org/en-US/docs/Web/API/Web_Crypto_API) is available — Node.js 20+, Deno, Bun, and all modern browsers.
10
+
11
+ ## Install
12
+
13
+ ```sh
14
+ npm install @ewanc26/tid
15
+ # or
16
+ pnpm add @ewanc26/tid
17
+ ```
18
+
19
+ ## Usage
20
+
21
+ ### Generate a TID for a historical timestamp
22
+
23
+ Pass an ISO 8601 string or a `Date` object. The clock is monotonic — if records arrive out of order, the timestamp is bumped forward so every call produces a strictly increasing TID within the same JS context.
24
+
25
+ ```ts
26
+ import { generateTID } from '@malachite/tid';
27
+
28
+ // From an ISO string (e.g. a Last.fm scrobble timestamp)
29
+ const tid = generateTID('2023-11-01T12:00:00Z');
30
+
31
+ // From a Date object
32
+ const tid2 = generateTID(new Date('2024-03-15T09:30:00Z'));
33
+ ```
34
+
35
+ ### Generate a TID for right now
36
+
37
+ ```ts
38
+ import { generateNextTID } from '@malachite/tid';
39
+
40
+ const tid = generateNextTID();
41
+ ```
42
+
43
+ ### Validate a TID
44
+
45
+ ```ts
46
+ import { validateTid } from '@malachite/tid';
47
+
48
+ validateTid('3jzfcijpj2z2a'); // true
49
+ validateTid('not-a-tid'); // false
50
+ ```
51
+
52
+ ### Decode a TID
53
+
54
+ ```ts
55
+ import { decodeTid } from '@malachite/tid';
56
+
57
+ const { timestampUs, clockId, date } = decodeTid('3jzfcijpj2z2a');
58
+ // timestampUs — microseconds since Unix epoch
59
+ // clockId — random 0–31 clock identifier
60
+ // date — equivalent JavaScript Date (millisecond precision)
61
+ ```
62
+
63
+ ### Compare / sort TIDs
64
+
65
+ Because the AT Protocol base-32 alphabet is ordered by timestamp, lexicographic string comparison is correct. `compareTids` returns `-1 | 0 | 1` for use as a `sort` comparator.
66
+
67
+ ```ts
68
+ import { compareTids } from '@malachite/tid';
69
+
70
+ const tids = ['3jzfcijpj2z2a', '3jzfabc000022', '3jzfzzzzzzz2a'];
71
+ tids.sort(compareTids);
72
+ // → ['3jzfabc000022', '3jzfcijpj2z2a', '3jzfzzzzzzz2a'] (chronological)
73
+ ```
74
+
75
+ ## API
76
+
77
+ | Export | Signature | Description |
78
+ |--------|-----------|-------------|
79
+ | `generateTID` | `(source: string \| Date) => string` | Generate a TID for a historical timestamp |
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**) |
85
+
86
+ ### `DecodedTid`
87
+
88
+ ```ts
89
+ interface DecodedTid {
90
+ timestampUs: number; // microseconds since Unix epoch
91
+ clockId: number; // 0–31
92
+ date: Date; // millisecond-precision equivalent
93
+ }
94
+ ```
95
+
96
+ ## Spec notes
97
+
98
+ - TIDs are 13 characters in the AT Protocol base-32 alphabet (`234567abcdefghijklmnopqrstuvwxyz`).
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>.
103
+
104
+ ## License
105
+
106
+ AGPL-3.0-only — same as [Malachite](https://github.com/ewanc26/malachite).
package/dist/index.cjs ADDED
@@ -0,0 +1,96 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/index.ts
21
+ var index_exports = {};
22
+ __export(index_exports, {
23
+ compareTids: () => compareTids,
24
+ decodeTid: () => decodeTid,
25
+ generateNextTID: () => generateNextTID,
26
+ generateTID: () => generateTID,
27
+ resetTidClock: () => resetTidClock,
28
+ validateTid: () => validateTid
29
+ });
30
+ module.exports = __toCommonJS(index_exports);
31
+ var S32 = "234567abcdefghijklmnopqrstuvwxyz";
32
+ var S32_MAP = {};
33
+ for (let i = 0; i < S32.length; i++) S32_MAP[S32[i]] = i;
34
+ function s32encode(n) {
35
+ if (n === 0) return "2";
36
+ let s = "";
37
+ let v = n;
38
+ while (v > 0) {
39
+ s = S32[v % 32] + s;
40
+ v = Math.floor(v / 32);
41
+ }
42
+ return s;
43
+ }
44
+ function s32decode(s) {
45
+ let n = 0;
46
+ for (const ch of s) n = n * 32 + (S32_MAP[ch] ?? 0);
47
+ return n;
48
+ }
49
+ var TID_RE = /^[234567abcdefghij][234567abcdefghijklmnopqrstuvwxyz]{12}$/;
50
+ function validateTid(tid) {
51
+ return TID_RE.test(tid);
52
+ }
53
+ function decodeTid(tid) {
54
+ if (!validateTid(tid)) throw new TypeError(`Invalid TID: "${tid}"`);
55
+ const timestampUs = s32decode(tid.slice(0, 11));
56
+ const clockId = s32decode(tid.slice(11));
57
+ return { timestampUs, clockId, date: new Date(Math.floor(timestampUs / 1e3)) };
58
+ }
59
+ var lastUs = 0;
60
+ var CLOCK_ID = (() => {
61
+ const buf = new Uint8Array(1);
62
+ (globalThis.crypto ?? globalThis.webcrypto).getRandomValues(buf);
63
+ return buf[0] % 32;
64
+ })();
65
+ function nextUs(targetUs) {
66
+ const us = targetUs <= lastUs ? lastUs + 1 : targetUs;
67
+ lastUs = us;
68
+ return us;
69
+ }
70
+ function makeTid(us) {
71
+ return s32encode(us).padStart(11, "2") + s32encode(CLOCK_ID).padStart(2, "2");
72
+ }
73
+ function generateTID(source) {
74
+ const ms = typeof source === "string" ? new Date(source).getTime() : source.getTime();
75
+ return makeTid(nextUs(ms * 1e3));
76
+ }
77
+ function generateNextTID() {
78
+ return makeTid(nextUs(Date.now() * 1e3));
79
+ }
80
+ function compareTids(a, b) {
81
+ if (a < b) return -1;
82
+ if (a > b) return 1;
83
+ return 0;
84
+ }
85
+ function resetTidClock() {
86
+ lastUs = 0;
87
+ }
88
+ // Annotate the CommonJS export names for ESM import in node:
89
+ 0 && (module.exports = {
90
+ compareTids,
91
+ decodeTid,
92
+ generateNextTID,
93
+ generateTID,
94
+ resetTidClock,
95
+ validateTid
96
+ });
@@ -0,0 +1,62 @@
1
+ /**
2
+ * @malachite/tid — AT Protocol TID generation
3
+ *
4
+ * Zero-dependency, spec-compliant TID (Timestamp Identifier) generation for
5
+ * the AT Protocol. Works in Node.js 20+ and all modern browsers.
6
+ *
7
+ * Spec: https://atproto.com/specs/tid
8
+ */
9
+ /**
10
+ * Returns `true` if `tid` is a well-formed AT Protocol TID.
11
+ */
12
+ declare function validateTid(tid: string): boolean;
13
+ interface DecodedTid {
14
+ /** Microseconds since the Unix epoch. */
15
+ timestampUs: number;
16
+ /** Clock identifier (0–31). */
17
+ clockId: number;
18
+ /** Convenience: the timestamp as a `Date` (millisecond precision). */
19
+ date: Date;
20
+ }
21
+ /**
22
+ * Decode a TID into its constituent parts.
23
+ * Throws a `TypeError` if the TID is malformed.
24
+ */
25
+ declare function decodeTid(tid: string): DecodedTid;
26
+ /**
27
+ * Generate a TID for a historical timestamp.
28
+ *
29
+ * Accepts either an ISO 8601 string or a `Date` object. Monotonicity is
30
+ * guaranteed — if records arrive out of order the clock is bumped forward so
31
+ * every call produces a strictly increasing TID within the same JS context.
32
+ *
33
+ * @example
34
+ * const tid = generateTID('2023-11-01T12:00:00Z');
35
+ * const tid = generateTID(new Date());
36
+ */
37
+ declare function generateTID(source: string | Date): string;
38
+ /**
39
+ * Generate a TID for the current wall-clock time.
40
+ *
41
+ * @example
42
+ * const tid = generateNextTID();
43
+ */
44
+ declare function generateNextTID(): string;
45
+ /**
46
+ * Compare two TIDs lexicographically.
47
+ *
48
+ * Because the AT Protocol base-32 alphabet is lexicographically ordered by
49
+ * timestamp, string comparison is sufficient and correct.
50
+ *
51
+ * Returns `-1`, `0`, or `1` (suitable as a `Array.prototype.sort` comparator).
52
+ */
53
+ declare function compareTids(a: string, b: string): -1 | 0 | 1;
54
+ /**
55
+ * Reset the module-level monotonic clock to zero.
56
+ *
57
+ * **Only use this in tests.** Calling it in production risks generating
58
+ * duplicate or non-monotonic TIDs.
59
+ */
60
+ declare function resetTidClock(): void;
61
+
62
+ export { type DecodedTid, compareTids, decodeTid, generateNextTID, generateTID, resetTidClock, validateTid };
@@ -0,0 +1,62 @@
1
+ /**
2
+ * @malachite/tid — AT Protocol TID generation
3
+ *
4
+ * Zero-dependency, spec-compliant TID (Timestamp Identifier) generation for
5
+ * the AT Protocol. Works in Node.js 20+ and all modern browsers.
6
+ *
7
+ * Spec: https://atproto.com/specs/tid
8
+ */
9
+ /**
10
+ * Returns `true` if `tid` is a well-formed AT Protocol TID.
11
+ */
12
+ declare function validateTid(tid: string): boolean;
13
+ interface DecodedTid {
14
+ /** Microseconds since the Unix epoch. */
15
+ timestampUs: number;
16
+ /** Clock identifier (0–31). */
17
+ clockId: number;
18
+ /** Convenience: the timestamp as a `Date` (millisecond precision). */
19
+ date: Date;
20
+ }
21
+ /**
22
+ * Decode a TID into its constituent parts.
23
+ * Throws a `TypeError` if the TID is malformed.
24
+ */
25
+ declare function decodeTid(tid: string): DecodedTid;
26
+ /**
27
+ * Generate a TID for a historical timestamp.
28
+ *
29
+ * Accepts either an ISO 8601 string or a `Date` object. Monotonicity is
30
+ * guaranteed — if records arrive out of order the clock is bumped forward so
31
+ * every call produces a strictly increasing TID within the same JS context.
32
+ *
33
+ * @example
34
+ * const tid = generateTID('2023-11-01T12:00:00Z');
35
+ * const tid = generateTID(new Date());
36
+ */
37
+ declare function generateTID(source: string | Date): string;
38
+ /**
39
+ * Generate a TID for the current wall-clock time.
40
+ *
41
+ * @example
42
+ * const tid = generateNextTID();
43
+ */
44
+ declare function generateNextTID(): string;
45
+ /**
46
+ * Compare two TIDs lexicographically.
47
+ *
48
+ * Because the AT Protocol base-32 alphabet is lexicographically ordered by
49
+ * timestamp, string comparison is sufficient and correct.
50
+ *
51
+ * Returns `-1`, `0`, or `1` (suitable as a `Array.prototype.sort` comparator).
52
+ */
53
+ declare function compareTids(a: string, b: string): -1 | 0 | 1;
54
+ /**
55
+ * Reset the module-level monotonic clock to zero.
56
+ *
57
+ * **Only use this in tests.** Calling it in production risks generating
58
+ * duplicate or non-monotonic TIDs.
59
+ */
60
+ declare function resetTidClock(): void;
61
+
62
+ export { type DecodedTid, compareTids, decodeTid, generateNextTID, generateTID, resetTidClock, validateTid };
package/dist/index.js ADDED
@@ -0,0 +1,66 @@
1
+ // src/index.ts
2
+ var S32 = "234567abcdefghijklmnopqrstuvwxyz";
3
+ var S32_MAP = {};
4
+ for (let i = 0; i < S32.length; i++) S32_MAP[S32[i]] = i;
5
+ function s32encode(n) {
6
+ if (n === 0) return "2";
7
+ let s = "";
8
+ let v = n;
9
+ while (v > 0) {
10
+ s = S32[v % 32] + s;
11
+ v = Math.floor(v / 32);
12
+ }
13
+ return s;
14
+ }
15
+ function s32decode(s) {
16
+ let n = 0;
17
+ for (const ch of s) n = n * 32 + (S32_MAP[ch] ?? 0);
18
+ return n;
19
+ }
20
+ var TID_RE = /^[234567abcdefghij][234567abcdefghijklmnopqrstuvwxyz]{12}$/;
21
+ function validateTid(tid) {
22
+ return TID_RE.test(tid);
23
+ }
24
+ function decodeTid(tid) {
25
+ if (!validateTid(tid)) throw new TypeError(`Invalid TID: "${tid}"`);
26
+ const timestampUs = s32decode(tid.slice(0, 11));
27
+ const clockId = s32decode(tid.slice(11));
28
+ return { timestampUs, clockId, date: new Date(Math.floor(timestampUs / 1e3)) };
29
+ }
30
+ var lastUs = 0;
31
+ var CLOCK_ID = (() => {
32
+ const buf = new Uint8Array(1);
33
+ (globalThis.crypto ?? globalThis.webcrypto).getRandomValues(buf);
34
+ return buf[0] % 32;
35
+ })();
36
+ function nextUs(targetUs) {
37
+ const us = targetUs <= lastUs ? lastUs + 1 : targetUs;
38
+ lastUs = us;
39
+ return us;
40
+ }
41
+ function makeTid(us) {
42
+ return s32encode(us).padStart(11, "2") + s32encode(CLOCK_ID).padStart(2, "2");
43
+ }
44
+ function generateTID(source) {
45
+ const ms = typeof source === "string" ? new Date(source).getTime() : source.getTime();
46
+ return makeTid(nextUs(ms * 1e3));
47
+ }
48
+ function generateNextTID() {
49
+ return makeTid(nextUs(Date.now() * 1e3));
50
+ }
51
+ function compareTids(a, b) {
52
+ if (a < b) return -1;
53
+ if (a > b) return 1;
54
+ return 0;
55
+ }
56
+ function resetTidClock() {
57
+ lastUs = 0;
58
+ }
59
+ export {
60
+ compareTids,
61
+ decodeTid,
62
+ generateNextTID,
63
+ generateTID,
64
+ resetTidClock,
65
+ validateTid
66
+ };
package/package.json ADDED
@@ -0,0 +1,37 @@
1
+ {
2
+ "name": "@ewanc26/tid",
3
+ "version": "1.0.0",
4
+ "description": "Zero-dependency AT Protocol TID generation for Node.js and browsers",
5
+ "type": "module",
6
+ "exports": {
7
+ ".": {
8
+ "types": "./dist/index.d.ts",
9
+ "import": "./dist/index.js",
10
+ "require": "./dist/index.cjs"
11
+ }
12
+ },
13
+ "main": "./dist/index.cjs",
14
+ "module": "./dist/index.js",
15
+ "types": "./dist/index.d.ts",
16
+ "files": ["dist", "README.md", "LICENCE"],
17
+ "scripts": {
18
+ "build": "tsup src/index.ts --format esm,cjs --dts --clean",
19
+ "dev": "tsup src/index.ts --format esm,cjs --dts --watch",
20
+ "type-check": "tsc --noEmit",
21
+ "test": "node --test dist/index.test.js"
22
+ },
23
+ "keywords": [
24
+ "atproto",
25
+ "bluesky",
26
+ "tid",
27
+ "timestamp",
28
+ "identifier",
29
+ "lexicon"
30
+ ],
31
+ "author": "",
32
+ "license": "AGPL-3.0-only",
33
+ "devDependencies": {
34
+ "tsup": "^8.5.0",
35
+ "typescript": "^5.9.3"
36
+ }
37
+ }