@mountra/mountra-sdk 0.3.0 → 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.
- package/CHANGELOG.md +13 -0
- package/README.md +84 -1
- package/dist/index.cjs +1112 -91
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +234 -11
- package/dist/index.d.ts +234 -11
- package/dist/index.js +1110 -91
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -25,8 +25,10 @@ __export(src_exports, {
|
|
|
25
25
|
InMemoryDownloadCacheStore: () => InMemoryDownloadCacheStore,
|
|
26
26
|
InMemoryDownloadCheckpointStore: () => InMemoryDownloadCheckpointStore,
|
|
27
27
|
InMemoryJumboChunkCheckpointStore: () => InMemoryJumboChunkCheckpointStore,
|
|
28
|
+
InMemoryMetadataCacheStore: () => InMemoryMetadataCacheStore,
|
|
28
29
|
IndexedDBDownloadCacheStore: () => IndexedDBDownloadCacheStore,
|
|
29
30
|
IndexedDBDownloadCheckpointStore: () => IndexedDBDownloadCheckpointStore,
|
|
31
|
+
IndexedDBMetadataCacheStore: () => IndexedDBMetadataCacheStore,
|
|
30
32
|
JUMBO_CHUNK_THRESHOLD_BYTES: () => JUMBO_CHUNK_THRESHOLD_BYTES,
|
|
31
33
|
LocalStorageJumboChunkCheckpointStore: () => LocalStorageJumboChunkCheckpointStore,
|
|
32
34
|
MAX_S3_MULTIPART_PARTS: () => MAX_S3_MULTIPART_PARTS,
|
|
@@ -140,6 +142,9 @@ function matchesLocator(request, locator) {
|
|
|
140
142
|
}
|
|
141
143
|
return String(request.ino) === String(locator.ino) && request.path === void 0;
|
|
142
144
|
}
|
|
145
|
+
function isPathWithin(path, ancestor) {
|
|
146
|
+
return ancestor === "/" || path === ancestor || path.startsWith(`${ancestor}/`);
|
|
147
|
+
}
|
|
143
148
|
function normalizePath(value) {
|
|
144
149
|
if (!value || !value.trim()) {
|
|
145
150
|
throw new TypeError("Mountra file path cannot be empty");
|
|
@@ -170,6 +175,73 @@ function normalizeIno(value) {
|
|
|
170
175
|
return value;
|
|
171
176
|
}
|
|
172
177
|
|
|
178
|
+
// src/idb.ts
|
|
179
|
+
function getIndexedDb() {
|
|
180
|
+
return globalThis.indexedDB ?? null;
|
|
181
|
+
}
|
|
182
|
+
function requestResult(request) {
|
|
183
|
+
return new Promise((resolve, reject) => {
|
|
184
|
+
request.onsuccess = () => resolve(request.result);
|
|
185
|
+
request.onerror = () => reject(request.error ?? new Error("IndexedDB request failed"));
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
async function runTransaction(database, storeNames, mode, body) {
|
|
189
|
+
const transaction = database.transaction(storeNames, mode);
|
|
190
|
+
const done = new Promise((resolve, reject) => {
|
|
191
|
+
transaction.oncomplete = () => resolve();
|
|
192
|
+
transaction.onerror = () => reject(transaction.error ?? new Error("IndexedDB transaction failed"));
|
|
193
|
+
transaction.onabort = () => reject(transaction.error ?? new Error("IndexedDB transaction aborted"));
|
|
194
|
+
});
|
|
195
|
+
done.catch(() => void 0);
|
|
196
|
+
let result;
|
|
197
|
+
try {
|
|
198
|
+
result = await body(transaction);
|
|
199
|
+
} catch (error) {
|
|
200
|
+
try {
|
|
201
|
+
transaction.abort();
|
|
202
|
+
} catch {
|
|
203
|
+
}
|
|
204
|
+
throw error;
|
|
205
|
+
}
|
|
206
|
+
await done;
|
|
207
|
+
return result;
|
|
208
|
+
}
|
|
209
|
+
function workspaceRange(workspace) {
|
|
210
|
+
return IDBKeyRange.bound([workspace], [workspace, []]);
|
|
211
|
+
}
|
|
212
|
+
var CLOCK_KEY = "clock";
|
|
213
|
+
async function nextClock(meta) {
|
|
214
|
+
const clock = (await requestResult(meta.get(CLOCK_KEY)) ?? 0) + 1;
|
|
215
|
+
await requestResult(meta.put(clock, CLOCK_KEY));
|
|
216
|
+
return clock;
|
|
217
|
+
}
|
|
218
|
+
function openDatabase(name, version, upgrade, onClose) {
|
|
219
|
+
const indexedDb = getIndexedDb();
|
|
220
|
+
if (!indexedDb) {
|
|
221
|
+
return Promise.resolve(null);
|
|
222
|
+
}
|
|
223
|
+
return new Promise((resolve) => {
|
|
224
|
+
let request;
|
|
225
|
+
try {
|
|
226
|
+
request = indexedDb.open(name, version);
|
|
227
|
+
} catch {
|
|
228
|
+
resolve(null);
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
request.onupgradeneeded = () => upgrade(request.result);
|
|
232
|
+
request.onsuccess = () => {
|
|
233
|
+
const database = request.result;
|
|
234
|
+
database.onversionchange = () => {
|
|
235
|
+
database.close();
|
|
236
|
+
onClose();
|
|
237
|
+
};
|
|
238
|
+
resolve(database);
|
|
239
|
+
};
|
|
240
|
+
request.onerror = () => resolve(null);
|
|
241
|
+
request.onblocked = () => resolve(null);
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
|
|
173
245
|
// src/cache.ts
|
|
174
246
|
var DEFAULT_MAX_BYTES = 512 * 1024 * 1024;
|
|
175
247
|
var DEFAULT_MAX_CHUNK_BYTES = 128 * 1024 * 1024;
|
|
@@ -195,6 +267,13 @@ var InMemoryDownloadCacheStore = class {
|
|
|
195
267
|
async removeFile(workspace, fileKey) {
|
|
196
268
|
this.files.delete(memoryKey(workspace, fileKey));
|
|
197
269
|
}
|
|
270
|
+
async removeFilesUnder(workspace, path) {
|
|
271
|
+
for (const [key, entry] of this.files) {
|
|
272
|
+
if (entry.workspace === workspace && isPathWithin(entry.file.path, path)) {
|
|
273
|
+
this.files.delete(key);
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
}
|
|
198
277
|
async getChunk(workspace, storageKey) {
|
|
199
278
|
const key = memoryKey(workspace, storageKey);
|
|
200
279
|
const entry = this.chunks.get(key);
|
|
@@ -258,48 +337,9 @@ var CHUNK_DATA_STORE = "chunk-data";
|
|
|
258
337
|
var META_STORE = "meta";
|
|
259
338
|
var LRU_INDEX = "lastUsed";
|
|
260
339
|
var USAGE_KEY = "bytes";
|
|
261
|
-
var CLOCK_KEY = "clock";
|
|
262
|
-
function getIndexedDb() {
|
|
263
|
-
return globalThis.indexedDB ?? null;
|
|
264
|
-
}
|
|
265
|
-
function workspaceRange(workspace) {
|
|
266
|
-
return IDBKeyRange.bound([workspace], [workspace, []]);
|
|
267
|
-
}
|
|
268
340
|
function sameKey(left, right) {
|
|
269
341
|
return Array.isArray(left) && left[0] === right[0] && left[1] === right[1];
|
|
270
342
|
}
|
|
271
|
-
async function nextClock(meta) {
|
|
272
|
-
const clock = (await requestResult(meta.get(CLOCK_KEY)) ?? 0) + 1;
|
|
273
|
-
await requestResult(meta.put(clock, CLOCK_KEY));
|
|
274
|
-
return clock;
|
|
275
|
-
}
|
|
276
|
-
function requestResult(request) {
|
|
277
|
-
return new Promise((resolve, reject) => {
|
|
278
|
-
request.onsuccess = () => resolve(request.result);
|
|
279
|
-
request.onerror = () => reject(request.error ?? new Error("IndexedDB request failed"));
|
|
280
|
-
});
|
|
281
|
-
}
|
|
282
|
-
async function runTransaction(database, storeNames, mode, body) {
|
|
283
|
-
const transaction = database.transaction(storeNames, mode);
|
|
284
|
-
const done = new Promise((resolve, reject) => {
|
|
285
|
-
transaction.oncomplete = () => resolve();
|
|
286
|
-
transaction.onerror = () => reject(transaction.error ?? new Error("IndexedDB transaction failed"));
|
|
287
|
-
transaction.onabort = () => reject(transaction.error ?? new Error("IndexedDB transaction aborted"));
|
|
288
|
-
});
|
|
289
|
-
done.catch(() => void 0);
|
|
290
|
-
let result;
|
|
291
|
-
try {
|
|
292
|
-
result = await body(transaction);
|
|
293
|
-
} catch (error) {
|
|
294
|
-
try {
|
|
295
|
-
transaction.abort();
|
|
296
|
-
} catch {
|
|
297
|
-
}
|
|
298
|
-
throw error;
|
|
299
|
-
}
|
|
300
|
-
await done;
|
|
301
|
-
return result;
|
|
302
|
-
}
|
|
303
343
|
var IndexedDBDownloadCacheStore = class {
|
|
304
344
|
databaseName;
|
|
305
345
|
fallback = new InMemoryDownloadCacheStore();
|
|
@@ -337,6 +377,23 @@ var IndexedDBDownloadCacheStore = class {
|
|
|
337
377
|
await requestResult(transaction.objectStore(FILES_STORE).delete([workspace, fileKey]));
|
|
338
378
|
});
|
|
339
379
|
}
|
|
380
|
+
async removeFilesUnder(workspace, path) {
|
|
381
|
+
const database = await this.openDatabase();
|
|
382
|
+
if (!database) {
|
|
383
|
+
return this.fallback.removeFilesUnder(workspace, path);
|
|
384
|
+
}
|
|
385
|
+
await runTransaction(database, [FILES_STORE], "readwrite", async (transaction) => {
|
|
386
|
+
const cursorRequest = transaction.objectStore(FILES_STORE).openCursor(workspaceRange(workspace));
|
|
387
|
+
let cursor = await requestResult(cursorRequest);
|
|
388
|
+
while (cursor) {
|
|
389
|
+
if (isPathWithin(cursor.value.path, path)) {
|
|
390
|
+
await requestResult(cursor.delete());
|
|
391
|
+
}
|
|
392
|
+
cursor.continue();
|
|
393
|
+
cursor = await requestResult(cursorRequest);
|
|
394
|
+
}
|
|
395
|
+
});
|
|
396
|
+
}
|
|
340
397
|
async getChunk(workspace, storageKey) {
|
|
341
398
|
const database = await this.openDatabase();
|
|
342
399
|
if (!database) {
|
|
@@ -460,47 +517,27 @@ var IndexedDBDownloadCacheStore = class {
|
|
|
460
517
|
});
|
|
461
518
|
}
|
|
462
519
|
async openDatabase() {
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
database.createObjectStore(CHUNKS_STORE).createIndex(LRU_INDEX, "lastUsed");
|
|
483
|
-
}
|
|
484
|
-
if (!database.objectStoreNames.contains(CHUNK_DATA_STORE)) {
|
|
485
|
-
database.createObjectStore(CHUNK_DATA_STORE);
|
|
486
|
-
}
|
|
487
|
-
if (!database.objectStoreNames.contains(META_STORE)) {
|
|
488
|
-
database.createObjectStore(META_STORE);
|
|
489
|
-
}
|
|
490
|
-
};
|
|
491
|
-
request.onsuccess = () => {
|
|
492
|
-
const database = request.result;
|
|
493
|
-
database.onversionchange = () => {
|
|
494
|
-
database.close();
|
|
495
|
-
this.databasePromise = void 0;
|
|
496
|
-
};
|
|
497
|
-
resolve(database);
|
|
498
|
-
};
|
|
499
|
-
request.onerror = () => resolve(null);
|
|
500
|
-
request.onblocked = () => resolve(null);
|
|
501
|
-
});
|
|
520
|
+
this.databasePromise ??= openDatabase(
|
|
521
|
+
this.databaseName,
|
|
522
|
+
CACHE_DATABASE_VERSION,
|
|
523
|
+
(database) => {
|
|
524
|
+
if (!database.objectStoreNames.contains(FILES_STORE)) {
|
|
525
|
+
database.createObjectStore(FILES_STORE);
|
|
526
|
+
}
|
|
527
|
+
if (!database.objectStoreNames.contains(CHUNKS_STORE)) {
|
|
528
|
+
database.createObjectStore(CHUNKS_STORE).createIndex(LRU_INDEX, "lastUsed");
|
|
529
|
+
}
|
|
530
|
+
if (!database.objectStoreNames.contains(CHUNK_DATA_STORE)) {
|
|
531
|
+
database.createObjectStore(CHUNK_DATA_STORE);
|
|
532
|
+
}
|
|
533
|
+
if (!database.objectStoreNames.contains(META_STORE)) {
|
|
534
|
+
database.createObjectStore(META_STORE);
|
|
535
|
+
}
|
|
536
|
+
},
|
|
537
|
+
() => {
|
|
538
|
+
this.databasePromise = void 0;
|
|
502
539
|
}
|
|
503
|
-
|
|
540
|
+
);
|
|
504
541
|
return this.databasePromise;
|
|
505
542
|
}
|
|
506
543
|
};
|
|
@@ -639,6 +676,20 @@ var DownloadCache = class {
|
|
|
639
676
|
fileKeys.map((fileKey) => this.store.removeFile(workspace, fileKey).catch(() => void 0))
|
|
640
677
|
);
|
|
641
678
|
}
|
|
679
|
+
/** Drop the files at or below `paths`, which a write changed; their chunks stay cached. */
|
|
680
|
+
async invalidatePaths(workspace, paths) {
|
|
681
|
+
try {
|
|
682
|
+
if (!this.store.removeFilesUnder) {
|
|
683
|
+
await this.store.clear(workspace);
|
|
684
|
+
return;
|
|
685
|
+
}
|
|
686
|
+
for (const path of paths) {
|
|
687
|
+
await this.store.removeFilesUnder(workspace, path);
|
|
688
|
+
}
|
|
689
|
+
} catch {
|
|
690
|
+
await this.store.clear(workspace).catch(() => void 0);
|
|
691
|
+
}
|
|
692
|
+
}
|
|
642
693
|
clear(workspace) {
|
|
643
694
|
return this.store.clear(workspace);
|
|
644
695
|
}
|
|
@@ -1202,6 +1253,14 @@ var MountraDownloader = class {
|
|
|
1202
1253
|
async invalidateCachedFile(selector, target) {
|
|
1203
1254
|
await this.cachedFileScope(selector, target)?.invalidate();
|
|
1204
1255
|
}
|
|
1256
|
+
/** Drop the cached files at or below paths that a delete, move, or copy changed. */
|
|
1257
|
+
async invalidateCachedPaths(selector, paths) {
|
|
1258
|
+
const cache = this.transport.cache;
|
|
1259
|
+
const workspace = workspaceCacheKey(this.transport.baseUrl, selector);
|
|
1260
|
+
if (cache && workspace !== null) {
|
|
1261
|
+
await cache.invalidatePaths(workspace, paths);
|
|
1262
|
+
}
|
|
1263
|
+
}
|
|
1205
1264
|
/**
|
|
1206
1265
|
* Cache a file this client just uploaded, so downloading it needs no
|
|
1207
1266
|
* network. Falls back to invalidation when any chunk cannot be cached.
|
|
@@ -1584,11 +1643,7 @@ var MountraDownloader = class {
|
|
|
1584
1643
|
}
|
|
1585
1644
|
};
|
|
1586
1645
|
|
|
1587
|
-
// src/
|
|
1588
|
-
var JUMBO_CHUNK_THRESHOLD_BYTES = 128 * 1024 * 1024;
|
|
1589
|
-
var MIN_S3_MULTIPART_PART_SIZE_BYTES = 5 * 1024 * 1024;
|
|
1590
|
-
var DEFAULT_S3_MULTIPART_PART_SIZE_BYTES = 16 * 1024 * 1024;
|
|
1591
|
-
var MAX_S3_MULTIPART_PARTS = 1e4;
|
|
1646
|
+
// src/errors.ts
|
|
1592
1647
|
var MountraApiError = class extends Error {
|
|
1593
1648
|
status;
|
|
1594
1649
|
code;
|
|
@@ -1601,9 +1656,936 @@ var MountraApiError = class extends Error {
|
|
|
1601
1656
|
this.body = input.body;
|
|
1602
1657
|
}
|
|
1603
1658
|
};
|
|
1659
|
+
|
|
1660
|
+
// src/metadata.ts
|
|
1661
|
+
function isRecord2(value) {
|
|
1662
|
+
return typeof value === "object" && value !== null;
|
|
1663
|
+
}
|
|
1664
|
+
function invalidField(field) {
|
|
1665
|
+
return new TypeError(`Mountra metadata response field ${field} is invalid`);
|
|
1666
|
+
}
|
|
1667
|
+
function readString2(value, field) {
|
|
1668
|
+
if (typeof value !== "string") {
|
|
1669
|
+
throw invalidField(field);
|
|
1670
|
+
}
|
|
1671
|
+
return value;
|
|
1672
|
+
}
|
|
1673
|
+
function readCount(value, field) {
|
|
1674
|
+
if (typeof value !== "number" || !Number.isSafeInteger(value) || value < 0) {
|
|
1675
|
+
throw invalidField(field);
|
|
1676
|
+
}
|
|
1677
|
+
return value;
|
|
1678
|
+
}
|
|
1679
|
+
function readBoolean(value, field) {
|
|
1680
|
+
if (typeof value !== "boolean") {
|
|
1681
|
+
throw invalidField(field);
|
|
1682
|
+
}
|
|
1683
|
+
return value;
|
|
1684
|
+
}
|
|
1685
|
+
function optionalInteger(value) {
|
|
1686
|
+
return typeof value === "number" && Number.isSafeInteger(value) ? value : void 0;
|
|
1687
|
+
}
|
|
1688
|
+
function optionalText(value) {
|
|
1689
|
+
return typeof value === "string" && value ? value : void 0;
|
|
1690
|
+
}
|
|
1691
|
+
function withoutUndefined(value) {
|
|
1692
|
+
for (const key of Object.keys(value)) {
|
|
1693
|
+
if (value[key] === void 0) {
|
|
1694
|
+
delete value[key];
|
|
1695
|
+
}
|
|
1696
|
+
}
|
|
1697
|
+
return value;
|
|
1698
|
+
}
|
|
1699
|
+
function readNodeFields(raw, field) {
|
|
1700
|
+
return {
|
|
1701
|
+
name: readString2(raw.name, `${field}.name`),
|
|
1702
|
+
path: normalizePath(readString2(raw.path, `${field}.path`)),
|
|
1703
|
+
ino: readCount(raw.ino, `${field}.ino`),
|
|
1704
|
+
isDirectory: readBoolean(raw.is_directory, `${field}.is_directory`),
|
|
1705
|
+
size: readCount(raw.size, `${field}.size`),
|
|
1706
|
+
mtimeMs: readCount(raw.mtime_ms, `${field}.mtime_ms`),
|
|
1707
|
+
uid: optionalInteger(raw.uid),
|
|
1708
|
+
gid: optionalInteger(raw.gid),
|
|
1709
|
+
uidSubjectId: optionalText(raw.uid_subject_id),
|
|
1710
|
+
uidSubjectType: optionalText(raw.uid_subject_type),
|
|
1711
|
+
gidSubjectId: optionalText(raw.gid_subject_id),
|
|
1712
|
+
gidSubjectType: optionalText(raw.gid_subject_type)
|
|
1713
|
+
};
|
|
1714
|
+
}
|
|
1715
|
+
function readEntry(raw, field) {
|
|
1716
|
+
if (!isRecord2(raw)) {
|
|
1717
|
+
throw invalidField(field);
|
|
1718
|
+
}
|
|
1719
|
+
return withoutUndefined({
|
|
1720
|
+
...readNodeFields(raw, field),
|
|
1721
|
+
atimeMs: readCount(raw.atime_ms, `${field}.atime_ms`),
|
|
1722
|
+
uidDisplayName: optionalText(raw.uid_display_name),
|
|
1723
|
+
uidUsername: optionalText(raw.uid_username),
|
|
1724
|
+
gidDisplayName: optionalText(raw.gid_display_name),
|
|
1725
|
+
gidUsername: optionalText(raw.gid_username)
|
|
1726
|
+
});
|
|
1727
|
+
}
|
|
1728
|
+
function readTreeNode(raw, field) {
|
|
1729
|
+
if (!isRecord2(raw) || !Array.isArray(raw.children)) {
|
|
1730
|
+
throw invalidField(field);
|
|
1731
|
+
}
|
|
1732
|
+
return withoutUndefined({
|
|
1733
|
+
...readNodeFields(raw, field),
|
|
1734
|
+
children: raw.children.map((child, index) => readTreeNode(child, `${field}.children[${index}]`))
|
|
1735
|
+
});
|
|
1736
|
+
}
|
|
1737
|
+
function readListResponse(value) {
|
|
1738
|
+
if (!isRecord2(value) || !Array.isArray(value.entries)) {
|
|
1739
|
+
throw invalidField("entries");
|
|
1740
|
+
}
|
|
1741
|
+
return value.entries.map((entry, index) => readEntry(entry, `entries[${index}]`));
|
|
1742
|
+
}
|
|
1743
|
+
function readOperationResponse(value) {
|
|
1744
|
+
if (!isRecord2(value)) {
|
|
1745
|
+
throw new TypeError("Mountra file operation response is invalid");
|
|
1746
|
+
}
|
|
1747
|
+
return {
|
|
1748
|
+
path: normalizePath(readString2(value.path, "path")),
|
|
1749
|
+
ino: readCount(value.ino, "ino")
|
|
1750
|
+
};
|
|
1751
|
+
}
|
|
1752
|
+
function sameJson(left, right) {
|
|
1753
|
+
if (left === right) {
|
|
1754
|
+
return true;
|
|
1755
|
+
}
|
|
1756
|
+
if (!isRecord2(left) || !isRecord2(right) || Array.isArray(left) !== Array.isArray(right)) {
|
|
1757
|
+
return false;
|
|
1758
|
+
}
|
|
1759
|
+
const leftKeys = Object.keys(left);
|
|
1760
|
+
return leftKeys.length === Object.keys(right).length && leftKeys.every((key) => Object.hasOwn(right, key) && sameJson(left[key], right[key]));
|
|
1761
|
+
}
|
|
1762
|
+
function sameContent(left, right) {
|
|
1763
|
+
return left.kind === "list" && right.kind === "list" ? sameJson(left.entries, right.entries) : left.kind === "tree" && right.kind === "tree" && sameJson(left.root, right.root);
|
|
1764
|
+
}
|
|
1765
|
+
function workspaceBody(selector) {
|
|
1766
|
+
return {
|
|
1767
|
+
...selector.workspaceCuid !== void 0 ? { workspace_cuid: selector.workspaceCuid } : {},
|
|
1768
|
+
...selector.workspaceId !== void 0 ? { workspace_id: selector.workspaceId } : {}
|
|
1769
|
+
};
|
|
1770
|
+
}
|
|
1771
|
+
var MountraMetadata = class {
|
|
1772
|
+
transport;
|
|
1773
|
+
constructor(transport) {
|
|
1774
|
+
this.transport = transport;
|
|
1775
|
+
}
|
|
1776
|
+
list(options = {}) {
|
|
1777
|
+
return this.read("list", options, (record, source) => ({
|
|
1778
|
+
path: record.path,
|
|
1779
|
+
entries: record.entries,
|
|
1780
|
+
source,
|
|
1781
|
+
fetchedAt: record.fetchedAt
|
|
1782
|
+
}));
|
|
1783
|
+
}
|
|
1784
|
+
tree(options = {}) {
|
|
1785
|
+
return this.read("tree", options, (record, source) => ({
|
|
1786
|
+
path: record.path,
|
|
1787
|
+
root: record.root,
|
|
1788
|
+
source,
|
|
1789
|
+
fetchedAt: record.fetchedAt
|
|
1790
|
+
}));
|
|
1791
|
+
}
|
|
1792
|
+
async mkdir(options) {
|
|
1793
|
+
const selector = this.resolveWorkspace(options);
|
|
1794
|
+
const created = await this.operate(selector, "/file/mkdir", { path: normalizePath(options.path) }, options.signal);
|
|
1795
|
+
await this.afterWrite(selector, {
|
|
1796
|
+
type: "create",
|
|
1797
|
+
path: created.path,
|
|
1798
|
+
ino: created.ino,
|
|
1799
|
+
isDirectory: true,
|
|
1800
|
+
size: 0
|
|
1801
|
+
});
|
|
1802
|
+
return created;
|
|
1803
|
+
}
|
|
1804
|
+
async rename(options) {
|
|
1805
|
+
const selector = this.resolveWorkspace(options);
|
|
1806
|
+
const fromPath = normalizePath(options.fromPath);
|
|
1807
|
+
const renamed = await this.operate(
|
|
1808
|
+
selector,
|
|
1809
|
+
"/file/rename",
|
|
1810
|
+
{ from_path: fromPath, to_name: options.toName },
|
|
1811
|
+
options.signal
|
|
1812
|
+
);
|
|
1813
|
+
await this.afterWrite(selector, { type: "move", fromPath, toPath: renamed.path, ino: renamed.ino });
|
|
1814
|
+
return renamed;
|
|
1815
|
+
}
|
|
1816
|
+
async move(options) {
|
|
1817
|
+
const selector = this.resolveWorkspace(options);
|
|
1818
|
+
const fromPath = normalizePath(options.fromPath);
|
|
1819
|
+
const moved = await this.operate(
|
|
1820
|
+
selector,
|
|
1821
|
+
"/file/move",
|
|
1822
|
+
{ from_path: fromPath, to_path: normalizePath(options.toPath) },
|
|
1823
|
+
options.signal
|
|
1824
|
+
);
|
|
1825
|
+
await this.afterWrite(selector, { type: "move", fromPath, toPath: moved.path, ino: moved.ino });
|
|
1826
|
+
return moved;
|
|
1827
|
+
}
|
|
1828
|
+
async copy(options) {
|
|
1829
|
+
const selector = this.resolveWorkspace(options);
|
|
1830
|
+
const fromPath = normalizePath(options.fromPath);
|
|
1831
|
+
const copied = await this.operate(
|
|
1832
|
+
selector,
|
|
1833
|
+
"/file/copy",
|
|
1834
|
+
{ from_path: fromPath, to_path: normalizePath(options.toPath) },
|
|
1835
|
+
options.signal
|
|
1836
|
+
);
|
|
1837
|
+
await this.afterWrite(selector, { type: "copy", fromPath, toPath: copied.path, ino: copied.ino });
|
|
1838
|
+
return copied;
|
|
1839
|
+
}
|
|
1840
|
+
async delete(options) {
|
|
1841
|
+
const selector = this.resolveWorkspace(options);
|
|
1842
|
+
const path = normalizePath(options.path);
|
|
1843
|
+
await this.transport.requestJson("/file/delete", { ...workspaceBody(selector), path }, options.signal);
|
|
1844
|
+
await this.afterWrite(selector, { type: "remove", path });
|
|
1845
|
+
}
|
|
1846
|
+
/** Show a file that an upload just finished. The upload updates the download cache itself. */
|
|
1847
|
+
recordUpload(selector, file) {
|
|
1848
|
+
return this.applyChange(selector, {
|
|
1849
|
+
type: "create",
|
|
1850
|
+
path: normalizePath(file.path),
|
|
1851
|
+
...file.ino !== void 0 ? { ino: file.ino } : {},
|
|
1852
|
+
isDirectory: false,
|
|
1853
|
+
size: file.size
|
|
1854
|
+
});
|
|
1855
|
+
}
|
|
1856
|
+
async clearCache(selector) {
|
|
1857
|
+
const cache = this.transport.cache;
|
|
1858
|
+
if (!cache) {
|
|
1859
|
+
return;
|
|
1860
|
+
}
|
|
1861
|
+
if (selector === void 0) {
|
|
1862
|
+
await cache.clear();
|
|
1863
|
+
return;
|
|
1864
|
+
}
|
|
1865
|
+
const workspace = workspaceCacheKey(this.transport.baseUrl, selector);
|
|
1866
|
+
if (workspace === null) {
|
|
1867
|
+
throw new TypeError("workspaceCuid or workspaceId is required");
|
|
1868
|
+
}
|
|
1869
|
+
await cache.clear(workspace);
|
|
1870
|
+
}
|
|
1871
|
+
async read(kind, options, present) {
|
|
1872
|
+
const selector = this.resolveWorkspace(options);
|
|
1873
|
+
const path = normalizePath(options.path ?? "/");
|
|
1874
|
+
const scope = this.cacheScope(selector);
|
|
1875
|
+
const { signal, onUpdate, onError } = options;
|
|
1876
|
+
const cached = options.mode === "async" && scope ? await scope.cache.get(scope.workspace, kind, path) : null;
|
|
1877
|
+
if (!cached) {
|
|
1878
|
+
return present(await this.fetch(kind, selector, path, signal), "remote");
|
|
1879
|
+
}
|
|
1880
|
+
void this.fetch(kind, selector, path, signal).then(
|
|
1881
|
+
(fresh) => {
|
|
1882
|
+
if (!signal?.aborted && !sameContent(cached, fresh)) {
|
|
1883
|
+
onUpdate?.(present(fresh, "remote"));
|
|
1884
|
+
}
|
|
1885
|
+
},
|
|
1886
|
+
(error) => {
|
|
1887
|
+
if (!signal?.aborted) {
|
|
1888
|
+
onError?.(error);
|
|
1889
|
+
}
|
|
1890
|
+
}
|
|
1891
|
+
);
|
|
1892
|
+
return present(cached, "cache");
|
|
1893
|
+
}
|
|
1894
|
+
/** Fetch from Mountra, and cache the result before returning it. */
|
|
1895
|
+
async fetch(kind, selector, path, signal) {
|
|
1896
|
+
const scope = this.cacheScope(selector);
|
|
1897
|
+
const ticket = scope?.cache.beginFetch(scope.workspace);
|
|
1898
|
+
let body;
|
|
1899
|
+
try {
|
|
1900
|
+
body = await this.transport.requestJson(`/file/${kind}`, { ...workspaceBody(selector), path }, signal);
|
|
1901
|
+
} catch (error) {
|
|
1902
|
+
if (scope && ticket && error instanceof MountraApiError && error.status === 404) {
|
|
1903
|
+
await scope.cache.forgetFetched(scope.workspace, path, ticket);
|
|
1904
|
+
}
|
|
1905
|
+
throw error;
|
|
1906
|
+
}
|
|
1907
|
+
const fetchedAt = ticket?.startedAt ?? Date.now();
|
|
1908
|
+
let record;
|
|
1909
|
+
if (kind === "list") {
|
|
1910
|
+
record = { kind, path, entries: readListResponse(body), fetchedAt };
|
|
1911
|
+
} else {
|
|
1912
|
+
if (!isRecord2(body)) {
|
|
1913
|
+
throw invalidField("root");
|
|
1914
|
+
}
|
|
1915
|
+
record = { kind, path, root: readTreeNode(body.root, "root"), fetchedAt };
|
|
1916
|
+
}
|
|
1917
|
+
if (scope && ticket) {
|
|
1918
|
+
await scope.cache.storeFetched(scope.workspace, record, ticket);
|
|
1919
|
+
}
|
|
1920
|
+
return record;
|
|
1921
|
+
}
|
|
1922
|
+
async operate(selector, endpoint, body, signal) {
|
|
1923
|
+
return readOperationResponse(
|
|
1924
|
+
await this.transport.requestJson(endpoint, { ...workspaceBody(selector), ...body }, signal)
|
|
1925
|
+
);
|
|
1926
|
+
}
|
|
1927
|
+
/**
|
|
1928
|
+
* Reflect a write in both caches. Any download cached at a path the write
|
|
1929
|
+
* changed is stale: the file is gone, moved, or a new one now lives there.
|
|
1930
|
+
*/
|
|
1931
|
+
async afterWrite(selector, change) {
|
|
1932
|
+
const paths = change.type === "move" ? [change.fromPath, change.toPath] : change.type === "copy" ? [change.toPath] : [change.path];
|
|
1933
|
+
await Promise.all([
|
|
1934
|
+
this.applyChange(selector, change),
|
|
1935
|
+
this.transport.invalidateDownloads(selector, paths)
|
|
1936
|
+
]);
|
|
1937
|
+
}
|
|
1938
|
+
async applyChange(selector, change) {
|
|
1939
|
+
const scope = this.cacheScope(selector);
|
|
1940
|
+
await scope?.cache.applyChange(scope.workspace, change);
|
|
1941
|
+
}
|
|
1942
|
+
/** Fall back to the client workspace only when the call names none, as downloads do. */
|
|
1943
|
+
resolveWorkspace(selector) {
|
|
1944
|
+
const named = selector.workspaceCuid !== void 0 || selector.workspaceId !== void 0;
|
|
1945
|
+
const { workspaceCuid, workspaceId } = named ? selector : this.transport.clientSelector;
|
|
1946
|
+
if (workspaceCuid === void 0 && workspaceId === void 0) {
|
|
1947
|
+
throw new TypeError("workspaceCuid or workspaceId is required");
|
|
1948
|
+
}
|
|
1949
|
+
return {
|
|
1950
|
+
...workspaceCuid !== void 0 ? { workspaceCuid } : {},
|
|
1951
|
+
...workspaceId !== void 0 ? { workspaceId } : {}
|
|
1952
|
+
};
|
|
1953
|
+
}
|
|
1954
|
+
cacheScope(selector) {
|
|
1955
|
+
const cache = this.transport.cache;
|
|
1956
|
+
const workspace = workspaceCacheKey(this.transport.baseUrl, selector);
|
|
1957
|
+
return cache && workspace !== null ? { cache, workspace } : null;
|
|
1958
|
+
}
|
|
1959
|
+
};
|
|
1960
|
+
|
|
1961
|
+
// src/metadata-cache.ts
|
|
1962
|
+
var DEFAULT_MAX_ENTRIES = 1e3;
|
|
1963
|
+
var KINDS = ["list", "tree"];
|
|
1964
|
+
function parentOf(path) {
|
|
1965
|
+
const index = path.lastIndexOf("/");
|
|
1966
|
+
return index <= 0 ? "/" : path.slice(0, index);
|
|
1967
|
+
}
|
|
1968
|
+
function baseName(path) {
|
|
1969
|
+
return path.slice(path.lastIndexOf("/") + 1) || "/";
|
|
1970
|
+
}
|
|
1971
|
+
function selfAndAncestors(path) {
|
|
1972
|
+
const paths = [path];
|
|
1973
|
+
for (let current = path; current !== "/"; ) {
|
|
1974
|
+
current = parentOf(current);
|
|
1975
|
+
paths.push(current);
|
|
1976
|
+
}
|
|
1977
|
+
return paths;
|
|
1978
|
+
}
|
|
1979
|
+
function rebasePath(path, from, to) {
|
|
1980
|
+
return `${to}${path.slice(from.length)}`;
|
|
1981
|
+
}
|
|
1982
|
+
function memoryKey2(workspace, kind, path) {
|
|
1983
|
+
return JSON.stringify([workspace, kind, path]);
|
|
1984
|
+
}
|
|
1985
|
+
var InMemoryMetadataCacheStore = class {
|
|
1986
|
+
/** Map iteration order is the eviction order: the first entry was written least recently. */
|
|
1987
|
+
records = /* @__PURE__ */ new Map();
|
|
1988
|
+
async get(workspace, kind, path) {
|
|
1989
|
+
const entry = this.records.get(memoryKey2(workspace, kind, path));
|
|
1990
|
+
return entry ? structuredClone(entry.record) : null;
|
|
1991
|
+
}
|
|
1992
|
+
async getSubtree(workspace, path, kind) {
|
|
1993
|
+
return [...this.records.values()].filter(
|
|
1994
|
+
(entry) => entry.workspace === workspace && (kind === void 0 || entry.record.kind === kind) && isPathWithin(entry.record.path, path)
|
|
1995
|
+
).map((entry) => structuredClone(entry.record));
|
|
1996
|
+
}
|
|
1997
|
+
async write(workspace, batch, maxEntries) {
|
|
1998
|
+
for (const { kind, path } of batch.remove) {
|
|
1999
|
+
this.records.delete(memoryKey2(workspace, kind, path));
|
|
2000
|
+
}
|
|
2001
|
+
for (const record of batch.put) {
|
|
2002
|
+
const key = memoryKey2(workspace, record.kind, record.path);
|
|
2003
|
+
this.records.delete(key);
|
|
2004
|
+
this.records.set(key, { workspace, record: structuredClone(record) });
|
|
2005
|
+
}
|
|
2006
|
+
for (const key of this.records.keys()) {
|
|
2007
|
+
if (this.records.size <= maxEntries) {
|
|
2008
|
+
break;
|
|
2009
|
+
}
|
|
2010
|
+
this.records.delete(key);
|
|
2011
|
+
}
|
|
2012
|
+
}
|
|
2013
|
+
async clear(workspace) {
|
|
2014
|
+
for (const [key, entry] of this.records) {
|
|
2015
|
+
if (workspace === void 0 || entry.workspace === workspace) {
|
|
2016
|
+
this.records.delete(key);
|
|
2017
|
+
}
|
|
2018
|
+
}
|
|
2019
|
+
}
|
|
2020
|
+
};
|
|
2021
|
+
var METADATA_DATABASE_NAME = "mountra-sdk-metadata-cache";
|
|
2022
|
+
var METADATA_DATABASE_VERSION = 1;
|
|
2023
|
+
var RECORDS_STORE = "records";
|
|
2024
|
+
var META_STORE2 = "meta";
|
|
2025
|
+
var WRITE_ORDER_INDEX = "written";
|
|
2026
|
+
var IndexedDBMetadataCacheStore = class {
|
|
2027
|
+
databaseName;
|
|
2028
|
+
fallback = new InMemoryMetadataCacheStore();
|
|
2029
|
+
databasePromise;
|
|
2030
|
+
constructor(options = {}) {
|
|
2031
|
+
this.databaseName = options.databaseName ?? METADATA_DATABASE_NAME;
|
|
2032
|
+
}
|
|
2033
|
+
async get(workspace, kind, path) {
|
|
2034
|
+
const database = await this.openDatabase();
|
|
2035
|
+
if (!database) {
|
|
2036
|
+
return this.fallback.get(workspace, kind, path);
|
|
2037
|
+
}
|
|
2038
|
+
return runTransaction(database, [RECORDS_STORE], "readonly", async (transaction) => {
|
|
2039
|
+
const stored = await requestResult(
|
|
2040
|
+
transaction.objectStore(RECORDS_STORE).get([workspace, kind, path])
|
|
2041
|
+
);
|
|
2042
|
+
return stored?.record ?? null;
|
|
2043
|
+
});
|
|
2044
|
+
}
|
|
2045
|
+
async getSubtree(workspace, path, kind) {
|
|
2046
|
+
const database = await this.openDatabase();
|
|
2047
|
+
if (!database) {
|
|
2048
|
+
return this.fallback.getSubtree(workspace, path, kind);
|
|
2049
|
+
}
|
|
2050
|
+
const upper = path === "/" ? "0" : `${path}0`;
|
|
2051
|
+
return runTransaction(database, [RECORDS_STORE], "readonly", async (transaction) => {
|
|
2052
|
+
const records = [];
|
|
2053
|
+
for (const candidate of kind === void 0 ? KINDS : [kind]) {
|
|
2054
|
+
const stored = await requestResult(
|
|
2055
|
+
transaction.objectStore(RECORDS_STORE).getAll(IDBKeyRange.bound([workspace, candidate, path], [workspace, candidate, upper], false, true))
|
|
2056
|
+
);
|
|
2057
|
+
records.push(
|
|
2058
|
+
...stored.map(({ record }) => record).filter((record) => isPathWithin(record.path, path))
|
|
2059
|
+
);
|
|
2060
|
+
}
|
|
2061
|
+
return records;
|
|
2062
|
+
});
|
|
2063
|
+
}
|
|
2064
|
+
async write(workspace, batch, maxEntries) {
|
|
2065
|
+
const database = await this.openDatabase();
|
|
2066
|
+
if (!database) {
|
|
2067
|
+
return this.fallback.write(workspace, batch, maxEntries);
|
|
2068
|
+
}
|
|
2069
|
+
await runTransaction(database, [RECORDS_STORE, META_STORE2], "readwrite", async (transaction) => {
|
|
2070
|
+
const records = transaction.objectStore(RECORDS_STORE);
|
|
2071
|
+
const meta = transaction.objectStore(META_STORE2);
|
|
2072
|
+
for (const { kind, path } of batch.remove) {
|
|
2073
|
+
await requestResult(records.delete([workspace, kind, path]));
|
|
2074
|
+
}
|
|
2075
|
+
for (const record of batch.put) {
|
|
2076
|
+
const stored = { record, written: await nextClock(meta) };
|
|
2077
|
+
await requestResult(records.put(stored, [workspace, record.kind, record.path]));
|
|
2078
|
+
}
|
|
2079
|
+
let count = await requestResult(records.count());
|
|
2080
|
+
if (count <= maxEntries) {
|
|
2081
|
+
return;
|
|
2082
|
+
}
|
|
2083
|
+
const cursorRequest = records.index(WRITE_ORDER_INDEX).openCursor();
|
|
2084
|
+
let cursor = await requestResult(cursorRequest);
|
|
2085
|
+
while (cursor && count > maxEntries) {
|
|
2086
|
+
await requestResult(cursor.delete());
|
|
2087
|
+
count -= 1;
|
|
2088
|
+
cursor.continue();
|
|
2089
|
+
cursor = await requestResult(cursorRequest);
|
|
2090
|
+
}
|
|
2091
|
+
});
|
|
2092
|
+
}
|
|
2093
|
+
async clear(workspace) {
|
|
2094
|
+
const database = await this.openDatabase();
|
|
2095
|
+
if (!database) {
|
|
2096
|
+
return this.fallback.clear(workspace);
|
|
2097
|
+
}
|
|
2098
|
+
await runTransaction(database, [RECORDS_STORE], "readwrite", async (transaction) => {
|
|
2099
|
+
const records = transaction.objectStore(RECORDS_STORE);
|
|
2100
|
+
await requestResult(
|
|
2101
|
+
workspace === void 0 ? records.clear() : records.delete(workspaceRange(workspace))
|
|
2102
|
+
);
|
|
2103
|
+
});
|
|
2104
|
+
}
|
|
2105
|
+
async openDatabase() {
|
|
2106
|
+
this.databasePromise ??= openDatabase(
|
|
2107
|
+
this.databaseName,
|
|
2108
|
+
METADATA_DATABASE_VERSION,
|
|
2109
|
+
(database) => {
|
|
2110
|
+
if (!database.objectStoreNames.contains(RECORDS_STORE)) {
|
|
2111
|
+
database.createObjectStore(RECORDS_STORE).createIndex(WRITE_ORDER_INDEX, "written");
|
|
2112
|
+
}
|
|
2113
|
+
if (!database.objectStoreNames.contains(META_STORE2)) {
|
|
2114
|
+
database.createObjectStore(META_STORE2);
|
|
2115
|
+
}
|
|
2116
|
+
},
|
|
2117
|
+
() => {
|
|
2118
|
+
this.databasePromise = void 0;
|
|
2119
|
+
}
|
|
2120
|
+
);
|
|
2121
|
+
return this.databasePromise;
|
|
2122
|
+
}
|
|
2123
|
+
};
|
|
2124
|
+
var NODE_FIELDS = [
|
|
2125
|
+
"name",
|
|
2126
|
+
"path",
|
|
2127
|
+
"ino",
|
|
2128
|
+
"isDirectory",
|
|
2129
|
+
"size",
|
|
2130
|
+
"mtimeMs",
|
|
2131
|
+
"uid",
|
|
2132
|
+
"gid",
|
|
2133
|
+
"uidSubjectId",
|
|
2134
|
+
"uidSubjectType",
|
|
2135
|
+
"gidSubjectId",
|
|
2136
|
+
"gidSubjectType"
|
|
2137
|
+
];
|
|
2138
|
+
function pickDefined(source, keys) {
|
|
2139
|
+
const picked = {};
|
|
2140
|
+
for (const key of keys) {
|
|
2141
|
+
if (source[key] !== void 0) {
|
|
2142
|
+
picked[key] = source[key];
|
|
2143
|
+
}
|
|
2144
|
+
}
|
|
2145
|
+
return picked;
|
|
2146
|
+
}
|
|
2147
|
+
function nodeFromEntry(entry, children) {
|
|
2148
|
+
return { ...pickDefined(entry, NODE_FIELDS), children };
|
|
2149
|
+
}
|
|
2150
|
+
function entryFromNode(node, previous) {
|
|
2151
|
+
return {
|
|
2152
|
+
...pickDefined(node, NODE_FIELDS),
|
|
2153
|
+
atimeMs: previous.atimeMs,
|
|
2154
|
+
...node.uid === previous.uid ? pickDefined(previous, ["uidDisplayName", "uidUsername"]) : {},
|
|
2155
|
+
...node.gid === previous.gid ? pickDefined(previous, ["gidDisplayName", "gidUsername"]) : {}
|
|
2156
|
+
};
|
|
2157
|
+
}
|
|
2158
|
+
function findNode(root, path) {
|
|
2159
|
+
let node = root;
|
|
2160
|
+
while (node && node.path !== path) {
|
|
2161
|
+
node = isPathWithin(path, node.path) ? node.children.find((child) => isPathWithin(path, child.path)) : void 0;
|
|
2162
|
+
}
|
|
2163
|
+
return node;
|
|
2164
|
+
}
|
|
2165
|
+
function rebaseNode(node, from, to) {
|
|
2166
|
+
return {
|
|
2167
|
+
...node,
|
|
2168
|
+
...node.path === from ? { name: baseName(to) } : {},
|
|
2169
|
+
path: rebasePath(node.path, from, to),
|
|
2170
|
+
children: node.children.map((child) => rebaseNode(child, from, to))
|
|
2171
|
+
};
|
|
2172
|
+
}
|
|
2173
|
+
function compareChildren(left, right) {
|
|
2174
|
+
return Number(right.isDirectory) - Number(left.isDirectory) || left.name.localeCompare(right.name);
|
|
2175
|
+
}
|
|
2176
|
+
function withChild(children, child) {
|
|
2177
|
+
const result = [...children];
|
|
2178
|
+
const existing = result.findIndex((candidate) => candidate.path === child.path);
|
|
2179
|
+
if (existing >= 0) {
|
|
2180
|
+
result[existing] = child;
|
|
2181
|
+
return result;
|
|
2182
|
+
}
|
|
2183
|
+
const position = result.findIndex((candidate) => compareChildren(child, candidate) < 0);
|
|
2184
|
+
result.splice(position < 0 ? result.length : position, 0, child);
|
|
2185
|
+
return result;
|
|
2186
|
+
}
|
|
2187
|
+
function recordKey(kind, path) {
|
|
2188
|
+
return `${kind}:${path}`;
|
|
2189
|
+
}
|
|
2190
|
+
var MetadataDraft = class {
|
|
2191
|
+
records = /* @__PURE__ */ new Map();
|
|
2192
|
+
changed = /* @__PURE__ */ new Set();
|
|
2193
|
+
removed = /* @__PURE__ */ new Map();
|
|
2194
|
+
constructor(records) {
|
|
2195
|
+
for (const record of records) {
|
|
2196
|
+
this.records.set(recordKey(record.kind, record.path), record);
|
|
2197
|
+
}
|
|
2198
|
+
}
|
|
2199
|
+
list(path) {
|
|
2200
|
+
const record = this.records.get(recordKey("list", path));
|
|
2201
|
+
return record?.kind === "list" ? record : void 0;
|
|
2202
|
+
}
|
|
2203
|
+
tree(path) {
|
|
2204
|
+
const record = this.records.get(recordKey("tree", path));
|
|
2205
|
+
return record?.kind === "tree" ? record : void 0;
|
|
2206
|
+
}
|
|
2207
|
+
/** Loaded trees that hold `path`: those rooted at `path` or at one of its ancestors. */
|
|
2208
|
+
treesContaining(path) {
|
|
2209
|
+
return [...this.records.values()].filter(
|
|
2210
|
+
(record) => record.kind === "tree" && isPathWithin(path, record.path)
|
|
2211
|
+
);
|
|
2212
|
+
}
|
|
2213
|
+
/** Loaded records at `path` or below it. */
|
|
2214
|
+
within(path) {
|
|
2215
|
+
return [...this.records.values()].filter((record) => isPathWithin(record.path, path));
|
|
2216
|
+
}
|
|
2217
|
+
put(record) {
|
|
2218
|
+
const key = recordKey(record.kind, record.path);
|
|
2219
|
+
this.records.set(key, record);
|
|
2220
|
+
this.removed.delete(key);
|
|
2221
|
+
this.changed.add(key);
|
|
2222
|
+
}
|
|
2223
|
+
/** Mark a loaded record that was changed in place. */
|
|
2224
|
+
touch(record) {
|
|
2225
|
+
this.changed.add(recordKey(record.kind, record.path));
|
|
2226
|
+
}
|
|
2227
|
+
remove(record) {
|
|
2228
|
+
const key = recordKey(record.kind, record.path);
|
|
2229
|
+
this.records.delete(key);
|
|
2230
|
+
this.changed.delete(key);
|
|
2231
|
+
this.removed.set(key, { kind: record.kind, path: record.path });
|
|
2232
|
+
}
|
|
2233
|
+
batch() {
|
|
2234
|
+
return {
|
|
2235
|
+
put: [...this.changed].map((key) => this.records.get(key)),
|
|
2236
|
+
remove: [...this.removed.values()]
|
|
2237
|
+
};
|
|
2238
|
+
}
|
|
2239
|
+
findEntry(path) {
|
|
2240
|
+
return this.list(parentOf(path))?.entries.find((entry) => entry.path === path);
|
|
2241
|
+
}
|
|
2242
|
+
/** The node of `path` with its complete subtree, from any loaded tree. */
|
|
2243
|
+
findNode(path) {
|
|
2244
|
+
for (const tree of this.treesContaining(path)) {
|
|
2245
|
+
const node = findNode(tree.root, path);
|
|
2246
|
+
if (node) {
|
|
2247
|
+
return node;
|
|
2248
|
+
}
|
|
2249
|
+
}
|
|
2250
|
+
return void 0;
|
|
2251
|
+
}
|
|
2252
|
+
/** Remove `path` from the listing and the trees that show it. */
|
|
2253
|
+
removeEntry(path) {
|
|
2254
|
+
const parentPath = parentOf(path);
|
|
2255
|
+
const list = this.list(parentPath);
|
|
2256
|
+
if (list?.entries.some((entry) => entry.path === path)) {
|
|
2257
|
+
list.entries = list.entries.filter((entry) => entry.path !== path);
|
|
2258
|
+
this.touch(list);
|
|
2259
|
+
}
|
|
2260
|
+
for (const tree of this.treesContaining(parentPath)) {
|
|
2261
|
+
const parent = findNode(tree.root, parentPath);
|
|
2262
|
+
if (parent?.children.some((child) => child.path === path)) {
|
|
2263
|
+
parent.children = parent.children.filter((child) => child.path !== path);
|
|
2264
|
+
this.touch(tree);
|
|
2265
|
+
}
|
|
2266
|
+
}
|
|
2267
|
+
}
|
|
2268
|
+
/**
|
|
2269
|
+
* Show `path` in its parent's listing and in the trees holding its parent.
|
|
2270
|
+
* A record that cannot show it faithfully, because the entry or the
|
|
2271
|
+
* complete node is unknown, is dropped instead.
|
|
2272
|
+
*/
|
|
2273
|
+
insertEntry(path, entry, node) {
|
|
2274
|
+
const parentPath = parentOf(path);
|
|
2275
|
+
const list = this.list(parentPath);
|
|
2276
|
+
if (list && entry) {
|
|
2277
|
+
list.entries = withChild(list.entries, entry);
|
|
2278
|
+
this.touch(list);
|
|
2279
|
+
} else if (list) {
|
|
2280
|
+
this.remove(list);
|
|
2281
|
+
}
|
|
2282
|
+
for (const tree of this.treesContaining(parentPath)) {
|
|
2283
|
+
const parent = findNode(tree.root, parentPath);
|
|
2284
|
+
if (node && parent?.isDirectory) {
|
|
2285
|
+
parent.children = withChild(parent.children, structuredClone(node));
|
|
2286
|
+
this.touch(tree);
|
|
2287
|
+
} else {
|
|
2288
|
+
this.remove(tree);
|
|
2289
|
+
}
|
|
2290
|
+
}
|
|
2291
|
+
}
|
|
2292
|
+
removeSubtree(path) {
|
|
2293
|
+
for (const record of this.within(path)) {
|
|
2294
|
+
this.remove(record);
|
|
2295
|
+
}
|
|
2296
|
+
}
|
|
2297
|
+
/** Re-key the records at or below `from` to `to`, rewriting the paths they hold. */
|
|
2298
|
+
moveSubtree(from, to) {
|
|
2299
|
+
for (const record of this.within(from)) {
|
|
2300
|
+
this.remove(record);
|
|
2301
|
+
const path = rebasePath(record.path, from, to);
|
|
2302
|
+
this.put(
|
|
2303
|
+
record.kind === "list" ? {
|
|
2304
|
+
...record,
|
|
2305
|
+
path,
|
|
2306
|
+
entries: record.entries.map((entry) => ({ ...entry, path: rebasePath(entry.path, from, to) }))
|
|
2307
|
+
} : { ...record, path, root: rebaseNode(record.root, from, to) }
|
|
2308
|
+
);
|
|
2309
|
+
}
|
|
2310
|
+
}
|
|
2311
|
+
};
|
|
2312
|
+
function changeScope(change) {
|
|
2313
|
+
const paths = change.type === "move" || change.type === "copy" ? [change.fromPath, change.toPath] : [change.path];
|
|
2314
|
+
return {
|
|
2315
|
+
lists: paths.map(parentOf),
|
|
2316
|
+
trees: paths.flatMap((path) => selfAndAncestors(parentOf(path))),
|
|
2317
|
+
// A copy keeps its source; any other change makes records below its paths stale or moves them.
|
|
2318
|
+
subtrees: change.type === "copy" ? [change.toPath] : paths
|
|
2319
|
+
};
|
|
2320
|
+
}
|
|
2321
|
+
function patchChange(draft, change, now) {
|
|
2322
|
+
switch (change.type) {
|
|
2323
|
+
case "create": {
|
|
2324
|
+
const previousEntry = draft.findEntry(change.path);
|
|
2325
|
+
const previousNode = draft.findNode(change.path);
|
|
2326
|
+
draft.removeSubtree(change.path);
|
|
2327
|
+
if (change.ino === void 0) {
|
|
2328
|
+
draft.insertEntry(change.path, null, null);
|
|
2329
|
+
return;
|
|
2330
|
+
}
|
|
2331
|
+
const fields = {
|
|
2332
|
+
name: baseName(change.path),
|
|
2333
|
+
path: change.path,
|
|
2334
|
+
ino: change.ino,
|
|
2335
|
+
isDirectory: change.isDirectory,
|
|
2336
|
+
size: change.size,
|
|
2337
|
+
mtimeMs: now
|
|
2338
|
+
};
|
|
2339
|
+
const sameKind = (previous) => previous?.isDirectory === change.isDirectory;
|
|
2340
|
+
draft.insertEntry(
|
|
2341
|
+
change.path,
|
|
2342
|
+
{ atimeMs: now, ...sameKind(previousEntry) ? previousEntry : {}, ...fields },
|
|
2343
|
+
{ ...sameKind(previousNode) ? previousNode : {}, ...fields, children: [] }
|
|
2344
|
+
);
|
|
2345
|
+
return;
|
|
2346
|
+
}
|
|
2347
|
+
case "remove":
|
|
2348
|
+
draft.removeEntry(change.path);
|
|
2349
|
+
draft.removeSubtree(change.path);
|
|
2350
|
+
return;
|
|
2351
|
+
case "move": {
|
|
2352
|
+
const { fromPath, toPath } = change;
|
|
2353
|
+
if (fromPath === toPath) {
|
|
2354
|
+
return;
|
|
2355
|
+
}
|
|
2356
|
+
const entry = draft.findEntry(fromPath);
|
|
2357
|
+
const node = draft.findNode(fromPath);
|
|
2358
|
+
draft.removeEntry(fromPath);
|
|
2359
|
+
draft.removeSubtree(toPath);
|
|
2360
|
+
draft.moveSubtree(fromPath, toPath);
|
|
2361
|
+
const fields = { name: baseName(toPath), path: toPath, ino: change.ino };
|
|
2362
|
+
const movedEntry = entry ? { ...entry, ...fields } : null;
|
|
2363
|
+
draft.insertEntry(
|
|
2364
|
+
toPath,
|
|
2365
|
+
movedEntry,
|
|
2366
|
+
node ? { ...rebaseNode(node, fromPath, toPath), ino: change.ino } : movedEntry && !movedEntry.isDirectory ? nodeFromEntry(movedEntry, []) : null
|
|
2367
|
+
);
|
|
2368
|
+
return;
|
|
2369
|
+
}
|
|
2370
|
+
case "copy": {
|
|
2371
|
+
const source = draft.findEntry(change.fromPath) ?? draft.findNode(change.fromPath);
|
|
2372
|
+
draft.removeSubtree(change.toPath);
|
|
2373
|
+
if (!source || source.isDirectory) {
|
|
2374
|
+
draft.insertEntry(change.toPath, null, null);
|
|
2375
|
+
return;
|
|
2376
|
+
}
|
|
2377
|
+
const fields = {
|
|
2378
|
+
name: baseName(change.toPath),
|
|
2379
|
+
path: change.toPath,
|
|
2380
|
+
ino: change.ino,
|
|
2381
|
+
isDirectory: false,
|
|
2382
|
+
size: source.size,
|
|
2383
|
+
mtimeMs: now
|
|
2384
|
+
};
|
|
2385
|
+
draft.insertEntry(change.toPath, { ...fields, atimeMs: now }, { ...fields, children: [] });
|
|
2386
|
+
return;
|
|
2387
|
+
}
|
|
2388
|
+
}
|
|
2389
|
+
}
|
|
2390
|
+
function childrenFromEntries(directory, entries) {
|
|
2391
|
+
const children = [];
|
|
2392
|
+
for (const entry of entries) {
|
|
2393
|
+
const previous = directory.children.find((child) => child.path === entry.path);
|
|
2394
|
+
if (!entry.isDirectory) {
|
|
2395
|
+
children.push(nodeFromEntry(entry, []));
|
|
2396
|
+
} else if (previous?.isDirectory && previous.ino === entry.ino) {
|
|
2397
|
+
children.push(nodeFromEntry(entry, previous.children));
|
|
2398
|
+
} else {
|
|
2399
|
+
return null;
|
|
2400
|
+
}
|
|
2401
|
+
}
|
|
2402
|
+
return children;
|
|
2403
|
+
}
|
|
2404
|
+
function entriesFromNodes(entries, nodes) {
|
|
2405
|
+
const previous = new Map(entries.map((entry) => [entry.path, entry]));
|
|
2406
|
+
const fresh = new Map(nodes.map((node) => [node.path, node]));
|
|
2407
|
+
for (const node of nodes) {
|
|
2408
|
+
const entry = previous.get(node.path);
|
|
2409
|
+
if (!entry || entry.ino !== node.ino || entry.isDirectory !== node.isDirectory) {
|
|
2410
|
+
return null;
|
|
2411
|
+
}
|
|
2412
|
+
}
|
|
2413
|
+
return entries.filter((entry) => fresh.has(entry.path)).map((entry) => entryFromNode(fresh.get(entry.path), entry));
|
|
2414
|
+
}
|
|
2415
|
+
function patchFetchedList(draft, record) {
|
|
2416
|
+
if ((draft.list(record.path)?.fetchedAt ?? 0) > record.fetchedAt) {
|
|
2417
|
+
return;
|
|
2418
|
+
}
|
|
2419
|
+
draft.put(record);
|
|
2420
|
+
for (const tree of draft.treesContaining(record.path)) {
|
|
2421
|
+
if (tree.fetchedAt > record.fetchedAt) {
|
|
2422
|
+
continue;
|
|
2423
|
+
}
|
|
2424
|
+
const directory = findNode(tree.root, record.path);
|
|
2425
|
+
const children = directory?.isDirectory ? childrenFromEntries(directory, record.entries) : null;
|
|
2426
|
+
if (directory && children) {
|
|
2427
|
+
directory.children = children;
|
|
2428
|
+
draft.touch(tree);
|
|
2429
|
+
} else {
|
|
2430
|
+
draft.remove(tree);
|
|
2431
|
+
}
|
|
2432
|
+
}
|
|
2433
|
+
}
|
|
2434
|
+
function patchFetchedTree(draft, record) {
|
|
2435
|
+
if ((draft.tree(record.path)?.fetchedAt ?? 0) > record.fetchedAt) {
|
|
2436
|
+
return;
|
|
2437
|
+
}
|
|
2438
|
+
draft.put(record);
|
|
2439
|
+
for (const other of draft.within(record.path)) {
|
|
2440
|
+
if (other === record || other.fetchedAt > record.fetchedAt) {
|
|
2441
|
+
continue;
|
|
2442
|
+
}
|
|
2443
|
+
const node = findNode(record.root, other.path);
|
|
2444
|
+
if (other.kind === "tree") {
|
|
2445
|
+
if (node?.isDirectory) {
|
|
2446
|
+
draft.put({ ...other, root: structuredClone(node), fetchedAt: record.fetchedAt });
|
|
2447
|
+
} else {
|
|
2448
|
+
draft.remove(other);
|
|
2449
|
+
}
|
|
2450
|
+
continue;
|
|
2451
|
+
}
|
|
2452
|
+
const entries = node?.isDirectory ? entriesFromNodes(other.entries, node.children) : null;
|
|
2453
|
+
if (entries) {
|
|
2454
|
+
other.entries = entries;
|
|
2455
|
+
draft.touch(other);
|
|
2456
|
+
} else {
|
|
2457
|
+
draft.remove(other);
|
|
2458
|
+
}
|
|
2459
|
+
}
|
|
2460
|
+
if (record.path === "/") {
|
|
2461
|
+
return;
|
|
2462
|
+
}
|
|
2463
|
+
const parentPath = parentOf(record.path);
|
|
2464
|
+
for (const tree of draft.treesContaining(parentPath)) {
|
|
2465
|
+
if (tree.fetchedAt > record.fetchedAt) {
|
|
2466
|
+
continue;
|
|
2467
|
+
}
|
|
2468
|
+
const parent = findNode(tree.root, parentPath);
|
|
2469
|
+
if (parent?.isDirectory) {
|
|
2470
|
+
parent.children = withChild(parent.children, structuredClone(record.root));
|
|
2471
|
+
draft.touch(tree);
|
|
2472
|
+
} else {
|
|
2473
|
+
draft.remove(tree);
|
|
2474
|
+
}
|
|
2475
|
+
}
|
|
2476
|
+
}
|
|
2477
|
+
var MetadataCache = class {
|
|
2478
|
+
maxEntries;
|
|
2479
|
+
store;
|
|
2480
|
+
/** Per workspace, how many writes this client has applied. */
|
|
2481
|
+
writeSeqs = /* @__PURE__ */ new Map();
|
|
2482
|
+
/** Per workspace, the tail of the queue that serializes cache updates. */
|
|
2483
|
+
queues = /* @__PURE__ */ new Map();
|
|
2484
|
+
constructor(options) {
|
|
2485
|
+
this.maxEntries = options.maxEntries ?? DEFAULT_MAX_ENTRIES;
|
|
2486
|
+
if (!Number.isSafeInteger(this.maxEntries) || this.maxEntries <= 0) {
|
|
2487
|
+
throw new RangeError("metadataCache.maxEntries must be a positive safe integer");
|
|
2488
|
+
}
|
|
2489
|
+
this.store = options.store ?? new IndexedDBMetadataCacheStore();
|
|
2490
|
+
}
|
|
2491
|
+
async get(workspace, kind, path) {
|
|
2492
|
+
try {
|
|
2493
|
+
const record = await this.store.get(workspace, kind, path);
|
|
2494
|
+
return record?.kind === kind && record.path === path ? record : null;
|
|
2495
|
+
} catch {
|
|
2496
|
+
return null;
|
|
2497
|
+
}
|
|
2498
|
+
}
|
|
2499
|
+
beginFetch(workspace) {
|
|
2500
|
+
return { startedAt: Date.now(), writeSeq: this.writeSeqs.get(workspace) ?? 0 };
|
|
2501
|
+
}
|
|
2502
|
+
/** Cache a fetched result, and bring the records it overlaps up to date with it. */
|
|
2503
|
+
async storeFetched(workspace, record, ticket) {
|
|
2504
|
+
await this.update(workspace, ticket, async () => {
|
|
2505
|
+
if (record.kind === "list") {
|
|
2506
|
+
const draft2 = await this.load(workspace, { lists: [record.path], trees: selfAndAncestors(record.path) });
|
|
2507
|
+
patchFetchedList(draft2, record);
|
|
2508
|
+
return draft2;
|
|
2509
|
+
}
|
|
2510
|
+
const draft = await this.load(workspace, { trees: selfAndAncestors(record.path), subtrees: [record.path] });
|
|
2511
|
+
patchFetchedTree(draft, record);
|
|
2512
|
+
return draft;
|
|
2513
|
+
}).catch(() => void 0);
|
|
2514
|
+
}
|
|
2515
|
+
/** Mountra reported that `path` does not exist. */
|
|
2516
|
+
async forgetFetched(workspace, path, ticket) {
|
|
2517
|
+
const change = { type: "remove", path };
|
|
2518
|
+
await this.update(workspace, ticket, async () => {
|
|
2519
|
+
const draft = await this.load(workspace, changeScope(change));
|
|
2520
|
+
patchChange(draft, change, Date.now());
|
|
2521
|
+
return draft;
|
|
2522
|
+
}).catch(() => void 0);
|
|
2523
|
+
}
|
|
2524
|
+
/** Reflect a write that Mountra completed. */
|
|
2525
|
+
async applyChange(workspace, change) {
|
|
2526
|
+
this.writeSeqs.set(workspace, (this.writeSeqs.get(workspace) ?? 0) + 1);
|
|
2527
|
+
await this.update(workspace, null, async () => {
|
|
2528
|
+
const draft = await this.load(workspace, changeScope(change));
|
|
2529
|
+
patchChange(draft, change, Date.now());
|
|
2530
|
+
return draft;
|
|
2531
|
+
}).catch(
|
|
2532
|
+
() => (
|
|
2533
|
+
// A write that cannot be reflected must not leave records that contradict it.
|
|
2534
|
+
this.store.clear(workspace).catch(() => void 0)
|
|
2535
|
+
)
|
|
2536
|
+
);
|
|
2537
|
+
}
|
|
2538
|
+
clear(workspace) {
|
|
2539
|
+
return this.store.clear(workspace);
|
|
2540
|
+
}
|
|
2541
|
+
/**
|
|
2542
|
+
* Run updates of one workspace one at a time. An update for a fetch is
|
|
2543
|
+
* dropped when this client completed a write since the fetch started: the
|
|
2544
|
+
* result may predate that write.
|
|
2545
|
+
*/
|
|
2546
|
+
update(workspace, ticket, build) {
|
|
2547
|
+
const run = async () => {
|
|
2548
|
+
if (ticket && ticket.writeSeq !== (this.writeSeqs.get(workspace) ?? 0)) {
|
|
2549
|
+
return;
|
|
2550
|
+
}
|
|
2551
|
+
const batch = (await build()).batch();
|
|
2552
|
+
if (batch.put.length > 0 || batch.remove.length > 0) {
|
|
2553
|
+
await this.store.write(workspace, batch, this.maxEntries);
|
|
2554
|
+
}
|
|
2555
|
+
};
|
|
2556
|
+
const result = (this.queues.get(workspace) ?? Promise.resolve()).then(run);
|
|
2557
|
+
const tail = result.catch(() => void 0);
|
|
2558
|
+
this.queues.set(workspace, tail);
|
|
2559
|
+
void tail.then(() => {
|
|
2560
|
+
if (this.queues.get(workspace) === tail) {
|
|
2561
|
+
this.queues.delete(workspace);
|
|
2562
|
+
}
|
|
2563
|
+
});
|
|
2564
|
+
return result;
|
|
2565
|
+
}
|
|
2566
|
+
async load(workspace, scope) {
|
|
2567
|
+
const [records, subtrees] = await Promise.all([
|
|
2568
|
+
Promise.all([
|
|
2569
|
+
...[...new Set(scope.lists)].map((path) => this.store.get(workspace, "list", path)),
|
|
2570
|
+
...[...new Set(scope.trees)].map((path) => this.store.get(workspace, "tree", path))
|
|
2571
|
+
]),
|
|
2572
|
+
Promise.all([...new Set(scope.subtrees)].map((path) => this.store.getSubtree(workspace, path)))
|
|
2573
|
+
]);
|
|
2574
|
+
return new MetadataDraft([
|
|
2575
|
+
...records.filter((record) => record !== null),
|
|
2576
|
+
...subtrees.flat()
|
|
2577
|
+
]);
|
|
2578
|
+
}
|
|
2579
|
+
};
|
|
2580
|
+
|
|
2581
|
+
// src/upload.ts
|
|
2582
|
+
var JUMBO_CHUNK_THRESHOLD_BYTES = 128 * 1024 * 1024;
|
|
2583
|
+
var MIN_S3_MULTIPART_PART_SIZE_BYTES = 5 * 1024 * 1024;
|
|
2584
|
+
var DEFAULT_S3_MULTIPART_PART_SIZE_BYTES = 16 * 1024 * 1024;
|
|
2585
|
+
var MAX_S3_MULTIPART_PARTS = 1e4;
|
|
1604
2586
|
var memoryCheckpointRecords = /* @__PURE__ */ new Map();
|
|
1605
2587
|
function parseJumboCheckpoint(value) {
|
|
1606
|
-
if (!
|
|
2588
|
+
if (!isRecord3(value) || value.version !== 1 || typeof value.storageKey !== "string") {
|
|
1607
2589
|
return null;
|
|
1608
2590
|
}
|
|
1609
2591
|
if (typeof value.chunkIndex !== "number" || typeof value.chunkSize !== "number" || typeof value.chunkSha256 !== "string" || typeof value.partSize !== "number" || typeof value.uploadId !== "string" || !Array.isArray(value.parts)) {
|
|
@@ -1611,7 +2593,7 @@ function parseJumboCheckpoint(value) {
|
|
|
1611
2593
|
}
|
|
1612
2594
|
const parts = [];
|
|
1613
2595
|
for (const part of value.parts) {
|
|
1614
|
-
if (!
|
|
2596
|
+
if (!isRecord3(part)) {
|
|
1615
2597
|
return null;
|
|
1616
2598
|
}
|
|
1617
2599
|
if (typeof part.partNumber !== "number" || typeof part.etag !== "string" || typeof part.size !== "number" || typeof part.sha256 !== "string") {
|
|
@@ -1790,11 +2772,11 @@ function throwIfAborted2(signal) {
|
|
|
1790
2772
|
}
|
|
1791
2773
|
throw new DOMException("The upload was aborted", "AbortError");
|
|
1792
2774
|
}
|
|
1793
|
-
function
|
|
2775
|
+
function isRecord3(value) {
|
|
1794
2776
|
return typeof value === "object" && value !== null;
|
|
1795
2777
|
}
|
|
1796
2778
|
function parseApiErrorBody(body) {
|
|
1797
|
-
if (!
|
|
2779
|
+
if (!isRecord3(body)) {
|
|
1798
2780
|
return {};
|
|
1799
2781
|
}
|
|
1800
2782
|
return {
|
|
@@ -1803,7 +2785,7 @@ function parseApiErrorBody(body) {
|
|
|
1803
2785
|
};
|
|
1804
2786
|
}
|
|
1805
2787
|
function ensurePrepareResponse(value) {
|
|
1806
|
-
if (!
|
|
2788
|
+
if (!isRecord3(value) || typeof value.upload_id !== "string") {
|
|
1807
2789
|
throw new TypeError("Mountra prepare response is invalid");
|
|
1808
2790
|
}
|
|
1809
2791
|
if (!Array.isArray(value.upload_targets)) {
|
|
@@ -1812,13 +2794,13 @@ function ensurePrepareResponse(value) {
|
|
|
1812
2794
|
return value;
|
|
1813
2795
|
}
|
|
1814
2796
|
function ensureFinishResponse(value) {
|
|
1815
|
-
if (!
|
|
2797
|
+
if (!isRecord3(value) || typeof value.upload_id !== "string") {
|
|
1816
2798
|
throw new TypeError("Mountra finish response is invalid");
|
|
1817
2799
|
}
|
|
1818
2800
|
return value;
|
|
1819
2801
|
}
|
|
1820
2802
|
function ensureUploadStatusResponse(value) {
|
|
1821
|
-
if (!
|
|
2803
|
+
if (!isRecord3(value) || typeof value.upload_id !== "string") {
|
|
1822
2804
|
throw new TypeError("Mountra upload status response is invalid");
|
|
1823
2805
|
}
|
|
1824
2806
|
if (!Array.isArray(value.chunks)) {
|
|
@@ -1869,6 +2851,7 @@ var MountraClient = class {
|
|
|
1869
2851
|
jumboChunkCheckpointStore;
|
|
1870
2852
|
jumboPartSize;
|
|
1871
2853
|
downloader;
|
|
2854
|
+
metadata;
|
|
1872
2855
|
constructor(options) {
|
|
1873
2856
|
this.baseUrl = normalizeBaseUrl(options.baseUrl);
|
|
1874
2857
|
this.apiPrefix = normalizeApiPrefix(options.apiPrefix ?? "/api");
|
|
@@ -1895,6 +2878,13 @@ var MountraClient = class {
|
|
|
1895
2878
|
cache: options.downloadCache ? new DownloadCache(options.downloadCache) : null,
|
|
1896
2879
|
requestJson: (path, body, signal) => this.requestJson(path, body, signal)
|
|
1897
2880
|
});
|
|
2881
|
+
this.metadata = new MountraMetadata({
|
|
2882
|
+
baseUrl: this.baseUrl,
|
|
2883
|
+
clientSelector: this.clientSelector,
|
|
2884
|
+
cache: options.metadataCache ? new MetadataCache(options.metadataCache) : null,
|
|
2885
|
+
requestJson: (path, body, signal) => this.requestJson(path, body, signal),
|
|
2886
|
+
invalidateDownloads: (selector, paths) => this.downloader.invalidateCachedPaths(selector, paths)
|
|
2887
|
+
});
|
|
1898
2888
|
}
|
|
1899
2889
|
async exportFile(options) {
|
|
1900
2890
|
return this.downloader.exportFile(options);
|
|
@@ -1921,6 +2911,33 @@ var MountraClient = class {
|
|
|
1921
2911
|
getDownloadCacheUsage(selector) {
|
|
1922
2912
|
return this.downloader.cacheUsage(selector);
|
|
1923
2913
|
}
|
|
2914
|
+
/** List the entries of a directory. See `MetadataReadMode` for how `mode` uses the metadata cache. */
|
|
2915
|
+
list(options) {
|
|
2916
|
+
return this.metadata.list(options);
|
|
2917
|
+
}
|
|
2918
|
+
/** Return a directory with its complete subtree. See `MetadataReadMode` for how `mode` uses the metadata cache. */
|
|
2919
|
+
tree(options) {
|
|
2920
|
+
return this.metadata.tree(options);
|
|
2921
|
+
}
|
|
2922
|
+
mkdir(options) {
|
|
2923
|
+
return this.metadata.mkdir(options);
|
|
2924
|
+
}
|
|
2925
|
+
rename(options) {
|
|
2926
|
+
return this.metadata.rename(options);
|
|
2927
|
+
}
|
|
2928
|
+
move(options) {
|
|
2929
|
+
return this.metadata.move(options);
|
|
2930
|
+
}
|
|
2931
|
+
copy(options) {
|
|
2932
|
+
return this.metadata.copy(options);
|
|
2933
|
+
}
|
|
2934
|
+
delete(options) {
|
|
2935
|
+
return this.metadata.delete(options);
|
|
2936
|
+
}
|
|
2937
|
+
/** Clear the metadata cache of one workspace, or of every workspace when none is given. */
|
|
2938
|
+
clearMetadataCache(selector) {
|
|
2939
|
+
return this.metadata.clearCache(selector);
|
|
2940
|
+
}
|
|
1924
2941
|
/** Create a stable resource object for one file in this or another workspace. */
|
|
1925
2942
|
file(locator) {
|
|
1926
2943
|
return new MountraFile(
|
|
@@ -2101,6 +3118,7 @@ var MountraClient = class {
|
|
|
2101
3118
|
if (finished.finish_status !== "completed") {
|
|
2102
3119
|
throw new Error(`Mountra upload did not finish successfully: ${finished.finish_status}`);
|
|
2103
3120
|
}
|
|
3121
|
+
await this.metadata.recordUpload(this.cacheWorkspace(options), finished);
|
|
2104
3122
|
await this.downloader.cacheUploadedFile(
|
|
2105
3123
|
this.cacheWorkspace(options),
|
|
2106
3124
|
{
|
|
@@ -2303,6 +3321,7 @@ var MountraClient = class {
|
|
|
2303
3321
|
})
|
|
2304
3322
|
);
|
|
2305
3323
|
if (finished.finish_status === "completed") {
|
|
3324
|
+
await this.metadata.recordUpload(this.cacheWorkspace(selector), finished);
|
|
2306
3325
|
await this.downloader.invalidateCachedFile(this.cacheWorkspace(selector), {
|
|
2307
3326
|
path: finished.path,
|
|
2308
3327
|
...finished.ino !== void 0 ? { ino: finished.ino } : {}
|
|
@@ -2359,8 +3378,10 @@ function createMountraClient(options) {
|
|
|
2359
3378
|
InMemoryDownloadCacheStore,
|
|
2360
3379
|
InMemoryDownloadCheckpointStore,
|
|
2361
3380
|
InMemoryJumboChunkCheckpointStore,
|
|
3381
|
+
InMemoryMetadataCacheStore,
|
|
2362
3382
|
IndexedDBDownloadCacheStore,
|
|
2363
3383
|
IndexedDBDownloadCheckpointStore,
|
|
3384
|
+
IndexedDBMetadataCacheStore,
|
|
2364
3385
|
JUMBO_CHUNK_THRESHOLD_BYTES,
|
|
2365
3386
|
LocalStorageJumboChunkCheckpointStore,
|
|
2366
3387
|
MAX_S3_MULTIPART_PARTS,
|