sliftutils 1.7.139 → 1.7.141
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/index.d.ts +4 -0
- package/misc/dist/ownIPs.ts.cache +2 -2
- package/package.json +1 -1
- package/security/authorizedKeys/dist/secureSSH.ts.cache +6 -4
- package/security/notifications/dist/setupNotify.ts.cache +6 -4
- package/security/signedFiles/dist/signFilesCli.ts.cache +52 -0
- package/storage/IArchives.d.ts +4 -0
- package/storage/IArchives.ts +9 -0
- package/storage/backblaze.ts +6 -1
- package/storage/dist/IArchives.ts.cache +12 -3
- package/storage/dist/backblaze.ts.cache +7 -2
- package/storage/remoteStorage/createArchives.ts +41 -39
- package/storage/remoteStorage/dist/createArchives.ts.cache +89 -81
|
@@ -3,7 +3,7 @@ import { delay } from "socket-function/src/batching";
|
|
|
3
3
|
import {
|
|
4
4
|
IArchives, RemoteConfig, RemoteConfigBase, SourceConfig,
|
|
5
5
|
ArchiveFileInfo, ArchivesConfig, ArchivesSyncStatus, ChangesAfterConfig, DelConfig, FindConfig, GetConfig, GetInfoConfig, MoveFileConfig, SetConfig, SetLargeFileConfig, STORAGE_WRONG_VALID_WINDOW,
|
|
6
|
-
STORAGE_WRONG_ROUTE, STORAGE_NOT_CONFIGURED, FULL_ROUTE, VARIABLE_SHARD, LARGE_SET_THRESHOLD, bufferChunkStream,
|
|
6
|
+
STORAGE_WRONG_ROUTE, STORAGE_NOT_CONFIGURED, FULL_ROUTE, VARIABLE_SHARD, LARGE_SET_THRESHOLD, bufferChunkStream, validateFileName,
|
|
7
7
|
} from "../IArchives";
|
|
8
8
|
import { copyArchiveFile } from "../archiveHelpers";
|
|
9
9
|
import {
|
|
@@ -83,7 +83,7 @@ function coverRoutes(candidates: SourceWrapper[]): SourceWrapper[] | undefined {
|
|
|
83
83
|
return chosen;
|
|
84
84
|
}
|
|
85
85
|
|
|
86
|
-
/** READS ONLY. Drops sources that recently failed while disconnected - unless that would leave nothing, in which case a down source is still better than no source, and we retry it immediately. Never applies to writes (or
|
|
86
|
+
/** READS ONLY. Drops sources that recently failed while disconnected - unless that would leave nothing, in which case a down source is still better than no source, and we retry it immediately. Never applies to writes (or fallbacks:false reads of the write target): the write node is strictly the FIRST source matching the route and valid window, regardless of connectivity - a client's flaky view of the network must never scatter writes across the chain (spec: client writes are consistent, client reads are redundant). */
|
|
87
87
|
function preferUsable(sources: SourceWrapper[]): SourceWrapper[] {
|
|
88
88
|
let usable = sources.filter(x => !x.isOnCooldown());
|
|
89
89
|
return usable.length && usable || sources;
|
|
@@ -106,12 +106,12 @@ export class ArchivesChain implements IArchives {
|
|
|
106
106
|
return `chain ${urls.join(", ")}`;
|
|
107
107
|
}
|
|
108
108
|
|
|
109
|
-
// The ONE dispatch for every operation
|
|
110
|
-
private async run<T>(state: ChainState, config: { apiOnly?: boolean; write?: boolean; route?: number;
|
|
111
|
-
if (config.fast && config.
|
|
112
|
-
throw new Error(`fast
|
|
109
|
+
// The ONE dispatch for every operation, on ONE flag: fallbacks false -> the primary node only, via runPrimary; fallbacks true -> the shared fallback loop, trying sources in config order (or latency order for fast reads) and moving on whenever one fails. Every caller sets fallbacks unconditionally - reads turn it on unless the caller said noFallbacks, writes turn it off unless the caller said fallbacks. Writes in the loop differ from reads only in calling source.write.
|
|
110
|
+
private async run<T>(state: ChainState, config: { fallbacks: boolean; apiOnly?: boolean; write?: boolean; route?: number; retries?: number; fast?: boolean; timeout?: SmartTimeout }, run: (archives: IArchives, sourceUrl: string) => Promise<T>): Promise<T> {
|
|
111
|
+
if (config.fast && !config.fallbacks) {
|
|
112
|
+
throw new Error(`fast requires fallbacks for ${this.getDebugName()}: without fallbacks only one source (the write node) is considered, so there is no order to speed up`);
|
|
113
113
|
}
|
|
114
|
-
if (
|
|
114
|
+
if (!config.fallbacks) {
|
|
115
115
|
return await this.runPrimary(config, run);
|
|
116
116
|
}
|
|
117
117
|
let retries = config.retries;
|
|
@@ -163,16 +163,11 @@ export class ArchivesChain implements IArchives {
|
|
|
163
163
|
await this.prepareWrongTargetRetry(state, message.includes(STORAGE_WRONG_VALID_WINDOW) && "window" || message.includes(STORAGE_WRONG_ROUTE) && "route" || "unconfigured");
|
|
164
164
|
break;
|
|
165
165
|
}
|
|
166
|
-
// fallbacks means availability above everything: ANY failing source - down, misconfigured, rejecting, mid-switchover - is skipped and the next covering source takes the call. Only every source failing throws (below).
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
if (!source.isConnected()) source.noteFailure();
|
|
170
|
-
errors.push(message);
|
|
171
|
-
continue;
|
|
172
|
-
}
|
|
173
|
-
if (source.isConnected()) throw e;
|
|
174
|
-
source.noteFailure();
|
|
166
|
+
// fallbacks means availability above everything: ANY failing source - down, misconfigured, rejecting, mid-switchover - is skipped and the next covering source takes the call. Only every source failing throws (below).
|
|
167
|
+
console.error(`Source failed for ${this.getDebugName()}, falling back to the next source: ${message}`);
|
|
168
|
+
if (!source.isConnected()) source.noteFailure();
|
|
175
169
|
errors.push(message);
|
|
170
|
+
continue;
|
|
176
171
|
}
|
|
177
172
|
}
|
|
178
173
|
if (wrongTarget) {
|
|
@@ -198,7 +193,7 @@ export class ArchivesChain implements IArchives {
|
|
|
198
193
|
}
|
|
199
194
|
}
|
|
200
195
|
|
|
201
|
-
// Writes and
|
|
196
|
+
// Writes and fallbacks:false reads are the same case: take the authoritative node - strictly the first source matching the route and valid window, whether it is up or down - and use it, never falling back to another node. It's important that writing always accesses the same node everywhere, even if that node is down - otherwise we're just writing into the void, and who knows if the writes will even be accepted, or clobbered, or what; and fallbacks:false reads want the same node precisely because it is the one writes target. A slow call is almost always better than throwing, so a failing primary is retried (the SAME node, re-resolved each attempt since a config refresh can change which source is primary) until the deadline, then throws.
|
|
202
197
|
private async runPrimary<T>(config: { write?: boolean; route?: number; timeout?: SmartTimeout }, run: (archives: IArchives, sourceUrl: string) => Promise<T>): Promise<T> {
|
|
203
198
|
let retriedWrongWindow = false;
|
|
204
199
|
let retriedWrongRoute = false;
|
|
@@ -314,7 +309,7 @@ export class ArchivesChain implements IArchives {
|
|
|
314
309
|
await this.state.refreshActiveConfig();
|
|
315
310
|
}
|
|
316
311
|
|
|
317
|
-
private async request<T>(config: { apiOnly?: boolean; write?: boolean; route?: number;
|
|
312
|
+
private async request<T>(config: { fallbacks: boolean; apiOnly?: boolean; write?: boolean; route?: number; retries?: number; fast?: boolean; timeout?: SmartTimeout }, run: (archives: IArchives, sourceUrl: string) => Promise<T>): Promise<T> {
|
|
318
313
|
let state = await this.state.getState();
|
|
319
314
|
return await this.run(state, config, run);
|
|
320
315
|
}
|
|
@@ -355,11 +350,12 @@ export class ArchivesChain implements IArchives {
|
|
|
355
350
|
}
|
|
356
351
|
/** get2, but trying sources in latency order (fastest first) instead of config order. While this is much faster, it might miss immediate writes: the write node is no longer tried first, so a lagging replica may answer with a slightly older value. Exclusive with noFallbacks (which only considers one source - the write node - so there is no order to speed up); passing both throws. */
|
|
357
352
|
public async getFast(fileName: string, config?: GetConfig): Promise<{ data: Buffer; writeTime: number; size: number; url: string } | { data?: undefined; writeTime?: undefined; size?: undefined; url: string }> {
|
|
353
|
+
validateFileName(fileName, "getFast");
|
|
358
354
|
if (config?.sourceUrl) {
|
|
359
355
|
// A specific source leaves nothing for the latency ordering to decide
|
|
360
356
|
return await this.get2(fileName, config);
|
|
361
357
|
}
|
|
362
|
-
return await this.request({ route: getRoute(fileName),
|
|
358
|
+
return await this.request({ fallbacks: !config?.noFallbacks, route: getRoute(fileName), retries: config?.retries, fast: true, timeout: { path: fileName } }, async (archives, url) => {
|
|
363
359
|
let result = await archives.get2(fileName, config);
|
|
364
360
|
// Empty data is a tombstone, not content - see get2
|
|
365
361
|
if (!result || !result.data || !result.data.length && !config?.includeTombstones && !(config?.range && result.size)) return { url };
|
|
@@ -368,6 +364,7 @@ export class ArchivesChain implements IArchives {
|
|
|
368
364
|
}
|
|
369
365
|
/** Always resolves with a url - the authority that answered. A value that doesn't exist is still an answer FROM a server, so it comes back as { url } with no data (never plain undefined); errors from every source throw instead. */
|
|
370
366
|
public async get2(fileName: string, config?: GetConfig): Promise<{ data: Buffer; writeTime: number; size: number; url: string } | { data?: undefined; writeTime?: undefined; size?: undefined; url: string }> {
|
|
367
|
+
validateFileName(fileName, "get2");
|
|
371
368
|
const sourceUrl = config?.sourceUrl;
|
|
372
369
|
if (sourceUrl) {
|
|
373
370
|
return await this.runOnSource(sourceUrl, async archives => {
|
|
@@ -376,7 +373,7 @@ export class ArchivesChain implements IArchives {
|
|
|
376
373
|
return { data: result.data, writeTime: result.writeTime, size: result.size, url: sourceUrl };
|
|
377
374
|
});
|
|
378
375
|
}
|
|
379
|
-
return await this.request({ route: getRoute(fileName),
|
|
376
|
+
return await this.request({ fallbacks: !config?.noFallbacks, route: getRoute(fileName), retries: config?.retries, timeout: { path: fileName } }, async (archives, url) => {
|
|
380
377
|
let result = await archives.get2(fileName, config);
|
|
381
378
|
// Empty data is a tombstone, not content (unless the caller asked for tombstones) - a ranged read of a REAL file can legitimately be empty though (range past EOF), which the total size distinguishes
|
|
382
379
|
if (!result || !result.data || !result.data.length && !config?.includeTombstones && !(config?.range && result.size)) return { url };
|
|
@@ -384,6 +381,7 @@ export class ArchivesChain implements IArchives {
|
|
|
384
381
|
});
|
|
385
382
|
}
|
|
386
383
|
public async getInfo(fileName: string, config?: GetInfoConfig): Promise<{ writeTime: number; size: number; url: string } | undefined> {
|
|
384
|
+
validateFileName(fileName, "getInfo");
|
|
387
385
|
const sourceUrl = config?.sourceUrl;
|
|
388
386
|
if (sourceUrl) {
|
|
389
387
|
return await this.runOnSource(sourceUrl, async archives => {
|
|
@@ -391,15 +389,15 @@ export class ArchivesChain implements IArchives {
|
|
|
391
389
|
return result && { ...result, url: sourceUrl } || undefined;
|
|
392
390
|
});
|
|
393
391
|
}
|
|
394
|
-
return await this.request({ route: getRoute(fileName),
|
|
392
|
+
return await this.request({ fallbacks: !config?.noFallbacks, route: getRoute(fileName), retries: config?.retries }, async (archives, url) => {
|
|
395
393
|
let result = await archives.getInfo(fileName, config);
|
|
396
394
|
return result && { ...result, url } || undefined;
|
|
397
395
|
});
|
|
398
396
|
}
|
|
399
397
|
|
|
400
398
|
// Without fallbacks: the AUTHORITATIVE covering ONLY - the first source per route in config order (the same node every write and read targets), down or not. It is NEVER excluded and NEVER substituted, so a listing that can't reach its write nodes retries those same nodes until the deadline and then fails, rather than quietly reading second-hand data off a replica or backblaze. exclude/cooldown/substitution apply ONLY when the caller opted into fallbacks.
|
|
401
|
-
private selectCoveringSources(state: ChainState, config
|
|
402
|
-
if (config
|
|
399
|
+
private selectCoveringSources(state: ChainState, config: { fallbacks: boolean; exclude?: Set<SourceWrapper> }): SourceWrapper[] {
|
|
400
|
+
if (config.fallbacks) {
|
|
403
401
|
let candidates = state.sources.filter(x => configWindowCurrent(x.config) && x.api && !config.exclude?.has(x));
|
|
404
402
|
let usable = candidates.filter(x => !x.isOnCooldown());
|
|
405
403
|
let chosen = coverRoutes(usable) || coverRoutes(candidates);
|
|
@@ -414,7 +412,7 @@ export class ArchivesChain implements IArchives {
|
|
|
414
412
|
return chosen;
|
|
415
413
|
}
|
|
416
414
|
|
|
417
|
-
private async runOnCovering<T>(operation: string, run: (archives: IArchives) => Promise<T>, config
|
|
415
|
+
private async runOnCovering<T>(operation: string, run: (archives: IArchives) => Promise<T>, config: { fallbacks: boolean }): Promise<T[]> {
|
|
418
416
|
let startTime = Date.now();
|
|
419
417
|
let deadline = Date.now() + COVERING_RETRY_TIMEOUT;
|
|
420
418
|
// Sources that failed during this call - with fallbacks, the next attempt covers their routes with the next source holding them instead
|
|
@@ -428,7 +426,7 @@ export class ArchivesChain implements IArchives {
|
|
|
428
426
|
let selected: SourceWrapper[] | undefined;
|
|
429
427
|
let coverGap: Error | undefined;
|
|
430
428
|
try {
|
|
431
|
-
selected = this.selectCoveringSources(state, { fallbacks: config
|
|
429
|
+
selected = this.selectCoveringSources(state, { fallbacks: config.fallbacks, exclude: failed });
|
|
432
430
|
} catch (e) {
|
|
433
431
|
coverGap = e as Error;
|
|
434
432
|
}
|
|
@@ -437,10 +435,10 @@ export class ArchivesChain implements IArchives {
|
|
|
437
435
|
let error = new Error(
|
|
438
436
|
`${operation} cannot run: ${coverGap!.message}`
|
|
439
437
|
+ (described ? ` ${allFailures.size} source(s) failed first: ${described}.` : "")
|
|
440
|
-
+ ` Fallbacks = ${
|
|
438
|
+
+ ` Fallbacks = ${config.fallbacks}. Tries = ${tries}, Took ${formatTime(Date.now() - startTime)}`
|
|
441
439
|
);
|
|
442
440
|
// With fallbacks the loop only lands here after every substitute was tried, so waiting will not produce a new source - the caller gets the whole cascade now
|
|
443
|
-
if (config
|
|
441
|
+
if (config.fallbacks && allFailures.size) {
|
|
444
442
|
throw error;
|
|
445
443
|
}
|
|
446
444
|
if (Date.now() >= deadline) {
|
|
@@ -476,10 +474,10 @@ export class ArchivesChain implements IArchives {
|
|
|
476
474
|
allFailures.set(failure.source, failure.error);
|
|
477
475
|
}
|
|
478
476
|
let error = failures.length === 1
|
|
479
|
-
? new Error(`${operation} failed because source ${failures[0].source.getDebugName()} is unavailable: ${failures[0].error.message ?? failures[0].error}. Fallbacks = ${
|
|
480
|
-
: new Error(`${operation} failed because ${failures.length} of the ${covering.length} sources covering this attempt are unavailable: ${failures.map(x => `${x.source.getDebugName()}: ${x.error.message ?? x.error}`).join(" | ")}. Fallbacks = ${
|
|
477
|
+
? new Error(`${operation} failed because source ${failures[0].source.getDebugName()} is unavailable: ${failures[0].error.message ?? failures[0].error}. Fallbacks = ${config.fallbacks}. Tries = ${tries}, Took ${formatTime(Date.now() - startTime)}`)
|
|
478
|
+
: new Error(`${operation} failed because ${failures.length} of the ${covering.length} sources covering this attempt are unavailable: ${failures.map(x => `${x.source.getDebugName()}: ${x.error.message ?? x.error}`).join(" | ")}. Fallbacks = ${config.fallbacks}. Tries = ${tries}, Took ${formatTime(Date.now() - startTime)}`);
|
|
481
479
|
// Substitution comes BEFORE the deadline check: a single connect timeout can eat the entire deadline, and giving up then - without ever trying the substitute that fallbacks exist for - throws exactly when falling back matters most. The loop keeps substituting for as long as a covering set exists; every pass adds its failures to `failed`, so it always terminates at the no-covering branch above, which throws everything collected here.
|
|
482
|
-
if (config
|
|
480
|
+
if (config.fallbacks) {
|
|
483
481
|
console.warn(`(retrying with fallbacks) ${error.message}`);
|
|
484
482
|
continue;
|
|
485
483
|
}
|
|
@@ -496,7 +494,7 @@ export class ArchivesChain implements IArchives {
|
|
|
496
494
|
return (await this.findInfo(prefix, config)).map(x => x.path);
|
|
497
495
|
}
|
|
498
496
|
public async findInfo(prefix: string, config?: FindConfig): Promise<ArchiveFileInfo[]> {
|
|
499
|
-
let results = await this.runOnCovering(`The find of ${JSON.stringify(prefix)}`, archives => archives.findInfo(prefix, config), { fallbacks: config?.fallbacks });
|
|
497
|
+
let results = await this.runOnCovering(`The find of ${JSON.stringify(prefix)}`, archives => archives.findInfo(prefix, config), { fallbacks: !!config?.fallbacks });
|
|
500
498
|
let byPath = new Map<string, ArchiveFileInfo>();
|
|
501
499
|
for (let list of results) {
|
|
502
500
|
for (let file of list) {
|
|
@@ -511,7 +509,7 @@ export class ArchivesChain implements IArchives {
|
|
|
511
509
|
return merged;
|
|
512
510
|
}
|
|
513
511
|
public async getChangesAfter2(config: ChangesAfterConfig): Promise<ArchiveFileInfo[]> {
|
|
514
|
-
let results = await this.runOnCovering(`The changes listing since ${formatDateTimeDetailed(config.time)}`, archives => archives.getChangesAfter2(config));
|
|
512
|
+
let results = await this.runOnCovering(`The changes listing since ${formatDateTimeDetailed(config.time)}`, archives => archives.getChangesAfter2(config), { fallbacks: false });
|
|
515
513
|
let byPath = new Map<string, ArchiveFileInfo>();
|
|
516
514
|
for (let list of results) {
|
|
517
515
|
for (let file of list) {
|
|
@@ -531,7 +529,7 @@ export class ArchivesChain implements IArchives {
|
|
|
531
529
|
throw new Error(`getSyncStatus is not supported: ${archives.getDebugName()} does not implement it`);
|
|
532
530
|
}
|
|
533
531
|
return await archives.getSyncStatus();
|
|
534
|
-
});
|
|
532
|
+
}, { fallbacks: false });
|
|
535
533
|
return {
|
|
536
534
|
allScansComplete: statuses.every(x => x.allScansComplete),
|
|
537
535
|
indexSize: statuses.reduce((sum, x) => sum + x.indexSize, 0),
|
|
@@ -541,7 +539,7 @@ export class ArchivesChain implements IArchives {
|
|
|
541
539
|
public async getConfig(): Promise<ArchivesConfig> {
|
|
542
540
|
let state = await this.state.getState();
|
|
543
541
|
if (!state.sources.some(x => x.api)) return { remoteConfig: state.config };
|
|
544
|
-
let config = await this.run(state, { apiOnly: true }, archives => archives.getConfig());
|
|
542
|
+
let config = await this.run(state, { fallbacks: true, apiOnly: true }, archives => archives.getConfig());
|
|
545
543
|
return { ...config, remoteConfig: state.config };
|
|
546
544
|
}
|
|
547
545
|
public async hasWriteAccess(): Promise<boolean> {
|
|
@@ -554,6 +552,7 @@ export class ArchivesChain implements IArchives {
|
|
|
554
552
|
}
|
|
555
553
|
|
|
556
554
|
public async set(fileName: string, data: Buffer, config?: SetConfig): Promise<string> {
|
|
555
|
+
validateFileName(fileName, "set");
|
|
557
556
|
if (!data.length) {
|
|
558
557
|
throw new Error(`Empty write refused: set was called with an empty buffer for ${JSON.stringify(fileName)}: an empty file IS a deletion in this system and would read back as missing - call del instead`);
|
|
559
558
|
}
|
|
@@ -568,7 +567,7 @@ export class ArchivesChain implements IArchives {
|
|
|
568
567
|
await this.setLargeFile({ path: fileName, ...config, ...bufferChunkStream(data) });
|
|
569
568
|
return fileName;
|
|
570
569
|
}
|
|
571
|
-
await this.request({
|
|
570
|
+
await this.request({ fallbacks: !!config?.fallbacks, write: true, retries: config?.retries, route: getRoute(fileName), timeout: { uploadBytes: data.length, label: `Upload of ${JSON.stringify(fileName)} (${data.length} bytes)` } }, archives => archives.set(fileName, data, config));
|
|
572
571
|
return fileName;
|
|
573
572
|
}
|
|
574
573
|
|
|
@@ -610,14 +609,16 @@ export class ArchivesChain implements IArchives {
|
|
|
610
609
|
return ROUTING_FILE;
|
|
611
610
|
}
|
|
612
611
|
public async del(fileName: string, config?: DelConfig): Promise<void> {
|
|
613
|
-
|
|
612
|
+
validateFileName(fileName, "del");
|
|
613
|
+
await this.request({ fallbacks: !!config?.fallbacks, write: true, retries: config?.retries, route: getRoute(fileName), timeout: { uploadBytes: 0, label: `Deletion of ${JSON.stringify(fileName)}` } }, archives => archives.del(fileName, config));
|
|
614
614
|
}
|
|
615
615
|
|
|
616
616
|
/** See IArchives.undelete: restores a file marked for deletion, dispatched to the write node as SetConfig.undelete (the write node propagates the restore to its peers itself). */
|
|
617
617
|
public async undelete(fileName: string): Promise<void> {
|
|
618
|
+
validateFileName(fileName, "undelete");
|
|
618
619
|
// set refuses empty buffers, and an undelete carries no data - the byte is ignored
|
|
619
620
|
let placeholder = Buffer.from([1]);
|
|
620
|
-
await this.request({ write: true, route: getRoute(fileName), timeout: { uploadBytes: placeholder.length, label: `Undelete of ${JSON.stringify(fileName)}` } }, archives => archives.set(fileName, placeholder, { undelete: true }));
|
|
621
|
+
await this.request({ fallbacks: false, write: true, route: getRoute(fileName), timeout: { uploadBytes: placeholder.length, label: `Undelete of ${JSON.stringify(fileName)}` } }, archives => archives.set(fileName, placeholder, { undelete: true }));
|
|
621
622
|
}
|
|
622
623
|
|
|
623
624
|
/** See IArchives.move. When one node is the write target for BOTH paths, that node moves the file itself - the bytes never come through us - with the same wrong-window/route re-resolution as any write. When the paths route to different shards no single node holds both, so the move degrades to a copy through us plus a delete, CONFIRMED at the destination before the source is touched. No smart timeout on the node-side move: it can be a big file's worth of node-side work, which the upload-sized deadlines would misjudge. */
|
|
@@ -631,7 +632,7 @@ export class ArchivesChain implements IArchives {
|
|
|
631
632
|
let state = await this.state.getState();
|
|
632
633
|
let target = state.sources.find(x => configWindowCurrent(x.config) && routeContains(x.config.route, fromRoute));
|
|
633
634
|
if (target && routeContains(target.config.route, toRoute)) {
|
|
634
|
-
await this.request({ write: true, route: fromRoute }, async archives => {
|
|
635
|
+
await this.request({ fallbacks: false, write: true, route: fromRoute }, async archives => {
|
|
635
636
|
if (!archives.move) {
|
|
636
637
|
throw new Error(`Move is not supported by this source: ${archives.getDebugName()} (moving ${JSON.stringify(config.fromPath)} to ${JSON.stringify(config.toPath)})`);
|
|
637
638
|
}
|
|
@@ -732,6 +733,7 @@ export class ArchivesChain implements IArchives {
|
|
|
732
733
|
|
|
733
734
|
/** A large file is written exactly like a small one - same write node, same wrong-window/route re-resolution, same fallbacks - so a value's SIZE never decides its write semantics (set streams through here past LARGE_SET_THRESHOLD, and a file that grew past it must not suddenly lose the availability its caller asked for). The one difference: every attempt after the first has to rewind the stream, so a config without restartStream gets a single attempt. */
|
|
734
735
|
public async setLargeFile(config: SetLargeFileConfig): Promise<void> {
|
|
736
|
+
validateFileName(config.path, "setLargeFile");
|
|
735
737
|
if (config.path.includes(VARIABLE_SHARD) && parseVariableRoute(config.path) === undefined) {
|
|
736
738
|
throw new Error(`setLargeFile does not support VARIABLE_SHARD keys (there is no way to return the materialized key); write the file with set, or materialize the key yourself. Key: ${JSON.stringify(config.path)}`);
|
|
737
739
|
}
|
|
@@ -742,7 +744,7 @@ export class ArchivesChain implements IArchives {
|
|
|
742
744
|
return;
|
|
743
745
|
}
|
|
744
746
|
let attempt = 0;
|
|
745
|
-
await this.request({
|
|
747
|
+
await this.request({ fallbacks: !!config.fallbacks, write: true, retries: config.retries, route }, async archives => {
|
|
746
748
|
attempt++;
|
|
747
749
|
// The previous attempt consumed some (or all) of the stream, and this source needs the file from its first byte
|
|
748
750
|
if (attempt > 1) await restartStream();
|