sliftutils 1.7.89 → 1.7.90

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.
@@ -31,8 +31,8 @@ export type BucketState = {
31
31
  };
32
32
  /** The loaded bucket, loading it (which instantiates its stores and starts their synchronization) if needed. A bucket that does not exist on this server throws - callers never see undefined buckets. */
33
33
  export declare function requireBucket(account: string, bucketName: string): Promise<BucketState>;
34
- /** The store serving a request: the one the client's selected entry NAMES. Nothing else is compared - not the window, not the route, not the flags - because the name is what says which storage this is, and everything else is policy the store itself applies to the request. A client a config version behind on some flag therefore still reaches the right store, which is the whole point of naming it. */
35
- export declare function findBucketStore(account: string, bucketName: string, sourceConfig: SourceConfig | undefined): Promise<LoadedStore>;
34
+ /** The store serving a request: the one the client's selected entry NAMES. Account, name, and bucket ARE the folder, so this is a direct lookup, never a search of what exists - and a name this server has never seen is CREATED, never rejected, because asking for a name is the instruction to have that store (one name, one folder, one index; it configures itself once the routing config lands in it). Nothing else about the request is compared - not the window, not the route, not the flags - which is the whole point of naming it: a client a config version behind on some flag still reaches the right store. */
35
+ export declare function findBucketStore(account: string, bucketName: string, sourceConfig: SourceConfig | undefined): BlobStore;
36
36
  /** Internal (store-to-store) reads skip store selection entirely: the caller is another store whose index says this MACHINE holds the bytes - the persisted holder identity is just a URL, which cannot name a store. Whichever store's folder has the newest copy answers. */
