@thatopen/services 0.8.0 → 0.9.0
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/README.md +62 -62
- package/dist/built-in/index.d.ts +66 -41
- package/dist/cli.js +31 -12
- package/dist/core/client.d.ts +6 -0
- package/dist/core/platform-client.d.ts +139 -0
- package/dist/core/platform-client.live.test.d.ts +1 -0
- package/dist/core/platform-client.notifications.test.d.ts +1 -0
- package/dist/index.cjs.js +2 -2
- package/dist/index.d.ts +1 -0
- package/dist/index.es.js +635 -445
- package/dist/types/notifications.d.ts +54 -0
- package/dist/vite-env.d.ts +1 -1
- package/docs/cli/create.md +29 -29
- package/docs/cli/local-server.md +38 -38
- package/docs/cli/login.md +32 -32
- package/docs/cli/publish.md +31 -31
- package/docs/cli/run.md +26 -26
- package/docs/cli/serve.md +43 -43
- package/docs/cli/swap.md +30 -30
- package/package.json +1 -1
- package/src/built-in/CDEManager/example.ts +214 -214
- package/src/built-in/CDEManager/examples/FileEditing/example.ts +226 -226
- package/src/built-in/CDEManager/examples/FileMoves/example.ts +186 -186
- package/src/built-in/CDEManager/examples/FileOperations/example.ts +295 -295
- package/src/built-in/CDEManager/examples/FileTransfers/example.ts +210 -210
- package/src/built-in/CDEManager/examples/FolderManagement/example.ts +230 -230
- package/src/built-in/CDEManager/examples/MetadataSchema/example.ts +230 -230
- package/src/built-in/CDEManager/examples/MetadataViews/example.ts +166 -166
- package/src/built-in/CDEManager/examples/ProjectConfig/example.ts +177 -177
- package/src/built-in/UIManager/src/app/example.ts +97 -97
- package/src/built-in/UIManager/src/app/examples/AppState/example.ts +140 -140
- package/src/built-in/UIManager/src/app/examples/Banner/example.ts +159 -159
- package/src/built-in/UIManager/src/app/examples/Contexts/example.ts +123 -123
- package/src/built-in/UIManager/src/app/examples/FileDrop/example.ts +124 -124
- package/src/built-in/UIManager/src/app/examples/FloatingLayout/example.ts +90 -90
- package/src/built-in/UIManager/src/app/examples/Modal/example.ts +163 -163
- package/src/built-in/UIManager/src/app/examples/Notifications/example.ts +133 -133
- package/src/built-in/UIManager/src/app/examples/ProjectFileStorage/example.ts +150 -150
- package/src/built-in/UIManager/src/app/examples/Sidebar/example.ts +102 -102
- package/src/built-in/UIManager/src/clashes-panel/example.ts +183 -183
- package/src/built-in/UIManager/src/git-history-panel/example.ts +70 -70
- package/src/built-in/UIManager/src/models-list/example.ts +122 -122
- package/src/built-in/UIManager/src/topics-panel/example.ts +170 -170
- package/src/built-in/index.ts +66 -41
- package/src/cli/templates/app/index.html +35 -35
- package/src/cli/templates/app/package.json +32 -32
- package/src/cli/templates/app/src/bim-components/CloudRunner/index.ts +95 -95
- package/src/cli/templates/app/src/bim-components/CloudRunner/src/index.ts +1 -1
- package/src/cli/templates/app/src/bim-components/CloudRunner/src/types.ts +5 -5
- package/src/cli/templates/app/src/bim-components/index.ts +1 -1
- package/src/cli/templates/app/src/globals.ts +1 -1
- package/src/cli/templates/app/src/ui-components/app-info-section/index.ts +26 -26
- package/src/cli/templates/app/src/ui-components/app-info-section/src/index.ts +1 -1
- package/src/cli/templates/app/src/ui-components/app-info-section/src/types.ts +15 -15
- package/src/cli/templates/app/src/ui-components/cloud-runner-section/index.ts +37 -37
- package/src/cli/templates/app/src/ui-components/cloud-runner-section/src/index.ts +1 -1
- package/src/cli/templates/app/src/ui-components/cloud-runner-section/src/types.ts +14 -14
- package/src/cli/templates/app/src/ui-components/index.ts +2 -2
- package/src/cli/templates/app/tsconfig.json +16 -16
- package/src/cli/templates/app/vite.config.js +57 -57
- package/src/cli/templates/cloud-component/README.md +205 -205
- package/src/cli/templates/cloud-component/_thatopen +5 -5
- package/src/cli/templates/cloud-component/declarations.json +4 -4
- package/src/cli/templates/cloud-component/package.json +21 -21
- package/src/cli/templates/cloud-component/src/main.ts +70 -70
- package/src/cli/templates/cloud-component/tsconfig.json +16 -16
- package/src/cli/templates/cloud-component/vite.config.js +46 -46
- package/src/cli/templates/shared/AGENTS.md +8 -8
- package/src/cli/templates/shared/CLAUDE.md +1 -1
- package/src/cli/templates/shared/_gitignore +5 -5
|
@@ -1,210 +1,210 @@
|
|
|
1
|
-
/* MD
|
|
2
|
-
## File uploads, versioning, and the download cache
|
|
3
|
-
---
|
|
4
|
-
A site coordinator drops a batch of files onto the CDE every Monday after the
|
|
5
|
-
weekend review: some are entirely new models, some are revised versions of files
|
|
6
|
-
already in the project, and sometimes the metadata group they upload into should
|
|
7
|
-
be stamped automatically onto the new files. If the platform makes the coordinator
|
|
8
|
-
sort this out manually, it slows the handoff and introduces naming errors. And if
|
|
9
|
-
an upload fails halfway through a batch, the file table should land in a safe state
|
|
10
|
-
rather than freeze at a loading spinner.
|
|
11
|
-
|
|
12
|
-
CDEManager's upload method compares each incoming file against the current list by
|
|
13
|
-
base name and extension. Files with no match are created from scratch; files that
|
|
14
|
-
match an existing entry produce a new version instead. When a metadata group is
|
|
15
|
-
active, the group's values are stamped onto every file in the batch without any
|
|
16
|
-
extra action from the user. The state machine moves through a dedicated uploading
|
|
17
|
-
state so any component in the platform can show a progress indicator by reading a
|
|
18
|
-
single source of truth.
|
|
19
|
-
|
|
20
|
-
This tutorial covers uploading new files and reading the result from the
|
|
21
|
-
upload-completed event; uploading a file whose name matches an existing entry and
|
|
22
|
-
confirming it is versioned rather than duplicated; attaching metadata automatically
|
|
23
|
-
when the upload runs inside an active metadata group; recovering cleanly from an
|
|
24
|
-
upload error through the state machine; and reading file content through the
|
|
25
|
-
in-memory cache that avoids re-fetching the same version twice.
|
|
26
|
-
|
|
27
|
-
By the end, you'll have a working demonstration of the full upload lifecycle —
|
|
28
|
-
create, version, auto-metadata, error recovery — plus the cache hit and miss
|
|
29
|
-
behavior that the viewer relies on when loading models.
|
|
30
|
-
*/
|
|
31
|
-
|
|
32
|
-
import * as OBC from "@thatopen/components";
|
|
33
|
-
// In production platform apps, CDEManager is loaded through the platform client:
|
|
34
|
-
// const cde = await client.initBuiltInComponent(CDEManager.uuid, components);
|
|
35
|
-
// Here we import directly from source for local testing.
|
|
36
|
-
import { CDEManager } from "../../index";
|
|
37
|
-
import type { CDEFile } from "../../src/types";
|
|
38
|
-
|
|
39
|
-
/* MD
|
|
40
|
-
### 🛠️ Setting up
|
|
41
|
-
All flows in this example use a minimal mock client so they run without a backend
|
|
42
|
-
connection. Unlike mock flows that clear the client context after loading permissions,
|
|
43
|
-
upload and download operations require an active client throughout — so the context
|
|
44
|
-
stays set for the full example.
|
|
45
|
-
*/
|
|
46
|
-
|
|
47
|
-
const components = new OBC.Components();
|
|
48
|
-
const cde = components.get(CDEManager);
|
|
49
|
-
|
|
50
|
-
const output = document.getElementById("output") as HTMLPreElement;
|
|
51
|
-
const log = (msg: string) => { console.log(msg); output.textContent += msg + "\n"; };
|
|
52
|
-
|
|
53
|
-
let fileCounter = 0;
|
|
54
|
-
// Track created version records so the versioning demo can find them.
|
|
55
|
-
const mockVersions: Record<string, { tag: string }[]> = {};
|
|
56
|
-
|
|
57
|
-
const mockClient = {
|
|
58
|
-
checkPermissionBatch: async () => Array(5).fill({ hasPermission: true }),
|
|
59
|
-
getProjectData: async () => ({ project: { title: "Demo Project" }, users: [] }),
|
|
60
|
-
listFolders: async () => [],
|
|
61
|
-
createFile: async ({ name, versionTag, parentFolderId }: any) => {
|
|
62
|
-
const id = `file-${fileCounter++}`;
|
|
63
|
-
mockVersions[id] = [{ tag: versionTag }];
|
|
64
|
-
return {
|
|
65
|
-
item: { _id: id, name, folderId: parentFolderId ?? null, createdAt: new Date().toISOString(), createdBy: "mock-user", itemType: "FILE" },
|
|
66
|
-
version: { tag: versionTag },
|
|
67
|
-
};
|
|
68
|
-
},
|
|
69
|
-
listVersions: async (fileId: string) => mockVersions[fileId] ?? [],
|
|
70
|
-
createVersion: async (fileId: string, _file: File, tag: string) => {
|
|
71
|
-
mockVersions[fileId] = [...(mockVersions[fileId] ?? []), { tag }];
|
|
72
|
-
return { tag };
|
|
73
|
-
},
|
|
74
|
-
updateFileVersionMetadata: async () => ({}),
|
|
75
|
-
downloadFile: async (_id: string) => ({ arrayBuffer: async () => new ArrayBuffer(4096) }),
|
|
76
|
-
archiveFile: async () => ({}),
|
|
77
|
-
};
|
|
78
|
-
|
|
79
|
-
cde.ctx = { client: mockClient as any, projectId: "mock" };
|
|
80
|
-
await cde.loadFolders();
|
|
81
|
-
|
|
82
|
-
cde.onMachineStateChanged.add((s) => log(` → ${s.kind}`));
|
|
83
|
-
|
|
84
|
-
/* MD
|
|
85
|
-
### 📤 Uploading new files
|
|
86
|
-
Calling the upload method transitions the machine to an uploading state, carries
|
|
87
|
-
out the operation, then returns to idle and fires a completion event. The event
|
|
88
|
-
payload separates newly created files from versioned ones, so the UI can show a
|
|
89
|
-
targeted confirmation — "2 files added" versus "1 file updated" — without parsing
|
|
90
|
-
the whole file list.
|
|
91
|
-
*/
|
|
92
|
-
|
|
93
|
-
cde.onUploadCompleted.add(({ created, versioned }) => {
|
|
94
|
-
log(` [onUploadCompleted] created: ${created.length}, versioned: ${versioned.length}`);
|
|
95
|
-
for (const f of created) log(` + ${f.name}`);
|
|
96
|
-
for (const f of versioned) log(` ↑ ${f.name}`);
|
|
97
|
-
});
|
|
98
|
-
|
|
99
|
-
log("\n--- upload: create new files ---");
|
|
100
|
-
const f1 = new File([new ArrayBuffer(100)], "column.ifc", { type: "application/octet-stream" });
|
|
101
|
-
const f2 = new File([new ArrayBuffer(50)], "report.pdf");
|
|
102
|
-
await cde.uploadFiles([f1, f2]);
|
|
103
|
-
log(` cde.files after upload: ${cde.files.length}`);
|
|
104
|
-
|
|
105
|
-
/* MD
|
|
106
|
-
### 🔄 Versioning an existing file
|
|
107
|
-
When the incoming file's base name and extension match an entry already in the list,
|
|
108
|
-
the upload method creates a new version rather than a duplicate. The file counter in
|
|
109
|
-
the version tag is computed from the existing version history, so the tag always
|
|
110
|
-
follows on from whatever the project already has.
|
|
111
|
-
*/
|
|
112
|
-
|
|
113
|
-
log("\n--- upload: version existing file ---");
|
|
114
|
-
// "column.ifc" is already in cde.files from the previous upload.
|
|
115
|
-
const f1v2 = new File([new ArrayBuffer(120)], "column.ifc", { type: "application/octet-stream" });
|
|
116
|
-
await cde.uploadFiles([f1v2]);
|
|
117
|
-
log(` cde.files after version upload: ${cde.files.length}`);
|
|
118
|
-
|
|
119
|
-
const colFile = cde.files.find(f => f.name === "column.ifc")!;
|
|
120
|
-
log(` column.ifc version tag: ${(colFile as any).versionTag}`);
|
|
121
|
-
|
|
122
|
-
/* MD
|
|
123
|
-
### 🏷️ Auto-metadata from a metadata group
|
|
124
|
-
When metadata grouping is active and a group is selected, every file uploaded in
|
|
125
|
-
that session inherits the group's metadata values automatically. The coordinator
|
|
126
|
-
selects the "Structural SD phase" group, drops files, and every new file arrives
|
|
127
|
-
already tagged — no post-upload editing required.
|
|
128
|
-
*/
|
|
129
|
-
|
|
130
|
-
log("\n--- upload: auto-metadata ---");
|
|
131
|
-
cde.sendMachineEvent({ type: "SET_GROUPS_VIEW_MODE", mode: "metadata" });
|
|
132
|
-
cde.selectedMetadataGroup = { discipline: "STR", phase: "SD" };
|
|
133
|
-
|
|
134
|
-
const f3 = new File([new ArrayBuffer(80)], "foundations-v2.ifc", { type: "application/octet-stream" });
|
|
135
|
-
await cde.uploadFiles([f3]);
|
|
136
|
-
|
|
137
|
-
const uploaded = cde.files.find(f => f.name === "foundations-v2.ifc")!;
|
|
138
|
-
log(` discipline: ${uploaded.metadata.discipline}, phase: ${uploaded.metadata.phase}`);
|
|
139
|
-
|
|
140
|
-
// Reset back to groups view for subsequent sections.
|
|
141
|
-
cde.sendMachineEvent({ type: "SET_GROUPS_VIEW_MODE", mode: "groups" });
|
|
142
|
-
cde.selectedMetadataGroup = null;
|
|
143
|
-
|
|
144
|
-
/* MD
|
|
145
|
-
### ⚠️ Recovering from an upload error
|
|
146
|
-
If the backend rejects the upload, the machine moves to an error state rather than
|
|
147
|
-
silently returning to idle. The error state carries the reason message so the UI can
|
|
148
|
-
surface it, and sending a dismiss event returns to idle so the user can try again.
|
|
149
|
-
No manual state cleanup is needed.
|
|
150
|
-
*/
|
|
151
|
-
|
|
152
|
-
log("\n--- upload: error recovery ---");
|
|
153
|
-
cde.ctx = {
|
|
154
|
-
client: {
|
|
155
|
-
...mockClient,
|
|
156
|
-
createFile: async () => { throw new Error("Upload failed — server unavailable"); },
|
|
157
|
-
} as any,
|
|
158
|
-
projectId: "mock",
|
|
159
|
-
};
|
|
160
|
-
|
|
161
|
-
const fBroken = new File([new ArrayBuffer(10)], "broken.ifc", { type: "application/octet-stream" });
|
|
162
|
-
await cde.uploadFiles([fBroken]);
|
|
163
|
-
|
|
164
|
-
if (cde.machineState.kind === "error") {
|
|
165
|
-
log(` error reason: ${cde.machineState.reason}`);
|
|
166
|
-
cde.sendMachineEvent({ type: "DISMISS" });
|
|
167
|
-
}
|
|
168
|
-
log(` state after dismiss: ${cde.machineState.kind}`);
|
|
169
|
-
|
|
170
|
-
// Restore the working mock client.
|
|
171
|
-
cde.ctx = { client: mockClient as any, projectId: "mock" };
|
|
172
|
-
|
|
173
|
-
/* MD
|
|
174
|
-
### 💾 File download cache
|
|
175
|
-
The download method on CDEManager keeps a buffer cache in memory so the viewer
|
|
176
|
-
does not re-fetch the same file version twice during a session. The cache logs each
|
|
177
|
-
access — a miss triggers a download, a hit returns the stored buffer immediately.
|
|
178
|
-
Only extensions in `cacheExtensions` are stored; all others are downloaded every
|
|
179
|
-
time so the cache stays focused on the large binary files that benefit most from it.
|
|
180
|
-
|
|
181
|
-
Note: files uploaded in the same session are already placed in the cache by the
|
|
182
|
-
upload method. The demo below uses a file injected directly — simulating a file
|
|
183
|
-
that arrived from a previous session load — to show a clean miss on the first
|
|
184
|
-
access.
|
|
185
|
-
*/
|
|
186
|
-
|
|
187
|
-
log("\n--- download cache ---");
|
|
188
|
-
|
|
189
|
-
// A file loaded from the backend (not uploaded this session) starts with no cache entry.
|
|
190
|
-
const serverFile = {
|
|
191
|
-
_id: "server-ifc", name: "arch.ifc", folderId: null, versionTag: "v3",
|
|
192
|
-
createdBy: "carol", createdAt: new Date(), metadata: {},
|
|
193
|
-
} as CDEFile;
|
|
194
|
-
cde.files = [...cde.files, serverFile];
|
|
195
|
-
|
|
196
|
-
// First access — cache miss, buffer fetched and stored.
|
|
197
|
-
const buf1 = await cde.downloadFile(serverFile);
|
|
198
|
-
log(` arch.ifc first access (miss): ${buf1.byteLength} bytes`);
|
|
199
|
-
|
|
200
|
-
// Second access — cache hit, no network call.
|
|
201
|
-
const buf2 = await cde.downloadFile(serverFile);
|
|
202
|
-
log(` arch.ifc second access (hit): ${buf2.byteLength} bytes`);
|
|
203
|
-
|
|
204
|
-
// PDF extension is not in cacheExtensions — fetched but not stored.
|
|
205
|
-
const pdfFile = cde.files.find(f => f.name === "report.pdf")!;
|
|
206
|
-
const bufPdf = await cde.downloadFile(pdfFile);
|
|
207
|
-
log(` report.pdf access (skip): ${bufPdf.byteLength} bytes`);
|
|
208
|
-
|
|
209
|
-
log(`\n cacheExtensions: [${[...cde.cacheExtensions].join(", ")}]`);
|
|
210
|
-
log(` cacheMaxBytes: ${(cde.cacheMaxBytes / 1024 / 1024).toFixed(0)} MB`);
|
|
1
|
+
/* MD
|
|
2
|
+
## File uploads, versioning, and the download cache
|
|
3
|
+
---
|
|
4
|
+
A site coordinator drops a batch of files onto the CDE every Monday after the
|
|
5
|
+
weekend review: some are entirely new models, some are revised versions of files
|
|
6
|
+
already in the project, and sometimes the metadata group they upload into should
|
|
7
|
+
be stamped automatically onto the new files. If the platform makes the coordinator
|
|
8
|
+
sort this out manually, it slows the handoff and introduces naming errors. And if
|
|
9
|
+
an upload fails halfway through a batch, the file table should land in a safe state
|
|
10
|
+
rather than freeze at a loading spinner.
|
|
11
|
+
|
|
12
|
+
CDEManager's upload method compares each incoming file against the current list by
|
|
13
|
+
base name and extension. Files with no match are created from scratch; files that
|
|
14
|
+
match an existing entry produce a new version instead. When a metadata group is
|
|
15
|
+
active, the group's values are stamped onto every file in the batch without any
|
|
16
|
+
extra action from the user. The state machine moves through a dedicated uploading
|
|
17
|
+
state so any component in the platform can show a progress indicator by reading a
|
|
18
|
+
single source of truth.
|
|
19
|
+
|
|
20
|
+
This tutorial covers uploading new files and reading the result from the
|
|
21
|
+
upload-completed event; uploading a file whose name matches an existing entry and
|
|
22
|
+
confirming it is versioned rather than duplicated; attaching metadata automatically
|
|
23
|
+
when the upload runs inside an active metadata group; recovering cleanly from an
|
|
24
|
+
upload error through the state machine; and reading file content through the
|
|
25
|
+
in-memory cache that avoids re-fetching the same version twice.
|
|
26
|
+
|
|
27
|
+
By the end, you'll have a working demonstration of the full upload lifecycle —
|
|
28
|
+
create, version, auto-metadata, error recovery — plus the cache hit and miss
|
|
29
|
+
behavior that the viewer relies on when loading models.
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
import * as OBC from "@thatopen/components";
|
|
33
|
+
// In production platform apps, CDEManager is loaded through the platform client:
|
|
34
|
+
// const cde = await client.initBuiltInComponent(CDEManager.uuid, components);
|
|
35
|
+
// Here we import directly from source for local testing.
|
|
36
|
+
import { CDEManager } from "../../index";
|
|
37
|
+
import type { CDEFile } from "../../src/types";
|
|
38
|
+
|
|
39
|
+
/* MD
|
|
40
|
+
### 🛠️ Setting up
|
|
41
|
+
All flows in this example use a minimal mock client so they run without a backend
|
|
42
|
+
connection. Unlike mock flows that clear the client context after loading permissions,
|
|
43
|
+
upload and download operations require an active client throughout — so the context
|
|
44
|
+
stays set for the full example.
|
|
45
|
+
*/
|
|
46
|
+
|
|
47
|
+
const components = new OBC.Components();
|
|
48
|
+
const cde = components.get(CDEManager);
|
|
49
|
+
|
|
50
|
+
const output = document.getElementById("output") as HTMLPreElement;
|
|
51
|
+
const log = (msg: string) => { console.log(msg); output.textContent += msg + "\n"; };
|
|
52
|
+
|
|
53
|
+
let fileCounter = 0;
|
|
54
|
+
// Track created version records so the versioning demo can find them.
|
|
55
|
+
const mockVersions: Record<string, { tag: string }[]> = {};
|
|
56
|
+
|
|
57
|
+
const mockClient = {
|
|
58
|
+
checkPermissionBatch: async () => Array(5).fill({ hasPermission: true }),
|
|
59
|
+
getProjectData: async () => ({ project: { title: "Demo Project" }, users: [] }),
|
|
60
|
+
listFolders: async () => [],
|
|
61
|
+
createFile: async ({ name, versionTag, parentFolderId }: any) => {
|
|
62
|
+
const id = `file-${fileCounter++}`;
|
|
63
|
+
mockVersions[id] = [{ tag: versionTag }];
|
|
64
|
+
return {
|
|
65
|
+
item: { _id: id, name, folderId: parentFolderId ?? null, createdAt: new Date().toISOString(), createdBy: "mock-user", itemType: "FILE" },
|
|
66
|
+
version: { tag: versionTag },
|
|
67
|
+
};
|
|
68
|
+
},
|
|
69
|
+
listVersions: async (fileId: string) => mockVersions[fileId] ?? [],
|
|
70
|
+
createVersion: async (fileId: string, _file: File, tag: string) => {
|
|
71
|
+
mockVersions[fileId] = [...(mockVersions[fileId] ?? []), { tag }];
|
|
72
|
+
return { tag };
|
|
73
|
+
},
|
|
74
|
+
updateFileVersionMetadata: async () => ({}),
|
|
75
|
+
downloadFile: async (_id: string) => ({ arrayBuffer: async () => new ArrayBuffer(4096) }),
|
|
76
|
+
archiveFile: async () => ({}),
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
cde.ctx = { client: mockClient as any, projectId: "mock" };
|
|
80
|
+
await cde.loadFolders();
|
|
81
|
+
|
|
82
|
+
cde.onMachineStateChanged.add((s) => log(` → ${s.kind}`));
|
|
83
|
+
|
|
84
|
+
/* MD
|
|
85
|
+
### 📤 Uploading new files
|
|
86
|
+
Calling the upload method transitions the machine to an uploading state, carries
|
|
87
|
+
out the operation, then returns to idle and fires a completion event. The event
|
|
88
|
+
payload separates newly created files from versioned ones, so the UI can show a
|
|
89
|
+
targeted confirmation — "2 files added" versus "1 file updated" — without parsing
|
|
90
|
+
the whole file list.
|
|
91
|
+
*/
|
|
92
|
+
|
|
93
|
+
cde.onUploadCompleted.add(({ created, versioned }) => {
|
|
94
|
+
log(` [onUploadCompleted] created: ${created.length}, versioned: ${versioned.length}`);
|
|
95
|
+
for (const f of created) log(` + ${f.name}`);
|
|
96
|
+
for (const f of versioned) log(` ↑ ${f.name}`);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
log("\n--- upload: create new files ---");
|
|
100
|
+
const f1 = new File([new ArrayBuffer(100)], "column.ifc", { type: "application/octet-stream" });
|
|
101
|
+
const f2 = new File([new ArrayBuffer(50)], "report.pdf");
|
|
102
|
+
await cde.uploadFiles([f1, f2]);
|
|
103
|
+
log(` cde.files after upload: ${cde.files.length}`);
|
|
104
|
+
|
|
105
|
+
/* MD
|
|
106
|
+
### 🔄 Versioning an existing file
|
|
107
|
+
When the incoming file's base name and extension match an entry already in the list,
|
|
108
|
+
the upload method creates a new version rather than a duplicate. The file counter in
|
|
109
|
+
the version tag is computed from the existing version history, so the tag always
|
|
110
|
+
follows on from whatever the project already has.
|
|
111
|
+
*/
|
|
112
|
+
|
|
113
|
+
log("\n--- upload: version existing file ---");
|
|
114
|
+
// "column.ifc" is already in cde.files from the previous upload.
|
|
115
|
+
const f1v2 = new File([new ArrayBuffer(120)], "column.ifc", { type: "application/octet-stream" });
|
|
116
|
+
await cde.uploadFiles([f1v2]);
|
|
117
|
+
log(` cde.files after version upload: ${cde.files.length}`);
|
|
118
|
+
|
|
119
|
+
const colFile = cde.files.find(f => f.name === "column.ifc")!;
|
|
120
|
+
log(` column.ifc version tag: ${(colFile as any).versionTag}`);
|
|
121
|
+
|
|
122
|
+
/* MD
|
|
123
|
+
### 🏷️ Auto-metadata from a metadata group
|
|
124
|
+
When metadata grouping is active and a group is selected, every file uploaded in
|
|
125
|
+
that session inherits the group's metadata values automatically. The coordinator
|
|
126
|
+
selects the "Structural SD phase" group, drops files, and every new file arrives
|
|
127
|
+
already tagged — no post-upload editing required.
|
|
128
|
+
*/
|
|
129
|
+
|
|
130
|
+
log("\n--- upload: auto-metadata ---");
|
|
131
|
+
cde.sendMachineEvent({ type: "SET_GROUPS_VIEW_MODE", mode: "metadata" });
|
|
132
|
+
cde.selectedMetadataGroup = { discipline: "STR", phase: "SD" };
|
|
133
|
+
|
|
134
|
+
const f3 = new File([new ArrayBuffer(80)], "foundations-v2.ifc", { type: "application/octet-stream" });
|
|
135
|
+
await cde.uploadFiles([f3]);
|
|
136
|
+
|
|
137
|
+
const uploaded = cde.files.find(f => f.name === "foundations-v2.ifc")!;
|
|
138
|
+
log(` discipline: ${uploaded.metadata.discipline}, phase: ${uploaded.metadata.phase}`);
|
|
139
|
+
|
|
140
|
+
// Reset back to groups view for subsequent sections.
|
|
141
|
+
cde.sendMachineEvent({ type: "SET_GROUPS_VIEW_MODE", mode: "groups" });
|
|
142
|
+
cde.selectedMetadataGroup = null;
|
|
143
|
+
|
|
144
|
+
/* MD
|
|
145
|
+
### ⚠️ Recovering from an upload error
|
|
146
|
+
If the backend rejects the upload, the machine moves to an error state rather than
|
|
147
|
+
silently returning to idle. The error state carries the reason message so the UI can
|
|
148
|
+
surface it, and sending a dismiss event returns to idle so the user can try again.
|
|
149
|
+
No manual state cleanup is needed.
|
|
150
|
+
*/
|
|
151
|
+
|
|
152
|
+
log("\n--- upload: error recovery ---");
|
|
153
|
+
cde.ctx = {
|
|
154
|
+
client: {
|
|
155
|
+
...mockClient,
|
|
156
|
+
createFile: async () => { throw new Error("Upload failed — server unavailable"); },
|
|
157
|
+
} as any,
|
|
158
|
+
projectId: "mock",
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
const fBroken = new File([new ArrayBuffer(10)], "broken.ifc", { type: "application/octet-stream" });
|
|
162
|
+
await cde.uploadFiles([fBroken]);
|
|
163
|
+
|
|
164
|
+
if (cde.machineState.kind === "error") {
|
|
165
|
+
log(` error reason: ${cde.machineState.reason}`);
|
|
166
|
+
cde.sendMachineEvent({ type: "DISMISS" });
|
|
167
|
+
}
|
|
168
|
+
log(` state after dismiss: ${cde.machineState.kind}`);
|
|
169
|
+
|
|
170
|
+
// Restore the working mock client.
|
|
171
|
+
cde.ctx = { client: mockClient as any, projectId: "mock" };
|
|
172
|
+
|
|
173
|
+
/* MD
|
|
174
|
+
### 💾 File download cache
|
|
175
|
+
The download method on CDEManager keeps a buffer cache in memory so the viewer
|
|
176
|
+
does not re-fetch the same file version twice during a session. The cache logs each
|
|
177
|
+
access — a miss triggers a download, a hit returns the stored buffer immediately.
|
|
178
|
+
Only extensions in `cacheExtensions` are stored; all others are downloaded every
|
|
179
|
+
time so the cache stays focused on the large binary files that benefit most from it.
|
|
180
|
+
|
|
181
|
+
Note: files uploaded in the same session are already placed in the cache by the
|
|
182
|
+
upload method. The demo below uses a file injected directly — simulating a file
|
|
183
|
+
that arrived from a previous session load — to show a clean miss on the first
|
|
184
|
+
access.
|
|
185
|
+
*/
|
|
186
|
+
|
|
187
|
+
log("\n--- download cache ---");
|
|
188
|
+
|
|
189
|
+
// A file loaded from the backend (not uploaded this session) starts with no cache entry.
|
|
190
|
+
const serverFile = {
|
|
191
|
+
_id: "server-ifc", name: "arch.ifc", folderId: null, versionTag: "v3",
|
|
192
|
+
createdBy: "carol", createdAt: new Date(), metadata: {},
|
|
193
|
+
} as CDEFile;
|
|
194
|
+
cde.files = [...cde.files, serverFile];
|
|
195
|
+
|
|
196
|
+
// First access — cache miss, buffer fetched and stored.
|
|
197
|
+
const buf1 = await cde.downloadFile(serverFile);
|
|
198
|
+
log(` arch.ifc first access (miss): ${buf1.byteLength} bytes`);
|
|
199
|
+
|
|
200
|
+
// Second access — cache hit, no network call.
|
|
201
|
+
const buf2 = await cde.downloadFile(serverFile);
|
|
202
|
+
log(` arch.ifc second access (hit): ${buf2.byteLength} bytes`);
|
|
203
|
+
|
|
204
|
+
// PDF extension is not in cacheExtensions — fetched but not stored.
|
|
205
|
+
const pdfFile = cde.files.find(f => f.name === "report.pdf")!;
|
|
206
|
+
const bufPdf = await cde.downloadFile(pdfFile);
|
|
207
|
+
log(` report.pdf access (skip): ${bufPdf.byteLength} bytes`);
|
|
208
|
+
|
|
209
|
+
log(`\n cacheExtensions: [${[...cde.cacheExtensions].join(", ")}]`);
|
|
210
|
+
log(` cacheMaxBytes: ${(cde.cacheMaxBytes / 1024 / 1024).toFixed(0)} MB`);
|