reqon-dsl 0.2.0
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/.claude/settings.local.json +31 -0
- package/.claude/skills/api-integration.md +125 -0
- package/.claude/skills/database-schema.md +51 -0
- package/.claude/skills/dsl-design.md +80 -0
- package/.claude/skills/property-testing.md +143 -0
- package/.claude/skills/reqon/SKILL.md +44 -0
- package/.claude/skills/reqon/references/examples.md +206 -0
- package/.claude/skills/reqon/references/syntax.md +263 -0
- package/.claude/skills/vscode-extension.md +113 -0
- package/.github/dependabot.yml +32 -0
- package/.github/pull_request_template.md +21 -0
- package/.github/workflows/ci.yml +174 -0
- package/.github/workflows/release.yml +73 -0
- package/CLAUDE.md +72 -0
- package/CONTRIBUTING.md +161 -0
- package/README.md +235 -0
- package/TODO.md +51 -0
- package/dist/ast/index.d.ts +1 -0
- package/dist/ast/index.js +1 -0
- package/dist/ast/nodes.d.ts +237 -0
- package/dist/ast/nodes.js +12 -0
- package/dist/auth/auth.test.d.ts +1 -0
- package/dist/auth/auth.test.js +255 -0
- package/dist/auth/circuit-breaker.d.ts +115 -0
- package/dist/auth/circuit-breaker.js +267 -0
- package/dist/auth/credentials.d.ts +91 -0
- package/dist/auth/credentials.js +169 -0
- package/dist/auth/index.d.ts +5 -0
- package/dist/auth/index.js +8 -0
- package/dist/auth/oauth2-provider.d.ts +41 -0
- package/dist/auth/oauth2-provider.js +131 -0
- package/dist/auth/rate-limiter.d.ts +61 -0
- package/dist/auth/rate-limiter.js +380 -0
- package/dist/auth/token-store.d.ts +30 -0
- package/dist/auth/token-store.js +148 -0
- package/dist/auth/types.d.ts +142 -0
- package/dist/auth/types.js +1 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +270 -0
- package/dist/errors/errors.test.d.ts +1 -0
- package/dist/errors/errors.test.js +165 -0
- package/dist/errors/index.d.ts +83 -0
- package/dist/errors/index.js +159 -0
- package/dist/execution/execution.test.d.ts +1 -0
- package/dist/execution/execution.test.js +246 -0
- package/dist/execution/index.d.ts +4 -0
- package/dist/execution/index.js +2 -0
- package/dist/execution/state.d.ts +136 -0
- package/dist/execution/state.js +82 -0
- package/dist/execution/store.d.ts +52 -0
- package/dist/execution/store.js +120 -0
- package/dist/index.d.ts +27 -0
- package/dist/index.js +57 -0
- package/dist/integration.test.d.ts +1 -0
- package/dist/integration.test.js +168 -0
- package/dist/interpreter/context.d.ts +15 -0
- package/dist/interpreter/context.js +29 -0
- package/dist/interpreter/evaluator.d.ts +5 -0
- package/dist/interpreter/evaluator.js +223 -0
- package/dist/interpreter/evaluator.test.d.ts +1 -0
- package/dist/interpreter/evaluator.test.js +512 -0
- package/dist/interpreter/executor.d.ts +131 -0
- package/dist/interpreter/executor.js +663 -0
- package/dist/interpreter/fetch-handler.d.ts +43 -0
- package/dist/interpreter/fetch-handler.js +203 -0
- package/dist/interpreter/http.d.ts +57 -0
- package/dist/interpreter/http.js +210 -0
- package/dist/interpreter/http.test.d.ts +1 -0
- package/dist/interpreter/http.test.js +299 -0
- package/dist/interpreter/index.d.ts +7 -0
- package/dist/interpreter/index.js +7 -0
- package/dist/interpreter/pagination.d.ts +63 -0
- package/dist/interpreter/pagination.js +155 -0
- package/dist/interpreter/progress.test.d.ts +1 -0
- package/dist/interpreter/progress.test.js +216 -0
- package/dist/interpreter/schema-matcher.d.ts +16 -0
- package/dist/interpreter/schema-matcher.js +136 -0
- package/dist/interpreter/schema-matcher.test.d.ts +1 -0
- package/dist/interpreter/schema-matcher.test.js +122 -0
- package/dist/interpreter/signals.d.ts +57 -0
- package/dist/interpreter/signals.js +73 -0
- package/dist/interpreter/step-handlers/for-handler.d.ts +17 -0
- package/dist/interpreter/step-handlers/for-handler.js +51 -0
- package/dist/interpreter/step-handlers/index.d.ts +8 -0
- package/dist/interpreter/step-handlers/index.js +8 -0
- package/dist/interpreter/step-handlers/map-handler.d.ts +10 -0
- package/dist/interpreter/step-handlers/map-handler.js +20 -0
- package/dist/interpreter/step-handlers/match-handler.d.ts +27 -0
- package/dist/interpreter/step-handlers/match-handler.js +61 -0
- package/dist/interpreter/step-handlers/store-handler.d.ts +13 -0
- package/dist/interpreter/step-handlers/store-handler.js +66 -0
- package/dist/interpreter/step-handlers/types.d.ts +15 -0
- package/dist/interpreter/step-handlers/types.js +1 -0
- package/dist/interpreter/step-handlers/validate-handler.d.ts +10 -0
- package/dist/interpreter/step-handlers/validate-handler.js +26 -0
- package/dist/interpreter/step-handlers/webhook-handler.d.ts +36 -0
- package/dist/interpreter/step-handlers/webhook-handler.js +104 -0
- package/dist/lexer/index.d.ts +10 -0
- package/dist/lexer/index.js +12 -0
- package/dist/lexer/lexer.d.ts +24 -0
- package/dist/lexer/lexer.js +264 -0
- package/dist/lexer/lexer.test.d.ts +1 -0
- package/dist/lexer/lexer.test.js +259 -0
- package/dist/lexer/tokens.d.ts +69 -0
- package/dist/lexer/tokens.js +146 -0
- package/dist/loader/index.d.ts +36 -0
- package/dist/loader/index.js +220 -0
- package/dist/loader/loader.test.d.ts +1 -0
- package/dist/loader/loader.test.js +287 -0
- package/dist/oas/index.d.ts +4 -0
- package/dist/oas/index.js +2 -0
- package/dist/oas/loader.d.ts +21 -0
- package/dist/oas/loader.js +82 -0
- package/dist/oas/oas.test.d.ts +1 -0
- package/dist/oas/oas.test.js +218 -0
- package/dist/oas/validator.d.ts +12 -0
- package/dist/oas/validator.js +227 -0
- package/dist/parser/base.d.ts +33 -0
- package/dist/parser/base.js +97 -0
- package/dist/parser/expressions.d.ts +27 -0
- package/dist/parser/expressions.js +248 -0
- package/dist/parser/expressions.test.d.ts +1 -0
- package/dist/parser/expressions.test.js +378 -0
- package/dist/parser/index.d.ts +3 -0
- package/dist/parser/index.js +3 -0
- package/dist/parser/match.test.d.ts +1 -0
- package/dist/parser/match.test.js +254 -0
- package/dist/parser/parser.d.ts +68 -0
- package/dist/parser/parser.js +1229 -0
- package/dist/parser/parser.test.d.ts +1 -0
- package/dist/parser/parser.test.js +333 -0
- package/dist/parser/schedule.test.d.ts +1 -0
- package/dist/parser/schedule.test.js +241 -0
- package/dist/plugin.d.ts +35 -0
- package/dist/plugin.js +68 -0
- package/dist/scheduler/cron-parser.d.ts +32 -0
- package/dist/scheduler/cron-parser.js +198 -0
- package/dist/scheduler/cron-parser.test.d.ts +1 -0
- package/dist/scheduler/cron-parser.test.js +188 -0
- package/dist/scheduler/index.d.ts +3 -0
- package/dist/scheduler/index.js +2 -0
- package/dist/scheduler/scheduler.d.ts +81 -0
- package/dist/scheduler/scheduler.js +376 -0
- package/dist/scheduler/types.d.ts +65 -0
- package/dist/scheduler/types.js +1 -0
- package/dist/stores/factory.d.ts +36 -0
- package/dist/stores/factory.js +73 -0
- package/dist/stores/file.d.ts +60 -0
- package/dist/stores/file.js +173 -0
- package/dist/stores/file.test.d.ts +1 -0
- package/dist/stores/file.test.js +165 -0
- package/dist/stores/index.d.ts +6 -0
- package/dist/stores/index.js +5 -0
- package/dist/stores/memory.d.ts +19 -0
- package/dist/stores/memory.js +51 -0
- package/dist/stores/memory.test.d.ts +1 -0
- package/dist/stores/memory.test.js +157 -0
- package/dist/stores/postgrest.d.ts +55 -0
- package/dist/stores/postgrest.js +217 -0
- package/dist/stores/stores.test.d.ts +1 -0
- package/dist/stores/stores.test.js +158 -0
- package/dist/stores/types.d.ts +31 -0
- package/dist/stores/types.js +26 -0
- package/dist/sync/index.d.ts +4 -0
- package/dist/sync/index.js +2 -0
- package/dist/sync/state.d.ts +69 -0
- package/dist/sync/state.js +66 -0
- package/dist/sync/store.d.ts +49 -0
- package/dist/sync/store.js +93 -0
- package/dist/sync/sync.test.d.ts +1 -0
- package/dist/sync/sync.test.js +221 -0
- package/dist/utils/async.d.ts +7 -0
- package/dist/utils/async.js +9 -0
- package/dist/utils/file.d.ts +38 -0
- package/dist/utils/file.js +92 -0
- package/dist/utils/index.d.ts +4 -0
- package/dist/utils/index.js +4 -0
- package/dist/utils/logger.d.ts +34 -0
- package/dist/utils/logger.js +39 -0
- package/dist/utils/path.d.ts +12 -0
- package/dist/utils/path.js +41 -0
- package/dist/webhook/index.d.ts +8 -0
- package/dist/webhook/index.js +7 -0
- package/dist/webhook/server.d.ts +84 -0
- package/dist/webhook/server.js +319 -0
- package/dist/webhook/store.d.ts +67 -0
- package/dist/webhook/store.js +193 -0
- package/dist/webhook/types.d.ts +88 -0
- package/dist/webhook/types.js +6 -0
- package/docusaurus/README.md +41 -0
- package/docusaurus/docs/advanced/execution-state.md +283 -0
- package/docusaurus/docs/advanced/extending-reqon.md +388 -0
- package/docusaurus/docs/advanced/multi-file-missions.md +250 -0
- package/docusaurus/docs/advanced/parallel-execution.md +353 -0
- package/docusaurus/docs/api-reference.md +443 -0
- package/docusaurus/docs/authentication/api-key.md +339 -0
- package/docusaurus/docs/authentication/basic.md +276 -0
- package/docusaurus/docs/authentication/bearer.md +282 -0
- package/docusaurus/docs/authentication/oauth2.md +317 -0
- package/docusaurus/docs/authentication/overview.md +251 -0
- package/docusaurus/docs/cli.md +229 -0
- package/docusaurus/docs/core-concepts/actions.md +286 -0
- package/docusaurus/docs/core-concepts/missions.md +264 -0
- package/docusaurus/docs/core-concepts/schemas.md +353 -0
- package/docusaurus/docs/core-concepts/sources.md +339 -0
- package/docusaurus/docs/core-concepts/stores.md +332 -0
- package/docusaurus/docs/dsl-syntax/expressions.md +361 -0
- package/docusaurus/docs/dsl-syntax/fetch.md +293 -0
- package/docusaurus/docs/dsl-syntax/for-loops.md +324 -0
- package/docusaurus/docs/dsl-syntax/map.md +345 -0
- package/docusaurus/docs/dsl-syntax/match.md +387 -0
- package/docusaurus/docs/dsl-syntax/pipelines.md +397 -0
- package/docusaurus/docs/dsl-syntax/validate.md +401 -0
- package/docusaurus/docs/error-handling/dead-letter-queues.md +399 -0
- package/docusaurus/docs/error-handling/flow-control.md +337 -0
- package/docusaurus/docs/error-handling/retry-strategies.md +368 -0
- package/docusaurus/docs/examples.md +488 -0
- package/docusaurus/docs/getting-started.md +256 -0
- package/docusaurus/docs/http/circuit-breaker.md +401 -0
- package/docusaurus/docs/http/incremental-sync.md +394 -0
- package/docusaurus/docs/http/pagination.md +361 -0
- package/docusaurus/docs/http/rate-limiting.md +383 -0
- package/docusaurus/docs/http/requests.md +328 -0
- package/docusaurus/docs/http/retry.md +402 -0
- package/docusaurus/docs/intro.md +90 -0
- package/docusaurus/docs/openapi/loading-specs.md +305 -0
- package/docusaurus/docs/openapi/operation-calls.md +314 -0
- package/docusaurus/docs/openapi/overview.md +212 -0
- package/docusaurus/docs/openapi/response-validation.md +344 -0
- package/docusaurus/docs/scheduling/cron.md +305 -0
- package/docusaurus/docs/scheduling/daemon-mode.md +317 -0
- package/docusaurus/docs/scheduling/intervals.md +289 -0
- package/docusaurus/docs/scheduling/overview.md +231 -0
- package/docusaurus/docs/stores/custom-adapters.md +376 -0
- package/docusaurus/docs/stores/file.md +236 -0
- package/docusaurus/docs/stores/memory.md +193 -0
- package/docusaurus/docs/stores/overview.md +274 -0
- package/docusaurus/docs/stores/postgrest.md +316 -0
- package/docusaurus/docusaurus.config.ts +148 -0
- package/docusaurus/package-lock.json +18029 -0
- package/docusaurus/package.json +47 -0
- package/docusaurus/sidebars.ts +155 -0
- package/docusaurus/src/components/HomepageFeatures/index.tsx +105 -0
- package/docusaurus/src/components/HomepageFeatures/styles.module.css +12 -0
- package/docusaurus/src/css/custom.css +169 -0
- package/docusaurus/src/pages/index.module.css +48 -0
- package/docusaurus/src/pages/index.tsx +110 -0
- package/docusaurus/src/pages/markdown-page.md +7 -0
- package/docusaurus/static/.nojekyll +0 -0
- package/docusaurus/static/img/docusaurus-social-card.jpg +0 -0
- package/docusaurus/static/img/docusaurus.png +0 -0
- package/docusaurus/static/img/favicon.ico +0 -0
- package/docusaurus/static/img/logo.svg +10 -0
- package/docusaurus/static/img/undraw_docusaurus_mountain.svg +171 -0
- package/docusaurus/static/img/undraw_docusaurus_react.svg +170 -0
- package/docusaurus/static/img/undraw_docusaurus_tree.svg +40 -0
- package/docusaurus/tsconfig.json +8 -0
- package/examples/README.md +112 -0
- package/examples/error-handling/README.md +150 -0
- package/examples/error-handling/payment-processor.vague +287 -0
- package/examples/github-sync/README.md +74 -0
- package/examples/github-sync/fetch-issues.vague +47 -0
- package/examples/github-sync/fetch-prs.vague +40 -0
- package/examples/github-sync/mission.vague +101 -0
- package/examples/github-sync/normalize.vague +70 -0
- package/examples/jsonplaceholder/README.md +28 -0
- package/examples/jsonplaceholder/posts.vague +48 -0
- package/examples/petstore/README.md +35 -0
- package/examples/petstore/openapi.yaml +97 -0
- package/examples/petstore/sync.vague +52 -0
- package/examples/temporal-comparison/README.md +297 -0
- package/examples/temporal-comparison/reconciliation.vague +355 -0
- package/examples/temporal-comparison/temporal/activities/index.ts +8 -0
- package/examples/temporal-comparison/temporal/activities/shipstation.ts +225 -0
- package/examples/temporal-comparison/temporal/activities/shopify.ts +257 -0
- package/examples/temporal-comparison/temporal/activities/storage.ts +198 -0
- package/examples/temporal-comparison/temporal/activities/stripe.ts +169 -0
- package/examples/temporal-comparison/temporal/activities/validation.ts +205 -0
- package/examples/temporal-comparison/temporal/client/schedule.ts +218 -0
- package/examples/temporal-comparison/temporal/config/retry.ts +63 -0
- package/examples/temporal-comparison/temporal/types/index.ts +129 -0
- package/examples/temporal-comparison/temporal/workers/main.ts +130 -0
- package/examples/temporal-comparison/temporal/workflows/orderReconciliation.ts +262 -0
- package/examples/xero/README.md +88 -0
- package/examples/xero/invoices.vague +189 -0
- package/package.json +40 -0
- package/src/api-integration.test.ts +954 -0
- package/src/ast/index.ts +1 -0
- package/src/ast/nodes.ts +310 -0
- package/src/auth/auth.test.ts +326 -0
- package/src/auth/circuit-breaker.test.ts +390 -0
- package/src/auth/circuit-breaker.ts +379 -0
- package/src/auth/credentials.test.ts +273 -0
- package/src/auth/credentials.ts +246 -0
- package/src/auth/index.ts +40 -0
- package/src/auth/oauth2-provider.ts +177 -0
- package/src/auth/rate-limiter.ts +459 -0
- package/src/auth/token-store.ts +177 -0
- package/src/auth/types.ts +159 -0
- package/src/benchmark/e2e.bench.ts +288 -0
- package/src/benchmark/evaluator.bench.ts +331 -0
- package/src/benchmark/fixtures.ts +295 -0
- package/src/benchmark/index.ts +108 -0
- package/src/benchmark/lexer.bench.ts +69 -0
- package/src/benchmark/parser.bench.ts +103 -0
- package/src/benchmark/resilience.bench.ts +193 -0
- package/src/benchmark/store.bench.ts +147 -0
- package/src/benchmark/utils.ts +230 -0
- package/src/cli.ts +313 -0
- package/src/errors/errors.test.ts +234 -0
- package/src/errors/index.ts +223 -0
- package/src/execution/execution.test.ts +307 -0
- package/src/execution/index.ts +21 -0
- package/src/execution/state.ts +207 -0
- package/src/execution/store.ts +188 -0
- package/src/index.ts +169 -0
- package/src/integration.test.ts +192 -0
- package/src/interpreter/context.ts +57 -0
- package/src/interpreter/evaluator.test.ts +796 -0
- package/src/interpreter/evaluator.ts +245 -0
- package/src/interpreter/executor.ts +946 -0
- package/src/interpreter/fetch-handler.ts +302 -0
- package/src/interpreter/http.test.ts +423 -0
- package/src/interpreter/http.ts +308 -0
- package/src/interpreter/index.ts +32 -0
- package/src/interpreter/pagination.ts +207 -0
- package/src/interpreter/progress.test.ts +276 -0
- package/src/interpreter/schema-matcher.test.ts +160 -0
- package/src/interpreter/schema-matcher.ts +168 -0
- package/src/interpreter/signals.ts +73 -0
- package/src/interpreter/step-handlers/for-handler.ts +65 -0
- package/src/interpreter/step-handlers/index.ts +17 -0
- package/src/interpreter/step-handlers/map-handler.ts +24 -0
- package/src/interpreter/step-handlers/match-handler.ts +101 -0
- package/src/interpreter/step-handlers/store-handler.ts +78 -0
- package/src/interpreter/step-handlers/types.ts +17 -0
- package/src/interpreter/step-handlers/validate-handler.ts +30 -0
- package/src/interpreter/step-handlers/webhook-handler.ts +142 -0
- package/src/lexer/index.ts +18 -0
- package/src/lexer/lexer.test.ts +316 -0
- package/src/lexer/tokens.ts +179 -0
- package/src/loader/index.ts +288 -0
- package/src/loader/loader.test.ts +360 -0
- package/src/oas/index.ts +4 -0
- package/src/oas/loader.ts +126 -0
- package/src/oas/oas.test.ts +254 -0
- package/src/oas/validator.ts +299 -0
- package/src/parser/base.ts +124 -0
- package/src/parser/expressions.test.ts +525 -0
- package/src/parser/expressions.ts +314 -0
- package/src/parser/index.ts +3 -0
- package/src/parser/match.test.ts +296 -0
- package/src/parser/parser.test.ts +739 -0
- package/src/parser/parser.ts +1469 -0
- package/src/parser/schedule.test.ts +287 -0
- package/src/parser/webhook.test.ts +248 -0
- package/src/plugin.ts +83 -0
- package/src/scheduler/cron-parser.test.ts +236 -0
- package/src/scheduler/cron-parser.ts +236 -0
- package/src/scheduler/index.ts +10 -0
- package/src/scheduler/scheduler.ts +443 -0
- package/src/scheduler/types.ts +71 -0
- package/src/stores/factory.ts +104 -0
- package/src/stores/file.test.ts +276 -0
- package/src/stores/file.ts +211 -0
- package/src/stores/index.ts +6 -0
- package/src/stores/memory.test.ts +238 -0
- package/src/stores/memory.ts +63 -0
- package/src/stores/postgrest.test.ts +488 -0
- package/src/stores/postgrest.ts +263 -0
- package/src/stores/stores.test.ts +197 -0
- package/src/stores/types.ts +58 -0
- package/src/sync/index.ts +16 -0
- package/src/sync/state.ts +126 -0
- package/src/sync/store.ts +139 -0
- package/src/sync/sync.test.ts +271 -0
- package/src/utils/async.ts +10 -0
- package/src/utils/file.ts +106 -0
- package/src/utils/index.ts +14 -0
- package/src/utils/logger.ts +53 -0
- package/src/utils/path.ts +47 -0
- package/src/webhook/index.ts +15 -0
- package/src/webhook/server.test.ts +253 -0
- package/src/webhook/server.ts +389 -0
- package/src/webhook/store.ts +239 -0
- package/src/webhook/types.ts +93 -0
- package/tsconfig.json +17 -0
- package/vitest.config.ts +39 -0
package/src/ast/index.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './nodes.js';
|
package/src/ast/nodes.ts
ADDED
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
Expression,
|
|
3
|
+
FieldDefinition,
|
|
4
|
+
SchemaDefinition,
|
|
5
|
+
Statement as VagueStatement,
|
|
6
|
+
} from 'vague-lang';
|
|
7
|
+
|
|
8
|
+
// Reqon extends Vague's statements
|
|
9
|
+
export type Statement = VagueStatement | MissionDefinition | SourceDefinition | StoreDefinition | ActionDefinition;
|
|
10
|
+
|
|
11
|
+
export interface ReqonProgram {
|
|
12
|
+
type: 'ReqonProgram';
|
|
13
|
+
statements: Statement[];
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// source Xero { auth: oauth2, base: "https://api.xero.com/..." }
|
|
17
|
+
// source Xero from "./xero-openapi.yaml" { auth: oauth2 }
|
|
18
|
+
export interface SourceDefinition {
|
|
19
|
+
type: 'SourceDefinition';
|
|
20
|
+
name: string;
|
|
21
|
+
specPath?: string; // OAS spec path (URL or file path)
|
|
22
|
+
config: SourceConfig;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface SourceConfig {
|
|
26
|
+
auth: AuthConfig;
|
|
27
|
+
base?: string; // Optional if using OAS (derived from spec)
|
|
28
|
+
headers?: Record<string, Expression>;
|
|
29
|
+
validateResponses?: boolean; // Validate responses against OAS schema
|
|
30
|
+
rateLimit?: RateLimitSourceConfig; // Rate limiting configuration
|
|
31
|
+
circuitBreaker?: CircuitBreakerSourceConfig; // Circuit breaker configuration
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface CircuitBreakerSourceConfig {
|
|
35
|
+
/** Number of failures before opening circuit (default: 5) */
|
|
36
|
+
failureThreshold?: number;
|
|
37
|
+
/** Time in seconds before attempting recovery (default: 30) */
|
|
38
|
+
resetTimeout?: number;
|
|
39
|
+
/** Number of successful requests in half-open to close circuit (default: 2) */
|
|
40
|
+
successThreshold?: number;
|
|
41
|
+
/** Time window in seconds for counting failures (default: 60) */
|
|
42
|
+
failureWindow?: number;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export interface RateLimitSourceConfig {
|
|
46
|
+
strategy?: 'pause' | 'throttle' | 'fail'; // Default: 'pause'
|
|
47
|
+
maxWait?: number; // Max seconds to wait (default: 300)
|
|
48
|
+
fallbackRpm?: number; // Fallback requests per minute if no headers
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export interface AuthConfig {
|
|
52
|
+
type: 'oauth2' | 'bearer' | 'basic' | 'api_key' | 'none';
|
|
53
|
+
// Details resolved at runtime from environment/config
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// store invoices_cache: nosql("invoices")
|
|
57
|
+
// store invoices_sql: sql("accounting.invoices")
|
|
58
|
+
export interface StoreDefinition {
|
|
59
|
+
type: 'StoreDefinition';
|
|
60
|
+
name: string;
|
|
61
|
+
storeType: 'nosql' | 'sql' | 'memory';
|
|
62
|
+
target: string; // collection/table name
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Schedule configuration for missions
|
|
66
|
+
// schedule: every 6 hours
|
|
67
|
+
// schedule: cron "0 */6 * * *"
|
|
68
|
+
// schedule: at "2025-01-20 09:00 UTC"
|
|
69
|
+
export interface ScheduleDefinition {
|
|
70
|
+
type: 'ScheduleDefinition';
|
|
71
|
+
scheduleType: 'interval' | 'cron' | 'once';
|
|
72
|
+
// For interval-based scheduling
|
|
73
|
+
interval?: IntervalSchedule;
|
|
74
|
+
// For cron-based scheduling
|
|
75
|
+
cronExpression?: string;
|
|
76
|
+
// For one-time scheduling
|
|
77
|
+
runAt?: string; // ISO 8601 datetime or parseable date string
|
|
78
|
+
// Optional timezone (defaults to UTC)
|
|
79
|
+
timezone?: string;
|
|
80
|
+
// Maximum concurrent executions (default: 1)
|
|
81
|
+
maxConcurrency?: number;
|
|
82
|
+
// Skip execution if previous run is still running (default: true)
|
|
83
|
+
skipIfRunning?: boolean;
|
|
84
|
+
// Retry configuration for failed scheduled runs
|
|
85
|
+
retryOnFailure?: ScheduleRetryConfig;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export interface IntervalSchedule {
|
|
89
|
+
value: number;
|
|
90
|
+
unit: 'seconds' | 'minutes' | 'hours' | 'days' | 'weeks';
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export interface ScheduleRetryConfig {
|
|
94
|
+
maxRetries: number;
|
|
95
|
+
delaySeconds: number;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// mission SyncXeroInvoices { ... }
|
|
99
|
+
export interface MissionDefinition {
|
|
100
|
+
type: 'MissionDefinition';
|
|
101
|
+
name: string;
|
|
102
|
+
schedule?: ScheduleDefinition;
|
|
103
|
+
sources: SourceDefinition[];
|
|
104
|
+
stores: StoreDefinition[];
|
|
105
|
+
schemas: SchemaDefinition[];
|
|
106
|
+
actions: ActionDefinition[];
|
|
107
|
+
pipeline: PipelineDefinition;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// action FetchInvoiceList { ... }
|
|
111
|
+
export interface ActionDefinition {
|
|
112
|
+
type: 'ActionDefinition';
|
|
113
|
+
name: string;
|
|
114
|
+
steps: ActionStep[];
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export type ActionStep = FetchStep | ForStep | MapStep | ValidateStep | StoreStep | MatchStep | LetStep | WebhookStep;
|
|
118
|
+
|
|
119
|
+
// let myVar = expression
|
|
120
|
+
export interface LetStep {
|
|
121
|
+
type: 'LetStep';
|
|
122
|
+
name: string;
|
|
123
|
+
value: Expression;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// wait { timeout: 60000, path: "/webhooks/callback", ... }
|
|
127
|
+
// Waits for an external webhook callback before continuing execution
|
|
128
|
+
export interface WebhookStep {
|
|
129
|
+
type: 'WebhookStep';
|
|
130
|
+
/** Timeout in milliseconds to wait for webhook (default: 300000 = 5 minutes) */
|
|
131
|
+
timeout?: number;
|
|
132
|
+
/** Path for the webhook endpoint (e.g., "/webhooks/callback") */
|
|
133
|
+
path?: string;
|
|
134
|
+
/** Number of webhook events to collect before continuing (default: 1) */
|
|
135
|
+
expectedEvents?: number;
|
|
136
|
+
/** Filter expression for matching webhook events */
|
|
137
|
+
eventFilter?: Expression;
|
|
138
|
+
/** Retry configuration if timeout occurs */
|
|
139
|
+
retryOnTimeout?: RetryConfig;
|
|
140
|
+
/** Store configuration for saving webhook payloads */
|
|
141
|
+
storage?: WebhookStorageConfig;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export interface WebhookStorageConfig {
|
|
145
|
+
/** Store name to save webhook payloads */
|
|
146
|
+
target: string;
|
|
147
|
+
/** Expression for extracting key from webhook payload */
|
|
148
|
+
key?: Expression;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Flow control directives for match arms
|
|
152
|
+
export type FlowDirective =
|
|
153
|
+
| { type: 'continue' } // proceed to next pipeline stage
|
|
154
|
+
| { type: 'skip' } // skip remaining steps, move to next stage
|
|
155
|
+
| { type: 'abort'; message?: string } // halt mission with error
|
|
156
|
+
| { type: 'retry'; backoff?: RetryConfig } // retry current action
|
|
157
|
+
| { type: 'queue'; target?: string } // queue for later processing
|
|
158
|
+
| { type: 'jump'; action: string; then?: 'retry' | 'continue' }; // jump to action
|
|
159
|
+
|
|
160
|
+
// match response { Schema1 -> steps..., Schema2 -> flow directive }
|
|
161
|
+
export interface MatchStep {
|
|
162
|
+
type: 'MatchStep';
|
|
163
|
+
target: Expression; // what to match (usually 'response')
|
|
164
|
+
arms: MatchArm[];
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
export interface MatchArm {
|
|
168
|
+
/** Schema name to match against, or '_' for wildcard */
|
|
169
|
+
schema: string;
|
|
170
|
+
/** Steps to execute if matched */
|
|
171
|
+
steps?: ActionStep[];
|
|
172
|
+
/** Flow control directive (if no steps) */
|
|
173
|
+
flow?: FlowDirective;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// get "/Invoices" { paginate: ..., until: ... }
|
|
177
|
+
// call Xero.getInvoices { paginate: ... } -- OAS operationId reference
|
|
178
|
+
// get "/Invoices" { since: lastSync } -- Incremental sync
|
|
179
|
+
export interface FetchStep {
|
|
180
|
+
type: 'FetchStep';
|
|
181
|
+
// Traditional: explicit method + path
|
|
182
|
+
method?: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';
|
|
183
|
+
path?: Expression; // Can contain interpolations like "/Invoices/{id}"
|
|
184
|
+
// OAS-based: source.operationId reference
|
|
185
|
+
operationRef?: OperationRef;
|
|
186
|
+
source?: string; // Which source to use (defaults to first defined)
|
|
187
|
+
body?: Expression;
|
|
188
|
+
headers?: Record<string, Expression>;
|
|
189
|
+
paginate?: PaginationConfig;
|
|
190
|
+
until?: Expression; // Condition to stop pagination
|
|
191
|
+
retry?: RetryConfig;
|
|
192
|
+
// Incremental sync
|
|
193
|
+
since?: SinceConfig;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// Configuration for incremental sync
|
|
197
|
+
export interface SinceConfig {
|
|
198
|
+
/** How to resolve the "since" timestamp */
|
|
199
|
+
type: 'lastSync' | 'expression';
|
|
200
|
+
/** Custom checkpoint key (defaults to source:endpoint) */
|
|
201
|
+
key?: string;
|
|
202
|
+
/** Query parameter name for the since value (default: varies by API) */
|
|
203
|
+
param?: string;
|
|
204
|
+
/** Date format for the since value */
|
|
205
|
+
format?: 'iso' | 'unix' | 'unix-ms' | 'date-only';
|
|
206
|
+
/** Expression to evaluate for 'expression' type */
|
|
207
|
+
expression?: Expression;
|
|
208
|
+
/** Field in response to use as the new "since" value for next sync */
|
|
209
|
+
updateFrom?: string;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
export interface OperationRef {
|
|
213
|
+
source: string; // Source name (e.g., "Xero")
|
|
214
|
+
operationId: string; // OAS operationId (e.g., "getInvoices")
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
export interface PaginationConfig {
|
|
218
|
+
type: 'offset' | 'cursor' | 'page';
|
|
219
|
+
param: string; // Query param name: "page", "offset", "cursor"
|
|
220
|
+
pageSize: number;
|
|
221
|
+
cursorPath?: string; // For cursor pagination: where to find next cursor in response
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
export interface RetryConfig {
|
|
225
|
+
maxAttempts: number;
|
|
226
|
+
backoff: 'exponential' | 'linear' | 'constant';
|
|
227
|
+
initialDelay: number; // ms
|
|
228
|
+
maxDelay?: number; // ms
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// for invoice in invoices_cache where .partial == true { ... }
|
|
232
|
+
export interface ForStep {
|
|
233
|
+
type: 'ForStep';
|
|
234
|
+
variable: string;
|
|
235
|
+
collection: Expression;
|
|
236
|
+
condition?: Expression; // where clause
|
|
237
|
+
steps: ActionStep[];
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// map invoice -> StandardInvoice { id: .InvoiceID, ... }
|
|
241
|
+
export interface MapStep {
|
|
242
|
+
type: 'MapStep';
|
|
243
|
+
source: Expression;
|
|
244
|
+
targetSchema: string;
|
|
245
|
+
mappings: FieldMapping[];
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
export interface FieldMapping {
|
|
249
|
+
field: string;
|
|
250
|
+
expression: Expression;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// validate response { assume .Total is decimal, ... }
|
|
254
|
+
export interface ValidateStep {
|
|
255
|
+
type: 'ValidateStep';
|
|
256
|
+
target: Expression;
|
|
257
|
+
constraints: ValidationConstraint[];
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
export interface ValidationConstraint {
|
|
261
|
+
type: 'ValidationConstraint';
|
|
262
|
+
condition: Expression;
|
|
263
|
+
message?: string;
|
|
264
|
+
severity: 'error' | 'warning';
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// store response -> invoices_cache { key: .InvoiceID, partial: false }
|
|
268
|
+
export interface StoreStep {
|
|
269
|
+
type: 'StoreStep';
|
|
270
|
+
source: Expression;
|
|
271
|
+
target: string; // store name
|
|
272
|
+
options: StoreOptions;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
export interface StoreOptions {
|
|
276
|
+
key?: Expression; // Primary key field
|
|
277
|
+
partial?: boolean; // Mark as partial entity
|
|
278
|
+
upsert?: boolean; // Update if exists
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// run FetchInvoiceList then HydrateInvoices then NormalizeInvoices
|
|
282
|
+
// run [FetchOrders, FetchPayments] then ReconcileData -- parallel stages
|
|
283
|
+
export interface PipelineDefinition {
|
|
284
|
+
type: 'PipelineDefinition';
|
|
285
|
+
stages: PipelineStage[];
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
export interface PipelineStage {
|
|
289
|
+
/** Single action name (for sequential stages) */
|
|
290
|
+
action?: string;
|
|
291
|
+
/** Multiple action names (for parallel stages with bracket syntax) */
|
|
292
|
+
actions?: string[];
|
|
293
|
+
/** Optional: only run if condition true */
|
|
294
|
+
condition?: Expression;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/** Helper to check if a stage is parallel */
|
|
298
|
+
export function isParallelStage(stage: PipelineStage): stage is PipelineStage & { actions: string[] } {
|
|
299
|
+
return Array.isArray(stage.actions) && stage.actions.length > 0;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/** Helper to get action names from a stage (works for both single and parallel) */
|
|
303
|
+
export function getStageActions(stage: PipelineStage): string[] {
|
|
304
|
+
if (stage.actions) return stage.actions;
|
|
305
|
+
if (stage.action) return [stage.action];
|
|
306
|
+
return [];
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// Re-export Vague types for convenience
|
|
310
|
+
export type { Expression, FieldDefinition, SchemaDefinition } from 'vague-lang';
|
|
@@ -0,0 +1,326 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
|
2
|
+
import {
|
|
3
|
+
AdaptiveRateLimiter,
|
|
4
|
+
parseRateLimitHeaders,
|
|
5
|
+
RateLimitError,
|
|
6
|
+
RateLimitTimeoutError,
|
|
7
|
+
} from './rate-limiter.js';
|
|
8
|
+
import { InMemoryTokenStore } from './token-store.js';
|
|
9
|
+
import type { OAuth2Tokens, RateLimitEvent } from './types.js';
|
|
10
|
+
|
|
11
|
+
describe('parseRateLimitHeaders', () => {
|
|
12
|
+
it('parses X-RateLimit headers', () => {
|
|
13
|
+
const headers = {
|
|
14
|
+
'X-RateLimit-Limit': '100',
|
|
15
|
+
'X-RateLimit-Remaining': '42',
|
|
16
|
+
'X-RateLimit-Reset': '1700000000',
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
const info = parseRateLimitHeaders(headers);
|
|
20
|
+
|
|
21
|
+
expect(info.limit).toBe(100);
|
|
22
|
+
expect(info.remaining).toBe(42);
|
|
23
|
+
expect(info.resetAt).toBeInstanceOf(Date);
|
|
24
|
+
expect(info.resetAt?.getTime()).toBe(1700000000 * 1000);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it('parses lowercase ratelimit headers', () => {
|
|
28
|
+
const headers = {
|
|
29
|
+
'ratelimit-limit': '60',
|
|
30
|
+
'ratelimit-remaining': '5',
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
const info = parseRateLimitHeaders(headers);
|
|
34
|
+
|
|
35
|
+
expect(info.limit).toBe(60);
|
|
36
|
+
expect(info.remaining).toBe(5);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it('parses Retry-After header (seconds)', () => {
|
|
40
|
+
const headers = {
|
|
41
|
+
'Retry-After': '30',
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
const info = parseRateLimitHeaders(headers);
|
|
45
|
+
|
|
46
|
+
expect(info.retryAfter).toBe(30);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('handles missing headers gracefully', () => {
|
|
50
|
+
const info = parseRateLimitHeaders({});
|
|
51
|
+
|
|
52
|
+
expect(info.limit).toBeUndefined();
|
|
53
|
+
expect(info.remaining).toBeUndefined();
|
|
54
|
+
expect(info.resetAt).toBeUndefined();
|
|
55
|
+
expect(info.retryAfter).toBeUndefined();
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
describe('AdaptiveRateLimiter', () => {
|
|
60
|
+
let limiter: AdaptiveRateLimiter;
|
|
61
|
+
|
|
62
|
+
beforeEach(() => {
|
|
63
|
+
limiter = new AdaptiveRateLimiter();
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it('allows requests when no limits recorded', async () => {
|
|
67
|
+
const canProceed = await limiter.canProceed('TestAPI');
|
|
68
|
+
expect(canProceed).toBe(true);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it('blocks requests when remaining is 0', async () => {
|
|
72
|
+
const futureReset = new Date(Date.now() + 60000);
|
|
73
|
+
limiter.recordResponse('TestAPI', {
|
|
74
|
+
remaining: 0,
|
|
75
|
+
limit: 100,
|
|
76
|
+
resetAt: futureReset,
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
const canProceed = await limiter.canProceed('TestAPI');
|
|
80
|
+
expect(canProceed).toBe(false);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it('allows requests after reset time passes', async () => {
|
|
84
|
+
const pastReset = new Date(Date.now() - 1000);
|
|
85
|
+
limiter.recordResponse('TestAPI', {
|
|
86
|
+
remaining: 0,
|
|
87
|
+
limit: 100,
|
|
88
|
+
resetAt: pastReset,
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
const canProceed = await limiter.canProceed('TestAPI');
|
|
92
|
+
expect(canProceed).toBe(true);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it('blocks during retry-after period', async () => {
|
|
96
|
+
limiter.recordResponse('TestAPI', {
|
|
97
|
+
retryAfter: 60, // 60 seconds
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
const canProceed = await limiter.canProceed('TestAPI');
|
|
101
|
+
expect(canProceed).toBe(false);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it('tracks limits per source', async () => {
|
|
105
|
+
limiter.recordResponse('API1', { remaining: 0, resetAt: new Date(Date.now() + 60000) });
|
|
106
|
+
limiter.recordResponse('API2', { remaining: 50, limit: 100 });
|
|
107
|
+
|
|
108
|
+
expect(await limiter.canProceed('API1')).toBe(false);
|
|
109
|
+
expect(await limiter.canProceed('API2')).toBe(true);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it('tracks limits per endpoint within source', async () => {
|
|
113
|
+
limiter.recordResponse('API', { remaining: 0, resetAt: new Date(Date.now() + 60000) }, '/invoices');
|
|
114
|
+
limiter.recordResponse('API', { remaining: 50, limit: 100 }, '/contacts');
|
|
115
|
+
|
|
116
|
+
expect(await limiter.canProceed('API', '/invoices')).toBe(false);
|
|
117
|
+
expect(await limiter.canProceed('API', '/contacts')).toBe(true);
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it('provides accurate status', () => {
|
|
121
|
+
limiter.recordResponse('TestAPI', {
|
|
122
|
+
remaining: 42,
|
|
123
|
+
limit: 100,
|
|
124
|
+
resetAt: new Date(Date.now() + 60000),
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
const status = limiter.getStatus('TestAPI');
|
|
128
|
+
|
|
129
|
+
expect(status.remaining).toBe(42);
|
|
130
|
+
expect(status.limit).toBe(100);
|
|
131
|
+
expect(status.isLimited).toBe(false);
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
describe('fail strategy', () => {
|
|
135
|
+
it('throws RateLimitError immediately when rate limited', async () => {
|
|
136
|
+
limiter.configure('FailAPI', { strategy: 'fail' });
|
|
137
|
+
limiter.recordResponse('FailAPI', {
|
|
138
|
+
remaining: 0,
|
|
139
|
+
resetAt: new Date(Date.now() + 60000),
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
await expect(limiter.waitForCapacity('FailAPI')).rejects.toThrow(RateLimitError);
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
it('includes reset time in error', async () => {
|
|
146
|
+
limiter.configure('FailAPI', { strategy: 'fail' });
|
|
147
|
+
const resetAt = new Date(Date.now() + 60000);
|
|
148
|
+
limiter.recordResponse('FailAPI', { remaining: 0, resetAt });
|
|
149
|
+
|
|
150
|
+
try {
|
|
151
|
+
await limiter.waitForCapacity('FailAPI');
|
|
152
|
+
expect.fail('Should have thrown');
|
|
153
|
+
} catch (error) {
|
|
154
|
+
expect(error).toBeInstanceOf(RateLimitError);
|
|
155
|
+
expect((error as RateLimitError).resetAt).toEqual(resetAt);
|
|
156
|
+
}
|
|
157
|
+
});
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
describe('pause strategy', () => {
|
|
161
|
+
it('throws RateLimitTimeoutError when maxWait exceeded', async () => {
|
|
162
|
+
limiter.configure('PauseAPI', { strategy: 'pause', maxWait: 1 });
|
|
163
|
+
limiter.recordResponse('PauseAPI', {
|
|
164
|
+
remaining: 0,
|
|
165
|
+
resetAt: new Date(Date.now() + 60000), // 60s in future
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
await expect(limiter.waitForCapacity('PauseAPI')).rejects.toThrow(RateLimitTimeoutError);
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
it('proceeds after reset time passes', async () => {
|
|
172
|
+
limiter.configure('PauseAPI', { strategy: 'pause', maxWait: 5 });
|
|
173
|
+
// Reset in 50ms
|
|
174
|
+
limiter.recordResponse('PauseAPI', {
|
|
175
|
+
remaining: 0,
|
|
176
|
+
resetAt: new Date(Date.now() + 50),
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
// Should complete without throwing
|
|
180
|
+
await limiter.waitForCapacity('PauseAPI');
|
|
181
|
+
});
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
describe('throttle strategy', () => {
|
|
185
|
+
it('calculates delay based on remaining requests and reset time', () => {
|
|
186
|
+
limiter.configure('ThrottleAPI', { strategy: 'throttle' });
|
|
187
|
+
limiter.recordResponse('ThrottleAPI', {
|
|
188
|
+
remaining: 10,
|
|
189
|
+
limit: 100,
|
|
190
|
+
resetAt: new Date(Date.now() + 10000), // 10s left
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
const delay = limiter.getThrottleDelay('ThrottleAPI');
|
|
194
|
+
// 10s / 10 remaining = 1s per request = 1000ms
|
|
195
|
+
expect(delay).toBeGreaterThanOrEqual(900);
|
|
196
|
+
expect(delay).toBeLessThanOrEqual(1100);
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
it('returns 0 delay when not in throttle mode', () => {
|
|
200
|
+
limiter.configure('PauseAPI', { strategy: 'pause' });
|
|
201
|
+
limiter.recordResponse('PauseAPI', { remaining: 10, limit: 100 });
|
|
202
|
+
|
|
203
|
+
const delay = limiter.getThrottleDelay('PauseAPI');
|
|
204
|
+
expect(delay).toBe(0);
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
it('uses fallback RPM when no rate limit headers', () => {
|
|
208
|
+
limiter.configure('FallbackAPI', { strategy: 'throttle', fallbackRpm: 60 });
|
|
209
|
+
limiter.recordResponse('FallbackAPI', {}); // No rate limit info
|
|
210
|
+
|
|
211
|
+
const delay = limiter.getThrottleDelay('FallbackAPI');
|
|
212
|
+
// 60 RPM = 1 per second = 1000ms intervals
|
|
213
|
+
expect(delay).toBeLessThanOrEqual(1000);
|
|
214
|
+
});
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
describe('callbacks', () => {
|
|
218
|
+
it('calls onRateLimited when rate limited', async () => {
|
|
219
|
+
const onRateLimited = vi.fn();
|
|
220
|
+
limiter.setCallbacks({ onRateLimited });
|
|
221
|
+
// Use maxWait of 10s so it doesn't timeout immediately (wait is only 50ms)
|
|
222
|
+
limiter.configure('CallbackAPI', { strategy: 'pause', maxWait: 10 });
|
|
223
|
+
limiter.recordResponse('CallbackAPI', {
|
|
224
|
+
remaining: 0,
|
|
225
|
+
resetAt: new Date(Date.now() + 50), // Resets in 50ms
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
await limiter.waitForCapacity('CallbackAPI');
|
|
229
|
+
|
|
230
|
+
expect(onRateLimited).toHaveBeenCalledTimes(1);
|
|
231
|
+
expect(onRateLimited).toHaveBeenCalledWith(
|
|
232
|
+
expect.objectContaining({
|
|
233
|
+
source: 'CallbackAPI',
|
|
234
|
+
strategy: 'pause',
|
|
235
|
+
})
|
|
236
|
+
);
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
it('calls onResumed after waiting completes', async () => {
|
|
240
|
+
const onResumed = vi.fn();
|
|
241
|
+
limiter.setCallbacks({ onResumed });
|
|
242
|
+
limiter.configure('ResumeAPI', { strategy: 'pause', maxWait: 5 });
|
|
243
|
+
// Reset in 50ms
|
|
244
|
+
limiter.recordResponse('ResumeAPI', {
|
|
245
|
+
remaining: 0,
|
|
246
|
+
resetAt: new Date(Date.now() + 50),
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
await limiter.waitForCapacity('ResumeAPI');
|
|
250
|
+
|
|
251
|
+
expect(onResumed).toHaveBeenCalledTimes(1);
|
|
252
|
+
expect(onResumed).toHaveBeenCalledWith(
|
|
253
|
+
expect.objectContaining({
|
|
254
|
+
source: 'ResumeAPI',
|
|
255
|
+
})
|
|
256
|
+
);
|
|
257
|
+
});
|
|
258
|
+
});
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
describe('InMemoryTokenStore', () => {
|
|
262
|
+
let store: InMemoryTokenStore;
|
|
263
|
+
|
|
264
|
+
beforeEach(() => {
|
|
265
|
+
store = new InMemoryTokenStore();
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
it('stores and retrieves tokens', async () => {
|
|
269
|
+
const tokens: OAuth2Tokens = {
|
|
270
|
+
accessToken: 'access123',
|
|
271
|
+
refreshToken: 'refresh456',
|
|
272
|
+
expiresAt: new Date(Date.now() + 3600000),
|
|
273
|
+
};
|
|
274
|
+
|
|
275
|
+
await store.set('connection-1', tokens);
|
|
276
|
+
const retrieved = await store.get('connection-1');
|
|
277
|
+
|
|
278
|
+
expect(retrieved?.accessToken).toBe('access123');
|
|
279
|
+
expect(retrieved?.refreshToken).toBe('refresh456');
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
it('returns null for unknown connections', async () => {
|
|
283
|
+
const result = await store.get('unknown');
|
|
284
|
+
expect(result).toBeNull();
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
it('deletes tokens', async () => {
|
|
288
|
+
await store.set('connection-1', { accessToken: 'test' });
|
|
289
|
+
await store.delete('connection-1');
|
|
290
|
+
|
|
291
|
+
const result = await store.get('connection-1');
|
|
292
|
+
expect(result).toBeNull();
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
it('lists all connections', async () => {
|
|
296
|
+
await store.set('conn-1', { accessToken: 'a' });
|
|
297
|
+
await store.set('conn-2', { accessToken: 'b' });
|
|
298
|
+
await store.set('conn-3', { accessToken: 'c' });
|
|
299
|
+
|
|
300
|
+
const connections = await store.list();
|
|
301
|
+
|
|
302
|
+
expect(connections).toHaveLength(3);
|
|
303
|
+
expect(connections).toContain('conn-1');
|
|
304
|
+
expect(connections).toContain('conn-2');
|
|
305
|
+
expect(connections).toContain('conn-3');
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
it('identifies tokens needing refresh', async () => {
|
|
309
|
+
// Token expiring in 10 seconds (within 5 min buffer)
|
|
310
|
+
await store.set('expiring-soon', {
|
|
311
|
+
accessToken: 'test',
|
|
312
|
+
expiresAt: new Date(Date.now() + 10000),
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
// Token not expiring soon
|
|
316
|
+
await store.set('valid', {
|
|
317
|
+
accessToken: 'test',
|
|
318
|
+
expiresAt: new Date(Date.now() + 3600000),
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
const needsRefresh = await store.getTokensNeedingRefresh(300);
|
|
322
|
+
|
|
323
|
+
expect(needsRefresh).toContain('expiring-soon');
|
|
324
|
+
expect(needsRefresh).not.toContain('valid');
|
|
325
|
+
});
|
|
326
|
+
});
|