@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,2348 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for R2PageVFS - R2 Page-based Virtual File System
|
|
3
|
+
*
|
|
4
|
+
* TDD test specifications for page-level access to R2-backed storage
|
|
5
|
+
* with SWR caching integration. This VFS provides page-granularity
|
|
6
|
+
* reads optimized for PGLite database pages stored in R2.
|
|
7
|
+
*
|
|
8
|
+
* Page Storage Format:
|
|
9
|
+
* - Pages stored at: {prefix}/pages/{8-digit-padded-page-number}
|
|
10
|
+
* - Example: db/pages/00000001, db/pages/00000042
|
|
11
|
+
* - Default page size: 8192 bytes (8KB, PostgreSQL default)
|
|
12
|
+
*
|
|
13
|
+
* @module storage/r2-page-vfs.test
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
|
|
17
|
+
import {
|
|
18
|
+
createR2PageVFS,
|
|
19
|
+
formatPageKey,
|
|
20
|
+
buildR2Key,
|
|
21
|
+
buildCacheKey,
|
|
22
|
+
parsePageNumber,
|
|
23
|
+
createEmptyStats,
|
|
24
|
+
R2_PAGE_VFS_DEFAULTS,
|
|
25
|
+
} from './r2-page-vfs'
|
|
26
|
+
import type {
|
|
27
|
+
R2PageVFS,
|
|
28
|
+
R2PageVFSConfig,
|
|
29
|
+
R2PageVFSStats,
|
|
30
|
+
PageReadResult,
|
|
31
|
+
BatchPageResult,
|
|
32
|
+
} from './r2-page-vfs'
|
|
33
|
+
import type { SWRCacheLayer, SWRResult, ExecutionContext } from './swr-cache'
|
|
34
|
+
import {
|
|
35
|
+
createMockR2Bucket,
|
|
36
|
+
createMockSWRCache,
|
|
37
|
+
createMockExecutionContext,
|
|
38
|
+
createPageData,
|
|
39
|
+
} from '../__tests__/test-utils'
|
|
40
|
+
|
|
41
|
+
// Helper to create mock R2Object with vi.fn() for arrayBuffer
|
|
42
|
+
const createMockR2Object = (data: Uint8Array) => ({
|
|
43
|
+
arrayBuffer: vi.fn().mockResolvedValue(data.buffer),
|
|
44
|
+
text: vi.fn().mockResolvedValue(new TextDecoder().decode(data)),
|
|
45
|
+
body: null,
|
|
46
|
+
key: '',
|
|
47
|
+
version: '',
|
|
48
|
+
httpEtag: '',
|
|
49
|
+
etag: '',
|
|
50
|
+
size: data.length,
|
|
51
|
+
uploaded: new Date(),
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
describe('R2PageVFS - R2 Page-based Virtual File System', () => {
|
|
55
|
+
let mockBucket: ReturnType<typeof createMockR2Bucket>
|
|
56
|
+
let mockSwrCache: ReturnType<typeof createMockSWRCache>
|
|
57
|
+
let mockCtx: ReturnType<typeof createMockExecutionContext>
|
|
58
|
+
|
|
59
|
+
beforeEach(() => {
|
|
60
|
+
vi.clearAllMocks()
|
|
61
|
+
mockBucket = createMockR2Bucket()
|
|
62
|
+
mockSwrCache = createMockSWRCache()
|
|
63
|
+
mockCtx = createMockExecutionContext()
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
afterEach(() => {
|
|
67
|
+
vi.restoreAllMocks()
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
describe('R2PageVFSConfig', () => {
|
|
71
|
+
it('should require bucket, prefix, and version', () => {
|
|
72
|
+
const config: R2PageVFSConfig = {
|
|
73
|
+
bucket: mockBucket as unknown as R2Bucket,
|
|
74
|
+
prefix: 'mydb',
|
|
75
|
+
version: '1',
|
|
76
|
+
swrCache: mockSwrCache,
|
|
77
|
+
}
|
|
78
|
+
const vfs = createR2PageVFS(config)
|
|
79
|
+
expect(vfs).toBeDefined()
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
it('should have optional pageSize defaulting to 8192', () => {
|
|
83
|
+
const config: R2PageVFSConfig = {
|
|
84
|
+
bucket: mockBucket as unknown as R2Bucket,
|
|
85
|
+
prefix: 'mydb',
|
|
86
|
+
version: '1',
|
|
87
|
+
swrCache: mockSwrCache,
|
|
88
|
+
}
|
|
89
|
+
const vfs = createR2PageVFS(config)
|
|
90
|
+
expect(vfs.getConfig().pageSize).toBe(8192)
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
it('should require swrCache for caching integration', () => {
|
|
94
|
+
const config: R2PageVFSConfig = {
|
|
95
|
+
bucket: mockBucket as unknown as R2Bucket,
|
|
96
|
+
prefix: 'mydb',
|
|
97
|
+
version: '1',
|
|
98
|
+
swrCache: mockSwrCache,
|
|
99
|
+
}
|
|
100
|
+
const vfs = createR2PageVFS(config)
|
|
101
|
+
expect(vfs.getConfig().swrCache).toBe(mockSwrCache)
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
it('should accept optional concurrencyLimit for batch operations', () => {
|
|
105
|
+
const config: R2PageVFSConfig = {
|
|
106
|
+
bucket: mockBucket as unknown as R2Bucket,
|
|
107
|
+
prefix: 'mydb',
|
|
108
|
+
version: '1',
|
|
109
|
+
swrCache: mockSwrCache,
|
|
110
|
+
concurrencyLimit: 5,
|
|
111
|
+
}
|
|
112
|
+
const vfs = createR2PageVFS(config)
|
|
113
|
+
expect(vfs.getConfig().concurrencyLimit).toBe(5)
|
|
114
|
+
})
|
|
115
|
+
})
|
|
116
|
+
|
|
117
|
+
describe('createR2PageVFS()', () => {
|
|
118
|
+
it('should return an R2PageVFS instance', () => {
|
|
119
|
+
const vfs = createR2PageVFS({
|
|
120
|
+
bucket: mockBucket as unknown as R2Bucket,
|
|
121
|
+
prefix: 'mydb',
|
|
122
|
+
version: '1',
|
|
123
|
+
swrCache: mockSwrCache,
|
|
124
|
+
})
|
|
125
|
+
expect(vfs).toBeDefined()
|
|
126
|
+
expect(typeof vfs.readPage).toBe('function')
|
|
127
|
+
expect(typeof vfs.readPages).toBe('function')
|
|
128
|
+
expect(typeof vfs.getStats).toBe('function')
|
|
129
|
+
})
|
|
130
|
+
|
|
131
|
+
it('should configure SWR cache with origin fetcher', () => {
|
|
132
|
+
createR2PageVFS({
|
|
133
|
+
bucket: mockBucket as unknown as R2Bucket,
|
|
134
|
+
prefix: 'mydb',
|
|
135
|
+
version: '1',
|
|
136
|
+
swrCache: mockSwrCache,
|
|
137
|
+
})
|
|
138
|
+
expect(mockSwrCache.setOriginFetcher).toHaveBeenCalled()
|
|
139
|
+
})
|
|
140
|
+
|
|
141
|
+
it('should use default page size of 8192 when not specified', () => {
|
|
142
|
+
const vfs = createR2PageVFS({
|
|
143
|
+
bucket: mockBucket as unknown as R2Bucket,
|
|
144
|
+
prefix: 'mydb',
|
|
145
|
+
version: '1',
|
|
146
|
+
swrCache: mockSwrCache,
|
|
147
|
+
})
|
|
148
|
+
expect(vfs.getConfig().pageSize).toBe(8192)
|
|
149
|
+
})
|
|
150
|
+
|
|
151
|
+
it('should validate prefix does not end with slash', () => {
|
|
152
|
+
expect(() =>
|
|
153
|
+
createR2PageVFS({
|
|
154
|
+
bucket: mockBucket as unknown as R2Bucket,
|
|
155
|
+
prefix: 'mydb/',
|
|
156
|
+
version: '1',
|
|
157
|
+
swrCache: mockSwrCache,
|
|
158
|
+
})
|
|
159
|
+
).toThrow('Prefix must not end with a slash')
|
|
160
|
+
})
|
|
161
|
+
})
|
|
162
|
+
|
|
163
|
+
describe('page reading', () => {
|
|
164
|
+
it('should read page from R2 by page number', async () => {
|
|
165
|
+
const pageData = createPageData()
|
|
166
|
+
const mockR2Object = createMockR2Object(pageData)
|
|
167
|
+
mockBucket.get.mockResolvedValue(mockR2Object)
|
|
168
|
+
;(mockSwrCache.get as ReturnType<typeof vi.fn>).mockResolvedValue({
|
|
169
|
+
data: null,
|
|
170
|
+
hit: false,
|
|
171
|
+
stale: false,
|
|
172
|
+
revalidating: false,
|
|
173
|
+
})
|
|
174
|
+
|
|
175
|
+
const vfs = createR2PageVFS({
|
|
176
|
+
bucket: mockBucket as unknown as R2Bucket,
|
|
177
|
+
prefix: 'mydb',
|
|
178
|
+
version: '1',
|
|
179
|
+
swrCache: mockSwrCache,
|
|
180
|
+
})
|
|
181
|
+
|
|
182
|
+
const result = await vfs.readPage(1, mockCtx)
|
|
183
|
+
expect(result.data).toEqual(pageData)
|
|
184
|
+
expect(mockBucket.get).toHaveBeenCalledWith('mydb/v1/pages/00000001')
|
|
185
|
+
})
|
|
186
|
+
|
|
187
|
+
it('should return null for non-existent pages', async () => {
|
|
188
|
+
mockBucket.get.mockResolvedValue(null)
|
|
189
|
+
;(mockSwrCache.get as ReturnType<typeof vi.fn>).mockResolvedValue({
|
|
190
|
+
data: null,
|
|
191
|
+
hit: false,
|
|
192
|
+
stale: false,
|
|
193
|
+
revalidating: false,
|
|
194
|
+
})
|
|
195
|
+
|
|
196
|
+
const vfs = createR2PageVFS({
|
|
197
|
+
bucket: mockBucket as unknown as R2Bucket,
|
|
198
|
+
prefix: 'mydb',
|
|
199
|
+
version: '1',
|
|
200
|
+
swrCache: mockSwrCache,
|
|
201
|
+
})
|
|
202
|
+
|
|
203
|
+
const result = await vfs.readPage(999999, mockCtx)
|
|
204
|
+
expect(result.data).toBeNull()
|
|
205
|
+
expect(result.source).toBe('miss')
|
|
206
|
+
})
|
|
207
|
+
|
|
208
|
+
it('should handle page size correctly (default 8KB)', async () => {
|
|
209
|
+
const pageData = createPageData(8192)
|
|
210
|
+
const mockR2Object = createMockR2Object(pageData)
|
|
211
|
+
mockBucket.get.mockResolvedValue(mockR2Object)
|
|
212
|
+
;(mockSwrCache.get as ReturnType<typeof vi.fn>).mockResolvedValue({
|
|
213
|
+
data: null,
|
|
214
|
+
hit: false,
|
|
215
|
+
stale: false,
|
|
216
|
+
revalidating: false,
|
|
217
|
+
})
|
|
218
|
+
|
|
219
|
+
const vfs = createR2PageVFS({
|
|
220
|
+
bucket: mockBucket as unknown as R2Bucket,
|
|
221
|
+
prefix: 'mydb',
|
|
222
|
+
version: '1',
|
|
223
|
+
swrCache: mockSwrCache,
|
|
224
|
+
})
|
|
225
|
+
|
|
226
|
+
const result = await vfs.readPage(1, mockCtx)
|
|
227
|
+
expect(result.data?.length).toBe(8192)
|
|
228
|
+
})
|
|
229
|
+
|
|
230
|
+
it('should use correct R2 key format: prefix/v{version}/pages/00000001', async () => {
|
|
231
|
+
mockBucket.get.mockResolvedValue(null)
|
|
232
|
+
;(mockSwrCache.get as ReturnType<typeof vi.fn>).mockResolvedValue({
|
|
233
|
+
data: null,
|
|
234
|
+
hit: false,
|
|
235
|
+
stale: false,
|
|
236
|
+
revalidating: false,
|
|
237
|
+
})
|
|
238
|
+
|
|
239
|
+
const vfs = createR2PageVFS({
|
|
240
|
+
bucket: mockBucket as unknown as R2Bucket,
|
|
241
|
+
prefix: 'mydb',
|
|
242
|
+
version: '1',
|
|
243
|
+
swrCache: mockSwrCache,
|
|
244
|
+
})
|
|
245
|
+
|
|
246
|
+
await vfs.readPage(1, mockCtx)
|
|
247
|
+
expect(mockBucket.get).toHaveBeenCalledWith('mydb/v1/pages/00000001')
|
|
248
|
+
})
|
|
249
|
+
|
|
250
|
+
it('should pad page numbers to 8 digits', async () => {
|
|
251
|
+
mockBucket.get.mockResolvedValue(null)
|
|
252
|
+
;(mockSwrCache.get as ReturnType<typeof vi.fn>).mockResolvedValue({
|
|
253
|
+
data: null,
|
|
254
|
+
hit: false,
|
|
255
|
+
stale: false,
|
|
256
|
+
revalidating: false,
|
|
257
|
+
})
|
|
258
|
+
|
|
259
|
+
const vfs = createR2PageVFS({
|
|
260
|
+
bucket: mockBucket as unknown as R2Bucket,
|
|
261
|
+
prefix: 'mydb',
|
|
262
|
+
version: '1',
|
|
263
|
+
swrCache: mockSwrCache,
|
|
264
|
+
})
|
|
265
|
+
|
|
266
|
+
await vfs.readPage(42, mockCtx)
|
|
267
|
+
expect(mockBucket.get).toHaveBeenCalledWith('mydb/v1/pages/00000042')
|
|
268
|
+
|
|
269
|
+
await vfs.readPage(12345678, mockCtx)
|
|
270
|
+
expect(mockBucket.get).toHaveBeenCalledWith('mydb/v1/pages/12345678')
|
|
271
|
+
})
|
|
272
|
+
|
|
273
|
+
it('should handle maximum page number (99999999)', async () => {
|
|
274
|
+
mockBucket.get.mockResolvedValue(null)
|
|
275
|
+
;(mockSwrCache.get as ReturnType<typeof vi.fn>).mockResolvedValue({
|
|
276
|
+
data: null,
|
|
277
|
+
hit: false,
|
|
278
|
+
stale: false,
|
|
279
|
+
revalidating: false,
|
|
280
|
+
})
|
|
281
|
+
|
|
282
|
+
const vfs = createR2PageVFS({
|
|
283
|
+
bucket: mockBucket as unknown as R2Bucket,
|
|
284
|
+
prefix: 'mydb',
|
|
285
|
+
version: '1',
|
|
286
|
+
swrCache: mockSwrCache,
|
|
287
|
+
})
|
|
288
|
+
|
|
289
|
+
await vfs.readPage(99999999, mockCtx)
|
|
290
|
+
expect(mockBucket.get).toHaveBeenCalledWith('mydb/v1/pages/99999999')
|
|
291
|
+
})
|
|
292
|
+
|
|
293
|
+
it('should validate page number is positive integer', async () => {
|
|
294
|
+
const vfs = createR2PageVFS({
|
|
295
|
+
bucket: mockBucket as unknown as R2Bucket,
|
|
296
|
+
prefix: 'mydb',
|
|
297
|
+
version: '1',
|
|
298
|
+
swrCache: mockSwrCache,
|
|
299
|
+
})
|
|
300
|
+
|
|
301
|
+
await expect(vfs.readPage(-1, mockCtx)).rejects.toThrow('Invalid page number')
|
|
302
|
+
await expect(vfs.readPage(1.5, mockCtx)).rejects.toThrow('Invalid page number')
|
|
303
|
+
})
|
|
304
|
+
|
|
305
|
+
it('should include version in R2 key path for isolation', async () => {
|
|
306
|
+
mockBucket.get.mockResolvedValue(null)
|
|
307
|
+
;(mockSwrCache.get as ReturnType<typeof vi.fn>).mockResolvedValue({
|
|
308
|
+
data: null,
|
|
309
|
+
hit: false,
|
|
310
|
+
stale: false,
|
|
311
|
+
revalidating: false,
|
|
312
|
+
})
|
|
313
|
+
|
|
314
|
+
const vfs = createR2PageVFS({
|
|
315
|
+
bucket: mockBucket as unknown as R2Bucket,
|
|
316
|
+
prefix: 'mydb',
|
|
317
|
+
version: '2',
|
|
318
|
+
swrCache: mockSwrCache,
|
|
319
|
+
})
|
|
320
|
+
|
|
321
|
+
await vfs.readPage(1, mockCtx)
|
|
322
|
+
expect(mockBucket.get).toHaveBeenCalledWith('mydb/v2/pages/00000001')
|
|
323
|
+
})
|
|
324
|
+
})
|
|
325
|
+
|
|
326
|
+
describe('caching with SWR', () => {
|
|
327
|
+
it('should check primary cache first', async () => {
|
|
328
|
+
const pageData = createPageData()
|
|
329
|
+
;(mockSwrCache.get as ReturnType<typeof vi.fn>).mockResolvedValue({
|
|
330
|
+
data: pageData,
|
|
331
|
+
hit: true,
|
|
332
|
+
stale: false,
|
|
333
|
+
revalidating: false,
|
|
334
|
+
})
|
|
335
|
+
|
|
336
|
+
const vfs = createR2PageVFS({
|
|
337
|
+
bucket: mockBucket as unknown as R2Bucket,
|
|
338
|
+
prefix: 'mydb',
|
|
339
|
+
version: '1',
|
|
340
|
+
swrCache: mockSwrCache,
|
|
341
|
+
})
|
|
342
|
+
|
|
343
|
+
const result = await vfs.readPage(1, mockCtx)
|
|
344
|
+
expect(result.hit).toBe(true)
|
|
345
|
+
expect(result.source).toBe('primary-cache')
|
|
346
|
+
expect(mockBucket.get).not.toHaveBeenCalled()
|
|
347
|
+
})
|
|
348
|
+
|
|
349
|
+
it('should check stale cache on primary miss', async () => {
|
|
350
|
+
const pageData = createPageData()
|
|
351
|
+
;(mockSwrCache.get as ReturnType<typeof vi.fn>).mockResolvedValue({
|
|
352
|
+
data: pageData,
|
|
353
|
+
hit: true,
|
|
354
|
+
stale: true,
|
|
355
|
+
revalidating: true,
|
|
356
|
+
})
|
|
357
|
+
|
|
358
|
+
const vfs = createR2PageVFS({
|
|
359
|
+
bucket: mockBucket as unknown as R2Bucket,
|
|
360
|
+
prefix: 'mydb',
|
|
361
|
+
version: '1',
|
|
362
|
+
swrCache: mockSwrCache,
|
|
363
|
+
})
|
|
364
|
+
|
|
365
|
+
const result = await vfs.readPage(1, mockCtx)
|
|
366
|
+
expect(result.hit).toBe(true)
|
|
367
|
+
expect(result.stale).toBe(true)
|
|
368
|
+
expect(result.source).toBe('stale-cache')
|
|
369
|
+
})
|
|
370
|
+
|
|
371
|
+
it('should fetch from R2 on complete miss', async () => {
|
|
372
|
+
const pageData = createPageData()
|
|
373
|
+
const mockR2Object = createMockR2Object(pageData)
|
|
374
|
+
mockBucket.get.mockResolvedValue(mockR2Object)
|
|
375
|
+
;(mockSwrCache.get as ReturnType<typeof vi.fn>).mockResolvedValue({
|
|
376
|
+
data: null,
|
|
377
|
+
hit: false,
|
|
378
|
+
stale: false,
|
|
379
|
+
revalidating: false,
|
|
380
|
+
})
|
|
381
|
+
|
|
382
|
+
const vfs = createR2PageVFS({
|
|
383
|
+
bucket: mockBucket as unknown as R2Bucket,
|
|
384
|
+
prefix: 'mydb',
|
|
385
|
+
version: '1',
|
|
386
|
+
swrCache: mockSwrCache,
|
|
387
|
+
})
|
|
388
|
+
|
|
389
|
+
const result = await vfs.readPage(1, mockCtx)
|
|
390
|
+
expect(result.data).toEqual(pageData)
|
|
391
|
+
expect(result.source).toBe('r2')
|
|
392
|
+
expect(mockBucket.get).toHaveBeenCalled()
|
|
393
|
+
})
|
|
394
|
+
|
|
395
|
+
it('should populate both caches after R2 fetch', async () => {
|
|
396
|
+
const pageData = createPageData()
|
|
397
|
+
const mockR2Object = createMockR2Object(pageData)
|
|
398
|
+
mockBucket.get.mockResolvedValue(mockR2Object)
|
|
399
|
+
;(mockSwrCache.get as ReturnType<typeof vi.fn>).mockResolvedValue({
|
|
400
|
+
data: null,
|
|
401
|
+
hit: false,
|
|
402
|
+
stale: false,
|
|
403
|
+
revalidating: false,
|
|
404
|
+
})
|
|
405
|
+
|
|
406
|
+
const vfs = createR2PageVFS({
|
|
407
|
+
bucket: mockBucket as unknown as R2Bucket,
|
|
408
|
+
prefix: 'mydb',
|
|
409
|
+
version: '1',
|
|
410
|
+
swrCache: mockSwrCache,
|
|
411
|
+
})
|
|
412
|
+
|
|
413
|
+
await vfs.readPage(1, mockCtx)
|
|
414
|
+
expect(mockSwrCache.put).toHaveBeenCalledWith('page-v1-00000001', pageData)
|
|
415
|
+
})
|
|
416
|
+
|
|
417
|
+
it('should trigger background revalidation on stale hit', async () => {
|
|
418
|
+
const pageData = createPageData()
|
|
419
|
+
;(mockSwrCache.get as ReturnType<typeof vi.fn>).mockResolvedValue({
|
|
420
|
+
data: pageData,
|
|
421
|
+
hit: true,
|
|
422
|
+
stale: true,
|
|
423
|
+
revalidating: true,
|
|
424
|
+
})
|
|
425
|
+
|
|
426
|
+
const vfs = createR2PageVFS({
|
|
427
|
+
bucket: mockBucket as unknown as R2Bucket,
|
|
428
|
+
prefix: 'mydb',
|
|
429
|
+
version: '1',
|
|
430
|
+
swrCache: mockSwrCache,
|
|
431
|
+
})
|
|
432
|
+
|
|
433
|
+
const result = await vfs.readPage(1, mockCtx)
|
|
434
|
+
expect(result.stale).toBe(true)
|
|
435
|
+
const stats = vfs.getStats()
|
|
436
|
+
expect(stats.backgroundRevalidations).toBe(1)
|
|
437
|
+
})
|
|
438
|
+
|
|
439
|
+
it('should use page-specific cache keys', async () => {
|
|
440
|
+
mockBucket.get.mockResolvedValue(null)
|
|
441
|
+
;(mockSwrCache.get as ReturnType<typeof vi.fn>).mockResolvedValue({
|
|
442
|
+
data: null,
|
|
443
|
+
hit: false,
|
|
444
|
+
stale: false,
|
|
445
|
+
revalidating: false,
|
|
446
|
+
})
|
|
447
|
+
|
|
448
|
+
const vfs = createR2PageVFS({
|
|
449
|
+
bucket: mockBucket as unknown as R2Bucket,
|
|
450
|
+
prefix: 'mydb',
|
|
451
|
+
version: '1',
|
|
452
|
+
swrCache: mockSwrCache,
|
|
453
|
+
})
|
|
454
|
+
|
|
455
|
+
await vfs.readPage(42, mockCtx)
|
|
456
|
+
expect(mockSwrCache.get).toHaveBeenCalledWith('page-v1-00000042', mockCtx)
|
|
457
|
+
})
|
|
458
|
+
|
|
459
|
+
it('should return stale flag in result when serving stale data', async () => {
|
|
460
|
+
const pageData = createPageData()
|
|
461
|
+
;(mockSwrCache.get as ReturnType<typeof vi.fn>).mockResolvedValue({
|
|
462
|
+
data: pageData,
|
|
463
|
+
hit: true,
|
|
464
|
+
stale: true,
|
|
465
|
+
revalidating: false,
|
|
466
|
+
})
|
|
467
|
+
|
|
468
|
+
const vfs = createR2PageVFS({
|
|
469
|
+
bucket: mockBucket as unknown as R2Bucket,
|
|
470
|
+
prefix: 'mydb',
|
|
471
|
+
version: '1',
|
|
472
|
+
swrCache: mockSwrCache,
|
|
473
|
+
})
|
|
474
|
+
|
|
475
|
+
const result = await vfs.readPage(1, mockCtx)
|
|
476
|
+
expect(result.stale).toBe(true)
|
|
477
|
+
})
|
|
478
|
+
|
|
479
|
+
it('should pass execution context for background revalidation', async () => {
|
|
480
|
+
;(mockSwrCache.get as ReturnType<typeof vi.fn>).mockResolvedValue({
|
|
481
|
+
data: null,
|
|
482
|
+
hit: false,
|
|
483
|
+
stale: false,
|
|
484
|
+
revalidating: false,
|
|
485
|
+
})
|
|
486
|
+
mockBucket.get.mockResolvedValue(null)
|
|
487
|
+
|
|
488
|
+
const vfs = createR2PageVFS({
|
|
489
|
+
bucket: mockBucket as unknown as R2Bucket,
|
|
490
|
+
prefix: 'mydb',
|
|
491
|
+
version: '1',
|
|
492
|
+
swrCache: mockSwrCache,
|
|
493
|
+
})
|
|
494
|
+
|
|
495
|
+
await vfs.readPage(1, mockCtx)
|
|
496
|
+
expect(mockSwrCache.get).toHaveBeenCalledWith(expect.any(String), mockCtx)
|
|
497
|
+
})
|
|
498
|
+
})
|
|
499
|
+
|
|
500
|
+
describe('version management', () => {
|
|
501
|
+
it('should include version in cache keys for busting', async () => {
|
|
502
|
+
;(mockSwrCache.get as ReturnType<typeof vi.fn>).mockResolvedValue({
|
|
503
|
+
data: null,
|
|
504
|
+
hit: false,
|
|
505
|
+
stale: false,
|
|
506
|
+
revalidating: false,
|
|
507
|
+
})
|
|
508
|
+
mockBucket.get.mockResolvedValue(null)
|
|
509
|
+
|
|
510
|
+
const vfs1 = createR2PageVFS({
|
|
511
|
+
bucket: mockBucket as unknown as R2Bucket,
|
|
512
|
+
prefix: 'mydb',
|
|
513
|
+
version: '1',
|
|
514
|
+
swrCache: mockSwrCache,
|
|
515
|
+
})
|
|
516
|
+
|
|
517
|
+
await vfs1.readPage(1, mockCtx)
|
|
518
|
+
expect(mockSwrCache.get).toHaveBeenCalledWith('page-v1-00000001', mockCtx)
|
|
519
|
+
|
|
520
|
+
vi.clearAllMocks()
|
|
521
|
+
const mockSwrCache2 = createMockSWRCache()
|
|
522
|
+
;(mockSwrCache2.get as ReturnType<typeof vi.fn>).mockResolvedValue({
|
|
523
|
+
data: null,
|
|
524
|
+
hit: false,
|
|
525
|
+
stale: false,
|
|
526
|
+
revalidating: false,
|
|
527
|
+
})
|
|
528
|
+
|
|
529
|
+
const vfs2 = createR2PageVFS({
|
|
530
|
+
bucket: mockBucket as unknown as R2Bucket,
|
|
531
|
+
prefix: 'mydb',
|
|
532
|
+
version: '2',
|
|
533
|
+
swrCache: mockSwrCache2,
|
|
534
|
+
})
|
|
535
|
+
|
|
536
|
+
await vfs2.readPage(1, mockCtx)
|
|
537
|
+
expect(mockSwrCache2.get).toHaveBeenCalledWith('page-v2-00000001', mockCtx)
|
|
538
|
+
})
|
|
539
|
+
|
|
540
|
+
it('should read latest version from R2 metadata', async () => {
|
|
541
|
+
const versionData = new TextEncoder().encode('v2.1.0')
|
|
542
|
+
const mockR2Object = createMockR2Object(versionData)
|
|
543
|
+
mockBucket.get.mockResolvedValue(mockR2Object)
|
|
544
|
+
|
|
545
|
+
const vfs = createR2PageVFS({
|
|
546
|
+
bucket: mockBucket as unknown as R2Bucket,
|
|
547
|
+
prefix: 'mydb',
|
|
548
|
+
version: '1',
|
|
549
|
+
swrCache: mockSwrCache,
|
|
550
|
+
})
|
|
551
|
+
|
|
552
|
+
const latestVersion = await vfs.getLatestVersion()
|
|
553
|
+
expect(latestVersion).toBe('v2.1.0')
|
|
554
|
+
expect(mockBucket.get).toHaveBeenCalledWith('mydb/version')
|
|
555
|
+
})
|
|
556
|
+
|
|
557
|
+
it('should invalidate caches when version changes', async () => {
|
|
558
|
+
mockBucket.list.mockResolvedValue({
|
|
559
|
+
objects: [
|
|
560
|
+
{ key: 'mydb/v1/pages/00000001' },
|
|
561
|
+
{ key: 'mydb/v1/pages/00000002' },
|
|
562
|
+
],
|
|
563
|
+
truncated: false,
|
|
564
|
+
})
|
|
565
|
+
|
|
566
|
+
const vfs = createR2PageVFS({
|
|
567
|
+
bucket: mockBucket as unknown as R2Bucket,
|
|
568
|
+
prefix: 'mydb',
|
|
569
|
+
version: '1',
|
|
570
|
+
swrCache: mockSwrCache,
|
|
571
|
+
})
|
|
572
|
+
|
|
573
|
+
await vfs.invalidateCache()
|
|
574
|
+
expect(mockSwrCache.invalidate).toHaveBeenCalledWith('page-v1-00000001')
|
|
575
|
+
expect(mockSwrCache.invalidate).toHaveBeenCalledWith('page-v1-00000002')
|
|
576
|
+
})
|
|
577
|
+
|
|
578
|
+
it('should support version comparison for cache invalidation', async () => {
|
|
579
|
+
const versionData = new TextEncoder().encode('1')
|
|
580
|
+
const mockR2Object = createMockR2Object(versionData)
|
|
581
|
+
mockBucket.get.mockResolvedValue(mockR2Object)
|
|
582
|
+
|
|
583
|
+
const vfs = createR2PageVFS({
|
|
584
|
+
bucket: mockBucket as unknown as R2Bucket,
|
|
585
|
+
prefix: 'mydb',
|
|
586
|
+
version: '1',
|
|
587
|
+
swrCache: mockSwrCache,
|
|
588
|
+
})
|
|
589
|
+
|
|
590
|
+
const isCurrent = await vfs.isVersionCurrent()
|
|
591
|
+
expect(isCurrent).toBe(true)
|
|
592
|
+
|
|
593
|
+
const versionData2 = new TextEncoder().encode('2')
|
|
594
|
+
const mockR2Object2 = createMockR2Object(versionData2)
|
|
595
|
+
mockBucket.get.mockResolvedValue(mockR2Object2)
|
|
596
|
+
|
|
597
|
+
const isNotCurrent = await vfs.isVersionCurrent()
|
|
598
|
+
expect(isNotCurrent).toBe(false)
|
|
599
|
+
})
|
|
600
|
+
|
|
601
|
+
it('should store version as string for flexibility', () => {
|
|
602
|
+
const vfs = createR2PageVFS({
|
|
603
|
+
bucket: mockBucket as unknown as R2Bucket,
|
|
604
|
+
prefix: 'mydb',
|
|
605
|
+
version: '2024-01-15-abc123',
|
|
606
|
+
swrCache: mockSwrCache,
|
|
607
|
+
})
|
|
608
|
+
|
|
609
|
+
expect(vfs.getConfig().version).toBe('2024-01-15-abc123')
|
|
610
|
+
})
|
|
611
|
+
|
|
612
|
+
it('should handle missing version file gracefully', async () => {
|
|
613
|
+
mockBucket.get.mockResolvedValue(null)
|
|
614
|
+
|
|
615
|
+
const vfs = createR2PageVFS({
|
|
616
|
+
bucket: mockBucket as unknown as R2Bucket,
|
|
617
|
+
prefix: 'mydb',
|
|
618
|
+
version: '1',
|
|
619
|
+
swrCache: mockSwrCache,
|
|
620
|
+
})
|
|
621
|
+
|
|
622
|
+
await expect(vfs.getLatestVersion()).rejects.toThrow('Version file not found')
|
|
623
|
+
})
|
|
624
|
+
})
|
|
625
|
+
|
|
626
|
+
describe('batch operations', () => {
|
|
627
|
+
it('should support reading multiple pages in parallel', async () => {
|
|
628
|
+
const pageData = createPageData()
|
|
629
|
+
;(mockSwrCache.get as ReturnType<typeof vi.fn>).mockResolvedValue({
|
|
630
|
+
data: pageData,
|
|
631
|
+
hit: true,
|
|
632
|
+
stale: false,
|
|
633
|
+
revalidating: false,
|
|
634
|
+
})
|
|
635
|
+
|
|
636
|
+
const vfs = createR2PageVFS({
|
|
637
|
+
bucket: mockBucket as unknown as R2Bucket,
|
|
638
|
+
prefix: 'mydb',
|
|
639
|
+
version: '1',
|
|
640
|
+
swrCache: mockSwrCache,
|
|
641
|
+
})
|
|
642
|
+
|
|
643
|
+
const result = await vfs.readPages([1, 2, 3], mockCtx)
|
|
644
|
+
expect(result.pages.size).toBe(3)
|
|
645
|
+
expect(result.pages.has(1)).toBe(true)
|
|
646
|
+
expect(result.pages.has(2)).toBe(true)
|
|
647
|
+
expect(result.pages.has(3)).toBe(true)
|
|
648
|
+
})
|
|
649
|
+
|
|
650
|
+
it('should respect concurrency limits', async () => {
|
|
651
|
+
const pageData = createPageData()
|
|
652
|
+
let concurrentCalls = 0
|
|
653
|
+
let maxConcurrent = 0
|
|
654
|
+
|
|
655
|
+
;(mockSwrCache.get as ReturnType<typeof vi.fn>).mockImplementation(async () => {
|
|
656
|
+
concurrentCalls++
|
|
657
|
+
maxConcurrent = Math.max(maxConcurrent, concurrentCalls)
|
|
658
|
+
await new Promise((resolve) => setTimeout(resolve, 10))
|
|
659
|
+
concurrentCalls--
|
|
660
|
+
return {
|
|
661
|
+
data: pageData,
|
|
662
|
+
hit: true,
|
|
663
|
+
stale: false,
|
|
664
|
+
revalidating: false,
|
|
665
|
+
}
|
|
666
|
+
})
|
|
667
|
+
|
|
668
|
+
const vfs = createR2PageVFS({
|
|
669
|
+
bucket: mockBucket as unknown as R2Bucket,
|
|
670
|
+
prefix: 'mydb',
|
|
671
|
+
version: '1',
|
|
672
|
+
swrCache: mockSwrCache,
|
|
673
|
+
concurrencyLimit: 2,
|
|
674
|
+
})
|
|
675
|
+
|
|
676
|
+
await vfs.readPages([1, 2, 3, 4, 5], mockCtx)
|
|
677
|
+
expect(maxConcurrent).toBeLessThanOrEqual(2)
|
|
678
|
+
})
|
|
679
|
+
|
|
680
|
+
it('should return Map of page number to data', async () => {
|
|
681
|
+
const pageData = createPageData()
|
|
682
|
+
;(mockSwrCache.get as ReturnType<typeof vi.fn>).mockResolvedValue({
|
|
683
|
+
data: pageData,
|
|
684
|
+
hit: true,
|
|
685
|
+
stale: false,
|
|
686
|
+
revalidating: false,
|
|
687
|
+
})
|
|
688
|
+
|
|
689
|
+
const vfs = createR2PageVFS({
|
|
690
|
+
bucket: mockBucket as unknown as R2Bucket,
|
|
691
|
+
prefix: 'mydb',
|
|
692
|
+
version: '1',
|
|
693
|
+
swrCache: mockSwrCache,
|
|
694
|
+
})
|
|
695
|
+
|
|
696
|
+
const result = await vfs.readPages([1, 2], mockCtx)
|
|
697
|
+
expect(result.pages instanceof Map).toBe(true)
|
|
698
|
+
expect(result.pages.get(1)?.data).toEqual(pageData)
|
|
699
|
+
})
|
|
700
|
+
|
|
701
|
+
it('should handle partial failures in batch', async () => {
|
|
702
|
+
const pageData = createPageData()
|
|
703
|
+
let callCount = 0
|
|
704
|
+
;(mockSwrCache.get as ReturnType<typeof vi.fn>).mockImplementation(async () => {
|
|
705
|
+
callCount++
|
|
706
|
+
if (callCount === 2) {
|
|
707
|
+
return { data: null, hit: false, stale: false, revalidating: false }
|
|
708
|
+
}
|
|
709
|
+
return { data: pageData, hit: true, stale: false, revalidating: false }
|
|
710
|
+
})
|
|
711
|
+
mockBucket.get.mockResolvedValue(null)
|
|
712
|
+
|
|
713
|
+
const vfs = createR2PageVFS({
|
|
714
|
+
bucket: mockBucket as unknown as R2Bucket,
|
|
715
|
+
prefix: 'mydb',
|
|
716
|
+
version: '1',
|
|
717
|
+
swrCache: mockSwrCache,
|
|
718
|
+
})
|
|
719
|
+
|
|
720
|
+
const result = await vfs.readPages([1, 2, 3], mockCtx)
|
|
721
|
+
expect(result.pages.size).toBe(3)
|
|
722
|
+
expect(result.pages.get(1)?.data).toEqual(pageData)
|
|
723
|
+
expect(result.pages.get(2)?.data).toBeNull()
|
|
724
|
+
expect(result.pages.get(3)?.data).toEqual(pageData)
|
|
725
|
+
})
|
|
726
|
+
|
|
727
|
+
it('should deduplicate page numbers in batch request', async () => {
|
|
728
|
+
const pageData = createPageData()
|
|
729
|
+
;(mockSwrCache.get as ReturnType<typeof vi.fn>).mockResolvedValue({
|
|
730
|
+
data: pageData,
|
|
731
|
+
hit: true,
|
|
732
|
+
stale: false,
|
|
733
|
+
revalidating: false,
|
|
734
|
+
})
|
|
735
|
+
|
|
736
|
+
const vfs = createR2PageVFS({
|
|
737
|
+
bucket: mockBucket as unknown as R2Bucket,
|
|
738
|
+
prefix: 'mydb',
|
|
739
|
+
version: '1',
|
|
740
|
+
swrCache: mockSwrCache,
|
|
741
|
+
})
|
|
742
|
+
|
|
743
|
+
await vfs.readPages([1, 1, 2, 2, 2], mockCtx)
|
|
744
|
+
expect(mockSwrCache.get).toHaveBeenCalledTimes(2) // Only 2 unique pages
|
|
745
|
+
})
|
|
746
|
+
|
|
747
|
+
it('should use cache for batch operations', async () => {
|
|
748
|
+
const pageData = createPageData()
|
|
749
|
+
;(mockSwrCache.get as ReturnType<typeof vi.fn>).mockResolvedValue({
|
|
750
|
+
data: pageData,
|
|
751
|
+
hit: true,
|
|
752
|
+
stale: false,
|
|
753
|
+
revalidating: false,
|
|
754
|
+
})
|
|
755
|
+
|
|
756
|
+
const vfs = createR2PageVFS({
|
|
757
|
+
bucket: mockBucket as unknown as R2Bucket,
|
|
758
|
+
prefix: 'mydb',
|
|
759
|
+
version: '1',
|
|
760
|
+
swrCache: mockSwrCache,
|
|
761
|
+
})
|
|
762
|
+
|
|
763
|
+
const result = await vfs.readPages([1, 2, 3], mockCtx)
|
|
764
|
+
expect(result.cacheHits).toBe(3)
|
|
765
|
+
expect(result.r2Reads).toBe(0)
|
|
766
|
+
})
|
|
767
|
+
|
|
768
|
+
it('should order-preserve results regardless of completion order', async () => {
|
|
769
|
+
const pageData = createPageData()
|
|
770
|
+
;(mockSwrCache.get as ReturnType<typeof vi.fn>).mockImplementation(async (key: string) => {
|
|
771
|
+
// Simulate different response times
|
|
772
|
+
const delay = key.includes('00000002') ? 50 : 10
|
|
773
|
+
await new Promise((resolve) => setTimeout(resolve, delay))
|
|
774
|
+
return { data: pageData, hit: true, stale: false, revalidating: false }
|
|
775
|
+
})
|
|
776
|
+
|
|
777
|
+
const vfs = createR2PageVFS({
|
|
778
|
+
bucket: mockBucket as unknown as R2Bucket,
|
|
779
|
+
prefix: 'mydb',
|
|
780
|
+
version: '1',
|
|
781
|
+
swrCache: mockSwrCache,
|
|
782
|
+
})
|
|
783
|
+
|
|
784
|
+
const result = await vfs.readPages([1, 2, 3], mockCtx)
|
|
785
|
+
const keys = Array.from(result.pages.keys())
|
|
786
|
+
expect(keys).toEqual([1, 2, 3])
|
|
787
|
+
})
|
|
788
|
+
|
|
789
|
+
it('should optimize batch by grouping cache hits and misses', async () => {
|
|
790
|
+
const pageData = createPageData()
|
|
791
|
+
const mockR2Object = createMockR2Object(pageData)
|
|
792
|
+
mockBucket.get.mockResolvedValue(mockR2Object)
|
|
793
|
+
|
|
794
|
+
let callCount = 0
|
|
795
|
+
;(mockSwrCache.get as ReturnType<typeof vi.fn>).mockImplementation(async () => {
|
|
796
|
+
callCount++
|
|
797
|
+
if (callCount <= 2) {
|
|
798
|
+
return { data: pageData, hit: true, stale: false, revalidating: false }
|
|
799
|
+
}
|
|
800
|
+
return { data: null, hit: false, stale: false, revalidating: false }
|
|
801
|
+
})
|
|
802
|
+
|
|
803
|
+
const vfs = createR2PageVFS({
|
|
804
|
+
bucket: mockBucket as unknown as R2Bucket,
|
|
805
|
+
prefix: 'mydb',
|
|
806
|
+
version: '1',
|
|
807
|
+
swrCache: mockSwrCache,
|
|
808
|
+
})
|
|
809
|
+
|
|
810
|
+
const result = await vfs.readPages([1, 2, 3], mockCtx)
|
|
811
|
+
expect(result.cacheHits).toBe(2)
|
|
812
|
+
expect(result.r2Reads).toBe(1)
|
|
813
|
+
})
|
|
814
|
+
})
|
|
815
|
+
|
|
816
|
+
describe('stats', () => {
|
|
817
|
+
it('should track cache hits by tier', async () => {
|
|
818
|
+
const pageData = createPageData()
|
|
819
|
+
;(mockSwrCache.get as ReturnType<typeof vi.fn>)
|
|
820
|
+
.mockResolvedValueOnce({ data: pageData, hit: true, stale: false, revalidating: false })
|
|
821
|
+
.mockResolvedValueOnce({ data: pageData, hit: true, stale: true, revalidating: false })
|
|
822
|
+
|
|
823
|
+
const vfs = createR2PageVFS({
|
|
824
|
+
bucket: mockBucket as unknown as R2Bucket,
|
|
825
|
+
prefix: 'mydb',
|
|
826
|
+
version: '1',
|
|
827
|
+
swrCache: mockSwrCache,
|
|
828
|
+
})
|
|
829
|
+
|
|
830
|
+
await vfs.readPage(1, mockCtx)
|
|
831
|
+
await vfs.readPage(2, mockCtx)
|
|
832
|
+
|
|
833
|
+
const stats = vfs.getStats()
|
|
834
|
+
expect(stats.primaryCacheHits).toBe(1)
|
|
835
|
+
expect(stats.staleCacheHits).toBe(1)
|
|
836
|
+
})
|
|
837
|
+
|
|
838
|
+
it('should track R2 reads', async () => {
|
|
839
|
+
const pageData = createPageData()
|
|
840
|
+
const mockR2Object = createMockR2Object(pageData)
|
|
841
|
+
mockBucket.get.mockResolvedValue(mockR2Object)
|
|
842
|
+
;(mockSwrCache.get as ReturnType<typeof vi.fn>).mockResolvedValue({
|
|
843
|
+
data: null,
|
|
844
|
+
hit: false,
|
|
845
|
+
stale: false,
|
|
846
|
+
revalidating: false,
|
|
847
|
+
})
|
|
848
|
+
|
|
849
|
+
const vfs = createR2PageVFS({
|
|
850
|
+
bucket: mockBucket as unknown as R2Bucket,
|
|
851
|
+
prefix: 'mydb',
|
|
852
|
+
version: '1',
|
|
853
|
+
swrCache: mockSwrCache,
|
|
854
|
+
})
|
|
855
|
+
|
|
856
|
+
await vfs.readPage(1, mockCtx)
|
|
857
|
+
await vfs.readPage(2, mockCtx)
|
|
858
|
+
|
|
859
|
+
const stats = vfs.getStats()
|
|
860
|
+
expect(stats.r2Reads).toBe(2)
|
|
861
|
+
})
|
|
862
|
+
|
|
863
|
+
it('should track bytes transferred', async () => {
|
|
864
|
+
const pageData = createPageData(8192)
|
|
865
|
+
;(mockSwrCache.get as ReturnType<typeof vi.fn>).mockResolvedValue({
|
|
866
|
+
data: pageData,
|
|
867
|
+
hit: true,
|
|
868
|
+
stale: false,
|
|
869
|
+
revalidating: false,
|
|
870
|
+
})
|
|
871
|
+
|
|
872
|
+
const vfs = createR2PageVFS({
|
|
873
|
+
bucket: mockBucket as unknown as R2Bucket,
|
|
874
|
+
prefix: 'mydb',
|
|
875
|
+
version: '1',
|
|
876
|
+
swrCache: mockSwrCache,
|
|
877
|
+
})
|
|
878
|
+
|
|
879
|
+
await vfs.readPage(1, mockCtx)
|
|
880
|
+
await vfs.readPage(2, mockCtx)
|
|
881
|
+
|
|
882
|
+
const stats = vfs.getStats()
|
|
883
|
+
expect(stats.bytesRead).toBe(16384)
|
|
884
|
+
expect(stats.bytesFromCache).toBe(16384)
|
|
885
|
+
})
|
|
886
|
+
|
|
887
|
+
it('should track cache miss count', async () => {
|
|
888
|
+
mockBucket.get.mockResolvedValue(null)
|
|
889
|
+
;(mockSwrCache.get as ReturnType<typeof vi.fn>).mockResolvedValue({
|
|
890
|
+
data: null,
|
|
891
|
+
hit: false,
|
|
892
|
+
stale: false,
|
|
893
|
+
revalidating: false,
|
|
894
|
+
})
|
|
895
|
+
|
|
896
|
+
const vfs = createR2PageVFS({
|
|
897
|
+
bucket: mockBucket as unknown as R2Bucket,
|
|
898
|
+
prefix: 'mydb',
|
|
899
|
+
version: '1',
|
|
900
|
+
swrCache: mockSwrCache,
|
|
901
|
+
})
|
|
902
|
+
|
|
903
|
+
await vfs.readPage(1, mockCtx)
|
|
904
|
+
await vfs.readPage(2, mockCtx)
|
|
905
|
+
|
|
906
|
+
const stats = vfs.getStats()
|
|
907
|
+
expect(stats.cacheMisses).toBe(2)
|
|
908
|
+
})
|
|
909
|
+
|
|
910
|
+
it('should calculate cache hit ratio', async () => {
|
|
911
|
+
const pageData = createPageData()
|
|
912
|
+
;(mockSwrCache.get as ReturnType<typeof vi.fn>)
|
|
913
|
+
.mockResolvedValueOnce({ data: pageData, hit: true, stale: false, revalidating: false })
|
|
914
|
+
.mockResolvedValueOnce({ data: pageData, hit: true, stale: false, revalidating: false })
|
|
915
|
+
.mockResolvedValueOnce({ data: null, hit: false, stale: false, revalidating: false })
|
|
916
|
+
mockBucket.get.mockResolvedValue(null)
|
|
917
|
+
|
|
918
|
+
const vfs = createR2PageVFS({
|
|
919
|
+
bucket: mockBucket as unknown as R2Bucket,
|
|
920
|
+
prefix: 'mydb',
|
|
921
|
+
version: '1',
|
|
922
|
+
swrCache: mockSwrCache,
|
|
923
|
+
})
|
|
924
|
+
|
|
925
|
+
await vfs.readPage(1, mockCtx)
|
|
926
|
+
await vfs.readPage(2, mockCtx)
|
|
927
|
+
await vfs.readPage(3, mockCtx)
|
|
928
|
+
|
|
929
|
+
const stats = vfs.getStats()
|
|
930
|
+
expect(stats.cacheHitRatio).toBeCloseTo(2 / 3, 2)
|
|
931
|
+
})
|
|
932
|
+
|
|
933
|
+
it('should track batch operation count', async () => {
|
|
934
|
+
const pageData = createPageData()
|
|
935
|
+
;(mockSwrCache.get as ReturnType<typeof vi.fn>).mockResolvedValue({
|
|
936
|
+
data: pageData,
|
|
937
|
+
hit: true,
|
|
938
|
+
stale: false,
|
|
939
|
+
revalidating: false,
|
|
940
|
+
})
|
|
941
|
+
|
|
942
|
+
const vfs = createR2PageVFS({
|
|
943
|
+
bucket: mockBucket as unknown as R2Bucket,
|
|
944
|
+
prefix: 'mydb',
|
|
945
|
+
version: '1',
|
|
946
|
+
swrCache: mockSwrCache,
|
|
947
|
+
})
|
|
948
|
+
|
|
949
|
+
await vfs.readPages([1, 2, 3], mockCtx)
|
|
950
|
+
await vfs.readPages([4, 5], mockCtx)
|
|
951
|
+
|
|
952
|
+
const stats = vfs.getStats()
|
|
953
|
+
expect(stats.batchOperations).toBe(2)
|
|
954
|
+
})
|
|
955
|
+
|
|
956
|
+
it('should track errors by type', async () => {
|
|
957
|
+
;(mockSwrCache.get as ReturnType<typeof vi.fn>).mockRejectedValue(new Error('Cache error'))
|
|
958
|
+
mockBucket.get.mockRejectedValue(new Error('R2 error'))
|
|
959
|
+
|
|
960
|
+
const vfs = createR2PageVFS({
|
|
961
|
+
bucket: mockBucket as unknown as R2Bucket,
|
|
962
|
+
prefix: 'mydb',
|
|
963
|
+
version: '1',
|
|
964
|
+
swrCache: mockSwrCache,
|
|
965
|
+
retryCount: 1,
|
|
966
|
+
})
|
|
967
|
+
|
|
968
|
+
try {
|
|
969
|
+
await vfs.readPage(1, mockCtx)
|
|
970
|
+
} catch {
|
|
971
|
+
// Expected
|
|
972
|
+
}
|
|
973
|
+
|
|
974
|
+
const stats = vfs.getStats()
|
|
975
|
+
expect(stats.errors).toBeGreaterThan(0)
|
|
976
|
+
})
|
|
977
|
+
|
|
978
|
+
it('should support stats reset', async () => {
|
|
979
|
+
const pageData = createPageData()
|
|
980
|
+
;(mockSwrCache.get as ReturnType<typeof vi.fn>).mockResolvedValue({
|
|
981
|
+
data: pageData,
|
|
982
|
+
hit: true,
|
|
983
|
+
stale: false,
|
|
984
|
+
revalidating: false,
|
|
985
|
+
})
|
|
986
|
+
|
|
987
|
+
const vfs = createR2PageVFS({
|
|
988
|
+
bucket: mockBucket as unknown as R2Bucket,
|
|
989
|
+
prefix: 'mydb',
|
|
990
|
+
version: '1',
|
|
991
|
+
swrCache: mockSwrCache,
|
|
992
|
+
})
|
|
993
|
+
|
|
994
|
+
await vfs.readPage(1, mockCtx)
|
|
995
|
+
expect(vfs.getStats().primaryCacheHits).toBe(1)
|
|
996
|
+
|
|
997
|
+
vfs.resetStats()
|
|
998
|
+
expect(vfs.getStats().primaryCacheHits).toBe(0)
|
|
999
|
+
})
|
|
1000
|
+
|
|
1001
|
+
it('should track background revalidation count', async () => {
|
|
1002
|
+
const pageData = createPageData()
|
|
1003
|
+
;(mockSwrCache.get as ReturnType<typeof vi.fn>).mockResolvedValue({
|
|
1004
|
+
data: pageData,
|
|
1005
|
+
hit: true,
|
|
1006
|
+
stale: true,
|
|
1007
|
+
revalidating: true,
|
|
1008
|
+
})
|
|
1009
|
+
|
|
1010
|
+
const vfs = createR2PageVFS({
|
|
1011
|
+
bucket: mockBucket as unknown as R2Bucket,
|
|
1012
|
+
prefix: 'mydb',
|
|
1013
|
+
version: '1',
|
|
1014
|
+
swrCache: mockSwrCache,
|
|
1015
|
+
})
|
|
1016
|
+
|
|
1017
|
+
await vfs.readPage(1, mockCtx)
|
|
1018
|
+
await vfs.readPage(2, mockCtx)
|
|
1019
|
+
|
|
1020
|
+
const stats = vfs.getStats()
|
|
1021
|
+
expect(stats.backgroundRevalidations).toBe(2)
|
|
1022
|
+
})
|
|
1023
|
+
|
|
1024
|
+
it('should track average latency per tier', async () => {
|
|
1025
|
+
const pageData = createPageData()
|
|
1026
|
+
;(mockSwrCache.get as ReturnType<typeof vi.fn>).mockResolvedValue({
|
|
1027
|
+
data: pageData,
|
|
1028
|
+
hit: true,
|
|
1029
|
+
stale: false,
|
|
1030
|
+
revalidating: false,
|
|
1031
|
+
})
|
|
1032
|
+
|
|
1033
|
+
const vfs = createR2PageVFS({
|
|
1034
|
+
bucket: mockBucket as unknown as R2Bucket,
|
|
1035
|
+
prefix: 'mydb',
|
|
1036
|
+
version: '1',
|
|
1037
|
+
swrCache: mockSwrCache,
|
|
1038
|
+
})
|
|
1039
|
+
|
|
1040
|
+
await vfs.readPage(1, mockCtx)
|
|
1041
|
+
|
|
1042
|
+
const stats = vfs.getStats()
|
|
1043
|
+
expect(stats.avgCacheLatencyMs).toBeGreaterThanOrEqual(0)
|
|
1044
|
+
})
|
|
1045
|
+
})
|
|
1046
|
+
|
|
1047
|
+
describe('cache invalidation', () => {
|
|
1048
|
+
it('should invalidate single page from cache', async () => {
|
|
1049
|
+
const vfs = createR2PageVFS({
|
|
1050
|
+
bucket: mockBucket as unknown as R2Bucket,
|
|
1051
|
+
prefix: 'mydb',
|
|
1052
|
+
version: '1',
|
|
1053
|
+
swrCache: mockSwrCache,
|
|
1054
|
+
})
|
|
1055
|
+
|
|
1056
|
+
await vfs.invalidatePage(42)
|
|
1057
|
+
expect(mockSwrCache.invalidate).toHaveBeenCalledWith('page-v1-00000042')
|
|
1058
|
+
})
|
|
1059
|
+
|
|
1060
|
+
it('should invalidate all pages for current version', async () => {
|
|
1061
|
+
mockBucket.list.mockResolvedValue({
|
|
1062
|
+
objects: [
|
|
1063
|
+
{ key: 'mydb/v1/pages/00000001' },
|
|
1064
|
+
{ key: 'mydb/v1/pages/00000002' },
|
|
1065
|
+
{ key: 'mydb/v1/pages/00000003' },
|
|
1066
|
+
],
|
|
1067
|
+
truncated: false,
|
|
1068
|
+
})
|
|
1069
|
+
|
|
1070
|
+
const vfs = createR2PageVFS({
|
|
1071
|
+
bucket: mockBucket as unknown as R2Bucket,
|
|
1072
|
+
prefix: 'mydb',
|
|
1073
|
+
version: '1',
|
|
1074
|
+
swrCache: mockSwrCache,
|
|
1075
|
+
})
|
|
1076
|
+
|
|
1077
|
+
await vfs.invalidateCache()
|
|
1078
|
+
expect(mockSwrCache.invalidate).toHaveBeenCalledTimes(3)
|
|
1079
|
+
})
|
|
1080
|
+
|
|
1081
|
+
it('should support invalidation by page range', async () => {
|
|
1082
|
+
const vfs = createR2PageVFS({
|
|
1083
|
+
bucket: mockBucket as unknown as R2Bucket,
|
|
1084
|
+
prefix: 'mydb',
|
|
1085
|
+
version: '1',
|
|
1086
|
+
swrCache: mockSwrCache,
|
|
1087
|
+
})
|
|
1088
|
+
|
|
1089
|
+
await vfs.invalidatePages(1, 3)
|
|
1090
|
+
expect(mockSwrCache.invalidate).toHaveBeenCalledWith('page-v1-00000001')
|
|
1091
|
+
expect(mockSwrCache.invalidate).toHaveBeenCalledWith('page-v1-00000002')
|
|
1092
|
+
expect(mockSwrCache.invalidate).toHaveBeenCalledWith('page-v1-00000003')
|
|
1093
|
+
})
|
|
1094
|
+
|
|
1095
|
+
it('should not affect other version caches on invalidate', async () => {
|
|
1096
|
+
const vfs = createR2PageVFS({
|
|
1097
|
+
bucket: mockBucket as unknown as R2Bucket,
|
|
1098
|
+
prefix: 'mydb',
|
|
1099
|
+
version: '1',
|
|
1100
|
+
swrCache: mockSwrCache,
|
|
1101
|
+
})
|
|
1102
|
+
|
|
1103
|
+
await vfs.invalidatePage(42)
|
|
1104
|
+
expect(mockSwrCache.invalidate).toHaveBeenCalledWith('page-v1-00000042')
|
|
1105
|
+
expect(mockSwrCache.invalidate).not.toHaveBeenCalledWith('page-v2-00000042')
|
|
1106
|
+
})
|
|
1107
|
+
})
|
|
1108
|
+
|
|
1109
|
+
describe('error handling', () => {
|
|
1110
|
+
it('should handle R2 network errors gracefully', async () => {
|
|
1111
|
+
;(mockSwrCache.get as ReturnType<typeof vi.fn>).mockResolvedValue({
|
|
1112
|
+
data: null,
|
|
1113
|
+
hit: false,
|
|
1114
|
+
stale: false,
|
|
1115
|
+
revalidating: false,
|
|
1116
|
+
})
|
|
1117
|
+
mockBucket.get.mockRejectedValue(new Error('Network error'))
|
|
1118
|
+
|
|
1119
|
+
const vfs = createR2PageVFS({
|
|
1120
|
+
bucket: mockBucket as unknown as R2Bucket,
|
|
1121
|
+
prefix: 'mydb',
|
|
1122
|
+
version: '1',
|
|
1123
|
+
swrCache: mockSwrCache,
|
|
1124
|
+
retryCount: 1,
|
|
1125
|
+
})
|
|
1126
|
+
|
|
1127
|
+
const result = await vfs.readPage(1, mockCtx)
|
|
1128
|
+
expect(result.data).toBeNull()
|
|
1129
|
+
expect(result.source).toBe('miss')
|
|
1130
|
+
})
|
|
1131
|
+
|
|
1132
|
+
it('should handle corrupt page data', async () => {
|
|
1133
|
+
const invalidData = new Uint8Array(100) // Wrong size
|
|
1134
|
+
const mockR2Object = createMockR2Object(invalidData)
|
|
1135
|
+
mockBucket.get.mockResolvedValue(mockR2Object)
|
|
1136
|
+
;(mockSwrCache.get as ReturnType<typeof vi.fn>).mockResolvedValue({
|
|
1137
|
+
data: null,
|
|
1138
|
+
hit: false,
|
|
1139
|
+
stale: false,
|
|
1140
|
+
revalidating: false,
|
|
1141
|
+
})
|
|
1142
|
+
|
|
1143
|
+
const vfs = createR2PageVFS({
|
|
1144
|
+
bucket: mockBucket as unknown as R2Bucket,
|
|
1145
|
+
prefix: 'mydb',
|
|
1146
|
+
version: '1',
|
|
1147
|
+
swrCache: mockSwrCache,
|
|
1148
|
+
retryCount: 1,
|
|
1149
|
+
})
|
|
1150
|
+
|
|
1151
|
+
await expect(vfs.readPage(1, mockCtx)).rejects.toThrow('Invalid page size')
|
|
1152
|
+
})
|
|
1153
|
+
|
|
1154
|
+
it('should retry transient R2 errors', async () => {
|
|
1155
|
+
const pageData = createPageData()
|
|
1156
|
+
const mockR2Object = createMockR2Object(pageData)
|
|
1157
|
+
mockBucket.get
|
|
1158
|
+
.mockRejectedValueOnce(new Error('Transient error'))
|
|
1159
|
+
.mockResolvedValueOnce(mockR2Object)
|
|
1160
|
+
;(mockSwrCache.get as ReturnType<typeof vi.fn>).mockResolvedValue({
|
|
1161
|
+
data: null,
|
|
1162
|
+
hit: false,
|
|
1163
|
+
stale: false,
|
|
1164
|
+
revalidating: false,
|
|
1165
|
+
})
|
|
1166
|
+
|
|
1167
|
+
const vfs = createR2PageVFS({
|
|
1168
|
+
bucket: mockBucket as unknown as R2Bucket,
|
|
1169
|
+
prefix: 'mydb',
|
|
1170
|
+
version: '1',
|
|
1171
|
+
swrCache: mockSwrCache,
|
|
1172
|
+
retryCount: 3,
|
|
1173
|
+
})
|
|
1174
|
+
|
|
1175
|
+
const result = await vfs.readPage(1, mockCtx)
|
|
1176
|
+
expect(result.data).toEqual(pageData)
|
|
1177
|
+
expect(mockBucket.get).toHaveBeenCalledTimes(2)
|
|
1178
|
+
})
|
|
1179
|
+
|
|
1180
|
+
it('should propagate fatal errors', async () => {
|
|
1181
|
+
;(mockSwrCache.get as ReturnType<typeof vi.fn>).mockResolvedValue({
|
|
1182
|
+
data: null,
|
|
1183
|
+
hit: false,
|
|
1184
|
+
stale: false,
|
|
1185
|
+
revalidating: false,
|
|
1186
|
+
})
|
|
1187
|
+
|
|
1188
|
+
// Create a mock that times out
|
|
1189
|
+
mockBucket.get.mockImplementation(
|
|
1190
|
+
() =>
|
|
1191
|
+
new Promise((_, reject) => {
|
|
1192
|
+
setTimeout(() => reject(new Error('R2 operation timed out')), 50)
|
|
1193
|
+
})
|
|
1194
|
+
)
|
|
1195
|
+
|
|
1196
|
+
const vfs = createR2PageVFS({
|
|
1197
|
+
bucket: mockBucket as unknown as R2Bucket,
|
|
1198
|
+
prefix: 'mydb',
|
|
1199
|
+
version: '1',
|
|
1200
|
+
swrCache: mockSwrCache,
|
|
1201
|
+
timeoutMs: 10,
|
|
1202
|
+
retryCount: 1,
|
|
1203
|
+
})
|
|
1204
|
+
|
|
1205
|
+
await expect(vfs.readPage(1, mockCtx)).rejects.toThrow('timed out')
|
|
1206
|
+
})
|
|
1207
|
+
|
|
1208
|
+
it('should handle cache API errors', async () => {
|
|
1209
|
+
const pageData = createPageData()
|
|
1210
|
+
const mockR2Object = createMockR2Object(pageData)
|
|
1211
|
+
;(mockSwrCache.get as ReturnType<typeof vi.fn>).mockRejectedValue(new Error('Cache error'))
|
|
1212
|
+
mockBucket.get.mockResolvedValue(mockR2Object)
|
|
1213
|
+
|
|
1214
|
+
const vfs = createR2PageVFS({
|
|
1215
|
+
bucket: mockBucket as unknown as R2Bucket,
|
|
1216
|
+
prefix: 'mydb',
|
|
1217
|
+
version: '1',
|
|
1218
|
+
swrCache: mockSwrCache,
|
|
1219
|
+
})
|
|
1220
|
+
|
|
1221
|
+
const result = await vfs.readPage(1, mockCtx)
|
|
1222
|
+
expect(result.data).toEqual(pageData)
|
|
1223
|
+
expect(result.source).toBe('r2')
|
|
1224
|
+
})
|
|
1225
|
+
|
|
1226
|
+
it('should timeout long-running R2 operations', async () => {
|
|
1227
|
+
;(mockSwrCache.get as ReturnType<typeof vi.fn>).mockResolvedValue({
|
|
1228
|
+
data: null,
|
|
1229
|
+
hit: false,
|
|
1230
|
+
stale: false,
|
|
1231
|
+
revalidating: false,
|
|
1232
|
+
})
|
|
1233
|
+
|
|
1234
|
+
mockBucket.get.mockImplementation(
|
|
1235
|
+
() => new Promise((resolve) => setTimeout(resolve, 1000))
|
|
1236
|
+
)
|
|
1237
|
+
|
|
1238
|
+
const vfs = createR2PageVFS({
|
|
1239
|
+
bucket: mockBucket as unknown as R2Bucket,
|
|
1240
|
+
prefix: 'mydb',
|
|
1241
|
+
version: '1',
|
|
1242
|
+
swrCache: mockSwrCache,
|
|
1243
|
+
timeoutMs: 50,
|
|
1244
|
+
retryCount: 1,
|
|
1245
|
+
})
|
|
1246
|
+
|
|
1247
|
+
await expect(vfs.readPage(1, mockCtx)).rejects.toThrow('timed out')
|
|
1248
|
+
})
|
|
1249
|
+
})
|
|
1250
|
+
|
|
1251
|
+
describe('page key formatting', () => {
|
|
1252
|
+
it('should format page number as 8-digit padded string', () => {
|
|
1253
|
+
expect(formatPageKey(1)).toBe('00000001')
|
|
1254
|
+
expect(formatPageKey(42)).toBe('00000042')
|
|
1255
|
+
expect(formatPageKey(12345678)).toBe('12345678')
|
|
1256
|
+
expect(formatPageKey(99999999)).toBe('99999999')
|
|
1257
|
+
})
|
|
1258
|
+
|
|
1259
|
+
it('should build full R2 key with prefix and version', () => {
|
|
1260
|
+
expect(buildR2Key('mydb', '1', 1)).toBe('mydb/v1/pages/00000001')
|
|
1261
|
+
expect(buildR2Key('mydb', '2', 42)).toBe('mydb/v2/pages/00000042')
|
|
1262
|
+
})
|
|
1263
|
+
|
|
1264
|
+
it('should build cache key with version for SWR', () => {
|
|
1265
|
+
expect(buildCacheKey('1', 1)).toBe('page-v1-00000001')
|
|
1266
|
+
expect(buildCacheKey('2', 42)).toBe('page-v2-00000042')
|
|
1267
|
+
})
|
|
1268
|
+
|
|
1269
|
+
it('should parse page number from R2 key', () => {
|
|
1270
|
+
expect(parsePageNumber('mydb/v1/pages/00000042')).toBe(42)
|
|
1271
|
+
expect(parsePageNumber('mydb/v1/pages/00000001')).toBe(1)
|
|
1272
|
+
expect(parsePageNumber('mydb/v1/pages/12345678')).toBe(12345678)
|
|
1273
|
+
expect(parsePageNumber('invalid/key')).toBeNull()
|
|
1274
|
+
})
|
|
1275
|
+
})
|
|
1276
|
+
|
|
1277
|
+
describe('integration with SWRCacheLayer', () => {
|
|
1278
|
+
it('should wire origin fetcher to R2 reads', () => {
|
|
1279
|
+
createR2PageVFS({
|
|
1280
|
+
bucket: mockBucket as unknown as R2Bucket,
|
|
1281
|
+
prefix: 'mydb',
|
|
1282
|
+
version: '1',
|
|
1283
|
+
swrCache: mockSwrCache,
|
|
1284
|
+
})
|
|
1285
|
+
|
|
1286
|
+
expect(mockSwrCache.setOriginFetcher).toHaveBeenCalled()
|
|
1287
|
+
const fetcher = (mockSwrCache.setOriginFetcher as ReturnType<typeof vi.fn>).mock.calls[0][0]
|
|
1288
|
+
expect(typeof fetcher).toBe('function')
|
|
1289
|
+
})
|
|
1290
|
+
|
|
1291
|
+
it('should respect SWR TTL configuration', () => {
|
|
1292
|
+
const vfs = createR2PageVFS({
|
|
1293
|
+
bucket: mockBucket as unknown as R2Bucket,
|
|
1294
|
+
prefix: 'mydb',
|
|
1295
|
+
version: '1',
|
|
1296
|
+
swrCache: mockSwrCache,
|
|
1297
|
+
})
|
|
1298
|
+
|
|
1299
|
+
// TTL is managed by SWRCacheLayer, VFS just passes data through
|
|
1300
|
+
expect(vfs.getConfig().swrCache).toBe(mockSwrCache)
|
|
1301
|
+
})
|
|
1302
|
+
|
|
1303
|
+
it('should handle SWR revalidation callback', async () => {
|
|
1304
|
+
const pageData = createPageData()
|
|
1305
|
+
const mockR2Object = createMockR2Object(pageData)
|
|
1306
|
+
mockBucket.get.mockResolvedValue(mockR2Object)
|
|
1307
|
+
|
|
1308
|
+
createR2PageVFS({
|
|
1309
|
+
bucket: mockBucket as unknown as R2Bucket,
|
|
1310
|
+
prefix: 'mydb',
|
|
1311
|
+
version: '1',
|
|
1312
|
+
swrCache: mockSwrCache,
|
|
1313
|
+
})
|
|
1314
|
+
|
|
1315
|
+
// Get the origin fetcher that was registered
|
|
1316
|
+
const fetcher = (mockSwrCache.setOriginFetcher as ReturnType<typeof vi.fn>).mock.calls[0][0]
|
|
1317
|
+
|
|
1318
|
+
// Call the fetcher with a valid cache key
|
|
1319
|
+
const result = await fetcher('page-v1-00000042')
|
|
1320
|
+
expect(result).toEqual(pageData)
|
|
1321
|
+
expect(mockBucket.get).toHaveBeenCalledWith('mydb/v1/pages/00000042')
|
|
1322
|
+
})
|
|
1323
|
+
|
|
1324
|
+
it('should coordinate stats between VFS and SWR', async () => {
|
|
1325
|
+
const pageData = createPageData()
|
|
1326
|
+
;(mockSwrCache.get as ReturnType<typeof vi.fn>).mockResolvedValue({
|
|
1327
|
+
data: pageData,
|
|
1328
|
+
hit: true,
|
|
1329
|
+
stale: false,
|
|
1330
|
+
revalidating: false,
|
|
1331
|
+
})
|
|
1332
|
+
|
|
1333
|
+
const vfs = createR2PageVFS({
|
|
1334
|
+
bucket: mockBucket as unknown as R2Bucket,
|
|
1335
|
+
prefix: 'mydb',
|
|
1336
|
+
version: '1',
|
|
1337
|
+
swrCache: mockSwrCache,
|
|
1338
|
+
})
|
|
1339
|
+
|
|
1340
|
+
await vfs.readPage(1, mockCtx)
|
|
1341
|
+
|
|
1342
|
+
const stats = vfs.getStats()
|
|
1343
|
+
expect(stats.primaryCacheHits).toBe(1)
|
|
1344
|
+
expect(stats.bytesFromCache).toBe(pageData.length)
|
|
1345
|
+
})
|
|
1346
|
+
})
|
|
1347
|
+
})
|
|
1348
|
+
|
|
1349
|
+
describe('R2PageVFSStats', () => {
|
|
1350
|
+
it('should have primaryCacheHits counter', () => {
|
|
1351
|
+
const stats = createEmptyStats()
|
|
1352
|
+
expect(stats.primaryCacheHits).toBe(0)
|
|
1353
|
+
})
|
|
1354
|
+
|
|
1355
|
+
it('should have staleCacheHits counter', () => {
|
|
1356
|
+
const stats = createEmptyStats()
|
|
1357
|
+
expect(stats.staleCacheHits).toBe(0)
|
|
1358
|
+
})
|
|
1359
|
+
|
|
1360
|
+
it('should have cacheMisses counter', () => {
|
|
1361
|
+
const stats = createEmptyStats()
|
|
1362
|
+
expect(stats.cacheMisses).toBe(0)
|
|
1363
|
+
})
|
|
1364
|
+
|
|
1365
|
+
it('should have r2Reads counter', () => {
|
|
1366
|
+
const stats = createEmptyStats()
|
|
1367
|
+
expect(stats.r2Reads).toBe(0)
|
|
1368
|
+
})
|
|
1369
|
+
|
|
1370
|
+
it('should have r2Errors counter', () => {
|
|
1371
|
+
const stats = createEmptyStats()
|
|
1372
|
+
expect(stats.r2Errors).toBe(0)
|
|
1373
|
+
})
|
|
1374
|
+
|
|
1375
|
+
it('should have bytesRead counter', () => {
|
|
1376
|
+
const stats = createEmptyStats()
|
|
1377
|
+
expect(stats.bytesRead).toBe(0)
|
|
1378
|
+
})
|
|
1379
|
+
|
|
1380
|
+
it('should have bytesFromCache counter', () => {
|
|
1381
|
+
const stats = createEmptyStats()
|
|
1382
|
+
expect(stats.bytesFromCache).toBe(0)
|
|
1383
|
+
})
|
|
1384
|
+
|
|
1385
|
+
it('should have bytesFromR2 counter', () => {
|
|
1386
|
+
const stats = createEmptyStats()
|
|
1387
|
+
expect(stats.bytesFromR2).toBe(0)
|
|
1388
|
+
})
|
|
1389
|
+
|
|
1390
|
+
it('should have batchOperations counter', () => {
|
|
1391
|
+
const stats = createEmptyStats()
|
|
1392
|
+
expect(stats.batchOperations).toBe(0)
|
|
1393
|
+
})
|
|
1394
|
+
|
|
1395
|
+
it('should have backgroundRevalidations counter', () => {
|
|
1396
|
+
const stats = createEmptyStats()
|
|
1397
|
+
expect(stats.backgroundRevalidations).toBe(0)
|
|
1398
|
+
})
|
|
1399
|
+
|
|
1400
|
+
it('should have cacheHitRatio calculated property', () => {
|
|
1401
|
+
const stats = createEmptyStats()
|
|
1402
|
+
expect(stats.cacheHitRatio).toBe(0)
|
|
1403
|
+
})
|
|
1404
|
+
|
|
1405
|
+
it('should have avgLatencyMs metrics', () => {
|
|
1406
|
+
const stats = createEmptyStats()
|
|
1407
|
+
expect(stats.avgCacheLatencyMs).toBe(0)
|
|
1408
|
+
expect(stats.avgR2LatencyMs).toBe(0)
|
|
1409
|
+
})
|
|
1410
|
+
})
|
|
1411
|
+
|
|
1412
|
+
describe('PageReadResult', () => {
|
|
1413
|
+
it('should include data as Uint8Array or null', async () => {
|
|
1414
|
+
const mockBucket = createMockR2Bucket()
|
|
1415
|
+
const mockSwrCache = createMockSWRCache()
|
|
1416
|
+
const mockCtx = createMockExecutionContext()
|
|
1417
|
+
const pageData = createPageData()
|
|
1418
|
+
;(mockSwrCache.get as ReturnType<typeof vi.fn>).mockResolvedValue({
|
|
1419
|
+
data: pageData,
|
|
1420
|
+
hit: true,
|
|
1421
|
+
stale: false,
|
|
1422
|
+
revalidating: false,
|
|
1423
|
+
})
|
|
1424
|
+
|
|
1425
|
+
const vfs = createR2PageVFS({
|
|
1426
|
+
bucket: mockBucket as unknown as R2Bucket,
|
|
1427
|
+
prefix: 'mydb',
|
|
1428
|
+
version: '1',
|
|
1429
|
+
swrCache: mockSwrCache,
|
|
1430
|
+
})
|
|
1431
|
+
|
|
1432
|
+
const result = await vfs.readPage(1, mockCtx)
|
|
1433
|
+
expect(result.data instanceof Uint8Array).toBe(true)
|
|
1434
|
+
})
|
|
1435
|
+
|
|
1436
|
+
it('should include hit boolean for cache status', async () => {
|
|
1437
|
+
const mockBucket = createMockR2Bucket()
|
|
1438
|
+
const mockSwrCache = createMockSWRCache()
|
|
1439
|
+
const mockCtx = createMockExecutionContext()
|
|
1440
|
+
const pageData = createPageData()
|
|
1441
|
+
;(mockSwrCache.get as ReturnType<typeof vi.fn>).mockResolvedValue({
|
|
1442
|
+
data: pageData,
|
|
1443
|
+
hit: true,
|
|
1444
|
+
stale: false,
|
|
1445
|
+
revalidating: false,
|
|
1446
|
+
})
|
|
1447
|
+
|
|
1448
|
+
const vfs = createR2PageVFS({
|
|
1449
|
+
bucket: mockBucket as unknown as R2Bucket,
|
|
1450
|
+
prefix: 'mydb',
|
|
1451
|
+
version: '1',
|
|
1452
|
+
swrCache: mockSwrCache,
|
|
1453
|
+
})
|
|
1454
|
+
|
|
1455
|
+
const result = await vfs.readPage(1, mockCtx)
|
|
1456
|
+
expect(typeof result.hit).toBe('boolean')
|
|
1457
|
+
})
|
|
1458
|
+
|
|
1459
|
+
it('should include stale boolean for freshness', async () => {
|
|
1460
|
+
const mockBucket = createMockR2Bucket()
|
|
1461
|
+
const mockSwrCache = createMockSWRCache()
|
|
1462
|
+
const mockCtx = createMockExecutionContext()
|
|
1463
|
+
const pageData = createPageData()
|
|
1464
|
+
;(mockSwrCache.get as ReturnType<typeof vi.fn>).mockResolvedValue({
|
|
1465
|
+
data: pageData,
|
|
1466
|
+
hit: true,
|
|
1467
|
+
stale: true,
|
|
1468
|
+
revalidating: false,
|
|
1469
|
+
})
|
|
1470
|
+
|
|
1471
|
+
const vfs = createR2PageVFS({
|
|
1472
|
+
bucket: mockBucket as unknown as R2Bucket,
|
|
1473
|
+
prefix: 'mydb',
|
|
1474
|
+
version: '1',
|
|
1475
|
+
swrCache: mockSwrCache,
|
|
1476
|
+
})
|
|
1477
|
+
|
|
1478
|
+
const result = await vfs.readPage(1, mockCtx)
|
|
1479
|
+
expect(typeof result.stale).toBe('boolean')
|
|
1480
|
+
expect(result.stale).toBe(true)
|
|
1481
|
+
})
|
|
1482
|
+
|
|
1483
|
+
it('should include source tier identifier', async () => {
|
|
1484
|
+
const mockBucket = createMockR2Bucket()
|
|
1485
|
+
const mockSwrCache = createMockSWRCache()
|
|
1486
|
+
const mockCtx = createMockExecutionContext()
|
|
1487
|
+
const pageData = createPageData()
|
|
1488
|
+
;(mockSwrCache.get as ReturnType<typeof vi.fn>).mockResolvedValue({
|
|
1489
|
+
data: pageData,
|
|
1490
|
+
hit: true,
|
|
1491
|
+
stale: false,
|
|
1492
|
+
revalidating: false,
|
|
1493
|
+
})
|
|
1494
|
+
|
|
1495
|
+
const vfs = createR2PageVFS({
|
|
1496
|
+
bucket: mockBucket as unknown as R2Bucket,
|
|
1497
|
+
prefix: 'mydb',
|
|
1498
|
+
version: '1',
|
|
1499
|
+
swrCache: mockSwrCache,
|
|
1500
|
+
})
|
|
1501
|
+
|
|
1502
|
+
const result = await vfs.readPage(1, mockCtx)
|
|
1503
|
+
expect(['primary-cache', 'stale-cache', 'r2', 'miss']).toContain(result.source)
|
|
1504
|
+
})
|
|
1505
|
+
|
|
1506
|
+
it('should include latencyMs for performance tracking', async () => {
|
|
1507
|
+
const mockBucket = createMockR2Bucket()
|
|
1508
|
+
const mockSwrCache = createMockSWRCache()
|
|
1509
|
+
const mockCtx = createMockExecutionContext()
|
|
1510
|
+
const pageData = createPageData()
|
|
1511
|
+
;(mockSwrCache.get as ReturnType<typeof vi.fn>).mockResolvedValue({
|
|
1512
|
+
data: pageData,
|
|
1513
|
+
hit: true,
|
|
1514
|
+
stale: false,
|
|
1515
|
+
revalidating: false,
|
|
1516
|
+
})
|
|
1517
|
+
|
|
1518
|
+
const vfs = createR2PageVFS({
|
|
1519
|
+
bucket: mockBucket as unknown as R2Bucket,
|
|
1520
|
+
prefix: 'mydb',
|
|
1521
|
+
version: '1',
|
|
1522
|
+
swrCache: mockSwrCache,
|
|
1523
|
+
})
|
|
1524
|
+
|
|
1525
|
+
const result = await vfs.readPage(1, mockCtx)
|
|
1526
|
+
expect(typeof result.latencyMs).toBe('number')
|
|
1527
|
+
expect(result.latencyMs).toBeGreaterThanOrEqual(0)
|
|
1528
|
+
})
|
|
1529
|
+
})
|
|
1530
|
+
|
|
1531
|
+
describe('Circuit Breaker', () => {
|
|
1532
|
+
let mockBucket: ReturnType<typeof createMockR2Bucket>
|
|
1533
|
+
let mockSwrCache: ReturnType<typeof createMockSWRCache>
|
|
1534
|
+
let mockCtx: ReturnType<typeof createMockContext>
|
|
1535
|
+
|
|
1536
|
+
beforeEach(() => {
|
|
1537
|
+
vi.clearAllMocks()
|
|
1538
|
+
mockBucket = createMockR2Bucket()
|
|
1539
|
+
mockSwrCache = createMockSWRCache()
|
|
1540
|
+
mockCtx = createMockExecutionContext()
|
|
1541
|
+
})
|
|
1542
|
+
|
|
1543
|
+
afterEach(() => {
|
|
1544
|
+
vi.restoreAllMocks()
|
|
1545
|
+
})
|
|
1546
|
+
|
|
1547
|
+
it('should start in closed state', () => {
|
|
1548
|
+
const vfs = createR2PageVFS({
|
|
1549
|
+
bucket: mockBucket as unknown as R2Bucket,
|
|
1550
|
+
prefix: 'mydb',
|
|
1551
|
+
version: '1',
|
|
1552
|
+
swrCache: mockSwrCache,
|
|
1553
|
+
})
|
|
1554
|
+
|
|
1555
|
+
const stats = vfs.getStats()
|
|
1556
|
+
expect(stats.circuitBreakerState).toBe('closed')
|
|
1557
|
+
expect(stats.circuitBreakerTrips).toBe(0)
|
|
1558
|
+
})
|
|
1559
|
+
|
|
1560
|
+
it('should accept custom circuit breaker configuration', () => {
|
|
1561
|
+
const vfs = createR2PageVFS({
|
|
1562
|
+
bucket: mockBucket as unknown as R2Bucket,
|
|
1563
|
+
prefix: 'mydb',
|
|
1564
|
+
version: '1',
|
|
1565
|
+
swrCache: mockSwrCache,
|
|
1566
|
+
circuitBreaker: {
|
|
1567
|
+
failureThreshold: 3,
|
|
1568
|
+
resetTimeoutMs: 5000,
|
|
1569
|
+
},
|
|
1570
|
+
})
|
|
1571
|
+
|
|
1572
|
+
expect(vfs.getConfig().circuitBreaker.failureThreshold).toBe(3)
|
|
1573
|
+
expect(vfs.getConfig().circuitBreaker.resetTimeoutMs).toBe(5000)
|
|
1574
|
+
})
|
|
1575
|
+
|
|
1576
|
+
it('should open circuit after reaching failure threshold', async () => {
|
|
1577
|
+
;(mockSwrCache.get as ReturnType<typeof vi.fn>).mockResolvedValue({
|
|
1578
|
+
data: null,
|
|
1579
|
+
hit: false,
|
|
1580
|
+
stale: false,
|
|
1581
|
+
revalidating: false,
|
|
1582
|
+
})
|
|
1583
|
+
mockBucket.get.mockRejectedValue(new Error('R2 error'))
|
|
1584
|
+
|
|
1585
|
+
const vfs = createR2PageVFS({
|
|
1586
|
+
bucket: mockBucket as unknown as R2Bucket,
|
|
1587
|
+
prefix: 'mydb',
|
|
1588
|
+
version: '1',
|
|
1589
|
+
swrCache: mockSwrCache,
|
|
1590
|
+
retryCount: 1,
|
|
1591
|
+
circuitBreaker: {
|
|
1592
|
+
failureThreshold: 3,
|
|
1593
|
+
resetTimeoutMs: 5000,
|
|
1594
|
+
},
|
|
1595
|
+
})
|
|
1596
|
+
|
|
1597
|
+
// Trigger failures
|
|
1598
|
+
for (let i = 0; i < 3; i++) {
|
|
1599
|
+
try {
|
|
1600
|
+
await vfs.readPage(i, mockCtx)
|
|
1601
|
+
} catch {
|
|
1602
|
+
// Expected
|
|
1603
|
+
}
|
|
1604
|
+
}
|
|
1605
|
+
|
|
1606
|
+
const stats = vfs.getStats()
|
|
1607
|
+
expect(stats.circuitBreakerState).toBe('open')
|
|
1608
|
+
expect(stats.circuitBreakerTrips).toBe(1)
|
|
1609
|
+
})
|
|
1610
|
+
|
|
1611
|
+
it('should block requests when circuit is open', async () => {
|
|
1612
|
+
;(mockSwrCache.get as ReturnType<typeof vi.fn>).mockResolvedValue({
|
|
1613
|
+
data: null,
|
|
1614
|
+
hit: false,
|
|
1615
|
+
stale: false,
|
|
1616
|
+
revalidating: false,
|
|
1617
|
+
})
|
|
1618
|
+
mockBucket.get.mockRejectedValue(new Error('R2 error'))
|
|
1619
|
+
|
|
1620
|
+
const vfs = createR2PageVFS({
|
|
1621
|
+
bucket: mockBucket as unknown as R2Bucket,
|
|
1622
|
+
prefix: 'mydb',
|
|
1623
|
+
version: '1',
|
|
1624
|
+
swrCache: mockSwrCache,
|
|
1625
|
+
retryCount: 1,
|
|
1626
|
+
circuitBreaker: {
|
|
1627
|
+
failureThreshold: 2,
|
|
1628
|
+
resetTimeoutMs: 60000,
|
|
1629
|
+
},
|
|
1630
|
+
})
|
|
1631
|
+
|
|
1632
|
+
// Open the circuit
|
|
1633
|
+
for (let i = 0; i < 2; i++) {
|
|
1634
|
+
try {
|
|
1635
|
+
await vfs.readPage(i, mockCtx)
|
|
1636
|
+
} catch {
|
|
1637
|
+
// Expected
|
|
1638
|
+
}
|
|
1639
|
+
}
|
|
1640
|
+
|
|
1641
|
+
expect(vfs.getStats().circuitBreakerState).toBe('open')
|
|
1642
|
+
|
|
1643
|
+
// Clear the call count
|
|
1644
|
+
mockBucket.get.mockClear()
|
|
1645
|
+
|
|
1646
|
+
// Next request should fail fast without calling R2
|
|
1647
|
+
const result = await vfs.readPage(99, mockCtx)
|
|
1648
|
+
expect(result.data).toBeNull()
|
|
1649
|
+
expect(result.source).toBe('miss')
|
|
1650
|
+
// R2 should not be called when circuit is open
|
|
1651
|
+
expect(mockBucket.get).not.toHaveBeenCalled()
|
|
1652
|
+
})
|
|
1653
|
+
|
|
1654
|
+
it('should transition to half-open after reset timeout', async () => {
|
|
1655
|
+
vi.useFakeTimers()
|
|
1656
|
+
|
|
1657
|
+
;(mockSwrCache.get as ReturnType<typeof vi.fn>).mockResolvedValue({
|
|
1658
|
+
data: null,
|
|
1659
|
+
hit: false,
|
|
1660
|
+
stale: false,
|
|
1661
|
+
revalidating: false,
|
|
1662
|
+
})
|
|
1663
|
+
mockBucket.get.mockRejectedValue(new Error('R2 error'))
|
|
1664
|
+
|
|
1665
|
+
const vfs = createR2PageVFS({
|
|
1666
|
+
bucket: mockBucket as unknown as R2Bucket,
|
|
1667
|
+
prefix: 'mydb',
|
|
1668
|
+
version: '1',
|
|
1669
|
+
swrCache: mockSwrCache,
|
|
1670
|
+
retryCount: 1,
|
|
1671
|
+
circuitBreaker: {
|
|
1672
|
+
failureThreshold: 2,
|
|
1673
|
+
resetTimeoutMs: 1000,
|
|
1674
|
+
},
|
|
1675
|
+
})
|
|
1676
|
+
|
|
1677
|
+
// Open the circuit
|
|
1678
|
+
for (let i = 0; i < 2; i++) {
|
|
1679
|
+
try {
|
|
1680
|
+
await vfs.readPage(i, mockCtx)
|
|
1681
|
+
} catch {
|
|
1682
|
+
// Expected
|
|
1683
|
+
}
|
|
1684
|
+
}
|
|
1685
|
+
|
|
1686
|
+
expect(vfs.getStats().circuitBreakerState).toBe('open')
|
|
1687
|
+
|
|
1688
|
+
// Advance time past reset timeout
|
|
1689
|
+
vi.advanceTimersByTime(1001)
|
|
1690
|
+
|
|
1691
|
+
expect(vfs.getStats().circuitBreakerState).toBe('half-open')
|
|
1692
|
+
|
|
1693
|
+
vi.useRealTimers()
|
|
1694
|
+
})
|
|
1695
|
+
|
|
1696
|
+
it('should close circuit on successful request in half-open state', async () => {
|
|
1697
|
+
vi.useFakeTimers()
|
|
1698
|
+
|
|
1699
|
+
const pageData = createPageData()
|
|
1700
|
+
const mockR2Object = createMockR2Object(pageData)
|
|
1701
|
+
|
|
1702
|
+
;(mockSwrCache.get as ReturnType<typeof vi.fn>).mockResolvedValue({
|
|
1703
|
+
data: null,
|
|
1704
|
+
hit: false,
|
|
1705
|
+
stale: false,
|
|
1706
|
+
revalidating: false,
|
|
1707
|
+
})
|
|
1708
|
+
mockBucket.get.mockRejectedValue(new Error('R2 error'))
|
|
1709
|
+
|
|
1710
|
+
const vfs = createR2PageVFS({
|
|
1711
|
+
bucket: mockBucket as unknown as R2Bucket,
|
|
1712
|
+
prefix: 'mydb',
|
|
1713
|
+
version: '1',
|
|
1714
|
+
swrCache: mockSwrCache,
|
|
1715
|
+
retryCount: 1,
|
|
1716
|
+
circuitBreaker: {
|
|
1717
|
+
failureThreshold: 2,
|
|
1718
|
+
resetTimeoutMs: 1000,
|
|
1719
|
+
},
|
|
1720
|
+
})
|
|
1721
|
+
|
|
1722
|
+
// Open the circuit
|
|
1723
|
+
for (let i = 0; i < 2; i++) {
|
|
1724
|
+
try {
|
|
1725
|
+
await vfs.readPage(i, mockCtx)
|
|
1726
|
+
} catch {
|
|
1727
|
+
// Expected
|
|
1728
|
+
}
|
|
1729
|
+
}
|
|
1730
|
+
|
|
1731
|
+
// Advance time to half-open
|
|
1732
|
+
vi.advanceTimersByTime(1001)
|
|
1733
|
+
expect(vfs.getStats().circuitBreakerState).toBe('half-open')
|
|
1734
|
+
|
|
1735
|
+
// Successful request should close the circuit
|
|
1736
|
+
mockBucket.get.mockResolvedValue(mockR2Object)
|
|
1737
|
+
const result = await vfs.readPage(10, mockCtx)
|
|
1738
|
+
expect(result.data).toEqual(pageData)
|
|
1739
|
+
expect(vfs.getStats().circuitBreakerState).toBe('closed')
|
|
1740
|
+
|
|
1741
|
+
vi.useRealTimers()
|
|
1742
|
+
})
|
|
1743
|
+
|
|
1744
|
+
it('should re-open circuit on failed request in half-open state', async () => {
|
|
1745
|
+
vi.useFakeTimers()
|
|
1746
|
+
|
|
1747
|
+
;(mockSwrCache.get as ReturnType<typeof vi.fn>).mockResolvedValue({
|
|
1748
|
+
data: null,
|
|
1749
|
+
hit: false,
|
|
1750
|
+
stale: false,
|
|
1751
|
+
revalidating: false,
|
|
1752
|
+
})
|
|
1753
|
+
mockBucket.get.mockRejectedValue(new Error('R2 error'))
|
|
1754
|
+
|
|
1755
|
+
const vfs = createR2PageVFS({
|
|
1756
|
+
bucket: mockBucket as unknown as R2Bucket,
|
|
1757
|
+
prefix: 'mydb',
|
|
1758
|
+
version: '1',
|
|
1759
|
+
swrCache: mockSwrCache,
|
|
1760
|
+
retryCount: 1,
|
|
1761
|
+
circuitBreaker: {
|
|
1762
|
+
failureThreshold: 2,
|
|
1763
|
+
resetTimeoutMs: 1000,
|
|
1764
|
+
},
|
|
1765
|
+
})
|
|
1766
|
+
|
|
1767
|
+
// Open the circuit
|
|
1768
|
+
for (let i = 0; i < 2; i++) {
|
|
1769
|
+
try {
|
|
1770
|
+
await vfs.readPage(i, mockCtx)
|
|
1771
|
+
} catch {
|
|
1772
|
+
// Expected
|
|
1773
|
+
}
|
|
1774
|
+
}
|
|
1775
|
+
|
|
1776
|
+
expect(vfs.getStats().circuitBreakerTrips).toBe(1)
|
|
1777
|
+
|
|
1778
|
+
// Advance time to half-open
|
|
1779
|
+
vi.advanceTimersByTime(1001)
|
|
1780
|
+
expect(vfs.getStats().circuitBreakerState).toBe('half-open')
|
|
1781
|
+
|
|
1782
|
+
// Failed request should re-open the circuit
|
|
1783
|
+
try {
|
|
1784
|
+
await vfs.readPage(10, mockCtx)
|
|
1785
|
+
} catch {
|
|
1786
|
+
// Expected
|
|
1787
|
+
}
|
|
1788
|
+
|
|
1789
|
+
expect(vfs.getStats().circuitBreakerState).toBe('open')
|
|
1790
|
+
expect(vfs.getStats().circuitBreakerTrips).toBe(2)
|
|
1791
|
+
|
|
1792
|
+
vi.useRealTimers()
|
|
1793
|
+
})
|
|
1794
|
+
|
|
1795
|
+
it('should reset circuit breaker state on stats reset', async () => {
|
|
1796
|
+
;(mockSwrCache.get as ReturnType<typeof vi.fn>).mockResolvedValue({
|
|
1797
|
+
data: null,
|
|
1798
|
+
hit: false,
|
|
1799
|
+
stale: false,
|
|
1800
|
+
revalidating: false,
|
|
1801
|
+
})
|
|
1802
|
+
mockBucket.get.mockRejectedValue(new Error('R2 error'))
|
|
1803
|
+
|
|
1804
|
+
const vfs = createR2PageVFS({
|
|
1805
|
+
bucket: mockBucket as unknown as R2Bucket,
|
|
1806
|
+
prefix: 'mydb',
|
|
1807
|
+
version: '1',
|
|
1808
|
+
swrCache: mockSwrCache,
|
|
1809
|
+
retryCount: 1,
|
|
1810
|
+
circuitBreaker: {
|
|
1811
|
+
failureThreshold: 2,
|
|
1812
|
+
resetTimeoutMs: 5000,
|
|
1813
|
+
},
|
|
1814
|
+
})
|
|
1815
|
+
|
|
1816
|
+
// Open the circuit
|
|
1817
|
+
for (let i = 0; i < 2; i++) {
|
|
1818
|
+
try {
|
|
1819
|
+
await vfs.readPage(i, mockCtx)
|
|
1820
|
+
} catch {
|
|
1821
|
+
// Expected
|
|
1822
|
+
}
|
|
1823
|
+
}
|
|
1824
|
+
|
|
1825
|
+
expect(vfs.getStats().circuitBreakerState).toBe('open')
|
|
1826
|
+
expect(vfs.getStats().circuitBreakerTrips).toBe(1)
|
|
1827
|
+
|
|
1828
|
+
// Reset stats
|
|
1829
|
+
vfs.resetStats()
|
|
1830
|
+
|
|
1831
|
+
expect(vfs.getStats().circuitBreakerState).toBe('closed')
|
|
1832
|
+
expect(vfs.getStats().circuitBreakerTrips).toBe(0)
|
|
1833
|
+
})
|
|
1834
|
+
|
|
1835
|
+
it('should track circuit breaker trips correctly across multiple open/close cycles', async () => {
|
|
1836
|
+
vi.useFakeTimers()
|
|
1837
|
+
|
|
1838
|
+
const pageData = createPageData()
|
|
1839
|
+
const mockR2Object = createMockR2Object(pageData)
|
|
1840
|
+
|
|
1841
|
+
;(mockSwrCache.get as ReturnType<typeof vi.fn>).mockResolvedValue({
|
|
1842
|
+
data: null,
|
|
1843
|
+
hit: false,
|
|
1844
|
+
stale: false,
|
|
1845
|
+
revalidating: false,
|
|
1846
|
+
})
|
|
1847
|
+
|
|
1848
|
+
const vfs = createR2PageVFS({
|
|
1849
|
+
bucket: mockBucket as unknown as R2Bucket,
|
|
1850
|
+
prefix: 'mydb',
|
|
1851
|
+
version: '1',
|
|
1852
|
+
swrCache: mockSwrCache,
|
|
1853
|
+
retryCount: 1,
|
|
1854
|
+
circuitBreaker: {
|
|
1855
|
+
failureThreshold: 2,
|
|
1856
|
+
resetTimeoutMs: 1000,
|
|
1857
|
+
},
|
|
1858
|
+
})
|
|
1859
|
+
|
|
1860
|
+
// First cycle: open the circuit
|
|
1861
|
+
mockBucket.get.mockRejectedValue(new Error('R2 error'))
|
|
1862
|
+
for (let i = 0; i < 2; i++) {
|
|
1863
|
+
try {
|
|
1864
|
+
await vfs.readPage(i, mockCtx)
|
|
1865
|
+
} catch {
|
|
1866
|
+
// Expected
|
|
1867
|
+
}
|
|
1868
|
+
}
|
|
1869
|
+
expect(vfs.getStats().circuitBreakerTrips).toBe(1)
|
|
1870
|
+
|
|
1871
|
+
// Recover
|
|
1872
|
+
vi.advanceTimersByTime(1001)
|
|
1873
|
+
mockBucket.get.mockResolvedValue(mockR2Object)
|
|
1874
|
+
await vfs.readPage(10, mockCtx)
|
|
1875
|
+
expect(vfs.getStats().circuitBreakerState).toBe('closed')
|
|
1876
|
+
|
|
1877
|
+
// Second cycle: open the circuit again
|
|
1878
|
+
mockBucket.get.mockRejectedValue(new Error('R2 error'))
|
|
1879
|
+
for (let i = 0; i < 2; i++) {
|
|
1880
|
+
try {
|
|
1881
|
+
await vfs.readPage(i + 20, mockCtx)
|
|
1882
|
+
} catch {
|
|
1883
|
+
// Expected
|
|
1884
|
+
}
|
|
1885
|
+
}
|
|
1886
|
+
expect(vfs.getStats().circuitBreakerTrips).toBe(2)
|
|
1887
|
+
|
|
1888
|
+
vi.useRealTimers()
|
|
1889
|
+
})
|
|
1890
|
+
|
|
1891
|
+
it('should include circuit breaker state in stats', () => {
|
|
1892
|
+
const stats = createEmptyStats()
|
|
1893
|
+
expect(stats.circuitBreakerState).toBe('closed')
|
|
1894
|
+
expect(stats.circuitBreakerTrips).toBe(0)
|
|
1895
|
+
})
|
|
1896
|
+
|
|
1897
|
+
it('should not affect cache hits when circuit is open', async () => {
|
|
1898
|
+
const pageData = createPageData()
|
|
1899
|
+
;(mockSwrCache.get as ReturnType<typeof vi.fn>).mockResolvedValue({
|
|
1900
|
+
data: pageData,
|
|
1901
|
+
hit: true,
|
|
1902
|
+
stale: false,
|
|
1903
|
+
revalidating: false,
|
|
1904
|
+
})
|
|
1905
|
+
mockBucket.get.mockRejectedValue(new Error('R2 error'))
|
|
1906
|
+
|
|
1907
|
+
const vfs = createR2PageVFS({
|
|
1908
|
+
bucket: mockBucket as unknown as R2Bucket,
|
|
1909
|
+
prefix: 'mydb',
|
|
1910
|
+
version: '1',
|
|
1911
|
+
swrCache: mockSwrCache,
|
|
1912
|
+
retryCount: 1,
|
|
1913
|
+
circuitBreaker: {
|
|
1914
|
+
failureThreshold: 2,
|
|
1915
|
+
resetTimeoutMs: 60000,
|
|
1916
|
+
},
|
|
1917
|
+
})
|
|
1918
|
+
|
|
1919
|
+
// Even with circuit open, cache hits should work
|
|
1920
|
+
// First, make some cache misses to open the circuit
|
|
1921
|
+
;(mockSwrCache.get as ReturnType<typeof vi.fn>).mockResolvedValue({
|
|
1922
|
+
data: null,
|
|
1923
|
+
hit: false,
|
|
1924
|
+
stale: false,
|
|
1925
|
+
revalidating: false,
|
|
1926
|
+
})
|
|
1927
|
+
for (let i = 0; i < 2; i++) {
|
|
1928
|
+
try {
|
|
1929
|
+
await vfs.readPage(i, mockCtx)
|
|
1930
|
+
} catch {
|
|
1931
|
+
// Expected
|
|
1932
|
+
}
|
|
1933
|
+
}
|
|
1934
|
+
|
|
1935
|
+
expect(vfs.getStats().circuitBreakerState).toBe('open')
|
|
1936
|
+
|
|
1937
|
+
// Now cache hits should still work
|
|
1938
|
+
;(mockSwrCache.get as ReturnType<typeof vi.fn>).mockResolvedValue({
|
|
1939
|
+
data: pageData,
|
|
1940
|
+
hit: true,
|
|
1941
|
+
stale: false,
|
|
1942
|
+
revalidating: false,
|
|
1943
|
+
})
|
|
1944
|
+
const result = await vfs.readPage(99, mockCtx)
|
|
1945
|
+
expect(result.data).toEqual(pageData)
|
|
1946
|
+
expect(result.source).toBe('primary-cache')
|
|
1947
|
+
})
|
|
1948
|
+
})
|
|
1949
|
+
|
|
1950
|
+
describe('Cache Warming Strategies', () => {
|
|
1951
|
+
let mockBucket: ReturnType<typeof createMockR2Bucket>
|
|
1952
|
+
let mockSwrCache: ReturnType<typeof createMockSWRCache>
|
|
1953
|
+
let mockCtx: ReturnType<typeof createMockExecutionContext>
|
|
1954
|
+
|
|
1955
|
+
beforeEach(() => {
|
|
1956
|
+
vi.clearAllMocks()
|
|
1957
|
+
mockBucket = createMockR2Bucket()
|
|
1958
|
+
mockSwrCache = createMockSWRCache()
|
|
1959
|
+
mockCtx = createMockExecutionContext()
|
|
1960
|
+
})
|
|
1961
|
+
|
|
1962
|
+
afterEach(() => {
|
|
1963
|
+
vi.restoreAllMocks()
|
|
1964
|
+
})
|
|
1965
|
+
|
|
1966
|
+
it('should warm cache by reading pages via readPages (batch read populates cache)', async () => {
|
|
1967
|
+
const pageData = createPageData()
|
|
1968
|
+
const mockR2Object = createMockR2Object(pageData)
|
|
1969
|
+
mockBucket.get.mockResolvedValue(mockR2Object)
|
|
1970
|
+
;(mockSwrCache.get as ReturnType<typeof vi.fn>).mockResolvedValue({
|
|
1971
|
+
data: null,
|
|
1972
|
+
hit: false,
|
|
1973
|
+
stale: false,
|
|
1974
|
+
revalidating: false,
|
|
1975
|
+
})
|
|
1976
|
+
|
|
1977
|
+
const vfs = createR2PageVFS({
|
|
1978
|
+
bucket: mockBucket as unknown as R2Bucket,
|
|
1979
|
+
prefix: 'mydb',
|
|
1980
|
+
version: '1',
|
|
1981
|
+
swrCache: mockSwrCache,
|
|
1982
|
+
})
|
|
1983
|
+
|
|
1984
|
+
// Warm cache by reading pages - this populates the cache
|
|
1985
|
+
const result = await vfs.readPages([1, 2, 3, 4, 5], mockCtx)
|
|
1986
|
+
|
|
1987
|
+
// Verify all pages were fetched from R2 and cached
|
|
1988
|
+
expect(mockBucket.get).toHaveBeenCalledTimes(5)
|
|
1989
|
+
expect(mockSwrCache.put).toHaveBeenCalledTimes(5)
|
|
1990
|
+
expect(result.pages.size).toBe(5)
|
|
1991
|
+
expect(result.r2Reads).toBe(5)
|
|
1992
|
+
})
|
|
1993
|
+
|
|
1994
|
+
it('should warm cache efficiently by serving from cache on subsequent reads', async () => {
|
|
1995
|
+
const pageData = createPageData()
|
|
1996
|
+
const mockR2Object = createMockR2Object(pageData)
|
|
1997
|
+
mockBucket.get.mockResolvedValue(mockR2Object)
|
|
1998
|
+
|
|
1999
|
+
// First call is a miss, subsequent calls are hits
|
|
2000
|
+
;(mockSwrCache.get as ReturnType<typeof vi.fn>)
|
|
2001
|
+
.mockResolvedValueOnce({ data: null, hit: false, stale: false, revalidating: false })
|
|
2002
|
+
.mockResolvedValueOnce({ data: null, hit: false, stale: false, revalidating: false })
|
|
2003
|
+
.mockResolvedValueOnce({ data: null, hit: false, stale: false, revalidating: false })
|
|
2004
|
+
.mockResolvedValue({ data: pageData, hit: true, stale: false, revalidating: false })
|
|
2005
|
+
|
|
2006
|
+
const vfs = createR2PageVFS({
|
|
2007
|
+
bucket: mockBucket as unknown as R2Bucket,
|
|
2008
|
+
prefix: 'mydb',
|
|
2009
|
+
version: '1',
|
|
2010
|
+
swrCache: mockSwrCache,
|
|
2011
|
+
})
|
|
2012
|
+
|
|
2013
|
+
// First batch - cold cache (3 pages)
|
|
2014
|
+
const result1 = await vfs.readPages([1, 2, 3], mockCtx)
|
|
2015
|
+
expect(result1.r2Reads).toBe(3)
|
|
2016
|
+
|
|
2017
|
+
// Second batch - warm cache (same pages should be hits)
|
|
2018
|
+
const result2 = await vfs.readPages([1, 2, 3], mockCtx)
|
|
2019
|
+
expect(result2.cacheHits).toBe(3)
|
|
2020
|
+
expect(result2.r2Reads).toBe(0)
|
|
2021
|
+
})
|
|
2022
|
+
|
|
2023
|
+
it('should use stale-while-revalidate for efficient cache warming patterns', async () => {
|
|
2024
|
+
const pageData = createPageData()
|
|
2025
|
+
;(mockSwrCache.get as ReturnType<typeof vi.fn>).mockResolvedValue({
|
|
2026
|
+
data: pageData,
|
|
2027
|
+
hit: true,
|
|
2028
|
+
stale: true,
|
|
2029
|
+
revalidating: true,
|
|
2030
|
+
})
|
|
2031
|
+
|
|
2032
|
+
const vfs = createR2PageVFS({
|
|
2033
|
+
bucket: mockBucket as unknown as R2Bucket,
|
|
2034
|
+
prefix: 'mydb',
|
|
2035
|
+
version: '1',
|
|
2036
|
+
swrCache: mockSwrCache,
|
|
2037
|
+
})
|
|
2038
|
+
|
|
2039
|
+
// Reading pages with stale data triggers background revalidation
|
|
2040
|
+
const result = await vfs.readPages([1, 2, 3], mockCtx)
|
|
2041
|
+
|
|
2042
|
+
// All pages should return stale data immediately
|
|
2043
|
+
expect(result.pages.size).toBe(3)
|
|
2044
|
+
result.pages.forEach((page) => {
|
|
2045
|
+
expect(page.stale).toBe(true)
|
|
2046
|
+
expect(page.data).toEqual(pageData)
|
|
2047
|
+
})
|
|
2048
|
+
|
|
2049
|
+
// Stats should show background revalidations were triggered
|
|
2050
|
+
const stats = vfs.getStats()
|
|
2051
|
+
expect(stats.backgroundRevalidations).toBe(3)
|
|
2052
|
+
})
|
|
2053
|
+
})
|
|
2054
|
+
|
|
2055
|
+
describe('Multiple Concurrent Batch Reads', () => {
|
|
2056
|
+
let mockBucket: ReturnType<typeof createMockR2Bucket>
|
|
2057
|
+
let mockSwrCache: ReturnType<typeof createMockSWRCache>
|
|
2058
|
+
let mockCtx: ReturnType<typeof createMockExecutionContext>
|
|
2059
|
+
|
|
2060
|
+
beforeEach(() => {
|
|
2061
|
+
vi.clearAllMocks()
|
|
2062
|
+
mockBucket = createMockR2Bucket()
|
|
2063
|
+
mockSwrCache = createMockSWRCache()
|
|
2064
|
+
mockCtx = createMockExecutionContext()
|
|
2065
|
+
})
|
|
2066
|
+
|
|
2067
|
+
afterEach(() => {
|
|
2068
|
+
vi.restoreAllMocks()
|
|
2069
|
+
})
|
|
2070
|
+
|
|
2071
|
+
it('should handle multiple concurrent batch read operations', async () => {
|
|
2072
|
+
const pageData = createPageData()
|
|
2073
|
+
;(mockSwrCache.get as ReturnType<typeof vi.fn>).mockResolvedValue({
|
|
2074
|
+
data: pageData,
|
|
2075
|
+
hit: true,
|
|
2076
|
+
stale: false,
|
|
2077
|
+
revalidating: false,
|
|
2078
|
+
})
|
|
2079
|
+
|
|
2080
|
+
const vfs = createR2PageVFS({
|
|
2081
|
+
bucket: mockBucket as unknown as R2Bucket,
|
|
2082
|
+
prefix: 'mydb',
|
|
2083
|
+
version: '1',
|
|
2084
|
+
swrCache: mockSwrCache,
|
|
2085
|
+
})
|
|
2086
|
+
|
|
2087
|
+
// Launch multiple batch reads concurrently
|
|
2088
|
+
const [result1, result2, result3] = await Promise.all([
|
|
2089
|
+
vfs.readPages([1, 2, 3], mockCtx),
|
|
2090
|
+
vfs.readPages([4, 5, 6], mockCtx),
|
|
2091
|
+
vfs.readPages([7, 8, 9], mockCtx),
|
|
2092
|
+
])
|
|
2093
|
+
|
|
2094
|
+
// All batches should complete successfully
|
|
2095
|
+
expect(result1.pages.size).toBe(3)
|
|
2096
|
+
expect(result2.pages.size).toBe(3)
|
|
2097
|
+
expect(result3.pages.size).toBe(3)
|
|
2098
|
+
})
|
|
2099
|
+
|
|
2100
|
+
it('should handle multiple concurrent batch operations efficiently', async () => {
|
|
2101
|
+
const pageData = createPageData()
|
|
2102
|
+
let totalCalls = 0
|
|
2103
|
+
|
|
2104
|
+
;(mockSwrCache.get as ReturnType<typeof vi.fn>).mockImplementation(async () => {
|
|
2105
|
+
totalCalls++
|
|
2106
|
+
await new Promise((resolve) => setTimeout(resolve, 5))
|
|
2107
|
+
return { data: pageData, hit: true, stale: false, revalidating: false }
|
|
2108
|
+
})
|
|
2109
|
+
|
|
2110
|
+
const vfs = createR2PageVFS({
|
|
2111
|
+
bucket: mockBucket as unknown as R2Bucket,
|
|
2112
|
+
prefix: 'mydb',
|
|
2113
|
+
version: '1',
|
|
2114
|
+
swrCache: mockSwrCache,
|
|
2115
|
+
})
|
|
2116
|
+
|
|
2117
|
+
// Multiple batches with many pages running concurrently
|
|
2118
|
+
await Promise.all([
|
|
2119
|
+
vfs.readPages([1, 2, 3, 4, 5], mockCtx),
|
|
2120
|
+
vfs.readPages([6, 7, 8, 9, 10], mockCtx),
|
|
2121
|
+
])
|
|
2122
|
+
|
|
2123
|
+
// All 10 unique pages should have been fetched
|
|
2124
|
+
expect(totalCalls).toBe(10)
|
|
2125
|
+
})
|
|
2126
|
+
|
|
2127
|
+
it('should handle overlapping page numbers across concurrent batches', async () => {
|
|
2128
|
+
const pageData = createPageData()
|
|
2129
|
+
|
|
2130
|
+
;(mockSwrCache.get as ReturnType<typeof vi.fn>).mockImplementation(async () => {
|
|
2131
|
+
await new Promise((resolve) => setTimeout(resolve, 5))
|
|
2132
|
+
return { data: pageData, hit: true, stale: false, revalidating: false }
|
|
2133
|
+
})
|
|
2134
|
+
|
|
2135
|
+
const vfs = createR2PageVFS({
|
|
2136
|
+
bucket: mockBucket as unknown as R2Bucket,
|
|
2137
|
+
prefix: 'mydb',
|
|
2138
|
+
version: '1',
|
|
2139
|
+
swrCache: mockSwrCache,
|
|
2140
|
+
})
|
|
2141
|
+
|
|
2142
|
+
// Batches with overlapping pages (page 3, 4 are in both)
|
|
2143
|
+
const [result1, result2] = await Promise.all([
|
|
2144
|
+
vfs.readPages([1, 2, 3, 4], mockCtx),
|
|
2145
|
+
vfs.readPages([3, 4, 5, 6], mockCtx),
|
|
2146
|
+
])
|
|
2147
|
+
|
|
2148
|
+
// Both results should be complete
|
|
2149
|
+
expect(result1.pages.size).toBe(4)
|
|
2150
|
+
expect(result2.pages.size).toBe(4)
|
|
2151
|
+
|
|
2152
|
+
// Both batches should return data for overlapping pages
|
|
2153
|
+
expect(result1.pages.get(3)?.data).toEqual(pageData)
|
|
2154
|
+
expect(result2.pages.get(3)?.data).toEqual(pageData)
|
|
2155
|
+
})
|
|
2156
|
+
|
|
2157
|
+
it('should track aggregate stats from multiple concurrent batches', async () => {
|
|
2158
|
+
const pageData = createPageData()
|
|
2159
|
+
;(mockSwrCache.get as ReturnType<typeof vi.fn>).mockResolvedValue({
|
|
2160
|
+
data: pageData,
|
|
2161
|
+
hit: true,
|
|
2162
|
+
stale: false,
|
|
2163
|
+
revalidating: false,
|
|
2164
|
+
})
|
|
2165
|
+
|
|
2166
|
+
const vfs = createR2PageVFS({
|
|
2167
|
+
bucket: mockBucket as unknown as R2Bucket,
|
|
2168
|
+
prefix: 'mydb',
|
|
2169
|
+
version: '1',
|
|
2170
|
+
swrCache: mockSwrCache,
|
|
2171
|
+
})
|
|
2172
|
+
|
|
2173
|
+
await Promise.all([
|
|
2174
|
+
vfs.readPages([1, 2, 3], mockCtx),
|
|
2175
|
+
vfs.readPages([4, 5, 6], mockCtx),
|
|
2176
|
+
vfs.readPages([7, 8], mockCtx),
|
|
2177
|
+
])
|
|
2178
|
+
|
|
2179
|
+
const stats = vfs.getStats()
|
|
2180
|
+
expect(stats.batchOperations).toBe(3)
|
|
2181
|
+
expect(stats.primaryCacheHits).toBe(8) // Total pages across all batches
|
|
2182
|
+
})
|
|
2183
|
+
|
|
2184
|
+
it('should handle partial failures in one batch without affecting others', async () => {
|
|
2185
|
+
const pageData = createPageData()
|
|
2186
|
+
|
|
2187
|
+
;(mockSwrCache.get as ReturnType<typeof vi.fn>).mockImplementation(async (key: string) => {
|
|
2188
|
+
// Fail for page 2 only
|
|
2189
|
+
if (key.includes('00000002')) {
|
|
2190
|
+
return { data: null, hit: false, stale: false, revalidating: false }
|
|
2191
|
+
}
|
|
2192
|
+
return { data: pageData, hit: true, stale: false, revalidating: false }
|
|
2193
|
+
})
|
|
2194
|
+
mockBucket.get.mockResolvedValue(null) // R2 also returns null
|
|
2195
|
+
|
|
2196
|
+
const vfs = createR2PageVFS({
|
|
2197
|
+
bucket: mockBucket as unknown as R2Bucket,
|
|
2198
|
+
prefix: 'mydb',
|
|
2199
|
+
version: '1',
|
|
2200
|
+
swrCache: mockSwrCache,
|
|
2201
|
+
})
|
|
2202
|
+
|
|
2203
|
+
const [result1, result2] = await Promise.all([
|
|
2204
|
+
vfs.readPages([1, 2, 3], mockCtx), // Page 2 will have null data
|
|
2205
|
+
vfs.readPages([4, 5, 6], mockCtx), // All pages should succeed
|
|
2206
|
+
])
|
|
2207
|
+
|
|
2208
|
+
// First batch has partial success
|
|
2209
|
+
expect(result1.pages.get(1)?.data).toEqual(pageData)
|
|
2210
|
+
expect(result1.pages.get(2)?.data).toBeNull()
|
|
2211
|
+
expect(result1.pages.get(3)?.data).toEqual(pageData)
|
|
2212
|
+
|
|
2213
|
+
// Second batch should be unaffected
|
|
2214
|
+
expect(result2.pages.get(4)?.data).toEqual(pageData)
|
|
2215
|
+
expect(result2.pages.get(5)?.data).toEqual(pageData)
|
|
2216
|
+
expect(result2.pages.get(6)?.data).toEqual(pageData)
|
|
2217
|
+
})
|
|
2218
|
+
})
|
|
2219
|
+
|
|
2220
|
+
describe('Edge Cases', () => {
|
|
2221
|
+
it('should handle page 0 (header page)', async () => {
|
|
2222
|
+
const mockBucket = createMockR2Bucket()
|
|
2223
|
+
const mockSwrCache = createMockSWRCache()
|
|
2224
|
+
const mockCtx = createMockExecutionContext()
|
|
2225
|
+
;(mockSwrCache.get as ReturnType<typeof vi.fn>).mockResolvedValue({
|
|
2226
|
+
data: null,
|
|
2227
|
+
hit: false,
|
|
2228
|
+
stale: false,
|
|
2229
|
+
revalidating: false,
|
|
2230
|
+
})
|
|
2231
|
+
mockBucket.get.mockResolvedValue(null)
|
|
2232
|
+
|
|
2233
|
+
const vfs = createR2PageVFS({
|
|
2234
|
+
bucket: mockBucket as unknown as R2Bucket,
|
|
2235
|
+
prefix: 'mydb',
|
|
2236
|
+
version: '1',
|
|
2237
|
+
swrCache: mockSwrCache,
|
|
2238
|
+
})
|
|
2239
|
+
|
|
2240
|
+
await vfs.readPage(0, mockCtx)
|
|
2241
|
+
expect(mockBucket.get).toHaveBeenCalledWith('mydb/v1/pages/00000000')
|
|
2242
|
+
})
|
|
2243
|
+
|
|
2244
|
+
it('should handle empty pages (all zeros)', async () => {
|
|
2245
|
+
const mockBucket = createMockR2Bucket()
|
|
2246
|
+
const mockSwrCache = createMockSWRCache()
|
|
2247
|
+
const mockCtx = createMockExecutionContext()
|
|
2248
|
+
const zeroPage = createPageData(8192, 0)
|
|
2249
|
+
const mockR2Object = createMockR2Object(zeroPage)
|
|
2250
|
+
mockBucket.get.mockResolvedValue(mockR2Object)
|
|
2251
|
+
;(mockSwrCache.get as ReturnType<typeof vi.fn>).mockResolvedValue({
|
|
2252
|
+
data: null,
|
|
2253
|
+
hit: false,
|
|
2254
|
+
stale: false,
|
|
2255
|
+
revalidating: false,
|
|
2256
|
+
})
|
|
2257
|
+
|
|
2258
|
+
const vfs = createR2PageVFS({
|
|
2259
|
+
bucket: mockBucket as unknown as R2Bucket,
|
|
2260
|
+
prefix: 'mydb',
|
|
2261
|
+
version: '1',
|
|
2262
|
+
swrCache: mockSwrCache,
|
|
2263
|
+
})
|
|
2264
|
+
|
|
2265
|
+
const result = await vfs.readPage(1, mockCtx)
|
|
2266
|
+
expect(result.data).toEqual(zeroPage)
|
|
2267
|
+
})
|
|
2268
|
+
|
|
2269
|
+
it('should handle concurrent reads to same page', async () => {
|
|
2270
|
+
const mockBucket = createMockR2Bucket()
|
|
2271
|
+
const mockSwrCache = createMockSWRCache()
|
|
2272
|
+
const mockCtx = createMockExecutionContext()
|
|
2273
|
+
const pageData = createPageData()
|
|
2274
|
+
;(mockSwrCache.get as ReturnType<typeof vi.fn>).mockImplementation(async () => {
|
|
2275
|
+
await new Promise((resolve) => setTimeout(resolve, 10))
|
|
2276
|
+
return { data: pageData, hit: true, stale: false, revalidating: false }
|
|
2277
|
+
})
|
|
2278
|
+
|
|
2279
|
+
const vfs = createR2PageVFS({
|
|
2280
|
+
bucket: mockBucket as unknown as R2Bucket,
|
|
2281
|
+
prefix: 'mydb',
|
|
2282
|
+
version: '1',
|
|
2283
|
+
swrCache: mockSwrCache,
|
|
2284
|
+
})
|
|
2285
|
+
|
|
2286
|
+
const results = await Promise.all([
|
|
2287
|
+
vfs.readPage(1, mockCtx),
|
|
2288
|
+
vfs.readPage(1, mockCtx),
|
|
2289
|
+
vfs.readPage(1, mockCtx),
|
|
2290
|
+
])
|
|
2291
|
+
|
|
2292
|
+
expect(results.every((r) => r.data?.length === 8192)).toBe(true)
|
|
2293
|
+
})
|
|
2294
|
+
|
|
2295
|
+
it('should handle R2 returning partial page', async () => {
|
|
2296
|
+
const mockBucket = createMockR2Bucket()
|
|
2297
|
+
const mockSwrCache = createMockSWRCache()
|
|
2298
|
+
const mockCtx = createMockExecutionContext()
|
|
2299
|
+
const partialPage = new Uint8Array(4096) // Half a page
|
|
2300
|
+
const mockR2Object = createMockR2Object(partialPage)
|
|
2301
|
+
mockBucket.get.mockResolvedValue(mockR2Object)
|
|
2302
|
+
;(mockSwrCache.get as ReturnType<typeof vi.fn>).mockResolvedValue({
|
|
2303
|
+
data: null,
|
|
2304
|
+
hit: false,
|
|
2305
|
+
stale: false,
|
|
2306
|
+
revalidating: false,
|
|
2307
|
+
})
|
|
2308
|
+
|
|
2309
|
+
const vfs = createR2PageVFS({
|
|
2310
|
+
bucket: mockBucket as unknown as R2Bucket,
|
|
2311
|
+
prefix: 'mydb',
|
|
2312
|
+
version: '1',
|
|
2313
|
+
swrCache: mockSwrCache,
|
|
2314
|
+
retryCount: 1,
|
|
2315
|
+
})
|
|
2316
|
+
|
|
2317
|
+
await expect(vfs.readPage(1, mockCtx)).rejects.toThrow('Invalid page size')
|
|
2318
|
+
})
|
|
2319
|
+
|
|
2320
|
+
it('should handle version change mid-operation', async () => {
|
|
2321
|
+
const mockBucket = createMockR2Bucket()
|
|
2322
|
+
const mockSwrCache = createMockSWRCache()
|
|
2323
|
+
const mockCtx = createMockExecutionContext()
|
|
2324
|
+
const pageData = createPageData()
|
|
2325
|
+
;(mockSwrCache.get as ReturnType<typeof vi.fn>).mockResolvedValue({
|
|
2326
|
+
data: pageData,
|
|
2327
|
+
hit: true,
|
|
2328
|
+
stale: false,
|
|
2329
|
+
revalidating: false,
|
|
2330
|
+
})
|
|
2331
|
+
|
|
2332
|
+
const vfs = createR2PageVFS({
|
|
2333
|
+
bucket: mockBucket as unknown as R2Bucket,
|
|
2334
|
+
prefix: 'mydb',
|
|
2335
|
+
version: '1',
|
|
2336
|
+
swrCache: mockSwrCache,
|
|
2337
|
+
})
|
|
2338
|
+
|
|
2339
|
+
// Start batch read
|
|
2340
|
+
const batchPromise = vfs.readPages([1, 2, 3, 4, 5], mockCtx)
|
|
2341
|
+
|
|
2342
|
+
// Version is fixed at VFS creation time, so mid-operation version changes
|
|
2343
|
+
// don't affect the current VFS instance
|
|
2344
|
+
const result = await batchPromise
|
|
2345
|
+
expect(result.pages.size).toBe(5)
|
|
2346
|
+
expect(vfs.getConfig().version).toBe('1')
|
|
2347
|
+
})
|
|
2348
|
+
})
|