@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.
- package/CHANGELOG.md +24 -0
- package/README.md +123 -36
- package/dist/index.js +45 -4
- package/dist/index.js.map +183 -11
- package/oxlint.config.ts +6 -0
- package/package.json +16 -10
- package/src/abort/README.md +92 -0
- package/src/abort/abort-manager.ts +278 -0
- package/src/abort/abort-signal-listener-manager.ts +81 -0
- package/src/abort/index.ts +2 -0
- package/src/basic/README.md +69 -118
- package/src/basic/function.ts +81 -62
- package/src/basic/is.ts +152 -71
- package/src/basic/promise.ts +29 -8
- package/src/basic/string.ts +2 -33
- package/src/color/README.md +105 -0
- package/src/color/index.ts +3 -0
- package/src/color/internal.ts +42 -0
- package/src/color/rgb/analyze.ts +236 -0
- package/src/color/rgb/construct.ts +130 -0
- package/src/color/rgb/convert.ts +227 -0
- package/src/color/rgb/derive.ts +303 -0
- package/src/color/rgb/index.ts +6 -0
- package/src/color/rgb/internal.ts +208 -0
- package/src/color/rgb/parse.ts +302 -0
- package/src/color/rgb/serialize.ts +144 -0
- package/src/color/types.ts +57 -0
- package/src/color/xyz/analyze.ts +80 -0
- package/src/color/xyz/construct.ts +19 -0
- package/src/color/xyz/convert.ts +71 -0
- package/src/color/xyz/index.ts +3 -0
- package/src/color/xyz/internal.ts +23 -0
- package/src/css/README.md +93 -0
- package/src/css/class.ts +559 -0
- package/src/css/index.ts +1 -0
- package/src/encoding/README.md +66 -79
- package/src/encoding/base64.ts +13 -4
- package/src/environment/README.md +97 -0
- package/src/environment/basic.ts +26 -0
- package/src/environment/device.ts +311 -0
- package/src/environment/feature.ts +285 -0
- package/src/environment/geo.ts +337 -0
- package/src/environment/index.ts +7 -0
- package/src/environment/runtime.ts +400 -0
- package/src/environment/snapshot.ts +60 -0
- package/src/environment/variable.ts +239 -0
- package/src/event/README.md +90 -0
- package/src/event/class-event-proxy.ts +228 -0
- package/src/event/common.ts +19 -0
- package/src/event/event-manager.ts +203 -0
- package/src/event/index.ts +4 -0
- package/src/event/instance-event-proxy.ts +186 -0
- package/src/event/internal.ts +24 -0
- package/src/exception/README.md +96 -0
- package/src/exception/browser.ts +219 -0
- package/src/exception/index.ts +4 -0
- package/src/exception/nodejs.ts +169 -0
- package/src/exception/normalize.ts +106 -0
- package/src/exception/types.ts +99 -0
- package/src/identifier/README.md +92 -0
- package/src/identifier/id.ts +119 -0
- package/src/identifier/index.ts +2 -0
- package/src/identifier/uuid.ts +187 -0
- package/src/index.ts +16 -1
- package/src/log/README.md +79 -0
- package/src/log/index.ts +5 -0
- package/src/log/log-emitter.ts +72 -0
- package/src/log/log-record.ts +10 -0
- package/src/log/log-scheduler.ts +74 -0
- package/src/log/log-type.ts +8 -0
- package/src/log/logger.ts +543 -0
- package/src/orchestration/README.md +89 -0
- package/src/orchestration/coordination/barrier.ts +214 -0
- package/src/orchestration/coordination/count-down-latch.ts +215 -0
- package/src/orchestration/coordination/errors.ts +98 -0
- package/src/orchestration/coordination/index.ts +16 -0
- package/src/orchestration/coordination/internal/wait-constraints.ts +95 -0
- package/src/orchestration/coordination/internal/wait-queue.ts +109 -0
- package/src/orchestration/coordination/keyed-lock.ts +168 -0
- package/src/orchestration/coordination/mutex.ts +257 -0
- package/src/orchestration/coordination/permit.ts +127 -0
- package/src/orchestration/coordination/read-write-lock.ts +444 -0
- package/src/orchestration/coordination/semaphore.ts +280 -0
- package/src/orchestration/index.ts +1 -0
- package/src/random/README.md +55 -86
- package/src/random/index.ts +1 -1
- package/src/random/string.ts +35 -0
- package/src/reactor/README.md +4 -0
- package/src/reactor/reactor-core/primitive.ts +9 -9
- package/src/reactor/reactor-core/reactive-system.ts +5 -5
- package/src/singleton/README.md +79 -0
- package/src/singleton/factory.ts +55 -0
- package/src/singleton/index.ts +2 -0
- package/src/singleton/manager.ts +204 -0
- package/src/storage/README.md +107 -0
- package/src/storage/index.ts +1 -0
- package/src/storage/table.ts +449 -0
- package/src/timer/README.md +86 -0
- package/src/timer/expiration/expiration-manager.ts +594 -0
- package/src/timer/expiration/index.ts +3 -0
- package/src/timer/expiration/min-heap.ts +208 -0
- package/src/timer/expiration/remaining-manager.ts +241 -0
- package/src/timer/index.ts +1 -0
- package/src/type/README.md +54 -307
- package/src/type/class.ts +2 -2
- package/src/type/index.ts +14 -14
- package/src/type/is.ts +265 -2
- package/src/type/object.ts +37 -0
- package/src/type/string.ts +7 -2
- package/src/type/tuple.ts +6 -6
- package/src/type/union.ts +16 -0
- package/src/web/README.md +77 -0
- package/src/web/capture.ts +35 -0
- package/src/web/clipboard.ts +97 -0
- package/src/web/dom.ts +117 -0
- package/src/web/download.ts +16 -0
- package/src/web/event.ts +46 -0
- package/src/web/index.ts +10 -0
- package/src/web/local-storage.ts +113 -0
- package/src/web/location.ts +28 -0
- package/src/web/permission.ts +172 -0
- package/src/web/script-loader.ts +432 -0
- package/tests/unit/abort/abort-manager.spec.ts +225 -0
- package/tests/unit/abort/abort-signal-listener-manager.spec.ts +62 -0
- package/tests/unit/basic/array.spec.ts +1 -1
- package/tests/unit/basic/stream.spec.ts +1 -1
- package/tests/unit/basic/string.spec.ts +0 -9
- package/tests/unit/color/rgb/analyze.spec.ts +110 -0
- package/tests/unit/color/rgb/construct.spec.ts +56 -0
- package/tests/unit/color/rgb/convert.spec.ts +60 -0
- package/tests/unit/color/rgb/derive.spec.ts +103 -0
- package/tests/unit/color/rgb/parse.spec.ts +66 -0
- package/tests/unit/color/rgb/serialize.spec.ts +46 -0
- package/tests/unit/color/xyz/analyze.spec.ts +33 -0
- package/tests/unit/color/xyz/construct.spec.ts +10 -0
- package/tests/unit/color/xyz/convert.spec.ts +18 -0
- package/tests/unit/css/class.spec.ts +157 -0
- package/tests/unit/environment/basic.spec.ts +20 -0
- package/tests/unit/environment/device.spec.ts +146 -0
- package/tests/unit/environment/feature.spec.ts +388 -0
- package/tests/unit/environment/geo.spec.ts +111 -0
- package/tests/unit/environment/runtime.spec.ts +364 -0
- package/tests/unit/environment/snapshot.spec.ts +4 -0
- package/tests/unit/environment/variable.spec.ts +190 -0
- package/tests/unit/event/class-event-proxy.spec.ts +225 -0
- package/tests/unit/event/event-manager.spec.ts +246 -0
- package/tests/unit/event/instance-event-proxy.spec.ts +187 -0
- package/tests/unit/exception/browser.spec.ts +213 -0
- package/tests/unit/exception/nodejs.spec.ts +144 -0
- package/tests/unit/exception/normalize.spec.ts +57 -0
- package/tests/unit/identifier/id.spec.ts +71 -0
- package/tests/unit/identifier/uuid.spec.ts +85 -0
- package/tests/unit/log/log-emitter.spec.ts +33 -0
- package/tests/unit/log/log-scheduler.spec.ts +40 -0
- package/tests/unit/log/log-type.spec.ts +7 -0
- package/tests/unit/log/logger.spec.ts +222 -0
- package/tests/unit/orchestration/coordination/barrier.spec.ts +96 -0
- package/tests/unit/orchestration/coordination/count-down-latch.spec.ts +63 -0
- package/tests/unit/orchestration/coordination/errors.spec.ts +29 -0
- package/tests/unit/orchestration/coordination/keyed-lock.spec.ts +109 -0
- package/tests/unit/orchestration/coordination/mutex.spec.ts +132 -0
- package/tests/unit/orchestration/coordination/permit.spec.ts +43 -0
- package/tests/unit/orchestration/coordination/read-write-lock.spec.ts +154 -0
- package/tests/unit/orchestration/coordination/semaphore.spec.ts +135 -0
- package/tests/unit/random/string.spec.ts +11 -0
- package/tests/unit/reactor/alien-signals-effect.spec.ts +11 -10
- package/tests/unit/reactor/preact-signal.spec.ts +1 -2
- package/tests/unit/singleton/singleton.spec.ts +49 -0
- package/tests/unit/storage/table.spec.ts +620 -0
- package/tests/unit/timer/expiration/expiration-manager.spec.ts +464 -0
- package/tests/unit/timer/expiration/min-heap.spec.ts +71 -0
- package/tests/unit/timer/expiration/remaining-manager.spec.ts +234 -0
- package/.oxlintrc.json +0 -5
- package/src/random/uuid.ts +0 -103
- 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
|
+
})
|