Initial Commit

This commit is contained in:
JB
2025-10-06 23:31:31 -04:00
commit 0c8630b8ba
243 changed files with 166945 additions and 0 deletions

View File

@@ -0,0 +1,26 @@
name: Release
permissions:
contents: write
on:
push:
tags:
- 'v*'
jobs:
release:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Set node
uses: actions/setup-node@v4
with:
node-version: lts/*
- run: npx changelogithub
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

View File

@@ -0,0 +1,38 @@
name: Unit Test
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
test:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Install pnpm
uses: pnpm/action-setup@v4.1.0
- name: Set node LTS
uses: actions/setup-node@v4
with:
node-version: lts/*
cache: pnpm
- name: Install
run: pnpm install
- name: Build
run: pnpm run build
- name: Lint
run: pnpm run lint
- name: Typecheck
run: pnpm run typecheck
- name: Test
run: pnpm run test

5
packages/util/.gitignore vendored Normal file
View File

@@ -0,0 +1,5 @@
node_modules
dist
*.log
.DS_Store
.env.keys

View File

@@ -0,0 +1,8 @@
trailingComma: all
tabWidth: 2
useTabs: false
semi: true
singleQuote: true
printWidth: 140
experimentalTernaries: true
quoteProps: consistent

3
packages/util/.vscode/settings.json vendored Normal file
View File

@@ -0,0 +1,3 @@
{
"editor.formatOnSave": true
}

23
packages/util/README.md Normal file
View File

@@ -0,0 +1,23 @@
# tsdown-starter
A starter for creating a TypeScript package.
## Development
- Install dependencies:
```bash
npm install
```
- Run the unit tests:
```bash
npm run test
```
- Build the library:
```bash
npm run build
```

9
packages/util/build.ts Normal file
View File

@@ -0,0 +1,9 @@
const bundle = await Bun.build({
entrypoints: ['./src/***.ts', '!./src/**/*.test.ts'],
outdir: 'dist',
minify: true,
});
if (!bundle.success) {
throw new AggregateError(bundle.logs);
}

464
packages/util/bun.lock Normal file
View File

