@evolu/common 8.9.0 → 8.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (233) hide show
  1. package/dist/src/Bytes.d.ts +647 -0
  2. package/dist/src/Bytes.d.ts.map +1 -0
  3. package/dist/src/{Binary.js → Bytes.js} +266 -16
  4. package/dist/src/Config.d.ts +142 -0
  5. package/dist/src/Config.d.ts.map +1 -0
  6. package/dist/src/Config.js +181 -0
  7. package/dist/src/Fs.d.ts +302 -0
  8. package/dist/src/Fs.d.ts.map +1 -0
  9. package/dist/src/Fs.js +111 -0
  10. package/dist/src/Number.d.ts +50 -7
  11. package/dist/src/Number.d.ts.map +1 -1
  12. package/dist/src/Number.js +47 -8
  13. package/dist/src/Object.d.ts +32 -0
  14. package/dist/src/Object.d.ts.map +1 -1
  15. package/dist/src/Object.js +41 -0
  16. package/dist/src/Sqlite.js +1 -1
  17. package/dist/src/Time.d.ts +74 -12
  18. package/dist/src/Time.d.ts.map +1 -1
  19. package/dist/src/Time.js +40 -2
  20. package/dist/src/Type.d.ts +1451 -79
  21. package/dist/src/Type.d.ts.map +1 -1
  22. package/dist/src/Type.js +1303 -96
  23. package/dist/src/index.d.ts +3 -1
  24. package/dist/src/index.d.ts.map +1 -1
  25. package/dist/src/index.js +3 -1
  26. package/dist/src/intl/_en.d.ts +24 -1
  27. package/dist/src/intl/_en.d.ts.map +1 -1
  28. package/dist/src/intl/_en.js +20 -0
  29. package/dist/src/intl/ar.d.ts +24 -1
  30. package/dist/src/intl/ar.d.ts.map +1 -1
  31. package/dist/src/intl/ar.js +20 -0
  32. package/dist/src/intl/bn.d.ts +24 -1
  33. package/dist/src/intl/bn.d.ts.map +1 -1
  34. package/dist/src/intl/bn.js +20 -0
  35. package/dist/src/intl/ca.d.ts +24 -1
  36. package/dist/src/intl/ca.d.ts.map +1 -1
  37. package/dist/src/intl/ca.js +20 -0
  38. package/dist/src/intl/cs.d.ts +24 -1
  39. package/dist/src/intl/cs.d.ts.map +1 -1
  40. package/dist/src/intl/cs.js +20 -0
  41. package/dist/src/intl/da.d.ts +24 -1
  42. package/dist/src/intl/da.d.ts.map +1 -1
  43. package/dist/src/intl/da.js +20 -0
  44. package/dist/src/intl/de.d.ts +24 -1
  45. package/dist/src/intl/de.d.ts.map +1 -1
  46. package/dist/src/intl/de.js +20 -0
  47. package/dist/src/intl/el.d.ts +24 -1
  48. package/dist/src/intl/el.d.ts.map +1 -1
  49. package/dist/src/intl/el.js +20 -0
  50. package/dist/src/intl/es.d.ts +24 -1
  51. package/dist/src/intl/es.d.ts.map +1 -1
  52. package/dist/src/intl/es.js +20 -0
  53. package/dist/src/intl/fa.d.ts +24 -1
  54. package/dist/src/intl/fa.d.ts.map +1 -1
  55. package/dist/src/intl/fa.js +20 -0
  56. package/dist/src/intl/fi.d.ts +24 -1
  57. package/dist/src/intl/fi.d.ts.map +1 -1
  58. package/dist/src/intl/fi.js +20 -0
  59. package/dist/src/intl/fil.d.ts +24 -1
  60. package/dist/src/intl/fil.d.ts.map +1 -1
  61. package/dist/src/intl/fil.js +20 -0
  62. package/dist/src/intl/fr.d.ts +24 -1
  63. package/dist/src/intl/fr.d.ts.map +1 -1
  64. package/dist/src/intl/fr.js +20 -0
  65. package/dist/src/intl/he.d.ts +24 -1
  66. package/dist/src/intl/he.d.ts.map +1 -1
  67. package/dist/src/intl/he.js +20 -0
  68. package/dist/src/intl/hi.d.ts +24 -1
  69. package/dist/src/intl/hi.d.ts.map +1 -1
  70. package/dist/src/intl/hi.js +20 -0
  71. package/dist/src/intl/hr.d.ts +24 -1
  72. package/dist/src/intl/hr.d.ts.map +1 -1
  73. package/dist/src/intl/hr.js +20 -0
  74. package/dist/src/intl/hu.d.ts +22 -1
  75. package/dist/src/intl/hu.d.ts.map +1 -1
  76. package/dist/src/intl/hu.js +18 -0
  77. package/dist/src/intl/id.d.ts +24 -1
  78. package/dist/src/intl/id.d.ts.map +1 -1
  79. package/dist/src/intl/id.js +20 -0
  80. package/dist/src/intl/it.d.ts +24 -1
  81. package/dist/src/intl/it.d.ts.map +1 -1
  82. package/dist/src/intl/it.js +20 -0
  83. package/dist/src/intl/ja.d.ts +24 -1
  84. package/dist/src/intl/ja.d.ts.map +1 -1
  85. package/dist/src/intl/ja.js +20 -0
  86. package/dist/src/intl/ko.d.ts +24 -1
  87. package/dist/src/intl/ko.d.ts.map +1 -1
  88. package/dist/src/intl/ko.js +20 -0
  89. package/dist/src/intl/ml.d.ts +24 -1
  90. package/dist/src/intl/ml.d.ts.map +1 -1
  91. package/dist/src/intl/ml.js +20 -0
  92. package/dist/src/intl/mr.d.ts +24 -1
  93. package/dist/src/intl/mr.d.ts.map +1 -1
  94. package/dist/src/intl/mr.js +20 -0
  95. package/dist/src/intl/ms.d.ts +24 -1
  96. package/dist/src/intl/ms.d.ts.map +1 -1
  97. package/dist/src/intl/ms.js +20 -0
  98. package/dist/src/intl/nb.d.ts +22 -1
  99. package/dist/src/intl/nb.d.ts.map +1 -1
  100. package/dist/src/intl/nb.js +18 -0
  101. package/dist/src/intl/nl.d.ts +24 -1
  102. package/dist/src/intl/nl.d.ts.map +1 -1
  103. package/dist/src/intl/nl.js +20 -0
  104. package/dist/src/intl/pa.d.ts +24 -1
  105. package/dist/src/intl/pa.d.ts.map +1 -1
  106. package/dist/src/intl/pa.js +20 -0
  107. package/dist/src/intl/pl.d.ts +23 -0
  108. package/dist/src/intl/pl.d.ts.map +1 -1
  109. package/dist/src/intl/pl.js +20 -0
  110. package/dist/src/intl/pt-BR.d.ts +24 -1
  111. package/dist/src/intl/pt-BR.d.ts.map +1 -1
  112. package/dist/src/intl/pt-BR.js +20 -0
  113. package/dist/src/intl/pt.d.ts +24 -1
  114. package/dist/src/intl/pt.d.ts.map +1 -1
  115. package/dist/src/intl/pt.js +20 -0
  116. package/dist/src/intl/ro.d.ts +24 -1
  117. package/dist/src/intl/ro.d.ts.map +1 -1
  118. package/dist/src/intl/ro.js +20 -0
  119. package/dist/src/intl/sk.d.ts +24 -1
  120. package/dist/src/intl/sk.d.ts.map +1 -1
  121. package/dist/src/intl/sk.js +20 -0
  122. package/dist/src/intl/sl.d.ts +24 -1
  123. package/dist/src/intl/sl.d.ts.map +1 -1
  124. package/dist/src/intl/sl.js +20 -0
  125. package/dist/src/intl/sv.d.ts +24 -1
  126. package/dist/src/intl/sv.d.ts.map +1 -1
  127. package/dist/src/intl/sv.js +20 -0
  128. package/dist/src/intl/sw.d.ts +21 -0
  129. package/dist/src/intl/sw.d.ts.map +1 -1
  130. package/dist/src/intl/sw.js +18 -0
  131. package/dist/src/intl/ta.d.ts +24 -1
  132. package/dist/src/intl/ta.d.ts.map +1 -1
  133. package/dist/src/intl/ta.js +20 -0
  134. package/dist/src/intl/te.d.ts +24 -1
  135. package/dist/src/intl/te.d.ts.map +1 -1
  136. package/dist/src/intl/te.js +20 -0
  137. package/dist/src/intl/th.d.ts +24 -1
  138. package/dist/src/intl/th.d.ts.map +1 -1
  139. package/dist/src/intl/th.js +20 -0
  140. package/dist/src/intl/tr.d.ts +24 -1
  141. package/dist/src/intl/tr.d.ts.map +1 -1
  142. package/dist/src/intl/tr.js +20 -0
  143. package/dist/src/intl/uk.d.ts +80 -57
  144. package/dist/src/intl/uk.d.ts.map +1 -1
  145. package/dist/src/intl/uk.js +174 -149
  146. package/dist/src/intl/ur.d.ts +24 -1
  147. package/dist/src/intl/ur.d.ts.map +1 -1
  148. package/dist/src/intl/ur.js +20 -0
  149. package/dist/src/intl/vi.d.ts +24 -1
  150. package/dist/src/intl/vi.d.ts.map +1 -1
  151. package/dist/src/intl/vi.js +20 -0
  152. package/dist/src/intl/zh-CN.d.ts +24 -1
  153. package/dist/src/intl/zh-CN.d.ts.map +1 -1
  154. package/dist/src/intl/zh-CN.js +20 -0
  155. package/dist/src/intl/zh-TW.d.ts +24 -1
  156. package/dist/src/intl/zh-TW.d.ts.map +1 -1
  157. package/dist/src/intl/zh-TW.js +20 -0
  158. package/dist/src/local-first/Protocol.d.ts +1 -1
  159. package/dist/src/local-first/Protocol.d.ts.map +1 -1
  160. package/dist/src/local-first/Protocol.js +1 -1
  161. package/dist/src/local-first/Schema.d.ts +10 -0
  162. package/dist/src/local-first/Schema.d.ts.map +1 -1
  163. package/dist/src/local-first/Storage.js +1 -1
  164. package/dist/src/local-first/Timestamp.js +1 -1
  165. package/package.json +1 -1
  166. package/src/{Binary.test.ts → Bytes.test.ts} +286 -1
  167. package/src/{Binary.ts → Bytes.ts} +652 -21
  168. package/src/Config.test.ts +672 -0
  169. package/src/Config.ts +410 -0
  170. package/src/Fs.test.ts +105 -0
  171. package/src/Fs.ts +414 -0
  172. package/src/Number.test.ts +82 -18
  173. package/src/Number.ts +76 -8
  174. package/src/Object.test.ts +115 -1
  175. package/src/Object.ts +44 -0
  176. package/src/Sqlite.ts +1 -1
  177. package/src/Time.test.ts +82 -11
  178. package/src/Time.ts +141 -16
  179. package/src/Type.test.ts +3114 -157
  180. package/src/Type.ts +2553 -196
  181. package/src/index.ts +3 -1
  182. package/src/intl/_en.ts +70 -0
  183. package/src/intl/ar.ts +71 -0
  184. package/src/intl/bn.ts +70 -0
  185. package/src/intl/ca.ts +70 -0
  186. package/src/intl/cs.ts +70 -0
  187. package/src/intl/da.ts +70 -0
  188. package/src/intl/de.ts +70 -0
  189. package/src/intl/el.ts +70 -0
  190. package/src/intl/es.ts +70 -0
  191. package/src/intl/fa.ts +70 -0
  192. package/src/intl/fi.ts +70 -0
  193. package/src/intl/fil.ts +70 -0
  194. package/src/intl/fr.ts +70 -0
  195. package/src/intl/he.ts +70 -0
  196. package/src/intl/hi.ts +70 -0
  197. package/src/intl/hr.ts +70 -0
  198. package/src/intl/hu.ts +69 -0
  199. package/src/intl/id.ts +70 -0
  200. package/src/intl/intl.test.ts +819 -1
  201. package/src/intl/it.ts +70 -0
  202. package/src/intl/ja.ts +70 -0
  203. package/src/intl/ko.ts +68 -0
  204. package/src/intl/ml.ts +70 -0
  205. package/src/intl/mr.ts +70 -0
  206. package/src/intl/ms.ts +71 -0
  207. package/src/intl/nb.ts +69 -0
  208. package/src/intl/nl.ts +70 -0
  209. package/src/intl/pa.ts +70 -0
  210. package/src/intl/pl.ts +63 -0
  211. package/src/intl/pt-BR.ts +70 -0
  212. package/src/intl/pt.ts +71 -0
  213. package/src/intl/ro.ts +70 -0
  214. package/src/intl/sk.ts +71 -0
  215. package/src/intl/sl.ts +70 -0
  216. package/src/intl/sv.ts +70 -0
  217. package/src/intl/sw.ts +62 -0
  218. package/src/intl/ta.ts +70 -0
  219. package/src/intl/te.ts +70 -0
  220. package/src/intl/th.ts +68 -0
  221. package/src/intl/tr.ts +70 -0
  222. package/src/intl/uk.ts +228 -155
  223. package/src/intl/ur.ts +70 -0
  224. package/src/intl/vi.ts +70 -0
  225. package/src/intl/zh-CN.ts +68 -0
  226. package/src/intl/zh-TW.ts +68 -0
  227. package/src/local-first/Protocol.test.ts +1 -1
  228. package/src/local-first/Protocol.ts +1 -1
  229. package/src/local-first/Schema.ts +11 -0
  230. package/src/local-first/Storage.ts +1 -1
  231. package/src/local-first/Timestamp.ts +1 -1
  232. package/dist/src/Binary.d.ts +0 -254
  233. package/dist/src/Binary.d.ts.map +0 -1
