@slopware/sloppy-darwin-arm64 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 +31 -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,1542 @@
|
|
|
1
|
+
import { isPlainObject } from "./internal/validation.js";
|
|
2
|
+
import { disposeAll } from "./internal/disposable.js";
|
|
3
|
+
import { serializeJson } from "./results.js";
|
|
4
|
+
import { isSchema } from "./schema.js";
|
|
5
|
+
import { Text } from "./codec.js";
|
|
6
|
+
import { isRealDataProvider } from "./data.js";
|
|
7
|
+
import { Redis, SloppyRedisError, __decodeRedisValue, __normalizeRedisKey } from "./redis.js";
|
|
8
|
+
|
|
9
|
+
const CACHE_MARKER = Symbol("SloppyCache");
|
|
10
|
+
const DEFAULT_MEMORY_MAX_ENTRIES = 1024;
|
|
11
|
+
const DEFAULT_KEY_MAX_LENGTH = 512;
|
|
12
|
+
const DEFAULT_TAG_MAX_LENGTH = 128;
|
|
13
|
+
const DEFAULT_VALUE_MAX_BYTES = 1024 * 1024;
|
|
14
|
+
const DEFAULT_DISTRIBUTED_TABLE = "sloppy_cache_entries";
|
|
15
|
+
const IDENTIFIER_PATTERN = /^[A-Za-z_][0-9A-Za-z_]{0,62}$/u;
|
|
16
|
+
const ASYNC_DISPOSE = Symbol.asyncDispose;
|
|
17
|
+
const DEFAULT_REDIS_PREFIX = "sloppy:cache:";
|
|
18
|
+
const DEFAULT_REDIS_TTL_MS = 60000;
|
|
19
|
+
const DEFAULT_REDIS_MAX_VALUE_BYTES = DEFAULT_VALUE_MAX_BYTES;
|
|
20
|
+
const REDIS_REVERSE_TAG_SUFFIX = ":tags";
|
|
21
|
+
const REDIS_SET_CACHE_SCRIPT = `
|
|
22
|
+
local existing = redis.call("SMEMBERS", KEYS[3])
|
|
23
|
+
for _, tagKey in ipairs(existing) do
|
|
24
|
+
redis.call("SREM", tagKey, KEYS[1])
|
|
25
|
+
end
|
|
26
|
+
redis.call("DEL", KEYS[3])
|
|
27
|
+
redis.call("SET", KEYS[1], ARGV[1], "PX", ARGV[2])
|
|
28
|
+
redis.call("SADD", KEYS[2], KEYS[1])
|
|
29
|
+
redis.call("PEXPIRE", KEYS[2], ARGV[3])
|
|
30
|
+
if #KEYS > 3 then
|
|
31
|
+
redis.call("SADD", KEYS[2], KEYS[3])
|
|
32
|
+
redis.call("PEXPIRE", KEYS[3], ARGV[3])
|
|
33
|
+
end
|
|
34
|
+
for index = 4, #KEYS do
|
|
35
|
+
redis.call("SADD", KEYS[index], KEYS[1])
|
|
36
|
+
redis.call("SADD", KEYS[3], KEYS[index])
|
|
37
|
+
redis.call("SADD", KEYS[2], KEYS[index])
|
|
38
|
+
redis.call("PEXPIRE", KEYS[index], ARGV[3])
|
|
39
|
+
end
|
|
40
|
+
return 1`;
|
|
41
|
+
const REDIS_REMOVE_CACHE_SCRIPT = `
|
|
42
|
+
local tags = redis.call("SMEMBERS", KEYS[3])
|
|
43
|
+
for _, tagKey in ipairs(tags) do
|
|
44
|
+
redis.call("SREM", tagKey, KEYS[1])
|
|
45
|
+
end
|
|
46
|
+
redis.call("DEL", KEYS[3])
|
|
47
|
+
redis.call("SREM", KEYS[2], KEYS[1])
|
|
48
|
+
redis.call("SREM", KEYS[2], KEYS[3])
|
|
49
|
+
return redis.call("DEL", KEYS[1])`;
|
|
50
|
+
const REDIS_INVALIDATE_TAG_SCRIPT = `
|
|
51
|
+
local entries = redis.call("SMEMBERS", KEYS[1])
|
|
52
|
+
for _, key in ipairs(entries) do
|
|
53
|
+
local reverseKey = key .. ARGV[1]
|
|
54
|
+
local tags = redis.call("SMEMBERS", reverseKey)
|
|
55
|
+
for _, tagKey in ipairs(tags) do
|
|
56
|
+
redis.call("SREM", tagKey, key)
|
|
57
|
+
end
|
|
58
|
+
redis.call("DEL", reverseKey)
|
|
59
|
+
redis.call("DEL", key)
|
|
60
|
+
redis.call("SREM", KEYS[2], key)
|
|
61
|
+
redis.call("SREM", KEYS[2], reverseKey)
|
|
62
|
+
end
|
|
63
|
+
redis.call("DEL", KEYS[1])
|
|
64
|
+
redis.call("SREM", KEYS[2], KEYS[1])
|
|
65
|
+
return #entries`;
|
|
66
|
+
const REDIS_TOUCH_CACHE_SCRIPT = `
|
|
67
|
+
if redis.call("EXISTS", KEYS[1]) == 0 then
|
|
68
|
+
return 0
|
|
69
|
+
end
|
|
70
|
+
redis.call("PEXPIRE", KEYS[1], ARGV[1])
|
|
71
|
+
redis.call("PEXPIRE", KEYS[2], ARGV[2])
|
|
72
|
+
local tags = redis.call("SMEMBERS", KEYS[3])
|
|
73
|
+
if #tags > 0 then
|
|
74
|
+
redis.call("PEXPIRE", KEYS[3], ARGV[2])
|
|
75
|
+
end
|
|
76
|
+
for _, tagKey in ipairs(tags) do
|
|
77
|
+
redis.call("PEXPIRE", tagKey, ARGV[2])
|
|
78
|
+
end
|
|
79
|
+
return 1`;
|
|
80
|
+
|
|
81
|
+
function nowMs(clock = undefined) {
|
|
82
|
+
if (clock !== undefined && typeof clock.now === "function") {
|
|
83
|
+
return clock.now().getTime();
|
|
84
|
+
}
|
|
85
|
+
if (clock !== undefined && typeof clock.monotonicNowMs === "function") {
|
|
86
|
+
return clock.monotonicNowMs();
|
|
87
|
+
}
|
|
88
|
+
return Date.now();
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function nowDate(clock = undefined) {
|
|
92
|
+
if (clock !== undefined && typeof clock.now === "function") {
|
|
93
|
+
return clock.now();
|
|
94
|
+
}
|
|
95
|
+
return new Date();
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function cloneJsonValue(value, subject = "cache value") {
|
|
99
|
+
let text;
|
|
100
|
+
try {
|
|
101
|
+
text = serializeJson(value);
|
|
102
|
+
} catch (error) {
|
|
103
|
+
throw new TypeError(`Sloppy ${subject} must be JSON-serializable.`, { cause: error });
|
|
104
|
+
}
|
|
105
|
+
if (text === undefined) {
|
|
106
|
+
throw new TypeError(`Sloppy ${subject} must be JSON-serializable.`);
|
|
107
|
+
}
|
|
108
|
+
return JSON.parse(text);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function jsonBytes(text) {
|
|
112
|
+
return Text.utf8.encode(text).byteLength;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function stableHash(value) {
|
|
116
|
+
const text = String(value);
|
|
117
|
+
let hash = 0x811c9dc5;
|
|
118
|
+
for (let index = 0; index < text.length; index += 1) {
|
|
119
|
+
hash ^= text.charCodeAt(index) & 0xff;
|
|
120
|
+
hash = Math.imul(hash, 0x01000193) >>> 0;
|
|
121
|
+
}
|
|
122
|
+
return `fnv1a32:${hash.toString(16).padStart(8, "0")}`;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function normalizeName(name, subject = "cache name") {
|
|
126
|
+
if (typeof name !== "string" || name.trim().length === 0 || name.length > 128 || /[\x00-\x1F\x7F]/u.test(name)) {
|
|
127
|
+
throw new TypeError(`Sloppy ${subject} must be a non-empty stable string at most 128 characters.`);
|
|
128
|
+
}
|
|
129
|
+
return name;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function normalizeTokenName(name, subject = "cache token name") {
|
|
133
|
+
if (typeof name !== "string") {
|
|
134
|
+
throw new TypeError(`Sloppy ${subject} must be a non-empty stable token name.`);
|
|
135
|
+
}
|
|
136
|
+
const normalized = name.trim().toLowerCase().replace(/\s+/gu, "-");
|
|
137
|
+
if (normalized.length === 0 || normalized.length > 128 || !/^[a-z0-9][a-z0-9._-]*$/u.test(normalized)) {
|
|
138
|
+
throw new TypeError(`Sloppy ${subject} must start with a letter or digit and contain only letters, digits, '.', '_', or '-'.`);
|
|
139
|
+
}
|
|
140
|
+
return normalized;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function normalizeKey(key, options = {}) {
|
|
144
|
+
const maxLength = options.maxKeyLength ?? DEFAULT_KEY_MAX_LENGTH;
|
|
145
|
+
if (!Number.isInteger(maxLength) || maxLength < 1 || maxLength > 4096) {
|
|
146
|
+
throw new TypeError("Sloppy cache maxKeyLength must be an integer from 1 to 4096.");
|
|
147
|
+
}
|
|
148
|
+
if (typeof key !== "string" || key.length === 0 || key.length > maxLength || /[\x00-\x1F\x7F]/u.test(key)) {
|
|
149
|
+
throw new TypeError(`Sloppy cache key must be a non-empty string at most ${maxLength} characters without control characters.`);
|
|
150
|
+
}
|
|
151
|
+
return key;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function normalizeTag(tag, options = {}) {
|
|
155
|
+
const maxLength = options.maxTagLength ?? DEFAULT_TAG_MAX_LENGTH;
|
|
156
|
+
if (!Number.isInteger(maxLength) || maxLength < 1 || maxLength > 1024) {
|
|
157
|
+
throw new TypeError("Sloppy cache maxTagLength must be an integer from 1 to 1024.");
|
|
158
|
+
}
|
|
159
|
+
if (typeof tag !== "string" || tag.length === 0 || tag.length > maxLength || /[\x00-\x1F\x7F]/u.test(tag)) {
|
|
160
|
+
throw new TypeError(`Sloppy cache tag must be a non-empty string at most ${maxLength} characters without control characters.`);
|
|
161
|
+
}
|
|
162
|
+
return tag;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function normalizeTags(tags, options = {}) {
|
|
166
|
+
if (tags === undefined) {
|
|
167
|
+
return Object.freeze([]);
|
|
168
|
+
}
|
|
169
|
+
if (!Array.isArray(tags)) {
|
|
170
|
+
throw new TypeError("Sloppy cache tags must be an array.");
|
|
171
|
+
}
|
|
172
|
+
return Object.freeze([...new Set(tags.map((tag) => normalizeTag(tag, options)))]);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function normalizeTtlMs(value, subject = "ttlMs") {
|
|
176
|
+
if (value === undefined) {
|
|
177
|
+
return undefined;
|
|
178
|
+
}
|
|
179
|
+
if (!Number.isInteger(value) || value < 0 || value > 0x7fffffff) {
|
|
180
|
+
throw new TypeError(`Sloppy cache ${subject} must be an integer from 0 to 2147483647.`);
|
|
181
|
+
}
|
|
182
|
+
return value;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function normalizeMaxValueBytes(value) {
|
|
186
|
+
if (value === undefined) {
|
|
187
|
+
return undefined;
|
|
188
|
+
}
|
|
189
|
+
if (!Number.isInteger(value) || value < 1 || value > Number.MAX_SAFE_INTEGER) {
|
|
190
|
+
throw new TypeError("Sloppy cache maxValueBytes must be a positive integer.");
|
|
191
|
+
}
|
|
192
|
+
return value;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function normalizeAbsoluteExpiration(value) {
|
|
196
|
+
if (value === undefined) {
|
|
197
|
+
return undefined;
|
|
198
|
+
}
|
|
199
|
+
const date = value instanceof Date ? value : new Date(value);
|
|
200
|
+
const time = date.getTime();
|
|
201
|
+
if (!Number.isFinite(time)) {
|
|
202
|
+
throw new TypeError("Sloppy cache absoluteExpiration must be a valid Date or ISO timestamp.");
|
|
203
|
+
}
|
|
204
|
+
return time;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function normalizeSchema(value) {
|
|
208
|
+
if (value === undefined) {
|
|
209
|
+
return undefined;
|
|
210
|
+
}
|
|
211
|
+
if (!isSchema(value)) {
|
|
212
|
+
throw new TypeError("Sloppy cache schema must be a Schema value.");
|
|
213
|
+
}
|
|
214
|
+
return value;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function normalizeEntryOptions(options = {}) {
|
|
218
|
+
if (options === undefined) {
|
|
219
|
+
return Object.freeze({});
|
|
220
|
+
}
|
|
221
|
+
if (!isPlainObject(options)) {
|
|
222
|
+
throw new TypeError("Sloppy cache entry options must be a plain object.");
|
|
223
|
+
}
|
|
224
|
+
return Object.freeze({
|
|
225
|
+
ttlMs: normalizeTtlMs(options.ttlMs),
|
|
226
|
+
absoluteExpiration: normalizeAbsoluteExpiration(options.absoluteExpiration),
|
|
227
|
+
slidingExpirationMs: normalizeTtlMs(options.slidingExpirationMs, "slidingExpirationMs"),
|
|
228
|
+
tags: normalizeTags(options.tags, options),
|
|
229
|
+
schema: normalizeSchema(options.schema),
|
|
230
|
+
maxValueBytes: normalizeMaxValueBytes(options.maxValueBytes),
|
|
231
|
+
staleWhileRevalidateMs: normalizeTtlMs(options.staleWhileRevalidateMs, "staleWhileRevalidateMs"),
|
|
232
|
+
stampedeProtection: options.stampedeProtection !== false,
|
|
233
|
+
namespace: options.namespace === undefined ? undefined : normalizeName(options.namespace, "cache namespace"),
|
|
234
|
+
cacheNull: options.cacheNull !== false,
|
|
235
|
+
signal: options.signal,
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function expiresAtFromOptions(options, clock = undefined) {
|
|
240
|
+
const base = nowMs(clock);
|
|
241
|
+
let expiresAt = options.ttlMs === undefined ? undefined : base + options.ttlMs;
|
|
242
|
+
if (options.absoluteExpiration !== undefined) {
|
|
243
|
+
expiresAt = expiresAt === undefined ? options.absoluteExpiration : Math.min(expiresAt, options.absoluteExpiration);
|
|
244
|
+
}
|
|
245
|
+
return expiresAt;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function refreshSlidingExpiration(baseMs, slidingExpirationMs, absoluteExpiration = undefined) {
|
|
249
|
+
const refreshed = baseMs + slidingExpirationMs;
|
|
250
|
+
return absoluteExpiration === undefined ? refreshed : Math.min(refreshed, absoluteExpiration);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function isExpired(entry, clock = undefined) {
|
|
254
|
+
return entry.expiresAt !== undefined && nowMs(clock) >= entry.expiresAt;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
function validateValueWithSchema(value, schema, key) {
|
|
258
|
+
if (schema === undefined) {
|
|
259
|
+
return value;
|
|
260
|
+
}
|
|
261
|
+
const result = schema.validate(value);
|
|
262
|
+
if (result.ok) {
|
|
263
|
+
return result.value;
|
|
264
|
+
}
|
|
265
|
+
throw new SloppyCacheError("SLOPPY_E_CACHE_SCHEMA_MISMATCH", `Sloppy cache value for '${stableHash(key)}' failed schema validation.`, {
|
|
266
|
+
keyHash: stableHash(key),
|
|
267
|
+
issues: result.issues,
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
function cacheToken(name = "default") {
|
|
272
|
+
return `cache.${normalizeTokenName(name, "cache token name")}`;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
function cacheMetricName(operation) {
|
|
276
|
+
switch (operation) {
|
|
277
|
+
case "gets":
|
|
278
|
+
return "cache.gets.total";
|
|
279
|
+
case "hits":
|
|
280
|
+
return "cache.hits.total";
|
|
281
|
+
case "misses":
|
|
282
|
+
return "cache.misses.total";
|
|
283
|
+
case "sets":
|
|
284
|
+
return "cache.sets.total";
|
|
285
|
+
case "removes":
|
|
286
|
+
return "cache.removes.total";
|
|
287
|
+
case "evictions":
|
|
288
|
+
return "cache.evictions.total";
|
|
289
|
+
case "expired":
|
|
290
|
+
return "cache.expired.total";
|
|
291
|
+
case "tagInvalidations":
|
|
292
|
+
return "cache.tag_invalidations.total";
|
|
293
|
+
case "factoryRuns":
|
|
294
|
+
return "cache.get_or_create.factory.total";
|
|
295
|
+
case "stampedeWaiters":
|
|
296
|
+
return "cache.stampede.waiters.total";
|
|
297
|
+
case "staleHits":
|
|
298
|
+
return "cache.stale_hits.total";
|
|
299
|
+
default:
|
|
300
|
+
return undefined;
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
function recordCacheMetric(cache, operation) {
|
|
305
|
+
const name = cacheMetricName(operation);
|
|
306
|
+
if (name === undefined || cache.metrics === undefined || cache.metrics === null) {
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
309
|
+
const labels = Object.freeze({
|
|
310
|
+
cache: cache.name,
|
|
311
|
+
backend: cache.kind,
|
|
312
|
+
operation,
|
|
313
|
+
});
|
|
314
|
+
try {
|
|
315
|
+
if (typeof cache.metrics.increment === "function") {
|
|
316
|
+
cache.metrics.increment(name, labels);
|
|
317
|
+
return;
|
|
318
|
+
}
|
|
319
|
+
cache.metrics.counter?.(name, {
|
|
320
|
+
description: "Cache operations by cache name, backend, and operation.",
|
|
321
|
+
})?.inc(labels);
|
|
322
|
+
} catch {
|
|
323
|
+
// Metrics must not change cache behavior.
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
class SloppyCacheError extends Error {
|
|
328
|
+
constructor(code, message, details = undefined) {
|
|
329
|
+
super(message);
|
|
330
|
+
this.name = "SloppyCacheError";
|
|
331
|
+
this.code = code;
|
|
332
|
+
this.details = details === undefined ? undefined : Object.freeze({ ...details });
|
|
333
|
+
this.__sloppyCacheError = true;
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
class BaseCache {
|
|
338
|
+
constructor(name, kind, options = {}) {
|
|
339
|
+
this.name = normalizeName(name ?? "default");
|
|
340
|
+
this.kind = kind;
|
|
341
|
+
this.namespace = normalizeName(options.namespace ?? this.name, "cache namespace");
|
|
342
|
+
this.maxKeyLength = options.maxKeyLength ?? DEFAULT_KEY_MAX_LENGTH;
|
|
343
|
+
this.maxTagLength = options.maxTagLength ?? DEFAULT_TAG_MAX_LENGTH;
|
|
344
|
+
this.clock = options.clock;
|
|
345
|
+
this.metrics = options.metrics;
|
|
346
|
+
this.disposed = false;
|
|
347
|
+
this.inflight = new Map();
|
|
348
|
+
this.counters = {
|
|
349
|
+
gets: 0,
|
|
350
|
+
hits: 0,
|
|
351
|
+
misses: 0,
|
|
352
|
+
sets: 0,
|
|
353
|
+
removes: 0,
|
|
354
|
+
evictions: 0,
|
|
355
|
+
expired: 0,
|
|
356
|
+
tagInvalidations: 0,
|
|
357
|
+
factoryRuns: 0,
|
|
358
|
+
stampedeWaiters: 0,
|
|
359
|
+
staleHits: 0,
|
|
360
|
+
};
|
|
361
|
+
Object.defineProperty(this, CACHE_MARKER, { value: true });
|
|
362
|
+
Object.defineProperty(this, "__sloppyCache", { value: true, enumerable: true });
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
_assertOpen(operation) {
|
|
366
|
+
if (this.disposed) {
|
|
367
|
+
throw new SloppyCacheError("SLOPPY_E_CACHE_DISPOSED", `Sloppy cache '${this.name}' is disposed.`, { operation });
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
_key(key) {
|
|
372
|
+
return normalizeKey(key, this);
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
_entryOptions(options = {}) {
|
|
376
|
+
return normalizeEntryOptions(options);
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
_record(operation) {
|
|
380
|
+
if (Object.prototype.hasOwnProperty.call(this.counters, operation)) {
|
|
381
|
+
this.counters[operation] += 1;
|
|
382
|
+
}
|
|
383
|
+
recordCacheMetric(this, operation);
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
__setMetricsRegistry(metrics) {
|
|
387
|
+
this.metrics = metrics;
|
|
388
|
+
return this;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
async getOrCreate(key, options, factory) {
|
|
392
|
+
this._assertOpen("getOrCreate");
|
|
393
|
+
const normalizedKey = this._key(key);
|
|
394
|
+
const normalizedOptions = this._entryOptions(options);
|
|
395
|
+
if (typeof factory !== "function") {
|
|
396
|
+
throw new TypeError("Sloppy cache getOrCreate factory must be a function.");
|
|
397
|
+
}
|
|
398
|
+
const existing = await this.get(normalizedKey, normalizedOptions);
|
|
399
|
+
if (existing !== undefined) {
|
|
400
|
+
return existing;
|
|
401
|
+
}
|
|
402
|
+
if (normalizedOptions.stampedeProtection === false) {
|
|
403
|
+
return this._runFactory(normalizedKey, normalizedOptions, factory);
|
|
404
|
+
}
|
|
405
|
+
const inflightKey = `${this.namespace}\0${normalizedKey}`;
|
|
406
|
+
const current = this.inflight.get(inflightKey);
|
|
407
|
+
if (current !== undefined) {
|
|
408
|
+
this._record("stampedeWaiters");
|
|
409
|
+
return current;
|
|
410
|
+
}
|
|
411
|
+
const created = this._runFactory(normalizedKey, normalizedOptions, factory)
|
|
412
|
+
.finally(() => {
|
|
413
|
+
this.inflight.delete(inflightKey);
|
|
414
|
+
});
|
|
415
|
+
this.inflight.set(inflightKey, created);
|
|
416
|
+
return created;
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
async _runFactory(key, options, factory) {
|
|
420
|
+
this._record("factoryRuns");
|
|
421
|
+
if (options.signal?.aborted === true) {
|
|
422
|
+
options.signal.throwIfAborted?.();
|
|
423
|
+
throw new SloppyCacheError("SLOPPY_E_CACHE_CANCELLED", "Sloppy cache factory was cancelled before it started.", {
|
|
424
|
+
keyHash: stableHash(key),
|
|
425
|
+
});
|
|
426
|
+
}
|
|
427
|
+
const value = await factory(options.signal);
|
|
428
|
+
if (value === null && options.cacheNull === false) {
|
|
429
|
+
return value;
|
|
430
|
+
}
|
|
431
|
+
const validated = validateValueWithSchema(value, options.schema, key);
|
|
432
|
+
await this.set(key, validated, options);
|
|
433
|
+
return validated;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
delete(key) {
|
|
437
|
+
return this.remove(key);
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
invalidate(key) {
|
|
441
|
+
return this.remove(key);
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
stats() {
|
|
445
|
+
return Object.freeze({
|
|
446
|
+
name: this.name,
|
|
447
|
+
kind: this.kind,
|
|
448
|
+
namespace: this.namespace,
|
|
449
|
+
disposed: this.disposed,
|
|
450
|
+
...this.counters,
|
|
451
|
+
});
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
dispose() {
|
|
455
|
+
this.disposed = true;
|
|
456
|
+
this.inflight.clear();
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
class MemoryCache extends BaseCache {
|
|
461
|
+
constructor(name, options = {}) {
|
|
462
|
+
super(name, "memory", options);
|
|
463
|
+
if (!isPlainObject(options)) {
|
|
464
|
+
throw new TypeError("Sloppy memory cache options must be a plain object.");
|
|
465
|
+
}
|
|
466
|
+
this.maxEntries = options.maxEntries ?? DEFAULT_MEMORY_MAX_ENTRIES;
|
|
467
|
+
if (!Number.isInteger(this.maxEntries) || this.maxEntries < 1 || this.maxEntries > 1_000_000) {
|
|
468
|
+
throw new TypeError("Sloppy memory cache maxEntries must be an integer from 1 to 1000000.");
|
|
469
|
+
}
|
|
470
|
+
this.defaultTtlMs = normalizeTtlMs(options.ttlMs);
|
|
471
|
+
this.entries = new Map();
|
|
472
|
+
Object.seal(this);
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
_entryOptions(options = {}) {
|
|
476
|
+
const normalized = normalizeEntryOptions(options);
|
|
477
|
+
return Object.freeze({
|
|
478
|
+
...normalized,
|
|
479
|
+
ttlMs: normalized.ttlMs ?? this.defaultTtlMs,
|
|
480
|
+
});
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
_entry(key, value, options) {
|
|
484
|
+
const json = serializeJson(value);
|
|
485
|
+
if (json === undefined) {
|
|
486
|
+
throw new TypeError("Sloppy memory cache value must be JSON-serializable.");
|
|
487
|
+
}
|
|
488
|
+
if (jsonBytes(json) > (options.maxValueBytes ?? DEFAULT_VALUE_MAX_BYTES)) {
|
|
489
|
+
throw new SloppyCacheError("SLOPPY_E_CACHE_VALUE_TOO_LARGE", "Sloppy memory cache value exceeds maxValueBytes.", {
|
|
490
|
+
keyHash: stableHash(key),
|
|
491
|
+
});
|
|
492
|
+
}
|
|
493
|
+
const timestamp = nowMs(this.clock);
|
|
494
|
+
return {
|
|
495
|
+
key,
|
|
496
|
+
valueJson: json,
|
|
497
|
+
tags: options.tags,
|
|
498
|
+
createdAt: timestamp,
|
|
499
|
+
updatedAt: timestamp,
|
|
500
|
+
lastAccessedAt: timestamp,
|
|
501
|
+
absoluteExpiration: options.absoluteExpiration,
|
|
502
|
+
expiresAt: expiresAtFromOptions(options, this.clock),
|
|
503
|
+
slidingExpirationMs: options.slidingExpirationMs,
|
|
504
|
+
};
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
_deleteExpired() {
|
|
508
|
+
for (const [key, entry] of this.entries) {
|
|
509
|
+
if (isExpired(entry, this.clock)) {
|
|
510
|
+
this.entries.delete(key);
|
|
511
|
+
this._record("expired");
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
_evictIfNeeded() {
|
|
517
|
+
if (this.entries.size <= this.maxEntries) {
|
|
518
|
+
return;
|
|
519
|
+
}
|
|
520
|
+
this._deleteExpired();
|
|
521
|
+
while (this.entries.size > this.maxEntries) {
|
|
522
|
+
let oldestKey;
|
|
523
|
+
let oldestAccess = Infinity;
|
|
524
|
+
for (const [key, entry] of this.entries) {
|
|
525
|
+
if (entry.lastAccessedAt < oldestAccess) {
|
|
526
|
+
oldestAccess = entry.lastAccessedAt;
|
|
527
|
+
oldestKey = key;
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
if (oldestKey === undefined) {
|
|
531
|
+
return;
|
|
532
|
+
}
|
|
533
|
+
this.entries.delete(oldestKey);
|
|
534
|
+
this._record("evictions");
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
async get(key, schemaOrOptions = undefined) {
|
|
539
|
+
this._assertOpen("get");
|
|
540
|
+
const normalizedKey = this._key(key);
|
|
541
|
+
const options = isSchema(schemaOrOptions)
|
|
542
|
+
? Object.freeze({ schema: schemaOrOptions })
|
|
543
|
+
: this._entryOptions(schemaOrOptions ?? {});
|
|
544
|
+
this._record("gets");
|
|
545
|
+
const entry = this.entries.get(normalizedKey);
|
|
546
|
+
if (entry === undefined) {
|
|
547
|
+
this._record("misses");
|
|
548
|
+
return undefined;
|
|
549
|
+
}
|
|
550
|
+
if (isExpired(entry, this.clock)) {
|
|
551
|
+
this.entries.delete(normalizedKey);
|
|
552
|
+
this._record("expired");
|
|
553
|
+
this._record("misses");
|
|
554
|
+
return undefined;
|
|
555
|
+
}
|
|
556
|
+
entry.lastAccessedAt = nowMs(this.clock);
|
|
557
|
+
if (entry.slidingExpirationMs !== undefined) {
|
|
558
|
+
entry.expiresAt = refreshSlidingExpiration(entry.lastAccessedAt, entry.slidingExpirationMs, entry.absoluteExpiration);
|
|
559
|
+
}
|
|
560
|
+
this._record("hits");
|
|
561
|
+
return validateValueWithSchema(JSON.parse(entry.valueJson), options.schema, normalizedKey);
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
async has(key) {
|
|
565
|
+
return (await this.get(key)) !== undefined;
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
async set(key, value, options = {}) {
|
|
569
|
+
this._assertOpen("set");
|
|
570
|
+
const normalizedKey = this._key(key);
|
|
571
|
+
const normalizedOptions = this._entryOptions(options);
|
|
572
|
+
const validated = validateValueWithSchema(value, normalizedOptions.schema, normalizedKey);
|
|
573
|
+
this.entries.set(normalizedKey, this._entry(normalizedKey, validated, normalizedOptions));
|
|
574
|
+
this._record("sets");
|
|
575
|
+
this._evictIfNeeded();
|
|
576
|
+
return this;
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
async remove(key) {
|
|
580
|
+
this._assertOpen("remove");
|
|
581
|
+
const removed = this.entries.delete(this._key(key));
|
|
582
|
+
if (removed) {
|
|
583
|
+
this._record("removes");
|
|
584
|
+
}
|
|
585
|
+
return removed;
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
async invalidateTag(tag) {
|
|
589
|
+
return this.invalidateTags([tag]);
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
async invalidateTags(tags) {
|
|
593
|
+
this._assertOpen("invalidateTags");
|
|
594
|
+
const normalized = normalizeTags(tags, this);
|
|
595
|
+
let removed = 0;
|
|
596
|
+
for (const [key, entry] of this.entries) {
|
|
597
|
+
if (entry.tags.some((tag) => normalized.includes(tag))) {
|
|
598
|
+
this.entries.delete(key);
|
|
599
|
+
removed += 1;
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
this._record("tagInvalidations");
|
|
603
|
+
return removed;
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
async clear(options = {}) {
|
|
607
|
+
this._assertOpen("clear");
|
|
608
|
+
if (options !== undefined && !isPlainObject(options)) {
|
|
609
|
+
throw new TypeError("Sloppy cache clear options must be a plain object.");
|
|
610
|
+
}
|
|
611
|
+
const count = this.entries.size;
|
|
612
|
+
this.entries.clear();
|
|
613
|
+
return count;
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
async cleanup() {
|
|
617
|
+
this._assertOpen("cleanup");
|
|
618
|
+
const before = this.entries.size;
|
|
619
|
+
this._deleteExpired();
|
|
620
|
+
return before - this.entries.size;
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
stats() {
|
|
624
|
+
return Object.freeze({
|
|
625
|
+
...super.stats(),
|
|
626
|
+
entries: this.entries.size,
|
|
627
|
+
maxEntries: this.maxEntries,
|
|
628
|
+
});
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
dispose() {
|
|
632
|
+
super.dispose();
|
|
633
|
+
this.entries.clear();
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
function providerKind(db, operation) {
|
|
638
|
+
const debug = typeof db?.__debug === "function" ? db.__debug() : undefined;
|
|
639
|
+
if (debug?.kind === "sqlite-connection" && isRealDataProvider(db, "sqlite")) {
|
|
640
|
+
return "sqlite";
|
|
641
|
+
}
|
|
642
|
+
if (debug?.kind === "postgres-connection" && isRealDataProvider(db, "postgres")) {
|
|
643
|
+
return "postgres";
|
|
644
|
+
}
|
|
645
|
+
if (debug?.kind === "sqlserver-connection" && isRealDataProvider(db, "sqlserver")) {
|
|
646
|
+
return "sqlserver";
|
|
647
|
+
}
|
|
648
|
+
const expected = operation === "sqlServer" ? "sqlserver" : operation;
|
|
649
|
+
throw new TypeError(`Sloppy Cache.${operation} requires a real ${expected} connection from sloppy/data.`);
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
function placeholder(kind, index) {
|
|
653
|
+
return kind === "postgres" ? `$${index}` : "?";
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
function validateTableName(value) {
|
|
657
|
+
const table = value ?? DEFAULT_DISTRIBUTED_TABLE;
|
|
658
|
+
if (typeof table !== "string" || !IDENTIFIER_PATTERN.test(table)) {
|
|
659
|
+
throw new TypeError("Sloppy distributed cache table must be a simple SQL identifier.");
|
|
660
|
+
}
|
|
661
|
+
return table;
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
function isSqlServerDuplicateKeyError(error) {
|
|
665
|
+
const code = error?.number ?? error?.code ?? error?.state;
|
|
666
|
+
if (code === 2627 || code === 2601 || code === "2627" || code === "2601") {
|
|
667
|
+
return true;
|
|
668
|
+
}
|
|
669
|
+
const message = String(error?.message ?? error ?? "");
|
|
670
|
+
return /duplicate key|unique constraint|violation of (primary key|unique)/iu.test(message);
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
function affectedRows(result) {
|
|
674
|
+
const value = result?.affectedRows ?? result?.affected_rows ?? result?.rowCount ?? result?.rowsAffected;
|
|
675
|
+
return Number.isInteger(value) ? value : undefined;
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
function distributedSql(kind, table) {
|
|
679
|
+
const columns = "namespace, cache_key, value_json, created_at, updated_at, expires_at, absolute_expires_at, sliding_expiration_ms, tags_json";
|
|
680
|
+
const values = Array.from({ length: 9 }, (_, index) => placeholder(kind, index + 1)).join(", ");
|
|
681
|
+
const updateSet = [
|
|
682
|
+
"value_json = excluded.value_json",
|
|
683
|
+
"updated_at = excluded.updated_at",
|
|
684
|
+
"expires_at = excluded.expires_at",
|
|
685
|
+
"absolute_expires_at = excluded.absolute_expires_at",
|
|
686
|
+
"sliding_expiration_ms = excluded.sliding_expiration_ms",
|
|
687
|
+
"tags_json = excluded.tags_json",
|
|
688
|
+
].join(", ");
|
|
689
|
+
if (kind === "postgres") {
|
|
690
|
+
return Object.freeze({
|
|
691
|
+
ensure: `create table if not exists ${table} (` +
|
|
692
|
+
"namespace text not null, cache_key text not null, value_json text not null, " +
|
|
693
|
+
"created_at text not null, updated_at text not null, expires_at text null, " +
|
|
694
|
+
"absolute_expires_at text null, " +
|
|
695
|
+
"sliding_expiration_ms integer null, tags_json text not null, primary key (namespace, cache_key))",
|
|
696
|
+
ensureAbsoluteExpires: `alter table ${table} add column if not exists absolute_expires_at text null`,
|
|
697
|
+
get: `select value_json, expires_at, absolute_expires_at, sliding_expiration_ms, tags_json from ${table} where namespace = $1 and cache_key = $2`,
|
|
698
|
+
selectNamespace: `select cache_key, tags_json from ${table} where namespace = $1`,
|
|
699
|
+
deleteOne: `delete from ${table} where namespace = $1 and cache_key = $2`,
|
|
700
|
+
clearNamespace: `delete from ${table} where namespace = $1`,
|
|
701
|
+
clearAll: `delete from ${table}`,
|
|
702
|
+
cleanup: `delete from ${table} where expires_at is not null and expires_at <= $1`,
|
|
703
|
+
set: `insert into ${table} (${columns}) values (${values}) on conflict (namespace, cache_key) do update set ${updateSet}`,
|
|
704
|
+
});
|
|
705
|
+
}
|
|
706
|
+
if (kind === "sqlserver") {
|
|
707
|
+
return Object.freeze({
|
|
708
|
+
ensure: `if object_id(N'dbo.${table}', N'U') is null begin create table dbo.${table} (` +
|
|
709
|
+
"namespace nvarchar(128) not null, cache_key nvarchar(256) not null, value_json nvarchar(max) not null, " +
|
|
710
|
+
"created_at nvarchar(64) not null, updated_at nvarchar(64) not null, expires_at nvarchar(64) null, " +
|
|
711
|
+
"absolute_expires_at nvarchar(64) null, " +
|
|
712
|
+
"sliding_expiration_ms int null, tags_json nvarchar(max) not null, constraint " +
|
|
713
|
+
`pk_${table} primary key (namespace, cache_key)) end`,
|
|
714
|
+
ensureAbsoluteExpires: `if col_length(N'dbo.${table}', N'absolute_expires_at') is null alter table dbo.${table} add absolute_expires_at nvarchar(64) null`,
|
|
715
|
+
get: `select value_json, expires_at, absolute_expires_at, sliding_expiration_ms, tags_json from dbo.${table} where namespace = ? and cache_key = ?`,
|
|
716
|
+
selectNamespace: `select cache_key, tags_json from dbo.${table} where namespace = ?`,
|
|
717
|
+
deleteOne: `delete from dbo.${table} where namespace = ? and cache_key = ?`,
|
|
718
|
+
clearNamespace: `delete from dbo.${table} where namespace = ?`,
|
|
719
|
+
clearAll: `delete from dbo.${table}`,
|
|
720
|
+
cleanup: `delete from dbo.${table} where expires_at is not null and expires_at <= ?`,
|
|
721
|
+
update: `update dbo.${table} set value_json = ?, updated_at = ?, expires_at = ?, absolute_expires_at = ?, sliding_expiration_ms = ?, tags_json = ? where namespace = ? and cache_key = ?`,
|
|
722
|
+
insert: `insert into dbo.${table} (${columns}) values (${values})`,
|
|
723
|
+
});
|
|
724
|
+
}
|
|
725
|
+
return Object.freeze({
|
|
726
|
+
ensure: `create table if not exists ${table} (` +
|
|
727
|
+
"namespace text not null, cache_key text not null, value_json text not null, " +
|
|
728
|
+
"created_at text not null, updated_at text not null, expires_at text null, " +
|
|
729
|
+
"absolute_expires_at text null, " +
|
|
730
|
+
"sliding_expiration_ms integer null, tags_json text not null, primary key (namespace, cache_key))",
|
|
731
|
+
ensureAbsoluteExpires: `alter table ${table} add column absolute_expires_at text null`,
|
|
732
|
+
get: `select value_json, expires_at, absolute_expires_at, sliding_expiration_ms, tags_json from ${table} where namespace = ? and cache_key = ?`,
|
|
733
|
+
selectNamespace: `select cache_key, tags_json from ${table} where namespace = ?`,
|
|
734
|
+
deleteOne: `delete from ${table} where namespace = ? and cache_key = ?`,
|
|
735
|
+
clearNamespace: `delete from ${table} where namespace = ?`,
|
|
736
|
+
clearAll: `delete from ${table}`,
|
|
737
|
+
cleanup: `delete from ${table} where expires_at is not null and expires_at <= ?`,
|
|
738
|
+
set: `insert into ${table} (${columns}) values (${values}) on conflict(namespace, cache_key) do update set ${updateSet}`,
|
|
739
|
+
});
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
class DistributedCache extends BaseCache {
|
|
743
|
+
constructor(name, db, kind, options = {}) {
|
|
744
|
+
super(name, kind, options);
|
|
745
|
+
if (!isPlainObject(options)) {
|
|
746
|
+
throw new TypeError("Sloppy distributed cache options must be a plain object.");
|
|
747
|
+
}
|
|
748
|
+
this.db = db;
|
|
749
|
+
this.provider = kind;
|
|
750
|
+
this.table = validateTableName(options.table);
|
|
751
|
+
this.maxValueBytes = options.maxValueBytes ?? DEFAULT_VALUE_MAX_BYTES;
|
|
752
|
+
if (!Number.isInteger(this.maxValueBytes) || this.maxValueBytes < 1) {
|
|
753
|
+
throw new TypeError("Sloppy distributed cache maxValueBytes must be a positive integer.");
|
|
754
|
+
}
|
|
755
|
+
this.defaultTtlMs = normalizeTtlMs(options.ttlMs);
|
|
756
|
+
this.sql = distributedSql(kind, this.table);
|
|
757
|
+
this.initialized = false;
|
|
758
|
+
Object.seal(this);
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
async _ensure() {
|
|
762
|
+
if (this.initialized) {
|
|
763
|
+
return;
|
|
764
|
+
}
|
|
765
|
+
await this.db.exec(this.sql.ensure, []);
|
|
766
|
+
if (this.sql.ensureAbsoluteExpires !== undefined) {
|
|
767
|
+
try {
|
|
768
|
+
await this.db.exec(this.sql.ensureAbsoluteExpires, []);
|
|
769
|
+
} catch (error) {
|
|
770
|
+
if (!/duplicate column|already exists/iu.test(String(error?.message ?? error))) {
|
|
771
|
+
throw error;
|
|
772
|
+
}
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
this.initialized = true;
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
_entryOptions(options = {}) {
|
|
779
|
+
const normalized = normalizeEntryOptions(options);
|
|
780
|
+
return Object.freeze({
|
|
781
|
+
...normalized,
|
|
782
|
+
ttlMs: normalized.ttlMs ?? this.defaultTtlMs,
|
|
783
|
+
});
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
_iso(timeMs) {
|
|
787
|
+
return timeMs === undefined ? null : new Date(timeMs).toISOString();
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
_time(value) {
|
|
791
|
+
if (value === null || value === undefined || value === "") {
|
|
792
|
+
return undefined;
|
|
793
|
+
}
|
|
794
|
+
const time = Date.parse(String(value));
|
|
795
|
+
return Number.isFinite(time) ? time : undefined;
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
_rowOptions(row, fallbackOptions = {}) {
|
|
799
|
+
const expiresAt = this._time(row.expires_at ?? row.expiresAt);
|
|
800
|
+
const absoluteExpiration = this._time(row.absolute_expires_at ?? row.absoluteExpiresAt);
|
|
801
|
+
const tags = JSON.parse(row.tags_json ?? row.tagsJson ?? "[]");
|
|
802
|
+
const slidingExpirationMs = row.sliding_expiration_ms ?? row.slidingExpirationMs;
|
|
803
|
+
const options = {
|
|
804
|
+
tags,
|
|
805
|
+
schema: fallbackOptions.schema,
|
|
806
|
+
};
|
|
807
|
+
if (slidingExpirationMs !== null && slidingExpirationMs !== undefined) {
|
|
808
|
+
options.slidingExpirationMs = Number(slidingExpirationMs);
|
|
809
|
+
if (absoluteExpiration !== undefined) {
|
|
810
|
+
options.absoluteExpiration = absoluteExpiration;
|
|
811
|
+
} else if (expiresAt !== undefined) {
|
|
812
|
+
options.absoluteExpiration = expiresAt;
|
|
813
|
+
}
|
|
814
|
+
} else if (expiresAt !== undefined) {
|
|
815
|
+
options.absoluteExpiration = expiresAt;
|
|
816
|
+
}
|
|
817
|
+
return options;
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
async _refreshSliding(normalizedKey, row, value, fallbackOptions) {
|
|
821
|
+
const slidingExpirationMs = row.sliding_expiration_ms ?? row.slidingExpirationMs;
|
|
822
|
+
if (slidingExpirationMs === null || slidingExpirationMs === undefined) {
|
|
823
|
+
return;
|
|
824
|
+
}
|
|
825
|
+
await this.set(normalizedKey, value, {
|
|
826
|
+
...this._rowOptions(row, fallbackOptions),
|
|
827
|
+
slidingExpirationMs: Number(slidingExpirationMs),
|
|
828
|
+
});
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
async _getWithMetadata(normalizedKey, schemaOrOptions = undefined) {
|
|
832
|
+
await this._ensure();
|
|
833
|
+
const options = isSchema(schemaOrOptions)
|
|
834
|
+
? Object.freeze({ schema: schemaOrOptions })
|
|
835
|
+
: this._entryOptions(schemaOrOptions ?? {});
|
|
836
|
+
const row = await this.db.queryOne(this.sql.get, [this.namespace, normalizedKey]);
|
|
837
|
+
if (row === null || row === undefined) {
|
|
838
|
+
return undefined;
|
|
839
|
+
}
|
|
840
|
+
const expiresAt = this._time(row.expires_at ?? row.expiresAt);
|
|
841
|
+
if (expiresAt !== undefined && nowMs(this.clock) >= expiresAt) {
|
|
842
|
+
await this.remove(normalizedKey);
|
|
843
|
+
this._record("expired");
|
|
844
|
+
return undefined;
|
|
845
|
+
}
|
|
846
|
+
const value = validateValueWithSchema(JSON.parse(row.value_json ?? row.valueJson), options.schema, normalizedKey);
|
|
847
|
+
const entryOptions = this._rowOptions(row, options);
|
|
848
|
+
await this._refreshSliding(normalizedKey, row, value, options);
|
|
849
|
+
return Object.freeze({ value, options: entryOptions });
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
async get(key, schemaOrOptions = undefined) {
|
|
853
|
+
this._assertOpen("get");
|
|
854
|
+
await this._ensure();
|
|
855
|
+
const normalizedKey = this._key(key);
|
|
856
|
+
this._record("gets");
|
|
857
|
+
const entry = await this._getWithMetadata(normalizedKey, schemaOrOptions);
|
|
858
|
+
if (entry === undefined) {
|
|
859
|
+
this._record("misses");
|
|
860
|
+
return undefined;
|
|
861
|
+
}
|
|
862
|
+
this._record("hits");
|
|
863
|
+
return entry.value;
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
async has(key) {
|
|
867
|
+
return (await this.get(key)) !== undefined;
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
async set(key, value, options = {}) {
|
|
871
|
+
this._assertOpen("set");
|
|
872
|
+
await this._ensure();
|
|
873
|
+
const normalizedKey = this._key(key);
|
|
874
|
+
const normalizedOptions = this._entryOptions(options);
|
|
875
|
+
const validated = validateValueWithSchema(value, normalizedOptions.schema, normalizedKey);
|
|
876
|
+
const json = serializeJson(validated);
|
|
877
|
+
if (jsonBytes(json) > this.maxValueBytes) {
|
|
878
|
+
throw new SloppyCacheError("SLOPPY_E_CACHE_VALUE_TOO_LARGE", "Sloppy distributed cache value exceeds maxValueBytes.", {
|
|
879
|
+
keyHash: stableHash(normalizedKey),
|
|
880
|
+
provider: this.provider,
|
|
881
|
+
});
|
|
882
|
+
}
|
|
883
|
+
const timestamp = nowDate(this.clock).toISOString();
|
|
884
|
+
const expiresAt = this._iso(expiresAtFromOptions(normalizedOptions, this.clock));
|
|
885
|
+
const absoluteExpiresAt = this._iso(normalizedOptions.absoluteExpiration);
|
|
886
|
+
const tagsJson = serializeJson(normalizedOptions.tags);
|
|
887
|
+
if (this.provider === "sqlserver") {
|
|
888
|
+
try {
|
|
889
|
+
await this.db.exec(this.sql.insert, [
|
|
890
|
+
this.namespace,
|
|
891
|
+
normalizedKey,
|
|
892
|
+
json,
|
|
893
|
+
timestamp,
|
|
894
|
+
timestamp,
|
|
895
|
+
expiresAt,
|
|
896
|
+
absoluteExpiresAt,
|
|
897
|
+
normalizedOptions.slidingExpirationMs ?? null,
|
|
898
|
+
tagsJson,
|
|
899
|
+
]);
|
|
900
|
+
} catch (error) {
|
|
901
|
+
if (!isSqlServerDuplicateKeyError(error)) {
|
|
902
|
+
throw error;
|
|
903
|
+
}
|
|
904
|
+
const updateResult = await this.db.exec(this.sql.update, [
|
|
905
|
+
json,
|
|
906
|
+
timestamp,
|
|
907
|
+
expiresAt,
|
|
908
|
+
absoluteExpiresAt,
|
|
909
|
+
normalizedOptions.slidingExpirationMs ?? null,
|
|
910
|
+
tagsJson,
|
|
911
|
+
this.namespace,
|
|
912
|
+
normalizedKey,
|
|
913
|
+
]);
|
|
914
|
+
const updated = affectedRows(updateResult);
|
|
915
|
+
if (updated !== undefined && updated < 1) {
|
|
916
|
+
throw new SloppyCacheError("SLOPPY_E_CACHE_WRITE_FAILED", "Sloppy SQL Server distributed cache update did not write a row.", {
|
|
917
|
+
keyHash: stableHash(normalizedKey),
|
|
918
|
+
provider: this.provider,
|
|
919
|
+
});
|
|
920
|
+
}
|
|
921
|
+
}
|
|
922
|
+
} else {
|
|
923
|
+
await this.db.exec(this.sql.set, [
|
|
924
|
+
this.namespace,
|
|
925
|
+
normalizedKey,
|
|
926
|
+
json,
|
|
927
|
+
timestamp,
|
|
928
|
+
timestamp,
|
|
929
|
+
expiresAt,
|
|
930
|
+
absoluteExpiresAt,
|
|
931
|
+
normalizedOptions.slidingExpirationMs ?? null,
|
|
932
|
+
tagsJson,
|
|
933
|
+
]);
|
|
934
|
+
}
|
|
935
|
+
this._record("sets");
|
|
936
|
+
return this;
|
|
937
|
+
}
|
|
938
|
+
|
|
939
|
+
async remove(key) {
|
|
940
|
+
this._assertOpen("remove");
|
|
941
|
+
await this._ensure();
|
|
942
|
+
await this.db.exec(this.sql.deleteOne, [this.namespace, this._key(key)]);
|
|
943
|
+
this._record("removes");
|
|
944
|
+
return true;
|
|
945
|
+
}
|
|
946
|
+
|
|
947
|
+
async invalidateTag(tag) {
|
|
948
|
+
return this.invalidateTags([tag]);
|
|
949
|
+
}
|
|
950
|
+
|
|
951
|
+
async invalidateTags(tags) {
|
|
952
|
+
this._assertOpen("invalidateTags");
|
|
953
|
+
await this._ensure();
|
|
954
|
+
const normalized = normalizeTags(tags, this);
|
|
955
|
+
const rows = await this.db.query(this.sql.selectNamespace, [this.namespace]);
|
|
956
|
+
let removed = 0;
|
|
957
|
+
for (const row of rows) {
|
|
958
|
+
const rowTags = JSON.parse(row.tags_json ?? row.tagsJson ?? "[]");
|
|
959
|
+
if (rowTags.some((current) => normalized.includes(current))) {
|
|
960
|
+
await this.db.exec(this.sql.deleteOne, [this.namespace, row.cache_key ?? row.cacheKey]);
|
|
961
|
+
removed += 1;
|
|
962
|
+
}
|
|
963
|
+
}
|
|
964
|
+
this._record("tagInvalidations");
|
|
965
|
+
return removed;
|
|
966
|
+
}
|
|
967
|
+
|
|
968
|
+
async clear(options = {}) {
|
|
969
|
+
this._assertOpen("clear");
|
|
970
|
+
await this._ensure();
|
|
971
|
+
if (options !== undefined && !isPlainObject(options)) {
|
|
972
|
+
throw new TypeError("Sloppy distributed cache clear options must be a plain object.");
|
|
973
|
+
}
|
|
974
|
+
if (options.dangerouslyClearAll === true) {
|
|
975
|
+
await this.db.exec(this.sql.clearAll, []);
|
|
976
|
+
return true;
|
|
977
|
+
}
|
|
978
|
+
await this.db.exec(this.sql.clearNamespace, [this.namespace]);
|
|
979
|
+
return true;
|
|
980
|
+
}
|
|
981
|
+
|
|
982
|
+
async cleanup() {
|
|
983
|
+
this._assertOpen("cleanup");
|
|
984
|
+
await this._ensure();
|
|
985
|
+
await this.db.exec(this.sql.cleanup, [nowDate(this.clock).toISOString()]);
|
|
986
|
+
return true;
|
|
987
|
+
}
|
|
988
|
+
}
|
|
989
|
+
|
|
990
|
+
class HybridCache extends BaseCache {
|
|
991
|
+
constructor(name, options) {
|
|
992
|
+
if (!isPlainObject(options)) {
|
|
993
|
+
throw new TypeError("Sloppy hybrid cache options must be a plain object.");
|
|
994
|
+
}
|
|
995
|
+
super(name, "hybrid", options);
|
|
996
|
+
if (!isCache(options.memory) || options.memory.kind !== "memory") {
|
|
997
|
+
throw new TypeError("Sloppy hybrid cache memory must be a memory Cache instance.");
|
|
998
|
+
}
|
|
999
|
+
if (!isCache(options.distributed) || options.distributed.kind === "memory" || options.distributed.kind === "hybrid") {
|
|
1000
|
+
throw new TypeError("Sloppy hybrid cache distributed must be a distributed Cache instance.");
|
|
1001
|
+
}
|
|
1002
|
+
this.memory = options.memory;
|
|
1003
|
+
this.distributed = options.distributed;
|
|
1004
|
+
this.populateMemoryOnDistributedHit = options.populateMemoryOnDistributedHit !== false;
|
|
1005
|
+
this.failOpenOnDistributedRead = options.failOpenOnDistributedRead === true;
|
|
1006
|
+
this.owned = options.owned !== false;
|
|
1007
|
+
Object.seal(this);
|
|
1008
|
+
}
|
|
1009
|
+
|
|
1010
|
+
async get(key, schemaOrOptions = undefined) {
|
|
1011
|
+
this._assertOpen("get");
|
|
1012
|
+
this._record("gets");
|
|
1013
|
+
const normalizedKey = this._key(key);
|
|
1014
|
+
const memory = await this.memory.get(normalizedKey, schemaOrOptions);
|
|
1015
|
+
if (memory !== undefined) {
|
|
1016
|
+
this._record("hits");
|
|
1017
|
+
return memory;
|
|
1018
|
+
}
|
|
1019
|
+
let distributed;
|
|
1020
|
+
let distributedOptions;
|
|
1021
|
+
try {
|
|
1022
|
+
if (typeof this.distributed._getWithMetadata === "function") {
|
|
1023
|
+
const entry = await this.distributed._getWithMetadata(normalizedKey, schemaOrOptions);
|
|
1024
|
+
distributed = entry?.value;
|
|
1025
|
+
distributedOptions = entry?.options;
|
|
1026
|
+
} else {
|
|
1027
|
+
distributed = await this.distributed.get(normalizedKey, schemaOrOptions);
|
|
1028
|
+
}
|
|
1029
|
+
} catch (error) {
|
|
1030
|
+
if (this.failOpenOnDistributedRead) {
|
|
1031
|
+
this._record("misses");
|
|
1032
|
+
return undefined;
|
|
1033
|
+
}
|
|
1034
|
+
throw error;
|
|
1035
|
+
}
|
|
1036
|
+
if (distributed === undefined) {
|
|
1037
|
+
this._record("misses");
|
|
1038
|
+
return undefined;
|
|
1039
|
+
}
|
|
1040
|
+
if (this.populateMemoryOnDistributedHit) {
|
|
1041
|
+
await this.memory.set(normalizedKey, distributed, {
|
|
1042
|
+
...(distributedOptions ?? {}),
|
|
1043
|
+
...(isPlainObject(schemaOrOptions) ? schemaOrOptions : {}),
|
|
1044
|
+
});
|
|
1045
|
+
}
|
|
1046
|
+
this._record("hits");
|
|
1047
|
+
return distributed;
|
|
1048
|
+
}
|
|
1049
|
+
|
|
1050
|
+
async has(key) {
|
|
1051
|
+
return (await this.get(key)) !== undefined;
|
|
1052
|
+
}
|
|
1053
|
+
|
|
1054
|
+
async set(key, value, options = {}) {
|
|
1055
|
+
this._assertOpen("set");
|
|
1056
|
+
await this.distributed.set(key, value, options);
|
|
1057
|
+
await this.memory.set(key, value, options);
|
|
1058
|
+
this._record("sets");
|
|
1059
|
+
return this;
|
|
1060
|
+
}
|
|
1061
|
+
|
|
1062
|
+
async remove(key) {
|
|
1063
|
+
this._assertOpen("remove");
|
|
1064
|
+
await Promise.all([this.memory.remove(key), this.distributed.remove(key)]);
|
|
1065
|
+
this._record("removes");
|
|
1066
|
+
return true;
|
|
1067
|
+
}
|
|
1068
|
+
|
|
1069
|
+
async invalidateTag(tag) {
|
|
1070
|
+
return this.invalidateTags([tag]);
|
|
1071
|
+
}
|
|
1072
|
+
|
|
1073
|
+
async invalidateTags(tags) {
|
|
1074
|
+
this._assertOpen("invalidateTags");
|
|
1075
|
+
const removed = await Promise.all([this.memory.invalidateTags(tags), this.distributed.invalidateTags(tags)]);
|
|
1076
|
+
this._record("tagInvalidations");
|
|
1077
|
+
return removed.reduce((sum, value) => sum + Number(value ?? 0), 0);
|
|
1078
|
+
}
|
|
1079
|
+
|
|
1080
|
+
async clear(options = {}) {
|
|
1081
|
+
this._assertOpen("clear");
|
|
1082
|
+
await Promise.all([this.memory.clear(options), this.distributed.clear(options)]);
|
|
1083
|
+
return true;
|
|
1084
|
+
}
|
|
1085
|
+
|
|
1086
|
+
async cleanup(options = {}) {
|
|
1087
|
+
this._assertOpen("cleanup");
|
|
1088
|
+
await Promise.all([this.memory.cleanup(options), this.distributed.cleanup(options)]);
|
|
1089
|
+
return true;
|
|
1090
|
+
}
|
|
1091
|
+
|
|
1092
|
+
stats() {
|
|
1093
|
+
return Object.freeze({
|
|
1094
|
+
...super.stats(),
|
|
1095
|
+
memory: this.memory.stats(),
|
|
1096
|
+
distributed: this.distributed.stats(),
|
|
1097
|
+
});
|
|
1098
|
+
}
|
|
1099
|
+
|
|
1100
|
+
async dispose() {
|
|
1101
|
+
super.dispose();
|
|
1102
|
+
if (this.owned) {
|
|
1103
|
+
await disposeAll([this.memory, this.distributed]);
|
|
1104
|
+
}
|
|
1105
|
+
}
|
|
1106
|
+
}
|
|
1107
|
+
|
|
1108
|
+
function redisCacheError(message) {
|
|
1109
|
+
return new TypeError(`SLOPPY_E_CACHE_INVALID_OPTIONS: ${message}`);
|
|
1110
|
+
}
|
|
1111
|
+
|
|
1112
|
+
function positiveRedisInteger(value, fallback, subject) {
|
|
1113
|
+
const selected = value ?? fallback;
|
|
1114
|
+
if (!Number.isInteger(selected) || selected < 1 || selected > Number.MAX_SAFE_INTEGER) {
|
|
1115
|
+
throw redisCacheError(`${subject} must be a positive integer.`);
|
|
1116
|
+
}
|
|
1117
|
+
return selected;
|
|
1118
|
+
}
|
|
1119
|
+
|
|
1120
|
+
function normalizeRedisCacheKey(key) {
|
|
1121
|
+
return __normalizeRedisKey(String(key));
|
|
1122
|
+
}
|
|
1123
|
+
|
|
1124
|
+
function normalizeRedisCacheArgs(nameOrRedis, maybeOptions) {
|
|
1125
|
+
if (typeof nameOrRedis === "string") {
|
|
1126
|
+
const options = maybeOptions ?? {};
|
|
1127
|
+
if (!isPlainObject(options)) {
|
|
1128
|
+
throw redisCacheError("Cache.redis options must be a plain object.");
|
|
1129
|
+
}
|
|
1130
|
+
return { name: normalizeName(nameOrRedis), options };
|
|
1131
|
+
}
|
|
1132
|
+
if (nameOrRedis?.__sloppyRedisRegistration !== undefined || typeof nameOrRedis?.command === "function") {
|
|
1133
|
+
const options = maybeOptions ?? {};
|
|
1134
|
+
if (!isPlainObject(options)) {
|
|
1135
|
+
throw redisCacheError("Cache.redis options must be a plain object.");
|
|
1136
|
+
}
|
|
1137
|
+
return { name: normalizeName(options.name ?? "default"), options: { ...options, client: nameOrRedis } };
|
|
1138
|
+
}
|
|
1139
|
+
throw redisCacheError("Cache.redis expects a cache name or Redis client.");
|
|
1140
|
+
}
|
|
1141
|
+
|
|
1142
|
+
function redisCacheKeys(cache, key) {
|
|
1143
|
+
const hash = stableHash(key);
|
|
1144
|
+
return Object.freeze({
|
|
1145
|
+
hash,
|
|
1146
|
+
entry: `${cache.prefix}${cache.name}:entry:${hash}`,
|
|
1147
|
+
tags: `${cache.prefix}${cache.name}:entry:${hash}${REDIS_REVERSE_TAG_SUFFIX}`,
|
|
1148
|
+
keys: `${cache.prefix}${cache.name}:keys`,
|
|
1149
|
+
});
|
|
1150
|
+
}
|
|
1151
|
+
|
|
1152
|
+
function redisTagKey(cache, tag) {
|
|
1153
|
+
return `${cache.prefix}${cache.name}:tag:${stableHash(tag)}`;
|
|
1154
|
+
}
|
|
1155
|
+
|
|
1156
|
+
function redisTtlFor(cache, options) {
|
|
1157
|
+
const expiresAt = expiresAtFromOptions(options, cache.clock);
|
|
1158
|
+
if (expiresAt === undefined) {
|
|
1159
|
+
return cache.defaultTtlMs;
|
|
1160
|
+
}
|
|
1161
|
+
return Math.max(1, Math.ceil(expiresAt - nowMs(cache.clock)));
|
|
1162
|
+
}
|
|
1163
|
+
|
|
1164
|
+
function encodeRedisCacheValue(value, maxValueBytes, key) {
|
|
1165
|
+
const text = serializeJson(value);
|
|
1166
|
+
if (text === undefined) {
|
|
1167
|
+
throw redisCacheError("Cache value must be JSON serializable.");
|
|
1168
|
+
}
|
|
1169
|
+
if (jsonBytes(text) > maxValueBytes) {
|
|
1170
|
+
throw new SloppyRedisError("SLOPPY_E_REDIS_VALUE_TOO_LARGE", "Cache value exceeds maxValueBytes.", {
|
|
1171
|
+
keyHash: stableHash(key),
|
|
1172
|
+
});
|
|
1173
|
+
}
|
|
1174
|
+
return `J:${text}`;
|
|
1175
|
+
}
|
|
1176
|
+
|
|
1177
|
+
function decodeRedisCacheValue(value) {
|
|
1178
|
+
if (value === null || value === undefined) {
|
|
1179
|
+
return undefined;
|
|
1180
|
+
}
|
|
1181
|
+
return __decodeRedisValue(value);
|
|
1182
|
+
}
|
|
1183
|
+
|
|
1184
|
+
class RedisCache extends BaseCache {
|
|
1185
|
+
constructor(nameOrRedis, maybeOptions = undefined) {
|
|
1186
|
+
const parsed = normalizeRedisCacheArgs(nameOrRedis, maybeOptions);
|
|
1187
|
+
super(parsed.name, "redis", parsed.options);
|
|
1188
|
+
const options = parsed.options;
|
|
1189
|
+
const ownsClient = options.client === undefined;
|
|
1190
|
+
const client = ownsClient ? Redis.client(this.name, options) : options.client;
|
|
1191
|
+
if (client === undefined || typeof client.command !== "function") {
|
|
1192
|
+
throw redisCacheError("Cache.redis client must be a Redis client.");
|
|
1193
|
+
}
|
|
1194
|
+
const prefix = options.prefix ?? DEFAULT_REDIS_PREFIX;
|
|
1195
|
+
if (typeof prefix !== "string" || prefix.length === 0 || prefix.includes("\0")) {
|
|
1196
|
+
throw redisCacheError("Cache.redis prefix must be a non-empty string without NUL.");
|
|
1197
|
+
}
|
|
1198
|
+
this.client = client;
|
|
1199
|
+
this.prefix = prefix;
|
|
1200
|
+
this.defaultTtlMs = positiveRedisInteger(options.ttlMs, DEFAULT_REDIS_TTL_MS, "Cache.redis ttlMs");
|
|
1201
|
+
this.maxValueBytes = positiveRedisInteger(options.maxValueBytes, DEFAULT_REDIS_MAX_VALUE_BYTES, "Cache.redis maxValueBytes");
|
|
1202
|
+
this.disposeClient = ownsClient || options.disposeClient === true || options.ownsClient === true;
|
|
1203
|
+
this.state = Object.freeze({
|
|
1204
|
+
kind: "redis",
|
|
1205
|
+
name: this.name,
|
|
1206
|
+
prefix: this.prefix,
|
|
1207
|
+
client: this.client.name,
|
|
1208
|
+
});
|
|
1209
|
+
Object.defineProperty(this, "__sloppyCacheRegistration", {
|
|
1210
|
+
value: Object.freeze({
|
|
1211
|
+
kind: "redis",
|
|
1212
|
+
name: this.name,
|
|
1213
|
+
token: cacheToken(this.name),
|
|
1214
|
+
create: () => this,
|
|
1215
|
+
}),
|
|
1216
|
+
enumerable: false,
|
|
1217
|
+
});
|
|
1218
|
+
if (ASYNC_DISPOSE !== undefined) {
|
|
1219
|
+
this[ASYNC_DISPOSE] = this.dispose;
|
|
1220
|
+
}
|
|
1221
|
+
Object.seal(this);
|
|
1222
|
+
}
|
|
1223
|
+
|
|
1224
|
+
_entryOptions(options = {}) {
|
|
1225
|
+
const normalized = normalizeEntryOptions(options);
|
|
1226
|
+
return Object.freeze({
|
|
1227
|
+
...normalized,
|
|
1228
|
+
ttlMs: normalized.ttlMs ?? this.defaultTtlMs,
|
|
1229
|
+
});
|
|
1230
|
+
}
|
|
1231
|
+
|
|
1232
|
+
_redisKey(key) {
|
|
1233
|
+
return normalizeRedisCacheKey(this._key(key));
|
|
1234
|
+
}
|
|
1235
|
+
|
|
1236
|
+
async _getWithMetadata(normalizedKey, schemaOrOptions = undefined) {
|
|
1237
|
+
const options = isSchema(schemaOrOptions)
|
|
1238
|
+
? Object.freeze({ schema: schemaOrOptions })
|
|
1239
|
+
: this._entryOptions(schemaOrOptions ?? {});
|
|
1240
|
+
const keys = redisCacheKeys(this, normalizedKey);
|
|
1241
|
+
const value = await this.client.command("GET", [keys.entry]);
|
|
1242
|
+
if (value === null || value === undefined) {
|
|
1243
|
+
return undefined;
|
|
1244
|
+
}
|
|
1245
|
+
if (options.slidingExpirationMs !== undefined) {
|
|
1246
|
+
const ttlMs = positiveRedisInteger(options.slidingExpirationMs, undefined, "slidingExpirationMs");
|
|
1247
|
+
await this.client.script(REDIS_TOUCH_CACHE_SCRIPT, [keys.entry, keys.keys, keys.tags], [ttlMs, Math.max(ttlMs, this.defaultTtlMs)]);
|
|
1248
|
+
}
|
|
1249
|
+
const decoded = decodeRedisCacheValue(value);
|
|
1250
|
+
return Object.freeze({
|
|
1251
|
+
value: validateValueWithSchema(decoded, options.schema, normalizedKey),
|
|
1252
|
+
options,
|
|
1253
|
+
});
|
|
1254
|
+
}
|
|
1255
|
+
|
|
1256
|
+
async get(key, schemaOrOptions = undefined) {
|
|
1257
|
+
this._assertOpen("get");
|
|
1258
|
+
const normalizedKey = this._redisKey(key);
|
|
1259
|
+
this._record("gets");
|
|
1260
|
+
const entry = await this._getWithMetadata(normalizedKey, schemaOrOptions);
|
|
1261
|
+
if (entry === undefined) {
|
|
1262
|
+
this._record("misses");
|
|
1263
|
+
return undefined;
|
|
1264
|
+
}
|
|
1265
|
+
this._record("hits");
|
|
1266
|
+
return entry.value;
|
|
1267
|
+
}
|
|
1268
|
+
|
|
1269
|
+
async has(key) {
|
|
1270
|
+
this._assertOpen("has");
|
|
1271
|
+
const keys = redisCacheKeys(this, this._redisKey(key));
|
|
1272
|
+
return await this.client.exists(keys.entry);
|
|
1273
|
+
}
|
|
1274
|
+
|
|
1275
|
+
async getOrCreate(key, optionsOrFactory, maybeFactory = undefined) {
|
|
1276
|
+
if (typeof optionsOrFactory === "function") {
|
|
1277
|
+
return super.getOrCreate(key, maybeFactory ?? {}, optionsOrFactory);
|
|
1278
|
+
}
|
|
1279
|
+
return super.getOrCreate(key, optionsOrFactory, maybeFactory);
|
|
1280
|
+
}
|
|
1281
|
+
|
|
1282
|
+
async set(key, value, options = {}) {
|
|
1283
|
+
this._assertOpen("set");
|
|
1284
|
+
const normalizedKey = this._redisKey(key);
|
|
1285
|
+
const normalizedOptions = this._entryOptions(options);
|
|
1286
|
+
const validated = validateValueWithSchema(value, normalizedOptions.schema, normalizedKey);
|
|
1287
|
+
const keys = redisCacheKeys(this, normalizedKey);
|
|
1288
|
+
const tags = normalizeTags(normalizedOptions.tags, this);
|
|
1289
|
+
const ttlMs = redisTtlFor(this, normalizedOptions);
|
|
1290
|
+
const encoded = encodeRedisCacheValue(validated, this.maxValueBytes, normalizedKey);
|
|
1291
|
+
const tagKeys = tags.map((tag) => redisTagKey(this, tag));
|
|
1292
|
+
await this.client.script(REDIS_SET_CACHE_SCRIPT, [keys.entry, keys.keys, keys.tags, ...tagKeys], [
|
|
1293
|
+
encoded,
|
|
1294
|
+
ttlMs,
|
|
1295
|
+
Math.max(ttlMs, this.defaultTtlMs),
|
|
1296
|
+
]);
|
|
1297
|
+
this._record("sets");
|
|
1298
|
+
return true;
|
|
1299
|
+
}
|
|
1300
|
+
|
|
1301
|
+
async remove(key) {
|
|
1302
|
+
this._assertOpen("remove");
|
|
1303
|
+
const keys = redisCacheKeys(this, this._redisKey(key));
|
|
1304
|
+
const count = await this.client.script(REDIS_REMOVE_CACHE_SCRIPT, [keys.entry, keys.keys, keys.tags], []);
|
|
1305
|
+
if (Number(count) > 0) {
|
|
1306
|
+
this._record("removes");
|
|
1307
|
+
}
|
|
1308
|
+
return Number(count) > 0;
|
|
1309
|
+
}
|
|
1310
|
+
|
|
1311
|
+
async invalidateTag(tag) {
|
|
1312
|
+
return this.invalidateTags([tag]);
|
|
1313
|
+
}
|
|
1314
|
+
|
|
1315
|
+
async invalidateTags(tags) {
|
|
1316
|
+
this._assertOpen("invalidateTags");
|
|
1317
|
+
const normalized = normalizeTags(tags, this);
|
|
1318
|
+
let total = 0;
|
|
1319
|
+
for (const tag of normalized) {
|
|
1320
|
+
const deleted = await this.client.script(REDIS_INVALIDATE_TAG_SCRIPT, [redisTagKey(this, tag), `${this.prefix}${this.name}:keys`], [REDIS_REVERSE_TAG_SUFFIX]);
|
|
1321
|
+
total += Number(deleted) || 0;
|
|
1322
|
+
}
|
|
1323
|
+
this._record("tagInvalidations");
|
|
1324
|
+
return total;
|
|
1325
|
+
}
|
|
1326
|
+
|
|
1327
|
+
async clear(options = {}) {
|
|
1328
|
+
this._assertOpen("clear");
|
|
1329
|
+
if (options !== undefined && !isPlainObject(options)) {
|
|
1330
|
+
throw new TypeError("Sloppy redis cache clear options must be a plain object.");
|
|
1331
|
+
}
|
|
1332
|
+
let cursor = "0";
|
|
1333
|
+
let removed = 0;
|
|
1334
|
+
do {
|
|
1335
|
+
const scan = await this.client.scan({ cursor, match: `${this.prefix}${this.name}:*`, count: 100 });
|
|
1336
|
+
cursor = scan.cursor;
|
|
1337
|
+
if (scan.keys.length > 0) {
|
|
1338
|
+
const replies = await this.client.pipeline(scan.keys.map((key) => ["DEL", key]));
|
|
1339
|
+
removed += replies.reduce((sum, value) => sum + (Number(value) || 0), 0);
|
|
1340
|
+
}
|
|
1341
|
+
} while (cursor !== "0");
|
|
1342
|
+
return removed;
|
|
1343
|
+
}
|
|
1344
|
+
|
|
1345
|
+
async cleanup() {
|
|
1346
|
+
this._assertOpen("cleanup");
|
|
1347
|
+
return undefined;
|
|
1348
|
+
}
|
|
1349
|
+
|
|
1350
|
+
health() {
|
|
1351
|
+
return this.client.health();
|
|
1352
|
+
}
|
|
1353
|
+
|
|
1354
|
+
stats() {
|
|
1355
|
+
return Object.freeze({
|
|
1356
|
+
...super.stats(),
|
|
1357
|
+
prefix: this.prefix,
|
|
1358
|
+
deletes: this.counters.removes,
|
|
1359
|
+
tagInvalidations: this.counters.tagInvalidations,
|
|
1360
|
+
});
|
|
1361
|
+
}
|
|
1362
|
+
|
|
1363
|
+
async dispose() {
|
|
1364
|
+
if (this.disposed) {
|
|
1365
|
+
return;
|
|
1366
|
+
}
|
|
1367
|
+
super.dispose();
|
|
1368
|
+
if (this.disposeClient) {
|
|
1369
|
+
await this.client.dispose();
|
|
1370
|
+
}
|
|
1371
|
+
}
|
|
1372
|
+
}
|
|
1373
|
+
|
|
1374
|
+
function createRedisCache(nameOrRedis, maybeOptions = undefined) {
|
|
1375
|
+
return new RedisCache(nameOrRedis, maybeOptions);
|
|
1376
|
+
}
|
|
1377
|
+
class NoopCache extends BaseCache {
|
|
1378
|
+
constructor(name = "noop") {
|
|
1379
|
+
super(name, "noop", {});
|
|
1380
|
+
}
|
|
1381
|
+
async get(key, schemaOrOptions = undefined) {
|
|
1382
|
+
this._assertOpen("get");
|
|
1383
|
+
this._key(key);
|
|
1384
|
+
if (isSchema(schemaOrOptions)) {
|
|
1385
|
+
return undefined;
|
|
1386
|
+
}
|
|
1387
|
+
this._entryOptions(schemaOrOptions ?? {});
|
|
1388
|
+
return undefined;
|
|
1389
|
+
}
|
|
1390
|
+
async has(key) {
|
|
1391
|
+
this._assertOpen("has");
|
|
1392
|
+
this._key(key);
|
|
1393
|
+
return false;
|
|
1394
|
+
}
|
|
1395
|
+
async set(key, value, options = {}) {
|
|
1396
|
+
this._assertOpen("set");
|
|
1397
|
+
this._key(key);
|
|
1398
|
+
this._entryOptions(options);
|
|
1399
|
+
cloneJsonValue(value);
|
|
1400
|
+
return this;
|
|
1401
|
+
}
|
|
1402
|
+
async remove(key) {
|
|
1403
|
+
this._assertOpen("remove");
|
|
1404
|
+
this._key(key);
|
|
1405
|
+
return false;
|
|
1406
|
+
}
|
|
1407
|
+
async invalidateTag(tag) {
|
|
1408
|
+
this._assertOpen("invalidateTag");
|
|
1409
|
+
normalizeTag(tag, this);
|
|
1410
|
+
return 0;
|
|
1411
|
+
}
|
|
1412
|
+
async invalidateTags(tags) {
|
|
1413
|
+
this._assertOpen("invalidateTags");
|
|
1414
|
+
normalizeTags(tags, this);
|
|
1415
|
+
return 0;
|
|
1416
|
+
}
|
|
1417
|
+
async clear(options = {}) {
|
|
1418
|
+
this._assertOpen("clear");
|
|
1419
|
+
if (options !== undefined && !isPlainObject(options)) {
|
|
1420
|
+
throw new TypeError("Sloppy cache clear options must be a plain object.");
|
|
1421
|
+
}
|
|
1422
|
+
return 0;
|
|
1423
|
+
}
|
|
1424
|
+
async cleanup() {
|
|
1425
|
+
this._assertOpen("cleanup");
|
|
1426
|
+
return 0;
|
|
1427
|
+
}
|
|
1428
|
+
}
|
|
1429
|
+
|
|
1430
|
+
function isCache(value) {
|
|
1431
|
+
return value !== null && typeof value === "object" && value[CACHE_MARKER] === true && value.__sloppyCache === true;
|
|
1432
|
+
}
|
|
1433
|
+
|
|
1434
|
+
function cacheFromFactoryArgs(kind, nameOrOptions, maybeOptions) {
|
|
1435
|
+
if (typeof nameOrOptions === "string") {
|
|
1436
|
+
return { name: nameOrOptions, options: maybeOptions ?? {} };
|
|
1437
|
+
}
|
|
1438
|
+
return { name: "default", options: nameOrOptions ?? {} };
|
|
1439
|
+
}
|
|
1440
|
+
|
|
1441
|
+
function distributedFromFactoryArgs(operation, dbOrOptions, maybeOptions) {
|
|
1442
|
+
if (isPlainObject(dbOrOptions) && dbOrOptions.db !== undefined) {
|
|
1443
|
+
return {
|
|
1444
|
+
name: dbOrOptions.name ?? "default",
|
|
1445
|
+
db: dbOrOptions.db,
|
|
1446
|
+
options: { ...dbOrOptions, ...maybeOptions },
|
|
1447
|
+
};
|
|
1448
|
+
}
|
|
1449
|
+
return {
|
|
1450
|
+
name: maybeOptions?.name ?? "default",
|
|
1451
|
+
db: dbOrOptions,
|
|
1452
|
+
options: maybeOptions ?? {},
|
|
1453
|
+
};
|
|
1454
|
+
}
|
|
1455
|
+
|
|
1456
|
+
function createDistributed(operation, expectedKind, dbOrOptions, maybeOptions) {
|
|
1457
|
+
const { name, db, options } = distributedFromFactoryArgs(operation, dbOrOptions, maybeOptions);
|
|
1458
|
+
const actualKind = providerKind(db, operation);
|
|
1459
|
+
if (actualKind !== expectedKind) {
|
|
1460
|
+
throw new TypeError(`Sloppy Cache.${operation} expected ${expectedKind} connection, got ${actualKind}.`);
|
|
1461
|
+
}
|
|
1462
|
+
return new DistributedCache(name, db, actualKind, options);
|
|
1463
|
+
}
|
|
1464
|
+
|
|
1465
|
+
function key(...parts) {
|
|
1466
|
+
if (parts.length === 0) {
|
|
1467
|
+
throw new TypeError("Sloppy Cache.key requires at least one part.");
|
|
1468
|
+
}
|
|
1469
|
+
return parts.map((part) => {
|
|
1470
|
+
if (part === null || part === undefined) {
|
|
1471
|
+
throw new TypeError("Sloppy Cache.key parts must not be null or undefined.");
|
|
1472
|
+
}
|
|
1473
|
+
return encodeURIComponent(String(part));
|
|
1474
|
+
}).join(":");
|
|
1475
|
+
}
|
|
1476
|
+
|
|
1477
|
+
function tags(...values) {
|
|
1478
|
+
return normalizeTags(values.flat());
|
|
1479
|
+
}
|
|
1480
|
+
|
|
1481
|
+
const Cache = Object.freeze({
|
|
1482
|
+
memory(nameOrOptions = undefined, maybeOptions = undefined) {
|
|
1483
|
+
const { name, options } = cacheFromFactoryArgs("memory", nameOrOptions, maybeOptions);
|
|
1484
|
+
return new MemoryCache(name, options);
|
|
1485
|
+
},
|
|
1486
|
+
sqlite(dbOrOptions, maybeOptions = undefined) {
|
|
1487
|
+
return createDistributed("sqlite", "sqlite", dbOrOptions, maybeOptions);
|
|
1488
|
+
},
|
|
1489
|
+
postgres(dbOrOptions, maybeOptions = undefined) {
|
|
1490
|
+
return createDistributed("postgres", "postgres", dbOrOptions, maybeOptions);
|
|
1491
|
+
},
|
|
1492
|
+
sqlServer(dbOrOptions, maybeOptions = undefined) {
|
|
1493
|
+
return createDistributed("sqlServer", "sqlserver", dbOrOptions, maybeOptions);
|
|
1494
|
+
},
|
|
1495
|
+
sqlserver(dbOrOptions, maybeOptions = undefined) {
|
|
1496
|
+
return createDistributed("sqlServer", "sqlserver", dbOrOptions, maybeOptions);
|
|
1497
|
+
},
|
|
1498
|
+
redis(nameOrRedis, maybeOptions = undefined) {
|
|
1499
|
+
return createRedisCache(nameOrRedis, maybeOptions);
|
|
1500
|
+
},
|
|
1501
|
+
distributed(kind, db, options = undefined) {
|
|
1502
|
+
if (kind === "sqlite") {
|
|
1503
|
+
return createDistributed("sqlite", "sqlite", db, options);
|
|
1504
|
+
}
|
|
1505
|
+
if (kind === "postgres") {
|
|
1506
|
+
return createDistributed("postgres", "postgres", db, options);
|
|
1507
|
+
}
|
|
1508
|
+
if (kind === "sqlserver" || kind === "sqlServer") {
|
|
1509
|
+
return createDistributed("sqlServer", "sqlserver", db, options);
|
|
1510
|
+
}
|
|
1511
|
+
throw new TypeError("Sloppy Cache.distributed kind must be sqlite, postgres, or sqlserver.");
|
|
1512
|
+
},
|
|
1513
|
+
hybrid(name, options) {
|
|
1514
|
+
return new HybridCache(name, options);
|
|
1515
|
+
},
|
|
1516
|
+
noop(name = "noop") {
|
|
1517
|
+
return new NoopCache(name);
|
|
1518
|
+
},
|
|
1519
|
+
token: cacheToken,
|
|
1520
|
+
key,
|
|
1521
|
+
tags,
|
|
1522
|
+
isCache,
|
|
1523
|
+
keyHash: stableHash,
|
|
1524
|
+
__testing: Object.freeze({
|
|
1525
|
+
distributedSql,
|
|
1526
|
+
normalizeEntryOptions,
|
|
1527
|
+
createRedisCache,
|
|
1528
|
+
}),
|
|
1529
|
+
});
|
|
1530
|
+
|
|
1531
|
+
export {
|
|
1532
|
+
Cache,
|
|
1533
|
+
createRedisCache,
|
|
1534
|
+
SloppyCacheError,
|
|
1535
|
+
isCache,
|
|
1536
|
+
normalizeEntryOptions,
|
|
1537
|
+
normalizeKey,
|
|
1538
|
+
normalizeName,
|
|
1539
|
+
normalizeTag,
|
|
1540
|
+
normalizeTags,
|
|
1541
|
+
stableHash,
|
|
1542
|
+
};
|