@planet-matrix/mobius-model 0.4.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 +32 -0
- package/README.md +134 -21
- package/dist/index.js +45 -4
- package/dist/index.js.map +186 -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 -117
- package/src/basic/enhance.ts +10 -0
- package/src/basic/function.ts +81 -62
- package/src/basic/index.ts +2 -0
- package/src/basic/is.ts +152 -71
- package/src/basic/object.ts +82 -0
- 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 +92 -0
- package/src/encoding/base64.ts +107 -0
- package/src/encoding/index.ts +1 -0
- 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 +18 -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 +78 -0
- package/src/random/index.ts +1 -0
- 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/object.spec.ts +32 -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/encoding/base64.spec.ts +40 -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
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
|
|
2
|
+
const internalUUID_REGEXP = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* 判断输入值是否为合法的 UUID 字符串。
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* ```
|
|
9
|
+
* // Expect: true
|
|
10
|
+
* const example1 = isUuid("550e8400-e29b-41d4-a716-446655440000")
|
|
11
|
+
* // Expect: false
|
|
12
|
+
* const example2 = isUuid("not-a-uuid")
|
|
13
|
+
* ```
|
|
14
|
+
*/
|
|
15
|
+
export const isUuid = (input: string): boolean => {
|
|
16
|
+
return internalUUID_REGEXP.test(input)
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* 断言输入值是合法的 UUID 字符串。
|
|
21
|
+
*
|
|
22
|
+
* 当输入值不满足 UUID 格式时,该函数会抛出 TypeError,适合在标识边界层做快速失败。
|
|
23
|
+
*
|
|
24
|
+
* @example
|
|
25
|
+
* ```
|
|
26
|
+
* // Expect: no throw
|
|
27
|
+
* const example1 = assertUuid("550e8400-e29b-41d4-a716-446655440000")
|
|
28
|
+
* // Expect: throws TypeError
|
|
29
|
+
* const example2 = () => assertUuid("not-a-uuid")
|
|
30
|
+
* ```
|
|
31
|
+
*
|
|
32
|
+
* @throws {TypeError} when input is not a valid UUID
|
|
33
|
+
*/
|
|
34
|
+
export const assertUuid = (input: string): void => {
|
|
35
|
+
if (isUuid(input) === false) {
|
|
36
|
+
throw new TypeError(`Expected a valid UUID string, got: ${input}`)
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* 读取合法 UUID 字符串中的版本号。
|
|
42
|
+
*
|
|
43
|
+
* 该函数会先校验输入格式;如果输入不是合法 UUID,则会抛出 TypeError。
|
|
44
|
+
*
|
|
45
|
+
* @example
|
|
46
|
+
* ```
|
|
47
|
+
* // Expect: 4
|
|
48
|
+
* const example1 = getUuidVersion("550e8400-e29b-41d4-a716-446655440000")
|
|
49
|
+
* // Expect: 1
|
|
50
|
+
* const example2 = getUuidVersion("123e4567-e89b-12d3-a456-426614174000")
|
|
51
|
+
* ```
|
|
52
|
+
*
|
|
53
|
+
* @throws {TypeError} when input is not a valid UUID
|
|
54
|
+
*/
|
|
55
|
+
export const getUuidVersion = (input: string): number => {
|
|
56
|
+
assertUuid(input)
|
|
57
|
+
return Number.parseInt(input[14]!, 16)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* 生成 UUID v4 字符串。
|
|
62
|
+
*
|
|
63
|
+
* 当运行时提供 Web Crypto 能力时,优先使用安全随机源;否则回退到兼容性方案。
|
|
64
|
+
*
|
|
65
|
+
* @example
|
|
66
|
+
* ```
|
|
67
|
+
* const uuid = generateUuidV4()
|
|
68
|
+
* // Expect: true
|
|
69
|
+
* const example1 = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(uuid)
|
|
70
|
+
* ```
|
|
71
|
+
*/
|
|
72
|
+
export const generateUuidV4 = (): string => {
|
|
73
|
+
if (globalThis.crypto !== undefined) {
|
|
74
|
+
const { crypto } = globalThis
|
|
75
|
+
|
|
76
|
+
if (typeof crypto.randomUUID === "function") {
|
|
77
|
+
return crypto.randomUUID()
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (typeof crypto.getRandomValues === "function" && typeof Uint8Array === "function") {
|
|
81
|
+
const buffer = new Uint8Array(16)
|
|
82
|
+
crypto.getRandomValues(buffer)
|
|
83
|
+
|
|
84
|
+
buffer[6] = (buffer[6]! & 0x0F) | 0x40
|
|
85
|
+
buffer[8] = (buffer[8]! & 0x3F) | 0x80
|
|
86
|
+
|
|
87
|
+
const hex = [...buffer].map(value => value.toString(16).padStart(2, "0")).join("")
|
|
88
|
+
return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const fallbackUUID = (): string => {
|
|
93
|
+
const random = (value: number): string => {
|
|
94
|
+
return ((value ^ ((Math.random() * 16) >> (value / 4))) & 15).toString(16)
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return "10000000-1000-4000-8000-100000000000".replaceAll(/[018]/g, (char: string) => random(Number(char)))
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return fallbackUUID()
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* 通过 URL.createObjectURL 侧向取得一个 UUID v4 风格的字符串。
|
|
105
|
+
*
|
|
106
|
+
* 该函数依赖 Blob 与 URL API,适合浏览器等具备对象 URL 能力的环境。
|
|
107
|
+
* 如果只需要常规 UUID v4,优先使用 generateUuidV4。
|
|
108
|
+
*
|
|
109
|
+
* @example
|
|
110
|
+
* ```
|
|
111
|
+
* const uuid = generateUuidV4FromUrl()
|
|
112
|
+
* // Expect: true
|
|
113
|
+
* const example1 = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(uuid)
|
|
114
|
+
* // Expect: 36
|
|
115
|
+
* const example2 = uuid.length
|
|
116
|
+
* ```
|
|
117
|
+
*/
|
|
118
|
+
export const generateUuidV4FromUrl = (): string => {
|
|
119
|
+
const tempURL = URL.createObjectURL(new Blob())
|
|
120
|
+
const uuidInUrl = tempURL
|
|
121
|
+
URL.revokeObjectURL(tempURL)
|
|
122
|
+
const uuid = uuidInUrl.slice(uuidInUrl.lastIndexOf("/") + 1)
|
|
123
|
+
return uuid
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const internalUUIDv7RandomMask74 = (1n << 74n) - 1n
|
|
127
|
+
const internalUUIDv7RandomMask48 = (1n << 48n) - 1n
|
|
128
|
+
let internalUUIDv7LastTimestamp = -1n
|
|
129
|
+
let internalUUIDv7MonotonicValue = 0n
|
|
130
|
+
|
|
131
|
+
const internalUUIDv7FillRandom = (buffer: Uint8Array): void => {
|
|
132
|
+
if (globalThis.crypto !== undefined) {
|
|
133
|
+
const { crypto } = globalThis
|
|
134
|
+
if (typeof crypto.getRandomValues === "function") {
|
|
135
|
+
crypto.getRandomValues(buffer)
|
|
136
|
+
return
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
console.warn("generateUuidV7 requires crypto.getRandomValues")
|
|
141
|
+
for (let i = 0; i < 10; i = i + 1) {
|
|
142
|
+
buffer[i] = Math.floor(Math.random() * 256)
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const internalUUIDv7NextRandom74 = (timestamp: bigint): bigint => {
|
|
147
|
+
if (timestamp !== internalUUIDv7LastTimestamp) {
|
|
148
|
+
const randomValues = new Uint8Array(10)
|
|
149
|
+
internalUUIDv7FillRandom(randomValues)
|
|
150
|
+
|
|
151
|
+
const randomHex = Array.from(randomValues, value => value.toString(16).padStart(2, "0")).join("")
|
|
152
|
+
internalUUIDv7MonotonicValue = BigInt(`0x${randomHex}`) & internalUUIDv7RandomMask74
|
|
153
|
+
internalUUIDv7LastTimestamp = timestamp
|
|
154
|
+
return internalUUIDv7MonotonicValue
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
internalUUIDv7MonotonicValue = (internalUUIDv7MonotonicValue + 1n) & internalUUIDv7RandomMask74
|
|
158
|
+
return internalUUIDv7MonotonicValue
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* 生成 UUID v7 字符串。
|
|
163
|
+
*
|
|
164
|
+
* 结果以前导时间戳编码排序信息,适合需要近似按生成时间排序的标识场景。
|
|
165
|
+
*
|
|
166
|
+
* @example
|
|
167
|
+
* ```
|
|
168
|
+
* const uuid = generateUuidV7()
|
|
169
|
+
* // Expect: true
|
|
170
|
+
* const example1 = /^[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(uuid)
|
|
171
|
+
* ```
|
|
172
|
+
*/
|
|
173
|
+
export const generateUuidV7 = (): string => {
|
|
174
|
+
const timestamp = BigInt(Date.now())
|
|
175
|
+
const random74 = internalUUIDv7NextRandom74(timestamp)
|
|
176
|
+
const timeHex = timestamp.toString(16).padStart(12, "0")
|
|
177
|
+
|
|
178
|
+
const randAValue = Number((random74 >> 62n) & 0xFFFn)
|
|
179
|
+
const randBValue = Number((random74 >> 48n) & 0x3FFFn)
|
|
180
|
+
const trailingValue = random74 & internalUUIDv7RandomMask48
|
|
181
|
+
|
|
182
|
+
const randA = ((0x7 << 12) | randAValue) & 0xFFFF
|
|
183
|
+
const randB = ((0x2 << 14) | randBValue) & 0xFFFF
|
|
184
|
+
const trailingHex = trailingValue.toString(16).padStart(12, "0")
|
|
185
|
+
|
|
186
|
+
return `${timeHex.slice(0, 8)}-${timeHex.slice(8, 12)}-${randA.toString(16).padStart(4, "0")}-${randB.toString(16).padStart(4, "0")}-${trailingHex}`
|
|
187
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -1,3 +1,20 @@
|
|
|
1
1
|
export * as Basic from "./basic/index.ts"
|
|
2
|
-
export * as Type from "./type/index.ts"
|
|
2
|
+
export type * as Type from "./type/index.ts"
|
|
3
|
+
export * as Environment from "./environment/index.ts"
|
|
4
|
+
export * as Exception from "./exception/index.ts"
|
|
5
|
+
export * as Event from "./event/index.ts"
|
|
6
|
+
|
|
7
|
+
export * as Singleton from "./singleton/index.ts"
|
|
8
|
+
export * as Log from "./log/index.ts"
|
|
9
|
+
export * as Abort from "./abort/index.ts"
|
|
3
10
|
export * as Reactor from "./reactor/index.ts"
|
|
11
|
+
export * as Timer from "./timer/index.ts"
|
|
12
|
+
export * as Orchestration from "./orchestration/index.ts"
|
|
13
|
+
|
|
14
|
+
export * as Identifier from "./identifier/index.ts"
|
|
15
|
+
export * as Encoding from "./encoding/index.ts"
|
|
16
|
+
export * as Random from "./random/index.ts"
|
|
17
|
+
export * as Color from "./color/index.ts"
|
|
18
|
+
|
|
19
|
+
export * as Web from "./web/index.ts"
|
|
20
|
+
export * as CSS from "./css/index.ts"
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# Log
|
|
2
|
+
|
|
3
|
+
## Description
|
|
4
|
+
|
|
5
|
+
Log 模块用于提供结构化日志(structured logging)能力,主要承载日志记录、日志上下文组织、日志发送以及日志调度等稳定语义。
|
|
6
|
+
|
|
7
|
+
它面向的是“如何把日志当作一类可长期维护的数据模型”这一问题,而不是若干零散的输出函数。模块内的公共能力应帮助调用方稳定地表达日志类型、日志记录、日志发送端和日志调度过程,而不是把日志格式拼接、上下文透传和输出策略分散到业务代码里临时处理。
|
|
8
|
+
|
|
9
|
+
## For Understanding
|
|
10
|
+
|
|
11
|
+
理解 Log 模块时,首先应把它看作日志生命周期的建模层,而不是异常捕获入口的集合。它解决的是“如何描述一条日志、如何附加上下文、如何把日志发往不同输出端、如何协调这些输出过程”这一类问题,因此适合放在应用、服务、组件或基础设施边界之间,作为统一的日志语义层。
|
|
12
|
+
|
|
13
|
+
这个模块的边界重点在于“组织和消费日志”,而不是“发现错误来源”。像浏览器全局异常监听、Node.js 进程级未处理异常监听、框架级错误边界之类的能力,虽然最终经常会把结果送入 logger,但它们表达的是上游事件来源,不应直接并入 Log 模块本身。否则模块会同时承担异常捕获、日志建模和日志分发三类职责,边界会迅速变得模糊。
|
|
14
|
+
|
|
15
|
+
因此,适合进入本模块的能力,应当围绕以下几类稳定语义展开:日志级别与日志记录本身的表达、logger 的上下文继承与派生、日志发送端的抽象、以及对发送过程的统一调度。若新增能力主要是在监听某个宿主环境的异常入口,通常应优先放到更靠近环境或异常语义的模块中,再由上层决定是否接入 Log 模块。
|
|
16
|
+
|
|
17
|
+
## For Using
|
|
18
|
+
|
|
19
|
+
当你希望在系统内维持一致的日志结构,而不是在每次输出时手工拼接字符串、标签和上下文时,可以使用这个模块。它适合那些需要长期保留日志语义、希望在多个输出端之间复用同一份日志记录结构的场景。
|
|
20
|
+
|
|
21
|
+
从使用角度看,这个模块大致可以理解为几类能力的组合。第一类是日志记录本身,用来表达日志类型、消息和附加数据,让日志作为结构化对象被传递与消费。第二类是 logger 入口,用来在局部范围中派生带上下文的日志能力,使业务层不必重复附加相同的标签、作用域或元信息。第三类是发送与调度能力,用来把日志记录发往控制台或其它输出端,并把输出时机与策略统一收敛到日志系统内部。
|
|
22
|
+
|
|
23
|
+
更合适的接入方式,是在业务边界尽早把日志上下文组织清楚,再将记录交给 logger、emitter 或 scheduler 处理,而不是在底层输出瞬间才临时补齐信息。这样做能够让日志在演进过程中保持稳定结构,也便于后续替换输出端、追加上下文或统一调整日志策略。
|
|
24
|
+
|
|
25
|
+
## For Contributing
|
|
26
|
+
|
|
27
|
+
贡献 Log 模块时,首要任务不是补一个“能打印出来”的便捷函数,而是确认新增能力是否确实表达了稳定的日志语义。这个模块应长期围绕日志记录、上下文组织、输出抽象和调度协作展开;任何新增公共能力,都应说明它解决的是哪一段日志生命周期问题,以及它为什么值得成为长期承诺的一部分。
|
|
28
|
+
|
|
29
|
+
在扩展时,应持续守住以下边界:不要把宿主环境特有的异常捕获入口直接塞进来;不要把只对某个特定日志平台或某个业务日志格式成立的细节公开承诺出去;不要让 logger 退化为简单字符串打印器。对外语义应优先是“日志模型”和“日志协作方式”,而不是某一次实现中的输出技巧。
|
|
30
|
+
|
|
31
|
+
### JSDoc 注释格式要求
|
|
32
|
+
|
|
33
|
+
- 每个公开导出的目标(类型、函数、变量、类等)都应包含 JSDoc 注释,让人在不跳转实现的情况下就能理解用途。
|
|
34
|
+
- JSDoc 注释第一行应为清晰且简洁的描述,该描述优先使用中文(英文也可以)。
|
|
35
|
+
- 如果描述后还有其他内容,应在描述后加一个空行。
|
|
36
|
+
- 如果有示例,应使用 `@example` 标签,后接三重反引号代码块(不带语言标识)。
|
|
37
|
+
- 如果有示例,应包含多个场景,展示不同用法,尤其要覆盖常见组合方式或边界输入。
|
|
38
|
+
- 如果有示例,应使用注释格式说明每个场景:`// Expect: <result>`。
|
|
39
|
+
- 如果有示例,应将结果赋值给 `example1`、`example2` 之类的变量,以保持示例易读。
|
|
40
|
+
- 如果有示例,`// Expect: <result>` 应该位于 `example1`、`example2` 之前,以保持示例的逻辑清晰。
|
|
41
|
+
- 如果有示例,应优先使用确定性示例;避免断言精确的随机输出。
|
|
42
|
+
- 如果函数返回结构化字符串,应展示其预期格式特征。
|
|
43
|
+
- 如果有参考资料,应将 `@see` 放在 `@example` 代码块之后,并用一个空行分隔。
|
|
44
|
+
|
|
45
|
+
### 实现规范要求
|
|
46
|
+
|
|
47
|
+
- 不同程序元素之间使用一个空行分隔,保持结构清楚。这里的程序元素,通常指函数、类型、常量,以及直接服务于它们的辅助元素。
|
|
48
|
+
- 某程序元素独占的辅助元素与该程序元素本身视为一个整体,不要在它们之间添加空行。
|
|
49
|
+
- 程序元素的辅助元素应该放置在该程序元素的上方,以保持阅读时的逻辑顺序。
|
|
50
|
+
- 若辅助元素被多个程序元素共享,则应将其视为独立的程序元素,放在这些程序元素中第一个相关目标的上方,并与后续程序元素之间保留一个空行。
|
|
51
|
+
- 辅助元素也应该像其它程序元素一样,保持清晰的命名和适当的注释,以便在需要阅读实现细节时能够快速理解它们的作用和使用方式。
|
|
52
|
+
- 辅助元素的命名必须以前缀 `internal` 开头(或 `Internal`,大小写不敏感)。
|
|
53
|
+
- 辅助元素永远不要公开导出。
|
|
54
|
+
- 被模块内多个不同文件中的程序元素共享的辅助元素,应该放在一个单独的文件中,例如 `./src/log/internal.ts`。
|
|
55
|
+
- 模块内可以包含子模块。只有当某个子目录表达一个稳定、可单独理解、且可能被父模块重导出的子问题域时,才应将其视为子模块。
|
|
56
|
+
- 子模块包含多个文件时,应该为其单独创建子文件夹,并为其创建单独的 Barrel 文件;父模块的 Barrel 文件再重导出子模块的 Barrel 文件。
|
|
57
|
+
- 子模块不需要有自己的 `README.md`。
|
|
58
|
+
- 子模块可以有自己的 `internal.ts` 文件,多个子模块共享的辅助元素应该放在父模块的 `internal.ts` 文件中,单个子模块共享的辅助元素应该放在该子模块的 `internal.ts` 文件中。
|
|
59
|
+
- 对模块依赖关系的要求(通常是不循环依赖或不反向依赖)与对 DRY 的要求可能产生冲突。此时,若复用的代码数量不大,可以适当牺牲 DRY,复制粘贴并保留必要的注释说明;若复用的代码数量较大,则可以将其抽象到新的文件或子模块中,如 `common.ts`,并在需要的地方导入使用。
|
|
60
|
+
|
|
61
|
+
- 与日志相关的实现应优先围绕记录模型、上下文继承、发送端抽象与调度协作展开,避免把宿主异常来源或业务平台专用格式混入模块边界。
|
|
62
|
+
|
|
63
|
+
### 导出策略要求
|
|
64
|
+
|
|
65
|
+
- 保持内部辅助项和内部符号为私有,不要让外部接入依赖临时性的内部结构。
|
|
66
|
+
- 每个模块都应有一个用于重导出所有公共 API 的 Barrel 文件。
|
|
67
|
+
- Barrel 文件应命名为 `index.ts`,放在模块目录根部,并且所有公共 API 都应从该文件导出。
|
|
68
|
+
- 新增公共能力时,应优先检查它是否表达稳定、清楚且值得长期维护的日志语义,而不是某段实现细节的便捷暴露;仅在确认需要长期对外承诺时再加入 Barrel 导出。
|
|
69
|
+
|
|
70
|
+
### 测试要求
|
|
71
|
+
|
|
72
|
+
- 若程序元素是函数,则只为该函数编写一个测试,如果该函数需要测试多个用例,应放在同一个测试中。
|
|
73
|
+
- 若程序元素是类,则至少要为该类的每一个方法编写一个测试,如果该方法需要测试多个用例,应放在同一个测试中。
|
|
74
|
+
- 若程序元素是类,除了为该类的每一个方法编写至少一个测试之外,还可以为该类编写任意多个测试,以覆盖该类的不同使用场景或边界情况。
|
|
75
|
+
- 若编写测试时需要用到辅助元素(Mock 或 Spy 等),可以在测试文件中直接定义这些辅助元素。若辅助元素较为简单,则可以直接放在每一个测试内部,优先保证每个测试的独立性,而不是追求极致 DRY;若辅助元素较为复杂或需要在多个测试中复用,则可以放在测试文件顶部,供该测试文件中的所有测试使用。
|
|
76
|
+
- 测试顺序应与源文件中被测试目标的原始顺序保持一致。
|
|
77
|
+
- 若该模块不需要测试,必须在说明文件中明确说明该模块不需要测试,并说明理由。一般来说,只有在该模块没有可执行的公共函数、只承载类型层表达,或其语义已被上层模块的测试完整覆盖且重复测试几乎不再带来额外价值时,才适合这样处理。
|
|
78
|
+
- 模块的单元测试文件目录是 `./tests/unit/log`,若模块包含子模块,则子模块的单元测试文件目录为 `./tests/unit/log/<sub-module-name>`。
|
|
79
|
+
- 对 logger、emitter、scheduler 这一类能力,应优先覆盖上下文继承、发送顺序、失败传播、默认行为与资源清理等关键场景。
|
package/src/log/index.ts
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import type { LogRecord } from "./log-record.ts"
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Describe options consumed by log emitters.
|
|
5
|
+
*/
|
|
6
|
+
export interface LogEmitterOptions extends Omit<LogRecord, "type"> {
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Define the base emitter contract for dispatching formatted logs.
|
|
10
|
+
*/
|
|
11
|
+
export abstract class LogEmitter {
|
|
12
|
+
readonly tags: string[]
|
|
13
|
+
readonly messages: unknown[]
|
|
14
|
+
|
|
15
|
+
readonly formattedMessages: string[]
|
|
16
|
+
|
|
17
|
+
constructor(options: LogEmitterOptions) {
|
|
18
|
+
this.tags = options.tags ?? []
|
|
19
|
+
this.messages = options.messages ?? []
|
|
20
|
+
|
|
21
|
+
this.formattedMessages = []
|
|
22
|
+
this.formattedMessages.push(this.tags.map(tag => `[${tag}]`).join(""))
|
|
23
|
+
this.formattedMessages.push(this.messages.map(message => String(message)).join(" "))
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
abstract emit(): void
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Emit `log` records via `console.log`.
|
|
31
|
+
*/
|
|
32
|
+
export class ConsoleLogLogEmitter extends LogEmitter {
|
|
33
|
+
emit(): void {
|
|
34
|
+
console.log(this.formattedMessages.join(" "))
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Emit `info` records via `console.info`.
|
|
40
|
+
*/
|
|
41
|
+
export class ConsoleInfoLogEmitter extends LogEmitter {
|
|
42
|
+
emit(): void {
|
|
43
|
+
console.info(this.formattedMessages.join(" "))
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Emit `warn` records via `console.warn`.
|
|
49
|
+
*/
|
|
50
|
+
export class ConsoleWarnLogEmitter extends LogEmitter {
|
|
51
|
+
emit(): void {
|
|
52
|
+
console.warn(this.formattedMessages.join(" "))
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Emit `error` records via `console.error`.
|
|
58
|
+
*/
|
|
59
|
+
export class ConsoleErrorLogEmitter extends LogEmitter {
|
|
60
|
+
emit(): void {
|
|
61
|
+
console.error(this.formattedMessages.join(" "))
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Emit `debug` records via `console.debug`.
|
|
67
|
+
*/
|
|
68
|
+
export class ConsoleDebugLogEmitter extends LogEmitter {
|
|
69
|
+
emit(): void {
|
|
70
|
+
console.debug(this.formattedMessages.join(" "))
|
|
71
|
+
}
|
|
72
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import type { LogRecord } from "./log-record.ts"
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Describe a schedulable log emission task.
|
|
5
|
+
*/
|
|
6
|
+
export interface LogTask {
|
|
7
|
+
record: LogRecord
|
|
8
|
+
emit: () => void
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Represent supported scheduling strategies.
|
|
12
|
+
*/
|
|
13
|
+
export type LogSchedulerStrategy = "immediate"
|
|
14
|
+
/**
|
|
15
|
+
* Describe options used to configure a log scheduler.
|
|
16
|
+
*/
|
|
17
|
+
export interface LogSchedulerOptions {
|
|
18
|
+
strategy: LogSchedulerStrategy
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Queue and dispatch log tasks based on scheduling strategy.
|
|
22
|
+
*/
|
|
23
|
+
export class LogScheduler {
|
|
24
|
+
protected strategy: LogSchedulerStrategy
|
|
25
|
+
|
|
26
|
+
protected taskQueue: LogTask[][]
|
|
27
|
+
|
|
28
|
+
constructor(options: LogSchedulerOptions) {
|
|
29
|
+
this.strategy = options.strategy
|
|
30
|
+
|
|
31
|
+
this.taskQueue = []
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Check if there are any tasks in the queue.
|
|
36
|
+
*/
|
|
37
|
+
hasTasks(): boolean {
|
|
38
|
+
return this.taskQueue.length !== 0
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Enqueue tasks and run immediately when strategy requires.
|
|
43
|
+
*/
|
|
44
|
+
enqueue(task: LogTask[]): void {
|
|
45
|
+
this.taskQueue.push(task)
|
|
46
|
+
if (this.strategy === "immediate") {
|
|
47
|
+
this.run()
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Flush queued tasks in insertion order.
|
|
53
|
+
*/
|
|
54
|
+
run(): void {
|
|
55
|
+
const tasksToRun = [...this.taskQueue].flat()
|
|
56
|
+
this.taskQueue = []
|
|
57
|
+
for (const task of tasksToRun) {
|
|
58
|
+
task.emit()
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Get the singleton global `LogScheduler` instance.
|
|
65
|
+
*/
|
|
66
|
+
export const getGlobalLogScheduler: () => LogScheduler = (
|
|
67
|
+
() => {
|
|
68
|
+
let instance: LogScheduler | undefined = undefined
|
|
69
|
+
return (): LogScheduler => {
|
|
70
|
+
instance = instance ?? new LogScheduler({ strategy: "immediate" })
|
|
71
|
+
return instance
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
)()
|