sliftutils 1.7.108 → 1.7.109

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
@@ -4820,7 +4820,7 @@ declare module "sliftutils/storage/remoteStorage/storeSync" {
4820
4820
  waitForRequiredScans(): Promise<void>;
4821
4821
  /** Rescans our own disk's metadata into the index - used around valid window handoffs, where another process wrote files to the shared folder that our index hasn't seen. */
4822
4822
  rescanBase(): Promise<void>;
4823
- /** One synchronization round of a source: the PULL direction always (its listing, applied to our index), and with "push" the push direction too (what our index says the source is missing, written to it). Push is an argument rather than a separate call because it cannot run without the pull's listing - the index alone cannot say what the source already holds. Listings unblock (initialScan) between the halves, so they never wait behind a push. */
4823
+ /** One synchronization round of a source: the PULL direction always (its listing, applied to our index), and with "push" the push direction too (what our index says the source is missing, written to it). Push is an argument rather than a separate call because it cannot run without the pull's listing - the index alone cannot say what the source already holds. Listings unblock (initialScan) between the halves, so they never wait behind a push. Only one round per SOURCE runs at a time (cache keys the serializer by source index). */
4824
4824
  private syncSource;
4825
4825
  /** A boundary scan of the node that owned (part of) our route in the valid window before ours, when that node is different storage (a disk rescan can't see its writes): just its changes since the boundary neighborhood, with matching values pulled onto our own disk. */
4826
4826
  boundaryScanRemote(source: IArchives, config: {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sliftutils",
3
- "version": "1.7.108",
3
+ "version": "1.7.109",
4
4
  "main": "index.js",
5
5
  "license": "MIT",
6
6
  "files": [
@@ -37,7 +37,7 @@ export declare class StoreSync {
37
37
  waitForRequiredScans(): Promise<void>;
38
38
  /** Rescans our own disk's metadata into the index - used around valid window handoffs, where another process wrote files to the shared folder that our index hasn't seen. */
39
39
  rescanBase(): Promise<void>;
40
- /** One synchronization round of a source: the PULL direction always (its listing, applied to our index), and with "push" the push direction too (what our index says the source is missing, written to it). Push is an argument rather than a separate call because it cannot run without the pull's listing - the index alone cannot say what the source already holds. Listings unblock (initialScan) between the halves, so they never wait behind a push. */
40
+ /** One synchronization round of a source: the PULL direction always (its listing, applied to our index), and with "push" the push direction too (what our index says the source is missing, written to it). Push is an argument rather than a separate call because it cannot run without the pull's listing - the index alone cannot say what the source already holds. Listings unblock (initialScan) between the halves, so they never wait behind a push. Only one round per SOURCE runs at a time (cache keys the serializer by source index). */
41
41
  private syncSource;
42
42
  /** A boundary scan of the node that owned (part of) our route in the valid window before ours, when that node is different storage (a disk rescan can't see its writes): just its changes since the boundary neighborhood, with matching values pulled onto our own disk. */
43
43
  boundaryScanRemote(source: IArchives, config: {
@@ -1,4 +1,5 @@
1
- import { runInfinitePoll, delay, runInfinitePollCallAtStart } from "socket-function/src/batching";
1
+ import { runInfinitePoll, delay, runInfinitePollCallAtStart, runInSerial } from "socket-function/src/batching";
2
+ import { cache } from "socket-function/src/caching";
2
3
  import { timeInMinute, sort, promiseObj } from "socket-function/src/misc";
3
4
  import { formatNumber, formatTime } from "socket-function/src/formatting/format";
4
5
  import {
@@ -219,11 +220,11 @@ export class StoreSync {
219
220
 
220
221
  /** Rescans our own disk's metadata into the index - used around valid window handoffs, where another process wrote files to the shared folder that our index hasn't seen. */
221
222
  public async rescanBase(): Promise<void> {
222
- await this.syncSource(0);
223
+ await this.syncSource(0)();
223
224
  }
224
225
 
225
- /** One synchronization round of a source: the PULL direction always (its listing, applied to our index), and with "push" the push direction too (what our index says the source is missing, written to it). Push is an argument rather than a separate call because it cannot run without the pull's listing - the index alone cannot say what the source already holds. Listings unblock (initialScan) between the halves, so they never wait behind a push. */
226
- private async syncSource(sourceIndex: number, push?: "push"): Promise<void> {
226
+ /** One synchronization round of a source: the PULL direction always (its listing, applied to our index), and with "push" the push direction too (what our index says the source is missing, written to it). Push is an argument rather than a separate call because it cannot run without the pull's listing - the index alone cannot say what the source already holds. Listings unblock (initialScan) between the halves, so they never wait behind a push. Only one round per SOURCE runs at a time (cache keys the serializer by source index). */
227
+ private syncSource = cache((sourceIndex: number) => runInSerial(async (push?: "push"): Promise<void> => {
227
228
  let listing = await this.pullSource(sourceIndex);
228
229
  let state = this.states[sourceIndex];
229
230
  state.scanComplete = true;
@@ -231,7 +232,7 @@ export class StoreSync {
231
232
  if (push && !this.store.stopped.stop && !state.stopped.stop) {
232
233
  await this.pushSource(sourceIndex, listing);
233
234
  }
234
- }
235
+ }));
235
236
 
236
237
  /** A boundary scan of the node that owned (part of) our route in the valid window before ours, when that node is different storage (a disk rescan can't see its writes): just its changes since the boundary neighborhood, with matching values pulled onto our own disk. */
237
238
  public async boundaryScanRemote(source: IArchives, config: { since: number; route?: [number, number] }): Promise<void> {
@@ -304,12 +305,15 @@ export class StoreSync {
304
305
  }
305
306
  }
306
307
  if (this.store.stopped.stop || state.stopped.stop) return;
307
- let pollInterval = sourceObj.sourceConfig?.type === "remote" ? FULL_RESCAN_INTERVAL : FULL_RESCAN_NON_REMOTE_INTERVAL;
308
- await runInfinitePollCallAtStart(pollInterval, async () => {
308
+ // Hourly for our own disk (no sourceConfig - a cheap local walk, and how a sibling process's writes into the shared folder are found) and for remote peers (index-backed, cheap listings); the slow interval is only for sources where a full listing is genuinely expensive (backblaze)
309
+ let pollInterval = (!sourceObj.sourceConfig || sourceObj.sourceConfig.type === "remote") && FULL_RESCAN_INTERVAL || FULL_RESCAN_NON_REMOTE_INTERVAL;
310
+ // Both loops below run one at a time: a change-poll tick landing mid full round would otherwise start a second copy pass over the same pending list, downloading everything twice
311
+ let serial = runInSerial(async (fnc: () => Promise<void>) => await fnc());
312
+ await runInfinitePollCallAtStart(pollInterval, () => serial(async () => {
309
313
  while (!this.store.stopped.stop && !state.stopped.stop) {
310
314
  try {
311
- await this.syncSource(sourceIndex, "push");
312
- if (!noFullSync()) await this.copySourceFiles(sourceIndex);
315
+ await this.syncSource(sourceIndex)("push");
316
+ if (!noFullSync()) await this.copySourceFiles(sourceIndex)();
313
317
  } catch (e) {
314
318
  logSyncEvent({ event: "scanFailed", store: this.store.folder, source: source.getDebugName(), retryInMs: SCAN_RETRY_DELAY, error: String((e as Error).stack ?? e).slice(0, 2000) });
315
319
  await delay(SCAN_RETRY_DELAY);
@@ -319,16 +323,16 @@ export class StoreSync {
319
323
  state.initialScan.resolve(undefined);
320
324
  break;
321
325
  }
322
- }, state.stopped);
326
+ }), state.stopped);
323
327
  if (state.supportsChangesAfter) {
324
- runInfinitePoll(CHANGES_POLL_INTERVAL, async () => {
328
+ runInfinitePoll(CHANGES_POLL_INTERVAL, () => serial(async () => {
325
329
  // A scan that has not succeeded yet is retried HERE, before anything else - polling changes and copying against a never-scanned source would run on arbitrarily stale information
326
330
  if (!state.scanSucceeded) {
327
- await this.syncSource(sourceIndex, "push");
331
+ await this.syncSource(sourceIndex)("push");
328
332
  }
329
333
  await this.pollChanges(sourceIndex);
330
- if (!noFullSync()) await this.copySourceFiles(sourceIndex);
331
- }, state.stopped);
334
+ if (!noFullSync()) await this.copySourceFiles(sourceIndex)();
335
+ }), state.stopped);
332
336
  }
333
337
  }
334
338
 
@@ -522,8 +526,8 @@ export class StoreSync {
522
526
  state.changesAfterTime = pollStart - CHANGES_POLL_OVERLAP;
523
527
  }
524
528
 
525
- // Downloads the files a source currently holds onto our own base source (the local disk), preserving their modified times — so a newer local write always wins. Skipped for noFullSync sources (fronting a large database without copying it); reads still down-cache lazily.
526
- private async copySourceFiles(sourceIndex: number): Promise<void> {
529
+ // Downloads the files a source currently holds onto our own base source (the local disk), preserving their modified times — so a newer local write always wins. Skipped for noFullSync sources (fronting a large database without copying it); reads still down-cache lazily. Only one pass per SOURCE runs at a time (cache keys the serializer by source index) - two passes over the same pending list would download everything twice.
530
+ private copySourceFiles = cache((sourceIndex: number) => runInSerial(async (): Promise<void> => {
527
531
  if (sourceIndex === 0) return;
528
532
  let { source } = this.store.sources[sourceIndex];
529
533
  let state = this.states[sourceIndex];
@@ -648,7 +652,7 @@ export class StoreSync {
648
652
  }
649
653
  logSyncEvent({ event: "fullSyncFinish", store: this.store.folder, source: source.getDebugName(), diffBased: state.supportsChangesAfter, durationMs: Date.now() - activity.startTime, totalFiles: pending.length, totalBytes, processedFiles: activity.doneFiles, copiedFiles, copiedBytes, missingOnSource, failed, aborted });
650
654
  }
651
- }
655
+ }));
652
656
 
653
657
  // ── maintenance ──
654
658