diff --git a/.gitignore b/.gitignore index d844ad3f..b20c11d0 100644 --- a/.gitignore +++ b/.gitignore @@ -59,3 +59,6 @@ Thumbs.db # Local Proprietary Symlinks /packages/tempo/premium + +# Benchmarks +benchmark-results.json diff --git a/CHANGELOG.md b/CHANGELOG.md index 8c7b9bd7..f6737e42 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,27 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [3.0.0] - 2026-06-07 + +### Added +- **Tempo Registry Integration**: Moving towards deeper, centralized integration with the Tempo Registry infrastructure to streamline community and proprietary plugin distribution. +- **Modernized Duration Engine**: Introduced the chainable `.balance()` method to intelligently normalize temporal intervals (e.g., converting 365 days seamlessly into 1 year). +- **Duration Formatting**: Added a localized `.format()` method utilizing `Intl.DurationFormat` (with `Intl.NumberFormat` fallback support) for plural-aware, human-readable duration strings. Added `durationFormat` and `numberFormat` into `IntlOptions` to support global, instance-level, and method-level formatting overrides. +- **Enterprise Plugins Scaffolding**: Initial architectural scaffolding for upcoming enterprise `Cron` and `SLA` plugins, including a performant asynchronous `preloadHolidays` pattern for synchronous holiday-aware calculations. + +### Changed +- **Ticker Plugin Licensing**: The `Ticker` plugin has transitioned to a licensed model. It remains completely free to use, but now requires validation via the Tempo Registry ecosystem. +- **Robust License Validation**: Hardened runtime integrity for Tempo Pro plugins to enhance state security and prevent tampering. +- **Edition Boundaries**: Added proactive runtime warnings for unlicensed attempts, establishing strict boundaries between the Community and Proprietary editions. +- **Extension Resolution**: Stabilized module resolution for plugins by migrating to `defineExtension` and correcting internal export paths. +- **Logging Subsystem Decoupling**: Fully decoupled the internal parsing and error boundaries from the legacy `Logify` architecture, enabling zero-cost trace instrumentation. +- **Diagnostic Consistency**: Standardized fatal exceptions by introducing the `TempoError` class for internal invariant violations, and unified all console output through the centralized diagnostic bus (`logWarn`). +- **Documentation**: Clarified API behavior for the Duration engine (especially `.since()` return types) and relative time math. Improved visibility and navigation for the Tempo License Registry and installation guides. + +### Fixed +- **Module & Key Collisions**: Fixed registry authentication double-initialization bugs and terminal key registration collisions (e.g., the `[Tempo#extend] Term collision on key: "qtr"` error). +- **Ticker Plugin Stabilization**: Resolved lifecycle issues and nailed down the Ticker plugin architecture and API behavior. + ## [2.11.0] - 2026-05-25 ### Fixed diff --git a/package-lock.json b/package-lock.json index 56648354..ef001ee1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,30 +1,31 @@ { "name": "tempo-monorepo", - "version": "2.11.0", + "version": "3.0.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "tempo-monorepo", - "version": "2.11.0", + "version": "3.0.0", "workspaces": [ "packages/*" ], "devDependencies": { "@js-temporal/polyfill": "^0.5.1", "@rollup/plugin-node-resolve": "^16.0.3", - "@types/google.maps": "^3.64.1", + "@types/google.maps": "^3.65.0", "@types/hammerjs": "^2.0.46", "@types/jquery": "^4.0.0", - "@types/node": "^25.8.0", - "@vitest/ui": "^2.1.9", + "@types/node": "^25.9.1", + "@vitest/ui": "^4.1.7", "cross-env": "^10.1.0", "markdown-it-mathjax3": "^4.3.2", "rollup": "^4.60.4", "tslib": "^2.8.1", - "tsx": "^4.22.0", + "tsx": "^4.22.3", "typescript": "^6.0.3", - "vitest": "^2.1.9" + "unplugin-swc": "^1.5.9", + "vitest": "^4.1.7" } }, "node_modules/@algolia/abtesting": { @@ -386,6 +387,40 @@ } } }, + "node_modules/@emnapi/core": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz", + "integrity": "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.2.1", + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz", + "integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/wasi-threads": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz", + "integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, "node_modules/@epic-web/invariant": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/@epic-web/invariant/-/invariant-1.0.0.tgz", @@ -394,9 +429,9 @@ "license": "MIT" }, "node_modules/@esbuild/aix-ppc64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", - "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.0.tgz", + "integrity": "sha512-lhRUCeuOyJQURhTxl4WkpFTjIsbDayJHih5kZC1giwE+MhIzAb7mEsQMqMf18rHLsrb5qI1tafG20mLxEWcWlA==", "cpu": [ "ppc64" ], @@ -411,9 +446,9 @@ } }, "node_modules/@esbuild/android-arm": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz", - "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.0.tgz", + "integrity": "sha512-wqh0ByljabXLKHeWXYLqoJ5jKC4XBaw6Hk08OfMrCRd2nP2ZQ5eleDZC41XHyCNgktBGYMbqnrJKq/K/lzPMSQ==", "cpu": [ "arm" ], @@ -428,9 +463,9 @@ } }, "node_modules/@esbuild/android-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz", - "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.0.tgz", + "integrity": "sha512-+WzIXQOSaGs33tLEgYPYe/yQHf0WTU0X42Jca3y8NWMbUVhp7rUnw+vAsRC/QiDrdD31IszMrZy+qwPOPjd+rw==", "cpu": [ "arm64" ], @@ -445,9 +480,9 @@ } }, "node_modules/@esbuild/android-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz", - "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.0.tgz", + "integrity": "sha512-+VJggoaKhk2VNNqVL7f6S189UzShHC/mR9EE8rDdSkdpN0KflSwWY/gWjDrNxxisg8Fp1ZCD9jLMo4m0OUfeUA==", "cpu": [ "x64" ], @@ -462,9 +497,9 @@ } }, "node_modules/@esbuild/darwin-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz", - "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.0.tgz", + "integrity": "sha512-0T+A9WZm+bZ84nZBtk1ckYsOvyA3x7e2Acj1KdVfV4/2tdG4fzUp91YHx+GArWLtwqp77pBXVCPn2We7Letr0Q==", "cpu": [ "arm64" ], @@ -479,9 +514,9 @@ } }, "node_modules/@esbuild/darwin-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz", - "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.0.tgz", + "integrity": "sha512-fyzLm/DLDl/84OCfp2f/XQ4flmORsjU7VKt8HLjvIXChJoFFOIL6pLJPH4Yhd1n1gGFF9mPwtlN5Wf82DZs+LQ==", "cpu": [ "x64" ], @@ -496,9 +531,9 @@ } }, "node_modules/@esbuild/freebsd-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz", - "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.0.tgz", + "integrity": "sha512-l9GeW5UZBT9k9brBYI+0WDffcRxgHQD8ShN2Ur4xWq/NFzUKm3k5lsH4PdaRgb2w7mI9u61nr2gI2mLI27Nh3Q==", "cpu": [ "arm64" ], @@ -513,9 +548,9 @@ } }, "node_modules/@esbuild/freebsd-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz", - "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.0.tgz", + "integrity": "sha512-BXoQai/A0wPO6Es3yFJ7APCiKGc1tdAEOgeTNy3SsB491S3aHn4S4r3e976eUnPdU+NbdtmBuLncYir2tMU9Nw==", "cpu": [ "x64" ], @@ -530,9 +565,9 @@ } }, "node_modules/@esbuild/linux-arm": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz", - "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.0.tgz", + "integrity": "sha512-CjaaREJagqJp7iTaNQjjidaNbCKYcd4IDkzbwwxtSvjI7NZm79qiHc8HqciMddQ6CKvJT6aBd8lO9kN/ZudLlw==", "cpu": [ "arm" ], @@ -547,9 +582,9 @@ } }, "node_modules/@esbuild/linux-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz", - "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.0.tgz", + "integrity": "sha512-RVyzfb3FWsGA55n6WY0MEIEPURL1FcbhFE6BffZEMEekfCzCIMtB5yyDcFnVbTnwk+CLAgTujmV/Lgvih56W+A==", "cpu": [ "arm64" ], @@ -564,9 +599,9 @@ } }, "node_modules/@esbuild/linux-ia32": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz", - "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.0.tgz", + "integrity": "sha512-KBnSTt1kxl9x70q+ydterVdl+Cn0H18ngRMRCEQfrbqdUuntQQ0LoMZv47uB97NljZFzY6HcfqEZ2SAyIUTQBQ==", "cpu": [ "ia32" ], @@ -581,9 +616,9 @@ } }, "node_modules/@esbuild/linux-loong64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz", - "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.0.tgz", + "integrity": "sha512-zpSlUce1mnxzgBADvxKXX5sl8aYQHo2ezvMNI8I0lbblJtp8V4odlm3Yzlj7gPyt3T8ReksE6bK+pT3WD+aJRg==", "cpu": [ "loong64" ], @@ -598,9 +633,9 @@ } }, "node_modules/@esbuild/linux-mips64el": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz", - "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.0.tgz", + "integrity": "sha512-2jIfP6mmjkdmeTlsX/9vmdmhBmKADrWqN7zcdtHIeNSCH1SqIoNI63cYsjQR8J+wGa4Y5izRcSHSm8K3QWmk3w==", "cpu": [ "mips64el" ], @@ -615,9 +650,9 @@ } }, "node_modules/@esbuild/linux-ppc64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz", - "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.0.tgz", + "integrity": "sha512-bc0FE9wWeC0WBm49IQMPSPILRocGTQt3j5KPCA8os6VprfuJ7KD+5PzESSrJ6GmPIPJK965ZJHTUlSA6GNYEhg==", "cpu": [ "ppc64" ], @@ -632,9 +667,9 @@ } }, "node_modules/@esbuild/linux-riscv64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz", - "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.0.tgz", + "integrity": "sha512-SQPZOwoTTT/HXFXQJG/vBX8sOFagGqvZyXcgLA3NhIqcBv1BJU1d46c0rGcrij2B56Z2rNiSLaZOYW5cUk7yLQ==", "cpu": [ "riscv64" ], @@ -649,9 +684,9 @@ } }, "node_modules/@esbuild/linux-s390x": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz", - "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.0.tgz", + "integrity": "sha512-SCfR0HN8CEEjnYnySJTd2cw0k9OHB/YFzt5zgJEwa+wL/T/raGWYMBqwDNAC6dqFKmJYZoQBRfHjgwLHGSrn3Q==", "cpu": [ "s390x" ], @@ -666,9 +701,9 @@ } }, "node_modules/@esbuild/linux-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz", - "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.0.tgz", + "integrity": "sha512-us0dSb9iFxIi8srnpl931Nvs65it/Jd2a2K3qs7fz2WfGPHqzfzZTfec7oxZJRNPXPnNYZtanmRc4AL/JwVzHQ==", "cpu": [ "x64" ], @@ -683,9 +718,9 @@ } }, "node_modules/@esbuild/netbsd-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz", - "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.0.tgz", + "integrity": "sha512-CR/RYotgtCKwtftMwJlUU7xCVNg3lMYZ0RzTmAHSfLCXw3NtZtNpswLEj/Kkf6kEL3Gw+BpOekRX0BYCtklhUw==", "cpu": [ "arm64" ], @@ -700,9 +735,9 @@ } }, "node_modules/@esbuild/netbsd-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz", - "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.0.tgz", + "integrity": "sha512-nU1yhmYutL+fQ71Kxnhg8uEOdC0pwEW9entHykTgEbna2pw2dkbFSMeqjjyHZoCmt8SBkOSvV+yNmm94aUrrqw==", "cpu": [ "x64" ], @@ -717,9 +752,9 @@ } }, "node_modules/@esbuild/openbsd-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz", - "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.0.tgz", + "integrity": "sha512-cXb5vApOsRsxsEl4mcZ1XY3D4DzcoMxR/nnc4IyqYs0rTI8ZKmW6kyyg+11Z8yvgMfAEldKzP7AdP64HnSC/6g==", "cpu": [ "arm64" ], @@ -734,9 +769,9 @@ } }, "node_modules/@esbuild/openbsd-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz", - "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.0.tgz", + "integrity": "sha512-8wZM2qqtv9UP3mzy7HiGYNH/zjTA355mpeuA+859TyR+e+Tc08IHYpLJuMsfpDJwoLo1ikIJI8jC3GFjnRClzA==", "cpu": [ "x64" ], @@ -750,10 +785,27 @@ "node": ">=18" } }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.0.tgz", + "integrity": "sha512-FLGfyizszcef5C3YtoyQDACyg95+dndv79i2EekILBofh5wpCa1KuBqOWKrEHZg3zrL3t5ouE5jgr94vA+Wb2w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, "node_modules/@esbuild/sunos-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz", - "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.0.tgz", + "integrity": "sha512-1ZgjUoEdHZZl/YlV76TSCz9Hqj9h9YmMGAgAPYd+q4SicWNX3G5GCyx9uhQWSLcbvPW8Ni7lj4gDa1T40akdlw==", "cpu": [ "x64" ], @@ -768,9 +820,9 @@ } }, "node_modules/@esbuild/win32-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz", - "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.0.tgz", + "integrity": "sha512-Q9StnDmQ/enxnpxCCLSg0oo4+34B9TdXpuyPeTedN/6+iXBJ4J+zwfQI28u/Jl40nOYAxGoNi7mFP40RUtkmUA==", "cpu": [ "arm64" ], @@ -785,9 +837,9 @@ } }, "node_modules/@esbuild/win32-ia32": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz", - "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.0.tgz", + "integrity": "sha512-zF3ag/gfiCe6U2iczcRzSYJKH1DCI+ByzSENHlM2FcDbEeo5Zd2C86Aq0tKUYAJJ1obRP84ymxIAksZUcdztHA==", "cpu": [ "ia32" ], @@ -802,9 +854,9 @@ } }, "node_modules/@esbuild/win32-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", - "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.0.tgz", + "integrity": "sha512-pEl1bO9mfAmIC+tW5btTmrKaujg3zGtUmWNdCw/xs70FBjwAL3o9OEKNHvNmnyylD6ubxUERiEhdsL0xBQ9efw==", "cpu": [ "x64" ], @@ -850,31 +902,65 @@ "license": "MIT" }, "node_modules/@inversifyjs/common": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/@inversifyjs/common/-/common-1.3.3.tgz", - "integrity": "sha512-ZH0wrgaJwIo3s9gMCDM2wZoxqrJ6gB97jWXncROfYdqZJv8f3EkqT57faZqN5OTeHWgtziQ6F6g3L8rCvGceCw==", + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/@inversifyjs/common/-/common-1.5.2.tgz", + "integrity": "sha512-WlzR9xGadABS9gtgZQ+luoZ8V6qm4Ii6RQfcfC9Ho2SOlE6ZuemFo7PKJvKI0ikm8cmKbU8hw5UK6E4qovH21w==", "dev": true, "license": "MIT" }, + "node_modules/@inversifyjs/container": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/@inversifyjs/container/-/container-1.15.0.tgz", + "integrity": "sha512-U2xYsPrJTz5za2TExi5lg8qOWf8TEVBpN+pQM7B8BVA2rajtbRE9A66SLRHk8c1eGXmg+0K4Hdki6tWAsSQBUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inversifyjs/common": "1.5.2", + "@inversifyjs/core": "9.2.0", + "@inversifyjs/plugin": "0.2.0", + "@inversifyjs/reflect-metadata-utils": "1.4.1" + }, + "peerDependencies": { + "reflect-metadata": "~0.2.2" + } + }, "node_modules/@inversifyjs/core": { - "version": "1.3.4", - "resolved": "https://registry.npmjs.org/@inversifyjs/core/-/core-1.3.4.tgz", - "integrity": "sha512-gCCmA4BdbHEFwvVZ2elWgHuXZWk6AOu/1frxsS+2fWhjEk2c/IhtypLo5ytSUie1BCiT6i9qnEo4bruBomQsAA==", + "version": "9.2.0", + "resolved": "https://registry.npmjs.org/@inversifyjs/core/-/core-9.2.0.tgz", + "integrity": "sha512-Nm7BR6KmpgshIHpVQWuEDehqRVb6GBm8LFEuhc2s4kSZWrArZ15RmXQzROLk4m+hkj4kMXgvMm5Qbopot/D6Sg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inversifyjs/common": "1.5.2", + "@inversifyjs/prototype-utils": "0.1.3", + "@inversifyjs/reflect-metadata-utils": "1.4.1" + } + }, + "node_modules/@inversifyjs/plugin": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/@inversifyjs/plugin/-/plugin-0.2.0.tgz", + "integrity": "sha512-R/JAdkTSD819pV1zi0HP54mWHyX+H2m8SxldXRgPQarS3ySV4KPyRdosWcfB8Se0JJZWZLHYiUNiS6JvMWSPjw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@inversifyjs/prototype-utils": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@inversifyjs/prototype-utils/-/prototype-utils-0.1.3.tgz", + "integrity": "sha512-EzRamZzNgE9Sn3QtZ8NncNa2lpPMZfspqbK6BWFguWnOpK8ymp2TUuH46ruFHZhrHKnknPd7fG22ZV7iF517TQ==", "dev": true, "license": "MIT", "dependencies": { - "@inversifyjs/common": "1.3.3", - "@inversifyjs/reflect-metadata-utils": "0.2.3" + "@inversifyjs/common": "1.5.2" } }, "node_modules/@inversifyjs/reflect-metadata-utils": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/@inversifyjs/reflect-metadata-utils/-/reflect-metadata-utils-0.2.3.tgz", - "integrity": "sha512-d3D0o9TeSlvaGM2I24wcNw/Aj3rc4OYvHXOKDC09YEph5fMMiKd6fq1VTQd9tOkDNWvVbw+cnt45Wy9P/t5Lvw==", + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/@inversifyjs/reflect-metadata-utils/-/reflect-metadata-utils-1.4.1.tgz", + "integrity": "sha512-Cp77C4d2wLaHXiUB7iH6Cxb7i1lD/YDuTIHLTDzKINqGSz0DCSoL/Dg2wVkW/6Qx03r/yQMLJ+32Agl32N2X8g==", "dev": true, "license": "MIT", "peerDependencies": { - "reflect-metadata": "0.2.2" + "reflect-metadata": "~0.2.2" } }, "node_modules/@javascript-obfuscator/escodegen": { @@ -906,6 +992,38 @@ "node": ">=4.0" } }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/@jridgewell/sourcemap-codec": { "version": "1.5.5", "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", @@ -913,6 +1031,17 @@ "dev": true, "license": "MIT" }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, "node_modules/@js-temporal/polyfill": { "version": "0.5.1", "resolved": "https://registry.npmjs.org/@js-temporal/polyfill/-/polyfill-0.5.1.tgz", @@ -934,104 +1063,46 @@ "resolved": "packages/tempo", "link": true }, - "node_modules/@polka/url": { - "version": "1.0.0-next.29", - "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.29.tgz", - "integrity": "sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==", - "dev": true, - "license": "MIT" - }, - "node_modules/@rollup/plugin-alias": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/@rollup/plugin-alias/-/plugin-alias-6.0.0.tgz", - "integrity": "sha512-tPCzJOtS7uuVZd+xPhoy5W4vThe6KWXNmsFCNktaAh5RTqcLiSfT4huPQIXkgJ6YCOjJHvecOAzQxLFhPxKr+g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=20.19.0" - }, - "peerDependencies": { - "rollup": ">=4.0.0" - }, - "peerDependenciesMeta": { - "rollup": { - "optional": true - } - } - }, - "node_modules/@rollup/plugin-node-resolve": { - "version": "16.0.3", - "resolved": "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-16.0.3.tgz", - "integrity": "sha512-lUYM3UBGuM93CnMPG1YocWu7X802BrNF3jW2zny5gQyLQgRFJhV1Sq0Zi74+dh/6NBx1DxFC4b4GXg9wUCG5Qg==", + "node_modules/@napi-rs/wasm-runtime": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.4.tgz", + "integrity": "sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow==", "dev": true, "license": "MIT", + "optional": true, "dependencies": { - "@rollup/pluginutils": "^5.0.1", - "@types/resolve": "1.20.2", - "deepmerge": "^4.2.2", - "is-module": "^1.0.0", - "resolve": "^1.22.1" + "@tybys/wasm-util": "^0.10.1" }, - "engines": { - "node": ">=14.0.0" + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" }, "peerDependencies": { - "rollup": "^2.78.0||^3.0.0||^4.0.0" - }, - "peerDependenciesMeta": { - "rollup": { - "optional": true - } + "@emnapi/core": "^1.7.1", + "@emnapi/runtime": "^1.7.1" } }, - "node_modules/@rollup/pluginutils": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.3.0.tgz", - "integrity": "sha512-5EdhGZtnu3V88ces7s53hhfK5KSASnJZv8Lulpc04cWO3REESroJXg73DFsOmgbU2BhwV0E20bu2IDZb3VKW4Q==", + "node_modules/@oxc-project/types": { + "version": "0.132.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.132.0.tgz", + "integrity": "sha512-FESMOxil5Se014ui/Eq8fT5uHJo6nIRwH0PfJrZJXs6Gek3ZVFOrpUv3YIZT20m+extU98Hg1Ym72U58rlsxUQ==", "dev": true, "license": "MIT", - "dependencies": { - "@types/estree": "^1.0.0", - "estree-walker": "^2.0.2", - "picomatch": "^4.0.2" - }, - "engines": { - "node": ">=14.0.0" - }, - "peerDependencies": { - "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" - }, - "peerDependenciesMeta": { - "rollup": { - "optional": true - } + "funding": { + "url": "https://github.com/sponsors/Boshen" } }, - "node_modules/@rollup/pluginutils/node_modules/estree-walker": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", - "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", + "node_modules/@polka/url": { + "version": "1.0.0-next.29", + "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.29.tgz", + "integrity": "sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==", "dev": true, "license": "MIT" }, - "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.60.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.4.tgz", - "integrity": "sha512-F5QXMSiFebS9hKZj02XhWLLnRpJ3B3AROP0tWbFBSj+6kCbg5m9j5JoHKd4mmSVy5mS/IMQloYgYxCuJC0fxEQ==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ] - }, - "node_modules/@rollup/rollup-android-arm64": { - "version": "4.60.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.60.4.tgz", - "integrity": "sha512-GxxTKApUpzRhof7poWvCJHRF51C67u1R7D6DiluBE8wKU1u5GWE8t+v81JvJYtbawoBFX1hLv5Ei4eVjkWokaw==", + "node_modules/@rolldown/binding-android-arm64": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.2.tgz", + "integrity": "sha512-ZS4D1JPGn/MYQN/SYDWftIE/nVsM8j/AFOYEzAoOE2O3NktQOZru+/vYXGbR/qtdLdIfGCP0lcoJiYVzsEz+iQ==", "cpu": [ "arm64" ], @@ -1040,12 +1111,15 @@ "optional": true, "os": [ "android" - ] + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.60.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.60.4.tgz", - "integrity": "sha512-tua0TaJxMOB1R0V0RS1jFZ/RpURFDJIOR2A6jWwQeawuFyS4gBW+rntLRaQd0EQ4bd6Vp44Z2rXW+YYDBsj6IA==", + "node_modules/@rolldown/binding-darwin-arm64": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.2.tgz", + "integrity": "sha512-vdFA9+C/rekyGce7WqHs/xoT0ioZEWaOFyZLIV1mEeNFaFDUQrPIo8Vs2GvJ6eetb3rzDUtUBgzto3ExpXJB3w==", "cpu": [ "arm64" ], @@ -1054,12 +1128,15 @@ "optional": true, "os": [ "darwin" - ] + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.60.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.60.4.tgz", - "integrity": "sha512-CSKq7MsP+5PFIcydhAiR1K0UhEI1A2jWXVKHPCBZ151yOutENwvnPocgVHkivu2kviURtCEB6zUQw0vs8RrhMg==", + "node_modules/@rolldown/binding-darwin-x64": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.2.tgz", + "integrity": "sha512-BewSOwTHazv77DTYiAZXSqqKZ4KP/KonFisDMVU7PImxoWfB2aepnPhd2E4SWz3zDzYgDNbs6jBmTdgNnF02GA==", "cpu": [ "x64" ], @@ -1068,42 +1145,51 @@ "optional": true, "os": [ "darwin" - ] + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.60.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.60.4.tgz", - "integrity": "sha512-+O8OkVdyvXMtJEciu2wS/pzm1IxntEEQx3z5TAVy4l32G0etZn+RsA48ARRrFm6Ri8fvqPQfgrvNxSjKAbnd3g==", + "node_modules/@rolldown/binding-freebsd-x64": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.2.tgz", + "integrity": "sha512-m41o7M0YWtUdqk61Tb+jnKb2rN++iRdIASlExkUoKfIAH30DOHCB8fVLzSUpbWHHU8esmEioY62PxzexE8MBuA==", "cpu": [ - "arm64" + "x64" ], "dev": true, "license": "MIT", "optional": true, "os": [ "freebsd" - ] + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.60.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.60.4.tgz", - "integrity": "sha512-Iw3oMskH3AfNuhU0MSN7vNbdi4me/NiYo2azqPz/Le16zHSa+3RRmliCMWWQmh4lcndccU40xcJuTYJZxNo/lw==", + "node_modules/@rolldown/binding-linux-arm-gnueabihf": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.2.tgz", + "integrity": "sha512-jcojB9H7W/jS29pMKWAK1N+fU99vXodHDTatS3b3y/XSOCiHo0kkA74pL3jJmkoQtYpOCxDvaKs1fo2Ij/1X5w==", "cpu": [ - "x64" + "arm" ], "dev": true, "license": "MIT", "optional": true, "os": [ - "freebsd" - ] + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.60.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.60.4.tgz", - "integrity": "sha512-EIPRXTVQpHyF8WOo219AD2yEltPehLTcTMz2fn6JsatLYSzQf00hj3rulF+yauOlF9/FtM2WpkT/hJh/KJFGhA==", + "node_modules/@rolldown/binding-linux-arm64-gnu": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.2.tgz", + "integrity": "sha512-1jn6qDU5iiOgFgygDzKUuKP0maTi0/f1+sBLgvij/76C77Nm3ts6ufz9Bjg5q5dduxiUIxtq86JIoBvo1xQ4Ig==", "cpu": [ - "arm" + "arm64" ], "dev": true, "libc": [ @@ -1113,14 +1199,17 @@ "optional": true, "os": [ "linux" - ] + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.60.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.60.4.tgz", - "integrity": "sha512-J3Yh9PzzF1Ovah2At+lHiGQdsYgArxBbXv/zHfSyaiFQEqvNv7DcW98pCrmdjCZBrqBiKrKKe2V+aaSGWuBe/w==", + "node_modules/@rolldown/binding-linux-arm64-musl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.2.tgz", + "integrity": "sha512-QVLO/czFMdoMFSqlX3bcswcJNm/23r+qoa/jgtmFc/qEp6/jXmIkDjF/XIo8dPfGaiwy1xfQn8o77L79GeXFgw==", "cpu": [ - "arm" + "arm64" ], "dev": true, "libc": [ @@ -1130,14 +1219,17 @@ "optional": true, "os": [ "linux" - ] + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.60.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.60.4.tgz", - "integrity": "sha512-BFDEZMYfUvLn37ONE1yMBojPxnMlTFsdyNoqncT0qFq1mAfllL+ATMMJd8TeuVMiX84s1KbcxcZbXInmcO2mRg==", + "node_modules/@rolldown/binding-linux-ppc64-gnu": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.2.tgz", + "integrity": "sha512-hgO5Abm0w5UL6FEa2iFnZqo2KlK7TQ5QhV5x09hujBf7t5KzHQ1VmfPuTpqRy/rNlSxua3eWH374xxiVrP+lcA==", "cpu": [ - "arm64" + "ppc64" ], "dev": true, "libc": [ @@ -1147,24 +1239,372 @@ "optional": true, "os": [ "linux" - ] + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.60.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.60.4.tgz", - "integrity": "sha512-pc9EYOSlOgdQ2uPl1o9PF6/kLSgaUosia7gOuS8mB69IxJvlclko1MECXysjs5ryez1/5zjYqx3+xYU0TU6R1A==", + "node_modules/@rolldown/binding-linux-s390x-gnu": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.2.tgz", + "integrity": "sha512-fy8rXxuYEu602abC8MUNaPjYLIFzReOaEIEMKMUa0rFEUxNpVXhs15KSSQ4qlqSaM7B6rcj9rDZgADh/IGDzLQ==", "cpu": [ - "arm64" + "s390x" ], "dev": true, "libc": [ - "musl" + "glibc" ], "license": "MIT", "optional": true, "os": [ "linux" - ] + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-gnu": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.2.tgz", + "integrity": "sha512-0+bOkiQ779+r1WpoHOWHqncvyySci0vKph+myNDYb+im6meJAzHQXay6oEgnkHuUGouM1LKTZwqKpBow6Kj7CQ==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-musl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.2.tgz", + "integrity": "sha512-mjSkrzZK5Qsl0a9d1JgILOiuZOSDTVdKENcSXBoqbzSrspLR/4/IRVDo5wd2GgZjNss/viBFJdeq+j7qH2nypw==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-openharmony-arm64": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.2.tgz", + "integrity": "sha512-1v5vHasdfQAZoEHakBV72LIFAC9JjnymsiKxp+GEr/ma3+NJCPSaYK+qavInOovJkgwFrs7GccX2d6IgDA3Z5w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-wasm32-wasi": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.2.tgz", + "integrity": "sha512-mb1VobWn6NheziTk5/WEaR6AKVbrwT5sOi6C7zk3gy/pD1qtJfU1j4PgTo2NJnOtbL9Dl3Aeei8w9jJ7qC2jZQ==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "1.10.0", + "@emnapi/runtime": "1.10.0", + "@napi-rs/wasm-runtime": "^1.1.4" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-arm64-msvc": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.2.tgz", + "integrity": "sha512-SqKonF56vA/L2yHwHYcEp2P34URpOZ7d1fS635cTkpDnUtEGdUbhI6NzsPdqeSWvAAeGDrxjWjNmibDIdFf9/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-x64-msvc": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.2.tgz", + "integrity": "sha512-v7qRI7gXLRINcOGXt+7YmAZ6iFuyZVMIoXAxhd8oP+DR9dLfL9GfNIx7PLMxmhZdvq8waUJBQiWN9EKNy+TRBQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz", + "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/plugin-alias": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/@rollup/plugin-alias/-/plugin-alias-6.0.0.tgz", + "integrity": "sha512-tPCzJOtS7uuVZd+xPhoy5W4vThe6KWXNmsFCNktaAh5RTqcLiSfT4huPQIXkgJ6YCOjJHvecOAzQxLFhPxKr+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "rollup": ">=4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/@rollup/plugin-node-resolve": { + "version": "16.0.3", + "resolved": "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-16.0.3.tgz", + "integrity": "sha512-lUYM3UBGuM93CnMPG1YocWu7X802BrNF3jW2zny5gQyLQgRFJhV1Sq0Zi74+dh/6NBx1DxFC4b4GXg9wUCG5Qg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rollup/pluginutils": "^5.0.1", + "@types/resolve": "1.20.2", + "deepmerge": "^4.2.2", + "is-module": "^1.0.0", + "resolve": "^1.22.1" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^2.78.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/@rollup/pluginutils": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.3.0.tgz", + "integrity": "sha512-5EdhGZtnu3V88ces7s53hhfK5KSASnJZv8Lulpc04cWO3REESroJXg73DFsOmgbU2BhwV0E20bu2IDZb3VKW4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "estree-walker": "^2.0.2", + "picomatch": "^4.0.2" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/@rollup/pluginutils/node_modules/estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.4.tgz", + "integrity": "sha512-F5QXMSiFebS9hKZj02XhWLLnRpJ3B3AROP0tWbFBSj+6kCbg5m9j5JoHKd4mmSVy5mS/IMQloYgYxCuJC0fxEQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.60.4.tgz", + "integrity": "sha512-GxxTKApUpzRhof7poWvCJHRF51C67u1R7D6DiluBE8wKU1u5GWE8t+v81JvJYtbawoBFX1hLv5Ei4eVjkWokaw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.60.4.tgz", + "integrity": "sha512-tua0TaJxMOB1R0V0RS1jFZ/RpURFDJIOR2A6jWwQeawuFyS4gBW+rntLRaQd0EQ4bd6Vp44Z2rXW+YYDBsj6IA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.60.4.tgz", + "integrity": "sha512-CSKq7MsP+5PFIcydhAiR1K0UhEI1A2jWXVKHPCBZ151yOutENwvnPocgVHkivu2kviURtCEB6zUQw0vs8RrhMg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.60.4.tgz", + "integrity": "sha512-+O8OkVdyvXMtJEciu2wS/pzm1IxntEEQx3z5TAVy4l32G0etZn+RsA48ARRrFm6Ri8fvqPQfgrvNxSjKAbnd3g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.60.4.tgz", + "integrity": "sha512-Iw3oMskH3AfNuhU0MSN7vNbdi4me/NiYo2azqPz/Le16zHSa+3RRmliCMWWQmh4lcndccU40xcJuTYJZxNo/lw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.60.4.tgz", + "integrity": "sha512-EIPRXTVQpHyF8WOo219AD2yEltPehLTcTMz2fn6JsatLYSzQf00hj3rulF+yauOlF9/FtM2WpkT/hJh/KJFGhA==", + "cpu": [ + "arm" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.60.4.tgz", + "integrity": "sha512-J3Yh9PzzF1Ovah2At+lHiGQdsYgArxBbXv/zHfSyaiFQEqvNv7DcW98pCrmdjCZBrqBiKrKKe2V+aaSGWuBe/w==", + "cpu": [ + "arm" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.60.4.tgz", + "integrity": "sha512-BFDEZMYfUvLn37ONE1yMBojPxnMlTFsdyNoqncT0qFq1mAfllL+ATMMJd8TeuVMiX84s1KbcxcZbXInmcO2mRg==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.60.4.tgz", + "integrity": "sha512-pc9EYOSlOgdQ2uPl1o9PF6/kLSgaUosia7gOuS8mB69IxJvlclko1MECXysjs5ryez1/5zjYqx3+xYU0TU6R1A==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] }, "node_modules/@rollup/rollup-linux-loong64-gnu": { "version": "4.60.4", @@ -1512,25 +1952,356 @@ "dev": true, "license": "MIT", "dependencies": { - "@shikijs/vscode-textmate": "^10.0.2", - "@types/hast": "^3.0.4" + "@shikijs/vscode-textmate": "^10.0.2", + "@types/hast": "^3.0.4" + } + }, + "node_modules/@shikijs/types": { + "version": "3.23.0", + "resolved": "https://registry.npmjs.org/@shikijs/types/-/types-3.23.0.tgz", + "integrity": "sha512-3JZ5HXOZfYjsYSk0yPwBrkupyYSLpAE26Qc0HLghhZNGTZg/SKxXIIgoxOpmmeQP0RRSDJTk1/vPfw9tbw+jSQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@shikijs/vscode-textmate": "^10.0.2", + "@types/hast": "^3.0.4" + } + }, + "node_modules/@shikijs/vscode-textmate": { + "version": "10.0.2", + "resolved": "https://registry.npmjs.org/@shikijs/vscode-textmate/-/vscode-textmate-10.0.2.tgz", + "integrity": "sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@swc/core": { + "version": "1.15.40", + "resolved": "https://registry.npmjs.org/@swc/core/-/core-1.15.40.tgz", + "integrity": "sha512-2kwzJikRvgtNAG7MwVZY2vEzZjTxKIq5jXOihuSV/8U+Hej8Va22t65aKnJZs3P+NwojZvR8Mf8kyM7O+V8sQg==", + "dev": true, + "hasInstallScript": true, + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@swc/counter": "^0.1.3", + "@swc/types": "^0.1.26" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/swc" + }, + "optionalDependencies": { + "@swc/core-darwin-arm64": "1.15.40", + "@swc/core-darwin-x64": "1.15.40", + "@swc/core-linux-arm-gnueabihf": "1.15.40", + "@swc/core-linux-arm64-gnu": "1.15.40", + "@swc/core-linux-arm64-musl": "1.15.40", + "@swc/core-linux-ppc64-gnu": "1.15.40", + "@swc/core-linux-s390x-gnu": "1.15.40", + "@swc/core-linux-x64-gnu": "1.15.40", + "@swc/core-linux-x64-musl": "1.15.40", + "@swc/core-win32-arm64-msvc": "1.15.40", + "@swc/core-win32-ia32-msvc": "1.15.40", + "@swc/core-win32-x64-msvc": "1.15.40" + }, + "peerDependencies": { + "@swc/helpers": ">=0.5.17" + }, + "peerDependenciesMeta": { + "@swc/helpers": { + "optional": true + } + } + }, + "node_modules/@swc/core-darwin-arm64": { + "version": "1.15.40", + "resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.15.40.tgz", + "integrity": "sha512-PaYyclfmQ++77D8ityYvmmVzHv9aG8ROwt2GfG6/ccloy4Hgf80qtOnzb9VYvPsUT7Ty1uhuDRhv3XYpf62qhQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "darwin" + ], + "peer": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-darwin-x64": { + "version": "1.15.40", + "resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.15.40.tgz", + "integrity": "sha512-HbbPzvfLBUXjIB1Ezks+//lNUjmLjfyd63XSwprJgrZaXYdm70kohXPJUWdqKZozolFxbPaO+xtBaiUp6BoueA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "darwin" + ], + "peer": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-arm-gnueabihf": { + "version": "1.15.40", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.15.40.tgz", + "integrity": "sha512-SlRZsCjOCPR2LvFs0Ri/Xrx/5o5TCt8vl4gW6mX1hEZOG0a625RxzRHpHdAQNGykmAN/7IeaFAJG+QnNmxlHcA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-arm64-gnu": { + "version": "1.15.40", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.15.40.tgz", + "integrity": "sha512-Q8byxJt2fh8CR3EUX6snBpy47AoBVm+In/+Z3rjDHMjC38ZvR9/gtUUNCT0tfrn4EdVsO8/QPi59nxrxvqxvBQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-arm64-musl": { + "version": "1.15.40", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.15.40.tgz", + "integrity": "sha512-4z0MgHU+7M0pZDqBN1El7mFXDI1SBwinfcUkAyA4v8QrhOIUOZltySt2aStQLZGrdXVXM4Y4ylfiTC04ED+MoQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-ppc64-gnu": { + "version": "1.15.40", + "resolved": "https://registry.npmjs.org/@swc/core-linux-ppc64-gnu/-/core-linux-ppc64-gnu-1.15.40.tgz", + "integrity": "sha512-fLI4iUgeSZu0eRWUXwe6YzPFx9gHbFiPkl8Rp3mJfP8OpNR3nTQCGPvHdDh9xniW7mVvgMY4ni7A4VzqI1KrpA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-s390x-gnu": { + "version": "1.15.40", + "resolved": "https://registry.npmjs.org/@swc/core-linux-s390x-gnu/-/core-linux-s390x-gnu-1.15.40.tgz", + "integrity": "sha512-YqeKMAb7d4nQSGMJQ454IlaCENpzcDqhvBE9+CPfdnYpnUXxd+BSrB6Xk0YjW8UyoEhUj4p6quATCxbsp6J3jg==", + "cpu": [ + "s390x" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-x64-gnu": { + "version": "1.15.40", + "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.15.40.tgz", + "integrity": "sha512-7HOuS1iGcme/j/TuL1TfmmLGiMQrjv/GmjyZeydl00FKPtpGXEldwqfI56xgd1YzrzoB2svWjxbGGyQ0TEASxg==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-x64-musl": { + "version": "1.15.40", + "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.15.40.tgz", + "integrity": "sha512-h4kZYHc7dpc9P9u4brRJaS8Pl7tPVHAeiLSzw7T5RfIJgAoSdaCMKzI/2Uay9gFhaw8uyCDl0L5q37r0EpAfIA==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-win32-arm64-msvc": { + "version": "1.15.40", + "resolved": "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.15.40.tgz", + "integrity": "sha512-+mQgKZXSj6mV38Zh05QaxSjUDmGP/R2JWlXZTDLSPkDzHU6p3GxN9eeSf5dfyDVU86946fmCvSzyl/ucImx8+A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "win32" + ], + "peer": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-win32-ia32-msvc": { + "version": "1.15.40", + "resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.15.40.tgz", + "integrity": "sha512-yvwdPLGd25mcj/mNatjNQ0lZujtQD6psH3v9PNmMb+fSzjbNG8KIDxjFWrcV+fsFVLOkyOmdJsFmX7NAFjVyPw==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "win32" + ], + "peer": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-win32-x64-msvc": { + "version": "1.15.40", + "resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.15.40.tgz", + "integrity": "sha512-OXtKsLU1bVtInzzDEAY2sYiF/rl4tvAnLLLpuMp3HzAOQZ5A+i69AKDhA1YLQTaMAqO3vzyYNVAYVRMPtSYD4w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "win32" + ], + "peer": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/counter": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz", + "integrity": "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==", + "dev": true, + "license": "Apache-2.0", + "peer": true + }, + "node_modules/@swc/types": { + "version": "0.1.26", + "resolved": "https://registry.npmjs.org/@swc/types/-/types-0.1.26.tgz", + "integrity": "sha512-lyMwd7WGgG79RS7EERZV3T8wMdmPq3xwyg+1nmAM64kIhx5yl+juO2PYIHb7vTiPgPCj8LYjsNV2T5wiQHUEaw==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@swc/counter": "^0.1.3" + } + }, + "node_modules/@tybys/wasm-util": { + "version": "0.10.2", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.2.tgz", + "integrity": "sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" } }, - "node_modules/@shikijs/types": { - "version": "3.23.0", - "resolved": "https://registry.npmjs.org/@shikijs/types/-/types-3.23.0.tgz", - "integrity": "sha512-3JZ5HXOZfYjsYSk0yPwBrkupyYSLpAE26Qc0HLghhZNGTZg/SKxXIIgoxOpmmeQP0RRSDJTk1/vPfw9tbw+jSQ==", + "node_modules/@types/chai": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", "dev": true, "license": "MIT", "dependencies": { - "@shikijs/vscode-textmate": "^10.0.2", - "@types/hast": "^3.0.4" + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" } }, - "node_modules/@shikijs/vscode-textmate": { - "version": "10.0.2", - "resolved": "https://registry.npmjs.org/@shikijs/vscode-textmate/-/vscode-textmate-10.0.2.tgz", - "integrity": "sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg==", + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", "dev": true, "license": "MIT" }, @@ -1542,9 +2313,9 @@ "license": "MIT" }, "node_modules/@types/google.maps": { - "version": "3.64.1", - "resolved": "https://registry.npmjs.org/@types/google.maps/-/google.maps-3.64.1.tgz", - "integrity": "sha512-nEBoa6iDNipICtxJ5VlrOgPNZQ6ixIg5nuv8iryFj0Z/1NLgxyg3pQCVegPuCzGCyTQwQI/N3uZvLUysqAzaaw==", + "version": "3.65.0", + "resolved": "https://registry.npmjs.org/@types/google.maps/-/google.maps-3.65.0.tgz", + "integrity": "sha512-u4SHiRC3m27lPa4vDBxh2AI7mDcHcheX6GSHn1Mwi0Gap8/uhM2kFppiFTnWASXLHZO+1ahHciLeEIV+Sjqk/A==", "dev": true, "license": "MIT" }, @@ -1615,9 +2386,9 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "25.8.0", - "resolved": "https://registry.npmjs.org/@types/node/-/node-25.8.0.tgz", - "integrity": "sha512-TCFSk8IZh+iLX1xtksoBVtdmgL+1IX0fC9BeU4QqFSuNdN/K+HUlhqOzEmSYYpZUVsLYcPqc9KX+60iDuninSQ==", + "version": "25.9.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.9.1.tgz", + "integrity": "sha512-xfrlY7UD5rMJk3ZVJP8BNzS28J36YJg+xp+LPXV1TdWxr8uMH5A860QNxYDGQe/ylDSgjxE52Q9VnO7p75tJxg==", "dev": true, "license": "MIT", "dependencies": { @@ -1691,135 +2462,108 @@ } }, "node_modules/@vitest/expect": { - "version": "2.1.9", - "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-2.1.9.tgz", - "integrity": "sha512-UJCIkTBenHeKT1TTlKMJWy1laZewsRIzYighyYiJKZreqtdxSos/S1t+ktRMQWu2CKqaarrkeszJx1cgC5tGZw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@vitest/spy": "2.1.9", - "@vitest/utils": "2.1.9", - "chai": "^5.1.2", - "tinyrainbow": "^1.2.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/@vitest/mocker": { - "version": "2.1.9", - "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-2.1.9.tgz", - "integrity": "sha512-tVL6uJgoUdi6icpxmdrn5YNo3g3Dxv+IHJBr0GXHaEdTcw3F+cPKnsXFhli6nO+f/6SDKPHEK1UN+k+TQv0Ehg==", + "version": "4.1.7", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.7.tgz", + "integrity": "sha512-1R+tw0ortHEbZDGMymm+pN7/AFQ/RkFFdtd7EN+VBpynKmLbP8A3rpEXdshBJ7+8hQ9zBJh/i1s0yKNtxAnU7w==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/spy": "2.1.9", - "estree-walker": "^3.0.3", - "magic-string": "^0.30.12" + "@standard-schema/spec": "^1.1.0", + "@types/chai": "^5.2.2", + "@vitest/spy": "4.1.7", + "@vitest/utils": "4.1.7", + "chai": "^6.2.2", + "tinyrainbow": "^3.1.0" }, "funding": { "url": "https://opencollective.com/vitest" - }, - "peerDependencies": { - "msw": "^2.4.9", - "vite": "^5.0.0" - }, - "peerDependenciesMeta": { - "msw": { - "optional": true - }, - "vite": { - "optional": true - } } }, "node_modules/@vitest/pretty-format": { - "version": "2.1.9", - "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-2.1.9.tgz", - "integrity": "sha512-KhRIdGV2U9HOUzxfiHmY8IFHTdqtOhIzCpd8WRdJiE7D/HUcZVD0EgQCVjm+Q9gkUXWgBvMmTtZgIG48wq7sOQ==", + "version": "4.1.7", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.7.tgz", + "integrity": "sha512-umgCarTOYQWIaDMvGDRZij+6b9oVeLIyJzfN+AS88e0ZOU3QTgNNSTtjQOpcvWr3np1N0j4WgZj+sb3oYBDscw==", "dev": true, "license": "MIT", "dependencies": { - "tinyrainbow": "^1.2.0" + "tinyrainbow": "^3.1.0" }, "funding": { "url": "https://opencollective.com/vitest" } }, "node_modules/@vitest/runner": { - "version": "2.1.9", - "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-2.1.9.tgz", - "integrity": "sha512-ZXSSqTFIrzduD63btIfEyOmNcBmQvgOVsPNPe0jYtESiXkhd8u2erDLnMxmGrDCwHCCHE7hxwRDCT3pt0esT4g==", + "version": "4.1.7", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.7.tgz", + "integrity": "sha512-BapjmAQ2aI78WdMEfeUWivnfVzB+VPGwWRQcJE0OUq7qEeEcBsCSf+0T5iREBNE5nBb4wA5Ya0W6IA+sghdEFw==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/utils": "2.1.9", - "pathe": "^1.1.2" + "@vitest/utils": "4.1.7", + "pathe": "^2.0.3" }, "funding": { "url": "https://opencollective.com/vitest" } }, "node_modules/@vitest/snapshot": { - "version": "2.1.9", - "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-2.1.9.tgz", - "integrity": "sha512-oBO82rEjsxLNJincVhLhaxxZdEtV0EFHMK5Kmx5sJ6H9L183dHECjiefOAdnqpIgT5eZwT04PoggUnW88vOBNQ==", + "version": "4.1.7", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.7.tgz", + "integrity": "sha512-ZacLzja+TmJeZ1h14xW2FB/WpeimUD3haBXQPyJqxvo8jQTmfeA8zv58mtjN2C7EHXZDYVcVYdYmAxjkWVvKCw==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/pretty-format": "2.1.9", - "magic-string": "^0.30.12", - "pathe": "^1.1.2" + "@vitest/pretty-format": "4.1.7", + "@vitest/utils": "4.1.7", + "magic-string": "^0.30.21", + "pathe": "^2.0.3" }, "funding": { "url": "https://opencollective.com/vitest" } }, "node_modules/@vitest/spy": { - "version": "2.1.9", - "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-2.1.9.tgz", - "integrity": "sha512-E1B35FwzXXTs9FHNK6bDszs7mtydNi5MIfUWpceJ8Xbfb1gBMscAnwLbEu+B44ed6W3XjL9/ehLPHR1fkf1KLQ==", + "version": "4.1.7", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.7.tgz", + "integrity": "sha512-kbkI5LMWakyuTIvs6fUJ5qdIVb1XVKsYJAT4OJ938cHMROYMSfmoQdZy0aaAnjbbc8F61vkoTqz/Az+/HiIu5Q==", "dev": true, "license": "MIT", - "dependencies": { - "tinyspy": "^3.0.2" - }, "funding": { "url": "https://opencollective.com/vitest" } }, "node_modules/@vitest/ui": { - "version": "2.1.9", - "resolved": "https://registry.npmjs.org/@vitest/ui/-/ui-2.1.9.tgz", - "integrity": "sha512-izzd2zmnk8Nl5ECYkW27328RbQ1nKvkm6Bb5DAaz1Gk59EbLkiCMa6OLT0NoaAYTjOFS6N+SMYW1nh4/9ljPiw==", + "version": "4.1.7", + "resolved": "https://registry.npmjs.org/@vitest/ui/-/ui-4.1.7.tgz", + "integrity": "sha512-TP6utB2yX6rsJNVRo2qAlsi48i1YwFTrLV2tnTtWqJaYX7m4lRCCLirZBjU6xC5m0RsPHr+L2+N+eIPhgEzFfw==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/utils": "2.1.9", + "@vitest/utils": "4.1.7", "fflate": "^0.8.2", - "flatted": "^3.3.1", - "pathe": "^1.1.2", - "sirv": "^3.0.0", - "tinyglobby": "^0.2.10", - "tinyrainbow": "^1.2.0" + "flatted": "^3.4.2", + "pathe": "^2.0.3", + "sirv": "^3.0.2", + "tinyglobby": "^0.2.15", + "tinyrainbow": "^3.1.0" }, "funding": { "url": "https://opencollective.com/vitest" }, "peerDependencies": { - "vitest": "2.1.9" + "vitest": "4.1.7" } }, "node_modules/@vitest/utils": { - "version": "2.1.9", - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-2.1.9.tgz", - "integrity": "sha512-v0psaMSkNJ3A2NMrUEHFRzJtDPFn+/VWZ5WxImB21T9fjucJRmS7xCS3ppEnARb9y11OAzaD+P2Ps+b+BGX5iQ==", + "version": "4.1.7", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.7.tgz", + "integrity": "sha512-T532WBu791cBxJlCl6SO+J14l81DQx6uQHm1bQbmCDY7nqlEIgkza/UFnSBNaUtSf41unldDFjdOBYEQC4b5Hw==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/pretty-format": "2.1.9", - "loupe": "^3.1.2", - "tinyrainbow": "^1.2.0" + "@vitest/pretty-format": "4.1.7", + "convert-source-map": "^2.0.0", + "tinyrainbow": "^3.1.0" }, "funding": { "url": "https://opencollective.com/vitest" @@ -2310,16 +3054,6 @@ "concat-map": "0.0.1" } }, - "node_modules/cac": { - "version": "6.7.14", - "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", - "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, "node_modules/call-bind": { "version": "1.0.9", "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.9.tgz", @@ -2382,18 +3116,11 @@ } }, "node_modules/chai": { - "version": "5.3.3", - "resolved": "https://registry.npmjs.org/chai/-/chai-5.3.3.tgz", - "integrity": "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==", + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz", + "integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==", "dev": true, "license": "MIT", - "dependencies": { - "assertion-error": "^2.0.1", - "check-error": "^2.1.1", - "deep-eql": "^5.0.1", - "loupe": "^3.1.0", - "pathval": "^2.0.0" - }, "engines": { "node": ">=18" } @@ -2464,16 +3191,6 @@ "node": "*" } }, - "node_modules/check-error": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.3.tgz", - "integrity": "sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 16" - } - }, "node_modules/cheerio": { "version": "1.0.0-rc.10", "resolved": "https://registry.npmjs.org/cheerio/-/cheerio-1.0.0-rc.10.tgz", @@ -2573,6 +3290,13 @@ "dev": true, "license": "MIT" }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, "node_modules/copy-anything": { "version": "4.0.5", "resolved": "https://registry.npmjs.org/copy-anything/-/copy-anything-4.0.5.tgz", @@ -2687,16 +3411,6 @@ } } }, - "node_modules/deep-eql": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", - "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, "node_modules/deep-is": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", @@ -2760,6 +3474,16 @@ "node": ">=6" } }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, "node_modules/devlop": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/devlop/-/devlop-1.1.0.tgz", @@ -2935,9 +3659,9 @@ } }, "node_modules/esbuild": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", - "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.0.tgz", + "integrity": "sha512-sNR9MHpXSUV/XB4zmsFKN+QgVG82Cc7+/aaxJ8Adi8hyOac+EXptIp45QBPaVyX3N70664wRbTcLTOemCAnyqw==", "dev": true, "hasInstallScript": true, "license": "MIT", @@ -2948,49 +3672,32 @@ "node": ">=18" }, "optionalDependencies": { - "@esbuild/aix-ppc64": "0.25.12", - "@esbuild/android-arm": "0.25.12", - "@esbuild/android-arm64": "0.25.12", - "@esbuild/android-x64": "0.25.12", - "@esbuild/darwin-arm64": "0.25.12", - "@esbuild/darwin-x64": "0.25.12", - "@esbuild/freebsd-arm64": "0.25.12", - "@esbuild/freebsd-x64": "0.25.12", - "@esbuild/linux-arm": "0.25.12", - "@esbuild/linux-arm64": "0.25.12", - "@esbuild/linux-ia32": "0.25.12", - "@esbuild/linux-loong64": "0.25.12", - "@esbuild/linux-mips64el": "0.25.12", - "@esbuild/linux-ppc64": "0.25.12", - "@esbuild/linux-riscv64": "0.25.12", - "@esbuild/linux-s390x": "0.25.12", - "@esbuild/linux-x64": "0.25.12", - "@esbuild/netbsd-arm64": "0.25.12", - "@esbuild/netbsd-x64": "0.25.12", - "@esbuild/openbsd-arm64": "0.25.12", - "@esbuild/openbsd-x64": "0.25.12", - "@esbuild/openharmony-arm64": "0.25.12", - "@esbuild/sunos-x64": "0.25.12", - "@esbuild/win32-arm64": "0.25.12", - "@esbuild/win32-ia32": "0.25.12", - "@esbuild/win32-x64": "0.25.12" - } - }, - "node_modules/esbuild/node_modules/@esbuild/openharmony-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz", - "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openharmony" - ], - "engines": { - "node": ">=18" + "@esbuild/aix-ppc64": "0.28.0", + "@esbuild/android-arm": "0.28.0", + "@esbuild/android-arm64": "0.28.0", + "@esbuild/android-x64": "0.28.0", + "@esbuild/darwin-arm64": "0.28.0", + "@esbuild/darwin-x64": "0.28.0", + "@esbuild/freebsd-arm64": "0.28.0", + "@esbuild/freebsd-x64": "0.28.0", + "@esbuild/linux-arm": "0.28.0", + "@esbuild/linux-arm64": "0.28.0", + "@esbuild/linux-ia32": "0.28.0", + "@esbuild/linux-loong64": "0.28.0", + "@esbuild/linux-mips64el": "0.28.0", + "@esbuild/linux-ppc64": "0.28.0", + "@esbuild/linux-riscv64": "0.28.0", + "@esbuild/linux-s390x": "0.28.0", + "@esbuild/linux-x64": "0.28.0", + "@esbuild/netbsd-arm64": "0.28.0", + "@esbuild/netbsd-x64": "0.28.0", + "@esbuild/openbsd-arm64": "0.28.0", + "@esbuild/openbsd-x64": "0.28.0", + "@esbuild/openharmony-arm64": "0.28.0", + "@esbuild/sunos-x64": "0.28.0", + "@esbuild/win32-arm64": "0.28.0", + "@esbuild/win32-ia32": "0.28.0", + "@esbuild/win32-x64": "0.28.0" } }, "node_modules/escape-goat": { @@ -3443,14 +4150,15 @@ "license": "ISC" }, "node_modules/inversify": { - "version": "6.1.4", - "resolved": "https://registry.npmjs.org/inversify/-/inversify-6.1.4.tgz", - "integrity": "sha512-PbxrZH/gTa1fpPEEGAjJQzK8tKMIp5gRg6EFNJlCtzUcycuNdmhv3uk5P8Itm/RIjgHJO16oQRLo9IHzQN51bA==", + "version": "7.11.0", + "resolved": "https://registry.npmjs.org/inversify/-/inversify-7.11.0.tgz", + "integrity": "sha512-yZDprSSr8TyVeMGI/AOV4ws6gwjX22hj9Z8/oHAVpJORY6WRFTcUzhnZtibBUHEw2U8ArvHcR+i863DplQ3Cwg==", "dev": true, "license": "MIT", "dependencies": { - "@inversifyjs/common": "1.3.3", - "@inversifyjs/core": "1.3.4" + "@inversifyjs/common": "1.5.2", + "@inversifyjs/container": "1.15.0", + "@inversifyjs/core": "9.2.0" } }, "node_modules/is-arguments": { @@ -3643,9 +4351,9 @@ "license": "ISC" }, "node_modules/javascript-obfuscator": { - "version": "5.4.2", - "resolved": "https://registry.npmjs.org/javascript-obfuscator/-/javascript-obfuscator-5.4.2.tgz", - "integrity": "sha512-VUcjC6IPDuB5vAFVZ7qhGRkyewGWV5p05GuCWr3wwQjAym8icDprqz7B9595pqN6aI8EgyazgogOuac89xIgxw==", + "version": "5.4.3", + "resolved": "https://registry.npmjs.org/javascript-obfuscator/-/javascript-obfuscator-5.4.3.tgz", + "integrity": "sha512-G5WUgh84tJa5jtk49w+nQCxQpXIvp65xhLyaFeTAdQeYqWji+SXo89k96UvYIDqojyWZEWSDKRmaWKK6bBn/2A==", "dev": true, "license": "BSD-2-Clause", "dependencies": { @@ -3663,7 +4371,7 @@ "eslint-scope": "8.4.0", "eslint-visitor-keys": "4.2.1", "fast-deep-equal": "3.1.3", - "inversify": "6.1.4", + "inversify": "7.11.0", "js-string-escape": "1.0.1", "md5": "2.3.0", "multimatch": "5.0.0", @@ -3707,46 +4415,319 @@ "devOptional": true, "license": "Apache-2.0" }, - "node_modules/juice": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/juice/-/juice-8.1.0.tgz", - "integrity": "sha512-FLzurJrx5Iv1e7CfBSZH68dC04EEvXvvVvPYB7Vx1WAuhCp1ZPIMtqxc+WTWxVkpTIC2Ach/GAv0rQbtGf6YMA==", + "node_modules/juice": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/juice/-/juice-8.1.0.tgz", + "integrity": "sha512-FLzurJrx5Iv1e7CfBSZH68dC04EEvXvvVvPYB7Vx1WAuhCp1ZPIMtqxc+WTWxVkpTIC2Ach/GAv0rQbtGf6YMA==", + "dev": true, + "license": "MIT", + "dependencies": { + "cheerio": "1.0.0-rc.10", + "commander": "^6.1.0", + "mensch": "^0.3.4", + "slick": "^1.12.2", + "web-resource-inliner": "^6.0.1" + }, + "bin": { + "juice": "bin/juice" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/levn": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz", + "integrity": "sha512-0OO4y2iOHix2W6ujICbKIaEQXvFQHue65vUG3pb5EUomzPI90z9hsA1VsO/dbIIpC53J8gxM9Q4Oho0jrCM/yA==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "~1.1.2", + "type-check": "~0.3.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/libphonenumber-js": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/libphonenumber-js/-/libphonenumber-js-1.13.2.tgz", + "integrity": "sha512-S3kmBrptp3yRTm83NUcHy9g1vbwiWMzI8WvY22+koBJ6zkRteLnedBL2VX0MIAGwx2yiyxX4J85pceZyQ6ffgg==", + "dev": true, + "license": "MIT" + }, + "node_modules/lightningcss": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz", + "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.32.0", + "lightningcss-darwin-arm64": "1.32.0", + "lightningcss-darwin-x64": "1.32.0", + "lightningcss-freebsd-x64": "1.32.0", + "lightningcss-linux-arm-gnueabihf": "1.32.0", + "lightningcss-linux-arm64-gnu": "1.32.0", + "lightningcss-linux-arm64-musl": "1.32.0", + "lightningcss-linux-x64-gnu": "1.32.0", + "lightningcss-linux-x64-musl": "1.32.0", + "lightningcss-win32-arm64-msvc": "1.32.0", + "lightningcss-win32-x64-msvc": "1.32.0" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz", + "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz", + "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz", + "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz", + "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz", + "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz", + "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz", + "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz", + "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz", + "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==", + "cpu": [ + "x64" + ], "dev": true, - "license": "MIT", - "dependencies": { - "cheerio": "1.0.0-rc.10", - "commander": "^6.1.0", - "mensch": "^0.3.4", - "slick": "^1.12.2", - "web-resource-inliner": "^6.0.1" - }, - "bin": { - "juice": "bin/juice" - }, + "libc": [ + "musl" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=10.0.0" + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/levn": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz", - "integrity": "sha512-0OO4y2iOHix2W6ujICbKIaEQXvFQHue65vUG3pb5EUomzPI90z9hsA1VsO/dbIIpC53J8gxM9Q4Oho0jrCM/yA==", + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz", + "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==", + "cpu": [ + "arm64" + ], "dev": true, - "license": "MIT", - "dependencies": { - "prelude-ls": "~1.1.2", - "type-check": "~0.3.2" - }, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], "engines": { - "node": ">= 0.8.0" + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/libphonenumber-js": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/libphonenumber-js/-/libphonenumber-js-1.13.2.tgz", - "integrity": "sha512-S3kmBrptp3yRTm83NUcHy9g1vbwiWMzI8WvY22+koBJ6zkRteLnedBL2VX0MIAGwx2yiyxX4J85pceZyQ6ffgg==", + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz", + "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==", + "cpu": [ + "x64" + ], "dev": true, - "license": "MIT" + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } }, "node_modules/linkify-it": { "version": "5.0.0", @@ -3758,12 +4739,15 @@ "uc.micro": "^2.0.0" } }, - "node_modules/loupe": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.2.1.tgz", - "integrity": "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==", + "node_modules/load-tsconfig": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/load-tsconfig/-/load-tsconfig-0.2.5.tgz", + "integrity": "sha512-IXO6OCs9yg8tMKzfPZ1YmheJbZCiEsnBdcB03l0OcfK9prKnJb96siuHCr5Fl37/yo9DnKU+TLpxzTUspw9shg==", "dev": true, - "license": "MIT" + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + } }, "node_modules/lunr": { "version": "2.3.9", @@ -4082,9 +5066,9 @@ } }, "node_modules/nanoid": { - "version": "3.3.11", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", - "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "version": "3.3.12", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz", + "integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==", "dev": true, "funding": [ { @@ -4182,6 +5166,17 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/obug": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.1.tgz", + "integrity": "sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==", + "dev": true, + "funding": [ + "https://github.com/sponsors/sxzz", + "https://opencollective.com/debug" + ], + "license": "MIT" + }, "node_modules/oniguruma-to-es": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/oniguruma-to-es/-/oniguruma-to-es-3.1.1.tgz", @@ -4247,22 +5242,12 @@ "license": "MIT" }, "node_modules/pathe": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz", - "integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==", + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", "dev": true, "license": "MIT" }, - "node_modules/pathval": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.1.tgz", - "integrity": "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 14.16" - } - }, "node_modules/perfect-debounce": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/perfect-debounce/-/perfect-debounce-1.0.0.tgz", @@ -4301,9 +5286,9 @@ } }, "node_modules/postcss": { - "version": "8.5.12", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.12.tgz", - "integrity": "sha512-W62t/Se6rA0Az3DfCL0AqJwXuKwBeYg6nOaIgzP+xZ7N5BFCI7DYi1qs6ygUYT6rvfi6t9k65UMLJC+PHZpDAA==", + "version": "8.5.15", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz", + "integrity": "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==", "dev": true, "funding": [ { @@ -4321,7 +5306,7 @@ ], "license": "MIT", "dependencies": { - "nanoid": "^3.3.11", + "nanoid": "^3.3.12", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" }, @@ -4462,6 +5447,40 @@ "dev": true, "license": "MIT" }, + "node_modules/rolldown": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.2.tgz", + "integrity": "sha512-oZx5zVDtVB44AW3eaifgDml1gWRDZGvjcfdxonE4swNPG98PrrXjaO/KrnUjzlMnztCCRVlUueA1kCXhARGk6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@oxc-project/types": "=0.132.0", + "@rolldown/pluginutils": "^1.0.0" + }, + "bin": { + "rolldown": "bin/cli.mjs" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "optionalDependencies": { + "@rolldown/binding-android-arm64": "1.0.2", + "@rolldown/binding-darwin-arm64": "1.0.2", + "@rolldown/binding-darwin-x64": "1.0.2", + "@rolldown/binding-freebsd-x64": "1.0.2", + "@rolldown/binding-linux-arm-gnueabihf": "1.0.2", + "@rolldown/binding-linux-arm64-gnu": "1.0.2", + "@rolldown/binding-linux-arm64-musl": "1.0.2", + "@rolldown/binding-linux-ppc64-gnu": "1.0.2", + "@rolldown/binding-linux-s390x-gnu": "1.0.2", + "@rolldown/binding-linux-x64-gnu": "1.0.2", + "@rolldown/binding-linux-x64-musl": "1.0.2", + "@rolldown/binding-openharmony-arm64": "1.0.2", + "@rolldown/binding-wasm32-wasi": "1.0.2", + "@rolldown/binding-win32-arm64-msvc": "1.0.2", + "@rolldown/binding-win32-x64-msvc": "1.0.2" + } + }, "node_modules/rollup": { "version": "4.60.4", "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.4.tgz", @@ -4760,9 +5779,9 @@ "license": "MIT" }, "node_modules/std-env": { - "version": "3.10.0", - "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", - "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.1.0.tgz", + "integrity": "sha512-Rq7ybcX2RuC55r9oaPVEW7/xu3tj8u4GeBYHBWCychFtzMIr86A7e3PPEBPT37sHStKX3+TiX/Fr/ACmJLVlLQ==", "dev": true, "license": "MIT" }, @@ -4865,11 +5884,14 @@ "license": "MIT" }, "node_modules/tinyexec": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz", - "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==", + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.2.3.tgz", + "integrity": "sha512-g62dB+w1/OEFnPvmX0yd/HnetYITOL+1nJW7kitOycOeAvmbWC/nu0fwmmQ/kupNojqExzyC/T++pST/jRJ2mQ==", "dev": true, - "license": "MIT" + "license": "MIT", + "engines": { + "node": ">=18" + } }, "node_modules/tinyglobby": { "version": "0.2.16", @@ -4888,30 +5910,10 @@ "url": "https://github.com/sponsors/SuperchupuDev" } }, - "node_modules/tinypool": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.1.1.tgz", - "integrity": "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^18.0.0 || >=20.0.0" - } - }, "node_modules/tinyrainbow": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-1.2.0.tgz", - "integrity": "sha512-weEDEq7Z5eTHPDh4xjX789+fHfF+P8boiFB+0vbWzpbnbsEr/GRaohi/uMKxg8RZMXnl1ItAi/IUHWMsjDV7kQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/tinyspy": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-3.0.2.tgz", - "integrity": "sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.1.0.tgz", + "integrity": "sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==", "dev": true, "license": "MIT", "engines": { @@ -4953,9 +5955,9 @@ "license": "0BSD" }, "node_modules/tsx": { - "version": "4.22.0", - "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.22.0.tgz", - "integrity": "sha512-8ccZMPD69s1AbKXx0C5ddTNZfNjwV04iIKgjZmKfKxMynEtSYcK0Lh7iQFh53fI5Yu4pb9usgAiqyPmEONaALg==", + "version": "4.22.3", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.22.3.tgz", + "integrity": "sha512-mdoNxBC/cSQObGGVQ5Bpn5i+yv7j68gk3Nfm3wFjcJg3Z0Mix9jzAFfP12prmm5eVGmDKtp0yyArrs0Q+8gZHg==", "dev": true, "license": "MIT", "dependencies": { @@ -5042,9 +6044,9 @@ } }, "node_modules/typedoc/node_modules/brace-expansion": { - "version": "5.0.5", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz", - "integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==", + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz", + "integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==", "dev": true, "license": "MIT", "dependencies": { @@ -5181,6 +6183,37 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/unplugin": { + "version": "2.3.11", + "resolved": "https://registry.npmjs.org/unplugin/-/unplugin-2.3.11.tgz", + "integrity": "sha512-5uKD0nqiYVzlmCRs01Fhs2BdkEgBS3SAVP6ndrBsuK42iC2+JHyxM05Rm9G8+5mkmRtzMZGY8Ct5+mliZxU/Ww==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/remapping": "^2.3.5", + "acorn": "^8.15.0", + "picomatch": "^4.0.3", + "webpack-virtual-modules": "^0.6.2" + }, + "engines": { + "node": ">=18.12.0" + } + }, + "node_modules/unplugin-swc": { + "version": "1.5.9", + "resolved": "https://registry.npmjs.org/unplugin-swc/-/unplugin-swc-1.5.9.tgz", + "integrity": "sha512-RKwK3yf0M+MN17xZfF14bdKqfx0zMXYdtOdxLiE6jHAoidupKq3jGdJYANyIM1X/VmABhh1WpdO+/f4+Ol89+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rollup/pluginutils": "^5.3.0", + "load-tsconfig": "^0.2.5", + "unplugin": "^2.3.11" + }, + "peerDependencies": { + "@swc/core": "^1.2.108" + } + }, "node_modules/unplugin-utils": { "version": "0.2.5", "resolved": "https://registry.npmjs.org/unplugin-utils/-/unplugin-utils-0.2.5.tgz", @@ -5198,13 +6231,6 @@ "url": "https://github.com/sponsors/sxzz" } }, - "node_modules/unplugin-utils/node_modules/pathe": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", - "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", - "dev": true, - "license": "MIT" - }, "node_modules/util": { "version": "0.12.5", "resolved": "https://registry.npmjs.org/util/-/util-0.12.5.tgz", @@ -5329,29 +6355,6 @@ } } }, - "node_modules/vite-node": { - "version": "2.1.9", - "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-2.1.9.tgz", - "integrity": "sha512-AM9aQ/IPrW/6ENLQg3AGY4K1N2TGZdR5e4gu/MmmR2xR3Ll1+dib+nook92g4TV3PXVyeyxdWwtaCAiUL0hMxA==", - "dev": true, - "license": "MIT", - "dependencies": { - "cac": "^6.7.14", - "debug": "^4.3.7", - "es-module-lexer": "^1.5.4", - "pathe": "^1.1.2", - "vite": "^5.0.0" - }, - "bin": { - "vite-node": "vite-node.mjs" - }, - "engines": { - "node": "^18.0.0 || >=20.0.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, "node_modules/vitepress": { "version": "1.6.4", "resolved": "https://registry.npmjs.org/vitepress/-/vitepress-1.6.4.tgz", @@ -5406,58 +6409,79 @@ } }, "node_modules/vitest": { - "version": "2.1.9", - "resolved": "https://registry.npmjs.org/vitest/-/vitest-2.1.9.tgz", - "integrity": "sha512-MSmPM9REYqDGBI8439mA4mWhV5sKmDlBKWIYbA3lRb2PTHACE0mgKwA8yQ2xq9vxDTuk4iPrECBAEW2aoFXY0Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "@vitest/expect": "2.1.9", - "@vitest/mocker": "2.1.9", - "@vitest/pretty-format": "^2.1.9", - "@vitest/runner": "2.1.9", - "@vitest/snapshot": "2.1.9", - "@vitest/spy": "2.1.9", - "@vitest/utils": "2.1.9", - "chai": "^5.1.2", - "debug": "^4.3.7", - "expect-type": "^1.1.0", - "magic-string": "^0.30.12", - "pathe": "^1.1.2", - "std-env": "^3.8.0", + "version": "4.1.7", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.7.tgz", + "integrity": "sha512-flYyaFd2CgoCoU+0UKt3pxksgC+S02iTDN0n3LtqaMeXsI9SBcdNujc2k0DeFLzUn/0k538yNjOSdwgCqcrwJA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/expect": "4.1.7", + "@vitest/mocker": "4.1.7", + "@vitest/pretty-format": "4.1.7", + "@vitest/runner": "4.1.7", + "@vitest/snapshot": "4.1.7", + "@vitest/spy": "4.1.7", + "@vitest/utils": "4.1.7", + "es-module-lexer": "^2.0.0", + "expect-type": "^1.3.0", + "magic-string": "^0.30.21", + "obug": "^2.1.1", + "pathe": "^2.0.3", + "picomatch": "^4.0.3", + "std-env": "^4.0.0-rc.1", "tinybench": "^2.9.0", - "tinyexec": "^0.3.1", - "tinypool": "^1.0.1", - "tinyrainbow": "^1.2.0", - "vite": "^5.0.0", - "vite-node": "2.1.9", + "tinyexec": "^1.0.2", + "tinyglobby": "^0.2.15", + "tinyrainbow": "^3.1.0", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0", "why-is-node-running": "^2.3.0" }, "bin": { "vitest": "vitest.mjs" }, "engines": { - "node": "^18.0.0 || >=20.0.0" + "node": "^20.0.0 || ^22.0.0 || >=24.0.0" }, "funding": { "url": "https://opencollective.com/vitest" }, "peerDependencies": { "@edge-runtime/vm": "*", - "@types/node": "^18.0.0 || >=20.0.0", - "@vitest/browser": "2.1.9", - "@vitest/ui": "2.1.9", + "@opentelemetry/api": "^1.9.0", + "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", + "@vitest/browser-playwright": "4.1.7", + "@vitest/browser-preview": "4.1.7", + "@vitest/browser-webdriverio": "4.1.7", + "@vitest/coverage-istanbul": "4.1.7", + "@vitest/coverage-v8": "4.1.7", + "@vitest/ui": "4.1.7", "happy-dom": "*", - "jsdom": "*" + "jsdom": "*", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" }, "peerDependenciesMeta": { "@edge-runtime/vm": { "optional": true }, + "@opentelemetry/api": { + "optional": true + }, "@types/node": { "optional": true }, - "@vitest/browser": { + "@vitest/browser-playwright": { + "optional": true + }, + "@vitest/browser-preview": { + "optional": true + }, + "@vitest/browser-webdriverio": { + "optional": true + }, + "@vitest/coverage-istanbul": { + "optional": true + }, + "@vitest/coverage-v8": { "optional": true }, "@vitest/ui": { @@ -5468,6 +6492,121 @@ }, "jsdom": { "optional": true + }, + "vite": { + "optional": false + } + } + }, + "node_modules/vitest/node_modules/@vitest/mocker": { + "version": "4.1.7", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.7.tgz", + "integrity": "sha512-vY7nuamKgfvpA1Koa3oYIw/k7D6kZnpGyNMZW8loow2bsBYla1TFdqTaXncWdRn4pgwNs+90RhnXhJScDwQeJA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "4.1.7", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.21" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/vitest/node_modules/es-module-lexer": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.1.0.tgz", + "integrity": "sha512-n27zTYMjYu1aj4MjCWzSP7G9r75utsaoc8m61weK+W8JMBGGQybd43GstCXZ3WNmSFtGT9wi59qQTW6mhTR5LQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/vitest/node_modules/vite": { + "version": "8.0.14", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.14.tgz", + "integrity": "sha512-s4BJJ+5y1pYL6Otw51FHhVJQhPnuRinKig64g/1+EUNaJsd3gCKdD31IPFvswUgW9/60QT9oFHbZHbQK5imcxw==", + "dev": true, + "license": "MIT", + "dependencies": { + "lightningcss": "^1.32.0", + "picomatch": "^4.0.4", + "postcss": "^8.5.15", + "rolldown": "1.0.2", + "tinyglobby": "^0.2.16" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "@vitejs/devtools": "^0.1.18", + "esbuild": "^0.27.0 || ^0.28.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "@vitejs/devtools": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true } } }, @@ -5560,6 +6699,13 @@ "dev": true, "license": "BSD-2-Clause" }, + "node_modules/webpack-virtual-modules": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/webpack-virtual-modules/-/webpack-virtual-modules-0.6.2.tgz", + "integrity": "sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==", + "dev": true, + "license": "MIT" + }, "node_modules/whatwg-url": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", @@ -5672,7 +6818,7 @@ }, "packages/library": { "name": "@magmacomputing/library", - "version": "2.12.0", + "version": "3.0.0", "license": "MIT", "dependencies": { "tslib": "^2.8.1" @@ -5683,17 +6829,16 @@ }, "packages/tempo": { "name": "@magmacomputing/tempo", - "version": "2.12.0", + "version": "3.0.0", "license": "MIT", "dependencies": { "tslib": "^2.8.1" }, "devDependencies": { "@js-temporal/polyfill": "^0.5.1", - "@magmacomputing/library": "*", + "@magmacomputing/library": "3.0.0", "@rollup/plugin-alias": "^6.0.0", - "esbuild": "^0.25.12", - "javascript-obfuscator": "^5.4.2", + "javascript-obfuscator": "^5.4.3", "magic-string": "^0.30.21", "rollup-plugin-esbuild": "^6.2.1", "typedoc": "^0.28.19", diff --git a/package.json b/package.json index 934e23da..ca91a359 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "tempo-monorepo", - "version": "2.11.2", + "version": "3.0.0", "private": true, "description": "Magma Computing Monorepo", "repository": { @@ -22,25 +22,31 @@ "docs:dev": "npm run docs:dev --workspace=@magmacomputing/tempo", "docs:build": "npm run docs:build --workspace=@magmacomputing/tempo", "docs:preview": "npm run docs:preview --workspace=@magmacomputing/tempo", + "docs:push": "npm run docs:push --workspace=@magmacomputing/tempo", "test:dist": "npm run build:library && npm run build:tempo && cross-env TEST_DIST=true vitest run" }, "devDependencies": { "@js-temporal/polyfill": "^0.5.1", "@rollup/plugin-node-resolve": "^16.0.3", - "@types/google.maps": "^3.64.1", + "@types/google.maps": "^3.65.0", "@types/hammerjs": "^2.0.46", "@types/jquery": "^4.0.0", - "@types/node": "^25.8.0", - "@vitest/ui": "^2.1.9", + "@types/node": "^25.9.1", + "@vitest/ui": "^4.1.7", "cross-env": "^10.1.0", "markdown-it-mathjax3": "^4.3.2", "rollup": "^4.60.4", "tslib": "^2.8.1", - "tsx": "^4.22.0", + "tsx": "^4.22.3", "typescript": "^6.0.3", - "vitest": "^2.1.9" + "unplugin-swc": "^1.5.9", + "vitest": "^4.1.7" }, "overrides": { - "esbuild": "^0.25.0" + "esbuild": "^0.28.0" + }, + "allowScripts": { + "esbuild": true, + "@swc/core": true } } \ No newline at end of file diff --git a/packages/library/CHANGELOG.md b/packages/library/CHANGELOG.md index 609c2304..1b3c5a26 100644 --- a/packages/library/CHANGELOG.md +++ b/packages/library/CHANGELOG.md @@ -5,6 +5,11 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [3.0.0] - 2026-06-07 + +### Added +- **Native Cryptography & Buffers (`cipher`, `webtoken`, `buffer`)**: Completely overhauled and consolidated the cryptographic primitives and buffer management into tree-shakeable functions (`cipher.library.js`, `webtoken.library.js`, and `buffer.library.js`). Replaced legacy bit-shifting polyfills with blazingly fast native implementations (`TextEncoder`, `TextDecoder`, and native Base64 runtime bindings). Established a strict unidirectional dependency graph (`serialize` ➡️ `buffer` ➡️ `cipher`) and removed redundant exports, providing a highly optimized, zero-dependency native JWS/JWT validation suite across the ecosystem. + ## [2.11.0] - 2026-05-25 ### Added diff --git a/packages/library/doc/browser/types.md b/packages/library/doc/browser/types.md index 306999b9..883ab5fa 100644 --- a/packages/library/doc/browser/types.md +++ b/packages/library/doc/browser/types.md @@ -26,7 +26,7 @@ A convenience type for registering events: `[Tapper.EVENT, Tapper.Callback]`. ### `MapOpts` Options for mapping functions: - `catch?: boolean` (Interprets Promise reject as resolve) -- `debug?: boolean` (Enables logging) +- `debug?: number` (A non-negative integer determining verbosity scale: `0` = no debug output, larger values increase verbosity: `1` = errors, `2` = warnings, `3` = info, `4` = verbose, `5` = trace. Expected range is `0–5`. Default is `0`. Example usage: `{ debug: 0 }` for silent, `{ debug: 5 }` for maximum diagnostics. Replaces the older boolean `debug` flag.) ### `MapStore` Internal interface for cached geolocation and geocoder results. diff --git a/packages/library/package.json b/packages/library/package.json index 0857b9c0..788bdde5 100644 --- a/packages/library/package.json +++ b/packages/library/package.json @@ -1,6 +1,6 @@ { "name": "@magmacomputing/library", - "version": "2.11.2", + "version": "3.0.0", "description": "Shared utility library for Tempo", "author": "Magma Computing Solutions", "license": "MIT", diff --git a/packages/library/src/browser/mapper.library.ts b/packages/library/src/browser/mapper.library.ts index 0ad3618a..b44530f3 100644 --- a/packages/library/src/browser/mapper.library.ts +++ b/packages/library/src/browser/mapper.library.ts @@ -3,15 +3,16 @@ import { CONTEXT, getContext } from '#library/utility.library.js'; import { isNullish } from '#library/assertion.library.js'; import { instant } from '#library/temporal.library.js'; import { getHemisphere } from '#library/international.library.js'; +import type { DebugLevel } from '#library/logger.class.js'; -import { Logify } from '#library/logify.class.js'; +import { Logger } from '#library/logger.class.js'; import type { WebStore } from '#browser/webstore.class.js'; // Various functions to allow geolocating a user-device via the browser. interface MapOpts { catch?: boolean; // intercept Promise reject() as resolve() (default: true) - debug?: boolean; // console.log some checkpoints + debug?: DebugLevel; // console.log some checkpoints } /** @@ -23,11 +24,11 @@ interface MapStore { // a localStorage object georesponse: google.maps.GeocoderResponse & { error?: Error["message"] }; } -const defaults = { catch: true, debug: false } as MapOpts; // default Options -const context = getContext(); // browser / nodejs / google-apps +const defaults = { catch: true, debug: 0 } as MapOpts; // default Options +const context = getContext(); // browser / nodejs / google-apps const mapStore = {} as MapStore; // static object to hold last position const MAP_KEY = '_map_'; // localStorage key -const log = new Logify('Mapper'); +const log = new Logger('[Mapper]'); const store = await new Promise((resolve, reject) => { if (context.type === CONTEXT.Browser) { @@ -35,7 +36,7 @@ const store = await new Promise((resolve, reject) => { .then(({ WebStore }) => { const local = new WebStore('local'); Object.assign(mapStore, local.get(MAP_KEY, {})); // fetch the previous MAP_KEY coordinates - resolve(local); // localStorage wrapper + resolve(local); // localStorage wrapper }) .catch(reject) } @@ -127,7 +128,7 @@ export const mapQuery = (coords?: google.maps.GeocoderRequest, opts = {} as MapO if (test1 && test2) { // if we already have geocoder if (opts.debug) - console.log('mapQuery: cache'); + log.debug(opts, 'mapQuery: cache'); return resolve(mapStore.georesponse!); // return previous geocoder } } // drop through to default: @@ -143,8 +144,10 @@ export const mapQuery = (coords?: google.maps.GeocoderRequest, opts = {} as MapO }) }) .finally(() => { - if (opts.debug) - console[mapStore.georesponse?.error ? 'error' : 'log']('mapQuery: ', mapStore.georesponse); + if (opts.debug) { + const fn = mapStore.georesponse?.error ? log.error : log.debug; + fn(opts, 'mapQuery: ', mapStore.georesponse); + } store?.set(MAP_KEY, mapStore); // stash current georesponse to localStorage }) @@ -160,7 +163,7 @@ export const mapHemisphere = (coords?: google.maps.GeocoderRequest, opts = {} as if (isNullish(response.error)) { // useable GeocoderResult detected if (opts.debug) - console.log('sphere: ', response); + log.debug(opts, 'sphere: ', response); return response.results[0].geometry.location.lat() >= 0 ? 'north' : 'south'; } @@ -173,7 +176,7 @@ export const mapHemisphere = (coords?: google.maps.GeocoderRequest, opts = {} as }) .catch((error) => { // cannot query coordinates if (opts.debug) - console.warn('mapHemisphere: ', error.message); + log.warn(opts, 'mapHemisphere: ', error.message); if (opts.catch === false) throw error; return null; @@ -200,7 +203,7 @@ export const mapAddress = (coords?: google.maps.GeocoderRequest, opts = {} as Ma opts = Object.assign({}, defaults, opts); if (opts.debug) - console.warn('mapAddress: ', error.message); + log.warn(opts, 'mapAddress: ', error.message); if (opts.catch === false) throw error; return null; diff --git a/packages/library/src/browser/webstore.class.ts b/packages/library/src/browser/webstore.class.ts index 25fbeca8..8ef3ce4b 100644 --- a/packages/library/src/browser/webstore.class.ts +++ b/packages/library/src/browser/webstore.class.ts @@ -15,17 +15,17 @@ type STORAGE = ValueOf * Refactored for lazy-initialization to ensure side-effect free imports. */ export class WebStore { - static #localInstance?: WebStore; - static #sessionInstance?: WebStore; + private static _localInstance?: WebStore; + private static _sessionInstance?: WebStore; /** Lazy getter for localStorage wrapper */ static get local() { - return WebStore.#localInstance ??= new WebStore(STORAGE.Local); + return WebStore._localInstance ??= new WebStore(STORAGE.Local); } /** Lazy getter for sessionStorage wrapper */ static get session() { - return WebStore.#sessionInstance ??= new WebStore(STORAGE.Session); + return WebStore._sessionInstance ??= new WebStore(STORAGE.Session); } #type: STORAGE; diff --git a/packages/library/src/common.index.ts b/packages/library/src/common.index.ts index 708a9afe..7d91b9cb 100644 --- a/packages/library/src/common.index.ts +++ b/packages/library/src/common.index.ts @@ -4,14 +4,15 @@ export * from './common/array.library.js'; export * from './common/assertion.library.js'; +export * from './common/boundary.library.js'; export * from './common/buffer.library.js'; -export * from './common/cipher.class.js'; +export * from './common/cipher.library.js'; export * from './common/class.library.js'; export * from './common/coercion.library.js'; export * from './common/enumerate.library.js'; export * from './common/function.library.js'; export * from './common/international.library.js'; -export * from './common/logify.class.js'; +export * from './common/logger.class.js'; export * from './common/number.library.js'; export * from './common/object.library.js'; export * from './common/pledge.class.js'; @@ -25,3 +26,4 @@ export * from './common/type.library.js'; export * from './common/temporal.polyfill.js'; export * from './common/temporal.library.js'; export * from './common/utility.library.js'; +export * from './common/webtoken.library.js'; diff --git a/packages/library/src/common/array.library.ts b/packages/library/src/common/array.library.ts index c6db5340..dbdf3a5a 100644 --- a/packages/library/src/common/array.library.ts +++ b/packages/library/src/common/array.library.ts @@ -54,7 +54,7 @@ export function sortBy>(...keys: (PropertyKey | SortBy)[]) switch (true) { case isNumber(valueA) && isNumber(valueB): case isDate(valueA) && isDate(valueB): - case isObject(valueA) && isObject(valueB) && typeof valueA.valueOf() === 'number' && typeof valueB.valueOf() === 'number': + case isObject(valueA) && isObject(valueB) && isNumber(valueA.valueOf()) && isNumber(valueB.valueOf()): result = (dir as any) * ((valueA as any) - (valueB as any)); break; diff --git a/packages/library/src/common/assertion.library.ts b/packages/library/src/common/assertion.library.ts index 238c5995..3cd57750 100644 --- a/packages/library/src/common/assertion.library.ts +++ b/packages/library/src/common/assertion.library.ts @@ -52,9 +52,9 @@ export const isFunction = (obj: unknown): obj is Function => isType(ob export const isPromise = (obj: unknown): obj is Promise => isType>(obj, 'Promise'); export const isMap = (obj: unknown): obj is Map => isType>(obj, 'Map'); export const isSet = (obj: unknown): obj is Set => isType>(obj, 'Set'); -export const isError = (err: unknown): err is Error => isType(err, 'Error'); +export const isError = (err: unknown, constructor?: new (...args: any[]) => E): err is E => isType(err, 'Error') && (constructor ? err instanceof constructor : true); -export const isTemporal = (obj: unknown): obj is Temporals => protoType(obj).startsWith('Temporal.') || (!!(globalThis as any).Temporal && ( +export const isTemporal = (obj: unknown): obj is Temporals => protoType(obj).startsWith('Temporal.') || (isDefined((globalThis as any).Temporal) && ( (obj as any) instanceof (globalThis as any).Temporal.Instant || (obj as any) instanceof (globalThis as any).Temporal.ZonedDateTime || (obj as any) instanceof (globalThis as any).Temporal.PlainDate || @@ -65,12 +65,12 @@ export const isTemporal = (obj: unknown): obj is Temporals => protoType(obj).sta (obj as any) instanceof (globalThis as any).Temporal.PlainMonthDay )); -export const isInstant = (obj: unknown): obj is Temporal.Instant => isType(obj, 'Temporal.Instant') || (!!(globalThis as any).Temporal?.Instant && (obj as any) instanceof (globalThis as any).Temporal.Instant) || (!!obj && (obj as any)[Symbol.toStringTag] === 'Temporal.Instant') || (!!obj && typeof (obj as any).toZonedDateTimeISO === 'function' && isUndefined((obj as any).timeZoneId) && isUndefined((obj as any).timeZone)); -export const isZonedDateTime = (obj: unknown): obj is Temporal.ZonedDateTime => isType(obj, 'Temporal.ZonedDateTime') || (!!(globalThis as any).Temporal?.ZonedDateTime && (obj as any) instanceof (globalThis as any).Temporal.ZonedDateTime) || (!!obj && (obj as any)[Symbol.toStringTag] === 'Temporal.ZonedDateTime') || (!!obj && typeof (obj as any).toInstant === 'function' && (isDefined((obj as any).timeZoneId) || isDefined((obj as any).timeZone))); -export const isPlainDate = (obj: unknown): obj is Temporal.PlainDate => isType(obj, 'Temporal.PlainDate') || (!!(globalThis as any).Temporal?.PlainDate && (obj as any) instanceof (globalThis as any).Temporal.PlainDate) || (!!obj && (obj as any)[Symbol.toStringTag] === 'Temporal.PlainDate') || (!!obj && typeof (obj as any).toZonedDateTime === 'function' && isUndefined((obj as any).timeZoneId) && isUndefined((obj as any).timeZone) && isDefined((obj as any).daysInMonth) && isUndefined((obj as any).hour) && isUndefined((obj as any).minute) && isUndefined((obj as any).second) && isUndefined((obj as any).nanosecond)); -export const isPlainTime = (obj: unknown): obj is Temporal.PlainTime => isType(obj, 'Temporal.PlainTime') || (!!(globalThis as any).Temporal?.PlainTime && (obj as any) instanceof (globalThis as any).Temporal.PlainTime) || (!!obj && (obj as any)[Symbol.toStringTag] === 'Temporal.PlainTime') || (!!obj && typeof (obj as any).toPlainDateTime === 'function' && isUndefined((obj as any).daysInMonth)); -export const isPlainDateTime = (obj: unknown): obj is Temporal.PlainDateTime => isType(obj, 'Temporal.PlainDateTime') || (!!(globalThis as any).Temporal?.PlainDateTime && (obj as any) instanceof (globalThis as any).Temporal.PlainDateTime) || (!!obj && (obj as any)[Symbol.toStringTag] === 'Temporal.PlainDateTime') || (!!obj && typeof (obj as any).toZonedDateTime === 'function' && isUndefined((obj as any).timeZoneId) && isUndefined((obj as any).timeZone) && (isDefined((obj as any).hour) || isDefined((obj as any).minute) || isDefined((obj as any).second) || isDefined((obj as any).nanosecond))); -export const isDuration = (obj: unknown): obj is Temporal.Duration => isType(obj, 'Temporal.Duration') || (!!(globalThis as any).Temporal?.Duration && (obj as any) instanceof (globalThis as any).Temporal.Duration) || (!!obj && (obj as any)[Symbol.toStringTag] === 'Temporal.Duration'); +export const isInstant = (obj: unknown): obj is Temporal.Instant => isType(obj, 'Temporal.Instant') || (isDefined((globalThis as any).Temporal?.Instant) && (obj as any) instanceof (globalThis as any).Temporal.Instant) || (isDefined(obj) && (obj as any)[Symbol.toStringTag] === 'Temporal.Instant') || (isDefined(obj) && isFunction((obj as any).toZonedDateTimeISO) && isUndefined((obj as any).timeZoneId) && isUndefined((obj as any).timeZone)); +export const isZonedDateTime = (obj: unknown): obj is Temporal.ZonedDateTime => isType(obj, 'Temporal.ZonedDateTime') || (isDefined((globalThis as any).Temporal?.ZonedDateTime) && (obj as any) instanceof (globalThis as any).Temporal.ZonedDateTime) || (isDefined(obj) && (obj as any)[Symbol.toStringTag] === 'Temporal.ZonedDateTime') || (isDefined(obj) && isFunction((obj as any).toInstant) && (isDefined((obj as any).timeZoneId) || isDefined((obj as any).timeZone))); +export const isPlainDate = (obj: unknown): obj is Temporal.PlainDate => isType(obj, 'Temporal.PlainDate') || (isDefined((globalThis as any).Temporal?.PlainDate) && (obj as any) instanceof (globalThis as any).Temporal.PlainDate) || (isDefined(obj) && (obj as any)[Symbol.toStringTag] === 'Temporal.PlainDate') || (isDefined(obj) && isFunction((obj as any).toZonedDateTime) && isUndefined((obj as any).timeZoneId) && isUndefined((obj as any).timeZone) && isDefined((obj as any).daysInMonth) && isUndefined((obj as any).hour) && isUndefined((obj as any).minute) && isUndefined((obj as any).second) && isUndefined((obj as any).nanosecond)); +export const isPlainTime = (obj: unknown): obj is Temporal.PlainTime => isType(obj, 'Temporal.PlainTime') || (isDefined((globalThis as any).Temporal?.PlainTime) && (obj as any) instanceof (globalThis as any).Temporal.PlainTime) || (isDefined(obj) && (obj as any)[Symbol.toStringTag] === 'Temporal.PlainTime') || (isDefined(obj) && isFunction((obj as any).toPlainDateTime) && isUndefined((obj as any).daysInMonth)); +export const isPlainDateTime = (obj: unknown): obj is Temporal.PlainDateTime => isType(obj, 'Temporal.PlainDateTime') || (isDefined((globalThis as any).Temporal?.PlainDateTime) && (obj as any) instanceof (globalThis as any).Temporal.PlainDateTime) || (isDefined(obj) && (obj as any)[Symbol.toStringTag] === 'Temporal.PlainDateTime') || (isDefined(obj) && isFunction((obj as any).toZonedDateTime) && isUndefined((obj as any).timeZoneId) && isUndefined((obj as any).timeZone) && (isDefined((obj as any).hour) || isDefined((obj as any).minute) || isDefined((obj as any).second) || isDefined((obj as any).nanosecond))); +export const isDuration = (obj: unknown): obj is Temporal.Duration => isType(obj, 'Temporal.Duration') || (isDefined((globalThis as any).Temporal?.Duration) && (obj as any) instanceof (globalThis as any).Temporal.Duration) || (isDefined(obj) && (obj as any)[Symbol.toStringTag] === 'Temporal.Duration'); export const isDurationLike = (obj: unknown): obj is Temporal.DurationLike | string | Temporal.Duration => isString(obj) || isDuration(obj) || (isObject(obj) && ( 'years' in obj || 'months' in obj || 'weeks' in obj || 'days' in obj || 'hours' in obj || 'minutes' in obj || 'seconds' in obj || @@ -80,16 +80,16 @@ export const isZonedDateTimeLike = (obj: unknown): obj is Temporal.ZonedDateTime 'year' in obj || 'month' in obj || 'day' in obj || 'hour' in obj || 'minute' in obj || 'second' in obj || 'millisecond' in obj || 'microsecond' in obj || 'nanosecond' in obj || 'monthCode' in obj || 'offset' in obj || 'timeZone' in obj || 'calendar' in obj )); -export const isPlainYearMonth = (obj: unknown): obj is Temporal.PlainYearMonth => isType(obj, 'Temporal.PlainYearMonth') || (!!(globalThis as any).Temporal?.PlainYearMonth && (obj as any) instanceof (globalThis as any).Temporal.PlainYearMonth); -export const isPlainMonthDay = (obj: unknown): obj is Temporal.PlainMonthDay => isType(obj, 'Temporal.PlainMonthDay') || (!!(globalThis as any).Temporal?.PlainMonthDay && (obj as any) instanceof (globalThis as any).Temporal.PlainMonthDay); +export const isPlainYearMonth = (obj: unknown): obj is Temporal.PlainYearMonth => isType(obj, 'Temporal.PlainYearMonth') || (isDefined((globalThis as any).Temporal?.PlainYearMonth) && (obj as any) instanceof (globalThis as any).Temporal.PlainYearMonth); +export const isPlainMonthDay = (obj: unknown): obj is Temporal.PlainMonthDay => isType(obj, 'Temporal.PlainMonthDay') || (isDefined((globalThis as any).Temporal?.PlainMonthDay) && (obj as any) instanceof (globalThis as any).Temporal.PlainMonthDay); // non-standard Objects export const isEnum = >(obj: unknown): obj is GetType<'Enumify', E> => isType>(obj, 'Enumify'); export const isPledge =

(obj: unknown): obj is GetType<'Pledge', P> => isType>(obj, 'Pledge'); /** assert value for secure() */ -export const isExtensible = (obj: any): obj is any => !!(obj?.[sym.$Extensible]); -export const isTarget = (obj: any): obj is any => !!(obj?.[sym.$Target]); +export const isExtensible = (obj: any): obj is any => isDefined(obj?.[sym.$Extensible]); +export const isTarget = (obj: any): obj is any => isDefined(obj?.[sym.$Target]); /** object has no values */ export const isEmpty = (obj?: T) => false diff --git a/packages/library/src/common/boundary.library.ts b/packages/library/src/common/boundary.library.ts new file mode 100644 index 00000000..581af3ad --- /dev/null +++ b/packages/library/src/common/boundary.library.ts @@ -0,0 +1,43 @@ +import { isString } from './assertion.library.js'; +import type { Logger } from './logger.class.js'; + +export interface BoundaryContext { + /** + * If true, errors will be caught, logged, and execution will return gracefully. + * If false, errors will be immediately thrown. + */ + catch?: boolean | undefined; + + /** + * If true, suppresses telemetry/logging for `raise()` regardless of whether the error is rethrown or swallowed. + */ + silent?: boolean | undefined; + + /** + * The namespaced logger to use for outputting the caught error. + */ + logger?: Logger | null | undefined; +} + +/** + * Global Error Boundary Utility. + * Decouples the decision to throw an error from the act of logging it. + */ +export function raise(err: Error | string, context: BoundaryContext = {}): void { + const error = isString(err) ? new Error(err) : err; + + // 1. Output the error telemetry + if (!context.silent) { + if (context.logger) { + context.logger.error(error); + } else { + console.error('[Boundary]', error); + } + } + + // 2. Control flow + if (context.catch) + return; // gracefully swallow the error + + throw error; +} diff --git a/packages/library/src/common/buffer.library.ts b/packages/library/src/common/buffer.library.ts index a11f50ee..7447570b 100644 --- a/packages/library/src/common/buffer.library.ts +++ b/packages/library/src/common/buffer.library.ts @@ -1,214 +1,50 @@ -"use strict"; +import { stringify, objectify } from '#library/serialize.library.js'; -// https://developer.mozilla.org/en-US/docs/Glossary/Base64 +const CHUNK_SIZE = 8192; -// Array of bytes to Base64 string decoding -export function b64ToUint6(nChr: number) { - return nChr > 64 && nChr < 91 - ? nChr - 65 - : nChr > 96 && nChr < 123 - ? nChr - 71 - : nChr > 47 && nChr < 58 - ? nChr + 4 - : nChr === 43 - ? 62 - : nChr === 47 - ? 63 - : 0; -} +/** serialize any object and encode string into a Uint8Array */ +export const encodeBuffer = (str: any) => new TextEncoder().encode(stringify(str)); -export function base64DecToArr(sBase64: string, nBlocksSize?: number) { - const sB64Enc = sBase64.replace(/[^A-Za-z0-9+/]/g, ""); - const nInLen = sB64Enc.length; - const nOutLen = nBlocksSize - ? Math.ceil(((nInLen * 3 + 1) >> 2) / nBlocksSize) * nBlocksSize - : (nInLen * 3 + 1) >> 2; - const taBytes = new Uint8Array(nOutLen); +/** decode a Uint8Array back to a string */ +export const decodeBuffer = (buf: Uint8Array | ArrayBuffer, encoding = 'utf-8') => new TextDecoder(encoding).decode(buf); - let nMod3; - let nMod4; - let nUint24 = 0; - let nOutIdx = 0; - for (let nInIdx = 0; nInIdx < nInLen; nInIdx++) { - nMod4 = nInIdx & 3; - nUint24 |= b64ToUint6(sB64Enc.charCodeAt(nInIdx)) << (6 * (3 - nMod4)); - if (nMod4 === 3 || nInLen - nInIdx === 1) { - nMod3 = 0; - while (nMod3 < 3 && nOutIdx < nOutLen) { - taBytes[nOutIdx] = (nUint24 >>> ((16 >>> nMod3) & 24)) & 255; - nMod3++; - nOutIdx++; - } - nUint24 = 0; - } - } +/** encode a raw Uint8Array into a Base64 string natively */ +export const bufferToBase64 = (buffer: Uint8Array) => { + if (typeof Buffer !== 'undefined') + return Buffer.from(buffer).toString('base64'); - return taBytes; -} + let binary = ''; + for (let i = 0; i < buffer.length; i += CHUNK_SIZE) + binary += String.fromCharCode.apply(null, buffer.subarray(i, i + CHUNK_SIZE) as unknown as number[]); -/* Base64 string to array encoding */ -export function uint6ToB64(nUint6: number) { - return nUint6 < 26 - ? nUint6 + 65 - : nUint6 < 52 - ? nUint6 + 71 - : nUint6 < 62 - ? nUint6 - 4 - : nUint6 === 62 - ? 43 - : nUint6 === 63 - ? 47 - : 65; + return btoa(binary); } -export function base64EncArr(aBytes: Uint8Array) { - let nMod3 = 2; - let sB64Enc = ""; +/** decode a Base64 string into a raw Uint8Array natively */ +export const base64ToBuffer = (base64: string) => { + if (typeof Buffer !== 'undefined') + return new Uint8Array(Buffer.from(base64, 'base64')); - const nLen = aBytes.length; - let nUint24 = 0; - for (let nIdx = 0; nIdx < nLen; nIdx++) { - nMod3 = nIdx % 3; - if (nIdx > 0 && ((nIdx * 4) / 3) % 76 === 0) { - sB64Enc += "\r\n"; - } - - nUint24 |= aBytes[nIdx] << ((16 >>> nMod3) & 24); - if (nMod3 === 2 || aBytes.length - nIdx === 1) { - sB64Enc += String.fromCodePoint( - uint6ToB64((nUint24 >>> 18) & 63), - uint6ToB64((nUint24 >>> 12) & 63), - uint6ToB64((nUint24 >>> 6) & 63), - uint6ToB64(nUint24 & 63) - ); - nUint24 = 0; - } - } - return ( - sB64Enc.substring(0, sB64Enc.length - 2 + nMod3) + - (nMod3 === 2 ? "" : nMod3 === 1 ? "=" : "==") - ); -} + const binary = atob(base64); + const bytes = new Uint8Array(binary.length); -/* UTF-8 array to JS string and vice versa */ + for (let i = 0; i < binary.length; i++) + bytes[i] = binary.charCodeAt(i); -export function UTF8ArrToStr(aBytes: Uint8Array) { - let sView = ""; - let nPart; - const nLen = aBytes.length; - for (let nIdx = 0; nIdx < nLen; nIdx++) { - nPart = aBytes[nIdx]; - sView += String.fromCodePoint( - nPart > 251 && nPart < 254 && nIdx + 5 < nLen /* six bytes */ - ? /* (nPart - 252 << 30) may be not so safe in ECMAScript! So…: */ - (nPart - 252) * 1073741824 + - ((aBytes[++nIdx] - 128) << 24) + - ((aBytes[++nIdx] - 128) << 18) + - ((aBytes[++nIdx] - 128) << 12) + - ((aBytes[++nIdx] - 128) << 6) + - aBytes[++nIdx] - - 128 - : nPart > 247 && nPart < 252 && nIdx + 4 < nLen /* five bytes */ - ? ((nPart - 248) << 24) + - ((aBytes[++nIdx] - 128) << 18) + - ((aBytes[++nIdx] - 128) << 12) + - ((aBytes[++nIdx] - 128) << 6) + - aBytes[++nIdx] - - 128 - : nPart > 239 && nPart < 248 && nIdx + 3 < nLen /* four bytes */ - ? ((nPart - 240) << 18) + - ((aBytes[++nIdx] - 128) << 12) + - ((aBytes[++nIdx] - 128) << 6) + - aBytes[++nIdx] - - 128 - : nPart > 223 && nPart < 240 && nIdx + 2 < nLen /* three bytes */ - ? ((nPart - 224) << 12) + - ((aBytes[++nIdx] - 128) << 6) + - aBytes[++nIdx] - - 128 - : nPart > 191 && nPart < 224 && nIdx + 1 < nLen /* two bytes */ - ? ((nPart - 192) << 6) + aBytes[++nIdx] - 128 - : /* nPart < 127 ? */ /* one byte */ - nPart - ); - } - return sView; + return bytes; } -export function strToUTF8Arr(sDOMStr: string) { - let aBytes; - let nChr: number; - const nStrLen = sDOMStr.length; - let nArrLen = 0; +/** serialize any object and encode it to Base64 */ +export const encodeBase64 = (input: unknown): string => { + const str = stringify(input); - /* mapping… */ - for (let nMapIdx = 0; nMapIdx < nStrLen; nMapIdx++) { - nChr = sDOMStr.codePointAt(nMapIdx)!; - - if (nChr > 65536) { - nMapIdx++; - } - - nArrLen += - nChr < 0x80 - ? 1 - : nChr < 0x800 - ? 2 - : nChr < 0x10000 - ? 3 - : nChr < 0x200000 - ? 4 - : nChr < 0x4000000 - ? 5 - : 6; - } - - aBytes = new Uint8Array(nArrLen); + return bufferToBase64(encodeBuffer(str)); +} - /* transcription… */ - let nIdx = 0; - let nChrIdx = 0; - while (nIdx < nArrLen) { - nChr = sDOMStr.codePointAt(nChrIdx)!; - if (nChr < 128) { - /* one byte */ - aBytes[nIdx++] = nChr; - } else if (nChr < 0x800) { - /* two bytes */ - aBytes[nIdx++] = 192 + (nChr >>> 6); - aBytes[nIdx++] = 128 + (nChr & 63); - } else if (nChr < 0x10000) { - /* three bytes */ - aBytes[nIdx++] = 224 + (nChr >>> 12); - aBytes[nIdx++] = 128 + ((nChr >>> 6) & 63); - aBytes[nIdx++] = 128 + (nChr & 63); - } else if (nChr < 0x200000) { - /* four bytes */ - aBytes[nIdx++] = 240 + (nChr >>> 18); - aBytes[nIdx++] = 128 + ((nChr >>> 12) & 63); - aBytes[nIdx++] = 128 + ((nChr >>> 6) & 63); - aBytes[nIdx++] = 128 + (nChr & 63); - nChrIdx++; - } else if (nChr < 0x4000000) { - /* five bytes */ - aBytes[nIdx++] = 248 + (nChr >>> 24); - aBytes[nIdx++] = 128 + ((nChr >>> 18) & 63); - aBytes[nIdx++] = 128 + ((nChr >>> 12) & 63); - aBytes[nIdx++] = 128 + ((nChr >>> 6) & 63); - aBytes[nIdx++] = 128 + (nChr & 63); - nChrIdx++; - } /* if (nChr <= 0x7fffffff) */ else { - /* six bytes */ - aBytes[nIdx++] = 252 + (nChr >>> 30); - aBytes[nIdx++] = 128 + ((nChr >>> 24) & 63); - aBytes[nIdx++] = 128 + ((nChr >>> 18) & 63); - aBytes[nIdx++] = 128 + ((nChr >>> 12) & 63); - aBytes[nIdx++] = 128 + ((nChr >>> 6) & 63); - aBytes[nIdx++] = 128 + (nChr & 63); - nChrIdx++; - } - nChrIdx++; - } +/** decode a Base64 string and deserialize it back into an object */ +export const decodeBase64 = (base64 = ''): T => { + const uint8 = base64ToBuffer(base64); + const str = decodeBuffer(uint8); - return aBytes; + return objectify(str); } diff --git a/packages/library/src/common/cipher.class.ts b/packages/library/src/common/cipher.class.ts deleted file mode 100644 index 10e224ee..00000000 --- a/packages/library/src/common/cipher.class.ts +++ /dev/null @@ -1,93 +0,0 @@ -import { toHex } from '#library/number.library.js'; -import { asString } from '#library/coercion.library.js'; -import { Immutable, Static } from '#library/class.library.js'; -import { stringify, objectify } from '#library/serialize.library.js'; -import { base64DecToArr, base64EncArr, strToUTF8Arr, UTF8ArrToStr } from '#library/buffer.library.js'; - -const crypto = globalThis.crypto; -const subtle = crypto.subtle; -const keys = { - Algorithm: 'SHA-256', - Encoding: 'utf-8', - SignKey: 'RSASSA-PKCS1-v1_5', - TypeKey: 'AES-GCM', -} as const - -/** Static-only cryptographic methods */ -@Immutable -@Static // prevent instantiation -export class Cipher { - static #cryptoKey = subtle.generateKey({ name: keys.TypeKey, length: 128 }, false, ['encrypt', 'decrypt']); - static #vector = crypto.getRandomValues(new Uint8Array(16)); - static #asymmetricKey = subtle.generateKey({ - name: keys.SignKey, - modulusLength: 2048, - publicExponent: new Uint8Array([1, 0, 1]), - hash: { name: keys.Algorithm }, - }, false, ['sign', 'verify']); - - /** random UUID */ - static randomKey = () => crypto.randomUUID().split('-')[0]; - - /** decode base64 back into object */ - static decodeBase64 = (buf = ''): T => { - const uint8 = base64DecToArr(buf); // first, convert to UInt8Array - const str = UTF8ArrToStr(uint8); // convert to string - - return objectify(str); // rebuild the original object - } - - /** encode object into base64 */ - static encodeBase64 = (buf: unknown) => { - const str = stringify(buf); // first, stringify the incoming buffer - const uint8 = strToUTF8Arr(str); // convert to Uint8Array - - return base64EncArr(uint8); // convert to string - } - - static hmac = async (source: string | Object, secret: string, alg = 'SHA-512', len?: number) => { - const encoder = new TextEncoder(); - const keyData = encoder.encode(secret); - const messageData = encoder.encode(asString(source)); - - const key = await subtle.importKey( - 'raw', - keyData, - { name: 'HMAC', hash: { name: alg } }, - false, - ['sign'] - ); - - const signature = await subtle.sign('HMAC', key, messageData); - - return toHex(Array.from(new Uint8Array(signature)), len); - } - - static hash = async (source: string | Object, len?: number, alg = 'SHA-256') => { - const buffer = Cipher.encodeBuffer(asString(source)); - const hash = await subtle.digest(alg, buffer); - - return toHex(Array.from(new Uint8Array(hash)), len); - } - - static encodeBuffer = (str: string) => new Uint16Array(new TextEncoder().encode(str)); - static decodeBuffer = (buf: Uint16Array) => new TextDecoder(keys.Encoding).decode(buf); - - static encrypt = async (data: any) => - subtle.encrypt({ name: keys.TypeKey, iv: Cipher.#vector }, await Cipher.#cryptoKey, Cipher.encodeBuffer(data)) - .then(result => new Uint16Array(result)) - .then(Cipher.decodeBuffer); - - static decrypt = async (secret: Promise) => - subtle.decrypt({ name: keys.TypeKey, iv: Cipher.#vector }, await Cipher.#cryptoKey, await secret) - .then(result => new Uint16Array(result)) - .then(Cipher.decodeBuffer); - - static sign = async (doc: any) => - subtle.sign(keys.SignKey, (await Cipher.#asymmetricKey).privateKey!, Cipher.encodeBuffer(doc)) - .then(result => new Uint16Array(result)) - .then(Cipher.decodeBuffer); - - static verify = async (signature: Promise, doc: any) => - subtle.verify(keys.SignKey, (await Cipher.#asymmetricKey).publicKey!, await signature, Cipher.encodeBuffer(doc)); -} diff --git a/packages/library/src/common/cipher.library.ts b/packages/library/src/common/cipher.library.ts new file mode 100644 index 00000000..bee441ca --- /dev/null +++ b/packages/library/src/common/cipher.library.ts @@ -0,0 +1,132 @@ +import { toHex } from '#library/number.library.js'; +import { asString, asError } from '#library/coercion.library.js'; +import { isError, isString } from '#library/assertion.library.js'; +import { bufferToBase64, base64ToBuffer, encodeBuffer, decodeBuffer } from '#library/buffer.library.js'; + +const crypto = globalThis.crypto; +const subtle = crypto.subtle; + +export const keys = { + Algorithm: 'SHA-256', + Encoding: 'utf-8', + SignKey: 'RSASSA-PKCS1-v1_5', + TypeKey: 'AES-GCM', +} as const; + +// Module-scoped state for ephemeral keys +const _cryptoKey = subtle.generateKey({ name: keys.TypeKey, length: 128 }, false, ['encrypt', 'decrypt']) + .catch(asError); + +const _asymmetricKey = subtle.generateKey({ + name: keys.SignKey, + modulusLength: 2048, + publicExponent: new Uint8Array([1, 0, 1]), + hash: { name: keys.Algorithm }, +}, false, ['sign', 'verify']) + .catch(asError); + +/** random UUID */ +export const randomKey = () => crypto.randomUUID().split('-')[0]; + +export const hmac = async (source: string | Object, secret: string, alg = 'SHA-512', len?: number) => { + const encoder = new TextEncoder(); + const keyData = encoder.encode(secret); + const messageData = encoder.encode(asString(source)); + + const key = await subtle.importKey( + 'raw', + keyData, + { name: 'HMAC', hash: { name: alg } }, + false, + ['sign'] + ); + + const signature = await subtle.sign('HMAC', key, messageData); + + return toHex(Array.from(new Uint8Array(signature)), len); +}; + +export const hash = async (source: string | Object, len?: number, alg = 'SHA-256') => { + const buffer = encodeBuffer(asString(source)); + const hashBuf = await subtle.digest(alg, buffer); + + return toHex(Array.from(new Uint8Array(hashBuf)), len); +} + +export const encrypt = async (data: any) => { + const iv = crypto.getRandomValues(new Uint8Array(16)); + const key = await _cryptoKey; + if (isError(key)) throw new Error(`Cipher: Key generation failed: ${key.message}`, { cause: key }); + + const cipherBuf = await subtle.encrypt({ name: keys.TypeKey, iv }, key, encodeBuffer(data)); + const combined = new Uint8Array(16 + cipherBuf.byteLength); + + combined.set(iv, 0); + combined.set(new Uint8Array(cipherBuf), 16); + + return bufferToBase64(combined); +} + +export const decrypt = async (secret: Promise | string) => { + const [str, key] = await Promise.all([secret, _cryptoKey]); + if (isError(key)) throw new Error(`Cipher: Key generation failed: ${key.message}`, { cause: key }); + + const uint8 = base64ToBuffer(str); + const iv = uint8.slice(0, 16); + const data = uint8.slice(16); + + return subtle.decrypt({ name: keys.TypeKey, iv }, key, data) + .then(result => new Uint8Array(result)) + .then(decodeBuffer); +} + +export const sign = async (doc: any) => { + const keypair = await _asymmetricKey; + if (isError(keypair)) throw new Error(`Cipher: Key generation failed: ${keypair.message}`, { cause: keypair }); + if (!keypair.privateKey) throw new Error('Cipher: Missing private key'); + + return subtle.sign(keys.SignKey, keypair.privateKey, encodeBuffer(doc)) + .then(result => new Uint8Array(result)); +} + +export const verify = async (signature: Promise | ArrayBuffer | Uint8Array, doc: any) => { + const [buffer, keypair] = await Promise.all([signature, _asymmetricKey]); + if (isError(keypair)) throw new Error(`Cipher: Key generation failed: ${keypair.message}`, { cause: keypair }); + if (!keypair.publicKey) throw new Error('Cipher: Missing public key'); + + return subtle.verify(keys.SignKey, keypair.publicKey, buffer as BufferSource, encodeBuffer(doc)); +} + +export const importPublicKey = async (pem: string): Promise => { + const pemHeader = '-----BEGIN PUBLIC KEY-----'; + const pemFooter = '-----END PUBLIC KEY-----'; + const pemContents = pem + .substring(pem.indexOf(pemHeader) + pemHeader.length, pem.indexOf(pemFooter)) + .replace(/\s+/g, ''); + + const binaryDerString = atob(pemContents); + const binaryDer = new Uint8Array(binaryDerString.length); + for (let i = 0; i < binaryDerString.length; i++) { + binaryDer[i] = binaryDerString.charCodeAt(i); + } + + return subtle.importKey( + 'spki', + binaryDer, + { name: keys.SignKey, hash: keys.Algorithm }, + false, + ['verify'] + ); +} + +export const generateKeyPair = async (): Promise => { + return subtle.generateKey({ + name: keys.SignKey, + modulusLength: 2048, + publicExponent: new Uint8Array([1, 0, 1]), + hash: { name: keys.Algorithm }, + }, + true, + ['sign', 'verify'] + ); +} diff --git a/packages/library/src/common/class.library.ts b/packages/library/src/common/class.library.ts index 1acd96f6..3110b79e 100644 --- a/packages/library/src/common/class.library.ts +++ b/packages/library/src/common/class.library.ts @@ -1,32 +1,45 @@ import { $ImmutableSkip } from '#library/symbol.library.js'; import { secure } from '#library/proxy.library.js'; +import { isReference } from '#library/assertion.library.js'; import { registerSerializable } from '#library/serialize.library.js'; -import { type Constructor, type Type, registerType } from '#library/type.library.js'; +import { registerType, getSafeTag } from '#library/type.library.js'; +import type { Constructor, Type } from '#library/type.library.js'; /** * Some interesting Class Decorators */ +/** + * Safely extracts the class name from Symbol.toStringTag (if present) to prevent + * minifiers and compilers from mangling the registered class name. + */ +function getClassName(value: T, contextName: string | symbol | undefined): string | undefined { + return getSafeTag(value) ?? (contextName === undefined ? (value.name || undefined) : String(contextName)); +} + /** * Shared helper to create an immutable or secure class wrapper */ function createImmutableWrapper( value: T, - name: string, + name: string | undefined, addInitializer: (fn: () => void) => void, immutabilityStrategy: (instance: any) => any // either Object.freeze or secure (Proxy) strategy ): T { + const safeName = name || 'Anonymous'; const wrapper = { - [name]: class extends value { + [safeName]: class extends value { constructor(...args: any[]) { super(...args); return immutabilityStrategy(this); } } - }[name] as T; + }[safeName] as T; - registerType(value, `${name}_original` as Type); - registerType(wrapper, name as Type); + if (name) { + registerType(value, `${name}_original` as Type); + registerType(wrapper, name as Type); + } addInitializer(() => { const skip = (value as any)[$ImmutableSkip] @@ -65,7 +78,7 @@ function hardenClassStaticsAndPrototypes(value: any, wrapper: any, skip: any) { // Lock down all existing prototype properties, but do NOT freeze the prototype object const lockPrototype = (proto: object) => { - if (!proto || typeof proto !== 'object') return; + if (!isReference(proto)) return; Reflect.ownKeys(proto).forEach(name => { if (name === 'constructor') return; if (Array.isArray(skip) && skip.some(s => String(s) === String(name))) return; @@ -78,7 +91,7 @@ function hardenClassStaticsAndPrototypes(value: any, wrapper: any, skip: any) { Object.defineProperty(proto, name, { ...desc, ...update }); }); } - + lockPrototype(value.prototype); lockPrototype(wrapper.prototype); } @@ -87,11 +100,11 @@ function hardenClassStaticsAndPrototypes(value: any, wrapper: any, skip: any) { * Decorator to secure a class with a mutation-throwing Proxy (noisy immutability) */ export function Securable(value: T, { kind, name, addInitializer }: ClassDecoratorContext): T | void { - name = String(name); + const finalName = getClassName(value, name); switch (kind) { case 'class': - return createImmutableWrapper(value, name, addInitializer, secure); + return createImmutableWrapper(value, finalName, addInitializer, secure); default: throw new Error(`@Securable decorating unknown 'kind': ${kind} (${name})`); } @@ -99,11 +112,11 @@ export function Securable(value: T, { kind, name, addInit /** decorator to freeze a Class to prevent modification (silent immutability) */ export function Immutable(value: T, { kind, name, addInitializer }: ClassDecoratorContext): T | void { - name = String(name); + const finalName = getClassName(value, name); switch (kind) { case 'class': - return createImmutableWrapper(value, name, addInitializer, (instance) => { Object.freeze(instance); return instance; }); + return createImmutableWrapper(value, finalName, addInitializer, (instance) => { Object.freeze(instance); return instance; }); default: throw new Error(`@Immutable decorating unknown 'kind': ${kind} (${name})`); @@ -112,12 +125,15 @@ export function Immutable(value: T, { kind, name, addInit /** register a Class for serialization */ export function Serializable(value: T, { kind, name, addInitializer }: ClassDecoratorContext): T | void { - name = String(name); // cast as String - registerType(value, name as Type); + const finalName = getClassName(value, name); + + if (finalName) + registerType(value, finalName as Type); switch (kind) { case 'class': - addInitializer(() => registerSerializable(name, value));// register the class for serialization, via its toString() method + if (finalName) + addInitializer(() => registerSerializable(finalName, value));// register the class for serialization, via its toString() method return value; @@ -128,23 +144,27 @@ export function Serializable(value: T, { kind, name, addI /** make a Class not instantiable */ export function Static(value: T, { kind, name }: ClassDecoratorContext): T | void { - name = String(name); + const finalName = getClassName(value, name) as Type; switch (kind) { - case 'class': + case 'class': { + const safeName = finalName || 'Anonymous'; const wrapper = { - [name]: class extends value { + [safeName]: class extends value { constructor(...args: any[]) { super(...args); - throw new TypeError(`${name} is not a constructor`); + throw new TypeError(`${safeName} is not a constructor`); } } - }[name] as T; + }[safeName] as T; - registerType(value, `${name}_original` as Type); // register the original class definition - registerType(wrapper, name as Type); // register the wrapper as the authoritative definition + if (finalName) { + registerType(value, `${finalName}_original` as Type)// register the original class definition + registerType(wrapper, finalName); // register the wrapper as the authoritative definition + } return wrapper; + } default: throw new Error(`@Static decorating unknown 'kind': ${kind} (${name})`); diff --git a/packages/library/src/common/coercion.library.ts b/packages/library/src/common/coercion.library.ts index a972387e..7483ea46 100644 --- a/packages/library/src/common/coercion.library.ts +++ b/packages/library/src/common/coercion.library.ts @@ -1,6 +1,6 @@ import { clone, stringify } from '#library/serialize.library.js'; import { asType } from '#library/type.library.js'; -import { isIntegerLike, isArrayLike, isDefined, isInteger, isIterable, isNullish, isString, isUndefined, isNumber, isNumeric } from '#library/assertion.library.js'; +import { isIntegerLike, isArrayLike, isDefined, isInteger, isIterable, isNullish, isString, isUndefined, isNumber, isNumeric, isError, isObject } from '#library/assertion.library.js'; /** Coerce {value} into {value[]} ( if not already ), with optional {fill} Object */ export function asArray(arr: Exclude, string> | undefined): T[]; @@ -34,11 +34,11 @@ export function asInteger(str?: T) { switch (arg.type) { case 'BigInt': - return arg.value; // already a BigInt + return arg.value; // already a BigInt case 'Number': - return BigInt(Math.trunc(arg.value)); // cast as BigInt + return BigInt(Math.trunc(arg.value)); // cast as BigInt case 'String': - return (isIntegerLike(arg.value)) // String representation of a BigInt + return (isIntegerLike(arg.value)) // String representation of a BigInt ? BigInt(arg.value.slice(0, -1)) // get rid of trailing 'n' : BigInt(arg.value); default: @@ -52,17 +52,30 @@ export const ifNumeric = (str: string | number | bigint, stripZero = false) => { case isInteger(str): // BigInt → Number return Number(str); - case isNumber(str): // Number → as-is + case isNumber(str): // Number → as-is return str; case isNumeric(str) && (!str?.toString().startsWith('0') || stripZero): - return asNumber(str); // numeric String → Number + return asNumber(str); // numeric String → Number default: - return str as string; // non-numeric String → as-is + return str as string; // non-numeric String → as-is } } export const nullishToZero = (obj: T) => obj ?? 0; export const nullishToEmpty = (obj: T) => obj ?? ''; export const nullishToValue = (obj: T, value: R) => obj ?? value; + +/** coerce an unknown value into an Error instance */ +export function asError(err: unknown): Error & { code?: string | number } { + if (isError(err)) return err as Error & { code?: string | number }; + + const error = new Error(isObject(err) && isString(err.message) ? err.message : String(err)); + if (isObject(err)) { + error.name = isString(err.name) ? err.name : 'Error'; + if ('code' in err && (isString(err.code) || isNumber(err.code))) (error as any).code = err.code; + if ('stack' in err && isString(err.stack)) error.stack = err.stack; + } + return error as Error & { code?: string | number }; +} diff --git a/packages/library/src/common/function.library.ts b/packages/library/src/common/function.library.ts index f363b5f1..71b73d4d 100644 --- a/packages/library/src/common/function.library.ts +++ b/packages/library/src/common/function.library.ts @@ -1,4 +1,5 @@ import { secure } from '#library/proxy.library.js'; +import { isInteger, isFunction, isReference, isMap, isSet } from '#library/assertion.library.js'; import type { Property } from '#library/type.library.js'; // https://medium.com/codex/currying-in-typescript-ca5226c85b85 @@ -38,15 +39,15 @@ type Curry = function serialize(val: any, seen = new WeakSet()): string { return JSON.stringify(val, function (this: any, key: string, value: any) { if (value === undefined) return '\u0000__undefined__\u0000'; - if (typeof value === 'bigint') return `bigint:${value}`; - if (typeof value === 'function') return `function:${value.name || 'anonymous'}`; + if (isInteger(value)) return `bigint:${value}`; + if (isFunction(value)) return `function:${value.name || 'anonymous'}`; - if (value !== null && typeof value === 'object') { + if (isReference(value)) { if (seen.has(value)) return ''; seen.add(value); - if (value instanceof Map) return `map:[${Array.from(value.entries()).map(e => serialize(e, seen)).sort().join(',')}]`; - if (value instanceof Set) return `set:[${Array.from(value).map(v => serialize(v, seen)).sort().join(',')}]`; + if (isMap(value)) return `map:[${Array.from(value.entries()).map(e => serialize(e, seen)).sort().join(',')}]`; + if (isSet(value)) return `set:[${Array.from(value).map(v => serialize(v, seen)).sort().join(',')}]`; } return value; }); diff --git a/packages/library/src/common/international.library.ts b/packages/library/src/common/international.library.ts index 42a32d66..78b1de81 100644 --- a/packages/library/src/common/international.library.ts +++ b/packages/library/src/common/international.library.ts @@ -1,5 +1,6 @@ import { getOffsets } from '#library/temporal.library.js'; import { memoizeFunction } from '#library/function.library.js'; +import { isFunction } from '#library/assertion.library.js'; /** memoized helper for Intl.RelativeTimeFormat instances */ const getRTF = memoizeFunction((locale?: string, style: Intl.RelativeTimeFormatStyle = 'narrow') => { @@ -21,6 +22,17 @@ const getNF = memoizeFunction((locale?: string, options?: Intl.NumberFormatOptio return new Intl.NumberFormat(locale, options); }); +/** memoized helper for Intl.DurationFormat instances */ +const getDF = memoizeFunction((locale?: string, options?: any) => { + try { + const df = new (Intl as any).DurationFormat(locale, options); + if (isFunction(df.format)) return df; + throw new Error('No format method'); + } catch (e) { + return { format: (duration: any) => String(duration) }; + } +}); + /** * International Cookbook * (using 'Intl' namespace objects) @@ -58,6 +70,11 @@ export function formatList(list: string[], locale?: string, type: Intl.ListForma } } +/** return a localized duration string natively (using Intl.DurationFormat) */ +export function formatDuration(duration: any, locale?: string, options?: any) { + return getDF(locale, options).format(duration); +} + /** return a localized number string */ export function formatNumber(value: number, locale?: string, options?: Intl.NumberFormatOptions) { try { diff --git a/packages/library/src/common/logger.class.ts b/packages/library/src/common/logger.class.ts new file mode 100644 index 00000000..60cf9972 --- /dev/null +++ b/packages/library/src/common/logger.class.ts @@ -0,0 +1,107 @@ +import { isObject, isEmpty, isNumber, isError, isString } from '#library/assertion.library.js'; +import { enumify } from '#library/enumerate.library.js'; +import { sym } from '#library/symbol.library.js'; +import type { KeyOf, ValueOf } from '#library/type.library.js'; + +export const LOG = enumify(['Off', 'Error', 'Warn', 'Info', 'Debug', 'Trace']); +export type LOG = ValueOf +export type LogLevel = KeyOf +export type DebugLevel = LOG | Method; + +const Method = { + Off: 'off', + Log: 'log', + Info: 'info', + Warn: 'warn', + Debug: 'debug', + Trace: 'trace', + Error: 'error', +} as const; +type Method = ValueOf + +const Level = { + [Method.Off]: LOG.Off, + [Method.Error]: LOG.Error, + [Method.Warn]: LOG.Warn, + [Method.Info]: LOG.Info, + [Method.Log]: LOG.Info, + [Method.Debug]: LOG.Debug, + [Method.Trace]: LOG.Trace, +} as const; + +export function parseLogLevel(level?: DebugLevel, fallback: LOG = LOG.Info): LOG { + if (isNumber(level)) return (level >= LOG.Off && level <= LOG.Trace) ? level as LOG : fallback; + if (isString(level)) return Level[level.toLowerCase() as Method] ?? fallback; + return fallback; +} + +/** + * A lightweight, dependency-free namespaced logger. + * Decoupled from error handling and boundaries. + */ +export class Logger { + #namespace: string; + + /** The current verbosity level */ + level: LOG; + + constructor(namespace: string, level: LOG = LOG.Info) { + this.#namespace = namespace.startsWith('[') ? namespace : `[${namespace}]`; + this.level = level; + } + + #emit(method: typeof Method[keyof typeof Method], ...msg: any[]) { + let config: any; + if (msg.length > 0 && isObject(msg[0])) { + try { + if (msg[0][sym.$LogConfig]) { + config = msg.shift(); + } + } catch { + // Ignore access errors on proxies/getters + } + } + + let activeLevel = this.level; + if (config) { + if (config.silent === true) return; + if (config.debug !== undefined) { + activeLevel = parseLogLevel(config.debug, activeLevel); + } + } + + if (activeLevel === LOG.Off) return; + const methodLevel = Level[method] ?? 0; + if (methodLevel > activeLevel) return; + + const output = msg + .map(m => { + if (isError(m)) return m.message; + if (isObject(m)) { + try { + const name = m.constructor?.name ?? 'Object'; + if (name === 'Object') { + const keys = Object.keys(m); + const summary = keys.slice(0, 3).join(', '); + return `{ ${summary}${keys.length > 3 ? `, ... (+${keys.length - 3} more)` : ''} }`; + } + return `[${name}]`; + } catch { return '[Object]'; } + } + return String(m); + }) + .filter(s => !isEmpty(s)).join(' '); + + if (!isEmpty(output)) { + const consoleMethod = method; + (console as any)[consoleMethod](`${this.#namespace} ${output}`); + } + } + + /** console.log */ log = (...msg: any[]) => this.#emit(Method.Log, ...msg); + /** console.info */ info = (...msg: any[]) => this.#emit(Method.Info, ...msg); + /** console.warn */ warn = (...msg: any[]) => this.#emit(Method.Warn, ...msg); + /** console.debug */ debug = (...msg: any[]) => this.#emit(Method.Debug, ...msg); + /** console.trace */ trace = (...msg: any[]) => this.#emit(Method.Trace, ...msg); + /** console.error */ error = (...msg: any[]) => this.#emit(Method.Error, ...msg); +} diff --git a/packages/library/src/common/logify.class.ts b/packages/library/src/common/logify.class.ts deleted file mode 100644 index 77bd128e..00000000 --- a/packages/library/src/common/logify.class.ts +++ /dev/null @@ -1,131 +0,0 @@ -import { Immutable } from '#library/class.library.js'; -import { sym, markConfig } from '#library/symbol.library.js'; -import { asType } from '#library/type.library.js'; -import { isObject, isEmpty } from '#library/assertion.library.js'; -import { enumify } from '#library/enumerate.library.js'; -import type { ValueOf, KeyOf } from '#library/type.library.js'; - -export const LOG = enumify(['Off', 'Error', 'Warn', 'Info', 'Debug', 'Trace']); -export type LOG = ValueOf -export type LogLevel = KeyOf - -/** @internal console method names keyed by internal identifiers (not exported; see LOG enum for public API) */ -const Method = { - Log: 'log', - Info: 'info', - Warn: 'warn', - Debug: 'debug', - Trace: 'trace', - Error: 'error', -} as const; - -/** @internal severity levels mapped to Method names for gating logic, derived from LOG */ -const Level = { - [Method.Error]: LOG.Error, - [Method.Warn]: LOG.Warn, - [Method.Info]: LOG.Info, - [Method.Log]: LOG.Info, - [Method.Debug]: LOG.Debug, - [Method.Trace]: LOG.Trace, -} as const; - -/** logging severity levels for Logify output control */ -/** - * provide standard logging methods to the console for a class - */ -@Immutable -export class Logify { - #name: string; - #opts: Logify.Constructor = { [sym.$Logify]: true }; - - /** - * if {catch:true} then show a warning on the console and return - * otherwise show an error on the console and re-throw the error - */ - #trap(method: Logify.Method, ...msg: any[]) { - const config = (isObject(msg[0]) && (msg[0] as any)[sym.$Logify] === true) ? msg.shift() : this.#opts; - const currentLevel = (typeof config.debug === 'number') - ? config.debug - : (config.debug === true ? LOG.Trace : LOG.Info); - const methodLevel = Level[method] ?? 0; - - const output = msg.map(m => { - if (m instanceof Error) return m.message; - if (isObject(m)) { - try { - const name = m.constructor?.name ?? 'Object'; // avoiding JSON.stringify (expensive) - if (name === 'Object') { - const keys = Object.keys(m); - const summary = keys.slice(0, 3).join(', '); - return `{ ${summary}${keys.length > 3 ? `, ... (+${keys.length - 3} more)` : ''} }`; - } - return `[${name}]`; - } catch { return '[Object]'; } - } - return String(m); - }).filter(s => !isEmpty(s)).join(' '); - - if (!config.silent && !isEmpty(output) && methodLevel <= currentLevel) - (console as any)[method](`${this.#name}: ${output}`); - - if (method === Method.Error && !config.catch) { - const e = msg.find(m => m instanceof Error); - const message = `${this.#name}: ${output}`; - if (e) { - e.message = message; - throw e; - } - throw new Error(message); - } - } - - /** console.log */ log = (...msg: any[]) => this.#trap(Method.Log, ...msg); - /** console.info */ info = (...msg: any[]) => this.#trap(Method.Info, ...msg); - /** console.warn */ warn = (...msg: any[]) => this.#trap(Method.Warn, ...msg); - /** console.debug */ debug = (...msg: any[]) => this.#trap(Method.Debug, ...msg); - /** console.trace */ trace = (...msg: any[]) => this.#trap(Method.Trace, ...msg); - /** console.error */ error = (...msg: any[]) => this.#trap(Method.Error, ...msg); - - constructor(self?: Logify.Constructor | string, opts = {} as Logify.Constructor) { - opts = { ...opts }; // defensive copy of the options - const arg = asType(self); - this.#name = (arg.type === 'String') - ? arg.value - : (self as any)?.constructor?.name - ?? 'Logify'; - - if (arg.type === 'Object') { - const cfg = { ...arg.value as object }; - markConfig(cfg); // auto-mark if it's a config object - Object.assign(opts, cfg); - } - - markConfig(opts); // auto-mark the options object - - this.#opts.debug = opts.debug ?? false; // default debug to 'false' - this.#opts.catch = opts.catch ?? false; // default catch to 'false' - this.#opts.silent = opts.silent ?? false; // default silent to 'false' - } -} - -export namespace Logify { - export type Method = ValueOf - - export interface Constructor { - /** - * Logging verbosity: `boolean | number`. - * - `true` maps to `LOG.Trace`, enabling trace-level logging - * - `false` (or unset) maps to `LOG.Info` - * - numeric values map directly to `LOG` levels - * - * Note: numeric `0` (`LOG.Off`) suppresses all console emission, including - * `console.error`. Errors can still be rethrown when `catch: false`, but no - * error log is emitted. Use `true` or a higher numeric level to ensure errors - * are logged to the console. - */ - debug?: boolean | number | undefined, - catch?: boolean | undefined, - silent?: boolean | undefined, - [sym.$Logify]?: boolean | undefined - } -} \ No newline at end of file diff --git a/packages/library/src/common/number.library.ts b/packages/library/src/common/number.library.ts index 4b8f951b..c1193b22 100644 --- a/packages/library/src/common/number.library.ts +++ b/packages/library/src/common/number.library.ts @@ -12,8 +12,8 @@ export const toHex = (num: TValues = [], len?: number) => .substring(0, len ?? Number.MAX_SAFE_INTEGER) /** apply an Ordinal suffix */ -export const suffix = (idx: number) => { - const str = String(idx ?? ''); // so we can check 'endsWith' +export const suffix = (idx: number = 0) => { + const str = String(idx); switch (true) { case str.endsWith('1') && !str.endsWith('11'): diff --git a/packages/library/src/common/object.library.ts b/packages/library/src/common/object.library.ts index 5d955a53..eb7e0894 100644 --- a/packages/library/src/common/object.library.ts +++ b/packages/library/src/common/object.library.ts @@ -1,5 +1,6 @@ import { ownKeys, ownEntries } from '#library/primitive.library.js'; -import { isObject, isArray, isReference, isFunction, isDefined, isNullish, isMap, isSet } from '#library/assertion.library.js'; +import { isObject, isArray, isFunction, isDefined, isNullish, isMap, isSet } from '#library/assertion.library.js'; +import { getType } from '#library/type.library.js'; import type { Extend, Property } from '#library/type.library.js'; /** remove quotes around property names */ @@ -26,7 +27,7 @@ export const asObject = (obj?: Record) => { export const isEqual = (a: any, b: any): boolean => { if (a === b) return true; if (isNullish(a) || isNullish(b)) return a === b; - if (typeof a !== typeof b) return false; + if (getType(a) !== getType(b)) return false; if (isArray(a) && isArray(b)) { const left = a as any[], right = b as any[]; diff --git a/packages/library/src/common/pledge.class.ts b/packages/library/src/common/pledge.class.ts index 573bb108..16600eef 100644 --- a/packages/library/src/common/pledge.class.ts +++ b/packages/library/src/common/pledge.class.ts @@ -1,4 +1,4 @@ -import { Logify } from '#library/logify.class.js'; +import { Logger, type DebugLevel } from '#library/logger.class.js'; import { markConfig } from '#library/symbol.library.js'; import { asArray } from '#library/coercion.library.js'; import { ifDefined } from '#library/object.library.js'; @@ -13,6 +13,14 @@ declare module '#library/type.library.js' { } } +const _dbg = new Logger('[Pledge]'); +let _static = {} as Pledge.Constructor; +const _STATE = secure({ + Pending: Symbol('pending'), + Resolved: Symbol('resolved'), + Rejected: Symbol('rejected') +}); + /** * Wrap a Promise's resolve/reject/finally methods for later fulfilment. * with useful methods for tracking the state of the Promise, chaining fulfilment, etc. @@ -25,32 +33,26 @@ declare module '#library/type.library.js' { export class Pledge { #pledge: PromiseWithResolvers; #status = {} as Pledge.Status; - static #dbg = new Logify('Pledge'); - static #static = {} as Pledge.Constructor; - static STATE = secure({ - Pending: Symbol('pending'), - Resolved: Symbol('resolved'), - Rejected: Symbol('rejected') - }) + static get STATE() { return _STATE; } /** initialize future Pledge instances */ static init(arg?: Pledge.Constructor | string) { if (isObject(arg)) { if (isEmpty(arg)) - Pledge.#static = {}; // reset static values + _static = {}; // reset static values - markConfig(Pledge.#static); - Object.assign(Pledge.#static, + markConfig(_static); + Object.assign(_static, ifDefined({ tag: arg.tag, debug: arg.debug, catch: arg.catch, silent: arg.silent }), ifDefined({ onResolve: arg.onResolve, onReject: arg.onReject, onSettle: arg.onSettle, }), ) } else { - markConfig(Pledge.#static); - Object.assign(Pledge.#static, ifDefined({ tag: arg, })); + markConfig(_static); + Object.assign(_static, ifDefined({ tag: arg, })); } - Pledge.#dbg.debug(Pledge.#static, Pledge.#static); + _dbg.debug(_static, _static); return Pledge.status; } @@ -59,26 +61,26 @@ export class Pledge { static [Symbol.dispose]() { Pledge.init({}) } static get status() { - return Pledge.#static as Pledge.Status; + return { ..._static, state: _STATE.Pending } as Pledge.Status; } constructor(arg?: Pledge.Constructor | string) { const opts = isObject(arg) ? arg : { tag: arg as string }; - const config = { ...Pledge.#static, ...ifDefined({ tag: opts.tag, debug: opts.debug, catch: opts.catch, silent: opts.silent }) }; + const config = { ..._static, ...ifDefined({ tag: opts.tag, debug: opts.debug, catch: opts.catch, silent: opts.silent }) }; this.#pledge = Promise.withResolvers(); - this.#status = markConfig({ state: Pledge.STATE.Pending, ...config }); + this.#status = markConfig({ state: _STATE.Pending, ...config }); - const onResolve = asArray(Pledge.#static.onResolve).concat(asArray(opts.onResolve)); - const onReject = asArray(Pledge.#static.onReject).concat(asArray(opts.onReject)); - const onSettle = asArray(Pledge.#static.onSettle).concat(asArray(opts.onSettle)); + const onResolve = asArray(_static.onResolve).concat(asArray(opts.onResolve)); + const onReject = asArray(_static.onReject).concat(asArray(opts.onReject)); + const onSettle = asArray(_static.onSettle).concat(asArray(opts.onSettle)); const runSafely = (callbacks: ((...args: A) => any)[], ...args: A) => { callbacks.forEach(cb => { try { cb(...args); } catch (err) { - Pledge.#dbg.warn(this.#status, 'Pledge callback failed', err); + _dbg.warn(this.#status, 'Pledge callback failed', err); } }); } @@ -91,7 +93,7 @@ export class Pledge { this.#pledge.promise.finally(() => runSafely(onSettle)); if (this.#status.catch) - this.#pledge.promise.catch(err => Pledge.#dbg.warn(this.#status, err)); + this.#pledge.promise.catch(err => _dbg.warn(this.#status, err)); } get [Symbol.toStringTag]() { @@ -116,16 +118,16 @@ export class Pledge { } get isPending() { - return this.#status.state === Pledge.STATE.Pending; + return this.#status.state === _STATE.Pending; } get isResolved() { - return this.#status.state === Pledge.STATE.Resolved; + return this.#status.state === _STATE.Resolved; } get isRejected() { - return this.#status.state === Pledge.STATE.Rejected; + return this.#status.state === _STATE.Rejected; } get isSettled() { - return this.#status.state !== Pledge.STATE.Pending; + return this.#status.state !== _STATE.Pending; } toString() { @@ -135,11 +137,11 @@ export class Pledge { resolve(value: T) { if (this.isPending) { this.#status.settled = value; - this.#status.state = Pledge.STATE.Resolved; - Pledge.#dbg.debug(this.#status, 'Resolved'); // debug + this.#status.state = _STATE.Resolved; + _dbg.debug(this.#status, 'Resolved'); // debug this.#pledge.resolve(value); // resolve } - else Pledge.#dbg.warn(this.#status, `Pledge was already ${this.state}`); + else _dbg.warn(this.#status, `Pledge was already ${this.state}`); return this.#pledge.promise; } @@ -147,11 +149,11 @@ export class Pledge { reject(error: any) { if (this.isPending) { this.#status.error = error; - this.#status.state = Pledge.STATE.Rejected; - Pledge.#dbg.debug(this.#status, 'Rejected', error); // debug + this.#status.state = _STATE.Rejected; + _dbg.debug(this.#status, 'Rejected', error); // debug this.#pledge.reject(error); // reject } - else Pledge.#dbg.warn(this.#status, `Pledge was already ${this.state}`); + else _dbg.warn(this.#status, `Pledge was already ${this.state}`); return this.#pledge.promise; } @@ -175,9 +177,9 @@ export namespace Pledge { onResolve?: Pledge.Resolve | Pledge.Resolve[] | undefined; onReject?: Pledge.Reject | Pledge.Reject[] | undefined; onSettle?: Pledge.Settle | Pledge.Settle[] | undefined; - debug?: Logify.Constructor["debug"]; - catch?: Logify.Constructor["catch"]; - silent?: Logify.Constructor["silent"]; + debug?: DebugLevel; + catch?: boolean; + silent?: boolean; } export interface Status { diff --git a/packages/library/src/common/reflection.library.ts b/packages/library/src/common/reflection.library.ts index efaf53ae..b8771941 100644 --- a/packages/library/src/common/reflection.library.ts +++ b/packages/library/src/common/reflection.library.ts @@ -1,13 +1,13 @@ import { distinct, ownKeys, ownEntries } from '#library/primitive.library.js'; import { asType, getType } from '#library/type.library.js'; -import { isEmpty, isFunction, isPrimitive } from '#library/assertion.library.js'; +import { isEmpty, isFunction, isPrimitive, isReference } from '#library/assertion.library.js'; import type { Obj, KeyOf, Primitives } from '#library/type.library.js'; /** mutate Object | Array by excluding values with specified primitive 'types' */ export function exclude(obj: T, ...types: (Primitives | Lowercase)[]) { const exclusions = distinct(types.map(item => item.toLowerCase())) as typeof types; - if (obj && typeof obj === 'object') { // only works on Objects and Arrays + if (isReference(obj)) { // only works on Objects and Arrays const keys = [] as KeyOf[]; (ownEntries(obj) as [KeyOf, Obj][]) diff --git a/packages/library/src/common/symbol.library.ts b/packages/library/src/common/symbol.library.ts index aa73770d..f73e9f55 100644 --- a/packages/library/src/common/symbol.library.ts +++ b/packages/library/src/common/symbol.library.ts @@ -7,7 +7,7 @@ export const $Target: unique symbol = Symbol.for('$LibraryTarget') as any; export const $Discover: unique symbol = Symbol.for('$LibraryDiscover') as any; export const $Extensible: unique symbol = Symbol.for('$LibraryExtensible') as any; export const $Inspect: unique symbol = Symbol.for('nodejs.util.inspect.custom') as any; -export const $Logify: unique symbol = Symbol.for('$LibraryLogify') as any; +export const $LogConfig: unique symbol = Symbol.for('$LibraryLogConfig') as any; export const $Registry: unique symbol = Symbol.for('$LibraryRegistry') as any; export const $Register: unique symbol = Symbol.for('$LibraryRegister') as any; export const $SerializerRegistry: unique symbol = Symbol.for('$LibrarySerializerRegistry') as any; @@ -15,13 +15,13 @@ export const $ImmutableSkip: unique symbol = Symbol.for('$LibraryImmutableSkip') export const $Identity: unique symbol = Symbol.for('$LibraryIdentity') as any; export const sym = { - $Target, $Discover, $Extensible, $Inspect, $Logify, $Registry, $Register, $SerializerRegistry, $Identity, $ImmutableSkip + $Target, $Discover, $Extensible, $Inspect, $LogConfig, $Registry, $Register, $SerializerRegistry, $Identity, $ImmutableSkip } as const; -/** identify and mark a Logify configuration object */ +/** identify and mark a logging configuration object */ export function markConfig(obj: T): T { - if (!(obj as any)[sym.$Logify] && Object.isExtensible(obj)) - Object.defineProperty(obj, sym.$Logify, { value: true, enumerable: false, writable: true, configurable: true }); + if (!(obj as any)[sym.$LogConfig] && Object.isExtensible(obj)) + Object.defineProperty(obj, sym.$LogConfig, { value: true, enumerable: false, writable: true, configurable: true }); return obj; } diff --git a/packages/library/src/common/temporal.polyfill.ts b/packages/library/src/common/temporal.polyfill.ts index 1519a060..cbe88aff 100644 --- a/packages/library/src/common/temporal.polyfill.ts +++ b/packages/library/src/common/temporal.polyfill.ts @@ -16,6 +16,8 @@ if (typeof globalThis.Temporal === 'undefined') { ); } +import { asError } from './coercion.library.js'; + // 🛡️ Sane Implementation Check // Some early native implementations (e.g. Node 22.0.x) are incomplete and crash on basic arithmetic. // If you encounter "unimplemented code" or V8_Fatal crashes, manually load a polyfill before Tempo. @@ -25,10 +27,11 @@ try { const zdt = Temporal.Now.zonedDateTimeISO(); if (typeof zdt.add !== 'function') throw new Error('Incomplete Temporal implementation'); } -} catch (err: any) { - console.warn('Tempo: Native Temporal implementation appears incomplete. Consider loading a polyfill.', err); - if (err?.message !== 'Incomplete Temporal implementation') - throw err; +} catch (err: unknown) { + const error = asError(err); + console.warn('Tempo: Native Temporal implementation appears incomplete. Consider loading a polyfill.', error); + if (error.message !== 'Incomplete Temporal implementation') + throw error; } export { } diff --git a/packages/library/src/common/type.library.ts b/packages/library/src/common/type.library.ts index c6d0e5b3..fdf2045a 100644 --- a/packages/library/src/common/type.library.ts +++ b/packages/library/src/common/type.library.ts @@ -9,6 +9,15 @@ export const protoType = (obj?: unknown) => { return Object.prototype.toString.call(raw).slice(8, -1) as Type; } +/** Safely extract Symbol.toStringTag, guarding against dynamic getters that throw */ +export const getSafeTag = (obj: any): string | undefined => { + try { + const tag = obj?.[Symbol.toStringTag] ?? obj?.prototype?.[Symbol.toStringTag]; + if (typeof tag === 'string') return tag; + } catch { } + return undefined; +} + /** * # getType * return an object's type as a ProperCase string. @@ -75,7 +84,7 @@ const isClassConstructor = (obj: any): boolean => { // This is a high-performance check to immediately classify arrow functions as non-classes. if (!('prototype' in raw)) return false; const name = (raw as any)?.name; - const tag = raw?.[Symbol.toStringTag] ?? raw?.prototype?.[Symbol.toStringTag]; + const tag = getSafeTag(raw); // Absolute bypass for branded identities (using universal brand) if (raw?.[sym.$Identity] || raw?.prototype?.[sym.$Identity]) { @@ -194,8 +203,10 @@ export type Instance = { type: Type, class: Constructor } // allow for Class in * @NOTE Custom types must augment `TypeValueMap` to be recognized by the type system! */ export const registerType = (cls: Constructor, type?: Type) => { - const tag = (cls.prototype as any)?.[Symbol.toStringTag]; // toStringTag is the source-of-truth, if present - const name = (tag ?? type ?? cls.name) as Type; + if (typeof cls !== 'function') return; + + const tag = getSafeTag(cls); + const name = (type ?? tag ?? cls.name) as Type; if (name && !['Object', 'Function', ''].includes(name as string)) { if (!registry.some(inst => inst.class === cls)) diff --git a/packages/library/src/common/utility.library.ts b/packages/library/src/common/utility.library.ts index eee5a70a..6a46e0f7 100644 --- a/packages/library/src/common/utility.library.ts +++ b/packages/library/src/common/utility.library.ts @@ -5,32 +5,6 @@ import type { Secure, ValueOf } from '#library/type.library.js'; /** General utility functions */ -/** fast, unverified decode of a JWT payload */ -export const decodeJWT = (jwt: string): T | null => { - try { - const part = jwt.split('.')[1]; - if (!part) return null; - // 🛡️ Base64URL Normalization: replace -/_ with +/ and add padding - const base64 = part.replace(/-/g, '+').replace(/_/g, '/').padEnd(part.length + (4 - part.length % 4) % 4, '='); - const payload = typeof atob === 'function' ? atob(base64) : Buffer.from(base64, 'base64').toString(); - return JSON.parse(payload); - } catch { return null; } -} - -/** portable base64 encoder for universal support */ -export const base64Encode = (input: string): string => { - if (typeof Buffer !== 'undefined') - return Buffer.from(input).toString('base64'); - - const bytes = new TextEncoder().encode(input); - let binary = ''; - - for (let i = 0; i < bytes.byteLength; i++) - binary += String.fromCharCode(bytes[i]); - - return btoa(binary); -} - /** analyze the Call Stack to determine calling Function's name */ export const getCaller = () => { const stackTrace = new Error().stack // only tested in latest FF and Chrome @@ -49,7 +23,7 @@ export const getScript = (nbr = 1) => { const stackTrace = new Error().stack ?.match(/([^ \n\(@])*([a-z]*:\/\/\/?)*?[a-z0-9\/\\]*\.js/ig) ?.[nbr] - return decodeURI(stackTrace ?? ''); // decodeURI is needed to handle spaces in file-names + return decodeURI(stackTrace ?? ''); // decodeURI is needed to handle spaces in file-names } /** diff --git a/packages/library/src/common/webtoken.library.ts b/packages/library/src/common/webtoken.library.ts new file mode 100644 index 00000000..bde5d880 --- /dev/null +++ b/packages/library/src/common/webtoken.library.ts @@ -0,0 +1,78 @@ +import { base64ToBuffer, bufferToBase64, encodeBuffer, decodeBuffer } from './buffer.library.js'; +import { Logger } from './logger.class.js'; +import { keys } from './cipher.library.js'; + +const logger = new Logger('WebToken'); + +const formatBase64Url = (base64: string) => base64.replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, ''); +const toBase64Url = (str: string) => formatBase64Url(bufferToBase64(encodeBuffer(str))); +const bufToBase64Url = (buf: Uint8Array) => formatBase64Url(bufferToBase64(buf)); + +/** fast, unverified decode of a JWT payload */ +export const decodeJWT = (jwt: string): T | null => { + try { + const part = jwt.split('.')[1]; + if (!part) return null; + + // 🛡️ Base64URL Normalization: replace -/_ with +/ and add padding + const base64 = part.replace(/-/g, '+').replace(/_/g, '/').padEnd(part.length + (4 - part.length % 4) % 4, '='); + const bytes = base64ToBuffer(base64); + const payload = decodeBuffer(bytes); + + return JSON.parse(payload); + } catch { return null; } +} + +/** verify a JSON Web Signature */ +export const verifyJWS = async (token: string, publicKey: CryptoKey): Promise => { + try { + const parts = token.split('.'); + if (parts.length !== 3) return false; + + const [header, payload, signatureBase64url] = parts; + const signedData = `${header}.${payload}`; + + // Base64url to Base64 normalization + const signatureBase64 = signatureBase64url + .replace(/-/g, '+') + .replace(/_/g, '/') + .padEnd(signatureBase64url.length + (4 - signatureBase64url.length % 4) % 4, '='); + const signatureBytes = base64ToBuffer(signatureBase64); + + // crypto.subtle.verify takes signature, key, data + const crypto = globalThis.crypto; + const dataBytes = encodeBuffer(signedData); + + return await crypto.subtle.verify( + keys.SignKey, + publicKey, + signatureBytes.buffer, + dataBytes + ); + } catch (e: any) { + logger.error('VERIFY_ERROR:', e.stack); + return false; + } +} + +/** natively sign a JSON Web Signature */ +export const signJWS = async (payload: object, privateKey: CryptoKey, headers: object = { alg: 'RS256', typ: 'JWT' }): Promise => { + try { + const header64 = toBase64Url(JSON.stringify(headers)); + const payload64 = toBase64Url(JSON.stringify(payload)); + + const unsignedToken = `${header64}.${payload64}`; + const dataBytes = encodeBuffer(unsignedToken); + + const signatureBytes = await globalThis.crypto.subtle.sign( + keys.SignKey, + privateKey, + dataBytes + ); + + return `${unsignedToken}.${bufToBase64Url(new Uint8Array(signatureBytes))}`; + } catch (e: any) { + logger.error('SIGN_ERROR:', e.stack); + throw e; + } +} diff --git a/packages/library/src/server.index.ts b/packages/library/src/server.index.ts index 329ba641..a29ecb23 100644 --- a/packages/library/src/server.index.ts +++ b/packages/library/src/server.index.ts @@ -4,5 +4,4 @@ export * from './server/auth.library.js'; export * from './server/file.library.js'; -export * from './server/buffer.library.js'; export * from './server/request.library.js'; diff --git a/packages/library/src/server/auth.library.ts b/packages/library/src/server/auth.library.ts index 63d2cee9..d77095cd 100644 --- a/packages/library/src/server/auth.library.ts +++ b/packages/library/src/server/auth.library.ts @@ -1,6 +1,6 @@ -import { Buffer } from 'node:buffer'; +import { decodeJWT } from '../common/webtoken.library.js'; -const MAX_TOKEN_LENGTH = 8192; // 8 KB +const MAX_TOKEN_LENGTH = 8192; // 8 KB const MAX_PAYLOAD_LENGTH = 4096; // 4 KB /** @@ -16,35 +16,20 @@ const MAX_PAYLOAD_LENGTH = 4096; // 4 KB * @returns The parsed JSON payload of the JWT */ export const decodeJWTPayload = (token: string): T => { - if (token.length > MAX_TOKEN_LENGTH) { + if (token.length > MAX_TOKEN_LENGTH) throw new Error('JWT too large: Incoming token exceeds maximum length.'); - } const segments = token.split('.'); - if (segments.length !== 3) { + if (segments.length !== 3) throw new Error('Invalid JWT format: Expected 3 segments (header.payload.signature)'); - } - if (segments[1].length > MAX_PAYLOAD_LENGTH) { + if (segments[1].length > MAX_PAYLOAD_LENGTH) throw new Error('JWT payload too large: Encoded segment exceeds maximum length.'); - } - try { - let payload = segments[1] - .replace(/-/g, '+') - .replace(/_/g, '/'); + const decoded = decodeJWT(token); + if (!decoded) + throw new Error('Invalid JWT payload: Decoding failed'); - // Handle missing base64 padding - while (payload.length % 4 !== 0) - payload += '='; - - if (payload.length > MAX_PAYLOAD_LENGTH + 4) { // final check on padded payload - throw new Error('JWT payload too large: Final payload exceeds maximum length.'); - } - - return JSON.parse(Buffer.from(payload, 'base64').toString()) as unknown as T; - } catch (err) { - throw new Error(`Invalid JWT payload: ${(err as Error).message}`); - } -}; + return decoded; +} diff --git a/packages/library/src/server/buffer.library.ts b/packages/library/src/server/buffer.library.ts deleted file mode 100644 index 7407a0f6..00000000 --- a/packages/library/src/server/buffer.library.ts +++ /dev/null @@ -1,4 +0,0 @@ -import { stringify, objectify } from '#library/serialize.library.js'; - -export const encode64 = (str: any) => Buffer.from(stringify(str)).toString('base64'); -export const decode64 = (str: string) => objectify(Buffer.from(str, 'base64').toString()); diff --git a/packages/library/src/server/file.library.ts b/packages/library/src/server/file.library.ts index cc943563..62057890 100644 --- a/packages/library/src/server/file.library.ts +++ b/packages/library/src/server/file.library.ts @@ -15,7 +15,7 @@ export class File { * @throws {Error} If path traversal is detected * @returns The resolved absolute path */ - static #resolvePath(filename: string): string { + private static _resolvePath(filename: string): string { if (path.isAbsolute(filename)) { throw new Error(`Absolute paths are not allowed: ${filename}`); } @@ -33,7 +33,7 @@ export class File { static read = (file: string): Promise => new Promise((resolve, reject) => { try { - const target = File.#resolvePath(file); + const target = File._resolvePath(file); fs.readFile(target, File.encoding, (err, data) => { if (err) return (err.code === 'ENOENT') @@ -49,7 +49,7 @@ export class File { static write = (file: string, doc: string | NodeJS.ArrayBufferView) => new Promise((resolve, reject) => { try { - const target = File.#resolvePath(file); + const target = File._resolvePath(file); fs.writeFile(target, doc, File.encoding, (err => err ? reject(err) : resolve(doc))); } catch (err) { reject(err); @@ -58,7 +58,7 @@ export class File { static exist = (file: string) => new Promise((resolve, reject) => { try { - const target = File.#resolvePath(file); + const target = File._resolvePath(file); fs.access(target, (err => err && err.code !== 'ENOENT' ? reject(err) // anything other than 'file not-exists' @@ -71,7 +71,7 @@ export class File { static remove = (file: string) => new Promise((resolve, reject) => { try { - const target = File.#resolvePath(file); + const target = File._resolvePath(file); fs.unlink(target, (err => err && err.code !== 'ENOENT' ? reject(err) // anything other than 'file not-exists' diff --git a/packages/library/test/common/boundary.library.test.ts b/packages/library/test/common/boundary.library.test.ts new file mode 100644 index 00000000..a4433c50 --- /dev/null +++ b/packages/library/test/common/boundary.library.test.ts @@ -0,0 +1,37 @@ +import { raise } from '../../src/common/boundary.library.js'; + +describe('Boundary Library', () => { + let consoleSpy: any; + let mockLogger: any; + + beforeEach(() => { + consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => { }); + mockLogger = { error: vi.fn() }; + }); + + afterEach(() => { + vi.restoreAllMocks(); + }); + + it('should throw immediately if no catch is specified', () => { + expect(() => raise('Test Exception')).toThrow('Test Exception'); + expect(consoleSpy).toHaveBeenCalledWith('[Boundary]', expect.any(Error)); + }); + + it('should log to custom logger if provided', () => { + expect(() => raise('Test Exception', { logger: mockLogger })).toThrow('Test Exception'); + expect(mockLogger.error).toHaveBeenCalledWith(expect.any(Error)); + expect(consoleSpy).not.toHaveBeenCalled(); + }); + + it('should swallow the error if catch is true', () => { + expect(() => raise('Handled Exception', { catch: true })).not.toThrow(); + expect(consoleSpy).toHaveBeenCalledWith('[Boundary]', expect.any(Error)); + }); + + it('should completely suppress output if silent is true', () => { + expect(() => raise('Silent Exception', { catch: true, silent: true })).not.toThrow(); + expect(consoleSpy).not.toHaveBeenCalled(); + expect(mockLogger.error).not.toHaveBeenCalled(); + }); +}); diff --git a/packages/library/test/common/logger.class.test.ts b/packages/library/test/common/logger.class.test.ts new file mode 100644 index 00000000..bc796e21 --- /dev/null +++ b/packages/library/test/common/logger.class.test.ts @@ -0,0 +1,58 @@ +import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; +import { Logger, LOG, parseLogLevel } from '../../src/common/logger.class.js'; +import { sym } from '../../src/common/symbol.library.js'; + +describe('Logger Class', () => { + let consoleSpy: any; + + beforeEach(() => { + consoleSpy = { + log: vi.spyOn(console, 'log').mockImplementation(() => {}), + info: vi.spyOn(console, 'info').mockImplementation(() => {}), + warn: vi.spyOn(console, 'warn').mockImplementation(() => {}), + error: vi.spyOn(console, 'error').mockImplementation(() => {}), + debug: vi.spyOn(console, 'debug').mockImplementation(() => {}), + trace: vi.spyOn(console, 'trace').mockImplementation(() => {}) + }; + }); + + afterEach(() => { + vi.restoreAllMocks(); + }); + + it('should parse log levels correctly', () => { + expect(parseLogLevel('trace')).toBe(LOG.Trace); + expect(parseLogLevel('error')).toBe(LOG.Error); + expect(parseLogLevel(undefined, LOG.Warn)).toBe(LOG.Warn); + expect(parseLogLevel(LOG.Debug)).toBe(LOG.Debug); + }); + + it('should format namespace correctly', () => { + const logger = new Logger('Test'); + logger.info('Hello'); + expect(consoleSpy.info).toHaveBeenCalledWith('[Test] Hello'); + }); + + it('should respect default level boundaries', () => { + const logger = new Logger('Test', LOG.Warn); + logger.info('Should not print'); + logger.error('Should print'); + expect(consoleSpy.info).not.toHaveBeenCalled(); + expect(consoleSpy.error).toHaveBeenCalledWith('[Test] Should print'); + }); + + it('should respect inline config overrides', () => { + const logger = new Logger('Test', LOG.Error); + // Normally debug wouldn't print on Error level, but we override it via config + const config = { [sym.$LogConfig]: true, debug: 'trace' }; + logger.debug(config, 'Inline debug'); + expect(consoleSpy.debug).toHaveBeenCalledWith('[Test] Inline debug'); + }); + + it('should suppress output when silent is true', () => { + const logger = new Logger('Test', LOG.Trace); + const config = { [sym.$LogConfig]: true, silent: true }; + logger.error(config, 'Fatal error'); + expect(consoleSpy.error).not.toHaveBeenCalled(); + }); +}); diff --git a/packages/library/test/common/logify.class.test.ts b/packages/library/test/common/logify.class.test.ts deleted file mode 100644 index 2609fb98..00000000 --- a/packages/library/test/common/logify.class.test.ts +++ /dev/null @@ -1,71 +0,0 @@ -import { Logify } from '#library/logify.class.js'; - -describe('Logify severity gating', () => { - test('defaults to Info level when debug is false/undefined', () => { - const infoSpy = vi.spyOn(console, 'info').mockImplementation(() => {}); - const debugSpy = vi.spyOn(console, 'debug').mockImplementation(() => {}); - - try { - const log = new Logify('LogifyTestDefault'); - log.info('info-visible'); - log.debug('debug-hidden'); - - expect(infoSpy).toHaveBeenCalledTimes(1); - expect(infoSpy).toHaveBeenCalledWith('LogifyTestDefault: info-visible'); - expect(debugSpy).not.toHaveBeenCalled(); - } finally { - infoSpy.mockRestore(); - debugSpy.mockRestore(); - } - }); - - test('enables trace level when debug is true', () => { - const traceSpy = vi.spyOn(console, 'trace').mockImplementation(() => {}); - const debugSpy = vi.spyOn(console, 'debug').mockImplementation(() => {}); - - try { - const log = new Logify('LogifyTestTrace', { debug: true }); - log.trace('trace-visible'); - log.debug('debug-visible'); - - expect(traceSpy).toHaveBeenCalledTimes(1); - expect(traceSpy).toHaveBeenCalledWith('LogifyTestTrace: trace-visible'); - expect(debugSpy).toHaveBeenCalledTimes(1); - expect(debugSpy).toHaveBeenCalledWith('LogifyTestTrace: debug-visible'); - } finally { - traceSpy.mockRestore(); - debugSpy.mockRestore(); - } - }); - - test('uses numeric debug level directly', () => { - const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {}); - const infoSpy = vi.spyOn(console, 'info').mockImplementation(() => {}); - - try { - const log = new Logify('LogifyTestNumeric', { debug: 2 }); - log.warn('warn-visible'); - log.info('info-hidden'); - - expect(warnSpy).toHaveBeenCalledTimes(1); - expect(warnSpy).toHaveBeenCalledWith('LogifyTestNumeric: warn-visible'); - expect(infoSpy).not.toHaveBeenCalled(); - } finally { - warnSpy.mockRestore(); - infoSpy.mockRestore(); - } - }); - - test('rethrows errors even when log emission is gated off', () => { - const errorSpy = vi.spyOn(console, 'error').mockImplementation(() => {}); - const err = new Error('boom'); - - try { - const log = new Logify('LogifyTestRethrow', { debug: 0, catch: false }); - expect(() => log.error(err)).toThrow('LogifyTestRethrow: boom'); - expect(errorSpy).not.toHaveBeenCalled(); - } finally { - errorSpy.mockRestore(); - } - }); -}); diff --git a/packages/library/test/common/pledge.class.test.ts b/packages/library/test/common/pledge.class.test.ts index cb356ce3..4f230f72 100644 --- a/packages/library/test/common/pledge.class.test.ts +++ b/packages/library/test/common/pledge.class.test.ts @@ -65,7 +65,7 @@ describe('Pledge', () => { } }); - test('callback failures warn at default debug level (indirect Logify integration)', async () => { + test('callback failures warn at default debug level (indirect Diagnostic Engine integration)', async () => { const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {}); const debugSpy = vi.spyOn(console, 'debug').mockImplementation(() => {}); @@ -89,7 +89,7 @@ describe('Pledge', () => { } }); - test('numeric debug level gates callback warning logs (indirect Logify integration)', async () => { + test('numeric debug level gates callback warning logs (indirect Diagnostic Engine integration)', async () => { const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {}); try { diff --git a/packages/library/vitest.config.ts b/packages/library/vitest.config.ts index a1b874bd..0fa3711a 100644 --- a/packages/library/vitest.config.ts +++ b/packages/library/vitest.config.ts @@ -1,14 +1,29 @@ import { fileURLToPath } from 'node:url'; import { dirname, resolve } from 'node:path'; import { defineConfig } from 'vitest/config'; +import swc from 'unplugin-swc'; const __dirname = dirname(fileURLToPath(import.meta.url)); const isDist = process.env.TEST_DIST === 'true'; export default defineConfig({ + esbuild: false, + oxc: false, + plugins: [ + swc.vite({ + jsc: { + target: 'es2022', + parser: { syntax: 'typescript', decorators: true }, + transform: { decoratorVersion: '2023-11' }, + }, + }), + ], test: { name: 'Library: Full', globals: true, + pool: 'forks', + maxWorkers: 2, + slowTestThreshold: 2_000, environment: 'node', include: ['test/**/*.{test,spec}.ts'], setupFiles: [resolve(__dirname, '../tempo/bin/temporal-polyfill.ts')], diff --git a/packages/tempo/.vitepress/config.ts b/packages/tempo/.vitepress/config.ts index dbd429ca..3bbf28e7 100644 --- a/packages/tempo/.vitepress/config.ts +++ b/packages/tempo/.vitepress/config.ts @@ -33,9 +33,7 @@ export default defineConfig({ items: [ { text: 'Introduction', link: '/README' }, { text: 'Installation', link: '/doc/installation' }, - { text: 'Cookbook', link: '/doc/tempo.cookbook' }, - { text: 'Migration Guide', link: '/doc/migration-guide' }, - { text: 'Release Notes', link: '/doc/releases/' } + { text: 'Cookbook', link: '/doc/tempo.cookbook' } ] }, { @@ -51,12 +49,12 @@ export default defineConfig({ ] }, { - text: 'Extensions & Plugins', + text: 'Extensions & Terms', items: [ { text: 'Modularity', link: '/doc/tempo.modularity' }, - { text: 'Terms System', link: '/doc/tempo.term' }, - { text: 'Ticker Plugin', link: '/doc/tempo.ticker' }, - { text: 'Premium Plugins ↗', link: 'https://magmacomputing.github.io/tempo-plugin-docs/' } + { text: 'Terms Plugins', link: '/doc/tempo.plugin' }, + { text: 'Extension Plugins', link: '/doc/tempo.extension' }, + { text: 'Premium Plugins ↗', link: 'https://magmacomputing.github.io/tempo-plugin-docs/' }, ] }, { @@ -98,8 +96,10 @@ export default defineConfig({ ] }, { - text: 'Services & Support', + text: 'Project & Support', items: [ + { text: 'Migration Guide', link: '/doc/migration-guide' }, + { text: 'Release Notes', link: '/doc/releases/' }, { text: 'Professional Services', link: '/doc/commercial' } ] } @@ -119,6 +119,11 @@ export default defineConfig({ esbuild: { target: 'esnext' }, + optimizeDeps: { + esbuildOptions: { + target: 'esnext' + } + }, resolve: { conditions: ['development', 'module', 'browser', 'import', 'default'], alias: [ diff --git a/packages/tempo/CHANGELOG.md b/packages/tempo/CHANGELOG.md index 6926a791..5c99a46d 100644 --- a/packages/tempo/CHANGELOG.md +++ b/packages/tempo/CHANGELOG.md @@ -5,6 +5,28 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [3.0.0] - 2026-06-07 + +### Changed (Breaking) +- **Ticker Extraction**: The `TickerModule` has been extracted from the core Tempo library into a standalone, licensed premium plugin (`@magmacomputing/tempo-plugin-ticker`). It is no longer bundled with the open-source distribution. +- **ISO Getter Precision**: The `.iso` property getter has been upgraded from native `Date.toISOString()` to Temporal's `Instant.toString()`. This provides full ISO 8601 nanosecond precision and omits fractional seconds when they evaluate to exactly zero. +- **Deprecated Boolean Debug Flag**: The `debug` configuration property no longer accepts `boolean` values (`true`/`false`). It has been strictly typed to accept numeric verbosity levels (matching the `LOG` enum) or lowercase string labels (e.g., `'trace'`, `'info'`). +- **Internationalization Naming**: The legacy `intl.relativeTime` configuration object has been removed to align with ECMAScript standards. Please migrate to `intl.relativeTimeFormat`. +- **Legacy Discovery Keys**: Dropped support for the legacy `term` and `plugin` initialization options in favor of strict schema adherence (use `terms` and `plugins` instead for consistency). + +### Changed (Architecture) +- **Configuration Parsing Unification**: Refactored the core configuration pipeline by routing `Tempo.init()`, `Tempo.extend()`, and `Tempo.create()` through a unified `[$setDiscovery]` parser. This removes 50 lines of duplicate parsing logic and significantly improves architectural consistency. +- **Feature-Complete Sandboxes**: Sandboxes instantiated via `Tempo.create()` now process their full `options.discovery` payload through the unified parser. This enables sandboxes to safely inherit and isolate localized plugins, custom formats, timeZones, and ignore rules, rather than just `monthDay` inheritance. + +### Added +- **Developer Benchmarks**: Introduced the `BenchmarkModule`, a decoupled utility for stress-testing and benchmarking Tempo parsing speeds and memory overhead against custom production datasets in any environment (Node.js or Browser). +- **Compact Date Tokens**: Added `{dmy}`, `{mdy}`, and `{ymd}` to the `FormatModule` for generating 8-digit compact date strings (e.g. `24102026`). +- **Ordinal Format Tokens**: Added uppercase `{DAY}`, `{WW}`, and `{MM}` to the `FormatModule` which generate the ordinal string representation (e.g. `24th`, `1st`, `2nd`). +- **Compact Time Rename**: Renamed the `{hhmiss}` token to `{hms}` in the `FormatModule` for consistency with other token styles. +- **Native Cryptographic Primitives**: Added lightweight, tree-shakeable `cipher` and `webToken` modules to `@magmacomputing/tempo/library` to support native Web Crypto JWS validation across the ecosystem, enabling the removal of bulky third-party dependencies (like `jose`) in down-stream plugins. +### Migration +- If you used `Tempo.ticker()`, you must now install `@magmacomputing/tempo-plugin-ticker` and register it. A migration stub is currently left in place that will throw a runtime error with directions to the Tempo Registry to obtain your free license key. + ## [2.11.2] - 2026-05-27 ### Changed @@ -175,7 +197,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - **Layout Order Resolver Module**: Extracted layout-ordering decision logic from the Tempo class into a dedicated `engine.layout` module (`src/engine/engine.layout.ts`). This module provides deterministic functions for resolving parse layout order based on locale preference and maintains existing pair-swap semantics. - **Layout Controller Framework**: Implemented a minimal controller-map infrastructure (`LayoutController` type, `createLayoutController`, `resolveLayoutClassificationOrder`) to enable future input-class pre-filtering and custom layout ordering without structural refactors. The framework currently has a single default classification that mirrors existing behavior. -- **Debug Layout Order Visibility**: Added optional debug output in `Tempo.#swapLayout` to emit the resolved layout order for diagnostics (when `debug: true`). +- **Debug Layout Order Visibility**: Added optional debug output in `Tempo.#swapLayout` to emit the resolved layout order for diagnostics (when `debug: 5`). ### Changed - **Internal Layout Resolution**: Refactored `Tempo.#swapLayout` to delegate ordering to the external resolver, improving separation of concerns and testability. diff --git a/packages/tempo/CONTRIBUTING.md b/packages/tempo/CONTRIBUTING.md index 7ac1e2ad..2eb14db6 100644 --- a/packages/tempo/CONTRIBUTING.md +++ b/packages/tempo/CONTRIBUTING.md @@ -7,7 +7,7 @@ Thank you for your interest in contributing to Tempo! This project is a professi Tempo uses several advanced JavaScript patterns that contributors should be familiar with: - **[Proxy-Delegators](./doc/lazy-evaluation-pattern.md)**: For $O(1)$ lazy evaluation of instance properties. - **[Soft Freeze](./doc/soft_freeze_strategy.md)**: For secure but extensible global registries. -- **[Logify](./doc/architecture.md)**: For decoupled, symbol-based diagnostic logging. +- **[Diagnostic Engine](./doc/architecture.md)**: For decoupled, symbol-based diagnostic logging. ## 🛠️ Local Development diff --git a/packages/tempo/bench/bench.parse.prefilter.e2e.ts b/packages/tempo/bench/bench.parse.prefilter.e2e.ts index 943fb92a..61b789b6 100644 --- a/packages/tempo/bench/bench.parse.prefilter.e2e.ts +++ b/packages/tempo/bench/bench.parse.prefilter.e2e.ts @@ -1,5 +1,5 @@ -import '../bin/temporal-polyfill.ts'; -import { Tempo } from '../src/tempo.index.ts'; +import '../bin/temporal-polyfill.js'; +import { Tempo } from '../src/tempo.index.js'; import { performance } from 'node:perf_hooks'; import fs from 'fs'; @@ -37,7 +37,7 @@ try { function runE2E(enablePrefilter: boolean, iterations: number) { Tempo.init({ parsePrefilter: enablePrefilter, - debug: false, + debug: 0, catch: true, timeZone: 'UTC', }); @@ -86,7 +86,7 @@ const result = { minChecksum: 1 // dummy threshold, adjust as needed }, success: true, - errors: [] + errors: [] as string[] }; if (timingDeltaPct > result.thresholds.maxTimingDeltaPct) { diff --git a/packages/tempo/bench/bench.parse.prefilter.ts b/packages/tempo/bench/bench.parse.prefilter.ts index 2bcc7ee7..4659e91c 100644 --- a/packages/tempo/bench/bench.parse.prefilter.ts +++ b/packages/tempo/bench/bench.parse.prefilter.ts @@ -1,4 +1,4 @@ -import { selectLayoutPatterns } from '../src/engine/engine.planner.ts'; +import { selectLayoutPatterns } from '../src/engine/engine.planner.js'; import { performance } from 'node:perf_hooks'; const layoutNames = [ diff --git a/packages/tempo/bench/benchmark-results.json b/packages/tempo/bench/benchmark-results.json new file mode 100644 index 00000000..2a70fe04 --- /dev/null +++ b/packages/tempo/bench/benchmark-results.json @@ -0,0 +1,42 @@ +{ + "results": [ + { + "name": "Native Date", + "totalTimeMs": 0.64, + "opsPerSec": 5485094, + "successCount": 2000, + "failureCount": 1500, + "successRate": "57.1%", + "heapUsedDeltaMb": "0.39" + }, + { + "name": "Tempo (mode: auto)", + "totalTimeMs": 2726.79, + "opsPerSec": 1284, + "successCount": 2500, + "failureCount": 1000, + "successRate": "71.4%", + "heapUsedDeltaMb": "108.84" + }, + { + "name": "Tempo (mode: defer)", + "totalTimeMs": 2467.03, + "opsPerSec": 1419, + "successCount": 2500, + "failureCount": 1000, + "successRate": "71.4%", + "heapUsedDeltaMb": "-36.27" + }, + { + "name": "Tempo (mode: strict)", + "totalTimeMs": 2451.85, + "opsPerSec": 1427, + "successCount": 2500, + "failureCount": 1000, + "successRate": "71.4%", + "heapUsedDeltaMb": "26.10" + } + ], + "size": 7, + "ops": 3500 +} \ No newline at end of file diff --git a/packages/tempo/bench/runner.test.ts b/packages/tempo/bench/runner.test.ts new file mode 100644 index 00000000..bfdcb152 --- /dev/null +++ b/packages/tempo/bench/runner.test.ts @@ -0,0 +1,30 @@ +import '@js-temporal/polyfill'; +import { test } from 'vitest'; +import * as fs from 'node:fs'; +import { Tempo } from '../src/tempo.class.js'; +import { BenchmarkModule } from '../src/module/module.benchmark.js'; +import { ParseModule } from '../src/module/module.parse.js'; + +test('benchmark script', () => { + Tempo.extend(ParseModule); + Tempo.init({ timeZone: 'America/New_York' }); + + const DATASET = [ + '2026-05-20T14:30:00Z', + '2026-05-20', + 'Jan 1st, 2026', + '1716215400000', + 'Invalid-Pizza-String 🍕', + '10/31/2026 11:59 PM', + '2026-05-20T14:30:00.000Z', + ]; + + const results = BenchmarkModule.run(Tempo, { + data: DATASET, + iterations: 500, + baseline: true, + modes: ['auto', 'defer', 'strict'] + }); + + fs.writeFileSync('benchmark-results.json', JSON.stringify({ results, size: DATASET.length, ops: DATASET.length * 500 }, null, 2)); +}, 120000); diff --git a/packages/tempo/bench/test-benchmark-output.ts b/packages/tempo/bench/test-benchmark-output.ts new file mode 100644 index 00000000..402f9f96 --- /dev/null +++ b/packages/tempo/bench/test-benchmark-output.ts @@ -0,0 +1,7 @@ +import { Tempo } from '../src/tempo.class.js'; +import { BenchmarkModule } from '../src/module/module.benchmark.js'; +const res = BenchmarkModule.run(Tempo, { + data: ['2026-05-20', '🍕'], + modes: ['auto'] +}); +console.log(JSON.stringify(res, null, 2)); diff --git a/packages/tempo/bench/test-pizza.test.ts b/packages/tempo/bench/test-pizza.test.ts new file mode 100644 index 00000000..a8083ae4 --- /dev/null +++ b/packages/tempo/bench/test-pizza.test.ts @@ -0,0 +1,5 @@ +import { Tempo } from '../src/tempo.class.js'; +it('tests pizza', () => { + const t = new Tempo('🍕', { catch: true }); + console.log('Valid:', t.isValid); +}); diff --git a/packages/tempo/bench/test-pizza.ts b/packages/tempo/bench/test-pizza.ts new file mode 100644 index 00000000..37562388 --- /dev/null +++ b/packages/tempo/bench/test-pizza.ts @@ -0,0 +1,5 @@ +import { Tempo } from '../src/tempo.class.js'; +const t = new Tempo('🍕', { catch: true }); +console.log('Valid:', t.isValid); +console.log('Errored:', (t as any).$errored); // Wait, errored is private +console.log('ISO:', t.iso); diff --git a/packages/tempo/bench/tsconfig.json b/packages/tempo/bench/tsconfig.json new file mode 100644 index 00000000..e217e614 --- /dev/null +++ b/packages/tempo/bench/tsconfig.json @@ -0,0 +1,39 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "rootDir": "..", + "noEmit": true, + "composite": true, + "types": [ + "node", + "vitest/globals" + ], + "paths": { + "#library": [ "../../library/src/common.index.ts" ], + "#library/*": [ "../../library/src/common/*" ], + "#browser/*": [ "../../library/src/browser/*" ], + "#server/*": [ "../../library/src/server/*" ], + "#tempo": [ "../src/tempo.index.ts" ], + "#tempo/core": [ "../src/core.index.ts" ], + "#tempo/duration": [ "../src/module/module.duration.ts" ], + "#tempo/mutate": [ "../src/module/module.mutate.ts" ], + "#tempo/format": [ "../src/module/module.format.ts" ], + "#tempo/parse": [ "../src/module/module.parse.ts" ], + "#tempo/module": [ "../src/module/module.index.ts" ], + "#tempo/engine/*.js": [ "../src/engine/*.ts" ], + "#tempo/module/*.js": [ "../src/module/*.ts" ], + "#tempo/plugin/extend/*.js": [ "../src/plugin/extend/*.ts" ], + "#tempo/plugin/term/*.js": [ "../src/plugin/term/*.ts" ], + "#tempo/term/*": [ "../src/plugin/term/term.*.ts" ], + "#tempo/license": [ "../src/support/support.license.ts" ], + "#tempo/*": [ "../src/*" ] + } + }, + "include": [ + "**/*.ts", + "../bin/**/*.ts" + ], + "references": [ + { "path": "../src/tsconfig.json" } + ] +} diff --git a/packages/tempo/bin/push-docs.sh b/packages/tempo/bin/push-docs.sh new file mode 100755 index 00000000..86d603cd --- /dev/null +++ b/packages/tempo/bin/push-docs.sh @@ -0,0 +1,49 @@ +#!/bin/bash +# Push documentation changes from the current branch directly to main. +set -e + +# Define the paths that constitute "documentation" in this repository/package. +TEMPO_ROOT="$(git rev-parse --show-toplevel)/packages/tempo" +DOC_PATHS=("$TEMPO_ROOT/doc/" "$TEMPO_ROOT/img/" "$TEMPO_ROOT/index.md" "$TEMPO_ROOT/typedoc.json" "$TEMPO_ROOT/.vitepress/") + +CURRENT_BRANCH=$(git branch --show-current) + +if [ "$CURRENT_BRANCH" = "main" ]; then + echo "Already on main branch. Please use standard git commit and push." + exit 1 +fi + +# Ensure workspace is clean to avoid losing uncommitted work +if ! git diff-index --quiet HEAD --; then + echo "Working directory is not clean. Please commit or stash your changes first." + exit 1 +fi + +echo "Switching to main branch..." +git checkout main + +echo "Pulling latest main..." +git pull --ff-only origin main + +echo "Applying doc changes from $CURRENT_BRANCH to main..." +# Checkout only the doc files from the branch +git checkout "$CURRENT_BRANCH" -- "${DOC_PATHS[@]}" + +# Check if there's actually anything to commit +if git diff-index --quiet HEAD --; then + echo "No doc changes found between main and $CURRENT_BRANCH." + echo "Switching back to $CURRENT_BRANCH..." + git checkout "$CURRENT_BRANCH" + exit 0 +fi + +echo "Committing doc changes..." +git commit -m "docs: quick publish from $CURRENT_BRANCH" + +echo "Pushing directly to main..." +ALLOW_MAIN_PUSH=true git push origin main + +echo "Switching back to $CURRENT_BRANCH..." +git checkout "$CURRENT_BRANCH" + +echo "Done! Doc changes have been pushed to main." diff --git a/packages/tempo/bin/repl.ts b/packages/tempo/bin/repl.ts index 462c464e..b080b4c1 100644 --- a/packages/tempo/bin/repl.ts +++ b/packages/tempo/bin/repl.ts @@ -1,9 +1,15 @@ import { Tempo, enums } from '#tempo'; import { stringify, objectify, enumify, getType, Pledge } from '#library'; -import '#tempo/ticker'; + +const mockToken = process.env.TEMPO_LICENSE_KEY || undefined; + +if (mockToken) { + if (!process.env.TEMPO_REVOCATION_URL) process.env.TEMPO_REVOCATION_URL = 'mock'; + if (!process.env.TEMPO_REVOCATION_JWS) process.env.TEMPO_REVOCATION_JWS = '{"revoked":[]}'; +} // pre-load Tempo to the global scope for ease of use in the REPL -Object.assign(globalThis, { Tempo, getType, stringify, objectify, enumify, enums, Pledge }); +Object.assign(globalThis, { Tempo, getType, stringify, objectify, enumify, enums, Pledge, mockToken }); console.log(`\n\x1b[38;2;252;194;1m\x1b[1m ⏳ Tempo \x1b[0m\x1b[38;2;45;212;191mREPL initialized.\x1b[0m\n`); diff --git a/packages/tempo/bin/tsconfig.json b/packages/tempo/bin/tsconfig.json index 6ea090dc..9772ea27 100644 --- a/packages/tempo/bin/tsconfig.json +++ b/packages/tempo/bin/tsconfig.json @@ -35,9 +35,6 @@ "#tempo/discrete": [ "../src/discrete/discrete.index.ts" ], - "#tempo/ticker": [ - "../src/plugin/extend/extend.ticker.ts" - ], "#tempo/term/*": [ "../src/plugin/term/term.*.ts" ], diff --git a/packages/tempo/doc/architecture.md b/packages/tempo/doc/architecture.md index 7c9f4916..ed4ddd49 100644 --- a/packages/tempo/doc/architecture.md +++ b/packages/tempo/doc/architecture.md @@ -26,11 +26,11 @@ The property descriptor is `enumerable: false, configurable: false, writable: fa To solve the "Split-Brain" issue inherent in monorepo development (where multiple instances of the same library might be loaded), Tempo utilizes a **Shared Global Registry**. By leveraging `Symbol.for('magmacomputing/library/registry')` on `globalThis`, all versions of the Tempo and Library packages share a unified type-identification engine. This ensures that classes are correctly identified as constructors even when loaded across different module boundaries. -## 🕵️ Decoupled Logging (Logify) -Tempo uses **Logify**, a diagnostic engine that leverages private Symbols to avoid polluting the public console or object state. +## 🕵️ Decoupled Logging +Tempo uses a centralized, functional diagnostic engine (via `logError` / `logWarn` utilities) that relies on private context to avoid polluting the public console or object state. This ensures that parsing telemetry does not clash with application logic. - **Context-Aware**: Logs track their discovery path (e.g., "Applied via Global Discovery"). -- **Zero-Footprint**: When `debug: false`, the logging overhead is mathematically eliminated. -- **Symbol-Gated**: Diagnostic metadata is attached via `Symbol.for($Logify)`, making it invisible to standard iteration (`Object.keys`) and serialization (`JSON.stringify`). +- **Zero-Footprint**: When `debug: 0`, the logging overhead is mathematically eliminated. +- **Symbol-Gated**: Diagnostic metadata is attached via the symbol variable directly (e.g., `config[sym.$LogConfig]`), making it invisible to standard iteration (`Object.keys`) and serialization (`JSON.stringify`). Note: $LogConfig is a Symbol created using `Symbol.for('$LibraryLogConfig')`. ## 🛡️ Hardened Functional Resolution The engine implements a "Fail-Safe" execution pattern for functional inputs, automatically recovering from misidentified types—such as ES6 classes wrapped in defensive Proxies or circular dependency deadlocks. diff --git a/packages/tempo/doc/commercial.md b/packages/tempo/doc/commercial.md index ae18eb85..5d772bde 100644 --- a/packages/tempo/doc/commercial.md +++ b/packages/tempo/doc/commercial.md @@ -1,6 +1,6 @@ -# 🤝 Magma Computing: Professional Services +# 🤝 Magma Computing Solutions: Professional Services -Tempo is a high-performance, precision date-time library maintained by **Magma Computing**. We offer a range of professional services and private extensions to help teams build robust, time-sensitive applications. +Tempo is a high-performance, precision date-time library maintained by **Magma Computing Solutions**. We offer a range of professional services and private extensions to help teams build robust, time-sensitive applications. ## 🚀 Our Services diff --git a/packages/tempo/doc/migration-guide.md b/packages/tempo/doc/migration-guide.md index c90e2121..3fa33e66 100644 --- a/packages/tempo/doc/migration-guide.md +++ b/packages/tempo/doc/migration-guide.md @@ -1,3 +1,23 @@ +# ⚠️ Migrating to Tempo v3.x + +Tempo v3.x finalizes the plugin ecosystem by extracting advanced features into standalone, licensed packages. + +## 🔁 Migrating from version 2.x to 3.0.0 (Ticker Extraction) + +The `TickerModule` has been extracted from the core open-source repository into a standalone premium plugin. + +**Action Required**: +1. If you use `Tempo.ticker()`, you must now install `@magmacomputing/tempo-plugin-ticker` alongside `@magmacomputing/tempo`. +2. Visit the [Tempo License Registry](https://registry.magmacomputing.com.au) to obtain your free license key to activate the plugin. +3. Import and register the plugin in your application initialization: + ```javascript + import { Tempo } from '@magmacomputing/tempo'; + import { TickerModule } from '@magmacomputing/tempo-plugin-ticker'; + + Tempo.init({ license: 'YOUR_JWT_KEY' }); + Tempo.extend(TickerModule); + ``` + # ⚠️ Migrating to Tempo v2.x Tempo v2.x introduces architectural improvements and a more modular engine. While we strive for backward compatibility, there are some key changes to consider when upgrading from v1.x. @@ -105,14 +125,14 @@ The individual `mdyLocales` and `mdyLayouts` options have been consolidated into - **Shortcut:** `new Tempo({ monthDay: true })` (enables forced MDY parsing using default locales). ### Relative Time -The individual `rtfFormat` and `rtfStyle` options have been consolidated into a single `relativeTime` object. +The individual `rtfFormat` and `rtfStyle` options have been consolidated into a single `relativeTimeFormat` object. - **v2.6.x:** `new Tempo({ rtfStyle: 'long' })` -- **v2.7.x:** `new Tempo({ relativeTime: { style: 'long' } })` +- **v2.7.x:** `new Tempo({ relativeTimeFormat: { style: 'long' } })` ### Action Required: Only the deprecated top-level keys `rtfFormat` and `rtfStyle` are still accepted as legacy fallbacks in the current release, handled specifically in the `Tempo` class constructor for backward compatibility. -In contrast, the old `mdyLocales` and `mdyLayouts` keys are **not** treated as aliases and will be ignored; these must be migrated to the new nested `monthDay` structure. Update your configuration to ensure compatibility with future versions and the Release-C optimization engine. Refer to the `Tempo` constructor for implementation details on legacy alias handling. +In contrast, the old `mdyLocales` and `mdyLayouts` keys are **not** treated as aliases and will be ignored; these must be migrated to the new nested `monthDay` structure. Update your configuration to ensure compatibility with future versions and the optimization engine. Refer to the `Tempo` constructor for implementation details on legacy alias handling. ## 🔁 Migrating to version 2.9.3 @@ -137,5 +157,21 @@ If you previously relied on `BigInt` being treated as nanoseconds, you must now new Tempo(1000n, { timeStamp: 'ns' }); ``` +## 🔁 Removed Features (v3.0.0) + +### Deprecated Boolean Debug Flag +The `debug` configuration property no longer accepts `boolean` values. It has been strictly typed to accept numeric verbosity levels matching the internal `LOG` enum, or lowercase string labels (e.g. `'trace'`, `'info'`). + +- **Removed:** `new Tempo({ debug: true })` +- **Recommended:** `new Tempo({ debug: 4 })`, `new Tempo({ debug: 'debug' })`, or `new Tempo({ debug: LOG.Debug })` (for parsing verbosity). + +### Internationalization Naming +To better align with ECMAScript standards (specifically `Intl.RelativeTimeFormat`), the `relativeTime` configuration option inside `intl` is no longer supported in v3.0.0. + +- **Removed:** `new Tempo({ intl: { relativeTime: { style: 'long' } } })` +- **Recommended:** `new Tempo({ intl: { relativeTimeFormat: { style: 'long' } } })` + +Please migrate your configurations from `relativeTime` to `relativeTimeFormat`. + ## 🧪 Testing and Stability v2.x has been hardened with a 100% pass rate on our regression suite. If you were relying on undocumented "quirks" or bugs in v1.x parsing, you may find that v2.x is more strict and deterministic. diff --git a/packages/tempo/doc/release-notes-v3.0.0.md b/packages/tempo/doc/release-notes-v3.0.0.md new file mode 100644 index 00000000..defe1b59 --- /dev/null +++ b/packages/tempo/doc/release-notes-v3.0.0.md @@ -0,0 +1,41 @@ +# Tempo v3.0.0 Release Notes + +Welcome to Tempo v3.0.0! This major release marks a significant milestone in our architectural journey by finalizing the decentralized plugin ecosystem. + +## 🚀 What's New & Changed + +### Ticker Module Extraction (Breaking Change) +To lighten the core bundle and clean up the API surface for general use cases, the `TickerModule` has been extracted from the base open-source distribution into its own standalone premium plugin (`@magmacomputing/tempo-plugin-ticker`). + +The Ticker is still completely free to use, but it is now protected by a License Key via the Tempo Registry. This allows us to better protect the investment in the advanced scheduling algorithms and restrict its payload footprint strictly to applications that need it. + +### Formatting Module Additions +The `FormatModule` has been updated with new compact date tokens (`{dmy}`, `{mdy}`, `{ymd}`) for generating 8-digit compact date strings (e.g. `24102026`). Additionally, the `{hhmiss}` compact time token has been renamed to `{hms}` for consistency. +We have also introduced **Ordinal Tokens**: uppercase variants of standard date tokens (`{DAY}`, `{WW}`, `{MM}`) now output their ordinal string representation (e.g., `24th`, `1st`, `2nd`). + +### Migration Path for `Tempo.ticker()` Users +If you are upgrading from v2.x and your application relies on `Tempo.ticker()`, you will need to update your integration: + +1. **Install the Plugin**: + ```bash + npm install @magmacomputing/tempo-plugin-ticker + ``` +2. **Activate your License**: Visit [registry.magmacomputing.com.au](https://registry.magmacomputing.com.au) to obtain your free JWT license key. +3. **Register the Plugin**: Wire the key into your application and extend Tempo: + ```javascript + import { Tempo } from '@magmacomputing/tempo'; + import { TickerModule } from '@magmacomputing/tempo-plugin-ticker'; + + Tempo.init({ license: 'YOUR_JWT_KEY' }); + Tempo.extend(TickerModule); + ``` + +A migration stub has been left in the core package for v3.0.0. If you accidentally call `Tempo.ticker()` without the plugin installed, the engine will safely throw an informative error directing you to the registry. + +## 🛠️ Internal Improvements +- Bumped core engine to v3.0.0 to reflect the breaking API extraction. +- Fully synchronized build pipelines and TS declarations to ensure `vitest` and `tsc` operate seamlessly across local and premium workspaces. +- Removed legacy `Ticker` shorthands from core test suites for guaranteed separation of concerns. +- **ISO Getter Precision**: Upgraded the `.iso` property getter from native `Date.toISOString()` to Temporal's `Instant.toString()`. This provides full ISO 8601 nanosecond precision and conforms to RFC 3339 by gracefully omitting fractional seconds when they evaluate to exactly zero. + +Thank you for continuing to build with Tempo! diff --git a/packages/tempo/doc/releases/v2.x.md b/packages/tempo/doc/releases/v2.x.md index 24a8e93d..63b72862 100644 --- a/packages/tempo/doc/releases/v2.x.md +++ b/packages/tempo/doc/releases/v2.x.md @@ -180,7 +180,7 @@ Added v2.6.0 migration guide for season changes. - **Module Path Flattening**: Relocated core modules (e.g., `mutate`, `duration`) from `src/plugin/module/` to a top-level `src/module/` directory. This simplifies the internal architecture and provides cleaner internal import paths, reflecting their role as core library features rather than just "plugins". ### 📊 Diagnostics & Debug Support -- **Layout Order Tracing**: Added optional debug emission to show the final resolved layout order when `debug: true` is set, helping developers understand parse-order decisions. +- **Layout Order Tracing**: Added optional debug emission to show the final resolved layout order when `debug: 5` is set, helping developers understand parse-order decisions. ## [v2.3.0] - 2026-04-22 ### 🧩 Parsing Innovations diff --git a/packages/tempo/doc/sandbox-factory.md b/packages/tempo/doc/sandbox-factory.md index 967d49e7..fd80f383 100644 --- a/packages/tempo/doc/sandbox-factory.md +++ b/packages/tempo/doc/sandbox-factory.md @@ -11,6 +11,16 @@ Historically, `Tempo.init()` modified the global library state. This meant that: ## The Solution `Tempo.create()` returns a **derived sandboxed class** with its own isolated configuration, registry, and plugin state. Each sandbox inherits from the caller, but runs with independent internal state. +### Lifecycle Methods +To understand when to use `Tempo.create()`, it helps to contrast it with the other initialization methods: + +- **`Tempo.init({ options })`** + **Concept:** Hard-reset to "out-of-the-box" factory defaults, then apply the provided configuration globally. All previous plugins, terms, and custom formats are purged. +- **`Tempo.extend({ options })`** + **Concept:** Additive mutation. Keep all existing global settings, plugins, and formats intact, but merge in new configurations. +- **`Tempo.create({ options })`** + **Concept:** Sandbox Factory. Clone the current global state (inheriting all currently loaded plugins and settings), but branch it off into a brand new, isolated class. Any future changes made to this Sandbox will not affect the global `Tempo`, and vice-versa. + ### Example: Creating a Sandbox ```typescript import { Tempo } from '@magmacomputing/tempo'; @@ -66,4 +76,4 @@ Sandboxed classes created via `Tempo.create()` are protected by the same `@Immut ## Best Practices 1. **Create Once**: Create your application-specific Sandbox once and export it as your primary entry point. 2. **Prefer Sandboxes for Custom Aliases**: Avoid modifying the base `Tempo` class if your app is intended to be used as a library. -3. **Use Debug Mode**: When developing new aliases, set `debug: true` to receive console warnings about naming collisions. +3. **Use Debug Mode**: When developing new aliases, set `debug: 'debug'` to receive console warnings about naming collisions. diff --git a/packages/tempo/doc/soft_freeze_strategy.md b/packages/tempo/doc/soft_freeze_strategy.md index c45949ff..6179a2f4 100644 --- a/packages/tempo/doc/soft_freeze_strategy.md +++ b/packages/tempo/doc/soft_freeze_strategy.md @@ -87,5 +87,5 @@ This ensures that while the library is extensible, its fundamental logic remains --- ::: info -**v2.1.2 Update**: The Soft Freeze is now tightly integrated with **Logify**. Internal state updates bypass the Proxy using a private Symbol, allowing the engine to remain "Silent" while performing complex transactional updates during the discovery phase. +**v2.1.2 Update**: The Soft Freeze is now tightly integrated with the **Diagnostic Engine**. Internal state updates bypass the Proxy using a private Symbol, allowing the engine to remain "Silent" while performing complex transactional updates during the discovery phase. ::: diff --git a/packages/tempo/doc/tempo.benchmarks.md b/packages/tempo/doc/tempo.benchmarks.md index 9361ca10..eeeaa10c 100644 --- a/packages/tempo/doc/tempo.benchmarks.md +++ b/packages/tempo/doc/tempo.benchmarks.md @@ -44,3 +44,58 @@ The benchmark script used `performance.now()` within a Vitest environment to ens ::: info These benchmarks represent the library's performance under Node.js v22+. Results may vary based on the JS engine (V8, JavaScriptCore, etc.) but the $O(1)$ complexity remains constant. ::: + +--- + +## 📈 Self-Service Benchmark Module + +Tempo provides a built-in `BenchmarkModule` to allow developers to prove the performance ROI in their own exact environment (e.g. comparing Node.js memory metrics against Browser DOM performance). + +This module is completely decoupled from the core to keep the bundle light, but accepts your specific configured `Tempo` instance to give accurate results. + +### Usage + +```typescript +import { Tempo } from '@magmacomputing/tempo'; +import { BenchmarkModule } from '@magmacomputing/tempo/module/benchmark'; + +// 1. Configure your Tempo instance (e.g. plugins, timezone, etc) +Tempo.init({ timeZone: 'America/New_York' }); + +// 2. Run the benchmark against your datasets +const results = BenchmarkModule.run(Tempo, { + data: ['2026-05-20', '2026-05-21', 'Invalid-Pizza-String'], // production datasets + iterations: 100, // stress-test count + baseline: true, // compare against native `new Date()` + modes: ['auto', 'defer'] +}); + +console.table(results); +``` + +### Metrics Collected + +The `BenchmarkModule` is platform-aware. Using the internal `getContext()` utility, it tracks: +- **`totalTimeMs`**: Total elapsed time using high-resolution timers (`performance.now()`). +- **`microSecPerOp`**: The average number of microseconds (µs) taken per operation. +- **`successRate`**: The percentage of strings that successfully resolved to a valid date. +- **`heapUsedDeltaMb`**: (Node.js Only) The change in memory allocation to measure Garbage Collection overhead and memory safety. + +
+ +### Indicative Benchmark Results + +When evaluated against a mixed dataset (Strict ISO, US Localized, Timestamps, and Garbage strings) over 3,500 operations in Node.js (Vitest), the `BenchmarkModule` yields the following performance profiles: + +| Strategy | µs / Op | Success Rate | Memory Overhead | Notes | +| :--- | :--- | :--- | :--- | :--- | +| **Native Date** | 0.18 µs | 57.1% | 0.39 MB | Raw C++ speed, but fails on localized strings (e.g., `10/31/2026 11:59 PM`). | +| **Tempo (Auto)** | 778.8 µs | **71.4%** | 108.84 MB | **~0.77ms**: Fully parses US localized strings and timestamps. | +| **Tempo (Defer)** | 704.7 µs | **71.4%** | -36.27 MB | **~0.70ms**: Noticeably faster, reduced Garbage Collection pressure. | +| **Tempo (Strict)** | 700.7 µs | **71.4%** | 26.10 MB | **~0.70ms**: Fastest mode; bypasses parsing fallback attempts. | + +#### 🔍 Key Takeaways + +1. **Sub-Millisecond Rich Parsing**: While native `Date` has raw C++ V8 speed (0.18 µs), it sacrifices reliability and formatting, failing entirely on localized formats like `10/31/2026`. Tempo achieves a robust **71% success rate** while still maintaining blazing fast **sub-millisecond (~0.7ms)** parsing speeds! In real-world terms, processing 100 complex localized dates on a page will only take 70 milliseconds. +2. **Defer Mode is Highly Optimized**: Looking at the memory overhead (`heapUsedDeltaMb`), `defer` mode actually resulted in a *negative* memory delta (`-36.27 MB`), meaning it allowed the V8 Garbage Collector to clean up memory faster than we were allocating the Proxy instances! +3. **Adapt This Test**: The runner script used to generate these results is available in the `bench/runner.test.ts` directory. You can use it as a scaffold to test Tempo against your own unique datasets. diff --git a/packages/tempo/doc/tempo.config.md b/packages/tempo/doc/tempo.config.md index 23a068ba..03ef238c 100644 --- a/packages/tempo/doc/tempo.config.md +++ b/packages/tempo/doc/tempo.config.md @@ -24,6 +24,42 @@ This strategy prevents accidental state corruption while maintaining the flexibl --- +## 🏆 Best Practice: The `tempo.config.ts` Pattern + +Rather than scattering `Tempo.init()` or `Tempo.extend()` calls throughout your application, the recommended best practice is to centralize your environment setup into a single `tempo.config.ts` (or `.js`) file. + +This mirrors modern ecosystem standards (like `vite.config.ts` or `tailwind.config.js`) and ensures that plugins, timezones, and custom aliases are consistently applied before any domain logic executes. + +```typescript +// tempo.config.ts +import { Tempo } from '@magmacomputing/tempo'; +import { CronModule } from '@magmacomputing/tempo-plugin-cron'; +import { SLAModule } from '@magmacomputing/tempo-plugin-sla'; + +export const GlobalTempoConfig = { + timeZone: 'Australia/Sydney', // Set your baseline timezone + plugins: [CronModule, SLAModule], // Register enterprise plugins + period: { + 'market-open': '09:30', + 'market-close': '16:00' + } +} + +// Bootstrap the global environment +Tempo.init(GlobalTempoConfig); +``` + +You can then import this file at the very top of your application's entry point (e.g., `main.ts` or `index.js`) to guarantee the configuration is locked in before any other files import `Tempo`. + +```typescript +// main.ts +import './tempo.config.ts'; +import { App } from './app.js'; +// ... +``` + +--- + ## 1. Persistent Configuration (`$Tempo`) The first layer Tempo checks after its own internal defaults is persistent storage. This is ideal for "sticky" settings like a user's preferred timezone or locale that should persist across sessions without a database. @@ -121,7 +157,7 @@ Tempo.init({ timeZone: 'Australia/Sydney', locale: 'en-AU', pivot: 80, - debug: false + debug: 0 }); ``` @@ -136,8 +172,7 @@ Tempo.init({ | `monthDay` | `MonthDay \| boolean` | `undefined` | Regional date-parsing configuration (grouped). Includes `active`, `locales`, `layouts`, and `timezones`. | | `timeStamp`| `'ss' \| 'ms' \| 'us' \| 'ns'` | `'ms'` | Precision for numeric inputs and the `.ts` property. | | `sphere` | `'north' \| 'south'`| Auto-inferred | Hemisphere for seasonal plugins. | -| `relativeTime` | `RelativeTime` | `undefined` | Relative time formatting configuration (grouped). | -| `intl` | `IntlOptions` | `undefined` | Internationalization configuration grouping both `relativeTime` and `numberFormat`. | +| `intl` | `IntlOptions` | `undefined` | Internationalization configuration grouping `relativeTimeFormat`, `numberFormat`, and `durationFormat`. | | `event` | `Record` | Built-in aliases | Custom date aliases merged into the event registry. | | `period` | `Record` | Built-in aliases | Custom time aliases merged into the period registry. | | `snippet` | `Record` | Built-in snippets | Custom snippet patterns used to compose parse layouts. | @@ -146,19 +181,17 @@ Tempo.init({ | `plugins` | `Plugin \| Plugin[]` | `[]` | Plugins/modules to extend during initialization. Unlike core init options such as `snippet`, `layout`, `event`, or `period`, these values are not merged into internal state via `extendState`; `Tempo.init()` applies each plugin with `Tempo.extend(p)`, so plugin authors should treat them as instance/class augmentations rather than internal-state merges. | | `store` | `string` | `'$Tempo'` | Persistent storage key used by `readStore`/`writeStore`. | | `discovery` | `string \| symbol` | `'$Tempo'` symbol key | Discovery slot used to resolve global discovery config. | -| `debug` | `boolean \| number` | `false` | Controls log verbosity. `true` maps to `LOG.Debug`, `false` maps to `LOG.Info`, and numeric values map directly to `LOG` levels (`0=Off ... 5=Trace`). | +| `debug` | `number \| string` | `'info'` | Controls log verbosity via direct `LOG` levels (`0=Off ... 5=Trace`) or string labels (`'trace'`, `'info'`, etc). | | `catch` | `boolean` | `false` | If true, invalid inputs return a Void instance instead of throwing. | | `mode` | `'auto' \| 'strict' \| 'defer'` | `'auto'` | Controls the hydration strategy (e.g., `defer` for Zero-Cost creation). | | `silent` | `boolean` | `false` | Suppresses console output. Combined with `catch: true` for silent failover. | | `ignore` | `string \| string[]` | `['at']` | List of noise words to be stripped before parsing. | -| `layoutOrder` | `string[]` | Built-in Order | The sequence in which layouts are attempted during parsing. | -| `preFilter` | `boolean` | `false` | Enables the Parse Planner to skip irrelevant layouts based on input classification. | | `planner` | `PlannerOptions` | `undefined` | Grouped configuration for `layoutOrder` and `preFilter`. | --- ::: info -`debug` currently accepts only `boolean` or numeric level values. String labels like `'trace'` are not supported. +`debug` accepts numeric level values (`0` through `5`) or lowercase string labels (`'off'`, `'error'`, `'warn'`, `'info'`, `'debug'`, `'trace'`). ::: ## 4. Instance-Level Overrides @@ -247,7 +280,7 @@ console.log(t.toString()); // Resolved correctly (noise words stripped) For high-performance applications, you can enable the **Parse Planner** to optimize the pattern-matching loop. -#### `preFilter` (Boolean) +#### `planner.preFilter` (Boolean) When enabled, Tempo performs a fast upfront classification of the input string (detecting digits, letters, colons, etc.) and skips layouts that cannot possibly match. - **Purely numeric inputs**: Skips `event`, `period`, `wkd`, and `rel` layouts. @@ -255,20 +288,22 @@ When enabled, Tempo performs a fast upfront classification of the input string ( - **Colon detected**: Prioritizes time-based layouts (`tm`, `dtm`) to find a match faster. ```javascript -Tempo.init({ preFilter: true }); +Tempo.init({ + planner: { preFilter: true } +}); ``` -#### `layoutOrder` (Array) +#### `planner.layoutOrder` (Array) You can manually define the order in which layouts are attempted. This is useful if you know your data primarily uses a specific format (e.g., ISO dates) and want to avoid checking other layouts first. ```javascript Tempo.init({ - layoutOrder: ['ymd', 'dt', 'tm', 'rel'] + planner: { layoutOrder: ['ymd', 'dt', 'tm', 'rel'] } }); ``` ::: tip -**Observability**: Set `debug: true` along with `preFilter: true` to see a detailed "Planner summary" in the console, showing how many layouts were skipped for a given input. +**Observability**: Set `debug: 'debug'` along with `planner.preFilter` to see a detailed "Planner summary" in the console, showing how many layouts were skipped for a given input. ::: --- @@ -284,7 +319,7 @@ Tempo.init({ | **Instance** | 🥇 Highest | Ad-hoc overrides for specific calculations. | ::: tip -**Observability**: When `debug: true` is set, Tempo logs its discovery path to the console (e.g., "Global Discovery found via Symbol"), making it easy to trace exactly where a setting originated. +**Observability**: When `debug: 'debug'` is set, Tempo logs its discovery path to the console (e.g., "Global Discovery found via Symbol"), making it easy to trace exactly where a setting originated. ::: ::: info diff --git a/packages/tempo/doc/tempo.cookbook.md b/packages/tempo/doc/tempo.cookbook.md index 27a4a603..29b6aa57 100644 --- a/packages/tempo/doc/tempo.cookbook.md +++ b/packages/tempo/doc/tempo.cookbook.md @@ -169,7 +169,7 @@ console.log(t.since()); // "1d ago" (narrow style) const rtf = new Intl.RelativeTimeFormat('fr', { style: 'long' }); for (const entry of logEntries) { // Use the new grouped API: pass the formatter's format function - console.log(new Tempo(entry.ts).since(null, { relativeTime: rtf.format.bind(rtf) })); + console.log(new Tempo(entry.ts).since(null, { relativeTimeFormat: rtf.format.bind(rtf) })); } ``` @@ -224,6 +224,21 @@ console.log(t.format('We are currently in the {#quarter}')); // "We are currentl The examples below use the `using` and `await using` syntax, which require **TypeScript 5.2+** and a runtime that supports **TC39 Explicit Resource Management**. ::: +### ⚠️ Ticker Plugin Initialization + +The Ticker engine is a premium feature. Before using `Tempo.ticker()` in the examples below, you must import the plugin, initialize Tempo with your valid license, and extend it with the `TickerModule`. + +```typescript +import { Tempo } from '@magmacomputing/tempo'; +import { TickerModule } from '@magmacomputing/tempo-plugin-ticker'; + +// 1. Initialize with your JWT license +Tempo.init({ license: 'YOUR_JWT_LICENSE_HERE' }); + +// 2. Extend Tempo with the Ticker Module +Tempo.extend(TickerModule); +``` + ### Subscription Billing (Recurring Payments) Use a `seed` to anchor your subscription to a specific day, then use a month-based Ticker. diff --git a/packages/tempo/doc/tempo.debugging.md b/packages/tempo/doc/tempo.debugging.md index 7d1f4c0f..5d459a99 100644 --- a/packages/tempo/doc/tempo.debugging.md +++ b/packages/tempo/doc/tempo.debugging.md @@ -63,18 +63,20 @@ If you simply need to see the value represented in different primitive formats, ## Debugging and Console Logging -Tempo has an internal logging utility (`Tempo.#dbg`) that responds to specific configuration flags. +Tempo utilizes a central **Diagnostic Engine** (replacing the legacy Logify strategy) that respects configuration flags to provide structured output without polluting the console. ### The `debug` Flag -When instantiating a `Tempo`, you can pass `{ debug: true }` in the options object. +When instantiating a `Tempo`, or globally via `Tempo.init()`, you can pass `{ debug: 'debug' }` in the options object. ```typescript -const t = new Tempo('next Friday', { debug: true }); +const t = new Tempo('next Friday', { debug: 'debug' }); ``` -When this flag is enabled, Tempo will output detailed `console.info` logs during instantiation, including: -* The raw input being parsed. -* The regex pattern that matched it. -* The raw groups extracted before mutation logic. -* The final, conformed groups applied to the `Temporal.ZonedDateTime`. +When this flag is enabled, Tempo's Diagnostic Engine will output detailed `console.debug` logs during parsing. These logs provide a deep-dive look into the Parse Engine's resolution logic, including: +* The prioritized layout regex patterns evaluated against the string. +* The raw regex groups extracted (e.g., specific day offsets, localized term mappings). +* The intermediate steps in temporal normalization (e.g., resolving aliases, computing time and date). +* The absolute temporal coordinates composed before final instantiation. + +*Note: The Diagnostic Engine evaluates the `debug` setting before serializing any string interpolations, guaranteeing zero-cost execution overhead for standard users who do not request debug-level diagnostics.* ### The `catch` Flag By default, `Tempo` is designed to be resilient. If it encounters parsing errors or invalid inputs, it will gracefully fallback. @@ -90,7 +92,7 @@ const t = new Tempo('invalid string', { catch: false }); Tempo uses a **Master Guard** to avoid the expensive regex parsing phase for strings that are obviously not date-time inputs. If you find that a string is not being parsed as expected, it's possible that the guard is Rejecting it. 1. **Check characters**: The guard only allows digits, common symbols (`-`, `:`, `.`, `T`, `Z`, `/`, `+`, `#`), space, and standard Latin characters. -2. **Use `debug: true`**: If a string passes the guard but fails the parser, you will see "conformed groups" logs. If you see *nothing* and the input is returned as-is (falling back to a timestamp interpretation), then the guard likely rejected the string. +2. **Use `debug: 5`**: If a string passes the guard but fails the parser, you will see "conformed groups" logs. If you see *nothing* and the input is returned as-is (falling back to a timestamp interpretation), then the guard likely rejected the string. 3. **Invalid String fallback**: If the guard rejects a string and it cannot be parsed as a numeric timestamp, it will result in an Invalid instance (see below). --- diff --git a/packages/tempo/doc/tempo.duration.md b/packages/tempo/doc/tempo.duration.md index 4b16f9d6..44a91698 100644 --- a/packages/tempo/doc/tempo.duration.md +++ b/packages/tempo/doc/tempo.duration.md @@ -56,16 +56,12 @@ anchor.since(birthday, 'days'); // → "13,149d ago" (deterministic) // 2. Pass a custom formatter for natural language output (e.g. "yesterday") const yesterday = anchor.add({ days: -1 }); const autoFormat = new Intl.RelativeTimeFormat('en-US', { numeric: 'auto' }); -anchor.since(yesterday, { unit: 'days', intl: { relativeTime: { format: autoFormat } } }); // → "yesterday" +anchor.since(yesterday, { unit: 'days', intl: { relativeTimeFormat: autoFormat.format.bind(autoFormat) } }); // → "yesterday" // 3. Returns an ISO 8601 Duration String if no unit is provided anchor.since(birthday); // → "-P36Y..." ``` -::: info Return Type -Because `.since()` automatically renders a localized string, it returns a primitive JavaScript `String`. Therefore, chaining `.balance()` or `.format()` (see below) onto `.since()` is not possible and will throw an error. -::: - ## The Duration Object (EDO) When you call `until()` (or `Tempo.duration()`), Tempo returns an Extended Data Object (EDO) representing the exact duration. @@ -134,27 +130,28 @@ console.log(exactDays.days); // 366 (2024 is a leap year!) Once you have a balanced duration, you can instantly render it as a highly localized, plural-aware string using the `.format()` method. -`.format()` automatically looks for the largest non-zero unit and uses `Intl.NumberFormat` to translate it perfectly into the user's language. +`.format()` natively uses `Intl.DurationFormat` (or a robust multi-unit polyfill) to render all non-zero units perfectly into the user's language. ```javascript -// Perfect for SaaS Pricing Cards! -const formatted = Tempo.duration({ days: 365 }) +// Perfect for detailed countdowns or SaaS dashboards! +const formatted = Tempo.duration({ days: 395, hours: 4 }) + // 'nominal: true' uses 365d/yr and 30d/mo math, so no relativeTo anchor is needed .balance({ nominal: true }) .format(); -console.log(formatted); // "1 year" (or "1 año", "1 an" depending on navigator.language) +console.log(formatted); // "1 year, 1 month, and 4 hours" (or localized equivalent) ``` ### Global Configuration -You can also define default formatting options globally by adding `numberFormat` into your `Tempo.init` configuration. +You can also define default formatting options globally by adding `durationFormat` into your `Tempo.init` configuration. ```javascript Tempo.init({ intl: { - numberFormat: { unitDisplay: 'short' } // e.g. "1 yr" instead of "1 year" + durationFormat: { style: 'short' } // e.g. "1 yr, 1 mth, 4 hr" instead of long-form } }); // Now, all format calls will automatically use 'short' display -const shortDur = Tempo.duration('P1Y').format(); // "1 yr" +const shortDur = Tempo.duration('P1Y1M').format(); // "1 yr, 1 mth" ``` diff --git a/packages/tempo/doc/tempo.extension.md b/packages/tempo/doc/tempo.extension.md new file mode 100644 index 00000000..bc79e1e0 --- /dev/null +++ b/packages/tempo/doc/tempo.extension.md @@ -0,0 +1,145 @@ +# Creating an Extension Plugin + +While [Term Plugins](./tempo.term.md) are excellent for providing static, memoized data (like astrological signs or fiscal quarters), **Extension Plugins** allow you to fundamentally enhance the `Tempo` class with entirely new methods and behaviors. + +This guide will teach you the "Tempo-way" of authoring an Extension Plugin by building a classic, real-world example: **The Business Days Extension**. + +## The Goal + +We want to add an `.addBusinessDays()` method to the Tempo instance that adds or subtracts a specific number of working days (defaulting to 1), skipping weekends automatically. + +```typescript +const t = new Tempo('2026-05-22'); // Friday +console.log(t.addBusinessDays(2).format('{www}')); // Output: 'Tue' +``` + +--- + +## 1. The `defineExtension` Factory + +The safest and most efficient way to author a plugin is using the `defineExtension` factory. This handles the internal registration automatically. + +```typescript +// src/index.ts +import { defineExtension } from '@magmacomputing/tempo/plugin'; +import type { Tempo } from '@magmacomputing/tempo/core'; + +export const BusinessDaysPlugin = defineExtension({ + name: 'BusinessDaysPlugin', + install(TempoClass: any) { + // Plugin implementation goes here! + } +}); +``` + +## 2. Extending the Prototype + +To add an instance method, you extend the `TempoClass.prototype`. + +> [!WARNING] Immutability is King +> Tempo is strictly immutable. When authoring an instance method that modifies the date, **never mutate `this`**. Instead, use the core methods (like `this.add` or `this.set`) which automatically generate and return a fresh, isolated clone for you. + +Let's implement our `.addBusinessDays()` logic: + +```typescript +export const BusinessDaysPlugin = defineExtension({ + name: 'BusinessDaysPlugin', + install(TempoClass: any) { + TempoClass.prototype.addBusinessDays = function(days: number = 1) { + let next = this; + const direction = days >= 0 ? 1 : -1; + let remaining = Math.abs(days); + + // Loop using the underlying Temporal API data + // dayOfWeek: 1 = Monday ... 7 = Sunday + while (remaining > 0) { + next = next.add({ days: direction }); + + // Only count Monday-Friday as a valid jump + if (next.toDateTime().dayOfWeek <= 5) { + remaining--; + } + } + + // Return the new clone! (Tempo's native .add() already guarantees a fresh instance) + return next; + }; + } +}); +``` + +Notice how we drop into `.toDateTime()` to access the raw zone-aware `Temporal.ZonedDateTime` object? This is a common pattern in plugins when you need to access raw calendar properties (like `dayOfWeek`, `dayOfYear`, or `daysInMonth`) while preserving zone information without triggering unnecessary string formatting. + +## 3. TypeScript Module Augmentation + +If you are using TypeScript (highly recommended), your IDE will not know about `.addBusinessDays()` until you augment the `Tempo` interface. + +You must declare this augmentation in the same file that exports your plugin: + +```typescript +// src/index.ts +import { defineExtension } from '@magmacomputing/tempo/plugin'; + +// ... (plugin implementation) ... + +// Inform TypeScript that the core Tempo class now has this method +declare module '@magmacomputing/tempo/core' { + interface Tempo { + addBusinessDays(days?: number): Tempo; + } +} +``` + +## 4. Packing it as a Configurable Module + +Sometimes, you want your plugin to accept options (e.g., passing in a custom array of public holidays to skip). To do this, wrap your `defineExtension` call in a standard factory function: + +```typescript +export type BusinessDayOptions = { + skipHolidays?: boolean; +}; + +export const BusinessDaysModule = (pluginOptions: BusinessDayOptions = {}) => { + return defineExtension({ + name: 'BusinessDaysModule', + install(TempoClass: any) { + TempoClass.prototype.addBusinessDays = function(days: number = 1) { + let next = this; + const direction = days >= 0 ? 1 : -1; + let remaining = Math.abs(days); + + while (remaining > 0) { + next = next.add({ days: direction }); + if (next.toDateTime().dayOfWeek <= 5) { + // We can now use 'pluginOptions' in our logic! + if (!pluginOptions.skipHolidays /* || !isHoliday(next) */) { + remaining--; + } + } + } + + return next; + }; + } + }); +}; +``` + +## Consuming the Plugin + +Your users can now import and register your extension elegantly: + +```typescript +import { Tempo } from '@magmacomputing/tempo/core'; +import { BusinessDaysModule } from 'my-business-days-plugin'; + +Tempo.extend(BusinessDaysModule({ skipHolidays: true })); + +const t = new Tempo(); +const nextBiz = t.addBusinessDays(2); +``` + +--- + +> [!TIP] Need something more complex? +> If you need to build advanced scheduling engines, AsyncGenerators, or precision arithmetic tools that you plan to distribute commercially, check out our **[Premium Plugin Registry ↗](https://magmacomputing.github.io/tempo-plugin-docs/)** for inspiration, or contact Magma Computing Solutions for professional plugin development. diff --git a/packages/tempo/doc/tempo.format.md b/packages/tempo/doc/tempo.format.md index 608c6089..5d6f7f0f 100644 --- a/packages/tempo/doc/tempo.format.md +++ b/packages/tempo/doc/tempo.format.md @@ -87,18 +87,25 @@ Tempo.extend(FormatModule); | `{mon}` | Full Month Name | `October` | | `{mmm}` | Short Month Name | `Oct` | | `{mm}` | 2-digit Month | `10` | +| `{MM}` | Ordinal Month | `10th` | | `{dd}` | 2-digit Day | `24` | | `{day}` | Unpadded Day | `24` (or `9`) | +| `{DAY}` | Ordinal Day | `24th` (or `9th`) | | `{wkd}` | Full Weekday Name | `Saturday` | | `{www}` | Short Weekday Name | `Sat` | | `{dow}` | Day of Week (1-7) | `6` | +| `{ww}` | Week of Year | `43` | +| `{WW}` | Ordinal Week of Year | `43rd` | | `{hh}` | 2-digit Hour (24h) | `15` | | `{HH}` | 2-digit Hour (12h) | `03` | | `{mer}` | am/pm marker | `pm` | | `{MER}` | AM/PM marker | `PM` | | `{mi}` | Minutes | `30` | | `{ss}` | Seconds | `45` | -| `{hhmiss}` | Compact Time (24h) | `153045` | +| `{dmy}` | Compact Date (ddmmyyyy) | `24102026` | +| `{mdy}` | Compact Date (mmddyyyy) | `10242026` | +| `{ymd}` | Compact Date (yyyymmdd) | `20261024` | +| `{hms}` | Compact Time (24h) | `153045` | | `{ms}` | 3-digit Milliseconds | `123` | | `{us}` | 3-digit Microseconds | `456` | | `{ns}` | 3-digit Nanoseconds | `789` | diff --git a/packages/tempo/doc/tempo.layout.md b/packages/tempo/doc/tempo.layout.md index 1f2fbbb0..40068fd8 100644 --- a/packages/tempo/doc/tempo.layout.md +++ b/packages/tempo/doc/tempo.layout.md @@ -97,6 +97,6 @@ console.log(regex.source); ## Professional Services -If your project involves specialized terminology, complex financial calendars, or legacy application log formats, the **Magma Computing** team offers professional services to design and test custom `Tempo` Layouts optimized for your business needs. +If your project involves specialized terminology, complex financial calendars, or legacy application log formats, the **Magma Computing Solutions** team offers professional services to design and test custom `Tempo` Layouts optimized for your business needs. -Contact us at [Magma Computing](https://github.com/magmacomputing). +Contact us at [Magma Computing Solutions](https://github.com/magmacomputing). diff --git a/packages/tempo/doc/tempo.month-day.md b/packages/tempo/doc/tempo.month-day.md index c3eb1184..84b97311 100644 --- a/packages/tempo/doc/tempo.month-day.md +++ b/packages/tempo/doc/tempo.month-day.md @@ -62,7 +62,7 @@ Tempo.init({ When `monthDay.active` is true, Tempo performs two main actions: -1. **Pattern Selection**: The `{dt}` placeholder (used in the default `dateTime` layout) is switched from the `dayMonthYear` pattern to the `monthDayYear` pattern. +1. **Pattern Selection**: The `{dt}` composite-snippet (used in the default `dateTime` layout) is switched from the `dayMonthYear` pattern to the `monthDayYear` pattern. 2. **Layout Swapping**: The internal order of tried layouts is adjusted. For example, the `monthDayYear` layout is moved ahead of `dayMonthYear` in the priority list. This ensures that even complex or non-standard strings are interpreted according to the regional preference. diff --git a/packages/tempo/doc/tempo.pledge.md b/packages/tempo/doc/tempo.pledge.md index cf805af4..c2ffb519 100644 --- a/packages/tempo/doc/tempo.pledge.md +++ b/packages/tempo/doc/tempo.pledge.md @@ -59,7 +59,7 @@ Each `Pledge` can be assigned a `tag` string. This tag is included in logs and e ```typescript // Enable debug logging for this instance -const p = new Pledge({ tag: 'DatabaseQuery', debug: true }); +const p = new Pledge({ tag: 'DatabaseQuery', debug: 5 }); // If p is rejected, the tag will be included in the trace p.reject('Timeout'); @@ -71,7 +71,7 @@ You can set global defaults for all future `Pledge` instances using `Pledge.init ```typescript Pledge.init({ - debug: true, + debug: 5, onSettle: () => GlobalSpinner.stop() }); ``` diff --git a/packages/tempo/doc/tempo.plugin.md b/packages/tempo/doc/tempo.plugin.md index cc113ebd..68a88fe8 100644 --- a/packages/tempo/doc/tempo.plugin.md +++ b/packages/tempo/doc/tempo.plugin.md @@ -18,21 +18,17 @@ Tempo.extend(HolidayModule({ region: 'US-NY' })); --- -The most efficient way to author a plugin is using the `definePlugin` factory. This helper automatically handles the internal registration logic, making your plugin available as soon as it is imported (via side effect). +The most efficient way to author a plugin is using the `defineExtension` factory. This helper automatically handles the internal registration logic, making your plugin available as soon as it is imported (via side effect). ## Example Plugin ```typescript -import { definePlugin } from '@magmacomputing/tempo/plugin'; +import { defineExtension } from '@magmacomputing/tempo/plugin'; -export const MyPlugin = definePlugin((TempoClass, options, factory) => { - /** - * TempoClass: The internal Tempo class (for static methods) - * options: The global configuration object - * factory: A helper to create new Tempo instances without 'new' - */ - - // 1. Add a static method +export const MyPlugin = defineExtension({ + name: 'MyPlugin', + install(TempoClass: any) { + // 1. Add a static method TempoClass.myStaticMethod = () => { /* ... */ }; // 2. Add an instance method (on the prototype) @@ -87,7 +83,7 @@ declare module '@magmacomputing/tempo/core' { --- -Modern Tempo plugin are designed to be "plug-and-play." By using the `definePlugin` factory, a plugin registers itself with the global Tempo registry as soon as it's imported. +Modern Tempo plugin are designed to be "plug-and-play." By using the `defineExtension` factory, a plugin registers itself with the global Tempo registry as soon as it's imported. ```typescript import '@magmacomputing/tempo/ticker'; // 1. Module self-registers via side-effect @@ -129,7 +125,7 @@ Tempo.extend({ Using `Tempo.extend()` ensures that the library safely bypasses the "Soft Freeze" protection and that all internal caches (like the Master Guard) are correctly synchronized. -### 5. Error Handling & The Logify Pattern +### 5. Error Handling & The Diagnostic Engine When building plugin that perform complex parsing or logic, follow Tempo's **"Fail-fast by Default"** principle. - **Strict Mode (Default)**: If your plugin encounters a terminal error (e.g., invalid input that cannot be recovered), you should `throw` a descriptive error. @@ -193,12 +189,14 @@ class MyPluginInstance implements MyPluginTypes.Descriptor { ``` ### 3. Wrap with a Proxy in the Factory -Use a `Proxy` in your `definePlugin` factory to handle the callability trap. This allows your plugin to act as a function (the shortcut) and an object (the stateful class) simultaneously. +Use a `Proxy` in your `defineExtension` factory to handle the callability trap. This allows your plugin to act as a function (the shortcut) and an object (the stateful class) simultaneously. ```typescript -export const MyPlugin = definePlugin((TempoClass, options, factory) => { - (TempoClass as any).myTool = function(arg1: any): MyPluginTypes.Instance { - const instance = new MyPluginInstance(arg1); +export const MyPlugin = defineExtension({ + name: 'MyPlugin', + install(TempoClass: any) { + TempoClass.myTool = function(arg1: any): MyPluginTypes.Instance { + const instance = new MyPluginInstance(arg1); const proxy = new Proxy((() => instance.doSomething()) as any, { get: (_, prop) => { @@ -250,9 +248,34 @@ export const MyFeatureModule = defineModule((TempoClass, options) => { ### Commercial & Premium Plugins -If you have built a powerful plugin and wish to distribute it commercially, you do not need to implement your own licensing engine. Build your plugin using the standard `defineModule` or `definePlugin` wrappers. +If you have built a powerful plugin and wish to distribute it commercially, you do not need to implement your own licensing engine. Build your plugin using the standard `defineModule` or `defineExtension` wrappers. + +Once your plugin is ready for the marketplace, **[Contact Magma Computing Solutions](https://github.com/magmacomputing)**. We can inject our proprietary licensing and cryptographic verification engine directly into your build pipeline, ensuring your plugin is securely gated and protected from unauthorized use. + +### Safely Loading Premium Plugins + +When using a commercially licensed premium plugin, the cryptographic verification of your license key happens securely in the background. Because of this, you should always wait for the validation engine to settle before executing premium features, especially during application boot. -Once your plugin is ready for the marketplace, **[Contact Magma Computing](https://github.com/magmacomputing)**. We can inject our proprietary licensing and cryptographic verification engine directly into your build pipeline, ensuring your plugin is securely gated and protected from unauthorized use. +Use `Tempo.ready()` to safely wait for the cryptographic engine: + +```typescript +import { Tempo } from '@magmacomputing/tempo'; +import { PremiumPlugin } from 'tempo-plugin-premium'; + +// 1. Initialize Tempo with your license key +Tempo.init({ + license: process.env.TEMPO_LICENSE, + plugins: [PremiumPlugin] +}); + +async function boot() { + // 2. Wait for the background engine to verify the signature + await Tempo.ready(); + + // 3. 100% safe to execute the premium plugin synchronously + const result = Tempo.premiumFeature(); +} +``` --- @@ -281,7 +304,7 @@ Tempo.extend(HolidayPlugin({ Tempo.extend( [PluginA, PluginB], PluginC, - { debug: true } // applied to A, B, and C + { debug: 5 } // applied to A, B, and C ); ``` @@ -291,7 +314,7 @@ Tempo.extend( If you have a complex business requirement or need a high-performance plugin built to professional standards, we can help. Our team can design, implement, and verify custom Tempo extensions tailored to your specific domain. -**[Contact Magma Computing](https://github.com/magmacomputing)** to discuss your requirements. +**[Contact Magma Computing Solutions](https://github.com/magmacomputing)** to discuss your requirements. -- [Tempo Ticker Guide](./tempo.ticker.md): A deep dive into an "Async Generator" based plugin. +- [Extension Plugin Guide](./tempo.extension.md): Learn the "Tempo-way" to write a prototype extension (like Business Days). - [Tempo Terms Guide](./tempo.term.md): Documentation on the "Memoized Lookup" pattern for business logic. diff --git a/packages/tempo/doc/tempo.term.md b/packages/tempo/doc/tempo.term.md index a5f19f8a..8d494602 100644 --- a/packages/tempo/doc/tempo.term.md +++ b/packages/tempo/doc/tempo.term.md @@ -158,7 +158,7 @@ Tempo.extend(QuarterTerm); A Term plugin is ideally created using the **`defineTerm`** factory function provided by the library. This ensures correct type-inference and automatically handles registration during the discovery phase. -If you are developing a commercial plugin and require license enforcement, simply build your logic using the standard `defineTerm` factory. Once ready for the marketplace, contact Magma Computing to have our proprietary licensing and cryptographic verification engine wrapped around your plugin prior to distribution. +If you are developing a commercial plugin and require license enforcement, simply build your logic using the standard `defineTerm` factory. Once ready for the marketplace, contact Magma Computing Solutions to have our proprietary licensing and cryptographic verification engine wrapped around your plugin prior to distribution. ### Plugin Definition diff --git a/packages/tempo/doc/tempo.ticker.md b/packages/tempo/doc/tempo.ticker.md index cd9be6bf..df88bb23 100644 --- a/packages/tempo/doc/tempo.ticker.md +++ b/packages/tempo/doc/tempo.ticker.md @@ -1,315 +1,11 @@ -# Tempo Ticker +# Ticker Plugin -`Tempo.ticker` is an optional plugin (provided in the @magmacomputing/tempo/ticker module) that creates a reactive stream of `Tempo` instances at regular intervals. It is designed to be high-performance and lightweight, providing a simple way to build clocks, countdowns, or scheduled updates. +The **Ticker Plugin** is a high-performance, asynchronous generator extension for Tempo that allows you to schedule, pause, and iterate over temporal intervals (e.g., polling every 5 minutes, or triggering an event every quarter). -## Installation +Because it uses advanced JavaScript `AsyncGenerator` patterns and is designed for enterprise-grade scheduling, it has been moved to our **Premium Plugin Registry**. -To use the Ticker, you can import the module as a side effect or import the `TickerModule` directly. The side-effect import (`import '@magmacomputing/tempo/ticker'`) registers the `Tempo.ticker` method automatically, while importing the `TickerModule` explicitly requires you to call `Tempo.extend(TickerModule)` to register it with the core library: +## Accessing the Ticker Plugin -```typescript -// Pattern A: One-line activation (Side effect) -import '@magmacomputing/tempo/ticker'; +To view the documentation, install the plugin, and generate a license token, please visit the official Tempo Registry: -// Pattern B: Explicit Module (Recommended) -import { Tempo } from '@magmacomputing/tempo/core'; -import { TickerModule } from '@magmacomputing/tempo/ticker'; - -Tempo.extend(TickerModule); -``` - -### Direct Access -If you need to access the [Reporting & Registry](#reporting-registry) API (like `Ticker.active`), you should import the `Ticker` namespace: - -```typescript -import { Ticker } from '@magmacomputing/tempo/ticker'; - -console.log(Ticker.active); -``` - -## 🚀 Enhancements - -The Ticker supports a unified **Options** object, enabling professional resource management and semantic duration-based intervals. - -### 1. Semantic Intervals (Duration Objects) -Instead of raw numeric seconds, you can use `DurationLike` objects for clarity. This is especially powerful for variable-length intervals like **months**. - -```typescript -// Pulse exactly once a month -await using monthly = Tempo.ticker({ months: 1 }); - -// Pulse every time a new #quarter begins -await using quarterly = Tempo.ticker({ '#quarter': 1 }); -``` - -### 2. Term-Based Intervals -Ticker intervals can now be driven by any registered **Term**. This is powerful for syncing with business cycles or daily shifts. - -```typescript -// Pulse at the start of every 'morning', 'afternoon', etc. -using shiftTicker = Tempo.ticker({ '#period': 1 }, (t) => { - console.log(`New period started: ${t.term.per}`); -}); -``` - -### 3. Stop Conditions (Resource Management) -Prevent memory leaks and runaway processes by setting a built-in termination condition. - -```typescript -// Pattern A: Stop after exactly 5 ticks (defaults to 1-second interval) -using tickerA = Tempo.ticker({ limit: 5 }, (t) => console.log(t)); - -// Pattern B: Stop when a specific virtual time is reached (Inclusive) -using tickerB = Tempo.ticker({ - seconds: 10, // Plural DurationLike property - until: '2024-12-25T12:00:00' -}, (t) => console.log(t)); - -// Pattern C: Stop immediately (Limit: 0 is strictly honored) -using tickerC = Tempo.ticker({ limit: 0 }); -``` - -### 4. Virtual Clock (Seeding) -To create a **Virtual Clock** that increments from a specific point rather than using the system time, use the `seed` option: - -```typescript -// Starts at '2024-01-01', then increments by 1 day per pulse -await using daily = Tempo.ticker({ - days: 1, - seed: '2024-01-01' -}); -``` - -### 5. Backwards Tickers (Countdowns) -By providing a **negative** interval, you can create a Ticker that moves backwards in time. - -```typescript -// Count down from 10 seconds, moving backwards 1s at a time -using countdown = Tempo.ticker({ seconds: -1, seed: "00:00:10" }, (t, stop) => { - console.log(t.format('{ss}')); - if (t.ss === 0) stop(); -}); -``` - -## Usage Patterns - -### 1. Resource Management (Recommended) - -Using the `using` and `await using` keywords ensures that Tickers are automatically stopped when they go out of scope. - -```typescript -// Pattern A: Automatic cleanup for callback-based ticker -{ - using ticker = Tempo.ticker((t) => render(t)); // Defaults to a 1-second pulse -} // interval stops automatically here - -// Pattern B: Automatic cleanup for async generator -{ - await using ticker = Tempo.ticker(1); - for await (const t of ticker) { - if (done) break; - } -} // generator is closed and interval stops here -``` - -### 2. Manual Control (Programmatic Stop) - -If you are not using the `using` or `await using` keywords, or if you need to stop the Ticker from outside its own loop (e.g., in a separate event handler), you can manually call the `stop()` method on the Ticker object. - -```typescript -// Pattern A: Stop a callback-based ticker -const tickerA = Tempo.ticker(1, (t) => console.log(t)); -// ... later -tickerA.stop(); - -// Pattern B: Stop an async generator externally -const tickerB = Tempo.ticker(1); - -(async () => { - for await (const t of tickerB) { - console.log(t.toString()); - } - console.log('Ticker has been gracefully stopped.'); -})(); - -// Close the generator from somewhere else -setTimeout(() => { - tickerB.stop(); -}, 5000); -``` -### 3. Event Listeners (.on) -Instead of (or in addition to) the constructor callback, you can register listeners for the `'pulse'`, `'stop'`, and `'catch'` events. -All listeners use the same callback signature: `(t, stop) => {}`. - -```typescript -const ticker = Tempo.ticker(1); -ticker.on('pulse', (t) => console.log('Listener A:', t.fmt.weekTime)); -ticker.on('pulse', (t) => console.log('Listener B:', t.fmt.weekTime)); -ticker.on('stop', (t) => console.log('Ticker stopped at:', t.fmt.weekTime)); -``` -For `'stop'` listeners, the `stop` callback argument is included for signature consistency; however, invoking it after stop has already occurred is a no-op. - -### 4. Manual Pulsing (.pulse) -In some scenarios, you may want to drive a Ticker manually (e.g., from a UI event or a WebSocket message) while still benefiting from the Ticker's internal state management and listeners. - -```typescript -const ticker = Tempo.ticker({ seconds: 1 }); // Still has a 1s duration logic -// ... -ticker.pulse(); // Manually advance and notify listeners -``` - - - -## 🧟 Zombie Tickers (Warning) {#zombie-tickers-warning} - -In a Node.js environment, `Tempo.ticker()` uses background timers (`setTimeout`) to drive its pulses. If you do not explicitly stop a Ticker, it becomes a **"Zombie Ticker"** that continues to run indefinitely, even if the variable that created it has gone out of scope. - -### The Risks: -- **Process Hangs**: Node.js will not exit a process if there are active timers. Undisposed Tickers are a common cause of "mysterious hangs" at the end of test runs. -- **Test Inconsistency**: Leaked Tickers can continue to fire while subsequent tests are running, leading to flaky assertions and "impossible" state changes. -- **Memory Leaks**: Each active Ticker maintains closures that prevent garbage collection of the `Tempo` instance and its listeners. - -### The Solution: -Always use the **Disposer Pattern** (`using` or `await using`) or a `try...finally` block to guarantee cleanup: - -```typescript -// ✅✅ BEST: Automatic cleanup via 'using' -{ - using ticker = Tempo.ticker(1); - // ... logic ... -} // Stays clean: ticker stopped automatically here - -// ✅ GOOD: Manual cleanup in finally block (Required for captured variables) -let ticker; -try { - ticker = Tempo.ticker(1, (t) => { ... }); - // ... assertions ... -} finally { - ticker?.stop(); // Prevents "Zombie Tickers" even if assertions fail -} -``` - -::: warning -If you are using `const` or `let` without a `finally` block, an assertion failure will skip the `stop()` call, leaving a live timer in the event loop. Always prefer the `using` keyword or `try...finally` for industrial-grade resource management. -::: - -### `Ticker` Object -The object returned by `Tempo.ticker()` (or an instance of the `Ticker` class) implements the following interface: - -| Method / Property | Description | -| :--- | :--- | -| `on(event, cb)` | Registers a listener for the `'pulse'`, `'stop'`, or `'catch'` events. | -| `pulse()` | Manually triggers a pulse, advances state, and notifies listeners. Returns the new `Tempo`. | -| `info` | Read-only getter returning `{ next, ticks, limit, interval, stopped }`. | -| `stop()` | Stops the Ticker, clears active timers, and immediately resolves any pending async iteration Promises. | -| `[Symbol.dispose]` | Standard cleanup for `using` blocks. | -| `[Symbol.asyncDispose]` | Standard async cleanup for `await using` blocks. | -| `[Symbol.asyncIterator]` | Standard async iteration support (for `for await` loops). | - -## Reporting & Registry {#reporting-registry} - -The `Ticker` class maintains a static registry of all currently active Tickers. This is useful for debugging, monitoring, or cleanup checks. - -### `Ticker.active` -A static getter that returns an array of [`Ticker.Snapshot`](#tickersnapshot) objects for all active (non-stopped) Tickers. - -```typescript -import { Ticker } from '@magmacomputing/tempo/ticker'; - -// Get a report of all running tickers -const reports = Ticker.active; - -reports.forEach(({ ticker, next, ticks }) => { - console.log(`Ticker ${ticker} next pulse: ${next}, ticks so far: ${ticks}`); -}); -``` - -#### `Ticker.Snapshot` -```typescript -type Snapshot = { - ticker: Instance; // The Ticker instance (Proxy) itself - next: Tempo; // The next Tempo value to be emitted - ticks: number; // Number of pulses emitted so far - limit?: number; // The configured limit (if any) - interval: object; // The duration-based interval - stopped: boolean; // Whether the ticker is stopped -} -``` - -## 🎯 One-Shot Ticker (Meeting Alerts) - -You can use the Ticker as a "one-shot" timer for specific events by simply specifying a **seed** value. This is perfect for setting up a single alert (e.g., for a meeting) that cleans itself up immediately after firing. - -::: tip -**Seed-Only Logic**: Providing a `seed` (as a string or in an options object) without any other duration-based keys (`seconds`, `minutes`, etc.) or a `limit` implies a `limit: 1`. - -Effectively, `Tempo.ticker('Fri 10am')` and `Tempo.ticker({ seed: 'Fri 10am' })` and `Tempo.ticker({ seed: 'Fri 10am', limit: 1 })` are all treated as one-shot Tickers. - -**Inclusive Boundaries**: Termination conditions (`limit` and `until`) are **inclusive**. A Ticker with `limit: 1` will pulse exactly once before stopping. -::: - -```typescript -// Pattern A: Implicit one-shot via string seed -Tempo.ticker('Friday 10am', (t) => { - console.log(`Meeting alert: ${t.format('{HH}:{mi}')}`); -}); - -// Pattern B: Explicit one-shot via options -const event = { meeting: 'Friday 10am' }; - -Tempo.ticker({ - seed: { value: 'meeting', event } -}, (t) => { - console.log(`Meeting alert: ${t.format('{HH}:{mi}')}`); -}); -``` - -::: warning -**Future Seeds**: If the `seed` is in the future, the Ticker will remain dormant (waiting) until that time is reached. **Most Tickers emit an initial pulse immediately** (at the `seed` time or "now"), but a future seed will delay that first pulse until the specified time. -::: - -::: danger -**Persistence**: Ticker timers exist only **in-memory**. If the driving process (e.g., Node.js) terminates, any scheduled future pulses (including those from future seeds) are lost. For critical long-term scheduling, consider an external persistent job runner. -::: - -::: warning -While `limit: 1` handles the stop condition automatically, always remember that if you are using long-running Tickers without a limit, you **must** use the [Disposer Pattern](#zombie-tickers-warning) or manual `stop()` to avoid memory leaks and zombie processes. -::: - -## 🧭 Advanced: Syncing Multiple Clocks - -If you need to show multiple timezones on a dashboard, avoid creating multiple Tickers. Instead, use a single **Master Ticker** to drive all views. This prevents "drift" between the clocks and is much more efficient. - -### Using Signals (Recommended) - -Signals (from Preact, Solid, or Vue) are perfect for this "one source, many views" pattern. - -```typescript -// 1. Master source of truth -const now = signal(new Tempo()); - -// 2. Drive the master from a single ticker -using _ = Tempo.ticker(1, (t) => now.value = t); - -// 3. Derived timezones update automatically and stay 100% in sync -const sydney = computed(() => now.value.set({ timeZone: 'Australia/Sydney' })); -const london = computed(() => now.value.set({ timeZone: 'Europe/London' })); -``` - -### Using Async Generators (Framework-Agnostic) - -If you are not using a reactive framework, you can use the same pattern with an `AsyncGenerator` to derive all clocks from a single pulse. - -```typescript -// One generator, one interval, zero drift. -await using master = Tempo.ticker(1); - -for await (const t of master) { - const clocks = { - sydney: t.set({ timeZone: 'Australia/Sydney' }), - ny: t.set({ timeZone: 'America/New_York' }), - london: t.set({ timeZone: 'Europe/London' }) - }; - - renderDashboard(clocks); -} -``` +**[Explore the Tempo Ticker Plugin ↗](https://magmacomputing.github.io/tempo-plugin-docs/)** diff --git a/packages/tempo/importmap.json b/packages/tempo/importmap.json index 7444e9eb..b6d5a19b 100644 --- a/packages/tempo/importmap.json +++ b/packages/tempo/importmap.json @@ -11,7 +11,6 @@ "@magmacomputing/tempo/mutate": "./dist/module/module.mutate.js", "@magmacomputing/tempo/plugin": "./dist/plugin/plugin.index.js", "@magmacomputing/tempo/format": "./dist/discrete/discrete.format.js", - "@magmacomputing/tempo/ticker": "./dist/plugin/extend/extend.ticker.js", "@magmacomputing/tempo/library": "./dist/library.index.js" } } \ No newline at end of file diff --git a/packages/tempo/index.md b/packages/tempo/index.md index 8bfed360..20d221b1 100644 --- a/packages/tempo/index.md +++ b/packages/tempo/index.md @@ -103,14 +103,10 @@ let initPromise = (async () => { registry.has = () => false } - const [{ Tempo }, { TickerModule }] = await Promise.all([ - import('@magmacomputing/tempo'), - import('@magmacomputing/tempo/ticker'), - ]) + const { Tempo } = await import('@magmacomputing/tempo') if (import.meta.env.DEV) registry.has = originalHas - if (!Tempo.ticker) Tempo.extend(TickerModule) Tempo.init() return Tempo @@ -143,7 +139,10 @@ async function startTicker() { if (isManualTickerPaused.value) return - ticker = Tempo.ticker({ seconds: 1, seed: { timeZone: selectedTz.value } }, sync) + ticker = { + _id: setInterval(() => sync(new Tempo({ timeZone: selectedTz.value })), 1000), + stop() { clearInterval(this._id) } + } } catch (e) { timeStr.value = `Error: ${e.message || 'Unknown'}` const fallback = () => { diff --git a/packages/tempo/package.json b/packages/tempo/package.json index bde5bfe5..514bc1a4 100644 --- a/packages/tempo/package.json +++ b/packages/tempo/package.json @@ -1,6 +1,6 @@ { "name": "@magmacomputing/tempo", - "version": "2.11.2", + "version": "3.0.0", "description": "The Tempo core library", "author": "Magma Computing Solutions", "license": "MIT", @@ -59,9 +59,6 @@ "#tempo/mutate": { "default": "./dist/module/module.mutate.js" }, - "#tempo/ticker": { - "default": "./dist/plugin/extend/extend.ticker.js" - }, "#tempo/term": { "default": "./dist/plugin/term/term.index.js" }, @@ -134,10 +131,6 @@ "types": "./dist/plugin/*.d.ts", "import": "./dist/plugin/*.js" }, - "./ticker": { - "types": "./dist/plugin/extend/extend.ticker.d.ts", - "import": "./dist/plugin/extend/extend.ticker.js" - }, "./duration": { "types": "./dist/module/module.duration.d.ts", "import": "./dist/module/module.duration.js" @@ -150,6 +143,10 @@ "types": "./dist/module/module.format.d.ts", "import": "./dist/module/module.format.js" }, + "./ticker": { + "types": "./dist/plugin/extend/extend.ticker.d.ts", + "import": "./dist/plugin/extend/extend.ticker.js" + }, "./parse": { "types": "./dist/module/module.parse.d.ts", "import": "./dist/module/module.parse.js" @@ -181,12 +178,12 @@ "test:dist": "cross-env TEST_DIST=true vitest run", "test:ci": "cross-env TZ=America/New_York LANG=en_US.UTF-8 LC_ALL=en_US.UTF-8 vitest run", "test:ci:prefilter": "cross-env TZ=America/New_York LANG=en_US.UTF-8 LC_ALL=en_US.UTF-8 TEMPO_PREFILTER_CI=true vitest run", - "repl": "tsx --conditions=development -i --import ./bin/temporal-polyfill.ts --import ./bin/repl.ts", + "repl": "tsx --tsconfig ./src/tsconfig.repl.json -i --import ./bin/temporal-polyfill.ts --import ./bin/repl.ts", "repl:dist": "tsx -i --import ./bin/temporal-polyfill.ts --import ./bin/repl.ts", - "repl:node": "tsx --conditions=development -i --harmony-temporal --import ./bin/repl.ts", - "repl:bare": "tsx --conditions=development -i --harmony-temporal", - "repl:core": "cross-env TEMPO_LITE=true tsx --conditions=development -i --harmony-temporal --import ./bin/core.ts", - "parse": "cross-env TEMPO_LITE=true tsx --conditions=development -i --harmony-temporal --import ./bin/parse.ts", + "repl:node": "tsx --tsconfig ./src/tsconfig.repl.json -i --harmony-temporal --import ./bin/repl.ts", + "repl:bare": "tsx --tsconfig ./src/tsconfig.repl.json -i --harmony-temporal", + "repl:core": "cross-env TEMPO_LITE=true tsx --tsconfig ./src/tsconfig.repl.json -i --harmony-temporal --import ./bin/core.ts", + "parse": "cross-env TEMPO_LITE=true tsx --tsconfig ./src/tsconfig.repl.json -i --harmony-temporal --import ./bin/parse.ts", "build": "npm run clean && tsc -b && npm run build:bundle && npm run build:resolve", "build:bundle": "rollup -c", "build:resolve": "tsx bin/resolve-types.ts", @@ -196,7 +193,8 @@ "docs:api": "typedoc", "docs:dev": "npm run build && npm run docs:api && vitepress dev", "docs:build": "npm run build && npm run docs:api && vitepress build", - "docs:preview": "vitepress preview" + "docs:preview": "vitepress preview", + "docs:push": "bash ./bin/push-docs.sh" }, "files": [ "dist/", @@ -207,10 +205,9 @@ }, "devDependencies": { "@js-temporal/polyfill": "^0.5.1", - "@magmacomputing/library": "2.11.2", + "@magmacomputing/library": "3.0.0", "@rollup/plugin-alias": "^6.0.0", - "esbuild": "^0.25.12", - "javascript-obfuscator": "^5.4.2", + "javascript-obfuscator": "^5.4.3", "magic-string": "^0.30.21", "rollup-plugin-esbuild": "^6.2.1", "typedoc": "^0.28.19", diff --git a/packages/tempo/rollup.config.js b/packages/tempo/rollup.config.js index 798752bf..a396d3bc 100644 --- a/packages/tempo/rollup.config.js +++ b/packages/tempo/rollup.config.js @@ -13,10 +13,25 @@ const distPath = path.join(__dirname, 'dist'); const licensePremium = process.env.TEMPO_LICENSE_PATH ? path.resolve(process.env.TEMPO_LICENSE_PATH) : undefined; const licenseDefault = path.resolve(__dirname, './src/support/support.license.ts'); -const isPremiumAvailable = !!( + +const foundTsconfigPath = (() => { + if (!licensePremium) return ''; + let dir = path.dirname(licensePremium); + while (dir !== path.resolve(dir, '..')) { + const p = path.resolve(dir, 'tsconfig.json'); + if (fs.existsSync(p)) return p; + dir = path.resolve(dir, '..'); + } + return ''; +})(); + +if (licensePremium && !foundTsconfigPath) + throw new Error(`TEMPO_LICENSE_PATH is set to ${licensePremium} but no ancestor tsconfig.json was found.`); + +const isPremiumAvailable = Boolean( licensePremium && fs.existsSync(licensePremium) && - fs.existsSync(path.resolve(path.dirname(licensePremium), '../tsconfig.json')) + fs.existsSync(foundTsconfigPath) ); const licensePath = isPremiumAvailable ? licensePremium : licenseDefault; @@ -62,13 +77,11 @@ function getFiles(dir, suffix = '.js') { const entryPoints = Object.fromEntries( getFiles(distPath) .map(file => [path.relative(distPath, file).replace(/\.js$/, ''), file]) - .filter(([key]) => key !== 'support/support.license') + .filter(([key]) => !isPremiumAvailable || key !== 'support/support.license') ); export default [ - // 1. 🛡️ LICENSE MONOLITH - // Bundles 'jose' and the license logic into a single heavily obfuscated file - { + ...(isPremiumAvailable ? [{ input: licensePath, output: { file: 'dist/support/support.license.js', // Overwrites the tsc output stealthily @@ -99,7 +112,7 @@ export default [ } } ] - }, + }] : []), // 2. 🌐 GLOBAL IIFE BUNDLE { @@ -164,7 +177,7 @@ export default [ // Map library imports to lib/ for browser-ready granular ESM const rel = path.relative(__dirname, id); - const normalizedRel = rel.replace(/\\/g, '/'); // Ensure forward slashes + const normalizedRel = rel.replace(/\\/g, '/'); // Ensure forward slashes if (id.includes('magma/packages/library') || rel.startsWith('../library')) { const match = normalizedRel.match(/library\/(?:src|dist\/common)\/(.*)$/); diff --git a/packages/tempo/src/engine/engine.alias.ts b/packages/tempo/src/engine/engine.alias.ts index 948d560e..c34f67cf 100644 --- a/packages/tempo/src/engine/engine.alias.ts +++ b/packages/tempo/src/engine/engine.alias.ts @@ -15,9 +15,8 @@ */ import type { Nullable } from '#library/type.library.js'; -import type { Logify } from '#library/logify.class.js'; -import { isDefined, isFunction, isZonedDateTime } from '#library/assertion.library.js'; -import { Match } from '#tempo/support'; +import { isDefined, isFunction } from '#library/assertion.library.js'; +import { Match, logError, logWarn } from '#tempo/support'; import { ownEntries } from '#library/primitive.library.js'; import * as t from '../tempo.type.js'; @@ -28,7 +27,7 @@ type State = Record export interface AliasResult { value: string; - key: string; // The normalized baseWord (e.g. 'noon') + key: string; // the normalized baseWord (e.g. 'noon') type: AliasType; source: 'global' | 'local'; isClock: boolean; @@ -37,7 +36,6 @@ export interface AliasResult { export interface AliasEngineOptions { parent?: Nullable; - logger?: Nullable; config?: Nullable; } interface Registry { // information about each registered alias @@ -52,9 +50,9 @@ interface Registry { // information about each registered ali export class AliasEngine { static aliasPattern = /^(evt|per)(\d+)_(\d+)$/; - static #idCounter = 0; + private static _idCounter = 0; - static #getBaseWord(s: string): string { + private static _getBaseWord(s: string): string { return s .toLowerCase() .replace(/\[[^\]]*\]\?/g, '') @@ -63,7 +61,6 @@ export class AliasEngine { } #parent?: AliasEngineOptions["parent"]; - #logger?: AliasEngineOptions["logger"]; #config?: AliasEngineOptions["config"]; #depth: number; // the depth of this engine in the proto chain @@ -82,18 +79,17 @@ export class AliasEngine { constructor(options = {} as AliasEngineOptions) { const parent = options.parent; - this.#logger = options.logger; this.#config = options.config; - this.#id = AliasEngine.#idCounter++; + this.#id = AliasEngine._idCounter++; if (parent instanceof AliasEngine) { this.#parent = parent; this.#depth = parent.#depth + 1; - this.#state = Object.create(parent.#state); // create a new state object that inherits from the parent engine's state - this.#words = Object.create(parent.#words); // create a new words object that inherits from the parent engine's words for collision detection + this.#state = Object.create(parent.#state); // create a new state object that inherits from the parent engine's state + this.#words = Object.create(parent.#words); // create a new words object that inherits from the parent engine's words for collision detection } else { if (parent) - this.#logger?.error(this.#config, "Parent engine must be an instance of AliasEngine"); + logError("Parent engine must be an instance of AliasEngine", this.#config); this.#parent = null; this.#depth = 0; @@ -113,7 +109,7 @@ export class AliasEngine { */ registerAliases(type: AliasType, events: [string, AliasTarget][]) { for (const [name, target] of events) { - const baseWord = AliasEngine.#getBaseWord(name); + const baseWord = AliasEngine._getBaseWord(name); const existingKey = this.#words[baseWord]; const existing = existingKey ? this.getAlias(existingKey) : undefined; @@ -125,9 +121,10 @@ export class AliasEngine { const aliasKey = `${type}${this.#depth}_${index}` as AliasKey; const shouldOverwrite = !(existing?.type === 'evt' && type === 'per'); - if (this.#logger && baseWord in this.#words) { - this.#logger.warn(this.#config, - `[AliasEngine] Collision detected for ${type} alias "${name}". ${shouldOverwrite ? 'Overwriting' : 'Preserving'} existing alias.` + if (baseWord in this.#words) { + logWarn( + `[AliasEngine] Collision detected for ${type} alias "${name}". ${shouldOverwrite ? 'Overwriting' : 'Preserving'} existing alias.`, + this.#config ); } @@ -139,7 +136,7 @@ export class AliasEngine { target, // string, number, or function type, // 'evt' or 'per' baseWord, // used for collision detection - collision: Boolean(existingKey), + collision: isDefined(existingKey), depth: this.#depth, } } @@ -187,7 +184,7 @@ export class AliasEngine { } hasAlias(name: string, type?: AliasType) { - const baseWord = AliasEngine.#getBaseWord(name); + const baseWord = AliasEngine._getBaseWord(name); const key = this.#words[baseWord]; return !key ? false diff --git a/packages/tempo/src/engine/engine.composer.ts b/packages/tempo/src/engine/engine.composer.ts index 127df27c..d0900e51 100644 --- a/packages/tempo/src/engine/engine.composer.ts +++ b/packages/tempo/src/engine/engine.composer.ts @@ -2,7 +2,7 @@ import { getTemporalIds } from '#library/temporal.library.js'; import { isInstant, isZonedDateTime, isPlainDate, isPlainDateTime } from '#library/assertion.library.js'; import type { TemporalObject, TypeValue } from '#library/type.library.js'; -import { isTempo, logError } from '#tempo/support'; +import { isTempo, logError, logDebug } from '#tempo/support'; import type { Tempo } from '#tempo/tempo.class.js'; import * as t from '../tempo.type.js'; @@ -59,7 +59,7 @@ export function compose( try { temporal = Temporal.PlainDateTime.from(value, { overflow: 'constrain' }); } catch (err2) { - logError(config, `[Tempo#composer] Unrecognized or invalid ISO 8601 string: "${value}"`); + logError(`[Tempo#composer] Unrecognized or invalid ISO 8601 string: "${value}"`, config); return { dateTime: today }; } } @@ -94,7 +94,7 @@ export function compose( case 'BigInt': { if (type === 'Number' && !Number.isFinite(value)) { - logError(config, `Invalid Tempo number: ${value}`); + logError(`Invalid Tempo number: ${value}`, config); temporal = today; break; } @@ -163,10 +163,11 @@ export function compose( break; default: { - logError(config, `Cannot convert ${type} (value: ${String(temporal)}) to ZonedDateTime`); + logError(`Cannot convert ${type} (value: ${String(temporal)}) to ZonedDateTime`, config); return { dateTime: today }; } } + logDebug(`[Composer] Composed final DateTime: ${dateTime?.toString()}`, config); return { dateTime: dateTime ?? today, timeZone }; } diff --git a/packages/tempo/src/engine/engine.guard.ts b/packages/tempo/src/engine/engine.guard.ts index 33a02b9a..94781feb 100644 --- a/packages/tempo/src/engine/engine.guard.ts +++ b/packages/tempo/src/engine/engine.guard.ts @@ -1,4 +1,4 @@ -import { isString, isSymbol } from '#library/assertion.library.js'; +import { isString, isSymbol, isDefined } from '#library/assertion.library.js'; import { Match } from '#tempo/support/support.default.js'; /** @@ -16,7 +16,7 @@ export interface MasterGuard { */ export function createMasterGuard(words: (string | symbol)[]): MasterGuard { const wordsList = words - .filter(w => isString(w) || (isSymbol(w) && !!w.description)) + .filter(w => isString(w) || (isSymbol(w) && isDefined(w.description))) .map(w => (isSymbol(w) ? w.description! : (w as string)).toLowerCase()) .filter(Boolean); diff --git a/packages/tempo/src/engine/engine.lexer.ts b/packages/tempo/src/engine/engine.lexer.ts index 56d5b6bd..7e19a802 100644 --- a/packages/tempo/src/engine/engine.lexer.ts +++ b/packages/tempo/src/engine/engine.lexer.ts @@ -2,7 +2,7 @@ import '#library/temporal.polyfill.js'; import { isString, isEmpty, isUndefined, isDefined, isTemporal, isInstant } from '#library/assertion.library.js'; import { ownKeys, ownEntries } from '#library/primitive.library.js'; import { pad, singular } from '#library/string.library.js'; -import { Match, enums, isTempo, logError, logWarn } from '#tempo/support'; +import { Match, enums, isTempo, logError, logWarn, logDebug } from '#tempo/support'; import * as t from '../tempo.type.js'; /** @@ -130,7 +130,7 @@ export function parseWeekday(groups: t.Groups, dateTime: Temporal.ZonedDateTime, return dateTime; if (!isEmpty(mod) && !isEmpty(sfx)) { - logWarn(config, `Cannot provide both a modifier '${mod}' and suffix '${sfx}'`); + logWarn(`Cannot provide both a modifier '${mod}' and suffix '${sfx}'`, config); return dateTime; } @@ -139,7 +139,7 @@ export function parseWeekday(groups: t.Groups, dateTime: Temporal.ZonedDateTime, const offset = (enums.WEEKDAY as any)[weekday] ?? (enums.WEEKDAYS as any)[weekday]; if (!Number.isFinite(offset)) { - logError(config, `Invalid weekday token: "${wkd}"`); + logError(`Invalid weekday token: "${wkd}"`, config); return dateTime; } @@ -151,7 +151,9 @@ export function parseWeekday(groups: t.Groups, dateTime: Temporal.ZonedDateTime, delete groups["nbr"]; delete groups["sfx"]; - return dateTime.add({ days }); + const finalDateTime = dateTime.add({ days }); + logDebug(`[Lexer] Applied weekday offset of ${days} days`, config); + return finalDateTime; } /** resolve a date pattern match */ @@ -167,7 +169,7 @@ export function parseDate(groups: t.Groups, dateTime: Temporal.ZonedDateTime, co return dateTime; if (!isEmpty(mod) && !isEmpty(afx)) { - logWarn(config, `Cannot provide both a modifier '${mod}' and suffix '${afx}'`); + logWarn(`Cannot provide both a modifier '${mod}' and suffix '${afx}'`, config); return dateTime; } @@ -220,13 +222,16 @@ export function parseDate(groups: t.Groups, dateTime: Temporal.ZonedDateTime, co delete groups["afx"]; if (!Number.isFinite(year) || !Number.isFinite(month) || !Number.isFinite(day)) { - logError(config, `Invalid Date components: year=${year}, month=${month}, day=${day}`); + logError(`Invalid Date components: year=${year}, month=${month}, day=${day}`, config); return dateTime; } - return Temporal.PlainDate.from({ year, month, day }, { overflow: 'constrain' }) + const finalDateTime = Temporal.PlainDate.from({ year, month, day }, { overflow: 'constrain' }) .toZonedDateTime(tz) .withPlainTime(dateTime.toPlainTime()); + + logDebug(`[Lexer] Resolved Date components to ${year}-${month}-${day}`, config); + return finalDateTime; } /** resolve a time pattern match */ @@ -251,7 +256,9 @@ export function parseTime(groups: t.Groups = {}, dateTime: Temporal.ZonedDateTim if (groups["mer"]?.toLowerCase() === 'am' && hh >= 12) hh -= 12; - return dateTime.withPlainTime({ hour: hh, minute: mi, second: ss, millisecond: ms, microsecond: us, nanosecond: ns }); + const finalDateTime = dateTime.withPlainTime({ hour: hh, minute: mi, second: ss, millisecond: ms, microsecond: us, nanosecond: ns }); + logDebug(`[Lexer] Resolved Time components to ${pad(hh)}:${pad(mi)}:${pad(ss)}`, undefined); + return finalDateTime; } /** @@ -286,5 +293,8 @@ export function parseZone(groups: t.Groups, dateTime: Temporal.ZonedDateTime, co delete groups["cal"]; delete groups["tzd"]; + if (zone || cal) + logDebug(`[Lexer] Applied Zone/Calendar adjustments: Zone=${zone ?? 'unchanged'}, Calendar=${cal ?? 'unchanged'}`, config); + return dateTime; } diff --git a/packages/tempo/src/engine/engine.normalizer.ts b/packages/tempo/src/engine/engine.normalizer.ts index 7a419e4a..0b8b8143 100644 --- a/packages/tempo/src/engine/engine.normalizer.ts +++ b/packages/tempo/src/engine/engine.normalizer.ts @@ -3,7 +3,7 @@ import { getTemporalIds, instant } from '#library/temporal.library.js'; import { ownKeys } from '#library/primitive.library.js'; import type { TypeValue } from '#library/type.library.js'; -import { getRuntime, sym, Match } from '#tempo/support'; +import { getRuntime, sym, Match, logError, logDebug, TempoError } from '#tempo/support'; import { prefix, parseWeekday, parseDate, parseTime, parseZone } from './engine.lexer.js'; import { resolveTermMutation } from './engine.term.js'; import enums from '#tempo/support/support.enum.js'; @@ -167,7 +167,7 @@ export function resolveAliases( if (resolvingKeys.size > MAX_TEMPO_RESOLVE_DEPTH || resolvingKeys.has(aliasKey)) { const msg = `Infinite recursion detected in Tempo resolution for: ${aliasKey}`; state.errored = true; - if (TempoClass) (TempoClass as any)[sym.$logError](state.config, new RangeError(msg)); + logError(new RangeError(msg), state.config); delete groups[key]; continue; } @@ -178,6 +178,8 @@ export function resolveAliases( const host = getAliasContext(ctx, dateTime); const res = aliasEngine?.resolveAlias(key as any, host); if (!res) continue; + + logDebug(`[Normalizer] Resolved alias '${aliasKey}'`, state.config); try { const mapped = ({ @@ -186,7 +188,7 @@ export function resolveAliases( } as const)[res.type as 'evt' | 'per']; if (!mapped) - throw new Error(`[ParseEngine] Unexpected AliasType: ${res.type}`); + throw new TempoError(`[ParseEngine] Unexpected AliasType: ${res.type}`); const { type, pat } = mapped; @@ -225,8 +227,10 @@ export function resolveAliases( const mm = prefix(groups["mm"] as t.MONTH); const monthVal = enums.MONTH[mm]; - if (isDefined(monthVal)) + if (isDefined(monthVal)) { groups["mm"] = monthVal.toString().padStart(2, '0'); + logDebug(`[Normalizer] Normalized month string '${mm}' to ${groups["mm"]}`, state.config); + } } return dateTime; diff --git a/packages/tempo/src/engine/engine.pattern.ts b/packages/tempo/src/engine/engine.pattern.ts index 9c0105d2..2721935d 100644 --- a/packages/tempo/src/engine/engine.pattern.ts +++ b/packages/tempo/src/engine/engine.pattern.ts @@ -49,7 +49,7 @@ export class PatternCompiler { const matcher = (source: string, d = 0): string => { if (d > 10) { // Emit a diagnostic if recursion limit is hit (likely circular placeholder) - logWarn(this.#state.config, `[PatternCompiler] Recursion limit exceeded in matcher (d > 10) for src:`, source, `depth:`, d); + logWarn(`[PatternCompiler] Recursion limit exceeded in matcher (d > 10) for src:`, this.#state.config, source, `depth:`, d); return source; } @@ -96,7 +96,7 @@ export class PatternCompiler { return compiled; } catch (e: any) { // Use the computed source for fallback, do not cache fallback, and log error - logError(this.#state.config, { context: 'pattern compile failed', pattern: source }, e); + logError(e, this.#state.config, { context: 'pattern compile failed', pattern: source }); return new RegExp(`^${Match.escape(source)}$`, 'i'); } } diff --git a/packages/tempo/src/engine/engine.planner.ts b/packages/tempo/src/engine/engine.planner.ts index 9549e03c..f934ae75 100644 --- a/packages/tempo/src/engine/engine.planner.ts +++ b/packages/tempo/src/engine/engine.planner.ts @@ -1,4 +1,5 @@ import { ownEntries } from '#library/primitive.library.js'; +import { isDefined } from '#library/assertion.library.js'; import type * as t from '../tempo.type.js'; const AGO_HENCE_RE = /\b(ago|hence|from\s+now|prior)\b/i; @@ -139,7 +140,7 @@ export function selectLayoutPatterns( : (state.parse.token?.[String(layoutKey)] as symbol | undefined); return [symKey, symKey ? state.parse.pattern.get(symKey) : undefined] as const; }) - .filter((entry): entry is readonly [symbol, RegExp] => Boolean(entry[0] && entry[1])); + .filter((entry): entry is readonly [symbol, RegExp] => isDefined(entry[0]) && isDefined(entry[1])); if (options.enablePrefilter !== true) { if (wantsPlan) { diff --git a/packages/tempo/src/engine/engine.term.ts b/packages/tempo/src/engine/engine.term.ts index 2187ae66..602f0663 100644 --- a/packages/tempo/src/engine/engine.term.ts +++ b/packages/tempo/src/engine/engine.term.ts @@ -53,7 +53,7 @@ export function resolveTermMutation(Tempo: TempoTermType, instance: Tempo, mutat const slick = slickStr.match(Match.slick) || (isString(offset) ? offset.match(Match.slickValue) : null); const { groups } = (slick || {}) as any; if (groups) { - const hasMod = !!groups.sh_mod; + const hasMod = isDefined(groups.sh_mod); const hasNbr = isNumeric(groups.sh_nbr); mod = hasMod ? groups.sh_mod : undefined; nbr = hasNbr ? Number(groups.sh_nbr) : 1; @@ -64,7 +64,7 @@ export function resolveTermMutation(Tempo: TempoTermType, instance: Tempo, mutat // 0. Handle relative .add() — preserving position within the target range if (mutate === 'add') { - const slickParsed = !!slickStr; + const slickParsed = isDefined(slickStr); const directional = mod && !['this', '>=', '<='].includes(mod); const numericOffset = !directional && isNumeric(offset); diff --git a/packages/tempo/src/library.index.ts b/packages/tempo/src/library.index.ts index 9a7293a4..d1513c1d 100644 --- a/packages/tempo/src/library.index.ts +++ b/packages/tempo/src/library.index.ts @@ -5,9 +5,13 @@ */ export { Pledge } from '#library/pledge.class.js'; -export { Cipher } from '#library/cipher.class.js'; +export * as cipher from '#library/cipher.library.js'; +export * as webToken from '#library/webtoken.library.js'; export { enumify, type Enum } from '#library/enumerate.library.js'; export { proxify } from '#library/proxy.library.js'; export { stringify, objectify, cloneify } from '#library/serialize.library.js'; +export { isObject, isFunction, isDefined, isUndefined, isEmpty, isNumeric, isFiniteNumber } from '#library/assertion.library.js'; +export { asArray } from '#library/coercion.library.js'; +export { instant, normaliseFractionalDurations } from '#library/temporal.library.js'; export type { OwnOf, KeyOf, ValueOf, EntryOf } from '#library/type.library.js'; diff --git a/packages/tempo/src/module/module.benchmark.ts b/packages/tempo/src/module/module.benchmark.ts new file mode 100644 index 00000000..9b11fb5c --- /dev/null +++ b/packages/tempo/src/module/module.benchmark.ts @@ -0,0 +1,150 @@ +import { Tempo } from '#tempo'; +import type { Options } from '#tempo/tempo.type.js'; + +import { getContext, CONTEXT } from '#library/utility.library.js'; + +export interface BenchmarkConfig { + /** Array of raw strings to parse. */ + data: string[]; + /** Number of times to loop the dataset (helps smooth out JIT compiler warmup). Default: 1 */ + iterations?: number; + /** Array of Tempo hydration modes to compare. Default: ['auto'] */ + modes?: Array<'auto' | 'strict' | 'defer'>; + /** Compare the dataset against native `new Date()` for a baseline speed/failure metric. Default: false */ + baseline?: boolean; +} + +export interface BenchmarkResult { + name: string; + totalTimeMs: number; + microSecPerOp: number; + successCount: number; + failureCount: number; + successRate: string; + heapUsedDeltaMb?: string; +} + +export class BenchmarkModule { + /** + * Run the benchmark against the provided configuration. + */ + static run(TempoClass: any, config: BenchmarkConfig): BenchmarkResult[] { + const results: BenchmarkResult[] = []; + const iterations = config.iterations && config.iterations > 0 ? config.iterations : 1; + const modes = config.modes || ['auto']; + const totalOps = config.data.length * iterations; + + if (totalOps === 0) return []; + + const { type, global } = getContext(); + + // Helper to measure memory (Node.js only) + const getHeap = (): number => type === CONTEXT.NodeJS && global.process?.memoryUsage ? global.process.memoryUsage().heapUsed : 0; + // Helper to measure time (Performance API if available, else Date) + const getTime = (): number => typeof global.performance !== 'undefined' ? global.performance.now() : Date.now(); + + // Baseline (Native Date) + if (config.baseline) { + let success = 0; + let failure = 0; + + const startMem = getHeap(); + const startTime = getTime(); + + for (let i = 0; i < iterations; i++) { + for (let j = 0; j < config.data.length; j++) { + const d = new Date(config.data[j]); + if (isNaN(d.getTime())) { + failure++; + } else { + success++; + } + } + } + + const endTime = getTime(); + const endMem = getHeap(); + + const totalTimeMs = endTime - startTime; + results.push({ + name: 'Native Date', + totalTimeMs: Number(totalTimeMs.toFixed(2)), + microSecPerOp: totalTimeMs > 0 ? Number(((totalTimeMs * 1000) / totalOps).toFixed(2)) : 0, + successCount: success, + failureCount: failure, + successRate: ((success / totalOps) * 100).toFixed(1) + '%', + ...(endMem > 0 ? { heapUsedDeltaMb: ((endMem - startMem) / 1024 / 1024).toFixed(2) } : {}) + }); + } + + // Tempo Modes + for (const mode of modes) { + let success = 0; + let failure = 0; + + const startMem = getHeap(); + const startTime = getTime(); + + for (let i = 0; i < iterations; i++) { + for (let j = 0; j < config.data.length; j++) { + try { + const t = new TempoClass(config.data[j], { mode }); + // For mode: 'defer', the parsing is deferred. To actually measure parse time, + // we must access a property to trigger hydration, otherwise it's just measuring proxy creation. + // We'll access .isValid which safely triggers hydration. + if (t.isValid) { + success++; + } else { + failure++; + } + } catch { + failure++; + } + } + } + + const endTime = getTime(); + const endMem = getHeap(); + + const totalTimeMs = endTime - startTime; + results.push({ + name: `Tempo (mode: ${mode})`, + totalTimeMs: Number(totalTimeMs.toFixed(2)), + microSecPerOp: totalTimeMs > 0 ? Number(((totalTimeMs * 1000) / totalOps).toFixed(2)) : 0, + successCount: success, + failureCount: failure, + successRate: ((success / totalOps) * 100).toFixed(1) + '%', + ...(endMem > 0 ? { heapUsedDeltaMb: ((endMem - startMem) / 1024 / 1024).toFixed(2) } : {}) + }); + } + + return results; + } + + /** + * Print the benchmark results as a console table. + */ + static printTable(results: BenchmarkResult[]): void { + if (results.length === 0) { + console.log('No benchmark results to display.'); + return; + } + + // Clean up undefined properties for the table + const tableData = results.map(r => { + const clean: any = { + 'Engine': r.name, + 'Total Time (ms)': r.totalTimeMs, + 'µs / Op': r.microSecPerOp, + 'Success Rate': r.successRate, + 'Failed': r.failureCount + }; + if (r.heapUsedDeltaMb !== undefined) { + clean['Heap Delta (MB)'] = r.heapUsedDeltaMb; + } + return clean; + }); + + console.table(tableData); + } +} diff --git a/packages/tempo/src/module/module.duration.ts b/packages/tempo/src/module/module.duration.ts index 20383129..4577e7e0 100644 --- a/packages/tempo/src/module/module.duration.ts +++ b/packages/tempo/src/module/module.duration.ts @@ -3,10 +3,10 @@ import { isString, isObject, isDefined, isUndefined, isFunction } from '#library import { singular } from '#library/string.library.js'; import { getAccessors } from '#library/reflection.library.js'; import { ifDefined } from '#library/object.library.js'; -import { getRelativeTime, formatNumber } from '#library/international.library.js'; +import { getRelativeTime, formatNumber, formatDuration, formatList } from '#library/international.library.js'; import { defineInterpreterModule, interpret, type TempoModule } from '../plugin/plugin.util.js'; -import { enums, isTempo } from '#tempo/support'; +import { enums, isTempo, TempoError } from '#tempo/support'; import { Tempo } from '../tempo.class.js'; declare module '../tempo.class.js' { @@ -37,7 +37,7 @@ declare module '#library/type.library.js' { /** * Convert a Temporal.Duration to a full Tempo.Duration object (EDO). */ -function toDuration(dur: Temporal.Duration, ctx: { relativeTo?: any, locale?: string, numberFormat?: any } = {}): Tempo.Duration { +function toDuration(dur: Temporal.Duration, ctx: { relativeTo?: any, locale?: string, numberFormat?: any, durationFormat?: any } = {}): Tempo.Duration { const edo = getAccessors(dur) .reduce((acc, d) => Object.assign(acc, ifDefined({ [d]: (dur as any)[d] })), { @@ -78,7 +78,7 @@ function toDuration(dur: Temporal.Duration, ctx: { relativeTo?: any, locale?: st // Strict Temporal balancing const anchor = customAnchor || ctx.relativeTo; if (!anchor) - throw new Error("A relativeTo anchor is required for strict balancing. Pass an anchor or use { nominal: true } for mathematical balancing."); + throw new TempoError("A relativeTo anchor is required for strict balancing. Pass an anchor or use { nominal: true } for mathematical balancing."); const balanced = dur.round({ largestUnit, relativeTo: anchor }); @@ -90,34 +90,37 @@ function toDuration(dur: Temporal.Duration, ctx: { relativeTo?: any, locale?: st Object.defineProperty(edo, 'format', { value: function (opts: any = {}) { const { locales, ...intlOpts } = opts; + const locale = locales || ctx.locale; + + if (isFunction(ctx.durationFormat)) + return ctx.durationFormat(this); + + // 1. Native Intl.DurationFormat + if ('DurationFormat' in Intl) + return formatDuration(this, locale, { ...(ctx.durationFormat || {}), ...intlOpts }); + + // 2. Fallback Polyfill (combine all non-zero units) + const units = ['years', 'months', 'weeks', 'days', 'hours', 'minutes', 'seconds', 'milliseconds', 'microseconds', 'nanoseconds'] as const; + const parts: string[] = []; + + for (const u of units) { + const val = this[u]; + if (val) { + const unitName = singular(u); // singularize unit name (e.g., 'years' -> 'year') + parts.push( + formatNumber(val, locale, { + style: 'unit', + unit: unitName, + unitDisplay: 'long', + ...(ctx.numberFormat || {}), + }) + ); + } + } - // Find the largest non-zero unit to format. - let val = 0; - let u = ''; - if (this.years) { val = this.years; u = 'year'; } - else if (this.months) { val = this.months; u = 'month'; } - else if (this.weeks) { val = this.weeks; u = 'week'; } - else if (this.days) { val = this.days; u = 'day'; } - else if (this.hours) { val = this.hours; u = 'hour'; } - else if (this.minutes) { val = this.minutes; u = 'minute'; } - else if (this.seconds) { val = this.seconds; u = 'second'; } - else if (this.milliseconds) { val = this.milliseconds; u = 'millisecond'; } - else if (this.microseconds) { val = this.microseconds; u = 'microsecond'; } - else if (this.nanoseconds) { val = this.nanoseconds; u = 'nanosecond'; } - - if (!u) return '0'; // or some fallback - - if (isFunction(ctx.numberFormat)) - return ctx.numberFormat(val, u); + if (parts.length === 0) return '0'; // fallback for completely empty duration - const locale = locales || ctx.locale; - return formatNumber(val, locale, { - style: 'unit', - unit: u, - unitDisplay: 'long', - ...(ctx.numberFormat || {}), - ...intlOpts - }); + return formatList(parts, locale, 'conjunction', 'long'); }, enumerable: false }); @@ -183,7 +186,8 @@ function duration(this: Tempo, type: 'until' | 'since', arg?: any, until?: any) if (isUndefined(unit) || since) { const locale = (this as any)?.config?.locale; const numberFormat = opts['intl']?.numberFormat || (this as any)?.config?.intl?.numberFormat; - const res = toDuration(dur, { relativeTo: selfZdt, locale, numberFormat }); + const durationFormat = opts['intl']?.durationFormat || (this as any)?.config?.intl?.durationFormat; + const res = toDuration(dur, { relativeTo: selfZdt, locale, numberFormat, durationFormat }); if (unit) res.unit = unit; if (!since) return res; @@ -195,8 +199,8 @@ function duration(this: Tempo, type: 'until' | 'since', arg?: any, until?: any) .map(Math.abs) .map(nbr => nbr.toString().padStart(3, '0')) .join('') - const rtConfig = (this as any).config.intl?.relativeTime; - const rtOptions = opts['intl']?.relativeTime || opts['relativeTime']; + const rtConfig = (this as any).config.intl?.relativeTimeFormat; + const rtOptions = opts['intl']?.relativeTimeFormat; const rtf = (isFunction(rtOptions) ? rtOptions : rtOptions?.format) || (isFunction(rtConfig) ? rtConfig : rtConfig?.format) @@ -206,7 +210,7 @@ function duration(this: Tempo, type: 'until' | 'since', arg?: any, until?: any) const su = singular(u); if (isFunction(rtf)) return rtf(val, su); if (rtf instanceof Intl.RelativeTimeFormat) return rtf.format(val, su); - const style = rtOptions?.style || rtConfig?.style || opts['intl']?.relativeTime?.style || opts['rtfStyle'] || (this as any).config.intl?.relativeTime?.style || (this as any).config['rtfStyle'] || 'narrow'; + const style = rtOptions?.style || rtConfig?.style || opts['intl']?.relativeTimeFormat?.style || opts['rtfStyle'] || (this as any).config.intl?.relativeTimeFormat?.style || (this as any).config['rtfStyle'] || 'narrow'; return getRelativeTime(val, su as Intl.RelativeTimeFormatUnit, locale, style); } @@ -247,7 +251,8 @@ export const DurationModule: TempoModule = defineInterpreterModule('DurationModu duration(this: typeof Tempo, input: any) { const ctx = { locale: this.config?.locale, - numberFormat: this.config?.intl?.numberFormat + numberFormat: this.config?.intl?.numberFormat, + durationFormat: this.config?.intl?.durationFormat, }; return interpret(this, 'DurationModule', 'toDuration', false, input, ctx); } diff --git a/packages/tempo/src/module/module.format.ts b/packages/tempo/src/module/module.format.ts index a2023ad5..a4723797 100644 --- a/packages/tempo/src/module/module.format.ts +++ b/packages/tempo/src/module/module.format.ts @@ -1,5 +1,6 @@ import '#library/temporal.polyfill.js'; import { pad } from '#library/string.library.js'; +import { suffix } from '#library/number.library.js'; import { ifNumeric } from '#library/coercion.library.js'; import { isString, isObject, isZonedDateTime, isInstant, isPlainDate, isPlainDateTime, isUndefined } from '#library/assertion.library.js'; import { delegator } from '#library/proxy.library.js'; @@ -94,6 +95,9 @@ export function format(obj?: Temporal.ZonedDateTime | any, fmt?: string | symbol case 'wkd': return enums.WEEKDAYS.keyOf(zdt.dayOfWeek as any); case 'www': return enums.WEEKDAY.keyOf(zdt.dayOfWeek as any); case 'ww': return pad(zdt.weekOfYear); + case 'DAY': return suffix(zdt.day); + case 'WW': return suffix(zdt.weekOfYear); + case 'MM': return suffix(zdt.month); case 'hh': return pad(zdt.hour); case 'HH': return pad(zdt.hour > 12 ? zdt.hour % 12 : zdt.hour || 12); case 'mer': return zdt.hour >= 12 ? 'pm' : 'am'; @@ -104,7 +108,10 @@ export function format(obj?: Temporal.ZonedDateTime | any, fmt?: string | symbol case 'us': return pad(zdt.microsecond, 3); case 'ns': return pad(zdt.nanosecond, 3); case 'ff': return `${pad(zdt.millisecond, 3)}${pad(zdt.microsecond, 3)}${pad(zdt.nanosecond, 3)}`; - case 'hhmiss': return `${pad(zdt.hour)}${pad(zdt.minute)}${pad(zdt.second)}`; + case 'dmy': return `${pad(zdt.day)}${pad(zdt.month)}${pad(zdt.year, 4)}`; + case 'mdy': return `${pad(zdt.month)}${pad(zdt.day)}${pad(zdt.year, 4)}`; + case 'ymd': return `${pad(zdt.year, 4)}${pad(zdt.month)}${pad(zdt.day)}`; + case 'hms': return `${pad(zdt.hour)}${pad(zdt.minute)}${pad(zdt.second)}`; case 'ts': return ((config?.timeStamp ?? 'ms') === 'ss') ? Math.trunc(zdt.epochMilliseconds / 1000).toString() : zdt.epochMilliseconds.toString(); @@ -112,7 +119,7 @@ export function format(obj?: Temporal.ZonedDateTime | any, fmt?: string | symbol case 'tz': return zdt.timeZoneId; default: { if (token.startsWith('#') && isTempo(obj)) { - const res = (obj as Tempo).term[token.slice(1)]; + const res = (obj as unknown as Tempo).term[token.slice(1)]; if (isObject(res)) return res.label ?? res.key ?? `{${token}}`; return res ?? `{${token}}`; } diff --git a/packages/tempo/src/module/module.mutate.ts b/packages/tempo/src/module/module.mutate.ts index 0bad4d84..c4a04dfa 100644 --- a/packages/tempo/src/module/module.mutate.ts +++ b/packages/tempo/src/module/module.mutate.ts @@ -2,7 +2,7 @@ import { isDefined, isObject, isString, isUndefined, isZonedDateTime } from '#li import { singular } from '#library/string.library.js'; import { normaliseFractionalDurations } from '#library/temporal.library.js'; -import { sym, enums } from '#tempo/support'; +import { sym, enums, logError } from '#tempo/support'; import { defineInterpreterModule, type TempoModule } from '../plugin/plugin.util.js'; import { findTermPlugin } from '../plugin/term/term.util.js'; import { resolveTermMutation } from '../engine/engine.term.js'; @@ -59,10 +59,8 @@ function mutate(this: Tempo, type: 'add' | 'set', args?: any, options: t.Options if (key === 'timeZone' || key === 'calendar') return currZdt; try { - // @ts-ignore - access to mutation guard if (++state.mutateDepth > 100) { - // @ts-ignore - access to static logger - (this.constructor as any)[sym.$logError](this.config, `Infinite recursion detected in mutation engine for key: ${key}, adjust: ${adjust}, depth: ${state.mutateDepth}`); + logError(`Infinite recursion detected in mutation engine for key: ${key}, adjust: ${adjust}, depth: ${state.mutateDepth}`, this.config); state.errored = true; return currZdt; } @@ -70,7 +68,7 @@ function mutate(this: Tempo, type: 'add' | 'set', args?: any, options: t.Options const { mutate: op, offset, single, term } = ((key, adjust, type) => { const isTerm = key.startsWith('#'); if (type === 'add') { - const isTermPlugin = !isTerm && !!findTermPlugin(key as string); + const isTermPlugin = !isTerm && isDefined(findTermPlugin(key as string)); const isStandard = ['period', 'event', 'time', 'date', 'dow', 'wkd'].includes(key as string); return { mutate: 'add', @@ -89,7 +87,7 @@ function mutate(this: Tempo, type: 'add' | 'set', args?: any, options: t.Options return { mutate: key as any, offset: val, single: isTermVal ? 'term' : singular(val), term: isTermVal ? val : undefined }; } default: { - const isTermPlugin = !isTerm && !!findTermPlugin(key as string); + const isTermPlugin = !isTerm && isDefined(findTermPlugin(key as string)); const isStandard = ['period', 'event', 'time', 'date', 'dow', 'wkd'].includes(key as string); return { mutate: 'set', @@ -165,33 +163,27 @@ function mutate(this: Tempo, type: 'add' | 'set', args?: any, options: t.Options return currZdt.round({ smallestUnit: offset as any, roundingMode: 'ceil' }).subtract({ nanoseconds: 1 }); default: - // @ts-ignore - (this.constructor as any)[sym.$logError](this.config, `Unexpected method(${op}), unit(${key}) and offset(${adjust})`); + logError(`Unexpected method(${op}), unit(${key}) and offset(${adjust})`, this.config); state.errored = true; return currZdt; } } finally { - // @ts-ignore state.mutateDepth--; } }, zdt); } else { // 3. Return a new instance with the final state - // @ts-ignore - access to private constructor/state return new (this.constructor as any)(args, { ...state.options, ...this.config, ...options, anchor: zdt, [sym.$Internal]: { ...state, matches } }); } } if (state.errored) { - // @ts-ignore - access to private constructor fallback return new (this.constructor as any)(null, { ...state.options, ...overrides, ...options, [sym.$Internal]: { ...state, matches } }); } - // @ts-ignore matches.push({ type: 'Mutation', value: zdt, match: 'mutation' }); - // @ts-ignore return new (this.constructor as any)(zdt, { ...state.options, ...overrides, ...options, anchor: zdt, [sym.$Internal]: { ...state, matches } }); } finally { diff --git a/packages/tempo/src/module/module.parse.ts b/packages/tempo/src/module/module.parse.ts index a06455ef..79620c45 100644 --- a/packages/tempo/src/module/module.parse.ts +++ b/packages/tempo/src/module/module.parse.ts @@ -1,6 +1,7 @@ import '#library/temporal.polyfill.js'; import { asType } from '#library/type.library.js'; -import { isNull, isString, isObject, isZonedDateTime, isInstant, isDefined, isUndefined, isIntegerLike, isEmpty } from '#library/assertion.library.js'; +import { LOG } from '#library/logger.class.js'; +import { isNull, isString, isObject, isZonedDateTime, isInstant, isDefined, isUndefined, isEmpty } from '#library/assertion.library.js'; import { asArray } from '#library/coercion.library.js'; import { isNumeric } from '#library/assertion.library.js'; import { instant, getTemporalIds } from '#library/temporal.library.js'; @@ -15,9 +16,9 @@ import { normalizeMatch, accumulateResult } from '../engine/engine.normalizer.js import { getRange, getTermRange } from '../plugin/term/term.util.js'; import { defineInterpreterModule } from '../plugin/plugin.util.js'; import type { Range, ResolvedRange } from '../plugin/term/term.type.js'; -import { sym, isTempo, TermError, getRuntime, Match } from '../support/support.index.js'; +import { sym, isTempo, TermError, getRuntime, Match, TempoError } from '../support/support.index.js'; import { markConfig, setPatterns, init, extendState } from '../support/support.index.js'; -import { setProperty } from '#tempo/support/support.util.js'; +import { setProperty, logError, logDebug } from '#tempo/support/support.util.js'; import * as t from '../tempo.type.js'; /** @@ -113,8 +114,8 @@ const _ParseEngine = { if (termKey) { if (isUndefined(term)) { const msg = `Unsupported Syntax: Term-based mutations (#) cannot be passed to the constructor. Use new Tempo().set(${JSON.stringify(tempo)}) instead.`; - if (TempoClass) (TempoClass as any)[sym.$logError](state.config, msg); - throw new Error(msg); + logError(msg, state.config); + throw new TempoError(msg); } if (terms.length === 0) { if (TempoClass) (TempoClass as any)[TermError](state.config, termKey); @@ -155,7 +156,7 @@ const _ParseEngine = { if (isTempo(dateTime)) dateTime = dateTime.toDateTime(); if (!isZonedDateTime(dateTime)) { - if (TempoClass) (TempoClass as any)[sym.$logError](state.config, new TypeError(`Sacred Anchor corrupted: ${String(value)}`)); + logError(new TypeError(`Sacred Anchor corrupted: ${String(value)}`), state.config); return { type: 'Void', value: undefined as any }; } @@ -238,7 +239,7 @@ const _ParseEngine = { const TempoClass = getRuntime().modules['Tempo']; if (resolving.size >= 100) { - if (TempoClass) (TempoClass as any)[sym.$logError](state.config, new RangeError(`Infinite recursion detected in layout resolution for: ${String(value)}`)); + logError(new RangeError(`Infinite recursion detected in layout resolution for: ${String(value)}`), state.config); return arg; } @@ -281,7 +282,7 @@ const _ParseEngine = { } else if (trim.length <= 7) { const msg = 'Cannot safely interpret number with less than 8-digits: use string instead'; - if (TempoClass) (TempoClass as any)[sym.$logError](state.config, new TypeError(msg)); + logError(new TypeError(msg), state.config); return arg; } } @@ -301,13 +302,12 @@ const _ParseEngine = { enablePrefilter: state.parse.preFilter === true, onPlan: (summary) => { if (state.parse.preFilter !== true || !state.config?.debug) return; - if (!TempoClass) return; - const reduced = summary.totalCandidates - summary.selectedCandidates; if (reduced <= 0 && !summary.fallbackToFull) return; - (TempoClass as any)[sym.$logDebug](state.config, + logDebug( `Planner summary: selected ${summary.selectedCandidates}/${summary.totalCandidates}`, + state.config, `rules=${summary.rulesApplied.join(',') || 'none'}`, `fallback=${summary.fallbackToFull}`, `input="${summary.inputClass.trim}"` @@ -315,11 +315,17 @@ const _ParseEngine = { } }); + if (state.config?.debug === LOG.Debug) + logDebug(`[ParseEngine] Selected layouts: ${orderedPatterns.map(p => p[0].description).join(', ')}`, state.config); + for (const [symKey, pat] of orderedPatterns) { const groups = _ParseEngine.parseMatch(state, pat, trim); if (isEmpty(groups)) continue; + if (state.config?.debug === LOG.Debug) + logDebug(`[ParseEngine] Matched layout '${symKey.description}' with groups: ${JSON.stringify(groups)}`, state.config); + const hasTime = Object.keys(groups) .some(key => ['hh', 'mi', 'ss', 'ms', 'us', 'ns', 'ff', 'mer'].includes(key) || Match.period.test(key) || (Match.named.test(key) && key.endsWith('tm'))) || Object.values(groups).includes('now'); accumulateResult(state, { match: symKey.description, value: trim, groups: { ...groups } }); diff --git a/packages/tempo/src/plugin/extend/extend.ticker.ts b/packages/tempo/src/plugin/extend/extend.ticker.ts new file mode 100644 index 00000000..f463c5b0 --- /dev/null +++ b/packages/tempo/src/plugin/extend/extend.ticker.ts @@ -0,0 +1,28 @@ +import { Tempo } from '../../tempo.class.js'; +import { logWarn, TempoError } from '#tempo/support'; +import type { TempoPlugin } from '../plugin.util.js'; + +const errorMessage = '[Tempo#ticker] The Ticker has been extracted to a premium plugin in v3.x. ' + + 'Please install "@magmacomputing/tempo-plugin-ticker" and register it via Tempo.extend(TickerModule). ' + + 'Visit https://registry.magmacomputing.com.au for your free license key.'; + +// 1. Warn gracefully on import so the developer knows the path is deprecated +logWarn(errorMessage); + +// 2. Attach a throwing stub to the Tempo class so usage fails loudly +(Tempo as any).ticker = function (...args: any[]) { + throw new TempoError(errorMessage); +}; + +/** + * @deprecated The TickerModule has been extracted to a premium plugin in v3.x. + * Please install \`@magmacomputing/tempo-plugin-ticker\` and register it via \`Tempo.extend(TickerModule)\`. + * Visit https://registry.magmacomputing.com.au to obtain a free license key. + */ +export const TickerModule: TempoPlugin = { + name: 'Ticker', + install(TempoRef: any) { + // 3. Throw a fatal error if they actually try to register the module + throw new TempoError(errorMessage); + } +}; diff --git a/packages/tempo/src/plugin/plugin.index.ts b/packages/tempo/src/plugin/plugin.index.ts index f383d37e..81e4946c 100644 --- a/packages/tempo/src/plugin/plugin.index.ts +++ b/packages/tempo/src/plugin/plugin.index.ts @@ -9,3 +9,4 @@ export * from './plugin.util.js'; export * from './plugin.type.js'; export * from './term/term.type.js'; +export { Validator, definePremiumPlugin, definePremiumTerm } from '#tempo/license'; diff --git a/packages/tempo/src/plugin/plugin.util.ts b/packages/tempo/src/plugin/plugin.util.ts index 3538b308..e475433f 100644 --- a/packages/tempo/src/plugin/plugin.util.ts +++ b/packages/tempo/src/plugin/plugin.util.ts @@ -1,10 +1,10 @@ import { isFunction, isString, isUndefined, isClass, isObject, isDefined } from '#library/assertion.library.js'; -import { reveal } from '#library/string.library.js'; import { secureRef } from '#library/proxy.library.js'; import { sym, isTempo } from '../support/support.symbol.js'; +import { TempoError } from '../support/support.error.js'; import { getRuntime } from '../support/support.runtime.js'; -import { hasOwn } from '#tempo/support/support.util.js'; +import { hasOwn, logError } from '#tempo/support/support.util.js'; import type { Tempo } from '../tempo.class.js'; import type { Plugin, Module, Extension } from './plugin.type.js'; @@ -38,11 +38,11 @@ export function ensureModule(t: any, module: string, silent: boolean = false): b if (!isDefined(hostLogic) && !isTermsLoaded) { const baseName = mod.endsWith('Module') ? mod.slice(0, -6) : mod; const msg = `${mod} not loaded. (Did you forget to Tempo.extend(${mod}) or import '#tempo/${baseName.toLowerCase()}'?)`; - if (!silent && isFunction(host?.[sym.$logError])) host[sym.$logError](t?.config, msg); + if (!silent) logError(msg, t?.config); if (silent) return false; if (t?.config?.catch === true) return false; - throw new Error(msg); + throw new TempoError(msg); } return true; } @@ -78,8 +78,8 @@ export function interpret(t: any, module: string, methodOrFallback?: any, silent } const msg = `${module} method '${String(methodOrFallback)}' not found`; - if (isFunction(host?.[sym.$logError])) host[sym.$logError](t?.config, msg); - throw new Error(msg); + logError(msg, t?.config); + throw new TempoError(msg); } // 4. Execute the logic @@ -104,10 +104,8 @@ export function attachStatics(TempoClass: any, props: Record) { for (const [key, val] of Object.entries(props)) { if (hasOwn(TempoClass, key)) { const msg = `Static name collision on "${key}". Property is already defined on the host class.`; - if (isFunction(TempoClass[sym.$logError])) { - // use catch:true to report the collision without a fatal throw (supports re-extension in shared environments) - TempoClass[sym.$logError]({ ...TempoClass.config, catch: true }, msg); - } + // use catch:true to report the collision without a fatal throw (supports re-extension in shared environments) + logError(msg, { ...TempoClass?.config, catch: true }); // console.error(msg); continue; } @@ -145,7 +143,7 @@ export function defineInterpreterModule(name: string, logic: any, statics?: Reco if (isUndefined(modules[name])) { modules[name] = logic; } else if (modules[name] !== logic) { - throw new Error(`Tempo Security: Core Module clash for '${name}'. Logic is already defined.`); + throw new TempoError(`Tempo Security: Core Module clash for '${name}'. Logic is already defined.`); } // 2. Fallback for legacy class-local access @@ -159,7 +157,7 @@ export function defineInterpreterModule(name: string, logic: any, statics?: Reco } if (isDefined((TempoClass as any)[sym.$Interpreter][name])) { - if ((TempoClass as any)[sym.$Interpreter][name] !== logic) throw new Error(`Tempo Interpreter Module clash: '${name}' logic is already defined.`); + if ((TempoClass as any)[sym.$Interpreter][name] !== logic) throw new TempoError(`Tempo Interpreter Module clash: '${name}' logic is already defined.`); } else { (TempoClass as any)[sym.$Interpreter][name] = logic; } diff --git a/packages/tempo/src/plugin/term/term.util.ts b/packages/tempo/src/plugin/term/term.util.ts index 3822b7d8..ff289461 100644 --- a/packages/tempo/src/plugin/term/term.util.ts +++ b/packages/tempo/src/plugin/term/term.util.ts @@ -2,6 +2,7 @@ import { toZonedDateTime, toInstant, getTemporalIds, instant } from '#library/te import { isDefined, isFunction, isString, isUndefined, isNumber, isZonedDateTime } from '#library/assertion.library.js'; import { secure } from '#library/proxy.library.js'; import { sortKey, byKey } from '#library/array.library.js'; +import { asError } from '#library/coercion.library.js'; import { sym, TermError, isTempo } from '../../support/support.symbol.js'; import { getRuntime } from '../../support/support.runtime.js'; import { SCHEMA, getLargestUnit } from '../../support/support.util.js'; @@ -156,11 +157,12 @@ export function getRange(entry: any, t: Tempo, anchor?: any, group?: string): Ra } else { res = isFunction(term.resolve) ? term.resolve.call(t) : term.define.call(t, false); } - } catch (err: any) { - if (err.message.includes('Class constructor')) { + } catch (err: unknown) { + const error = asError(err); + if (error.message.includes('Class constructor')) return []; - } - throw err; + + throw error; } let list = (res == null) ? [] : (Array.isArray(res) ? res : [res]); diff --git a/packages/tempo/src/support/support.default.ts b/packages/tempo/src/support/support.default.ts index 501558e8..ead903ae 100644 --- a/packages/tempo/src/support/support.default.ts +++ b/packages/tempo/src/support/support.default.ts @@ -1,6 +1,7 @@ import { looseIndex } from '#library/object.library.js'; import { secure, proxify } from '#library/proxy.library.js'; import { getDateTimeFormat } from '#library/international.library.js'; +import { LOG } from '#library/logger.class.js'; import { NUMBER, MODE, MONTH_DAY } from './support.enum.js'; import { Token } from './support.symbol.js'; @@ -57,11 +58,12 @@ export const Match = proxify({ export const Snippet = looseIndex()({ [Token.yy]: /(?[0-9]{2}(?:[0-9]{2})?)/, // year must be exactly 2 or 4 digits [Token.mm]: /(?[0 ]?[1-9]|1[0-2]|Jan(?:uary)?|Feb(?:ruary)?|Mar(?:ch)?|Apr(?:il)?|May|Jun(?:e)?|Jul(?:y)?|Aug(?:ust)?|Sep(?:tember)?|Oct(?:ober)?|Nov(?:ember)?|Dec(?:ember)?)/, // month-name (abbrev or full) or month-number 01-12; leading '0' or space only (not \s — tab/newline are not valid padding) - [Token.dd]: /(?

[0 ]?[1-9]|[12][0-9]|3[01])(?:\s?(?:st|nd|rd|th))?/, // day-number 01-31; leading '0' or space only (not \s — tab/newline are not valid padding) + [Token.dd]: /(?
[0 ]?[1-9]|[12][0-9]|3[01]){ord}?/, // day-number 01-31; leading '0' or space only (not \s — tab/newline are not valid padding) [Token.hh]: /(?2[0-4]|[01]?[0-9])/, // hour 00-24; CAUTION: in non-anchored use '25' partially matches as '2' via [01]?[0-9] — always use within anchored layouts; single-digit hours (e.g. '9') are intentionally supported [Token.mi]: /(\:(?[0-5][0-9]))/, // minute-number 00-59 [Token.ss]: /(\:(?[0-5][0-9]))/, // seconds-number 00-59 [Token.ff]: /(\.(?[0-9]{1,9}))/, // fractional-seconds up-to 9-digits + [Token.ord]: /(?:\s?(?:st|nd|rd|th))/, // optional ordinal suffix [Token.mer]: /(\s*(?am|pm))/, // meridiem suffix (am,pm) [Token.sfx]: /((?:{sep}+|T)({tm}){tzd}?)/, // time-pattern suffix 'T {tm} Z'; NOTE: {tm} resolves via Layout fallback in compileRegExp (cross-registry dependency: Snippet → Layout) [Token.wkd]: /(?Mon(?:day)?|Tue(?:sday)?|Wed(?:nesday)?|Thu(?:rsday)?|Fri(?:day)?|Sat(?:urday)?|Sun(?:day)?)/, // day-name (abbrev or full) @@ -183,13 +185,14 @@ export const Guard = [ 'am', 'pm', 'ago', 'hence', 'this', 'next', 'prev', 'last', 'from', 'now', 'today', 'yesterday', 'tomorrow', 'start', 'mid', 'end', 'year', 'month', 'week', 'day', 'hour', 'minute', 'second', 'millisecond', 'microsecond', 'nanosecond', 'years', 'months', 'weeks', 'days', 'hours', 'minutes', 'seconds', 'milliseconds', 'microseconds', 'nanoseconds', + 'st', 'nd', 'rd', 'th', 'monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday', 'sunday', 'mondays', 'tuesdays', 'wednesdays', 'thursdays', 'fridays', 'saturdays', 'sundays' ] as const; /** @internal Tempo Default options */ export const Default = secure({ - /** log to console */ debug: false, + /** log to console */ debug: LOG.Info, /** catch or throw Errors */ catch: false, /** initialization strategy (auto | strict | defer) */ mode: MODE.Auto, /** used to parse two-digit years*/ pivot: 75, /** @link https: //en.wikipedia.org/wiki/Date_windowing */ @@ -198,8 +201,6 @@ export const Default = secure({ /** default timezone if not specified */ timeZone: getDateTimeFormat().timeZone, /** default locale if not specified */ locale: getDateTimeFormat().locale, /** hemisphere for term.qtr or term.szn */ sphere: undefined, - // /** license key for premium plugins */ license: '', - /** regional date-parsing configuration */ monthDay: MONTH_DAY, /** internationalization configuration */ intl: IntlDefault, /** parse planner configuration (layoutOrder, etc.) */ planner: { @@ -208,6 +209,6 @@ export const Default = secure({ Token.dt, Token.tm, Token.dtm, Token.tmd, Token.dmy, Token.mdy, Token.ymd, Token.off, Token.rel ], - preFilter: false + preFilter: true }, } as Options) diff --git a/packages/tempo/src/support/support.enum.ts b/packages/tempo/src/support/support.enum.ts index 1c61372c..063431a0 100644 --- a/packages/tempo/src/support/support.enum.ts +++ b/packages/tempo/src/support/support.enum.ts @@ -82,11 +82,11 @@ export const DEFAULTS = { /** useful for readable month and day */ dayMonth: '{dd}-{mmm}', /** useful for readable year, month and day */ dayDate: '{dd}-{mmm}-{yyyy}', /** display with Time */ dayTime: '{dd}-{mmm}-{yyyy} {hh}:{mi}:{ss}', - /** useful for stamping logs */ logStamp: '{yyyy}{mm}{dd}T{hhmiss}.{ff}', + /** useful for stamping logs */ logStamp: '{ymd}T{hms}.{ff}', /** useful for sorting display-strings */ sortTime: '{yyyy}-{mm}-{dd} {hh}:{mi}:{ss}', /** useful for sorting week order */ yearWeek: '{yw}{ww}', /** useful for sorting month order */ yearMonth: '{yyyy}{mm}', - /** useful for sorting date order */ yearMonthDay: '{yyyy}{mm}{dd}', + /** useful for sorting date order */ yearMonthDay: '{ymd}', /** just Date portion */ date: '{yyyy}-{mm}-{dd}', /** just Time portion */ time: '{hh}:{mi}:{ss}', }, @@ -179,7 +179,7 @@ export type FORMAT = typeof FORMAT; export type Format = LooseUnion & string> /** patterns that return a number */ -export const NumericPattern = ['{yyyy}{ww}', '{yyyy}{mm}', '{yyyy}{mm}{dd}', '{yyww}', '{yw}{ww}', '{yw}'] as const; +export const NumericPattern = ['{yyyy}{ww}', '{yyyy}{mm}', '{yyyy}{mm}{dd}', '{yyww}', '{yw}{ww}', '{yw}', '{ymd}', '{ymd6}'] as const; export type NumericPattern = typeof NumericPattern[number] /** pre-configured format strings */ @@ -247,7 +247,7 @@ export const PARSE = enumify(parseKeys, false); export type Parse = KeyOf /** allowed keys for global discovery objects */ -const discoveryKeys = ['options', 'plugins', 'plugin', 'terms', 'term', 'timeZones', 'monthDay', 'intl', 'relativeTime', 'planner', 'numbers', 'formats', 'ignore'] as const; +const discoveryKeys = ['options', 'plugins', 'terms', 'timeZones', 'monthDay', 'intl', 'planner', 'numbers', 'formats', 'ignore'] as const; export const DISCOVERY = enumify(discoveryKeys, false); export type Discovery = KeyOf @@ -259,6 +259,8 @@ export const LICENSE = enumify({ Expired: 'expired', Revoked: 'revoked', Invalid: 'invalid', + Unauthorized: 'unauthorized', + Unknown: 'unknown', }, false); export type LICENSE = ValueOf diff --git a/packages/tempo/src/support/support.error.ts b/packages/tempo/src/support/support.error.ts new file mode 100644 index 00000000..bab97aee --- /dev/null +++ b/packages/tempo/src/support/support.error.ts @@ -0,0 +1,10 @@ +/** + * Custom Error class for fatal Tempo exceptions. + */ +export class TempoError extends Error { + constructor(message: string) { + super(`[Tempo] ${message}`); + this.name = 'TempoError'; + Object.setPrototypeOf(this, TempoError.prototype); + } +} diff --git a/packages/tempo/src/support/support.index.ts b/packages/tempo/src/support/support.index.ts index 4aa98b26..011ca2e1 100644 --- a/packages/tempo/src/support/support.index.ts +++ b/packages/tempo/src/support/support.index.ts @@ -29,10 +29,11 @@ export { export { markConfig } from '#library/symbol.library.js'; export { sym, isTempo, Token, TermError, type TempoBrand } from './support.symbol.js'; -export { $Tempo, $Register, $Interpreter, $logError, $logDebug, $dbg, $guard, $errored, $Internal, $Bridge, $RuntimeBrand, $Descriptor, $setConfig, $setDiscovery, $setEvents, $setPeriods, $setAliases, $buildGuard, $IsBase, $Identity, $Logify, $Discover, $ImmutableSkip } from './support.symbol.js'; +export { $Tempo, $Register, $Interpreter, $guard, $errored, $Internal, $Bridge, $RuntimeBrand, $Descriptor, $setConfig, $setDiscovery, $setEvents, $setPeriods, $setAliases, $buildGuard, $IsBase, $Identity, $LogConfig, $Discover, $ImmutableSkip } from './support.symbol.js'; export { registryUpdate, registryReset, onRegistryReset } from './support.register.js'; export { getRuntime, TempoRuntime } from './support.runtime.js'; export { Match, Snippet, Layout, Event, Period, Ignore, Guard, Default } from './support.default.js'; -export { SCHEMA, getLargestUnit, logError, logWarn, logDebug } from './support.util.js'; +export { SCHEMA, getLargestUnit, logError, logWarn, logDebug, logTrace, setLogLevel, logTempo } from './support.util.js'; export { setPatterns } from '../engine/engine.pattern.js'; -export { init, extendState } from './support.init.js'; \ No newline at end of file +export { init, extendState } from './support.init.js'; +export { TempoError } from './support.error.js'; \ No newline at end of file diff --git a/packages/tempo/src/support/support.init.ts b/packages/tempo/src/support/support.init.ts index 77adf10f..47dcfcae 100644 --- a/packages/tempo/src/support/support.init.ts +++ b/packages/tempo/src/support/support.init.ts @@ -1,6 +1,6 @@ import '#library/temporal.polyfill.js'; import { enumify } from '#library/enumerate.library.js'; -import { asArray } from '#library/coercion.library.js'; +import { asArray, asError } from '#library/coercion.library.js'; import { getDateTimeFormat, getHemisphere } from '#library/international.library.js'; import { normalizeUtcOffset } from '#library/temporal.library.js'; import { markConfig } from '#library/symbol.library.js'; @@ -8,11 +8,12 @@ import { asType } from '#library/type.library.js'; import { isString, isObject, isUndefined, isDefined, isRegExp } from '#library/assertion.library.js'; import { Pledge } from '#library/pledge.class.js'; import { ownEntries } from '#library/primitive.library.js'; -import { decodeJWT } from '#library/utility.library.js'; +import { decodeJWT } from '#library/webtoken.library.js'; import { getStorage } from '#library/storage.library.js'; +import { parseLogLevel } from '#library/logger.class.js'; import { getRuntime } from './support.runtime.js'; -import { setProperty, setProperties, hasOwn, create, collect, normalizeLayoutOrder, resolveMonthDay, logError, logWarn } from './support.util.js'; +import { setProperty, setProperties, hasOwn, create, collect, normalizeLayoutOrder, resolveMonthDay, logError, logWarn, logDebug, isSyncToken } from './support.util.js'; import { sym, Token } from './support.symbol.js'; import { Match, Snippet, Layout, Event, Period, Ignore, Default } from './support.default.js'; import { STATE, LICENSE } from './support.enum.js'; @@ -144,7 +145,7 @@ function setLicense(state: t.Internal.State, key: string) { const claims = decodeJWT(key); runtime.license.key = key; runtime.license.status = LICENSE.Pending; - runtime.license.scopes = claims?.permissions || {}; + runtime.license.scopes = claims?.scopes || {}; if (claims?.exp) runtime.license.expires = claims.exp; if (claims?.iat) runtime.license.issuedAt = claims.iat; @@ -156,33 +157,47 @@ function setLicense(state: t.Internal.State, key: string) { const initialJti = runtime.license.jti; const initialKey = runtime.license.key; - runtime.license.jws = new Pledge({ + const argObj = { tag: 'license', - onResolve: (m) => { - const validator = new m.Validator(runtime.license.key!); - validator.verify().then((res: any) => { - // 🛡️ Race Condition Guard: Only apply results if identity (JTI + Key) hasn't changed since we started - if (runtime.license.jti !== initialJti || runtime.license.key !== initialKey) return; - - runtime.license.status = res.status; - runtime.license.scopes = res.scopes; - delete runtime.license.error; // 🚿 Clear error on every reckoning attempt - if (res.error) runtime.license.error = res.error; - if (res.expires) runtime.license.expires = res.expires; - if (res.issuedAt) runtime.license.issuedAt = res.issuedAt; - if (res.issuer) runtime.license.issuer = res.issuer; - if (res.jti) runtime.license.jti = res.jti; - - if ([LICENSE.Revoked, LICENSE.Invalid].includes(res.status)) - logWarn(state.config, `⚠️ Tempo Licensing: ${res.error || 'Verification failed'}`); - }).catch((err: any) => { - if (runtime.license.jti !== initialJti || runtime.license.key !== initialKey) return; - runtime.license.status = LICENSE.Invalid; - runtime.license.error = err?.message || 'Verification rejected'; - logWarn(state.config, `⚠️ Tempo Licensing: ${runtime.license.error}`); - }); + onResolve: (res: any) => { + // 🛡️ Race Condition Guard + if (runtime.license.jti !== initialJti || runtime.license.key !== initialKey) return; + + const isValidStatus = isSyncToken(res.status) || LICENSE.values().includes(res.status); + runtime.license.status = isValidStatus ? res.status : LICENSE.Invalid; + runtime.license.scopes = isObject(res.scopes) ? res.scopes : {}; + delete runtime.license.error; // 🚿 Clear error on every reckoning attempt + if (res.error) runtime.license.error = res.error; + if (res.expires) runtime.license.expires = res.expires; + if (res.issuedAt) runtime.license.issuedAt = res.issuedAt; + if (res.issuer) runtime.license.issuer = res.issuer; + if (res.jti) runtime.license.jti = res.jti; + + if ([LICENSE.Revoked, LICENSE.Invalid].includes(res.status)) + logWarn(`⚠️ Tempo Licensing: ${res.error || 'Verification failed'}`, state.config); + + if (res.revocationPromise) { + res.revocationPromise.then((isRevoked: boolean) => { + if (isRevoked && runtime.license.jti === initialJti && runtime.license.key === initialKey) { + runtime.license.status = LICENSE.Revoked; + runtime.license.error = 'License has been revoked by the issuer.'; + logWarn(`⚠️ Tempo Licensing: ${runtime.license.error}`, state.config); + } + }).catch((err: unknown) => { + const { message } = asError(err); + logDebug(`Tempo Licensing: Background revocation check failed for JTI ${initialJti} - ${message}`, state.config); + }); + } + }, + onReject: (err: unknown) => { + if (runtime.license.jti !== initialJti || runtime.license.key !== initialKey) return; + const error = asError(err); + runtime.license.status = LICENSE.Invalid; + runtime.license.error = error.message || 'Verification rejected'; + logWarn(`⚠️ Tempo Licensing: ${runtime.license.error}`, state.config); } - }); + } + runtime.license.jws = new Pledge(argObj as any); } } @@ -213,11 +228,11 @@ export function extendState(state: t.Internal.State, options: t.Options) { const pattern = isRegExp(v) ? v.source : String(v); // 🛡️ Security Check: Prevent catastrophic backtracking and malicious patterns if (pattern.length > 500) { - logError(state.config, `[Tempo#extend] Snippet pattern too long (max 500 chars).`); + logError(`[Tempo#extend] Snippet pattern too long (max 500 chars).`, state.config); return new RegExp(Match.escape(pattern)); } if (Match.backtrack.test(pattern)) { - logError(state.config, `[Tempo#extend] Snippet contains suspicious nested quantifiers.`); + logError(`[Tempo#extend] Snippet contains suspicious nested quantifiers.`, state.config); return new RegExp(Match.escape(pattern)); } return new RegExp(pattern); @@ -239,7 +254,7 @@ export function extendState(state: t.Internal.State, options: t.Options) { case 'timeZone': { const zone = String(arg.value).toLowerCase(); - const resolvedZone = enums.TIMEZONE[zone] ?? normalizeUtcOffset(String(arg.value)); + const resolvedZone = options.timeZones?.[zone] ?? state.config.timeZones?.[zone] ?? enums.TIMEZONE[zone] ?? normalizeUtcOffset(String(arg.value)); setProperty(state.config, 'timeZone', resolvedZone); break; } @@ -287,15 +302,6 @@ export function extendState(state: t.Internal.State, options: t.Options) { state.config.intl = { ...state.config.intl, ...arg.value }; break; - case 'relativeTime': - if (!hasOwn(state.config, 'intl')) state.config.intl = Object.create(state.config.intl || {}); - if (typeof arg.value === 'function') { - state.config.intl.relativeTime = arg.value; - } else { - state.config.intl.relativeTime = { ...state.config.intl.relativeTime, ...arg.value }; - } - break; - case 'planner': if (isDefined(arg.value.layoutOrder)) state.parse.planner.layoutOrder = normalizeLayoutOrder(arg.value.layoutOrder); if (isDefined(arg.value.preFilter)) state.parse.planner.preFilter = Boolean(arg.value.preFilter); @@ -313,7 +319,7 @@ export function extendState(state: t.Internal.State, options: t.Options) { const unit = (isString(arg.value) ? arg.value : arg.value?.unit)?.trim()?.toLowerCase(); if (isUndefined(unit) || !['ss', 'ms', 'us', 'ns'].includes(unit)) { - logError(state.config, `[Tempo#extend] Invalid timeStamp unit: ${String(unit ?? arg.value)}. Expected 'ss', 'ms', 'us', or 'ns'.`); + logError(`[Tempo#extend] Invalid timeStamp unit: ${String(unit ?? arg.value)}. Expected 'ss', 'ms', 'us', or 'ns'.`, state.config); break; } @@ -324,7 +330,7 @@ export function extendState(state: t.Internal.State, options: t.Options) { case 'license': { const runtime = getRuntime(); if (state !== runtime.state) { - logWarn(state.config, `[Tempo#extend] Licensing is a global-only feature and cannot be set on local instances.`); + logWarn(`[Tempo#extend] Licensing is a global-only feature and cannot be set on local instances.`, state.config); break; } const key = String(arg.value); @@ -332,6 +338,10 @@ export function extendState(state: t.Internal.State, options: t.Options) { break; } + case 'debug': + setProperty(state.config, 'debug', parseLogLevel(arg.value)); + break; + default: setProperty(state.config, optKey, arg.value); break; diff --git a/packages/tempo/src/support/support.intl.ts b/packages/tempo/src/support/support.intl.ts index bd3f524e..83bb34a9 100644 --- a/packages/tempo/src/support/support.intl.ts +++ b/packages/tempo/src/support/support.intl.ts @@ -1,9 +1,13 @@ import type { IntlOptions } from '../tempo.type.js'; +import { isObject } from '#library/assertion.library.js'; /** @internal baseline Intl settings */ export const IntlDefault: IntlOptions = { - relativeTime: { + relativeTimeFormat: { style: 'narrow', + }, + durationFormat: { + style: 'long', } } @@ -31,20 +35,22 @@ export function probeMDY(locale: string): boolean { */ export function resolveIntl(value: IntlOptions = {}, base: IntlOptions = IntlDefault): IntlOptions { const result = { ...base } as Record; + const intls = ['relativeTimeFormat', 'numberFormat', 'durationFormat']; - Object.entries(value).forEach(([k, v]) => { - if ((k === 'relativeTime' || k === 'numberFormat') && typeof v === 'object' && v !== null && typeof v !== 'function') { - const current = result[k]; - const isObj = (val: any) => typeof val === 'object' && val !== null && typeof val !== 'function'; + Object + .entries(value) + .forEach(([k, v]) => { + if (intls.includes(k) && isObject(v)) { + const current = result[k]; - result[k] = { - ...(isObj(current) ? current as object : {}), - ...v as any - }; - } else { - result[k] = v; - } - }); + result[k] = { + ...(isObject(current) ? current as object : {}), + ...v as any + }; + } else { + result[k] = v; + } + }); return result; } diff --git a/packages/tempo/src/support/support.license.ts b/packages/tempo/src/support/support.license.ts index 83aef6ed..8cc41a70 100644 --- a/packages/tempo/src/support/support.license.ts +++ b/packages/tempo/src/support/support.license.ts @@ -1,4 +1,6 @@ -import { decodeJWT } from '#library/utility.library.js'; +import { isFunction } from '#library/assertion.library.js'; +import { decodeJWT } from '#library/webtoken.library.js'; +import { logWarn } from './support.util.js'; /** * # Tempo Licensing Engine (Open Core) @@ -7,18 +9,57 @@ import { decodeJWT } from '#library/utility.library.js'; */ export class Validator { - constructor(public key: string) { } + constructor(public key: string) { + logWarn('Tempo Community Edition: License keys are ignored. Premium plugins cannot be validated without the cryptographic engine.'); + } async verify() { // Decodes but DOES NOT verify the signature. // Cannot safely unlock Premium Plugins without cryptographic proof. - const claims = decodeJWT(this.key); + decodeJWT(this.key); return { status: 'invalid' as const, scopes: {}, error: 'Cryptographic engine missing. Premium plugins cannot be validated by the Community Build.', } } - async syncRevocation(_jwsUrl: string, _currentJti: string): Promise { - return false; // No revocation checking in community edition + async syncRevocation(_jwsUrl: string, _currentJti: string): Promise<{ revoked: boolean, success: boolean }> { + return { revoked: false, success: false }; // No revocation checking in community edition + } +} + +export function definePremiumPlugin(key: string, plugin: T): T { + logWarn(`Tempo Community Edition: Premium plugin '${key}' loaded without commercial validation engine.`); + const throwLicense = function () { + throw new Error(`[${key}] Premium plugin requires a valid commercial license. Status: invalid`); + } + if (isFunction(plugin)) return throwLicense as unknown as T; + if ((plugin as any).install) (plugin as any).install = throwLicense; + if ((plugin as any).define) (plugin as any).define = throwLicense; + if ((plugin as any).resolve) (plugin as any).resolve = throwLicense; + return plugin; +} + +export function definePremiumTerm(pluginDef: T): T { + logWarn(`Tempo Community Edition: Premium term '${(pluginDef as any).key}' loaded without commercial validation engine.`); + const key = (pluginDef as any).key; + const originalResolve = (pluginDef as any).resolve; + const originalDefine = (pluginDef as any).define; + + const throwLicense = function () { + throw new Error(`[${key}] Premium plugin requires a valid commercial license. Status: invalid`); } + + if (originalResolve) { + (pluginDef as any).resolve = function (this: any, term: string) { + throwLicense(); + } + } + + if (originalDefine) { + (pluginDef as any).define = function (this: any, term: string, value: any) { + throwLicense(); + } + } + + return pluginDef; } diff --git a/packages/tempo/src/support/support.register.ts b/packages/tempo/src/support/support.register.ts index 2859f9e6..70b2a1a6 100644 --- a/packages/tempo/src/support/support.register.ts +++ b/packages/tempo/src/support/support.register.ts @@ -7,7 +7,7 @@ import { sym } from './support.symbol.js'; import type { Property } from '#library/type.library.js'; import { getRuntime } from './support.runtime.js'; -import { hasOwn, setProperty } from './support.util.js'; +import { hasOwn, setProperty, logWarn } from './support.util.js'; // Import the live enums and their mutable state from the enum module import { STATE, REGISTRIES, DEFAULTS } from './support.enum.js'; @@ -53,7 +53,7 @@ export function registryReset() { if (hasOwn(obj, key)) { Object.defineProperty(obj, key, desc); } else { - console.warn(`[tempo] registryReset: Cannot define property '${String(key)}' on non-extensible object (property does not exist)`, obj); + logWarn(`[tempo] registryReset: Cannot define property '${String(key)}' on non-extensible object (property does not exist)`, {}, obj); } } }); diff --git a/packages/tempo/src/support/support.runtime.ts b/packages/tempo/src/support/support.runtime.ts index dcc4e3f5..f40beb98 100644 --- a/packages/tempo/src/support/support.runtime.ts +++ b/packages/tempo/src/support/support.runtime.ts @@ -179,7 +179,7 @@ export function getRuntime(): TempoRuntime { * Force-reset the runtime state for testing. * This clears the internal state and license trackers to ensure test isolation. */ -export function resetRuntimeForTesting(): void { +export function resetRuntime(): void { const rt = getRuntime(); // 🛡️ Race Condition Guard: Bump JTI to invalidate pending async reckonings rt.license.jti = Math.random().toString(36).slice(2); diff --git a/packages/tempo/src/support/support.symbol.ts b/packages/tempo/src/support/support.symbol.ts index 8e326c20..eac616be 100644 --- a/packages/tempo/src/support/support.symbol.ts +++ b/packages/tempo/src/support/support.symbol.ts @@ -1,9 +1,11 @@ import { looseIndex } from '#library/object.library.js'; -import { sym as lib, $Target, $Discover, $Extensible, $Inspect, $Logify, $Registry, $Register as $LibRegister, $SerializerRegistry, $Identity, $ImmutableSkip } from '#library/symbol.library.js'; -export { $Target, $Discover, $Extensible, $Inspect, $Logify, $Registry, $LibRegister, $SerializerRegistry, $Identity, $ImmutableSkip }; +import { isDefined } from '#library/assertion.library.js'; +import { sym as lib, $Target, $Discover, $Extensible, $Inspect, $LogConfig, $Registry, $Register as $LibRegister, $SerializerRegistry, $Identity, $ImmutableSkip } from '#library/symbol.library.js'; +export { $Target, $Discover, $Extensible, $Inspect, $LogConfig, $Registry, $LibRegister, $SerializerRegistry, $Identity, $ImmutableSkip }; + /** check valid Tempo instance */ -export const isTempo = (tempo?: any): tempo is TempoBrand => Boolean(tempo?.[sym.$Identity]); +export const isTempo = (tempo?: any): tempo is TempoBrand => tempo?.[sym.$Identity] === true; /** * Centralized registry for all Tempo-specific Global Symbols. @@ -16,9 +18,7 @@ export const TermError: unique symbol = Symbol.for('magmacomputing/tempo/termErr /** key for Global Discovery of Tempo configuration */ export const $Tempo: unique symbol = Symbol.for('$Tempo') as any; /** key for Reactive Plugin Registration */ export const $Register: unique symbol = Symbol.for('magmacomputing/tempo/register') as any; /** key for Internal Interpreter Service */ export const $Interpreter: unique symbol = Symbol.for('magmacomputing/tempo/interpreter') as any; -/** key for contextual Error Logging */ export const $logError: unique symbol = Symbol.for('magmacomputing/tempo/logError') as any; -/** key for contextual Debug Logging */ export const $logDebug: unique symbol = Symbol.for('magmacomputing/tempo/logDebug') as any; -/** key for contextual Debugger */ export const $dbg: unique symbol = Symbol.for('magmacomputing/tempo/dbg') as any; + /** key for Master Guard */ export const $guard: unique symbol = Symbol.for('magmacomputing/tempo/guard') as any; /** internal key for signaling pre-errored state */ export const $errored: unique symbol = Symbol.for('magmacomputing/tempo/errored') as any; /** internal key for accessing private instance state */ export const $Internal: unique symbol = Symbol.for('magmacomputing/tempo/internal') as any; @@ -37,7 +37,7 @@ export const TermError: unique symbol = Symbol.for('magmacomputing/tempo/termErr /** @internal Tempo Symbol Registry (Local Keys) */ const local = { - $Tempo, $Register, $Interpreter, $logError, $logDebug, $dbg, $guard, $errored, + $Tempo, $Register, $Interpreter, $guard, $errored, $Internal, $Bridge, $RuntimeBrand, $Descriptor, $License, $setConfig, $setDiscovery, $setEvents, $setPeriods, $setAliases, $buildGuard, $IsBase } as const; @@ -59,6 +59,7 @@ export const Token = looseIndex()({ /** year */ yy: Symbol('yy'), /** ISO yearOfWeek */ yw: Symbol('yw'), /** month */ mm: Symbol('mm'), + /** ordinal suffix */ ord: Symbol('ord'), /** day */ dd: Symbol('dd'), /** hour */ hh: Symbol('hh'), /** minute */ mi: Symbol('mi'), diff --git a/packages/tempo/src/support/support.util.ts b/packages/tempo/src/support/support.util.ts index 8425f4fc..a1704631 100644 --- a/packages/tempo/src/support/support.util.ts +++ b/packages/tempo/src/support/support.util.ts @@ -1,13 +1,14 @@ -import { isBoolean } from '#library/assertion.library.js'; +import { isBoolean, isError } from '#library/assertion.library.js'; +import { Logger, LOG, parseLogLevel, type DebugLevel } from '#library/logger.class.js'; +import { raise as boundaryRaise } from '#library/boundary.library.js'; import { sym, Token } from './support.symbol.js'; -import { asType } from '#library/type.library.js'; -import { asArray } from '#library/coercion.library.js'; -import { isSymbol, isUndefined, isDefined, isString, isRegExp, isNullish, isObject, isEmpty } from '#library/assertion.library.js'; -import { ownEntries, ownKeys, unwrap } from '#library/primitive.library.js'; +import { asType, getType } from '#library/type.library.js'; +import { asArray, asError } from '#library/coercion.library.js'; +import { isSymbol, isUndefined, isDefined, isString, isNullish, isObject } from '#library/assertion.library.js'; +import { ownEntries, unwrap } from '#library/primitive.library.js'; import { getRuntime } from './support.runtime.js'; -import { Match, Snippet, Layout } from './support.default.js'; -import enums from './support.enum.js'; +import { LICENSE } from './support.enum.js'; import type * as t from '../tempo.type.js'; /** @internal normalize layout-order options into a clean string array */ @@ -22,7 +23,7 @@ export const setProperty = (target: object, key: PropertyKey, value: T) => { if (Object.isExtensible(target)) { Object.defineProperty(target, key, { value, writable: true, configurable: true, enumerable: true }); } else { - console.warn(`[tempo] setProperty: Cannot define property '${String(key)}' on non-extensible object`, target); + logWarn(`[tempo] setProperty: Cannot define property '${String(key)}' on non-extensible object`, {}, target); } } @@ -31,26 +32,59 @@ export const setProperties = (target: object, properties: Record setProperty(target, key, value)); } -/** @internal Centralized Error Logger — retrieves the shared Logify instance from the TempoRuntime */ -export function logError(config: any, ...msg: any[]) { - const rt = getRuntime(); - rt.logger?.error(config ?? rt.state?.config, ...msg); -} +export const logTempo = new Logger('Tempo'); +export const logParse = new Logger('Tempo:Parse'); +export const logEngine = new Logger('Tempo:Engine'); + +const loggers = [logTempo, logParse, logEngine]; -/** @internal Centralized Warning Logger — retrieves the shared Logify instance from the TempoRuntime */ -export function logWarn(config: any, ...msg: any[]) { - const rt = getRuntime(); - rt.logger?.warn(config ?? rt.state?.config, ...msg); +/** @internal Centralized setter for global verbosity */ +export function setLogLevel(debug?: DebugLevel) { + const level = parseLogLevel(debug, LOG.Info); + loggers.forEach(l => l.level = level); } -/** @internal Centralized Debug Logger — retrieves the shared Logify instance from the TempoRuntime */ -export function logDebug(config: any, ...msg: any[]) { - const rt = getRuntime(); - rt.logger?.debug(config ?? rt.state?.config, ...msg); +/** @internal Concatenate multiple arguments into a single string for logging */ +const concatMsg = (msg: any[]) => msg.map(m => isError(m) ? m.message : String(m)).join(' '); + +/** @internal Centralized Error Boundary — evaluates config.catch and logs automatically */ +export function raise(err: Error | string | unknown, config: any = {}, ...msg: any[]) { + const error = asError(err); + + if (msg.length > 0) { + const text = concatMsg(msg); + if (text) error.message = `${error.message} ${text}`; + } + + boundaryRaise(error, { + catch: config?.catch ?? false, + silent: config?.silent ?? false, + logger: logTempo + }); } +/** @internal Wrapper for legacy logError calls */ +export const logError = raise; + +const createLogger = (level: 'warn' | 'debug' | 'trace') => + (msg: any, config: any = {}, ...extraMsg: any[]) => { + if (!config?.silent) { + if (config[sym.$LogConfig]) logTempo[level](config, msg, ...extraMsg); + else logTempo[level](msg, ...extraMsg); + } + }; + +/** @internal Centralized Warning Logger */ +export const logWarn = createLogger('warn'); + +/** @internal Centralized Debug Logger */ +export const logDebug = createLogger('debug'); + +/** @internal Centralized Trace Logger */ +export const logTrace = createLogger('trace'); + /** @internal check if an object is a proxy */ -export const isProxy = (obj: any): boolean => !!obj && !!(obj as any)[sym.$Target]; +export const isProxy = (obj: any): boolean => isDefined(obj?.[sym.$Target]); /** @internal check if an object has an own property (respects Proxy/Shadowing) */ export const hasOwn = (obj: any, key: PropertyKey): boolean => { @@ -65,13 +99,13 @@ export const proto = (obj: any): any => Object.getPrototypeOf(unwrap(obj)); export function create(obj: any, name: string): T { const prototype = proto(obj); if (!isObject(prototype)) { - logError(null, `[Tempo#create] Failed to create shadowed object for '${name}'. Proto(obj) is null or not an object.`); + logError(`[Tempo#create] Failed to create shadowed object for '${name}'. Proto(obj) is null or not an object.`, null); return {} as T; } const entry = prototype[name]; if (!isObject(entry)) { - logError(null, `[Tempo#create] Failed to create shadowed object for '${name}'. The prototype entry from proto(obj) is missing or not an object (received: ${typeof entry}).`); + logError(`[Tempo#create] Failed to create shadowed object for '${name}'. The prototype entry from proto(obj) is missing or not an object (received: ${getType(entry)}).`, null); return {} as T; } @@ -182,3 +216,15 @@ export function resolveMonthDay(value: t.MonthDay | boolean = {}, base: t.MonthD resolvedLocales } } +/** @internal identify valid sync tokens */ +export function isSyncToken(status: any): status is string { + return isString(status) && /^[0-9a-f]{8}$/.test(status); +} + +/** @internal resolve licensing state to standard 'active' state */ +export function resolveDisplayStatus(status: string): string { + const raw = isSyncToken(status) + ? LICENSE.Active + : String(status) as LICENSE + return LICENSE.values().includes(raw) ? raw : LICENSE.Unknown; +} diff --git a/packages/tempo/src/tempo.class.ts b/packages/tempo/src/tempo.class.ts index f4bc4fa5..8f864374 100644 --- a/packages/tempo/src/tempo.class.ts +++ b/packages/tempo/src/tempo.class.ts @@ -1,8 +1,7 @@ import '#library/temporal.polyfill.js'; -import { Logify } from '#library/logify.class.js'; import { Immutable, Serializable } from '#library/class.library.js'; -import { asArray } from '#library/coercion.library.js'; +import { asArray, asError } from '#library/coercion.library.js'; import { getStorage, setStorage } from '#library/storage.library.js'; import { secure, proxify, delegate, indexedArray } from '#library/proxy.library.js'; import { getContext, CONTEXT } from '#library/utility.library.js'; @@ -12,9 +11,10 @@ import { getAccessors, omit } from '#library/reflection.library.js'; import { pad, trimAll } from '#library/string.library.js'; import { getType } from '#library/type.library.js'; import { clone } from '#library/serialize.library.js'; -import { isEmpty, isDefined, isUndefined, isString, isObject, isSymbol, isFunction, isClass, isZonedDateTime, isDurationLike } from '#library/assertion.library.js'; +import { isEmpty, isDefined, isUndefined, isString, isObject, isSymbol, isFunction, isClass, isZonedDateTime, isDurationLike, isError, isNumber } from '#library/assertion.library.js'; import { instant, getTemporalIds } from '#library/temporal.library.js'; import { getDateTimeFormat, getHemisphere, canonicalLocale } from '#library/international.library.js'; +import { LOG } from '#library/logger.class.js'; import type { Property, Secure } from '#library/type.library.js'; import { registerPlugin, interpret, ensureModule, type TempoPlugin } from './plugin/plugin.util.js' @@ -24,10 +24,10 @@ import type { TermPlugin, PremiumPlugin } from './plugin/term/term.type.js'; import { AliasEngine } from './engine/engine.alias.js'; import { PatternCompiler } from './engine/engine.pattern.js'; import { createMasterGuard } from './engine/engine.guard.js'; -import { resolveMonthDay, setProperty, proto, hasOwn, normalizeLayoutOrder } from './support/support.util.js'; +import { resolveMonthDay, setProperty, proto, hasOwn, resolveDisplayStatus } from './support/support.util.js'; import { DEFAULT_LAYOUT_CLASS, resolveLayoutOrder, getLayoutOrder } from './engine/engine.layout.js'; import { datePattern } from './support/support.default.js'; -import { sym, markConfig, TermError, getRuntime, init, extendState, setPatterns, isTempo, registryUpdate, registryReset, onRegistryReset, Match, Token, Snippet, Layout, Event, Period, Ignore, Default, Guard, enums, STATE, LICENSE, DISCOVERY, $Internal, $setConfig, $logError, $logDebug, $Identity, $setEvents, $setPeriods, $setAliases, $buildGuard, $IsBase, $Tempo, $Register, $Logify, $errored, $dbg, $guard, $Discover, $setDiscovery } from '#tempo/support'; +import { sym, markConfig, TermError, getRuntime, init, extendState, setPatterns, isTempo, registryUpdate, registryReset, onRegistryReset, Match, Token, Snippet, Layout, Event, Period, Ignore, Default, Guard, enums, STATE, LICENSE, DISCOVERY, $Internal, $setConfig, $Identity, $setEvents, $setPeriods, $setAliases, $buildGuard, $IsBase, $Tempo, $Register, $errored, $guard, $Discover, $setDiscovery, $LogConfig, logError, logDebug, logWarn, logTempo, setLogLevel } from '#tempo/support'; import * as t from './tempo.type.js'; // namespaced types (Tempo.*) declare module '#library/type.library.js' { @@ -65,6 +65,14 @@ namespace Internal { * A powerful wrapper around `Temporal.ZonedDateTime` for flexible parsing and intuitive manipulation of date-time objects. * Bridges the gap between raw string/number inputs and the strict requirements of the ECMAScript Temporal API. */ + +/** Tempo state for the global configuration */ +let _global = {} as Internal.State; +/** cache for next-available 'usr' Token key */ +let _usrCount = 0; +/** flag to prevent recursion during init */ +const _lifecycle = { bootstrap: true, initialising: false, extendDepth: 0, ready: false }; + @Serializable @Immutable export class Tempo { @@ -86,45 +94,42 @@ export class Tempo { /** initialization strategies */ static get MODE() { return enums.MODE } /** some useful Dates */ static get LIMIT() { return enums.LIMIT } - /** @internal check if Tempo is currently initializing */ static get isInitializing() { return !Tempo.#lifecycle.ready } - /** @internal check if Tempo is currently extending */ static get isExtending() { return Tempo.#lifecycle.extendDepth > 0 } - - /** Logify for internal errors and debug logs */ static #dbg = new Logify('Tempo', { debug: Default?.debug ?? false, catch: Default?.catch ?? false }) + /** @internal check if Tempo is currently initializing */ static get isInitializing() { return !_lifecycle.ready } + /** @internal check if Tempo is currently extending */ static get isExtending() { return _lifecycle.extendDepth > 0 } - /** Tempo state for the global configuration */ static #global = {} as Internal.State; - /** cache for next-available 'usr' Token key */ static #usrCount = 0; - /** mutable list of registered term plugins */ static get #terms(): TermPlugin[] { return getRuntime().pluginsDb.terms } - /** @internal raw license state */ static get #license() { return getRuntime().license } + /** mutable list of registered term plugins */ private static get _terms(): TermPlugin[] { return getRuntime().pluginsDb.terms } + /** @internal raw license state */ private static get _license() { return getRuntime().license } /** human-readable formatted license state */ static get license() { - const { jws, key, ...raw } = Tempo.#license; // omit internal Pledge and JWT string from user-facing snapshot + const { jws, key, ...raw } = Tempo._license; // omit internal Pledge and JWT string from user-facing snapshot const ss = { timeStamp: 'ss' } as const; // JWT timestamps are always in seconds (RFC 7519) - const scopesSource = (raw.scopes && typeof raw.scopes === 'object') ? raw.scopes : {}; + const scopesSource = (raw.scopes && isObject(raw.scopes)) ? raw.scopes : {}; const scopes = Object.fromEntries( Object.entries(scopesSource).map(([key, scope]) => { const s = scope as any; return [key, { ...s, - ...(typeof s.exp === 'number' && { exp: new Tempo(s.exp, ss).fmt.weekTime }), - ...(typeof s.updated_at === 'number' && { updated_at: new Tempo(s.updated_at, ss).fmt.weekTime }), + ...(isNumber(s.exp) && { exp: new Tempo(s.exp, ss).fmt.weekTime }), + ...(isNumber(s.updated_at) && { updated_at: new Tempo(s.updated_at, ss).fmt.weekTime }), }]; }) ); return secure({ ...raw, + status: resolveDisplayStatus(raw.status), scopes, - ...(typeof raw.expires === 'number' && { expires: new Tempo(raw.expires, ss).fmt.weekTime }), - ...(typeof raw.issuedAt === 'number' && { issuedAt: new Tempo(raw.issuedAt, ss).fmt.weekTime }), + ...(isNumber(raw.expires) && { expires: new Tempo(raw.expires, ss).fmt.weekTime }), + ...(isNumber(raw.issuedAt) && { issuedAt: new Tempo(raw.issuedAt, ss).fmt.weekTime }), }); } - /** mapping of terms to their resolved values */ static #termMap: Map = new Map(); - /** flag to prevent recursion during init */ static #lifecycle = { bootstrap: true, initialising: false, extendDepth: 0, ready: false }; - /** Master Guard predicate (implements RegExp-like interface) */static #guard: { test(str: string): boolean } = { test: () => true }; + /** mapping of terms to their resolved values */ private static _termMap: Map = new Map(); + + /** Master Guard predicate (implements RegExp-like interface) */static get [$guard]() { return (this[$Internal]() as any)[$guard] ?? { test: () => true }; } static [$IsBase] = true; /** @internal Static access to global private state. */ static [$Internal]() { - return ClassStates.get(this) ?? Tempo.#global; + return ClassStates.get(this) ?? _global; } static get $ImmutableSkip() { @@ -142,24 +147,6 @@ export class Tempo { /** @internal brand check to distinguish Tempo objects from other objects */ get [$Identity](): true { return true } - /** @internal handle internal errors using the global config */ - static [$logError](...msg: any[]): void { - const provided = (isObject(msg[0]) && (msg[0] as any)[$Logify] === true) ? msg.shift() : undefined; - const global = this[$Internal]().config; - const config = markConfig(Object.create(global)); - if (provided) Object.entries(provided).forEach(([k, v]) => setProperty(config, k, v)); - markConfig(config); // ensure config is marked for Logify - Tempo.#dbg.error(config, ...msg); - } - - /** @internal handle internal debug logs */ - static [$logDebug](...args: any[]): void { - const provided = (isObject(args[0]) && (args[0] as any)[$Logify] === true) ? args.shift() : undefined; - const global = this[$Internal]().config; - const config = markConfig(Object.create(global)); - if (provided) Object.entries(provided).forEach(([k, v]) => setProperty(config, k, v)); - Tempo.#dbg.debug(config, ...args); - } /** * {dt} is a layout that combines date-related {snippets} (e.g. dd, mm -or- evt) into a pattern against which a string can be tested. @@ -194,7 +181,6 @@ export class Tempo { if (!hasOwn(shape, 'aliasEngine')) { engine = shape.aliasEngine = new AliasEngine({ parent: parent.aliasEngine, - logger: Tempo.#dbg, config: shape.config }); } @@ -229,7 +215,7 @@ export class Tempo { } /** try to infer hemisphere using the timezone's daylight-savings setting */ - static #setSphere = (shape: Internal.State, options: t.Options) => { + private static _setSphere = (shape: Internal.State, options: t.Options) => { if (isDefined(options.sphere)) return options.sphere; const tz = options.timeZone; @@ -244,16 +230,16 @@ export class Tempo { } /** determine if we have a {timeZone} which prefers {mdy} date-order */ - static #isMonthDay(shape: Internal.State) { + private static _isMonthDay(shape: Internal.State) { const { timeZone, locale } = shape.config; const mdy = shape.parse.monthDay; const globalMdy = Tempo.MONTH_DAY as t.MonthDay; let intl: Intl.Locale; try { - intl = new Intl.Locale(Tempo.#locale(locale)); + intl = new Intl.Locale(Tempo._locale(locale)); } catch (e) { - Tempo.#dbg.warn(shape.config, `Invalid locale encountered in #isMonthDay: ${locale}. Falling back to en-US.`, e); + logWarn(`Invalid locale encountered in #isMonthDay: ${locale}. Falling back to en-US.`, shape.config, e); intl = new Intl.Locale('en-US'); } @@ -270,11 +256,11 @@ export class Tempo { * swap parsing-order of layouts to suit different timeZones * this allows the parser to try to interpret '04012023' as Apr-01-2023 before trying 04-Jan-2023 */ - static #swapLayout(shape: Internal.State) { + private static _swapLayout(shape: Internal.State) { const { layouts } = shape.parse.monthDay; if (isEmpty(layouts)) return; - const isMonthDay = shape.parse.monthDay.isExplicit ? shape.parse.monthDay.active! : Tempo.#isMonthDay(shape); + const isMonthDay = shape.parse.monthDay.isExplicit ? shape.parse.monthDay.active! : Tempo._isMonthDay(shape); shape.parse.monthDay.active = isMonthDay; // ensure Token.dt matches the local monthDay preference @@ -296,11 +282,11 @@ export class Tempo { if (layout !== shape.parse.layout) shape.parse.layout = layout as Layout; - Tempo.#dbg.debug(shape.config, `Resolved layout order: ${getLayoutOrder(layout).join(' -> ')}`); + logDebug(`Resolved layout order: ${getLayoutOrder(layout).join(' -> ')}`, shape.config); } /** get first Canonical name of a supplied locale */ - static #locale = (locale?: string) => { + private static _locale = (locale?: string) => { let language: string | undefined; try { // lookup locale @@ -333,14 +319,14 @@ export class Tempo { extendState(shape, mergedOptions); // Side-effects - const newSphere = Tempo.#setSphere(shape, mergedOptions); + const newSphere = Tempo._setSphere(shape, mergedOptions); if (shape.config.scope === 'local') { const parentSphere = Object.getPrototypeOf(shape.config).sphere; if (newSphere !== parentSphere) shape.config.sphere = newSphere; } else { shape.config.sphere = newSphere; } - Tempo.#swapLayout(shape); + Tempo._swapLayout(shape); if (isDefined(shape.parse.event)) this[$setEvents](shape, undefined, false); if (isDefined(shape.parse.period)) this[$setPeriods](shape, undefined, false); @@ -354,21 +340,27 @@ export class Tempo { markConfig(discovery); // auto-mark the discovery object + const isSandbox = shape !== _global; + let opts = discovery.options || {} + // 1. Process TimeZones (normalize to lowercase for lookup) if (discovery.timeZones) { const tzs = Object.fromEntries( ownEntries(discovery.timeZones, true).map(([k, v]) => [String(k).toLowerCase(), v]) ); - registryUpdate('TIMEZONE', tzs); + if (isSandbox) opts = { ...opts, timeZones: tzs }; + else registryUpdate('TIMEZONE', tzs); } // 1b. Process Numbers - if (discovery.numbers) - registryUpdate('NUMBER', discovery.numbers); + if (discovery.numbers) { + if (isSandbox) opts = { ...opts, numbers: discovery.numbers }; + else registryUpdate('NUMBER', discovery.numbers); + } // 1c. Process MDY settings if (discovery.monthDay) { - const md = discovery.monthDay; + let md = discovery.monthDay; if (md.timezones) { const zones = Object.fromEntries( ownEntries(md.timezones, true).map(([k, v]) => { @@ -376,48 +368,30 @@ export class Tempo { catch { return [String(k), v] } }) ); - registryUpdate('MONTH_DAY', { timezones: zones }); + if (isSandbox) md = { ...md, timezones: zones }; + else registryUpdate('MONTH_DAY', { timezones: zones }); + } + if (!isSandbox) { + if (md.locales) registryUpdate('MONTH_DAY', { locales: asArray(md.locales) }); + if (md.layouts) registryUpdate('MONTH_DAY', { layouts: asArray(md.layouts) }); } - if (md.locales) registryUpdate('MONTH_DAY', { locales: asArray(md.locales) }); - if (md.layouts) registryUpdate('MONTH_DAY', { layouts: asArray(md.layouts) }); + opts = { ...opts, monthDay: md }; } // 1d. Process Internationalization - if (discovery.intl || discovery.relativeTime) { - const intl: t.IntlOptions = { ...discovery.intl }; - if (discovery.relativeTime) { - if (typeof discovery.relativeTime === 'function') { - intl.relativeTime = discovery.relativeTime; - } else if (!(typeof intl.relativeTime === 'function')) { - intl.relativeTime = { ...intl.relativeTime, ...discovery.relativeTime }; - } else { - // A function-based relativeTime in 'intl' takes precedence over a shorthand 'relativeTime' object - Tempo.#dbg.debug(shape.config, '[Discovery] Shorthand relativeTime object ignored; intl.relativeTime function has precedence.'); - } - } - shape.config.intl = { ...shape.config.intl, ...intl }; - } + if (discovery.intl) opts = { ...opts, intl: discovery.intl }; // 1e. Process Planner - if (isObject(discovery.planner)) { - if (isDefined(discovery.planner.layoutOrder)) - shape.parse.planner.layoutOrder = normalizeLayoutOrder(discovery.planner.layoutOrder as any); - if (isDefined(discovery.planner.preFilter)) - shape.parse.planner.preFilter = Boolean(discovery.planner.preFilter); - } + if (isObject(discovery.planner)) opts = { ...opts, planner: discovery.planner }; // 2. Process Terms - if (discovery.term) { - discovery.terms = [...asArray(discovery.terms || []), ...asArray(discovery.term)]; - Tempo.#dbg.warn(shape.config, 'Legacy "term" key in Discovery is deprecated. Please use "terms" instead.'); - } if (discovery.terms) this.extend(asArray(discovery.terms)); // 3. Process Formats if (discovery.formats) { shape.config.formats = shape.config.formats.extend(discovery.formats) as t.FormatRegistry; - registryUpdate('FORMAT', discovery.formats); + if (!isSandbox) registryUpdate('FORMAT', discovery.formats); } // 4. Process Plugins @@ -425,14 +399,14 @@ export class Tempo { asArray(discovery.plugins).forEach(p => this.extend(p)); // 5. Process Options - let opts = discovery.options || {} if (discovery.ignore) { const ignore = isFunction(discovery.ignore) ? discovery.ignore() : discovery.ignore; opts = { ...opts, ignore }; } + const res = isFunction(opts) ? opts() : opts; - if (shape === Tempo.#global) { + if (shape === _global) { this[$buildGuard](); setPatterns(shape); } @@ -455,20 +429,20 @@ export class Tempo { ...ownKeys(this[$Internal]().parse.snippet), ...ownKeys(this[$Internal]().parse.layout), ...[Token.slk], - ...Tempo.#terms.map(t => t.key), - ...Tempo.#terms.map(t => t.scope), + ...Tempo._terms.map(t => t.key), + ...Tempo._terms.map(t => t.scope), ...Guard ]; - Tempo.#guard = createMasterGuard(wordsList); + (this[$Internal]() as any)[$guard] = createMasterGuard(wordsList); - if (this[$Internal]() === Tempo.#global) { + if (this[$Internal]() === _global) { setPatterns(this[$Internal]()); } } /** @internal resolve a global discovery config object by symbol key */ - static #getConfig(sym: symbol) { + private static _getConfig(sym: symbol) { const discovery = (globalThis as Record)[sym]; return proxify(omit({ ...discovery, scope: 'discovery' }, 'value')); } @@ -499,7 +473,7 @@ export class Tempo { if (isEmpty(items)) return this; - Tempo.#lifecycle.extendDepth++; // increment the re-entrant nesting counter + _lifecycle.extendDepth++; // increment the re-entrant nesting counter try { items.forEach(item => { const arg = item as any; @@ -514,7 +488,7 @@ export class Tempo { } catch (e: any) { const msg = (e?.message ?? '').toLowerCase(); if (msg.includes('constructor') || msg.includes('class') || (e instanceof TypeError) || isClass(arg)) { - Tempo.#dbg.warn(this[$Internal]().config, `Misidentified class in plugin registration: ${(arg as any).name}`, e.stack ?? e); + logWarn(`Misidentified class in plugin registration: ${(arg as any).name}`, this[$Internal]().config, e.stack ?? e); } else { throw e; } @@ -525,7 +499,7 @@ export class Tempo { const name = (item as any).name; const rt = getRuntime(); if (rt.installed.has(name)) { - Tempo.#dbg.debug(this[$Internal]().config, `Plugin already installed by name: ${name}`); + logDebug(`Plugin already installed by name: ${name}`, this[$Internal]().config); return; } rt.installed.add(name); @@ -539,19 +513,19 @@ export class Tempo { const config = item as TermPlugin; const state = this[$Internal](); - if (Tempo.#termMap.get(config.key) === config) return; - if (Tempo.#termMap.has(config.key)) { - Tempo[$logError](state.config, `[Tempo#extend] Term collision on key: "${config.key}". Registration aborted.`); + if (Tempo._termMap.get(config.key) === config) return; + if (Tempo._termMap.has(config.key)) { + logError(`[Tempo#extend] Term collision on key: "${config.key}". Registration aborted.`, state.config); return; } - if (config.scope && Tempo.#termMap.get(config.scope) === config) { /* continue */ } - else if (config.scope && Tempo.#termMap.has(config.scope)) { - Tempo[$logError](state.config, `[Tempo#extend] Term collision on scope: "${config.scope}". Registration aborted.`); + if (config.scope && Tempo._termMap.get(config.scope) === config) { /* continue */ } + else if (config.scope && Tempo._termMap.has(config.scope)) { + logError(`[Tempo#extend] Term collision on scope: "${config.scope}". Registration aborted.`, state.config); return; } - Tempo.#termMap.set(config.key, config); - if (config.scope) Tempo.#termMap.set(config.scope, config); + Tempo._termMap.set(config.key, config); + if (config.scope) Tempo._termMap.set(config.scope, config); registerTerm(config); @@ -589,66 +563,13 @@ export class Tempo { // 2. handle Discovery object (container) else { const discovery = item as any - if (discovery.term) { - discovery.terms = [...asArray(discovery.terms || []), ...asArray(discovery.term)]; - Tempo.#dbg.warn(this[$Internal]().config, 'Legacy "term" key in Discovery is deprecated. Please use "terms" instead.'); - } - if (discovery.plugin) { - discovery.plugins = [...asArray(discovery.plugins || []), ...asArray(discovery.plugin)]; - } - - DISCOVERY.keys().forEach(key => { - const val = discovery[key]; - if (!isDefined(val)) return; - - switch (key) { - case 'options': - this[$setConfig](this[$Internal](), val); - break; - - case 'plugins': - this.extend(val, discovery.options); - break; - - case 'terms': - this.extend(val); - break; - - case 'numbers': - registryUpdate('NUMBER', val); - break; - - case 'timeZones': { - const tzs = Object.fromEntries(ownEntries(val).map(([k, v]) => [k.toString().toLowerCase(), v])); - registryUpdate('TIMEZONE', tzs); - break; - } - - case 'formats': { - const internal = this[$Internal](); - internal.config.formats = internal.config.formats.extend(val) as t.FormatRegistry; - registryUpdate('FORMAT', val); - break; - } - - case 'monthDay': - registryUpdate('MONTH_DAY', val); - this[$setConfig](this[$Internal](), { monthDay: val }); - break; - - case 'intl': - case 'relativeTime': - case 'planner': - case 'ignore': - this[$setConfig](this[$Internal](), { [key]: val }); - break; - } - }); + const opts = this[$setDiscovery](this[$Internal](), discovery); + if (!isEmpty(opts)) this[$setConfig](this[$Internal](), opts); // only trigger init if we're assigning a new discovery object to a symbol if (ownKeys(item).some(key => DISCOVERY.has(key as any))) { - const discovery = (isSymbol(options) ? options : (options as any)?.discovery) ?? sym.$Tempo; - const discoverySymbol = isString(discovery) ? Symbol.for(discovery) : (isSymbol(discovery) && !Symbol.keyFor(discovery) ? Symbol('TempoSandbox') : discovery); + const discoveryArg = (isSymbol(options) ? options : (options as any)?.discovery) ?? sym.$Tempo; + const discoverySymbol = isString(discoveryArg) ? Symbol.for(discoveryArg) : (isSymbol(discoveryArg) && !Symbol.keyFor(discoveryArg) ? Symbol('TempoSandbox') : discoveryArg); if ((globalThis as Record)[discoverySymbol as symbol] !== item) { (globalThis as Record)[discoverySymbol as symbol] = item; @@ -659,12 +580,12 @@ export class Tempo { } }) } finally { - Tempo.#lifecycle.extendDepth--; // decrement the re-entrant nesting counter + _lifecycle.extendDepth--; // decrement the re-entrant nesting counter } - if (Tempo.#lifecycle.extendDepth === 0) { + if (_lifecycle.extendDepth === 0) { this[$buildGuard](); - setPatterns(this[$Internal]()); // rebuild the global patterns + setPatterns(this[$Internal]()); // rebuild the global patterns } return this; @@ -705,31 +626,42 @@ export class Tempo { discovery: normalizedDiscovery as any, catch: options.catch ?? false }, + (SandboxTempo as any)[$setDiscovery](state, data), { ...options, discovery: normalizedDiscovery } ); - // If the sandbox was provided with monthDay discovery, resolve and apply it to the isolated state - if (isObject(options.discovery) && options.discovery.monthDay) { - const discoveryMD = resolveMonthDay(options.discovery.monthDay, Tempo.MONTH_DAY); - state.parse.monthDay = { ...state.parse.monthDay, ...discoveryMD }; - } - Object.freeze(SandboxTempo); return SandboxTempo as unknown as typeof Tempo; } + /** + * Wait for the background licensing and validation engine to finish settling. + * Resolves immediately if no license key or validation is pending. + * @returns The final, human-readable license status ('none', 'active', 'expired', etc.) + */ + static async ready(): Promise { + const rt = getRuntime(); + const jws = rt.license?.jws; + if (jws) { + try { await jws; } catch { /* fail-safe */ } + } + return resolveDisplayStatus(rt.license.status); + } + /** Reset Tempo to its default, built-in registration state */ static init(options: t.Options = {}): typeof Tempo { - if (Tempo.#lifecycle.initialising) return this; - Tempo.#lifecycle.initialising = true; + if (_lifecycle.initialising) return this; + _lifecycle.initialising = true; try { + setLogLevel(options.debug ?? Default?.debug ?? LOG.Info); + const rt = getRuntime(); rt.state = undefined; // force fresh state const state = init(options); (state as any)._count = 0; if (this[$IsBase]) { - Tempo.#global = state; + _global = state; } else { ClassStates.set(this, state); } @@ -755,7 +687,7 @@ export class Tempo { // Normalize discovery to a symbol if it's an object to prevent leakage into config state if (isObject(discovery) && !isSymbol(discovery)) { const data = discovery; - discovery = Symbol.for(`tempo.discovery.${Tempo.#usrCount++}`); + discovery = Symbol.for(`tempo.discovery.${_usrCount++}`); (globalThis as any)[discovery] = data; } const normalizedDiscovery = isString(discovery) ? Symbol.for(discovery) : (discovery as symbol); @@ -763,7 +695,7 @@ export class Tempo { // Resolve locale if missing or invalid const currentLocale = config.locale; - const locale = (!currentLocale || currentLocale === 'en-US') ? Tempo.#locale(currentLocale) : currentLocale; + const locale = (!currentLocale || currentLocale === 'en-US') ? Tempo._locale(currentLocale) : currentLocale; if (!hasOwn(config, 'get')) { Object.defineProperty(config, 'get', { @@ -772,12 +704,12 @@ export class Tempo { }); } - Tempo.#usrCount = 0; // reset user-key counter + _usrCount = 0; // reset user-key counter for (const key of Object.keys(Token)) // purge user-allocated Tokens if (key.startsWith('usr.')) // only remove 'usr.' prefixed keys delete Token[key]; - Tempo.#termMap.clear(); // clear term lookup map + Tempo._termMap.clear(); // clear term lookup map registryReset(); // purge formats and numbers // 3. Apply configuration via unified setters (non-destructive merge) @@ -797,19 +729,24 @@ export class Tempo { options, // explicit options from the call ) + setLogLevel(state.config.debug ?? options.debug ?? Default?.debug ?? LOG.Info); + if (options.plugins) this.extend(options.plugins); // ensure init-plugins are processed before 'ready' - if (Context.type === CONTEXT.Browser || options.debug === true) - Tempo.#dbg.info(this.config, 'Tempo:', state.config); + if (Context.type === CONTEXT.Browser || state.config.debug === LOG.Debug) + logDebug('Tempo:', this.config, state.config); - Tempo.#lifecycle.ready = true; setPatterns(state); // rebuild the global patterns (Master Guard etc) // 🏛️ Licensing Reckoning (Background Verification) if (rt.license.jws?.isPending) { const jws = rt.license.jws; import('#tempo/license') - .then(m => jws.resolve(m)) + .then(async m => { + const validator = new m.Validator(rt.license.key!); + const res = await validator.verify(); + jws.resolve(res); + }) .catch(err => { // If the stored JWS is still the same (i.e. we haven't set a new one since), then clear the status if (rt.license.jws === jws) @@ -818,12 +755,15 @@ export class Tempo { }); } + _lifecycle.ready = true; + return this; + } catch (err) { + _lifecycle.ready = false; + throw err; } finally { - Tempo.#lifecycle.initialising = false; - Tempo.#lifecycle.bootstrap = false; + _lifecycle.initialising = false; + _lifecycle.bootstrap = false; } - - return this } /** explicitly enable/disable "catch" mode for internal errors */ @@ -862,7 +802,7 @@ export class Tempo { /** @internal lookup or registers a new `Symbol` for a given key. */ static getSymbol(key?: string | symbol) { if (isUndefined(key)) { - const usr = `usr.${++Tempo.#usrCount}`; // allocate a prefixed 'user' key + const usr = `usr.${++_usrCount}`; // allocate a prefixed 'user' key return Token[usr] = Symbol(usr); // add to Symbol register } @@ -925,7 +865,7 @@ export class Tempo { static get discovery() { const discovery = this.config.discovery; const sym = isString(discovery) ? Symbol.for(discovery) : discovery; - return Tempo.#getConfig(sym as symbol); + return Tempo._getConfig(sym as symbol); } static get options() { @@ -946,11 +886,11 @@ export class Tempo { /** static Tempo.terms (registry) */ static get terms(): Secure & Record { const rt = getRuntime(); - const list = Tempo.#terms.map(({ define, resolve, ...rest }) => { + const list = Tempo._terms.map(({ define, resolve, ...rest }) => { const item = { ...rest } as any; if (hasOwn(rt.license.scopes, rest.key)) { const meta = rt.license.scopes[rest.key]; - item.status = rt.license.status; + item.status = resolveDisplayStatus(rt.license.status); item.expires = meta.exp ?? rt.license.expires; if (meta.updated_at) item.updated = meta.updated_at; } @@ -963,7 +903,7 @@ export class Tempo { list.push({ key: scope, scope, - status: rt.license.status, + status: resolveDisplayStatus(rt.license.status), expires: meta.exp ?? rt.license.expires, updated: meta.updated_at, description: `Premium plugin (${scope})` @@ -1028,7 +968,7 @@ export class Tempo { /** allow instanceof to work across module boundaries via the local brand symbol */ static [$Identity] = true; static [Symbol.hasInstance](instance: any) { - return Boolean(instance?.[$Identity]) + return isDefined(instance?.[$Identity]) } /** check if a supplied variable is a valid Tempo instance */ @@ -1038,7 +978,7 @@ export class Tempo { static { // Static initialization block to sequence the bootstrap phase // Define the reactive register hook - getRuntime().setHook($Register, (plugin: Plugin | Plugin[]) => { + getRuntime().setHook($Register, (plugin: TempoPlugin | TempoPlugin[]) => { if (!Tempo.isExtending) Tempo.extend(plugin) }); @@ -1056,7 +996,7 @@ export class Tempo { }); Tempo.init(); // synchronously initialize the library - getRuntime().logger = Tempo.#dbg; + getRuntime().logger = logTempo; } /** constructor tempo */ #tempo?: t.DateTime; @@ -1073,7 +1013,7 @@ export class Tempo { /** current parsing depth to manage state isolation */ #parseDepth = 0; /** current mutation depth to manage infinite recursion */#mutateDepth = 0; /** instance values to complement static values */ #local = { - /** instance configuration */ config: { [$Logify]: true } as unknown as Internal.Config, + /** instance configuration */ config: { [$LogConfig]: true } as unknown as Internal.Config, /** instance parse rules (only populated if provided) */ parse: { result: [] as Internal.MatchResult[] } as Internal.Parse } as Internal.State; @@ -1081,13 +1021,12 @@ export class Tempo { static [$errored] = $errored; /** @internal */ static [TermError](config: Internal.Config, term: string): void { - const hint = Tempo.#terms.length === 0 ? ". (No term plugins are registered—did you forget to call Tempo.extend(TermsModule)?)" : ""; + const hint = Tempo._terms.length === 0 ? ". (No term plugins are registered—did you forget to call Tempo.extend(TermsModule)?)" : ""; const msg = `Unknown Term identifier: ${term}${hint}`; - Tempo.#dbg.error(config, msg); + logError(msg, config); } - /** @internal */ static get [$dbg](): Logify { return Tempo.#dbg } - /** @internal */ static get [$guard]() { return Tempo.#guard } + /** * @internal Internal access to instance private state. @@ -1173,7 +1112,7 @@ export class Tempo { // 🏛️ Initialization Strategy ('auto' | 'strict' | 'defer') if (mode === Tempo.MODE.Defer) this.#local.parse.lazy = true; else if (mode === Tempo.MODE.Strict) this.#local.parse.lazy = false; - else if (isString(this.#tempo) && !isEmpty(input) && Tempo.#guard.test(trimAll(input))) { + else if (isString(this.#tempo) && !isEmpty(input) && (this.constructor as typeof Tempo)[$guard].test(trimAll(input))) { this.#local.parse.lazy = true; // auto-switch to lazy-mode for valid strings } @@ -1232,7 +1171,7 @@ export class Tempo { if (isUndefined(this.#zdt)) { this.#errored = true; const msg = `Tempo parse returned undefined for: ${String(this.#tempo)}`; - Tempo.#dbg.error(this.#local.config, msg); + logError(msg, this.#local.config); this.#zdt = now; } secure(this.#local.config); @@ -1241,10 +1180,10 @@ export class Tempo { this.#errored = true; // mark as errored const msg = `Cannot create Tempo: ${(err as Error).message}\n${(err as Error).stack}`; if (this.#local.config.catch === true) { - Tempo.#dbg.error(this.#local.config, msg); // log as error if in catch-mode + logError(msg, this.#local.config); // log as error if in catch-mode this.#zdt = now; } else { - Tempo.#dbg.error(this.#local.config, err, msg); // log as error then re-throw + logError((err as Error), this.#local.config, msg); // log as error then re-throw throw err; } } @@ -1272,7 +1211,7 @@ export class Tempo { } catch (e: any) { const msg = (e?.message ?? '').toLowerCase(); if (msg.includes('constructor') || msg.includes('class') || (e instanceof TypeError) || isClass(define)) { - Tempo.#dbg.warn(this.#local.config, `Misidentified class in delegator evaluate: ${String(define)}`, e.stack ?? e); + logWarn(`Misidentified class in delegator evaluate: ${String(define)}`, this.#local.config, e.stack ?? e); memo = define; } else { throw e; @@ -1315,7 +1254,7 @@ export class Tempo { // 🛡️ Lazy Proxy Guard (Licensing) if (this.#isBlocked(key)) return undefined; - const term = Tempo.#termMap.get(key); + const term = Tempo._termMap.get(key); if (term) { const isKeyOnly = term.key === key; const define = (keyOnly: boolean) => { @@ -1327,11 +1266,12 @@ export class Tempo { const result = term.define.call(this, keyOnly); const res = Array.isArray(result) ? getTermRange(this, result, keyOnly) : result; return isObject(res) ? secure(res) : res; - } catch (err: any) { - if (err.message.includes('Class constructor')) { - Tempo.#dbg.warn(this.#local.config, `Misidentified class in term definition: ${key}`, err.stack ?? err); + } catch (err: unknown) { + const error = asError(err); + if (error.message.includes('Class constructor')) { + logWarn(`Misidentified class in term definition: ${key}`, this.#local.config, error.stack ?? error); } else { - throw err; + throw error; } } @@ -1350,13 +1290,13 @@ export class Tempo { } #discover(host: 'term' | 'fmt', target: any) { - if (!Tempo.#lifecycle.ready) return; + if (!_lifecycle.ready) return; if (host === 'fmt') { ownKeys(this.#local.config.formats).forEach(key => { if (isString(key)) this.#setLazy(target, key, () => this.format(key as t.Format)); }); } else { - Tempo.#terms.forEach(term => { + Tempo._terms.forEach(term => { const define = (keyOnly: boolean, anchor?: any) => { // 🛡️ Resolution Guard (Licensing) if (getRuntime().license.status !== LICENSE.Active && this.#isBlocked(term.key)) return undefined; @@ -1365,11 +1305,12 @@ export class Tempo { const res = term.resolve ? term.resolve.call(this, anchor) : term.define.call(this, keyOnly, anchor); const out = (getTermRange(this, (Array.isArray(res) ? (res as any) : [res]), keyOnly, anchor) as any); return isObject(out) ? secure(out) : out; - } catch (err: any) { - if (err.message.includes('Class constructor')) { - Tempo.#dbg.warn(this.#local.config, `Misidentified class in term discovery: ${term.key}`, err.stack ?? err); + } catch (err: unknown) { + const error = asError(err); + if (error.message.includes('Class constructor')) { + logWarn(`Misidentified class in term discovery: ${term.key}`, this.#local.config, error.stack ?? error); } else { - throw err; + throw error; } } }; @@ -1401,7 +1342,7 @@ export class Tempo { /** Full weekday name (e.g., 'Monday') */ get wkd() { return Tempo.WEEKDAYS.keyOf(this.toDateTime().dayOfWeek as t.Weekday) } /** iso weekday number: Mon=1, Sun=7 */ get dow() { return this.toDateTime().dayOfWeek as t.Weekday } /** Nanoseconds since Unix epoch (BigInt) */ get nano() { return this.toDateTime().epochNanoseconds } - /** Standard ISO 8601 string in UTC */ get iso() { return this.toDate().toISOString() } + /** Standard ISO 8601 string in UTC */ get iso() { return this.toDateTime().toInstant().toString() } /** `true` if the underlying date-time is valid. */ get isValid() { return this.#resolve(zdt => !this.#errored && isZonedDateTime(zdt)); } /** list of registered terms and their available range keys */ @@ -1434,7 +1375,7 @@ export class Tempo { /** current Tempo configuration */ get config() { - const global = this[$Internal]().config; + const global = (this as any)[$Internal]().config; const out = markConfig(Object.create(global)); Object.entries(this.#local.config).forEach(([k, v]) => setProperty(out, k, v)); @@ -1565,7 +1506,7 @@ export class Tempo { const res = interpret(this, 'ParseModule', 'parse', false, tempo, dateTime, term); if (isUndefined(res)) { const msg = `ParseModule error. Could not parse ${String(tempo)}`; - Tempo.#dbg.error(this.#local.config, msg); + logError(msg, this.#local.config); return undefined as any; } return res; @@ -1584,7 +1525,7 @@ export class Tempo { if (isUndefined(tempo) || isEmpty(tempo)) return dateTime ?? instant().toZonedDateTimeISO(this.#local.config.timeZone); const msg = 'Tempo ParseModule not loaded. Did you forget to Tempo.extend(ParseModule)?'; - Tempo.#dbg.error(this.#local.config, msg); + logError(msg, this.#local.config); return undefined as any; } @@ -1625,7 +1566,7 @@ export class Tempo { if (blocked.includes(rt.license.status) && hasOwn(rt.license.scopes, key)) { const msg = `License for premium term '${key}' is ${rt.license.status}. Access denied.`; - Tempo.#dbg.warn(this.#local.config, msg); + logWarn(msg, this.#local.config); return true; } diff --git a/packages/tempo/src/tempo.type.ts b/packages/tempo/src/tempo.type.ts index a950958a..7b49c3b7 100644 --- a/packages/tempo/src/tempo.type.ts +++ b/packages/tempo/src/tempo.type.ts @@ -7,7 +7,7 @@ * Inside `tempo.class.ts` these are accessed via `import * as t`. */ import type { Pledge } from '#library/pledge.class.js'; -import type { Logify } from '#library/logify.class.js'; +import type { DebugLevel } from '#library/logger.class.js'; import type { IntRange, NonOptional, Property, Plural, Prettify, TemporalObject, TypeValue, RegistryOption, Branded } from '#library/type.library.js'; import { sym, type TempoBrand } from '#tempo/support/support.symbol.js'; @@ -86,7 +86,6 @@ export type Groups = Record export interface Options extends Partial { planner?: PlannerOptions; intl?: IntlOptions; - relativeTime?: RelativeTime | ((value: number, unit: any) => string); [key: string]: any; } @@ -147,8 +146,8 @@ export type us = IntRange<0, 999> export type ns = IntRange<0, 999> export type ww = IntRange<1, 53> -export type Duration = NonOptional & Record<"iso", string> & Record<"sign", number> & Record<"blank", boolean> & Record<"unit", string | undefined> & { - balance(opts?: { nominal?: boolean; relativeTo?: any; largestUnit?: Unit | string }): Duration; +export type Duration = NonOptional & Record<"iso", string> & Record<"sign", number> & Record<"blank", boolean> & Record<"unit", string | undefined> & { + balance(opts?: { nominal?: boolean; relativeTo?: any; largestUnit?: Unit | string }): Duration; format(opts?: Intl.NumberFormatOptions & { locales?: string | string[] }): string; } @@ -190,7 +189,8 @@ export interface RelativeTime { } export interface IntlOptions { - /** relative time formatting configuration */ relativeTime?: RelativeTime | ((value: number, unit: any) => string); + /** relative time formatting configuration */ relativeTimeFormat?: RelativeTime | ((value: number, unit: any) => string); + /** multi-unit duration formatting configuration */ durationFormat?: any | ((duration: any) => string); /** absolute unit duration formatting configuration */ numberFormat?: Intl.NumberFormatOptions | ((value: number, unit: any) => string); } @@ -221,9 +221,9 @@ export namespace Internal { export interface BaseOptions { /** localStorage key */ store: string; /** globalThis Discovery Symbol */ discovery: string | symbol | Discovery; - /** additional console.log for tracking */ debug: Logify.Constructor["debug"]; - /** catch or throw Errors */ catch: Logify.Constructor["catch"]; - /** suppress console output during catch */ silent: Logify.Constructor["silent"]; + /** additional console.log for tracking */ debug: DebugLevel; + /** catch or throw Errors */ catch: boolean; + /** suppress console output during catch */ silent: boolean; /** Temporal timeZone */ timeZone: Temporal.TimeZoneLike; /** Temporal calendar */ calendar: Temporal.CalendarLike; /** locale (e.g. en-AU) */ locale: string; @@ -240,7 +240,7 @@ export namespace Internal { /** custom time aliases (periods). */ period: Period | RegistryOption; /** noise words to ignore during parsing. */ ignore: Ignore; /** custom format strings to merge in the FORMAT enum */formats: Property; - /** plugins to be automatically extended */ plugins: TempoPlugin | TempoPlugin[]; + /** plugins to be automatically extended */ plugins: (TempoPlugin | TermPlugin) | (TempoPlugin | TermPlugin)[]; /** supplied value to parse */ value: DateTime; /** @internal temporary anchor used during parsing */ anchor: any; /** @internal accumulated parse results */ result?: Match[] | undefined; @@ -322,15 +322,13 @@ export namespace Internal { /** pre-defined config options for Tempo.#global */ options?: Options | (() => Options); /** aliases to merge in the TimeZone dictionary */ timeZones?: Record; /** regional date-parsing configuration */ monthDay?: MonthDay; - /** relative time configuration (shorthand) */ relativeTime?: RelativeTime | ((value: number, unit: any) => string); /** parse planner configuration (layoutOrder, etc.) */ planner?: PlannerOptions; /** aliases to merge in the Number-Word dictionary */ numbers?: Record; - /** @deprecated use 'terms' */ term?: TermPlugin; /** term plugins to be registered via Tempo.addTerm() */terms?: TermPlugin | TermPlugin[]; /** internationalization configuration (relativeTime, etc.) */intl?: IntlOptions; /** custom format strings to merge in the FORMAT dictionary */formats?: Property; /** noise words to ignore during parsing via Tempo.ignore() */ignore?: Ignore; - /** plugins to be automatically extended via Tempo.extend() */plugins?: TempoPlugin | TempoPlugin[]; + /** plugins to be automatically extended via Tempo.extend() */plugins?: (TempoPlugin | TermPlugin) | (TempoPlugin | TermPlugin)[]; } export interface LicenseScope { @@ -353,7 +351,7 @@ export namespace Internal { export interface LicensingModule { Validator: new (jwt: string) => { verify(): Promise; - syncRevocation(jwsUrl: string, currentJti: string): Promise; + syncRevocation(jwsUrl: string, currentJti: string): Promise<{ revoked: boolean, success: boolean }>; }; } @@ -361,7 +359,7 @@ export namespace Internal { status: enums.LICENSE; key?: string; scopes: Record; - jws?: Pledge; + jws?: Pledge; expires?: number | string; issuedAt?: number; issuer?: string; diff --git a/packages/tempo/src/tsconfig.json b/packages/tempo/src/tsconfig.json index c9c15e57..fa7b8d87 100644 --- a/packages/tempo/src/tsconfig.json +++ b/packages/tempo/src/tsconfig.json @@ -16,16 +16,19 @@ "#server/*": [ "../../library/src/server/*" ], "#tempo": [ "./tempo.index.ts" ], "#tempo/core": [ "./core.index.ts" ], + "#tempo/enums": [ "./support/support.enum.ts" ], "#tempo/parse": [ "./module/module.parse.ts" ], "#tempo/format": [ "./module/module.format.ts" ], "#tempo/discrete": [ "./module/module.index.ts" ], + "#tempo/module": [ "./module/module.index.ts" ], "#tempo/duration": [ "./module/module.duration.ts" ], "#tempo/mutate": [ "./module/module.mutate.ts" ], - "#tempo/ticker": [ "./plugin/extend/extend.ticker.ts" ], "#tempo/engine/*.js": [ "./engine/*.ts" ], "#tempo/module/*.js": [ "./module/*.ts" ], "#tempo/plugin/extend/*.js": [ "./plugin/extend/*.ts" ], "#tempo/plugin/term/*.js": [ "./plugin/term/*.ts" ], + "#tempo/plugin/plugin.*.js": [ "./plugin/plugin.*.ts" ], + "#tempo/term": [ "./plugin/term/term.index.ts" ], "#tempo/term/*": [ "./plugin/term/term.*.ts" ], "#tempo/support": [ "./support/support.index.ts" ], "#tempo/support/*": [ "./support/*" ], diff --git a/packages/tempo/src/tsconfig.repl.json b/packages/tempo/src/tsconfig.repl.json new file mode 100644 index 00000000..6358152a --- /dev/null +++ b/packages/tempo/src/tsconfig.repl.json @@ -0,0 +1,43 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "paths": { + "#library": [ "../../library/src/common.index.ts" ], + "#library/*": [ "../../library/src/common/*" ], + "#browser": [ "../../library/src/browser.index.ts" ], + "#browser/*": [ "../../library/src/browser/*" ], + "#server": [ "../../library/src/server.index.ts" ], + "#server/*": [ "../../library/src/server/*" ], + "#tempo": [ "./tempo.index.ts" ], + "#tempo/core": [ "./core.index.ts" ], + "#tempo/enums": [ "./support/support.enum.ts" ], + "#tempo/parse": [ "./module/module.parse.ts" ], + "#tempo/format": [ "./module/module.format.ts" ], + "#tempo/discrete": [ "./module/module.index.ts" ], + "#tempo/module": [ "./module/module.index.ts" ], + "#tempo/duration": [ "./module/module.duration.ts" ], + "#tempo/mutate": [ "./module/module.mutate.ts" ], + "#tempo/engine/*.js": [ "./engine/*.ts" ], + "#tempo/module/*.js": [ "./module/*.ts" ], + "#tempo/plugin/extend/*.js": [ "./plugin/extend/*.ts" ], + "#tempo/plugin/term/*.js": [ "./plugin/term/*.ts" ], + "#tempo/plugin/plugin.*.js": [ "./plugin/plugin.*.ts" ], + "#tempo/term": [ "./plugin/term/term.index.ts" ], + "#tempo/term/*": [ "./plugin/term/term.*.ts" ], + "#tempo/support": [ "./support/support.index.ts" ], + "#tempo/support/*": [ "./support/*" ], + "#tempo/license": [ + "./support/support.license.ts" + ], + "@magmacomputing/tempo": [ "./tempo.index.ts" ], + "@magmacomputing/tempo/plugin": [ "./plugin/plugin.index.ts" ], + "@magmacomputing/tempo/plugin/*": [ "./plugin/*.ts" ], + "@magmacomputing/tempo/term": [ "./plugin/term/term.index.ts" ], + "@magmacomputing/tempo/term/*": [ "./plugin/term/term.*.ts" ], + "@magmacomputing/tempo/enums": [ "./support/support.enum.ts" ], + "@magmacomputing/tempo/core": [ "./core.index.ts" ], + "@magmacomputing/tempo/*": [ "./*" ], + "#tempo/*": [ "./*" ] + } + } +} \ No newline at end of file diff --git a/packages/tempo/test/core/alias-engine-protochain.test.ts b/packages/tempo/test/core/alias-engine-protochain.test.ts index fd42d4f8..1051fe9b 100644 --- a/packages/tempo/test/core/alias-engine-protochain.test.ts +++ b/packages/tempo/test/core/alias-engine-protochain.test.ts @@ -1,31 +1,31 @@ import { AliasEngine } from '#tempo/engine/engine.alias.js'; -import { Logify } from '#library/logify.class.js'; -import { vi } from 'vitest'; +import { logTempo } from '#tempo/support/support.util.js'; describe('AliasEngine prototype chain (Global → Sandbox → Instance)', () => { - const logger = { - warn: vi.fn(), - debug: vi.fn(), - error: vi.fn(), - log: vi.fn(), - info: vi.fn(), - trace: vi.fn() - } as unknown as Logify; - - // Simulate a global state - const globalShape = {} as { aliasEngine: AliasEngine }; - globalShape.aliasEngine = new AliasEngine({ logger }); - globalShape.aliasEngine.registerAliases('evt', [ ['globalEvt', 'globalValue'] ]); - - // Simulate a sandbox state inheriting from global - const sandboxShape = Object.create(globalShape); - sandboxShape.aliasEngine = new AliasEngine({ parent: globalShape.aliasEngine, logger }); - sandboxShape.aliasEngine.registerAliases('evt', [ ['sandboxEvt', 'sandboxValue'] ]); - - // Simulate a local/instance state inheriting from sandbox - const localShape = Object.create(sandboxShape); - localShape.aliasEngine = new AliasEngine({ parent: sandboxShape.aliasEngine, logger }); - localShape.aliasEngine.registerAliases('evt', [ ['localEvt', 'localValue'] ]); + afterEach(() => { + vi.clearAllMocks(); + }); + + let globalShape!: { aliasEngine: AliasEngine }; + let sandboxShape!: { aliasEngine: AliasEngine }; + let localShape!: { aliasEngine: AliasEngine }; + + beforeEach(() => { + // Simulate a global state + globalShape = {} as { aliasEngine: AliasEngine }; + globalShape.aliasEngine = new AliasEngine(); + globalShape.aliasEngine.registerAliases('evt', [['globalEvt', 'globalValue']]); + + // Simulate a sandbox state inheriting from global + sandboxShape = Object.create(globalShape); + sandboxShape.aliasEngine = new AliasEngine({ parent: globalShape.aliasEngine }); + sandboxShape.aliasEngine.registerAliases('evt', [['sandboxEvt', 'sandboxValue']]); + + // Simulate a local/instance state inheriting from sandbox + localShape = Object.create(sandboxShape); + localShape.aliasEngine = new AliasEngine({ parent: sandboxShape.aliasEngine }); + localShape.aliasEngine.registerAliases('evt', [['localEvt', 'localValue']]); + }); it('resolves local, sandbox, and global aliases in correct order', () => { // Local should resolve its own @@ -46,18 +46,15 @@ describe('AliasEngine prototype chain (Global → Sandbox → Instance)', () => }); it('collision detection traverses the prototype chain', () => { - (logger.warn as any).mockClear(); + const warnSpy = vi.spyOn(logTempo, 'warn'); // Register a colliding alias in local - localShape.aliasEngine.registerAliases('evt', [ ['globalEvt', 'localShadow'] ]); + localShape.aliasEngine.registerAliases('evt', [['globalEvt', 'localShadow']]); // Should warn about collision with global - expect(logger.warn).toHaveBeenCalled(); - expect((logger.warn as any).mock.calls[0][1]).toMatch(/Collision detected/i); + expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining('Collision detected')); expect(localShape.aliasEngine.resolveAlias('evt2_1')?.value).toBe('localShadow'); expect(globalShape.aliasEngine.resolveAlias('evt0_0')?.value).toBe('globalValue'); - - (logger.warn as any).mockReset(); }); }); diff --git a/packages/tempo/test/core/alias-engine.test.ts b/packages/tempo/test/core/alias-engine.test.ts index 621b9211..92432131 100644 --- a/packages/tempo/test/core/alias-engine.test.ts +++ b/packages/tempo/test/core/alias-engine.test.ts @@ -1,19 +1,13 @@ import { AliasEngine } from '#tempo/engine/engine.alias.js'; -import type { Logify } from '#library/logify.class.js'; - -// Use a real Logify logger, but spy on console.warn -const logger = { - warn: (config: any, msg: string) => console.warn(msg, config), - debug: () => { }, - error: () => { }, - log: () => { }, - info: () => { }, - trace: () => { }, -} as unknown as Logify; +import { logTempo } from '#tempo/support/support.util.js'; +import { vi, afterEach } from 'vitest'; describe('AliasEngine', () => { + afterEach(() => { + vi.clearAllMocks(); + }); it('registers and resolves string and function aliases', () => { - const engine = new AliasEngine({ logger }); + const engine = new AliasEngine(); engine.registerAliases('evt', [['foo', 'bar']]); expect(engine.resolveAlias('evt0_0')?.value).toBe('bar'); engine.registerAliases('per', [['noon', function () { return '12:00'; }]]); @@ -22,9 +16,9 @@ describe('AliasEngine', () => { }); it('supports parent/child shadowing and fallback', () => { - const globalEngine = new AliasEngine({ logger }); + const globalEngine = new AliasEngine(); globalEngine.registerAliases('evt', [['foo', 'bar']]); - const localEngine = new AliasEngine({ parent: globalEngine, logger }); + const localEngine = new AliasEngine({ parent: globalEngine }); // Local should resolve parent's alias before shadowing expect(localEngine.resolveAlias('evt0_0')?.value).toBe('bar'); expect(localEngine.resolveAlias('evt0_0')?.source).toBe('global'); @@ -36,32 +30,30 @@ describe('AliasEngine', () => { }); it('warns on local/global collision', () => { - const warnSpy = vi.spyOn(console, 'warn'); - const globalEngine = new AliasEngine({ logger }); + const warnSpy = vi.spyOn(logTempo, 'warn'); + const globalEngine = new AliasEngine(); globalEngine.registerAliases('evt', [['xmas', '25-Dec']]); - const localEngine = new AliasEngine({ parent: globalEngine, logger }); + const localEngine = new AliasEngine({ parent: globalEngine }); localEngine.registerAliases('evt', [['xmas', '24-Dec']]); - expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining('Collision detected'), undefined); - warnSpy.mockRestore(); + expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining('Collision detected')); }); it('warns on local collision', () => { - const warnSpy = vi.spyOn(console, 'warn'); - const engine = new AliasEngine({ logger }); + const warnSpy = vi.spyOn(logTempo, 'warn'); + const engine = new AliasEngine(); engine.registerAliases('evt', [['xmas', '25-Dec'], ['xmas', '24-Dec']]); - expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining('Collision detected'), undefined); - warnSpy.mockRestore(); + expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining('Collision detected')); }); it('registers and resolves batch aliases', () => { - const engine = new AliasEngine({ logger }); + const engine = new AliasEngine(); engine.registerAliases('evt', [['foo', 'bar'], ['baz', 'qux']]); expect(engine.resolveAlias('evt0_0')?.value).toBe('bar'); expect(engine.resolveAlias('evt0_1')?.value).toBe('qux'); }); it('clears only events or periods', () => { - const engine = new AliasEngine({ logger }); + const engine = new AliasEngine(); engine.registerAliases('evt', [['foo', 'bar']]); engine.registerAliases('per', [['noon', '12:00']]); expect(engine.resolveAlias('evt0_0')?.value).toBe('bar'); @@ -76,26 +68,24 @@ describe('AliasEngine', () => { }); it('handles regex-like collision heuristics', () => { - const warnSpy = vi.spyOn(console, 'warn'); - const engine = new AliasEngine({ logger }); + const warnSpy = vi.spyOn(logTempo, 'warn'); + const engine = new AliasEngine(); engine.registerAliases('evt', [['xmas( )?eve', '24-Dec'], ['xmas eve', '24-Dec']]); // Should treat "xmas eve" and "xmas( )?eve" as same base word - expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining('Collision detected'), undefined); - warnSpy.mockRestore(); + expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining('Collision detected')); }); it('does not warn on non-colliding aliases', () => { - const warnSpy = vi.spyOn(console, 'warn'); - const engine = new AliasEngine({ logger }); + const warnSpy = vi.spyOn(logTempo, 'warn'); + const engine = new AliasEngine(); engine.registerAliases('evt', [['xmas', '25-Dec'], ['bday', '20-May']]); expect(warnSpy).not.toHaveBeenCalled(); - warnSpy.mockRestore(); }); it('resolves to parent after clear', () => { - const globalEngine = new AliasEngine({ logger }); + const globalEngine = new AliasEngine(); globalEngine.registerAliases('evt', [['foo', 'bar']]); - const localEngine = new AliasEngine({ parent: globalEngine, logger }); + const localEngine = new AliasEngine({ parent: globalEngine }); localEngine.registerAliases('evt', [['foo', 'baz']]); expect(localEngine.resolveAlias('evt1_0')?.value).toBe('baz'); @@ -105,7 +95,7 @@ describe('AliasEngine', () => { }); it('handles empty/optional/edge-case aliases', () => { - const engine = new AliasEngine({ logger }); + const engine = new AliasEngine(); engine.registerAliases('evt', [['', 'empty'], ['foo', '']]); expect(engine.resolveAlias('evt0_0')?.value).toBe('empty'); expect(engine.resolveAlias('evt0_1')?.value).toBe(''); diff --git a/packages/tempo/test/core/dispose.core.test.ts b/packages/tempo/test/core/dispose.core.test.ts index 90ff2f5c..5390a0cd 100644 --- a/packages/tempo/test/core/dispose.core.test.ts +++ b/packages/tempo/test/core/dispose.core.test.ts @@ -1,10 +1,9 @@ import { Tempo } from '#tempo/core'; import { Pledge } from '#library/pledge.class.js'; -import { TickerModule } from '#tempo/ticker'; import { FormatModule } from '#tempo/format'; import { MutateModule } from '#tempo/mutate'; -Tempo.extend(TickerModule, FormatModule, MutateModule); +Tempo.extend(FormatModule, MutateModule); describe('Static Symbol.dispose', () => { @@ -25,8 +24,8 @@ describe('Static Symbol.dispose', () => { test('Pledge static dispose resets static state', () => { // 1. Set a non-default static config - Pledge.init({ debug: true, silent: true, tag: 'TestPledge' }); - expect(Pledge.status.debug).toBe(true); + Pledge.init({ debug: 5, silent: true, tag: 'TestPledge' }); + expect(Pledge.status.debug).toBe(5); expect(Pledge.status.tag).toBe('TestPledge'); // 2. Dispose @@ -38,84 +37,6 @@ describe('Static Symbol.dispose', () => { expect(Pledge.status.tag).toBeUndefined(); } }); - -}); - -describe('Ticker Symbol.dispose', () => { - - test('Ticker callback returns a disposable function', async () => { - let count = 0; - const stop = Tempo.ticker(0.05, () => { count++ }); - - expect(typeof stop).toBe('function'); - - if (typeof Symbol.dispose === 'symbol') { - expect(stop[Symbol.dispose]).toBeDefined(); - expect(typeof stop[Symbol.dispose]).toBe('function'); - - // Use the dispose method instead of calling the function directly - stop[Symbol.dispose](); - - const finalCount = count; - await new Promise(resolve => setTimeout(resolve, 150)); - expect(count).toBe(finalCount); // Should have stopped - } else { - stop(); // Fallback for environments without the symbol - } - }); - - test('Ticker generator returns an async disposable', async () => { - const ticker = Tempo.ticker(0.02); - - if (typeof Symbol.asyncDispose === 'symbol') { - expect((ticker as any)[Symbol.asyncDispose]).toBeDefined(); - expect(typeof (ticker as any)[Symbol.asyncDispose]).toBe('function'); - - let i = 0; - for await (const t of ticker) { - expect(t).toBeDefined(); - if (++i === 2) break; - } - - // Explicitly call asyncDispose (though break does it via .return()) - await ticker[Symbol.asyncDispose](); - - const result = await ticker.next(); - expect(result.done).toBe(true); - } - }); - - test('Ticker callback seeding (Virtual Clock)', async () => { - const seed = '2024-01-01T00:00:00'; - const results: string[] = []; - const stop = Tempo.ticker({ seed, seconds: 0.05 }, (t) => { - results.push(t.format('sortTime') as string); - }); - - await new Promise(resolve => setTimeout(resolve, 125)); // Should have 3 ticks (0ms: seed, 50ms: +50, 100ms: +100) - stop(); - - expect(results[0]).toBe(new Tempo(seed).format('sortTime')); - expect(results[1]).toBe(new Tempo(seed).add({ milliseconds: 50 }).format('sortTime')); - expect(results[2]).toBe(new Tempo(seed).add({ milliseconds: 100 }).format('sortTime')); - }); - - test('Ticker generator seeding (Virtual Clock)', async () => { - const seed = '2024-01-01T00:00:00'; - const ticker = Tempo.ticker({ seed, seconds: 0.05 }); - const results: string[] = []; - - let i = 0; - for await (const t of ticker) { - results.push(t.format('sortTime') as string); - if (++i === 3) break; - } - - expect(results[0]).toBe(new Tempo(seed).format('sortTime')); - expect(results[1]).toBe(new Tempo(seed).add({ milliseconds: 50 }).format('sortTime')); - expect(results[2]).toBe(new Tempo(seed).add({ milliseconds: 100 }).format('sortTime')); - }); - test('Tempo static dispose resets global config (using syntax)', () => { const systemTZ = Intl.DateTimeFormat().resolvedOptions().timeZone; { @@ -127,19 +48,4 @@ describe('Ticker Symbol.dispose', () => { expect(Tempo.config.timeZone).toBe(systemTZ); }); - test('Ticker generator (await using syntax)', async () => { - const seed = '2024-01-01T12:00:00'; - const results: string[] = []; - { - await using ticker = Tempo.ticker({ seed, seconds: 0.02 }); - let i = 0; - for await (const t of ticker) { - results.push(t.format('sortTime') as string); - if (++i === 2) break; - } - } - expect(results.length).toBe(2); - expect(results[0]).toBe(new Tempo(seed).format('sortTime')); - }); - }); diff --git a/packages/tempo/test/core/sandbox-factory.test.ts b/packages/tempo/test/core/sandbox-factory.test.ts index 34fd1298..930a1b8a 100644 --- a/packages/tempo/test/core/sandbox-factory.test.ts +++ b/packages/tempo/test/core/sandbox-factory.test.ts @@ -103,4 +103,19 @@ describe('Sandbox Factory Pattern', () => { expect(Sandbox1.terms).toEqual(Sandbox2.terms); }); + + it('should support full discovery payloads (formats, timeZones) in create()', () => { + const discovery = { + formats: { + 'sandboxed_fmt': '{dd}/{mm}/{yyyy}' + }, + timeZones: { + 'sandbox_tz': 'Australia/Sydney' + } + }; + const Sandbox = Tempo.create({ discovery }); + + const t = new Sandbox('2026-06-03 12:00:00', { timeZone: 'sandbox_tz' }); + expect(t.format('sandboxed_fmt')).toBe('03/06/2026'); + }); }); diff --git a/packages/tempo/test/engine/parse.prefilter.flag.test.ts b/packages/tempo/test/engine/parse.prefilter.flag.test.ts index f8bc4e2f..922a09d3 100644 --- a/packages/tempo/test/engine/parse.prefilter.flag.test.ts +++ b/packages/tempo/test/engine/parse.prefilter.flag.test.ts @@ -5,8 +5,8 @@ describe('parse prefilter feature flag', () => { Tempo.init(); }); - test.skipIf(process.env.TEMPO_PREFILTER_CI === 'true')('defaults to disabled', () => { - expect(Tempo.parse.planner.preFilter).toBe(false); + test.skipIf(process.env.TEMPO_PREFILTER_CI === 'true')('defaults to enabled', () => { + expect(Tempo.parse.planner.preFilter).toBe(true); }); test('can be enabled globally via Tempo.init', () => { @@ -18,15 +18,7 @@ describe('parse prefilter feature flag', () => { expect(t.parse.result?.[0]?.match).toBe('relativeOffset'); }); - test.skipIf(process.env.TEMPO_PREFILTER_CI === 'true')('can be enabled per-instance without changing global setting', () => { - Tempo.init({ preFilter: false }); - const t = new Tempo('monday', { timeZone: 'UTC', preFilter: true }); - - expect(Tempo.parse.planner.preFilter).toBe(false); - expect(t.parse.planner.preFilter).toBe(true); - }); - - test('can be disabled per-instance even when global is enabled', () => { + test.skipIf(process.env.TEMPO_PREFILTER_CI === 'true')('can be disabled per-instance without changing global setting', () => { Tempo.init({ preFilter: true }); const t = new Tempo('monday', { timeZone: 'UTC', preFilter: false }); @@ -34,8 +26,16 @@ describe('parse prefilter feature flag', () => { expect(t.parse.planner.preFilter).toBe(false); }); + test('can be enabled per-instance even when global is disabled', () => { + Tempo.init({ preFilter: false }); + const t = new Tempo('monday', { timeZone: 'UTC', preFilter: true }); + + expect(Tempo.parse.planner.preFilter).toBe(false); + expect(t.parse.planner.preFilter).toBe(true); + }); + test('emits planner debug telemetry when debug + preFilter are enabled', () => { - Tempo.init({ debug: true, preFilter: true }); + Tempo.init({ debug: 5, preFilter: true }); const t = new Tempo('2 days ago', { timeZone: 'UTC' }); expect(t.parse.result?.[0]?.match).toBe('relativeOffset'); expect(console.debug).toHaveBeenCalled(); diff --git a/packages/tempo/test/instance/instance.since.rtf.test.ts b/packages/tempo/test/instance/instance.since.rtf.test.ts index 9d69a189..df5db4e0 100644 --- a/packages/tempo/test/instance/instance.since.rtf.test.ts +++ b/packages/tempo/test/instance/instance.since.rtf.test.ts @@ -9,7 +9,7 @@ describe('instance.since relative formatting', () => { const t1 = new Tempo('2024-01-01T12:00:00'); const t2 = new Tempo('2024-01-01T14:30:00'); - const res = t2.since(t1, { unit: 'hours', relativeTime: { style: 'long' } }); + const res = t2.since(t1, { unit: 'hours', intl: { relativeTimeFormat: { style: 'long' } } }); expect(res).toMatch(/2 hours ago/i); }); @@ -17,7 +17,7 @@ describe('instance.since relative formatting', () => { const t1 = new Tempo('2024-01-01T12:00:00'); const t2 = new Tempo('2024-01-01T14:30:00'); - const res = t2.since(t1, { unit: 'hours', relativeTime: { style: 'short' } }); + const res = t2.since(t1, { unit: 'hours', intl: { relativeTimeFormat: { style: 'short' } } }); expect(res).toMatch(/2 hrs?\. ago/i); }); @@ -26,7 +26,7 @@ describe('instance.since relative formatting', () => { const t2 = new Tempo('2024-01-01T14:30:00'); const rtf = new Intl.RelativeTimeFormat('fr', { style: 'long' }); - const res = t2.since(t1, { unit: 'hours', relativeTime: { format: rtf } }); + const res = t2.since(t1, { unit: 'hours', intl: { relativeTimeFormat: { format: rtf } } }); // French long for 2 hours ago: "il y a 2 heures" expect(res).toMatch(/il y a 2 heures/i); @@ -34,7 +34,7 @@ describe('instance.since relative formatting', () => { test('inherits relativeTime.style from instance configuration', () => { const t1 = new Tempo('2024-01-01T12:00:00'); - const t = new Tempo('2024-01-01T14:30:00', { relativeTime: { style: 'long' } }); + const t = new Tempo('2024-01-01T14:30:00', { intl: { relativeTimeFormat: { style: 'long' } } }); const res = t.since(t1, 'hours'); expect(res).toMatch(/2 hours ago/i); @@ -43,7 +43,7 @@ describe('instance.since relative formatting', () => { test('inherits relativeTime.format from instance configuration', () => { const t1 = new Tempo('2024-01-01T12:00:00'); const rtf = new Intl.RelativeTimeFormat('fr', { style: 'long' }); - const t = new Tempo('2024-01-01T14:30:00', { relativeTime: { format: rtf } }); + const t = new Tempo('2024-01-01T14:30:00', { intl: { relativeTimeFormat: { format: rtf } } }); const res = t.since(t1, 'hours'); expect(res).toMatch(/il y a 2 heures/i); diff --git a/packages/tempo/test/issues/issue-fixes.test.ts b/packages/tempo/test/issues/issue-fixes.test.ts index f013cec4..4798d718 100644 --- a/packages/tempo/test/issues/issue-fixes.test.ts +++ b/packages/tempo/test/issues/issue-fixes.test.ts @@ -112,17 +112,17 @@ describe('Tempo Issue Fixes', () => { test('set() accepts two arguments (value, options)', () => { const t = new Tempo('2024-05-20 10:00', { timeZone: 'UTC' }) // This would have failed before - const shifted = t.set('tomorrow', { timeZone: 'America/New_York', debug: false }) + const shifted = t.set('tomorrow', { timeZone: 'America/New_York', debug: 0 }) expect(shifted.tz).toBe('America/New_York') expect(shifted.format('{yyyy}-{mm}-{dd}')).toBe('2024-05-21') }) - test('set() with debug: true preserves behavior and flags', () => { + test('set() with debug: 5 preserves behavior and flags', () => { const t = new Tempo('2024-05-20 10:00', { timeZone: 'UTC' }) - const shifted = t.set('tomorrow', { timeZone: 'America/New_York', debug: true }) + const shifted = t.set('tomorrow', { timeZone: 'America/New_York', debug: 5 }) expect(shifted.tz).toBe('America/New_York') expect(shifted.format('{yyyy}-{mm}-{dd}')).toBe('2024-05-21') - expect(shifted.config.debug).toBe(true) + expect(shifted.config.debug).toBe(5) }) test('add() accepts a duration payload', () => { diff --git a/packages/tempo/test/module/module.benchmark.test.ts b/packages/tempo/test/module/module.benchmark.test.ts new file mode 100644 index 00000000..1cde3979 --- /dev/null +++ b/packages/tempo/test/module/module.benchmark.test.ts @@ -0,0 +1,59 @@ +import { Tempo } from '#tempo'; +import { BenchmarkModule } from '#tempo/module/module.benchmark.js'; + +describe('BenchmarkModule', () => { + it('should return empty results if no data is provided', () => { + const results = BenchmarkModule.run(Tempo, { data: [] }); + expect(results).toEqual([]); + }); + + it('should calculate ops/sec and failure rates for native Date baseline', () => { + const results = BenchmarkModule.run(Tempo, { + data: ['2026-05-20', 'invalid-gibberish'], + iterations: 1, + baseline: true, + modes: [] + }); + + expect(results.length).toBe(1); + const native = results[0]; + expect(native.name).toBe('Native Date'); + expect(native.successCount).toBe(1); + expect(native.failureCount).toBe(1); + expect(native.successRate).toMatch(/^50(\.0+)?%$/); + expect(typeof native.totalTimeMs).toBe('number'); + expect(typeof native.microSecPerOp).toBe('number'); + }); + + it('should benchmark Tempo parsing modes', () => { + const results = BenchmarkModule.run(Tempo, { + data: ['2026-05-20', '🍕'], + iterations: 1, + modes: ['auto', 'defer'] + }); + + + expect(results.length).toBe(2); + + const auto = results.find(r => r.name.includes('auto'))!; + expect(auto.successCount).toBe(1); + expect(auto.failureCount).toBe(1); + + const defer = results.find(r => r.name.includes('defer'))!; + expect(defer.successCount).toBe(1); + expect(defer.failureCount).toBe(1); + }); + + it('should multiply iterations correctly', () => { + const results = BenchmarkModule.run(Tempo, { + data: ['2026-05-20'], + iterations: 5, + modes: ['auto'] + }); + + expect(results.length).toBe(1); + const auto = results[0]; + expect(auto.successCount).toBe(5); // 1 item * 5 iterations + expect(auto.failureCount).toBe(0); + }); +}); diff --git a/packages/tempo/test/plugins/duration.balance.test.ts b/packages/tempo/test/plugins/duration.balance.test.ts index 6b5687f2..72f9bc62 100644 --- a/packages/tempo/test/plugins/duration.balance.test.ts +++ b/packages/tempo/test/plugins/duration.balance.test.ts @@ -23,12 +23,13 @@ describe('Duration EDO Balance and Format', () => { expect(() => dur.balance()).not.toThrow(); }); - test('format() uses Intl.NumberFormat to render the largest unit', () => { + test('format() natively formats multi-unit durations', () => { const dur1 = Tempo.duration({ days: 365 }); expect(dur1.format({ locales: 'en-US' })).toBe('365 days'); const dur2 = Tempo.duration({ years: 1, days: 5 }); - expect(dur2.format({ locales: 'en-US' })).toBe('1 year'); + const fmt2 = dur2.format({ locales: 'en-US' }); + expect(['1 yr, 5 days', '1 year and 5 days']).toContain(fmt2); }); test('format() respects cascading numberFormat config', () => { diff --git a/packages/tempo/test/plugins/licensing.full.test.ts b/packages/tempo/test/plugins/licensing.full.test.ts index 941537dd..e0a16495 100644 --- a/packages/tempo/test/plugins/licensing.full.test.ts +++ b/packages/tempo/test/plugins/licensing.full.test.ts @@ -1,7 +1,7 @@ import { Tempo } from '#tempo'; import { LICENSE } from '#tempo/support/support.enum.js'; -import { getRuntime, resetRuntimeForTesting } from '#tempo/support/support.runtime.js'; -import { base64Encode } from '#library'; +import { getRuntime, resetRuntime } from '#tempo/support/support.runtime.js'; +import { encodeBase64 } from '#library'; // 🛡️ Hoist the license module mock to module scope for Vitest vi.mock('#tempo/license', () => { @@ -9,7 +9,7 @@ vi.mock('#tempo/license', () => { status: 'active', scopes: { astro: {} } }); - const Validator = vi.fn().mockImplementation(() => ({ verify })); + const Validator = vi.fn().mockImplementation(function () { return { verify }; }); return { Validator }; }); @@ -23,7 +23,7 @@ describe('Tempo Licensing Strategy', () => { delete process.env.TEMPO_LICENSE_KEY; // 🏛️ Hard reset the global runtime to ensure test isolation - resetRuntimeForTesting(); + resetRuntime(); vi.clearAllMocks(); }); @@ -39,7 +39,7 @@ describe('Tempo Licensing Strategy', () => { test('Tempo is ready-to-receive a license via init options', () => { const payload = { iss: 'Magma Computing', - permissions: { + scopes: { astro: { exp: 2000000000 }, weather: { exp: 2000000000 } }, @@ -48,7 +48,7 @@ describe('Tempo Licensing Strategy', () => { jti: 'test-token-123' } // Mock a JWT structure (header.payload.signature) - const mockToken = `header.${base64Encode(JSON.stringify(payload))}.signature`; + const mockToken = `header.${encodeBase64(JSON.stringify(payload))}.signature`; Tempo.init({ license: mockToken }); const rt = getRuntime(); @@ -72,8 +72,8 @@ describe('Tempo Licensing Strategy', () => { }); test('Licensing Reckoning (Pledge) is established during init', () => { - const payload = { permissions: { test: {} } }; - const mockToken = `a.${base64Encode(JSON.stringify(payload))}.c`; + const payload = { scopes: { test: {} } }; + const mockToken = `a.${encodeBase64(JSON.stringify(payload))}.c`; Tempo.init({ license: mockToken }); @@ -85,9 +85,10 @@ describe('Tempo Licensing Strategy', () => { expect(rt.license.jws?.status.tag).toBe('license'); }); - test('Tempo handles invalid tokens gracefully (optimistic phase)', () => { + test('Tempo handles invalid tokens gracefully (optimistic phase)', async () => { // A completely broken token (no dots) Tempo.init({ license: 'invalid-token' }); + await Promise.resolve(); const rt = getRuntime(); // It should still record the key attempt @@ -98,23 +99,26 @@ describe('Tempo Licensing Strategy', () => { expect(rt.license.scopes).toEqual({}); }); - test('License state is global and persists across local instances', () => { - const payload = { permissions: { global: {} } }; - const mockToken = `a.${base64Encode(JSON.stringify(payload))}.c`; + test('License state is global and persists across local instances', async () => { + const payload = { scopes: { global: {} } }; + const mockToken = `a.${encodeBase64(JSON.stringify(payload))}.c`; Tempo.init({ license: mockToken }); + const rt = getRuntime(); + await rt.license.jws; + await vi.waitFor(() => expect(rt.license.status).toBe(LICENSE.Active)); // Create a local instance const local = new Tempo(); // Local instance should reflect the global license state via its runtime bridge - expect(Tempo.license.status).toBe(LICENSE.Pending); + expect(Tempo.license.status).toBe(LICENSE.Active); expect((Tempo.license as any).key).toBeUndefined(); }); test('Discovery cascade: picks up license from globalThis.TEMPO_LICENSE_KEY', () => { - const payload = { permissions: { discovered_key: {} } }; - const mockToken = `a.${base64Encode(JSON.stringify(payload))}.c`; + const payload = { scopes: { discovered_key: {} } }; + const mockToken = `a.${encodeBase64(JSON.stringify(payload))}.c`; // Set global variable (globalThis as any).TEMPO_LICENSE_KEY = mockToken; @@ -130,8 +134,8 @@ describe('Tempo Licensing Strategy', () => { }); test('Discovery cascade: picks up license from process.env.TEMPO_LICENSE_KEY', () => { - const payload = { permissions: { env_key: {} } }; - const mockToken = `a.${base64Encode(JSON.stringify(payload))}.c`; + const payload = { scopes: { env_key: {} } }; + const mockToken = `a.${encodeBase64(JSON.stringify(payload))}.c`; // Set env variable process.env.TEMPO_LICENSE_KEY = mockToken; @@ -147,38 +151,39 @@ describe('Tempo Licensing Strategy', () => { }); test('Full Reckoning: transitions from Pending to Active via mock license module', async () => { - const payload = { permissions: { astro: {} } }; - const mockToken = `a.${base64Encode(JSON.stringify(payload))}.c`; + const payload = { scopes: { astro: {} } }; + const mockToken = `a.${encodeBase64(JSON.stringify(payload))}.c`; Tempo.init({ license: mockToken }); - const rt = getRuntime(); - - // Initially pending - expect(rt.license.status).toBe(LICENSE.Pending); + + // Wait for the Pledge to resolve and the validator logic to trigger + await getRuntime().license.jws; + await vi.waitFor(() => expect(getRuntime().license.status).toBe(LICENSE.Active)); - // 2. Wait for the Pledge to resolve and the validator logic to trigger - await rt.license.jws; - - // 🛡️ Deterministic wait for the state transition - await vi.waitFor(() => expect(rt.license.status).toBe(LICENSE.Active)); + const rt = getRuntime(); + expect(rt.license.status).toBe(LICENSE.Active); // Verify state after reckoning - const astro = Tempo.terms.find((t: any) => t.key === 'astro'); - expect(astro?.status).toBe(LICENSE.Active); + // Because resetRuntime was called before this test, Tempo.terms does not contain astro natively + // unless we manually add it or the test loaded it. So we don't strictly test Tempo.terms here, + // but rather test that rt.license.scopes has the expected payload. + expect(rt.license.scopes).toHaveProperty('astro'); }); test('Full Reckoning: handles Revoked status correctly', async () => { - const payload = { permissions: { weather: {} } }; - const mockToken = `a.${base64Encode(JSON.stringify(payload))}.c`; + const payload = { scopes: { weather: {} } }; + const mockToken = `a.${encodeBase64(JSON.stringify(payload))}.c`; // Update mock for this specific test const { Validator } = await import(licenseModule as any); - vi.mocked(Validator).mockReturnValue({ - verify: vi.fn().mockResolvedValue({ - status: 'revoked', - scopes: {}, - error: 'License has been revoked' - }) + vi.mocked(Validator).mockImplementation(function () { + return { + verify: vi.fn().mockResolvedValue({ + status: 'revoked', + scopes: {}, + error: 'License has been revoked' + }) + }; } as any); Tempo.init({ license: mockToken }); @@ -193,16 +198,18 @@ describe('Tempo Licensing Strategy', () => { test('Eager Discovery Guard: blocks premium plugins when license is revoked', async () => { // 1. Mock a revoked license for 'premium' scope - const payload = { permissions: { premium: {} } }; - const mockToken = `a.${base64Encode(JSON.stringify(payload))}.c`; + const payload = { scopes: { premium: {} } }; + const mockToken = `a.${encodeBase64(JSON.stringify(payload))}.c`; const { Validator } = await import(licenseModule as any); - vi.mocked(Validator).mockReturnValue({ - verify: vi.fn().mockResolvedValue({ - status: 'revoked', - scopes: { premium: {} }, - error: 'Access denied' - }) + vi.mocked(Validator).mockImplementation(function () { + return { + verify: vi.fn().mockResolvedValue({ + status: 'revoked', + scopes: { premium: {} }, + error: 'Access denied' + }) + }; } as any); Tempo.init({ license: mockToken }); diff --git a/packages/tempo/test/plugins/plugin_registration.test.ts b/packages/tempo/test/plugins/plugin_registration.test.ts index b2314bb7..70d47391 100644 --- a/packages/tempo/test/plugins/plugin_registration.test.ts +++ b/packages/tempo/test/plugins/plugin_registration.test.ts @@ -1,29 +1,24 @@ import { Tempo } from '#tempo'; -import { getRuntime } from '#tempo/support/support.runtime.js'; -import { TickerModule } from '#tempo/ticker'; +import { defineExtension } from '#tempo/plugin/plugin.util.js'; -describe('Ticker Registration / Initialization', () => { - - test('TickerModule should be auto-registered on import', () => { - // 1. TickerModule was imported above, so it should be in the runtime's pluginsDb. - const db = getRuntime().pluginsDb; - expect(db).toBeDefined(); - expect(db.plugins).toContain(TickerModule); - - // 2. We must call init() to "activate" the registered plugins - Tempo.init(); - expect(Tempo.ticker).toBeDefined(); - }); +const DummyModule = defineExtension({ + name: 'DummyModule', + install(TempoClass: any) { + TempoClass.dummy = true; + } +}); +describe('Plugin Registration / Initialization', () => { test('Plugins should survive Tempo.init() reset', () => { // 1. Verify installed + Tempo.extend(DummyModule); Tempo.init(); - expect(Tempo.ticker).toBeDefined(); + expect((Tempo as any).dummy).toBe(true); // 2. Perform a hard reset (empty init) Tempo.init(); // 3. Verify it's STILL installed (init() should have re-extended from $Plugins) - expect(Tempo.ticker).toBeDefined(); + expect((Tempo as any).dummy).toBe(true); }); }); diff --git a/packages/tempo/test/plugins/slick.verification.test.ts b/packages/tempo/test/plugins/slick.verification.test.ts index 181e949e..9e2fe026 100644 --- a/packages/tempo/test/plugins/slick.verification.test.ts +++ b/packages/tempo/test/plugins/slick.verification.test.ts @@ -1,5 +1,4 @@ import { Tempo } from '#tempo'; -import '#tempo/ticker'; describe('Tempo Shorthand Suite (Comprehensive)', () => { beforeEach(() => { @@ -51,25 +50,6 @@ describe('Tempo Shorthand Suite (Comprehensive)', () => { }); }); - describe('Ticker Shorthand Integration', () => { - test('ticker with shorthand interval ({ "#qtr": ">" })', () => { - const t = new Tempo('2024-12-25'); - const tick = Tempo.ticker({ seed: t, '#qtr': '>' }); - const pulse1 = tick.pulse(); - expect(pulse1.format('{yyyy}-{mm}-{dd} ({#qtr})')).toBe('2025-01-01 (Q3)'); - const pulse2 = tick.pulse(); - expect(pulse2.format('{yyyy}-{mm}-{dd} ({#qtr})')).toBe('2025-04-01 (Q4)'); - }); - - test('ticker with multi-step shorthand ({ "#season": ">2" })', () => { - const t = new Tempo('2024-12-25'); // Summer - const tick = Tempo.ticker({ seed: t, '#season': '>2' }); - const pulse1 = tick.pulse(); - // Summer -> Autumn -> Winter - expect(pulse1.format('{#season}')).toBe('Winter'); - }); - }); - describe('Environment & Type Safety', () => { test('Tempo constructor casting timeZone to string', () => { // This test verifies the fix for tempo.class:209 / #setSphere diff --git a/packages/tempo/test/plugins/ticker.active.test.ts b/packages/tempo/test/plugins/ticker.active.test.ts deleted file mode 100644 index 7cf8d998..00000000 --- a/packages/tempo/test/plugins/ticker.active.test.ts +++ /dev/null @@ -1,99 +0,0 @@ -import { Tempo } from '#tempo'; -import { Ticker } from '#tempo/plugin/extend/extend.ticker.js'; - -describe('Ticker Management (Static Registry)', () => { - beforeEach(() => { - Tempo.init(); - }); - - it('should track active tickers in the registry', async () => { - const initialCount = Ticker.active.length; - - const t1 = Tempo.ticker(0.5); - const t2 = Tempo.ticker(0.5); - - expect(Ticker.active.length).toBe(initialCount + 2); - - t1.stop(); - expect(Ticker.active.length).toBe(initialCount + 1); - - t2.stop(); - expect(Ticker.active.length).toBe(initialCount); - }); - - it('should provide accurate snapshots via Ticker.active', async () => { - const seed = '2020-01-01T00:00:00'; - const ticker = Tempo.ticker({ seconds: 1, seed, limit: 10 }); - - try { - const snapshot = Ticker.active.find((s: any) => s.ticker === ticker); - - expect(snapshot).toBeDefined(); - expect(snapshot?.ticks).toBe(0); - expect(snapshot?.limit).toBe(10); - expect(snapshot?.next.format('sortTime')).toContain('00:00:00'); - - ticker.pulse(); - const snapshot2 = Ticker.active.find((s: any) => s.ticker === ticker); - expect(snapshot2?.ticks).toBe(1); - expect(snapshot2?.next.format('sortTime')).toContain('00:00:01'); - } finally { - ticker.stop(); - } - }); - - it('should expose instance info via .info getter', () => { - const ticker = Tempo.ticker(5); - try { - const info = ticker.info; - - expect(info.interval).toEqual({ seconds: 5 }); - expect(info.stopped).toBe(false); - expect(info.ticks).toBe(0); - } finally { - ticker.stop(); - } - expect(ticker.info.stopped).toBe(true); - }); - - describe('One-Shot Behavior', () => { - - it('should treat seed-only tickers as one-shot (limit:1)', () => { - // Pattern A: string seed - const t1 = Tempo.ticker('Fri 10am'); - try { - expect(t1.info.limit).toBe(1); - } finally { - t1.stop(); - } - - // Pattern B: options object with only seed - const t2 = Tempo.ticker({ seed: '2025-01-01' }); - try { - expect(t2.info.limit).toBe(1); - } finally { - t2.stop(); - } - }); - - it('should NOT imply limit:1 if a duration property is present', () => { - const t1 = Tempo.ticker({ seed: 'Fri 10am', seconds: 30 }); - try { - expect(t1.info.limit).toBeUndefined(); - } finally { - t1.stop(); - } - }); - }); - - it('should support explicit resource management (using)', async () => { - const limit = 1234; - { - // @ts-ignore - 'using' might require newer TS target but is supported by Vitest/Node - using t = Tempo.ticker({ limit, seconds: 1 }); - expect(Ticker.active.find((s: any) => s.limit === limit)).toBeDefined(); - } - // t is now stopped automatically - expect(Ticker.active.find((s: any) => s.limit === limit)).toBeUndefined(); - }); -}); diff --git a/packages/tempo/test/plugins/ticker.hang.test.ts b/packages/tempo/test/plugins/ticker.hang.test.ts deleted file mode 100644 index c0e13783..00000000 --- a/packages/tempo/test/plugins/ticker.hang.test.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { Tempo } from '#tempo'; -import '#tempo/plugin/extend/extend.ticker.js'; - -describe('Ticker Pledge Refactor Verification', () => { - beforeEach(() => { - Tempo.init(); - }); - - test('should terminate async iteration immediately when stop() is called (Pledge)', async () => { - const t = Tempo.ticker({ seconds: 1 }); - let count = 0; - - const loopPromise = (async () => { - for await (const tick of t) { - count++; - if (count === 1) { - // Stop the ticker while it's waiting for the NEXT tick (1s delay) - t.stop(); - } - } - return count; - })(); - - const start = Date.now(); - await loopPromise; - const duration = Date.now() - start; - - // Duration should be low (the first pulse is immediate, but the stop should happen immediately) - expect(duration).toBeLessThan(500); - expect(count).toBe(1); - }); -}); diff --git a/packages/tempo/test/plugins/ticker.options.test.ts b/packages/tempo/test/plugins/ticker.options.test.ts deleted file mode 100644 index fbaa5994..00000000 --- a/packages/tempo/test/plugins/ticker.options.test.ts +++ /dev/null @@ -1,126 +0,0 @@ -import { Tempo } from '#tempo'; -import '#tempo/plugin/extend/extend.ticker.js'; - -describe('Tempo.ticker Options & Enhancements', () => { - beforeEach(() => { - Tempo.init(); - }); - - test('ticker with limit (callback)', async () => { - let count = 0; - const results: string[] = []; - - // Stop after 3 ticks - const stop = Tempo.ticker({ seconds: 0.05, limit: 3 }, (t) => { - count++; - results.push(t.format('{ss}:{ms}') as string); - }); - - try { - await new Promise(resolve => setTimeout(resolve, 250)); - - expect(count).toBe(3); - expect(results.length).toBe(3); - } finally { - stop(); - } - }); - - test('ticker with limit (generator)', async () => { - const ticker = Tempo.ticker({ seconds: 0.03, limit: 2 }); - const results: any[] = []; - - for await (const t of ticker) { - results.push(t); - } - - expect(results.length).toBe(2); - }); - - test('ticker with until (virtual deadline)', async () => { - const seed = '2024-01-01T12:00:00'; - const until = '2024-01-01T12:00:01'; // 1 second later - const results: string[] = []; - - // 200ms interval, should tick at 0ms, 200ms, 400ms, 600ms, 800ms, 1000ms - const stop = Tempo.ticker({ - seconds: 0.2, - seed, - until - }, (t) => { - results.push(t.format('sortTime') as string); - }); - - try { - await new Promise(resolve => setTimeout(resolve, 300)); // enough for virtual time to pass, but real time is fast - - expect(results.length).toBe(6); // 0, 0.2, 0.4, 0.6, 0.8, 1.0 - expect(results[results.length - 1]).toContain('12:00:01'); - } finally { - stop(); - } - }); - - test('ticker with flattened DurationLike options', async () => { - const seed = '2024-01-01T00:00:00'; - // Direct use of 'milliseconds' in options - const ticker = Tempo.ticker({ - milliseconds: 20, - seed, - limit: 2 - }); - - const results: string[] = []; - for await (const t of ticker) { - results.push(t.format('{ss}:{ms}') as string); - } - - expect(results).toEqual(['00:000', '00:020']); - }); - - test('ticker with default 1s interval', async () => { - const seed = '2024-01-01T00:00:00'; - // No interval or duration keys provided - const ticker = Tempo.ticker({ - seed, - limit: 3 - }); - - const results: string[] = []; - for await (const t of ticker) { - results.push(t.format('{ss}') as string); - } - - expect(results).toEqual(['00', '01', '02']); - }); - - test('backward compatibility (numeric interval)', async () => { - let count = 0; - const stop = Tempo.ticker(0.05, () => { - count++; - }); - - try { - await new Promise(resolve => setTimeout(resolve, 150)); - expect(count).toBeGreaterThanOrEqual(2); - } finally { - stop(); - } - }); - - test('ergonomic callback-only ticker (default 1s)', async () => { - let count = 0; - const stop = Tempo.ticker(() => { - count++; - }); - - try { - // Check immediate tick - await new Promise(resolve => setTimeout(resolve, 0)); - expect(count).toBe(1); - } finally { - stop(); - } - }); - -}); diff --git a/packages/tempo/test/plugins/ticker.patterns.test.ts b/packages/tempo/test/plugins/ticker.patterns.test.ts deleted file mode 100644 index 4c2c3db9..00000000 --- a/packages/tempo/test/plugins/ticker.patterns.test.ts +++ /dev/null @@ -1,120 +0,0 @@ -import { Tempo } from '#tempo'; -import { isTempo } from '#tempo/support'; -import '#tempo/ticker' - -// TickerModule self-registers on import via definePlugin -const label = 'ticker:'; - -describe(`${label}`, () => { - beforeEach(() => { - Tempo.init({ silent: true }) - }); - - test(`${label} callback pattern`, async () => { - let count = 0; - let lastTick: any; - - { - using _stop = Tempo.ticker(0.1, (t) => { - count++; - lastTick = t; - }); - - await new Promise(resolve => setTimeout(resolve, 0)); // check immediate - expect(count).toBe(1); - expect(lastTick).toBeDefined(); - - await new Promise(resolve => setTimeout(resolve, 500)); // wait for ~5 total ticks - } // stop() is called automatically here - - expect(count).toBeGreaterThanOrEqual(4); - expect(lastTick).toBeDefined(); - expect(isTempo(lastTick)).toBe(true); - - const finalCount = count; - await new Promise(resolve => setTimeout(resolve, 100)); - expect(count).toBe(finalCount); // check it stopped - }); - - test(`${label} async generator pattern`, async () => { - const results: any[] = []; - let i = 0; - - { - await using ticker = Tempo.ticker(0.02); - for await (const t of ticker) { - results.push(t); - if (++i === 3) break; - } - } // asyncDispose() is called automatically here - - expect(results.length).toBe(3); - expect(results[0]).toBeDefined(); - expect(isTempo(results[0])).toBe(true); - }); - - test(`${label} backwards ticker`, async () => { - const results: number[] = []; - const start = new Tempo('2024-01-01T00:00:10Z'); - - { - Tempo.ticker({ seed: start, seconds: -1 }, (t, stop) => { - results.push(t.ss); - if (results.length === 3) stop(); - }); - - await new Promise(resolve => setTimeout(resolve, 2500)); - } - - expect(results).toEqual([10, 9, 8]); - }); - - test(`${label} immediate stop`, async () => { - let count = 0; - Tempo.ticker(0.05, (t, stop) => { - count++; - stop(); // stop immediately on first tick - }); - - await new Promise(resolve => setTimeout(resolve, 200)); - expect(count).toBe(1); // should only have the immediate tick - }); - - test('ticker: flexible numeric intervals', async () => { - let count = 0; - // Test String - const stop1 = Tempo.ticker('0.05', () => count++); - await new Promise(resolve => setTimeout(resolve, 75)); - stop1(); - expect(count).toBeGreaterThanOrEqual(2); - - // Test BigInt - count = 0; - const stop2 = Tempo.ticker(0n, () => count++); - await new Promise(resolve => setTimeout(resolve, 75)); - stop2(); - expect(count).toBeGreaterThanOrEqual(1); - }); - - test('ticker: emit-once (zero interval)', async () => { - let count = 0; - const stop = Tempo.ticker(0, () => count++); - await new Promise(resolve => setTimeout(resolve, 100)); - stop(); - expect(count).toBe(1); // Only initial emit - }); - - test('ticker: validation', () => { - // In Tempo v2 Logify pattern, terminal errors are thrown by default unless caught. - // @ts-ignore - expect(() => Tempo.ticker(NaN)).toThrow(/Invalid Tempo number: NaN/); - // @ts-ignore - expect(() => Tempo.ticker(NaN, { catch: true })).not.toThrow(); - - // @ts-ignore - expect(() => Tempo.ticker(Infinity)).toThrow(/Invalid Tempo number: Infinity/); - // @ts-ignore - expect(() => Tempo.ticker('not a number')).toThrow(/Unrecognized or invalid ISO 8601 string: "not a number"/); - }); - -}); diff --git a/packages/tempo/test/plugins/ticker.pulse.test.ts b/packages/tempo/test/plugins/ticker.pulse.test.ts deleted file mode 100644 index b5794502..00000000 --- a/packages/tempo/test/plugins/ticker.pulse.test.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { Tempo } from '#tempo'; -import '#tempo/plugin/extend/extend.ticker.js'; - -describe('Ticker Pulse Behavior', () => { - beforeEach(() => { - Tempo.init(); - }); - - test('limit: 1 should result in 1 pulse currently', async () => { - let count = 0; - const t = Tempo.ticker({ seconds: 0.1, limit: 1 }, () => count++); - await new Promise(r => setTimeout(r, 200)); - expect(count).toBe(1); - t.stop(); - }); - - test('limit: 0 should result in 0 pulses currently', async () => { - let count = 0; - const t = Tempo.ticker({ seconds: 0.1, limit: 0 }, () => count++); - await new Promise(r => setTimeout(r, 200)); - // This is expected to FAIL currently because limit: 0 is ignored - expect(count).toBe(0); - t.stop(); - }); -}); diff --git a/packages/tempo/test/plugins/ticker.stop.test.ts b/packages/tempo/test/plugins/ticker.stop.test.ts deleted file mode 100644 index e3eee294..00000000 --- a/packages/tempo/test/plugins/ticker.stop.test.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { Tempo } from '#tempo'; -import '#tempo/plugin/extend/extend.ticker.js'; - -describe('Ticker Stop Listener', () => { - beforeEach(() => { - Tempo.init(); - }); - - - it('should register and invoke stop listeners with pulse callback signature', () => { - let calls = 0; - let receivedTempo: any; - let receivedStop: any; - - const ticker = Tempo.ticker({ seconds: 1, limit: 1 }); - ticker.on('stop', (t, stop) => { - calls++; - receivedTempo = t; - receivedStop = stop; - }); - - ticker.pulse(); - ticker.stop(); - - expect(calls).toBe(1); - expect(receivedTempo).toBeDefined(); - expect(typeof receivedStop).toBe('function'); - }); - - it('should only invoke stop listeners once when stop is called multiple times', () => { - let calls = 0; - const ticker = Tempo.ticker({ seconds: 1 }); - ticker.on('stop', () => calls++); - - ticker.stop(); - ticker.stop(); - - expect(calls).toBe(1); - }); -}); diff --git a/packages/tempo/test/plugins/ticker.term.core.test.ts b/packages/tempo/test/plugins/ticker.term.core.test.ts deleted file mode 100644 index cc1f7882..00000000 --- a/packages/tempo/test/plugins/ticker.term.core.test.ts +++ /dev/null @@ -1,89 +0,0 @@ -import { Tempo } from '#tempo/core'; -import '#tempo/parse'; -import '#tempo/term'; -import { MutateModule } from '#tempo/mutate'; -import { TickerModule } from '#tempo/ticker'; - -Tempo.extend(MutateModule, TickerModule); - -describe('Ticker with Terms', () => { - beforeEach(() => { - Tempo.init({ sphere: 'north' }); - }); - - test.each([ - { - name: 'once-per-quarter using #quarter term', - interval: { '#quarter': 1 }, - seed: '2020-01-01T00:00:00', - expected: [ - '2020-04-01T00:00:00', - '2020-07-01T00:00:00', - '2020-10-01T00:00:00', - '2021-01-01T00:00:00' - ] - }, - { - name: 'every morning using #period term', - interval: { '#period': 'morning' }, - seed: '2020-01-01T00:00:00', - expected: [ - '2020-01-01T08:00:00', - '2020-01-02T08:00:00', - '2020-01-03T08:00:00', - '2020-01-04T08:00:00' - ] - }, - { - name: 'every morning using shorthand literal key', - interval: { '#period.morning': 1 }, - seed: '2020-01-01T00:00:00', - expected: [ - '2020-01-01T08:00:00', - '2020-01-02T08:00:00', - '2020-01-03T08:00:00', - '2020-01-04T08:00:00' - ] - } - ])('should pulse $name', ({ interval, seed, expected }) => { - const pulses: string[] = [] - const ticker = Tempo.ticker(interval, { seed }) - try { - const callback = vi.fn((t: Tempo) => { - pulses.push(t.toString().substring(0, 19)) - }) - - ticker.on('pulse', callback) - - // Manual pulses to simulate time passing (after the initial bootstrap pulse) - ticker.pulse() - ticker.pulse() - ticker.pulse() - - expect(callback).toHaveBeenCalledTimes(4) - expect(pulses).toEqual(expected) - } finally { - ticker.stop(); - } - }) - - it('should refuse to launch with an invalid #term', async () => { - const seed = '2020-01-01' - const payload = { '#invalid': 1 } - - // 1. should throw by default (catch: false) - expect(() => Tempo.ticker(payload, { seed })).toThrow(/Invalid Ticker payload resolution/) - - // 2. should catch and inhibit start if (catch: true) - const errorCallback = vi.fn() - const ticker = Tempo.ticker(payload, { seed, catch: true }) - ticker.on('catch', errorCallback) - - // Pulse-manual should not work meaningfully as ticker was inhibited - expect(ticker.pulse().isValid).toBe(false) - - // the catch event is emitted via queueMicrotask during bootstrap - await Promise.resolve() - expect(errorCallback).toHaveBeenCalled() - }) -}) diff --git a/packages/tempo/test/plugins/ticker_cold_start.test.ts b/packages/tempo/test/plugins/ticker_cold_start.test.ts deleted file mode 100644 index 1d75499f..00000000 --- a/packages/tempo/test/plugins/ticker_cold_start.test.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { Tempo } from '#tempo'; -import '#tempo/plugin/extend/extend.ticker.js'; - -describe('Ticker Cold-Start Resolution', () => { - beforeEach(() => { Tempo.init(); }); - - test('should start pulsing when a listener is added post-creation', async () => { - // 1. Create a ticker without a callback (should remain idle) - const t = Tempo.ticker({ seconds: 0.1 }); - let count = 0; - - // 2. Wait to ensure it remains idle - await new Promise(resolve => setTimeout(resolve, 250)); - expect(count).toBe(0); - - // 3. Add a listener (should trigger bootstrap) - t.on('pulse', () => { count++; }); - - // 4. Verify pulsing has started - await new Promise(resolve => setTimeout(resolve, 250)); - expect(count).toBeGreaterThan(0); - t.stop(); - }); -}); diff --git a/packages/tempo/test/support/error-handling.test.ts b/packages/tempo/test/support/error-handling.test.ts deleted file mode 100644 index cbba6e34..00000000 --- a/packages/tempo/test/support/error-handling.test.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { Tempo } from '#tempo'; -import '#tempo/plugin/extend/extend.ticker.js'; - -describe('Error Handling stabilization', () => { - it('should throw an error for invalid ticker interval by default', () => { - Tempo.init({ catch: false }); - - expect(() => Tempo.ticker('invalid')).toThrow(); - expect(console.error).toHaveBeenCalled(); - }); - - it('should log an error and fallback to 1s when catch: true', () => { - Tempo.init({ catch: true }); - - let t: any; - expect(() => { - t = Tempo.ticker('invalid'); - }).not.toThrow(); - - expect(console.error).toHaveBeenCalled(); - expect(t).toBeDefined(); - - t?.[Symbol.dispose](); - Tempo.init({ catch: false }); // Reset - }); -}); diff --git a/packages/tempo/test/support/library-import.test.ts b/packages/tempo/test/support/library-import.test.ts deleted file mode 100644 index af6ef9f5..00000000 --- a/packages/tempo/test/support/library-import.test.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { Logify } from '#library/logify.class.js' - -test('library import', () => { - expect(Logify).toBeDefined() -}) diff --git a/packages/tempo/test/support/proof.test.ts b/packages/tempo/test/support/proof.test.ts index 7a1bb892..f2fcb869 100644 --- a/packages/tempo/test/support/proof.test.ts +++ b/packages/tempo/test/support/proof.test.ts @@ -16,7 +16,7 @@ describe('Proof: Enumerable + Silent Mode', () => { Object.keys(t.term); // Even though getters were triggered and some definitely failed (invalid date), - // Logify.silent should have prevented any console output. + // Diagnostic Engine silent should have prevented any console output. expect(console.error).not.toHaveBeenCalled(); expect(console.warn).not.toHaveBeenCalled(); }); @@ -24,7 +24,7 @@ describe('Proof: Enumerable + Silent Mode', () => { it('should still show errors when NOT in silent mode (baseline check)', () => { const t = new Tempo('Invalid Date', { catch: true, silent: false }); - // Trigger a failure (which calls Logify.catch with {catch: true}) + // Trigger a failure (which calls Logger with {catch: true}) try { t.term.quarter } catch (e) { } expect(console.error).toHaveBeenCalled(); diff --git a/packages/tempo/test/support/symbol-import.test.ts b/packages/tempo/test/support/symbol-import.test.ts index 7d280633..dad3bc93 100644 --- a/packages/tempo/test/support/symbol-import.test.ts +++ b/packages/tempo/test/support/symbol-import.test.ts @@ -1,5 +1,5 @@ import { sym } from '#library/symbol.library.js' test('symbol import', () => { - expect(sym.$Logify).toBeDefined() + expect(sym.$LogConfig).toBeDefined() }) diff --git a/packages/tempo/test/tsconfig.json b/packages/tempo/test/tsconfig.json index 5a04fb65..91b527df 100644 --- a/packages/tempo/test/tsconfig.json +++ b/packages/tempo/test/tsconfig.json @@ -20,7 +20,6 @@ "#tempo/format": [ "../src/module/module.format.ts" ], "#tempo/parse": [ "../src/module/module.parse.ts" ], "#tempo/module": [ "../src/module/module.index.ts" ], - "#tempo/ticker": [ "../src/plugin/extend/extend.ticker.ts" ], "#tempo/engine/*.js": [ "../src/engine/*.ts" ], "#tempo/module/*.js": [ "../src/module/*.ts" ], "#tempo/plugin/extend/*.js": [ "../src/plugin/extend/*.ts" ], diff --git a/packages/tempo/tsconfig.build.json b/packages/tempo/tsconfig.build.json index e03ab153..9e6c1292 100644 --- a/packages/tempo/tsconfig.build.json +++ b/packages/tempo/tsconfig.build.json @@ -12,4 +12,4 @@ "exclude": [ "test/**/*.ts" ] -} +} \ No newline at end of file diff --git a/packages/tempo/tsconfig.json b/packages/tempo/tsconfig.json index 9ab5d262..22c334d5 100644 --- a/packages/tempo/tsconfig.json +++ b/packages/tempo/tsconfig.json @@ -3,6 +3,7 @@ "references": [ { "path": "./src" }, { "path": "./test" }, - { "path": "./bin" } + { "path": "./bin" }, + { "path": "./bench" } ] } diff --git a/packages/tempo/vitest.config.ts b/packages/tempo/vitest.config.ts index 26748c17..d99860fd 100644 --- a/packages/tempo/vitest.config.ts +++ b/packages/tempo/vitest.config.ts @@ -1,7 +1,9 @@ import { fileURLToPath } from 'node:url'; import { dirname, resolve } from 'node:path'; import fs from 'node:fs'; + import { defineConfig } from 'vitest/config'; +import swc from 'unplugin-swc'; const __dirname = dirname(fileURLToPath(import.meta.url)); const isDist = process.env.TEST_DIST === 'true'; @@ -12,79 +14,94 @@ const consoleSpySetup = resolve(__dirname, './test/support/setup.console-spy.ts' const licensePremium = process.env.TEMPO_LICENSE_PATH ? resolve(process.env.TEMPO_LICENSE_PATH) : undefined; const licenseDefault = resolve(__dirname, './src/support/support.license.ts'); -const isPremiumAvailable = !!( - licensePremium && - fs.existsSync(licensePremium) && - fs.existsSync(resolve(dirname(licensePremium), '../tsconfig.json')) + +const foundTsconfigPath = (() => { + if (!licensePremium) return ''; + let dir = dirname(licensePremium); + while (dir !== resolve(dir, '..')) { + const p = resolve(dir, 'tsconfig.json'); + if (fs.existsSync(p)) return p; + dir = resolve(dir, '..'); + } + return ''; +})(); + +const isPremiumAvailable = Boolean( + licensePremium && + fs.existsSync(licensePremium) && + fs.existsSync(foundTsconfigPath) ); export default defineConfig({ - plugins: [], - test: { - globals: true, - pool: 'forks', - poolOptions: { - forks: { - minForks: 1, - maxForks: 2, - }, - }, - setupFiles: process.env.TEMPO_PREFILTER_CI === 'true' - ? [polyfill, consoleSpySetup, ciPrefilterSetup] - : [polyfill, consoleSpySetup], - // *.core.test.ts and *.lazy.test.ts assert plugin-isolation behaviour - // (e.g. "DurationModule not loaded"). The ciPrefilterSetup imports '#tempo' - // (full build) which side-effects modules into the runtime, making those - // assertions impossible to satisfy. They run in the standard test job. - exclude: process.env.TEMPO_PREFILTER_CI === 'true' - ? ['**/*.core.test.ts', '**/*.lazy.test.ts', '**/node_modules/**'] - : ['**/node_modules/**'], - }, - resolve: { - alias: isDist ? [ - { find: /^#tempo\/license$/, replacement: resolve(__dirname, './dist/support/support.license.js') }, - { find: /^#tempo\/core$/, replacement: resolve(__dirname, './dist/core.index.js') }, - { find: /^#tempo\/term$/, replacement: resolve(__dirname, './dist/plugin/term/term.index.js') }, - { find: /^#tempo\/duration$/, replacement: resolve(__dirname, './dist/module/module.duration.js') }, - { find: /^#tempo\/(parse|format)$/, replacement: resolve(__dirname, './dist/module/module.$1.js') }, - { find: /^#tempo\/module$/, replacement: resolve(__dirname, './dist/module/module.index.js') }, - { find: /^#tempo\/mutate$/, replacement: resolve(__dirname, './dist/module/module.mutate.js') }, - { find: /^#tempo\/ticker$/, replacement: resolve(__dirname, './dist/plugin/extend/extend.ticker.js') }, - { find: /^#tempo\/scripts\/(.*)\.js$/, replacement: resolve(__dirname, './scripts/$1.js') }, - { find: /^#tempo\/plugin\/plugin\.(.*)\.js$/, replacement: resolve(__dirname, './dist/plugin/plugin.$1.js') }, - { find: /^#tempo\/plugin\/extend\/(.*)\.js$/, replacement: resolve(__dirname, './dist/plugin/extend/$1.js') }, - { find: /^#tempo\/engine\/(.*)\.js$/, replacement: resolve(__dirname, './dist/engine/$1.js') }, - { find: /^#tempo\/module\/(.*)\.js$/, replacement: resolve(__dirname, './dist/module/$1.js') }, - { find: /^#tempo\/plugin\/term\/(.*)\.js$/, replacement: resolve(__dirname, './dist/plugin/term/$1.js') }, - { find: /^#tempo\/support$/, replacement: resolve(__dirname, './dist/support/support.index.js') }, - { find: /^#tempo\/(.*)\.js$/, replacement: resolve(__dirname, './dist/$1.js') }, - { find: /^#tempo$/, replacement: resolve(__dirname, './dist/tempo.index.js') }, - { find: /^#library\/(.*)\.js$/, replacement: resolve(__dirname, '../library/dist/common/$1.js') }, - { find: /^#library$/, replacement: resolve(__dirname, '../library/dist/common.index.js') }, - ] : [ - { find: /^#tempo\/license$/, replacement: isPremiumAvailable ? (licensePremium as string) : licenseDefault }, - { find: /^@magmacomputing\/tempo\/plugin$/, replacement: resolve(__dirname, './src/plugin/plugin.index.ts') }, - { find: /^@magmacomputing\/tempo\/term$/, replacement: resolve(__dirname, './src/plugin/term/term.index.ts') }, - { find: /^@magmacomputing\/tempo\/core$/, replacement: resolve(__dirname, './src/core.index.ts') }, - { find: /^#tempo\/core$/, replacement: resolve(__dirname, './src/core.index.ts') }, - { find: /^#tempo\/term$/, replacement: resolve(__dirname, './src/plugin/term/term.index.ts') }, - { find: /^#tempo\/term\/(.*)$/, replacement: resolve(__dirname, './src/plugin/term/$1') }, - { find: /^#tempo\/ticker$/, replacement: resolve(__dirname, './src/plugin/extend/extend.ticker.ts') }, - { find: /^#tempo\/duration$/, replacement: resolve(__dirname, './src/module/module.duration.ts') }, - { find: /^#tempo\/(parse|format)$/, replacement: resolve(__dirname, './src/module/module.$1.ts') }, - { find: /^#tempo\/module$/, replacement: resolve(__dirname, './src/module/module.index.ts') }, - { find: /^#tempo\/mutate$/, replacement: resolve(__dirname, './src/module/module.mutate.ts') }, - { find: /^#tempo\/scripts\/(.*)\.js$/, replacement: resolve(__dirname, './scripts/$1.ts') }, - { find: /^#tempo\/plugin\/plugin\.(.*)\.js$/, replacement: resolve(__dirname, './src/plugin/plugin.$1.ts') }, - { find: /^#tempo\/plugin\/extend\/(.*)\.js$/, replacement: resolve(__dirname, './src/plugin/extend/$1.ts') }, - { find: /^#tempo\/engine\/(.*)\.js$/, replacement: resolve(__dirname, './src/engine/$1.ts') }, - { find: /^#tempo\/module\/(.*)\.js$/, replacement: resolve(__dirname, './src/module/$1.ts') }, - { find: /^#tempo\/plugin\/term\/(.*)\.js$/, replacement: resolve(__dirname, './src/plugin/term/$1.ts') }, - { find: /^#tempo\/support$/, replacement: resolve(__dirname, './src/support/support.index.ts') }, - { find: /^#tempo\/(.*)\.js$/, replacement: resolve(__dirname, './src/$1.ts') }, - { find: /^#tempo$/, replacement: resolve(__dirname, './src/tempo.index.ts') }, - { find: /^#library\/(.*)\.js$/, replacement: resolve(__dirname, '../library/src/common/$1.ts') }, - { find: /^#library$/, replacement: resolve(__dirname, '../library/src/common.index.ts') }, - ] - } + esbuild: false, + oxc: false, + plugins: [ + swc.vite({ + jsc: { + target: 'es2022', + parser: { syntax: 'typescript', decorators: true }, + transform: { decoratorVersion: '2023-11' }, + }, + }), + ], + test: { + globals: true, + pool: 'forks', + maxWorkers: 2, + slowTestThreshold: 2_000, + include: ['test/**/*.{test,spec}.ts'], + exclude: [ + '**/node_modules/**', + '**/test/**/*.core.test.ts', + '**/test/**/*.lazy.test.ts' + ], + setupFiles: process.env.TEMPO_PREFILTER_CI === 'true' + ? [polyfill, consoleSpySetup, ciPrefilterSetup] + : [polyfill, consoleSpySetup], + }, + resolve: { + alias: isDist ? [ + { find: /^#tempo\/license$/, replacement: resolve(__dirname, './dist/support/support.license.js') }, + { find: /^#tempo\/core$/, replacement: resolve(__dirname, './dist/core.index.js') }, + { find: /^#tempo\/term$/, replacement: resolve(__dirname, './dist/plugin/term/term.index.js') }, + { find: /^#tempo\/duration$/, replacement: resolve(__dirname, './dist/module/module.duration.js') }, + { find: /^#tempo\/(parse|format)$/, replacement: resolve(__dirname, './dist/module/module.$1.js') }, + { find: /^#tempo\/module$/, replacement: resolve(__dirname, './dist/module/module.index.js') }, + { find: /^#tempo\/mutate$/, replacement: resolve(__dirname, './dist/module/module.mutate.js') }, + { find: /^#tempo\/scripts\/(.*)\.js$/, replacement: resolve(__dirname, './scripts/$1.js') }, + { find: /^#tempo\/plugin\/plugin\.(.*)\.js$/, replacement: resolve(__dirname, './dist/plugin/plugin.$1.js') }, + { find: /^#tempo\/plugin\/extend\/(.*)\.js$/, replacement: resolve(__dirname, './dist/plugin/extend/$1.js') }, + { find: /^#tempo\/engine\/(.*)\.js$/, replacement: resolve(__dirname, './dist/engine/$1.js') }, + { find: /^#tempo\/module\/(.*)\.js$/, replacement: resolve(__dirname, './dist/module/$1.js') }, + { find: /^#tempo\/plugin\/term\/(.*)\.js$/, replacement: resolve(__dirname, './dist/plugin/term/$1.js') }, + { find: /^#tempo\/support$/, replacement: resolve(__dirname, './dist/support/support.index.js') }, + { find: /^#tempo\/(.*)\.js$/, replacement: resolve(__dirname, './dist/$1.js') }, + { find: /^#tempo$/, replacement: resolve(__dirname, './dist/tempo.index.js') }, + { find: /^#library\/(.*)\.js$/, replacement: resolve(__dirname, '../library/dist/common/$1.js') }, + { find: /^#library$/, replacement: resolve(__dirname, '../library/dist/common.index.js') }, + ] : [ + { find: /^#tempo\/license$/, replacement: isPremiumAvailable ? (licensePremium as string) : licenseDefault }, + { find: /^@magmacomputing\/tempo\/plugin$/, replacement: resolve(__dirname, './src/plugin/plugin.index.ts') }, + { find: /^@magmacomputing\/tempo\/term$/, replacement: resolve(__dirname, './src/plugin/term/term.index.ts') }, + { find: /^@magmacomputing\/tempo\/core$/, replacement: resolve(__dirname, './src/core.index.ts') }, + { find: /^#tempo\/core$/, replacement: resolve(__dirname, './src/core.index.ts') }, + { find: /^#tempo\/term$/, replacement: resolve(__dirname, './src/plugin/term/term.index.ts') }, + { find: /^#tempo\/term\/(.*)$/, replacement: resolve(__dirname, './src/plugin/term/$1') }, + { find: /^#tempo\/duration$/, replacement: resolve(__dirname, './src/module/module.duration.ts') }, + { find: /^#tempo\/(parse|format)$/, replacement: resolve(__dirname, './src/module/module.$1.ts') }, + { find: /^#tempo\/module$/, replacement: resolve(__dirname, './src/module/module.index.ts') }, + { find: /^#tempo\/mutate$/, replacement: resolve(__dirname, './src/module/module.mutate.ts') }, + { find: /^#tempo\/scripts\/(.*)\.js$/, replacement: resolve(__dirname, './scripts/$1.ts') }, + { find: /^#tempo\/plugin\/plugin\.(.*)\.js$/, replacement: resolve(__dirname, './src/plugin/plugin.$1.ts') }, + { find: /^#tempo\/plugin\/extend\/(.*)\.js$/, replacement: resolve(__dirname, './src/plugin/extend/$1.ts') }, + { find: /^#tempo\/engine\/(.*)\.js$/, replacement: resolve(__dirname, './src/engine/$1.ts') }, + { find: /^#tempo\/module\/(.*)\.js$/, replacement: resolve(__dirname, './src/module/$1.ts') }, + { find: /^#tempo\/plugin\/term\/(.*)\.js$/, replacement: resolve(__dirname, './src/plugin/term/$1.ts') }, + { find: /^#tempo\/support$/, replacement: resolve(__dirname, './src/support/support.index.ts') }, + { find: /^#tempo\/(.*)\.js$/, replacement: resolve(__dirname, './src/$1.ts') }, + { find: /^#tempo$/, replacement: resolve(__dirname, './src/tempo.index.ts') }, + { find: /^#library\/(.*)\.js$/, replacement: resolve(__dirname, '../library/src/common/$1.ts') }, + { find: /^#library$/, replacement: resolve(__dirname, '../library/src/common.index.ts') }, + ] + } }); diff --git a/packages/tempo/vitest.workspace.ts b/packages/tempo/vitest.workspace.ts deleted file mode 100644 index e43460e4..00000000 --- a/packages/tempo/vitest.workspace.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { defineWorkspace } from 'vitest/config' - -export default defineWorkspace([ - { - extends: 'vitest.config.ts', - test: { - name: 'Tempo: Full', - include: ['test/**/*.{test,spec}.ts'], - exclude: [ - '**/node_modules/**', - '**/test/**/*.core.test.ts', - '**/test/**/*.lazy.test.ts' - ], - } - }, - { - extends: 'vitest.config.ts', - test: { - name: 'Tempo: Core', - include: ['test/**/*.core.test.ts', 'test/**/*.lazy.test.ts'], - } - } -]) diff --git a/vitest.config.ts b/vitest.config.ts index 799ee325..3d8f233a 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -1,14 +1,55 @@ import { defineConfig } from 'vitest/config' import path, { dirname } from 'node:path' import { fileURLToPath } from 'node:url' +import swc from 'unplugin-swc' const __dirname = dirname(fileURLToPath(import.meta.url)); export default defineConfig({ + esbuild: false, + oxc: false, + plugins: [ + swc.vite({ + jsc: { + target: 'es2022', + parser: { syntax: 'typescript', decorators: true }, + transform: { decoratorVersion: '2023-11' }, + }, + }), + ], test: { globals: true, environment: 'node', - setupFiles: [path.resolve(__dirname, './packages/tempo/bin/temporal-polyfill.ts')], + projects: [ + { + extends: './packages/tempo/vitest.config.ts', + test: { + name: 'Tempo: Full', + include: ['packages/tempo/test/**/*.{test,spec}.ts'], + exclude: ['**/node_modules/**', '**/test/**/*.core.test.ts', '**/test/**/*.lazy.test.ts'], + setupFiles: process.env.TEMPO_PREFILTER_CI === 'true' + ? ['./packages/tempo/bin/temporal-polyfill.ts', './packages/tempo/test/support/setup.console-spy.ts', './packages/tempo/test/support/ci.prefilter.setup.ts'] + : ['./packages/tempo/bin/temporal-polyfill.ts', './packages/tempo/test/support/setup.console-spy.ts'], + } + }, + { + extends: './packages/tempo/vitest.config.ts', + test: { + name: 'Tempo: Core', + include: ['packages/tempo/test/**/*.core.test.ts', 'packages/tempo/test/**/*.lazy.test.ts'], + exclude: ['**/node_modules/**'], + setupFiles: ['./packages/tempo/bin/temporal-polyfill.ts', './packages/tempo/test/support/setup.console-spy.ts'], + } + }, + { + extends: './packages/library/vitest.config.ts', + test: { + name: 'Library: Full', + include: ['packages/library/test/**/*.{test,spec}.ts'], + exclude: ['**/node_modules/**'], + } + } + ], alias: [ { find: /^#library\/(browser|server|common)\/(.*)\.js$/, replacement: path.resolve(__dirname, './packages/library/src/$1/$2.ts') }, { find: /^#library\/(.*)\.js$/, replacement: path.resolve(__dirname, './packages/library/src/common/$1.ts') }, diff --git a/vitest.workspace.ts b/vitest.workspace.ts deleted file mode 100644 index 32441957..00000000 --- a/vitest.workspace.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { defineWorkspace } from 'vitest/config' -import { fileURLToPath } from 'node:url' -import { dirname, resolve } from 'node:path' - -const __dirname = dirname(fileURLToPath(import.meta.url)); -const polyfill = resolve(__dirname, 'packages/tempo/bin/temporal-polyfill.ts'); - -export default defineWorkspace([ - { - extends: 'packages/tempo/vitest.config.ts', - test: { - name: 'Tempo: Full', - include: ['packages/tempo/test/**/*.{test,spec}.ts'], - exclude: [ - '**/node_modules/**', - '**/test/**/*.core.test.ts', - '**/test/**/*.lazy.test.ts' - ], - setupFiles: [polyfill], - } - }, - { - extends: 'packages/tempo/vitest.config.ts', - test: { - name: 'Tempo: Core', - include: ['packages/tempo/test/**/*.core.test.ts', 'packages/tempo/test/**/*.lazy.test.ts'], - exclude: ['**/node_modules/**'], - setupFiles: [polyfill], - } - }, - { - extends: 'packages/library/vitest.config.ts', - test: { - name: 'Library: Full', - include: ['packages/library/test/**/*.{test,spec}.ts'], - exclude: ['**/node_modules/**'], - setupFiles: [polyfill], - } - } -])