@planet-matrix/mobius-model 0.6.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 (258) hide show
  1. package/CHANGELOG.md +50 -0
  2. package/oxlint.config.ts +1 -2
  3. package/package.json +29 -17
  4. package/scripts/build.ts +2 -52
  5. package/src/ai/README.md +1 -0
  6. package/src/ai/ai.ts +107 -0
  7. package/src/ai/chat-completion-ai/aihubmix-chat-completion.ts +78 -0
  8. package/src/ai/chat-completion-ai/chat-completion-ai.ts +270 -0
  9. package/src/ai/chat-completion-ai/chat-completion.ts +189 -0
  10. package/src/ai/chat-completion-ai/index.ts +7 -0
  11. package/src/ai/chat-completion-ai/lingyiwanwu-chat-completion.ts +78 -0
  12. package/src/ai/chat-completion-ai/ohmygpt-chat-completion.ts +78 -0
  13. package/src/ai/chat-completion-ai/openai-next-chat-completion.ts +78 -0
  14. package/src/ai/embedding-ai/embedding-ai.ts +63 -0
  15. package/src/ai/embedding-ai/embedding.ts +50 -0
  16. package/src/ai/embedding-ai/index.ts +4 -0
  17. package/src/ai/embedding-ai/openai-next-embedding.ts +23 -0
  18. package/src/ai/index.ts +4 -0
  19. package/src/aio/README.md +100 -0
  20. package/src/aio/content.ts +141 -0
  21. package/src/aio/index.ts +3 -0
  22. package/src/aio/json.ts +127 -0
  23. package/src/aio/prompt.ts +246 -0
  24. package/src/basic/README.md +20 -15
  25. package/src/basic/error.ts +19 -5
  26. package/src/basic/function.ts +2 -2
  27. package/src/basic/index.ts +1 -0
  28. package/src/basic/promise.ts +141 -71
  29. package/src/basic/schedule.ts +111 -0
  30. package/src/basic/stream.ts +135 -25
  31. package/src/credential/README.md +107 -0
  32. package/src/credential/api-key.ts +158 -0
  33. package/src/credential/bearer.ts +73 -0
  34. package/src/credential/index.ts +4 -0
  35. package/src/credential/json-web-token.ts +96 -0
  36. package/src/credential/password.ts +170 -0
  37. package/src/cron/README.md +86 -0
  38. package/src/cron/cron.ts +87 -0
  39. package/src/cron/index.ts +1 -0
  40. package/src/drizzle/README.md +1 -0
  41. package/src/drizzle/drizzle.ts +1 -0
  42. package/src/drizzle/helper.ts +47 -0
  43. package/src/drizzle/index.ts +5 -0
  44. package/src/drizzle/infer.ts +52 -0
  45. package/src/drizzle/kysely.ts +8 -0
  46. package/src/drizzle/pagination.ts +198 -0
  47. package/src/email/README.md +1 -0
  48. package/src/email/index.ts +1 -0
  49. package/src/email/resend.ts +25 -0
  50. package/src/event/class-event-proxy.ts +5 -6
  51. package/src/event/common.ts +13 -3
  52. package/src/event/event-manager.ts +3 -3
  53. package/src/event/instance-event-proxy.ts +5 -6
  54. package/src/event/internal.ts +4 -4
  55. package/src/exception/README.md +28 -19
  56. package/src/exception/error/error.ts +123 -0
  57. package/src/exception/error/index.ts +2 -0
  58. package/src/exception/error/match.ts +38 -0
  59. package/src/exception/error/must-fix.ts +17 -0
  60. package/src/exception/index.ts +2 -0
  61. package/src/file-system/find.ts +53 -0
  62. package/src/file-system/index.ts +2 -0
  63. package/src/file-system/path.ts +76 -0
  64. package/src/file-system/resolve.ts +22 -0
  65. package/src/form/README.md +25 -0
  66. package/src/form/index.ts +1 -0
  67. package/src/form/inputor-controller/base.ts +861 -0
  68. package/src/form/inputor-controller/boolean.ts +39 -0
  69. package/src/form/inputor-controller/file.ts +39 -0
  70. package/src/form/inputor-controller/form.ts +179 -0
  71. package/src/form/inputor-controller/helper.ts +117 -0
  72. package/src/form/inputor-controller/index.ts +17 -0
  73. package/src/form/inputor-controller/multi-select.ts +99 -0
  74. package/src/form/inputor-controller/number.ts +116 -0
  75. package/src/form/inputor-controller/select.ts +109 -0
  76. package/src/form/inputor-controller/text.ts +82 -0
  77. package/src/http/READMD.md +1 -0
  78. package/src/http/api/api-core.ts +84 -0
  79. package/src/http/api/api-handler.ts +79 -0
  80. package/src/http/api/api-host.ts +47 -0
  81. package/src/http/api/api-result.ts +56 -0
  82. package/src/http/api/api-schema.ts +154 -0
  83. package/src/http/api/api-server.ts +130 -0
  84. package/src/http/api/api-test.ts +142 -0
  85. package/src/http/api/api-type.ts +34 -0
  86. package/src/http/api/api.ts +81 -0
  87. package/src/http/api/index.ts +11 -0
  88. package/src/http/api-adapter/api-core-node-http.ts +260 -0
  89. package/src/http/api-adapter/api-host-node-http.ts +156 -0
  90. package/src/http/api-adapter/api-result-arktype.ts +294 -0
  91. package/src/http/api-adapter/api-result-zod.ts +286 -0
  92. package/src/http/api-adapter/index.ts +5 -0
  93. package/src/http/bin/gen-api-list/gen-api-list.ts +126 -0
  94. package/src/http/bin/gen-api-list/index.ts +1 -0
  95. package/src/http/bin/gen-api-test/gen-api-test.ts +136 -0
  96. package/src/http/bin/gen-api-test/index.ts +1 -0
  97. package/src/http/bin/gen-api-type/calc-code.ts +25 -0
  98. package/src/http/bin/gen-api-type/gen-api-type.ts +127 -0
  99. package/src/http/bin/gen-api-type/index.ts +2 -0
  100. package/src/http/bin/index.ts +2 -0
  101. package/src/http/index.ts +3 -0
  102. package/src/huawei/README.md +1 -0
  103. package/src/huawei/index.ts +2 -0
  104. package/src/huawei/moderation/index.ts +1 -0
  105. package/src/huawei/moderation/moderation.ts +355 -0
  106. package/src/huawei/obs/esdk-obs-nodejs.d.ts +87 -0
  107. package/src/huawei/obs/index.ts +1 -0
  108. package/src/huawei/obs/obs.ts +42 -0
  109. package/src/index.ts +21 -2
  110. package/src/json/README.md +92 -0
  111. package/src/json/index.ts +1 -0
  112. package/src/json/repair.ts +18 -0
  113. package/src/log/logger.ts +15 -4
  114. package/src/openai/README.md +1 -0
  115. package/src/openai/index.ts +1 -0
  116. package/src/openai/openai.ts +509 -0
  117. package/src/orchestration/README.md +9 -7
  118. package/src/orchestration/dispatching/dispatcher.ts +83 -0
  119. package/src/orchestration/dispatching/index.ts +2 -0
  120. package/src/orchestration/dispatching/selector/base-selector.ts +39 -0
  121. package/src/orchestration/dispatching/selector/down-count-selector.ts +119 -0
  122. package/src/orchestration/dispatching/selector/index.ts +2 -0
  123. package/src/orchestration/index.ts +2 -0
  124. package/src/orchestration/scheduling/index.ts +2 -0
  125. package/src/orchestration/scheduling/scheduler.ts +103 -0
  126. package/src/orchestration/scheduling/task.ts +32 -0
  127. package/src/random/README.md +8 -7
  128. package/src/random/base.ts +66 -0
  129. package/src/random/index.ts +5 -1
  130. package/src/random/random-boolean.ts +40 -0
  131. package/src/random/random-integer.ts +60 -0
  132. package/src/random/random-number.ts +72 -0
  133. package/src/random/random-string.ts +66 -0
  134. package/src/request/README.md +108 -0
  135. package/src/request/fetch/base.ts +108 -0
  136. package/src/request/fetch/browser.ts +280 -0
  137. package/src/request/fetch/general.ts +20 -0
  138. package/src/request/fetch/index.ts +4 -0
  139. package/src/request/fetch/nodejs.ts +280 -0
  140. package/src/request/index.ts +2 -0
  141. package/src/request/request/base.ts +246 -0
  142. package/src/request/request/general.ts +63 -0
  143. package/src/request/request/index.ts +3 -0
  144. package/src/request/request/resource.ts +68 -0
  145. package/src/result/README.md +4 -0
  146. package/src/result/controller.ts +58 -0
  147. package/src/result/either.ts +363 -0
  148. package/src/result/generator.ts +168 -0
  149. package/src/result/index.ts +3 -0
  150. package/src/route/README.md +105 -0
  151. package/src/route/adapter/browser.ts +122 -0
  152. package/src/route/adapter/driver.ts +56 -0
  153. package/src/route/adapter/index.ts +2 -0
  154. package/src/route/index.ts +3 -0
  155. package/src/route/router/index.ts +2 -0
  156. package/src/route/router/route.ts +630 -0
  157. package/src/route/router/router.ts +1641 -0
  158. package/src/route/uri/hash.ts +307 -0
  159. package/src/route/uri/index.ts +7 -0
  160. package/src/route/uri/pathname.ts +376 -0
  161. package/src/route/uri/search.ts +412 -0
  162. package/src/service/README.md +1 -0
  163. package/src/service/index.ts +1 -0
  164. package/src/service/service.ts +110 -0
  165. package/src/socket/README.md +105 -0
  166. package/src/socket/client/index.ts +2 -0
  167. package/src/socket/client/socket-unit.ts +658 -0
  168. package/src/socket/client/socket.ts +203 -0
  169. package/src/socket/common/index.ts +2 -0
  170. package/src/socket/common/socket-unit-common.ts +23 -0
  171. package/src/socket/common/socket-unit-heartbeat.ts +427 -0
  172. package/src/socket/index.ts +3 -0
  173. package/src/socket/server/index.ts +3 -0
  174. package/src/socket/server/server.ts +183 -0
  175. package/src/socket/server/socket-unit.ts +448 -0
  176. package/src/socket/server/socket.ts +264 -0
  177. package/src/storage/table.ts +3 -3
  178. package/src/timer/expiration/expiration-manager.ts +3 -3
  179. package/src/timer/expiration/remaining-manager.ts +3 -3
  180. package/src/tube/README.md +99 -0
  181. package/src/tube/helper.ts +137 -0
  182. package/src/tube/index.ts +2 -0
  183. package/src/tube/tube.ts +880 -0
  184. package/src/weixin/README.md +1 -0
  185. package/src/weixin/index.ts +2 -0
  186. package/src/weixin/official-account/authorization.ts +157 -0
  187. package/src/weixin/official-account/index.ts +2 -0
  188. package/src/weixin/official-account/js-api.ts +132 -0
  189. package/src/weixin/open/index.ts +1 -0
  190. package/src/weixin/open/oauth2.ts +131 -0
  191. package/tests/unit/ai/ai.spec.ts +85 -0
  192. package/tests/unit/aio/content.spec.ts +105 -0
  193. package/tests/unit/aio/json.spec.ts +146 -0
  194. package/tests/unit/aio/prompt.spec.ts +111 -0
  195. package/tests/unit/basic/error.spec.ts +16 -4
  196. package/tests/unit/basic/promise.spec.ts +158 -50
  197. package/tests/unit/basic/schedule.spec.ts +74 -0
  198. package/tests/unit/basic/stream.spec.ts +90 -37
  199. package/tests/unit/credential/api-key.spec.ts +36 -0
  200. package/tests/unit/credential/bearer.spec.ts +23 -0
  201. package/tests/unit/credential/json-web-token.spec.ts +23 -0
  202. package/tests/unit/credential/password.spec.ts +40 -0
  203. package/tests/unit/cron/cron.spec.ts +84 -0
  204. package/tests/unit/event/class-event-proxy.spec.ts +3 -3
  205. package/tests/unit/event/event-manager.spec.ts +3 -3
  206. package/tests/unit/event/instance-event-proxy.spec.ts +3 -3
  207. package/tests/unit/exception/error/error.spec.ts +83 -0
  208. package/tests/unit/exception/error/match.spec.ts +81 -0
  209. package/tests/unit/form/inputor-controller/base.spec.ts +458 -0
  210. package/tests/unit/form/inputor-controller/boolean.spec.ts +30 -0
  211. package/tests/unit/form/inputor-controller/file.spec.ts +27 -0
  212. package/tests/unit/form/inputor-controller/form.spec.ts +120 -0
  213. package/tests/unit/form/inputor-controller/helper.spec.ts +67 -0
  214. package/tests/unit/form/inputor-controller/multi-select.spec.ts +34 -0
  215. package/tests/unit/form/inputor-controller/number.spec.ts +36 -0
  216. package/tests/unit/form/inputor-controller/select.spec.ts +49 -0
  217. package/tests/unit/form/inputor-controller/text.spec.ts +34 -0
  218. package/tests/unit/http/api/api-core-host.spec.ts +207 -0
  219. package/tests/unit/http/api/api-schema.spec.ts +120 -0
  220. package/tests/unit/http/api/api-server.spec.ts +363 -0
  221. package/tests/unit/http/api/api-test.spec.ts +117 -0
  222. package/tests/unit/http/api/api.spec.ts +121 -0
  223. package/tests/unit/http/api-adapter/node-http.spec.ts +187 -0
  224. package/tests/unit/identifier/uuid.spec.ts +0 -1
  225. package/tests/unit/json/repair.spec.ts +11 -0
  226. package/tests/unit/log/logger.spec.ts +19 -4
  227. package/tests/unit/openai/openai.spec.ts +64 -0
  228. package/tests/unit/orchestration/dispatching/dispatcher.spec.ts +41 -0
  229. package/tests/unit/orchestration/dispatching/selector/down-count-selector.spec.ts +81 -0
  230. package/tests/unit/orchestration/scheduling/scheduler.spec.ts +103 -0
  231. package/tests/unit/random/base.spec.ts +58 -0
  232. package/tests/unit/random/random-boolean.spec.ts +25 -0
  233. package/tests/unit/random/random-integer.spec.ts +32 -0
  234. package/tests/unit/random/random-number.spec.ts +33 -0
  235. package/tests/unit/random/random-string.spec.ts +22 -0
  236. package/tests/unit/request/fetch/browser.spec.ts +222 -0
  237. package/tests/unit/request/fetch/general.spec.ts +43 -0
  238. package/tests/unit/request/fetch/nodejs.spec.ts +225 -0
  239. package/tests/unit/request/request/base.spec.ts +382 -0
  240. package/tests/unit/request/request/general.spec.ts +160 -0
  241. package/tests/unit/result/controller.spec.ts +82 -0
  242. package/tests/unit/result/either.spec.ts +377 -0
  243. package/tests/unit/result/generator.spec.ts +273 -0
  244. package/tests/unit/route/router/route.spec.ts +430 -0
  245. package/tests/unit/route/router/router.spec.ts +407 -0
  246. package/tests/unit/route/uri/hash.spec.ts +72 -0
  247. package/tests/unit/route/uri/pathname.spec.ts +146 -0
  248. package/tests/unit/route/uri/search.spec.ts +107 -0
  249. package/tests/unit/socket/client.spec.ts +208 -0
  250. package/tests/unit/socket/server.spec.ts +133 -0
  251. package/tests/unit/socket/socket-unit-heartbeat.spec.ts +214 -0
  252. package/tests/unit/tube/helper.spec.ts +139 -0
  253. package/tests/unit/tube/tube.spec.ts +501 -0
  254. package/vite.config.ts +2 -1
  255. package/dist/index.js +0 -50
  256. package/dist/index.js.map +0 -209
  257. package/src/random/string.ts +0 -35
  258. package/tests/unit/random/string.spec.ts +0 -11
