@apifuse/provider-sdk 2.2.0-beta.2 → 2.2.0-beta.21
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 +487 -0
- package/CHANGELOG.md +90 -0
- package/README.md +49 -3
- package/SUBMISSION.md +1 -1
- package/bin/apifuse-check.ts +44 -59
- package/bin/apifuse-create.ts +1 -1
- package/bin/apifuse-dev.ts +27 -52
- package/bin/apifuse-pack-check.ts +14 -0
- package/bin/apifuse-pack-smoke.ts +36 -81
- package/bin/apifuse-pack-types.ts +305 -0
- package/bin/apifuse-perf.ts +45 -127
- package/bin/apifuse-record.ts +659 -111
- package/bin/apifuse-submit-check.ts +546 -48
- 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-semantics.ts +204 -0
- package/bin/submit-check-xml.ts +134 -0
- package/dist/auth-turn/index.d.ts +3 -3
- package/dist/auth.d.ts +2 -2
- package/dist/auth.js +9 -18
- package/dist/ceremonies/index.d.ts +9 -1
- package/dist/ceremonies/index.js +65 -18
- 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/config/loader.d.ts +176 -8
- package/dist/config/loader.js +424 -95
- package/dist/contract-serialization.d.ts +2 -2
- package/dist/contract-serialization.js +3 -6
- package/dist/contract-types.d.ts +2 -2
- package/dist/contract.d.ts +3 -3
- package/dist/contract.js +4 -6
- package/dist/define.d.ts +9 -1
- package/dist/define.js +304 -119
- package/dist/dev.d.ts +1 -1
- package/dist/dev.js +1 -1
- package/dist/error-resolution.d.ts +2 -0
- package/dist/error-resolution.js +90 -0
- package/dist/errors.d.ts +22 -1
- package/dist/errors.js +88 -0
- 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 +46 -42
- package/dist/index.js +41 -37
- package/dist/lint.d.ts +1 -1
- package/dist/lint.js +8 -15
- 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 +12 -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 +2 -1
- package/dist/runtime/auth-flow.js +4 -3
- package/dist/runtime/browser.d.ts +1 -1
- package/dist/runtime/browser.js +15 -29
- package/dist/runtime/cache.d.ts +1 -1
- package/dist/runtime/cache.js +4 -8
- package/dist/runtime/choice.d.ts +1 -1
- package/dist/runtime/choice.js +31 -35
- 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 +31 -4
- package/dist/runtime/http.d.ts +2 -2
- package/dist/runtime/http.js +387 -47
- 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 +345 -22
- package/dist/runtime/keyring.js +1 -1
- package/dist/runtime/namespace.js +1 -1
- package/dist/runtime/native-network.d.ts +127 -0
- package/dist/runtime/native-network.js +1298 -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 +55 -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 +2 -4
- package/dist/runtime/request-options.d.ts +68 -1
- package/dist/runtime/request-options.js +548 -0
- package/dist/runtime/secrets.d.ts +27 -0
- package/dist/runtime/secrets.js +51 -0
- package/dist/runtime/state.d.ts +2 -2
- package/dist/runtime/state.js +238 -26
- package/dist/runtime/stealth.d.ts +5 -3
- package/dist/runtime/stealth.js +423 -143
- package/dist/runtime/stt.d.ts +1 -1
- package/dist/runtime/stt.js +11 -15
- 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 +104 -3
- package/dist/server/self-test.js +673 -115
- package/dist/server/serve.d.ts +116 -4
- package/dist/server/serve.js +799 -128
- package/dist/server/types.d.ts +34 -9
- package/dist/server/types.js +8 -1
- package/dist/stateful/errors.d.ts +14 -0
- package/dist/stateful/errors.js +14 -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 +71 -0
- package/dist/stateful/stateful-provider-session-routing.js +353 -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 +5 -14
- package/dist/stream-evidence.d.ts +74 -0
- package/dist/stream-evidence.js +785 -0
- package/dist/stream.d.ts +1 -1
- package/dist/testing/index.d.ts +2 -2
- package/dist/testing/index.js +2 -2
- package/dist/testing/run.d.ts +32 -2
- package/dist/testing/run.js +478 -28
- package/dist/types.d.ts +342 -14
- package/dist/types.js +1 -0
- package/dist/user-input.d.ts +30 -0
- package/dist/user-input.js +66 -0
- package/package.json +16 -5
- package/src/auth-turn/index.ts +1 -1
- package/src/auth.ts +28 -86
- package/src/ceremonies/index.ts +103 -78
- 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/config/loader.ts +652 -204
- package/src/contract-serialization.ts +5 -11
- package/src/contract-types.ts +2 -2
- package/src/contract.ts +12 -28
- package/src/define.ts +499 -498
- package/src/dev.ts +4 -9
- package/src/error-resolution.ts +91 -0
- package/src/errors.ts +110 -5
- 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 +112 -42
- package/src/lint.ts +88 -152
- 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 +66 -11
- package/src/public-schema-field-lint.ts +7 -33
- package/src/recipes/gov-api.ts +2 -5
- package/src/runtime/auth-flow.ts +7 -7
- package/src/runtime/browser.ts +64 -187
- package/src/runtime/cache.ts +20 -67
- package/src/runtime/choice.ts +79 -132
- package/src/runtime/credential.ts +2 -2
- package/src/runtime/env.ts +1 -1
- package/src/runtime/executor.ts +43 -20
- package/src/runtime/http.ts +494 -57
- package/src/runtime/insights.ts +15 -53
- package/src/runtime/instrumentation.ts +495 -66
- package/src/runtime/keyring.ts +7 -19
- package/src/runtime/namespace.ts +2 -7
- package/src/runtime/native-network.ts +1686 -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 +79 -77
- package/src/runtime/redirects.ts +66 -0
- package/src/runtime/redis.ts +4 -12
- package/src/runtime/request-options.ts +679 -9
- package/src/runtime/secrets.ts +64 -0
- package/src/runtime/state.ts +353 -133
- package/src/runtime/stealth.ts +505 -154
- package/src/runtime/stt.ts +38 -94
- package/src/runtime/trace.ts +14 -44
- package/src/runtime/waterfall.ts +5 -18
- package/src/schema.ts +23 -84
- package/src/serve.ts +1 -1
- package/src/server/index.ts +29 -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 +857 -132
- package/src/server/serve.ts +1151 -328
- package/src/server/types.ts +12 -13
- package/src/stateful/README.md +146 -0
- package/src/stateful/errors.ts +23 -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 +555 -0
- package/src/stateful/stateful-provider-session-runtime.ts +403 -0
- package/src/stateful-signing.ts +46 -0
- package/src/stealth/profiles.ts +10 -26
- package/src/stream-evidence.ts +988 -0
- package/src/stream.ts +8 -19
- package/src/testing/index.ts +10 -2
- package/src/testing/run.ts +653 -74
- package/src/types.ts +408 -28
- 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,5 @@
|
|
|
1
|
-
import { providerStateRedisUrlFromEnv } from "../config/loader";
|
|
2
|
-
import { ProviderError } from "../errors";
|
|
1
|
+
import { providerStateRedisUrlFromEnv } from "../config/loader.js";
|
|
2
|
+
import { ProviderError } from "../errors.js";
|
|
3
3
|
import type {
|
|
4
4
|
ProviderRuntimeState,
|
|
5
5
|
ProviderStateNamespace,
|
|
@@ -7,16 +7,103 @@ import type {
|
|
|
7
7
|
StateNamespaceOptions,
|
|
8
8
|
StateValue,
|
|
9
9
|
StateWriteOptions,
|
|
10
|
-
} from "../types";
|
|
10
|
+
} from "../types.js";
|
|
11
11
|
import {
|
|
12
12
|
createProviderRedisClient,
|
|
13
13
|
ensureRedisReady,
|
|
14
14
|
type ProviderRedisClient,
|
|
15
15
|
withRedisTimeout,
|
|
16
|
-
} from "./redis";
|
|
16
|
+
} from "./redis.js";
|
|
17
17
|
|
|
18
18
|
const DEFAULT_REDIS_TIMEOUT_MS = 250;
|
|
19
19
|
const REDIS_STATE_PREFIX = "apifuse:provider-state:v1";
|
|
20
|
+
const LEGACY_INDEX_SCAN_COUNT = 256;
|
|
21
|
+
const LEGACY_INDEX_SCAN_MAX_PAGES = 8;
|
|
22
|
+
const SET_WITH_QUOTA_SCRIPT = `
|
|
23
|
+
local now = tonumber(ARGV[1])
|
|
24
|
+
local max_entries = tonumber(ARGV[2])
|
|
25
|
+
local expires_at = tonumber(ARGV[3])
|
|
26
|
+
local index_ttl = tonumber(ARGV[4])
|
|
27
|
+
local envelope = ARGV[5]
|
|
28
|
+
|
|
29
|
+
redis.call("ZREMRANGEBYSCORE", KEYS[2], "-inf", now)
|
|
30
|
+
local exists = redis.call("EXISTS", KEYS[1])
|
|
31
|
+
local indexed = redis.call("ZSCORE", KEYS[2], KEYS[1])
|
|
32
|
+
if exists == 0 and 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
|
+
local current_version = 0
|
|
45
|
+
if current then
|
|
46
|
+
local ok, decoded = pcall(cjson.decode, current)
|
|
47
|
+
if not ok or type(decoded) ~= "table" or type(decoded.version) ~= "number" then
|
|
48
|
+
return {-2, current}
|
|
49
|
+
end
|
|
50
|
+
current_version = decoded.version
|
|
51
|
+
end
|
|
52
|
+
if current_version ~= tonumber(ARGV[1]) then
|
|
53
|
+
return {-1, current or false}
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
local now = tonumber(ARGV[2])
|
|
57
|
+
local max_entries = tonumber(ARGV[3])
|
|
58
|
+
local expires_at = tonumber(ARGV[4])
|
|
59
|
+
local index_ttl = tonumber(ARGV[5])
|
|
60
|
+
local envelope = ARGV[6]
|
|
61
|
+
redis.call("ZREMRANGEBYSCORE", KEYS[2], "-inf", now)
|
|
62
|
+
local exists = current and 1 or 0
|
|
63
|
+
local indexed = redis.call("ZSCORE", KEYS[2], KEYS[1])
|
|
64
|
+
if exists == 0 and not indexed and redis.call("ZCARD", KEYS[2]) >= max_entries then
|
|
65
|
+
return {0, false}
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
redis.call("SET", KEYS[1], envelope, "PXAT", expires_at)
|
|
69
|
+
redis.call("ZADD", KEYS[2], expires_at, KEYS[1])
|
|
70
|
+
redis.call("PEXPIRE", KEYS[2], index_ttl)
|
|
71
|
+
return {1, envelope}
|
|
72
|
+
`;
|
|
73
|
+
|
|
74
|
+
const DELETE_WITH_INDEX_SCRIPT = `
|
|
75
|
+
redis.call("DEL", KEYS[1])
|
|
76
|
+
redis.call("ZREM", KEYS[2], KEYS[1])
|
|
77
|
+
return 1
|
|
78
|
+
`;
|
|
79
|
+
|
|
80
|
+
// Older SDKs wrote only the value key. Every operation that depends on the
|
|
81
|
+
// namespace index advances a bounded SCAN cursor and lazily imports active
|
|
82
|
+
// legacy envelopes into the new ZSET. The cursor is
|
|
83
|
+
// deliberately cyclic rather than permanently "complete": an old pod may
|
|
84
|
+
// still write an unindexed key during a rolling deploy. Each list/write call
|
|
85
|
+
// does a fixed amount of migration work; Redis KEYS and unbounded scans remain
|
|
86
|
+
// forbidden.
|
|
87
|
+
const BACKFILL_LEGACY_INDEX_SCRIPT = `
|
|
88
|
+
local now = tonumber(ARGV[1])
|
|
89
|
+
local index_ttl = tonumber(ARGV[2])
|
|
90
|
+
local next_cursor = ARGV[3]
|
|
91
|
+
|
|
92
|
+
redis.call("ZREMRANGEBYSCORE", KEYS[1], "-inf", now)
|
|
93
|
+
for i = 4, #ARGV, 3 do
|
|
94
|
+
local key = ARGV[i]
|
|
95
|
+
local expected = ARGV[i + 1]
|
|
96
|
+
local expires_at = tonumber(ARGV[i + 2])
|
|
97
|
+
if redis.call("GET", key) == expected then
|
|
98
|
+
redis.call("ZADD", KEYS[1], "NX", expires_at, key)
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
if redis.call("EXISTS", KEYS[1]) == 1 then
|
|
102
|
+
redis.call("PEXPIRE", KEYS[1], index_ttl)
|
|
103
|
+
end
|
|
104
|
+
redis.call("SET", KEYS[2], next_cursor, "PX", index_ttl)
|
|
105
|
+
return redis.call("ZCARD", KEYS[1])
|
|
106
|
+
`;
|
|
20
107
|
|
|
21
108
|
type RedisProviderRuntimeStateOptions = {
|
|
22
109
|
readonly redisUrl: string;
|
|
@@ -57,37 +144,24 @@ async function withRequiredRedis<T>(operation: () => Promise<T>): Promise<T> {
|
|
|
57
144
|
return await withRedisTimeout(operation, {
|
|
58
145
|
timeoutMs: DEFAULT_REDIS_TIMEOUT_MS,
|
|
59
146
|
onTimeout: () => {
|
|
60
|
-
throw new UnsupportedProviderStateError(
|
|
61
|
-
"Provider runtime state Redis timed out",
|
|
62
|
-
);
|
|
147
|
+
throw new UnsupportedProviderStateError("Provider runtime state Redis timed out");
|
|
63
148
|
},
|
|
64
149
|
onError: () => {
|
|
65
|
-
throw new UnsupportedProviderStateError(
|
|
66
|
-
"Provider runtime state Redis is unavailable",
|
|
67
|
-
);
|
|
150
|
+
throw new UnsupportedProviderStateError("Provider runtime state Redis is unavailable");
|
|
68
151
|
},
|
|
69
152
|
});
|
|
70
153
|
}
|
|
71
154
|
|
|
72
155
|
async function requireRedisReady(redis: ProviderRedisClient): Promise<void> {
|
|
73
156
|
if (await ensureRedisReady(redis, DEFAULT_REDIS_TIMEOUT_MS)) return;
|
|
74
|
-
throw new UnsupportedProviderStateError(
|
|
75
|
-
"Provider runtime state Redis is unavailable",
|
|
76
|
-
);
|
|
157
|
+
throw new UnsupportedProviderStateError("Provider runtime state Redis is unavailable");
|
|
77
158
|
}
|
|
78
159
|
|
|
79
|
-
function providerStatePrefix(
|
|
80
|
-
providerId: string | undefined,
|
|
81
|
-
namespace: string,
|
|
82
|
-
): string {
|
|
160
|
+
function providerStatePrefix(providerId: string | undefined, namespace: string): string {
|
|
83
161
|
return `${REDIS_STATE_PREFIX}:${providerId ?? "default"}:${namespace}`;
|
|
84
162
|
}
|
|
85
163
|
|
|
86
|
-
function providerStateKey(
|
|
87
|
-
providerId: string | undefined,
|
|
88
|
-
namespace: string,
|
|
89
|
-
key: string,
|
|
90
|
-
): string {
|
|
164
|
+
function providerStateKey(providerId: string | undefined, namespace: string, key: string): string {
|
|
91
165
|
return `${providerStatePrefix(providerId, namespace)}:${key}`;
|
|
92
166
|
}
|
|
93
167
|
|
|
@@ -100,6 +174,10 @@ function publicStateKey(
|
|
|
100
174
|
return redisKey.startsWith(prefix) ? redisKey.slice(prefix.length) : redisKey;
|
|
101
175
|
}
|
|
102
176
|
|
|
177
|
+
function redisGlobLiteral(value: string): string {
|
|
178
|
+
return value.replace(/[\\*?\[\]]/g, "\\$&");
|
|
179
|
+
}
|
|
180
|
+
|
|
103
181
|
function parseStateDurationMs(ttl: StateWriteOptions["ttl"]): number {
|
|
104
182
|
const match = /^(\d+)(ms|s|m|h|d)$/.exec(ttl ?? "1h");
|
|
105
183
|
if (!match) return 3_600_000;
|
|
@@ -128,13 +206,21 @@ function envelopeFromJson(
|
|
|
128
206
|
// biome-ignore lint/suspicious/noExplicitAny: state envelopes deserialize caller-owned generic values.
|
|
129
207
|
): StateValue<any> | null {
|
|
130
208
|
if (!raw) return null;
|
|
131
|
-
|
|
209
|
+
// A corrupt/undecodable persisted envelope must be treated as absent rather
|
|
210
|
+
// than throwing a raw JSON.parse SyntaxError: an uncaught SyntaxError escapes
|
|
211
|
+
// the provider error taxonomy, is masked as internal_error 500, and is then
|
|
212
|
+
// retried by the hub (2026-07-22 catchtable reserve RCA, candidate A). Returning
|
|
213
|
+
// null also keeps list() from aborting the whole scan on a single bad entry.
|
|
214
|
+
let parsed: unknown;
|
|
215
|
+
try {
|
|
216
|
+
parsed = JSON.parse(raw);
|
|
217
|
+
} catch {
|
|
218
|
+
return null;
|
|
219
|
+
}
|
|
132
220
|
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
133
221
|
return null;
|
|
134
222
|
}
|
|
135
|
-
const record: Record<string, unknown> = Object.fromEntries(
|
|
136
|
-
Object.entries(parsed),
|
|
137
|
-
);
|
|
223
|
+
const record: Record<string, unknown> = Object.fromEntries(Object.entries(parsed));
|
|
138
224
|
if (
|
|
139
225
|
typeof record.version !== "number" ||
|
|
140
226
|
typeof record.expiresAt !== "string" ||
|
|
@@ -176,15 +262,91 @@ class RedisProviderStateNamespace implements ProviderStateNamespace {
|
|
|
176
262
|
return providerStateKey(this.providerId, this.namespaceName, key);
|
|
177
263
|
}
|
|
178
264
|
|
|
179
|
-
private
|
|
265
|
+
private indexKey(): string {
|
|
266
|
+
// Keep bookkeeping outside the caller-owned keyspace. A provider may use
|
|
267
|
+
// any state key (including "__index"), so a suffix inside the namespace
|
|
268
|
+
// could turn the ZSET into a string and break every subsequent write.
|
|
269
|
+
const namespaceIdentity = Buffer.from(
|
|
270
|
+
providerStatePrefix(this.providerId, this.namespaceName),
|
|
271
|
+
"utf8",
|
|
272
|
+
).toString("base64url");
|
|
273
|
+
return `${REDIS_STATE_PREFIX}:index:${namespaceIdentity}`;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
private legacyScanCursorKey(): string {
|
|
277
|
+
return `${this.indexKey()}:legacy-scan-cursor`;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
private legacyPrefix(): string {
|
|
180
281
|
return `${providerStatePrefix(this.providerId, this.namespaceName)}:`;
|
|
181
282
|
}
|
|
182
283
|
|
|
183
|
-
private async
|
|
284
|
+
private async backfillLegacyIndex(): Promise<void> {
|
|
184
285
|
await requireRedisReady(this.backend.redis);
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
286
|
+
const cursorKey = this.legacyScanCursorKey();
|
|
287
|
+
let cursor =
|
|
288
|
+
(await withRequiredRedis(() => this.backend.redis.get(cursorKey))) ?? "0";
|
|
289
|
+
const pattern = `${redisGlobLiteral(this.legacyPrefix())}*`;
|
|
290
|
+
const indexTtlMs = parseStateDurationMs(this.options.maxTtl);
|
|
291
|
+
|
|
292
|
+
for (let page = 0; page < LEGACY_INDEX_SCAN_MAX_PAGES; page += 1) {
|
|
293
|
+
const [nextCursor, keys] = await withRequiredRedis(() =>
|
|
294
|
+
this.backend.redis.scan(
|
|
295
|
+
cursor,
|
|
296
|
+
"MATCH",
|
|
297
|
+
pattern,
|
|
298
|
+
"COUNT",
|
|
299
|
+
LEGACY_INDEX_SCAN_COUNT,
|
|
300
|
+
),
|
|
301
|
+
);
|
|
302
|
+
const rawValues =
|
|
303
|
+
keys.length > 0
|
|
304
|
+
? await withRequiredRedis(() => this.backend.redis.mget(keys))
|
|
305
|
+
: [];
|
|
306
|
+
const now = Date.now();
|
|
307
|
+
const activeLegacyArgs: string[] = [];
|
|
308
|
+
for (const [index, raw] of rawValues.entries()) {
|
|
309
|
+
const key = keys[index];
|
|
310
|
+
if (!key || !raw) continue;
|
|
311
|
+
const envelope = envelopeFromJson(
|
|
312
|
+
publicStateKey(this.providerId, this.namespaceName, key),
|
|
313
|
+
raw,
|
|
314
|
+
);
|
|
315
|
+
const expiresAtMs = envelope ? Date.parse(envelope.expiresAt) : Number.NaN;
|
|
316
|
+
if (!Number.isFinite(expiresAtMs) || expiresAtMs <= now) continue;
|
|
317
|
+
activeLegacyArgs.push(key, raw, String(expiresAtMs));
|
|
318
|
+
}
|
|
319
|
+
await withRequiredRedis(() =>
|
|
320
|
+
this.backend.redis.eval(
|
|
321
|
+
BACKFILL_LEGACY_INDEX_SCRIPT,
|
|
322
|
+
2,
|
|
323
|
+
this.indexKey(),
|
|
324
|
+
cursorKey,
|
|
325
|
+
String(now),
|
|
326
|
+
String(indexTtlMs),
|
|
327
|
+
nextCursor,
|
|
328
|
+
...activeLegacyArgs,
|
|
329
|
+
),
|
|
330
|
+
);
|
|
331
|
+
cursor = nextCursor;
|
|
332
|
+
if (cursor === "0") break;
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
private async indexedKeys(limit: number): Promise<string[]> {
|
|
337
|
+
await requireRedisReady(this.backend.redis);
|
|
338
|
+
const now = Date.now();
|
|
339
|
+
return await withRequiredRedis(async () => {
|
|
340
|
+
await this.backend.redis.zremrangebyscore(this.indexKey(), "-inf", now);
|
|
341
|
+
return await this.backend.redis.zrangebyscore(
|
|
342
|
+
this.indexKey(),
|
|
343
|
+
now + 1,
|
|
344
|
+
"+inf",
|
|
345
|
+
"LIMIT",
|
|
346
|
+
0,
|
|
347
|
+
limit,
|
|
348
|
+
);
|
|
349
|
+
});
|
|
188
350
|
}
|
|
189
351
|
|
|
190
352
|
private enforceValueSize(value: unknown): void {
|
|
@@ -196,80 +358,87 @@ class RedisProviderStateNamespace implements ProviderStateNamespace {
|
|
|
196
358
|
}
|
|
197
359
|
}
|
|
198
360
|
|
|
199
|
-
private
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
361
|
+
private quotaExceeded(): UnsupportedProviderStateError {
|
|
362
|
+
return new UnsupportedProviderStateError(
|
|
363
|
+
`Provider runtime state namespace quota exceeded (${this.options.maxEntries + 1} > ${this.options.maxEntries})`,
|
|
364
|
+
);
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
private writeTiming(ttl: StateWriteOptions["ttl"]): {
|
|
368
|
+
expiresAt: string;
|
|
369
|
+
expiresAtMs: number;
|
|
370
|
+
indexTtlMs: number;
|
|
371
|
+
} {
|
|
372
|
+
const ttlMs = parseStateDurationMs(ttl ?? this.options.defaultTtl);
|
|
373
|
+
const maxTtlMs = parseStateDurationMs(this.options.maxTtl);
|
|
374
|
+
if (ttlMs > maxTtlMs) {
|
|
204
375
|
throw new UnsupportedProviderStateError(
|
|
205
|
-
`Provider runtime state
|
|
376
|
+
`Provider runtime state ttl exceeds maxTtl (${ttlMs} > ${maxTtlMs})`,
|
|
206
377
|
);
|
|
207
378
|
}
|
|
379
|
+
const expiresAtMs = Date.now() + ttlMs;
|
|
380
|
+
return {
|
|
381
|
+
expiresAt: new Date(expiresAtMs).toISOString(),
|
|
382
|
+
expiresAtMs,
|
|
383
|
+
indexTtlMs: maxTtlMs,
|
|
384
|
+
};
|
|
208
385
|
}
|
|
209
386
|
|
|
210
|
-
async list<T>(options?: {
|
|
211
|
-
limit
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
const keys = (await this.
|
|
215
|
-
const publicKey = publicStateKey(
|
|
216
|
-
this.providerId,
|
|
217
|
-
this.namespaceName,
|
|
218
|
-
key,
|
|
219
|
-
);
|
|
387
|
+
async list<T>(options?: { limit?: number; prefix?: string }): Promise<StateValue<T>[]> {
|
|
388
|
+
const requestedLimit = Math.max(0, options?.limit ?? this.options.maxEntries);
|
|
389
|
+
if (requestedLimit === 0) return [];
|
|
390
|
+
await this.backfillLegacyIndex();
|
|
391
|
+
const keys = (await this.indexedKeys(this.options.maxEntries)).filter((key) => {
|
|
392
|
+
const publicKey = publicStateKey(this.providerId, this.namespaceName, key);
|
|
220
393
|
return options?.prefix ? publicKey.startsWith(options.prefix) : true;
|
|
221
394
|
});
|
|
222
|
-
const limited = keys.slice(0,
|
|
395
|
+
const limited = keys.slice(0, requestedLimit);
|
|
223
396
|
if (limited.length === 0) return [];
|
|
224
|
-
const values = await withRequiredRedis(() =>
|
|
225
|
-
this.backend.redis.mget(limited),
|
|
226
|
-
);
|
|
397
|
+
const values = await withRequiredRedis(() => this.backend.redis.mget(limited));
|
|
227
398
|
return values.flatMap((raw, index) => {
|
|
228
399
|
const key = limited[index];
|
|
229
400
|
if (!key) return [];
|
|
230
|
-
const value = envelopeFromJson(
|
|
231
|
-
publicStateKey(this.providerId, this.namespaceName, key),
|
|
232
|
-
raw,
|
|
233
|
-
);
|
|
401
|
+
const value = envelopeFromJson(publicStateKey(this.providerId, this.namespaceName, key), raw);
|
|
234
402
|
return value ? [value] : [];
|
|
235
403
|
});
|
|
236
404
|
}
|
|
237
405
|
|
|
238
406
|
async get<T>(key: string): Promise<StateValue<T> | null> {
|
|
239
407
|
await requireRedisReady(this.backend.redis);
|
|
240
|
-
const raw = await withRequiredRedis(() =>
|
|
241
|
-
this.backend.redis.get(this.redisKey(key)),
|
|
242
|
-
);
|
|
408
|
+
const raw = await withRequiredRedis(() => this.backend.redis.get(this.redisKey(key)));
|
|
243
409
|
return envelopeFromJson(key, raw);
|
|
244
410
|
}
|
|
245
411
|
|
|
246
|
-
async set<T>(
|
|
247
|
-
key: string,
|
|
248
|
-
value: T,
|
|
249
|
-
options?: StateWriteOptions,
|
|
250
|
-
): Promise<StateValue<T>> {
|
|
412
|
+
async set<T>(key: string, value: T, options?: StateWriteOptions): Promise<StateValue<T>> {
|
|
251
413
|
this.enforceValueSize(value);
|
|
252
|
-
await this.
|
|
414
|
+
await this.backfillLegacyIndex();
|
|
253
415
|
const current = await this.get<T>(key);
|
|
254
416
|
const createdAt = current?.createdAt ?? new Date().toISOString();
|
|
255
417
|
const version = (current?.version ?? 0) + 1;
|
|
256
|
-
const
|
|
257
|
-
const
|
|
258
|
-
|
|
259
|
-
const
|
|
260
|
-
|
|
261
|
-
|
|
418
|
+
const timing = this.writeTiming(options?.ttl);
|
|
419
|
+
const envelope = redisEnvelope(value, version, createdAt, timing.expiresAt);
|
|
420
|
+
await requireRedisReady(this.backend.redis);
|
|
421
|
+
const result = await withRequiredRedis(() =>
|
|
422
|
+
this.backend.redis.eval(
|
|
423
|
+
SET_WITH_QUOTA_SCRIPT,
|
|
424
|
+
2,
|
|
262
425
|
this.redisKey(key),
|
|
426
|
+
this.indexKey(),
|
|
427
|
+
String(Date.now()),
|
|
428
|
+
String(this.options.maxEntries),
|
|
429
|
+
String(timing.expiresAtMs),
|
|
430
|
+
String(timing.indexTtlMs),
|
|
263
431
|
JSON.stringify(envelope),
|
|
264
|
-
"PX",
|
|
265
|
-
ttlMs,
|
|
266
432
|
),
|
|
267
433
|
);
|
|
434
|
+
if (!Array.isArray(result) || Number(result[0]) !== 1) {
|
|
435
|
+
throw this.quotaExceeded();
|
|
436
|
+
}
|
|
268
437
|
return {
|
|
269
438
|
key,
|
|
270
439
|
value,
|
|
271
440
|
version,
|
|
272
|
-
expiresAt,
|
|
441
|
+
expiresAt: timing.expiresAt,
|
|
273
442
|
createdAt,
|
|
274
443
|
updatedAt: envelope.updatedAt,
|
|
275
444
|
};
|
|
@@ -293,16 +462,64 @@ class RedisProviderStateNamespace implements ProviderStateNamespace {
|
|
|
293
462
|
options?: StateWriteOptions,
|
|
294
463
|
): Promise<StateCasResult<T>> {
|
|
295
464
|
this.enforceValueSize(value);
|
|
465
|
+
await this.backfillLegacyIndex();
|
|
296
466
|
const current = await this.get<T>(key);
|
|
297
467
|
if ((current?.version ?? 0) !== expectedVersion) {
|
|
298
468
|
return { ok: false, current };
|
|
299
469
|
}
|
|
300
|
-
|
|
470
|
+
const createdAt = current?.createdAt ?? new Date().toISOString();
|
|
471
|
+
const timing = this.writeTiming(options?.ttl);
|
|
472
|
+
const envelope = redisEnvelope(
|
|
473
|
+
value,
|
|
474
|
+
expectedVersion + 1,
|
|
475
|
+
createdAt,
|
|
476
|
+
timing.expiresAt,
|
|
477
|
+
);
|
|
478
|
+
await requireRedisReady(this.backend.redis);
|
|
479
|
+
const result = await withRequiredRedis(() =>
|
|
480
|
+
this.backend.redis.eval(
|
|
481
|
+
COMPARE_AND_SET_WITH_QUOTA_SCRIPT,
|
|
482
|
+
2,
|
|
483
|
+
this.redisKey(key),
|
|
484
|
+
this.indexKey(),
|
|
485
|
+
String(expectedVersion),
|
|
486
|
+
String(Date.now()),
|
|
487
|
+
String(this.options.maxEntries),
|
|
488
|
+
String(timing.expiresAtMs),
|
|
489
|
+
String(timing.indexTtlMs),
|
|
490
|
+
JSON.stringify(envelope),
|
|
491
|
+
),
|
|
492
|
+
);
|
|
493
|
+
if (Array.isArray(result) && Number(result[0]) === 0) {
|
|
494
|
+
throw this.quotaExceeded();
|
|
495
|
+
}
|
|
496
|
+
if (!Array.isArray(result) || Number(result[0]) !== 1) {
|
|
497
|
+
const rawCurrent = Array.isArray(result) && typeof result[1] === "string" ? result[1] : null;
|
|
498
|
+
return { ok: false, current: envelopeFromJson(key, rawCurrent) };
|
|
499
|
+
}
|
|
500
|
+
return {
|
|
501
|
+
ok: true,
|
|
502
|
+
value: {
|
|
503
|
+
key,
|
|
504
|
+
value,
|
|
505
|
+
version: envelope.version,
|
|
506
|
+
expiresAt: timing.expiresAt,
|
|
507
|
+
createdAt,
|
|
508
|
+
updatedAt: envelope.updatedAt,
|
|
509
|
+
},
|
|
510
|
+
};
|
|
301
511
|
}
|
|
302
512
|
|
|
303
513
|
async delete(key: string): Promise<void> {
|
|
304
514
|
await requireRedisReady(this.backend.redis);
|
|
305
|
-
await withRequiredRedis(() =>
|
|
515
|
+
await withRequiredRedis(() =>
|
|
516
|
+
this.backend.redis.eval(
|
|
517
|
+
DELETE_WITH_INDEX_SCRIPT,
|
|
518
|
+
2,
|
|
519
|
+
this.redisKey(key),
|
|
520
|
+
this.indexKey(),
|
|
521
|
+
),
|
|
522
|
+
);
|
|
306
523
|
}
|
|
307
524
|
|
|
308
525
|
async increment(
|
|
@@ -313,11 +530,7 @@ class RedisProviderStateNamespace implements ProviderStateNamespace {
|
|
|
313
530
|
): Promise<StateValue<Record<string, unknown>>> {
|
|
314
531
|
const current = (await this.get<Record<string, unknown>>(key))?.value ?? {};
|
|
315
532
|
const previous = typeof current[field] === "number" ? current[field] : 0;
|
|
316
|
-
return await this.set(
|
|
317
|
-
key,
|
|
318
|
-
{ ...current, [field]: previous + delta },
|
|
319
|
-
options,
|
|
320
|
-
);
|
|
533
|
+
return await this.set(key, { ...current, [field]: previous + delta }, options);
|
|
321
534
|
}
|
|
322
535
|
}
|
|
323
536
|
|
|
@@ -330,43 +543,26 @@ class RedisProviderRuntimeState implements ProviderRuntimeState {
|
|
|
330
543
|
this.providerId = options.providerId;
|
|
331
544
|
}
|
|
332
545
|
|
|
333
|
-
namespace(
|
|
334
|
-
name
|
|
335
|
-
options: StateNamespaceOptions,
|
|
336
|
-
): ProviderStateNamespace {
|
|
337
|
-
return new RedisProviderStateNamespace(
|
|
338
|
-
this.backend,
|
|
339
|
-
this.providerId,
|
|
340
|
-
name,
|
|
341
|
-
options,
|
|
342
|
-
);
|
|
546
|
+
namespace(name: string, options: StateNamespaceOptions): ProviderStateNamespace {
|
|
547
|
+
return new RedisProviderStateNamespace(this.backend, this.providerId, name, options);
|
|
343
548
|
}
|
|
344
549
|
}
|
|
345
550
|
|
|
346
551
|
export class UnsupportedProviderStateError extends ProviderError {
|
|
347
|
-
constructor(
|
|
348
|
-
message = "Provider runtime state is not available in this runtime",
|
|
349
|
-
) {
|
|
552
|
+
constructor(message = "Provider runtime state is not available in this runtime") {
|
|
350
553
|
super(message, { code: "PROVIDER_STATE_UNSUPPORTED" });
|
|
351
554
|
this.name = "UnsupportedProviderStateError";
|
|
352
555
|
}
|
|
353
556
|
}
|
|
354
557
|
|
|
355
558
|
class UnsupportedProviderStateNamespace implements ProviderStateNamespace {
|
|
356
|
-
async list<T>(_options?: {
|
|
357
|
-
limit?: number;
|
|
358
|
-
prefix?: string;
|
|
359
|
-
}): Promise<StateValue<T>[]> {
|
|
559
|
+
async list<T>(_options?: { limit?: number; prefix?: string }): Promise<StateValue<T>[]> {
|
|
360
560
|
throw new UnsupportedProviderStateError();
|
|
361
561
|
}
|
|
362
562
|
async get<T>(_key: string): Promise<StateValue<T> | null> {
|
|
363
563
|
throw new UnsupportedProviderStateError();
|
|
364
564
|
}
|
|
365
|
-
async set<T>(
|
|
366
|
-
_key: string,
|
|
367
|
-
_value: T,
|
|
368
|
-
_options?: StateWriteOptions,
|
|
369
|
-
): Promise<StateValue<T>> {
|
|
565
|
+
async set<T>(_key: string, _value: T, _options?: StateWriteOptions): Promise<StateValue<T>> {
|
|
370
566
|
throw new UnsupportedProviderStateError();
|
|
371
567
|
}
|
|
372
568
|
async patch<T extends Record<string, unknown>>(
|
|
@@ -398,10 +594,7 @@ class UnsupportedProviderStateNamespace implements ProviderStateNamespace {
|
|
|
398
594
|
}
|
|
399
595
|
|
|
400
596
|
class UnsupportedProviderRuntimeState implements ProviderRuntimeState {
|
|
401
|
-
namespace(
|
|
402
|
-
_name: string,
|
|
403
|
-
_options: StateNamespaceOptions,
|
|
404
|
-
): ProviderStateNamespace {
|
|
597
|
+
namespace(_name: string, _options: StateNamespaceOptions): ProviderStateNamespace {
|
|
405
598
|
return new UnsupportedProviderStateNamespace();
|
|
406
599
|
}
|
|
407
600
|
}
|
|
@@ -412,6 +605,31 @@ class MemoryProviderStateNamespace implements ProviderStateNamespace {
|
|
|
412
605
|
|
|
413
606
|
constructor(private readonly options: StateNamespaceOptions) {}
|
|
414
607
|
|
|
608
|
+
private enforceValueSize(value: unknown): void {
|
|
609
|
+
const bytes = Buffer.byteLength(JSON.stringify(value), "utf8");
|
|
610
|
+
if (bytes > this.options.maxValueBytes) {
|
|
611
|
+
throw new UnsupportedProviderStateError(
|
|
612
|
+
`Provider runtime state value exceeds maxValueBytes (${bytes} > ${this.options.maxValueBytes})`,
|
|
613
|
+
);
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
private enforceWritePolicy(key: string, value: unknown, ttl: StateWriteOptions["ttl"]): void {
|
|
618
|
+
this.enforceValueSize(value);
|
|
619
|
+
const ttlMs = parseStateDurationMs(ttl ?? this.options.defaultTtl);
|
|
620
|
+
const maxTtlMs = parseStateDurationMs(this.options.maxTtl);
|
|
621
|
+
if (ttlMs > maxTtlMs) {
|
|
622
|
+
throw new UnsupportedProviderStateError(
|
|
623
|
+
`Provider runtime state ttl exceeds maxTtl (${ttlMs} > ${maxTtlMs})`,
|
|
624
|
+
);
|
|
625
|
+
}
|
|
626
|
+
if (!this.values.has(key) && this.values.size >= this.options.maxEntries) {
|
|
627
|
+
throw new UnsupportedProviderStateError(
|
|
628
|
+
`Provider runtime state namespace quota exceeded (${this.options.maxEntries + 1} > ${this.options.maxEntries})`,
|
|
629
|
+
);
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
|
|
415
633
|
private pruneExpired(nowMs = Date.now()): void {
|
|
416
634
|
for (const [key, row] of this.values.entries()) {
|
|
417
635
|
if (row.expiresAt && Date.parse(row.expiresAt) <= nowMs) {
|
|
@@ -420,10 +638,7 @@ class MemoryProviderStateNamespace implements ProviderStateNamespace {
|
|
|
420
638
|
}
|
|
421
639
|
}
|
|
422
640
|
|
|
423
|
-
async list<T>(_options?: {
|
|
424
|
-
limit?: number;
|
|
425
|
-
prefix?: string;
|
|
426
|
-
}): Promise<StateValue<T>[]> {
|
|
641
|
+
async list<T>(_options?: { limit?: number; prefix?: string }): Promise<StateValue<T>[]> {
|
|
427
642
|
this.pruneExpired();
|
|
428
643
|
const rows = Array.from(this.values.values()).filter((value) =>
|
|
429
644
|
_options?.prefix ? value.key.startsWith(_options.prefix) : true,
|
|
@@ -436,17 +651,12 @@ class MemoryProviderStateNamespace implements ProviderStateNamespace {
|
|
|
436
651
|
return this.values.get(key) ?? null;
|
|
437
652
|
}
|
|
438
653
|
|
|
439
|
-
async set<T>(
|
|
440
|
-
key: string,
|
|
441
|
-
value: T,
|
|
442
|
-
options?: StateWriteOptions,
|
|
443
|
-
): Promise<StateValue<T>> {
|
|
654
|
+
async set<T>(key: string, value: T, options?: StateWriteOptions): Promise<StateValue<T>> {
|
|
444
655
|
this.pruneExpired();
|
|
656
|
+
this.enforceWritePolicy(key, value, options?.ttl);
|
|
445
657
|
const now = new Date().toISOString();
|
|
446
658
|
const current = this.values.get(key);
|
|
447
|
-
const expiresAt = resolveMemoryStateExpiresAt(
|
|
448
|
-
options?.ttl ?? this.options.defaultTtl,
|
|
449
|
-
);
|
|
659
|
+
const expiresAt = resolveMemoryStateExpiresAt(options?.ttl ?? this.options.defaultTtl);
|
|
450
660
|
const row = {
|
|
451
661
|
key,
|
|
452
662
|
value,
|
|
@@ -470,14 +680,30 @@ class MemoryProviderStateNamespace implements ProviderStateNamespace {
|
|
|
470
680
|
}
|
|
471
681
|
|
|
472
682
|
async compareAndSet<T>(
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
683
|
+
key: string,
|
|
684
|
+
expectedVersion: number,
|
|
685
|
+
value: T,
|
|
686
|
+
options?: StateWriteOptions,
|
|
477
687
|
): Promise<StateCasResult<T>> {
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
)
|
|
688
|
+
this.pruneExpired();
|
|
689
|
+
const current = this.values.get(key) as StateValue<T> | undefined;
|
|
690
|
+
if ((current?.version ?? 0) !== expectedVersion) {
|
|
691
|
+
return { ok: false, current: current ?? null };
|
|
692
|
+
}
|
|
693
|
+
this.enforceWritePolicy(key, value, options?.ttl);
|
|
694
|
+
const now = new Date().toISOString();
|
|
695
|
+
const stored = {
|
|
696
|
+
key,
|
|
697
|
+
value,
|
|
698
|
+
version: expectedVersion + 1,
|
|
699
|
+
expiresAt: resolveMemoryStateExpiresAt(
|
|
700
|
+
options?.ttl ?? this.options.defaultTtl,
|
|
701
|
+
),
|
|
702
|
+
createdAt: current?.createdAt ?? now,
|
|
703
|
+
updatedAt: now,
|
|
704
|
+
} satisfies StateValue<T>;
|
|
705
|
+
this.values.set(key, stored);
|
|
706
|
+
return { ok: true, value: stored };
|
|
481
707
|
}
|
|
482
708
|
|
|
483
709
|
async delete(key: string): Promise<void> {
|
|
@@ -499,10 +725,7 @@ class MemoryProviderStateNamespace implements ProviderStateNamespace {
|
|
|
499
725
|
class MemoryProviderRuntimeState implements ProviderRuntimeState {
|
|
500
726
|
readonly namespaces = new Map<string, MemoryProviderStateNamespace>();
|
|
501
727
|
|
|
502
|
-
namespace(
|
|
503
|
-
name: string,
|
|
504
|
-
_options: StateNamespaceOptions,
|
|
505
|
-
): ProviderStateNamespace {
|
|
728
|
+
namespace(name: string, _options: StateNamespaceOptions): ProviderStateNamespace {
|
|
506
729
|
const existing = this.namespaces.get(name);
|
|
507
730
|
if (existing) return existing;
|
|
508
731
|
const created = new MemoryProviderStateNamespace(_options);
|
|
@@ -536,10 +759,7 @@ export function createRedisProviderRuntimeState(
|
|
|
536
759
|
}
|
|
537
760
|
|
|
538
761
|
export function createProviderRuntimeStateFromEnv(
|
|
539
|
-
options: {
|
|
540
|
-
readonly providerId?: string;
|
|
541
|
-
readonly allowMemoryFallback?: boolean;
|
|
542
|
-
} = {},
|
|
762
|
+
options: { readonly providerId?: string; readonly allowMemoryFallback?: boolean } = {},
|
|
543
763
|
): ProviderRuntimeState {
|
|
544
764
|
const redisUrl = providerStateRedisUrlFromEnv();
|
|
545
765
|
if (redisUrl) {
|