@netless/window-manager 1.0.13-bate.0 → 1.0.13-bate.1

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@netless/window-manager",
3
- "version": "1.0.13-bate.0",
3
+ "version": "1.0.13-bate.1",
4
4
  "description": "Multi-window mode for Netless Whiteboard",
5
5
  "author": "l1shen <lishen1635@gmail.com> (https://github.com/l1shen)",
6
6
  "license": "MIT",
@@ -23,7 +23,7 @@
23
23
  },
24
24
  "peerDependencies": {
25
25
  "jspdf": "2.5.1",
26
- "white-web-sdk": "^2.16.53"
26
+ "white-web-sdk": "^2.16.54"
27
27
  },
28
28
  "peerDependenciesMeta": {
29
29
  "jspdf": {
@@ -72,6 +72,6 @@
72
72
  "typescript": "^4.5.5",
73
73
  "vite": "^2.9.9",
74
74
  "vitest": "^0.14.1",
75
- "white-web-sdk": "^2.16.53"
75
+ "white-web-sdk": "^2.16.54"
76
76
  }
77
77
  }
@@ -7,7 +7,6 @@ import type { Cursor } from "./Cursor/Cursor";
7
7
  import { getExtendClass } from "./Utils/extendClass";
8
8
  import type { ExtendClass } from "./Utils/extendClass";
9
9
  import type { NotMinimizedBoxState, TeleBoxState } from "@netless/telebox-insider";
10
- import { LocalConsole } from "./Utils/log";
11
10
 
