@slopware/sloppy-linux-x64 0.1.0-alpha.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +201 -0
- package/README.md +5 -0
- package/bin/sloppy +0 -0
- package/bin/sloppyc +0 -0
- package/docs/KNOWN_LIMITATIONS.md +16 -0
- package/docs/LICENSES.md +6 -0
- package/docs/NOTICE.md +8 -0
- package/examples/README.md +140 -0
- package/examples/auth-api/README.md +20 -0
- package/examples/auth-api/app.js +61 -0
- package/examples/auth-api/appsettings.json +7 -0
- package/examples/auth-api/sloppy.json +5 -0
- package/examples/cache-basic/README.md +9 -0
- package/examples/cache-basic/app.js +32 -0
- package/examples/cache-hybrid-postgres/README.md +10 -0
- package/examples/cache-hybrid-postgres/app.js +27 -0
- package/examples/cache-output-api/README.md +10 -0
- package/examples/cache-output-api/app.js +35 -0
- package/examples/codec-base64-hex/README.md +14 -0
- package/examples/codec-base64-hex/app.js +15 -0
- package/examples/codec-checksums/README.md +15 -0
- package/examples/codec-checksums/app.js +8 -0
- package/examples/codec-compression/README.md +13 -0
- package/examples/codec-compression/app.js +9 -0
- package/examples/codec-streaming-compression/README.md +19 -0
- package/examples/codec-streaming-compression/app.js +16 -0
- package/examples/codec-text-binary/README.md +16 -0
- package/examples/codec-text-binary/app.js +17 -0
- package/examples/compiler-hello/README.md +71 -0
- package/examples/compiler-hello/app.js +7 -0
- package/examples/compiler-hello/expected/app.js +8 -0
- package/examples/compiler-hello/expected/app.js.map +53 -0
- package/examples/compiler-hello/expected/app.plan.json +229 -0
- package/examples/compiler-hello/expected/routes.slrt +0 -0
- package/examples/config-basic/README.md +13 -0
- package/examples/config-basic/app.js +13 -0
- package/examples/config-basic/appsettings.json +7 -0
- package/examples/config-secrets-redaction/README.md +9 -0
- package/examples/config-secrets-redaction/app.js +9 -0
- package/examples/config-secrets-redaction/appsettings.json +5 -0
- package/examples/config-strict-mode/README.md +7 -0
- package/examples/config-strict-mode/app.js +10 -0
- package/examples/config-strict-mode/appsettings.json +7 -0
- package/examples/configured-api/README.md +38 -0
- package/examples/configured-api/app.js +12 -0
- package/examples/configured-api/appsettings.Development.json +5 -0
- package/examples/configured-api/appsettings.json +6 -0
- package/examples/configured-api/sloppy.json +5 -0
- package/examples/core-config-secrets/README.md +10 -0
- package/examples/core-config-secrets/app.js +15 -0
- package/examples/core-fs-time-codec/README.md +9 -0
- package/examples/core-fs-time-codec/app.js +8 -0
- package/examples/core-network-time-codec/README.md +11 -0
- package/examples/core-network-time-codec/app.js +20 -0
- package/examples/core-policy-audit/README.md +7 -0
- package/examples/core-policy-audit/app.js +22 -0
- package/examples/core-process-time-codec/README.md +8 -0
- package/examples/core-process-time-codec/app.js +28 -0
- package/examples/core-worker-time/README.md +8 -0
- package/examples/core-worker-time/app.js +17 -0
- package/examples/crypto-hash-hmac/README.md +17 -0
- package/examples/crypto-hash-hmac/app.js +29 -0
- package/examples/crypto-password/README.md +21 -0
- package/examples/crypto-password/app.js +12 -0
- package/examples/crypto-random-token/README.md +16 -0
- package/examples/crypto-random-token/app.js +12 -0
- package/examples/crypto-secret-constant-time/README.md +21 -0
- package/examples/crypto-secret-constant-time/app.js +15 -0
- package/examples/data-foundation/README.md +39 -0
- package/examples/data-foundation/app.js +63 -0
- package/examples/dependency-graph/README.md +19 -0
- package/examples/dependency-graph/fixtures/graph-helper/index.js +3 -0
- package/examples/dependency-graph/fixtures/graph-helper/package.json +6 -0
- package/examples/dependency-graph/package.json +7 -0
- package/examples/dependency-graph/public/message.txt +1 -0
- package/examples/dependency-graph/sloppy.json +9 -0
- package/examples/dependency-graph/src/main.ts +8 -0
- package/examples/dogfood/README.md +23 -0
- package/examples/dogfood/dogfood.json +136 -0
- package/examples/dynamic-module-include/README.md +20 -0
- package/examples/dynamic-module-include/public/readme.txt +1 -0
- package/examples/dynamic-module-include/sloppy.json +12 -0
- package/examples/dynamic-module-include/src/main.ts +6 -0
- package/examples/dynamic-module-include/src/plugins/alpha.js +3 -0
- package/examples/dynamic-module-include/src/plugins/beta.js +3 -0
- package/examples/ergonomics/README.md +42 -0
- package/examples/ergonomics/app.js +38 -0
- package/examples/framework-controller/README.md +12 -0
- package/examples/framework-controller/app.js +31 -0
- package/examples/framework-di-services/README.md +17 -0
- package/examples/framework-di-services/app.ts +40 -0
- package/examples/framework-explicit-binding/README.md +12 -0
- package/examples/framework-explicit-binding/app.ts +34 -0
- package/examples/framework-hello/README.md +16 -0
- package/examples/framework-hello/app.ts +16 -0
- package/examples/framework-postgres-crud/README.md +73 -0
- package/examples/framework-postgres-crud/app.ts +64 -0
- package/examples/framework-sqlite-crud/README.md +52 -0
- package/examples/framework-sqlite-crud/app.ts +90 -0
- package/examples/framework-sqlite-crud/appsettings.json +11 -0
- package/examples/framework-sqlserver-crud/README.md +73 -0
- package/examples/framework-sqlserver-crud/app.ts +64 -0
- package/examples/framework-validation-errors/README.md +12 -0
- package/examples/framework-validation-errors/app.ts +16 -0
- package/examples/fs-basic/README.md +24 -0
- package/examples/fs-basic/app.js +12 -0
- package/examples/fs-roots-policy/README.md +14 -0
- package/examples/fs-roots-policy/app.js +4 -0
- package/examples/fs-streams/README.md +18 -0
- package/examples/fs-streams/app.js +11 -0
- package/examples/fs-watch/README.md +19 -0
- package/examples/fs-watch/app.js +11 -0
- package/examples/hello/README.md +63 -0
- package/examples/hello/app.js +19 -0
- package/examples/hello-minimal/README.md +51 -0
- package/examples/hello-minimal/sloppy.json +5 -0
- package/examples/hello-minimal/src/main.ts +9 -0
- package/examples/http-client-basic/README.md +11 -0
- package/examples/http-client-basic/app.js +46 -0
- package/examples/http-client-generated/README.md +22 -0
- package/examples/http-client-generated/openapi.json +45 -0
- package/examples/http-client-resilience/README.md +4 -0
- package/examples/http-client-resilience/app.js +38 -0
- package/examples/http-client-runtime-loopback/README.md +24 -0
- package/examples/http-client-testhost/README.md +4 -0
- package/examples/http-client-testhost/app.js +27 -0
- package/examples/http-client-testhost-package-mock/README.md +26 -0
- package/examples/http-client-typed/README.md +5 -0
- package/examples/http-client-typed/app.js +33 -0
- package/examples/modules-api/README.md +30 -0
- package/examples/modules-api/app.js +9 -0
- package/examples/modules-api/modules/routes.js +16 -0
- package/examples/modules-api/sloppy.json +5 -0
- package/examples/modules-basic/README.md +32 -0
- package/examples/modules-basic/app.js +41 -0
- package/examples/net-deadline-cancel/README.md +13 -0
- package/examples/net-deadline-cancel/app.js +34 -0
- package/examples/net-local-ipc/README.md +12 -0
- package/examples/net-local-ipc/app.js +46 -0
- package/examples/net-policy-strict/README.md +12 -0
- package/examples/net-policy-strict/app.js +34 -0
- package/examples/net-tcp-client/README.md +10 -0
- package/examples/net-tcp-client/app.js +23 -0
- package/examples/net-tcp-echo/README.md +11 -0
- package/examples/net-tcp-echo/app.js +45 -0
- package/examples/net-tcp-server/README.md +10 -0
- package/examples/net-tcp-server/app.js +28 -0
- package/examples/node-compat-path-events/README.md +15 -0
- package/examples/node-compat-path-events/sloppy.json +6 -0
- package/examples/node-compat-path-events/src/main.ts +15 -0
- package/examples/ops-compiler/README.md +9 -0
- package/examples/ops-compiler/app.js +26 -0
- package/examples/ops-health-metrics-management/README.md +14 -0
- package/examples/ops-health-metrics-management/app.js +24 -0
- package/examples/orm-basic/README.md +17 -0
- package/examples/orm-basic/app.js +82 -0
- package/examples/orm-cursor-export/README.md +16 -0
- package/examples/orm-cursor-export/app.js +28 -0
- package/examples/orm-migrations/README.md +14 -0
- package/examples/orm-migrations/migrations/.gitkeep +1 -0
- package/examples/orm-migrations/sloppy.json +9 -0
- package/examples/orm-migrations/src/app.ts +34 -0
- package/examples/orm-relations-includes/README.md +10 -0
- package/examples/orm-relations-includes/app.js +47 -0
- package/examples/orm-testservices/README.md +37 -0
- package/examples/orm-testservices/test.mjs +32 -0
- package/examples/os-runtime-api/README.md +11 -0
- package/examples/os-runtime-api/app.js +44 -0
- package/examples/package-zod-like/README.md +28 -0
- package/examples/package-zod-like/fixtures/zod-like/index.js +48 -0
- package/examples/package-zod-like/fixtures/zod-like/package.json +12 -0
- package/examples/package-zod-like/package.json +7 -0
- package/examples/package-zod-like/sloppy.json +6 -0
- package/examples/package-zod-like/src/main.ts +16 -0
- package/examples/postgres-basic/README.md +31 -0
- package/examples/postgres-basic/app.js +50 -0
- package/examples/prealpha-control-plane/README.md +50 -0
- package/examples/prealpha-control-plane/appsettings.Development.json +11 -0
- package/examples/prealpha-control-plane/appsettings.json +15 -0
- package/examples/prealpha-control-plane/sloppy.json +5 -0
- package/examples/prealpha-control-plane/src/db/schema.js +7 -0
- package/examples/prealpha-control-plane/src/db/seed.js +6 -0
- package/examples/prealpha-control-plane/src/main.js +21 -0
- package/examples/prealpha-control-plane/src/routes/apps.js +34 -0
- package/examples/prealpha-control-plane/src/routes/builds.js +25 -0
- package/examples/prealpha-control-plane/src/routes/deployments.js +19 -0
- package/examples/prealpha-control-plane/src/routes/diagnostics.js +11 -0
- package/examples/prealpha-control-plane/src/routes/health.js +27 -0
- package/examples/prealpha-control-plane/src/routes/projects.js +38 -0
- package/examples/prealpha-control-plane/src/services/diagnosticsSink.js +11 -0
- package/examples/prealpha-control-plane/src/services/repositories.js +9 -0
- package/examples/prealpha-control-plane/src/validation/schemas.js +6 -0
- package/examples/program-fs-process/README.md +31 -0
- package/examples/program-fs-process/sloppy.json +9 -0
- package/examples/program-fs-process/src/main.ts +27 -0
- package/examples/program-hello/README.md +32 -0
- package/examples/program-hello/main.ts +8 -0
- package/examples/program-hello/message.ts +1 -0
- package/examples/program-hello/sloppy.json +5 -0
- package/examples/rate-limit-auth/README.md +3 -0
- package/examples/rate-limit-auth/app.js +14 -0
- package/examples/rate-limit-basic/README.md +3 -0
- package/examples/rate-limit-basic/app.js +13 -0
- package/examples/rate-limit-redis/README.md +5 -0
- package/examples/rate-limit-redis/app.js +20 -0
- package/examples/rate-limit-testhost/README.md +4 -0
- package/examples/rate-limit-testhost/app.js +13 -0
- package/examples/rate-limit-websocket/README.md +3 -0
- package/examples/rate-limit-websocket/app.js +16 -0
- package/examples/realtime-auth/README.md +8 -0
- package/examples/realtime-auth/app.js +25 -0
- package/examples/realtime-auth/test.mjs +43 -0
- package/examples/realtime-chat/README.md +8 -0
- package/examples/realtime-chat/app.js +32 -0
- package/examples/realtime-chat/test.mjs +52 -0
- package/examples/realtime-dashboard/README.md +20 -0
- package/examples/realtime-dashboard/app.js +37 -0
- package/examples/realtime-presence/README.md +8 -0
- package/examples/realtime-presence/app.js +32 -0
- package/examples/realtime-presence/test.mjs +50 -0
- package/examples/realtime-testhost/README.md +8 -0
- package/examples/realtime-testhost/test.mjs +31 -0
- package/examples/redis-basic/README.md +17 -0
- package/examples/redis-basic/app.js +39 -0
- package/examples/redis-cache/README.md +14 -0
- package/examples/redis-cache/app.js +36 -0
- package/examples/redis-locks/README.md +13 -0
- package/examples/redis-locks/app.js +49 -0
- package/examples/request-context/README.md +32 -0
- package/examples/request-context/app.js +15 -0
- package/examples/sqlite-basic/README.md +52 -0
- package/examples/sqlite-basic/app.js +56 -0
- package/examples/sqlserver-basic/README.md +36 -0
- package/examples/sqlserver-basic/app.js +59 -0
- package/examples/static-files-basic/README.md +11 -0
- package/examples/static-files-basic/app.js +12 -0
- package/examples/static-files-basic/public/app.js +1 -0
- package/examples/static-files-basic/public/site.css +3 -0
- package/examples/static-files-package/README.md +12 -0
- package/examples/static-files-package/app.js +10 -0
- package/examples/static-files-package/public/index.html +2 -0
- package/examples/static-files-precompressed/README.md +12 -0
- package/examples/static-files-precompressed/app.js +11 -0
- package/examples/static-files-precompressed/public/app.js +1 -0
- package/examples/static-files-precompressed/public/app.js.br +0 -0
- package/examples/static-files-precompressed/public/app.js.gz +0 -0
- package/examples/static-files-spa/README.md +12 -0
- package/examples/static-files-spa/app.js +16 -0
- package/examples/static-files-spa/dist/assets/app.js +1 -0
- package/examples/static-files-spa/dist/index.html +4 -0
- package/examples/static-files-testhost/README.md +8 -0
- package/examples/static-files-testhost/app.js +13 -0
- package/examples/static-files-testhost/public/app.js +1 -0
- package/examples/static-files-testhost/public/app.js.gz +0 -0
- package/examples/static-files-testhost/test.mjs +38 -0
- package/examples/testhost-basic/README.md +26 -0
- package/examples/testhost-db/README.md +31 -0
- package/examples/testservices-postgres/README.md +68 -0
- package/examples/testservices-redis/README.md +71 -0
- package/examples/testservices-sqlserver/README.md +75 -0
- package/examples/time-basic/README.md +18 -0
- package/examples/time-basic/app.js +12 -0
- package/examples/time-deadline-cancellation/README.md +11 -0
- package/examples/time-deadline-cancellation/app.js +27 -0
- package/examples/time-fake-clock/README.md +14 -0
- package/examples/time-fake-clock/app.js +25 -0
- package/examples/time-interval-schedule/README.md +13 -0
- package/examples/time-interval-schedule/app.js +60 -0
- package/examples/users-api-sqlite/README.md +74 -0
- package/examples/users-api-sqlite/app.js +11 -0
- package/examples/users-api-sqlite/appsettings.Development.json +11 -0
- package/examples/users-api-sqlite/appsettings.json +11 -0
- package/examples/users-api-sqlite/modules/users.js +40 -0
- package/examples/users-api-sqlite/sloppy.json +5 -0
- package/examples/validation-errors/README.md +36 -0
- package/examples/validation-errors/app.js +14 -0
- package/examples/validation-errors/invalid-user.http +6 -0
- package/examples/validation-errors/sloppy.json +5 -0
- package/examples/web-dynamic-routes/README.md +17 -0
- package/examples/web-dynamic-routes/app.ts +27 -0
- package/examples/webhooks-basic/README.md +11 -0
- package/examples/webhooks-basic/app.js +48 -0
- package/examples/websocket-auth/README.md +8 -0
- package/examples/websocket-auth/app.js +16 -0
- package/examples/websocket-echo/README.md +9 -0
- package/examples/websocket-echo/app.js +36 -0
- package/examples/websocket-json-schema/README.md +5 -0
- package/examples/websocket-json-schema/app.js +25 -0
- package/examples/websocket-testhost/README.md +11 -0
- package/examples/websocket-testhost/test.mjs +49 -0
- package/examples/workers-background-service/README.md +7 -0
- package/examples/workers-background-service/app.js +16 -0
- package/examples/workers-js-isolate/README.md +8 -0
- package/examples/workers-js-isolate/app.js +19 -0
- package/examples/workers-js-isolate/workers/parser.ts +11 -0
- package/examples/workers-shutdown/README.md +6 -0
- package/examples/workers-shutdown/app.js +26 -0
- package/examples/workers-workerpool/README.md +6 -0
- package/examples/workers-workerpool/app.js +23 -0
- package/examples/workers-workqueue/README.md +8 -0
- package/examples/workers-workqueue/app.js +24 -0
- package/manifest.json +59 -0
- package/package.json +34 -0
- package/stdlib/sloppy/README.md +177 -0
- package/stdlib/sloppy/app.js +2142 -0
- package/stdlib/sloppy/auth.js +1813 -0
- package/stdlib/sloppy/bootstrap.manifest.json +83 -0
- package/stdlib/sloppy/cache.js +1542 -0
- package/stdlib/sloppy/codec.js +1153 -0
- package/stdlib/sloppy/config.js +61 -0
- package/stdlib/sloppy/crypto.js +312 -0
- package/stdlib/sloppy/data.js +2945 -0
- package/stdlib/sloppy/ffi.js +185 -0
- package/stdlib/sloppy/fs.js +795 -0
- package/stdlib/sloppy/health.js +603 -0
- package/stdlib/sloppy/http.js +1595 -0
- package/stdlib/sloppy/index.js +59 -0
- package/stdlib/sloppy/internal/bytes.js +31 -0
- package/stdlib/sloppy/internal/capabilities.js +155 -0
- package/stdlib/sloppy/internal/config.js +640 -0
- package/stdlib/sloppy/internal/disposable.js +31 -0
- package/stdlib/sloppy/internal/headers.js +63 -0
- package/stdlib/sloppy/internal/intrinsics.js +2 -0
- package/stdlib/sloppy/internal/json.js +20 -0
- package/stdlib/sloppy/internal/logging.js +278 -0
- package/stdlib/sloppy/internal/modules.js +405 -0
- package/stdlib/sloppy/internal/redaction.js +87 -0
- package/stdlib/sloppy/internal/routes.js +2279 -0
- package/stdlib/sloppy/internal/runtime-classic.js +19837 -0
- package/stdlib/sloppy/internal/services.js +690 -0
- package/stdlib/sloppy/internal/shared.js +32 -0
- package/stdlib/sloppy/internal/testhost-diagnostics.js +88 -0
- package/stdlib/sloppy/internal/testhost-http-server.js +238 -0
- package/stdlib/sloppy/internal/testhost-http.js +118 -0
- package/stdlib/sloppy/internal/testhost-loopback.js +50 -0
- package/stdlib/sloppy/internal/testservices-docker.js +154 -0
- package/stdlib/sloppy/internal/validation.js +117 -0
- package/stdlib/sloppy/metrics.js +427 -0
- package/stdlib/sloppy/net.js +5208 -0
- package/stdlib/sloppy/node/assert/strict.js +39 -0
- package/stdlib/sloppy/node/assert.js +228 -0
- package/stdlib/sloppy/node/buffer.js +247 -0
- package/stdlib/sloppy/node/console.js +33 -0
- package/stdlib/sloppy/node/constants.js +9 -0
- package/stdlib/sloppy/node/crypto.js +89 -0
- package/stdlib/sloppy/node/diagnostics_channel.js +41 -0
- package/stdlib/sloppy/node/events.js +113 -0
- package/stdlib/sloppy/node/fs/promises.js +27 -0
- package/stdlib/sloppy/node/fs.js +280 -0
- package/stdlib/sloppy/node/http.js +11 -0
- package/stdlib/sloppy/node/https.js +11 -0
- package/stdlib/sloppy/node/module.js +40 -0
- package/stdlib/sloppy/node/os.js +22 -0
- package/stdlib/sloppy/node/path.js +78 -0
- package/stdlib/sloppy/node/perf_hooks.js +12 -0
- package/stdlib/sloppy/node/process.js +129 -0
- package/stdlib/sloppy/node/querystring.js +21 -0
- package/stdlib/sloppy/node/stream/promises.js +3 -0
- package/stdlib/sloppy/node/stream.js +132 -0
- package/stdlib/sloppy/node/string_decoder.js +23 -0
- package/stdlib/sloppy/node/timers.js +26 -0
- package/stdlib/sloppy/node/tty.js +18 -0
- package/stdlib/sloppy/node/url.js +17 -0
- package/stdlib/sloppy/node/util.js +95 -0
- package/stdlib/sloppy/node/zlib.js +72 -0
- package/stdlib/sloppy/orm.js +2188 -0
- package/stdlib/sloppy/os.js +580 -0
- package/stdlib/sloppy/problem-details.js +29 -0
- package/stdlib/sloppy/providers/sqlite.js +26 -0
- package/stdlib/sloppy/rate-limit.js +856 -0
- package/stdlib/sloppy/realtime.js +1508 -0
- package/stdlib/sloppy/redis.js +1272 -0
- package/stdlib/sloppy/request-id.js +184 -0
- package/stdlib/sloppy/request-logging.js +101 -0
- package/stdlib/sloppy/results.js +933 -0
- package/stdlib/sloppy/schema.js +546 -0
- package/stdlib/sloppy/testing.js +4081 -0
- package/stdlib/sloppy/testservices.js +1041 -0
- package/stdlib/sloppy/time.js +894 -0
- package/stdlib/sloppy/webhooks.js +1330 -0
- package/stdlib/sloppy/workers.js +986 -0
- package/templates/api/README.md +82 -0
- package/templates/api/appsettings.Development.json +14 -0
- package/templates/api/appsettings.json +13 -0
- package/templates/api/data/.gitkeep +1 -0
- package/templates/api/gitignore +4 -0
- package/templates/api/migrations/0001_create_users.sql +1 -0
- package/templates/api/package.json +16 -0
- package/templates/api/public/hello.txt +1 -0
- package/templates/api/sloppy.json +14 -0
- package/templates/api/src/config.ts +1 -0
- package/templates/api/src/db/migrate.ts +14 -0
- package/templates/api/src/db/schema.ts +4 -0
- package/templates/api/src/db/usersRepository.ts +23 -0
- package/templates/api/src/main.ts +18 -0
- package/templates/api/src/models/user.ts +7 -0
- package/templates/api/src/routes/health.ts +20 -0
- package/templates/api/src/routes/users.ts +40 -0
- package/templates/api/src/services/usersService.ts +21 -0
- package/templates/api/tsconfig.json +15 -0
- package/templates/cli/README.md +16 -0
- package/templates/cli/gitignore +2 -0
- package/templates/cli/package.json +13 -0
- package/templates/cli/sloppy.json +6 -0
- package/templates/cli/src/commands/echo.ts +9 -0
- package/templates/cli/src/commands/inspect.ts +20 -0
- package/templates/cli/src/main.ts +50 -0
- package/templates/cli/tsconfig.json +15 -0
- package/templates/minimal-api/README.md +14 -0
- package/templates/minimal-api/gitignore +3 -0
- package/templates/minimal-api/package.json +14 -0
- package/templates/minimal-api/sloppy.json +5 -0
- package/templates/minimal-api/src/main.ts +9 -0
- package/templates/minimal-api/tsconfig.json +15 -0
- package/templates/node-compat/README.md +40 -0
- package/templates/node-compat/gitignore +2 -0
- package/templates/node-compat/package.json +11 -0
- package/templates/node-compat/sloppy.json +6 -0
- package/templates/node-compat/src/main.ts +40 -0
- package/templates/package-api/README.md +44 -0
- package/templates/package-api/fixtures/validator-lite/index.js +7 -0
- package/templates/package-api/fixtures/validator-lite/package.json +6 -0
- package/templates/package-api/gitignore +3 -0
- package/templates/package-api/package.json +17 -0
- package/templates/package-api/sloppy.json +5 -0
- package/templates/package-api/src/main.ts +10 -0
- package/templates/package-api/src/routes/health.ts +5 -0
- package/templates/package-api/src/routes/users.ts +12 -0
- package/templates/package-api/tsconfig.json +15 -0
- package/templates/program/README.md +12 -0
- package/templates/program/gitignore +1 -0
- package/templates/program/package.json +10 -0
- package/templates/program/sloppy.json +6 -0
- package/templates/program/src/main.ts +9 -0
|
@@ -0,0 +1,1272 @@
|
|
|
1
|
+
import { isPlainObject } from "./internal/validation.js";
|
|
2
|
+
import { Base64, Text } from "./codec.js";
|
|
3
|
+
import { Random, Secret } from "./crypto.js";
|
|
4
|
+
import { Metrics } from "./metrics.js";
|
|
5
|
+
import { TcpClient, TcpConnection } from "./net.js";
|
|
6
|
+
import { Schema } from "./schema.js";
|
|
7
|
+
|
|
8
|
+
const ASYNC_DISPOSE = Symbol.asyncDispose;
|
|
9
|
+
const SECRET_REDACTION = "[REDACTED]";
|
|
10
|
+
const DEFAULT_PORT = 6379;
|
|
11
|
+
const DEFAULT_CONNECT_TIMEOUT_MS = 5000;
|
|
12
|
+
const DEFAULT_COMMAND_TIMEOUT_MS = 5000;
|
|
13
|
+
const DEFAULT_MAX_VALUE_BYTES = 1024 * 1024;
|
|
14
|
+
const DEFAULT_MAX_RESPONSE_BYTES = 16 * 1024 * 1024;
|
|
15
|
+
const DEFAULT_MAX_ARRAY_ITEMS = 1024 * 1024;
|
|
16
|
+
const DEFAULT_MAX_ARRAY_DEPTH = 32;
|
|
17
|
+
const DEFAULT_POOL_MAX_CONNECTIONS = 8;
|
|
18
|
+
const DEFAULT_POOL_IDLE_TIMEOUT_MS = 30000;
|
|
19
|
+
const DEFAULT_POOL_PENDING_LIMIT = 128;
|
|
20
|
+
const DEFAULT_POOL_PENDING_TIMEOUT_MS = 5000;
|
|
21
|
+
const COMMAND_PATTERN = /^[A-Z][A-Z0-9_]*$/u;
|
|
22
|
+
const NAME_PATTERN = /^[A-Za-z0-9_.-]+$/u;
|
|
23
|
+
const LOCK_RELEASE_SCRIPT = `
|
|
24
|
+
if redis.call("GET", KEYS[1]) == ARGV[1] then
|
|
25
|
+
return redis.call("DEL", KEYS[1])
|
|
26
|
+
end
|
|
27
|
+
return 0`;
|
|
28
|
+
const LOCK_EXTEND_SCRIPT = `
|
|
29
|
+
if redis.call("GET", KEYS[1]) == ARGV[1] then
|
|
30
|
+
return redis.call("PEXPIRE", KEYS[1], ARGV[2])
|
|
31
|
+
end
|
|
32
|
+
return 0`;
|
|
33
|
+
|
|
34
|
+
const ERROR_NAMES = Object.freeze({
|
|
35
|
+
SLOPPY_E_REDIS_INVALID_OPTIONS: "SloppyRedisInvalidOptionsError",
|
|
36
|
+
SLOPPY_E_REDIS_CONNECT_FAILED: "SloppyRedisConnectError",
|
|
37
|
+
SLOPPY_E_REDIS_AUTH_FAILED: "SloppyRedisAuthError",
|
|
38
|
+
SLOPPY_E_REDIS_COMMAND_FAILED: "SloppyRedisCommandError",
|
|
39
|
+
SLOPPY_E_REDIS_PROTOCOL_ERROR: "SloppyRedisProtocolError",
|
|
40
|
+
SLOPPY_E_REDIS_TIMEOUT: "SloppyRedisTimeoutError",
|
|
41
|
+
SLOPPY_E_REDIS_CANCELLED: "SloppyRedisCancelledError",
|
|
42
|
+
SLOPPY_E_REDIS_CLOSED: "SloppyRedisClosedError",
|
|
43
|
+
SLOPPY_E_REDIS_RESPONSE_VALIDATION_FAILED: "SloppyRedisResponseValidationError",
|
|
44
|
+
SLOPPY_E_REDIS_VALUE_TOO_LARGE: "SloppyRedisValueTooLargeError",
|
|
45
|
+
SLOPPY_E_REDIS_LOCK_TIMEOUT: "SloppyRedisLockTimeoutError",
|
|
46
|
+
SLOPPY_E_REDIS_LOCK_LOST: "SloppyRedisLockLostError",
|
|
47
|
+
SLOPPY_E_REDIS_LOCK_RELEASE_FAILED: "SloppyRedisLockReleaseError",
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
class SloppyRedisError extends Error {
|
|
51
|
+
constructor(code, message, options = undefined) {
|
|
52
|
+
super(`${code}: ${message}`);
|
|
53
|
+
this.name = ERROR_NAMES[code] ?? "SloppyRedisError";
|
|
54
|
+
this.code = code;
|
|
55
|
+
if (options?.cause !== undefined) {
|
|
56
|
+
this.cause = options.cause;
|
|
57
|
+
}
|
|
58
|
+
if (options?.redisCode !== undefined) {
|
|
59
|
+
this.redisCode = options.redisCode;
|
|
60
|
+
}
|
|
61
|
+
if (options?.command !== undefined) {
|
|
62
|
+
this.command = options.command;
|
|
63
|
+
}
|
|
64
|
+
Object.defineProperty(this, "details", {
|
|
65
|
+
value: options?.details,
|
|
66
|
+
enumerable: false,
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
class RedisErrorReply {
|
|
72
|
+
constructor(message) {
|
|
73
|
+
this.message = message;
|
|
74
|
+
this.code = String(message).split(/\s+/u, 1)[0] || "ERR";
|
|
75
|
+
Object.freeze(this);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function redisError(code, message, options = undefined) {
|
|
80
|
+
return new SloppyRedisError(code, message, options);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function secretToText(value) {
|
|
84
|
+
if (value === undefined || value === null) {
|
|
85
|
+
return undefined;
|
|
86
|
+
}
|
|
87
|
+
if (typeof value === "string") {
|
|
88
|
+
return value;
|
|
89
|
+
}
|
|
90
|
+
if (typeof value.bytes === "function") {
|
|
91
|
+
return Text.utf8.decode(value.bytes());
|
|
92
|
+
}
|
|
93
|
+
throw redisError("SLOPPY_E_REDIS_INVALID_OPTIONS", "Redis password must be a string or Secret.");
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function assertName(name, subject = "client name") {
|
|
97
|
+
if (typeof name !== "string" || name.length === 0 || name.length > 128 || !NAME_PATTERN.test(name)) {
|
|
98
|
+
throw redisError(
|
|
99
|
+
"SLOPPY_E_REDIS_INVALID_OPTIONS",
|
|
100
|
+
`Redis ${subject} must contain only letters, digits, dots, underscores, or hyphens.`,
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function normalizeInteger(value, fallback, subject, min, max) {
|
|
106
|
+
if (value === undefined) {
|
|
107
|
+
return fallback;
|
|
108
|
+
}
|
|
109
|
+
if (!Number.isInteger(value) || value < min || value > max) {
|
|
110
|
+
throw redisError("SLOPPY_E_REDIS_INVALID_OPTIONS", `Redis ${subject} must be an integer from ${min} to ${max}.`);
|
|
111
|
+
}
|
|
112
|
+
return value;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function normalizeTimeout(value, fallback, subject) {
|
|
116
|
+
return normalizeInteger(value, fallback, subject, 1, 0xffffffff);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function normalizeNonNegativeTimeout(value, fallback, subject) {
|
|
120
|
+
return normalizeInteger(value, fallback, subject, 0, 0xffffffff);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function normalizePositiveBytes(value, fallback, subject) {
|
|
124
|
+
return normalizeInteger(value, fallback, subject, 1, Number.MAX_SAFE_INTEGER);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function normalizePool(options = undefined) {
|
|
128
|
+
if (options === undefined) {
|
|
129
|
+
options = {};
|
|
130
|
+
}
|
|
131
|
+
if (!isPlainObject(options)) {
|
|
132
|
+
throw redisError("SLOPPY_E_REDIS_INVALID_OPTIONS", "Redis pool options must be a plain object.");
|
|
133
|
+
}
|
|
134
|
+
const pendingQueueTimeoutMs = options.pendingQueueTimeoutMs ?? options.acquireTimeoutMs;
|
|
135
|
+
return Object.freeze({
|
|
136
|
+
maxConnections: normalizeInteger(options.maxConnections, DEFAULT_POOL_MAX_CONNECTIONS, "pool.maxConnections", 1, 1024),
|
|
137
|
+
idleTimeoutMs: normalizeTimeout(options.idleTimeoutMs, DEFAULT_POOL_IDLE_TIMEOUT_MS, "pool.idleTimeoutMs"),
|
|
138
|
+
pendingQueueLimit: normalizeInteger(options.pendingQueueLimit, DEFAULT_POOL_PENDING_LIMIT, "pool.pendingQueueLimit", 0, 100000),
|
|
139
|
+
pendingQueueTimeoutMs: normalizeTimeout(pendingQueueTimeoutMs, DEFAULT_POOL_PENDING_TIMEOUT_MS, "pool.acquireTimeoutMs"),
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function parseRedisUrl(rawUrl, options) {
|
|
144
|
+
if (typeof rawUrl !== "string" || rawUrl.length === 0) {
|
|
145
|
+
throw redisError("SLOPPY_E_REDIS_INVALID_OPTIONS", "Redis url must be a non-empty string.");
|
|
146
|
+
}
|
|
147
|
+
let parsed;
|
|
148
|
+
try {
|
|
149
|
+
parsed = new URL(rawUrl);
|
|
150
|
+
} catch (error) {
|
|
151
|
+
throw redisError("SLOPPY_E_REDIS_INVALID_OPTIONS", "Redis url must be a valid redis:// or rediss:// URL.", { cause: error });
|
|
152
|
+
}
|
|
153
|
+
if (parsed.protocol !== "redis:" && parsed.protocol !== "rediss:") {
|
|
154
|
+
throw redisError("SLOPPY_E_REDIS_INVALID_OPTIONS", "Redis url must use redis:// or rediss://.");
|
|
155
|
+
}
|
|
156
|
+
if (parsed.hostname.length === 0) {
|
|
157
|
+
throw redisError("SLOPPY_E_REDIS_INVALID_OPTIONS", "Redis url must include a host.");
|
|
158
|
+
}
|
|
159
|
+
const pathDatabase = parsed.pathname.length > 1 ? Number(parsed.pathname.slice(1)) : undefined;
|
|
160
|
+
if (pathDatabase !== undefined && (!Number.isInteger(pathDatabase) || pathDatabase < 0)) {
|
|
161
|
+
throw redisError("SLOPPY_E_REDIS_INVALID_OPTIONS", "Redis URL database path must be a non-negative integer.");
|
|
162
|
+
}
|
|
163
|
+
const username = options.username ?? (parsed.username.length === 0 ? undefined : decodeURIComponent(parsed.username));
|
|
164
|
+
const password = secretToText(options.password) ?? (parsed.password.length === 0 ? undefined : decodeURIComponent(parsed.password));
|
|
165
|
+
const database = normalizeInteger(options.database, pathDatabase ?? 0, "database", 0, options.maxDatabase ?? 15);
|
|
166
|
+
return Object.freeze({
|
|
167
|
+
rawUrl,
|
|
168
|
+
redactedUrl: redactRedisUrl(rawUrl),
|
|
169
|
+
scheme: parsed.protocol.slice(0, -1),
|
|
170
|
+
tls: options.tls ?? parsed.protocol === "rediss:",
|
|
171
|
+
host: parsed.hostname,
|
|
172
|
+
port: parsed.port === "" ? DEFAULT_PORT : Number(parsed.port),
|
|
173
|
+
username,
|
|
174
|
+
password,
|
|
175
|
+
database,
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function normalizeClientOptions(name, options = {}) {
|
|
180
|
+
assertName(name);
|
|
181
|
+
if (!isPlainObject(options)) {
|
|
182
|
+
throw redisError("SLOPPY_E_REDIS_INVALID_OPTIONS", "Redis client options must be a plain object.");
|
|
183
|
+
}
|
|
184
|
+
const endpoint = parseRedisUrl(options.url, options);
|
|
185
|
+
const validateOnConnect = options.validateOnConnect ?? options.pingOnConnect;
|
|
186
|
+
return Object.freeze({
|
|
187
|
+
name,
|
|
188
|
+
endpoint,
|
|
189
|
+
connectTimeoutMs: normalizeTimeout(options.connectTimeoutMs, DEFAULT_CONNECT_TIMEOUT_MS, "connectTimeoutMs"),
|
|
190
|
+
commandTimeoutMs: normalizeTimeout(options.commandTimeoutMs, DEFAULT_COMMAND_TIMEOUT_MS, "commandTimeoutMs"),
|
|
191
|
+
maxValueBytes: normalizePositiveBytes(options.maxValueBytes, DEFAULT_MAX_VALUE_BYTES, "maxValueBytes"),
|
|
192
|
+
maxResponseBytes: normalizePositiveBytes(options.maxResponseBytes, DEFAULT_MAX_RESPONSE_BYTES, "maxResponseBytes"),
|
|
193
|
+
maxArrayItems: normalizeInteger(options.maxArrayItems, DEFAULT_MAX_ARRAY_ITEMS, "maxArrayItems", 1, Number.MAX_SAFE_INTEGER),
|
|
194
|
+
maxArrayDepth: normalizeInteger(options.maxArrayDepth, DEFAULT_MAX_ARRAY_DEPTH, "maxArrayDepth", 1, 256),
|
|
195
|
+
pool: normalizePool(options.pool),
|
|
196
|
+
validateOnConnect: validateOnConnect !== false,
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function redactRedisUrl(value) {
|
|
201
|
+
try {
|
|
202
|
+
const parsed = new URL(String(value));
|
|
203
|
+
const hasUsername = parsed.username.length > 0;
|
|
204
|
+
const hasPassword = parsed.password.length > 0;
|
|
205
|
+
if (hasPassword) {
|
|
206
|
+
parsed.password = SECRET_REDACTION;
|
|
207
|
+
}
|
|
208
|
+
if (hasUsername && hasPassword) {
|
|
209
|
+
parsed.username = SECRET_REDACTION;
|
|
210
|
+
}
|
|
211
|
+
return parsed.toString();
|
|
212
|
+
} catch {
|
|
213
|
+
return String(value).replace(/(:\/\/[^:\s]+:)([^@\s]+)(@)/u, `$1${SECRET_REDACTION}$3`);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function safeCommandName(name) {
|
|
218
|
+
if (typeof name !== "string" || !COMMAND_PATTERN.test(name.toUpperCase())) {
|
|
219
|
+
throw redisError("SLOPPY_E_REDIS_INVALID_OPTIONS", "Redis command name must be a safe command token.");
|
|
220
|
+
}
|
|
221
|
+
return name.toUpperCase();
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function bytesFromArgument(value, subject = "Redis command argument") {
|
|
225
|
+
if (typeof value === "string") {
|
|
226
|
+
return Text.utf8.encode(value);
|
|
227
|
+
}
|
|
228
|
+
if (typeof value === "number") {
|
|
229
|
+
if (!Number.isSafeInteger(value)) {
|
|
230
|
+
throw redisError("SLOPPY_E_REDIS_INVALID_OPTIONS", `${subject} number must be a safe integer.`);
|
|
231
|
+
}
|
|
232
|
+
return Text.utf8.encode(String(value));
|
|
233
|
+
}
|
|
234
|
+
if (typeof value === "bigint") {
|
|
235
|
+
return Text.utf8.encode(String(value));
|
|
236
|
+
}
|
|
237
|
+
if (value instanceof Uint8Array) {
|
|
238
|
+
return value;
|
|
239
|
+
}
|
|
240
|
+
throw redisError("SLOPPY_E_REDIS_INVALID_OPTIONS", `${subject} must be a string, bytes, number, or bigint.`);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function concatBytes(chunks, total) {
|
|
244
|
+
const output = new Uint8Array(total);
|
|
245
|
+
let offset = 0;
|
|
246
|
+
for (const chunk of chunks) {
|
|
247
|
+
output.set(chunk, offset);
|
|
248
|
+
offset += chunk.byteLength;
|
|
249
|
+
}
|
|
250
|
+
return output;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function encodeCommand(args) {
|
|
254
|
+
if (!Array.isArray(args) || args.length === 0) {
|
|
255
|
+
throw redisError("SLOPPY_E_REDIS_INVALID_OPTIONS", "Redis command must contain at least a command name.");
|
|
256
|
+
}
|
|
257
|
+
const parts = [Text.utf8.encode(`*${args.length}\r\n`)];
|
|
258
|
+
let total = parts[0].byteLength;
|
|
259
|
+
for (const arg of args) {
|
|
260
|
+
const bytes = bytesFromArgument(arg);
|
|
261
|
+
const head = Text.utf8.encode(`$${bytes.byteLength}\r\n`);
|
|
262
|
+
const tail = Text.utf8.encode("\r\n");
|
|
263
|
+
parts.push(head, bytes, tail);
|
|
264
|
+
total += head.byteLength + bytes.byteLength + tail.byteLength;
|
|
265
|
+
}
|
|
266
|
+
return concatBytes(parts, total);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
function findCrlf(bytes, start) {
|
|
270
|
+
for (let index = start; index + 1 < bytes.byteLength; index += 1) {
|
|
271
|
+
if (bytes[index] === 13 && bytes[index + 1] === 10) {
|
|
272
|
+
return index;
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
return -1;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
function ascii(bytes, start, end) {
|
|
279
|
+
return Text.utf8.decode(bytes.subarray(start, end));
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
function parseIntegerText(text, subject) {
|
|
283
|
+
if (!/^-?[0-9]+$/u.test(text)) {
|
|
284
|
+
throw redisError("SLOPPY_E_REDIS_PROTOCOL_ERROR", `Redis protocol ${subject} is malformed.`);
|
|
285
|
+
}
|
|
286
|
+
const value = Number(text);
|
|
287
|
+
if (!Number.isSafeInteger(value)) {
|
|
288
|
+
throw redisError("SLOPPY_E_REDIS_PROTOCOL_ERROR", `Redis protocol ${subject} is outside safe integer range.`);
|
|
289
|
+
}
|
|
290
|
+
return value;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
class RespParser {
|
|
294
|
+
constructor(options = {}) {
|
|
295
|
+
this._buffer = new Uint8Array(0);
|
|
296
|
+
this._maxResponseBytes = options.maxResponseBytes ?? DEFAULT_MAX_RESPONSE_BYTES;
|
|
297
|
+
this._maxArrayItems = options.maxArrayItems ?? DEFAULT_MAX_ARRAY_ITEMS;
|
|
298
|
+
this._maxArrayDepth = options.maxArrayDepth ?? DEFAULT_MAX_ARRAY_DEPTH;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
get bufferedBytes() {
|
|
302
|
+
return this._buffer.byteLength;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
feed(chunk) {
|
|
306
|
+
if (!(chunk instanceof Uint8Array)) {
|
|
307
|
+
throw redisError("SLOPPY_E_REDIS_PROTOCOL_ERROR", "Redis parser input must be bytes.");
|
|
308
|
+
}
|
|
309
|
+
const total = this._buffer.byteLength + chunk.byteLength;
|
|
310
|
+
if (total > this._maxResponseBytes) {
|
|
311
|
+
throw redisError("SLOPPY_E_REDIS_PROTOCOL_ERROR", "Redis response exceeded the configured byte limit.");
|
|
312
|
+
}
|
|
313
|
+
const next = new Uint8Array(total);
|
|
314
|
+
next.set(this._buffer, 0);
|
|
315
|
+
next.set(chunk, this._buffer.byteLength);
|
|
316
|
+
this._buffer = next;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
read() {
|
|
320
|
+
const parsed = this._parseAt(0, 0);
|
|
321
|
+
if (parsed === undefined) {
|
|
322
|
+
return undefined;
|
|
323
|
+
}
|
|
324
|
+
this._buffer = this._buffer.subarray(parsed.offset);
|
|
325
|
+
return parsed.value;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
_parseLine(offset, subject) {
|
|
329
|
+
const end = findCrlf(this._buffer, offset);
|
|
330
|
+
if (end < 0) {
|
|
331
|
+
return undefined;
|
|
332
|
+
}
|
|
333
|
+
return { text: ascii(this._buffer, offset, end), offset: end + 2, subject };
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
_parseAt(offset, depth) {
|
|
337
|
+
if (offset >= this._buffer.byteLength) {
|
|
338
|
+
return undefined;
|
|
339
|
+
}
|
|
340
|
+
if (depth > this._maxArrayDepth) {
|
|
341
|
+
throw redisError("SLOPPY_E_REDIS_PROTOCOL_ERROR", "Redis response exceeded maximum array depth.");
|
|
342
|
+
}
|
|
343
|
+
const prefix = this._buffer[offset];
|
|
344
|
+
if (prefix === 43) {
|
|
345
|
+
const line = this._parseLine(offset + 1, "simple string");
|
|
346
|
+
return line === undefined ? undefined : { value: line.text, offset: line.offset };
|
|
347
|
+
}
|
|
348
|
+
if (prefix === 45) {
|
|
349
|
+
const line = this._parseLine(offset + 1, "error");
|
|
350
|
+
return line === undefined ? undefined : { value: new RedisErrorReply(line.text), offset: line.offset };
|
|
351
|
+
}
|
|
352
|
+
if (prefix === 58) {
|
|
353
|
+
const line = this._parseLine(offset + 1, "integer");
|
|
354
|
+
return line === undefined ? undefined : { value: parseIntegerText(line.text, "integer"), offset: line.offset };
|
|
355
|
+
}
|
|
356
|
+
if (prefix === 36) {
|
|
357
|
+
const line = this._parseLine(offset + 1, "bulk length");
|
|
358
|
+
if (line === undefined) {
|
|
359
|
+
return undefined;
|
|
360
|
+
}
|
|
361
|
+
const length = parseIntegerText(line.text, "bulk length");
|
|
362
|
+
if (length === -1) {
|
|
363
|
+
return { value: null, offset: line.offset };
|
|
364
|
+
}
|
|
365
|
+
if (length < 0) {
|
|
366
|
+
throw redisError("SLOPPY_E_REDIS_PROTOCOL_ERROR", "Redis bulk length is malformed.");
|
|
367
|
+
}
|
|
368
|
+
if (length > this._maxResponseBytes) {
|
|
369
|
+
throw redisError("SLOPPY_E_REDIS_PROTOCOL_ERROR", "Redis bulk response exceeded the configured byte limit.");
|
|
370
|
+
}
|
|
371
|
+
const end = line.offset + length;
|
|
372
|
+
if (end + 2 > this._buffer.byteLength) {
|
|
373
|
+
return undefined;
|
|
374
|
+
}
|
|
375
|
+
if (this._buffer[end] !== 13 || this._buffer[end + 1] !== 10) {
|
|
376
|
+
throw redisError("SLOPPY_E_REDIS_PROTOCOL_ERROR", "Redis bulk response is missing CRLF terminator.");
|
|
377
|
+
}
|
|
378
|
+
return { value: this._buffer.slice(line.offset, end), offset: end + 2 };
|
|
379
|
+
}
|
|
380
|
+
if (prefix === 42) {
|
|
381
|
+
const line = this._parseLine(offset + 1, "array length");
|
|
382
|
+
if (line === undefined) {
|
|
383
|
+
return undefined;
|
|
384
|
+
}
|
|
385
|
+
const length = parseIntegerText(line.text, "array length");
|
|
386
|
+
if (length === -1) {
|
|
387
|
+
return { value: null, offset: line.offset };
|
|
388
|
+
}
|
|
389
|
+
if (length < 0 || length > this._maxArrayItems) {
|
|
390
|
+
throw redisError("SLOPPY_E_REDIS_PROTOCOL_ERROR", "Redis array length is outside configured limits.");
|
|
391
|
+
}
|
|
392
|
+
const values = [];
|
|
393
|
+
let cursor = line.offset;
|
|
394
|
+
for (let index = 0; index < length; index += 1) {
|
|
395
|
+
const parsed = this._parseAt(cursor, depth + 1);
|
|
396
|
+
if (parsed === undefined) {
|
|
397
|
+
return undefined;
|
|
398
|
+
}
|
|
399
|
+
values.push(parsed.value);
|
|
400
|
+
cursor = parsed.offset;
|
|
401
|
+
}
|
|
402
|
+
return { value: values, offset: cursor };
|
|
403
|
+
}
|
|
404
|
+
throw redisError("SLOPPY_E_REDIS_PROTOCOL_ERROR", "Redis response prefix is not valid RESP2.");
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
function replyText(reply) {
|
|
409
|
+
if (reply === null || reply === undefined) {
|
|
410
|
+
return undefined;
|
|
411
|
+
}
|
|
412
|
+
if (reply instanceof Uint8Array) {
|
|
413
|
+
return Text.utf8.decode(reply);
|
|
414
|
+
}
|
|
415
|
+
return String(reply);
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
function assertNotErrorReply(reply, command) {
|
|
419
|
+
if (reply instanceof RedisErrorReply) {
|
|
420
|
+
const code = command === "AUTH" ? "SLOPPY_E_REDIS_AUTH_FAILED" : "SLOPPY_E_REDIS_COMMAND_FAILED";
|
|
421
|
+
throw redisError(code, `Redis command ${command} failed: ${reply.message}`, {
|
|
422
|
+
redisCode: reply.code,
|
|
423
|
+
command,
|
|
424
|
+
});
|
|
425
|
+
}
|
|
426
|
+
return reply;
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
function withTimeout(promise, timeoutMs, onTimeout) {
|
|
430
|
+
let timer;
|
|
431
|
+
return Promise.race([
|
|
432
|
+
promise,
|
|
433
|
+
new Promise((_, reject) => {
|
|
434
|
+
timer = setTimeout(() => reject(onTimeout()), timeoutMs);
|
|
435
|
+
}),
|
|
436
|
+
]).finally(() => clearTimeout(timer));
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
async function openConnection(options) {
|
|
440
|
+
try {
|
|
441
|
+
if (options.endpoint.tls) {
|
|
442
|
+
const bridge = globalThis.__sloppy?.net;
|
|
443
|
+
if (bridge === undefined || typeof bridge.connectTls !== "function") {
|
|
444
|
+
throw redisError("SLOPPY_E_REDIS_CONNECT_FAILED", "Redis rediss:// requires the outbound TLS bridge.");
|
|
445
|
+
}
|
|
446
|
+
const handle = await bridge.connectTls({
|
|
447
|
+
host: options.endpoint.host,
|
|
448
|
+
port: options.endpoint.port,
|
|
449
|
+
timeoutMs: options.connectTimeoutMs,
|
|
450
|
+
noDelay: true,
|
|
451
|
+
serverName: options.endpoint.host,
|
|
452
|
+
});
|
|
453
|
+
return new TcpConnection(bridge, handle);
|
|
454
|
+
}
|
|
455
|
+
return await TcpClient.connect({
|
|
456
|
+
host: options.endpoint.host,
|
|
457
|
+
port: options.endpoint.port,
|
|
458
|
+
timeoutMs: options.connectTimeoutMs,
|
|
459
|
+
noDelay: true,
|
|
460
|
+
});
|
|
461
|
+
} catch (error) {
|
|
462
|
+
if (error instanceof SloppyRedisError) {
|
|
463
|
+
throw error;
|
|
464
|
+
}
|
|
465
|
+
throw redisError("SLOPPY_E_REDIS_CONNECT_FAILED", "Redis connection failed.", { cause: error });
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
class RedisConnection {
|
|
470
|
+
constructor(connection, options, metrics) {
|
|
471
|
+
this.connection = connection;
|
|
472
|
+
this.options = options;
|
|
473
|
+
this.parser = new RespParser(options);
|
|
474
|
+
this.metrics = metrics;
|
|
475
|
+
this.broken = false;
|
|
476
|
+
this.closed = false;
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
async initialize() {
|
|
480
|
+
const password = this.options.endpoint.password;
|
|
481
|
+
if (password !== undefined) {
|
|
482
|
+
const args = this.options.endpoint.username === undefined
|
|
483
|
+
? ["AUTH", password]
|
|
484
|
+
: ["AUTH", this.options.endpoint.username, password];
|
|
485
|
+
const reply = await this.command(args, { commandName: "AUTH" });
|
|
486
|
+
if (replyText(reply) !== "OK") {
|
|
487
|
+
throw redisError("SLOPPY_E_REDIS_AUTH_FAILED", "Redis AUTH did not return OK.");
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
if (this.options.endpoint.database !== 0) {
|
|
491
|
+
const reply = await this.command(["SELECT", this.options.endpoint.database], { commandName: "SELECT" });
|
|
492
|
+
if (replyText(reply) !== "OK") {
|
|
493
|
+
throw redisError("SLOPPY_E_REDIS_COMMAND_FAILED", "Redis SELECT did not return OK.", { command: "SELECT" });
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
if (this.options.validateOnConnect) {
|
|
497
|
+
await this.command(["PING"], { commandName: "PING" });
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
async command(args, options = {}) {
|
|
502
|
+
const commandName = options.commandName ?? safeCommandName(String(args[0]));
|
|
503
|
+
const encoded = encodeCommand(args);
|
|
504
|
+
this.metrics.bytesOut += encoded.byteLength;
|
|
505
|
+
const run = async () => {
|
|
506
|
+
await this.connection.write(encoded);
|
|
507
|
+
while (true) {
|
|
508
|
+
const reply = this.parser.read();
|
|
509
|
+
if (reply !== undefined) {
|
|
510
|
+
return assertNotErrorReply(reply, commandName);
|
|
511
|
+
}
|
|
512
|
+
const chunk = await this.connection.read({ maxBytes: 8192 });
|
|
513
|
+
if (!(chunk instanceof Uint8Array) || chunk.byteLength === 0) {
|
|
514
|
+
throw redisError("SLOPPY_E_REDIS_PROTOCOL_ERROR", "Redis connection closed before a complete response.");
|
|
515
|
+
}
|
|
516
|
+
this.metrics.bytesIn += chunk.byteLength;
|
|
517
|
+
this.parser.feed(chunk);
|
|
518
|
+
}
|
|
519
|
+
};
|
|
520
|
+
try {
|
|
521
|
+
return await withTimeout(
|
|
522
|
+
run(),
|
|
523
|
+
options.timeoutMs ?? this.options.commandTimeoutMs,
|
|
524
|
+
() => redisError("SLOPPY_E_REDIS_TIMEOUT", `Redis command ${commandName} timed out.`, { command: commandName }),
|
|
525
|
+
);
|
|
526
|
+
} catch (error) {
|
|
527
|
+
this.broken = true;
|
|
528
|
+
if (error instanceof SloppyRedisError) {
|
|
529
|
+
throw error;
|
|
530
|
+
}
|
|
531
|
+
throw redisError("SLOPPY_E_REDIS_COMMAND_FAILED", `Redis command ${commandName} failed.`, { cause: error, command: commandName });
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
async pipeline(commands, options = {}) {
|
|
536
|
+
const encodedCommands = commands.map((command) => encodeCommand(command));
|
|
537
|
+
const total = encodedCommands.reduce((sum, bytes) => sum + bytes.byteLength, 0);
|
|
538
|
+
const payload = concatBytes(encodedCommands, total);
|
|
539
|
+
this.metrics.bytesOut += payload.byteLength;
|
|
540
|
+
const run = async () => {
|
|
541
|
+
await this.connection.write(payload);
|
|
542
|
+
const replies = [];
|
|
543
|
+
while (replies.length < commands.length) {
|
|
544
|
+
const reply = this.parser.read();
|
|
545
|
+
if (reply !== undefined) {
|
|
546
|
+
replies.push(assertNotErrorReply(reply, safeCommandName(String(commands[replies.length][0]))));
|
|
547
|
+
continue;
|
|
548
|
+
}
|
|
549
|
+
const chunk = await this.connection.read({ maxBytes: 8192 });
|
|
550
|
+
if (!(chunk instanceof Uint8Array) || chunk.byteLength === 0) {
|
|
551
|
+
throw redisError("SLOPPY_E_REDIS_PROTOCOL_ERROR", "Redis connection closed before a complete pipeline response.");
|
|
552
|
+
}
|
|
553
|
+
this.metrics.bytesIn += chunk.byteLength;
|
|
554
|
+
this.parser.feed(chunk);
|
|
555
|
+
}
|
|
556
|
+
return replies;
|
|
557
|
+
};
|
|
558
|
+
try {
|
|
559
|
+
return await withTimeout(
|
|
560
|
+
run(),
|
|
561
|
+
options.timeoutMs ?? this.options.commandTimeoutMs,
|
|
562
|
+
() => redisError("SLOPPY_E_REDIS_TIMEOUT", "Redis pipeline timed out.", { command: "PIPELINE" }),
|
|
563
|
+
);
|
|
564
|
+
} catch (error) {
|
|
565
|
+
this.broken = true;
|
|
566
|
+
throw error;
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
async close() {
|
|
571
|
+
if (this.closed) {
|
|
572
|
+
return;
|
|
573
|
+
}
|
|
574
|
+
this.closed = true;
|
|
575
|
+
await this.connection.close().catch(() => {});
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
async abort() {
|
|
579
|
+
if (this.closed) {
|
|
580
|
+
return;
|
|
581
|
+
}
|
|
582
|
+
this.closed = true;
|
|
583
|
+
await this.connection.abort().catch(() => {});
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
class RedisConnectionPool {
|
|
588
|
+
constructor(options, metrics) {
|
|
589
|
+
this.options = options;
|
|
590
|
+
this.metrics = metrics;
|
|
591
|
+
this.idle = [];
|
|
592
|
+
this.active = 0;
|
|
593
|
+
this.queue = [];
|
|
594
|
+
this.closed = false;
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
async _create() {
|
|
598
|
+
this.active += 1;
|
|
599
|
+
this.metrics.connectionsCreated += 1;
|
|
600
|
+
let connection;
|
|
601
|
+
try {
|
|
602
|
+
connection = new RedisConnection(await openConnection(this.options), this.options, this.metrics);
|
|
603
|
+
await connection.initialize();
|
|
604
|
+
return connection;
|
|
605
|
+
} catch (error) {
|
|
606
|
+
this.active -= 1;
|
|
607
|
+
if (connection !== undefined) {
|
|
608
|
+
await connection.abort().catch(() => {});
|
|
609
|
+
}
|
|
610
|
+
this._drainQueue();
|
|
611
|
+
throw error;
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
async acquire() {
|
|
616
|
+
if (this.closed) {
|
|
617
|
+
throw redisError("SLOPPY_E_REDIS_CLOSED", "Redis client is closed.");
|
|
618
|
+
}
|
|
619
|
+
const now = Date.now();
|
|
620
|
+
while (this.idle.length > 0) {
|
|
621
|
+
const entry = this.idle.pop();
|
|
622
|
+
if (entry.connection.closed || entry.connection.broken || now - entry.releasedAt > this.options.pool.idleTimeoutMs) {
|
|
623
|
+
this.active -= 1;
|
|
624
|
+
this.metrics.connectionsClosed += 1;
|
|
625
|
+
await entry.connection.close().catch(() => {});
|
|
626
|
+
continue;
|
|
627
|
+
}
|
|
628
|
+
this.metrics.connectionsReused += 1;
|
|
629
|
+
return entry.connection;
|
|
630
|
+
}
|
|
631
|
+
if (this.active < this.options.pool.maxConnections) {
|
|
632
|
+
return await this._create();
|
|
633
|
+
}
|
|
634
|
+
if (this.queue.length >= this.options.pool.pendingQueueLimit) {
|
|
635
|
+
this.metrics.poolRejected += 1;
|
|
636
|
+
throw redisError("SLOPPY_E_REDIS_TIMEOUT", "Redis connection pool pending queue is full.");
|
|
637
|
+
}
|
|
638
|
+
this.metrics.poolWait += 1;
|
|
639
|
+
return await new Promise((resolve, reject) => {
|
|
640
|
+
const waiter = { resolve, reject, timer: undefined };
|
|
641
|
+
waiter.timer = setTimeout(() => {
|
|
642
|
+
this.queue = this.queue.filter((entry) => entry !== waiter);
|
|
643
|
+
this.metrics.poolRejected += 1;
|
|
644
|
+
reject(redisError("SLOPPY_E_REDIS_TIMEOUT", "Redis connection acquisition timed out."));
|
|
645
|
+
}, this.options.pool.pendingQueueTimeoutMs);
|
|
646
|
+
this.queue.push(waiter);
|
|
647
|
+
});
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
release(connection, reusable) {
|
|
651
|
+
if (connection === undefined) {
|
|
652
|
+
return;
|
|
653
|
+
}
|
|
654
|
+
reusable = reusable && !this.closed && !connection.closed && !connection.broken;
|
|
655
|
+
if (this.queue.length > 0 && reusable) {
|
|
656
|
+
const waiter = this.queue.shift();
|
|
657
|
+
clearTimeout(waiter.timer);
|
|
658
|
+
waiter.resolve(connection);
|
|
659
|
+
return;
|
|
660
|
+
}
|
|
661
|
+
if (reusable) {
|
|
662
|
+
this.idle.push({ connection, releasedAt: Date.now() });
|
|
663
|
+
return;
|
|
664
|
+
}
|
|
665
|
+
this.active -= 1;
|
|
666
|
+
this.metrics.connectionsClosed += 1;
|
|
667
|
+
void connection.close();
|
|
668
|
+
this._drainQueue();
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
_drainQueue() {
|
|
672
|
+
while (!this.closed && this.queue.length > 0 && this.active < this.options.pool.maxConnections) {
|
|
673
|
+
const waiter = this.queue.shift();
|
|
674
|
+
clearTimeout(waiter.timer);
|
|
675
|
+
this._create().then(waiter.resolve, waiter.reject);
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
async close() {
|
|
680
|
+
if (this.closed) {
|
|
681
|
+
return;
|
|
682
|
+
}
|
|
683
|
+
this.closed = true;
|
|
684
|
+
for (const waiter of this.queue.splice(0)) {
|
|
685
|
+
clearTimeout(waiter.timer);
|
|
686
|
+
waiter.reject(redisError("SLOPPY_E_REDIS_CLOSED", "Redis client is closed."));
|
|
687
|
+
}
|
|
688
|
+
const idle = this.idle.splice(0);
|
|
689
|
+
await Promise.all(idle.map((entry) => entry.connection.close()));
|
|
690
|
+
this.active = Math.max(0, this.active - idle.length);
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
stats() {
|
|
694
|
+
return Object.freeze({
|
|
695
|
+
activeConnections: this.active,
|
|
696
|
+
idleConnections: this.idle.length,
|
|
697
|
+
queuedRequests: this.queue.length,
|
|
698
|
+
});
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
function valueBytes(kind, bytes, maxValueBytes) {
|
|
703
|
+
if (bytes.byteLength > maxValueBytes) {
|
|
704
|
+
throw redisError("SLOPPY_E_REDIS_VALUE_TOO_LARGE", "Redis value exceeds maxValueBytes.");
|
|
705
|
+
}
|
|
706
|
+
const prefix = Text.utf8.encode(`${kind}:`);
|
|
707
|
+
const output = new Uint8Array(prefix.byteLength + bytes.byteLength);
|
|
708
|
+
output.set(prefix, 0);
|
|
709
|
+
output.set(bytes, prefix.byteLength);
|
|
710
|
+
return output;
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
function encodeJsonValue(value, maxValueBytes) {
|
|
714
|
+
let text;
|
|
715
|
+
try {
|
|
716
|
+
text = JSON.stringify(value);
|
|
717
|
+
} catch (error) {
|
|
718
|
+
throw redisError("SLOPPY_E_REDIS_INVALID_OPTIONS", "Redis JSON value could not be serialized.", { cause: error });
|
|
719
|
+
}
|
|
720
|
+
if (text === undefined) {
|
|
721
|
+
throw redisError("SLOPPY_E_REDIS_INVALID_OPTIONS", "Redis JSON value must be JSON serializable.");
|
|
722
|
+
}
|
|
723
|
+
return valueBytes("J", Text.utf8.encode(text), maxValueBytes);
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
function decodeValue(bytes, schemaOrOptions = undefined) {
|
|
727
|
+
if (bytes === null) {
|
|
728
|
+
return undefined;
|
|
729
|
+
}
|
|
730
|
+
if (!(bytes instanceof Uint8Array) || bytes.byteLength < 2 || bytes[1] !== 58) {
|
|
731
|
+
return bytes;
|
|
732
|
+
}
|
|
733
|
+
const payload = bytes.subarray(2);
|
|
734
|
+
const kind = String.fromCharCode(bytes[0]);
|
|
735
|
+
if (kind === "T") {
|
|
736
|
+
return Text.utf8.decode(payload);
|
|
737
|
+
}
|
|
738
|
+
if (kind === "B") {
|
|
739
|
+
try {
|
|
740
|
+
return Base64.decode(Text.utf8.decode(payload));
|
|
741
|
+
} catch (error) {
|
|
742
|
+
throw redisError("SLOPPY_E_REDIS_RESPONSE_VALIDATION_FAILED", "Redis Base64 value could not be decoded.", { cause: error });
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
if (kind !== "J") {
|
|
746
|
+
return bytes;
|
|
747
|
+
}
|
|
748
|
+
let value;
|
|
749
|
+
try {
|
|
750
|
+
value = JSON.parse(Text.utf8.decode(payload));
|
|
751
|
+
} catch (error) {
|
|
752
|
+
throw redisError("SLOPPY_E_REDIS_RESPONSE_VALIDATION_FAILED", "Redis JSON value could not be parsed.", { cause: error });
|
|
753
|
+
}
|
|
754
|
+
const schemaValue = Schema.isSchema(schemaOrOptions) ? schemaOrOptions : schemaOrOptions?.schema;
|
|
755
|
+
if (schemaValue !== undefined) {
|
|
756
|
+
try {
|
|
757
|
+
return Schema.validate(value, schemaValue);
|
|
758
|
+
} catch (error) {
|
|
759
|
+
throw redisError("SLOPPY_E_REDIS_RESPONSE_VALIDATION_FAILED", "Redis JSON value failed schema validation.", { cause: error });
|
|
760
|
+
}
|
|
761
|
+
}
|
|
762
|
+
return value;
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
function normalizeKey(key) {
|
|
766
|
+
if (typeof key !== "string" || key.length === 0 || key.length > 1024 || key.includes("\0")) {
|
|
767
|
+
throw redisError("SLOPPY_E_REDIS_INVALID_OPTIONS", "Redis key must be a non-empty string up to 1024 bytes without NUL.");
|
|
768
|
+
}
|
|
769
|
+
return key;
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
function pxFromTtl(ttlMs, subject = "ttlMs") {
|
|
773
|
+
if (ttlMs === undefined) {
|
|
774
|
+
return undefined;
|
|
775
|
+
}
|
|
776
|
+
return normalizeTimeout(ttlMs, undefined, subject);
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
function setArgs(key, value, options = {}) {
|
|
780
|
+
if (!isPlainObject(options)) {
|
|
781
|
+
throw redisError("SLOPPY_E_REDIS_INVALID_OPTIONS", "Redis set options must be a plain object.");
|
|
782
|
+
}
|
|
783
|
+
const args = ["SET", normalizeKey(key), value];
|
|
784
|
+
const ttlMs = pxFromTtl(options.ttlMs);
|
|
785
|
+
if (ttlMs !== undefined) {
|
|
786
|
+
args.push("PX", ttlMs);
|
|
787
|
+
}
|
|
788
|
+
if (options.nx === true) {
|
|
789
|
+
args.push("NX");
|
|
790
|
+
}
|
|
791
|
+
if (options.xx === true) {
|
|
792
|
+
args.push("XX");
|
|
793
|
+
}
|
|
794
|
+
if (options.get === true) {
|
|
795
|
+
args.push("GET");
|
|
796
|
+
}
|
|
797
|
+
return args;
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
function createMetrics(name) {
|
|
801
|
+
const registry = Metrics.defaultRegistry;
|
|
802
|
+
return {
|
|
803
|
+
name,
|
|
804
|
+
commands: 0,
|
|
805
|
+
commandErrors: 0,
|
|
806
|
+
bytesIn: 0,
|
|
807
|
+
bytesOut: 0,
|
|
808
|
+
connectionsCreated: 0,
|
|
809
|
+
connectionsReused: 0,
|
|
810
|
+
connectionsClosed: 0,
|
|
811
|
+
poolWait: 0,
|
|
812
|
+
poolRejected: 0,
|
|
813
|
+
lockAcquire: 0,
|
|
814
|
+
lockTimeout: 0,
|
|
815
|
+
recordCommand(command, outcome, durationMs) {
|
|
816
|
+
this.commands += 1;
|
|
817
|
+
if (outcome !== "ok") {
|
|
818
|
+
this.commandErrors += 1;
|
|
819
|
+
}
|
|
820
|
+
registry.counter("redis.commands.total").inc({ client: name, command, outcome });
|
|
821
|
+
registry.histogram("redis.command.duration").observe({ client: name, command, outcome }, durationMs);
|
|
822
|
+
},
|
|
823
|
+
snapshot(poolStats) {
|
|
824
|
+
return Object.freeze({
|
|
825
|
+
commandsTotal: this.commands,
|
|
826
|
+
commandErrorsTotal: this.commandErrors,
|
|
827
|
+
bytesInTotal: this.bytesIn,
|
|
828
|
+
bytesOutTotal: this.bytesOut,
|
|
829
|
+
connectionsCreated: this.connectionsCreated,
|
|
830
|
+
connectionsReused: this.connectionsReused,
|
|
831
|
+
connectionsClosed: this.connectionsClosed,
|
|
832
|
+
poolWaitTotal: this.poolWait,
|
|
833
|
+
poolRejectedTotal: this.poolRejected,
|
|
834
|
+
locksAcquireTotal: this.lockAcquire,
|
|
835
|
+
locksTimeoutTotal: this.lockTimeout,
|
|
836
|
+
...poolStats,
|
|
837
|
+
});
|
|
838
|
+
},
|
|
839
|
+
};
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
function token(name) {
|
|
843
|
+
assertName(name, "token name");
|
|
844
|
+
return Object.freeze({
|
|
845
|
+
__sloppyRedisToken: `redis.${name}`,
|
|
846
|
+
toString() {
|
|
847
|
+
return `redis.${name}`;
|
|
848
|
+
},
|
|
849
|
+
});
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
function tokenDisplay(tokenValue) {
|
|
853
|
+
return tokenValue?.__sloppyRedisToken ?? String(tokenValue);
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
function randomOwnerToken() {
|
|
857
|
+
try {
|
|
858
|
+
return Random.hex(16);
|
|
859
|
+
} catch (error) {
|
|
860
|
+
const bytes = new Uint8Array(16);
|
|
861
|
+
const crypto = globalThis.crypto;
|
|
862
|
+
if (crypto?.getRandomValues !== undefined) {
|
|
863
|
+
crypto.getRandomValues(bytes);
|
|
864
|
+
return Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
865
|
+
}
|
|
866
|
+
throw redisError("SLOPPY_E_REDIS_SECURE_RANDOM_UNAVAILABLE", "Redis lock owner tokens require secure randomness.", { cause: error });
|
|
867
|
+
}
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
function createRedisClient(name, rawOptions) {
|
|
871
|
+
const options = normalizeClientOptions(name, rawOptions);
|
|
872
|
+
const metrics = createMetrics(name);
|
|
873
|
+
const pool = new RedisConnectionPool(options, metrics);
|
|
874
|
+
let closed = false;
|
|
875
|
+
const scripts = new Map();
|
|
876
|
+
|
|
877
|
+
async function lease(callback) {
|
|
878
|
+
if (closed) {
|
|
879
|
+
throw redisError("SLOPPY_E_REDIS_CLOSED", "Redis client is closed.");
|
|
880
|
+
}
|
|
881
|
+
const connection = await pool.acquire();
|
|
882
|
+
let reusable = true;
|
|
883
|
+
try {
|
|
884
|
+
return await callback(connection);
|
|
885
|
+
} catch (error) {
|
|
886
|
+
reusable = false;
|
|
887
|
+
throw error;
|
|
888
|
+
} finally {
|
|
889
|
+
pool.release(connection, reusable);
|
|
890
|
+
}
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
async function runCommand(commandName, args, commandOptions = undefined) {
|
|
894
|
+
commandName = safeCommandName(commandName);
|
|
895
|
+
const started = Date.now();
|
|
896
|
+
try {
|
|
897
|
+
const reply = await lease((connection) => connection.command([commandName, ...args], {
|
|
898
|
+
timeoutMs: commandOptions?.timeoutMs,
|
|
899
|
+
commandName,
|
|
900
|
+
}));
|
|
901
|
+
metrics.recordCommand(commandName, "ok", Date.now() - started);
|
|
902
|
+
return reply;
|
|
903
|
+
} catch (error) {
|
|
904
|
+
metrics.recordCommand(commandName, "error", Date.now() - started);
|
|
905
|
+
throw error;
|
|
906
|
+
}
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
function closeClient() {
|
|
910
|
+
if (closed) {
|
|
911
|
+
return Promise.resolve();
|
|
912
|
+
}
|
|
913
|
+
closed = true;
|
|
914
|
+
return pool.close();
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
const client = {
|
|
918
|
+
name,
|
|
919
|
+
token: token(name),
|
|
920
|
+
async ping(message = undefined) {
|
|
921
|
+
const reply = await runCommand("PING", message === undefined ? [] : [String(message)]);
|
|
922
|
+
return replyText(reply);
|
|
923
|
+
},
|
|
924
|
+
async get(key, schemaOrOptions = undefined) {
|
|
925
|
+
const reply = await runCommand("GET", [normalizeKey(key)]);
|
|
926
|
+
return decodeValue(reply, schemaOrOptions);
|
|
927
|
+
},
|
|
928
|
+
async getText(key) {
|
|
929
|
+
const reply = await runCommand("GET", [normalizeKey(key)]);
|
|
930
|
+
if (reply === null) {
|
|
931
|
+
return undefined;
|
|
932
|
+
}
|
|
933
|
+
const value = decodeValue(reply);
|
|
934
|
+
return typeof value === "string" ? value : Text.utf8.decode(reply);
|
|
935
|
+
},
|
|
936
|
+
async getBytes(key) {
|
|
937
|
+
const reply = await runCommand("GET", [normalizeKey(key)]);
|
|
938
|
+
if (reply === null) {
|
|
939
|
+
return undefined;
|
|
940
|
+
}
|
|
941
|
+
const value = decodeValue(reply);
|
|
942
|
+
return value instanceof Uint8Array ? value : reply;
|
|
943
|
+
},
|
|
944
|
+
async set(key, value, setOptions = {}) {
|
|
945
|
+
const reply = await runCommand("SET", setArgs(key, encodeJsonValue(value, options.maxValueBytes), setOptions).slice(1));
|
|
946
|
+
return setOptions.get === true ? decodeValue(reply) : replyText(reply) === "OK";
|
|
947
|
+
},
|
|
948
|
+
async setText(key, value, setOptions = {}) {
|
|
949
|
+
if (typeof value !== "string") {
|
|
950
|
+
throw redisError("SLOPPY_E_REDIS_INVALID_OPTIONS", "Redis setText value must be a string.");
|
|
951
|
+
}
|
|
952
|
+
const reply = await runCommand("SET", setArgs(key, valueBytes("T", Text.utf8.encode(value), options.maxValueBytes), setOptions).slice(1));
|
|
953
|
+
return setOptions.get === true ? decodeValue(reply) : replyText(reply) === "OK";
|
|
954
|
+
},
|
|
955
|
+
async setBytes(key, value, setOptions = {}) {
|
|
956
|
+
if (!(value instanceof Uint8Array)) {
|
|
957
|
+
throw redisError("SLOPPY_E_REDIS_INVALID_OPTIONS", "Redis setBytes value must be bytes.");
|
|
958
|
+
}
|
|
959
|
+
const encoded = Text.utf8.encode(Base64.encode(value));
|
|
960
|
+
const reply = await runCommand("SET", setArgs(key, valueBytes("B", encoded, options.maxValueBytes), setOptions).slice(1));
|
|
961
|
+
return setOptions.get === true ? decodeValue(reply) : replyText(reply) === "OK";
|
|
962
|
+
},
|
|
963
|
+
async delete(key) {
|
|
964
|
+
return await runCommand("DEL", [normalizeKey(key)]);
|
|
965
|
+
},
|
|
966
|
+
async exists(key) {
|
|
967
|
+
return await runCommand("EXISTS", [normalizeKey(key)]) === 1;
|
|
968
|
+
},
|
|
969
|
+
async mget(keys, schemaOrOptions = undefined) {
|
|
970
|
+
if (!Array.isArray(keys) || keys.length === 0) {
|
|
971
|
+
throw redisError("SLOPPY_E_REDIS_INVALID_OPTIONS", "Redis mget keys must be a non-empty array.");
|
|
972
|
+
}
|
|
973
|
+
const replies = await runCommand("MGET", keys.map(normalizeKey));
|
|
974
|
+
if (!Array.isArray(replies)) {
|
|
975
|
+
throw redisError("SLOPPY_E_REDIS_PROTOCOL_ERROR", "Redis MGET response must be an array.");
|
|
976
|
+
}
|
|
977
|
+
return replies.map((reply) => decodeValue(reply, schemaOrOptions));
|
|
978
|
+
},
|
|
979
|
+
async mset(entries, setOptions = {}) {
|
|
980
|
+
const args = [];
|
|
981
|
+
const iterable = entries instanceof Map ? entries.entries() : Object.entries(entries ?? {});
|
|
982
|
+
for (const [key, value] of iterable) {
|
|
983
|
+
args.push(normalizeKey(key), encodeJsonValue(value, options.maxValueBytes));
|
|
984
|
+
}
|
|
985
|
+
if (args.length === 0) {
|
|
986
|
+
throw redisError("SLOPPY_E_REDIS_INVALID_OPTIONS", "Redis mset entries must not be empty.");
|
|
987
|
+
}
|
|
988
|
+
if (setOptions.ttlMs !== undefined) {
|
|
989
|
+
await client.pipeline(args.reduce((commands, _value, index) => {
|
|
990
|
+
if (index % 2 === 0) {
|
|
991
|
+
commands.push(setArgs(args[index], args[index + 1], { ttlMs: setOptions.ttlMs }));
|
|
992
|
+
}
|
|
993
|
+
return commands;
|
|
994
|
+
}, []));
|
|
995
|
+
return true;
|
|
996
|
+
}
|
|
997
|
+
return replyText(await runCommand("MSET", args)) === "OK";
|
|
998
|
+
},
|
|
999
|
+
async incr(key, incrOptions = {}) {
|
|
1000
|
+
const amount = incrOptions.by ?? 1;
|
|
1001
|
+
return await runCommand(amount === 1 ? "INCR" : "INCRBY", amount === 1 ? [normalizeKey(key)] : [normalizeKey(key), amount]);
|
|
1002
|
+
},
|
|
1003
|
+
async decr(key, decrOptions = {}) {
|
|
1004
|
+
const amount = decrOptions.by ?? 1;
|
|
1005
|
+
return await runCommand(amount === 1 ? "DECR" : "DECRBY", amount === 1 ? [normalizeKey(key)] : [normalizeKey(key), amount]);
|
|
1006
|
+
},
|
|
1007
|
+
async expire(key, ttlMs) {
|
|
1008
|
+
return await runCommand("PEXPIRE", [normalizeKey(key), pxFromTtl(ttlMs)]) === 1;
|
|
1009
|
+
},
|
|
1010
|
+
async ttl(key) {
|
|
1011
|
+
return await runCommand("TTL", [normalizeKey(key)]);
|
|
1012
|
+
},
|
|
1013
|
+
async pttl(key) {
|
|
1014
|
+
return await runCommand("PTTL", [normalizeKey(key)]);
|
|
1015
|
+
},
|
|
1016
|
+
async scan(scanOptions = {}) {
|
|
1017
|
+
const cursor = scanOptions.cursor ?? "0";
|
|
1018
|
+
const args = [String(cursor)];
|
|
1019
|
+
if (scanOptions.match !== undefined) {
|
|
1020
|
+
args.push("MATCH", String(scanOptions.match));
|
|
1021
|
+
}
|
|
1022
|
+
if (scanOptions.count !== undefined) {
|
|
1023
|
+
args.push("COUNT", normalizeInteger(scanOptions.count, undefined, "scan.count", 1, 100000));
|
|
1024
|
+
}
|
|
1025
|
+
const reply = await runCommand("SCAN", args);
|
|
1026
|
+
if (!Array.isArray(reply) || reply.length !== 2 || !Array.isArray(reply[1])) {
|
|
1027
|
+
throw redisError("SLOPPY_E_REDIS_PROTOCOL_ERROR", "Redis SCAN response is malformed.");
|
|
1028
|
+
}
|
|
1029
|
+
return Object.freeze({
|
|
1030
|
+
cursor: replyText(reply[0]),
|
|
1031
|
+
keys: Object.freeze(reply[1].map(replyText)),
|
|
1032
|
+
});
|
|
1033
|
+
},
|
|
1034
|
+
async command(commandName, args = [], commandOptions = undefined) {
|
|
1035
|
+
if (!Array.isArray(args)) {
|
|
1036
|
+
throw redisError("SLOPPY_E_REDIS_INVALID_OPTIONS", "Redis command args must be an array.");
|
|
1037
|
+
}
|
|
1038
|
+
return await runCommand(commandName, args, commandOptions);
|
|
1039
|
+
},
|
|
1040
|
+
async pipeline(commands, pipelineOptions = undefined) {
|
|
1041
|
+
if (!Array.isArray(commands) || commands.length === 0) {
|
|
1042
|
+
throw redisError("SLOPPY_E_REDIS_INVALID_OPTIONS", "Redis pipeline commands must be a non-empty array.");
|
|
1043
|
+
}
|
|
1044
|
+
const normalized = commands.map((command) => {
|
|
1045
|
+
if (!Array.isArray(command) || command.length === 0) {
|
|
1046
|
+
throw redisError("SLOPPY_E_REDIS_INVALID_OPTIONS", "Redis pipeline command must be a non-empty array.");
|
|
1047
|
+
}
|
|
1048
|
+
return [safeCommandName(String(command[0])), ...command.slice(1)];
|
|
1049
|
+
});
|
|
1050
|
+
const started = Date.now();
|
|
1051
|
+
try {
|
|
1052
|
+
const replies = await lease((connection) => connection.pipeline(normalized, pipelineOptions));
|
|
1053
|
+
for (const command of normalized) {
|
|
1054
|
+
metrics.recordCommand(command[0], "ok", Date.now() - started);
|
|
1055
|
+
}
|
|
1056
|
+
return replies;
|
|
1057
|
+
} catch (error) {
|
|
1058
|
+
for (const command of normalized) {
|
|
1059
|
+
metrics.recordCommand(command[0], "error", Date.now() - started);
|
|
1060
|
+
}
|
|
1061
|
+
throw error;
|
|
1062
|
+
}
|
|
1063
|
+
},
|
|
1064
|
+
async withConnection(callback) {
|
|
1065
|
+
if (typeof callback !== "function") {
|
|
1066
|
+
throw redisError("SLOPPY_E_REDIS_INVALID_OPTIONS", "Redis withConnection callback must be a function.");
|
|
1067
|
+
}
|
|
1068
|
+
return await lease((connection) => callback(Object.freeze({
|
|
1069
|
+
command(commandName, args = [], commandOptions = undefined) {
|
|
1070
|
+
return connection.command([safeCommandName(commandName), ...args], {
|
|
1071
|
+
...commandOptions,
|
|
1072
|
+
commandName: safeCommandName(commandName),
|
|
1073
|
+
});
|
|
1074
|
+
},
|
|
1075
|
+
})));
|
|
1076
|
+
},
|
|
1077
|
+
async eval(script, keys = [], args = [], commandOptions = undefined) {
|
|
1078
|
+
validateScript(script);
|
|
1079
|
+
return await runCommand("EVAL", [script, keys.length, ...keys.map(normalizeKey), ...args], commandOptions);
|
|
1080
|
+
},
|
|
1081
|
+
async evalSha(sha, keys = [], args = [], commandOptions = undefined) {
|
|
1082
|
+
validateSha(sha);
|
|
1083
|
+
return await runCommand("EVALSHA", [sha, keys.length, ...keys.map(normalizeKey), ...args], commandOptions);
|
|
1084
|
+
},
|
|
1085
|
+
async scriptLoad(script) {
|
|
1086
|
+
validateScript(script);
|
|
1087
|
+
const sha = replyText(await runCommand("SCRIPT", ["LOAD", script]));
|
|
1088
|
+
scripts.set(script, sha);
|
|
1089
|
+
return sha;
|
|
1090
|
+
},
|
|
1091
|
+
async script(script, keys = [], args = [], commandOptions = undefined) {
|
|
1092
|
+
let sha = scripts.get(script);
|
|
1093
|
+
if (sha === undefined) {
|
|
1094
|
+
sha = await client.scriptLoad(script);
|
|
1095
|
+
}
|
|
1096
|
+
try {
|
|
1097
|
+
return await client.evalSha(sha, keys, args, commandOptions);
|
|
1098
|
+
} catch (error) {
|
|
1099
|
+
if (String(error?.message ?? error).includes("NOSCRIPT")) {
|
|
1100
|
+
sha = await client.scriptLoad(script);
|
|
1101
|
+
return await client.evalSha(sha, keys, args, commandOptions);
|
|
1102
|
+
}
|
|
1103
|
+
throw error;
|
|
1104
|
+
}
|
|
1105
|
+
},
|
|
1106
|
+
metrics() {
|
|
1107
|
+
return metrics.snapshot(pool.stats());
|
|
1108
|
+
},
|
|
1109
|
+
diagnostics() {
|
|
1110
|
+
return Object.freeze({
|
|
1111
|
+
name,
|
|
1112
|
+
url: options.endpoint.redactedUrl,
|
|
1113
|
+
host: options.endpoint.host,
|
|
1114
|
+
port: options.endpoint.port,
|
|
1115
|
+
database: options.endpoint.database,
|
|
1116
|
+
tls: options.endpoint.tls,
|
|
1117
|
+
closed,
|
|
1118
|
+
pool: pool.stats(),
|
|
1119
|
+
});
|
|
1120
|
+
},
|
|
1121
|
+
async health() {
|
|
1122
|
+
if (closed) {
|
|
1123
|
+
return { status: "unhealthy", errorCode: "SLOPPY_E_REDIS_CLOSED", data: { name } };
|
|
1124
|
+
}
|
|
1125
|
+
try {
|
|
1126
|
+
await client.ping();
|
|
1127
|
+
return { status: "healthy", data: { name } };
|
|
1128
|
+
} catch (error) {
|
|
1129
|
+
const rawMessage = String(error?.message ?? error);
|
|
1130
|
+
const message = options.endpoint.password === undefined || options.endpoint.password.length === 0
|
|
1131
|
+
? rawMessage
|
|
1132
|
+
: rawMessage.replaceAll(options.endpoint.password, SECRET_REDACTION);
|
|
1133
|
+
return {
|
|
1134
|
+
status: "unhealthy",
|
|
1135
|
+
errorCode: error?.code ?? "SLOPPY_E_REDIS_COMMAND_FAILED",
|
|
1136
|
+
message,
|
|
1137
|
+
data: { name, url: options.endpoint.redactedUrl },
|
|
1138
|
+
};
|
|
1139
|
+
}
|
|
1140
|
+
},
|
|
1141
|
+
close: closeClient,
|
|
1142
|
+
dispose: closeClient,
|
|
1143
|
+
};
|
|
1144
|
+
if (ASYNC_DISPOSE !== undefined) {
|
|
1145
|
+
client[ASYNC_DISPOSE] = closeClient;
|
|
1146
|
+
}
|
|
1147
|
+
Object.defineProperty(client, "__sloppyRedisRegistration", {
|
|
1148
|
+
value: Object.freeze({
|
|
1149
|
+
kind: "client",
|
|
1150
|
+
name,
|
|
1151
|
+
token: token(name),
|
|
1152
|
+
create: () => client,
|
|
1153
|
+
options: {
|
|
1154
|
+
url: options.endpoint.redactedUrl,
|
|
1155
|
+
database: options.endpoint.database,
|
|
1156
|
+
connectTimeoutMs: options.connectTimeoutMs,
|
|
1157
|
+
commandTimeoutMs: options.commandTimeoutMs,
|
|
1158
|
+
pool: options.pool,
|
|
1159
|
+
},
|
|
1160
|
+
}),
|
|
1161
|
+
enumerable: false,
|
|
1162
|
+
});
|
|
1163
|
+
return Object.freeze(client);
|
|
1164
|
+
}
|
|
1165
|
+
|
|
1166
|
+
function validateScript(script) {
|
|
1167
|
+
if (typeof script !== "string" || script.length === 0 || script.length > 256 * 1024) {
|
|
1168
|
+
throw redisError("SLOPPY_E_REDIS_INVALID_OPTIONS", "Redis Lua script must be a non-empty string up to 256 KiB.");
|
|
1169
|
+
}
|
|
1170
|
+
}
|
|
1171
|
+
|
|
1172
|
+
function validateSha(sha) {
|
|
1173
|
+
if (typeof sha !== "string" || !/^[a-f0-9]{40}$/iu.test(sha)) {
|
|
1174
|
+
throw redisError("SLOPPY_E_REDIS_INVALID_OPTIONS", "Redis script SHA must be a 40-character hex string.");
|
|
1175
|
+
}
|
|
1176
|
+
}
|
|
1177
|
+
|
|
1178
|
+
function normalizeLockOptions(options = {}) {
|
|
1179
|
+
if (!isPlainObject(options)) {
|
|
1180
|
+
throw redisError("SLOPPY_E_REDIS_INVALID_OPTIONS", "Redis lock options must be a plain object.");
|
|
1181
|
+
}
|
|
1182
|
+
return Object.freeze({
|
|
1183
|
+
prefix: options.prefix ?? "sloppy:locks:",
|
|
1184
|
+
});
|
|
1185
|
+
}
|
|
1186
|
+
|
|
1187
|
+
function lockKey(prefix, name) {
|
|
1188
|
+
normalizeKey(name);
|
|
1189
|
+
return `${prefix}${name}`;
|
|
1190
|
+
}
|
|
1191
|
+
|
|
1192
|
+
function createLocks(client, rawOptions = {}) {
|
|
1193
|
+
const options = normalizeLockOptions(rawOptions);
|
|
1194
|
+
async function acquire(name, acquireOptions = {}) {
|
|
1195
|
+
const ttlMs = pxFromTtl(acquireOptions.ttlMs ?? 30000);
|
|
1196
|
+
const waitTimeoutMs = normalizeNonNegativeTimeout(acquireOptions.waitTimeoutMs, 0, "lock.waitTimeoutMs");
|
|
1197
|
+
const retryDelayMs = normalizeTimeout(acquireOptions.retryDelayMs ?? 50, 50, "lock.retryDelayMs");
|
|
1198
|
+
const key = lockKey(options.prefix, name);
|
|
1199
|
+
const owner = randomOwnerToken();
|
|
1200
|
+
const started = Date.now();
|
|
1201
|
+
while (true) {
|
|
1202
|
+
const ok = replyText(await client.command("SET", [key, owner, "PX", ttlMs, "NX"])) === "OK";
|
|
1203
|
+
if (ok) {
|
|
1204
|
+
let released = false;
|
|
1205
|
+
const lease = {
|
|
1206
|
+
name,
|
|
1207
|
+
key,
|
|
1208
|
+
owner: "[redacted]",
|
|
1209
|
+
async extend(nextTtlMs) {
|
|
1210
|
+
const ttlMs = pxFromTtl(nextTtlMs, "lock.extend ttlMs");
|
|
1211
|
+
if (ttlMs === undefined) {
|
|
1212
|
+
throw redisError("SLOPPY_E_REDIS_INVALID_OPTIONS", "Redis lock extend ttlMs is required.");
|
|
1213
|
+
}
|
|
1214
|
+
const result = await client.script(LOCK_EXTEND_SCRIPT, [key], [owner, ttlMs]);
|
|
1215
|
+
if (result !== 1) {
|
|
1216
|
+
throw redisError("SLOPPY_E_REDIS_LOCK_LOST", "Redis lock lease is no longer owned by this client.");
|
|
1217
|
+
}
|
|
1218
|
+
return true;
|
|
1219
|
+
},
|
|
1220
|
+
async release() {
|
|
1221
|
+
if (released) {
|
|
1222
|
+
return false;
|
|
1223
|
+
}
|
|
1224
|
+
const result = await client.script(LOCK_RELEASE_SCRIPT, [key], [owner]);
|
|
1225
|
+
released = true;
|
|
1226
|
+
if (result !== 1) {
|
|
1227
|
+
throw redisError("SLOPPY_E_REDIS_LOCK_RELEASE_FAILED", "Redis lock release did not own the lease.");
|
|
1228
|
+
}
|
|
1229
|
+
return true;
|
|
1230
|
+
},
|
|
1231
|
+
async dispose() {
|
|
1232
|
+
if (!released) {
|
|
1233
|
+
await lease.release().catch(() => {});
|
|
1234
|
+
}
|
|
1235
|
+
},
|
|
1236
|
+
};
|
|
1237
|
+
if (ASYNC_DISPOSE !== undefined) {
|
|
1238
|
+
lease[ASYNC_DISPOSE] = lease.dispose;
|
|
1239
|
+
}
|
|
1240
|
+
return Object.freeze(lease);
|
|
1241
|
+
}
|
|
1242
|
+
if (waitTimeoutMs === 0 || Date.now() - started >= waitTimeoutMs) {
|
|
1243
|
+
throw redisError("SLOPPY_E_REDIS_LOCK_TIMEOUT", "Redis lock acquisition timed out.");
|
|
1244
|
+
}
|
|
1245
|
+
await new Promise((resolve) => setTimeout(resolve, Math.min(retryDelayMs, waitTimeoutMs - (Date.now() - started))));
|
|
1246
|
+
}
|
|
1247
|
+
}
|
|
1248
|
+
return Object.freeze({ acquire });
|
|
1249
|
+
}
|
|
1250
|
+
|
|
1251
|
+
const Redis = Object.freeze({
|
|
1252
|
+
client: createRedisClient,
|
|
1253
|
+
locks: createLocks,
|
|
1254
|
+
token,
|
|
1255
|
+
encodeCommand,
|
|
1256
|
+
RespParser,
|
|
1257
|
+
SloppyRedisError,
|
|
1258
|
+
Secret,
|
|
1259
|
+
_redactUrl: redactRedisUrl,
|
|
1260
|
+
_tokenDisplay: tokenDisplay,
|
|
1261
|
+
});
|
|
1262
|
+
|
|
1263
|
+
export {
|
|
1264
|
+
Redis,
|
|
1265
|
+
RedisErrorReply,
|
|
1266
|
+
RespParser,
|
|
1267
|
+
SloppyRedisError,
|
|
1268
|
+
decodeValue as __decodeRedisValue,
|
|
1269
|
+
encodeCommand,
|
|
1270
|
+
normalizeKey as __normalizeRedisKey,
|
|
1271
|
+
token as redisToken,
|
|
1272
|
+
};
|