sliftutils 1.7.65 → 1.7.67

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 (37) hide show
  1. package/CLAUDE.md +1 -0
  2. package/dist/test.ts.cache +187 -36
  3. package/dist/treeSummary.ts.cache +359 -0
  4. package/index.d.ts +56 -18
  5. package/package.json +1 -1
  6. package/storage/ArchivesDisk.d.ts +2 -2
  7. package/storage/ArchivesDisk.ts +3 -2
  8. package/storage/IArchives.d.ts +19 -2
  9. package/storage/IArchives.ts +23 -5
  10. package/storage/backblaze.d.ts +2 -2
  11. package/storage/backblaze.ts +5 -4
  12. package/storage/dist/ArchivesDisk.ts.cache +6 -4
  13. package/storage/dist/IArchives.ts.cache +6 -6
  14. package/storage/dist/backblaze.ts.cache +7 -6
  15. package/storage/remoteStorage/ArchivesRemote.d.ts +2 -2
  16. package/storage/remoteStorage/ArchivesRemote.ts +5 -5
  17. package/storage/remoteStorage/ArchivesUrl.d.ts +2 -2
  18. package/storage/remoteStorage/ArchivesUrl.ts +8 -6
  19. package/storage/remoteStorage/blobStore.d.ts +21 -3
  20. package/storage/remoteStorage/blobStore.ts +43 -12
  21. package/storage/remoteStorage/createArchives.d.ts +2 -2
  22. package/storage/remoteStorage/createArchives.ts +5 -6
  23. package/storage/remoteStorage/dist/ArchivesRemote.ts.cache +7 -7
  24. package/storage/remoteStorage/dist/ArchivesUrl.ts.cache +12 -8
  25. package/storage/remoteStorage/dist/blobStore.ts.cache +43 -13
  26. package/storage/remoteStorage/dist/createArchives.ts.cache +6 -7
  27. package/storage/remoteStorage/dist/sourceWrapper.ts.cache +3 -4
  28. package/storage/remoteStorage/dist/storageController.ts.cache +11 -8
  29. package/storage/remoteStorage/dist/storageServerState.ts.cache +38 -9
  30. package/storage/remoteStorage/sourceWrapper.ts +2 -3
  31. package/storage/remoteStorage/storageController.d.ts +3 -3
  32. package/storage/remoteStorage/storageController.ts +8 -5
  33. package/storage/remoteStorage/storageServerState.d.ts +3 -0
  34. package/storage/remoteStorage/storageServerState.ts +34 -7
  35. package/test-files-cache.json +1 -0
  36. package/test.ts +127 -10
  37. package/treeSummary.ts +331 -54
