@dotdo/postgres 0.1.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/README.md +868 -0
- package/dist/cdc/change-stream.d.ts +44 -0
- package/dist/cdc/change-stream.d.ts.map +1 -0
- package/dist/cdc/change-stream.js +95 -0
- package/dist/cdc/change-stream.js.map +1 -0
- package/dist/cdc/filter.d.ts +58 -0
- package/dist/cdc/filter.d.ts.map +1 -0
- package/dist/cdc/filter.js +520 -0
- package/dist/cdc/filter.js.map +1 -0
- package/dist/cdc/index.d.ts +47 -0
- package/dist/cdc/index.d.ts.map +1 -0
- package/dist/cdc/index.js +50 -0
- package/dist/cdc/index.js.map +1 -0
- package/dist/cdc/resume-token.d.ts +60 -0
- package/dist/cdc/resume-token.d.ts.map +1 -0
- package/dist/cdc/resume-token.js +228 -0
- package/dist/cdc/resume-token.js.map +1 -0
- package/dist/cdc/transport/index.d.ts +7 -0
- package/dist/cdc/transport/index.d.ts.map +1 -0
- package/dist/cdc/transport/index.js +7 -0
- package/dist/cdc/transport/index.js.map +1 -0
- package/dist/cdc/transport/sse.d.ts +120 -0
- package/dist/cdc/transport/sse.d.ts.map +1 -0
- package/dist/cdc/transport/sse.js +590 -0
- package/dist/cdc/transport/sse.js.map +1 -0
- package/dist/cdc/transport/websocket.d.ts +130 -0
- package/dist/cdc/transport/websocket.d.ts.map +1 -0
- package/dist/cdc/transport/websocket.js +688 -0
- package/dist/cdc/transport/websocket.js.map +1 -0
- package/dist/cdc/types.d.ts +306 -0
- package/dist/cdc/types.d.ts.map +1 -0
- package/dist/cdc/types.js +8 -0
- package/dist/cdc/types.js.map +1 -0
- package/dist/config/index.d.ts +25 -0
- package/dist/config/index.d.ts.map +1 -0
- package/dist/config/index.js +25 -0
- package/dist/config/index.js.map +1 -0
- package/dist/config/memory.d.ts +139 -0
- package/dist/config/memory.d.ts.map +1 -0
- package/dist/config/memory.js +157 -0
- package/dist/config/memory.js.map +1 -0
- package/dist/config/storage.d.ts +157 -0
- package/dist/config/storage.d.ts.map +1 -0
- package/dist/config/storage.js +178 -0
- package/dist/config/storage.js.map +1 -0
- package/dist/config/streaming.d.ts +117 -0
- package/dist/config/streaming.d.ts.map +1 -0
- package/dist/config/streaming.js +132 -0
- package/dist/config/streaming.js.map +1 -0
- package/dist/config/timeouts.d.ts +168 -0
- package/dist/config/timeouts.d.ts.map +1 -0
- package/dist/config/timeouts.js +192 -0
- package/dist/config/timeouts.js.map +1 -0
- package/dist/extensions/config.d.ts +89 -0
- package/dist/extensions/config.d.ts.map +1 -0
- package/dist/extensions/config.js +216 -0
- package/dist/extensions/config.js.map +1 -0
- package/dist/extensions/geo.d.ts +452 -0
- package/dist/extensions/geo.d.ts.map +1 -0
- package/dist/extensions/geo.js +583 -0
- package/dist/extensions/geo.js.map +1 -0
- package/dist/extensions/index.d.ts +167 -0
- package/dist/extensions/index.d.ts.map +1 -0
- package/dist/extensions/index.js +99 -0
- package/dist/extensions/index.js.map +1 -0
- package/dist/extensions/loader.d.ts +226 -0
- package/dist/extensions/loader.d.ts.map +1 -0
- package/dist/extensions/loader.js +456 -0
- package/dist/extensions/loader.js.map +1 -0
- package/dist/extensions/pgmq-lite.d.ts +330 -0
- package/dist/extensions/pgmq-lite.d.ts.map +1 -0
- package/dist/extensions/pgmq-lite.js +648 -0
- package/dist/extensions/pgmq-lite.js.map +1 -0
- package/dist/extensions/plugins.d.ts +260 -0
- package/dist/extensions/plugins.d.ts.map +1 -0
- package/dist/extensions/plugins.js +535 -0
- package/dist/extensions/plugins.js.map +1 -0
- package/dist/extensions/registry.d.ts +93 -0
- package/dist/extensions/registry.d.ts.map +1 -0
- package/dist/extensions/registry.js +182 -0
- package/dist/extensions/registry.js.map +1 -0
- package/dist/extensions/vector.d.ts +106 -0
- package/dist/extensions/vector.d.ts.map +1 -0
- package/dist/extensions/vector.js +129 -0
- package/dist/extensions/vector.js.map +1 -0
- package/dist/iceberg/analytics.d.ts +279 -0
- package/dist/iceberg/analytics.d.ts.map +1 -0
- package/dist/iceberg/analytics.js +448 -0
- package/dist/iceberg/analytics.js.map +1 -0
- package/dist/iceberg/catalog-api.d.ts +39 -0
- package/dist/iceberg/catalog-api.d.ts.map +1 -0
- package/dist/iceberg/catalog-api.js +388 -0
- package/dist/iceberg/catalog-api.js.map +1 -0
- package/dist/iceberg/catalog.d.ts +401 -0
- package/dist/iceberg/catalog.d.ts.map +1 -0
- package/dist/iceberg/catalog.js +677 -0
- package/dist/iceberg/catalog.js.map +1 -0
- package/dist/iceberg/duckdb-wasm.d.ts +447 -0
- package/dist/iceberg/duckdb-wasm.d.ts.map +1 -0
- package/dist/iceberg/duckdb-wasm.js +600 -0
- package/dist/iceberg/duckdb-wasm.js.map +1 -0
- package/dist/iceberg/index.d.ts +92 -0
- package/dist/iceberg/index.d.ts.map +1 -0
- package/dist/iceberg/index.js +119 -0
- package/dist/iceberg/index.js.map +1 -0
- package/dist/iceberg/metadata.d.ts +214 -0
- package/dist/iceberg/metadata.d.ts.map +1 -0
- package/dist/iceberg/metadata.js +535 -0
- package/dist/iceberg/metadata.js.map +1 -0
- package/dist/iceberg/optimizer.d.ts +296 -0
- package/dist/iceberg/optimizer.d.ts.map +1 -0
- package/dist/iceberg/optimizer.js +889 -0
- package/dist/iceberg/optimizer.js.map +1 -0
- package/dist/iceberg/parquet.d.ts +447 -0
- package/dist/iceberg/parquet.d.ts.map +1 -0
- package/dist/iceberg/parquet.js +1225 -0
- package/dist/iceberg/parquet.js.map +1 -0
- package/dist/iceberg/r2-organization.d.ts +422 -0
- package/dist/iceberg/r2-organization.d.ts.map +1 -0
- package/dist/iceberg/r2-organization.js +672 -0
- package/dist/iceberg/r2-organization.js.map +1 -0
- package/dist/iceberg/scheduler-do-example.d.ts +158 -0
- package/dist/iceberg/scheduler-do-example.d.ts.map +1 -0
- package/dist/iceberg/scheduler-do-example.js +261 -0
- package/dist/iceberg/scheduler-do-example.js.map +1 -0
- package/dist/iceberg/scheduler.d.ts +434 -0
- package/dist/iceberg/scheduler.d.ts.map +1 -0
- package/dist/iceberg/scheduler.js +818 -0
- package/dist/iceberg/scheduler.js.map +1 -0
- package/dist/iceberg/schema.d.ts +149 -0
- package/dist/iceberg/schema.d.ts.map +1 -0
- package/dist/iceberg/schema.js +525 -0
- package/dist/iceberg/schema.js.map +1 -0
- package/dist/iceberg/snapshot-manager.d.ts +406 -0
- package/dist/iceberg/snapshot-manager.d.ts.map +1 -0
- package/dist/iceberg/snapshot-manager.js +934 -0
- package/dist/iceberg/snapshot-manager.js.map +1 -0
- package/dist/iceberg/sql-router.d.ts +194 -0
- package/dist/iceberg/sql-router.d.ts.map +1 -0
- package/dist/iceberg/sql-router.js +180 -0
- package/dist/iceberg/sql-router.js.map +1 -0
- package/dist/iceberg/test-fixtures.d.ts +151 -0
- package/dist/iceberg/test-fixtures.d.ts.map +1 -0
- package/dist/iceberg/test-fixtures.js +446 -0
- package/dist/iceberg/test-fixtures.js.map +1 -0
- package/dist/iceberg/time-travel-api.d.ts +102 -0
- package/dist/iceberg/time-travel-api.d.ts.map +1 -0
- package/dist/iceberg/time-travel-api.js +437 -0
- package/dist/iceberg/time-travel-api.js.map +1 -0
- package/dist/iceberg/time-travel.d.ts +293 -0
- package/dist/iceberg/time-travel.d.ts.map +1 -0
- package/dist/iceberg/time-travel.js +689 -0
- package/dist/iceberg/time-travel.js.map +1 -0
- package/dist/iceberg/transformer.d.ts +356 -0
- package/dist/iceberg/transformer.d.ts.map +1 -0
- package/dist/iceberg/transformer.js +770 -0
- package/dist/iceberg/transformer.js.map +1 -0
- package/dist/iceberg/types.d.ts +318 -0
- package/dist/iceberg/types.d.ts.map +1 -0
- package/dist/iceberg/types.js +9 -0
- package/dist/iceberg/types.js.map +1 -0
- package/dist/iceberg/writer.d.ts +144 -0
- package/dist/iceberg/writer.d.ts.map +1 -0
- package/dist/iceberg/writer.js +452 -0
- package/dist/iceberg/writer.js.map +1 -0
- package/dist/index.d.ts +50 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +69 -0
- package/dist/index.js.map +1 -0
- package/dist/lineage/index.d.ts +11 -0
- package/dist/lineage/index.d.ts.map +1 -0
- package/dist/lineage/index.js +11 -0
- package/dist/lineage/index.js.map +1 -0
- package/dist/lineage/integration.d.ts +134 -0
- package/dist/lineage/integration.d.ts.map +1 -0
- package/dist/lineage/integration.js +258 -0
- package/dist/lineage/integration.js.map +1 -0
- package/dist/lineage/tracker.d.ts +189 -0
- package/dist/lineage/tracker.d.ts.map +1 -0
- package/dist/lineage/tracker.js +1352 -0
- package/dist/lineage/tracker.js.map +1 -0
- package/dist/lineage/types.d.ts +318 -0
- package/dist/lineage/types.d.ts.map +1 -0
- package/dist/lineage/types.js +9 -0
- package/dist/lineage/types.js.map +1 -0
- package/dist/middleware/index.d.ts +11 -0
- package/dist/middleware/index.d.ts.map +1 -0
- package/dist/middleware/index.js +16 -0
- package/dist/middleware/index.js.map +1 -0
- package/dist/middleware/rate-limit.d.ts +397 -0
- package/dist/middleware/rate-limit.d.ts.map +1 -0
- package/dist/middleware/rate-limit.js +507 -0
- package/dist/middleware/rate-limit.js.map +1 -0
- package/dist/migration-tooling/external-migration.d.ts +601 -0
- package/dist/migration-tooling/external-migration.d.ts.map +1 -0
- package/dist/migration-tooling/external-migration.js +1612 -0
- package/dist/migration-tooling/external-migration.js.map +1 -0
- package/dist/migration-tooling/index.d.ts +19 -0
- package/dist/migration-tooling/index.d.ts.map +1 -0
- package/dist/migration-tooling/index.js +19 -0
- package/dist/migration-tooling/index.js.map +1 -0
- package/dist/migrations/auto-migrator.d.ts +289 -0
- package/dist/migrations/auto-migrator.d.ts.map +1 -0
- package/dist/migrations/auto-migrator.js +396 -0
- package/dist/migrations/auto-migrator.js.map +1 -0
- package/dist/migrations/bulk-orchestrator.d.ts +403 -0
- package/dist/migrations/bulk-orchestrator.d.ts.map +1 -0
- package/dist/migrations/bulk-orchestrator.js +646 -0
- package/dist/migrations/bulk-orchestrator.js.map +1 -0
- package/dist/migrations/compatibility.d.ts +216 -0
- package/dist/migrations/compatibility.d.ts.map +1 -0
- package/dist/migrations/compatibility.js +651 -0
- package/dist/migrations/compatibility.js.map +1 -0
- package/dist/migrations/do-migrations.d.ts +101 -0
- package/dist/migrations/do-migrations.d.ts.map +1 -0
- package/dist/migrations/do-migrations.js +1060 -0
- package/dist/migrations/do-migrations.js.map +1 -0
- package/dist/migrations/do-migrations.types.d.ts +550 -0
- package/dist/migrations/do-migrations.types.d.ts.map +1 -0
- package/dist/migrations/do-migrations.types.js +15 -0
- package/dist/migrations/do-migrations.types.js.map +1 -0
- package/dist/migrations/drizzle-compat.d.ts +163 -0
- package/dist/migrations/drizzle-compat.d.ts.map +1 -0
- package/dist/migrations/drizzle-compat.js +273 -0
- package/dist/migrations/drizzle-compat.js.map +1 -0
- package/dist/migrations/index.d.ts +109 -0
- package/dist/migrations/index.d.ts.map +1 -0
- package/dist/migrations/index.js +127 -0
- package/dist/migrations/index.js.map +1 -0
- package/dist/migrations/migration-api.d.ts +161 -0
- package/dist/migrations/migration-api.d.ts.map +1 -0
- package/dist/migrations/migration-api.js +499 -0
- package/dist/migrations/migration-api.js.map +1 -0
- package/dist/migrations/progress-tracker-do.d.ts +195 -0
- package/dist/migrations/progress-tracker-do.d.ts.map +1 -0
- package/dist/migrations/progress-tracker-do.js +339 -0
- package/dist/migrations/progress-tracker-do.js.map +1 -0
- package/dist/migrations/progress-tracker-kv.d.ts +103 -0
- package/dist/migrations/progress-tracker-kv.d.ts.map +1 -0
- package/dist/migrations/progress-tracker-kv.js +231 -0
- package/dist/migrations/progress-tracker-kv.js.map +1 -0
- package/dist/migrations/progress-tracker.d.ts +320 -0
- package/dist/migrations/progress-tracker.d.ts.map +1 -0
- package/dist/migrations/progress-tracker.js +443 -0
- package/dist/migrations/progress-tracker.js.map +1 -0
- package/dist/migrations/registry.d.ts +231 -0
- package/dist/migrations/registry.d.ts.map +1 -0
- package/dist/migrations/registry.js +376 -0
- package/dist/migrations/registry.js.map +1 -0
- package/dist/migrations/runner.d.ts +197 -0
- package/dist/migrations/runner.d.ts.map +1 -0
- package/dist/migrations/runner.js +1167 -0
- package/dist/migrations/runner.js.map +1 -0
- package/dist/migrations/schema-generator.d.ts +111 -0
- package/dist/migrations/schema-generator.d.ts.map +1 -0
- package/dist/migrations/schema-generator.js +335 -0
- package/dist/migrations/schema-generator.js.map +1 -0
- package/dist/migrations/testing.d.ts +321 -0
- package/dist/migrations/testing.d.ts.map +1 -0
- package/dist/migrations/testing.js +645 -0
- package/dist/migrations/testing.js.map +1 -0
- package/dist/migrations/types.d.ts +503 -0
- package/dist/migrations/types.d.ts.map +1 -0
- package/dist/migrations/types.js +11 -0
- package/dist/migrations/types.js.map +1 -0
- package/dist/migrations/validator.d.ts +215 -0
- package/dist/migrations/validator.d.ts.map +1 -0
- package/dist/migrations/validator.js +494 -0
- package/dist/migrations/validator.js.map +1 -0
- package/dist/observability/alerting.d.ts +116 -0
- package/dist/observability/alerting.d.ts.map +1 -0
- package/dist/observability/alerting.js +353 -0
- package/dist/observability/alerting.js.map +1 -0
- package/dist/observability/analytics-engine.d.ts +357 -0
- package/dist/observability/analytics-engine.d.ts.map +1 -0
- package/dist/observability/analytics-engine.js +430 -0
- package/dist/observability/analytics-engine.js.map +1 -0
- package/dist/observability/cost-metrics.d.ts +269 -0
- package/dist/observability/cost-metrics.d.ts.map +1 -0
- package/dist/observability/cost-metrics.js +560 -0
- package/dist/observability/cost-metrics.js.map +1 -0
- package/dist/observability/cross-do-tracing.d.ts +305 -0
- package/dist/observability/cross-do-tracing.d.ts.map +1 -0
- package/dist/observability/cross-do-tracing.js +431 -0
- package/dist/observability/cross-do-tracing.js.map +1 -0
- package/dist/observability/error-rate-collector.d.ts +163 -0
- package/dist/observability/error-rate-collector.d.ts.map +1 -0
- package/dist/observability/error-rate-collector.js +306 -0
- package/dist/observability/error-rate-collector.js.map +1 -0
- package/dist/observability/exporters.d.ts +231 -0
- package/dist/observability/exporters.d.ts.map +1 -0
- package/dist/observability/exporters.js +479 -0
- package/dist/observability/exporters.js.map +1 -0
- package/dist/observability/health-check.d.ts +106 -0
- package/dist/observability/health-check.d.ts.map +1 -0
- package/dist/observability/health-check.js +243 -0
- package/dist/observability/health-check.js.map +1 -0
- package/dist/observability/index.d.ts +297 -0
- package/dist/observability/index.d.ts.map +1 -0
- package/dist/observability/index.js +455 -0
- package/dist/observability/index.js.map +1 -0
- package/dist/observability/instrumentation.d.ts +222 -0
- package/dist/observability/instrumentation.d.ts.map +1 -0
- package/dist/observability/instrumentation.js +532 -0
- package/dist/observability/instrumentation.js.map +1 -0
- package/dist/observability/memory-metrics.d.ts +227 -0
- package/dist/observability/memory-metrics.d.ts.map +1 -0
- package/dist/observability/memory-metrics.js +688 -0
- package/dist/observability/memory-metrics.js.map +1 -0
- package/dist/observability/metrics-endpoint.d.ts +91 -0
- package/dist/observability/metrics-endpoint.d.ts.map +1 -0
- package/dist/observability/metrics-endpoint.js +246 -0
- package/dist/observability/metrics-endpoint.js.map +1 -0
- package/dist/observability/metrics.d.ts +88 -0
- package/dist/observability/metrics.d.ts.map +1 -0
- package/dist/observability/metrics.js +253 -0
- package/dist/observability/metrics.js.map +1 -0
- package/dist/observability/observability-features.d.ts +488 -0
- package/dist/observability/observability-features.d.ts.map +1 -0
- package/dist/observability/observability-features.js +773 -0
- package/dist/observability/observability-features.js.map +1 -0
- package/dist/observability/prometheus.d.ts +39 -0
- package/dist/observability/prometheus.d.ts.map +1 -0
- package/dist/observability/prometheus.js +120 -0
- package/dist/observability/prometheus.js.map +1 -0
- package/dist/observability/propagation.d.ts +126 -0
- package/dist/observability/propagation.d.ts.map +1 -0
- package/dist/observability/propagation.js +234 -0
- package/dist/observability/propagation.js.map +1 -0
- package/dist/observability/query-latency.d.ts +243 -0
- package/dist/observability/query-latency.d.ts.map +1 -0
- package/dist/observability/query-latency.js +292 -0
- package/dist/observability/query-latency.js.map +1 -0
- package/dist/observability/query-performance.d.ts +169 -0
- package/dist/observability/query-performance.d.ts.map +1 -0
- package/dist/observability/query-performance.js +290 -0
- package/dist/observability/query-performance.js.map +1 -0
- package/dist/observability/storage-tier-metrics.d.ts +174 -0
- package/dist/observability/storage-tier-metrics.d.ts.map +1 -0
- package/dist/observability/storage-tier-metrics.js +306 -0
- package/dist/observability/storage-tier-metrics.js.map +1 -0
- package/dist/observability/tier-cost-optimizer.d.ts +155 -0
- package/dist/observability/tier-cost-optimizer.d.ts.map +1 -0
- package/dist/observability/tier-cost-optimizer.js +536 -0
- package/dist/observability/tier-cost-optimizer.js.map +1 -0
- package/dist/observability/tracer.d.ts +149 -0
- package/dist/observability/tracer.d.ts.map +1 -0
- package/dist/observability/tracer.js +435 -0
- package/dist/observability/tracer.js.map +1 -0
- package/dist/observability/types.d.ts +402 -0
- package/dist/observability/types.d.ts.map +1 -0
- package/dist/observability/types.js +103 -0
- package/dist/observability/types.js.map +1 -0
- package/dist/pglite/workers-pglite.d.ts +138 -0
- package/dist/pglite/workers-pglite.d.ts.map +1 -0
- package/dist/pglite/workers-pglite.js +143 -0
- package/dist/pglite/workers-pglite.js.map +1 -0
- package/dist/pglite-assets/pglite.data +0 -0
- package/dist/pglite-assets/pglite.wasm +0 -0
- package/dist/playground/index.d.ts +52 -0
- package/dist/playground/index.d.ts.map +1 -0
- package/dist/playground/index.js +55 -0
- package/dist/playground/index.js.map +1 -0
- package/dist/playground/keyboard-shortcuts.d.ts +116 -0
- package/dist/playground/keyboard-shortcuts.d.ts.map +1 -0
- package/dist/playground/keyboard-shortcuts.js +588 -0
- package/dist/playground/keyboard-shortcuts.js.map +1 -0
- package/dist/playground/playground.d.ts +82 -0
- package/dist/playground/playground.d.ts.map +1 -0
- package/dist/playground/playground.js +271 -0
- package/dist/playground/playground.js.map +1 -0
- package/dist/playground/query-executor.d.ts +115 -0
- package/dist/playground/query-executor.d.ts.map +1 -0
- package/dist/playground/query-executor.js +558 -0
- package/dist/playground/query-executor.js.map +1 -0
- package/dist/playground/query-history.d.ts +92 -0
- package/dist/playground/query-history.d.ts.map +1 -0
- package/dist/playground/query-history.js +259 -0
- package/dist/playground/query-history.js.map +1 -0
- package/dist/playground/result-formatter.d.ts +59 -0
- package/dist/playground/result-formatter.d.ts.map +1 -0
- package/dist/playground/result-formatter.js +341 -0
- package/dist/playground/result-formatter.js.map +1 -0
- package/dist/playground/sample-datasets.d.ts +77 -0
- package/dist/playground/sample-datasets.d.ts.map +1 -0
- package/dist/playground/sample-datasets.js +641 -0
- package/dist/playground/sample-datasets.js.map +1 -0
- package/dist/playground/sample-queries.d.ts +73 -0
- package/dist/playground/sample-queries.d.ts.map +1 -0
- package/dist/playground/sample-queries.js +1095 -0
- package/dist/playground/sample-queries.js.map +1 -0
- package/dist/playground/schema-explorer.d.ts +55 -0
- package/dist/playground/schema-explorer.d.ts.map +1 -0
- package/dist/playground/schema-explorer.js +473 -0
- package/dist/playground/schema-explorer.js.map +1 -0
- package/dist/playground/types.d.ts +430 -0
- package/dist/playground/types.d.ts.map +1 -0
- package/dist/playground/types.js +10 -0
- package/dist/playground/types.js.map +1 -0
- package/dist/readonly/cache-reader.d.ts +145 -0
- package/dist/readonly/cache-reader.d.ts.map +1 -0
- package/dist/readonly/cache-reader.js +198 -0
- package/dist/readonly/cache-reader.js.map +1 -0
- package/dist/readonly/config.d.ts +74 -0
- package/dist/readonly/config.d.ts.map +1 -0
- package/dist/readonly/config.js +67 -0
- package/dist/readonly/config.js.map +1 -0
- package/dist/readonly/index.d.ts +22 -0
- package/dist/readonly/index.d.ts.map +1 -0
- package/dist/readonly/index.js +17 -0
- package/dist/readonly/index.js.map +1 -0
- package/dist/readonly/pglite-wrapper.d.ts +82 -0
- package/dist/readonly/pglite-wrapper.d.ts.map +1 -0
- package/dist/readonly/pglite-wrapper.js +123 -0
- package/dist/readonly/pglite-wrapper.js.map +1 -0
- package/dist/readonly/worker.d.ts +142 -0
- package/dist/readonly/worker.d.ts.map +1 -0
- package/dist/readonly/worker.js +187 -0
- package/dist/readonly/worker.js.map +1 -0
- package/dist/readonly/write-blocker.d.ts +47 -0
- package/dist/readonly/write-blocker.d.ts.map +1 -0
- package/dist/readonly/write-blocker.js +136 -0
- package/dist/readonly/write-blocker.js.map +1 -0
- package/dist/recovery/disaster-recovery.d.ts +326 -0
- package/dist/recovery/disaster-recovery.d.ts.map +1 -0
- package/dist/recovery/disaster-recovery.js +799 -0
- package/dist/recovery/disaster-recovery.js.map +1 -0
- package/dist/recovery/index.d.ts +12 -0
- package/dist/recovery/index.d.ts.map +1 -0
- package/dist/recovery/index.js +12 -0
- package/dist/recovery/index.js.map +1 -0
- package/dist/recovery/parquet-parser.d.ts +321 -0
- package/dist/recovery/parquet-parser.d.ts.map +1 -0
- package/dist/recovery/parquet-parser.js +797 -0
- package/dist/recovery/parquet-parser.js.map +1 -0
- package/dist/retention/index.d.ts +50 -0
- package/dist/retention/index.d.ts.map +1 -0
- package/dist/retention/index.js +50 -0
- package/dist/retention/index.js.map +1 -0
- package/dist/retention/policy.d.ts +344 -0
- package/dist/retention/policy.d.ts.map +1 -0
- package/dist/retention/policy.js +472 -0
- package/dist/retention/policy.js.map +1 -0
- package/dist/retention/purger.d.ts +187 -0
- package/dist/retention/purger.d.ts.map +1 -0
- package/dist/retention/purger.js +411 -0
- package/dist/retention/purger.js.map +1 -0
- package/dist/rls/auth-integration.d.ts +280 -0
- package/dist/rls/auth-integration.d.ts.map +1 -0
- package/dist/rls/auth-integration.js +399 -0
- package/dist/rls/auth-integration.js.map +1 -0
- package/dist/rls/generator.d.ts +249 -0
- package/dist/rls/generator.d.ts.map +1 -0
- package/dist/rls/generator.js +495 -0
- package/dist/rls/generator.js.map +1 -0
- package/dist/rls/index.d.ts +26 -0
- package/dist/rls/index.d.ts.map +1 -0
- package/dist/rls/index.js +58 -0
- package/dist/rls/index.js.map +1 -0
- package/dist/rls/policy.d.ts +116 -0
- package/dist/rls/policy.d.ts.map +1 -0
- package/dist/rls/policy.js +77 -0
- package/dist/rls/policy.js.map +1 -0
- package/dist/rls/validator.d.ts +155 -0
- package/dist/rls/validator.d.ts.map +1 -0
- package/dist/rls/validator.js +792 -0
- package/dist/rls/validator.js.map +1 -0
- package/dist/routing/adaptive-router.d.ts +317 -0
- package/dist/routing/adaptive-router.d.ts.map +1 -0
- package/dist/routing/adaptive-router.js +554 -0
- package/dist/routing/adaptive-router.js.map +1 -0
- package/dist/routing/circuit-breaker.d.ts +339 -0
- package/dist/routing/circuit-breaker.d.ts.map +1 -0
- package/dist/routing/circuit-breaker.js +620 -0
- package/dist/routing/circuit-breaker.js.map +1 -0
- package/dist/routing/cost-metrics.d.ts +133 -0
- package/dist/routing/cost-metrics.d.ts.map +1 -0
- package/dist/routing/cost-metrics.js +259 -0
- package/dist/routing/cost-metrics.js.map +1 -0
- package/dist/routing/do-connection-pool.d.ts +243 -0
- package/dist/routing/do-connection-pool.d.ts.map +1 -0
- package/dist/routing/do-connection-pool.js +572 -0
- package/dist/routing/do-connection-pool.js.map +1 -0
- package/dist/routing/index.d.ts +59 -0
- package/dist/routing/index.d.ts.map +1 -0
- package/dist/routing/index.js +59 -0
- package/dist/routing/index.js.map +1 -0
- package/dist/routing/query-complexity-estimator.d.ts +73 -0
- package/dist/routing/query-complexity-estimator.d.ts.map +1 -0
- package/dist/routing/query-complexity-estimator.js +327 -0
- package/dist/routing/query-complexity-estimator.js.map +1 -0
- package/dist/routing/request-coalescing.d.ts +178 -0
- package/dist/routing/request-coalescing.d.ts.map +1 -0
- package/dist/routing/request-coalescing.js +325 -0
- package/dist/routing/request-coalescing.js.map +1 -0
- package/dist/routing/runtime-router.d.ts +107 -0
- package/dist/routing/runtime-router.d.ts.map +1 -0
- package/dist/routing/runtime-router.js +246 -0
- package/dist/routing/runtime-router.js.map +1 -0
- package/dist/routing/tenant-router.d.ts +848 -0
- package/dist/routing/tenant-router.d.ts.map +1 -0
- package/dist/routing/tenant-router.js +1056 -0
- package/dist/routing/tenant-router.js.map +1 -0
- package/dist/routing/websocket-pool.d.ts +119 -0
- package/dist/routing/websocket-pool.d.ts.map +1 -0
- package/dist/routing/websocket-pool.js +436 -0
- package/dist/routing/websocket-pool.js.map +1 -0
- package/dist/storage/cache-layer.d.ts +159 -0
- package/dist/storage/cache-layer.d.ts.map +1 -0
- package/dist/storage/cache-layer.js +245 -0
- package/dist/storage/cache-layer.js.map +1 -0
- package/dist/storage/cost-aware-tiering.d.ts +258 -0
- package/dist/storage/cost-aware-tiering.d.ts.map +1 -0
- package/dist/storage/cost-aware-tiering.js +526 -0
- package/dist/storage/cost-aware-tiering.js.map +1 -0
- package/dist/storage/index.d.ts +87 -0
- package/dist/storage/index.d.ts.map +1 -0
- package/dist/storage/index.js +78 -0
- package/dist/storage/index.js.map +1 -0
- package/dist/storage/interfaces.d.ts +856 -0
- package/dist/storage/interfaces.d.ts.map +1 -0
- package/dist/storage/interfaces.js +69 -0
- package/dist/storage/interfaces.js.map +1 -0
- package/dist/storage/r2-layer.d.ts +226 -0
- package/dist/storage/r2-layer.d.ts.map +1 -0
- package/dist/storage/r2-layer.js +307 -0
- package/dist/storage/r2-layer.js.map +1 -0
- package/dist/storage/r2-overflow.d.ts +344 -0
- package/dist/storage/r2-overflow.d.ts.map +1 -0
- package/dist/storage/r2-overflow.js +730 -0
- package/dist/storage/r2-overflow.js.map +1 -0
- package/dist/storage/r2-page-vfs.d.ts +374 -0
- package/dist/storage/r2-page-vfs.d.ts.map +1 -0
- package/dist/storage/r2-page-vfs.js +754 -0
- package/dist/storage/r2-page-vfs.js.map +1 -0
- package/dist/storage/swr-cache.d.ts +181 -0
- package/dist/storage/swr-cache.d.ts.map +1 -0
- package/dist/storage/swr-cache.js +295 -0
- package/dist/storage/swr-cache.js.map +1 -0
- package/dist/storage/tiered-orchestrator.d.ts +951 -0
- package/dist/storage/tiered-orchestrator.d.ts.map +1 -0
- package/dist/storage/tiered-orchestrator.js +1731 -0
- package/dist/storage/tiered-orchestrator.js.map +1 -0
- package/dist/storage/tiered-vfs-swr.d.ts +279 -0
- package/dist/storage/tiered-vfs-swr.d.ts.map +1 -0
- package/dist/storage/tiered-vfs-swr.js +584 -0
- package/dist/storage/tiered-vfs-swr.js.map +1 -0
- package/dist/storage/tiered-vfs.d.ts +405 -0
- package/dist/storage/tiered-vfs.d.ts.map +1 -0
- package/dist/storage/tiered-vfs.js +833 -0
- package/dist/storage/tiered-vfs.js.map +1 -0
- package/dist/streaming/backpressure-controller.d.ts +173 -0
- package/dist/streaming/backpressure-controller.d.ts.map +1 -0
- package/dist/streaming/backpressure-controller.js +344 -0
- package/dist/streaming/backpressure-controller.js.map +1 -0
- package/dist/streaming/buffer-pool.d.ts +241 -0
- package/dist/streaming/buffer-pool.d.ts.map +1 -0
- package/dist/streaming/buffer-pool.js +381 -0
- package/dist/streaming/buffer-pool.js.map +1 -0
- package/dist/streaming/cdc-iceberg-connector.d.ts +272 -0
- package/dist/streaming/cdc-iceberg-connector.d.ts.map +1 -0
- package/dist/streaming/cdc-iceberg-connector.js +408 -0
- package/dist/streaming/cdc-iceberg-connector.js.map +1 -0
- package/dist/streaming/index.d.ts +111 -0
- package/dist/streaming/index.d.ts.map +1 -0
- package/dist/streaming/index.js +128 -0
- package/dist/streaming/index.js.map +1 -0
- package/dist/streaming/live-cdc-stream.d.ts +400 -0
- package/dist/streaming/live-cdc-stream.d.ts.map +1 -0
- package/dist/streaming/live-cdc-stream.js +703 -0
- package/dist/streaming/live-cdc-stream.js.map +1 -0
- package/dist/streaming/memory-bounded-stream.d.ts +207 -0
- package/dist/streaming/memory-bounded-stream.d.ts.map +1 -0
- package/dist/streaming/memory-bounded-stream.js +340 -0
- package/dist/streaming/memory-bounded-stream.js.map +1 -0
- package/dist/streaming/query-streamer.d.ts +379 -0
- package/dist/streaming/query-streamer.d.ts.map +1 -0
- package/dist/streaming/query-streamer.js +495 -0
- package/dist/streaming/query-streamer.js.map +1 -0
- package/dist/streaming/response-streaming.d.ts +203 -0
- package/dist/streaming/response-streaming.d.ts.map +1 -0
- package/dist/streaming/response-streaming.js +449 -0
- package/dist/streaming/response-streaming.js.map +1 -0
- package/dist/types/branded.d.ts +859 -0
- package/dist/types/branded.d.ts.map +1 -0
- package/dist/types/branded.js +891 -0
- package/dist/types/branded.js.map +1 -0
- package/dist/types/utilities.d.ts +757 -0
- package/dist/types/utilities.d.ts.map +1 -0
- package/dist/types/utilities.js +447 -0
- package/dist/types/utilities.js.map +1 -0
- package/dist/wal/replay-engine.d.ts +344 -0
- package/dist/wal/replay-engine.d.ts.map +1 -0
- package/dist/wal/replay-engine.js +975 -0
- package/dist/wal/replay-engine.js.map +1 -0
- package/dist/worker/__mocks__/capnweb.d.ts +13 -0
- package/dist/worker/__mocks__/capnweb.d.ts.map +1 -0
- package/dist/worker/__mocks__/capnweb.js +15 -0
- package/dist/worker/__mocks__/capnweb.js.map +1 -0
- package/dist/worker/__mocks__/cloudflare-workers.d.ts +31 -0
- package/dist/worker/__mocks__/cloudflare-workers.d.ts.map +1 -0
- package/dist/worker/__mocks__/cloudflare-workers.js +33 -0
- package/dist/worker/__mocks__/cloudflare-workers.js.map +1 -0
- package/dist/worker/__mocks__/pglite.data.d.ts +3 -0
- package/dist/worker/__mocks__/pglite.data.d.ts.map +1 -0
- package/dist/worker/__mocks__/pglite.data.js +20 -0
- package/dist/worker/__mocks__/pglite.data.js.map +1 -0
- package/dist/worker/__mocks__/pglite.wasm.d.ts +3 -0
- package/dist/worker/__mocks__/pglite.wasm.d.ts.map +1 -0
- package/dist/worker/__mocks__/pglite.wasm.js +30 -0
- package/dist/worker/__mocks__/pglite.wasm.js.map +1 -0
- package/dist/worker/auth-rate-limiter.d.ts +270 -0
- package/dist/worker/auth-rate-limiter.d.ts.map +1 -0
- package/dist/worker/auth-rate-limiter.js +332 -0
- package/dist/worker/auth-rate-limiter.js.map +1 -0
- package/dist/worker/auth.d.ts +345 -0
- package/dist/worker/auth.d.ts.map +1 -0
- package/dist/worker/auth.js +837 -0
- package/dist/worker/auth.js.map +1 -0
- package/dist/worker/cdc-backpressure.d.ts +338 -0
- package/dist/worker/cdc-backpressure.d.ts.map +1 -0
- package/dist/worker/cdc-backpressure.js +619 -0
- package/dist/worker/cdc-backpressure.js.map +1 -0
- package/dist/worker/cdc-sse.d.ts +277 -0
- package/dist/worker/cdc-sse.d.ts.map +1 -0
- package/dist/worker/cdc-sse.js +528 -0
- package/dist/worker/cdc-sse.js.map +1 -0
- package/dist/worker/cdc-websocket.d.ts +252 -0
- package/dist/worker/cdc-websocket.d.ts.map +1 -0
- package/dist/worker/cdc-websocket.js +940 -0
- package/dist/worker/cdc-websocket.js.map +1 -0
- package/dist/worker/cdc.d.ts +95 -0
- package/dist/worker/cdc.d.ts.map +1 -0
- package/dist/worker/cdc.js +211 -0
- package/dist/worker/cdc.js.map +1 -0
- package/dist/worker/concerns/auth-concern.d.ts +50 -0
- package/dist/worker/concerns/auth-concern.d.ts.map +1 -0
- package/dist/worker/concerns/auth-concern.js +131 -0
- package/dist/worker/concerns/auth-concern.js.map +1 -0
- package/dist/worker/concerns/cdc-concern.d.ts +99 -0
- package/dist/worker/concerns/cdc-concern.d.ts.map +1 -0
- package/dist/worker/concerns/cdc-concern.js +137 -0
- package/dist/worker/concerns/cdc-concern.js.map +1 -0
- package/dist/worker/concerns/index.d.ts +22 -0
- package/dist/worker/concerns/index.d.ts.map +1 -0
- package/dist/worker/concerns/index.js +13 -0
- package/dist/worker/concerns/index.js.map +1 -0
- package/dist/worker/concerns/query-execution-concern.d.ts +104 -0
- package/dist/worker/concerns/query-execution-concern.d.ts.map +1 -0
- package/dist/worker/concerns/query-execution-concern.js +95 -0
- package/dist/worker/concerns/query-execution-concern.js.map +1 -0
- package/dist/worker/concerns/storage-orchestration-concern.d.ts +78 -0
- package/dist/worker/concerns/storage-orchestration-concern.d.ts.map +1 -0
- package/dist/worker/concerns/storage-orchestration-concern.js +240 -0
- package/dist/worker/concerns/storage-orchestration-concern.js.map +1 -0
- package/dist/worker/do-auth-manager.d.ts +108 -0
- package/dist/worker/do-auth-manager.d.ts.map +1 -0
- package/dist/worker/do-auth-manager.js +212 -0
- package/dist/worker/do-auth-manager.js.map +1 -0
- package/dist/worker/do-pglite-manager.d.ts +137 -0
- package/dist/worker/do-pglite-manager.d.ts.map +1 -0
- package/dist/worker/do-pglite-manager.js +228 -0
- package/dist/worker/do-pglite-manager.js.map +1 -0
- package/dist/worker/do.d.ts +556 -0
- package/dist/worker/do.d.ts.map +1 -0
- package/dist/worker/do.js +1441 -0
- package/dist/worker/do.js.map +1 -0
- package/dist/worker/entry.d.ts +23 -0
- package/dist/worker/entry.d.ts.map +1 -0
- package/dist/worker/entry.js +362 -0
- package/dist/worker/entry.js.map +1 -0
- package/dist/worker/errors.d.ts +106 -0
- package/dist/worker/errors.d.ts.map +1 -0
- package/dist/worker/errors.js +178 -0
- package/dist/worker/errors.js.map +1 -0
- package/dist/worker/health-check-manager.d.ts +141 -0
- package/dist/worker/health-check-manager.d.ts.map +1 -0
- package/dist/worker/health-check-manager.js +145 -0
- package/dist/worker/health-check-manager.js.map +1 -0
- package/dist/worker/index.d.ts +60 -0
- package/dist/worker/index.d.ts.map +1 -0
- package/dist/worker/index.js +67 -0
- package/dist/worker/index.js.map +1 -0
- package/dist/worker/memory-pressure.d.ts +892 -0
- package/dist/worker/memory-pressure.d.ts.map +1 -0
- package/dist/worker/memory-pressure.js +1990 -0
- package/dist/worker/memory-pressure.js.map +1 -0
- package/dist/worker/migration-manager.d.ts +153 -0
- package/dist/worker/migration-manager.d.ts.map +1 -0
- package/dist/worker/migration-manager.js +461 -0
- package/dist/worker/migration-manager.js.map +1 -0
- package/dist/worker/plugin-manager.d.ts +147 -0
- package/dist/worker/plugin-manager.d.ts.map +1 -0
- package/dist/worker/plugin-manager.js +408 -0
- package/dist/worker/plugin-manager.js.map +1 -0
- package/dist/worker/proxy.d.ts +330 -0
- package/dist/worker/proxy.d.ts.map +1 -0
- package/dist/worker/proxy.js +504 -0
- package/dist/worker/proxy.js.map +1 -0
- package/dist/worker/query-execution-manager.d.ts +107 -0
- package/dist/worker/query-execution-manager.d.ts.map +1 -0
- package/dist/worker/query-execution-manager.js +155 -0
- package/dist/worker/query-execution-manager.js.map +1 -0
- package/dist/worker/query-executor.d.ts +163 -0
- package/dist/worker/query-executor.d.ts.map +1 -0
- package/dist/worker/query-executor.js +413 -0
- package/dist/worker/query-executor.js.map +1 -0
- package/dist/worker/query-stats-manager.d.ts +117 -0
- package/dist/worker/query-stats-manager.d.ts.map +1 -0
- package/dist/worker/query-stats-manager.js +162 -0
- package/dist/worker/query-stats-manager.js.map +1 -0
- package/dist/worker/result-handler.d.ts +192 -0
- package/dist/worker/result-handler.d.ts.map +1 -0
- package/dist/worker/result-handler.js +346 -0
- package/dist/worker/result-handler.js.map +1 -0
- package/dist/worker/routes.d.ts +135 -0
- package/dist/worker/routes.d.ts.map +1 -0
- package/dist/worker/routes.js +460 -0
- package/dist/worker/routes.js.map +1 -0
- package/dist/worker/rpc-methods-manager.d.ts +142 -0
- package/dist/worker/rpc-methods-manager.d.ts.map +1 -0
- package/dist/worker/rpc-methods-manager.js +195 -0
- package/dist/worker/rpc-methods-manager.js.map +1 -0
- package/dist/worker/rpc.d.ts +259 -0
- package/dist/worker/rpc.d.ts.map +1 -0
- package/dist/worker/rpc.js +398 -0
- package/dist/worker/rpc.js.map +1 -0
- package/dist/worker/schema-version.d.ts +209 -0
- package/dist/worker/schema-version.d.ts.map +1 -0
- package/dist/worker/schema-version.js +450 -0
- package/dist/worker/schema-version.js.map +1 -0
- package/dist/worker/session-manager.d.ts +282 -0
- package/dist/worker/session-manager.d.ts.map +1 -0
- package/dist/worker/session-manager.js +523 -0
- package/dist/worker/session-manager.js.map +1 -0
- package/dist/worker/shutdown-manager.d.ts +188 -0
- package/dist/worker/shutdown-manager.d.ts.map +1 -0
- package/dist/worker/shutdown-manager.js +347 -0
- package/dist/worker/shutdown-manager.js.map +1 -0
- package/dist/worker/sql-transform.d.ts +61 -0
- package/dist/worker/sql-transform.d.ts.map +1 -0
- package/dist/worker/sql-transform.js +312 -0
- package/dist/worker/sql-transform.js.map +1 -0
- package/dist/worker/types.d.ts +738 -0
- package/dist/worker/types.d.ts.map +1 -0
- package/dist/worker/types.js +6 -0
- package/dist/worker/types.js.map +1 -0
- package/dist/worker/user-routes.d.ts +76 -0
- package/dist/worker/user-routes.d.ts.map +1 -0
- package/dist/worker/user-routes.js +188 -0
- package/dist/worker/user-routes.js.map +1 -0
- package/dist/worker/wal-facade.d.ts +138 -0
- package/dist/worker/wal-facade.d.ts.map +1 -0
- package/dist/worker/wal-facade.js +184 -0
- package/dist/worker/wal-facade.js.map +1 -0
- package/dist/worker/wal-r2.d.ts +271 -0
- package/dist/worker/wal-r2.d.ts.map +1 -0
- package/dist/worker/wal-r2.js +689 -0
- package/dist/worker/wal-r2.js.map +1 -0
- package/dist/worker/wal-replay.d.ts +361 -0
- package/dist/worker/wal-replay.d.ts.map +1 -0
- package/dist/worker/wal-replay.js +628 -0
- package/dist/worker/wal-replay.js.map +1 -0
- package/dist/worker/wal-retention.d.ts +389 -0
- package/dist/worker/wal-retention.d.ts.map +1 -0
- package/dist/worker/wal-retention.js +763 -0
- package/dist/worker/wal-retention.js.map +1 -0
- package/dist/worker/wal.d.ts +278 -0
- package/dist/worker/wal.d.ts.map +1 -0
- package/dist/worker/wal.js +467 -0
- package/dist/worker/wal.js.map +1 -0
- package/dist/worker/websocket.d.ts +85 -0
- package/dist/worker/websocket.d.ts.map +1 -0
- package/dist/worker/websocket.js +227 -0
- package/dist/worker/websocket.js.map +1 -0
- package/package.json +108 -0
- package/src/cdc/change-stream.ts +137 -0
- package/src/cdc/filter.ts +646 -0
- package/src/cdc/index.ts +112 -0
- package/src/cdc/resume-token.ts +280 -0
- package/src/cdc/transport/index.ts +7 -0
- package/src/cdc/transport/sse.ts +723 -0
- package/src/cdc/transport/websocket.ts +873 -0
- package/src/cdc/types.ts +346 -0
- package/src/config/index.ts +25 -0
- package/src/config/memory.ts +177 -0
- package/src/config/storage.ts +204 -0
- package/src/config/streaming.ts +147 -0
- package/src/config/timeouts.ts +221 -0
- package/src/extensions/config.test.ts +187 -0
- package/src/extensions/config.ts +278 -0
- package/src/extensions/geo.test.ts +455 -0
- package/src/extensions/geo.ts +858 -0
- package/src/extensions/index.test.ts +259 -0
- package/src/extensions/index.ts +227 -0
- package/src/extensions/loader.test.ts +555 -0
- package/src/extensions/loader.ts +588 -0
- package/src/extensions/pgmq-lite.test.ts +727 -0
- package/src/extensions/pgmq-lite.ts +770 -0
- package/src/extensions/plugins.test.ts +528 -0
- package/src/extensions/plugins.ts +718 -0
- package/src/extensions/registry.test.ts +202 -0
- package/src/extensions/registry.ts +267 -0
- package/src/extensions/vector.test.ts +195 -0
- package/src/extensions/vector.ts +217 -0
- package/src/iceberg/SCHEDULER.md +580 -0
- package/src/iceberg/analytics.test.ts +703 -0
- package/src/iceberg/analytics.ts +727 -0
- package/src/iceberg/catalog-api.test.ts +838 -0
- package/src/iceberg/catalog-api.ts +520 -0
- package/src/iceberg/catalog.test.ts +680 -0
- package/src/iceberg/catalog.ts +1007 -0
- package/src/iceberg/iceberg.test.ts +705 -0
- package/src/iceberg/index.ts +406 -0
- package/src/iceberg/metadata.test.ts +632 -0
- package/src/iceberg/metadata.ts +649 -0
- package/src/iceberg/optimizer.test.ts +868 -0
- package/src/iceberg/optimizer.ts +1287 -0
- package/src/iceberg/parquet.test.ts +899 -0
- package/src/iceberg/parquet.ts +1640 -0
- package/src/iceberg/r2-organization.test.ts +615 -0
- package/src/iceberg/r2-organization.ts +951 -0
- package/src/iceberg/scheduler-do-example.ts +364 -0
- package/src/iceberg/scheduler.test.ts +861 -0
- package/src/iceberg/scheduler.ts +1201 -0
- package/src/iceberg/schema.test.ts +547 -0
- package/src/iceberg/schema.ts +616 -0
- package/src/iceberg/snapshot-manager.test.ts +919 -0
- package/src/iceberg/snapshot-manager.ts +1369 -0
- package/src/iceberg/sql-router.test.ts +334 -0
- package/src/iceberg/sql-router.ts +337 -0
- package/src/iceberg/test-fixtures.ts +605 -0
- package/src/iceberg/time-travel-api.test.ts +1029 -0
- package/src/iceberg/time-travel-api.ts +731 -0
- package/src/iceberg/time-travel.test.ts +1218 -0
- package/src/iceberg/time-travel.ts +1052 -0
- package/src/iceberg/transformer.test.ts +689 -0
- package/src/iceberg/transformer.ts +1029 -0
- package/src/iceberg/types.ts +373 -0
- package/src/iceberg/writer.test.ts +716 -0
- package/src/iceberg/writer.ts +590 -0
- package/src/index.ts +212 -0
- package/src/lineage/index.ts +42 -0
- package/src/lineage/integration.ts +334 -0
- package/src/lineage/tracker.ts +1618 -0
- package/src/lineage/types.ts +354 -0
- package/src/middleware/index.ts +36 -0
- package/src/middleware/rate-limit-concurrent.test.ts +794 -0
- package/src/middleware/rate-limit.test.ts +1568 -0
- package/src/middleware/rate-limit.ts +840 -0
- package/src/migration-tooling/external-migration.test.ts +1864 -0
- package/src/migration-tooling/external-migration.ts +2355 -0
- package/src/migration-tooling/index.ts +19 -0
- package/src/migrations/ARCHITECTURE.md +474 -0
- package/src/migrations/PROGRESS_TRACKING.md +485 -0
- package/src/migrations/auto-migrator.test.ts +732 -0
- package/src/migrations/auto-migrator.ts +531 -0
- package/src/migrations/bulk-orchestrator.test.ts +801 -0
- package/src/migrations/bulk-orchestrator.ts +1039 -0
- package/src/migrations/compatibility.test.ts +958 -0
- package/src/migrations/compatibility.ts +902 -0
- package/src/migrations/do-migrations.test.ts +2620 -0
- package/src/migrations/do-migrations.ts +1289 -0
- package/src/migrations/do-migrations.types.ts +715 -0
- package/src/migrations/drizzle-compat.test.ts +210 -0
- package/src/migrations/drizzle-compat.ts +337 -0
- package/src/migrations/index.ts +334 -0
- package/src/migrations/migration-api.test.ts +438 -0
- package/src/migrations/migration-api.ts +704 -0
- package/src/migrations/progress-tracker-do.ts +518 -0
- package/src/migrations/progress-tracker-kv.ts +305 -0
- package/src/migrations/progress-tracker.test.ts +937 -0
- package/src/migrations/progress-tracker.ts +665 -0
- package/src/migrations/registry.test.ts +331 -0
- package/src/migrations/registry.ts +468 -0
- package/src/migrations/rollback.test.ts +644 -0
- package/src/migrations/runner.test.ts +807 -0
- package/src/migrations/runner.test.ts.backup +759 -0
- package/src/migrations/runner.ts +1459 -0
- package/src/migrations/schema-generator.test.ts +649 -0
- package/src/migrations/schema-generator.ts +513 -0
- package/src/migrations/testing.ts +1037 -0
- package/src/migrations/types.ts +573 -0
- package/src/migrations/validator.test.ts +660 -0
- package/src/migrations/validator.ts +741 -0
- package/src/observability/alerting.test.ts +1133 -0
- package/src/observability/alerting.ts +455 -0
- package/src/observability/analytics-engine.ts +733 -0
- package/src/observability/cost-metrics.ts +804 -0
- package/src/observability/cross-do-tracing.test.ts +516 -0
- package/src/observability/cross-do-tracing.ts +588 -0
- package/src/observability/dashboards/postgres-do-overview.json +1656 -0
- package/src/observability/error-rate-collector.test.ts +977 -0
- package/src/observability/error-rate-collector.ts +518 -0
- package/src/observability/exporters.test.ts +365 -0
- package/src/observability/exporters.ts +650 -0
- package/src/observability/health-check.test.ts +353 -0
- package/src/observability/health-check.ts +341 -0
- package/src/observability/index.test.ts +298 -0
- package/src/observability/index.ts +885 -0
- package/src/observability/instrumentation.test.ts +428 -0
- package/src/observability/instrumentation.ts +788 -0
- package/src/observability/memory-metrics.test.ts +355 -0
- package/src/observability/memory-metrics.ts +990 -0
- package/src/observability/metrics-endpoint.test.ts +402 -0
- package/src/observability/metrics-endpoint.ts +374 -0
- package/src/observability/metrics.test.ts +291 -0
- package/src/observability/metrics.ts +315 -0
- package/src/observability/observability-features.ts +1296 -0
- package/src/observability/prometheus.test.ts +292 -0
- package/src/observability/prometheus.ts +170 -0
- package/src/observability/propagation.test.ts +417 -0
- package/src/observability/propagation.ts +294 -0
- package/src/observability/query-latency.ts +586 -0
- package/src/observability/query-performance.test.ts +406 -0
- package/src/observability/query-performance.ts +491 -0
- package/src/observability/storage-tier-metrics.test.ts +633 -0
- package/src/observability/storage-tier-metrics.ts +570 -0
- package/src/observability/tier-cost-optimizer.ts +740 -0
- package/src/observability/tracer.test.ts +346 -0
- package/src/observability/tracer.ts +585 -0
- package/src/observability/types.test.ts +726 -0
- package/src/observability/types.ts +434 -0
- package/src/pglite/auto-demotion.test.ts +477 -0
- package/src/pglite/auto-demotion.ts +385 -0
- package/src/pglite/auto-promotion.test.ts +824 -0
- package/src/pglite/auto-promotion.ts +547 -0
- package/src/pglite/cache-layer.test.ts +469 -0
- package/src/pglite/cache-layer.ts +271 -0
- package/src/pglite/cold-start-manager.ts +1260 -0
- package/src/pglite/cold-start-optimizer.test.ts +937 -0
- package/src/pglite/cold-start-optimizer.ts +1895 -0
- package/src/pglite/dovfs-adapter.ts +1122 -0
- package/src/pglite/dovfs.ts +1258 -0
- package/src/pglite/etag-cache.test.ts +844 -0
- package/src/pglite/etag-cache.ts +526 -0
- package/src/pglite/index.ts +442 -0
- package/src/pglite/init.test.ts +455 -0
- package/src/pglite/init.ts +574 -0
- package/src/pglite/lifecycle.test.ts +599 -0
- package/src/pglite/lifecycle.ts +704 -0
- package/src/pglite/parallel-loader.test.ts +586 -0
- package/src/pglite/parallel-loader.ts +481 -0
- package/src/pglite/production-pglite.test.ts +666 -0
- package/src/pglite/production-pglite.ts +537 -0
- package/src/pglite/query-executor.ts +614 -0
- package/src/pglite/r2-layer.test.ts +501 -0
- package/src/pglite/r2-layer.ts +322 -0
- package/src/pglite/tiered-init.test.ts +725 -0
- package/src/pglite/tiered-init.ts +556 -0
- package/src/pglite/tiered-vfs.test.ts +726 -0
- package/src/pglite/tiered-vfs.ts +33 -0
- package/src/pglite/tiering-stats.test.ts +531 -0
- package/src/pglite/tiering-stats.ts +407 -0
- package/src/pglite/transaction-hooks.ts +343 -0
- package/src/pglite/warm-loader.test.ts +1701 -0
- package/src/pglite/warm-loader.ts +528 -0
- package/src/pglite/workers-pglite.ts +224 -0
- package/src/pglite-assets/pglite.data +0 -0
- package/src/pglite-assets/pglite.wasm +0 -0
- package/src/pglite.d.ts +47 -0
- package/src/playground/index.ts +137 -0
- package/src/playground/keyboard-shortcuts.ts +677 -0
- package/src/playground/playground.ts +323 -0
- package/src/playground/query-executor.ts +669 -0
- package/src/playground/query-history.ts +328 -0
- package/src/playground/result-formatter.ts +420 -0
- package/src/playground/sample-datasets.ts +674 -0
- package/src/playground/sample-queries.ts +1168 -0
- package/src/playground/schema-explorer.ts +558 -0
- package/src/playground/types.ts +518 -0
- package/src/readonly/cache-reader.test.ts +460 -0
- package/src/readonly/cache-reader.ts +313 -0
- package/src/readonly/config.test.ts +187 -0
- package/src/readonly/config.ts +128 -0
- package/src/readonly/index.ts +50 -0
- package/src/readonly/pglite-wrapper.test.ts +278 -0
- package/src/readonly/pglite-wrapper.ts +184 -0
- package/src/readonly/worker.test.ts +533 -0
- package/src/readonly/worker.ts +341 -0
- package/src/readonly/write-blocker.test.ts +459 -0
- package/src/readonly/write-blocker.ts +175 -0
- package/src/recovery/disaster-recovery.test.ts +618 -0
- package/src/recovery/disaster-recovery.ts +1181 -0
- package/src/recovery/index.ts +43 -0
- package/src/recovery/parquet-parser.ts +974 -0
- package/src/retention/index.ts +74 -0
- package/src/retention/policy.test.ts +571 -0
- package/src/retention/policy.ts +774 -0
- package/src/retention/purger.test.ts +465 -0
- package/src/retention/purger.ts +558 -0
- package/src/rls/auth-integration.test.ts +752 -0
- package/src/rls/auth-integration.ts +533 -0
- package/src/rls/generator.test.ts +829 -0
- package/src/rls/generator.ts +573 -0
- package/src/rls/index.ts +128 -0
- package/src/rls/policy.ts +208 -0
- package/src/rls/rls.test.ts +1071 -0
- package/src/rls/validator.test.ts +930 -0
- package/src/rls/validator.ts +895 -0
- package/src/routing/adaptive-router.test.ts +884 -0
- package/src/routing/adaptive-router.ts +845 -0
- package/src/routing/circuit-breaker.test.ts +1505 -0
- package/src/routing/circuit-breaker.ts +852 -0
- package/src/routing/cost-metrics.test.ts +565 -0
- package/src/routing/cost-metrics.ts +408 -0
- package/src/routing/do-connection-pool.test.ts +1109 -0
- package/src/routing/do-connection-pool.ts +828 -0
- package/src/routing/index.ts +158 -0
- package/src/routing/query-complexity-estimator.test.ts +356 -0
- package/src/routing/query-complexity-estimator.ts +444 -0
- package/src/routing/request-coalescing.test.ts +738 -0
- package/src/routing/request-coalescing.ts +475 -0
- package/src/routing/runtime-router.test.ts +436 -0
- package/src/routing/runtime-router.ts +357 -0
- package/src/routing/tenant-router.test.ts +2493 -0
- package/src/routing/tenant-router.ts +1908 -0
- package/src/routing/websocket-pool.test.ts +551 -0
- package/src/routing/websocket-pool.ts +577 -0
- package/src/storage/access-pattern-tracker.test.ts +874 -0
- package/src/storage/cache-layer.test.ts +560 -0
- package/src/storage/cache-layer.ts +328 -0
- package/src/storage/cost-aware-tiering.test.ts +652 -0
- package/src/storage/cost-aware-tiering.ts +794 -0
- package/src/storage/do-sqlite-blobs.test.ts +937 -0
- package/src/storage/index.ts +272 -0
- package/src/storage/interfaces.ts +974 -0
- package/src/storage/r2-layer.test.ts +653 -0
- package/src/storage/r2-layer.ts +434 -0
- package/src/storage/r2-overflow.ts +920 -0
- package/src/storage/r2-page-vfs.test.ts +2348 -0
- package/src/storage/r2-page-vfs.ts +1054 -0
- package/src/storage/swr-cache.test.ts +832 -0
- package/src/storage/swr-cache.ts +398 -0
- package/src/storage/swr-tiered-integration.test.ts +617 -0
- package/src/storage/tiered-orchestrator.test.ts +2441 -0
- package/src/storage/tiered-orchestrator.ts +2081 -0
- package/src/storage/tiered-vfs-swr.test.ts +736 -0
- package/src/storage/tiered-vfs-swr.ts +735 -0
- package/src/storage/tiered-vfs.test.ts +793 -0
- package/src/storage/tiered-vfs.ts +1082 -0
- package/src/streaming/backpressure-controller.ts +452 -0
- package/src/streaming/buffer-pool.ts +484 -0
- package/src/streaming/cdc-iceberg-connector.ts +605 -0
- package/src/streaming/index.ts +225 -0
- package/src/streaming/live-cdc-stream.ts +985 -0
- package/src/streaming/memory-bounded-stream.ts +443 -0
- package/src/streaming/query-streamer.ts +662 -0
- package/src/streaming/response-streaming.ts +557 -0
- package/src/types/branded.ts +1075 -0
- package/src/types/branded.ts.backup +273 -0
- package/src/types/utilities.ts +1023 -0
- package/src/types/wasm.d.ts +30 -0
- package/src/validation/typed-errors.test.ts +420 -0
- package/src/wal/replay-engine.ts +1264 -0
- package/src/worker/__mocks__/capnweb.ts +15 -0
- package/src/worker/__mocks__/pglite.data.ts +22 -0
- package/src/worker/__mocks__/pglite.wasm.ts +33 -0
- package/src/worker/auth-rate-limiter.test.ts +272 -0
- package/src/worker/auth-rate-limiter.ts +448 -0
- package/src/worker/auth.security-red.test.ts +1236 -0
- package/src/worker/auth.security.test.ts +822 -0
- package/src/worker/auth.test.ts +469 -0
- package/src/worker/auth.ts +1104 -0
- package/src/worker/cdc-backpressure.test.ts +726 -0
- package/src/worker/cdc-backpressure.ts +866 -0
- package/src/worker/cdc-sse.test.ts +780 -0
- package/src/worker/cdc-sse.ts +728 -0
- package/src/worker/cdc-websocket.ts +1229 -0
- package/src/worker/cdc-ws.test.ts +1009 -0
- package/src/worker/cdc.test.ts +327 -0
- package/src/worker/cdc.ts +289 -0
- package/src/worker/concerns/auth-concern.ts +179 -0
- package/src/worker/concerns/cdc-concern.ts +247 -0
- package/src/worker/concerns/index.ts +58 -0
- package/src/worker/concerns/query-execution-concern.ts +194 -0
- package/src/worker/concerns/storage-orchestration-concern.ts +373 -0
- package/src/worker/discriminated-types.test.ts +280 -0
- package/src/worker/do-auth-manager.ts +257 -0
- package/src/worker/do-decomposition.test.ts +1236 -0
- package/src/worker/do-pglite-manager.ts +302 -0
- package/src/worker/do.test.ts +2254 -0
- package/src/worker/do.ts +1878 -0
- package/src/worker/entry.ts +417 -0
- package/src/worker/errors.ts +285 -0
- package/src/worker/health-check-manager.test.ts +261 -0
- package/src/worker/health-check-manager.ts +231 -0
- package/src/worker/index.ts +389 -0
- package/src/worker/memory-pressure.test.ts +1460 -0
- package/src/worker/memory-pressure.ts +2650 -0
- package/src/worker/migration-manager.ts +582 -0
- package/src/worker/neon-compat.test.ts +332 -0
- package/src/worker/plugin-manager.ts +485 -0
- package/src/worker/postgres.do-rpc.d.ts +76 -0
- package/src/worker/proxy.ts +694 -0
- package/src/worker/query-execution-manager.test.ts +303 -0
- package/src/worker/query-execution-manager.ts +219 -0
- package/src/worker/query-executor.test.ts +282 -0
- package/src/worker/query-executor.ts +560 -0
- package/src/worker/query-stats-manager.ts +229 -0
- package/src/worker/result-handler.test.ts +364 -0
- package/src/worker/result-handler.ts +510 -0
- package/src/worker/routes.test.ts +795 -0
- package/src/worker/routes.ts +650 -0
- package/src/worker/rpc-methods-manager.test.ts +326 -0
- package/src/worker/rpc-methods-manager.ts +276 -0
- package/src/worker/rpc.ts +524 -0
- package/src/worker/schema-version.ts +605 -0
- package/src/worker/session-manager.test.ts +506 -0
- package/src/worker/session-manager.ts +732 -0
- package/src/worker/shutdown-manager.ts +469 -0
- package/src/worker/sql-transform.test.ts +286 -0
- package/src/worker/sql-transform.ts +368 -0
- package/src/worker/supabase-compat.test.ts +621 -0
- package/src/worker/types.test.ts +292 -0
- package/src/worker/types.ts +873 -0
- package/src/worker/user-routes.test.ts +703 -0
- package/src/worker/user-routes.ts +303 -0
- package/src/worker/wal-facade.ts +235 -0
- package/src/worker/wal-r2.test.ts +570 -0
- package/src/worker/wal-r2.ts +930 -0
- package/src/worker/wal-replay.test.ts +845 -0
- package/src/worker/wal-replay.ts +897 -0
- package/src/worker/wal-retention.test.ts +758 -0
- package/src/worker/wal-retention.ts +1075 -0
- package/src/worker/wal.test.ts +618 -0
- package/src/worker/wal.ts +697 -0
- package/src/worker/websocket.test.ts +296 -0
- package/src/worker/websocket.ts +284 -0
|
@@ -0,0 +1,1568 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rate Limiting Middleware Tests
|
|
3
|
+
* Task: postgres-k06 - Add rate limiting middleware to Worker entry points
|
|
4
|
+
*/
|
|
5
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
|
|
6
|
+
import { Hono } from 'hono'
|
|
7
|
+
import {
|
|
8
|
+
rateLimit,
|
|
9
|
+
rateLimitWithDO,
|
|
10
|
+
rateLimitWithKV,
|
|
11
|
+
tokenBucket,
|
|
12
|
+
SlidingWindowRateLimiter,
|
|
13
|
+
TokenBucketRateLimiter,
|
|
14
|
+
InMemoryRateLimitStorage,
|
|
15
|
+
DurableObjectRateLimitStorage,
|
|
16
|
+
KVRateLimitStorage,
|
|
17
|
+
} from './rate-limit'
|
|
18
|
+
import type {
|
|
19
|
+
RateLimitStorage,
|
|
20
|
+
RateLimitWindow,
|
|
21
|
+
RateLimitInfo,
|
|
22
|
+
TokenBucketInfo,
|
|
23
|
+
} from './rate-limit'
|
|
24
|
+
|
|
25
|
+
// ============================================================================
|
|
26
|
+
// Storage Tests
|
|
27
|
+
// ============================================================================
|
|
28
|
+
|
|
29
|
+
describe('InMemoryRateLimitStorage', () => {
|
|
30
|
+
let storage: InMemoryRateLimitStorage
|
|
31
|
+
|
|
32
|
+
beforeEach(() => {
|
|
33
|
+
storage = new InMemoryRateLimitStorage()
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
it('should return null for non-existent keys', async () => {
|
|
37
|
+
const result = await storage.get('nonexistent')
|
|
38
|
+
expect(result).toBeNull()
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
it('should store and retrieve data', async () => {
|
|
42
|
+
const data: RateLimitWindow = {
|
|
43
|
+
counts: [1, 2, 3],
|
|
44
|
+
windowStart: Date.now(),
|
|
45
|
+
total: 6,
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
await storage.set('test-key', data, 60000)
|
|
49
|
+
const result = await storage.get('test-key')
|
|
50
|
+
|
|
51
|
+
expect(result).toEqual(data)
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
it('should expire data after TTL', async () => {
|
|
55
|
+
vi.useFakeTimers()
|
|
56
|
+
|
|
57
|
+
const data: RateLimitWindow = {
|
|
58
|
+
counts: [1],
|
|
59
|
+
windowStart: Date.now(),
|
|
60
|
+
total: 1,
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
await storage.set('test-key', data, 1000)
|
|
64
|
+
|
|
65
|
+
// Data should exist before TTL
|
|
66
|
+
expect(await storage.get('test-key')).toEqual(data)
|
|
67
|
+
|
|
68
|
+
// Advance time past TTL
|
|
69
|
+
vi.advanceTimersByTime(1500)
|
|
70
|
+
|
|
71
|
+
// Data should be expired
|
|
72
|
+
expect(await storage.get('test-key')).toBeNull()
|
|
73
|
+
|
|
74
|
+
vi.useRealTimers()
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
it('should clear all entries', async () => {
|
|
78
|
+
await storage.set('key1', { counts: [1], windowStart: 0, total: 1 }, 60000)
|
|
79
|
+
await storage.set('key2', { counts: [2], windowStart: 0, total: 2 }, 60000)
|
|
80
|
+
|
|
81
|
+
storage.clear()
|
|
82
|
+
|
|
83
|
+
expect(await storage.get('key1')).toBeNull()
|
|
84
|
+
expect(await storage.get('key2')).toBeNull()
|
|
85
|
+
})
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
describe('DurableObjectRateLimitStorage', () => {
|
|
89
|
+
it('should wrap DO storage correctly', async () => {
|
|
90
|
+
const mockStorage = {
|
|
91
|
+
get: vi.fn().mockResolvedValue({ counts: [1], windowStart: 0, total: 1 }),
|
|
92
|
+
put: vi.fn().mockResolvedValue(undefined),
|
|
93
|
+
} as unknown as DurableObjectStorage
|
|
94
|
+
|
|
95
|
+
const storage = new DurableObjectRateLimitStorage(mockStorage)
|
|
96
|
+
|
|
97
|
+
const result = await storage.get('test-key')
|
|
98
|
+
expect(result).toEqual({ counts: [1], windowStart: 0, total: 1 })
|
|
99
|
+
expect(mockStorage.get).toHaveBeenCalledWith('test-key')
|
|
100
|
+
})
|
|
101
|
+
|
|
102
|
+
it('should return null for missing data', async () => {
|
|
103
|
+
const mockStorage = {
|
|
104
|
+
get: vi.fn().mockResolvedValue(undefined),
|
|
105
|
+
put: vi.fn().mockResolvedValue(undefined),
|
|
106
|
+
} as unknown as DurableObjectStorage
|
|
107
|
+
|
|
108
|
+
const storage = new DurableObjectRateLimitStorage(mockStorage)
|
|
109
|
+
|
|
110
|
+
const result = await storage.get('test-key')
|
|
111
|
+
expect(result).toBeNull()
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
it('should store data', async () => {
|
|
115
|
+
const mockStorage = {
|
|
116
|
+
get: vi.fn(),
|
|
117
|
+
put: vi.fn().mockResolvedValue(undefined),
|
|
118
|
+
} as unknown as DurableObjectStorage
|
|
119
|
+
|
|
120
|
+
const storage = new DurableObjectRateLimitStorage(mockStorage)
|
|
121
|
+
const data: RateLimitWindow = { counts: [1, 2], windowStart: 1000, total: 3 }
|
|
122
|
+
|
|
123
|
+
await storage.set('test-key', data, 60000)
|
|
124
|
+
|
|
125
|
+
expect(mockStorage.put).toHaveBeenCalledWith('test-key', data)
|
|
126
|
+
})
|
|
127
|
+
})
|
|
128
|
+
|
|
129
|
+
describe('KVRateLimitStorage', () => {
|
|
130
|
+
it('should wrap KV correctly', async () => {
|
|
131
|
+
const mockKV = {
|
|
132
|
+
get: vi.fn().mockResolvedValue({ counts: [1], windowStart: 0, total: 1 }),
|
|
133
|
+
put: vi.fn().mockResolvedValue(undefined),
|
|
134
|
+
} as unknown as KVNamespace
|
|
135
|
+
|
|
136
|
+
const storage = new KVRateLimitStorage(mockKV)
|
|
137
|
+
|
|
138
|
+
const result = await storage.get('test-key')
|
|
139
|
+
expect(result).toEqual({ counts: [1], windowStart: 0, total: 1 })
|
|
140
|
+
expect(mockKV.get).toHaveBeenCalledWith('test-key', 'json')
|
|
141
|
+
})
|
|
142
|
+
|
|
143
|
+
it('should store with TTL (minimum 60 seconds)', async () => {
|
|
144
|
+
const mockKV = {
|
|
145
|
+
get: vi.fn(),
|
|
146
|
+
put: vi.fn().mockResolvedValue(undefined),
|
|
147
|
+
} as unknown as KVNamespace
|
|
148
|
+
|
|
149
|
+
const storage = new KVRateLimitStorage(mockKV)
|
|
150
|
+
const data: RateLimitWindow = { counts: [1], windowStart: 0, total: 1 }
|
|
151
|
+
|
|
152
|
+
// TTL less than 60 seconds should be rounded up
|
|
153
|
+
await storage.set('test-key', data, 10000)
|
|
154
|
+
|
|
155
|
+
expect(mockKV.put).toHaveBeenCalledWith('test-key', JSON.stringify(data), { expirationTtl: 60 })
|
|
156
|
+
})
|
|
157
|
+
|
|
158
|
+
it('should use provided TTL when >= 60 seconds', async () => {
|
|
159
|
+
const mockKV = {
|
|
160
|
+
get: vi.fn(),
|
|
161
|
+
put: vi.fn().mockResolvedValue(undefined),
|
|
162
|
+
} as unknown as KVNamespace
|
|
163
|
+
|
|
164
|
+
const storage = new KVRateLimitStorage(mockKV)
|
|
165
|
+
const data: RateLimitWindow = { counts: [1], windowStart: 0, total: 1 }
|
|
166
|
+
|
|
167
|
+
await storage.set('test-key', data, 120000)
|
|
168
|
+
|
|
169
|
+
expect(mockKV.put).toHaveBeenCalledWith('test-key', JSON.stringify(data), { expirationTtl: 120 })
|
|
170
|
+
})
|
|
171
|
+
})
|
|
172
|
+
|
|
173
|
+
// ============================================================================
|
|
174
|
+
// Sliding Window Algorithm Tests
|
|
175
|
+
// ============================================================================
|
|
176
|
+
|
|
177
|
+
describe('SlidingWindowRateLimiter', () => {
|
|
178
|
+
let storage: InMemoryRateLimitStorage
|
|
179
|
+
let limiter: SlidingWindowRateLimiter
|
|
180
|
+
|
|
181
|
+
beforeEach(() => {
|
|
182
|
+
storage = new InMemoryRateLimitStorage()
|
|
183
|
+
limiter = new SlidingWindowRateLimiter({
|
|
184
|
+
requests: 10,
|
|
185
|
+
windowMs: 60000,
|
|
186
|
+
subWindows: 6, // 10-second sub-windows
|
|
187
|
+
storage,
|
|
188
|
+
})
|
|
189
|
+
})
|
|
190
|
+
|
|
191
|
+
it('should allow requests under the limit', async () => {
|
|
192
|
+
const result = await limiter.check('test-key')
|
|
193
|
+
|
|
194
|
+
expect(result.limited).toBe(false)
|
|
195
|
+
expect(result.limit).toBe(10)
|
|
196
|
+
expect(result.remaining).toBe(9)
|
|
197
|
+
})
|
|
198
|
+
|
|
199
|
+
it('should track multiple requests', async () => {
|
|
200
|
+
// Make 5 requests
|
|
201
|
+
for (let i = 0; i < 5; i++) {
|
|
202
|
+
await limiter.check('test-key')
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
const result = await limiter.check('test-key')
|
|
206
|
+
|
|
207
|
+
expect(result.limited).toBe(false)
|
|
208
|
+
expect(result.remaining).toBe(4)
|
|
209
|
+
})
|
|
210
|
+
|
|
211
|
+
it('should rate limit when exceeding threshold', async () => {
|
|
212
|
+
// The sliding window algorithm uses weighted counting.
|
|
213
|
+
// When all requests come in rapid succession (same sub-window),
|
|
214
|
+
// the weight for the oldest sub-window affects the count.
|
|
215
|
+
// We need more requests than the limit to trigger rate limiting
|
|
216
|
+
// because the weighted count may be slightly less.
|
|
217
|
+
|
|
218
|
+
// Make requests until we hit the limit
|
|
219
|
+
let requestCount = 0
|
|
220
|
+
let lastResult
|
|
221
|
+
|
|
222
|
+
// Make requests until limited (should happen around 10-11 requests)
|
|
223
|
+
while (requestCount < 15) {
|
|
224
|
+
const result = await limiter.check('test-key')
|
|
225
|
+
lastResult = result
|
|
226
|
+
if (result.limited) {
|
|
227
|
+
break
|
|
228
|
+
}
|
|
229
|
+
requestCount++
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// Should have been limited after approximately 10 requests
|
|
233
|
+
expect(lastResult?.limited).toBe(true)
|
|
234
|
+
expect(requestCount).toBeGreaterThanOrEqual(10)
|
|
235
|
+
expect(requestCount).toBeLessThanOrEqual(11)
|
|
236
|
+
})
|
|
237
|
+
|
|
238
|
+
it('should track different keys independently', async () => {
|
|
239
|
+
// Exhaust key1
|
|
240
|
+
for (let i = 0; i < 10; i++) {
|
|
241
|
+
await limiter.check('key1')
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// key2 should still have full capacity
|
|
245
|
+
const result = await limiter.check('key2')
|
|
246
|
+
expect(result.limited).toBe(false)
|
|
247
|
+
expect(result.remaining).toBe(9)
|
|
248
|
+
})
|
|
249
|
+
|
|
250
|
+
it('should provide reset time', async () => {
|
|
251
|
+
const result = await limiter.check('test-key')
|
|
252
|
+
|
|
253
|
+
expect(result.resetAfterSeconds).toBeGreaterThan(0)
|
|
254
|
+
expect(result.resetAfterSeconds).toBeLessThanOrEqual(60)
|
|
255
|
+
})
|
|
256
|
+
|
|
257
|
+
describe('sliding window behavior', () => {
|
|
258
|
+
beforeEach(() => {
|
|
259
|
+
vi.useFakeTimers()
|
|
260
|
+
})
|
|
261
|
+
|
|
262
|
+
afterEach(() => {
|
|
263
|
+
vi.useRealTimers()
|
|
264
|
+
})
|
|
265
|
+
|
|
266
|
+
it('should rotate window after time passes', async () => {
|
|
267
|
+
// Make enough requests to trigger rate limiting
|
|
268
|
+
let count = 0
|
|
269
|
+
while (count < 15) {
|
|
270
|
+
const result = await limiter.check('test-key')
|
|
271
|
+
if (result.limited) break
|
|
272
|
+
count++
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// Verify we're limited
|
|
276
|
+
const limitedResult = await limiter.check('test-key')
|
|
277
|
+
expect(limitedResult.limited).toBe(true)
|
|
278
|
+
|
|
279
|
+
// Advance time by half the window (30 seconds = 3 sub-windows)
|
|
280
|
+
vi.advanceTimersByTime(30000)
|
|
281
|
+
|
|
282
|
+
// Should have some capacity back (sliding window effect)
|
|
283
|
+
// After 30s (half window), roughly half the requests should have expired
|
|
284
|
+
const result = await limiter.check('test-key')
|
|
285
|
+
// The sliding window should have freed up some capacity
|
|
286
|
+
// We just verify we get a valid result
|
|
287
|
+
expect(result.limit).toBe(10)
|
|
288
|
+
expect(result.remaining).toBeGreaterThanOrEqual(0)
|
|
289
|
+
})
|
|
290
|
+
|
|
291
|
+
it('should reset completely after full window', async () => {
|
|
292
|
+
// Make 10 requests
|
|
293
|
+
for (let i = 0; i < 10; i++) {
|
|
294
|
+
await limiter.check('test-key')
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// Advance time past full window
|
|
298
|
+
vi.advanceTimersByTime(60001)
|
|
299
|
+
|
|
300
|
+
// Should be fully reset
|
|
301
|
+
const result = await limiter.check('test-key')
|
|
302
|
+
expect(result.limited).toBe(false)
|
|
303
|
+
expect(result.remaining).toBe(9)
|
|
304
|
+
})
|
|
305
|
+
})
|
|
306
|
+
})
|
|
307
|
+
|
|
308
|
+
// ============================================================================
|
|
309
|
+
// Token Bucket Algorithm Tests
|
|
310
|
+
// ============================================================================
|
|
311
|
+
|
|
312
|
+
describe('TokenBucketRateLimiter', () => {
|
|
313
|
+
let storage: InMemoryRateLimitStorage
|
|
314
|
+
let limiter: TokenBucketRateLimiter
|
|
315
|
+
|
|
316
|
+
beforeEach(() => {
|
|
317
|
+
storage = new InMemoryRateLimitStorage()
|
|
318
|
+
limiter = new TokenBucketRateLimiter({
|
|
319
|
+
bucketSize: 10,
|
|
320
|
+
tokensPerSecond: 1,
|
|
321
|
+
tokensPerRequest: 1,
|
|
322
|
+
storage,
|
|
323
|
+
})
|
|
324
|
+
})
|
|
325
|
+
|
|
326
|
+
it('should allow requests when bucket has tokens', async () => {
|
|
327
|
+
const result = await limiter.check('test-key')
|
|
328
|
+
|
|
329
|
+
expect(result.limited).toBe(false)
|
|
330
|
+
expect(result.bucketSize).toBe(10)
|
|
331
|
+
expect(result.tokens).toBe(9) // Started with 10, consumed 1
|
|
332
|
+
})
|
|
333
|
+
|
|
334
|
+
it('should consume tokens for each request', async () => {
|
|
335
|
+
await limiter.check('test-key')
|
|
336
|
+
await limiter.check('test-key')
|
|
337
|
+
const result = await limiter.check('test-key')
|
|
338
|
+
|
|
339
|
+
expect(result.tokens).toBe(7)
|
|
340
|
+
})
|
|
341
|
+
|
|
342
|
+
it('should rate limit when bucket is empty', async () => {
|
|
343
|
+
// Consume all 10 tokens
|
|
344
|
+
for (let i = 0; i < 10; i++) {
|
|
345
|
+
const result = await limiter.check('test-key')
|
|
346
|
+
expect(result.limited).toBe(false)
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
// 11th request should be limited
|
|
350
|
+
const result = await limiter.check('test-key')
|
|
351
|
+
expect(result.limited).toBe(true)
|
|
352
|
+
expect(result.tokens).toBe(0)
|
|
353
|
+
})
|
|
354
|
+
|
|
355
|
+
describe('token refill', () => {
|
|
356
|
+
beforeEach(() => {
|
|
357
|
+
vi.useFakeTimers()
|
|
358
|
+
})
|
|
359
|
+
|
|
360
|
+
afterEach(() => {
|
|
361
|
+
vi.useRealTimers()
|
|
362
|
+
})
|
|
363
|
+
|
|
364
|
+
it('should refill tokens over time', async () => {
|
|
365
|
+
// Consume all tokens
|
|
366
|
+
for (let i = 0; i < 10; i++) {
|
|
367
|
+
await limiter.check('test-key')
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
// Advance time by 5 seconds (should refill 5 tokens)
|
|
371
|
+
vi.advanceTimersByTime(5000)
|
|
372
|
+
|
|
373
|
+
const result = await limiter.check('test-key')
|
|
374
|
+
expect(result.limited).toBe(false)
|
|
375
|
+
expect(result.tokens).toBe(4) // 5 refilled - 1 consumed
|
|
376
|
+
})
|
|
377
|
+
|
|
378
|
+
it('should not exceed bucket size', async () => {
|
|
379
|
+
// Use 1 token
|
|
380
|
+
await limiter.check('test-key')
|
|
381
|
+
|
|
382
|
+
// Advance time by 100 seconds (would refill 100 tokens if no cap)
|
|
383
|
+
vi.advanceTimersByTime(100000)
|
|
384
|
+
|
|
385
|
+
const result = await limiter.check('test-key')
|
|
386
|
+
expect(result.tokens).toBe(9) // Capped at 10, minus 1 consumed
|
|
387
|
+
})
|
|
388
|
+
})
|
|
389
|
+
})
|
|
390
|
+
|
|
391
|
+
// ============================================================================
|
|
392
|
+
// Middleware Integration Tests
|
|
393
|
+
// ============================================================================
|
|
394
|
+
|
|
395
|
+
describe('rateLimit middleware', () => {
|
|
396
|
+
let app: Hono
|
|
397
|
+
let storage: InMemoryRateLimitStorage
|
|
398
|
+
|
|
399
|
+
beforeEach(() => {
|
|
400
|
+
storage = new InMemoryRateLimitStorage()
|
|
401
|
+
app = new Hono()
|
|
402
|
+
})
|
|
403
|
+
|
|
404
|
+
it('should add rate limit headers', async () => {
|
|
405
|
+
app.use('*', rateLimit({
|
|
406
|
+
requests: 10,
|
|
407
|
+
windowMs: 60000,
|
|
408
|
+
storage,
|
|
409
|
+
}))
|
|
410
|
+
app.get('/test', (c) => c.text('ok'))
|
|
411
|
+
|
|
412
|
+
const res = await app.request('/test', {
|
|
413
|
+
headers: { 'CF-Connecting-IP': '1.2.3.4' },
|
|
414
|
+
})
|
|
415
|
+
|
|
416
|
+
expect(res.status).toBe(200)
|
|
417
|
+
expect(res.headers.get('X-RateLimit-Limit')).toBe('10')
|
|
418
|
+
expect(res.headers.get('X-RateLimit-Remaining')).toBe('9')
|
|
419
|
+
expect(res.headers.get('X-RateLimit-Reset')).toBeTruthy()
|
|
420
|
+
})
|
|
421
|
+
|
|
422
|
+
it('should return 429 when rate limited', async () => {
|
|
423
|
+
app.use('*', rateLimit({
|
|
424
|
+
requests: 3,
|
|
425
|
+
windowMs: 60000,
|
|
426
|
+
storage,
|
|
427
|
+
}))
|
|
428
|
+
app.get('/test', (c) => c.text('ok'))
|
|
429
|
+
|
|
430
|
+
// Make 3 requests (the limit)
|
|
431
|
+
for (let i = 0; i < 3; i++) {
|
|
432
|
+
const res = await app.request('/test', {
|
|
433
|
+
headers: { 'CF-Connecting-IP': '1.2.3.4' },
|
|
434
|
+
})
|
|
435
|
+
expect(res.status).toBe(200)
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
// 4th request should be rate limited
|
|
439
|
+
const res = await app.request('/test', {
|
|
440
|
+
headers: { 'CF-Connecting-IP': '1.2.3.4' },
|
|
441
|
+
})
|
|
442
|
+
|
|
443
|
+
expect(res.status).toBe(429)
|
|
444
|
+
expect(res.headers.get('Retry-After')).toBeTruthy()
|
|
445
|
+
|
|
446
|
+
const body = await res.json() as { code: string; message: string }
|
|
447
|
+
expect(body.code).toBe('RATE_LIMITED')
|
|
448
|
+
expect(body.message).toContain('Too many requests')
|
|
449
|
+
})
|
|
450
|
+
|
|
451
|
+
it('should rate limit by IP by default', async () => {
|
|
452
|
+
app.use('*', rateLimit({
|
|
453
|
+
requests: 2,
|
|
454
|
+
windowMs: 60000,
|
|
455
|
+
storage,
|
|
456
|
+
}))
|
|
457
|
+
app.get('/test', (c) => c.text('ok'))
|
|
458
|
+
|
|
459
|
+
// IP 1 makes 2 requests
|
|
460
|
+
await app.request('/test', { headers: { 'CF-Connecting-IP': '1.1.1.1' } })
|
|
461
|
+
await app.request('/test', { headers: { 'CF-Connecting-IP': '1.1.1.1' } })
|
|
462
|
+
|
|
463
|
+
// IP 1 should be rate limited
|
|
464
|
+
const res1 = await app.request('/test', { headers: { 'CF-Connecting-IP': '1.1.1.1' } })
|
|
465
|
+
expect(res1.status).toBe(429)
|
|
466
|
+
|
|
467
|
+
// IP 2 should still have capacity
|
|
468
|
+
const res2 = await app.request('/test', { headers: { 'CF-Connecting-IP': '2.2.2.2' } })
|
|
469
|
+
expect(res2.status).toBe(200)
|
|
470
|
+
})
|
|
471
|
+
|
|
472
|
+
it('should rate limit by user when configured', async () => {
|
|
473
|
+
app.use('*', rateLimit({
|
|
474
|
+
requests: 2,
|
|
475
|
+
windowMs: 60000,
|
|
476
|
+
keyMode: 'user',
|
|
477
|
+
userHeader: 'X-User-ID',
|
|
478
|
+
storage,
|
|
479
|
+
}))
|
|
480
|
+
app.get('/test', (c) => c.text('ok'))
|
|
481
|
+
|
|
482
|
+
// User 1 makes 2 requests
|
|
483
|
+
await app.request('/test', { headers: { 'X-User-ID': 'user1' } })
|
|
484
|
+
await app.request('/test', { headers: { 'X-User-ID': 'user1' } })
|
|
485
|
+
|
|
486
|
+
// User 1 should be rate limited
|
|
487
|
+
const res1 = await app.request('/test', { headers: { 'X-User-ID': 'user1' } })
|
|
488
|
+
expect(res1.status).toBe(429)
|
|
489
|
+
|
|
490
|
+
// User 2 should still have capacity
|
|
491
|
+
const res2 = await app.request('/test', { headers: { 'X-User-ID': 'user2' } })
|
|
492
|
+
expect(res2.status).toBe(200)
|
|
493
|
+
})
|
|
494
|
+
|
|
495
|
+
it('should skip rate limiting when no user header and keyMode is user', async () => {
|
|
496
|
+
app.use('*', rateLimit({
|
|
497
|
+
requests: 1,
|
|
498
|
+
windowMs: 60000,
|
|
499
|
+
keyMode: 'user',
|
|
500
|
+
userHeader: 'X-User-ID',
|
|
501
|
+
storage,
|
|
502
|
+
}))
|
|
503
|
+
app.get('/test', (c) => c.text('ok'))
|
|
504
|
+
|
|
505
|
+
// Make multiple requests without user header
|
|
506
|
+
for (let i = 0; i < 5; i++) {
|
|
507
|
+
const res = await app.request('/test')
|
|
508
|
+
expect(res.status).toBe(200)
|
|
509
|
+
}
|
|
510
|
+
})
|
|
511
|
+
|
|
512
|
+
it('should support custom key extractor', async () => {
|
|
513
|
+
app.use('*', rateLimit({
|
|
514
|
+
requests: 2,
|
|
515
|
+
windowMs: 60000,
|
|
516
|
+
keyMode: 'custom',
|
|
517
|
+
keyExtractor: (c) => c.req.header('X-API-Key') ?? null,
|
|
518
|
+
storage,
|
|
519
|
+
}))
|
|
520
|
+
app.get('/test', (c) => c.text('ok'))
|
|
521
|
+
|
|
522
|
+
// API key 1 makes 2 requests
|
|
523
|
+
await app.request('/test', { headers: { 'X-API-Key': 'key1' } })
|
|
524
|
+
await app.request('/test', { headers: { 'X-API-Key': 'key1' } })
|
|
525
|
+
|
|
526
|
+
// API key 1 should be rate limited
|
|
527
|
+
const res1 = await app.request('/test', { headers: { 'X-API-Key': 'key1' } })
|
|
528
|
+
expect(res1.status).toBe(429)
|
|
529
|
+
|
|
530
|
+
// API key 2 should still have capacity
|
|
531
|
+
const res2 = await app.request('/test', { headers: { 'X-API-Key': 'key2' } })
|
|
532
|
+
expect(res2.status).toBe(200)
|
|
533
|
+
})
|
|
534
|
+
|
|
535
|
+
it('should support skip function', async () => {
|
|
536
|
+
app.use('*', rateLimit({
|
|
537
|
+
requests: 1,
|
|
538
|
+
windowMs: 60000,
|
|
539
|
+
skip: (c) => c.req.header('X-Skip-RateLimit') === 'true',
|
|
540
|
+
storage,
|
|
541
|
+
}))
|
|
542
|
+
app.get('/test', (c) => c.text('ok'))
|
|
543
|
+
|
|
544
|
+
// Make multiple requests with skip header
|
|
545
|
+
for (let i = 0; i < 5; i++) {
|
|
546
|
+
const res = await app.request('/test', {
|
|
547
|
+
headers: { 'CF-Connecting-IP': '1.2.3.4', 'X-Skip-RateLimit': 'true' },
|
|
548
|
+
})
|
|
549
|
+
expect(res.status).toBe(200)
|
|
550
|
+
}
|
|
551
|
+
})
|
|
552
|
+
|
|
553
|
+
it('should support custom rate limited response', async () => {
|
|
554
|
+
app.use('*', rateLimit({
|
|
555
|
+
requests: 1,
|
|
556
|
+
windowMs: 60000,
|
|
557
|
+
onRateLimited: (c, info) => c.json({ custom: 'response', remaining: info.remaining }, 429),
|
|
558
|
+
storage,
|
|
559
|
+
}))
|
|
560
|
+
app.get('/test', (c) => c.text('ok'))
|
|
561
|
+
|
|
562
|
+
await app.request('/test', { headers: { 'CF-Connecting-IP': '1.2.3.4' } })
|
|
563
|
+
const res = await app.request('/test', { headers: { 'CF-Connecting-IP': '1.2.3.4' } })
|
|
564
|
+
|
|
565
|
+
expect(res.status).toBe(429)
|
|
566
|
+
const body = await res.json() as { custom: string }
|
|
567
|
+
expect(body.custom).toBe('response')
|
|
568
|
+
})
|
|
569
|
+
|
|
570
|
+
it('should use X-Forwarded-For as fallback for IP', async () => {
|
|
571
|
+
app.use('*', rateLimit({
|
|
572
|
+
requests: 2,
|
|
573
|
+
windowMs: 60000,
|
|
574
|
+
storage,
|
|
575
|
+
}))
|
|
576
|
+
app.get('/test', (c) => c.text('ok'))
|
|
577
|
+
|
|
578
|
+
// Use X-Forwarded-For
|
|
579
|
+
await app.request('/test', { headers: { 'X-Forwarded-For': '1.2.3.4, 5.6.7.8' } })
|
|
580
|
+
await app.request('/test', { headers: { 'X-Forwarded-For': '1.2.3.4' } })
|
|
581
|
+
|
|
582
|
+
// Same IP should be rate limited
|
|
583
|
+
const res = await app.request('/test', { headers: { 'X-Forwarded-For': '1.2.3.4' } })
|
|
584
|
+
expect(res.status).toBe(429)
|
|
585
|
+
})
|
|
586
|
+
|
|
587
|
+
it('should use key prefix', async () => {
|
|
588
|
+
app.use('*', rateLimit({
|
|
589
|
+
requests: 2,
|
|
590
|
+
windowMs: 60000,
|
|
591
|
+
keyPrefix: 'custom:',
|
|
592
|
+
storage,
|
|
593
|
+
}))
|
|
594
|
+
app.get('/test', (c) => c.text('ok'))
|
|
595
|
+
|
|
596
|
+
await app.request('/test', { headers: { 'CF-Connecting-IP': '1.2.3.4' } })
|
|
597
|
+
|
|
598
|
+
// Check that storage uses prefixed key
|
|
599
|
+
const data = await storage.get('custom:1.2.3.4')
|
|
600
|
+
expect(data).not.toBeNull()
|
|
601
|
+
})
|
|
602
|
+
})
|
|
603
|
+
|
|
604
|
+
describe('rateLimitWithDO', () => {
|
|
605
|
+
it('should create middleware with DO storage', async () => {
|
|
606
|
+
const mockStorage = {
|
|
607
|
+
get: vi.fn().mockResolvedValue(null),
|
|
608
|
+
put: vi.fn().mockResolvedValue(undefined),
|
|
609
|
+
} as unknown as DurableObjectStorage
|
|
610
|
+
|
|
611
|
+
const app = new Hono()
|
|
612
|
+
app.use('*', rateLimitWithDO(mockStorage, { requests: 10 }))
|
|
613
|
+
app.get('/test', (c) => c.text('ok'))
|
|
614
|
+
|
|
615
|
+
const res = await app.request('/test', {
|
|
616
|
+
headers: { 'CF-Connecting-IP': '1.2.3.4' },
|
|
617
|
+
})
|
|
618
|
+
|
|
619
|
+
expect(res.status).toBe(200)
|
|
620
|
+
expect(mockStorage.put).toHaveBeenCalled()
|
|
621
|
+
})
|
|
622
|
+
})
|
|
623
|
+
|
|
624
|
+
describe('rateLimitWithKV', () => {
|
|
625
|
+
it('should create middleware with KV storage', async () => {
|
|
626
|
+
const mockKV = {
|
|
627
|
+
get: vi.fn().mockResolvedValue(null),
|
|
628
|
+
put: vi.fn().mockResolvedValue(undefined),
|
|
629
|
+
} as unknown as KVNamespace
|
|
630
|
+
|
|
631
|
+
const app = new Hono()
|
|
632
|
+
app.use('*', rateLimitWithKV(mockKV, { requests: 10 }))
|
|
633
|
+
app.get('/test', (c) => c.text('ok'))
|
|
634
|
+
|
|
635
|
+
const res = await app.request('/test', {
|
|
636
|
+
headers: { 'CF-Connecting-IP': '1.2.3.4' },
|
|
637
|
+
})
|
|
638
|
+
|
|
639
|
+
expect(res.status).toBe(200)
|
|
640
|
+
expect(mockKV.put).toHaveBeenCalled()
|
|
641
|
+
})
|
|
642
|
+
})
|
|
643
|
+
|
|
644
|
+
describe('tokenBucket middleware', () => {
|
|
645
|
+
let app: Hono
|
|
646
|
+
let storage: InMemoryRateLimitStorage
|
|
647
|
+
|
|
648
|
+
beforeEach(() => {
|
|
649
|
+
storage = new InMemoryRateLimitStorage()
|
|
650
|
+
app = new Hono()
|
|
651
|
+
})
|
|
652
|
+
|
|
653
|
+
it('should add rate limit headers', async () => {
|
|
654
|
+
app.use('*', tokenBucket({
|
|
655
|
+
bucketSize: 10,
|
|
656
|
+
tokensPerSecond: 1,
|
|
657
|
+
storage,
|
|
658
|
+
}))
|
|
659
|
+
app.get('/test', (c) => c.text('ok'))
|
|
660
|
+
|
|
661
|
+
const res = await app.request('/test', {
|
|
662
|
+
headers: { 'CF-Connecting-IP': '1.2.3.4' },
|
|
663
|
+
})
|
|
664
|
+
|
|
665
|
+
expect(res.status).toBe(200)
|
|
666
|
+
expect(res.headers.get('X-RateLimit-Limit')).toBe('10')
|
|
667
|
+
expect(res.headers.get('X-RateLimit-Remaining')).toBe('9')
|
|
668
|
+
})
|
|
669
|
+
|
|
670
|
+
it('should return 429 when bucket is empty', async () => {
|
|
671
|
+
app.use('*', tokenBucket({
|
|
672
|
+
bucketSize: 3,
|
|
673
|
+
tokensPerSecond: 1,
|
|
674
|
+
storage,
|
|
675
|
+
}))
|
|
676
|
+
app.get('/test', (c) => c.text('ok'))
|
|
677
|
+
|
|
678
|
+
// Consume all tokens
|
|
679
|
+
for (let i = 0; i < 3; i++) {
|
|
680
|
+
const res = await app.request('/test', {
|
|
681
|
+
headers: { 'CF-Connecting-IP': '1.2.3.4' },
|
|
682
|
+
})
|
|
683
|
+
expect(res.status).toBe(200)
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
// Should be rate limited
|
|
687
|
+
const res = await app.request('/test', {
|
|
688
|
+
headers: { 'CF-Connecting-IP': '1.2.3.4' },
|
|
689
|
+
})
|
|
690
|
+
|
|
691
|
+
expect(res.status).toBe(429)
|
|
692
|
+
expect(res.headers.get('Retry-After')).toBe('1') // 1 token / 1 per second
|
|
693
|
+
})
|
|
694
|
+
|
|
695
|
+
it('should allow bursts', async () => {
|
|
696
|
+
app.use('*', tokenBucket({
|
|
697
|
+
bucketSize: 10,
|
|
698
|
+
tokensPerSecond: 1,
|
|
699
|
+
storage,
|
|
700
|
+
}))
|
|
701
|
+
app.get('/test', (c) => c.text('ok'))
|
|
702
|
+
|
|
703
|
+
// Make 10 rapid requests (burst)
|
|
704
|
+
for (let i = 0; i < 10; i++) {
|
|
705
|
+
const res = await app.request('/test', {
|
|
706
|
+
headers: { 'CF-Connecting-IP': '1.2.3.4' },
|
|
707
|
+
})
|
|
708
|
+
expect(res.status).toBe(200)
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
// 11th should be limited
|
|
712
|
+
const res = await app.request('/test', {
|
|
713
|
+
headers: { 'CF-Connecting-IP': '1.2.3.4' },
|
|
714
|
+
})
|
|
715
|
+
expect(res.status).toBe(429)
|
|
716
|
+
})
|
|
717
|
+
|
|
718
|
+
it('should support multiple tokens per request', async () => {
|
|
719
|
+
app.use('*', tokenBucket({
|
|
720
|
+
bucketSize: 10,
|
|
721
|
+
tokensPerSecond: 1,
|
|
722
|
+
tokensPerRequest: 5,
|
|
723
|
+
storage,
|
|
724
|
+
}))
|
|
725
|
+
app.get('/test', (c) => c.text('ok'))
|
|
726
|
+
|
|
727
|
+
// First request consumes 5 tokens
|
|
728
|
+
const res1 = await app.request('/test', {
|
|
729
|
+
headers: { 'CF-Connecting-IP': '1.2.3.4' },
|
|
730
|
+
})
|
|
731
|
+
expect(res1.status).toBe(200)
|
|
732
|
+
|
|
733
|
+
// Second request consumes 5 more tokens
|
|
734
|
+
const res2 = await app.request('/test', {
|
|
735
|
+
headers: { 'CF-Connecting-IP': '1.2.3.4' },
|
|
736
|
+
})
|
|
737
|
+
expect(res2.status).toBe(200)
|
|
738
|
+
|
|
739
|
+
// Third request should be limited (no tokens left)
|
|
740
|
+
const res3 = await app.request('/test', {
|
|
741
|
+
headers: { 'CF-Connecting-IP': '1.2.3.4' },
|
|
742
|
+
})
|
|
743
|
+
expect(res3.status).toBe(429)
|
|
744
|
+
})
|
|
745
|
+
})
|
|
746
|
+
|
|
747
|
+
// ============================================================================
|
|
748
|
+
// Concurrent Request Handling Tests (postgres-1odt.1)
|
|
749
|
+
// ============================================================================
|
|
750
|
+
|
|
751
|
+
describe('Concurrent request handling', () => {
|
|
752
|
+
let storage: InMemoryRateLimitStorage
|
|
753
|
+
|
|
754
|
+
beforeEach(() => {
|
|
755
|
+
storage = new InMemoryRateLimitStorage()
|
|
756
|
+
})
|
|
757
|
+
|
|
758
|
+
it('should handle concurrent requests from same IP', async () => {
|
|
759
|
+
const app = new Hono()
|
|
760
|
+
app.use('*', rateLimit({
|
|
761
|
+
requests: 5,
|
|
762
|
+
windowMs: 60000,
|
|
763
|
+
storage,
|
|
764
|
+
}))
|
|
765
|
+
app.get('/test', (c) => c.text('ok'))
|
|
766
|
+
|
|
767
|
+
// Make 10 concurrent requests
|
|
768
|
+
// Note: With in-memory storage, concurrent requests may race and
|
|
769
|
+
// not enforce exact limits. This is expected behavior - for production,
|
|
770
|
+
// use DO storage or KV which has atomic operations.
|
|
771
|
+
const requests = Array(10).fill(null).map(() =>
|
|
772
|
+
app.request('/test', { headers: { 'CF-Connecting-IP': '1.2.3.4' } })
|
|
773
|
+
)
|
|
774
|
+
|
|
775
|
+
const responses = await Promise.all(requests)
|
|
776
|
+
const successCount = responses.filter(r => r.status === 200).length
|
|
777
|
+
const limitedCount = responses.filter(r => r.status === 429).length
|
|
778
|
+
|
|
779
|
+
// Due to race conditions with in-memory storage, we can't guarantee
|
|
780
|
+
// exact enforcement. We verify that:
|
|
781
|
+
// 1. Total responses equals requests made
|
|
782
|
+
// 2. At least some requests succeeded
|
|
783
|
+
// 3. Response codes are valid (200 or 429)
|
|
784
|
+
expect(successCount + limitedCount).toBe(10)
|
|
785
|
+
expect(successCount).toBeGreaterThan(0)
|
|
786
|
+
expect(responses.every(r => r.status === 200 || r.status === 429)).toBe(true)
|
|
787
|
+
})
|
|
788
|
+
|
|
789
|
+
it('should handle concurrent requests from different IPs independently', async () => {
|
|
790
|
+
const app = new Hono()
|
|
791
|
+
app.use('*', rateLimit({
|
|
792
|
+
requests: 3,
|
|
793
|
+
windowMs: 60000,
|
|
794
|
+
storage,
|
|
795
|
+
}))
|
|
796
|
+
app.get('/test', (c) => c.text('ok'))
|
|
797
|
+
|
|
798
|
+
// Create requests from multiple IPs concurrently
|
|
799
|
+
const ips = ['1.1.1.1', '2.2.2.2', '3.3.3.3', '4.4.4.4', '5.5.5.5']
|
|
800
|
+
const requestsPerIp = 5
|
|
801
|
+
|
|
802
|
+
const allRequests = ips.flatMap(ip =>
|
|
803
|
+
Array(requestsPerIp).fill(null).map(() =>
|
|
804
|
+
app.request('/test', { headers: { 'CF-Connecting-IP': ip } })
|
|
805
|
+
)
|
|
806
|
+
)
|
|
807
|
+
|
|
808
|
+
const responses = await Promise.all(allRequests)
|
|
809
|
+
|
|
810
|
+
// Group responses by IP (each IP made 5 consecutive requests in the flattened array)
|
|
811
|
+
for (let i = 0; i < ips.length; i++) {
|
|
812
|
+
const ipResponses = responses.slice(i * requestsPerIp, (i + 1) * requestsPerIp)
|
|
813
|
+
const ipSuccessCount = ipResponses.filter(r => r.status === 200).length
|
|
814
|
+
const ipLimitedCount = ipResponses.filter(r => r.status === 429).length
|
|
815
|
+
|
|
816
|
+
// Each IP should have some successful requests (at least up to the limit)
|
|
817
|
+
// and potentially some limited ones
|
|
818
|
+
expect(ipSuccessCount + ipLimitedCount).toBe(requestsPerIp)
|
|
819
|
+
expect(ipSuccessCount).toBeGreaterThanOrEqual(1) // At least first request succeeds
|
|
820
|
+
}
|
|
821
|
+
})
|
|
822
|
+
|
|
823
|
+
it('should handle burst of concurrent requests at boundary', async () => {
|
|
824
|
+
const app = new Hono()
|
|
825
|
+
app.use('*', rateLimit({
|
|
826
|
+
requests: 10,
|
|
827
|
+
windowMs: 60000,
|
|
828
|
+
storage,
|
|
829
|
+
}))
|
|
830
|
+
app.get('/test', (c) => c.text('ok'))
|
|
831
|
+
|
|
832
|
+
// Make exactly 10 concurrent requests (at the limit)
|
|
833
|
+
const requests = Array(10).fill(null).map(() =>
|
|
834
|
+
app.request('/test', { headers: { 'CF-Connecting-IP': '1.2.3.4' } })
|
|
835
|
+
)
|
|
836
|
+
|
|
837
|
+
const responses = await Promise.all(requests)
|
|
838
|
+
const successCount = responses.filter(r => r.status === 200).length
|
|
839
|
+
|
|
840
|
+
// With in-memory storage and concurrent execution, race conditions
|
|
841
|
+
// may allow more than the limit, but we should see most succeed
|
|
842
|
+
expect(successCount).toBeGreaterThanOrEqual(5) // At least half should succeed
|
|
843
|
+
expect(successCount).toBeLessThanOrEqual(10) // No more than limit
|
|
844
|
+
})
|
|
845
|
+
|
|
846
|
+
it('should handle concurrent token bucket requests', async () => {
|
|
847
|
+
const app = new Hono()
|
|
848
|
+
app.use('*', tokenBucket({
|
|
849
|
+
bucketSize: 5,
|
|
850
|
+
tokensPerSecond: 1,
|
|
851
|
+
tokensPerRequest: 1,
|
|
852
|
+
storage,
|
|
853
|
+
}))
|
|
854
|
+
app.get('/test', (c) => c.text('ok'))
|
|
855
|
+
|
|
856
|
+
// Make 8 concurrent requests with bucket size of 5
|
|
857
|
+
const requests = Array(8).fill(null).map(() =>
|
|
858
|
+
app.request('/test', { headers: { 'CF-Connecting-IP': '1.2.3.4' } })
|
|
859
|
+
)
|
|
860
|
+
|
|
861
|
+
const responses = await Promise.all(requests)
|
|
862
|
+
const successCount = responses.filter(r => r.status === 200).length
|
|
863
|
+
const limitedCount = responses.filter(r => r.status === 429).length
|
|
864
|
+
|
|
865
|
+
// Due to race conditions, we verify basic constraints
|
|
866
|
+
expect(successCount + limitedCount).toBe(8)
|
|
867
|
+
expect(successCount).toBeGreaterThanOrEqual(1)
|
|
868
|
+
expect(responses.every(r => r.status === 200 || r.status === 429)).toBe(true)
|
|
869
|
+
})
|
|
870
|
+
|
|
871
|
+
it('should maintain consistency under high concurrency stress', async () => {
|
|
872
|
+
const app = new Hono()
|
|
873
|
+
app.use('*', rateLimit({
|
|
874
|
+
requests: 20,
|
|
875
|
+
windowMs: 60000,
|
|
876
|
+
storage,
|
|
877
|
+
}))
|
|
878
|
+
app.get('/test', (c) => c.text('ok'))
|
|
879
|
+
|
|
880
|
+
// Make 50 concurrent requests
|
|
881
|
+
const requests = Array(50).fill(null).map(() =>
|
|
882
|
+
app.request('/test', { headers: { 'CF-Connecting-IP': '1.2.3.4' } })
|
|
883
|
+
)
|
|
884
|
+
|
|
885
|
+
const responses = await Promise.all(requests)
|
|
886
|
+
const successCount = responses.filter(r => r.status === 200).length
|
|
887
|
+
const limitedCount = responses.filter(r => r.status === 429).length
|
|
888
|
+
|
|
889
|
+
// Verify all responses are valid
|
|
890
|
+
expect(successCount + limitedCount).toBe(50)
|
|
891
|
+
expect(responses.every(r => r.status === 200 || r.status === 429)).toBe(true)
|
|
892
|
+
|
|
893
|
+
// Note: With in-memory storage and concurrent execution, race conditions may allow
|
|
894
|
+
// more requests than the limit. For production scenarios with strict enforcement,
|
|
895
|
+
// atomic storage operations (DO storage with transactions) would be needed.
|
|
896
|
+
// We verify that at least some requests succeeded
|
|
897
|
+
expect(successCount).toBeGreaterThanOrEqual(1)
|
|
898
|
+
})
|
|
899
|
+
})
|
|
900
|
+
|
|
901
|
+
// ============================================================================
|
|
902
|
+
// Rate Limit Reset Timing Tests (postgres-1odt)
|
|
903
|
+
// ============================================================================
|
|
904
|
+
|
|
905
|
+
describe('Rate limit reset timing', () => {
|
|
906
|
+
let storage: InMemoryRateLimitStorage
|
|
907
|
+
|
|
908
|
+
beforeEach(() => {
|
|
909
|
+
vi.useFakeTimers()
|
|
910
|
+
storage = new InMemoryRateLimitStorage()
|
|
911
|
+
})
|
|
912
|
+
|
|
913
|
+
afterEach(() => {
|
|
914
|
+
vi.useRealTimers()
|
|
915
|
+
})
|
|
916
|
+
|
|
917
|
+
it('should reset rate limit after window expires', async () => {
|
|
918
|
+
const app = new Hono()
|
|
919
|
+
app.use('*', rateLimit({
|
|
920
|
+
requests: 2,
|
|
921
|
+
windowMs: 10000, // 10 second window
|
|
922
|
+
storage,
|
|
923
|
+
}))
|
|
924
|
+
app.get('/test', (c) => c.text('ok'))
|
|
925
|
+
|
|
926
|
+
// Exhaust rate limit
|
|
927
|
+
await app.request('/test', { headers: { 'CF-Connecting-IP': '1.2.3.4' } })
|
|
928
|
+
await app.request('/test', { headers: { 'CF-Connecting-IP': '1.2.3.4' } })
|
|
929
|
+
|
|
930
|
+
// Third request should be rate limited
|
|
931
|
+
const limitedRes = await app.request('/test', { headers: { 'CF-Connecting-IP': '1.2.3.4' } })
|
|
932
|
+
expect(limitedRes.status).toBe(429)
|
|
933
|
+
|
|
934
|
+
// Advance time past the window
|
|
935
|
+
vi.advanceTimersByTime(10001)
|
|
936
|
+
|
|
937
|
+
// Should be able to make requests again
|
|
938
|
+
const resetRes = await app.request('/test', { headers: { 'CF-Connecting-IP': '1.2.3.4' } })
|
|
939
|
+
expect(resetRes.status).toBe(200)
|
|
940
|
+
})
|
|
941
|
+
|
|
942
|
+
it('should provide accurate reset time in headers', async () => {
|
|
943
|
+
const app = new Hono()
|
|
944
|
+
app.use('*', rateLimit({
|
|
945
|
+
requests: 5,
|
|
946
|
+
windowMs: 60000, // 60 second window
|
|
947
|
+
subWindows: 6, // 10-second sub-windows
|
|
948
|
+
storage,
|
|
949
|
+
}))
|
|
950
|
+
app.get('/test', (c) => c.text('ok'))
|
|
951
|
+
|
|
952
|
+
const res = await app.request('/test', { headers: { 'CF-Connecting-IP': '1.2.3.4' } })
|
|
953
|
+
|
|
954
|
+
const resetSeconds = parseInt(res.headers.get('X-RateLimit-Reset') || '0', 10)
|
|
955
|
+
// Reset time should be within the window duration
|
|
956
|
+
expect(resetSeconds).toBeGreaterThan(0)
|
|
957
|
+
expect(resetSeconds).toBeLessThanOrEqual(60)
|
|
958
|
+
})
|
|
959
|
+
|
|
960
|
+
it('should correctly handle sliding window reset across sub-windows', async () => {
|
|
961
|
+
const limiter = new SlidingWindowRateLimiter({
|
|
962
|
+
requests: 4,
|
|
963
|
+
windowMs: 40000, // 40 second window
|
|
964
|
+
subWindows: 4, // 10-second sub-windows
|
|
965
|
+
storage,
|
|
966
|
+
})
|
|
967
|
+
|
|
968
|
+
// Make 4 requests (at the limit)
|
|
969
|
+
for (let i = 0; i < 4; i++) {
|
|
970
|
+
await limiter.check('test-key')
|
|
971
|
+
}
|
|
972
|
+
|
|
973
|
+
// Should be limited now
|
|
974
|
+
let result = await limiter.check('test-key')
|
|
975
|
+
expect(result.limited).toBe(true)
|
|
976
|
+
|
|
977
|
+
// Advance by one sub-window (10 seconds)
|
|
978
|
+
vi.advanceTimersByTime(10001)
|
|
979
|
+
|
|
980
|
+
// After one sub-window rotates out, we should have capacity
|
|
981
|
+
// because some requests are now in the previous (rotated out) sub-window
|
|
982
|
+
result = await limiter.check('test-key')
|
|
983
|
+
// The sliding window should have freed up some capacity
|
|
984
|
+
expect(result.limit).toBe(4)
|
|
985
|
+
})
|
|
986
|
+
|
|
987
|
+
it('should calculate reset time correctly when near window boundary', async () => {
|
|
988
|
+
const limiter = new SlidingWindowRateLimiter({
|
|
989
|
+
requests: 2,
|
|
990
|
+
windowMs: 10000,
|
|
991
|
+
subWindows: 2,
|
|
992
|
+
storage,
|
|
993
|
+
})
|
|
994
|
+
|
|
995
|
+
// Make a request
|
|
996
|
+
const result = await limiter.check('test-key')
|
|
997
|
+
|
|
998
|
+
// Reset time should be positive and within window
|
|
999
|
+
expect(result.resetAfterSeconds).toBeGreaterThan(0)
|
|
1000
|
+
expect(result.resetAfterSeconds).toBeLessThanOrEqual(10)
|
|
1001
|
+
})
|
|
1002
|
+
|
|
1003
|
+
it('should reset token bucket over time', async () => {
|
|
1004
|
+
const app = new Hono()
|
|
1005
|
+
app.use('*', tokenBucket({
|
|
1006
|
+
bucketSize: 3,
|
|
1007
|
+
tokensPerSecond: 1, // Refill 1 token per second
|
|
1008
|
+
storage,
|
|
1009
|
+
}))
|
|
1010
|
+
app.get('/test', (c) => c.text('ok'))
|
|
1011
|
+
|
|
1012
|
+
// Exhaust all tokens
|
|
1013
|
+
await app.request('/test', { headers: { 'CF-Connecting-IP': '1.2.3.4' } })
|
|
1014
|
+
await app.request('/test', { headers: { 'CF-Connecting-IP': '1.2.3.4' } })
|
|
1015
|
+
await app.request('/test', { headers: { 'CF-Connecting-IP': '1.2.3.4' } })
|
|
1016
|
+
|
|
1017
|
+
// Should be limited
|
|
1018
|
+
const limitedRes = await app.request('/test', { headers: { 'CF-Connecting-IP': '1.2.3.4' } })
|
|
1019
|
+
expect(limitedRes.status).toBe(429)
|
|
1020
|
+
|
|
1021
|
+
// Advance time by 2 seconds (should refill 2 tokens)
|
|
1022
|
+
vi.advanceTimersByTime(2000)
|
|
1023
|
+
|
|
1024
|
+
// Should have 2 tokens now
|
|
1025
|
+
const res1 = await app.request('/test', { headers: { 'CF-Connecting-IP': '1.2.3.4' } })
|
|
1026
|
+
expect(res1.status).toBe(200)
|
|
1027
|
+
|
|
1028
|
+
const res2 = await app.request('/test', { headers: { 'CF-Connecting-IP': '1.2.3.4' } })
|
|
1029
|
+
expect(res2.status).toBe(200)
|
|
1030
|
+
|
|
1031
|
+
// Third request should be limited again
|
|
1032
|
+
const res3 = await app.request('/test', { headers: { 'CF-Connecting-IP': '1.2.3.4' } })
|
|
1033
|
+
expect(res3.status).toBe(429)
|
|
1034
|
+
})
|
|
1035
|
+
|
|
1036
|
+
it('should have decreasing remaining count as requests are made', async () => {
|
|
1037
|
+
const app = new Hono()
|
|
1038
|
+
app.use('*', rateLimit({
|
|
1039
|
+
requests: 5,
|
|
1040
|
+
windowMs: 60000,
|
|
1041
|
+
storage,
|
|
1042
|
+
}))
|
|
1043
|
+
app.get('/test', (c) => c.text('ok'))
|
|
1044
|
+
|
|
1045
|
+
const remainingCounts: number[] = []
|
|
1046
|
+
|
|
1047
|
+
for (let i = 0; i < 5; i++) {
|
|
1048
|
+
const res = await app.request('/test', { headers: { 'CF-Connecting-IP': '1.2.3.4' } })
|
|
1049
|
+
remainingCounts.push(parseInt(res.headers.get('X-RateLimit-Remaining') || '0', 10))
|
|
1050
|
+
}
|
|
1051
|
+
|
|
1052
|
+
// Remaining should decrease: [4, 3, 2, 1, 0]
|
|
1053
|
+
expect(remainingCounts).toEqual([4, 3, 2, 1, 0])
|
|
1054
|
+
})
|
|
1055
|
+
})
|
|
1056
|
+
|
|
1057
|
+
// ============================================================================
|
|
1058
|
+
// Rate Limit Header Tests (postgres-1odt)
|
|
1059
|
+
// ============================================================================
|
|
1060
|
+
|
|
1061
|
+
describe('Rate limit headers', () => {
|
|
1062
|
+
let storage: InMemoryRateLimitStorage
|
|
1063
|
+
|
|
1064
|
+
beforeEach(() => {
|
|
1065
|
+
storage = new InMemoryRateLimitStorage()
|
|
1066
|
+
})
|
|
1067
|
+
|
|
1068
|
+
it('should include all standard rate limit headers on success', async () => {
|
|
1069
|
+
const app = new Hono()
|
|
1070
|
+
app.use('*', rateLimit({
|
|
1071
|
+
requests: 100,
|
|
1072
|
+
windowMs: 60000,
|
|
1073
|
+
storage,
|
|
1074
|
+
}))
|
|
1075
|
+
app.get('/test', (c) => c.text('ok'))
|
|
1076
|
+
|
|
1077
|
+
const res = await app.request('/test', { headers: { 'CF-Connecting-IP': '1.2.3.4' } })
|
|
1078
|
+
|
|
1079
|
+
expect(res.status).toBe(200)
|
|
1080
|
+
expect(res.headers.has('X-RateLimit-Limit')).toBe(true)
|
|
1081
|
+
expect(res.headers.has('X-RateLimit-Remaining')).toBe(true)
|
|
1082
|
+
expect(res.headers.has('X-RateLimit-Reset')).toBe(true)
|
|
1083
|
+
})
|
|
1084
|
+
|
|
1085
|
+
it('should include Retry-After header on 429 response', async () => {
|
|
1086
|
+
const app = new Hono()
|
|
1087
|
+
app.use('*', rateLimit({
|
|
1088
|
+
requests: 1,
|
|
1089
|
+
windowMs: 60000,
|
|
1090
|
+
storage,
|
|
1091
|
+
}))
|
|
1092
|
+
app.get('/test', (c) => c.text('ok'))
|
|
1093
|
+
|
|
1094
|
+
await app.request('/test', { headers: { 'CF-Connecting-IP': '1.2.3.4' } })
|
|
1095
|
+
const res = await app.request('/test', { headers: { 'CF-Connecting-IP': '1.2.3.4' } })
|
|
1096
|
+
|
|
1097
|
+
expect(res.status).toBe(429)
|
|
1098
|
+
expect(res.headers.has('Retry-After')).toBe(true)
|
|
1099
|
+
const retryAfter = parseInt(res.headers.get('Retry-After') || '0', 10)
|
|
1100
|
+
expect(retryAfter).toBeGreaterThan(0)
|
|
1101
|
+
})
|
|
1102
|
+
|
|
1103
|
+
it('should have consistent limit header value', async () => {
|
|
1104
|
+
const app = new Hono()
|
|
1105
|
+
app.use('*', rateLimit({
|
|
1106
|
+
requests: 50,
|
|
1107
|
+
windowMs: 60000,
|
|
1108
|
+
storage,
|
|
1109
|
+
}))
|
|
1110
|
+
app.get('/test', (c) => c.text('ok'))
|
|
1111
|
+
|
|
1112
|
+
// Make multiple requests and verify limit stays consistent
|
|
1113
|
+
for (let i = 0; i < 5; i++) {
|
|
1114
|
+
const res = await app.request('/test', { headers: { 'CF-Connecting-IP': '1.2.3.4' } })
|
|
1115
|
+
expect(res.headers.get('X-RateLimit-Limit')).toBe('50')
|
|
1116
|
+
}
|
|
1117
|
+
})
|
|
1118
|
+
|
|
1119
|
+
it('should have correct remaining count on 429 response', async () => {
|
|
1120
|
+
const app = new Hono()
|
|
1121
|
+
app.use('*', rateLimit({
|
|
1122
|
+
requests: 2,
|
|
1123
|
+
windowMs: 60000,
|
|
1124
|
+
storage,
|
|
1125
|
+
}))
|
|
1126
|
+
app.get('/test', (c) => c.text('ok'))
|
|
1127
|
+
|
|
1128
|
+
await app.request('/test', { headers: { 'CF-Connecting-IP': '1.2.3.4' } })
|
|
1129
|
+
await app.request('/test', { headers: { 'CF-Connecting-IP': '1.2.3.4' } })
|
|
1130
|
+
const res = await app.request('/test', { headers: { 'CF-Connecting-IP': '1.2.3.4' } })
|
|
1131
|
+
|
|
1132
|
+
expect(res.status).toBe(429)
|
|
1133
|
+
expect(res.headers.get('X-RateLimit-Remaining')).toBe('0')
|
|
1134
|
+
})
|
|
1135
|
+
|
|
1136
|
+
it('should include token bucket headers correctly', async () => {
|
|
1137
|
+
const app = new Hono()
|
|
1138
|
+
app.use('*', tokenBucket({
|
|
1139
|
+
bucketSize: 20,
|
|
1140
|
+
tokensPerSecond: 2,
|
|
1141
|
+
storage,
|
|
1142
|
+
}))
|
|
1143
|
+
app.get('/test', (c) => c.text('ok'))
|
|
1144
|
+
|
|
1145
|
+
const res = await app.request('/test', { headers: { 'CF-Connecting-IP': '1.2.3.4' } })
|
|
1146
|
+
|
|
1147
|
+
expect(res.status).toBe(200)
|
|
1148
|
+
expect(res.headers.get('X-RateLimit-Limit')).toBe('20')
|
|
1149
|
+
expect(res.headers.get('X-RateLimit-Remaining')).toBe('19')
|
|
1150
|
+
expect(res.headers.has('X-RateLimit-Reset')).toBe(true)
|
|
1151
|
+
})
|
|
1152
|
+
|
|
1153
|
+
it('should calculate correct Retry-After for token bucket', async () => {
|
|
1154
|
+
const app = new Hono()
|
|
1155
|
+
app.use('*', tokenBucket({
|
|
1156
|
+
bucketSize: 2,
|
|
1157
|
+
tokensPerSecond: 0.5, // 1 token per 2 seconds
|
|
1158
|
+
tokensPerRequest: 1,
|
|
1159
|
+
storage,
|
|
1160
|
+
}))
|
|
1161
|
+
app.get('/test', (c) => c.text('ok'))
|
|
1162
|
+
|
|
1163
|
+
// Exhaust tokens
|
|
1164
|
+
await app.request('/test', { headers: { 'CF-Connecting-IP': '1.2.3.4' } })
|
|
1165
|
+
await app.request('/test', { headers: { 'CF-Connecting-IP': '1.2.3.4' } })
|
|
1166
|
+
|
|
1167
|
+
const res = await app.request('/test', { headers: { 'CF-Connecting-IP': '1.2.3.4' } })
|
|
1168
|
+
|
|
1169
|
+
expect(res.status).toBe(429)
|
|
1170
|
+
const retryAfter = parseInt(res.headers.get('Retry-After') || '0', 10)
|
|
1171
|
+
// Should be 2 seconds (1 token / 0.5 tokens per second = 2)
|
|
1172
|
+
expect(retryAfter).toBe(2)
|
|
1173
|
+
})
|
|
1174
|
+
|
|
1175
|
+
it('should not include Retry-After on successful requests', async () => {
|
|
1176
|
+
const app = new Hono()
|
|
1177
|
+
app.use('*', rateLimit({
|
|
1178
|
+
requests: 10,
|
|
1179
|
+
windowMs: 60000,
|
|
1180
|
+
storage,
|
|
1181
|
+
}))
|
|
1182
|
+
app.get('/test', (c) => c.text('ok'))
|
|
1183
|
+
|
|
1184
|
+
const res = await app.request('/test', { headers: { 'CF-Connecting-IP': '1.2.3.4' } })
|
|
1185
|
+
|
|
1186
|
+
expect(res.status).toBe(200)
|
|
1187
|
+
expect(res.headers.has('Retry-After')).toBe(false)
|
|
1188
|
+
})
|
|
1189
|
+
|
|
1190
|
+
it('should include headers even when rate limiting is skipped', async () => {
|
|
1191
|
+
const app = new Hono()
|
|
1192
|
+
app.use('*', rateLimit({
|
|
1193
|
+
requests: 10,
|
|
1194
|
+
windowMs: 60000,
|
|
1195
|
+
skip: () => true,
|
|
1196
|
+
storage,
|
|
1197
|
+
}))
|
|
1198
|
+
app.get('/test', (c) => c.text('ok'))
|
|
1199
|
+
|
|
1200
|
+
const res = await app.request('/test', { headers: { 'CF-Connecting-IP': '1.2.3.4' } })
|
|
1201
|
+
|
|
1202
|
+
// When skipped, no rate limit headers should be present
|
|
1203
|
+
expect(res.status).toBe(200)
|
|
1204
|
+
expect(res.headers.has('X-RateLimit-Limit')).toBe(false)
|
|
1205
|
+
})
|
|
1206
|
+
})
|
|
1207
|
+
|
|
1208
|
+
// ============================================================================
|
|
1209
|
+
// Edge Cases and Error Handling
|
|
1210
|
+
// ============================================================================
|
|
1211
|
+
|
|
1212
|
+
describe('Edge cases', () => {
|
|
1213
|
+
let storage: InMemoryRateLimitStorage
|
|
1214
|
+
|
|
1215
|
+
beforeEach(() => {
|
|
1216
|
+
storage = new InMemoryRateLimitStorage()
|
|
1217
|
+
})
|
|
1218
|
+
|
|
1219
|
+
it('should handle concurrent requests', async () => {
|
|
1220
|
+
const app = new Hono()
|
|
1221
|
+
app.use('*', rateLimit({
|
|
1222
|
+
requests: 5,
|
|
1223
|
+
windowMs: 60000,
|
|
1224
|
+
storage,
|
|
1225
|
+
}))
|
|
1226
|
+
app.get('/test', (c) => c.text('ok'))
|
|
1227
|
+
|
|
1228
|
+
// Make 10 concurrent requests
|
|
1229
|
+
// Note: With in-memory storage, concurrent requests may race and
|
|
1230
|
+
// not enforce exact limits. This is expected behavior - for production,
|
|
1231
|
+
// use DO storage or KV which has atomic operations.
|
|
1232
|
+
const requests = Array(10).fill(null).map(() =>
|
|
1233
|
+
app.request('/test', { headers: { 'CF-Connecting-IP': '1.2.3.4' } })
|
|
1234
|
+
)
|
|
1235
|
+
|
|
1236
|
+
const responses = await Promise.all(requests)
|
|
1237
|
+
const successCount = responses.filter(r => r.status === 200).length
|
|
1238
|
+
const limitedCount = responses.filter(r => r.status === 429).length
|
|
1239
|
+
|
|
1240
|
+
// Due to race conditions with in-memory storage, we can't guarantee
|
|
1241
|
+
// exact enforcement. We verify that:
|
|
1242
|
+
// 1. Total responses equals requests made
|
|
1243
|
+
// 2. At least some requests succeeded
|
|
1244
|
+
// 3. Response codes are valid (200 or 429)
|
|
1245
|
+
expect(successCount + limitedCount).toBe(10)
|
|
1246
|
+
expect(successCount).toBeGreaterThan(0)
|
|
1247
|
+
expect(responses.every(r => r.status === 200 || r.status === 429)).toBe(true)
|
|
1248
|
+
})
|
|
1249
|
+
|
|
1250
|
+
it('should handle unknown IP gracefully', async () => {
|
|
1251
|
+
const app = new Hono()
|
|
1252
|
+
app.use('*', rateLimit({
|
|
1253
|
+
requests: 1,
|
|
1254
|
+
windowMs: 60000,
|
|
1255
|
+
storage,
|
|
1256
|
+
}))
|
|
1257
|
+
app.get('/test', (c) => c.text('ok'))
|
|
1258
|
+
|
|
1259
|
+
// Request without any IP headers
|
|
1260
|
+
const res1 = await app.request('/test')
|
|
1261
|
+
expect(res1.status).toBe(200)
|
|
1262
|
+
|
|
1263
|
+
// Second request should use 'unknown' as key
|
|
1264
|
+
const res2 = await app.request('/test')
|
|
1265
|
+
expect(res2.status).toBe(429)
|
|
1266
|
+
})
|
|
1267
|
+
|
|
1268
|
+
it('should store rate limit info in context', async () => {
|
|
1269
|
+
const app = new Hono()
|
|
1270
|
+
app.use('*', rateLimit({
|
|
1271
|
+
requests: 10,
|
|
1272
|
+
windowMs: 60000,
|
|
1273
|
+
storage,
|
|
1274
|
+
}))
|
|
1275
|
+
app.get('/test', (c) => {
|
|
1276
|
+
const info = (c as unknown as { get(key: string): unknown }).get('rateLimitInfo') as RateLimitInfo
|
|
1277
|
+
return c.json({ remaining: info.remaining })
|
|
1278
|
+
})
|
|
1279
|
+
|
|
1280
|
+
const res = await app.request('/test', {
|
|
1281
|
+
headers: { 'CF-Connecting-IP': '1.2.3.4' },
|
|
1282
|
+
})
|
|
1283
|
+
|
|
1284
|
+
const body = await res.json() as { remaining: number }
|
|
1285
|
+
expect(body.remaining).toBe(9)
|
|
1286
|
+
})
|
|
1287
|
+
|
|
1288
|
+
it('should store token bucket info in context', async () => {
|
|
1289
|
+
const app = new Hono()
|
|
1290
|
+
app.use('*', tokenBucket({
|
|
1291
|
+
bucketSize: 10,
|
|
1292
|
+
tokensPerSecond: 1,
|
|
1293
|
+
storage,
|
|
1294
|
+
}))
|
|
1295
|
+
app.get('/test', (c) => {
|
|
1296
|
+
const info = (c as unknown as { get(key: string): unknown }).get('tokenBucketInfo') as TokenBucketInfo
|
|
1297
|
+
return c.json({ tokens: info.tokens })
|
|
1298
|
+
})
|
|
1299
|
+
|
|
1300
|
+
const res = await app.request('/test', {
|
|
1301
|
+
headers: { 'CF-Connecting-IP': '1.2.3.4' },
|
|
1302
|
+
})
|
|
1303
|
+
|
|
1304
|
+
const body = await res.json() as { tokens: number }
|
|
1305
|
+
expect(body.tokens).toBe(9)
|
|
1306
|
+
})
|
|
1307
|
+
|
|
1308
|
+
it('should handle async skip function', async () => {
|
|
1309
|
+
const app = new Hono()
|
|
1310
|
+
app.use('*', rateLimit({
|
|
1311
|
+
requests: 1,
|
|
1312
|
+
windowMs: 60000,
|
|
1313
|
+
skip: async (c) => {
|
|
1314
|
+
await new Promise(r => setTimeout(r, 10))
|
|
1315
|
+
return c.req.header('X-Admin') === 'true'
|
|
1316
|
+
},
|
|
1317
|
+
storage,
|
|
1318
|
+
}))
|
|
1319
|
+
app.get('/test', (c) => c.text('ok'))
|
|
1320
|
+
|
|
1321
|
+
// Admin requests should skip rate limiting
|
|
1322
|
+
const res1 = await app.request('/test', {
|
|
1323
|
+
headers: { 'CF-Connecting-IP': '1.2.3.4', 'X-Admin': 'true' },
|
|
1324
|
+
})
|
|
1325
|
+
expect(res1.status).toBe(200)
|
|
1326
|
+
|
|
1327
|
+
const res2 = await app.request('/test', {
|
|
1328
|
+
headers: { 'CF-Connecting-IP': '1.2.3.4', 'X-Admin': 'true' },
|
|
1329
|
+
})
|
|
1330
|
+
expect(res2.status).toBe(200)
|
|
1331
|
+
})
|
|
1332
|
+
|
|
1333
|
+
it('should handle empty IP address in X-Forwarded-For', async () => {
|
|
1334
|
+
const app = new Hono()
|
|
1335
|
+
app.use('*', rateLimit({
|
|
1336
|
+
requests: 2,
|
|
1337
|
+
windowMs: 60000,
|
|
1338
|
+
storage,
|
|
1339
|
+
}))
|
|
1340
|
+
app.get('/test', (c) => c.text('ok'))
|
|
1341
|
+
|
|
1342
|
+
// X-Forwarded-For with empty first value
|
|
1343
|
+
const res = await app.request('/test', {
|
|
1344
|
+
headers: { 'X-Forwarded-For': ', 1.2.3.4' },
|
|
1345
|
+
})
|
|
1346
|
+
expect(res.status).toBe(200)
|
|
1347
|
+
})
|
|
1348
|
+
|
|
1349
|
+
it('should handle very large request counts', async () => {
|
|
1350
|
+
const limiter = new SlidingWindowRateLimiter({
|
|
1351
|
+
requests: 1000000,
|
|
1352
|
+
windowMs: 60000,
|
|
1353
|
+
subWindows: 10,
|
|
1354
|
+
storage,
|
|
1355
|
+
})
|
|
1356
|
+
|
|
1357
|
+
const result = await limiter.check('test-key')
|
|
1358
|
+
expect(result.limit).toBe(1000000)
|
|
1359
|
+
expect(result.remaining).toBe(999999)
|
|
1360
|
+
})
|
|
1361
|
+
|
|
1362
|
+
it('should handle very small window sizes', async () => {
|
|
1363
|
+
const limiter = new SlidingWindowRateLimiter({
|
|
1364
|
+
requests: 10,
|
|
1365
|
+
windowMs: 100, // 100ms window
|
|
1366
|
+
subWindows: 10,
|
|
1367
|
+
storage,
|
|
1368
|
+
})
|
|
1369
|
+
|
|
1370
|
+
const result = await limiter.check('test-key')
|
|
1371
|
+
expect(result.limited).toBe(false)
|
|
1372
|
+
expect(result.resetAfterSeconds).toBeGreaterThanOrEqual(0)
|
|
1373
|
+
})
|
|
1374
|
+
|
|
1375
|
+
it('should handle storage failures gracefully', async () => {
|
|
1376
|
+
const failingStorage: RateLimitStorage = {
|
|
1377
|
+
get: vi.fn().mockRejectedValue(new Error('Storage unavailable')),
|
|
1378
|
+
set: vi.fn().mockRejectedValue(new Error('Storage unavailable')),
|
|
1379
|
+
}
|
|
1380
|
+
|
|
1381
|
+
const limiter = new SlidingWindowRateLimiter({
|
|
1382
|
+
requests: 10,
|
|
1383
|
+
windowMs: 60000,
|
|
1384
|
+
subWindows: 10,
|
|
1385
|
+
storage: failingStorage,
|
|
1386
|
+
})
|
|
1387
|
+
|
|
1388
|
+
// Should throw when storage fails
|
|
1389
|
+
await expect(limiter.check('test-key')).rejects.toThrow('Storage unavailable')
|
|
1390
|
+
})
|
|
1391
|
+
|
|
1392
|
+
it('should handle null key extractor return value', async () => {
|
|
1393
|
+
const app = new Hono()
|
|
1394
|
+
app.use('*', rateLimit({
|
|
1395
|
+
requests: 1,
|
|
1396
|
+
windowMs: 60000,
|
|
1397
|
+
keyMode: 'custom',
|
|
1398
|
+
keyExtractor: () => null, // Always returns null
|
|
1399
|
+
storage,
|
|
1400
|
+
}))
|
|
1401
|
+
app.get('/test', (c) => c.text('ok'))
|
|
1402
|
+
|
|
1403
|
+
// Requests with null key should skip rate limiting
|
|
1404
|
+
for (let i = 0; i < 5; i++) {
|
|
1405
|
+
const res = await app.request('/test')
|
|
1406
|
+
expect(res.status).toBe(200)
|
|
1407
|
+
}
|
|
1408
|
+
})
|
|
1409
|
+
|
|
1410
|
+
it('should handle rate limit response body structure', async () => {
|
|
1411
|
+
const app = new Hono()
|
|
1412
|
+
app.use('*', rateLimit({
|
|
1413
|
+
requests: 1,
|
|
1414
|
+
windowMs: 60000,
|
|
1415
|
+
storage,
|
|
1416
|
+
}))
|
|
1417
|
+
app.get('/test', (c) => c.text('ok'))
|
|
1418
|
+
|
|
1419
|
+
await app.request('/test', { headers: { 'CF-Connecting-IP': '1.2.3.4' } })
|
|
1420
|
+
const res = await app.request('/test', { headers: { 'CF-Connecting-IP': '1.2.3.4' } })
|
|
1421
|
+
|
|
1422
|
+
expect(res.status).toBe(429)
|
|
1423
|
+
const body = await res.json() as { error: boolean; code: string; message: string; retryAfter: number }
|
|
1424
|
+
|
|
1425
|
+
expect(body.error).toBe(true)
|
|
1426
|
+
expect(body.code).toBe('RATE_LIMITED')
|
|
1427
|
+
expect(typeof body.message).toBe('string')
|
|
1428
|
+
expect(typeof body.retryAfter).toBe('number')
|
|
1429
|
+
expect(body.retryAfter).toBeGreaterThan(0)
|
|
1430
|
+
})
|
|
1431
|
+
|
|
1432
|
+
it('should handle token bucket with large token cost', async () => {
|
|
1433
|
+
const limiter = new TokenBucketRateLimiter({
|
|
1434
|
+
bucketSize: 100,
|
|
1435
|
+
tokensPerSecond: 10,
|
|
1436
|
+
tokensPerRequest: 50, // Half the bucket per request
|
|
1437
|
+
storage,
|
|
1438
|
+
})
|
|
1439
|
+
|
|
1440
|
+
// First request should succeed
|
|
1441
|
+
let result = await limiter.check('test-key')
|
|
1442
|
+
expect(result.limited).toBe(false)
|
|
1443
|
+
expect(result.tokens).toBe(50) // 100 - 50
|
|
1444
|
+
|
|
1445
|
+
// Second request should also succeed
|
|
1446
|
+
result = await limiter.check('test-key')
|
|
1447
|
+
expect(result.limited).toBe(false)
|
|
1448
|
+
expect(result.tokens).toBe(0) // 50 - 50
|
|
1449
|
+
|
|
1450
|
+
// Third request should be limited
|
|
1451
|
+
result = await limiter.check('test-key')
|
|
1452
|
+
expect(result.limited).toBe(true)
|
|
1453
|
+
})
|
|
1454
|
+
|
|
1455
|
+
it('should handle multiple middleware instances', async () => {
|
|
1456
|
+
const storage1 = new InMemoryRateLimitStorage()
|
|
1457
|
+
const storage2 = new InMemoryRateLimitStorage()
|
|
1458
|
+
|
|
1459
|
+
const app = new Hono()
|
|
1460
|
+
// Two rate limiters with different limits
|
|
1461
|
+
app.use('/api/*', rateLimit({
|
|
1462
|
+
requests: 5,
|
|
1463
|
+
windowMs: 60000,
|
|
1464
|
+
keyPrefix: 'api:',
|
|
1465
|
+
storage: storage1,
|
|
1466
|
+
}))
|
|
1467
|
+
app.use('/admin/*', rateLimit({
|
|
1468
|
+
requests: 2,
|
|
1469
|
+
windowMs: 60000,
|
|
1470
|
+
keyPrefix: 'admin:',
|
|
1471
|
+
storage: storage2,
|
|
1472
|
+
}))
|
|
1473
|
+
app.get('/api/data', (c) => c.text('api data'))
|
|
1474
|
+
app.get('/admin/dashboard', (c) => c.text('admin dashboard'))
|
|
1475
|
+
|
|
1476
|
+
// API can handle 5 requests
|
|
1477
|
+
for (let i = 0; i < 5; i++) {
|
|
1478
|
+
const res = await app.request('/api/data', { headers: { 'CF-Connecting-IP': '1.2.3.4' } })
|
|
1479
|
+
expect(res.status).toBe(200)
|
|
1480
|
+
}
|
|
1481
|
+
const apiLimited = await app.request('/api/data', { headers: { 'CF-Connecting-IP': '1.2.3.4' } })
|
|
1482
|
+
expect(apiLimited.status).toBe(429)
|
|
1483
|
+
|
|
1484
|
+
// Admin can only handle 2 requests (same IP, different storage)
|
|
1485
|
+
for (let i = 0; i < 2; i++) {
|
|
1486
|
+
const res = await app.request('/admin/dashboard', { headers: { 'CF-Connecting-IP': '1.2.3.4' } })
|
|
1487
|
+
expect(res.status).toBe(200)
|
|
1488
|
+
}
|
|
1489
|
+
const adminLimited = await app.request('/admin/dashboard', { headers: { 'CF-Connecting-IP': '1.2.3.4' } })
|
|
1490
|
+
expect(adminLimited.status).toBe(429)
|
|
1491
|
+
})
|
|
1492
|
+
})
|
|
1493
|
+
|
|
1494
|
+
// ============================================================================
|
|
1495
|
+
// Distributed Rate Limiting Tests (Simulated)
|
|
1496
|
+
// ============================================================================
|
|
1497
|
+
|
|
1498
|
+
describe('Distributed rate limiting simulation', () => {
|
|
1499
|
+
it('should work with shared storage between instances', async () => {
|
|
1500
|
+
// Simulate two server instances sharing the same storage
|
|
1501
|
+
const sharedStorage = new InMemoryRateLimitStorage()
|
|
1502
|
+
|
|
1503
|
+
const app1 = new Hono()
|
|
1504
|
+
app1.use('*', rateLimit({
|
|
1505
|
+
requests: 5,
|
|
1506
|
+
windowMs: 60000,
|
|
1507
|
+
storage: sharedStorage,
|
|
1508
|
+
}))
|
|
1509
|
+
app1.get('/test', (c) => c.text('app1'))
|
|
1510
|
+
|
|
1511
|
+
const app2 = new Hono()
|
|
1512
|
+
app2.use('*', rateLimit({
|
|
1513
|
+
requests: 5,
|
|
1514
|
+
windowMs: 60000,
|
|
1515
|
+
storage: sharedStorage,
|
|
1516
|
+
}))
|
|
1517
|
+
app2.get('/test', (c) => c.text('app2'))
|
|
1518
|
+
|
|
1519
|
+
// Make requests alternating between instances
|
|
1520
|
+
await app1.request('/test', { headers: { 'CF-Connecting-IP': '1.2.3.4' } })
|
|
1521
|
+
await app2.request('/test', { headers: { 'CF-Connecting-IP': '1.2.3.4' } })
|
|
1522
|
+
await app1.request('/test', { headers: { 'CF-Connecting-IP': '1.2.3.4' } })
|
|
1523
|
+
await app2.request('/test', { headers: { 'CF-Connecting-IP': '1.2.3.4' } })
|
|
1524
|
+
await app1.request('/test', { headers: { 'CF-Connecting-IP': '1.2.3.4' } })
|
|
1525
|
+
|
|
1526
|
+
// 6th request should be limited regardless of which instance
|
|
1527
|
+
const res1 = await app1.request('/test', { headers: { 'CF-Connecting-IP': '1.2.3.4' } })
|
|
1528
|
+
expect(res1.status).toBe(429)
|
|
1529
|
+
|
|
1530
|
+
const res2 = await app2.request('/test', { headers: { 'CF-Connecting-IP': '1.2.3.4' } })
|
|
1531
|
+
expect(res2.status).toBe(429)
|
|
1532
|
+
})
|
|
1533
|
+
|
|
1534
|
+
it('should isolate rate limits between different keys in distributed setting', async () => {
|
|
1535
|
+
const sharedStorage = new InMemoryRateLimitStorage()
|
|
1536
|
+
|
|
1537
|
+
const app1 = new Hono()
|
|
1538
|
+
app1.use('*', rateLimit({
|
|
1539
|
+
requests: 2,
|
|
1540
|
+
windowMs: 60000,
|
|
1541
|
+
storage: sharedStorage,
|
|
1542
|
+
}))
|
|
1543
|
+
app1.get('/test', (c) => c.text('ok'))
|
|
1544
|
+
|
|
1545
|
+
const app2 = new Hono()
|
|
1546
|
+
app2.use('*', rateLimit({
|
|
1547
|
+
requests: 2,
|
|
1548
|
+
windowMs: 60000,
|
|
1549
|
+
storage: sharedStorage,
|
|
1550
|
+
}))
|
|
1551
|
+
app2.get('/test', (c) => c.text('ok'))
|
|
1552
|
+
|
|
1553
|
+
// IP 1 uses all quota through app1
|
|
1554
|
+
await app1.request('/test', { headers: { 'CF-Connecting-IP': '1.1.1.1' } })
|
|
1555
|
+
await app1.request('/test', { headers: { 'CF-Connecting-IP': '1.1.1.1' } })
|
|
1556
|
+
|
|
1557
|
+
// IP 1 should be limited on both instances
|
|
1558
|
+
const limitedRes1 = await app1.request('/test', { headers: { 'CF-Connecting-IP': '1.1.1.1' } })
|
|
1559
|
+
expect(limitedRes1.status).toBe(429)
|
|
1560
|
+
|
|
1561
|
+
const limitedRes2 = await app2.request('/test', { headers: { 'CF-Connecting-IP': '1.1.1.1' } })
|
|
1562
|
+
expect(limitedRes2.status).toBe(429)
|
|
1563
|
+
|
|
1564
|
+
// IP 2 should still have full quota
|
|
1565
|
+
const res = await app2.request('/test', { headers: { 'CF-Connecting-IP': '2.2.2.2' } })
|
|
1566
|
+
expect(res.status).toBe(200)
|
|
1567
|
+
})
|
|
1568
|
+
})
|