@arronqzy/react-view 1.0.28 → 1.0.29

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": "@arronqzy/react-view",
3
- "version": "1.0.28",
3
+ "version": "1.0.29",
4
4
  "files": [
5
5
  "src",
6
6
  "dist",
@@ -31,13 +31,13 @@
31
31
  "@codemirror/theme-one-dark": "^6.1.2",
32
32
  "@codemirror/view": "^6.36.2",
33
33
  "@arronqzy/ui": "1.0.9",
34
- "@arronqzy/react-blueprint": "1.0.16",
35
34
  "@arronqzy/blueprint-dsl": "1.0.14",
36
- "@arronqzy/i18n": "0.1.11",
35
+ "@arronqzy/i18n": "0.1.12",
36
+ "@arronqzy/react-blueprint": "1.0.16",
37
37
  "@arronqzy/rx-store": "1.0.8",
38
+ "@arronqzy/view-table": "0.1.8",
38
39
  "@arronqzy/view-scene3d": "0.1.5",
39
- "@arronqzy/webllm-assistant": "0.1.16",
40
- "@arronqzy/view-table": "0.1.8"
40
+ "@arronqzy/webllm-assistant": "0.1.16"
41
41
  },
42
42
  "devDependencies": {
43
43
  "@types/react": "^19.0.1",
@@ -46,8 +46,8 @@
46
46
  "eslint": "^8.57.0",
47
47
  "typescript": "^5.5.4",
48
48
  "@arronqzy/eslint-config": "0.0.0",
49
- "@arronqzy/tailwind": "1.0.2",
50
- "@arronqzy/typescript-config": "1.0.0"
49
+ "@arronqzy/typescript-config": "1.0.0",
50
+ "@arronqzy/tailwind": "1.0.2"
51
51
  },
