sliftutils 1.7.48 → 1.7.49

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 (32) hide show
  1. package/index.d.ts +76 -33
  2. package/package.json +1 -1
  3. package/storage/ArchivesDisk.d.ts +3 -4
  4. package/storage/ArchivesDisk.ts +9 -2
  5. package/storage/IArchives.d.ts +21 -6
  6. package/storage/IArchives.ts +25 -6
  7. package/storage/backblaze.d.ts +3 -4
  8. package/storage/backblaze.ts +87 -35
  9. package/storage/dist/ArchivesDisk.ts.cache +3 -2
  10. package/storage/dist/IArchives.ts.cache +2 -2
  11. package/storage/dist/backblaze.ts.cache +8 -2
  12. package/storage/remoteStorage/ArchivesRemote.d.ts +3 -5
  13. package/storage/remoteStorage/ArchivesRemote.ts +5 -5
  14. package/storage/remoteStorage/ArchivesUrl.d.ts +2 -1
  15. package/storage/remoteStorage/ArchivesUrl.ts +13 -3
  16. package/storage/remoteStorage/blobStore.d.ts +13 -4
  17. package/storage/remoteStorage/blobStore.ts +191 -105
  18. package/storage/remoteStorage/createArchives.d.ts +3 -5
  19. package/storage/remoteStorage/createArchives.ts +5 -10
  20. package/storage/remoteStorage/dist/ArchivesRemote.ts.cache +3 -3
  21. package/storage/remoteStorage/dist/blobStore.ts.cache +178 -91
  22. package/storage/remoteStorage/dist/createArchives.ts.cache +2 -2
  23. package/storage/remoteStorage/dist/storageController.ts.cache +4 -4
  24. package/storage/remoteStorage/dist/storageServerState.ts.cache +46 -12
  25. package/storage/remoteStorage/remoteConfig.d.ts +3 -1
  26. package/storage/remoteStorage/remoteConfig.ts +11 -1
  27. package/storage/remoteStorage/sourcesList.d.ts +15 -0
  28. package/storage/remoteStorage/sourcesList.ts +69 -0
  29. package/storage/remoteStorage/storageController.d.ts +3 -3
  30. package/storage/remoteStorage/storageController.ts +6 -9
  31. package/storage/remoteStorage/storageServerState.d.ts +3 -0
  32. package/storage/remoteStorage/storageServerState.ts +37 -12
@@ -1,11 +1,13 @@
1
1
  /// <reference types="node" />
2
2
  /// <reference types="node" />
3
- import { RemoteConfig, RemoteConfigBase, HostedConfig, BackblazeConfig } from "../IArchives";
3
+ import { RemoteConfig, RemoteConfigBase, HostedConfig, BackblazeConfig, ArchiveFileInfo, ChangesAfterConfig } from "../IArchives";
4
4
  export declare const ROUTING_FILE = "storage/storagerouting.json";
5
5
  /** 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. */
6
6
  export declare function parseVariableRoute(key: string): number | undefined;
7
7
  /** Where a key routes in [0, 1). A materialized variable-shard suffix completely overrides the hash. */
8
8
  export declare function getRoute(key: string): number;
9
+ /** 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. */
10
+ export declare function filterChanges(files: ArchiveFileInfo[], config: ChangesAfterConfig): ArchiveFileInfo[];
9
11
  export declare function routeContains(route: [number, number] | undefined, value: number): boolean;
10
12
  export declare function routesOverlap(a: [number, number] | undefined, b: [number, number] | undefined): boolean;
11
13
  /** The overlap of two route ranges, or undefined when they don't overlap. */
@@ -2,7 +2,7 @@ import { sort } from "socket-function/src/misc";
2
2
  import { getBufferInt } from "socket-function/src/bits";
3
3
  import { setFlag } from "socket-function/require/compileFlags";
4
4
  import jsSha256 from "js-sha256";
5
- import { RemoteConfig, RemoteConfigBase, HostedConfig, BackblazeConfig, FULL_VALID_WINDOW, FULL_ROUTE, VARIABLE_SHARD } from "../IArchives";
5
+ import { RemoteConfig, RemoteConfigBase, HostedConfig, BackblazeConfig, ArchiveFileInfo, ChangesAfterConfig, FULL_VALID_WINDOW, FULL_ROUTE, VARIABLE_SHARD } from "../IArchives";
6
6
 
