@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,169 @@
1
+ import { useNodejs } from "#Source/environment/index.ts"
2
+
3
+ import { normalizeNodejsExceptionRecord } from "./normalize.ts"
4
+ import type {
5
+ ExceptionListenerCleanup,
6
+ NodejsExceptionRecord,
7
+ ObserveExceptionsOptions,
8
+ } from "./types.ts"
9
+
10
+ /**
11
+ * 表示 Nodejs 侧异常监听回调。
12
+ */
13
+ export type NodejsExceptionListener = (record: NodejsExceptionRecord) => void
14
+
15
+ /**
16
+ * 监听 Node.js 的 `uncaughtExceptionMonitor` 事件。
17
+ */
18
+ export const onNodejsUncaughtExceptionMonitor = (
19
+ listener: NodejsExceptionListener,
20
+ options?: ObserveExceptionsOptions | undefined,
21
+ ): ExceptionListenerCleanup => {
22
+ return useNodejs((context) => {
23
+ const currentProcess = context.globalThis.process
24
+ const internalListener = (exception: unknown, origin: unknown): void => {
25
+ listener(normalizeNodejsExceptionRecord({
26
+ source: "nodejs.uncaught-exception-monitor",
27
+ exception,
28
+ timestamp: options?.captureTimestamp?.() ?? Date.now(),
29
+ originalEvent: { exception, origin },
30
+ origin: typeof origin === "string" ? origin : undefined,
31
+ }))
32
+ }
33
+
34
+ currentProcess.on("uncaughtExceptionMonitor", internalListener)
35
+
36
+ return () => {
37
+ if (typeof currentProcess.off === "function") {
38
+ currentProcess.off("uncaughtExceptionMonitor", internalListener)
39
+ return
40
+ }
41
+
42
+ currentProcess.removeListener?.("uncaughtExceptionMonitor", internalListener)
43
+ }
44
+ }, () => {
45
+ return (): void => {
46
+ return undefined
47
+ }
48
+ })
49
+ }
50
+
51
+ /**
52
+ * 监听 Node.js 的 `uncaughtException` 事件。
53
+ */
54
+ export const onNodejsUncaughtException = (
55
+ listener: NodejsExceptionListener,
56
+ options?: ObserveExceptionsOptions | undefined,
57
+ ): ExceptionListenerCleanup => {
58
+ return useNodejs((context) => {
59
+ const currentProcess = context.globalThis.process
60
+ const internalListener = (exception: unknown, origin: unknown): void => {
61
+ listener(normalizeNodejsExceptionRecord({
62
+ source: "nodejs.uncaught-exception",
63
+ exception,
64
+ timestamp: options?.captureTimestamp?.() ?? Date.now(),
65
+ originalEvent: { exception, origin },
66
+ origin: typeof origin === "string" ? origin : undefined,
67
+ }))
68
+ }
69
+
70
+ currentProcess.on("uncaughtException", internalListener)
71
+
72
+ return () => {
73
+ if (typeof currentProcess.off === "function") {
74
+ currentProcess.off("uncaughtException", internalListener)
75
+ return
76
+ }
77
+
78
+ currentProcess.removeListener?.("uncaughtException", internalListener)
79
+ }
80
+ }, () => {
81
+ return (): void => {
82
+ return undefined
83
+ }
84
+ })
85
+ }
86
+
87
+ /**
88
+ * 监听 Node.js 的 `unhandledRejection` 事件。
89
+ */
90
+ export const onNodejsUnhandledRejection = (
91
+ listener: NodejsExceptionListener,
92
+ options?: ObserveExceptionsOptions | undefined,
93
+ ): ExceptionListenerCleanup => {
94
+ return useNodejs((context) => {
95
+ const currentProcess = context.globalThis.process
96
+ const internalListener = (reason: unknown, promise: unknown): void => {
97
+ listener(normalizeNodejsExceptionRecord({
98
+ source: "nodejs.unhandled-rejection",
99
+ exception: reason,
100
+ timestamp: options?.captureTimestamp?.() ?? Date.now(),
101
+ originalEvent: { reason, promise },
102
+ promise: promise instanceof Promise ? promise : undefined,
103
+ }))
104
+ }
105
+
106
+ currentProcess.on("unhandledRejection", internalListener)
107
+
108
+ return () => {
109
+ if (typeof currentProcess.off === "function") {
110
+ currentProcess.off("unhandledRejection", internalListener)
111
+ return
112
+ }
113
+
114
+ currentProcess.removeListener?.("unhandledRejection", internalListener)
115
+ }
116
+ }, () => {
117
+ return (): void => {
118
+ return undefined
119
+ }
120
+ })
121
+ }
122
+
123
+ /**
124
+ * 表示批量监听 Nodejs 异常时可选的配置。
125
+ *
126
+ * 未显式传入时,三个 Node.js 入口默认都会启用;仅当对应选项为 `false` 时才跳过该入口。
127
+ */
128
+ export interface ObserveNodejsExceptionsOptions extends ObserveExceptionsOptions {
129
+ includeUncaughtExceptionMonitor?: boolean | undefined
130
+ includeUncaughtException?: boolean | undefined
131
+ includeUnhandledRejection?: boolean | undefined
132
+ }
133
+
134
+ /**
135
+ * 批量监听 Nodejs 侧全局异常入口。
136
+ *
137
+ * 默认会同时监听 `uncaughtExceptionMonitor`、`uncaughtException` 和 `unhandledRejection`。
138
+ * 如需关闭某个入口,请将对应的 `include*` 选项显式设为 `false`。
139
+ */
140
+ export const observeNodejsExceptions = (
141
+ listener: NodejsExceptionListener,
142
+ options?: ObserveNodejsExceptionsOptions | undefined,
143
+ ): ExceptionListenerCleanup => {
144
+ const {
145
+ includeUncaughtExceptionMonitor,
146
+ includeUncaughtException,
147
+ includeUnhandledRejection
148
+ } = options ?? {}
149
+
150
+ const cleanups: ExceptionListenerCleanup[] = []
151
+
152
+ if (includeUncaughtExceptionMonitor !== false) {
153
+ cleanups.push(onNodejsUncaughtExceptionMonitor(listener, options))
154
+ }
155
+
156
+ if (includeUncaughtException !== false) {
157
+ cleanups.push(onNodejsUncaughtException(listener, options))
158
+ }
159
+
160
+ if (includeUnhandledRejection !== false) {
161
+ cleanups.push(onNodejsUnhandledRejection(listener, options))
162
+ }
163
+
164
+ return () => {
165
+ for (const cleanup of cleanups) {
166
+ cleanup()
167
+ }
168
+ }
169
+ }
@@ -0,0 +1,106 @@
1
+ import type {
2
+ BrowserExceptionRecord,
3
+ BrowserExceptionRecordInput,
4
+ ExceptionRecord,
5
+ ExceptionRecordInput,
6
+ NodejsExceptionRecord,
7
+ NodejsExceptionRecordInput,
8
+ } from "./types.ts"
9
+
10
+ const internalIsErrorLike = (exception: unknown): exception is { message: string } => {
11
+ if (exception instanceof Error) {
12
+ return true
13
+ }
14
+
15
+ if (typeof exception !== "object" || exception === null) {
16
+ return false
17
+ }
18
+
19
+ if (("message" in exception) === false) {
20
+ return false
21
+ }
22
+
23
+ const { message } = exception as { message?: unknown }
24
+ return typeof message === "string"
25
+ }
26
+
27
+ const internalGetExceptionMessage = (exception: unknown): string | undefined => {
28
+ if (internalIsErrorLike(exception)) {
29
+ return exception.message
30
+ }
31
+
32
+ if (typeof exception === "string") {
33
+ return exception
34
+ }
35
+
36
+ return undefined
37
+ }
38
+
39
+ /**
40
+ * 将异常输入标准化为共享异常记录。
41
+ */
42
+ export const normalizeExceptionRecord = (input: ExceptionRecordInput): ExceptionRecord => {
43
+ const {
44
+ runtime,
45
+ source,
46
+ exception,
47
+ message,
48
+ timestamp,
49
+ originalEvent,
50
+ }: ExceptionRecordInput = input
51
+
52
+ return {
53
+ runtime,
54
+ source,
55
+ exception,
56
+ message: message ?? internalGetExceptionMessage(exception),
57
+ timestamp: timestamp ?? Date.now(),
58
+ originalEvent,
59
+ }
60
+ }
61
+
62
+ /**
63
+ * 将浏览器异常输入标准化为浏览器异常记录。
64
+ */
65
+ export const normalizeBrowserExceptionRecord = (input: BrowserExceptionRecordInput): BrowserExceptionRecord => {
66
+ const {
67
+ source,
68
+ filename,
69
+ lineno,
70
+ colno,
71
+ } = input
72
+
73
+ return {
74
+ ...normalizeExceptionRecord({
75
+ ...input,
76
+ runtime: "browser",
77
+ }),
78
+ runtime: "browser",
79
+ source,
80
+ filename,
81
+ lineno,
82
+ colno,
83
+ }
84
+ }
85
+
86
+ /**
87
+ * 将 Nodejs 异常输入标准化为 Nodejs 异常记录。
88
+ */
89
+ export const normalizeNodejsExceptionRecord = (input: NodejsExceptionRecordInput): NodejsExceptionRecord => {
90
+ const {
91
+ source,
92
+ origin,
93
+ promise,
94
+ } = input
95
+
96
+ return {
97
+ ...normalizeExceptionRecord({
98
+ ...input,
99
+ runtime: "nodejs",
100
+ }),
101
+ runtime: "nodejs",
102
+ source,
103
+ origin,
104
+ promise,
105
+ }
106
+ }
@@ -0,0 +1,99 @@
1
+ /**
2
+ * 表示异常来源所属的运行时类型。
3
+ */
4
+ export type ExceptionRuntime =
5
+ | "browser"
6
+ | "nodejs"
7
+ | "unknown"
8
+
9
+ /**
10
+ * 表示取消异常监听的清理函数。
11
+ */
12
+ export type ExceptionListenerCleanup = () => void
13
+
14
+ /**
15
+ * 表示一条标准化异常记录中的共享字段。
16
+ */
17
+ export interface ExceptionRecord {
18
+ runtime: ExceptionRuntime
19
+ source: string
20
+ exception: unknown
21
+ message?: string | undefined
22
+ timestamp: number
23
+ originalEvent?: unknown | undefined
24
+ }
25
+
26
+ /**
27
+ * 表示标准化异常记录时所需的共享输入。
28
+ */
29
+ export interface ExceptionRecordInput {
30
+ runtime: ExceptionRuntime
31
+ source: string
32
+ exception: unknown
33
+ message?: string | undefined
34
+ timestamp?: number | undefined
35
+ originalEvent?: unknown | undefined
36
+ }
37
+
38
+ /**
39
+ * 表示浏览器侧全局异常来源。
40
+ */
41
+ export type BrowserExceptionSource =
42
+ | "browser.global-error"
43
+ | "browser.window-onerror"
44
+ | "browser.unhandled-rejection"
45
+
46
+ /**
47
+ * 表示浏览器侧标准化异常记录。
48
+ */
49
+ export interface BrowserExceptionRecord extends ExceptionRecord {
50
+ runtime: "browser"
51
+ source: BrowserExceptionSource
52
+ filename?: string | undefined
53
+ lineno?: number | undefined
54
+ colno?: number | undefined
55
+ }
56
+
57
+ /**
58
+ * 表示标准化浏览器异常记录时所需的输入。
59
+ */
60
+ export interface BrowserExceptionRecordInput extends Omit<ExceptionRecordInput, "runtime" | "source"> {
61
+ source: BrowserExceptionSource
62
+ filename?: string | undefined
63
+ lineno?: number | undefined
64
+ colno?: number | undefined
65
+ }
66
+
67
+ /**
68
+ * 表示 Nodejs 侧全局异常来源。
69
+ */
70
+ export type NodejsExceptionSource =
71
+ | "nodejs.uncaught-exception-monitor"
72
+ | "nodejs.uncaught-exception"
73
+ | "nodejs.unhandled-rejection"
74
+
75
+ /**
76
+ * 表示 Nodejs 侧标准化异常记录。
77
+ */
78
+ export interface NodejsExceptionRecord extends ExceptionRecord {
79
+ runtime: "nodejs"
80
+ source: NodejsExceptionSource
81
+ origin?: string | undefined
82
+ promise?: Promise<unknown> | undefined
83
+ }
84
+
85
+ /**
86
+ * 表示标准化 Nodejs 异常记录时所需的输入。
87
+ */
88
+ export interface NodejsExceptionRecordInput extends Omit<ExceptionRecordInput, "runtime" | "source"> {
89
+ source: NodejsExceptionSource
90
+ origin?: string | undefined
91
+ promise?: Promise<unknown> | undefined
92
+ }
93
+
94
+ /**
95
+ * 表示批量安装异常监听器时可选的通用配置。
96
+ */
97
+ export interface ObserveExceptionsOptions {
98
+ captureTimestamp?: (() => number) | undefined
99
+ }
@@ -0,0 +1,92 @@
1
+ # Identifier
2
+
3
+ ## Description
4
+
5
+ Identifier 模块用于提供围绕逻辑标识(logical identifier)进行生成、判断、断言、比较与序列化的基础能力,并统一收纳与通用唯一标识符(UUID, Universally Unique Identifier)相关的格式校验与生成语义。
6
+
7
+ 它关注的是“系统如何稳定表达身份与标识”,而不是某个业务系统的编号规则、数据库主键策略或外部协议字段约定。
8
+
9
+ ## For Understanding
10
+
11
+ 理解 Identifier 模块时,应先把逻辑身份与标识格式区分开。这里同时承载两类彼此相关、但层次不同的能力:
12
+
13
+ - `Id` 用于表达库内部的逻辑身份,是一种不透明的运行时标识值。
14
+ - `UUID` 用于表达广泛使用的通用标识字符串格式,适合在系统边界做校验、断言、版本读取和字符串生成。
15
+
16
+ 把这两类能力放在同一个模块中,核心原因不是它们实现方式相同,而是它们都服务于“谁是谁、一个标识是否合法、一个标识如何被稳定生成和传递”这一共同问题域。相对地,随机字符串本身更偏向随机输入生成,不再属于这个边界。
17
+
18
+ 这个模块不适合承载数据库自增键、雪花算法、业务编码规则、分布式全局编号体系或任何需要额外持久化与跨系统一致性承诺的标识模型。若未来出现这类需求,应按更明确的问题域继续建模,而不是直接膨胀当前模块。
19
+
20
+ ## For Using
21
+
22
+ 当你需要在系统中稳定表达身份语义,或在边界层处理 UUID 一类的通用标识字符串时,可以使用这个模块。它适合放在跨模块协作、基础设施边界、日志关联、资源索引和接口参数校验等场景中。
23
+
24
+ 当前公共能力大致可以按以下几类理解:
25
+
26
+ - 逻辑身份生成:用于创建新的 `Id`,或基于同一种子复用同一个运行时标识实例。
27
+ - 逻辑身份判断与比较:用于判断一个值是否为 `Id`,以及区分“同一个实例”和“同一个内部标识值”。
28
+ - 逻辑身份序列化:用于把 `Id` 转换为适合日志、调试和展示的字符串形式。
29
+ - UUID 校验与断言:用于在外部输入进入系统前确认其格式是否合法。
30
+ - UUID 生成:用于生成 UUID v4 与 UUID v7 字符串,以满足常规唯一性和近似时间有序标识场景。
31
+
32
+ 更合理的接入方式,是在边界层尽早决定你需要的是“内部逻辑身份”还是“外部通用标识字符串”。两者虽然都属于 Identifier,但不应相互混用。
33
+
34
+ ## For Contributing
35
+
36
+ 贡献 Identifier 模块时,应优先判断新增能力是否真的在澄清标识语义,而不是借“编号”或“随机”名义引入其它问题域。这里应长期承载的是逻辑身份模型与通用标识格式模型,而不是各种临时编码技巧的集合。
37
+
38
+ 在扩展时,应优先遵守以下边界:
39
+
40
+ - `Id` 应继续保持为不透明的逻辑身份值,不要让调用方依赖其内部结构。
41
+ - UUID 相关能力应围绕格式判断、断言、版本提取和生成展开,不要混入业务编号规则。
42
+ - 如果某个能力的重点已经变成“随机文本如何生成”,应优先考虑 Random 模块,而不是继续放入 Identifier。
43
+ - 如果某个能力需要更强的持久化、安全性或跨系统一致性承诺,应在更具体的问题域中单独建模。
44
+
45
+ ### JSDoc 注释格式要求
46
+
47
+ - 每个公开导出的目标(类型、函数、变量、类等)都应包含 JSDoc 注释,让人在不跳转实现的情况下就能理解用途。
48
+ - JSDoc 注释第一行应为清晰且简洁的描述,该描述优先使用中文(英文也可以)。
49
+ - 如果描述后还有其他内容,应在描述后加一个空行。
50
+ - 如果有示例,应使用 `@example` 标签,后接三重反引号代码块(不带语言标识)。
51
+ - 如果有示例,应包含多个场景,展示不同用法,尤其要覆盖常见组合方式或边界输入。
52
+ - 如果有示例,应使用注释格式说明每个场景:`// Expect: <result>`。
53
+ - 如果有示例,应将结果赋值给 `example1`、`example2` 之类的变量,以保持示例易读。
54
+ - 如果有示例,`// Expect: <result>` 应该位于 `example1`、`example2` 之前,以保持示例的逻辑清晰。
55
+ - 如果有示例,应优先使用确定性示例;避免断言精确的随机输出。
56
+ - 如果函数返回结构化字符串,应展示其预期格式特征。
57
+ - 如果有参考资料,应将 `@see` 放在 `@example` 代码块之后,并用一个空行分隔。
58
+
59
+ ### 实现规范要求
60
+
61
+ - 不同程序元素之间使用一个空行分隔,保持结构清楚。这里的程序元素,通常指函数、类型、常量,以及直接服务于它们的辅助元素。
62
+ - 某程序元素独占的辅助元素与该程序元素本身视为一个整体,不要在它们之间添加空行。
63
+ - 程序元素的辅助元素应该放置在该程序元素的上方,以保持阅读时的逻辑顺序。
64
+ - 若辅助元素被多个程序元素共享,则应将其视为独立的程序元素,放在这些程序元素中第一个相关目标的上方,并与后续程序元素之间保留一个空行。
65
+ - 辅助元素也应该像其它程序元素一样,保持清晰的命名和适当的注释,以便在需要阅读实现细节时能够快速理解它们的作用和使用方式。
66
+ - 辅助元素的命名必须以前缀 `internal` 开头(或 `Internal`,大小写不敏感)。
67
+ - 辅助元素永远不要公开导出。
68
+ - 被模块内多个不同文件中的程序元素共享的辅助元素,应该放在一个单独的文件中,例如 `./src/identifier/internal.ts`;若当前没有必要,不必为了形式强行拆分。
69
+ - 模块内可以包含子模块。只有当某个子目录表达一个稳定、可单独理解、且可能被父模块重导出的子问题域时,才应将其视为子模块。
70
+ - 子模块包含多个文件时,应该为其单独创建子文件夹,并为其创建单独的 Barrel 文件;父模块的 Barrel 文件再重导出子模块的 Barrel 文件。
71
+ - 子模块不需要有自己的 `README.md`。
72
+ - 子模块可以有自己的 `internal.ts` 文件,多个子模块共享的辅助元素应该放在父模块的 `internal.ts` 文件中,单个子模块共享的辅助元素应该放在该子模块的 `internal.ts` 文件中。
73
+ - 对模块依赖关系的要求(通常是不循环依赖或不反向依赖)与对 DRY 的要求可能产生冲突。此时,若复用的代码数量不大,可以适当牺牲 DRY,复制粘贴并保留必要的注释说明;若复用的代码数量较大,则可以将其抽象到新的文件或子模块中,如 `common.ts`,并在需要的地方导入使用。
74
+ - 实现应继续保证逻辑身份语义与标识字符串语义都通过稳定公共入口暴露,而不是通过内部形状或偶然路径被外部依赖。
75
+
76
+ ### 导出策略要求
77
+
78
+ - 保持内部辅助项和内部符号为私有,不要让外部接入依赖临时性的内部结构。
79
+ - 每个模块都应有一个用于重导出所有公共 API 的 Barrel 文件。
80
+ - Barrel 文件应命名为 `index.ts`,放在模块目录根部,并且所有公共 API 都应从该文件导出。
81
+ - 新增公共能力时,应优先检查它是否表达稳定、清楚且值得长期维护的标识语义,而不是某段实现细节的便捷暴露;仅在确认需要长期对外承诺时再加入 Barrel 导出。
82
+
83
+ ### 测试要求
84
+
85
+ - 若程序元素是函数,则只为该函数编写一个测试,如果该函数需要测试多个用例,应放在同一个测试中。
86
+ - 若程序元素是类,则至少要为该类的每一个方法编写一个测试,如果该方法需要测试多个用例,应放在同一个测试中。
87
+ - 若程序元素是类,除了为该类的每一个方法编写至少一个测试之外,还可以为该类编写任意多个测试,以覆盖该类的不同使用场景或边界情况。
88
+ - 若编写测试时需要用到辅助元素(Mock 或 Spy 等),可以在测试文件中直接定义这些辅助元素。若辅助元素较为简单,则可以直接放在每一个测试内部,优先保证每个测试的独立性,而不是追求极致 DRY;若辅助元素较为复杂或需要在多个测试中复用,则可以放在测试文件顶部,供该测试文件中的所有测试使用。
89
+ - 测试顺序应与源文件中被测试目标的原始顺序保持一致。
90
+ - 若该模块不需要测试,必须在说明文件中明确说明该模块不需要测试,并说明理由。一般来说,只有在该模块没有可执行的公共函数、只承载类型层表达,或其语义已被上层模块的测试完整覆盖且重复测试几乎不再带来额外价值时,才适合这样处理。
91
+ - 模块的单元测试文件目录是 `./tests/unit/identifier`,若模块包含子模块,则子模块的单元测试文件目录为 `./tests/unit/identifier/<sub-module-name>`。
92
+ - 测试应重点覆盖带种子与不带种子的 `Id` 生成语义、`Id` 的比较与序列化,以及 UUID 的格式校验、版本读取和不同生成路径的结果特征。
@@ -0,0 +1,119 @@
1
+ import { generateUuidV7 } from "./uuid.ts"
2
+
3
+ const INTERNAL_ID: unique symbol = Symbol("INTERNAL_ID")
4
+ type IdentifierSeed = string | number | symbol
5
+
6
+ /**
7
+ * Represent a logical identity value.
8
+ *
9
+ * @example
10
+ * ```
11
+ * const userId = generateId("user:1")
12
+ * const example1 = isId(userId)
13
+ * // Expect: true
14
+ *
15
+ * const example2 = String(userId)
16
+ * // Expect: starts with "Symbol-ID-"
17
+ * ```
18
+ */
19
+ export interface Id {
20
+ [INTERNAL_ID]: string
21
+ [Symbol.toPrimitive]: (hint: string) => string
22
+ }
23
+
24
+ const internalIdPacket: Record<IdentifierSeed, Id> = {}
25
+
26
+ /**
27
+ * 根据种子生成 `Id`,或在未提供种子时生成一个新的 `Id`。
28
+ *
29
+ * 当传入种子时,同一种子会复用同一个 `Id` 实例;未传入种子时,每次都会得到新的 `Id`。
30
+ *
31
+ * @example
32
+ * ```
33
+ * const stableA = generateId("user:1")
34
+ * const stableB = generateId("user:1")
35
+ * const example1 = Object.is(stableA, stableB)
36
+ * // Expect: true
37
+ *
38
+ * const freshA = generateId()
39
+ * const freshB = generateId()
40
+ * const example2 = Object.is(freshA, freshB)
41
+ * // Expect: false
42
+ * ```
43
+ */
44
+ export const generateId = (seed?: IdentifierSeed | undefined): Id => {
45
+ if (seed === undefined) {
46
+ const id = {
47
+ [INTERNAL_ID]: generateUuidV7(),
48
+ [Symbol.toPrimitive]: (): string => {
49
+ return idToString(id)
50
+ },
51
+ }
52
+ return id
53
+ }
54
+
55
+ const id = internalIdPacket[seed] ?? {
56
+ [INTERNAL_ID]: generateUuidV7(),
57
+ [Symbol.toPrimitive]: (): string => {
58
+ return idToString(id)
59
+ },
60
+ }
61
+ internalIdPacket[seed] = id
62
+
63
+ return id
64
+ }
65
+
66
+ /**
67
+ * 判断一个值是否为 `Id`。
68
+ */
69
+ export const isId = (target: unknown): target is Id => {
70
+ return typeof target === "object" && target !== null && INTERNAL_ID in target
71
+ }
72
+
73
+ type AssertId = (target: unknown) => asserts target is Id
74
+
75
+ /**
76
+ * 断言一个值是 `Id`。
77
+ *
78
+ * @throws {TypeError} 当 target 不是 `Id` 时抛出
79
+ */
80
+ export const assertId: AssertId = (target: unknown): void => {
81
+ if (isId(target) === false) {
82
+ throw new TypeError(`Expected Id, got: ${String(target)}`)
83
+ }
84
+ }
85
+
86
+ /**
87
+ * 按引用身份比较两个 `Id` 值是否为同一个实例。
88
+ */
89
+ export const isSameId = (a: Id, b: Id): boolean => {
90
+ return Object.is(a, b)
91
+ }
92
+
93
+ /**
94
+ * 按内部标识值比较两个 `Id` 是否相等。
95
+ */
96
+ export const isEqualId = (a: Id, b: Id): boolean => {
97
+ return a[INTERNAL_ID] === b[INTERNAL_ID]
98
+ }
99
+
100
+ /**
101
+ * 将 `Id` 转换为可读字符串。
102
+ *
103
+ * 返回值格式为 `Symbol-ID-<uuid>`。
104
+ *
105
+ * @example
106
+ * ```
107
+ * const userId = generateId("user:1")
108
+ * const example1 = idToString(userId)
109
+ * // Expect: starts with "Symbol-ID-"
110
+ *
111
+ * const anonymousId = generateId()
112
+ * const example2 = idToString(anonymousId)
113
+ * // Expect: matches the format "Symbol-ID-<uuid>"
114
+ * ```
115
+ */
116
+ export const idToString = (id: Id): string => {
117
+ assertId(id)
118
+ return `Symbol-ID-${id[INTERNAL_ID]}`
119
+ }
@@ -0,0 +1,2 @@
1
+ export * from "./id.ts"
2
+ export * from "./uuid.ts"