effect 4.0.2 → 4.0.4
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/AGENTS.md +24 -0
- package/CLAUDE.md +24 -0
- package/ai-docs/package.json +2 -2
- package/ai-docs/src/52_graphql/10_basics.ts +111 -0
- package/ai-docs/src/52_graphql/20_testing.ts +45 -0
- package/ai-docs/src/52_graphql/30_batching.ts +116 -0
- package/ai-docs/src/52_graphql/40_aliased-batching.ts +122 -0
- package/ai-docs/src/52_graphql/50_errors-and-retries.ts +74 -0
- package/ai-docs/src/52_graphql/index.md +6 -0
- package/dist/Context.d.ts.map +1 -1
- package/dist/Context.js +12 -3
- package/dist/Context.js.map +1 -1
- package/dist/Crypto.d.ts +33 -5
- package/dist/Crypto.d.ts.map +1 -1
- package/dist/Crypto.js +8 -2
- package/dist/Crypto.js.map +1 -1
- package/dist/DateTime.d.ts +9 -6
- package/dist/DateTime.d.ts.map +1 -1
- package/dist/DateTime.js +9 -6
- package/dist/DateTime.js.map +1 -1
- package/dist/Duration.d.ts +6 -6
- package/dist/Duration.d.ts.map +1 -1
- package/dist/Duration.js +27 -8
- package/dist/Duration.js.map +1 -1
- package/dist/FileSystem.d.ts +44 -2
- package/dist/FileSystem.d.ts.map +1 -1
- package/dist/FileSystem.js.map +1 -1
- package/dist/RequestResolver.d.ts.map +1 -1
- package/dist/RequestResolver.js +4 -10
- package/dist/RequestResolver.js.map +1 -1
- package/dist/Scheduler.d.ts +9 -0
- package/dist/Scheduler.d.ts.map +1 -1
- package/dist/Scheduler.js +9 -0
- package/dist/Scheduler.js.map +1 -1
- package/dist/Schema.d.ts +481 -1
- package/dist/Schema.d.ts.map +1 -1
- package/dist/Schema.js +362 -4
- package/dist/Schema.js.map +1 -1
- package/dist/SchemaGetter.d.ts +4 -0
- package/dist/SchemaGetter.d.ts.map +1 -1
- package/dist/SchemaGetter.js +52 -33
- package/dist/SchemaGetter.js.map +1 -1
- package/dist/Sink.d.ts.map +1 -1
- package/dist/Sink.js +38 -17
- package/dist/Sink.js.map +1 -1
- package/dist/Stdio.d.ts +7 -1
- package/dist/Stdio.d.ts.map +1 -1
- package/dist/Stdio.js +1 -0
- package/dist/Stdio.js.map +1 -1
- package/dist/Stream.d.ts +22 -11
- package/dist/Stream.d.ts.map +1 -1
- package/dist/Stream.js +21 -6
- package/dist/Stream.js.map +1 -1
- package/dist/Utils.js +8 -3
- package/dist/Utils.js.map +1 -1
- package/dist/ai/AnthropicStructuredOutput.js +0 -2
- package/dist/ai/AnthropicStructuredOutput.js.map +1 -1
- package/dist/ai/Chat.d.ts +2 -2
- package/dist/ai/Chat.d.ts.map +1 -1
- package/dist/ai/LanguageModel.d.ts +38 -7
- package/dist/ai/LanguageModel.d.ts.map +1 -1
- package/dist/ai/LanguageModel.js +25 -7
- package/dist/ai/LanguageModel.js.map +1 -1
- package/dist/ai/McpServer.d.ts +61 -0
- package/dist/ai/McpServer.d.ts.map +1 -1
- package/dist/ai/McpServer.js +196 -44
- package/dist/ai/McpServer.js.map +1 -1
- package/dist/ai/Prompt.d.ts.map +1 -1
- package/dist/ai/Prompt.js +28 -5
- package/dist/ai/Prompt.js.map +1 -1
- package/dist/ai/Response.d.ts +4 -3
- package/dist/ai/Response.d.ts.map +1 -1
- package/dist/ai/Response.js.map +1 -1
- package/dist/ai/internal/mcpProtocol/v2026_07_28.js +5 -0
- package/dist/ai/internal/mcpProtocol/v2026_07_28.js.map +1 -1
- package/dist/ai/internal/mcpProtocol.js.map +1 -1
- package/dist/ai/internal/mcpRuntime.js +1 -0
- package/dist/ai/internal/mcpRuntime.js.map +1 -1
- package/dist/cli/CliOutput.d.ts +27 -0
- package/dist/cli/CliOutput.d.ts.map +1 -1
- package/dist/cli/CliOutput.js +33 -10
- package/dist/cli/CliOutput.js.map +1 -1
- package/dist/cli/Command.d.ts.map +1 -1
- package/dist/cli/Command.js +6 -3
- package/dist/cli/Command.js.map +1 -1
- package/dist/cli/Completions.d.ts +3 -3
- package/dist/cli/Completions.d.ts.map +1 -1
- package/dist/cli/Completions.js +8 -5
- package/dist/cli/Completions.js.map +1 -1
- package/dist/cli/GlobalFlag.d.ts +5 -4
- package/dist/cli/GlobalFlag.d.ts.map +1 -1
- package/dist/cli/GlobalFlag.js +3 -3
- package/dist/cli/GlobalFlag.js.map +1 -1
- package/dist/cli/Prompt.d.ts +17 -1
- package/dist/cli/Prompt.d.ts.map +1 -1
- package/dist/cli/Prompt.js +119 -114
- package/dist/cli/Prompt.js.map +1 -1
- package/dist/cli/internal/ansi.d.ts +0 -1
- package/dist/cli/internal/ansi.d.ts.map +1 -1
- package/dist/cli/internal/ansi.js +20 -3
- package/dist/cli/internal/ansi.js.map +1 -1
- package/dist/cli/internal/completions/powershell.d.ts +2 -0
- package/dist/cli/internal/completions/powershell.d.ts.map +1 -0
- package/dist/cli/internal/completions/powershell.js +273 -0
- package/dist/cli/internal/completions/powershell.js.map +1 -0
- package/dist/cli/internal/completions/zsh.js +3 -2
- package/dist/cli/internal/completions/zsh.js.map +1 -1
- package/dist/cli/internal/wizard.d.ts +3 -3
- package/dist/cli/internal/wizard.d.ts.map +1 -1
- package/dist/cli/internal/wizard.js +21 -18
- package/dist/cli/internal/wizard.js.map +1 -1
- package/dist/cluster/ClusterSchema.d.ts +28 -0
- package/dist/cluster/ClusterSchema.d.ts.map +1 -1
- package/dist/cluster/ClusterSchema.js +30 -0
- package/dist/cluster/ClusterSchema.js.map +1 -1
- package/dist/cluster/ClusterWorkflowEngine.d.ts.map +1 -1
- package/dist/cluster/ClusterWorkflowEngine.js +56 -26
- package/dist/cluster/ClusterWorkflowEngine.js.map +1 -1
- package/dist/cluster/MessageStorage.d.ts +18 -3
- package/dist/cluster/MessageStorage.d.ts.map +1 -1
- package/dist/cluster/MessageStorage.js +31 -11
- package/dist/cluster/MessageStorage.js.map +1 -1
- package/dist/cluster/Runners.d.ts.map +1 -1
- package/dist/cluster/Runners.js +8 -2
- package/dist/cluster/Runners.js.map +1 -1
- package/dist/cluster/Sharding.d.ts.map +1 -1
- package/dist/cluster/Sharding.js +187 -66
- package/dist/cluster/Sharding.js.map +1 -1
- package/dist/cluster/SqlMessageStorage.d.ts +59 -19
- package/dist/cluster/SqlMessageStorage.d.ts.map +1 -1
- package/dist/cluster/SqlMessageStorage.js +84 -55
- package/dist/cluster/SqlMessageStorage.js.map +1 -1
- package/dist/cluster/SqlRunnerStorage.d.ts +68 -19
- package/dist/cluster/SqlRunnerStorage.d.ts.map +1 -1
- package/dist/cluster/SqlRunnerStorage.js +191 -113
- package/dist/cluster/SqlRunnerStorage.js.map +1 -1
- package/dist/cluster/internal/entityManager.js +104 -37
- package/dist/cluster/internal/entityManager.js.map +1 -1
- package/dist/encoding/Sse.d.ts +6 -0
- package/dist/encoding/Sse.d.ts.map +1 -1
- package/dist/encoding/Sse.js +62 -70
- package/dist/encoding/Sse.js.map +1 -1
- package/dist/graphql/GraphQL.d.ts +349 -0
- package/dist/graphql/GraphQL.d.ts.map +1 -0
- package/dist/graphql/GraphQL.js +207 -0
- package/dist/graphql/GraphQL.js.map +1 -0
- package/dist/graphql/GraphQLClient.d.ts +562 -0
- package/dist/graphql/GraphQLClient.d.ts.map +1 -0
- package/dist/graphql/GraphQLClient.js +498 -0
- package/dist/graphql/GraphQLClient.js.map +1 -0
- package/dist/graphql/GraphQLClientError.d.ts +445 -0
- package/dist/graphql/GraphQLClientError.d.ts.map +1 -0
- package/dist/graphql/GraphQLClientError.js +479 -0
- package/dist/graphql/GraphQLClientError.js.map +1 -0
- package/dist/graphql/GraphQLGroup.d.ts +118 -0
- package/dist/graphql/GraphQLGroup.d.ts.map +1 -0
- package/dist/graphql/GraphQLGroup.js +109 -0
- package/dist/graphql/GraphQLGroup.js.map +1 -0
- package/dist/graphql/GraphQLMiddleware.d.ts +249 -0
- package/dist/graphql/GraphQLMiddleware.d.ts.map +1 -0
- package/dist/graphql/GraphQLMiddleware.js +130 -0
- package/dist/graphql/GraphQLMiddleware.js.map +1 -0
- package/dist/graphql/GraphQLProtocol.d.ts +731 -0
- package/dist/graphql/GraphQLProtocol.d.ts.map +1 -0
- package/dist/graphql/GraphQLProtocol.js +719 -0
- package/dist/graphql/GraphQLProtocol.js.map +1 -0
- package/dist/graphql/GraphQLResolver.d.ts +453 -0
- package/dist/graphql/GraphQLResolver.d.ts.map +1 -0
- package/dist/graphql/GraphQLResolver.js +552 -0
- package/dist/graphql/GraphQLResolver.js.map +1 -0
- package/dist/graphql/index.d.ts +40 -0
- package/dist/graphql/index.d.ts.map +1 -0
- package/dist/graphql/index.js +41 -0
- package/dist/graphql/index.js.map +1 -0
- package/dist/http/FindMyWay/internal/router.js +3 -6
- package/dist/http/FindMyWay/internal/router.js.map +1 -1
- package/dist/http/HttpClient.d.ts +21 -0
- package/dist/http/HttpClient.d.ts.map +1 -1
- package/dist/http/HttpClient.js +63 -12
- package/dist/http/HttpClient.js.map +1 -1
- package/dist/http/HttpEffect.d.ts.map +1 -1
- package/dist/http/HttpEffect.js +14 -7
- package/dist/http/HttpEffect.js.map +1 -1
- package/dist/http/HttpMiddleware.d.ts.map +1 -1
- package/dist/http/HttpMiddleware.js +31 -32
- package/dist/http/HttpMiddleware.js.map +1 -1
- package/dist/http/HttpRouter.d.ts +35 -0
- package/dist/http/HttpRouter.d.ts.map +1 -1
- package/dist/http/HttpRouter.js +35 -0
- package/dist/http/HttpRouter.js.map +1 -1
- package/dist/http/HttpServerResponse.d.ts +8 -6
- package/dist/http/HttpServerResponse.d.ts.map +1 -1
- package/dist/http/HttpServerResponse.js +27 -16
- package/dist/http/HttpServerResponse.js.map +1 -1
- package/dist/http/HttpTraceContext.d.ts.map +1 -1
- package/dist/http/HttpTraceContext.js +4 -2
- package/dist/http/HttpTraceContext.js.map +1 -1
- package/dist/http/Multipart.d.ts.map +1 -1
- package/dist/http/Multipart.js +15 -1
- package/dist/http/Multipart.js.map +1 -1
- package/dist/http-api/HttpApiScalar.d.ts +5 -0
- package/dist/http-api/HttpApiScalar.d.ts.map +1 -1
- package/dist/http-api/HttpApiScalar.js.map +1 -1
- package/dist/internal/dateTime.js +40 -35
- package/dist/internal/dateTime.js.map +1 -1
- package/dist/internal/effect.js +19 -9
- package/dist/internal/effect.js.map +1 -1
- package/dist/internal/graphql.d.ts +2 -0
- package/dist/internal/graphql.d.ts.map +1 -0
- package/dist/internal/graphql.js +195 -0
- package/dist/internal/graphql.js.map +1 -0
- package/dist/internal/graphqlHeaders.d.ts +4 -0
- package/dist/internal/graphqlHeaders.d.ts.map +1 -0
- package/dist/internal/graphqlHeaders.js +11 -0
- package/dist/internal/graphqlHeaders.js.map +1 -0
- package/dist/internal/request.js +20 -1
- package/dist/internal/request.js.map +1 -1
- package/dist/internal/schema/toCodeDocument.js +4 -1
- package/dist/internal/schema/toCodeDocument.js.map +1 -1
- package/dist/internal/schema/toJsonSchemaDocument.js +29 -5
- package/dist/internal/schema/toJsonSchemaDocument.js.map +1 -1
- package/dist/internal/version.js +1 -1
- package/dist/net/AddressResolver.d.ts +145 -0
- package/dist/net/AddressResolver.d.ts.map +1 -0
- package/dist/net/AddressResolver.js +114 -0
- package/dist/net/AddressResolver.js.map +1 -0
- package/dist/net/Dns.d.ts +596 -0
- package/dist/net/Dns.d.ts.map +1 -0
- package/dist/net/Dns.js +549 -0
- package/dist/net/Dns.js.map +1 -0
- package/dist/net/Host.d.ts +369 -0
- package/dist/net/Host.d.ts.map +1 -0
- package/dist/net/Host.js +397 -0
- package/dist/net/Host.js.map +1 -0
- package/dist/net/IpInterface.d.ts +76 -3
- package/dist/net/IpInterface.d.ts.map +1 -1
- package/dist/net/IpInterface.js +53 -4
- package/dist/net/IpInterface.js.map +1 -1
- package/dist/net/IpNetwork.d.ts +82 -8
- package/dist/net/IpNetwork.d.ts.map +1 -1
- package/dist/net/IpNetwork.js +60 -4
- package/dist/net/IpNetwork.js.map +1 -1
- package/dist/net/NetAddress.d.ts +396 -35
- package/dist/net/NetAddress.d.ts.map +1 -1
- package/dist/net/NetAddress.js +358 -34
- package/dist/net/NetAddress.js.map +1 -1
- package/dist/net/index.d.ts +15 -0
- package/dist/net/index.d.ts.map +1 -1
- package/dist/net/index.js +15 -0
- package/dist/net/index.js.map +1 -1
- package/dist/observability/OtlpMetrics.d.ts.map +1 -1
- package/dist/observability/OtlpMetrics.js +15 -21
- package/dist/observability/OtlpMetrics.js.map +1 -1
- package/dist/observability/PrometheusMetrics.js +1 -0
- package/dist/observability/PrometheusMetrics.js.map +1 -1
- package/dist/persistence/PersistedQueue.d.ts +32 -3
- package/dist/persistence/PersistedQueue.d.ts.map +1 -1
- package/dist/persistence/PersistedQueue.js +75 -16
- package/dist/persistence/PersistedQueue.js.map +1 -1
- package/dist/rpc/RpcClient.d.ts +22 -6
- package/dist/rpc/RpcClient.d.ts.map +1 -1
- package/dist/rpc/RpcClient.js +12 -5
- package/dist/rpc/RpcClient.js.map +1 -1
- package/dist/rpc/RpcServer.d.ts +6 -0
- package/dist/rpc/RpcServer.d.ts.map +1 -1
- package/dist/rpc/RpcServer.js +78 -29
- package/dist/rpc/RpcServer.js.map +1 -1
- package/dist/socket/DatagramSocket.d.ts +456 -0
- package/dist/socket/DatagramSocket.d.ts.map +1 -0
- package/dist/socket/DatagramSocket.js +535 -0
- package/dist/socket/DatagramSocket.js.map +1 -0
- package/dist/socket/Socket.d.ts +20 -1
- package/dist/socket/Socket.d.ts.map +1 -1
- package/dist/socket/Socket.js +9 -1
- package/dist/socket/Socket.js.map +1 -1
- package/dist/socket/index.d.ts +5 -0
- package/dist/socket/index.d.ts.map +1 -1
- package/dist/socket/index.js +5 -0
- package/dist/socket/index.js.map +1 -1
- package/dist/sql/Migrator.d.ts.map +1 -1
- package/dist/sql/Migrator.js +7 -2
- package/dist/sql/Migrator.js.map +1 -1
- package/dist/sql/SqlClient.d.ts +7 -1
- package/dist/sql/SqlClient.d.ts.map +1 -1
- package/dist/sql/SqlClient.js +31 -6
- package/dist/sql/SqlClient.js.map +1 -1
- package/dist/testing/TestCrypto.d.ts +73 -0
- package/dist/testing/TestCrypto.d.ts.map +1 -0
- package/dist/testing/TestCrypto.js +87 -0
- package/dist/testing/TestCrypto.js.map +1 -0
- package/dist/testing/index.d.ts +5 -0
- package/dist/testing/index.d.ts.map +1 -1
- package/dist/testing/index.js +5 -0
- package/dist/testing/index.js.map +1 -1
- package/dist/workflow/DurableDeferred.d.ts +54 -0
- package/dist/workflow/DurableDeferred.d.ts.map +1 -1
- package/dist/workflow/DurableDeferred.js +108 -50
- package/dist/workflow/DurableDeferred.js.map +1 -1
- package/dist/workflow/WorkflowEngine.d.ts +24 -2
- package/dist/workflow/WorkflowEngine.d.ts.map +1 -1
- package/dist/workflow/WorkflowEngine.js +13 -3
- package/dist/workflow/WorkflowEngine.js.map +1 -1
- package/package.json +6 -3
- package/src/Context.ts +12 -3
- package/src/Crypto.ts +43 -9
- package/src/DateTime.ts +9 -6
- package/src/Duration.ts +36 -16
- package/src/FileSystem.ts +48 -2
- package/src/RequestResolver.ts +4 -10
- package/src/Scheduler.ts +9 -0
- package/src/Schema.ts +677 -5
- package/src/SchemaGetter.ts +47 -35
- package/src/Sink.ts +45 -18
- package/src/Stdio.ts +9 -2
- package/src/Stream.ts +44 -15
- package/src/Utils.ts +8 -5
- package/src/ai/AnthropicStructuredOutput.ts +2 -2
- package/src/ai/Chat.ts +2 -2
- package/src/ai/LanguageModel.ts +96 -24
- package/src/ai/McpServer.ts +288 -94
- package/src/ai/Prompt.ts +24 -10
- package/src/ai/Response.ts +6 -3
- package/src/ai/internal/mcpProtocol/v2026_07_28.ts +5 -0
- package/src/ai/internal/mcpProtocol.ts +3 -0
- package/src/ai/internal/mcpRuntime.ts +2 -0
- package/src/cli/CliOutput.ts +33 -16
- package/src/cli/Command.ts +4 -3
- package/src/cli/Completions.ts +9 -6
- package/src/cli/GlobalFlag.ts +19 -11
- package/src/cli/Prompt.ts +159 -149
- package/src/cli/internal/ansi.ts +26 -3
- package/src/cli/internal/completions/powershell.ts +322 -0
- package/src/cli/internal/completions/zsh.ts +4 -2
- package/src/cli/internal/wizard.ts +34 -26
- package/src/cluster/ClusterSchema.ts +32 -0
- package/src/cluster/ClusterWorkflowEngine.ts +64 -36
- package/src/cluster/MessageStorage.ts +45 -14
- package/src/cluster/Runners.ts +8 -2
- package/src/cluster/Sharding.ts +219 -58
- package/src/cluster/SqlMessageStorage.ts +118 -84
- package/src/cluster/SqlRunnerStorage.ts +224 -128
- package/src/cluster/internal/entityManager.ts +151 -55
- package/src/encoding/Sse.ts +69 -79
- package/src/graphql/GraphQL.ts +494 -0
- package/src/graphql/GraphQLClient.ts +912 -0
- package/src/graphql/GraphQLClientError.ts +542 -0
- package/src/graphql/GraphQLGroup.ts +167 -0
- package/src/graphql/GraphQLMiddleware.ts +253 -0
- package/src/graphql/GraphQLProtocol.ts +1286 -0
- package/src/graphql/GraphQLResolver.ts +1010 -0
- package/src/graphql/index.ts +48 -0
- package/src/http/FindMyWay/internal/router.ts +4 -7
- package/src/http/HttpClient.ts +110 -12
- package/src/http/HttpEffect.ts +11 -5
- package/src/http/HttpMiddleware.ts +33 -34
- package/src/http/HttpRouter.ts +35 -0
- package/src/http/HttpServerResponse.ts +28 -16
- package/src/http/HttpTraceContext.ts +8 -2
- package/src/http/Multipart.ts +17 -1
- package/src/http-api/HttpApiScalar.ts +5 -0
- package/src/internal/dateTime.ts +13 -10
- package/src/internal/effect.ts +22 -15
- package/src/internal/graphql.ts +263 -0
- package/src/internal/graphqlHeaders.ts +11 -0
- package/src/internal/request.ts +31 -3
- package/src/internal/schema/toCodeDocument.ts +7 -1
- package/src/internal/schema/toJsonSchemaDocument.ts +34 -2
- package/src/internal/version.ts +1 -1
- package/src/net/AddressResolver.ts +222 -0
- package/src/net/Dns.ts +1035 -0
- package/src/net/Host.ts +544 -0
- package/src/net/IpInterface.ts +117 -7
- package/src/net/IpNetwork.ts +116 -17
- package/src/net/NetAddress.ts +620 -76
- package/src/net/index.ts +18 -0
- package/src/observability/OtlpMetrics.ts +12 -23
- package/src/observability/PrometheusMetrics.ts +1 -0
- package/src/persistence/PersistedQueue.ts +146 -53
- package/src/rpc/RpcClient.ts +41 -19
- package/src/rpc/RpcServer.ts +115 -40
- package/src/socket/DatagramSocket.ts +919 -0
- package/src/socket/Socket.ts +28 -2
- package/src/socket/index.ts +6 -0
- package/src/sql/Migrator.ts +21 -5
- package/src/sql/SqlClient.ts +39 -15
- package/src/testing/TestCrypto.ts +94 -0
- package/src/testing/index.ts +6 -0
- package/src/workflow/DurableDeferred.ts +160 -47
- package/src/workflow/WorkflowEngine.ts +50 -5
|
@@ -0,0 +1,919 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pull-based UDP sockets with scoped native handles.
|
|
3
|
+
*
|
|
4
|
+
* Acquiring a reader opens and binds a socket owned by its scope. Only one
|
|
5
|
+
* reader is open at a time; another acquisition waits interruptibly for it to
|
|
6
|
+
* close. Use `Effect.retry` around a scoped consume loop to open a new socket
|
|
7
|
+
* on each retry.
|
|
8
|
+
*
|
|
9
|
+
* Writes wait while no reader is open, so a send-only client must still
|
|
10
|
+
* acquire a reader.
|
|
11
|
+
*
|
|
12
|
+
* **Example** (Portable request/reply client)
|
|
13
|
+
*
|
|
14
|
+
* ```ts import.meta.vitest
|
|
15
|
+
* import { Effect } from "effect"
|
|
16
|
+
* import { NetAddress } from "effect/net"
|
|
17
|
+
* import { DatagramSocket } from "effect/socket"
|
|
18
|
+
*
|
|
19
|
+
* const server = NetAddress.inetAddressFromIpStringUnsafe("127.0.0.1", 5353)
|
|
20
|
+
*
|
|
21
|
+
* export const request = (payload: string) =>
|
|
22
|
+
* Effect.gen(function*() {
|
|
23
|
+
* const socket = yield* DatagramSocket.DatagramSocket
|
|
24
|
+
* const reader = yield* socket.reader
|
|
25
|
+
* const writer = yield* socket.writer
|
|
26
|
+
* yield* writer.write({ payload, address: server })
|
|
27
|
+
* const [response] = yield* reader.pull
|
|
28
|
+
* return response.payload
|
|
29
|
+
* }).pipe(Effect.scoped)
|
|
30
|
+
* ```
|
|
31
|
+
*
|
|
32
|
+
* @stability experimental
|
|
33
|
+
* @since 4.0.0
|
|
34
|
+
*/
|
|
35
|
+
import type { NonEmptyReadonlyArray } from "../Array.ts"
|
|
36
|
+
import * as Context from "../Context.ts"
|
|
37
|
+
import * as Effect from "../Effect.ts"
|
|
38
|
+
import * as Fiber from "../Fiber.ts"
|
|
39
|
+
import { constVoid } from "../Function.ts"
|
|
40
|
+
import { args, contA, contAll, exitSucceed, makePrimitive, type Primitive, withFiber } from "../internal/core.ts"
|
|
41
|
+
import type { FiberImpl } from "../internal/effect.ts"
|
|
42
|
+
import * as Latch from "../Latch.ts"
|
|
43
|
+
import * as NetAddress from "../net/NetAddress.ts"
|
|
44
|
+
import * as Predicate from "../Predicate.ts"
|
|
45
|
+
import * as Schema from "../Schema.ts"
|
|
46
|
+
import * as Scope from "../Scope.ts"
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Runtime type identifier attached to `DatagramSocket` services.
|
|
50
|
+
*
|
|
51
|
+
* @stability experimental
|
|
52
|
+
* @category type IDs
|
|
53
|
+
* @since 4.0.0
|
|
54
|
+
*/
|
|
55
|
+
export const TypeId = "~effect/socket/DatagramSocket"
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Service tag for UDP socket transports.
|
|
59
|
+
*
|
|
60
|
+
* @stability experimental
|
|
61
|
+
* @category services
|
|
62
|
+
* @since 4.0.0
|
|
63
|
+
*/
|
|
64
|
+
export const DatagramSocket: Context.Service<DatagramSocket, DatagramSocket> = Context.Service<DatagramSocket>(
|
|
65
|
+
"effect/socket/DatagramSocket"
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* A UDP socket with a scoped, exclusive reader. Acquiring `reader` opens and
|
|
70
|
+
* binds a native socket; subsequent acquisitions wait for its scope to close.
|
|
71
|
+
* Acquiring `writer` cannot fail, but writes wait for an open reader.
|
|
72
|
+
*
|
|
73
|
+
* @stability experimental
|
|
74
|
+
* @category models
|
|
75
|
+
* @since 4.0.0
|
|
76
|
+
*/
|
|
77
|
+
export interface DatagramSocket {
|
|
78
|
+
readonly [TypeId]: typeof TypeId
|
|
79
|
+
readonly reader: Effect.Effect<Reader, DatagramSocketError, Scope.Scope>
|
|
80
|
+
readonly writer: Effect.Effect<Writer, never, Scope.Scope>
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* A received packet.
|
|
85
|
+
*
|
|
86
|
+
* **Details**
|
|
87
|
+
*
|
|
88
|
+
* The payload is safe to retain. The sender's `address` is parsed on first
|
|
89
|
+
* read and cached.
|
|
90
|
+
*
|
|
91
|
+
* Reply with `writer.write({ payload, address: received })` to reuse the raw
|
|
92
|
+
* sender address without parsing. This requires a datagram from a platform
|
|
93
|
+
* adapter or `makeFromBackingSocket`. For hand-built datagrams or custom
|
|
94
|
+
* readers, use `{ payload, address: received.address }`.
|
|
95
|
+
*
|
|
96
|
+
* @stability experimental
|
|
97
|
+
* @category models
|
|
98
|
+
* @since 4.0.0
|
|
99
|
+
*/
|
|
100
|
+
export interface Datagram {
|
|
101
|
+
readonly payload: Uint8Array
|
|
102
|
+
readonly address: NetAddress.InetAddress
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* A packet to send.
|
|
107
|
+
*
|
|
108
|
+
* **Details**
|
|
109
|
+
*
|
|
110
|
+
* String payloads are encoded as UTF-8. `address` can be omitted when the
|
|
111
|
+
* socket has a `peer` or is connected. On a connected socket an explicit
|
|
112
|
+
* `InetAddress` fails the write, while a received `Datagram` is allowed and
|
|
113
|
+
* goes out on the connected path.
|
|
114
|
+
*
|
|
115
|
+
* Pass a received `Datagram` as the destination to reply, or as the whole
|
|
116
|
+
* argument to echo it. Both reuse the raw sender address and require a
|
|
117
|
+
* datagram from a platform adapter or `makeFromBackingSocket`. Hand-built
|
|
118
|
+
* datagrams and custom readers must pass an explicit `InetAddress` instead.
|
|
119
|
+
*
|
|
120
|
+
* @stability experimental
|
|
121
|
+
* @category models
|
|
122
|
+
* @since 4.0.0
|
|
123
|
+
*/
|
|
124
|
+
export interface OutgoingDatagram {
|
|
125
|
+
readonly payload: Uint8Array | string
|
|
126
|
+
readonly address?: NetAddress.InetAddress | Datagram | undefined
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* The receive side of an open native socket.
|
|
131
|
+
*
|
|
132
|
+
* **Details**
|
|
133
|
+
*
|
|
134
|
+
* `pull` returns everything queued since the last pull, or waits for the next
|
|
135
|
+
* packet. After a terminal error, `pull` first returns the queued packets and
|
|
136
|
+
* then fails with the same error until the reader's scope closes. Closing the
|
|
137
|
+
* scope discards the queue and fails every waiting `pull` with
|
|
138
|
+
* `DatagramSocketClosedError`.
|
|
139
|
+
*
|
|
140
|
+
* Concurrent pulls wait in FIFO order. Each incoming packet goes to the
|
|
141
|
+
* oldest waiter; a terminal error fails all waiters.
|
|
142
|
+
*
|
|
143
|
+
* `address` is the bound local address, parsed on first read and cached.
|
|
144
|
+
*
|
|
145
|
+
* `dropped` counts receive-buffer overflow since this reader opened, not
|
|
146
|
+
* kernel or network loss or packets discarded on scope close.
|
|
147
|
+
*
|
|
148
|
+
* `joinMulticast` joins a group until its scope closes; leave failures are
|
|
149
|
+
* ignored. Its `interface` selects where packets arrive (ingress), not the
|
|
150
|
+
* platform's `multicast.interface` for sending (egress).
|
|
151
|
+
*
|
|
152
|
+
* @stability experimental
|
|
153
|
+
* @category models
|
|
154
|
+
* @since 4.0.0
|
|
155
|
+
*/
|
|
156
|
+
export interface Reader {
|
|
157
|
+
readonly pull: Effect.Effect<NonEmptyReadonlyArray<Datagram>, DatagramSocketError>
|
|
158
|
+
readonly address: NetAddress.InetAddress
|
|
159
|
+
readonly dropped: () => number
|
|
160
|
+
readonly joinMulticast: <A extends NetAddress.IpAddress>(options: {
|
|
161
|
+
readonly group: NetAddress.MulticastAddress<A>
|
|
162
|
+
readonly interface?: NetAddress.MulticastInterface<A> | undefined
|
|
163
|
+
readonly source?: A | undefined
|
|
164
|
+
}) => Effect.Effect<void, DatagramSocketError, Scope.Scope>
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* The send side of a `DatagramSocket`. Writes wait until a reader is open, so
|
|
169
|
+
* a send-only client must still acquire a reader.
|
|
170
|
+
*
|
|
171
|
+
* **Details**
|
|
172
|
+
*
|
|
173
|
+
* A write completes when the runtime reports the send's result, and send
|
|
174
|
+
* errors fail it with a `DatagramSocketWriteError` carrying the destination
|
|
175
|
+
* `address`. After a terminal reader error, writes fail with that error until
|
|
176
|
+
* the reader's scope closes, and then wait for the next reader.
|
|
177
|
+
*
|
|
178
|
+
* `writeAll` fails with the first error, without resending. Partial delivery is
|
|
179
|
+
* unspecified. The error has no packet index, and its `address` is absent on Bun.
|
|
180
|
+
*
|
|
181
|
+
* There is no built-in write timeout; use `Effect.timeout`.
|
|
182
|
+
*
|
|
183
|
+
* @stability experimental
|
|
184
|
+
* @category models
|
|
185
|
+
* @since 4.0.0
|
|
186
|
+
*/
|
|
187
|
+
export interface Writer {
|
|
188
|
+
readonly write: (datagram: OutgoingDatagram) => Effect.Effect<void, DatagramSocketError>
|
|
189
|
+
readonly writeAll: (datagrams: NonEmptyReadonlyArray<OutgoingDatagram>) => Effect.Effect<void, DatagramSocketError>
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Receive queue options.
|
|
194
|
+
*
|
|
195
|
+
* **Details**
|
|
196
|
+
*
|
|
197
|
+
* The queue is bounded in packets, with no byte cap. `capacity` defaults to
|
|
198
|
+
* 1024 and must be a positive safe integer. The worst-case
|
|
199
|
+
* memory is capacity × 64 KiB, about 64 MiB at the default.
|
|
200
|
+
*
|
|
201
|
+
* When the queue is full, `"dropping"` (the default) discards the incoming
|
|
202
|
+
* packet and `"sliding"` discards the oldest queued one. Both count the
|
|
203
|
+
* packet in `Reader.dropped`.
|
|
204
|
+
*
|
|
205
|
+
* @stability experimental
|
|
206
|
+
* @category models
|
|
207
|
+
* @since 4.0.0
|
|
208
|
+
*/
|
|
209
|
+
export interface ReceiveBufferOptions {
|
|
210
|
+
readonly capacity?: number | undefined
|
|
211
|
+
readonly strategy?: "dropping" | "sliding" | undefined
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Creates a `DatagramSocket` from its `reader` and `writer` effects.
|
|
216
|
+
*
|
|
217
|
+
* @stability experimental
|
|
218
|
+
* @category constructors
|
|
219
|
+
* @since 4.0.0
|
|
220
|
+
*/
|
|
221
|
+
export const make = (options: {
|
|
222
|
+
readonly reader: DatagramSocket["reader"]
|
|
223
|
+
readonly writer: DatagramSocket["writer"]
|
|
224
|
+
}): DatagramSocket =>
|
|
225
|
+
DatagramSocket.of({
|
|
226
|
+
[TypeId]: TypeId,
|
|
227
|
+
reader: options.reader,
|
|
228
|
+
writer: options.writer
|
|
229
|
+
})
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* A native host and port, parsed on demand.
|
|
233
|
+
*
|
|
234
|
+
* @stability experimental
|
|
235
|
+
* @category models
|
|
236
|
+
* @since 4.0.0
|
|
237
|
+
*/
|
|
238
|
+
export interface BackingAddress {
|
|
239
|
+
readonly host: string
|
|
240
|
+
readonly port: number
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Native transport contract used by platform adapters.
|
|
245
|
+
*
|
|
246
|
+
* **Details**
|
|
247
|
+
*
|
|
248
|
+
* `address` is the raw bound address, read once at open. `scopeIds` maps IPv6
|
|
249
|
+
* zone names to scope IDs, for parsing named zones and formatting
|
|
250
|
+
* destinations. `peer` is the default destination, and `connected` marks a
|
|
251
|
+
* natively connected socket, whose sends receive no destination.
|
|
252
|
+
*
|
|
253
|
+
* `send` and `sendMany` hand datagrams to the runtime and call `done` exactly
|
|
254
|
+
* once, synchronously or later, when the runtime reports the result. They must
|
|
255
|
+
* not throw. `sendMany` passes the index of the failing datagram to `done`
|
|
256
|
+
* when the runtime knows it, so core can attach its address.
|
|
257
|
+
*
|
|
258
|
+
* Adapters normalize native errors before passing them to core.
|
|
259
|
+
* Read destination `host` and `port` during the call; do not retain the record.
|
|
260
|
+
*
|
|
261
|
+
* `close` must not throw.
|
|
262
|
+
*
|
|
263
|
+
* @stability experimental
|
|
264
|
+
* @category models
|
|
265
|
+
* @since 4.0.0
|
|
266
|
+
*/
|
|
267
|
+
export interface BackingSocket {
|
|
268
|
+
readonly address: BackingAddress
|
|
269
|
+
readonly scopeIds?: ReadonlyMap<string, number> | undefined
|
|
270
|
+
readonly peer?: BackingAddress | undefined
|
|
271
|
+
readonly connected?: boolean | undefined
|
|
272
|
+
readonly send: (
|
|
273
|
+
payload: Uint8Array,
|
|
274
|
+
destination: BackingAddress | undefined,
|
|
275
|
+
done: (error?: DatagramSocketError) => void
|
|
276
|
+
) => void
|
|
277
|
+
readonly sendMany: (
|
|
278
|
+
payloads: ReadonlyArray<Uint8Array>,
|
|
279
|
+
destinations: ReadonlyArray<BackingAddress | undefined>,
|
|
280
|
+
done: (error?: DatagramSocketError, index?: number) => void
|
|
281
|
+
) => void
|
|
282
|
+
readonly joinMulticast: <A extends NetAddress.IpAddress>(options: {
|
|
283
|
+
readonly group: NetAddress.MulticastAddress<A>
|
|
284
|
+
readonly interface?: NetAddress.MulticastInterface<A> | undefined
|
|
285
|
+
readonly source?: A | undefined
|
|
286
|
+
}) => Effect.Effect<() => Effect.Effect<void, never>, DatagramSocketError>
|
|
287
|
+
readonly close: () => void
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* Callbacks installed before opening the backing socket; packets may arrive
|
|
292
|
+
* synchronously.
|
|
293
|
+
*
|
|
294
|
+
* **Details**
|
|
295
|
+
*
|
|
296
|
+
* `onPacket` queues a received packet. `onReadError` is a terminal receive
|
|
297
|
+
* error and `onClose` reports the native socket closing underneath the
|
|
298
|
+
* reader; both are sticky. `onError` forwards errors with no write left to
|
|
299
|
+
* fail, such as ICMP reports, to the user's `onError` option.
|
|
300
|
+
*
|
|
301
|
+
* Each callback can be passed on its own, for example as a runtime's event
|
|
302
|
+
* listener.
|
|
303
|
+
*
|
|
304
|
+
* @stability experimental
|
|
305
|
+
* @category models
|
|
306
|
+
* @since 4.0.0
|
|
307
|
+
*/
|
|
308
|
+
export interface BackingEvents {
|
|
309
|
+
readonly onPacket: (payload: Uint8Array, host: string, port: number) => void
|
|
310
|
+
readonly onReadError: (error: DatagramSocketError) => void
|
|
311
|
+
readonly onError: (error: DatagramSocketError) => void
|
|
312
|
+
readonly onClose: () => void
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/**
|
|
316
|
+
* The normalized kind of a send or receive failure.
|
|
317
|
+
*
|
|
318
|
+
* @stability experimental
|
|
319
|
+
* @category errors
|
|
320
|
+
* @since 4.0.0
|
|
321
|
+
*/
|
|
322
|
+
export type IoErrorKind = "MessageTooLarge" | "Unreachable" | "ConnectionRefused" | "PermissionDenied" | "Unknown"
|
|
323
|
+
|
|
324
|
+
/**
|
|
325
|
+
* Opening or binding the native socket, applying its options, or resolving
|
|
326
|
+
* a name failed.
|
|
327
|
+
*
|
|
328
|
+
* @stability experimental
|
|
329
|
+
* @category errors
|
|
330
|
+
* @since 4.0.0
|
|
331
|
+
*/
|
|
332
|
+
export class DatagramSocketOpenError
|
|
333
|
+
extends Schema.Error<DatagramSocketOpenError>("effect/socket/DatagramSocket/OpenError")({
|
|
334
|
+
_tag: Schema.tag("DatagramSocketOpenError"),
|
|
335
|
+
kind: Schema.Literals(["AddressInUse", "AddressNotAvailable", "PermissionDenied", "Unknown"]),
|
|
336
|
+
cause: Schema.Defect()
|
|
337
|
+
})
|
|
338
|
+
{}
|
|
339
|
+
|
|
340
|
+
/**
|
|
341
|
+
* A receive failed, or an ICMP report arrived with no write left to fail.
|
|
342
|
+
*
|
|
343
|
+
* @stability experimental
|
|
344
|
+
* @category errors
|
|
345
|
+
* @since 4.0.0
|
|
346
|
+
*/
|
|
347
|
+
export class DatagramSocketReadError
|
|
348
|
+
extends Schema.Error<DatagramSocketReadError>("effect/socket/DatagramSocket/ReadError")({
|
|
349
|
+
_tag: Schema.tag("DatagramSocketReadError"),
|
|
350
|
+
kind: Schema.Literals(["MessageTooLarge", "Unreachable", "ConnectionRefused", "PermissionDenied", "Unknown"]),
|
|
351
|
+
cause: Schema.Defect()
|
|
352
|
+
})
|
|
353
|
+
{}
|
|
354
|
+
|
|
355
|
+
/**
|
|
356
|
+
* A send failed. `address` is the destination when it is known.
|
|
357
|
+
*
|
|
358
|
+
* @stability experimental
|
|
359
|
+
* @category errors
|
|
360
|
+
* @since 4.0.0
|
|
361
|
+
*/
|
|
362
|
+
export class DatagramSocketWriteError
|
|
363
|
+
extends Schema.Error<DatagramSocketWriteError>("effect/socket/DatagramSocket/WriteError")({
|
|
364
|
+
_tag: Schema.tag("DatagramSocketWriteError"),
|
|
365
|
+
kind: Schema.Literals(["MessageTooLarge", "Unreachable", "ConnectionRefused", "PermissionDenied", "Unknown"]),
|
|
366
|
+
address: Schema.optional(Schema.InetAddress),
|
|
367
|
+
cause: Schema.Defect()
|
|
368
|
+
})
|
|
369
|
+
{}
|
|
370
|
+
|
|
371
|
+
/**
|
|
372
|
+
* The native socket closed underneath the reader, or the reader's scope
|
|
373
|
+
* closed.
|
|
374
|
+
*
|
|
375
|
+
* @stability experimental
|
|
376
|
+
* @category errors
|
|
377
|
+
* @since 4.0.0
|
|
378
|
+
*/
|
|
379
|
+
export class DatagramSocketClosedError
|
|
380
|
+
extends Schema.Error<DatagramSocketClosedError>("effect/socket/DatagramSocket/ClosedError")({
|
|
381
|
+
_tag: Schema.tag("DatagramSocketClosedError")
|
|
382
|
+
})
|
|
383
|
+
{}
|
|
384
|
+
|
|
385
|
+
/**
|
|
386
|
+
* The runtime lacks a capability, such as source-specific multicast on Deno.
|
|
387
|
+
*
|
|
388
|
+
* @stability experimental
|
|
389
|
+
* @category errors
|
|
390
|
+
* @since 4.0.0
|
|
391
|
+
*/
|
|
392
|
+
export class DatagramSocketUnsupportedError
|
|
393
|
+
extends Schema.Error<DatagramSocketUnsupportedError>("effect/socket/DatagramSocket/UnsupportedError")({
|
|
394
|
+
_tag: Schema.tag("DatagramSocketUnsupportedError"),
|
|
395
|
+
capability: Schema.String,
|
|
396
|
+
runtime: Schema.String
|
|
397
|
+
})
|
|
398
|
+
{
|
|
399
|
+
/**
|
|
400
|
+
* Names the runtime and the capability it lacks.
|
|
401
|
+
*
|
|
402
|
+
* @stability experimental
|
|
403
|
+
* @since 4.0.0
|
|
404
|
+
*/
|
|
405
|
+
override get message() {
|
|
406
|
+
return `${this.runtime} does not support ${this.capability}`
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
/**
|
|
411
|
+
* Schema for the union of `DatagramSocketError` reasons.
|
|
412
|
+
*
|
|
413
|
+
* @stability experimental
|
|
414
|
+
* @category errors
|
|
415
|
+
* @since 4.0.0
|
|
416
|
+
*/
|
|
417
|
+
export const DatagramSocketErrorReason = Schema.Union([
|
|
418
|
+
DatagramSocketOpenError,
|
|
419
|
+
DatagramSocketReadError,
|
|
420
|
+
DatagramSocketWriteError,
|
|
421
|
+
DatagramSocketClosedError,
|
|
422
|
+
DatagramSocketUnsupportedError
|
|
423
|
+
])
|
|
424
|
+
/**
|
|
425
|
+
* The union of `DatagramSocketError` reasons.
|
|
426
|
+
*
|
|
427
|
+
* @stability experimental
|
|
428
|
+
* @category errors
|
|
429
|
+
* @since 4.0.0
|
|
430
|
+
*/
|
|
431
|
+
export type DatagramSocketErrorReason = typeof DatagramSocketErrorReason.Type
|
|
432
|
+
|
|
433
|
+
/**
|
|
434
|
+
* Runtime type identifier attached to `DatagramSocketError` values.
|
|
435
|
+
*
|
|
436
|
+
* @stability experimental
|
|
437
|
+
* @category errors
|
|
438
|
+
* @since 4.0.0
|
|
439
|
+
*/
|
|
440
|
+
export const DatagramSocketErrorTypeId = "~effect/socket/DatagramSocket/DatagramSocketError"
|
|
441
|
+
|
|
442
|
+
/**
|
|
443
|
+
* Returns `true` when a value is a `DatagramSocketError`.
|
|
444
|
+
*
|
|
445
|
+
* @stability experimental
|
|
446
|
+
* @category guards
|
|
447
|
+
* @since 4.0.0
|
|
448
|
+
*/
|
|
449
|
+
export const isDatagramSocketError = (u: unknown): u is DatagramSocketError =>
|
|
450
|
+
Predicate.hasProperty(u, DatagramSocketErrorTypeId)
|
|
451
|
+
|
|
452
|
+
/**
|
|
453
|
+
* The error raised by `DatagramSocket` operations, wrapping a specific
|
|
454
|
+
* reason. Its `cause` and `message` come from the reason.
|
|
455
|
+
*
|
|
456
|
+
* @stability experimental
|
|
457
|
+
* @category errors
|
|
458
|
+
* @since 4.0.0
|
|
459
|
+
*/
|
|
460
|
+
export class DatagramSocketError
|
|
461
|
+
extends Schema.TaggedError<DatagramSocketError>(DatagramSocketErrorTypeId)("DatagramSocketError", {
|
|
462
|
+
_tag: Schema.tag("DatagramSocketError"),
|
|
463
|
+
reason: DatagramSocketErrorReason
|
|
464
|
+
})
|
|
465
|
+
{
|
|
466
|
+
// @effect-diagnostics-next-line overriddenSchemaConstructor:off
|
|
467
|
+
constructor(props: { readonly reason: DatagramSocketErrorReason }) {
|
|
468
|
+
if ("cause" in props.reason) {
|
|
469
|
+
super({ ...props, cause: props.reason.cause } as any)
|
|
470
|
+
} else {
|
|
471
|
+
super(props)
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
readonly [DatagramSocketErrorTypeId] = DatagramSocketErrorTypeId
|
|
475
|
+
override get message() {
|
|
476
|
+
return this.reason.message
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
/**
|
|
481
|
+
* Builds a `DatagramSocket` over a backing socket, opening a new socket for
|
|
482
|
+
* each reader acquisition.
|
|
483
|
+
*
|
|
484
|
+
* **Details**
|
|
485
|
+
*
|
|
486
|
+
* `open` receives the event callbacks before the handle exists, so packets can
|
|
487
|
+
* be queued while it opens. Core owns the receive queue and its overflow
|
|
488
|
+
* strategy, reader exclusivity, sticky errors, lazy address parsing, the
|
|
489
|
+
* writer latch and destination formatting. The handle is closed when the
|
|
490
|
+
* reader's scope closes. If acquisition is interrupted while `open` is still
|
|
491
|
+
* running, the interruption returns at once, `open` is left to finish, and
|
|
492
|
+
* the handle it produces is closed. The next reader waits until that open has
|
|
493
|
+
* settled, so two native sockets never exist at once. An adopted `acquire`
|
|
494
|
+
* must therefore finish in bounded time.
|
|
495
|
+
*
|
|
496
|
+
* `open` runs in the reader's scope, with the services of the fiber that
|
|
497
|
+
* built the socket, so an adopted `acquire` can use them and register
|
|
498
|
+
* finalizers that run when the reader closes. Where the fiber acquiring the
|
|
499
|
+
* reader has a service with the same tag, its own takes precedence.
|
|
500
|
+
*
|
|
501
|
+
* Concurrent pulls are served in the order they started waiting.
|
|
502
|
+
*
|
|
503
|
+
* `onError` receives errors with no write left to fail. It runs
|
|
504
|
+
* synchronously, and anything it throws is ignored.
|
|
505
|
+
*
|
|
506
|
+
* Invalid `receiveBuffer.capacity` values cause a defect; use a positive
|
|
507
|
+
* safe integer.
|
|
508
|
+
*
|
|
509
|
+
* @stability experimental
|
|
510
|
+
* @category constructors
|
|
511
|
+
* @since 4.0.0
|
|
512
|
+
*/
|
|
513
|
+
export const makeFromBackingSocket = <R = never>(
|
|
514
|
+
open: (events: BackingEvents) => Effect.Effect<BackingSocket, DatagramSocketError, R>,
|
|
515
|
+
options?: {
|
|
516
|
+
readonly receiveBuffer?: ReceiveBufferOptions | undefined
|
|
517
|
+
readonly onError?: ((error: DatagramSocketError) => void) | undefined
|
|
518
|
+
} | undefined
|
|
519
|
+
): Effect.Effect<DatagramSocket, never, Exclude<R, Scope.Scope>> =>
|
|
520
|
+
withFiber((fiber) => {
|
|
521
|
+
const capacity = options?.receiveBuffer?.capacity ?? 1024
|
|
522
|
+
if (!Number.isSafeInteger(capacity) || capacity < 1) {
|
|
523
|
+
throw new RangeError(`DatagramSocket receive buffer capacity must be a positive integer, received ${capacity}`)
|
|
524
|
+
}
|
|
525
|
+
return Effect.succeed(
|
|
526
|
+
makeFromBackingSocketWithContext(
|
|
527
|
+
open,
|
|
528
|
+
fiber.context as Context.Context<R>,
|
|
529
|
+
capacity,
|
|
530
|
+
options?.receiveBuffer?.strategy === "sliding",
|
|
531
|
+
options?.onError
|
|
532
|
+
)
|
|
533
|
+
)
|
|
534
|
+
})
|
|
535
|
+
|
|
536
|
+
const makeFromBackingSocketWithContext = <R>(
|
|
537
|
+
open: (events: BackingEvents) => Effect.Effect<BackingSocket, DatagramSocketError, R>,
|
|
538
|
+
services: Context.Context<R>,
|
|
539
|
+
capacity: number,
|
|
540
|
+
sliding: boolean,
|
|
541
|
+
onError: ((error: DatagramSocketError) => void) | undefined
|
|
542
|
+
): DatagramSocket => {
|
|
543
|
+
const free = Latch.makeUnsafe(true)
|
|
544
|
+
const latch = Latch.makeUnsafe(false)
|
|
545
|
+
let current: ReaderState | undefined
|
|
546
|
+
|
|
547
|
+
const abandon = (state: ReaderState, opening: Fiber.Fiber<BackingSocket, DatagramSocketError>) => {
|
|
548
|
+
state.close()
|
|
549
|
+
// Release reader ownership only after the interrupted open settles.
|
|
550
|
+
opening.addObserver((exit) => {
|
|
551
|
+
if (exit._tag === "Success") exit.value.close()
|
|
552
|
+
free.openUnsafe()
|
|
553
|
+
})
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
const release = (state: ReaderState) => {
|
|
557
|
+
state.close()
|
|
558
|
+
current = undefined
|
|
559
|
+
latch.closeUnsafe()
|
|
560
|
+
free.openUnsafe()
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
const reader: DatagramSocket["reader"] = Effect.uninterruptibleMask(
|
|
564
|
+
Effect.fnUntraced(function*(restore) {
|
|
565
|
+
while (!free.closeUnsafe()) yield* restore(free.await)
|
|
566
|
+
const scope = yield* Effect.scope
|
|
567
|
+
const state = new ReaderState(capacity, sliding, onError)
|
|
568
|
+
// Native opens may be uncancellable. Interrupt only the wait; `abandon`
|
|
569
|
+
// closes any socket returned later.
|
|
570
|
+
const opened = open(state.events).pipe(
|
|
571
|
+
Effect.updateContext((input: Context.Context<never>) =>
|
|
572
|
+
Context.add(Context.merge(services, input), Scope.Scope, scope)
|
|
573
|
+
)
|
|
574
|
+
) as Effect.Effect<BackingSocket, DatagramSocketError>
|
|
575
|
+
const opening = yield* Effect.forkDetach(opened, { startImmediately: true })
|
|
576
|
+
state.handle = yield* restore(Fiber.join(opening)).pipe(
|
|
577
|
+
Effect.onError(() => Effect.sync(() => abandon(state, opening)))
|
|
578
|
+
)
|
|
579
|
+
current = state
|
|
580
|
+
latch.openUnsafe()
|
|
581
|
+
yield* Effect.addFinalizer(() => Effect.sync(() => release(state)))
|
|
582
|
+
return makeReader(state)
|
|
583
|
+
})
|
|
584
|
+
)
|
|
585
|
+
|
|
586
|
+
const write = (datagram: OutgoingDatagram): Effect.Effect<void, DatagramSocketError> =>
|
|
587
|
+
withFiber((fiber) => {
|
|
588
|
+
const state = current
|
|
589
|
+
return state === undefined ? latch.whenOpen(write(datagram)) : state.write(datagram, fiber)
|
|
590
|
+
})
|
|
591
|
+
const writeAll = (datagrams: NonEmptyReadonlyArray<OutgoingDatagram>): Effect.Effect<void, DatagramSocketError> =>
|
|
592
|
+
withFiber((fiber) => {
|
|
593
|
+
const state = current
|
|
594
|
+
return state === undefined ? latch.whenOpen(writeAll(datagrams)) : state.writeAll(datagrams, fiber)
|
|
595
|
+
})
|
|
596
|
+
const writer: DatagramSocket["writer"] = Effect.succeed({ write, writeAll })
|
|
597
|
+
|
|
598
|
+
return make({ reader, writer })
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
const encoder = new TextEncoder()
|
|
602
|
+
const encode = (payload: Uint8Array | string) => typeof payload === "string" ? encoder.encode(payload) : payload
|
|
603
|
+
const emptyScopeIds: ReadonlyMap<string, number> = new Map()
|
|
604
|
+
|
|
605
|
+
const closedError = () => new DatagramSocketError({ reason: new DatagramSocketClosedError() })
|
|
606
|
+
|
|
607
|
+
// Rejected writes return this sentinel, avoiding per-packet error type checks.
|
|
608
|
+
const rejected: BackingAddress = { host: "", port: 0 }
|
|
609
|
+
|
|
610
|
+
class DatagramImpl implements Datagram, BackingAddress {
|
|
611
|
+
payload: Uint8Array
|
|
612
|
+
host: string
|
|
613
|
+
port: number
|
|
614
|
+
readonly owner: ReaderState
|
|
615
|
+
#address: NetAddress.InetAddress | undefined
|
|
616
|
+
constructor(payload: Uint8Array, host: string, port: number, owner: ReaderState) {
|
|
617
|
+
this.payload = payload
|
|
618
|
+
this.host = host
|
|
619
|
+
this.port = port
|
|
620
|
+
this.owner = owner
|
|
621
|
+
}
|
|
622
|
+
get address(): NetAddress.InetAddress {
|
|
623
|
+
return this.#address ??= NetAddress.inetAddressFromNativeUnsafe(this.host, this.port, this.owner.scopeIds)
|
|
624
|
+
}
|
|
625
|
+
// only for a queued record, which no pull has returned yet
|
|
626
|
+
reuse(payload: Uint8Array, host: string, port: number) {
|
|
627
|
+
this.payload = payload
|
|
628
|
+
this.host = host
|
|
629
|
+
this.port = port
|
|
630
|
+
this.#address = undefined
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
const targetOf = (datagram: OutgoingDatagram): NetAddress.InetAddress | DatagramImpl | undefined =>
|
|
635
|
+
"host" in datagram ? datagram as DatagramImpl : datagram.address as NetAddress.InetAddress | DatagramImpl | undefined
|
|
636
|
+
|
|
637
|
+
class ReaderState {
|
|
638
|
+
readonly capacity: number
|
|
639
|
+
readonly sliding: boolean
|
|
640
|
+
readonly listener: ((error: DatagramSocketError) => void) | undefined
|
|
641
|
+
readonly events: BackingEvents = {
|
|
642
|
+
onPacket: (payload, host, port) => this.push(payload, host, port),
|
|
643
|
+
onReadError: (error) => this.fail(error),
|
|
644
|
+
onError: (error) => this.report(error),
|
|
645
|
+
onClose: () => this.fail(closedError())
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
// set when `open` completes; packets can arrive before that
|
|
649
|
+
handle: BackingSocket | undefined = undefined
|
|
650
|
+
failure: Effect.Effect<never, DatagramSocketError> | undefined = undefined
|
|
651
|
+
// separate from `failure`: after a sticky read error the socket is still
|
|
652
|
+
// open, so ICMP reports still reach `onError`
|
|
653
|
+
closed = false
|
|
654
|
+
|
|
655
|
+
// Sliding overflow turns the full buffer into a ring, oldest at `head`.
|
|
656
|
+
buffer: Array<DatagramImpl> = []
|
|
657
|
+
head = 0
|
|
658
|
+
dropped = 0
|
|
659
|
+
|
|
660
|
+
// A single consumer uses the first waiter slot without touching the array.
|
|
661
|
+
waiter: FiberImpl | undefined = undefined
|
|
662
|
+
waiters: Array<FiberImpl> = []
|
|
663
|
+
readonly unpark: Primitive = unpark(this)
|
|
664
|
+
|
|
665
|
+
bound: NetAddress.InetAddress | undefined = undefined
|
|
666
|
+
// one-entry cache for `write` to the same explicit address
|
|
667
|
+
lastTarget: NetAddress.InetAddress | undefined = undefined
|
|
668
|
+
lastDestination: BackingAddress | undefined = undefined
|
|
669
|
+
rejection: DatagramSocketError | undefined = undefined
|
|
670
|
+
|
|
671
|
+
constructor(capacity: number, sliding: boolean, listener: ((error: DatagramSocketError) => void) | undefined) {
|
|
672
|
+
this.capacity = capacity
|
|
673
|
+
this.sliding = sliding
|
|
674
|
+
this.listener = listener
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
push(payload: Uint8Array, host: string, port: number) {
|
|
678
|
+
if (this.failure !== undefined) return
|
|
679
|
+
if (this.buffer.length >= this.capacity) return this.overflow(payload, host, port)
|
|
680
|
+
const datagram = new DatagramImpl(payload, host, port, this)
|
|
681
|
+
if (this.waiter !== undefined) return this.wake(datagram)
|
|
682
|
+
const buffer = this.buffer
|
|
683
|
+
buffer[buffer.length] = datagram
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
wake(datagram: DatagramImpl) {
|
|
687
|
+
const fiber = this.waiter!
|
|
688
|
+
this.promoteWaiter()
|
|
689
|
+
fiber.evaluate(exitSucceed([datagram]) as any)
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
overflow(payload: Uint8Array, host: string, port: number) {
|
|
693
|
+
this.dropped++
|
|
694
|
+
if (!this.sliding) return
|
|
695
|
+
const buffer = this.buffer
|
|
696
|
+
const head = this.head
|
|
697
|
+
buffer[head].reuse(payload, host, port)
|
|
698
|
+
this.head = head + 1 === buffer.length ? 0 : head + 1
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
take(): NonEmptyReadonlyArray<Datagram> {
|
|
702
|
+
const buffer = this.buffer
|
|
703
|
+
const head = this.head
|
|
704
|
+
this.buffer = []
|
|
705
|
+
if (head === 0) return buffer as unknown as NonEmptyReadonlyArray<Datagram>
|
|
706
|
+
this.head = 0
|
|
707
|
+
const length = buffer.length
|
|
708
|
+
const batch = new Array<DatagramImpl>(length)
|
|
709
|
+
let j = 0
|
|
710
|
+
for (let i = head; i < length; i++) batch[j++] = buffer[i]
|
|
711
|
+
for (let i = 0; i < head; i++) batch[j++] = buffer[i]
|
|
712
|
+
return batch as unknown as NonEmptyReadonlyArray<Datagram>
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
park(fiber: FiberImpl) {
|
|
716
|
+
if (this.waiter === undefined) this.waiter = fiber
|
|
717
|
+
else this.waiters.push(fiber)
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
promoteWaiter() {
|
|
721
|
+
this.waiter = this.waiters.length === 0 ? undefined : this.waiters.shift()
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
removeWaiter(fiber: FiberImpl) {
|
|
725
|
+
if (this.waiter === fiber) return this.promoteWaiter()
|
|
726
|
+
if (this.waiters.length === 0) return
|
|
727
|
+
const index = this.waiters.indexOf(fiber)
|
|
728
|
+
if (index !== -1) this.waiters.splice(index, 1)
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
failWaiters() {
|
|
732
|
+
const waiter = this.waiter
|
|
733
|
+
if (waiter === undefined) return
|
|
734
|
+
const failure = this.failure!
|
|
735
|
+
const waiters = this.waiters
|
|
736
|
+
this.waiter = undefined
|
|
737
|
+
this.waiters = []
|
|
738
|
+
waiter.evaluate(failure as any)
|
|
739
|
+
for (let i = 0; i < waiters.length; i++) waiters[i].evaluate(failure as any)
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
fail(error: DatagramSocketError) {
|
|
743
|
+
if (this.failure !== undefined) return
|
|
744
|
+
this.failure = Effect.fail(error)
|
|
745
|
+
this.failWaiters()
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
close() {
|
|
749
|
+
this.closed = true
|
|
750
|
+
this.buffer = []
|
|
751
|
+
this.head = 0
|
|
752
|
+
this.failure = Effect.fail(closedError())
|
|
753
|
+
this.failWaiters()
|
|
754
|
+
this.handle?.close()
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
report(error: DatagramSocketError) {
|
|
758
|
+
const listener = this.listener
|
|
759
|
+
if (listener === undefined || this.closed) return
|
|
760
|
+
try {
|
|
761
|
+
listener(error)
|
|
762
|
+
} catch {
|
|
763
|
+
// Listener errors cannot fail native callbacks.
|
|
764
|
+
}
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
// packets can arrive before the handle does, so they read this lazily
|
|
768
|
+
get scopeIds(): ReadonlyMap<string, number> {
|
|
769
|
+
return this.handle?.scopeIds ?? emptyScopeIds
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
get address(): NetAddress.InetAddress {
|
|
773
|
+
return this.bound ??= NetAddress.inetAddressFromNativeUnsafe(
|
|
774
|
+
this.handle!.address.host,
|
|
775
|
+
this.handle!.address.port,
|
|
776
|
+
this.scopeIds
|
|
777
|
+
)
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
// Synchronous sends avoid parking; async completions resume inline.
|
|
781
|
+
// Ignore completions after interruption.
|
|
782
|
+
write(datagram: OutgoingDatagram, fiber: FiberImpl): Effect.Effect<void, DatagramSocketError> {
|
|
783
|
+
if (this.failure !== undefined) return this.failure
|
|
784
|
+
const target = targetOf(datagram)
|
|
785
|
+
const destination = this.destination(target)
|
|
786
|
+
if (destination === rejected) return Effect.fail(this.rejection!)
|
|
787
|
+
const handle = this.handle!
|
|
788
|
+
const payload = encode(datagram.payload)
|
|
789
|
+
let result: Effect.Effect<void, DatagramSocketError> | undefined
|
|
790
|
+
let parked = false
|
|
791
|
+
handle.send(payload, destination, (error) => {
|
|
792
|
+
if (result !== undefined) return
|
|
793
|
+
result = error === undefined ? Effect.void : Effect.fail(this.withAddress(error, target))
|
|
794
|
+
if (parked) fiber.evaluate(result as any)
|
|
795
|
+
})
|
|
796
|
+
if (result !== undefined) return result
|
|
797
|
+
parked = true
|
|
798
|
+
return fiber.yieldWith(() => {
|
|
799
|
+
parked = false
|
|
800
|
+
}) as any
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
writeAll(
|
|
804
|
+
datagrams: NonEmptyReadonlyArray<OutgoingDatagram>,
|
|
805
|
+
fiber: FiberImpl
|
|
806
|
+
): Effect.Effect<void, DatagramSocketError> {
|
|
807
|
+
if (this.failure !== undefined) return this.failure
|
|
808
|
+
const payloads = new Array<Uint8Array>(datagrams.length)
|
|
809
|
+
const destinations = new Array<BackingAddress | undefined>(datagrams.length)
|
|
810
|
+
for (let i = 0; i < datagrams.length; i++) {
|
|
811
|
+
const datagram = datagrams[i]
|
|
812
|
+
const destination = this.destination(targetOf(datagram))
|
|
813
|
+
if (destination === rejected) return Effect.fail(this.rejection!)
|
|
814
|
+
payloads[i] = encode(datagram.payload)
|
|
815
|
+
destinations[i] = destination
|
|
816
|
+
}
|
|
817
|
+
let result: Effect.Effect<void, DatagramSocketError> | undefined
|
|
818
|
+
let parked = false
|
|
819
|
+
this.handle!.sendMany(payloads, destinations, (error, index) => {
|
|
820
|
+
if (result !== undefined) return
|
|
821
|
+
result = error === undefined
|
|
822
|
+
? Effect.void
|
|
823
|
+
: Effect.fail(index === undefined ? error : this.withAddress(error, targetOf(datagrams[index]!)))
|
|
824
|
+
if (parked) fiber.evaluate(result as any)
|
|
825
|
+
})
|
|
826
|
+
if (result !== undefined) return result
|
|
827
|
+
parked = true
|
|
828
|
+
return fiber.yieldWith(() => {
|
|
829
|
+
parked = false
|
|
830
|
+
}) as any
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
// Returns `rejected` on failure, with the error stored in `rejection`.
|
|
834
|
+
destination(target: NetAddress.InetAddress | DatagramImpl | undefined): BackingAddress | undefined {
|
|
835
|
+
const handle = this.handle!
|
|
836
|
+
if (target === undefined) {
|
|
837
|
+
if (handle.connected) return undefined
|
|
838
|
+
return handle.peer ?? this.reject("DatagramSocket write has no destination and the socket has no peer")
|
|
839
|
+
}
|
|
840
|
+
if (!("_tag" in target)) return handle.connected ? undefined : target as DatagramImpl
|
|
841
|
+
if (handle.connected) return this.reject("an explicit address cannot be used on a connected DatagramSocket", target)
|
|
842
|
+
if (target === this.lastTarget) return this.lastDestination
|
|
843
|
+
const destination = { host: NetAddress.formatNativeHost(target, this.scopeIds), port: target.port }
|
|
844
|
+
this.lastTarget = target
|
|
845
|
+
this.lastDestination = destination
|
|
846
|
+
return destination
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
reject(message: string, address?: NetAddress.InetAddress): BackingAddress {
|
|
850
|
+
this.rejection = new DatagramSocketError({
|
|
851
|
+
reason: new DatagramSocketWriteError({ kind: "Unknown", address, cause: new Error(message) })
|
|
852
|
+
})
|
|
853
|
+
return rejected
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
withAddress(
|
|
857
|
+
error: DatagramSocketError,
|
|
858
|
+
target: NetAddress.InetAddress | DatagramImpl | undefined
|
|
859
|
+
): DatagramSocketError {
|
|
860
|
+
const reason = error.reason
|
|
861
|
+
if (reason._tag !== "DatagramSocketWriteError" || reason.address !== undefined) return error
|
|
862
|
+
const address = this.addressOf(target)
|
|
863
|
+
if (address === undefined) return error
|
|
864
|
+
return new DatagramSocketError({
|
|
865
|
+
reason: new DatagramSocketWriteError({ kind: reason.kind, address, cause: reason.cause })
|
|
866
|
+
})
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
addressOf(target: NetAddress.InetAddress | DatagramImpl | undefined): NetAddress.InetAddress | undefined {
|
|
870
|
+
try {
|
|
871
|
+
if (target !== undefined) return "_tag" in target ? target : (target as DatagramImpl).address
|
|
872
|
+
const peer = this.handle!.peer
|
|
873
|
+
return peer === undefined
|
|
874
|
+
? undefined
|
|
875
|
+
: NetAddress.inetAddressFromNativeUnsafe(peer.host, peer.port, this.scopeIds)
|
|
876
|
+
} catch {
|
|
877
|
+
// an unknown IPv6 zone leaves the address out
|
|
878
|
+
return undefined
|
|
879
|
+
}
|
|
880
|
+
}
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
// Park the fiber itself and resume it inline, avoiding `Effect.callback` allocations.
|
|
884
|
+
const makePull = (state: ReaderState): Reader["pull"] =>
|
|
885
|
+
withFiber((fiber): any => {
|
|
886
|
+
if (state.buffer.length !== 0) {
|
|
887
|
+
const batch = state.take()
|
|
888
|
+
const cont = fiber.getCont(contA)
|
|
889
|
+
return cont ? cont[contA](batch, fiber) : fiber.yieldWith(exitSucceed(batch))
|
|
890
|
+
}
|
|
891
|
+
if (state.failure !== undefined) return state.failure
|
|
892
|
+
state.park(fiber)
|
|
893
|
+
fiber._stack.push(state.unpark)
|
|
894
|
+
return fiber.yieldWith(constVoid)
|
|
895
|
+
})
|
|
896
|
+
|
|
897
|
+
// `contAll` removes interrupted waiters too; after a wake, removal is a no-op.
|
|
898
|
+
const unpark: (state: ReaderState) => Primitive = makePrimitive({
|
|
899
|
+
op: "DatagramSocketUnpark",
|
|
900
|
+
[contAll](fiber) {
|
|
901
|
+
this[args].removeWaiter(fiber)
|
|
902
|
+
}
|
|
903
|
+
})
|
|
904
|
+
|
|
905
|
+
const makeReader = (state: ReaderState): Reader => ({
|
|
906
|
+
pull: makePull(state),
|
|
907
|
+
get address() {
|
|
908
|
+
return state.address
|
|
909
|
+
},
|
|
910
|
+
dropped: () => state.dropped,
|
|
911
|
+
joinMulticast: (options) =>
|
|
912
|
+
Effect.suspend(() => {
|
|
913
|
+
if (state.failure !== undefined) return state.failure
|
|
914
|
+
return Effect.acquireRelease(
|
|
915
|
+
state.handle!.joinMulticast(options),
|
|
916
|
+
(leave) => state.closed ? Effect.void : Effect.ignoreCause(leave())
|
|
917
|
+
)
|
|
918
|
+
})
|
|
919
|
+
})
|