@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.
Files changed (63) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/oxlint.config.ts +1 -2
  3. package/package.json +5 -5
  4. package/scripts/build.ts +2 -52
  5. package/src/basic/promise.ts +141 -71
  6. package/src/drizzle/pagination.ts +0 -2
  7. package/src/event/class-event-proxy.ts +0 -2
  8. package/src/event/instance-event-proxy.ts +0 -2
  9. package/src/exception/README.md +28 -19
  10. package/src/exception/error/error.ts +123 -0
  11. package/src/exception/error/index.ts +2 -0
  12. package/src/exception/error/match.ts +38 -0
  13. package/src/exception/error/must-fix.ts +17 -0
  14. package/src/exception/index.ts +2 -0
  15. package/src/file-system/find.ts +53 -0
  16. package/src/file-system/index.ts +2 -0
  17. package/src/file-system/path.ts +76 -0
  18. package/src/file-system/resolve.ts +22 -0
  19. package/src/form/inputor-controller/base.ts +0 -13
  20. package/src/form/inputor-controller/form.ts +0 -2
  21. package/src/http/api/api-type.ts +0 -3
  22. package/src/http/api-adapter/api-result-arktype.ts +0 -3
  23. package/src/index.ts +2 -0
  24. package/src/openai/openai.ts +0 -1
  25. package/src/request/fetch/browser.ts +0 -5
  26. package/src/request/fetch/nodejs.ts +0 -5
  27. package/src/request/request/base.ts +0 -4
  28. package/src/request/request/general.ts +0 -1
  29. package/src/result/controller.ts +11 -7
  30. package/src/result/either.ts +230 -60
  31. package/src/result/generator.ts +168 -0
  32. package/src/result/index.ts +1 -0
  33. package/src/route/router/router.ts +0 -1
  34. package/src/route/uri/hash.ts +0 -1
  35. package/src/route/uri/search.ts +0 -1
  36. package/src/service/README.md +1 -0
  37. package/src/service/index.ts +1 -0
  38. package/src/service/service.ts +110 -0
  39. package/src/socket/client/socket-unit.ts +0 -2
  40. package/src/socket/server/socket-unit.ts +0 -1
  41. package/src/tube/helper.ts +0 -1
  42. package/src/weixin/official-account/authorization.ts +0 -2
  43. package/src/weixin/official-account/js-api.ts +0 -2
  44. package/src/weixin/open/oauth2.ts +0 -2
  45. package/tests/unit/aio/json.spec.ts +0 -1
  46. package/tests/unit/basic/promise.spec.ts +158 -50
  47. package/tests/unit/credential/api-key.spec.ts +0 -1
  48. package/tests/unit/credential/password.spec.ts +0 -1
  49. package/tests/unit/exception/error/error.spec.ts +83 -0
  50. package/tests/unit/exception/error/match.spec.ts +81 -0
  51. package/tests/unit/http/api-adapter/node-http.spec.ts +0 -4
  52. package/tests/unit/identifier/uuid.spec.ts +0 -1
  53. package/tests/unit/request/request/base.spec.ts +0 -3
  54. package/tests/unit/request/request/general.spec.ts +0 -1
  55. package/tests/unit/result/controller.spec.ts +82 -0
  56. package/tests/unit/result/either.spec.ts +377 -0
  57. package/tests/unit/result/generator.spec.ts +273 -0
  58. package/tests/unit/route/router/route.spec.ts +0 -1
  59. package/tests/unit/route/uri/pathname.spec.ts +0 -1
  60. package/tests/unit/socket/server.spec.ts +0 -2
  61. package/vite.config.ts +2 -1
  62. package/dist/index.js +0 -720
  63. 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
- isPromiseFailResult,
5
+ promiseIsFailResult,
6
6
  promiseCatch,
7
- promiseConstructFailResult,
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, Promise.resolve(3))
21
- const example2 = await promiseThen((value: string) => `${value}!`, Promise.resolve("ok"))
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(() => "fallback", Promise.reject(new Error("x")))
29
- const example2 = await promiseCatch(() => 0, Promise.resolve(3))
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
- }, Promise.resolve(10))
41
+ })
41
42
 
42
43
  expect(example1).toBe(10)
43
44
  expect(cleaned).toBe(true)
44
45
  })
45
46
 
46
- test("promiseConstructFailResult creates standardized failure objects", () => {
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 = promiseConstructFailResult(reason)
66
+ const failResult = promiseCreateFailResult(reason)
49
67
 
50
- expect(isPromiseFailResult(failResult)).toBe(true)
68
+ expect(promiseIsFailResult(failResult)).toBe(true)
51
69
  expect(failResult.reason).toBe(reason)
52
70
  })
53
71
 
