effect 4.0.2 → 4.0.3

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.
Files changed (243) hide show
  1. package/dist/Context.js +3 -3
  2. package/dist/Context.js.map +1 -1
  3. package/dist/Crypto.d.ts +33 -5
  4. package/dist/Crypto.d.ts.map +1 -1
  5. package/dist/Crypto.js +8 -2
  6. package/dist/Crypto.js.map +1 -1
  7. package/dist/DateTime.d.ts +9 -6
  8. package/dist/DateTime.d.ts.map +1 -1
  9. package/dist/DateTime.js +9 -6
  10. package/dist/DateTime.js.map +1 -1
  11. package/dist/Duration.d.ts +6 -6
  12. package/dist/Duration.d.ts.map +1 -1
  13. package/dist/Duration.js +27 -8
  14. package/dist/Duration.js.map +1 -1
  15. package/dist/FileSystem.d.ts +44 -2
  16. package/dist/FileSystem.d.ts.map +1 -1
  17. package/dist/FileSystem.js.map +1 -1
  18. package/dist/Schema.d.ts +473 -1
  19. package/dist/Schema.d.ts.map +1 -1
  20. package/dist/Schema.js +355 -4
  21. package/dist/Schema.js.map +1 -1
  22. package/dist/Stdio.d.ts +7 -1
  23. package/dist/Stdio.d.ts.map +1 -1
  24. package/dist/Stdio.js +1 -0
  25. package/dist/Stdio.js.map +1 -1
  26. package/dist/Stream.d.ts +11 -11
  27. package/dist/Stream.js +5 -5
  28. package/dist/ai/McpServer.d.ts +6 -0
  29. package/dist/ai/McpServer.d.ts.map +1 -1
  30. package/dist/ai/McpServer.js +65 -23
  31. package/dist/ai/McpServer.js.map +1 -1
  32. package/dist/ai/internal/mcpProtocol/v2026_07_28.js +5 -0
  33. package/dist/ai/internal/mcpProtocol/v2026_07_28.js.map +1 -1
  34. package/dist/ai/internal/mcpProtocol.js.map +1 -1
  35. package/dist/ai/internal/mcpRuntime.js +1 -0
  36. package/dist/ai/internal/mcpRuntime.js.map +1 -1
  37. package/dist/cli/CliOutput.d.ts +27 -0
  38. package/dist/cli/CliOutput.d.ts.map +1 -1
  39. package/dist/cli/CliOutput.js +33 -10
  40. package/dist/cli/CliOutput.js.map +1 -1
  41. package/dist/cli/Command.d.ts.map +1 -1
  42. package/dist/cli/Command.js +6 -3
  43. package/dist/cli/Command.js.map +1 -1
  44. package/dist/cli/Completions.d.ts +3 -3
  45. package/dist/cli/Completions.d.ts.map +1 -1
  46. package/dist/cli/Completions.js +8 -5
  47. package/dist/cli/Completions.js.map +1 -1
  48. package/dist/cli/GlobalFlag.d.ts +5 -4
  49. package/dist/cli/GlobalFlag.d.ts.map +1 -1
  50. package/dist/cli/GlobalFlag.js +3 -3
  51. package/dist/cli/GlobalFlag.js.map +1 -1
  52. package/dist/cli/Prompt.d.ts +17 -1
  53. package/dist/cli/Prompt.d.ts.map +1 -1
  54. package/dist/cli/Prompt.js +119 -114
  55. package/dist/cli/Prompt.js.map +1 -1
  56. package/dist/cli/internal/ansi.d.ts +0 -1
  57. package/dist/cli/internal/ansi.d.ts.map +1 -1
  58. package/dist/cli/internal/ansi.js +20 -3
  59. package/dist/cli/internal/ansi.js.map +1 -1
  60. package/dist/cli/internal/completions/powershell.d.ts +2 -0
  61. package/dist/cli/internal/completions/powershell.d.ts.map +1 -0
  62. package/dist/cli/internal/completions/powershell.js +273 -0
  63. package/dist/cli/internal/completions/powershell.js.map +1 -0
  64. package/dist/cli/internal/completions/zsh.js +3 -2
  65. package/dist/cli/internal/completions/zsh.js.map +1 -1
  66. package/dist/cli/internal/wizard.d.ts +3 -3
  67. package/dist/cli/internal/wizard.d.ts.map +1 -1
  68. package/dist/cli/internal/wizard.js +21 -18
  69. package/dist/cli/internal/wizard.js.map +1 -1
  70. package/dist/cluster/ClusterSchema.d.ts +28 -0
  71. package/dist/cluster/ClusterSchema.d.ts.map +1 -1
  72. package/dist/cluster/ClusterSchema.js +30 -0
  73. package/dist/cluster/ClusterSchema.js.map +1 -1
  74. package/dist/cluster/ClusterWorkflowEngine.d.ts.map +1 -1
  75. package/dist/cluster/ClusterWorkflowEngine.js +10 -18
  76. package/dist/cluster/ClusterWorkflowEngine.js.map +1 -1
  77. package/dist/cluster/MessageStorage.d.ts +18 -3
  78. package/dist/cluster/MessageStorage.d.ts.map +1 -1
  79. package/dist/cluster/MessageStorage.js +31 -11
  80. package/dist/cluster/MessageStorage.js.map +1 -1
  81. package/dist/cluster/Runners.d.ts.map +1 -1
  82. package/dist/cluster/Runners.js +8 -2
  83. package/dist/cluster/Runners.js.map +1 -1
  84. package/dist/cluster/Sharding.d.ts.map +1 -1
  85. package/dist/cluster/Sharding.js +181 -62
  86. package/dist/cluster/Sharding.js.map +1 -1
  87. package/dist/cluster/SqlMessageStorage.d.ts +59 -19
  88. package/dist/cluster/SqlMessageStorage.d.ts.map +1 -1
  89. package/dist/cluster/SqlMessageStorage.js +84 -55
  90. package/dist/cluster/SqlMessageStorage.js.map +1 -1
  91. package/dist/cluster/SqlRunnerStorage.d.ts +68 -19
  92. package/dist/cluster/SqlRunnerStorage.d.ts.map +1 -1
  93. package/dist/cluster/SqlRunnerStorage.js +191 -113
  94. package/dist/cluster/SqlRunnerStorage.js.map +1 -1
  95. package/dist/cluster/internal/entityManager.js +104 -37
  96. package/dist/cluster/internal/entityManager.js.map +1 -1
  97. package/dist/http/FindMyWay/internal/router.js +3 -6
  98. package/dist/http/FindMyWay/internal/router.js.map +1 -1
  99. package/dist/http/HttpClient.d.ts +6 -0
  100. package/dist/http/HttpClient.d.ts.map +1 -1
  101. package/dist/http/HttpClient.js +39 -2
  102. package/dist/http/HttpClient.js.map +1 -1
  103. package/dist/http/HttpTraceContext.d.ts.map +1 -1
  104. package/dist/http/HttpTraceContext.js +4 -2
  105. package/dist/http/HttpTraceContext.js.map +1 -1
  106. package/dist/internal/effect.js +12 -7
  107. package/dist/internal/effect.js.map +1 -1
  108. package/dist/internal/version.js +1 -1
  109. package/dist/net/AddressResolver.d.ts +145 -0
  110. package/dist/net/AddressResolver.d.ts.map +1 -0
  111. package/dist/net/AddressResolver.js +114 -0
  112. package/dist/net/AddressResolver.js.map +1 -0
  113. package/dist/net/Dns.d.ts +596 -0
  114. package/dist/net/Dns.d.ts.map +1 -0
  115. package/dist/net/Dns.js +549 -0
  116. package/dist/net/Dns.js.map +1 -0
  117. package/dist/net/Host.d.ts +320 -0
  118. package/dist/net/Host.d.ts.map +1 -0
  119. package/dist/net/Host.js +342 -0
  120. package/dist/net/Host.js.map +1 -0
  121. package/dist/net/IpInterface.d.ts +76 -3
  122. package/dist/net/IpInterface.d.ts.map +1 -1
  123. package/dist/net/IpInterface.js +53 -4
  124. package/dist/net/IpInterface.js.map +1 -1
  125. package/dist/net/IpNetwork.d.ts +82 -8
  126. package/dist/net/IpNetwork.d.ts.map +1 -1
  127. package/dist/net/IpNetwork.js +60 -4
  128. package/dist/net/IpNetwork.js.map +1 -1
  129. package/dist/net/NetAddress.d.ts +388 -35
  130. package/dist/net/NetAddress.d.ts.map +1 -1
  131. package/dist/net/NetAddress.js +350 -34
  132. package/dist/net/NetAddress.js.map +1 -1
  133. package/dist/net/index.d.ts +15 -0
  134. package/dist/net/index.d.ts.map +1 -1
  135. package/dist/net/index.js +15 -0
  136. package/dist/net/index.js.map +1 -1
  137. package/dist/observability/OtlpMetrics.d.ts.map +1 -1
  138. package/dist/observability/OtlpMetrics.js +15 -21
  139. package/dist/observability/OtlpMetrics.js.map +1 -1
  140. package/dist/observability/PrometheusMetrics.js +1 -0
  141. package/dist/observability/PrometheusMetrics.js.map +1 -1
  142. package/dist/persistence/PersistedQueue.d.ts +32 -3
  143. package/dist/persistence/PersistedQueue.d.ts.map +1 -1
  144. package/dist/persistence/PersistedQueue.js +75 -16
  145. package/dist/persistence/PersistedQueue.js.map +1 -1
  146. package/dist/rpc/RpcClient.d.ts +22 -6
  147. package/dist/rpc/RpcClient.d.ts.map +1 -1
  148. package/dist/rpc/RpcClient.js +12 -5
  149. package/dist/rpc/RpcClient.js.map +1 -1
  150. package/dist/rpc/RpcServer.d.ts +6 -0
  151. package/dist/rpc/RpcServer.d.ts.map +1 -1
  152. package/dist/rpc/RpcServer.js +42 -20
  153. package/dist/rpc/RpcServer.js.map +1 -1
  154. package/dist/socket/DatagramSocket.d.ts +456 -0
  155. package/dist/socket/DatagramSocket.d.ts.map +1 -0
  156. package/dist/socket/DatagramSocket.js +535 -0
  157. package/dist/socket/DatagramSocket.js.map +1 -0
  158. package/dist/socket/Socket.d.ts +14 -1
  159. package/dist/socket/Socket.d.ts.map +1 -1
  160. package/dist/socket/Socket.js +9 -1
  161. package/dist/socket/Socket.js.map +1 -1
  162. package/dist/socket/index.d.ts +5 -0
  163. package/dist/socket/index.d.ts.map +1 -1
  164. package/dist/socket/index.js +5 -0
  165. package/dist/socket/index.js.map +1 -1
  166. package/dist/sql/Migrator.d.ts.map +1 -1
  167. package/dist/sql/Migrator.js +7 -2
  168. package/dist/sql/Migrator.js.map +1 -1
  169. package/dist/sql/SqlClient.d.ts +7 -1
  170. package/dist/sql/SqlClient.d.ts.map +1 -1
  171. package/dist/sql/SqlClient.js +31 -6
  172. package/dist/sql/SqlClient.js.map +1 -1
  173. package/dist/testing/TestCrypto.d.ts +73 -0
  174. package/dist/testing/TestCrypto.d.ts.map +1 -0
  175. package/dist/testing/TestCrypto.js +87 -0
  176. package/dist/testing/TestCrypto.js.map +1 -0
  177. package/dist/testing/index.d.ts +5 -0
  178. package/dist/testing/index.d.ts.map +1 -1
  179. package/dist/testing/index.js +5 -0
  180. package/dist/testing/index.js.map +1 -1
  181. package/dist/workflow/DurableDeferred.d.ts +54 -0
  182. package/dist/workflow/DurableDeferred.d.ts.map +1 -1
  183. package/dist/workflow/DurableDeferred.js +78 -49
  184. package/dist/workflow/DurableDeferred.js.map +1 -1
  185. package/dist/workflow/WorkflowEngine.d.ts.map +1 -1
  186. package/dist/workflow/WorkflowEngine.js +2 -1
  187. package/dist/workflow/WorkflowEngine.js.map +1 -1
  188. package/package.json +1 -1
  189. package/src/Context.ts +3 -3
  190. package/src/Crypto.ts +43 -9
  191. package/src/DateTime.ts +9 -6
  192. package/src/Duration.ts +36 -16
  193. package/src/FileSystem.ts +48 -2
  194. package/src/Schema.ts +669 -5
  195. package/src/Stdio.ts +9 -2
  196. package/src/Stream.ts +11 -11
  197. package/src/ai/McpServer.ts +63 -23
  198. package/src/ai/internal/mcpProtocol/v2026_07_28.ts +5 -0
  199. package/src/ai/internal/mcpProtocol.ts +3 -0
  200. package/src/ai/internal/mcpRuntime.ts +2 -0
  201. package/src/cli/CliOutput.ts +33 -16
  202. package/src/cli/Command.ts +4 -3
  203. package/src/cli/Completions.ts +9 -6
  204. package/src/cli/GlobalFlag.ts +19 -11
  205. package/src/cli/Prompt.ts +159 -149
  206. package/src/cli/internal/ansi.ts +26 -3
  207. package/src/cli/internal/completions/powershell.ts +322 -0
  208. package/src/cli/internal/completions/zsh.ts +4 -2
  209. package/src/cli/internal/wizard.ts +34 -26
  210. package/src/cluster/ClusterSchema.ts +32 -0
  211. package/src/cluster/ClusterWorkflowEngine.ts +13 -24
  212. package/src/cluster/MessageStorage.ts +45 -14
  213. package/src/cluster/Runners.ts +8 -2
  214. package/src/cluster/Sharding.ts +207 -49
  215. package/src/cluster/SqlMessageStorage.ts +118 -84
  216. package/src/cluster/SqlRunnerStorage.ts +224 -128
  217. package/src/cluster/internal/entityManager.ts +151 -55
  218. package/src/http/FindMyWay/internal/router.ts +4 -7
  219. package/src/http/HttpClient.ts +56 -2
  220. package/src/http/HttpTraceContext.ts +8 -2
  221. package/src/internal/effect.ts +15 -13
  222. package/src/internal/version.ts +1 -1
  223. package/src/net/AddressResolver.ts +222 -0
  224. package/src/net/Dns.ts +1035 -0
  225. package/src/net/Host.ts +472 -0
  226. package/src/net/IpInterface.ts +117 -7
  227. package/src/net/IpNetwork.ts +116 -17
  228. package/src/net/NetAddress.ts +611 -76
  229. package/src/net/index.ts +18 -0
  230. package/src/observability/OtlpMetrics.ts +12 -23
  231. package/src/observability/PrometheusMetrics.ts +1 -0
  232. package/src/persistence/PersistedQueue.ts +146 -53
  233. package/src/rpc/RpcClient.ts +41 -19
  234. package/src/rpc/RpcServer.ts +60 -25
  235. package/src/socket/DatagramSocket.ts +919 -0
  236. package/src/socket/Socket.ts +22 -2
  237. package/src/socket/index.ts +6 -0
  238. package/src/sql/Migrator.ts +21 -5
  239. package/src/sql/SqlClient.ts +39 -15
  240. package/src/testing/TestCrypto.ts +94 -0
  241. package/src/testing/index.ts +6 -0
  242. package/src/workflow/DurableDeferred.ts +123 -48
  243. package/src/workflow/WorkflowEngine.ts +2 -1