@@ -0,0 +1,464 @@
{
"lockfileVersion": 1,
"workspaces": {
"": {
"name": "tsdown-starter",
"dependencies": {
"cron-parser": "^5.3.1",
"date-fns": "^4.1.0",
"node-cache": "^5.1.2",
"stream-chain": "^3.4.0",
"stream-json": "^1.9.1",
"winston": "^3.17.0",
},
"devDependencies": {
"@types/bun": "^1.2.21",
"@types/node": "^22.15.17",
"@types/node-cache": "^4.2.5",
"@types/stream-chain": "^2.1.0",
"@types/stream-json": "^1.7.8",
"bumpp": "^10.1.0",
"tsdown": "^0.14.2",
"typescript": "^5.9.2",
"vitest": "^3.1.3",
},
},
},
"packages": {
"@babel/generator": ["@babel/generator@7.28.3", "", { "dependencies": { "@babel/parser": "^7.28.3", "@babel/types": "^7.28.2", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-3lSpxGgvnmZznmBkCRnVREPUFJv2wrv9iAoFDvADJc0ypmdOxdUtcLeBgBJ6zE0PMeTKnxeQzyk0xTBq4Ep7zw=="],
"@babel/helper-string-parser": ["@babel/helper-string-parser@7.27.1", "", {}, "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA=="],
"@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.27.1", "", {}, "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow=="],
"@babel/parser": ["@babel/parser@7.28.4", "", { "dependencies": { "@babel/types": "^7.28.4" }, "bin": "./bin/babel-parser.js" }, "sha512-yZbBqeM6TkpP9du/I2pUZnJsRMGGvOuIrhjzC1AwHwW+6he4mni6Bp/m8ijn0iOuZuPI2BfkCoSRunpyjnrQKg=="],
"@babel/types": ["@babel/types@7.28.4", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.27.1" } }, "sha512-bkFqkLhh3pMBUQQkpVgWDWq/lqzc2678eUyDlTBhRqhCHFguYYGM0Efga7tYk4TogG/3x0EEl66/OQ+WGbWB/Q=="],
"@colors/colors": ["@colors/colors@1.6.0", "", {}, "sha512-Ir+AOibqzrIsL6ajt3Rz3LskB7OiMVHqltZmspbW/TJuTVuyOMirVqAkjfY6JISiLHgyNqicAC8AyHHGzNd/dA=="],
"@dabh/diagnostics": ["@dabh/diagnostics@2.0.3", "", { "dependencies": { "colorspace": "1.1.x", "enabled": "2.0.x", "kuler": "^2.0.0" } }, "sha512-hrlQOIi7hAfzsMqlGSFyVucrx38O+j6wiGOf//H2ecvIEqYN4ADBSS2iLMh5UFyDunCNniUIPk/q3riFv45xRA=="],
"@emnapi/core": ["@emnapi/core@1.5.0", "", { "dependencies": { "@emnapi/wasi-threads": "1.1.0", "tslib": "^2.4.0" } }, "sha512-sbP8GzB1WDzacS8fgNPpHlp6C9VZe+SJP3F90W9rLemaQj2PzIuTEl1qDOYQf58YIpyjViI24y9aPWCjEzY2cg=="],
"@emnapi/runtime": ["@emnapi/runtime@1.5.0", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-97/BJ3iXHww3djw6hYIfErCZFee7qCtrneuLa20UXFCOTCfBM2cvQHjWJ2EG0s0MtdNwInarqCTz35i4wWXHsQ=="],
"@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.1.0", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ=="],
"@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.9", "", { "os": "aix", "cpu": "ppc64" }, "sha512-OaGtL73Jck6pBKjNIe24BnFE6agGl+6KxDtTfHhy1HmhthfKouEcOhqpSL64K4/0WCtbKFLOdzD/44cJ4k9opA=="],
"@esbuild/android-arm": ["@esbuild/android-arm@0.25.9", "", { "os": "android", "cpu": "arm" }, "sha512-5WNI1DaMtxQ7t7B6xa572XMXpHAaI/9Hnhk8lcxF4zVN4xstUgTlvuGDorBguKEnZO70qwEcLpfifMLoxiPqHQ=="],
"@esbuild/android-arm64": ["@esbuild/android-arm64@0.25.9", "", { "os": "android", "cpu": "arm64" }, "sha512-IDrddSmpSv51ftWslJMvl3Q2ZT98fUSL2/rlUXuVqRXHCs5EUF1/f+jbjF5+NG9UffUDMCiTyh8iec7u8RlTLg=="],
"@esbuild/android-x64": ["@esbuild/android-x64@0.25.9", "", { "os": "android", "cpu": "x64" }, "sha512-I853iMZ1hWZdNllhVZKm34f4wErd4lMyeV7BLzEExGEIZYsOzqDWDf+y082izYUE8gtJnYHdeDpN/6tUdwvfiw=="],
"@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.25.9", "", { "os": "darwin", "cpu": "arm64" }, "sha512-XIpIDMAjOELi/9PB30vEbVMs3GV1v2zkkPnuyRRURbhqjyzIINwj+nbQATh4H9GxUgH1kFsEyQMxwiLFKUS6Rg=="],
"@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.25.9", "", { "os": "darwin", "cpu": "x64" }, "sha512-jhHfBzjYTA1IQu8VyrjCX4ApJDnH+ez+IYVEoJHeqJm9VhG9Dh2BYaJritkYK3vMaXrf7Ogr/0MQ8/MeIefsPQ=="],
"@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.25.9", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-z93DmbnY6fX9+KdD4Ue/H6sYs+bhFQJNCPZsi4XWJoYblUqT06MQUdBCpcSfuiN72AbqeBFu5LVQTjfXDE2A6Q=="],
"@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.25.9", "", { "os": "freebsd", "cpu": "x64" }, "sha512-mrKX6H/vOyo5v71YfXWJxLVxgy1kyt1MQaD8wZJgJfG4gq4DpQGpgTB74e5yBeQdyMTbgxp0YtNj7NuHN0PoZg=="],
"@esbuild/linux-arm": ["@esbuild/linux-arm@0.25.9", "", { "os": "linux", "cpu": "arm" }, "sha512-HBU2Xv78SMgaydBmdor38lg8YDnFKSARg1Q6AT0/y2ezUAKiZvc211RDFHlEZRFNRVhcMamiToo7bDx3VEOYQw=="],
"@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.25.9", "", { "os": "linux", "cpu": "arm64" }, "sha512-BlB7bIcLT3G26urh5Dmse7fiLmLXnRlopw4s8DalgZ8ef79Jj4aUcYbk90g8iCa2467HX8SAIidbL7gsqXHdRw=="],
"@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.25.9", "", { "os": "linux", "cpu": "ia32" }, "sha512-e7S3MOJPZGp2QW6AK6+Ly81rC7oOSerQ+P8L0ta4FhVi+/j/v2yZzx5CqqDaWjtPFfYz21Vi1S0auHrap3Ma3A=="],
"@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.25.9", "", { "os": "linux", "cpu": "none" }, "sha512-Sbe10Bnn0oUAB2AalYztvGcK+o6YFFA/9829PhOCUS9vkJElXGdphz0A3DbMdP8gmKkqPmPcMJmJOrI3VYB1JQ=="],
"@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.25.9", "", { "os": "linux", "cpu": "none" }, "sha512-YcM5br0mVyZw2jcQeLIkhWtKPeVfAerES5PvOzaDxVtIyZ2NUBZKNLjC5z3/fUlDgT6w89VsxP2qzNipOaaDyA=="],
"@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.25.9", "", { "os": "linux", "cpu": "ppc64" }, "sha512-++0HQvasdo20JytyDpFvQtNrEsAgNG2CY1CLMwGXfFTKGBGQT3bOeLSYE2l1fYdvML5KUuwn9Z8L1EWe2tzs1w=="],
"@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.25.9", "", { "os": "linux", "cpu": "none" }, "sha512-uNIBa279Y3fkjV+2cUjx36xkx7eSjb8IvnL01eXUKXez/CBHNRw5ekCGMPM0BcmqBxBcdgUWuUXmVWwm4CH9kg=="],
"@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.25.9", "", { "os": "linux", "cpu": "s390x" }, "sha512-Mfiphvp3MjC/lctb+7D287Xw1DGzqJPb/J2aHHcHxflUo+8tmN/6d4k6I2yFR7BVo5/g7x2Monq4+Yew0EHRIA=="],
"@esbuild/linux-x64": ["@esbuild/linux-x64@0.25.9", "", { "os": "linux", "cpu": "x64" }, "sha512-iSwByxzRe48YVkmpbgoxVzn76BXjlYFXC7NvLYq+b+kDjyyk30J0JY47DIn8z1MO3K0oSl9fZoRmZPQI4Hklzg=="],
"@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.25.9", "", { "os": "none", "cpu": "arm64" }, "sha512-9jNJl6FqaUG+COdQMjSCGW4QiMHH88xWbvZ+kRVblZsWrkXlABuGdFJ1E9L7HK+T0Yqd4akKNa/lO0+jDxQD4Q=="],
"@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.25.9", "", { "os": "none", "cpu": "x64" }, "sha512-RLLdkflmqRG8KanPGOU7Rpg829ZHu8nFy5Pqdi9U01VYtG9Y0zOG6Vr2z4/S+/3zIyOxiK6cCeYNWOFR9QP87g=="],
"@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.25.9", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-YaFBlPGeDasft5IIM+CQAhJAqS3St3nJzDEgsgFixcfZeyGPCd6eJBWzke5piZuZ7CtL656eOSYKk4Ls2C0FRQ=="],
"@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.25.9", "", { "os": "openbsd", "cpu": "x64" }, "sha512-1MkgTCuvMGWuqVtAvkpkXFmtL8XhWy+j4jaSO2wxfJtilVCi0ZE37b8uOdMItIHz4I6z1bWWtEX4CJwcKYLcuA=="],
"@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.25.9", "", { "os": "none", "cpu": "arm64" }, "sha512-4Xd0xNiMVXKh6Fa7HEJQbrpP3m3DDn43jKxMjxLLRjWnRsfxjORYJlXPO4JNcXtOyfajXorRKY9NkOpTHptErg=="],
"@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.25.9", "", { "os": "sunos", "cpu": "x64" }, "sha512-WjH4s6hzo00nNezhp3wFIAfmGZ8U7KtrJNlFMRKxiI9mxEK1scOMAaa9i4crUtu+tBr+0IN6JCuAcSBJZfnphw=="],
"@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.25.9", "", { "os": "win32", "cpu": "arm64" }, "sha512-mGFrVJHmZiRqmP8xFOc6b84/7xa5y5YvR1x8djzXpJBSv/UsNK6aqec+6JDjConTgvvQefdGhFDAs2DLAds6gQ=="],
"@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.25.9", "", { "os": "win32", "cpu": "ia32" }, "sha512-b33gLVU2k11nVx1OhX3C8QQP6UHQK4ZtN56oFWvVXvz2VkDoe6fbG8TOgHFxEvqeqohmRnIHe5A1+HADk4OQww=="],
"@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.9", "", { "os": "win32", "cpu": "x64" }, "sha512-PPOl1mi6lpLNQxnGoyAfschAodRFYXJ+9fs6WHXz7CSWKbOqiMZsubC+BQsVKuul+3vKLuwTHsS2c2y9EoKwxQ=="],
"@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="],
"@jridgewell/resolve-uri": ["@jridgewell/resolve-uri@3.1.2", "", {}, "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw=="],
"@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.5", "", {}, "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="],
"@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.30", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-GQ7Nw5G2lTu/BtHTKfXhKHok2WGetd4XYcVKGx00SjAk8GMwgJM3zr6zORiPGuOE+/vkc90KtTosSSvaCjKb2Q=="],
"@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.0.3", "", { "dependencies": { "@emnapi/core": "^1.4.5", "@emnapi/runtime": "^1.4.5", "@tybys/wasm-util": "^0.10.0" } }, "sha512-rZxtMsLwjdXkMUGC3WwsPwLNVqVqnTJT6MNIB6e+5fhMcSCPP0AOsNWuMQ5mdCq6HNjs/ZeWAEchpqeprqBD2Q=="],
"@oxc-project/runtime": ["@oxc-project/runtime@0.82.3", "", {}, "sha512-LNh5GlJvYHAnMurO+EyA8jJwN1rki7l3PSHuosDh2I7h00T6/u9rCkUjg/SvPmT1CZzvhuW0y+gf7jcqUy/Usg=="],
"@oxc-project/types": ["@oxc-project/types@0.82.3", "", {}, "sha512-6nCUxBnGX0c6qfZW5MaF6/fmu5dHJDMiMPaioKHKs5mi5+8/FHQ7WGjgQIz1zxpmceMYfdIXkOaLYE+ejbuOtA=="],
"@quansync/fs": ["@quansync/fs@0.1.5", "", { "dependencies": { "quansync": "^0.2.11" } }, "sha512-lNS9hL2aS2NZgNW7BBj+6EBl4rOf8l+tQ0eRY6JWCI8jI2kc53gSoqbjojU0OnAWhzoXiOjFyGsHcDGePB3lhA=="],
"@rolldown/binding-android-arm64": ["@rolldown/binding-android-arm64@1.0.0-beta.35", "", { "os": "android", "cpu": "arm64" }, "sha512-zVTg0544Ib1ldJSWwjy8URWYHlLFJ98rLnj+2FIj5fRs4KqGKP4VgH/pVUbXNGxeLFjItie6NSK1Un7nJixneQ=="],
"@rolldown/binding-darwin-arm64": ["@rolldown/binding-darwin-arm64@1.0.0-beta.35", "", { "os": "darwin", "cpu": "arm64" }, "sha512-WPy0qx22CABTKDldEExfpYHWHulRoPo+m/YpyxP+6ODUPTQexWl8Wp12fn1CVP0xi0rOBj7ugs6+kKMAJW56wQ=="],
"@rolldown/binding-darwin-x64": ["@rolldown/binding-darwin-x64@1.0.0-beta.35", "", { "os": "darwin", "cpu": "x64" }, "sha512-3k1TabJafF/GgNubXMkfp93d5p30SfIMOmQ5gm1tFwO+baMxxVPwDs3FDvSl+feCWwXxBA+bzemgkaDlInmp1Q=="],
"@rolldown/binding-freebsd-x64": ["@rolldown/binding-freebsd-x64@1.0.0-beta.35", "", { "os": "freebsd", "cpu": "x64" }, "sha512-GAiapN5YyIocnBVNEiOxMfWO9NqIeEKKWohj1sPLGc61P+9N1meXOOCiAPbLU+adXq0grtbYySid+Or7f2q+Mg=="],
"@rolldown/binding-linux-arm-gnueabihf": ["@rolldown/binding-linux-arm-gnueabihf@1.0.0-beta.35", "", { "os": "linux", "cpu": "arm" }, "sha512-okPKKIE73qkUMvq7dxDyzD0VIysdV4AirHqjf8tGTjuNoddUAl3WAtMYbuZCEKJwUyI67UINKO1peFVlYEb+8w=="],
"@rolldown/binding-linux-arm64-gnu": ["@rolldown/binding-linux-arm64-gnu@1.0.0-beta.35", "", { "os": "linux", "cpu": "arm64" }, "sha512-Nky8Q2cxyKVkEETntrvcmlzNir5khQbDfX3PflHPbZY7XVZalllRqw7+MW5vn+jTsk5BfKVeLsvrF4344IU55g=="],
"@rolldown/binding-linux-arm64-musl": ["@rolldown/binding-linux-arm64-musl@1.0.0-beta.35", "", { "os": "linux", "cpu": "arm64" }, "sha512-8aHpWVSfZl3Dy2VNFG9ywmlCPAJx45g0z+qdOeqmYceY7PBAT4QGzii9ig1hPb1pY8K45TXH44UzQwr2fx352Q=="],
"@rolldown/binding-linux-x64-gnu": ["@rolldown/binding-linux-x64-gnu@1.0.0-beta.35", "", { "os": "linux", "cpu": "x64" }, "sha512-1r1Ac/vTcm1q4kRiX/NB6qtorF95PhjdCxKH3Z5pb+bWMDZnmcz18fzFlT/3C6Qpj/ZqUF+EUrG4QEDXtVXGgg=="],
"@rolldown/binding-linux-x64-musl": ["@rolldown/binding-linux-x64-musl@1.0.0-beta.35", "", { "os": "linux", "cpu": "x64" }, "sha512-AFl1LnuhUBDfX2j+cE6DlVGROv4qG7GCPDhR1kJqi2+OuXGDkeEjqRvRQOFErhKz1ckkP/YakvN7JheLJ2PKHQ=="],
"@rolldown/binding-openharmony-arm64": ["@rolldown/binding-openharmony-arm64@1.0.0-beta.35", "", { "os": "none", "cpu": "arm64" }, "sha512-Tuwb8vPs+TVJlHhyLik+nwln/burvIgaPDgg6wjNZ23F1ttjZi0w0rQSZfAgsX4jaUbylwCETXQmTp3w6vcJMw=="],
"@rolldown/binding-wasm32-wasi": ["@rolldown/binding-wasm32-wasi@1.0.0-beta.35", "", { "dependencies": { "@napi-rs/wasm-runtime": "^1.0.3" }, "cpu": "none" }, "sha512-rG0OozgqNUYcpu50MpICMlJflexRVtQfjlN9QYf6hoel46VvY0FbKGwBKoeUp2K5D4i8lV04DpEMfTZlzRjeiA=="],
"@rolldown/binding-win32-arm64-msvc": ["@rolldown/binding-win32-arm64-msvc@1.0.0-beta.35", "", { "os": "win32", "cpu": "arm64" }, "sha512-WeOfAZrycFo9+ZqTDp3YDCAOLolymtKGwImrr9n+OW0lpwI2UKyKXbAwGXRhydAYbfrNmuqWyfyoAnLh3X9Hjg=="],
"@rolldown/binding-win32-ia32-msvc": ["@rolldown/binding-win32-ia32-msvc@1.0.0-beta.35", "", { "os": "win32", "cpu": "ia32" }, "sha512-XkLT7ikKGiUDvLh7qtJHRukbyyP1BIrD1xb7A+w4PjIiOKeOH8NqZ+PBaO4plT7JJnLxx+j9g/3B7iylR1nTFQ=="],
"@rolldown/binding-win32-x64-msvc": ["@rolldown/binding-win32-x64-msvc@1.0.0-beta.35", "", { "os": "win32", "cpu": "x64" }, "sha512-rftASFKVzjbcQHTCYHaBIDrnQFzbeV50tm4hVugG3tPjd435RHZC2pbeGV5IPdKEqyJSuurM/GfbV3kLQ3LY/A=="],
"@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-beta.35", "", {}, "sha512-slYrCpoxJUqzFDDNlvrOYRazQUNRvWPjXA17dAOISY3rDMxX6k8K4cj2H+hEYMHF81HO3uNd5rHVigAWRM5dSg=="],
"@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.50.1", "", { "os": "android", "cpu": "arm" }, "sha512-HJXwzoZN4eYTdD8bVV22DN8gsPCAj3V20NHKOs8ezfXanGpmVPR7kalUHd+Y31IJp9stdB87VKPFbsGY3H/2ag=="],
"@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.50.1", "", { "os": "android", "cpu": "arm64" }, "sha512-PZlsJVcjHfcH53mOImyt3bc97Ep3FJDXRpk9sMdGX0qgLmY0EIWxCag6EigerGhLVuL8lDVYNnSo8qnTElO4xw=="],
"@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.50.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-xc6i2AuWh++oGi4ylOFPmzJOEeAa2lJeGUGb4MudOtgfyyjr4UPNK+eEWTPLvmPJIY/pgw6ssFIox23SyrkkJw=="],
"@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.50.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-2ofU89lEpDYhdLAbRdeyz/kX3Y2lpYc6ShRnDjY35bZhd2ipuDMDi6ZTQ9NIag94K28nFMofdnKeHR7BT0CATw=="],
"@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.50.1", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-wOsE6H2u6PxsHY/BeFHA4VGQN3KUJFZp7QJBmDYI983fgxq5Th8FDkVuERb2l9vDMs1D5XhOrhBrnqcEY6l8ZA=="],
"@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.50.1", "", { "os": "freebsd", "cpu": "x64" }, "sha512-A/xeqaHTlKbQggxCqispFAcNjycpUEHP52mwMQZUNqDUJFFYtPHCXS1VAG29uMlDzIVr+i00tSFWFLivMcoIBQ=="],
"@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.50.1", "", { "os": "linux", "cpu": "arm" }, "sha512-54v4okehwl5TaSIkpp97rAHGp7t3ghinRd/vyC1iXqXMfjYUTm7TfYmCzXDoHUPTTf36L8pr0E7YsD3CfB3ZDg=="],
"@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.50.1", "", { "os": "linux", "cpu": "arm" }, "sha512-p/LaFyajPN/0PUHjv8TNyxLiA7RwmDoVY3flXHPSzqrGcIp/c2FjwPPP5++u87DGHtw+5kSH5bCJz0mvXngYxw=="],
"@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.50.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-2AbMhFFkTo6Ptna1zO7kAXXDLi7H9fGTbVaIq2AAYO7yzcAsuTNWPHhb2aTA6GPiP+JXh85Y8CiS54iZoj4opw=="],
"@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.50.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-Cgef+5aZwuvesQNw9eX7g19FfKX5/pQRIyhoXLCiBOrWopjo7ycfB292TX9MDcDijiuIJlx1IzJz3IoCPfqs9w=="],
"@rollup/rollup-linux-loongarch64-gnu": ["@rollup/rollup-linux-loongarch64-gnu@4.50.1", "", { "os": "linux", "cpu": "none" }, "sha512-RPhTwWMzpYYrHrJAS7CmpdtHNKtt2Ueo+BlLBjfZEhYBhK00OsEqM08/7f+eohiF6poe0YRDDd8nAvwtE/Y62Q=="],
"@rollup/rollup-linux-ppc64-gnu": ["@rollup/rollup-linux-ppc64-gnu@4.50.1", "", { "os": "linux", "cpu": "ppc64" }, "sha512-eSGMVQw9iekut62O7eBdbiccRguuDgiPMsw++BVUg+1K7WjZXHOg/YOT9SWMzPZA+w98G+Fa1VqJgHZOHHnY0Q=="],
"@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.50.1", "", { "os": "linux", "cpu": "none" }, "sha512-S208ojx8a4ciIPrLgazF6AgdcNJzQE4+S9rsmOmDJkusvctii+ZvEuIC4v/xFqzbuP8yDjn73oBlNDgF6YGSXQ=="],
"@rollup/rollup-linux-riscv64-musl": ["@rollup/rollup-linux-riscv64-musl@4.50.1", "", { "os": "linux", "cpu": "none" }, "sha512-3Ag8Ls1ggqkGUvSZWYcdgFwriy2lWo+0QlYgEFra/5JGtAd6C5Hw59oojx1DeqcA2Wds2ayRgvJ4qxVTzCHgzg=="],
"@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.50.1", "", { "os": "linux", "cpu": "s390x" }, "sha512-t9YrKfaxCYe7l7ldFERE1BRg/4TATxIg+YieHQ966jwvo7ddHJxPj9cNFWLAzhkVsbBvNA4qTbPVNsZKBO4NSg=="],
"@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.50.1", "", { "os": "linux", "cpu": "x64" }, "sha512-MCgtFB2+SVNuQmmjHf+wfI4CMxy3Tk8XjA5Z//A0AKD7QXUYFMQcns91K6dEHBvZPCnhJSyDWLApk40Iq/H3tA=="],
"@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.50.1", "", { "os": "linux", "cpu": "x64" }, "sha512-nEvqG+0jeRmqaUMuwzlfMKwcIVffy/9KGbAGyoa26iu6eSngAYQ512bMXuqqPrlTyfqdlB9FVINs93j534UJrg=="],
"@rollup/rollup-openharmony-arm64": ["@rollup/rollup-openharmony-arm64@4.50.1", "", { "os": "none", "cpu": "arm64" }, "sha512-RDsLm+phmT3MJd9SNxA9MNuEAO/J2fhW8GXk62G/B4G7sLVumNFbRwDL6v5NrESb48k+QMqdGbHgEtfU0LCpbA=="],
"@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.50.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-hpZB/TImk2FlAFAIsoElM3tLzq57uxnGYwplg6WDyAxbYczSi8O2eQ+H2Lx74504rwKtZ3N2g4bCUkiamzS6TQ=="],
"@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.50.1", "", { "os": "win32", "cpu": "ia32" }, "sha512-SXjv8JlbzKM0fTJidX4eVsH+Wmnp0/WcD8gJxIZyR6Gay5Qcsmdbi9zVtnbkGPG8v2vMR1AD06lGWy5FLMcG7A=="],
"@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.50.1", "", { "os": "win32", "cpu": "x64" }, "sha512-StxAO/8ts62KZVRAm4JZYq9+NqNsV7RvimNK+YM7ry//zebEH6meuugqW/P5OFUCjyQgui+9fUxT6d5NShvMvA=="],
"@tybys/wasm-util": ["@tybys/wasm-util@0.10.0", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-VyyPYFlOMNylG45GoAe0xDoLwWuowvf92F9kySqzYh8vmYm7D2u4iUJKa1tOUpS70Ku13ASrOkS4ScXFsTaCNQ=="],
"@types/bun": ["@types/bun@1.2.21", "", { "dependencies": { "bun-types": "1.2.21" } }, "sha512-NiDnvEqmbfQ6dmZ3EeUO577s4P5bf4HCTXtI6trMc6f6RzirY5IrF3aIookuSpyslFzrnvv2lmEWv5HyC1X79A=="],
"@types/chai": ["@types/chai@5.2.2", "", { "dependencies": { "@types/deep-eql": "*" } }, "sha512-8kB30R7Hwqf40JPiKhVzodJs2Qc1ZJ5zuT3uzw5Hq/dhNCl3G3l83jfpdI1e20BP348+fV7VIL/+FxaXkqBmWg=="],
"@types/deep-eql": ["@types/deep-eql@4.0.2", "", {}, "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw=="],
"@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="],
"@types/node": ["@types/node@22.18.1", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-rzSDyhn4cYznVG+PCzGe1lwuMYJrcBS1fc3JqSa2PvtABwWo+dZ1ij5OVok3tqfpEBCBoaR4d7upFJk73HRJDw=="],
"@types/node-cache": ["@types/node-cache@4.2.5", "", { "dependencies": { "node-cache": "*" } }, "sha512-faK2Owokboz53g8ooq2dw3iDJ6/HMTCIa2RvMte5WMTiABy+wA558K+iuyRtlR67Un5q9gEKysSDtqZYbSa0Pg=="],
"@types/react": ["@types/react@19.1.12", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-cMoR+FoAf/Jyq6+Df2/Z41jISvGZZ2eTlnsaJRptmZ76Caldwy1odD4xTr/gNV9VLj0AWgg/nmkevIyUfIIq5w=="],
"@types/stream-chain": ["@types/stream-chain@2.1.0", "", { "dependencies": { "@types/node": "*" } }, "sha512-guDyAl6s/CAzXUOWpGK2bHvdiopLIwpGu8v10+lb9hnQOyo4oj/ZUQFOvqFjKGsE3wJP1fpIesCcMvbXuWsqOg=="],
"@types/stream-json": ["@types/stream-json@1.7.8", "", { "dependencies": { "@types/node": "*", "@types/stream-chain": "*" } }, "sha512-MU1OB1eFLcYWd1LjwKXrxdoPtXSRzRmAnnxs4Js/ayB5O/NvHraWwuOaqMWIebpYwM6khFlsJOHEhI9xK/ab4Q=="],
"@types/triple-beam": ["@types/triple-beam@1.3.5", "", {}, "sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw=="],
"@vitest/expect": ["@vitest/expect@3.2.4", "", { "dependencies": { "@types/chai": "^5.2.2", "@vitest/spy": "3.2.4", "@vitest/utils": "3.2.4", "chai": "^5.2.0", "tinyrainbow": "^2.0.0" } }, "sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig=="],
"@vitest/mocker": ["@vitest/mocker@3.2.4", "", { "dependencies": { "@vitest/spy": "3.2.4", "estree-walker": "^3.0.3", "magic-string": "^0.30.17" }, "peerDependencies": { "msw": "^2.4.9", "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" }, "optionalPeers": ["msw", "vite"] }, "sha512-46ryTE9RZO/rfDd7pEqFl7etuyzekzEhUbTW3BvmeO/BcCMEgq59BKhek3dXDWgAj4oMK6OZi+vRr1wPW6qjEQ=="],
"@vitest/pretty-format": ["@vitest/pretty-format@3.2.4", "", { "dependencies": { "tinyrainbow": "^2.0.0" } }, "sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA=="],
"@vitest/runner": ["@vitest/runner@3.2.4", "", { "dependencies": { "@vitest/utils": "3.2.4", "pathe": "^2.0.3", "strip-literal": "^3.0.0" } }, "sha512-oukfKT9Mk41LreEW09vt45f8wx7DordoWUZMYdY/cyAk7w5TWkTRCNZYF7sX7n2wB7jyGAl74OxgwhPgKaqDMQ=="],
"@vitest/snapshot": ["@vitest/snapshot@3.2.4", "", { "dependencies": { "@vitest/pretty-format": "3.2.4", "magic-string": "^0.30.17", "pathe": "^2.0.3" } }, "sha512-dEYtS7qQP2CjU27QBC5oUOxLE/v5eLkGqPE0ZKEIDGMs4vKWe7IjgLOeauHsR0D5YuuycGRO5oSRXnwnmA78fQ=="],
"@vitest/spy": ["@vitest/spy@3.2.4", "", { "dependencies": { "tinyspy": "^4.0.3" } }, "sha512-vAfasCOe6AIK70iP5UD11Ac4siNUNJ9i/9PZ3NKx07sG6sUxeag1LWdNrMWeKKYBLlzuK+Gn65Yd5nyL6ds+nw=="],
"@vitest/utils": ["@vitest/utils@3.2.4", "", { "dependencies": { "@vitest/pretty-format": "3.2.4", "loupe": "^3.1.4", "tinyrainbow": "^2.0.0" } }, "sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA=="],
"ansis": ["ansis@4.1.0", "", {}, "sha512-BGcItUBWSMRgOCe+SVZJ+S7yTRG0eGt9cXAHev72yuGcY23hnLA7Bky5L/xLyPINoSN95geovfBkqoTlNZYa7w=="],
"args-tokenizer": ["args-tokenizer@0.3.0", "", {}, "sha512-xXAd7G2Mll5W8uo37GETpQ2VrE84M181Z7ugHFGQnJZ50M2mbOv0osSZ9VsSgPfJQ+LVG0prSi0th+ELMsno7Q=="],
"assertion-error": ["assertion-error@2.0.1", "", {}, "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA=="],
"ast-kit": ["ast-kit@2.1.2", "", { "dependencies": { "@babel/parser": "^7.28.0", "pathe": "^2.0.3" } }, "sha512-cl76xfBQM6pztbrFWRnxbrDm9EOqDr1BF6+qQnnDZG2Co2LjyUktkN9GTJfBAfdae+DbT2nJf2nCGAdDDN7W2g=="],
"async": ["async@3.2.6", "", {}, "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA=="],
"birpc": ["birpc@2.5.0", "", {}, "sha512-VSWO/W6nNQdyP520F1mhf+Lc2f8pjGQOtoHHm7Ze8Go1kX7akpVIrtTa0fn+HB0QJEDVacl6aO08YE0PgXfdnQ=="],
"bumpp": ["bumpp@10.2.3", "", { "dependencies": { "ansis": "^4.1.0", "args-tokenizer": "^0.3.0", "c12": "^3.2.0", "cac": "^6.7.14", "escalade": "^3.2.0", "jsonc-parser": "^3.3.1", "package-manager-detector": "^1.3.0", "semver": "^7.7.2", "tinyexec": "^1.0.1", "tinyglobby": "^0.2.14", "yaml": "^2.8.1" }, "bin": { "bumpp": "bin/bumpp.mjs" } }, "sha512-nsFBZACxuBVu6yzDSaZZaWpX5hTQ+++9WtYkmO+0Bd3cpSq0Mzvqw5V83n+fOyRj3dYuZRFCQf5Z9NNfZj+Rnw=="],
"bun-types": ["bun-types@1.2.21", "", { "dependencies": { "@types/node": "*" }, "peerDependencies": { "@types/react": "^19" } }, "sha512-sa2Tj77Ijc/NTLS0/Odjq/qngmEPZfbfnOERi0KRUYhT9R8M4VBioWVmMWE5GrYbKMc+5lVybXygLdibHaqVqw=="],
"c12": ["c12@3.2.0", "", { "dependencies": { "chokidar": "^4.0.3", "confbox": "^0.2.2", "defu": "^6.1.4", "dotenv": "^17.2.1", "exsolve": "^1.0.7", "giget": "^2.0.0", "jiti": "^2.5.1", "ohash": "^2.0.11", "pathe": "^2.0.3", "perfect-debounce": "^1.0.0", "pkg-types": "^2.2.0", "rc9": "^2.1.2" }, "peerDependencies": { "magicast": "^0.3.5" }, "optionalPeers": ["magicast"] }, "sha512-ixkEtbYafL56E6HiFuonMm1ZjoKtIo7TH68/uiEq4DAwv9NcUX2nJ95F8TrbMeNjqIkZpruo3ojXQJ+MGG5gcQ=="],
"cac": ["cac@6.7.14", "", {}, "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ=="],
"chai": ["chai@5.3.3", "", { "dependencies": { "assertion-error": "^2.0.1", "check-error": "^2.1.1", "deep-eql": "^5.0.1", "loupe": "^3.1.0", "pathval": "^2.0.0" } }, "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw=="],
"check-error": ["check-error@2.1.1", "", {}, "sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw=="],
"chokidar": ["chokidar@4.0.3", "", { "dependencies": { "readdirp": "^4.0.1" } }, "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA=="],
"citty": ["citty@0.1.6", "", { "dependencies": { "consola": "^3.2.3" } }, "sha512-tskPPKEs8D2KPafUypv2gxwJP8h/OaJmC82QQGGDQcHvXX43xF2VDACcJVmZ0EuSxkpO9Kc4MlrA3q0+FG58AQ=="],
"clone": ["clone@2.1.2", "", {}, "sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w=="],
"color": ["color@3.2.1", "", { "dependencies": { "color-convert": "^1.9.3", "color-string": "^1.6.0" } }, "sha512-aBl7dZI9ENN6fUGC7mWpMTPNHmWUSNan9tuWN6ahh5ZLNk9baLJOnSMlrQkHcrfFgz2/RigjUVAjdx36VcemKA=="],
"color-convert": ["color-convert@1.9.3", "", { "dependencies": { "color-name": "1.1.3" } }, "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg=="],
"color-name": ["color-name@1.1.3", "", {}, "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw=="],
"color-string": ["color-string@1.9.1", "", { "dependencies": { "color-name": "^1.0.0", "simple-swizzle": "^0.2.2" } }, "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg=="],
"colorspace": ["colorspace@1.1.4", "", { "dependencies": { "color": "^3.1.3", "text-hex": "1.0.x" } }, "sha512-BgvKJiuVu1igBUF2kEjRCZXol6wiiGbY5ipL/oVPwm0BL9sIpMIzM8IK7vwuxIIzOXMV3Ey5w+vxhm0rR/TN8w=="],
"confbox": ["confbox@0.2.2", "", {}, "sha512-1NB+BKqhtNipMsov4xI/NnhCKp9XG9NamYp5PVm9klAT0fsrNPjaFICsCFhNhwZJKNh7zB/3q8qXz0E9oaMNtQ=="],
"consola": ["consola@3.4.2", "", {}, "sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA=="],
"cron-parser": ["cron-parser@5.3.1", "", { "dependencies": { "luxon": "^3.7.1" } }, "sha512-Mu5Jk1b4cUfY8u34+thI9TZxvQiuhaMBS2Ag84rOSoHlU33xtIPkXwr6lWuw3XPmxSxq317B+hl0o4J+LdhwNg=="],
"csstype": ["csstype@3.1.3", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="],
"date-fns": ["date-fns@4.1.0", "", {}, "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg=="],
"debug": ["debug@4.4.1", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ=="],
"deep-eql": ["deep-eql@5.0.2", "", {}, "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q=="],
"defu": ["defu@6.1.4", "", {}, "sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg=="],
"destr": ["destr@2.0.5", "", {}, "sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA=="],
"diff": ["diff@8.0.2", "", {}, "sha512-sSuxWU5j5SR9QQji/o2qMvqRNYRDOcBTgsJ/DeCf4iSN4gW+gNMXM7wFIP+fdXZxoNiAnHUTGjCr+TSWXdRDKg=="],
"dotenv": ["dotenv@17.2.2", "", {}, "sha512-Sf2LSQP+bOlhKWWyhFsn0UsfdK/kCWRv1iuA2gXAwt3dyNabr6QSj00I2V10pidqz69soatm9ZwZvpQMTIOd5Q=="],
"dts-resolver": ["dts-resolver@2.1.2", "", { "peerDependencies": { "oxc-resolver": ">=11.0.0" }, "optionalPeers": ["oxc-resolver"] }, "sha512-xeXHBQkn2ISSXxbJWD828PFjtyg+/UrMDo7W4Ffcs7+YWCquxU8YjV1KoxuiL+eJ5pg3ll+bC6flVv61L3LKZg=="],
"empathic": ["empathic@2.0.0", "", {}, "sha512-i6UzDscO/XfAcNYD75CfICkmfLedpyPDdozrLMmQc5ORaQcdMoc21OnlEylMIqI7U8eniKrPMxxtj8k0vhmJhA=="],
"enabled": ["enabled@2.0.0", "", {}, "sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ=="],
"es-module-lexer": ["es-module-lexer@1.7.0", "", {}, "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA=="],
"esbuild": ["esbuild@0.25.9", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.9", "@esbuild/android-arm": "0.25.9", "@esbuild/android-arm64": "0.25.9", "@esbuild/android-x64": "0.25.9", "@esbuild/darwin-arm64": "0.25.9", "@esbuild/darwin-x64": "0.25.9", "@esbuild/freebsd-arm64": "0.25.9", "@esbuild/freebsd-x64": "0.25.9", "@esbuild/linux-arm": "0.25.9", "@esbuild/linux-arm64": "0.25.9", "@esbuild/linux-ia32": "0.25.9", "@esbuild/linux-loong64": "0.25.9", "@esbuild/linux-mips64el": "0.25.9", "@esbuild/linux-ppc64": "0.25.9", "@esbuild/linux-riscv64": "0.25.9", "@esbuild/linux-s390x": "0.25.9", "@esbuild/linux-x64": "0.25.9", "@esbuild/netbsd-arm64": "0.25.9", "@esbuild/netbsd-x64": "0.25.9", "@esbuild/openbsd-arm64": "0.25.9", "@esbuild/openbsd-x64": "0.25.9", "@esbuild/openharmony-arm64": "0.25.9", "@esbuild/sunos-x64": "0.25.9", "@esbuild/win32-arm64": "0.25.9", "@esbuild/win32-ia32": "0.25.9", "@esbuild/win32-x64": "0.25.9" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-CRbODhYyQx3qp7ZEwzxOk4JBqmD/seJrzPa/cGjY1VtIn5E09Oi9/dB4JwctnfZ8Q8iT7rioVv5k/FNT/uf54g=="],
"escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="],
"estree-walker": ["estree-walker@3.0.3", "", { "dependencies": { "@types/estree": "^1.0.0" } }, "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g=="],
"expect-type": ["expect-type@1.2.2", "", {}, "sha512-JhFGDVJ7tmDJItKhYgJCGLOWjuK9vPxiXoUFLwLDc99NlmklilbiQJwoctZtt13+xMw91MCk/REan6MWHqDjyA=="],
"exsolve": ["exsolve@1.0.7", "", {}, "sha512-VO5fQUzZtI6C+vx4w/4BWJpg3s/5l+6pRQEHzFRM8WFi4XffSP1Z+4qi7GbjWbvRQEbdIco5mIMq+zX4rPuLrw=="],
"fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="],
"fecha": ["fecha@4.2.3", "", {}, "sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw=="],
"fn.name": ["fn.name@1.1.0", "", {}, "sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw=="],
"fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="],
"get-tsconfig": ["get-tsconfig@4.10.1", "", { "dependencies": { "resolve-pkg-maps": "^1.0.0" } }, "sha512-auHyJ4AgMz7vgS8Hp3N6HXSmlMdUyhSUrfBF16w153rxtLIEOE+HGqaBppczZvnHLqQJfiHotCYpNhl0lUROFQ=="],
"giget": ["giget@2.0.0", "", { "dependencies": { "citty": "^0.1.6", "consola": "^3.4.0", "defu": "^6.1.4", "node-fetch-native": "^1.6.6", "nypm": "^0.6.0", "pathe": "^2.0.3" }, "bin": { "giget": "dist/cli.mjs" } }, "sha512-L5bGsVkxJbJgdnwyuheIunkGatUF/zssUoxxjACCseZYAVbaqdh9Tsmmlkl8vYan09H7sbvKt4pS8GqKLBrEzA=="],
"hookable": ["hookable@5.5.3", "", {}, "sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ=="],
"inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="],
"is-arrayish": ["is-arrayish@0.3.2", "", {}, "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ=="],
"is-stream": ["is-stream@2.0.1", "", {}, "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg=="],
"jiti": ["jiti@2.5.1", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-twQoecYPiVA5K/h6SxtORw/Bs3ar+mLUtoPSc7iMXzQzK8d7eJ/R09wmTwAjiamETn1cXYPGfNnu7DMoHgu12w=="],
"js-tokens": ["js-tokens@9.0.1", "", {}, "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ=="],
"jsesc": ["jsesc@3.1.0", "", { "bin": { "jsesc": "bin/jsesc" } }, "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA=="],
"jsonc-parser": ["jsonc-parser@3.3.1", "", {}, "sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ=="],
"kuler": ["kuler@2.0.0", "", {}, "sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A=="],
"logform": ["logform@2.7.0", "", { "dependencies": { "@colors/colors": "1.6.0", "@types/triple-beam": "^1.3.2", "fecha": "^4.2.0", "ms": "^2.1.1", "safe-stable-stringify": "^2.3.1", "triple-beam": "^1.3.0" } }, "sha512-TFYA4jnP7PVbmlBIfhlSe+WKxs9dklXMTEGcBCIvLhE/Tn3H6Gk1norupVW7m5Cnd4bLcr08AytbyV/xj7f/kQ=="],
"loupe": ["loupe@3.2.1", "", {}, "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ=="],
"luxon": ["luxon@3.7.2", "", {}, "sha512-vtEhXh/gNjI9Yg1u4jX/0YVPMvxzHuGgCm6tC5kZyb08yjGWGnqAjGJvcXbqQR2P3MyMEFnRbpcdFS6PBcLqew=="],
"magic-string": ["magic-string@0.30.18", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-yi8swmWbO17qHhwIBNeeZxTceJMeBvWJaId6dyvTSOwTipqeHhMhOrz6513r1sOKnpvQ7zkhlG8tPrpilwTxHQ=="],
"ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
"nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="],
"node-cache": ["node-cache@5.1.2", "", { "dependencies": { "clone": "2.x" } }, "sha512-t1QzWwnk4sjLWaQAS8CHgOJ+RAfmHpxFWmc36IWTiWHQfs0w5JDMBS1b1ZxQteo0vVVuWJvIUKHDkkeK7vIGCg=="],
"node-fetch-native": ["node-fetch-native@1.6.7", "", {}, "sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q=="],
"nypm": ["nypm@0.6.1", "", { "dependencies": { "citty": "^0.1.6", "consola": "^3.4.2", "pathe": "^2.0.3", "pkg-types": "^2.2.0", "tinyexec": "^1.0.1" }, "bin": { "nypm": "dist/cli.mjs" } }, "sha512-hlacBiRiv1k9hZFiphPUkfSQ/ZfQzZDzC+8z0wL3lvDAOUu/2NnChkKuMoMjNur/9OpKuz2QsIeiPVN0xM5Q0w=="],
"ohash": ["ohash@2.0.11", "", {}, "sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ=="],
"one-time": ["one-time@1.0.0", "", { "dependencies": { "fn.name": "1.x.x" } }, "sha512-5DXOiRKwuSEcQ/l0kGCF6Q3jcADFv5tSmRaJck/OqkVFcOzutB134KRSfF0xDrL39MNnqxbHBbUUcjZIhTgb2g=="],
"package-manager-detector": ["package-manager-detector@1.3.0", "", {}, "sha512-ZsEbbZORsyHuO00lY1kV3/t72yp6Ysay6Pd17ZAlNGuGwmWDLCJxFpRs0IzfXfj1o4icJOkUEioexFHzyPurSQ=="],
"pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="],
"pathval": ["pathval@2.0.1", "", {}, "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ=="],
"perfect-debounce": ["perfect-debounce@1.0.0", "", {}, "sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA=="],
"picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="],
"picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="],
"pkg-types": ["pkg-types@2.3.0", "", { "dependencies": { "confbox": "^0.2.2", "exsolve": "^1.0.7", "pathe": "^2.0.3" } }, "sha512-SIqCzDRg0s9npO5XQ3tNZioRY1uK06lA41ynBC1YmFTmnY6FjUjVt6s4LoADmwoig1qqD0oK8h1p/8mlMx8Oig=="],
"postcss": ["postcss@8.5.6", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg=="],
"quansync": ["quansync@0.2.11", "", {}, "sha512-AifT7QEbW9Nri4tAwR5M/uzpBuqfZf+zwaEM/QkzEjj7NBuFD2rBuy0K3dE+8wltbezDV7JMA0WfnCPYRSYbXA=="],
"rc9": ["rc9@2.1.2", "", { "dependencies": { "defu": "^6.1.4", "destr": "^2.0.3" } }, "sha512-btXCnMmRIBINM2LDZoEmOogIZU7Qe7zn4BpomSKZ/ykbLObuBdvG+mFq11DL6fjH1DRwHhrlgtYWG96bJiC7Cg=="],
"readable-stream": ["readable-stream@3.6.2", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA=="],
"readdirp": ["readdirp@4.1.2", "", {}, "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg=="],
"resolve-pkg-maps": ["resolve-pkg-maps@1.0.0", "", {}, "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw=="],
"rolldown": ["rolldown@1.0.0-beta.35", "", { "dependencies": { "@oxc-project/runtime": "=0.82.3", "@oxc-project/types": "=0.82.3", "@rolldown/pluginutils": "1.0.0-beta.35", "ansis": "^4.0.0" }, "optionalDependencies": { "@rolldown/binding-android-arm64": "1.0.0-beta.35", "@rolldown/binding-darwin-arm64": "1.0.0-beta.35", "@rolldown/binding-darwin-x64": "1.0.0-beta.35", "@rolldown/binding-freebsd-x64": "1.0.0-beta.35", "@rolldown/binding-linux-arm-gnueabihf": "1.0.0-beta.35", "@rolldown/binding-linux-arm64-gnu": "1.0.0-beta.35", "@rolldown/binding-linux-arm64-musl": "1.0.0-beta.35", "@rolldown/binding-linux-x64-gnu": "1.0.0-beta.35", "@rolldown/binding-linux-x64-musl": "1.0.0-beta.35", "@rolldown/binding-openharmony-arm64": "1.0.0-beta.35", "@rolldown/binding-wasm32-wasi": "1.0.0-beta.35", "@rolldown/binding-win32-arm64-msvc": "1.0.0-beta.35", "@rolldown/binding-win32-ia32-msvc": "1.0.0-beta.35", "@rolldown/binding-win32-x64-msvc": "1.0.0-beta.35" }, "bin": { "rolldown": "bin/cli.mjs" } }, "sha512-gJATyqcsJe0Cs8RMFO8XgFjfTc0lK1jcSvirDQDSIfsJE+vt53QH/Ob+OBSJsXb98YtZXHfP/bHpELpPwCprow=="],
"rolldown-plugin-dts": ["rolldown-plugin-dts@0.15.10", "", { "dependencies": { "@babel/generator": "^7.28.3", "@babel/parser": "^7.28.3", "@babel/types": "^7.28.2", "ast-kit": "^2.1.2", "birpc": "^2.5.0", "debug": "^4.4.1", "dts-resolver": "^2.1.2", "get-tsconfig": "^4.10.1" }, "peerDependencies": { "@typescript/native-preview": ">=7.0.0-dev.20250601.1", "rolldown": "^1.0.0-beta.9", "typescript": "^5.0.0", "vue-tsc": "~3.0.3" }, "optionalPeers": ["@typescript/native-preview", "typescript", "vue-tsc"] }, "sha512-8cPVAVQUo9tYAoEpc3jFV9RxSil13hrRRg8cHC9gLXxRMNtWPc1LNMSDXzjyD+5Vny49sDZH77JlXp/vlc4I3g=="],
"rollup": ["rollup@4.50.1", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.50.1", "@rollup/rollup-android-arm64": "4.50.1", "@rollup/rollup-darwin-arm64": "4.50.1", "@rollup/rollup-darwin-x64": "4.50.1", "@rollup/rollup-freebsd-arm64": "4.50.1", "@rollup/rollup-freebsd-x64": "4.50.1", "@rollup/rollup-linux-arm-gnueabihf": "4.50.1", "@rollup/rollup-linux-arm-musleabihf": "4.50.1", "@rollup/rollup-linux-arm64-gnu": "4.50.1", "@rollup/rollup-linux-arm64-musl": "4.50.1", "@rollup/rollup-linux-loongarch64-gnu": "4.50.1", "@rollup/rollup-linux-ppc64-gnu": "4.50.1", "@rollup/rollup-linux-riscv64-gnu": "4.50.1", "@rollup/rollup-linux-riscv64-musl": "4.50.1", "@rollup/rollup-linux-s390x-gnu": "4.50.1", "@rollup/rollup-linux-x64-gnu": "4.50.1", "@rollup/rollup-linux-x64-musl": "4.50.1", "@rollup/rollup-openharmony-arm64": "4.50.1", "@rollup/rollup-win32-arm64-msvc": "4.50.1", "@rollup/rollup-win32-ia32-msvc": "4.50.1", "@rollup/rollup-win32-x64-msvc": "4.50.1", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-78E9voJHwnXQMiQdiqswVLZwJIzdBKJ1GdI5Zx6XwoFKUIk09/sSrr+05QFzvYb8q6Y9pPV45zzDuYa3907TZA=="],
"safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="],
"safe-stable-stringify": ["safe-stable-stringify@2.5.0", "", {}, "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA=="],
"semver": ["semver@7.7.2", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA=="],
"siginfo": ["siginfo@2.0.0", "", {}, "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g=="],
"simple-swizzle": ["simple-swizzle@0.2.2", "", { "dependencies": { "is-arrayish": "^0.3.1" } }, "sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg=="],
"source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="],
"stack-trace": ["stack-trace@0.0.10", "", {}, "sha512-KGzahc7puUKkzyMt+IqAep+TVNbKP+k2Lmwhub39m1AsTSkaDutx56aDCo+HLDzf/D26BIHTJWNiTG1KAJiQCg=="],
"stackback": ["stackback@0.0.2", "", {}, "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw=="],
"std-env": ["std-env@3.9.0", "", {}, "sha512-UGvjygr6F6tpH7o2qyqR6QYpwraIjKSdtzyBdyytFOHmPZY917kwdwLG0RbOjWOnKmnm3PeHjaoLLMie7kPLQw=="],
"stream-chain": ["stream-chain@3.4.0", "", {}, "sha512-cyDiaDqAfgmeiv0PWFXCg9oKNVYNzYxHK9j5CMsYMHZDk+/yYcSV+CXQZliZ0U4mNU8DLqiVNZXUfs8BqhgwMw=="],
"stream-json": ["stream-json@1.9.1", "", { "dependencies": { "stream-chain": "^2.2.5" } }, "sha512-uWkjJ+2Nt/LO9Z/JyKZbMusL8Dkh97uUBTv3AJQ74y07lVahLY4eEFsPsE97pxYBwr8nnjMAIch5eqI0gPShyw=="],
"string_decoder": ["string_decoder@1.3.0", "", { "dependencies": { "safe-buffer": "~5.2.0" } }, "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA=="],
"strip-literal": ["strip-literal@3.0.0", "", { "dependencies": { "js-tokens": "^9.0.1" } }, "sha512-TcccoMhJOM3OebGhSBEmp3UZ2SfDMZUEBdRA/9ynfLi8yYajyWX3JiXArcJt4Umh4vISpspkQIY8ZZoCqjbviA=="],
"text-hex": ["text-hex@1.0.0", "", {}, "sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg=="],
"tinybench": ["tinybench@2.9.0", "", {}, "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg=="],
"tinyexec": ["tinyexec@1.0.1", "", {}, "sha512-5uC6DDlmeqiOwCPmK9jMSdOuZTh8bU39Ys6yidB+UTt5hfZUPGAypSgFRiEp+jbi9qH40BLDvy85jIU88wKSqw=="],
"tinyglobby": ["tinyglobby@0.2.15", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.3" } }, "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ=="],
"tinypool": ["tinypool@1.1.1", "", {}, "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg=="],
"tinyrainbow": ["tinyrainbow@2.0.0", "", {}, "sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw=="],
"tinyspy": ["tinyspy@4.0.3", "", {}, "sha512-t2T/WLB2WRgZ9EpE4jgPJ9w+i66UZfDc8wHh0xrwiRNN+UwH98GIJkTeZqX9rg0i0ptwzqW+uYeIF0T4F8LR7A=="],
"tree-kill": ["tree-kill@1.2.2", "", { "bin": { "tree-kill": "cli.js" } }, "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A=="],
"triple-beam": ["triple-beam@1.4.1", "", {}, "sha512-aZbgViZrg1QNcG+LULa7nhZpJTZSLm/mXnHXnbAbjmN5aSa0y7V+wvv6+4WaBtpISJzThKy+PIPxc1Nq1EJ9mg=="],
"tsdown": ["tsdown@0.14.2", "", { "dependencies": { "ansis": "^4.1.0", "cac": "^6.7.14", "chokidar": "^4.0.3", "debug": "^4.4.1", "diff": "^8.0.2", "empathic": "^2.0.0", "hookable": "^5.5.3", "rolldown": "latest", "rolldown-plugin-dts": "^0.15.8", "semver": "^7.7.2", "tinyexec": "^1.0.1", "tinyglobby": "^0.2.14", "tree-kill": "^1.2.2", "unconfig": "^7.3.3" }, "peerDependencies": { "@arethetypeswrong/core": "^0.18.1", "publint": "^0.3.0", "typescript": "^5.0.0", "unplugin-lightningcss": "^0.4.0", "unplugin-unused": "^0.5.0" }, "optionalPeers": ["@arethetypeswrong/core", "publint", "typescript", "unplugin-lightningcss", "unplugin-unused"], "bin": { "tsdown": "dist/run.mjs" } }, "sha512-6ThtxVZoTlR5YJov5rYvH8N1+/S/rD/pGfehdCLGznGgbxz+73EASV1tsIIZkLw2n+SXcERqHhcB/OkyxdKv3A=="],
"tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
"typescript": ["typescript@5.9.2", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A=="],
"unconfig": ["unconfig@7.3.3", "", { "dependencies": { "@quansync/fs": "^0.1.5", "defu": "^6.1.4", "jiti": "^2.5.1", "quansync": "^0.2.11" } }, "sha512-QCkQoOnJF8L107gxfHL0uavn7WD9b3dpBcFX6HtfQYmjw2YzWxGuFQ0N0J6tE9oguCBJn9KOvfqYDCMPHIZrBA=="],
"undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="],
"util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="],
"vite": ["vite@7.1.4", "", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.14" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.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" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-X5QFK4SGynAeeIt+A7ZWnApdUyHYm+pzv/8/A57LqSGcI88U6R6ipOs3uCesdc6yl7nl+zNO0t8LmqAdXcQihw=="],
"vite-node": ["vite-node@3.2.4", "", { "dependencies": { "cac": "^6.7.14", "debug": "^4.4.1", "es-module-lexer": "^1.7.0", "pathe": "^2.0.3", "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" }, "bin": { "vite-node": "vite-node.mjs" } }, "sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg=="],
"vitest": ["vitest@3.2.4", "", { "dependencies": { "@types/chai": "^5.2.2", "@vitest/expect": "3.2.4", "@vitest/mocker": "3.2.4", "@vitest/pretty-format": "^3.2.4", "@vitest/runner": "3.2.4", "@vitest/snapshot": "3.2.4", "@vitest/spy": "3.2.4", "@vitest/utils": "3.2.4", "chai": "^5.2.0", "debug": "^4.4.1", "expect-type": "^1.2.1", "magic-string": "^0.30.17", "pathe": "^2.0.3", "picomatch": "^4.0.2", "std-env": "^3.9.0", "tinybench": "^2.9.0", "tinyexec": "^0.3.2", "tinyglobby": "^0.2.14", "tinypool": "^1.1.1", "tinyrainbow": "^2.0.0", "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0", "vite-node": "3.2.4", "why-is-node-running": "^2.3.0" }, "peerDependencies": { "@edge-runtime/vm": "*", "@types/debug": "^4.1.12", "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", "@vitest/browser": "3.2.4", "@vitest/ui": "3.2.4", "happy-dom": "*", "jsdom": "*" }, "optionalPeers": ["@edge-runtime/vm", "@types/debug", "@types/node", "@vitest/browser", "@vitest/ui", "happy-dom", "jsdom"], "bin": { "vitest": "vitest.mjs" } }, "sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A=="],
"why-is-node-running": ["why-is-node-running@2.3.0", "", { "dependencies": { "siginfo": "^2.0.0", "stackback": "0.0.2" }, "bin": { "why-is-node-running": "cli.js" } }, "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w=="],
"winston": ["winston@3.17.0", "", { "dependencies": { "@colors/colors": "^1.6.0", "@dabh/diagnostics": "^2.0.2", "async": "^3.2.3", "is-stream": "^2.0.0", "logform": "^2.7.0", "one-time": "^1.0.0", "readable-stream": "^3.4.0", "safe-stable-stringify": "^2.3.1", "stack-trace": "0.0.x", "triple-beam": "^1.3.0", "winston-transport": "^4.9.0" } }, "sha512-DLiFIXYC5fMPxaRg832S6F5mJYvePtmO5G9v9IgUFPhXm9/GkXarH/TUrBAVzhTCzAj9anE/+GjrgXp/54nOgw=="],
"winston-transport": ["winston-transport@4.9.0", "", { "dependencies": { "logform": "^2.7.0", "readable-stream": "^3.6.2", "triple-beam": "^1.3.0" } }, "sha512-8drMJ4rkgaPo1Me4zD/3WLfI/zPdA9o2IipKODunnGDcuqbHwjsbB79ylv04LCGGzU0xQ6vTznOMpQGaLhhm6A=="],
"yaml": ["yaml@2.8.1", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-lcYcMxX2PO9XMGvAJkJ3OsNMw+/7FKes7/hgerGUYWIoWu5j/+YQqcZr5JnPZWzOsEBgMbSbiSTn/dv/69Mkpw=="],
"color-string/color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="],
"stream-json/stream-chain": ["stream-chain@2.2.5", "", {}, "sha512-1TJmBx6aSWqZ4tx7aTpBDXK0/e2hhcNSTV8+CbFJtDjbb+I1mZ8lHit0Grw9GRT+6JbIrrDd8esncgBi8aBXGA=="],
"vitest/tinyexec": ["tinyexec@0.3.2", "", {}, "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA=="],
}
}

