@planet-matrix/mobius-model 0.5.0 → 0.6.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 (175) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/README.md +123 -36
  3. package/dist/index.js +45 -4
  4. package/dist/index.js.map +183 -11
  5. package/oxlint.config.ts +6 -0
  6. package/package.json +16 -10
  7. package/src/abort/README.md +92 -0
  8. package/src/abort/abort-manager.ts +278 -0
  9. package/src/abort/abort-signal-listener-manager.ts +81 -0
  10. package/src/abort/index.ts +2 -0
  11. package/src/basic/README.md +69 -118
  12. package/src/basic/function.ts +81 -62
  13. package/src/basic/is.ts +152 -71
  14. package/src/basic/promise.ts +29 -8
  15. package/src/basic/string.ts +2 -33
  16. package/src/color/README.md +105 -0
  17. package/src/color/index.ts +3 -0
  18. package/src/color/internal.ts +42 -0
  19. package/src/color/rgb/analyze.ts +236 -0
  20. package/src/color/rgb/construct.ts +130 -0
  21. package/src/color/rgb/convert.ts +227 -0
  22. package/src/color/rgb/derive.ts +303 -0
  23. package/src/color/rgb/index.ts +6 -0
  24. package/src/color/rgb/internal.ts +208 -0
  25. package/src/color/rgb/parse.ts +302 -0
  26. package/src/color/rgb/serialize.ts +144 -0
  27. package/src/color/types.ts +57 -0
  28. package/src/color/xyz/analyze.ts +80 -0
  29. package/src/color/xyz/construct.ts +19 -0
  30. package/src/color/xyz/convert.ts +71 -0
  31. package/src/color/xyz/index.ts +3 -0
  32. package/src/color/xyz/internal.ts +23 -0
  33. package/src/css/README.md +93 -0
  34. package/src/css/class.ts +559 -0
  35. package/src/css/index.ts +1 -0
  36. package/src/encoding/README.md +66 -79
  37. package/src/encoding/base64.ts +13 -4
  38. package/src/environment/README.md +97 -0
  39. package/src/environment/basic.ts +26 -0
  40. package/src/environment/device.ts +311 -0
  41. package/src/environment/feature.ts +285 -0
  42. package/src/environment/geo.ts +337 -0
  43. package/src/environment/index.ts +7 -0
  44. package/src/environment/runtime.ts +400 -0
  45. package/src/environment/snapshot.ts +60 -0
  46. package/src/environment/variable.ts +239 -0
  47. package/src/event/README.md +90 -0
  48. package/src/event/class-event-proxy.ts +228 -0
  49. package/src/event/common.ts +19 -0
  50. package/src/event/event-manager.ts +203 -0
  51. package/src/event/index.ts +4 -0
  52. package/src/event/instance-event-proxy.ts +186 -0
  53. package/src/event/internal.ts +24 -0
  54. package/src/exception/README.md +96 -0
  55. package/src/exception/browser.ts +219 -0
  56. package/src/exception/index.ts +4 -0
  57. package/src/exception/nodejs.ts +169 -0
  58. package/src/exception/normalize.ts +106 -0
  59. package/src/exception/types.ts +99 -0
  60. package/src/identifier/README.md +92 -0
  61. package/src/identifier/id.ts +119 -0
  62. package/src/identifier/index.ts +2 -0
  63. package/src/identifier/uuid.ts +187 -0
  64. package/src/index.ts +16 -1
  65. package/src/log/README.md +79 -0
  66. package/src/log/index.ts +5 -0
  67. package/src/log/log-emitter.ts +72 -0
  68. package/src/log/log-record.ts +10 -0
  69. package/src/log/log-scheduler.ts +74 -0
  70. package/src/log/log-type.ts +8 -0
  71. package/src/log/logger.ts +543 -0
  72. package/src/orchestration/README.md +89 -0
  73. package/src/orchestration/coordination/barrier.ts +214 -0
  74. package/src/orchestration/coordination/count-down-latch.ts +215 -0
  75. package/src/orchestration/coordination/errors.ts +98 -0
  76. package/src/orchestration/coordination/index.ts +16 -0
  77. package/src/orchestration/coordination/internal/wait-constraints.ts +95 -0
  78. package/src/orchestration/coordination/internal/wait-queue.ts +109 -0
  79. package/src/orchestration/coordination/keyed-lock.ts +168 -0
  80. package/src/orchestration/coordination/mutex.ts +257 -0
  81. package/src/orchestration/coordination/permit.ts +127 -0
  82. package/src/orchestration/coordination/read-write-lock.ts +444 -0
  83. package/src/orchestration/coordination/semaphore.ts +280 -0
  84. package/src/orchestration/index.ts +1 -0
  85. package/src/random/README.md +55 -86
  86. package/src/random/index.ts +1 -1
  87. package/src/random/string.ts +35 -0
  88. package/src/reactor/README.md +4 -0
  89. package/src/reactor/reactor-core/primitive.ts +9 -9
  90. package/src/reactor/reactor-core/reactive-system.ts +5 -5
  91. package/src/singleton/README.md +79 -0
  92. package/src/singleton/factory.ts +55 -0
  93. package/src/singleton/index.ts +2 -0
  94. package/src/singleton/manager.ts +204 -0
  95. package/src/storage/README.md +107 -0
  96. package/src/storage/index.ts +1 -0
  97. package/src/storage/table.ts +449 -0
  98. package/src/timer/README.md +86 -0
  99. package/src/timer/expiration/expiration-manager.ts +594 -0
  100. package/src/timer/expiration/index.ts +3 -0
  101. package/src/timer/expiration/min-heap.ts +208 -0
  102. package/src/timer/expiration/remaining-manager.ts +241 -0
  103. package/src/timer/index.ts +1 -0
  104. package/src/type/README.md +54 -307
  105. package/src/type/class.ts +2 -2
  106. package/src/type/index.ts +14 -14
  107. package/src/type/is.ts +265 -2
  108. package/src/type/object.ts +37 -0
  109. package/src/type/string.ts +7 -2
  110. package/src/type/tuple.ts +6 -6
  111. package/src/type/union.ts +16 -0
  112. package/src/web/README.md +77 -0
  113. package/src/web/capture.ts +35 -0
  114. package/src/web/clipboard.ts +97 -0
  115. package/src/web/dom.ts +117 -0
  116. package/src/web/download.ts +16 -0
  117. package/src/web/event.ts +46 -0
  118. package/src/web/index.ts +10 -0
  119. package/src/web/local-storage.ts +113 -0
  120. package/src/web/location.ts +28 -0
  121. package/src/web/permission.ts +172 -0
  122. package/src/web/script-loader.ts +432 -0
  123. package/tests/unit/abort/abort-manager.spec.ts +225 -0
  124. package/tests/unit/abort/abort-signal-listener-manager.spec.ts +62 -0
  125. package/tests/unit/basic/array.spec.ts +1 -1
  126. package/tests/unit/basic/stream.spec.ts +1 -1
  127. package/tests/unit/basic/string.spec.ts +0 -9
  128. package/tests/unit/color/rgb/analyze.spec.ts +110 -0
  129. package/tests/unit/color/rgb/construct.spec.ts +56 -0
  130. package/tests/unit/color/rgb/convert.spec.ts +60 -0
  131. package/tests/unit/color/rgb/derive.spec.ts +103 -0
  132. package/tests/unit/color/rgb/parse.spec.ts +66 -0
  133. package/tests/unit/color/rgb/serialize.spec.ts +46 -0
  134. package/tests/unit/color/xyz/analyze.spec.ts +33 -0
  135. package/tests/unit/color/xyz/construct.spec.ts +10 -0
  136. package/tests/unit/color/xyz/convert.spec.ts +18 -0
  137. package/tests/unit/css/class.spec.ts +157 -0
  138. package/tests/unit/environment/basic.spec.ts +20 -0
  139. package/tests/unit/environment/device.spec.ts +146 -0
  140. package/tests/unit/environment/feature.spec.ts +388 -0
  141. package/tests/unit/environment/geo.spec.ts +111 -0
  142. package/tests/unit/environment/runtime.spec.ts +364 -0
  143. package/tests/unit/environment/snapshot.spec.ts +4 -0
  144. package/tests/unit/environment/variable.spec.ts +190 -0
  145. package/tests/unit/event/class-event-proxy.spec.ts +225 -0
  146. package/tests/unit/event/event-manager.spec.ts +246 -0
  147. package/tests/unit/event/instance-event-proxy.spec.ts +187 -0
  148. package/tests/unit/exception/browser.spec.ts +213 -0
  149. package/tests/unit/exception/nodejs.spec.ts +144 -0
  150. package/tests/unit/exception/normalize.spec.ts +57 -0
  151. package/tests/unit/identifier/id.spec.ts +71 -0
  152. package/tests/unit/identifier/uuid.spec.ts +85 -0
  153. package/tests/unit/log/log-emitter.spec.ts +33 -0
  154. package/tests/unit/log/log-scheduler.spec.ts +40 -0
  155. package/tests/unit/log/log-type.spec.ts +7 -0
  156. package/tests/unit/log/logger.spec.ts +222 -0
  157. package/tests/unit/orchestration/coordination/barrier.spec.ts +96 -0
  158. package/tests/unit/orchestration/coordination/count-down-latch.spec.ts +63 -0
  159. package/tests/unit/orchestration/coordination/errors.spec.ts +29 -0
  160. package/tests/unit/orchestration/coordination/keyed-lock.spec.ts +109 -0
  161. package/tests/unit/orchestration/coordination/mutex.spec.ts +132 -0
  162. package/tests/unit/orchestration/coordination/permit.spec.ts +43 -0
  163. package/tests/unit/orchestration/coordination/read-write-lock.spec.ts +154 -0
  164. package/tests/unit/orchestration/coordination/semaphore.spec.ts +135 -0
  165. package/tests/unit/random/string.spec.ts +11 -0
  166. package/tests/unit/reactor/alien-signals-effect.spec.ts +11 -10
  167. package/tests/unit/reactor/preact-signal.spec.ts +1 -2
  168. package/tests/unit/singleton/singleton.spec.ts +49 -0
  169. package/tests/unit/storage/table.spec.ts +620 -0
  170. package/tests/unit/timer/expiration/expiration-manager.spec.ts +464 -0
  171. package/tests/unit/timer/expiration/min-heap.spec.ts +71 -0
  172. package/tests/unit/timer/expiration/remaining-manager.spec.ts +234 -0
  173. package/.oxlintrc.json +0 -5
  174. package/src/random/uuid.ts +0 -103
  175. package/tests/unit/random/uuid.spec.ts +0 -37
