sliftutils 1.7.67 → 1.7.69

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 (33) hide show
  1. package/dist/test.ts.cache +3 -3
  2. package/index.d.ts +68 -14
  3. package/package.json +1 -1
  4. package/storage/ArchivesDisk.d.ts +2 -2
  5. package/storage/ArchivesDisk.ts +8 -4
  6. package/storage/IArchives.d.ts +15 -2
  7. package/storage/IArchives.ts +19 -3
  8. package/storage/backblaze.d.ts +2 -2
  9. package/storage/backblaze.ts +20 -3
  10. package/storage/dist/ArchivesDisk.ts.cache +2 -2
  11. package/storage/dist/IArchives.ts.cache +3 -3
  12. package/storage/dist/backblaze.ts.cache +2 -2
  13. package/storage/remoteStorage/ArchivesRemote.d.ts +2 -2
  14. package/storage/remoteStorage/ArchivesRemote.ts +3 -3
  15. package/storage/remoteStorage/accessStats.d.ts +25 -0
  16. package/storage/remoteStorage/accessStats.ts +86 -0
  17. package/storage/remoteStorage/blobStore.d.ts +1 -0
  18. package/storage/remoteStorage/blobStore.ts +31 -12
  19. package/storage/remoteStorage/createArchives.d.ts +2 -2
  20. package/storage/remoteStorage/createArchives.ts +6 -3
  21. package/storage/remoteStorage/dist/ArchivesRemote.ts.cache +2 -2
  22. package/storage/remoteStorage/dist/ArchivesUrl.ts.cache +2 -2
  23. package/storage/remoteStorage/dist/accessStats.ts.cache +78 -0
  24. package/storage/remoteStorage/dist/blobStore.ts.cache +2 -2
  25. package/storage/remoteStorage/dist/createArchives.ts.cache +2 -2
  26. package/storage/remoteStorage/dist/storageController.ts.cache +51 -14
  27. package/storage/remoteStorage/dist/storageServerState.ts.cache +25 -74
  28. package/storage/remoteStorage/storageController.d.ts +9 -1
  29. package/storage/remoteStorage/storageController.ts +52 -11
  30. package/storage/remoteStorage/storageServerState.d.ts +6 -3
  31. package/storage/remoteStorage/storageServerState.ts +46 -73
  32. package/test.ts +1 -1
  33. package/treeSummary.d.ts +31 -0
@@ -2,6 +2,8 @@
2
2
  /// <reference types="node" />
3
3
  import { ArchiveFileInfo, ArchivesConfig, ArchivesSyncStatus, ChangesAfterConfig } from "../IArchives";
4
4
  import { ServerBucketInfo, ActiveBucketInfo } from "./storageServerState";
5
+ import { AccessTotals, AccessSummaryState } from "./accessStats";
6
+ import type { SummaryEntry } from "../../treeSummary";
5
7
  export declare const REMOTE_STORAGE_CLASS_GUID = "RemoteStorageController-b7e42a91";
6
8
  export declare const STORAGE_AUTH_PURPOSE = "remoteStorage-auth-1";
7
9
  export declare const STORAGE_NOT_AUTHENTICATED = "REMOTE_STORAGE_NOT_AUTHENTICATED_cf2f7b1e";
@@ -71,7 +73,7 @@ export declare const RemoteStorageController: import("socket-function/SocketFunc
71
73
  size: number;
72
74
  } | undefined>;
73
75
  set: (account: string, bucketName: string, path: string, data: Buffer, lastModified?: number, forceSetImmutable?: boolean, internal?: boolean) => Promise<void>;