54
- test("isPromiseFailResult identifies standardized failure objects", () => {
55
- const failResult = promiseConstructFailResult(new Error("x"))
72
+ test("promiseIsFailResult identifies standardized failure objects", () => {
73
+ const failResult = promiseCreateFailResult(new Error("x"))
56
74
 
57
- expect(isPromiseFailResult(failResult)).toBe(true)
58
- expect(isPromiseFailResult({ reason: "x" })).toBe(false)
59
- expect(isPromiseFailResult(null)).toBe(false)
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 = promiseConstructFailResult(new Error("x"))
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(isPromiseFailResult(filtered[0])).toBe(true)
87
- expect(isPromiseFailResult(filtered[1])).toBe(true)
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 isPromiseFailResult(previousResult) ? -1 : previousResult + index + 1
112
+ return promiseIsFailResult(previousResult) ? -1 : previousResult + index + 1
95
113
  },
96
114
  async ({ previousResult }): Promise<number> => {
97
- if (isPromiseFailResult(previousResult)) {
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 isPromiseFailResult(previousResult) ? -1 : previousResult + 1
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(isPromiseFailResult(results[2])).toBe(true)
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 attempts = 0
132
+ let successAttempts = 0
115
133
 
116
134
  const success = await promiseRetryWhile(
117
135
  (value) => value < 3,
118
136
  async () => {
119
- attempts = attempts + 1
120
- return attempts
137
+ successAttempts = successAttempts + 1
138
+ return successAttempts
121
139
  },
122
- { maxTryTimes: 5 },
140
+ { maxTryIndex: 5 },
123
141
  )
124
142
 
125
- attempts = 0
143
+ let failedAttempts = 0
126
144
  const failed = await promiseRetryWhile(
127
145
  () => true,
128
146
  async () => {
129
- attempts = attempts + 1
147
+ failedAttempts = failedAttempts + 1
130
148
  throw new Error("x")
131
149
  },
132
- { maxTryTimes: 2 },
150
+ { maxTryIndex: 2 },
133
151
  )
134
152
 
135
- await expect(promiseRetryWhile(async () => false, async () => 1, { maxTryTimes: 0 })).rejects.toThrow(
136
- "`maxTryTimes` must be greater than 0.",
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(attempts).toBe(2)
141
- expect(isPromiseFailResult(failed)).toBe(true)
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 attempts = 0
176
+ let successAttempts = 0
146
177
 
147
178
  const success = await promiseRetryUntil(
148
- (value, time) => value >= 2 && time >= 2,
179
+ (value, index) => value >= 2 && index >= 1,
149
180
  async () => {
150
- attempts = attempts + 1
151
- return attempts
181
+ successAttempts = successAttempts + 1
182
+ return successAttempts
152
183
  },
153
- { maxTryTimes: 5 },
184
+ { maxTryIndex: 5 },
154
185
  )
155
186
 
156
- attempts = 0
187
+ let failedAttempts = 0
157
188
  const failed = await promiseRetryUntil(
158
189
  () => false,
159
190
  async () => {
160
- attempts = attempts + 1
191
+ failedAttempts = failedAttempts + 1
161
192
  throw new Error("x")
162
193
  },
163
- { maxTryTimes: 2 },
194
+ { maxTryIndex: 2 },
164
195
  )
165
196
 
166
- await expect(promiseRetryUntil(async () => true, async () => 1, { maxTryTimes: 0 })).rejects.toThrow(
167
- "`maxTryTimes` must be greater than 0.",
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(attempts).toBe(2)
172
- expect(isPromiseFailResult(failed)).toBe(true)
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, 3])
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(1)
220
- expect(isPromiseFailResult(onRejected.mock.calls[0]?.[1])).toBe(true)
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()
@@ -8,7 +8,6 @@ test("generateApiKey creates a stable sk-prefixed key shape", () => {
8
8
  return buffer
9
9
  }
10
10
 
11
- // oxlint-disable-next-line typescript/no-unsafe-type-assertion
12
11
  const target = buffer as Uint8Array
13
12
  target.forEach((_, index) => {
14
13
  target[index] = index
@@ -8,7 +8,6 @@ test("Password.generateSalt returns a hexadecimal salt string of the requested b
8
8
  return buffer
9
9
  }
10
10
 
11
- // oxlint-disable-next-line typescript/no-unsafe-type-assertion
12
11
  const target = buffer as Uint8Array
13
12
  target.fill(0xAB)
14
13
  return buffer
@@ -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()
@@ -66,7 +66,6 @@ test("generateUuidV7 returns UUIDv7 format and is monotonic within the same mill
66
66
  return buffer
67
67
  }
68
68
 
69
- // oxlint-disable-next-line typescript/no-unsafe-type-assertion
70
69
  const target = buffer as Uint8Array
71
70
  target.fill(0)
72
71
  return buffer
@@ -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
+ })