package/src/Config.ts ADDED
@@ -0,0 +1,410 @@
1
+ /**
2
+ * Strict configuration Types with explicit application policy.
3
+ *
4
+ * Decode configuration at startup, then pass validated values to constructors
5
+ * or Tasks. The application decides how to load configuration, combine sources,
6
+ * and apply defaults. Declare defaults with {@link withDefault}, or use `??`
7
+ * where a value is consumed. Invalid supplied values still fail decoding.
8
+ *
9
+ * Resolve source precedence before replacing absence with defaults. If a source
10
+ * Type already provides defaults and later composition needs to distinguish
11
+ * them from supplied values, use `strategy: "preserve"` and inspect
12
+ * `defaultUsed` explicitly. Preserving this evidence does not choose a merging
13
+ * policy: the application still decides which source wins.
14
+ *
15
+ * {@link env} creates a pure, reversible codec. It does not read global state,
16
+ * load files, merge sources, or introduce a Run dependency. Tests can pass
17
+ * ordinary objects; a Node.js entry point can pass `process.env`.
18
+ *
19
+ * @module
20
+ */
21
+
22
+ import { assert } from "./Assert.ts";
23
+ import {
24
+ createMutableRecord,
25
+ filterObjectKeys,
26
+ isPlainObject,
27
+ } from "./Object.ts";
28
+ import { ok } from "./Result.ts";
29
+ import {
30
+ CamelCaseIdentifier,
31
+ camelCaseToConstantCase,
32
+ ConstantCaseIdentifier,
33
+ EvoluType,
34
+ maxLength,
35
+ object,
36
+ objectKeys,
37
+ String,
38
+ transform,
39
+ Unknown,
40
+ type AnyType,
41
+ type InferErrors,
42
+ type ValidateLiteral,
43
+ type ObjectKeysType,
44
+ type ObjectProps,
45
+ type ObjectType,
46
+ type TransformType,
47
+ type TypeNode,
48
+ type withDefault,
49
+ } from "./Type.ts";
50
+ import type {
51
+ CompileTimeError,
52
+ IsUnion,
53
+ UnionToIntersection,
54
+ } from "./Types.ts";
55
+
56
+ /**
57
+ * An environment variable name in Evolu's configuration convention.
58
+ *
59
+ * Names are {@link ConstantCaseIdentifier}s of at most 255 characters, including
60
+ * the application prefix. This is an Evolu convention, not an operating-system
61
+ * limit. Double underscores do not introduce nesting.
62
+ *
63
+ * ### Example
64
+ *
65
+ * ```ts
66
+ * import { assertErr, assertOk, EnvName } from "@evolu/common";
67
+ *
68
+ * assertOk(
69
+ * EnvName.fromUnknown("APP_MAX_OWNER_BYTES"),
70
+ * "APP_MAX_OWNER_BYTES",
71
+ * );
72
+ * assertErr(EnvName.fromUnknown("APP_maxOwnerBytes"));
73
+ * assertErr(EnvName.fromUnknown("APP_DB__PORT"));
74
+ * ```
75
+ */
76
+ export const EnvName = /*#__PURE__*/ maxLength(255)(ConstantCaseIdentifier);
77
+ export type EnvName = typeof EnvName.Output;
78
+
79
+ /** Fields and one-level namespace groups used to construct an {@link env} Type. */
80
+ export type EnvProps = Readonly<
81
+ Record<string, ObjectProps[string] | ObjectProps>
82
+ >;
83
+
84
+ // Classify declarations by casing; identifier Types validate the full grammar
85
+ // and name lengths during construction.
86
+ type EnvDeclarationKind<Key> = Key extends string
87
+ ? Key extends Uppercase<Key>
88
+ ? "group"
89
+ : Key extends Uncapitalize<Key>
90
+ ? "field"
91
+ : "invalid"
92
+ : "invalid";
93
+
94
+ type EnvFlatProps<Props extends EnvProps> =
95
+ UnionToIntersection<
96
+ {
97
+ [Key in keyof Props]: EnvDeclarationKind<Key> extends "group"
98
+ ? Props[Key]
99
+ : EnvDeclarationKind<Key> extends "field"
100
+ ? { readonly [Field in Key]: Props[Key] }
101
+ : never;
102
+ }[keyof Props]
103
+ > extends infer Flat extends ObjectProps
104
+ ? Flat
105
+ : {};
106
+
107
+ type EnvFieldType<Field> = Field extends TypeNode
108
+ ? Field
109
+ : Field extends { readonly type: infer T extends TypeNode }
110
+ ? T
111
+ : never;
112
+
113
+ type EnvKeysValidation<Props> =
114
+ | (string extends keyof Props
115
+ ? CompileTimeError<
116
+ "Env",
117
+ "Environment properties must use fixed string keys."
118
+ >
119
+ : never)
120
+ | (IsUnion<Props> extends true
121
+ ? CompileTimeError<
122
+ "Env",
123
+ "Environment properties must use one concrete schema."
124
+ >
125
+ : never)
126
+ | {
127
+ [Key in keyof Props]: Key extends string
128
+ ? ValidateLiteral<Key> extends Key
129
+ ? never
130
+ : CompileTimeError<
131
+ "Env",
132
+ "Environment properties must use fixed string keys."
133
+ >
134
+ : CompileTimeError<
135
+ "Env",
136
+ "Environment properties must use fixed string keys."
137
+ >;
138
+ }[keyof Props];
139
+
140
+ type EnvFieldValidation<Field> =
141
+ IsUnion<Field> extends true
142
+ ? CompileTimeError<"Env", "Environment fields must use one concrete Type.">
143
+ : EnvFieldType<Field> extends infer T extends AnyType
144
+ ? IsUnion<T> extends true
145
+ ? CompileTimeError<
146
+ "Env",
147
+ "Environment fields must use one concrete Type."
148
+ >
149
+ : [T["CanonicalInput"]] extends [string]
150
+ ? Extract<
151
+ | "ObjectMissingProperty"
152
+ | "ObjectPropertyAccess"
153
+ | "ObjectExcessProperty",
154
+ InferErrors<T>["type"]
155
+ > extends never
156
+ ? never
157
+ : CompileTimeError<
158
+ "Env",
159
+ "Environment fields must not use error tags reserved for Object structure."
160
+ >
161
+ : CompileTimeError<
162
+ "Env",
163
+ "Environment fields must encode to strings."
164
+ >
165
+ : CompileTimeError<
166
+ "Env",
167
+ "Environment fields must use one concrete Type."
168
+ >;
169
+
170
+ type EnvFieldsValidation<Props extends ObjectProps> =
171
+ | EnvKeysValidation<Props>
172
+ | {
173
+ [Key in keyof Props]: EnvDeclarationKind<Key> extends "field"
174
+ ? EnvFieldValidation<Props[Key]>
175
+ : CompileTimeError<
176
+ "Env",
177
+ "Environment fields must use camelCase names."
178
+ >;
179
+ }[keyof Props];
180
+
181
+ type EnvValidation<Props extends EnvProps> =
182
+ | EnvKeysValidation<Props>
183
+ | {
184
+ [Key in keyof Props]: EnvDeclarationKind<Key> extends "group"
185
+ ? Props[Key] extends ObjectProps
186
+ ? EnvFieldsValidation<Props[Key]>
187
+ : CompileTimeError<
188
+ "Env",
189
+ "Environment namespaces must contain a group of fields."
190
+ >
191
+ : EnvDeclarationKind<Key> extends "field"
192
+ ? Props[Key] extends ObjectProps[string]
193
+ ? EnvFieldValidation<Props[Key]>
194
+ : CompileTimeError<
195
+ "Env",
196
+ "Environment fields must be Types, optional properties, or defaulted properties."
197
+ >
198
+ : CompileTimeError<
199
+ "Env",
200
+ "Environment declarations must use camelCase field names or CONSTANT_CASE namespace names."
201
+ >;
202
+ }[keyof Props];
203
+
204
+ type EnvKeyType = TransformType<
205
+ typeof String,
206
+ typeof CamelCaseIdentifier,
207
+ "EnvKey",
208
+ never,
209
+ string
210
+ >;
211
+
212
+ /** The configuration codec returned by {@link env}. */
213
+ export interface EnvType<Props extends EnvProps> extends TransformType<
214
+ typeof Unknown,
215
+ ObjectKeysType<EnvKeyType, ObjectType<EnvFlatProps<Props>>>,
216
+ "Env",
217
+ never,
218
+ Readonly<
219
+ Record<
220
+ string,
221
+ ObjectType<EnvFlatProps<Props>>["CanonicalInput"][keyof ObjectType<
222
+ EnvFlatProps<Props>
223
+ >["CanonicalInput"]]
224
+ >
225
+ >
226
+ > {}
227
+
228
+ /**
229
+ * Creates a reversible environment-variable codec with a flat decoded output.
230
+ *
231
+ * Declare camelCase fields directly, or put them in one-level CONSTANT_CASE
232
+ * namespace groups. A direct `port` field reads `PORT`; a `maxOwnerBytes` field
233
+ * in an `EVOLU_RELAY` group reads `EVOLU_RELAY_MAX_OWNER_BYTES`. Groups affect
234
+ * external names only. Duplicate decoded fields or encoded names are rejected
235
+ * during construction, even when their Types are identical.
236
+ *
237
+ * Selects exact declared unprefixed names and all keys within declared
238
+ * namespaces. Namespace selection ignores casing so incorrectly cased names
239
+ * fail validation instead of being ignored. Selected names must match their
240
+ * exact declared spelling. Unrelated variables are ignored; misspelled
241
+ * unprefixed names or namespace prefixes can therefore still look absent.
242
+ *
243
+ * Field Types must encode to strings. Optional fields may be absent, but
244
+ * explicit `undefined` values are rejected. Empty strings remain present and
245
+ * must satisfy the field Type. Compose {@link withDefault} to supply defaults.
246
+ * Encoding emits canonical external names and string values, preserving absence
247
+ * when the field codec does. Errors retain external names and nested paths.
248
+ *
249
+ * Reads own string properties from any non-null object, including
250
+ * `process.env`. Selected properties must be enumerable data properties;
251
+ * getters are never read. Prototypes and internal contents such as Map entries
252
+ * are ignored. Non-object inputs fail with standard object errors. Schema names
253
+ * must satisfy {@link EnvName}; groups cannot be nested. Loading and source
254
+ * precedence remain explicit application code.
255
+ *
256
+ * ### Example
257
+ *
258
+ * ```ts
259
+ * import {
260
+ * assertEqual,
261
+ * assertErr,
262
+ * assertOk,
263
+ * assertType,
264
+ * ByteSizeLiteral,
265
+ * env,
266
+ * optional,
267
+ * Port,
268
+ * PortFromString,
269
+ * withDefault,
270
+ * } from "@evolu/common";
271
+ *
272
+ * const RelayEnv = env({
273
+ * port: withDefault(optional(PortFromString), Port.orThrow(4000)),
274
+ * EVOLU_RELAY: {
275
+ * maxOwnerBytes: optional(ByteSizeLiteral),
276
+ * },
277
+ * });
278
+ *
279
+ * const result = RelayEnv.fromUnknown({
280
+ * PORT: "04000",
281
+ * EVOLU_RELAY_MAX_OWNER_BYTES: "512KiB",
282
+ * HOME: "/home/evolu",
283
+ * });
284
+ *
285
+ * assertOk(result, { port: 4000, maxOwnerBytes: "512KiB" });
286
+ * assertType<typeof result.value.port, Port>();
287
+ * assertEqual(RelayEnv.to(result.value), {
288
+ * PORT: "4000",
289
+ * EVOLU_RELAY_MAX_OWNER_BYTES: "512KiB",
290
+ * });
291
+ *
292
+ * assertOk(RelayEnv.fromUnknown({}), { port: 4000 });
293
+ * assertErr(RelayEnv.fromUnknown({ PORT: "" }));
294
+ * assertErr(RelayEnv.fromUnknown({ EVOLU_RELAY_MAX_OWENR_BYTES: "1MiB" }));
295
+ * assertErr(RelayEnv.fromUnknown({ evolu_relay_max_owner_bytes: "1MiB" }));
296
+ * ```
297
+ */
298
+ export const env = <const Props extends EnvProps>(
299
+ props: Props,
300
+ ..._validation: [EnvValidation<Props>] extends [never]
301
+ ? []
302
+ : [EnvValidation<Props>]
303
+ ): EnvType<Props> => {
304
+ assert(
305
+ isPlainObject(props),
306
+ "Environment properties must be a plain object.",
307
+ );
308
+ const flat = createMutableRecord<string, ObjectProps[string]>();
309
+ const inputNameByOutputName = new Map<string, string>();
310
+ const outputNameByInputName = new Map<string, string>();
311
+ const prefixes: Array<string> = [];
312
+ const unprefixedNames = new Set<string>();
313
+
314
+ for (const rootName of Reflect.ownKeys(props)) {
315
+ const descriptor = Object.getOwnPropertyDescriptor(props, rootName)!;
316
+ assert(
317
+ typeof rootName === "string" &&
318
+ descriptor.enumerable &&
319
+ "value" in descriptor,
320
+ "Environment properties must be enumerable string data properties.",
321
+ );
322
+ const grouped = ConstantCaseIdentifier.is(rootName);
323
+ assert(
324
+ grouped || CamelCaseIdentifier.is(rootName),
325
+ `Invalid environment schema name ${JSON.stringify(rootName)}. Expected a camelCase field or CONSTANT_CASE namespace.`,
326
+ );
327
+ const rootValue: unknown = descriptor.value;
328
+ const fields = grouped ? rootValue : { [rootName]: rootValue };
329
+ assert(
330
+ isPlainObject(fields) && !EvoluType.is(fields),
331
+ "Environment groups must be plain objects containing fields.",
332
+ );
333
+ if (grouped) {
334
+ assert(
335
+ EnvName.is(rootName + "_A"),
336
+ `Invalid environment namespace ${JSON.stringify(rootName)}. Names must fit within 255 characters.`,
337
+ );
338
+ prefixes.push(rootName + "_");
339
+ }
340
+ for (const name of Reflect.ownKeys(fields)) {
341
+ const field = Object.getOwnPropertyDescriptor(fields, name)!;
342
+ assert(
343
+ typeof name === "string" && field.enumerable && "value" in field,
344
+ "Environment fields must be enumerable string data properties.",
345
+ );
346
+ assert(
347
+ CamelCaseIdentifier.is(name),
348
+ `Invalid environment field ${JSON.stringify(name)}. Expected a camelCase identifier; groups cannot be nested.`,
349
+ );
350
+ assert(
351
+ !inputNameByOutputName.has(name),
352
+ `Duplicate environment field ${JSON.stringify(name)}.`,
353
+ );
354
+ const encoded =
355
+ (grouped ? rootName + "_" : "") + camelCaseToConstantCase(name);
356
+ assert(
357
+ EnvName.is(encoded),
358
+ `Invalid environment schema name ${JSON.stringify(encoded)}. Expected a CONSTANT_CASE identifier of at most 255 characters.`,
359
+ );
360
+ assert(
361
+ !outputNameByInputName.has(encoded),
362
+ `Duplicate environment variable ${JSON.stringify(encoded)}.`,
363
+ );
364
+ const property: unknown = field.value;
365
+ const fieldType: unknown = EvoluType.is(property)
366
+ ? property
367
+ : isPlainObject(property)
368
+ ? Object.getOwnPropertyDescriptor(property, "type")?.value
369
+ : undefined;
370
+ assert(
371
+ EvoluType.is(fieldType),
372
+ `Invalid environment field ${JSON.stringify(name)}. Expected a Type or optional/defaulted property; groups must use CONSTANT_CASE names.`,
373
+ );
374
+ flat[name] = property as ObjectProps[string];
375
+ inputNameByOutputName.set(name, encoded);
376
+ outputNameByInputName.set(encoded, name);
377
+ if (!grouped) unprefixedNames.add(encoded);
378
+ }
379
+ }
380
+
381
+ // Runtime construction validates the flattened fields; the public signature
382
+ // checks their concrete Types and string encodings before flattening.
383
+ const output = (
384
+ object as unknown as (props: ObjectProps) => ObjectType<ObjectProps>
385
+ )(flat);
386
+ const key = transform("EnvKey", String, CamelCaseIdentifier, {
387
+ // Identity fallback keeps this exposed codec round-trippable for other
388
+ // valid camelCase identifiers. objectKeys accepts only declared fields.
389
+ from: (value) => ok(outputNameByInputName.get(value) ?? value),
390
+ to: (value) => inputNameByOutputName.get(value) ?? value,
391
+ });
392
+ const withKeys = objectKeys(key)(output);
393
+ const type = transform("Env", Unknown, withKeys, {
394
+ from: (value) => {
395
+ if (typeof value !== "object" || value === null) return ok(value);
396
+ return ok(
397
+ filterObjectKeys(
398
+ value,
399
+ (key) =>
400
+ unprefixedNames.has(key) ||
401
+ prefixes.some((prefix) => key.toUpperCase().startsWith(prefix)),
402
+ ),
403
+ );
404
+ },
405
+ to: (value) => value,
406
+ });
407
+ // The maps preserve each field's name and codec; only the static flattening
408
+ // cannot be expressed by the runtime loops above.
409
+ return type as unknown as EnvType<Props>;
410
+ };
package/src/Fs.test.ts ADDED
@@ -0,0 +1,105 @@
1
+ import { describe, it } from "node:test";
2
+ import {
3
+ assert,
4
+ assertEqual,
5
+ assertErr,
6
+ assertInstanceOf,
7
+ assertOk,
8
+ assertSame,
9
+ } from "./Assert.ts";
10
+ import { testCreateFs, type Fs, type FsDep, type FsError } from "./Fs.ts";
11
+ import { err, ok } from "./Result.ts";
12
+ import { AbortError, testCreateRun, type Task } from "./Task.ts";
13
+ import { assertType } from "./Type.ts";
14
+
15
+ describe("testCreateFs", () => {
16
+ it("accepts native copy options and rejects the former overwrite option", () => {
17
+ const fs = testCreateFs();
18
+ const task = fs.copy("source", "destination", {
19
+ force: false,
20
+ errorOnExist: true,
21
+ preserveTimestamps: true,
22
+ });
23
+ assertType<typeof task, Task<void, FsError>>();
24
+
25
+ // @ts-expect-error: FsCopyOptions has no overwrite property; use Node's force and errorOnExist options.
26
+ fs.copy("source", "destination", { overwrite: false });
27
+ });
28
+
29
+ for (const [method, createTask] of [
30
+ ["readFile", (fs: Fs) => fs.readFile("file.txt")],
31
+ ["writeFile", (fs: Fs) => fs.writeFile("file.txt", "content")],
32
+ ["readDirectory", (fs: Fs) => fs.readDirectory("directory")],
33
+ ["createDirectory", (fs: Fs) => fs.createDirectory("directory")],
34
+ ["copy", (fs: Fs) => fs.copy("source", "destination")],
35
+ ["copyFile", (fs: Fs) => fs.copyFile("source", "destination")],
36
+ ["rename", (fs: Fs) => fs.rename("source", "destination")],
37
+ ["remove", (fs: Fs) => fs.remove("file.txt")],
38
+ ["getMetadata", (fs: Fs) => fs.getMetadata("file.txt")],
39
+ ["exists", (fs: Fs) => fs.exists("file.txt")],
40
+ ["createTempDirectory", (fs: Fs) => fs.createTempDirectory()],
41
+ ] as const) {
42
+ it(`panics when an unconfigured ${method} Task runs`, async () => {
43
+ const fs = testCreateFs();
44
+ const task = createTask(fs);
45
+ await using run = testCreateRun();
46
+
47
+ const result = await run.abortable<unknown, FsError>(task);
48
+
49
+ assertErr(result);
50
+ assert(AbortError.is(result.error), "Unexpected Fs calls must abort.");
51
+ assertEqual(result.error.reason.type, "PanicAbortReason");
52
+ const { defect } = result.error.reason;
53
+ assertInstanceOf(defect, Error);
54
+ assertEqual(defect.message, `Unexpected Fs.${method} call`);
55
+ assertEqual(run.deps.reportDefect.getDefects(), [result.error]);
56
+ });
57
+ }
58
+
59
+ it("uses an injected override with the original arguments", async () => {
60
+ const path = new URL("file:///message.txt");
61
+ const data = new TextEncoder().encode("hello");
62
+ const options = { flag: "a" } as const;
63
+ let calls = 0;
64
+ const writeFile: Fs["writeFile"] =
65
+ (receivedPath, receivedData, receivedOptions) => () => {
66
+ calls += 1;
67
+ assertSame(receivedPath, path);
68
+ assertSame(receivedData, data);
69
+ assertSame(receivedOptions, options);
70
+ return ok();
71
+ };
72
+ const fs = testCreateFs({ writeFile });
73
+ assertSame(fs.writeFile, writeFile);
74
+ await using run = testCreateRun({ fs });
75
+ const saveMessage: Task<void, FsError, FsDep> = (run) =>
76
+ run(run.deps.fs.writeFile(path, data, options));
77
+
78
+ assertEqual(calls, 0);
79
+ assertOk(await run(saveMessage));
80
+ assertEqual(calls, 1);
81
+ });
82
+
83
+ it("preserves readFile overloads and injected errors", async () => {
84
+ const error: FsError = {
85
+ type: "FsError",
86
+ reason: "PermissionDenied",
87
+ path: "protected.txt",
88
+ syscall: "open",
89
+ message: "Permission denied",
90
+ };
91
+ const fs = testCreateFs({ readFile: () => () => err(error) });
92
+ await using run = testCreateRun();
93
+ const bytes = fs.readFile("protected.txt");
94
+ const text = fs.readFile("protected.txt", "utf8");
95
+ const encoded = fs.readFile("protected.txt", { encoding: "utf8" });
96
+ assertType<typeof bytes, Task<Uint8Array, FsError>>();
97
+ assertType<typeof text, Task<string, FsError>>();
98
+ assertType<typeof encoded, Task<string, FsError>>();
99
+
100
+ assertErr(await run(bytes), error);
101
+ assertErr(await run(text), error);
102
+ assertErr(await run(encoded), error);
103
+ assertEqual(run.deps.reportDefect.getDefects(), []);
104
+ });
105
+ });