7
7
  setFlag(require, "js-sha256", "allowclient", true);
8
8
 
@@ -31,6 +31,16 @@ export function getRoute(key: string): number {
31
31
  return hash % ROUTE_PRECISION / ROUTE_PRECISION;
32
32
  }
33
33
 
34
+ /** 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. */
35
+ export function filterChanges(files: ArchiveFileInfo[], config: ChangesAfterConfig): ArchiveFileInfo[] {
36
+ return files.filter(file => {
37
+ if (file.createTime <= config.time) return false;
38
+ let routes = config.routes;
39
+ if (routes && !routes.some(route => routeContains(route, getRoute(file.path)))) return false;
40
+ return true;
41
+ });
42
+ }
43
+
34
44
  // Route ranges are [start, end) - inclusive start, exclusive end
35
45
  export function routeContains(route: [number, number] | undefined, value: number): boolean {
36
46
  if (!route) return true;
@@ -0,0 +1,15 @@
1
+ export declare class SourcesList {
2
+ private filePath;
3
+ constructor(filePath: string);
4
+ private urls;
5
+ private indexes;
6
+ private endsClean;
7
+ private lastReload;
8
+ private appendQueue;
9
+ private load;
10
+ getUrl(sourcesListIndex: number): string | undefined;
11
+ /** 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. */
12
+ getUrlReloading(sourcesListIndex: number): Promise<string | undefined>;
13
+ /** 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. */
14
+ ensure(url: string): Promise<number>;
15
+ }
@@ -0,0 +1,69 @@
1
+ import fs from "fs";
2
+
3
+ const RELOAD_THROTTLE = 5 * 1000;
4
+
5
+ // The persistent identity behind IndexEntry.sourcesListIndex: an append-only file of source URLs / disk folder paths, one per line, where a URL's 0-based line number IS the sourcesListIndex the index persists. Because the file is only ever appended to, a line number means the same URL forever - across restarts and across processes sharing the folder. A torn append (crash mid-write) leaves an unterminated partial line; the next writer terminates it before appending, so the garbage line permanently occupies its index (harmless - it never matches a real URL) and every reader agrees on the numbering.
6
+ export class SourcesList {
7
+ constructor(private filePath: string) { }
8
+ private urls: string[] = [];
9
+ private indexes = new Map<string, number>();
10
+ private endsClean = true;
11
+ private lastReload = 0;
12
+ private appendQueue = Promise.resolve();
13
+
14
+ private async load(): Promise<void> {
15
+ let content = "";
16
+ try {
17
+ content = await fs.promises.readFile(this.filePath, "utf8");
18
+ } catch (e) {
19
+ if ((e as { code?: string }).code !== "ENOENT") throw e;
20
+ }
21
+ this.endsClean = !content || content.endsWith("\n");
22
+ let lines = content.split("\n");
23
+ if (lines[lines.length - 1] === "") {
24
+ lines.pop();
25
+ }
26
+ this.urls = lines;
27
+ this.indexes.clear();
28
+ for (let i = 0; i < lines.length; i++) {
29
+ if (!this.indexes.has(lines[i])) {
30
+ this.indexes.set(lines[i], i);
31
+ }
32
+ }
33
+ }
34
+
35
+ public getUrl(sourcesListIndex: number): string | undefined {
36
+ return this.urls[sourcesListIndex];
37
+ }
38
+
39
+ /** 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. */
40
+ public async getUrlReloading(sourcesListIndex: number): Promise<string | undefined> {
41
+ if (sourcesListIndex < this.urls.length) return this.urls[sourcesListIndex];
42
+ if (Date.now() - this.lastReload < RELOAD_THROTTLE) return undefined;
43
+ this.lastReload = Date.now();
44
+ await this.load();
45
+ return this.urls[sourcesListIndex];
46
+ }
47
+
48
+ /** 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. */
49
+ public ensure(url: string): Promise<number> {
50
+ if (url.includes("\n")) {
51
+ throw new Error(`Source URLs cannot contain newlines (they are stored one per line): ${JSON.stringify(url)}`);
52
+ }
53
+ let result = this.appendQueue.then(async () => {
54
+ await this.load();
55
+ let existing = this.indexes.get(url);
56
+ if (existing !== undefined) return existing;
57
+ let prefix = this.endsClean && "" || "\n";
58
+ await fs.promises.appendFile(this.filePath, prefix + url + "\n");
59
+ this.endsClean = true;
60
+ let index = this.urls.length;
61
+ this.urls.push(url);
62
+ this.indexes.set(url, index);
63
+ console.log(`Registered new source ${JSON.stringify(url)} as sourcesListIndex ${index} in ${this.filePath}`);
64
+ return index;
65
+ });
66
+ this.appendQueue = result.then(() => { }, () => { });
67
+ return result;
68
+ }
69
+ }
@@ -1,6 +1,6 @@
1
1
  /// <reference types="node" />
2
2
  /// <reference types="node" />
3
- import { ArchiveFileInfo, ArchivesConfig, ArchivesSyncStatus } from "../IArchives";
3
+ import { ArchiveFileInfo, ArchivesConfig, ArchivesSyncStatus, ChangesAfterConfig } from "../IArchives";
4
4
  import { ServerBucketInfo, ActiveBucketInfo } from "./storageServerState";
5
5
  export declare const REMOTE_STORAGE_CLASS_GUID = "RemoteStorageController-b7e42a91";
6
6
  export declare const STORAGE_AUTH_PURPOSE = "remoteStorage-auth-1";
@@ -70,7 +70,7 @@ export declare const RemoteStorageController: import("socket-function/SocketFunc
70
70
  writeTime: number;
71
71
  size: number;
72
72
  } | undefined>;
73
- set: (account: string, bucketName: string, path: string, data: Buffer, lastModified?: number) => Promise<void>;
73
+ set: (account: string, bucketName: string, path: string, data: Buffer, lastModified?: number, forceSetImmutable?: boolean) => Promise<void>;
74
74
  del: (account: string, bucketName: string, path: string) => Promise<void>;
75
75
  getInfo: (account: string, bucketName: string, path: string) => Promise<{
76
76
  writeTime: number;
@@ -80,7 +80,7 @@ export declare const RemoteStorageController: import("socket-function/SocketFunc
80
80
  shallow?: boolean;
81
81
  type?: "files" | "folders";
82
82
  }) => Promise<ArchiveFileInfo[]>;
83
- getChangesAfter: (account: string, bucketName: string, time: number) => Promise<ArchiveFileInfo[]>;
83
+ getChangesAfter2: (account: string, bucketName: string, config: ChangesAfterConfig) => Promise<ArchiveFileInfo[]>;
84
84
  getArchivesConfig: (account: string, bucketName: string) => Promise<ArchivesConfig>;
85
85
  listBuckets: (account: string) => Promise<ServerBucketInfo[]>;
86
86
  getActiveBucket: (account: string, bucketName: string) => Promise<ActiveBucketInfo | string>;
@@ -6,7 +6,7 @@ import { performLocalCall } from "socket-function/src/callManager";
6
6
  import { RequireController } from "socket-function/require/RequireController";
7
7
  import { timeInMinute } from "socket-function/src/misc";
8
8
  import { getCommonName, getPublicIdentifier, getOwnMachineId, verify, verifyMachineIdForPublicKey } from "../../misc/https/certs";
9
- import { ArchiveFileInfo, ArchivesConfig, ArchivesSyncStatus, IMMUTABLE_CACHE_TIME } from "../IArchives";
9
+ import { ArchiveFileInfo, ArchivesConfig, ArchivesSyncStatus, ChangesAfterConfig, IMMUTABLE_CACHE_TIME } from "../IArchives";
10
10
  import { ROUTING_FILE } from "./remoteConfig";
11
11
  import {
12
12
  getStorageServerConfig, getTrust, getRequests, getLoadedBucket, writeBucketFile,
@@ -294,10 +294,10 @@ class RemoteStorageControllerBase {
294
294
  if (!bucket) return undefined;
295
295
  return await bucket.store.get2(path, { range });
296
296
  }
297
- async set(account: string, bucketName: string, path: string, data: Buffer, lastModified?: number): Promise<void> {
297
+ async set(account: string, bucketName: string, path: string, data: Buffer, lastModified?: number, forceSetImmutable?: boolean): Promise<void> {
298
298
  assertValidName(bucketName, "bucket name");
299
299
  assertValidPath(path);
300
- await writeBucketFile(account, bucketName, path, Buffer.from(data), { lastModified });
300
+ await writeBucketFile(account, bucketName, path, Buffer.from(data), { lastModified, forceSetImmutable });
301
301
  }
302
302
  async del(account: string, bucketName: string, path: string): Promise<void> {
303
303
  assertValidName(bucketName, "bucket name");
@@ -315,13 +315,10 @@ class RemoteStorageControllerBase {
315
315
  if (!bucket) return [];
316
316
  return await bucket.store.findInfo(prefix, config);
317
317
  }
318
- async getChangesAfter(account: string, bucketName: string, time: number): Promise<ArchiveFileInfo[]> {
318
+ async getChangesAfter2(account: string, bucketName: string, config: ChangesAfterConfig): Promise<ArchiveFileInfo[]> {
319
319
  let bucket = await getBucket(account, bucketName);
320
320
  if (!bucket) return [];
321
- if (!bucket.store.getChangesAfter) {
322
- throw new Error(`Bucket ${account}/${bucketName} does not support getChangesAfter (rawDisk buckets have no index)`);
323
- }
324
- return await bucket.store.getChangesAfter(time);
321
+ return await bucket.store.getChangesAfter2(config);
325
322
  }
326
323
  async getArchivesConfig(account: string, bucketName: string): Promise<ArchivesConfig> {
327
324
  let bucket = await getBucket(account, bucketName);
@@ -523,7 +520,7 @@ export const RemoteStorageController = SocketFunction.register(
523
520
  del: { hooks: [accountAccess] },
524
521
  getInfo: { hooks: [accountAccess] },
525
522
  findInfo: { hooks: [accountAccess] },
526
- getChangesAfter: { hooks: [accountAccess] },
523
+ getChangesAfter2: { hooks: [accountAccess] },
527
524
  getArchivesConfig: { hooks: [accountAccess] },
528
525
  getIndexInfo: { hooks: [accountAccess] },
529
526
  listBuckets: { hooks: [accountAccess] },
@@ -46,10 +46,13 @@ export type LoadedBucket = {
46
46
  };
47
47
  export declare function addExtraListenPort(port: number): void;
48
48
  export declare function removeExtraListenPort(port: number): void;
49
+ /** 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. */
50
+ export declare function resolveSourceArchives(url: string): IArchives;
49
51
  export declare function getLoadedBucket(account: string, bucketName: string): Promise<LoadedBucket | undefined>;
50
52
  export declare function assertMutable(bucket: LoadedBucket, filePath: string, writeTime: number): Promise<void>;
51
53
  export declare function writeBucketFile(account: string, bucketName: string, filePath: string, data: Buffer, config?: {
52
54
  lastModified?: number;
55
+ forceSetImmutable?: boolean;
53
56
  }): Promise<void>;
54
57
  export declare function getBucketConfig(bucket: LoadedBucket): ArchivesConfig;
55
58
  /** 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. */
@@ -9,10 +9,10 @@ import { ArchivesDisk } from "../ArchivesDisk";
9
9
  import { BlobStore, IBucketStore, DEFAULT_FAST_WRITE_DELAY, WINDOW_END_FLUSH_MARGIN } from "./blobStore";
10
10
  import {
11
11
  RemoteConfig, HostedConfig, BackblazeConfig, IArchives, ArchivesSource, ArchiveFileInfo, ArchivesConfig,
12
- ArchivesSyncStatus,
12
+ ArchivesSyncStatus, ChangesAfterConfig, SetConfig,
13
13
  STORAGE_WRONG_VALID_WINDOW, STORAGE_WRONG_ROUTE, FULL_ROUTE,
14
14
  } from "../IArchives";
15
- import { ROUTING_FILE, parseRoutingData, serializeRemoteConfig, parseHostedUrl, replaceHostedUrlPort, buildFileUrl, getConfigVersion, getRoute, routeContains, routeIntersection } from "./remoteConfig";
15
+ import { ROUTING_FILE, parseRoutingData, serializeRemoteConfig, parseHostedUrl, replaceHostedUrlPort, buildFileUrl, getConfigVersion, getRoute, routeContains, routeIntersection, normalizeSource } from "./remoteConfig";
16
16
  import { injectIntermediateSource, expireIntermediateSources, getIntermediateSources, findSplitUrl, nextIntermediateVersion, INTERMEDIATE_EXPIRE_GRACE } from "./intermediateSources";
17
17
  import { getTakeoverIntermediate } from "./deployTakeover";
18
18
  import { createApiArchives, listServerActiveBucketKeys } from "./createArchives";
@@ -424,6 +424,21 @@ type StorePlan = {
424
424
  structureKey: string;
425
425
  };
426
426
 
427
+ const resolvedSourceArchives = new Map<string, IArchives>();
428
+ /** 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. */
429
+ export function resolveSourceArchives(url: string): IArchives {
430
+ let existing = resolvedSourceArchives.get(url);
431
+ if (existing) return existing;
432
+ let archives: IArchives;
433
+ if (url.startsWith("https://")) {
434
+ archives = createApiArchives(normalizeSource(url));
435
+ } else {
436
+ archives = new ArchivesDisk(url);
437
+ }
438
+ resolvedSourceArchives.set(url, archives);
439
+ return archives;
440
+ }
441
+
427
442
  function sourceIdentity(sourceConfig: HostedConfig | BackblazeConfig | undefined): string {
428
443
  if (!sourceConfig) return "disk";
429
444
  return JSON.stringify({ ...sourceConfig, validWindow: undefined, route: undefined });
@@ -491,6 +506,7 @@ function buildBucket(account: string, bucketName: string, routing: RemoteConfig,
491
506
  }
492
507
  let sources: ArchivesSource[] = plan.sourceSpecs.map(spec => ({
493
508
  source: spec.sourceConfig && createApiArchives(spec.sourceConfig) || new ArchivesDisk(folder),
509
+ url: spec.sourceConfig?.url || folder,
494
510
  validWindow: spec.validWindow,
495
511
  route: spec.route,
496
512
  noFullSync: spec.noFullSync,
@@ -503,6 +519,7 @@ function buildBucket(account: string, bucketName: string, routing: RemoteConfig,
503
519
  },
504
520
  readerDiskLimit: plan.readerDiskLimit,
505
521
  onWriteCounted: (kind, bytes) => countBucketWrite(`${account}/${bucketName}`, kind, bytes),
522
+ resolveSourceUrl: resolveSourceArchives,
506
523
  });
507
524
  }
508
525
  let loaded: LoadedBucket = { account, bucketName, routing, routingJSON: JSON.stringify(routing), selfEntries, self, store, structureKey: plan.structureKey };
@@ -584,10 +601,11 @@ async function checkRoutingChanged(account: string, bucketName: string, config?:
584
601
  loaded.store.updateSources(plan.sourceSpecs.map(spec => {
585
602
  const sourceConfig = spec.sourceConfig;
586
603
  if (!sourceConfig) {
587
- return { identity: sourceIdentity(undefined), validWindow: spec.validWindow, create: (): IArchives => new ArchivesDisk(getBucketFolder(account, bucketName)) };
604
+ return { identity: sourceIdentity(undefined), url: getBucketFolder(account, bucketName), validWindow: spec.validWindow, create: (): IArchives => new ArchivesDisk(getBucketFolder(account, bucketName)) };
588
605
  }
589
606
  return {
590
607
  identity: sourceIdentity(sourceConfig),
608
+ url: sourceConfig.url,
591
609
  validWindow: spec.validWindow,
592
610
  route: spec.route,
593
611
  noFullSync: spec.noFullSync,
@@ -666,7 +684,7 @@ async function writeRoutingConfig(account: string, bucketName: string, data: Buf
666
684
  await scheduleRoutingReload(account, bucketName);
667
685
  }
668
686
 
669
- export async function writeBucketFile(account: string, bucketName: string, filePath: string, data: Buffer, config?: { lastModified?: number }): Promise<void> {
687
+ export async function writeBucketFile(account: string, bucketName: string, filePath: string, data: Buffer, config?: { lastModified?: number; forceSetImmutable?: boolean }): Promise<void> {
670
688
  assertWritesAllowed();
671
689
  if (filePath === ROUTING_FILE) {
672
690
  let key = `${account}/${bucketName}`;
@@ -691,14 +709,24 @@ export async function writeBucketFile(account: string, bucketName: string, fileP
691
709
  throw new Error(`${STORAGE_WRONG_ROUTE} This server does not handle route ${route} (key ${JSON.stringify(filePath)}) for bucket ${account}/${bucketName} (our routes at this time: ${JSON.stringify(timeValid.map(x => x.route || FULL_ROUTE))}). Re-resolve the source for this key and retry.`);
692
710
  }
693
711
  }
694
- await assertMutable(loaded, filePath, writeTime);
712
+ if (config?.forceSetImmutable) {
713
+ if (!config.lastModified) {
714
+ throw new Error(`forceSetImmutable requires lastModified (synchronization writes are ordered by their write time), writing ${JSON.stringify(filePath)} to bucket ${account}/${bucketName}`);
715
+ }
716
+ // Immutability wins: an existing path is kept instead of the push throwing (see SetConfig.forceSetImmutable)
717
+ let self = selectEntryAt(loaded.selfEntries, writeTime, route);
718
+ if (self?.immutable && await loaded.store.getInfo(filePath)) return;
719
+ } else {
720
+ await assertMutable(loaded, filePath, writeTime);
721
+ }
695
722
  await loaded.store.set(filePath, data, { ...getWriteConfig(loaded, writeTime, route), lastModified: writeTime });
696
723
  }
697
724
 
698
725
  export function getBucketConfig(bucket: LoadedBucket): ArchivesConfig {
699
726
  let progress = bucket.store.getSyncProgress?.();
700
727
  return {
701
- supportsChangesAfter: !!bucket.store.getChangesAfter,
728
+ // Native change-feed support = the store keeps an index (rawDisk buckets emulate getChangesAfter2 with a full listing) - clients use this to pick their scan cadence
729
+ supportsChangesAfter: bucket.store instanceof BlobStore,
702
730
  remoteConfig: bucket.routing,
703
731
  index: progress?.index,
704
732
  indexSources: progress?.sources,
@@ -1033,7 +1061,7 @@ class ArchivesLocalBucket implements IArchives {
1033
1061
  if (!bucket) return undefined;
1034
1062
  return await bucket.store.get2(fileName, config);
1035
1063
  }
1036
- public async set(fileName: string, data: Buffer, config?: { lastModified?: number }): Promise<string> {
1064
+ public async set(fileName: string, data: Buffer, config?: SetConfig): Promise<string> {
1037
1065
  await writeBucketFile(this.account, this.bucketName, fileName, data, config);
1038
1066
  return fileName;
1039
1067
  }
@@ -1087,13 +1115,10 @@ class ArchivesLocalBucket implements IArchives {
1087
1115
  public async hasWriteAccess(): Promise<boolean> {
1088
1116
  return true;
1089
1117
  }
1090
- public async getChangesAfter(time: number): Promise<ArchiveFileInfo[]> {
1118
+ public async getChangesAfter2(config: ChangesAfterConfig): Promise<ArchiveFileInfo[]> {
1091
1119
  let bucket = await this.getBucket();
1092
1120
  if (!bucket) return [];
1093
- if (!bucket.store.getChangesAfter) {
1094
- throw new Error(`Bucket ${this.account}/${this.bucketName} does not support getChangesAfter (rawDisk buckets have no index)`);
1095
- }
1096
- return await bucket.store.getChangesAfter(time);
1121
+ return await bucket.store.getChangesAfter2(config);
1097
1122
  }
1098
1123
  public async getSyncStatus(): Promise<ArchivesSyncStatus> {
1099
1124
  let bucket = await this.getBucket();