@kine-design/crud 0.0.1-beta.2 → 0.0.1-beta.21

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 (118) hide show
  1. package/.vlaude/last-session-id +1 -0
  2. package/components/crudPage/KCrudPage.tsx +178 -0
  3. package/components/crudPage/crudPage.css +64 -0
  4. package/components/crudPage/index.ts +10 -0
  5. package/components/editableTable/KEditableTable.tsx +281 -0
  6. package/components/editableTable/editableTable.css +268 -0
  7. package/components/editableTable/index.ts +10 -0
  8. package/components/formPage/KApprovalDialog.tsx +142 -0
  9. package/components/formPage/KFormCard.tsx +65 -0
  10. package/components/formPage/KFormPage.tsx +128 -0
  11. package/components/formPage/KMasterDetailPage.tsx +205 -0
  12. package/components/formPage/KStickyActionBar.tsx +33 -0
  13. package/components/formPage/formPage.css +629 -0
  14. package/components/formPage/index.ts +14 -0
  15. package/components/layout/KContent.tsx +20 -0
  16. package/components/layout/KHeader.tsx +37 -0
  17. package/components/layout/KLayout.tsx +82 -0
  18. package/components/layout/KSider.tsx +80 -0
  19. package/components/layout/index.ts +18 -0
  20. package/components/layout/layout.css +262 -0
  21. package/components/login/KLoginPage.tsx +129 -0
  22. package/components/login/index.ts +10 -0
  23. package/components/login/login.css +118 -0
  24. package/components/navMenu/KNavMenu.tsx +175 -0
  25. package/components/navMenu/index.ts +2 -0
  26. package/components/navMenu/navMenu.css +197 -0
  27. package/components/pageHeader/KPageHeader.tsx +85 -0
  28. package/components/pageHeader/index.ts +9 -0
  29. package/components/pageHeader/pageHeader.css +93 -0
  30. package/components/searchTable/KSearchTable.tsx +138 -0
  31. package/components/searchTable/index.ts +10 -0
  32. package/components/searchTable/searchTable.css +121 -0
  33. package/components/upload/KFileList.tsx +95 -0
  34. package/components/upload/KImageUpload.tsx +286 -0
  35. package/components/upload/KUpload.tsx +206 -0
  36. package/components/upload/index.ts +13 -0
  37. package/components/upload/types.ts +26 -0
  38. package/components/upload/upload.css +345 -0
  39. package/composables/auth/authGuard.ts +128 -0
  40. package/composables/auth/index.ts +23 -0
  41. package/composables/auth/types.ts +109 -0
  42. package/composables/auth/useAuth.ts +278 -0
  43. package/composables/auth/vCan.ts +95 -0
  44. package/composables/defineRepository.ts +224 -0
  45. package/composables/error/createErrorHandler.ts +46 -0
  46. package/composables/error/defaultFeedbackHandler.ts +76 -0
  47. package/composables/error/dispatchError.ts +70 -0
  48. package/composables/error/index.ts +32 -0
  49. package/composables/error/types.ts +57 -0
  50. package/composables/error/useErrorHandler.ts +41 -0
  51. package/composables/form/index.ts +18 -0
  52. package/composables/form/renderFormField.tsx +119 -0
  53. package/composables/form/types.ts +129 -0
  54. package/composables/form/useFormPage.ts +183 -0
  55. package/composables/index.ts +62 -0
  56. package/composables/page/index.ts +11 -0
  57. package/composables/page/types.ts +62 -0
  58. package/composables/page/useCrudPage.ts +88 -0
  59. package/composables/request/composables.ts +206 -0
  60. package/composables/request/controlGate.ts +143 -0
  61. package/composables/request/createRequest.ts +173 -0
  62. package/composables/request/index.ts +71 -0
  63. package/composables/request/orchestrator.ts +145 -0
  64. package/composables/request/requestBuilder.ts +418 -0
  65. package/composables/request/transport/fetchTransport.ts +79 -0
  66. package/composables/request/transport/xhrTransport.ts +100 -0
  67. package/composables/request/types.ts +226 -0
  68. package/composables/request/upload.ts +146 -0
  69. package/composables/router/createRouterGuard.ts +134 -0
  70. package/composables/router/defineCrudRoutes.ts +116 -0
  71. package/composables/router/index.ts +22 -0
  72. package/composables/router/types.ts +128 -0
  73. package/composables/router/useMenuFromRoutes.ts +109 -0
  74. package/composables/router/useTabStore.ts +183 -0
  75. package/composables/search/index.ts +11 -0
  76. package/composables/search/useAutoCompleteSearch.ts +161 -0
  77. package/composables/setupCrud.ts +43 -0
  78. package/composables/storage/createStorageAdapter.ts +72 -0
  79. package/composables/storage/index.ts +13 -0
  80. package/composables/storage/types.ts +30 -0
  81. package/composables/storage/useStorage.ts +108 -0
  82. package/composables/store/defineUserStore.ts +122 -0
  83. package/composables/store/index.ts +11 -0
  84. package/composables/types.ts +118 -0
  85. package/dist/components/crudPage/KCrudPage.d.ts +14 -0
  86. package/dist/components/crudPage/index.d.ts +9 -0
  87. package/dist/components/editableTable/KEditableTable.d.ts +146 -0
  88. package/dist/components/editableTable/index.d.ts +10 -0
  89. package/dist/components/formPage/KApprovalDialog.d.ts +99 -0
  90. package/dist/components/formPage/KFormCard.d.ts +49 -0
  91. package/dist/components/formPage/KFormPage.d.ts +14 -0
  92. package/dist/components/formPage/KMasterDetailPage.d.ts +14 -0
  93. package/dist/components/formPage/KStickyActionBar.d.ts +16 -0
  94. package/dist/components/formPage/index.d.ts +14 -0
  95. package/dist/components/layout/KLayout.d.ts +7 -4
  96. package/dist/composables/auth/useAuth.d.ts +5 -5
  97. package/dist/composables/error/types.d.ts +2 -1
  98. package/dist/composables/form/index.d.ts +12 -0
  99. package/dist/composables/form/renderFormField.d.ts +11 -0
  100. package/dist/composables/form/types.d.ts +104 -0
  101. package/dist/composables/form/useFormPage.d.ts +38 -0
  102. package/dist/composables/index.d.ts +2 -0
  103. package/dist/composables/page/index.d.ts +10 -0
  104. package/dist/composables/page/types.d.ts +61 -0
  105. package/dist/composables/page/useCrudPage.d.ts +14 -0
  106. package/dist/composables/request/createRequest.d.ts +2 -0
  107. package/dist/composables/request/requestBuilder.d.ts +2 -0
  108. package/dist/composables/search/index.d.ts +10 -0
  109. package/dist/composables/search/useAutoCompleteSearch.d.ts +50 -0
  110. package/dist/crud.css +2499 -663
  111. package/dist/crud.js +11512 -2910
  112. package/dist/index.d.ts +11 -0
  113. package/dist/setup.d.ts +2 -2
  114. package/index.ts +144 -0
  115. package/package.json +20 -19
  116. package/setup.ts +288 -0
  117. package/tsconfig.json +12 -0
  118. package/vite.config.build.ts +52 -0
