querysub 0.373.0 → 0.375.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/package.json +2 -4
- package/src/-f-node-discovery/NodeDiscovery.ts +2 -2
- package/src/0-path-value-core/PathValueCommitter.ts +1 -1
- package/src/0-path-value-core/PathValueController.ts +2 -2
- package/src/0-path-value-core/archiveLocks/ArchiveLocks2.ts +12 -12
- package/src/0-path-value-core/auditLogs.ts +1 -1
- package/src/0-path-value-core/pathValueCore.ts +2 -2
- package/src/3-path-functions/PathFunctionRunner.ts +1 -1
- package/src/3-path-functions/PathFunctionRunnerMain.ts +1 -1
- package/src/4-dom/qreact.tsx +2 -2
- package/src/4-querysub/QuerysubController.ts +1 -1
- package/src/5-diagnostics/diskValueAudit.ts +1 -1
- package/src/deployManager/components/MachineDetailPage.tsx +2 -5
- package/src/deployManager/components/ServiceDetailPage.tsx +2 -5
- package/src/deployManager/machineApplyMainCode.ts +7 -0
- package/src/diagnostics/NodeViewer.tsx +4 -5
- package/src/diagnostics/logs/IndexedLogs/BufferIndexHelpers.ts +1 -1
- package/src/diagnostics/logs/IndexedLogs/IndexedLogs.ts +7 -7
- package/src/diagnostics/logs/IndexedLogs/LogViewer3.tsx +221 -220
- package/src/diagnostics/logs/IndexedLogs/LogViewerParams.ts +21 -0
- package/src/diagnostics/logs/IndexedLogs/bufferMatcher.ts +3 -3
- package/src/diagnostics/logs/diskLogger.ts +1 -39
- package/src/diagnostics/logs/diskShimConsoleLogs.ts +2 -0
- package/src/diagnostics/logs/errorNotifications2/errorNotifications2.ts +3 -0
- package/src/diagnostics/logs/injectFileLocationToConsole.ts +3 -0
- package/src/diagnostics/logs/lifeCycleAnalysis/lifeCycles.tsx +32 -22
- package/src/diagnostics/managementPages.tsx +0 -18
- package/src/diagnostics/watchdog.ts +1 -1
- package/src/user-implementation/userData.ts +3 -3
- package/test.ts +0 -5
- package/bin/error-email.js +0 -8
- package/bin/error-im.js +0 -8
- package/src/diagnostics/logs/FastArchiveAppendable.ts +0 -843
- package/src/diagnostics/logs/FastArchiveController.ts +0 -573
- package/src/diagnostics/logs/FastArchiveViewer.tsx +0 -1090
- package/src/diagnostics/logs/LogViewer2.tsx +0 -552
- package/src/diagnostics/logs/errorNotifications/ErrorDigestPage.tsx +0 -409
- package/src/diagnostics/logs/errorNotifications/ErrorNotificationController.ts +0 -756
- package/src/diagnostics/logs/errorNotifications/ErrorSuppressionUI.tsx +0 -280
- package/src/diagnostics/logs/errorNotifications/ErrorWarning.tsx +0 -254
- package/src/diagnostics/logs/errorNotifications/errorDigestEmail.tsx +0 -233
- package/src/diagnostics/logs/errorNotifications/errorDigestEntry.tsx +0 -14
- package/src/diagnostics/logs/errorNotifications/errorDigests.tsx +0 -292
- package/src/diagnostics/logs/errorNotifications/errorWatchEntry.tsx +0 -209
- package/src/diagnostics/logs/importLogsEntry.ts +0 -38
- package/src/diagnostics/logs/lifeCycleAnalysis/LifeCyclePages.tsx +0 -150
- package/src/diagnostics/logs/logViewerExtractField.ts +0 -36
|
@@ -1,843 +0,0 @@
|
|
|
1
|
-
module.hotreload = true;
|
|
2
|
-
module.noserverhotreload = false;
|
|
3
|
-
import { measureBlock, measureFnc, measureWrap } from "socket-function/src/profiling/measure";
|
|
4
|
-
import { getMachineId, getOwnMachineId } from "../../-a-auth/certs";
|
|
5
|
-
import { isDefined, parseFileNameKVP, parsePath, partialCopyObject, streamToIteratable, sum, toFileNameKVP } from "../../misc";
|
|
6
|
-
import { registerShutdownHandler } from "../periodic";
|
|
7
|
-
import { batchFunction, delay, runInSerial, runInfinitePoll, runInfinitePollCallAtStart } from "socket-function/src/batching";
|
|
8
|
-
import { PromiseObj, isNode, keyByArray, list, nextId, sort, timeInDay, timeInHour, timeInMinute } from "socket-function/src/misc";
|
|
9
|
-
import os from "os";
|
|
10
|
-
import { getOwnThreadId } from "../../-f-node-discovery/NodeDiscovery";
|
|
11
|
-
import fs from "fs";
|
|
12
|
-
import { MaybePromise, canHaveChildren } from "socket-function/src/types";
|
|
13
|
-
import { formatNumber, formatTime } from "socket-function/src/formatting/format";
|
|
14
|
-
import { cache, lazy } from "socket-function/src/caching";
|
|
15
|
-
import { Archives, getArchives, nestArchives } from "../../-a-archives/archives";
|
|
16
|
-
import { Zip } from "../../zip";
|
|
17
|
-
import { SocketFunction } from "socket-function/SocketFunction";
|
|
18
|
-
import { assertIsManagementUser } from "../managementPages";
|
|
19
|
-
import { getControllerNodeIdList } from "../../-g-core-values/NodeCapabilities";
|
|
20
|
-
import { errorToUndefined, ignoreErrors, timeoutToUndefinedSilent } from "../../errors";
|
|
21
|
-
import { getCallObj } from "socket-function/src/nodeProxy";
|
|
22
|
-
import { getSyncedController } from "../../library-components/SyncedController";
|
|
23
|
-
import { getBrowserUrlNode, getOwnNodeId } from "../../-f-node-discovery/NodeDiscovery";
|
|
24
|
-
import { secureRandom, shuffle } from "../../misc/random";
|
|
25
|
-
import { getPathIndex, getPathStr2 } from "../../path";
|
|
26
|
-
import { onNextPaint } from "../../functional/onNextPaint";
|
|
27
|
-
import { getArchivesBackblazePrivateImmutable, getArchivesBackblazePublicImmutable } from "../../-a-archives/archivesBackBlaze";
|
|
28
|
-
import { httpsRequest } from "socket-function/src/https";
|
|
29
|
-
import { getDomain, isPublic } from "../../config";
|
|
30
|
-
import { getIPDomain } from "../../-e-certs/EdgeCertController";
|
|
31
|
-
import { getArchivesPrivateFileSystem } from "../../-a-archives/archivesPrivateFileSystem";
|
|
32
|
-
import { createArchivesLimitedCache } from "../../-a-archives/archivesLimitedCache";
|
|
33
|
-
import { sha256 } from "js-sha256";
|
|
34
|
-
import { assertIsNetworkTrusted } from "../../-d-trust/NetworkTrust2";
|
|
35
|
-
import { blue, magenta } from "socket-function/src/formatting/logColors";
|
|
36
|
-
import { FileMetadata, FastArchiveAppendableControllerBase, FastArchiveAppendableController, getFileMetadataHash } from "./FastArchiveController";
|
|
37
|
-
import { fsExistsAsync } from "../../fs";
|
|
38
|
-
import { ScanFnc } from "./FastArchiveViewer";
|
|
39
|
-
import { getArchivesLocal } from "../../-a-archives/archivesDisk";
|
|
40
|
-
|
|
41
|
-
// NOTE: In a single command line micro-test it looks like we can write about 40K writes of 500 per once, when using 10X parallel, on a fairly potato server. We should probably batch though, and only do 1X parallel.
|
|
42
|
-
/*
|
|
43
|
-
Append Benchmarks
|
|
44
|
-
10 processes
|
|
45
|
-
Windows = 100K/S
|
|
46
|
-
Linux Digital Ocean = 1M/S
|
|
47
|
-
Linux PI SD Card = 300K/S
|
|
48
|
-
Linux PI USB = 300K/S
|
|
49
|
-
1 process
|
|
50
|
-
Windows = 40K/S
|
|
51
|
-
Linux Digital Ocean = 685K/S
|
|
52
|
-
Linux PI = 200K/S
|
|
53
|
-
|
|
54
|
-
rm test.txt
|
|
55
|
-
for i in {0..9}; do node -e 'const fs=require("fs");const id='$i';let i=0;const start=Date.now();while(Date.now()-start<5000){fs.appendFileSync("test.txt", `${id},${i++}\n`)}' & done; wait
|
|
56
|
-
node -e 'const fs=require("fs");const seqs=new Map();fs.readFileSync("test.txt","utf8").trim().split("\n").forEach((l,i)=>{const[id,seq]=l.split(",").map(Number);if(!seqs.has(id))seqs.set(id,{last:-1,errs:0});const s=seqs.get(id);if(seq!==s.last+1){console.error(`Error for id ${id} at line ${i}: ${s.last}->${seq}`);s.errs++}s.last=seq});seqs.forEach((v,id)=>console.log(`ID ${id}: final seq ${v.last}, ${v.errs} gaps`))'
|
|
57
|
-
*/
|
|
58
|
-
|
|
59
|
-
const UNCOMPRESSED_LOG_FILE_WARN_THRESHOLD = 1024 * 1024 * 512;
|
|
60
|
-
const UNCOMPRESSED_LOG_FILE_STOP_THRESHOLD = 1024 * 1024 * 1024 * 2;
|
|
61
|
-
|
|
62
|
-
// Add a large wait, due to day light saving time, or whatever
|
|
63
|
-
const UPLOAD_THRESHOLD = timeInHour * 1.5;
|
|
64
|
-
const DEAD_TIMEOUT = timeInHour * 6;
|
|
65
|
-
const DELETE_TIMEOUT = timeInHour * 12;
|
|
66
|
-
|
|
67
|
-
const MAX_WORK_PER_PAINT = 40;
|
|
68
|
-
|
|
69
|
-
const ON_DATA_BATCH_COUNT = 1024 * 10;
|
|
70
|
-
//const ON_DATA_BATCH_COUNT = 1;
|
|
71
|
-
|
|
72
|
-
// NOTE: This used to be small, for atomic writes, but it causes lag, and... why do we need atomic writes? I believe we use a file per thread, so... we will be the only writer.
|
|
73
|
-
const WRITE_CHUNK_SIZE = 1024 * 1024 * 16;
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
const MAX_LOCAL_CACHED_FILES = 1000 * 10;
|
|
78
|
-
const MAX_LOCAL_CACHED_SIZE = 1024 * 1024 * 1024 * 10;
|
|
79
|
-
|
|
80
|
-
const getFileCache = cache((rootPath: string) => {
|
|
81
|
-
let data = getArchivesPrivateFileSystem(rootPath);
|
|
82
|
-
return createArchivesLimitedCache(data, {
|
|
83
|
-
maxFiles: MAX_LOCAL_CACHED_FILES,
|
|
84
|
-
maxSize: MAX_LOCAL_CACHED_SIZE,
|
|
85
|
-
});
|
|
86
|
-
});
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
// IMPORTANT! Use these like this, with one object per type of log
|
|
90
|
-
// const errorAppendable = new FastArchiveAppendable<LogObject>("logs/error/");
|
|
91
|
-
// const warnAppendable = new FastArchiveAppendable<LogObject>("logs/warn/");
|
|
92
|
-
// const infoAppendable = new FastArchiveAppendable<LogObject>("logs/info/");
|
|
93
|
-
// const logsAppendable = new FastArchiveAppendable<LogObject>("logs/logs/");
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
// NOTE: We don't unescape. Because it massively slows down encoding, and... our delimitter is unlikely to randomly appear. IF it does, then it is likely not structural, and so the data will only look a bit weird (as in, it is likely not a length that cbor uses to decode or anything like that.
|
|
97
|
-
export const objectDelimitterBuffer = Buffer.from([0x253, 0xe5, 0x05, 0x199, 0x5c, 0xbb, 0x63, 0x251]);
|
|
98
|
-
|
|
99
|
-
export type DatumStats = {
|
|
100
|
-
matchedSize: number;
|
|
101
|
-
notMatchedSize: number;
|
|
102
|
-
errors: number;
|
|
103
|
-
matchedCount: number;
|
|
104
|
-
notMatchedCount: number;
|
|
105
|
-
};
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
export function getFileTimeStamp(path: string): {
|
|
109
|
-
startTime: number;
|
|
110
|
-
endTime: number;
|
|
111
|
-
} {
|
|
112
|
-
let file = path.replaceAll("\\", "/").split("/").at(-1)!;
|
|
113
|
-
// Remove .log extension and parse as ISO date
|
|
114
|
-
let dateStr = file.replace(/\.log$/, "");
|
|
115
|
-
// Add missing parts to make it a valid ISO string: "2025-09-06T07" -> "2025-09-06T07:00:00.000Z"
|
|
116
|
-
if (dateStr.length === 13) { // YYYY-MM-DDTHH format
|
|
117
|
-
dateStr += ":00:00.000Z";
|
|
118
|
-
}
|
|
119
|
-
let startTime = new Date(dateStr).getTime();
|
|
120
|
-
if (!startTime) {
|
|
121
|
-
let pathParts = path.split("/");
|
|
122
|
-
let year = parseInt(pathParts[0], 10);
|
|
123
|
-
let month = parseInt(pathParts[1], 10) - 1;
|
|
124
|
-
let day = parseInt(pathParts[2], 10);
|
|
125
|
-
let hour = parseInt(pathParts[3], 10);
|
|
126
|
-
let hourStart = Date.UTC(year, month, day, hour);
|
|
127
|
-
startTime = hourStart;
|
|
128
|
-
}
|
|
129
|
-
return {
|
|
130
|
-
startTime,
|
|
131
|
-
endTime: startTime + timeInHour,
|
|
132
|
-
};
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
// NOTE: Global, to prevent hot reloading, or redundant FastArchiveAppendable, from breaking things.
|
|
137
|
-
const appendableSerialLock = (globalThis as any).appendableSerialLock ?? runInSerial(async (fnc: () => Promise<void>) => {
|
|
138
|
-
await fnc();
|
|
139
|
-
});
|
|
140
|
-
(globalThis as any).appendableSerialLock = appendableSerialLock;
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
export class FastArchiveAppendable<Datum> {
|
|
144
|
-
private lastSizeWarningTime = 0;
|
|
145
|
-
|
|
146
|
-
public constructor(public rootPath: string) {
|
|
147
|
-
if (!this.rootPath.endsWith("/")) {
|
|
148
|
-
this.rootPath += "/";
|
|
149
|
-
}
|
|
150
|
-
if (isNode()) {
|
|
151
|
-
registerShutdownHandler(async () => {
|
|
152
|
-
await this.flushNow();
|
|
153
|
-
});
|
|
154
|
-
runInfinitePoll(timeInMinute, async () => {
|
|
155
|
-
await this.flushNow();
|
|
156
|
-
});
|
|
157
|
-
// Random, to try to prevent the dead file detection code from getting in sync. It's fine if it gets in sync, it's just inefficient as we'll have multiple services uploading the same file to the same location at the same time.
|
|
158
|
-
void runInfinitePoll(timeInMinute * 20 + Math.random() * timeInMinute * 5, async () => {
|
|
159
|
-
await this.moveLogsToBackblaze();
|
|
160
|
-
});
|
|
161
|
-
}
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
public getArchives = cache((forceGetPublic: boolean) => {
|
|
165
|
-
let archives: Archives;
|
|
166
|
-
if (!isPublic() && !forceGetPublic) {
|
|
167
|
-
archives = getArchivesLocal(getDomain());
|
|
168
|
-
} else {
|
|
169
|
-
archives = getArchivesBackblazePrivateImmutable(getDomain());
|
|
170
|
-
}
|
|
171
|
-
return nestArchives("fast-logs/" + this.rootPath, archives);
|
|
172
|
-
});
|
|
173
|
-
|
|
174
|
-
public baseGetLocalPathRoot = () => {
|
|
175
|
-
return (
|
|
176
|
-
os.homedir()
|
|
177
|
-
+ "/fast-log-cache/"
|
|
178
|
-
+ getDomain() + "/"
|
|
179
|
-
+ this.rootPath
|
|
180
|
-
);
|
|
181
|
-
};
|
|
182
|
-
public getLocalPathRoot = lazy(() => {
|
|
183
|
-
let path = this.baseGetLocalPathRoot();
|
|
184
|
-
if (!fs.existsSync(path)) {
|
|
185
|
-
fs.mkdirSync(path, { recursive: true });
|
|
186
|
-
}
|
|
187
|
-
return path;
|
|
188
|
-
});
|
|
189
|
-
|
|
190
|
-
// NOTE: Batching is both faster, and allows a lot of the code to log (which will cause an append) without causing an infinite loop.
|
|
191
|
-
private pendingWriteQueue: unknown[] = [];
|
|
192
|
-
@measureFnc
|
|
193
|
-
private escapeDelimitter(data: Buffer) {
|
|
194
|
-
if (!data.includes(objectDelimitterBuffer)) return data;
|
|
195
|
-
|
|
196
|
-
let startIndex = 0;
|
|
197
|
-
while (true) {
|
|
198
|
-
let index = data.indexOf(objectDelimitterBuffer, startIndex);
|
|
199
|
-
if (index === -1) break;
|
|
200
|
-
data[index]++;
|
|
201
|
-
startIndex = index;
|
|
202
|
-
}
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
private insideAppend = false;
|
|
207
|
-
/** NOTE: If the input data might contain user data, or mutated, use partialCopyObject first, to make a copy and prevent it from being excessively large. */
|
|
208
|
-
@measureFnc
|
|
209
|
-
public append(data: Datum) {
|
|
210
|
-
// Hmm... so...
|
|
211
|
-
if (!isNode()) return;
|
|
212
|
-
// Just from logging, so... ignore it
|
|
213
|
-
if (this.insideAppend) return;
|
|
214
|
-
this.insideAppend = true;
|
|
215
|
-
try {
|
|
216
|
-
this.pendingWriteQueue.push(data);
|
|
217
|
-
} finally {
|
|
218
|
-
this.insideAppend = false;
|
|
219
|
-
}
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
// NOTE: No timing on this as it's just waiting on the disk. It's not actually slowing anything down.
|
|
223
|
-
public async flushNow(now = Date.now()) {
|
|
224
|
-
|
|
225
|
-
await appendableSerialLock(async () => {
|
|
226
|
-
if (this.pendingWriteQueue.length === 0) return;
|
|
227
|
-
|
|
228
|
-
// 2025-09-06T07
|
|
229
|
-
let hourFile = new Date(now).toISOString().slice(0, 13) + ".log";
|
|
230
|
-
let localCacheFolder = this.getLocalPathRoot() + getOwnThreadId() + "/";
|
|
231
|
-
let localCachePath = localCacheFolder + hourFile;
|
|
232
|
-
await fs.promises.mkdir(localCacheFolder, { recursive: true });
|
|
233
|
-
// Always heartbeat
|
|
234
|
-
await fs.promises.writeFile(localCacheFolder + "heartbeat", Buffer.from(now + ""));
|
|
235
|
-
|
|
236
|
-
try {
|
|
237
|
-
let beforeSize = await fs.promises.stat(localCachePath);
|
|
238
|
-
if (beforeSize.size > UNCOMPRESSED_LOG_FILE_STOP_THRESHOLD) {
|
|
239
|
-
console.error(`FastArchiveAppendable: ${localCachePath} too large, refusing to add more data to it, current size ${formatNumber(beforeSize.size)}B > ${formatNumber(UNCOMPRESSED_LOG_FILE_STOP_THRESHOLD)}B`);
|
|
240
|
-
return;
|
|
241
|
-
}
|
|
242
|
-
} catch {
|
|
243
|
-
// File not existing is fine
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
let chunks: Buffer[][] = [];
|
|
247
|
-
measureBlock(() => {
|
|
248
|
-
// NOTE: We can't use anything but JSON, as we need it to be scannable before decoding it (otherwise scanning takes 100X longer)
|
|
249
|
-
let writeData = this.pendingWriteQueue.map(v => {
|
|
250
|
-
let buffer = Buffer.from(JSON.stringify(v));
|
|
251
|
-
this.escapeDelimitter(buffer);
|
|
252
|
-
return buffer;
|
|
253
|
-
});
|
|
254
|
-
this.pendingWriteQueue = [];
|
|
255
|
-
|
|
256
|
-
// Group lines into WRITE_ATOMIC_LIMIT byte chunks
|
|
257
|
-
|
|
258
|
-
let currentChunk: Buffer[] = [];
|
|
259
|
-
let currentSize = 0;
|
|
260
|
-
for (let line of writeData) {
|
|
261
|
-
if (currentSize + line.length + objectDelimitterBuffer.length > WRITE_CHUNK_SIZE && currentChunk.length > 0) {
|
|
262
|
-
chunks.push(currentChunk);
|
|
263
|
-
currentChunk = [];
|
|
264
|
-
currentSize = 0;
|
|
265
|
-
}
|
|
266
|
-
currentChunk.push(line);
|
|
267
|
-
currentSize += line.length;
|
|
268
|
-
currentChunk.push(objectDelimitterBuffer);
|
|
269
|
-
currentSize += objectDelimitterBuffer.length;
|
|
270
|
-
}
|
|
271
|
-
if (currentChunk.length > 0) {
|
|
272
|
-
chunks.push(currentChunk);
|
|
273
|
-
}
|
|
274
|
-
}, `FastArchiveAppendable|serialize log data`);
|
|
275
|
-
|
|
276
|
-
for (let chunk of chunks) {
|
|
277
|
-
await fs.promises.appendFile(localCachePath, Buffer.concat(chunk));
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
let finalSize = await fs.promises.stat(localCachePath);
|
|
281
|
-
if (finalSize.size > UNCOMPRESSED_LOG_FILE_WARN_THRESHOLD) {
|
|
282
|
-
const now = Date.now();
|
|
283
|
-
const timeSinceLastWarning = now - this.lastSizeWarningTime;
|
|
284
|
-
|
|
285
|
-
if (timeSinceLastWarning >= timeInMinute * 15) {
|
|
286
|
-
console.error(`FastArchiveAppendable: ${localCachePath} is getting very big. This might cause logging to be disabled, to maintain readable of the logs (which will be required to debug why we are writing so much data! Current size ${formatNumber(finalSize.size)}B > ${formatNumber(UNCOMPRESSED_LOG_FILE_WARN_THRESHOLD)}B)`);
|
|
287
|
-
this.lastSizeWarningTime = now;
|
|
288
|
-
}
|
|
289
|
-
}
|
|
290
|
-
});
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
public static getBackblazePath(config: { fileName: string; threadId: string }): string {
|
|
295
|
-
// 2025-09-06T07
|
|
296
|
-
let mainName = config.fileName.replace(/\.log$/, "");
|
|
297
|
-
let [year, month, day, hour] = mainName.split(/[-T:]/);
|
|
298
|
-
return `${year}/${month}/${day}/${hour}/${config.threadId}.log`;
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
public async moveLogsToBackblaze() {
|
|
302
|
-
await appendableSerialLock(async () => {
|
|
303
|
-
let rootCacheFolder = this.baseGetLocalPathRoot();
|
|
304
|
-
if (!await fsExistsAsync(rootCacheFolder)) return;
|
|
305
|
-
console.log(magenta(`Moving old logs to Backblaze from ${rootCacheFolder}`));
|
|
306
|
-
// Ugh... what is this even? Is it... hot reloading?
|
|
307
|
-
if (rootCacheFolder.includes("undefined/undefined/undefined/")) {
|
|
308
|
-
require("debugbreak")(2);
|
|
309
|
-
debugger;
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
let archives = this.getArchives(false);
|
|
313
|
-
async function moveLogsForFolder(threadId: string) {
|
|
314
|
-
let threadDir = rootCacheFolder + threadId + "/";
|
|
315
|
-
if (!await fsExistsAsync(threadDir)) return;
|
|
316
|
-
let files = await fs.promises.readdir(threadDir);
|
|
317
|
-
// Shuffle, so if we do run multiple at the same time, we are less likely to use the same files at the same time.
|
|
318
|
-
files = shuffle(files, Math.random());
|
|
319
|
-
for (let file of files) {
|
|
320
|
-
if (file === "heartbeat") continue;
|
|
321
|
-
let fullPath = threadDir + file;
|
|
322
|
-
try {
|
|
323
|
-
if (!await fsExistsAsync(fullPath)) continue;
|
|
324
|
-
// We could use modified time here? Although, this is nice if we move files around, and then manually have them moved, although even then... this could cause problem be tripping while we are copying the file, so... maybe this is just wrong?
|
|
325
|
-
let timeStamp = getFileTimeStamp(fullPath);
|
|
326
|
-
if (timeStamp.endTime > Date.now() - UPLOAD_THRESHOLD) continue;
|
|
327
|
-
|
|
328
|
-
// NOTE: Because we use the same target path, if multiple services do this at the same time it's fine. Not great, but... fine.
|
|
329
|
-
let backblazePath = FastArchiveAppendable.getBackblazePath({ fileName: file, threadId });
|
|
330
|
-
console.log(magenta(`Moving ${fullPath} to Backblaze as ${backblazePath}`));
|
|
331
|
-
|
|
332
|
-
let data = await measureBlock(async () => fs.promises.readFile(fullPath), "FastArchiveAppendable|readBeforeUploading");
|
|
333
|
-
let compressed = await measureBlock(async () => Zip.gzip(data), "FastArchiveAppendable|compress");
|
|
334
|
-
console.log(`Uploading ${formatNumber(data.length)}B (compressed to ${formatNumber(compressed.length)}B) logs to ${backblazePath} from ${fullPath}`);
|
|
335
|
-
await archives.set(backblazePath, compressed);
|
|
336
|
-
// Ignore unlink errors to reduce excess logging. This races on startup, so it is likely we'll hit this a fair amount (especially because archives.set is so slow)
|
|
337
|
-
try {
|
|
338
|
-
await fs.promises.unlink(fullPath);
|
|
339
|
-
} catch { }
|
|
340
|
-
} catch (e: any) {
|
|
341
|
-
// Just skip it, if the first file in the directory is broken we don't want to never move any files
|
|
342
|
-
console.error(`Error moving log file ${fullPath}: ${e.stack}`);
|
|
343
|
-
}
|
|
344
|
-
}
|
|
345
|
-
}
|
|
346
|
-
|
|
347
|
-
await moveLogsForFolder(getOwnThreadId());
|
|
348
|
-
let allFolders = await fs.promises.readdir(rootCacheFolder);
|
|
349
|
-
for (let threadId of allFolders) {
|
|
350
|
-
if (threadId === getOwnThreadId()) continue;
|
|
351
|
-
|
|
352
|
-
let heartbeat = 0;
|
|
353
|
-
try {
|
|
354
|
-
let heartbeatStr = await fs.promises.readFile(rootCacheFolder + threadId + "/heartbeat", "utf8");
|
|
355
|
-
heartbeat = Number(heartbeatStr);
|
|
356
|
-
} catch { }
|
|
357
|
-
if (heartbeat < Date.now() - DEAD_TIMEOUT) {
|
|
358
|
-
await moveLogsForFolder(threadId);
|
|
359
|
-
}
|
|
360
|
-
if (heartbeat < Date.now() - DELETE_TIMEOUT) {
|
|
361
|
-
await fs.promises.rmdir(rootCacheFolder + threadId, { recursive: true });
|
|
362
|
-
}
|
|
363
|
-
}
|
|
364
|
-
});
|
|
365
|
-
}
|
|
366
|
-
|
|
367
|
-
private lastSynchronize: {
|
|
368
|
-
stopSynchronize: () => void;
|
|
369
|
-
parametersHash: string;
|
|
370
|
-
parametersResult: PromiseObj<{
|
|
371
|
-
files: FileMetadata[];
|
|
372
|
-
}>;
|
|
373
|
-
} | undefined;
|
|
374
|
-
public cancelAllSynchronizes() {
|
|
375
|
-
// We miss some cases here, but... it's probably fine. This should usually cancel.
|
|
376
|
-
this.lastSynchronize?.stopSynchronize();
|
|
377
|
-
this.lastSynchronize = undefined;
|
|
378
|
-
}
|
|
379
|
-
/** If called cancels the previous outstanding synchronize. */
|
|
380
|
-
public async synchronizeData(config: {
|
|
381
|
-
range: {
|
|
382
|
-
startTime: number;
|
|
383
|
-
endTime: number;
|
|
384
|
-
};
|
|
385
|
-
cacheBust: number;
|
|
386
|
-
forceGetPublic?: boolean;
|
|
387
|
-
scanFnc?: ScanFnc;
|
|
388
|
-
getWantData?: (file: FileMetadata) => Promise<((posStart: number, posEnd: number, data: Buffer) => boolean) | undefined>;
|
|
389
|
-
onData: (datum: Datum[], file: FileMetadata) => void;
|
|
390
|
-
// Called after onData
|
|
391
|
-
onStats?: (stats: DatumStats, file: FileMetadata) => void;
|
|
392
|
-
onError?: (error: Error, file: FileMetadata) => void;
|
|
393
|
-
onFinish?: () => void;
|
|
394
|
-
|
|
395
|
-
onProgress?: (progress: {
|
|
396
|
-
section: string;
|
|
397
|
-
value: number;
|
|
398
|
-
max: number;
|
|
399
|
-
}) => void;
|
|
400
|
-
}): Promise<{
|
|
401
|
-
metadata: {
|
|
402
|
-
files: FileMetadata[];
|
|
403
|
-
};
|
|
404
|
-
stopSynchronize: () => void;
|
|
405
|
-
} | "cancelled"> {
|
|
406
|
-
let { onData, onStats, onError } = config;
|
|
407
|
-
// Create unique client sync ID upfront
|
|
408
|
-
let syncId = nextId();
|
|
409
|
-
|
|
410
|
-
console.log(`Synchronizing ${this.rootPath} with syncId ${syncId}`, config);
|
|
411
|
-
|
|
412
|
-
let isPublicValue = isPublic() || config.forceGetPublic || false;
|
|
413
|
-
|
|
414
|
-
// Register progress callback immediately so we can receive progress during setup
|
|
415
|
-
// - It also helps with cancellation
|
|
416
|
-
FastArchiveAppendableControllerBase.progressCallbacks.set(syncId, config.onProgress ?? (() => { }));
|
|
417
|
-
|
|
418
|
-
let stopped = false;
|
|
419
|
-
let stoppedPromise = new PromiseObj<void>();
|
|
420
|
-
const stopSynchronize = () => {
|
|
421
|
-
stoppedPromise.resolve();
|
|
422
|
-
// Wait a bit for trailing progress, as progress is batched and delayed
|
|
423
|
-
setTimeout(() => {
|
|
424
|
-
stopped = true;
|
|
425
|
-
FastArchiveAppendableControllerBase.progressCallbacks.delete(syncId);
|
|
426
|
-
}, 5000);
|
|
427
|
-
};
|
|
428
|
-
|
|
429
|
-
let parametersHash = sha256(JSON.stringify({
|
|
430
|
-
range: config.range,
|
|
431
|
-
isPublicValue,
|
|
432
|
-
cacheBust: config.cacheBust,
|
|
433
|
-
version: 1,
|
|
434
|
-
})) + ".parameters";
|
|
435
|
-
|
|
436
|
-
let synchronizeObj = {
|
|
437
|
-
stopSynchronize,
|
|
438
|
-
parametersHash,
|
|
439
|
-
parametersResult: new PromiseObj<{
|
|
440
|
-
files: FileMetadata[];
|
|
441
|
-
}>(),
|
|
442
|
-
};
|
|
443
|
-
let last = this.lastSynchronize;
|
|
444
|
-
// Wait for the last one to finish getting the parameters, as we'll use the same ones.
|
|
445
|
-
if (last?.parametersHash === parametersHash) {
|
|
446
|
-
await last.parametersResult.promise;
|
|
447
|
-
// Another call happened before we finished
|
|
448
|
-
if (this.lastSynchronize !== last) return "cancelled";
|
|
449
|
-
}
|
|
450
|
-
this.lastSynchronize?.stopSynchronize();
|
|
451
|
-
|
|
452
|
-
this.lastSynchronize = synchronizeObj;
|
|
453
|
-
|
|
454
|
-
let baseOnProgress = config.onProgress;
|
|
455
|
-
let timeOfLastPaint = Date.now();
|
|
456
|
-
let throttlePerPaint = runInSerial(async () => {
|
|
457
|
-
let now = Date.now();
|
|
458
|
-
let workDone = now - timeOfLastPaint;
|
|
459
|
-
if (workDone < MAX_WORK_PER_PAINT) return;
|
|
460
|
-
await onNextPaint();
|
|
461
|
-
timeOfLastPaint = now;
|
|
462
|
-
});
|
|
463
|
-
|
|
464
|
-
let createProgress = async (section: string, max: number) => {
|
|
465
|
-
section = `${this.rootPath}|${section}`;
|
|
466
|
-
let cancelled: Error | undefined;
|
|
467
|
-
let lastValue = 0;
|
|
468
|
-
let baseBatch = batchFunction({ delay: 150, },
|
|
469
|
-
async (config: { value: number, overrideMax?: number }[]) => {
|
|
470
|
-
if (cancelled) return;
|
|
471
|
-
if (stopped) {
|
|
472
|
-
cancelled = new Error(`Synchronization stopped`);
|
|
473
|
-
return;
|
|
474
|
-
}
|
|
475
|
-
|
|
476
|
-
let value = config.at(-1)!.value;
|
|
477
|
-
lastValue = value;
|
|
478
|
-
let usedMax = config.map(c => c.overrideMax).filter(isDefined).at(-1) ?? max;
|
|
479
|
-
value = Math.min(value, usedMax);
|
|
480
|
-
baseOnProgress?.({ section, value, max: usedMax });
|
|
481
|
-
}
|
|
482
|
-
);
|
|
483
|
-
let deltaBatch = batchFunction({ delay: 150, },
|
|
484
|
-
async (config: { value: number, max: number }[]) => {
|
|
485
|
-
if (cancelled) return;
|
|
486
|
-
if (stopped) {
|
|
487
|
-
cancelled = new Error(`Synchronization stopped`);
|
|
488
|
-
return;
|
|
489
|
-
}
|
|
490
|
-
for (let delta of config) {
|
|
491
|
-
lastValue += delta.value;
|
|
492
|
-
max += delta.max;
|
|
493
|
-
}
|
|
494
|
-
baseOnProgress?.({ section, value: lastValue, max });
|
|
495
|
-
}
|
|
496
|
-
);
|
|
497
|
-
let firstCall = true;
|
|
498
|
-
let onProgress = async (value: number, overrideMax?: number, delta?: boolean) => {
|
|
499
|
-
// Call it immediately, so the output order isn't broken by the batching delay varying slightly
|
|
500
|
-
if (firstCall) {
|
|
501
|
-
firstCall = false;
|
|
502
|
-
baseOnProgress?.({ section, value: 0, max });
|
|
503
|
-
}
|
|
504
|
-
if (cancelled) throw cancelled;
|
|
505
|
-
if (delta) {
|
|
506
|
-
void deltaBatch({ value, max: overrideMax ?? 0 });
|
|
507
|
-
} else {
|
|
508
|
-
void baseBatch({ value, overrideMax });
|
|
509
|
-
}
|
|
510
|
-
await throttlePerPaint();
|
|
511
|
-
};
|
|
512
|
-
// Ordering is better if we wait until the first progress to send any progress
|
|
513
|
-
//await onProgress(0);
|
|
514
|
-
return onProgress;
|
|
515
|
-
};
|
|
516
|
-
|
|
517
|
-
try {
|
|
518
|
-
const localCache = getFileCache(this.rootPath);
|
|
519
|
-
|
|
520
|
-
let syncResult: { files: FileMetadata[]; createTime?: number; } | undefined;
|
|
521
|
-
let cachedSyncResultBuffer = await localCache.get(parametersHash);
|
|
522
|
-
let downloadSyncId: string = "";
|
|
523
|
-
let controller = FastArchiveAppendableController.nodes[getBrowserUrlNode()];
|
|
524
|
-
if (cachedSyncResultBuffer?.length) {
|
|
525
|
-
try {
|
|
526
|
-
syncResult = JSON.parse(cachedSyncResultBuffer.toString());
|
|
527
|
-
downloadSyncId = await controller.createSyncSession();
|
|
528
|
-
} catch (e: any) {
|
|
529
|
-
console.error(`Failed to parsed cached sync result, synchronizing from scratch instead\n${e.stack}`);
|
|
530
|
-
}
|
|
531
|
-
}
|
|
532
|
-
if (!syncResult) {
|
|
533
|
-
let findingFiles = await createProgress("Finding files", 0);
|
|
534
|
-
syncResult = await controller.startSynchronize({
|
|
535
|
-
syncId,
|
|
536
|
-
range: config.range,
|
|
537
|
-
rootPath: this.rootPath,
|
|
538
|
-
forceGetPublic: config.forceGetPublic,
|
|
539
|
-
});
|
|
540
|
-
syncResult.createTime = Date.now();
|
|
541
|
-
await findingFiles(syncResult.files.length, syncResult.files.length, true);
|
|
542
|
-
await localCache.set(parametersHash, Buffer.from(JSON.stringify(syncResult)));
|
|
543
|
-
}
|
|
544
|
-
synchronizeObj.parametersResult.resolve(syncResult);
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
let downloadProgress = await createProgress(`Downloading (bytes)`, 0);
|
|
548
|
-
let decompressProgress = await createProgress("Decompressing (bytes)", 0);
|
|
549
|
-
let scanProgress = await createProgress("Scanning (datums)", 0);
|
|
550
|
-
let decodeProgress = await createProgress("Decoding (datums)", 0);
|
|
551
|
-
let processProgress = await createProgress("Processing (datums)", 0);
|
|
552
|
-
let corruptDatumsProgress = await createProgress("Corrupt Datums (datums)", 0);
|
|
553
|
-
|
|
554
|
-
let scanFnc = config.scanFnc;
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
async function downloadAndParseFile(file: FileMetadata, runInner: (code: () => Promise<void>) => Promise<void>) {
|
|
558
|
-
const onFetchedData = runInSerial(async (data: Buffer) => {
|
|
559
|
-
await downloadProgress(data.length, data.length, true);
|
|
560
|
-
await decompressWriter.write(data);
|
|
561
|
-
});
|
|
562
|
-
|
|
563
|
-
const onDecompressedData = createLogScanner({
|
|
564
|
-
debugName: file.path,
|
|
565
|
-
onParsedData,
|
|
566
|
-
});
|
|
567
|
-
let batchedData: Buffer[] = [];
|
|
568
|
-
let notMatchedCount = 0;
|
|
569
|
-
let matchedSize = 0;
|
|
570
|
-
let notMatchedSize = 0;
|
|
571
|
-
|
|
572
|
-
let scanProgressCount = 0;
|
|
573
|
-
|
|
574
|
-
const wantData = await config.getWantData?.(file);
|
|
575
|
-
|
|
576
|
-
function onParsedData(posStart: number, posEnd: number, buffer: Buffer | "done"): MaybePromise<void> {
|
|
577
|
-
if (buffer !== "done") {
|
|
578
|
-
scanProgressCount++;
|
|
579
|
-
}
|
|
580
|
-
if (buffer !== "done") {
|
|
581
|
-
if (scanFnc) {
|
|
582
|
-
scanFnc(posStart, posEnd, buffer);
|
|
583
|
-
}
|
|
584
|
-
if (wantData && !wantData(posStart, posEnd, buffer)) {
|
|
585
|
-
notMatchedSize += (posEnd - posStart);
|
|
586
|
-
notMatchedCount++;
|
|
587
|
-
return;
|
|
588
|
-
}
|
|
589
|
-
batchedData.push(buffer.slice(posStart, posEnd));
|
|
590
|
-
matchedSize += (posEnd - posStart);
|
|
591
|
-
}
|
|
592
|
-
// IMPORTANT! We use scanProgressCount here, so queries that match few searches get results quickly!
|
|
593
|
-
if (scanProgressCount >= ON_DATA_BATCH_COUNT || buffer === "done") {
|
|
594
|
-
return (async () => {
|
|
595
|
-
await scanProgress(scanProgressCount, scanProgressCount, true);
|
|
596
|
-
scanProgressCount = 0;
|
|
597
|
-
let errors: Error[] = [];
|
|
598
|
-
|
|
599
|
-
let data = await measureBlock(async () => {
|
|
600
|
-
let decoded: Datum[] = [];
|
|
601
|
-
for (let datum of batchedData) {
|
|
602
|
-
try {
|
|
603
|
-
decoded.push(JSON.parse(datum.toString()) as Datum);
|
|
604
|
-
} catch (e: any) {
|
|
605
|
-
errors.push(e);
|
|
606
|
-
}
|
|
607
|
-
await decodeProgress(1, 1, true);
|
|
608
|
-
}
|
|
609
|
-
return decoded;
|
|
610
|
-
}, "FastArchiveAppendable|deserializeData");
|
|
611
|
-
if (errors.length > 0) {
|
|
612
|
-
console.error(`${errors.length} errors decoding datums in ${file.path}, first error is:\n${errors[0].stack}`);
|
|
613
|
-
await corruptDatumsProgress(errors.length, errors.length, true);
|
|
614
|
-
}
|
|
615
|
-
batchedData = [];
|
|
616
|
-
if (data.length > 0) {
|
|
617
|
-
await measureBlock(() => onData(data, file), "FastArchiveAppendable|onData(callback)");
|
|
618
|
-
}
|
|
619
|
-
if (onStats) {
|
|
620
|
-
let stats: DatumStats = { matchedSize, notMatchedSize, errors: errors.length, notMatchedCount, matchedCount: data.length };
|
|
621
|
-
matchedSize = 0;
|
|
622
|
-
notMatchedSize = 0;
|
|
623
|
-
notMatchedCount = 0;
|
|
624
|
-
await measureBlock(() => onStats!(stats, file), "FastArchiveAppendable|onStats(callback)");
|
|
625
|
-
}
|
|
626
|
-
let count = data.length;
|
|
627
|
-
await processProgress(count, count, true);
|
|
628
|
-
})();
|
|
629
|
-
}
|
|
630
|
-
}
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
// Create decompression stream
|
|
634
|
-
const decompressStream = new DecompressionStream("gzip");
|
|
635
|
-
const decompressWriter = decompressStream.writable.getWriter();
|
|
636
|
-
const decompressReader = decompressStream.readable.getReader();
|
|
637
|
-
|
|
638
|
-
// Decompress pipeline
|
|
639
|
-
let decompressPromise = (async () => {
|
|
640
|
-
for await (let value of streamToIteratable(decompressReader)) {
|
|
641
|
-
if (stoppedPromise.resolveCalled) return;
|
|
642
|
-
let buffer = Buffer.from(value);
|
|
643
|
-
|
|
644
|
-
await decompressProgress(buffer.length, buffer.length, true);
|
|
645
|
-
await onDecompressedData(buffer);
|
|
646
|
-
}
|
|
647
|
-
await onDecompressedData("done");
|
|
648
|
-
})();
|
|
649
|
-
|
|
650
|
-
// Fetch the file data in a streaming manner
|
|
651
|
-
|
|
652
|
-
// TODO: Stream from the local cache instead? It should be possible, we can get the total size, and read chunks.
|
|
653
|
-
let hash = getFileMetadataHash(file) + ".file";
|
|
654
|
-
let contents = await localCache.get(hash);
|
|
655
|
-
if (contents?.length !== file.size) {
|
|
656
|
-
contents = undefined;
|
|
657
|
-
}
|
|
658
|
-
if (stoppedPromise.resolveCalled) return;
|
|
659
|
-
await runInner(async () => {
|
|
660
|
-
if (contents?.length) {
|
|
661
|
-
const CHUNK_SIZE = 1000 * 1000 * 10;
|
|
662
|
-
for (let i = 0; i < contents.length; i += CHUNK_SIZE) {
|
|
663
|
-
let data = contents.slice(i, i + CHUNK_SIZE);
|
|
664
|
-
await onFetchedData(data);
|
|
665
|
-
}
|
|
666
|
-
} else {
|
|
667
|
-
let urlObj = new URL(file.url);
|
|
668
|
-
urlObj.searchParams.set("cacheBust", config.cacheBust.toString());
|
|
669
|
-
urlObj.searchParams.set("isPublic", isPublicValue.toString());
|
|
670
|
-
if (file.nodeId && downloadSyncId) {
|
|
671
|
-
let args = JSON.parse(urlObj.searchParams.get("args") || "");
|
|
672
|
-
args[0] = downloadSyncId;
|
|
673
|
-
urlObj.searchParams.set("args", JSON.stringify(args));
|
|
674
|
-
}
|
|
675
|
-
let url = urlObj.toString();
|
|
676
|
-
const response = await fetch(url);
|
|
677
|
-
if (!response.ok) {
|
|
678
|
-
throw new Error(`Failed to fetch ${url}: ${response.status} ${response.statusText}`);
|
|
679
|
-
}
|
|
680
|
-
|
|
681
|
-
if (!response.body) {
|
|
682
|
-
throw new Error(`Response body is undefined for ${url}`);
|
|
683
|
-
}
|
|
684
|
-
let values: Buffer[] = [];
|
|
685
|
-
const reader = response.body.getReader();
|
|
686
|
-
void stoppedPromise.promise.finally(() => {
|
|
687
|
-
void response.body?.cancel();
|
|
688
|
-
});
|
|
689
|
-
try {
|
|
690
|
-
for await (let value of streamToIteratable(reader)) {
|
|
691
|
-
// Cancel entirely
|
|
692
|
-
if (stoppedPromise.resolveCalled) return;
|
|
693
|
-
if (!value) continue;
|
|
694
|
-
let buffer = Buffer.from(value);
|
|
695
|
-
values.push(buffer);
|
|
696
|
-
await onFetchedData(buffer);
|
|
697
|
-
}
|
|
698
|
-
} finally {
|
|
699
|
-
reader.releaseLock();
|
|
700
|
-
}
|
|
701
|
-
|
|
702
|
-
await localCache.set(hash, Buffer.concat(values));
|
|
703
|
-
}
|
|
704
|
-
await decompressWriter.close();
|
|
705
|
-
await decompressPromise;
|
|
706
|
-
});
|
|
707
|
-
}
|
|
708
|
-
|
|
709
|
-
// Fork off the processing
|
|
710
|
-
void (async () => {
|
|
711
|
-
try {
|
|
712
|
-
// Iterate over all files and process them
|
|
713
|
-
let fileProgress = await createProgress("Files", syncResult.files.length);
|
|
714
|
-
let fileInnerProgress = await createProgress("Files Inner", syncResult.files.length);
|
|
715
|
-
let failedFiles = await createProgress("Failed Files", 0);
|
|
716
|
-
let runSerial = runInSerial(async (fnc: () => Promise<void>) => {
|
|
717
|
-
try {
|
|
718
|
-
await fnc();
|
|
719
|
-
} finally {
|
|
720
|
-
await fileInnerProgress(1, 0, true);
|
|
721
|
-
}
|
|
722
|
-
});
|
|
723
|
-
async function downloadFileWrapper(file: FileMetadata) {
|
|
724
|
-
if (stoppedPromise.resolveCalled) return;
|
|
725
|
-
try {
|
|
726
|
-
await downloadAndParseFile(file, runSerial);
|
|
727
|
-
} catch (e: any) {
|
|
728
|
-
console.warn(`Failed to download and parse file ${file.path}:\n${e.stack}`);
|
|
729
|
-
await failedFiles(1, 1, true);
|
|
730
|
-
if (onError) {
|
|
731
|
-
onError(e, file);
|
|
732
|
-
}
|
|
733
|
-
}
|
|
734
|
-
if (stoppedPromise.resolveCalled) return;
|
|
735
|
-
await fileProgress(1, 0, true);
|
|
736
|
-
}
|
|
737
|
-
let remaining = syncResult.files.slice();
|
|
738
|
-
async function runThread() {
|
|
739
|
-
while (true) {
|
|
740
|
-
let file = remaining.shift();
|
|
741
|
-
if (!file) {
|
|
742
|
-
return;
|
|
743
|
-
}
|
|
744
|
-
await downloadFileWrapper(file);
|
|
745
|
-
}
|
|
746
|
-
}
|
|
747
|
-
await Promise.all(list(32).map(() => runThread()));
|
|
748
|
-
|
|
749
|
-
await (await createProgress("Done", 0))(1, 1, true);
|
|
750
|
-
} catch (e: any) {
|
|
751
|
-
baseOnProgress?.({
|
|
752
|
-
section: `Error ${e.stack}`,
|
|
753
|
-
value: 1,
|
|
754
|
-
max: 1,
|
|
755
|
-
});
|
|
756
|
-
} finally {
|
|
757
|
-
config.onFinish?.();
|
|
758
|
-
stopSynchronize();
|
|
759
|
-
}
|
|
760
|
-
})();
|
|
761
|
-
|
|
762
|
-
return {
|
|
763
|
-
metadata: syncResult,
|
|
764
|
-
stopSynchronize,
|
|
765
|
-
};
|
|
766
|
-
|
|
767
|
-
} catch (e) {
|
|
768
|
-
await stopSynchronize();
|
|
769
|
-
throw e;
|
|
770
|
-
}
|
|
771
|
-
}
|
|
772
|
-
}
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
export function createLogScanner(config: {
|
|
776
|
-
debugName: string;
|
|
777
|
-
onParsedData: (posStart: number, posEnd: number, buffer: Buffer | "done") => MaybePromise<void>;
|
|
778
|
-
}): (data: Buffer | "done") => Promise<void> {
|
|
779
|
-
const { onParsedData } = config;
|
|
780
|
-
let pendingData: Buffer[] = [];
|
|
781
|
-
|
|
782
|
-
let finished = false;
|
|
783
|
-
|
|
784
|
-
let delimitterMatchIndex = 0;
|
|
785
|
-
return (async (data: Buffer | "done") => {
|
|
786
|
-
if (data === "done") {
|
|
787
|
-
finished = true;
|
|
788
|
-
// Flush any pending data, even though we have no delimitter. It will probably fail to parse, but... maybe it will work?
|
|
789
|
-
if (pendingData.length > 0) {
|
|
790
|
-
let combinedBuffer = Buffer.concat(pendingData);
|
|
791
|
-
pendingData = [];
|
|
792
|
-
await onParsedData(0, combinedBuffer.length, combinedBuffer);
|
|
793
|
-
}
|
|
794
|
-
await onParsedData(0, 0, "done");
|
|
795
|
-
return;
|
|
796
|
-
}
|
|
797
|
-
if (finished) {
|
|
798
|
-
throw new Error(`Finished scan, but we received more data: ${data.length}, sample is: ${data.slice(0, 100).toString("hex")}, ${config.debugName}`);
|
|
799
|
-
}
|
|
800
|
-
|
|
801
|
-
let lastStart = 0;
|
|
802
|
-
await measureBlock(async () => {
|
|
803
|
-
for (let i = 0; i < data.length; i++) {
|
|
804
|
-
if (data[i] === objectDelimitterBuffer[delimitterMatchIndex]) {
|
|
805
|
-
delimitterMatchIndex++;
|
|
806
|
-
} else {
|
|
807
|
-
delimitterMatchIndex = 0;
|
|
808
|
-
}
|
|
809
|
-
if (delimitterMatchIndex === objectDelimitterBuffer.length) {
|
|
810
|
-
delimitterMatchIndex = 0;
|
|
811
|
-
|
|
812
|
-
let buffer: Buffer;
|
|
813
|
-
let posStart = -1;
|
|
814
|
-
let posEnd = -1;
|
|
815
|
-
if (pendingData.length > 0) {
|
|
816
|
-
buffer = Buffer.concat([
|
|
817
|
-
...pendingData,
|
|
818
|
-
data.slice(lastStart, i + 1),
|
|
819
|
-
]).slice(0, -objectDelimitterBuffer.length);
|
|
820
|
-
pendingData = [];
|
|
821
|
-
posStart = 0;
|
|
822
|
-
posEnd = buffer.length;
|
|
823
|
-
} else {
|
|
824
|
-
buffer = data;
|
|
825
|
-
posStart = lastStart;
|
|
826
|
-
posEnd = i + 1 - objectDelimitterBuffer.length;
|
|
827
|
-
}
|
|
828
|
-
// Only sometimes awaiting here makes scanning almost 2X faster, in the normal case, somehow?
|
|
829
|
-
let maybePromise = onParsedData(posStart, posEnd, buffer);
|
|
830
|
-
if (maybePromise) {
|
|
831
|
-
await maybePromise;
|
|
832
|
-
}
|
|
833
|
-
|
|
834
|
-
lastStart = i + 1;
|
|
835
|
-
}
|
|
836
|
-
}
|
|
837
|
-
if (lastStart < data.length) {
|
|
838
|
-
let remaining = data.slice(lastStart);
|
|
839
|
-
pendingData.push(remaining);
|
|
840
|
-
}
|
|
841
|
-
}, "FastArchiveAppendable|scanForDatumDelimitters");
|
|
842
|
-
});
|
|
843
|
-
}
|