12
11
  export enum Fields {
13
12
  Apps = "apps",
@@ -55,7 +54,6 @@ export type ISize = Size & { id: string };
55
54
 
56
55
  export class AttributesDelegate {
57
56
  static readonly kind = "AttributesDelegate";
58
- private setMainViewCameraConsole = new LocalConsole("setMainViewCamera", 30);
59
57
  constructor(private context: StoreContext) {}
60
58
 
61
59
  public setContext(context: StoreContext) {
@@ -212,7 +210,6 @@ export class AttributesDelegate {
212
210
  }
213
211
 
214
212
  public setMainViewCamera(camera: ICamera) {
215
- this.setMainViewCameraConsole.log(JSON.stringify(camera));
216
213
  this.context.safeSetAttributes({ [Fields.MainViewCamera]: { ...camera } });
217
214
  }
218
215
 
@@ -1,5 +1,4 @@
1
1
  import { ResizeObserver as ResizeObserverPolyfill } from "@juggle/resize-observer";
2
- import { isFunction } from "lodash";
3
2
  import { WindowManager } from "./index";
4
3
  import type { EmitterType } from "./InternalEmitter";
5
4
  import type { UnsubscribeFn } from "emittery";
@@ -11,7 +10,7 @@ export class ContainerResizeObserver {
11
10
  private containerResizeObserver?: ResizeObserver;
12
11
  private disposer?: UnsubscribeFn;
13
12
 
14
- private updateSizerLocalConsole = new LocalConsole("updateSizer", 30);
13
+ private updateSizerLocalConsole = new LocalConsole("updateSizer", 100);
15
14
 
16
15
  constructor(private emitter: EmitterType) {}
17
16
 
@@ -77,10 +76,9 @@ export class ContainerResizeObserver {
77
76
  }
78
77
 
79
78
  public disconnect() {
79
+ this.updateSizerLocalConsole.destroy();
80
80
  this.containerResizeObserver?.disconnect();
81
- if (isFunction(this.disposer)) {
82
- this.disposer();
83
- this.disposer = undefined;
84
- }
81
+ this.disposer?.();
82
+ this.disposer = undefined;
85
83
  }
86
84
  }
@@ -30,8 +30,8 @@ export const onObjectByEvent = (event: UpdateEventKind) => {
30
30
  };
31
31
  };
32
32
 
33
- export const safeListenPropsUpdated = <T>(
34
- getProps: () => T,
33
+ export const safeListenPropsUpdated = <T extends Record<string, unknown>>(
34
+ getProps: () => T | null | undefined,
35
35
  callback: AkkoObjectUpdatedListener<T>,
36
36
  onDestroyed?: (props: unknown) => void
37
37
  ) => {
@@ -0,0 +1,78 @@
1
+ /** 合法标识符形式的 key 省略引号,形如 `{aaa:undefined}` */
2
+ function formatAttributesLogObjectKey(key: string): string {
3
+ return /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(key) ? key : JSON.stringify(key);
4
+ }
5
+
6
+ /**
7
+ * attributes 调试日志:对象/数组会写成近似 JS 字面量(保留 `undefined`、数组空洞),避免 `[object Object]`;
8
+ * 并处理 BigInt、循环引用等。
9
+ */
10
+ export function stringifyForAttributesLog(value: unknown, seen?: WeakSet<object>): string {
11
+ if (value === undefined) {
12
+ return "undefined";
13
+ }
14
+ if (value === null) {
15
+ return "null";
16
+ }
17
+ const t = typeof value;
18
+ if (t === "bigint") {
19
+ return `${value}n`;
20
+ }
21
+ if (t === "symbol") {
22
+ return String(value);
23
+ }
24
+ if (t === "function") {
25
+ const fn = value as (...args: unknown[]) => unknown;
26
+ return `[Function ${fn.name || "anonymous"}]`;
27
+ }
28
+ if (t !== "object") {
29
+ return t === "string" ? JSON.stringify(value as string) : String(value);
30
+ }
31
+
32
+ const obj = value as object;
33
+ if (seen?.has(obj)) {
34
+ return "[Circular]";
35
+ }
36
+
37
+ const nextSeen = seen ?? new WeakSet<object>();
38
+ nextSeen.add(obj);
39
+ try {
40
+ if (Array.isArray(value)) {
41
+ return `[${Array.from(value as unknown[], (item) =>
42
+ stringifyForAttributesLog(item, nextSeen),
43
+ ).join(",")}]`;
44
+ }
45
+ if (value instanceof Date) {
46
+ return JSON.stringify(value.toISOString());
47
+ }
48
+ if (value instanceof RegExp) {
49
+ return String(value);
50
+ }
51
+ const keys = Object.keys(value as object);
52
+ const pairs = keys.map((k) => {
53
+ let v: unknown;
54
+ try {
55
+ v = (value as Record<string, unknown>)[k];
56
+ } catch {
57
+ return `${formatAttributesLogObjectKey(k)}:[Threw]`;
58
+ }
59
+ return `${formatAttributesLogObjectKey(k)}:${stringifyForAttributesLog(v, nextSeen)}`;
60
+ });
61
+ return `{${pairs.join(",")}}`;
62
+ } catch {
63
+ return "[Unserializable]";
64
+ } finally {
65
+ nextSeen.delete(obj);
66
+ }
67
+ }
68
+
69
+ /** 仅一层 key 合并:可作为 attributes 片段的「普通对象」(非数组、Date 等) */
70
+ export function isShallowMergeAttributesRecord(value: unknown): value is Record<string, unknown> {
71
+ return (
72
+ value !== null &&
73
+ typeof value === "object" &&
74
+ !Array.isArray(value) &&
75
+ !(value instanceof Date) &&
76
+ !(value instanceof RegExp)
77
+ );
78
+ }
package/src/Utils/log.ts CHANGED
@@ -1,5 +1,30 @@
1
+ import type { Logger } from "white-web-sdk";
2
+ import { isShallowMergeAttributesRecord, stringifyForAttributesLog } from "./attributesLogStringify";
1
3
  import { WindowManager } from "../index";
2
4
 
5
+ /** ArgusLog 经 `logger.info` 上报的单条字符串上限(含前缀) */
6
+ const ARGUS_LOG_INFO_MAX_LENGTH = 1500;
7
+
8
+ function truncateArgusLogInfoMessage(message: string): string {
9
+ if (message.length <= ARGUS_LOG_INFO_MAX_LENGTH) {
10
+ return message;
11
+ }
12
+ const ellipsis = "…";
13
+ return message.slice(0, ARGUS_LOG_INFO_MAX_LENGTH - ellipsis.length) + ellipsis;
14
+ }
15
+
16
+ function keysPathEqual(a: string[], b: string[]): boolean {
17
+ if (a.length !== b.length) {
18
+ return false;
19
+ }
20
+ for (let i = 0; i < a.length; i++) {
21
+ if (a[i] !== b[i]) {
22
+ return false;
23
+ }
24
+ }
25
+ return true;
26
+ }
27
+
3
28
  export const log = (...args: any[]): void => {
4
29
  if (WindowManager.debug) {
5
30
  console.log(`[WindowManager]:`, ...args);
@@ -41,4 +66,207 @@ export class LocalConsole {
41
66
  }
42
67
  console.log(`[window-manager][${this.name}]: ${args.join(", ")}`);
43
68
  }
69
+
70
+ /**
71
+ * 销毁:清除 debounce 定时器与未输出的暂存参数。
72
+ * 持有 LocalConsole 的类在销毁时应调用。
73
+ */
74
+ destroy(): void {
75
+ if (this.flushTimer != null) {
76
+ clearTimeout(this.flushTimer);
77
+ this.flushTimer = null;
78
+ }
79
+ this.pendingArgs = null;
80
+ }
81
+ }
82
+
83
+ /**
84
+ * 按 `[WindowManager][tagName]` 前缀输出。
85
+ * 若传入 `debounceTime`(毫秒):窗口内多次 `log` 不立即输出,只在连续停止调用满 `debounceTime` 后输出**最后一次**的参数(尾部 debounce)。
86
+ */
87
+ export class ArgusLog {
88
+ private pendingArgs: unknown[] | null = null;
89
+ private flushTimer: ReturnType<typeof setTimeout> | null = null;
90
+
91
+ /** debounce 窗口内按一层 key 合并;同 key 后者覆盖;非普通对象则整段待输出被本次值替换 */
92
+ private pendingShallowMerge:
93
+ | { kind: "record"; label: string; data: Record<string, unknown> }
94
+ | { kind: "atom"; label: string; value: unknown }
95
+ | null = null;
96
+ private shallowMergeTimer: ReturnType<typeof setTimeout> | null = null;
97
+
98
+ /** debounce 窗口内 safeUpdateAttributes:同 keys 数组则只更新 value,否则追加一段,flush 时拼成一条 */
99
+ private pendingUpdateSegments: { keys: string[]; value: unknown }[] | null = null;
100
+ private updateMergeTimer: ReturnType<typeof setTimeout> | null = null;
101
+
102
+ constructor(
103
+ private readonly logger: Logger,
104
+ private readonly name: string,
105
+ private readonly debounceTime?: number,
106
+ ) {}
107
+
108
+ private emitInfo(message: string): void {
109
+ this.logger.info(truncateArgusLogInfoMessage(message));
110
+ }
111
+
112
+ private flush(): void {
113
+ this.flushTimer = null;
114
+ const args = this.pendingArgs;
115
+ this.pendingArgs = null;
116
+ if (args === null) {
117
+ return;
118
+ }
119
+ this.emitInfo(`[WindowManager][${this.name}]: ${args.join(", ")}`);
120
+ }
121
+
122
+ private flushShallowMerge(): void {
123
+ this.shallowMergeTimer = null;
124
+ const p = this.pendingShallowMerge;
125
+ this.pendingShallowMerge = null;
126
+ if (p === null) {
127
+ return;
128
+ }
129
+ const body =
130
+ p.kind === "record" ? stringifyForAttributesLog(p.data) : stringifyForAttributesLog(p.value);
131
+ this.emitInfo(`[WindowManager][${this.name}]: ${p.label} ${body}`);
132
+ // 输出后释放合并对象引用,避免长时间持有 attributes 快照
133
+ if (p.kind === "record") {
134
+ for (const k of Object.keys(p.data)) {
135
+ delete p.data[k];
136
+ }
137
+ }
138
+ }
139
+
140
+ log(...args: unknown[]): void {
141
+ const ms = this.debounceTime;
142
+ if (ms != null && ms > 0) {
143
+ this.pendingArgs = args;
144
+ if (this.flushTimer != null) {
145
+ clearTimeout(this.flushTimer);
146
+ }
147
+ this.flushTimer = setTimeout(() => this.flush(), ms);
148
+ return;
149
+ }
150
+ this.emitInfo(`[WindowManager][${this.name}]: ${args.join(", ")}`);
151
+ }
152
+
153
+ /**
154
+ * 带 debounce 时:窗口内多次调用会把「一层 key」合并进同一条日志(不同 key 并存,同 key 取最后一次)。
155
+ * `payload` 为普通对象时做浅合并;否则视为原子值,覆盖当前待合并状态(丢弃此前累积的对象 key)。
156
+ * 无 debounce 或时间为 0 时立即输出。
157
+ */
158
+ logDebouncedShallowMerge(label: string, payload: unknown): void {
159
+ const ms = this.debounceTime;
160
+ const debounced = ms != null && ms > 0;
161
+
162
+ const emit = (text: string): void => {
163
+ this.emitInfo(`[WindowManager][${this.name}]: ${label} ${text}`);
164
+ };
165
+
166
+ if (!debounced) {
167
+ emit(stringifyForAttributesLog(payload));
168
+ return;
169
+ }
170
+
171
+ if (this.shallowMergeTimer != null) {
172
+ clearTimeout(this.shallowMergeTimer);
173
+ this.shallowMergeTimer = null;
174
+ }
175
+
176
+ if (isShallowMergeAttributesRecord(payload)) {
177
+ if (this.pendingShallowMerge?.kind === "record") {
178
+ this.pendingShallowMerge = {
179
+ kind: "record",
180
+ label,
181
+ data: { ...this.pendingShallowMerge.data, ...payload },
182
+ };
183
+ } else {
184
+ this.pendingShallowMerge = { kind: "record", label, data: { ...payload } };
185
+ }
186
+ } else {
187
+ this.pendingShallowMerge = { kind: "atom", label, value: payload };
188
+ }
189
+
190
+ this.shallowMergeTimer = setTimeout(() => this.flushShallowMerge(), ms);
191
+ }
192
+
193
+ private flushUpdateAttributesMerge(): void {
194
+ this.updateMergeTimer = null;
195
+ const segments = this.pendingUpdateSegments;
196
+ this.pendingUpdateSegments = null;
197
+ if (segments === null || segments.length === 0) {
198
+ return;
199
+ }
200
+ const parts = segments.map(
201
+ (s) => `${s.keys.join(", ")} ${stringifyForAttributesLog(s.value)}`,
202
+ );
203
+ this.emitInfo(`[WindowManager][${this.name}]: safeUpdateAttributes ${parts.join(" | ")}`);
204
+ for (const s of segments) {
205
+ s.keys.length = 0;
206
+ s.value = undefined;
207
+ }
208
+ segments.length = 0;
209
+ }
210
+
211
+ /**
212
+ * 带 debounce 时:连续调用若 `keys` 与上一段完全相同则覆盖该段的 `value`;否则追加一段。
213
+ * flush 时输出一条日志,多段用 ` | ` 连接。
214
+ */
215
+ logDebouncedUpdateAttributes(keys: string[], value: unknown): void {
216
+ const ms = this.debounceTime;
217
+ const debounced = ms != null && ms > 0;
218
+ const keysCopy = [...keys];
219
+
220
+ if (!debounced) {
221
+ this.emitInfo(
222
+ `[WindowManager][${this.name}]: safeUpdateAttributes ${keysCopy.join(", ")} ${stringifyForAttributesLog(value)}`,
223
+ );
224
+ return;
225
+ }
226
+
227
+ if (this.updateMergeTimer != null) {
228
+ clearTimeout(this.updateMergeTimer);
229
+ this.updateMergeTimer = null;
230
+ }
231
+
232
+ if (this.pendingUpdateSegments === null || this.pendingUpdateSegments.length === 0) {
233
+ this.pendingUpdateSegments = [{ keys: keysCopy, value }];
234
+ } else {
235
+ const last = this.pendingUpdateSegments[this.pendingUpdateSegments.length - 1];
236
+ if (keysPathEqual(last.keys, keysCopy)) {
237
+ last.value = value;
238
+ } else {
239
+ this.pendingUpdateSegments.push({ keys: keysCopy, value });
240
+ }
241
+ }
242
+
243
+ this.updateMergeTimer = setTimeout(() => this.flushUpdateAttributesMerge(), ms);
244
+ }
245
+
246
+ /**
247
+ * 销毁:清除所有 `setTimeout` debounce 定时器,并丢弃尚未输出的暂存日志(不补打日志)。
248
+ * WindowManager 销毁时应调用,避免泄漏与销毁后仍触发 `logger.info`。
249
+ */
250
+ destroy(): void {
251
+ if (this.flushTimer != null) {
252
+ clearTimeout(this.flushTimer);
253
+ this.flushTimer = null;
254
+ }
255
+ if (this.shallowMergeTimer != null) {
256
+ clearTimeout(this.shallowMergeTimer);
257
+ this.shallowMergeTimer = null;
258
+ }
259
+ if (this.updateMergeTimer != null) {
260
+ clearTimeout(this.updateMergeTimer);
261
+ this.updateMergeTimer = null;
262
+ }
263
+ this.pendingArgs = null;
264
+ this.pendingShallowMerge = null;
265
+ this.pendingUpdateSegments = null;
266
+ }
267
+
268
+ /** 与 `destroy()` 相同,保留旧名以兼容 */
269
+ dispose(): void {
270
+ this.destroy();
271
+ }
44
272
  }
@@ -3,7 +3,7 @@ import { callbacks } from "../callback";
3
3
  import { createView } from "./ViewManager";
4
4
  import { debounce, get, isEmpty, isEqual } from "lodash";
5
5
  import { internalEmitter } from "../InternalEmitter";
6
- import { Fields } from "../AttributesDelegate";
6
+ import { Fields, type MainViewCamera } from "../AttributesDelegate";
7
7
  import { setViewFocusScenePath } from "../Utils/Common";
8
8
  import { SideEffectManager } from "side-effect-manager";
9
9
  import type { Camera, Room, Size, View } from "white-web-sdk";
@@ -11,6 +11,14 @@ import type { AppManager } from "../AppManager";
11
11
  import { Events } from "../constants";
12
12
  import { LocalConsole } from "../Utils/log";
13
13
 
14
+ type MainViewScreenLike = {
15
+ refreshSize?: (width: number, height: number) => void;
16
+ resizeObserver?: {
17
+ disconnect?: () => void;
18
+ observe?: (target: Element) => void;
19
+ };
20
+ };
21
+
14
22
  export class MainViewProxy {
15
23
  /** Refresh the view's camera in an interval of 1.5s. */
16
24
  public polling = false;
@@ -27,9 +35,10 @@ export class MainViewProxy {
27
35
 
28
36
  private sideEffectManager = new SideEffectManager();
29
37
 
30
- private playgroundSizeChangeListenerLocalConsole = new LocalConsole("playgroundSizeChangeListener", 30);
31
- private sizeUpdatedLocalConsole = new LocalConsole("sizeUpdated", 30);
32
- private cameraUpdatedLocalConsole = new LocalConsole("cameraUpdated", 30);
38
+ private playgroundSizeChangeListenerLocalConsole = new LocalConsole("playgroundSizeChangeListener", 100);
39
+ private sizeUpdatedLocalConsole = new LocalConsole("sizeUpdated", 100);
40
+ private cameraUpdatedLocalConsole = new LocalConsole("cameraUpdated", 100);
41
+ private cameraReactionLocalConsole = new LocalConsole("cameraReaction", 100);
33
42
 
34
43
  constructor(private manager: AppManager) {
35
44
  this.mainView = this.createMainView();
@@ -155,6 +164,7 @@ export class MainViewProxy {
155
164
  element: HTMLDivElement
156
165
  ) {
157
166
  const { width: viewWidth, height: viewHeight } = this.mainView.size;
167
+ let targetElement = element;
158
168
  if (
159
169
  Math.abs(viewWidth - observedSize.width) <= 0.5 &&
160
170
  Math.abs(viewHeight - observedSize.height) <= 0.5
@@ -170,17 +180,36 @@ export class MainViewProxy {
170
180
  return;
171
181
  }
172
182
  this.isForcingMainViewDivElement = true;
173
- this.mainView.divElement = null;
174
- this.mainView.divElement = element;
175
- queueMicrotask(() => {
176
- const rect = element.getBoundingClientRect();
177
- console.log("[window-manager] forceSyncMainViewDivElementResult " + JSON.stringify({
178
- reason,
179
- viewSize: this.mainView.size,
180
- rect: { width: rect.width, height: rect.height },
181
- }));
182
- this.isForcingMainViewDivElement = false;
183
- });
183
+ try {
184
+ const mainView = this.mainView as View & { screen?: MainViewScreenLike };
185
+ const screen = mainView.screen;
186
+ const resizeObserver = screen?.resizeObserver;
187
+ if (typeof screen?.refreshSize === "function") {
188
+ console.log(
189
+ "[window-manager] forceSyncMainViewDivElement observerReset " +
190
+ JSON.stringify({
191
+ reason,
192
+ viewSize: this.mainView.size,
193
+ observedSize,
194
+ })
195
+ );
196
+ // Reset the observer queue so we sync against the current DOM box,
197
+ // not a stale ResizeObserver entry from a rapid resize burst.
198
+ resizeObserver?.disconnect?.();
199
+ screen.refreshSize(observedSize.width, observedSize.height);
200
+ resizeObserver?.observe?.(element);
201
+ }
202
+ } finally {
203
+ queueMicrotask(() => {
204
+ const rect = targetElement.getBoundingClientRect();
205
+ console.log("[window-manager] forceSyncMainViewDivElementResult " + JSON.stringify({
206
+ reason,
207
+ viewSize: this.mainView.size,
208
+ rect: { width: rect.width, height: rect.height },
209
+ }));
210
+ this.isForcingMainViewDivElement = false;
211
+ });
212
+ }
184
213
  }
185
214
 
186
215
  public start() {
@@ -210,11 +239,11 @@ export class MainViewProxy {
210
239
  private cameraReaction = () => {
211
240
  return reaction(
212
241
  () => this.mainViewCamera,
213
- camera => {
242
+ (camera: MainViewCamera | undefined) => {
214
243
  if (camera && camera.id !== this.manager.uid) {
215
- console.log("[window-manager] cameraReaction " + JSON.stringify(camera) + JSON.stringify(this.mainViewSize));
216
244
  this.moveCameraToContian(this.mainViewSize);
217
245
  this.moveCamera(camera);
246
+ this.cameraReactionLocalConsole.log(`camera: ${JSON.stringify(camera)}, current size: ${JSON.stringify(this.mainViewSize)}`);
218
247
  }
219
248
  },
220
249
  { fireImmediately: true }
@@ -420,6 +449,10 @@ export class MainViewProxy {
420
449
  cancelAnimationFrame(this.wrapperRectWorkaroundFrame);
421
450
  this.wrapperRectWorkaroundFrame = 0;
422
451
  }
452
+ this.playgroundSizeChangeListenerLocalConsole.destroy();
453
+ this.sizeUpdatedLocalConsole.destroy();
454
+ this.cameraUpdatedLocalConsole.destroy();
455
+ this.cameraReactionLocalConsole.destroy();
423
456
  this.removeMainViewListener();
424
457
  this.stop();
425
458
  this.sideEffectManager.flushAll();
package/src/index.ts CHANGED
@@ -12,7 +12,7 @@ import { Fields } from "./AttributesDelegate";
12
12
  import { initDb } from "./Register/storage";
13
13
  import { InvisiblePlugin, isPlayer, isRoom, RoomPhase, ViewMode } from "white-web-sdk";
14
14
  import { isEqual, isNull, isObject, omit, isNumber } from "lodash";
15
- import { log } from "./Utils/log";
15
+ import { ArgusLog, log } from "./Utils/log";
16
16
  import { PageStateImpl } from "./PageState";
17
17
  import { ReconnectRefresher } from "./ReconnectRefresher";
18
18
  import { replaceRoomFunction } from "./Utils/RoomHacker";
@@ -245,6 +245,8 @@ export class WindowManager
245
245
 
246
246
  private _roomLogger?: Logger;
247
247
 
248
+ public attributesDeboundceLog?: ArgusLog;
249
+
248
250
  get Logger(): Logger | undefined {
249
251
  return this._roomLogger;
250
252
  }
@@ -287,6 +289,7 @@ export class WindowManager
287
289
  manager = await this.initManager(room);
288
290
  if (manager) {
289
291
  manager._roomLogger = (room as unknown as { logger: Logger }).logger;
292
+ manager.attributesDeboundceLog = new ArgusLog(manager._roomLogger, "attributes", 300);
290
293
  if (WindowManager.registered.size > 0) {
291
294
  manager._roomLogger.info(
292
295
  `[WindowManager] registered apps: ${JSON.stringify(
@@ -1064,6 +1067,8 @@ export class WindowManager
1064
1067
  }
1065
1068
 
1066
1069
  private _destroy() {
1070
+ this.attributesDeboundceLog?.destroy();
1071
+ this.attributesDeboundceLog = undefined;
1067
1072
  this.containerResizeObserver?.disconnect();
1068
1073
  this.appManager?.destroy();
1069
1074
  this.cursorManager?.destroy();
@@ -1106,15 +1111,19 @@ export class WindowManager
1106
1111
 
1107
1112
  public safeSetAttributes(attributes: any): void {
1108
1113
  if (this.canOperate) {
1109
- this.Logger && this.Logger.info(`[WindowManager]: safeSetAttributes ${JSON.stringify(attributes)}`);
1110
1114
  this.setAttributes(attributes);
1115
+ if (this.attributesDeboundceLog) {
1116
+ this.attributesDeboundceLog.logDebouncedShallowMerge("safeSetAttributes", attributes);
1117
+ }
1111
1118
  }
1112
1119
  }
1113
1120
 
1114
1121
  public safeUpdateAttributes(keys: string[], value: any): void {
1115
1122
  if (this.canOperate) {
1116
- this.Logger && this.Logger.info(`[WindowManager]: safeUpdateAttributes ${keys.join(", ")} ${value}`);
1117
1123
  this.updateAttributes(keys, value);
1124
+ if (this.attributesDeboundceLog) {
1125
+ this.attributesDeboundceLog.logDebouncedUpdateAttributes(keys, value);
1126
+ }
1118
1127
  }
1119
1128
  }
1120
1129
 
@@ -1124,8 +1133,8 @@ export class WindowManager
1124
1133
 
1125
1134
  public cleanCurrentScene(): void {
1126
1135
  log("clean current scene");
1127
- this.Logger && this.Logger.info(`[WindowManager]: cleanCurrentScene ${this.focusedView?.focusScenePath}`);
1128
1136
  this.focusedView?.cleanCurrentScene();
1137
+ this.Logger && this.Logger.info(`[WindowManager]: cleanCurrentScene ${this.focusedView?.focusScenePath}`);
1129
1138
  }
1130
1139
 
1131
1140
  public redo(): number {
@@ -1137,8 +1146,8 @@ export class WindowManager
1137
1146
  }
1138
1147
 
1139
1148
  public delete(): void {
1140
- this.Logger && this.Logger.info(`[WindowManager]: delete ${this.focusedView?.focusScenePath}`);
1141
1149
  this.focusedView?.delete();
1150
+ this.Logger && this.Logger.info(`[WindowManager]: delete ${this.focusedView?.focusScenePath}`);
1142
1151
  }
1143
1152
 
1144
1153
  public copy(): void {