@evolu/common 8.7.0 → 8.8.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 (300) hide show
  1. package/dist/src/Array.d.ts +21 -21
  2. package/dist/src/Array.js +5 -5
  3. package/dist/src/Assert.d.ts +478 -141
  4. package/dist/src/Assert.d.ts.map +1 -1
  5. package/dist/src/Assert.js +644 -99
  6. package/dist/src/{Buffer.d.ts → Binary.d.ts} +85 -12
  7. package/dist/src/Binary.d.ts.map +1 -0
  8. package/dist/src/{Buffer.js → Binary.js} +206 -24
  9. package/dist/src/Console.d.ts +2 -10
  10. package/dist/src/Console.d.ts.map +1 -1
  11. package/dist/src/Console.js +6 -12
  12. package/dist/src/Crypto.d.ts +5 -5
  13. package/dist/src/Crypto.js +1 -1
  14. package/dist/src/Eq.d.ts +35 -40
  15. package/dist/src/Eq.d.ts.map +1 -1
  16. package/dist/src/Eq.js +51 -87
  17. package/dist/src/Function.d.ts +9 -14
  18. package/dist/src/Function.d.ts.map +1 -1
  19. package/dist/src/Function.js +7 -9
  20. package/dist/src/Http.d.ts +6 -7
  21. package/dist/src/Http.d.ts.map +1 -1
  22. package/dist/src/LeakDetector.d.ts.map +1 -1
  23. package/dist/src/LeakDetector.js +1 -0
  24. package/dist/src/LockManager.d.ts +1 -1
  25. package/dist/src/LockManager.js +1 -1
  26. package/dist/src/Lookup.d.ts +3 -2
  27. package/dist/src/Lookup.d.ts.map +1 -1
  28. package/dist/src/Lookup.js +4 -3
  29. package/dist/src/Number.d.ts +1 -1
  30. package/dist/src/Number.d.ts.map +1 -1
  31. package/dist/src/Number.js +1 -1
  32. package/dist/src/Object.d.ts +4 -4
  33. package/dist/src/Object.d.ts.map +1 -1
  34. package/dist/src/Object.js +5 -4
  35. package/dist/src/Option.d.ts +1 -2
  36. package/dist/src/Option.d.ts.map +1 -1
  37. package/dist/src/Option.js +1 -2
  38. package/dist/src/Platform.js +2 -0
  39. package/dist/src/Polyfills.js +4 -1
  40. package/dist/src/Random.d.ts +1 -1
  41. package/dist/src/Result.d.ts +49 -54
  42. package/dist/src/Result.d.ts.map +1 -1
  43. package/dist/src/Result.js +17 -22
  44. package/dist/src/Set.d.ts +6 -6
  45. package/dist/src/Set.js +2 -2
  46. package/dist/src/Sqlite.js +1 -1
  47. package/dist/src/String.js +3 -2
  48. package/dist/src/Task.d.ts +32 -32
  49. package/dist/src/Task.d.ts.map +1 -1
  50. package/dist/src/Task.js +20 -22
  51. package/dist/src/Test.d.ts +31 -3
  52. package/dist/src/Test.d.ts.map +1 -1
  53. package/dist/src/Test.js +46 -3
  54. package/dist/src/Time.d.ts +3 -11
  55. package/dist/src/Time.d.ts.map +1 -1
  56. package/dist/src/Time.js +10 -18
  57. package/dist/src/Type.d.ts +87 -85
  58. package/dist/src/Type.d.ts.map +1 -1
  59. package/dist/src/Type.js +70 -63
  60. package/dist/src/Types.d.ts +12 -12
  61. package/dist/src/Types.js +1 -1
  62. package/dist/src/WebSocket.d.ts +4 -7
  63. package/dist/src/WebSocket.d.ts.map +1 -1
  64. package/dist/src/Worker.d.ts +1 -1
  65. package/dist/src/Worker.js +7 -7
  66. package/dist/src/index.d.ts +1 -1
  67. package/dist/src/index.js +1 -1
  68. package/dist/src/intl/_en.d.ts.map +1 -1
  69. package/dist/src/intl/_en.js +1 -1
  70. package/dist/src/intl/ar.d.ts.map +1 -1
  71. package/dist/src/intl/ar.js +1 -1
  72. package/dist/src/intl/bn.d.ts.map +1 -1
  73. package/dist/src/intl/bn.js +1 -1
  74. package/dist/src/intl/ca.d.ts.map +1 -1
  75. package/dist/src/intl/ca.js +1 -1
  76. package/dist/src/intl/cs.d.ts.map +1 -1
  77. package/dist/src/intl/cs.js +1 -1
  78. package/dist/src/intl/da.d.ts.map +1 -1
  79. package/dist/src/intl/da.js +1 -1
  80. package/dist/src/intl/de.d.ts.map +1 -1
  81. package/dist/src/intl/de.js +1 -1
  82. package/dist/src/intl/el.d.ts.map +1 -1
  83. package/dist/src/intl/el.js +1 -1
  84. package/dist/src/intl/es.d.ts.map +1 -1
  85. package/dist/src/intl/es.js +1 -1
  86. package/dist/src/intl/fa.d.ts.map +1 -1
  87. package/dist/src/intl/fa.js +1 -1
  88. package/dist/src/intl/fi.d.ts.map +1 -1
  89. package/dist/src/intl/fi.js +1 -1
  90. package/dist/src/intl/fil.d.ts.map +1 -1
  91. package/dist/src/intl/fil.js +1 -1
  92. package/dist/src/intl/fr.d.ts.map +1 -1
  93. package/dist/src/intl/fr.js +1 -1
  94. package/dist/src/intl/he.d.ts.map +1 -1
  95. package/dist/src/intl/he.js +1 -1
  96. package/dist/src/intl/hi.d.ts.map +1 -1
  97. package/dist/src/intl/hi.js +1 -1
  98. package/dist/src/intl/hr.d.ts.map +1 -1
  99. package/dist/src/intl/hr.js +1 -1
  100. package/dist/src/intl/hu.d.ts.map +1 -1
  101. package/dist/src/intl/hu.js +1 -1
  102. package/dist/src/intl/id.d.ts.map +1 -1
  103. package/dist/src/intl/id.js +1 -1
  104. package/dist/src/intl/it.d.ts.map +1 -1
  105. package/dist/src/intl/it.js +1 -1
  106. package/dist/src/intl/ja.d.ts.map +1 -1
  107. package/dist/src/intl/ja.js +1 -1
  108. package/dist/src/intl/ko.d.ts.map +1 -1
  109. package/dist/src/intl/ko.js +1 -1
  110. package/dist/src/intl/ml.d.ts.map +1 -1
  111. package/dist/src/intl/ml.js +1 -1
  112. package/dist/src/intl/mr.d.ts.map +1 -1
  113. package/dist/src/intl/mr.js +1 -1
  114. package/dist/src/intl/ms.d.ts.map +1 -1
  115. package/dist/src/intl/ms.js +1 -1
  116. package/dist/src/intl/nb.d.ts.map +1 -1
  117. package/dist/src/intl/nb.js +1 -1
  118. package/dist/src/intl/nl.d.ts.map +1 -1
  119. package/dist/src/intl/nl.js +1 -1
  120. package/dist/src/intl/pa.d.ts.map +1 -1
  121. package/dist/src/intl/pa.js +1 -1
  122. package/dist/src/intl/pl.d.ts.map +1 -1
  123. package/dist/src/intl/pl.js +1 -1
  124. package/dist/src/intl/pt-BR.d.ts.map +1 -1
  125. package/dist/src/intl/pt-BR.js +1 -1
  126. package/dist/src/intl/pt.d.ts.map +1 -1
  127. package/dist/src/intl/pt.js +1 -1
  128. package/dist/src/intl/ro.d.ts.map +1 -1
  129. package/dist/src/intl/ro.js +1 -1
  130. package/dist/src/intl/sk.d.ts.map +1 -1
  131. package/dist/src/intl/sk.js +1 -1
  132. package/dist/src/intl/sl.d.ts.map +1 -1
  133. package/dist/src/intl/sl.js +1 -1
  134. package/dist/src/intl/sv.d.ts.map +1 -1
  135. package/dist/src/intl/sv.js +1 -1
  136. package/dist/src/intl/sw.d.ts.map +1 -1
  137. package/dist/src/intl/sw.js +1 -1
  138. package/dist/src/intl/ta.d.ts.map +1 -1
  139. package/dist/src/intl/ta.js +1 -1
  140. package/dist/src/intl/te.d.ts.map +1 -1
  141. package/dist/src/intl/te.js +1 -1
  142. package/dist/src/intl/th.d.ts.map +1 -1
  143. package/dist/src/intl/th.js +1 -1
  144. package/dist/src/intl/tr.d.ts.map +1 -1
  145. package/dist/src/intl/tr.js +1 -1
  146. package/dist/src/intl/uk.d.ts.map +1 -1
  147. package/dist/src/intl/uk.js +1 -1
  148. package/dist/src/intl/ur.d.ts.map +1 -1
  149. package/dist/src/intl/ur.js +1 -1
  150. package/dist/src/intl/vi.d.ts.map +1 -1
  151. package/dist/src/intl/vi.js +1 -1
  152. package/dist/src/intl/zh-CN.d.ts.map +1 -1
  153. package/dist/src/intl/zh-CN.js +1 -1
  154. package/dist/src/intl/zh-TW.d.ts.map +1 -1
  155. package/dist/src/intl/zh-TW.js +1 -1
  156. package/dist/src/local-first/Evolu.d.ts +12 -12
  157. package/dist/src/local-first/Evolu.d.ts.map +1 -1
  158. package/dist/src/local-first/Evolu.js +2 -2
  159. package/dist/src/local-first/Protocol.d.ts +14 -76
  160. package/dist/src/local-first/Protocol.d.ts.map +1 -1
  161. package/dist/src/local-first/Protocol.js +29 -206
  162. package/dist/src/local-first/Query.d.ts +11 -11
  163. package/dist/src/local-first/Query.js +6 -6
  164. package/dist/src/local-first/Relay.d.ts +2 -2
  165. package/dist/src/local-first/Schema.d.ts +3 -5
  166. package/dist/src/local-first/Schema.d.ts.map +1 -1
  167. package/dist/src/local-first/Schema.js +2 -4
  168. package/dist/src/local-first/Shared.d.ts.map +1 -1
  169. package/dist/src/local-first/Shared.js +5 -5
  170. package/dist/src/local-first/Storage.js +4 -4
  171. package/dist/src/local-first/Timestamp.d.ts +9 -0
  172. package/dist/src/local-first/Timestamp.d.ts.map +1 -1
  173. package/dist/src/local-first/Timestamp.js +13 -10
  174. package/package.json +7 -4
  175. package/src/Array.test.ts +809 -0
  176. package/src/Array.ts +21 -21
  177. package/src/Assert.test.ts +1181 -0
  178. package/src/Assert.ts +971 -227
  179. package/src/BigInt.test.ts +28 -0
  180. package/src/Binary.test.ts +1128 -0
  181. package/src/{Buffer.ts → Binary.ts} +273 -46
  182. package/src/Brand.test.ts +90 -0
  183. package/src/Cache.test.ts +183 -0
  184. package/src/Callbacks.test.ts +66 -0
  185. package/src/Console.test.ts +809 -0
  186. package/src/Console.ts +9 -17
  187. package/src/Crypto.test.ts +138 -0
  188. package/src/Crypto.ts +5 -5
  189. package/src/Eq.test.ts +333 -0
  190. package/src/Eq.ts +110 -176
  191. package/src/Error.test.ts +118 -0
  192. package/src/Function.test.ts +204 -0
  193. package/src/Function.ts +10 -15
  194. package/src/Http.test.ts +816 -0
  195. package/src/Http.ts +6 -7
  196. package/src/LeakDetector.test.ts +210 -0
  197. package/src/LeakDetector.ts +1 -0
  198. package/src/LockManager.ts +2 -2
  199. package/src/Lookup.test.ts +418 -0
  200. package/src/Lookup.ts +4 -3
  201. package/src/Microtask.test.ts +140 -0
  202. package/src/Number.test.ts +218 -0
  203. package/src/Number.ts +2 -2
  204. package/src/Object.test.ts +293 -0
  205. package/src/Object.ts +5 -4
  206. package/src/Option.test.ts +104 -0
  207. package/src/Option.ts +1 -2
  208. package/src/Order.test.ts +56 -0
  209. package/src/Platform.ts +2 -0
  210. package/src/Polyfills.ts +3 -4
  211. package/src/Random.test.ts +89 -0
  212. package/src/Random.ts +1 -1
  213. package/src/Ref.test.ts +139 -0
  214. package/src/RefCount.test.ts +184 -0
  215. package/src/Relation.test.ts +505 -0
  216. package/src/Resource.test.ts +4623 -0
  217. package/src/Result.test.ts +1829 -0
  218. package/src/Result.ts +49 -54
  219. package/src/Schedule.test.ts +1554 -0
  220. package/src/Set.test.ts +279 -0
  221. package/src/Set.ts +6 -6
  222. package/src/Sqlite.ts +1 -1
  223. package/src/Store.test.ts +288 -0
  224. package/src/String.test.ts +54 -0
  225. package/src/String.ts +3 -3
  226. package/src/Task.test.ts +10820 -0
  227. package/src/Task.ts +38 -43
  228. package/src/Test.test.ts +88 -0
  229. package/src/Test.ts +49 -3
  230. package/src/Time.test.ts +926 -0
  231. package/src/Time.ts +13 -21
  232. package/src/Type.base64.test.ts +88 -0
  233. package/src/Type.test.ts +20465 -0
  234. package/src/Type.ts +114 -111
  235. package/src/Types.test.ts +324 -0
  236. package/src/Types.ts +13 -13
  237. package/src/WebSocket.ts +4 -7
  238. package/src/Worker.test.ts +755 -0
  239. package/src/Worker.ts +13 -18
  240. package/src/index.ts +1 -1
  241. package/src/intl/_en.ts +1 -1
  242. package/src/intl/ar.ts +1 -1
  243. package/src/intl/bn.ts +1 -1
  244. package/src/intl/ca.ts +1 -1
  245. package/src/intl/cs.ts +1 -1
  246. package/src/intl/da.ts +1 -1
  247. package/src/intl/de.ts +1 -1
  248. package/src/intl/el.ts +1 -1
  249. package/src/intl/es.ts +1 -1
  250. package/src/intl/fa.ts +1 -1
  251. package/src/intl/fi.ts +1 -1
  252. package/src/intl/fil.ts +1 -1
  253. package/src/intl/fr.ts +1 -1
  254. package/src/intl/he.ts +1 -1
  255. package/src/intl/hi.ts +1 -1
  256. package/src/intl/hr.ts +1 -1
  257. package/src/intl/hu.ts +1 -1
  258. package/src/intl/id.ts +1 -1
  259. package/src/intl/intl.test.ts +3106 -0
  260. package/src/intl/it.ts +1 -1
  261. package/src/intl/ja.ts +1 -1
  262. package/src/intl/ko.ts +1 -1
  263. package/src/intl/ml.ts +1 -1
  264. package/src/intl/mr.ts +1 -1
  265. package/src/intl/ms.ts +1 -1
  266. package/src/intl/nb.ts +1 -1
  267. package/src/intl/nl.ts +1 -1
  268. package/src/intl/pa.ts +1 -1
  269. package/src/intl/pl.ts +1 -1
  270. package/src/intl/pt-BR.ts +1 -1
  271. package/src/intl/pt.ts +1 -1
  272. package/src/intl/ro.ts +1 -1
  273. package/src/intl/sk.ts +1 -1
  274. package/src/intl/sl.ts +1 -1
  275. package/src/intl/sv.ts +1 -1
  276. package/src/intl/sw.ts +1 -1
  277. package/src/intl/ta.ts +1 -1
  278. package/src/intl/te.ts +1 -1
  279. package/src/intl/th.ts +1 -1
  280. package/src/intl/tr.ts +1 -1
  281. package/src/intl/uk.ts +1 -1
  282. package/src/intl/ur.ts +1 -1
  283. package/src/intl/vi.ts +1 -1
  284. package/src/intl/zh-CN.ts +1 -1
  285. package/src/intl/zh-TW.ts +1 -1
  286. package/src/local-first/Evolu.test.ts +1921 -0
  287. package/src/local-first/Evolu.ts +18 -14
  288. package/src/local-first/Owner.test.ts +83 -0
  289. package/src/local-first/Protocol.test.ts +1037 -0
  290. package/src/local-first/Protocol.ts +58 -261
  291. package/src/local-first/Query.test.ts +349 -0
  292. package/src/local-first/Query.ts +11 -11
  293. package/src/local-first/Relay.ts +2 -2
  294. package/src/local-first/Schema.ts +3 -5
  295. package/src/local-first/Shared.test.ts +1828 -0
  296. package/src/local-first/Shared.ts +9 -8
  297. package/src/local-first/Storage.ts +4 -4
  298. package/src/local-first/Timestamp.test.ts +374 -0
  299. package/src/local-first/Timestamp.ts +22 -10
  300. package/dist/src/Buffer.d.ts.map +0 -1
