querysub 0.590.0 → 0.592.0
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.
- package/bin/stop-machine.js +4 -0
- package/package.json +5 -2
- package/src/-a-archives/archiveCache.ts +28 -7
- package/src/-a-archives/archiveCache2.ts +5 -2
- package/src/-a-archives/archives.ts +2 -63
- package/src/-a-archives/archivesDisk.ts +2 -13
- package/src/-a-archives/archivesMemoryCache.ts +27 -17
- package/src/-a-archives/archivesMemoryCache2.ts +23 -0
- package/src/-d-trust/NetworkTrust2.ts +2 -2
- package/src/-e-certs/certAuthority.ts +2 -2
- package/src/-f-node-discovery/NodeDiscovery.ts +2 -2
- package/src/0-path-value-core/AuthorityLookup.ts +0 -4
- package/src/0-path-value-core/ShardPrefixes.ts +2 -4
- package/src/0-path-value-core/archiveLocks/ArchiveLocks2.ts +23 -10
- package/src/0-path-value-core/archiveLocks/archiveSnapshots.ts +0 -1
- package/src/0-path-value-core/pathValueArchives.ts +1 -0
- package/src/4-deploy/edgeBootstrap.ts +50 -11
- package/src/4-deploy/edgeNodes.ts +7 -13
- package/src/4-querysub/Querysub.ts +2 -2
- package/src/config2.ts +1 -7
- package/src/deployManager/components/ServiceMeasureBars.tsx +60 -50
- package/src/deployManager/components/ServicesListPage.tsx +29 -13
- package/src/deployManager/machineDaemonShared.ts +2 -0
- package/src/deployManager/machineSchema.ts +5 -6
- package/src/deployManager/setupMachineMain.ts +1 -2
- package/src/deployManager/stopMachineMain.ts +45 -0
- package/src/diagnostics/logs/IndexedLogs/IndexedLogs.ts +34 -21
- package/src/diagnostics/logs/IndexedLogs/MCPIndexedLogs.ts +3 -3
- package/src/diagnostics/logs/IndexedLogs/TimeFileTree.ts +11 -2
- package/src/diagnostics/logs/IndexedLogs/moveIndexLogsToPublic.ts +3 -4
- package/src/diagnostics/logs/errorTickets/tickets.ts +3 -4
- package/src/diagnostics/logs/lifeCycleAnalysis/lifeCycles.tsx +2 -4
- package/src/diagnostics/watchdog.ts +0 -5
- package/src/-a-archives/archivesBackBlaze.ts +0 -156
- package/src/-a-archives/archivesCborT.ts +0 -52
- package/src/-a-archives/archivesLimitedCache.ts +0 -307
- package/src/-a-archives/archivesPrivateFileSystem.ts +0 -326
- package/src/-a-archives/copyLocalToBackblaze.ts +0 -24
- package/src/-b-authorities/cdnAuthority.ts +0 -53
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
import { lazy } from "socket-function/src/caching";
|
|
2
|
-
import {
|
|
3
|
-
import { getArchivesBackblaze } from "../../../-a-archives/archivesBackBlaze";
|
|
2
|
+
import { getArchives2 } from "../../../-a-archives/archives2";
|
|
4
3
|
import { archiveJSONT } from "../../../-a-archives/archivesJSONT";
|
|
5
|
-
import { getDomain } from "../../../config";
|
|
6
4
|
import { runInfinitePollCallAtStart } from "socket-function/src/batching";
|
|
7
5
|
import { timeInMinute } from "socket-function/src/misc";
|
|
8
6
|
import { SocketFunction } from "socket-function/SocketFunction";
|
|
@@ -56,7 +54,7 @@ export function getVariables(entry: LifeCycleEntry): { key: string, title?: stri
|
|
|
56
54
|
return variables;
|
|
57
55
|
}
|
|
58
56
|
|
|
59
|
-
const lifeCycles = archiveJSONT<LifeCycle>(() =>
|
|
57
|
+
const lifeCycles = archiveJSONT<LifeCycle>(() => getArchives2("logs/life-cycles/"));
|
|
60
58
|
let lifeCyclesCache: LifeCycle[] = [];
|
|
61
59
|
|
|
62
60
|
let ensureWatching = lazy(async () => {
|
|
@@ -38,10 +38,6 @@ export type MeasureSummary = {
|
|
|
38
38
|
startTime: number;
|
|
39
39
|
endTime: number;
|
|
40
40
|
profiledTime: number;
|
|
41
|
-
entries: {
|
|
42
|
-
name: string;
|
|
43
|
-
ownTime: number;
|
|
44
|
-
}[];
|
|
45
41
|
};
|
|
46
42
|
|
|
47
43
|
let measureChunks: MeasureChunk[] = [];
|
|
@@ -78,7 +74,6 @@ export function getLastMeasureSummary(): MeasureSummary | undefined {
|
|
|
78
74
|
startTime: chunk.startTime,
|
|
79
75
|
endTime: chunk.endTime,
|
|
80
76
|
profiledTime: chunk.profiledTime,
|
|
81
|
-
entries: chunk.entries.map(x => ({ name: x.name, ownTime: x.ownTime.sum })),
|
|
82
77
|
};
|
|
83
78
|
}
|
|
84
79
|
export function getMeasureBreakdown(range: { startTime: number; endTime: number }): MeasureChunk | undefined {
|
|
@@ -1,156 +0,0 @@
|
|
|
1
|
-
import { cache } from "socket-function/src/caching";
|
|
2
|
-
import { Archives } from "./archives";
|
|
3
|
-
import fs from "fs";
|
|
4
|
-
import { isNode, timeInMinute } from "socket-function/src/misc";
|
|
5
|
-
import { isLogBackblaze } from "../config";
|
|
6
|
-
import { getBackblazePath } from "../misc/appPaths";
|
|
7
|
-
import { ArchivesBackblaze as ArchivesBackblazeBase } from "sliftutils/storage/backblaze";
|
|
8
|
-
import type { IArchives, ArchivesConfig } from "sliftutils/storage/IArchives";
|
|
9
|
-
|
|
10
|
-
export function hasBackblazePermissions() {
|
|
11
|
-
return isNode() && fs.existsSync(getBackblazePath());
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
// The implementation lives in sliftutils (it reads credentials via getSecret, which our appSecrets.ts serves from getBackblazePath). We forward to an inner instance instead of subclassing, because our Archives interface's move/copy take a querysub Archives target (with getBaseArchives unwrapping) which is type-incompatible with the base class's IArchives signatures.
|
|
15
|
-
export class ArchivesBackblaze {
|
|
16
|
-
private archives: ArchivesBackblazeBase;
|
|
17
|
-
|
|
18
|
-
public constructor(config: {
|
|
19
|
-
bucketName: string;
|
|
20
|
-
public?: boolean;
|
|
21
|
-
immutable?: boolean;
|
|
22
|
-
cacheTime?: number;
|
|
23
|
-
allowedOrigins?: string[];
|
|
24
|
-
}) {
|
|
25
|
-
this.archives = new ArchivesBackblazeBase(config);
|
|
26
|
-
if (isLogBackblaze()) {
|
|
27
|
-
this.archives.enableLogging();
|
|
28
|
-
}
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
public enableLogging(): void {
|
|
32
|
-
this.archives.enableLogging();
|
|
33
|
-
}
|
|
34
|
-
public getDebugName(): string {
|
|
35
|
-
return this.archives.getDebugName();
|
|
36
|
-
}
|
|
37
|
-
public async hasWriteAccess(): Promise<boolean> {
|
|
38
|
-
return this.archives.hasWriteAccess();
|
|
39
|
-
}
|
|
40
|
-
public async getConfig(): Promise<ArchivesConfig> {
|
|
41
|
-
return this.archives.getConfig();
|
|
42
|
-
}
|
|
43
|
-
public async get(fileName: string, config?: { range?: { start: number; end: number; }; retryCount?: number }): Promise<Buffer | undefined> {
|
|
44
|
-
return this.archives.get(fileName, config);
|
|
45
|
-
}
|
|
46
|
-
public async get2(fileName: string, config?: { range?: { start: number; end: number; } }): Promise<{ data: Buffer; writeTime: number; size: number } | undefined> {
|
|
47
|
-
return this.archives.get2(fileName, config);
|
|
48
|
-
}
|
|
49
|
-
public async set(fileName: string, data: Buffer, config?: { lastModified?: number }): Promise<string> {
|
|
50
|
-
return this.archives.set(fileName, data, config);
|
|
51
|
-
}
|
|
52
|
-
public async append(fileName: string, data: Buffer): Promise<void> {
|
|
53
|
-
throw new Error(`ArchivesBackblaze does not support append. Use set instead.`);
|
|
54
|
-
}
|
|
55
|
-
public async del(fileName: string): Promise<void> {
|
|
56
|
-
return this.archives.del(fileName);
|
|
57
|
-
}
|
|
58
|
-
public async setLargeFile(config: { path: string; getNextData(): Promise<Buffer | undefined>; }): Promise<void> {
|
|
59
|
-
return this.archives.setLargeFile(config);
|
|
60
|
-
}
|
|
61
|
-
public async getInfo(fileName: string): Promise<{ writeTime: number; size: number; } | undefined> {
|
|
62
|
-
return this.archives.getInfo(fileName);
|
|
63
|
-
}
|
|
64
|
-
public async find(prefix: string, config?: { shallow?: boolean; type: "files" | "folders" }): Promise<string[]> {
|
|
65
|
-
return this.archives.find(prefix, config);
|
|
66
|
-
}
|
|
67
|
-
public async findInfo(prefix: string, config?: { shallow?: boolean; type: "files" | "folders" }): Promise<{ path: string; createTime: number; size: number; }[]> {
|
|
68
|
-
return this.archives.findInfo(prefix, config);
|
|
69
|
-
}
|
|
70
|
-
public async assertPathValid(path: string): Promise<void> {
|
|
71
|
-
return this.archives.assertPathValid(path);
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
public async move(config: {
|
|
75
|
-
path: string;
|
|
76
|
-
target: Archives;
|
|
77
|
-
targetPath: string;
|
|
78
|
-
copyInstead?: boolean;
|
|
79
|
-
}): Promise<void> {
|
|
80
|
-
let { target, targetPath } = config;
|
|
81
|
-
let base = target.getBaseArchives?.();
|
|
82
|
-
if (base) {
|
|
83
|
-
target = base.archives;
|
|
84
|
-
targetPath = base.parentPath + targetPath;
|
|
85
|
-
}
|
|
86
|
-
// Hand the inner instance across so the base implementation's bucket-to-bucket copyFile fast path (an instanceof check on its own class) still triggers.
|
|
87
|
-
let innerTarget: IArchives = target instanceof ArchivesBackblaze
|
|
88
|
-
? target.archives
|
|
89
|
-
: target as unknown as IArchives;
|
|
90
|
-
return this.archives.move({
|
|
91
|
-
path: config.path,
|
|
92
|
-
target: innerTarget as any,
|
|
93
|
-
targetPath,
|
|
94
|
-
copyInstead: config.copyInstead,
|
|
95
|
-
});
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
public async copy(config: {
|
|
99
|
-
path: string;
|
|
100
|
-
target: Archives;
|
|
101
|
-
targetPath: string;
|
|
102
|
-
}): Promise<void> {
|
|
103
|
-
return this.move({ ...config, copyInstead: true });
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
public async getURL(path: string): Promise<string> {
|
|
107
|
-
return this.archives.getURL(path);
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
public async getDownloadAuthorization(config: {
|
|
111
|
-
fileNamePrefix?: string;
|
|
112
|
-
validDurationInSeconds: number;
|
|
113
|
-
b2ContentDisposition?: string;
|
|
114
|
-
b2ContentLanguage?: string;
|
|
115
|
-
b2Expires?: string;
|
|
116
|
-
b2CacheControl?: string;
|
|
117
|
-
b2ContentEncoding?: string;
|
|
118
|
-
b2ContentType?: string;
|
|
119
|
-
}): Promise<{
|
|
120
|
-
bucketId: string;
|
|
121
|
-
fileNamePrefix: string;
|
|
122
|
-
authorizationToken: string;
|
|
123
|
-
}> {
|
|
124
|
-
return this.archives.getDownloadAuthorization(config);
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
export const getArchivesBackblaze = cache((domain: string) => {
|
|
129
|
-
return new ArchivesBackblaze({ bucketName: domain });
|
|
130
|
-
});
|
|
131
|
-
export const getArchivesBackblazePrivateImmutable = cache((domain: string) => {
|
|
132
|
-
return new ArchivesBackblaze({
|
|
133
|
-
bucketName: domain + "-private-immutable",
|
|
134
|
-
immutable: true
|
|
135
|
-
});
|
|
136
|
-
});
|
|
137
|
-
export const getArchivesBackblazePublicImmutable = cache((domain: string) => {
|
|
138
|
-
return new ArchivesBackblaze({
|
|
139
|
-
bucketName: domain + "-public-immutable",
|
|
140
|
-
public: true,
|
|
141
|
-
immutable: true,
|
|
142
|
-
allowedOrigins: [`https://${domain}`, `https://127-0-0-1.${domain}:7007`],
|
|
143
|
-
});
|
|
144
|
-
});
|
|
145
|
-
|
|
146
|
-
// NOTE: Cache by a minute. This might be a bad idea, but... usually whole reason for public is
|
|
147
|
-
// for cloudflare caching (as otherwise we can just access it through a server), or for large files
|
|
148
|
-
// (which should be cached anyways, and probably even use immutable caching).
|
|
149
|
-
export const getArchivesBackblazePublic = cache((domain: string) => {
|
|
150
|
-
return new ArchivesBackblaze({
|
|
151
|
-
bucketName: domain + "-public",
|
|
152
|
-
public: true,
|
|
153
|
-
cacheTime: timeInMinute,
|
|
154
|
-
allowedOrigins: [`https://${domain}`, `https://127-0-0-1.${domain}:7007`],
|
|
155
|
-
});
|
|
156
|
-
});
|
|
@@ -1,52 +0,0 @@
|
|
|
1
|
-
import { lazy } from "socket-function/src/caching";
|
|
2
|
-
import { Archives } from "./archives";
|
|
3
|
-
import { decodeCborx, encodeCborx } from "../misc/cloneHelpers";
|
|
4
|
-
import { ArchiveT } from "./archivesJSONT";
|
|
5
|
-
|
|
6
|
-
export function archiveCborT<T>(archives: () => Archives): ArchiveT<T> {
|
|
7
|
-
archives = lazy(archives);
|
|
8
|
-
async function get(key: string) {
|
|
9
|
-
let buffer = await archives().get(key);
|
|
10
|
-
if (!buffer) return undefined;
|
|
11
|
-
return decodeCborx(buffer) as T;
|
|
12
|
-
}
|
|
13
|
-
async function set(key: string, value: T) {
|
|
14
|
-
await archives().set(key, encodeCborx(value));
|
|
15
|
-
}
|
|
16
|
-
async function deleteFnc(key: string) {
|
|
17
|
-
await archives().del(key);
|
|
18
|
-
}
|
|
19
|
-
async function keys() {
|
|
20
|
-
return (await archives().find("")).map(value => value.toString());
|
|
21
|
-
}
|
|
22
|
-
async function values() {
|
|
23
|
-
let keysArray = await keys();
|
|
24
|
-
let results: T[] = [];
|
|
25
|
-
await Promise.all(keysArray.map(async key => {
|
|
26
|
-
let value = await get(key);
|
|
27
|
-
if (value) {
|
|
28
|
-
results.push(value);
|
|
29
|
-
}
|
|
30
|
-
}));
|
|
31
|
-
return results;
|
|
32
|
-
}
|
|
33
|
-
async function entries(): Promise<[string, T][]> {
|
|
34
|
-
let keysArray = await keys();
|
|
35
|
-
let results: [string, T][] = [];
|
|
36
|
-
await Promise.all(keysArray.map(async key => {
|
|
37
|
-
let value = await get(key);
|
|
38
|
-
if (value) {
|
|
39
|
-
results.push([key, value]);
|
|
40
|
-
}
|
|
41
|
-
}));
|
|
42
|
-
return results;
|
|
43
|
-
}
|
|
44
|
-
async function* asyncIterator(): AsyncIterator<[string, T]> {
|
|
45
|
-
for (let [key, value] of await entries()) {
|
|
46
|
-
yield [key, value];
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
return {
|
|
50
|
-
get, set, delete: deleteFnc, keys, values, entries, [Symbol.asyncIterator]: asyncIterator
|
|
51
|
-
};
|
|
52
|
-
}
|
|
@@ -1,307 +0,0 @@
|
|
|
1
|
-
import { formatNumber, formatTime } from "socket-function/src/formatting/format";
|
|
2
|
-
import { Archives } from "./archives";
|
|
3
|
-
import { cache } from "socket-function/src/caching";
|
|
4
|
-
import { measureFnc } from "socket-function/src/profiling/measure";
|
|
5
|
-
import { batchFunction, runInfinitePoll } from "socket-function/src/batching";
|
|
6
|
-
import { timeInHour } from "socket-function/src/misc";
|
|
7
|
-
|
|
8
|
-
interface FileInfo {
|
|
9
|
-
writeTime: number;
|
|
10
|
-
accessTime: number;
|
|
11
|
-
size: number;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
interface IndexData {
|
|
15
|
-
files: Record<string, FileInfo>;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
class ArchivesLimitedCache {
|
|
19
|
-
private baseArchives: Archives;
|
|
20
|
-
private maxFiles: number;
|
|
21
|
-
private maxSize: number;
|
|
22
|
-
private cache = new Map<string, FileInfo>();
|
|
23
|
-
private initialized = false;
|
|
24
|
-
private readonly indexPath = ".cache-index.json";
|
|
25
|
-
|
|
26
|
-
constructor(baseArchives: Archives, config: { maxFiles: number; maxSize: number }) {
|
|
27
|
-
this.baseArchives = baseArchives;
|
|
28
|
-
this.maxFiles = config.maxFiles;
|
|
29
|
-
this.maxSize = config.maxSize;
|
|
30
|
-
this.initOptionalMethods();
|
|
31
|
-
this.initPeriodicSync();
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
public getDebugName(): string {
|
|
35
|
-
return `limitedCache(${this.maxFiles}files,${Math.round(this.maxSize / (1024 * 1024))}MB)/${this.baseArchives.getDebugName()}`;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
@measureFnc
|
|
39
|
-
private async rebuildCacheFromFiles(): Promise<void> {
|
|
40
|
-
const allFiles = await this.baseArchives.findInfo("", { type: "files" });
|
|
41
|
-
const newCache = new Map<string, FileInfo>();
|
|
42
|
-
|
|
43
|
-
// Build map of actual files (excluding index file)
|
|
44
|
-
for (const file of allFiles) {
|
|
45
|
-
if (file.path === this.indexPath) continue;
|
|
46
|
-
|
|
47
|
-
newCache.set(file.path, {
|
|
48
|
-
writeTime: file.createTime,
|
|
49
|
-
accessTime: this.cache.get(file.path)?.accessTime ?? file.createTime,
|
|
50
|
-
size: file.size,
|
|
51
|
-
});
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
// Update cache with actual files
|
|
55
|
-
this.cache.clear();
|
|
56
|
-
for (const [path, info] of newCache.entries()) {
|
|
57
|
-
this.cache.set(path, info);
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
@measureFnc
|
|
62
|
-
private async ensureInitialized(): Promise<void> {
|
|
63
|
-
if (this.initialized) return;
|
|
64
|
-
|
|
65
|
-
// First try to load from index file
|
|
66
|
-
await this.loadIndex();
|
|
67
|
-
|
|
68
|
-
// If index is empty or missing, fall back to scanning all files
|
|
69
|
-
if (this.cache.size === 0) {
|
|
70
|
-
console.log("Index file missing or empty, scanning all files...");
|
|
71
|
-
await this.rebuildCacheFromFiles();
|
|
72
|
-
// Save the newly built index
|
|
73
|
-
await this.saveIndex();
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
this.initialized = true;
|
|
77
|
-
|
|
78
|
-
// Cleanup if we're already over limits
|
|
79
|
-
await this.applyLimits();
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
private async applyLimits(): Promise<void> {
|
|
83
|
-
const files = Array.from(this.cache.entries()).map(([path, info]) => ({
|
|
84
|
-
path,
|
|
85
|
-
...info,
|
|
86
|
-
}));
|
|
87
|
-
|
|
88
|
-
const totalSize = files.reduce((sum, file) => sum + file.size, 0);
|
|
89
|
-
const totalFiles = files.length;
|
|
90
|
-
|
|
91
|
-
if (totalFiles <= this.maxFiles && totalSize <= this.maxSize) {
|
|
92
|
-
return; // No cleanup needed
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
// Sort by access time (oldest first)
|
|
96
|
-
files.sort((a, b) => a.accessTime - b.accessTime);
|
|
97
|
-
|
|
98
|
-
let currentSize = totalSize;
|
|
99
|
-
let currentFiles = totalFiles;
|
|
100
|
-
|
|
101
|
-
// Remove files until we're under both limits
|
|
102
|
-
for (const file of files) {
|
|
103
|
-
if (currentFiles <= this.maxFiles && currentSize <= this.maxSize) {
|
|
104
|
-
break;
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
console.warn(`Deleting file ${file.path} to maintain limits. Currently have ${formatNumber(currentFiles)} files and ${formatNumber(currentSize)}B`);
|
|
108
|
-
try {
|
|
109
|
-
await this.baseArchives.del(file.path);
|
|
110
|
-
this.cache.delete(file.path);
|
|
111
|
-
currentSize -= file.size;
|
|
112
|
-
currentFiles--;
|
|
113
|
-
} catch (error) {
|
|
114
|
-
// File might already be deleted, continue
|
|
115
|
-
console.warn(`Failed to delete file ${file.path}:`, error);
|
|
116
|
-
this.cache.delete(file.path); // Remove from cache anyway
|
|
117
|
-
currentSize -= file.size;
|
|
118
|
-
currentFiles--;
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
// Trigger index flush if any files were deleted
|
|
123
|
-
if (currentFiles < totalFiles) {
|
|
124
|
-
this.triggerIndexFlush();
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
private updateAccessTime(path: string): void {
|
|
129
|
-
const info = this.cache.get(path);
|
|
130
|
-
if (info) {
|
|
131
|
-
info.accessTime = Date.now();
|
|
132
|
-
this.triggerIndexFlush();
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
public async get(path: string, config?: { range?: { start: number; end: number; }; retryCount?: number }): Promise<Buffer | undefined> {
|
|
137
|
-
await this.ensureInitialized();
|
|
138
|
-
this.updateAccessTime(path);
|
|
139
|
-
return this.baseArchives.get(path, config);
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
public async set(path: string, data: Buffer): Promise<void> {
|
|
143
|
-
await this.ensureInitialized();
|
|
144
|
-
// Limit before, not after, so we don't remove the file we just added
|
|
145
|
-
await this.applyLimits();
|
|
146
|
-
await this.baseArchives.set(path, data);
|
|
147
|
-
const now = Date.now();
|
|
148
|
-
this.cache.set(path, {
|
|
149
|
-
writeTime: now,
|
|
150
|
-
accessTime: now,
|
|
151
|
-
size: data.length,
|
|
152
|
-
});
|
|
153
|
-
this.triggerIndexFlush();
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
public async append(path: string, data: Buffer): Promise<void> {
|
|
157
|
-
await this.ensureInitialized();
|
|
158
|
-
await this.baseArchives.append(path, data);
|
|
159
|
-
// Update the cache info - increment size and update times
|
|
160
|
-
const info = this.cache.get(path);
|
|
161
|
-
const now = Date.now();
|
|
162
|
-
if (info) {
|
|
163
|
-
info.size += data.length;
|
|
164
|
-
info.writeTime = now;
|
|
165
|
-
info.accessTime = now;
|
|
166
|
-
} else {
|
|
167
|
-
// File didn't exist in cache, add it
|
|
168
|
-
this.cache.set(path, {
|
|
169
|
-
writeTime: now,
|
|
170
|
-
accessTime: now,
|
|
171
|
-
size: data.length,
|
|
172
|
-
});
|
|
173
|
-
}
|
|
174
|
-
this.triggerIndexFlush();
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
public async del(path: string): Promise<void> {
|
|
178
|
-
await this.ensureInitialized();
|
|
179
|
-
await this.baseArchives.del(path);
|
|
180
|
-
this.cache.delete(path);
|
|
181
|
-
this.triggerIndexFlush();
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
public async getInfo(path: string): Promise<{ writeTime: number; size: number; } | undefined> {
|
|
185
|
-
await this.ensureInitialized();
|
|
186
|
-
this.updateAccessTime(path);
|
|
187
|
-
return this.baseArchives.getInfo(path);
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
public async setLargeFile(config: { path: string; getNextData(): Promise<Buffer | undefined>; }): Promise<void> {
|
|
191
|
-
throw new Error(`setLargeFile in archivesLimitedCache is not supported`);
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
public async find(prefix: string, config?: { shallow?: boolean; type: "files" | "folders" }): Promise<string[]> {
|
|
195
|
-
await this.ensureInitialized();
|
|
196
|
-
return this.baseArchives.find(prefix, config);
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
public async findInfo(prefix: string, config?: { shallow?: boolean; type: "files" | "folders" }): Promise<{ path: string; createTime: number; size: number; }[]> {
|
|
200
|
-
await this.ensureInitialized();
|
|
201
|
-
return this.baseArchives.findInfo(prefix, config);
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
public async move(config: { path: string; target: Archives; targetPath: string }): Promise<void> {
|
|
205
|
-
throw new Error(`Move in archivesLimitedCache is not supported`);
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
public async copy(config: { path: string; target: Archives; targetPath: string }): Promise<void> {
|
|
209
|
-
throw new Error(`Copy in archivesLimitedCache is not supported`);
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
public enableLogging(): void {
|
|
213
|
-
this.baseArchives.enableLogging();
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
public async assertPathValid(path: string): Promise<void> {
|
|
217
|
-
await this.baseArchives.assertPathValid(path);
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
public getBaseArchives?: () => { parentPath: string; archives: Archives; } | undefined = () => {
|
|
221
|
-
const base = this.baseArchives.getBaseArchives?.();
|
|
222
|
-
if (base) {
|
|
223
|
-
return base;
|
|
224
|
-
}
|
|
225
|
-
return { parentPath: "", archives: this.baseArchives };
|
|
226
|
-
};
|
|
227
|
-
|
|
228
|
-
public getURL?: (path: string) => Promise<string>;
|
|
229
|
-
public getDownloadAuthorization?: (config: { validDurationInSeconds: number }) => Promise<{ bucketId: string; fileNamePrefix: string; authorizationToken: string; }>;
|
|
230
|
-
|
|
231
|
-
private batchedFlushIndex = batchFunction(
|
|
232
|
-
{ delay: 2000 }, // Flush index changes every 2 seconds
|
|
233
|
-
async () => {
|
|
234
|
-
// We don't actually need the batched values, just flush the index
|
|
235
|
-
await this.saveIndex();
|
|
236
|
-
}
|
|
237
|
-
);
|
|
238
|
-
|
|
239
|
-
private async loadIndex(): Promise<void> {
|
|
240
|
-
try {
|
|
241
|
-
const indexBuffer = await this.baseArchives.get(this.indexPath);
|
|
242
|
-
if (!indexBuffer) {
|
|
243
|
-
// No existing index, will need to build it
|
|
244
|
-
return;
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
const indexData: IndexData = JSON.parse(indexBuffer.toString("utf8"));
|
|
248
|
-
|
|
249
|
-
// Load the cache from the index
|
|
250
|
-
this.cache.clear();
|
|
251
|
-
for (const [path, info] of Object.entries(indexData.files)) {
|
|
252
|
-
this.cache.set(path, info);
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
console.log(`Loaded index with ${this.cache.size} files`);
|
|
256
|
-
} catch (error) {
|
|
257
|
-
console.warn(`Failed to load index file:`, error);
|
|
258
|
-
// Continue without index, will rebuild
|
|
259
|
-
}
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
private async saveIndex(): Promise<void> {
|
|
263
|
-
try {
|
|
264
|
-
const indexData: IndexData = {
|
|
265
|
-
files: Object.fromEntries(this.cache.entries())
|
|
266
|
-
};
|
|
267
|
-
|
|
268
|
-
const indexBuffer = Buffer.from(JSON.stringify(indexData), "utf8");
|
|
269
|
-
await this.baseArchives.set(this.indexPath, indexBuffer);
|
|
270
|
-
} catch (error) {
|
|
271
|
-
console.warn(`Failed to save index file:`, error);
|
|
272
|
-
}
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
private triggerIndexFlush(): void {
|
|
276
|
-
void this.batchedFlushIndex(undefined);
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
private initPeriodicSync(): void {
|
|
280
|
-
// Sync index with actual files every hour to prevent drift
|
|
281
|
-
runInfinitePoll(timeInHour, async () => {
|
|
282
|
-
await this.syncIndexWithFiles();
|
|
283
|
-
});
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
private async syncIndexWithFiles(): Promise<void> {
|
|
287
|
-
try {
|
|
288
|
-
console.log("Syncing index with actual files...");
|
|
289
|
-
await this.rebuildCacheFromFiles();
|
|
290
|
-
|
|
291
|
-
// Save updated index
|
|
292
|
-
await this.saveIndex();
|
|
293
|
-
console.log(`Index synced with ${this.cache.size} actual files`);
|
|
294
|
-
} catch (error) {
|
|
295
|
-
console.warn("Failed to sync index with files:", error);
|
|
296
|
-
}
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
private initOptionalMethods(): void {
|
|
300
|
-
this.getURL = this.baseArchives.getURL;
|
|
301
|
-
this.getDownloadAuthorization = this.baseArchives.getDownloadAuthorization;
|
|
302
|
-
}
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
export function createArchivesLimitedCache(baseArchives: Archives, config: { maxFiles: number; maxSize: number }): Archives {
|
|
306
|
-
return new ArchivesLimitedCache(baseArchives, config);
|
|
307
|
-
}
|