sliftutils 1.7.117 → 1.7.119

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 CHANGED
@@ -4217,7 +4217,8 @@ declare module "sliftutils/storage/remoteStorage/remoteConfig" {
4217
4217
  export declare function sourceIdentity(sourceConfig: SourceConfig | undefined): string;
4218
4218
  /** What an index entry records as the holder of its bytes (see ArchivesSource.url), so it must name the endpoint FOREVER. An intermediate is a switchover's temporary alternate port onto another source, and that port is gone for good once its window passes - so it is recorded as the source it was split out of, which holds the same bucket and outlives it. */
4219
4219
  export declare function sourcePersistentUrl(sourceConfig: SourceConfig | undefined, folder: string): string;
4220
- export declare function parseRoutingData(data: Buffer): RemoteConfig;
4220
+ /** Reads a stored routing config. NEVER throws: this runs on every READ of a stored config, and a torn/corrupt file must not brick the paths that would fix it - above all writeRoutingConfig, where throwing while reading the OLD config blocks the write of the NEW one forever. Unreadable data is logged and read as undefined - the same as the file not existing. Judging a config on its way IN is the writer's job (see assertValidRemoteConfig at the write entry points), where rejecting bad data with a throw is correct. */
4221
+ export declare function parseRoutingData(data: Buffer): RemoteConfig | undefined;
4221
4222
  export declare function serializeRemoteConfig(config: RemoteConfig): Buffer;
4222
4223
 
4223
4224
  }
@@ -4299,6 +4300,8 @@ declare module "sliftutils/storage/remoteStorage/sourceWrapper" {
4299
4300
  seedLatency(ms: number): void;
4300
4301
  /** Median of the recent pings (API or URL-form, whichever this source measures), plus DISCONNECTED_LATENCY_PENALTY while the source is disconnected - so a down source still sorts and can still be picked, just after every connected one. Sources with no measurements yet sort last (Infinity), except our own in-process server, which is the best possible target (0). */
4301
4302
  getLatency(): number;
4303
+ /** writeBlocked from missing backblaze credentials is re-checked on every write attempt: the secret load can fail TRANSIENTLY (early startup, a file-read hiccup), and one failed load must never permanently stop this process from writing to the bucket. The other writeBlocked reasons (read-only mode, browsers) are static properties of the process, so there is nothing to re-check. */
4304
+ recheckWriteBlocked(): Promise<void>;
4302
4305
  /** Writes always go through the API, so a permission error throws to the caller on every write (and access granted in the meantime is picked up automatically). */
4303
4306
  write<T>(run: (archives: IArchives) => Promise<T>): Promise<T>;
4304
4307
  dispose(): void;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sliftutils",
3
- "version": "1.7.117",
3
+ "version": "1.7.119",
4
4
  "main": "index.js",
5
5
  "license": "MIT",
6
6
  "files": [
@@ -213,12 +213,8 @@ export class BlobStore {
213
213
  private async readRoutingConfig(): Promise<RemoteConfig | undefined> {
214
214
  let data = await this.ownDisk.get(ROUTING_FILE);
215
215
  if (!data || !data.length) return undefined;
216
- try {
217
- return parseRoutingData(data);
218
- } catch (e) {
219
- logStorageError(`Ignoring the routing config in store ${this.folder}: it could not be parsed: ${(e as Error).stack ?? e}`);
220
- return undefined;
221
- }
216
+ // Unparseable reads as "no config" (parseRoutingData logs it) - a store must never be unable to start because its own routing file is torn
217
+ return parseRoutingData(data);
222
218
  }
223
219
 
224
220
  /** The version of the routing config this store is running, so a copy found on a peer is only taken when it is genuinely newer. -1 means it has none. */
@@ -975,6 +971,9 @@ export class BlobStore {
975
971
  */
976
972
  private assertRoutingConfigWritable(data: Buffer): void {
977
973
  let routing = parseRoutingData(data);
974
+ if (!routing) {
975
+ throw new Error(`Routing config write rejected - not a parseable config. Refusing to write ${ROUTING_FILE} to store ${this.folder}: the data is not a valid { version?, sources: [...] } JSON config (${data.length} bytes)`);
976
+ }
978
977
  assertValidRemoteConfig(routing);
979
978
  let incoming = getConfigVersion(routing);
980
979
  let current = this.routingVersion();
@@ -394,14 +394,23 @@ export async function writeRoutingToAllStores(config: { configured: RemoteConfig
394
394
  let routingData = serializeRemoteConfig(configured);
395
395
  let routingWriteTime = Math.round(Date.now());
396
396
  let targets: SourceWrapper[] = [];
397
+ let skipped: SourceWrapper[] = [];
397
398
  let seen = new Set<string>();
398
399
  for (let source of sources) {
399
400
  let key = `${source.config.url}|${source.config.name}`;
400
401
  if (seen.has(key)) continue;
401
402
  seen.add(key);
403
+ // writeBlocked is a capability of this process (read-only mode, a browser, no backblaze credentials) - not connectivity, so skipping is not a pre-check race. Counting these as failures put the retry loop on its 5-minute cadence FOREVER over writes this process cannot make; the stores get the config from the writers that can (servers, node clients, peers pulling it from each other). The recheck first: the backblaze-credentials block can be a TRANSIENT secret-load failure at startup, and skipping on the stale verdict would otherwise make it permanent (see SourceWrapper.recheckWriteBlocked).
404
+ if (source.writeBlocked) {
405
+ await source.recheckWriteBlocked();
406
+ }
407
+ if (source.writeBlocked || !source.api) {
408
+ skipped.push(source);
409
+ continue;
410
+ }
402
411
  targets.push(source);
403
412
  }
404
- console.log(`Writing routing config version ${getConfigVersion(configured)} for ${debugName} to all ${targets.length} stores (write time ${formatDateTimeDetailed(routingWriteTime)}): ${targets.map(x => `${x.config.url} (store ${JSON.stringify(x.config.name)})`).join(", ")}`);
413
+ console.log(`Writing routing config version ${getConfigVersion(configured)} for ${debugName} to all ${targets.length} writable stores (write time ${formatDateTimeDetailed(routingWriteTime)}): ${targets.map(x => `${x.config.url} (store ${JSON.stringify(x.config.name)})`).join(", ") || "none"}${skipped.length && `. Skipping ${skipped.length} not writable from this process: ${skipped.map(x => `${x.config.url} (store ${JSON.stringify(x.config.name)}, ${x.writeBlocked || "no API access"})`).join(", ")}` || ""}`);
405
414
  let failures: string[] = [];
406
415
  await Promise.all(targets.map(async source => {
407
416
  try {
@@ -563,7 +563,11 @@ export class ArchivesChain implements IArchives {
563
563
 
564
564
  private async setRoutingConfig(data: Buffer, config?: { lastModified?: number }): Promise<string> {
565
565
  // Checked before the first node is written to, not just by each node as it arrives: every server would reject it anyway, and finding that out one node at a time is how a config write ends up half-applied
566
- assertValidRemoteConfig(parseRoutingData(data));
566
+ let parsedConfig = parseRoutingData(data);
567
+ if (!parsedConfig) {
568
+ throw new Error(`Routing config write rejected - not a parseable config. The data is not a valid { version?, sources: [...] } JSON config (${data.length} bytes, for ${this.getDebugName()})`);
569
+ }
570
+ assertValidRemoteConfig(parsedConfig);
567
571
  let state = await this.state.getState();
568
572
  let writeTime = Math.round(config?.lastModified || Date.now());
569
573
  let written: string[] = [];