@netless/window-manager 1.0.13-test.9 → 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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@netless/window-manager",
3
- "version": "1.0.13-test.9",
3
+ "version": "1.0.14",
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
  }
@@ -149,7 +149,7 @@ export class AppProxy implements PageRemoveService {
149
149
  ): Promise<{ appId: string; app: NetlessApp }> {
150
150
  const params = this.params;
151
151
  if (!params.kind) {
152
- this.Logger?.error(`[WindowManager]: kind require`);
152
+ this.Logger && this.Logger.error(`[WindowManager]: kind require`);
153
153
  throw new Error("[WindowManager]: kind require");
154
154
  }
155
155
  const appImpl = await appRegister.appClasses.get(params.kind)?.();
@@ -167,7 +167,7 @@ export class AppProxy implements PageRemoveService {
167
167
  params.isDragContent
168
168
  );
169
169
  } else {
170
- this.Logger?.error(`[WindowManager]: app load failed ${params.kind} ${params.src}`);
170
+ this.Logger && this.Logger.error(`[WindowManager]: app load failed ${params.kind} ${params.src}`);
171
171
  throw new Error(`[WindowManager]: app load failed ${params.kind} ${params.src}`);
172
172
  }
173
173
  internalEmitter.emit("updateManagerRect");
@@ -216,7 +216,7 @@ export class AppProxy implements PageRemoveService {
216
216
  }
217
217
  setTimeout(async () => {
218
218
  // 延迟执行 setup, 防止初始化的属性没有更新成功
219
- this.Logger?.info(`[WindowManager]: setup app ${this.kind}, appId: ${appId}`);
219
+ this.Logger && this.Logger.info(`[WindowManager]: setup app ${this.kind}, appId: ${appId}`);
220
220
  const result = await app.setup(context);
221
221
  this.appResult = result;
222
222
  appRegister.notifyApp(this.kind, "created", { appId, result });
@@ -251,7 +251,7 @@ export class AppProxy implements PageRemoveService {
251
251
  this.boxManager.focusBox({ appId }, false);
252
252
  }
253
253
  } catch (error: any) {
254
- this.Logger?.error(`[WindowManager]: app setup error: ${error.message}`);
254
+ this.Logger && this.Logger.error(`[WindowManager]: app setup error: ${error.message}`);
255
255
  throw new Error(`[WindowManager]: app setup error: ${error.message}`);
256
256
  }
257
257
  }
@@ -538,7 +538,7 @@ export class AppProxy implements PageRemoveService {
538
538
  await appRegister.notifyApp(this.kind, "destroy", { appId: this.id });
539
539
  await this.appEmitter.emit("destroy", { error });
540
540
  } catch (error) {
541
- this.Logger?.error(`[WindowManager]: notifyApp error: ${error.message}`);
541
+ this.Logger && this.Logger.error(`[WindowManager]: notifyApp error: ${error.message}`);
542
542
  console.error("[WindowManager]: notifyApp error", error.message, error.stack);
543
543
  }
544
544
  this.appEmitter.clearListeners();
@@ -561,7 +561,7 @@ export class AppProxy implements PageRemoveService {
561
561
  this.manager.refresher.remove(this.stateKey);
562
562
  this.manager.refresher.remove(`${this.id}-fullPath`);
563
563
  this._prevFullPath = undefined;
564
- this.Logger?.info(`[WindowManager]: destroy app ${this.kind} appId: ${this.id}`);
564
+ this.Logger && this.Logger.info(`[WindowManager]: destroy app ${this.kind} appId: ${this.id}`);
565
565
  }
566
566
 
