@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.js
CHANGED
|
@@ -94,6 +94,9 @@ function matchesLocator(request, locator) {
|
|
|
94
94
|
}
|
|
95
95
|
return String(request.ino) === String(locator.ino) && request.path === void 0;
|
|
96
96
|
}
|
|
97
|
+
function isPathWithin(path, ancestor) {
|
|
98
|
+
return ancestor === "/" || path === ancestor || path.startsWith(`${ancestor}/`);
|
|
99
|
+
}
|
|
97
100
|
function normalizePath(value) {
|
|
98
101
|
if (!value || !value.trim()) {
|
|
99
102
|
throw new TypeError("Mountra file path cannot be empty");
|
|
@@ -124,6 +127,73 @@ function normalizeIno(value) {
|
|
|
124
127
|
return value;
|
|
125
128
|
}
|
|
126
129
|
|
|
130
|
+
// src/idb.ts
|
|
131
|
+
function getIndexedDb() {
|
|
132
|
+
return globalThis.indexedDB ?? null;
|
|
133
|
+
}
|
|
134
|
+
function requestResult(request) {
|
|
135
|
+
return new Promise((resolve, reject) => {
|
|
136
|
+
request.onsuccess = () => resolve(request.result);
|
|
137
|
+
request.onerror = () => reject(request.error ?? new Error("IndexedDB request failed"));
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
async function runTransaction(database, storeNames, mode, body) {
|
|
141
|
+
const transaction = database.transaction(storeNames, mode);
|
|
142
|
+
const done = new Promise((resolve, reject) => {
|
|
143
|
+
transaction.oncomplete = () => resolve();
|
|
144
|
+
transaction.onerror = () => reject(transaction.error ?? new Error("IndexedDB transaction failed"));
|
|
145
|
+
transaction.onabort = () => reject(transaction.error ?? new Error("IndexedDB transaction aborted"));
|
|
146
|
+
});
|
|
147
|
+
done.catch(() => void 0);
|
|
148
|
+
let result;
|
|
149
|
+
try {
|
|
150
|
+
result = await body(transaction);
|
|
151
|
+
} catch (error) {
|
|
152
|
+
try {
|
|
153
|
+
transaction.abort();
|
|
154
|
+
} catch {
|
|
155
|
+
}
|
|
156
|
+
throw error;
|
|
157
|
+
}
|
|
158
|
+
await done;
|
|
159
|
+
return result;
|
|
160
|
+
}
|
|
161
|
+
function workspaceRange(workspace) {
|
|
162
|
+
return IDBKeyRange.bound([workspace], [workspace, []]);
|
|
163
|
+
}
|
|
164
|
+
var CLOCK_KEY = "clock";
|
|
165
|
+
async function nextClock(meta) {
|
|
166
|
+
const clock = (await requestResult(meta.get(CLOCK_KEY)) ?? 0) + 1;
|
|
167
|
+
await requestResult(meta.put(clock, CLOCK_KEY));
|
|
168
|
+
return clock;
|
|
169
|
+
}
|
|
170
|
+
function openDatabase(name, version, upgrade, onClose) {
|
|
171
|
+
const indexedDb = getIndexedDb();
|
|
172
|
+
if (!indexedDb) {
|
|
173
|
+
return Promise.resolve(null);
|
|
174
|
+
}
|
|
175
|
+
return new Promise((resolve) => {
|
|
176
|
+
let request;
|
|
177
|
+
try {
|
|
178
|
+
request = indexedDb.open(name, version);
|
|
179
|
+
} catch {
|
|
180
|
+
resolve(null);
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
request.onupgradeneeded = () => upgrade(request.result);
|
|
184
|
+
request.onsuccess = () => {
|
|
185
|
+
const database = request.result;
|
|
186
|
+
database.onversionchange = () => {
|
|
187
|
+
database.close();
|
|
188
|
+
onClose();
|
|
189
|
+
};
|
|
190
|
+
resolve(database);
|
|
191
|
+
};
|
|
192
|
+
request.onerror = () => resolve(null);
|
|
193
|
+
request.onblocked = () => resolve(null);
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
|
|
127
197
|
// src/cache.ts
|
|
128
198
|
var DEFAULT_MAX_BYTES = 512 * 1024 * 1024;
|
|
129
199
|
var DEFAULT_MAX_CHUNK_BYTES = 128 * 1024 * 1024;
|
|
@@ -149,6 +219,13 @@ var InMemoryDownloadCacheStore = class {
|
|
|
149
219
|
async removeFile(workspace, fileKey) {
|
|
150
220
|
this.files.delete(memoryKey(workspace, fileKey));
|
|
151
221
|
}
|
|
222
|
+
async removeFilesUnder(workspace, path) {
|
|
223
|
+
for (const [key, entry] of this.files) {
|
|
224
|
+
if (entry.workspace === workspace && isPathWithin(entry.file.path, path)) {
|
|
225
|
+
this.files.delete(key);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
}
|
|
152
229
|
async getChunk(workspace, storageKey) {
|
|
153
230
|
const key = memoryKey(workspace, storageKey);
|
|
154
231
|
const entry = this.chunks.get(key);
|
|
@@ -212,48 +289,9 @@ var CHUNK_DATA_STORE = "chunk-data";
|
|
|
212
289
|
var META_STORE = "meta";
|
|
213
290
|
var LRU_INDEX = "lastUsed";
|
|
214
291
|
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
292
|
function sameKey(left, right) {
|
|
223
293
|
return Array.isArray(left) && left[0] === right[0] && left[1] === right[1];
|
|
224
294
|
}
|
|
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
295
|
var IndexedDBDownloadCacheStore = class {
|
|
258
296
|
databaseName;
|
|
259
297
|
fallback = new InMemoryDownloadCacheStore();
|
|
@@ -291,6 +329,23 @@ var IndexedDBDownloadCacheStore = class {
|
|
|
291
329
|
await requestResult(transaction.objectStore(FILES_STORE).delete([workspace, fileKey]));
|
|
292
330
|
});
|
|
293
331
|
}
|
|
332
|
+
async removeFilesUnder(workspace, path) {
|
|
333
|
+
const database = await this.openDatabase();
|
|
334
|
+
if (!database) {
|
|
335
|
+
return this.fallback.removeFilesUnder(workspace, path);
|
|
336
|
+
}
|
|
337
|
+
await runTransaction(database, [FILES_STORE], "readwrite", async (transaction) => {
|
|
338
|
+
const cursorRequest = transaction.objectStore(FILES_STORE).openCursor(workspaceRange(workspace));
|
|
339
|
+
let cursor = await requestResult(cursorRequest);
|
|
340
|
+
while (cursor) {
|
|
341
|
+
if (isPathWithin(cursor.value.path, path)) {
|
|
342
|
+
await requestResult(cursor.delete());
|
|
343
|
+
}
|
|
344
|
+
cursor.continue();
|
|
345
|
+
cursor = await requestResult(cursorRequest);
|
|
346
|
+
}
|
|
347
|
+
});
|
|
348
|
+
}
|
|
294
349
|
async getChunk(workspace, storageKey) {
|
|
295
350
|
const database = await this.openDatabase();
|
|
296
351
|
if (!database) {
|
|
@@ -414,47 +469,27 @@ var IndexedDBDownloadCacheStore = class {
|
|
|
414
469
|
});
|
|
415
470
|
}
|
|
416
471
|
async openDatabase() {
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
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
|
-
});
|
|
472
|
+
this.databasePromise ??= openDatabase(
|
|
473
|
+
this.databaseName,
|
|
474
|
+
CACHE_DATABASE_VERSION,
|
|
475
|
+
(database) => {
|
|
476
|
+
if (!database.objectStoreNames.contains(FILES_STORE)) {
|
|
477
|
+
database.createObjectStore(FILES_STORE);
|
|
478
|
+
}
|
|
479
|
+
if (!database.objectStoreNames.contains(CHUNKS_STORE)) {
|
|
480
|
+
database.createObjectStore(CHUNKS_STORE).createIndex(LRU_INDEX, "lastUsed");
|
|
481
|
+
}
|
|
482
|
+
if (!database.objectStoreNames.contains(CHUNK_DATA_STORE)) {
|
|
483
|
+
database.createObjectStore(CHUNK_DATA_STORE);
|
|
484
|
+
}
|
|
485
|
+
if (!database.objectStoreNames.contains(META_STORE)) {
|
|
486
|
+
database.createObjectStore(META_STORE);
|
|
487
|
+
}
|
|
488
|
+
},
|
|
489
|
+
() => {
|
|
490
|
+
this.databasePromise = void 0;
|
|
456
491
|
}
|
|
457
|
-
|
|
492
|
+
);
|
|
458
493
|
return this.databasePromise;
|
|
459
494
|
}
|
|
460
495
|
};
|
|
@@ -593,6 +628,20 @@ var DownloadCache = class {
|
|
|
593
628
|
fileKeys.map((fileKey) => this.store.removeFile(workspace, fileKey).catch(() => void 0))
|
|
594
629
|
);
|
|
595
630
|
}
|
|
631
|
+
/** Drop the files at or below `paths`, which a write changed; their chunks stay cached. */
|
|
632
|
+
async invalidatePaths(workspace, paths) {
|
|
633
|
+
try {
|
|
634
|
+
if (!this.store.removeFilesUnder) {
|
|
635
|
+
await this.store.clear(workspace);
|
|
636
|
+
return;
|
|
637
|
+
}
|
|
638
|
+
for (const path of paths) {
|
|
639
|
+
await this.store.removeFilesUnder(workspace, path);
|
|
640
|
+
}
|
|
641
|
+
} catch {
|
|
642
|
+
await this.store.clear(workspace).catch(() => void 0);
|
|
643
|
+
}
|
|
644
|
+
}
|
|
596
645
|
clear(workspace) {
|
|
597
646
|
return this.store.clear(workspace);
|
|
598
647
|
}
|
|
@@ -1156,6 +1205,14 @@ var MountraDownloader = class {
|
|
|
1156
1205
|
async invalidateCachedFile(selector, target) {
|
|
1157
1206
|
await this.cachedFileScope(selector, target)?.invalidate();
|
|
1158
1207
|
}
|
|
1208
|
+
/** Drop the cached files at or below paths that a delete, move, or copy changed. */
|
|
1209
|
+
async invalidateCachedPaths(selector, paths) {
|
|
1210
|
+
const cache = this.transport.cache;
|
|
1211
|
+
const workspace = workspaceCacheKey(this.transport.baseUrl, selector);
|
|
1212
|
+
if (cache && workspace !== null) {
|
|
1213
|
+
await cache.invalidatePaths(workspace, paths);
|
|
1214
|
+
}
|
|
1215
|
+
}
|
|
1159
1216
|
/**
|
|
1160
1217
|
* Cache a file this client just uploaded, so downloading it needs no
|
|
1161
1218
|
* network. Falls back to invalidation when any chunk cannot be cached.
|
|
@@ -1538,11 +1595,7 @@ var MountraDownloader = class {
|
|
|
1538
1595
|
}
|
|
1539
1596
|
};
|
|
1540
1597
|
|
|
1541
|
-
// src/
|
|
1542
|
-
var JUMBO_CHUNK_THRESHOLD_BYTES = 128 * 1024 * 1024;
|
|
1543
|
-
var MIN_S3_MULTIPART_PART_SIZE_BYTES = 5 * 1024 * 1024;
|
|
1544
|
-
var DEFAULT_S3_MULTIPART_PART_SIZE_BYTES = 16 * 1024 * 1024;
|
|
1545
|
-
var MAX_S3_MULTIPART_PARTS = 1e4;
|
|
1598
|
+
// src/errors.ts
|
|
1546
1599
|
var MountraApiError = class extends Error {
|
|
1547
1600
|
status;
|
|
1548
1601
|
code;
|
|
@@ -1555,9 +1608,936 @@ var MountraApiError = class extends Error {
|
|
|
1555
1608
|
this.body = input.body;
|
|
1556
1609
|
}
|
|
1557
1610
|
};
|
|
1611
|
+
|
|
1612
|
+
// src/metadata.ts
|
|
1613
|
+
function isRecord2(value) {
|
|
1614
|
+
return typeof value === "object" && value !== null;
|
|
1615
|
+
}
|
|
1616
|
+
function invalidField(field) {
|
|
1617
|
+
return new TypeError(`Mountra metadata response field ${field} is invalid`);
|
|
1618
|
+
}
|
|
1619
|
+
function readString2(value, field) {
|
|
1620
|
+
if (typeof value !== "string") {
|
|
1621
|
+
throw invalidField(field);
|
|
1622
|
+
}
|
|
1623
|
+
return value;
|
|
1624
|
+
}
|
|
1625
|
+
function readCount(value, field) {
|
|
1626
|
+
if (typeof value !== "number" || !Number.isSafeInteger(value) || value < 0) {
|
|
1627
|
+
throw invalidField(field);
|
|
1628
|
+
}
|
|
1629
|
+
return value;
|
|
1630
|
+
}
|
|
1631
|
+
function readBoolean(value, field) {
|
|
1632
|
+
if (typeof value !== "boolean") {
|
|
1633
|
+
throw invalidField(field);
|
|
1634
|
+
}
|
|
1635
|
+
return value;
|
|
1636
|
+
}
|
|
1637
|
+
function optionalInteger(value) {
|
|
1638
|
+
return typeof value === "number" && Number.isSafeInteger(value) ? value : void 0;
|
|
1639
|
+
}
|
|
1640
|
+
function optionalText(value) {
|
|
1641
|
+
return typeof value === "string" && value ? value : void 0;
|
|
1642
|
+
}
|
|
1643
|
+
function withoutUndefined(value) {
|
|
1644
|
+
for (const key of Object.keys(value)) {
|
|
1645
|
+
if (value[key] === void 0) {
|
|
1646
|
+
delete value[key];
|
|
1647
|
+
}
|
|
1648
|
+
}
|
|
1649
|
+
return value;
|
|
1650
|
+
}
|
|
1651
|
+
function readNodeFields(raw, field) {
|
|
1652
|
+
return {
|
|
1653
|
+
name: readString2(raw.name, `${field}.name`),
|
|
1654
|
+
path: normalizePath(readString2(raw.path, `${field}.path`)),
|
|
1655
|
+
ino: readCount(raw.ino, `${field}.ino`),
|
|
1656
|
+
isDirectory: readBoolean(raw.is_directory, `${field}.is_directory`),
|
|
1657
|
+
size: readCount(raw.size, `${field}.size`),
|
|
1658
|
+
mtimeMs: readCount(raw.mtime_ms, `${field}.mtime_ms`),
|
|
1659
|
+
uid: optionalInteger(raw.uid),
|
|
1660
|
+
gid: optionalInteger(raw.gid),
|
|
1661
|
+
uidSubjectId: optionalText(raw.uid_subject_id),
|
|
1662
|
+
uidSubjectType: optionalText(raw.uid_subject_type),
|
|
1663
|
+
gidSubjectId: optionalText(raw.gid_subject_id),
|
|
1664
|
+
gidSubjectType: optionalText(raw.gid_subject_type)
|
|
1665
|
+
};
|
|
1666
|
+
}
|
|
1667
|
+
function readEntry(raw, field) {
|
|
1668
|
+
if (!isRecord2(raw)) {
|
|
1669
|
+
throw invalidField(field);
|
|
1670
|
+
}
|
|
1671
|
+
return withoutUndefined({
|
|
1672
|
+
...readNodeFields(raw, field),
|
|
1673
|
+
atimeMs: readCount(raw.atime_ms, `${field}.atime_ms`),
|
|
1674
|
+
uidDisplayName: optionalText(raw.uid_display_name),
|
|
1675
|
+
uidUsername: optionalText(raw.uid_username),
|
|
1676
|
+
gidDisplayName: optionalText(raw.gid_display_name),
|
|
1677
|
+
gidUsername: optionalText(raw.gid_username)
|
|
1678
|
+
});
|
|
1679
|
+
}
|
|
1680
|
+
function readTreeNode(raw, field) {
|
|
1681
|
+
if (!isRecord2(raw) || !Array.isArray(raw.children)) {
|
|
1682
|
+
throw invalidField(field);
|
|
1683
|
+
}
|
|
1684
|
+
return withoutUndefined({
|
|
1685
|
+
...readNodeFields(raw, field),
|
|
1686
|
+
children: raw.children.map((child, index) => readTreeNode(child, `${field}.children[${index}]`))
|
|
1687
|
+
});
|
|
1688
|
+
}
|
|
1689
|
+
function readListResponse(value) {
|
|
1690
|
+
if (!isRecord2(value) || !Array.isArray(value.entries)) {
|
|
1691
|
+
throw invalidField("entries");
|
|
1692
|
+
}
|
|
1693
|
+
return value.entries.map((entry, index) => readEntry(entry, `entries[${index}]`));
|
|
1694
|
+
}
|
|
1695
|
+
function readOperationResponse(value) {
|
|
1696
|
+
if (!isRecord2(value)) {
|
|
1697
|
+
throw new TypeError("Mountra file operation response is invalid");
|
|
1698
|
+
}
|
|
1699
|
+
return {
|
|
1700
|
+
path: normalizePath(readString2(value.path, "path")),
|
|
1701
|
+
ino: readCount(value.ino, "ino")
|
|
1702
|
+
};
|
|
1703
|
+
}
|
|
1704
|
+
function sameJson(left, right) {
|
|
1705
|
+
if (left === right) {
|
|
1706
|
+
return true;
|
|
1707
|
+
}
|
|
1708
|
+
if (!isRecord2(left) || !isRecord2(right) || Array.isArray(left) !== Array.isArray(right)) {
|
|
1709
|
+
return false;
|
|
1710
|
+
}
|
|
1711
|
+
const leftKeys = Object.keys(left);
|
|
1712
|
+
return leftKeys.length === Object.keys(right).length && leftKeys.every((key) => Object.hasOwn(right, key) && sameJson(left[key], right[key]));
|
|
1713
|
+
}
|
|
1714
|
+
function sameContent(left, right) {
|
|
1715
|
+
return left.kind === "list" && right.kind === "list" ? sameJson(left.entries, right.entries) : left.kind === "tree" && right.kind === "tree" && sameJson(left.root, right.root);
|
|
1716
|
+
}
|
|
1717
|
+
function workspaceBody(selector) {
|
|
1718
|
+
return {
|
|
1719
|
+
...selector.workspaceCuid !== void 0 ? { workspace_cuid: selector.workspaceCuid } : {},
|
|
1720
|
+
...selector.workspaceId !== void 0 ? { workspace_id: selector.workspaceId } : {}
|
|
1721
|
+
};
|
|
1722
|
+
}
|
|
1723
|
+
var MountraMetadata = class {
|
|
1724
|
+
transport;
|
|
1725
|
+
constructor(transport) {
|
|
1726
|
+
this.transport = transport;
|
|
1727
|
+
}
|
|
1728
|
+
list(options = {}) {
|
|
1729
|
+
return this.read("list", options, (record, source) => ({
|
|
1730
|
+
path: record.path,
|
|
1731
|
+
entries: record.entries,
|
|
1732
|
+
source,
|
|
1733
|
+
fetchedAt: record.fetchedAt
|
|
1734
|
+
}));
|
|
1735
|
+
}
|
|
1736
|
+
tree(options = {}) {
|
|
1737
|
+
return this.read("tree", options, (record, source) => ({
|
|
1738
|
+
path: record.path,
|
|
1739
|
+
root: record.root,
|
|
1740
|
+
source,
|
|
1741
|
+
fetchedAt: record.fetchedAt
|
|
1742
|
+
}));
|
|
1743
|
+
}
|
|
1744
|
+
async mkdir(options) {
|
|
1745
|
+
const selector = this.resolveWorkspace(options);
|
|
1746
|
+
const created = await this.operate(selector, "/file/mkdir", { path: normalizePath(options.path) }, options.signal);
|
|
1747
|
+
await this.afterWrite(selector, {
|
|
1748
|
+
type: "create",
|
|
1749
|
+
path: created.path,
|
|
1750
|
+
ino: created.ino,
|
|
1751
|
+
isDirectory: true,
|
|
1752
|
+
size: 0
|
|
1753
|
+
});
|
|
1754
|
+
return created;
|
|
1755
|
+
}
|
|
1756
|
+
async rename(options) {
|
|
1757
|
+
const selector = this.resolveWorkspace(options);
|
|
1758
|
+
const fromPath = normalizePath(options.fromPath);
|
|
1759
|
+
const renamed = await this.operate(
|
|
1760
|
+
selector,
|
|
1761
|
+
"/file/rename",
|
|
1762
|
+
{ from_path: fromPath, to_name: options.toName },
|
|
1763
|
+
options.signal
|
|
1764
|
+
);
|
|
1765
|
+
await this.afterWrite(selector, { type: "move", fromPath, toPath: renamed.path, ino: renamed.ino });
|
|
1766
|
+
return renamed;
|
|
1767
|
+
}
|
|
1768
|
+
async move(options) {
|
|
1769
|
+
const selector = this.resolveWorkspace(options);
|
|
1770
|
+
const fromPath = normalizePath(options.fromPath);
|
|
1771
|
+
const moved = await this.operate(
|
|
1772
|
+
selector,
|
|
1773
|
+
"/file/move",
|
|
1774
|
+
{ from_path: fromPath, to_path: normalizePath(options.toPath) },
|
|
1775
|
+
options.signal
|
|
1776
|
+
);
|
|
1777
|
+
await this.afterWrite(selector, { type: "move", fromPath, toPath: moved.path, ino: moved.ino });
|
|
1778
|
+
return moved;
|
|
1779
|
+
}
|
|
1780
|
+
async copy(options) {
|
|
1781
|
+
const selector = this.resolveWorkspace(options);
|
|
1782
|
+
const fromPath = normalizePath(options.fromPath);
|
|
1783
|
+
const copied = await this.operate(
|
|
1784
|
+
selector,
|
|
1785
|
+
"/file/copy",
|
|
1786
|
+
{ from_path: fromPath, to_path: normalizePath(options.toPath) },
|
|
1787
|
+
options.signal
|
|
1788
|
+
);
|
|
1789
|
+
await this.afterWrite(selector, { type: "copy", fromPath, toPath: copied.path, ino: copied.ino });
|
|
1790
|
+
return copied;
|
|
1791
|
+
}
|
|
1792
|
+
async delete(options) {
|
|
1793
|
+
const selector = this.resolveWorkspace(options);
|
|
1794
|
+
const path = normalizePath(options.path);
|
|
1795
|
+
await this.transport.requestJson("/file/delete", { ...workspaceBody(selector), path }, options.signal);
|
|
1796
|
+
await this.afterWrite(selector, { type: "remove", path });
|
|
1797
|
+
}
|
|
1798
|
+
/** Show a file that an upload just finished. The upload updates the download cache itself. */
|
|
1799
|
+
recordUpload(selector, file) {
|
|
1800
|
+
return this.applyChange(selector, {
|
|
1801
|
+
type: "create",
|
|
1802
|
+
path: normalizePath(file.path),
|
|
1803
|
+
...file.ino !== void 0 ? { ino: file.ino } : {},
|
|
1804
|
+
isDirectory: false,
|
|
1805
|
+
size: file.size
|
|
1806
|
+
});
|
|
1807
|
+
}
|
|
1808
|
+
async clearCache(selector) {
|
|
1809
|
+
const cache = this.transport.cache;
|
|
1810
|
+
if (!cache) {
|
|
1811
|
+
return;
|
|
1812
|
+
}
|
|
1813
|
+
if (selector === void 0) {
|
|
1814
|
+
await cache.clear();
|
|
1815
|
+
return;
|
|
1816
|
+
}
|
|
1817
|
+
const workspace = workspaceCacheKey(this.transport.baseUrl, selector);
|
|
1818
|
+
if (workspace === null) {
|
|
1819
|
+
throw new TypeError("workspaceCuid or workspaceId is required");
|
|
1820
|
+
}
|
|
1821
|
+
await cache.clear(workspace);
|
|
1822
|
+
}
|
|
1823
|
+
async read(kind, options, present) {
|
|
1824
|
+
const selector = this.resolveWorkspace(options);
|
|
1825
|
+
const path = normalizePath(options.path ?? "/");
|
|
1826
|
+
const scope = this.cacheScope(selector);
|
|
1827
|
+
const { signal, onUpdate, onError } = options;
|
|
1828
|
+
const cached = options.mode === "async" && scope ? await scope.cache.get(scope.workspace, kind, path) : null;
|
|
1829
|
+
if (!cached) {
|
|
1830
|
+
return present(await this.fetch(kind, selector, path, signal), "remote");
|
|
1831
|
+
}
|
|
1832
|
+
void this.fetch(kind, selector, path, signal).then(
|
|
1833
|
+
(fresh) => {
|
|
1834
|
+
if (!signal?.aborted && !sameContent(cached, fresh)) {
|
|
1835
|
+
onUpdate?.(present(fresh, "remote"));
|
|
1836
|
+
}
|
|
1837
|
+
},
|
|
1838
|
+
(error) => {
|
|
1839
|
+
if (!signal?.aborted) {
|
|
1840
|
+
onError?.(error);
|
|
1841
|
+
}
|
|
1842
|
+
}
|
|
1843
|
+
);
|
|
1844
|
+
return present(cached, "cache");
|
|
1845
|
+
}
|
|
1846
|
+
/** Fetch from Mountra, and cache the result before returning it. */
|
|
1847
|
+
async fetch(kind, selector, path, signal) {
|
|
1848
|
+
const scope = this.cacheScope(selector);
|
|
1849
|
+
const ticket = scope?.cache.beginFetch(scope.workspace);
|
|
1850
|
+
let body;
|
|
1851
|
+
try {
|
|
1852
|
+
body = await this.transport.requestJson(`/file/${kind}`, { ...workspaceBody(selector), path }, signal);
|
|
1853
|
+
} catch (error) {
|
|
1854
|
+
if (scope && ticket && error instanceof MountraApiError && error.status === 404) {
|
|
1855
|
+
await scope.cache.forgetFetched(scope.workspace, path, ticket);
|
|
1856
|
+
}
|
|
1857
|
+
throw error;
|
|
1858
|
+
}
|
|
1859
|
+
const fetchedAt = ticket?.startedAt ?? Date.now();
|
|
1860
|
+
let record;
|
|
1861
|
+
if (kind === "list") {
|
|
1862
|
+
record = { kind, path, entries: readListResponse(body), fetchedAt };
|
|
1863
|
+
} else {
|
|
1864
|
+
if (!isRecord2(body)) {
|
|
1865
|
+
throw invalidField("root");
|
|
1866
|
+
}
|
|
1867
|
+
record = { kind, path, root: readTreeNode(body.root, "root"), fetchedAt };
|
|
1868
|
+
}
|
|
1869
|
+
if (scope && ticket) {
|
|
1870
|
+
await scope.cache.storeFetched(scope.workspace, record, ticket);
|
|
1871
|
+
}
|
|
1872
|
+
return record;
|
|
1873
|
+
}
|
|
1874
|
+
async operate(selector, endpoint, body, signal) {
|
|
1875
|
+
return readOperationResponse(
|
|
1876
|
+
await this.transport.requestJson(endpoint, { ...workspaceBody(selector), ...body }, signal)
|
|
1877
|
+
);
|
|
1878
|
+
}
|
|
1879
|
+
/**
|
|
1880
|
+
* Reflect a write in both caches. Any download cached at a path the write
|
|
1881
|
+
* changed is stale: the file is gone, moved, or a new one now lives there.
|
|
1882
|
+
*/
|
|
1883
|
+
async afterWrite(selector, change) {
|
|
1884
|
+
const paths = change.type === "move" ? [change.fromPath, change.toPath] : change.type === "copy" ? [change.toPath] : [change.path];
|
|
1885
|
+
await Promise.all([
|
|
1886
|
+
this.applyChange(selector, change),
|
|
1887
|
+
this.transport.invalidateDownloads(selector, paths)
|
|
1888
|
+
]);
|
|
1889
|
+
}
|
|
1890
|
+
async applyChange(selector, change) {
|
|
1891
|
+
const scope = this.cacheScope(selector);
|
|
1892
|
+
await scope?.cache.applyChange(scope.workspace, change);
|
|
1893
|
+
}
|
|
1894
|
+
/** Fall back to the client workspace only when the call names none, as downloads do. */
|
|
1895
|
+
resolveWorkspace(selector) {
|
|
1896
|
+
const named = selector.workspaceCuid !== void 0 || selector.workspaceId !== void 0;
|
|
1897
|
+
const { workspaceCuid, workspaceId } = named ? selector : this.transport.clientSelector;
|
|
1898
|
+
if (workspaceCuid === void 0 && workspaceId === void 0) {
|
|
1899
|
+
throw new TypeError("workspaceCuid or workspaceId is required");
|
|
1900
|
+
}
|
|
1901
|
+
return {
|
|
1902
|
+
...workspaceCuid !== void 0 ? { workspaceCuid } : {},
|
|
1903
|
+
...workspaceId !== void 0 ? { workspaceId } : {}
|
|
1904
|
+
};
|
|
1905
|
+
}
|
|
1906
|
+
cacheScope(selector) {
|
|
1907
|
+
const cache = this.transport.cache;
|
|
1908
|
+
const workspace = workspaceCacheKey(this.transport.baseUrl, selector);
|
|
1909
|
+
return cache && workspace !== null ? { cache, workspace } : null;
|
|
1910
|
+
}
|
|
1911
|
+
};
|
|
1912
|
+
|
|
1913
|
+
// src/metadata-cache.ts
|
|
1914
|
+
var DEFAULT_MAX_ENTRIES = 1e3;
|
|
1915
|
+
var KINDS = ["list", "tree"];
|
|
1916
|
+
function parentOf(path) {
|
|
1917
|
+
const index = path.lastIndexOf("/");
|
|
1918
|
+
return index <= 0 ? "/" : path.slice(0, index);
|
|
1919
|
+
}
|
|
1920
|
+
function baseName(path) {
|
|
1921
|
+
return path.slice(path.lastIndexOf("/") + 1) || "/";
|
|
1922
|
+
}
|
|
1923
|
+
function selfAndAncestors(path) {
|
|
1924
|
+
const paths = [path];
|
|
1925
|
+
for (let current = path; current !== "/"; ) {
|
|
1926
|
+
current = parentOf(current);
|
|
1927
|
+
paths.push(current);
|
|
1928
|
+
}
|
|
1929
|
+
return paths;
|
|
1930
|
+
}
|
|
1931
|
+
function rebasePath(path, from, to) {
|
|
1932
|
+
return `${to}${path.slice(from.length)}`;
|
|
1933
|
+
}
|
|
1934
|
+
function memoryKey2(workspace, kind, path) {
|
|
1935
|
+
return JSON.stringify([workspace, kind, path]);
|
|
1936
|
+
}
|
|
1937
|
+
var InMemoryMetadataCacheStore = class {
|
|
1938
|
+
/** Map iteration order is the eviction order: the first entry was written least recently. */
|
|
1939
|
+
records = /* @__PURE__ */ new Map();
|
|
1940
|
+
async get(workspace, kind, path) {
|
|
1941
|
+
const entry = this.records.get(memoryKey2(workspace, kind, path));
|
|
1942
|
+
return entry ? structuredClone(entry.record) : null;
|
|
1943
|
+
}
|
|
1944
|
+
async getSubtree(workspace, path, kind) {
|
|
1945
|
+
return [...this.records.values()].filter(
|
|
1946
|
+
(entry) => entry.workspace === workspace && (kind === void 0 || entry.record.kind === kind) && isPathWithin(entry.record.path, path)
|
|
1947
|
+
).map((entry) => structuredClone(entry.record));
|
|
1948
|
+
}
|
|
1949
|
+
async write(workspace, batch, maxEntries) {
|
|
1950
|
+
for (const { kind, path } of batch.remove) {
|
|
1951
|
+
this.records.delete(memoryKey2(workspace, kind, path));
|
|
1952
|
+
}
|
|
1953
|
+
for (const record of batch.put) {
|
|
1954
|
+
const key = memoryKey2(workspace, record.kind, record.path);
|
|
1955
|
+
this.records.delete(key);
|
|
1956
|
+
this.records.set(key, { workspace, record: structuredClone(record) });
|
|
1957
|
+
}
|
|
1958
|
+
for (const key of this.records.keys()) {
|
|
1959
|
+
if (this.records.size <= maxEntries) {
|
|
1960
|
+
break;
|
|
1961
|
+
}
|
|
1962
|
+
this.records.delete(key);
|
|
1963
|
+
}
|
|
1964
|
+
}
|
|
1965
|
+
async clear(workspace) {
|
|
1966
|
+
for (const [key, entry] of this.records) {
|
|
1967
|
+
if (workspace === void 0 || entry.workspace === workspace) {
|
|
1968
|
+
this.records.delete(key);
|
|
1969
|
+
}
|
|
1970
|
+
}
|
|
1971
|
+
}
|
|
1972
|
+
};
|
|
1973
|
+
var METADATA_DATABASE_NAME = "mountra-sdk-metadata-cache";
|
|
1974
|
+
var METADATA_DATABASE_VERSION = 1;
|
|
1975
|
+
var RECORDS_STORE = "records";
|
|
1976
|
+
var META_STORE2 = "meta";
|
|
1977
|
+
var WRITE_ORDER_INDEX = "written";
|
|
1978
|
+
var IndexedDBMetadataCacheStore = class {
|
|
1979
|
+
databaseName;
|
|
1980
|
+
fallback = new InMemoryMetadataCacheStore();
|
|
1981
|
+
databasePromise;
|
|
1982
|
+
constructor(options = {}) {
|
|
1983
|
+
this.databaseName = options.databaseName ?? METADATA_DATABASE_NAME;
|
|
1984
|
+
}
|
|
1985
|
+
async get(workspace, kind, path) {
|
|
1986
|
+
const database = await this.openDatabase();
|
|
1987
|
+
if (!database) {
|
|
1988
|
+
return this.fallback.get(workspace, kind, path);
|
|
1989
|
+
}
|
|
1990
|
+
return runTransaction(database, [RECORDS_STORE], "readonly", async (transaction) => {
|
|
1991
|
+
const stored = await requestResult(
|
|
1992
|
+
transaction.objectStore(RECORDS_STORE).get([workspace, kind, path])
|
|
1993
|
+
);
|
|
1994
|
+
return stored?.record ?? null;
|
|
1995
|
+
});
|
|
1996
|
+
}
|
|
1997
|
+
async getSubtree(workspace, path, kind) {
|
|
1998
|
+
const database = await this.openDatabase();
|
|
1999
|
+
if (!database) {
|
|
2000
|
+
return this.fallback.getSubtree(workspace, path, kind);
|
|
2001
|
+
}
|
|
2002
|
+
const upper = path === "/" ? "0" : `${path}0`;
|
|
2003
|
+
return runTransaction(database, [RECORDS_STORE], "readonly", async (transaction) => {
|
|
2004
|
+
const records = [];
|
|
2005
|
+
for (const candidate of kind === void 0 ? KINDS : [kind]) {
|
|
2006
|
+
const stored = await requestResult(
|
|
2007
|
+
transaction.objectStore(RECORDS_STORE).getAll(IDBKeyRange.bound([workspace, candidate, path], [workspace, candidate, upper], false, true))
|
|
2008
|
+
);
|
|
2009
|
+
records.push(
|
|
2010
|
+
...stored.map(({ record }) => record).filter((record) => isPathWithin(record.path, path))
|
|
2011
|
+
);
|
|
2012
|
+
}
|
|
2013
|
+
return records;
|
|
2014
|
+
});
|
|
2015
|
+
}
|
|
2016
|
+
async write(workspace, batch, maxEntries) {
|
|
2017
|
+
const database = await this.openDatabase();
|
|
2018
|
+
if (!database) {
|
|
2019
|
+
return this.fallback.write(workspace, batch, maxEntries);
|
|
2020
|
+
}
|
|
2021
|
+
await runTransaction(database, [RECORDS_STORE, META_STORE2], "readwrite", async (transaction) => {
|
|
2022
|
+
const records = transaction.objectStore(RECORDS_STORE);
|
|
2023
|
+
const meta = transaction.objectStore(META_STORE2);
|
|
2024
|
+
for (const { kind, path } of batch.remove) {
|
|
2025
|
+
await requestResult(records.delete([workspace, kind, path]));
|
|
2026
|
+
}
|
|
2027
|
+
for (const record of batch.put) {
|
|
2028
|
+
const stored = { record, written: await nextClock(meta) };
|
|
2029
|
+
await requestResult(records.put(stored, [workspace, record.kind, record.path]));
|
|
2030
|
+
}
|
|
2031
|
+
let count = await requestResult(records.count());
|
|
2032
|
+
if (count <= maxEntries) {
|
|
2033
|
+
return;
|
|
2034
|
+
}
|
|
2035
|
+
const cursorRequest = records.index(WRITE_ORDER_INDEX).openCursor();
|
|
2036
|
+
let cursor = await requestResult(cursorRequest);
|
|
2037
|
+
while (cursor && count > maxEntries) {
|
|
2038
|
+
await requestResult(cursor.delete());
|
|
2039
|
+
count -= 1;
|
|
2040
|
+
cursor.continue();
|
|
2041
|
+
cursor = await requestResult(cursorRequest);
|
|
2042
|
+
}
|
|
2043
|
+
});
|
|
2044
|
+
}
|
|
2045
|
+
async clear(workspace) {
|
|
2046
|
+
const database = await this.openDatabase();
|
|
2047
|
+
if (!database) {
|
|
2048
|
+
return this.fallback.clear(workspace);
|
|
2049
|
+
}
|
|
2050
|
+
await runTransaction(database, [RECORDS_STORE], "readwrite", async (transaction) => {
|
|
2051
|
+
const records = transaction.objectStore(RECORDS_STORE);
|
|
2052
|
+
await requestResult(
|
|
2053
|
+
workspace === void 0 ? records.clear() : records.delete(workspaceRange(workspace))
|
|
2054
|
+
);
|
|
2055
|
+
});
|
|
2056
|
+
}
|
|
2057
|
+
async openDatabase() {
|
|
2058
|
+
this.databasePromise ??= openDatabase(
|
|
2059
|
+
this.databaseName,
|
|
2060
|
+
METADATA_DATABASE_VERSION,
|
|
2061
|
+
(database) => {
|
|
2062
|
+
if (!database.objectStoreNames.contains(RECORDS_STORE)) {
|
|
2063
|
+
database.createObjectStore(RECORDS_STORE).createIndex(WRITE_ORDER_INDEX, "written");
|
|
2064
|
+
}
|
|
2065
|
+
if (!database.objectStoreNames.contains(META_STORE2)) {
|
|
2066
|
+
database.createObjectStore(META_STORE2);
|
|
2067
|
+
}
|
|
2068
|
+
},
|
|
2069
|
+
() => {
|
|
2070
|
+
this.databasePromise = void 0;
|
|
2071
|
+
}
|
|
2072
|
+
);
|
|
2073
|
+
return this.databasePromise;
|
|
2074
|
+
}
|
|
2075
|
+
};
|
|
2076
|
+
var NODE_FIELDS = [
|
|
2077
|
+
"name",
|
|
2078
|
+
"path",
|
|
2079
|
+
"ino",
|
|
2080
|
+
"isDirectory",
|
|
2081
|
+
"size",
|
|
2082
|
+
"mtimeMs",
|
|
2083
|
+
"uid",
|
|
2084
|
+
"gid",
|
|
2085
|
+
"uidSubjectId",
|
|
2086
|
+
"uidSubjectType",
|
|
2087
|
+
"gidSubjectId",
|
|
2088
|
+
"gidSubjectType"
|
|
2089
|
+
];
|
|
2090
|
+
function pickDefined(source, keys) {
|
|
2091
|
+
const picked = {};
|
|
2092
|
+
for (const key of keys) {
|
|
2093
|
+
if (source[key] !== void 0) {
|
|
2094
|
+
picked[key] = source[key];
|
|
2095
|
+
}
|
|
2096
|
+
}
|
|
2097
|
+
return picked;
|
|
2098
|
+
}
|
|
2099
|
+
function nodeFromEntry(entry, children) {
|
|
2100
|
+
return { ...pickDefined(entry, NODE_FIELDS), children };
|
|
2101
|
+
}
|
|
2102
|
+
function entryFromNode(node, previous) {
|
|
2103
|
+
return {
|
|
2104
|
+
...pickDefined(node, NODE_FIELDS),
|
|
2105
|
+
atimeMs: previous.atimeMs,
|
|
2106
|
+
...node.uid === previous.uid ? pickDefined(previous, ["uidDisplayName", "uidUsername"]) : {},
|
|
2107
|
+
...node.gid === previous.gid ? pickDefined(previous, ["gidDisplayName", "gidUsername"]) : {}
|
|
2108
|
+
};
|
|
2109
|
+
}
|
|
2110
|
+
function findNode(root, path) {
|
|
2111
|
+
let node = root;
|
|
2112
|
+
while (node && node.path !== path) {
|
|
2113
|
+
node = isPathWithin(path, node.path) ? node.children.find((child) => isPathWithin(path, child.path)) : void 0;
|
|
2114
|
+
}
|
|
2115
|
+
return node;
|
|
2116
|
+
}
|
|
2117
|
+
function rebaseNode(node, from, to) {
|
|
2118
|
+
return {
|
|
2119
|
+
...node,
|
|
2120
|
+
...node.path === from ? { name: baseName(to) } : {},
|
|
2121
|
+
path: rebasePath(node.path, from, to),
|
|
2122
|
+
children: node.children.map((child) => rebaseNode(child, from, to))
|
|
2123
|
+
};
|
|
2124
|
+
}
|
|
2125
|
+
function compareChildren(left, right) {
|
|
2126
|
+
return Number(right.isDirectory) - Number(left.isDirectory) || left.name.localeCompare(right.name);
|
|
2127
|
+
}
|
|
2128
|
+
function withChild(children, child) {
|
|
2129
|
+
const result = [...children];
|
|
2130
|
+
const existing = result.findIndex((candidate) => candidate.path === child.path);
|
|
2131
|
+
if (existing >= 0) {
|
|
2132
|
+
result[existing] = child;
|
|
2133
|
+
return result;
|
|
2134
|
+
}
|
|
2135
|
+
const position = result.findIndex((candidate) => compareChildren(child, candidate) < 0);
|
|
2136
|
+
result.splice(position < 0 ? result.length : position, 0, child);
|
|
2137
|
+
return result;
|
|
2138
|
+
}
|
|
2139
|
+
function recordKey(kind, path) {
|
|
2140
|
+
return `${kind}:${path}`;
|
|
2141
|
+
}
|
|
2142
|
+
var MetadataDraft = class {
|
|
2143
|
+
records = /* @__PURE__ */ new Map();
|
|
2144
|
+
changed = /* @__PURE__ */ new Set();
|
|
2145
|
+
removed = /* @__PURE__ */ new Map();
|
|
2146
|
+
constructor(records) {
|
|
2147
|
+
for (const record of records) {
|
|
2148
|
+
this.records.set(recordKey(record.kind, record.path), record);
|
|
2149
|
+
}
|
|
2150
|
+
}
|
|
2151
|
+
list(path) {
|
|
2152
|
+
const record = this.records.get(recordKey("list", path));
|
|
2153
|
+
return record?.kind === "list" ? record : void 0;
|
|
2154
|
+
}
|
|
2155
|
+
tree(path) {
|
|
2156
|
+
const record = this.records.get(recordKey("tree", path));
|
|
2157
|
+
return record?.kind === "tree" ? record : void 0;
|
|
2158
|
+
}
|
|
2159
|
+
/** Loaded trees that hold `path`: those rooted at `path` or at one of its ancestors. */
|
|
2160
|
+
treesContaining(path) {
|
|
2161
|
+
return [...this.records.values()].filter(
|
|
2162
|
+
(record) => record.kind === "tree" && isPathWithin(path, record.path)
|
|
2163
|
+
);
|
|
2164
|
+
}
|
|
2165
|
+
/** Loaded records at `path` or below it. */
|
|
2166
|
+
within(path) {
|
|
2167
|
+
return [...this.records.values()].filter((record) => isPathWithin(record.path, path));
|
|
2168
|
+
}
|
|
2169
|
+
put(record) {
|
|
2170
|
+
const key = recordKey(record.kind, record.path);
|
|
2171
|
+
this.records.set(key, record);
|
|
2172
|
+
this.removed.delete(key);
|
|
2173
|
+
this.changed.add(key);
|
|
2174
|
+
}
|
|
2175
|
+
/** Mark a loaded record that was changed in place. */
|
|
2176
|
+
touch(record) {
|
|
2177
|
+
this.changed.add(recordKey(record.kind, record.path));
|
|
2178
|
+
}
|
|
2179
|
+
remove(record) {
|
|
2180
|
+
const key = recordKey(record.kind, record.path);
|
|
2181
|
+
this.records.delete(key);
|
|
2182
|
+
this.changed.delete(key);
|
|
2183
|
+
this.removed.set(key, { kind: record.kind, path: record.path });
|
|
2184
|
+
}
|
|
2185
|
+
batch() {
|
|
2186
|
+
return {
|
|
2187
|
+
put: [...this.changed].map((key) => this.records.get(key)),
|
|
2188
|
+
remove: [...this.removed.values()]
|
|
2189
|
+
};
|
|
2190
|
+
}
|
|
2191
|
+
findEntry(path) {
|
|
2192
|
+
return this.list(parentOf(path))?.entries.find((entry) => entry.path === path);
|
|
2193
|
+
}
|
|
2194
|
+
/** The node of `path` with its complete subtree, from any loaded tree. */
|
|
2195
|
+
findNode(path) {
|
|
2196
|
+
for (const tree of this.treesContaining(path)) {
|
|
2197
|
+
const node = findNode(tree.root, path);
|
|
2198
|
+
if (node) {
|
|
2199
|
+
return node;
|
|
2200
|
+
}
|
|
2201
|
+
}
|
|
2202
|
+
return void 0;
|
|
2203
|
+
}
|
|
2204
|
+
/** Remove `path` from the listing and the trees that show it. */
|
|
2205
|
+
removeEntry(path) {
|
|
2206
|
+
const parentPath = parentOf(path);
|
|
2207
|
+
const list = this.list(parentPath);
|
|
2208
|
+
if (list?.entries.some((entry) => entry.path === path)) {
|
|
2209
|
+
list.entries = list.entries.filter((entry) => entry.path !== path);
|
|
2210
|
+
this.touch(list);
|
|
2211
|
+
}
|
|
2212
|
+
for (const tree of this.treesContaining(parentPath)) {
|
|
2213
|
+
const parent = findNode(tree.root, parentPath);
|
|
2214
|
+
if (parent?.children.some((child) => child.path === path)) {
|
|
2215
|
+
parent.children = parent.children.filter((child) => child.path !== path);
|
|
2216
|
+
this.touch(tree);
|
|
2217
|
+
}
|
|
2218
|
+
}
|
|
2219
|
+
}
|
|
2220
|
+
/**
|
|
2221
|
+
* Show `path` in its parent's listing and in the trees holding its parent.
|
|
2222
|
+
* A record that cannot show it faithfully, because the entry or the
|
|
2223
|
+
* complete node is unknown, is dropped instead.
|
|
2224
|
+
*/
|
|
2225
|
+
insertEntry(path, entry, node) {
|
|
2226
|
+
const parentPath = parentOf(path);
|
|
2227
|
+
const list = this.list(parentPath);
|
|
2228
|
+
if (list && entry) {
|
|
2229
|
+
list.entries = withChild(list.entries, entry);
|
|
2230
|
+
this.touch(list);
|
|
2231
|
+
} else if (list) {
|
|
2232
|
+
this.remove(list);
|
|
2233
|
+
}
|
|
2234
|
+
for (const tree of this.treesContaining(parentPath)) {
|
|
2235
|
+
const parent = findNode(tree.root, parentPath);
|
|
2236
|
+
if (node && parent?.isDirectory) {
|
|
2237
|
+
parent.children = withChild(parent.children, structuredClone(node));
|
|
2238
|
+
this.touch(tree);
|
|
2239
|
+
} else {
|
|
2240
|
+
this.remove(tree);
|
|
2241
|
+
}
|
|
2242
|
+
}
|
|
2243
|
+
}
|
|
2244
|
+
removeSubtree(path) {
|
|
2245
|
+
for (const record of this.within(path)) {
|
|
2246
|
+
this.remove(record);
|
|
2247
|
+
}
|
|
2248
|
+
}
|
|
2249
|
+
/** Re-key the records at or below `from` to `to`, rewriting the paths they hold. */
|
|
2250
|
+
moveSubtree(from, to) {
|
|
2251
|
+
for (const record of this.within(from)) {
|
|
2252
|
+
this.remove(record);
|
|
2253
|
+
const path = rebasePath(record.path, from, to);
|
|
2254
|
+
this.put(
|
|
2255
|
+
record.kind === "list" ? {
|
|
2256
|
+
...record,
|
|
2257
|
+
path,
|
|
2258
|
+
entries: record.entries.map((entry) => ({ ...entry, path: rebasePath(entry.path, from, to) }))
|
|
2259
|
+
} : { ...record, path, root: rebaseNode(record.root, from, to) }
|
|
2260
|
+
);
|
|
2261
|
+
}
|
|
2262
|
+
}
|
|
2263
|
+
};
|
|
2264
|
+
function changeScope(change) {
|
|
2265
|
+
const paths = change.type === "move" || change.type === "copy" ? [change.fromPath, change.toPath] : [change.path];
|
|
2266
|
+
return {
|
|
2267
|
+
lists: paths.map(parentOf),
|
|
2268
|
+
trees: paths.flatMap((path) => selfAndAncestors(parentOf(path))),
|
|
2269
|
+
// A copy keeps its source; any other change makes records below its paths stale or moves them.
|
|
2270
|
+
subtrees: change.type === "copy" ? [change.toPath] : paths
|
|
2271
|
+
};
|
|
2272
|
+
}
|
|
2273
|
+
function patchChange(draft, change, now) {
|
|
2274
|
+
switch (change.type) {
|
|
2275
|
+
case "create": {
|
|
2276
|
+
const previousEntry = draft.findEntry(change.path);
|
|
2277
|
+
const previousNode = draft.findNode(change.path);
|
|
2278
|
+
draft.removeSubtree(change.path);
|
|
2279
|
+
if (change.ino === void 0) {
|
|
2280
|
+
draft.insertEntry(change.path, null, null);
|
|
2281
|
+
return;
|
|
2282
|
+
}
|
|
2283
|
+
const fields = {
|
|
2284
|
+
name: baseName(change.path),
|
|
2285
|
+
path: change.path,
|
|
2286
|
+
ino: change.ino,
|
|
2287
|
+
isDirectory: change.isDirectory,
|
|
2288
|
+
size: change.size,
|
|
2289
|
+
mtimeMs: now
|
|
2290
|
+
};
|
|
2291
|
+
const sameKind = (previous) => previous?.isDirectory === change.isDirectory;
|
|
2292
|
+
draft.insertEntry(
|
|
2293
|
+
change.path,
|
|
2294
|
+
{ atimeMs: now, ...sameKind(previousEntry) ? previousEntry : {}, ...fields },
|
|
2295
|
+
{ ...sameKind(previousNode) ? previousNode : {}, ...fields, children: [] }
|
|
2296
|
+
);
|
|
2297
|
+
return;
|
|
2298
|
+
}
|
|
2299
|
+
case "remove":
|
|
2300
|
+
draft.removeEntry(change.path);
|
|
2301
|
+
draft.removeSubtree(change.path);
|
|
2302
|
+
return;
|
|
2303
|
+
case "move": {
|
|
2304
|
+
const { fromPath, toPath } = change;
|
|
2305
|
+
if (fromPath === toPath) {
|
|
2306
|
+
return;
|
|
2307
|
+
}
|
|
2308
|
+
const entry = draft.findEntry(fromPath);
|
|
2309
|
+
const node = draft.findNode(fromPath);
|
|
2310
|
+
draft.removeEntry(fromPath);
|
|
2311
|
+
draft.removeSubtree(toPath);
|
|
2312
|
+
draft.moveSubtree(fromPath, toPath);
|
|
2313
|
+
const fields = { name: baseName(toPath), path: toPath, ino: change.ino };
|
|
2314
|
+
const movedEntry = entry ? { ...entry, ...fields } : null;
|
|
2315
|
+
draft.insertEntry(
|
|
2316
|
+
toPath,
|
|
2317
|
+
movedEntry,
|
|
2318
|
+
node ? { ...rebaseNode(node, fromPath, toPath), ino: change.ino } : movedEntry && !movedEntry.isDirectory ? nodeFromEntry(movedEntry, []) : null
|
|
2319
|
+
);
|
|
2320
|
+
return;
|
|
2321
|
+
}
|
|
2322
|
+
case "copy": {
|
|
2323
|
+
const source = draft.findEntry(change.fromPath) ?? draft.findNode(change.fromPath);
|
|
2324
|
+
draft.removeSubtree(change.toPath);
|
|
2325
|
+
if (!source || source.isDirectory) {
|
|
2326
|
+
draft.insertEntry(change.toPath, null, null);
|
|
2327
|
+
return;
|
|
2328
|
+
}
|
|
2329
|
+
const fields = {
|
|
2330
|
+
name: baseName(change.toPath),
|
|
2331
|
+
path: change.toPath,
|
|
2332
|
+
ino: change.ino,
|
|
2333
|
+
isDirectory: false,
|
|
2334
|
+
size: source.size,
|
|
2335
|
+
mtimeMs: now
|
|
2336
|
+
};
|
|
2337
|
+
draft.insertEntry(change.toPath, { ...fields, atimeMs: now }, { ...fields, children: [] });
|
|
2338
|
+
return;
|
|
2339
|
+
}
|
|
2340
|
+
}
|
|
2341
|
+
}
|
|
2342
|
+
function childrenFromEntries(directory, entries) {
|
|
2343
|
+
const children = [];
|
|
2344
|
+
for (const entry of entries) {
|
|
2345
|
+
const previous = directory.children.find((child) => child.path === entry.path);
|
|
2346
|
+
if (!entry.isDirectory) {
|
|
2347
|
+
children.push(nodeFromEntry(entry, []));
|
|
2348
|
+
} else if (previous?.isDirectory && previous.ino === entry.ino) {
|
|
2349
|
+
children.push(nodeFromEntry(entry, previous.children));
|
|
2350
|
+
} else {
|
|
2351
|
+
return null;
|
|
2352
|
+
}
|
|
2353
|
+
}
|
|
2354
|
+
return children;
|
|
2355
|
+
}
|
|
2356
|
+
function entriesFromNodes(entries, nodes) {
|
|
2357
|
+
const previous = new Map(entries.map((entry) => [entry.path, entry]));
|
|
2358
|
+
const fresh = new Map(nodes.map((node) => [node.path, node]));
|
|
2359
|
+
for (const node of nodes) {
|
|
2360
|
+
const entry = previous.get(node.path);
|
|
2361
|
+
if (!entry || entry.ino !== node.ino || entry.isDirectory !== node.isDirectory) {
|
|
2362
|
+
return null;
|
|
2363
|
+
}
|
|
2364
|
+
}
|
|
2365
|
+
return entries.filter((entry) => fresh.has(entry.path)).map((entry) => entryFromNode(fresh.get(entry.path), entry));
|
|
2366
|
+
}
|
|
2367
|
+
function patchFetchedList(draft, record) {
|
|
2368
|
+
if ((draft.list(record.path)?.fetchedAt ?? 0) > record.fetchedAt) {
|
|
2369
|
+
return;
|
|
2370
|
+
}
|
|
2371
|
+
draft.put(record);
|
|
2372
|
+
for (const tree of draft.treesContaining(record.path)) {
|
|
2373
|
+
if (tree.fetchedAt > record.fetchedAt) {
|
|
2374
|
+
continue;
|
|
2375
|
+
}
|
|
2376
|
+
const directory = findNode(tree.root, record.path);
|
|
2377
|
+
const children = directory?.isDirectory ? childrenFromEntries(directory, record.entries) : null;
|
|
2378
|
+
if (directory && children) {
|
|
2379
|
+
directory.children = children;
|
|
2380
|
+
draft.touch(tree);
|
|
2381
|
+
} else {
|
|
2382
|
+
draft.remove(tree);
|
|
2383
|
+
}
|
|
2384
|
+
}
|
|
2385
|
+
}
|
|
2386
|
+
function patchFetchedTree(draft, record) {
|
|
2387
|
+
if ((draft.tree(record.path)?.fetchedAt ?? 0) > record.fetchedAt) {
|
|
2388
|
+
return;
|
|
2389
|
+
}
|
|
2390
|
+
draft.put(record);
|
|
2391
|
+
for (const other of draft.within(record.path)) {
|
|
2392
|
+
if (other === record || other.fetchedAt > record.fetchedAt) {
|
|
2393
|
+
continue;
|
|
2394
|
+
}
|
|
2395
|
+
const node = findNode(record.root, other.path);
|
|
2396
|
+
if (other.kind === "tree") {
|
|
2397
|
+
if (node?.isDirectory) {
|
|
2398
|
+
draft.put({ ...other, root: structuredClone(node), fetchedAt: record.fetchedAt });
|
|
2399
|
+
} else {
|
|
2400
|
+
draft.remove(other);
|
|
2401
|
+
}
|
|
2402
|
+
continue;
|
|
2403
|
+
}
|
|
2404
|
+
const entries = node?.isDirectory ? entriesFromNodes(other.entries, node.children) : null;
|
|
2405
|
+
if (entries) {
|
|
2406
|
+
other.entries = entries;
|
|
2407
|
+
draft.touch(other);
|
|
2408
|
+
} else {
|
|
2409
|
+
draft.remove(other);
|
|
2410
|
+
}
|
|
2411
|
+
}
|
|
2412
|
+
if (record.path === "/") {
|
|
2413
|
+
return;
|
|
2414
|
+
}
|
|
2415
|
+
const parentPath = parentOf(record.path);
|
|
2416
|
+
for (const tree of draft.treesContaining(parentPath)) {
|
|
2417
|
+
if (tree.fetchedAt > record.fetchedAt) {
|
|
2418
|
+
continue;
|
|
2419
|
+
}
|
|
2420
|
+
const parent = findNode(tree.root, parentPath);
|
|
2421
|
+
if (parent?.isDirectory) {
|
|
2422
|
+
parent.children = withChild(parent.children, structuredClone(record.root));
|
|
2423
|
+
draft.touch(tree);
|
|
2424
|
+
} else {
|
|
2425
|
+
draft.remove(tree);
|
|
2426
|
+
}
|
|
2427
|
+
}
|
|
2428
|
+
}
|
|
2429
|
+
var MetadataCache = class {
|
|
2430
|
+
maxEntries;
|
|
2431
|
+
store;
|
|
2432
|
+
/** Per workspace, how many writes this client has applied. */
|
|
2433
|
+
writeSeqs = /* @__PURE__ */ new Map();
|
|
2434
|
+
/** Per workspace, the tail of the queue that serializes cache updates. */
|
|
2435
|
+
queues = /* @__PURE__ */ new Map();
|
|
2436
|
+
constructor(options) {
|
|
2437
|
+
this.maxEntries = options.maxEntries ?? DEFAULT_MAX_ENTRIES;
|
|
2438
|
+
if (!Number.isSafeInteger(this.maxEntries) || this.maxEntries <= 0) {
|
|
2439
|
+
throw new RangeError("metadataCache.maxEntries must be a positive safe integer");
|
|
2440
|
+
}
|
|
2441
|
+
this.store = options.store ?? new IndexedDBMetadataCacheStore();
|
|
2442
|
+
}
|
|
2443
|
+
async get(workspace, kind, path) {
|
|
2444
|
+
try {
|
|
2445
|
+
const record = await this.store.get(workspace, kind, path);
|
|
2446
|
+
return record?.kind === kind && record.path === path ? record : null;
|
|
2447
|
+
} catch {
|
|
2448
|
+
return null;
|
|
2449
|
+
}
|
|
2450
|
+
}
|
|
2451
|
+
beginFetch(workspace) {
|
|
2452
|
+
return { startedAt: Date.now(), writeSeq: this.writeSeqs.get(workspace) ?? 0 };
|
|
2453
|
+
}
|
|
2454
|
+
/** Cache a fetched result, and bring the records it overlaps up to date with it. */
|
|
2455
|
+
async storeFetched(workspace, record, ticket) {
|
|
2456
|
+
await this.update(workspace, ticket, async () => {
|
|
2457
|
+
if (record.kind === "list") {
|
|
2458
|
+
const draft2 = await this.load(workspace, { lists: [record.path], trees: selfAndAncestors(record.path) });
|
|
2459
|
+
patchFetchedList(draft2, record);
|
|
2460
|
+
return draft2;
|
|
2461
|
+
}
|
|
2462
|
+
const draft = await this.load(workspace, { trees: selfAndAncestors(record.path), subtrees: [record.path] });
|
|
2463
|
+
patchFetchedTree(draft, record);
|
|
2464
|
+
return draft;
|
|
2465
|
+
}).catch(() => void 0);
|
|
2466
|
+
}
|
|
2467
|
+
/** Mountra reported that `path` does not exist. */
|
|
2468
|
+
async forgetFetched(workspace, path, ticket) {
|
|
2469
|
+
const change = { type: "remove", path };
|
|
2470
|
+
await this.update(workspace, ticket, async () => {
|
|
2471
|
+
const draft = await this.load(workspace, changeScope(change));
|
|
2472
|
+
patchChange(draft, change, Date.now());
|
|
2473
|
+
return draft;
|
|
2474
|
+
}).catch(() => void 0);
|
|
2475
|
+
}
|
|
2476
|
+
/** Reflect a write that Mountra completed. */
|
|
2477
|
+
async applyChange(workspace, change) {
|
|
2478
|
+
this.writeSeqs.set(workspace, (this.writeSeqs.get(workspace) ?? 0) + 1);
|
|
2479
|
+
await this.update(workspace, null, async () => {
|
|
2480
|
+
const draft = await this.load(workspace, changeScope(change));
|
|
2481
|
+
patchChange(draft, change, Date.now());
|
|
2482
|
+
return draft;
|
|
2483
|
+
}).catch(
|
|
2484
|
+
() => (
|
|
2485
|
+
// A write that cannot be reflected must not leave records that contradict it.
|
|
2486
|
+
this.store.clear(workspace).catch(() => void 0)
|
|
2487
|
+
)
|
|
2488
|
+
);
|
|
2489
|
+
}
|
|
2490
|
+
clear(workspace) {
|
|
2491
|
+
return this.store.clear(workspace);
|
|
2492
|
+
}
|
|
2493
|
+
/**
|
|
2494
|
+
* Run updates of one workspace one at a time. An update for a fetch is
|
|
2495
|
+
* dropped when this client completed a write since the fetch started: the
|
|
2496
|
+
* result may predate that write.
|
|
2497
|
+
*/
|
|
2498
|
+
update(workspace, ticket, build) {
|
|
2499
|
+
const run = async () => {
|
|
2500
|
+
if (ticket && ticket.writeSeq !== (this.writeSeqs.get(workspace) ?? 0)) {
|
|
2501
|
+
return;
|
|
2502
|
+
}
|
|
2503
|
+
const batch = (await build()).batch();
|
|
2504
|
+
if (batch.put.length > 0 || batch.remove.length > 0) {
|
|
2505
|
+
await this.store.write(workspace, batch, this.maxEntries);
|
|
2506
|
+
}
|
|
2507
|
+
};
|
|
2508
|
+
const result = (this.queues.get(workspace) ?? Promise.resolve()).then(run);
|
|
2509
|
+
const tail = result.catch(() => void 0);
|
|
2510
|
+
this.queues.set(workspace, tail);
|
|
2511
|
+
void tail.then(() => {
|
|
2512
|
+
if (this.queues.get(workspace) === tail) {
|
|
2513
|
+
this.queues.delete(workspace);
|
|
2514
|
+
}
|
|
2515
|
+
});
|
|
2516
|
+
return result;
|
|
2517
|
+
}
|
|
2518
|
+
async load(workspace, scope) {
|
|
2519
|
+
const [records, subtrees] = await Promise.all([
|
|
2520
|
+
Promise.all([
|
|
2521
|
+
...[...new Set(scope.lists)].map((path) => this.store.get(workspace, "list", path)),
|
|
2522
|
+
...[...new Set(scope.trees)].map((path) => this.store.get(workspace, "tree", path))
|
|
2523
|
+
]),
|
|
2524
|
+
Promise.all([...new Set(scope.subtrees)].map((path) => this.store.getSubtree(workspace, path)))
|
|
2525
|
+
]);
|
|
2526
|
+
return new MetadataDraft([
|
|
2527
|
+
...records.filter((record) => record !== null),
|
|
2528
|
+
...subtrees.flat()
|
|
2529
|
+
]);
|
|
2530
|
+
}
|
|
2531
|
+
};
|
|
2532
|
+
|
|
2533
|
+
// src/upload.ts
|
|
2534
|
+
var JUMBO_CHUNK_THRESHOLD_BYTES = 128 * 1024 * 1024;
|
|
2535
|
+
var MIN_S3_MULTIPART_PART_SIZE_BYTES = 5 * 1024 * 1024;
|
|
2536
|
+
var DEFAULT_S3_MULTIPART_PART_SIZE_BYTES = 16 * 1024 * 1024;
|
|
2537
|
+
var MAX_S3_MULTIPART_PARTS = 1e4;
|
|
1558
2538
|
var memoryCheckpointRecords = /* @__PURE__ */ new Map();
|
|
1559
2539
|
function parseJumboCheckpoint(value) {
|
|
1560
|
-
if (!
|
|
2540
|
+
if (!isRecord3(value) || value.version !== 1 || typeof value.storageKey !== "string") {
|
|
1561
2541
|
return null;
|
|
1562
2542
|
}
|
|
1563
2543
|
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)) {
|
|
@@ -1565,7 +2545,7 @@ function parseJumboCheckpoint(value) {
|
|
|
1565
2545
|
}
|
|
1566
2546
|
const parts = [];
|
|
1567
2547
|
for (const part of value.parts) {
|
|
1568
|
-
if (!
|
|
2548
|
+
if (!isRecord3(part)) {
|
|
1569
2549
|
return null;
|
|
1570
2550
|
}
|
|
1571
2551
|
if (typeof part.partNumber !== "number" || typeof part.etag !== "string" || typeof part.size !== "number" || typeof part.sha256 !== "string") {
|
|
@@ -1744,11 +2724,11 @@ function throwIfAborted2(signal) {
|
|
|
1744
2724
|
}
|
|
1745
2725
|
throw new DOMException("The upload was aborted", "AbortError");
|
|
1746
2726
|
}
|
|
1747
|
-
function
|
|
2727
|
+
function isRecord3(value) {
|
|
1748
2728
|
return typeof value === "object" && value !== null;
|
|
1749
2729
|
}
|
|
1750
2730
|
function parseApiErrorBody(body) {
|
|
1751
|
-
if (!
|
|
2731
|
+
if (!isRecord3(body)) {
|
|
1752
2732
|
return {};
|
|
1753
2733
|
}
|
|
1754
2734
|
return {
|
|
@@ -1757,7 +2737,7 @@ function parseApiErrorBody(body) {
|
|
|
1757
2737
|
};
|
|
1758
2738
|
}
|
|
1759
2739
|
function ensurePrepareResponse(value) {
|
|
1760
|
-
if (!
|
|
2740
|
+
if (!isRecord3(value) || typeof value.upload_id !== "string") {
|
|
1761
2741
|
throw new TypeError("Mountra prepare response is invalid");
|
|
1762
2742
|
}
|
|
1763
2743
|
if (!Array.isArray(value.upload_targets)) {
|
|
@@ -1766,13 +2746,13 @@ function ensurePrepareResponse(value) {
|
|
|
1766
2746
|
return value;
|
|
1767
2747
|
}
|
|
1768
2748
|
function ensureFinishResponse(value) {
|
|
1769
|
-
if (!
|
|
2749
|
+
if (!isRecord3(value) || typeof value.upload_id !== "string") {
|
|
1770
2750
|
throw new TypeError("Mountra finish response is invalid");
|
|
1771
2751
|
}
|
|
1772
2752
|
return value;
|
|
1773
2753
|
}
|
|
1774
2754
|
function ensureUploadStatusResponse(value) {
|
|
1775
|
-
if (!
|
|
2755
|
+
if (!isRecord3(value) || typeof value.upload_id !== "string") {
|
|
1776
2756
|
throw new TypeError("Mountra upload status response is invalid");
|
|
1777
2757
|
}
|
|
1778
2758
|
if (!Array.isArray(value.chunks)) {
|
|
@@ -1823,6 +2803,7 @@ var MountraClient = class {
|
|
|
1823
2803
|
jumboChunkCheckpointStore;
|
|
1824
2804
|
jumboPartSize;
|
|
1825
2805
|
downloader;
|
|
2806
|
+
metadata;
|
|
1826
2807
|
constructor(options) {
|
|
1827
2808
|
this.baseUrl = normalizeBaseUrl(options.baseUrl);
|
|
1828
2809
|
this.apiPrefix = normalizeApiPrefix(options.apiPrefix ?? "/api");
|
|
@@ -1849,6 +2830,13 @@ var MountraClient = class {
|
|
|
1849
2830
|
cache: options.downloadCache ? new DownloadCache(options.downloadCache) : null,
|
|
1850
2831
|
requestJson: (path, body, signal) => this.requestJson(path, body, signal)
|
|
1851
2832
|
});
|
|
2833
|
+
this.metadata = new MountraMetadata({
|
|
2834
|
+
baseUrl: this.baseUrl,
|
|
2835
|
+
clientSelector: this.clientSelector,
|
|
2836
|
+
cache: options.metadataCache ? new MetadataCache(options.metadataCache) : null,
|
|
2837
|
+
requestJson: (path, body, signal) => this.requestJson(path, body, signal),
|
|
2838
|
+
invalidateDownloads: (selector, paths) => this.downloader.invalidateCachedPaths(selector, paths)
|
|
2839
|
+
});
|
|
1852
2840
|
}
|
|
1853
2841
|
async exportFile(options) {
|
|
1854
2842
|
return this.downloader.exportFile(options);
|
|
@@ -1875,6 +2863,33 @@ var MountraClient = class {
|
|
|
1875
2863
|
getDownloadCacheUsage(selector) {
|
|
1876
2864
|
return this.downloader.cacheUsage(selector);
|
|
1877
2865
|
}
|
|
2866
|
+
/** List the entries of a directory. See `MetadataReadMode` for how `mode` uses the metadata cache. */
|
|
2867
|
+
list(options) {
|
|
2868
|
+
return this.metadata.list(options);
|
|
2869
|
+
}
|
|
2870
|
+
/** Return a directory with its complete subtree. See `MetadataReadMode` for how `mode` uses the metadata cache. */
|
|
2871
|
+
tree(options) {
|
|
2872
|
+
return this.metadata.tree(options);
|
|
2873
|
+
}
|
|
2874
|
+
mkdir(options) {
|
|
2875
|
+
return this.metadata.mkdir(options);
|
|
2876
|
+
}
|
|
2877
|
+
rename(options) {
|
|
2878
|
+
return this.metadata.rename(options);
|
|
2879
|
+
}
|
|
2880
|
+
move(options) {
|
|
2881
|
+
return this.metadata.move(options);
|
|
2882
|
+
}
|
|
2883
|
+
copy(options) {
|
|
2884
|
+
return this.metadata.copy(options);
|
|
2885
|
+
}
|
|
2886
|
+
delete(options) {
|
|
2887
|
+
return this.metadata.delete(options);
|
|
2888
|
+
}
|
|
2889
|
+
/** Clear the metadata cache of one workspace, or of every workspace when none is given. */
|
|
2890
|
+
clearMetadataCache(selector) {
|
|
2891
|
+
return this.metadata.clearCache(selector);
|
|
2892
|
+
}
|
|
1878
2893
|
/** Create a stable resource object for one file in this or another workspace. */
|
|
1879
2894
|
file(locator) {
|
|
1880
2895
|
return new MountraFile(
|
|
@@ -2055,6 +3070,7 @@ var MountraClient = class {
|
|
|
2055
3070
|
if (finished.finish_status !== "completed") {
|
|
2056
3071
|
throw new Error(`Mountra upload did not finish successfully: ${finished.finish_status}`);
|
|
2057
3072
|
}
|
|
3073
|
+
await this.metadata.recordUpload(this.cacheWorkspace(options), finished);
|
|
2058
3074
|
await this.downloader.cacheUploadedFile(
|
|
2059
3075
|
this.cacheWorkspace(options),
|
|
2060
3076
|
{
|
|
@@ -2257,6 +3273,7 @@ var MountraClient = class {
|
|
|
2257
3273
|
})
|
|
2258
3274
|
);
|
|
2259
3275
|
if (finished.finish_status === "completed") {
|
|
3276
|
+
await this.metadata.recordUpload(this.cacheWorkspace(selector), finished);
|
|
2260
3277
|
await this.downloader.invalidateCachedFile(this.cacheWorkspace(selector), {
|
|
2261
3278
|
path: finished.path,
|
|
2262
3279
|
...finished.ino !== void 0 ? { ino: finished.ino } : {}
|
|
@@ -2312,8 +3329,10 @@ export {
|
|
|
2312
3329
|
InMemoryDownloadCacheStore,
|
|
2313
3330
|
InMemoryDownloadCheckpointStore,
|
|
2314
3331
|
InMemoryJumboChunkCheckpointStore,
|
|
3332
|
+
InMemoryMetadataCacheStore,
|
|
2315
3333
|
IndexedDBDownloadCacheStore,
|
|
2316
3334
|
IndexedDBDownloadCheckpointStore,
|
|
3335
|
+
IndexedDBMetadataCacheStore,
|
|
2317
3336
|
JUMBO_CHUNK_THRESHOLD_BYTES,
|
|
2318
3337
|
LocalStorageJumboChunkCheckpointStore,
|
|
2319
3338
|
MAX_S3_MULTIPART_PARTS,
|