@@ -0,0 +1,816 @@
1
+ import { describe, it } from "node:test";
2
+ import {
3
+ assertEqual,
4
+ assertErr,
5
+ assertFalse,
6
+ assertInstanceOf,
7
+ assertOk,
8
+ assertRejects,
9
+ assertSame,
10
+ assertTrue,
11
+ } from "./Assert.ts";
12
+
13
+ import {
14
+ fetch,
15
+ testCreateNativeFetchErroringBody,
16
+ testCreateNativeFetch,
17
+ type FetchError,
18
+ type FetchResponse,
19
+ type FetchTransportError,
20
+ } from "./Http.ts";
21
+ import { err, ok, type Result } from "./Result.ts";
22
+ import {
23
+ createAbortError,
24
+ createPanicAbortReason,
25
+ testAbortError,
26
+ testAbortReason,
27
+ testCreateRun,
28
+ type Task,
29
+ } from "./Task.ts";
30
+ import { assertType } from "./Type.ts";
31
+
32
+ describe("fetch", () => {
33
+ it("uses deps.nativeFetch with init and the Task Run signal", async () => {
34
+ const headers = new Headers({ accept: "text/plain" });
35
+ const nativeFetch = testCreateNativeFetch(() => new Response("hello"));
36
+ await using run = testCreateRun({ nativeFetch });
37
+
38
+ const fiber = run(fetch("/hello", "text", { headers }));
39
+ const result = await fiber;
40
+
41
+ assertOk(result, "hello");
42
+ assertEqual(nativeFetch.calls, [
43
+ {
44
+ input: "/hello",
45
+ init: { headers, signal: fiber.run.signal },
46
+ },
47
+ ]);
48
+ });
49
+
50
+ it("returns FetchTransportError when the test default nativeFetch is not replaced", async () => {
51
+ await using run = testCreateRun();
52
+
53
+ const result = await run(fetch("/missing-test-double", "text"));
54
+
55
+ assertErr(result);
56
+ assertSame(result.error.type, "FetchTransportError");
57
+ assertInstanceOf(result.error.error, Error);
58
+ assertEqual(
59
+ result.error.error.message,
60
+ "Provide a nativeFetch test double",
61
+ );
62
+ });
63
+
64
+ it("returns FetchTransportError when test nativeFetch has no queued handler", async () => {
65
+ const nativeFetch = testCreateNativeFetch();
66
+ await using run = testCreateRun({ nativeFetch });
67
+
68
+ const result = await run(fetch("/unexpected", "text"));
69
+
70
+ assertErr(result);
71
+ assertSame(result.error.type, "FetchTransportError");
72
+ assertInstanceOf(result.error.error, Error);
73
+ assertEqual(result.error.error.message, "Unexpected NativeFetch call");
74
+ });
75
+
76
+ it("test nativeFetch queues handlers after creation", async () => {
77
+ const nativeFetch = testCreateNativeFetch();
78
+ nativeFetch.handle(() => new Response("hello"));
79
+ await using run = testCreateRun({ nativeFetch });
80
+
81
+ const result = await run(fetch("/queued", "text"));
82
+
83
+ assertOk(result, "hello");
84
+ });
85
+
86
+ it("returns FetchTransportError when nativeFetch rejects asynchronously", async () => {
87
+ const failure = new Error("network failed");
88
+ const nativeFetch = testCreateNativeFetch(() => Promise.reject(failure));
89
+ await using run = testCreateRun({ nativeFetch });
90
+
91
+ const result = await run(fetch("/network-failure", "text"));
92
+
93
+ assertErr(result, {
94
+ type: "FetchTransportError",
95
+ error: failure,
96
+ });
97
+ });
98
+
99
+ it("infers overload result types", () => {
100
+ interface TestError {
101
+ readonly type: "TestError";
102
+ }
103
+
104
+ {
105
+ const actual = fetch("/text", "text");
106
+ assertType<typeof actual, Task<string, FetchError>>();
107
+ }
108
+ {
109
+ const actual = fetch("/json", "json");
110
+ assertType<typeof actual, Task<unknown, FetchError>>();
111
+ }
112
+ {
113
+ const actual = fetch("/bytes", "bytes");
114
+ assertType<typeof actual, Task<Uint8Array<ArrayBuffer>, FetchError>>();
115
+ }
116
+ {
117
+ const actual = fetch("/headers", "headers");
118
+ assertType<typeof actual, Task<FetchResponse, FetchTransportError>>();
119
+ }
120
+ {
121
+ const actual = fetch("/consumer", () => ok("value"));
122
+ assertType<typeof actual, Task<string, FetchTransportError>>();
123
+ }
124
+ {
125
+ const actual = fetch("/consumer-error", (): Result<string, TestError> =>
126
+ err({ type: "TestError" }),
127
+ );
128
+ assertType<
129
+ typeof actual,
130
+ Task<string, FetchTransportError | TestError>
131
+ >();
132
+ }
133
+ });
134
+
135
+ describe("body modes", () => {
136
+ describe("2xx response returns", () => {
137
+ it("text as string", async () => {
138
+ const nativeFetch = testCreateNativeFetch(() => new Response("hello"));
139
+ await using run = testCreateRun({ nativeFetch });
140
+
141
+ const result = await run(fetch("/hello", "text"));
142
+
143
+ assertOk(result, "hello");
144
+ });
145
+
146
+ it("json as unknown", async () => {
147
+ const nativeFetch = testCreateNativeFetch(
148
+ () => new Response('{"name":"Ada"}'),
149
+ );
150
+ await using run = testCreateRun({ nativeFetch });
151
+
152
+ const result = await run(fetch("/user", "json"));
153
+
154
+ assertOk(result, { name: "Ada" });
155
+ });
156
+
157
+ it("bytes as Uint8Array", async () => {
158
+ const nativeFetch = testCreateNativeFetch(
159
+ () => new Response(new Uint8Array([1, 2, 3])),
160
+ );
161
+ await using run = testCreateRun({ nativeFetch });
162
+
163
+ const result = await run(fetch("/bytes", "bytes"));
164
+
165
+ assertOk(result, new Uint8Array([1, 2, 3]));
166
+ });
167
+ });
168
+
169
+ describe("non-2xx response returns FetchStatusError", () => {
170
+ it("drains body as text", async () => {
171
+ const response = new Response("denied", {
172
+ status: 404,
173
+ statusText: "Not Found",
174
+ });
175
+ const nativeFetch = testCreateNativeFetch(() => response);
176
+ await using run = testCreateRun({ nativeFetch });
177
+
178
+ const result = await run(fetch("/missing", "text"));
179
+
180
+ assertErr(result, {
181
+ type: "FetchStatusError",
182
+ response,
183
+ body: ok("denied"),
184
+ });
185
+ });
186
+
187
+ it("json does not attempt JSON parsing", async () => {
188
+ const response = Object.assign(
189
+ new Response("denied", {
190
+ status: 404,
191
+ statusText: "Not Found",
192
+ }),
193
+ {
194
+ json: () => Promise.reject(new Error("json reader was called")),
195
+ },
196
+ );
197
+ const nativeFetch = testCreateNativeFetch(() => response);
198
+ await using run = testCreateRun({ nativeFetch });
199
+
200
+ const result = await run(fetch("/missing", "json"));
201
+
202
+ assertErr(result, {
203
+ type: "FetchStatusError",
204
+ response,
205
+ body: ok("denied"),
206
+ });
207
+ });
208
+
209
+ it("stores body read error from text drain", async () => {
210
+ const failure = new Error("stream failed");
211
+ const response = Object.assign(
212
+ new Response(null, {
213
+ status: 500,
214
+ statusText: "Internal Server Error",
215
+ }),
216
+ { text: () => Promise.reject(failure) },
217
+ );
218
+ const nativeFetch = testCreateNativeFetch(() => response);
219
+ await using run = testCreateRun({ nativeFetch });
220
+
221
+ const result = await run(fetch("/broken", "text"));
222
+
223
+ assertErr(result, {
224
+ type: "FetchStatusError",
225
+ response,
226
+ body: err(failure),
227
+ });
228
+ });
229
+ });
230
+
231
+ describe("2xx response returns FetchBodyError for", () => {
232
+ it("text body read failure", async () => {
233
+ const failure = new Error("stream failed");
234
+ const response = new Response(
235
+ testCreateNativeFetchErroringBody(failure),
236
+ );
237
+ const nativeFetch = testCreateNativeFetch(() => response);
238
+ await using run = testCreateRun({ nativeFetch });
239
+
240
+ const result = await run(fetch("/broken", "text"));
241
+
242
+ assertErr(result);
243
+ assertSame(result.error.type, "FetchBodyError");
244
+ assertSame(result.error.response, response);
245
+ assertSame(result.error.error, failure);
246
+ });
247
+
248
+ it("json invalid JSON", async () => {
249
+ const response = new Response("not json");
250
+ const nativeFetch = testCreateNativeFetch(() => response);
251
+ await using run = testCreateRun({ nativeFetch });
252
+
253
+ const result = await run(fetch("/invalid", "json"));
254
+
255
+ assertErr(result);
256
+ assertSame(result.error.type, "FetchBodyError");
257
+ assertSame(result.error.response, response);
258
+ assertInstanceOf(result.error.error, SyntaxError);
259
+ });
260
+
261
+ it("json empty body such as 204", async () => {
262
+ const response = new Response(null, { status: 204 });
263
+ const nativeFetch = testCreateNativeFetch(() => response);
264
+ await using run = testCreateRun({ nativeFetch });
265
+
266
+ const result = await run(fetch("/empty", "json"));
267
+
268
+ assertErr(result);
269
+ assertSame(result.error.type, "FetchBodyError");
270
+ assertSame(result.error.response, response);
271
+ assertInstanceOf(result.error.error, SyntaxError);
272
+ });
273
+
274
+ it("bytes body read failure", async () => {
275
+ const failure = new Error("stream failed");
276
+ const response = Object.assign(new Response(), {
277
+ bytes: () => Promise.reject(failure),
278
+ });
279
+ const nativeFetch = testCreateNativeFetch(() => response);
280
+ await using run = testCreateRun({ nativeFetch });
281
+
282
+ const result = await run(fetch("/broken", "bytes"));
283
+
284
+ assertErr(result, {
285
+ type: "FetchBodyError",
286
+ response,
287
+ error: failure,
288
+ });
289
+ });
290
+ });
291
+ });
292
+
293
+ describe("headers mode", () => {
294
+ it("returns plain response metadata", async () => {
295
+ const response = new Response(null, {
296
+ status: 204,
297
+ statusText: "No Content",
298
+ headers: { etag: "abc" },
299
+ });
300
+ const nativeFetch = testCreateNativeFetch(() => response);
301
+ await using run = testCreateRun({ nativeFetch });
302
+
303
+ const result = await run(fetch("/metadata", "headers"));
304
+
305
+ assertOk(result, response);
306
+ assertEqual(result.value.status, 204);
307
+ assertEqual(result.value.statusText, "No Content");
308
+ assertEqual(result.value.headers.get("etag"), "abc");
309
+ });
310
+
311
+ it("does not judge status: non-2xx resolves with metadata", async () => {
312
+ const response = new Response("denied", {
313
+ status: 404,
314
+ statusText: "Not Found",
315
+ });
316
+ const nativeFetch = testCreateNativeFetch(() => response);
317
+ await using run = testCreateRun({ nativeFetch });
318
+
319
+ const result = await run(fetch("/missing", "headers"));
320
+
321
+ assertOk(result, response);
322
+ });
323
+
324
+ it("cancels the body before headers mode resolves", async () => {
325
+ let wasCancelled = false;
326
+ const response = new Response(
327
+ new ReadableStream<Uint8Array>({
328
+ cancel: () => {
329
+ wasCancelled = true;
330
+ },
331
+ }),
332
+ );
333
+ const nativeFetch = testCreateNativeFetch(() => response);
334
+ await using run = testCreateRun({ nativeFetch });
335
+
336
+ const result = await run(fetch("/body", "headers"));
337
+
338
+ assertOk(result, response);
339
+ assertTrue(wasCancelled);
340
+ });
341
+
342
+ it("a body cancel failure does not fail headers mode", async () => {
343
+ const response = new Response(
344
+ new ReadableStream<Uint8Array>({
345
+ cancel: () => {
346
+ throw new Error("cancel failed");
347
+ },
348
+ }),
349
+ );
350
+ const nativeFetch = testCreateNativeFetch(() => response);
351
+ await using run = testCreateRun({ nativeFetch });
352
+
353
+ const result = await run(fetch("/metadata", "headers"));
354
+
355
+ assertOk(result, response);
356
+ });
357
+ });
358
+
359
+ describe("consumer", () => {
360
+ it("receives the Response and returns plain values", async () => {
361
+ const response = new Response("hello", {
362
+ status: 201,
363
+ headers: { "cache-control": "max-age=60" },
364
+ });
365
+ const nativeFetch = testCreateNativeFetch(() => response);
366
+ await using run = testCreateRun({ nativeFetch });
367
+
368
+ const result = await run(
369
+ fetch("/metadata", (response) =>
370
+ ok({
371
+ status: response.status,
372
+ cacheControl: response.headers.get("cache-control"),
373
+ }),
374
+ ),
375
+ );
376
+
377
+ assertOk(result, {
378
+ status: 201,
379
+ cacheControl: "max-age=60",
380
+ });
381
+ });
382
+
383
+ it("keeps native status semantics: non-2xx resolves into the consumer", async () => {
384
+ const response = new Response("denied", {
385
+ status: 404,
386
+ statusText: "Not Found",
387
+ });
388
+ const nativeFetch = testCreateNativeFetch(() => response);
389
+ await using run = testCreateRun({ nativeFetch });
390
+
391
+ const result = await run(
392
+ fetch("/missing", (response) =>
393
+ ok({
394
+ status: response.status,
395
+ text: response.statusText,
396
+ }),
397
+ ),
398
+ );
399
+
400
+ assertOk(result, {
401
+ status: 404,
402
+ text: "Not Found",
403
+ });
404
+ });
405
+
406
+ it("passes a returned Result error through unchanged", async () => {
407
+ const testError = { type: "TestError" } as const;
408
+ const nativeFetch = testCreateNativeFetch(() => new Response("hello"));
409
+ await using run = testCreateRun({ nativeFetch });
410
+
411
+ const result = await run(fetch("/domain-error", () => err(testError)));
412
+
413
+ assertErr(result, testError);
414
+ });
415
+
416
+ it("a throw outside abort is a defect that panics the Run tree", async () => {
417
+ const defect = new Error("consumer failed");
418
+ const panicAbortError = createAbortError(createPanicAbortReason(defect));
419
+ const nativeFetch = testCreateNativeFetch(() => new Response("hello"));
420
+ await using run = testCreateRun({ nativeFetch });
421
+
422
+ await assertRejects(
423
+ run(
424
+ fetch("/defect", () => {
425
+ throw defect;
426
+ }),
427
+ ),
428
+ panicAbortError,
429
+ );
430
+ assertEqual(await run.deps.reportDefect.next(), panicAbortError);
431
+ });
432
+
433
+ it("an Ok returned after abort is kept", async () => {
434
+ const consumerStarted = Promise.withResolvers<void>();
435
+ const continueConsumer = Promise.withResolvers<void>();
436
+ const nativeFetch = testCreateNativeFetch(() => new Response("hello"));
437
+ await using run = testCreateRun({ nativeFetch });
438
+
439
+ const fiber = run.abortable(
440
+ fetch("/abort", async () => {
441
+ consumerStarted.resolve();
442
+ await continueConsumer.promise;
443
+ return ok("value");
444
+ }),
445
+ );
446
+
447
+ await consumerStarted.promise;
448
+ fiber.abort();
449
+ continueConsumer.resolve();
450
+
451
+ assertEqual(await fiber, ok("value"));
452
+ });
453
+ });
454
+
455
+ describe("abort", () => {
456
+ describe("Run AbortError", () => {
457
+ it("rethrows signal.reason when a body read rejects with the Run AbortError", async () => {
458
+ const bodyReadStarted = Promise.withResolvers<void>();
459
+ const continueBodyRead = Promise.withResolvers<void>();
460
+ const nativeFetch = testCreateNativeFetch((_input, init) => {
461
+ const signal = init?.signal;
462
+ if (!signal) throw new Error("Missing signal");
463
+
464
+ return Object.assign(new Response("hello"), {
465
+ text: async () => {
466
+ bodyReadStarted.resolve();
467
+ await continueBodyRead.promise;
468
+ throw signal.reason;
469
+ },
470
+ });
471
+ });
472
+ await using run = testCreateRun({ nativeFetch });
473
+
474
+ const fiber = run.abortable(fetch("/abort", "text"));
475
+
476
+ await bodyReadStarted.promise;
477
+ fiber.abort(testAbortReason);
478
+ continueBodyRead.resolve();
479
+
480
+ assertEqual(await fiber, err(testAbortError));
481
+ });
482
+
483
+ it("run(fetch) rejects with AbortError when the request aborts before response", async () => {
484
+ const nativeFetchStarted = Promise.withResolvers<void>();
485
+ const nativeFetch = testCreateNativeFetch(
486
+ (_input, init) =>
487
+ new Promise<Response>((_resolve, reject) => {
488
+ init?.signal?.addEventListener(
489
+ "abort",
490
+ () => {
491
+ // oxlint-disable-next-line typescript/prefer-promise-reject-errors -- Fetch can reject with Task's structured AbortError.
492
+ reject(init.signal?.reason);
493
+ },
494
+ { once: true },
495
+ );
496
+ nativeFetchStarted.resolve();
497
+ }),
498
+ );
499
+ await using root = testCreateRun({ nativeFetch });
500
+ await using run = root.create();
501
+
502
+ const fiber = run(fetch("/abort", "text"));
503
+
504
+ await nativeFetchStarted.promise;
505
+ run.abort(testAbortReason);
506
+
507
+ await assertRejects(fiber, testAbortError);
508
+ });
509
+
510
+ it("run.abortable returns AbortError as a Result error", async () => {
511
+ const nativeFetchStarted = Promise.withResolvers<void>();
512
+ const nativeFetch = testCreateNativeFetch(
513
+ (_input, init) =>
514
+ new Promise<Response>((_resolve, reject) => {
515
+ init?.signal?.addEventListener(
516
+ "abort",
517
+ () => {
518
+ // oxlint-disable-next-line typescript/prefer-promise-reject-errors -- Fetch can reject with Task's structured AbortError.
519
+ reject(init.signal?.reason);
520
+ },
521
+ { once: true },
522
+ );
523
+ nativeFetchStarted.resolve();
524
+ }),
525
+ );
526
+ await using run = testCreateRun({ nativeFetch });
527
+
528
+ const fiber = run.abortable(fetch("/abort", "text"));
529
+
530
+ await nativeFetchStarted.promise;
531
+ fiber.abort(testAbortReason);
532
+
533
+ assertEqual(await fiber, err(testAbortError));
534
+ });
535
+
536
+ it("a consumer is not invoked when abort precedes the response", async () => {
537
+ const nativeFetchStarted = Promise.withResolvers<void>();
538
+ const responseDeferred = Promise.withResolvers<Response>();
539
+ let wasConsumerInvoked = false;
540
+ const nativeFetch = testCreateNativeFetch(() => {
541
+ nativeFetchStarted.resolve();
542
+ return responseDeferred.promise;
543
+ });
544
+ await using run = testCreateRun({ nativeFetch });
545
+
546
+ const fiber = run.abortable(
547
+ fetch("/abort", () => {
548
+ wasConsumerInvoked = true;
549
+ return ok("value");
550
+ }),
551
+ );
552
+
553
+ await nativeFetchStarted.promise;
554
+ fiber.abort(testAbortReason);
555
+ responseDeferred.resolve(new Response("hello"));
556
+
557
+ assertEqual(await fiber, err(testAbortError));
558
+ assertFalse(wasConsumerInvoked);
559
+ });
560
+
561
+ it("a response arriving after abort is cancelled and abort wins", async () => {
562
+ const nativeFetchStarted = Promise.withResolvers<void>();
563
+ const responseDeferred = Promise.withResolvers<Response>();
564
+ let wasCancelled = false;
565
+ const nativeFetch = testCreateNativeFetch(() => {
566
+ nativeFetchStarted.resolve();
567
+ return responseDeferred.promise;
568
+ });
569
+ await using run = testCreateRun({ nativeFetch });
570
+
571
+ const fiber = run.abortable(fetch("/abort", "text"));
572
+
573
+ await nativeFetchStarted.promise;
574
+ fiber.abort(testAbortReason);
575
+ responseDeferred.resolve(
576
+ new Response(
577
+ new ReadableStream<Uint8Array>({
578
+ cancel: () => {
579
+ wasCancelled = true;
580
+ },
581
+ }),
582
+ ),
583
+ );
584
+
585
+ assertEqual(await fiber, err(testAbortError));
586
+ assertTrue(wasCancelled);
587
+ });
588
+
589
+ it("rethrows signal.reason from a status-body drain after abort", async () => {
590
+ const bodyDrainStarted = Promise.withResolvers<void>();
591
+ const continueBodyDrain = Promise.withResolvers<void>();
592
+ const nativeFetch = testCreateNativeFetch((_input, init) => {
593
+ const signal = init?.signal;
594
+ if (!signal) throw new Error("Missing signal");
595
+
596
+ return Object.assign(new Response(null, { status: 500 }), {
597
+ text: async () => {
598
+ bodyDrainStarted.resolve();
599
+ await continueBodyDrain.promise;
600
+ throw signal.reason;
601
+ },
602
+ });
603
+ });
604
+ await using run = testCreateRun({ nativeFetch });
605
+
606
+ const fiber = run.abortable(fetch("/abort", "text"));
607
+
608
+ await bodyDrainStarted.promise;
609
+ fiber.abort(testAbortReason);
610
+ continueBodyDrain.resolve();
611
+
612
+ assertEqual(await fiber, err(testAbortError));
613
+ });
614
+
615
+ it("rethrows signal.reason from headers-mode body cancel after abort", async () => {
616
+ const bodyCancelStarted = Promise.withResolvers<void>();
617
+ const continueBodyCancel = Promise.withResolvers<void>();
618
+ const nativeFetch = testCreateNativeFetch((_input, init) => {
619
+ const signal = init?.signal;
620
+ if (!signal) throw new Error("Missing signal");
621
+
622
+ return new Response(
623
+ new ReadableStream<Uint8Array>({
624
+ cancel: async () => {
625
+ bodyCancelStarted.resolve();
626
+ await continueBodyCancel.promise;
627
+ throw signal.reason;
628
+ },
629
+ }),
630
+ );
631
+ });
632
+ await using run = testCreateRun({ nativeFetch });
633
+
634
+ const fiber = run.abortable(fetch("/abort", "headers"));
635
+
636
+ await bodyCancelStarted.promise;
637
+ fiber.abort(testAbortReason);
638
+ continueBodyCancel.resolve();
639
+
640
+ assertEqual(await fiber, err(testAbortError));
641
+ });
642
+
643
+ it("rethrows the Run AbortError thrown by a consumer", async () => {
644
+ const consumerStarted = Promise.withResolvers<void>();
645
+ const continueConsumer = Promise.withResolvers<void>();
646
+ let signal: AbortSignal | undefined;
647
+ const nativeFetch = testCreateNativeFetch((_input, init) => {
648
+ signal = init?.signal ?? undefined;
649
+ return new Response("hello");
650
+ });
651
+ await using run = testCreateRun({ nativeFetch });
652
+
653
+ const fiber = run.abortable(
654
+ fetch("/abort", async () => {
655
+ consumerStarted.resolve();
656
+ await continueConsumer.promise;
657
+ if (!signal) throw new Error("Missing signal");
658
+ throw signal.reason;
659
+ }),
660
+ );
661
+
662
+ await consumerStarted.promise;
663
+ fiber.abort(testAbortReason);
664
+ continueConsumer.resolve();
665
+
666
+ assertEqual(await fiber, err(testAbortError));
667
+ });
668
+
669
+ it("an aborted status-body drain becomes AbortError, not FetchStatusError", async () => {
670
+ const bodyDrainStarted = Promise.withResolvers<void>();
671
+ const continueBodyDrain = Promise.withResolvers<void>();
672
+ const response = Object.assign(new Response(null, { status: 500 }), {
673
+ text: async () => {
674
+ bodyDrainStarted.resolve();
675
+ await continueBodyDrain.promise;
676
+ throw createHostAbortError();
677
+ },
678
+ });
679
+ const nativeFetch = testCreateNativeFetch(() => response);
680
+ await using run = testCreateRun({ nativeFetch });
681
+
682
+ const fiber = run.abortable(fetch("/abort", "text"));
683
+
684
+ await bodyDrainStarted.promise;
685
+ fiber.abort(testAbortReason);
686
+ continueBodyDrain.resolve();
687
+
688
+ assertEqual(await fiber, err(testAbortError));
689
+ });
690
+ });
691
+
692
+ describe("host abort normalization", () => {
693
+ it("normalizes a host abort error from native fetch", async () => {
694
+ const nativeFetchStarted = Promise.withResolvers<void>();
695
+ const nativeFetch = testCreateNativeFetch(
696
+ (_input, init) =>
697
+ new Promise<Response>((_resolve, reject) => {
698
+ init?.signal?.addEventListener(
699
+ "abort",
700
+ () => {
701
+ reject(createHostAbortError());
702
+ },
703
+ { once: true },
704
+ );
705
+ nativeFetchStarted.resolve();
706
+ }),
707
+ );
708
+ await using run = testCreateRun({ nativeFetch });
709
+
710
+ const fiber = run.abortable(fetch("/abort", "text"));
711
+
712
+ await nativeFetchStarted.promise;
713
+ fiber.abort(testAbortReason);
714
+
715
+ assertEqual(await fiber, err(testAbortError));
716
+ });
717
+
718
+ it("normalizes a host abort error from a mode body read", async () => {
719
+ const bodyReadStarted = Promise.withResolvers<void>();
720
+ const continueBodyRead = Promise.withResolvers<void>();
721
+ const response = Object.assign(new Response("hello"), {
722
+ text: async () => {
723
+ bodyReadStarted.resolve();
724
+ await continueBodyRead.promise;
725
+ throw createHostAbortError();
726
+ },
727
+ });
728
+ const nativeFetch = testCreateNativeFetch(() => response);
729
+ await using run = testCreateRun({ nativeFetch });
730
+
731
+ const fiber = run.abortable(fetch("/abort", "text"));
732
+
733
+ await bodyReadStarted.promise;
734
+ fiber.abort(testAbortReason);
735
+ continueBodyRead.resolve();
736
+
737
+ assertEqual(await fiber, err(testAbortError));
738
+ });
739
+
740
+ it("normalizes a host abort error from a headers-mode body cancel", async () => {
741
+ const bodyCancelStarted = Promise.withResolvers<void>();
742
+ const continueBodyCancel = Promise.withResolvers<void>();
743
+ const response = new Response(
744
+ new ReadableStream<Uint8Array>({
745
+ cancel: async () => {
746
+ bodyCancelStarted.resolve();
747
+ await continueBodyCancel.promise;
748
+ throw createHostAbortError();
749
+ },
750
+ }),
751
+ );
752
+ const nativeFetch = testCreateNativeFetch(() => response);
753
+ await using run = testCreateRun({ nativeFetch });
754
+
755
+ const fiber = run.abortable(fetch("/abort", "headers"));
756
+
757
+ await bodyCancelStarted.promise;
758
+ fiber.abort(testAbortReason);
759
+ continueBodyCancel.resolve();
760
+
761
+ assertEqual(await fiber, err(testAbortError));
762
+ });
763
+
764
+ it("normalizes a host abort error thrown by a consumer", async () => {
765
+ const consumerStarted = Promise.withResolvers<void>();
766
+ const continueConsumer = Promise.withResolvers<void>();
767
+ const nativeFetch = testCreateNativeFetch(() => new Response("hello"));
768
+ await using run = testCreateRun({ nativeFetch });
769
+
770
+ const fiber = run.abortable(
771
+ fetch("/abort", async () => {
772
+ consumerStarted.resolve();
773
+ await continueConsumer.promise;
774
+ throw createHostAbortError();
775
+ }),
776
+ );
777
+
778
+ await consumerStarted.promise;
779
+ fiber.abort(testAbortReason);
780
+ continueConsumer.resolve();
781
+
782
+ assertEqual(await fiber, err(testAbortError));
783
+ });
784
+ });
785
+
786
+ describe("consumer after abort", () => {
787
+ it("an Err returned by a consumer after abort becomes AbortError", async () => {
788
+ const testError = { type: "TestError" } as const;
789
+ const consumerStarted = Promise.withResolvers<void>();
790
+ const continueConsumer = Promise.withResolvers<void>();
791
+ const nativeFetch = testCreateNativeFetch(() => new Response("hello"));
792
+ await using run = testCreateRun({ nativeFetch });
793
+
794
+ const fiber = run.abortable(
795
+ fetch("/abort", async () => {
796
+ consumerStarted.resolve();
797
+ await continueConsumer.promise;
798
+ return err(testError);
799
+ }),
800
+ );
801
+
802
+ await consumerStarted.promise;
803
+ fiber.abort(testAbortReason);
804
+ continueConsumer.resolve();
805
+
806
+ assertEqual(await fiber, err(testAbortError));
807
+ });
808
+ });
809
+ });
810
+ });
811
+
812
+ const createHostAbortError = (): Error => {
813
+ const error = new Error("Fetch is aborted");
814
+ error.name = "AbortError";
815
+ return error;
816
+ };