@mountra/mountra-sdk 0.2.0 → 0.3.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/CHANGELOG.md +21 -0
- package/README.md +60 -14
- package/dist/index.cjs +861 -186
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +175 -3
- package/dist/index.d.ts +175 -3
- package/dist/index.js +859 -186
- package/dist/index.js.map +1 -1
- package/package.json +2 -1
package/dist/index.js
CHANGED
|
@@ -1,3 +1,606 @@
|
|
|
1
|
+
// src/file.ts
|
|
2
|
+
function normalizeMountraFileLocator(locator, fallbackWorkspace = {}) {
|
|
3
|
+
if (!locator || typeof locator !== "object") {
|
|
4
|
+
throw new TypeError("Mountra file locator is required");
|
|
5
|
+
}
|
|
6
|
+
const hasPath = locator.path !== void 0;
|
|
7
|
+
const hasIno = locator.ino !== void 0;
|
|
8
|
+
if (hasPath === hasIno) {
|
|
9
|
+
throw new TypeError("Mountra file locator requires exactly one of path or ino");
|
|
10
|
+
}
|
|
11
|
+
const workspaceId = locator.workspaceId ?? locator.wsid ?? fallbackWorkspace.workspaceId;
|
|
12
|
+
if (locator.workspaceId !== void 0 && locator.wsid !== void 0 && String(locator.workspaceId) !== String(locator.wsid)) {
|
|
13
|
+
throw new TypeError("workspaceId and wsid must identify the same workspace");
|
|
14
|
+
}
|
|
15
|
+
const workspaceCuid = locator.workspaceCuid ?? fallbackWorkspace.workspaceCuid;
|
|
16
|
+
if (workspaceCuid === void 0 && workspaceId === void 0) {
|
|
17
|
+
throw new TypeError("workspaceCuid, workspaceId, or wsid is required");
|
|
18
|
+
}
|
|
19
|
+
if (hasPath) {
|
|
20
|
+
const path = normalizePath(locator.path);
|
|
21
|
+
return {
|
|
22
|
+
...workspaceCuid !== void 0 ? { workspaceCuid } : {},
|
|
23
|
+
...workspaceId !== void 0 ? { workspaceId } : {},
|
|
24
|
+
path
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
return {
|
|
28
|
+
...workspaceCuid !== void 0 ? { workspaceCuid } : {},
|
|
29
|
+
...workspaceId !== void 0 ? { workspaceId } : {},
|
|
30
|
+
ino: normalizeIno(locator.ino)
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
var MountraFile = class {
|
|
34
|
+
constructor(client, locator) {
|
|
35
|
+
this.client = client;
|
|
36
|
+
this.locator = Object.freeze({ ...locator });
|
|
37
|
+
}
|
|
38
|
+
client;
|
|
39
|
+
locator;
|
|
40
|
+
get workspaceCuid() {
|
|
41
|
+
return this.locator.workspaceCuid;
|
|
42
|
+
}
|
|
43
|
+
get workspaceId() {
|
|
44
|
+
return this.locator.workspaceId;
|
|
45
|
+
}
|
|
46
|
+
/** `wsid` is the short alias used by the file-object API. */
|
|
47
|
+
get wsid() {
|
|
48
|
+
return this.locator.workspaceId;
|
|
49
|
+
}
|
|
50
|
+
get path() {
|
|
51
|
+
return "path" in this.locator ? this.locator.path : void 0;
|
|
52
|
+
}
|
|
53
|
+
get ino() {
|
|
54
|
+
return "ino" in this.locator ? this.locator.ino : void 0;
|
|
55
|
+
}
|
|
56
|
+
export(options = {}) {
|
|
57
|
+
return this.client.exportFile({ ...options, ...this.locator });
|
|
58
|
+
}
|
|
59
|
+
download(options = {}) {
|
|
60
|
+
return this.client.downloadFile({ ...options, ...this.locator });
|
|
61
|
+
}
|
|
62
|
+
async resume(options = {}) {
|
|
63
|
+
const { downloadKey, ...resumeOptions } = options;
|
|
64
|
+
const checkpoints = await this.client.listResumableDownloads();
|
|
65
|
+
const candidates = checkpoints.filter(
|
|
66
|
+
(checkpoint2) => checkpoint2.fileHandle && matchesLocator(checkpoint2.request, this.locator)
|
|
67
|
+
);
|
|
68
|
+
const checkpoint = downloadKey ? candidates.find((candidate) => candidate.key === downloadKey) : [...candidates].sort((left, right) => right.updatedAt - left.updatedAt)[0];
|
|
69
|
+
if (!checkpoint) {
|
|
70
|
+
if (downloadKey) {
|
|
71
|
+
throw new Error("Download checkpoint does not belong to this Mountra file");
|
|
72
|
+
}
|
|
73
|
+
throw new Error("No resumable download was found for this Mountra file");
|
|
74
|
+
}
|
|
75
|
+
return this.client.resumeDownload(checkpoint.key, resumeOptions);
|
|
76
|
+
}
|
|
77
|
+
};
|
|
78
|
+
function matchesLocator(request, locator) {
|
|
79
|
+
if (locator.workspaceCuid !== void 0 && request.workspaceCuid !== locator.workspaceCuid) {
|
|
80
|
+
return false;
|
|
81
|
+
}
|
|
82
|
+
if (locator.workspaceId !== void 0 && String(request.workspaceId) !== String(locator.workspaceId)) {
|
|
83
|
+
return false;
|
|
84
|
+
}
|
|
85
|
+
if ("path" in locator) {
|
|
86
|
+
if (request.path === void 0 || request.ino !== void 0) {
|
|
87
|
+
return false;
|
|
88
|
+
}
|
|
89
|
+
try {
|
|
90
|
+
return normalizePath(request.path) === locator.path;
|
|
91
|
+
} catch {
|
|
92
|
+
return false;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
return String(request.ino) === String(locator.ino) && request.path === void 0;
|
|
96
|
+
}
|
|
97
|
+
function normalizePath(value) {
|
|
98
|
+
if (!value || !value.trim()) {
|
|
99
|
+
throw new TypeError("Mountra file path cannot be empty");
|
|
100
|
+
}
|
|
101
|
+
const raw = value.trim();
|
|
102
|
+
const absolute = raw.startsWith("/") ? raw : `/${raw}`;
|
|
103
|
+
const segments = absolute.split("/").filter((segment) => segment.length > 0 && segment !== ".");
|
|
104
|
+
const stack = [];
|
|
105
|
+
for (const segment of segments) {
|
|
106
|
+
if (segment === "..") {
|
|
107
|
+
stack.pop();
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
stack.push(segment);
|
|
111
|
+
}
|
|
112
|
+
return stack.length === 0 ? "/" : `/${stack.join("/")}`;
|
|
113
|
+
}
|
|
114
|
+
function normalizeIno(value) {
|
|
115
|
+
if (typeof value === "number") {
|
|
116
|
+
if (!Number.isSafeInteger(value) || value < 0) {
|
|
117
|
+
throw new TypeError("Mountra file ino must be a non-negative safe integer");
|
|
118
|
+
}
|
|
119
|
+
return value;
|
|
120
|
+
}
|
|
121
|
+
if (!/^\d+$/.test(value)) {
|
|
122
|
+
throw new TypeError("Mountra file ino must be a non-negative integer string");
|
|
123
|
+
}
|
|
124
|
+
return value;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// src/cache.ts
|
|
128
|
+
var DEFAULT_MAX_BYTES = 512 * 1024 * 1024;
|
|
129
|
+
var DEFAULT_MAX_CHUNK_BYTES = 128 * 1024 * 1024;
|
|
130
|
+
var DEFAULT_TTL_MS = 5 * 60 * 1e3;
|
|
131
|
+
function copyFile(file) {
|
|
132
|
+
return { ...file, chunks: file.chunks.map((chunk) => ({ ...chunk })) };
|
|
133
|
+
}
|
|
134
|
+
function memoryKey(workspace, key) {
|
|
135
|
+
return JSON.stringify([workspace, key]);
|
|
136
|
+
}
|
|
137
|
+
var InMemoryDownloadCacheStore = class {
|
|
138
|
+
files = /* @__PURE__ */ new Map();
|
|
139
|
+
/** Map iteration order is the LRU order: the first entry is the least recently used. */
|
|
140
|
+
chunks = /* @__PURE__ */ new Map();
|
|
141
|
+
bytes = 0;
|
|
142
|
+
async getFile(workspace, fileKey) {
|
|
143
|
+
const entry = this.files.get(memoryKey(workspace, fileKey));
|
|
144
|
+
return entry ? copyFile(entry.file) : null;
|
|
145
|
+
}
|
|
146
|
+
async putFile(workspace, fileKey, file) {
|
|
147
|
+
this.files.set(memoryKey(workspace, fileKey), { workspace, file: copyFile(file) });
|
|
148
|
+
}
|
|
149
|
+
async removeFile(workspace, fileKey) {
|
|
150
|
+
this.files.delete(memoryKey(workspace, fileKey));
|
|
151
|
+
}
|
|
152
|
+
async getChunk(workspace, storageKey) {
|
|
153
|
+
const key = memoryKey(workspace, storageKey);
|
|
154
|
+
const entry = this.chunks.get(key);
|
|
155
|
+
if (!entry) {
|
|
156
|
+
return null;
|
|
157
|
+
}
|
|
158
|
+
this.chunks.delete(key);
|
|
159
|
+
this.chunks.set(key, entry);
|
|
160
|
+
return entry.data;
|
|
161
|
+
}
|
|
162
|
+
async putChunk(workspace, storageKey, data, maxBytes) {
|
|
163
|
+
const key = memoryKey(workspace, storageKey);
|
|
164
|
+
this.removeChunk(key);
|
|
165
|
+
if (data.size > maxBytes) {
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
this.chunks.set(key, { workspace, data });
|
|
169
|
+
this.bytes += data.size;
|
|
170
|
+
for (const candidate of this.chunks.keys()) {
|
|
171
|
+
if (this.bytes <= maxBytes) {
|
|
172
|
+
break;
|
|
173
|
+
}
|
|
174
|
+
this.removeChunk(candidate);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
async clear(workspace) {
|
|
178
|
+
for (const [key, entry] of this.chunks) {
|
|
179
|
+
if (workspace === void 0 || entry.workspace === workspace) {
|
|
180
|
+
this.removeChunk(key);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
for (const [key, entry] of this.files) {
|
|
184
|
+
if (workspace === void 0 || entry.workspace === workspace) {
|
|
185
|
+
this.files.delete(key);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
async usage(workspace) {
|
|
190
|
+
const usage = { bytes: 0, chunks: 0 };
|
|
191
|
+
for (const entry of this.chunks.values()) {
|
|
192
|
+
if (workspace === void 0 || entry.workspace === workspace) {
|
|
193
|
+
usage.bytes += entry.data.size;
|
|
194
|
+
usage.chunks += 1;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
return usage;
|
|
198
|
+
}
|
|
199
|
+
removeChunk(key) {
|
|
200
|
+
const entry = this.chunks.get(key);
|
|
201
|
+
if (entry) {
|
|
202
|
+
this.chunks.delete(key);
|
|
203
|
+
this.bytes -= entry.data.size;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
};
|
|
207
|
+
var CACHE_DATABASE_NAME = "mountra-sdk-download-cache";
|
|
208
|
+
var CACHE_DATABASE_VERSION = 1;
|
|
209
|
+
var FILES_STORE = "files";
|
|
210
|
+
var CHUNKS_STORE = "chunks";
|
|
211
|
+
var CHUNK_DATA_STORE = "chunk-data";
|
|
212
|
+
var META_STORE = "meta";
|
|
213
|
+
var LRU_INDEX = "lastUsed";
|
|
214
|
+
var USAGE_KEY = "bytes";
|
|
215
|
+
var CLOCK_KEY = "clock";
|
|
216
|
+
function getIndexedDb() {
|
|
217
|
+
return globalThis.indexedDB ?? null;
|
|
218
|
+
}
|
|
219
|
+
function workspaceRange(workspace) {
|
|
220
|
+
return IDBKeyRange.bound([workspace], [workspace, []]);
|
|
221
|
+
}
|
|
222
|
+
function sameKey(left, right) {
|
|
223
|
+
return Array.isArray(left) && left[0] === right[0] && left[1] === right[1];
|
|
224
|
+
}
|
|
225
|
+
async function nextClock(meta) {
|
|
226
|
+
const clock = (await requestResult(meta.get(CLOCK_KEY)) ?? 0) + 1;
|
|
227
|
+
await requestResult(meta.put(clock, CLOCK_KEY));
|
|
228
|
+
return clock;
|
|
229
|
+
}
|
|
230
|
+
function requestResult(request) {
|
|
231
|
+
return new Promise((resolve, reject) => {
|
|
232
|
+
request.onsuccess = () => resolve(request.result);
|
|
233
|
+
request.onerror = () => reject(request.error ?? new Error("IndexedDB request failed"));
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
async function runTransaction(database, storeNames, mode, body) {
|
|
237
|
+
const transaction = database.transaction(storeNames, mode);
|
|
238
|
+
const done = new Promise((resolve, reject) => {
|
|
239
|
+
transaction.oncomplete = () => resolve();
|
|
240
|
+
transaction.onerror = () => reject(transaction.error ?? new Error("IndexedDB transaction failed"));
|
|
241
|
+
transaction.onabort = () => reject(transaction.error ?? new Error("IndexedDB transaction aborted"));
|
|
242
|
+
});
|
|
243
|
+
done.catch(() => void 0);
|
|
244
|
+
let result;
|
|
245
|
+
try {
|
|
246
|
+
result = await body(transaction);
|
|
247
|
+
} catch (error) {
|
|
248
|
+
try {
|
|
249
|
+
transaction.abort();
|
|
250
|
+
} catch {
|
|
251
|
+
}
|
|
252
|
+
throw error;
|
|
253
|
+
}
|
|
254
|
+
await done;
|
|
255
|
+
return result;
|
|
256
|
+
}
|
|
257
|
+
var IndexedDBDownloadCacheStore = class {
|
|
258
|
+
databaseName;
|
|
259
|
+
fallback = new InMemoryDownloadCacheStore();
|
|
260
|
+
databasePromise;
|
|
261
|
+
constructor(options = {}) {
|
|
262
|
+
this.databaseName = options.databaseName ?? CACHE_DATABASE_NAME;
|
|
263
|
+
}
|
|
264
|
+
async getFile(workspace, fileKey) {
|
|
265
|
+
const database = await this.openDatabase();
|
|
266
|
+
if (!database) {
|
|
267
|
+
return this.fallback.getFile(workspace, fileKey);
|
|
268
|
+
}
|
|
269
|
+
return runTransaction(database, [FILES_STORE], "readonly", async (transaction) => {
|
|
270
|
+
const file = await requestResult(
|
|
271
|
+
transaction.objectStore(FILES_STORE).get([workspace, fileKey])
|
|
272
|
+
);
|
|
273
|
+
return file ? copyFile(file) : null;
|
|
274
|
+
});
|
|
275
|
+
}
|
|
276
|
+
async putFile(workspace, fileKey, file) {
|
|
277
|
+
const database = await this.openDatabase();
|
|
278
|
+
if (!database) {
|
|
279
|
+
return this.fallback.putFile(workspace, fileKey, file);
|
|
280
|
+
}
|
|
281
|
+
await runTransaction(database, [FILES_STORE], "readwrite", async (transaction) => {
|
|
282
|
+
await requestResult(transaction.objectStore(FILES_STORE).put(copyFile(file), [workspace, fileKey]));
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
async removeFile(workspace, fileKey) {
|
|
286
|
+
const database = await this.openDatabase();
|
|
287
|
+
if (!database) {
|
|
288
|
+
return this.fallback.removeFile(workspace, fileKey);
|
|
289
|
+
}
|
|
290
|
+
await runTransaction(database, [FILES_STORE], "readwrite", async (transaction) => {
|
|
291
|
+
await requestResult(transaction.objectStore(FILES_STORE).delete([workspace, fileKey]));
|
|
292
|
+
});
|
|
293
|
+
}
|
|
294
|
+
async getChunk(workspace, storageKey) {
|
|
295
|
+
const database = await this.openDatabase();
|
|
296
|
+
if (!database) {
|
|
297
|
+
return this.fallback.getChunk(workspace, storageKey);
|
|
298
|
+
}
|
|
299
|
+
const key = [workspace, storageKey];
|
|
300
|
+
return runTransaction(
|
|
301
|
+
database,
|
|
302
|
+
[CHUNKS_STORE, CHUNK_DATA_STORE, META_STORE],
|
|
303
|
+
"readwrite",
|
|
304
|
+
async (transaction) => {
|
|
305
|
+
const chunks = transaction.objectStore(CHUNKS_STORE);
|
|
306
|
+
const record = await requestResult(chunks.get(key));
|
|
307
|
+
if (!record) {
|
|
308
|
+
return null;
|
|
309
|
+
}
|
|
310
|
+
const data = await requestResult(
|
|
311
|
+
transaction.objectStore(CHUNK_DATA_STORE).get(key)
|
|
312
|
+
);
|
|
313
|
+
const meta = transaction.objectStore(META_STORE);
|
|
314
|
+
if (!data) {
|
|
315
|
+
const bytes = await requestResult(meta.get(USAGE_KEY)) ?? 0;
|
|
316
|
+
await requestResult(chunks.delete(key));
|
|
317
|
+
await requestResult(meta.put(Math.max(bytes - record.size, 0), USAGE_KEY));
|
|
318
|
+
return null;
|
|
319
|
+
}
|
|
320
|
+
await requestResult(chunks.put({ size: record.size, lastUsed: await nextClock(meta) }, key));
|
|
321
|
+
return data;
|
|
322
|
+
}
|
|
323
|
+
);
|
|
324
|
+
}
|
|
325
|
+
async putChunk(workspace, storageKey, data, maxBytes) {
|
|
326
|
+
const database = await this.openDatabase();
|
|
327
|
+
if (!database) {
|
|
328
|
+
return this.fallback.putChunk(workspace, storageKey, data, maxBytes);
|
|
329
|
+
}
|
|
330
|
+
const key = [workspace, storageKey];
|
|
331
|
+
await runTransaction(
|
|
332
|
+
database,
|
|
333
|
+
[CHUNKS_STORE, CHUNK_DATA_STORE, META_STORE],
|
|
334
|
+
"readwrite",
|
|
335
|
+
async (transaction) => {
|
|
336
|
+
const chunks = transaction.objectStore(CHUNKS_STORE);
|
|
337
|
+
const chunkData = transaction.objectStore(CHUNK_DATA_STORE);
|
|
338
|
+
const meta = transaction.objectStore(META_STORE);
|
|
339
|
+
let bytes = await requestResult(meta.get(USAGE_KEY)) ?? 0;
|
|
340
|
+
const existing = await requestResult(chunks.get(key));
|
|
341
|
+
if (existing) {
|
|
342
|
+
bytes -= existing.size;
|
|
343
|
+
await requestResult(chunks.delete(key));
|
|
344
|
+
await requestResult(chunkData.delete(key));
|
|
345
|
+
}
|
|
346
|
+
if (data.size <= maxBytes) {
|
|
347
|
+
await requestResult(chunks.put({ size: data.size, lastUsed: await nextClock(meta) }, key));
|
|
348
|
+
await requestResult(chunkData.put(data, key));
|
|
349
|
+
bytes += data.size;
|
|
350
|
+
}
|
|
351
|
+
if (bytes > maxBytes) {
|
|
352
|
+
const cursorRequest = chunks.index(LRU_INDEX).openCursor();
|
|
353
|
+
let cursor = await requestResult(cursorRequest);
|
|
354
|
+
while (cursor && bytes > maxBytes) {
|
|
355
|
+
if (!sameKey(cursor.primaryKey, key)) {
|
|
356
|
+
bytes -= cursor.value.size;
|
|
357
|
+
await requestResult(chunkData.delete(cursor.primaryKey));
|
|
358
|
+
await requestResult(cursor.delete());
|
|
359
|
+
}
|
|
360
|
+
cursor.continue();
|
|
361
|
+
cursor = await requestResult(cursorRequest);
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
await requestResult(meta.put(Math.max(bytes, 0), USAGE_KEY));
|
|
365
|
+
}
|
|
366
|
+
);
|
|
367
|
+
}
|
|
368
|
+
async clear(workspace) {
|
|
369
|
+
const database = await this.openDatabase();
|
|
370
|
+
if (!database) {
|
|
371
|
+
return this.fallback.clear(workspace);
|
|
372
|
+
}
|
|
373
|
+
await runTransaction(
|
|
374
|
+
database,
|
|
375
|
+
[FILES_STORE, CHUNKS_STORE, CHUNK_DATA_STORE, META_STORE],
|
|
376
|
+
"readwrite",
|
|
377
|
+
async (transaction) => {
|
|
378
|
+
const files = transaction.objectStore(FILES_STORE);
|
|
379
|
+
const chunks = transaction.objectStore(CHUNKS_STORE);
|
|
380
|
+
const chunkData = transaction.objectStore(CHUNK_DATA_STORE);
|
|
381
|
+
const meta = transaction.objectStore(META_STORE);
|
|
382
|
+
if (workspace === void 0) {
|
|
383
|
+
await requestResult(files.clear());
|
|
384
|
+
await requestResult(chunks.clear());
|
|
385
|
+
await requestResult(chunkData.clear());
|
|
386
|
+
await requestResult(meta.put(0, USAGE_KEY));
|
|
387
|
+
return;
|
|
388
|
+
}
|
|
389
|
+
const range = workspaceRange(workspace);
|
|
390
|
+
const removed = await requestResult(chunks.getAll(range));
|
|
391
|
+
const bytes = await requestResult(meta.get(USAGE_KEY)) ?? 0;
|
|
392
|
+
await requestResult(files.delete(range));
|
|
393
|
+
await requestResult(chunks.delete(range));
|
|
394
|
+
await requestResult(chunkData.delete(range));
|
|
395
|
+
await requestResult(
|
|
396
|
+
meta.put(Math.max(bytes - removed.reduce((total, record) => total + record.size, 0), 0), USAGE_KEY)
|
|
397
|
+
);
|
|
398
|
+
}
|
|
399
|
+
);
|
|
400
|
+
}
|
|
401
|
+
async usage(workspace) {
|
|
402
|
+
const database = await this.openDatabase();
|
|
403
|
+
if (!database) {
|
|
404
|
+
return this.fallback.usage(workspace);
|
|
405
|
+
}
|
|
406
|
+
return runTransaction(database, [CHUNKS_STORE], "readonly", async (transaction) => {
|
|
407
|
+
const records = await requestResult(
|
|
408
|
+
transaction.objectStore(CHUNKS_STORE).getAll(workspace === void 0 ? void 0 : workspaceRange(workspace))
|
|
409
|
+
);
|
|
410
|
+
return {
|
|
411
|
+
bytes: records.reduce((total, record) => total + record.size, 0),
|
|
412
|
+
chunks: records.length
|
|
413
|
+
};
|
|
414
|
+
});
|
|
415
|
+
}
|
|
416
|
+
async openDatabase() {
|
|
417
|
+
if (!this.databasePromise) {
|
|
418
|
+
const indexedDb = getIndexedDb();
|
|
419
|
+
if (!indexedDb) {
|
|
420
|
+
this.databasePromise = Promise.resolve(null);
|
|
421
|
+
} else {
|
|
422
|
+
this.databasePromise = new Promise((resolve) => {
|
|
423
|
+
let request;
|
|
424
|
+
try {
|
|
425
|
+
request = indexedDb.open(this.databaseName, CACHE_DATABASE_VERSION);
|
|
426
|
+
} catch {
|
|
427
|
+
resolve(null);
|
|
428
|
+
return;
|
|
429
|
+
}
|
|
430
|
+
request.onupgradeneeded = () => {
|
|
431
|
+
const database = request.result;
|
|
432
|
+
if (!database.objectStoreNames.contains(FILES_STORE)) {
|
|
433
|
+
database.createObjectStore(FILES_STORE);
|
|
434
|
+
}
|
|
435
|
+
if (!database.objectStoreNames.contains(CHUNKS_STORE)) {
|
|
436
|
+
database.createObjectStore(CHUNKS_STORE).createIndex(LRU_INDEX, "lastUsed");
|
|
437
|
+
}
|
|
438
|
+
if (!database.objectStoreNames.contains(CHUNK_DATA_STORE)) {
|
|
439
|
+
database.createObjectStore(CHUNK_DATA_STORE);
|
|
440
|
+
}
|
|
441
|
+
if (!database.objectStoreNames.contains(META_STORE)) {
|
|
442
|
+
database.createObjectStore(META_STORE);
|
|
443
|
+
}
|
|
444
|
+
};
|
|
445
|
+
request.onsuccess = () => {
|
|
446
|
+
const database = request.result;
|
|
447
|
+
database.onversionchange = () => {
|
|
448
|
+
database.close();
|
|
449
|
+
this.databasePromise = void 0;
|
|
450
|
+
};
|
|
451
|
+
resolve(database);
|
|
452
|
+
};
|
|
453
|
+
request.onerror = () => resolve(null);
|
|
454
|
+
request.onblocked = () => resolve(null);
|
|
455
|
+
});
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
return this.databasePromise;
|
|
459
|
+
}
|
|
460
|
+
};
|
|
461
|
+
function isImmutableStorageKey(storageKey) {
|
|
462
|
+
return /(?:^|\/)blobs\/sha256\/[0-9a-f]{64}$/.test(storageKey);
|
|
463
|
+
}
|
|
464
|
+
function workspaceCacheKey(baseUrl, selector) {
|
|
465
|
+
const workspaceCuid = selector.workspaceCuid?.trim();
|
|
466
|
+
const workspaceId = selector.workspaceId !== void 0 ? String(selector.workspaceId).trim() : "";
|
|
467
|
+
const workspace = workspaceCuid || workspaceId;
|
|
468
|
+
return workspace ? JSON.stringify([baseUrl, workspace]) : null;
|
|
469
|
+
}
|
|
470
|
+
function fileCacheKey(target) {
|
|
471
|
+
try {
|
|
472
|
+
if (target.ino !== void 0) {
|
|
473
|
+
return `ino:${BigInt(target.ino).toString()}`;
|
|
474
|
+
}
|
|
475
|
+
if (target.path !== void 0) {
|
|
476
|
+
return `path:${normalizePath(target.path)}`;
|
|
477
|
+
}
|
|
478
|
+
} catch {
|
|
479
|
+
}
|
|
480
|
+
return null;
|
|
481
|
+
}
|
|
482
|
+
function isValidLayout(file) {
|
|
483
|
+
let offset = 0;
|
|
484
|
+
for (const [index, chunk] of file.chunks.entries()) {
|
|
485
|
+
if (chunk.index !== index || chunk.offset !== offset || chunk.size < 0) {
|
|
486
|
+
return false;
|
|
487
|
+
}
|
|
488
|
+
offset += chunk.size;
|
|
489
|
+
}
|
|
490
|
+
return file.chunks.length > 0 && offset === file.size;
|
|
491
|
+
}
|
|
492
|
+
var DownloadCache = class {
|
|
493
|
+
maxBytes;
|
|
494
|
+
maxChunkBytes;
|
|
495
|
+
ttlMs;
|
|
496
|
+
store;
|
|
497
|
+
constructor(options) {
|
|
498
|
+
this.maxBytes = options.maxBytes ?? DEFAULT_MAX_BYTES;
|
|
499
|
+
if (!Number.isSafeInteger(this.maxBytes) || this.maxBytes <= 0) {
|
|
500
|
+
throw new RangeError("downloadCache.maxBytes must be a positive safe integer");
|
|
501
|
+
}
|
|
502
|
+
const maxChunkBytes = options.maxChunkBytes ?? DEFAULT_MAX_CHUNK_BYTES;
|
|
503
|
+
if (!Number.isSafeInteger(maxChunkBytes) || maxChunkBytes <= 0) {
|
|
504
|
+
throw new RangeError("downloadCache.maxChunkBytes must be a positive safe integer");
|
|
505
|
+
}
|
|
506
|
+
this.maxChunkBytes = Math.min(maxChunkBytes, this.maxBytes);
|
|
507
|
+
this.ttlMs = options.ttlMs ?? DEFAULT_TTL_MS;
|
|
508
|
+
if (Number.isNaN(this.ttlMs) || this.ttlMs < 0) {
|
|
509
|
+
throw new RangeError("downloadCache.ttlMs must be a non-negative number");
|
|
510
|
+
}
|
|
511
|
+
this.store = options.store ?? new IndexedDBDownloadCacheStore();
|
|
512
|
+
}
|
|
513
|
+
/** Return a file that has not expired together with all of its chunk bytes. */
|
|
514
|
+
async loadFile(workspace, fileKey) {
|
|
515
|
+
try {
|
|
516
|
+
const file = await this.store.getFile(workspace, fileKey);
|
|
517
|
+
if (!file) {
|
|
518
|
+
return null;
|
|
519
|
+
}
|
|
520
|
+
if (file.expiresAt <= Date.now() || !isValidLayout(file)) {
|
|
521
|
+
await this.store.removeFile(workspace, fileKey);
|
|
522
|
+
return null;
|
|
523
|
+
}
|
|
524
|
+
const chunks = /* @__PURE__ */ new Map();
|
|
525
|
+
for (const chunk of file.chunks) {
|
|
526
|
+
const data = await this.readChunk(workspace, chunk);
|
|
527
|
+
if (!data) {
|
|
528
|
+
return null;
|
|
529
|
+
}
|
|
530
|
+
chunks.set(chunk.index, data);
|
|
531
|
+
}
|
|
532
|
+
return { file, chunks };
|
|
533
|
+
} catch {
|
|
534
|
+
return null;
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
async readChunk(workspace, chunk) {
|
|
538
|
+
if (chunk.size === 0) {
|
|
539
|
+
return new Blob([]);
|
|
540
|
+
}
|
|
541
|
+
if (!isImmutableStorageKey(chunk.storageKey)) {
|
|
542
|
+
return null;
|
|
543
|
+
}
|
|
544
|
+
try {
|
|
545
|
+
const data = await this.store.getChunk(workspace, chunk.storageKey);
|
|
546
|
+
return data && data.size === chunk.size ? data : null;
|
|
547
|
+
} catch {
|
|
548
|
+
return null;
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
canStoreChunk(chunk) {
|
|
552
|
+
return isImmutableStorageKey(chunk.storageKey) && chunk.size <= this.maxChunkBytes;
|
|
553
|
+
}
|
|
554
|
+
/** Returns whether the chunk is now cached. */
|
|
555
|
+
async storeChunk(workspace, chunk, data) {
|
|
556
|
+
if (chunk.size === 0) {
|
|
557
|
+
return true;
|
|
558
|
+
}
|
|
559
|
+
if (!this.canStoreChunk(chunk) || data.size !== chunk.size) {
|
|
560
|
+
return false;
|
|
561
|
+
}
|
|
562
|
+
try {
|
|
563
|
+
await this.store.putChunk(workspace, chunk.storageKey, data, this.maxBytes);
|
|
564
|
+
return true;
|
|
565
|
+
} catch {
|
|
566
|
+
return false;
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
async storeFile(workspace, fileKey, file) {
|
|
570
|
+
if (this.ttlMs <= 0) {
|
|
571
|
+
return;
|
|
572
|
+
}
|
|
573
|
+
const cachedAt = Date.now();
|
|
574
|
+
try {
|
|
575
|
+
await this.store.putFile(workspace, fileKey, {
|
|
576
|
+
path: file.path,
|
|
577
|
+
size: file.size,
|
|
578
|
+
...file.contentType ? { contentType: file.contentType } : {},
|
|
579
|
+
chunks: file.chunks.map(({ index, offset, size, storageKey }) => ({
|
|
580
|
+
index,
|
|
581
|
+
offset,
|
|
582
|
+
size,
|
|
583
|
+
storageKey
|
|
584
|
+
})),
|
|
585
|
+
cachedAt,
|
|
586
|
+
expiresAt: cachedAt + this.ttlMs
|
|
587
|
+
});
|
|
588
|
+
} catch {
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
async invalidateFiles(workspace, fileKeys) {
|
|
592
|
+
await Promise.all(
|
|
593
|
+
fileKeys.map((fileKey) => this.store.removeFile(workspace, fileKey).catch(() => void 0))
|
|
594
|
+
);
|
|
595
|
+
}
|
|
596
|
+
clear(workspace) {
|
|
597
|
+
return this.store.clear(workspace);
|
|
598
|
+
}
|
|
599
|
+
usage(workspace) {
|
|
600
|
+
return this.store.usage(workspace);
|
|
601
|
+
}
|
|
602
|
+
};
|
|
603
|
+
|
|
1
604
|
// src/download.ts
|
|
2
605
|
function copyCheckpoint(checkpoint) {
|
|
3
606
|
return {
|
|
@@ -25,7 +628,7 @@ var InMemoryDownloadCheckpointStore = class {
|
|
|
25
628
|
var DOWNLOAD_DATABASE_NAME = "mountra-sdk";
|
|
26
629
|
var DOWNLOAD_DATABASE_VERSION = 1;
|
|
27
630
|
var DOWNLOAD_STORE_NAME = "download-checkpoints";
|
|
28
|
-
function
|
|
631
|
+
function getIndexedDb2() {
|
|
29
632
|
return globalThis.indexedDB ?? null;
|
|
30
633
|
}
|
|
31
634
|
var IndexedDBDownloadCheckpointStore = class {
|
|
@@ -90,7 +693,7 @@ var IndexedDBDownloadCheckpointStore = class {
|
|
|
90
693
|
}
|
|
91
694
|
async openDatabase() {
|
|
92
695
|
if (!this.databasePromise) {
|
|
93
|
-
const indexedDb =
|
|
696
|
+
const indexedDb = getIndexedDb2();
|
|
94
697
|
if (!indexedDb) {
|
|
95
698
|
this.databasePromise = Promise.resolve(null);
|
|
96
699
|
} else {
|
|
@@ -285,8 +888,9 @@ var SpeedWindow = class {
|
|
|
285
888
|
};
|
|
286
889
|
var BlobDownloadSink = class {
|
|
287
890
|
parts = [];
|
|
288
|
-
|
|
289
|
-
|
|
891
|
+
/** Parts arrive in file order, so the position is implied. */
|
|
892
|
+
async write(bytes, _position) {
|
|
893
|
+
this.parts.push(bytes instanceof Blob ? bytes : bytes.slice());
|
|
290
894
|
}
|
|
291
895
|
async flush() {
|
|
292
896
|
}
|
|
@@ -294,6 +898,9 @@ var BlobDownloadSink = class {
|
|
|
294
898
|
}
|
|
295
899
|
blob(contentType) {
|
|
296
900
|
const buffers = this.parts.map((part) => {
|
|
901
|
+
if (part instanceof Blob) {
|
|
902
|
+
return part;
|
|
903
|
+
}
|
|
297
904
|
const buffer = new ArrayBuffer(part.byteLength);
|
|
298
905
|
new Uint8Array(buffer).set(part);
|
|
299
906
|
return buffer;
|
|
@@ -323,7 +930,7 @@ var FileSystemDownloadSink = class {
|
|
|
323
930
|
throw new Error("File System Access writer position does not match the download checkpoint");
|
|
324
931
|
}
|
|
325
932
|
await this.writable.write(bytes);
|
|
326
|
-
this.position += bytes.byteLength;
|
|
933
|
+
this.position += bytes instanceof Blob ? bytes.size : bytes.byteLength;
|
|
327
934
|
}
|
|
328
935
|
async flush() {
|
|
329
936
|
if (this.writable) {
|
|
@@ -409,39 +1016,39 @@ function buildDownloadChunks(exported, useClientAssembly) {
|
|
|
409
1016
|
}
|
|
410
1017
|
];
|
|
411
1018
|
}
|
|
412
|
-
function buildDownloadKey(
|
|
1019
|
+
function buildDownloadKey(plan, baseUrl, request) {
|
|
413
1020
|
return encodeURIComponent(
|
|
414
1021
|
JSON.stringify({
|
|
415
1022
|
request: requestKey(request, baseUrl),
|
|
416
|
-
path:
|
|
417
|
-
size:
|
|
418
|
-
chunks: chunks.map((chunk) => [chunk.index, chunk.offset, chunk.size, chunk.storageKey])
|
|
1023
|
+
path: plan.path,
|
|
1024
|
+
size: plan.size,
|
|
1025
|
+
chunks: plan.chunks.map((chunk) => [chunk.index, chunk.offset, chunk.size, chunk.storageKey])
|
|
419
1026
|
})
|
|
420
1027
|
);
|
|
421
1028
|
}
|
|
422
|
-
function checkpointMatches(checkpoint,
|
|
423
|
-
if (checkpoint.version !== 1 || checkpoint.path !==
|
|
1029
|
+
function checkpointMatches(checkpoint, plan) {
|
|
1030
|
+
if (checkpoint.version !== 1 || checkpoint.path !== plan.path || checkpoint.size !== plan.size) {
|
|
424
1031
|
return false;
|
|
425
1032
|
}
|
|
426
|
-
if (checkpoint.chunks.length !== chunks.length) {
|
|
1033
|
+
if (checkpoint.chunks.length !== plan.chunks.length) {
|
|
427
1034
|
return false;
|
|
428
1035
|
}
|
|
429
1036
|
return checkpoint.chunks.every((saved, index) => {
|
|
430
|
-
const current = chunks[index];
|
|
1037
|
+
const current = plan.chunks[index];
|
|
431
1038
|
return Boolean(
|
|
432
1039
|
current && saved.index === current.index && saved.offset === current.offset && saved.size === current.size && saved.storageKey === current.storageKey && saved.downloadedBytes >= 0 && saved.downloadedBytes <= saved.size
|
|
433
1040
|
);
|
|
434
1041
|
});
|
|
435
1042
|
}
|
|
436
|
-
function createCheckpoint(key, baseUrl, request,
|
|
1043
|
+
function createCheckpoint(key, baseUrl, request, plan, fileHandle) {
|
|
437
1044
|
return {
|
|
438
1045
|
version: 1,
|
|
439
1046
|
key,
|
|
440
1047
|
requestKey: requestKey(request, baseUrl),
|
|
441
1048
|
request: { ...request },
|
|
442
|
-
path:
|
|
443
|
-
size:
|
|
444
|
-
chunks: chunks.map((chunk) => ({
|
|
1049
|
+
path: plan.path,
|
|
1050
|
+
size: plan.size,
|
|
1051
|
+
chunks: plan.chunks.map((chunk) => ({
|
|
445
1052
|
index: chunk.index,
|
|
446
1053
|
offset: chunk.offset,
|
|
447
1054
|
size: chunk.size,
|
|
@@ -459,8 +1066,8 @@ function totalDownloadedBytes(checkpoint) {
|
|
|
459
1066
|
function completedChunkCount(checkpoint) {
|
|
460
1067
|
return checkpoint.chunks.filter((chunk) => chunk.completed).length;
|
|
461
1068
|
}
|
|
462
|
-
function defaultFilename(
|
|
463
|
-
const candidate = requested?.trim() ||
|
|
1069
|
+
function defaultFilename(path, requested) {
|
|
1070
|
+
const candidate = requested?.trim() || path.split("/").filter(Boolean).pop() || "download";
|
|
464
1071
|
return candidate.replace(/[\\/\r\n]/g, "_") || "download";
|
|
465
1072
|
}
|
|
466
1073
|
function triggerBrowserDownload(blob, filename) {
|
|
@@ -485,33 +1092,38 @@ var MountraDownloader = class {
|
|
|
485
1092
|
this.transport = transport;
|
|
486
1093
|
}
|
|
487
1094
|
async exportFile(options) {
|
|
488
|
-
|
|
489
|
-
return ensureExportFileResponse(
|
|
490
|
-
await this.transport.requestJson(
|
|
491
|
-
"/file/export",
|
|
492
|
-
requestBodyFromCheckpoint(request),
|
|
493
|
-
options.signal
|
|
494
|
-
)
|
|
495
|
-
);
|
|
1095
|
+
return this.exportRequest(this.resolveRequest(options), options.signal);
|
|
496
1096
|
}
|
|
497
1097
|
async downloadFile(options) {
|
|
498
1098
|
const preferFileSystemAccess = options.preferFileSystemAccess !== false;
|
|
499
1099
|
const fileHandle = options.fileHandle ?? (preferFileSystemAccess && getShowSaveFilePicker() ? await this.pickSaveFile(options.downloadFilename ?? options.path?.split("/").filter(Boolean).pop()) : void 0);
|
|
500
|
-
const
|
|
501
|
-
|
|
1100
|
+
const request = this.resolveRequest(options);
|
|
1101
|
+
const plan = await this.loadCachedPlan(request, options.cache) ?? this.planExport(
|
|
1102
|
+
await this.exportRequest(request, options.signal),
|
|
1103
|
+
request,
|
|
1104
|
+
options.cache,
|
|
1105
|
+
fileHandle
|
|
1106
|
+
);
|
|
1107
|
+
return this.downloadResolved(plan, request, options, fileHandle, false);
|
|
502
1108
|
}
|
|
503
1109
|
async downloadExport(exported, options = { path: exported.path }) {
|
|
504
1110
|
let fileHandle = options.fileHandle;
|
|
505
1111
|
if (!fileHandle && options.preferFileSystemAccess !== false && getShowSaveFilePicker()) {
|
|
506
1112
|
fileHandle = await this.pickSaveFile(
|
|
507
|
-
options.downloadFilename ?? defaultFilename(exported),
|
|
1113
|
+
options.downloadFilename ?? defaultFilename(exported.path),
|
|
508
1114
|
exported.contentType
|
|
509
1115
|
);
|
|
510
1116
|
}
|
|
511
|
-
const request =
|
|
1117
|
+
const request = this.resolveRequest(
|
|
512
1118
|
options.path === void 0 && options.ino === void 0 ? { ...options, path: exported.path } : options
|
|
513
1119
|
);
|
|
514
|
-
return this.downloadResolved(
|
|
1120
|
+
return this.downloadResolved(
|
|
1121
|
+
this.planExport(exported, request, options.cache, fileHandle),
|
|
1122
|
+
request,
|
|
1123
|
+
options,
|
|
1124
|
+
fileHandle,
|
|
1125
|
+
false
|
|
1126
|
+
);
|
|
515
1127
|
}
|
|
516
1128
|
async resumeDownload(downloadKey, options = {}) {
|
|
517
1129
|
if (!downloadKey.trim()) {
|
|
@@ -527,7 +1139,7 @@ var MountraDownloader = class {
|
|
|
527
1139
|
signal: options.signal
|
|
528
1140
|
});
|
|
529
1141
|
return this.downloadResolved(
|
|
530
|
-
exported,
|
|
1142
|
+
this.planExport(exported, checkpoint.request, void 0, checkpoint.fileHandle),
|
|
531
1143
|
checkpoint.request,
|
|
532
1144
|
options,
|
|
533
1145
|
checkpoint.fileHandle,
|
|
@@ -540,6 +1152,124 @@ var MountraDownloader = class {
|
|
|
540
1152
|
clearDownloadCheckpoint(downloadKey) {
|
|
541
1153
|
return this.transport.checkpointStore.remove(downloadKey);
|
|
542
1154
|
}
|
|
1155
|
+
/** Drop the cached layout of a file that was just written, so the next download sees the new content. */
|
|
1156
|
+
async invalidateCachedFile(selector, target) {
|
|
1157
|
+
await this.cachedFileScope(selector, target)?.invalidate();
|
|
1158
|
+
}
|
|
1159
|
+
/**
|
|
1160
|
+
* Cache a file this client just uploaded, so downloading it needs no
|
|
1161
|
+
* network. Falls back to invalidation when any chunk cannot be cached.
|
|
1162
|
+
*/
|
|
1163
|
+
async cacheUploadedFile(selector, file, mode = "default") {
|
|
1164
|
+
const scope = this.cachedFileScope(selector, file);
|
|
1165
|
+
if (!scope) {
|
|
1166
|
+
return;
|
|
1167
|
+
}
|
|
1168
|
+
await scope.invalidate();
|
|
1169
|
+
if (mode === "no-store") {
|
|
1170
|
+
return;
|
|
1171
|
+
}
|
|
1172
|
+
let complete = true;
|
|
1173
|
+
for (const chunk of file.chunks) {
|
|
1174
|
+
complete = await scope.cache.storeChunk(scope.workspace, chunk, chunk.data) && complete;
|
|
1175
|
+
}
|
|
1176
|
+
if (complete) {
|
|
1177
|
+
for (const fileKey of scope.fileKeys) {
|
|
1178
|
+
await scope.cache.storeFile(scope.workspace, fileKey, file);
|
|
1179
|
+
}
|
|
1180
|
+
}
|
|
1181
|
+
}
|
|
1182
|
+
cachedFileScope(selector, target) {
|
|
1183
|
+
const cache = this.transport.cache;
|
|
1184
|
+
const workspace = workspaceCacheKey(this.transport.baseUrl, selector);
|
|
1185
|
+
if (!cache || workspace === null) {
|
|
1186
|
+
return null;
|
|
1187
|
+
}
|
|
1188
|
+
const fileKeys = [
|
|
1189
|
+
target.path !== void 0 ? fileCacheKey({ path: target.path }) : null,
|
|
1190
|
+
target.ino !== void 0 ? fileCacheKey({ ino: target.ino }) : null
|
|
1191
|
+
].filter((fileKey) => fileKey !== null);
|
|
1192
|
+
return {
|
|
1193
|
+
cache,
|
|
1194
|
+
workspace,
|
|
1195
|
+
fileKeys,
|
|
1196
|
+
invalidate: () => cache.invalidateFiles(workspace, fileKeys)
|
|
1197
|
+
};
|
|
1198
|
+
}
|
|
1199
|
+
async clearCache(selector) {
|
|
1200
|
+
const cache = this.transport.cache;
|
|
1201
|
+
if (!cache) {
|
|
1202
|
+
return;
|
|
1203
|
+
}
|
|
1204
|
+
await cache.clear(selector === void 0 ? void 0 : this.requireCacheWorkspace(selector));
|
|
1205
|
+
}
|
|
1206
|
+
async cacheUsage(selector) {
|
|
1207
|
+
const cache = this.transport.cache;
|
|
1208
|
+
if (!cache) {
|
|
1209
|
+
return { bytes: 0, chunks: 0 };
|
|
1210
|
+
}
|
|
1211
|
+
return cache.usage(selector === void 0 ? void 0 : this.requireCacheWorkspace(selector));
|
|
1212
|
+
}
|
|
1213
|
+
requireCacheWorkspace(selector) {
|
|
1214
|
+
const workspace = workspaceCacheKey(this.transport.baseUrl, selector);
|
|
1215
|
+
if (workspace === null) {
|
|
1216
|
+
throw new TypeError("workspaceCuid or workspaceId is required");
|
|
1217
|
+
}
|
|
1218
|
+
return workspace;
|
|
1219
|
+
}
|
|
1220
|
+
/** Fall back to the client workspace only when the request names none, as uploads do. */
|
|
1221
|
+
resolveRequest(options) {
|
|
1222
|
+
const hasWorkspace = options.workspaceCuid !== void 0 || options.workspaceId !== void 0;
|
|
1223
|
+
return downloadRequestFromOptions(
|
|
1224
|
+
hasWorkspace ? options : {
|
|
1225
|
+
...options,
|
|
1226
|
+
workspaceCuid: this.transport.clientSelector.workspaceCuid,
|
|
1227
|
+
workspaceId: this.transport.clientSelector.workspaceId
|
|
1228
|
+
}
|
|
1229
|
+
);
|
|
1230
|
+
}
|
|
1231
|
+
async exportRequest(request, signal) {
|
|
1232
|
+
return ensureExportFileResponse(
|
|
1233
|
+
await this.transport.requestJson("/file/export", requestBodyFromCheckpoint(request), signal)
|
|
1234
|
+
);
|
|
1235
|
+
}
|
|
1236
|
+
cacheScope(request, mode = "default") {
|
|
1237
|
+
const cache = this.transport.cache;
|
|
1238
|
+
if (!cache || mode === "no-store") {
|
|
1239
|
+
return null;
|
|
1240
|
+
}
|
|
1241
|
+
const workspace = workspaceCacheKey(this.transport.baseUrl, request);
|
|
1242
|
+
const fileKey = fileCacheKey(request);
|
|
1243
|
+
return workspace !== null && fileKey !== null ? { cache, workspace, fileKey } : null;
|
|
1244
|
+
}
|
|
1245
|
+
/** A plan that needs no network at all, when the cached layout is fresh and every chunk is cached. */
|
|
1246
|
+
async loadCachedPlan(request, mode) {
|
|
1247
|
+
const scope = this.cacheScope(request, mode);
|
|
1248
|
+
if (!scope || mode === "no-cache") {
|
|
1249
|
+
return null;
|
|
1250
|
+
}
|
|
1251
|
+
const cached = await scope.cache.loadFile(scope.workspace, scope.fileKey);
|
|
1252
|
+
if (!cached) {
|
|
1253
|
+
return null;
|
|
1254
|
+
}
|
|
1255
|
+
return {
|
|
1256
|
+
path: cached.file.path,
|
|
1257
|
+
size: cached.file.size,
|
|
1258
|
+
...cached.file.contentType ? { contentType: cached.file.contentType } : {},
|
|
1259
|
+
chunks: cached.file.chunks,
|
|
1260
|
+
cachedChunks: cached.chunks
|
|
1261
|
+
};
|
|
1262
|
+
}
|
|
1263
|
+
planExport(exported, request, mode, fileHandle) {
|
|
1264
|
+
const useClientAssembly = (exported.chunks?.length ?? 0) > 1 && (Boolean(fileHandle) || this.cacheScope(request, mode) !== null);
|
|
1265
|
+
return {
|
|
1266
|
+
path: exported.path,
|
|
1267
|
+
size: exported.size,
|
|
1268
|
+
...exported.contentType ? { contentType: exported.contentType } : {},
|
|
1269
|
+
chunks: buildDownloadChunks(exported, useClientAssembly),
|
|
1270
|
+
exported
|
|
1271
|
+
};
|
|
1272
|
+
}
|
|
543
1273
|
async pickSaveFile(requestedFilename, contentType) {
|
|
544
1274
|
const picker = getShowSaveFilePicker();
|
|
545
1275
|
if (!picker) {
|
|
@@ -565,13 +1295,15 @@ var MountraDownloader = class {
|
|
|
565
1295
|
throw new DOMException("Write permission was not granted", "NotAllowedError");
|
|
566
1296
|
}
|
|
567
1297
|
}
|
|
568
|
-
async downloadResolved(
|
|
569
|
-
const
|
|
570
|
-
const
|
|
571
|
-
const key = buildDownloadKey(exported, chunks, this.transport.baseUrl, request);
|
|
1298
|
+
async downloadResolved(plan, request, options, fileHandle, forceResume) {
|
|
1299
|
+
const chunks = plan.chunks;
|
|
1300
|
+
const key = buildDownloadKey(plan, this.transport.baseUrl, request);
|
|
572
1301
|
const existing = await this.transport.checkpointStore.load(key);
|
|
573
1302
|
const resumeRequested = !("resume" in options) || options.resume !== false;
|
|
574
|
-
let checkpoint = existing && checkpointMatches(existing,
|
|
1303
|
+
let checkpoint = existing && checkpointMatches(existing, plan) && (forceResume || resumeRequested) && (forceResume || fileHandle && await this.isSameFileHandle(existing.fileHandle, fileHandle)) ? existing : createCheckpoint(key, this.transport.baseUrl, request, plan, fileHandle);
|
|
1304
|
+
const cacheScope = this.cacheScope(request, "cache" in options ? options.cache : void 0);
|
|
1305
|
+
const cachedChunkIndexes = /* @__PURE__ */ new Set();
|
|
1306
|
+
let cachedBytes = 0;
|
|
575
1307
|
if (fileHandle) {
|
|
576
1308
|
checkpoint.fileHandle = fileHandle;
|
|
577
1309
|
}
|
|
@@ -586,11 +1318,11 @@ var MountraDownloader = class {
|
|
|
586
1318
|
const emit = (phase, currentChunkIndex) => {
|
|
587
1319
|
const downloadedBytes = totalDownloadedBytes(checkpoint);
|
|
588
1320
|
const speedBytesPerSecond = speedWindow.record(downloadedBytes, Date.now());
|
|
589
|
-
const remainingBytes = Math.max(
|
|
1321
|
+
const remainingBytes = Math.max(plan.size - downloadedBytes, 0);
|
|
590
1322
|
options.onProgress?.({
|
|
591
1323
|
phase,
|
|
592
1324
|
downloadedBytes,
|
|
593
|
-
totalBytes:
|
|
1325
|
+
totalBytes: plan.size,
|
|
594
1326
|
downloadedChunks: completedChunkCount(checkpoint),
|
|
595
1327
|
totalChunks: checkpoint.chunks.length,
|
|
596
1328
|
...currentChunkIndex !== void 0 ? { currentChunkIndex } : {},
|
|
@@ -620,12 +1352,30 @@ var MountraDownloader = class {
|
|
|
620
1352
|
if (savedChunk.completed) {
|
|
621
1353
|
continue;
|
|
622
1354
|
}
|
|
1355
|
+
const cached = plan.cachedChunks?.get(chunk.index) ?? (cacheScope ? await cacheScope.cache.readChunk(cacheScope.workspace, chunk) : null);
|
|
1356
|
+
if (cached) {
|
|
1357
|
+
throwIfAborted(options.signal);
|
|
1358
|
+
const remaining = cached.slice(savedChunk.downloadedBytes);
|
|
1359
|
+
await sink.write(remaining, chunk.offset + savedChunk.downloadedBytes);
|
|
1360
|
+
cachedBytes += remaining.size;
|
|
1361
|
+
savedChunk.downloadedBytes = chunk.size;
|
|
1362
|
+
savedChunk.completed = true;
|
|
1363
|
+
cachedChunkIndexes.add(chunk.index);
|
|
1364
|
+
emit("downloading", chunk.index);
|
|
1365
|
+
await persist(false);
|
|
1366
|
+
continue;
|
|
1367
|
+
}
|
|
1368
|
+
const url = chunk.url;
|
|
1369
|
+
if (!url) {
|
|
1370
|
+
throw new Error(`Chunk ${chunk.index} is no longer in the download cache`);
|
|
1371
|
+
}
|
|
1372
|
+
const capture = cacheScope && savedChunk.downloadedBytes === 0 && cacheScope.cache.canStoreChunk(chunk) ? [] : null;
|
|
623
1373
|
let attempt = 0;
|
|
624
1374
|
while (true) {
|
|
625
1375
|
throwIfAborted(options.signal);
|
|
626
1376
|
const rangeStart = savedChunk.downloadedBytes;
|
|
627
1377
|
try {
|
|
628
|
-
const response = await this.transport.fetchImpl(
|
|
1378
|
+
const response = await this.transport.fetchImpl(url, {
|
|
629
1379
|
method: "GET",
|
|
630
1380
|
...rangeStart > 0 ? { headers: { Range: `bytes=${rangeStart}-` } } : {},
|
|
631
1381
|
...options.signal ? { signal: options.signal } : {}
|
|
@@ -641,9 +1391,10 @@ var MountraDownloader = class {
|
|
|
641
1391
|
chunk,
|
|
642
1392
|
savedChunk,
|
|
643
1393
|
sink,
|
|
644
|
-
|
|
1394
|
+
plan.size,
|
|
645
1395
|
emit,
|
|
646
1396
|
persist,
|
|
1397
|
+
capture,
|
|
647
1398
|
options.signal
|
|
648
1399
|
);
|
|
649
1400
|
if (savedChunk.downloadedBytes !== savedChunk.size) {
|
|
@@ -653,6 +1404,9 @@ var MountraDownloader = class {
|
|
|
653
1404
|
}
|
|
654
1405
|
savedChunk.completed = true;
|
|
655
1406
|
await persist(true);
|
|
1407
|
+
if (cacheScope && capture && await cacheScope.cache.storeChunk(cacheScope.workspace, chunk, new Blob(capture))) {
|
|
1408
|
+
cachedChunkIndexes.add(chunk.index);
|
|
1409
|
+
}
|
|
656
1410
|
emit("downloading", chunk.index);
|
|
657
1411
|
break;
|
|
658
1412
|
} catch (error) {
|
|
@@ -667,25 +1421,29 @@ var MountraDownloader = class {
|
|
|
667
1421
|
}
|
|
668
1422
|
}
|
|
669
1423
|
}
|
|
670
|
-
if (totalDownloadedBytes(checkpoint) !==
|
|
1424
|
+
if (totalDownloadedBytes(checkpoint) !== plan.size) {
|
|
671
1425
|
throw new Error("Download completed with an unexpected byte count");
|
|
672
1426
|
}
|
|
673
1427
|
if (fileHandle) {
|
|
674
|
-
await sink.finalize(
|
|
1428
|
+
await sink.finalize(plan.size);
|
|
675
1429
|
} else {
|
|
676
1430
|
await sink.finalize();
|
|
677
1431
|
}
|
|
678
1432
|
await this.transport.checkpointStore.remove(key);
|
|
1433
|
+
if (cacheScope && plan.exported && chunks.every((chunk) => chunk.size === 0 || cachedChunkIndexes.has(chunk.index))) {
|
|
1434
|
+
await cacheScope.cache.storeFile(cacheScope.workspace, cacheScope.fileKey, plan);
|
|
1435
|
+
}
|
|
679
1436
|
emit("completed");
|
|
680
|
-
const blob = sink instanceof BlobDownloadSink ? sink.blob(
|
|
1437
|
+
const blob = sink instanceof BlobDownloadSink ? sink.blob(plan.contentType) : void 0;
|
|
681
1438
|
if (blob) {
|
|
682
1439
|
const filename = "downloadFilename" in options ? options.downloadFilename : void 0;
|
|
683
|
-
triggerBrowserDownload(blob, defaultFilename(
|
|
1440
|
+
triggerBrowserDownload(blob, defaultFilename(plan.path, filename));
|
|
684
1441
|
}
|
|
685
1442
|
return {
|
|
686
|
-
export: exported,
|
|
1443
|
+
...plan.exported ? { export: plan.exported } : {},
|
|
687
1444
|
downloadKey: key,
|
|
688
|
-
downloadedBytes:
|
|
1445
|
+
downloadedBytes: plan.size,
|
|
1446
|
+
cachedBytes,
|
|
689
1447
|
resumed,
|
|
690
1448
|
...fileHandle ? { fileHandle } : {},
|
|
691
1449
|
...blob ? { blob } : {}
|
|
@@ -695,13 +1453,13 @@ var MountraDownloader = class {
|
|
|
695
1453
|
throw error;
|
|
696
1454
|
}
|
|
697
1455
|
}
|
|
698
|
-
async consumeResponse(response, chunk, savedChunk, sink, totalSize, emit, persist, signal) {
|
|
1456
|
+
async consumeResponse(response, chunk, savedChunk, sink, totalSize, emit, persist, capture, signal) {
|
|
699
1457
|
const sourceOffset = savedChunk.downloadedBytes;
|
|
700
1458
|
let bytesToSkip = response.status === 200 && sourceOffset > 0 ? sourceOffset : 0;
|
|
701
1459
|
const reader = response.body?.getReader();
|
|
702
1460
|
if (!reader) {
|
|
703
1461
|
const body = new Uint8Array(await response.arrayBuffer());
|
|
704
|
-
await this.consumeBytes(body, bytesToSkip, chunk, savedChunk, sink, totalSize, emit, persist, signal);
|
|
1462
|
+
await this.consumeBytes(body, bytesToSkip, chunk, savedChunk, sink, totalSize, emit, persist, capture, signal);
|
|
705
1463
|
return;
|
|
706
1464
|
}
|
|
707
1465
|
try {
|
|
@@ -727,10 +1485,11 @@ var MountraDownloader = class {
|
|
|
727
1485
|
totalSize,
|
|
728
1486
|
emit,
|
|
729
1487
|
persist,
|
|
1488
|
+
capture,
|
|
730
1489
|
signal
|
|
731
1490
|
);
|
|
732
1491
|
} else {
|
|
733
|
-
await this.consumeBytes(bytes, 0, chunk, savedChunk, sink, totalSize, emit, persist, signal);
|
|
1492
|
+
await this.consumeBytes(bytes, 0, chunk, savedChunk, sink, totalSize, emit, persist, capture, signal);
|
|
734
1493
|
}
|
|
735
1494
|
}
|
|
736
1495
|
} finally {
|
|
@@ -740,7 +1499,7 @@ var MountraDownloader = class {
|
|
|
740
1499
|
throw new Error(`Download response did not contain the saved offset for chunk ${chunk.index}`);
|
|
741
1500
|
}
|
|
742
1501
|
}
|
|
743
|
-
async consumeBytes(bytes, offset, chunk, savedChunk, sink, totalSize, emit, persist, signal) {
|
|
1502
|
+
async consumeBytes(bytes, offset, chunk, savedChunk, sink, totalSize, emit, persist, capture, signal) {
|
|
744
1503
|
const remaining = chunk.size - savedChunk.downloadedBytes;
|
|
745
1504
|
const writableBytes = bytes.byteLength - offset;
|
|
746
1505
|
if (writableBytes > remaining) {
|
|
@@ -752,11 +1511,8 @@ var MountraDownloader = class {
|
|
|
752
1511
|
throwIfAborted(signal);
|
|
753
1512
|
const payload = bytes.subarray(offset);
|
|
754
1513
|
const position = chunk.offset + savedChunk.downloadedBytes;
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
} else {
|
|
758
|
-
await sink.write(payload);
|
|
759
|
-
}
|
|
1514
|
+
await sink.write(payload, position);
|
|
1515
|
+
capture?.push(payload.slice());
|
|
760
1516
|
savedChunk.downloadedBytes += payload.byteLength;
|
|
761
1517
|
emit("downloading", chunk.index);
|
|
762
1518
|
await persist(false);
|
|
@@ -782,132 +1538,6 @@ var MountraDownloader = class {
|
|
|
782
1538
|
}
|
|
783
1539
|
};
|
|
784
1540
|
|
|
785
|
-
// src/file.ts
|
|
786
|
-
function normalizeMountraFileLocator(locator, fallbackWorkspace = {}) {
|
|
787
|
-
if (!locator || typeof locator !== "object") {
|
|
788
|
-
throw new TypeError("Mountra file locator is required");
|
|
789
|
-
}
|
|
790
|
-
const hasPath = locator.path !== void 0;
|
|
791
|
-
const hasIno = locator.ino !== void 0;
|
|
792
|
-
if (hasPath === hasIno) {
|
|
793
|
-
throw new TypeError("Mountra file locator requires exactly one of path or ino");
|
|
794
|
-
}
|
|
795
|
-
const workspaceId = locator.workspaceId ?? locator.wsid ?? fallbackWorkspace.workspaceId;
|
|
796
|
-
if (locator.workspaceId !== void 0 && locator.wsid !== void 0 && String(locator.workspaceId) !== String(locator.wsid)) {
|
|
797
|
-
throw new TypeError("workspaceId and wsid must identify the same workspace");
|
|
798
|
-
}
|
|
799
|
-
const workspaceCuid = locator.workspaceCuid ?? fallbackWorkspace.workspaceCuid;
|
|
800
|
-
if (workspaceCuid === void 0 && workspaceId === void 0) {
|
|
801
|
-
throw new TypeError("workspaceCuid, workspaceId, or wsid is required");
|
|
802
|
-
}
|
|
803
|
-
if (hasPath) {
|
|
804
|
-
const path = normalizePath(locator.path);
|
|
805
|
-
return {
|
|
806
|
-
...workspaceCuid !== void 0 ? { workspaceCuid } : {},
|
|
807
|
-
...workspaceId !== void 0 ? { workspaceId } : {},
|
|
808
|
-
path
|
|
809
|
-
};
|
|
810
|
-
}
|
|
811
|
-
return {
|
|
812
|
-
...workspaceCuid !== void 0 ? { workspaceCuid } : {},
|
|
813
|
-
...workspaceId !== void 0 ? { workspaceId } : {},
|
|
814
|
-
ino: normalizeIno(locator.ino)
|
|
815
|
-
};
|
|
816
|
-
}
|
|
817
|
-
var MountraFile = class {
|
|
818
|
-
constructor(client, locator) {
|
|
819
|
-
this.client = client;
|
|
820
|
-
this.locator = Object.freeze({ ...locator });
|
|
821
|
-
}
|
|
822
|
-
client;
|
|
823
|
-
locator;
|
|
824
|
-
get workspaceCuid() {
|
|
825
|
-
return this.locator.workspaceCuid;
|
|
826
|
-
}
|
|
827
|
-
get workspaceId() {
|
|
828
|
-
return this.locator.workspaceId;
|
|
829
|
-
}
|
|
830
|
-
/** `wsid` is the short alias used by the file-object API. */
|
|
831
|
-
get wsid() {
|
|
832
|
-
return this.locator.workspaceId;
|
|
833
|
-
}
|
|
834
|
-
get path() {
|
|
835
|
-
return "path" in this.locator ? this.locator.path : void 0;
|
|
836
|
-
}
|
|
837
|
-
get ino() {
|
|
838
|
-
return "ino" in this.locator ? this.locator.ino : void 0;
|
|
839
|
-
}
|
|
840
|
-
export(options = {}) {
|
|
841
|
-
return this.client.exportFile({ ...options, ...this.locator });
|
|
842
|
-
}
|
|
843
|
-
download(options = {}) {
|
|
844
|
-
return this.client.downloadFile({ ...options, ...this.locator });
|
|
845
|
-
}
|
|
846
|
-
async resume(options = {}) {
|
|
847
|
-
const { downloadKey, ...resumeOptions } = options;
|
|
848
|
-
const checkpoints = await this.client.listResumableDownloads();
|
|
849
|
-
const candidates = checkpoints.filter(
|
|
850
|
-
(checkpoint2) => checkpoint2.fileHandle && matchesLocator(checkpoint2.request, this.locator)
|
|
851
|
-
);
|
|
852
|
-
const checkpoint = downloadKey ? candidates.find((candidate) => candidate.key === downloadKey) : [...candidates].sort((left, right) => right.updatedAt - left.updatedAt)[0];
|
|
853
|
-
if (!checkpoint) {
|
|
854
|
-
if (downloadKey) {
|
|
855
|
-
throw new Error("Download checkpoint does not belong to this Mountra file");
|
|
856
|
-
}
|
|
857
|
-
throw new Error("No resumable download was found for this Mountra file");
|
|
858
|
-
}
|
|
859
|
-
return this.client.resumeDownload(checkpoint.key, resumeOptions);
|
|
860
|
-
}
|
|
861
|
-
};
|
|
862
|
-
function matchesLocator(request, locator) {
|
|
863
|
-
if (locator.workspaceCuid !== void 0 && request.workspaceCuid !== locator.workspaceCuid) {
|
|
864
|
-
return false;
|
|
865
|
-
}
|
|
866
|
-
if (locator.workspaceId !== void 0 && String(request.workspaceId) !== String(locator.workspaceId)) {
|
|
867
|
-
return false;
|
|
868
|
-
}
|
|
869
|
-
if ("path" in locator) {
|
|
870
|
-
if (request.path === void 0 || request.ino !== void 0) {
|
|
871
|
-
return false;
|
|
872
|
-
}
|
|
873
|
-
try {
|
|
874
|
-
return normalizePath(request.path) === locator.path;
|
|
875
|
-
} catch {
|
|
876
|
-
return false;
|
|
877
|
-
}
|
|
878
|
-
}
|
|
879
|
-
return String(request.ino) === String(locator.ino) && request.path === void 0;
|
|
880
|
-
}
|
|
881
|
-
function normalizePath(value) {
|
|
882
|
-
if (!value || !value.trim()) {
|
|
883
|
-
throw new TypeError("Mountra file path cannot be empty");
|
|
884
|
-
}
|
|
885
|
-
const raw = value.trim();
|
|
886
|
-
const absolute = raw.startsWith("/") ? raw : `/${raw}`;
|
|
887
|
-
const segments = absolute.split("/").filter((segment) => segment.length > 0 && segment !== ".");
|
|
888
|
-
const stack = [];
|
|
889
|
-
for (const segment of segments) {
|
|
890
|
-
if (segment === "..") {
|
|
891
|
-
stack.pop();
|
|
892
|
-
continue;
|
|
893
|
-
}
|
|
894
|
-
stack.push(segment);
|
|
895
|
-
}
|
|
896
|
-
return stack.length === 0 ? "/" : `/${stack.join("/")}`;
|
|
897
|
-
}
|
|
898
|
-
function normalizeIno(value) {
|
|
899
|
-
if (typeof value === "number") {
|
|
900
|
-
if (!Number.isSafeInteger(value) || value < 0) {
|
|
901
|
-
throw new TypeError("Mountra file ino must be a non-negative safe integer");
|
|
902
|
-
}
|
|
903
|
-
return value;
|
|
904
|
-
}
|
|
905
|
-
if (!/^\d+$/.test(value)) {
|
|
906
|
-
throw new TypeError("Mountra file ino must be a non-negative integer string");
|
|
907
|
-
}
|
|
908
|
-
return value;
|
|
909
|
-
}
|
|
910
|
-
|
|
911
1541
|
// src/upload.ts
|
|
912
1542
|
var JUMBO_CHUNK_THRESHOLD_BYTES = 128 * 1024 * 1024;
|
|
913
1543
|
var MIN_S3_MULTIPART_PART_SIZE_BYTES = 5 * 1024 * 1024;
|
|
@@ -1160,8 +1790,7 @@ function splitFileIntoChunks(file, chunkCount = 1) {
|
|
|
1160
1790
|
}
|
|
1161
1791
|
const chunkSize = Math.ceil(file.size / chunkCount);
|
|
1162
1792
|
const chunks = [];
|
|
1163
|
-
for (let index = 0;
|
|
1164
|
-
const offset = index * chunkSize;
|
|
1793
|
+
for (let index = 0, offset = 0; offset < file.size; index += 1, offset += chunkSize) {
|
|
1165
1794
|
const end = Math.min(offset + chunkSize, file.size);
|
|
1166
1795
|
chunks.push({
|
|
1167
1796
|
index,
|
|
@@ -1217,6 +1846,7 @@ var MountraClient = class {
|
|
|
1217
1846
|
clientSelector: this.clientSelector,
|
|
1218
1847
|
fetchImpl: this.fetchImpl,
|
|
1219
1848
|
checkpointStore: options.downloadCheckpointStore ?? new IndexedDBDownloadCheckpointStore(),
|
|
1849
|
+
cache: options.downloadCache ? new DownloadCache(options.downloadCache) : null,
|
|
1220
1850
|
requestJson: (path, body, signal) => this.requestJson(path, body, signal)
|
|
1221
1851
|
});
|
|
1222
1852
|
}
|
|
@@ -1238,6 +1868,13 @@ var MountraClient = class {
|
|
|
1238
1868
|
clearDownloadCheckpoint(downloadKey) {
|
|
1239
1869
|
return this.downloader.clearDownloadCheckpoint(downloadKey);
|
|
1240
1870
|
}
|
|
1871
|
+
/** Clear the download cache of one workspace, or of every workspace when none is given. */
|
|
1872
|
+
clearDownloadCache(selector) {
|
|
1873
|
+
return this.downloader.clearCache(selector);
|
|
1874
|
+
}
|
|
1875
|
+
getDownloadCacheUsage(selector) {
|
|
1876
|
+
return this.downloader.cacheUsage(selector);
|
|
1877
|
+
}
|
|
1241
1878
|
/** Create a stable resource object for one file in this or another workspace. */
|
|
1242
1879
|
file(locator) {
|
|
1243
1880
|
return new MountraFile(
|
|
@@ -1418,6 +2055,26 @@ var MountraClient = class {
|
|
|
1418
2055
|
if (finished.finish_status !== "completed") {
|
|
1419
2056
|
throw new Error(`Mountra upload did not finish successfully: ${finished.finish_status}`);
|
|
1420
2057
|
}
|
|
2058
|
+
await this.downloader.cacheUploadedFile(
|
|
2059
|
+
this.cacheWorkspace(options),
|
|
2060
|
+
{
|
|
2061
|
+
path: finished.path,
|
|
2062
|
+
...finished.ino !== void 0 ? { ino: finished.ino } : {},
|
|
2063
|
+
size: context.fileSize,
|
|
2064
|
+
...context.contentType ? { contentType: context.contentType } : {},
|
|
2065
|
+
chunks: orderedTargets.map((target) => {
|
|
2066
|
+
const chunk = chunks[target.chunk_index];
|
|
2067
|
+
return {
|
|
2068
|
+
index: chunk.index,
|
|
2069
|
+
offset: chunk.offset,
|
|
2070
|
+
size: chunk.size,
|
|
2071
|
+
storageKey: target.storage_key,
|
|
2072
|
+
data: chunk.blob
|
|
2073
|
+
};
|
|
2074
|
+
})
|
|
2075
|
+
},
|
|
2076
|
+
options.cache
|
|
2077
|
+
);
|
|
1421
2078
|
await Promise.all(
|
|
1422
2079
|
[...jumboCheckpointKeys].map(
|
|
1423
2080
|
(checkpointKey) => this.jumboChunkCheckpointStore.remove(checkpointKey)
|
|
@@ -1593,12 +2250,26 @@ var MountraClient = class {
|
|
|
1593
2250
|
if (!uploadId.trim()) {
|
|
1594
2251
|
throw new TypeError("uploadId is required");
|
|
1595
2252
|
}
|
|
1596
|
-
|
|
2253
|
+
const finished = ensureFinishResponse(
|
|
1597
2254
|
await this.requestJson("/upload/finish", {
|
|
1598
2255
|
...getWorkspaceBody(this.clientSelector, selector),
|
|
1599
2256
|
upload_id: uploadId
|
|
1600
2257
|
})
|
|
1601
2258
|
);
|
|
2259
|
+
if (finished.finish_status === "completed") {
|
|
2260
|
+
await this.downloader.invalidateCachedFile(this.cacheWorkspace(selector), {
|
|
2261
|
+
path: finished.path,
|
|
2262
|
+
...finished.ino !== void 0 ? { ino: finished.ino } : {}
|
|
2263
|
+
});
|
|
2264
|
+
}
|
|
2265
|
+
return finished;
|
|
2266
|
+
}
|
|
2267
|
+
/** Same workspace resolution as `getWorkspaceBody`, so the cache key matches what Mountra used. */
|
|
2268
|
+
cacheWorkspace(selector) {
|
|
2269
|
+
return {
|
|
2270
|
+
workspaceCuid: selector.workspaceCuid ?? this.clientSelector.workspaceCuid,
|
|
2271
|
+
workspaceId: selector.workspaceId ?? this.clientSelector.workspaceId
|
|
2272
|
+
};
|
|
1602
2273
|
}
|
|
1603
2274
|
endpoint(path) {
|
|
1604
2275
|
return `${this.baseUrl}${this.apiPrefix}${path}`;
|
|
@@ -1638,8 +2309,10 @@ function createMountraClient(options) {
|
|
|
1638
2309
|
export {
|
|
1639
2310
|
DEFAULT_S3_MULTIPART_PART_SIZE_BYTES,
|
|
1640
2311
|
DownloadHttpError,
|
|
2312
|
+
InMemoryDownloadCacheStore,
|
|
1641
2313
|
InMemoryDownloadCheckpointStore,
|
|
1642
2314
|
InMemoryJumboChunkCheckpointStore,
|
|
2315
|
+
IndexedDBDownloadCacheStore,
|
|
1643
2316
|
IndexedDBDownloadCheckpointStore,
|
|
1644
2317
|
JUMBO_CHUNK_THRESHOLD_BYTES,
|
|
1645
2318
|
LocalStorageJumboChunkCheckpointStore,
|