effect 4.0.7 → 4.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 +1 -1
- package/dist/BigDecimal.d.ts.map +1 -1
- package/dist/BigDecimal.js +3 -0
- package/dist/BigDecimal.js.map +1 -1
- package/dist/Channel.d.ts +12 -6
- package/dist/Channel.d.ts.map +1 -1
- package/dist/Channel.js +9 -3
- package/dist/Channel.js.map +1 -1
- package/dist/Config.d.ts +7 -1
- package/dist/Config.d.ts.map +1 -1
- package/dist/Config.js.map +1 -1
- package/dist/Context.d.ts +18 -1
- package/dist/Context.d.ts.map +1 -1
- package/dist/Context.js +27 -8
- package/dist/Context.js.map +1 -1
- package/dist/Cron.d.ts +81 -11
- package/dist/Cron.d.ts.map +1 -1
- package/dist/Cron.js +101 -26
- package/dist/Cron.js.map +1 -1
- package/dist/Duration.d.ts +78 -0
- package/dist/Duration.d.ts.map +1 -1
- package/dist/Duration.js +123 -0
- package/dist/Duration.js.map +1 -1
- package/dist/Effect.d.ts +164 -7
- package/dist/Effect.d.ts.map +1 -1
- package/dist/Effect.js +11 -1
- package/dist/Effect.js.map +1 -1
- package/dist/FileSystem.d.ts +59 -4
- package/dist/FileSystem.d.ts.map +1 -1
- package/dist/FileSystem.js +9 -2
- package/dist/FileSystem.js.map +1 -1
- package/dist/Glob.d.ts +165 -0
- package/dist/Glob.d.ts.map +1 -0
- package/dist/Glob.js +127 -0
- package/dist/Glob.js.map +1 -0
- package/dist/Match.d.ts +86 -0
- package/dist/Match.d.ts.map +1 -1
- package/dist/Match.js +72 -0
- package/dist/Match.js.map +1 -1
- package/dist/Pool.d.ts +51 -0
- package/dist/Pool.d.ts.map +1 -1
- package/dist/Pool.js +59 -9
- package/dist/Pool.js.map +1 -1
- package/dist/Random.d.ts.map +1 -1
- package/dist/Random.js +3 -3
- package/dist/Random.js.map +1 -1
- package/dist/RcMap.d.ts.map +1 -1
- package/dist/RcMap.js +8 -2
- package/dist/RcMap.js.map +1 -1
- package/dist/Schedule.d.ts +18 -1
- package/dist/Schedule.d.ts.map +1 -1
- package/dist/Schedule.js +7 -2
- package/dist/Schedule.js.map +1 -1
- package/dist/Schema.d.ts +666 -41
- package/dist/Schema.d.ts.map +1 -1
- package/dist/Schema.js +650 -0
- package/dist/Schema.js.map +1 -1
- package/dist/SchemaGetter.d.ts +6 -1
- package/dist/SchemaGetter.d.ts.map +1 -1
- package/dist/SchemaGetter.js +22 -11
- package/dist/SchemaGetter.js.map +1 -1
- package/dist/SchemaRepresentation.d.ts +14 -0
- package/dist/SchemaRepresentation.d.ts.map +1 -1
- package/dist/SchemaRepresentation.js +16 -0
- package/dist/SchemaRepresentation.js.map +1 -1
- package/dist/SchemaTransformation.d.ts +8 -3
- package/dist/SchemaTransformation.d.ts.map +1 -1
- package/dist/SchemaTransformation.js +6 -3
- package/dist/SchemaTransformation.js.map +1 -1
- package/dist/ScopedCache.d.ts.map +1 -1
- package/dist/ScopedCache.js +5 -7
- package/dist/ScopedCache.js.map +1 -1
- package/dist/Semaphore.d.ts +130 -0
- package/dist/Semaphore.d.ts.map +1 -1
- package/dist/Semaphore.js +40 -0
- package/dist/Semaphore.js.map +1 -1
- package/dist/Stream.d.ts +13 -4
- package/dist/Stream.d.ts.map +1 -1
- package/dist/Stream.js +9 -0
- package/dist/Stream.js.map +1 -1
- package/dist/Terminal.d.ts +18 -1
- package/dist/Terminal.d.ts.map +1 -1
- package/dist/Terminal.js +1 -1
- package/dist/Terminal.js.map +1 -1
- package/dist/ai/AiError.d.ts +43 -116
- package/dist/ai/AiError.d.ts.map +1 -1
- package/dist/ai/AiError.js +53 -72
- package/dist/ai/AiError.js.map +1 -1
- package/dist/ai/Decision.d.ts +196 -15
- package/dist/ai/Decision.d.ts.map +1 -1
- package/dist/ai/Decision.js +130 -0
- package/dist/ai/Decision.js.map +1 -1
- package/dist/ai/DecisionModel.d.ts +54 -13
- package/dist/ai/DecisionModel.d.ts.map +1 -1
- package/dist/ai/DecisionModel.js +123 -12
- package/dist/ai/DecisionModel.js.map +1 -1
- package/dist/ai/McpServer.d.ts +630 -29
- package/dist/ai/McpServer.d.ts.map +1 -1
- package/dist/ai/McpServer.js +562 -116
- package/dist/ai/McpServer.js.map +1 -1
- package/dist/ai/McpTest.d.ts +130 -0
- package/dist/ai/McpTest.d.ts.map +1 -0
- package/dist/ai/McpTest.js +237 -0
- package/dist/ai/McpTest.js.map +1 -0
- package/dist/ai/Response.d.ts +1 -25
- package/dist/ai/Response.d.ts.map +1 -1
- package/dist/ai/Response.js +1 -10
- package/dist/ai/Response.js.map +1 -1
- package/dist/ai/Tool.d.ts +19 -1
- package/dist/ai/Tool.d.ts.map +1 -1
- package/dist/ai/Tool.js.map +1 -1
- package/dist/ai/index.d.ts +5 -0
- package/dist/ai/index.d.ts.map +1 -1
- package/dist/ai/index.js +5 -0
- package/dist/ai/index.js.map +1 -1
- package/dist/ai/internal/mcpCore.js +17 -12
- package/dist/ai/internal/mcpCore.js.map +1 -1
- package/dist/ai/internal/mcpProtocol/v2024_11_05.js +4 -5
- package/dist/ai/internal/mcpProtocol/v2024_11_05.js.map +1 -1
- package/dist/ai/internal/mcpProtocol/v2025_03_26.js +4 -5
- package/dist/ai/internal/mcpProtocol/v2025_03_26.js.map +1 -1
- package/dist/ai/internal/mcpProtocol/v2025_06_18.js +4 -5
- package/dist/ai/internal/mcpProtocol/v2025_06_18.js.map +1 -1
- package/dist/ai/internal/mcpProtocol/v2025_11_25.js +4 -5
- package/dist/ai/internal/mcpProtocol/v2025_11_25.js.map +1 -1
- package/dist/ai/internal/mcpProtocol/v2026_07_28.js +18 -10
- package/dist/ai/internal/mcpProtocol/v2026_07_28.js.map +1 -1
- package/dist/ai/internal/mcpProtocol.js +13 -0
- package/dist/ai/internal/mcpProtocol.js.map +1 -1
- package/dist/ai/internal/mcpRuntime.js +5 -3
- package/dist/ai/internal/mcpRuntime.js.map +1 -1
- package/dist/ai/internal/mcpStatefulRuntime.js +6 -1
- package/dist/ai/internal/mcpStatefulRuntime.js.map +1 -1
- package/dist/cli/Argument.d.ts +6 -0
- package/dist/cli/Argument.d.ts.map +1 -1
- package/dist/cli/Argument.js +2 -0
- package/dist/cli/Argument.js.map +1 -1
- package/dist/cli/CliOutput.d.ts +65 -1
- package/dist/cli/CliOutput.d.ts.map +1 -1
- package/dist/cli/CliOutput.js +94 -37
- package/dist/cli/CliOutput.js.map +1 -1
- package/dist/cli/Command.d.ts +23 -0
- package/dist/cli/Command.d.ts.map +1 -1
- package/dist/cli/Command.js +8 -0
- package/dist/cli/Command.js.map +1 -1
- package/dist/cli/Flag.d.ts +107 -0
- package/dist/cli/Flag.d.ts.map +1 -1
- package/dist/cli/Flag.js +37 -0
- package/dist/cli/Flag.js.map +1 -1
- package/dist/cli/HelpDoc.d.ts +15 -1
- package/dist/cli/HelpDoc.d.ts.map +1 -1
- package/dist/cli/Param.d.ts +6 -0
- package/dist/cli/Param.d.ts.map +1 -1
- package/dist/cli/Param.js +2 -0
- package/dist/cli/Param.js.map +1 -1
- package/dist/cli/Primitive.d.ts +14 -0
- package/dist/cli/Primitive.d.ts.map +1 -1
- package/dist/cli/Primitive.js +15 -1
- package/dist/cli/Primitive.js.map +1 -1
- package/dist/cli/Prompt.d.ts +1 -0
- package/dist/cli/Prompt.d.ts.map +1 -1
- package/dist/cli/Prompt.js +1 -0
- package/dist/cli/Prompt.js.map +1 -1
- package/dist/cli/internal/help.d.ts.map +1 -1
- package/dist/cli/internal/help.js +17 -10
- package/dist/cli/internal/help.js.map +1 -1
- package/dist/cluster/Sharding.d.ts.map +1 -1
- package/dist/cluster/Sharding.js +90 -28
- package/dist/cluster/Sharding.js.map +1 -1
- package/dist/cluster/ShardingConfig.d.ts +72 -6
- package/dist/cluster/ShardingConfig.d.ts.map +1 -1
- package/dist/cluster/ShardingConfig.js +4 -0
- package/dist/cluster/ShardingConfig.js.map +1 -1
- package/dist/cluster/SingleRunner.d.ts +3 -3
- package/dist/cluster/SingleRunner.d.ts.map +1 -1
- package/dist/cluster/SingleRunner.js +1 -1
- package/dist/cluster/SqlMessageStorage.d.ts +37 -6
- package/dist/cluster/SqlMessageStorage.d.ts.map +1 -1
- package/dist/cluster/SqlMessageStorage.js +34 -7
- package/dist/cluster/SqlMessageStorage.js.map +1 -1
- package/dist/cluster/SqlRunnerStorage.d.ts.map +1 -1
- package/dist/cluster/SqlRunnerStorage.js +23 -8
- package/dist/cluster/SqlRunnerStorage.js.map +1 -1
- package/dist/crypto/Aead.d.ts +322 -0
- package/dist/crypto/Aead.d.ts.map +1 -0
- package/dist/crypto/Aead.js +331 -0
- package/dist/crypto/Aead.js.map +1 -0
- package/dist/crypto/Cipher.d.ts +235 -0
- package/dist/crypto/Cipher.d.ts.map +1 -0
- package/dist/crypto/Cipher.js +233 -0
- package/dist/crypto/Cipher.js.map +1 -0
- package/dist/crypto/CryptoError.d.ts +212 -0
- package/dist/crypto/CryptoError.d.ts.map +1 -0
- package/dist/crypto/CryptoError.js +202 -0
- package/dist/crypto/CryptoError.js.map +1 -0
- package/dist/crypto/Digest.d.ts +143 -0
- package/dist/crypto/Digest.d.ts.map +1 -0
- package/dist/crypto/Digest.js +146 -0
- package/dist/crypto/Digest.js.map +1 -0
- package/dist/crypto/Kdf.d.ts +220 -0
- package/dist/crypto/Kdf.d.ts.map +1 -0
- package/dist/crypto/Kdf.js +212 -0
- package/dist/crypto/Kdf.js.map +1 -0
- package/dist/crypto/KeyAgreement.d.ts +323 -0
- package/dist/crypto/KeyAgreement.d.ts.map +1 -0
- package/dist/crypto/KeyAgreement.js +303 -0
- package/dist/crypto/KeyAgreement.js.map +1 -0
- package/dist/crypto/KeyFormat.d.ts +309 -0
- package/dist/crypto/KeyFormat.d.ts.map +1 -0
- package/dist/crypto/KeyFormat.js +556 -0
- package/dist/crypto/KeyFormat.js.map +1 -0
- package/dist/crypto/KeyWrap.d.ts +389 -0
- package/dist/crypto/KeyWrap.d.ts.map +1 -0
- package/dist/crypto/KeyWrap.js +381 -0
- package/dist/crypto/KeyWrap.js.map +1 -0
- package/dist/crypto/Mac.d.ts +277 -0
- package/dist/crypto/Mac.d.ts.map +1 -0
- package/dist/crypto/Mac.js +288 -0
- package/dist/crypto/Mac.js.map +1 -0
- package/dist/crypto/SecureRandom.d.ts +340 -0
- package/dist/crypto/SecureRandom.d.ts.map +1 -0
- package/dist/crypto/SecureRandom.js +257 -0
- package/dist/crypto/SecureRandom.js.map +1 -0
- package/dist/crypto/Signature.d.ts +448 -0
- package/dist/crypto/Signature.d.ts.map +1 -0
- package/dist/crypto/Signature.js +484 -0
- package/dist/crypto/Signature.js.map +1 -0
- package/dist/crypto/WebCrypto.d.ts +61 -0
- package/dist/crypto/WebCrypto.d.ts.map +1 -0
- package/dist/crypto/WebCrypto.js +63 -0
- package/dist/crypto/WebCrypto.js.map +1 -0
- package/dist/crypto/index.d.ts +65 -0
- package/dist/crypto/index.d.ts.map +1 -0
- package/dist/crypto/index.js +66 -0
- package/dist/crypto/index.js.map +1 -0
- package/dist/crypto/internal/asymmetric.d.ts +2 -0
- package/dist/crypto/internal/asymmetric.d.ts.map +1 -0
- package/dist/crypto/internal/asymmetric.js +210 -0
- package/dist/crypto/internal/asymmetric.js.map +1 -0
- package/dist/crypto/internal/chacha.d.ts +46 -0
- package/dist/crypto/internal/chacha.d.ts.map +1 -0
- package/dist/crypto/internal/chacha.js +885 -0
- package/dist/crypto/internal/chacha.js.map +1 -0
- package/dist/crypto/internal/der.d.ts +6 -0
- package/dist/crypto/internal/der.d.ts.map +1 -0
- package/dist/crypto/internal/der.js +86 -0
- package/dist/crypto/internal/der.js.map +1 -0
- package/dist/crypto/internal/digest.d.ts +2 -0
- package/dist/crypto/internal/digest.d.ts.map +1 -0
- package/dist/crypto/internal/digest.js +15 -0
- package/dist/crypto/internal/digest.js.map +1 -0
- package/dist/crypto/internal/key.d.ts +2 -0
- package/dist/crypto/internal/key.d.ts.map +1 -0
- package/dist/crypto/internal/key.js +170 -0
- package/dist/crypto/internal/key.js.map +1 -0
- package/dist/crypto/internal/md5.d.ts +39 -0
- package/dist/crypto/internal/md5.d.ts.map +1 -0
- package/dist/crypto/internal/md5.js +179 -0
- package/dist/crypto/internal/md5.js.map +1 -0
- package/dist/crypto/internal/webCrypto.d.ts +2 -0
- package/dist/crypto/internal/webCrypto.d.ts.map +1 -0
- package/dist/crypto/internal/webCrypto.js +111 -0
- package/dist/crypto/internal/webCrypto.js.map +1 -0
- package/dist/encoding/Base64Url.d.ts.map +1 -1
- package/dist/encoding/Base64Url.js +3 -2
- package/dist/encoding/Base64Url.js.map +1 -1
- package/dist/encoding/Csv.d.ts +608 -0
- package/dist/encoding/Csv.d.ts.map +1 -0
- package/dist/encoding/Csv.js +718 -0
- package/dist/encoding/Csv.js.map +1 -0
- package/dist/encoding/Hex.d.ts +3 -2
- package/dist/encoding/Hex.d.ts.map +1 -1
- package/dist/encoding/Hex.js +3 -2
- package/dist/encoding/Hex.js.map +1 -1
- package/dist/encoding/SchemaBinary.d.ts.map +1 -1
- package/dist/encoding/SchemaBinary.js +42 -4
- package/dist/encoding/SchemaBinary.js.map +1 -1
- package/dist/encoding/Toml.d.ts +17 -3
- package/dist/encoding/Toml.d.ts.map +1 -1
- package/dist/encoding/Toml.js +104 -16
- package/dist/encoding/Toml.js.map +1 -1
- package/dist/encoding/Yaml.d.ts +8 -1
- package/dist/encoding/Yaml.d.ts.map +1 -1
- package/dist/encoding/Yaml.js +54 -18
- package/dist/encoding/Yaml.js.map +1 -1
- package/dist/encoding/index.d.ts +5 -0
- package/dist/encoding/index.d.ts.map +1 -1
- package/dist/encoding/index.js +5 -0
- package/dist/encoding/index.js.map +1 -1
- package/dist/eventlog/EventJournal.d.ts.map +1 -1
- package/dist/eventlog/EventJournal.js +6 -3
- package/dist/eventlog/EventJournal.js.map +1 -1
- package/dist/eventlog/EventLog.d.ts +119 -4
- package/dist/eventlog/EventLog.d.ts.map +1 -1
- package/dist/eventlog/EventLog.js +200 -12
- package/dist/eventlog/EventLog.js.map +1 -1
- package/dist/eventlog/EventLogEncryption.d.ts +41 -4
- package/dist/eventlog/EventLogEncryption.d.ts.map +1 -1
- package/dist/eventlog/EventLogEncryption.js +83 -64
- package/dist/eventlog/EventLogEncryption.js.map +1 -1
- package/dist/eventlog/EventLogMessage.d.ts +279 -0
- package/dist/eventlog/EventLogMessage.d.ts.map +1 -1
- package/dist/eventlog/EventLogMessage.js +217 -1
- package/dist/eventlog/EventLogMessage.js.map +1 -1
- package/dist/eventlog/EventLogRemote.d.ts +200 -28
- package/dist/eventlog/EventLogRemote.d.ts.map +1 -1
- package/dist/eventlog/EventLogRemote.js +186 -16
- package/dist/eventlog/EventLogRemote.js.map +1 -1
- package/dist/eventlog/EventLogServer.d.ts +3 -0
- package/dist/eventlog/EventLogServer.d.ts.map +1 -1
- package/dist/eventlog/EventLogServer.js +18 -10
- package/dist/eventlog/EventLogServer.js.map +1 -1
- package/dist/eventlog/EventLogServerEncrypted.d.ts +326 -5
- package/dist/eventlog/EventLogServerEncrypted.d.ts.map +1 -1
- package/dist/eventlog/EventLogServerEncrypted.js +769 -14
- package/dist/eventlog/EventLogServerEncrypted.js.map +1 -1
- package/dist/eventlog/EventLogSessionAuth.d.ts +5 -3
- package/dist/eventlog/EventLogSessionAuth.d.ts.map +1 -1
- package/dist/eventlog/EventLogSessionAuth.js +41 -64
- package/dist/eventlog/EventLogSessionAuth.js.map +1 -1
- package/dist/eventlog/EventRow.d.ts +4 -0
- package/dist/eventlog/EventRow.d.ts.map +1 -1
- package/dist/eventlog/EventRow.js +12 -2
- package/dist/eventlog/EventRow.js.map +1 -1
- package/dist/eventlog/SqlEventJournal.js +2 -2
- package/dist/eventlog/SqlEventJournal.js.map +1 -1
- package/dist/eventlog/SqlEventLogServerEncrypted.d.ts +21 -5
- package/dist/eventlog/SqlEventLogServerEncrypted.d.ts.map +1 -1
- package/dist/eventlog/SqlEventLogServerEncrypted.js +1048 -113
- package/dist/eventlog/SqlEventLogServerEncrypted.js.map +1 -1
- package/dist/eventlog/SqlEventLogServerUnencrypted.js +2 -2
- package/dist/eventlog/SqlEventLogServerUnencrypted.js.map +1 -1
- package/dist/eventlog/internal/batches.d.ts +12 -0
- package/dist/eventlog/internal/batches.d.ts.map +1 -0
- package/dist/eventlog/internal/batches.js +26 -0
- package/dist/eventlog/internal/batches.js.map +1 -0
- package/dist/eventlog/internal/compaction.d.ts +38 -0
- package/dist/eventlog/internal/compaction.d.ts.map +1 -0
- package/dist/eventlog/internal/compaction.js +33 -0
- package/dist/eventlog/internal/compaction.js.map +1 -0
- package/dist/eventlog/internal/identityRootSecretDerivation.js +66 -67
- package/dist/eventlog/internal/identityRootSecretDerivation.js.map +1 -1
- package/dist/eventlog/internal/remoteCompaction.d.ts +31 -0
- package/dist/eventlog/internal/remoteCompaction.d.ts.map +1 -0
- package/dist/eventlog/internal/remoteCompaction.js +76 -0
- package/dist/eventlog/internal/remoteCompaction.js.map +1 -0
- package/dist/http/Cookies.d.ts +172 -0
- package/dist/http/Cookies.d.ts.map +1 -1
- package/dist/http/Cookies.js +143 -1
- package/dist/http/Cookies.js.map +1 -1
- package/dist/http/FetchHttpClient.d.ts +26 -2
- package/dist/http/FetchHttpClient.d.ts.map +1 -1
- package/dist/http/FetchHttpClient.js +78 -15
- package/dist/http/FetchHttpClient.js.map +1 -1
- package/dist/http/HttpClient.d.ts +79 -8
- package/dist/http/HttpClient.d.ts.map +1 -1
- package/dist/http/HttpClient.js +65 -5
- package/dist/http/HttpClient.js.map +1 -1
- package/dist/http/HttpClientRequest.d.ts +9 -5
- package/dist/http/HttpClientRequest.d.ts.map +1 -1
- package/dist/http/HttpClientRequest.js +2 -3
- package/dist/http/HttpClientRequest.js.map +1 -1
- package/dist/http/HttpClientResponse.d.ts.map +1 -1
- package/dist/http/HttpClientResponse.js +3 -2
- package/dist/http/HttpClientResponse.js.map +1 -1
- package/dist/http/HttpMethod.d.ts +7 -1
- package/dist/http/HttpMethod.d.ts.map +1 -1
- package/dist/http/HttpMethod.js +3 -8
- package/dist/http/HttpMethod.js.map +1 -1
- package/dist/http/HttpServerRequest.d.ts +5 -2
- package/dist/http/HttpServerRequest.d.ts.map +1 -1
- package/dist/http/HttpServerRequest.js +6 -17
- package/dist/http/HttpServerRequest.js.map +1 -1
- package/dist/http/HttpServerResponse.d.ts.map +1 -1
- package/dist/http/HttpServerResponse.js +3 -2
- package/dist/http/HttpServerResponse.js.map +1 -1
- package/dist/http/HttpTraceContext.d.ts +2 -1
- package/dist/http/HttpTraceContext.d.ts.map +1 -1
- package/dist/http/HttpTraceContext.js +7 -5
- package/dist/http/HttpTraceContext.js.map +1 -1
- package/dist/http/ProblemDetails.d.ts +96 -0
- package/dist/http/ProblemDetails.d.ts.map +1 -0
- package/dist/http/ProblemDetails.js +98 -0
- package/dist/http/ProblemDetails.js.map +1 -0
- package/dist/http/index.d.ts +5 -0
- package/dist/http/index.d.ts.map +1 -1
- package/dist/http/index.js +5 -0
- package/dist/http/index.js.map +1 -1
- package/dist/http/internal/decompression.js +3 -2
- package/dist/http/internal/decompression.js.map +1 -1
- package/dist/http/internal/urlParams.js +8 -0
- package/dist/http/internal/urlParams.js.map +1 -1
- package/dist/http-api/HttpApiBuilder.js +19 -5
- package/dist/http-api/HttpApiBuilder.js.map +1 -1
- package/dist/http-api/HttpApiClient.d.ts +63 -0
- package/dist/http-api/HttpApiClient.d.ts.map +1 -1
- package/dist/http-api/HttpApiClient.js +51 -0
- package/dist/http-api/HttpApiClient.js.map +1 -1
- package/dist/http-api/HttpApiEndpoint.d.ts.map +1 -1
- package/dist/http-api/HttpApiEndpoint.js +14 -5
- package/dist/http-api/HttpApiEndpoint.js.map +1 -1
- package/dist/http-api/HttpApiError.d.ts +185 -13
- package/dist/http-api/HttpApiError.d.ts.map +1 -1
- package/dist/http-api/HttpApiError.js +128 -0
- package/dist/http-api/HttpApiError.js.map +1 -1
- package/dist/http-api/HttpApiMiddleware.d.ts +64 -17
- package/dist/http-api/HttpApiMiddleware.d.ts.map +1 -1
- package/dist/http-api/HttpApiMiddleware.js +85 -1
- package/dist/http-api/HttpApiMiddleware.js.map +1 -1
- package/dist/http-api/HttpApiSchema.d.ts +76 -14
- package/dist/http-api/HttpApiSchema.d.ts.map +1 -1
- package/dist/http-api/HttpApiSchema.js +53 -0
- package/dist/http-api/HttpApiSchema.js.map +1 -1
- package/dist/index.d.ts +5 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -5
- package/dist/index.js.map +1 -1
- package/dist/internal/effect.js +25 -14
- package/dist/internal/effect.js.map +1 -1
- package/dist/internal/glob.d.ts +2 -0
- package/dist/internal/glob.d.ts.map +1 -0
- package/dist/internal/glob.js +630 -0
- package/dist/internal/glob.js.map +1 -0
- package/dist/internal/matcher.js +10 -0
- package/dist/internal/matcher.js.map +1 -1
- package/dist/internal/persistence.js +9 -0
- package/dist/internal/persistence.js.map +1 -1
- package/dist/internal/rcRef.js +4 -1
- package/dist/internal/rcRef.js.map +1 -1
- package/dist/internal/schema/toCodeDocument.js +6 -3
- package/dist/internal/schema/toCodeDocument.js.map +1 -1
- package/dist/internal/trie.js +42 -13
- package/dist/internal/trie.js.map +1 -1
- package/dist/internal/ulid.js +41 -0
- package/dist/internal/ulid.js.map +1 -1
- package/dist/internal/uuid.js +52 -8
- package/dist/internal/uuid.js.map +1 -1
- package/dist/internal/version.js +1 -1
- package/dist/observability/Otlp.d.ts +15 -4
- package/dist/observability/Otlp.d.ts.map +1 -1
- package/dist/observability/Otlp.js +10 -0
- package/dist/observability/Otlp.js.map +1 -1
- package/dist/observability/OtlpResource.d.ts +7 -0
- package/dist/observability/OtlpResource.d.ts.map +1 -1
- package/dist/observability/OtlpResource.js +11 -1
- package/dist/observability/OtlpResource.js.map +1 -1
- package/dist/persistence/KeyValueStore.d.ts +47 -2
- package/dist/persistence/KeyValueStore.d.ts.map +1 -1
- package/dist/persistence/KeyValueStore.js +129 -18
- package/dist/persistence/KeyValueStore.js.map +1 -1
- package/dist/persistence/PersistedQueue.d.ts +9 -0
- package/dist/persistence/PersistedQueue.d.ts.map +1 -1
- package/dist/persistence/PersistedQueue.js +94 -52
- package/dist/persistence/PersistedQueue.js.map +1 -1
- package/dist/persistence/Persistence.d.ts +10 -4
- package/dist/persistence/Persistence.d.ts.map +1 -1
- package/dist/persistence/Persistence.js +18 -11
- package/dist/persistence/Persistence.js.map +1 -1
- package/dist/process/ChildProcess.d.ts +207 -67
- package/dist/process/ChildProcess.d.ts.map +1 -1
- package/dist/process/ChildProcess.js +56 -158
- package/dist/process/ChildProcess.js.map +1 -1
- package/dist/process/ChildProcessSpawner.d.ts +5 -0
- package/dist/process/ChildProcessSpawner.d.ts.map +1 -1
- package/dist/process/ChildProcessSpawner.js.map +1 -1
- package/dist/process/CommandLine.d.ts +294 -0
- package/dist/process/CommandLine.d.ts.map +1 -0
- package/dist/process/CommandLine.js +481 -0
- package/dist/process/CommandLine.js.map +1 -0
- package/dist/process/index.d.ts +5 -0
- package/dist/process/index.d.ts.map +1 -1
- package/dist/process/index.js +5 -0
- package/dist/process/index.js.map +1 -1
- package/dist/reactivity/Atom.d.ts +2 -1
- package/dist/reactivity/Atom.d.ts.map +1 -1
- package/dist/reactivity/Atom.js +3 -4
- package/dist/reactivity/Atom.js.map +1 -1
- package/dist/reactivity/AtomRegistry.js.map +1 -1
- package/dist/rpc/RpcClient.d.ts +16 -1
- package/dist/rpc/RpcClient.d.ts.map +1 -1
- package/dist/rpc/RpcClient.js +50 -16
- package/dist/rpc/RpcClient.js.map +1 -1
- package/dist/rpc/RpcMessage.d.ts +10 -1
- package/dist/rpc/RpcMessage.d.ts.map +1 -1
- package/dist/rpc/RpcMessage.js +1 -0
- package/dist/rpc/RpcMessage.js.map +1 -1
- package/dist/rpc/RpcSerialization.d.ts.map +1 -1
- package/dist/rpc/RpcSerialization.js +6 -0
- package/dist/rpc/RpcSerialization.js.map +1 -1
- package/dist/rpc/RpcServer.d.ts +31 -0
- package/dist/rpc/RpcServer.d.ts.map +1 -1
- package/dist/rpc/RpcServer.js +139 -66
- package/dist/rpc/RpcServer.js.map +1 -1
- package/dist/schema/Model.d.ts +14 -0
- package/dist/schema/Model.d.ts.map +1 -1
- package/dist/schema/Model.js +18 -3
- package/dist/schema/Model.js.map +1 -1
- package/dist/sql/Migrator.d.ts +0 -19
- package/dist/sql/Migrator.d.ts.map +1 -1
- package/dist/sql/Migrator.js +3 -48
- package/dist/sql/Migrator.js.map +1 -1
- package/dist/sql/MigratorFileSystem.d.ts +21 -0
- package/dist/sql/MigratorFileSystem.d.ts.map +1 -0
- package/dist/sql/MigratorFileSystem.js +57 -0
- package/dist/sql/MigratorFileSystem.js.map +1 -0
- package/dist/sql/SqlClient.d.ts.map +1 -1
- package/dist/sql/SqlClient.js +5 -9
- package/dist/sql/SqlClient.js.map +1 -1
- package/dist/sql/Statement.d.ts +20 -6
- package/dist/sql/Statement.d.ts.map +1 -1
- package/dist/sql/Statement.js +0 -13
- package/dist/sql/Statement.js.map +1 -1
- package/dist/sql/index.js +1 -1
- package/dist/testing/index.d.ts +0 -5
- package/dist/testing/index.d.ts.map +1 -1
- package/dist/testing/index.js +0 -5
- package/dist/testing/index.js.map +1 -1
- package/dist/workflow/internal/crypto.js +9 -8
- package/dist/workflow/internal/crypto.js.map +1 -1
- package/package.json +3 -1
- package/src/BigDecimal.ts +3 -0
- package/src/Channel.ts +17 -11
- package/src/Config.ts +7 -1
- package/src/Context.ts +34 -10
- package/src/Cron.ts +178 -29
- package/src/Duration.ts +140 -0
- package/src/Effect.ts +914 -85
- package/src/FileSystem.ts +65 -4
- package/src/Glob.ts +205 -0
- package/src/Match.ts +127 -0
- package/src/Pool.ts +73 -15
- package/src/Random.ts +3 -3
- package/src/RcMap.ts +5 -1
- package/src/Schedule.ts +26 -3
- package/src/Schema.ts +1189 -62
- package/src/SchemaGetter.ts +27 -11
- package/src/SchemaRepresentation.ts +19 -0
- package/src/SchemaTransformation.ts +8 -3
- package/src/ScopedCache.ts +5 -9
- package/src/Semaphore.ts +141 -0
- package/src/Stream.ts +15 -6
- package/src/Terminal.ts +19 -1
- package/src/ai/AiError.ts +56 -80
- package/src/ai/Decision.ts +257 -6
- package/src/ai/DecisionModel.ts +171 -28
- package/src/ai/McpServer.ts +1269 -286
- package/src/ai/McpTest.ts +380 -0
- package/src/ai/Response.ts +2 -22
- package/src/ai/Tool.ts +27 -8
- package/src/ai/index.ts +6 -0
- package/src/ai/internal/mcpCore.ts +78 -44
- package/src/ai/internal/mcpProtocol/v2024_11_05.ts +4 -7
- package/src/ai/internal/mcpProtocol/v2025_03_26.ts +4 -7
- package/src/ai/internal/mcpProtocol/v2025_06_18.ts +4 -7
- package/src/ai/internal/mcpProtocol/v2025_11_25.ts +4 -7
- package/src/ai/internal/mcpProtocol/v2026_07_28.ts +15 -10
- package/src/ai/internal/mcpProtocol.ts +29 -0
- package/src/ai/internal/mcpRuntime.ts +9 -5
- package/src/ai/internal/mcpStatefulRuntime.ts +14 -2
- package/src/cli/Argument.ts +6 -0
- package/src/cli/CliOutput.ts +117 -49
- package/src/cli/Command.ts +23 -0
- package/src/cli/Flag.ts +107 -0
- package/src/cli/HelpDoc.ts +18 -1
- package/src/cli/Param.ts +6 -0
- package/src/cli/Primitive.ts +16 -1
- package/src/cli/Prompt.ts +1 -0
- package/src/cli/internal/help.ts +21 -9
- package/src/cluster/Sharding.ts +93 -26
- package/src/cluster/ShardingConfig.ts +41 -3
- package/src/cluster/SingleRunner.ts +3 -3
- package/src/cluster/SqlMessageStorage.ts +49 -17
- package/src/cluster/SqlRunnerStorage.ts +37 -27
- package/src/crypto/Aead.ts +517 -0
- package/src/crypto/Cipher.ts +326 -0
- package/src/crypto/CryptoError.ts +242 -0
- package/src/crypto/Digest.ts +167 -0
- package/src/crypto/Kdf.ts +315 -0
- package/src/crypto/KeyAgreement.ts +489 -0
- package/src/crypto/KeyFormat.ts +712 -0
- package/src/crypto/KeyWrap.ts +639 -0
- package/src/crypto/Mac.ts +386 -0
- package/src/crypto/SecureRandom.ts +459 -0
- package/src/crypto/Signature.ts +673 -0
- package/src/crypto/WebCrypto.ts +64 -0
- package/src/crypto/index.ts +78 -0
- package/src/crypto/internal/asymmetric.ts +329 -0
- package/src/crypto/internal/chacha.ts +1088 -0
- package/src/crypto/internal/der.ts +110 -0
- package/src/crypto/internal/digest.ts +16 -0
- package/src/crypto/internal/key.ts +235 -0
- package/src/crypto/internal/md5.ts +170 -0
- package/src/crypto/internal/webCrypto.ts +150 -0
- package/src/encoding/Base64Url.ts +5 -2
- package/src/encoding/Csv.ts +1298 -0
- package/src/encoding/Hex.ts +3 -2
- package/src/encoding/SchemaBinary.ts +40 -4
- package/src/encoding/Toml.ts +85 -20
- package/src/encoding/Yaml.ts +61 -18
- package/src/encoding/index.ts +6 -0
- package/src/eventlog/EventJournal.ts +4 -3
- package/src/eventlog/EventLog.ts +340 -15
- package/src/eventlog/EventLogEncryption.ts +118 -110
- package/src/eventlog/EventLogMessage.ts +273 -1
- package/src/eventlog/EventLogRemote.ts +367 -23
- package/src/eventlog/EventLogServer.ts +23 -8
- package/src/eventlog/EventLogServerEncrypted.ts +1130 -67
- package/src/eventlog/EventLogSessionAuth.ts +56 -70
- package/src/eventlog/EventRow.ts +12 -3
- package/src/eventlog/SqlEventJournal.ts +2 -2
- package/src/eventlog/SqlEventLogServerEncrypted.ts +1244 -136
- package/src/eventlog/SqlEventLogServerUnencrypted.ts +2 -2
- package/src/eventlog/internal/batches.ts +30 -0
- package/src/eventlog/internal/compaction.ts +64 -0
- package/src/eventlog/internal/identityRootSecretDerivation.ts +93 -123
- package/src/eventlog/internal/remoteCompaction.ts +92 -0
- package/src/http/Cookies.ts +281 -1
- package/src/http/FetchHttpClient.ts +88 -16
- package/src/http/HttpClient.ts +89 -22
- package/src/http/HttpClientRequest.ts +12 -9
- package/src/http/HttpClientResponse.ts +3 -2
- package/src/http/HttpMethod.ts +10 -2
- package/src/http/HttpServerRequest.ts +15 -30
- package/src/http/HttpServerResponse.ts +3 -2
- package/src/http/HttpTraceContext.ts +7 -4
- package/src/http/ProblemDetails.ts +149 -0
- package/src/http/index.ts +6 -0
- package/src/http/internal/decompression.ts +4 -4
- package/src/http/internal/urlParams.ts +8 -0
- package/src/http-api/HttpApiBuilder.ts +24 -12
- package/src/http-api/HttpApiClient.ts +67 -1
- package/src/http-api/HttpApiEndpoint.ts +14 -5
- package/src/http-api/HttpApiError.ts +250 -0
- package/src/http-api/HttpApiMiddleware.ts +95 -1
- package/src/http-api/HttpApiSchema.ts +105 -14
- package/src/index.ts +6 -6
- package/src/internal/effect.ts +34 -18
- package/src/internal/glob.ts +736 -0
- package/src/internal/matcher.ts +17 -0
- package/src/internal/persistence.ts +11 -0
- package/src/internal/rcRef.ts +4 -1
- package/src/internal/schema/toCodeDocument.ts +7 -3
- package/src/internal/trie.ts +43 -14
- package/src/internal/ulid.ts +44 -0
- package/src/internal/uuid.ts +57 -16
- package/src/internal/version.ts +1 -1
- package/src/observability/Otlp.ts +21 -4
- package/src/observability/OtlpResource.ts +9 -1
- package/src/persistence/KeyValueStore.ts +219 -22
- package/src/persistence/PersistedQueue.ts +128 -59
- package/src/persistence/Persistence.ts +20 -13
- package/src/process/ChildProcess.ts +255 -260
- package/src/process/ChildProcessSpawner.ts +5 -0
- package/src/process/CommandLine.ts +630 -0
- package/src/process/index.ts +6 -0
- package/src/reactivity/Atom.ts +5 -5
- package/src/reactivity/AtomRegistry.ts +1 -1
- package/src/rpc/RpcClient.ts +62 -26
- package/src/rpc/RpcMessage.ts +10 -1
- package/src/rpc/RpcSerialization.ts +3 -0
- package/src/rpc/RpcServer.ts +176 -78
- package/src/schema/Model.ts +26 -3
- package/src/sql/Migrator.ts +4 -63
- package/src/sql/MigratorFileSystem.ts +71 -0
- package/src/sql/SqlClient.ts +2 -16
- package/src/sql/Statement.ts +8 -7
- package/src/sql/index.ts +1 -1
- package/src/testing/index.ts +0 -6
- package/src/workflow/internal/crypto.ts +15 -11
- package/dist/Crypto.d.ts +0 -244
- package/dist/Crypto.d.ts.map +0 -1
- package/dist/Crypto.js +0 -139
- package/dist/Crypto.js.map +0 -1
- package/dist/testing/TestCrypto.d.ts +0 -73
- package/dist/testing/TestCrypto.d.ts.map +0 -1
- package/dist/testing/TestCrypto.js +0 -87
- package/dist/testing/TestCrypto.js.map +0 -1
- package/src/Crypto.ts +0 -347
- package/src/testing/TestCrypto.ts +0 -94
|
@@ -5,29 +5,45 @@
|
|
|
5
5
|
* `EventLogServerEncrypted` when entries should be stored without exposing
|
|
6
6
|
* plaintext event data to the database. It persists the server remote id,
|
|
7
7
|
* session authentication bindings, and encrypted batch tables, assigns stable
|
|
8
|
-
* sequence numbers,
|
|
9
|
-
* encrypting writes and decrypting reads.
|
|
8
|
+
* sequence numbers, streams changes, and stores encrypted compaction snapshots.
|
|
9
|
+
* Clients remain responsible for encrypting writes and decrypting reads.
|
|
10
10
|
*
|
|
11
11
|
* @stability unstable
|
|
12
12
|
* @since 4.0.0
|
|
13
13
|
*/
|
|
14
14
|
import * as Arr from "../Array.ts"
|
|
15
15
|
import type { NonEmptyArray } from "../Array.ts"
|
|
16
|
+
import * as Cause from "../Cause.ts"
|
|
17
|
+
import * as Clock from "../Clock.ts"
|
|
18
|
+
import * as Digest from "../crypto/Digest.ts"
|
|
19
|
+
import * as SecureRandom from "../crypto/SecureRandom.ts"
|
|
20
|
+
import * as Duration from "../Duration.ts"
|
|
16
21
|
import * as Effect from "../Effect.ts"
|
|
17
22
|
import * as Layer from "../Layer.ts"
|
|
23
|
+
import * as Option from "../Option.ts"
|
|
18
24
|
import * as PubSub from "../PubSub.ts"
|
|
19
25
|
import * as RcMap from "../RcMap.ts"
|
|
26
|
+
import * as Schedule from "../Schedule.ts"
|
|
20
27
|
import * as Schema from "../Schema.ts"
|
|
21
28
|
import type * as Scope from "../Scope.ts"
|
|
22
29
|
import * as Semaphore from "../Semaphore.ts"
|
|
23
30
|
import * as SqlClient from "../sql/SqlClient.ts"
|
|
24
|
-
import
|
|
31
|
+
import * as SqlError from "../sql/SqlError.ts"
|
|
25
32
|
import type * as Statement from "../sql/Statement.ts"
|
|
26
33
|
import * as Stream from "../Stream.ts"
|
|
27
34
|
import { type EntryId, makeRemoteIdUnsafe, type RemoteId } from "./EventJournal.ts"
|
|
28
|
-
import * as EventLogEncryption from "./EventLogEncryption.ts"
|
|
29
|
-
import type { WriteAck } from "./EventLogMessage.ts"
|
|
35
|
+
import type * as EventLogEncryption from "./EventLogEncryption.ts"
|
|
36
|
+
import type { StoreId, WriteAck } from "./EventLogMessage.ts"
|
|
30
37
|
import * as EventLogServerEncrypted from "./EventLogServerEncrypted.ts"
|
|
38
|
+
import {
|
|
39
|
+
hourMillis,
|
|
40
|
+
lagHour,
|
|
41
|
+
logCommit,
|
|
42
|
+
logNoOp,
|
|
43
|
+
logRollback,
|
|
44
|
+
maxSegments,
|
|
45
|
+
positionsPerSegment
|
|
46
|
+
} from "./internal/compaction.ts"
|
|
31
47
|
|
|
32
48
|
/**
|
|
33
49
|
* Creates encrypted event-log server `Storage` backed by SQL.
|
|
@@ -38,6 +54,11 @@ import * as EventLogServerEncrypted from "./EventLogServerEncrypted.ts"
|
|
|
38
54
|
* batches in dialect-specific tables, creating per-identity/store batch and entry
|
|
39
55
|
* id tables as needed.
|
|
40
56
|
*
|
|
57
|
+
* The storage always provides `compaction`. Its tables are created on startup
|
|
58
|
+
* and when a store is first opened. A sweep in the surrounding scope prunes
|
|
59
|
+
* superseded generations every `sweepInterval`. Startup dies on invalid
|
|
60
|
+
* compaction options (see `EventLogServerEncrypted.makeCompactionConfig`).
|
|
61
|
+
*
|
|
41
62
|
* **Gotchas**
|
|
42
63
|
*
|
|
43
64
|
* Storage does not retry sequence conflicts between competing instances.
|
|
@@ -46,6 +67,14 @@ import * as EventLogServerEncrypted from "./EventLogServerEncrypted.ts"
|
|
|
46
67
|
* are local to each storage instance; clients on other instances must reconnect
|
|
47
68
|
* to read those writes.
|
|
48
69
|
*
|
|
70
|
+
* Inside a caller's transaction, `write` and `compaction.rollback` publish to
|
|
71
|
+
* live readers after the outermost commit, and not at all if it rolls back.
|
|
72
|
+
* Concurrent caller transactions may reach live readers out of sequence order.
|
|
73
|
+
* `changes` always reads in committed order.
|
|
74
|
+
*
|
|
75
|
+
* Instances sharing a database should share compaction options. The instance
|
|
76
|
+
* that writes a generation or stage fixes its prune time or expiry.
|
|
77
|
+
*
|
|
49
78
|
* @stability unstable
|
|
50
79
|
* @category constructors
|
|
51
80
|
* @since 4.0.0
|
|
@@ -53,14 +82,20 @@ import * as EventLogServerEncrypted from "./EventLogServerEncrypted.ts"
|
|
|
53
82
|
export const makeStorage = (options?: {
|
|
54
83
|
readonly entryTablePrefix?: string
|
|
55
84
|
readonly remoteIdTable?: string
|
|
85
|
+
readonly compaction?: EventLogServerEncrypted.CompactionOptions | undefined
|
|
56
86
|
}): Effect.Effect<
|
|
57
87
|
EventLogServerEncrypted.Storage["Service"],
|
|
58
88
|
SqlError.SqlError,
|
|
59
|
-
SqlClient.SqlClient |
|
|
89
|
+
SqlClient.SqlClient | Digest.Digest | Scope.Scope
|
|
60
90
|
> =>
|
|
61
91
|
Effect.gen(function*() {
|
|
62
|
-
const
|
|
92
|
+
const digest = yield* Digest.Digest
|
|
63
93
|
const sql = (yield* SqlClient.SqlClient).withoutTransforms()
|
|
94
|
+
const config = yield* EventLogServerEncrypted.makeCompactionConfig(options?.compaction)
|
|
95
|
+
const graceMillis = Duration.toMillis(config.grace)
|
|
96
|
+
const lagMillis = Duration.toMillis(config.lag)
|
|
97
|
+
const stageTtlMillis = Duration.toMillis(config.stageTtl)
|
|
98
|
+
const sweepIntervalMillis = Duration.toMillis(config.sweepInterval)
|
|
64
99
|
|
|
65
100
|
const tablePrefix = options?.entryTablePrefix ?? "effect_events"
|
|
66
101
|
const remoteIdTable = options?.remoteIdTable ?? "effect_remote_id"
|
|
@@ -121,6 +156,120 @@ export const makeStorage = (options?: {
|
|
|
121
156
|
)`
|
|
122
157
|
})
|
|
123
158
|
|
|
159
|
+
const types = sql.onDialectOrElse({
|
|
160
|
+
pg: () => ({ id: "TEXT", int: "INTEGER", bigint: "BIGINT", bytes: "BYTEA", entryId: "BYTEA", iv: "BYTEA" }),
|
|
161
|
+
mysql: () => ({
|
|
162
|
+
id: "VARCHAR(36)",
|
|
163
|
+
int: "INT",
|
|
164
|
+
bigint: "BIGINT",
|
|
165
|
+
bytes: "LONGBLOB",
|
|
166
|
+
entryId: "BINARY(16)",
|
|
167
|
+
iv: "BINARY(12)"
|
|
168
|
+
}),
|
|
169
|
+
mssql: () => ({
|
|
170
|
+
id: "NVARCHAR(36)",
|
|
171
|
+
int: "INT",
|
|
172
|
+
bigint: "BIGINT",
|
|
173
|
+
bytes: "VARBINARY(MAX)",
|
|
174
|
+
entryId: "VARBINARY(16)",
|
|
175
|
+
iv: "VARBINARY(12)"
|
|
176
|
+
}),
|
|
177
|
+
orElse: () => ({ id: "TEXT", int: "INTEGER", bigint: "INTEGER", bytes: "BLOB", entryId: "BLOB", iv: "BLOB" })
|
|
178
|
+
})
|
|
179
|
+
// Two instances can open the same table at once, and PostgreSQL and SQL
|
|
180
|
+
// Server can fail the loser of a concurrent create, so creation retries.
|
|
181
|
+
const createTable = (name: string, columns: string, indexes: ReadonlyArray<string> = []) =>
|
|
182
|
+
sql.onDialectOrElse({
|
|
183
|
+
mysql: () => {
|
|
184
|
+
const definitions = [columns, ...indexes.map((index) => `INDEX (${index})`)].join(", ")
|
|
185
|
+
return Effect.asVoid(sql`CREATE TABLE IF NOT EXISTS ${sql(name)} (${sql.literal(definitions)})`)
|
|
186
|
+
},
|
|
187
|
+
mssql: () =>
|
|
188
|
+
sql`IF OBJECT_ID(${name}, N'U') IS NULL CREATE TABLE ${sql(name)} (${sql.literal(columns)})`.pipe(
|
|
189
|
+
Effect.andThen(Effect.forEach(indexes, (index, i) => {
|
|
190
|
+
const indexName = `${name}_i${i}`
|
|
191
|
+
return sql`
|
|
192
|
+
IF NOT EXISTS (SELECT 1 FROM sys.indexes WHERE name = ${indexName} AND object_id = OBJECT_ID(${name}))
|
|
193
|
+
CREATE INDEX ${sql(indexName)} ON ${sql(name)} (${sql.literal(index)})`
|
|
194
|
+
}, { discard: true }))
|
|
195
|
+
),
|
|
196
|
+
orElse: () =>
|
|
197
|
+
sql`CREATE TABLE IF NOT EXISTS ${sql(name)} (${sql.literal(columns)})`.pipe(
|
|
198
|
+
Effect.andThen(Effect.forEach(
|
|
199
|
+
indexes,
|
|
200
|
+
(index, i) =>
|
|
201
|
+
sql`CREATE INDEX IF NOT EXISTS ${sql(`${name}_i${i}`)} ON ${sql(name)} (${sql.literal(index)})`,
|
|
202
|
+
{ discard: true }
|
|
203
|
+
))
|
|
204
|
+
)
|
|
205
|
+
}).pipe(Effect.retry({ times: 3 }))
|
|
206
|
+
|
|
207
|
+
const storesTable = `${tablePrefix}_compaction_stores`
|
|
208
|
+
const bucketsTable = `${tablePrefix}_compaction_buckets`
|
|
209
|
+
const activationsTable = `${tablePrefix}_compaction_activations`
|
|
210
|
+
const stagesTable = `${tablePrefix}_compaction_stages`
|
|
211
|
+
const storesSql = sql(storesTable)
|
|
212
|
+
const bucketsSql = sql(bucketsTable)
|
|
213
|
+
const activationsSql = sql(activationsTable)
|
|
214
|
+
const stagesSql = sql(stagesTable)
|
|
215
|
+
|
|
216
|
+
// One row per store, keyed by the scope hash its tables are named after.
|
|
217
|
+
// "Nothing" is 0 for the base, `pruned_through` and the lag head.
|
|
218
|
+
yield* createTable(
|
|
219
|
+
storesTable,
|
|
220
|
+
`scope ${types.id} PRIMARY KEY,
|
|
221
|
+
next_sequence ${types.int} NOT NULL,
|
|
222
|
+
active_activation ${types.id} NOT NULL,
|
|
223
|
+
active_batch_set ${types.id} NULL,
|
|
224
|
+
base_sequence ${types.int} NOT NULL,
|
|
225
|
+
active_kept_count ${types.int} NOT NULL,
|
|
226
|
+
no_op_at ${types.int} NULL,
|
|
227
|
+
no_op_count ${types.int} NOT NULL,
|
|
228
|
+
open_stage ${types.id} NULL,
|
|
229
|
+
next_sweep_at ${types.bigint} NULL,
|
|
230
|
+
sweep_lease_until ${types.bigint} NULL,
|
|
231
|
+
sweep_lease_owner ${types.id} NULL,
|
|
232
|
+
pruned_through ${types.int} NOT NULL`,
|
|
233
|
+
["next_sweep_at"]
|
|
234
|
+
)
|
|
235
|
+
// The highest last sequence stored in each storage-time hour.
|
|
236
|
+
yield* createTable(
|
|
237
|
+
bucketsTable,
|
|
238
|
+
`scope ${types.id} NOT NULL,
|
|
239
|
+
bucket_hour ${types.bigint} NOT NULL,
|
|
240
|
+
last_sequence ${types.int} NOT NULL,
|
|
241
|
+
PRIMARY KEY (scope, bucket_hour)`
|
|
242
|
+
)
|
|
243
|
+
// A `batch_set` of null means the originals only. `prunable_at` is set when
|
|
244
|
+
// the activation is superseded or withdrawn.
|
|
245
|
+
yield* createTable(
|
|
246
|
+
activationsTable,
|
|
247
|
+
`id ${types.id} PRIMARY KEY,
|
|
248
|
+
scope ${types.id} NOT NULL,
|
|
249
|
+
ordinal ${types.int} NOT NULL,
|
|
250
|
+
batch_set ${types.id} NULL,
|
|
251
|
+
base_sequence ${types.int} NOT NULL,
|
|
252
|
+
kept_count ${types.int} NULL,
|
|
253
|
+
previous ${types.id} NULL,
|
|
254
|
+
superseded_at ${types.bigint} NULL,
|
|
255
|
+
prunable_at ${types.bigint} NULL,
|
|
256
|
+
prune_started_at ${types.bigint} NULL`,
|
|
257
|
+
["scope, ordinal"]
|
|
258
|
+
)
|
|
259
|
+
// A stage's pending batch set is its own id. A committed stage's row is
|
|
260
|
+
// deleted at commit. Other stages go from `open` to `closed`, and the
|
|
261
|
+
// sweep deletes what they staged, then the row.
|
|
262
|
+
yield* createTable(
|
|
263
|
+
stagesTable,
|
|
264
|
+
`id ${types.id} PRIMARY KEY,
|
|
265
|
+
scope ${types.id} NOT NULL,
|
|
266
|
+
state ${types.id} NOT NULL,
|
|
267
|
+
pinned ${types.id} NOT NULL,
|
|
268
|
+
through_sequence ${types.int} NOT NULL,
|
|
269
|
+
expires_at ${types.bigint} NOT NULL`,
|
|
270
|
+
["scope, state", "state, expires_at"]
|
|
271
|
+
)
|
|
272
|
+
|
|
124
273
|
const remoteId = yield* sql<{ remote_id: Uint8Array }>`SELECT remote_id FROM ${remoteIdTableSql}`.pipe(
|
|
125
274
|
Effect.flatMap((results) => {
|
|
126
275
|
if (results.length > 0) {
|
|
@@ -134,120 +283,348 @@ export const makeStorage = (options?: {
|
|
|
134
283
|
})
|
|
135
284
|
)
|
|
136
285
|
|
|
137
|
-
const
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
pg: () =>
|
|
147
|
-
sql`
|
|
148
|
-
CREATE TABLE IF NOT EXISTS ${tableSql} (
|
|
149
|
-
sequence INTEGER PRIMARY KEY,
|
|
150
|
-
last_sequence INTEGER NOT NULL,
|
|
151
|
-
iv BYTEA NOT NULL,
|
|
152
|
-
entry_ids BYTEA NOT NULL,
|
|
153
|
-
encrypted_batch BYTEA NOT NULL
|
|
154
|
-
)`.pipe(
|
|
155
|
-
Effect.andThen(sql`CREATE INDEX IF NOT EXISTS ${indexSql} ON ${tableSql} (last_sequence)`),
|
|
156
|
-
Effect.andThen(sql`
|
|
157
|
-
CREATE TABLE IF NOT EXISTS ${idsTableSql} (
|
|
158
|
-
entry_id BYTEA PRIMARY KEY,
|
|
159
|
-
sequence INTEGER NOT NULL
|
|
160
|
-
)`)
|
|
161
|
-
),
|
|
162
|
-
mysql: () =>
|
|
163
|
-
sql`
|
|
164
|
-
CREATE TABLE IF NOT EXISTS ${tableSql} (
|
|
165
|
-
sequence INT PRIMARY KEY,
|
|
166
|
-
last_sequence INT NOT NULL,
|
|
167
|
-
iv BINARY(12) NOT NULL,
|
|
168
|
-
entry_ids LONGBLOB NOT NULL,
|
|
169
|
-
encrypted_batch LONGBLOB NOT NULL,
|
|
170
|
-
INDEX (last_sequence)
|
|
171
|
-
)`.pipe(
|
|
172
|
-
Effect.andThen(sql`
|
|
173
|
-
CREATE TABLE IF NOT EXISTS ${idsTableSql} (
|
|
174
|
-
entry_id BINARY(16) PRIMARY KEY,
|
|
175
|
-
sequence INT NOT NULL
|
|
176
|
-
)`)
|
|
177
|
-
),
|
|
178
|
-
mssql: () =>
|
|
179
|
-
sql`
|
|
180
|
-
IF OBJECT_ID(${table}, N'U') IS NULL
|
|
181
|
-
CREATE TABLE ${tableSql} (
|
|
182
|
-
sequence INT PRIMARY KEY,
|
|
183
|
-
last_sequence INT NOT NULL,
|
|
184
|
-
iv VARBINARY(12) NOT NULL,
|
|
185
|
-
entry_ids VARBINARY(MAX) NOT NULL,
|
|
186
|
-
encrypted_batch VARBINARY(MAX) NOT NULL,
|
|
187
|
-
INDEX ${indexSql} NONCLUSTERED (last_sequence)
|
|
188
|
-
)`.pipe(
|
|
189
|
-
Effect.andThen(sql`
|
|
190
|
-
IF OBJECT_ID(${`${table}_ids`}, N'U') IS NULL
|
|
191
|
-
CREATE TABLE ${idsTableSql} (
|
|
192
|
-
entry_id VARBINARY(16) PRIMARY KEY,
|
|
193
|
-
sequence INT NOT NULL
|
|
194
|
-
)`)
|
|
195
|
-
),
|
|
196
|
-
orElse: () =>
|
|
197
|
-
sql`
|
|
198
|
-
CREATE TABLE IF NOT EXISTS ${tableSql} (
|
|
199
|
-
sequence INTEGER PRIMARY KEY,
|
|
200
|
-
last_sequence INTEGER NOT NULL,
|
|
201
|
-
iv BLOB NOT NULL,
|
|
202
|
-
entry_ids BLOB NOT NULL,
|
|
203
|
-
encrypted_batch BLOB NOT NULL
|
|
204
|
-
)`.pipe(
|
|
205
|
-
Effect.andThen(sql`CREATE INDEX IF NOT EXISTS ${indexSql} ON ${tableSql} (last_sequence)`),
|
|
206
|
-
Effect.andThen(sql`
|
|
207
|
-
CREATE TABLE IF NOT EXISTS ${idsTableSql} (
|
|
208
|
-
entry_id BLOB PRIMARY KEY,
|
|
209
|
-
sequence INTEGER NOT NULL
|
|
210
|
-
)`)
|
|
211
|
-
)
|
|
212
|
-
})
|
|
286
|
+
const scopeTables = (scope: string) => {
|
|
287
|
+
const table = `${tablePrefix}_${scope}`
|
|
288
|
+
return {
|
|
289
|
+
tableSql: sql(table),
|
|
290
|
+
idsTableSql: sql(`${table}_ids`),
|
|
291
|
+
snapshotTableSql: sql(`${table}_snapshot`)
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
type ScopeTables = ReturnType<typeof scopeTables>
|
|
213
295
|
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
296
|
+
const ensureTables = Effect.fnUntraced(function*(scope: string, tables: ScopeTables) {
|
|
297
|
+
const table = `${tablePrefix}_${scope}`
|
|
298
|
+
const { idsTableSql, tableSql } = tables
|
|
299
|
+
const indexSql = sql(`${table}_last_sequence`)
|
|
300
|
+
|
|
301
|
+
yield* sql.onDialectOrElse({
|
|
302
|
+
pg: () =>
|
|
303
|
+
sql`
|
|
304
|
+
CREATE TABLE IF NOT EXISTS ${tableSql} (
|
|
305
|
+
sequence INTEGER PRIMARY KEY,
|
|
306
|
+
last_sequence INTEGER NOT NULL,
|
|
307
|
+
iv BYTEA NOT NULL,
|
|
308
|
+
entry_ids BYTEA NOT NULL,
|
|
309
|
+
encrypted_batch BYTEA NOT NULL
|
|
310
|
+
)`.pipe(
|
|
311
|
+
Effect.andThen(sql`CREATE INDEX IF NOT EXISTS ${indexSql} ON ${tableSql} (last_sequence)`),
|
|
312
|
+
Effect.andThen(sql`
|
|
313
|
+
CREATE TABLE IF NOT EXISTS ${idsTableSql} (
|
|
314
|
+
entry_id BYTEA PRIMARY KEY,
|
|
315
|
+
sequence INTEGER NOT NULL
|
|
316
|
+
)`)
|
|
317
|
+
),
|
|
318
|
+
mysql: () =>
|
|
319
|
+
sql`
|
|
320
|
+
CREATE TABLE IF NOT EXISTS ${tableSql} (
|
|
321
|
+
sequence INT PRIMARY KEY,
|
|
322
|
+
last_sequence INT NOT NULL,
|
|
323
|
+
iv BINARY(12) NOT NULL,
|
|
324
|
+
entry_ids LONGBLOB NOT NULL,
|
|
325
|
+
encrypted_batch LONGBLOB NOT NULL,
|
|
326
|
+
INDEX (last_sequence)
|
|
327
|
+
)`.pipe(
|
|
328
|
+
Effect.andThen(sql`
|
|
329
|
+
CREATE TABLE IF NOT EXISTS ${idsTableSql} (
|
|
330
|
+
entry_id BINARY(16) PRIMARY KEY,
|
|
331
|
+
sequence INT NOT NULL
|
|
332
|
+
)`)
|
|
333
|
+
),
|
|
334
|
+
mssql: () =>
|
|
335
|
+
sql`
|
|
336
|
+
IF OBJECT_ID(${table}, N'U') IS NULL
|
|
337
|
+
CREATE TABLE ${tableSql} (
|
|
338
|
+
sequence INT PRIMARY KEY,
|
|
339
|
+
last_sequence INT NOT NULL,
|
|
340
|
+
iv VARBINARY(12) NOT NULL,
|
|
341
|
+
entry_ids VARBINARY(MAX) NOT NULL,
|
|
342
|
+
encrypted_batch VARBINARY(MAX) NOT NULL,
|
|
343
|
+
INDEX ${indexSql} NONCLUSTERED (last_sequence)
|
|
344
|
+
)`.pipe(
|
|
345
|
+
Effect.andThen(sql`
|
|
346
|
+
IF OBJECT_ID(${`${table}_ids`}, N'U') IS NULL
|
|
347
|
+
CREATE TABLE ${idsTableSql} (
|
|
348
|
+
entry_id VARBINARY(16) PRIMARY KEY,
|
|
349
|
+
sequence INT NOT NULL
|
|
350
|
+
)`)
|
|
351
|
+
),
|
|
352
|
+
orElse: () =>
|
|
353
|
+
sql`
|
|
354
|
+
CREATE TABLE IF NOT EXISTS ${tableSql} (
|
|
355
|
+
sequence INTEGER PRIMARY KEY,
|
|
356
|
+
last_sequence INTEGER NOT NULL,
|
|
357
|
+
iv BLOB NOT NULL,
|
|
358
|
+
entry_ids BLOB NOT NULL,
|
|
359
|
+
encrypted_batch BLOB NOT NULL
|
|
360
|
+
)`.pipe(
|
|
361
|
+
Effect.andThen(sql`CREATE INDEX IF NOT EXISTS ${indexSql} ON ${tableSql} (last_sequence)`),
|
|
362
|
+
Effect.andThen(sql`
|
|
363
|
+
CREATE TABLE IF NOT EXISTS ${idsTableSql} (
|
|
364
|
+
entry_id BLOB PRIMARY KEY,
|
|
365
|
+
sequence INTEGER NOT NULL
|
|
366
|
+
)`)
|
|
367
|
+
)
|
|
368
|
+
}).pipe(Effect.retry({ times: 3 }))
|
|
369
|
+
// Staged and committed snapshot batches. `sequence` and
|
|
370
|
+
// `last_sequence` stay null until commit.
|
|
371
|
+
yield* createTable(
|
|
372
|
+
`${table}_snapshot`,
|
|
373
|
+
`batch_set ${types.id} NOT NULL,
|
|
374
|
+
batch_position ${types.bigint} NOT NULL,
|
|
375
|
+
sequence ${types.int} NULL,
|
|
376
|
+
last_sequence ${types.int} NULL,
|
|
377
|
+
entry_count ${types.int} NOT NULL,
|
|
378
|
+
iv ${types.iv} NOT NULL,
|
|
379
|
+
entry_ids ${types.bytes} NOT NULL,
|
|
380
|
+
encrypted_batch ${types.bytes} NOT NULL,
|
|
381
|
+
PRIMARY KEY (batch_set, batch_position)`,
|
|
382
|
+
["batch_set, last_sequence"]
|
|
383
|
+
)
|
|
384
|
+
yield* ensureStore(scope, tables)
|
|
223
385
|
})
|
|
224
386
|
|
|
225
|
-
//
|
|
226
|
-
//
|
|
227
|
-
const
|
|
387
|
+
// Stores whose tables and row this instance has ensured, so a store
|
|
388
|
+
// reopened after idling skips the DDL.
|
|
389
|
+
const ensured = new Set<string>()
|
|
390
|
+
const lookup = Effect.fnUntraced(function*(scopeKey: string) {
|
|
391
|
+
const scope = (yield* digest.digestHex(Digest.sha256, new TextEncoder().encode(scopeKey))).slice(0, 16)
|
|
392
|
+
const tables = scopeTables(scope)
|
|
393
|
+
if (!ensured.has(scope)) {
|
|
394
|
+
yield* ensureTables(scope, tables)
|
|
395
|
+
ensured.add(scope)
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
const pubsub = yield* Effect.acquireRelease(
|
|
399
|
+
PubSub.unbounded<EventLogEncryption.EncryptedRemoteBatch>(),
|
|
400
|
+
PubSub.shutdown
|
|
401
|
+
)
|
|
402
|
+
const semaphore = yield* Semaphore.make(1)
|
|
403
|
+
return { pubsub, semaphore, scope, ...tables } as const
|
|
404
|
+
}, withTracerDisabled)
|
|
405
|
+
type StoreResources = Effect.Success<ReturnType<typeof lookup>>
|
|
406
|
+
const resources = yield* RcMap.make({ lookup, idleTimeToLive: "5 minutes" })
|
|
407
|
+
|
|
408
|
+
// Statements that return rows the caller decodes.
|
|
409
|
+
const query = (statement: Statement.Statement<unknown>) =>
|
|
410
|
+
statement as unknown as Effect.Effect<ReadonlyArray<Record<string, unknown>>, SqlError.SqlError>
|
|
411
|
+
|
|
412
|
+
// Inserts the rows whose `keys` are not stored yet, leaving stored rows as
|
|
413
|
+
// they are, and returns how many were inserted. Every row has the same
|
|
414
|
+
// columns, and SQL Server's MERGE needs them unique by key.
|
|
415
|
+
const insertNew = (
|
|
416
|
+
tableSql: Statement.Identifier,
|
|
417
|
+
rows: Arr.NonEmptyReadonlyArray<Record<string, unknown>>,
|
|
418
|
+
keys: ReadonlyArray<string>
|
|
419
|
+
) =>
|
|
228
420
|
sql.onDialectOrElse({
|
|
229
421
|
mysql: () =>
|
|
230
|
-
sql`INSERT IGNORE INTO ${
|
|
422
|
+
sql`INSERT IGNORE INTO ${tableSql} ${sql.insert(rows)}`.raw.pipe(
|
|
231
423
|
Effect.map((result) => (result as { affectedRows: number }).affectedRows)
|
|
232
424
|
),
|
|
425
|
+
mssql: () => {
|
|
426
|
+
const columns = Object.keys(rows[0])
|
|
427
|
+
const values = sql.join(", ", false)(
|
|
428
|
+
rows.map((row) => sql`(${sql.join(", ", false)(columns.map((column) => sql`${row[column]}`))})`)
|
|
429
|
+
)
|
|
430
|
+
return sql`
|
|
431
|
+
MERGE ${tableSql} WITH (HOLDLOCK) AS target
|
|
432
|
+
USING (VALUES ${values}) AS source (${sql.literal(columns.join(", "))})
|
|
433
|
+
ON ${sql.literal(keys.map((key) => `target.${key} = source.${key}`).join(" AND "))}
|
|
434
|
+
WHEN NOT MATCHED THEN
|
|
435
|
+
INSERT (${sql.literal(columns.join(", "))})
|
|
436
|
+
VALUES (${sql.literal(columns.map((column) => `source.${column}`).join(", "))})
|
|
437
|
+
OUTPUT inserted.${sql.literal(keys[0])};
|
|
438
|
+
`.pipe(Effect.map((inserted) => inserted.length))
|
|
439
|
+
},
|
|
440
|
+
orElse: () =>
|
|
441
|
+
sql`INSERT INTO ${tableSql} ${sql.insert(rows)} ON CONFLICT DO NOTHING RETURNING ${sql(keys[0])}`.pipe(
|
|
442
|
+
Effect.map((inserted) => inserted.length)
|
|
443
|
+
)
|
|
444
|
+
})
|
|
445
|
+
|
|
446
|
+
const upsertBucket = (scope: string, hour: number, lastSequence: number) =>
|
|
447
|
+
sql.onDialectOrElse({
|
|
448
|
+
pg: () =>
|
|
449
|
+
sql`
|
|
450
|
+
INSERT INTO ${bucketsSql} (scope, bucket_hour, last_sequence) VALUES (${scope}, ${hour}, ${lastSequence})
|
|
451
|
+
ON CONFLICT (scope, bucket_hour)
|
|
452
|
+
DO UPDATE SET last_sequence = GREATEST(${bucketsSql}.last_sequence, EXCLUDED.last_sequence)`,
|
|
453
|
+
mysql: () =>
|
|
454
|
+
sql`
|
|
455
|
+
INSERT INTO ${bucketsSql} (scope, bucket_hour, last_sequence) VALUES (${scope}, ${hour}, ${lastSequence})
|
|
456
|
+
ON DUPLICATE KEY UPDATE last_sequence = GREATEST(last_sequence, VALUES(last_sequence))`,
|
|
233
457
|
mssql: () =>
|
|
234
458
|
sql`
|
|
235
|
-
MERGE ${
|
|
236
|
-
USING (VALUES ${
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
459
|
+
MERGE ${bucketsSql} WITH (HOLDLOCK) AS target
|
|
460
|
+
USING (VALUES (${scope}, ${hour}, ${lastSequence})) AS source (scope, bucket_hour, last_sequence)
|
|
461
|
+
ON target.scope = source.scope AND target.bucket_hour = source.bucket_hour
|
|
462
|
+
WHEN MATCHED AND source.last_sequence > target.last_sequence THEN
|
|
463
|
+
UPDATE SET last_sequence = source.last_sequence
|
|
240
464
|
WHEN NOT MATCHED THEN
|
|
241
|
-
INSERT (
|
|
242
|
-
|
|
243
|
-
`.pipe(Effect.map((inserted) => inserted.length)),
|
|
465
|
+
INSERT (scope, bucket_hour, last_sequence)
|
|
466
|
+
VALUES (source.scope, source.bucket_hour, source.last_sequence);`,
|
|
244
467
|
orElse: () =>
|
|
245
468
|
sql`
|
|
246
|
-
INSERT INTO ${
|
|
247
|
-
ON CONFLICT
|
|
248
|
-
|
|
469
|
+
INSERT INTO ${bucketsSql} (scope, bucket_hour, last_sequence) VALUES (${scope}, ${hour}, ${lastSequence})
|
|
470
|
+
ON CONFLICT (scope, bucket_hour)
|
|
471
|
+
DO UPDATE SET last_sequence = MAX(${bucketsSql}.last_sequence, excluded.last_sequence)`
|
|
249
472
|
})
|
|
250
473
|
|
|
474
|
+
const maxLastSequence = (tableSql: Statement.Identifier) =>
|
|
475
|
+
Effect.map(
|
|
476
|
+
query(sql`SELECT MAX(last_sequence) AS last_sequence FROM ${tableSql}`),
|
|
477
|
+
([row]) => Number(row?.last_sequence ?? 0)
|
|
478
|
+
)
|
|
479
|
+
|
|
480
|
+
// Creates the store row on first open. The first instance to insert it
|
|
481
|
+
// also records the originals activation and seeds a bucket at the current
|
|
482
|
+
// last sequence, so data written before the upgrade becomes compactable.
|
|
483
|
+
const ensureStore = Effect.fnUntraced(function*(scope: string, tables: ScopeTables) {
|
|
484
|
+
const existing = yield* query(sql`SELECT scope FROM ${storesSql} WHERE scope = ${scope}`)
|
|
485
|
+
if (existing.length > 0) return
|
|
486
|
+
const now = yield* Clock.currentTimeMillis
|
|
487
|
+
yield* sql.withTransaction(Effect.gen(function*() {
|
|
488
|
+
const lastSequence = yield* maxLastSequence(tables.tableSql)
|
|
489
|
+
const originals = SecureRandom.uuidV4Unsafe()
|
|
490
|
+
const inserted = yield* insertNew(storesSql, [{
|
|
491
|
+
scope,
|
|
492
|
+
next_sequence: lastSequence + 1,
|
|
493
|
+
active_activation: originals,
|
|
494
|
+
active_batch_set: null,
|
|
495
|
+
base_sequence: 0,
|
|
496
|
+
active_kept_count: 0,
|
|
497
|
+
no_op_at: null,
|
|
498
|
+
no_op_count: 0,
|
|
499
|
+
open_stage: null,
|
|
500
|
+
next_sweep_at: null,
|
|
501
|
+
sweep_lease_until: null,
|
|
502
|
+
sweep_lease_owner: null,
|
|
503
|
+
pruned_through: 0
|
|
504
|
+
}], ["scope"])
|
|
505
|
+
if (inserted === 0) return
|
|
506
|
+
yield* sql`INSERT INTO ${activationsSql} ${
|
|
507
|
+
sql.insert({
|
|
508
|
+
id: originals,
|
|
509
|
+
scope,
|
|
510
|
+
ordinal: 0,
|
|
511
|
+
batch_set: null,
|
|
512
|
+
base_sequence: 0,
|
|
513
|
+
kept_count: null,
|
|
514
|
+
previous: null,
|
|
515
|
+
superseded_at: null,
|
|
516
|
+
prunable_at: null,
|
|
517
|
+
prune_started_at: null
|
|
518
|
+
})
|
|
519
|
+
}`
|
|
520
|
+
if (lastSequence > 0) yield* upsertBucket(scope, Math.floor(now / hourMillis), lastSequence)
|
|
521
|
+
}))
|
|
522
|
+
})
|
|
523
|
+
|
|
524
|
+
const storeColumns = sql.literal(
|
|
525
|
+
"next_sequence, active_activation, active_batch_set, base_sequence, active_kept_count, no_op_at, " +
|
|
526
|
+
"no_op_count, open_stage, next_sweep_at, sweep_lease_until, sweep_lease_owner, pruned_through"
|
|
527
|
+
)
|
|
528
|
+
const selectStore = (scope: string) =>
|
|
529
|
+
Effect.map(
|
|
530
|
+
query(sql`SELECT ${storeColumns} FROM ${storesSql} WHERE scope = ${scope}`),
|
|
531
|
+
([row]) => row === undefined ? undefined : toStoreRow(row)
|
|
532
|
+
)
|
|
533
|
+
// Row locks held for the rest of the transaction. SQLite transactions take
|
|
534
|
+
// the database write lock instead.
|
|
535
|
+
const lockHint = sql.onDialectOrElse({
|
|
536
|
+
mssql: () => sql.literal("WITH (UPDLOCK, ROWLOCK, HOLDLOCK)"),
|
|
537
|
+
orElse: () => sql.literal("")
|
|
538
|
+
})
|
|
539
|
+
const forUpdate = sql.onDialectOrElse({
|
|
540
|
+
pg: () => sql.literal("FOR UPDATE"),
|
|
541
|
+
mysql: () => sql.literal("FOR UPDATE"),
|
|
542
|
+
orElse: () => sql.literal("")
|
|
543
|
+
})
|
|
544
|
+
const noClause = sql.literal("")
|
|
545
|
+
// Locks the store row for the rest of the transaction. Lock order is store
|
|
546
|
+
// row, then stage row, then snapshot rows.
|
|
547
|
+
const lockStore = (scope: string) =>
|
|
548
|
+
query(sql`SELECT ${storeColumns} FROM ${storesSql} ${lockHint} WHERE scope = ${scope} ${forUpdate}`).pipe(
|
|
549
|
+
Effect.flatMap(([row]) =>
|
|
550
|
+
row === undefined
|
|
551
|
+
? Effect.die(new Cause.NoSuchElementError(`No compaction store row for scope ${scope}`))
|
|
552
|
+
: Effect.succeed(toStoreRow(row))
|
|
553
|
+
)
|
|
554
|
+
)
|
|
555
|
+
|
|
556
|
+
const stageColumns = sql.literal("id, scope, state, pinned, through_sequence, expires_at")
|
|
557
|
+
const selectStage = (scope: string, stageId: string, lock: boolean) =>
|
|
558
|
+
query(sql`
|
|
559
|
+
SELECT ${stageColumns} FROM ${stagesSql} ${lock ? lockHint : noClause}
|
|
560
|
+
WHERE id = ${stageId} ${lock ? forUpdate : noClause}`).pipe(
|
|
561
|
+
Effect.map(([row]) => {
|
|
562
|
+
if (row === undefined) return undefined
|
|
563
|
+
const stage = toStageRow(row)
|
|
564
|
+
return stage.scope === scope ? stage : undefined
|
|
565
|
+
})
|
|
566
|
+
)
|
|
567
|
+
|
|
568
|
+
const selectActivation = (id: string) =>
|
|
569
|
+
Effect.map(
|
|
570
|
+
query(sql`SELECT ${activationColumns} FROM ${activationsSql} WHERE id = ${id}`),
|
|
571
|
+
([row]) => row === undefined ? undefined : toActivationRow(row)
|
|
572
|
+
)
|
|
573
|
+
const selectActivations = (scope: string) =>
|
|
574
|
+
Effect.map(
|
|
575
|
+
query(sql`SELECT ${activationColumns} FROM ${activationsSql} WHERE scope = ${scope} ORDER BY ordinal`),
|
|
576
|
+
(rows) => rows.map(toActivationRow)
|
|
577
|
+
)
|
|
578
|
+
const activationColumns = sql.literal(
|
|
579
|
+
"id, ordinal, batch_set, base_sequence, kept_count, previous, superseded_at, prunable_at, prune_started_at"
|
|
580
|
+
)
|
|
581
|
+
|
|
582
|
+
const releaseClaim = (scope: string, stageId: string) =>
|
|
583
|
+
sql`UPDATE ${storesSql} SET open_stage = NULL WHERE scope = ${scope} AND open_stage = ${stageId}`
|
|
584
|
+
// Closes an open stage, leaving what it staged to the sweep, and releases
|
|
585
|
+
// the store's busy claim.
|
|
586
|
+
const closeStage = (scope: string, stageId: string) =>
|
|
587
|
+
sql`UPDATE ${stagesSql} SET state = ${"closed"} WHERE id = ${stageId} AND state = ${"open"}`.pipe(
|
|
588
|
+
Effect.andThen(releaseClaim(scope, stageId))
|
|
589
|
+
)
|
|
590
|
+
|
|
591
|
+
const scheduleSweep = (scope: string, at: number) =>
|
|
592
|
+
sql`
|
|
593
|
+
UPDATE ${storesSql}
|
|
594
|
+
SET next_sweep_at = CASE WHEN next_sweep_at IS NULL OR next_sweep_at > ${at} THEN ${at} ELSE next_sweep_at END
|
|
595
|
+
WHERE scope = ${scope}`
|
|
596
|
+
|
|
597
|
+
// The clauses that cap a statement at `rows` rows, for `SELECT ${top} ... ${limit}`.
|
|
598
|
+
const rowCap = (rows: number) => ({
|
|
599
|
+
top: sql.onDialectOrElse({ mssql: () => sql.literal(`TOP (${rows})`), orElse: () => noClause }),
|
|
600
|
+
limit: sql.onDialectOrElse({ mssql: () => noClause, orElse: () => sql.literal(`LIMIT ${rows}`) })
|
|
601
|
+
})
|
|
602
|
+
const { limit, top } = rowCap(pageSize)
|
|
603
|
+
const { limit: chunkLimit, top: chunkTop } = rowCap(pruneChunkRows)
|
|
604
|
+
// Streams rows in key order, one page per statement.
|
|
605
|
+
const paged = <A>(
|
|
606
|
+
first: number,
|
|
607
|
+
page: (after: number) => Effect.Effect<ReadonlyArray<A>, SqlError.SqlError>,
|
|
608
|
+
key: (row: A) => number
|
|
609
|
+
) =>
|
|
610
|
+
Stream.paginate(first, (after) =>
|
|
611
|
+
Effect.map(page(after), (rows) =>
|
|
612
|
+
[
|
|
613
|
+
rows,
|
|
614
|
+
rows.length < pageSize ? Option.none<number>() : Option.some(key(rows[rows.length - 1]))
|
|
615
|
+
] as const))
|
|
616
|
+
|
|
617
|
+
const setRows = (snapshotTableSql: Statement.Identifier, batchSet: string) =>
|
|
618
|
+
paged(
|
|
619
|
+
-1,
|
|
620
|
+
(after) =>
|
|
621
|
+
query(sql`
|
|
622
|
+
SELECT ${top} batch_position, sequence, entry_ids FROM ${snapshotTableSql}
|
|
623
|
+
WHERE batch_set = ${batchSet} AND batch_position > ${after}
|
|
624
|
+
ORDER BY batch_position ${limit}`),
|
|
625
|
+
(row) => Number(row.batch_position)
|
|
626
|
+
)
|
|
627
|
+
|
|
251
628
|
const getSessionAuthBinding = (publicKey: string) =>
|
|
252
629
|
sql`
|
|
253
630
|
SELECT public_key, signing_public_key
|
|
@@ -262,6 +639,644 @@ export const makeStorage = (options?: {
|
|
|
262
639
|
Effect.orDie
|
|
263
640
|
)
|
|
264
641
|
|
|
642
|
+
// Runs `f` with the store's resources and the time, read before any
|
|
643
|
+
// transaction starts. SQL failures are defects.
|
|
644
|
+
const withStore = <A, E>(
|
|
645
|
+
publicKey: string,
|
|
646
|
+
storeId: StoreId,
|
|
647
|
+
f: (store: StoreResources, now: number) => Effect.Effect<A, E | SqlError.SqlError>
|
|
648
|
+
): Effect.Effect<A, Exclude<E, SqlError.SqlError>> =>
|
|
649
|
+
Effect.zip(RcMap.get(resources, makeEncryptedScopeKey(publicKey, storeId)), Clock.currentTimeMillis).pipe(
|
|
650
|
+
Effect.flatMap(([store, now]) => f(store, now)),
|
|
651
|
+
dieOnSqlError,
|
|
652
|
+
Effect.scoped,
|
|
653
|
+
withTracerDisabled
|
|
654
|
+
)
|
|
655
|
+
|
|
656
|
+
const notFound = (stageId: string) => Effect.fail(new EventLogServerEncrypted.CompactionNotFound({ stageId }))
|
|
657
|
+
|
|
658
|
+
// An open stage whose pin isn't being pruned yet, for Input and Fetch.
|
|
659
|
+
const findInputStage = Effect.fnUntraced(function*(scope: string, stageId: string, now: number) {
|
|
660
|
+
const stage = yield* selectStage(scope, stageId, false)
|
|
661
|
+
if (stage === undefined || stage.state !== "open" || stage.expiresAt <= now) return undefined
|
|
662
|
+
const pinned = yield* selectActivation(stage.pinned)
|
|
663
|
+
if (pinned === undefined || pinned.pruneStartedAt !== undefined) return undefined
|
|
664
|
+
return { stage, pinned }
|
|
665
|
+
})
|
|
666
|
+
// Fails a read that may have been truncated by a prune of its pin.
|
|
667
|
+
const recheckPin = (stageId: string, pinned: string) =>
|
|
668
|
+
Effect.flatMap(
|
|
669
|
+
selectActivation(pinned),
|
|
670
|
+
(activation) =>
|
|
671
|
+
activation === undefined || activation.pruneStartedAt !== undefined ? notFound(stageId) : Effect.void
|
|
672
|
+
)
|
|
673
|
+
|
|
674
|
+
const status = Effect.fnUntraced(function*({ scope, tableSql }: StoreResources, now: number) {
|
|
675
|
+
const store = (yield* selectStore(scope))!
|
|
676
|
+
const lastSequence = yield* maxLastSequence(tableSql)
|
|
677
|
+
const [lag] = yield* query(sql`
|
|
678
|
+
SELECT MAX(last_sequence) AS last_sequence FROM ${bucketsSql}
|
|
679
|
+
WHERE scope = ${scope} AND bucket_hour <= ${lagHour(now, lagMillis)}`)
|
|
680
|
+
return {
|
|
681
|
+
head: Math.max(store.nextSequence - 1, lastSequence),
|
|
682
|
+
base: store.base,
|
|
683
|
+
activeKeptCount: store.activeKeptCount,
|
|
684
|
+
lagHead: Number(lag?.last_sequence ?? 0),
|
|
685
|
+
noOp: store.noOpAt === undefined ? undefined : { at: store.noOpAt, count: store.noOpCount }
|
|
686
|
+
}
|
|
687
|
+
})
|
|
688
|
+
|
|
689
|
+
const begin = Effect.fnUntraced(function*({ scope }: StoreResources, now: number, through: number) {
|
|
690
|
+
const store = yield* lockStore(scope)
|
|
691
|
+
if (store.openStage !== undefined) {
|
|
692
|
+
const open = yield* selectStage(scope, store.openStage, true)
|
|
693
|
+
if (open !== undefined && open.state === "open") {
|
|
694
|
+
if (open.expiresAt > now) {
|
|
695
|
+
return { _tag: "Busy", expiresAt: open.expiresAt } as const
|
|
696
|
+
}
|
|
697
|
+
yield* closeStage(scope, open.id)
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
const stageId = SecureRandom.uuidV4Unsafe()
|
|
701
|
+
yield* sql`INSERT INTO ${stagesSql} ${
|
|
702
|
+
sql.insert({
|
|
703
|
+
id: stageId,
|
|
704
|
+
scope,
|
|
705
|
+
state: "open",
|
|
706
|
+
pinned: store.activeActivation,
|
|
707
|
+
through_sequence: through,
|
|
708
|
+
expires_at: now + stageTtlMillis
|
|
709
|
+
})
|
|
710
|
+
}`
|
|
711
|
+
yield* sql`UPDATE ${storesSql} SET open_stage = ${stageId} WHERE scope = ${scope}`
|
|
712
|
+
return { _tag: "Started", stageId } as const
|
|
713
|
+
}, sql.withTransaction)
|
|
714
|
+
|
|
715
|
+
const inputHeaders = Effect.fnUntraced(function*(
|
|
716
|
+
{ scope, snapshotTableSql, tableSql }: StoreResources,
|
|
717
|
+
now: number,
|
|
718
|
+
stageId: string
|
|
719
|
+
) {
|
|
720
|
+
const found = yield* findInputStage(scope, stageId, now)
|
|
721
|
+
if (found === undefined) {
|
|
722
|
+
return yield* notFound(stageId)
|
|
723
|
+
}
|
|
724
|
+
const { pinned, stage } = found
|
|
725
|
+
// Membership comes from the batch rows, never from the id table.
|
|
726
|
+
const snapshot = pinned.batchSet === undefined ? Stream.empty : setRows(snapshotTableSql, pinned.batchSet)
|
|
727
|
+
const tail = paged(pinned.base, (after) =>
|
|
728
|
+
query(sql`
|
|
729
|
+
SELECT ${top} sequence, entry_ids FROM ${tableSql}
|
|
730
|
+
WHERE sequence > ${after} AND last_sequence <= ${stage.through}
|
|
731
|
+
ORDER BY sequence ${limit}`), (row) => Number(row.sequence))
|
|
732
|
+
return snapshot.pipe(
|
|
733
|
+
Stream.concat(tail),
|
|
734
|
+
Stream.map((row): EventLogServerEncrypted.CompactionBatchHeader => ({
|
|
735
|
+
sequence: Number(row.sequence),
|
|
736
|
+
entryIds: toEntryIds(row.entry_ids as Uint8Array)
|
|
737
|
+
})),
|
|
738
|
+
Stream.orDie,
|
|
739
|
+
Stream.concat(Stream.fromEffectDrain(dieOnSqlError(recheckPin(stageId, pinned.id))))
|
|
740
|
+
)
|
|
741
|
+
})
|
|
742
|
+
|
|
743
|
+
const fetch = Effect.fnUntraced(function*(
|
|
744
|
+
{ scope, snapshotTableSql, tableSql }: StoreResources,
|
|
745
|
+
now: number,
|
|
746
|
+
stageId: string,
|
|
747
|
+
sequences: ReadonlyArray<number>
|
|
748
|
+
) {
|
|
749
|
+
const found = yield* findInputStage(scope, stageId, now)
|
|
750
|
+
if (found === undefined) {
|
|
751
|
+
return yield* notFound(stageId)
|
|
752
|
+
}
|
|
753
|
+
const { pinned, stage } = found
|
|
754
|
+
const bySequence = new Map<number, EventLogEncryption.EncryptedRemoteBatch>()
|
|
755
|
+
if (pinned.batchSet !== undefined) {
|
|
756
|
+
for (const chunk of Arr.chunksOf(sequences.filter((sequence) => sequence <= pinned.base), 200)) {
|
|
757
|
+
const rows = yield* sql`
|
|
758
|
+
SELECT sequence, iv, entry_ids, encrypted_batch FROM ${snapshotTableSql}
|
|
759
|
+
WHERE batch_set = ${pinned.batchSet} AND ${sql.in("sequence", chunk)}
|
|
760
|
+
`.pipe(Effect.flatMap(decodeBatches), Effect.orDie)
|
|
761
|
+
for (const batch of rows) bySequence.set(batch.sequence, batch)
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
const originals = sequences.filter((sequence) => sequence > pinned.base && sequence <= stage.through)
|
|
765
|
+
for (const chunk of Arr.chunksOf(originals, 200)) {
|
|
766
|
+
const rows = yield* sql`
|
|
767
|
+
SELECT sequence, iv, entry_ids, encrypted_batch FROM ${tableSql}
|
|
768
|
+
WHERE ${sql.in("sequence", chunk)}
|
|
769
|
+
`.pipe(Effect.flatMap(decodeBatches), Effect.orDie)
|
|
770
|
+
for (const batch of rows) bySequence.set(batch.sequence, batch)
|
|
771
|
+
}
|
|
772
|
+
yield* recheckPin(stageId, pinned.id)
|
|
773
|
+
return Stream.fromArray(sequences.flatMap((sequence) => {
|
|
774
|
+
const batch = bySequence.get(sequence)
|
|
775
|
+
return batch === undefined ? [] : [batch]
|
|
776
|
+
}))
|
|
777
|
+
})
|
|
778
|
+
|
|
779
|
+
const stagePart = (
|
|
780
|
+
store: StoreResources,
|
|
781
|
+
now: number,
|
|
782
|
+
stageId: string,
|
|
783
|
+
segment: number,
|
|
784
|
+
batches: Arr.NonEmptyReadonlyArray<EventLogEncryption.EncryptedBatch>
|
|
785
|
+
): Effect.Effect<
|
|
786
|
+
"Stored" | "AlreadyStored",
|
|
787
|
+
EventLogServerEncrypted.CompactionInvalidRequest | EventLogServerEncrypted.CompactionNotFound | SqlError.SqlError
|
|
788
|
+
> => {
|
|
789
|
+
const invalid = !Number.isSafeInteger(segment) || segment < 0 || segment >= maxSegments
|
|
790
|
+
? "Invalid segment index"
|
|
791
|
+
: batches.length > positionsPerSegment
|
|
792
|
+
? "Too many batches in a segment"
|
|
793
|
+
: undefined
|
|
794
|
+
// A Part that fails validation closes the stage.
|
|
795
|
+
if (invalid !== undefined) {
|
|
796
|
+
return Effect.andThen(
|
|
797
|
+
abort(store, stageId),
|
|
798
|
+
Effect.fail(new EventLogServerEncrypted.CompactionInvalidRequest({ stageId, message: invalid }))
|
|
799
|
+
)
|
|
800
|
+
}
|
|
801
|
+
const { scope, snapshotTableSql } = store
|
|
802
|
+
return sql.withTransaction(Effect.gen(function*() {
|
|
803
|
+
yield* lockStore(scope)
|
|
804
|
+
const stage = yield* selectStage(scope, stageId, true)
|
|
805
|
+
if (stage === undefined || stage.state !== "open" || stage.expiresAt <= now) {
|
|
806
|
+
return yield* notFound(stageId)
|
|
807
|
+
}
|
|
808
|
+
// The snapshot table's key makes a retried segment insert nothing.
|
|
809
|
+
// `sequence` and `last_sequence` stay null until commit.
|
|
810
|
+
const rows = batches.map((batch, i) => ({
|
|
811
|
+
batch_set: stageId,
|
|
812
|
+
batch_position: segment * positionsPerSegment + i,
|
|
813
|
+
entry_count: batch.entryIds.length,
|
|
814
|
+
iv: batch.iv,
|
|
815
|
+
entry_ids: concatEntryIds(batch.entryIds),
|
|
816
|
+
encrypted_batch: batch.encryptedBatch
|
|
817
|
+
}))
|
|
818
|
+
let inserted = 0
|
|
819
|
+
for (const chunk of Arr.chunksOf(rows, 200)) {
|
|
820
|
+
inserted += yield* insertNew(snapshotTableSql, chunk, ["batch_set", "batch_position"])
|
|
821
|
+
}
|
|
822
|
+
if (inserted === 0) return "AlreadyStored" as const
|
|
823
|
+
yield* sql`UPDATE ${stagesSql} SET expires_at = ${now + stageTtlMillis} WHERE id = ${stageId}`
|
|
824
|
+
return "Stored" as const
|
|
825
|
+
}))
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
const stagedHeaders = Effect.fnUntraced(function*(
|
|
829
|
+
{ scope, snapshotTableSql }: StoreResources,
|
|
830
|
+
now: number,
|
|
831
|
+
stageId: string
|
|
832
|
+
) {
|
|
833
|
+
const stage = yield* selectStage(scope, stageId, false)
|
|
834
|
+
if (stage === undefined || stage.state !== "open" || stage.expiresAt <= now) {
|
|
835
|
+
return yield* notFound(stageId)
|
|
836
|
+
}
|
|
837
|
+
return setRows(snapshotTableSql, stageId).pipe(
|
|
838
|
+
Stream.map((row) => ({
|
|
839
|
+
position: Number(row.batch_position),
|
|
840
|
+
entryIds: toEntryIds(row.entry_ids as Uint8Array)
|
|
841
|
+
})),
|
|
842
|
+
Stream.orDie
|
|
843
|
+
)
|
|
844
|
+
})
|
|
845
|
+
|
|
846
|
+
const commit = Effect.fnUntraced(function*(
|
|
847
|
+
{ scope, snapshotTableSql }: StoreResources,
|
|
848
|
+
now: number,
|
|
849
|
+
stageId: string,
|
|
850
|
+
virtualSequences: ReadonlyArray<number>
|
|
851
|
+
) {
|
|
852
|
+
const store = yield* lockStore(scope)
|
|
853
|
+
const stage = yield* selectStage(scope, stageId, true)
|
|
854
|
+
if (stage === undefined || stage.state !== "open" || stage.expiresAt <= now) {
|
|
855
|
+
return yield* notFound(stageId)
|
|
856
|
+
}
|
|
857
|
+
const staged = yield* query(sql`
|
|
858
|
+
SELECT batch_position, entry_count FROM ${snapshotTableSql}
|
|
859
|
+
WHERE batch_set = ${stageId} ORDER BY batch_position`)
|
|
860
|
+
let keptCount = 0
|
|
861
|
+
for (const row of staged) keptCount += Number(row.entry_count)
|
|
862
|
+
const audit = { through: stage.through, keptCount }
|
|
863
|
+
if (stage.pinned !== store.activeActivation) {
|
|
864
|
+
yield* closeStage(scope, stageId)
|
|
865
|
+
return { result: "Superseded", activationId: undefined, ...audit } as const
|
|
866
|
+
}
|
|
867
|
+
if (staged.length !== virtualSequences.length) {
|
|
868
|
+
return yield* Effect.die(new Cause.IllegalArgumentError("Expected one virtual sequence per staged batch"))
|
|
869
|
+
}
|
|
870
|
+
for (let i = 0; i < staged.length; i++) {
|
|
871
|
+
const sequence = virtualSequences[i]
|
|
872
|
+
const lastSequence = sequence + Number(staged[i].entry_count) - 1
|
|
873
|
+
yield* sql`
|
|
874
|
+
UPDATE ${snapshotTableSql} SET sequence = ${sequence}, last_sequence = ${lastSequence}
|
|
875
|
+
WHERE batch_set = ${stageId} AND batch_position = ${Number(staged[i].batch_position)}`
|
|
876
|
+
}
|
|
877
|
+
const activationId = SecureRandom.uuidV4Unsafe()
|
|
878
|
+
yield* insertActivation(scope, {
|
|
879
|
+
id: activationId,
|
|
880
|
+
batch_set: stageId,
|
|
881
|
+
base_sequence: stage.through,
|
|
882
|
+
kept_count: keptCount,
|
|
883
|
+
previous: store.activeActivation
|
|
884
|
+
})
|
|
885
|
+
yield* supersede(store.activeActivation, now)
|
|
886
|
+
yield* sql`
|
|
887
|
+
UPDATE ${storesSql}
|
|
888
|
+
SET active_activation = ${activationId}, active_batch_set = ${stageId},
|
|
889
|
+
base_sequence = ${stage.through}, active_kept_count = ${keptCount},
|
|
890
|
+
no_op_at = NULL, no_op_count = 0
|
|
891
|
+
WHERE scope = ${scope}`
|
|
892
|
+
yield* scheduleSweep(scope, now + graceMillis)
|
|
893
|
+
// The staged rows are now the active batch set.
|
|
894
|
+
yield* sql`DELETE FROM ${stagesSql} WHERE id = ${stageId}`
|
|
895
|
+
yield* releaseClaim(scope, stageId)
|
|
896
|
+
return { result: "Committed", activationId, ...audit } as const
|
|
897
|
+
}, sql.withTransaction)
|
|
898
|
+
|
|
899
|
+
const recordNoOp = Effect.fnUntraced(function*({ scope }: StoreResources, now: number, stageId: string) {
|
|
900
|
+
const store = yield* lockStore(scope)
|
|
901
|
+
const stage = yield* selectStage(scope, stageId, true)
|
|
902
|
+
if (stage === undefined || stage.state !== "open" || stage.expiresAt <= now) {
|
|
903
|
+
return yield* notFound(stageId)
|
|
904
|
+
}
|
|
905
|
+
// A rollback since begin cleared the no-op state, so a stale run leaves it.
|
|
906
|
+
if (stage.pinned === store.activeActivation) {
|
|
907
|
+
yield* sql`
|
|
908
|
+
UPDATE ${storesSql} SET no_op_at = ${stage.through}, no_op_count = ${store.noOpCount + 1}
|
|
909
|
+
WHERE scope = ${scope}`
|
|
910
|
+
}
|
|
911
|
+
yield* closeStage(scope, stageId)
|
|
912
|
+
return stage.through
|
|
913
|
+
}, sql.withTransaction)
|
|
914
|
+
|
|
915
|
+
const rollbackTransaction = Effect.fnUntraced(function*(
|
|
916
|
+
{ pubsub, scope, snapshotTableSql, tableSql }: StoreResources,
|
|
917
|
+
now: number,
|
|
918
|
+
expectedActivationId: string
|
|
919
|
+
) {
|
|
920
|
+
const refuse = (reason: EventLogServerEncrypted.CompactionRollbackError["reason"]) =>
|
|
921
|
+
Effect.fail(new EventLogServerEncrypted.CompactionRollbackError({ reason }))
|
|
922
|
+
const store = yield* lockStore(scope)
|
|
923
|
+
if (store.activeActivation !== expectedActivationId) return yield* refuse("NotActive")
|
|
924
|
+
const withdrawn = (yield* selectActivation(store.activeActivation))!
|
|
925
|
+
const target = withdrawn.previous === undefined ? undefined : yield* selectActivation(withdrawn.previous)
|
|
926
|
+
if (target === undefined) return yield* refuse("NoPrevious")
|
|
927
|
+
if (target.pruneStartedAt !== undefined) return yield* refuse("PruneStarted")
|
|
928
|
+
const activationId = SecureRandom.uuidV4Unsafe()
|
|
929
|
+
yield* insertActivation(scope, {
|
|
930
|
+
id: activationId,
|
|
931
|
+
batch_set: target.batchSet ?? null,
|
|
932
|
+
base_sequence: target.base,
|
|
933
|
+
kept_count: target.keptCount ?? null,
|
|
934
|
+
previous: target.previous ?? null
|
|
935
|
+
})
|
|
936
|
+
yield* supersede(withdrawn.id, now)
|
|
937
|
+
yield* sql`
|
|
938
|
+
UPDATE ${storesSql}
|
|
939
|
+
SET active_activation = ${activationId}, active_batch_set = ${target.batchSet ?? null},
|
|
940
|
+
base_sequence = ${target.base}, active_kept_count = ${target.keptCount ?? 0},
|
|
941
|
+
no_op_at = NULL, no_op_count = 0
|
|
942
|
+
WHERE scope = ${scope}`
|
|
943
|
+
yield* scheduleSweep(scope, now + graceMillis)
|
|
944
|
+
|
|
945
|
+
// Re-appends the input of the stage that built the withdrawn generation,
|
|
946
|
+
// the target's batch set and then the originals between the two bases,
|
|
947
|
+
// at sequences taken as `write` takes them.
|
|
948
|
+
let nextSequence = Math.max(store.nextSequence, (yield* maxLastSequence(tableSql)) + 1)
|
|
949
|
+
const reappended: Array<EventLogEncryption.EncryptedRemoteBatch> = []
|
|
950
|
+
const reappend = (rows: ReadonlyArray<Record<string, unknown>>) => {
|
|
951
|
+
const inserted: Array<BatchRow> = []
|
|
952
|
+
for (const row of rows) {
|
|
953
|
+
const entry_ids = row.entry_ids as Uint8Array
|
|
954
|
+
const batch: EventLogEncryption.EncryptedRemoteBatch = {
|
|
955
|
+
sequence: nextSequence,
|
|
956
|
+
entryIds: toEntryIds(entry_ids),
|
|
957
|
+
iv: row.iv as Uint8Array<ArrayBuffer>,
|
|
958
|
+
encryptedBatch: row.encrypted_batch as Uint8Array<ArrayBuffer>
|
|
959
|
+
}
|
|
960
|
+
nextSequence += batch.entryIds.length
|
|
961
|
+
reappended.push(batch)
|
|
962
|
+
inserted.push({
|
|
963
|
+
sequence: batch.sequence,
|
|
964
|
+
last_sequence: nextSequence - 1,
|
|
965
|
+
iv: batch.iv,
|
|
966
|
+
entry_ids,
|
|
967
|
+
encrypted_batch: batch.encryptedBatch
|
|
968
|
+
})
|
|
969
|
+
}
|
|
970
|
+
return Effect.forEach(
|
|
971
|
+
Arr.chunksOf(inserted, 200),
|
|
972
|
+
(chunk) => sql`INSERT INTO ${tableSql} ${sql.insert(chunk)}`,
|
|
973
|
+
{ discard: true }
|
|
974
|
+
)
|
|
975
|
+
}
|
|
976
|
+
if (target.batchSet !== undefined) {
|
|
977
|
+
yield* Stream.runForEachArray(
|
|
978
|
+
paged(
|
|
979
|
+
-1,
|
|
980
|
+
(after) =>
|
|
981
|
+
query(sql`
|
|
982
|
+
SELECT ${top} batch_position, iv, entry_ids, encrypted_batch FROM ${snapshotTableSql}
|
|
983
|
+
WHERE batch_set = ${target.batchSet} AND batch_position > ${after}
|
|
984
|
+
ORDER BY batch_position ${limit}`),
|
|
985
|
+
(row) => Number(row.batch_position)
|
|
986
|
+
),
|
|
987
|
+
reappend
|
|
988
|
+
)
|
|
989
|
+
}
|
|
990
|
+
// The copies land above `withdrawn.base`, so the pages never reach them.
|
|
991
|
+
yield* Stream.runForEachArray(
|
|
992
|
+
paged(
|
|
993
|
+
target.base,
|
|
994
|
+
(after) =>
|
|
995
|
+
query(sql`
|
|
996
|
+
SELECT ${top} sequence, iv, entry_ids, encrypted_batch FROM ${tableSql}
|
|
997
|
+
WHERE sequence > ${after} AND last_sequence <= ${withdrawn.base}
|
|
998
|
+
ORDER BY sequence ${limit}`),
|
|
999
|
+
(row) => Number(row.sequence)
|
|
1000
|
+
),
|
|
1001
|
+
reappend
|
|
1002
|
+
)
|
|
1003
|
+
if (reappended.length > 0) {
|
|
1004
|
+
yield* sql`UPDATE ${storesSql} SET next_sequence = ${nextSequence} WHERE scope = ${scope}`
|
|
1005
|
+
yield* upsertBucket(scope, Math.floor(now / hourMillis), nextSequence - 1)
|
|
1006
|
+
yield* sql.afterCommit(Effect.asVoid(PubSub.publishAll(pubsub, reappended)))
|
|
1007
|
+
}
|
|
1008
|
+
return { activationId, target, withdrawn }
|
|
1009
|
+
}, sql.withTransaction)
|
|
1010
|
+
|
|
1011
|
+
// The re-appended batches are published after the outermost commit. When
|
|
1012
|
+
// the storage owns the transaction, that happens before the semaphore is
|
|
1013
|
+
// released, so live readers get them in sequence order.
|
|
1014
|
+
const rollback = (store: StoreResources, now: number, expectedActivationId: string) =>
|
|
1015
|
+
store.semaphore.withPermits(1)(rollbackTransaction(store, now, expectedActivationId))
|
|
1016
|
+
|
|
1017
|
+
const history = Effect.fnUntraced(function*({ scope }: StoreResources) {
|
|
1018
|
+
const store = (yield* selectStore(scope))!
|
|
1019
|
+
const activations = yield* selectActivations(scope)
|
|
1020
|
+
return activations.map((activation): EventLogServerEncrypted.CompactionActivation => {
|
|
1021
|
+
const active = activation.id === store.activeActivation
|
|
1022
|
+
return {
|
|
1023
|
+
activationId: activation.id,
|
|
1024
|
+
base: activation.base,
|
|
1025
|
+
keptCount: activation.keptCount,
|
|
1026
|
+
active,
|
|
1027
|
+
supersededAt: active ? undefined : activation.supersededAt,
|
|
1028
|
+
restorable: activation.pruneStartedAt === undefined
|
|
1029
|
+
}
|
|
1030
|
+
})
|
|
1031
|
+
})
|
|
1032
|
+
|
|
1033
|
+
const abort = Effect.fnUntraced(function*({ scope }: StoreResources, stageId: string) {
|
|
1034
|
+
yield* lockStore(scope)
|
|
1035
|
+
const stage = yield* selectStage(scope, stageId, true)
|
|
1036
|
+
if (stage?.state === "open") yield* closeStage(scope, stageId)
|
|
1037
|
+
}, sql.withTransaction)
|
|
1038
|
+
|
|
1039
|
+
const compaction: EventLogServerEncrypted.StorageCompaction = {
|
|
1040
|
+
config,
|
|
1041
|
+
status: (publicKey, storeId) => withStore(publicKey, storeId, status),
|
|
1042
|
+
begin: (publicKey, storeId, { through }) =>
|
|
1043
|
+
withStore(publicKey, storeId, (store, now) => begin(store, now, through)),
|
|
1044
|
+
inputHeaders: (publicKey, storeId, stageId) =>
|
|
1045
|
+
Stream.unwrap(withStore(publicKey, storeId, (store, now) => inputHeaders(store, now, stageId))),
|
|
1046
|
+
fetch: (publicKey, storeId, stageId, sequences) =>
|
|
1047
|
+
Stream.unwrap(withStore(publicKey, storeId, (store, now) => fetch(store, now, stageId, sequences))),
|
|
1048
|
+
stagePart: (publicKey, storeId, stageId, segment, batches) =>
|
|
1049
|
+
withStore(publicKey, storeId, (store, now) => stagePart(store, now, stageId, segment, batches)),
|
|
1050
|
+
stagedHeaders: (publicKey, storeId, stageId) =>
|
|
1051
|
+
Stream.unwrap(withStore(publicKey, storeId, (store, now) => stagedHeaders(store, now, stageId))),
|
|
1052
|
+
commit: (publicKey, storeId, stageId, virtualSequences) =>
|
|
1053
|
+
withStore(publicKey, storeId, (store, now) => commit(store, now, stageId, virtualSequences)).pipe(
|
|
1054
|
+
Effect.tap((outcome) => logCommit({ publicKey, storeId, ...outcome })),
|
|
1055
|
+
Effect.map((outcome) => outcome.result)
|
|
1056
|
+
),
|
|
1057
|
+
recordNoOp: (publicKey, storeId, stageId) =>
|
|
1058
|
+
withStore(publicKey, storeId, (store, now) => recordNoOp(store, now, stageId)).pipe(
|
|
1059
|
+
Effect.flatMap((through) => logNoOp({ publicKey, storeId, stageId, through }))
|
|
1060
|
+
),
|
|
1061
|
+
abort: (publicKey, storeId, stageId) => withStore(publicKey, storeId, (store) => abort(store, stageId)),
|
|
1062
|
+
rollback: (publicKey, storeId, expectedActivationId) =>
|
|
1063
|
+
withStore(publicKey, storeId, (store, now) => rollback(store, now, expectedActivationId)).pipe(
|
|
1064
|
+
Effect.tap(({ activationId, target, withdrawn }) =>
|
|
1065
|
+
logRollback({ publicKey, storeId, activation: { ...target, id: activationId }, withdrawn })
|
|
1066
|
+
),
|
|
1067
|
+
Effect.map((outcome) => outcome.activationId)
|
|
1068
|
+
),
|
|
1069
|
+
history: (publicKey, storeId) => withStore(publicKey, storeId, (store) => history(store))
|
|
1070
|
+
}
|
|
1071
|
+
|
|
1072
|
+
const insertActivation = Effect.fnUntraced(function*(scope: string, activation: {
|
|
1073
|
+
readonly id: string
|
|
1074
|
+
readonly batch_set: string | null
|
|
1075
|
+
readonly base_sequence: number
|
|
1076
|
+
readonly kept_count: number | null
|
|
1077
|
+
readonly previous: string | null
|
|
1078
|
+
}) {
|
|
1079
|
+
const [last] = yield* query(sql`SELECT MAX(ordinal) AS ordinal FROM ${activationsSql} WHERE scope = ${scope}`)
|
|
1080
|
+
yield* sql`INSERT INTO ${activationsSql} ${
|
|
1081
|
+
sql.insert({
|
|
1082
|
+
...activation,
|
|
1083
|
+
scope,
|
|
1084
|
+
ordinal: Number(last?.ordinal ?? 0) + 1,
|
|
1085
|
+
superseded_at: null,
|
|
1086
|
+
prunable_at: null,
|
|
1087
|
+
prune_started_at: null
|
|
1088
|
+
})
|
|
1089
|
+
}`
|
|
1090
|
+
})
|
|
1091
|
+
// Supersedes or withdraws an activation, prunable `grace` later.
|
|
1092
|
+
const supersede = (activationId: string, now: number) =>
|
|
1093
|
+
sql`
|
|
1094
|
+
UPDATE ${activationsSql} SET superseded_at = ${now}, prunable_at = ${now + graceMillis}
|
|
1095
|
+
WHERE id = ${activationId}`
|
|
1096
|
+
|
|
1097
|
+
// Deletes up to `pruneChunkRows` rows matching `where` in key order and
|
|
1098
|
+
// returns whether it found any. No new rows match `where`, so a later call
|
|
1099
|
+
// resumes an interrupted one.
|
|
1100
|
+
const deleteChunk = Effect.fnUntraced(function*(
|
|
1101
|
+
tableSql: Statement.Identifier,
|
|
1102
|
+
key: string,
|
|
1103
|
+
where: Statement.Fragment
|
|
1104
|
+
) {
|
|
1105
|
+
const keys = yield* query(sql`
|
|
1106
|
+
SELECT ${chunkTop} ${sql(key)} AS chunk_key FROM ${tableSql}
|
|
1107
|
+
WHERE ${where} ORDER BY ${sql(key)} ${chunkLimit}`)
|
|
1108
|
+
if (keys.length === 0) return false
|
|
1109
|
+
yield* sql`DELETE FROM ${tableSql} WHERE ${where} AND ${sql(key)} <= ${keys[keys.length - 1].chunk_key}`
|
|
1110
|
+
return true
|
|
1111
|
+
})
|
|
1112
|
+
|
|
1113
|
+
// Runs one chunk of a prune under the store row lock, if this sweeper
|
|
1114
|
+
// still holds the lease and the active activation hasn't changed.
|
|
1115
|
+
const pruneChunk = <A>(
|
|
1116
|
+
scope: string,
|
|
1117
|
+
now: number,
|
|
1118
|
+
lease: { readonly owner: string; readonly activeActivation: string },
|
|
1119
|
+
f: Effect.Effect<A, SqlError.SqlError>
|
|
1120
|
+
) =>
|
|
1121
|
+
sql.withTransaction(Effect.gen(function*() {
|
|
1122
|
+
const store = yield* lockStore(scope)
|
|
1123
|
+
if (store.sweepLeaseOwner !== lease.owner || store.activeActivation !== lease.activeActivation) {
|
|
1124
|
+
return Option.none<A>()
|
|
1125
|
+
}
|
|
1126
|
+
const value = yield* f
|
|
1127
|
+
yield* sql`UPDATE ${storesSql} SET sweep_lease_until = ${now + sweepIntervalMillis} WHERE scope = ${scope}`
|
|
1128
|
+
return Option.some(value)
|
|
1129
|
+
}))
|
|
1130
|
+
|
|
1131
|
+
// Prunes what no activation still needs: the batch sets of generations
|
|
1132
|
+
// past their grace, and the originals at or below the lowest base still
|
|
1133
|
+
// kept. Deletes are idempotent, so a later sweep finishes one that died.
|
|
1134
|
+
const pruneStore = Effect.fnUntraced(function*(scope: string, now: number) {
|
|
1135
|
+
const { snapshotTableSql, tableSql } = scopeTables(scope)
|
|
1136
|
+
const owner = SecureRandom.uuidV4Unsafe()
|
|
1137
|
+
// Claim the store and mark what this prune removes before deleting anything.
|
|
1138
|
+
const plan = yield* sql.withTransaction(Effect.gen(function*() {
|
|
1139
|
+
const store = yield* lockStore(scope)
|
|
1140
|
+
if (store.nextSweepAt === undefined || store.nextSweepAt > now) return undefined
|
|
1141
|
+
if (store.sweepLeaseUntil !== undefined && store.sweepLeaseUntil > now) return undefined
|
|
1142
|
+
const activations = yield* selectActivations(scope)
|
|
1143
|
+
const isKept = (activation: ActivationRow) =>
|
|
1144
|
+
activation.id === store.activeActivation ||
|
|
1145
|
+
(activation.pruneStartedAt === undefined && (activation.prunableAt ?? Infinity) > now)
|
|
1146
|
+
const kept = activations.filter(isKept)
|
|
1147
|
+
const pruned = activations.filter((activation) => !isKept(activation))
|
|
1148
|
+
const starting = pruned.filter((activation) => activation.pruneStartedAt === undefined)
|
|
1149
|
+
for (const chunk of Arr.chunksOf(starting.map((activation) => activation.id), 200)) {
|
|
1150
|
+
yield* sql`UPDATE ${activationsSql} SET prune_started_at = ${now} WHERE ${sql.in("id", chunk)}`
|
|
1151
|
+
}
|
|
1152
|
+
const keptSets = new Set(kept.map((activation) => activation.batchSet))
|
|
1153
|
+
yield* sql`
|
|
1154
|
+
UPDATE ${storesSql} SET sweep_lease_until = ${now + sweepIntervalMillis}, sweep_lease_owner = ${owner}
|
|
1155
|
+
WHERE scope = ${scope}`
|
|
1156
|
+
return {
|
|
1157
|
+
lease: { owner, activeActivation: store.activeActivation },
|
|
1158
|
+
sets: Array.from(
|
|
1159
|
+
new Set(pruned.flatMap((activation) =>
|
|
1160
|
+
activation.batchSet === undefined || keptSets.has(activation.batchSet) ? [] : [activation.batchSet]
|
|
1161
|
+
))
|
|
1162
|
+
),
|
|
1163
|
+
keptBase: kept.reduce((base, activation) =>
|
|
1164
|
+
Math.min(base, activation.base), Infinity),
|
|
1165
|
+
prunedThrough: store.prunedThrough
|
|
1166
|
+
}
|
|
1167
|
+
}))
|
|
1168
|
+
if (plan === undefined) return
|
|
1169
|
+
for (const set of plan.sets) {
|
|
1170
|
+
while (true) {
|
|
1171
|
+
const deleted = yield* pruneChunk(
|
|
1172
|
+
scope,
|
|
1173
|
+
now,
|
|
1174
|
+
plan.lease,
|
|
1175
|
+
deleteChunk(snapshotTableSql, "batch_position", sql`batch_set = ${set}`)
|
|
1176
|
+
)
|
|
1177
|
+
if (Option.isNone(deleted)) return
|
|
1178
|
+
if (!deleted.value) break
|
|
1179
|
+
}
|
|
1180
|
+
}
|
|
1181
|
+
for (let start = plan.prunedThrough; start < plan.keptBase; start += pruneChunkSequences) {
|
|
1182
|
+
const end = Math.min(start + pruneChunkSequences, plan.keptBase)
|
|
1183
|
+
const held = yield* pruneChunk(
|
|
1184
|
+
scope,
|
|
1185
|
+
now,
|
|
1186
|
+
plan.lease,
|
|
1187
|
+
sql`DELETE FROM ${tableSql} WHERE last_sequence <= ${end}`.pipe(
|
|
1188
|
+
Effect.andThen(sql`
|
|
1189
|
+
UPDATE ${storesSql}
|
|
1190
|
+
SET pruned_through = CASE WHEN pruned_through < ${end} THEN ${end} ELSE pruned_through END
|
|
1191
|
+
WHERE scope = ${scope}`)
|
|
1192
|
+
)
|
|
1193
|
+
)
|
|
1194
|
+
if (Option.isNone(held)) return
|
|
1195
|
+
}
|
|
1196
|
+
yield* sql.withTransaction(Effect.gen(function*() {
|
|
1197
|
+
const store = yield* lockStore(scope)
|
|
1198
|
+
if (store.sweepLeaseOwner !== owner) return
|
|
1199
|
+
const [next] = yield* query(sql`
|
|
1200
|
+
SELECT MIN(prunable_at) AS next_sweep_at FROM ${activationsSql}
|
|
1201
|
+
WHERE scope = ${scope} AND id <> ${store.activeActivation} AND prune_started_at IS NULL`)
|
|
1202
|
+
yield* sql`
|
|
1203
|
+
UPDATE ${storesSql}
|
|
1204
|
+
SET next_sweep_at = ${next?.next_sweep_at ?? null}, sweep_lease_until = NULL, sweep_lease_owner = NULL
|
|
1205
|
+
WHERE scope = ${scope}`
|
|
1206
|
+
}))
|
|
1207
|
+
})
|
|
1208
|
+
|
|
1209
|
+
// Deletes a closed stage's rows matching `where`, one chunk per
|
|
1210
|
+
// transaction, each under the store row lock to keep the lock order.
|
|
1211
|
+
const deleteStaged = Effect.fnUntraced(function*(
|
|
1212
|
+
scope: string,
|
|
1213
|
+
tableSql: Statement.Identifier,
|
|
1214
|
+
key: string,
|
|
1215
|
+
where: Statement.Fragment
|
|
1216
|
+
) {
|
|
1217
|
+
while (yield* sql.withTransaction(Effect.andThen(lockStore(scope), deleteChunk(tableSql, key, where)))) {
|
|
1218
|
+
// Each chunk commits on its own.
|
|
1219
|
+
}
|
|
1220
|
+
})
|
|
1221
|
+
|
|
1222
|
+
// Closes stages past their claim, then deletes closed stages. A closed
|
|
1223
|
+
// stage's staged rows are read by nothing, so they go first in short
|
|
1224
|
+
// chunks, and the stage row last.
|
|
1225
|
+
const sweepStages = Effect.fnUntraced(function*(now: number) {
|
|
1226
|
+
const expired = yield* query(sql`
|
|
1227
|
+
SELECT id, scope FROM ${stagesSql} WHERE state = ${"open"} AND expires_at <= ${now}`)
|
|
1228
|
+
for (const row of expired) {
|
|
1229
|
+
const scope = String(row.scope)
|
|
1230
|
+
const stageId = String(row.id)
|
|
1231
|
+
yield* sql.withTransaction(Effect.gen(function*() {
|
|
1232
|
+
yield* lockStore(scope)
|
|
1233
|
+
const stage = yield* selectStage(scope, stageId, true)
|
|
1234
|
+
if (stage?.state === "open" && stage.expiresAt <= now) {
|
|
1235
|
+
yield* closeStage(scope, stageId)
|
|
1236
|
+
}
|
|
1237
|
+
}))
|
|
1238
|
+
}
|
|
1239
|
+
const closed = yield* query(sql`SELECT id, scope FROM ${stagesSql} WHERE state = ${"closed"}`)
|
|
1240
|
+
for (const row of closed) {
|
|
1241
|
+
const scope = String(row.scope)
|
|
1242
|
+
const stageId = String(row.id)
|
|
1243
|
+
yield* deleteStaged(scope, scopeTables(scope).snapshotTableSql, "batch_position", sql`batch_set = ${stageId}`)
|
|
1244
|
+
yield* sql.withTransaction(
|
|
1245
|
+
Effect.andThen(lockStore(scope), sql`DELETE FROM ${stagesSql} WHERE id = ${stageId}`)
|
|
1246
|
+
)
|
|
1247
|
+
}
|
|
1248
|
+
})
|
|
1249
|
+
|
|
1250
|
+
// Only the newest bucket past the lag matters for the lag head.
|
|
1251
|
+
const sweepBuckets = Effect.fnUntraced(function*(now: number) {
|
|
1252
|
+
const rows = yield* query(sql`
|
|
1253
|
+
SELECT scope, MAX(bucket_hour) AS newest FROM ${bucketsSql}
|
|
1254
|
+
WHERE bucket_hour <= ${lagHour(now, lagMillis)}
|
|
1255
|
+
GROUP BY scope HAVING COUNT(*) > 1`)
|
|
1256
|
+
for (const row of rows) {
|
|
1257
|
+
yield* sql`DELETE FROM ${bucketsSql} WHERE scope = ${String(row.scope)} AND bucket_hour < ${Number(row.newest)}`
|
|
1258
|
+
}
|
|
1259
|
+
})
|
|
1260
|
+
|
|
1261
|
+
const logSweepFailure = (cause: Cause.Cause<unknown>) =>
|
|
1262
|
+
Effect.logWarning("Compaction sweep failed", cause).pipe(
|
|
1263
|
+
Effect.annotateLogs({ service: "SqlEventLogServerEncrypted" })
|
|
1264
|
+
)
|
|
1265
|
+
const sweep = Effect.gen(function*() {
|
|
1266
|
+
const now = yield* Clock.currentTimeMillis
|
|
1267
|
+
yield* Effect.catchCause(sweepStages(now), logSweepFailure)
|
|
1268
|
+
const due = yield* query(sql`SELECT scope FROM ${storesSql} WHERE next_sweep_at <= ${now}`)
|
|
1269
|
+
for (const row of due) {
|
|
1270
|
+
yield* Effect.catchCause(pruneStore(String(row.scope), now), logSweepFailure)
|
|
1271
|
+
}
|
|
1272
|
+
yield* Effect.catchCause(sweepBuckets(now), logSweepFailure)
|
|
1273
|
+
}).pipe(Effect.catchCause(logSweepFailure), withTracerDisabled)
|
|
1274
|
+
// A fixed schedule starts an overdue sweep straight away, so slow queries
|
|
1275
|
+
// don't push sweeps back.
|
|
1276
|
+
yield* Effect.forkScoped(
|
|
1277
|
+
Effect.delay(Effect.repeat(sweep, Schedule.fixed(config.sweepInterval)), config.sweepInterval)
|
|
1278
|
+
)
|
|
1279
|
+
|
|
265
1280
|
return EventLogServerEncrypted.Storage.of({
|
|
266
1281
|
getId: Effect.succeed(remoteId),
|
|
267
1282
|
// Auto-increment sequences start at 1.
|
|
@@ -287,15 +1302,17 @@ export const makeStorage = (options?: {
|
|
|
287
1302
|
function*(publicKey, storeId, batches) {
|
|
288
1303
|
if (batches.length === 0) return []
|
|
289
1304
|
const scopeKey = makeEncryptedScopeKey(publicKey, storeId)
|
|
290
|
-
const { idsTableSql, pubsub, semaphore, tableSql } = yield* RcMap.get(resources, scopeKey)
|
|
1305
|
+
const { idsTableSql, pubsub, scope, semaphore, tableSql } = yield* RcMap.get(resources, scopeKey)
|
|
1306
|
+
const now = yield* Clock.currentTimeMillis
|
|
291
1307
|
|
|
292
1308
|
const writeBatches = Effect.gen(function*() {
|
|
293
1309
|
const stored: Array<EventLogEncryption.EncryptedRemoteBatch> = []
|
|
1310
|
+
// The store row lock makes sequence order commit order. The row's
|
|
1311
|
+
// `next_sequence` survives pruning, and `MAX(last_sequence)` covers
|
|
1312
|
+
// instances that predate the row.
|
|
1313
|
+
const store = yield* lockStore(scope)
|
|
294
1314
|
// Read before inserting ids, which record the sequence they are stored at.
|
|
295
|
-
|
|
296
|
-
SELECT MAX(last_sequence) AS last_sequence FROM ${tableSql}
|
|
297
|
-
`
|
|
298
|
-
let nextSequence = Number(last?.last_sequence ?? 0) + 1
|
|
1315
|
+
let nextSequence = Math.max(store.nextSequence, (yield* maxLastSequence(tableSql)) + 1)
|
|
299
1316
|
for (const batch of batches) {
|
|
300
1317
|
// Inserting the ids decides whether the batch is new. SQL Server's
|
|
301
1318
|
// MERGE also requires unique source ids.
|
|
@@ -307,7 +1324,8 @@ export const makeStorage = (options?: {
|
|
|
307
1324
|
}
|
|
308
1325
|
let inserted = 0
|
|
309
1326
|
for (const chunk of Arr.chunksOf(ids.values(), 200)) {
|
|
310
|
-
|
|
1327
|
+
// Stored ids keep their first sequence.
|
|
1328
|
+
inserted += yield* insertNew(idsTableSql, chunk, ["entry_id"])
|
|
311
1329
|
}
|
|
312
1330
|
if (inserted === 0) continue
|
|
313
1331
|
const row: BatchRow = {
|
|
@@ -321,14 +1339,16 @@ export const makeStorage = (options?: {
|
|
|
321
1339
|
stored.push({ sequence: nextSequence, ...batch })
|
|
322
1340
|
nextSequence += batch.entryIds.length
|
|
323
1341
|
}
|
|
1342
|
+
if (stored.length > 0) {
|
|
1343
|
+
yield* sql`UPDATE ${storesSql} SET next_sequence = ${nextSequence} WHERE scope = ${scope}`
|
|
1344
|
+
yield* upsertBucket(scope, Math.floor(now / hourMillis), nextSequence - 1)
|
|
1345
|
+
// Published after the outermost commit, as `rollback` publishes.
|
|
1346
|
+
yield* sql.afterCommit(Effect.asVoid(PubSub.publishAll(pubsub, stored)))
|
|
1347
|
+
}
|
|
324
1348
|
return stored
|
|
325
1349
|
})
|
|
326
1350
|
|
|
327
|
-
return yield* semaphore.withPermits(1)(
|
|
328
|
-
const stored = yield* sql.withTransaction(writeBatches)
|
|
329
|
-
yield* PubSub.publishAll(pubsub, stored)
|
|
330
|
-
return stored
|
|
331
|
-
}))
|
|
1351
|
+
return yield* semaphore.withPermits(1)(sql.withTransaction(writeBatches))
|
|
332
1352
|
},
|
|
333
1353
|
Effect.orDie,
|
|
334
1354
|
Effect.scoped,
|
|
@@ -350,11 +1370,34 @@ export const makeStorage = (options?: {
|
|
|
350
1370
|
Effect.scoped,
|
|
351
1371
|
withTracerDisabled
|
|
352
1372
|
),
|
|
1373
|
+
// Reads the store row, the active set and the tail as separate
|
|
1374
|
+
// statements. A transaction would take the SQLite write lock on every
|
|
1375
|
+
// stream open, and set rows never change before they are pruned.
|
|
353
1376
|
changes: Effect.fnUntraced(
|
|
354
1377
|
function*(publicKey, storeId, startSequence) {
|
|
355
1378
|
const scopeKey = makeEncryptedScopeKey(publicKey, storeId)
|
|
356
|
-
const { pubsub, tableSql } = yield* RcMap.get(resources, scopeKey)
|
|
1379
|
+
const { pubsub, scope, snapshotTableSql, tableSql } = yield* RcMap.get(resources, scopeKey)
|
|
357
1380
|
const subscription = yield* PubSub.subscribe(pubsub)
|
|
1381
|
+
const live = Stream.fromSubscription(subscription)
|
|
1382
|
+
const store = (yield* selectStore(scope))!
|
|
1383
|
+
// Below the base an active snapshot replaces the originals.
|
|
1384
|
+
if (store.activeBatchSet !== undefined && startSequence <= store.base) {
|
|
1385
|
+
const set = yield* sql`
|
|
1386
|
+
SELECT sequence, iv, entry_ids, encrypted_batch FROM ${snapshotTableSql}
|
|
1387
|
+
WHERE batch_set = ${store.activeBatchSet} AND last_sequence >= ${startSequence}
|
|
1388
|
+
ORDER BY batch_position ASC
|
|
1389
|
+
`.pipe(Effect.flatMap(decodeBatches))
|
|
1390
|
+
// The final batch ends at the base, so only an empty set reads nothing.
|
|
1391
|
+
if (set.length === 0) {
|
|
1392
|
+
return Stream.die(new Error(`The active compaction snapshot of store ${storeId} has no batches`))
|
|
1393
|
+
}
|
|
1394
|
+
const tail = yield* sql`
|
|
1395
|
+
SELECT sequence, iv, entry_ids, encrypted_batch FROM ${tableSql}
|
|
1396
|
+
WHERE sequence > ${store.base}
|
|
1397
|
+
ORDER BY sequence ASC
|
|
1398
|
+
`.pipe(Effect.flatMap(decodeBatches))
|
|
1399
|
+
return Stream.fromArray([...set, ...tail]).pipe(Stream.concat(live))
|
|
1400
|
+
}
|
|
358
1401
|
const initial = yield* sql`
|
|
359
1402
|
SELECT sequence, iv, entry_ids, encrypted_batch FROM ${tableSql}
|
|
360
1403
|
WHERE last_sequence >= ${startSequence}
|
|
@@ -362,15 +1405,76 @@ export const makeStorage = (options?: {
|
|
|
362
1405
|
`.pipe(
|
|
363
1406
|
Effect.flatMap(decodeBatches)
|
|
364
1407
|
)
|
|
365
|
-
return Stream.fromArray(initial).pipe(Stream.concat(
|
|
1408
|
+
return Stream.fromArray(initial).pipe(Stream.concat(live))
|
|
366
1409
|
},
|
|
367
1410
|
Effect.orDie,
|
|
368
1411
|
withTracerDisabled,
|
|
369
1412
|
Stream.unwrap
|
|
370
|
-
)
|
|
1413
|
+
),
|
|
1414
|
+
compaction
|
|
371
1415
|
})
|
|
372
1416
|
}).pipe(withTracerDisabled)
|
|
373
1417
|
|
|
1418
|
+
// Rows per statement when streaming compaction input or staged batches.
|
|
1419
|
+
const pageSize = 500
|
|
1420
|
+
// Sequences of originals deleted per prune chunk.
|
|
1421
|
+
const pruneChunkSequences = 10_000
|
|
1422
|
+
// Snapshot or staged rows deleted per prune chunk.
|
|
1423
|
+
const pruneChunkRows = 200
|
|
1424
|
+
|
|
1425
|
+
type StageState = "open" | "closed"
|
|
1426
|
+
|
|
1427
|
+
// SQL failures are defects for the compaction primitives.
|
|
1428
|
+
const dieOnSqlError = <A, E, R>(
|
|
1429
|
+
effect: Effect.Effect<A, E | SqlError.SqlError, R>
|
|
1430
|
+
): Effect.Effect<A, Exclude<E, SqlError.SqlError>, R> =>
|
|
1431
|
+
Effect.catchIf(
|
|
1432
|
+
effect,
|
|
1433
|
+
(error): error is SqlError.SqlError => SqlError.isSqlError(error),
|
|
1434
|
+
(error) => Effect.die(error)
|
|
1435
|
+
) as Effect.Effect<A, Exclude<E, SqlError.SqlError>, R>
|
|
1436
|
+
|
|
1437
|
+
const optionalNumber = (value: unknown) => value === null || value === undefined ? undefined : Number(value)
|
|
1438
|
+
const optionalString = (value: unknown) => value === null || value === undefined ? undefined : String(value)
|
|
1439
|
+
|
|
1440
|
+
const toStoreRow = (row: Record<string, unknown>) => ({
|
|
1441
|
+
nextSequence: Number(row.next_sequence),
|
|
1442
|
+
activeActivation: String(row.active_activation),
|
|
1443
|
+
activeBatchSet: optionalString(row.active_batch_set),
|
|
1444
|
+
base: Number(row.base_sequence),
|
|
1445
|
+
activeKeptCount: Number(row.active_kept_count),
|
|
1446
|
+
noOpAt: optionalNumber(row.no_op_at),
|
|
1447
|
+
noOpCount: Number(row.no_op_count),
|
|
1448
|
+
openStage: optionalString(row.open_stage),
|
|
1449
|
+
nextSweepAt: optionalNumber(row.next_sweep_at),
|
|
1450
|
+
sweepLeaseUntil: optionalNumber(row.sweep_lease_until),
|
|
1451
|
+
sweepLeaseOwner: optionalString(row.sweep_lease_owner),
|
|
1452
|
+
prunedThrough: Number(row.pruned_through)
|
|
1453
|
+
})
|
|
1454
|
+
|
|
1455
|
+
const toStageRow = (row: Record<string, unknown>) => ({
|
|
1456
|
+
id: String(row.id),
|
|
1457
|
+
scope: String(row.scope),
|
|
1458
|
+
state: String(row.state) as StageState,
|
|
1459
|
+
pinned: String(row.pinned),
|
|
1460
|
+
through: Number(row.through_sequence),
|
|
1461
|
+
expiresAt: Number(row.expires_at)
|
|
1462
|
+
})
|
|
1463
|
+
|
|
1464
|
+
type ActivationRow = ReturnType<typeof toActivationRow>
|
|
1465
|
+
|
|
1466
|
+
const toActivationRow = (row: Record<string, unknown>) => ({
|
|
1467
|
+
id: String(row.id),
|
|
1468
|
+
ordinal: Number(row.ordinal),
|
|
1469
|
+
batchSet: optionalString(row.batch_set),
|
|
1470
|
+
base: Number(row.base_sequence),
|
|
1471
|
+
keptCount: optionalNumber(row.kept_count),
|
|
1472
|
+
previous: optionalString(row.previous),
|
|
1473
|
+
supersededAt: optionalNumber(row.superseded_at),
|
|
1474
|
+
prunableAt: optionalNumber(row.prunable_at),
|
|
1475
|
+
pruneStartedAt: optionalNumber(row.prune_started_at)
|
|
1476
|
+
})
|
|
1477
|
+
|
|
374
1478
|
type IdRow = {
|
|
375
1479
|
readonly entry_id: Uint8Array
|
|
376
1480
|
readonly sequence: number
|
|
@@ -395,6 +1499,14 @@ const concatEntryIds = (entryIds: ReadonlyArray<Uint8Array>): Uint8Array => {
|
|
|
395
1499
|
return data
|
|
396
1500
|
}
|
|
397
1501
|
|
|
1502
|
+
const toEntryIds = (data: Uint8Array): NonEmptyArray<EntryId> => {
|
|
1503
|
+
const entryIds: Array<EntryId> = []
|
|
1504
|
+
for (let offset = 0; offset < data.byteLength; offset += 16) {
|
|
1505
|
+
entryIds.push(data.subarray(offset, offset + 16) as EntryId)
|
|
1506
|
+
}
|
|
1507
|
+
return entryIds as NonEmptyArray<EntryId>
|
|
1508
|
+
}
|
|
1509
|
+
|
|
398
1510
|
const EncryptedRemoteBatchSql = Schema.Struct({
|
|
399
1511
|
sequence: Schema.Natural,
|
|
400
1512
|
iv: Schema.Uint8Array,
|
|
@@ -414,18 +1526,12 @@ type SessionAuthBindingSql = Schema.Schema.Type<typeof SessionAuthBindingSql>
|
|
|
414
1526
|
const decodeBatchRows = Schema.decodeUnknownEffect(Schema.Array(EncryptedRemoteBatchSql))
|
|
415
1527
|
const decodeSessionAuthBindingRows = Schema.decodeUnknownEffect(Schema.Array(SessionAuthBindingSql))
|
|
416
1528
|
|
|
417
|
-
const toEncryptedRemoteBatch = (row: EncryptedRemoteBatchSql): EventLogEncryption.EncryptedRemoteBatch => {
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
sequence: row.sequence,
|
|
424
|
-
entryIds: entryIds as NonEmptyArray<EntryId>,
|
|
425
|
-
iv: row.iv as Uint8Array<ArrayBuffer>,
|
|
426
|
-
encryptedBatch: row.encrypted_batch as Uint8Array<ArrayBuffer>
|
|
427
|
-
}
|
|
428
|
-
}
|
|
1529
|
+
const toEncryptedRemoteBatch = (row: EncryptedRemoteBatchSql): EventLogEncryption.EncryptedRemoteBatch => ({
|
|
1530
|
+
sequence: row.sequence,
|
|
1531
|
+
entryIds: toEntryIds(row.entry_ids),
|
|
1532
|
+
iv: row.iv as Uint8Array<ArrayBuffer>,
|
|
1533
|
+
encryptedBatch: row.encrypted_batch as Uint8Array<ArrayBuffer>
|
|
1534
|
+
})
|
|
429
1535
|
|
|
430
1536
|
const decodeBatches = (
|
|
431
1537
|
rows: unknown
|
|
@@ -446,15 +1552,16 @@ const decodeSessionAuthBindings = (
|
|
|
446
1552
|
export const layerStorage = (options?: {
|
|
447
1553
|
readonly entryTablePrefix?: string
|
|
448
1554
|
readonly remoteIdTable?: string
|
|
1555
|
+
readonly compaction?: EventLogServerEncrypted.CompactionOptions | undefined
|
|
449
1556
|
}): Layer.Layer<
|
|
450
1557
|
EventLogServerEncrypted.Storage,
|
|
451
1558
|
SqlError.SqlError,
|
|
452
|
-
SqlClient.SqlClient |
|
|
1559
|
+
SqlClient.SqlClient | Digest.Digest
|
|
453
1560
|
> => Layer.effect(EventLogServerEncrypted.Storage)(makeStorage(options))
|
|
454
1561
|
|
|
455
1562
|
/**
|
|
456
1563
|
* Provides SQL-backed encrypted server `Storage` and supplies the default Web
|
|
457
|
-
* Crypto `
|
|
1564
|
+
* Crypto `Digest` layer.
|
|
458
1565
|
*
|
|
459
1566
|
* @stability unstable
|
|
460
1567
|
* @category layers
|
|
@@ -463,9 +1570,10 @@ export const layerStorage = (options?: {
|
|
|
463
1570
|
export const layerStorageSubtle = (options?: {
|
|
464
1571
|
readonly entryTablePrefix?: string
|
|
465
1572
|
readonly remoteIdTable?: string
|
|
1573
|
+
readonly compaction?: EventLogServerEncrypted.CompactionOptions | undefined
|
|
466
1574
|
}): Layer.Layer<EventLogServerEncrypted.Storage, SqlError.SqlError, SqlClient.SqlClient> =>
|
|
467
1575
|
layerStorage(options).pipe(
|
|
468
|
-
Layer.provide(
|
|
1576
|
+
Layer.provide(Digest.layerWebCrypto)
|
|
469
1577
|
)
|
|
470
1578
|
|
|
471
1579
|
const makeEncryptedScopeKey = (
|