a collection of lightweight TypeScript packages for AT Protocol, the protocol powering Bluesky
atproto bluesky typescript npm

feat(tid): use high-precision system time in Node.js

mary.my.id edf1b0ad b01eb657

verified
+5
.changeset/beige-pens-like.md
··· 1 + --- 2 + '@atcute/tid': minor 3 + --- 4 + 5 + use high-precision system time in Node.js
+1 -1
.gitignore
··· 16 16 .DS_Store 17 17 18 18 .pkgsize 19 - .research 19 + .research
+2
packages/misc/time-ms/.gitignore
··· 1 + build/ 2 + prebuilds/
+7
packages/misc/time-ms/CHANGELOG.md
··· 1 + # @atcute/time-ms 2 + 3 + ## 1.0.0 4 + 5 + ### minor changes 6 + 7 + - initial release
+17
packages/misc/time-ms/README.md
··· 1 + # @atcute/time-ms 2 + 3 + high precision system time 4 + 5 + ```sh 6 + npm install @atcute/time-ms 7 + ``` 8 + 9 + ## usage 10 + 11 + ```ts 12 + import { now } from '@atcute/time-ms'; 13 + 14 + const timestamp = now(); 15 + // ^ 1766739339478426 16 + // returns microseconds since unix epoch 17 + ```
+18
packages/misc/time-ms/binding.gyp
··· 1 + { 2 + "targets": [ 3 + { 4 + "target_name": "time_ms", 5 + "sources": ["src/time_ms.c"], 6 + "cflags": ["-Wall", "-Wextra", "-O3"], 7 + "xcode_settings": { 8 + "OTHER_CFLAGS": ["-Wall", "-Wextra", "-O3"] 9 + }, 10 + "msvs_settings": { 11 + "VCCLCompilerTool": { 12 + "Optimization": 2, 13 + "WarnAsError": "false" 14 + } 15 + } 16 + } 17 + ] 18 + }
+31
packages/misc/time-ms/lib/index.node.ts
··· 1 + import { join } from 'node:path'; 2 + 3 + type TimeBinding = { 4 + now: () => number; 5 + }; 6 + 7 + let binding: TimeBinding | null = null; 8 + 9 + try { 10 + // node-gyp-build handles platform/arch detection, libc variants, etc. 11 + binding = require('node-gyp-build')(join(import.meta.dirname, '..')) as TimeBinding; 12 + } catch { 13 + binding = null; 14 + } 15 + 16 + /** 17 + * whether the native module is available for the current runtime. 18 + */ 19 + export const hasNative = binding !== null; 20 + 21 + /** 22 + * returns the current time in microseconds since unix epoch. 23 + * @returns timestamp in microseconds 24 + */ 25 + export const now = (): number => { 26 + if (binding === null) { 27 + return Date.now() * 1_000; 28 + } 29 + 30 + return binding.now(); 31 + };
+12
packages/misc/time-ms/lib/index.ts
··· 1 + /** 2 + * whether the native module is available for the current runtime. 3 + */ 4 + export const hasNative = false; 5 + 6 + /** 7 + * returns the current time in microseconds since unix epoch. 8 + * @returns timestamp in microseconds 9 + */ 10 + export const now = (): number => { 11 + return Date.now() * 1_000; 12 + };
+45
packages/misc/time-ms/package.json
··· 1 + { 2 + "type": "module", 3 + "name": "@atcute/time-ms", 4 + "version": "1.0.0", 5 + "description": "high precision system time helper", 6 + "license": "0BSD", 7 + "repository": { 8 + "url": "https://github.com/mary-ext/atcute", 9 + "directory": "packages/utilities/time-ms" 10 + }, 11 + "publishConfig": { 12 + "access": "public" 13 + }, 14 + "files": [ 15 + "dist/", 16 + "lib/", 17 + "prebuilds/", 18 + "src/", 19 + "binding.gyp", 20 + "!lib/**/*.bench.ts", 21 + "!lib/**/*.test.ts" 22 + ], 23 + "exports": { 24 + ".": { 25 + "node": "./dist/index.node.js", 26 + "default": "./dist/index.js" 27 + } 28 + }, 29 + "sideEffects": false, 30 + "scripts": { 31 + "install": "node-gyp-build", 32 + "build:native": "prebuildify --napi --strip", 33 + "build": "tsgo --project tsconfig.build.json", 34 + "test": "vitest", 35 + "prepublish": "rm -rf dist; pnpm run build:native; pnpm run build" 36 + }, 37 + "devDependencies": { 38 + "prebuildify": "^6.0.1", 39 + "vitest": "^4.0.16" 40 + }, 41 + "dependencies": { 42 + "@types/node": "^22.19.3", 43 + "node-gyp-build": "^4.8.4" 44 + } 45 + }
+38
packages/misc/time-ms/src/time_ms.c
··· 1 + #include <node_api.h> 2 + #include <stdint.h> 3 + 4 + #ifdef _WIN32 5 + #include <windows.h> 6 + #else 7 + #include <time.h> 8 + #endif 9 + 10 + static napi_value now(napi_env env, napi_callback_info info) { 11 + int64_t microseconds; 12 + 13 + #ifdef _WIN32 14 + FILETIME ft; 15 + ULARGE_INTEGER ul; 16 + GetSystemTimePreciseAsFileTime(&ft); 17 + ul.LowPart = ft.dwLowDateTime; 18 + ul.HighPart = ft.dwHighDateTime; 19 + // convert from 100-nanosecond intervals since 1601 to microseconds since 1970 20 + microseconds = (int64_t)((ul.QuadPart - 116444736000000000ULL) / 10); 21 + #else 22 + struct timespec ts; 23 + clock_gettime(CLOCK_REALTIME, &ts); 24 + microseconds = (int64_t)ts.tv_sec * 1000000 + ts.tv_nsec / 1000; 25 + #endif 26 + 27 + napi_value result; 28 + napi_create_int64(env, microseconds, &result); 29 + return result; 30 + } 31 + 32 + static napi_value init(napi_env env, napi_value exports) { 33 + napi_property_descriptor desc = { "now", NULL, now, NULL, NULL, NULL, napi_default, NULL }; 34 + napi_define_properties(env, exports, 1, &desc); 35 + return exports; 36 + } 37 + 38 + NAPI_MODULE(NODE_GYP_MODULE_NAME, init)
+4
packages/misc/time-ms/tsconfig.build.json
··· 1 + { 2 + "extends": "./tsconfig.json", 3 + "exclude": ["**/*.test.ts"] 4 + }
+24
packages/misc/time-ms/tsconfig.json
··· 1 + { 2 + "compilerOptions": { 3 + "types": ["node"], 4 + "outDir": "dist/", 5 + "esModuleInterop": true, 6 + "skipLibCheck": true, 7 + "target": "ESNext", 8 + "allowJs": true, 9 + "resolveJsonModule": true, 10 + "moduleDetection": "force", 11 + "isolatedModules": true, 12 + "verbatimModuleSyntax": true, 13 + "strict": true, 14 + "noImplicitOverride": true, 15 + "noUnusedLocals": true, 16 + "noUnusedParameters": true, 17 + "noFallthroughCasesInSwitch": true, 18 + "module": "NodeNext", 19 + "sourceMap": true, 20 + "declaration": true, 21 + "declarationMap": true, 22 + }, 23 + "include": ["lib"], 24 + }
+14 -9
packages/utilities/tid/lib/index.ts
··· 1 + import { now as getNow } from '@atcute/time-ms'; 1 2 import { s32decode, s32encode } from './s32.js'; 2 3 3 - let lastTimestamp: number = 0; 4 + let lastTimestamp = 0; 5 + let lastCurrentTime = 0; 4 6 5 7 const TID_RE = /^[234567abcdefghij][234567abcdefghijklmnopqrstuvwxyz]{12}$/; 6 8 ··· 30 32 * Return a TID based on current time 31 33 */ 32 34 export const now = (): string => { 33 - // we need these two aspects, which Date.now() doesn't provide: 34 - // - monotonically increasing time 35 - // - microsecond precision 35 + const currentTime = getNow(); 36 + let timestamp: number; 36 37 37 - // while `performance.timeOrigin + performance.now()` could be used here, they 38 - // seem to have cross-browser differences, not sure on that yet. 38 + if (currentTime === lastCurrentTime) { 39 + // same time; increment to avoid collision 40 + timestamp = lastTimestamp + 1; 41 + } else { 42 + // time changed 43 + timestamp = currentTime; 44 + lastCurrentTime = currentTime; 45 + } 39 46 40 - let timestamp = Math.max(Date.now() * 1_000, lastTimestamp); 41 - lastTimestamp = timestamp + 1; 42 - 47 + lastTimestamp = timestamp; 43 48 return createRaw(timestamp, Math.floor(Math.random() * 1023)); 44 49 }; 45 50
+3
packages/utilities/tid/package.json
··· 30 30 "test": "vitest", 31 31 "prepublish": "rm -rf dist; pnpm run build" 32 32 }, 33 + "dependencies": { 34 + "@atcute/time-ms": "workspace:^" 35 + }, 33 36 "devDependencies": { 34 37 "vitest": "^4.0.16" 35 38 }
+56 -4
pnpm-lock.yaml
··· 454 454 '@atcute/uint8array': 455 455 specifier: workspace:^ 456 456 version: link:../../misc/uint8array 457 + '@atcute/util-fetch': 458 + specifier: workspace:^ 459 + version: link:../../misc/util-fetch 457 460 '@badrap/valita': 458 461 specifier: ^0.4.6 459 462 version: 0.4.6 ··· 697 700 vitest: 698 701 specifier: ^4.0.16 699 702 version: 4.0.16(@types/node@25.0.3)(@vitest/browser-playwright@4.0.16)(jiti@2.6.1)(tsx@4.20.6)(yaml@2.8.0) 703 + 704 + packages/misc/time-ms: 705 + dependencies: 706 + '@types/node': 707 + specifier: ^22.19.3 708 + version: 22.19.3 709 + node-gyp-build: 710 + specifier: ^4.8.4 711 + version: 4.8.4 712 + devDependencies: 713 + prebuildify: 714 + specifier: ^6.0.1 715 + version: 6.0.1 716 + vitest: 717 + specifier: ^4.0.16 718 + version: 4.0.16(@types/node@22.19.3)(@vitest/browser-playwright@4.0.16)(jiti@2.6.1)(tsx@4.20.6)(yaml@2.8.0) 700 719 701 720 packages/misc/uint8array: 702 721 devDependencies: ··· 1100 1119 version: 4.0.16(@types/node@25.0.3)(@vitest/browser-playwright@4.0.16)(jiti@2.6.1)(tsx@4.20.6)(yaml@2.8.0) 1101 1120 1102 1121 packages/utilities/tid: 1122 + dependencies: 1123 + '@atcute/time-ms': 1124 + specifier: workspace:^ 1125 + version: link:../../misc/time-ms 1103 1126 devDependencies: 1104 1127 vitest: 1105 1128 specifier: ^4.0.16 ··· 3593 3616 resolution: {integrity: sha512-+P72GAjVAbTxjjwUmwjVrqrdZROD4nf8KgpBoDxqXXTiYZZt/ud60dE5yvCSr9lRO8e8yv6kgJIC0K0PfZFVQw==} 3594 3617 hasBin: true 3595 3618 3619 + node-gyp-build@4.8.4: 3620 + resolution: {integrity: sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==} 3621 + hasBin: true 3622 + 3596 3623 nodemailer-html-to-text@3.2.0: 3597 3624 resolution: {integrity: sha512-RJUC6640QV1PzTHHapOrc6IzrAJUZtk2BdVdINZ9VTLm+mcQNyBO9LYyhrnufkzqiD9l8hPLJ97rSyK4WanPNg==} 3598 3625 engines: {node: '>= 10.23.0'} ··· 3601 3628 resolution: {integrity: sha512-Z+iLaBGVaSjbIzQ4pX6XV41HrooLsQ10ZWPUehGmuantvzWoDVBnmsdUcOIDM1t+yPor5pDhVlDESgOMEGxhHA==} 3602 3629 engines: {node: '>=6.0.0'} 3603 3630 3631 + npm-run-path@3.1.0: 3632 + resolution: {integrity: sha512-Dbl4A/VfiVGLgQv29URL9xshU8XDY1GeLy+fsaZ1AA8JDSfjvr5P5+pzRbWqRSBxk6/DW7MIh8lTM/PaGnP2kg==} 3633 + engines: {node: '>=8'} 3634 + 3604 3635 object-assign@4.1.1: 3605 3636 resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} 3606 3637 engines: {node: '>=0.10.0'} ··· 3825 3856 prebuild-install@7.1.3: 3826 3857 resolution: {integrity: sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==} 3827 3858 engines: {node: '>=10'} 3859 + hasBin: true 3860 + 3861 + prebuildify@6.0.1: 3862 + resolution: {integrity: sha512-8Y2oOOateom/s8dNBsGIcnm6AxPmLH4/nanQzL5lQMU+sC0CMhzARZHizwr36pUPLdvBnOkCNQzxg4djuFSgIw==} 3828 3863 hasBin: true 3829 3864 3830 3865 prettier@2.8.8: ··· 6435 6470 6436 6471 '@types/bn.js@5.2.0': 6437 6472 dependencies: 6438 - '@types/node': 25.0.3 6473 + '@types/node': 22.19.3 6439 6474 6440 6475 '@types/bun@1.3.5': 6441 6476 dependencies: ··· 6467 6502 '@types/node@25.0.3': 6468 6503 dependencies: 6469 6504 undici-types: 7.16.0 6505 + optional: true 6470 6506 6471 6507 '@types/ws@8.18.1': 6472 6508 dependencies: 6473 - '@types/node': 25.0.3 6509 + '@types/node': 22.19.3 6474 6510 6475 6511 '@typescript/native-preview-darwin-arm64@7.0.0-dev.20251221.1': 6476 6512 optional: true ··· 6787 6823 6788 6824 bun-types@1.3.5: 6789 6825 dependencies: 6790 - '@types/node': 25.0.3 6826 + '@types/node': 22.19.3 6791 6827 6792 6828 bytes@3.1.2: {} 6793 6829 ··· 7562 7598 detect-libc: 2.1.2 7563 7599 optional: true 7564 7600 7601 + node-gyp-build@4.8.4: {} 7602 + 7565 7603 nodemailer-html-to-text@3.2.0: 7566 7604 dependencies: 7567 7605 html-to-text: 7.1.1 7568 7606 7569 7607 nodemailer@6.10.1: {} 7608 + 7609 + npm-run-path@3.1.0: 7610 + dependencies: 7611 + path-key: 3.1.1 7570 7612 7571 7613 object-assign@4.1.1: {} 7572 7614 ··· 7797 7839 tar-fs: 2.1.4 7798 7840 tunnel-agent: 0.6.0 7799 7841 7842 + prebuildify@6.0.1: 7843 + dependencies: 7844 + minimist: 1.2.8 7845 + mkdirp-classic: 0.5.3 7846 + node-abi: 3.85.0 7847 + npm-run-path: 3.1.0 7848 + pump: 3.0.3 7849 + tar-fs: 2.1.4 7850 + 7800 7851 prettier@2.8.8: {} 7801 7852 7802 7853 prettier@3.7.4: {} ··· 8213 8264 8214 8265 undici-types@6.21.0: {} 8215 8266 8216 - undici-types@7.16.0: {} 8267 + undici-types@7.16.0: 8268 + optional: true 8217 8269 8218 8270 undici@6.22.0: {} 8219 8271