@apifuse/provider-sdk 2.2.0-beta.5 → 2.2.0-beta.50
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AUTHORING.md +537 -33
- package/CHANGELOG.md +201 -1
- package/README.md +83 -22
- package/SUBMISSION.md +2 -2
- package/bin/apifuse-check.ts +165 -11
- package/bin/apifuse-dev.ts +57 -13
- package/bin/apifuse-migrate-operation-declaration.ts +55 -0
- package/bin/apifuse-migrate-shape.ts +202 -0
- package/bin/apifuse-pack-check.ts +22 -2
- package/bin/apifuse-pack-smoke.ts +57 -2
- package/bin/apifuse-pack-types.ts +357 -38
- package/bin/apifuse-perf.ts +14 -13
- package/bin/apifuse-record.ts +709 -72
- package/bin/apifuse-submit-check.ts +2344 -324
- package/bin/apifuse-sync-assets.ts +117 -0
- package/bin/submit-check-delimited-text.ts +50 -0
- package/dist/auth-turn/index.d.ts +3 -3
- package/dist/auth-turn/index.js +1 -1
- package/dist/auth.d.ts +14 -0
- package/dist/auth.js +67 -0
- package/dist/ceremonies/index.d.ts +16 -0
- package/dist/ceremonies/index.js +141 -36
- package/dist/cli/commands.d.ts +1 -1
- package/dist/cli/commands.js +27 -0
- package/dist/cli/create.d.ts +4 -1
- package/dist/cli/create.js +42 -37
- package/dist/cli/migrate-operation-declaration.d.ts +59 -0
- package/dist/cli/migrate-operation-declaration.js +1178 -0
- package/dist/cli/migrate-operation-shape.d.ts +44 -0
- package/dist/cli/migrate-operation-shape.js +113 -0
- package/dist/cli/migrate-provider-shape.d.ts +52 -0
- package/dist/cli/migrate-provider-shape.js +578 -0
- package/dist/cli/prompt-assets.d.ts +80 -0
- package/dist/cli/prompt-assets.js +743 -0
- package/dist/cli/templates/provider/AGENTS.md.tpl +17 -8
- package/dist/cli/templates/provider/Dockerfile.tpl +1 -1
- package/dist/cli/templates/provider/README.md.tpl +5 -5
- package/dist/cli/templates/provider/index.ts.tpl +6 -3
- package/dist/cli/templates/provider/operations/ping.ts.tpl +4 -1
- package/dist/cli/templates/provider/provider.json.tpl +6 -0
- package/dist/config/loader.d.ts +179 -16
- package/dist/config/loader.js +435 -127
- package/dist/contract-serialization.js +4 -8
- package/dist/contract-types.d.ts +12 -5
- package/dist/contract.js +24 -10
- package/dist/declaration-validation.d.ts +33 -0
- package/dist/declaration-validation.js +266 -0
- package/dist/define.d.ts +66 -29
- package/dist/define.js +778 -69
- package/dist/dev.d.ts +3 -0
- package/dist/dev.js +1 -1
- package/dist/engine.d.ts +78 -0
- package/dist/engine.js +133 -0
- package/dist/error-observability.d.ts +7 -0
- package/dist/error-observability.js +61 -0
- package/dist/error-resolution.d.ts +4 -0
- package/dist/error-resolution.js +122 -0
- package/dist/errors.d.ts +33 -0
- package/dist/errors.js +40 -0
- package/dist/fixture-sanitization.d.ts +28 -0
- package/dist/fixture-sanitization.js +227 -0
- package/dist/health-scenario.d.ts +1842 -0
- package/dist/health-scenario.js +624 -0
- package/dist/index.d.ts +21 -10
- package/dist/index.js +12 -7
- package/dist/lint.d.ts +23 -11
- package/dist/lint.js +572 -69
- package/dist/native-address.d.ts +43 -0
- package/dist/native-address.js +281 -0
- package/dist/native-egress-policy.d.ts +31 -0
- package/dist/native-egress-policy.js +288 -0
- package/dist/observability.d.ts +5 -2
- package/dist/observability.js +48 -1
- package/dist/provider.d.ts +10 -2
- package/dist/provider.js +4 -1
- package/dist/runtime/auth-flow.d.ts +5 -1
- package/dist/runtime/auth-flow.js +6 -0
- package/dist/runtime/browser.d.ts +1 -0
- package/dist/runtime/browser.js +492 -49
- package/dist/runtime/cache.d.ts +1 -0
- package/dist/runtime/cache.js +169 -15
- package/dist/runtime/choice-wordlist.d.ts +9 -0
- package/dist/runtime/choice-wordlist.js +138 -0
- package/dist/runtime/choice.d.ts +13 -1
- package/dist/runtime/choice.js +490 -102
- package/dist/runtime/chrome149-header-order.d.ts +58 -0
- package/dist/runtime/chrome149-header-order.js +289 -0
- package/dist/runtime/env.js +12 -0
- package/dist/runtime/executor.d.ts +3 -2
- package/dist/runtime/executor.js +26 -25
- package/dist/runtime/http.d.ts +1 -0
- package/dist/runtime/http.js +515 -53
- package/dist/runtime/insights.js +2 -2
- package/dist/runtime/instrumentation.d.ts +2 -2
- package/dist/runtime/instrumentation.js +366 -8
- package/dist/runtime/native-network-errors.d.ts +33 -0
- package/dist/runtime/native-network-errors.js +69 -0
- package/dist/runtime/native-network.d.ts +96 -0
- package/dist/runtime/native-network.js +1232 -0
- package/dist/runtime/ocr.d.ts +29 -0
- package/dist/runtime/ocr.js +440 -0
- package/dist/runtime/otlp.d.ts +71 -2
- package/dist/runtime/otlp.js +397 -16
- package/dist/runtime/proxy-errors.js +6 -2
- package/dist/runtime/proxy-nodemaven.d.ts +56 -0
- package/dist/runtime/proxy-nodemaven.js +146 -0
- package/dist/runtime/proxy-telemetry.d.ts +80 -1
- package/dist/runtime/proxy-telemetry.js +154 -47
- package/dist/runtime/redirects.d.ts +29 -0
- package/dist/runtime/redirects.js +36 -0
- package/dist/runtime/redis.d.ts +1 -1
- package/dist/runtime/redis.js +4 -2
- package/dist/runtime/request-options.d.ts +68 -1
- package/dist/runtime/request-options.js +548 -0
- package/dist/runtime/resolver-config.d.ts +6 -0
- package/dist/runtime/resolver-config.js +6 -0
- package/dist/runtime/resolver-public.d.ts +1 -0
- package/dist/runtime/resolver-public.js +1 -0
- package/dist/runtime/resolver-shared.d.ts +14 -0
- package/dist/runtime/resolver-shared.js +12 -0
- package/dist/runtime/resolver-vendors/bindings.d.ts +48 -0
- package/dist/runtime/resolver-vendors/bindings.js +40 -0
- package/dist/runtime/resolver-vendors/browser.d.ts +22 -0
- package/dist/runtime/resolver-vendors/browser.js +377 -0
- package/dist/runtime/resolver-vendors/capsolver.d.ts +22 -0
- package/dist/runtime/resolver-vendors/capsolver.js +531 -0
- package/dist/runtime/resolver-vendors/hosts.d.ts +2 -0
- package/dist/runtime/resolver-vendors/hosts.js +33 -0
- package/dist/runtime/resolver-vendors/twocaptcha.d.ts +24 -0
- package/dist/runtime/resolver-vendors/twocaptcha.js +408 -0
- package/dist/runtime/resolver-vendors/types.d.ts +94 -0
- package/dist/runtime/resolver-vendors/types.js +96 -0
- package/dist/runtime/resolver.d.ts +72 -0
- package/dist/runtime/resolver.js +771 -0
- package/dist/runtime/secrets.d.ts +27 -0
- package/dist/runtime/secrets.js +51 -0
- package/dist/runtime/state.d.ts +3 -0
- package/dist/runtime/state.js +277 -71
- package/dist/runtime/stealth-cookies.d.ts +20 -0
- package/dist/runtime/stealth-cookies.js +111 -0
- package/dist/runtime/stealth.d.ts +41 -5
- package/dist/runtime/stealth.js +997 -291
- package/dist/runtime/stt.js +1 -12
- package/dist/runtime/timeout.d.ts +5 -0
- package/dist/runtime/timeout.js +12 -0
- package/dist/runtime/trace-config.d.ts +12 -0
- package/dist/runtime/trace-config.js +62 -0
- package/dist/runtime/trace.d.ts +5 -0
- package/dist/runtime/trace.js +43 -10
- package/dist/serve.d.ts +1 -1
- package/dist/serve.js +1 -1
- package/dist/server/error-observability.d.ts +1 -0
- package/dist/server/error-observability.js +1 -0
- package/dist/server/index.d.ts +5 -2
- package/dist/server/index.js +2 -2
- package/dist/server/self-test-input-tokens.d.ts +2 -1
- package/dist/server/self-test-input-tokens.js +18 -14
- package/dist/server/self-test.d.ts +14 -3
- package/dist/server/self-test.js +129 -58
- package/dist/server/serve-implementation.d.ts +230 -0
- package/dist/server/serve-implementation.js +2265 -0
- package/dist/server/serve.d.ts +1 -70
- package/dist/server/serve.js +1 -1131
- package/dist/server/trace-output.d.ts +6 -0
- package/dist/server/trace-output.js +79 -0
- package/dist/server/types.d.ts +30 -5
- package/dist/server/types.js +13 -1
- package/dist/stateful/errors.d.ts +19 -0
- package/dist/stateful/errors.js +24 -0
- package/dist/stateful/http-provider-event-emitter.d.ts +40 -0
- package/dist/stateful/http-provider-event-emitter.js +237 -0
- package/dist/stateful/http-session-owner-registry.d.ts +44 -0
- package/dist/stateful/http-session-owner-registry.js +210 -0
- package/dist/stateful/index.d.ts +18 -0
- package/dist/stateful/index.js +18 -0
- package/dist/stateful/provider-event-delivery-failures.d.ts +32 -0
- package/dist/stateful/provider-event-delivery-failures.js +43 -0
- package/dist/stateful/provider-event-pipeline-metrics.d.ts +46 -0
- package/dist/stateful/provider-event-pipeline-metrics.js +48 -0
- package/dist/stateful/provider-event-pipeline.d.ts +50 -0
- package/dist/stateful/provider-event-pipeline.js +1 -0
- package/dist/stateful/provider-events.d.ts +101 -0
- package/dist/stateful/provider-events.js +289 -0
- package/dist/stateful/session-key.d.ts +15 -0
- package/dist/stateful/session-key.js +86 -0
- package/dist/stateful/stateful-provider-adapter-context.d.ts +5 -0
- package/dist/stateful/stateful-provider-adapter-context.js +42 -0
- package/dist/stateful/stateful-provider-adapter-metrics.d.ts +15 -0
- package/dist/stateful/stateful-provider-adapter-metrics.js +21 -0
- package/dist/stateful/stateful-provider-adapter.d.ts +98 -0
- package/dist/stateful/stateful-provider-adapter.js +287 -0
- package/dist/stateful/stateful-provider-observability.d.ts +62 -0
- package/dist/stateful/stateful-provider-observability.js +161 -0
- package/dist/stateful/stateful-provider-owner-forwarder.d.ts +41 -0
- package/dist/stateful/stateful-provider-owner-forwarder.js +215 -0
- package/dist/stateful/stateful-provider-runtime-context.d.ts +32 -0
- package/dist/stateful/stateful-provider-runtime-context.js +60 -0
- package/dist/stateful/stateful-provider-runtime-executor.d.ts +34 -0
- package/dist/stateful/stateful-provider-runtime-executor.js +52 -0
- package/dist/stateful/stateful-provider-session-routing.d.ts +67 -0
- package/dist/stateful/stateful-provider-session-routing.js +345 -0
- package/dist/stateful/stateful-provider-session-runtime.d.ts +98 -0
- package/dist/stateful/stateful-provider-session-runtime.js +245 -0
- package/dist/stateful-signing.d.ts +18 -0
- package/dist/stateful-signing.js +27 -0
- package/dist/stealth/profiles.d.ts +9 -4
- package/dist/stealth/profiles.js +160 -211
- package/dist/stream-evidence.d.ts +74 -0
- package/dist/stream-evidence.js +785 -0
- package/dist/stream.js +7 -1
- package/dist/testing/index.d.ts +2 -1
- package/dist/testing/index.js +2 -1
- package/dist/testing/run.d.ts +32 -2
- package/dist/testing/run.js +489 -21
- package/dist/trace-sanitization.d.ts +5 -0
- package/dist/trace-sanitization.js +45 -0
- package/dist/types.d.ts +673 -131
- package/dist/types.js +1 -0
- package/package.json +45 -5
- package/src/auth-turn/index.ts +1 -1
- package/src/auth.ts +118 -0
- package/src/ceremonies/index.ts +189 -46
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/approval-override.ts.txt +6 -0
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/codemod-syntax.ts.txt +3 -0
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/connection-precedence.ts.txt +10 -0
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/docs-conflict.ts.txt +8 -0
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/examples-map.ts.txt +5 -0
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/examples-operation.ts.txt +16 -0
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/factory-map.ts.txt +3 -0
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/hoist-all.ts.txt +31 -0
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/hoisted-const.ts.txt +11 -0
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/imported-spread.ts.txt +11 -0
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/inline-map.ts.txt +11 -0
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/inline-spread-cast-tail.ts.txt +21 -0
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/inline-spread-ekitan.ts.txt +11 -0
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/inline-spread-override.ts.txt +14 -0
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/missing-english-locale.ts.txt +7 -0
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/no-safety.ts.txt +6 -0
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/non-literal.ts.txt +7 -0
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/redundant-approval.ts.txt +6 -0
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/safety-conflict.ts.txt +7 -0
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/stream.ts.txt +7 -0
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/tool-router-spread.ts.txt +15 -0
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/unparseable.ts.txt +4 -0
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/verbatim-template.ts.txt +12 -0
- package/src/cli/commands.ts +33 -0
- package/src/cli/create.ts +50 -37
- package/src/cli/migrate-operation-declaration.ts +1654 -0
- package/src/cli/migrate-operation-shape.ts +184 -0
- package/src/cli/migrate-provider-shape.ts +772 -0
- package/src/cli/prompt-assets.ts +865 -0
- package/src/cli/templates/provider/AGENTS.md.tpl +17 -8
- package/src/cli/templates/provider/Dockerfile.tpl +1 -1
- package/src/cli/templates/provider/README.md.tpl +5 -5
- package/src/cli/templates/provider/index.ts.tpl +6 -3
- package/src/cli/templates/provider/operations/ping.ts.tpl +4 -1
- package/src/cli/templates/provider/provider.json.tpl +6 -0
- package/src/config/loader.ts +690 -163
- package/src/contract-serialization.ts +5 -7
- package/src/contract-types.ts +12 -5
- package/src/contract.ts +24 -10
- package/src/declaration-validation.ts +330 -0
- package/src/define.ts +1048 -131
- package/src/dev.ts +4 -1
- package/src/engine.ts +279 -0
- package/src/error-observability.ts +64 -0
- package/src/error-resolution.ts +127 -0
- package/src/errors.ts +68 -0
- package/src/fixture-sanitization.ts +264 -0
- package/src/health-scenario.ts +875 -0
- package/src/index.ts +220 -13
- package/src/lint.ts +698 -90
- package/src/native-address.ts +340 -0
- package/src/native-egress-policy.ts +358 -0
- package/src/observability.ts +51 -1
- package/src/provider.ts +161 -3
- package/src/runtime/auth-flow.ts +12 -0
- package/src/runtime/browser.ts +661 -63
- package/src/runtime/cache.ts +189 -14
- package/src/runtime/choice-wordlist.ts +145 -0
- package/src/runtime/choice.ts +631 -120
- package/src/runtime/chrome149-header-order.ts +330 -0
- package/src/runtime/env.ts +13 -0
- package/src/runtime/executor.ts +43 -31
- package/src/runtime/http.ts +641 -61
- package/src/runtime/insights.ts +2 -2
- package/src/runtime/instrumentation.ts +520 -15
- package/src/runtime/native-network-errors.ts +99 -0
- package/src/runtime/native-network.ts +1605 -0
- package/src/runtime/ocr.ts +523 -0
- package/src/runtime/otlp.ts +467 -21
- package/src/runtime/proxy-errors.ts +12 -4
- package/src/runtime/proxy-nodemaven.ts +221 -0
- package/src/runtime/proxy-telemetry.ts +244 -75
- package/src/runtime/redirects.ts +66 -0
- package/src/runtime/redis.ts +7 -2
- package/src/runtime/request-options.ts +680 -1
- package/src/runtime/resolver-config.ts +6 -0
- package/src/runtime/resolver-public.ts +23 -0
- package/src/runtime/resolver-shared.ts +33 -0
- package/src/runtime/resolver-vendors/bindings.ts +56 -0
- package/src/runtime/resolver-vendors/browser.ts +533 -0
- package/src/runtime/resolver-vendors/capsolver.ts +707 -0
- package/src/runtime/resolver-vendors/hosts.ts +38 -0
- package/src/runtime/resolver-vendors/twocaptcha.ts +540 -0
- package/src/runtime/resolver-vendors/types.ts +212 -0
- package/src/runtime/resolver.ts +1157 -0
- package/src/runtime/secrets.ts +64 -0
- package/src/runtime/state.ts +394 -77
- package/src/runtime/stealth-cookies.ts +132 -0
- package/src/runtime/stealth.ts +1280 -336
- package/src/runtime/stt.ts +1 -19
- package/src/runtime/timeout.ts +18 -0
- package/src/runtime/trace-config.ts +78 -0
- package/src/runtime/trace.ts +57 -17
- package/src/serve.ts +6 -1
- package/src/server/error-observability.ts +1 -0
- package/src/server/index.ts +34 -2
- package/src/server/self-test-input-tokens.ts +29 -14
- package/src/server/self-test.ts +191 -68
- package/src/server/serve-implementation.ts +3378 -0
- package/src/server/serve.ts +1 -1632
- package/src/server/trace-output.ts +129 -0
- package/src/server/types.ts +13 -1
- package/src/stateful/README.md +146 -0
- package/src/stateful/errors.ts +35 -0
- package/src/stateful/http-provider-event-emitter.ts +314 -0
- package/src/stateful/http-session-owner-registry.ts +306 -0
- package/src/stateful/index.ts +18 -0
- package/src/stateful/provider-event-delivery-failures.ts +80 -0
- package/src/stateful/provider-event-pipeline-metrics.ts +95 -0
- package/src/stateful/provider-event-pipeline.ts +61 -0
- package/src/stateful/provider-events.ts +462 -0
- package/src/stateful/session-key.ts +111 -0
- package/src/stateful/stateful-provider-adapter-context.ts +59 -0
- package/src/stateful/stateful-provider-adapter-metrics.ts +48 -0
- package/src/stateful/stateful-provider-adapter.ts +562 -0
- package/src/stateful/stateful-provider-observability.ts +261 -0
- package/src/stateful/stateful-provider-owner-forwarder.ts +287 -0
- package/src/stateful/stateful-provider-runtime-context.ts +92 -0
- package/src/stateful/stateful-provider-runtime-executor.ts +96 -0
- package/src/stateful/stateful-provider-session-routing.ts +546 -0
- package/src/stateful/stateful-provider-session-runtime.ts +403 -0
- package/src/stateful-signing.ts +46 -0
- package/src/stealth/profiles.ts +202 -231
- package/src/stream-evidence.ts +988 -0
- package/src/stream.ts +8 -1
- package/src/testing/index.ts +10 -1
- package/src/testing/run.ts +658 -15
- package/src/trace-sanitization.ts +63 -0
- package/src/types.ts +788 -174
- package/dist/cli/templates/provider/CLAUDE.md.tpl +0 -1
- package/src/cli/templates/provider/CLAUDE.md.tpl +0 -1
- /package/dist/cli/templates/provider/{skills → .agents/skills}/fixtures-and-recording/SKILL.md.tpl +0 -0
- /package/dist/cli/templates/provider/{skills → .agents/skills}/health-checks-and-fail-closed/SKILL.md.tpl +0 -0
- /package/dist/cli/templates/provider/{skills → .agents/skills}/normalization-standards/SKILL.md.tpl +0 -0
- /package/dist/cli/templates/provider/{skills → .agents/skills}/pagination-and-counts/SKILL.md.tpl +0 -0
- /package/dist/cli/templates/provider/{skills → .agents/skills}/upstream-contract-verification/SKILL.md.tpl +0 -0
- /package/dist/cli/templates/provider/{skills → .agents/skills}/upstream-notes/README.md.tpl +0 -0
- /package/src/cli/templates/provider/{skills → .agents/skills}/fixtures-and-recording/SKILL.md.tpl +0 -0
- /package/src/cli/templates/provider/{skills → .agents/skills}/health-checks-and-fail-closed/SKILL.md.tpl +0 -0
- /package/src/cli/templates/provider/{skills → .agents/skills}/normalization-standards/SKILL.md.tpl +0 -0
- /package/src/cli/templates/provider/{skills → .agents/skills}/pagination-and-counts/SKILL.md.tpl +0 -0
- /package/src/cli/templates/provider/{skills → .agents/skills}/upstream-contract-verification/SKILL.md.tpl +0 -0
- /package/src/cli/templates/provider/{skills → .agents/skills}/upstream-notes/README.md.tpl +0 -0
|
@@ -0,0 +1,345 @@
|
|
|
1
|
+
import { StatefulRoutingDeadlineError } from "./errors.js";
|
|
2
|
+
import { NOOP_STATEFUL_PROVIDER_METRIC_EMITTER, } from "./stateful-provider-observability.js";
|
|
3
|
+
export { forwardingContextFromStatefulRuntimeContext, isStatefulOperationRuntimeContext, providerContextFromStatefulRuntimeContext, statefulForwardingContextFromProviderRequest, withStatefulLocalProviderContext, } from "./stateful-provider-runtime-context.js";
|
|
4
|
+
export { StatefulRoutingDeadlineError } from "./errors.js";
|
|
5
|
+
export class StatefulRoutingOwnershipError extends Error {
|
|
6
|
+
requestId;
|
|
7
|
+
sessionKey;
|
|
8
|
+
constructor(requestId, sessionKey, message) {
|
|
9
|
+
super(message);
|
|
10
|
+
this.name = "StatefulRoutingOwnershipError";
|
|
11
|
+
this.requestId = requestId;
|
|
12
|
+
this.sessionKey = sessionKey;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
const STATEFUL_SESSION_ESTABLISHMENT_FAILURE = Symbol.for("@apifuse/provider-sdk/stateful/session-establishment-failure@1");
|
|
16
|
+
function isStatefulSessionEstablishmentFailure(error) {
|
|
17
|
+
if ((typeof error !== "object" || error === null) && typeof error !== "function")
|
|
18
|
+
return false;
|
|
19
|
+
return (Object.getOwnPropertyDescriptor(error, STATEFUL_SESSION_ESTABLISHMENT_FAILURE)?.value === true);
|
|
20
|
+
}
|
|
21
|
+
export class StatefulSessionRouter {
|
|
22
|
+
#currentPod;
|
|
23
|
+
#registry;
|
|
24
|
+
#forwarder;
|
|
25
|
+
#executor;
|
|
26
|
+
#leaseDurationMs;
|
|
27
|
+
#leaseRenewIntervalMs;
|
|
28
|
+
#clock;
|
|
29
|
+
#metricEmitter;
|
|
30
|
+
#managedLeases = new Map();
|
|
31
|
+
#latestLocalAttempts = new Map();
|
|
32
|
+
#establishmentQueues = new Map();
|
|
33
|
+
#nextLocalAttempt = 0;
|
|
34
|
+
constructor(options) {
|
|
35
|
+
validateRouterLeaseDuration(options.leaseDurationMs);
|
|
36
|
+
const renewalFraction = options.leaseRenewalFraction ?? 1 / 3;
|
|
37
|
+
validateLeaseRenewalFraction(renewalFraction);
|
|
38
|
+
this.#currentPod = options.currentPod;
|
|
39
|
+
this.#registry = options.registry;
|
|
40
|
+
this.#forwarder = options.forwarder;
|
|
41
|
+
this.#executor = options.executor;
|
|
42
|
+
this.#leaseDurationMs = options.leaseDurationMs;
|
|
43
|
+
this.#leaseRenewIntervalMs = Math.max(1, Math.floor(options.leaseDurationMs * renewalFraction));
|
|
44
|
+
this.#clock = options.clock ?? (() => new Date());
|
|
45
|
+
this.#metricEmitter = options.metricEmitter ?? NOOP_STATEFUL_PROVIDER_METRIC_EMITTER;
|
|
46
|
+
}
|
|
47
|
+
async route(request) {
|
|
48
|
+
const now = this.#clock();
|
|
49
|
+
if (isExpiredDeadline(request, now)) {
|
|
50
|
+
this.#metricEmitter.increment("apifuse_stateful_provider_routing_deadline_expired_total", {
|
|
51
|
+
requestId: request.requestId,
|
|
52
|
+
providerId: request.providerId,
|
|
53
|
+
connectionId: request.connectionId,
|
|
54
|
+
serviceAccountId: request.serviceAccountId,
|
|
55
|
+
operationId: request.operationId,
|
|
56
|
+
sessionKey: request.sessionKey,
|
|
57
|
+
redactedErrorCode: "deadline_expired",
|
|
58
|
+
});
|
|
59
|
+
throw new StatefulRoutingDeadlineError(request.requestId, request.deadlineAt);
|
|
60
|
+
}
|
|
61
|
+
const deadlineSignal = makeDeadlineSignal(request, now);
|
|
62
|
+
try {
|
|
63
|
+
const route = await this.runEstablishmentTask(request.sessionKey, async () => {
|
|
64
|
+
const resolved = await this.#registry.resolve(request.sessionKey, now, deadlineSignal.signal);
|
|
65
|
+
if (resolved) {
|
|
66
|
+
validateOwnerGeneration(resolved);
|
|
67
|
+
return this.prepareRouteToOwner(request, resolved, deadlineSignal.signal);
|
|
68
|
+
}
|
|
69
|
+
const acquired = await this.#registry.acquire({
|
|
70
|
+
sessionKey: request.sessionKey,
|
|
71
|
+
ownerPodId: this.#currentPod.podId,
|
|
72
|
+
ownerEndpoint: this.#currentPod.endpoint,
|
|
73
|
+
leaseDurationMs: this.#leaseDurationMs,
|
|
74
|
+
status: "acquiring",
|
|
75
|
+
now,
|
|
76
|
+
}, deadlineSignal.signal);
|
|
77
|
+
validateOwnerGeneration(acquired.record);
|
|
78
|
+
this.#metricEmitter.increment("apifuse_stateful_provider_routing_reacquire_total", metricLabels(request, acquired.record));
|
|
79
|
+
return this.prepareRouteToOwner(request, acquired.record, deadlineSignal.signal);
|
|
80
|
+
});
|
|
81
|
+
return await this.routeToOwner(request, route, deadlineSignal);
|
|
82
|
+
}
|
|
83
|
+
finally {
|
|
84
|
+
deadlineSignal.cleanup();
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
async release() {
|
|
88
|
+
const sessionKeys = new Set([
|
|
89
|
+
...this.#managedLeases.keys(),
|
|
90
|
+
...this.#latestLocalAttempts.keys(),
|
|
91
|
+
]);
|
|
92
|
+
await Promise.all([...sessionKeys].map((key) => this.releaseSession(key)));
|
|
93
|
+
}
|
|
94
|
+
async releaseSession(sessionKey) {
|
|
95
|
+
return this.runEstablishmentTask(sessionKey, async () => {
|
|
96
|
+
try {
|
|
97
|
+
const managed = this.#managedLeases.get(sessionKey);
|
|
98
|
+
if (!managed)
|
|
99
|
+
return false;
|
|
100
|
+
this.stopManagedLease(managed);
|
|
101
|
+
this.#managedLeases.delete(sessionKey);
|
|
102
|
+
return await this.#registry.release({
|
|
103
|
+
sessionKey,
|
|
104
|
+
ownerPodId: managed.record.ownerPodId,
|
|
105
|
+
generation: managed.record.generation,
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
finally {
|
|
109
|
+
this.#latestLocalAttempts.delete(sessionKey);
|
|
110
|
+
}
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
/** @internal Test-only visibility into local-attempt lifecycle cleanup. */
|
|
114
|
+
get latestLocalAttemptCountForTesting() {
|
|
115
|
+
return this.#latestLocalAttempts.size;
|
|
116
|
+
}
|
|
117
|
+
async prepareRouteToOwner(request, owner, signal) {
|
|
118
|
+
if (owner.sessionKey !== request.sessionKey) {
|
|
119
|
+
throw new StatefulRoutingOwnershipError(request.requestId, request.sessionKey, "Resolved stateful session owner does not match request session key.");
|
|
120
|
+
}
|
|
121
|
+
if (owner.ownerPodId !== this.#currentPod.podId) {
|
|
122
|
+
this.forgetManagedLease(request.sessionKey);
|
|
123
|
+
this.#metricEmitter.increment("apifuse_stateful_provider_routing_forward_total", metricLabels(request, owner));
|
|
124
|
+
return { owner };
|
|
125
|
+
}
|
|
126
|
+
const localAttempt = ++this.#nextLocalAttempt;
|
|
127
|
+
this.#latestLocalAttempts.set(request.sessionKey, localAttempt);
|
|
128
|
+
try {
|
|
129
|
+
const validatedOwner = await this.validateLocalOwnership(request, owner, signal, false);
|
|
130
|
+
this.ensureManagedLease(request.sessionKey, validatedOwner);
|
|
131
|
+
this.#metricEmitter.increment("apifuse_stateful_provider_routing_local_total", metricLabels(request, validatedOwner));
|
|
132
|
+
return { owner: validatedOwner, localAttempt };
|
|
133
|
+
}
|
|
134
|
+
catch (error) {
|
|
135
|
+
if (this.#latestLocalAttempts.get(request.sessionKey) === localAttempt) {
|
|
136
|
+
this.#latestLocalAttempts.delete(request.sessionKey);
|
|
137
|
+
}
|
|
138
|
+
throw error;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
async routeToOwner(request, route, deadlineSignal) {
|
|
142
|
+
const { owner, localAttempt } = route;
|
|
143
|
+
if (localAttempt !== undefined) {
|
|
144
|
+
try {
|
|
145
|
+
return await this.#executor.executeLocal(request, owner, deadlineSignal.signal, (expected, signal) => this.runEstablishmentTask(request.sessionKey, () => this.validateLocalOwnership(request, expected, signal, true)));
|
|
146
|
+
}
|
|
147
|
+
catch (error) {
|
|
148
|
+
if (isStatefulSessionEstablishmentFailure(error)) {
|
|
149
|
+
await this.releaseFailedEstablishment(request.sessionKey, owner, localAttempt);
|
|
150
|
+
}
|
|
151
|
+
throw error;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
return this.#forwarder.forward(owner, request, deadlineSignal.signal);
|
|
155
|
+
}
|
|
156
|
+
ensureManagedLease(sessionKey, owner) {
|
|
157
|
+
const current = this.#managedLeases.get(sessionKey);
|
|
158
|
+
if (current &&
|
|
159
|
+
current.record.ownerPodId === owner.ownerPodId &&
|
|
160
|
+
current.record.generation === owner.generation) {
|
|
161
|
+
current.record = owner;
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
if (current)
|
|
165
|
+
this.stopManagedLease(current);
|
|
166
|
+
const managed = { sessionKey, record: owner };
|
|
167
|
+
this.#managedLeases.set(sessionKey, managed);
|
|
168
|
+
this.scheduleRenewal(managed);
|
|
169
|
+
}
|
|
170
|
+
scheduleRenewal(managed) {
|
|
171
|
+
managed.timer = setTimeout(() => void this.renewManagedLease(managed), this.#leaseRenewIntervalMs);
|
|
172
|
+
managed.timer.unref?.();
|
|
173
|
+
}
|
|
174
|
+
async renewManagedLease(managed) {
|
|
175
|
+
if (this.#managedLeases.get(managed.sessionKey) !== managed)
|
|
176
|
+
return;
|
|
177
|
+
try {
|
|
178
|
+
const renewed = await this.#registry.renew({
|
|
179
|
+
sessionKey: managed.sessionKey,
|
|
180
|
+
ownerPodId: managed.record.ownerPodId,
|
|
181
|
+
generation: managed.record.generation,
|
|
182
|
+
leaseDurationMs: this.#leaseDurationMs,
|
|
183
|
+
now: this.#clock(),
|
|
184
|
+
});
|
|
185
|
+
if (!renewed) {
|
|
186
|
+
this.forgetManagedLease(managed.sessionKey);
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
validateOwnerGeneration(renewed);
|
|
190
|
+
managed.record = renewed;
|
|
191
|
+
}
|
|
192
|
+
catch {
|
|
193
|
+
// A transient control-plane failure is retried on the next renewal interval.
|
|
194
|
+
}
|
|
195
|
+
finally {
|
|
196
|
+
if (this.#managedLeases.get(managed.sessionKey) === managed) {
|
|
197
|
+
this.scheduleRenewal(managed);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
async validateLocalOwnership(request, expected, signal, markConnected) {
|
|
202
|
+
const resolved = await this.#registry.resolve(request.sessionKey, this.#clock(), signal);
|
|
203
|
+
if (!sameOwner(resolved, expected)) {
|
|
204
|
+
this.forgetManagedLease(request.sessionKey);
|
|
205
|
+
throw ownershipChanged(request);
|
|
206
|
+
}
|
|
207
|
+
if (!markConnected)
|
|
208
|
+
return resolved;
|
|
209
|
+
const connected = await this.#registry.renew({
|
|
210
|
+
sessionKey: request.sessionKey,
|
|
211
|
+
ownerPodId: resolved.ownerPodId,
|
|
212
|
+
generation: resolved.generation,
|
|
213
|
+
leaseDurationMs: this.#leaseDurationMs,
|
|
214
|
+
status: "connected",
|
|
215
|
+
now: this.#clock(),
|
|
216
|
+
}, signal);
|
|
217
|
+
if (!sameOwner(connected, expected)) {
|
|
218
|
+
this.forgetManagedLease(request.sessionKey);
|
|
219
|
+
throw ownershipChanged(request);
|
|
220
|
+
}
|
|
221
|
+
validateOwnerGeneration(connected);
|
|
222
|
+
this.ensureManagedLease(request.sessionKey, connected);
|
|
223
|
+
return connected;
|
|
224
|
+
}
|
|
225
|
+
forgetManagedLease(sessionKey) {
|
|
226
|
+
this.#latestLocalAttempts.delete(sessionKey);
|
|
227
|
+
const managed = this.#managedLeases.get(sessionKey);
|
|
228
|
+
if (!managed)
|
|
229
|
+
return;
|
|
230
|
+
this.stopManagedLease(managed);
|
|
231
|
+
this.#managedLeases.delete(sessionKey);
|
|
232
|
+
}
|
|
233
|
+
async releaseFailedEstablishment(sessionKey, owner, localAttempt) {
|
|
234
|
+
await this.runEstablishmentTask(sessionKey, async () => {
|
|
235
|
+
if (this.#latestLocalAttempts.get(sessionKey) !== localAttempt)
|
|
236
|
+
return;
|
|
237
|
+
try {
|
|
238
|
+
const managed = this.#managedLeases.get(sessionKey);
|
|
239
|
+
if (managed &&
|
|
240
|
+
managed.record.ownerPodId === owner.ownerPodId &&
|
|
241
|
+
managed.record.generation === owner.generation) {
|
|
242
|
+
this.stopManagedLease(managed);
|
|
243
|
+
this.#managedLeases.delete(sessionKey);
|
|
244
|
+
}
|
|
245
|
+
try {
|
|
246
|
+
await this.#registry.release({
|
|
247
|
+
sessionKey,
|
|
248
|
+
ownerPodId: owner.ownerPodId,
|
|
249
|
+
generation: owner.generation,
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
catch (error) {
|
|
253
|
+
try {
|
|
254
|
+
console.error(JSON.stringify({
|
|
255
|
+
event: "stateful_session_establishment_lease_release_failed",
|
|
256
|
+
sessionKey: owner.sessionKey,
|
|
257
|
+
ownerPodId: owner.ownerPodId,
|
|
258
|
+
generation: owner.generation,
|
|
259
|
+
errorClass: error instanceof Error ? error.name : "UnknownError",
|
|
260
|
+
}));
|
|
261
|
+
}
|
|
262
|
+
catch { }
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
finally {
|
|
266
|
+
if (this.#latestLocalAttempts.get(sessionKey) === localAttempt) {
|
|
267
|
+
this.#latestLocalAttempts.delete(sessionKey);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
async runEstablishmentTask(sessionKey, task) {
|
|
273
|
+
const previous = this.#establishmentQueues.get(sessionKey) ?? Promise.resolve();
|
|
274
|
+
const next = previous.then(task, task);
|
|
275
|
+
const settled = next.then(() => undefined, () => undefined);
|
|
276
|
+
this.#establishmentQueues.set(sessionKey, settled);
|
|
277
|
+
try {
|
|
278
|
+
return await next;
|
|
279
|
+
}
|
|
280
|
+
finally {
|
|
281
|
+
if (this.#establishmentQueues.get(sessionKey) === settled) {
|
|
282
|
+
this.#establishmentQueues.delete(sessionKey);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
stopManagedLease(managed) {
|
|
287
|
+
if (managed.timer)
|
|
288
|
+
clearTimeout(managed.timer);
|
|
289
|
+
managed.timer = undefined;
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
function metricLabels(request, owner) {
|
|
293
|
+
return {
|
|
294
|
+
requestId: request.requestId,
|
|
295
|
+
providerId: request.providerId,
|
|
296
|
+
connectionId: request.connectionId,
|
|
297
|
+
serviceAccountId: request.serviceAccountId,
|
|
298
|
+
operationId: request.operationId,
|
|
299
|
+
sessionKey: request.sessionKey,
|
|
300
|
+
ownerPodId: owner.ownerPodId,
|
|
301
|
+
generation: owner.generation,
|
|
302
|
+
};
|
|
303
|
+
}
|
|
304
|
+
function validateRouterLeaseDuration(leaseDurationMs) {
|
|
305
|
+
if (!Number.isFinite(leaseDurationMs) || leaseDurationMs <= 0) {
|
|
306
|
+
throw new Error("Stateful session router leaseDurationMs must be a positive finite number.");
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
function validateLeaseRenewalFraction(fraction) {
|
|
310
|
+
if (!Number.isFinite(fraction) || fraction <= 0 || fraction >= 1) {
|
|
311
|
+
throw new Error("Stateful session router leaseRenewalFraction must be greater than 0 and less than 1.");
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
function validateOwnerGeneration(owner) {
|
|
315
|
+
if (!Number.isInteger(owner.generation) || owner.generation <= 0) {
|
|
316
|
+
throw new Error("Stateful session owner generation must be a positive integer.");
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
function sameOwner(actual, expected) {
|
|
320
|
+
return (actual !== null &&
|
|
321
|
+
actual.sessionKey === expected.sessionKey &&
|
|
322
|
+
actual.ownerPodId === expected.ownerPodId &&
|
|
323
|
+
actual.generation === expected.generation);
|
|
324
|
+
}
|
|
325
|
+
function ownershipChanged(request) {
|
|
326
|
+
return new StatefulRoutingOwnershipError(request.requestId, request.sessionKey, "Stateful session ownership changed before the local provider invocation.");
|
|
327
|
+
}
|
|
328
|
+
function isExpiredDeadline(request, now) {
|
|
329
|
+
if (!request.deadlineAt)
|
|
330
|
+
return false;
|
|
331
|
+
const deadlineMs = Date.parse(request.deadlineAt);
|
|
332
|
+
return !Number.isFinite(deadlineMs) || deadlineMs <= now.getTime();
|
|
333
|
+
}
|
|
334
|
+
function makeDeadlineSignal(request, now) {
|
|
335
|
+
const controller = new AbortController();
|
|
336
|
+
if (!request.deadlineAt) {
|
|
337
|
+
return { signal: controller.signal, cleanup: () => { } };
|
|
338
|
+
}
|
|
339
|
+
const delayMs = Date.parse(request.deadlineAt) - now.getTime();
|
|
340
|
+
const timer = setTimeout(() => controller.abort(), delayMs);
|
|
341
|
+
return {
|
|
342
|
+
signal: controller.signal,
|
|
343
|
+
cleanup: () => clearTimeout(timer),
|
|
344
|
+
};
|
|
345
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import type { SessionKey } from "./session-key.js";
|
|
2
|
+
/** @deprecated Use SessionKey from session-key.ts. */
|
|
3
|
+
export type StatefulProviderSessionKey = SessionKey;
|
|
4
|
+
export type SessionOwnerStatus = "acquiring" | "connected" | "draining" | "expired";
|
|
5
|
+
export interface SessionOwnerRecord {
|
|
6
|
+
/** Runtime-decoded key; registry inputs use the opaque SessionKey type. */
|
|
7
|
+
readonly sessionKey: string;
|
|
8
|
+
readonly ownerPodId: string;
|
|
9
|
+
readonly ownerEndpoint: string;
|
|
10
|
+
readonly generation: number;
|
|
11
|
+
readonly leaseExpiresAt: string;
|
|
12
|
+
readonly status: SessionOwnerStatus;
|
|
13
|
+
readonly lastUsedAt: string;
|
|
14
|
+
}
|
|
15
|
+
export interface AcquireSessionOwnerInput {
|
|
16
|
+
readonly sessionKey: SessionKey;
|
|
17
|
+
readonly ownerPodId: string;
|
|
18
|
+
readonly ownerEndpoint: string;
|
|
19
|
+
readonly leaseDurationMs: number;
|
|
20
|
+
readonly status?: Exclude<SessionOwnerStatus, "expired">;
|
|
21
|
+
readonly now?: Date;
|
|
22
|
+
}
|
|
23
|
+
export interface AcquireSessionOwnerResult {
|
|
24
|
+
readonly record: SessionOwnerRecord;
|
|
25
|
+
readonly acquired: boolean;
|
|
26
|
+
}
|
|
27
|
+
export interface RenewSessionOwnerInput {
|
|
28
|
+
readonly sessionKey: SessionKey;
|
|
29
|
+
readonly ownerPodId: string;
|
|
30
|
+
readonly generation: number;
|
|
31
|
+
readonly leaseDurationMs: number;
|
|
32
|
+
readonly status?: Exclude<SessionOwnerStatus, "expired">;
|
|
33
|
+
readonly now?: Date;
|
|
34
|
+
}
|
|
35
|
+
export interface ReleaseSessionOwnerInput {
|
|
36
|
+
readonly sessionKey: SessionKey;
|
|
37
|
+
readonly ownerPodId: string;
|
|
38
|
+
readonly generation: number;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* A session-owner registry is a fencing authority. Generations MUST be positive integers and
|
|
42
|
+
* strictly increase for each successful takeover of a session key, including after expiry or
|
|
43
|
+
* release. Implementations must retain a high-water mark (a tombstone) for at least the registry's
|
|
44
|
+
* lifetime; generation values must never be reused.
|
|
45
|
+
*/
|
|
46
|
+
export interface SessionOwnerRegistry {
|
|
47
|
+
resolve(sessionKey: SessionKey, now?: Date, signal?: AbortSignal): Promise<SessionOwnerRecord | null>;
|
|
48
|
+
acquire(input: AcquireSessionOwnerInput, signal?: AbortSignal): Promise<AcquireSessionOwnerResult>;
|
|
49
|
+
renew(input: RenewSessionOwnerInput, signal?: AbortSignal): Promise<SessionOwnerRecord | null>;
|
|
50
|
+
release(input: ReleaseSessionOwnerInput, signal?: AbortSignal): Promise<boolean>;
|
|
51
|
+
}
|
|
52
|
+
export interface SessionPoolPolicy {
|
|
53
|
+
readonly maxSessions: number;
|
|
54
|
+
/** Disable idle eviction for connection-owned listeners with `"unlimited"`. */
|
|
55
|
+
readonly idleTimeoutMs: number | "unlimited";
|
|
56
|
+
/** Disable age-based recycling for expensive healthy sessions with `"unlimited"`. */
|
|
57
|
+
readonly maxLifetimeMs: number | "unlimited";
|
|
58
|
+
}
|
|
59
|
+
export interface ManagedSessionIdentity {
|
|
60
|
+
readonly connectionId: string;
|
|
61
|
+
readonly serviceAccountId: string;
|
|
62
|
+
readonly ownerPodId: string;
|
|
63
|
+
readonly ownerEndpoint: string;
|
|
64
|
+
readonly ownerStatus: SessionOwnerStatus;
|
|
65
|
+
}
|
|
66
|
+
export interface ManagedSession<T> {
|
|
67
|
+
readonly sessionKey: string;
|
|
68
|
+
readonly generation: number;
|
|
69
|
+
readonly value: T;
|
|
70
|
+
readonly createdAt: string;
|
|
71
|
+
readonly lastUsedAt: string;
|
|
72
|
+
readonly identity?: ManagedSessionIdentity;
|
|
73
|
+
}
|
|
74
|
+
type SessionFactory<T> = () => T | Promise<T>;
|
|
75
|
+
type SessionCloseHook<T> = (session: ManagedSession<T>, reason: string) => void | Promise<void>;
|
|
76
|
+
export declare class InMemorySessionOwnerRegistry implements SessionOwnerRegistry {
|
|
77
|
+
#private;
|
|
78
|
+
resolve(sessionKey: SessionKey, now?: Date, signal?: AbortSignal): Promise<SessionOwnerRecord | null>;
|
|
79
|
+
acquire(input: AcquireSessionOwnerInput, signal?: AbortSignal): Promise<AcquireSessionOwnerResult>;
|
|
80
|
+
renew(input: RenewSessionOwnerInput, signal?: AbortSignal): Promise<SessionOwnerRecord | null>;
|
|
81
|
+
release(input: ReleaseSessionOwnerInput, signal?: AbortSignal): Promise<boolean>;
|
|
82
|
+
}
|
|
83
|
+
export declare class PodLocalSessionPool<T> {
|
|
84
|
+
#private;
|
|
85
|
+
private readonly policy;
|
|
86
|
+
private readonly closeSession;
|
|
87
|
+
constructor(policy: SessionPoolPolicy, closeSession: SessionCloseHook<T>);
|
|
88
|
+
getOrCreate(sessionKey: string, generation: number, factory: SessionFactory<T>, now?: Date, identity?: ManagedSessionIdentity): Promise<ManagedSession<T>>;
|
|
89
|
+
private getOrCreateUnlocked;
|
|
90
|
+
closeAll(reason: string): Promise<void>;
|
|
91
|
+
invalidate(sessionKey: string, reason: string): Promise<void>;
|
|
92
|
+
runExclusive<R>(sessionKey: string, task: () => R | Promise<R>): Promise<R>;
|
|
93
|
+
private evictExpired;
|
|
94
|
+
private evictOverCapacity;
|
|
95
|
+
private closeOne;
|
|
96
|
+
private assertOpen;
|
|
97
|
+
}
|
|
98
|
+
export {};
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
export class InMemorySessionOwnerRegistry {
|
|
2
|
+
#owners = new Map();
|
|
3
|
+
#generationHighWater = new Map();
|
|
4
|
+
async resolve(sessionKey, now = new Date(), signal) {
|
|
5
|
+
signal?.throwIfAborted();
|
|
6
|
+
const current = this.#owners.get(sessionKey);
|
|
7
|
+
if (!current || isLeaseExpired(current, now))
|
|
8
|
+
return null;
|
|
9
|
+
validateGeneration(current.generation);
|
|
10
|
+
return current;
|
|
11
|
+
}
|
|
12
|
+
async acquire(input, signal) {
|
|
13
|
+
signal?.throwIfAborted();
|
|
14
|
+
validateLeaseDuration(input.leaseDurationMs);
|
|
15
|
+
const now = input.now ?? new Date();
|
|
16
|
+
const current = this.#owners.get(input.sessionKey);
|
|
17
|
+
if (current && !isLeaseExpired(current, now)) {
|
|
18
|
+
validateGeneration(current.generation);
|
|
19
|
+
if (current.ownerPodId !== input.ownerPodId) {
|
|
20
|
+
return { record: current, acquired: false };
|
|
21
|
+
}
|
|
22
|
+
const record = makeOwnerRecord(input, current.generation, now);
|
|
23
|
+
this.#owners.set(input.sessionKey, record);
|
|
24
|
+
return { record, acquired: true };
|
|
25
|
+
}
|
|
26
|
+
const generation = Math.max(current?.generation ?? 0, this.#generationHighWater.get(input.sessionKey) ?? 0) + 1;
|
|
27
|
+
const record = makeOwnerRecord(input, generation, now);
|
|
28
|
+
this.#owners.set(input.sessionKey, record);
|
|
29
|
+
this.#generationHighWater.set(input.sessionKey, generation);
|
|
30
|
+
return { record, acquired: true };
|
|
31
|
+
}
|
|
32
|
+
async renew(input, signal) {
|
|
33
|
+
signal?.throwIfAborted();
|
|
34
|
+
validateGeneration(input.generation);
|
|
35
|
+
validateLeaseDuration(input.leaseDurationMs);
|
|
36
|
+
const now = input.now ?? new Date();
|
|
37
|
+
const current = this.#owners.get(input.sessionKey);
|
|
38
|
+
if (!current ||
|
|
39
|
+
current.ownerPodId !== input.ownerPodId ||
|
|
40
|
+
current.generation !== input.generation ||
|
|
41
|
+
isLeaseExpired(current, now)) {
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
const record = {
|
|
45
|
+
...current,
|
|
46
|
+
leaseExpiresAt: addMs(now, input.leaseDurationMs).toISOString(),
|
|
47
|
+
status: input.status ?? current.status,
|
|
48
|
+
lastUsedAt: now.toISOString(),
|
|
49
|
+
};
|
|
50
|
+
this.#owners.set(input.sessionKey, record);
|
|
51
|
+
return record;
|
|
52
|
+
}
|
|
53
|
+
async release(input, signal) {
|
|
54
|
+
signal?.throwIfAborted();
|
|
55
|
+
validateGeneration(input.generation);
|
|
56
|
+
const current = this.#owners.get(input.sessionKey);
|
|
57
|
+
if (!current ||
|
|
58
|
+
current.ownerPodId !== input.ownerPodId ||
|
|
59
|
+
current.generation !== input.generation) {
|
|
60
|
+
return false;
|
|
61
|
+
}
|
|
62
|
+
this.#generationHighWater.set(input.sessionKey, Math.max(this.#generationHighWater.get(input.sessionKey) ?? 0, current.generation));
|
|
63
|
+
this.#owners.delete(input.sessionKey);
|
|
64
|
+
return true;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
export class PodLocalSessionPool {
|
|
68
|
+
policy;
|
|
69
|
+
closeSession;
|
|
70
|
+
#sessions = new Map();
|
|
71
|
+
#queues = new Map();
|
|
72
|
+
#creates = new Map();
|
|
73
|
+
#closed = false;
|
|
74
|
+
constructor(policy, closeSession) {
|
|
75
|
+
this.policy = policy;
|
|
76
|
+
this.closeSession = closeSession;
|
|
77
|
+
validatePoolPolicy(policy);
|
|
78
|
+
}
|
|
79
|
+
async getOrCreate(sessionKey, generation, factory, now = new Date(), identity) {
|
|
80
|
+
validateGeneration(generation);
|
|
81
|
+
this.assertOpen(sessionKey);
|
|
82
|
+
const existingCreate = this.#creates.get(sessionKey);
|
|
83
|
+
if (existingCreate) {
|
|
84
|
+
try {
|
|
85
|
+
await existingCreate;
|
|
86
|
+
}
|
|
87
|
+
catch { }
|
|
88
|
+
this.assertOpen(sessionKey);
|
|
89
|
+
}
|
|
90
|
+
const create = this.getOrCreateUnlocked(sessionKey, generation, factory, now, identity);
|
|
91
|
+
this.#creates.set(sessionKey, create);
|
|
92
|
+
try {
|
|
93
|
+
const session = await create;
|
|
94
|
+
this.assertOpen(sessionKey);
|
|
95
|
+
return session;
|
|
96
|
+
}
|
|
97
|
+
finally {
|
|
98
|
+
if (this.#creates.get(sessionKey) === create)
|
|
99
|
+
this.#creates.delete(sessionKey);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
async getOrCreateUnlocked(sessionKey, generation, factory, now, identity) {
|
|
103
|
+
await this.evictExpired(now);
|
|
104
|
+
const current = this.#sessions.get(sessionKey);
|
|
105
|
+
if (current && current.generation === generation) {
|
|
106
|
+
const touched = { ...current, lastUsedAt: now.toISOString() };
|
|
107
|
+
this.#sessions.delete(sessionKey);
|
|
108
|
+
this.#sessions.set(sessionKey, touched);
|
|
109
|
+
return touched;
|
|
110
|
+
}
|
|
111
|
+
if (current)
|
|
112
|
+
await this.closeOne(sessionKey, "generation-changed");
|
|
113
|
+
const session = {
|
|
114
|
+
sessionKey,
|
|
115
|
+
generation,
|
|
116
|
+
value: await factory(),
|
|
117
|
+
createdAt: now.toISOString(),
|
|
118
|
+
lastUsedAt: now.toISOString(),
|
|
119
|
+
...(identity ? { identity } : {}),
|
|
120
|
+
};
|
|
121
|
+
this.#sessions.set(sessionKey, session);
|
|
122
|
+
await this.evictOverCapacity();
|
|
123
|
+
return session;
|
|
124
|
+
}
|
|
125
|
+
async closeAll(reason) {
|
|
126
|
+
this.#closed = true;
|
|
127
|
+
const errors = [];
|
|
128
|
+
await Promise.all([...this.#creates.values()].map(async (create) => {
|
|
129
|
+
let session;
|
|
130
|
+
try {
|
|
131
|
+
session = await create;
|
|
132
|
+
}
|
|
133
|
+
catch {
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
try {
|
|
137
|
+
await this.closeOne(session.sessionKey, reason);
|
|
138
|
+
}
|
|
139
|
+
catch (error) {
|
|
140
|
+
errors.push(error);
|
|
141
|
+
}
|
|
142
|
+
}));
|
|
143
|
+
for (const sessionKey of [...this.#sessions.keys()]) {
|
|
144
|
+
try {
|
|
145
|
+
await this.closeOne(sessionKey, reason);
|
|
146
|
+
}
|
|
147
|
+
catch (error) {
|
|
148
|
+
errors.push(error);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
if (errors.length > 0) {
|
|
152
|
+
throw new AggregateError(errors, `Failed to close ${errors.length} stateful session(s).`);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
async invalidate(sessionKey, reason) {
|
|
156
|
+
await this.closeOne(sessionKey, reason);
|
|
157
|
+
}
|
|
158
|
+
async runExclusive(sessionKey, task) {
|
|
159
|
+
const previous = this.#queues.get(sessionKey) ?? Promise.resolve();
|
|
160
|
+
const next = previous.then(task, task);
|
|
161
|
+
const settled = next.then(() => undefined, () => undefined);
|
|
162
|
+
this.#queues.set(sessionKey, settled);
|
|
163
|
+
await settled.finally(() => {
|
|
164
|
+
if (this.#queues.get(sessionKey) === settled) {
|
|
165
|
+
this.#queues.delete(sessionKey);
|
|
166
|
+
}
|
|
167
|
+
});
|
|
168
|
+
return next;
|
|
169
|
+
}
|
|
170
|
+
async evictExpired(now) {
|
|
171
|
+
for (const [sessionKey, session] of this.#sessions) {
|
|
172
|
+
if (isSessionExpired(session, this.policy, now)) {
|
|
173
|
+
await this.closeOne(sessionKey, "expired");
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
async evictOverCapacity() {
|
|
178
|
+
while (this.#sessions.size > this.policy.maxSessions) {
|
|
179
|
+
const lruKey = this.#sessions.keys().next().value;
|
|
180
|
+
if (lruKey === undefined)
|
|
181
|
+
return;
|
|
182
|
+
await this.closeOne(lruKey, "capacity");
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
async closeOne(sessionKey, reason) {
|
|
186
|
+
const session = this.#sessions.get(sessionKey);
|
|
187
|
+
if (!session)
|
|
188
|
+
return;
|
|
189
|
+
this.#sessions.delete(sessionKey);
|
|
190
|
+
await this.closeSession(session, reason);
|
|
191
|
+
}
|
|
192
|
+
assertOpen(sessionKey) {
|
|
193
|
+
if (!this.#closed)
|
|
194
|
+
return;
|
|
195
|
+
throw new Error(`Pod-local session pool is closed; cannot get or create session "${sessionKey}".`);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
function makeOwnerRecord(input, generation, now) {
|
|
199
|
+
validateGeneration(generation);
|
|
200
|
+
return {
|
|
201
|
+
sessionKey: input.sessionKey,
|
|
202
|
+
ownerPodId: input.ownerPodId,
|
|
203
|
+
ownerEndpoint: input.ownerEndpoint,
|
|
204
|
+
generation,
|
|
205
|
+
leaseExpiresAt: addMs(now, input.leaseDurationMs).toISOString(),
|
|
206
|
+
status: input.status ?? "acquiring",
|
|
207
|
+
lastUsedAt: now.toISOString(),
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
function isLeaseExpired(record, now) {
|
|
211
|
+
return Date.parse(record.leaseExpiresAt) <= now.getTime();
|
|
212
|
+
}
|
|
213
|
+
function isSessionExpired(session, policy, now) {
|
|
214
|
+
const nowMs = now.getTime();
|
|
215
|
+
return ((policy.idleTimeoutMs !== "unlimited" &&
|
|
216
|
+
nowMs - Date.parse(session.lastUsedAt) >= policy.idleTimeoutMs) ||
|
|
217
|
+
(policy.maxLifetimeMs !== "unlimited" &&
|
|
218
|
+
nowMs - Date.parse(session.createdAt) >= policy.maxLifetimeMs));
|
|
219
|
+
}
|
|
220
|
+
function validateLeaseDuration(leaseDurationMs) {
|
|
221
|
+
if (!Number.isFinite(leaseDurationMs) || leaseDurationMs <= 0) {
|
|
222
|
+
throw new Error("Session owner leaseDurationMs must be a positive finite number.");
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
function validateGeneration(generation) {
|
|
226
|
+
if (!Number.isInteger(generation) || generation <= 0) {
|
|
227
|
+
throw new Error("Session owner generation must be a positive integer.");
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
function validatePoolPolicy(policy) {
|
|
231
|
+
if (!Number.isInteger(policy.maxSessions) || policy.maxSessions <= 0) {
|
|
232
|
+
throw new Error("Session pool maxSessions must be a positive integer.");
|
|
233
|
+
}
|
|
234
|
+
if (policy.idleTimeoutMs !== "unlimited" &&
|
|
235
|
+
(!Number.isFinite(policy.idleTimeoutMs) || policy.idleTimeoutMs <= 0)) {
|
|
236
|
+
throw new Error('Session pool idleTimeoutMs must be a positive finite number or "unlimited".');
|
|
237
|
+
}
|
|
238
|
+
if (policy.maxLifetimeMs !== "unlimited" &&
|
|
239
|
+
(!Number.isFinite(policy.maxLifetimeMs) || policy.maxLifetimeMs <= 0)) {
|
|
240
|
+
throw new Error('Session pool maxLifetimeMs must be a positive finite number or "unlimited".');
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
function addMs(date, ms) {
|
|
244
|
+
return new Date(date.getTime() + ms);
|
|
245
|
+
}
|