@evolu/common 8.7.0 → 8.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/src/Array.d.ts +21 -21
- package/dist/src/Array.js +5 -5
- package/dist/src/Assert.d.ts +498 -141
- package/dist/src/Assert.d.ts.map +1 -1
- package/dist/src/Assert.js +648 -99
- package/dist/src/{Buffer.d.ts → Binary.d.ts} +85 -12
- package/dist/src/Binary.d.ts.map +1 -0
- package/dist/src/{Buffer.js → Binary.js} +206 -24
- package/dist/src/Console.d.ts +2 -10
- package/dist/src/Console.d.ts.map +1 -1
- package/dist/src/Console.js +6 -12
- package/dist/src/Crypto.d.ts +5 -5
- package/dist/src/Crypto.js +1 -1
- package/dist/src/Eq.d.ts +35 -40
- package/dist/src/Eq.d.ts.map +1 -1
- package/dist/src/Eq.js +51 -87
- package/dist/src/Function.d.ts +9 -14
- package/dist/src/Function.d.ts.map +1 -1
- package/dist/src/Function.js +7 -9
- package/dist/src/Http.d.ts +6 -7
- package/dist/src/Http.d.ts.map +1 -1
- package/dist/src/LeakDetector.d.ts.map +1 -1
- package/dist/src/LeakDetector.js +1 -0
- package/dist/src/LockManager.d.ts +1 -1
- package/dist/src/LockManager.js +1 -1
- package/dist/src/Lookup.d.ts +3 -2
- package/dist/src/Lookup.d.ts.map +1 -1
- package/dist/src/Lookup.js +4 -3
- package/dist/src/Number.d.ts +1 -1
- package/dist/src/Number.d.ts.map +1 -1
- package/dist/src/Number.js +1 -1
- package/dist/src/Object.d.ts +4 -4
- package/dist/src/Object.d.ts.map +1 -1
- package/dist/src/Object.js +5 -4
- package/dist/src/Option.d.ts +1 -2
- package/dist/src/Option.d.ts.map +1 -1
- package/dist/src/Option.js +1 -2
- package/dist/src/Platform.js +2 -0
- package/dist/src/Polyfills.d.ts +6 -0
- package/dist/src/Polyfills.d.ts.map +1 -1
- package/dist/src/Polyfills.js +10 -1
- package/dist/src/Random.d.ts +1 -1
- package/dist/src/Result.d.ts +49 -54
- package/dist/src/Result.d.ts.map +1 -1
- package/dist/src/Result.js +17 -22
- package/dist/src/Set.d.ts +6 -6
- package/dist/src/Set.js +2 -2
- package/dist/src/Sqlite.js +1 -1
- package/dist/src/String.js +3 -2
- package/dist/src/Task.d.ts +32 -32
- package/dist/src/Task.d.ts.map +1 -1
- package/dist/src/Task.js +20 -22
- package/dist/src/Test.d.ts +31 -3
- package/dist/src/Test.d.ts.map +1 -1
- package/dist/src/Test.js +46 -3
- package/dist/src/Time.d.ts +3 -11
- package/dist/src/Time.d.ts.map +1 -1
- package/dist/src/Time.js +10 -18
- package/dist/src/Type.d.ts +87 -85
- package/dist/src/Type.d.ts.map +1 -1
- package/dist/src/Type.js +70 -63
- package/dist/src/Types.d.ts +12 -12
- package/dist/src/Types.js +1 -1
- package/dist/src/WebSocket.d.ts +4 -7
- package/dist/src/WebSocket.d.ts.map +1 -1
- package/dist/src/Worker.d.ts +1 -1
- package/dist/src/Worker.js +7 -7
- package/dist/src/index.d.ts +1 -1
- package/dist/src/index.js +1 -1
- package/dist/src/intl/_en.d.ts.map +1 -1
- package/dist/src/intl/_en.js +1 -1
- package/dist/src/intl/ar.d.ts.map +1 -1
- package/dist/src/intl/ar.js +1 -1
- package/dist/src/intl/bn.d.ts.map +1 -1
- package/dist/src/intl/bn.js +1 -1
- package/dist/src/intl/ca.d.ts.map +1 -1
- package/dist/src/intl/ca.js +1 -1
- package/dist/src/intl/cs.d.ts.map +1 -1
- package/dist/src/intl/cs.js +1 -1
- package/dist/src/intl/da.d.ts.map +1 -1
- package/dist/src/intl/da.js +1 -1
- package/dist/src/intl/de.d.ts.map +1 -1
- package/dist/src/intl/de.js +1 -1
- package/dist/src/intl/el.d.ts.map +1 -1
- package/dist/src/intl/el.js +1 -1
- package/dist/src/intl/es.d.ts.map +1 -1
- package/dist/src/intl/es.js +1 -1
- package/dist/src/intl/fa.d.ts.map +1 -1
- package/dist/src/intl/fa.js +1 -1
- package/dist/src/intl/fi.d.ts.map +1 -1
- package/dist/src/intl/fi.js +1 -1
- package/dist/src/intl/fil.d.ts.map +1 -1
- package/dist/src/intl/fil.js +1 -1
- package/dist/src/intl/fr.d.ts.map +1 -1
- package/dist/src/intl/fr.js +1 -1
- package/dist/src/intl/he.d.ts.map +1 -1
- package/dist/src/intl/he.js +1 -1
- package/dist/src/intl/hi.d.ts.map +1 -1
- package/dist/src/intl/hi.js +1 -1
- package/dist/src/intl/hr.d.ts.map +1 -1
- package/dist/src/intl/hr.js +1 -1
- package/dist/src/intl/hu.d.ts.map +1 -1
- package/dist/src/intl/hu.js +1 -1
- package/dist/src/intl/id.d.ts.map +1 -1
- package/dist/src/intl/id.js +1 -1
- package/dist/src/intl/it.d.ts.map +1 -1
- package/dist/src/intl/it.js +1 -1
- package/dist/src/intl/ja.d.ts.map +1 -1
- package/dist/src/intl/ja.js +1 -1
- package/dist/src/intl/ko.d.ts.map +1 -1
- package/dist/src/intl/ko.js +1 -1
- package/dist/src/intl/ml.d.ts.map +1 -1
- package/dist/src/intl/ml.js +1 -1
- package/dist/src/intl/mr.d.ts.map +1 -1
- package/dist/src/intl/mr.js +1 -1
- package/dist/src/intl/ms.d.ts.map +1 -1
- package/dist/src/intl/ms.js +1 -1
- package/dist/src/intl/nb.d.ts.map +1 -1
- package/dist/src/intl/nb.js +1 -1
- package/dist/src/intl/nl.d.ts.map +1 -1
- package/dist/src/intl/nl.js +1 -1
- package/dist/src/intl/pa.d.ts.map +1 -1
- package/dist/src/intl/pa.js +1 -1
- package/dist/src/intl/pl.d.ts.map +1 -1
- package/dist/src/intl/pl.js +1 -1
- package/dist/src/intl/pt-BR.d.ts.map +1 -1
- package/dist/src/intl/pt-BR.js +1 -1
- package/dist/src/intl/pt.d.ts.map +1 -1
- package/dist/src/intl/pt.js +1 -1
- package/dist/src/intl/ro.d.ts.map +1 -1
- package/dist/src/intl/ro.js +1 -1
- package/dist/src/intl/sk.d.ts.map +1 -1
- package/dist/src/intl/sk.js +1 -1
- package/dist/src/intl/sl.d.ts.map +1 -1
- package/dist/src/intl/sl.js +1 -1
- package/dist/src/intl/sv.d.ts.map +1 -1
- package/dist/src/intl/sv.js +1 -1
- package/dist/src/intl/sw.d.ts.map +1 -1
- package/dist/src/intl/sw.js +1 -1
- package/dist/src/intl/ta.d.ts.map +1 -1
- package/dist/src/intl/ta.js +1 -1
- package/dist/src/intl/te.d.ts.map +1 -1
- package/dist/src/intl/te.js +1 -1
- package/dist/src/intl/th.d.ts.map +1 -1
- package/dist/src/intl/th.js +1 -1
- package/dist/src/intl/tr.d.ts.map +1 -1
- package/dist/src/intl/tr.js +1 -1
- package/dist/src/intl/uk.d.ts.map +1 -1
- package/dist/src/intl/uk.js +1 -1
- package/dist/src/intl/ur.d.ts.map +1 -1
- package/dist/src/intl/ur.js +1 -1
- package/dist/src/intl/vi.d.ts.map +1 -1
- package/dist/src/intl/vi.js +1 -1
- package/dist/src/intl/zh-CN.d.ts.map +1 -1
- package/dist/src/intl/zh-CN.js +1 -1
- package/dist/src/intl/zh-TW.d.ts.map +1 -1
- package/dist/src/intl/zh-TW.js +1 -1
- package/dist/src/local-first/Evolu.d.ts +12 -12
- package/dist/src/local-first/Evolu.d.ts.map +1 -1
- package/dist/src/local-first/Evolu.js +2 -2
- package/dist/src/local-first/Protocol.d.ts +14 -76
- package/dist/src/local-first/Protocol.d.ts.map +1 -1
- package/dist/src/local-first/Protocol.js +29 -206
- package/dist/src/local-first/Query.d.ts +11 -11
- package/dist/src/local-first/Query.js +6 -6
- package/dist/src/local-first/Relay.d.ts +2 -2
- package/dist/src/local-first/Schema.d.ts +3 -5
- package/dist/src/local-first/Schema.d.ts.map +1 -1
- package/dist/src/local-first/Schema.js +2 -4
- package/dist/src/local-first/Shared.d.ts.map +1 -1
- package/dist/src/local-first/Shared.js +5 -5
- package/dist/src/local-first/Storage.js +4 -4
- package/dist/src/local-first/Timestamp.d.ts +9 -0
- package/dist/src/local-first/Timestamp.d.ts.map +1 -1
- package/dist/src/local-first/Timestamp.js +13 -10
- package/package.json +7 -4
- package/src/Array.test.ts +809 -0
- package/src/Array.ts +21 -21
- package/src/Assert.test.ts +1232 -0
- package/src/Assert.ts +1023 -227
- package/src/BigInt.test.ts +28 -0
- package/src/Binary.test.ts +1128 -0
- package/src/{Buffer.ts → Binary.ts} +273 -46
- package/src/Brand.test.ts +90 -0
- package/src/Cache.test.ts +183 -0
- package/src/Callbacks.test.ts +66 -0
- package/src/Console.test.ts +809 -0
- package/src/Console.ts +9 -17
- package/src/Crypto.test.ts +138 -0
- package/src/Crypto.ts +5 -5
- package/src/Eq.test.ts +333 -0
- package/src/Eq.ts +110 -176
- package/src/Error.test.ts +118 -0
- package/src/Function.test.ts +204 -0
- package/src/Function.ts +10 -15
- package/src/Http.test.ts +816 -0
- package/src/Http.ts +6 -7
- package/src/LeakDetector.test.ts +210 -0
- package/src/LeakDetector.ts +1 -0
- package/src/LockManager.ts +2 -2
- package/src/Lookup.test.ts +418 -0
- package/src/Lookup.ts +4 -3
- package/src/Microtask.test.ts +140 -0
- package/src/Number.test.ts +218 -0
- package/src/Number.ts +2 -2
- package/src/Object.test.ts +293 -0
- package/src/Object.ts +5 -4
- package/src/Option.test.ts +104 -0
- package/src/Option.ts +1 -2
- package/src/Order.test.ts +56 -0
- package/src/Platform.ts +2 -0
- package/src/Polyfills.ts +9 -4
- package/src/Random.test.ts +89 -0
- package/src/Random.ts +1 -1
- package/src/Ref.test.ts +139 -0
- package/src/RefCount.test.ts +184 -0
- package/src/Relation.test.ts +505 -0
- package/src/Resource.test.ts +4623 -0
- package/src/Result.test.ts +1829 -0
- package/src/Result.ts +49 -54
- package/src/Schedule.test.ts +1554 -0
- package/src/Set.test.ts +279 -0
- package/src/Set.ts +6 -6
- package/src/Sqlite.ts +1 -1
- package/src/Store.test.ts +288 -0
- package/src/String.test.ts +54 -0
- package/src/String.ts +3 -3
- package/src/Task.test.ts +10820 -0
- package/src/Task.ts +38 -43
- package/src/Test.test.ts +88 -0
- package/src/Test.ts +49 -3
- package/src/Time.test.ts +926 -0
- package/src/Time.ts +13 -21
- package/src/Type.base64.test.ts +88 -0
- package/src/Type.test.ts +20465 -0
- package/src/Type.ts +114 -111
- package/src/Types.test.ts +324 -0
- package/src/Types.ts +13 -13
- package/src/WebSocket.ts +4 -7
- package/src/Worker.test.ts +755 -0
- package/src/Worker.ts +13 -18
- package/src/index.ts +1 -1
- package/src/intl/_en.ts +1 -1
- package/src/intl/ar.ts +1 -1
- package/src/intl/bn.ts +1 -1
- package/src/intl/ca.ts +1 -1
- package/src/intl/cs.ts +1 -1
- package/src/intl/da.ts +1 -1
- package/src/intl/de.ts +1 -1
- package/src/intl/el.ts +1 -1
- package/src/intl/es.ts +1 -1
- package/src/intl/fa.ts +1 -1
- package/src/intl/fi.ts +1 -1
- package/src/intl/fil.ts +1 -1
- package/src/intl/fr.ts +1 -1
- package/src/intl/he.ts +1 -1
- package/src/intl/hi.ts +1 -1
- package/src/intl/hr.ts +1 -1
- package/src/intl/hu.ts +1 -1
- package/src/intl/id.ts +1 -1
- package/src/intl/intl.test.ts +3106 -0
- package/src/intl/it.ts +1 -1
- package/src/intl/ja.ts +1 -1
- package/src/intl/ko.ts +1 -1
- package/src/intl/ml.ts +1 -1
- package/src/intl/mr.ts +1 -1
- package/src/intl/ms.ts +1 -1
- package/src/intl/nb.ts +1 -1
- package/src/intl/nl.ts +1 -1
- package/src/intl/pa.ts +1 -1
- package/src/intl/pl.ts +1 -1
- package/src/intl/pt-BR.ts +1 -1
- package/src/intl/pt.ts +1 -1
- package/src/intl/ro.ts +1 -1
- package/src/intl/sk.ts +1 -1
- package/src/intl/sl.ts +1 -1
- package/src/intl/sv.ts +1 -1
- package/src/intl/sw.ts +1 -1
- package/src/intl/ta.ts +1 -1
- package/src/intl/te.ts +1 -1
- package/src/intl/th.ts +1 -1
- package/src/intl/tr.ts +1 -1
- package/src/intl/uk.ts +1 -1
- package/src/intl/ur.ts +1 -1
- package/src/intl/vi.ts +1 -1
- package/src/intl/zh-CN.ts +1 -1
- package/src/intl/zh-TW.ts +1 -1
- package/src/local-first/Evolu.test.ts +1921 -0
- package/src/local-first/Evolu.ts +18 -14
- package/src/local-first/Owner.test.ts +83 -0
- package/src/local-first/Protocol.test.ts +1037 -0
- package/src/local-first/Protocol.ts +58 -261
- package/src/local-first/Query.test.ts +349 -0
- package/src/local-first/Query.ts +11 -11
- package/src/local-first/Relay.ts +2 -2
- package/src/local-first/Schema.ts +3 -5
- package/src/local-first/Shared.test.ts +1828 -0
- package/src/local-first/Shared.ts +9 -8
- package/src/local-first/Storage.ts +4 -4
- package/src/local-first/Timestamp.test.ts +374 -0
- package/src/local-first/Timestamp.ts +22 -10
- package/dist/src/Buffer.d.ts.map +0 -1
|
@@ -0,0 +1,1829 @@
|
|
|
1
|
+
import { describe, it, test } from "node:test";
|
|
2
|
+
import {
|
|
3
|
+
assertEqual,
|
|
4
|
+
assertErr,
|
|
5
|
+
assertFalse,
|
|
6
|
+
assertInstanceOf,
|
|
7
|
+
assertOk,
|
|
8
|
+
assertRejectsSame,
|
|
9
|
+
assertSame,
|
|
10
|
+
assertThrowsInstanceOf,
|
|
11
|
+
assertThrowsSame,
|
|
12
|
+
assertTrue,
|
|
13
|
+
} from "./Assert.ts";
|
|
14
|
+
|
|
15
|
+
import type { NonEmptyReadonlyArray } from "./Array.ts";
|
|
16
|
+
import type {
|
|
17
|
+
Done,
|
|
18
|
+
Err,
|
|
19
|
+
ExcludeDone,
|
|
20
|
+
InferDone,
|
|
21
|
+
InferErr,
|
|
22
|
+
InferOk,
|
|
23
|
+
NextResult,
|
|
24
|
+
OnlyDone,
|
|
25
|
+
Result,
|
|
26
|
+
} from "./Result.ts";
|
|
27
|
+
import {
|
|
28
|
+
allResult,
|
|
29
|
+
anyResult,
|
|
30
|
+
done,
|
|
31
|
+
err,
|
|
32
|
+
flatMapResult,
|
|
33
|
+
getOk,
|
|
34
|
+
getOrNull,
|
|
35
|
+
getOrThrow,
|
|
36
|
+
isErr,
|
|
37
|
+
isOk,
|
|
38
|
+
ok,
|
|
39
|
+
tryAsync,
|
|
40
|
+
trySync,
|
|
41
|
+
} from "./Result.ts";
|
|
42
|
+
import { parseStackTrace } from "./StackTrace.ts";
|
|
43
|
+
import { assertType } from "./Type.ts";
|
|
44
|
+
|
|
45
|
+
describe("InferOk and InferErr", () => {
|
|
46
|
+
it("infers Ok type", () => {
|
|
47
|
+
type MyResult = Result<string, { type: "MyError"; code: number }>;
|
|
48
|
+
assertType<InferOk<MyResult>, string>();
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it("infers Err type", () => {
|
|
52
|
+
interface MyError {
|
|
53
|
+
readonly type: "MyError";
|
|
54
|
+
readonly code: number;
|
|
55
|
+
}
|
|
56
|
+
type MyResult = Result<string, MyError>;
|
|
57
|
+
assertType<InferErr<MyResult>, MyError>();
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it("handles void Result", () => {
|
|
61
|
+
type VoidResult = Result<void, Error>;
|
|
62
|
+
assertType<InferOk<VoidResult>, void>();
|
|
63
|
+
assertType<InferErr<VoidResult>, Error>();
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it("works at runtime", () => {
|
|
67
|
+
interface MyError {
|
|
68
|
+
readonly type: "MyError";
|
|
69
|
+
readonly code: number;
|
|
70
|
+
}
|
|
71
|
+
type MyResult = Result<string, MyError>;
|
|
72
|
+
|
|
73
|
+
const okValue: InferOk<MyResult> = "hello";
|
|
74
|
+
const errValue: InferErr<MyResult> = { type: "MyError", code: 404 };
|
|
75
|
+
|
|
76
|
+
assertEqual(okValue, "hello");
|
|
77
|
+
assertEqual(errValue, { type: "MyError", code: 404 });
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
describe("ok", () => {
|
|
82
|
+
it("creates Ok with a value", () => {
|
|
83
|
+
assertEqual(ok(42), { ok: true, value: 42 });
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it("creates Ok<void> without arguments", () => {
|
|
87
|
+
assertEqual(ok(), { ok: true, value: undefined });
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it("caches ok() and ok(undefined)", () => {
|
|
91
|
+
assertSame(ok(), ok());
|
|
92
|
+
assertSame(ok(undefined), ok());
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it("rejects Ok<void> when Result expects a value", () => {
|
|
96
|
+
// @ts-expect-error Type 'Ok<void>' is not assignable to type 'Result<string, Error>'
|
|
97
|
+
const _result: Result<string, Error> = ok();
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it("returns Result<T, never> for correct type inference", () => {
|
|
101
|
+
const result = ok(42);
|
|
102
|
+
assertType<typeof result, Result<number>>();
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it("infers never for E when combining with err", () => {
|
|
106
|
+
interface MyError {
|
|
107
|
+
readonly type: "MyError";
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const example = (fail: boolean): Result<number, MyError> => {
|
|
111
|
+
if (fail) return err({ type: "MyError" });
|
|
112
|
+
return ok(42);
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
{
|
|
116
|
+
const actual = example(false);
|
|
117
|
+
assertType<typeof actual, Result<number, MyError>>();
|
|
118
|
+
}
|
|
119
|
+
});
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
describe("err", () => {
|
|
123
|
+
it("creates Err with an error", () => {
|
|
124
|
+
assertEqual(err("error"), { ok: false, error: "error" });
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it("returns Result<never, E> for correct type inference", () => {
|
|
128
|
+
const result = err("oops");
|
|
129
|
+
assertType<typeof result, Result<never, string>>();
|
|
130
|
+
});
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
describe("isOk and isErr", () => {
|
|
134
|
+
it("identifies Ok result", () => {
|
|
135
|
+
const result = ok(123);
|
|
136
|
+
|
|
137
|
+
assertTrue(isOk(result));
|
|
138
|
+
assertFalse(isErr(result));
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it("isOk narrows an Ok result", () => {
|
|
142
|
+
const result = ok(123);
|
|
143
|
+
|
|
144
|
+
if (isOk(result)) {
|
|
145
|
+
assertType<typeof result.value, number>();
|
|
146
|
+
}
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it("identifies Err result", () => {
|
|
150
|
+
const result = err({ type: "TestError" as const });
|
|
151
|
+
|
|
152
|
+
assertFalse(isOk(result));
|
|
153
|
+
assertTrue(isErr(result));
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
it("isErr narrows an Err result", () => {
|
|
157
|
+
const result = err({ type: "TestError" as const });
|
|
158
|
+
|
|
159
|
+
if (isErr(result)) {
|
|
160
|
+
assertType<
|
|
161
|
+
typeof result.error extends { readonly type: "TestError" }
|
|
162
|
+
? true
|
|
163
|
+
: false,
|
|
164
|
+
true
|
|
165
|
+
>();
|
|
166
|
+
}
|
|
167
|
+
});
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
describe("getOrThrow", () => {
|
|
171
|
+
it("returns value for Ok", () => {
|
|
172
|
+
assertEqual(getOrThrow(ok(42)), 42);
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
it("throws for Err", () => {
|
|
176
|
+
const error = assertThrowsInstanceOf(() => getOrThrow(err("error")), Error);
|
|
177
|
+
assertTrue(error.message.includes("getOrThrow"));
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
it("includes primitive error as cause", () => {
|
|
181
|
+
const error = assertThrowsInstanceOf(() => getOrThrow(err("error")), Error);
|
|
182
|
+
assertEqual(error.cause, "error");
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
it("includes Error instance as cause", () => {
|
|
186
|
+
const original = new TypeError("boom");
|
|
187
|
+
const error = assertThrowsInstanceOf(
|
|
188
|
+
() => getOrThrow(err(original)),
|
|
189
|
+
Error,
|
|
190
|
+
);
|
|
191
|
+
assertSame(error.cause, original);
|
|
192
|
+
});
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
describe("getOrNull", () => {
|
|
196
|
+
it("returns value for Ok", () => {
|
|
197
|
+
assertEqual(getOrNull(ok(42)), 42);
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
it("returns null for Err", () => {
|
|
201
|
+
assertSame(getOrNull(err("error")), null);
|
|
202
|
+
});
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
describe("getOk", () => {
|
|
206
|
+
it("extracts value from Result with never error", () => {
|
|
207
|
+
const result = ok(42);
|
|
208
|
+
assertEqual(getOk(result), 42);
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
it("rejects Result with possible error type", () => {
|
|
212
|
+
type IsAssignable =
|
|
213
|
+
Result<number, string> extends Result<number> ? true : false;
|
|
214
|
+
assertType<IsAssignable, false>();
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
it("throws when invariant is violated at runtime", () => {
|
|
218
|
+
const invalid = err("fail") as unknown as Result<number>;
|
|
219
|
+
const error = assertThrowsInstanceOf(() => getOk(invalid), Error);
|
|
220
|
+
assertTrue(error.message.includes("Expected Ok result."));
|
|
221
|
+
});
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
describe("trySync", () => {
|
|
225
|
+
interface ParseError {
|
|
226
|
+
readonly type: "ParseError";
|
|
227
|
+
readonly message: string;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
it("returns Ok on success", () => {
|
|
231
|
+
const result = trySync(
|
|
232
|
+
() => JSON.parse('{"key": "value"}') as unknown,
|
|
233
|
+
(error): ParseError => ({ type: "ParseError", message: String(error) }),
|
|
234
|
+
);
|
|
235
|
+
|
|
236
|
+
assertEqual(result, {
|
|
237
|
+
ok: true,
|
|
238
|
+
value: { key: "value" },
|
|
239
|
+
});
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
it("returns Err on exception", () => {
|
|
243
|
+
const result = trySync(
|
|
244
|
+
() => JSON.parse("{key: value}") as unknown,
|
|
245
|
+
(error): ParseError => ({ type: "ParseError", message: String(error) }),
|
|
246
|
+
);
|
|
247
|
+
|
|
248
|
+
assertErr(result);
|
|
249
|
+
const { message } = result.error;
|
|
250
|
+
assertTrue(message.includes("SyntaxError"));
|
|
251
|
+
assertEqual(result, {
|
|
252
|
+
ok: false,
|
|
253
|
+
error: { type: "ParseError", message },
|
|
254
|
+
});
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
it("returns Err with the exception when mapError is omitted", () => {
|
|
258
|
+
const failure = new Error("Something went wrong");
|
|
259
|
+
const result = trySync(() => {
|
|
260
|
+
throw failure;
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
assertType<typeof result, Result<never, unknown>>();
|
|
264
|
+
assertEqual(result, err(failure));
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
it("mapError may throw to escalate a failure", () => {
|
|
268
|
+
const failure = new Error("Something went wrong");
|
|
269
|
+
const escalated = new Error("Escalated");
|
|
270
|
+
|
|
271
|
+
assertThrowsSame(
|
|
272
|
+
() =>
|
|
273
|
+
trySync(
|
|
274
|
+
() => {
|
|
275
|
+
throw failure;
|
|
276
|
+
},
|
|
277
|
+
() => {
|
|
278
|
+
throw escalated;
|
|
279
|
+
},
|
|
280
|
+
),
|
|
281
|
+
escalated,
|
|
282
|
+
);
|
|
283
|
+
});
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
describe("tryAsync", () => {
|
|
287
|
+
it("returns Ok on resolved promise", async () => {
|
|
288
|
+
const result = await tryAsync(
|
|
289
|
+
() => Promise.resolve(),
|
|
290
|
+
(error) => ({ type: "TestError", message: String(error) }),
|
|
291
|
+
);
|
|
292
|
+
|
|
293
|
+
assertEqual(result, ok());
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
it("returns Err on rejected promise", async () => {
|
|
297
|
+
const result = await tryAsync(
|
|
298
|
+
// oxlint-disable-next-line typescript/require-await
|
|
299
|
+
async () => {
|
|
300
|
+
throw new Error("Something went wrong");
|
|
301
|
+
},
|
|
302
|
+
(error) => ({ type: "TestError", message: String(error) }),
|
|
303
|
+
);
|
|
304
|
+
|
|
305
|
+
assertEqual(
|
|
306
|
+
result,
|
|
307
|
+
err({
|
|
308
|
+
type: "TestError",
|
|
309
|
+
message: "Error: Something went wrong",
|
|
310
|
+
}),
|
|
311
|
+
);
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
it("returns Err with the rejection when mapError is omitted", async () => {
|
|
315
|
+
const failure = new Error("Something went wrong");
|
|
316
|
+
const result = await tryAsync(
|
|
317
|
+
// oxlint-disable-next-line typescript/require-await
|
|
318
|
+
async () => {
|
|
319
|
+
throw failure;
|
|
320
|
+
},
|
|
321
|
+
);
|
|
322
|
+
|
|
323
|
+
assertType<typeof result, Result<never, unknown>>();
|
|
324
|
+
assertEqual(result, err(failure));
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
it("maps custom error properties", async () => {
|
|
328
|
+
const result = await tryAsync(
|
|
329
|
+
// oxlint-disable-next-line typescript/require-await
|
|
330
|
+
async () => {
|
|
331
|
+
throw new TypeError("Invalid type");
|
|
332
|
+
},
|
|
333
|
+
(error) => ({
|
|
334
|
+
type: "CustomError",
|
|
335
|
+
name: error instanceof Error ? error.name : "UnknownError",
|
|
336
|
+
message: String(error),
|
|
337
|
+
}),
|
|
338
|
+
);
|
|
339
|
+
|
|
340
|
+
assertEqual(
|
|
341
|
+
result,
|
|
342
|
+
err({
|
|
343
|
+
type: "CustomError",
|
|
344
|
+
name: "TypeError",
|
|
345
|
+
message: "TypeError: Invalid type",
|
|
346
|
+
}),
|
|
347
|
+
);
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
it("mapError may throw to escalate a failure", async () => {
|
|
351
|
+
const failure = new Error("Something went wrong");
|
|
352
|
+
const escalated = new Error("Escalated");
|
|
353
|
+
|
|
354
|
+
await assertRejectsSame(
|
|
355
|
+
tryAsync(
|
|
356
|
+
() => Promise.reject(failure),
|
|
357
|
+
() => {
|
|
358
|
+
throw escalated;
|
|
359
|
+
},
|
|
360
|
+
),
|
|
361
|
+
escalated,
|
|
362
|
+
);
|
|
363
|
+
});
|
|
364
|
+
|
|
365
|
+
it("catches synchronous throws", async () => {
|
|
366
|
+
const result = await tryAsync(
|
|
367
|
+
() => {
|
|
368
|
+
throw new Error("Sync throw before promise");
|
|
369
|
+
},
|
|
370
|
+
(error) => ({ type: "TestError", message: String(error) }),
|
|
371
|
+
);
|
|
372
|
+
|
|
373
|
+
assertEqual(
|
|
374
|
+
result,
|
|
375
|
+
err({
|
|
376
|
+
type: "TestError",
|
|
377
|
+
message: "Error: Sync throw before promise",
|
|
378
|
+
}),
|
|
379
|
+
);
|
|
380
|
+
});
|
|
381
|
+
|
|
382
|
+
it("preserves its await boundary in rejected error stacks", async () => {
|
|
383
|
+
const result = await tryAsync(async () => {
|
|
384
|
+
await Promise.resolve();
|
|
385
|
+
throw new Error("Something went wrong");
|
|
386
|
+
});
|
|
387
|
+
|
|
388
|
+
assertErr(result);
|
|
389
|
+
assertInstanceOf(result.error, Error);
|
|
390
|
+
|
|
391
|
+
assertTrue(parseStackTrace(result.error.stack).files.includes("Result.ts"));
|
|
392
|
+
});
|
|
393
|
+
});
|
|
394
|
+
|
|
395
|
+
describe("NextResult", () => {
|
|
396
|
+
it("models success, failure, and done", () => {
|
|
397
|
+
type E = "E";
|
|
398
|
+
|
|
399
|
+
const a: NextResult<number, E, string> = ok(1);
|
|
400
|
+
const b: NextResult<number, E, string> = err(done("finished"));
|
|
401
|
+
const c: NextResult<number, E, string> = err<E>("E");
|
|
402
|
+
|
|
403
|
+
assertType<typeof a, NextResult<number, E, string>>();
|
|
404
|
+
assertFalse(b.ok);
|
|
405
|
+
assertFalse(c.ok);
|
|
406
|
+
});
|
|
407
|
+
|
|
408
|
+
it("extracts all type parameters", () => {
|
|
409
|
+
type MyNextResult = NextResult<number, string, { summary: string }>;
|
|
410
|
+
|
|
411
|
+
assertType<InferOk<MyNextResult>, number>();
|
|
412
|
+
assertType<InferErr<MyNextResult>, string | Done<{ summary: string }>>();
|
|
413
|
+
assertType<
|
|
414
|
+
InferDone<MyNextResult>,
|
|
415
|
+
{
|
|
416
|
+
summary: string;
|
|
417
|
+
}
|
|
418
|
+
>();
|
|
419
|
+
});
|
|
420
|
+
|
|
421
|
+
describe("done", () => {
|
|
422
|
+
it("creates Done with done value", () => {
|
|
423
|
+
assertEqual(done("finished"), {
|
|
424
|
+
type: "Done",
|
|
425
|
+
done: "finished",
|
|
426
|
+
});
|
|
427
|
+
});
|
|
428
|
+
|
|
429
|
+
it("creates Done<void> without arguments", () => {
|
|
430
|
+
assertEqual(done(), {
|
|
431
|
+
type: "Done",
|
|
432
|
+
done: undefined,
|
|
433
|
+
});
|
|
434
|
+
{
|
|
435
|
+
const actual = done();
|
|
436
|
+
assertType<typeof actual, Done<void>>();
|
|
437
|
+
}
|
|
438
|
+
});
|
|
439
|
+
|
|
440
|
+
it("preserves done type", () => {
|
|
441
|
+
const value = done({ count: 1 });
|
|
442
|
+
assertType<typeof value, Done<{ count: number }>>();
|
|
443
|
+
assertType<typeof value.done, { count: number }>();
|
|
444
|
+
});
|
|
445
|
+
});
|
|
446
|
+
|
|
447
|
+
describe("ExcludeDone and OnlyDone", () => {
|
|
448
|
+
it("ExcludeDone removes Done from a union", () => {
|
|
449
|
+
interface MyError {
|
|
450
|
+
readonly type: "MyError";
|
|
451
|
+
}
|
|
452
|
+
type E = MyError | Done<void>;
|
|
453
|
+
assertType<ExcludeDone<E>, MyError>();
|
|
454
|
+
});
|
|
455
|
+
|
|
456
|
+
it("OnlyDone keeps only Done from a union", () => {
|
|
457
|
+
interface MyError {
|
|
458
|
+
readonly type: "MyError";
|
|
459
|
+
}
|
|
460
|
+
type E = MyError | Done<"done">;
|
|
461
|
+
assertType<OnlyDone<E>, Done<"done">>();
|
|
462
|
+
});
|
|
463
|
+
|
|
464
|
+
it("OnlyDone returns never when there is no Done", () => {
|
|
465
|
+
type E = "E";
|
|
466
|
+
assertType<OnlyDone<E>, never>();
|
|
467
|
+
});
|
|
468
|
+
});
|
|
469
|
+
|
|
470
|
+
describe("InferDone", () => {
|
|
471
|
+
it("extracts Done type from NextResult with void done", () => {
|
|
472
|
+
type R = NextResult<number, string>;
|
|
473
|
+
assertType<InferDone<R>, void>();
|
|
474
|
+
});
|
|
475
|
+
|
|
476
|
+
it("extracts Done type from NextResult with complex done", () => {
|
|
477
|
+
type R = NextResult<
|
|
478
|
+
number,
|
|
479
|
+
string,
|
|
480
|
+
{ count: number; items: Array<string> }
|
|
481
|
+
>;
|
|
482
|
+
assertType<
|
|
483
|
+
InferDone<R>,
|
|
484
|
+
{
|
|
485
|
+
count: number;
|
|
486
|
+
items: Array<string>;
|
|
487
|
+
}
|
|
488
|
+
>();
|
|
489
|
+
});
|
|
490
|
+
|
|
491
|
+
it("returns never for Result without Done", () => {
|
|
492
|
+
type R = Result<number, string>;
|
|
493
|
+
assertType<InferDone<R>, never>();
|
|
494
|
+
});
|
|
495
|
+
|
|
496
|
+
it("works with union errors containing Done", () => {
|
|
497
|
+
interface MyError {
|
|
498
|
+
readonly type: "MyError";
|
|
499
|
+
}
|
|
500
|
+
type R = Result<number, MyError | Done<string>>;
|
|
501
|
+
assertType<InferDone<R>, string>();
|
|
502
|
+
});
|
|
503
|
+
});
|
|
504
|
+
});
|
|
505
|
+
|
|
506
|
+
describe("flatMapResult", () => {
|
|
507
|
+
it("composes an Ok with another Result-returning operation", () => {
|
|
508
|
+
const result = flatMapResult(ok(21), (value) => ok(value * 2));
|
|
509
|
+
|
|
510
|
+
assertEqual(result, ok(42));
|
|
511
|
+
assertType<typeof result, Result<number>>();
|
|
512
|
+
});
|
|
513
|
+
|
|
514
|
+
it("returns the existing Err without calling the operation", () => {
|
|
515
|
+
interface FirstError {
|
|
516
|
+
readonly type: "FirstError";
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
interface SecondError {
|
|
520
|
+
readonly type: "SecondError";
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
const first = (): Result<number, FirstError> => err({ type: "FirstError" });
|
|
524
|
+
let called = false;
|
|
525
|
+
const result = flatMapResult(first(), (): Result<string, SecondError> => {
|
|
526
|
+
called = true;
|
|
527
|
+
return err({ type: "SecondError" });
|
|
528
|
+
});
|
|
529
|
+
|
|
530
|
+
assertEqual(result, err({ type: "FirstError" }));
|
|
531
|
+
assertFalse(called);
|
|
532
|
+
assertType<
|
|
533
|
+
typeof result extends Result<string, FirstError | SecondError>
|
|
534
|
+
? true
|
|
535
|
+
: false,
|
|
536
|
+
true
|
|
537
|
+
>();
|
|
538
|
+
});
|
|
539
|
+
|
|
540
|
+
it("returns an error from the next operation", () => {
|
|
541
|
+
const result = flatMapResult(ok(42), () => err("fail"));
|
|
542
|
+
|
|
543
|
+
assertEqual(result, err("fail"));
|
|
544
|
+
});
|
|
545
|
+
});
|
|
546
|
+
|
|
547
|
+
describe("allResult", () => {
|
|
548
|
+
it("returns emptyArray for empty array", () => {
|
|
549
|
+
const result = allResult([]);
|
|
550
|
+
assertEqual(result, ok([]));
|
|
551
|
+
});
|
|
552
|
+
|
|
553
|
+
it("returns emptyRecord for empty record", () => {
|
|
554
|
+
const result = allResult({});
|
|
555
|
+
assertOk(result, {});
|
|
556
|
+
});
|
|
557
|
+
|
|
558
|
+
it("extracts all values from array of Ok results", () => {
|
|
559
|
+
const results = [ok(1), ok(2), ok(3)];
|
|
560
|
+
assertEqual(allResult(results), ok([1, 2, 3]));
|
|
561
|
+
});
|
|
562
|
+
|
|
563
|
+
it("returns first error from array", () => {
|
|
564
|
+
interface E1 {
|
|
565
|
+
readonly type: "E1";
|
|
566
|
+
}
|
|
567
|
+
interface E2 {
|
|
568
|
+
readonly type: "E2";
|
|
569
|
+
}
|
|
570
|
+
const results: NonEmptyReadonlyArray<Result<number, E1 | E2>> = [
|
|
571
|
+
ok(1),
|
|
572
|
+
err({ type: "E1" }),
|
|
573
|
+
err({ type: "E2" }),
|
|
574
|
+
];
|
|
575
|
+
assertEqual(allResult(results), err({ type: "E1" }));
|
|
576
|
+
});
|
|
577
|
+
|
|
578
|
+
it("extracts all values from struct", () => {
|
|
579
|
+
const result = allResult({ a: ok(1), b: ok("two") });
|
|
580
|
+
assertOk(result, { a: 1, b: "two" });
|
|
581
|
+
});
|
|
582
|
+
|
|
583
|
+
it("returns first error from struct", () => {
|
|
584
|
+
const result = allResult({ a: ok(1), b: err("fail"), c: ok(3) });
|
|
585
|
+
assertEqual(result, err("fail"));
|
|
586
|
+
});
|
|
587
|
+
|
|
588
|
+
it("tuple preserves types", () => {
|
|
589
|
+
const result = allResult([ok(1), ok("two"), ok(true)]);
|
|
590
|
+
if (result.ok) {
|
|
591
|
+
{
|
|
592
|
+
const actual = result.value[0];
|
|
593
|
+
assertType<typeof actual, number>();
|
|
594
|
+
}
|
|
595
|
+
{
|
|
596
|
+
const actual = result.value[1];
|
|
597
|
+
assertType<typeof actual, string>();
|
|
598
|
+
}
|
|
599
|
+
{
|
|
600
|
+
const actual = result.value[2];
|
|
601
|
+
assertType<typeof actual, boolean>();
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
});
|
|
605
|
+
|
|
606
|
+
it("struct preserves types", () => {
|
|
607
|
+
const result = allResult({ a: ok(1), b: ok("two") });
|
|
608
|
+
if (result.ok) {
|
|
609
|
+
assertType<typeof result.value, { a: number; b: string }>();
|
|
610
|
+
}
|
|
611
|
+
});
|
|
612
|
+
|
|
613
|
+
it("non-empty arrays preserve types", () => {
|
|
614
|
+
const result = allResult([ok(1), ok(2)]);
|
|
615
|
+
if (result.ok) {
|
|
616
|
+
{
|
|
617
|
+
const actual = result.value[0];
|
|
618
|
+
assertType<typeof actual, number>();
|
|
619
|
+
}
|
|
620
|
+
{
|
|
621
|
+
const actual = result.value[1];
|
|
622
|
+
assertType<typeof actual, number>();
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
});
|
|
626
|
+
|
|
627
|
+
it("works with Iterable", () => {
|
|
628
|
+
const set = new Set([ok(1), ok(2), ok(3)]);
|
|
629
|
+
const result = allResult(set);
|
|
630
|
+
assertEqual(result, ok([1, 2, 3]));
|
|
631
|
+
});
|
|
632
|
+
|
|
633
|
+
it("returns an empty array for an empty non-array Iterable", () => {
|
|
634
|
+
const results = new Set<Result<number>>();
|
|
635
|
+
const result = allResult(results);
|
|
636
|
+
|
|
637
|
+
assertType<typeof result, Result<ReadonlyArray<number>>>();
|
|
638
|
+
assertEqual(result, ok([]));
|
|
639
|
+
});
|
|
640
|
+
|
|
641
|
+
it("stops consuming an Iterable on the first error", () => {
|
|
642
|
+
const consumedValues: Array<number> = [];
|
|
643
|
+
const createResults = function* (): Generator<Result<number, string>> {
|
|
644
|
+
consumedValues.push(1);
|
|
645
|
+
yield ok(1);
|
|
646
|
+
consumedValues.push(2);
|
|
647
|
+
yield err("fail");
|
|
648
|
+
consumedValues.push(3);
|
|
649
|
+
yield ok(3);
|
|
650
|
+
};
|
|
651
|
+
|
|
652
|
+
const result = allResult(createResults());
|
|
653
|
+
|
|
654
|
+
assertErr(result, "fail");
|
|
655
|
+
assertEqual(consumedValues, [1, 2]);
|
|
656
|
+
});
|
|
657
|
+
|
|
658
|
+
it("ignores inherited record properties", () => {
|
|
659
|
+
const results = Object.assign(
|
|
660
|
+
Object.create({ inherited: err("fail") }) as Record<
|
|
661
|
+
string,
|
|
662
|
+
Result<number, string>
|
|
663
|
+
>,
|
|
664
|
+
{ own: ok(1) },
|
|
665
|
+
);
|
|
666
|
+
|
|
667
|
+
const result = allResult(results);
|
|
668
|
+
|
|
669
|
+
assertOk(result, { own: 1 });
|
|
670
|
+
});
|
|
671
|
+
|
|
672
|
+
it("does not collect Ok values", () => {
|
|
673
|
+
interface FirstError {
|
|
674
|
+
readonly type: "FirstError";
|
|
675
|
+
}
|
|
676
|
+
interface SecondError {
|
|
677
|
+
readonly type: "SecondError";
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
const first: Result<number, FirstError> = ok(1);
|
|
681
|
+
const second: Result<string, SecondError> = ok("two");
|
|
682
|
+
|
|
683
|
+
const result = allResult([first, second], { collect: false });
|
|
684
|
+
|
|
685
|
+
assertType<typeof result, Result<void, FirstError | SecondError>>();
|
|
686
|
+
assertOk(result, undefined);
|
|
687
|
+
});
|
|
688
|
+
|
|
689
|
+
it("returns the first Err without collecting Ok values", () => {
|
|
690
|
+
const result = allResult([ok(1), err("first"), err("second")], {
|
|
691
|
+
collect: false,
|
|
692
|
+
});
|
|
693
|
+
|
|
694
|
+
assertErr(result, "first");
|
|
695
|
+
});
|
|
696
|
+
|
|
697
|
+
it("does not collect Ok values from a record", () => {
|
|
698
|
+
const result = allResult(
|
|
699
|
+
{ first: ok(1), second: err("fail") },
|
|
700
|
+
{ collect: false },
|
|
701
|
+
);
|
|
702
|
+
|
|
703
|
+
assertType<typeof result, Result<void, string>>();
|
|
704
|
+
assertErr(result, "fail");
|
|
705
|
+
});
|
|
706
|
+
});
|
|
707
|
+
|
|
708
|
+
describe("allResult mapping overload", () => {
|
|
709
|
+
it("returns emptyArray for empty array", () => {
|
|
710
|
+
const result = allResult([], (x: number) => ok(x * 2));
|
|
711
|
+
assertEqual(result, ok([]));
|
|
712
|
+
});
|
|
713
|
+
|
|
714
|
+
it("returns emptyRecord for empty record", () => {
|
|
715
|
+
const result = allResult({}, (x: number) => ok(x * 2));
|
|
716
|
+
assertOk(result, {});
|
|
717
|
+
});
|
|
718
|
+
|
|
719
|
+
it("maps items and collects results", () => {
|
|
720
|
+
const result = allResult([1, 2, 3], (x) => ok(x * 2));
|
|
721
|
+
assertEqual(result, ok([2, 4, 6]));
|
|
722
|
+
});
|
|
723
|
+
|
|
724
|
+
it("returns first error", () => {
|
|
725
|
+
const result = allResult([1, 2, 3], (x) =>
|
|
726
|
+
x === 2 ? err("fail") : ok(x * 2),
|
|
727
|
+
);
|
|
728
|
+
assertEqual(result, err("fail"));
|
|
729
|
+
});
|
|
730
|
+
|
|
731
|
+
it("does not map values after the first error", () => {
|
|
732
|
+
const mappedValues: Array<number> = [];
|
|
733
|
+
const result = allResult([1, 2, 3], (value) => {
|
|
734
|
+
mappedValues.push(value);
|
|
735
|
+
return value === 2 ? err("fail") : ok(value * 2);
|
|
736
|
+
});
|
|
737
|
+
|
|
738
|
+
assertEqual(result, err("fail"));
|
|
739
|
+
assertEqual(mappedValues, [1, 2]);
|
|
740
|
+
});
|
|
741
|
+
|
|
742
|
+
it("infers heterogeneous mapper errors", () => {
|
|
743
|
+
interface FirstError {
|
|
744
|
+
readonly type: "FirstError";
|
|
745
|
+
}
|
|
746
|
+
interface SecondError {
|
|
747
|
+
readonly type: "SecondError";
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
const first = (): Result<void, FirstError> => ok();
|
|
751
|
+
const second = (): Result<void, SecondError> =>
|
|
752
|
+
err({ type: "SecondError" });
|
|
753
|
+
const result = allResult([first, second], (operation) => operation());
|
|
754
|
+
|
|
755
|
+
assertType<
|
|
756
|
+
typeof result,
|
|
757
|
+
Result<readonly [void, void], FirstError | SecondError>
|
|
758
|
+
>();
|
|
759
|
+
assertErr(result, { type: "SecondError" });
|
|
760
|
+
});
|
|
761
|
+
|
|
762
|
+
it("does not collect mapped Ok values", () => {
|
|
763
|
+
const result = allResult([1, 2, 3], (value) => ok(value * 2), {
|
|
764
|
+
collect: false,
|
|
765
|
+
});
|
|
766
|
+
|
|
767
|
+
assertType<typeof result, Result<void>>();
|
|
768
|
+
assertOk(result, undefined);
|
|
769
|
+
});
|
|
770
|
+
|
|
771
|
+
it("stops mapping without collecting on the first Err", () => {
|
|
772
|
+
interface FirstError {
|
|
773
|
+
readonly type: "FirstError";
|
|
774
|
+
}
|
|
775
|
+
interface SecondError {
|
|
776
|
+
readonly type: "SecondError";
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
const calls: Array<string> = [];
|
|
780
|
+
const first = (): Result<number, FirstError> => {
|
|
781
|
+
calls.push("first");
|
|
782
|
+
return ok(1);
|
|
783
|
+
};
|
|
784
|
+
const second = (): Result<number, SecondError> => {
|
|
785
|
+
calls.push("second");
|
|
786
|
+
return err({ type: "SecondError" });
|
|
787
|
+
};
|
|
788
|
+
const result = allResult([first, second], (operation) => operation(), {
|
|
789
|
+
collect: false,
|
|
790
|
+
});
|
|
791
|
+
|
|
792
|
+
assertType<typeof result, Result<void, FirstError | SecondError>>();
|
|
793
|
+
assertErr(result, { type: "SecondError" });
|
|
794
|
+
assertEqual(calls, ["first", "second"]);
|
|
795
|
+
});
|
|
796
|
+
|
|
797
|
+
it("maps struct and collects results", () => {
|
|
798
|
+
const result = allResult({ a: 1, b: 2 }, (x) => ok(x * 2));
|
|
799
|
+
assertOk(result, { a: 2, b: 4 });
|
|
800
|
+
});
|
|
801
|
+
|
|
802
|
+
it("maps a record without collecting Ok values", () => {
|
|
803
|
+
const result = allResult({ a: 1, b: 2 }, (x) => ok(x * 2), {
|
|
804
|
+
collect: false,
|
|
805
|
+
});
|
|
806
|
+
|
|
807
|
+
assertType<typeof result, Result<void>>();
|
|
808
|
+
assertOk(result, undefined);
|
|
809
|
+
});
|
|
810
|
+
|
|
811
|
+
it("returns first error from struct", () => {
|
|
812
|
+
const result = allResult({ a: 1, b: 2, c: 3 }, (x) =>
|
|
813
|
+
x === 2 ? err("fail") : ok(x * 2),
|
|
814
|
+
);
|
|
815
|
+
assertEqual(result, err("fail"));
|
|
816
|
+
});
|
|
817
|
+
|
|
818
|
+
it("struct preserves types", () => {
|
|
819
|
+
const result = allResult({ a: 1, b: 2 }, (x) => ok(String(x)));
|
|
820
|
+
if (result.ok) {
|
|
821
|
+
assertType<typeof result.value, Readonly<Record<"a" | "b", string>>>();
|
|
822
|
+
}
|
|
823
|
+
});
|
|
824
|
+
|
|
825
|
+
it("non-empty arrays preserve types", () => {
|
|
826
|
+
const result = allResult([1, 2, 3], (x) => ok(x * 2));
|
|
827
|
+
if (result.ok) {
|
|
828
|
+
{
|
|
829
|
+
const actual = result.value[0];
|
|
830
|
+
assertType<typeof actual, number>();
|
|
831
|
+
}
|
|
832
|
+
{
|
|
833
|
+
const actual = result.value[1];
|
|
834
|
+
assertType<typeof actual, number>();
|
|
835
|
+
}
|
|
836
|
+
{
|
|
837
|
+
const actual = result.value[2];
|
|
838
|
+
assertType<typeof actual, number>();
|
|
839
|
+
}
|
|
840
|
+
}
|
|
841
|
+
});
|
|
842
|
+
|
|
843
|
+
it("works with Iterable", () => {
|
|
844
|
+
const set = new Set([1, 2, 3]);
|
|
845
|
+
const result = allResult(set, (x) => ok(x * 2));
|
|
846
|
+
assertEqual(result, ok([2, 4, 6]));
|
|
847
|
+
});
|
|
848
|
+
});
|
|
849
|
+
|
|
850
|
+
describe("anyResult", () => {
|
|
851
|
+
it("returns first success", () => {
|
|
852
|
+
assertEqual(anyResult([err("a"), ok(42), err("b")]), ok(42));
|
|
853
|
+
});
|
|
854
|
+
|
|
855
|
+
it("returns last error when all fail", () => {
|
|
856
|
+
assertEqual(anyResult([err("a"), err("b"), err("c")]), err("c"));
|
|
857
|
+
});
|
|
858
|
+
|
|
859
|
+
it("returns first Ok even if it's first", () => {
|
|
860
|
+
assertEqual(anyResult([ok(1), ok(2), ok(3)]), ok(1));
|
|
861
|
+
});
|
|
862
|
+
|
|
863
|
+
it("preserves types", () => {
|
|
864
|
+
const result = anyResult([err({ type: "E1" as const }), ok(42)]);
|
|
865
|
+
if (result.ok) {
|
|
866
|
+
assertType<typeof result.value, number>();
|
|
867
|
+
}
|
|
868
|
+
});
|
|
869
|
+
});
|
|
870
|
+
|
|
871
|
+
test("example: parseJson with early return", () => {
|
|
872
|
+
interface ParseJsonError {
|
|
873
|
+
readonly type: "ParseJsonError";
|
|
874
|
+
readonly message: string;
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
const parseJson = (value: string): Result<unknown, ParseJsonError> => {
|
|
878
|
+
try {
|
|
879
|
+
return ok(JSON.parse(value));
|
|
880
|
+
} catch (error) {
|
|
881
|
+
return err({ type: "ParseJsonError", message: String(error) });
|
|
882
|
+
}
|
|
883
|
+
};
|
|
884
|
+
|
|
885
|
+
const json = parseJson('{"key": "value"}');
|
|
886
|
+
|
|
887
|
+
if (!json.ok) return undefined;
|
|
888
|
+
|
|
889
|
+
assertType<typeof json.value, unknown>();
|
|
890
|
+
return undefined;
|
|
891
|
+
});
|
|
892
|
+
|
|
893
|
+
// --- Result with resource management ---
|
|
894
|
+
//
|
|
895
|
+
// Result and resource management are orthogonal concerns:
|
|
896
|
+
// - Result answers: "Did the operation succeed?"
|
|
897
|
+
// - Disposable answers: "When do we clean up resources?"
|
|
898
|
+
//
|
|
899
|
+
// Pattern:
|
|
900
|
+
// 1. Call a function that returns Result<Resource, Error>
|
|
901
|
+
// 2. If !result.ok, return early → disposal happens automatically
|
|
902
|
+
// 3. If result.ok, add result.value to the stack → resource gets tracked
|
|
903
|
+
|
|
904
|
+
interface CreateResourceError {
|
|
905
|
+
readonly type: "CreateResourceError";
|
|
906
|
+
readonly reason: string;
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
interface Resource extends Disposable {
|
|
910
|
+
readonly id: string;
|
|
911
|
+
readonly isDisposed: () => boolean;
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
interface AsyncResource extends AsyncDisposable {
|
|
915
|
+
readonly id: string;
|
|
916
|
+
readonly isDisposed: () => boolean;
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
const createMockResource = (id: string): Resource => {
|
|
920
|
+
let disposed = false;
|
|
921
|
+
return {
|
|
922
|
+
id,
|
|
923
|
+
isDisposed: () => disposed,
|
|
924
|
+
[Symbol.dispose]: () => {
|
|
925
|
+
disposed = true;
|
|
926
|
+
},
|
|
927
|
+
};
|
|
928
|
+
};
|
|
929
|
+
|
|
930
|
+
const createMockAsyncResource = (id: string): AsyncResource => {
|
|
931
|
+
let disposed = false;
|
|
932
|
+
return {
|
|
933
|
+
id,
|
|
934
|
+
isDisposed: () => disposed,
|
|
935
|
+
[Symbol.asyncDispose]: async () => {
|
|
936
|
+
await Promise.resolve();
|
|
937
|
+
disposed = true;
|
|
938
|
+
},
|
|
939
|
+
};
|
|
940
|
+
};
|
|
941
|
+
|
|
942
|
+
const createResource = (
|
|
943
|
+
id: string,
|
|
944
|
+
shouldFail: boolean,
|
|
945
|
+
): Result<Resource, CreateResourceError> => {
|
|
946
|
+
if (shouldFail) {
|
|
947
|
+
return err({
|
|
948
|
+
type: "CreateResourceError",
|
|
949
|
+
reason: `Failed to create ${id}`,
|
|
950
|
+
});
|
|
951
|
+
}
|
|
952
|
+
return ok(createMockResource(id));
|
|
953
|
+
};
|
|
954
|
+
|
|
955
|
+
const createAsyncResource = async (
|
|
956
|
+
id: string,
|
|
957
|
+
shouldFail: boolean,
|
|
958
|
+
): Promise<Result<AsyncResource, CreateResourceError>> => {
|
|
959
|
+
await Promise.resolve();
|
|
960
|
+
if (shouldFail) {
|
|
961
|
+
return err({
|
|
962
|
+
type: "CreateResourceError",
|
|
963
|
+
reason: `Failed to create ${id}`,
|
|
964
|
+
});
|
|
965
|
+
}
|
|
966
|
+
return ok(createMockAsyncResource(id));
|
|
967
|
+
};
|
|
968
|
+
|
|
969
|
+
describe("Result with Resource management", () => {
|
|
970
|
+
describe("using keyword", () => {
|
|
971
|
+
it("disposes on success", () => {
|
|
972
|
+
const resource = createResource("db", false);
|
|
973
|
+
if (!resource.ok) throw new Error("Should not fail");
|
|
974
|
+
|
|
975
|
+
{
|
|
976
|
+
using _ = resource.value;
|
|
977
|
+
assertFalse(resource.value.isDisposed());
|
|
978
|
+
}
|
|
979
|
+
|
|
980
|
+
assertTrue(resource.value.isDisposed());
|
|
981
|
+
});
|
|
982
|
+
|
|
983
|
+
it("disposes on early return", () => {
|
|
984
|
+
let resource = null as Resource | null;
|
|
985
|
+
|
|
986
|
+
const process = (): Result<string, CreateResourceError> => {
|
|
987
|
+
const result = createResource("db", false);
|
|
988
|
+
if (!result.ok) return result;
|
|
989
|
+
|
|
990
|
+
resource = result.value;
|
|
991
|
+
using _ = resource;
|
|
992
|
+
|
|
993
|
+
return err({ type: "CreateResourceError", reason: "other failure" });
|
|
994
|
+
};
|
|
995
|
+
|
|
996
|
+
const result = process();
|
|
997
|
+
assertErr(result, {
|
|
998
|
+
type: "CreateResourceError",
|
|
999
|
+
reason: "other failure",
|
|
1000
|
+
});
|
|
1001
|
+
assertTrue(resource?.isDisposed());
|
|
1002
|
+
});
|
|
1003
|
+
|
|
1004
|
+
it("disposes on throw", () => {
|
|
1005
|
+
let resource = null as Resource | null;
|
|
1006
|
+
|
|
1007
|
+
const process = (): void => {
|
|
1008
|
+
const result = createResource("db", false);
|
|
1009
|
+
if (!result.ok) throw new Error("Should not fail");
|
|
1010
|
+
|
|
1011
|
+
resource = result.value;
|
|
1012
|
+
using _ = resource;
|
|
1013
|
+
|
|
1014
|
+
throw new Error("Unexpected!");
|
|
1015
|
+
};
|
|
1016
|
+
|
|
1017
|
+
const error = assertThrowsInstanceOf(() => {
|
|
1018
|
+
process();
|
|
1019
|
+
}, Error);
|
|
1020
|
+
assertTrue(error.message.includes("Unexpected!"));
|
|
1021
|
+
assertTrue(resource?.isDisposed());
|
|
1022
|
+
});
|
|
1023
|
+
|
|
1024
|
+
// Block scopes control resource lifetime (RAII pattern).
|
|
1025
|
+
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/using#using_in_a_block
|
|
1026
|
+
it("disposes at block scope exit", () => {
|
|
1027
|
+
const log: Array<string> = [];
|
|
1028
|
+
|
|
1029
|
+
const createLock = (name: string): Disposable => ({
|
|
1030
|
+
[Symbol.dispose]: () => {
|
|
1031
|
+
log.push(`unlock:${name}`);
|
|
1032
|
+
},
|
|
1033
|
+
});
|
|
1034
|
+
|
|
1035
|
+
const process = (): void => {
|
|
1036
|
+
log.push("start");
|
|
1037
|
+
|
|
1038
|
+
{
|
|
1039
|
+
using _ = createLock("a");
|
|
1040
|
+
log.push("critical-section-a");
|
|
1041
|
+
// lock "a" released here
|
|
1042
|
+
}
|
|
1043
|
+
|
|
1044
|
+
log.push("between");
|
|
1045
|
+
|
|
1046
|
+
{
|
|
1047
|
+
using _ = createLock("b");
|
|
1048
|
+
log.push("critical-section-b");
|
|
1049
|
+
// lock "b" released here
|
|
1050
|
+
}
|
|
1051
|
+
|
|
1052
|
+
log.push("end");
|
|
1053
|
+
};
|
|
1054
|
+
|
|
1055
|
+
process();
|
|
1056
|
+
assertEqual(log, [
|
|
1057
|
+
"start",
|
|
1058
|
+
"critical-section-a",
|
|
1059
|
+
"unlock:a",
|
|
1060
|
+
"between",
|
|
1061
|
+
"critical-section-b",
|
|
1062
|
+
"unlock:b",
|
|
1063
|
+
"end",
|
|
1064
|
+
]);
|
|
1065
|
+
});
|
|
1066
|
+
});
|
|
1067
|
+
|
|
1068
|
+
describe("DisposableStack", () => {
|
|
1069
|
+
it("disposes resources on successful completion", () => {
|
|
1070
|
+
const disposed: Array<string> = [];
|
|
1071
|
+
|
|
1072
|
+
const processResources = (): Result<string, CreateResourceError> => {
|
|
1073
|
+
using disposer = new DisposableStack();
|
|
1074
|
+
|
|
1075
|
+
const resource1 = createResource("db", false);
|
|
1076
|
+
if (!resource1.ok) return resource1;
|
|
1077
|
+
disposer.use(resource1.value);
|
|
1078
|
+
disposer.defer(() => {
|
|
1079
|
+
disposed.push("db");
|
|
1080
|
+
});
|
|
1081
|
+
|
|
1082
|
+
const resource2 = createResource("file", false);
|
|
1083
|
+
if (!resource2.ok) return resource2;
|
|
1084
|
+
disposer.use(resource2.value);
|
|
1085
|
+
disposer.defer(() => {
|
|
1086
|
+
disposed.push("file");
|
|
1087
|
+
});
|
|
1088
|
+
|
|
1089
|
+
return ok("processed");
|
|
1090
|
+
};
|
|
1091
|
+
|
|
1092
|
+
const result = processResources();
|
|
1093
|
+
assertOk(result, "processed");
|
|
1094
|
+
assertEqual(disposed, ["file", "db"]);
|
|
1095
|
+
});
|
|
1096
|
+
|
|
1097
|
+
it("disposes created resources when later creation fails", () => {
|
|
1098
|
+
const disposed: Array<string> = [];
|
|
1099
|
+
|
|
1100
|
+
const processResources = (): Result<string, CreateResourceError> => {
|
|
1101
|
+
using disposer = new DisposableStack();
|
|
1102
|
+
|
|
1103
|
+
const resource1 = createResource("db", false);
|
|
1104
|
+
if (!resource1.ok) return resource1;
|
|
1105
|
+
disposer.use(resource1.value);
|
|
1106
|
+
disposer.defer(() => {
|
|
1107
|
+
disposed.push("db");
|
|
1108
|
+
});
|
|
1109
|
+
|
|
1110
|
+
const resource2 = createResource("file", true);
|
|
1111
|
+
if (!resource2.ok) return resource2;
|
|
1112
|
+
|
|
1113
|
+
disposer.use(resource2.value);
|
|
1114
|
+
disposer.defer(() => {
|
|
1115
|
+
disposed.push("file");
|
|
1116
|
+
});
|
|
1117
|
+
|
|
1118
|
+
return ok("processed");
|
|
1119
|
+
};
|
|
1120
|
+
|
|
1121
|
+
const result = processResources();
|
|
1122
|
+
assertErr(result, {
|
|
1123
|
+
type: "CreateResourceError",
|
|
1124
|
+
reason: "Failed to create file",
|
|
1125
|
+
});
|
|
1126
|
+
assertEqual(disposed, ["db"]);
|
|
1127
|
+
});
|
|
1128
|
+
|
|
1129
|
+
it("disposes nothing when first creation fails", () => {
|
|
1130
|
+
const disposed: Array<string> = [];
|
|
1131
|
+
|
|
1132
|
+
const processResources = (): Result<string, CreateResourceError> => {
|
|
1133
|
+
using disposer = new DisposableStack();
|
|
1134
|
+
|
|
1135
|
+
const resource1 = createResource("db", true);
|
|
1136
|
+
if (!resource1.ok) return resource1;
|
|
1137
|
+
disposer.use(resource1.value);
|
|
1138
|
+
disposer.defer(() => {
|
|
1139
|
+
disposed.push("db");
|
|
1140
|
+
});
|
|
1141
|
+
|
|
1142
|
+
return ok("processed");
|
|
1143
|
+
};
|
|
1144
|
+
|
|
1145
|
+
const result = processResources();
|
|
1146
|
+
assertErr(result, {
|
|
1147
|
+
type: "CreateResourceError",
|
|
1148
|
+
reason: "Failed to create db",
|
|
1149
|
+
});
|
|
1150
|
+
assertEqual(disposed, []);
|
|
1151
|
+
});
|
|
1152
|
+
|
|
1153
|
+
it("works with adopt for non-disposable values", () => {
|
|
1154
|
+
let connectionClosed = false;
|
|
1155
|
+
|
|
1156
|
+
interface Connection {
|
|
1157
|
+
readonly query: (sql: string) => Array<string>;
|
|
1158
|
+
}
|
|
1159
|
+
|
|
1160
|
+
const openConnection = (
|
|
1161
|
+
shouldFail: boolean,
|
|
1162
|
+
): Result<Connection, CreateResourceError> => {
|
|
1163
|
+
if (shouldFail) {
|
|
1164
|
+
return err({
|
|
1165
|
+
type: "CreateResourceError",
|
|
1166
|
+
reason: "Connection failed",
|
|
1167
|
+
});
|
|
1168
|
+
}
|
|
1169
|
+
return ok({
|
|
1170
|
+
query: (sql: string) => [`result for: ${sql}`],
|
|
1171
|
+
});
|
|
1172
|
+
};
|
|
1173
|
+
|
|
1174
|
+
const closeConnection = (_conn: Connection): void => {
|
|
1175
|
+
connectionClosed = true;
|
|
1176
|
+
};
|
|
1177
|
+
|
|
1178
|
+
const queryDatabase = (): Result<Array<string>, CreateResourceError> => {
|
|
1179
|
+
using disposer = new DisposableStack();
|
|
1180
|
+
|
|
1181
|
+
const conn = openConnection(false);
|
|
1182
|
+
if (!conn.ok) return conn;
|
|
1183
|
+
|
|
1184
|
+
disposer.adopt(conn.value, closeConnection);
|
|
1185
|
+
|
|
1186
|
+
return ok(conn.value.query("SELECT * FROM users"));
|
|
1187
|
+
};
|
|
1188
|
+
|
|
1189
|
+
const result = queryDatabase();
|
|
1190
|
+
assertOk(result, ["result for: SELECT * FROM users"]);
|
|
1191
|
+
assertTrue(connectionClosed);
|
|
1192
|
+
});
|
|
1193
|
+
|
|
1194
|
+
it("handles multiple resources with mixed success/failure", () => {
|
|
1195
|
+
const log: Array<string> = [];
|
|
1196
|
+
|
|
1197
|
+
interface ProcessingError {
|
|
1198
|
+
readonly type: "ProcessingError";
|
|
1199
|
+
readonly step: string;
|
|
1200
|
+
}
|
|
1201
|
+
|
|
1202
|
+
type MyError = CreateResourceError | ProcessingError;
|
|
1203
|
+
|
|
1204
|
+
const process = (): Result<void, MyError> => {
|
|
1205
|
+
using disposer = new DisposableStack();
|
|
1206
|
+
|
|
1207
|
+
const db = createResource("db", false);
|
|
1208
|
+
if (!db.ok) return db;
|
|
1209
|
+
disposer.use(db.value);
|
|
1210
|
+
disposer.defer(() => {
|
|
1211
|
+
log.push("cleanup:db");
|
|
1212
|
+
});
|
|
1213
|
+
|
|
1214
|
+
const cache = createResource("cache", false);
|
|
1215
|
+
if (!cache.ok) return cache;
|
|
1216
|
+
disposer.use(cache.value);
|
|
1217
|
+
disposer.defer(() => {
|
|
1218
|
+
log.push("cleanup:cache");
|
|
1219
|
+
});
|
|
1220
|
+
|
|
1221
|
+
log.push("work:step1");
|
|
1222
|
+
|
|
1223
|
+
const step2Result = err({
|
|
1224
|
+
type: "ProcessingError",
|
|
1225
|
+
step: "step2",
|
|
1226
|
+
}) as Result<void, ProcessingError>;
|
|
1227
|
+
if (!step2Result.ok) return step2Result;
|
|
1228
|
+
|
|
1229
|
+
log.push("work:step2");
|
|
1230
|
+
return ok();
|
|
1231
|
+
};
|
|
1232
|
+
|
|
1233
|
+
const result = process();
|
|
1234
|
+
assertErr(result, { type: "ProcessingError", step: "step2" });
|
|
1235
|
+
assertEqual(log, ["work:step1", "cleanup:cache", "cleanup:db"]);
|
|
1236
|
+
});
|
|
1237
|
+
|
|
1238
|
+
it("disposes resources even when unexpected error is thrown", () => {
|
|
1239
|
+
const disposed: Array<string> = [];
|
|
1240
|
+
|
|
1241
|
+
const processResources = (): Result<string, CreateResourceError> => {
|
|
1242
|
+
using disposer = new DisposableStack();
|
|
1243
|
+
|
|
1244
|
+
const resource1 = createResource("db", false);
|
|
1245
|
+
if (!resource1.ok) return resource1;
|
|
1246
|
+
disposer.use(resource1.value);
|
|
1247
|
+
disposer.defer(() => {
|
|
1248
|
+
disposed.push("db");
|
|
1249
|
+
});
|
|
1250
|
+
|
|
1251
|
+
// Simulate unexpected error (bug in code, not a Result error)
|
|
1252
|
+
throw new Error("Unexpected bug!");
|
|
1253
|
+
|
|
1254
|
+
// This code is unreachable but shows the pattern
|
|
1255
|
+
// return ok("processed");
|
|
1256
|
+
};
|
|
1257
|
+
|
|
1258
|
+
// The unexpected error propagates, but disposal still happens
|
|
1259
|
+
const error = assertThrowsInstanceOf(() => processResources(), Error);
|
|
1260
|
+
assertTrue(error.message.includes("Unexpected bug!"));
|
|
1261
|
+
assertEqual(disposed, ["db"]);
|
|
1262
|
+
});
|
|
1263
|
+
|
|
1264
|
+
it("transfers ownership with move()", () => {
|
|
1265
|
+
const disposed: Array<string> = [];
|
|
1266
|
+
|
|
1267
|
+
const createResources = (): Result<
|
|
1268
|
+
DisposableStack,
|
|
1269
|
+
CreateResourceError
|
|
1270
|
+
> => {
|
|
1271
|
+
using disposer = new DisposableStack();
|
|
1272
|
+
|
|
1273
|
+
const r1 = createResource("a", false);
|
|
1274
|
+
if (!r1.ok) return r1;
|
|
1275
|
+
disposer.use(r1.value);
|
|
1276
|
+
disposer.defer(() => {
|
|
1277
|
+
disposed.push("a");
|
|
1278
|
+
});
|
|
1279
|
+
|
|
1280
|
+
const r2 = createResource("b", false);
|
|
1281
|
+
if (!r2.ok) return r2;
|
|
1282
|
+
disposer.use(r2.value);
|
|
1283
|
+
disposer.defer(() => {
|
|
1284
|
+
disposed.push("b");
|
|
1285
|
+
});
|
|
1286
|
+
|
|
1287
|
+
return ok(disposer.move());
|
|
1288
|
+
};
|
|
1289
|
+
|
|
1290
|
+
interface TransferError {
|
|
1291
|
+
readonly type: "TransferError";
|
|
1292
|
+
}
|
|
1293
|
+
|
|
1294
|
+
const useResources = (): Result<
|
|
1295
|
+
void,
|
|
1296
|
+
CreateResourceError | TransferError
|
|
1297
|
+
> => {
|
|
1298
|
+
const resources = createResources();
|
|
1299
|
+
if (!resources.ok) return resources;
|
|
1300
|
+
|
|
1301
|
+
using _ = resources.value;
|
|
1302
|
+
|
|
1303
|
+
disposed.push("work");
|
|
1304
|
+
|
|
1305
|
+
return ok();
|
|
1306
|
+
};
|
|
1307
|
+
|
|
1308
|
+
const result = useResources();
|
|
1309
|
+
assertOk(result, undefined);
|
|
1310
|
+
assertEqual(disposed, ["work", "b", "a"]);
|
|
1311
|
+
});
|
|
1312
|
+
});
|
|
1313
|
+
|
|
1314
|
+
describe("AsyncDisposableStack", () => {
|
|
1315
|
+
it("disposes async resources on successful completion", async () => {
|
|
1316
|
+
const disposed: Array<string> = [];
|
|
1317
|
+
|
|
1318
|
+
const processResources = async (): Promise<
|
|
1319
|
+
Result<string, CreateResourceError>
|
|
1320
|
+
> => {
|
|
1321
|
+
await using disposer = new AsyncDisposableStack();
|
|
1322
|
+
|
|
1323
|
+
const resource1 = await createAsyncResource("db", false);
|
|
1324
|
+
if (!resource1.ok) return resource1;
|
|
1325
|
+
disposer.use(resource1.value);
|
|
1326
|
+
disposer.defer(async () => {
|
|
1327
|
+
await Promise.resolve();
|
|
1328
|
+
disposed.push("db");
|
|
1329
|
+
});
|
|
1330
|
+
|
|
1331
|
+
const resource2 = await createAsyncResource("file", false);
|
|
1332
|
+
if (!resource2.ok) return resource2;
|
|
1333
|
+
disposer.use(resource2.value);
|
|
1334
|
+
disposer.defer(async () => {
|
|
1335
|
+
await Promise.resolve();
|
|
1336
|
+
disposed.push("file");
|
|
1337
|
+
});
|
|
1338
|
+
|
|
1339
|
+
return ok("processed");
|
|
1340
|
+
};
|
|
1341
|
+
|
|
1342
|
+
const result = await processResources();
|
|
1343
|
+
assertOk(result, "processed");
|
|
1344
|
+
assertEqual(disposed, ["file", "db"]);
|
|
1345
|
+
});
|
|
1346
|
+
|
|
1347
|
+
it("disposes created async resources when later creation fails", async () => {
|
|
1348
|
+
const disposed: Array<string> = [];
|
|
1349
|
+
|
|
1350
|
+
const processResources = async (): Promise<
|
|
1351
|
+
Result<string, CreateResourceError>
|
|
1352
|
+
> => {
|
|
1353
|
+
await using disposer = new AsyncDisposableStack();
|
|
1354
|
+
|
|
1355
|
+
const resource1 = await createAsyncResource("db", false);
|
|
1356
|
+
if (!resource1.ok) return resource1;
|
|
1357
|
+
disposer.use(resource1.value);
|
|
1358
|
+
disposer.defer(async () => {
|
|
1359
|
+
await Promise.resolve();
|
|
1360
|
+
disposed.push("db");
|
|
1361
|
+
});
|
|
1362
|
+
|
|
1363
|
+
const resource2 = await createAsyncResource("file", true);
|
|
1364
|
+
if (!resource2.ok) return resource2;
|
|
1365
|
+
disposer.use(resource2.value);
|
|
1366
|
+
disposer.defer(async () => {
|
|
1367
|
+
await Promise.resolve();
|
|
1368
|
+
disposed.push("file");
|
|
1369
|
+
});
|
|
1370
|
+
|
|
1371
|
+
return ok("processed");
|
|
1372
|
+
};
|
|
1373
|
+
|
|
1374
|
+
const result = await processResources();
|
|
1375
|
+
assertErr(result, {
|
|
1376
|
+
type: "CreateResourceError",
|
|
1377
|
+
reason: "Failed to create file",
|
|
1378
|
+
});
|
|
1379
|
+
assertEqual(disposed, ["db"]);
|
|
1380
|
+
});
|
|
1381
|
+
|
|
1382
|
+
it("can mix sync and async resources", async () => {
|
|
1383
|
+
const disposed: Array<string> = [];
|
|
1384
|
+
|
|
1385
|
+
const processResources = async (): Promise<
|
|
1386
|
+
Result<string, CreateResourceError>
|
|
1387
|
+
> => {
|
|
1388
|
+
await using disposer = new AsyncDisposableStack();
|
|
1389
|
+
|
|
1390
|
+
const syncResource = createResource("sync", false);
|
|
1391
|
+
if (!syncResource.ok) return syncResource;
|
|
1392
|
+
disposer.use(syncResource.value);
|
|
1393
|
+
disposer.defer(() => {
|
|
1394
|
+
disposed.push("sync");
|
|
1395
|
+
});
|
|
1396
|
+
|
|
1397
|
+
const asyncResource = await createAsyncResource("async", false);
|
|
1398
|
+
if (!asyncResource.ok) return asyncResource;
|
|
1399
|
+
disposer.use(asyncResource.value);
|
|
1400
|
+
disposer.defer(async () => {
|
|
1401
|
+
await Promise.resolve();
|
|
1402
|
+
disposed.push("async");
|
|
1403
|
+
});
|
|
1404
|
+
|
|
1405
|
+
return ok("mixed");
|
|
1406
|
+
};
|
|
1407
|
+
|
|
1408
|
+
const result = await processResources();
|
|
1409
|
+
assertOk(result, "mixed");
|
|
1410
|
+
assertEqual(disposed, ["async", "sync"]);
|
|
1411
|
+
});
|
|
1412
|
+
});
|
|
1413
|
+
});
|
|
1414
|
+
|
|
1415
|
+
/**
|
|
1416
|
+
* Evolu uses plain Result objects and explicit checks instead of
|
|
1417
|
+
* generator-based composition.
|
|
1418
|
+
*
|
|
1419
|
+
* Generators make sequential workflows more concise because `yield*` combines
|
|
1420
|
+
* error propagation with unwrapping the success value. That concision adds
|
|
1421
|
+
* generator machinery and makes control flow less explicit. With AI coding
|
|
1422
|
+
* tools, writing `if (!result.ok) return result` is cheap, so saving those
|
|
1423
|
+
* lines is less important.
|
|
1424
|
+
*
|
|
1425
|
+
* Generators also do not prevent accidental omission. A lazy operation called
|
|
1426
|
+
* without `yield*` is left out of the workflow, just as a Result can be
|
|
1427
|
+
* ignored. Dedicated tooling can detect either mistake, while tests remain the
|
|
1428
|
+
* runnable specification for successful and failing paths.
|
|
1429
|
+
*
|
|
1430
|
+
* A historical Apple M1 microbenchmark used 500,000 iterations of a three-step
|
|
1431
|
+
* Result chain and produced these rough numbers:
|
|
1432
|
+
*
|
|
1433
|
+
* - Imperative: ~25 ms
|
|
1434
|
+
* - Generator with a wrapper: ~330 ms (~13x slower)
|
|
1435
|
+
* - Iterable Result with an inline generator: ~1200 ms (~48x slower)
|
|
1436
|
+
* - Iterable Result with a hoisted generator: ~990 ms (~40x slower)
|
|
1437
|
+
*
|
|
1438
|
+
* This artificial workload does not predict application performance. It only
|
|
1439
|
+
* demonstrates that generator composition has measurable runtime overhead in a
|
|
1440
|
+
* tight synchronous loop. The skipped test below preserves the experiment.
|
|
1441
|
+
*/
|
|
1442
|
+
describe("design decisions", () => {
|
|
1443
|
+
describe("generators", () => {
|
|
1444
|
+
interface ParseError {
|
|
1445
|
+
readonly type: "ParseError";
|
|
1446
|
+
}
|
|
1447
|
+
|
|
1448
|
+
interface ValidationError {
|
|
1449
|
+
readonly type: "ValidationError";
|
|
1450
|
+
}
|
|
1451
|
+
|
|
1452
|
+
/** A generator that yields errors and returns a value on success. */
|
|
1453
|
+
type Gen<T, E> = Generator<Err<E>, T>;
|
|
1454
|
+
|
|
1455
|
+
/**
|
|
1456
|
+
* Converts a Result to a Gen for use with yield*.
|
|
1457
|
+
*
|
|
1458
|
+
* @yields {Err<E>} Err if the result is an error
|
|
1459
|
+
*/
|
|
1460
|
+
// oxlint-disable-next-line eslint/func-style -- Generators require the function keyword.
|
|
1461
|
+
function* gen<T, E>(result: Result<T, E>): Gen<T, E> {
|
|
1462
|
+
if (result.ok) {
|
|
1463
|
+
return result.value;
|
|
1464
|
+
}
|
|
1465
|
+
yield result;
|
|
1466
|
+
// This line is never reached - the runner exits on first yielded Err
|
|
1467
|
+
throw new Error("Unreachable");
|
|
1468
|
+
}
|
|
1469
|
+
|
|
1470
|
+
/** Runs a Gen and returns the Result. */
|
|
1471
|
+
const runGen = <T, E>(gen: Gen<T, E>): Result<T, E> => {
|
|
1472
|
+
const next = gen.next();
|
|
1473
|
+
if (!next.done) {
|
|
1474
|
+
// Generator yielded an Err - force cleanup by calling return()
|
|
1475
|
+
// This triggers finally blocks and `using` disposal in the generator
|
|
1476
|
+
gen.return(undefined as T);
|
|
1477
|
+
return next.value;
|
|
1478
|
+
}
|
|
1479
|
+
return ok(next.value);
|
|
1480
|
+
};
|
|
1481
|
+
|
|
1482
|
+
const parse = (input: string): Result<number, ParseError> => {
|
|
1483
|
+
const n = parseInt(input, 10);
|
|
1484
|
+
return isNaN(n) ? err({ type: "ParseError" }) : ok(n);
|
|
1485
|
+
};
|
|
1486
|
+
|
|
1487
|
+
const validate = (n: number): Result<number, ValidationError> =>
|
|
1488
|
+
n > 0 ? ok(n) : err({ type: "ValidationError" });
|
|
1489
|
+
|
|
1490
|
+
const double = (n: number): Result<number> => ok(n * 2);
|
|
1491
|
+
|
|
1492
|
+
it("composes multiple Results with generators", () => {
|
|
1493
|
+
const program = function* (
|
|
1494
|
+
input: string,
|
|
1495
|
+
): Gen<number, ParseError | ValidationError> {
|
|
1496
|
+
const parsed = yield* gen(parse(input));
|
|
1497
|
+
const validated = yield* gen(validate(parsed));
|
|
1498
|
+
const doubled = yield* gen(double(validated));
|
|
1499
|
+
return doubled;
|
|
1500
|
+
};
|
|
1501
|
+
|
|
1502
|
+
// Success case
|
|
1503
|
+
const success = runGen(program("21"));
|
|
1504
|
+
assertEqual(success, ok(42));
|
|
1505
|
+
|
|
1506
|
+
// Parse error
|
|
1507
|
+
const parseErr = runGen(program("not a number"));
|
|
1508
|
+
assertEqual(parseErr, err({ type: "ParseError" }));
|
|
1509
|
+
|
|
1510
|
+
// Validation error
|
|
1511
|
+
const validationErr = runGen(program("-5"));
|
|
1512
|
+
assertEqual(validationErr, err({ type: "ValidationError" }));
|
|
1513
|
+
});
|
|
1514
|
+
|
|
1515
|
+
it("is equivalent to imperative pattern", () => {
|
|
1516
|
+
// Generator version
|
|
1517
|
+
const withGenerator = (
|
|
1518
|
+
input: string,
|
|
1519
|
+
): Result<number, ParseError | ValidationError> => {
|
|
1520
|
+
const program = function* (): Gen<
|
|
1521
|
+
number,
|
|
1522
|
+
ParseError | ValidationError
|
|
1523
|
+
> {
|
|
1524
|
+
const parsed = yield* gen(parse(input));
|
|
1525
|
+
const validated = yield* gen(validate(parsed));
|
|
1526
|
+
const doubled = yield* gen(double(validated));
|
|
1527
|
+
return doubled;
|
|
1528
|
+
};
|
|
1529
|
+
return runGen(program());
|
|
1530
|
+
};
|
|
1531
|
+
|
|
1532
|
+
// Imperative version
|
|
1533
|
+
const imperative = (
|
|
1534
|
+
input: string,
|
|
1535
|
+
): Result<number, ParseError | ValidationError> => {
|
|
1536
|
+
const parsed = parse(input);
|
|
1537
|
+
if (!parsed.ok) return parsed;
|
|
1538
|
+
|
|
1539
|
+
const validated = validate(parsed.value);
|
|
1540
|
+
if (!validated.ok) return validated;
|
|
1541
|
+
|
|
1542
|
+
const doubled = double(validated.value);
|
|
1543
|
+
if (!doubled.ok) return doubled;
|
|
1544
|
+
|
|
1545
|
+
return ok(doubled.value);
|
|
1546
|
+
};
|
|
1547
|
+
|
|
1548
|
+
// Both produce identical results
|
|
1549
|
+
assertEqual(withGenerator("21"), imperative("21"));
|
|
1550
|
+
assertEqual(withGenerator("abc"), imperative("abc"));
|
|
1551
|
+
assertEqual(withGenerator("-5"), imperative("-5"));
|
|
1552
|
+
});
|
|
1553
|
+
|
|
1554
|
+
it("shows type inference works correctly", () => {
|
|
1555
|
+
const program = function* (): Gen<number, ParseError | ValidationError> {
|
|
1556
|
+
const a = yield* gen(parse("10"));
|
|
1557
|
+
const b = yield* gen(validate(a));
|
|
1558
|
+
return b * 2;
|
|
1559
|
+
};
|
|
1560
|
+
|
|
1561
|
+
const result = runGen(program());
|
|
1562
|
+
|
|
1563
|
+
assertType<typeof result, Result<number, ParseError | ValidationError>>();
|
|
1564
|
+
});
|
|
1565
|
+
|
|
1566
|
+
it.skip("generator vs imperative performance", () => {
|
|
1567
|
+
const ITERATIONS = 500_000;
|
|
1568
|
+
|
|
1569
|
+
const withGenerator = (input: string): Result<number, ParseError> =>
|
|
1570
|
+
runGen(
|
|
1571
|
+
(function* (): Gen<number, ParseError> {
|
|
1572
|
+
const a = yield* gen(parse(input));
|
|
1573
|
+
const b = yield* gen(parse(String(a + 1)));
|
|
1574
|
+
const c = yield* gen(parse(String(b + 1)));
|
|
1575
|
+
return c;
|
|
1576
|
+
})(),
|
|
1577
|
+
);
|
|
1578
|
+
|
|
1579
|
+
type IterableResult<T, E> =
|
|
1580
|
+
| {
|
|
1581
|
+
readonly ok: true;
|
|
1582
|
+
readonly value: T;
|
|
1583
|
+
[Symbol.iterator](): Gen<T, E>;
|
|
1584
|
+
}
|
|
1585
|
+
| {
|
|
1586
|
+
readonly ok: false;
|
|
1587
|
+
readonly error: E;
|
|
1588
|
+
[Symbol.iterator](): Gen<T, E>;
|
|
1589
|
+
};
|
|
1590
|
+
|
|
1591
|
+
const iterableOk = <T, E = never>(value: T): IterableResult<T, E> => ({
|
|
1592
|
+
ok: true,
|
|
1593
|
+
value,
|
|
1594
|
+
// oxlint-disable-next-line eslint/require-yield
|
|
1595
|
+
*[Symbol.iterator]() {
|
|
1596
|
+
return value;
|
|
1597
|
+
},
|
|
1598
|
+
});
|
|
1599
|
+
|
|
1600
|
+
const iterableErr = <E, T = never>(error: E): IterableResult<T, E> => ({
|
|
1601
|
+
ok: false,
|
|
1602
|
+
error,
|
|
1603
|
+
*[Symbol.iterator]() {
|
|
1604
|
+
yield { ok: false, error };
|
|
1605
|
+
throw new Error("Unreachable");
|
|
1606
|
+
},
|
|
1607
|
+
});
|
|
1608
|
+
|
|
1609
|
+
const parseIterable = (
|
|
1610
|
+
input: string,
|
|
1611
|
+
): IterableResult<number, ParseError> => {
|
|
1612
|
+
const n = parseInt(input, 10);
|
|
1613
|
+
return isNaN(n) ? iterableErr({ type: "ParseError" }) : iterableOk(n);
|
|
1614
|
+
};
|
|
1615
|
+
|
|
1616
|
+
const withIterableIterator = (
|
|
1617
|
+
input: string,
|
|
1618
|
+
): Result<number, ParseError> =>
|
|
1619
|
+
runGen(
|
|
1620
|
+
(function* (): Gen<number, ParseError> {
|
|
1621
|
+
const a = yield* parseIterable(input);
|
|
1622
|
+
const b = yield* parseIterable(String(a + 1));
|
|
1623
|
+
const c = yield* parseIterable(String(b + 1));
|
|
1624
|
+
return c;
|
|
1625
|
+
})(),
|
|
1626
|
+
);
|
|
1627
|
+
|
|
1628
|
+
const iterableProgram = function* (
|
|
1629
|
+
input: string,
|
|
1630
|
+
): Gen<number, ParseError> {
|
|
1631
|
+
const a = yield* parseIterable(input);
|
|
1632
|
+
const b = yield* parseIterable(String(a + 1));
|
|
1633
|
+
const c = yield* parseIterable(String(b + 1));
|
|
1634
|
+
return c;
|
|
1635
|
+
};
|
|
1636
|
+
const withIterableIteratorHoisted = (
|
|
1637
|
+
input: string,
|
|
1638
|
+
): Result<number, ParseError> => runGen(iterableProgram(input));
|
|
1639
|
+
|
|
1640
|
+
const imperative = (input: string): Result<number, ParseError> => {
|
|
1641
|
+
const a = parse(input);
|
|
1642
|
+
if (!a.ok) return a;
|
|
1643
|
+
const b = parse(String(a.value + 1));
|
|
1644
|
+
if (!b.ok) return b;
|
|
1645
|
+
const c = parse(String(b.value + 1));
|
|
1646
|
+
if (!c.ok) return c;
|
|
1647
|
+
return ok(c.value);
|
|
1648
|
+
};
|
|
1649
|
+
|
|
1650
|
+
const generatorStart = performance.now();
|
|
1651
|
+
for (let i = 0; i < ITERATIONS; i++) {
|
|
1652
|
+
withGenerator("1");
|
|
1653
|
+
}
|
|
1654
|
+
const generatorTime = performance.now() - generatorStart;
|
|
1655
|
+
|
|
1656
|
+
const iterableStart = performance.now();
|
|
1657
|
+
for (let i = 0; i < ITERATIONS; i++) {
|
|
1658
|
+
withIterableIterator("1");
|
|
1659
|
+
}
|
|
1660
|
+
const iterableTime = performance.now() - iterableStart;
|
|
1661
|
+
|
|
1662
|
+
const iterableHoistedStart = performance.now();
|
|
1663
|
+
for (let i = 0; i < ITERATIONS; i++) {
|
|
1664
|
+
withIterableIteratorHoisted("1");
|
|
1665
|
+
}
|
|
1666
|
+
const iterableHoistedTime = performance.now() - iterableHoistedStart;
|
|
1667
|
+
|
|
1668
|
+
const imperativeStart = performance.now();
|
|
1669
|
+
for (let i = 0; i < ITERATIONS; i++) {
|
|
1670
|
+
imperative("1");
|
|
1671
|
+
}
|
|
1672
|
+
const imperativeTime = performance.now() - imperativeStart;
|
|
1673
|
+
|
|
1674
|
+
/* oxlint-disable eslint/no-console -- This benchmark reports each measured implementation. */
|
|
1675
|
+
console.log(`Generator (wrapper): ${generatorTime.toFixed(2)} ms`);
|
|
1676
|
+
console.log(`Iterable (inline): ${iterableTime.toFixed(2)} ms`);
|
|
1677
|
+
console.log(`Iterable (hoisted): ${iterableHoistedTime.toFixed(2)} ms`);
|
|
1678
|
+
console.log(`Imperative: ${imperativeTime.toFixed(2)} ms`);
|
|
1679
|
+
console.log(
|
|
1680
|
+
`Generator wrapper is ${(generatorTime / imperativeTime).toFixed(1)}x slower`,
|
|
1681
|
+
);
|
|
1682
|
+
console.log(
|
|
1683
|
+
`Iterable inline is ${(iterableTime / imperativeTime).toFixed(1)}x slower`,
|
|
1684
|
+
);
|
|
1685
|
+
console.log(
|
|
1686
|
+
`Iterable hoisted is ${(iterableHoistedTime / imperativeTime).toFixed(1)}x slower`,
|
|
1687
|
+
);
|
|
1688
|
+
/* oxlint-enable eslint/no-console */
|
|
1689
|
+
});
|
|
1690
|
+
|
|
1691
|
+
it("does not run a lazy generator called without yield*", () => {
|
|
1692
|
+
let operationRun = false;
|
|
1693
|
+
|
|
1694
|
+
// oxlint-disable-next-line eslint/require-yield -- Generator bodies are lazy even without yield.
|
|
1695
|
+
const lazyOperation = function* (): Gen<number, never> {
|
|
1696
|
+
operationRun = true;
|
|
1697
|
+
return 1;
|
|
1698
|
+
};
|
|
1699
|
+
|
|
1700
|
+
const program = function* (): Gen<void, never> {
|
|
1701
|
+
lazyOperation();
|
|
1702
|
+
return yield* gen(ok());
|
|
1703
|
+
};
|
|
1704
|
+
|
|
1705
|
+
assertOk(runGen(program()), undefined);
|
|
1706
|
+
assertFalse(operationRun);
|
|
1707
|
+
});
|
|
1708
|
+
|
|
1709
|
+
it("disposes resources when generator exits early on error", () => {
|
|
1710
|
+
// This test demonstrates that runGen properly cleans up resources
|
|
1711
|
+
// by calling gen.return() when it encounters an error.
|
|
1712
|
+
// This triggers finally blocks and `using` disposal in the generator.
|
|
1713
|
+
|
|
1714
|
+
const disposed: Array<string> = [];
|
|
1715
|
+
|
|
1716
|
+
const createTestResource = (
|
|
1717
|
+
id: string,
|
|
1718
|
+
shouldFail: boolean,
|
|
1719
|
+
): Result<Disposable, ParseError> => {
|
|
1720
|
+
if (shouldFail) return err({ type: "ParseError" });
|
|
1721
|
+
return ok({
|
|
1722
|
+
[Symbol.dispose]: () => {
|
|
1723
|
+
disposed.push(id);
|
|
1724
|
+
},
|
|
1725
|
+
});
|
|
1726
|
+
};
|
|
1727
|
+
|
|
1728
|
+
const program = function* (): Gen<string, ParseError> {
|
|
1729
|
+
using disposer = new DisposableStack();
|
|
1730
|
+
|
|
1731
|
+
const r1 = yield* gen(createTestResource("db", false));
|
|
1732
|
+
disposer.use(r1);
|
|
1733
|
+
|
|
1734
|
+
// This fails - generator yields Err and runGen calls gen.return()
|
|
1735
|
+
const r2 = yield* gen(createTestResource("file", true));
|
|
1736
|
+
disposer.use(r2);
|
|
1737
|
+
|
|
1738
|
+
return "done";
|
|
1739
|
+
};
|
|
1740
|
+
|
|
1741
|
+
const result = runGen(program());
|
|
1742
|
+
|
|
1743
|
+
assertErr(result, { type: "ParseError" });
|
|
1744
|
+
// Resources ARE disposed because runGen calls gen.return() on error
|
|
1745
|
+
assertEqual(disposed, ["db"]);
|
|
1746
|
+
});
|
|
1747
|
+
|
|
1748
|
+
it("disposes resources when generator completes successfully", () => {
|
|
1749
|
+
const disposed: Array<string> = [];
|
|
1750
|
+
|
|
1751
|
+
const createTestResource = (id: string): Result<Disposable, ParseError> =>
|
|
1752
|
+
ok({
|
|
1753
|
+
[Symbol.dispose]: () => {
|
|
1754
|
+
disposed.push(id);
|
|
1755
|
+
},
|
|
1756
|
+
});
|
|
1757
|
+
|
|
1758
|
+
const program = function* (): Gen<string, ParseError> {
|
|
1759
|
+
using disposer = new DisposableStack();
|
|
1760
|
+
|
|
1761
|
+
const r1 = yield* gen(createTestResource("db"));
|
|
1762
|
+
disposer.use(r1);
|
|
1763
|
+
|
|
1764
|
+
const r2 = yield* gen(createTestResource("file"));
|
|
1765
|
+
disposer.use(r2);
|
|
1766
|
+
|
|
1767
|
+
return "done";
|
|
1768
|
+
};
|
|
1769
|
+
|
|
1770
|
+
const result = runGen(program());
|
|
1771
|
+
|
|
1772
|
+
assertOk(result, "done");
|
|
1773
|
+
// Resources ARE disposed on successful completion
|
|
1774
|
+
assertEqual(disposed, ["file", "db"]);
|
|
1775
|
+
});
|
|
1776
|
+
|
|
1777
|
+
it("supports direct yield* with the iterator protocol", () => {
|
|
1778
|
+
type IterableResult<T, E> =
|
|
1779
|
+
| {
|
|
1780
|
+
readonly ok: true;
|
|
1781
|
+
readonly value: T;
|
|
1782
|
+
[Symbol.iterator](): Gen<T, E>;
|
|
1783
|
+
}
|
|
1784
|
+
| {
|
|
1785
|
+
readonly ok: false;
|
|
1786
|
+
readonly error: E;
|
|
1787
|
+
[Symbol.iterator](): Gen<T, E>;
|
|
1788
|
+
};
|
|
1789
|
+
|
|
1790
|
+
const iterableOk = <T, E = never>(value: T): IterableResult<T, E> => ({
|
|
1791
|
+
ok: true,
|
|
1792
|
+
value,
|
|
1793
|
+
// oxlint-disable-next-line eslint/require-yield
|
|
1794
|
+
*[Symbol.iterator]() {
|
|
1795
|
+
return value;
|
|
1796
|
+
},
|
|
1797
|
+
});
|
|
1798
|
+
|
|
1799
|
+
const iterableErr = <E, T = never>(error: E): IterableResult<T, E> => ({
|
|
1800
|
+
ok: false,
|
|
1801
|
+
error,
|
|
1802
|
+
*[Symbol.iterator]() {
|
|
1803
|
+
yield { ok: false, error };
|
|
1804
|
+
throw new Error("Unreachable");
|
|
1805
|
+
},
|
|
1806
|
+
});
|
|
1807
|
+
|
|
1808
|
+
const parse = (input: string): IterableResult<number, ParseError> => {
|
|
1809
|
+
const n = parseInt(input, 10);
|
|
1810
|
+
return isNaN(n) ? iterableErr({ type: "ParseError" }) : iterableOk(n);
|
|
1811
|
+
};
|
|
1812
|
+
|
|
1813
|
+
const validate = (n: number): IterableResult<number, ValidationError> =>
|
|
1814
|
+
n > 0 ? iterableOk(n) : iterableErr({ type: "ValidationError" });
|
|
1815
|
+
|
|
1816
|
+
const program = function* (
|
|
1817
|
+
input: string,
|
|
1818
|
+
): Gen<number, ParseError | ValidationError> {
|
|
1819
|
+
const parsed = yield* parse(input);
|
|
1820
|
+
const validated = yield* validate(parsed);
|
|
1821
|
+
return validated * 2;
|
|
1822
|
+
};
|
|
1823
|
+
|
|
1824
|
+
assertEqual(runGen(program("21")), ok(42));
|
|
1825
|
+
assertEqual(runGen(program("abc")), err({ type: "ParseError" }));
|
|
1826
|
+
assertEqual(runGen(program("-5")), err({ type: "ValidationError" }));
|
|
1827
|
+
});
|
|
1828
|
+
});
|
|
1829
|
+
});
|