@nan0web/ui 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (131) hide show
  1. package/.datasets/README.dataset.jsonl +12 -0
  2. package/.editorconfig +20 -0
  3. package/CONTRIBUTING.md +42 -0
  4. package/LICENSE +15 -0
  5. package/README.md +238 -0
  6. package/docs/uk/README.md +240 -0
  7. package/package.json +64 -0
  8. package/playground/User.js +52 -0
  9. package/playground/currency.exchange.js +48 -0
  10. package/playground/i18n/index.js +21 -0
  11. package/playground/i18n/uk.js +53 -0
  12. package/playground/language.form.js +25 -0
  13. package/playground/main.js +72 -0
  14. package/playground/registration.form.js +58 -0
  15. package/playground/topup.telephone.js +62 -0
  16. package/src/App/Command/Options.js +78 -0
  17. package/src/App/Command/index.js +9 -0
  18. package/src/App/Core/CoreApp.js +129 -0
  19. package/src/App/Core/UI.js +116 -0
  20. package/src/App/Core/Widget.js +67 -0
  21. package/src/App/Core/index.js +11 -0
  22. package/src/App/Scenario.js +45 -0
  23. package/src/App/User/Command/Message.js +44 -0
  24. package/src/App/User/Command/Options.js +48 -0
  25. package/src/App/User/Command/index.js +11 -0
  26. package/src/App/User/UserApp.js +73 -0
  27. package/src/App/User/UserApp.test.js +56 -0
  28. package/src/App/User/UserUI.js +20 -0
  29. package/src/App/User/UserUI.test.js +51 -0
  30. package/src/App/User/index.js +15 -0
  31. package/src/App/index.js +22 -0
  32. package/src/Component/Process/Input.js +70 -0
  33. package/src/Component/Process/Process.js +26 -0
  34. package/src/Component/Process/index.js +5 -0
  35. package/src/Component/Welcome/Input.js +50 -0
  36. package/src/Component/Welcome/Welcome.js +26 -0
  37. package/src/Component/Welcome/index.js +5 -0
  38. package/src/Component/index.js +9 -0
  39. package/src/Frame/Frame.js +591 -0
  40. package/src/Frame/Frame.test.js +429 -0
  41. package/src/Frame/Props.js +102 -0
  42. package/src/Locale.js +119 -0
  43. package/src/Model/User/User.js +56 -0
  44. package/src/Model/index.js +7 -0
  45. package/src/README.md.js +371 -0
  46. package/src/StdIn.js +111 -0
  47. package/src/StdOut.js +99 -0
  48. package/src/View/RenderOptions.js +48 -0
  49. package/src/View/View.js +289 -0
  50. package/src/View/View.test.js +77 -0
  51. package/src/core/Form/Form.js +289 -0
  52. package/src/core/Form/Form.test.js +116 -0
  53. package/src/core/Form/Input.js +116 -0
  54. package/src/core/Form/Input.test.js +58 -0
  55. package/src/core/Form/Message.js +86 -0
  56. package/src/core/Form/Message.test.js +54 -0
  57. package/src/core/Form/index.js +11 -0
  58. package/src/core/InputAdapter.js +41 -0
  59. package/src/core/InputAdapter.test.js +35 -0
  60. package/src/core/Message/InputMessage.js +119 -0
  61. package/src/core/Message/InputMessage.test.js +45 -0
  62. package/src/core/Message/Message.js +77 -0
  63. package/src/core/Message/Message.test.js +58 -0
  64. package/src/core/Message/OutputMessage.js +143 -0
  65. package/src/core/Message/OutputMessage.test.js +61 -0
  66. package/src/core/Message/index.js +7 -0
  67. package/src/core/OutputAdapter.js +50 -0
  68. package/src/core/OutputAdapter.test.js +35 -0
  69. package/src/core/Stream.js +71 -0
  70. package/src/core/Stream.test.js +78 -0
  71. package/src/core/StreamEntry.js +59 -0
  72. package/src/core/index.js +13 -0
  73. package/src/functions.js +38 -0
  74. package/src/index.js +34 -0
  75. package/src/index.test.js +14 -0
  76. package/src/models/SimpleUser.js +18 -0
  77. package/stories/App/AppView.js +15 -0
  78. package/stories/App/AppView.test.js +22 -0
  79. package/stories/App/RenderOptions.js +14 -0
  80. package/stories/nodejs/interface.test.js +27 -0
  81. package/system.md +187 -0
  82. package/system1.md +137 -0
  83. package/task.md +181 -0
  84. package/tsconfig.json +23 -0
  85. package/types/App/Command/Options.d.ts +46 -0
  86. package/types/App/Command/index.d.ts +8 -0
  87. package/types/App/Core/CoreApp.d.ts +70 -0
  88. package/types/App/Core/UI.d.ts +49 -0
  89. package/types/App/Core/Widget.d.ts +40 -0
  90. package/types/App/Core/index.d.ts +10 -0
  91. package/types/App/Scenario.d.ts +26 -0
  92. package/types/App/User/Command/Message.d.ts +30 -0
  93. package/types/App/User/Command/Options.d.ts +27 -0
  94. package/types/App/User/Command/index.d.ts +8 -0
  95. package/types/App/User/UserApp.d.ts +31 -0
  96. package/types/App/User/UserUI.d.ts +18 -0
  97. package/types/App/User/index.d.ts +12 -0
  98. package/types/App/index.d.ts +14 -0
  99. package/types/Component/Process/Input.d.ts +48 -0
  100. package/types/Component/Process/Process.d.ts +13 -0
  101. package/types/Component/Process/index.d.ts +4 -0
  102. package/types/Component/Welcome/Input.d.ts +34 -0
  103. package/types/Component/Welcome/Welcome.d.ts +13 -0
  104. package/types/Component/Welcome/index.d.ts +4 -0
  105. package/types/Component/index.d.ts +8 -0
  106. package/types/Frame/Frame.d.ts +186 -0
  107. package/types/Frame/Props.d.ts +77 -0
  108. package/types/Locale.d.ts +55 -0
  109. package/types/Model/User/User.d.ts +36 -0
  110. package/types/Model/index.d.ts +6 -0
  111. package/types/StdIn.d.ts +62 -0
  112. package/types/StdOut.d.ts +52 -0
  113. package/types/View/RenderOptions.d.ts +29 -0
  114. package/types/View/View.d.ts +115 -0
  115. package/types/core/Form/Form.d.ts +123 -0
  116. package/types/core/Form/Input.d.ts +69 -0
  117. package/types/core/Form/Message.d.ts +28 -0
  118. package/types/core/Form/index.d.ts +5 -0
  119. package/types/core/InputAdapter.d.ts +28 -0
  120. package/types/core/Message/InputMessage.d.ts +71 -0
  121. package/types/core/Message/Message.d.ts +50 -0
  122. package/types/core/Message/OutputMessage.d.ts +53 -0
  123. package/types/core/Message/index.d.ts +5 -0
  124. package/types/core/OutputAdapter.d.ts +33 -0
  125. package/types/core/Stream.d.ts +27 -0
  126. package/types/core/StreamEntry.d.ts +45 -0
  127. package/types/core/index.d.ts +9 -0
  128. package/types/functions.d.ts +3 -0
  129. package/types/index.d.ts +20 -0
  130. package/types/models/SimpleUser.d.ts +21 -0
  131. package/vitest.config.js +26 -0