@@ -0,0 +1,143 @@
1
+ /**
2
+ * @description 控制门 — 防抖/节流/去重
3
+ * @author 阿怪
4
+ * @date 2026/3/15
5
+ * @version v0.0.1
6
+ *
7
+ * 江湖的业务千篇一律,复杂的代码好几百行。
8
+ */
9
+
10
+ import type { ControlPolicy, RequestMethod } from './types';
11
+
12
+ // ────────────────────────────────────────────────────────────────────────────
13
+ // CacheKey 生成
14
+ // ────────────────────────────────────────────────────────────────────────────
15
+
16
+ /** 根据请求方法、URL 和参数生成唯一的缓存 key */
17
+ export function buildCacheKey(method: RequestMethod, url: string, body?: unknown): string {
18
+ const bodyStr = body !== undefined && body !== null ? stableStringify(body) : '';
19
+ return `${method}:${url}:${bodyStr}`;
20
+ }
21
+
22
+ /** 稳定 JSON 序列化(key 排序),确保相同对象生成相同字符串 */
23
+ function stableStringify(obj: unknown): string {
24
+ if (obj === null || obj === undefined) return '';
25
+ if (typeof obj !== 'object') return String(obj);
26
+ if (obj instanceof FormData || obj instanceof Blob || obj instanceof File) return '';
27
+ if (Array.isArray(obj)) return `[${obj.map(stableStringify).join(',')}]`;
28
+
29
+ const sorted = Object.keys(obj as Record<string, unknown>).sort();
30
+ const pairs = sorted.map(k => `${JSON.stringify(k)}:${stableStringify((obj as Record<string, unknown>)[k])}`);
31
+ return `{${pairs.join(',')}}`;
32
+ }
33
+
34
+ // ────────────────────────────────────────────────────────────────────────────
35
+ // ControlGate
36
+ // ────────────────────────────────────────────────────────────────────────────
37
+
38
+ interface DebounceEntry {
39
+ timer: ReturnType<typeof setTimeout>;
40
+ reject: (reason: Error) => void;
41
+ }
42
+
43
+ export class ControlGate {
44
+ /** 防抖:取消前一个 timer */
45
+ private debounceMap = new Map<string, DebounceEntry>();
46
+ /** 节流:记录上次执行时间 */
47
+ private throttleMap = new Map<string, number>();
48
+ /** 去重:共享同一个 Promise */
49
+ private deduplicateMap = new Map<string, Promise<unknown>>();
50
+
51
+ /**
52
+ * 通过控制门执行请求。
53
+ * 按配置依次应用防抖、节流、去重策略。
54
+ */
55
+ async execute<T>(key: string, fn: () => Promise<T>, policy: ControlPolicy): Promise<T> {
56
+ // 防抖
57
+ if (policy.debounce !== undefined && policy.debounce > 0) {
58
+ return this.withDebounce(key, fn, policy.debounce);
59
+ }
60
+
61
+ // 节流
62
+ if (policy.throttle !== undefined && policy.throttle > 0) {
63
+ await this.waitThrottle(key, policy.throttle);
64
+ }
65
+
66
+ // 去重
67
+ if (policy.deduplicate) {
68
+ return this.withDeduplicate(key, fn) as Promise<T>;
69
+ }
70
+
71
+ return fn();
72
+ }
73
+
74
+ /** 清理所有挂起的 timer 和状态 */
75
+ dispose(): void {
76
+ for (const entry of this.debounceMap.values()) {
77
+ clearTimeout(entry.timer);
78
+ entry.reject(new Error('ControlGate 已销毁'));
79
+ }
80
+ this.debounceMap.clear();
81
+ this.throttleMap.clear();
82
+ this.deduplicateMap.clear();
83
+ }
84
+
85
+ // ── 防抖 ──────────────────────────────────────────────────────────────────
86
+
87
+ private withDebounce<T>(key: string, fn: () => Promise<T>, delay: number): Promise<T> {
88
+ // 取消前一个 pending 请求
89
+ const prev = this.debounceMap.get(key);
90
+ if (prev) {
91
+ clearTimeout(prev.timer);
92
+ prev.reject(new Error('请求已被防抖取消'));
93
+ }
94
+
95
+ return new Promise<T>((resolve, reject) => {
96
+ const timer = setTimeout(async () => {
97
+ this.debounceMap.delete(key);
98
+ try {
99
+ resolve(await fn());
100
+ } catch (e) {
101
+ reject(e);
102
+ }
103
+ }, delay);
104
+
105
+ this.debounceMap.set(key, { timer, reject });
106
+ });
107
+ }
108
+
109
+ // ── 节流 ──────────────────────────────────────────────────────────────────
110
+
111
+ private waitThrottle(key: string, interval: number): Promise<void> {
112
+ const lastTime = this.throttleMap.get(key) ?? 0;
113
+ const now = Date.now();
114
+ const elapsed = now - lastTime;
115
+
116
+ if (elapsed >= interval) {
117
+ this.throttleMap.set(key, now);
118
+ return Promise.resolve();
119
+ }
120
+
121
+ const waitTime = interval - elapsed;
122
+ return new Promise(resolve => {
123
+ setTimeout(() => {
124
+ this.throttleMap.set(key, Date.now());
125
+ resolve();
126
+ }, waitTime);
127
+ });
128
+ }
129
+
130
+ // ── 去重 ──────────────────────────────────────────────────────────────────
131
+
132
+ private withDeduplicate<T>(key: string, fn: () => Promise<T>): Promise<unknown> {
133
+ const existing = this.deduplicateMap.get(key);
134
+ if (existing) return existing;
135
+
136
+ const promise = fn().finally(() => {
137
+ this.deduplicateMap.delete(key);
138
+ });
139
+
140
+ this.deduplicateMap.set(key, promise);
141
+ return promise;
142
+ }
143
+ }
@@ -0,0 +1,173 @@
1
+ /**
2
+ * @description 请求客户端工厂
3
+ * @author 阿怪
4
+ * @date 2026/3/15
5
+ * @version v0.0.1
6
+ *
7
+ * 江湖的业务千篇一律,复杂的代码好几百行。
8
+ */
9
+
10
+ import type { RequestMethod, RequestOptions } from './types';
11
+ import { ControlGate } from './controlGate';
12
+ import { FetchTransport } from './transport/fetchTransport';
13
+ import { RequestBuilder, type RequestBuilderContext } from './requestBuilder';
14
+
15
+ // ────────────────────────────────────────────────────────────────────────────
16
+ // RequestClient
17
+ // ────────────────────────────────────────────────────────────────────────────
18
+
19
+ export interface RequestClient {
20
+ /** 请求基础 URL */
21
+ readonly baseURL: string;
22
+ /** 简便方法,直接返回 Promise<T> */
23
+ send<T>(method: RequestMethod, url: string, body?: unknown): Promise<T>;
24
+ /** GET 请求 */
25
+ get<T>(url: string, params?: Record<string, unknown>): Promise<T>;
26
+ /** POST 请求 */
27
+ post<T>(url: string, body?: unknown): Promise<T>;
28
+ /** PUT 请求 */
29
+ put<T>(url: string, body?: unknown): Promise<T>;
30
+ /** PATCH 请求 */
31
+ patch<T>(url: string, body?: unknown): Promise<T>;
32
+ /** DELETE 请求 */
33
+ delete<T>(url: string, body?: unknown): Promise<T>;
34
+ /** 返回 RequestBuilder,支持链式配置 */
35
+ request(method: RequestMethod, url: string, body?: unknown): RequestBuilder;
36
+ /** 注册 AbortController,供外部管理请求取消 */
37
+ registerAbortController(controller: AbortController): void;
38
+ /** 取消所有 pending 请求 */
39
+ abortAll(): void;
40
+ /** 销毁客户端,清理所有资源 */
41
+ dispose(): void;
42
+ }
43
+
44
+ // ────────────────────────────────────────────────────────────────────────────
45
+ // createRequest
46
+ // ────────────────────────────────────────────────────────────────────────────
47
+
48
+ export function createRequest(options: RequestOptions = {}): RequestClient {
49
+ const transport = options.transport ?? new FetchTransport();
50
+ const controlGate = new ControlGate();
51
+ const abortControllers = new Set<AbortController>();
52
+
53
+ const context: RequestBuilderContext = {
54
+ transport,
55
+ controlGate,
56
+ baseURL: options.baseURL ?? '',
57
+ defaultHeaders: options.headers ?? {},
58
+ getToken: options.getToken,
59
+ onUnauthorized: options.onUnauthorized,
60
+ responseInterceptor: options.responseInterceptor,
61
+ feedback: options.feedback,
62
+ registerAbort: (controller: AbortController) => {
63
+ abortControllers.add(controller);
64
+ // 完成后自动移除
65
+ const originalAbort = controller.abort.bind(controller);
66
+ controller.abort = (reason?: unknown) => {
67
+ abortControllers.delete(controller);
68
+ return originalAbort(reason);
69
+ };
70
+ },
71
+ };
72
+
73
+ function createBuilder(method: RequestMethod, url: string, body?: unknown): RequestBuilder {
74
+ const builder = new RequestBuilder(context, method, url, body);
75
+
76
+ // 应用默认配置
77
+ const dc = options.defaultConfig;
78
+ if (dc?.cache) builder.cache(dc.cache);
79
+ if (dc?.retry) builder.retry(dc.retry);
80
+ if (dc?.timeout) builder.timeout(dc.timeout);
81
+ if (dc?.totalTimeout) builder.totalTimeout(dc.totalTimeout);
82
+ if (dc?.lifecycle) builder.lifecycle(dc.lifecycle);
83
+ if (dc?.control?.debounce) builder.debounce(dc.control.debounce);
84
+ if (dc?.control?.throttle) builder.throttle(dc.control.throttle);
85
+ if (dc?.control?.deduplicate) builder.deduplicate(dc.control.deduplicate);
86
+
87
+ return builder;
88
+ }
89
+
90
+ const client: RequestClient = {
91
+ baseURL: context.baseURL,
92
+
93
+ send: <T>(method: RequestMethod, url: string, body?: unknown) =>
94
+ createBuilder(method, url, body).execute<T>(),
95
+
96
+ get: <T>(url: string, params?: Record<string, unknown>) =>
97
+ createBuilder('GET', url, params).execute<T>(),
98
+
99
+ post: <T>(url: string, body?: unknown) =>
100
+ createBuilder('POST', url, body).execute<T>(),
101
+
102
+ put: <T>(url: string, body?: unknown) =>
103
+ createBuilder('PUT', url, body).execute<T>(),
104
+
105
+ patch: <T>(url: string, body?: unknown) =>
106
+ createBuilder('PATCH', url, body).execute<T>(),
107
+
108
+ delete: <T>(url: string, body?: unknown) =>
109
+ createBuilder('DELETE', url, body).execute<T>(),
110
+
111
+ request: (method: RequestMethod, url: string, body?: unknown) =>
112
+ createBuilder(method, url, body),
113
+
114
+ registerAbortController: (controller: AbortController) => {
115
+ abortControllers.add(controller);
116
+ },
117
+
118
+ abortAll: () => {
119
+ for (const controller of abortControllers) {
120
+ controller.abort('客户端取消所有请求');
121
+ }
122
+ abortControllers.clear();
123
+ },
124
+
125
+ dispose: () => {
126
+ client.abortAll();
127
+ controlGate.dispose();
128
+ },
129
+ };
130
+
131
+ return client;
132
+ }
133
+
134
+ // ────────────────────────────────────────────────────────────────────────────
135
+ // 预设工厂
136
+ // ────────────────────────────────────────────────────────────────────────────
137
+
138
+ /** 默认配置:适用于大多数场景 */
139
+ export function createDefaultRequest(options: RequestOptions = {}): RequestClient {
140
+ return createRequest({
141
+ ...options,
142
+ defaultConfig: {
143
+ retry: { type: 'exponential', maxAttempts: 3, initialDelay: 300, multiplier: 2, maxDelay: 5000 },
144
+ timeout: 30000,
145
+ ...options.defaultConfig,
146
+ },
147
+ });
148
+ }
149
+
150
+ /** 严格配置:无重试、短超时 */
151
+ export function createStrictRequest(options: RequestOptions = {}): RequestClient {
152
+ return createRequest({
153
+ ...options,
154
+ defaultConfig: {
155
+ retry: { type: 'none' },
156
+ timeout: 10000,
157
+ ...options.defaultConfig,
158
+ },
159
+ });
160
+ }
161
+
162
+ /** 宽松配置:多重试、长超时 */
163
+ export function createPermissiveRequest(options: RequestOptions = {}): RequestClient {
164
+ return createRequest({
165
+ ...options,
166
+ defaultConfig: {
167
+ retry: { type: 'exponential', maxAttempts: 5, initialDelay: 500, multiplier: 2, maxDelay: 30000 },
168
+ timeout: 60000,
169
+ totalTimeout: 120000,
170
+ ...options.defaultConfig,
171
+ },
172
+ });
173
+ }
@@ -0,0 +1,71 @@
1
+ /**
2
+ * @description 请求模块统一导出
3
+ * @author 阿怪
4
+ * @date 2026/3/15
5
+ * @version v0.0.1
6
+ *
7
+ * 江湖的业务千篇一律,复杂的代码好几百行。
8
+ */
9
+
10
+ // ── 类型导出 ──────────────────────────────────────────────────────────────
11
+
12
+ export type {
13
+ RequestMethod,
14
+ ControlPolicy,
15
+ CachePolicy,
16
+ RetryPolicy,
17
+ Lifecycle,
18
+ RequestConfig,
19
+ WrappedResponse,
20
+ NetworkError,
21
+ UserFeedbackHandler,
22
+ Transport,
23
+ TransportRequest,
24
+ TransportResponse,
25
+ RequestOptions,
26
+ OrchestratorNode,
27
+ FailureStrategy,
28
+ UploadOptions,
29
+ UploaderOptions,
30
+ } from './types';
31
+
32
+ export { BusinessError, NetworkRequestError } from './types';
33
+
34
+ // ── 控制门 ────────────────────────────────────────────────────────────────
35
+
36
+ export { ControlGate, buildCacheKey } from './controlGate';
37
+
38
+ // ── 传输层 ────────────────────────────────────────────────────────────────
39
+
40
+ export { FetchTransport } from './transport/fetchTransport';
41
+ export { XhrTransport } from './transport/xhrTransport';
42
+
43
+ // ── 请求构建器 ──────────────────────────────────────────────────────────────
44
+
45
+ export { RequestBuilder } from './requestBuilder';
46
+ export type { RequestBuilderContext } from './requestBuilder';
47
+
48
+ // ── 请求客户端 ──────────────────────────────────────────────────────────────
49
+
50
+ export {
51
+ createRequest,
52
+ createDefaultRequest,
53
+ createStrictRequest,
54
+ createPermissiveRequest,
55
+ } from './createRequest';
56
+ export type { RequestClient } from './createRequest';
57
+
58
+ // ── 编排器 ────────────────────────────────────────────────────────────────
59
+
60
+ export { orchestrate, createNode, after } from './orchestrator';
61
+ export type { OrchestratorResult } from './orchestrator';
62
+
63
+ // ── Vue Composables ────────────────────────────────────────────────────────
64
+
65
+ export { useRequest, usePolling, useBatchLoader } from './composables';
66
+ export type { UsePollingReturn, BatchLoaderOptions, UseBatchLoaderReturn } from './composables';
67
+
68
+ // ── 上传 ──────────────────────────────────────────────────────────────────
69
+
70
+ export { createUploader } from './upload';
71
+ export type { UploadHandle, Uploader } from './upload';
@@ -0,0 +1,145 @@
1
+ /**
2
+ * @description DAG 编排器 — 拓扑排序 + 分层并发执行
3
+ * @author 阿怪
4
+ * @date 2026/3/15
5
+ * @version v0.0.1
6
+ *
7
+ * 江湖的业务千篇一律,复杂的代码好几百行。
8
+ */
9
+
10
+ import type { FailureStrategy, OrchestratorNode } from './types';
11
+
12
+ // ────────────────────────────────────────────────────────────────────────────
13
+ // 编排结果
14
+ // ────────────────────────────────────────────────────────────────────────────
15
+
16
+ export interface OrchestratorResult {
17
+ /** 所有成功节点的结果 */
18
+ results: Map<string, unknown>;
19
+ /** 失败节点的错误(仅 continueOnError 模式下存在) */
20
+ errors: Map<string, Error>;
21
+ }
22
+
23
+ // ────────────────────────────────────────────────────────────────────────────
24
+ // 拓扑排序
25
+ // ────────────────────────────────────────────────────────────────────────────
26
+
27
+ /** 拓扑排序,返回按层分组的节点 ID */
28
+ function topologicalSort(nodes: OrchestratorNode[]): string[][] {
29
+ const nodeMap = new Map(nodes.map(n => [n.id, n]));
30
+ const inDegree = new Map<string, number>();
31
+ const adjacency = new Map<string, string[]>();
32
+
33
+ // 初始化
34
+ for (const node of nodes) {
35
+ inDegree.set(node.id, 0);
36
+ adjacency.set(node.id, []);
37
+ }
38
+
39
+ // 构建入度表和邻接表
40
+ for (const node of nodes) {
41
+ for (const dep of node.dependencies) {
42
+ if (!nodeMap.has(dep)) {
43
+ throw new Error(`[orchestrator] 节点 "${node.id}" 依赖的 "${dep}" 不存在`);
44
+ }
45
+ adjacency.get(dep)!.push(node.id);
46
+ inDegree.set(node.id, (inDegree.get(node.id) ?? 0) + 1);
47
+ }
48
+ }
49
+
50
+ // BFS 分层
51
+ const layers: string[][] = [];
52
+ let queue = nodes.filter(n => inDegree.get(n.id) === 0).map(n => n.id);
53
+
54
+ while (queue.length > 0) {
55
+ layers.push([...queue]);
56
+ const nextQueue: string[] = [];
57
+
58
+ for (const id of queue) {
59
+ for (const neighbor of adjacency.get(id)!) {
60
+ const degree = inDegree.get(neighbor)! - 1;
61
+ inDegree.set(neighbor, degree);
62
+ if (degree === 0) {
63
+ nextQueue.push(neighbor);
64
+ }
65
+ }
66
+ }
67
+
68
+ queue = nextQueue;
69
+ }
70
+
71
+ // 检测循环依赖
72
+ const totalSorted = layers.reduce((sum, layer) => sum + layer.length, 0);
73
+ if (totalSorted !== nodes.length) {
74
+ throw new Error('[orchestrator] 检测到循环依赖');
75
+ }
76
+
77
+ return layers;
78
+ }
79
+
80
+ // ────────────────────────────────────────────────────────────────────────────
81
+ // orchestrate
82
+ // ────────────────────────────────────────────────────────────────────────────
83
+
84
+ /**
85
+ * 执行 DAG 编排。
86
+ * 拓扑排序 → 分层 → 同层 Promise.all 并发 → 收集结果。
87
+ */
88
+ export async function orchestrate(
89
+ nodes: OrchestratorNode[],
90
+ failureStrategy: FailureStrategy = 'failFast',
91
+ ): Promise<OrchestratorResult> {
92
+ if (nodes.length === 0) {
93
+ return { results: new Map(), errors: new Map() };
94
+ }
95
+
96
+ const nodeMap = new Map(nodes.map(n => [n.id, n]));
97
+ const layers = topologicalSort(nodes);
98
+ const results = new Map<string, unknown>();
99
+ const errors = new Map<string, Error>();
100
+
101
+ for (const layer of layers) {
102
+ const tasks = layer.map(async (id) => {
103
+ const node = nodeMap.get(id)!;
104
+ try {
105
+ const result = await node.execute();
106
+ results.set(id, result);
107
+ } catch (e) {
108
+ const error = e instanceof Error ? e : new Error(String(e));
109
+ errors.set(id, error);
110
+
111
+ if (failureStrategy === 'failFast') {
112
+ throw error;
113
+ }
114
+ }
115
+ });
116
+
117
+ if (failureStrategy === 'failFast') {
118
+ await Promise.all(tasks);
119
+ } else {
120
+ await Promise.allSettled(tasks);
121
+ }
122
+ }
123
+
124
+ return { results, errors };
125
+ }
126
+
127
+ // ────────────────────────────────────────────────────────────────────────────
128
+ // 辅助:节点构建
129
+ // ────────────────────────────────────────────────────────────────────────────
130
+
131
+ /** 创建编排节点 */
132
+ export function createNode<T>(
133
+ id: string,
134
+ execute: () => Promise<T>,
135
+ dependencies: string[] = [],
136
+ ): OrchestratorNode<T> {
137
+ return { id, execute, dependencies };
138
+ }
139
+
140
+ /** 声明依赖关系 — 返回带有 dependencies 的节点配置 */
141
+ export function after(...deps: string[]) {
142
+ return <T>(id: string, execute: () => Promise<T>): OrchestratorNode<T> => {
143
+ return { id, execute, dependencies: deps };
144
+ };
145
+ }