@arronqzy/react-view 1.0.20 → 1.0.22

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.
@@ -21,6 +21,8 @@ import {
21
21
  } from "../library/workspace-snapshot";
22
22
  import { runBusyTask } from "../utils/async-work";
23
23
  import { AbuilderEvents, emitAbuilderEvent } from "../library/event-subscription";
24
+ import { parseBlueprintData } from "@arronqzy/react-blueprint";
25
+ import { createEmptyPanelState, normalizeImportedPanelState } from "../utils/panelStateIO";
24
26
 
25
27
  export type UseWorkspaceProjectsOptions = {
26
28
  exportPanelData: () => State;
@@ -39,13 +41,20 @@ export type UseWorkspaceProjectsOptions = {
39
41
  onProjectApplied?: (record: WorkspaceProjectRecord) => void;
40
42
  /** 外部传入的初始工作区,挂载后自动加载(不触发 onProjectApplied) */
41
43
  initialWorkspace?: WorkspaceProjectRecord | null;
44
+ /** 隔离 IndexedDB / 预览缓存 / 同步频道 */
45
+ nameSpace?: string | null;
42
46
  };
43
47
 
44
- export function buildOnlinePreviewUrl(projectId: string): string {
48
+ export function buildOnlinePreviewUrl(
49
+ projectId: string,
50
+ nameSpace?: string | null
51
+ ): string {
45
52
  const url = new URL(window.location.href);
46
53
  url.search = "";
47
54
  url.searchParams.set("preview", "online");
48
55
  url.searchParams.set("projectId", projectId);
56
+ const ns = (nameSpace ?? "").trim();
57
+ if (ns) url.searchParams.set("ns", ns);
49
58
  const previewInstanceId =
50
59
  typeof crypto !== "undefined" && "randomUUID" in crypto
51
60
  ? crypto.randomUUID()
@@ -54,6 +63,28 @@ export function buildOnlinePreviewUrl(projectId: string): string {
54
63
  return url.toString();
55
64
  }
56
65
 
66
+ function previewWindowName(projectId: string, nameSpace?: string | null): string {
67
+ const ns = (nameSpace ?? "").trim();
68
+ return ns ? `abuilder-preview-${ns}-${projectId}` : `abuilder-preview-${projectId}`;
69
+ }
70
+
71
+ function hasWorkspacePayload(
72
+ record?: WorkspaceProjectRecord | null
73
+ ): record is WorkspaceProjectRecord {
74
+ if (!record || typeof record !== "object") return false;
75
+ return Boolean(
76
+ record.panelState ||
77
+ record.blueprintDocument ||
78
+ record.id ||
79
+ record.productName ||
80
+ record.titleIconDataUrl
81
+ );
82
+ }
83
+
84
+ function workspaceApplyKey(record: WorkspaceProjectRecord): string {
85
+ return `${record.id ?? "anon"}:${record.updatedAt ?? 0}`;
86
+ }
87
+
57
88
  export function useWorkspaceProjects({
58
89
  exportPanelData,
59
90
  importPanelData,
@@ -68,6 +99,7 @@ export function useWorkspaceProjects({
68
99
  panelRevision,
69
100
  onProjectApplied,
70
101
  initialWorkspace,
102
+ nameSpace = null,
71
103
  }: UseWorkspaceProjectsOptions) {
72
104
  const { t } = useI18nOptional();
73
105
  const [projects, setProjects] = useState<WorkspaceProjectListItem[]>([]);
@@ -99,7 +131,8 @@ export function useWorkspaceProjects({
99
131
  }, [refreshPreviewing]);
100
132
 
101
133
  const resolveProjectName = useCallback(() => {
102
- return productName.trim() || t("panel.defaults.unnamedProduct");
134
+ return (typeof productName === "string" ? productName : "").trim() ||
135
+ t("panel.defaults.unnamedProduct");
103
136
  }, [productName, t]);
104
137
 
105
138
  const buildCurrentSnapshot = useCallback((): WorkspaceSnapshot => {
@@ -112,9 +145,9 @@ export function useWorkspaceProjects({
112
145
  }, [blueprintDocument, blueprintMeta, productName, titleIconDataUrl]);
113
146
 
114
147
  const refreshProjects = useCallback(async () => {
115
- const items = await listWorkspaceProjects();
148
+ const items = await listWorkspaceProjects(nameSpace);
116
149
  setProjects(items);
117
- }, []);
150
+ }, [nameSpace]);
118
151
 
119
152
  useEffect(() => {
120
153
  void refreshProjects();
@@ -143,25 +176,33 @@ export function useWorkspaceProjects({
143
176
 
144
177
  const applyProjectRecord = useCallback(
145
178
  (record: WorkspaceProjectRecord, options?: { notifyApplied?: boolean }) => {
179
+ const parsedBlueprint = parseBlueprintData(record.blueprintDocument);
146
180
  const blueprintDocument =
147
- record.blueprintDocument ?? BlueprintGraph.empty().document;
181
+ parsedBlueprint.value ?? BlueprintGraph.empty().document;
148
182
  const blueprintMeta: BlueprintMetaDraft = {
149
183
  name: record.blueprintMeta?.name ?? t("panel.defaults.unnamedBlueprint"),
150
184
  remark: record.blueprintMeta?.remark ?? "",
151
185
  };
186
+ const panelState =
187
+ normalizeImportedPanelState(record.panelState) ?? createEmptyPanelState();
188
+ const productName =
189
+ typeof record.productName === "string" && record.productName.trim()
190
+ ? record.productName
191
+ : t("panel.defaults.unnamedProduct");
192
+ const titleIconDataUrl = record.titleIconDataUrl ?? "";
152
193
 
153
- importPanelData(record.panelState);
194
+ importPanelData(panelState);
154
195
  setBlueprintGraph(BlueprintGraph.fromDocument(blueprintDocument));
155
196
  setBlueprintMeta(blueprintMeta);
156
- setProductName(record.productName);
157
- setTitleIconDataUrl(record.titleIconDataUrl ?? "");
158
- setActiveProjectId(record.id);
159
- setActiveProjectName(record.name);
197
+ setProductName(productName);
198
+ setTitleIconDataUrl(titleIconDataUrl);
199
+ setActiveProjectId(record.id || null);
200
+ setActiveProjectName(record.name ?? null);
160
201
  syncedSnapshotRef.current = cloneWorkspaceSnapshot({
161
202
  blueprintDocument,
162
203
  blueprintMeta,
163
- productName: record.productName,
164
- titleIconDataUrl: record.titleIconDataUrl ?? "",
204
+ productName,
205
+ titleIconDataUrl,
165
206
  });
166
207
  syncedPanelRevisionRef.current = panelRevision;
167
208
  pendingRevisionSyncRef.current = true;
@@ -170,6 +211,9 @@ export function useWorkspaceProjects({
170
211
  ...record,
171
212
  blueprintDocument,
172
213
  blueprintMeta,
214
+ panelState,
215
+ productName,
216
+ titleIconDataUrl: titleIconDataUrl || undefined,
173
217
  });
174
218
  }
175
219
  },
@@ -185,14 +229,46 @@ export function useWorkspaceProjects({
185
229
  ]
186
230
  );
187
231
 
232
+ const applyEmptyWorkspace = useCallback(() => {
233
+ importPanelData(createEmptyPanelState());
234
+ setBlueprintGraph(BlueprintGraph.empty());
235
+ setBlueprintMeta({
236
+ name: t("panel.defaults.unnamedBlueprint"),
237
+ remark: "",
238
+ });
239
+ setProductName(t("panel.defaults.unnamedProduct"));
240
+ setTitleIconDataUrl("");
241
+ setActiveProjectId(null);
242
+ setActiveProjectName(null);
243
+ syncedSnapshotRef.current = null;
244
+ }, [
245
+ importPanelData,
246
+ setBlueprintGraph,
247
+ setBlueprintMeta,
248
+ setProductName,
249
+ setTitleIconDataUrl,
250
+ t,
251
+ ]);
252
+
188
253
  const initialWorkspaceAppliedKeyRef = useRef<string | null>(null);
254
+ const bootstrappedRef = useRef(false);
189
255
  useEffect(() => {
190
- if (!initialWorkspace?.id) return;
191
- const key = `${initialWorkspace.id}:${initialWorkspace.updatedAt}`;
256
+ if (!bootstrappedRef.current) {
257
+ bootstrappedRef.current = true;
258
+ if (hasWorkspacePayload(initialWorkspace)) {
259
+ applyProjectRecord(initialWorkspace, { notifyApplied: false });
260
+ initialWorkspaceAppliedKeyRef.current = workspaceApplyKey(initialWorkspace);
261
+ } else {
262
+ applyEmptyWorkspace();
263
+ }
264
+ return;
265
+ }
266
+ if (!hasWorkspacePayload(initialWorkspace)) return;
267
+ const key = workspaceApplyKey(initialWorkspace);
192
268
  if (initialWorkspaceAppliedKeyRef.current === key) return;
193
269
  initialWorkspaceAppliedKeyRef.current = key;
194
270
  applyProjectRecord(initialWorkspace, { notifyApplied: false });
195
- }, [applyProjectRecord, initialWorkspace]);
271
+ }, [applyEmptyWorkspace, applyProjectRecord, initialWorkspace]);
196
272
 
197
273
  const persistProject = useCallback(
198
274
  async (options: { id?: string; name: string; createdAt?: number }) => {
@@ -200,7 +276,7 @@ export function useWorkspaceProjects({
200
276
  const now = Date.now();
201
277
  const id = options.id ?? createWorkspaceProjectId();
202
278
  const createdAt = options.createdAt ?? now;
203
- const name = options.name.trim() || resolveProjectName();
279
+ const name = (options.name ?? "").trim() || resolveProjectName();
204
280
 
205
281
  const record: WorkspaceProjectRecord = {
206
282
  id,
@@ -210,13 +286,14 @@ export function useWorkspaceProjects({
210
286
  panelState: exportPanelData(),
211
287
  blueprintDocument: snapshot.blueprintDocument,
212
288
  blueprintMeta: snapshot.blueprintMeta,
213
- productName: snapshot.productName,
289
+ productName:
290
+ typeof snapshot.productName === "string" ? snapshot.productName : resolveProjectName(),
214
291
  titleIconDataUrl: snapshot.titleIconDataUrl || undefined,
215
292
  };
216
293
 
217
- await putWorkspaceProject(record);
218
- writeWorkspacePreviewCache(record);
219
- broadcastWorkspaceProjectUpdate(id, now);
294
+ await putWorkspaceProject(record, nameSpace);
295
+ writeWorkspacePreviewCache(record, nameSpace);
296
+ broadcastWorkspaceProjectUpdate(id, now, nameSpace);
220
297
  setActiveProjectId(id);
221
298
  setActiveProjectName(name);
222
299
  syncedSnapshotRef.current = snapshot;
@@ -225,7 +302,7 @@ export function useWorkspaceProjects({
225
302
  await refreshProjects();
226
303
  return record;
227
304
  },
228
- [buildCurrentSnapshot, exportPanelData, panelRevision, resolveProjectName, refreshProjects]
305
+ [buildCurrentSnapshot, exportPanelData, nameSpace, panelRevision, resolveProjectName, refreshProjects]
229
306
  );
230
307
 
231
308
  const handleCreateProject = useCallback(async () => {
@@ -242,7 +319,7 @@ export function useWorkspaceProjects({
242
319
  return handleCreateProject();
243
320
  }
244
321
  return runBusyTask(t("common.savingWorkspace"), async () => {
245
- const existing = await getWorkspaceProject(activeProjectId);
322
+ const existing = await getWorkspaceProject(activeProjectId, nameSpace);
246
323
  const name = resolveProjectName();
247
324
  const record = await persistProject({
248
325
  id: activeProjectId,
@@ -251,12 +328,12 @@ export function useWorkspaceProjects({
251
328
  });
252
329
  return { name, id: record.id };
253
330
  });
254
- }, [activeProjectId, handleCreateProject, persistProject, resolveProjectName, t]);
331
+ }, [activeProjectId, handleCreateProject, nameSpace, persistProject, resolveProjectName, t]);
255
332
 
256
333
  const handleOpenProject = useCallback(
257
334
  async (id: string) => {
258
335
  await runBusyTask(t("common.loadingWorkspace"), async () => {
259
- const record = await getWorkspaceProject(id);
336
+ const record = await getWorkspaceProject(id, nameSpace);
260
337
  if (!record) {
261
338
  await refreshProjects();
262
339
  throw new Error(t("panel.messages.workspaceNotFound"));
@@ -264,7 +341,7 @@ export function useWorkspaceProjects({
264
341
  applyProjectRecord(record);
265
342
  });
266
343
  },
267
- [applyProjectRecord, refreshProjects, t]
344
+ [applyProjectRecord, nameSpace, refreshProjects, t]
268
345
  );
269
346
 
270
347
  const handleSyncProject = useCallback(async () => {
@@ -272,7 +349,7 @@ export function useWorkspaceProjects({
272
349
  if (!activeProjectId) {
273
350
  throw new Error(t("panel.messages.saveWorkspaceFirst"));
274
351
  }
275
- const existing = await getWorkspaceProject(activeProjectId);
352
+ const existing = await getWorkspaceProject(activeProjectId, nameSpace);
276
353
  if (!existing) {
277
354
  setActiveProjectId(null);
278
355
  setActiveProjectName(null);
@@ -289,12 +366,12 @@ export function useWorkspaceProjects({
289
366
  emitAbuilderEvent(AbuilderEvents.workspaceSync, record);
290
367
  return name;
291
368
  });
292
- }, [activeProjectId, persistProject, refreshProjects, resolveProjectName, t]);
369
+ }, [activeProjectId, nameSpace, persistProject, refreshProjects, resolveProjectName, t]);
293
370
 
294
371
  const handleDeleteProject = useCallback(
295
372
  async (id: string) => {
296
373
  await runBusyTask(t("common.deletingWorkspace"), async () => {
297
- await deleteWorkspaceProject(id);
374
+ await deleteWorkspaceProject(id, nameSpace);
298
375
  if (activeProjectId === id) {
299
376
  setActiveProjectId(null);
300
377
  setActiveProjectName(null);
@@ -305,7 +382,7 @@ export function useWorkspaceProjects({
305
382
  await refreshProjects();
306
383
  });
307
384
  },
308
- [activeProjectId, refreshPreviewing, refreshProjects, t]
385
+ [activeProjectId, nameSpace, refreshPreviewing, refreshProjects, t]
309
386
  );
310
387
 
311
388
  const handlePreviewProject = useCallback(
@@ -315,9 +392,10 @@ export function useWorkspaceProjects({
315
392
  await handleSyncProject();
316
393
  }
317
394
  const record =
318
- (await getWorkspaceProject(id)) ?? readWorkspacePreviewCache(id);
395
+ (await getWorkspaceProject(id, nameSpace)) ??
396
+ readWorkspacePreviewCache(id, nameSpace);
319
397
  if (record) {
320
- writeWorkspacePreviewCache(record);
398
+ writeWorkspacePreviewCache(record, nameSpace);
321
399
  }
322
400
  const existing = previewWindowsRef.current.get(id);
323
401
  if (existing && !existing.closed) {
@@ -330,8 +408,8 @@ export function useWorkspaceProjects({
330
408
  return;
331
409
  }
332
410
  const win = window.open(
333
- buildOnlinePreviewUrl(id),
334
- `abuilder-preview-${id}`
411
+ buildOnlinePreviewUrl(id, nameSpace),
412
+ previewWindowName(id, nameSpace)
335
413
  );
336
414
  if (win) {
337
415
  previewWindowsRef.current.set(id, win);
@@ -345,7 +423,7 @@ export function useWorkspaceProjects({
345
423
  refreshPreviewing();
346
424
  });
347
425
  },
348
- [activeProjectId, dirty, handleSyncProject, refreshPreviewing, t]
426
+ [activeProjectId, dirty, handleSyncProject, nameSpace, refreshPreviewing, t]
349
427
  );
350
428
 
351
429
  return {
@@ -2,4 +2,18 @@ export * from "./ReactViewPanel";
2
2
  export * from "./ReactViewOnlinePreview";
3
3
  export { I18nRoot, I18nProvider } from "./I18nRoot";
4
4
  export type { I18nRootProps, Locale } from "./I18nRoot";
5
- export type * from "./types";
5
+ export type * from "./types";
6
+ export {
7
+ parseViewData,
8
+ validateViewData,
9
+ parseBlueprintData,
10
+ validateBlueprintData,
11
+ validateWorkspaceData,
12
+ parseWorkspaceData,
13
+ createEmptyWorkspace,
14
+ } from "./library/parse-workspace-data";
15
+ export { createWorkspaceProjectId } from "./library/workspace-project-db";
16
+ export type {
17
+ ParseCheckResult,
18
+ WorkspaceParseCheckResult,
19
+ } from "./library/parse-workspace-data";
@@ -620,7 +620,8 @@ async function paintPanelElementOntoCanvas(
620
620
  liveNode &&
621
621
  (el.materialType === "reference" ||
622
622
  el.materialType === "table" ||
623
- el.materialType === "grid")
623
+ el.materialType === "grid" ||
624
+ el.materialType === "viewport")
624
625
  ) {
625
626
  try {
626
627
  await drawNodeOntoCanvas(ctx, liveNode, 0, 0, w, h, 0);
@@ -681,7 +682,8 @@ export async function capturePreviewLayerSnapshot(
681
682
  ctx.fillStyle = options.backgroundColor ?? "#ffffff";
682
683
  ctx.fillRect(0, 0, width, height);
683
684
 
684
- const sorted = [...elements].sort((a, b) => (a.zIndex ?? 1) - (b.zIndex ?? 1));
685
+ const sorted = [...elements]
686
+ .sort((a, b) => (a.zIndex ?? 1) - (b.zIndex ?? 1));
685
687
  for (const el of sorted) {
686
688
  const liveNode =
687
689
  options.canvasRoot?.querySelector<HTMLElement>(
@@ -0,0 +1,142 @@
1
+ import {
2
+ BlueprintGraph,
3
+ parseBlueprintData,
4
+ validateBlueprintData,
5
+ type BlueprintDocument,
6
+ type BlueprintMetaDraft,
7
+ type ParseCheckResult,
8
+ } from "@arronqzy/react-blueprint";
9
+ import type { State } from "@arronqzy/rx-store";
10
+ import { createEmptyPanelState, normalizeImportedPanelState } from "../utils/panelStateIO";
11
+ import {
12
+ createWorkspaceProjectId,
13
+ type WorkspaceProjectRecord,
14
+ } from "./workspace-project-db";
15
+
16
+ export type { ParseCheckResult };
17
+
18
+ export type WorkspaceParseCheckResult = ParseCheckResult & {
19
+ view: ParseCheckResult;
20
+ blueprint: ParseCheckResult;
21
+ };
22
+
23
+ function isRecord(value: unknown): value is Record<string, unknown> {
24
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value);
25
+ }
26
+
27
+ function extractPanelState(data: unknown): unknown {
28
+ if (isRecord(data) && data.panelState !== undefined) return data.panelState;
29
+ return data;
30
+ }
31
+
32
+ function readBlueprintMeta(raw: unknown): BlueprintMetaDraft {
33
+ if (!isRecord(raw)) return { name: "", remark: "" };
34
+ return {
35
+ name: typeof raw.name === "string" ? raw.name : "",
36
+ remark: typeof raw.remark === "string" ? raw.remark : "",
37
+ };
38
+ }
39
+
40
+ /**
41
+ * 校验数据能否被视图解析(工作区 `panelState` 或裸面板状态均可)。
42
+ */
43
+ export function parseViewData(
44
+ data: unknown
45
+ ): ParseCheckResult & { value?: State } {
46
+ if (data == null) {
47
+ return { ok: false, error: "empty" };
48
+ }
49
+ const normalized = normalizeImportedPanelState(extractPanelState(data));
50
+ if (!normalized) {
51
+ return { ok: false, error: "invalid-view-data" };
52
+ }
53
+ return { ok: true, value: normalized };
54
+ }
55
+
56
+ export function validateViewData(data: unknown): ParseCheckResult {
57
+ const parsed = parseViewData(data);
58
+ return parsed.ok ? { ok: true } : { ok: false, error: parsed.error };
59
+ }
60
+
61
+ export { parseBlueprintData, validateBlueprintData };
62
+
63
+ /**
64
+ * 构造一份可交给 `initialWorkspace` 的空工作区。
65
+ */
66
+ export function createEmptyWorkspace(
67
+ init?: Partial<WorkspaceProjectRecord>
68
+ ): WorkspaceProjectRecord {
69
+ const now = Date.now();
70
+ return {
71
+ id: init?.id || createWorkspaceProjectId(),
72
+ name: init?.name ?? "",
73
+ createdAt: init?.createdAt ?? now,
74
+ updatedAt: init?.updatedAt ?? now,
75
+ panelState: init?.panelState ?? createEmptyPanelState(),
76
+ blueprintDocument: init?.blueprintDocument ?? BlueprintGraph.empty().document,
77
+ blueprintMeta: init?.blueprintMeta ?? { name: "", remark: "" },
78
+ productName: init?.productName ?? "",
79
+ titleIconDataUrl: init?.titleIconDataUrl,
80
+ };
81
+ }
82
+
83
+ /**
84
+ * 校验完整工作区数据:视图 `panelState` 与蓝图 `blueprintDocument` 都要能解析。
85
+ */
86
+ export function validateWorkspaceData(data: unknown): WorkspaceParseCheckResult {
87
+ if (data == null) {
88
+ const empty = { ok: false, error: "empty" } satisfies ParseCheckResult;
89
+ return { ok: false, error: "empty", view: empty, blueprint: empty };
90
+ }
91
+ if (!isRecord(data)) {
92
+ const invalid = { ok: false, error: "invalid-workspace-data" } satisfies ParseCheckResult;
93
+ return {
94
+ ok: false,
95
+ error: "invalid-workspace-data",
96
+ view: invalid,
97
+ blueprint: invalid,
98
+ };
99
+ }
100
+ const view = validateViewData(data);
101
+ const blueprint = validateBlueprintData(data);
102
+ const ok = view.ok && blueprint.ok;
103
+ return {
104
+ ok,
105
+ error: ok ? undefined : "invalid-workspace-data",
106
+ view,
107
+ blueprint,
108
+ };
109
+ }
110
+
111
+ /**
112
+ * 解析并规范化工作区。成功时 `value` 可直接作为 `initialWorkspace`。
113
+ */
114
+ export function parseWorkspaceData(
115
+ data: unknown
116
+ ): WorkspaceParseCheckResult & { value?: WorkspaceProjectRecord } {
117
+ const check = validateWorkspaceData(data);
118
+ if (!check.ok || !isRecord(data)) {
119
+ return check;
120
+ }
121
+ const view = parseViewData(data);
122
+ const blueprint = parseBlueprintData(data);
123
+ if (!view.value || !blueprint.value) {
124
+ return check;
125
+ }
126
+ const now = Date.now();
127
+ return {
128
+ ...check,
129
+ value: {
130
+ id: typeof data.id === "string" && data.id ? data.id : createWorkspaceProjectId(),
131
+ name: typeof data.name === "string" ? data.name : "",
132
+ createdAt: typeof data.createdAt === "number" ? data.createdAt : now,
133
+ updatedAt: typeof data.updatedAt === "number" ? data.updatedAt : now,
134
+ panelState: view.value,
135
+ blueprintDocument: blueprint.value as BlueprintDocument,
136
+ blueprintMeta: readBlueprintMeta(data.blueprintMeta),
137
+ productName: typeof data.productName === "string" ? data.productName : "",
138
+ titleIconDataUrl:
139
+ typeof data.titleIconDataUrl === "string" ? data.titleIconDataUrl : undefined,
140
+ },
141
+ };
142
+ }
@@ -1,12 +1,20 @@
1
+ import { appStorageKey } from "@arronqzy/blueprint-dsl";
1
2
  import type { WorkspaceProjectRecord } from "./workspace-project-db";
2
3
 
3
4
  const CACHE_PREFIX = "arronqzy-workspace-preview:";
4
5
 
5
- export function writeWorkspacePreviewCache(record: WorkspaceProjectRecord): void {
6
+ function previewCacheKey(projectId: string, nameSpace?: string | null): string {
7
+ return appStorageKey(CACHE_PREFIX + projectId, nameSpace);
8
+ }
9
+
10
+ export function writeWorkspacePreviewCache(
11
+ record: WorkspaceProjectRecord,
12
+ nameSpace?: string | null
13
+ ): void {
6
14
  try {
7
15
  // 只写轻量标记,避免把含 data URL 的整包再塞进 localStorage(配额小、内存翻倍)
8
16
  localStorage.setItem(
9
- CACHE_PREFIX + record.id,
17
+ previewCacheKey(record.id, nameSpace),
10
18
  JSON.stringify({ id: record.id, updatedAt: record.updatedAt })
11
19
  );
12
20
  } catch {
@@ -15,10 +23,11 @@ export function writeWorkspacePreviewCache(record: WorkspaceProjectRecord): void
15
23
  }
16
24
 
17
25
  export function readWorkspacePreviewCache(
18
- projectId: string
26
+ projectId: string,
27
+ nameSpace?: string | null
19
28
  ): WorkspaceProjectRecord | null {
20
29
  try {
21
- const raw = localStorage.getItem(CACHE_PREFIX + projectId);
30
+ const raw = localStorage.getItem(previewCacheKey(projectId, nameSpace));
22
31
  if (!raw) return null;
23
32
  const parsed = JSON.parse(raw) as Partial<WorkspaceProjectRecord> | null;
24
33
  if (!parsed || typeof parsed !== "object" || !parsed.panelState) return null;
@@ -1,6 +1,7 @@
1
1
  import type { BlueprintDocument } from "@arronqzy/react-blueprint";
2
2
  import type { BlueprintMetaDraft } from "@arronqzy/react-blueprint";
3
3
  import type { State } from "@arronqzy/rx-store";
4
+ import { appStorageKey } from "@arronqzy/blueprint-dsl";
4
5
 
5
6
  const DB_NAME = "arronqzy-workspace-projects";
6
7
  const DB_VERSION = 1;
@@ -25,9 +26,9 @@ export type WorkspaceProjectRecord = {
25
26
  titleIconDataUrl?: string;
26
27
  };
27
28
 
28
- function openDb(): Promise<IDBDatabase> {
29
+ function openDb(nameSpace?: string | null): Promise<IDBDatabase> {
29
30
  return new Promise((resolve, reject) => {
30
- const request = indexedDB.open(DB_NAME, DB_VERSION);
31
+ const request = indexedDB.open(appStorageKey(DB_NAME, nameSpace), DB_VERSION);
31
32
  request.onerror = () => reject(request.error ?? new Error("indexeddb-open-failed"));
32
33
  request.onupgradeneeded = () => {
33
34
  const db = request.result;
@@ -42,9 +43,10 @@ function openDb(): Promise<IDBDatabase> {
42
43
 
43
44
  function runTransaction<T>(
44
45
  mode: IDBTransactionMode,
45
- runner: (store: IDBObjectStore) => IDBRequest<T>
46
+ runner: (store: IDBObjectStore) => IDBRequest<T>,
47
+ nameSpace?: string | null
46
48
  ): Promise<T> {
47
- return openDb().then(
49
+ return openDb(nameSpace).then(
48
50
  (db) =>
49
51
  new Promise<T>((resolve, reject) => {
50
52
  const tx = db.transaction(STORE_NAME, mode);
@@ -77,8 +79,10 @@ export function createWorkspaceProjectId(): string {
77
79
  return `ws-${Date.now()}-${Math.random().toString(36).slice(2, 10)}`;
78
80
  }
79
81
 
80
- export async function listWorkspaceProjects(): Promise<WorkspaceProjectListItem[]> {
81
- const db = await openDb();
82
+ export async function listWorkspaceProjects(
83
+ nameSpace?: string | null
84
+ ): Promise<WorkspaceProjectListItem[]> {
85
+ const db = await openDb(nameSpace);
82
86
  return new Promise((resolve, reject) => {
83
87
  const tx = db.transaction(STORE_NAME, "readonly");
84
88
  const store = tx.objectStore(STORE_NAME);
@@ -110,21 +114,28 @@ export async function listWorkspaceProjects(): Promise<WorkspaceProjectListItem[
110
114
  }
111
115
 
112
116
  export async function getWorkspaceProject(
113
- id: string
117
+ id: string,
118
+ nameSpace?: string | null
114
119
  ): Promise<WorkspaceProjectRecord | null> {
115
- const record = await runTransaction<WorkspaceProjectRecord | undefined>("readonly", (store) =>
116
- store.get(id)
120
+ const record = await runTransaction<WorkspaceProjectRecord | undefined>(
121
+ "readonly",
122
+ (store) => store.get(id),
123
+ nameSpace
117
124
  );
118
125
  return record ?? null;
119
126
  }
120
127
 
121
128
  export async function putWorkspaceProject(
122
- record: WorkspaceProjectRecord
129
+ record: WorkspaceProjectRecord,
130
+ nameSpace?: string | null
123
131
  ): Promise<WorkspaceProjectRecord> {
124
- await runTransaction<IDBValidKey>("readwrite", (store) => store.put(record));
132
+ await runTransaction<IDBValidKey>("readwrite", (store) => store.put(record), nameSpace);
125
133
  return record;
126
134
  }
127
135
 
128
- export async function deleteWorkspaceProject(id: string): Promise<void> {
129
- await runTransaction<undefined>("readwrite", (store) => store.delete(id));
136
+ export async function deleteWorkspaceProject(
137
+ id: string,
138
+ nameSpace?: string | null
139
+ ): Promise<void> {
140
+ await runTransaction<undefined>("readwrite", (store) => store.delete(id), nameSpace);
130
141
  }
@@ -1,3 +1,5 @@
1
+ import { appStorageKey } from "@arronqzy/blueprint-dsl";
2
+
1
3
  const CHANNEL_PREFIX = "arronqzy-workspace-project";
2
4
 
3
5
  export type WorkspaceProjectSyncMessage = {
@@ -6,16 +8,22 @@ export type WorkspaceProjectSyncMessage = {
6
8
  updatedAt: number;
7
9
  };
8
10
 
9
- export function getWorkspaceProjectChannelName(projectId: string): string {
10
- return `${CHANNEL_PREFIX}:${projectId}`;
11
+ export function getWorkspaceProjectChannelName(
12
+ projectId: string,
13
+ nameSpace?: string | null
14
+ ): string {
15
+ return appStorageKey(`${CHANNEL_PREFIX}:${projectId}`, nameSpace);
11
16
  }
12
17
 
13
18
  export function broadcastWorkspaceProjectUpdate(
14
19
  projectId: string,
15
- updatedAt: number
20
+ updatedAt: number,
21
+ nameSpace?: string | null
16
22
  ): void {
17
23
  if (typeof BroadcastChannel === "undefined") return;
18
- const channel = new BroadcastChannel(getWorkspaceProjectChannelName(projectId));
24
+ const channel = new BroadcastChannel(
25
+ getWorkspaceProjectChannelName(projectId, nameSpace)
26
+ );
19
27
  channel.postMessage({
20
28
  type: "updated",
21
29
  projectId,
@@ -26,12 +34,15 @@ export function broadcastWorkspaceProjectUpdate(
26
34
 
27
35
  export function subscribeWorkspaceProjectUpdates(
28
36
  projectId: string,
29
- onUpdate: (message: WorkspaceProjectSyncMessage) => void
37
+ onUpdate: (message: WorkspaceProjectSyncMessage) => void,
38
+ nameSpace?: string | null
30
39
  ): () => void {
31
40
  if (typeof BroadcastChannel === "undefined") {
32
41
  return () => {};
33
42
  }
34
- const channel = new BroadcastChannel(getWorkspaceProjectChannelName(projectId));
43
+ const channel = new BroadcastChannel(
44
+ getWorkspaceProjectChannelName(projectId, nameSpace)
45
+ );
35
46
  channel.onmessage = (event: MessageEvent<WorkspaceProjectSyncMessage>) => {
36
47
  const data = event.data;
37
48
  if (!data || data.type !== "updated" || data.projectId !== projectId) return;