sliftutils 1.7.48 → 1.7.50

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.
Files changed (35) hide show
  1. package/index.d.ts +99 -38
  2. package/package.json +1 -1
  3. package/storage/ArchivesDisk.d.ts +5 -5
  4. package/storage/ArchivesDisk.ts +19 -5
  5. package/storage/IArchives.d.ts +36 -7
  6. package/storage/IArchives.ts +76 -8
  7. package/storage/backblaze.d.ts +4 -4
  8. package/storage/backblaze.ts +89 -36
  9. package/storage/dist/ArchivesDisk.ts.cache +8 -2
  10. package/storage/dist/IArchives.ts.cache +2 -2
  11. package/storage/dist/backblaze.ts.cache +86 -36
  12. package/storage/remoteStorage/ArchivesRemote.d.ts +4 -5
  13. package/storage/remoteStorage/ArchivesRemote.ts +7 -7
  14. package/storage/remoteStorage/ArchivesUrl.d.ts +2 -1
  15. package/storage/remoteStorage/ArchivesUrl.ts +13 -3
  16. package/storage/remoteStorage/blobStore.d.ts +15 -6
  17. package/storage/remoteStorage/blobStore.ts +198 -115
  18. package/storage/remoteStorage/createArchives.d.ts +4 -5
  19. package/storage/remoteStorage/createArchives.ts +6 -11
  20. package/storage/remoteStorage/dist/ArchivesRemote.ts.cache +5 -5
  21. package/storage/remoteStorage/dist/ArchivesUrl.ts.cache +15 -4
  22. package/storage/remoteStorage/dist/blobStore.ts.cache +194 -108
  23. package/storage/remoteStorage/dist/createArchives.ts.cache +4 -9
  24. package/storage/remoteStorage/dist/remoteConfig.ts.cache +15 -3
  25. package/storage/remoteStorage/dist/sourcesList.ts.cache +79 -0
  26. package/storage/remoteStorage/dist/storageController.ts.cache +7 -10
  27. package/storage/remoteStorage/dist/storageServerState.ts.cache +50 -18
  28. package/storage/remoteStorage/remoteConfig.d.ts +3 -1
  29. package/storage/remoteStorage/remoteConfig.ts +11 -1
  30. package/storage/remoteStorage/sourcesList.d.ts +15 -0
  31. package/storage/remoteStorage/sourcesList.ts +69 -0
  32. package/storage/remoteStorage/storageController.d.ts +4 -4
  33. package/storage/remoteStorage/storageController.ts +11 -14
  34. package/storage/remoteStorage/storageServerState.d.ts +3 -0
  35. package/storage/remoteStorage/storageServerState.ts +40 -15