@@ -0,0 +1,213 @@
1
+ import { afterEach, expect, test, vi } from "vitest"
2
+
3
+ import {
4
+ observeBrowserExceptions,
5
+ onBrowserGlobalError,
6
+ onBrowserUnhandledRejection,
7
+ onBrowserWindowOnError,
8
+ } from "#Source/exception/index.ts"
9
+
10
+ class WindowMock extends EventTarget {
11
+ onerror: OnErrorEventHandler | null = null
12
+ document = {}
13
+ }
14
+
15
+ const createBrowserErrorEvent = (options: {
16
+ message: string
17
+ filename?: string | undefined
18
+ lineno?: number | undefined
19
+ colno?: number | undefined
20
+ error?: unknown
21
+ }): Event => {
22
+ const event = new Event("error")
23
+ Object.defineProperties(event, {
24
+ message: { value: options.message, configurable: true },
25
+ filename: { value: options.filename, configurable: true },
26
+ lineno: { value: options.lineno, configurable: true },
27
+ colno: { value: options.colno, configurable: true },
28
+ error: { value: options.error, configurable: true },
29
+ })
30
+ return event
31
+ }
32
+
33
+ const createBrowserUnhandledRejectionEvent = (options: {
34
+ reason: unknown
35
+ promise: Promise<unknown>
36
+ }): Event => {
37
+ const event = new Event("unhandledrejection")
38
+ Object.defineProperties(event, {
39
+ reason: { value: options.reason, configurable: true },
40
+ promise: { value: options.promise, configurable: true },
41
+ })
42
+ return event
43
+ }
44
+
45
+ afterEach(() => {
46
+ vi.unstubAllGlobals()
47
+ vi.restoreAllMocks()
48
+ })
49
+
50
+ test("onBrowserGlobalError listens and cleans up window error events", () => {
51
+ const browserWindow = new WindowMock()
52
+ const listener = vi.fn()
53
+ vi.stubGlobal("window", browserWindow)
54
+ vi.stubGlobal("self", browserWindow)
55
+ vi.stubGlobal("document", browserWindow.document)
56
+
57
+ const cleanup = onBrowserGlobalError(listener)
58
+ const event = createBrowserErrorEvent({
59
+ message: "boom",
60
+ filename: "app.ts",
61
+ lineno: 8,
62
+ colno: 9,
63
+ error: new Error("boom"),
64
+ })
65
+ browserWindow.dispatchEvent(event)
66
+
67
+ expect(listener).toHaveBeenCalledTimes(1)
68
+ expect(listener).toHaveBeenCalledWith(expect.objectContaining({
69
+ runtime: "browser",
70
+ source: "browser.global-error",
71
+ message: "boom",
72
+ filename: "app.ts",
73
+ lineno: 8,
74
+ colno: 9,
75
+ }))
76
+
77
+ cleanup()
78
+ browserWindow.dispatchEvent(event)
79
+ expect(listener).toHaveBeenCalledTimes(1)
80
+ })
81
+
82
+ test("onBrowserWindowOnError composes listeners and restores window.onerror after the last cleanup", () => {
83
+ const browserWindow = new WindowMock()
84
+ const previousOnError = vi.fn(() => false)
85
+ const listener1 = vi.fn()
86
+ const listener2 = vi.fn()
87
+ // oxlint-disable-next-line prefer-add-event-listener
88
+ browserWindow.onerror = previousOnError
89
+ vi.stubGlobal("window", browserWindow)
90
+ vi.stubGlobal("self", browserWindow)
91
+ vi.stubGlobal("document", browserWindow.document)
92
+
93
+ const cleanup1 = onBrowserWindowOnError(listener1)
94
+ const cleanup2 = onBrowserWindowOnError(listener2)
95
+ browserWindow.onerror?.("boom", "app.ts", 3, 5, new Error("boom"))
96
+
97
+ expect(listener1).toHaveBeenCalledTimes(1)
98
+ expect(listener1).toHaveBeenCalledWith(expect.objectContaining({
99
+ runtime: "browser",
100
+ source: "browser.window-onerror",
101
+ filename: "app.ts",
102
+ lineno: 3,
103
+ colno: 5,
104
+ }))
105
+ expect(listener2).toHaveBeenCalledTimes(1)
106
+ expect(listener2).toHaveBeenCalledWith(expect.objectContaining({
107
+ runtime: "browser",
108
+ source: "browser.window-onerror",
109
+ filename: "app.ts",
110
+ lineno: 3,
111
+ colno: 5,
112
+ }))
113
+ expect(previousOnError).toHaveBeenCalledTimes(1)
114
+
115
+ cleanup1()
116
+ browserWindow.onerror?.("again", "next.ts", 7, 11, new Error("again"))
117
+
118
+ expect(listener1).toHaveBeenCalledTimes(1)
119
+ expect(listener2).toHaveBeenCalledTimes(2)
120
+ expect(previousOnError).toHaveBeenCalledTimes(2)
121
+
122
+ cleanup2()
123
+ expect(browserWindow.onerror).toBe(previousOnError)
124
+ })
125
+
126
+ test("onBrowserUnhandledRejection listens and cleans up promise rejection events", () => {
127
+ const browserWindow = new WindowMock()
128
+ const listener = vi.fn()
129
+ vi.stubGlobal("window", browserWindow)
130
+ vi.stubGlobal("self", browserWindow)
131
+ vi.stubGlobal("document", browserWindow.document)
132
+
133
+ const cleanup = onBrowserUnhandledRejection(listener)
134
+ const promise = Promise.resolve("ok")
135
+ const rejectionEvent = createBrowserUnhandledRejectionEvent({
136
+ promise,
137
+ reason: new Error("reject"),
138
+ })
139
+ browserWindow.dispatchEvent(rejectionEvent)
140
+
141
+ expect(listener).toHaveBeenCalledWith(expect.objectContaining({
142
+ runtime: "browser",
143
+ source: "browser.unhandled-rejection",
144
+ message: "reject",
145
+ }))
146
+
147
+ cleanup()
148
+ browserWindow.dispatchEvent(rejectionEvent)
149
+ expect(listener).toHaveBeenCalledTimes(1)
150
+ })
151
+
152
+ test("observeBrowserExceptions listens to all browser sources by default and supports explicit opt-out", () => {
153
+ const browserWindow = new WindowMock()
154
+ const listener = vi.fn()
155
+ vi.stubGlobal("window", browserWindow)
156
+ vi.stubGlobal("self", browserWindow)
157
+ vi.stubGlobal("document", browserWindow.document)
158
+
159
+ const cleanup1 = observeBrowserExceptions(listener, {
160
+ captureTimestamp: () => 777,
161
+ })
162
+ const promise = Promise.resolve("ok")
163
+
164
+ browserWindow.dispatchEvent(createBrowserErrorEvent({
165
+ message: "again",
166
+ error: new Error("again"),
167
+ }))
168
+ browserWindow.onerror?.("window boom", "page.ts", 1, 2, new Error("window boom"))
169
+ browserWindow.dispatchEvent(createBrowserUnhandledRejectionEvent({
170
+ promise,
171
+ reason: "reason-text",
172
+ }))
173
+
174
+ expect(listener).toHaveBeenCalledTimes(3)
175
+ expect(listener).toHaveBeenCalledWith(expect.objectContaining({
176
+ source: "browser.global-error",
177
+ timestamp: 777,
178
+ }))
179
+ expect(listener).toHaveBeenCalledWith(expect.objectContaining({
180
+ source: "browser.window-onerror",
181
+ timestamp: 777,
182
+ filename: "page.ts",
183
+ }))
184
+ expect(listener).toHaveBeenCalledWith(expect.objectContaining({
185
+ source: "browser.unhandled-rejection",
186
+ timestamp: 777,
187
+ message: "reason-text",
188
+ }))
189
+
190
+ cleanup1()
191
+
192
+ listener.mockClear()
193
+
194
+ const cleanup2 = observeBrowserExceptions(listener, {
195
+ includeWindowOnError: false,
196
+ })
197
+ browserWindow.dispatchEvent(createBrowserErrorEvent({
198
+ message: "still-on",
199
+ error: new Error("still-on"),
200
+ }))
201
+ browserWindow.onerror?.("ignored", "page.ts", 1, 2, new Error("ignored"))
202
+ browserWindow.dispatchEvent(createBrowserUnhandledRejectionEvent({
203
+ promise,
204
+ reason: "still-on",
205
+ }))
206
+
207
+ expect(listener).toHaveBeenCalledTimes(2)
208
+ expect(listener).not.toHaveBeenCalledWith(expect.objectContaining({
209
+ source: "browser.window-onerror",
210
+ }))
211
+
212
+ cleanup2()
213
+ })
@@ -0,0 +1,144 @@
1
+ import { afterEach, expect, test, vi } from "vitest"
2
+
3
+ import {
4
+ observeNodejsExceptions,
5
+ onNodejsUncaughtException,
6
+ onNodejsUncaughtExceptionMonitor,
7
+ onNodejsUnhandledRejection,
8
+ } from "#Source/exception/index.ts"
9
+
10
+ class ProcessMock {
11
+ protected listeners = new Map<string, Set<(...args: unknown[]) => void>>()
12
+ versions = { node: "20.0.0" }
13
+
14
+ on(eventName: string, listener: (...args: unknown[]) => void): this {
15
+ const eventListeners = this.listeners.get(eventName) ?? new Set()
16
+ eventListeners.add(listener)
17
+ this.listeners.set(eventName, eventListeners)
18
+ return this
19
+ }
20
+
21
+ off(eventName: string, listener: (...args: unknown[]) => void): this {
22
+ this.listeners.get(eventName)?.delete(listener)
23
+ return this
24
+ }
25
+
26
+ emit(eventName: string, ...args: unknown[]): void {
27
+ for (const listener of this.listeners.get(eventName) ?? []) {
28
+ listener(...args)
29
+ }
30
+ }
31
+ }
32
+
33
+ afterEach(() => {
34
+ vi.unstubAllGlobals()
35
+ vi.restoreAllMocks()
36
+ })
37
+
38
+ test("onNodejsUncaughtExceptionMonitor listens and cleans up monitor events", () => {
39
+ const mockedProcess = new ProcessMock()
40
+ const listener = vi.fn()
41
+ vi.stubGlobal("process", mockedProcess)
42
+
43
+ const cleanup = onNodejsUncaughtExceptionMonitor(listener)
44
+ mockedProcess.emit("uncaughtExceptionMonitor", new Error("boom"), "uncaughtException")
45
+
46
+ expect(listener).toHaveBeenCalledWith(expect.objectContaining({
47
+ runtime: "nodejs",
48
+ source: "nodejs.uncaught-exception-monitor",
49
+ message: "boom",
50
+ origin: "uncaughtException",
51
+ }))
52
+
53
+ cleanup()
54
+ mockedProcess.emit("uncaughtExceptionMonitor", new Error("boom"), "uncaughtException")
55
+ expect(listener).toHaveBeenCalledTimes(1)
56
+ })
57
+
58
+ test("onNodejsUncaughtException normalizes records and cleans up listeners", () => {
59
+ const mockedProcess = new ProcessMock()
60
+ const listener = vi.fn()
61
+ vi.stubGlobal("process", mockedProcess)
62
+
63
+ const cleanup = onNodejsUncaughtException(listener)
64
+
65
+ mockedProcess.emit("uncaughtException", new Error("broken"), "uncaughtException")
66
+
67
+ expect(listener).toHaveBeenCalledWith(expect.objectContaining({
68
+ source: "nodejs.uncaught-exception",
69
+ message: "broken",
70
+ origin: "uncaughtException",
71
+ }))
72
+
73
+ cleanup()
74
+ mockedProcess.emit("uncaughtException", new Error("ignored"), "uncaughtException")
75
+ expect(listener).toHaveBeenCalledTimes(1)
76
+ })
77
+
78
+ test("onNodejsUnhandledRejection normalizes records and cleans up listeners", () => {
79
+ const mockedProcess = new ProcessMock()
80
+ const listener = vi.fn()
81
+ vi.stubGlobal("process", mockedProcess)
82
+
83
+ const cleanup = onNodejsUnhandledRejection(listener)
84
+ const promise = Promise.resolve("ok")
85
+
86
+ mockedProcess.emit("unhandledRejection", "reason-text", promise)
87
+
88
+ expect(listener).toHaveBeenCalledWith(expect.objectContaining({
89
+ source: "nodejs.unhandled-rejection",
90
+ message: "reason-text",
91
+ promise,
92
+ }))
93
+
94
+ cleanup()
95
+ mockedProcess.emit("unhandledRejection", "ignored", promise)
96
+ expect(listener).toHaveBeenCalledTimes(1)
97
+ })
98
+
99
+ test("observeNodejsExceptions listens to all Node.js sources by default and supports explicit opt-out", () => {
100
+ const mockedProcess = new ProcessMock()
101
+ const listener = vi.fn()
102
+ vi.stubGlobal("process", mockedProcess)
103
+
104
+ const cleanup1 = observeNodejsExceptions(listener, {
105
+ captureTimestamp: () => 456,
106
+ })
107
+
108
+ const promise = Promise.resolve("ok")
109
+ mockedProcess.emit("uncaughtExceptionMonitor", new Error("watch"), "uncaughtException")
110
+ mockedProcess.emit("uncaughtException", new Error("boom"), "uncaughtException")
111
+ mockedProcess.emit("unhandledRejection", new Error("reject"), promise)
112
+
113
+ expect(listener).toHaveBeenCalledTimes(3)
114
+ expect(listener).toHaveBeenNthCalledWith(1, expect.objectContaining({
115
+ source: "nodejs.uncaught-exception-monitor",
116
+ timestamp: 456,
117
+ }))
118
+ expect(listener).toHaveBeenNthCalledWith(2, expect.objectContaining({
119
+ source: "nodejs.uncaught-exception",
120
+ timestamp: 456,
121
+ }))
122
+ expect(listener).toHaveBeenNthCalledWith(3, expect.objectContaining({
123
+ source: "nodejs.unhandled-rejection",
124
+ timestamp: 456,
125
+ }))
126
+
127
+ cleanup1()
128
+
129
+ listener.mockClear()
130
+
131
+ const cleanup2 = observeNodejsExceptions(listener, {
132
+ includeUncaughtExceptionMonitor: false,
133
+ })
134
+ mockedProcess.emit("uncaughtExceptionMonitor", new Error("ignored"), "uncaughtException")
135
+ mockedProcess.emit("uncaughtException", new Error("kept"), "uncaughtException")
136
+ mockedProcess.emit("unhandledRejection", "kept", promise)
137
+
138
+ expect(listener).toHaveBeenCalledTimes(2)
139
+ expect(listener).not.toHaveBeenCalledWith(expect.objectContaining({
140
+ source: "nodejs.uncaught-exception-monitor",
141
+ }))
142
+
143
+ cleanup2()
144
+ })
@@ -0,0 +1,57 @@
1
+ import { expect, test, vi } from "vitest"
2
+
3
+ import {
4
+ normalizeBrowserExceptionRecord,
5
+ normalizeExceptionRecord,
6
+ normalizeNodejsExceptionRecord,
7
+ } from "#Source/exception/index.ts"
8
+
9
+ test("normalizeExceptionRecord infers message and timestamp", () => {
10
+ vi.spyOn(Date, "now").mockReturnValue(123)
11
+
12
+ const example1 = normalizeExceptionRecord({
13
+ runtime: "unknown",
14
+ source: "custom",
15
+ exception: new Error("boom"),
16
+ })
17
+ const example2 = normalizeExceptionRecord({
18
+ runtime: "unknown",
19
+ source: "custom",
20
+ exception: "text-error",
21
+ })
22
+
23
+ expect(example1.message).toBe("boom")
24
+ expect(example1.timestamp).toBe(123)
25
+ expect(example2.message).toBe("text-error")
26
+ })
27
+
28
+ test("normalizeBrowserExceptionRecord preserves browser-specific fields", () => {
29
+ const example1 = normalizeBrowserExceptionRecord({
30
+ source: "browser.global-error",
31
+ exception: new Error("boom"),
32
+ filename: "app.ts",
33
+ lineno: 8,
34
+ colno: 13,
35
+ timestamp: 10,
36
+ })
37
+
38
+ expect(example1.runtime).toBe("browser")
39
+ expect(example1.filename).toBe("app.ts")
40
+ expect(example1.lineno).toBe(8)
41
+ expect(example1.colno).toBe(13)
42
+ })
43
+
44
+ test("normalizeNodejsExceptionRecord preserves nodejs-specific fields", () => {
45
+ const promise = Promise.resolve("ok")
46
+ const example1 = normalizeNodejsExceptionRecord({
47
+ source: "nodejs.unhandled-rejection",
48
+ exception: new Error("boom"),
49
+ origin: "unhandledRejection",
50
+ promise,
51
+ timestamp: 11,
52
+ })
53
+
54
+ expect(example1.runtime).toBe("nodejs")
55
+ expect(example1.origin).toBe("unhandledRejection")
56
+ expect(example1.promise).toBe(promise)
57
+ })
@@ -0,0 +1,71 @@
1
+ import { expect, test } from "vitest"
2
+
3
+ import {
4
+ assertId,
5
+ generateId,
6
+ idToString,
7
+ isEqualId,
8
+ isId,
9
+ isSameId,
10
+ } from "#Source/identifier/index.ts"
11
+
12
+ test("generateId is seed-stable and no-seed calls are uncached", () => {
13
+ const first = generateId("user:42")
14
+ const second = generateId("user:42")
15
+ const third = generateId("user:43")
16
+ const withoutSeed1 = generateId()
17
+ const withoutSeed2 = generateId()
18
+ const undefinedSeed1 = generateId(undefined)
19
+ const undefinedSeed2 = generateId(undefined)
20
+
21
+ expect(first).toBe(second)
22
+ expect(first).not.toBe(third)
23
+ expect(withoutSeed1).not.toBe(withoutSeed2)
24
+ expect(undefinedSeed1).not.toBe(undefinedSeed2)
25
+ expect(withoutSeed1).not.toBe(undefinedSeed1)
26
+ })
27
+
28
+ test("isId validates generated Id values", () => {
29
+ const id = generateId(1)
30
+
31
+ expect(isId(id)).toBe(true)
32
+ expect(isId(null)).toBe(false)
33
+ expect(isId(undefined)).toBe(false)
34
+ expect(isId("plain-string")).toBe(false)
35
+ expect(isId({})).toBe(false)
36
+ })
37
+
38
+ test("assertId throws for non-Id inputs", () => {
39
+ const id = generateId(Symbol("seed"))
40
+
41
+ expect(() => assertId(id)).not.toThrow()
42
+ expect(() => assertId("plain-string")).toThrow(TypeError)
43
+ expect(() => assertId(1)).toThrow(TypeError)
44
+ })
45
+
46
+ test("isSameId compares by instance identity", () => {
47
+ const id = generateId("same")
48
+ const sameReference = id
49
+ const another = generateId("another")
50
+
51
+ expect(isSameId(id, sameReference)).toBe(true)
52
+ expect(isSameId(id, another)).toBe(false)
53
+ })
54
+
55
+ test("isEqualId compares internal ID value", () => {
56
+ const first = generateId("equal-seed")
57
+ const second = generateId("equal-seed")
58
+ const third = generateId("different-seed")
59
+
60
+ expect(isEqualId(first, second)).toBe(true)
61
+ expect(isEqualId(first, third)).toBe(false)
62
+ })
63
+
64
+ test("idToString serializes Id to readable label", () => {
65
+ const id = generateId("to-string")
66
+ const asString = idToString(id)
67
+
68
+ expect(asString.startsWith("Symbol-ID-")).toBe(true)
69
+ // oxlint-disable-next-line typescript/no-base-to-string
70
+ expect(asString).toBe(String(id))
71
+ })
@@ -0,0 +1,85 @@
1
+ import { afterEach, expect, test, vi } from "vitest"
2
+
3
+ import { assertUuid, generateUuidV4, generateUuidV4FromUrl, generateUuidV7, getUuidVersion, isUuid } from "#Source/identifier/index.ts"
4
+
5
+ afterEach(() => {
6
+ vi.restoreAllMocks()
7
+ })
8
+
9
+ test("isUuid validates UUID input", () => {
10
+ expect(isUuid("550e8400-e29b-41d4-a716-446655440000")).toBe(true)
11
+ expect(isUuid("550E8400-E29B-41D4-A716-446655440000")).toBe(true)
12
+ expect(isUuid("123e4567-e89b-12d3-a456-426614174000")).toBe(true)
13
+
14
+ expect(isUuid("not-a-uuid")).toBe(false)
15
+ expect(isUuid("550e8400e29b41d4a716446655440000")).toBe(false)
16
+ expect(isUuid("550e8400-e29b-91d4-a716-446655440000")).toBe(false)
17
+ expect(isUuid("550e8400-e29b-41d4-c716-446655440000")).toBe(false)
18
+ })
19
+
20
+ test("assertUuid throws on malformed input", () => {
21
+ expect(() => assertUuid("550e8400-e29b-41d4-a716-446655440000")).not.toThrow()
22
+ expect(() => assertUuid("not-a-uuid")).toThrow(TypeError)
23
+ expect(() => assertUuid("550e8400e29b41d4a716446655440000")).toThrow(TypeError)
24
+ })
25
+
26
+ test("getUuidVersion returns UUID version and throws on malformed input", () => {
27
+ expect(getUuidVersion("550e8400-e29b-41d4-a716-446655440000")).toBe(4)
28
+ expect(getUuidVersion("123e4567-e89b-12d3-a456-426614174000")).toBe(1)
29
+
30
+ expect(() => getUuidVersion("not-a-uuid")).toThrow(TypeError)
31
+ })
32
+
33
+ test("generateUuid returns RFC 4122 version-4 UUID format", () => {
34
+ const UUID_V4_REGEXP = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i
35
+ const value1 = generateUuidV4()
36
+ const value2 = generateUuidV4()
37
+
38
+ expect(value1).toMatch(UUID_V4_REGEXP)
39
+ expect(value2).toMatch(UUID_V4_REGEXP)
40
+ expect(value1).not.toBe(value2)
41
+ })
42
+
43
+ test("generateUuidV4FromUrl extracts UUID from object URL and revokes URL", () => {
44
+ const mockedUuid = "1e34c01e-ab7c-4e60-b6f3-a6101e8c0d2d"
45
+ const mockedObjectUrl = `blob:https://www.cigaret.world/${mockedUuid}`
46
+ const createObjectUrlSpy = vi.spyOn(URL, "createObjectURL").mockReturnValue(mockedObjectUrl)
47
+ const revokeObjectUrlSpy = vi.spyOn(URL, "revokeObjectURL").mockImplementation(() => {
48
+ return undefined
49
+ })
50
+
51
+ const value = generateUuidV4FromUrl()
52
+
53
+ expect(value).toBe(mockedUuid)
54
+ expect(isUuid(value)).toBe(true)
55
+ expect(createObjectUrlSpy).toHaveBeenCalledTimes(1)
56
+ expect(revokeObjectUrlSpy).toHaveBeenCalledTimes(1)
57
+ expect(revokeObjectUrlSpy).toHaveBeenCalledWith(mockedObjectUrl)
58
+ })
59
+
60
+ test("generateUuidV7 returns UUIDv7 format and is monotonic within the same millisecond", () => {
61
+ const UUID_V7_REGEXP = /^[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i
62
+
63
+ vi.spyOn(Date, "now").mockReturnValue(1_700_000_000_000)
64
+ vi.spyOn(crypto, "getRandomValues").mockImplementation(buffer => {
65
+ if (buffer === null) {
66
+ return buffer
67
+ }
68
+
69
+ // oxlint-disable-next-line typescript/no-unsafe-type-assertion
70
+ const target = buffer as Uint8Array
71
+ target.fill(0)
72
+ return buffer
73
+ })
74
+
75
+ const value1 = generateUuidV7()
76
+ const value2 = generateUuidV7()
77
+ const value3 = generateUuidV7()
78
+
79
+ expect(value1).toMatch(UUID_V7_REGEXP)
80
+ expect(value2).toMatch(UUID_V7_REGEXP)
81
+ expect(value3).toMatch(UUID_V7_REGEXP)
82
+
83
+ expect(value1 < value2).toBe(true)
84
+ expect(value2 < value3).toBe(true)
85
+ })
@@ -0,0 +1,33 @@
1
+ import { afterEach, expect, test, vi } from "vitest"
2
+
3
+ import {
4
+ ConsoleDebugLogEmitter,
5
+ ConsoleErrorLogEmitter,
6
+ ConsoleInfoLogEmitter,
7
+ ConsoleLogLogEmitter,
8
+ ConsoleWarnLogEmitter,
9
+ } from "#Source/log/index.ts"
10
+
11
+ afterEach(() => {
12
+ vi.restoreAllMocks()
13
+ })
14
+
15
+ test("console log emitters format tags and messages before output", () => {
16
+ const logSpy = vi.spyOn(console, "log").mockImplementation(() => undefined)
17
+ const infoSpy = vi.spyOn(console, "info").mockImplementation(() => undefined)
18
+ const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => undefined)
19
+ const errorSpy = vi.spyOn(console, "error").mockImplementation(() => undefined)
20
+ const debugSpy = vi.spyOn(console, "debug").mockImplementation(() => undefined)
21
+
22
+ new ConsoleLogLogEmitter({ tags: ["scope", "unit"], messages: ["hello", 1] }).emit()
23
+ new ConsoleInfoLogEmitter({ tags: ["scope"], messages: ["info"] }).emit()
24
+ new ConsoleWarnLogEmitter({ tags: ["scope"], messages: ["warn"] }).emit()
25
+ new ConsoleErrorLogEmitter({ tags: ["scope"], messages: ["error"] }).emit()
26
+ new ConsoleDebugLogEmitter({ tags: ["scope"], messages: ["debug"] }).emit()
27
+
28
+ expect(logSpy).toHaveBeenCalledWith("[scope][unit] hello 1")
29
+ expect(infoSpy).toHaveBeenCalledWith("[scope] info")
30
+ expect(warnSpy).toHaveBeenCalledWith("[scope] warn")
31
+ expect(errorSpy).toHaveBeenCalledWith("[scope] error")
32
+ expect(debugSpy).toHaveBeenCalledWith("[scope] debug")
33
+ })
@@ -0,0 +1,40 @@
1
+ import { expect, test, vi } from "vitest"
2
+
3
+ import { LogScheduler, getGlobalLogScheduler } from "#Source/log/index.ts"
4
+
5
+ test("LogScheduler enqueue dispatches immediately in immediate strategy", () => {
6
+ const scheduler = new LogScheduler({ strategy: "immediate" })
7
+ const emit1 = vi.fn()
8
+ const emit2 = vi.fn()
9
+
10
+ scheduler.enqueue([
11
+ { record: { type: "log", tags: ["a"], messages: ["one"] }, emit: emit1 },
12
+ { record: { type: "info", tags: ["a"], messages: ["two"] }, emit: emit2 },
13
+ ])
14
+
15
+ expect(emit1).toHaveBeenCalledTimes(1)
16
+ expect(emit2).toHaveBeenCalledTimes(1)
17
+ })
18
+
19
+ test("LogScheduler run flushes queued tasks", () => {
20
+ const scheduler = new LogScheduler({ strategy: "immediate" })
21
+ const emit1 = vi.fn()
22
+ const emit2 = vi.fn()
23
+
24
+ scheduler.enqueue([
25
+ { record: { type: "log", tags: ["a"], messages: ["one"] }, emit: emit1 },
26
+ { record: { type: "info", tags: ["a"], messages: ["two"] }, emit: emit2 },
27
+ ])
28
+ scheduler.run()
29
+
30
+ expect(emit1).toHaveBeenCalledTimes(1)
31
+ expect(emit2).toHaveBeenCalledTimes(1)
32
+ expect(scheduler.hasTasks()).toEqual(false)
33
+ })
34
+
35
+ test("getGlobalLogScheduler returns singleton instance", () => {
36
+ const scheduler1 = getGlobalLogScheduler()
37
+ const scheduler2 = getGlobalLogScheduler()
38
+
39
+ expect(scheduler1).toBe(scheduler2)
40
+ })
@@ -0,0 +1,7 @@
1
+ import { expect, test } from "vitest"
2
+
3
+ import { LOG_TYPES } from "#Source/log/index.ts"
4
+
5
+ test("LOG_TYPES exposes supported log levels", () => {
6
+ expect(LOG_TYPES).toEqual(["log", "info", "warn", "error", "debug"])
7
+ })