View File

@@ -0,0 +1,7 @@
[test]
coverage = true
coverageSkipTestFiles = true
coverageReporter = ["text", "lcov"]
[run]
bun = true

View File

@@ -0,0 +1,32 @@
[
{
"id": 1,
"name": "Alice",
"age": 30,
"department": "Engineering"
},
{
"id": 2,
"name": "Bob",
"age": 25,
"department": "Marketing"
},
{
"id": 3,
"name": "Charlie",
"age": 35,
"department": "Engineering"
},
{
"id": 4,
"name": "Diana",
"age": 28,
"department": "Sales"
},
{
"id": 5,
"name": "Eve",
"age": 32,
"department": "Engineering"
}
]

View File

@@ -0,0 +1,3 @@
{
"invalid": "json",
"missing": "closing brace"

View File

@@ -0,0 +1,32 @@
{
"users": {
"alice": {
"id": 1,
"name": "Alice",
"age": 30,
"department": "Engineering"
},
"bob": {
"id": 2,
"name": "Bob",
"age": 25,
"department": "Marketing"
}
},
"departments": {
"engineering": {
"name": "Engineering",
"budget": 1000000,
"headCount": 15
},
"marketing": {
"name": "Marketing",
"budget": 500000,
"headCount": 8
}
},
"config": {
"version": "1.0.0",
"environment": "test"
}
}

View File

@@ -0,0 +1,56 @@
{
"name": "@star-kitten/util",
"version": "0.0.0",
"description": "Star Kitten utilities.",
"type": "module",
"license": "MIT",
"homepage": "https://github.com/author/library#readme",
"bugs": {
"url": "https://github.com/author/library/issues"
},
"repository": {
"type": "git",
"url": "git+https://github.com/author/library.git"
},
"author": "JB <j-b-3.deviate267@passmail.net>",
"files": [
"dist"
],
"main": "./dist/index.js",
"module": "./dist/index.js",
"types": "./dist/index.d.ts",
"exports": {
"./*": "./dist/*",
"./package.json": "./package.json"
},
"publishConfig": {
"access": "public"
},
"scripts": {
"build": "tsdown",
"dev": "tsdown --watch",
"test": "bun test",
"typecheck": "tsc --noEmit",
"release": "bumpp && npm publish"
},
"devDependencies": {
"@types/bun": "^1.2.21",
"@types/lodash": "^4.17.20",
"@types/node": "^22.15.17",
"@types/node-cache": "^4.2.5",
"@types/stream-chain": "^2.1.0",
"@types/stream-json": "^1.7.8",
"bumpp": "^10.1.0",
"tsdown": "^0.14.2",
"typescript": "^5.9.2"
},
"dependencies": {
"cron-parser": "^5.3.1",
"date-fns": "^4.1.0",
"lodash": "^4.17.21",
"node-cache": "^5.1.2",
"stream-chain": "^3.4.0",
"stream-json": "^1.9.1",
"winston": "^3.17.0"
}
}

View File

@@ -0,0 +1,62 @@
import fs from 'fs';
import path from 'path';
import { Readable } from 'stream';
import { exec } from 'child_process';
export async function downloadAndExtract(url: string, outputDir: string): Promise<void> {
const response = await fetch(url);
if (!response.ok || !response.body) throw new Error(`Failed to download ${url}`);
const nodeStream = Readable.fromWeb(response.body as any);
const compressedFilePath = path.join(outputDir, 'archive.tar.xz');
const fileStream = fs.createWriteStream(compressedFilePath);
nodeStream.pipe(fileStream);
return new Promise((resolve, reject) => {
fileStream.on('finish', () => {
// Use native tar command to extract files
exec(`tar -xJf ${compressedFilePath} -C ${outputDir}`, (error, stdout, stderr) => {
if (error) {
console.error(`Extraction error: ${stderr}`);
reject(error);
} else {
console.log('Extraction complete');
// Clean up the archive file
fs.unlink(compressedFilePath, (err) => {
if (err) {
console.error(`Error removing archive: ${err.message}`);
reject(err);
} else {
console.log('Archive cleaned up');
resolve();
}
});
}
});
});
fileStream.on('error', (err) => {
console.error('File stream error', err);
reject(err);
});
});
}
// CLI execution (only runs when file is executed directly)
if (import.meta.main) {
const args = process.argv.slice(2);
if (args.length !== 2) {
console.error('Usage: bun run downloadAndExtract.ts <url> <outputDir>');
process.exit(1);
}
const [url, outputDir] = args;
if (!fs.existsSync(outputDir)) {
fs.mkdirSync(outputDir, { recursive: true });
}
downloadAndExtract(url, outputDir).catch((err) => console.error('Download failed', err));
}

View File

@@ -0,0 +1,262 @@
import { describe, it, expect, beforeEach } from 'bun:test';
import { queryJsonArray, queryJsonObject } from './json-query';
import * as path from 'path';
// Test data interfaces
interface TestUser {
id: number;
name: string;
age: number;
department: string;
}
interface TestKeyValue {
key: string;
value: any;
}
// Test file paths
const basePath = path.join(__dirname, '../fixtures/jsonQuery');
const testArrayFile = path.join(basePath, 'test-data-array.json');
const testObjectFile = path.join(basePath, 'test-data-object.json');
const testInvalidFile = path.join(basePath, 'test-data-invalid.json');
const nonExistentFile = path.join(basePath, 'non-existent.json');
describe('queryJsonArray', () => {
beforeEach(() => {
// Clear any existing cache before each test
const NodeCache = require('node-cache');
const cache = new NodeCache();
cache.flushAll();
});
it('should find a matching item in JSON array', async () => {
const result = await queryJsonArray<TestUser>(testArrayFile, (user) => user.name === 'Alice');
expect(result).not.toBeNull();
expect(result?.name).toBe('Alice');
expect(result?.id).toBe(1);
expect(result?.age).toBe(30);
expect(result?.department).toBe('Engineering');
});
it('should find the first matching item when multiple matches exist', async () => {
const result = await queryJsonArray<TestUser>(testArrayFile, (user) => user.department === 'Engineering');
expect(result).not.toBeNull();
expect(result?.name).toBe('Alice'); // First engineering employee
expect(result?.id).toBe(1);
});
it('should return null when no match is found', async () => {
const result = await queryJsonArray<TestUser>(testArrayFile, (user) => user.name === 'NonExistent');
expect(result).toBeNull();
});
it('should handle complex query conditions', async () => {
const result = await queryJsonArray<TestUser>(testArrayFile, (user) => user.age > 30 && user.department === 'Engineering');
expect(result).not.toBeNull();
expect(result?.name).toBe('Charlie');
expect(result?.age).toBe(35);
});
it('should cache results when cacheKey is provided', async () => {
const cacheKey = 'test-alice-query';
// First call should hit the file
const result1 = await queryJsonArray<TestUser>(testArrayFile, (user) => user.name === 'Alice', cacheKey);
// Second call should hit the cache (we can't directly verify this without mocking,
// but we can verify the result is consistent)
const result2 = await queryJsonArray<TestUser>(
testArrayFile,
(user) => user.name === 'Bob', // Different query, but should return cached Alice
cacheKey,
);
expect(result1).toEqual(result2);
expect(result1?.name).toBe('Alice');
expect(result2?.name).toBe('Alice');
});
it('should respect custom cache expiry', async () => {
const cacheKey = 'test-expiry-query';
const customExpiry = 1; // 1 second
const result = await queryJsonArray<TestUser>(testArrayFile, (user) => user.name === 'Bob', cacheKey, customExpiry);
expect(result).not.toBeNull();
expect(result?.name).toBe('Bob');
});
it('should handle file read errors gracefully', async () => {
await expect(queryJsonArray<TestUser>(nonExistentFile, (user) => user.name === 'Alice')).rejects.toThrow();
});
it('should handle invalid JSON gracefully', async () => {
await expect(queryJsonArray<TestUser>(testInvalidFile, (user) => user.name === 'Alice')).rejects.toThrow();
});
it('should work with numeric queries', async () => {
const result = await queryJsonArray<TestUser>(testArrayFile, (user) => user.id === 3);
expect(result).not.toBeNull();
expect(result?.name).toBe('Charlie');
expect(result?.id).toBe(3);
});
it('should work with range queries', async () => {
const result = await queryJsonArray<TestUser>(testArrayFile, (user) => user.age >= 30 && user.age <= 32);
expect(result).not.toBeNull();
expect(result?.name).toBe('Alice'); // First match: age 30
});
it('should handle empty query results', async () => {
const result = await queryJsonArray<TestUser>(
testArrayFile,
(user) => user.age > 100, // No one is over 100
);
expect(result).toBeNull();
});
});
describe('queryJsonObject', () => {
beforeEach(() => {
// Clear any existing cache before each test
const NodeCache = require('node-cache');
const cache = new NodeCache();
cache.flushAll();
});
it('should find a matching key-value pair in JSON object', async () => {
const result = await queryJsonObject<any, TestKeyValue>(testObjectFile, (item) => item.key === 'users');
expect(result).not.toBeNull();
expect(typeof result).toBe('object');
expect(result?.alice?.name).toBe('Alice');
expect(result?.bob?.name).toBe('Bob');
});
it('should find nested object values', async () => {
const result = await queryJsonObject<any, TestKeyValue>(testObjectFile, (item) => item.key === 'departments');
expect(result).not.toBeNull();
expect(result?.engineering?.name).toBe('Engineering');
expect(result?.marketing?.budget).toBe(500000);
});
it('should find specific configuration values', async () => {
const result = await queryJsonObject<any, TestKeyValue>(testObjectFile, (item) => item.key === 'config');
expect(result).not.toBeNull();
expect(result?.version).toBe('1.0.0');
expect(result?.environment).toBe('test');
});
it('should return null when no match is found', async () => {
const result = await queryJsonObject<any, TestKeyValue>(testObjectFile, (item) => item.key === 'nonexistent');
expect(result).toBeNull();
});
it('should handle complex query conditions on values', async () => {
const result = await queryJsonObject<any, TestKeyValue>(testObjectFile, (item) => {
if (item.key === 'departments' && typeof item.value === 'object') {
return Object.values(item.value).some((dept: any) => dept.budget > 800000);
}
return false;
});
expect(result).not.toBeNull();
expect(result?.engineering?.budget).toBe(1000000);
});
it('should cache results when cacheKey is provided', async () => {
const cacheKey = 'test-config-query';
// First call should hit the file
const result1 = await queryJsonObject<any, TestKeyValue>(testObjectFile, (item) => item.key === 'config', cacheKey);
// Second call should hit the cache
const result2 = await queryJsonObject<any, TestKeyValue>(
testObjectFile,
(item) => item.key === 'users', // Different query, but should return cached config
cacheKey,
);
expect(result1).toEqual(result2);
expect(result1?.version).toBe('1.0.0');
expect(result2?.version).toBe('1.0.0');
});
it('should respect custom cache expiry', async () => {
const cacheKey = 'test-object-expiry-query';
const customExpiry = 2; // 2 seconds
const result = await queryJsonObject<any, TestKeyValue>(testObjectFile, (item) => item.key === 'users', cacheKey, customExpiry);
expect(result).not.toBeNull();
expect(result?.alice?.name).toBe('Alice');
});
it('should handle file read errors gracefully', async () => {
await expect(queryJsonObject<any, TestKeyValue>(nonExistentFile, (item) => item.key === 'config')).rejects.toThrow();
});
it('should handle invalid JSON gracefully', async () => {
await expect(queryJsonObject<any, TestKeyValue>(testInvalidFile, (item) => item.key === 'config')).rejects.toThrow();
});
it('should work with value-based queries', async () => {
const result = await queryJsonObject<any, TestKeyValue>(testObjectFile, (item) => {
return typeof item.value === 'object' && item.value?.version === '1.0.0';
});
expect(result).not.toBeNull();
expect(result?.version).toBe('1.0.0');
expect(result?.environment).toBe('test');
});
it('should handle queries that check both key and value', async () => {
const result = await queryJsonObject<any, TestKeyValue>(testObjectFile, (item) => {
return item.key.startsWith('dep') && typeof item.value === 'object';
});
expect(result).not.toBeNull();
expect(result?.engineering?.name).toBe('Engineering');
});
});
describe('Edge cases and error handling', () => {
it('should handle empty file paths', async () => {
await expect(queryJsonArray<any>('', () => true)).rejects.toThrow();
});
it('should handle null query functions gracefully', async () => {
await expect(queryJsonArray<TestUser>(testArrayFile, null as any)).rejects.toThrow();
});
it('should handle undefined query functions gracefully', async () => {
await expect(queryJsonArray<TestUser>(testArrayFile, undefined as any)).rejects.toThrow();
});
it('should work without caching parameters', async () => {
const result = await queryJsonArray<TestUser>(testArrayFile, (user) => user.name === 'Diana');
expect(result).not.toBeNull();
expect(result?.name).toBe('Diana');
expect(result?.department).toBe('Sales');
});
it('should work with minimal cache configuration', async () => {
const result = await queryJsonArray<TestUser>(testArrayFile, (user) => user.name === 'Eve', 'minimal-cache');
expect(result).not.toBeNull();
expect(result?.name).toBe('Eve');
expect(result?.age).toBe(32);
});
});

View File

@@ -0,0 +1,94 @@
import fs from 'node:fs';
import { chain } from 'stream-chain';
import { parser } from 'stream-json';
import { streamArray } from 'stream-json/streamers/StreamArray';
import { streamObject } from 'stream-json/streamers/StreamObject';
import NodeCache from 'node-cache';
const cache = new NodeCache({ stdTTL: 3600 });
/**
* Queries a large JSON array file for an item matching the provided query function.
* This function streams the file to avoid loading the entire content into memory.
*
* @param filePath - The path to the JSON file containing an array of items.
* @param query - A function that takes an item and returns true if it matches the criteria.
* @returns A promise that resolves to the first matching item or null if no match is found.
*/
export function queryJsonArray<T>(
filePath: string,
query: (item: T) => boolean,
cacheKey?: string,
cacheExpiry?: number,
): Promise<T | null> {
if (cacheKey) {
const cached = cache.get<T>(cacheKey);
if (cached) {
return Promise.resolve(cached);
}
}
return new Promise((resolve, reject) => {
const pipeline = chain([fs.createReadStream(filePath), parser(), streamArray(), (data) => (query(data.value) ? data.value : null)]);
pipeline.on('data', (value) => {
if (value) {
if (cacheKey) {
cache.set(cacheKey, value, cacheExpiry || 3600);
}
resolve(value);
}
});
pipeline.on('end', () => {
resolve(null); // No match found
});
pipeline.on('error', (err) => {
reject(err);
});
});
}
/**
* Queries a large JSON object file for a value matching the provided query function.
* This function streams the file to avoid loading the entire content into memory.
*
* @param filePath - The path to the JSON file containing an object of key-value pairs.
* @param query - A function that takes a key-value pair and returns true if it matches the criteria.
* @returns A promise that resolves to the first matching value or null if no match is found.
*/
export function queryJsonObject<T, K = { key: string; value: T }>(
filePath: string,
query: (item: K) => boolean,
cacheKey?: string,
cacheExpiry?: number,
): Promise<T | null> {
if (cacheKey) {
const cached = cache.get<T>(cacheKey);
if (cached) {
return Promise.resolve(cached);
}
}
return new Promise((resolve, reject) => {
const pipeline = chain([fs.createReadStream(filePath), parser(), streamObject(), (data) => (query(data) ? data.value : null)]);
pipeline.on('data', (value) => {
if (value) {
if (cacheKey) {
cache.set(cacheKey, value, cacheExpiry || 3600);
}
resolve(value);
}
});
pipeline.on('end', () => {
resolve(null); // No match found
});
pipeline.on('error', (err) => {
reject(err);
});
});
}

View File

@@ -0,0 +1,188 @@
import { describe, test, expect, beforeEach, afterEach } from 'bun:test';
import { connectDB, get, set, del, has, clear, getDB, asyncKV } from './kv';
beforeEach(() => {
// Use in-memory database for tests
connectDB(':memory:');
});
afterEach(() => {
// Clear the database after each test
clear();
});
describe('KV Store Tests', () => {
test('set and get string value', async () => {
await set('testKey', 'testValue');
const value = await get<string>('testKey');
expect(value).toBe('testValue');
});
test('set and get object value', async () => {
const obj = { name: 'test', value: 42 };
await set('testKey', obj);
const value = await get<typeof obj>('testKey');
expect(value).toEqual(obj);
});
test('set and get boolean value', async () => {
await set('testKey', true);
const value = await get<boolean>('testKey');
expect(value).toBe(true);
});
test('set and get null value', async () => {
await set('testKey', null);
const value = await get<null>('testKey');
expect(value).toBe(null);
});
test('get non-existent key returns undefined', async () => {
const value = await get('nonExistent');
expect(value).toBeUndefined();
});
test('set with TTL and get before expiration', async () => {
await set('testKey', 'testValue', 1); // 1 second TTL
const value = await get<string>('testKey');
expect(value).toBe('testValue');
});
test('set with TTL and get after expiration', async () => {
await set('testKey', 'testValue', 0.001); // Very short TTL
await new Promise((resolve) => setTimeout(resolve, 10)); // Wait for expiration
const value = await get<string>('testKey');
expect(value).toBeUndefined();
});
test('del existing key', async () => {
await set('testKey', 'testValue');
const deleted = await del('testKey');
expect(deleted).toBe(1); // Number of rows deleted
const value = await get('testKey');
expect(value).toBeUndefined();
});
test('del non-existent key', async () => {
const deleted = await del('nonExistent');
expect(deleted).toBe(0); // del returns 0 for non-existent keys
});
test('del multiple keys', async () => {
await set('key1', 'value1');
await set('key2', 'value2');
await set('key3', 'value3');
const deleted = await del(['key1', 'key2', 'nonExistent']);
expect(deleted).toBe(2); // key1 and key2 deleted, nonExistent not
expect(await get('key1')).toBeUndefined();
expect(await get('key2')).toBeUndefined();
expect(await get('key3')).toBeDefined(); // key3 still exists
});
test('has existing key', async () => {
await set('testKey', 'testValue');
const exists = await has('testKey');
expect(exists).toBe(true);
});
test('has non-existent key', async () => {
const exists = await has('nonExistent');
expect(exists).toBe(false);
});
test('has key with expired TTL', async () => {
await set('testKey', 'testValue', 0.001);
await new Promise((resolve) => setTimeout(resolve, 10));
const exists = await has('testKey');
expect(exists).toBe(false);
});
test('clear all keys', async () => {
await set('key1', 'value1');
await set('key2', 'value2');
await clear();
const value1 = await get('key1');
const value2 = await get('key2');
expect(value1).toBeUndefined();
expect(value2).toBeUndefined();
});
test('getDB returns database instance', () => {
const db = getDB();
expect(db).toBeDefined();
expect(db.constructor.name).toBe('Database');
});
test('connectDB with custom path', () => {
connectDB(':memory:'); // Already done in beforeEach, but testing again
const db = getDB();
expect(db).toBeDefined();
});
test('set and get zero value', async () => {
await set('testKey', 0);
const value = await get<number>('testKey');
expect(value).toBe(0);
});
test('set and get empty string', async () => {
await set('testKey', '');
const value = await get<string>('testKey');
expect(value).toBe('');
});
test('set with false value', async () => {
await set('testKey', false);
const value = await get<boolean>('testKey');
expect(value).toBe(false);
});
test('JSON parsing error falls back to string', async () => {
// Manually insert invalid JSON to test fallback
const db = getDB();
db.run(`INSERT OR REPLACE INTO kvstore VALUES (?, ?, ?, ?)`, ['testKey', 'invalid json', null, new Date().toISOString()]);
const value = await get<string>('testKey');
expect(value).toBe('invalid json');
});
// Tests for asyncKV wrapper functions
test('asyncKV set and get', async () => {
await asyncKV.set('asyncKey', 'asyncValue');
const value = await asyncKV.get<string>('asyncKey');
expect(value).toBe('asyncValue');
});
test('asyncKV del', async () => {
await asyncKV.set('asyncKey', 'asyncValue');
const deleted = await asyncKV.del('asyncKey');
expect(deleted).toBe(1);
const value = await asyncKV.get('asyncKey');
expect(value).toBeUndefined();
});
test('asyncKV has', async () => {
await asyncKV.set('asyncKey', 'asyncValue');
const exists = await asyncKV.has('asyncKey');
expect(exists).toBe(true);
await asyncKV.del('asyncKey');
const existsAfter = await asyncKV.has('asyncKey');
expect(existsAfter).toBe(false);
});
test('asyncKV clear', async () => {
await asyncKV.set('key1', 'value1');
await asyncKV.set('key2', 'value2');
await asyncKV.clear();
const value1 = await asyncKV.get('key1');
const value2 = await asyncKV.get('key2');
expect(value1).toBeUndefined();
expect(value2).toBeUndefined();
});
test('asyncKV del multiple keys', async () => {
await asyncKV.set('key1', 'value1');
await asyncKV.set('key2', 'value2');
const deleted = await asyncKV.del(['key1', 'key2']);
expect(deleted).toBe(2);
});
});

138
packages/util/src/kv.ts Normal file
View File

@@ -0,0 +1,138 @@
/**
* A simple key-value store using Bun's SQLite support.
* Supports string, object, boolean, and null values.
* Values can have an optional TTL (time-to-live) in seconds.
*
* Exports a set of async functions for easy replacement
* with other storage solutions in the future.
*
* Usage:
* import { get, set, del, has, clear, connectDB } from './kv';
*
* await connectDB(); // Optional: specify a custom database file path
* await set('myKey', 'myValue', 60); // Set a key with a value and TTL
* const value = await get('myKey'); // Get the value by key
* await del('myKey'); // Delete the key
* const exists = await has('myKey'); // Check if the key exists
* await clear(); // Clear all keys
*/
import { Database } from 'bun:sqlite';
interface KVItem {
key: string;
value: string;
ttl: number | null; // Unix timestamp in milliseconds
}
let db: Database | null = null;
export function connectDB(dbPath: string = process.env.STAR_KITTEN_KV_DB_PATH || ':memory:') {
db = new Database(dbPath);
db.run(
`CREATE TABLE IF NOT EXISTS kvstore (
key TEXT PRIMARY KEY,
value TEXT,
ttl INTEGER,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
UNIQUE(key)
)`,
);
}
export function getDB() {
if (!db) {
connectDB();
}
return db!;
}
export function get<T>(key: string): T | undefined {
const db = getDB();
const query = db.prepare(`SELECT value, ttl FROM kvstore WHERE key = ?`);
const row = query.get(key) as KVItem | undefined;
if (!row) return undefined;
if (row.ttl && Date.now() > row.ttl) {
del(key);
return undefined;
}
try {
return JSON.parse(row.value) as T;
} catch (error) {
return row.value as unknown as T;
}
}
export function del(key: string | string[]): number {
try {
if (typeof key === 'string') {
const result = getDB().run(`DELETE FROM kvstore WHERE key = ?`, [key]);
return result.changes;
} else {
const keys = key.map(() => '?').join(', ');
const query = `DELETE FROM kvstore WHERE key IN (${keys})`;
const result = getDB().run(query, key);
return result.changes;
}
} catch (error) {
return 0;
}
}
export function set<T>(key: string, value: T, ttlInSeconds: number | null = null): boolean {
const ttl = ttlInSeconds ? Date.now() + ttlInSeconds * 1000 : null;
try {
const stmt = getDB().run(`INSERT OR REPLACE INTO kvstore VALUES (?, ?, ?, ?)`, [
key,
JSON.stringify(value),
ttl,
new Date().toISOString(),
]);
return true;
} catch (error) {
return false;
}
}
export function has(key: string): boolean {
const db = getDB();
const query = db.prepare(`SELECT ttl FROM kvstore WHERE key = ?`);
const row = query.get(key) as { ttl?: number } | undefined;
if (!row) return false;
if (row.ttl && Date.now() > row.ttl) {
del(key);
return false;
}
return true;
}
export function purgeOlderThan(ageInSeconds: number): number {
const cutoff = Date.now() - ageInSeconds * 1000;
const result = getDB().run(`DELETE FROM kvstore WHERE created_at < ?`, [new Date(cutoff).toISOString()]);
return result.changes;
}
export function clear(): void {
getDB().run(`DELETE FROM kvstore`);
}
export default {
get,
set,
delete: del,
del,
has,
clear,
};
export const asyncKV = {
get: <T>(key: string): Promise<T | undefined> => Promise.resolve(get<T>(key)),
set: <T>(key: string, value: T, ttlInSeconds: number | null = null): Promise<boolean> =>
Promise.resolve(set<T>(key, value, ttlInSeconds)),
delete: (key: string | string[]): Promise<number> => Promise.resolve(del(key)),
del: (key: string | string[]): Promise<number> => Promise.resolve(del(key)),
has: (key: string): Promise<boolean> => Promise.resolve(has(key)),
clear: (): Promise<void> => Promise.resolve(clear()),
};

106
packages/util/src/logger.ts Normal file
View File

@@ -0,0 +1,106 @@
import { createLogger, format, transports } from 'winston';
const development = 'development';
const production = 'production';
const LOG_LEVEL = 'debug';
const NODE_ENV = process.env.NODE_ENV || development;
const DEBUG = process.env.DEBUG || false;
export function init(name: string = 'App') {
const jsonFormat = format.combine(format.timestamp(), format.json());
const logger = createLogger({
level: LOG_LEVEL,
format: format.json(),
defaultMeta: { service: name },
});
if (NODE_ENV !== development) {
logger.add(
new transports.Console({
format: jsonFormat,
}),
);
}
if (NODE_ENV !== production) {
const simpleFormat = format.printf(({ level, message, label, timestamp, stack }) => {
return `${timestamp} [${label || 'App'}] ${level}: ${message}${DEBUG && stack ? `\n${stack}` : ''}`;
});
logger.add(
new transports.Console({
format: format.combine(format.colorize(), format.timestamp(), simpleFormat),
}),
);
}
enum LogLevel {
DEBUG = 0,
INFO = 1,
WARN = 2,
ERROR = 3,
NONE = 4,
}
function logLevelValue(level: string) {
switch (level) {
case 'debug':
return 0;
case 'info':
return 1;
case 'warn':
return 2;
case 'error':
return 3;
default:
return 4;
}
}
const debug = (message: string, ...args: any[]) => {
if (logLevelValue(LOG_LEVEL) > LogLevel.DEBUG) return;
let e = new Error();
let frame = e.stack?.split('\n')[2]; // change to 3 for grandparent func
let lineNumber = frame?.split(':').reverse()[1];
let functionName = frame?.split(' ')[5];
let file = frame?.match(/src\/[a-zA-Z.:0-9]*/)?.[0];
logger.child({ label: functionName, lineNumber, file }).debug(message, ...args);
};
const info = (message: string, ...args: any[]) => {
if (logLevelValue(LOG_LEVEL) > LogLevel.INFO) return;
let e = new Error();
let frame = e.stack?.split('\n')[2]; // change to 3 for grandparent func
let lineNumber = frame?.split(':').reverse()[1];
let functionName = frame?.split(' ')[5];
let file = frame?.match(/src\/[a-zA-Z.:0-9]*/)?.[0];
logger.child({ label: functionName, lineNumber, file }).info(message, ...args);
};
const warn = (message: string, ...args: any[]) => {
if (logLevelValue(LOG_LEVEL) > LogLevel.WARN) return;
let e = new Error();
let frame = e.stack?.split('\n')[2]; // change to 3 for grandparent func
let lineNumber = frame?.split(':').reverse()[1];
let functionName = frame?.split(' ')[5];
let file = frame?.match(/src\/[a-zA-Z.:0-9]*/)?.[0];
logger.child({ label: functionName, lineNumber, file }).warn(message, ...args);
};
const error = (message: string, ...args: any[]) => {
if (logLevelValue(LOG_LEVEL) > LogLevel.ERROR) return;
let e = new Error();
let frame = e.stack?.split('\n')[2]; // change to 3 for grandparent func
let lineNumber = frame?.split(':').reverse()[1];
let functionName = frame?.split(' ')[5];
let file = frame?.match(/src\/[a-zA-Z.:0-9]*/)?.[0];
logger.child({ label: functionName, lineNumber, file }).error(message, ...args);
};
console.log = info;
console.debug = debug;
console.info = info;
console.warn = warn;
console.error = error;
return logger;
}

View File

@@ -0,0 +1,118 @@
import { describe, it, expect } from 'bun:test';
import { createReactiveState } from './reactive-state';
describe('createReactiveState', () => {
it('should call callback on property change with new and old state', async () => {
const initial = { count: 0 };
let called = false;
let newState: any, oldState: any;
const [state, subscribe] = createReactiveState(initial);
subscribe((n, o) => {
called = true;
newState = n;
oldState = o;
});
state.count = 1;
await new Promise((resolve) => setTimeout(resolve, 0)); // wait for microtask
expect(called).toBe(true);
expect(newState.count).toBe(1);
expect(oldState.count).toBe(0);
});
it('should maintain reactivity when assigned to another variable', async () => {
const initial = { count: 0 };
let called = false;
const [state, subscribe] = createReactiveState(initial);
subscribe((n, o) => {
called = true;
});
const newState = state;
newState.count = 1;
await new Promise((resolve) => setTimeout(resolve, 0)); // wait for microtask
expect(called).toBe(true);
expect(newState.count).toBe(1);
});
it('should be reactive when new properties are added', async () => {
const initial = { count: 0 };
let called = 0;
const [state, subscribe] = createReactiveState(initial);
subscribe((n, o) => {
called++;
});
state.newproperty = 'hello';
await new Promise((resolve) => setTimeout(resolve, 0)); // wait for microtask
state.secondProperty = 'world';
await new Promise((resolve) => setTimeout(resolve, 0)); // wait for microtask
expect(called).toBe(2);
expect(state.count).toBe(0);
expect(state.newproperty).toBe('hello');
expect(state.secondProperty).toBe('world');
});
it('should batch multiple changes into one callback call', async () => {
const initial = { a: 1, b: 2 };
let callCount = 0;
let finalNewState: any;
const [state, subscribe] = createReactiveState(initial);
subscribe((n) => {
callCount++;
finalNewState = n;
});
state.a = 10;
state.b = 20;
await new Promise((resolve) => setTimeout(resolve, 0));
expect(callCount).toBe(1);
expect(finalNewState.a).toBe(10);
expect(finalNewState.b).toBe(20);
});
it('should support nested object changes', async () => {
const initial = { nested: { value: 1 } };
let called = false;
let newState: any;
const [state, subscribe] = createReactiveState(initial);
subscribe((n) => {
called = true;
newState = n;
});
state.nested.value = 2;
await new Promise((resolve) => setTimeout(resolve, 0));
expect(called).toBe(true);
expect(newState.nested.value).toBe(2);
});
it('should allow unsubscribing from callbacks', async () => {
const initial = { count: 0 };
let callCount = 0;
const [state, subscribe] = createReactiveState(initial);
const unsubscribe = subscribe(() => {
callCount++;
});
state.count = 1;
await new Promise((resolve) => setTimeout(resolve, 0));
expect(callCount).toBe(1);
unsubscribe();
state.count = 2;
await new Promise((resolve) => setTimeout(resolve, 0));
expect(callCount).toBe(1); // should not increase
});
it('should support multiple subscribers', async () => {
const initial = { count: 0 };
let callCount1 = 0,
callCount2 = 0;
const [state, subscribe] = createReactiveState(initial);
const unsubscribe1 = subscribe(() => callCount1++);
const unsubscribe2 = subscribe(() => callCount2++);
state.count = 1;
await new Promise((resolve) => setTimeout(resolve, 0));
expect(callCount1).toBe(1);
expect(callCount2).toBe(1);
unsubscribe1();
state.count = 2;
await new Promise((resolve) => setTimeout(resolve, 0));
expect(callCount1).toBe(1);
expect(callCount2).toBe(2);
});
});

View File

@@ -0,0 +1,73 @@
import cloneDeep from 'lodash/cloneDeep';
/**
* Creates a reactive state object that batches changes and notifies all subscribers with the full old and new state.
*
* @param initialState - The initial state object
* @returns Object with { state: reactive proxy, subscribe: function to add callbacks }
*
* @example
* ```typescript
* const { state, subscribe } = createBatchedReactiveState({ count: 0, user: { name: 'Alice' } });
*
* const unsubscribe1 = subscribe((newState, oldState) => {
* console.log('Subscriber 1:', oldState, '->', newState);
* });
*
* const unsubscribe2 = subscribe((newState, oldState) => {
* console.log('Subscriber 2:', newState.count);
* });
*
* state.count = 1; // Triggers both subscribers once
* state.user.name = 'Bob'; // Triggers both subscribers once (batched)
*
* unsubscribe1(); // Remove first subscriber
* state.count = 2; // Only subscriber 2 is called
* ```
*/
export function createReactiveState<T extends object>(initialState: T) {
const callbacks = new Set<(newState: T, oldState: T) => void>();
let isBatching = false;
let oldState: T | null = null;
let scheduled = false;
const rootState = cloneDeep(initialState);
function createReactiveObject(obj: any): any {
return new Proxy(obj, {
get(target, property, receiver) {
const value = Reflect.get(target, property, receiver);
return typeof value === 'object' && value !== null ? createReactiveObject(value) : value;
},
set(target, property, value, receiver) {
if (!isBatching) {
isBatching = true;
oldState = cloneDeep(rootState);
}
const success = Reflect.set(target, property, value, receiver);
if (success) {
if (!scheduled) {
scheduled = true;
queueMicrotask(() => {
callbacks.forEach((cb) => cb(rootState, oldState!));
isBatching = false;
oldState = null;
scheduled = false;
});
}
}
return success;
},
});
}
const state = createReactiveObject(rootState);
return [
state,
function subscribe(callback: (newState: T, oldState: T) => void) {
callbacks.add(callback);
return () => callbacks.delete(callback);
},
] as const;
}

View File

@@ -0,0 +1 @@
export * from './scheduler.service';

View File

@@ -0,0 +1 @@
export * from './workerMessageHandler';

View File

@@ -0,0 +1,12 @@
import type { Job } from '../types';
export default function createWorkerMessageHandler(
workerName: string,
executor: (_: { jobId: number; job: Job }) => Promise<void>,
) {
return async (event: MessageEvent) => {
const { jobId, job } = event.data as { jobId: number; job: Job };
console.log(`${workerName} received job ${job.name} with data ${JSON.stringify(job.data)}`);
await executor({ jobId, job });
};
}

View File

@@ -0,0 +1,117 @@
import { Database } from 'bun:sqlite';
import { serialize, deserialize } from 'node:v8';
export interface QueueItem {
id: number;
jobId: string;
payload: any;
status: 'pending' | 'processing' | 'completed' | 'failed' | 'cancelled';
created_at: Date;
execute_at: Date;
completed_at?: Date;
failed_at?: Date;
cancelled_at?: Date;
}
export class Queue<DATA = any> {
private db: Database;
constructor(dbPath: string) {
this.db = new Database(dbPath, { create: true });
this.db.exec('PRAGMA journal_mode = WAL;');
this.db.run(`
CREATE TABLE IF NOT EXISTS queue_items (
id INTEGER PRIMARY KEY AUTOINCREMENT,
jobId TEXT NOT NULL,
payload BLOB NOT NULL,
status TEXT NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
execute_at DATETIME DEFAULT CURRENT_TIMESTAMP,
completed_at DATETIME,
failed_at DATETIME,
cancelled_at DATETIME
);
CREATE INDEX IF NOT EXISTS idx_jobId ON queue_items (jobId);
CREATE INDEX IF NOT EXISTS idx_status ON queue_items (status);
CREATE INDEX IF NOT EXISTS idx_execute_at ON queue_items (execute_at);
`);
}
enqueue(jobId: string, payload: DATA, delay?: number | Date) {
const executeAt = delay instanceof Date ? delay : new Date(Date.now() + (delay || 0));
this.db.run(
`INSERT INTO queue_items (jobId, payload, status, execute_at) VALUES (?, ?, ?, ?)`,
jobId,
serialize(payload) as any,
'pending',
executeAt.toISOString(),
);
}
dequeue() {
const stmt = this.db.prepare(`
UPDATE queue_items
SET status = 'processing'
WHERE id = (
SELECT id FROM queue_items
WHERE status = 'pending' AND execute_at <= ?
ORDER BY execute_at ASC, created_at ASC
LIMIT 1
)
RETURNING *
`);
const result = stmt.get(new Date().toISOString()) as QueueItem | null;
return result ? { ...result, payload: deserialize(result.payload) as DATA } : null;
}
complete(id: number) {
const stmt = this.db.prepare(`
UPDATE queue_items
SET status = 'completed', completed_at = ?
WHERE id = ?
`);
stmt.run(new Date().toISOString(), id);
}
fail(id: number) {
const stmt = this.db.prepare(`
UPDATE queue_items
SET status = 'failed', failed_at = ?
WHERE id = ?
`);
stmt.run(new Date().toISOString(), id);
}
getNextExecutionTime() {
const stmt = this.db.prepare(`
SELECT execute_at
FROM queue_items
WHERE status = 'pending'
ORDER BY execute_at ASC, created_at ASC
LIMIT 1
`);
const result = stmt.get() as { execute_at: string } | null;
return result ? new Date(result.execute_at) : null;
}
cancel(jobId: string) {
const stmt = this.db.prepare(`
UPDATE queue_items
SET status = 'cancelled', cancelled_at = ?
WHERE jobId = ?
AND status = 'pending'
`);
stmt.run(new Date().toISOString(), jobId);
}
isEmpty() {
const stmt = this.db.prepare(`
SELECT COUNT(*) as count
FROM queue_items
WHERE status = 'pending'
`);
const result = stmt.get() as { count: number };
return result.count === 0;
}
}

View File

@@ -0,0 +1,116 @@
import cronParser from 'cron-parser';
import { Queue } from './queue';
import { JobType, type Job } from './types';
let queue: Queue<Job>;
const workers: { [key: string]: Worker } = {};
const MAX_DEQUEUE_DELAY = parseInt(process.env.MAX_DEQUEUE_DELAY || '1000');
const workerMap: { [key: string]: string } = {
[JobType.EMAIL]: `${import.meta.dir}/workers/email.worker.ts`,
};
let paused = true;
let isRunning = false;
export const init = async () => {
queue = new Queue(process.env.QUEUE_DB_PATH || 'queue.db');
};
const getWorker = (type: string) => {
const worker = workers[type];
if (!worker) {
console.log(`Worker not found for job ${type}, creating new worker`);
return createWorker(type, workerMap[type]);
}
return worker;
};
const createWorker = (type: string, path: string) => {
console.debug(`Creating worker for job ${type} at path ${path}`);
// @ts-expect-error
const worker = new Worker(path, { type, smol: true });
workers[type] = worker;
return worker;
};
const runQueue = () => {
if (paused) {
console.debug('Scheduler paused. Not running jobs.');
isRunning = false;
return;
}
isRunning = true;
const item = queue.dequeue();
if (item) {
getWorker(item.payload.type).postMessage({
id: item.id,
job: item.payload,
});
return runQueue();
}
const nextExecutionTime = queue.getNextExecutionTime();
if (!nextExecutionTime && queue.isEmpty()) {
console.debug('No jobs to run. Exiting scheduler.');
shutdown();
return;
}
const delay = Math.min(
nextExecutionTime ? nextExecutionTime.getTime() - Date.now() : MAX_DEQUEUE_DELAY,
MAX_DEQUEUE_DELAY,
);
console.debug(`No jobs to run now. Next execution time is ${new Date(Date.now() + delay)}`);
setTimeout(runQueue, delay);
};
export const start = () => {
if (queue.isEmpty()) {
console.debug('No jobs to run. Exiting scheduler.');
return;
}
paused = false;
runQueue();
};
export const pause = () => {
paused = true;
};
export const resume = () => {
paused = false;
if (!isRunning) {
runQueue();
}
};
export function shutdown() {
try {
for (const key in workers) {
workers[key].terminate();
delete workers[key];
}
isRunning = false;
paused = true;
} catch (error) {
console.error(`Failed to shutdown workers.`, error);
}
}
export const schedule = (job: Job) => {
if (!job.start && job.repeat) {
// If job is set to repeat, get the next execution time based on the cron pattern if no start time is provided
const interval = cronParser.parse(job.repeat);
job.start = interval.next().getTime();
}
const delay = job.start ? job.start - Date.now() : 0;
queue.enqueue(job.id, job, Math.max(delay, 0));
if (!isRunning) {
start();
}
};
export const unschedule = (jobId: string) => {
console.debug(`Unscheduling job ${jobId}`);
return queue.cancel(jobId);
};

View File

@@ -0,0 +1 @@
export * from './jobs';

View File

@@ -0,0 +1,22 @@
export enum JobType {
EMAIL = 'email',
}
export interface Job {
id: string;
name: string;
type: JobType;
start: number;
repeat?: string;
data: any;
}
export interface EmailJob extends Job {
type: JobType.EMAIL;
data: {
to: string;
from: string;
subject: string;
body: string;
};
}

View File

@@ -0,0 +1,11 @@
import createWorkerMessageHandler from '../lib/workerMessageHandler';
import type { EmailJob } from '../types';
const sendMail = async ({ jobId, job: { name, data } }: { jobId: number; job: EmailJob }) => {
console.log(`Sending mail for job ${name} with data ${JSON.stringify(data)}`);
self.postMessage({ name, data, status: 'completed' });
};
declare var self: Worker;
self.onmessage = createWorkerMessageHandler('email', sendMail);

View File

@@ -0,0 +1 @@
export * from './email.worker';

View File

@@ -0,0 +1,54 @@
import { describe, it, expect } from "bun:test";
import { truncateText, formatNumberToShortForm } from "./text";
describe("truncateText", () => {
it("should truncate text longer than the specified length", () => {
const input = "This is a long text that needs to be truncated.";
const result = truncateText(input, 10);
expect(result).toBe("This is a ...");
});
it("should not truncate text shorter than the specified length", () => {
const input = "Short text";
const result = truncateText(input, 20);
expect(result).toBe(input);
});
it("should use the default length of 1000 if no length is specified", () => {
const input = "A".repeat(1001);
const result = truncateText(input);
expect(result).toBe("A".repeat(1000) + "...");
});
});
describe("formatNumberToShortForm", () => {
it("should format numbers in billions with 'b' suffix", () => {
const result = formatNumberToShortForm(1_500_000_000);
expect(result).toMatch(/1\.5.*b/);
});
it("should format numbers in millions with 'M' suffix", () => {
const result = formatNumberToShortForm(2_300_000);
expect(result).toMatch(/2\.3.*M/);
});
it("should format numbers in thousands with 'k' suffix", () => {
const result = formatNumberToShortForm(12_000);
expect(result).toMatch(/12.*k/);
});
it("should format numbers below 1000 without a suffix", () => {
const result = formatNumberToShortForm(999);
expect(result).toBe("999");
});
it("should handle negative numbers correctly", () => {
const result = formatNumberToShortForm(-1_200_000);
expect(result).toMatch(/-1\.2.*M/);
});
it("should respect the specified locale", () => {
const result = formatNumberToShortForm(1_234_567, "de-DE");
expect(result).toMatch(/1,23.*M/);
});
});

62
packages/util/src/text.ts Normal file
View File

@@ -0,0 +1,62 @@
export function truncateText(input: string, length: number = 1000): string {
return input.length > length ? input.substring(0, length) + '...' : input;
}
export function formatNumberToShortForm(number: number, locale: string = 'en-uS') {
let suffix = '';
let value = number;
if (Math.abs(number) >= 1e9) {
value = number / 1e9;
suffix = 'b';
} else if (Math.abs(number) >= 1e6) {
value = number / 1e6;
suffix = 'M';
} else if (Math.abs(number) >= 1e3) {
value = number / 1e3;
suffix = 'k';
}
// Format the number to have up to 4 significant digits
const formattedValue = new Intl.NumberFormat(locale, {
maximumSignificantDigits: 4,
minimumSignificantDigits: 3,
minimumFractionDigits: 0,
maximumFractionDigits: 2,
}).format(value);
return `${formattedValue}${suffix}`;
}
export function normalize(value: string) {
return value
.toString()
.normalize('NFD')
.replace(/[\u0300-\u036f]/g, '')
.replace(/[^a-zA-Z\s\d]/g, '')
.trim();
}
export function toTitleCase(value: string) {
return value.replace(/\w\S*/g, (txt) => txt.charAt(0).toUpperCase() + txt.substr(1).toLowerCase());
}
export function escapeMarkdown(text: string) {
return text.replace(/([\\_*~`|])/g, '\\$1');
}
export function escapeCodeBlock(text: string) {
return text.replace(/```/g, '`\u200b``');
}
export function escapeInlineCode(text: string) {
return text.replace(/`/g, '\u200b`');
}
export function escapeSpoiler(text: string) {
return text.replace(/\|\|/g, '|\u200b|');
}
export function escapeAll(text: string) {
return escapeMarkdown(escapeCodeBlock(escapeInlineCode(escapeSpoiler(text))));
}

View File

@@ -0,0 +1,286 @@
import { describe, it, expect } from "bun:test";
import { msToDuration, secondsToDuration } from "./time";
describe("msToDuration", () => {
it("should convert milliseconds to duration with all units", () => {
// 1 day, 2 hours, 3 minutes, 4 seconds = 93784000 ms
const ms = 1 * 24 * 60 * 60 * 1000 + 2 * 60 * 60 * 1000 + 3 * 60 * 1000 + 4 * 1000;
const result = msToDuration(ms);
expect(result).toEqual({
days: 1,
hours: 2,
minutes: 3,
seconds: 4
});
});
it("should handle zero milliseconds", () => {
const result = msToDuration(0);
expect(result).toEqual({
days: 0,
hours: 0,
minutes: 0,
seconds: 0
});
});
it("should handle only seconds", () => {
const result = msToDuration(5000); // 5 seconds
expect(result).toEqual({
days: 0,
hours: 0,
minutes: 0,
seconds: 5
});
});
it("should handle only minutes", () => {
const result = msToDuration(2 * 60 * 1000); // 2 minutes
expect(result).toEqual({
days: 0,
hours: 0,
minutes: 2,
seconds: 0
});
});
it("should handle only hours", () => {
const result = msToDuration(3 * 60 * 60 * 1000); // 3 hours
expect(result).toEqual({
days: 0,
hours: 3,
minutes: 0,
seconds: 0
});
});
it("should handle only days", () => {
const result = msToDuration(2 * 24 * 60 * 60 * 1000); // 2 days
expect(result).toEqual({
days: 2,
hours: 0,
minutes: 0,
seconds: 0
});
});
it("should handle partial seconds (floor down)", () => {
const result = msToDuration(1500); // 1.5 seconds
expect(result).toEqual({
days: 0,
hours: 0,
minutes: 0,
seconds: 1
});
});
it("should handle large values", () => {
// 365 days, 5 hours, 30 minutes, 45 seconds
const ms = 365 * 24 * 60 * 60 * 1000 + 5 * 60 * 60 * 1000 + 30 * 60 * 1000 + 45 * 1000;
const result = msToDuration(ms);
expect(result).toEqual({
days: 365,
hours: 5,
minutes: 30,
seconds: 45
});
});
it("should handle rollover correctly", () => {
// 25 hours should be 1 day, 1 hour
const ms = 25 * 60 * 60 * 1000;
const result = msToDuration(ms);
expect(result).toEqual({
days: 1,
hours: 1,
minutes: 0,
seconds: 0
});
});
it("should handle 61 minutes correctly", () => {
// 61 minutes should be 1 hour, 1 minute
const ms = 61 * 60 * 1000;
const result = msToDuration(ms);
expect(result).toEqual({
days: 0,
hours: 1,
minutes: 1,
seconds: 0
});
});
it("should handle negative values", () => {
const result = msToDuration(-5000);
expect(result).toEqual({
days: -1,
hours: -1,
minutes: -1,
seconds: -5
});
});
});
describe("secondsToDuration", () => {
it("should convert seconds to duration with all units", () => {
// 1 day, 2 hours, 3 minutes, 4 seconds = 93784 seconds
const seconds = 1 * 24 * 60 * 60 + 2 * 60 * 60 + 3 * 60 + 4;
const result = secondsToDuration(seconds);
expect(result).toEqual({
days: 1,
hours: 2,
minutes: 3,
seconds: 4
});
});
it("should handle zero seconds", () => {
const result = secondsToDuration(0);
expect(result).toEqual({
days: 0,
hours: 0,
minutes: 0,
seconds: 0
});
});
it("should handle only seconds", () => {
const result = secondsToDuration(45);
expect(result).toEqual({
days: 0,
hours: 0,
minutes: 0,
seconds: 45
});
});
it("should handle only minutes", () => {
const result = secondsToDuration(5 * 60); // 5 minutes
expect(result).toEqual({
days: 0,
hours: 0,
minutes: 5,
seconds: 0
});
});
it("should handle only hours", () => {
const result = secondsToDuration(4 * 60 * 60); // 4 hours
expect(result).toEqual({
days: 0,
hours: 4,
minutes: 0,
seconds: 0
});
});
it("should handle only days", () => {
const result = secondsToDuration(3 * 24 * 60 * 60); // 3 days
expect(result).toEqual({
days: 3,
hours: 0,
minutes: 0,
seconds: 0
});
});
it("should handle decimal seconds (floor down)", () => {
const result = secondsToDuration(59.7);
expect(result).toEqual({
days: 0,
hours: 0,
minutes: 0,
seconds: 59
});
});
it("should handle large values", () => {
// 100 days, 12 hours, 45 minutes, 30 seconds
const seconds = 100 * 24 * 60 * 60 + 12 * 60 * 60 + 45 * 60 + 30;
const result = secondsToDuration(seconds);
expect(result).toEqual({
days: 100,
hours: 12,
minutes: 45,
seconds: 30
});
});
it("should handle rollover correctly", () => {
// 25 hours should be 1 day, 1 hour
const seconds = 25 * 60 * 60;
const result = secondsToDuration(seconds);
expect(result).toEqual({
days: 1,
hours: 1,
minutes: 0,
seconds: 0
});
});
it("should handle 61 minutes correctly", () => {
// 61 minutes should be 1 hour, 1 minute
const seconds = 61 * 60;
const result = secondsToDuration(seconds);
expect(result).toEqual({
days: 0,
hours: 1,
minutes: 1,
seconds: 0
});
});
it("should handle 61 seconds correctly", () => {
// 61 seconds should be 1 minute, 1 second
const result = secondsToDuration(61);
expect(result).toEqual({
days: 0,
hours: 0,
minutes: 1,
seconds: 1
});
});
it("should handle negative values", () => {
const result = secondsToDuration(-3665); // -1 hour, -1 minute, -5 seconds
expect(result).toEqual({
days: -1,
hours: -2,
minutes: -2,
seconds: -5
});
});
it("should match msToDuration for equivalent values", () => {
const seconds = 3665; // 1 hour, 1 minute, 5 seconds
const ms = seconds * 1000;
const fromSeconds = secondsToDuration(seconds);
const fromMs = msToDuration(ms);
expect(fromSeconds).toEqual(fromMs);
});
});

19
packages/util/src/time.ts Normal file
View File

@@ -0,0 +1,19 @@
import { type Duration } from 'date-fns';
export function msToDuration(ms: number): Duration {
const seconds = Math.floor((ms / 1000) % 60);
const minutes = Math.floor((ms / (1000 * 60)) % 60);
const hours = Math.floor((ms / (1000 * 60 * 60)) % 24);
const days = Math.floor(ms / (1000 * 60 * 60 * 24));
return { days, hours, minutes, seconds };
}
export function secondsToDuration(secondsInput: number): Duration {
const seconds = Math.floor(secondsInput % 60);
const minutes = Math.floor((secondsInput / 60) % 60);
const hours = Math.floor((secondsInput / 3600) % 24);
const days = Math.floor(secondsInput / 86400);
return { days, hours, minutes, seconds };
}

View File

@@ -0,0 +1,27 @@
{
"compilerOptions": {
"lib": ["ESNext", "DOM"],
"target": "ESNext",
"module": "ESNext",
"moduleDetection": "force",
"jsx": "react-jsx",
"jsxImportSource": "@star-kitten/discord",
"allowJs": true,
"noImplicitAny": false,
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"verbatimModuleSyntax": true,
"noEmit": true,
"strict": true,
"skipLibCheck": true,
"noFallthroughCasesInSwitch": true,
"noUnusedLocals": false,
"noUnusedParameters": false,
"noPropertyAccessFromIndexSignature": false,
"experimentalDecorators": true,
"emitDecoratorMetadata": true,
"paths": {
"@/*": ["./src/*"]
}
}
}

View File

@@ -0,0 +1,10 @@
import { defineConfig } from 'tsdown';
export default defineConfig([
{
entry: ['./src/**/*.ts', '!./src/**/*.test.ts'],
platform: 'node',
dts: true,
external: ['bun:sqlite'],
},
]);