@planet-matrix/mobius-model 0.9.0 → 0.10.1
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/CHANGELOG.md +13 -0
- package/oxlint.config.ts +1 -2
- package/package.json +5 -5
- package/scripts/build.ts +2 -52
- package/src/basic/promise.ts +141 -71
- package/src/drizzle/pagination.ts +0 -2
- package/src/event/class-event-proxy.ts +0 -2
- package/src/event/instance-event-proxy.ts +0 -2
- package/src/exception/README.md +28 -19
- package/src/exception/error/error.ts +123 -0
- package/src/exception/error/index.ts +2 -0
- package/src/exception/error/match.ts +38 -0
- package/src/exception/error/must-fix.ts +17 -0
- package/src/exception/index.ts +2 -0
- package/src/file-system/find.ts +53 -0
- package/src/file-system/index.ts +2 -0
- package/src/file-system/path.ts +76 -0
- package/src/file-system/resolve.ts +22 -0
- package/src/form/inputor-controller/base.ts +0 -13
- package/src/form/inputor-controller/form.ts +0 -2
- package/src/http/api/api-type.ts +0 -3
- package/src/http/api-adapter/api-result-arktype.ts +0 -3
- package/src/index.ts +2 -0
- package/src/openai/openai.ts +0 -1
- package/src/request/fetch/browser.ts +0 -5
- package/src/request/fetch/nodejs.ts +0 -5
- package/src/request/request/base.ts +0 -4
- package/src/request/request/general.ts +0 -1
- package/src/result/controller.ts +11 -7
- package/src/result/either.ts +230 -60
- package/src/result/generator.ts +168 -0
- package/src/result/index.ts +1 -0
- package/src/route/router/router.ts +0 -1
- package/src/route/uri/hash.ts +0 -1
- package/src/route/uri/search.ts +0 -1
- package/src/service/README.md +1 -0
- package/src/service/index.ts +1 -0
- package/src/service/service.ts +110 -0
- package/src/socket/client/socket-unit.ts +0 -2
- package/src/socket/server/socket-unit.ts +0 -1
- package/src/tube/helper.ts +0 -1
- package/src/weixin/official-account/authorization.ts +0 -2
- package/src/weixin/official-account/js-api.ts +0 -2
- package/src/weixin/open/oauth2.ts +0 -2
- package/tests/unit/aio/json.spec.ts +0 -1
- package/tests/unit/basic/promise.spec.ts +158 -50
- package/tests/unit/credential/api-key.spec.ts +0 -1
- package/tests/unit/credential/password.spec.ts +0 -1
- package/tests/unit/exception/error/error.spec.ts +83 -0
- package/tests/unit/exception/error/match.spec.ts +81 -0
- package/tests/unit/http/api-adapter/node-http.spec.ts +0 -4
- package/tests/unit/identifier/uuid.spec.ts +0 -1
- package/tests/unit/request/request/base.spec.ts +0 -3
- package/tests/unit/request/request/general.spec.ts +0 -1
- package/tests/unit/result/controller.spec.ts +82 -0
- package/tests/unit/result/either.spec.ts +377 -0
- package/tests/unit/result/generator.spec.ts +273 -0
- package/tests/unit/route/router/route.spec.ts +0 -1
- package/tests/unit/route/uri/pathname.spec.ts +0 -1
- package/tests/unit/socket/server.spec.ts +0 -2
- package/vite.config.ts +2 -1
- package/dist/index.js +0 -720
- package/dist/index.js.map +0 -1005
|
@@ -2,9 +2,10 @@
|
|
|
2
2
|
import { expect, test, vi } from "vitest"
|
|
3
3
|
|
|
4
4
|
import {
|
|
5
|
-
|
|
5
|
+
promiseIsFailResult,
|
|
6
6
|
promiseCatch,
|
|
7
|
-
|
|
7
|
+
promiseCreateFailResult,
|
|
8
|
+
promiseDeferred,
|
|
8
9
|
promiseFilterFailResults,
|
|
9
10
|
promiseFilterSuccessResults,
|
|
10
11
|
promiseFinally,
|
|
@@ -17,16 +18,16 @@ import {
|
|
|
17
18
|
} from "#Source/basic/index.ts"
|
|
18
19
|
|
|
19
20
|
test("promiseThen chains and transforms resolved values", async () => {
|
|
20
|
-
const example1 = await promiseThen((value: number) => value * 2
|
|
21
|
-
const example2 = await promiseThen((value: string) => `${value}
|
|
21
|
+
const example1 = await promiseThen(Promise.resolve(3), (value: number) => value * 2)
|
|
22
|
+
const example2 = await promiseThen(Promise.resolve("ok"), (value: string) => `${value}!`)
|
|
22
23
|
|
|
23
24
|
expect(example1).toBe(6)
|
|
24
25
|
expect(example2).toBe("ok!")
|
|
25
26
|
})
|
|
26
27
|
|
|
27
28
|
test("promiseCatch handles rejected and resolved promises", async () => {
|
|
28
|
-
const example1 = await promiseCatch(
|
|
29
|
-
const example2 = await promiseCatch(() => 0
|
|
29
|
+
const example1 = await promiseCatch(Promise.reject(new Error("x")), () => "fallback")
|
|
30
|
+
const example2 = await promiseCatch(Promise.resolve(3), () => 0)
|
|
30
31
|
|
|
31
32
|
expect(example1).toBe("fallback")
|
|
32
33
|
expect(example2).toBe(3)
|
|
@@ -35,32 +36,49 @@ test("promiseCatch handles rejected and resolved promises", async () => {
|
|
|
35
36
|
test("promiseFinally runs finalizer and preserves resolution", async () => {
|
|
36
37
|
let cleaned = false
|
|
37
38
|
|
|
38
|
-
const example1 = await promiseFinally(() => {
|
|
39
|
+
const example1 = await promiseFinally(Promise.resolve(10), () => {
|
|
39
40
|
cleaned = true
|
|
40
|
-
}
|
|
41
|
+
})
|
|
41
42
|
|
|
42
43
|
expect(example1).toBe(10)
|
|
43
44
|
expect(cleaned).toBe(true)
|
|
44
45
|
})
|
|
45
46
|
|
|
46
|
-
test("
|
|
47
|
+
test("promiseDeferred exposes external resolve and reject for the created promise", async () => {
|
|
48
|
+
const resolvedDeferred = promiseDeferred<number>()
|
|
49
|
+
|
|
50
|
+
resolvedDeferred.resolve(42)
|
|
51
|
+
|
|
52
|
+
await expect(resolvedDeferred.promise).resolves.toBe(42)
|
|
53
|
+
expect(typeof resolvedDeferred.resolve).toBe("function")
|
|
54
|
+
expect(typeof resolvedDeferred.reject).toBe("function")
|
|
55
|
+
|
|
56
|
+
const rejectedDeferred = promiseDeferred<number>()
|
|
57
|
+
const reason = new Error("deferred failure")
|
|
58
|
+
|
|
59
|
+
rejectedDeferred.reject(reason)
|
|
60
|
+
|
|
61
|
+
await expect(rejectedDeferred.promise).rejects.toBe(reason)
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
test("promiseCreateFailResult creates standardized failure objects", () => {
|
|
47
65
|
const reason = new Error("x")
|
|
48
|
-
const failResult =
|
|
66
|
+
const failResult = promiseCreateFailResult(reason)
|
|
49
67
|
|
|
50
|
-
expect(
|
|
68
|
+
expect(promiseIsFailResult(failResult)).toBe(true)
|
|
51
69
|
expect(failResult.reason).toBe(reason)
|
|
52
70
|
})
|
|
53
71
|
|
|
54
|
-
test("
|
|
55
|
-
const failResult =
|
|
72
|
+
test("promiseIsFailResult identifies standardized failure objects", () => {
|
|
73
|
+
const failResult = promiseCreateFailResult(new Error("x"))
|
|
56
74
|
|
|
57
|
-
expect(
|
|
58
|
-
expect(
|
|
59
|
-
expect(
|
|
75
|
+
expect(promiseIsFailResult(failResult)).toBe(true)
|
|
76
|
+
expect(promiseIsFailResult({ reason: "x" })).toBe(false)
|
|
77
|
+
expect(promiseIsFailResult(null)).toBe(false)
|
|
60
78
|
})
|
|
61
79
|
|
|
62
80
|
test("promiseFilterSuccessResults keeps only successful values", () => {
|
|
63
|
-
const failResult =
|
|
81
|
+
const failResult = promiseCreateFailResult(new Error("x"))
|
|
64
82
|
|
|
65
83
|
const filtered = promiseFilterSuccessResults([1, failResult, 2])
|
|
66
84
|
|
|
@@ -83,93 +101,119 @@ test("promiseFilterFailResults keeps failure values with original indices", asyn
|
|
|
83
101
|
expect(filtered).toHaveLength(2)
|
|
84
102
|
expect(filtered[0]?.index).toBe(1)
|
|
85
103
|
expect(filtered[1]?.index).toBe(2)
|
|
86
|
-
expect(
|
|
87
|
-
expect(
|
|
104
|
+
expect(promiseIsFailResult(filtered[0])).toBe(true)
|
|
105
|
+
expect(promiseIsFailResult(filtered[1])).toBe(true)
|
|
88
106
|
})
|
|
89
107
|
|
|
90
108
|
test("promiseQueue executes makers in sequence and keeps failed results", async () => {
|
|
91
109
|
const results = await promiseQueue<number>([
|
|
92
110
|
async (): Promise<number> => 1,
|
|
93
111
|
async ({ previousResult, index }): Promise<number> => {
|
|
94
|
-
return
|
|
112
|
+
return promiseIsFailResult(previousResult) ? -1 : previousResult + index + 1
|
|
95
113
|
},
|
|
96
114
|
async ({ previousResult }): Promise<number> => {
|
|
97
|
-
if (
|
|
115
|
+
if (promiseIsFailResult(previousResult)) {
|
|
98
116
|
return -1
|
|
99
117
|
}
|
|
100
118
|
throw new Error(String(previousResult))
|
|
101
119
|
},
|
|
102
120
|
async ({ previousResult }): Promise<number> => {
|
|
103
|
-
return
|
|
121
|
+
return promiseIsFailResult(previousResult) ? -1 : previousResult + 1
|
|
104
122
|
},
|
|
105
123
|
])
|
|
106
124
|
|
|
107
125
|
expect(results[0]).toBe(1)
|
|
108
126
|
expect(results[1]).toBe(3)
|
|
109
|
-
expect(
|
|
127
|
+
expect(promiseIsFailResult(results[2])).toBe(true)
|
|
110
128
|
expect(results[3]).toBe(-1)
|
|
111
129
|
})
|
|
112
130
|
|
|
113
131
|
test("promiseRetryWhile retries while predicate is true", async () => {
|
|
114
|
-
let
|
|
132
|
+
let successAttempts = 0
|
|
115
133
|
|
|
116
134
|
const success = await promiseRetryWhile(
|
|
117
135
|
(value) => value < 3,
|
|
118
136
|
async () => {
|
|
119
|
-
|
|
120
|
-
return
|
|
137
|
+
successAttempts = successAttempts + 1
|
|
138
|
+
return successAttempts
|
|
121
139
|
},
|
|
122
|
-
{
|
|
140
|
+
{ maxTryIndex: 5 },
|
|
123
141
|
)
|
|
124
142
|
|
|
125
|
-
|
|
143
|
+
let failedAttempts = 0
|
|
126
144
|
const failed = await promiseRetryWhile(
|
|
127
145
|
() => true,
|
|
128
146
|
async () => {
|
|
129
|
-
|
|
147
|
+
failedAttempts = failedAttempts + 1
|
|
130
148
|
throw new Error("x")
|
|
131
149
|
},
|
|
132
|
-
{
|
|
150
|
+
{ maxTryIndex: 2 },
|
|
133
151
|
)
|
|
134
152
|
|
|
135
|
-
|
|
136
|
-
|
|
153
|
+
let singleAttemptCount = 0
|
|
154
|
+
const singleAttempt = await promiseRetryWhile(
|
|
155
|
+
() => true,
|
|
156
|
+
async () => {
|
|
157
|
+
singleAttemptCount = singleAttemptCount + 1
|
|
158
|
+
return singleAttemptCount
|
|
159
|
+
},
|
|
160
|
+
{ maxTryIndex: 0 },
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
await expect(promiseRetryWhile(async () => false, async () => 1, { maxTryIndex: -1 })).rejects.toThrow(
|
|
164
|
+
"`maxTryIndex` must be greater than or equal to 0.",
|
|
137
165
|
)
|
|
138
166
|
|
|
139
167
|
expect(success).toBe(3)
|
|
140
|
-
expect(
|
|
141
|
-
expect(
|
|
168
|
+
expect(successAttempts).toBe(3)
|
|
169
|
+
expect(promiseIsFailResult(failed)).toBe(true)
|
|
170
|
+
expect(failedAttempts).toBe(3)
|
|
171
|
+
expect(singleAttempt).toBe(1)
|
|
172
|
+
expect(singleAttemptCount).toBe(1)
|
|
142
173
|
})
|
|
143
174
|
|
|
144
175
|
test("promiseRetryUntil retries until predicate becomes true", async () => {
|
|
145
|
-
let
|
|
176
|
+
let successAttempts = 0
|
|
146
177
|
|
|
147
178
|
const success = await promiseRetryUntil(
|
|
148
|
-
(value,
|
|
179
|
+
(value, index) => value >= 2 && index >= 1,
|
|
149
180
|
async () => {
|
|
150
|
-
|
|
151
|
-
return
|
|
181
|
+
successAttempts = successAttempts + 1
|
|
182
|
+
return successAttempts
|
|
152
183
|
},
|
|
153
|
-
{
|
|
184
|
+
{ maxTryIndex: 5 },
|
|
154
185
|
)
|
|
155
186
|
|
|
156
|
-
|
|
187
|
+
let failedAttempts = 0
|
|
157
188
|
const failed = await promiseRetryUntil(
|
|
158
189
|
() => false,
|
|
159
190
|
async () => {
|
|
160
|
-
|
|
191
|
+
failedAttempts = failedAttempts + 1
|
|
161
192
|
throw new Error("x")
|
|
162
193
|
},
|
|
163
|
-
{
|
|
194
|
+
{ maxTryIndex: 2 },
|
|
164
195
|
)
|
|
165
196
|
|
|
166
|
-
|
|
167
|
-
|
|
197
|
+
let singleAttemptCount = 0
|
|
198
|
+
const singleAttempt = await promiseRetryUntil(
|
|
199
|
+
() => false,
|
|
200
|
+
async () => {
|
|
201
|
+
singleAttemptCount = singleAttemptCount + 1
|
|
202
|
+
return singleAttemptCount
|
|
203
|
+
},
|
|
204
|
+
{ maxTryIndex: 0 },
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
await expect(promiseRetryUntil(async () => true, async () => 1, { maxTryIndex: -1 })).rejects.toThrow(
|
|
208
|
+
"`maxTryIndex` must be greater than or equal to 0.",
|
|
168
209
|
)
|
|
169
210
|
|
|
170
211
|
expect(success).toBe(2)
|
|
171
|
-
expect(
|
|
172
|
-
expect(
|
|
212
|
+
expect(successAttempts).toBe(2)
|
|
213
|
+
expect(promiseIsFailResult(failed)).toBe(true)
|
|
214
|
+
expect(failedAttempts).toBe(3)
|
|
215
|
+
expect(singleAttempt).toBe(1)
|
|
216
|
+
expect(singleAttemptCount).toBe(1)
|
|
173
217
|
})
|
|
174
218
|
|
|
175
219
|
test("promiseInterval runs by interval and returns a stopper", async () => {
|
|
@@ -188,7 +232,7 @@ test("promiseInterval runs by interval and returns a stopper", async () => {
|
|
|
188
232
|
stop()
|
|
189
233
|
vi.advanceTimersByTime(50)
|
|
190
234
|
|
|
191
|
-
expect(calls).toEqual([1, 2
|
|
235
|
+
expect(calls).toEqual([0, 1, 2])
|
|
192
236
|
expect(typeof stop).toBe("function")
|
|
193
237
|
}
|
|
194
238
|
finally {
|
|
@@ -196,6 +240,31 @@ test("promiseInterval runs by interval and returns a stopper", async () => {
|
|
|
196
240
|
}
|
|
197
241
|
})
|
|
198
242
|
|
|
243
|
+
test("promiseInterval logs rejected runs instead of leaving them unhandled", async () => {
|
|
244
|
+
vi.useFakeTimers()
|
|
245
|
+
|
|
246
|
+
const errorSpy = vi.spyOn(console, "error").mockImplementation(() => undefined)
|
|
247
|
+
|
|
248
|
+
try {
|
|
249
|
+
promiseInterval(10, async () => {
|
|
250
|
+
throw new Error("interval failure")
|
|
251
|
+
})
|
|
252
|
+
|
|
253
|
+
vi.advanceTimersByTime(10)
|
|
254
|
+
await Promise.resolve()
|
|
255
|
+
await Promise.resolve()
|
|
256
|
+
|
|
257
|
+
expect(errorSpy).toHaveBeenCalledTimes(1)
|
|
258
|
+
expect(errorSpy.mock.calls[0]?.[0]).toBe("[promiseInterval] unexpected error occurred:")
|
|
259
|
+
expect(errorSpy.mock.calls[0]?.[1]).toBeInstanceOf(Error)
|
|
260
|
+
}
|
|
261
|
+
finally {
|
|
262
|
+
vi.clearAllTimers()
|
|
263
|
+
errorSpy.mockRestore()
|
|
264
|
+
vi.useRealTimers()
|
|
265
|
+
}
|
|
266
|
+
})
|
|
267
|
+
|
|
199
268
|
test("promiseForever loops continuously and reports rejections", async () => {
|
|
200
269
|
vi.useFakeTimers()
|
|
201
270
|
|
|
@@ -210,20 +279,59 @@ test("promiseForever loops continuously and reports rejections", async () => {
|
|
|
210
279
|
return value
|
|
211
280
|
})
|
|
212
281
|
|
|
213
|
-
promiseForever(promiseMaker, { breakTime: 5, onRejected })
|
|
282
|
+
const result = promiseForever(promiseMaker, { breakTime: 5, onRejected })
|
|
214
283
|
await Promise.resolve()
|
|
215
284
|
await Promise.resolve()
|
|
216
285
|
|
|
217
286
|
expect(promiseMaker).toHaveBeenCalledTimes(1)
|
|
218
287
|
expect(onRejected).toHaveBeenCalledTimes(1)
|
|
219
|
-
expect(onRejected.mock.calls[0]?.[0]).toBe(
|
|
220
|
-
expect(
|
|
288
|
+
expect(onRejected.mock.calls[0]?.[0]).toBe(0)
|
|
289
|
+
expect(promiseIsFailResult(onRejected.mock.calls[0]?.[1])).toBe(true)
|
|
290
|
+
expect(result.index).toBe(0)
|
|
291
|
+
expect(result.isStopped).toBe(false)
|
|
221
292
|
|
|
222
293
|
vi.advanceTimersByTime(16)
|
|
223
294
|
await Promise.resolve()
|
|
224
295
|
await Promise.resolve()
|
|
225
296
|
|
|
226
297
|
expect(promiseMaker.mock.calls.length).toBeGreaterThanOrEqual(2)
|
|
298
|
+
|
|
299
|
+
result.stop()
|
|
300
|
+
expect(result.isStopped).toBe(true)
|
|
301
|
+
}
|
|
302
|
+
finally {
|
|
303
|
+
vi.clearAllTimers()
|
|
304
|
+
vi.useRealTimers()
|
|
305
|
+
}
|
|
306
|
+
})
|
|
307
|
+
|
|
308
|
+
test("promiseForever stop prevents scheduling the next run when current run has not settled", async () => {
|
|
309
|
+
vi.useFakeTimers()
|
|
310
|
+
|
|
311
|
+
try {
|
|
312
|
+
const resolvers: Array<(value: number) => void> = []
|
|
313
|
+
const promiseMaker = vi.fn(async () => {
|
|
314
|
+
return await new Promise<number>((resolve) => {
|
|
315
|
+
resolvers.push(resolve)
|
|
316
|
+
})
|
|
317
|
+
})
|
|
318
|
+
|
|
319
|
+
const result = promiseForever(promiseMaker, { breakTime: 5 })
|
|
320
|
+
|
|
321
|
+
expect(promiseMaker).toHaveBeenCalledTimes(1)
|
|
322
|
+
|
|
323
|
+
result.stop()
|
|
324
|
+
expect(result.isStopped).toBe(true)
|
|
325
|
+
|
|
326
|
+
resolvers[0]?.(1)
|
|
327
|
+
await Promise.resolve()
|
|
328
|
+
await Promise.resolve()
|
|
329
|
+
|
|
330
|
+
vi.advanceTimersByTime(20)
|
|
331
|
+
await Promise.resolve()
|
|
332
|
+
await Promise.resolve()
|
|
333
|
+
|
|
334
|
+
expect(promiseMaker).toHaveBeenCalledTimes(1)
|
|
227
335
|
}
|
|
228
336
|
finally {
|
|
229
337
|
vi.clearAllTimers()
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { expect, test, vi } from "vitest"
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
createTaggedError,
|
|
5
|
+
isTaggedError,
|
|
6
|
+
} from "#Source/exception/index.ts"
|
|
7
|
+
|
|
8
|
+
test("createTaggedError creates tagged errors with inferred message, cause and serialization", () => {
|
|
9
|
+
const dateNowSpy = vi.spyOn(Date, "now").mockReturnValue(123)
|
|
10
|
+
|
|
11
|
+
try {
|
|
12
|
+
const ExampleError = createTaggedError("Example")<{
|
|
13
|
+
message: string
|
|
14
|
+
cause: Error
|
|
15
|
+
extra: number
|
|
16
|
+
}>()
|
|
17
|
+
const MessageOnlyError = createTaggedError("MessageOnly")<string>()
|
|
18
|
+
const cause = new Error("root")
|
|
19
|
+
|
|
20
|
+
cause.stack = "Error: root\n at cause"
|
|
21
|
+
|
|
22
|
+
const example1 = new ExampleError({
|
|
23
|
+
message: "boom",
|
|
24
|
+
cause,
|
|
25
|
+
extra: 1,
|
|
26
|
+
})
|
|
27
|
+
const example2 = new MessageOnlyError("plain-message")
|
|
28
|
+
const example3 = ExampleError.is(example1)
|
|
29
|
+
const example4 = ExampleError.is(
|
|
30
|
+
new (createTaggedError("Example")<{ message: string }>())({
|
|
31
|
+
message: "compatible",
|
|
32
|
+
}),
|
|
33
|
+
)
|
|
34
|
+
const example5 = example1.toJSON()
|
|
35
|
+
|
|
36
|
+
expect(example1).toBeInstanceOf(Error)
|
|
37
|
+
expect(example1.name).toBe("Example")
|
|
38
|
+
expect(example1.tag).toBe("Example")
|
|
39
|
+
expect(example1.data.extra).toBe(1)
|
|
40
|
+
expect(example1.message).toBe("boom")
|
|
41
|
+
expect(example1.cause).toBe(cause)
|
|
42
|
+
expect(example1.stack).toContain("Caused by: Error: root")
|
|
43
|
+
expect(example2.message).toBe("plain-message")
|
|
44
|
+
expect(example3).toBe(true)
|
|
45
|
+
expect(example4).toBe(true)
|
|
46
|
+
expect(example5).toMatchObject({
|
|
47
|
+
version: "1",
|
|
48
|
+
tag: "Example",
|
|
49
|
+
data: {
|
|
50
|
+
message: "boom",
|
|
51
|
+
cause,
|
|
52
|
+
extra: 1,
|
|
53
|
+
},
|
|
54
|
+
name: "Example",
|
|
55
|
+
message: "boom",
|
|
56
|
+
cause: {
|
|
57
|
+
name: "Error",
|
|
58
|
+
message: "root",
|
|
59
|
+
stack: "Error: root\n at cause",
|
|
60
|
+
},
|
|
61
|
+
timestamp: 123,
|
|
62
|
+
})
|
|
63
|
+
} finally {
|
|
64
|
+
dateNowSpy.mockRestore()
|
|
65
|
+
}
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
test("isTaggedError distinguishes tagged errors from plain values", () => {
|
|
69
|
+
const ExampleError = createTaggedError("Example")<{ message: string }>()
|
|
70
|
+
|
|
71
|
+
const example1 = new ExampleError({ message: "boom" })
|
|
72
|
+
const example2 = new Error("boom")
|
|
73
|
+
const example3 = {
|
|
74
|
+
tag: "Example",
|
|
75
|
+
data: { message: "boom" },
|
|
76
|
+
toJSON: () => ({ message: "boom" }),
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
expect(isTaggedError(example1)).toBe(true)
|
|
80
|
+
expect(isTaggedError(example2)).toBe(false)
|
|
81
|
+
expect(isTaggedError(example3)).toBe(false)
|
|
82
|
+
expect(isTaggedError(null)).toBe(false)
|
|
83
|
+
})
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { expect, test } from "vitest"
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
createTaggedError,
|
|
5
|
+
matchTaggedError,
|
|
6
|
+
matchTaggedErrorPartial,
|
|
7
|
+
} from "#Source/exception/index.ts"
|
|
8
|
+
|
|
9
|
+
class NetworkError extends createTaggedError("Network")<{
|
|
10
|
+
message: string
|
|
11
|
+
status: number
|
|
12
|
+
}>() {
|
|
13
|
+
//
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
class ValidationError extends createTaggedError("Validation")<{
|
|
17
|
+
message: string
|
|
18
|
+
field: string
|
|
19
|
+
}>() {
|
|
20
|
+
//
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
type ExampleTaggedError =
|
|
24
|
+
| NetworkError
|
|
25
|
+
| ValidationError
|
|
26
|
+
|
|
27
|
+
test("matchTaggedError dispatches by tag and throws when a handler is missing", () => {
|
|
28
|
+
const example1 = new NetworkError({
|
|
29
|
+
message: "bad gateway",
|
|
30
|
+
status: 502,
|
|
31
|
+
}) as ExampleTaggedError
|
|
32
|
+
const example2 = new ValidationError({
|
|
33
|
+
message: "invalid email",
|
|
34
|
+
field: "email",
|
|
35
|
+
}) as ExampleTaggedError
|
|
36
|
+
const example3 = matchTaggedError(example1, {
|
|
37
|
+
Network: (error) => `${error.tag}:${error.data.status}`,
|
|
38
|
+
Validation: (error) => `${error.tag}:${error.data.field}`,
|
|
39
|
+
})
|
|
40
|
+
const example4 = matchTaggedError(example2, {
|
|
41
|
+
Network: (error) => `${error.tag}:${error.data.status}`,
|
|
42
|
+
Validation: (error) => `${error.tag}:${error.data.field}`,
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
expect(example3).toBe("Network:502")
|
|
46
|
+
expect(example4).toBe("Validation:email")
|
|
47
|
+
expect(() => {
|
|
48
|
+
// @ts-expect-error Testing runtime error when handler is missing
|
|
49
|
+
return matchTaggedError(example2, {
|
|
50
|
+
Network: (error) => error.data.status,
|
|
51
|
+
})
|
|
52
|
+
}).toThrow("No handler for tag: Validation")
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
test("matchTaggedErrorPartial uses a fallback for unhandled tags", () => {
|
|
56
|
+
const example1 = new NetworkError({
|
|
57
|
+
message: "bad gateway",
|
|
58
|
+
status: 502,
|
|
59
|
+
}) as ExampleTaggedError
|
|
60
|
+
const example2 = new ValidationError({
|
|
61
|
+
message: "invalid email",
|
|
62
|
+
field: "email",
|
|
63
|
+
}) as ExampleTaggedError
|
|
64
|
+
const example3 = matchTaggedErrorPartial(
|
|
65
|
+
example1,
|
|
66
|
+
{
|
|
67
|
+
Network: (error) => `${error.tag}:${error.data.status}`,
|
|
68
|
+
},
|
|
69
|
+
(error) => `${error.tag}:${error.message}`,
|
|
70
|
+
)
|
|
71
|
+
const example4 = matchTaggedErrorPartial(
|
|
72
|
+
example2,
|
|
73
|
+
{
|
|
74
|
+
Network: (error) => `${error.tag}:${error.data.status}`,
|
|
75
|
+
},
|
|
76
|
+
(error) => `${error.tag}:${error.data.field}`,
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
expect(example3).toBe("Network:502")
|
|
80
|
+
expect(example4).toBe("Validation:email")
|
|
81
|
+
})
|
|
@@ -26,7 +26,6 @@ test("ApiHostNodeHttp exposes usable URLs and forwards request data to ApiCoreNo
|
|
|
26
26
|
})
|
|
27
27
|
|
|
28
28
|
const startResult = await apiHost.start()
|
|
29
|
-
// oxlint-disable-next-line typescript/no-unsafe-type-assertion
|
|
30
29
|
const address = apiHost.safeGetServer().address() as AddressInfo
|
|
31
30
|
|
|
32
31
|
expect(startResult.urlList).toContain(`http://127.0.0.1:${address.port}`)
|
|
@@ -61,7 +60,6 @@ test("ApiCoreNodeHttp returns a stable error response when the handler throws",
|
|
|
61
60
|
})
|
|
62
61
|
|
|
63
62
|
await apiHost.start()
|
|
64
|
-
// oxlint-disable-next-line typescript/no-unsafe-type-assertion
|
|
65
63
|
const address = apiHost.safeGetServer().address() as AddressInfo
|
|
66
64
|
|
|
67
65
|
const response = await fetch(`http://127.0.0.1:${address.port}/error`, {
|
|
@@ -89,7 +87,6 @@ test("ApiCoreNodeHttp treats an empty request body as an empty object", async ()
|
|
|
89
87
|
})
|
|
90
88
|
|
|
91
89
|
await apiHost.start()
|
|
92
|
-
// oxlint-disable-next-line typescript/no-unsafe-type-assertion
|
|
93
90
|
const address = apiHost.safeGetServer().address() as AddressInfo
|
|
94
91
|
|
|
95
92
|
const response = await fetch(`http://127.0.0.1:${address.port}/empty`, {
|
|
@@ -144,7 +141,6 @@ test("ApiCoreNodeHttp exposes normalized headers and parses multipart form data"
|
|
|
144
141
|
})
|
|
145
142
|
|
|
146
143
|
await apiHost.start()
|
|
147
|
-
// oxlint-disable-next-line typescript/no-unsafe-type-assertion
|
|
148
144
|
const address = apiHost.safeGetServer().address() as AddressInfo
|
|
149
145
|
|
|
150
146
|
const formData = new FormData()
|
|
@@ -184,7 +184,6 @@ test("BaseRequest.request emits lifecycle callbacks and events for success and e
|
|
|
184
184
|
}
|
|
185
185
|
}
|
|
186
186
|
const request = new TestRequest({
|
|
187
|
-
// oxlint-disable-next-line typescript/no-unsafe-type-assertion
|
|
188
187
|
fetchFactory: getFetch as unknown as FetchFactory,
|
|
189
188
|
modifyRequestOptions,
|
|
190
189
|
generatePatchOutput: ({ errorKind, error }) => {
|
|
@@ -315,7 +314,6 @@ test("BaseRequest.request converts network failures into patch outputs", async (
|
|
|
315
314
|
return new TestRejectingFetch(new TypeError("fetch failed"))
|
|
316
315
|
}
|
|
317
316
|
const request = new TestRequest({
|
|
318
|
-
// oxlint-disable-next-line typescript/no-unsafe-type-assertion
|
|
319
317
|
fetchFactory: getFetch as unknown as FetchFactory,
|
|
320
318
|
generatePatchOutput: ({ errorKind, error }) => {
|
|
321
319
|
return {
|
|
@@ -357,7 +355,6 @@ test("BaseRequest.request stringifies non-Error failures into unknown patch outp
|
|
|
357
355
|
return new TestRejectingFetch("boom")
|
|
358
356
|
}
|
|
359
357
|
const request = new TestRequest({
|
|
360
|
-
// oxlint-disable-next-line typescript/no-unsafe-type-assertion
|
|
361
358
|
fetchFactory: getFetch as unknown as FetchFactory,
|
|
362
359
|
generatePatchOutput: ({ errorKind, error }) => {
|
|
363
360
|
return {
|
|
@@ -123,7 +123,6 @@ test("GeneralRequest.request keeps success outputs and generates unknown patch o
|
|
|
123
123
|
return new TestGeneralRejectingFetch()
|
|
124
124
|
}
|
|
125
125
|
const request = new GeneralRequest<ExampleGeneralResource>({
|
|
126
|
-
// oxlint-disable-next-line typescript/no-unsafe-type-assertion
|
|
127
126
|
fetchFactory: fetchFactory as unknown as FetchFactory,
|
|
128
127
|
})
|
|
129
128
|
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { expect, test } from "vitest"
|
|
2
|
+
|
|
3
|
+
import type { Either } from "#Source/result/index.ts"
|
|
4
|
+
import { Controller, controllerFromEitherType, Left, Right } from "#Source/result/index.ts"
|
|
5
|
+
|
|
6
|
+
test("Controller.isLeft currently overflows through recursive static dispatch", () => {
|
|
7
|
+
expect(() => Controller.isLeft(new Left<string, number>("stop"))).toThrow(RangeError)
|
|
8
|
+
})
|
|
9
|
+
|
|
10
|
+
test("Controller.isRight currently overflows through recursive static dispatch", () => {
|
|
11
|
+
expect(() => Controller.isRight(new Right<string, number>(2))).toThrow(RangeError)
|
|
12
|
+
})
|
|
13
|
+
|
|
14
|
+
test("Controller.value exposes the same pending promise until the controller resolves", async () => {
|
|
15
|
+
const controller = new Controller<string, number>()
|
|
16
|
+
const firstValue = controller.value
|
|
17
|
+
const secondValue = controller.value
|
|
18
|
+
|
|
19
|
+
expect(secondValue).toBe(firstValue)
|
|
20
|
+
|
|
21
|
+
const returned = await controller.returnRight(2)
|
|
22
|
+
const settled = await firstValue
|
|
23
|
+
|
|
24
|
+
expect(settled).toBe(returned)
|
|
25
|
+
expect(settled.isRight()).toBe(true)
|
|
26
|
+
expect(settled.getRight()).toBe(2)
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
test("Controller.returnRight resolves the controller with a Right value", async () => {
|
|
30
|
+
const controller = new Controller<string, number>()
|
|
31
|
+
|
|
32
|
+
const returned = await controller.returnRight(2)
|
|
33
|
+
const settled = await controller.value
|
|
34
|
+
|
|
35
|
+
expect(returned).toBeInstanceOf(Right)
|
|
36
|
+
expect(returned).toBe(settled)
|
|
37
|
+
expect(returned.isRight()).toBe(true)
|
|
38
|
+
expect(returned.getRight()).toBe(2)
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
test("Controller.returnLeft resolves the controller with a Left value", async () => {
|
|
42
|
+
const controller = new Controller<string, number>()
|
|
43
|
+
|
|
44
|
+
const returned = await controller.returnLeft("stop")
|
|
45
|
+
const settled = await controller.value
|
|
46
|
+
|
|
47
|
+
expect(returned).toBeInstanceOf(Left)
|
|
48
|
+
expect(returned).toBe(settled)
|
|
49
|
+
expect(returned.isLeft()).toBe(true)
|
|
50
|
+
expect(returned.getLeft()).toBe("stop")
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
test("Controller.throw resolves the controller with a Left value and rejects with the same Left", async () => {
|
|
54
|
+
const controller = new Controller<string, number>()
|
|
55
|
+
let caught: unknown
|
|
56
|
+
|
|
57
|
+
try {
|
|
58
|
+
await controller.throw("stop")
|
|
59
|
+
} catch (error) {
|
|
60
|
+
caught = error
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const settled = await controller.value
|
|
64
|
+
|
|
65
|
+
expect(caught).toBeInstanceOf(Left)
|
|
66
|
+
expect(caught).toBe(settled)
|
|
67
|
+
expect(settled.isLeft()).toBe(true)
|
|
68
|
+
expect(settled.getLeft()).toBe("stop")
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
test("controllerFromEitherType creates a controller that matches the Either shape", async () => {
|
|
72
|
+
const controller = controllerFromEitherType<Either<string, number>>()
|
|
73
|
+
const typedController: Controller<string, number> = controller
|
|
74
|
+
|
|
75
|
+
const returned = await typedController.returnRight(3)
|
|
76
|
+
const settled = await controller.value
|
|
77
|
+
|
|
78
|
+
expect(controller).toBeInstanceOf(Controller)
|
|
79
|
+
expect(returned).toBe(settled)
|
|
80
|
+
expect(returned.isRight()).toBe(true)
|
|
81
|
+
expect(returned.getRight()).toBe(3)
|
|
82
|
+
})
|