@thatopen/services 0.3.15 → 0.4.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.
Files changed (68) hide show
  1. package/README.md +62 -62
  2. package/dist/built-in/index.d.ts +165 -2
  3. package/dist/cli.js +27 -13
  4. package/dist/core/client.d.ts +1 -0
  5. package/dist/core/platform-client.d.ts +7 -0
  6. package/dist/index.cjs.js +2 -2
  7. package/dist/index.es.js +396 -374
  8. package/dist/vite-env.d.ts +1 -1
  9. package/docs/builtin/paths.json +94 -86
  10. package/docs/cli/create.md +29 -29
  11. package/docs/cli/local-server.md +38 -38
  12. package/docs/cli/login.md +32 -32
  13. package/docs/cli/publish.md +31 -31
  14. package/docs/cli/run.md +26 -26
  15. package/docs/cli/serve.md +43 -43
  16. package/docs/cli/swap.md +30 -30
  17. package/package.json +4 -4
  18. package/resources/AGENTS.md +40 -40
  19. package/src/built-in/CDEManager/example.ts +214 -214
  20. package/src/built-in/CDEManager/examples/FileEditing/example.ts +226 -226
  21. package/src/built-in/CDEManager/examples/FileMoves/example.ts +186 -186
  22. package/src/built-in/CDEManager/examples/FileOperations/example.ts +295 -295
  23. package/src/built-in/CDEManager/examples/FileTransfers/example.ts +210 -210
  24. package/src/built-in/CDEManager/examples/FolderManagement/example.ts +230 -230
  25. package/src/built-in/CDEManager/examples/MetadataSchema/example.ts +230 -230
  26. package/src/built-in/CDEManager/examples/MetadataViews/example.ts +166 -166
  27. package/src/built-in/CDEManager/examples/ProjectConfig/example.ts +177 -177
  28. package/src/built-in/ClashesManager/example.ts +283 -283
  29. package/src/built-in/UIManager/src/app/example.ts +97 -97
  30. package/src/built-in/UIManager/src/app/examples/AppState/example.ts +140 -140
  31. package/src/built-in/UIManager/src/app/examples/Banner/example.ts +159 -159
  32. package/src/built-in/UIManager/src/app/examples/Contexts/example.ts +123 -123
  33. package/src/built-in/UIManager/src/app/examples/FileDrop/example.ts +124 -124
  34. package/src/built-in/UIManager/src/app/examples/FloatingLayout/example.ts +90 -90
  35. package/src/built-in/UIManager/src/app/examples/Modal/example.ts +163 -163
  36. package/src/built-in/UIManager/src/app/examples/Notifications/example.ts +133 -133
  37. package/src/built-in/UIManager/src/app/examples/ProjectFileStorage/example.ts +150 -0
  38. package/src/built-in/UIManager/src/app/examples/Sidebar/example.ts +102 -102
  39. package/src/built-in/UIManager/src/clashes-panel/example.ts +183 -160
  40. package/src/built-in/UIManager/src/models-list/example.ts +122 -122
  41. package/src/built-in/UIManager/src/topics-panel/example.ts +170 -0
  42. package/src/built-in/index.ts +1048 -881
  43. package/src/cli/templates/app/index.html +35 -35
  44. package/src/cli/templates/app/package.json +32 -32
  45. package/src/cli/templates/app/src/bim-components/CloudRunner/index.ts +95 -95
  46. package/src/cli/templates/app/src/bim-components/CloudRunner/src/index.ts +1 -1
  47. package/src/cli/templates/app/src/bim-components/CloudRunner/src/types.ts +5 -5
  48. package/src/cli/templates/app/src/bim-components/index.ts +1 -1
  49. package/src/cli/templates/app/src/globals.ts +1 -1
  50. package/src/cli/templates/app/src/ui-components/app-info-section/index.ts +26 -26
  51. package/src/cli/templates/app/src/ui-components/app-info-section/src/index.ts +1 -1
  52. package/src/cli/templates/app/src/ui-components/app-info-section/src/types.ts +15 -15
  53. package/src/cli/templates/app/src/ui-components/cloud-runner-section/index.ts +37 -37
  54. package/src/cli/templates/app/src/ui-components/cloud-runner-section/src/index.ts +1 -1
  55. package/src/cli/templates/app/src/ui-components/cloud-runner-section/src/types.ts +14 -14
  56. package/src/cli/templates/app/src/ui-components/index.ts +2 -2
  57. package/src/cli/templates/app/tsconfig.json +16 -16
  58. package/src/cli/templates/app/vite.config.js +57 -57
  59. package/src/cli/templates/cloud-component/README.md +205 -205
  60. package/src/cli/templates/cloud-component/_thatopen +5 -5
  61. package/src/cli/templates/cloud-component/declarations.json +4 -4
  62. package/src/cli/templates/cloud-component/package.json +21 -21
  63. package/src/cli/templates/cloud-component/src/main.ts +70 -70
  64. package/src/cli/templates/cloud-component/tsconfig.json +16 -16
  65. package/src/cli/templates/cloud-component/vite.config.js +46 -46
  66. package/src/cli/templates/shared/AGENTS.md +8 -8
  67. package/src/cli/templates/shared/CLAUDE.md +1 -1
  68. 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`);