@tmagic/editor 1.8.0-beta.19 → 1.8.0-beta.20

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.
@@ -1,6 +1,6 @@
1
1
  import KeyController, { KeyControllerEvent } from 'keycon';
2
2
 
3
- import { isPage, isPageFragment } from '@tmagic/utils';
3
+ import { isPageOrFragment } from '@tmagic/utils';
4
4
 
5
5
  import { KeyBindingCacheItem, KeyBindingCommand, KeyBindingItem } from '@editor/type';
6
6
 
@@ -19,7 +19,7 @@ class Keybinding extends BaseService {
19
19
  [KeyBindingCommand.DELETE_NODE]: () => {
20
20
  const nodes = editorService.get('nodes');
21
21
 
22
- if (!nodes || isPage(nodes[0]) || isPageFragment(nodes[0])) return;
22
+ if (!nodes || isPageOrFragment(nodes[0])) return;
23
23
  editorService.remove(nodes, { historySource: 'shortcut' });
24
24
  },
25
25
  [KeyBindingCommand.COPY_NODE]: () => {
@@ -29,7 +29,7 @@ class Keybinding extends BaseService {
29
29
  [KeyBindingCommand.CUT_NODE]: () => {
30
30
  const nodes = editorService.get('nodes');
31
31
 
32
- if (!nodes || isPage(nodes[0]) || isPageFragment(nodes[0])) return;
32
+ if (!nodes || isPageOrFragment(nodes[0])) return;
33
33
  editorService.copy(nodes);
34
34
  editorService.remove(nodes, { historySource: 'shortcut' });
35
35
  },
@@ -2,7 +2,7 @@ import { computed, markRaw, type ShallowRef } from 'vue';
2
2
  import { CopyDocument, Delete, DocumentCopy } from '@element-plus/icons-vue';
3
3
 
4
4
  import { cloneDeep, Id, MContainer, NodeType } from '@tmagic/core';
5
- import { calcValueByFontsize, isPage, isPageFragment } from '@tmagic/utils';
5
+ import { calcValueByFontsize, isPage, isPageOrFragment } from '@tmagic/utils';
6
6
 
7
7
  import ContentMenu from '@editor/components/ContentMenu.vue';
8
8
  import type { HistoryOpSource, MenuButton, Services } from '@editor/type';
@@ -20,7 +20,7 @@ export const useDeleteMenu = (historySource?: HistoryOpSource): MenuButton => ({
20
20
  icon: Delete,
21
21
  display: ({ editorService }) => {
22
22
  const node = editorService.get('node');
23
- return node?.type !== NodeType.ROOT && !isPage(node) && !isPageFragment(node);
23
+ return node?.type !== NodeType.ROOT && !isPageOrFragment(node);
24
24
  },
25
25
  handler: ({ editorService }) => {
26
26
  const nodes = editorService.get('nodes');
@@ -0,0 +1,149 @@
1
+ import serialize from 'serialize-javascript';
2
+
3
+ import type { Id, MApp } from '@tmagic/core';
4
+
5
+ import { error } from '../logger';
6
+
7
+ import type { CollectWorkerPayload, CollectWorkerRequest, CollectWorkerResponse, DepsData } from './worker';
8
+ import CollectWorker from './worker.ts?worker&inline';
9
+
10
+ export interface CollectWorkerResult {
11
+ deps: DepsData;
12
+ nodeIds: Id[];
13
+ }
14
+
15
+ /**
16
+ * 依赖收集 worker 的主线程客户端
17
+ *
18
+ * 依赖收集需要深度遍历整棵节点树并对每个属性做匹配,节点/数据源多时会长时间占用主线程导致页面卡死,
19
+ * 这里把遍历匹配放到 worker 中执行,主线程只负责把结果写回 target。
20
+ *
21
+ * 全量收集与增量收集共用一个常驻 worker:增量收集调用频繁(每次节点更新都会触发),
22
+ * 按次创建 worker 的启动开销无法接受,也容易漏掉销毁导致线程泄漏。
23
+ */
24
+ export class CollectWorkerClient {
25
+ private worker: Worker | null = null;
26
+
27
+ private seed = 0;
28
+
29
+ private pending = new Map<number, (response: CollectWorkerResponse | null) => void>();
30
+
31
+ public get isSupported() {
32
+ return typeof Worker !== 'undefined';
33
+ }
34
+
35
+ /**
36
+ * 在 worker 中全量收集整个 DSL 的依赖,target 由 worker 根据 DSL 重建
37
+ * @returns 收集结果,返回 null 表示 worker 不可用或执行失败
38
+ */
39
+ public collectDsl(dsl: MApp): Promise<DepsData | null> {
40
+ return this.request((id) => ({ id, dsl: serialize(dsl) })).then((response) => response?.deps || null);
41
+ }
42
+
43
+ /**
44
+ * 在 worker 中增量收集指定节点的依赖,target 以可序列化描述的形式传入
45
+ * @returns 收集结果,返回 null 表示 worker 不可用或执行失败,调用方需要回退到主线程收集
46
+ */
47
+ public collect(payload: CollectWorkerPayload): Promise<CollectWorkerResult | null> {
48
+ return this.request((id) => ({ id, payload: serialize(payload) })).then((response) =>
49
+ response ? { deps: response.deps || {}, nodeIds: response.nodeIds || [] } : null,
50
+ );
51
+ }
52
+
53
+ public terminate() {
54
+ this.worker?.terminate();
55
+ this.worker = null;
56
+ this.settleAllPending();
57
+ }
58
+
59
+ /**
60
+ * 丢弃在途请求并重建 worker
61
+ * clearIdleTasks / reset 中断收集时调用,避免已 abort 的长任务继续占着常驻 worker 拖慢后续收集
62
+ */
63
+ public abort() {
64
+ this.terminate();
65
+ }
66
+
67
+ /**
68
+ * worker 按收到的顺序逐个处理请求,因此请求间不会互相打断,用 id 把结果分发回各自的调用方
69
+ */
70
+ private request(createRequest: (id: number) => CollectWorkerRequest): Promise<CollectWorkerResponse | null> {
71
+ const worker = this.getWorker();
72
+
73
+ if (!worker) {
74
+ return Promise.resolve(null);
75
+ }
76
+
77
+ this.seed += 1;
78
+ const id = this.seed;
79
+
80
+ return new Promise<CollectWorkerResponse | null>((resolve) => {
81
+ this.pending.set(id, resolve);
82
+
83
+ try {
84
+ // 节点配置中可能存在函数等无法结构化克隆的值,需要先序列化再传递
85
+ worker.postMessage(createRequest(id));
86
+ } catch (e) {
87
+ error('magic editor: 依赖收集 worker 通信失败', e);
88
+ this.pending.delete(id);
89
+ resolve(null);
90
+ }
91
+ });
92
+ }
93
+
94
+ private getWorker() {
95
+ if (!this.isSupported) {
96
+ return null;
97
+ }
98
+
99
+ if (this.worker) {
100
+ return this.worker;
101
+ }
102
+
103
+ try {
104
+ const worker: Worker = new CollectWorker();
105
+ worker.onmessage = (e: MessageEvent<CollectWorkerResponse>) => {
106
+ this.handleResponse(e.data);
107
+ };
108
+ // worker 整体异常时无法定位到具体请求,只能结算全部在途请求并重建 worker
109
+ worker.onerror = () => {
110
+ this.handleFatalError();
111
+ };
112
+ worker.onmessageerror = () => {
113
+ this.handleFatalError();
114
+ };
115
+ this.worker = worker;
116
+ } catch (e) {
117
+ error('magic editor: 依赖收集 worker 创建失败', e);
118
+ return null;
119
+ }
120
+
121
+ return this.worker;
122
+ }
123
+
124
+ private handleResponse(response: CollectWorkerResponse) {
125
+ const resolve = this.pending.get(response?.id);
126
+
127
+ if (!resolve) {
128
+ return;
129
+ }
130
+
131
+ this.pending.delete(response.id);
132
+ resolve(response.failed ? null : response);
133
+ }
134
+
135
+ private handleFatalError() {
136
+ this.worker?.terminate();
137
+ this.worker = null;
138
+ this.settleAllPending();
139
+ }
140
+
141
+ private settleAllPending() {
142
+ const resolvers = [...this.pending.values()];
143
+ this.pending.clear();
144
+
145
+ for (const resolve of resolvers) {
146
+ resolve(null);
147
+ }
148
+ }
149
+ }
@@ -3,30 +3,85 @@ import {
3
3
  createDataSourceCondTarget,
4
4
  createDataSourceMethodTarget,
5
5
  createDataSourceTarget,
6
+ createTargetByDescriptor,
6
7
  type DepData,
8
+ type DepExtendedData,
7
9
  DepTargetType,
8
10
  type Id,
9
11
  type MApp,
12
+ type TargetDescriptor,
13
+ type TargetNode,
10
14
  traverseTarget,
11
15
  Watcher,
12
16
  } from '@tmagic/core';
13
17
 
14
18
  import { error } from '../logger';
15
19
 
16
- onmessage = (e) => {
17
- const watcher = new Watcher({ initialTargets: {} });
20
+ export type DepsData = Record<string, Record<Id, DepData>>;
18
21
 
19
- const { dsl } = e.data;
22
+ /** 全量收集请求:worker 中根据 DSL 重建代码块/数据源 target */
23
+ export interface DslCollectRequest {
24
+ /** 请求 id,用于把结果与调用方对应起来 */
25
+ id: number;
26
+ /** serialize-javascript 序列化后的 MApp */
27
+ dsl: string;
28
+ }
20
29
 
30
+ export interface CollectWorkerPayload {
31
+ /** 待收集的节点 */
32
+ nodes: TargetNode[];
33
+ /** 参与本次收集的 target 描述,worker 中据此重建 target */
34
+ targets: TargetDescriptor[];
35
+ depExtendedData: DepExtendedData;
36
+ deep: boolean;
37
+ }
38
+
39
+ /** 增量收集请求:target 由主线程以可序列化描述的形式传入 */
40
+ export interface NodesCollectRequest {
41
+ id: number;
42
+ /** serialize-javascript 序列化后的 CollectWorkerPayload */
43
+ payload: string;
44
+ }
45
+
46
+ export type CollectWorkerRequest = DslCollectRequest | NodesCollectRequest;
47
+
48
+ export interface CollectWorkerResponse {
49
+ id: number;
50
+ /** 收集结果,结构为 { [target.type]: { [target.id]: deps } } */
51
+ deps: DepsData;
52
+ /** 增量收集时本次覆盖到的节点 id(deep=true 时含子孙),主线程据此删除旧依赖 */
53
+ nodeIds?: Id[];
54
+ /** 收集失败,调用方需要回退到主线程收集 */
55
+ failed?: boolean;
56
+ }
57
+
58
+ /**
59
+ * 收集本次会覆盖到的节点 id
60
+ * 必须与 deep 一致:deep=false 时不能带上子孙 id,否则写回阶段 removeDep 会清掉未重收的子节点依赖
61
+ */
62
+ const getNodeIds = (nodes: TargetNode[], deep: boolean, ids: Id[] = []) => {
63
+ for (const node of nodes) {
64
+ ids.push(node.id);
65
+
66
+ if (deep && Array.isArray(node.items) && node.items.length) {
67
+ getNodeIds(node.items, deep, ids);
68
+ }
69
+ }
70
+
71
+ return ids;
72
+ };
73
+
74
+ const collectDsl = ({ id, dsl }: DslCollectRequest) => {
21
75
  try {
22
76
  // eslint-disable-next-line no-eval
23
77
  const mApp: MApp = eval(`(${dsl})`);
24
78
 
25
79
  if (!mApp) {
26
- postMessage({});
80
+ postMessage({ id, deps: {} });
81
+ return;
27
82
  }
28
83
 
29
- watcher.clearTargets();
84
+ const watcher = new Watcher({ initialTargets: {} });
30
85
 
31
86
  if (mApp.codeBlocks) {
32
87
  for (const [id, code] of Object.entries(mApp.codeBlocks)) {
@@ -48,7 +103,7 @@ onmessage = (e) => {
48
103
  watcher.collectItems(page, targets, { pageId: page.id }, true);
49
104
  }
50
105
 
51
- const data: Record<string, Record<Id, DepData>> = {
106
+ const deps: DepsData = {
52
107
  [DepTargetType.DATA_SOURCE]: {},
53
108
  [DepTargetType.DATA_SOURCE_METHOD]: {},
54
109
  [DepTargetType.DATA_SOURCE_COND]: {},
@@ -56,12 +111,61 @@ onmessage = (e) => {
56
111
  };
57
112
 
58
113
  traverseTarget(watcher.getTargetsList(), (target) => {
59
- data[target.type][target.id] = target.deps;
114
+ deps[target.type][target.id] = target.deps;
60
115
  });
61
116
 
62
- postMessage(data);
117
+ const response: CollectWorkerResponse = { id, deps };
118
+ postMessage(response);
63
119
  } catch (e: any) {
64
120
  error(e);
65
- postMessage({});
121
+ const response: CollectWorkerResponse = { id, deps: {}, failed: true };
122
+ postMessage(response);
66
123
  }
67
124
  };
125
+
126
+ const collectNodes = ({ id, payload }: NodesCollectRequest) => {
127
+ try {
128
+ // eslint-disable-next-line no-eval
129
+ const { nodes, targets: descriptors, depExtendedData, deep }: CollectWorkerPayload = eval(`(${payload})`);
130
+
131
+ const watcher = new Watcher({ initialTargets: {} });
132
+ // worker 中 target 均为新建(deps 为空),无需删除阶段,直接批量收集
133
+ const targets = descriptors.map((descriptor) => createTargetByDescriptor(descriptor));
134
+
135
+ for (const node of nodes) {
136
+ watcher.collectItems(node, targets, depExtendedData, deep);
137
+ }
138
+
139
+ const deps: DepsData = {};
140
+ for (const target of targets) {
141
+ if (!deps[target.type]) {
142
+ deps[target.type] = {};
143
+ }
144
+ deps[target.type][target.id] = target.deps;
145
+ }
146
+
147
+ const response: CollectWorkerResponse = { id, deps, nodeIds: getNodeIds(nodes, deep) };
148
+ postMessage(response);
149
+ } catch (e: any) {
150
+ error(e);
151
+ const response: CollectWorkerResponse = { id, deps: {}, nodeIds: [], failed: true };
152
+ postMessage(response);
153
+ }
154
+ };
155
+
156
+ /**
157
+ * 依赖收集 worker
158
+ *
159
+ * 依赖收集需要深度遍历整棵节点树并对每个属性做 target 匹配,节点/数据源多时会长时间占用主线程导致页面卡死,
160
+ * 因此把遍历匹配放到 worker 中执行。
161
+ * 全量与增量收集共用一个常驻 worker:收集逻辑只打进一份 inline worker 产物,
162
+ * 也不会因为频繁收集而反复创建线程,每个请求用 id 与调用方对应。
163
+ */
164
+ onmessage = (e: MessageEvent<CollectWorkerRequest>) => {
165
+ if ('dsl' in e.data) {
166
+ collectDsl(e.data);
167
+ return;
168
+ }
169
+
170
+ collectNodes(e.data);
171
+ };
@@ -3,7 +3,7 @@ import { isEmpty } from 'lodash-es';
3
3
 
4
4
  import type { Id, MContainer, MNode } from '@tmagic/core';
5
5
  import { NodeType } from '@tmagic/core';
6
- import { calcValueByFontsize, getElById, isPage, isPageFragment } from '@tmagic/utils';
6
+ import { calcValueByFontsize, getElById, isPage, isPageOrFragment } from '@tmagic/utils';
7
7
 
8
8
  import editorService from '@editor/services/editor';
9
9
  import propsService from '@editor/services/props';
@@ -58,7 +58,7 @@ export const beforePaste = (position: PastePosition, config: MNode[], doc?: Docu
58
58
  };
59
59
  }
60
60
  const root = editorService.get('root');
61
- if ((isPage(pasteConfig) || isPageFragment(pasteConfig)) && root) {
61
+ if (isPageOrFragment(pasteConfig) && root) {
62
62
  pasteConfig.name = generatePageNameByApp(root, isPage(pasteConfig) ? NodeType.PAGE : NodeType.PAGE_FRAGMENT);
63
63
  }
64
64
  return pasteConfig as MNode;
package/types/index.d.ts CHANGED
@@ -503,6 +503,9 @@ declare class DataSource extends BaseService {
503
503
  type DataSourceService = DataSource;
504
504
  declare const _default$42: DataSource;
505
505
  //#endregion
506
+ //#region temp/packages/editor/src/utils/dep/worker.d.ts
507
+ type DepsData = Record<string, Record<Id, DepData>>;
508
+ //#endregion
506
509
  //#region temp/packages/editor/src/services/dep.d.ts
507
510
  interface DepEvents {
508
511
  'add-target': [target: Target];
@@ -518,6 +521,7 @@ type StateKey = keyof State$1;
518
521
  declare class Dep extends BaseService {
519
522
  private state;
520
523
  private idleTask;
524
+ private collectWorker;
521
525
  private watcher;
522
526
  private waitingWorker?;
523
527
  private resolveWaitingWorker?;
@@ -536,8 +540,16 @@ declare class Dep extends BaseService {
536
540
  removeTarget(id: Id, type?: string): void;
537
541
  clearTargets(): void;
538
542
  collect(nodes: MNode[], depExtendedData?: DepExtendedData, deep?: boolean, type?: DepTargetType$1): void;
543
+ /**
544
+ * 空闲收集依赖
545
+ *
546
+ * 遍历匹配是整个收集过程中最耗时的部分(要深度遍历节点树并对每个属性做 target 匹配),
547
+ * 节点/数据源多时在主线程执行会长时间卡死页面,因此优先交给 worker 执行,主线程只负责把结果写回 target。
548
+ * 自定义 target 的 isTarget 是无法跨线程传递的闭包函数(没有 descriptor),仍走主线程空闲队列;
549
+ * worker 不可用(如 SSR)或执行失败时也会回退到主线程空闲队列。
550
+ */
539
551
  collectIdle(nodes: MNode[], depExtendedData?: DepExtendedData, deep?: boolean, type?: DepTargetType$1): Promise<boolean>;
540
- collectByWorker(dsl: MApp): Promise<Record<string, Record<string, DepData>>>;
552
+ collectByWorker(dsl: MApp): Promise<DepsData>;
541
553
  collectNode(node: MNode, target: Target, depExtendedData?: DepExtendedData, deep?: boolean): void;
542
554
  clear(nodes?: MNode[]): void;
543
555
  clearByType(type: DepTargetType$1, nodes?: MNode[]): void;
@@ -554,7 +566,17 @@ declare class Dep extends BaseService {
554
566
  * 按 pageId 匹配,可清掉页面内已被删除节点的残留依赖
555
567
  */
556
568
  private removePageDep;
569
+ /**
570
+ * 把遍历匹配交给 worker,结果回来后按 target 分片写回主线程
571
+ */
572
+ private collectByCollectWorker;
573
+ /**
574
+ * 用 worker 的收集结果替换 target 上这批节点的依赖
575
+ */
576
+ private enqueueApplyTask;
577
+ private enqueueTasks;
557
578
  private enqueueTask;
579
+ private beginBatchTask;
558
580
  private onBatchTaskDone;
559
581
  private settleBatchDs;
560
582
  private finishBatch;
@@ -1231,7 +1253,7 @@ declare class History extends BaseService {
1231
1253
  * 派发「页面 / 页面片结构变更」事件(`page-structure-change`)。
1232
1254
  *
1233
1255
  * 常规 `editorService.add` / `remove` 页面节点不会写入 `page` 历史栈(见 editor.add / remove 中
1234
- * 对 isPage / isPageFragment 的分支),因此不会产生任何 historyService 事件。该方法用于在这些
1256
+ * 对 isPageOrFragment 的分支),因此不会产生任何 historyService 事件。该方法用于在这些
1235
1257
  * 场景(以及 setRoot 整体替换 DSL 增删页面)下,向外统一通知页面结构的增删变化,供业务方感知。
1236
1258
  *
1237
1259
  * 一次操作涉及多个页面时,调用方应把本次增删的页面**合并为一个 change 一次性传入**,