52
52
  "scripts": {
53
53
  "lint": "eslint \"src/**/*.{ts,tsx}\"",
@@ -1079,11 +1079,18 @@ function ReactViewPanelInner({
1079
1079
  );
1080
1080
 
1081
1081
  const handleWorkspaceCreateProject = useCallback(async () => {
1082
- const result = await workspaceProjects.handleCreateProject();
1083
- if (result?.name) {
1084
- toast({ title: messages.workspaceCreated(result.name) });
1082
+ try {
1083
+ const result = await workspaceProjects.handleCreateProject();
1084
+ if (result?.name) {
1085
+ toast({ title: messages.workspaceCreated(result.name) });
1086
+ }
1087
+ return result;
1088
+ } catch (error) {
1089
+ toast({
1090
+ title: error instanceof Error ? error.message : t("panel.messages.createWorkspaceFailed"),
1091
+ });
1092
+ throw error;
1085
1093
  }
1086
- return result;
1087
1094
  }, [workspaceProjects]);
1088
1095
 
1089
1096
  const handleWorkspaceOpenProject = useCallback(
@@ -1121,8 +1128,16 @@ function ReactViewPanelInner({
1121
1128
 
1122
1129
  const handleWorkspaceDeleteProject = useCallback(
1123
1130
  async (id: string) => {
1124
- await workspaceProjects.handleDeleteProject(id);
1125
- toast({ title: t("panel.messages.workspaceDeleted") });
1131
+ try {
1132
+ await workspaceProjects.handleDeleteProject(id);
1133
+ toast({ title: t("panel.messages.workspaceDeleted") });
1134
+ } catch (error) {
1135
+ toast({
1136
+ title:
1137
+ error instanceof Error ? error.message : t("panel.messages.deleteWorkspaceFailed"),
1138
+ });
1139
+ throw error;
1140
+ }
1126
1141
  },
1127
1142
  [workspaceProjects]
1128
1143
  );
@@ -1,5 +1,6 @@
1
1
  import React, {
2
2
  useEffect,
3
+ useLayoutEffect,
3
4
  useMemo,
4
5
  useRef,
5
6
  useState,
@@ -146,6 +147,37 @@ export function PanelConfigSidebar({
146
147
  return selectedElement ? [selectedElement] : [];
147
148
  }, [selectedElement, selectedElements]);
148
149
  const isMultiSelectMode = effectiveSelectedElements.length > 1;
150
+ const configScrollKey =
151
+ !isMultiSelectMode && selectedElement ? selectedElement.id : null;
152
+ const configScrollPosByIdRef = useRef(new Map<string, number>());
153
+ const activeConfigScrollKeyRef = useRef<string | null>(null);
154
+
155
+ useEffect(() => {
156
+ const el = sidebarScrollRef.current;
157
+ if (!el) return;
158
+ const onScroll = () => {
159
+ const key = activeConfigScrollKeyRef.current;
160
+ if (!key) return;
161
+ configScrollPosByIdRef.current.set(key, el.scrollTop);
162
+ };
163
+ el.addEventListener("scroll", onScroll, { passive: true });
164
+ return () => el.removeEventListener("scroll", onScroll);
165
+ }, []);
166
+
167
+ useLayoutEffect(() => {
168
+ const el = sidebarScrollRef.current;
169
+ const prevKey = activeConfigScrollKeyRef.current;
170
+ if (el && prevKey && prevKey !== configScrollKey) {
171
+ configScrollPosByIdRef.current.set(prevKey, el.scrollTop);
172
+ }
173
+ activeConfigScrollKeyRef.current = configScrollKey;
174
+ if (!el) return;
175
+ const top =
176
+ configScrollKey != null
177
+ ? (configScrollPosByIdRef.current.get(configScrollKey) ?? 0)
178
+ : 0;
179
+ el.scrollTop = top;
180
+ }, [configScrollKey]);
149
181
 
150
182
  useEffect(() => {
151
183
  if (!selectedElement) return;
@@ -52,7 +52,9 @@ export function getPanelMessages(t: TranslateFn) {
52
52
  workspaceLoaded: t("panel.messages.workspaceLoaded"),
53
53
  workspaceLoadedDesc: t("panel.messages.workspaceLoadedDesc"),
54
54
  openWorkspaceFailed: t("panel.messages.openWorkspaceFailed"),
55
+ createWorkspaceFailed: t("panel.messages.createWorkspaceFailed"),
55
56
  syncFailed: t("panel.messages.syncFailed"),
57
+ deleteWorkspaceFailed: t("panel.messages.deleteWorkspaceFailed"),
56
58
  workspaceDeleted: t("panel.messages.workspaceDeleted"),
57
59
  openPreviewFailed: t("panel.messages.openPreviewFailed"),
58
60
  clearedBlueprintLogs: (removed: number) =>
@@ -1077,7 +1077,14 @@ export function usePanelElements() {
1077
1077
  /* fall through */
1078
1078
  }
1079
1079
  }
1080
- return JSON.parse(JSON.stringify(current)) as State;
1080
+ try {
1081
+ return JSON.parse(JSON.stringify(current)) as State;
1082
+ } catch (error) {
1083
+ throw Object.assign(new Error("indexeddb-serialize-failed"), {
1084
+ name: "DataCloneError",
1085
+ cause: error,
1086
+ });
1087
+ }
1081
1088
  }, []);
1082
1089
 
1083
1090
  const importPanelData = useCallback((nextState: State) => {
@@ -19,6 +19,7 @@ import {
19
19
  workspaceSnapshotsEqual,
20
20
  type WorkspaceSnapshot,
21
21
  } from "../library/workspace-snapshot";
22
+ import { mapWorkspaceStorageError } from "../library/workspace-storage-error";
22
23
  import { runBusyTask } from "../utils/async-work";
23
24
  import { AbuilderEvents, emitAbuilderEvent } from "../library/event-subscription";
24
25
  import { parseBlueprintData } from "@arronqzy/react-blueprint";
@@ -145,8 +146,13 @@ export function useWorkspaceProjects({
145
146
  }, [blueprintDocument, blueprintMeta, productName, titleIconDataUrl]);
146
147
 
147
148
  const refreshProjects = useCallback(async () => {
148
- const items = await listWorkspaceProjects(nameSpace);
149
- setProjects(items);
149
+ try {
150
+ const items = await listWorkspaceProjects(nameSpace);
151
+ setProjects(items);
152
+ } catch (error) {
153
+ // Listing failure should not brick the editor; create/sync still surface errors.
154
+ console.warn("[abuilder] listWorkspaceProjects failed", error);
155
+ }
150
156
  }, [nameSpace]);
151
157
 
152
158
  useEffect(() => {
@@ -272,37 +278,58 @@ export function useWorkspaceProjects({
272
278
 
273
279
  const persistProject = useCallback(
274
280
  async (options: { id?: string; name: string; createdAt?: number }) => {
275
- const snapshot = cloneWorkspaceSnapshot(buildCurrentSnapshot());
276
- const now = Date.now();
277
- const id = options.id ?? createWorkspaceProjectId();
278
- const createdAt = options.createdAt ?? now;
279
- const name = (options.name ?? "").trim() || resolveProjectName();
281
+ try {
282
+ const snapshot = cloneWorkspaceSnapshot(buildCurrentSnapshot());
283
+ const now = Date.now();
284
+ const id = options.id ?? createWorkspaceProjectId();
285
+ const createdAt = options.createdAt ?? now;
286
+ const name = (options.name ?? "").trim() || resolveProjectName();
280
287
 
281
- const record: WorkspaceProjectRecord = {
282
- id,
283
- name,
284
- createdAt,
285
- updatedAt: now,
286
- panelState: exportPanelData(),
287
- blueprintDocument: snapshot.blueprintDocument,
288
- blueprintMeta: snapshot.blueprintMeta,
289
- productName:
290
- typeof snapshot.productName === "string" ? snapshot.productName : resolveProjectName(),
291
- titleIconDataUrl: snapshot.titleIconDataUrl || undefined,
292
- };
288
+ let panelState: State;
289
+ try {
290
+ panelState = exportPanelData();
291
+ } catch (error) {
292
+ throw mapWorkspaceStorageError(error, t);
293
+ }
293
294
 
294
- await putWorkspaceProject(record, nameSpace);
295
- writeWorkspacePreviewCache(record, nameSpace);
296
- broadcastWorkspaceProjectUpdate(id, now, nameSpace);
297
- setActiveProjectId(id);
298
- setActiveProjectName(name);
299
- syncedSnapshotRef.current = snapshot;
300
- syncedPanelRevisionRef.current = panelRevision;
301
- pendingRevisionSyncRef.current = false;
302
- await refreshProjects();
303
- return record;
295
+ const record: WorkspaceProjectRecord = {
296
+ id,
297
+ name,
298
+ createdAt,
299
+ updatedAt: now,
300
+ panelState,
301
+ blueprintDocument: snapshot.blueprintDocument,
302
+ blueprintMeta: snapshot.blueprintMeta,
303
+ productName:
304
+ typeof snapshot.productName === "string"
305
+ ? snapshot.productName
306
+ : resolveProjectName(),
307
+ titleIconDataUrl: snapshot.titleIconDataUrl || undefined,
308
+ };
309
+
310
+ await putWorkspaceProject(record, nameSpace);
311
+ writeWorkspacePreviewCache(record, nameSpace);
312
+ broadcastWorkspaceProjectUpdate(id, now, nameSpace);
313
+ setActiveProjectId(id);
314
+ setActiveProjectName(name);
315
+ syncedSnapshotRef.current = snapshot;
316
+ syncedPanelRevisionRef.current = panelRevision;
317
+ pendingRevisionSyncRef.current = false;
318
+ await refreshProjects();
319
+ return record;
320
+ } catch (error) {
321
+ throw mapWorkspaceStorageError(error, t);
322
+ }
304
323
  },
305
- [buildCurrentSnapshot, exportPanelData, nameSpace, panelRevision, resolveProjectName, refreshProjects]
324
+ [
325
+ buildCurrentSnapshot,
326
+ exportPanelData,
327
+ nameSpace,
328
+ panelRevision,
329
+ resolveProjectName,
330
+ refreshProjects,
331
+ t,
332
+ ]
306
333
  );
307
334
 
308
335
  const handleCreateProject = useCallback(async () => {
@@ -332,14 +359,18 @@ export function useWorkspaceProjects({
332
359
 
333
360
  const handleOpenProject = useCallback(
334
361
  async (id: string) => {
335
- await runBusyTask(t("common.loadingWorkspace"), async () => {
336
- const record = await getWorkspaceProject(id, nameSpace);
337
- if (!record) {
338
- await refreshProjects();
339
- throw new Error(t("panel.messages.workspaceNotFound"));
340
- }
341
- applyProjectRecord(record);
342
- });
362
+ try {
363
+ await runBusyTask(t("common.loadingWorkspace"), async () => {
364
+ const record = await getWorkspaceProject(id, nameSpace);
365
+ if (!record) {
366
+ await refreshProjects();
367
+ throw new Error(t("panel.messages.workspaceNotFound"));
368
+ }
369
+ applyProjectRecord(record);
370
+ });
371
+ } catch (error) {
372
+ throw mapWorkspaceStorageError(error, t);
373
+ }
343
374
  },
344
375
  [applyProjectRecord, nameSpace, refreshProjects, t]
345
376
  );
@@ -370,17 +401,21 @@ export function useWorkspaceProjects({
370
401
 
371
402
  const handleDeleteProject = useCallback(
372
403
  async (id: string) => {
373
- await runBusyTask(t("common.deletingWorkspace"), async () => {
374
- await deleteWorkspaceProject(id, nameSpace);
375
- if (activeProjectId === id) {
376
- setActiveProjectId(null);
377
- setActiveProjectName(null);
378
- syncedSnapshotRef.current = null;
379
- }
380
- previewWindowsRef.current.delete(id);
381
- refreshPreviewing();
382
- await refreshProjects();
383
- });
404
+ try {
405
+ await runBusyTask(t("common.deletingWorkspace"), async () => {
406
+ await deleteWorkspaceProject(id, nameSpace);
407
+ if (activeProjectId === id) {
408
+ setActiveProjectId(null);
409
+ setActiveProjectName(null);
410
+ syncedSnapshotRef.current = null;
411
+ }
412
+ previewWindowsRef.current.delete(id);
413
+ refreshPreviewing();
414
+ await refreshProjects();
415
+ });
416
+ } catch (error) {
417
+ throw mapWorkspaceStorageError(error, t);
418
+ }
384
419
  },
385
420
  [activeProjectId, nameSpace, refreshPreviewing, refreshProjects, t]
386
421
  );
@@ -6,6 +6,7 @@ import { appStorageKey } from "@arronqzy/blueprint-dsl";
6
6
  const DB_NAME = "arronqzy-workspace-projects";
7
7
  const DB_VERSION = 1;
8
8
  const STORE_NAME = "projects";
9
+ const OPEN_TIMEOUT_MS = 10_000;
9
10
 
10
11
  export type WorkspaceProjectListItem = {
11
12
  id: string;
@@ -26,10 +27,77 @@ export type WorkspaceProjectRecord = {
26
27
  titleIconDataUrl?: string;
27
28
  };
28
29
 
30
+ const dbPromises = new Map<string, Promise<IDBDatabase>>();
31
+
32
+ function getIndexedDb(): IDBFactory {
33
+ if (typeof indexedDB === "undefined" || !indexedDB) {
34
+ throw Object.assign(new Error("indexeddb-unavailable"), {
35
+ name: "IndexedDbUnavailable",
36
+ });
37
+ }
38
+ return indexedDB;
39
+ }
40
+
41
+ /** Make a value safe for IndexedDB structured clone (drops non-cloneable fields). */
42
+ export function cloneForIndexedDb<T>(value: T): T {
43
+ if (typeof structuredClone === "function") {
44
+ try {
45
+ return structuredClone(value);
46
+ } catch {
47
+ /* fall through to JSON */
48
+ }
49
+ }
50
+ try {
51
+ return JSON.parse(JSON.stringify(value)) as T;
52
+ } catch (error) {
53
+ throw Object.assign(new Error("indexeddb-serialize-failed"), {
54
+ name: "DataCloneError",
55
+ cause: error,
56
+ });
57
+ }
58
+ }
59
+
29
60
  function openDb(nameSpace?: string | null): Promise<IDBDatabase> {
30
- return new Promise((resolve, reject) => {
31
- const request = indexedDB.open(appStorageKey(DB_NAME, nameSpace), DB_VERSION);
32
- request.onerror = () => reject(request.error ?? new Error("indexeddb-open-failed"));
61
+ const key = appStorageKey(DB_NAME, nameSpace);
62
+ const cached = dbPromises.get(key);
63
+ if (cached) return cached;
64
+
65
+ const promise = new Promise<IDBDatabase>((resolve, reject) => {
66
+ let settled = false;
67
+ const fail = (error: unknown) => {
68
+ if (settled) return;
69
+ settled = true;
70
+ dbPromises.delete(key);
71
+ reject(
72
+ error instanceof Error
73
+ ? error
74
+ : Object.assign(new Error("indexeddb-open-failed"), { cause: error })
75
+ );
76
+ };
77
+
78
+ let request: IDBOpenDBRequest;
79
+ try {
80
+ request = getIndexedDb().open(key, DB_VERSION);
81
+ } catch (error) {
82
+ fail(error);
83
+ return;
84
+ }
85
+
86
+ const timer = setTimeout(() => {
87
+ fail(
88
+ Object.assign(new Error("indexeddb-open-timeout"), {
89
+ name: "TimeoutError",
90
+ })
91
+ );
92
+ }, OPEN_TIMEOUT_MS);
93
+
94
+ request.onerror = () => {
95
+ clearTimeout(timer);
96
+ fail(request.error ?? new Error("indexeddb-open-failed"));
97
+ };
98
+ request.onblocked = () => {
99
+ // Wait for onsuccess / timeout; another tab may be holding an older connection.
100
+ };
33
101
  request.onupgradeneeded = () => {
34
102
  const db = request.result;
35
103
  if (!db.objectStoreNames.contains(STORE_NAME)) {
@@ -37,8 +105,35 @@ function openDb(nameSpace?: string | null): Promise<IDBDatabase> {
37
105
  store.createIndex("updatedAt", "updatedAt", { unique: false });
38
106
  }
39
107
  };
40
- request.onsuccess = () => resolve(request.result);
108
+ request.onsuccess = () => {
109
+ clearTimeout(timer);
110
+ if (settled) {
111
+ try {
112
+ request.result.close();
113
+ } catch {
114
+ /* ignore */
115
+ }
116
+ return;
117
+ }
118
+ settled = true;
119
+ const db = request.result;
120
+ db.onversionchange = () => {
121
+ try {
122
+ db.close();
123
+ } catch {
124
+ /* ignore */
125
+ }
126
+ dbPromises.delete(key);
127
+ };
128
+ db.onclose = () => {
129
+ dbPromises.delete(key);
130
+ };
131
+ resolve(db);
132
+ };
41
133
  });
134
+
135
+ dbPromises.set(key, promise);
136
+ return promise;
42
137
  }
43
138
 
44
139
  function runTransaction<T>(
@@ -49,27 +144,76 @@ function runTransaction<T>(
49
144
  return openDb(nameSpace).then(
50
145
  (db) =>
51
146
  new Promise<T>((resolve, reject) => {
52
- const tx = db.transaction(STORE_NAME, mode);
147
+ let tx: IDBTransaction;
148
+ try {
149
+ tx = db.transaction(STORE_NAME, mode);
150
+ } catch (error) {
151
+ // Stale connection (e.g. Safari closed it); reopen once.
152
+ dbPromises.delete(appStorageKey(DB_NAME, nameSpace));
153
+ reject(error);
154
+ return;
155
+ }
53
156
  const store = tx.objectStore(STORE_NAME);
54
157
  const request = runner(store);
55
158
  let result!: T;
159
+ let settled = false;
160
+
161
+ const fail = (error: unknown) => {
162
+ if (settled) return;
163
+ settled = true;
164
+ reject(
165
+ error instanceof Error
166
+ ? error
167
+ : Object.assign(new Error("indexeddb-transaction-failed"), {
168
+ cause: error,
169
+ })
170
+ );
171
+ };
56
172
 
57
173
  request.onerror = () =>
58
- reject(request.error ?? new Error("indexeddb-request-failed"));
174
+ fail(request.error ?? new Error("indexeddb-request-failed"));
59
175
  request.onsuccess = () => {
60
176
  result = request.result as T;
61
177
  };
62
178
 
63
179
  tx.oncomplete = () => {
64
- db.close();
180
+ if (settled) return;
181
+ settled = true;
65
182
  resolve(result);
66
183
  };
67
- tx.onerror = () => {
68
- db.close();
69
- reject(tx.error ?? new Error("indexeddb-transaction-failed"));
70
- };
184
+ tx.onerror = () => fail(tx.error ?? new Error("indexeddb-transaction-failed"));
185
+ tx.onabort = () => fail(tx.error ?? new Error("indexeddb-transaction-aborted"));
71
186
  })
72
- );
187
+ ).catch(async (error) => {
188
+ // One retry after connection reset (common on iOS/Safari when DB was closed).
189
+ const message = error instanceof Error ? error.message : String(error);
190
+ const name = error instanceof DOMException ? error.name : "";
191
+ if (
192
+ name === "InvalidStateError" ||
193
+ message.includes("InvalidStateError") ||
194
+ message.includes("closing")
195
+ ) {
196
+ dbPromises.delete(appStorageKey(DB_NAME, nameSpace));
197
+ const db = await openDb(nameSpace);
198
+ return new Promise<T>((resolve, reject) => {
199
+ const tx = db.transaction(STORE_NAME, mode);
200
+ const store = tx.objectStore(STORE_NAME);
201
+ const request = runner(store);
202
+ let result!: T;
203
+ request.onerror = () =>
204
+ reject(request.error ?? new Error("indexeddb-request-failed"));
205
+ request.onsuccess = () => {
206
+ result = request.result as T;
207
+ };
208
+ tx.oncomplete = () => resolve(result);
209
+ tx.onerror = () =>
210
+ reject(tx.error ?? new Error("indexeddb-transaction-failed"));
211
+ tx.onabort = () =>
212
+ reject(tx.error ?? new Error("indexeddb-transaction-aborted"));
213
+ });
214
+ }
215
+ throw error;
216
+ });
73
217
  }
74
218
 
75
219
  export function createWorkspaceProjectId(): string {
@@ -103,11 +247,9 @@ export async function listWorkspaceProjects(
103
247
  cursor.continue();
104
248
  };
105
249
  tx.oncomplete = () => {
106
- db.close();
107
250
  resolve(items.sort((a, b) => b.updatedAt - a.updatedAt));
108
251
  };
109
252
  tx.onerror = () => {
110
- db.close();
111
253
  reject(tx.error ?? new Error("indexeddb-transaction-failed"));
112
254
  };
113
255
  });
@@ -129,8 +271,13 @@ export async function putWorkspaceProject(
129
271
  record: WorkspaceProjectRecord,
130
272
  nameSpace?: string | null
131
273
  ): Promise<WorkspaceProjectRecord> {
132
- await runTransaction<IDBValidKey>("readwrite", (store) => store.put(record), nameSpace);
133
- return record;
274
+ const safeRecord = cloneForIndexedDb(record);
275
+ await runTransaction<IDBValidKey>(
276
+ "readwrite",
277
+ (store) => store.put(safeRecord),
278
+ nameSpace
279
+ );
280
+ return safeRecord;
134
281
  }
135
282
 
136
283
  export async function deleteWorkspaceProject(
@@ -0,0 +1,60 @@
1
+ /**
2
+ * Map IndexedDB / clone failures to localized workspace messages.
3
+ * Used by create / sync / open / delete so device-specific DOMExceptions
4
+ * show actionable copy instead of opaque browser English.
5
+ */
6
+ export function mapWorkspaceStorageError(
7
+ error: unknown,
8
+ t: (key: string) => string
9
+ ): Error {
10
+ const raw = error instanceof Error ? error : new Error(String(error));
11
+ const name =
12
+ (error as { name?: string } | null)?.name ||
13
+ (raw as { name?: string }).name ||
14
+ "";
15
+ const message = raw.message || "";
16
+
17
+ if (
18
+ name === "IndexedDbUnavailable" ||
19
+ name === "SecurityError" ||
20
+ message.includes("indexeddb-unavailable") ||
21
+ /indexedDB is not available|Access is denied|The operation is insecure/i.test(
22
+ message
23
+ )
24
+ ) {
25
+ return new Error(t("panel.messages.workspaceStorageUnavailable"));
26
+ }
27
+
28
+ if (
29
+ name === "TimeoutError" ||
30
+ message.includes("indexeddb-open-timeout")
31
+ ) {
32
+ return new Error(t("panel.messages.workspaceStorageTimeout"));
33
+ }
34
+
35
+ if (
36
+ name === "QuotaExceededError" ||
37
+ /QuotaExceeded|quota|storage.*full|exceeded the quota/i.test(message)
38
+ ) {
39
+ return new Error(t("panel.messages.workspaceStorageQuotaExceeded"));
40
+ }
41
+
42
+ if (
43
+ name === "DataCloneError" ||
44
+ message.includes("indexeddb-serialize-failed") ||
45
+ /could not be cloned|DataCloneError|circular/i.test(message)
46
+ ) {
47
+ return new Error(t("panel.messages.workspaceStorageSerializeFailed"));
48
+ }
49
+
50
+ if (
51
+ message.includes("indexeddb-open-failed") ||
52
+ message.includes("indexeddb-transaction-failed") ||
53
+ message.includes("indexeddb-request-failed") ||
54
+ message.includes("indexeddb-transaction-aborted")
55
+ ) {
56
+ return new Error(t("panel.messages.workspaceStorageFailed"));
57
+ }
58
+
59
+ return raw;
60
+ }