package/src/net/Dns.ts ADDED
@@ -0,0 +1,1035 @@
1
+ /**
2
+ * Host name resolution, DNS queries, and DNS record values.
3
+ *
4
+ * `lookup` resolves a host name to the addresses used to connect to it,
5
+ * `resolve` queries DNS records of one type, and `reverse` looks up the names
6
+ * of an address. Runtime packages provide the `Dns` service from the host
7
+ * platform's resolver, whose `lookup` uses the operating system resolver
8
+ * (`getaddrinfo`) and therefore also reads the hosts file and other system
9
+ * sources, not only DNS. `AddressResolver` builds on `lookup` to resolve
10
+ * `host:port` endpoints.
11
+ *
12
+ * @stability experimental
13
+ * @since 4.0.0
14
+ */
15
+ import * as Arr from "../Array.ts"
16
+ import * as Context from "../Context.ts"
17
+ import * as Data from "../Data.ts"
18
+ import * as Duration from "../Duration.ts"
19
+ import * as Effect from "../Effect.ts"
20
+ import * as Hex from "../encoding/Hex.ts"
21
+ import * as Equal from "../Equal.ts"
22
+ import * as Hash from "../Hash.ts"
23
+ import * as Inspectable from "../Inspectable.ts"
24
+ import * as Layer from "../Layer.ts"
25
+ import { hasProperty } from "../Predicate.ts"
26
+ import type { ReadonlyRecord } from "../Record.ts"
27
+ import * as Result from "../Result.ts"
28
+ import * as Host from "./Host.ts"
29
+ import * as NetAddress from "./NetAddress.ts"
30
+
31
+ const RecordTypeId = "~effect/net/Dns/DnsRecord" as const
32
+
33
+ // =============================================================================
34
+ // Records
35
+ // =============================================================================
36
+
37
+ interface RecordProto<Tag extends RecordType> extends Equal.Equal, Hash.Hash, Inspectable.Inspectable {
38
+ readonly _tag: Tag
39
+ readonly [RecordTypeId]: typeof RecordTypeId
40
+ }
41
+
42
+ /**
43
+ * An IPv4 address record.
44
+ *
45
+ * @stability experimental
46
+ * @category models
47
+ * @since 4.0.0
48
+ */
49
+ export interface A extends RecordProto<"A"> {
50
+ readonly address: NetAddress.Ipv4Address
51
+ }
52
+
53
+ /**
54
+ * An IPv6 address record.
55
+ *
56
+ * @stability experimental
57
+ * @category models
58
+ * @since 4.0.0
59
+ */
60
+ export interface Aaaa extends RecordProto<"AAAA"> {
61
+ readonly address: NetAddress.Ipv6Address
62
+ }
63
+
64
+ /**
65
+ * A certification authority authorization record.
66
+ *
67
+ * @stability experimental
68
+ * @category models
69
+ * @since 4.0.0
70
+ */
71
+ export interface Caa extends RecordProto<"CAA"> {
72
+ readonly critical: boolean
73
+ readonly tag: string
74
+ readonly value: string
75
+ }
76
+
77
+ /**
78
+ * A canonical name (alias) record.
79
+ *
80
+ * @stability experimental
81
+ * @category models
82
+ * @since 4.0.0
83
+ */
84
+ export interface Cname extends RecordProto<"CNAME"> {
85
+ readonly target: Host.DomainName
86
+ }
87
+
88
+ /**
89
+ * A mail exchange record.
90
+ *
91
+ * @stability experimental
92
+ * @category models
93
+ * @since 4.0.0
94
+ */
95
+ export interface Mx extends RecordProto<"MX"> {
96
+ readonly exchange: Host.DomainName
97
+ readonly priority: number
98
+ }
99
+
100
+ /**
101
+ * A naming authority pointer record.
102
+ *
103
+ * @stability experimental
104
+ * @category models
105
+ * @since 4.0.0
106
+ */
107
+ export interface Naptr extends RecordProto<"NAPTR"> {
108
+ readonly order: number
109
+ readonly preference: number
110
+ readonly flags: string
111
+ readonly service: string
112
+ readonly regexp: string
113
+ readonly replacement: Host.DomainName
114
+ }
115
+
116
+ /**
117
+ * A name server record.
118
+ *
119
+ * @stability experimental
120
+ * @category models
121
+ * @since 4.0.0
122
+ */
123
+ export interface Ns extends RecordProto<"NS"> {
124
+ readonly host: Host.DomainName
125
+ }
126
+
127
+ /**
128
+ * A pointer record, used for reverse lookups and DNS service discovery.
129
+ *
130
+ * **Details**
131
+ *
132
+ * `host` is a fully qualified name. It is a host name for reverse lookups,
133
+ * but may be a DNS-SD service instance name containing spaces or other
134
+ * characters, such as `EPSON TM-m30III._ipp._tcp.local.`. Labels are UTF-8
135
+ * text, with dots and backslashes inside a label escaped as `\.` and `\\`.
136
+ *
137
+ * @stability experimental
138
+ * @category models
139
+ * @since 4.0.0
140
+ */
141
+ export interface Ptr extends RecordProto<"PTR"> {
142
+ readonly host: string
143
+ }
144
+
145
+ /**
146
+ * A start of authority record.
147
+ *
148
+ * **Details**
149
+ *
150
+ * `admin` is the administrator mailbox written as a domain name, such as
151
+ * `hostmaster.example.com`. A dot inside the mailbox's local part is escaped,
152
+ * as in `john\.doe.example.com`, so `admin` is a plain string rather than a
153
+ * `Host.DomainName`. Labels are UTF-8 text, with dots and backslashes inside
154
+ * a label escaped as `\.` and `\\`.
155
+ *
156
+ * @stability experimental
157
+ * @category models
158
+ * @since 4.0.0
159
+ */
160
+ export interface Soa extends RecordProto<"SOA"> {
161
+ readonly primary: Host.DomainName
162
+ readonly admin: string
163
+ readonly serial: number
164
+ readonly refresh: Duration.Duration
165
+ readonly retry: Duration.Duration
166
+ readonly expire: Duration.Duration
167
+ readonly minimum: Duration.Duration
168
+ }
169
+
170
+ /**
171
+ * A service location record.
172
+ *
173
+ * @stability experimental
174
+ * @category models
175
+ * @since 4.0.0
176
+ */
177
+ export interface Srv extends RecordProto<"SRV"> {
178
+ readonly target: Host.DomainName
179
+ readonly port: number
180
+ readonly priority: number
181
+ readonly weight: number
182
+ }
183
+
184
+ /**
185
+ * A TLS authentication record (RFC 6698), which pins the certificate or public
186
+ * key that a TLS service presents, for DNS-Based Authentication of Named
187
+ * Entities (DANE). Records are published at a name made of the service's port
188
+ * and protocol, such as `_443._tcp.example.com`.
189
+ *
190
+ * **Details**
191
+ *
192
+ * `certUsage` says how the record constrains the certificate chain (0 to 3:
193
+ * PKIX-TA, PKIX-EE, DANE-TA, DANE-EE), `selector` whether `data` matches the
194
+ * full certificate (0) or its public key (1), and `matchingType` whether
195
+ * `data` holds the selected content itself (0) or its SHA-256 (1) or SHA-512
196
+ * (2) digest. Unassigned values are kept as they are.
197
+ *
198
+ * **Gotchas**
199
+ *
200
+ * `data` is a copy of the bytes the record was made from, but typed arrays
201
+ * cannot be frozen; treat it as read-only, because changing it changes the
202
+ * record's equality.
203
+ *
204
+ * @stability experimental
205
+ * @category models
206
+ * @since 4.0.0
207
+ */
208
+ export interface Tlsa extends RecordProto<"TLSA"> {
209
+ readonly certUsage: number
210
+ readonly selector: number
211
+ readonly matchingType: number
212
+ readonly data: Uint8Array
213
+ }
214
+
215
+ /**
216
+ * A text record. DNS stores text as one or more character strings, which are
217
+ * kept as separate chunks; protocols such as SPF read them joined, for example
218
+ * with `record.chunks.join("")`.
219
+ *
220
+ * **Details**
221
+ *
222
+ * Platform services decode each character string as UTF-8, replacing bytes
223
+ * that are not valid UTF-8 with U+FFFD, and `formatRecord` writes chunks back
224
+ * as UTF-8 bytes.
225
+ *
226
+ * @stability experimental
227
+ * @category models
228
+ * @since 4.0.0
229
+ */
230
+ export interface Txt extends RecordProto<"TXT"> {
231
+ readonly chunks: Arr.NonEmptyReadonlyArray<string>
232
+ }
233
+
234
+ /**
235
+ * Any supported DNS record value.
236
+ *
237
+ * **Details**
238
+ *
239
+ * Records are immutable values tagged by record type, compared and hashed by
240
+ * their fields. They hold the record data only: owner names and TTLs are not
241
+ * included because platform resolvers do not report them consistently. Names
242
+ * returned by platform services are fully qualified and end with a dot, so
243
+ * passing them back to the resolver does not apply search domains.
244
+ *
245
+ * @stability experimental
246
+ * @category models
247
+ * @since 4.0.0
248
+ */
249
+ export type DnsRecord = A | Aaaa | Caa | Cname | Mx | Naptr | Ns | Ptr | Soa | Srv | Tlsa | Txt
250
+
251
+ /**
252
+ * The type of a DNS record, such as `"A"` or `"SRV"`.
253
+ *
254
+ * @stability experimental
255
+ * @category models
256
+ * @since 4.0.0
257
+ */
258
+ export type RecordType =
259
+ | "A"
260
+ | "AAAA"
261
+ | "CAA"
262
+ | "CNAME"
263
+ | "MX"
264
+ | "NAPTR"
265
+ | "NS"
266
+ | "PTR"
267
+ | "SOA"
268
+ | "SRV"
269
+ | "TLSA"
270
+ | "TXT"
271
+
272
+ /**
273
+ * The record value for a record type.
274
+ *
275
+ * @stability experimental
276
+ * @category models
277
+ * @since 4.0.0
278
+ */
279
+ export type RecordFor<T extends RecordType> = Extract<DnsRecord, { readonly _tag: T }>
280
+
281
+ /**
282
+ * The fields of a record type, without its `_tag`.
283
+ *
284
+ * @see {@link makeRecord}
285
+ * @stability experimental
286
+ * @category models
287
+ * @since 4.0.0
288
+ */
289
+ export type RecordFields<T extends RecordType> = T extends RecordType ? Omit<RecordFor<T>, keyof RecordProto<T>>
290
+ : never
291
+
292
+ const isUint8 = (u: unknown): boolean => Number.isInteger(u) && (u as number) >= 0 && (u as number) <= 0xff
293
+ const isUint16 = (u: unknown): boolean => Number.isInteger(u) && (u as number) >= 0 && (u as number) <= 0xffff
294
+ const isUint32 = (u: unknown): boolean => Number.isInteger(u) && (u as number) >= 0 && (u as number) <= 0xffffffff
295
+ const isString = (u: unknown): u is string => typeof u === "string"
296
+ // SOA timers are 32-bit unsigned counts of seconds on the wire.
297
+ const isTimer = (u: unknown): boolean =>
298
+ Duration.isDuration(u) && Duration.isFinite(u) && isUint32(Duration.toSeconds(u))
299
+
300
+ const recordFields: {
301
+ readonly [K in RecordType]: { readonly [F in keyof RecordFields<K>]-?: (u: unknown) => boolean }
302
+ } = {
303
+ A: {
304
+ address: NetAddress.isIpv4Address
305
+ },
306
+ AAAA: {
307
+ address: NetAddress.isIpv6Address
308
+ },
309
+ CAA: {
310
+ critical: (u) => typeof u === "boolean",
311
+ tag: (u) => isString(u) && /^[a-z0-9]+$/i.test(u),
312
+ value: isString
313
+ },
314
+ CNAME: {
315
+ target: Host.isDomainName
316
+ },
317
+ MX: {
318
+ exchange: Host.isDomainName,
319
+ priority: isUint16
320
+ },
321
+ NAPTR: {
322
+ order: isUint16,
323
+ preference: isUint16,
324
+ flags: isString,
325
+ service: isString,
326
+ regexp: isString,
327
+ replacement: Host.isDomainName
328
+ },
329
+ NS: {
330
+ host: Host.isDomainName
331
+ },
332
+ PTR: {
333
+ host: (u) => isString(u) && u.length > 0
334
+ },
335
+ SOA: {
336
+ primary: Host.isDomainName,
337
+ admin: (u) => isString(u) && u.length > 0,
338
+ serial: isUint32,
339
+ refresh: isTimer,
340
+ retry: isTimer,
341
+ expire: isTimer,
342
+ minimum: isTimer
343
+ },
344
+ SRV: {
345
+ target: Host.isDomainName,
346
+ port: isUint16,
347
+ priority: isUint16,
348
+ weight: isUint16
349
+ },
350
+ TLSA: {
351
+ certUsage: isUint8,
352
+ selector: isUint8,
353
+ matchingType: isUint8,
354
+ data: (u) => u instanceof Uint8Array && u.length > 0
355
+ },
356
+ TXT: {
357
+ chunks: (u) => Array.isArray(u) && u.length > 0 && u.every(isString)
358
+ }
359
+ }
360
+
361
+ /**
362
+ * Every supported record type.
363
+ *
364
+ * @stability experimental
365
+ * @category constants
366
+ * @since 4.0.0
367
+ */
368
+ export const recordTypes: Arr.NonEmptyReadonlyArray<RecordType> = Object.keys(recordFields) as Array<any> as any
369
+
370
+ /**
371
+ * Returns `true` when a value is a supported record type.
372
+ *
373
+ * @stability experimental
374
+ * @category guards
375
+ * @since 4.0.0
376
+ */
377
+ export const isRecordType = (u: unknown): u is RecordType => recordTypes.includes(u as RecordType)
378
+
379
+ /**
380
+ * Returns `true` when a value is a DNS record.
381
+ *
382
+ * @stability experimental
383
+ * @category guards
384
+ * @since 4.0.0
385
+ */
386
+ export const isDnsRecord = (u: unknown): u is DnsRecord => hasProperty(u, RecordTypeId)
387
+
388
+ const RecordPrototype = {
389
+ ...Inspectable.BaseProto,
390
+ [RecordTypeId]: RecordTypeId,
391
+ [Equal.symbol](this: DnsRecord, that: Equal.Equal): boolean {
392
+ return isDnsRecord(that) && that._tag === this._tag &&
393
+ Object.keys(this).every((key) => Equal.equals((this as any)[key], (that as any)[key]))
394
+ },
395
+ [Hash.symbol](this: DnsRecord): number {
396
+ return Hash.structureKeys(this, Object.keys(this))
397
+ },
398
+ toString(this: DnsRecord): string {
399
+ return formatRecord(this)
400
+ },
401
+ toJSON(this: DnsRecord): unknown {
402
+ return { ...this }
403
+ }
404
+ }
405
+
406
+ /**
407
+ * Creates a checked DNS record of a record type from its fields.
408
+ *
409
+ * **Details**
410
+ *
411
+ * Every field is checked at runtime: addresses must belong to the record's
412
+ * address family, names must be normalized domain names, priorities, weights,
413
+ * and ports must be 16-bit unsigned integers, the SOA serial and timers must be
414
+ * 32-bit unsigned integers (the timers in whole seconds), the TLSA fields must
415
+ * be 8-bit unsigned integers with non-empty `data`, which is copied, TXT
416
+ * records need at least one chunk, and CAA property tags must be alphanumeric.
417
+ * Fields that do not belong to the record type are ignored.
418
+ *
419
+ * **Example** (Creating a service record)
420
+ *
421
+ * ```ts import.meta.vitest
422
+ * import { Result } from "effect"
423
+ * import { Dns, Host } from "effect/net"
424
+ *
425
+ * const record = Result.getOrThrow(Dns.makeRecord("SRV", {
426
+ * target: Host.domainNameFromStringUnsafe("db.internal"),
427
+ * port: 5432,
428
+ * priority: 10,
429
+ * weight: 5
430
+ * }))
431
+ * Dns.formatRecord(record) // => "SRV 10 5 5432 db.internal"
432
+ * ```
433
+ *
434
+ * @stability experimental
435
+ * @category constructors
436
+ * @since 4.0.0
437
+ */
438
+ export const makeRecord = <T extends RecordType>(
439
+ type: T,
440
+ fields: RecordFields<T>
441
+ ): Result.Result<RecordFor<T>, NetAddress.NetAddressError> => {
442
+ if (!isRecordType(type)) {
443
+ return Result.fail(new NetAddress.NetAddressError({ input: type, message: "unknown DNS record type" }))
444
+ }
445
+ const input: Record<string, unknown> = typeof fields === "object" && fields !== null ? fields : {}
446
+ const self = Object.create(RecordPrototype)
447
+ self._tag = type
448
+
449
+ for (const [key, check] of Object.entries<(u: unknown) => boolean>(recordFields[type])) {
450
+ const value = input[key]
451
+ if (!check(value)) {
452
+ return Result.fail(new NetAddress.NetAddressError({ input: fields, message: `invalid ${type} record ${key}` }))
453
+ }
454
+ self[key] = Array.isArray(value)
455
+ ? Object.freeze([...value])
456
+ : value instanceof Uint8Array
457
+ ? new Uint8Array(value)
458
+ : value
459
+ }
460
+
461
+ return Result.succeed(Object.freeze(self))
462
+ }
463
+
464
+ /**
465
+ * Creates a trusted DNS record of a record type from its fields, throwing on
466
+ * failure.
467
+ *
468
+ * @stability experimental
469
+ * @category unsafe
470
+ * @since 4.0.0
471
+ */
472
+ export const makeRecordUnsafe = <T extends RecordType>(type: T, fields: RecordFields<T>): RecordFor<T> =>
473
+ Result.getOrThrow(makeRecord(type, fields))
474
+
475
+ const encoder = new TextEncoder()
476
+
477
+ const decimalEscape = (byte: number): string => `\\${byte.toString().padStart(3, "0")}`
478
+
479
+ // Quotes a character string in DNS presentation format (RFC 1035, section 5.1):
480
+ // `"` and `\` are escaped, and bytes outside printable ASCII are written as `\DDD`.
481
+ const quote = (value: string): string => {
482
+ let out = "\""
483
+ for (const byte of encoder.encode(value)) {
484
+ out += byte === 0x22 || byte === 0x5c
485
+ ? `\\${String.fromCharCode(byte)}`
486
+ : byte < 0x20 || byte > 0x7e
487
+ ? decimalEscape(byte)
488
+ : String.fromCharCode(byte)
489
+ }
490
+ return out + "\""
491
+ }
492
+
493
+ const nameSpecials = [0x22, 0x24, 0x28, 0x29, 0x3b, 0x40]
494
+
495
+ // Writes a name held as text, such as `Ptr.host`, in DNS presentation format:
496
+ // its `\.` and `\\` escapes are kept, other special characters are escaped, and
497
+ // spaces and bytes outside printable ASCII are written as `\DDD`.
498
+ const formatName = (text: string): string => {
499
+ const bytes = encoder.encode(text)
500
+ let out = ""
501
+ for (let i = 0; i < bytes.length; i++) {
502
+ const byte = bytes[i]
503
+ if (byte === 0x5c) {
504
+ const next = bytes[i + 1]
505
+ if (next === 0x2e || next === 0x5c) {
506
+ out += `\\${String.fromCharCode(next)}`
507
+ i++
508
+ } else {
509
+ out += "\\\\"
510
+ }
511
+ } else if (byte <= 0x20 || byte > 0x7e) {
512
+ out += decimalEscape(byte)
513
+ } else {
514
+ out += nameSpecials.includes(byte) ? `\\${String.fromCharCode(byte)}` : String.fromCharCode(byte)
515
+ }
516
+ }
517
+ return out
518
+ }
519
+
520
+ /**
521
+ * Formats a record in DNS presentation format, prefixed by its type.
522
+ *
523
+ * @stability experimental
524
+ * @category encoding
525
+ * @since 4.0.0
526
+ */
527
+ export const formatRecord = (self: DnsRecord): string => {
528
+ switch (self._tag) {
529
+ case "A":
530
+ case "AAAA":
531
+ return `${self._tag} ${NetAddress.formatIp(self.address)}`
532
+ case "CAA":
533
+ return `CAA ${self.critical ? 128 : 0} ${self.tag} ${quote(self.value)}`
534
+ case "CNAME":
535
+ return `CNAME ${self.target}`
536
+ case "MX":
537
+ return `MX ${self.priority} ${self.exchange}`
538
+ case "NAPTR":
539
+ return `NAPTR ${self.order} ${self.preference} ${quote(self.flags)} ${quote(self.service)} ${
540
+ quote(self.regexp)
541
+ } ${self.replacement}`
542
+ case "NS":
543
+ return `NS ${self.host}`
544
+ case "PTR":
545
+ return `PTR ${formatName(self.host)}`
546
+ case "SOA":
547
+ return `SOA ${self.primary} ${formatName(self.admin)} ${self.serial} ${Duration.toSeconds(self.refresh)} ${
548
+ Duration.toSeconds(self.retry)
549
+ } ${Duration.toSeconds(self.expire)} ${Duration.toSeconds(self.minimum)}`
550
+ case "SRV":
551
+ return `SRV ${self.priority} ${self.weight} ${self.port} ${self.target}`
552
+ case "TLSA":
553
+ return `TLSA ${self.certUsage} ${self.selector} ${self.matchingType} ${Hex.encode(self.data).toUpperCase()}`
554
+ case "TXT":
555
+ return `TXT ${self.chunks.map(quote).join(" ")}`
556
+ }
557
+ }
558
+
559
+ /**
560
+ * Returns the reverse-lookup domain name for an IP address, in `in-addr.arpa`
561
+ * for IPv4 or `ip6.arpa` for IPv6.
562
+ *
563
+ * **Example** (Building reverse-lookup names)
564
+ *
565
+ * ```ts import.meta.vitest
566
+ * import { Dns, NetAddress } from "effect/net"
567
+ *
568
+ * Dns.reverseName(NetAddress.ipFromStringUnsafe("192.0.2.1")) // => "1.2.0.192.in-addr.arpa"
569
+ * ```
570
+ *
571
+ * @stability experimental
572
+ * @category converting
573
+ * @since 4.0.0
574
+ */
575
+ export const reverseName = (address: NetAddress.IpAddress): Host.DomainName => {
576
+ if (NetAddress.isIpv4Address(address)) {
577
+ return `${[...NetAddress.ipv4ToOctets(address)].reverse().join(".")}.in-addr.arpa` as Host.DomainName
578
+ }
579
+ const nibbles: Array<string> = []
580
+ for (const octet of NetAddress.ipv6ToOctets(address)) {
581
+ nibbles.push((octet >> 4).toString(16), (octet & 0xf).toString(16))
582
+ }
583
+ return `${nibbles.reverse().join(".")}.ip6.arpa` as Host.DomainName
584
+ }
585
+
586
+ // =============================================================================
587
+ // Name servers
588
+ // =============================================================================
589
+
590
+ /**
591
+ * Parses the address of a name server: an IP address, which uses port 53, or
592
+ * an IP address and port.
593
+ *
594
+ * **Details**
595
+ *
596
+ * IPv6 addresses with a port are written in brackets, and IPv6 addresses
597
+ * without one are not.
598
+ *
599
+ * **Example** (Parsing name server addresses)
600
+ *
601
+ * ```ts import.meta.vitest
602
+ * import { Result } from "effect"
603
+ * import { Dns, NetAddress } from "effect/net"
604
+ *
605
+ * const format = (input: string) => Result.map(Dns.nameServerFromString(input), NetAddress.formatInet)
606
+ *
607
+ * format("192.0.2.53") // => Result.succeed("192.0.2.53:53")
608
+ * format("2001:db8::53") // => Result.succeed("[2001:db8::53]:53")
609
+ * format("[2001:db8::53]:5353") // => Result.succeed("[2001:db8::53]:5353")
610
+ * Result.isFailure(format("ns.example")) // => true
611
+ * ```
612
+ *
613
+ * @stability experimental
614
+ * @category decoding
615
+ * @since 4.0.0
616
+ */
617
+ export const nameServerFromString = (
618
+ input: string
619
+ ): Result.Result<NetAddress.InetAddress, NetAddress.NetAddressError> =>
620
+ NetAddress.inetAddressFromString(
621
+ input.startsWith("[") || /^[^:]*:\d+$/.test(input) ? input : input.includes(":") ? `[${input}]:53` : `${input}:53`
622
+ )
623
+
624
+ /**
625
+ * Converts a name server to the internet address to query. Strings are parsed
626
+ * like `nameServerFromString`, internet addresses and address parts with a
627
+ * port are converted like `NetAddress.inetAddressFromInput`, and other IP
628
+ * address inputs are converted like `NetAddress.ipFromInput` and use port 53.
629
+ *
630
+ * @stability experimental
631
+ * @category constructors
632
+ * @since 4.0.0
633
+ */
634
+ export const nameServerFromInput = (
635
+ input: NetAddress.IpAddressInput | NetAddress.InetAddressInput
636
+ ): Result.Result<NetAddress.InetAddress, NetAddress.NetAddressError> =>
637
+ typeof input === "string"
638
+ ? nameServerFromString(input)
639
+ : "port" in input
640
+ ? NetAddress.inetAddressFromInput(input)
641
+ : Result.map(NetAddress.ipFromInput(input), (address) => NetAddress.inetAddressUnsafe(address, 53))
642
+
643
+ // =============================================================================
644
+ // Errors
645
+ // =============================================================================
646
+
647
+ /**
648
+ * The normalized reason for a failed name resolution.
649
+ *
650
+ * **Details**
651
+ *
652
+ * - `NotFound`: no records were found, either because the name does not exist
653
+ * or because it has no records of the requested type or address family.
654
+ * Like Go's `DNSError.IsNotFound`, the two cases are not distinguished
655
+ * because not every platform resolver can tell them apart.
656
+ * - `Timeout`, `Temporary`, and `ServerFailure`: transient failures that may
657
+ * succeed when retried.
658
+ * - `Refused`: the server refused the query or could not be reached.
659
+ * - `BadName`: the name was rejected as malformed.
660
+ * - `InvalidResponse`: the platform returned data that could not be converted
661
+ * into record values.
662
+ * - `Unsupported`: the platform does not support the operation or record type.
663
+ *
664
+ * @stability experimental
665
+ * @category errors
666
+ * @since 4.0.0
667
+ */
668
+ export type DnsErrorReason =
669
+ | "NotFound"
670
+ | "Timeout"
671
+ | "Temporary"
672
+ | "Refused"
673
+ | "ServerFailure"
674
+ | "BadName"
675
+ | "InvalidResponse"
676
+ | "Unsupported"
677
+ | "Unknown"
678
+
679
+ /**
680
+ * A failed lookup, query, or reverse lookup.
681
+ *
682
+ * @stability experimental
683
+ * @category errors
684
+ * @since 4.0.0
685
+ */
686
+ export class DnsError extends Data.TaggedError("DnsError")<{
687
+ readonly reason: DnsErrorReason
688
+ readonly method: "lookup" | "resolve" | "reverse"
689
+ readonly hostname: string
690
+ readonly recordType?: RecordType | undefined
691
+ readonly cause?: unknown
692
+ }> {
693
+ /**
694
+ * Formats the reason, operation, and name of the failed resolution.
695
+ *
696
+ * @since 4.0.0
697
+ */
698
+ override get message(): string {
699
+ return `${this.reason}: Dns.${this.method} (${this.hostname}${
700
+ this.recordType === undefined ? "" : ` ${this.recordType}`
701
+ })`
702
+ }
703
+
704
+ /**
705
+ * Whether the failure is transient and the operation may succeed if retried.
706
+ *
707
+ * @since 4.0.0
708
+ */
709
+ get isTemporary(): boolean {
710
+ return this.reason === "Timeout" || this.reason === "Temporary" || this.reason === "ServerFailure"
711
+ }
712
+ }
713
+
714
+ // =============================================================================
715
+ // Service
716
+ // =============================================================================
717
+
718
+ /**
719
+ * Options for address lookups. Without a `family`, addresses of both families
720
+ * are returned.
721
+ *
722
+ * @stability experimental
723
+ * @category models
724
+ * @since 4.0.0
725
+ */
726
+ export interface LookupOptions {
727
+ readonly family?: NetAddress.IpFamily | undefined
728
+ }
729
+
730
+ /**
731
+ * Service that resolves names to addresses, queries DNS records, and looks up
732
+ * the names of addresses.
733
+ *
734
+ * **Details**
735
+ *
736
+ * Every operation either returns at least one result or fails with a
737
+ * {@link DnsError}.
738
+ *
739
+ * @stability experimental
740
+ * @category services
741
+ * @since 4.0.0
742
+ */
743
+ export class Dns extends Context.Service<Dns, {
744
+ /**
745
+ * Looks up the addresses used to connect to a host name.
746
+ *
747
+ * **Details**
748
+ *
749
+ * Addresses are returned in the implementation's preferred order. Platform
750
+ * implementations use the operating system resolver and keep the system's
751
+ * order.
752
+ * Names are parsed and normalized; invalid names fail with `BadName`.
753
+ *
754
+ * **Gotchas**
755
+ *
756
+ * IPv6 link-local addresses (`fe80::/10`) are returned without the interface
757
+ * they belong to, because the runtimes' resolvers do not report it.
758
+ */
759
+ lookup<F extends NetAddress.IpFamily>(
760
+ host: Host.DomainNameInput,
761
+ options: { readonly family: F }
762
+ ): Effect.Effect<Arr.NonEmptyReadonlyArray<NetAddress.FamilyAddress<F>>, DnsError>
763
+ lookup(
764
+ host: Host.DomainNameInput,
765
+ options?: LookupOptions
766
+ ): Effect.Effect<Arr.NonEmptyReadonlyArray<NetAddress.IpAddress>, DnsError>
767
+
768
+ /**
769
+ * Queries the DNS records of one type for a name.
770
+ * Names are parsed and normalized; invalid names fail with `BadName`.
771
+ */
772
+ resolve<T extends RecordType>(
773
+ name: Host.DomainNameInput,
774
+ type: T
775
+ ): Effect.Effect<Arr.NonEmptyReadonlyArray<RecordFor<T>>, DnsError>
776
+
777
+ /**
778
+ * Looks up the host names of an address with a reverse DNS query.
779
+ *
780
+ * **Details**
781
+ *
782
+ * Addresses given as strings are parsed; invalid addresses fail with
783
+ * `BadName`. Names that are not valid host names are skipped, including
784
+ * names with non-ASCII labels, which are not converted to their ASCII
785
+ * (`xn--`) form because that form is a different DNS name. When every
786
+ * returned name is skipped, the lookup fails with `InvalidResponse`.
787
+ */
788
+ reverse(address: NetAddress.IpAddressInput): Effect.Effect<Arr.NonEmptyReadonlyArray<Host.DomainName>, DnsError>
789
+ }>()("effect/net/Dns") {}
790
+
791
+ const notFound = (method: DnsError["method"], hostname: string, recordType?: RecordType) =>
792
+ Effect.fail(new DnsError({ reason: "NotFound", method, hostname, recordType }))
793
+
794
+ const badName = (method: DnsError["method"], hostname: string, cause: unknown, recordType?: RecordType) =>
795
+ Effect.fail(new DnsError({ reason: "BadName", method, hostname, recordType, cause }))
796
+
797
+ const asciiName = /^[\w.-]+$/
798
+
799
+ /**
800
+ * Creates a `Dns` service from platform resolver operations.
801
+ *
802
+ * **Details**
803
+ *
804
+ * Lookup and query names are parsed and normalized, and reverse lookup
805
+ * addresses are parsed, before platform callbacks receive them. Invalid names
806
+ * and addresses fail with `BadName`.
807
+ *
808
+ * The constructor filters lookups by the requested address family, keeps only
809
+ * records of the requested type, removes duplicates, and turns empty results
810
+ * into `NotFound` failures. `reverse` receives the names of an address and
811
+ * keeps those that are valid host names, failing with `InvalidResponse` when
812
+ * there are names but none is valid. Without a `reverse` operation, the names
813
+ * are those of the PTR records at the address's `reverseName`, and failures of
814
+ * that query are reported as failures of `reverse`.
815
+ *
816
+ * Only `lookup` is required. Platforms that can resolve addresses but cannot
817
+ * query records, such as browsers, omit `resolve`; record queries, and reverse
818
+ * lookups without a `reverse` operation, then fail with `Unsupported`.
819
+ *
820
+ * @stability experimental
821
+ * @category constructors
822
+ * @since 4.0.0
823
+ */
824
+ export const make = (impl: {
825
+ readonly lookup: (
826
+ host: Host.DomainName,
827
+ family: NetAddress.IpFamily | undefined
828
+ ) => Effect.Effect<ReadonlyArray<NetAddress.IpAddress>, DnsError>
829
+ readonly resolve?:
830
+ | ((name: Host.DomainName, type: RecordType) => Effect.Effect<ReadonlyArray<DnsRecord>, DnsError>)
831
+ | undefined
832
+ readonly reverse?:
833
+ | ((address: NetAddress.IpAddress) => Effect.Effect<ReadonlyArray<string>, DnsError>)
834
+ | undefined
835
+ }): Dns["Service"] => {
836
+ const inFamily = (family: NetAddress.IpFamily | undefined) => (address: NetAddress.IpAddress): boolean =>
837
+ family === undefined || NetAddress.isFamily(address, family)
838
+
839
+ const resolveRecords = impl.resolve ??
840
+ ((name: Host.DomainName, type: RecordType) =>
841
+ Effect.fail(new DnsError({ reason: "Unsupported", method: "resolve", hostname: name, recordType: type })))
842
+
843
+ const reverseNames = impl.reverse ??
844
+ ((address: NetAddress.IpAddress) =>
845
+ resolveRecords(reverseName(address), "PTR").pipe(
846
+ Effect.map(Arr.flatMap((record) => record._tag === "PTR" ? [record.host] : [])),
847
+ Effect.mapError((error) =>
848
+ new DnsError({
849
+ reason: error.reason,
850
+ method: "reverse",
851
+ hostname: NetAddress.formatIp(address),
852
+ cause: error.cause
853
+ })
854
+ )
855
+ ))
856
+
857
+ const lookup = (input: Host.DomainNameInput, options?: LookupOptions) =>
858
+ Result.match(Host.domainNameFromInput(input), {
859
+ onFailure: (cause) => badName("lookup", input, cause),
860
+ onSuccess: (host) =>
861
+ impl.lookup(host, options?.family).pipe(
862
+ Effect.flatMap((addresses) =>
863
+ Arr.match(Arr.dedupe(addresses.filter(inFamily(options?.family))), {
864
+ onEmpty: () => notFound("lookup", host),
865
+ onNonEmpty: Effect.succeed
866
+ })
867
+ )
868
+ )
869
+ })
870
+
871
+ return Dns.of({
872
+ lookup,
873
+ resolve: <T extends RecordType>(input: Host.DomainNameInput, type: T) =>
874
+ Result.match(Host.domainNameFromInput(input), {
875
+ onFailure: (cause) => badName("resolve", input, cause, type),
876
+ onSuccess: (name) =>
877
+ resolveRecords(name, type).pipe(
878
+ Effect.flatMap((records) =>
879
+ Arr.match(Arr.dedupe(records.filter((record): record is RecordFor<T> => record._tag === type)), {
880
+ onEmpty: () => notFound("resolve", name, type),
881
+ onNonEmpty: Effect.succeed
882
+ })
883
+ )
884
+ )
885
+ }),
886
+ reverse: (input) =>
887
+ Result.match(NetAddress.ipFromInput(input), {
888
+ onFailure: (cause) => badName("reverse", String(input), cause),
889
+ onSuccess: (address) =>
890
+ reverseNames(address).pipe(
891
+ Effect.flatMap((names) => {
892
+ const hostname = NetAddress.formatIp(address)
893
+ // Only ASCII names: `domainNameFromString` would convert UTF-8 labels
894
+ // to a different, `xn--` name.
895
+ const hosts = Arr.dedupe(
896
+ Arr.filterMap(names.filter((name) => asciiName.test(name)), Host.domainNameFromString)
897
+ )
898
+ return Arr.isReadonlyArrayNonEmpty(hosts)
899
+ ? Effect.succeed(hosts)
900
+ : names.length > 0
901
+ ? Effect.fail(new DnsError({ reason: "InvalidResponse", method: "reverse", hostname }))
902
+ : notFound("reverse", hostname)
903
+ })
904
+ )
905
+ })
906
+ })
907
+ }
908
+
909
+ // =============================================================================
910
+ // Static resolver
911
+ // =============================================================================
912
+
913
+ /**
914
+ * Fixed names, addresses, and records for a static resolver.
915
+ *
916
+ * **Details**
917
+ *
918
+ * `hosts` works like a hosts file: its entries are used by `lookup` and
919
+ * `reverse`, but not by `resolve`. `records` holds DNS records by owner name and
920
+ * is used by all three operations. Names are normalized like
921
+ * `Host.domainNameFromString`, and a trailing dot is ignored. Addresses are
922
+ * converted like `NetAddress.ipFromInput`.
923
+ *
924
+ * @stability experimental
925
+ * @category models
926
+ * @since 4.0.0
927
+ */
928
+ export interface StaticZone {
929
+ readonly hosts?: ReadonlyRecord<string, ReadonlyArray<NetAddress.IpAddressInput>> | undefined
930
+ readonly records?: ReadonlyRecord<string, ReadonlyArray<DnsRecord>> | undefined
931
+ }
932
+
933
+ const zoneKey = (name: string): string => name.length > 1 && name.endsWith(".") ? name.slice(0, -1) : name
934
+
935
+ /**
936
+ * Creates a `Dns` service that answers from fixed data.
937
+ *
938
+ * **When to use**
939
+ *
940
+ * Use to test code that depends on `Dns`, or to provide fixed names on
941
+ * platforms without a resolver.
942
+ *
943
+ * **Details**
944
+ *
945
+ * Names without matching addresses or records fail with `NotFound`. CNAME
946
+ * records are returned by `CNAME` queries but are not followed.
947
+ *
948
+ * **Example** (Looking up a name with a static resolver)
949
+ *
950
+ * ```ts import.meta.vitest
951
+ * import { Effect, Result } from "effect"
952
+ * import { Dns, NetAddress } from "effect/net"
953
+ *
954
+ * const dns = Result.getOrThrow(Dns.makeStatic({
955
+ * hosts: { "db.internal": ["10.0.0.5"] }
956
+ * }))
957
+ *
958
+ * const program = dns.lookup("db.internal").pipe(
959
+ * Effect.map((addresses) => addresses.map(NetAddress.formatIp))
960
+ * )
961
+ *
962
+ * await Effect.runPromise(program) // => ["10.0.0.5"]
963
+ * ```
964
+ *
965
+ * @see {@link layerStatic}
966
+ * @stability experimental
967
+ * @category constructors
968
+ * @since 4.0.0
969
+ */
970
+ export const makeStatic = (zone: StaticZone): Result.Result<Dns["Service"], NetAddress.NetAddressError> => {
971
+ const entries = new Map<string, {
972
+ readonly addresses: Array<NetAddress.IpAddress>
973
+ readonly records: Array<DnsRecord>
974
+ }>()
975
+ const entry = (name: string) =>
976
+ Result.map(Host.domainNameFromString(name), (domain) => {
977
+ const key = zoneKey(domain)
978
+ let current = entries.get(key)
979
+ if (current === undefined) {
980
+ current = { addresses: [], records: [] }
981
+ entries.set(key, current)
982
+ }
983
+ return current
984
+ })
985
+
986
+ for (const [name, addresses] of Object.entries(zone.hosts ?? {})) {
987
+ const result = entry(name)
988
+ if (Result.isFailure(result)) return Result.fail(result.failure)
989
+ for (const input of addresses) {
990
+ const address = NetAddress.ipFromInput(input)
991
+ if (Result.isFailure(address)) return Result.fail(address.failure)
992
+ result.success.addresses.push(address.success)
993
+ }
994
+ }
995
+
996
+ for (const [name, records] of Object.entries(zone.records ?? {})) {
997
+ const result = entry(name)
998
+ if (Result.isFailure(result)) return Result.fail(result.failure)
999
+ for (const record of records) {
1000
+ if (!isDnsRecord(record)) {
1001
+ return Result.fail(new NetAddress.NetAddressError({ input: record, message: "expected a DNS record" }))
1002
+ }
1003
+ result.success.records.push(record)
1004
+ }
1005
+ }
1006
+
1007
+ const recordsAt = (name: string): ReadonlyArray<DnsRecord> => entries.get(zoneKey(name))?.records ?? []
1008
+
1009
+ return Result.succeed(make({
1010
+ lookup: (host) =>
1011
+ Effect.sync(() => [
1012
+ ...(entries.get(zoneKey(host))?.addresses ?? []),
1013
+ ...recordsAt(host).flatMap((record) => record._tag === "A" || record._tag === "AAAA" ? [record.address] : [])
1014
+ ]),
1015
+ resolve: (name) => Effect.sync(() => recordsAt(name)),
1016
+ reverse: (address) =>
1017
+ Effect.sync(() => [
1018
+ ...[...entries].flatMap(([name, current]) =>
1019
+ current.addresses.some((candidate) => Equal.equals(candidate, address)) ? [name] : []
1020
+ ),
1021
+ ...recordsAt(reverseName(address)).flatMap((record) => record._tag === "PTR" ? [record.host] : [])
1022
+ ])
1023
+ }))
1024
+ }
1025
+
1026
+ /**
1027
+ * Creates a layer that provides a `Dns` service answering from fixed data.
1028
+ *
1029
+ * @see {@link makeStatic} for the resolution rules
1030
+ * @stability experimental
1031
+ * @category layers
1032
+ * @since 4.0.0
1033
+ */
1034
+ export const layerStatic = (zone: StaticZone): Layer.Layer<Dns, NetAddress.NetAddressError> =>
1035
+ Layer.effect(Dns, Effect.fromResult(makeStatic(zone)))