567
567
  public close(): Promise<void> {
@@ -47,7 +47,6 @@ export class AppListeners {
47
47
  break;
48
48
  }
49
49
  case Events.SetMainViewScenePath: {
50
- console.log("[window-manager] mainMagixEventListener " + JSON.stringify(data.payload));
51
50
  this.setMainViewScenePathHandler(data.payload);
52
51
  break;
53
52
  }
package/src/AppManager.ts CHANGED
@@ -133,7 +133,6 @@ export class AppManager {
133
133
  this.createRootDirScenesCallback();
134
134
 
135
135
  appRegister.setSyncRegisterApp(payload => {
136
- this.Logger?.info(`[WindowManager] syncRegisterApp ${JSON.stringify(payload)}`);
137
136
  this.safeUpdateAttributes([Fields.Registered, payload.kind], payload);
138
137
  });
139
138
  }
@@ -146,7 +145,7 @@ export class AppManager {
146
145
  const { scenePath } = params;
147
146
  // 如果移除根目录就把 scenePath 设置为初始值
148
147
  if (scenePath === ROOT_DIR) {
149
- console.log("[window-manager] onRemoveScenes ROOT_DIR");
148
+ console.log("[window-manager] onRemoveScenes ROOT_DIR");
150
149
  await this.onRootDirRemoved();
151
150
  this.dispatchInternalEvent(Events.RootDirRemoved);
152
151
  return;
@@ -159,7 +158,7 @@ export class AppManager {
159
158
  sceneName = this.callbacksNode?.scenes[nextIndex];
160
159
  }
161
160
  if (sceneName) {
162
- console.log("[window-manager] onRemoveScenes setMainViewScenePath" + `${ROOT_DIR}${sceneName}`);
161
+ console.log(`[window-manager] onRemoveScenes setMainViewScenePath ${ROOT_DIR}${sceneName}`);
163
162
  this.setMainViewScenePath(`${ROOT_DIR}${sceneName}`);
164
163
  }
165
164
  await this.setMainViewSceneIndex(nextIndex);
@@ -605,7 +604,7 @@ export class AppManager {
605
604
  try {
606
605
  const appAttributes = this.attributes[id];
607
606
  if (!appAttributes) {
608
- this.Logger?.error(
607
+ this.Logger && this.Logger.error(
609
608
  `[WindowManager]: appAttributes is undefined, appId: ${id}`
610
609
  );
611
610
  throw new Error("appAttributes is undefined");
@@ -713,11 +712,12 @@ export class AppManager {
713
712
  callbacks.emit("onMainViewMounted", mainView);
714
713
  const hasRoot = this.hasRoot(mainView.divElement);
715
714
  const rect = this.getRectByDivElement(mainView.divElement);
716
- console.log("[window-manager] bindMainView hasRoot" + hasRoot + JSON.stringify(rect) +
717
- window.outerHeight + window.outerWidth,
718
- window.visualViewport?.width ?? "null", window.visualViewport?.height ?? "null",
719
- window.visualViewport?.offsetLeft ?? "null", window.visualViewport?.offsetTop ?? "null",
720
- );
715
+ let log = `[window-manager] bindMainView hasRoot:${hasRoot}, rect:${JSON.stringify(rect)}, outerHeight:${window.outerHeight}, outerWidth:${window.outerWidth}`;
716
+ const visualViewport = window.visualViewport;
717
+ if (visualViewport) {
718
+ log += `, visualViewportWidth:${visualViewport.width}, visualViewportHeight:${visualViewport.height}, visualViewportOffsetLeft:${visualViewport.offsetLeft}, visualViewportOffsetTop:${visualViewport.offsetTop}`;
719
+ }
720
+ console.log(log);
721
721
  }
722
722
 
723
723
  private hasRoot(divElement: HTMLDivElement){
@@ -818,7 +818,7 @@ export class AppManager {
818
818
  return appProxy;
819
819
  } else {
820
820
  this.appStatus.delete(appId);
821
- this.Logger?.error(`[WindowManager]: initialize AppProxy failed, appId: ${appId}`);
821
+ this.Logger && this.Logger.error(`[WindowManager]: initialize AppProxy failed, appId: ${appId}`);
822
822
  throw new Error("[WindowManger]: initialize AppProxy failed");
823
823
  }
824
824
  }
@@ -858,11 +858,11 @@ export class AppManager {
858
858
  const scenePathType = this.displayer.scenePathType(scenePath);
859
859
  const sceneDir = parseSceneDir(scenePath);
860
860
  if (sceneDir !== ROOT_DIR) {
861
- this.Logger?.error(`[WindowManager]: main view scenePath must in root dir "/"`);
861
+ this.Logger && this.Logger.error(`[WindowManager]: main view scenePath must in root dir "/"`);
862
862
  throw new Error(`[WindowManager]: main view scenePath must in root dir "/"`);
863
863
  }
864
864
  if (scenePathType === ScenePathType.None) {
865
- this.Logger?.error(`[WindowManager]: ${scenePath} not valid scene`);
865
+ this.Logger && this.Logger.error(`[WindowManager]: ${scenePath} not valid scene`);
866
866
  throw new Error(`[WindowManager]: ${scenePath} not valid scene`);
867
867
  } else if (scenePathType === ScenePathType.Page) {
868
868
  await this._setMainViewScenePath(scenePath);
@@ -879,7 +879,6 @@ export class AppManager {
879
879
  private async _setMainViewScenePath(scenePath: string) {
880
880
  const success = this.setMainViewFocusPath(scenePath);
881
881
  if (success) {
882
- console.log("[window-manager] _setMainViewScenePath ", scenePath);
883
882
  this.safeSetAttributes({ _mainScenePath: scenePath });
884
883
  this.store.setMainViewFocusPath(this.mainView);
885
884
  this.updateSceneIndex();
@@ -896,7 +895,6 @@ export class AppManager {
896
895
  const pageName = scenePath.replace(sceneDir, "").replace("/", "");
897
896
  const index = scenes.findIndex(scene => scene.name === pageName);
898
897
  if (isInteger(index) && index >= 0) {
899
- console.log("[window-manager] updateSceneIndex ", index);
900
898
  this.safeSetAttributes({ _mainSceneIndex: index });
901
899
  }
902
900
  }
@@ -915,14 +913,13 @@ export class AppManager {
915
913
  this.dispatchSetMainViewScenePath(scenePath);
916
914
  }
917
915
  } else {
918
- this.Logger?.error(`[WindowManager]: ${index} not valid index`);
916
+ this.Logger && this.Logger.error(`[WindowManager]: ${index} not valid index`);
919
917
  throw new Error(`[WindowManager]: ${index} not valid index`);
920
918
  }
921
919
  }
922
920
  }
923
921
 
924
922
  private dispatchSetMainViewScenePath(scenePath: string): void {
925
- console.log("[window-manager] dispatchSetMainViewScenePath ", JSON.stringify(scenePath));
926
923
  this.dispatchInternalEvent(Events.SetMainViewScenePath, { nextScenePath: scenePath });
927
924
  callbacks.emit("mainViewScenePathChange", scenePath);
928
925
  // 兼容 15 的 SDK, 需要 room 的当前 ScenePath
@@ -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", 50);
59
57
  constructor(private context: StoreContext) {}
60
58
 
61
59
  public setContext(context: StoreContext) {
@@ -196,12 +194,10 @@ export class AttributesDelegate {
196
194
  }
197
195
 
198
196
  public setMainViewScenePath(scenePath: string) {
199
- console.log("[window-manager] setMainViewScenePath ", scenePath);
200
197
  this.context.safeSetAttributes({ _mainScenePath: scenePath });
201
198
  }
202
199
 
203
200
  public setMainViewSceneIndex(index: number) {
204
- console.log("[window-manager] setMainViewSceneIndex ", index);
205
201
  this.context.safeSetAttributes({ _mainSceneIndex: index });
206
202
  }
207
203
 
@@ -214,19 +210,16 @@ export class AttributesDelegate {
214
210
  }
215
211
 
216
212
  public setMainViewCamera(camera: ICamera) {
217
- this.setMainViewCameraConsole.log(JSON.stringify(camera));
218
213
  this.context.safeSetAttributes({ [Fields.MainViewCamera]: { ...camera } });
219
214
  }
220
215
 
221
216
  public setMainViewSize(size: ISize) {
222
217
  if (size.width === 0 || size.height === 0) return;
223
- console.log("[window-manager] setMainViewSize ", JSON.stringify(size));
224
218
  this.context.safeSetAttributes({ [Fields.MainViewSize]: { ...size } });
225
219
  }
226
220
 
227
221
  public setMainViewCameraAndSize(camera: ICamera, size: ISize) {
228
222
  if (size.width === 0 || size.height === 0) return;
229
- console.log("[window-manager] setMainViewCameraAndSize ", JSON.stringify(camera), JSON.stringify(size));
230
223
  this.context.safeSetAttributes({
231
224
  [Fields.MainViewCamera]: { ...camera },
232
225
  [Fields.MainViewSize]: { ...size },
package/src/BoxManager.ts CHANGED
@@ -420,7 +420,6 @@ export class BoxManager {
420
420
  const rect = this.mainView.divElement?.getBoundingClientRect();
421
421
  if (rect && rect.width > 0 && rect.height > 0) {
422
422
  const containerRect = { x: 0, y: 0, width: rect.width, height: rect.height };
423
- console.log("[window-manager] updateManagerRect" + JSON.stringify(containerRect) + "mainView" + this.mainView.size);
424
423
  this.teleBoxManager.setContainerRect(containerRect);
425
424
  this.context.notifyContainerRectUpdate(this.teleBoxManager.containerRect);
426
425
  }
@@ -1,23 +1,26 @@
1
1
  import AppDocsViewer from "@netless/app-docs-viewer";
2
- import AppMediaPlayer, { setOptions } from "@netless/app-media-player";
3
2
  import { WindowManager } from "./index";
4
3
 
5
- export const setupBuiltin = () => {
4
+ const loadAppMediaPlayer = async () => {
5
+ const mod = await import("@netless/app-media-player");
6
6
  if (WindowManager.debug) {
7
- setOptions({ verbose: true });
7
+ mod.setOptions({ verbose: true });
8
8
  }
9
+ return (mod.default || mod) as any;
10
+ };
9
11
 
12
+ export const setupBuiltin = () => {
10
13
  WindowManager.register({
11
14
  kind: AppDocsViewer.kind,
12
15
  src: AppDocsViewer,
13
16
  });
14
17
  WindowManager.register({
15
- kind: AppMediaPlayer.kind,
16
- src: AppMediaPlayer as any,
18
+ kind: "MediaPlayer",
19
+ src: loadAppMediaPlayer,
17
20
  });
18
21
  };
19
22
 
20
23
  export const BuiltinApps = {
21
24
  DocsViewer: AppDocsViewer.kind as string,
22
- MediaPlayer: AppMediaPlayer.kind as string,
25
+ MediaPlayer: "MediaPlayer" as string,
23
26
  };
@@ -1,14 +1,16 @@
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";
5
+ import { LocalConsole } from "./Utils/log";
6
6
 
7
7
  const ResizeObserver = window.ResizeObserver || ResizeObserverPolyfill;
8
8
 
9
9
  export class ContainerResizeObserver {
10
10
  private containerResizeObserver?: ResizeObserver;
11
11
  private disposer?: UnsubscribeFn;
12
+
13
+ private updateSizerLocalConsole = new LocalConsole("updateSizer", 100);
12
14
 
13
15
  constructor(private emitter: EmitterType) {}
14
16
 
@@ -28,22 +30,19 @@ export class ContainerResizeObserver {
28
30
  sizer: HTMLElement,
29
31
  wrapper: HTMLDivElement
30
32
  ) {
31
- console.log(`[window-manager] observePlaygroundSize ${JSON.stringify(container.getBoundingClientRect())}, ${JSON.stringify(sizer.getBoundingClientRect())}, ${JSON.stringify(wrapper.getBoundingClientRect())}`);
32
- this.updateSizer(container.getBoundingClientRect(), sizer, wrapper);
33
+ this.updateSizer(container.getBoundingClientRect(), sizer, wrapper, 'observePlaygroundSize');
33
34
 
34
35
  this.containerResizeObserver = new ResizeObserver(entries => {
35
36
  const containerRect = entries[0]?.contentRect;
36
37
  if (containerRect) {
37
- this.updateSizer(containerRect, sizer, wrapper);
38
- console.log(`[window-manager] containerResizeObserver ${JSON.stringify(containerRect)}`);
38
+ this.updateSizer(containerRect, sizer, wrapper, 'containerResizeObserver');
39
39
  this.emitter.emit("playgroundSizeChange", containerRect);
40
40
  }
41
41
  });
42
42
 
43
43
  this.disposer = this.emitter.on("containerSizeRatioUpdate", () => {
44
44
  const containerRect = container.getBoundingClientRect();
45
- console.log(`[window-manager] containerSizeRatioUpdate ${JSON.stringify(containerRect)}`);
46
- this.updateSizer(containerRect, sizer, wrapper);
45
+ this.updateSizer(containerRect, sizer, wrapper, 'containerSizeRatioUpdate');
47
46
  this.emitter.emit("playgroundSizeChange", containerRect);
48
47
  });
49
48
 
@@ -53,7 +52,8 @@ export class ContainerResizeObserver {
53
52
  public updateSizer(
54
53
  { width, height }: DOMRectReadOnly,
55
54
  sizer: HTMLElement,
56
- wrapper: HTMLDivElement
55
+ wrapper: HTMLDivElement,
56
+ origin?: string
57
57
  ) {
58
58
  if (width && height) {
59
59
  if (height / width > WindowManager.containerSizeRatio) {
@@ -65,16 +65,20 @@ export class ContainerResizeObserver {
65
65
  }
66
66
  wrapper.style.width = `${width}px`;
67
67
  wrapper.style.height = `${height}px`;
68
- wrapper.style.backgroundColor = 'green';
69
- console.log(`[window-manager] updateSizer ${JSON.stringify({ width, height })} ${wrapper.style.width} ${wrapper.style.height} ${JSON.stringify(wrapper.getBoundingClientRect())}`);
68
+ const wrapperRect = wrapper.getBoundingClientRect();
69
+ this.updateSizerLocalConsole.log(`from ${origin}, traget size: ${JSON.stringify({ width, height })}, wrapperRect: ${wrapperRect.width} ${wrapperRect.height}`);
70
+ this.emitter.emit("wrapperRectChange", {
71
+ width: wrapperRect.width,
72
+ height: wrapperRect.height,
73
+ origin,
74
+ });
70
75
  }
71
76
  }
72
77
 
73
78
  public disconnect() {
79
+ this.updateSizerLocalConsole.destroy();
74
80
  this.containerResizeObserver?.disconnect();
75
- if (isFunction(this.disposer)) {
76
- this.disposer();
77
- this.disposer = undefined;
78
- }
81
+ this.disposer?.();
82
+ this.disposer = undefined;
79
83
  }
80
84
  }
@@ -29,6 +29,7 @@ export type EmitterEvent = {
29
29
  changePageState: undefined;
30
30
  writableChange: boolean;
31
31
  containerSizeRatioUpdate: number;
32
+ wrapperRectChange: { width: number; height: number; origin?: string };
32
33
  boxesStatusChange: Map<string, TeleBoxState>;
33
34
  lastNotMinimizedBoxesStatusChange: Map<string, NotMinimizedBoxState>;
34
35
  };
@@ -34,7 +34,6 @@ export const setScenePath = (room: Room | undefined, scenePath: string) => {
34
34
  if (room && room.isWritable) {
35
35
  if (room.state.sceneState.scenePath !== scenePath) {
36
36
  const nextScenePath = scenePath === "/" ? "" : scenePath;
37
- console.log("[window-manager] real setScenePath for current room ", nextScenePath);
38
37
  room.setScenePath(nextScenePath);
39
38
  }
40
39
  }
@@ -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
  ) => {
@@ -37,10 +37,24 @@ export const replaceRoomFunction = (room: Room | Player, manager: WindowManager)
37
37
  room.scalePptToFit = (...args) => {
38
38
  _scalePptToFit.call(room, ...args);
39
39
  if (manager.appManager?.mainViewProxy) {
40
- console.log("[window-manager] scalePptToFit " + JSON.stringify(args));
41
40
  manager.appManager.mainViewProxy.setCameraAndSize();
42
41
  }
43
42
  };
43
+ const _putScenes = room.putScenes;
44
+ room.putScenes = (...args) => {
45
+ const [path, scenes] = args;
46
+ const currentScenePath = manager.mainView.focusScenePath;
47
+ if (currentScenePath && path && scenes) {
48
+ console.log("[window-manager] putScenes " + JSON.stringify(args));
49
+ for (const scene of scenes) {
50
+ if (`${path}${scene.name}` === currentScenePath) {
51
+ console.error(`[window-manager] putScenes: scene name can not be the same as the current scene path: ${currentScenePath}`);
52
+ return;
53
+ }
54
+ }
55
+ }
56
+ return _putScenes.call(room, ...args);
57
+ };
44
58
  room.moveCamera = (camera: Camera) => manager.moveCamera(camera);
45
59
  room.moveCameraToContain = (...args) => manager.moveCameraToContain(...args);
46
60
  room.convertToPointInWorld = (...args) => manager.mainView.convertToPointInWorld(...args);
@@ -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
+ }