@apifuse/provider-sdk 2.2.0-beta.22 → 2.2.0-beta.24
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/CHANGELOG.md +8 -0
- package/bin/apifuse-dev.ts +4 -0
- package/bin/apifuse-pack-types.ts +234 -38
- package/bin/apifuse-perf.ts +15 -12
- package/bin/apifuse-record.ts +4 -0
- package/dist/config/loader.d.ts +8 -19
- package/dist/config/loader.js +28 -86
- package/dist/contract-types.d.ts +1 -0
- package/dist/contract.js +2 -0
- package/dist/define.d.ts +5 -1
- package/dist/define.js +79 -6
- package/dist/error-resolution.js +5 -0
- package/dist/index.d.ts +4 -2
- package/dist/index.js +2 -0
- package/dist/provider.d.ts +1 -1
- package/dist/runtime/auth-flow.d.ts +2 -1
- package/dist/runtime/auth-flow.js +4 -0
- package/dist/runtime/browser.js +78 -9
- package/dist/runtime/http.js +0 -1
- package/dist/runtime/instrumentation.js +26 -1
- package/dist/runtime/ocr.d.ts +29 -0
- package/dist/runtime/ocr.js +440 -0
- package/dist/runtime/resolver-vendors/bindings.d.ts +8 -0
- package/dist/runtime/resolver-vendors/bindings.js +15 -0
- package/dist/runtime/resolver-vendors/browser.d.ts +24 -0
- package/dist/runtime/resolver-vendors/browser.js +287 -0
- package/dist/runtime/resolver-vendors/types.d.ts +42 -0
- package/dist/runtime/resolver-vendors/types.js +57 -0
- package/dist/runtime/resolver.d.ts +39 -0
- package/dist/runtime/resolver.js +414 -0
- package/dist/runtime/state.d.ts +3 -0
- package/dist/runtime/state.js +245 -141
- package/dist/runtime/stealth.js +3 -6
- package/dist/runtime/stt.js +1 -12
- package/dist/runtime/timeout.d.ts +5 -0
- package/dist/runtime/timeout.js +12 -0
- package/dist/server/serve.d.ts +6 -1
- package/dist/server/serve.js +39 -8
- package/dist/testing/run.js +10 -0
- package/dist/types.d.ts +163 -4
- package/package.json +1 -1
- package/src/config/loader.ts +35 -111
- package/src/contract-types.ts +1 -0
- package/src/contract.ts +2 -0
- package/src/define.ts +121 -7
- package/src/error-resolution.ts +5 -0
- package/src/index.ts +45 -1
- package/src/provider.ts +1 -0
- package/src/runtime/auth-flow.ts +6 -0
- package/src/runtime/browser.ts +139 -19
- package/src/runtime/http.ts +0 -1
- package/src/runtime/instrumentation.ts +36 -2
- package/src/runtime/ocr.ts +523 -0
- package/src/runtime/resolver-vendors/bindings.ts +31 -0
- package/src/runtime/resolver-vendors/browser.ts +420 -0
- package/src/runtime/resolver-vendors/types.ts +113 -0
- package/src/runtime/resolver.ts +668 -0
- package/src/runtime/state.ts +323 -166
- package/src/runtime/stealth.ts +3 -6
- package/src/runtime/stt.ts +1 -19
- package/src/runtime/timeout.ts +18 -0
- package/src/server/serve.ts +80 -5
- package/src/testing/run.ts +15 -0
- package/src/types.ts +188 -4
package/dist/runtime/state.js
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
1
2
|
import { providerStateRedisUrlFromEnv } from "../config/loader.js";
|
|
2
3
|
import { ProviderError } from "../errors.js";
|
|
3
4
|
import { createProviderRedisClient, ensureRedisReady, withRedisTimeout, } from "./redis.js";
|
|
4
5
|
const DEFAULT_REDIS_TIMEOUT_MS = 250;
|
|
5
|
-
const REDIS_STATE_PREFIX = "apifuse:provider-state:
|
|
6
|
-
const
|
|
7
|
-
const
|
|
6
|
+
const REDIS_STATE_PREFIX = "apifuse:provider-state:v2";
|
|
7
|
+
const LEGACY_REDIS_STATE_PREFIX = "apifuse:provider-state:v1";
|
|
8
|
+
const PROVIDER_SCOPE_DISCRIMINATOR = "scope:provider";
|
|
9
|
+
const MISSING_CONNECTION_SCOPE_DISCRIMINATOR = "scope:connection:missing";
|
|
8
10
|
const SET_WITH_QUOTA_SCRIPT = `
|
|
9
11
|
local now = tonumber(ARGV[1])
|
|
10
12
|
local max_entries = tonumber(ARGV[2])
|
|
@@ -13,93 +15,169 @@ local index_ttl = tonumber(ARGV[4])
|
|
|
13
15
|
local envelope = ARGV[5]
|
|
14
16
|
|
|
15
17
|
redis.call("ZREMRANGEBYSCORE", KEYS[2], "-inf", now)
|
|
16
|
-
|
|
18
|
+
redis.call("ZREM", KEYS[2], KEYS[3])
|
|
17
19
|
local indexed = redis.call("ZSCORE", KEYS[2], KEYS[1])
|
|
18
|
-
if
|
|
20
|
+
if not indexed and redis.call("ZCARD", KEYS[2]) >= max_entries then
|
|
19
21
|
return {0, false}
|
|
20
22
|
end
|
|
21
23
|
|
|
22
24
|
redis.call("SET", KEYS[1], envelope, "PXAT", expires_at)
|
|
23
25
|
redis.call("ZADD", KEYS[2], expires_at, KEYS[1])
|
|
24
26
|
redis.call("PEXPIRE", KEYS[2], index_ttl)
|
|
27
|
+
if ARGV[6] == "1" then
|
|
28
|
+
redis.call("SET", KEYS[3], ARGV[7], "PX", index_ttl)
|
|
29
|
+
end
|
|
25
30
|
return {1, envelope}
|
|
26
31
|
`;
|
|
27
32
|
const COMPARE_AND_SET_WITH_QUOTA_SCRIPT = `
|
|
28
33
|
local current = redis.call("GET", KEYS[1])
|
|
34
|
+
local scoped_present = current and true or false
|
|
35
|
+
if current then
|
|
36
|
+
local ok, decoded = pcall(cjson.decode, current)
|
|
37
|
+
if ok and type(decoded) == "table" and decoded.deleted == true then
|
|
38
|
+
current = false
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
local now = tonumber(ARGV[2])
|
|
43
|
+
local max_entries = tonumber(ARGV[3])
|
|
44
|
+
local expires_at = tonumber(ARGV[4])
|
|
45
|
+
local index_ttl = tonumber(ARGV[5])
|
|
46
|
+
local envelope = ARGV[6]
|
|
47
|
+
local allow_legacy_claim = ARGV[7] == "1"
|
|
48
|
+
local legacy_tombstone = ARGV[8]
|
|
49
|
+
|
|
50
|
+
if not scoped_present then
|
|
51
|
+
local legacy = redis.call("GET", KEYS[3])
|
|
52
|
+
if legacy then
|
|
53
|
+
local ok, decoded = pcall(cjson.decode, legacy)
|
|
54
|
+
if ok and type(decoded) == "table" and decoded.deleted == true then
|
|
55
|
+
legacy = false
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
if legacy and not allow_legacy_claim then
|
|
59
|
+
return {-1, false}
|
|
60
|
+
end
|
|
61
|
+
if legacy then
|
|
62
|
+
local legacy_ttl = redis.call("PTTL", KEYS[3])
|
|
63
|
+
if legacy_ttl == -1 then
|
|
64
|
+
legacy_ttl = index_ttl
|
|
65
|
+
elseif legacy_ttl < 1 then
|
|
66
|
+
legacy_ttl = 1
|
|
67
|
+
end
|
|
68
|
+
if legacy_ttl > 0 then
|
|
69
|
+
redis.call("ZREMRANGEBYSCORE", KEYS[2], "-inf", now)
|
|
70
|
+
redis.call("ZREM", KEYS[2], KEYS[3])
|
|
71
|
+
local indexed = redis.call("ZSCORE", KEYS[2], KEYS[1])
|
|
72
|
+
if not indexed and redis.call("ZCARD", KEYS[2]) >= max_entries then
|
|
73
|
+
return {0, false}
|
|
74
|
+
end
|
|
75
|
+
redis.call("SET", KEYS[1], legacy, "PX", legacy_ttl)
|
|
76
|
+
redis.call("ZADD", KEYS[2], now + legacy_ttl, KEYS[1])
|
|
77
|
+
redis.call("PEXPIRE", KEYS[2], index_ttl)
|
|
78
|
+
redis.call("SET", KEYS[3], legacy_tombstone, "PX", index_ttl)
|
|
79
|
+
current = legacy
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
|
|
29
84
|
local current_version = 0
|
|
85
|
+
local current_decoded = false
|
|
30
86
|
if current then
|
|
31
87
|
local ok, decoded = pcall(cjson.decode, current)
|
|
32
88
|
if not ok or type(decoded) ~= "table" or type(decoded.version) ~= "number" then
|
|
33
89
|
return {-2, current}
|
|
34
90
|
end
|
|
35
91
|
current_version = decoded.version
|
|
92
|
+
current_decoded = decoded
|
|
36
93
|
end
|
|
37
94
|
if current_version ~= tonumber(ARGV[1]) then
|
|
38
95
|
return {-1, current or false}
|
|
39
96
|
end
|
|
40
97
|
|
|
41
|
-
local now = tonumber(ARGV[2])
|
|
42
|
-
local max_entries = tonumber(ARGV[3])
|
|
43
|
-
local expires_at = tonumber(ARGV[4])
|
|
44
|
-
local index_ttl = tonumber(ARGV[5])
|
|
45
|
-
local envelope = ARGV[6]
|
|
46
98
|
redis.call("ZREMRANGEBYSCORE", KEYS[2], "-inf", now)
|
|
47
|
-
|
|
99
|
+
redis.call("ZREM", KEYS[2], KEYS[3])
|
|
48
100
|
local indexed = redis.call("ZSCORE", KEYS[2], KEYS[1])
|
|
49
|
-
if
|
|
101
|
+
if not indexed and redis.call("ZCARD", KEYS[2]) >= max_entries then
|
|
50
102
|
return {0, false}
|
|
51
103
|
end
|
|
52
104
|
|
|
105
|
+
if current_decoded and type(current_decoded.createdAt) == "string" then
|
|
106
|
+
local next_decoded = cjson.decode(envelope)
|
|
107
|
+
next_decoded.createdAt = current_decoded.createdAt
|
|
108
|
+
envelope = cjson.encode(next_decoded)
|
|
109
|
+
end
|
|
53
110
|
redis.call("SET", KEYS[1], envelope, "PXAT", expires_at)
|
|
54
111
|
redis.call("ZADD", KEYS[2], expires_at, KEYS[1])
|
|
55
112
|
redis.call("PEXPIRE", KEYS[2], index_ttl)
|
|
113
|
+
if allow_legacy_claim then
|
|
114
|
+
redis.call("SET", KEYS[3], legacy_tombstone, "PX", index_ttl)
|
|
115
|
+
end
|
|
56
116
|
return {1, envelope}
|
|
57
117
|
`;
|
|
58
118
|
const DELETE_WITH_INDEX_SCRIPT = `
|
|
59
|
-
redis.call("
|
|
60
|
-
|
|
119
|
+
redis.call("SET", KEYS[1], ARGV[1], "PX", ARGV[2])
|
|
120
|
+
if ARGV[3] == "1" then
|
|
121
|
+
redis.call("SET", KEYS[3], ARGV[1], "PX", ARGV[2])
|
|
122
|
+
end
|
|
123
|
+
redis.call("ZREM", KEYS[2], KEYS[1], KEYS[3])
|
|
61
124
|
return 1
|
|
62
125
|
`;
|
|
63
|
-
//
|
|
64
|
-
//
|
|
65
|
-
//
|
|
66
|
-
//
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
local index_ttl = tonumber(ARGV[2])
|
|
73
|
-
local next_cursor = ARGV[3]
|
|
126
|
+
// A v1 key has no connection discriminator, so it cannot safely be exposed to
|
|
127
|
+
// every v2 scope. The first concrete connection (or explicit provider scope) to
|
|
128
|
+
// request a key atomically adopts that value and tombstones v1. The missing-
|
|
129
|
+
// connection sentinel never claims legacy state.
|
|
130
|
+
const CLAIM_LEGACY_SCRIPT = `
|
|
131
|
+
local scoped = redis.call("GET", KEYS[1])
|
|
132
|
+
if scoped then
|
|
133
|
+
return {1, scoped}
|
|
134
|
+
end
|
|
74
135
|
|
|
75
|
-
redis.call("
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
end
|
|
136
|
+
local legacy = redis.call("GET", KEYS[3])
|
|
137
|
+
if not legacy then
|
|
138
|
+
return {1, false}
|
|
139
|
+
end
|
|
140
|
+
local ok, decoded = pcall(cjson.decode, legacy)
|
|
141
|
+
if ok and type(decoded) == "table" and decoded.deleted == true then
|
|
142
|
+
return {1, false}
|
|
83
143
|
end
|
|
84
|
-
|
|
85
|
-
|
|
144
|
+
local legacy_ttl = redis.call("PTTL", KEYS[3])
|
|
145
|
+
if legacy_ttl == -1 then
|
|
146
|
+
legacy_ttl = tonumber(ARGV[3])
|
|
147
|
+
elseif legacy_ttl < 1 then
|
|
148
|
+
legacy_ttl = 1
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
local now = tonumber(ARGV[1])
|
|
152
|
+
local max_entries = tonumber(ARGV[2])
|
|
153
|
+
local index_ttl = tonumber(ARGV[3])
|
|
154
|
+
redis.call("ZREMRANGEBYSCORE", KEYS[2], "-inf", now)
|
|
155
|
+
redis.call("ZREM", KEYS[2], KEYS[3])
|
|
156
|
+
local indexed = redis.call("ZSCORE", KEYS[2], KEYS[1])
|
|
157
|
+
if not indexed and redis.call("ZCARD", KEYS[2]) >= max_entries then
|
|
158
|
+
return {0, false}
|
|
86
159
|
end
|
|
87
|
-
|
|
88
|
-
|
|
160
|
+
|
|
161
|
+
redis.call("SET", KEYS[1], legacy, "PX", legacy_ttl)
|
|
162
|
+
redis.call("ZADD", KEYS[2], now + legacy_ttl, KEYS[1])
|
|
163
|
+
redis.call("PEXPIRE", KEYS[2], index_ttl)
|
|
164
|
+
redis.call("SET", KEYS[3], ARGV[4], "PX", index_ttl)
|
|
165
|
+
return {1, legacy}
|
|
89
166
|
`;
|
|
90
167
|
const redisBackends = new Map();
|
|
91
|
-
function getRedisBackend(redisUrl) {
|
|
168
|
+
function getRedisBackend(redisUrl, injectedRedis) {
|
|
92
169
|
const existing = redisBackends.get(redisUrl);
|
|
93
170
|
if (existing)
|
|
94
171
|
return existing;
|
|
95
|
-
const redis =
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
172
|
+
const redis = injectedRedis ??
|
|
173
|
+
createProviderRedisClient({
|
|
174
|
+
redisUrl,
|
|
175
|
+
timeoutMs: DEFAULT_REDIS_TIMEOUT_MS,
|
|
176
|
+
onError: () => {
|
|
177
|
+
// Runtime state operations fail closed at their call sites. Avoid noisy
|
|
178
|
+
// unhandled Redis errors from background reconnect attempts.
|
|
179
|
+
},
|
|
180
|
+
});
|
|
103
181
|
const backend = { redis };
|
|
104
182
|
redisBackends.set(redisUrl, backend);
|
|
105
183
|
return backend;
|
|
@@ -120,18 +198,32 @@ async function requireRedisReady(redis) {
|
|
|
120
198
|
return;
|
|
121
199
|
throw new UnsupportedProviderStateError("Provider runtime state Redis is unavailable");
|
|
122
200
|
}
|
|
123
|
-
function
|
|
124
|
-
|
|
201
|
+
function connectionScopeDiscriminator(connectionId) {
|
|
202
|
+
if (connectionId === undefined)
|
|
203
|
+
return MISSING_CONNECTION_SCOPE_DISCRIMINATOR;
|
|
204
|
+
const digest = createHash("sha256").update(connectionId, "utf8").digest("hex");
|
|
205
|
+
return `scope:connection:sha256:${digest}`;
|
|
125
206
|
}
|
|
126
|
-
function
|
|
127
|
-
return `${
|
|
207
|
+
function providerStatePrefix(providerId, namespace, scopeDiscriminator) {
|
|
208
|
+
return `${REDIS_STATE_PREFIX}:${providerId ?? "default"}:${namespace}:${scopeDiscriminator}`;
|
|
128
209
|
}
|
|
129
|
-
function
|
|
130
|
-
|
|
131
|
-
return redisKey.startsWith(prefix) ? redisKey.slice(prefix.length) : redisKey;
|
|
210
|
+
function providerStateKey(providerId, namespace, scopeDiscriminator, key) {
|
|
211
|
+
return `${providerStatePrefix(providerId, namespace, scopeDiscriminator)}:${key}`;
|
|
132
212
|
}
|
|
133
|
-
function
|
|
134
|
-
return
|
|
213
|
+
function legacyProviderStatePrefix(providerId, namespace) {
|
|
214
|
+
return `${LEGACY_REDIS_STATE_PREFIX}:${providerId ?? "default"}:${namespace}`;
|
|
215
|
+
}
|
|
216
|
+
function legacyProviderStateKey(providerId, namespace, key) {
|
|
217
|
+
return `${legacyProviderStatePrefix(providerId, namespace)}:${key}`;
|
|
218
|
+
}
|
|
219
|
+
function publicStateKey(redisKey, prefixes) {
|
|
220
|
+
for (const prefix of prefixes) {
|
|
221
|
+
const prefixWithSeparator = `${prefix}:`;
|
|
222
|
+
if (redisKey.startsWith(prefixWithSeparator)) {
|
|
223
|
+
return redisKey.slice(prefixWithSeparator.length);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
return redisKey;
|
|
135
227
|
}
|
|
136
228
|
function parseStateDurationMs(ttl) {
|
|
137
229
|
const match = /^(\d+)(ms|s|m|h|d)$/.exec(ttl ?? "1h");
|
|
@@ -150,9 +242,6 @@ function parseStateDurationMs(ttl) {
|
|
|
150
242
|
: 86_400_000;
|
|
151
243
|
return Math.max(1, amount * multiplier);
|
|
152
244
|
}
|
|
153
|
-
function resolveExpiresAt(ttl) {
|
|
154
|
-
return new Date(Date.now() + parseStateDurationMs(ttl)).toISOString();
|
|
155
|
-
}
|
|
156
245
|
function envelopeFromJson(key, raw) {
|
|
157
246
|
if (!raw)
|
|
158
247
|
return null;
|
|
@@ -188,6 +277,20 @@ function envelopeFromJson(key, raw) {
|
|
|
188
277
|
updatedAt: record.updatedAt,
|
|
189
278
|
};
|
|
190
279
|
}
|
|
280
|
+
function isStateTombstone(raw) {
|
|
281
|
+
if (!raw)
|
|
282
|
+
return false;
|
|
283
|
+
try {
|
|
284
|
+
const parsed = JSON.parse(raw);
|
|
285
|
+
return (parsed !== null &&
|
|
286
|
+
typeof parsed === "object" &&
|
|
287
|
+
!Array.isArray(parsed) &&
|
|
288
|
+
parsed.deleted === true);
|
|
289
|
+
}
|
|
290
|
+
catch {
|
|
291
|
+
return false;
|
|
292
|
+
}
|
|
293
|
+
}
|
|
191
294
|
function redisEnvelope(value, version, createdAt, expiresAt) {
|
|
192
295
|
const updatedAt = new Date().toISOString();
|
|
193
296
|
return { value, version, expiresAt, createdAt, updatedAt };
|
|
@@ -197,56 +300,32 @@ class RedisProviderStateNamespace {
|
|
|
197
300
|
providerId;
|
|
198
301
|
namespaceName;
|
|
199
302
|
options;
|
|
200
|
-
|
|
303
|
+
scopeDiscriminator;
|
|
304
|
+
constructor(backend, providerId, namespaceName, options, scopeDiscriminator) {
|
|
201
305
|
this.backend = backend;
|
|
202
306
|
this.providerId = providerId;
|
|
203
307
|
this.namespaceName = namespaceName;
|
|
204
308
|
this.options = options;
|
|
309
|
+
this.scopeDiscriminator = scopeDiscriminator;
|
|
205
310
|
}
|
|
206
311
|
redisKey(key) {
|
|
207
|
-
return providerStateKey(this.providerId, this.namespaceName, key);
|
|
312
|
+
return providerStateKey(this.providerId, this.namespaceName, this.scopeDiscriminator, key);
|
|
313
|
+
}
|
|
314
|
+
legacyRedisKey(key) {
|
|
315
|
+
return legacyProviderStateKey(this.providerId, this.namespaceName, key);
|
|
316
|
+
}
|
|
317
|
+
statePrefix() {
|
|
318
|
+
return providerStatePrefix(this.providerId, this.namespaceName, this.scopeDiscriminator);
|
|
208
319
|
}
|
|
209
320
|
indexKey() {
|
|
210
321
|
// Keep bookkeeping outside the caller-owned keyspace. A provider may use
|
|
211
322
|
// any state key (including "__index"), so a suffix inside the namespace
|
|
212
323
|
// could turn the ZSET into a string and break every subsequent write.
|
|
213
|
-
const namespaceIdentity = Buffer.from(providerStatePrefix(this.providerId, this.namespaceName), "utf8").toString("base64url");
|
|
324
|
+
const namespaceIdentity = Buffer.from(providerStatePrefix(this.providerId, this.namespaceName, this.scopeDiscriminator), "utf8").toString("base64url");
|
|
214
325
|
return `${REDIS_STATE_PREFIX}:index:${namespaceIdentity}`;
|
|
215
326
|
}
|
|
216
|
-
|
|
217
|
-
return
|
|
218
|
-
}
|
|
219
|
-
legacyPrefix() {
|
|
220
|
-
return `${providerStatePrefix(this.providerId, this.namespaceName)}:`;
|
|
221
|
-
}
|
|
222
|
-
async backfillLegacyIndex() {
|
|
223
|
-
await requireRedisReady(this.backend.redis);
|
|
224
|
-
const cursorKey = this.legacyScanCursorKey();
|
|
225
|
-
let cursor = (await withRequiredRedis(() => this.backend.redis.get(cursorKey))) ?? "0";
|
|
226
|
-
const pattern = `${redisGlobLiteral(this.legacyPrefix())}*`;
|
|
227
|
-
const indexTtlMs = parseStateDurationMs(this.options.maxTtl);
|
|
228
|
-
for (let page = 0; page < LEGACY_INDEX_SCAN_MAX_PAGES; page += 1) {
|
|
229
|
-
const [nextCursor, keys] = await withRequiredRedis(() => this.backend.redis.scan(cursor, "MATCH", pattern, "COUNT", LEGACY_INDEX_SCAN_COUNT));
|
|
230
|
-
const rawValues = keys.length > 0
|
|
231
|
-
? await withRequiredRedis(() => this.backend.redis.mget(keys))
|
|
232
|
-
: [];
|
|
233
|
-
const now = Date.now();
|
|
234
|
-
const activeLegacyArgs = [];
|
|
235
|
-
for (const [index, raw] of rawValues.entries()) {
|
|
236
|
-
const key = keys[index];
|
|
237
|
-
if (!key || !raw)
|
|
238
|
-
continue;
|
|
239
|
-
const envelope = envelopeFromJson(publicStateKey(this.providerId, this.namespaceName, key), raw);
|
|
240
|
-
const expiresAtMs = envelope ? Date.parse(envelope.expiresAt) : Number.NaN;
|
|
241
|
-
if (!Number.isFinite(expiresAtMs) || expiresAtMs <= now)
|
|
242
|
-
continue;
|
|
243
|
-
activeLegacyArgs.push(key, raw, String(expiresAtMs));
|
|
244
|
-
}
|
|
245
|
-
await withRequiredRedis(() => this.backend.redis.eval(BACKFILL_LEGACY_INDEX_SCRIPT, 2, this.indexKey(), cursorKey, String(now), String(indexTtlMs), nextCursor, ...activeLegacyArgs));
|
|
246
|
-
cursor = nextCursor;
|
|
247
|
-
if (cursor === "0")
|
|
248
|
-
break;
|
|
249
|
-
}
|
|
327
|
+
canClaimLegacy() {
|
|
328
|
+
return this.scopeDiscriminator !== MISSING_CONNECTION_SCOPE_DISCRIMINATOR;
|
|
250
329
|
}
|
|
251
330
|
async indexedKeys(limit) {
|
|
252
331
|
await requireRedisReady(this.backend.redis);
|
|
@@ -256,6 +335,18 @@ class RedisProviderStateNamespace {
|
|
|
256
335
|
return await this.backend.redis.zrangebyscore(this.indexKey(), now + 1, "+inf", "LIMIT", 0, limit);
|
|
257
336
|
});
|
|
258
337
|
}
|
|
338
|
+
async readRaw(key) {
|
|
339
|
+
await requireRedisReady(this.backend.redis);
|
|
340
|
+
if (!this.canClaimLegacy()) {
|
|
341
|
+
return await withRequiredRedis(() => this.backend.redis.get(this.redisKey(key)));
|
|
342
|
+
}
|
|
343
|
+
const tombstone = JSON.stringify({ deleted: true });
|
|
344
|
+
const result = await withRequiredRedis(() => this.backend.redis.eval(CLAIM_LEGACY_SCRIPT, 3, this.redisKey(key), this.indexKey(), this.legacyRedisKey(key), String(Date.now()), String(this.options.maxEntries), String(parseStateDurationMs(this.options.maxTtl)), tombstone));
|
|
345
|
+
if (Array.isArray(result) && Number(result[0]) === 0) {
|
|
346
|
+
throw this.quotaExceeded();
|
|
347
|
+
}
|
|
348
|
+
return Array.isArray(result) && typeof result[1] === "string" ? result[1] : null;
|
|
349
|
+
}
|
|
259
350
|
enforceValueSize(value) {
|
|
260
351
|
const bytes = Buffer.byteLength(JSON.stringify(value), "utf8");
|
|
261
352
|
if (bytes > this.options.maxValueBytes) {
|
|
@@ -282,38 +373,40 @@ class RedisProviderStateNamespace {
|
|
|
282
373
|
const requestedLimit = Math.max(0, options?.limit ?? this.options.maxEntries);
|
|
283
374
|
if (requestedLimit === 0)
|
|
284
375
|
return [];
|
|
285
|
-
await this.
|
|
286
|
-
|
|
287
|
-
const publicKey = publicStateKey(this.providerId, this.namespaceName, key);
|
|
288
|
-
return options?.prefix ? publicKey.startsWith(options.prefix) : true;
|
|
289
|
-
});
|
|
290
|
-
const limited = keys.slice(0, requestedLimit);
|
|
291
|
-
if (limited.length === 0)
|
|
376
|
+
const keys = await this.indexedKeys(this.options.maxEntries);
|
|
377
|
+
if (keys.length === 0)
|
|
292
378
|
return [];
|
|
293
|
-
const values = await withRequiredRedis(() => this.backend.redis.mget(
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
379
|
+
const values = await withRequiredRedis(() => this.backend.redis.mget(keys));
|
|
380
|
+
const rows = [];
|
|
381
|
+
for (const [index, raw] of values.entries()) {
|
|
382
|
+
const redisKey = keys[index];
|
|
383
|
+
if (!redisKey)
|
|
384
|
+
continue;
|
|
385
|
+
const publicKey = publicStateKey(redisKey, [this.statePrefix()]);
|
|
386
|
+
if (options?.prefix && !publicKey.startsWith(options.prefix))
|
|
387
|
+
continue;
|
|
388
|
+
const value = envelopeFromJson(publicKey, raw);
|
|
389
|
+
if (!value)
|
|
390
|
+
continue;
|
|
391
|
+
rows.push(value);
|
|
392
|
+
}
|
|
393
|
+
return rows.slice(0, requestedLimit);
|
|
301
394
|
}
|
|
302
395
|
async get(key) {
|
|
303
|
-
await
|
|
304
|
-
|
|
396
|
+
const raw = await this.readRaw(key);
|
|
397
|
+
if (isStateTombstone(raw))
|
|
398
|
+
return null;
|
|
305
399
|
return envelopeFromJson(key, raw);
|
|
306
400
|
}
|
|
307
401
|
async set(key, value, options) {
|
|
308
402
|
this.enforceValueSize(value);
|
|
309
|
-
await this.backfillLegacyIndex();
|
|
310
403
|
const current = await this.get(key);
|
|
311
404
|
const createdAt = current?.createdAt ?? new Date().toISOString();
|
|
312
405
|
const version = (current?.version ?? 0) + 1;
|
|
313
406
|
const timing = this.writeTiming(options?.ttl);
|
|
314
407
|
const envelope = redisEnvelope(value, version, createdAt, timing.expiresAt);
|
|
315
408
|
await requireRedisReady(this.backend.redis);
|
|
316
|
-
const result = await withRequiredRedis(() => this.backend.redis.eval(SET_WITH_QUOTA_SCRIPT,
|
|
409
|
+
const result = await withRequiredRedis(() => this.backend.redis.eval(SET_WITH_QUOTA_SCRIPT, 3, this.redisKey(key), this.indexKey(), this.legacyRedisKey(key), String(Date.now()), String(this.options.maxEntries), String(timing.expiresAtMs), String(timing.indexTtlMs), JSON.stringify(envelope), this.canClaimLegacy() ? "1" : "0", JSON.stringify({ deleted: true })));
|
|
317
410
|
if (!Array.isArray(result) || Number(result[0]) !== 1) {
|
|
318
411
|
throw this.quotaExceeded();
|
|
319
412
|
}
|
|
@@ -334,16 +427,11 @@ class RedisProviderStateNamespace {
|
|
|
334
427
|
}
|
|
335
428
|
async compareAndSet(key, expectedVersion, value, options) {
|
|
336
429
|
this.enforceValueSize(value);
|
|
337
|
-
|
|
338
|
-
const current = await this.get(key);
|
|
339
|
-
if ((current?.version ?? 0) !== expectedVersion) {
|
|
340
|
-
return { ok: false, current };
|
|
341
|
-
}
|
|
342
|
-
const createdAt = current?.createdAt ?? new Date().toISOString();
|
|
430
|
+
const createdAt = new Date().toISOString();
|
|
343
431
|
const timing = this.writeTiming(options?.ttl);
|
|
344
432
|
const envelope = redisEnvelope(value, expectedVersion + 1, createdAt, timing.expiresAt);
|
|
345
433
|
await requireRedisReady(this.backend.redis);
|
|
346
|
-
const result = await withRequiredRedis(() => this.backend.redis.eval(COMPARE_AND_SET_WITH_QUOTA_SCRIPT,
|
|
434
|
+
const result = await withRequiredRedis(() => this.backend.redis.eval(COMPARE_AND_SET_WITH_QUOTA_SCRIPT, 3, this.redisKey(key), this.indexKey(), this.legacyRedisKey(key), String(expectedVersion), String(Date.now()), String(this.options.maxEntries), String(timing.expiresAtMs), String(timing.indexTtlMs), JSON.stringify(envelope), this.canClaimLegacy() ? "1" : "0", JSON.stringify({ deleted: true })));
|
|
347
435
|
if (Array.isArray(result) && Number(result[0]) === 0) {
|
|
348
436
|
throw this.quotaExceeded();
|
|
349
437
|
}
|
|
@@ -351,21 +439,17 @@ class RedisProviderStateNamespace {
|
|
|
351
439
|
const rawCurrent = Array.isArray(result) && typeof result[1] === "string" ? result[1] : null;
|
|
352
440
|
return { ok: false, current: envelopeFromJson(key, rawCurrent) };
|
|
353
441
|
}
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
expiresAt: timing.expiresAt,
|
|
361
|
-
createdAt,
|
|
362
|
-
updatedAt: envelope.updatedAt,
|
|
363
|
-
},
|
|
364
|
-
};
|
|
442
|
+
const rawWritten = typeof result[1] === "string" ? result[1] : null;
|
|
443
|
+
const written = envelopeFromJson(key, rawWritten);
|
|
444
|
+
if (!written) {
|
|
445
|
+
throw new UnsupportedProviderStateError("Provider runtime state CAS returned an invalid value");
|
|
446
|
+
}
|
|
447
|
+
return { ok: true, value: written };
|
|
365
448
|
}
|
|
366
449
|
async delete(key) {
|
|
367
450
|
await requireRedisReady(this.backend.redis);
|
|
368
|
-
|
|
451
|
+
const tombstone = JSON.stringify({ deleted: true });
|
|
452
|
+
await withRequiredRedis(() => this.backend.redis.eval(DELETE_WITH_INDEX_SCRIPT, 3, this.redisKey(key), this.indexKey(), this.legacyRedisKey(key), tombstone, String(parseStateDurationMs(this.options.maxTtl)), this.canClaimLegacy() ? "1" : "0"));
|
|
369
453
|
}
|
|
370
454
|
async increment(key, field, delta = 1, options) {
|
|
371
455
|
const current = (await this.get(key))?.value ?? {};
|
|
@@ -376,12 +460,19 @@ class RedisProviderStateNamespace {
|
|
|
376
460
|
class RedisProviderRuntimeState {
|
|
377
461
|
backend;
|
|
378
462
|
providerId;
|
|
379
|
-
|
|
380
|
-
|
|
463
|
+
redisUrl;
|
|
464
|
+
scopeDiscriminator;
|
|
465
|
+
constructor(options, scopeDiscriminator = MISSING_CONNECTION_SCOPE_DISCRIMINATOR) {
|
|
466
|
+
this.backend = getRedisBackend(options.redisUrl, options.__redisClient);
|
|
381
467
|
this.providerId = options.providerId;
|
|
468
|
+
this.redisUrl = options.redisUrl;
|
|
469
|
+
this.scopeDiscriminator = scopeDiscriminator;
|
|
470
|
+
}
|
|
471
|
+
forConnection(connectionId) {
|
|
472
|
+
return new RedisProviderRuntimeState({ redisUrl: this.redisUrl, providerId: this.providerId }, connectionScopeDiscriminator(connectionId));
|
|
382
473
|
}
|
|
383
474
|
namespace(name, options) {
|
|
384
|
-
return new RedisProviderStateNamespace(this.backend, this.providerId, name, options);
|
|
475
|
+
return new RedisProviderStateNamespace(this.backend, this.providerId, name, options, options.scope === "provider" ? PROVIDER_SCOPE_DISCRIMINATOR : this.scopeDiscriminator);
|
|
385
476
|
}
|
|
386
477
|
}
|
|
387
478
|
export class UnsupportedProviderStateError extends ProviderError {
|
|
@@ -414,6 +505,9 @@ class UnsupportedProviderStateNamespace {
|
|
|
414
505
|
}
|
|
415
506
|
}
|
|
416
507
|
class UnsupportedProviderRuntimeState {
|
|
508
|
+
forConnection(_connectionId) {
|
|
509
|
+
return new UnsupportedProviderRuntimeState();
|
|
510
|
+
}
|
|
417
511
|
namespace(_name, _options) {
|
|
418
512
|
return new UnsupportedProviderStateNamespace();
|
|
419
513
|
}
|
|
@@ -505,13 +599,23 @@ class MemoryProviderStateNamespace {
|
|
|
505
599
|
}
|
|
506
600
|
}
|
|
507
601
|
class MemoryProviderRuntimeState {
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
602
|
+
backend;
|
|
603
|
+
scopeDiscriminator;
|
|
604
|
+
constructor(backend = { namespaces: new Map() }, scopeDiscriminator = MISSING_CONNECTION_SCOPE_DISCRIMINATOR) {
|
|
605
|
+
this.backend = backend;
|
|
606
|
+
this.scopeDiscriminator = scopeDiscriminator;
|
|
607
|
+
}
|
|
608
|
+
forConnection(connectionId) {
|
|
609
|
+
return new MemoryProviderRuntimeState(this.backend, connectionScopeDiscriminator(connectionId));
|
|
610
|
+
}
|
|
611
|
+
namespace(name, options) {
|
|
612
|
+
const scopeDiscriminator = options.scope === "provider" ? PROVIDER_SCOPE_DISCRIMINATOR : this.scopeDiscriminator;
|
|
613
|
+
const namespaceIdentity = `${scopeDiscriminator}\0${name}`;
|
|
614
|
+
const existing = this.backend.namespaces.get(namespaceIdentity);
|
|
511
615
|
if (existing)
|
|
512
616
|
return existing;
|
|
513
|
-
const created = new MemoryProviderStateNamespace(
|
|
514
|
-
this.namespaces.set(
|
|
617
|
+
const created = new MemoryProviderStateNamespace(options);
|
|
618
|
+
this.backend.namespaces.set(namespaceIdentity, created);
|
|
515
619
|
return created;
|
|
516
620
|
}
|
|
517
621
|
}
|
package/dist/runtime/stealth.js
CHANGED
|
@@ -596,7 +596,6 @@ function createSessionFetcher(baseUrl, defaultProfile, clientOptions) {
|
|
|
596
596
|
const resolvedProxy = await resolveProxyConfigAsync({
|
|
597
597
|
proxy: options?.proxy ?? clientOptions.proxy,
|
|
598
598
|
upstream: clientOptions.upstream,
|
|
599
|
-
apifuseConfig: clientOptions.apifuseConfig,
|
|
600
599
|
affinityKey: clientOptions.affinityKey,
|
|
601
600
|
proxyAttempt: computeProxyAttemptIndex({
|
|
602
601
|
baseProxyAttempt: clientOptions.proxyAttempt,
|
|
@@ -662,11 +661,9 @@ function createSessionFetcher(baseUrl, defaultProfile, clientOptions) {
|
|
|
662
661
|
// A registry vendor chain (smartproxy/nodemaven) is the only policy whose
|
|
663
662
|
// successive attempts resolve a *different* endpoint, so it is the only one
|
|
664
663
|
// that may widen the attempt cap to the pool span, de-duplicate endpoints,
|
|
665
|
-
// and drive allocator stale-pool refresh.
|
|
666
|
-
//
|
|
667
|
-
//
|
|
668
|
-
// retry:false and unsafe-method controls. Static policies therefore follow
|
|
669
|
-
// the ordinary transport-retry budget instead.
|
|
664
|
+
// and drive allocator stale-pool refresh. Deprecated custom/decodo policies
|
|
665
|
+
// have no managed endpoint to rotate or refresh, so they follow the ordinary
|
|
666
|
+
// transport-retry budget instead.
|
|
670
667
|
const rotatesRegistryChain = usesPolicyAllocator && policyResolvesRegistryVendorChain(policyProxy);
|
|
671
668
|
const maxAttempts = rotatesRegistryChain ? policyProxyAttemptCap : retryAttemptCap;
|
|
672
669
|
const dedupeAllocatorEndpoints = rotatesRegistryChain;
|
package/dist/runtime/stt.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { ProviderError, TransportError, ValidationError } from "../errors.js";
|
|
2
|
+
import { createTimeoutController, isTimeoutLikeError } from "./timeout.js";
|
|
2
3
|
export const APIFUSE__STT__BACKEND_ENV = "APIFUSE__STT__BACKEND";
|
|
3
4
|
export const APIFUSE__STT__MODEL_ENV = "APIFUSE__STT__MODEL";
|
|
4
5
|
export const CLOUDFLARE_ACCOUNT_ID_ENV = "APIFUSE__CLOUDFLARE__ACCOUNT_ID";
|
|
@@ -112,12 +113,6 @@ function warnOrThrowUnsupportedOption(request, message) {
|
|
|
112
113
|
function normalizeCloudflareLanguage(language) {
|
|
113
114
|
return language?.split("-")[0]?.toLowerCase();
|
|
114
115
|
}
|
|
115
|
-
function isTimeoutLikeError(error) {
|
|
116
|
-
return (error instanceof Error &&
|
|
117
|
-
(error.name === "AbortError" ||
|
|
118
|
-
error.name === "TimeoutError" ||
|
|
119
|
-
/\b(timed out|timeout|deadline exceeded)\b/i.test(error.message)));
|
|
120
|
-
}
|
|
121
116
|
function toSttTransportError(error) {
|
|
122
117
|
if (error instanceof TransportError)
|
|
123
118
|
return error;
|
|
@@ -134,12 +129,6 @@ function toSttTransportError(error) {
|
|
|
134
129
|
cause: error instanceof Error ? error : undefined,
|
|
135
130
|
});
|
|
136
131
|
}
|
|
137
|
-
function createTimeoutController(signalTimeoutMs) {
|
|
138
|
-
const controller = new AbortController();
|
|
139
|
-
const timeout = setTimeout(() => controller.abort(), signalTimeoutMs);
|
|
140
|
-
timeout.unref?.();
|
|
141
|
-
return { controller, clear: () => clearTimeout(timeout) };
|
|
142
|
-
}
|
|
143
132
|
function toCloudflareInput(request) {
|
|
144
133
|
const prompt = resolveSttPrompt(request);
|
|
145
134
|
const input = {
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export function isTimeoutLikeError(error) {
|
|
2
|
+
return (error instanceof Error &&
|
|
3
|
+
(error.name === "AbortError" ||
|
|
4
|
+
error.name === "TimeoutError" ||
|
|
5
|
+
/\b(timed out|timeout|deadline exceeded)\b/i.test(error.message)));
|
|
6
|
+
}
|
|
7
|
+
export function createTimeoutController(signalTimeoutMs) {
|
|
8
|
+
const controller = new AbortController();
|
|
9
|
+
const timeout = setTimeout(() => controller.abort(), signalTimeoutMs);
|
|
10
|
+
timeout.unref?.();
|
|
11
|
+
return { controller, clear: () => clearTimeout(timeout) };
|
|
12
|
+
}
|