74
- del: (account: string, bucketName: string, path: string) => Promise<void>;
76
+ del: (account: string, bucketName: string, path: string, lastModified?: number, internal?: boolean) => Promise<void>;
75
77
  getInfo: (account: string, bucketName: string, path: string, includeTombstones?: boolean) => Promise<{
76
78
  writeTime: number;
77
79
  size: number;
@@ -88,6 +90,12 @@ export declare const RemoteStorageController: import("socket-function/SocketFunc
88
90
  clearWriteStats: (account: string) => Promise<{
89
91
  clearedBuckets: number;
90
92
  }>;
93
+ getAccessStats: (account: string) => Promise<AccessTotals>;
94
+ getAccessSummaries: (account: string, config: {
95
+ operation: string;
96
+ maxCount: number;
97
+ weightBySize?: boolean;
98
+ }) => Promise<SummaryEntry<AccessSummaryState>[]>;
91
99
  getIndexInfo: (account: string, bucketName: string) => Promise<{
92
100
  fileCount: number;
93
101
  byteCount: number;
@@ -15,6 +15,8 @@ import {
15
15
  getActiveBucket, activateBucket, ActiveBucketInfo, getActiveBucketKeys,
16
16
  } from "./storageServerState";
17
17
  import { StorageClientController } from "./storageClientController";
18
+ import { trackAccess, getAccessTotals, readAccessSummaries, clearAccountAccessStats, AccessTotals, AccessSummaryState } from "./accessStats";
19
+ import type { SummaryEntry } from "../../treeSummary";
18
20
 
19
21
  export const REMOTE_STORAGE_CLASS_GUID = "RemoteStorageController-b7e42a91";
20
22
  export const STORAGE_AUTH_PURPOSE = "remoteStorage-auth-1";
@@ -292,37 +294,60 @@ class RemoteStorageControllerBase {
292
294
  async get2(account: string, bucketName: string, path: string, range?: { start: number; end: number }, internal?: boolean): Promise<{ data: Buffer; writeTime: number; size: number } | undefined> {
293
295
  assertValidPath(path);
294
296
  let bucket = await getBucket(account, bucketName);
295
- if (!bucket) return undefined;
296
- if (internal && bucket.store.getInternal2) {
297
- return await bucket.store.getInternal2(path, { range });
297
+ let result: { data: Buffer; writeTime: number; size: number } | undefined;
298
+ if (bucket) {
299
+ if (internal && bucket.store.getInternal2) {
300
+ result = await bucket.store.getInternal2(path, { range });
301
+ } else {
302
+ result = await bucket.store.get2(path, { range });
303
+ }
298
304
  }
299
- return await bucket.store.get2(path, { range });
305
+ trackAccess({ account, operation: "get", path: `${bucketName}/${path}`, size: result && result.data.length || 0 });
306
+ return result;
300
307
  }
301
308
  async set(account: string, bucketName: string, path: string, data: Buffer, lastModified?: number, forceSetImmutable?: boolean, internal?: boolean): Promise<void> {
302
309
  assertValidName(bucketName, "bucket name");
303
310
  assertValidPath(path);
311
+ if (!data.length) {
312
+ throw new Error(`set was called with an empty buffer for ${JSON.stringify(path)} in bucket ${account}/${bucketName}: an empty file IS a deletion in this system and would read back as missing - call del instead`);
313
+ }
314
+ trackAccess({ account, operation: "set", path: `${bucketName}/${path}`, size: data.length });
304
315
  await writeBucketFile(account, bucketName, path, Buffer.from(data), { lastModified, forceSetImmutable, internal });
305
316
  }
306
- async del(account: string, bucketName: string, path: string): Promise<void> {
317
+ async del(account: string, bucketName: string, path: string, lastModified?: number, internal?: boolean): Promise<void> {
307
318
  assertValidName(bucketName, "bucket name");
308
319
  assertValidPath(path);
309
- await deleteBucketFile(account, bucketName, path);
320
+ trackAccess({ account, operation: "del", path: `${bucketName}/${path}` });
321
+ await deleteBucketFile(account, bucketName, path, { lastModified, internal });
310
322
  }
311
323
  async getInfo(account: string, bucketName: string, path: string, includeTombstones?: boolean): Promise<{ writeTime: number; size: number } | undefined> {
312
324
  assertValidPath(path);
325
+ trackAccess({ account, operation: "getInfo", path: `${bucketName}/${path}` });
313
326
  let bucket = await getBucket(account, bucketName);
314
327
  if (!bucket) return undefined;
315
328
  return await bucket.store.getInfo(path, { includeTombstones });
316
329
  }
317
330
  async findInfo(account: string, bucketName: string, prefix: string, config?: { shallow?: boolean; type?: "files" | "folders" }): Promise<ArchiveFileInfo[]> {
318
331
  let bucket = await getBucket(account, bucketName);
319
- if (!bucket) return [];
320
- return await bucket.store.findInfo(prefix, config);
332
+ let results = bucket && await bucket.store.findInfo(prefix, config) || [];
333
+ // The paths are only known once the results are in; an empty result still counts as one access at the prefix.
334
+ if (results.length) {
335
+ for (let info of results) {
336
+ trackAccess({ account, operation: "findInfo", path: `${bucketName}/${info.path}` });
337
+ }
338
+ } else {
339
+ trackAccess({ account, operation: "findInfo", path: `${bucketName}/${prefix}` });
340
+ }
341
+ return results;
321
342
  }
322
343
  async getChangesAfter2(account: string, bucketName: string, config: ChangesAfterConfig): Promise<ArchiveFileInfo[]> {
323
344
  let bucket = await getBucket(account, bucketName);
324
345
  if (!bucket) return [];
325
- return await bucket.store.getChangesAfter2(config);
346
+ let results = await bucket.store.getChangesAfter2(config);
347
+ for (let info of results) {
348
+ trackAccess({ account, operation: "getChangesAfter", path: `${bucketName}/${info.path}` });
349
+ }
350
+ return results;
326
351
  }
327
352
  async getArchivesConfig(account: string, bucketName: string): Promise<ArchivesConfig> {
328
353
  let bucket = await getBucket(account, bucketName);
@@ -348,10 +373,21 @@ class RemoteStorageControllerBase {
348
373
  assertValidName(bucketName, "bucket name");
349
374
  return await activateBucket(account, bucketName);
350
375
  }
351
- /** Zeroes the write statistics listBuckets reports, for every bucket in the account. */
376
+ /** Zeroes the write statistics listBuckets reports and the in-memory access statistics, for every bucket in the account. */
352
377
  async clearWriteStats(account: string): Promise<{ clearedBuckets: number }> {
353
378
  assertValidName(account, "account");
354
- return { clearedBuckets: await clearAccountWriteStats(account) };
379
+ clearAccountAccessStats(account);
380
+ return { clearedBuckets: clearAccountWriteStats(account) };
381
+ }
382
+ /** In-memory totals per operation type since startup (or the last clearWriteStats). */
383
+ async getAccessStats(account: string): Promise<AccessTotals> {
384
+ assertValidName(account, "account");
385
+ return getAccessTotals(account);
386
+ }
387
+ /** A path breakdown of one operation's accesses (operation names come from getAccessStats). maxCount is passed straight to TreeSummary.getSummaries. weightBySize is ignored for count-only operations, which return their count breakdown. */
388
+ async getAccessSummaries(account: string, config: { operation: string; maxCount: number; weightBySize?: boolean }): Promise<SummaryEntry<AccessSummaryState>[]> {
389
+ assertValidName(account, "account");
390
+ return readAccessSummaries({ account, ...config });
355
391
  }
356
392
  async getIndexInfo(account: string, bucketName: string): Promise<{ fileCount: number; byteCount: number; sources: { debugName: string; fileCount: number; byteCount: number }[] } | undefined> {
357
393
  let bucket = await getBucket(account, bucketName);
@@ -383,6 +419,7 @@ class RemoteStorageControllerBase {
383
419
  assertWritesAllowed();
384
420
  let info = largeUploadInfo.get(uploadId);
385
421
  if (!info) throw new Error(`Unknown large upload ${uploadId}`);
422
+ trackAccess({ account: info.account, operation: "uploadPart", path: `${info.bucketName}/${info.path}`, size: data.length });
386
423
  let bucket = await getBucket(info.account, info.bucketName);
387
424
  if (!bucket) throw new Error(`Bucket ${info.account}/${info.bucketName} no longer exists`);
388
425
  await bucket.store.appendLargeUpload(uploadId, Buffer.from(data));
@@ -437,6 +474,7 @@ class RemoteStorageControllerBase {
437
474
  }
438
475
  let info = await bucket.store.getInfo(filePath);
439
476
  if (!info || !info.size) {
477
+ trackAccess({ account, operation: "httpGet", path: `${bucketName}/${filePath}`, size: 0 });
440
478
  return setHTTPResultHeaders(Buffer.from(""), { status: "404" });
441
479
  }
442
480
  let ext = filePath.split(".").pop() || "";
@@ -477,6 +515,7 @@ class RemoteStorageControllerBase {
477
515
  }
478
516
  }
479
517
  let result = await bucket.store.get2(filePath, { range });
518
+ trackAccess({ account, operation: "httpGet", path: `${bucketName}/${filePath}`, size: result && result.data.length || 0 });
480
519
  if (!result) {
481
520
  return setHTTPResultHeaders(Buffer.from(""), { status: "404" });
482
521
  }
@@ -532,6 +571,8 @@ export const RemoteStorageController = SocketFunction.register(
532
571
  getActiveBucket: { hooks: [accountAccess] },
533
572
  activateBucket: { hooks: [accountAccess] },
534
573
  clearWriteStats: { hooks: [accountAccess] },
574
+ getAccessStats: { hooks: [accountAccess] },
575
+ getAccessSummaries: { hooks: [accountAccess] },
535
576
  getSyncStatus: { hooks: [accountAccess] },
536
577
  startLargeFile: { hooks: [accountAccess] },
537
578
  uploadPart: { hooks: [uploadAccess] },
@@ -28,8 +28,8 @@ export type BucketWriteStats = {
28
28
  flushedWrites: number;
29
29
  flushedBytes: number;
30
30
  };
31
- /** Zeroes the write statistics of every bucket in the account, including counts not yet flushed. */
32
- export declare function clearAccountWriteStats(account: string): Promise<number>;
31
+ /** Zeroes the write statistics of every bucket in the account. */
32
+ export declare function clearAccountWriteStats(account: string): number;
33
33
  export declare function setTrustedMachines(config: {
34
34
  account: string;
35
35
  machineIds: string[];
@@ -111,5 +111,8 @@ export declare function getActiveBucket(account: string, bucketName: string): Pr
111
111
  /** 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. */
112
112
  export declare function activateBucket(account: string, bucketName: string): Promise<ActiveBucketInfo | string>;
113
113
  export declare function listAccountBuckets(account: string): Promise<ServerBucketInfo[]>;
114
- export declare function deleteBucketFile(account: string, bucketName: string, filePath: string): Promise<void>;
114
+ export declare function deleteBucketFile(account: string, bucketName: string, filePath: string, config?: {
115
+ lastModified?: number;
116
+ internal?: boolean;
117
+ }): Promise<void>;
115
118
  export declare function getLocalArchives(account: string, bucketName: string): IArchives;
@@ -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, GetInfoConfig, SetConfig,
13
+ ArchivesSyncStatus, ChangesAfterConfig, DelConfig, 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";
@@ -98,78 +98,36 @@ export type BucketWriteStats = {
98
98
  function emptyWriteStats(): BucketWriteStats {
99
99
  return { originalWrites: 0, originalBytes: 0, flushedWrites: 0, flushedBytes: 0 };
100
100
  }
101
- function addWriteStats(a: BucketWriteStats, b: BucketWriteStats): BucketWriteStats {
102
- return {
103
- originalWrites: a.originalWrites + b.originalWrites,
104
- originalBytes: a.originalBytes + b.originalBytes,
105
- flushedWrites: a.flushedWrites + b.flushedWrites,
106
- flushedBytes: a.flushedBytes + b.flushedBytes,
107
- };
108
- }
109
- function getWriteStatsStorage(): Promise<IStorage<BucketWriteStats>> {
110
- return getSystemStorage<BucketWriteStats>("writeStats");
111
- }
112
101
 
113
- // Counted in memory as deltas since the last flush, so counting a write never touches the disk. The flush reads the stored totals and adds the delta, which also makes it correct across restarts.
114
- const writeStatDeltas = new Map<string, BucketWriteStats>();
115
-
116
- async function flushWriteStats(): Promise<void> {
117
- if (!writeStatDeltas.size) return;
118
- let pending = [...writeStatDeltas];
119
- writeStatDeltas.clear();
120
- let storage = await getWriteStatsStorage();
121
- for (let [key, delta] of pending) {
122
- try {
123
- await storage.set(key, addWriteStats(await storage.get(key) || emptyWriteStats(), delta));
124
- } catch (e) {
125
- // Put the delta back, so a failed flush loses nothing
126
- writeStatDeltas.set(key, addWriteStats(writeStatDeltas.get(key) || emptyWriteStats(), delta));
127
- console.error(`Flushing write stats for bucket ${key} failed: ${(e as Error).stack ?? e}`);
128
- }
129
- }
130
- }
131
-
132
- const startWriteStatsFlushing = lazy(() => {
133
- runInfinitePoll(WRITE_STATS_FLUSH_INTERVAL, flushWriteStats);
134
- });
102
+ // In memory only: totals since this process started (or the last clearWriteStats). Persisting them to disk was more machinery than the numbers were worth.
103
+ const writeStats = new Map<string, BucketWriteStats>();
135
104
 
136
105
  function countBucketWrite(key: string, kind: "original" | "flushed", bytes: number): void {
137
- let delta = writeStatDeltas.get(key);
138
- if (!delta) {
139
- delta = emptyWriteStats();
140
- writeStatDeltas.set(key, delta);
106
+ let stats = writeStats.get(key);
107
+ if (!stats) {
108
+ stats = emptyWriteStats();
109
+ writeStats.set(key, stats);
141
110
  }
142
111
  if (kind === "original") {
143
- delta.originalWrites++;
144
- delta.originalBytes += bytes;
112
+ stats.originalWrites++;
113
+ stats.originalBytes += bytes;
145
114
  } else {
146
- delta.flushedWrites++;
147
- delta.flushedBytes += bytes;
115
+ stats.flushedWrites++;
116
+ stats.flushedBytes += bytes;
148
117
  }
149
- startWriteStatsFlushing();
150
118
  }
151
119
 
152
- async function getBucketWriteStats(key: string): Promise<BucketWriteStats> {
153
- let storage = await getWriteStatsStorage();
154
- let stored = await storage.get(key) || emptyWriteStats();
155
- let delta = writeStatDeltas.get(key);
156
- return delta && addWriteStats(stored, delta) || stored;
120
+ function getBucketWriteStats(key: string): BucketWriteStats {
121
+ return writeStats.get(key) || emptyWriteStats();
157
122
  }
158
123
 
159
- /** Zeroes the write statistics of every bucket in the account, including counts not yet flushed. */
160
- export async function clearAccountWriteStats(account: string): Promise<number> {
161
- let storage = await getWriteStatsStorage();
124
+ /** Zeroes the write statistics of every bucket in the account. */
125
+ export function clearAccountWriteStats(account: string): number {
162
126
  let prefix = `${account}/`;
163
127
  let cleared = 0;
164
- for (let key of await storage.getKeys()) {
165
- if (!key.startsWith(prefix)) continue;
166
- writeStatDeltas.delete(key);
167
- await storage.remove(key);
168
- cleared++;
169
- }
170
- for (let key of [...writeStatDeltas.keys()]) {
128
+ for (let key of [...writeStats.keys()]) {
171
129
  if (!key.startsWith(prefix)) continue;
172
- writeStatDeltas.delete(key);
130
+ writeStats.delete(key);
173
131
  cleared++;
174
132
  }
175
133
  console.log(`Cleared the write statistics of ${cleared} buckets in account ${account}`);
@@ -211,7 +169,6 @@ const buckets = new Map<string, Promise<LoadedBucket | undefined>>();
211
169
 
212
170
  const MAX_REBUILD_TIMER_DELAY = 2 ** 31 - 1;
213
171
  const REBUILD_BOUNDARY_BUFFER = 1000;
214
- const WRITE_STATS_FLUSH_INTERVAL = 5 * 60 * 1000;
215
172
 
216
173
  function getBucketFolder(account: string, bucketName: string): string {
217
174
  return path.join(getStorageServerConfig().folder, "buckets2", account, bucketName);
@@ -1070,15 +1027,11 @@ export async function listAccountBuckets(account: string): Promise<ServerBucketI
1070
1027
  let key = `${account}/${bucketName}`;
1071
1028
  let folder = getBucketFolder(account, bucketName);
1072
1029
  let base: ServerBucketInfo = { bucketName, active: false, folder };
1073
- let [disk, writeStats] = await Promise.all([
1074
- timed("statfs", () => getDiskInfo(folder)).catch((e: Error) => {
1075
- base.diskError = String(e.stack ?? e).slice(0, 500);
1076
- return undefined;
1077
- }),
1078
- timed("writeStats", () => getBucketWriteStats(key)),
1079
- ]);
1080
- base.disk = disk;
1081
- base.writeStats = writeStats;
1030
+ base.disk = await timed("statfs", () => getDiskInfo(folder)).catch((e: Error) => {
1031
+ base.diskError = String(e.stack ?? e).slice(0, 500);
1032
+ return undefined;
1033
+ });
1034
+ base.writeStats = getBucketWriteStats(key);
1082
1035
  const loadedPromise = buckets.get(key);
1083
1036
  if (loadedPromise) {
1084
1037
  try {
@@ -1108,13 +1061,33 @@ export async function listAccountBuckets(account: string): Promise<ServerBucketI
1108
1061
  }
1109
1062
  }
1110
1063
 
1111
- export async function deleteBucketFile(account: string, bucketName: string, filePath: string): Promise<void> {
1064
+ export async function deleteBucketFile(account: string, bucketName: string, filePath: string, config?: { lastModified?: number; internal?: boolean }): Promise<void> {
1065
+ assertWritesAllowed();
1112
1066
  if (filePath === ROUTING_FILE) {
1113
1067
  throw new Error(`The routing config ${JSON.stringify(ROUTING_FILE)} cannot be deleted (overwrite it to change the bucket's configuration)`);
1114
1068
  }
1115
1069
  let loaded = await getLoadedBucket(account, bucketName);
1116
1070
  if (!loaded) return;
1117
- await loaded.store.del(filePath, getWriteConfig(loaded, Date.now(), getRoute(filePath)));
1071
+ let writeTime = config?.lastModified || Date.now();
1072
+ let route = getRoute(filePath);
1073
+ if (config?.internal) {
1074
+ if (!config.lastModified) {
1075
+ throw new Error(`Internal deletions must carry lastModified (they are synchronization pushes, ordered by their write time), deleting ${JSON.stringify(filePath)} in bucket ${account}/${bucketName}`);
1076
+ }
1077
+ // Same acceptance rule as internal writes (see SetConfig.internal): the stamp must land inside SOME window+route this server is configured for
1078
+ let covered = loaded.selfEntries.some(x => writeTime >= x.validWindow[0] && writeTime < x.validWindow[1] && routeContains(x.route, route));
1079
+ if (!covered) {
1080
+ throw new Error(`Internal deletion of ${JSON.stringify(filePath)} in 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 })))}`);
1081
+ }
1082
+ if (loaded.store.setInternal) {
1083
+ // setInternal treats an empty buffer as exactly a deletion: disk removal plus a tombstone index entry, no fan-out
1084
+ await loaded.store.setInternal(filePath, Buffer.alloc(0), { lastModified: writeTime });
1085
+ return;
1086
+ }
1087
+ await loaded.store.del(filePath);
1088
+ return;
1089
+ }
1090
+ await loaded.store.del(filePath, { ...getWriteConfig(loaded, writeTime, route), lastModified: writeTime });
1118
1091
  }
1119
1092
 
1120
1093
  const LARGE_FILE_PART_SIZE = 8 * 1024 * 1024;
@@ -1145,8 +1118,8 @@ class ArchivesLocalBucket implements IArchives {
1145
1118
  await writeBucketFile(this.account, this.bucketName, fileName, data, config);
1146
1119
  return fileName;
1147
1120
  }
1148
- public async del(fileName: string): Promise<void> {
1149
- await deleteBucketFile(this.account, this.bucketName, fileName);
1121
+ public async del(fileName: string, config?: DelConfig): Promise<void> {
1122
+ await deleteBucketFile(this.account, this.bucketName, fileName, config);
1150
1123
  }
1151
1124
  public async setLargeFile(config: { path: string; lastModified?: number; getNextData(): Promise<Buffer | undefined> }): Promise<void> {
1152
1125
  assertWritesAllowed();
package/test.ts CHANGED
@@ -57,7 +57,7 @@ function summarize(files: FileInfo[], weightBySize: boolean) {
57
57
  if (weightBySize) return summary.totalSize;
58
58
  return summary.count;
59
59
  },
60
- expectedOutputCount: SUMMARY_COUNT,
60
+ expectedOutputCount: 100,
61
61
  });
62
62
  let addStart = performance.now();
63
63
  for (let file of files) {
@@ -0,0 +1,31 @@
1
+ export type SummaryEntry<S> = {
2
+ path: string;
3
+ kind: "self" | "subtree" | "group" | "truncated";
4
+ summary: S;
5
+ weight: number;
6
+ };
7
+ export declare class TreeSummary<T, S> {
8
+ private config;
9
+ private root;
10
+ private nodeCount;
11
+ private maxTrackedNodes;
12
+ constructor(config: {
13
+ getPath: (value: T) => string;
14
+ createSummary: () => S;
15
+ addToSummary: (value: T, summary: S) => void;
16
+ mergeSummaries: (target: S, source: S) => void;
17
+ getWeight: (summary: S) => number;
18
+ expectedOutputCount?: number;
19
+ });
20
+ add(value: T): void;
21
+ private splitNode;
22
+ getTrackedNodeCount(): number;
23
+ getSummaries(maxCount: number): SummaryEntry<S>[];
24
+ private isRefinable;
25
+ private refine;
26
+ private splitParts;
27
+ private entryForParts;
28
+ private subtreeEntry;
29
+ private buildOutputSummary;
30
+ private prune;
31
+ }