@@ -6,7 +6,7 @@ import { ROUTING_FILE, getBucketBaseUrl, parseHostedUrl, parseBackblazeUrl, pars
6
6
  import { ArchivesRemote } from "./ArchivesRemote";
7
7
  import { ArchivesUrl } from "./ArchivesUrl";
8
8
  import { ArchivesBackblaze } from "../backblaze";
9
- import { getStorageServerConfigOptional, getLocalArchives } from "./storageServerState";
9
+ import { getLocalArchives, isOwnAddress } from "./storageServerState";
10
10
 
11
11
  export const RETRY_START_DELAY = 2 * 1000;
12
12
  export const RETRY_MAX_DELAY = 5 * 60 * 1000;
@@ -75,8 +75,7 @@ export class SourceWrapper {
75
75
  return wrapper;
76
76
  }
77
77
  let parsed = parseHostedUrl(config.url);
78
- let server = isNode() && getStorageServerConfigOptional() || undefined;
79
- if (server && parsed.address === server.domain && parsed.port === server.port) {
78
+ if (isNode() && isOwnAddress(parsed.address, parsed.port)) {
80
79
  // A bucket hosted by our own process - use it directly instead of calling ourselves
81
80
  wrapper.api = getLocalArchives(parsed.account, parsed.bucketName);
82
81
  return wrapper;
@@ -65,14 +65,14 @@ export declare const RemoteStorageController: import("socket-function/SocketFunc
65
65
  get2: (account: string, bucketName: string, path: string, range?: {
66
66
  start: number;
67
67
  end: number;
68
- }) => Promise<{
68
+ }, internal?: boolean) => Promise<{
69
69
  data: Buffer;
70
70
  writeTime: number;
71
71
  size: number;
72
72
  } | undefined>;
73
- set: (account: string, bucketName: string, path: string, data: Buffer, lastModified?: number, forceSetImmutable?: boolean) => Promise<void>;
73
+ set: (account: string, bucketName: string, path: string, data: Buffer, lastModified?: number, forceSetImmutable?: boolean, internal?: boolean) => Promise<void>;
74
74
  del: (account: string, bucketName: string, path: string) => Promise<void>;
75
- getInfo: (account: string, bucketName: string, path: string) => Promise<{
75
+ getInfo: (account: string, bucketName: string, path: string, includeTombstones?: boolean) => Promise<{
76
76
  writeTime: number;
77
77
  size: number;
78
78
  } | undefined>;
@@ -289,27 +289,30 @@ class RemoteStorageControllerBase {
289
289
  let result = await this.get2(account, bucketName, path, range);
290
290
  return result && result.data || undefined;
291
291
  }
292
- async get2(account: string, bucketName: string, path: string, range?: { start: number; end: number }): Promise<{ data: Buffer; writeTime: number; size: number } | undefined> {
292
+ async get2(account: string, bucketName: string, path: string, range?: { start: number; end: number }, internal?: boolean): Promise<{ data: Buffer; writeTime: number; size: number } | undefined> {
293
293
  assertValidPath(path);
294
294
  let bucket = await getBucket(account, bucketName);
295
295
  if (!bucket) return undefined;
296
+ if (internal && bucket.store.getInternal2) {
297
+ return await bucket.store.getInternal2(path, { range });
298
+ }
296
299
  return await bucket.store.get2(path, { range });
297
300
  }
298
- async set(account: string, bucketName: string, path: string, data: Buffer, lastModified?: number, forceSetImmutable?: boolean): Promise<void> {
301
+ async set(account: string, bucketName: string, path: string, data: Buffer, lastModified?: number, forceSetImmutable?: boolean, internal?: boolean): Promise<void> {
299
302
  assertValidName(bucketName, "bucket name");
300
303
  assertValidPath(path);
301
- await writeBucketFile(account, bucketName, path, Buffer.from(data), { lastModified, forceSetImmutable });
304
+ await writeBucketFile(account, bucketName, path, Buffer.from(data), { lastModified, forceSetImmutable, internal });
302
305
  }
303
306
  async del(account: string, bucketName: string, path: string): Promise<void> {
304
307
  assertValidName(bucketName, "bucket name");
305
308
  assertValidPath(path);
306
309
  await deleteBucketFile(account, bucketName, path);
307
310
  }
308
- async getInfo(account: string, bucketName: string, path: string): Promise<{ writeTime: number; size: number } | undefined> {
311
+ async getInfo(account: string, bucketName: string, path: string, includeTombstones?: boolean): Promise<{ writeTime: number; size: number } | undefined> {
309
312
  assertValidPath(path);
310
313
  let bucket = await getBucket(account, bucketName);
311
314
  if (!bucket) return undefined;
312
- return await bucket.store.getInfo(path);
315
+ return await bucket.store.getInfo(path, { includeTombstones });
313
316
  }
314
317
  async findInfo(account: string, bucketName: string, prefix: string, config?: { shallow?: boolean; type?: "files" | "folders" }): Promise<ArchiveFileInfo[]> {
315
318
  let bucket = await getBucket(account, bucketName);
@@ -46,6 +46,8 @@ export type LoadedBucket = {
46
46
  };
47
47
  export declare function addExtraListenPort(port: number): void;
48
48
  export declare function removeExtraListenPort(port: number): void;
49
+ /** Whether address:port is this server process. The ONE self test - findSelfIndexes, createApiArchives, and SourceWrapper all consult it, so "is this me" cannot disagree between the routing plan and connection building: a URL that is us on an extra listen port must never become a network client to ourselves, which is how infinite self-request loops form. */
50
+ export declare function isOwnAddress(address: string, port: number): boolean;
49
51
  /** 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
52
  export declare function resolveSourceArchives(url: string): IArchives;
51
53
  /** Our role in a bucket's routing config, summarized across ALL currently-valid self entries. Stored instead of a single representative HostedConfig, so nothing can accidentally use one entry's route or flags where the union is required - the standard config has the same URL twice: a routed write-shard entry plus an unrouted read-everything entry. */
@@ -63,6 +65,7 @@ export declare function assertMutable(bucket: LoadedBucket, filePath: string, wr
63
65
  export declare function writeBucketFile(account: string, bucketName: string, filePath: string, data: Buffer, config?: {
64
66
  lastModified?: number;
65
67
  forceSetImmutable?: boolean;
68
+ internal?: boolean;
66
69
  }): Promise<void>;
67
70
  export declare function getBucketConfig(bucket: LoadedBucket): ArchivesConfig;
68
71
  /** 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. */
@@ -10,7 +10,7 @@ import { ArchivesDisk } from "../ArchivesDisk";
10
10
  import { BlobStore, IBucketStore, DEFAULT_FAST_WRITE_DELAY, WINDOW_END_FLUSH_MARGIN } from "./blobStore";
11
11
  import {
12
12
  RemoteConfig, HostedConfig, BackblazeConfig, IArchives, ArchivesSource, ArchiveFileInfo, ArchivesConfig,
13
- ArchivesSyncStatus, ChangesAfterConfig, GetConfig, SetConfig,
13
+ ArchivesSyncStatus, ChangesAfterConfig, GetConfig, GetInfoConfig, SetConfig,
14
14
  STORAGE_WRONG_VALID_WINDOW, STORAGE_WRONG_ROUTE, FULL_ROUTE,
15
15
  } from "../IArchives";
16
16
  import { ROUTING_FILE, parseRoutingData, serializeRemoteConfig, parseHostedUrl, replaceHostedUrlPort, buildFileUrl, getConfigVersion, getRoute, routeContains, routeIntersection, normalizeSource } from "./remoteConfig";
@@ -224,15 +224,22 @@ export function addExtraListenPort(port: number): void {
224
224
  export function removeExtraListenPort(port: number): void {
225
225
  extraListenPorts.delete(port);
226
226
  }
227
+ /** Whether address:port is this server process. The ONE self test - findSelfIndexes, createApiArchives, and SourceWrapper all consult it, so "is this me" cannot disagree between the routing plan and connection building: a URL that is us on an extra listen port must never become a network client to ourselves, which is how infinite self-request loops form. */
228
+ export function isOwnAddress(address: string, port: number): boolean {
229
+ let config = getStorageServerConfigOptional();
230
+ if (!config) return false;
231
+ if (address !== config.domain) return false;
232
+ return port === config.port || extraListenPorts.has(port);
233
+ }
234
+
227
235
  function findSelfIndexes(routing: RemoteConfig, account: string, bucketName: string): number[] {
228
- let { domain, port } = getStorageServerConfig();
229
236
  let indexes: number[] = [];
230
237
  for (let i = 0; i < routing.sources.length; i++) {
231
238
  let source = routing.sources[i];
232
239
  if (typeof source === "string" || source.type !== "remote") continue;
233
240
  let parsed = parseHostedUrl(source.url);
234
- if (parsed.address !== domain || parsed.account !== account || parsed.bucketName !== bucketName) continue;
235
- if (parsed.port === port || extraListenPorts.has(parsed.port)) {
241
+ if (parsed.account !== account || parsed.bucketName !== bucketName) continue;
242
+ if (isOwnAddress(parsed.address, parsed.port)) {
236
243
  indexes.push(i);
237
244
  }
238
245
  }
@@ -737,7 +744,7 @@ async function writeRoutingConfig(account: string, bucketName: string, data: Buf
737
744
  await scheduleRoutingReload(account, bucketName);
738
745
  }
739
746
 
740
- export async function writeBucketFile(account: string, bucketName: string, filePath: string, data: Buffer, config?: { lastModified?: number; forceSetImmutable?: boolean }): Promise<void> {
747
+ export async function writeBucketFile(account: string, bucketName: string, filePath: string, data: Buffer, config?: { lastModified?: number; forceSetImmutable?: boolean; internal?: boolean }): Promise<void> {
741
748
  assertWritesAllowed();
742
749
  if (filePath === ROUTING_FILE) {
743
750
  let key = `${account}/${bucketName}`;
@@ -772,6 +779,23 @@ export async function writeBucketFile(account: string, bucketName: string, fileP
772
779
  } else {
773
780
  await assertMutable(loaded, filePath, writeTime);
774
781
  }
782
+ if (config?.internal) {
783
+ if (!config.lastModified) {
784
+ throw new Error(`Internal writes must carry lastModified (they are synchronization pushes, ordered by their write time), writing ${JSON.stringify(filePath)} to bucket ${account}/${bucketName}`);
785
+ }
786
+ // See SetConfig.internal: the stamp must land inside SOME window+route this server is configured for (any window, including past ones - synchronization moves old data), so a confused peer can't stuff data onto a node that was never meant to hold it
787
+ let covered = loaded.selfEntries.some(x => writeTime >= x.validWindow[0] && writeTime < x.validWindow[1] && routeContains(x.route, route));
788
+ if (!covered) {
789
+ throw new Error(`Internal write of ${JSON.stringify(filePath)} to bucket ${account}/${bucketName} rejected: writeTime ${writeTime} (${new Date(writeTime).toISOString()}) at route ${route} is outside every window/route this server is configured for: ${JSON.stringify(loaded.selfEntries.map(x => ({ validWindow: x.validWindow, route: x.route || FULL_ROUTE })))}`);
790
+ }
791
+ if (loaded.store.setInternal) {
792
+ await loaded.store.setInternal(filePath, data, { lastModified: writeTime });
793
+ return;
794
+ }
795
+ // rawDisk stores: the normal set already IS the disk
796
+ await loaded.store.set(filePath, data, { lastModified: writeTime });
797
+ return;
798
+ }
775
799
  await loaded.store.set(filePath, data, { ...getWriteConfig(loaded, writeTime, route), lastModified: writeTime });
776
800
  }
777
801
 
@@ -1112,6 +1136,9 @@ class ArchivesLocalBucket implements IArchives {
1112
1136
  public async get2(fileName: string, config?: GetConfig): Promise<{ data: Buffer; writeTime: number; size: number } | undefined> {
1113
1137
  let bucket = await this.getBucket();
1114
1138
  if (!bucket) return undefined;
1139
+ if (config?.internal && bucket.store.getInternal2) {
1140
+ return await bucket.store.getInternal2(fileName, config);
1141
+ }
1115
1142
  return await bucket.store.get2(fileName, config);
1116
1143
  }
1117
1144
  public async set(fileName: string, data: Buffer, config?: SetConfig): Promise<string> {
@@ -1143,10 +1170,10 @@ class ArchivesLocalBucket implements IArchives {
1143
1170
  throw e;
1144
1171
  }
1145
1172
  }
1146
- public async getInfo(fileName: string): Promise<{ writeTime: number; size: number } | undefined> {
1173
+ public async getInfo(fileName: string, config?: GetInfoConfig): Promise<{ writeTime: number; size: number } | undefined> {
1147
1174
  let bucket = await this.getBucket();
1148
1175
  if (!bucket) return undefined;
1149
- return await bucket.store.getInfo(fileName);
1176
+ return await bucket.store.getInfo(fileName, config);
1150
1177
  }
1151
1178
  public async findInfo(prefix: string, config?: { shallow?: boolean; type: "files" | "folders" }): Promise<ArchiveFileInfo[]> {
1152
1179
  let bucket = await this.getBucket();