@apifuse/provider-sdk 2.2.0-beta.5 → 2.2.0-beta.51
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/AUTHORING.md +537 -33
- package/CHANGELOG.md +205 -1
- package/README.md +83 -22
- package/SUBMISSION.md +2 -2
- package/bin/apifuse-check.ts +165 -11
- package/bin/apifuse-dev.ts +57 -13
- package/bin/apifuse-migrate-operation-declaration.ts +55 -0
- package/bin/apifuse-migrate-shape.ts +202 -0
- package/bin/apifuse-pack-check.ts +22 -2
- package/bin/apifuse-pack-smoke.ts +57 -2
- package/bin/apifuse-pack-types.ts +357 -38
- package/bin/apifuse-perf.ts +14 -13
- package/bin/apifuse-record.ts +709 -72
- package/bin/apifuse-submit-check.ts +2344 -324
- package/bin/apifuse-sync-assets.ts +117 -0
- package/bin/submit-check-delimited-text.ts +50 -0
- package/dist/auth-turn/index.d.ts +3 -3
- package/dist/auth-turn/index.js +1 -1
- package/dist/auth.d.ts +14 -0
- package/dist/auth.js +67 -0
- package/dist/ceremonies/index.d.ts +16 -0
- package/dist/ceremonies/index.js +141 -36
- package/dist/cli/commands.d.ts +1 -1
- package/dist/cli/commands.js +27 -0
- package/dist/cli/create.d.ts +4 -1
- package/dist/cli/create.js +42 -37
- package/dist/cli/migrate-operation-declaration.d.ts +59 -0
- package/dist/cli/migrate-operation-declaration.js +1249 -0
- package/dist/cli/migrate-operation-shape.d.ts +44 -0
- package/dist/cli/migrate-operation-shape.js +113 -0
- package/dist/cli/migrate-provider-shape.d.ts +52 -0
- package/dist/cli/migrate-provider-shape.js +578 -0
- package/dist/cli/prompt-assets.d.ts +80 -0
- package/dist/cli/prompt-assets.js +743 -0
- package/dist/cli/templates/provider/AGENTS.md.tpl +17 -8
- package/dist/cli/templates/provider/Dockerfile.tpl +1 -1
- package/dist/cli/templates/provider/README.md.tpl +5 -5
- package/dist/cli/templates/provider/index.ts.tpl +6 -3
- package/dist/cli/templates/provider/operations/ping.ts.tpl +4 -1
- package/dist/cli/templates/provider/provider.json.tpl +6 -0
- package/dist/config/loader.d.ts +179 -16
- package/dist/config/loader.js +435 -127
- package/dist/contract-serialization.js +4 -8
- package/dist/contract-types.d.ts +12 -5
- package/dist/contract.js +24 -10
- package/dist/declaration-validation.d.ts +33 -0
- package/dist/declaration-validation.js +266 -0
- package/dist/define.d.ts +66 -29
- package/dist/define.js +778 -69
- package/dist/dev.d.ts +3 -0
- package/dist/dev.js +1 -1
- package/dist/engine.d.ts +78 -0
- package/dist/engine.js +133 -0
- package/dist/error-observability.d.ts +7 -0
- package/dist/error-observability.js +61 -0
- package/dist/error-resolution.d.ts +4 -0
- package/dist/error-resolution.js +122 -0
- package/dist/errors.d.ts +33 -0
- package/dist/errors.js +40 -0
- package/dist/fixture-sanitization.d.ts +28 -0
- package/dist/fixture-sanitization.js +227 -0
- package/dist/health-scenario.d.ts +1842 -0
- package/dist/health-scenario.js +624 -0
- package/dist/index.d.ts +21 -10
- package/dist/index.js +12 -7
- package/dist/lint.d.ts +23 -11
- package/dist/lint.js +572 -69
- package/dist/native-address.d.ts +43 -0
- package/dist/native-address.js +281 -0
- package/dist/native-egress-policy.d.ts +31 -0
- package/dist/native-egress-policy.js +288 -0
- package/dist/observability.d.ts +5 -2
- package/dist/observability.js +48 -1
- package/dist/provider.d.ts +10 -2
- package/dist/provider.js +4 -1
- package/dist/runtime/auth-flow.d.ts +5 -1
- package/dist/runtime/auth-flow.js +6 -0
- package/dist/runtime/browser.d.ts +1 -0
- package/dist/runtime/browser.js +492 -49
- package/dist/runtime/cache.d.ts +1 -0
- package/dist/runtime/cache.js +169 -15
- package/dist/runtime/choice-wordlist.d.ts +9 -0
- package/dist/runtime/choice-wordlist.js +138 -0
- package/dist/runtime/choice.d.ts +13 -1
- package/dist/runtime/choice.js +490 -102
- package/dist/runtime/chrome149-header-order.d.ts +58 -0
- package/dist/runtime/chrome149-header-order.js +289 -0
- package/dist/runtime/env.js +12 -0
- package/dist/runtime/executor.d.ts +3 -2
- package/dist/runtime/executor.js +26 -25
- package/dist/runtime/http.d.ts +1 -0
- package/dist/runtime/http.js +515 -53
- package/dist/runtime/insights.js +2 -2
- package/dist/runtime/instrumentation.d.ts +2 -2
- package/dist/runtime/instrumentation.js +366 -8
- package/dist/runtime/native-network-errors.d.ts +33 -0
- package/dist/runtime/native-network-errors.js +69 -0
- package/dist/runtime/native-network.d.ts +96 -0
- package/dist/runtime/native-network.js +1232 -0
- package/dist/runtime/ocr.d.ts +29 -0
- package/dist/runtime/ocr.js +440 -0
- package/dist/runtime/otlp.d.ts +71 -2
- package/dist/runtime/otlp.js +397 -16
- package/dist/runtime/proxy-errors.js +6 -2
- package/dist/runtime/proxy-nodemaven.d.ts +56 -0
- package/dist/runtime/proxy-nodemaven.js +146 -0
- package/dist/runtime/proxy-telemetry.d.ts +80 -1
- package/dist/runtime/proxy-telemetry.js +154 -47
- package/dist/runtime/redirects.d.ts +29 -0
- package/dist/runtime/redirects.js +36 -0
- package/dist/runtime/redis.d.ts +1 -1
- package/dist/runtime/redis.js +4 -2
- package/dist/runtime/request-options.d.ts +68 -1
- package/dist/runtime/request-options.js +548 -0
- package/dist/runtime/resolver-config.d.ts +6 -0
- package/dist/runtime/resolver-config.js +6 -0
- package/dist/runtime/resolver-public.d.ts +1 -0
- package/dist/runtime/resolver-public.js +1 -0
- package/dist/runtime/resolver-shared.d.ts +14 -0
- package/dist/runtime/resolver-shared.js +12 -0
- package/dist/runtime/resolver-vendors/bindings.d.ts +48 -0
- package/dist/runtime/resolver-vendors/bindings.js +40 -0
- package/dist/runtime/resolver-vendors/browser.d.ts +22 -0
- package/dist/runtime/resolver-vendors/browser.js +377 -0
- package/dist/runtime/resolver-vendors/capsolver.d.ts +22 -0
- package/dist/runtime/resolver-vendors/capsolver.js +531 -0
- package/dist/runtime/resolver-vendors/hosts.d.ts +2 -0
- package/dist/runtime/resolver-vendors/hosts.js +33 -0
- package/dist/runtime/resolver-vendors/twocaptcha.d.ts +24 -0
- package/dist/runtime/resolver-vendors/twocaptcha.js +408 -0
- package/dist/runtime/resolver-vendors/types.d.ts +94 -0
- package/dist/runtime/resolver-vendors/types.js +96 -0
- package/dist/runtime/resolver.d.ts +72 -0
- package/dist/runtime/resolver.js +771 -0
- package/dist/runtime/secrets.d.ts +27 -0
- package/dist/runtime/secrets.js +51 -0
- package/dist/runtime/state.d.ts +3 -0
- package/dist/runtime/state.js +277 -71
- package/dist/runtime/stealth-cookies.d.ts +20 -0
- package/dist/runtime/stealth-cookies.js +111 -0
- package/dist/runtime/stealth.d.ts +41 -5
- package/dist/runtime/stealth.js +997 -291
- package/dist/runtime/stt.js +1 -12
- package/dist/runtime/timeout.d.ts +5 -0
- package/dist/runtime/timeout.js +12 -0
- package/dist/runtime/trace-config.d.ts +12 -0
- package/dist/runtime/trace-config.js +62 -0
- package/dist/runtime/trace.d.ts +5 -0
- package/dist/runtime/trace.js +43 -10
- package/dist/serve.d.ts +1 -1
- package/dist/serve.js +1 -1
- package/dist/server/error-observability.d.ts +1 -0
- package/dist/server/error-observability.js +1 -0
- package/dist/server/index.d.ts +5 -2
- package/dist/server/index.js +2 -2
- package/dist/server/self-test-input-tokens.d.ts +2 -1
- package/dist/server/self-test-input-tokens.js +18 -14
- package/dist/server/self-test.d.ts +14 -3
- package/dist/server/self-test.js +129 -58
- package/dist/server/serve-implementation.d.ts +230 -0
- package/dist/server/serve-implementation.js +2265 -0
- package/dist/server/serve.d.ts +1 -70
- package/dist/server/serve.js +1 -1131
- package/dist/server/trace-output.d.ts +6 -0
- package/dist/server/trace-output.js +79 -0
- package/dist/server/types.d.ts +30 -5
- package/dist/server/types.js +13 -1
- package/dist/stateful/errors.d.ts +19 -0
- package/dist/stateful/errors.js +24 -0
- package/dist/stateful/http-provider-event-emitter.d.ts +40 -0
- package/dist/stateful/http-provider-event-emitter.js +237 -0
- package/dist/stateful/http-session-owner-registry.d.ts +44 -0
- package/dist/stateful/http-session-owner-registry.js +210 -0
- package/dist/stateful/index.d.ts +18 -0
- package/dist/stateful/index.js +18 -0
- package/dist/stateful/provider-event-delivery-failures.d.ts +32 -0
- package/dist/stateful/provider-event-delivery-failures.js +43 -0
- package/dist/stateful/provider-event-pipeline-metrics.d.ts +46 -0
- package/dist/stateful/provider-event-pipeline-metrics.js +48 -0
- package/dist/stateful/provider-event-pipeline.d.ts +50 -0
- package/dist/stateful/provider-event-pipeline.js +1 -0
- package/dist/stateful/provider-events.d.ts +101 -0
- package/dist/stateful/provider-events.js +289 -0
- package/dist/stateful/session-key.d.ts +15 -0
- package/dist/stateful/session-key.js +86 -0
- package/dist/stateful/stateful-provider-adapter-context.d.ts +5 -0
- package/dist/stateful/stateful-provider-adapter-context.js +42 -0
- package/dist/stateful/stateful-provider-adapter-metrics.d.ts +15 -0
- package/dist/stateful/stateful-provider-adapter-metrics.js +21 -0
- package/dist/stateful/stateful-provider-adapter.d.ts +98 -0
- package/dist/stateful/stateful-provider-adapter.js +287 -0
- package/dist/stateful/stateful-provider-observability.d.ts +62 -0
- package/dist/stateful/stateful-provider-observability.js +161 -0
- package/dist/stateful/stateful-provider-owner-forwarder.d.ts +41 -0
- package/dist/stateful/stateful-provider-owner-forwarder.js +215 -0
- package/dist/stateful/stateful-provider-runtime-context.d.ts +32 -0
- package/dist/stateful/stateful-provider-runtime-context.js +60 -0
- package/dist/stateful/stateful-provider-runtime-executor.d.ts +34 -0
- package/dist/stateful/stateful-provider-runtime-executor.js +52 -0
- package/dist/stateful/stateful-provider-session-routing.d.ts +67 -0
- package/dist/stateful/stateful-provider-session-routing.js +345 -0
- package/dist/stateful/stateful-provider-session-runtime.d.ts +98 -0
- package/dist/stateful/stateful-provider-session-runtime.js +245 -0
- package/dist/stateful-signing.d.ts +18 -0
- package/dist/stateful-signing.js +27 -0
- package/dist/stealth/profiles.d.ts +9 -4
- package/dist/stealth/profiles.js +160 -211
- package/dist/stream-evidence.d.ts +74 -0
- package/dist/stream-evidence.js +785 -0
- package/dist/stream.js +7 -1
- package/dist/testing/index.d.ts +2 -1
- package/dist/testing/index.js +2 -1
- package/dist/testing/run.d.ts +32 -2
- package/dist/testing/run.js +489 -21
- package/dist/trace-sanitization.d.ts +5 -0
- package/dist/trace-sanitization.js +45 -0
- package/dist/types.d.ts +673 -131
- package/dist/types.js +1 -0
- package/package.json +45 -5
- package/src/auth-turn/index.ts +1 -1
- package/src/auth.ts +118 -0
- package/src/ceremonies/index.ts +189 -46
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/approval-override.ts.txt +6 -0
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/codemod-syntax.ts.txt +3 -0
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/connection-precedence.ts.txt +10 -0
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/docs-conflict.ts.txt +8 -0
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/examples-hoisted-array-spread.ts.txt +15 -0
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/examples-imported-array-spread.ts.txt +9 -0
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/examples-map.ts.txt +5 -0
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/examples-mixed-array-spread.ts.txt +24 -0
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/examples-operation.ts.txt +16 -0
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/factory-map.ts.txt +3 -0
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/hoist-all.ts.txt +31 -0
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/hoisted-const.ts.txt +11 -0
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/imported-spread.ts.txt +11 -0
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/inline-map.ts.txt +11 -0
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/inline-spread-cast-tail.ts.txt +21 -0
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/inline-spread-ekitan.ts.txt +11 -0
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/inline-spread-override.ts.txt +14 -0
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/missing-english-locale.ts.txt +7 -0
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/no-safety.ts.txt +6 -0
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/non-literal.ts.txt +7 -0
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/redundant-approval.ts.txt +6 -0
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/safety-conflict.ts.txt +7 -0
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/stream.ts.txt +7 -0
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/tool-router-spread.ts.txt +15 -0
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/unparseable.ts.txt +4 -0
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/verbatim-template.ts.txt +12 -0
- package/src/cli/commands.ts +33 -0
- package/src/cli/create.ts +50 -37
- package/src/cli/migrate-operation-declaration.ts +1751 -0
- package/src/cli/migrate-operation-shape.ts +184 -0
- package/src/cli/migrate-provider-shape.ts +772 -0
- package/src/cli/prompt-assets.ts +865 -0
- package/src/cli/templates/provider/AGENTS.md.tpl +17 -8
- package/src/cli/templates/provider/Dockerfile.tpl +1 -1
- package/src/cli/templates/provider/README.md.tpl +5 -5
- package/src/cli/templates/provider/index.ts.tpl +6 -3
- package/src/cli/templates/provider/operations/ping.ts.tpl +4 -1
- package/src/cli/templates/provider/provider.json.tpl +6 -0
- package/src/config/loader.ts +690 -163
- package/src/contract-serialization.ts +5 -7
- package/src/contract-types.ts +12 -5
- package/src/contract.ts +24 -10
- package/src/declaration-validation.ts +330 -0
- package/src/define.ts +1048 -131
- package/src/dev.ts +4 -1
- package/src/engine.ts +279 -0
- package/src/error-observability.ts +64 -0
- package/src/error-resolution.ts +127 -0
- package/src/errors.ts +68 -0
- package/src/fixture-sanitization.ts +264 -0
- package/src/health-scenario.ts +875 -0
- package/src/index.ts +220 -13
- package/src/lint.ts +698 -90
- package/src/native-address.ts +340 -0
- package/src/native-egress-policy.ts +358 -0
- package/src/observability.ts +51 -1
- package/src/provider.ts +161 -3
- package/src/runtime/auth-flow.ts +12 -0
- package/src/runtime/browser.ts +661 -63
- package/src/runtime/cache.ts +189 -14
- package/src/runtime/choice-wordlist.ts +145 -0
- package/src/runtime/choice.ts +631 -120
- package/src/runtime/chrome149-header-order.ts +330 -0
- package/src/runtime/env.ts +13 -0
- package/src/runtime/executor.ts +43 -31
- package/src/runtime/http.ts +641 -61
- package/src/runtime/insights.ts +2 -2
- package/src/runtime/instrumentation.ts +520 -15
- package/src/runtime/native-network-errors.ts +99 -0
- package/src/runtime/native-network.ts +1605 -0
- package/src/runtime/ocr.ts +523 -0
- package/src/runtime/otlp.ts +467 -21
- package/src/runtime/proxy-errors.ts +12 -4
- package/src/runtime/proxy-nodemaven.ts +221 -0
- package/src/runtime/proxy-telemetry.ts +244 -75
- package/src/runtime/redirects.ts +66 -0
- package/src/runtime/redis.ts +7 -2
- package/src/runtime/request-options.ts +680 -1
- package/src/runtime/resolver-config.ts +6 -0
- package/src/runtime/resolver-public.ts +23 -0
- package/src/runtime/resolver-shared.ts +33 -0
- package/src/runtime/resolver-vendors/bindings.ts +56 -0
- package/src/runtime/resolver-vendors/browser.ts +533 -0
- package/src/runtime/resolver-vendors/capsolver.ts +707 -0
- package/src/runtime/resolver-vendors/hosts.ts +38 -0
- package/src/runtime/resolver-vendors/twocaptcha.ts +540 -0
- package/src/runtime/resolver-vendors/types.ts +212 -0
- package/src/runtime/resolver.ts +1157 -0
- package/src/runtime/secrets.ts +64 -0
- package/src/runtime/state.ts +394 -77
- package/src/runtime/stealth-cookies.ts +132 -0
- package/src/runtime/stealth.ts +1280 -336
- package/src/runtime/stt.ts +1 -19
- package/src/runtime/timeout.ts +18 -0
- package/src/runtime/trace-config.ts +78 -0
- package/src/runtime/trace.ts +57 -17
- package/src/serve.ts +6 -1
- package/src/server/error-observability.ts +1 -0
- package/src/server/index.ts +34 -2
- package/src/server/self-test-input-tokens.ts +29 -14
- package/src/server/self-test.ts +191 -68
- package/src/server/serve-implementation.ts +3378 -0
- package/src/server/serve.ts +1 -1632
- package/src/server/trace-output.ts +129 -0
- package/src/server/types.ts +13 -1
- package/src/stateful/README.md +146 -0
- package/src/stateful/errors.ts +35 -0
- package/src/stateful/http-provider-event-emitter.ts +314 -0
- package/src/stateful/http-session-owner-registry.ts +306 -0
- package/src/stateful/index.ts +18 -0
- package/src/stateful/provider-event-delivery-failures.ts +80 -0
- package/src/stateful/provider-event-pipeline-metrics.ts +95 -0
- package/src/stateful/provider-event-pipeline.ts +61 -0
- package/src/stateful/provider-events.ts +462 -0
- package/src/stateful/session-key.ts +111 -0
- package/src/stateful/stateful-provider-adapter-context.ts +59 -0
- package/src/stateful/stateful-provider-adapter-metrics.ts +48 -0
- package/src/stateful/stateful-provider-adapter.ts +562 -0
- package/src/stateful/stateful-provider-observability.ts +261 -0
- package/src/stateful/stateful-provider-owner-forwarder.ts +287 -0
- package/src/stateful/stateful-provider-runtime-context.ts +92 -0
- package/src/stateful/stateful-provider-runtime-executor.ts +96 -0
- package/src/stateful/stateful-provider-session-routing.ts +546 -0
- package/src/stateful/stateful-provider-session-runtime.ts +403 -0
- package/src/stateful-signing.ts +46 -0
- package/src/stealth/profiles.ts +202 -231
- package/src/stream-evidence.ts +988 -0
- package/src/stream.ts +8 -1
- package/src/testing/index.ts +10 -1
- package/src/testing/run.ts +658 -15
- package/src/trace-sanitization.ts +63 -0
- package/src/types.ts +788 -174
- package/dist/cli/templates/provider/CLAUDE.md.tpl +0 -1
- package/src/cli/templates/provider/CLAUDE.md.tpl +0 -1
- /package/dist/cli/templates/provider/{skills → .agents/skills}/fixtures-and-recording/SKILL.md.tpl +0 -0
- /package/dist/cli/templates/provider/{skills → .agents/skills}/health-checks-and-fail-closed/SKILL.md.tpl +0 -0
- /package/dist/cli/templates/provider/{skills → .agents/skills}/normalization-standards/SKILL.md.tpl +0 -0
- /package/dist/cli/templates/provider/{skills → .agents/skills}/pagination-and-counts/SKILL.md.tpl +0 -0
- /package/dist/cli/templates/provider/{skills → .agents/skills}/upstream-contract-verification/SKILL.md.tpl +0 -0
- /package/dist/cli/templates/provider/{skills → .agents/skills}/upstream-notes/README.md.tpl +0 -0
- /package/src/cli/templates/provider/{skills → .agents/skills}/fixtures-and-recording/SKILL.md.tpl +0 -0
- /package/src/cli/templates/provider/{skills → .agents/skills}/health-checks-and-fail-closed/SKILL.md.tpl +0 -0
- /package/src/cli/templates/provider/{skills → .agents/skills}/normalization-standards/SKILL.md.tpl +0 -0
- /package/src/cli/templates/provider/{skills → .agents/skills}/pagination-and-counts/SKILL.md.tpl +0 -0
- /package/src/cli/templates/provider/{skills → .agents/skills}/upstream-contract-verification/SKILL.md.tpl +0 -0
- /package/src/cli/templates/provider/{skills → .agents/skills}/upstream-notes/README.md.tpl +0 -0
package/src/runtime/state.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
1
2
|
import { providerStateRedisUrlFromEnv } from "../config/loader.js";
|
|
2
3
|
import { ProviderError } from "../errors.js";
|
|
3
4
|
import type {
|
|
@@ -16,11 +17,85 @@ import {
|
|
|
16
17
|
} from "./redis.js";
|
|
17
18
|
|
|
18
19
|
const DEFAULT_REDIS_TIMEOUT_MS = 250;
|
|
19
|
-
const REDIS_STATE_PREFIX = "apifuse:provider-state:
|
|
20
|
+
const REDIS_STATE_PREFIX = "apifuse:provider-state:v2";
|
|
21
|
+
const PROVIDER_SCOPE_DISCRIMINATOR = "scope:provider";
|
|
22
|
+
const MISSING_CONNECTION_SCOPE_DISCRIMINATOR = "scope:connection:missing";
|
|
23
|
+
const SET_WITH_QUOTA_SCRIPT = `
|
|
24
|
+
local now = tonumber(ARGV[1])
|
|
25
|
+
local max_entries = tonumber(ARGV[2])
|
|
26
|
+
local expires_at = tonumber(ARGV[3])
|
|
27
|
+
local index_ttl = tonumber(ARGV[4])
|
|
28
|
+
local envelope = ARGV[5]
|
|
29
|
+
|
|
30
|
+
redis.call("ZREMRANGEBYSCORE", KEYS[2], "-inf", now)
|
|
31
|
+
local indexed = redis.call("ZSCORE", KEYS[2], KEYS[1])
|
|
32
|
+
if not indexed and redis.call("ZCARD", KEYS[2]) >= max_entries then
|
|
33
|
+
return {0, false}
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
redis.call("SET", KEYS[1], envelope, "PXAT", expires_at)
|
|
37
|
+
redis.call("ZADD", KEYS[2], expires_at, KEYS[1])
|
|
38
|
+
redis.call("PEXPIRE", KEYS[2], index_ttl)
|
|
39
|
+
return {1, envelope}
|
|
40
|
+
`;
|
|
41
|
+
|
|
42
|
+
const COMPARE_AND_SET_WITH_QUOTA_SCRIPT = `
|
|
43
|
+
local current = redis.call("GET", KEYS[1])
|
|
44
|
+
if current then
|
|
45
|
+
local ok, decoded = pcall(cjson.decode, current)
|
|
46
|
+
if ok and type(decoded) == "table" and decoded.deleted == true then
|
|
47
|
+
current = false
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
local now = tonumber(ARGV[2])
|
|
52
|
+
local max_entries = tonumber(ARGV[3])
|
|
53
|
+
local expires_at = tonumber(ARGV[4])
|
|
54
|
+
local index_ttl = tonumber(ARGV[5])
|
|
55
|
+
local envelope = ARGV[6]
|
|
56
|
+
|
|
57
|
+
local current_version = 0
|
|
58
|
+
local current_decoded = false
|
|
59
|
+
if current then
|
|
60
|
+
local ok, decoded = pcall(cjson.decode, current)
|
|
61
|
+
if not ok or type(decoded) ~= "table" or type(decoded.version) ~= "number" then
|
|
62
|
+
return {-2, current}
|
|
63
|
+
end
|
|
64
|
+
current_version = decoded.version
|
|
65
|
+
current_decoded = decoded
|
|
66
|
+
end
|
|
67
|
+
if current_version ~= tonumber(ARGV[1]) then
|
|
68
|
+
return {-1, current or false}
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
redis.call("ZREMRANGEBYSCORE", KEYS[2], "-inf", now)
|
|
72
|
+
local indexed = redis.call("ZSCORE", KEYS[2], KEYS[1])
|
|
73
|
+
if not indexed and redis.call("ZCARD", KEYS[2]) >= max_entries then
|
|
74
|
+
return {0, false}
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
if current_decoded and type(current_decoded.createdAt) == "string" then
|
|
78
|
+
local next_decoded = cjson.decode(envelope)
|
|
79
|
+
next_decoded.createdAt = current_decoded.createdAt
|
|
80
|
+
envelope = cjson.encode(next_decoded)
|
|
81
|
+
end
|
|
82
|
+
redis.call("SET", KEYS[1], envelope, "PXAT", expires_at)
|
|
83
|
+
redis.call("ZADD", KEYS[2], expires_at, KEYS[1])
|
|
84
|
+
redis.call("PEXPIRE", KEYS[2], index_ttl)
|
|
85
|
+
return {1, envelope}
|
|
86
|
+
`;
|
|
87
|
+
|
|
88
|
+
const DELETE_WITH_INDEX_SCRIPT = `
|
|
89
|
+
redis.call("SET", KEYS[1], ARGV[1], "PX", ARGV[2])
|
|
90
|
+
redis.call("ZREM", KEYS[2], KEYS[1])
|
|
91
|
+
return 1
|
|
92
|
+
`;
|
|
20
93
|
|
|
21
94
|
type RedisProviderRuntimeStateOptions = {
|
|
22
95
|
readonly redisUrl: string;
|
|
23
96
|
readonly providerId?: string;
|
|
97
|
+
/** Test seam; production callers use redisUrl-backed client sharing. */
|
|
98
|
+
readonly __redisClient?: ProviderRedisClient;
|
|
24
99
|
};
|
|
25
100
|
|
|
26
101
|
type RedisStateEnvelope = {
|
|
@@ -31,23 +106,32 @@ type RedisStateEnvelope = {
|
|
|
31
106
|
readonly updatedAt: string;
|
|
32
107
|
};
|
|
33
108
|
|
|
109
|
+
type RedisStateTombstone = {
|
|
110
|
+
readonly deleted: true;
|
|
111
|
+
};
|
|
112
|
+
|
|
34
113
|
type RedisBackend = {
|
|
35
114
|
readonly redis: ProviderRedisClient;
|
|
36
115
|
};
|
|
37
116
|
|
|
38
117
|
const redisBackends = new Map<string, RedisBackend>();
|
|
39
118
|
|
|
40
|
-
function getRedisBackend(
|
|
119
|
+
function getRedisBackend(
|
|
120
|
+
redisUrl: string,
|
|
121
|
+
injectedRedis?: ProviderRedisClient,
|
|
122
|
+
): RedisBackend {
|
|
41
123
|
const existing = redisBackends.get(redisUrl);
|
|
42
124
|
if (existing) return existing;
|
|
43
|
-
const redis =
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
125
|
+
const redis =
|
|
126
|
+
injectedRedis ??
|
|
127
|
+
createProviderRedisClient({
|
|
128
|
+
redisUrl,
|
|
129
|
+
timeoutMs: DEFAULT_REDIS_TIMEOUT_MS,
|
|
130
|
+
onError: () => {
|
|
131
|
+
// Runtime state operations fail closed at their call sites. Avoid noisy
|
|
132
|
+
// unhandled Redis errors from background reconnect attempts.
|
|
133
|
+
},
|
|
134
|
+
});
|
|
51
135
|
const backend = { redis };
|
|
52
136
|
redisBackends.set(redisUrl, backend);
|
|
53
137
|
return backend;
|
|
@@ -70,21 +154,37 @@ async function requireRedisReady(redis: ProviderRedisClient): Promise<void> {
|
|
|
70
154
|
throw new UnsupportedProviderStateError("Provider runtime state Redis is unavailable");
|
|
71
155
|
}
|
|
72
156
|
|
|
73
|
-
function
|
|
74
|
-
|
|
157
|
+
function connectionScopeDiscriminator(connectionId: string | undefined): string {
|
|
158
|
+
if (connectionId === undefined) return MISSING_CONNECTION_SCOPE_DISCRIMINATOR;
|
|
159
|
+
const digest = createHash("sha256").update(connectionId, "utf8").digest("hex");
|
|
160
|
+
return `scope:connection:sha256:${digest}`;
|
|
75
161
|
}
|
|
76
162
|
|
|
77
|
-
function
|
|
78
|
-
|
|
163
|
+
function providerStatePrefix(
|
|
164
|
+
providerId: string | undefined,
|
|
165
|
+
namespace: string,
|
|
166
|
+
scopeDiscriminator: string,
|
|
167
|
+
): string {
|
|
168
|
+
return `${REDIS_STATE_PREFIX}:${providerId ?? "default"}:${namespace}:${scopeDiscriminator}`;
|
|
79
169
|
}
|
|
80
170
|
|
|
81
|
-
function
|
|
171
|
+
function providerStateKey(
|
|
82
172
|
providerId: string | undefined,
|
|
83
173
|
namespace: string,
|
|
84
|
-
|
|
174
|
+
scopeDiscriminator: string,
|
|
175
|
+
key: string,
|
|
85
176
|
): string {
|
|
86
|
-
|
|
87
|
-
|
|
177
|
+
return `${providerStatePrefix(providerId, namespace, scopeDiscriminator)}:${key}`;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function publicStateKey(redisKey: string, prefixes: readonly string[]): string {
|
|
181
|
+
for (const prefix of prefixes) {
|
|
182
|
+
const prefixWithSeparator = `${prefix}:`;
|
|
183
|
+
if (redisKey.startsWith(prefixWithSeparator)) {
|
|
184
|
+
return redisKey.slice(prefixWithSeparator.length);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
return redisKey;
|
|
88
188
|
}
|
|
89
189
|
|
|
90
190
|
function parseStateDurationMs(ttl: StateWriteOptions["ttl"]): number {
|
|
@@ -105,17 +205,23 @@ function parseStateDurationMs(ttl: StateWriteOptions["ttl"]): number {
|
|
|
105
205
|
return Math.max(1, amount * multiplier);
|
|
106
206
|
}
|
|
107
207
|
|
|
108
|
-
function resolveExpiresAt(ttl: StateWriteOptions["ttl"]): string {
|
|
109
|
-
return new Date(Date.now() + parseStateDurationMs(ttl)).toISOString();
|
|
110
|
-
}
|
|
111
|
-
|
|
112
208
|
function envelopeFromJson(
|
|
113
209
|
key: string,
|
|
114
210
|
raw: string | null,
|
|
115
211
|
// biome-ignore lint/suspicious/noExplicitAny: state envelopes deserialize caller-owned generic values.
|
|
116
212
|
): StateValue<any> | null {
|
|
117
213
|
if (!raw) return null;
|
|
118
|
-
|
|
214
|
+
// A corrupt/undecodable persisted envelope must be treated as absent rather
|
|
215
|
+
// than throwing a raw JSON.parse SyntaxError: an uncaught SyntaxError escapes
|
|
216
|
+
// the provider error taxonomy, is masked as internal_error 500, and is then
|
|
217
|
+
// retried by the hub (2026-07-22 catchtable reserve RCA, candidate A). Returning
|
|
218
|
+
// null also keeps list() from aborting the whole scan on a single bad entry.
|
|
219
|
+
let parsed: unknown;
|
|
220
|
+
try {
|
|
221
|
+
parsed = JSON.parse(raw);
|
|
222
|
+
} catch {
|
|
223
|
+
return null;
|
|
224
|
+
}
|
|
119
225
|
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
120
226
|
return null;
|
|
121
227
|
}
|
|
@@ -139,6 +245,21 @@ function envelopeFromJson(
|
|
|
139
245
|
};
|
|
140
246
|
}
|
|
141
247
|
|
|
248
|
+
function isStateTombstone(raw: string | null): boolean {
|
|
249
|
+
if (!raw) return false;
|
|
250
|
+
try {
|
|
251
|
+
const parsed: unknown = JSON.parse(raw);
|
|
252
|
+
return (
|
|
253
|
+
parsed !== null &&
|
|
254
|
+
typeof parsed === "object" &&
|
|
255
|
+
!Array.isArray(parsed) &&
|
|
256
|
+
(parsed as { deleted?: unknown }).deleted === true
|
|
257
|
+
);
|
|
258
|
+
} catch {
|
|
259
|
+
return false;
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
142
263
|
function redisEnvelope(
|
|
143
264
|
value: unknown,
|
|
144
265
|
version: number,
|
|
@@ -155,19 +276,60 @@ class RedisProviderStateNamespace implements ProviderStateNamespace {
|
|
|
155
276
|
private readonly providerId: string | undefined,
|
|
156
277
|
private readonly namespaceName: string,
|
|
157
278
|
private readonly options: StateNamespaceOptions,
|
|
279
|
+
private readonly scopeDiscriminator: string,
|
|
158
280
|
) {}
|
|
159
281
|
|
|
160
282
|
private redisKey(key: string): string {
|
|
161
|
-
return providerStateKey(
|
|
283
|
+
return providerStateKey(
|
|
284
|
+
this.providerId,
|
|
285
|
+
this.namespaceName,
|
|
286
|
+
this.scopeDiscriminator,
|
|
287
|
+
key,
|
|
288
|
+
);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
private statePrefix(): string {
|
|
292
|
+
return providerStatePrefix(
|
|
293
|
+
this.providerId,
|
|
294
|
+
this.namespaceName,
|
|
295
|
+
this.scopeDiscriminator,
|
|
296
|
+
);
|
|
162
297
|
}
|
|
163
298
|
|
|
164
|
-
private
|
|
165
|
-
|
|
299
|
+
private indexKey(): string {
|
|
300
|
+
// Keep bookkeeping outside the caller-owned keyspace. A provider may use
|
|
301
|
+
// any state key (including "__index"), so a suffix inside the namespace
|
|
302
|
+
// could turn the ZSET into a string and break every subsequent write.
|
|
303
|
+
const namespaceIdentity = Buffer.from(
|
|
304
|
+
providerStatePrefix(
|
|
305
|
+
this.providerId,
|
|
306
|
+
this.namespaceName,
|
|
307
|
+
this.scopeDiscriminator,
|
|
308
|
+
),
|
|
309
|
+
"utf8",
|
|
310
|
+
).toString("base64url");
|
|
311
|
+
return `${REDIS_STATE_PREFIX}:index:${namespaceIdentity}`;
|
|
166
312
|
}
|
|
167
313
|
|
|
168
|
-
private async
|
|
314
|
+
private async indexedKeys(limit: number): Promise<string[]> {
|
|
169
315
|
await requireRedisReady(this.backend.redis);
|
|
170
|
-
|
|
316
|
+
const now = Date.now();
|
|
317
|
+
return await withRequiredRedis(async () => {
|
|
318
|
+
await this.backend.redis.zremrangebyscore(this.indexKey(), "-inf", now);
|
|
319
|
+
return await this.backend.redis.zrangebyscore(
|
|
320
|
+
this.indexKey(),
|
|
321
|
+
now + 1,
|
|
322
|
+
"+inf",
|
|
323
|
+
"LIMIT",
|
|
324
|
+
0,
|
|
325
|
+
limit,
|
|
326
|
+
);
|
|
327
|
+
});
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
private async readRaw(key: string): Promise<string | null> {
|
|
331
|
+
await requireRedisReady(this.backend.redis);
|
|
332
|
+
return await withRequiredRedis(() => this.backend.redis.get(this.redisKey(key)));
|
|
171
333
|
}
|
|
172
334
|
|
|
173
335
|
private enforceValueSize(value: unknown): void {
|
|
@@ -179,57 +341,86 @@ class RedisProviderStateNamespace implements ProviderStateNamespace {
|
|
|
179
341
|
}
|
|
180
342
|
}
|
|
181
343
|
|
|
182
|
-
private
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
344
|
+
private quotaExceeded(): UnsupportedProviderStateError {
|
|
345
|
+
return new UnsupportedProviderStateError(
|
|
346
|
+
`Provider runtime state namespace quota exceeded (${this.options.maxEntries + 1} > ${this.options.maxEntries})`,
|
|
347
|
+
);
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
private writeTiming(ttl: StateWriteOptions["ttl"]): {
|
|
351
|
+
expiresAt: string;
|
|
352
|
+
expiresAtMs: number;
|
|
353
|
+
indexTtlMs: number;
|
|
354
|
+
} {
|
|
355
|
+
const ttlMs = parseStateDurationMs(ttl ?? this.options.defaultTtl);
|
|
356
|
+
const maxTtlMs = parseStateDurationMs(this.options.maxTtl);
|
|
357
|
+
if (ttlMs > maxTtlMs) {
|
|
187
358
|
throw new UnsupportedProviderStateError(
|
|
188
|
-
`Provider runtime state
|
|
359
|
+
`Provider runtime state ttl exceeds maxTtl (${ttlMs} > ${maxTtlMs})`,
|
|
189
360
|
);
|
|
190
361
|
}
|
|
362
|
+
const expiresAtMs = Date.now() + ttlMs;
|
|
363
|
+
return {
|
|
364
|
+
expiresAt: new Date(expiresAtMs).toISOString(),
|
|
365
|
+
expiresAtMs,
|
|
366
|
+
indexTtlMs: maxTtlMs,
|
|
367
|
+
};
|
|
191
368
|
}
|
|
192
369
|
|
|
193
370
|
async list<T>(options?: { limit?: number; prefix?: string }): Promise<StateValue<T>[]> {
|
|
194
|
-
const
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
const
|
|
199
|
-
|
|
200
|
-
const
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
371
|
+
const requestedLimit = Math.max(0, options?.limit ?? this.options.maxEntries);
|
|
372
|
+
if (requestedLimit === 0) return [];
|
|
373
|
+
const keys = await this.indexedKeys(this.options.maxEntries);
|
|
374
|
+
if (keys.length === 0) return [];
|
|
375
|
+
const values = await withRequiredRedis(() => this.backend.redis.mget(keys));
|
|
376
|
+
const rows: StateValue<T>[] = [];
|
|
377
|
+
for (const [index, raw] of values.entries()) {
|
|
378
|
+
const redisKey = keys[index];
|
|
379
|
+
if (!redisKey) continue;
|
|
380
|
+
const publicKey = publicStateKey(redisKey, [this.statePrefix()]);
|
|
381
|
+
if (options?.prefix && !publicKey.startsWith(options.prefix)) continue;
|
|
382
|
+
const value = envelopeFromJson(publicKey, raw);
|
|
383
|
+
if (!value) continue;
|
|
384
|
+
rows.push(value);
|
|
385
|
+
}
|
|
386
|
+
return rows.slice(0, requestedLimit);
|
|
207
387
|
}
|
|
208
388
|
|
|
209
389
|
async get<T>(key: string): Promise<StateValue<T> | null> {
|
|
210
|
-
await
|
|
211
|
-
|
|
390
|
+
const raw = await this.readRaw(key);
|
|
391
|
+
if (isStateTombstone(raw)) return null;
|
|
212
392
|
return envelopeFromJson(key, raw);
|
|
213
393
|
}
|
|
214
394
|
|
|
215
395
|
async set<T>(key: string, value: T, options?: StateWriteOptions): Promise<StateValue<T>> {
|
|
216
396
|
this.enforceValueSize(value);
|
|
217
|
-
await this.enforceMaxEntries(key);
|
|
218
397
|
const current = await this.get<T>(key);
|
|
219
398
|
const createdAt = current?.createdAt ?? new Date().toISOString();
|
|
220
399
|
const version = (current?.version ?? 0) + 1;
|
|
221
|
-
const
|
|
222
|
-
const
|
|
223
|
-
|
|
224
|
-
const
|
|
225
|
-
|
|
226
|
-
|
|
400
|
+
const timing = this.writeTiming(options?.ttl);
|
|
401
|
+
const envelope = redisEnvelope(value, version, createdAt, timing.expiresAt);
|
|
402
|
+
await requireRedisReady(this.backend.redis);
|
|
403
|
+
const result = await withRequiredRedis(() =>
|
|
404
|
+
this.backend.redis.eval(
|
|
405
|
+
SET_WITH_QUOTA_SCRIPT,
|
|
406
|
+
2,
|
|
407
|
+
this.redisKey(key),
|
|
408
|
+
this.indexKey(),
|
|
409
|
+
String(Date.now()),
|
|
410
|
+
String(this.options.maxEntries),
|
|
411
|
+
String(timing.expiresAtMs),
|
|
412
|
+
String(timing.indexTtlMs),
|
|
413
|
+
JSON.stringify(envelope),
|
|
414
|
+
),
|
|
227
415
|
);
|
|
416
|
+
if (!Array.isArray(result) || Number(result[0]) !== 1) {
|
|
417
|
+
throw this.quotaExceeded();
|
|
418
|
+
}
|
|
228
419
|
return {
|
|
229
420
|
key,
|
|
230
421
|
value,
|
|
231
422
|
version,
|
|
232
|
-
expiresAt,
|
|
423
|
+
expiresAt: timing.expiresAt,
|
|
233
424
|
createdAt,
|
|
234
425
|
updatedAt: envelope.updatedAt,
|
|
235
426
|
};
|
|
@@ -253,16 +444,59 @@ class RedisProviderStateNamespace implements ProviderStateNamespace {
|
|
|
253
444
|
options?: StateWriteOptions,
|
|
254
445
|
): Promise<StateCasResult<T>> {
|
|
255
446
|
this.enforceValueSize(value);
|
|
256
|
-
const
|
|
257
|
-
|
|
258
|
-
|
|
447
|
+
const createdAt = new Date().toISOString();
|
|
448
|
+
const timing = this.writeTiming(options?.ttl);
|
|
449
|
+
const envelope = redisEnvelope(
|
|
450
|
+
value,
|
|
451
|
+
expectedVersion + 1,
|
|
452
|
+
createdAt,
|
|
453
|
+
timing.expiresAt,
|
|
454
|
+
);
|
|
455
|
+
await requireRedisReady(this.backend.redis);
|
|
456
|
+
const result = await withRequiredRedis(() =>
|
|
457
|
+
this.backend.redis.eval(
|
|
458
|
+
COMPARE_AND_SET_WITH_QUOTA_SCRIPT,
|
|
459
|
+
2,
|
|
460
|
+
this.redisKey(key),
|
|
461
|
+
this.indexKey(),
|
|
462
|
+
String(expectedVersion),
|
|
463
|
+
String(Date.now()),
|
|
464
|
+
String(this.options.maxEntries),
|
|
465
|
+
String(timing.expiresAtMs),
|
|
466
|
+
String(timing.indexTtlMs),
|
|
467
|
+
JSON.stringify(envelope),
|
|
468
|
+
),
|
|
469
|
+
);
|
|
470
|
+
if (Array.isArray(result) && Number(result[0]) === 0) {
|
|
471
|
+
throw this.quotaExceeded();
|
|
472
|
+
}
|
|
473
|
+
if (!Array.isArray(result) || Number(result[0]) !== 1) {
|
|
474
|
+
const rawCurrent = Array.isArray(result) && typeof result[1] === "string" ? result[1] : null;
|
|
475
|
+
return { ok: false, current: envelopeFromJson(key, rawCurrent) };
|
|
476
|
+
}
|
|
477
|
+
const rawWritten = typeof result[1] === "string" ? result[1] : null;
|
|
478
|
+
const written = envelopeFromJson(key, rawWritten) as StateValue<T> | null;
|
|
479
|
+
if (!written) {
|
|
480
|
+
throw new UnsupportedProviderStateError(
|
|
481
|
+
"Provider runtime state CAS returned an invalid value",
|
|
482
|
+
);
|
|
259
483
|
}
|
|
260
|
-
return { ok: true, value:
|
|
484
|
+
return { ok: true, value: written };
|
|
261
485
|
}
|
|
262
486
|
|
|
263
487
|
async delete(key: string): Promise<void> {
|
|
264
488
|
await requireRedisReady(this.backend.redis);
|
|
265
|
-
|
|
489
|
+
const tombstone = JSON.stringify({ deleted: true } satisfies RedisStateTombstone);
|
|
490
|
+
await withRequiredRedis(() =>
|
|
491
|
+
this.backend.redis.eval(
|
|
492
|
+
DELETE_WITH_INDEX_SCRIPT,
|
|
493
|
+
2,
|
|
494
|
+
this.redisKey(key),
|
|
495
|
+
this.indexKey(),
|
|
496
|
+
tombstone,
|
|
497
|
+
String(parseStateDurationMs(this.options.maxTtl)),
|
|
498
|
+
),
|
|
499
|
+
);
|
|
266
500
|
}
|
|
267
501
|
|
|
268
502
|
async increment(
|
|
@@ -280,14 +514,34 @@ class RedisProviderStateNamespace implements ProviderStateNamespace {
|
|
|
280
514
|
class RedisProviderRuntimeState implements ProviderRuntimeState {
|
|
281
515
|
readonly backend: RedisBackend;
|
|
282
516
|
readonly providerId?: string;
|
|
517
|
+
readonly redisUrl: string;
|
|
518
|
+
readonly scopeDiscriminator: string;
|
|
283
519
|
|
|
284
|
-
constructor(
|
|
285
|
-
|
|
520
|
+
constructor(
|
|
521
|
+
options: RedisProviderRuntimeStateOptions,
|
|
522
|
+
scopeDiscriminator = MISSING_CONNECTION_SCOPE_DISCRIMINATOR,
|
|
523
|
+
) {
|
|
524
|
+
this.backend = getRedisBackend(options.redisUrl, options.__redisClient);
|
|
286
525
|
this.providerId = options.providerId;
|
|
526
|
+
this.redisUrl = options.redisUrl;
|
|
527
|
+
this.scopeDiscriminator = scopeDiscriminator;
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
forConnection(connectionId: string | undefined): ProviderRuntimeState {
|
|
531
|
+
return new RedisProviderRuntimeState(
|
|
532
|
+
{ redisUrl: this.redisUrl, providerId: this.providerId },
|
|
533
|
+
connectionScopeDiscriminator(connectionId),
|
|
534
|
+
);
|
|
287
535
|
}
|
|
288
536
|
|
|
289
537
|
namespace(name: string, options: StateNamespaceOptions): ProviderStateNamespace {
|
|
290
|
-
return new RedisProviderStateNamespace(
|
|
538
|
+
return new RedisProviderStateNamespace(
|
|
539
|
+
this.backend,
|
|
540
|
+
this.providerId,
|
|
541
|
+
name,
|
|
542
|
+
options,
|
|
543
|
+
options.scope === "provider" ? PROVIDER_SCOPE_DISCRIMINATOR : this.scopeDiscriminator,
|
|
544
|
+
);
|
|
291
545
|
}
|
|
292
546
|
}
|
|
293
547
|
|
|
@@ -337,6 +591,10 @@ class UnsupportedProviderStateNamespace implements ProviderStateNamespace {
|
|
|
337
591
|
}
|
|
338
592
|
|
|
339
593
|
class UnsupportedProviderRuntimeState implements ProviderRuntimeState {
|
|
594
|
+
forConnection(_connectionId: string | undefined): ProviderRuntimeState {
|
|
595
|
+
return new UnsupportedProviderRuntimeState();
|
|
596
|
+
}
|
|
597
|
+
|
|
340
598
|
namespace(_name: string, _options: StateNamespaceOptions): ProviderStateNamespace {
|
|
341
599
|
return new UnsupportedProviderStateNamespace();
|
|
342
600
|
}
|
|
@@ -348,6 +606,31 @@ class MemoryProviderStateNamespace implements ProviderStateNamespace {
|
|
|
348
606
|
|
|
349
607
|
constructor(private readonly options: StateNamespaceOptions) {}
|
|
350
608
|
|
|
609
|
+
private enforceValueSize(value: unknown): void {
|
|
610
|
+
const bytes = Buffer.byteLength(JSON.stringify(value), "utf8");
|
|
611
|
+
if (bytes > this.options.maxValueBytes) {
|
|
612
|
+
throw new UnsupportedProviderStateError(
|
|
613
|
+
`Provider runtime state value exceeds maxValueBytes (${bytes} > ${this.options.maxValueBytes})`,
|
|
614
|
+
);
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
private enforceWritePolicy(key: string, value: unknown, ttl: StateWriteOptions["ttl"]): void {
|
|
619
|
+
this.enforceValueSize(value);
|
|
620
|
+
const ttlMs = parseStateDurationMs(ttl ?? this.options.defaultTtl);
|
|
621
|
+
const maxTtlMs = parseStateDurationMs(this.options.maxTtl);
|
|
622
|
+
if (ttlMs > maxTtlMs) {
|
|
623
|
+
throw new UnsupportedProviderStateError(
|
|
624
|
+
`Provider runtime state ttl exceeds maxTtl (${ttlMs} > ${maxTtlMs})`,
|
|
625
|
+
);
|
|
626
|
+
}
|
|
627
|
+
if (!this.values.has(key) && this.values.size >= this.options.maxEntries) {
|
|
628
|
+
throw new UnsupportedProviderStateError(
|
|
629
|
+
`Provider runtime state namespace quota exceeded (${this.options.maxEntries + 1} > ${this.options.maxEntries})`,
|
|
630
|
+
);
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
|
|
351
634
|
private pruneExpired(nowMs = Date.now()): void {
|
|
352
635
|
for (const [key, row] of this.values.entries()) {
|
|
353
636
|
if (row.expiresAt && Date.parse(row.expiresAt) <= nowMs) {
|
|
@@ -371,6 +654,7 @@ class MemoryProviderStateNamespace implements ProviderStateNamespace {
|
|
|
371
654
|
|
|
372
655
|
async set<T>(key: string, value: T, options?: StateWriteOptions): Promise<StateValue<T>> {
|
|
373
656
|
this.pruneExpired();
|
|
657
|
+
this.enforceWritePolicy(key, value, options?.ttl);
|
|
374
658
|
const now = new Date().toISOString();
|
|
375
659
|
const current = this.values.get(key);
|
|
376
660
|
const expiresAt = resolveMemoryStateExpiresAt(options?.ttl ?? this.options.defaultTtl);
|
|
@@ -397,14 +681,30 @@ class MemoryProviderStateNamespace implements ProviderStateNamespace {
|
|
|
397
681
|
}
|
|
398
682
|
|
|
399
683
|
async compareAndSet<T>(
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
684
|
+
key: string,
|
|
685
|
+
expectedVersion: number,
|
|
686
|
+
value: T,
|
|
687
|
+
options?: StateWriteOptions,
|
|
404
688
|
): Promise<StateCasResult<T>> {
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
)
|
|
689
|
+
this.pruneExpired();
|
|
690
|
+
const current = this.values.get(key) as StateValue<T> | undefined;
|
|
691
|
+
if ((current?.version ?? 0) !== expectedVersion) {
|
|
692
|
+
return { ok: false, current: current ?? null };
|
|
693
|
+
}
|
|
694
|
+
this.enforceWritePolicy(key, value, options?.ttl);
|
|
695
|
+
const now = new Date().toISOString();
|
|
696
|
+
const stored = {
|
|
697
|
+
key,
|
|
698
|
+
value,
|
|
699
|
+
version: expectedVersion + 1,
|
|
700
|
+
expiresAt: resolveMemoryStateExpiresAt(
|
|
701
|
+
options?.ttl ?? this.options.defaultTtl,
|
|
702
|
+
),
|
|
703
|
+
createdAt: current?.createdAt ?? now,
|
|
704
|
+
updatedAt: now,
|
|
705
|
+
} satisfies StateValue<T>;
|
|
706
|
+
this.values.set(key, stored);
|
|
707
|
+
return { ok: true, value: stored };
|
|
408
708
|
}
|
|
409
709
|
|
|
410
710
|
async delete(key: string): Promise<void> {
|
|
@@ -423,14 +723,31 @@ class MemoryProviderStateNamespace implements ProviderStateNamespace {
|
|
|
423
723
|
}
|
|
424
724
|
}
|
|
425
725
|
|
|
726
|
+
type MemoryProviderStateBackend = {
|
|
727
|
+
readonly namespaces: Map<string, MemoryProviderStateNamespace>;
|
|
728
|
+
};
|
|
729
|
+
|
|
426
730
|
class MemoryProviderRuntimeState implements ProviderRuntimeState {
|
|
427
|
-
|
|
731
|
+
constructor(
|
|
732
|
+
private readonly backend: MemoryProviderStateBackend = { namespaces: new Map() },
|
|
733
|
+
private readonly scopeDiscriminator = MISSING_CONNECTION_SCOPE_DISCRIMINATOR,
|
|
734
|
+
) {}
|
|
428
735
|
|
|
429
|
-
|
|
430
|
-
|
|
736
|
+
forConnection(connectionId: string | undefined): ProviderRuntimeState {
|
|
737
|
+
return new MemoryProviderRuntimeState(
|
|
738
|
+
this.backend,
|
|
739
|
+
connectionScopeDiscriminator(connectionId),
|
|
740
|
+
);
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
namespace(name: string, options: StateNamespaceOptions): ProviderStateNamespace {
|
|
744
|
+
const scopeDiscriminator =
|
|
745
|
+
options.scope === "provider" ? PROVIDER_SCOPE_DISCRIMINATOR : this.scopeDiscriminator;
|
|
746
|
+
const namespaceIdentity = `${scopeDiscriminator}\0${name}`;
|
|
747
|
+
const existing = this.backend.namespaces.get(namespaceIdentity);
|
|
431
748
|
if (existing) return existing;
|
|
432
|
-
const created = new MemoryProviderStateNamespace(
|
|
433
|
-
this.namespaces.set(
|
|
749
|
+
const created = new MemoryProviderStateNamespace(options);
|
|
750
|
+
this.backend.namespaces.set(namespaceIdentity, created);
|
|
434
751
|
return created;
|
|
435
752
|
}
|
|
436
753
|
}
|