tcdona_unilib 1.0.12 → 1.0.14

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/eff/aff.ts ADDED
@@ -0,0 +1,161 @@
1
+ import { Result } from 'neverthrow'
2
+ import { err, ok, ResultAsync } from 'neverthrow'
3
+ import {
4
+ Actrl,
5
+ ABORT_REASON,
6
+ shortId,
7
+ type AffFn,
8
+ type NonEmptyArray,
9
+ type ExtractOk,
10
+ type ExtractErr,
11
+ } from './aff.util'
12
+
13
+ export class Aff {
14
+ /**
15
+ * 创建子任务(继承父 signal,支持取消传播)
16
+ */
17
+ static make<T, E>(
18
+ actrlSpanId: string,
19
+ executor: AffFn<T, E>,
20
+ parentActrl: Actrl,
21
+ ): ResultAsync<T, E> {
22
+ const actrl = new Actrl(actrlSpanId)
23
+ const unlink = parentActrl.link(actrl)
24
+
25
+ const executorResult = executor(actrl)
26
+ // 统一处理 PromiseLike 和 ResultAsync,转换为 Promise<Result<T, E>>
27
+ // ResultAsync 实现了 PromiseLike,Promise.resolve 可以处理它
28
+ const promise: Promise<Result<T, E>> = Promise.resolve(
29
+ executorResult as PromiseLike<Result<T, E>>,
30
+ )
31
+ .catch((e) => err(e as E))
32
+ .then((result) => {
33
+ actrl.resultStatus = result.isOk() ? 'ok' : 'err'
34
+ unlink()
35
+ actrl.cancel(ABORT_REASON.COMPLETED)
36
+ return result
37
+ })
38
+
39
+ return new ResultAsync(promise)
40
+ }
41
+
42
+ /**
43
+ * 创建根级任务(无父 signal)
44
+ */
45
+ static root<T, E>(executor: AffFn<T, E>): ResultAsync<T, E> {
46
+ return Aff.make('root', executor, new Actrl('$', true))
47
+ }
48
+
49
+ static all<T extends NonEmptyArray<AffFn<unknown, unknown>>>(
50
+ executors: T,
51
+ parentActrl: Actrl,
52
+ ): ResultAsync<
53
+ { -readonly [P in keyof T]: ExtractOk<T[P]> },
54
+ ExtractErr<T[number]>
55
+ > {
56
+ const actrl = new Actrl('all#' + shortId())
57
+ const unlink = parentActrl.link(actrl)
58
+
59
+ // Result -> Promise 语义转换:err 变成 reject,这样 Promise.all 遇到 err 会立即返回
60
+ const toPromise = (ra: ResultAsync<unknown, unknown>) =>
61
+ ra.then((r) => (r.isOk() ? r.value : Promise.reject(r.error)))
62
+
63
+ const promise = Promise.all(
64
+ executors.map((fn, i) => toPromise(Aff.make(`all-${i}`, fn, actrl))),
65
+ )
66
+ .then((values) => {
67
+ unlink()
68
+ actrl.cancel(ABORT_REASON.COMPLETED)
69
+ return ok(values) as Result<
70
+ { -readonly [P in keyof T]: ExtractOk<T[P]> },
71
+ ExtractErr<T[number]>
72
+ >
73
+ })
74
+ .catch((e) => {
75
+ unlink()
76
+ actrl.cancel(ABORT_REASON.SETTLED)
77
+ return err(e) as Result<
78
+ { -readonly [P in keyof T]: ExtractOk<T[P]> },
79
+ ExtractErr<T[number]>
80
+ >
81
+ })
82
+
83
+ return new ResultAsync(promise)
84
+ }
85
+
86
+ static any<T extends NonEmptyArray<AffFn<unknown, unknown>>>(
87
+ executors: T,
88
+ parentActrl: Actrl,
89
+ ): ResultAsync<ExtractOk<T[number]>, ExtractErr<T[number]>[]> {
90
+ const actrl = new Actrl('any#' + shortId())
91
+ const unlink = parentActrl.link(actrl)
92
+
93
+ // Result -> Promise 语义转换
94
+ const toPromise = (ra: ResultAsync<unknown, unknown>) =>
95
+ ra.then((r) => (r.isOk() ? r.value : Promise.reject(r.error)))
96
+
97
+ const promise = Promise.any(
98
+ executors.map((fn, i) => toPromise(Aff.make(`any-${i}`, fn, actrl))),
99
+ )
100
+ .then((value) => {
101
+ unlink()
102
+ actrl.cancel(ABORT_REASON.SETTLED)
103
+ return ok(value) as Result<
104
+ ExtractOk<T[number]>,
105
+ ExtractErr<T[number]>[]
106
+ >
107
+ })
108
+ .catch((e: AggregateError) => {
109
+ unlink()
110
+ actrl.cancel(ABORT_REASON.COMPLETED)
111
+ return err(e.errors) as Result<
112
+ ExtractOk<T[number]>,
113
+ ExtractErr<T[number]>[]
114
+ >
115
+ })
116
+
117
+ return new ResultAsync(promise)
118
+ }
119
+
120
+ static race<T extends NonEmptyArray<AffFn<unknown, unknown>>>(
121
+ executors: T,
122
+ parentActrl: Actrl,
123
+ ): ResultAsync<ExtractOk<T[number]>, ExtractErr<T[number]>> {
124
+ const actrl = new Actrl('race#' + shortId())
125
+ const unlink = parentActrl.link(actrl)
126
+
127
+ const promise = Promise.race(
128
+ executors.map((fn, i) => Aff.make(`race-${i}`, fn, actrl)),
129
+ ).then((firstResult) => {
130
+ unlink()
131
+ actrl.cancel(ABORT_REASON.SETTLED)
132
+ return firstResult as Result<ExtractOk<T[number]>, ExtractErr<T[number]>>
133
+ })
134
+
135
+ return new ResultAsync(promise)
136
+ }
137
+
138
+ static allSettled<T extends NonEmptyArray<AffFn<unknown, unknown>>>(
139
+ executors: T,
140
+ parentActrl: Actrl,
141
+ ): ResultAsync<
142
+ { -readonly [P in keyof T]: Result<ExtractOk<T[P]>, ExtractErr<T[P]>> },
143
+ never
144
+ > {
145
+ const actrl = new Actrl('settled#' + shortId())
146
+ const unlink = parentActrl.link(actrl)
147
+
148
+ const promise = Promise.all(
149
+ executors.map((fn, i) => Aff.make(`allSettled-${i}`, fn, actrl)),
150
+ ).then((results) => {
151
+ unlink()
152
+ actrl.cancel(ABORT_REASON.COMPLETED)
153
+ return ok(results) as Result<
154
+ { -readonly [P in keyof T]: Result<ExtractOk<T[P]>, ExtractErr<T[P]>> },
155
+ never
156
+ >
157
+ })
158
+
159
+ return new ResultAsync(promise)
160
+ }
161
+ }
@@ -0,0 +1,218 @@
1
+ import { Result } from 'neverthrow'
2
+ import { err, ok, ResultAsync } from 'neverthrow'
3
+ import { tcNanoid } from '../staticMeta/string.nanoid'
4
+
5
+ export type ActrlSpan = {
6
+ id: string
7
+ parent: string
8
+ status: 'init'
9
+ startTime: number
10
+ duration: number
11
+ }
12
+
13
+ export class Actrl {
14
+ readonly ac: AbortController
15
+ public traceid: string = ''
16
+ public span: ActrlSpan = {
17
+ id: '',
18
+ parent: '',
19
+ status: 'init',
20
+ startTime: 0,
21
+ duration: 0,
22
+ }
23
+ private isRoot = false
24
+ public depth = 0 // 基于父子关系的真实深度
25
+ public resultStatus: 'ok' | 'err' | null = null // 任务返回状态
26
+ public logs: string[] = [] // 日志数组,用于收集日志信息
27
+ private rootLogs: string[] | null = null // 指向根 Actrl 的 logs,用于统一收集所有日志
28
+
29
+ constructor(spanid: string, isRoot = false) {
30
+ this.ac = new AbortController()
31
+ this.span.id = spanid
32
+ this.isRoot = isRoot
33
+ this.depth = isRoot ? 0 : 0
34
+ if (isRoot) {
35
+ this.rootLogs = this.logs // 根 Actrl 的 rootLogs 指向自己的 logs
36
+ }
37
+ }
38
+
39
+ get aborted() {
40
+ return this.ac.signal.aborted
41
+ }
42
+ get reason(): unknown {
43
+ return this.ac.signal.reason
44
+ }
45
+
46
+ cancel(reason?: unknown) {
47
+ this.ac.abort(reason)
48
+ }
49
+ link(childActrl: Actrl) {
50
+ // 让子 Actrl 共享根 Actrl 的 logs
51
+ childActrl.rootLogs = this.rootLogs || this.logs
52
+ // 获取目标日志数组(根 Actrl 的 logs)
53
+ const getTargetLogs = () => this.rootLogs || this.logs
54
+
55
+ if (this.isRoot) {
56
+ this.traceid = tcNanoid()
57
+ childActrl.traceid = this.traceid
58
+ childActrl.depth = 1
59
+
60
+ const indent = formatIndent(childActrl.depth)
61
+ const displayId = childActrl.span.id
62
+ getTargetLogs().push(formatLogStart(indent, displayId))
63
+
64
+ if (this.ac.signal.aborted) {
65
+ childActrl.cancel(this.ac.signal.reason)
66
+ getTargetLogs().push(
67
+ formatLogCancel(indent, childActrl.span.id, 'parent aborted'),
68
+ )
69
+ return noop
70
+ }
71
+ const handler = () => {
72
+ childActrl.cancel(this.ac.signal.reason)
73
+ // 记录取消日志(当父任务因为 SETTLED 被取消时)
74
+ if (this.ac.signal.reason === ABORT_REASON.SETTLED) {
75
+ getTargetLogs().push(formatLogCancel(indent, childActrl.span.id))
76
+ }
77
+ }
78
+ this.ac.signal.addEventListener('abort', handler, { once: true })
79
+ return () => {
80
+ // 判断是否被外部取消(reason 是 'settled' 表示被其他任务抢先完成)
81
+ const isCancelled =
82
+ childActrl.aborted && childActrl.reason === ABORT_REASON.SETTLED
83
+ let status = ''
84
+ if (isCancelled) {
85
+ status = ' [cancelled]'
86
+ } else if (childActrl.resultStatus) {
87
+ status = ' [' + childActrl.resultStatus + ']'
88
+ }
89
+ getTargetLogs().push(formatLogEnd(indent, childActrl.span.id, status))
90
+ this.ac.signal.removeEventListener('abort', handler)
91
+ }
92
+ }
93
+ childActrl.traceid = this.traceid
94
+ childActrl.span.parent = this.span.id
95
+ childActrl.depth = this.depth + 1
96
+
97
+ const indent = formatIndent(childActrl.depth)
98
+ const displayId = formatDisplayId(this.span.id, childActrl.span.id)
99
+ childActrl.span.id = displayId
100
+ getTargetLogs().push(formatLogStart(indent, displayId))
101
+
102
+ if (this.ac.signal.aborted) {
103
+ childActrl.cancel(this.ac.signal.reason)
104
+ getTargetLogs().push(
105
+ formatLogCancel(indent, childActrl.span.id, 'parent aborted'),
106
+ )
107
+ return noop
108
+ }
109
+ const handler = () => {
110
+ childActrl.cancel(this.ac.signal.reason)
111
+ // 记录取消日志(当父任务因为 SETTLED 被取消时)
112
+ if (this.ac.signal.reason === ABORT_REASON.SETTLED) {
113
+ getTargetLogs().push(formatLogCancel(indent, childActrl.span.id))
114
+ }
115
+ }
116
+ this.ac.signal.addEventListener('abort', handler, { once: true })
117
+ return () => {
118
+ // 判断是否被外部取消(reason 是 'settled' 表示被其他任务抢先完成)
119
+ const isCancelled =
120
+ childActrl.aborted && childActrl.reason === ABORT_REASON.SETTLED
121
+ let status = ''
122
+ if (isCancelled) {
123
+ status = ' [cancelled]'
124
+ } else if (childActrl.resultStatus) {
125
+ status = ' [' + childActrl.resultStatus + ']'
126
+ }
127
+ getTargetLogs().push(formatLogEnd(indent, childActrl.span.id, status))
128
+ this.ac.signal.removeEventListener('abort', handler)
129
+ }
130
+ }
131
+ on(
132
+ type: string,
133
+ listener: EventListenerOrEventListenerObject,
134
+ options?: boolean | AddEventListenerOptions,
135
+ ) {
136
+ this.ac.signal.addEventListener(type, listener, options)
137
+ }
138
+ off(
139
+ type: string,
140
+ listener: EventListenerOrEventListenerObject,
141
+ options?: boolean | EventListenerOptions,
142
+ ) {
143
+ this.ac.signal.removeEventListener(type, listener, options)
144
+ }
145
+ }
146
+
147
+ export type AffFn<T, E> = (
148
+ actrl: Actrl,
149
+ ) => PromiseLike<Result<T, E>> | ResultAsync<T, E>
150
+ export type NonEmptyArray<T> = readonly [T, ...T[]]
151
+ export type ExtractOk<T> = T extends AffFn<infer U, unknown> ? U : never
152
+ export type ExtractErr<T> = T extends AffFn<unknown, infer E> ? E : never
153
+
154
+ export const ABORT_REASON = { SETTLED: 'settled', COMPLETED: 'completed' } as const
155
+ export const noop = () => {}
156
+
157
+ // ============================================================
158
+ // 日志格式化函数
159
+ // ============================================================
160
+
161
+ /**
162
+ * 生成缩进字符串
163
+ */
164
+ export const formatIndent = (depth: number): string => {
165
+ return ' '.repeat(depth)
166
+ }
167
+
168
+ /**
169
+ * 格式化显示 ID(取父级最后一段 + 当前 id)
170
+ */
171
+ export const formatDisplayId = (parentId: string, childId: string): string => {
172
+ const parentShort = parentId.split('/').pop() || parentId
173
+ return parentShort + '/' + childId
174
+ }
175
+
176
+ /**
177
+ * 格式化开始日志
178
+ */
179
+ export const formatLogStart = (indent: string, id: string): string => {
180
+ return indent + '→ ' + id
181
+ }
182
+
183
+ /**
184
+ * 格式化结束日志
185
+ */
186
+ export const formatLogEnd = (indent: string, id: string, status?: string): string => {
187
+ return indent + '← ' + id + (status || '')
188
+ }
189
+
190
+ /**
191
+ * 格式化取消日志
192
+ */
193
+ export const formatLogCancel = (
194
+ indent: string,
195
+ id: string,
196
+ reason?: string,
197
+ ): string => {
198
+ if (reason === 'parent aborted') {
199
+ return indent + '✗ ' + id + ' [cancelled: parent aborted]'
200
+ }
201
+ return indent + '✗ ' + id + ' [cancelled]'
202
+ }
203
+
204
+ /**
205
+ * 打印日志
206
+ */
207
+ export const printLogs = (logs: string[], title?: string): void => {
208
+ if (logs.length > 0) {
209
+ if (title) {
210
+ console.log(`=== ${title} ===`)
211
+ }
212
+ console.log(logs.join('\n'))
213
+ }
214
+ }
215
+
216
+ // 简短 id 生成器
217
+ let _idCounter = 0
218
+ export const shortId = () => (++_idCounter).toString(36)
package/eff/eff.ts CHANGED
@@ -31,15 +31,15 @@ const noop = () => {}
31
31
  /**
32
32
  * 将父 signal abort 传播到子 controller,返回解绑函数
33
33
  */