package/index.d.ts CHANGED
@@ -858,7 +858,7 @@ declare module "sliftutils/render-utils/observer" {
858
858
  declare module "sliftutils/storage/ArchivesDisk" {
859
859
  /// <reference types="node" />
860
860
  /// <reference types="node" />
861
- import { IArchives, ArchiveFileInfo, ArchivesConfig } from "./IArchives";
861
+ import { IArchives, ArchiveFileInfo, ArchivesConfig, ChangesAfterConfig, SetConfig } from "./IArchives";
862
862
  export declare class ArchivesDisk implements IArchives {
863
863
  private folder;
864
864
  constructor(folder: string);
@@ -874,11 +874,10 @@ declare module "sliftutils/storage/ArchivesDisk" {
874
874
  };
875
875
  getDebugName(): string;
876
876
  getConfig(): Promise<ArchivesConfig>;
877
+ getChangesAfter2(config: ChangesAfterConfig): Promise<ArchiveFileInfo[]>;
877
878
  hasWriteAccess(): Promise<boolean>;
878
879
  private filePath;
879
- set(key: string, data: Buffer, config?: {
880
- lastModified?: number;
881
- }): Promise<string>;
880
+ set(key: string, data: Buffer, config?: SetConfig): Promise<string>;
882
881
  del(key: string): Promise<void>;
883
882
  get(key: string, config?: {
884
883
  range?: {
@@ -911,11 +910,12 @@ declare module "sliftutils/storage/ArchivesDisk" {
911
910
  private collectFiles;
912
911
  setLargeFile(config: {
913
912
  path: string;
913
+ lastModified?: number;
914
914
  getNextData(): Promise<Buffer | undefined>;
915
915
  }): Promise<void>;
916
916
  startLargeUpload(): Promise<string>;
917
917
  appendLargeUpload(id: string, data: Buffer): Promise<void>;
918
- finishLargeUpload(id: string, key: string): Promise<void>;
918
+ finishLargeUpload(id: string, key: string, lastModified?: number): Promise<void>;
919
919
  cancelLargeUpload(id: string): Promise<void>;
920
920
  getURL(path: string): Promise<string>;
921
921
  }
@@ -2149,6 +2149,18 @@ declare module "sliftutils/storage/IArchives" {
2149
2149
  allowedOrigins?: string[];
2150
2150
  };
2151
2151
  export declare const FULL_VALID_WINDOW: [number, number];
2152
+ export type ChangesAfterConfig = {
2153
+ time: number;
2154
+ /** Only keys routing into one of these [start, end) ranges. Only scanning passes this - it lets a store syncing a partial shard ask for just its slice. */
2155
+ routes?: [number, number][];
2156
+ };
2157
+ export type SetConfig = {
2158
+ lastModified?: number;
2159
+ /** Makes the write acceptable on immutable targets: an existing path is simply kept (immutability wins - nothing is overwritten) instead of the write throwing. Requires lastModified. Synchronization MUST pass this on every push - a plain set throws on immutable targets, which would abort reconciliation whenever one source in a chain is immutable. */
2160
+ forceSetImmutable?: boolean;
2161
+ /** Skips the target-side safety reads around the write (backblaze: the pre-write getInfo comparison and the post-upload existence poll). For writers whose own bookkeeping already decides what to write and orders it by write time (BlobStore's index-driven writes and synchronization), those reads are pure extra API calls - but the default stays checked, because other users of the raw backends rely on the checks. */
2162
+ noChecks?: boolean;
2163
+ };
2152
2164
  export type ArchiveFileInfo = {
2153
2165
  path: string;
2154
2166
  createTime: number;
@@ -2180,6 +2192,8 @@ declare module "sliftutils/storage/IArchives" {
2180
2192
  };
2181
2193
  export type ArchivesSource = {
2182
2194
  source: IArchives;
2195
+ /** The persistent identity of the endpoint: its routing URL (hosted/backblaze), or the disk folder path for the base disk source. The store persists this (via its append-only sources list) as IndexEntry.sourcesListIndex, so it must mean the same endpoint forever. */
2196
+ url: string;
2183
2197
  validWindow: [number, number];
2184
2198
  route?: [number, number];
2185
2199
  noFullSync?: boolean;
@@ -2190,6 +2204,19 @@ declare module "sliftutils/storage/IArchives" {
2190
2204
  export declare const FULL_ROUTE: [number, number];
2191
2205
  export declare const VARIABLE_SHARD = "VARIABLE_SHARD_f0234jfah08fgyhfgyssdds83nmp";
2192
2206
  export declare function windowAcceptsWrites(validWindow: [number, number] | undefined): boolean;
2207
+ /** Copies one file between two archives. Small files go as a single get2+set; past LARGE_COPY_THRESHOLD the copy streams through setLargeFile in LARGE_COPY_CHUNK ranged reads, so the whole file is never in memory. size/writeTime usually come from the caller's metadata scan; when either is omitted, getInfo fills them in. Returns the copied file's info, or undefined when the source doesn't have the file. */
2208
+ export declare function copyArchiveFile(config: {
2209
+ from: IArchives;
2210
+ to: IArchives;
2211
+ path: string;
2212
+ size?: number;
2213
+ writeTime?: number;
2214
+ forceSetImmutable?: boolean;
2215
+ noChecks?: boolean;
2216
+ }): Promise<{
2217
+ writeTime: number;
2218
+ size: number;
2219
+ } | undefined>;
2193
2220
  export type ArchivesSyncSourceStatus = {
2194
2221
  debugName: string;
2195
2222
  validWindow: [number, number];
@@ -2233,13 +2260,12 @@ declare module "sliftutils/storage/IArchives" {
2233
2260
  * VARIABLE_SHARD, where the shard value is materialized into the key (picked by shard latency,
2234
2261
  * see ArchivesChain) and the caller needs the returned key to ever read the value back.
2235
2262
  */
2236
- set(fileName: string, data: Buffer, config?: {
2237
- lastModified?: number;
2238
- }): Promise<string>;
2263
+ set(fileName: string, data: Buffer, config?: SetConfig): Promise<string>;
2239
2264
  del(fileName: string): Promise<void>;
2240
- /** Streams a file too large to hold in memory. getNextData returns undefined when done. */
2265
+ /** Streams a file too large to hold in memory. getNextData returns undefined when done. lastModified stamps the finished file like set's (synchronized copies need it to keep write ordering); backends that stamp their own times (backblaze) accept and ignore it. */
2241
2266
  setLargeFile(config: {
2242
2267
  path: string;
2268
+ lastModified?: number;
2243
2269
  getNextData(): Promise<Buffer | undefined>;
2244
2270
  }): Promise<void>;
2245
2271
  /** writeTime is the last-write time — see ArchiveFileInfo.createTime, which is the same value. */
@@ -2260,10 +2286,13 @@ declare module "sliftutils/storage/IArchives" {
2260
2286
  /** The bucket's configuration, which tells whether the optional functions are supported. */
2261
2287
  getConfig(): Promise<ArchivesConfig>;
2262
2288
  /**
2263
- * All files changed after the given time. Only exists when getConfig().supportsChangesAfter;
2264
- * backed by an index, so it is fast (unlike a full findInfo scan). Deletions are not reported.
2289
+ * All files changed after config.time, optionally restricted to keys routing into one of
2290
+ * config.routes (used by scanning, so partially-overlapping shards only receive their slice).
2291
+ * When getConfig().supportsChangesAfter, this is backed by an index (fast, and deletions ARE
2292
+ * reported, as size-0 tombstone entries). Every other backend emulates it: a full findInfo
2293
+ * listing filtered in memory - correct, but no cheaper than the listing itself.
2265
2294
  */
2266
- getChangesAfter?(time: number): Promise<ArchiveFileInfo[]>;
2295
+ getChangesAfter2(config: ChangesAfterConfig): Promise<ArchiveFileInfo[]>;
2267
2296
  /** Synchronization introspection, for backends that synchronize from sources (see BlobStore). */
2268
2297
  getSyncStatus?(): Promise<ArchivesSyncStatus>;
2269
2298
  }
@@ -2526,7 +2555,7 @@ declare module "sliftutils/storage/TransactionStorage" {
2526
2555
  declare module "sliftutils/storage/backblaze" {
2527
2556
  /// <reference types="node" />
2528
2557
  /// <reference types="node" />
2529
- import { IArchives, ArchivesConfig } from "./IArchives";
2558
+ import { IArchives, ArchivesConfig, ChangesAfterConfig, ArchiveFileInfo, SetConfig } from "./IArchives";
2530
2559
  export declare class ArchivesBackblaze implements IArchives {
2531
2560
  private config;
2532
2561
  constructor(config: {
@@ -2565,12 +2594,11 @@ declare module "sliftutils/storage/backblaze" {
2565
2594
  } | undefined>;
2566
2595
  getConfig(): Promise<ArchivesConfig>;
2567
2596
  hasWriteAccess(): Promise<boolean>;
2568
- set(fileName: string, data: Buffer, config?: {
2569
- lastModified?: number;
2570
- }): Promise<string>;
2597
+ set(fileName: string, data: Buffer, config?: SetConfig): Promise<string>;
2571
2598
  del(fileName: string): Promise<void>;
2572
2599
  setLargeFile(config: {
2573
2600
  path: string;
2601
+ lastModified?: number;
2574
2602
  getNextData(): Promise<Buffer | undefined>;
2575
2603
  }): Promise<void>;
2576
2604
  getInfo(fileName: string): Promise<{
@@ -2581,6 +2609,7 @@ declare module "sliftutils/storage/backblaze" {
2581
2609
  shallow?: boolean;
2582
2610
  type: "files" | "folders";
2583
2611
  }): Promise<string[]>;
2612
+ getChangesAfter2(config: ChangesAfterConfig): Promise<ArchiveFileInfo[]>;
2584
2613
  findInfo(prefix: string, config?: {
2585
2614
  shallow?: boolean;
2586
2615
  type: "files" | "folders";
@@ -2841,7 +2870,7 @@ declare module "sliftutils/storage/remoteFileStorage" {
2841
2870
  declare module "sliftutils/storage/remoteStorage/ArchivesRemote" {
2842
2871
  /// <reference types="node" />
2843
2872
  /// <reference types="node" />
2844
- import { IArchives, ArchiveFileInfo, ArchivesConfig, ArchivesSyncStatus } from "../IArchives";
2873
+ import { IArchives, ArchiveFileInfo, ArchivesConfig, ArchivesSyncStatus, ChangesAfterConfig, SetConfig } from "../IArchives";
2845
2874
  export type ArchivesRemoteConfig = {
2846
2875
  url: string;
2847
2876
  waitForAccess?: boolean;
@@ -2896,9 +2925,7 @@ declare module "sliftutils/storage/remoteStorage/ArchivesRemote" {
2896
2925
  writeTime: number;
2897
2926
  size: number;
2898
2927
  } | undefined>;
2899
- set(fileName: string, data: Buffer, config?: {
2900
- lastModified?: number;
2901
- }): Promise<string>;
2928
+ set(fileName: string, data: Buffer, config?: SetConfig): Promise<string>;
2902
2929
  del(fileName: string): Promise<void>;
2903
2930
  getInfo(fileName: string): Promise<{
2904
2931
  writeTime: number;
@@ -2912,11 +2939,12 @@ declare module "sliftutils/storage/remoteStorage/ArchivesRemote" {
2912
2939
  shallow?: boolean;
2913
2940
  type: "files" | "folders";
2914
2941
  }): Promise<string[]>;
2915
- getChangesAfter(time: number): Promise<ArchiveFileInfo[]>;
2942
+ getChangesAfter2(config: ChangesAfterConfig): Promise<ArchiveFileInfo[]>;
2916
2943
  getConfig(): Promise<ArchivesConfig>;
2917
2944
  getSyncStatus(): Promise<ArchivesSyncStatus>;
2918
2945
  setLargeFile(config: {
2919
2946
  path: string;
2947
+ lastModified?: number;
2920
2948
  getNextData(): Promise<Buffer | undefined>;
2921
2949
  }): Promise<void>;
2922
2950
  getURL(path: string): Promise<string>;
@@ -2927,7 +2955,7 @@ declare module "sliftutils/storage/remoteStorage/ArchivesRemote" {
2927
2955
  declare module "sliftutils/storage/remoteStorage/ArchivesUrl" {
2928
2956
  /// <reference types="node" />
2929
2957
  /// <reference types="node" />
2930
- import { IArchives, ArchiveFileInfo, ArchivesConfig } from "../IArchives";
2958
+ import { IArchives, ArchiveFileInfo, ArchivesConfig, ChangesAfterConfig } from "../IArchives";
2931
2959
  export declare class ArchivesUrl implements IArchives {
2932
2960
  private base;
2933
2961
  constructor(base: string);
@@ -2969,6 +2997,7 @@ declare module "sliftutils/storage/remoteStorage/ArchivesUrl" {
2969
2997
  shallow?: boolean;
2970
2998
  type: "files" | "folders";
2971
2999
  }): Promise<ArchiveFileInfo[]>;
3000
+ getChangesAfter2(config: ChangesAfterConfig): Promise<ArchiveFileInfo[]>;
2972
3001
  getURL(path: string): Promise<string>;
2973
3002
  getConfig(): Promise<ArchivesConfig>;
2974
3003
  hasWriteAccess(): Promise<boolean>;
@@ -2984,7 +3013,7 @@ declare module "sliftutils/storage/remoteStorage/accessPage" {
2984
3013
  declare module "sliftutils/storage/remoteStorage/blobStore" {
2985
3014
  /// <reference types="node" />
2986
3015
  /// <reference types="node" />
2987
- import { IArchives, ArchiveFileInfo, ArchivesSource, ArchivesSyncStatus, SyncActivity } from "../IArchives";
3016
+ import { IArchives, ArchiveFileInfo, ArchivesSource, ArchivesSyncStatus, ChangesAfterConfig, SyncActivity } from "../IArchives";
2988
3017
  export declare const DEFAULT_FAST_WRITE_DELAY: number;
2989
3018
  export declare const WINDOW_END_FLUSH_MARGIN: number;
2990
3019
  export type WriteConfig = {
@@ -3019,7 +3048,7 @@ declare module "sliftutils/storage/remoteStorage/blobStore" {
3019
3048
  shallow?: boolean;
3020
3049
  type?: "files" | "folders";
3021
3050
  }): Promise<ArchiveFileInfo[]>;
3022
- getChangesAfter?(time: number): Promise<ArchiveFileInfo[]>;
3051
+ getChangesAfter2(config: ChangesAfterConfig): Promise<ArchiveFileInfo[]>;
3023
3052
  getSyncStatus?(): Promise<ArchivesSyncStatus>;
3024
3053
  getSyncProgress?(): {
3025
3054
  index: {
@@ -3045,11 +3074,12 @@ declare module "sliftutils/storage/remoteStorage/blobStore" {
3045
3074
  }>;
3046
3075
  startLargeUpload(): Promise<string>;
3047
3076
  appendLargeUpload(id: string, data: Buffer): Promise<void>;
3048
- finishLargeUpload(id: string, key: string): Promise<void>;
3077
+ finishLargeUpload(id: string, key: string, lastModified?: number): Promise<void>;
3049
3078
  cancelLargeUpload(id: string): Promise<void>;
3050
3079
  };
3051
3080
  export type BlobSourceSpec = {
3052
3081
  identity: string;
3082
+ url: string;
3053
3083
  validWindow: [number, number];
3054
3084
  route?: [number, number];
3055
3085
  noFullSync?: boolean;
@@ -3063,6 +3093,7 @@ declare module "sliftutils/storage/remoteStorage/blobStore" {
3063
3093
  onIndexChanged?: ((key: string) => void) | undefined;
3064
3094
  readerDiskLimit?: number | undefined;
3065
3095
  onWriteCounted?: ((kind: "original" | "flushed", bytes: number) => void) | undefined;
3096
+ resolveSourceUrl?: ((url: string) => IArchives) | undefined;
3066
3097
  } | undefined);
3067
3098
  private stopped;
3068
3099
  private index;
@@ -3076,7 +3107,14 @@ declare module "sliftutils/storage/remoteStorage/blobStore" {
3076
3107
  private overlay;
3077
3108
  private sourceStates;
3078
3109
  private syncStarted;
3110
+ private sourcesList;
3111
+ private slotSourcesListIndexes;
3112
+ private slotRegistrations;
3079
3113
  private isLive;
3114
+ private registerSlot;
3115
+ private sourcesListIndexOfSlot;
3116
+ private slotForSourcesListIndex;
3117
+ private getEntryHolder;
3080
3118
  init: {
3081
3119
  (): Promise<void>;
3082
3120
  reset(): void;
@@ -3125,7 +3163,7 @@ declare module "sliftutils/storage/remoteStorage/blobStore" {
3125
3163
  private runSourceSync;
3126
3164
  private scanSource;
3127
3165
  private reconcileSource;
3128
- private applyScanned;
3166
+ private updateScanIndex;
3129
3167
  private pollChanges;
3130
3168
  private copySourceFiles;
3131
3169
  private waitForRequiredScans;
@@ -3160,12 +3198,12 @@ declare module "sliftutils/storage/remoteStorage/blobStore" {
3160
3198
  shallow?: boolean;
3161
3199
  type?: "files" | "folders";
3162
3200
  }): Promise<ArchiveFileInfo[]>;
3163
- getChangesAfter(time: number): Promise<ArchiveFileInfo[]>;
3201
+ getChangesAfter2(config: ChangesAfterConfig): Promise<ArchiveFileInfo[]>;
3164
3202
  getSyncStatus(): Promise<ArchivesSyncStatus>;
3165
3203
  private getDiskSource;
3166
3204
  startLargeUpload(): Promise<string>;
3167
3205
  appendLargeUpload(id: string, data: Buffer): Promise<void>;
3168
- finishLargeUpload(id: string, key: string): Promise<void>;
3206
+ finishLargeUpload(id: string, key: string, lastModified?: number): Promise<void>;
3169
3207
  cancelLargeUpload(id: string): Promise<void>;
3170
3208
  private flushOverlay;
3171
3209
  private evicting;
@@ -3183,7 +3221,7 @@ declare module "sliftutils/storage/remoteStorage/cliArgs" {
3183
3221
  declare module "sliftutils/storage/remoteStorage/createArchives" {
3184
3222
  /// <reference types="node" />
3185
3223
  /// <reference types="node" />
3186
- import { IArchives, RemoteConfig, RemoteConfigBase, HostedConfig, BackblazeConfig, ArchiveFileInfo, ArchivesConfig, ArchivesSyncStatus } from "../IArchives";
3224
+ import { IArchives, RemoteConfig, RemoteConfigBase, HostedConfig, BackblazeConfig, ArchiveFileInfo, ArchivesConfig, ArchivesSyncStatus, ChangesAfterConfig, SetConfig } from "../IArchives";
3187
3225
  import { ServerBucketInfo, ActiveBucketInfo } from "./storageServerState";
3188
3226
  /** The address, port, account, and bucket name a bucket routing URL addresses. Throws when the URL isn't a hosted bucket routing URL (https://host:port/file/<account>/<bucketName>/storage/storagerouting.json). */
3189
3227
  export { parseHostedUrl, parseBackblazeUrl, getBucketBaseUrl } from "./remoteConfig";
@@ -3260,18 +3298,17 @@ declare module "sliftutils/storage/remoteStorage/createArchives" {
3260
3298
  shallow?: boolean;
3261
3299
  type: "files" | "folders";
3262
3300
  }): Promise<ArchiveFileInfo[]>;
3263
- getChangesAfter(time: number): Promise<ArchiveFileInfo[]>;
3301
+ getChangesAfter2(config: ChangesAfterConfig): Promise<ArchiveFileInfo[]>;
3264
3302
  getSyncStatus(): Promise<ArchivesSyncStatus>;
3265
3303
  getConfig(): Promise<ArchivesConfig>;
3266
3304
  hasWriteAccess(): Promise<boolean>;
3267
- set(fileName: string, data: Buffer, config?: {
3268
- lastModified?: number;
3269
- }): Promise<string>;
3305
+ set(fileName: string, data: Buffer, config?: SetConfig): Promise<string>;
3270
3306
  private setRoutingConfig;
3271
3307
  del(fileName: string): Promise<void>;
3272
3308
  private setVariableShard;
3273
3309
  setLargeFile(config: {
3274
3310
  path: string;
3311
+ lastModified?: number;
3275
3312
  getNextData(): Promise<Buffer | undefined>;
3276
3313
  }): Promise<void>;
3277
3314
  getURL(path: string): Promise<string>;
@@ -3376,12 +3413,14 @@ declare module "sliftutils/storage/remoteStorage/productionEnv" {
3376
3413
  declare module "sliftutils/storage/remoteStorage/remoteConfig" {
3377
3414
  /// <reference types="node" />
3378
3415
  /// <reference types="node" />
3379
- import { RemoteConfig, RemoteConfigBase, HostedConfig, BackblazeConfig } from "../IArchives";
3416
+ import { RemoteConfig, RemoteConfigBase, HostedConfig, BackblazeConfig, ArchiveFileInfo, ChangesAfterConfig } from "../IArchives";
3380
3417
  export declare const ROUTING_FILE = "storage/storagerouting.json";
3381
3418
  /** The variable-shard route override embedded in the key ("<sentinel>_<value>", see VARIABLE_SHARD), or undefined when the key has no sentinel or the sentinel has no value yet. */
3382
3419
  export declare function parseVariableRoute(key: string): number | undefined;
3383
3420
  /** Where a key routes in [0, 1). A materialized variable-shard suffix completely overrides the hash. */
3384
3421
  export declare function getRoute(key: string): number;
3422
+ /** The in-memory getChangesAfter2 emulation, for backends without a native change feed: a full listing filtered down to files written after config.time whose keys route into config.routes. */
3423
+ export declare function filterChanges(files: ArchiveFileInfo[], config: ChangesAfterConfig): ArchiveFileInfo[];
3385
3424
  export declare function routeContains(route: [number, number] | undefined, value: number): boolean;
3386
3425
  export declare function routesOverlap(a: [number, number] | undefined, b: [number, number] | undefined): boolean;
3387
3426
  /** The overlap of two route ranges, or undefined when they don't overlap. */
@@ -3460,6 +3499,25 @@ declare module "sliftutils/storage/remoteStorage/sourceWrapper" {
3460
3499
 
3461
3500
  }
3462
3501
 
3502
+ declare module "sliftutils/storage/remoteStorage/sourcesList" {
3503
+ export declare class SourcesList {
3504
+ private filePath;
3505
+ constructor(filePath: string);
3506
+ private urls;
3507
+ private indexes;
3508
+ private endsClean;
3509
+ private lastReload;
3510
+ private appendQueue;
3511
+ private load;
3512
+ getUrl(sourcesListIndex: number): string | undefined;
3513
+ /** For a sourcesListIndex beyond our in-memory list (another process appended since we read): re-reads the file, at most once per RELOAD_THROTTLE. Returning undefined can therefore mean "throttled", not "definitely absent" - never treat it as proof the index is bogus. */
3514
+ getUrlReloading(sourcesListIndex: number): Promise<string | undefined>;
3515
+ /** The sourcesListIndex of the url, appending it if it is new. Appends are serialized within this process; before each append the file is re-read, so appends by other processes are picked up instead of duplicated. */
3516
+ ensure(url: string): Promise<number>;
3517
+ }
3518
+
3519
+ }
3520
+
3463
3521
  declare module "sliftutils/storage/remoteStorage/storageClientController" {
3464
3522
  /** Subscribe to server-pushed routing change notifications. Returns the unsubscribe function. */
3465
3523
  export declare function onServerRoutingChanged(listener: () => void): () => void;
@@ -3472,7 +3530,7 @@ declare module "sliftutils/storage/remoteStorage/storageClientController" {
3472
3530
  declare module "sliftutils/storage/remoteStorage/storageController" {
3473
3531
  /// <reference types="node" />
3474
3532
  /// <reference types="node" />
3475
- import { ArchiveFileInfo, ArchivesConfig, ArchivesSyncStatus } from "../IArchives";
3533
+ import { ArchiveFileInfo, ArchivesConfig, ArchivesSyncStatus, ChangesAfterConfig } from "../IArchives";
3476
3534
  import { ServerBucketInfo, ActiveBucketInfo } from "./storageServerState";
3477
3535
  export declare const REMOTE_STORAGE_CLASS_GUID = "RemoteStorageController-b7e42a91";
3478
3536
  export declare const STORAGE_AUTH_PURPOSE = "remoteStorage-auth-1";
@@ -3542,7 +3600,7 @@ declare module "sliftutils/storage/remoteStorage/storageController" {
3542
3600
  writeTime: number;
3543
3601
  size: number;
3544
3602
  } | undefined>;
3545
- set: (account: string, bucketName: string, path: string, data: Buffer, lastModified?: number) => Promise<void>;
3603
+ set: (account: string, bucketName: string, path: string, data: Buffer, lastModified?: number, forceSetImmutable?: boolean) => Promise<void>;
3546
3604
  del: (account: string, bucketName: string, path: string) => Promise<void>;
3547
3605
  getInfo: (account: string, bucketName: string, path: string) => Promise<{
3548
3606
  writeTime: number;
@@ -3552,7 +3610,7 @@ declare module "sliftutils/storage/remoteStorage/storageController" {
3552
3610
  shallow?: boolean;
3553
3611
  type?: "files" | "folders";
3554
3612
  }) => Promise<ArchiveFileInfo[]>;
3555
- getChangesAfter: (account: string, bucketName: string, time: number) => Promise<ArchiveFileInfo[]>;
3613
+ getChangesAfter2: (account: string, bucketName: string, config: ChangesAfterConfig) => Promise<ArchiveFileInfo[]>;
3556
3614
  getArchivesConfig: (account: string, bucketName: string) => Promise<ArchivesConfig>;
3557
3615
  listBuckets: (account: string) => Promise<ServerBucketInfo[]>;
3558
3616
  getActiveBucket: (account: string, bucketName: string) => Promise<ActiveBucketInfo | string>;
@@ -3570,7 +3628,7 @@ declare module "sliftutils/storage/remoteStorage/storageController" {
3570
3628
  }[];
3571
3629
  } | undefined>;
3572
3630
  getSyncStatus: (account: string, bucketName: string) => Promise<ArchivesSyncStatus>;
3573
- startLargeFile: (account: string, bucketName: string, path: string) => Promise<string>;
3631
+ startLargeFile: (account: string, bucketName: string, path: string, lastModified?: number) => Promise<string>;
3574
3632
  uploadPart: (uploadId: string, data: Buffer) => Promise<void>;
3575
3633
  finishLargeFile: (uploadId: string) => Promise<void>;
3576
3634
  cancelLargeFile: (uploadId: string) => Promise<void>;
@@ -3647,10 +3705,13 @@ declare module "sliftutils/storage/remoteStorage/storageServerState" {
3647
3705
  };
3648
3706
  export declare function addExtraListenPort(port: number): void;
3649
3707
  export declare function removeExtraListenPort(port: number): void;
3708
+ /** A cached IArchives for a persisted source identity: a routing URL (hosted/backblaze) or a disk folder path - the form BlobStore's sources list stores. Configuration (valid windows, routes) decides WHEN a source should be used; for reading bytes the index says a source holds, the URL alone is enough - even for sources no longer in any config. */
3709
+ export declare function resolveSourceArchives(url: string): IArchives;
3650
3710
  export declare function getLoadedBucket(account: string, bucketName: string): Promise<LoadedBucket | undefined>;
3651
3711
  export declare function assertMutable(bucket: LoadedBucket, filePath: string, writeTime: number): Promise<void>;
3652
3712
  export declare function writeBucketFile(account: string, bucketName: string, filePath: string, data: Buffer, config?: {
3653
3713
  lastModified?: number;
3714
+ forceSetImmutable?: boolean;
3654
3715
  }): Promise<void>;
3655
3716
  export declare function getBucketConfig(bucket: LoadedBucket): ArchivesConfig;
3656
3717
  /** Which buckets this process currently has loaded - what a deploy successor asks its predecessor for, so it activates exactly the buckets that are actually in use. */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sliftutils",
3
- "version": "1.7.48",
3
+ "version": "1.7.50",
4
4
  "main": "index.js",
5
5
  "license": "MIT",
6
6
  "files": [
@@ -1,6 +1,6 @@
1
1
  /// <reference types="node" />
2
2
  /// <reference types="node" />
3
- import { IArchives, ArchiveFileInfo, ArchivesConfig } from "./IArchives";
3
+ import { IArchives, ArchiveFileInfo, ArchivesConfig, ChangesAfterConfig, SetConfig } from "./IArchives";
4
4
  export declare class ArchivesDisk implements IArchives {
5
5
  private folder;
6
6
  constructor(folder: string);
@@ -16,11 +16,10 @@ export declare class ArchivesDisk implements IArchives {
16
16
  };
17
17
  getDebugName(): string;
18
18
  getConfig(): Promise<ArchivesConfig>;
19
+ getChangesAfter2(config: ChangesAfterConfig): Promise<ArchiveFileInfo[]>;
19
20
  hasWriteAccess(): Promise<boolean>;
20
21
  private filePath;
21
- set(key: string, data: Buffer, config?: {
22
- lastModified?: number;
23
- }): Promise<string>;
22
+ set(key: string, data: Buffer, config?: SetConfig): Promise<string>;
24
23
  del(key: string): Promise<void>;
25
24
  get(key: string, config?: {
26
25
  range?: {
@@ -53,11 +52,12 @@ export declare class ArchivesDisk implements IArchives {
53
52
  private collectFiles;
54
53
  setLargeFile(config: {
55
54
  path: string;
55
+ lastModified?: number;
56
56
  getNextData(): Promise<Buffer | undefined>;
57
57
  }): Promise<void>;
58
58
  startLargeUpload(): Promise<string>;
59
59
  appendLargeUpload(id: string, data: Buffer): Promise<void>;
60
- finishLargeUpload(id: string, key: string): Promise<void>;
60
+ finishLargeUpload(id: string, key: string, lastModified?: number): Promise<void>;
61
61
  cancelLargeUpload(id: string): Promise<void>;
62
62
  getURL(path: string): Promise<string>;
63
63
  }
@@ -3,7 +3,8 @@ import path from "path";
3
3
  import { lazy } from "socket-function/src/caching";
4
4
  import { runInfinitePoll } from "socket-function/src/batching";
5
5
  import { sort, binarySearchBasic } from "socket-function/src/misc";
6
- import { IArchives, ArchiveFileInfo, ArchivesConfig, assertValidLastModified } from "./IArchives";
6
+ import { IArchives, ArchiveFileInfo, ArchivesConfig, ChangesAfterConfig, SetConfig, assertValidLastModified } from "./IArchives";
7
+ import { filterChanges } from "./remoteStorage/remoteConfig";
7
8
 
8
9
  // The base file-system IArchives: storage is one-to-one with the file system, every key is exactly one real file under <folder>/files, so the file system itself is the index. File handles are cached and reused, and closed once idle (see FileHandleCache). All operations on a file run in serial, so they can't collide with each other or with handle closing. Used as the disk synchronization source of BlobStore (see remoteStorage/blobStore.ts).
9
10
 
@@ -124,6 +125,11 @@ export class ArchivesDisk implements IArchives {
124
125
  return {};
125
126
  }
126
127
 
128
+ public async getChangesAfter2(config: ChangesAfterConfig): Promise<ArchiveFileInfo[]> {
129
+ // No native change feed - a full listing filtered in memory (see the scanning note in BlobStore.scanSource for why the listing itself takes no filters)
130
+ return filterChanges(await this.findInfo(""), config);
131
+ }
132
+
127
133
  public async hasWriteAccess(): Promise<boolean> {
128
134
  return true;
129
135
  }
@@ -136,7 +142,8 @@ export class ArchivesDisk implements IArchives {
136
142
  return result;
137
143
  }
138
144
 
139
- public async set(key: string, data: Buffer, config?: { lastModified?: number }): Promise<string> {
145
+ // forceSetImmutable is accepted and needs no handling: disk sources are never immutable, and the older-write no-op below already gives synchronization its only-take-the-latest semantics
146
+ public async set(key: string, data: Buffer, config?: SetConfig): Promise<string> {
140
147
  await this.init();
141
148
  let lastModified = config?.lastModified;
142
149
  if (lastModified) {
@@ -255,7 +262,7 @@ export class ArchivesDisk implements IArchives {
255
262
  }
256
263
  }
257
264
 
258
- public async setLargeFile(config: { path: string; getNextData(): Promise<Buffer | undefined> }): Promise<void> {
265
+ public async setLargeFile(config: { path: string; lastModified?: number; getNextData(): Promise<Buffer | undefined> }): Promise<void> {
259
266
  let id = await this.startLargeUpload();
260
267
  try {
261
268
  while (true) {
@@ -263,7 +270,7 @@ export class ArchivesDisk implements IArchives {
263
270
  if (!data) break;
264
271
  await this.appendLargeUpload(id, data);
265
272
  }
266
- await this.finishLargeUpload(id, config.path);
273
+ await this.finishLargeUpload(id, config.path, config.lastModified);
267
274
  } catch (e) {
268
275
  await this.cancelLargeUpload(id);
269
276
  throw e;
@@ -286,9 +293,12 @@ export class ArchivesDisk implements IArchives {
286
293
  await handle.write(data, 0, data.length);
287
294
  });
288
295
  }
289
- public async finishLargeUpload(id: string, key: string): Promise<void> {
296
+ public async finishLargeUpload(id: string, key: string, lastModified?: number): Promise<void> {
290
297
  let upload = this.largeUploads.get(id);
291
298
  if (!upload) throw new Error(`Unknown large upload ${id}`);
299
+ if (lastModified) {
300
+ assertValidLastModified(lastModified);
301
+ }
292
302
  const tmpPath = upload.tmpPath;
293
303
  this.largeUploads.delete(id);
294
304
  let filePath = this.filePath(key);
@@ -304,6 +314,10 @@ export class ArchivesDisk implements IArchives {
304
314
  // Nothing was ever appended, so the upload file was never created
305
315
  await fs.promises.writeFile(filePath, Buffer.alloc(0));
306
316
  }
317
+ // The rename preserves the temp file's mtime, which is just when the last append happened - the logical write time has to be stamped explicitly (it is the metadata scans order everything by)
318
+ if (lastModified) {
319
+ await fs.promises.utimes(filePath, new Date(lastModified), new Date(lastModified));
320
+ }
307
321
  });
308
322
  }
309
323
  public async cancelLargeUpload(id: string): Promise<void> {
@@ -48,6 +48,18 @@ export type BackblazeConfig = CommonConfig & {
48
48
  allowedOrigins?: string[];
49
49
  };
50
50
  export declare const FULL_VALID_WINDOW: [number, number];
51
+ export type ChangesAfterConfig = {
52
+ time: number;
53
+ /** Only keys routing into one of these [start, end) ranges. Only scanning passes this - it lets a store syncing a partial shard ask for just its slice. */
54
+ routes?: [number, number][];
55
+ };
56
+ export type SetConfig = {
57
+ lastModified?: number;
58
+ /** Makes the write acceptable on immutable targets: an existing path is simply kept (immutability wins - nothing is overwritten) instead of the write throwing. Requires lastModified. Synchronization MUST pass this on every push - a plain set throws on immutable targets, which would abort reconciliation whenever one source in a chain is immutable. */
59
+ forceSetImmutable?: boolean;
60
+ /** Skips the target-side safety reads around the write (backblaze: the pre-write getInfo comparison and the post-upload existence poll). For writers whose own bookkeeping already decides what to write and orders it by write time (BlobStore's index-driven writes and synchronization), those reads are pure extra API calls - but the default stays checked, because other users of the raw backends rely on the checks. */
61
+ noChecks?: boolean;
62
+ };
51
63
  export type ArchiveFileInfo = {
52
64
  path: string;
53
65
  createTime: number;
@@ -79,6 +91,8 @@ export type ArchivesConfig = {
79
91
  };
80
92
  export type ArchivesSource = {
81
93
  source: IArchives;
94
+ /** The persistent identity of the endpoint: its routing URL (hosted/backblaze), or the disk folder path for the base disk source. The store persists this (via its append-only sources list) as IndexEntry.sourcesListIndex, so it must mean the same endpoint forever. */
95
+ url: string;
82
96
  validWindow: [number, number];
83
97
  route?: [number, number];
84
98
  noFullSync?: boolean;
@@ -89,6 +103,19 @@ export declare const STORAGE_WRONG_ROUTE = "REMOTE_STORAGE_WRONG_ROUTE_c94d2e17"
89
103
  export declare const FULL_ROUTE: [number, number];
90
104
  export declare const VARIABLE_SHARD = "VARIABLE_SHARD_f0234jfah08fgyhfgyssdds83nmp";
91
105
  export declare function windowAcceptsWrites(validWindow: [number, number] | undefined): boolean;
106
+ /** Copies one file between two archives. Small files go as a single get2+set; past LARGE_COPY_THRESHOLD the copy streams through setLargeFile in LARGE_COPY_CHUNK ranged reads, so the whole file is never in memory. size/writeTime usually come from the caller's metadata scan; when either is omitted, getInfo fills them in. Returns the copied file's info, or undefined when the source doesn't have the file. */
107
+ export declare function copyArchiveFile(config: {
108
+ from: IArchives;
109
+ to: IArchives;
110
+ path: string;
111
+ size?: number;
112
+ writeTime?: number;
113
+ forceSetImmutable?: boolean;
114
+ noChecks?: boolean;
115
+ }): Promise<{
116
+ writeTime: number;
117
+ size: number;
118
+ } | undefined>;
92
119
  export type ArchivesSyncSourceStatus = {
93
120
  debugName: string;
94
121
  validWindow: [number, number];
@@ -132,13 +159,12 @@ export interface IArchives {
132
159
  * VARIABLE_SHARD, where the shard value is materialized into the key (picked by shard latency,
133
160
  * see ArchivesChain) and the caller needs the returned key to ever read the value back.
134
161
  */
135
- set(fileName: string, data: Buffer, config?: {
136
- lastModified?: number;
137
- }): Promise<string>;
162
+ set(fileName: string, data: Buffer, config?: SetConfig): Promise<string>;
138
163
  del(fileName: string): Promise<void>;
139
- /** Streams a file too large to hold in memory. getNextData returns undefined when done. */
164
+ /** Streams a file too large to hold in memory. getNextData returns undefined when done. lastModified stamps the finished file like set's (synchronized copies need it to keep write ordering); backends that stamp their own times (backblaze) accept and ignore it. */
140
165
  setLargeFile(config: {
141
166
  path: string;
167
+ lastModified?: number;
142
168
  getNextData(): Promise<Buffer | undefined>;
143
169
  }): Promise<void>;
144
170
  /** writeTime is the last-write time — see ArchiveFileInfo.createTime, which is the same value. */
@@ -159,10 +185,13 @@ export interface IArchives {
159
185
  /** The bucket's configuration, which tells whether the optional functions are supported. */
160
186
  getConfig(): Promise<ArchivesConfig>;
161
187
  /**
162
- * All files changed after the given time. Only exists when getConfig().supportsChangesAfter;
163
- * backed by an index, so it is fast (unlike a full findInfo scan). Deletions are not reported.
188
+ * All files changed after config.time, optionally restricted to keys routing into one of
189
+ * config.routes (used by scanning, so partially-overlapping shards only receive their slice).
190
+ * When getConfig().supportsChangesAfter, this is backed by an index (fast, and deletions ARE
191
+ * reported, as size-0 tombstone entries). Every other backend emulates it: a full findInfo
192
+ * listing filtered in memory - correct, but no cheaper than the listing itself.
164
193
  */
165
- getChangesAfter?(time: number): Promise<ArchiveFileInfo[]>;
194
+ getChangesAfter2(config: ChangesAfterConfig): Promise<ArchiveFileInfo[]>;
166
195
  /** Synchronization introspection, for backends that synchronize from sources (see BlobStore). */
167
196
  getSyncStatus?(): Promise<ArchivesSyncStatus>;
168
197
  }