37
37
  export declare function readBucketInternal(account: string, bucketName: string, config: {
38
38
  path: string;
@@ -48,7 +48,7 @@ export declare function readBucketInternal(account: string, bucketName: string,
48
48
  } | undefined>;
49
49
  export declare function getBucketConfig(bucket: BucketState): ArchivesConfig;
50
50
  export declare function bucketSyncStatus(bucket: BucketState): Promise<ArchivesSyncStatus>;
51
- export declare function bucketIndexTotals(bucket: BucketState): Promise<{
51
+ export declare function debugBucketIndexTotals(bucket: BucketState): Promise<{
52
52
  fileCount: number;
53
53
  byteCount: number;
54
54
  sources: {
@@ -104,10 +104,10 @@ export type ActiveBucketInfo = {
104
104
  config: ArchivesConfig;
105
105
  };
106
106
  /** The live in-memory state of ONE bucket, answered without touching the disk (no routing file read, no statfs, no stored write stats). Returns an error string when the bucket is not loaded here, which is the normal state for a bucket nothing has accessed since startup. */
107
- export declare function getActiveBucket(account: string, bucketName: string): Promise<ActiveBucketInfo | string>;
107
+ export declare function debugGetActiveBucket(account: string, bucketName: string): Promise<ActiveBucketInfo | string>;
108
108
  /** Loads a bucket that exists on this server's disk into memory, which starts its synchronization and window timers, and returns its live state. Nothing is written and no other server is contacted - unlike building an ArchivesChain for it, which would probe every source and could write the routing config. Already-loaded buckets just return their state. */
109
109
  export declare function activateBucket(account: string, bucketName: string): Promise<ActiveBucketInfo | string>;
110
- export declare function listAccountBuckets(account: string): Promise<ServerBucketInfo[]>;
110
+ export declare function debugListAccountBuckets(account: string): Promise<ServerBucketInfo[]>;
111
111
  export type BucketWriteStats = {
112
112
  /** Every set call the bucket accepted */
113
113
  originalWrites: number;
@@ -117,4 +117,4 @@ export type BucketWriteStats = {
117
117
  flushedBytes: number;
118
118
  };
119
119
  /** Zeroes the write statistics of every bucket in the account. */
120
- export declare function clearAccountWriteStats(account: string): number;
120
+ export declare function debugClearAccountWriteStats(account: string): number;
@@ -8,6 +8,7 @@ import { ROUTING_FILE, parseRoutingData, serializeRemoteConfig, normalizeSource
8
8
  import { createStoreSource, applySourceConfig } from "./storeSources";
9
9
  import { broadcastRoutingChanged } from "./storageController";
10
10
  import { getStorageServerConfig, assertWritesAllowed } from "./serverConfig";
11
+ import { assertValidSourceName } from "./validation";
11
12
  import { getBucketFolder, listAccountStoreFolders, listBucketStoreFolders, readRoutingFromDisk, getDiskInfo, BucketDiskInfo } from "./bucketDisk";
12
13
  import { scheduleBoundaryWork, reinjectIntermediates } from "./intermediateManagement";
13
14
  import { planSelfStores, isSelfSource, SelfSummary } from "./storePlan";
@@ -53,23 +54,14 @@ export async function requireBucket(account: string, bucketName: string): Promis
53
54
  return bucket;
54
55
  }
55
56
 
56
- /** The store serving a request: the one the client's selected entry NAMES. Nothing else is compared - not the window, not the route, not the flags - because the name is what says which storage this is, and everything else is policy the store itself applies to the request. A client a config version behind on some flag therefore still reaches the right store, which is the whole point of naming it. */
57
- export async function findBucketStore(account: string, bucketName: string, sourceConfig: SourceConfig | undefined): Promise<LoadedStore> {
58
- let bucket = await requireBucket(account, bucketName);
57
+ /** The store serving a request: the one the client's selected entry NAMES. Account, name, and bucket ARE the folder, so this is a direct lookup, never a search of what exists - and a name this server has never seen is CREATED, never rejected, because asking for a name is the instruction to have that store (one name, one folder, one index; it configures itself once the routing config lands in it). Nothing else about the request is compared - not the window, not the route, not the flags - which is the whole point of naming it: a client a config version behind on some flag still reaches the right store. */
58
+ export function findBucketStore(account: string, bucketName: string, sourceConfig: SourceConfig | undefined): BlobStore {
59
59
  if (!sourceConfig) {
60
- throw new Error(`No remote source configuration was provided for bucket ${account}/${bucketName}: every request must say which configured source it selected. This server has: ${JSON.stringify(bucket.stores.map(x => x.name))}`);
61
- }
62
- let store = bucket.stores.find(x => x.name === sourceConfig.name);
63
- if (store) return store;
64
- // The loaded bucket is a snapshot of the disk from when it loaded; a store created since (a new name in a rewritten routing config) is on the disk but not in the snapshot, so a miss re-reads the disk once before it is believed
65
- let previousNames = bucket.stores.map(x => x.name);
66
- let reloaded = await reloadBucket(account, bucketName);
67
- store = reloaded?.stores.find(x => x.name === sourceConfig.name);
68
- if (store) {
69
- console.log(`Reloaded bucket ${account}/${bucketName} to find store ${JSON.stringify(sourceConfig.name)}: its stores changed from ${JSON.stringify(previousNames)} to ${JSON.stringify(reloaded?.stores.map(x => x.name))}`);
70
- return store;
60
+ throw new Error(`No remote source configuration was provided for bucket ${account}/${bucketName}: every request must say which configured source it selected`);
71
61
  }
72
- throw new Error(`This server has no store named ${JSON.stringify(sourceConfig.name)} for bucket ${account}/${bucketName} (it has: ${JSON.stringify((reloaded || bucket).stores.map(x => x.name))}). Requested: ${JSON.stringify(sourceConfig)}`);
62
+ // Not part of resolving the request (the name already says everything): loading the bucket is what schedules its switchover work and marks it active for deploy handoffs, and a bucket serving requests must be loaded
63
+ void getLoadedBucket(account, bucketName).catch((e: Error) => console.error(`Loading bucket ${account}/${bucketName} failed: ${e.stack ?? e}`));
64
+ return getStore(account, bucketName, sourceConfig.name);
73
65
  }
74
66
 
75
67
  /** Internal (store-to-store) reads skip store selection entirely: the caller is another store whose index says this MACHINE holds the bytes - the persisted holder identity is just a URL, which cannot name a store. Whichever store's folder has the newest copy answers. */
@@ -118,7 +110,7 @@ export async function bucketSyncStatus(bucket: BucketState): Promise<ArchivesSyn
118
110
  };
119
111
  }
120
112
 
121
- export async function bucketIndexTotals(bucket: BucketState): Promise<{ fileCount: number; byteCount: number; sources: { debugName: string; fileCount: number; byteCount: number }[] }> {
113
+ export async function debugBucketIndexTotals(bucket: BucketState): Promise<{ fileCount: number; byteCount: number; sources: { debugName: string; fileCount: number; byteCount: number }[] }> {
122
114
  let totals = await Promise.all(bucket.stores.map(s => s.store.computeIndexTotals()));
123
115
  return {
124
116
  fileCount: totals.reduce((sum, x) => sum + x.fileCount, 0),
@@ -166,9 +158,12 @@ function sourcePersistentUrl(sourceConfig: SourceConfig | undefined, folder: str
166
158
  const stores = new Map<string, BlobStore>();
167
159
 
168
160
  export function getStore(account: string, bucketName: string, name: string): BlobStore {
161
+ // The one place a name becomes a folder, so the one place it is validated (it can come straight off the wire)
162
+ assertValidSourceName(name);
169
163
  let folder = getBucketFolder(name, account, bucketName);
170
164
  let existing = stores.get(folder);
171
165
  if (existing) return existing;
166
+ console.log(`Opening store ${JSON.stringify(name)} of bucket ${account}/${bucketName} (folder ${folder}) - created on this first use if it never existed`);
172
167
  let store = new BlobStore(folder, name, {
173
168
  // Which entries in ITS routing config are this server's copy of it
174
169
  isSelf: source => isSelfSource(source, account, bucketName),
@@ -297,7 +292,7 @@ function toActiveBucketInfo(loaded: BucketState): ActiveBucketInfo {
297
292
  }
298
293
 
299
294
  /** The live in-memory state of ONE bucket, answered without touching the disk (no routing file read, no statfs, no stored write stats). Returns an error string when the bucket is not loaded here, which is the normal state for a bucket nothing has accessed since startup. */
300
- export async function getActiveBucket(account: string, bucketName: string): Promise<ActiveBucketInfo | string> {
295
+ export async function debugGetActiveBucket(account: string, bucketName: string): Promise<ActiveBucketInfo | string> {
301
296
  let key = `${account}/${bucketName}`;
302
297
  let loadedPromise = buckets.get(key);
303
298
  if (!loadedPromise) {
@@ -338,7 +333,7 @@ export async function activateBucket(account: string, bucketName: string): Promi
338
333
  return toActiveBucketInfo(loaded);
339
334
  }
340
335
 
341
- export async function listAccountBuckets(account: string): Promise<ServerBucketInfo[]> {
336
+ export async function debugListAccountBuckets(account: string): Promise<ServerBucketInfo[]> {
342
337
  let start = Date.now();
343
338
  let timings = new Map<string, number>();
344
339
  async function timed<T>(name: string, run: () => Promise<T>): Promise<T> {
@@ -391,7 +386,7 @@ export async function listAccountBuckets(account: string): Promise<ServerBucketI
391
386
  } finally {
392
387
  // The parts run concurrently, so these are summed wall times per part, not a breakdown of the total
393
388
  let parts = [...timings].map(([name, ms]) => `${name} ${ms}ms`).join(", ");
394
- console.log(`listAccountBuckets(${account}) took ${Date.now() - start}ms for ${names.length} buckets: ${parts}`);
389
+ console.log(`debugListAccountBuckets(${account}) took ${Date.now() - start}ms for ${names.length} buckets: ${parts}`);
395
390
  }
396
391
  }
397
392
 
@@ -430,7 +425,7 @@ function getBucketWriteStats(key: string): BucketWriteStats {
430
425
  }
431
426
 
432
427
  /** Zeroes the write statistics of every bucket in the account. */
433
- export function clearAccountWriteStats(account: string): number {
428
+ export function debugClearAccountWriteStats(account: string): number {
434
429
  let prefix = `${account}/`;
435
430
  let cleared = 0;
436
431
  for (let key of [...writeStats.keys()]) {