@@ -0,0 +1,77 @@
1
+ import { Message as BaseMessage } from "@nan0web/co"
2
+
3
+ /**
4
+ * Base UI message class.
5
+ *
6
+ * @class UIMessage
7
+ * @extends BaseMessage
8
+ */
9
+ class UIMessage extends BaseMessage {
10
+ static TYPES = {
11
+ TEXT: 'text',
12
+ FORM: 'form',
13
+ PROGRESS: 'progress',
14
+ ERROR: 'error',
15
+ INFO: 'info',
16
+ SUCCESS: 'success',
17
+ WARNING: 'warning',
18
+ COMMAND: 'command',
19
+ NAVIGATION: 'navigation'
20
+ }
21
+
22
+ /** @type {string} */
23
+ type = ""
24
+ /** @type {string} */
25
+ id = ""
26
+
27
+ /**
28
+ * Creates a UIMessage.
29
+ *
30
+ * @param {Object} [input={}] - Message properties.
31
+ */
32
+ constructor(input = {}) {
33
+ super(input)
34
+
35
+ const {
36
+ type = this.type,
37
+ id = this.id,
38
+ } = input
39
+ this.id = id || `ui-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`
40
+ this.type = String(type)
41
+
42
+ if (!('body' in input) && 'content' in input) {
43
+ this.body = Array.isArray(input.content) ? input.content : [input.content]
44
+ }
45
+ }
46
+
47
+ /**
48
+ * Creates a UIMessage instance from plain data.
49
+ *
50
+ * @param {Object} data - Message data.
51
+ * @returns {UIMessage}
52
+ */
53
+ static from(data) {
54
+ if (data instanceof UIMessage) return data
55
+ return new this(data)
56
+ }
57
+
58
+ /**
59
+ * Checks if the message type is valid.
60
+ *
61
+ * @returns {boolean}
62
+ */
63
+ isValidType() {
64
+ return Object.values(UIMessage.TYPES).includes(this.type)
65
+ }
66
+
67
+ /**
68
+ * Checks whether the message contains any body content.
69
+ *
70
+ * @returns {boolean}
71
+ */
72
+ isEmpty() {
73
+ return !this.body || this.body.length === 0
74
+ }
75
+ }
76
+
77
+ export default UIMessage
@@ -0,0 +1,58 @@
1
+ import { describe, it } from "node:test"
2
+ import { strict as assert } from "node:assert"
3
+ import UIMessage from "./Message.js"
4
+
5
+ describe("UIMessage", () => {
6
+ it("should create instance with default values", () => {
7
+ const msg = new UIMessage()
8
+ assert.ok(msg instanceof UIMessage)
9
+ assert.equal(typeof msg.type, "string")
10
+ assert.equal(typeof msg.id, "string")
11
+ assert.ok(msg.time instanceof Date)
12
+ })
13
+
14
+ it("should create instance with custom values", () => {
15
+ const props = {
16
+ type: "test",
17
+ id: "custom-id",
18
+ body: ["test content"]
19
+ }
20
+ const msg = new UIMessage(props)
21
+ assert.equal(msg.type, "test")
22
+ assert.equal(msg.id, "custom-id")
23
+ assert.deepEqual(msg.body, ["test content"])
24
+ })
25
+
26
+ it("should generate unique id when not provided", () => {
27
+ const msg1 = new UIMessage()
28
+ const msg2 = new UIMessage()
29
+ assert.notEqual(msg1.id, msg2.id)
30
+ })
31
+
32
+ it("should use content as body when body not provided", () => {
33
+ const msg = new UIMessage({ content: ["hello"] })
34
+ assert.deepEqual(msg.body, ["hello"])
35
+ })
36
+
37
+ it("should create from data using from() method", () => {
38
+ const data = { type: "info", body: ["test"] }
39
+ const msg = UIMessage.from(data)
40
+ assert.ok(msg instanceof UIMessage)
41
+ assert.equal(msg.type, "info")
42
+ assert.deepEqual(msg.body, ["test"])
43
+ })
44
+
45
+ it("should validate type correctly", () => {
46
+ const validMsg = new UIMessage({ type: UIMessage.TYPES.TEXT })
47
+ const invalidMsg = new UIMessage({ type: "invalid-type" })
48
+ assert.ok(validMsg.isValidType())
49
+ assert.ok(!invalidMsg.isValidType())
50
+ })
51
+
52
+ it("should check if message is empty", () => {
53
+ const emptyMsg = new UIMessage()
54
+ const nonEmptyMsg = new UIMessage({ body: ["content"] })
55
+ assert.ok(emptyMsg.isEmpty())
56
+ assert.ok(!nonEmptyMsg.isEmpty())
57
+ })
58
+ })
@@ -0,0 +1,143 @@
1
+ import UIMessage from "./Message.js"
2
+
3
+ /**
4
+ * OutputMessage – message sent from the system to the UI.
5
+ *
6
+ * @class OutputMessage
7
+ * @extends UIMessage
8
+ */
9
+ export default class OutputMessage extends UIMessage {
10
+ static PRIORITY = {
11
+ LOW: 0,
12
+ NORMAL: 1,
13
+ HIGH: 2,
14
+ CRITICAL: 3
15
+ }
16
+
17
+ /** @type {string[]} */
18
+ body
19
+ /** @type {Object} */
20
+ meta = {}
21
+ /** @type {Error|null} */
22
+ error = null
23
+ /** @type {number} */
24
+ priority = OutputMessage.PRIORITY.NORMAL
25
+
26
+ /**
27
+ * Creates an OutputMessage.
28
+ *
29
+ * @param {Object} [input={}] - Message properties.
30
+ */
31
+ constructor(input = {}) {
32
+ super(input)
33
+
34
+ const {
35
+ content = [],
36
+ body = [],
37
+ meta = {},
38
+ error = null,
39
+ priority = OutputMessage.PRIORITY.NORMAL
40
+ } = input
41
+
42
+ const contentSource = 'body' in input ? body :
43
+ 'content' in input ? content : []
44
+
45
+ this.body = Array.isArray(contentSource) ?
46
+ contentSource :
47
+ (contentSource ? [String(contentSource)] : [])
48
+
49
+ this.meta = meta
50
+ this.error = error instanceof Error ? error :
51
+ error ? new Error(String(error)) : null
52
+ this.priority = Number(priority)
53
+
54
+ if (!this.type && this.error) {
55
+ this.type = UIMessage.TYPES.ERROR
56
+ } else if (!this.type) {
57
+ this.type = UIMessage.TYPES.INFO
58
+ }
59
+ }
60
+
61
+ /** @returns {string[]} */
62
+ get content() {
63
+ return this.body
64
+ }
65
+ /** @param {string[]|string} value */
66
+ set content(value) {
67
+ this.body = Array.isArray(value) ? value : [String(value)]
68
+ }
69
+ /** @returns {number} */
70
+ get size() {
71
+ return this.content.length
72
+ }
73
+ /** @returns {boolean} */
74
+ get isError() {
75
+ return this.error !== null || this.type === UIMessage.TYPES.ERROR
76
+ }
77
+ /** @returns {boolean} */
78
+ get isInfo() {
79
+ return this.type === UIMessage.TYPES.INFO || this.type === UIMessage.TYPES.SUCCESS
80
+ }
81
+
82
+ /**
83
+ * Combines multiple messages into a new one.
84
+ *
85
+ * @param {...OutputMessage} messages - Messages to combine.
86
+ * @returns {OutputMessage}
87
+ */
88
+ combine(...messages) {
89
+ const combinedContent = [...this.content]
90
+ let combinedMeta = { ...this.meta }
91
+ let combinedError = this.error
92
+ let combinedPriority = this.priority
93
+
94
+ messages.forEach(msg => {
95
+ if (msg instanceof OutputMessage) {
96
+ combinedContent.push(...msg.content)
97
+ combinedMeta = { ...combinedMeta, ...msg.meta }
98
+ if (msg.error) combinedError = msg.error
99
+ combinedPriority = Math.max(combinedPriority, msg.priority)
100
+ }
101
+ })
102
+
103
+ return new OutputMessage({
104
+ content: combinedContent,
105
+ meta: combinedMeta,
106
+ error: combinedError,
107
+ priority: combinedPriority,
108
+ type: this.type
109
+ })
110
+ }
111
+
112
+ /**
113
+ * Serialises the message to a plain JSON object.
114
+ *
115
+ * @returns {Object}
116
+ */
117
+ toJSON() {
118
+ return {
119
+ body: this.body,
120
+ content: this.content,
121
+ meta: this.meta,
122
+ type: this.type,
123
+ id: this.id,
124
+ time: this.time.toISOString(),
125
+ error: this.error ? {
126
+ message: this.error.message,
127
+ stack: this.error.stack
128
+ } : null,
129
+ priority: this.priority
130
+ }
131
+ }
132
+
133
+ /**
134
+ * Creates an OutputMessage from plain input.
135
+ *
136
+ * @param {Object} input - Message data.
137
+ * @returns {OutputMessage}
138
+ */
139
+ static from(input) {
140
+ if (input instanceof OutputMessage) return input
141
+ return new OutputMessage(input)
142
+ }
143
+ }
@@ -0,0 +1,61 @@
1
+ import { describe, it } from "node:test"
2
+ import { strict as assert } from "node:assert"
3
+ import OutputMessage from "./OutputMessage.js"
4
+
5
+ describe("OutputMessage", () => {
6
+ it("should create instance with default values", () => {
7
+ const msg = new OutputMessage()
8
+ assert.ok(msg instanceof OutputMessage)
9
+ assert.deepEqual(msg.body, [])
10
+ assert.deepEqual(msg.meta, {})
11
+ assert.equal(msg.error, null)
12
+ assert.equal(msg.priority, OutputMessage.PRIORITY.NORMAL)
13
+ })
14
+
15
+ it("should create instance with custom values", () => {
16
+ const props = {
17
+ content: ["test content"],
18
+ meta: { key: "value" },
19
+ error: new Error("test error"),
20
+ priority: OutputMessage.PRIORITY.HIGH
21
+ }
22
+ const msg = new OutputMessage(props)
23
+ assert.deepEqual(msg.content, ["test content"])
24
+ assert.deepEqual(msg.meta, { key: "value" })
25
+ assert.ok(msg.error instanceof Error)
26
+ assert.equal(msg.priority, OutputMessage.PRIORITY.HIGH)
27
+ })
28
+
29
+ it("should set type based on error presence", () => {
30
+ const errorMsg = new OutputMessage({ error: "error occurred" })
31
+ const infoMsg = new OutputMessage({ content: ["info"] })
32
+ assert.equal(errorMsg.type, "error")
33
+ assert.equal(infoMsg.type, "info")
34
+ })
35
+
36
+ it("should handle string content", () => {
37
+ const msg = new OutputMessage({ content: "single string" })
38
+ assert.deepEqual(msg.content, ["single string"])
39
+ })
40
+
41
+ it("should combine messages", () => {
42
+ const msg1 = new OutputMessage({ content: ["part1"], priority: 1 })
43
+ const msg2 = new OutputMessage({ content: ["part2"], priority: 2 })
44
+ const combined = msg1.combine(msg2)
45
+ assert.deepEqual(combined.content, ["part1", "part2"])
46
+ assert.equal(combined.priority, 2)
47
+ })
48
+
49
+ it("should convert to JSON", () => {
50
+ const msg = new OutputMessage({
51
+ content: ["test"],
52
+ meta: { test: true },
53
+ error: new Error("test error")
54
+ })
55
+ const json = msg.toJSON()
56
+ assert.ok(json.body)
57
+ assert.ok(json.meta)
58
+ assert.ok(json.error)
59
+ assert.ok(json.time)
60
+ })
61
+ })
@@ -0,0 +1,7 @@
1
+ import UIMessage from "./Message.js"
2
+ import InputMessage from "./InputMessage.js"
3
+ import OutputMessage from "./OutputMessage.js"
4
+
5
+ export { UIMessage, InputMessage, OutputMessage }
6
+
7
+ export default UIMessage
@@ -0,0 +1,50 @@
1
+ import Event from '@nan0web/event/oop'
2
+ import OutputMessage from './Message/OutputMessage.js'
3
+ import FormMessage from './Form/Message.js'
4
+
5
+ /**
6
+ * Abstract output adapter for UI implementations.
7
+ *
8
+ * @class OutputAdapter
9
+ * @extends Event
10
+ */
11
+ class OutputAdapter extends Event {
12
+ /**
13
+ * Renders a message to the user.
14
+ *
15
+ * @param {OutputMessage|FormMessage} message - Message to render.
16
+ * @throws {Error} If not overridden by a subclass.
17
+ */
18
+ render(message) {
19
+ throw new Error("render() must be implemented by subclass")
20
+ }
21
+
22
+ /**
23
+ * Shows progress of a long‑running operation.
24
+ *
25
+ * @param {number} progress - Progress value in range 0‑1.
26
+ * @param {Object} [metadata={}] - Additional metadata.
27
+ * @returns {void}
28
+ */
29
+ progress(progress, metadata = {}) {
30
+ this.render(OutputMessage.from({
31
+ content: [],
32
+ metadata: {
33
+ ...metadata,
34
+ progress,
35
+ elementType: "progress"
36
+ }
37
+ }))
38
+ }
39
+
40
+ /**
41
+ * Stops the output stream. Default implementation does nothing.
42
+ *
43
+ * @returns {void}
44
+ */
45
+ stop() {
46
+ // Default implementation – does nothing
47
+ }
48
+ }
49
+
50
+ export default OutputAdapter
@@ -0,0 +1,35 @@
1
+ import { describe, it } from "node:test"
2
+ import { strict as assert } from "node:assert"
3
+ import OutputAdapter from "./OutputAdapter.js"
4
+
5
+ describe("OutputAdapter", () => {
6
+ it("should create instance", () => {
7
+ const adapter = new OutputAdapter()
8
+ assert.ok(adapter instanceof OutputAdapter)
9
+ })
10
+
11
+ it("should throw error on render method", () => {
12
+ const adapter = new OutputAdapter()
13
+ assert.throws(() => adapter.render(), {
14
+ message: "render() must be implemented by subclass"
15
+ })
16
+ })
17
+
18
+ it("should call render on progress", () => {
19
+ const adapter = new OutputAdapter()
20
+ let rendered = false
21
+
22
+ // Override render for test
23
+ adapter.render = () => {
24
+ rendered = true
25
+ }
26
+
27
+ adapter.progress(0.5)
28
+ assert.ok(rendered)
29
+ })
30
+
31
+ it("should stop without error", () => {
32
+ const adapter = new OutputAdapter()
33
+ assert.doesNotThrow(() => adapter.stop())
34
+ })
35
+ })
@@ -0,0 +1,71 @@
1
+ import StreamEntry from "./StreamEntry.js"
2
+
3
+ /**
4
+ * Agnostic UI stream for processing progress.
5
+ *
6
+ * @class UIStream
7
+ */
8
+ export default class UIStream {
9
+ /**
10
+ * Creates an async generator that runs the supplied processor function.
11
+ *
12
+ * @param {AbortSignal} signal - Abort signal.
13
+ * @param {() => Promise<StreamEntry>} processorFn - Async function that returns a result.
14
+ * @returns {() => AsyncGenerator<StreamEntry>} Async generator function.
15
+ */
16
+ static createProcessor(signal, processorFn) {
17
+ return async function* () {
18
+ try {
19
+ const result = await processorFn()
20
+ yield result
21
+ } catch (/** @type {any} */ error) {
22
+ if (error.name === 'AbortError') {
23
+ yield StreamEntry.from({ done: true, cancelled: true })
24
+ } else {
25
+ yield StreamEntry.from({ error: error.message })
26
+ }
27
+ }
28
+ }
29
+ }
30
+
31
+ /**
32
+ * Runs a generator with progress callbacks and abort handling.
33
+ *
34
+ * @param {AbortSignal} signal - Abort signal.
35
+ * @param {Function} generator - Function returning an async iterator.
36
+ * @param {Function} [onProgress] - Called with (progress, item).
37
+ * @param {Function} [onError] - Called with (errorMessage, item).
38
+ * @param {Function} [onComplete] - Called with (item) when done.
39
+ * @returns {Promise<void>}
40
+ */
41
+ static async process(signal, generator, onProgress, onError, onComplete) {
42
+ const iter = generator()
43
+
44
+ try {
45
+ for await (const item of iter) {
46
+ if (signal.aborted) {
47
+ throw new DOMException('Aborted', 'AbortError')
48
+ }
49
+
50
+ if (item.progress !== undefined) {
51
+ onProgress?.(item.progress, item)
52
+ } else if (item.error) {
53
+ onError?.(item.error, item)
54
+ } else if (item.done) {
55
+ onComplete?.(item)
56
+ break
57
+ } else {
58
+ // Intermediate results
59
+ onProgress?.(null, item)
60
+ }
61
+ }
62
+ } catch (/** @type {any} */ error) {
63
+ if (error.name === 'AbortError') {
64
+ onComplete?.({ cancelled: true })
65
+ } else {
66
+ onError?.(error.message)
67
+ }
68
+ throw error
69
+ }
70
+ }
71
+ }
@@ -0,0 +1,78 @@
1
+ import { describe, it } from "node:test"
2
+ import { strict as assert } from "node:assert"
3
+ import UIStream from "./Stream.js"
4
+ import StreamEntry from "./StreamEntry.js"
5
+
6
+ describe("UIStream", () => {
7
+ it("should create processor generator", async () => {
8
+ const controller = new AbortController()
9
+ const processorFn = () => Promise.resolve("test result")
10
+ const generator = UIStream.createProcessor(controller.signal, processorFn)
11
+
12
+ assert.equal(typeof generator, "function")
13
+
14
+ for await (const item of generator()) {
15
+ assert.equal(item, "test result")
16
+ break
17
+ }
18
+ })
19
+
20
+ it("should handle aborted signal", async () => {
21
+ const controller = new AbortController()
22
+ controller.abort()
23
+
24
+ const processorFn = () => Promise.resolve(StreamEntry.from({ value: "Hello", done: true }))
25
+ const generator = UIStream.createProcessor(controller.signal, processorFn)
26
+
27
+ for await (const item of generator()) {
28
+ assert.deepEqual(item, StreamEntry.from({ done: true, value: "Hello" }))
29
+ break
30
+ }
31
+ })
32
+
33
+ it("should process stream with callbacks", async () => {
34
+ const controller = new AbortController()
35
+ let progressCalled = false
36
+ let completeCalled = false
37
+
38
+ const generatorFn = async function* () {
39
+ yield StreamEntry.from({ progress: 0.5 })
40
+ yield StreamEntry.from({ done: true })
41
+ }
42
+
43
+ const onProgress = () => { progressCalled = true }
44
+ const onComplete = () => { completeCalled = true }
45
+
46
+ await UIStream.process(
47
+ controller.signal,
48
+ generatorFn,
49
+ onProgress,
50
+ null,
51
+ onComplete
52
+ )
53
+
54
+ assert.ok(progressCalled)
55
+ assert.ok(completeCalled)
56
+ })
57
+
58
+ it("should handle errors in stream processing", async () => {
59
+ const controller = new AbortController()
60
+ let errorCalled = false
61
+
62
+ const generatorFn = async function* () {
63
+ yield { error: "test error" }
64
+ }
65
+
66
+ const onError = () => { errorCalled = true }
67
+
68
+ await UIStream.process(
69
+ controller.signal,
70
+ generatorFn,
71
+ null,
72
+ onError,
73
+ null
74
+ )
75
+
76
+ assert.ok(errorCalled)
77
+ })
78
+ })
@@ -0,0 +1,59 @@
1
+ /**
2
+ * Represents an entry in a stream with value, completion status, cancellation status, and error message.
3
+ */
4
+ export default class StreamEntry {
5
+ /**
6
+ * The value of the stream entry.
7
+ * @type {any}
8
+ */
9
+ value = undefined
10
+
11
+ /**
12
+ * Indicates if the stream entry is done (completed).
13
+ * @type {boolean}
14
+ */
15
+ done = false
16
+
17
+ /**
18
+ * Indicates if the stream entry has been cancelled.
19
+ * @type {boolean}
20
+ */
21
+ cancelled = false
22
+
23
+ /**
24
+ * Error message associated with the stream entry.
25
+ * @type {string}
26
+ */
27
+ error = ""
28
+
29
+ /**
30
+ * Creates a new StreamEntry instance.
31
+ * @param {Object} [input={}] - Input object to initialize the stream entry.
32
+ * @param {any} [input.value] - The value for the stream entry.
33
+ * @param {boolean} [input.done] - Whether the stream entry is completed.
34
+ * @param {boolean} [input.cancelled] - Whether the stream entry is cancelled.
35
+ * @param {string} [input.error] - Error message for the stream entry.
36
+ */
37
+ constructor(input = {}) {
38
+ const {
39
+ value = this.value,
40
+ done = this.done,
41
+ cancelled = this.cancelled,
42
+ error = this.error,
43
+ } = input
44
+ this.value = value
45
+ this.done = Boolean(done)
46
+ this.cancelled = Boolean(cancelled)
47
+ this.error = String(error)
48
+ }
49
+
50
+ /**
51
+ * Creates a StreamEntry instance from the given input.
52
+ * @param {any} input - The input to create a StreamEntry from.
53
+ * @returns {StreamEntry} A new or existing StreamEntry instance.
54
+ */
55
+ static from(input) {
56
+ if (input instanceof StreamEntry) return input
57
+ return new StreamEntry(input)
58
+ }
59
+ }
@@ -0,0 +1,13 @@
1
+ // export default App
2
+
3
+ export { default as InputAdapter } from "./InputAdapter.js"
4
+ export { default as OutputAdapter } from "./OutputAdapter.js"
5
+ export { default as UIStream } from "./Stream.js"
6
+
7
+ export { default as UIMessage } from "./Message/Message.js"
8
+ export { default as InputMessage } from "./Message/InputMessage.js"
9
+ export { default as OutputMessage } from "./Message/OutputMessage.js"
10
+
11
+ export { default as FormMessage } from "./Form/Message.js"
12
+ export { default as FormInput } from "./Form/Input.js"
13
+ export { default as UIForm } from "./Form/Form.js"
@@ -0,0 +1,38 @@
1
+ import { empty } from "@nan0web/types"
2
+
3
+ export const spaces = (options = {}) => {
4
+ const { cols = [], padding = 1, aligns = [] } = options
5
+ return (row) => (
6
+ row.map((str, i) => {
7
+ const pad = " ".repeat(cols[i] - str.length + padding)
8
+ return aligns[i] === "r" ? pad + str : str + pad
9
+ })
10
+ )
11
+ }
12
+
13
+ export const weight = (arr) => {
14
+ return (Fn = v => v) => {
15
+ const cols = []
16
+ arr.forEach(m => {
17
+ Fn(m).forEach((str, i) => {
18
+ if (undefined === cols[i]) cols[i] = 0
19
+ cols[i] = Math.max(str.length, cols[i])
20
+ })
21
+ })
22
+ return cols
23
+ }
24
+ }
25
+
26
+
27
+ export const table = (options = {}) => {
28
+ const {
29
+ Fn = v => v,
30
+ cols = [],
31
+ padding = 1,
32
+ aligns = []
33
+ } = options
34
+ return (arr) => {
35
+ const options = { cols: empty(cols) ? weight(arr)(Fn) : cols, padding, aligns }
36
+ return arr.map(row => spaces(options)(row))
37
+ }
38
+ }