@vivero/stoma 0.1.0-rc.10
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 +196 -0
- package/LICENSE +21 -0
- package/README.md +325 -0
- package/dist/adapters/bun.d.ts +9 -0
- package/dist/adapters/bun.js +8 -0
- package/dist/adapters/bun.js.map +1 -0
- package/dist/adapters/cloudflare.d.ts +49 -0
- package/dist/adapters/cloudflare.js +85 -0
- package/dist/adapters/cloudflare.js.map +1 -0
- package/dist/adapters/deno.d.ts +9 -0
- package/dist/adapters/deno.js +8 -0
- package/dist/adapters/deno.js.map +1 -0
- package/dist/adapters/durable-object.d.ts +63 -0
- package/dist/adapters/durable-object.js +46 -0
- package/dist/adapters/durable-object.js.map +1 -0
- package/dist/adapters/index.d.ts +13 -0
- package/dist/adapters/index.js +53 -0
- package/dist/adapters/index.js.map +1 -0
- package/dist/adapters/memory.d.ts +9 -0
- package/dist/adapters/memory.js +14 -0
- package/dist/adapters/memory.js.map +1 -0
- package/dist/adapters/node.d.ts +9 -0
- package/dist/adapters/node.js +8 -0
- package/dist/adapters/node.js.map +1 -0
- package/dist/adapters/postgres.d.ts +109 -0
- package/dist/adapters/postgres.js +242 -0
- package/dist/adapters/postgres.js.map +1 -0
- package/dist/adapters/redis.d.ts +116 -0
- package/dist/adapters/redis.js +194 -0
- package/dist/adapters/redis.js.map +1 -0
- package/dist/adapters/testing.d.ts +32 -0
- package/dist/adapters/testing.js +33 -0
- package/dist/adapters/testing.js.map +1 -0
- package/dist/adapters/types.d.ts +4 -0
- package/dist/adapters/types.js +1 -0
- package/dist/adapters/types.js.map +1 -0
- package/dist/config/index.d.ts +11 -0
- package/dist/config/index.js +21 -0
- package/dist/config/index.js.map +1 -0
- package/dist/config/merge.d.ts +48 -0
- package/dist/config/merge.js +83 -0
- package/dist/config/merge.js.map +1 -0
- package/dist/config/schema.d.ts +254 -0
- package/dist/config/schema.js +109 -0
- package/dist/config/schema.js.map +1 -0
- package/dist/core/errors.d.ts +66 -0
- package/dist/core/errors.js +47 -0
- package/dist/core/errors.js.map +1 -0
- package/dist/core/gateway.d.ts +44 -0
- package/dist/core/gateway.js +400 -0
- package/dist/core/gateway.js.map +1 -0
- package/dist/core/health.d.ts +78 -0
- package/dist/core/health.js +65 -0
- package/dist/core/health.js.map +1 -0
- package/dist/core/pipeline.d.ts +62 -0
- package/dist/core/pipeline.js +214 -0
- package/dist/core/pipeline.js.map +1 -0
- package/dist/core/protocol.d.ts +4 -0
- package/dist/core/protocol.js +1 -0
- package/dist/core/protocol.js.map +1 -0
- package/dist/core/scope.d.ts +67 -0
- package/dist/core/scope.js +44 -0
- package/dist/core/scope.js.map +1 -0
- package/dist/core/types.d.ts +252 -0
- package/dist/core/types.js +1 -0
- package/dist/core/types.js.map +1 -0
- package/dist/index.d.ts +57 -0
- package/dist/index.js +158 -0
- package/dist/index.js.map +1 -0
- package/dist/observability/admin.d.ts +32 -0
- package/dist/observability/admin.js +85 -0
- package/dist/observability/admin.js.map +1 -0
- package/dist/observability/metrics.d.ts +78 -0
- package/dist/observability/metrics.js +107 -0
- package/dist/observability/metrics.js.map +1 -0
- package/dist/observability/tracing.d.ts +149 -0
- package/dist/observability/tracing.js +191 -0
- package/dist/observability/tracing.js.map +1 -0
- package/dist/policies/auth/api-key-auth.d.ts +64 -0
- package/dist/policies/auth/api-key-auth.js +93 -0
- package/dist/policies/auth/api-key-auth.js.map +1 -0
- package/dist/policies/auth/basic-auth.d.ts +33 -0
- package/dist/policies/auth/basic-auth.js +96 -0
- package/dist/policies/auth/basic-auth.js.map +1 -0
- package/dist/policies/auth/crypto.d.ts +29 -0
- package/dist/policies/auth/crypto.js +100 -0
- package/dist/policies/auth/crypto.js.map +1 -0
- package/dist/policies/auth/generate-http-signature.d.ts +30 -0
- package/dist/policies/auth/generate-http-signature.js +79 -0
- package/dist/policies/auth/generate-http-signature.js.map +1 -0
- package/dist/policies/auth/generate-jwt.d.ts +44 -0
- package/dist/policies/auth/generate-jwt.js +99 -0
- package/dist/policies/auth/generate-jwt.js.map +1 -0
- package/dist/policies/auth/http-signature-base.d.ts +55 -0
- package/dist/policies/auth/http-signature-base.js +140 -0
- package/dist/policies/auth/http-signature-base.js.map +1 -0
- package/dist/policies/auth/jws.d.ts +46 -0
- package/dist/policies/auth/jws.js +317 -0
- package/dist/policies/auth/jws.js.map +1 -0
- package/dist/policies/auth/jwt-auth.d.ts +64 -0
- package/dist/policies/auth/jwt-auth.js +266 -0
- package/dist/policies/auth/jwt-auth.js.map +1 -0
- package/dist/policies/auth/oauth2.d.ts +38 -0
- package/dist/policies/auth/oauth2.js +254 -0
- package/dist/policies/auth/oauth2.js.map +1 -0
- package/dist/policies/auth/rbac.d.ts +30 -0
- package/dist/policies/auth/rbac.js +115 -0
- package/dist/policies/auth/rbac.js.map +1 -0
- package/dist/policies/auth/verify-http-signature.d.ts +30 -0
- package/dist/policies/auth/verify-http-signature.js +147 -0
- package/dist/policies/auth/verify-http-signature.js.map +1 -0
- package/dist/policies/index.d.ts +51 -0
- package/dist/policies/index.js +109 -0
- package/dist/policies/index.js.map +1 -0
- package/dist/policies/mock.d.ts +60 -0
- package/dist/policies/mock.js +29 -0
- package/dist/policies/mock.js.map +1 -0
- package/dist/policies/observability/assign-metrics.d.ts +37 -0
- package/dist/policies/observability/assign-metrics.js +29 -0
- package/dist/policies/observability/assign-metrics.js.map +1 -0
- package/dist/policies/observability/metrics-reporter.d.ts +25 -0
- package/dist/policies/observability/metrics-reporter.js +62 -0
- package/dist/policies/observability/metrics-reporter.js.map +1 -0
- package/dist/policies/observability/request-log.d.ts +135 -0
- package/dist/policies/observability/request-log.js +134 -0
- package/dist/policies/observability/request-log.js.map +1 -0
- package/dist/policies/observability/server-timing.d.ts +35 -0
- package/dist/policies/observability/server-timing.js +89 -0
- package/dist/policies/observability/server-timing.js.map +1 -0
- package/dist/policies/proxy.d.ts +59 -0
- package/dist/policies/proxy.js +47 -0
- package/dist/policies/proxy.js.map +1 -0
- package/dist/policies/resilience/circuit-breaker.d.ts +4 -0
- package/dist/policies/resilience/circuit-breaker.js +280 -0
- package/dist/policies/resilience/circuit-breaker.js.map +1 -0
- package/dist/policies/resilience/latency-injection.d.ts +35 -0
- package/dist/policies/resilience/latency-injection.js +26 -0
- package/dist/policies/resilience/latency-injection.js.map +1 -0
- package/dist/policies/resilience/retry.d.ts +71 -0
- package/dist/policies/resilience/retry.js +79 -0
- package/dist/policies/resilience/retry.js.map +1 -0
- package/dist/policies/resilience/timeout.d.ts +32 -0
- package/dist/policies/resilience/timeout.js +46 -0
- package/dist/policies/resilience/timeout.js.map +1 -0
- package/dist/policies/sdk/define-policy.d.ts +176 -0
- package/dist/policies/sdk/define-policy.js +42 -0
- package/dist/policies/sdk/define-policy.js.map +1 -0
- package/dist/policies/sdk/helpers.d.ts +132 -0
- package/dist/policies/sdk/helpers.js +87 -0
- package/dist/policies/sdk/helpers.js.map +1 -0
- package/dist/policies/sdk/index.d.ts +10 -0
- package/dist/policies/sdk/index.js +35 -0
- package/dist/policies/sdk/index.js.map +1 -0
- package/dist/policies/sdk/priority.d.ts +44 -0
- package/dist/policies/sdk/priority.js +36 -0
- package/dist/policies/sdk/priority.js.map +1 -0
- package/dist/policies/sdk/testing.d.ts +53 -0
- package/dist/policies/sdk/testing.js +41 -0
- package/dist/policies/sdk/testing.js.map +1 -0
- package/dist/policies/sdk/trace.d.ts +73 -0
- package/dist/policies/sdk/trace.js +25 -0
- package/dist/policies/sdk/trace.js.map +1 -0
- package/dist/policies/traffic/cache.d.ts +4 -0
- package/dist/policies/traffic/cache.js +224 -0
- package/dist/policies/traffic/cache.js.map +1 -0
- package/dist/policies/traffic/dynamic-routing.d.ts +54 -0
- package/dist/policies/traffic/dynamic-routing.js +36 -0
- package/dist/policies/traffic/dynamic-routing.js.map +1 -0
- package/dist/policies/traffic/geo-ip-filter.d.ts +37 -0
- package/dist/policies/traffic/geo-ip-filter.js +74 -0
- package/dist/policies/traffic/geo-ip-filter.js.map +1 -0
- package/dist/policies/traffic/http-callout.d.ts +59 -0
- package/dist/policies/traffic/http-callout.js +69 -0
- package/dist/policies/traffic/http-callout.js.map +1 -0
- package/dist/policies/traffic/interrupt.d.ts +46 -0
- package/dist/policies/traffic/interrupt.js +38 -0
- package/dist/policies/traffic/interrupt.js.map +1 -0
- package/dist/policies/traffic/ip-filter.d.ts +47 -0
- package/dist/policies/traffic/ip-filter.js +57 -0
- package/dist/policies/traffic/ip-filter.js.map +1 -0
- package/dist/policies/traffic/json-threat-protection.d.ts +51 -0
- package/dist/policies/traffic/json-threat-protection.js +173 -0
- package/dist/policies/traffic/json-threat-protection.js.map +1 -0
- package/dist/policies/traffic/rate-limit.d.ts +4 -0
- package/dist/policies/traffic/rate-limit.js +145 -0
- package/dist/policies/traffic/rate-limit.js.map +1 -0
- package/dist/policies/traffic/regex-threat-protection.d.ts +54 -0
- package/dist/policies/traffic/regex-threat-protection.js +109 -0
- package/dist/policies/traffic/regex-threat-protection.js.map +1 -0
- package/dist/policies/traffic/request-limit.d.ts +27 -0
- package/dist/policies/traffic/request-limit.js +41 -0
- package/dist/policies/traffic/request-limit.js.map +1 -0
- package/dist/policies/traffic/resource-filter.d.ts +38 -0
- package/dist/policies/traffic/resource-filter.js +184 -0
- package/dist/policies/traffic/resource-filter.js.map +1 -0
- package/dist/policies/traffic/ssl-enforce.d.ts +27 -0
- package/dist/policies/traffic/ssl-enforce.js +38 -0
- package/dist/policies/traffic/ssl-enforce.js.map +1 -0
- package/dist/policies/traffic/traffic-shadow.d.ts +40 -0
- package/dist/policies/traffic/traffic-shadow.js +87 -0
- package/dist/policies/traffic/traffic-shadow.js.map +1 -0
- package/dist/policies/transform/assign-attributes.d.ts +33 -0
- package/dist/policies/transform/assign-attributes.js +38 -0
- package/dist/policies/transform/assign-attributes.js.map +1 -0
- package/dist/policies/transform/assign-content.d.ts +40 -0
- package/dist/policies/transform/assign-content.js +185 -0
- package/dist/policies/transform/assign-content.js.map +1 -0
- package/dist/policies/transform/cors.d.ts +57 -0
- package/dist/policies/transform/cors.js +23 -0
- package/dist/policies/transform/cors.js.map +1 -0
- package/dist/policies/transform/json-validation.d.ts +50 -0
- package/dist/policies/transform/json-validation.js +125 -0
- package/dist/policies/transform/json-validation.js.map +1 -0
- package/dist/policies/transform/override-method.d.ts +33 -0
- package/dist/policies/transform/override-method.js +48 -0
- package/dist/policies/transform/override-method.js.map +1 -0
- package/dist/policies/transform/request-validation.d.ts +59 -0
- package/dist/policies/transform/request-validation.js +121 -0
- package/dist/policies/transform/request-validation.js.map +1 -0
- package/dist/policies/transform/transform.d.ts +75 -0
- package/dist/policies/transform/transform.js +116 -0
- package/dist/policies/transform/transform.js.map +1 -0
- package/dist/policies/types.d.ts +4 -0
- package/dist/policies/types.js +1 -0
- package/dist/policies/types.js.map +1 -0
- package/dist/protocol-2fD3DJrL.d.ts +725 -0
- package/dist/utils/cidr.d.ts +58 -0
- package/dist/utils/cidr.js +107 -0
- package/dist/utils/cidr.js.map +1 -0
- package/dist/utils/debug.d.ts +1 -0
- package/dist/utils/debug.js +13 -0
- package/dist/utils/debug.js.map +1 -0
- package/dist/utils/headers.d.ts +68 -0
- package/dist/utils/headers.js +25 -0
- package/dist/utils/headers.js.map +1 -0
- package/dist/utils/ip.d.ts +64 -0
- package/dist/utils/ip.js +29 -0
- package/dist/utils/ip.js.map +1 -0
- package/dist/utils/redact.d.ts +30 -0
- package/dist/utils/redact.js +52 -0
- package/dist/utils/redact.js.map +1 -0
- package/dist/utils/request-id.d.ts +11 -0
- package/dist/utils/request-id.js +7 -0
- package/dist/utils/request-id.js.map +1 -0
- package/dist/utils/timing-safe.d.ts +31 -0
- package/dist/utils/timing-safe.js +17 -0
- package/dist/utils/timing-safe.js.map +1 -0
- package/dist/utils/timing.d.ts +27 -0
- package/dist/utils/timing.js +12 -0
- package/dist/utils/timing.js.map +1 -0
- package/dist/utils/trace-context.d.ts +51 -0
- package/dist/utils/trace-context.js +37 -0
- package/dist/utils/trace-context.js.map +1 -0
- package/package.json +213 -0
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
import { GatewayError } from "../../core/errors";
|
|
2
|
+
import {
|
|
3
|
+
Priority,
|
|
4
|
+
policyDebug,
|
|
5
|
+
resolveConfig,
|
|
6
|
+
safeCall,
|
|
7
|
+
setDebugHeader,
|
|
8
|
+
withSkip
|
|
9
|
+
} from "../sdk";
|
|
10
|
+
function defaultSnapshot() {
|
|
11
|
+
return {
|
|
12
|
+
state: "closed",
|
|
13
|
+
failureCount: 0,
|
|
14
|
+
successCount: 0,
|
|
15
|
+
lastFailureTime: 0,
|
|
16
|
+
lastStateChange: Date.now()
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
class InMemoryCircuitBreakerStore {
|
|
20
|
+
circuits = /* @__PURE__ */ new Map();
|
|
21
|
+
getOrCreate(key) {
|
|
22
|
+
let snap = this.circuits.get(key);
|
|
23
|
+
if (!snap) {
|
|
24
|
+
snap = defaultSnapshot();
|
|
25
|
+
this.circuits.set(key, snap);
|
|
26
|
+
}
|
|
27
|
+
return snap;
|
|
28
|
+
}
|
|
29
|
+
async getState(key) {
|
|
30
|
+
return { ...this.getOrCreate(key) };
|
|
31
|
+
}
|
|
32
|
+
async recordSuccess(key) {
|
|
33
|
+
const snap = this.getOrCreate(key);
|
|
34
|
+
snap.successCount++;
|
|
35
|
+
return { ...snap };
|
|
36
|
+
}
|
|
37
|
+
async recordFailure(key) {
|
|
38
|
+
const snap = this.getOrCreate(key);
|
|
39
|
+
snap.failureCount++;
|
|
40
|
+
snap.lastFailureTime = Date.now();
|
|
41
|
+
return { ...snap };
|
|
42
|
+
}
|
|
43
|
+
async transition(key, to) {
|
|
44
|
+
const snap = this.getOrCreate(key);
|
|
45
|
+
snap.state = to;
|
|
46
|
+
snap.lastStateChange = Date.now();
|
|
47
|
+
if (to === "closed") {
|
|
48
|
+
snap.failureCount = 0;
|
|
49
|
+
snap.successCount = 0;
|
|
50
|
+
}
|
|
51
|
+
if (to === "half-open") {
|
|
52
|
+
snap.successCount = 0;
|
|
53
|
+
}
|
|
54
|
+
return { ...snap };
|
|
55
|
+
}
|
|
56
|
+
async reset(key) {
|
|
57
|
+
this.circuits.delete(key);
|
|
58
|
+
}
|
|
59
|
+
/** Remove all circuits (for testing) */
|
|
60
|
+
clear() {
|
|
61
|
+
this.circuits.clear();
|
|
62
|
+
}
|
|
63
|
+
/** Release all state. */
|
|
64
|
+
destroy() {
|
|
65
|
+
this.circuits.clear();
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
async function transitionAndNotify(resolvedStore, key, from, to, onStateChange, debug) {
|
|
69
|
+
await safeCall(
|
|
70
|
+
() => resolvedStore.transition(key, to),
|
|
71
|
+
void 0,
|
|
72
|
+
debug,
|
|
73
|
+
"store.transition()"
|
|
74
|
+
);
|
|
75
|
+
if (onStateChange) {
|
|
76
|
+
await safeCall(
|
|
77
|
+
() => Promise.resolve(onStateChange(key, from, to)),
|
|
78
|
+
void 0,
|
|
79
|
+
debug,
|
|
80
|
+
"onStateChange()"
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
function circuitBreaker(config) {
|
|
85
|
+
const resolved = resolveConfig(
|
|
86
|
+
{
|
|
87
|
+
failureThreshold: 5,
|
|
88
|
+
resetTimeoutMs: 3e4,
|
|
89
|
+
halfOpenMax: 1,
|
|
90
|
+
failureOn: [500, 502, 503, 504],
|
|
91
|
+
openStatusCode: 503
|
|
92
|
+
},
|
|
93
|
+
config
|
|
94
|
+
);
|
|
95
|
+
let store = config?.store;
|
|
96
|
+
if (!store) {
|
|
97
|
+
store = new InMemoryCircuitBreakerStore();
|
|
98
|
+
}
|
|
99
|
+
const resolvedStore = store;
|
|
100
|
+
const onStateChange = config?.onStateChange;
|
|
101
|
+
const halfOpenProbes = /* @__PURE__ */ new Map();
|
|
102
|
+
const handler = async (c, next) => {
|
|
103
|
+
const debug = policyDebug(c, "circuit-breaker");
|
|
104
|
+
const key = config?.key ? config.key(c) : new URL(c.req.url).pathname;
|
|
105
|
+
const snap = await safeCall(
|
|
106
|
+
() => resolvedStore.getState(key),
|
|
107
|
+
defaultSnapshot(),
|
|
108
|
+
debug,
|
|
109
|
+
"store.getState()"
|
|
110
|
+
);
|
|
111
|
+
const now = Date.now();
|
|
112
|
+
setDebugHeader(c, "x-stoma-circuit-key", key);
|
|
113
|
+
setDebugHeader(c, "x-stoma-circuit-state", snap.state);
|
|
114
|
+
setDebugHeader(c, "x-stoma-circuit-failures", snap.failureCount);
|
|
115
|
+
if (snap.state === "open") {
|
|
116
|
+
if (now - snap.lastStateChange >= resolved.resetTimeoutMs) {
|
|
117
|
+
debug(`open -> half-open (key=${key})`);
|
|
118
|
+
await transitionAndNotify(
|
|
119
|
+
resolvedStore,
|
|
120
|
+
key,
|
|
121
|
+
"open",
|
|
122
|
+
"half-open",
|
|
123
|
+
onStateChange,
|
|
124
|
+
debug
|
|
125
|
+
);
|
|
126
|
+
halfOpenProbes.set(key, 0);
|
|
127
|
+
} else {
|
|
128
|
+
const retryAfter = Math.ceil(
|
|
129
|
+
(resolved.resetTimeoutMs - (now - snap.lastStateChange)) / 1e3
|
|
130
|
+
);
|
|
131
|
+
throw new GatewayError(
|
|
132
|
+
resolved.openStatusCode,
|
|
133
|
+
"circuit_open",
|
|
134
|
+
"Service temporarily unavailable",
|
|
135
|
+
{ "retry-after": String(retryAfter) }
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
if (snap.state === "half-open" || snap.state === "open" && now - snap.lastStateChange >= resolved.resetTimeoutMs) {
|
|
140
|
+
const inFlight = halfOpenProbes.get(key) ?? 0;
|
|
141
|
+
if (inFlight >= resolved.halfOpenMax) {
|
|
142
|
+
throw new GatewayError(
|
|
143
|
+
resolved.openStatusCode,
|
|
144
|
+
"circuit_open",
|
|
145
|
+
"Service temporarily unavailable",
|
|
146
|
+
{ "retry-after": String(Math.ceil(resolved.resetTimeoutMs / 1e3)) }
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
halfOpenProbes.set(key, inFlight + 1);
|
|
150
|
+
try {
|
|
151
|
+
await next();
|
|
152
|
+
const isFailure = resolved.failureOn.includes(c.res.status);
|
|
153
|
+
if (isFailure) {
|
|
154
|
+
debug(
|
|
155
|
+
`half-open probe failed (key=${key}, status=${c.res.status}) -> open`
|
|
156
|
+
);
|
|
157
|
+
await safeCall(
|
|
158
|
+
() => resolvedStore.recordFailure(key),
|
|
159
|
+
void 0,
|
|
160
|
+
debug,
|
|
161
|
+
"store.recordFailure()"
|
|
162
|
+
);
|
|
163
|
+
await transitionAndNotify(
|
|
164
|
+
resolvedStore,
|
|
165
|
+
key,
|
|
166
|
+
"half-open",
|
|
167
|
+
"open",
|
|
168
|
+
onStateChange,
|
|
169
|
+
debug
|
|
170
|
+
);
|
|
171
|
+
} else {
|
|
172
|
+
debug(`half-open probe succeeded (key=${key}) -> closed`);
|
|
173
|
+
await safeCall(
|
|
174
|
+
() => resolvedStore.recordSuccess(key),
|
|
175
|
+
void 0,
|
|
176
|
+
debug,
|
|
177
|
+
"store.recordSuccess()"
|
|
178
|
+
);
|
|
179
|
+
await transitionAndNotify(
|
|
180
|
+
resolvedStore,
|
|
181
|
+
key,
|
|
182
|
+
"half-open",
|
|
183
|
+
"closed",
|
|
184
|
+
onStateChange,
|
|
185
|
+
debug
|
|
186
|
+
);
|
|
187
|
+
halfOpenProbes.delete(key);
|
|
188
|
+
}
|
|
189
|
+
} catch (err) {
|
|
190
|
+
debug(`half-open probe threw (key=${key}) -> open`);
|
|
191
|
+
await safeCall(
|
|
192
|
+
() => resolvedStore.recordFailure(key),
|
|
193
|
+
void 0,
|
|
194
|
+
debug,
|
|
195
|
+
"store.recordFailure()"
|
|
196
|
+
);
|
|
197
|
+
await transitionAndNotify(
|
|
198
|
+
resolvedStore,
|
|
199
|
+
key,
|
|
200
|
+
"half-open",
|
|
201
|
+
"open",
|
|
202
|
+
onStateChange,
|
|
203
|
+
debug
|
|
204
|
+
);
|
|
205
|
+
throw err;
|
|
206
|
+
} finally {
|
|
207
|
+
const current = halfOpenProbes.get(key) ?? 1;
|
|
208
|
+
if (current <= 1) {
|
|
209
|
+
halfOpenProbes.delete(key);
|
|
210
|
+
} else {
|
|
211
|
+
halfOpenProbes.set(key, current - 1);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
try {
|
|
217
|
+
await next();
|
|
218
|
+
} catch (err) {
|
|
219
|
+
const updated = await safeCall(
|
|
220
|
+
() => resolvedStore.recordFailure(key),
|
|
221
|
+
null,
|
|
222
|
+
debug,
|
|
223
|
+
"store.recordFailure()"
|
|
224
|
+
);
|
|
225
|
+
if (updated && updated.failureCount >= resolved.failureThreshold) {
|
|
226
|
+
debug(
|
|
227
|
+
`closed -> open (key=${key}, failures=${updated.failureCount}/${resolved.failureThreshold})`
|
|
228
|
+
);
|
|
229
|
+
await transitionAndNotify(
|
|
230
|
+
resolvedStore,
|
|
231
|
+
key,
|
|
232
|
+
"closed",
|
|
233
|
+
"open",
|
|
234
|
+
onStateChange,
|
|
235
|
+
debug
|
|
236
|
+
);
|
|
237
|
+
}
|
|
238
|
+
throw err;
|
|
239
|
+
}
|
|
240
|
+
if (resolved.failureOn.includes(c.res.status)) {
|
|
241
|
+
const updated = await safeCall(
|
|
242
|
+
() => resolvedStore.recordFailure(key),
|
|
243
|
+
null,
|
|
244
|
+
debug,
|
|
245
|
+
"store.recordFailure()"
|
|
246
|
+
);
|
|
247
|
+
if (updated && updated.failureCount >= resolved.failureThreshold) {
|
|
248
|
+
debug(
|
|
249
|
+
`closed -> open (key=${key}, failures=${updated.failureCount}/${resolved.failureThreshold})`
|
|
250
|
+
);
|
|
251
|
+
await transitionAndNotify(
|
|
252
|
+
resolvedStore,
|
|
253
|
+
key,
|
|
254
|
+
"closed",
|
|
255
|
+
"open",
|
|
256
|
+
onStateChange,
|
|
257
|
+
debug
|
|
258
|
+
);
|
|
259
|
+
}
|
|
260
|
+
} else {
|
|
261
|
+
await safeCall(
|
|
262
|
+
() => resolvedStore.recordSuccess(key),
|
|
263
|
+
void 0,
|
|
264
|
+
debug,
|
|
265
|
+
"store.recordSuccess()"
|
|
266
|
+
);
|
|
267
|
+
}
|
|
268
|
+
};
|
|
269
|
+
return {
|
|
270
|
+
name: "circuit-breaker",
|
|
271
|
+
priority: Priority.CIRCUIT_BREAKER,
|
|
272
|
+
handler: withSkip(config?.skip, handler),
|
|
273
|
+
httpOnly: true
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
export {
|
|
277
|
+
InMemoryCircuitBreakerStore,
|
|
278
|
+
circuitBreaker
|
|
279
|
+
};
|
|
280
|
+
//# sourceMappingURL=circuit-breaker.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../src/policies/resilience/circuit-breaker.ts"],"sourcesContent":["/**\n * Circuit breaker policy - protect upstream services from cascading failures.\n *\n * Implements the three-state circuit breaker pattern (closed / open / half-open)\n * with pluggable state storage via {@link CircuitBreakerStore}.\n *\n * @module circuit-breaker\n */\nimport type { Context } from \"hono\";\nimport { GatewayError } from \"../../core/errors\";\nimport type { DebugLogger } from \"../../utils/debug\";\nimport {\n Priority,\n policyDebug,\n resolveConfig,\n safeCall,\n setDebugHeader,\n withSkip,\n} from \"../sdk\";\nimport type { Policy, PolicyConfig } from \"../types\";\n\n// --- Store interface ---\n\n/** The three states of the circuit breaker state machine. */\nexport type CircuitState = \"closed\" | \"open\" | \"half-open\";\n\n/** Point-in-time snapshot of a circuit's state and counters. */\nexport interface CircuitBreakerSnapshot {\n /** Current circuit state. */\n state: CircuitState;\n /** Number of consecutive failures since last reset. */\n failureCount: number;\n /** Number of successful probes in half-open state. */\n successCount: number;\n /** Epoch ms of the most recent failure. `0` if no failures recorded. */\n lastFailureTime: number;\n /** Epoch ms of the most recent state transition. */\n lastStateChange: number;\n}\n\n/**\n * Pluggable storage backend for circuit breaker state.\n *\n * Implement this interface to store circuit state in Durable Objects,\n * KV, or any shared datastore for multi-instance deployments.\n */\nexport interface CircuitBreakerStore {\n /** Read the current snapshot for a circuit key. */\n getState(key: string): Promise<CircuitBreakerSnapshot>;\n /** Record a successful request and return the updated snapshot. */\n recordSuccess(key: string): Promise<CircuitBreakerSnapshot>;\n /** Record a failed request and return the updated snapshot. */\n recordFailure(key: string): Promise<CircuitBreakerSnapshot>;\n /** Transition the circuit to a new state and return the updated snapshot. */\n transition(key: string, to: CircuitState): Promise<CircuitBreakerSnapshot>;\n /** Fully reset a circuit, removing all state. */\n reset(key: string): Promise<void>;\n /** Optional cleanup - release timers, close connections, etc. */\n destroy?(): void;\n}\n\n// --- In-memory default ---\n\nfunction defaultSnapshot(): CircuitBreakerSnapshot {\n return {\n state: \"closed\",\n failureCount: 0,\n successCount: 0,\n lastFailureTime: 0,\n lastStateChange: Date.now(),\n };\n}\n\nexport class InMemoryCircuitBreakerStore implements CircuitBreakerStore {\n private circuits = new Map<string, CircuitBreakerSnapshot>();\n\n private getOrCreate(key: string): CircuitBreakerSnapshot {\n let snap = this.circuits.get(key);\n if (!snap) {\n snap = defaultSnapshot();\n this.circuits.set(key, snap);\n }\n return snap;\n }\n\n async getState(key: string): Promise<CircuitBreakerSnapshot> {\n return { ...this.getOrCreate(key) };\n }\n\n async recordSuccess(key: string): Promise<CircuitBreakerSnapshot> {\n const snap = this.getOrCreate(key);\n snap.successCount++;\n return { ...snap };\n }\n\n async recordFailure(key: string): Promise<CircuitBreakerSnapshot> {\n const snap = this.getOrCreate(key);\n snap.failureCount++;\n snap.lastFailureTime = Date.now();\n return { ...snap };\n }\n\n async transition(\n key: string,\n to: CircuitState\n ): Promise<CircuitBreakerSnapshot> {\n const snap = this.getOrCreate(key);\n snap.state = to;\n snap.lastStateChange = Date.now();\n if (to === \"closed\") {\n snap.failureCount = 0;\n snap.successCount = 0;\n }\n if (to === \"half-open\") {\n snap.successCount = 0;\n }\n return { ...snap };\n }\n\n async reset(key: string): Promise<void> {\n this.circuits.delete(key);\n }\n\n /** Remove all circuits (for testing) */\n clear(): void {\n this.circuits.clear();\n }\n\n /** Release all state. */\n destroy(): void {\n this.circuits.clear();\n }\n}\n\n// --- Policy ---\n\nexport interface CircuitBreakerConfig extends PolicyConfig {\n /** Number of failures before opening the circuit. Default: 5. */\n failureThreshold?: number;\n /** Time in ms before transitioning from open → half-open. Default: 30000. */\n resetTimeoutMs?: number;\n /** Max concurrent probes allowed in half-open state. Default: 1. */\n halfOpenMax?: number;\n /** Status codes considered failures. Default: [500, 502, 503, 504]. */\n failureOn?: number[];\n /** Storage backend. Default: InMemoryCircuitBreakerStore. */\n store?: CircuitBreakerStore;\n /** Key extractor. Default: request URL pathname. */\n key?: (c: Context) => string;\n /** HTTP status code when the circuit is open. Default: 503. */\n openStatusCode?: number;\n /**\n * Callback invoked on every state transition.\n *\n * Called via `safeCall` so errors are swallowed - a failing callback\n * never blocks traffic. Useful for metrics, logging, or alerting.\n *\n * @param key - The circuit key that transitioned.\n * @param from - The previous circuit state.\n * @param to - The new circuit state.\n */\n onStateChange?: (\n key: string,\n from: CircuitState,\n to: CircuitState\n ) => void | Promise<void>;\n}\n\n/**\n * Transition the circuit and invoke the onStateChange callback (if configured).\n *\n * Both the store transition and the callback are wrapped in safeCall so\n * failures are swallowed - a broken store or callback never blocks traffic.\n */\nasync function transitionAndNotify(\n resolvedStore: CircuitBreakerStore,\n key: string,\n from: CircuitState,\n to: CircuitState,\n onStateChange: CircuitBreakerConfig[\"onStateChange\"],\n debug: DebugLogger\n): Promise<void> {\n await safeCall(\n () => resolvedStore.transition(key, to),\n undefined,\n debug,\n \"store.transition()\"\n );\n if (onStateChange) {\n await safeCall(\n () => Promise.resolve(onStateChange(key, from, to)),\n undefined,\n debug,\n \"onStateChange()\"\n );\n }\n}\n\n/**\n * Protect upstream services by breaking the circuit on repeated failures.\n *\n * Implements the three-state circuit breaker pattern:\n * - **Closed** - requests flow normally; failures are counted.\n * - **Open** - requests are immediately rejected with 503; a `Retry-After` header is set.\n * - **Half-open** - a limited number of probe requests are allowed through to test recovery.\n *\n * State transitions: `closed → open` when failures reach the threshold,\n * `open → half-open` after the reset timeout, `half-open → closed` on\n * probe success or `half-open → open` on probe failure.\n *\n * @param config - Failure threshold, reset timeout, and storage backend.\n * @returns A {@link Policy} at priority 30.\n *\n * @example\n * ```ts\n * // Open after 5 failures, retry after 30s\n * circuitBreaker();\n *\n * // Tighter threshold with custom store\n * circuitBreaker({\n * failureThreshold: 3,\n * resetTimeoutMs: 10_000,\n * failureOn: [500, 502, 503],\n * store: new InMemoryCircuitBreakerStore(),\n * });\n *\n * // With state change notifications\n * circuitBreaker({\n * failureThreshold: 5,\n * onStateChange: (key, from, to) => {\n * console.log(`Circuit ${key}: ${from} -> ${to}`);\n * },\n * });\n * ```\n */\nexport function circuitBreaker(config?: CircuitBreakerConfig): Policy {\n const resolved = resolveConfig<CircuitBreakerConfig>(\n {\n failureThreshold: 5,\n resetTimeoutMs: 30_000,\n halfOpenMax: 1,\n failureOn: [500, 502, 503, 504],\n openStatusCode: 503,\n },\n config\n );\n\n let store = config?.store;\n if (!store) {\n store = new InMemoryCircuitBreakerStore();\n }\n const resolvedStore = store;\n const onStateChange = config?.onStateChange;\n\n // Track half-open probes in flight (per key)\n const halfOpenProbes = new Map<string, number>();\n\n const handler: import(\"hono\").MiddlewareHandler = async (c, next) => {\n const debug = policyDebug(c, \"circuit-breaker\");\n const key = config?.key ? config.key(c) : new URL(c.req.url).pathname;\n\n // Resilient to store failures - fail-open (assume closed) if the\n // store is unreachable, so a broken store never blocks traffic.\n const snap = await safeCall(\n () => resolvedStore.getState(key),\n defaultSnapshot(),\n debug,\n \"store.getState()\"\n );\n const now = Date.now();\n setDebugHeader(c, \"x-stoma-circuit-key\", key);\n setDebugHeader(c, \"x-stoma-circuit-state\", snap.state);\n setDebugHeader(c, \"x-stoma-circuit-failures\", snap.failureCount);\n\n // --- OPEN ---\n if (snap.state === \"open\") {\n if (now - snap.lastStateChange >= resolved.resetTimeoutMs!) {\n // Transition to half-open\n debug(`open -> half-open (key=${key})`);\n await transitionAndNotify(\n resolvedStore,\n key,\n \"open\",\n \"half-open\",\n onStateChange,\n debug\n );\n halfOpenProbes.set(key, 0);\n } else {\n const retryAfter = Math.ceil(\n (resolved.resetTimeoutMs! - (now - snap.lastStateChange)) / 1000\n );\n throw new GatewayError(\n resolved.openStatusCode!,\n \"circuit_open\",\n \"Service temporarily unavailable\",\n { \"retry-after\": String(retryAfter) }\n );\n }\n }\n\n // --- HALF-OPEN: limit concurrent probes ---\n if (\n snap.state === \"half-open\" ||\n (snap.state === \"open\" &&\n now - snap.lastStateChange >= resolved.resetTimeoutMs!)\n ) {\n const inFlight = halfOpenProbes.get(key) ?? 0;\n if (inFlight >= resolved.halfOpenMax!) {\n throw new GatewayError(\n resolved.openStatusCode!,\n \"circuit_open\",\n \"Service temporarily unavailable\",\n { \"retry-after\": String(Math.ceil(resolved.resetTimeoutMs! / 1000)) }\n );\n }\n halfOpenProbes.set(key, inFlight + 1);\n\n try {\n await next();\n\n const isFailure = resolved.failureOn!.includes(c.res.status);\n if (isFailure) {\n debug(\n `half-open probe failed (key=${key}, status=${c.res.status}) -> open`\n );\n await safeCall(\n () => resolvedStore.recordFailure(key),\n undefined,\n debug,\n \"store.recordFailure()\"\n );\n await transitionAndNotify(\n resolvedStore,\n key,\n \"half-open\",\n \"open\",\n onStateChange,\n debug\n );\n } else {\n debug(`half-open probe succeeded (key=${key}) -> closed`);\n await safeCall(\n () => resolvedStore.recordSuccess(key),\n undefined,\n debug,\n \"store.recordSuccess()\"\n );\n await transitionAndNotify(\n resolvedStore,\n key,\n \"half-open\",\n \"closed\",\n onStateChange,\n debug\n );\n halfOpenProbes.delete(key);\n }\n } catch (err) {\n debug(`half-open probe threw (key=${key}) -> open`);\n await safeCall(\n () => resolvedStore.recordFailure(key),\n undefined,\n debug,\n \"store.recordFailure()\"\n );\n await transitionAndNotify(\n resolvedStore,\n key,\n \"half-open\",\n \"open\",\n onStateChange,\n debug\n );\n throw err;\n } finally {\n const current = halfOpenProbes.get(key) ?? 1;\n if (current <= 1) {\n halfOpenProbes.delete(key);\n } else {\n halfOpenProbes.set(key, current - 1);\n }\n }\n return;\n }\n\n // --- CLOSED ---\n try {\n await next();\n } catch (err) {\n // If recordFailure fails, skip the threshold check entirely -\n // we have no snapshot to compare against.\n const updated = await safeCall(\n () => resolvedStore.recordFailure(key),\n null,\n debug,\n \"store.recordFailure()\"\n );\n if (updated && updated.failureCount >= resolved.failureThreshold!) {\n debug(\n `closed -> open (key=${key}, failures=${updated.failureCount}/${resolved.failureThreshold})`\n );\n await transitionAndNotify(\n resolvedStore,\n key,\n \"closed\",\n \"open\",\n onStateChange,\n debug\n );\n }\n throw err;\n }\n\n if (resolved.failureOn!.includes(c.res.status)) {\n const updated = await safeCall(\n () => resolvedStore.recordFailure(key),\n null,\n debug,\n \"store.recordFailure()\"\n );\n if (updated && updated.failureCount >= resolved.failureThreshold!) {\n debug(\n `closed -> open (key=${key}, failures=${updated.failureCount}/${resolved.failureThreshold})`\n );\n await transitionAndNotify(\n resolvedStore,\n key,\n \"closed\",\n \"open\",\n onStateChange,\n debug\n );\n }\n } else {\n await safeCall(\n () => resolvedStore.recordSuccess(key),\n undefined,\n debug,\n \"store.recordSuccess()\"\n );\n }\n };\n\n return {\n name: \"circuit-breaker\",\n priority: Priority.CIRCUIT_BREAKER,\n handler: withSkip(config?.skip, handler),\n httpOnly: true,\n };\n}\n"],"mappings":"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","names":[]}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { g as PolicyConfig, P as Policy } from '../../protocol-2fD3DJrL.js';
|
|
2
|
+
import 'hono';
|
|
3
|
+
import '../sdk/trace.js';
|
|
4
|
+
import '@vivero/stoma-core';
|
|
5
|
+
|
|
6
|
+
interface LatencyInjectionConfig extends PolicyConfig {
|
|
7
|
+
/** Base delay in milliseconds. Required. */
|
|
8
|
+
delayMs: number;
|
|
9
|
+
/** Jitter proportion (0 to 1). Actual delay varies by +/- jitter * delayMs. Default: 0. */
|
|
10
|
+
jitter?: number;
|
|
11
|
+
/** Probability of injecting latency (0 to 1). Default: 1 (always). */
|
|
12
|
+
probability?: number;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Inject artificial latency into the pipeline for chaos/resilience testing.
|
|
16
|
+
*
|
|
17
|
+
* When active, pauses execution for a configurable duration before calling
|
|
18
|
+
* `next()`. Supports jitter to vary the delay and a probability setting to
|
|
19
|
+
* inject latency only a fraction of the time.
|
|
20
|
+
*
|
|
21
|
+
* @param config - Delay duration, jitter, and injection probability.
|
|
22
|
+
* @returns A {@link Policy} at priority 5 (early pipeline).
|
|
23
|
+
*
|
|
24
|
+
* @example
|
|
25
|
+
* ```ts
|
|
26
|
+
* // Fixed 100ms delay on every request
|
|
27
|
+
* latencyInjection({ delayMs: 100 });
|
|
28
|
+
*
|
|
29
|
+
* // 200ms +/- 50% jitter, injected 30% of the time
|
|
30
|
+
* latencyInjection({ delayMs: 200, jitter: 0.5, probability: 0.3 });
|
|
31
|
+
* ```
|
|
32
|
+
*/
|
|
33
|
+
declare const latencyInjection: (config: LatencyInjectionConfig) => Policy;
|
|
34
|
+
|
|
35
|
+
export { type LatencyInjectionConfig, latencyInjection };
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { definePolicy, Priority } from "../sdk";
|
|
2
|
+
const latencyInjection = /* @__PURE__ */ definePolicy({
|
|
3
|
+
name: "latency-injection",
|
|
4
|
+
priority: Priority.EARLY,
|
|
5
|
+
httpOnly: true,
|
|
6
|
+
defaults: { jitter: 0, probability: 1 },
|
|
7
|
+
handler: async (_c, next, { config, debug }) => {
|
|
8
|
+
if (Math.random() >= config.probability) {
|
|
9
|
+
debug("skipping injection (probability miss)");
|
|
10
|
+
await next();
|
|
11
|
+
return;
|
|
12
|
+
}
|
|
13
|
+
let delay = config.delayMs;
|
|
14
|
+
if (config.jitter > 0) {
|
|
15
|
+
delay += (Math.random() * 2 - 1) * config.jitter * config.delayMs;
|
|
16
|
+
}
|
|
17
|
+
delay = Math.max(0, delay);
|
|
18
|
+
debug(`injecting ${delay.toFixed(0)}ms latency`);
|
|
19
|
+
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
20
|
+
await next();
|
|
21
|
+
}
|
|
22
|
+
});
|
|
23
|
+
export {
|
|
24
|
+
latencyInjection
|
|
25
|
+
};
|
|
26
|
+
//# sourceMappingURL=latency-injection.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../src/policies/resilience/latency-injection.ts"],"sourcesContent":["/**\n * Latency injection policy - simulate network delays for chaos testing.\n *\n * @module latency-injection\n */\n\nimport { definePolicy, Priority } from \"../sdk\";\nimport type { PolicyConfig } from \"../types\";\n\nexport interface LatencyInjectionConfig extends PolicyConfig {\n /** Base delay in milliseconds. Required. */\n delayMs: number;\n /** Jitter proportion (0 to 1). Actual delay varies by +/- jitter * delayMs. Default: 0. */\n jitter?: number;\n /** Probability of injecting latency (0 to 1). Default: 1 (always). */\n probability?: number;\n}\n\n/**\n * Inject artificial latency into the pipeline for chaos/resilience testing.\n *\n * When active, pauses execution for a configurable duration before calling\n * `next()`. Supports jitter to vary the delay and a probability setting to\n * inject latency only a fraction of the time.\n *\n * @param config - Delay duration, jitter, and injection probability.\n * @returns A {@link Policy} at priority 5 (early pipeline).\n *\n * @example\n * ```ts\n * // Fixed 100ms delay on every request\n * latencyInjection({ delayMs: 100 });\n *\n * // 200ms +/- 50% jitter, injected 30% of the time\n * latencyInjection({ delayMs: 200, jitter: 0.5, probability: 0.3 });\n * ```\n */\nexport const latencyInjection =\n /*#__PURE__*/ definePolicy<LatencyInjectionConfig>({\n name: \"latency-injection\",\n priority: Priority.EARLY,\n httpOnly: true,\n defaults: { jitter: 0, probability: 1 },\n handler: async (_c, next, { config, debug }) => {\n // Roll against probability\n if (Math.random() >= config.probability!) {\n debug(\"skipping injection (probability miss)\");\n await next();\n return;\n }\n\n // Compute delay with optional jitter\n let delay = config.delayMs;\n if (config.jitter! > 0) {\n delay += (Math.random() * 2 - 1) * config.jitter! * config.delayMs;\n }\n // Clamp to minimum 0\n delay = Math.max(0, delay);\n\n debug(`injecting ${delay.toFixed(0)}ms latency`);\n await new Promise((resolve) => setTimeout(resolve, delay));\n\n await next();\n },\n });\n"],"mappings":"AAMA,SAAS,cAAc,gBAAgB;AA+BhC,MAAM,mBACG,6BAAqC;AAAA,EACjD,MAAM;AAAA,EACN,UAAU,SAAS;AAAA,EACnB,UAAU;AAAA,EACV,UAAU,EAAE,QAAQ,GAAG,aAAa,EAAE;AAAA,EACtC,SAAS,OAAO,IAAI,MAAM,EAAE,QAAQ,MAAM,MAAM;AAE9C,QAAI,KAAK,OAAO,KAAK,OAAO,aAAc;AACxC,YAAM,uCAAuC;AAC7C,YAAM,KAAK;AACX;AAAA,IACF;AAGA,QAAI,QAAQ,OAAO;AACnB,QAAI,OAAO,SAAU,GAAG;AACtB,gBAAU,KAAK,OAAO,IAAI,IAAI,KAAK,OAAO,SAAU,OAAO;AAAA,IAC7D;AAEA,YAAQ,KAAK,IAAI,GAAG,KAAK;AAEzB,UAAM,aAAa,MAAM,QAAQ,CAAC,CAAC,YAAY;AAC/C,UAAM,IAAI,QAAQ,CAAC,YAAY,WAAW,SAAS,KAAK,CAAC;AAEzD,UAAM,KAAK;AAAA,EACb;AACF,CAAC;","names":[]}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { g as PolicyConfig, P as Policy } from '../../protocol-2fD3DJrL.js';
|
|
2
|
+
import 'hono';
|
|
3
|
+
import '../sdk/trace.js';
|
|
4
|
+
import '@vivero/stoma-core';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Retry policy - automatic retry with configurable backoff for failed upstream calls.
|
|
8
|
+
*
|
|
9
|
+
* Retries work by inspecting the response status after `next()` completes.
|
|
10
|
+
* When a retryable status code is detected and a `_proxyRequest` exists on
|
|
11
|
+
* the Hono context (set by the URL upstream handler in `gateway.ts`), the
|
|
12
|
+
* policy clones the stored request and re-issues it via `fetch()` directly -
|
|
13
|
+
* no `globalThis.fetch` patching, fully concurrency-safe.
|
|
14
|
+
*
|
|
15
|
+
* For handler-based or service-binding upstreams there is no `_proxyRequest`,
|
|
16
|
+
* so the retry policy is effectively a no-op - which is the correct behavior
|
|
17
|
+
* since those upstream types would require calling `next()` multiple times
|
|
18
|
+
* (disallowed by Hono's compose model).
|
|
19
|
+
*
|
|
20
|
+
* @module retry
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
interface RetryConfig extends PolicyConfig {
|
|
24
|
+
/** Maximum number of retries. Default: 3. */
|
|
25
|
+
maxRetries?: number;
|
|
26
|
+
/** Status codes that trigger a retry. Default: [502, 503, 504]. */
|
|
27
|
+
retryOn?: number[];
|
|
28
|
+
/** Backoff strategy. Default: "exponential". */
|
|
29
|
+
backoff?: "fixed" | "exponential";
|
|
30
|
+
/** Base delay in ms for backoff. Default: 200. */
|
|
31
|
+
baseDelayMs?: number;
|
|
32
|
+
/** Maximum delay in ms. Default: 5000. */
|
|
33
|
+
maxDelayMs?: number;
|
|
34
|
+
/** HTTP methods eligible for retry. Default: idempotent methods. */
|
|
35
|
+
retryMethods?: string[];
|
|
36
|
+
/** Response header name for the retry count. Default: `"x-retry-count"`. */
|
|
37
|
+
retryCountHeader?: string;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Retry failed upstream calls with configurable backoff.
|
|
41
|
+
*
|
|
42
|
+
* After `next()` completes, checks the response status against `retryOn`
|
|
43
|
+
* codes. If a retry is warranted and a `_proxyRequest` is available on the
|
|
44
|
+
* context (set by `createUrlUpstream()` in `gateway.ts`), the policy clones
|
|
45
|
+
* the stored request and calls `fetch()` directly - fully concurrency-safe
|
|
46
|
+
* with no `globalThis.fetch` patching.
|
|
47
|
+
*
|
|
48
|
+
* For handler-based or service-binding upstreams (no `_proxyRequest`), the
|
|
49
|
+
* retry policy is a no-op since there is no stored request to re-issue.
|
|
50
|
+
* Sets `X-Retry-Count` on the response when retries occur.
|
|
51
|
+
*
|
|
52
|
+
* @param config - Retry limits, backoff strategy, and retryable status codes.
|
|
53
|
+
* @returns A {@link Policy} at priority 90 (runs late, wraps the upstream fetch).
|
|
54
|
+
*
|
|
55
|
+
* @example
|
|
56
|
+
* ```ts
|
|
57
|
+
* // Retry 502/503/504 up to 3 times with exponential backoff
|
|
58
|
+
* retry();
|
|
59
|
+
*
|
|
60
|
+
* // Fixed 500ms delay, retry on 500 too
|
|
61
|
+
* retry({
|
|
62
|
+
* maxRetries: 2,
|
|
63
|
+
* retryOn: [500, 502, 503, 504],
|
|
64
|
+
* backoff: "fixed",
|
|
65
|
+
* baseDelayMs: 500,
|
|
66
|
+
* });
|
|
67
|
+
* ```
|
|
68
|
+
*/
|
|
69
|
+
declare function retry(config?: RetryConfig): Policy;
|
|
70
|
+
|
|
71
|
+
export { type RetryConfig, retry };
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { Priority, policyDebug, resolveConfig, withSkip } from "../sdk";
|
|
2
|
+
const DEFAULT_RETRY_METHODS = ["GET", "HEAD", "OPTIONS", "PUT", "DELETE"];
|
|
3
|
+
function sleep(ms) {
|
|
4
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
5
|
+
}
|
|
6
|
+
function computeDelay(attempt, strategy, baseMs, maxMs) {
|
|
7
|
+
const jitter = Math.random() * baseMs;
|
|
8
|
+
return strategy === "fixed" ? baseMs + jitter : Math.min(baseMs * 2 ** attempt + jitter, maxMs);
|
|
9
|
+
}
|
|
10
|
+
function retry(config) {
|
|
11
|
+
const resolved = resolveConfig(
|
|
12
|
+
{
|
|
13
|
+
maxRetries: 3,
|
|
14
|
+
retryOn: [502, 503, 504],
|
|
15
|
+
backoff: "exponential",
|
|
16
|
+
baseDelayMs: 200,
|
|
17
|
+
maxDelayMs: 5e3,
|
|
18
|
+
retryMethods: DEFAULT_RETRY_METHODS,
|
|
19
|
+
retryCountHeader: "x-retry-count"
|
|
20
|
+
},
|
|
21
|
+
config
|
|
22
|
+
);
|
|
23
|
+
const handler = async (c, next) => {
|
|
24
|
+
const debug = policyDebug(c, "retry");
|
|
25
|
+
const method = c.req.method.toUpperCase();
|
|
26
|
+
if (!resolved.retryMethods.includes(method)) {
|
|
27
|
+
await next();
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
await next();
|
|
31
|
+
const proxyRequest = c.get("_proxyRequest");
|
|
32
|
+
if (!proxyRequest) {
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
let retryCount = 0;
|
|
36
|
+
for (let attempt = 0; attempt < resolved.maxRetries; attempt++) {
|
|
37
|
+
if (!resolved.retryOn.includes(c.res.status)) {
|
|
38
|
+
break;
|
|
39
|
+
}
|
|
40
|
+
const delay = computeDelay(
|
|
41
|
+
attempt,
|
|
42
|
+
resolved.backoff,
|
|
43
|
+
resolved.baseDelayMs,
|
|
44
|
+
resolved.maxDelayMs
|
|
45
|
+
);
|
|
46
|
+
debug(
|
|
47
|
+
`attempt ${attempt + 1}/${resolved.maxRetries} failed (status=${c.res.status}), retrying in ${Math.round(delay)}ms`
|
|
48
|
+
);
|
|
49
|
+
await c.res.body?.cancel();
|
|
50
|
+
await sleep(delay);
|
|
51
|
+
let retryResponse;
|
|
52
|
+
try {
|
|
53
|
+
retryResponse = await fetch(proxyRequest.clone());
|
|
54
|
+
} catch {
|
|
55
|
+
debug(`retry attempt ${attempt + 1} fetch error, synthesizing 502`);
|
|
56
|
+
retryResponse = new Response(null, { status: 502 });
|
|
57
|
+
}
|
|
58
|
+
retryCount = attempt + 1;
|
|
59
|
+
c.res = new Response(retryResponse.body, {
|
|
60
|
+
status: retryResponse.status,
|
|
61
|
+
statusText: retryResponse.statusText,
|
|
62
|
+
headers: new Headers(retryResponse.headers)
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
if (retryCount > 0) {
|
|
66
|
+
c.res.headers.set(resolved.retryCountHeader, String(retryCount));
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
return {
|
|
70
|
+
name: "retry",
|
|
71
|
+
priority: Priority.RETRY,
|
|
72
|
+
handler: withSkip(config?.skip, handler),
|
|
73
|
+
httpOnly: true
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
export {
|
|
77
|
+
retry
|
|
78
|
+
};
|
|
79
|
+
//# sourceMappingURL=retry.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../src/policies/resilience/retry.ts"],"sourcesContent":["/**\n * Retry policy - automatic retry with configurable backoff for failed upstream calls.\n *\n * Retries work by inspecting the response status after `next()` completes.\n * When a retryable status code is detected and a `_proxyRequest` exists on\n * the Hono context (set by the URL upstream handler in `gateway.ts`), the\n * policy clones the stored request and re-issues it via `fetch()` directly -\n * no `globalThis.fetch` patching, fully concurrency-safe.\n *\n * For handler-based or service-binding upstreams there is no `_proxyRequest`,\n * so the retry policy is effectively a no-op - which is the correct behavior\n * since those upstream types would require calling `next()` multiple times\n * (disallowed by Hono's compose model).\n *\n * @module retry\n */\n\nimport { Priority, policyDebug, resolveConfig, withSkip } from \"../sdk\";\nimport type { Policy, PolicyConfig } from \"../types\";\n\nexport interface RetryConfig extends PolicyConfig {\n /** Maximum number of retries. Default: 3. */\n maxRetries?: number;\n /** Status codes that trigger a retry. Default: [502, 503, 504]. */\n retryOn?: number[];\n /** Backoff strategy. Default: \"exponential\". */\n backoff?: \"fixed\" | \"exponential\";\n /** Base delay in ms for backoff. Default: 200. */\n baseDelayMs?: number;\n /** Maximum delay in ms. Default: 5000. */\n maxDelayMs?: number;\n /** HTTP methods eligible for retry. Default: idempotent methods. */\n retryMethods?: string[];\n /** Response header name for the retry count. Default: `\"x-retry-count\"`. */\n retryCountHeader?: string;\n}\n\nconst DEFAULT_RETRY_METHODS = [\"GET\", \"HEAD\", \"OPTIONS\", \"PUT\", \"DELETE\"];\n\nfunction sleep(ms: number): Promise<void> {\n return new Promise((resolve) => setTimeout(resolve, ms));\n}\n\nfunction computeDelay(\n attempt: number,\n strategy: \"fixed\" | \"exponential\",\n baseMs: number,\n maxMs: number\n): number {\n const jitter = Math.random() * baseMs;\n return strategy === \"fixed\"\n ? baseMs + jitter\n : Math.min(baseMs * 2 ** attempt + jitter, maxMs);\n}\n\n/**\n * Retry failed upstream calls with configurable backoff.\n *\n * After `next()` completes, checks the response status against `retryOn`\n * codes. If a retry is warranted and a `_proxyRequest` is available on the\n * context (set by `createUrlUpstream()` in `gateway.ts`), the policy clones\n * the stored request and calls `fetch()` directly - fully concurrency-safe\n * with no `globalThis.fetch` patching.\n *\n * For handler-based or service-binding upstreams (no `_proxyRequest`), the\n * retry policy is a no-op since there is no stored request to re-issue.\n * Sets `X-Retry-Count` on the response when retries occur.\n *\n * @param config - Retry limits, backoff strategy, and retryable status codes.\n * @returns A {@link Policy} at priority 90 (runs late, wraps the upstream fetch).\n *\n * @example\n * ```ts\n * // Retry 502/503/504 up to 3 times with exponential backoff\n * retry();\n *\n * // Fixed 500ms delay, retry on 500 too\n * retry({\n * maxRetries: 2,\n * retryOn: [500, 502, 503, 504],\n * backoff: \"fixed\",\n * baseDelayMs: 500,\n * });\n * ```\n */\nexport function retry(config?: RetryConfig): Policy {\n const resolved = resolveConfig<RetryConfig>(\n {\n maxRetries: 3,\n retryOn: [502, 503, 504],\n backoff: \"exponential\" as const,\n baseDelayMs: 200,\n maxDelayMs: 5_000,\n retryMethods: DEFAULT_RETRY_METHODS,\n retryCountHeader: \"x-retry-count\",\n },\n config\n );\n\n const handler: import(\"hono\").MiddlewareHandler = async (c, next) => {\n const debug = policyDebug(c, \"retry\");\n const method = c.req.method.toUpperCase();\n\n // Non-retryable methods pass through without retry logic\n if (!resolved.retryMethods!.includes(method)) {\n await next();\n return;\n }\n\n // Execute the downstream pipeline (policies + upstream)\n await next();\n\n // Retrieve the stored proxy request (set by createUrlUpstream in gateway.ts).\n // For handler/service-binding upstreams this is undefined - retry is a no-op.\n const proxyRequest = c.get(\"_proxyRequest\") as Request | undefined;\n if (!proxyRequest) {\n return;\n }\n\n // Retry loop: check response status and re-issue if retryable\n let retryCount = 0;\n\n for (let attempt = 0; attempt < resolved.maxRetries!; attempt++) {\n if (!resolved.retryOn!.includes(c.res.status)) {\n break;\n }\n\n const delay = computeDelay(\n attempt,\n resolved.backoff!,\n resolved.baseDelayMs!,\n resolved.maxDelayMs!\n );\n debug(\n `attempt ${attempt + 1}/${resolved.maxRetries} failed (status=${c.res.status}), retrying in ${Math.round(delay)}ms`\n );\n\n // Cancel the body of the failed response to release resources\n await c.res.body?.cancel();\n\n await sleep(delay);\n\n // Clone the stored proxy request and re-issue via fetch directly\n let retryResponse: Response;\n try {\n retryResponse = await fetch(proxyRequest.clone());\n } catch {\n // Network error during retry - treat as a retryable 502 so\n // the loop continues to the next attempt.\n debug(`retry attempt ${attempt + 1} fetch error, synthesizing 502`);\n retryResponse = new Response(null, { status: 502 });\n }\n retryCount = attempt + 1;\n\n // Replace the response on the context\n c.res = new Response(retryResponse.body, {\n status: retryResponse.status,\n statusText: retryResponse.statusText,\n headers: new Headers(retryResponse.headers),\n });\n }\n\n if (retryCount > 0) {\n c.res.headers.set(resolved.retryCountHeader!, String(retryCount));\n }\n };\n\n return {\n name: \"retry\",\n priority: Priority.RETRY,\n handler: withSkip(config?.skip, handler),\n httpOnly: true,\n };\n}\n"],"mappings":"AAiBA,SAAS,UAAU,aAAa,eAAe,gBAAgB;AAoB/D,MAAM,wBAAwB,CAAC,OAAO,QAAQ,WAAW,OAAO,QAAQ;AAExE,SAAS,MAAM,IAA2B;AACxC,SAAO,IAAI,QAAQ,CAAC,YAAY,WAAW,SAAS,EAAE,CAAC;AACzD;AAEA,SAAS,aACP,SACA,UACA,QACA,OACQ;AACR,QAAM,SAAS,KAAK,OAAO,IAAI;AAC/B,SAAO,aAAa,UAChB,SAAS,SACT,KAAK,IAAI,SAAS,KAAK,UAAU,QAAQ,KAAK;AACpD;AAgCO,SAAS,MAAM,QAA8B;AAClD,QAAM,WAAW;AAAA,IACf;AAAA,MACE,YAAY;AAAA,MACZ,SAAS,CAAC,KAAK,KAAK,GAAG;AAAA,MACvB,SAAS;AAAA,MACT,aAAa;AAAA,MACb,YAAY;AAAA,MACZ,cAAc;AAAA,MACd,kBAAkB;AAAA,IACpB;AAAA,IACA;AAAA,EACF;AAEA,QAAM,UAA4C,OAAO,GAAG,SAAS;AACnE,UAAM,QAAQ,YAAY,GAAG,OAAO;AACpC,UAAM,SAAS,EAAE,IAAI,OAAO,YAAY;AAGxC,QAAI,CAAC,SAAS,aAAc,SAAS,MAAM,GAAG;AAC5C,YAAM,KAAK;AACX;AAAA,IACF;AAGA,UAAM,KAAK;AAIX,UAAM,eAAe,EAAE,IAAI,eAAe;AAC1C,QAAI,CAAC,cAAc;AACjB;AAAA,IACF;AAGA,QAAI,aAAa;AAEjB,aAAS,UAAU,GAAG,UAAU,SAAS,YAAa,WAAW;AAC/D,UAAI,CAAC,SAAS,QAAS,SAAS,EAAE,IAAI,MAAM,GAAG;AAC7C;AAAA,MACF;AAEA,YAAM,QAAQ;AAAA,QACZ;AAAA,QACA,SAAS;AAAA,QACT,SAAS;AAAA,QACT,SAAS;AAAA,MACX;AACA;AAAA,QACE,WAAW,UAAU,CAAC,IAAI,SAAS,UAAU,mBAAmB,EAAE,IAAI,MAAM,kBAAkB,KAAK,MAAM,KAAK,CAAC;AAAA,MACjH;AAGA,YAAM,EAAE,IAAI,MAAM,OAAO;AAEzB,YAAM,MAAM,KAAK;AAGjB,UAAI;AACJ,UAAI;AACF,wBAAgB,MAAM,MAAM,aAAa,MAAM,CAAC;AAAA,MAClD,QAAQ;AAGN,cAAM,iBAAiB,UAAU,CAAC,gCAAgC;AAClE,wBAAgB,IAAI,SAAS,MAAM,EAAE,QAAQ,IAAI,CAAC;AAAA,MACpD;AACA,mBAAa,UAAU;AAGvB,QAAE,MAAM,IAAI,SAAS,cAAc,MAAM;AAAA,QACvC,QAAQ,cAAc;AAAA,QACtB,YAAY,cAAc;AAAA,QAC1B,SAAS,IAAI,QAAQ,cAAc,OAAO;AAAA,MAC5C,CAAC;AAAA,IACH;AAEA,QAAI,aAAa,GAAG;AAClB,QAAE,IAAI,QAAQ,IAAI,SAAS,kBAAmB,OAAO,UAAU,CAAC;AAAA,IAClE;AAAA,EACF;AAEA,SAAO;AAAA,IACL,MAAM;AAAA,IACN,UAAU,SAAS;AAAA,IACnB,SAAS,SAAS,QAAQ,MAAM,OAAO;AAAA,IACvC,UAAU;AAAA,EACZ;AACF;","names":[]}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { g as PolicyConfig, P as Policy } from '../../protocol-2fD3DJrL.js';
|
|
2
|
+
import 'hono';
|
|
3
|
+
import '../sdk/trace.js';
|
|
4
|
+
import '@vivero/stoma-core';
|
|
5
|
+
|
|
6
|
+
interface TimeoutConfig extends PolicyConfig {
|
|
7
|
+
/** Timeout in milliseconds. Default: 30000. */
|
|
8
|
+
timeoutMs?: number;
|
|
9
|
+
/** Error message when timeout fires. */
|
|
10
|
+
message?: string;
|
|
11
|
+
/** HTTP status code when timeout fires. Default: 504. */
|
|
12
|
+
statusCode?: number;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Enforce a time budget for downstream execution.
|
|
16
|
+
*
|
|
17
|
+
* Races `next()` against a timer. If the timer fires first, throws a
|
|
18
|
+
* GatewayError (default 504). The timer is always cleaned up, even on
|
|
19
|
+
* downstream errors.
|
|
20
|
+
*
|
|
21
|
+
* @param config - Timeout duration and custom error message. Defaults to 30 seconds.
|
|
22
|
+
* @returns A {@link Policy} at priority 85 (runs late, close to upstream).
|
|
23
|
+
*
|
|
24
|
+
* @example
|
|
25
|
+
* ```ts
|
|
26
|
+
* // 5-second timeout with custom message
|
|
27
|
+
* timeout({ timeoutMs: 5000, message: "Upstream did not respond in time" });
|
|
28
|
+
* ```
|
|
29
|
+
*/
|
|
30
|
+
declare const timeout: (config?: TimeoutConfig | undefined) => Policy;
|
|
31
|
+
|
|
32
|
+
export { type TimeoutConfig, timeout };
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { GatewayError } from "../../core/errors";
|
|
2
|
+
import { definePolicy, Priority } from "../sdk";
|
|
3
|
+
const timeout = /* @__PURE__ */ definePolicy({
|
|
4
|
+
name: "timeout",
|
|
5
|
+
priority: Priority.TIMEOUT,
|
|
6
|
+
httpOnly: true,
|
|
7
|
+
defaults: { timeoutMs: 3e4, message: "Gateway timeout", statusCode: 504 },
|
|
8
|
+
handler: async (c, next, { config, trace }) => {
|
|
9
|
+
const controller = new AbortController();
|
|
10
|
+
const timer = setTimeout(() => controller.abort(), config.timeoutMs);
|
|
11
|
+
c.set("_timeoutSignal", controller.signal);
|
|
12
|
+
try {
|
|
13
|
+
const start = Date.now();
|
|
14
|
+
await Promise.race([
|
|
15
|
+
next(),
|
|
16
|
+
new Promise((_, reject) => {
|
|
17
|
+
controller.signal.addEventListener(
|
|
18
|
+
"abort",
|
|
19
|
+
() => reject(
|
|
20
|
+
new GatewayError(
|
|
21
|
+
config.statusCode,
|
|
22
|
+
"gateway_timeout",
|
|
23
|
+
config.message
|
|
24
|
+
)
|
|
25
|
+
)
|
|
26
|
+
);
|
|
27
|
+
})
|
|
28
|
+
]);
|
|
29
|
+
trace("passed", {
|
|
30
|
+
budgetMs: config.timeoutMs,
|
|
31
|
+
elapsed: Date.now() - start
|
|
32
|
+
});
|
|
33
|
+
} catch (err) {
|
|
34
|
+
if (err instanceof GatewayError && err.code === "gateway_timeout") {
|
|
35
|
+
trace("fired", { budgetMs: config.timeoutMs });
|
|
36
|
+
}
|
|
37
|
+
throw err;
|
|
38
|
+
} finally {
|
|
39
|
+
clearTimeout(timer);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
export {
|
|
44
|
+
timeout
|
|
45
|
+
};
|
|
46
|
+
//# sourceMappingURL=timeout.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../src/policies/resilience/timeout.ts"],"sourcesContent":["/**\n * Timeout policy - enforce a response time budget for downstream handlers.\n *\n * @module timeout\n */\nimport { GatewayError } from \"../../core/errors\";\nimport { definePolicy, Priority } from \"../sdk\";\nimport type { PolicyConfig } from \"../types\";\n\nexport interface TimeoutConfig extends PolicyConfig {\n /** Timeout in milliseconds. Default: 30000. */\n timeoutMs?: number;\n /** Error message when timeout fires. */\n message?: string;\n /** HTTP status code when timeout fires. Default: 504. */\n statusCode?: number;\n}\n\n/**\n * Enforce a time budget for downstream execution.\n *\n * Races `next()` against a timer. If the timer fires first, throws a\n * GatewayError (default 504). The timer is always cleaned up, even on\n * downstream errors.\n *\n * @param config - Timeout duration and custom error message. Defaults to 30 seconds.\n * @returns A {@link Policy} at priority 85 (runs late, close to upstream).\n *\n * @example\n * ```ts\n * // 5-second timeout with custom message\n * timeout({ timeoutMs: 5000, message: \"Upstream did not respond in time\" });\n * ```\n */\nexport const timeout = /*#__PURE__*/ definePolicy<TimeoutConfig>({\n name: \"timeout\",\n priority: Priority.TIMEOUT,\n httpOnly: true,\n defaults: { timeoutMs: 30_000, message: \"Gateway timeout\", statusCode: 504 },\n handler: async (c, next, { config, trace }) => {\n const controller = new AbortController();\n const timer = setTimeout(() => controller.abort(), config.timeoutMs!);\n\n // Store the abort signal on context so the URL upstream handler can\n // pass it to fetch(), enabling true cancellation of in-flight requests.\n c.set(\"_timeoutSignal\", controller.signal);\n\n try {\n const start = Date.now();\n await Promise.race([\n next(),\n new Promise<never>((_, reject) => {\n controller.signal.addEventListener(\"abort\", () =>\n reject(\n new GatewayError(\n config.statusCode!,\n \"gateway_timeout\",\n config.message!\n )\n )\n );\n }),\n ]);\n trace(\"passed\", {\n budgetMs: config.timeoutMs!,\n elapsed: Date.now() - start,\n });\n } catch (err) {\n if (err instanceof GatewayError && err.code === \"gateway_timeout\") {\n trace(\"fired\", { budgetMs: config.timeoutMs! });\n }\n throw err;\n } finally {\n clearTimeout(timer);\n }\n },\n});\n"],"mappings":"AAKA,SAAS,oBAAoB;AAC7B,SAAS,cAAc,gBAAgB;AA4BhC,MAAM,UAAwB,6BAA4B;AAAA,EAC/D,MAAM;AAAA,EACN,UAAU,SAAS;AAAA,EACnB,UAAU;AAAA,EACV,UAAU,EAAE,WAAW,KAAQ,SAAS,mBAAmB,YAAY,IAAI;AAAA,EAC3E,SAAS,OAAO,GAAG,MAAM,EAAE,QAAQ,MAAM,MAAM;AAC7C,UAAM,aAAa,IAAI,gBAAgB;AACvC,UAAM,QAAQ,WAAW,MAAM,WAAW,MAAM,GAAG,OAAO,SAAU;AAIpE,MAAE,IAAI,kBAAkB,WAAW,MAAM;AAEzC,QAAI;AACF,YAAM,QAAQ,KAAK,IAAI;AACvB,YAAM,QAAQ,KAAK;AAAA,QACjB,KAAK;AAAA,QACL,IAAI,QAAe,CAAC,GAAG,WAAW;AAChC,qBAAW,OAAO;AAAA,YAAiB;AAAA,YAAS,MAC1C;AAAA,cACE,IAAI;AAAA,gBACF,OAAO;AAAA,gBACP;AAAA,gBACA,OAAO;AAAA,cACT;AAAA,YACF;AAAA,UACF;AAAA,QACF,CAAC;AAAA,MACH,CAAC;AACD,YAAM,UAAU;AAAA,QACd,UAAU,OAAO;AAAA,QACjB,SAAS,KAAK,IAAI,IAAI;AAAA,MACxB,CAAC;AAAA,IACH,SAAS,KAAK;AACZ,UAAI,eAAe,gBAAgB,IAAI,SAAS,mBAAmB;AACjE,cAAM,SAAS,EAAE,UAAU,OAAO,UAAW,CAAC;AAAA,MAChD;AACA,YAAM;AAAA,IACR,UAAE;AACA,mBAAa,KAAK;AAAA,IACpB;AAAA,EACF;AACF,CAAC;","names":[]}
|