@@ -0,0 +1,146 @@
1
+ import { expect, test, vi } from "vitest"
2
+
3
+ import type { StandardSchemaV1 } from "@standard-schema/spec"
4
+ import { Logger } from "#Source/log/index.ts"
5
+ import {
6
+ JsonModeResponseParser,
7
+ extractJsonBlock,
8
+ } from "#Source/aio/index.ts"
9
+
10
+ interface TestJsonOutput {
11
+ value: number
12
+ label?: string | undefined
13
+ }
14
+
15
+ const createSyncSchema = (
16
+ options: { validateCount?: { current: number } | undefined } = {}
17
+ ): StandardSchemaV1<unknown, TestJsonOutput> => {
18
+ const validateCount = options.validateCount
19
+
20
+ return {
21
+ "~standard": {
22
+ version: 1 as const,
23
+ vendor: "mobius-aio-json-sync-spec",
24
+ validate: (value: unknown): StandardSchemaV1.Result<TestJsonOutput> => {
25
+ if (validateCount !== undefined) {
26
+ validateCount.current = validateCount.current + 1
27
+ }
28
+
29
+ if (typeof value !== "object" || value === null) {
30
+ return {
31
+ issues: [{ message: "value must be an object" }],
32
+ }
33
+ }
34
+
35
+ const record = value as Record<string, unknown>
36
+ if (typeof record["value"] !== "number") {
37
+ return {
38
+ issues: [{ message: "value must be a number" }],
39
+ }
40
+ }
41
+
42
+ const output: TestJsonOutput = {
43
+ value: record["value"],
44
+ }
45
+
46
+ if (typeof record["label"] === "string") {
47
+ output.label = record["label"]
48
+ }
49
+
50
+ return {
51
+ value: output,
52
+ }
53
+ },
54
+ },
55
+ }
56
+ }
57
+
58
+ const createAsyncSchema = (): StandardSchemaV1<unknown, TestJsonOutput> => {
59
+ return {
60
+ "~standard": {
61
+ version: 1 as const,
62
+ vendor: "mobius-aio-json-async-spec",
63
+ validate: async (_value: unknown): Promise<StandardSchemaV1.Result<TestJsonOutput>> => {
64
+ await Promise.resolve()
65
+
66
+ return {
67
+ value: {
68
+ value: 1,
69
+ },
70
+ }
71
+ },
72
+ },
73
+ }
74
+ }
75
+
76
+ test("extractJsonBlock returns the first fenced json block and throws when none exists", () => {
77
+ const response = [
78
+ "before",
79
+ "```json",
80
+ "{\"value\":1}",
81
+ "```",
82
+ "```json",
83
+ "{\"value\":2}",
84
+ "```",
85
+ ].join("\n")
86
+
87
+ expect(extractJsonBlock(response)).toBe(["```json", "{\"value\":1}", "```"].join("\n"))
88
+ expect(() => extractJsonBlock("plain text only")).toThrow("Failed to extract json block from response")
89
+ })
90
+
91
+ test("JsonModeResponseParser.extractJsonContent strips fences, repairs json-like content and falls back to raw text", () => {
92
+ const parser = new JsonModeResponseParser({
93
+ outputSchema: createSyncSchema(),
94
+ })
95
+
96
+ const repairedFromFence = parser.extractJsonContent(["header", "```json", "{value:1,}", "```"].join("\n"))
97
+ const repairedFromRawText = parser.extractJsonContent("{label:'mobius',value:2,}")
98
+
99
+ expect(repairedFromFence).toBe('\n{"value":1}\n')
100
+ expect(repairedFromRawText).toBe('{"label":"mobius","value":2}')
101
+ })
102
+
103
+ test("JsonModeResponseParser.parse validates sync output, caches by source text and throws for invalid or async schemas", () => {
104
+ const validateCount = { current: 0 }
105
+ const parser = new JsonModeResponseParser({
106
+ outputSchema: createSyncSchema({ validateCount }),
107
+ })
108
+
109
+ const first = parser.parse('{"value":1,"label":"ok"}')
110
+ const second = parser.parse('{"value":1,"label":"ok"}')
111
+
112
+ expect(first).toEqual({ value: 1, label: "ok" })
113
+ expect(second).toBe(first)
114
+ expect(validateCount.current).toBe(1)
115
+
116
+ const invalidParser = new JsonModeResponseParser({
117
+ outputSchema: createSyncSchema(),
118
+ })
119
+ expect(() => invalidParser.parse('{"label":"missing-value"}')).toThrow("Variable validation failed")
120
+
121
+ const asyncParser = new JsonModeResponseParser({
122
+ outputSchema: createAsyncSchema(),
123
+ })
124
+ expect(() => asyncParser.parse('{"value":1}')).toThrow("Validation result is a promise")
125
+ })
126
+
127
+ test("JsonModeResponseParser.check returns false and logs the failure context when parsing fails", () => {
128
+ const logger = new Logger({
129
+ name: "AioJsonTest",
130
+ configs: {
131
+ enabled: false,
132
+ },
133
+ })
134
+ const logSpy = vi.spyOn(logger, "log")
135
+ const parser = new JsonModeResponseParser({
136
+ logger,
137
+ outputSchema: createSyncSchema(),
138
+ })
139
+
140
+ expect(parser.check('{"value":3}')).toBe(true)
141
+ expect(parser.check('{"label":"missing-value"}')).toBe(false)
142
+
143
+ expect(logSpy).toHaveBeenCalledTimes(2)
144
+ expect(logSpy.mock.calls[0]?.[0]).toBe("check error:")
145
+ expect(logSpy.mock.calls[1]).toEqual(["error response:", '{"label":"missing-value"}'])
146
+ })
@@ -0,0 +1,111 @@
1
+ import { expect, test } from "vitest"
2
+
3
+ import {
4
+ Prompt,
5
+ emptyLine,
6
+ orderedListItem,
7
+ scopeContent,
8
+ text,
9
+ unorderedListItem,
10
+ } from "#Source/aio/index.ts"
11
+
12
+ test("text creates a plain text prompt block", () => {
13
+ expect(text("hello")).toEqual({
14
+ type: "text",
15
+ content: "hello",
16
+ })
17
+ })
18
+
19
+ test("emptyLine creates an empty-line prompt block", () => {
20
+ expect(emptyLine()).toEqual({
21
+ type: "empty-line",
22
+ })
23
+ })
24
+
25
+ test("scopeContent wraps content with triple-quote boundaries", () => {
26
+ expect(scopeContent("context")).toEqual({
27
+ type: "scope-content",
28
+ content: '"""\ncontext\n"""',
29
+ })
30
+ })
31
+
32
+ test("orderedListItem creates an ordered-list prompt block", () => {
33
+ expect(orderedListItem("first")).toEqual({
34
+ type: "ordered-list-item",
35
+ content: "first",
36
+ })
37
+ })
38
+
39
+ test("unorderedListItem creates an unordered-list prompt block", () => {
40
+ expect(unorderedListItem("first")).toEqual({
41
+ type: "unordered-list-item",
42
+ content: "first",
43
+ })
44
+ })
45
+
46
+ test("Prompt.addBlock appends a block produced by helpers and returns the same instance", async () => {
47
+ const prompt = new Prompt({})
48
+
49
+ expect(prompt.addBlock(helpers => helpers.text("from-helper"))).toBe(prompt)
50
+ await expect(prompt.getPromptInString()).resolves.toBe("from-helper")
51
+ })
52
+
53
+ test("Prompt.addText appends a text block", async () => {
54
+ const prompt = new Prompt({})
55
+
56
+ expect(prompt.addText("alpha")).toBe(prompt)
57
+ await expect(prompt.getPromptInString()).resolves.toBe("alpha")
58
+ })
59
+
60
+ test("Prompt.addEmptyLine appends an empty line block", async () => {
61
+ const prompt = new Prompt({})
62
+
63
+ expect(prompt.addEmptyLine()).toBe(prompt)
64
+ await expect(prompt.getPromptInString()).resolves.toBe("\r\n")
65
+ })
66
+
67
+ test("Prompt.addScopeContent appends a scoped content block", async () => {
68
+ const prompt = new Prompt({})
69
+
70
+ expect(prompt.addScopeContent("body")).toBe(prompt)
71
+ await expect(prompt.getPromptInString()).resolves.toBe('"""\nbody\n"""')
72
+ })
73
+
74
+ test("Prompt.addOrderedListItem appends an ordered list block", async () => {
75
+ const prompt = new Prompt({})
76
+
77
+ expect(prompt.addOrderedListItem("step")).toBe(prompt)
78
+ await expect(prompt.getPromptInString()).resolves.toBe("1. step")
79
+ })
80
+
81
+ test("Prompt.addUnorderedListItem appends an unordered list block", async () => {
82
+ const prompt = new Prompt({})
83
+
84
+ expect(prompt.addUnorderedListItem("item")).toBe(prompt)
85
+ await expect(prompt.getPromptInString()).resolves.toBe("- item")
86
+ })
87
+
88
+ test("Prompt.getPromptInString renders prompt blocks with list numbering and resets between block groups", async () => {
89
+ const prompt = new Prompt({})
90
+
91
+ prompt
92
+ .addText("Intro")
93
+ .addOrderedListItem("First")
94
+ .addOrderedListItem("Second")
95
+ .addText("Break")
96
+ .addOrderedListItem("Reset")
97
+ .addUnorderedListItem("One")
98
+ .addUnorderedListItem("Two")
99
+ .addScopeContent("body")
100
+
101
+ await expect(prompt.getPromptInString()).resolves.toBe([
102
+ "Intro",
103
+ "1. First",
104
+ "2. Second",
105
+ "Break",
106
+ "1. Reset",
107
+ "- One",
108
+ "- Two",
109
+ '"""\nbody\n"""',
110
+ ].join("\r\n"))
111
+ })
@@ -2,7 +2,8 @@ import { expect, test } from "vitest"
2
2
 
3
3
  import {
4
4
  errorIsNetworkError,
5
- errorStringifyException,
5
+ errorStringify,
6
+ toError,
6
7
  } from "#Source/basic/index.ts"
7
8
 
8
9
  test("errorIsNetworkError returns expected values", () => {
@@ -26,7 +27,18 @@ test("errorIsNetworkError returns expected values", () => {
26
27
  expect(errorIsNetworkError(safariWithStack)).toBe(false)
27
28
  })
28
29
 
29
- test("errorStringifyException returns readable output", () => {
30
- expect(errorStringifyException(new Error("boom"))).toBe("Error: boom")
31
- expect(errorStringifyException(123)).toBe("123")
30
+ test("errorStringify returns readable output", () => {
31
+ expect(errorStringify(new Error("boom"))).toBe("Error: boom")
32
+ expect(errorStringify(123)).toBe("123")
33
+ })
34
+
35
+ test("toError returns expected values", () => {
36
+ const error = new Error("boom")
37
+
38
+ expect(toError(error)).toBe(error)
39
+
40
+ const wrappedError = toError(123)
41
+
42
+ expect(wrappedError).toBeInstanceOf(Error)
43
+ expect(wrappedError.message).toBe("123")
32
44
  })
@@ -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()