34
- const linkSignal = (
34
+ const link = (
35
35
  parentSignal: AbortSignal,
36
- controller: AbortController,
36
+ childController: AbortController,
37
37
  ): (() => void) => {
38
38
  if (parentSignal.aborted) {
39
- controller.abort(parentSignal.reason)
39
+ childController.abort(parentSignal.reason)
40
40
  return noop
41
41
  }
42
- const handler = () => controller.abort(parentSignal.reason)
42
+ const handler = () => childController.abort(parentSignal.reason)
43
43
  parentSignal.addEventListener('abort', handler, { once: true })
44
44
  return () => parentSignal.removeEventListener('abort', handler)
45
45
  }
@@ -68,10 +68,9 @@ const runAll = <T, E, R, ER>(
68
68
  },
69
69
  ) => void,
70
70
  ): ResultAsync<R, ER> =>
71
- Eff.create(({ signal }) => {
71
+ Eff.create(({ signal: parentSignal }) => {
72
72
  const controller = new AbortController()
73
- const childSignal = controller.signal
74
- const unlink = linkSignal(signal, controller)
73
+ const unlink = link(parentSignal, controller)
75
74
 
76
75
  return new Promise((resolve) => {
77
76
  let done = false
@@ -90,7 +89,7 @@ const runAll = <T, E, R, ER>(
90
89
  resolve(r)
91
90
  }
92
91
 
93
- if (signal.aborted) {
92
+ if (parentSignal.aborted) {
94
93
  settle(ok([] as R))
95
94
  return
96
95
  }
@@ -98,7 +97,7 @@ const runAll = <T, E, R, ER>(
98
97
  let remaining = executors.length
99
98
 
100
99
  executors.forEach((executor, i) => {
101
- Eff.create(executor, childSignal).then((r) => {
100
+ Eff.create(executor, controller.signal).then((r) => {
102
101
  if (done) return
103
102
  remaining--
104
103
  onResult(r, i, settle, context)
@@ -129,7 +128,7 @@ export class Eff {
129
128
  parentSignal: AbortSignal,
130
129
  ): ResultAsync<T, E> {
131
130
  const controller = new AbortController()
132
- const unlink = linkSignal(parentSignal, controller)
131
+ const unlink = link(parentSignal, controller)
133
132
 
134
133
  const promise: Promise<Result<T, E>> = Promise.resolve()
135
134
  .then(() => executor({ signal: controller.signal }))
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tcdona_unilib",
3
- "version": "1.0.12",
3
+ "version": "1.0.14",
4
4
  "description": "Unified dependency aggregation layer for shared libraries",
5
5
  "type": "module",
6
6
  "sideEffects": false,
@@ -46,32 +46,33 @@
46
46
  "./viteplugin/*.ts": "./viteplugin/*.ts"
47
47
  },
48
48
  "scripts": {
49
- "viteplugin": "vite build -c viteplugin/vite.config.ts",
50
- "pub": "bun run ./tc.pub.ts"
49
+ "test": "vitest",
50
+ "viteplugin": "vite build -c viteplugin/vite.config.ts"
51
51
  },
52
52
  "keywords": [],
53
53
  "author": "",
54
54
  "license": "ISC",
55
55
  "devDependencies": {
56
56
  "@types/bun": "^1.3.5",
57
- "@types/node": "^20.10.6",
58
- "vitest": "^4.0.16"
57
+ "@types/node": "^20.19.28"
59
58
  },
60
59
  "dependencies": {
61
- "@ast-grep/napi": "^0.40.3",
62
- "@dotenvx/dotenvx": "^1.51.2",
63
- "@inquirer/prompts": "^8.1.0",
60
+ "@ast-grep/napi": "^0.40.5",
61
+ "@dotenvx/dotenvx": "^1.51.4",
62
+ "@inquirer/prompts": "^8.2.0",
64
63
  "@types/big.js": "^6.2.2",
64
+ "@types/js-yaml": "^4.0.9",
65
65
  "@types/turndown": "^5.0.6",
66
- "animejs": "^4.1.3",
66
+ "animejs": "^4.2.2",
67
67
  "big.js": "^7.0.1",
68
68
  "clipboardy": "^5.0.2",
69
69
  "dayjs": "^1.11.19",
70
70
  "effector": "^23.4.4",
71
71
  "es-toolkit": "^1.43.0",
72
- "exome": "^2.8.0",
72
+ "exome": "^2.8.1",
73
73
  "hono": "^4.11.3",
74
74
  "hotkeys-js": "^4.0.0",
75
+ "js-yaml": "^4.1.1",
75
76
  "koka": "^1.0.9",
76
77
  "koka-domain": "^1.0.0",
77
78
  "magic-regexp": "^0.10.0",
@@ -82,11 +83,12 @@
82
83
  "pathe": "^2.0.3",
83
84
  "pinyin-pro": "^3.27.0",
84
85
  "prettier": "^3.7.4",
85
- "tinypool": "^2.0.0",
86
+ "tinypool": "^2.1.0",
86
87
  "turndown": "^7.2.2",
87
- "vite": "^7.3.0",
88
+ "vite": "^7.3.1",
89
+ "vitest": "^4.0.17",
88
90
  "vue": "^3.5.26",
89
- "zod": "^4.2.1",
91
+ "zod": "^4.3.5",
90
92
  "zx": "^8.8.5"
91
93
  }
92
94
  }
@@ -105,9 +105,9 @@ export class TcErr extends Error {
105
105
  cons.error(
106
106
  getTimestamp(),
107
107
  `[${fileId.conf.srcFile}:${fileId.conf.id}]${fileId.conf.tag.length > 0 ? `[${fileId.conf.tag.join(', ')}]` : ''}`,
108
- cause,
109
108
  rest,
110
109
  )
110
+ cons.log(cause)
111
111
  }
112
112
  }
113
113
 
@@ -1,5 +1,4 @@
1
1
  import { fs, YAML } from 'zx'
2
-
3
2
  /**
4
3
  * YAML 文件读写:无 data 参数时读取,有 data 参数时写入
5
4
  */
@@ -8,7 +7,7 @@ export function yml(path: string, data?: unknown): unknown {
8
7
  fs.writeFileSync(
9
8
  path,
10
9
  // @ts-expect-error YAML.stringify 类型定义有误
11
- YAML.stringify(data, { singleQuote: true, sortKeys: true }),
10
+ YAML.stringify(data, { singleQuote: true }),
12
11
  'utf-8',
13
12
  )
14
13
  } else {
@@ -16,3 +15,13 @@ export function yml(path: string, data?: unknown): unknown {
16
15
  return YAML.parse(ymlContent)
17
16
  }
18
17
  }
18
+
19
+ import { default as jsYAML } from 'js-yaml'
20
+ export const yml2 = (path: string, data?: unknown): unknown => {
21
+ if (typeof data !== 'undefined') {
22
+ fs.writeFileSync(path, jsYAML.dump(data, { sortKeys: true }), 'utf-8')
23
+ } else {
24
+ const ymlContent = fs.readFileSync(path, 'utf-8')
25
+ return jsYAML.load(ymlContent)
26
+ }
27
+ }