@apifuse/provider-sdk 2.2.0-beta.3 → 2.2.0-beta.30
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 +493 -5
- package/CHANGELOG.md +127 -1
- package/README.md +52 -6
- package/SUBMISSION.md +1 -1
- package/bin/apifuse-check.ts +106 -62
- package/bin/apifuse-create.ts +1 -1
- package/bin/apifuse-dev.ts +63 -55
- package/bin/apifuse-pack-check.ts +22 -2
- package/bin/apifuse-pack-smoke.ts +78 -82
- package/bin/apifuse-pack-types.ts +583 -0
- package/bin/apifuse-perf.ts +59 -140
- package/bin/apifuse-record.ts +698 -113
- package/bin/apifuse-submit-check.ts +517 -44
- package/bin/apifuse-sync-assets.ts +117 -0
- package/bin/apifuse.ts +1 -1
- package/bin/submit-check-delimited-text.ts +50 -0
- package/bin/submit-check-xml.ts +1 -1
- package/dist/auth-turn/index.d.ts +4 -4
- package/dist/auth-turn/index.js +1 -1
- package/dist/auth.d.ts +16 -2
- package/dist/auth.js +76 -18
- package/dist/ceremonies/index.d.ts +9 -1
- package/dist/ceremonies/index.js +117 -29
- package/dist/cli/commands.d.ts +1 -1
- package/dist/cli/commands.js +8 -0
- package/dist/cli/create.d.ts +3 -0
- package/dist/cli/create.js +34 -35
- 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/README.md.tpl +4 -4
- package/dist/config/loader.d.ts +176 -17
- package/dist/config/loader.js +434 -161
- package/dist/contract-serialization.d.ts +2 -2
- package/dist/contract-serialization.js +7 -14
- package/dist/contract-types.d.ts +3 -2
- package/dist/contract.d.ts +3 -3
- package/dist/contract.js +6 -6
- package/dist/declaration-validation.d.ts +23 -0
- package/dist/declaration-validation.js +159 -0
- package/dist/define.d.ts +13 -1
- package/dist/define.js +391 -122
- package/dist/dev.d.ts +1 -1
- package/dist/dev.js +1 -1
- package/dist/error-resolution.d.ts +4 -0
- package/dist/error-resolution.js +122 -0
- package/dist/errors.d.ts +19 -1
- package/dist/errors.js +41 -3
- package/dist/fixture-sanitization.d.ts +26 -0
- package/dist/fixture-sanitization.js +216 -0
- package/dist/i18n/catalog.d.ts +2 -2
- package/dist/i18n/catalog.js +4 -10
- package/dist/i18n/index.d.ts +2 -2
- package/dist/i18n/index.js +2 -2
- package/dist/i18n/keys.d.ts +2 -2
- package/dist/index.d.ts +50 -42
- package/dist/index.js +41 -37
- package/dist/lint.d.ts +6 -1
- package/dist/lint.js +370 -18
- 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 +13 -11
- package/dist/provider.js +10 -9
- package/dist/public-schema-field-lint.d.ts +1 -1
- package/dist/recipes/gov-api.js +1 -1
- package/dist/runtime/auth-flow.d.ts +3 -1
- package/dist/runtime/auth-flow.js +8 -3
- package/dist/runtime/browser.d.ts +1 -1
- package/dist/runtime/browser.js +138 -40
- package/dist/runtime/cache.d.ts +2 -1
- package/dist/runtime/cache.js +173 -23
- 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 +514 -124
- package/dist/runtime/credential.d.ts +1 -1
- package/dist/runtime/credential.js +1 -1
- package/dist/runtime/env.d.ts +1 -1
- package/dist/runtime/executor.d.ts +1 -1
- package/dist/runtime/executor.js +25 -3
- package/dist/runtime/http.d.ts +3 -2
- package/dist/runtime/http.js +517 -55
- package/dist/runtime/insights.d.ts +1 -1
- package/dist/runtime/insights.js +6 -13
- package/dist/runtime/instrumentation.d.ts +2 -2
- package/dist/runtime/instrumentation.js +371 -23
- package/dist/runtime/keyring.js +1 -1
- package/dist/runtime/namespace.js +1 -1
- 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 +1 -1
- package/dist/runtime/perf.d.ts +1 -1
- package/dist/runtime/provider.d.ts +1 -1
- package/dist/runtime/provider.js +1 -2
- package/dist/runtime/proxy-errors.d.ts +1 -1
- package/dist/runtime/proxy-errors.js +9 -7
- package/dist/runtime/proxy-nodemaven.d.ts +56 -0
- package/dist/runtime/proxy-nodemaven.js +146 -0
- package/dist/runtime/proxy-retry-policy.d.ts +2 -2
- package/dist/runtime/proxy-retry-policy.js +2 -2
- package/dist/runtime/proxy-telemetry.d.ts +2 -1
- package/dist/runtime/proxy-telemetry.js +58 -52
- 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 +5 -5
- 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 +3 -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 +20 -0
- package/dist/runtime/resolver-vendors/browser.js +282 -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 +368 -0
- package/dist/runtime/resolver-vendors/types.d.ts +83 -0
- package/dist/runtime/resolver-vendors/types.js +69 -0
- package/dist/runtime/resolver.d.ts +59 -0
- package/dist/runtime/resolver.js +705 -0
- package/dist/runtime/secrets.d.ts +27 -0
- package/dist/runtime/secrets.js +51 -0
- package/dist/runtime/state.d.ts +5 -2
- package/dist/runtime/state.js +280 -74
- package/dist/runtime/stealth-cookies.d.ts +20 -0
- package/dist/runtime/stealth-cookies.js +111 -0
- package/dist/runtime/stealth.d.ts +30 -5
- package/dist/runtime/stealth.js +523 -259
- package/dist/runtime/stt.d.ts +1 -1
- package/dist/runtime/stt.js +12 -27
- package/dist/runtime/timeout.d.ts +5 -0
- package/dist/runtime/timeout.js +12 -0
- package/dist/runtime/trace.d.ts +2 -2
- package/dist/runtime/trace.js +2 -4
- package/dist/runtime/waterfall.d.ts +1 -1
- package/dist/schema.d.ts +1 -1
- package/dist/schema.js +7 -15
- package/dist/serve.d.ts +1 -1
- package/dist/serve.js +1 -1
- package/dist/server/index.d.ts +7 -7
- package/dist/server/index.js +6 -6
- 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-redaction.d.ts +1 -1
- package/dist/server/self-test-redaction.js +1 -1
- package/dist/server/self-test.d.ts +117 -3
- package/dist/server/self-test.js +787 -151
- package/dist/server/serve-implementation.d.ts +210 -0
- package/dist/server/serve-implementation.js +2077 -0
- package/dist/server/serve.d.ts +1 -70
- package/dist/server/serve.js +1 -1143
- package/dist/server/types.d.ts +34 -9
- package/dist/server/types.js +8 -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 +1 -1
- package/dist/stealth/profiles.js +21 -21
- package/dist/stream-evidence.d.ts +74 -0
- package/dist/stream-evidence.js +785 -0
- package/dist/stream.d.ts +1 -1
- package/dist/stream.js +7 -1
- package/dist/testing/index.d.ts +3 -2
- package/dist/testing/index.js +3 -2
- package/dist/testing/run.d.ts +32 -2
- package/dist/testing/run.js +488 -28
- package/dist/types.d.ts +566 -19
- package/dist/types.js +1 -0
- package/dist/user-input.d.ts +30 -0
- package/dist/user-input.js +66 -0
- package/package.json +42 -7
- package/src/auth-turn/index.ts +2 -2
- package/src/auth.ts +146 -86
- package/src/ceremonies/index.ts +167 -92
- package/src/cli/commands.ts +10 -0
- package/src/cli/create.ts +42 -35
- package/src/cli/prompt-assets.ts +865 -0
- package/src/cli/templates/provider/AGENTS.md.tpl +17 -8
- package/src/cli/templates/provider/README.md.tpl +4 -4
- package/src/config/loader.ts +667 -289
- package/src/contract-serialization.ts +10 -18
- package/src/contract-types.ts +3 -2
- package/src/contract.ts +14 -28
- package/src/declaration-validation.ts +202 -0
- package/src/define.ts +631 -495
- package/src/dev.ts +4 -9
- package/src/error-resolution.ts +127 -0
- package/src/errors.ts +56 -11
- package/src/fixture-sanitization.ts +247 -0
- package/src/i18n/catalog.ts +10 -32
- package/src/i18n/index.ts +2 -2
- package/src/i18n/keys.ts +5 -11
- package/src/index.ts +157 -44
- package/src/lint.ts +488 -154
- 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 +65 -11
- package/src/public-schema-field-lint.ts +7 -33
- package/src/recipes/gov-api.ts +2 -5
- package/src/runtime/auth-flow.ts +13 -7
- package/src/runtime/browser.ts +252 -207
- package/src/runtime/cache.ts +209 -81
- package/src/runtime/choice-wordlist.ts +145 -0
- package/src/runtime/choice.ts +685 -216
- package/src/runtime/credential.ts +2 -2
- package/src/runtime/env.ts +1 -1
- package/src/runtime/executor.ts +37 -19
- package/src/runtime/http.ts +645 -65
- package/src/runtime/insights.ts +15 -53
- package/src/runtime/instrumentation.ts +530 -67
- package/src/runtime/keyring.ts +7 -19
- package/src/runtime/namespace.ts +2 -7
- 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 +12 -23
- package/src/runtime/perf.ts +1 -1
- package/src/runtime/provider.ts +4 -9
- package/src/runtime/proxy-errors.ts +29 -42
- package/src/runtime/proxy-nodemaven.ts +221 -0
- package/src/runtime/proxy-retry-policy.ts +3 -3
- package/src/runtime/proxy-telemetry.ts +84 -77
- package/src/runtime/redirects.ts +66 -0
- package/src/runtime/redis.ts +10 -13
- package/src/runtime/request-options.ts +679 -9
- package/src/runtime/resolver-config.ts +6 -0
- package/src/runtime/resolver-public.ts +18 -0
- package/src/runtime/resolver-shared.ts +17 -0
- package/src/runtime/resolver-vendors/bindings.ts +56 -0
- package/src/runtime/resolver-vendors/browser.ts +408 -0
- package/src/runtime/resolver-vendors/hosts.ts +38 -0
- package/src/runtime/resolver-vendors/twocaptcha.ts +500 -0
- package/src/runtime/resolver-vendors/types.ts +173 -0
- package/src/runtime/resolver.ts +1060 -0
- package/src/runtime/secrets.ts +64 -0
- package/src/runtime/state.ts +399 -161
- package/src/runtime/stealth-cookies.ts +132 -0
- package/src/runtime/stealth.ts +681 -295
- package/src/runtime/stt.ts +39 -113
- package/src/runtime/timeout.ts +18 -0
- package/src/runtime/trace.ts +14 -44
- package/src/runtime/waterfall.ts +5 -18
- package/src/schema.ts +23 -84
- package/src/serve.ts +6 -1
- package/src/server/index.ts +30 -7
- package/src/server/self-test-input-tokens.ts +29 -14
- package/src/server/self-test-redaction.ts +2 -2
- package/src/server/self-test.ts +1030 -180
- package/src/server/serve-implementation.ts +3060 -0
- package/src/server/serve.ts +1 -1781
- package/src/server/types.ts +12 -13
- 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 +27 -33
- package/src/stream-evidence.ts +988 -0
- package/src/stream.ts +16 -20
- package/src/testing/index.ts +11 -2
- package/src/testing/run.ts +668 -74
- package/src/types.ts +665 -35
- package/src/user-input.ts +118 -0
- 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,5 +1,6 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { providerStateRedisUrlFromEnv } from "../config/loader.js";
|
|
3
|
+
import { ProviderError } from "../errors.js";
|
|
3
4
|
import type {
|
|
4
5
|
ProviderRuntimeState,
|
|
5
6
|
ProviderStateNamespace,
|
|
@@ -7,20 +8,94 @@ import type {
|
|
|
7
8
|
StateNamespaceOptions,
|
|
8
9
|
StateValue,
|
|
9
10
|
StateWriteOptions,
|
|
10
|
-
} from "../types";
|
|
11
|
+
} from "../types.js";
|
|
11
12
|
import {
|
|
12
13
|
createProviderRedisClient,
|
|
13
14
|
ensureRedisReady,
|
|
14
15
|
type ProviderRedisClient,
|
|
15
16
|
withRedisTimeout,
|
|
16
|
-
} from "./redis";
|
|
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;
|
|
@@ -57,47 +141,50 @@ async function withRequiredRedis<T>(operation: () => Promise<T>): Promise<T> {
|
|
|
57
141
|
return await withRedisTimeout(operation, {
|
|
58
142
|
timeoutMs: DEFAULT_REDIS_TIMEOUT_MS,
|
|
59
143
|
onTimeout: () => {
|
|
60
|
-
throw new UnsupportedProviderStateError(
|
|
61
|
-
"Provider runtime state Redis timed out",
|
|
62
|
-
);
|
|
144
|
+
throw new UnsupportedProviderStateError("Provider runtime state Redis timed out");
|
|
63
145
|
},
|
|
64
146
|
onError: () => {
|
|
65
|
-
throw new UnsupportedProviderStateError(
|
|
66
|
-
"Provider runtime state Redis is unavailable",
|
|
67
|
-
);
|
|
147
|
+
throw new UnsupportedProviderStateError("Provider runtime state Redis is unavailable");
|
|
68
148
|
},
|
|
69
149
|
});
|
|
70
150
|
}
|
|
71
151
|
|
|
72
152
|
async function requireRedisReady(redis: ProviderRedisClient): Promise<void> {
|
|
73
153
|
if (await ensureRedisReady(redis, DEFAULT_REDIS_TIMEOUT_MS)) return;
|
|
74
|
-
throw new UnsupportedProviderStateError(
|
|
75
|
-
|
|
76
|
-
|
|
154
|
+
throw new UnsupportedProviderStateError("Provider runtime state Redis is unavailable");
|
|
155
|
+
}
|
|
156
|
+
|
|
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}`;
|
|
77
161
|
}
|
|
78
162
|
|
|
79
163
|
function providerStatePrefix(
|
|
80
164
|
providerId: string | undefined,
|
|
81
165
|
namespace: string,
|
|
166
|
+
scopeDiscriminator: string,
|
|
82
167
|
): string {
|
|
83
|
-
return `${REDIS_STATE_PREFIX}:${providerId ?? "default"}:${namespace}`;
|
|
168
|
+
return `${REDIS_STATE_PREFIX}:${providerId ?? "default"}:${namespace}:${scopeDiscriminator}`;
|
|
84
169
|
}
|
|
85
170
|
|
|
86
171
|
function providerStateKey(
|
|
87
172
|
providerId: string | undefined,
|
|
88
173
|
namespace: string,
|
|
174
|
+
scopeDiscriminator: string,
|
|
89
175
|
key: string,
|
|
90
176
|
): string {
|
|
91
|
-
return `${providerStatePrefix(providerId, namespace)}:${key}`;
|
|
177
|
+
return `${providerStatePrefix(providerId, namespace, scopeDiscriminator)}:${key}`;
|
|
92
178
|
}
|
|
93
179
|
|
|
94
|
-
function publicStateKey(
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
)
|
|
99
|
-
|
|
100
|
-
|
|
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;
|
|
101
188
|
}
|
|
102
189
|
|
|
103
190
|
function parseStateDurationMs(ttl: StateWriteOptions["ttl"]): number {
|
|
@@ -118,23 +205,27 @@ function parseStateDurationMs(ttl: StateWriteOptions["ttl"]): number {
|
|
|
118
205
|
return Math.max(1, amount * multiplier);
|
|
119
206
|
}
|
|
120
207
|
|
|
121
|
-
function resolveExpiresAt(ttl: StateWriteOptions["ttl"]): string {
|
|
122
|
-
return new Date(Date.now() + parseStateDurationMs(ttl)).toISOString();
|
|
123
|
-
}
|
|
124
|
-
|
|
125
208
|
function envelopeFromJson(
|
|
126
209
|
key: string,
|
|
127
210
|
raw: string | null,
|
|
128
211
|
// biome-ignore lint/suspicious/noExplicitAny: state envelopes deserialize caller-owned generic values.
|
|
129
212
|
): StateValue<any> | null {
|
|
130
213
|
if (!raw) return null;
|
|
131
|
-
|
|
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
|
+
}
|
|
132
225
|
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
133
226
|
return null;
|
|
134
227
|
}
|
|
135
|
-
const record: Record<string, unknown> = Object.fromEntries(
|
|
136
|
-
Object.entries(parsed),
|
|
137
|
-
);
|
|
228
|
+
const record: Record<string, unknown> = Object.fromEntries(Object.entries(parsed));
|
|
138
229
|
if (
|
|
139
230
|
typeof record.version !== "number" ||
|
|
140
231
|
typeof record.expiresAt !== "string" ||
|
|
@@ -154,6 +245,21 @@ function envelopeFromJson(
|
|
|
154
245
|
};
|
|
155
246
|
}
|
|
156
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
|
+
|
|
157
263
|
function redisEnvelope(
|
|
158
264
|
value: unknown,
|
|
159
265
|
version: number,
|
|
@@ -170,21 +276,60 @@ class RedisProviderStateNamespace implements ProviderStateNamespace {
|
|
|
170
276
|
private readonly providerId: string | undefined,
|
|
171
277
|
private readonly namespaceName: string,
|
|
172
278
|
private readonly options: StateNamespaceOptions,
|
|
279
|
+
private readonly scopeDiscriminator: string,
|
|
173
280
|
) {}
|
|
174
281
|
|
|
175
282
|
private redisKey(key: string): string {
|
|
176
|
-
return providerStateKey(
|
|
283
|
+
return providerStateKey(
|
|
284
|
+
this.providerId,
|
|
285
|
+
this.namespaceName,
|
|
286
|
+
this.scopeDiscriminator,
|
|
287
|
+
key,
|
|
288
|
+
);
|
|
177
289
|
}
|
|
178
290
|
|
|
179
|
-
private
|
|
180
|
-
return
|
|
291
|
+
private statePrefix(): string {
|
|
292
|
+
return providerStatePrefix(
|
|
293
|
+
this.providerId,
|
|
294
|
+
this.namespaceName,
|
|
295
|
+
this.scopeDiscriminator,
|
|
296
|
+
);
|
|
181
297
|
}
|
|
182
298
|
|
|
183
|
-
private
|
|
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}`;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
private async indexedKeys(limit: number): Promise<string[]> {
|
|
184
315
|
await requireRedisReady(this.backend.redis);
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
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)));
|
|
188
333
|
}
|
|
189
334
|
|
|
190
335
|
private enforceValueSize(value: unknown): void {
|
|
@@ -196,80 +341,86 @@ class RedisProviderStateNamespace implements ProviderStateNamespace {
|
|
|
196
341
|
}
|
|
197
342
|
}
|
|
198
343
|
|
|
199
|
-
private
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
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) {
|
|
204
358
|
throw new UnsupportedProviderStateError(
|
|
205
|
-
`Provider runtime state
|
|
359
|
+
`Provider runtime state ttl exceeds maxTtl (${ttlMs} > ${maxTtlMs})`,
|
|
206
360
|
);
|
|
207
361
|
}
|
|
362
|
+
const expiresAtMs = Date.now() + ttlMs;
|
|
363
|
+
return {
|
|
364
|
+
expiresAt: new Date(expiresAtMs).toISOString(),
|
|
365
|
+
expiresAtMs,
|
|
366
|
+
indexTtlMs: maxTtlMs,
|
|
367
|
+
};
|
|
208
368
|
}
|
|
209
369
|
|
|
210
|
-
async list<T>(options?: {
|
|
211
|
-
limit
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
);
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
);
|
|
227
|
-
return values.flatMap((raw, index) => {
|
|
228
|
-
const key = limited[index];
|
|
229
|
-
if (!key) return [];
|
|
230
|
-
const value = envelopeFromJson(
|
|
231
|
-
publicStateKey(this.providerId, this.namespaceName, key),
|
|
232
|
-
raw,
|
|
233
|
-
);
|
|
234
|
-
return value ? [value] : [];
|
|
235
|
-
});
|
|
370
|
+
async list<T>(options?: { limit?: number; prefix?: string }): Promise<StateValue<T>[]> {
|
|
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);
|
|
236
387
|
}
|
|
237
388
|
|
|
238
389
|
async get<T>(key: string): Promise<StateValue<T> | null> {
|
|
239
|
-
await
|
|
240
|
-
|
|
241
|
-
this.backend.redis.get(this.redisKey(key)),
|
|
242
|
-
);
|
|
390
|
+
const raw = await this.readRaw(key);
|
|
391
|
+
if (isStateTombstone(raw)) return null;
|
|
243
392
|
return envelopeFromJson(key, raw);
|
|
244
393
|
}
|
|
245
394
|
|
|
246
|
-
async set<T>(
|
|
247
|
-
key: string,
|
|
248
|
-
value: T,
|
|
249
|
-
options?: StateWriteOptions,
|
|
250
|
-
): Promise<StateValue<T>> {
|
|
395
|
+
async set<T>(key: string, value: T, options?: StateWriteOptions): Promise<StateValue<T>> {
|
|
251
396
|
this.enforceValueSize(value);
|
|
252
|
-
await this.enforceMaxEntries(key);
|
|
253
397
|
const current = await this.get<T>(key);
|
|
254
398
|
const createdAt = current?.createdAt ?? new Date().toISOString();
|
|
255
399
|
const version = (current?.version ?? 0) + 1;
|
|
256
|
-
const
|
|
257
|
-
const
|
|
258
|
-
|
|
259
|
-
const
|
|
260
|
-
|
|
261
|
-
|
|
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,
|
|
262
407
|
this.redisKey(key),
|
|
408
|
+
this.indexKey(),
|
|
409
|
+
String(Date.now()),
|
|
410
|
+
String(this.options.maxEntries),
|
|
411
|
+
String(timing.expiresAtMs),
|
|
412
|
+
String(timing.indexTtlMs),
|
|
263
413
|
JSON.stringify(envelope),
|
|
264
|
-
"PX",
|
|
265
|
-
ttlMs,
|
|
266
414
|
),
|
|
267
415
|
);
|
|
416
|
+
if (!Array.isArray(result) || Number(result[0]) !== 1) {
|
|
417
|
+
throw this.quotaExceeded();
|
|
418
|
+
}
|
|
268
419
|
return {
|
|
269
420
|
key,
|
|
270
421
|
value,
|
|
271
422
|
version,
|
|
272
|
-
expiresAt,
|
|
423
|
+
expiresAt: timing.expiresAt,
|
|
273
424
|
createdAt,
|
|
274
425
|
updatedAt: envelope.updatedAt,
|
|
275
426
|
};
|
|
@@ -293,16 +444,59 @@ class RedisProviderStateNamespace implements ProviderStateNamespace {
|
|
|
293
444
|
options?: StateWriteOptions,
|
|
294
445
|
): Promise<StateCasResult<T>> {
|
|
295
446
|
this.enforceValueSize(value);
|
|
296
|
-
const
|
|
297
|
-
|
|
298
|
-
|
|
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();
|
|
299
472
|
}
|
|
300
|
-
|
|
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
|
+
);
|
|
483
|
+
}
|
|
484
|
+
return { ok: true, value: written };
|
|
301
485
|
}
|
|
302
486
|
|
|
303
487
|
async delete(key: string): Promise<void> {
|
|
304
488
|
await requireRedisReady(this.backend.redis);
|
|
305
|
-
|
|
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
|
+
);
|
|
306
500
|
}
|
|
307
501
|
|
|
308
502
|
async increment(
|
|
@@ -313,60 +507,59 @@ class RedisProviderStateNamespace implements ProviderStateNamespace {
|
|
|
313
507
|
): Promise<StateValue<Record<string, unknown>>> {
|
|
314
508
|
const current = (await this.get<Record<string, unknown>>(key))?.value ?? {};
|
|
315
509
|
const previous = typeof current[field] === "number" ? current[field] : 0;
|
|
316
|
-
return await this.set(
|
|
317
|
-
key,
|
|
318
|
-
{ ...current, [field]: previous + delta },
|
|
319
|
-
options,
|
|
320
|
-
);
|
|
510
|
+
return await this.set(key, { ...current, [field]: previous + delta }, options);
|
|
321
511
|
}
|
|
322
512
|
}
|
|
323
513
|
|
|
324
514
|
class RedisProviderRuntimeState implements ProviderRuntimeState {
|
|
325
515
|
readonly backend: RedisBackend;
|
|
326
516
|
readonly providerId?: string;
|
|
517
|
+
readonly redisUrl: string;
|
|
518
|
+
readonly scopeDiscriminator: string;
|
|
327
519
|
|
|
328
|
-
constructor(
|
|
329
|
-
|
|
520
|
+
constructor(
|
|
521
|
+
options: RedisProviderRuntimeStateOptions,
|
|
522
|
+
scopeDiscriminator = MISSING_CONNECTION_SCOPE_DISCRIMINATOR,
|
|
523
|
+
) {
|
|
524
|
+
this.backend = getRedisBackend(options.redisUrl, options.__redisClient);
|
|
330
525
|
this.providerId = options.providerId;
|
|
526
|
+
this.redisUrl = options.redisUrl;
|
|
527
|
+
this.scopeDiscriminator = scopeDiscriminator;
|
|
331
528
|
}
|
|
332
529
|
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
530
|
+
forConnection(connectionId: string | undefined): ProviderRuntimeState {
|
|
531
|
+
return new RedisProviderRuntimeState(
|
|
532
|
+
{ redisUrl: this.redisUrl, providerId: this.providerId },
|
|
533
|
+
connectionScopeDiscriminator(connectionId),
|
|
534
|
+
);
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
namespace(name: string, options: StateNamespaceOptions): ProviderStateNamespace {
|
|
337
538
|
return new RedisProviderStateNamespace(
|
|
338
539
|
this.backend,
|
|
339
540
|
this.providerId,
|
|
340
541
|
name,
|
|
341
542
|
options,
|
|
543
|
+
options.scope === "provider" ? PROVIDER_SCOPE_DISCRIMINATOR : this.scopeDiscriminator,
|
|
342
544
|
);
|
|
343
545
|
}
|
|
344
546
|
}
|
|
345
547
|
|
|
346
548
|
export class UnsupportedProviderStateError extends ProviderError {
|
|
347
|
-
constructor(
|
|
348
|
-
message = "Provider runtime state is not available in this runtime",
|
|
349
|
-
) {
|
|
549
|
+
constructor(message = "Provider runtime state is not available in this runtime") {
|
|
350
550
|
super(message, { code: "PROVIDER_STATE_UNSUPPORTED" });
|
|
351
551
|
this.name = "UnsupportedProviderStateError";
|
|
352
552
|
}
|
|
353
553
|
}
|
|
354
554
|
|
|
355
555
|
class UnsupportedProviderStateNamespace implements ProviderStateNamespace {
|
|
356
|
-
async list<T>(_options?: {
|
|
357
|
-
limit?: number;
|
|
358
|
-
prefix?: string;
|
|
359
|
-
}): Promise<StateValue<T>[]> {
|
|
556
|
+
async list<T>(_options?: { limit?: number; prefix?: string }): Promise<StateValue<T>[]> {
|
|
360
557
|
throw new UnsupportedProviderStateError();
|
|
361
558
|
}
|
|
362
559
|
async get<T>(_key: string): Promise<StateValue<T> | null> {
|
|
363
560
|
throw new UnsupportedProviderStateError();
|
|
364
561
|
}
|
|
365
|
-
async set<T>(
|
|
366
|
-
_key: string,
|
|
367
|
-
_value: T,
|
|
368
|
-
_options?: StateWriteOptions,
|
|
369
|
-
): Promise<StateValue<T>> {
|
|
562
|
+
async set<T>(_key: string, _value: T, _options?: StateWriteOptions): Promise<StateValue<T>> {
|
|
370
563
|
throw new UnsupportedProviderStateError();
|
|
371
564
|
}
|
|
372
565
|
async patch<T extends Record<string, unknown>>(
|
|
@@ -398,10 +591,11 @@ class UnsupportedProviderStateNamespace implements ProviderStateNamespace {
|
|
|
398
591
|
}
|
|
399
592
|
|
|
400
593
|
class UnsupportedProviderRuntimeState implements ProviderRuntimeState {
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
594
|
+
forConnection(_connectionId: string | undefined): ProviderRuntimeState {
|
|
595
|
+
return new UnsupportedProviderRuntimeState();
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
namespace(_name: string, _options: StateNamespaceOptions): ProviderStateNamespace {
|
|
405
599
|
return new UnsupportedProviderStateNamespace();
|
|
406
600
|
}
|
|
407
601
|
}
|
|
@@ -412,6 +606,31 @@ class MemoryProviderStateNamespace implements ProviderStateNamespace {
|
|
|
412
606
|
|
|
413
607
|
constructor(private readonly options: StateNamespaceOptions) {}
|
|
414
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
|
+
|
|
415
634
|
private pruneExpired(nowMs = Date.now()): void {
|
|
416
635
|
for (const [key, row] of this.values.entries()) {
|
|
417
636
|
if (row.expiresAt && Date.parse(row.expiresAt) <= nowMs) {
|
|
@@ -420,10 +639,7 @@ class MemoryProviderStateNamespace implements ProviderStateNamespace {
|
|
|
420
639
|
}
|
|
421
640
|
}
|
|
422
641
|
|
|
423
|
-
async list<T>(_options?: {
|
|
424
|
-
limit?: number;
|
|
425
|
-
prefix?: string;
|
|
426
|
-
}): Promise<StateValue<T>[]> {
|
|
642
|
+
async list<T>(_options?: { limit?: number; prefix?: string }): Promise<StateValue<T>[]> {
|
|
427
643
|
this.pruneExpired();
|
|
428
644
|
const rows = Array.from(this.values.values()).filter((value) =>
|
|
429
645
|
_options?.prefix ? value.key.startsWith(_options.prefix) : true,
|
|
@@ -436,17 +652,12 @@ class MemoryProviderStateNamespace implements ProviderStateNamespace {
|
|
|
436
652
|
return this.values.get(key) ?? null;
|
|
437
653
|
}
|
|
438
654
|
|
|
439
|
-
async set<T>(
|
|
440
|
-
key: string,
|
|
441
|
-
value: T,
|
|
442
|
-
options?: StateWriteOptions,
|
|
443
|
-
): Promise<StateValue<T>> {
|
|
655
|
+
async set<T>(key: string, value: T, options?: StateWriteOptions): Promise<StateValue<T>> {
|
|
444
656
|
this.pruneExpired();
|
|
657
|
+
this.enforceWritePolicy(key, value, options?.ttl);
|
|
445
658
|
const now = new Date().toISOString();
|
|
446
659
|
const current = this.values.get(key);
|
|
447
|
-
const expiresAt = resolveMemoryStateExpiresAt(
|
|
448
|
-
options?.ttl ?? this.options.defaultTtl,
|
|
449
|
-
);
|
|
660
|
+
const expiresAt = resolveMemoryStateExpiresAt(options?.ttl ?? this.options.defaultTtl);
|
|
450
661
|
const row = {
|
|
451
662
|
key,
|
|
452
663
|
value,
|
|
@@ -470,14 +681,30 @@ class MemoryProviderStateNamespace implements ProviderStateNamespace {
|
|
|
470
681
|
}
|
|
471
682
|
|
|
472
683
|
async compareAndSet<T>(
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
684
|
+
key: string,
|
|
685
|
+
expectedVersion: number,
|
|
686
|
+
value: T,
|
|
687
|
+
options?: StateWriteOptions,
|
|
477
688
|
): Promise<StateCasResult<T>> {
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
)
|
|
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 };
|
|
481
708
|
}
|
|
482
709
|
|
|
483
710
|
async delete(key: string): Promise<void> {
|
|
@@ -496,17 +723,31 @@ class MemoryProviderStateNamespace implements ProviderStateNamespace {
|
|
|
496
723
|
}
|
|
497
724
|
}
|
|
498
725
|
|
|
726
|
+
type MemoryProviderStateBackend = {
|
|
727
|
+
readonly namespaces: Map<string, MemoryProviderStateNamespace>;
|
|
728
|
+
};
|
|
729
|
+
|
|
499
730
|
class MemoryProviderRuntimeState implements ProviderRuntimeState {
|
|
500
|
-
|
|
731
|
+
constructor(
|
|
732
|
+
private readonly backend: MemoryProviderStateBackend = { namespaces: new Map() },
|
|
733
|
+
private readonly scopeDiscriminator = MISSING_CONNECTION_SCOPE_DISCRIMINATOR,
|
|
734
|
+
) {}
|
|
735
|
+
|
|
736
|
+
forConnection(connectionId: string | undefined): ProviderRuntimeState {
|
|
737
|
+
return new MemoryProviderRuntimeState(
|
|
738
|
+
this.backend,
|
|
739
|
+
connectionScopeDiscriminator(connectionId),
|
|
740
|
+
);
|
|
741
|
+
}
|
|
501
742
|
|
|
502
|
-
namespace(
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
const existing = this.namespaces.get(
|
|
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);
|
|
507
748
|
if (existing) return existing;
|
|
508
|
-
const created = new MemoryProviderStateNamespace(
|
|
509
|
-
this.namespaces.set(
|
|
749
|
+
const created = new MemoryProviderStateNamespace(options);
|
|
750
|
+
this.backend.namespaces.set(namespaceIdentity, created);
|
|
510
751
|
return created;
|
|
511
752
|
}
|
|
512
753
|
}
|
|
@@ -536,10 +777,7 @@ export function createRedisProviderRuntimeState(
|
|
|
536
777
|
}
|
|
537
778
|
|
|
538
779
|
export function createProviderRuntimeStateFromEnv(
|
|
539
|
-
options: {
|
|
540
|
-
readonly providerId?: string;
|
|
541
|
-
readonly allowMemoryFallback?: boolean;
|
|
542
|
-
} = {},
|
|
780
|
+
options: { readonly providerId?: string; readonly allowMemoryFallback?: boolean } = {},
|
|
543
781
|
): ProviderRuntimeState {
|
|
544
782
|
const redisUrl = providerStateRedisUrlFromEnv();
|
|
545
783
|
if (redisUrl) {
|