@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,2441 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for TieredStorageOrchestrator
|
|
3
|
+
*
|
|
4
|
+
* Tests the unified tiered storage system:
|
|
5
|
+
* - HOT tier: Cloudflare Cache API
|
|
6
|
+
* - WARM tier: Durable Object SQLite blobs
|
|
7
|
+
* - COLD tier: R2 object storage
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
|
11
|
+
import {
|
|
12
|
+
TieredStorageOrchestrator,
|
|
13
|
+
createTieredStorageOrchestrator,
|
|
14
|
+
TieredOrchestratorConfig,
|
|
15
|
+
StorageTier,
|
|
16
|
+
TierMigrationEvent,
|
|
17
|
+
} from './tiered-orchestrator'
|
|
18
|
+
import type { CacheLayer } from './cache-layer'
|
|
19
|
+
import type { R2StorageLayer } from './r2-layer'
|
|
20
|
+
import type { SWRCacheLayer } from './swr-cache'
|
|
21
|
+
|
|
22
|
+
// Mock CacheLayer
|
|
23
|
+
const mockCacheLayer = {
|
|
24
|
+
get: vi.fn(),
|
|
25
|
+
put: vi.fn(),
|
|
26
|
+
delete: vi.fn(),
|
|
27
|
+
has: vi.fn(),
|
|
28
|
+
getWithMetadata: vi.fn(),
|
|
29
|
+
getStats: vi.fn().mockReturnValue({
|
|
30
|
+
hits: 0,
|
|
31
|
+
misses: 0,
|
|
32
|
+
writes: 0,
|
|
33
|
+
deletes: 0,
|
|
34
|
+
bytesRead: 0,
|
|
35
|
+
bytesWritten: 0,
|
|
36
|
+
errors: 0,
|
|
37
|
+
hitRatio: 0,
|
|
38
|
+
}),
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Mock SWRCacheLayer
|
|
42
|
+
const mockSWRCacheLayer = {
|
|
43
|
+
get: vi.fn().mockResolvedValue({ data: null, hit: false, stale: false, revalidating: false }),
|
|
44
|
+
put: vi.fn().mockResolvedValue(undefined),
|
|
45
|
+
invalidate: vi.fn().mockResolvedValue(undefined),
|
|
46
|
+
has: vi.fn().mockResolvedValue(false),
|
|
47
|
+
setOriginFetcher: vi.fn(),
|
|
48
|
+
getStats: vi.fn().mockReturnValue({
|
|
49
|
+
primaryHits: 0,
|
|
50
|
+
staleHits: 0,
|
|
51
|
+
misses: 0,
|
|
52
|
+
revalidations: 0,
|
|
53
|
+
writes: 0,
|
|
54
|
+
invalidations: 0,
|
|
55
|
+
bytesRead: 0,
|
|
56
|
+
bytesWritten: 0,
|
|
57
|
+
errors: 0,
|
|
58
|
+
primaryHitRatio: 0,
|
|
59
|
+
combinedHitRatio: 0,
|
|
60
|
+
}),
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Mock DOStorage
|
|
64
|
+
const mockDOStorage = {
|
|
65
|
+
get: vi.fn(),
|
|
66
|
+
put: vi.fn().mockResolvedValue(undefined),
|
|
67
|
+
delete: vi.fn().mockResolvedValue(undefined),
|
|
68
|
+
list: vi.fn(),
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Mock R2StorageLayer
|
|
72
|
+
const mockR2Layer = {
|
|
73
|
+
get: vi.fn(),
|
|
74
|
+
getRange: vi.fn(),
|
|
75
|
+
put: vi.fn().mockResolvedValue(undefined),
|
|
76
|
+
delete: vi.fn().mockResolvedValue(undefined),
|
|
77
|
+
deleteMany: vi.fn(),
|
|
78
|
+
head: vi.fn(),
|
|
79
|
+
has: vi.fn(),
|
|
80
|
+
list: vi.fn(),
|
|
81
|
+
getStats: vi.fn().mockReturnValue({
|
|
82
|
+
reads: 0,
|
|
83
|
+
rangeReads: 0,
|
|
84
|
+
writes: 0,
|
|
85
|
+
deletes: 0,
|
|
86
|
+
bytesRead: 0,
|
|
87
|
+
bytesWritten: 0,
|
|
88
|
+
errors: 0,
|
|
89
|
+
headRequests: 0,
|
|
90
|
+
listRequests: 0,
|
|
91
|
+
}),
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
describe('TieredStorageOrchestrator', () => {
|
|
95
|
+
beforeEach(() => {
|
|
96
|
+
vi.clearAllMocks()
|
|
97
|
+
mockSWRCacheLayer.get.mockResolvedValue({ data: null, hit: false, stale: false, revalidating: false })
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
describe('createTieredStorageOrchestrator()', () => {
|
|
101
|
+
it('should create an orchestrator instance', () => {
|
|
102
|
+
const orchestrator = createTieredStorageOrchestrator({
|
|
103
|
+
cacheLayer: mockCacheLayer as unknown as CacheLayer,
|
|
104
|
+
doStorage: mockDOStorage as unknown as DurableObjectStorage,
|
|
105
|
+
r2Layer: mockR2Layer as unknown as R2StorageLayer,
|
|
106
|
+
})
|
|
107
|
+
expect(orchestrator).toBeInstanceOf(TieredStorageOrchestrator)
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
it('should create an orchestrator with SWR cache layer', () => {
|
|
111
|
+
const orchestrator = createTieredStorageOrchestrator({
|
|
112
|
+
swrCacheLayer: mockSWRCacheLayer as unknown as SWRCacheLayer,
|
|
113
|
+
doStorage: mockDOStorage as unknown as DurableObjectStorage,
|
|
114
|
+
r2Layer: mockR2Layer as unknown as R2StorageLayer,
|
|
115
|
+
})
|
|
116
|
+
expect(orchestrator).toBeInstanceOf(TieredStorageOrchestrator)
|
|
117
|
+
})
|
|
118
|
+
|
|
119
|
+
it('should accept all configuration options', () => {
|
|
120
|
+
const config: TieredOrchestratorConfig = {
|
|
121
|
+
cacheLayer: mockCacheLayer as unknown as CacheLayer,
|
|
122
|
+
doStorage: mockDOStorage as unknown as DurableObjectStorage,
|
|
123
|
+
r2Layer: mockR2Layer as unknown as R2StorageLayer,
|
|
124
|
+
pageSize: 16384,
|
|
125
|
+
coldToWarmThreshold: 5,
|
|
126
|
+
warmToHotThreshold: 10,
|
|
127
|
+
promotionWindowMs: 120000,
|
|
128
|
+
hotTtlMs: 600000,
|
|
129
|
+
warmTtlMs: 7200000,
|
|
130
|
+
maxHotPages: 500,
|
|
131
|
+
autoPromote: true,
|
|
132
|
+
autoDemote: true,
|
|
133
|
+
writeThrough: true,
|
|
134
|
+
doPrefix: 'data:',
|
|
135
|
+
}
|
|
136
|
+
const orchestrator = createTieredStorageOrchestrator(config)
|
|
137
|
+
expect(orchestrator.getConfig().pageSize).toBe(16384)
|
|
138
|
+
expect(orchestrator.getConfig().coldToWarmThreshold).toBe(5)
|
|
139
|
+
expect(orchestrator.getConfig().writeThrough).toBe(true)
|
|
140
|
+
})
|
|
141
|
+
})
|
|
142
|
+
|
|
143
|
+
describe('read() - tiered lookup', () => {
|
|
144
|
+
it('should read from hot tier first (Cache)', async () => {
|
|
145
|
+
const data = new Uint8Array([1, 2, 3, 4, 5])
|
|
146
|
+
mockCacheLayer.get.mockResolvedValue(data)
|
|
147
|
+
|
|
148
|
+
const orchestrator = createTieredStorageOrchestrator({
|
|
149
|
+
cacheLayer: mockCacheLayer as unknown as CacheLayer,
|
|
150
|
+
doStorage: mockDOStorage as unknown as DurableObjectStorage,
|
|
151
|
+
r2Layer: mockR2Layer as unknown as R2StorageLayer,
|
|
152
|
+
})
|
|
153
|
+
|
|
154
|
+
const result = await orchestrator.read('page-123')
|
|
155
|
+
expect(result.data).toEqual(data)
|
|
156
|
+
expect(result.tier).toBe('hot')
|
|
157
|
+
expect(mockCacheLayer.get).toHaveBeenCalledWith('page-123')
|
|
158
|
+
expect(mockDOStorage.get).not.toHaveBeenCalled()
|
|
159
|
+
expect(mockR2Layer.get).not.toHaveBeenCalled()
|
|
160
|
+
})
|
|
161
|
+
|
|
162
|
+
it('should read from hot tier first (SWR Cache)', async () => {
|
|
163
|
+
const data = new Uint8Array([1, 2, 3, 4, 5])
|
|
164
|
+
mockSWRCacheLayer.get.mockResolvedValue({ data, hit: true, stale: false, revalidating: false })
|
|
165
|
+
|
|
166
|
+
const orchestrator = createTieredStorageOrchestrator({
|
|
167
|
+
swrCacheLayer: mockSWRCacheLayer as unknown as SWRCacheLayer,
|
|
168
|
+
doStorage: mockDOStorage as unknown as DurableObjectStorage,
|
|
169
|
+
r2Layer: mockR2Layer as unknown as R2StorageLayer,
|
|
170
|
+
})
|
|
171
|
+
|
|
172
|
+
const result = await orchestrator.read('page-123')
|
|
173
|
+
expect(result.data).toEqual(data)
|
|
174
|
+
expect(result.tier).toBe('hot')
|
|
175
|
+
expect(result.stale).toBe(false)
|
|
176
|
+
expect(mockDOStorage.get).not.toHaveBeenCalled()
|
|
177
|
+
})
|
|
178
|
+
|
|
179
|
+
it('should return stale data with revalidation flag from SWR', async () => {
|
|
180
|
+
const data = new Uint8Array([1, 2, 3])
|
|
181
|
+
mockSWRCacheLayer.get.mockResolvedValue({ data, hit: true, stale: true, revalidating: true })
|
|
182
|
+
|
|
183
|
+
const orchestrator = createTieredStorageOrchestrator({
|
|
184
|
+
swrCacheLayer: mockSWRCacheLayer as unknown as SWRCacheLayer,
|
|
185
|
+
doStorage: mockDOStorage as unknown as DurableObjectStorage,
|
|
186
|
+
r2Layer: mockR2Layer as unknown as R2StorageLayer,
|
|
187
|
+
})
|
|
188
|
+
|
|
189
|
+
const result = await orchestrator.read('page-123')
|
|
190
|
+
expect(result.data).toEqual(data)
|
|
191
|
+
expect(result.tier).toBe('hot')
|
|
192
|
+
expect(result.stale).toBe(true)
|
|
193
|
+
expect(result.revalidating).toBe(true)
|
|
194
|
+
})
|
|
195
|
+
|
|
196
|
+
it('should read from warm tier on hot miss', async () => {
|
|
197
|
+
const data = new Uint8Array([10, 20, 30])
|
|
198
|
+
mockCacheLayer.get.mockResolvedValue(null)
|
|
199
|
+
mockDOStorage.get.mockResolvedValue(data)
|
|
200
|
+
|
|
201
|
+
const orchestrator = createTieredStorageOrchestrator({
|
|
202
|
+
cacheLayer: mockCacheLayer as unknown as CacheLayer,
|
|
203
|
+
doStorage: mockDOStorage as unknown as DurableObjectStorage,
|
|
204
|
+
r2Layer: mockR2Layer as unknown as R2StorageLayer,
|
|
205
|
+
})
|
|
206
|
+
|
|
207
|
+
const result = await orchestrator.read('page-123')
|
|
208
|
+
expect(result.data).toEqual(data)
|
|
209
|
+
expect(result.tier).toBe('warm')
|
|
210
|
+
expect(mockCacheLayer.get).toHaveBeenCalled()
|
|
211
|
+
expect(mockDOStorage.get).toHaveBeenCalledWith('page:page-123')
|
|
212
|
+
})
|
|
213
|
+
|
|
214
|
+
it('should read from cold tier on warm miss', async () => {
|
|
215
|
+
const data = new Uint8Array([100, 200])
|
|
216
|
+
mockCacheLayer.get.mockResolvedValue(null)
|
|
217
|
+
mockDOStorage.get.mockResolvedValue(undefined)
|
|
218
|
+
mockR2Layer.get.mockResolvedValue(data)
|
|
219
|
+
|
|
220
|
+
const orchestrator = createTieredStorageOrchestrator({
|
|
221
|
+
cacheLayer: mockCacheLayer as unknown as CacheLayer,
|
|
222
|
+
doStorage: mockDOStorage as unknown as DurableObjectStorage,
|
|
223
|
+
r2Layer: mockR2Layer as unknown as R2StorageLayer,
|
|
224
|
+
})
|
|
225
|
+
|
|
226
|
+
const result = await orchestrator.read('page-123')
|
|
227
|
+
expect(result.data).toEqual(data)
|
|
228
|
+
expect(result.tier).toBe('cold')
|
|
229
|
+
expect(mockR2Layer.get).toHaveBeenCalledWith('page-123')
|
|
230
|
+
})
|
|
231
|
+
|
|
232
|
+
it('should return null when not found in any tier', async () => {
|
|
233
|
+
mockCacheLayer.get.mockResolvedValue(null)
|
|
234
|
+
mockDOStorage.get.mockResolvedValue(undefined)
|
|
235
|
+
mockR2Layer.get.mockResolvedValue(null)
|
|
236
|
+
|
|
237
|
+
const orchestrator = createTieredStorageOrchestrator({
|
|
238
|
+
cacheLayer: mockCacheLayer as unknown as CacheLayer,
|
|
239
|
+
doStorage: mockDOStorage as unknown as DurableObjectStorage,
|
|
240
|
+
r2Layer: mockR2Layer as unknown as R2StorageLayer,
|
|
241
|
+
})
|
|
242
|
+
|
|
243
|
+
const result = await orchestrator.read('nonexistent')
|
|
244
|
+
expect(result.data).toBeNull()
|
|
245
|
+
expect(result.tier).toBeUndefined()
|
|
246
|
+
})
|
|
247
|
+
|
|
248
|
+
it('should track access in storage index', async () => {
|
|
249
|
+
const data = new Uint8Array([1, 2, 3])
|
|
250
|
+
mockCacheLayer.get.mockResolvedValue(null)
|
|
251
|
+
mockDOStorage.get.mockResolvedValue(data)
|
|
252
|
+
|
|
253
|
+
const orchestrator = createTieredStorageOrchestrator({
|
|
254
|
+
cacheLayer: mockCacheLayer as unknown as CacheLayer,
|
|
255
|
+
doStorage: mockDOStorage as unknown as DurableObjectStorage,
|
|
256
|
+
r2Layer: mockR2Layer as unknown as R2StorageLayer,
|
|
257
|
+
})
|
|
258
|
+
|
|
259
|
+
await orchestrator.read('page-123')
|
|
260
|
+
const entry = orchestrator.getIndexEntry('page-123')
|
|
261
|
+
expect(entry).toBeDefined()
|
|
262
|
+
expect(entry?.tier).toBe('warm')
|
|
263
|
+
expect(entry?.accessCount).toBe(1)
|
|
264
|
+
|
|
265
|
+
await orchestrator.read('page-123')
|
|
266
|
+
const entry2 = orchestrator.getIndexEntry('page-123')
|
|
267
|
+
expect(entry2?.accessCount).toBe(2)
|
|
268
|
+
})
|
|
269
|
+
|
|
270
|
+
it('should skip tracking when skipTracking is true', async () => {
|
|
271
|
+
const data = new Uint8Array([1, 2, 3])
|
|
272
|
+
mockCacheLayer.get.mockResolvedValue(null)
|
|
273
|
+
mockDOStorage.get.mockResolvedValue(data)
|
|
274
|
+
|
|
275
|
+
const orchestrator = createTieredStorageOrchestrator({
|
|
276
|
+
cacheLayer: mockCacheLayer as unknown as CacheLayer,
|
|
277
|
+
doStorage: mockDOStorage as unknown as DurableObjectStorage,
|
|
278
|
+
r2Layer: mockR2Layer as unknown as R2StorageLayer,
|
|
279
|
+
})
|
|
280
|
+
|
|
281
|
+
await orchestrator.read('page-123', { skipTracking: true })
|
|
282
|
+
expect(orchestrator.getIndexEntry('page-123')).toBeUndefined()
|
|
283
|
+
})
|
|
284
|
+
})
|
|
285
|
+
|
|
286
|
+
describe('write()', () => {
|
|
287
|
+
it('should write to warm tier by default', async () => {
|
|
288
|
+
const data = new Uint8Array([1, 2, 3])
|
|
289
|
+
|
|
290
|
+
const orchestrator = createTieredStorageOrchestrator({
|
|
291
|
+
cacheLayer: mockCacheLayer as unknown as CacheLayer,
|
|
292
|
+
doStorage: mockDOStorage as unknown as DurableObjectStorage,
|
|
293
|
+
r2Layer: mockR2Layer as unknown as R2StorageLayer,
|
|
294
|
+
})
|
|
295
|
+
|
|
296
|
+
await orchestrator.write('page-123', data)
|
|
297
|
+
expect(mockDOStorage.put).toHaveBeenCalledWith('page:page-123', data)
|
|
298
|
+
expect(mockCacheLayer.put).not.toHaveBeenCalled()
|
|
299
|
+
expect(mockR2Layer.put).not.toHaveBeenCalled()
|
|
300
|
+
})
|
|
301
|
+
|
|
302
|
+
it('should write to hot tier when specified', async () => {
|
|
303
|
+
const data = new Uint8Array([1, 2, 3])
|
|
304
|
+
|
|
305
|
+
const orchestrator = createTieredStorageOrchestrator({
|
|
306
|
+
cacheLayer: mockCacheLayer as unknown as CacheLayer,
|
|
307
|
+
doStorage: mockDOStorage as unknown as DurableObjectStorage,
|
|
308
|
+
r2Layer: mockR2Layer as unknown as R2StorageLayer,
|
|
309
|
+
})
|
|
310
|
+
|
|
311
|
+
await orchestrator.write('page-123', data, { tier: 'hot' })
|
|
312
|
+
expect(mockDOStorage.put).toHaveBeenCalledWith('page:page-123', data)
|
|
313
|
+
expect(mockCacheLayer.put).toHaveBeenCalledWith('page-123', data, expect.any(Object))
|
|
314
|
+
})
|
|
315
|
+
|
|
316
|
+
it('should write to cold tier when specified', async () => {
|
|
317
|
+
const data = new Uint8Array([1, 2, 3])
|
|
318
|
+
|
|
319
|
+
const orchestrator = createTieredStorageOrchestrator({
|
|
320
|
+
cacheLayer: mockCacheLayer as unknown as CacheLayer,
|
|
321
|
+
doStorage: mockDOStorage as unknown as DurableObjectStorage,
|
|
322
|
+
r2Layer: mockR2Layer as unknown as R2StorageLayer,
|
|
323
|
+
})
|
|
324
|
+
|
|
325
|
+
await orchestrator.write('page-123', data, { tier: 'cold' })
|
|
326
|
+
expect(mockR2Layer.put).toHaveBeenCalledWith('page-123', data, expect.any(Object))
|
|
327
|
+
expect(mockDOStorage.put).not.toHaveBeenCalled()
|
|
328
|
+
})
|
|
329
|
+
|
|
330
|
+
it('should write-through to cold tier when enabled', async () => {
|
|
331
|
+
const data = new Uint8Array([1, 2, 3])
|
|
332
|
+
|
|
333
|
+
const orchestrator = createTieredStorageOrchestrator({
|
|
334
|
+
cacheLayer: mockCacheLayer as unknown as CacheLayer,
|
|
335
|
+
doStorage: mockDOStorage as unknown as DurableObjectStorage,
|
|
336
|
+
r2Layer: mockR2Layer as unknown as R2StorageLayer,
|
|
337
|
+
})
|
|
338
|
+
|
|
339
|
+
await orchestrator.write('page-123', data, { tier: 'warm', writeThrough: true })
|
|
340
|
+
expect(mockDOStorage.put).toHaveBeenCalled()
|
|
341
|
+
expect(mockR2Layer.put).toHaveBeenCalled()
|
|
342
|
+
})
|
|
343
|
+
|
|
344
|
+
it('should update storage index on write', async () => {
|
|
345
|
+
const data = new Uint8Array([1, 2, 3])
|
|
346
|
+
|
|
347
|
+
const orchestrator = createTieredStorageOrchestrator({
|
|
348
|
+
cacheLayer: mockCacheLayer as unknown as CacheLayer,
|
|
349
|
+
doStorage: mockDOStorage as unknown as DurableObjectStorage,
|
|
350
|
+
r2Layer: mockR2Layer as unknown as R2StorageLayer,
|
|
351
|
+
})
|
|
352
|
+
|
|
353
|
+
await orchestrator.write('page-123', data)
|
|
354
|
+
const entry = orchestrator.getIndexEntry('page-123')
|
|
355
|
+
expect(entry).toBeDefined()
|
|
356
|
+
expect(entry?.tier).toBe('warm')
|
|
357
|
+
expect(entry?.size).toBe(3)
|
|
358
|
+
})
|
|
359
|
+
})
|
|
360
|
+
|
|
361
|
+
describe('delete()', () => {
|
|
362
|
+
it('should delete from all tiers', async () => {
|
|
363
|
+
mockCacheLayer.delete.mockResolvedValue(true)
|
|
364
|
+
|
|
365
|
+
const orchestrator = createTieredStorageOrchestrator({
|
|
366
|
+
cacheLayer: mockCacheLayer as unknown as CacheLayer,
|
|
367
|
+
doStorage: mockDOStorage as unknown as DurableObjectStorage,
|
|
368
|
+
r2Layer: mockR2Layer as unknown as R2StorageLayer,
|
|
369
|
+
})
|
|
370
|
+
|
|
371
|
+
// First write to create index entry
|
|
372
|
+
await orchestrator.write('page-123', new Uint8Array([1, 2, 3]))
|
|
373
|
+
expect(orchestrator.hasInIndex('page-123')).toBe(true)
|
|
374
|
+
|
|
375
|
+
// Then delete
|
|
376
|
+
await orchestrator.delete('page-123')
|
|
377
|
+
expect(mockCacheLayer.delete).toHaveBeenCalledWith('page-123')
|
|
378
|
+
expect(mockDOStorage.delete).toHaveBeenCalledWith('page:page-123')
|
|
379
|
+
expect(mockR2Layer.delete).toHaveBeenCalledWith('page-123')
|
|
380
|
+
expect(orchestrator.hasInIndex('page-123')).toBe(false)
|
|
381
|
+
})
|
|
382
|
+
})
|
|
383
|
+
|
|
384
|
+
describe('promoteToWarm()', () => {
|
|
385
|
+
it('should promote data from cold to warm tier', async () => {
|
|
386
|
+
const data = new Uint8Array([1, 2, 3])
|
|
387
|
+
mockR2Layer.get.mockResolvedValue(data)
|
|
388
|
+
|
|
389
|
+
const orchestrator = createTieredStorageOrchestrator({
|
|
390
|
+
cacheLayer: mockCacheLayer as unknown as CacheLayer,
|
|
391
|
+
doStorage: mockDOStorage as unknown as DurableObjectStorage,
|
|
392
|
+
r2Layer: mockR2Layer as unknown as R2StorageLayer,
|
|
393
|
+
})
|
|
394
|
+
|
|
395
|
+
const event = await orchestrator.promoteToWarm('page-123')
|
|
396
|
+
expect(event.success).toBe(true)
|
|
397
|
+
expect(event.fromTier).toBe('cold')
|
|
398
|
+
expect(event.toTier).toBe('warm')
|
|
399
|
+
expect(mockDOStorage.put).toHaveBeenCalledWith('page:page-123', data)
|
|
400
|
+
})
|
|
401
|
+
|
|
402
|
+
it('should use provided data instead of fetching', async () => {
|
|
403
|
+
const data = new Uint8Array([1, 2, 3])
|
|
404
|
+
|
|
405
|
+
const orchestrator = createTieredStorageOrchestrator({
|
|
406
|
+
cacheLayer: mockCacheLayer as unknown as CacheLayer,
|
|
407
|
+
doStorage: mockDOStorage as unknown as DurableObjectStorage,
|
|
408
|
+
r2Layer: mockR2Layer as unknown as R2StorageLayer,
|
|
409
|
+
})
|
|
410
|
+
|
|
411
|
+
const event = await orchestrator.promoteToWarm('page-123', data)
|
|
412
|
+
expect(event.success).toBe(true)
|
|
413
|
+
expect(mockR2Layer.get).not.toHaveBeenCalled()
|
|
414
|
+
expect(mockDOStorage.put).toHaveBeenCalledWith('page:page-123', data)
|
|
415
|
+
})
|
|
416
|
+
|
|
417
|
+
it('should fail if data not found in cold tier', async () => {
|
|
418
|
+
mockR2Layer.get.mockResolvedValue(null)
|
|
419
|
+
|
|
420
|
+
const orchestrator = createTieredStorageOrchestrator({
|
|
421
|
+
cacheLayer: mockCacheLayer as unknown as CacheLayer,
|
|
422
|
+
doStorage: mockDOStorage as unknown as DurableObjectStorage,
|
|
423
|
+
r2Layer: mockR2Layer as unknown as R2StorageLayer,
|
|
424
|
+
})
|
|
425
|
+
|
|
426
|
+
const event = await orchestrator.promoteToWarm('nonexistent')
|
|
427
|
+
expect(event.success).toBe(false)
|
|
428
|
+
expect(event.error).toContain('not found')
|
|
429
|
+
})
|
|
430
|
+
|
|
431
|
+
it('should notify migration callbacks', async () => {
|
|
432
|
+
const data = new Uint8Array([1, 2, 3])
|
|
433
|
+
mockR2Layer.get.mockResolvedValue(data)
|
|
434
|
+
const callback = vi.fn()
|
|
435
|
+
|
|
436
|
+
const orchestrator = createTieredStorageOrchestrator({
|
|
437
|
+
cacheLayer: mockCacheLayer as unknown as CacheLayer,
|
|
438
|
+
doStorage: mockDOStorage as unknown as DurableObjectStorage,
|
|
439
|
+
r2Layer: mockR2Layer as unknown as R2StorageLayer,
|
|
440
|
+
})
|
|
441
|
+
orchestrator.onMigration(callback)
|
|
442
|
+
|
|
443
|
+
await orchestrator.promoteToWarm('page-123')
|
|
444
|
+
expect(callback).toHaveBeenCalledWith(expect.objectContaining({
|
|
445
|
+
key: 'page-123',
|
|
446
|
+
fromTier: 'cold',
|
|
447
|
+
toTier: 'warm',
|
|
448
|
+
success: true,
|
|
449
|
+
}))
|
|
450
|
+
})
|
|
451
|
+
})
|
|
452
|
+
|
|
453
|
+
describe('promoteToHot()', () => {
|
|
454
|
+
it('should promote data from warm to hot tier', async () => {
|
|
455
|
+
const data = new Uint8Array([1, 2, 3])
|
|
456
|
+
mockDOStorage.get.mockResolvedValue(data)
|
|
457
|
+
|
|
458
|
+
const orchestrator = createTieredStorageOrchestrator({
|
|
459
|
+
cacheLayer: mockCacheLayer as unknown as CacheLayer,
|
|
460
|
+
doStorage: mockDOStorage as unknown as DurableObjectStorage,
|
|
461
|
+
r2Layer: mockR2Layer as unknown as R2StorageLayer,
|
|
462
|
+
})
|
|
463
|
+
|
|
464
|
+
const event = await orchestrator.promoteToHot('page-123')
|
|
465
|
+
expect(event.success).toBe(true)
|
|
466
|
+
expect(event.fromTier).toBe('warm')
|
|
467
|
+
expect(event.toTier).toBe('hot')
|
|
468
|
+
expect(mockCacheLayer.put).toHaveBeenCalledWith('page-123', data, expect.any(Object))
|
|
469
|
+
})
|
|
470
|
+
|
|
471
|
+
it('should promote to SWR cache when configured', async () => {
|
|
472
|
+
const data = new Uint8Array([1, 2, 3])
|
|
473
|
+
mockDOStorage.get.mockResolvedValue(data)
|
|
474
|
+
|
|
475
|
+
const orchestrator = createTieredStorageOrchestrator({
|
|
476
|
+
swrCacheLayer: mockSWRCacheLayer as unknown as SWRCacheLayer,
|
|
477
|
+
doStorage: mockDOStorage as unknown as DurableObjectStorage,
|
|
478
|
+
r2Layer: mockR2Layer as unknown as R2StorageLayer,
|
|
479
|
+
})
|
|
480
|
+
|
|
481
|
+
const event = await orchestrator.promoteToHot('page-123')
|
|
482
|
+
expect(event.success).toBe(true)
|
|
483
|
+
expect(mockSWRCacheLayer.put).toHaveBeenCalledWith('page-123', data)
|
|
484
|
+
})
|
|
485
|
+
})
|
|
486
|
+
|
|
487
|
+
describe('demoteFromHot()', () => {
|
|
488
|
+
it('should demote data from hot to warm tier', async () => {
|
|
489
|
+
mockCacheLayer.delete.mockResolvedValue(true)
|
|
490
|
+
|
|
491
|
+
const orchestrator = createTieredStorageOrchestrator({
|
|
492
|
+
cacheLayer: mockCacheLayer as unknown as CacheLayer,
|
|
493
|
+
doStorage: mockDOStorage as unknown as DurableObjectStorage,
|
|
494
|
+
r2Layer: mockR2Layer as unknown as R2StorageLayer,
|
|
495
|
+
})
|
|
496
|
+
|
|
497
|
+
// First add entry to index
|
|
498
|
+
await orchestrator.write('page-123', new Uint8Array([1, 2, 3]), { tier: 'hot' })
|
|
499
|
+
|
|
500
|
+
const event = await orchestrator.demoteFromHot('page-123')
|
|
501
|
+
expect(event.success).toBe(true)
|
|
502
|
+
expect(event.fromTier).toBe('hot')
|
|
503
|
+
expect(event.toTier).toBe('warm')
|
|
504
|
+
expect(mockCacheLayer.delete).toHaveBeenCalledWith('page-123')
|
|
505
|
+
})
|
|
506
|
+
|
|
507
|
+
it('should invalidate SWR cache when configured', async () => {
|
|
508
|
+
const orchestrator = createTieredStorageOrchestrator({
|
|
509
|
+
swrCacheLayer: mockSWRCacheLayer as unknown as SWRCacheLayer,
|
|
510
|
+
doStorage: mockDOStorage as unknown as DurableObjectStorage,
|
|
511
|
+
r2Layer: mockR2Layer as unknown as R2StorageLayer,
|
|
512
|
+
})
|
|
513
|
+
|
|
514
|
+
const event = await orchestrator.demoteFromHot('page-123')
|
|
515
|
+
expect(event.success).toBe(true)
|
|
516
|
+
expect(mockSWRCacheLayer.invalidate).toHaveBeenCalledWith('page-123')
|
|
517
|
+
})
|
|
518
|
+
})
|
|
519
|
+
|
|
520
|
+
describe('demoteFromWarm()', () => {
|
|
521
|
+
it('should demote data from warm to cold tier', async () => {
|
|
522
|
+
const data = new Uint8Array([1, 2, 3])
|
|
523
|
+
mockDOStorage.get.mockResolvedValue(data)
|
|
524
|
+
|
|
525
|
+
const orchestrator = createTieredStorageOrchestrator({
|
|
526
|
+
cacheLayer: mockCacheLayer as unknown as CacheLayer,
|
|
527
|
+
doStorage: mockDOStorage as unknown as DurableObjectStorage,
|
|
528
|
+
r2Layer: mockR2Layer as unknown as R2StorageLayer,
|
|
529
|
+
})
|
|
530
|
+
|
|
531
|
+
const event = await orchestrator.demoteFromWarm('page-123')
|
|
532
|
+
expect(event.success).toBe(true)
|
|
533
|
+
expect(event.fromTier).toBe('warm')
|
|
534
|
+
expect(event.toTier).toBe('cold')
|
|
535
|
+
expect(mockR2Layer.put).toHaveBeenCalledWith('page-123', data, expect.any(Object))
|
|
536
|
+
expect(mockDOStorage.delete).toHaveBeenCalledWith('page:page-123')
|
|
537
|
+
})
|
|
538
|
+
|
|
539
|
+
it('should fail if data not found in warm tier', async () => {
|
|
540
|
+
mockDOStorage.get.mockResolvedValue(undefined)
|
|
541
|
+
|
|
542
|
+
const orchestrator = createTieredStorageOrchestrator({
|
|
543
|
+
cacheLayer: mockCacheLayer as unknown as CacheLayer,
|
|
544
|
+
doStorage: mockDOStorage as unknown as DurableObjectStorage,
|
|
545
|
+
r2Layer: mockR2Layer as unknown as R2StorageLayer,
|
|
546
|
+
})
|
|
547
|
+
|
|
548
|
+
const event = await orchestrator.demoteFromWarm('nonexistent')
|
|
549
|
+
expect(event.success).toBe(false)
|
|
550
|
+
expect(event.error).toContain('not found')
|
|
551
|
+
})
|
|
552
|
+
})
|
|
553
|
+
|
|
554
|
+
describe('runDemotionCycle()', () => {
|
|
555
|
+
it('should demote expired hot tier entries', async () => {
|
|
556
|
+
mockCacheLayer.get.mockResolvedValue(null)
|
|
557
|
+
mockDOStorage.get.mockResolvedValue(new Uint8Array([1, 2, 3]))
|
|
558
|
+
mockCacheLayer.delete.mockResolvedValue(true)
|
|
559
|
+
|
|
560
|
+
const orchestrator = createTieredStorageOrchestrator({
|
|
561
|
+
cacheLayer: mockCacheLayer as unknown as CacheLayer,
|
|
562
|
+
doStorage: mockDOStorage as unknown as DurableObjectStorage,
|
|
563
|
+
r2Layer: mockR2Layer as unknown as R2StorageLayer,
|
|
564
|
+
hotTtlMs: 0, // Immediate expiry for testing
|
|
565
|
+
autoDemote: true,
|
|
566
|
+
})
|
|
567
|
+
|
|
568
|
+
// Add a hot tier entry
|
|
569
|
+
await orchestrator.write('page-123', new Uint8Array([1, 2, 3]), { tier: 'hot' })
|
|
570
|
+
|
|
571
|
+
// Run demotion cycle
|
|
572
|
+
const events = await orchestrator.runDemotionCycle()
|
|
573
|
+
expect(events.length).toBeGreaterThan(0)
|
|
574
|
+
expect(events[0].fromTier).toBe('hot')
|
|
575
|
+
expect(events[0].toTier).toBe('warm')
|
|
576
|
+
})
|
|
577
|
+
|
|
578
|
+
it('should not demote when autoDemote is false', async () => {
|
|
579
|
+
const orchestrator = createTieredStorageOrchestrator({
|
|
580
|
+
cacheLayer: mockCacheLayer as unknown as CacheLayer,
|
|
581
|
+
doStorage: mockDOStorage as unknown as DurableObjectStorage,
|
|
582
|
+
r2Layer: mockR2Layer as unknown as R2StorageLayer,
|
|
583
|
+
autoDemote: false,
|
|
584
|
+
})
|
|
585
|
+
|
|
586
|
+
const events = await orchestrator.runDemotionCycle()
|
|
587
|
+
expect(events).toHaveLength(0)
|
|
588
|
+
})
|
|
589
|
+
})
|
|
590
|
+
|
|
591
|
+
describe('evictLRUFromHot()', () => {
|
|
592
|
+
it('should evict least recently used entries from hot tier', async () => {
|
|
593
|
+
mockCacheLayer.delete.mockResolvedValue(true)
|
|
594
|
+
|
|
595
|
+
const orchestrator = createTieredStorageOrchestrator({
|
|
596
|
+
cacheLayer: mockCacheLayer as unknown as CacheLayer,
|
|
597
|
+
doStorage: mockDOStorage as unknown as DurableObjectStorage,
|
|
598
|
+
r2Layer: mockR2Layer as unknown as R2StorageLayer,
|
|
599
|
+
})
|
|
600
|
+
|
|
601
|
+
// Add multiple hot tier entries
|
|
602
|
+
await orchestrator.write('page-1', new Uint8Array([1]), { tier: 'hot' })
|
|
603
|
+
await orchestrator.write('page-2', new Uint8Array([2]), { tier: 'hot' })
|
|
604
|
+
await orchestrator.write('page-3', new Uint8Array([3]), { tier: 'hot' })
|
|
605
|
+
|
|
606
|
+
const events = await orchestrator.evictLRUFromHot(2)
|
|
607
|
+
expect(events.length).toBe(2)
|
|
608
|
+
expect(events[0].reason).toBe('lru_eviction')
|
|
609
|
+
})
|
|
610
|
+
})
|
|
611
|
+
|
|
612
|
+
describe('syncDirtyToCold()', () => {
|
|
613
|
+
it('should sync dirty entries to cold tier', async () => {
|
|
614
|
+
const data = new Uint8Array([1, 2, 3])
|
|
615
|
+
mockDOStorage.get.mockResolvedValue(data)
|
|
616
|
+
|
|
617
|
+
const orchestrator = createTieredStorageOrchestrator({
|
|
618
|
+
cacheLayer: mockCacheLayer as unknown as CacheLayer,
|
|
619
|
+
doStorage: mockDOStorage as unknown as DurableObjectStorage,
|
|
620
|
+
r2Layer: mockR2Layer as unknown as R2StorageLayer,
|
|
621
|
+
})
|
|
622
|
+
|
|
623
|
+
// Write without write-through (creates dirty entry)
|
|
624
|
+
await orchestrator.write('page-123', data, { tier: 'warm' })
|
|
625
|
+
expect(orchestrator.getIndexEntry('page-123')?.dirty).toBe(true)
|
|
626
|
+
|
|
627
|
+
// Sync dirty entries
|
|
628
|
+
const synced = await orchestrator.syncDirtyToCold()
|
|
629
|
+
expect(synced).toBe(1)
|
|
630
|
+
expect(mockR2Layer.put).toHaveBeenCalled()
|
|
631
|
+
expect(orchestrator.getIndexEntry('page-123')?.dirty).toBe(false)
|
|
632
|
+
})
|
|
633
|
+
})
|
|
634
|
+
|
|
635
|
+
describe('getStats()', () => {
|
|
636
|
+
it('should return comprehensive statistics', async () => {
|
|
637
|
+
const orchestrator = createTieredStorageOrchestrator({
|
|
638
|
+
cacheLayer: mockCacheLayer as unknown as CacheLayer,
|
|
639
|
+
doStorage: mockDOStorage as unknown as DurableObjectStorage,
|
|
640
|
+
r2Layer: mockR2Layer as unknown as R2StorageLayer,
|
|
641
|
+
})
|
|
642
|
+
|
|
643
|
+
// Generate some activity
|
|
644
|
+
await orchestrator.write('page-1', new Uint8Array([1, 2, 3]))
|
|
645
|
+
await orchestrator.write('page-2', new Uint8Array([4, 5, 6]))
|
|
646
|
+
|
|
647
|
+
const stats = orchestrator.getStats()
|
|
648
|
+
expect(stats.index.totalEntries).toBe(2)
|
|
649
|
+
expect(stats.index.byTier.warm).toBe(2)
|
|
650
|
+
expect(stats.warm.writes).toBe(2)
|
|
651
|
+
expect(stats.health.hot).toBe('healthy')
|
|
652
|
+
expect(stats.health.warm).toBe('healthy')
|
|
653
|
+
expect(stats.health.cold).toBe('healthy')
|
|
654
|
+
})
|
|
655
|
+
})
|
|
656
|
+
|
|
657
|
+
describe('storage index', () => {
|
|
658
|
+
it('should track tier for keys', async () => {
|
|
659
|
+
const orchestrator = createTieredStorageOrchestrator({
|
|
660
|
+
cacheLayer: mockCacheLayer as unknown as CacheLayer,
|
|
661
|
+
doStorage: mockDOStorage as unknown as DurableObjectStorage,
|
|
662
|
+
r2Layer: mockR2Layer as unknown as R2StorageLayer,
|
|
663
|
+
})
|
|
664
|
+
|
|
665
|
+
await orchestrator.write('page-warm', new Uint8Array([1]), { tier: 'warm' })
|
|
666
|
+
await orchestrator.write('page-cold', new Uint8Array([2]), { tier: 'cold' })
|
|
667
|
+
await orchestrator.write('page-hot', new Uint8Array([3]), { tier: 'hot' })
|
|
668
|
+
|
|
669
|
+
expect(orchestrator.getTier('page-warm')).toBe('warm')
|
|
670
|
+
expect(orchestrator.getTier('page-cold')).toBe('cold')
|
|
671
|
+
expect(orchestrator.getTier('page-hot')).toBe('hot')
|
|
672
|
+
})
|
|
673
|
+
|
|
674
|
+
it('should clear index', async () => {
|
|
675
|
+
const orchestrator = createTieredStorageOrchestrator({
|
|
676
|
+
cacheLayer: mockCacheLayer as unknown as CacheLayer,
|
|
677
|
+
doStorage: mockDOStorage as unknown as DurableObjectStorage,
|
|
678
|
+
r2Layer: mockR2Layer as unknown as R2StorageLayer,
|
|
679
|
+
})
|
|
680
|
+
|
|
681
|
+
await orchestrator.write('page-1', new Uint8Array([1]))
|
|
682
|
+
expect(orchestrator.hasInIndex('page-1')).toBe(true)
|
|
683
|
+
|
|
684
|
+
orchestrator.clearIndex()
|
|
685
|
+
expect(orchestrator.hasInIndex('page-1')).toBe(false)
|
|
686
|
+
})
|
|
687
|
+
})
|
|
688
|
+
|
|
689
|
+
describe('resetStats()', () => {
|
|
690
|
+
it('should reset all statistics', async () => {
|
|
691
|
+
const orchestrator = createTieredStorageOrchestrator({
|
|
692
|
+
cacheLayer: mockCacheLayer as unknown as CacheLayer,
|
|
693
|
+
doStorage: mockDOStorage as unknown as DurableObjectStorage,
|
|
694
|
+
r2Layer: mockR2Layer as unknown as R2StorageLayer,
|
|
695
|
+
})
|
|
696
|
+
|
|
697
|
+
// Generate some activity
|
|
698
|
+
await orchestrator.write('page-1', new Uint8Array([1, 2, 3]))
|
|
699
|
+
|
|
700
|
+
let stats = orchestrator.getStats()
|
|
701
|
+
expect(stats.warm.writes).toBe(1)
|
|
702
|
+
|
|
703
|
+
orchestrator.resetStats()
|
|
704
|
+
stats = orchestrator.getStats()
|
|
705
|
+
expect(stats.warm.writes).toBe(0)
|
|
706
|
+
})
|
|
707
|
+
})
|
|
708
|
+
|
|
709
|
+
describe('resetHealth()', () => {
|
|
710
|
+
it('should reset tier health', () => {
|
|
711
|
+
const orchestrator = createTieredStorageOrchestrator({
|
|
712
|
+
cacheLayer: mockCacheLayer as unknown as CacheLayer,
|
|
713
|
+
doStorage: mockDOStorage as unknown as DurableObjectStorage,
|
|
714
|
+
r2Layer: mockR2Layer as unknown as R2StorageLayer,
|
|
715
|
+
})
|
|
716
|
+
|
|
717
|
+
orchestrator.resetHealth('hot')
|
|
718
|
+
expect(orchestrator.getStats().health.hot).toBe('healthy')
|
|
719
|
+
})
|
|
720
|
+
|
|
721
|
+
it('should reset all tier health', () => {
|
|
722
|
+
const orchestrator = createTieredStorageOrchestrator({
|
|
723
|
+
cacheLayer: mockCacheLayer as unknown as CacheLayer,
|
|
724
|
+
doStorage: mockDOStorage as unknown as DurableObjectStorage,
|
|
725
|
+
r2Layer: mockR2Layer as unknown as R2StorageLayer,
|
|
726
|
+
})
|
|
727
|
+
|
|
728
|
+
orchestrator.resetHealth()
|
|
729
|
+
const stats = orchestrator.getStats()
|
|
730
|
+
expect(stats.health.hot).toBe('healthy')
|
|
731
|
+
expect(stats.health.warm).toBe('healthy')
|
|
732
|
+
expect(stats.health.cold).toBe('healthy')
|
|
733
|
+
})
|
|
734
|
+
})
|
|
735
|
+
|
|
736
|
+
describe('auto-promotion', () => {
|
|
737
|
+
it('should auto-promote from cold to warm on threshold', async () => {
|
|
738
|
+
const data = new Uint8Array([1, 2, 3])
|
|
739
|
+
mockCacheLayer.get.mockResolvedValue(null)
|
|
740
|
+
mockDOStorage.get.mockResolvedValue(undefined)
|
|
741
|
+
mockR2Layer.get.mockResolvedValue(data)
|
|
742
|
+
|
|
743
|
+
const orchestrator = createTieredStorageOrchestrator({
|
|
744
|
+
cacheLayer: mockCacheLayer as unknown as CacheLayer,
|
|
745
|
+
doStorage: mockDOStorage as unknown as DurableObjectStorage,
|
|
746
|
+
r2Layer: mockR2Layer as unknown as R2StorageLayer,
|
|
747
|
+
coldToWarmThreshold: 2,
|
|
748
|
+
autoPromote: true,
|
|
749
|
+
})
|
|
750
|
+
|
|
751
|
+
// First read
|
|
752
|
+
await orchestrator.read('page-123')
|
|
753
|
+
expect(mockDOStorage.put).not.toHaveBeenCalled()
|
|
754
|
+
|
|
755
|
+
// Second read - should trigger promotion
|
|
756
|
+
await orchestrator.read('page-123')
|
|
757
|
+
// Promotion happens in background, so we need to wait a tick
|
|
758
|
+
await new Promise(resolve => setTimeout(resolve, 0))
|
|
759
|
+
expect(mockDOStorage.put).toHaveBeenCalled()
|
|
760
|
+
})
|
|
761
|
+
|
|
762
|
+
it('should skip auto-promotion when disabled', async () => {
|
|
763
|
+
const data = new Uint8Array([1, 2, 3])
|
|
764
|
+
mockCacheLayer.get.mockResolvedValue(null)
|
|
765
|
+
mockDOStorage.get.mockResolvedValue(undefined)
|
|
766
|
+
mockR2Layer.get.mockResolvedValue(data)
|
|
767
|
+
|
|
768
|
+
const orchestrator = createTieredStorageOrchestrator({
|
|
769
|
+
cacheLayer: mockCacheLayer as unknown as CacheLayer,
|
|
770
|
+
doStorage: mockDOStorage as unknown as DurableObjectStorage,
|
|
771
|
+
r2Layer: mockR2Layer as unknown as R2StorageLayer,
|
|
772
|
+
coldToWarmThreshold: 1,
|
|
773
|
+
autoPromote: false,
|
|
774
|
+
})
|
|
775
|
+
|
|
776
|
+
await orchestrator.read('page-123')
|
|
777
|
+
expect(mockDOStorage.put).not.toHaveBeenCalled()
|
|
778
|
+
})
|
|
779
|
+
|
|
780
|
+
it('should skip promotion when skipPromotion is true', async () => {
|
|
781
|
+
const data = new Uint8Array([1, 2, 3])
|
|
782
|
+
mockCacheLayer.get.mockResolvedValue(null)
|
|
783
|
+
mockDOStorage.get.mockResolvedValue(undefined)
|
|
784
|
+
mockR2Layer.get.mockResolvedValue(data)
|
|
785
|
+
|
|
786
|
+
const orchestrator = createTieredStorageOrchestrator({
|
|
787
|
+
cacheLayer: mockCacheLayer as unknown as CacheLayer,
|
|
788
|
+
doStorage: mockDOStorage as unknown as DurableObjectStorage,
|
|
789
|
+
r2Layer: mockR2Layer as unknown as R2StorageLayer,
|
|
790
|
+
coldToWarmThreshold: 1,
|
|
791
|
+
autoPromote: true,
|
|
792
|
+
})
|
|
793
|
+
|
|
794
|
+
await orchestrator.read('page-123', { skipPromotion: true })
|
|
795
|
+
expect(mockDOStorage.put).not.toHaveBeenCalled()
|
|
796
|
+
})
|
|
797
|
+
})
|
|
798
|
+
})
|
|
799
|
+
|
|
800
|
+
/**
|
|
801
|
+
* ========================================================================
|
|
802
|
+
* RED TESTS: Unified Tiering Orchestrator Features
|
|
803
|
+
* Issue: postgres-h91k [TIER-RED]
|
|
804
|
+
*
|
|
805
|
+
* These tests define the expected behavior for features that don't exist yet:
|
|
806
|
+
* 1. DO lifecycle hooks (persist hot on hibernate, restore on wake)
|
|
807
|
+
* 2. Background demotion cycles via DO alarm
|
|
808
|
+
* 3. Circuit breaker for tier failures
|
|
809
|
+
* 4. Integrated metrics aggregation
|
|
810
|
+
* 5. Advanced promotion/demotion strategies
|
|
811
|
+
* 6. Concurrent operation handling
|
|
812
|
+
* ========================================================================
|
|
813
|
+
*/
|
|
814
|
+
describe('TieredStorageOrchestrator - Unified Tiering Features [RED]', () => {
|
|
815
|
+
beforeEach(() => {
|
|
816
|
+
vi.clearAllMocks()
|
|
817
|
+
mockSWRCacheLayer.get.mockResolvedValue({ data: null, hit: false, stale: false, revalidating: false })
|
|
818
|
+
})
|
|
819
|
+
|
|
820
|
+
describe('DO Lifecycle Hooks', () => {
|
|
821
|
+
describe('onHibernate()', () => {
|
|
822
|
+
it('should persist all hot tier entries to warm tier on hibernate', async () => {
|
|
823
|
+
const data1 = new Uint8Array([1, 2, 3])
|
|
824
|
+
const data2 = new Uint8Array([4, 5, 6])
|
|
825
|
+
|
|
826
|
+
// Setup: SWR cache has data
|
|
827
|
+
mockSWRCacheLayer.get
|
|
828
|
+
.mockImplementation(async (key: string) => {
|
|
829
|
+
if (key === 'page-1') return { data: data1, hit: true, stale: false, revalidating: false }
|
|
830
|
+
if (key === 'page-2') return { data: data2, hit: true, stale: false, revalidating: false }
|
|
831
|
+
return { data: null, hit: false, stale: false, revalidating: false }
|
|
832
|
+
})
|
|
833
|
+
|
|
834
|
+
const orchestrator = createTieredStorageOrchestrator({
|
|
835
|
+
swrCacheLayer: mockSWRCacheLayer as unknown as SWRCacheLayer,
|
|
836
|
+
doStorage: mockDOStorage as unknown as DurableObjectStorage,
|
|
837
|
+
r2Layer: mockR2Layer as unknown as R2StorageLayer,
|
|
838
|
+
})
|
|
839
|
+
|
|
840
|
+
// Add entries to hot tier tracking
|
|
841
|
+
await orchestrator.write('page-1', data1, { tier: 'hot' })
|
|
842
|
+
await orchestrator.write('page-2', data2, { tier: 'hot' })
|
|
843
|
+
|
|
844
|
+
// This method should exist and persist hot entries
|
|
845
|
+
// @ts-expect-error - Method doesn't exist yet (RED test)
|
|
846
|
+
const result = await orchestrator.onHibernate()
|
|
847
|
+
|
|
848
|
+
expect(result).toBeDefined()
|
|
849
|
+
expect(result.persistedCount).toBe(2)
|
|
850
|
+
expect(result.success).toBe(true)
|
|
851
|
+
})
|
|
852
|
+
|
|
853
|
+
it('should sync all dirty entries to cold tier on hibernate', async () => {
|
|
854
|
+
const data = new Uint8Array([1, 2, 3])
|
|
855
|
+
mockDOStorage.get.mockResolvedValue(data)
|
|
856
|
+
|
|
857
|
+
const orchestrator = createTieredStorageOrchestrator({
|
|
858
|
+
cacheLayer: mockCacheLayer as unknown as CacheLayer,
|
|
859
|
+
doStorage: mockDOStorage as unknown as DurableObjectStorage,
|
|
860
|
+
r2Layer: mockR2Layer as unknown as R2StorageLayer,
|
|
861
|
+
})
|
|
862
|
+
|
|
863
|
+
// Write without write-through (creates dirty entry)
|
|
864
|
+
await orchestrator.write('page-1', data, { tier: 'warm' })
|
|
865
|
+
|
|
866
|
+
// @ts-expect-error - Method doesn't exist yet (RED test)
|
|
867
|
+
const result = await orchestrator.onHibernate()
|
|
868
|
+
|
|
869
|
+
expect(result.syncedToCold).toBeGreaterThan(0)
|
|
870
|
+
expect(mockR2Layer.put).toHaveBeenCalled()
|
|
871
|
+
})
|
|
872
|
+
|
|
873
|
+
it('should return hibernate state for later restoration', async () => {
|
|
874
|
+
const data = new Uint8Array([1, 2, 3])
|
|
875
|
+
|
|
876
|
+
const orchestrator = createTieredStorageOrchestrator({
|
|
877
|
+
cacheLayer: mockCacheLayer as unknown as CacheLayer,
|
|
878
|
+
doStorage: mockDOStorage as unknown as DurableObjectStorage,
|
|
879
|
+
r2Layer: mockR2Layer as unknown as R2StorageLayer,
|
|
880
|
+
})
|
|
881
|
+
|
|
882
|
+
await orchestrator.write('page-1', data, { tier: 'warm' })
|
|
883
|
+
|
|
884
|
+
// @ts-expect-error - Method doesn't exist yet (RED test)
|
|
885
|
+
const state = await orchestrator.onHibernate()
|
|
886
|
+
|
|
887
|
+
expect(state.hotKeys).toBeDefined()
|
|
888
|
+
expect(state.indexSnapshot).toBeDefined()
|
|
889
|
+
expect(state.timestamp).toBeDefined()
|
|
890
|
+
})
|
|
891
|
+
})
|
|
892
|
+
|
|
893
|
+
describe('onWake()', () => {
|
|
894
|
+
it('should restore hot tier entries from hibernate state', async () => {
|
|
895
|
+
const data = new Uint8Array([1, 2, 3])
|
|
896
|
+
mockDOStorage.get.mockResolvedValue(data)
|
|
897
|
+
|
|
898
|
+
const orchestrator = createTieredStorageOrchestrator({
|
|
899
|
+
swrCacheLayer: mockSWRCacheLayer as unknown as SWRCacheLayer,
|
|
900
|
+
doStorage: mockDOStorage as unknown as DurableObjectStorage,
|
|
901
|
+
r2Layer: mockR2Layer as unknown as R2StorageLayer,
|
|
902
|
+
})
|
|
903
|
+
|
|
904
|
+
const hibernateState = {
|
|
905
|
+
hotKeys: ['page-1', 'page-2'],
|
|
906
|
+
indexSnapshot: new Map(),
|
|
907
|
+
timestamp: Date.now() - 1000,
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
// @ts-expect-error - Method doesn't exist yet (RED test)
|
|
911
|
+
const result = await orchestrator.onWake(hibernateState)
|
|
912
|
+
|
|
913
|
+
expect(result.restoredCount).toBe(2)
|
|
914
|
+
expect(mockSWRCacheLayer.put).toHaveBeenCalledTimes(2)
|
|
915
|
+
})
|
|
916
|
+
|
|
917
|
+
it('should prefetch frequently accessed pages on wake', async () => {
|
|
918
|
+
const data = new Uint8Array([1, 2, 3])
|
|
919
|
+
mockDOStorage.get.mockResolvedValue(data)
|
|
920
|
+
|
|
921
|
+
const orchestrator = createTieredStorageOrchestrator({
|
|
922
|
+
cacheLayer: mockCacheLayer as unknown as CacheLayer,
|
|
923
|
+
doStorage: mockDOStorage as unknown as DurableObjectStorage,
|
|
924
|
+
r2Layer: mockR2Layer as unknown as R2StorageLayer,
|
|
925
|
+
})
|
|
926
|
+
|
|
927
|
+
const hibernateState = {
|
|
928
|
+
hotKeys: [],
|
|
929
|
+
indexSnapshot: new Map([
|
|
930
|
+
['page-high-access', { key: 'page-high-access', tier: 'warm' as StorageTier, accessCount: 100, size: 8192, lastAccess: Date.now() - 5000, created: Date.now() - 100000, modified: Date.now() - 5000, dirty: false }],
|
|
931
|
+
]),
|
|
932
|
+
timestamp: Date.now() - 1000,
|
|
933
|
+
}
|
|
934
|
+
|
|
935
|
+
// @ts-expect-error - Method doesn't exist yet (RED test)
|
|
936
|
+
const result = await orchestrator.onWake(hibernateState, { prefetchThreshold: 50 })
|
|
937
|
+
|
|
938
|
+
expect(result.prefetchedCount).toBeGreaterThan(0)
|
|
939
|
+
})
|
|
940
|
+
|
|
941
|
+
it('should validate and repair storage index on wake', async () => {
|
|
942
|
+
const orchestrator = createTieredStorageOrchestrator({
|
|
943
|
+
cacheLayer: mockCacheLayer as unknown as CacheLayer,
|
|
944
|
+
doStorage: mockDOStorage as unknown as DurableObjectStorage,
|
|
945
|
+
r2Layer: mockR2Layer as unknown as R2StorageLayer,
|
|
946
|
+
})
|
|
947
|
+
|
|
948
|
+
// @ts-expect-error - Method doesn't exist yet (RED test)
|
|
949
|
+
const result = await orchestrator.onWake(null, { validateIndex: true })
|
|
950
|
+
|
|
951
|
+
expect(result.indexValidated).toBe(true)
|
|
952
|
+
expect(result.indexRepairs).toBeDefined()
|
|
953
|
+
})
|
|
954
|
+
})
|
|
955
|
+
})
|
|
956
|
+
|
|
957
|
+
describe('Background Task Scheduling (DO Alarm)', () => {
|
|
958
|
+
describe('scheduleAlarm()', () => {
|
|
959
|
+
it('should schedule a background demotion cycle', async () => {
|
|
960
|
+
const mockAlarmStorage = {
|
|
961
|
+
...mockDOStorage,
|
|
962
|
+
setAlarm: vi.fn().mockResolvedValue(undefined),
|
|
963
|
+
getAlarm: vi.fn().mockResolvedValue(null),
|
|
964
|
+
deleteAlarm: vi.fn().mockResolvedValue(undefined),
|
|
965
|
+
}
|
|
966
|
+
|
|
967
|
+
const orchestrator = createTieredStorageOrchestrator({
|
|
968
|
+
cacheLayer: mockCacheLayer as unknown as CacheLayer,
|
|
969
|
+
doStorage: mockAlarmStorage as unknown as DurableObjectStorage,
|
|
970
|
+
r2Layer: mockR2Layer as unknown as R2StorageLayer,
|
|
971
|
+
})
|
|
972
|
+
|
|
973
|
+
// @ts-expect-error - Method doesn't exist yet (RED test)
|
|
974
|
+
await orchestrator.scheduleAlarm({ type: 'demotion', delayMs: 60000 })
|
|
975
|
+
|
|
976
|
+
expect(mockAlarmStorage.setAlarm).toHaveBeenCalled()
|
|
977
|
+
})
|
|
978
|
+
|
|
979
|
+
it('should schedule a background sync cycle', async () => {
|
|
980
|
+
const mockAlarmStorage = {
|
|
981
|
+
...mockDOStorage,
|
|
982
|
+
setAlarm: vi.fn().mockResolvedValue(undefined),
|
|
983
|
+
getAlarm: vi.fn().mockResolvedValue(null),
|
|
984
|
+
}
|
|
985
|
+
|
|
986
|
+
const orchestrator = createTieredStorageOrchestrator({
|
|
987
|
+
cacheLayer: mockCacheLayer as unknown as CacheLayer,
|
|
988
|
+
doStorage: mockAlarmStorage as unknown as DurableObjectStorage,
|
|
989
|
+
r2Layer: mockR2Layer as unknown as R2StorageLayer,
|
|
990
|
+
})
|
|
991
|
+
|
|
992
|
+
// @ts-expect-error - Method doesn't exist yet (RED test)
|
|
993
|
+
await orchestrator.scheduleAlarm({ type: 'sync', delayMs: 300000 })
|
|
994
|
+
|
|
995
|
+
expect(mockAlarmStorage.setAlarm).toHaveBeenCalled()
|
|
996
|
+
})
|
|
997
|
+
|
|
998
|
+
it('should not schedule duplicate alarms', async () => {
|
|
999
|
+
const existingAlarmTime = Date.now() + 30000
|
|
1000
|
+
const mockAlarmStorage = {
|
|
1001
|
+
...mockDOStorage,
|
|
1002
|
+
setAlarm: vi.fn().mockResolvedValue(undefined),
|
|
1003
|
+
getAlarm: vi.fn().mockResolvedValue(existingAlarmTime),
|
|
1004
|
+
}
|
|
1005
|
+
|
|
1006
|
+
const orchestrator = createTieredStorageOrchestrator({
|
|
1007
|
+
cacheLayer: mockCacheLayer as unknown as CacheLayer,
|
|
1008
|
+
doStorage: mockAlarmStorage as unknown as DurableObjectStorage,
|
|
1009
|
+
r2Layer: mockR2Layer as unknown as R2StorageLayer,
|
|
1010
|
+
})
|
|
1011
|
+
|
|
1012
|
+
// @ts-expect-error - Method doesn't exist yet (RED test)
|
|
1013
|
+
await orchestrator.scheduleAlarm({ type: 'demotion', delayMs: 60000, skipIfExists: true })
|
|
1014
|
+
|
|
1015
|
+
expect(mockAlarmStorage.setAlarm).not.toHaveBeenCalled()
|
|
1016
|
+
})
|
|
1017
|
+
})
|
|
1018
|
+
|
|
1019
|
+
describe('handleAlarm()', () => {
|
|
1020
|
+
it('should run demotion cycle on alarm', async () => {
|
|
1021
|
+
const orchestrator = createTieredStorageOrchestrator({
|
|
1022
|
+
cacheLayer: mockCacheLayer as unknown as CacheLayer,
|
|
1023
|
+
doStorage: mockDOStorage as unknown as DurableObjectStorage,
|
|
1024
|
+
r2Layer: mockR2Layer as unknown as R2StorageLayer,
|
|
1025
|
+
hotTtlMs: 0, // Immediate expiry
|
|
1026
|
+
autoDemote: true,
|
|
1027
|
+
})
|
|
1028
|
+
|
|
1029
|
+
// Add hot tier entry
|
|
1030
|
+
await orchestrator.write('page-1', new Uint8Array([1, 2, 3]), { tier: 'hot' })
|
|
1031
|
+
|
|
1032
|
+
// @ts-expect-error - Method doesn't exist yet (RED test)
|
|
1033
|
+
const result = await orchestrator.handleAlarm({ type: 'demotion' })
|
|
1034
|
+
|
|
1035
|
+
expect(result.demotionCount).toBeGreaterThanOrEqual(0)
|
|
1036
|
+
expect(result.nextAlarmMs).toBeDefined()
|
|
1037
|
+
})
|
|
1038
|
+
|
|
1039
|
+
it('should run sync cycle on alarm', async () => {
|
|
1040
|
+
const data = new Uint8Array([1, 2, 3])
|
|
1041
|
+
mockDOStorage.get.mockResolvedValue(data)
|
|
1042
|
+
|
|
1043
|
+
const orchestrator = createTieredStorageOrchestrator({
|
|
1044
|
+
cacheLayer: mockCacheLayer as unknown as CacheLayer,
|
|
1045
|
+
doStorage: mockDOStorage as unknown as DurableObjectStorage,
|
|
1046
|
+
r2Layer: mockR2Layer as unknown as R2StorageLayer,
|
|
1047
|
+
})
|
|
1048
|
+
|
|
1049
|
+
// Create dirty entry
|
|
1050
|
+
await orchestrator.write('page-1', data, { tier: 'warm' })
|
|
1051
|
+
|
|
1052
|
+
// @ts-expect-error - Method doesn't exist yet (RED test)
|
|
1053
|
+
const result = await orchestrator.handleAlarm({ type: 'sync' })
|
|
1054
|
+
|
|
1055
|
+
expect(result.syncedCount).toBeGreaterThanOrEqual(0)
|
|
1056
|
+
})
|
|
1057
|
+
|
|
1058
|
+
it('should reschedule alarm after completion', async () => {
|
|
1059
|
+
const mockAlarmStorage = {
|
|
1060
|
+
...mockDOStorage,
|
|
1061
|
+
setAlarm: vi.fn().mockResolvedValue(undefined),
|
|
1062
|
+
getAlarm: vi.fn().mockResolvedValue(null),
|
|
1063
|
+
}
|
|
1064
|
+
|
|
1065
|
+
const orchestrator = createTieredStorageOrchestrator({
|
|
1066
|
+
cacheLayer: mockCacheLayer as unknown as CacheLayer,
|
|
1067
|
+
doStorage: mockAlarmStorage as unknown as DurableObjectStorage,
|
|
1068
|
+
r2Layer: mockR2Layer as unknown as R2StorageLayer,
|
|
1069
|
+
})
|
|
1070
|
+
|
|
1071
|
+
// @ts-expect-error - Method doesn't exist yet (RED test)
|
|
1072
|
+
await orchestrator.handleAlarm({ type: 'demotion', rescheduleMs: 60000 })
|
|
1073
|
+
|
|
1074
|
+
expect(mockAlarmStorage.setAlarm).toHaveBeenCalled()
|
|
1075
|
+
})
|
|
1076
|
+
})
|
|
1077
|
+
})
|
|
1078
|
+
|
|
1079
|
+
describe('Circuit Breaker Pattern', () => {
|
|
1080
|
+
describe('tier failure handling', () => {
|
|
1081
|
+
it('should open circuit breaker after consecutive failures', async () => {
|
|
1082
|
+
mockR2Layer.get.mockRejectedValue(new Error('R2 unavailable'))
|
|
1083
|
+
|
|
1084
|
+
const orchestrator = createTieredStorageOrchestrator({
|
|
1085
|
+
cacheLayer: mockCacheLayer as unknown as CacheLayer,
|
|
1086
|
+
doStorage: mockDOStorage as unknown as DurableObjectStorage,
|
|
1087
|
+
r2Layer: mockR2Layer as unknown as R2StorageLayer,
|
|
1088
|
+
// @ts-expect-error - Config option doesn't exist yet
|
|
1089
|
+
circuitBreaker: {
|
|
1090
|
+
enabled: true,
|
|
1091
|
+
failureThreshold: 3,
|
|
1092
|
+
resetTimeoutMs: 30000,
|
|
1093
|
+
},
|
|
1094
|
+
})
|
|
1095
|
+
|
|
1096
|
+
// Trigger multiple failures
|
|
1097
|
+
mockCacheLayer.get.mockResolvedValue(null)
|
|
1098
|
+
mockDOStorage.get.mockResolvedValue(undefined)
|
|
1099
|
+
|
|
1100
|
+
for (let i = 0; i < 5; i++) {
|
|
1101
|
+
try {
|
|
1102
|
+
await orchestrator.read(`page-${i}`)
|
|
1103
|
+
} catch {
|
|
1104
|
+
// Expected to fail
|
|
1105
|
+
}
|
|
1106
|
+
}
|
|
1107
|
+
|
|
1108
|
+
// @ts-expect-error - Method doesn't exist yet (RED test)
|
|
1109
|
+
const circuitState = orchestrator.getCircuitBreakerState('cold')
|
|
1110
|
+
|
|
1111
|
+
expect(circuitState.state).toBe('open')
|
|
1112
|
+
expect(circuitState.failures).toBeGreaterThanOrEqual(3)
|
|
1113
|
+
})
|
|
1114
|
+
|
|
1115
|
+
it('should skip unavailable tier when circuit is open', async () => {
|
|
1116
|
+
mockR2Layer.get.mockRejectedValue(new Error('R2 unavailable'))
|
|
1117
|
+
|
|
1118
|
+
const orchestrator = createTieredStorageOrchestrator({
|
|
1119
|
+
cacheLayer: mockCacheLayer as unknown as CacheLayer,
|
|
1120
|
+
doStorage: mockDOStorage as unknown as DurableObjectStorage,
|
|
1121
|
+
r2Layer: mockR2Layer as unknown as R2StorageLayer,
|
|
1122
|
+
// @ts-expect-error - Config option doesn't exist yet
|
|
1123
|
+
circuitBreaker: {
|
|
1124
|
+
enabled: true,
|
|
1125
|
+
failureThreshold: 1,
|
|
1126
|
+
},
|
|
1127
|
+
})
|
|
1128
|
+
|
|
1129
|
+
// Open the circuit
|
|
1130
|
+
mockCacheLayer.get.mockResolvedValue(null)
|
|
1131
|
+
mockDOStorage.get.mockResolvedValue(undefined)
|
|
1132
|
+
|
|
1133
|
+
try {
|
|
1134
|
+
await orchestrator.read('page-1')
|
|
1135
|
+
} catch {
|
|
1136
|
+
// Expected
|
|
1137
|
+
}
|
|
1138
|
+
|
|
1139
|
+
// Circuit is now open, R2 should be skipped
|
|
1140
|
+
vi.clearAllMocks()
|
|
1141
|
+
mockCacheLayer.get.mockResolvedValue(null)
|
|
1142
|
+
mockDOStorage.get.mockResolvedValue(undefined)
|
|
1143
|
+
await orchestrator.read('page-2')
|
|
1144
|
+
|
|
1145
|
+
// R2 should not have been called because circuit is open
|
|
1146
|
+
expect(mockR2Layer.get).not.toHaveBeenCalled()
|
|
1147
|
+
})
|
|
1148
|
+
|
|
1149
|
+
it('should transition to half-open after reset timeout', async () => {
|
|
1150
|
+
vi.useFakeTimers()
|
|
1151
|
+
|
|
1152
|
+
const orchestrator = createTieredStorageOrchestrator({
|
|
1153
|
+
cacheLayer: mockCacheLayer as unknown as CacheLayer,
|
|
1154
|
+
doStorage: mockDOStorage as unknown as DurableObjectStorage,
|
|
1155
|
+
r2Layer: mockR2Layer as unknown as R2StorageLayer,
|
|
1156
|
+
// @ts-expect-error - Config option doesn't exist yet
|
|
1157
|
+
circuitBreaker: {
|
|
1158
|
+
enabled: true,
|
|
1159
|
+
failureThreshold: 1,
|
|
1160
|
+
resetTimeoutMs: 5000,
|
|
1161
|
+
},
|
|
1162
|
+
})
|
|
1163
|
+
|
|
1164
|
+
// Open the circuit
|
|
1165
|
+
mockCacheLayer.get.mockResolvedValue(null)
|
|
1166
|
+
mockDOStorage.get.mockResolvedValue(undefined)
|
|
1167
|
+
mockR2Layer.get.mockRejectedValue(new Error('R2 unavailable'))
|
|
1168
|
+
|
|
1169
|
+
try {
|
|
1170
|
+
await orchestrator.read('page-1')
|
|
1171
|
+
} catch {
|
|
1172
|
+
// Expected
|
|
1173
|
+
}
|
|
1174
|
+
|
|
1175
|
+
// Advance time past reset timeout
|
|
1176
|
+
vi.advanceTimersByTime(6000)
|
|
1177
|
+
|
|
1178
|
+
// @ts-expect-error - Method doesn't exist yet (RED test)
|
|
1179
|
+
const circuitState = orchestrator.getCircuitBreakerState('cold')
|
|
1180
|
+
|
|
1181
|
+
expect(circuitState.state).toBe('half-open')
|
|
1182
|
+
|
|
1183
|
+
vi.useRealTimers()
|
|
1184
|
+
})
|
|
1185
|
+
|
|
1186
|
+
it('should close circuit after successful operation in half-open state', async () => {
|
|
1187
|
+
vi.useFakeTimers()
|
|
1188
|
+
|
|
1189
|
+
const orchestrator = createTieredStorageOrchestrator({
|
|
1190
|
+
cacheLayer: mockCacheLayer as unknown as CacheLayer,
|
|
1191
|
+
doStorage: mockDOStorage as unknown as DurableObjectStorage,
|
|
1192
|
+
r2Layer: mockR2Layer as unknown as R2StorageLayer,
|
|
1193
|
+
// @ts-expect-error - Config option doesn't exist yet
|
|
1194
|
+
circuitBreaker: {
|
|
1195
|
+
enabled: true,
|
|
1196
|
+
failureThreshold: 1,
|
|
1197
|
+
resetTimeoutMs: 5000,
|
|
1198
|
+
},
|
|
1199
|
+
})
|
|
1200
|
+
|
|
1201
|
+
// Open the circuit
|
|
1202
|
+
mockCacheLayer.get.mockResolvedValue(null)
|
|
1203
|
+
mockDOStorage.get.mockResolvedValue(undefined)
|
|
1204
|
+
mockR2Layer.get.mockRejectedValueOnce(new Error('R2 unavailable'))
|
|
1205
|
+
|
|
1206
|
+
try {
|
|
1207
|
+
await orchestrator.read('page-1')
|
|
1208
|
+
} catch {
|
|
1209
|
+
// Expected
|
|
1210
|
+
}
|
|
1211
|
+
|
|
1212
|
+
// Advance to half-open
|
|
1213
|
+
vi.advanceTimersByTime(6000)
|
|
1214
|
+
|
|
1215
|
+
// Now R2 succeeds
|
|
1216
|
+
mockR2Layer.get.mockResolvedValue(new Uint8Array([1, 2, 3]))
|
|
1217
|
+
|
|
1218
|
+
await orchestrator.read('page-2')
|
|
1219
|
+
|
|
1220
|
+
// @ts-expect-error - Method doesn't exist yet (RED test)
|
|
1221
|
+
const circuitState = orchestrator.getCircuitBreakerState('cold')
|
|
1222
|
+
|
|
1223
|
+
expect(circuitState.state).toBe('closed')
|
|
1224
|
+
|
|
1225
|
+
vi.useRealTimers()
|
|
1226
|
+
})
|
|
1227
|
+
})
|
|
1228
|
+
|
|
1229
|
+
describe('fallback behavior', () => {
|
|
1230
|
+
it('should use fallback tier when primary tier circuit is open', async () => {
|
|
1231
|
+
const data = new Uint8Array([1, 2, 3])
|
|
1232
|
+
|
|
1233
|
+
const orchestrator = createTieredStorageOrchestrator({
|
|
1234
|
+
cacheLayer: mockCacheLayer as unknown as CacheLayer,
|
|
1235
|
+
doStorage: mockDOStorage as unknown as DurableObjectStorage,
|
|
1236
|
+
r2Layer: mockR2Layer as unknown as R2StorageLayer,
|
|
1237
|
+
// @ts-expect-error - Config option doesn't exist yet
|
|
1238
|
+
circuitBreaker: {
|
|
1239
|
+
enabled: true,
|
|
1240
|
+
failureThreshold: 1,
|
|
1241
|
+
fallbackEnabled: true,
|
|
1242
|
+
},
|
|
1243
|
+
})
|
|
1244
|
+
|
|
1245
|
+
// Configure circuit to be open for cold tier
|
|
1246
|
+
// @ts-expect-error - Method doesn't exist yet
|
|
1247
|
+
orchestrator.setCircuitBreakerState('cold', 'open')
|
|
1248
|
+
|
|
1249
|
+
// Data should come from warm tier fallback
|
|
1250
|
+
mockCacheLayer.get.mockResolvedValue(null)
|
|
1251
|
+
mockDOStorage.get.mockResolvedValue(data)
|
|
1252
|
+
|
|
1253
|
+
const result = await orchestrator.read('page-1')
|
|
1254
|
+
|
|
1255
|
+
expect(result.data).toEqual(data)
|
|
1256
|
+
expect(result.tier).toBe('warm')
|
|
1257
|
+
// @ts-expect-error - Property doesn't exist yet
|
|
1258
|
+
expect(result.fallback).toBe(true)
|
|
1259
|
+
})
|
|
1260
|
+
})
|
|
1261
|
+
})
|
|
1262
|
+
|
|
1263
|
+
describe('Integrated Metrics Aggregation', () => {
|
|
1264
|
+
describe('getAggregatedMetrics()', () => {
|
|
1265
|
+
it('should aggregate metrics from all components', async () => {
|
|
1266
|
+
const orchestrator = createTieredStorageOrchestrator({
|
|
1267
|
+
cacheLayer: mockCacheLayer as unknown as CacheLayer,
|
|
1268
|
+
doStorage: mockDOStorage as unknown as DurableObjectStorage,
|
|
1269
|
+
r2Layer: mockR2Layer as unknown as R2StorageLayer,
|
|
1270
|
+
})
|
|
1271
|
+
|
|
1272
|
+
// Generate some activity
|
|
1273
|
+
await orchestrator.write('page-1', new Uint8Array([1, 2, 3]))
|
|
1274
|
+
await orchestrator.write('page-2', new Uint8Array([4, 5, 6]))
|
|
1275
|
+
|
|
1276
|
+
// @ts-expect-error - Method doesn't exist yet (RED test)
|
|
1277
|
+
const metrics = orchestrator.getAggregatedMetrics()
|
|
1278
|
+
|
|
1279
|
+
expect(metrics).toMatchObject({
|
|
1280
|
+
timestamp: expect.any(Number),
|
|
1281
|
+
period: expect.any(String),
|
|
1282
|
+
tiers: {
|
|
1283
|
+
hot: expect.objectContaining({
|
|
1284
|
+
reads: expect.any(Number),
|
|
1285
|
+
writes: expect.any(Number),
|
|
1286
|
+
hitRatio: expect.any(Number),
|
|
1287
|
+
}),
|
|
1288
|
+
warm: expect.objectContaining({
|
|
1289
|
+
reads: expect.any(Number),
|
|
1290
|
+
writes: expect.any(Number),
|
|
1291
|
+
bytesStored: expect.any(Number),
|
|
1292
|
+
}),
|
|
1293
|
+
cold: expect.objectContaining({
|
|
1294
|
+
reads: expect.any(Number),
|
|
1295
|
+
writes: expect.any(Number),
|
|
1296
|
+
bytesStored: expect.any(Number),
|
|
1297
|
+
}),
|
|
1298
|
+
},
|
|
1299
|
+
migrations: expect.objectContaining({
|
|
1300
|
+
promotions: expect.any(Number),
|
|
1301
|
+
demotions: expect.any(Number),
|
|
1302
|
+
failures: expect.any(Number),
|
|
1303
|
+
}),
|
|
1304
|
+
circuitBreakers: expect.any(Object),
|
|
1305
|
+
})
|
|
1306
|
+
})
|
|
1307
|
+
|
|
1308
|
+
it('should track latency percentiles', async () => {
|
|
1309
|
+
const orchestrator = createTieredStorageOrchestrator({
|
|
1310
|
+
cacheLayer: mockCacheLayer as unknown as CacheLayer,
|
|
1311
|
+
doStorage: mockDOStorage as unknown as DurableObjectStorage,
|
|
1312
|
+
r2Layer: mockR2Layer as unknown as R2StorageLayer,
|
|
1313
|
+
// @ts-expect-error - Config option doesn't exist yet
|
|
1314
|
+
metrics: {
|
|
1315
|
+
trackLatency: true,
|
|
1316
|
+
},
|
|
1317
|
+
})
|
|
1318
|
+
|
|
1319
|
+
mockCacheLayer.get.mockResolvedValue(null)
|
|
1320
|
+
mockDOStorage.get.mockResolvedValue(new Uint8Array([1, 2, 3]))
|
|
1321
|
+
|
|
1322
|
+
// Generate some reads
|
|
1323
|
+
for (let i = 0; i < 10; i++) {
|
|
1324
|
+
await orchestrator.read(`page-${i}`)
|
|
1325
|
+
}
|
|
1326
|
+
|
|
1327
|
+
// @ts-expect-error - Method doesn't exist yet (RED test)
|
|
1328
|
+
const metrics = orchestrator.getAggregatedMetrics()
|
|
1329
|
+
|
|
1330
|
+
expect(metrics.latency).toBeDefined()
|
|
1331
|
+
expect(metrics.latency.p50).toBeDefined()
|
|
1332
|
+
expect(metrics.latency.p95).toBeDefined()
|
|
1333
|
+
expect(metrics.latency.p99).toBeDefined()
|
|
1334
|
+
})
|
|
1335
|
+
|
|
1336
|
+
it('should calculate cost estimates', async () => {
|
|
1337
|
+
const orchestrator = createTieredStorageOrchestrator({
|
|
1338
|
+
cacheLayer: mockCacheLayer as unknown as CacheLayer,
|
|
1339
|
+
doStorage: mockDOStorage as unknown as DurableObjectStorage,
|
|
1340
|
+
r2Layer: mockR2Layer as unknown as R2StorageLayer,
|
|
1341
|
+
// @ts-expect-error - Config option doesn't exist yet
|
|
1342
|
+
metrics: {
|
|
1343
|
+
trackCost: true,
|
|
1344
|
+
costConfig: {
|
|
1345
|
+
r2StoragePerGBMonth: 0.015,
|
|
1346
|
+
r2ClassAOperations: 0.0000045,
|
|
1347
|
+
r2ClassBOperations: 0.00000036,
|
|
1348
|
+
doStoragePerGBMonth: 0.20,
|
|
1349
|
+
cacheOperations: 0, // FREE
|
|
1350
|
+
},
|
|
1351
|
+
},
|
|
1352
|
+
})
|
|
1353
|
+
|
|
1354
|
+
await orchestrator.write('page-1', new Uint8Array(1024 * 1024), { tier: 'cold' })
|
|
1355
|
+
|
|
1356
|
+
// @ts-expect-error - Method doesn't exist yet (RED test)
|
|
1357
|
+
const metrics = orchestrator.getAggregatedMetrics()
|
|
1358
|
+
|
|
1359
|
+
expect(metrics.estimatedCost).toBeDefined()
|
|
1360
|
+
expect(metrics.estimatedCost.r2).toBeGreaterThan(0)
|
|
1361
|
+
expect(metrics.estimatedCost.total).toBeGreaterThan(0)
|
|
1362
|
+
})
|
|
1363
|
+
})
|
|
1364
|
+
|
|
1365
|
+
describe('exportMetrics()', () => {
|
|
1366
|
+
it('should export metrics in Prometheus format', async () => {
|
|
1367
|
+
const orchestrator = createTieredStorageOrchestrator({
|
|
1368
|
+
cacheLayer: mockCacheLayer as unknown as CacheLayer,
|
|
1369
|
+
doStorage: mockDOStorage as unknown as DurableObjectStorage,
|
|
1370
|
+
r2Layer: mockR2Layer as unknown as R2StorageLayer,
|
|
1371
|
+
})
|
|
1372
|
+
|
|
1373
|
+
await orchestrator.write('page-1', new Uint8Array([1, 2, 3]))
|
|
1374
|
+
|
|
1375
|
+
// @ts-expect-error - Method doesn't exist yet (RED test)
|
|
1376
|
+
const prometheus = orchestrator.exportMetrics('prometheus')
|
|
1377
|
+
|
|
1378
|
+
expect(prometheus).toContain('tiered_storage_writes_total')
|
|
1379
|
+
expect(prometheus).toContain('tiered_storage_bytes_written')
|
|
1380
|
+
})
|
|
1381
|
+
|
|
1382
|
+
it('should export metrics in JSON format', async () => {
|
|
1383
|
+
const orchestrator = createTieredStorageOrchestrator({
|
|
1384
|
+
cacheLayer: mockCacheLayer as unknown as CacheLayer,
|
|
1385
|
+
doStorage: mockDOStorage as unknown as DurableObjectStorage,
|
|
1386
|
+
r2Layer: mockR2Layer as unknown as R2StorageLayer,
|
|
1387
|
+
})
|
|
1388
|
+
|
|
1389
|
+
// @ts-expect-error - Method doesn't exist yet (RED test)
|
|
1390
|
+
const json = orchestrator.exportMetrics('json')
|
|
1391
|
+
|
|
1392
|
+
expect(typeof json).toBe('object')
|
|
1393
|
+
expect(json.timestamp).toBeDefined()
|
|
1394
|
+
})
|
|
1395
|
+
})
|
|
1396
|
+
})
|
|
1397
|
+
|
|
1398
|
+
describe('Component Integration', () => {
|
|
1399
|
+
describe('with TieredVFS', () => {
|
|
1400
|
+
it('should integrate with TieredVFS for PGLite page operations', async () => {
|
|
1401
|
+
const orchestrator = createTieredStorageOrchestrator({
|
|
1402
|
+
cacheLayer: mockCacheLayer as unknown as CacheLayer,
|
|
1403
|
+
doStorage: mockDOStorage as unknown as DurableObjectStorage,
|
|
1404
|
+
r2Layer: mockR2Layer as unknown as R2StorageLayer,
|
|
1405
|
+
})
|
|
1406
|
+
|
|
1407
|
+
// @ts-expect-error - Method doesn't exist yet (RED test)
|
|
1408
|
+
const vfs = orchestrator.createVFSInterface({
|
|
1409
|
+
pageSize: 8192,
|
|
1410
|
+
promotionThreshold: 3,
|
|
1411
|
+
})
|
|
1412
|
+
|
|
1413
|
+
expect(vfs.readPage).toBeDefined()
|
|
1414
|
+
expect(vfs.writePage).toBeDefined()
|
|
1415
|
+
expect(vfs.deletePage).toBeDefined()
|
|
1416
|
+
})
|
|
1417
|
+
})
|
|
1418
|
+
|
|
1419
|
+
describe('with AutoPromoter', () => {
|
|
1420
|
+
it('should configure auto-promoter with custom policies', async () => {
|
|
1421
|
+
const orchestrator = createTieredStorageOrchestrator({
|
|
1422
|
+
cacheLayer: mockCacheLayer as unknown as CacheLayer,
|
|
1423
|
+
doStorage: mockDOStorage as unknown as DurableObjectStorage,
|
|
1424
|
+
r2Layer: mockR2Layer as unknown as R2StorageLayer,
|
|
1425
|
+
})
|
|
1426
|
+
|
|
1427
|
+
// @ts-expect-error - Method doesn't exist yet (RED test)
|
|
1428
|
+
orchestrator.configureAutoPromoter({
|
|
1429
|
+
coldToWarm: {
|
|
1430
|
+
accessThreshold: 5,
|
|
1431
|
+
timeWindowMs: 60000,
|
|
1432
|
+
},
|
|
1433
|
+
warmToHot: {
|
|
1434
|
+
accessThreshold: 10,
|
|
1435
|
+
timeWindowMs: 30000,
|
|
1436
|
+
},
|
|
1437
|
+
})
|
|
1438
|
+
|
|
1439
|
+
// @ts-expect-error - Method doesn't exist yet (RED test)
|
|
1440
|
+
const config = orchestrator.getAutoPromoterConfig()
|
|
1441
|
+
|
|
1442
|
+
expect(config.coldToWarm.accessThreshold).toBe(5)
|
|
1443
|
+
expect(config.warmToHot.accessThreshold).toBe(10)
|
|
1444
|
+
})
|
|
1445
|
+
})
|
|
1446
|
+
|
|
1447
|
+
describe('with AutoDemoter', () => {
|
|
1448
|
+
it('should configure auto-demoter with custom policies', async () => {
|
|
1449
|
+
const orchestrator = createTieredStorageOrchestrator({
|
|
1450
|
+
cacheLayer: mockCacheLayer as unknown as CacheLayer,
|
|
1451
|
+
doStorage: mockDOStorage as unknown as DurableObjectStorage,
|
|
1452
|
+
r2Layer: mockR2Layer as unknown as R2StorageLayer,
|
|
1453
|
+
})
|
|
1454
|
+
|
|
1455
|
+
// @ts-expect-error - Method doesn't exist yet (RED test)
|
|
1456
|
+
orchestrator.configureAutoDemoter({
|
|
1457
|
+
hotToWarm: {
|
|
1458
|
+
ttlMs: 300000, // 5 minutes
|
|
1459
|
+
maxEntries: 1000,
|
|
1460
|
+
},
|
|
1461
|
+
warmToCold: {
|
|
1462
|
+
ttlMs: 3600000, // 1 hour
|
|
1463
|
+
maxEntries: 10000,
|
|
1464
|
+
},
|
|
1465
|
+
})
|
|
1466
|
+
|
|
1467
|
+
// @ts-expect-error - Method doesn't exist yet (RED test)
|
|
1468
|
+
const config = orchestrator.getAutoDemoterConfig()
|
|
1469
|
+
|
|
1470
|
+
expect(config.hotToWarm.ttlMs).toBe(300000)
|
|
1471
|
+
expect(config.warmToCold.maxEntries).toBe(10000)
|
|
1472
|
+
})
|
|
1473
|
+
|
|
1474
|
+
it('should support custom demotion strategies', async () => {
|
|
1475
|
+
const orchestrator = createTieredStorageOrchestrator({
|
|
1476
|
+
cacheLayer: mockCacheLayer as unknown as CacheLayer,
|
|
1477
|
+
doStorage: mockDOStorage as unknown as DurableObjectStorage,
|
|
1478
|
+
r2Layer: mockR2Layer as unknown as R2StorageLayer,
|
|
1479
|
+
})
|
|
1480
|
+
|
|
1481
|
+
const customStrategy = vi.fn().mockReturnValue(['page-1', 'page-2'])
|
|
1482
|
+
|
|
1483
|
+
// @ts-expect-error - Method doesn't exist yet (RED test)
|
|
1484
|
+
orchestrator.setDemotionStrategy('hot', customStrategy)
|
|
1485
|
+
|
|
1486
|
+
await orchestrator.write('page-1', new Uint8Array([1]), { tier: 'hot' })
|
|
1487
|
+
await orchestrator.write('page-2', new Uint8Array([2]), { tier: 'hot' })
|
|
1488
|
+
await orchestrator.write('page-3', new Uint8Array([3]), { tier: 'hot' })
|
|
1489
|
+
|
|
1490
|
+
// @ts-expect-error - Method doesn't exist yet (RED test)
|
|
1491
|
+
const events = await orchestrator.runDemotionWithStrategy('hot', 2)
|
|
1492
|
+
|
|
1493
|
+
expect(customStrategy).toHaveBeenCalled()
|
|
1494
|
+
expect(events.length).toBe(2)
|
|
1495
|
+
})
|
|
1496
|
+
})
|
|
1497
|
+
})
|
|
1498
|
+
|
|
1499
|
+
describe('Error Recovery', () => {
|
|
1500
|
+
describe('atomic operations', () => {
|
|
1501
|
+
it('should rollback promotion on partial failure', async () => {
|
|
1502
|
+
const data = new Uint8Array([1, 2, 3])
|
|
1503
|
+
mockR2Layer.get.mockResolvedValue(data)
|
|
1504
|
+
mockDOStorage.put.mockRejectedValue(new Error('DO write failed'))
|
|
1505
|
+
|
|
1506
|
+
const orchestrator = createTieredStorageOrchestrator({
|
|
1507
|
+
cacheLayer: mockCacheLayer as unknown as CacheLayer,
|
|
1508
|
+
doStorage: mockDOStorage as unknown as DurableObjectStorage,
|
|
1509
|
+
r2Layer: mockR2Layer as unknown as R2StorageLayer,
|
|
1510
|
+
})
|
|
1511
|
+
|
|
1512
|
+
const event = await orchestrator.promoteToWarm('page-1')
|
|
1513
|
+
|
|
1514
|
+
expect(event.success).toBe(false)
|
|
1515
|
+
// Data should still be accessible from cold tier
|
|
1516
|
+
expect(orchestrator.getTier('page-1')).not.toBe('warm')
|
|
1517
|
+
})
|
|
1518
|
+
|
|
1519
|
+
it('should handle demotion failure gracefully', async () => {
|
|
1520
|
+
const data = new Uint8Array([1, 2, 3])
|
|
1521
|
+
mockDOStorage.get.mockResolvedValue(data)
|
|
1522
|
+
mockDOStorage.put.mockResolvedValue(undefined)
|
|
1523
|
+
mockR2Layer.put.mockRejectedValue(new Error('R2 write failed'))
|
|
1524
|
+
|
|
1525
|
+
const orchestrator = createTieredStorageOrchestrator({
|
|
1526
|
+
cacheLayer: mockCacheLayer as unknown as CacheLayer,
|
|
1527
|
+
doStorage: mockDOStorage as unknown as DurableObjectStorage,
|
|
1528
|
+
r2Layer: mockR2Layer as unknown as R2StorageLayer,
|
|
1529
|
+
})
|
|
1530
|
+
|
|
1531
|
+
// Write to warm tier first
|
|
1532
|
+
await orchestrator.write('page-1', data, { tier: 'warm' })
|
|
1533
|
+
|
|
1534
|
+
const event = await orchestrator.demoteFromWarm('page-1')
|
|
1535
|
+
|
|
1536
|
+
expect(event.success).toBe(false)
|
|
1537
|
+
// Data should still be in warm tier
|
|
1538
|
+
expect(orchestrator.getTier('page-1')).toBe('warm')
|
|
1539
|
+
})
|
|
1540
|
+
})
|
|
1541
|
+
|
|
1542
|
+
describe('consistency checks', () => {
|
|
1543
|
+
it('should detect and repair tier inconsistencies', async () => {
|
|
1544
|
+
const orchestrator = createTieredStorageOrchestrator({
|
|
1545
|
+
cacheLayer: mockCacheLayer as unknown as CacheLayer,
|
|
1546
|
+
doStorage: mockDOStorage as unknown as DurableObjectStorage,
|
|
1547
|
+
r2Layer: mockR2Layer as unknown as R2StorageLayer,
|
|
1548
|
+
})
|
|
1549
|
+
|
|
1550
|
+
// Simulate inconsistency: index says warm, but data is only in cold
|
|
1551
|
+
// @ts-expect-error - Accessing private method for test setup
|
|
1552
|
+
orchestrator.storageIndex?.set('page-1', {
|
|
1553
|
+
key: 'page-1',
|
|
1554
|
+
tier: 'warm',
|
|
1555
|
+
size: 100,
|
|
1556
|
+
lastAccess: Date.now(),
|
|
1557
|
+
accessCount: 1,
|
|
1558
|
+
created: Date.now(),
|
|
1559
|
+
modified: Date.now(),
|
|
1560
|
+
dirty: false,
|
|
1561
|
+
})
|
|
1562
|
+
|
|
1563
|
+
mockDOStorage.get.mockResolvedValue(undefined) // Not in warm
|
|
1564
|
+
mockR2Layer.get.mockResolvedValue(new Uint8Array([1, 2, 3])) // But in cold
|
|
1565
|
+
|
|
1566
|
+
// @ts-expect-error - Method doesn't exist yet (RED test)
|
|
1567
|
+
const result = await orchestrator.runConsistencyCheck()
|
|
1568
|
+
|
|
1569
|
+
expect(result.inconsistencies).toBeGreaterThan(0)
|
|
1570
|
+
expect(result.repairs).toBeDefined()
|
|
1571
|
+
})
|
|
1572
|
+
})
|
|
1573
|
+
})
|
|
1574
|
+
|
|
1575
|
+
describe('Advanced Tier Promotion Logic', () => {
|
|
1576
|
+
describe('time-window based promotion', () => {
|
|
1577
|
+
it('should not promote if access count met but time window exceeded', async () => {
|
|
1578
|
+
const data = new Uint8Array([1, 2, 3])
|
|
1579
|
+
mockCacheLayer.get.mockResolvedValue(null)
|
|
1580
|
+
mockDOStorage.get.mockResolvedValue(undefined)
|
|
1581
|
+
mockR2Layer.get.mockResolvedValue(data)
|
|
1582
|
+
|
|
1583
|
+
const orchestrator = createTieredStorageOrchestrator({
|
|
1584
|
+
cacheLayer: mockCacheLayer as unknown as CacheLayer,
|
|
1585
|
+
doStorage: mockDOStorage as unknown as DurableObjectStorage,
|
|
1586
|
+
r2Layer: mockR2Layer as unknown as R2StorageLayer,
|
|
1587
|
+
coldToWarmThreshold: 2,
|
|
1588
|
+
promotionWindowMs: 1000, // Very short window
|
|
1589
|
+
autoPromote: true,
|
|
1590
|
+
})
|
|
1591
|
+
|
|
1592
|
+
// First access
|
|
1593
|
+
await orchestrator.read('page-123')
|
|
1594
|
+
|
|
1595
|
+
// Simulate time passing beyond window
|
|
1596
|
+
// @ts-expect-error - Method doesn't exist yet (RED test)
|
|
1597
|
+
orchestrator.setIndexEntryTimestamp('page-123', 'created', Date.now() - 5000)
|
|
1598
|
+
|
|
1599
|
+
// Second access - should NOT trigger promotion (time window exceeded)
|
|
1600
|
+
vi.clearAllMocks()
|
|
1601
|
+
await orchestrator.read('page-123')
|
|
1602
|
+
await new Promise(resolve => setTimeout(resolve, 0))
|
|
1603
|
+
|
|
1604
|
+
expect(mockDOStorage.put).not.toHaveBeenCalled()
|
|
1605
|
+
})
|
|
1606
|
+
})
|
|
1607
|
+
|
|
1608
|
+
describe('burst access pattern detection', () => {
|
|
1609
|
+
it('should detect burst access patterns and prioritize promotion', async () => {
|
|
1610
|
+
const data = new Uint8Array([1, 2, 3])
|
|
1611
|
+
mockCacheLayer.get.mockResolvedValue(null)
|
|
1612
|
+
mockDOStorage.get.mockResolvedValue(undefined)
|
|
1613
|
+
mockR2Layer.get.mockResolvedValue(data)
|
|
1614
|
+
|
|
1615
|
+
const orchestrator = createTieredStorageOrchestrator({
|
|
1616
|
+
cacheLayer: mockCacheLayer as unknown as CacheLayer,
|
|
1617
|
+
doStorage: mockDOStorage as unknown as DurableObjectStorage,
|
|
1618
|
+
r2Layer: mockR2Layer as unknown as R2StorageLayer,
|
|
1619
|
+
// @ts-expect-error - Config option doesn't exist yet
|
|
1620
|
+
burstDetection: {
|
|
1621
|
+
enabled: true,
|
|
1622
|
+
windowMs: 1000,
|
|
1623
|
+
accessThreshold: 5,
|
|
1624
|
+
},
|
|
1625
|
+
})
|
|
1626
|
+
|
|
1627
|
+
// Simulate burst: 10 accesses in rapid succession
|
|
1628
|
+
for (let i = 0; i < 10; i++) {
|
|
1629
|
+
await orchestrator.read('page-burst')
|
|
1630
|
+
}
|
|
1631
|
+
|
|
1632
|
+
// @ts-expect-error - Method doesn't exist yet (RED test)
|
|
1633
|
+
const metrics = orchestrator.getAccessPatternMetrics('page-burst')
|
|
1634
|
+
|
|
1635
|
+
expect(metrics.isBurst).toBe(true)
|
|
1636
|
+
expect(metrics.accessRate).toBeGreaterThan(5)
|
|
1637
|
+
})
|
|
1638
|
+
})
|
|
1639
|
+
})
|
|
1640
|
+
|
|
1641
|
+
describe('Advanced Tier Demotion Logic', () => {
|
|
1642
|
+
describe('LRU with size-based eviction', () => {
|
|
1643
|
+
it('should evict large entries first when memory pressure is high', async () => {
|
|
1644
|
+
mockCacheLayer.delete.mockResolvedValue(true)
|
|
1645
|
+
|
|
1646
|
+
const orchestrator = createTieredStorageOrchestrator({
|
|
1647
|
+
cacheLayer: mockCacheLayer as unknown as CacheLayer,
|
|
1648
|
+
doStorage: mockDOStorage as unknown as DurableObjectStorage,
|
|
1649
|
+
r2Layer: mockR2Layer as unknown as R2StorageLayer,
|
|
1650
|
+
// @ts-expect-error - Config option doesn't exist yet
|
|
1651
|
+
evictionStrategy: 'size-weighted-lru',
|
|
1652
|
+
})
|
|
1653
|
+
|
|
1654
|
+
// Add entries of different sizes
|
|
1655
|
+
await orchestrator.write('small', new Uint8Array(100), { tier: 'hot' })
|
|
1656
|
+
await orchestrator.write('medium', new Uint8Array(1000), { tier: 'hot' })
|
|
1657
|
+
await orchestrator.write('large', new Uint8Array(10000), { tier: 'hot' })
|
|
1658
|
+
|
|
1659
|
+
// @ts-expect-error - Method doesn't exist yet (RED test)
|
|
1660
|
+
const events = await orchestrator.evictByMemoryPressure(5000) // Need 5KB
|
|
1661
|
+
|
|
1662
|
+
// Large entry should be evicted first to free most space
|
|
1663
|
+
expect(events.some((e: TierMigrationEvent) => e.key === 'large')).toBe(true)
|
|
1664
|
+
})
|
|
1665
|
+
})
|
|
1666
|
+
|
|
1667
|
+
describe('priority-based demotion', () => {
|
|
1668
|
+
it('should respect entry priority when demoting', async () => {
|
|
1669
|
+
mockCacheLayer.delete.mockResolvedValue(true)
|
|
1670
|
+
|
|
1671
|
+
const orchestrator = createTieredStorageOrchestrator({
|
|
1672
|
+
cacheLayer: mockCacheLayer as unknown as CacheLayer,
|
|
1673
|
+
doStorage: mockDOStorage as unknown as DurableObjectStorage,
|
|
1674
|
+
r2Layer: mockR2Layer as unknown as R2StorageLayer,
|
|
1675
|
+
hotTtlMs: 0, // Immediate expiry
|
|
1676
|
+
autoDemote: true,
|
|
1677
|
+
})
|
|
1678
|
+
|
|
1679
|
+
// Add entries with different priorities
|
|
1680
|
+
await orchestrator.write('low-priority', new Uint8Array([1]), { tier: 'hot' })
|
|
1681
|
+
await orchestrator.write('high-priority', new Uint8Array([2]), { tier: 'hot' })
|
|
1682
|
+
|
|
1683
|
+
// Set priority
|
|
1684
|
+
// @ts-expect-error - Method doesn't exist yet (RED test)
|
|
1685
|
+
orchestrator.setEntryPriority('high-priority', 'high')
|
|
1686
|
+
// @ts-expect-error - Method doesn't exist yet (RED test)
|
|
1687
|
+
orchestrator.setEntryPriority('low-priority', 'low')
|
|
1688
|
+
|
|
1689
|
+
// Run demotion cycle with limit of 1
|
|
1690
|
+
const events = await orchestrator.runDemotionCycle(1)
|
|
1691
|
+
|
|
1692
|
+
// Low priority entry should be demoted first
|
|
1693
|
+
expect(events[0].key).toBe('low-priority')
|
|
1694
|
+
})
|
|
1695
|
+
})
|
|
1696
|
+
})
|
|
1697
|
+
|
|
1698
|
+
describe('Concurrent Operations', () => {
|
|
1699
|
+
describe('batch operations', () => {
|
|
1700
|
+
it('should support batch promotion', async () => {
|
|
1701
|
+
const data = new Uint8Array([1, 2, 3])
|
|
1702
|
+
mockR2Layer.get.mockResolvedValue(data)
|
|
1703
|
+
|
|
1704
|
+
const orchestrator = createTieredStorageOrchestrator({
|
|
1705
|
+
cacheLayer: mockCacheLayer as unknown as CacheLayer,
|
|
1706
|
+
doStorage: mockDOStorage as unknown as DurableObjectStorage,
|
|
1707
|
+
r2Layer: mockR2Layer as unknown as R2StorageLayer,
|
|
1708
|
+
})
|
|
1709
|
+
|
|
1710
|
+
// @ts-expect-error - Method doesn't exist yet (RED test)
|
|
1711
|
+
const results = await orchestrator.batchPromoteToWarm(['page-1', 'page-2', 'page-3'])
|
|
1712
|
+
|
|
1713
|
+
expect(results.length).toBe(3)
|
|
1714
|
+
expect(results.filter((r: TierMigrationEvent) => r.success).length).toBeGreaterThan(0)
|
|
1715
|
+
})
|
|
1716
|
+
|
|
1717
|
+
it('should support batch demotion', async () => {
|
|
1718
|
+
const data = new Uint8Array([1, 2, 3])
|
|
1719
|
+
mockDOStorage.get.mockResolvedValue(data)
|
|
1720
|
+
|
|
1721
|
+
const orchestrator = createTieredStorageOrchestrator({
|
|
1722
|
+
cacheLayer: mockCacheLayer as unknown as CacheLayer,
|
|
1723
|
+
doStorage: mockDOStorage as unknown as DurableObjectStorage,
|
|
1724
|
+
r2Layer: mockR2Layer as unknown as R2StorageLayer,
|
|
1725
|
+
})
|
|
1726
|
+
|
|
1727
|
+
// Add entries to warm tier
|
|
1728
|
+
await orchestrator.write('page-1', data, { tier: 'warm' })
|
|
1729
|
+
await orchestrator.write('page-2', data, { tier: 'warm' })
|
|
1730
|
+
await orchestrator.write('page-3', data, { tier: 'warm' })
|
|
1731
|
+
|
|
1732
|
+
// @ts-expect-error - Method doesn't exist yet (RED test)
|
|
1733
|
+
const results = await orchestrator.batchDemoteFromWarm(['page-1', 'page-2', 'page-3'])
|
|
1734
|
+
|
|
1735
|
+
expect(results.length).toBe(3)
|
|
1736
|
+
expect(results.filter((r: TierMigrationEvent) => r.success).length).toBeGreaterThan(0)
|
|
1737
|
+
})
|
|
1738
|
+
})
|
|
1739
|
+
})
|
|
1740
|
+
|
|
1741
|
+
describe('Telemetry and Observability', () => {
|
|
1742
|
+
describe('health checks', () => {
|
|
1743
|
+
it('should perform comprehensive health check across all tiers', async () => {
|
|
1744
|
+
const orchestrator = createTieredStorageOrchestrator({
|
|
1745
|
+
cacheLayer: mockCacheLayer as unknown as CacheLayer,
|
|
1746
|
+
doStorage: mockDOStorage as unknown as DurableObjectStorage,
|
|
1747
|
+
r2Layer: mockR2Layer as unknown as R2StorageLayer,
|
|
1748
|
+
})
|
|
1749
|
+
|
|
1750
|
+
// @ts-expect-error - Method doesn't exist yet (RED test)
|
|
1751
|
+
const health = await orchestrator.healthCheck()
|
|
1752
|
+
|
|
1753
|
+
expect(health).toMatchObject({
|
|
1754
|
+
overall: expect.stringMatching(/healthy|degraded|unhealthy/),
|
|
1755
|
+
tiers: {
|
|
1756
|
+
hot: expect.objectContaining({ status: expect.any(String) }),
|
|
1757
|
+
warm: expect.objectContaining({ status: expect.any(String) }),
|
|
1758
|
+
cold: expect.objectContaining({ status: expect.any(String) }),
|
|
1759
|
+
},
|
|
1760
|
+
latency: expect.objectContaining({
|
|
1761
|
+
hot: expect.any(Number),
|
|
1762
|
+
warm: expect.any(Number),
|
|
1763
|
+
cold: expect.any(Number),
|
|
1764
|
+
}),
|
|
1765
|
+
})
|
|
1766
|
+
})
|
|
1767
|
+
})
|
|
1768
|
+
})
|
|
1769
|
+
|
|
1770
|
+
/**
|
|
1771
|
+
* ========================================================================
|
|
1772
|
+
* Additional RED Tests: Unified Tiering Orchestrator
|
|
1773
|
+
* Issue: postgres-h91k [TIER-RED]
|
|
1774
|
+
*
|
|
1775
|
+
* These tests cover additional edge cases and advanced features.
|
|
1776
|
+
* ========================================================================
|
|
1777
|
+
*/
|
|
1778
|
+
|
|
1779
|
+
describe('Memory Pressure Management', () => {
|
|
1780
|
+
describe('getMemoryUsage()', () => {
|
|
1781
|
+
it('should calculate total memory usage across all tiers', async () => {
|
|
1782
|
+
const orchestrator = createTieredStorageOrchestrator({
|
|
1783
|
+
cacheLayer: mockCacheLayer as unknown as CacheLayer,
|
|
1784
|
+
doStorage: mockDOStorage as unknown as DurableObjectStorage,
|
|
1785
|
+
r2Layer: mockR2Layer as unknown as R2StorageLayer,
|
|
1786
|
+
})
|
|
1787
|
+
|
|
1788
|
+
await orchestrator.write('page-1', new Uint8Array(1000), { tier: 'hot' })
|
|
1789
|
+
await orchestrator.write('page-2', new Uint8Array(2000), { tier: 'warm' })
|
|
1790
|
+
|
|
1791
|
+
// @ts-expect-error - Method doesn't exist yet (RED test)
|
|
1792
|
+
const usage = orchestrator.getMemoryUsage()
|
|
1793
|
+
|
|
1794
|
+
expect(usage).toMatchObject({
|
|
1795
|
+
hot: expect.any(Number),
|
|
1796
|
+
warm: expect.any(Number),
|
|
1797
|
+
cold: expect.any(Number),
|
|
1798
|
+
total: expect.any(Number),
|
|
1799
|
+
limit: expect.any(Number),
|
|
1800
|
+
utilizationPercent: expect.any(Number),
|
|
1801
|
+
})
|
|
1802
|
+
expect(usage.hot).toBe(1000)
|
|
1803
|
+
expect(usage.warm).toBe(2000)
|
|
1804
|
+
})
|
|
1805
|
+
})
|
|
1806
|
+
|
|
1807
|
+
describe('setMemoryLimit()', () => {
|
|
1808
|
+
it('should configure memory limits for each tier', async () => {
|
|
1809
|
+
const orchestrator = createTieredStorageOrchestrator({
|
|
1810
|
+
cacheLayer: mockCacheLayer as unknown as CacheLayer,
|
|
1811
|
+
doStorage: mockDOStorage as unknown as DurableObjectStorage,
|
|
1812
|
+
r2Layer: mockR2Layer as unknown as R2StorageLayer,
|
|
1813
|
+
})
|
|
1814
|
+
|
|
1815
|
+
// @ts-expect-error - Method doesn't exist yet (RED test)
|
|
1816
|
+
orchestrator.setMemoryLimit({
|
|
1817
|
+
hot: 10 * 1024 * 1024, // 10MB
|
|
1818
|
+
warm: 50 * 1024 * 1024, // 50MB
|
|
1819
|
+
})
|
|
1820
|
+
|
|
1821
|
+
// @ts-expect-error - Method doesn't exist yet (RED test)
|
|
1822
|
+
const limits = orchestrator.getMemoryLimits()
|
|
1823
|
+
|
|
1824
|
+
expect(limits.hot).toBe(10 * 1024 * 1024)
|
|
1825
|
+
expect(limits.warm).toBe(50 * 1024 * 1024)
|
|
1826
|
+
})
|
|
1827
|
+
|
|
1828
|
+
it('should trigger eviction when memory limit exceeded', async () => {
|
|
1829
|
+
mockCacheLayer.delete.mockResolvedValue(true)
|
|
1830
|
+
|
|
1831
|
+
const orchestrator = createTieredStorageOrchestrator({
|
|
1832
|
+
cacheLayer: mockCacheLayer as unknown as CacheLayer,
|
|
1833
|
+
doStorage: mockDOStorage as unknown as DurableObjectStorage,
|
|
1834
|
+
r2Layer: mockR2Layer as unknown as R2StorageLayer,
|
|
1835
|
+
})
|
|
1836
|
+
|
|
1837
|
+
// @ts-expect-error - Method doesn't exist yet (RED test)
|
|
1838
|
+
orchestrator.setMemoryLimit({ hot: 100 }) // Very small limit
|
|
1839
|
+
|
|
1840
|
+
// Write more than the limit
|
|
1841
|
+
await orchestrator.write('page-1', new Uint8Array(50), { tier: 'hot' })
|
|
1842
|
+
await orchestrator.write('page-2', new Uint8Array(60), { tier: 'hot' })
|
|
1843
|
+
|
|
1844
|
+
// Should have triggered auto-eviction
|
|
1845
|
+
// @ts-expect-error - Method doesn't exist yet (RED test)
|
|
1846
|
+
const usage = orchestrator.getMemoryUsage()
|
|
1847
|
+
expect(usage.hot).toBeLessThanOrEqual(100)
|
|
1848
|
+
})
|
|
1849
|
+
})
|
|
1850
|
+
})
|
|
1851
|
+
|
|
1852
|
+
describe('Read Coalescing', () => {
|
|
1853
|
+
describe('coalesced reads for concurrent requests', () => {
|
|
1854
|
+
it('should coalesce multiple concurrent reads for the same key', async () => {
|
|
1855
|
+
const data = new Uint8Array([1, 2, 3])
|
|
1856
|
+
let fetchCount = 0
|
|
1857
|
+
mockCacheLayer.get.mockResolvedValue(null)
|
|
1858
|
+
mockDOStorage.get.mockImplementation(async () => {
|
|
1859
|
+
fetchCount++
|
|
1860
|
+
await new Promise(resolve => setTimeout(resolve, 10))
|
|
1861
|
+
return data
|
|
1862
|
+
})
|
|
1863
|
+
|
|
1864
|
+
const orchestrator = createTieredStorageOrchestrator({
|
|
1865
|
+
cacheLayer: mockCacheLayer as unknown as CacheLayer,
|
|
1866
|
+
doStorage: mockDOStorage as unknown as DurableObjectStorage,
|
|
1867
|
+
r2Layer: mockR2Layer as unknown as R2StorageLayer,
|
|
1868
|
+
// @ts-expect-error - Config option doesn't exist yet (RED test)
|
|
1869
|
+
enableReadCoalescing: true,
|
|
1870
|
+
})
|
|
1871
|
+
|
|
1872
|
+
// Trigger 5 concurrent reads for the same key
|
|
1873
|
+
const results = await Promise.all([
|
|
1874
|
+
orchestrator.read('page-123'),
|
|
1875
|
+
orchestrator.read('page-123'),
|
|
1876
|
+
orchestrator.read('page-123'),
|
|
1877
|
+
orchestrator.read('page-123'),
|
|
1878
|
+
orchestrator.read('page-123'),
|
|
1879
|
+
])
|
|
1880
|
+
|
|
1881
|
+
// All should return the same data
|
|
1882
|
+
for (const result of results) {
|
|
1883
|
+
expect(result.data).toEqual(data)
|
|
1884
|
+
}
|
|
1885
|
+
|
|
1886
|
+
// But only ONE fetch should have been made
|
|
1887
|
+
expect(fetchCount).toBe(1)
|
|
1888
|
+
})
|
|
1889
|
+
})
|
|
1890
|
+
})
|
|
1891
|
+
|
|
1892
|
+
describe('Prefetching', () => {
|
|
1893
|
+
describe('prefetch()', () => {
|
|
1894
|
+
it('should prefetch data into hot tier', async () => {
|
|
1895
|
+
const data = new Uint8Array([1, 2, 3])
|
|
1896
|
+
mockR2Layer.get.mockResolvedValue(data)
|
|
1897
|
+
mockDOStorage.get.mockResolvedValue(undefined)
|
|
1898
|
+
mockCacheLayer.get.mockResolvedValue(null)
|
|
1899
|
+
|
|
1900
|
+
const orchestrator = createTieredStorageOrchestrator({
|
|
1901
|
+
cacheLayer: mockCacheLayer as unknown as CacheLayer,
|
|
1902
|
+
doStorage: mockDOStorage as unknown as DurableObjectStorage,
|
|
1903
|
+
r2Layer: mockR2Layer as unknown as R2StorageLayer,
|
|
1904
|
+
})
|
|
1905
|
+
|
|
1906
|
+
// @ts-expect-error - Method doesn't exist yet (RED test)
|
|
1907
|
+
const result = await orchestrator.prefetch(['page-1', 'page-2', 'page-3'], { targetTier: 'hot' })
|
|
1908
|
+
|
|
1909
|
+
expect(result.prefetchedCount).toBe(3)
|
|
1910
|
+
expect(result.failedKeys).toHaveLength(0)
|
|
1911
|
+
})
|
|
1912
|
+
|
|
1913
|
+
it('should respect concurrent prefetch limit', async () => {
|
|
1914
|
+
const data = new Uint8Array([1, 2, 3])
|
|
1915
|
+
let concurrentOps = 0
|
|
1916
|
+
let maxConcurrent = 0
|
|
1917
|
+
|
|
1918
|
+
mockR2Layer.get.mockImplementation(async () => {
|
|
1919
|
+
concurrentOps++
|
|
1920
|
+
maxConcurrent = Math.max(maxConcurrent, concurrentOps)
|
|
1921
|
+
await new Promise(resolve => setTimeout(resolve, 10))
|
|
1922
|
+
concurrentOps--
|
|
1923
|
+
return data
|
|
1924
|
+
})
|
|
1925
|
+
|
|
1926
|
+
const orchestrator = createTieredStorageOrchestrator({
|
|
1927
|
+
cacheLayer: mockCacheLayer as unknown as CacheLayer,
|
|
1928
|
+
doStorage: mockDOStorage as unknown as DurableObjectStorage,
|
|
1929
|
+
r2Layer: mockR2Layer as unknown as R2StorageLayer,
|
|
1930
|
+
})
|
|
1931
|
+
|
|
1932
|
+
const keys = Array.from({ length: 20 }, (_, i) => `page-${i}`)
|
|
1933
|
+
|
|
1934
|
+
// @ts-expect-error - Method doesn't exist yet (RED test)
|
|
1935
|
+
await orchestrator.prefetch(keys, { targetTier: 'warm', maxConcurrency: 5 })
|
|
1936
|
+
|
|
1937
|
+
expect(maxConcurrent).toBeLessThanOrEqual(5)
|
|
1938
|
+
})
|
|
1939
|
+
})
|
|
1940
|
+
})
|
|
1941
|
+
|
|
1942
|
+
describe('Event Streaming', () => {
|
|
1943
|
+
describe('subscribeToEvents()', () => {
|
|
1944
|
+
it('should stream tier migration events', async () => {
|
|
1945
|
+
const data = new Uint8Array([1, 2, 3])
|
|
1946
|
+
mockR2Layer.get.mockResolvedValue(data)
|
|
1947
|
+
|
|
1948
|
+
const orchestrator = createTieredStorageOrchestrator({
|
|
1949
|
+
cacheLayer: mockCacheLayer as unknown as CacheLayer,
|
|
1950
|
+
doStorage: mockDOStorage as unknown as DurableObjectStorage,
|
|
1951
|
+
r2Layer: mockR2Layer as unknown as R2StorageLayer,
|
|
1952
|
+
})
|
|
1953
|
+
|
|
1954
|
+
const events: TierMigrationEvent[] = []
|
|
1955
|
+
// @ts-expect-error - Method doesn't exist yet (RED test)
|
|
1956
|
+
const unsubscribe = orchestrator.subscribeToEvents((event: TierMigrationEvent) => {
|
|
1957
|
+
events.push(event)
|
|
1958
|
+
})
|
|
1959
|
+
|
|
1960
|
+
await orchestrator.promoteToWarm('page-1')
|
|
1961
|
+
await orchestrator.promoteToWarm('page-2')
|
|
1962
|
+
|
|
1963
|
+
expect(events.length).toBe(2)
|
|
1964
|
+
expect(events[0].key).toBe('page-1')
|
|
1965
|
+
expect(events[1].key).toBe('page-2')
|
|
1966
|
+
|
|
1967
|
+
unsubscribe()
|
|
1968
|
+
|
|
1969
|
+
// After unsubscribe, should not receive more events
|
|
1970
|
+
await orchestrator.promoteToWarm('page-3')
|
|
1971
|
+
expect(events.length).toBe(2)
|
|
1972
|
+
})
|
|
1973
|
+
|
|
1974
|
+
it('should filter events by type', async () => {
|
|
1975
|
+
mockCacheLayer.delete.mockResolvedValue(true)
|
|
1976
|
+
|
|
1977
|
+
const orchestrator = createTieredStorageOrchestrator({
|
|
1978
|
+
cacheLayer: mockCacheLayer as unknown as CacheLayer,
|
|
1979
|
+
doStorage: mockDOStorage as unknown as DurableObjectStorage,
|
|
1980
|
+
r2Layer: mockR2Layer as unknown as R2StorageLayer,
|
|
1981
|
+
})
|
|
1982
|
+
|
|
1983
|
+
const promotionEvents: TierMigrationEvent[] = []
|
|
1984
|
+
// @ts-expect-error - Method doesn't exist yet (RED test)
|
|
1985
|
+
orchestrator.subscribeToEvents(
|
|
1986
|
+
(event: TierMigrationEvent) => promotionEvents.push(event),
|
|
1987
|
+
{ filter: { type: 'promotion' } }
|
|
1988
|
+
)
|
|
1989
|
+
|
|
1990
|
+
await orchestrator.write('page-1', new Uint8Array([1, 2, 3]), { tier: 'hot' })
|
|
1991
|
+
await orchestrator.demoteFromHot('page-1')
|
|
1992
|
+
|
|
1993
|
+
// Should not have captured demotion events
|
|
1994
|
+
expect(promotionEvents.every(e => e.toTier !== 'warm' || e.fromTier !== 'hot')).toBe(true)
|
|
1995
|
+
})
|
|
1996
|
+
})
|
|
1997
|
+
})
|
|
1998
|
+
|
|
1999
|
+
describe('Snapshot and Restore', () => {
|
|
2000
|
+
describe('createSnapshot()', () => {
|
|
2001
|
+
it('should create a serializable snapshot of storage state', async () => {
|
|
2002
|
+
const orchestrator = createTieredStorageOrchestrator({
|
|
2003
|
+
cacheLayer: mockCacheLayer as unknown as CacheLayer,
|
|
2004
|
+
doStorage: mockDOStorage as unknown as DurableObjectStorage,
|
|
2005
|
+
r2Layer: mockR2Layer as unknown as R2StorageLayer,
|
|
2006
|
+
})
|
|
2007
|
+
|
|
2008
|
+
await orchestrator.write('page-1', new Uint8Array([1, 2, 3]), { tier: 'warm' })
|
|
2009
|
+
await orchestrator.write('page-2', new Uint8Array([4, 5, 6]), { tier: 'warm' })
|
|
2010
|
+
|
|
2011
|
+
// @ts-expect-error - Method doesn't exist yet (RED test)
|
|
2012
|
+
const snapshot = await orchestrator.createSnapshot()
|
|
2013
|
+
|
|
2014
|
+
expect(snapshot).toMatchObject({
|
|
2015
|
+
version: expect.any(Number),
|
|
2016
|
+
timestamp: expect.any(Number),
|
|
2017
|
+
index: expect.any(Array),
|
|
2018
|
+
stats: expect.any(Object),
|
|
2019
|
+
})
|
|
2020
|
+
expect(snapshot.index.length).toBe(2)
|
|
2021
|
+
})
|
|
2022
|
+
})
|
|
2023
|
+
|
|
2024
|
+
describe('restoreFromSnapshot()', () => {
|
|
2025
|
+
it('should restore storage state from snapshot', async () => {
|
|
2026
|
+
const orchestrator = createTieredStorageOrchestrator({
|
|
2027
|
+
cacheLayer: mockCacheLayer as unknown as CacheLayer,
|
|
2028
|
+
doStorage: mockDOStorage as unknown as DurableObjectStorage,
|
|
2029
|
+
r2Layer: mockR2Layer as unknown as R2StorageLayer,
|
|
2030
|
+
})
|
|
2031
|
+
|
|
2032
|
+
const snapshot = {
|
|
2033
|
+
version: 1,
|
|
2034
|
+
timestamp: Date.now(),
|
|
2035
|
+
index: [
|
|
2036
|
+
{ key: 'page-1', tier: 'warm' as StorageTier, size: 100, lastAccess: Date.now(), accessCount: 5, created: Date.now(), modified: Date.now(), dirty: false },
|
|
2037
|
+
{ key: 'page-2', tier: 'cold' as StorageTier, size: 200, lastAccess: Date.now(), accessCount: 2, created: Date.now(), modified: Date.now(), dirty: false },
|
|
2038
|
+
],
|
|
2039
|
+
stats: {},
|
|
2040
|
+
}
|
|
2041
|
+
|
|
2042
|
+
// @ts-expect-error - Method doesn't exist yet (RED test)
|
|
2043
|
+
await orchestrator.restoreFromSnapshot(snapshot)
|
|
2044
|
+
|
|
2045
|
+
expect(orchestrator.hasInIndex('page-1')).toBe(true)
|
|
2046
|
+
expect(orchestrator.hasInIndex('page-2')).toBe(true)
|
|
2047
|
+
expect(orchestrator.getTier('page-1')).toBe('warm')
|
|
2048
|
+
expect(orchestrator.getTier('page-2')).toBe('cold')
|
|
2049
|
+
})
|
|
2050
|
+
})
|
|
2051
|
+
})
|
|
2052
|
+
|
|
2053
|
+
describe('Tier Warmup Strategies', () => {
|
|
2054
|
+
describe('warmup()', () => {
|
|
2055
|
+
it('should warm up cold data to warm tier based on strategy', async () => {
|
|
2056
|
+
const data = new Uint8Array([1, 2, 3])
|
|
2057
|
+
mockR2Layer.get.mockResolvedValue(data)
|
|
2058
|
+
mockR2Layer.list.mockResolvedValue({
|
|
2059
|
+
objects: [
|
|
2060
|
+
{ key: 'page-1', size: 100 },
|
|
2061
|
+
{ key: 'page-2', size: 200 },
|
|
2062
|
+
{ key: 'page-3', size: 300 },
|
|
2063
|
+
],
|
|
2064
|
+
truncated: false,
|
|
2065
|
+
})
|
|
2066
|
+
|
|
2067
|
+
const orchestrator = createTieredStorageOrchestrator({
|
|
2068
|
+
cacheLayer: mockCacheLayer as unknown as CacheLayer,
|
|
2069
|
+
doStorage: mockDOStorage as unknown as DurableObjectStorage,
|
|
2070
|
+
r2Layer: mockR2Layer as unknown as R2StorageLayer,
|
|
2071
|
+
})
|
|
2072
|
+
|
|
2073
|
+
// @ts-expect-error - Method doesn't exist yet (RED test)
|
|
2074
|
+
const result = await orchestrator.warmup({
|
|
2075
|
+
strategy: 'all',
|
|
2076
|
+
targetTier: 'warm',
|
|
2077
|
+
maxBytes: 1000,
|
|
2078
|
+
})
|
|
2079
|
+
|
|
2080
|
+
expect(result.warmedUp).toBeGreaterThan(0)
|
|
2081
|
+
expect(result.bytesTransferred).toBeLessThanOrEqual(1000)
|
|
2082
|
+
})
|
|
2083
|
+
|
|
2084
|
+
it('should warm up based on recent access patterns', async () => {
|
|
2085
|
+
const data = new Uint8Array([1, 2, 3])
|
|
2086
|
+
mockR2Layer.get.mockResolvedValue(data)
|
|
2087
|
+
|
|
2088
|
+
const orchestrator = createTieredStorageOrchestrator({
|
|
2089
|
+
cacheLayer: mockCacheLayer as unknown as CacheLayer,
|
|
2090
|
+
doStorage: mockDOStorage as unknown as DurableObjectStorage,
|
|
2091
|
+
r2Layer: mockR2Layer as unknown as R2StorageLayer,
|
|
2092
|
+
})
|
|
2093
|
+
|
|
2094
|
+
// Setup: add some entries to index with varying access counts
|
|
2095
|
+
// @ts-expect-error - Accessing private for test setup
|
|
2096
|
+
orchestrator.storageIndex?.set('page-high', {
|
|
2097
|
+
key: 'page-high', tier: 'cold', size: 100, accessCount: 100,
|
|
2098
|
+
lastAccess: Date.now(), created: Date.now(), modified: Date.now(), dirty: false
|
|
2099
|
+
})
|
|
2100
|
+
// @ts-expect-error - Accessing private for test setup
|
|
2101
|
+
orchestrator.storageIndex?.set('page-low', {
|
|
2102
|
+
key: 'page-low', tier: 'cold', size: 100, accessCount: 1,
|
|
2103
|
+
lastAccess: Date.now(), created: Date.now(), modified: Date.now(), dirty: false
|
|
2104
|
+
})
|
|
2105
|
+
|
|
2106
|
+
// @ts-expect-error - Method doesn't exist yet (RED test)
|
|
2107
|
+
const result = await orchestrator.warmup({
|
|
2108
|
+
strategy: 'most-accessed',
|
|
2109
|
+
targetTier: 'warm',
|
|
2110
|
+
limit: 1,
|
|
2111
|
+
})
|
|
2112
|
+
|
|
2113
|
+
expect(result.keys).toContain('page-high')
|
|
2114
|
+
expect(result.keys).not.toContain('page-low')
|
|
2115
|
+
})
|
|
2116
|
+
})
|
|
2117
|
+
})
|
|
2118
|
+
|
|
2119
|
+
describe('Garbage Collection', () => {
|
|
2120
|
+
describe('garbageCollect()', () => {
|
|
2121
|
+
it('should remove orphaned entries from storage index', async () => {
|
|
2122
|
+
mockDOStorage.get.mockResolvedValue(undefined) // Nothing in warm tier
|
|
2123
|
+
mockR2Layer.get.mockResolvedValue(null) // Nothing in cold tier
|
|
2124
|
+
|
|
2125
|
+
const orchestrator = createTieredStorageOrchestrator({
|
|
2126
|
+
cacheLayer: mockCacheLayer as unknown as CacheLayer,
|
|
2127
|
+
doStorage: mockDOStorage as unknown as DurableObjectStorage,
|
|
2128
|
+
r2Layer: mockR2Layer as unknown as R2StorageLayer,
|
|
2129
|
+
})
|
|
2130
|
+
|
|
2131
|
+
// Manually add orphaned entry to index
|
|
2132
|
+
// @ts-expect-error - Accessing private for test setup
|
|
2133
|
+
orchestrator.storageIndex?.set('orphaned-page', {
|
|
2134
|
+
key: 'orphaned-page', tier: 'warm', size: 100, accessCount: 1,
|
|
2135
|
+
lastAccess: Date.now(), created: Date.now(), modified: Date.now(), dirty: false
|
|
2136
|
+
})
|
|
2137
|
+
|
|
2138
|
+
// @ts-expect-error - Method doesn't exist yet (RED test)
|
|
2139
|
+
const result = await orchestrator.garbageCollect()
|
|
2140
|
+
|
|
2141
|
+
expect(result.removedEntries).toBe(1)
|
|
2142
|
+
expect(orchestrator.hasInIndex('orphaned-page')).toBe(false)
|
|
2143
|
+
})
|
|
2144
|
+
|
|
2145
|
+
it('should compact cold tier by removing duplicates', async () => {
|
|
2146
|
+
const orchestrator = createTieredStorageOrchestrator({
|
|
2147
|
+
cacheLayer: mockCacheLayer as unknown as CacheLayer,
|
|
2148
|
+
doStorage: mockDOStorage as unknown as DurableObjectStorage,
|
|
2149
|
+
r2Layer: mockR2Layer as unknown as R2StorageLayer,
|
|
2150
|
+
})
|
|
2151
|
+
|
|
2152
|
+
// @ts-expect-error - Method doesn't exist yet (RED test)
|
|
2153
|
+
const result = await orchestrator.garbageCollect({ compactCold: true })
|
|
2154
|
+
|
|
2155
|
+
expect(result.compactionStats).toBeDefined()
|
|
2156
|
+
expect(result.compactionStats.bytesReclaimed).toBeGreaterThanOrEqual(0)
|
|
2157
|
+
})
|
|
2158
|
+
})
|
|
2159
|
+
})
|
|
2160
|
+
|
|
2161
|
+
describe('Rate Limiting', () => {
|
|
2162
|
+
describe('tier operation rate limiting', () => {
|
|
2163
|
+
it('should respect rate limits for R2 operations', async () => {
|
|
2164
|
+
const data = new Uint8Array([1, 2, 3])
|
|
2165
|
+
mockR2Layer.get.mockResolvedValue(data)
|
|
2166
|
+
|
|
2167
|
+
const orchestrator = createTieredStorageOrchestrator({
|
|
2168
|
+
cacheLayer: mockCacheLayer as unknown as CacheLayer,
|
|
2169
|
+
doStorage: mockDOStorage as unknown as DurableObjectStorage,
|
|
2170
|
+
r2Layer: mockR2Layer as unknown as R2StorageLayer,
|
|
2171
|
+
// @ts-expect-error - Config option doesn't exist yet (RED test)
|
|
2172
|
+
rateLimits: {
|
|
2173
|
+
cold: {
|
|
2174
|
+
readsPerSecond: 10,
|
|
2175
|
+
writesPerSecond: 5,
|
|
2176
|
+
},
|
|
2177
|
+
},
|
|
2178
|
+
})
|
|
2179
|
+
|
|
2180
|
+
const startTime = Date.now()
|
|
2181
|
+
|
|
2182
|
+
// Attempt 20 reads rapidly
|
|
2183
|
+
const promises = []
|
|
2184
|
+
for (let i = 0; i < 20; i++) {
|
|
2185
|
+
promises.push(orchestrator.read(`page-${i}`))
|
|
2186
|
+
}
|
|
2187
|
+
|
|
2188
|
+
await Promise.all(promises)
|
|
2189
|
+
const endTime = Date.now()
|
|
2190
|
+
|
|
2191
|
+
// Should have taken at least 1 second due to rate limiting (20 reads at 10/sec)
|
|
2192
|
+
expect(endTime - startTime).toBeGreaterThanOrEqual(1000)
|
|
2193
|
+
})
|
|
2194
|
+
})
|
|
2195
|
+
})
|
|
2196
|
+
|
|
2197
|
+
describe('Compression', () => {
|
|
2198
|
+
describe('compressed storage', () => {
|
|
2199
|
+
it('should compress data before storing in cold tier', async () => {
|
|
2200
|
+
const orchestrator = createTieredStorageOrchestrator({
|
|
2201
|
+
cacheLayer: mockCacheLayer as unknown as CacheLayer,
|
|
2202
|
+
doStorage: mockDOStorage as unknown as DurableObjectStorage,
|
|
2203
|
+
r2Layer: mockR2Layer as unknown as R2StorageLayer,
|
|
2204
|
+
// @ts-expect-error - Config option doesn't exist yet (RED test)
|
|
2205
|
+
compression: {
|
|
2206
|
+
enabled: true,
|
|
2207
|
+
algorithm: 'gzip',
|
|
2208
|
+
minSizeBytes: 100,
|
|
2209
|
+
},
|
|
2210
|
+
})
|
|
2211
|
+
|
|
2212
|
+
const largeData = new Uint8Array(1000).fill(65) // Repetitive data compresses well
|
|
2213
|
+
|
|
2214
|
+
await orchestrator.write('page-compressible', largeData, { tier: 'cold' })
|
|
2215
|
+
|
|
2216
|
+
// The data stored in R2 should be compressed
|
|
2217
|
+
expect(mockR2Layer.put).toHaveBeenCalledWith(
|
|
2218
|
+
'page-compressible',
|
|
2219
|
+
expect.any(Uint8Array),
|
|
2220
|
+
expect.objectContaining({
|
|
2221
|
+
customMetadata: expect.objectContaining({
|
|
2222
|
+
compressed: 'true',
|
|
2223
|
+
originalSize: '1000',
|
|
2224
|
+
}),
|
|
2225
|
+
})
|
|
2226
|
+
)
|
|
2227
|
+
|
|
2228
|
+
// The stored data should be smaller than original
|
|
2229
|
+
const storedData = mockR2Layer.put.mock.calls[0][1] as Uint8Array
|
|
2230
|
+
expect(storedData.length).toBeLessThan(largeData.length)
|
|
2231
|
+
})
|
|
2232
|
+
|
|
2233
|
+
it('should decompress data when reading from cold tier', async () => {
|
|
2234
|
+
// Mock compressed data
|
|
2235
|
+
const originalData = new Uint8Array(1000).fill(65)
|
|
2236
|
+
const compressedData = new Uint8Array([/* simulated compressed data */])
|
|
2237
|
+
|
|
2238
|
+
mockCacheLayer.get.mockResolvedValue(null)
|
|
2239
|
+
mockDOStorage.get.mockResolvedValue(undefined)
|
|
2240
|
+
mockR2Layer.get.mockResolvedValue(compressedData)
|
|
2241
|
+
// @ts-expect-error - Mock method doesn't have full typing
|
|
2242
|
+
mockR2Layer.head = vi.fn().mockResolvedValue({
|
|
2243
|
+
customMetadata: { compressed: 'true', originalSize: '1000' },
|
|
2244
|
+
})
|
|
2245
|
+
|
|
2246
|
+
const orchestrator = createTieredStorageOrchestrator({
|
|
2247
|
+
cacheLayer: mockCacheLayer as unknown as CacheLayer,
|
|
2248
|
+
doStorage: mockDOStorage as unknown as DurableObjectStorage,
|
|
2249
|
+
r2Layer: mockR2Layer as unknown as R2StorageLayer,
|
|
2250
|
+
// @ts-expect-error - Config option doesn't exist yet (RED test)
|
|
2251
|
+
compression: {
|
|
2252
|
+
enabled: true,
|
|
2253
|
+
algorithm: 'gzip',
|
|
2254
|
+
},
|
|
2255
|
+
})
|
|
2256
|
+
|
|
2257
|
+
// @ts-expect-error - Method should auto-decompress
|
|
2258
|
+
const result = await orchestrator.read('page-compressed', { decompress: true })
|
|
2259
|
+
|
|
2260
|
+
expect(result.data?.length).toBe(1000)
|
|
2261
|
+
})
|
|
2262
|
+
})
|
|
2263
|
+
})
|
|
2264
|
+
|
|
2265
|
+
describe('Tier Pinning', () => {
|
|
2266
|
+
describe('pinToTier()', () => {
|
|
2267
|
+
it('should prevent data from being demoted', async () => {
|
|
2268
|
+
mockCacheLayer.delete.mockResolvedValue(true)
|
|
2269
|
+
|
|
2270
|
+
const orchestrator = createTieredStorageOrchestrator({
|
|
2271
|
+
cacheLayer: mockCacheLayer as unknown as CacheLayer,
|
|
2272
|
+
doStorage: mockDOStorage as unknown as DurableObjectStorage,
|
|
2273
|
+
r2Layer: mockR2Layer as unknown as R2StorageLayer,
|
|
2274
|
+
hotTtlMs: 0, // Immediate expiry
|
|
2275
|
+
autoDemote: true,
|
|
2276
|
+
})
|
|
2277
|
+
|
|
2278
|
+
await orchestrator.write('pinned-page', new Uint8Array([1, 2, 3]), { tier: 'hot' })
|
|
2279
|
+
|
|
2280
|
+
// @ts-expect-error - Method doesn't exist yet (RED test)
|
|
2281
|
+
orchestrator.pinToTier('pinned-page', 'hot')
|
|
2282
|
+
|
|
2283
|
+
// Run demotion cycle
|
|
2284
|
+
const events = await orchestrator.runDemotionCycle()
|
|
2285
|
+
|
|
2286
|
+
// Pinned page should NOT have been demoted
|
|
2287
|
+
expect(events.find(e => e.key === 'pinned-page')).toBeUndefined()
|
|
2288
|
+
expect(orchestrator.getTier('pinned-page')).toBe('hot')
|
|
2289
|
+
})
|
|
2290
|
+
})
|
|
2291
|
+
|
|
2292
|
+
describe('unpinFromTier()', () => {
|
|
2293
|
+
it('should allow data to be demoted again', async () => {
|
|
2294
|
+
mockCacheLayer.delete.mockResolvedValue(true)
|
|
2295
|
+
|
|
2296
|
+
const orchestrator = createTieredStorageOrchestrator({
|
|
2297
|
+
cacheLayer: mockCacheLayer as unknown as CacheLayer,
|
|
2298
|
+
doStorage: mockDOStorage as unknown as DurableObjectStorage,
|
|
2299
|
+
r2Layer: mockR2Layer as unknown as R2StorageLayer,
|
|
2300
|
+
hotTtlMs: 0,
|
|
2301
|
+
autoDemote: true,
|
|
2302
|
+
})
|
|
2303
|
+
|
|
2304
|
+
await orchestrator.write('page-1', new Uint8Array([1, 2, 3]), { tier: 'hot' })
|
|
2305
|
+
|
|
2306
|
+
// @ts-expect-error - Method doesn't exist yet (RED test)
|
|
2307
|
+
orchestrator.pinToTier('page-1', 'hot')
|
|
2308
|
+
// @ts-expect-error - Method doesn't exist yet (RED test)
|
|
2309
|
+
orchestrator.unpinFromTier('page-1')
|
|
2310
|
+
|
|
2311
|
+
const events = await orchestrator.runDemotionCycle()
|
|
2312
|
+
|
|
2313
|
+
// Should now be demoted
|
|
2314
|
+
expect(events.find(e => e.key === 'page-1')).toBeDefined()
|
|
2315
|
+
})
|
|
2316
|
+
})
|
|
2317
|
+
})
|
|
2318
|
+
|
|
2319
|
+
describe('Cost Optimization', () => {
|
|
2320
|
+
describe('getCostBreakdown()', () => {
|
|
2321
|
+
it('should provide detailed cost breakdown by operation type', async () => {
|
|
2322
|
+
const orchestrator = createTieredStorageOrchestrator({
|
|
2323
|
+
cacheLayer: mockCacheLayer as unknown as CacheLayer,
|
|
2324
|
+
doStorage: mockDOStorage as unknown as DurableObjectStorage,
|
|
2325
|
+
r2Layer: mockR2Layer as unknown as R2StorageLayer,
|
|
2326
|
+
// @ts-expect-error - Config doesn't exist yet
|
|
2327
|
+
metrics: {
|
|
2328
|
+
trackCost: true,
|
|
2329
|
+
costConfig: {
|
|
2330
|
+
r2StoragePerGBMonth: 0.015,
|
|
2331
|
+
r2ClassAOperations: 0.0000045,
|
|
2332
|
+
r2ClassBOperations: 0.00000036,
|
|
2333
|
+
doStoragePerGBMonth: 0.20,
|
|
2334
|
+
cacheOperations: 0,
|
|
2335
|
+
},
|
|
2336
|
+
},
|
|
2337
|
+
})
|
|
2338
|
+
|
|
2339
|
+
// Simulate some operations
|
|
2340
|
+
await orchestrator.write('page-1', new Uint8Array(1024 * 1024), { tier: 'cold' })
|
|
2341
|
+
await orchestrator.write('page-2', new Uint8Array(1024 * 1024), { tier: 'warm' })
|
|
2342
|
+
|
|
2343
|
+
// @ts-expect-error - Method doesn't exist yet (RED test)
|
|
2344
|
+
const costBreakdown = orchestrator.getCostBreakdown()
|
|
2345
|
+
|
|
2346
|
+
expect(costBreakdown).toMatchObject({
|
|
2347
|
+
storage: {
|
|
2348
|
+
r2: expect.any(Number),
|
|
2349
|
+
do: expect.any(Number),
|
|
2350
|
+
cache: expect.any(Number),
|
|
2351
|
+
},
|
|
2352
|
+
operations: {
|
|
2353
|
+
r2Writes: expect.any(Number),
|
|
2354
|
+
r2Reads: expect.any(Number),
|
|
2355
|
+
doWrites: expect.any(Number),
|
|
2356
|
+
doReads: expect.any(Number),
|
|
2357
|
+
},
|
|
2358
|
+
total: expect.any(Number),
|
|
2359
|
+
projectedMonthly: expect.any(Number),
|
|
2360
|
+
})
|
|
2361
|
+
})
|
|
2362
|
+
})
|
|
2363
|
+
|
|
2364
|
+
describe('optimizeTierPlacement()', () => {
|
|
2365
|
+
it('should suggest optimal tier placement based on access patterns', async () => {
|
|
2366
|
+
const orchestrator = createTieredStorageOrchestrator({
|
|
2367
|
+
cacheLayer: mockCacheLayer as unknown as CacheLayer,
|
|
2368
|
+
doStorage: mockDOStorage as unknown as DurableObjectStorage,
|
|
2369
|
+
r2Layer: mockR2Layer as unknown as R2StorageLayer,
|
|
2370
|
+
})
|
|
2371
|
+
|
|
2372
|
+
// Setup: add entries with varying access patterns
|
|
2373
|
+
// @ts-expect-error - Accessing private for test
|
|
2374
|
+
orchestrator.storageIndex?.set('hot-candidate', {
|
|
2375
|
+
key: 'hot-candidate', tier: 'cold', size: 1000, accessCount: 100,
|
|
2376
|
+
lastAccess: Date.now(), created: Date.now() - 3600000, modified: Date.now(), dirty: false
|
|
2377
|
+
})
|
|
2378
|
+
// @ts-expect-error - Accessing private for test
|
|
2379
|
+
orchestrator.storageIndex?.set('cold-candidate', {
|
|
2380
|
+
key: 'cold-candidate', tier: 'warm', size: 10000, accessCount: 1,
|
|
2381
|
+
lastAccess: Date.now() - 86400000, created: Date.now() - 86400000 * 7, modified: Date.now() - 86400000, dirty: false
|
|
2382
|
+
})
|
|
2383
|
+
|
|
2384
|
+
// @ts-expect-error - Method doesn't exist yet (RED test)
|
|
2385
|
+
const suggestions = await orchestrator.optimizeTierPlacement()
|
|
2386
|
+
|
|
2387
|
+
expect(suggestions.promotions).toContainEqual(
|
|
2388
|
+
expect.objectContaining({ key: 'hot-candidate', suggestedTier: 'warm' })
|
|
2389
|
+
)
|
|
2390
|
+
expect(suggestions.demotions).toContainEqual(
|
|
2391
|
+
expect.objectContaining({ key: 'cold-candidate', suggestedTier: 'cold' })
|
|
2392
|
+
)
|
|
2393
|
+
expect(suggestions.estimatedSavings).toBeGreaterThanOrEqual(0)
|
|
2394
|
+
})
|
|
2395
|
+
})
|
|
2396
|
+
})
|
|
2397
|
+
|
|
2398
|
+
describe('Durability and Replication', () => {
|
|
2399
|
+
describe('ensureDurability()', () => {
|
|
2400
|
+
it('should verify data exists in cold tier', async () => {
|
|
2401
|
+
const data = new Uint8Array([1, 2, 3])
|
|
2402
|
+
mockDOStorage.get.mockResolvedValue(data)
|
|
2403
|
+
mockR2Layer.has.mockResolvedValue(true)
|
|
2404
|
+
|
|
2405
|
+
const orchestrator = createTieredStorageOrchestrator({
|
|
2406
|
+
cacheLayer: mockCacheLayer as unknown as CacheLayer,
|
|
2407
|
+
doStorage: mockDOStorage as unknown as DurableObjectStorage,
|
|
2408
|
+
r2Layer: mockR2Layer as unknown as R2StorageLayer,
|
|
2409
|
+
})
|
|
2410
|
+
|
|
2411
|
+
await orchestrator.write('page-1', data, { tier: 'warm' })
|
|
2412
|
+
|
|
2413
|
+
// @ts-expect-error - Method doesn't exist yet (RED test)
|
|
2414
|
+
const result = await orchestrator.ensureDurability('page-1')
|
|
2415
|
+
|
|
2416
|
+
expect(result.durable).toBe(true)
|
|
2417
|
+
expect(result.locations).toContain('cold')
|
|
2418
|
+
})
|
|
2419
|
+
|
|
2420
|
+
it('should copy data to cold tier if not present', async () => {
|
|
2421
|
+
const data = new Uint8Array([1, 2, 3])
|
|
2422
|
+
mockDOStorage.get.mockResolvedValue(data)
|
|
2423
|
+
mockR2Layer.has.mockResolvedValue(false)
|
|
2424
|
+
|
|
2425
|
+
const orchestrator = createTieredStorageOrchestrator({
|
|
2426
|
+
cacheLayer: mockCacheLayer as unknown as CacheLayer,
|
|
2427
|
+
doStorage: mockDOStorage as unknown as DurableObjectStorage,
|
|
2428
|
+
r2Layer: mockR2Layer as unknown as R2StorageLayer,
|
|
2429
|
+
})
|
|
2430
|
+
|
|
2431
|
+
await orchestrator.write('page-1', data, { tier: 'warm' })
|
|
2432
|
+
|
|
2433
|
+
// @ts-expect-error - Method doesn't exist yet (RED test)
|
|
2434
|
+
const result = await orchestrator.ensureDurability('page-1', { copyToCold: true })
|
|
2435
|
+
|
|
2436
|
+
expect(result.durable).toBe(true)
|
|
2437
|
+
expect(mockR2Layer.put).toHaveBeenCalledWith('page-1', data, expect.any(Object))
|
|
2438
|
+
})
|
|
2439
|
+
})
|
|
2440
|
+
})
|
|
2441
|
+
})
|