querysub 0.2.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.
Files changed (169) hide show
  1. package/.dependency-cruiser.js +304 -0
  2. package/.eslintrc.js +51 -0
  3. package/.github/copilot-instructions.md +1 -0
  4. package/.vscode/settings.json +25 -0
  5. package/bin/deploy.js +4 -0
  6. package/bin/function.js +4 -0
  7. package/bin/server.js +4 -0
  8. package/costsBenefits.txt +112 -0
  9. package/deploy.ts +3 -0
  10. package/inject.ts +1 -0
  11. package/package.json +60 -0
  12. package/prompts.txt +54 -0
  13. package/spec.txt +820 -0
  14. package/src/-a-archives/archiveCache.ts +913 -0
  15. package/src/-a-archives/archives.ts +148 -0
  16. package/src/-a-archives/archivesBackBlaze.ts +792 -0
  17. package/src/-a-archives/archivesDisk.ts +418 -0
  18. package/src/-a-archives/copyLocalToBackblaze.ts +24 -0
  19. package/src/-a-auth/certs.ts +517 -0
  20. package/src/-a-auth/der.ts +122 -0
  21. package/src/-a-auth/ed25519.ts +1015 -0
  22. package/src/-a-auth/node-forge-ed25519.d.ts +17 -0
  23. package/src/-b-authorities/dnsAuthority.ts +203 -0
  24. package/src/-b-authorities/emailAuthority.ts +57 -0
  25. package/src/-c-identity/IdentityController.ts +200 -0
  26. package/src/-d-trust/NetworkTrust2.ts +150 -0
  27. package/src/-e-certs/EdgeCertController.ts +288 -0
  28. package/src/-e-certs/certAuthority.ts +192 -0
  29. package/src/-f-node-discovery/NodeDiscovery.ts +543 -0
  30. package/src/-g-core-values/NodeCapabilities.ts +134 -0
  31. package/src/-g-core-values/oneTimeForward.ts +91 -0
  32. package/src/-h-path-value-serialize/PathValueSerializer.ts +769 -0
  33. package/src/-h-path-value-serialize/stringSerializer.ts +176 -0
  34. package/src/0-path-value-core/LoggingClient.tsx +24 -0
  35. package/src/0-path-value-core/NodePathAuthorities.ts +978 -0
  36. package/src/0-path-value-core/PathController.ts +1 -0
  37. package/src/0-path-value-core/PathValueCommitter.ts +565 -0
  38. package/src/0-path-value-core/PathValueController.ts +231 -0
  39. package/src/0-path-value-core/archiveLocks/ArchiveLocks.ts +154 -0
  40. package/src/0-path-value-core/archiveLocks/ArchiveLocks2.ts +820 -0
  41. package/src/0-path-value-core/archiveLocks/archiveSnapshots.ts +180 -0
  42. package/src/0-path-value-core/debugLogs.ts +90 -0
  43. package/src/0-path-value-core/pathValueArchives.ts +483 -0
  44. package/src/0-path-value-core/pathValueCore.ts +2217 -0
  45. package/src/1-path-client/RemoteWatcher.ts +558 -0
  46. package/src/1-path-client/pathValueClientWatcher.ts +702 -0
  47. package/src/2-proxy/PathValueProxyWatcher.ts +1857 -0
  48. package/src/2-proxy/archiveMoveHarness.ts +376 -0
  49. package/src/2-proxy/garbageCollection.ts +753 -0
  50. package/src/2-proxy/pathDatabaseProxyBase.ts +37 -0
  51. package/src/2-proxy/pathValueProxy.ts +139 -0
  52. package/src/2-proxy/schema2.ts +518 -0
  53. package/src/3-path-functions/PathFunctionHelpers.ts +129 -0
  54. package/src/3-path-functions/PathFunctionRunner.ts +619 -0
  55. package/src/3-path-functions/PathFunctionRunnerMain.ts +67 -0
  56. package/src/3-path-functions/deployBlock.ts +10 -0
  57. package/src/3-path-functions/deployCheck.ts +7 -0
  58. package/src/3-path-functions/deployMain.ts +160 -0
  59. package/src/3-path-functions/pathFunctionLoader.ts +282 -0
  60. package/src/3-path-functions/syncSchema.ts +475 -0
  61. package/src/3-path-functions/tests/functionsTest.ts +135 -0
  62. package/src/3-path-functions/tests/rejectTest.ts +77 -0
  63. package/src/4-dom/css.tsx +29 -0
  64. package/src/4-dom/cssTypes.d.ts +212 -0
  65. package/src/4-dom/qreact.tsx +2322 -0
  66. package/src/4-dom/qreactTest.tsx +417 -0
  67. package/src/4-querysub/Querysub.ts +877 -0
  68. package/src/4-querysub/QuerysubController.ts +620 -0
  69. package/src/4-querysub/copyEvent.ts +0 -0
  70. package/src/4-querysub/permissions.ts +289 -0
  71. package/src/4-querysub/permissionsShared.ts +1 -0
  72. package/src/4-querysub/querysubPrediction.ts +525 -0
  73. package/src/5-diagnostics/FullscreenModal.tsx +67 -0
  74. package/src/5-diagnostics/GenericFormat.tsx +165 -0
  75. package/src/5-diagnostics/Modal.tsx +79 -0
  76. package/src/5-diagnostics/Table.tsx +183 -0
  77. package/src/5-diagnostics/TimeGrouper.tsx +114 -0
  78. package/src/5-diagnostics/diskValueAudit.ts +216 -0
  79. package/src/5-diagnostics/memoryValueAudit.ts +442 -0
  80. package/src/5-diagnostics/nodeMetadata.ts +135 -0
  81. package/src/5-diagnostics/qreactDebug.tsx +309 -0
  82. package/src/5-diagnostics/shared.ts +26 -0
  83. package/src/5-diagnostics/synchronousLagTracking.ts +47 -0
  84. package/src/TestController.ts +35 -0
  85. package/src/allowclient.flag +0 -0
  86. package/src/bits.ts +86 -0
  87. package/src/buffers.ts +69 -0
  88. package/src/config.ts +53 -0
  89. package/src/config2.ts +48 -0
  90. package/src/diagnostics/ActionsHistory.ts +56 -0
  91. package/src/diagnostics/NodeViewer.tsx +503 -0
  92. package/src/diagnostics/SizeLimiter.ts +62 -0
  93. package/src/diagnostics/TimeDebug.tsx +18 -0
  94. package/src/diagnostics/benchmark.ts +139 -0
  95. package/src/diagnostics/errorLogs/ErrorLogController.ts +515 -0
  96. package/src/diagnostics/errorLogs/ErrorLogCore.ts +274 -0
  97. package/src/diagnostics/errorLogs/LogClassifiers.tsx +302 -0
  98. package/src/diagnostics/errorLogs/LogFilterUI.tsx +84 -0
  99. package/src/diagnostics/errorLogs/LogNotify.tsx +101 -0
  100. package/src/diagnostics/errorLogs/LogTimeSelector.tsx +724 -0
  101. package/src/diagnostics/errorLogs/LogViewer.tsx +757 -0
  102. package/src/diagnostics/errorLogs/hookErrors.ts +60 -0
  103. package/src/diagnostics/errorLogs/logFiltering.tsx +149 -0
  104. package/src/diagnostics/heapTag.ts +13 -0
  105. package/src/diagnostics/listenOnDebugger.ts +77 -0
  106. package/src/diagnostics/logs/DiskLoggerPage.tsx +572 -0
  107. package/src/diagnostics/logs/ObjectDisplay.tsx +165 -0
  108. package/src/diagnostics/logs/ansiFormat.ts +108 -0
  109. package/src/diagnostics/logs/diskLogGlobalContext.ts +38 -0
  110. package/src/diagnostics/logs/diskLogger.ts +305 -0
  111. package/src/diagnostics/logs/diskShimConsoleLogs.ts +32 -0
  112. package/src/diagnostics/logs/injectFileLocationToConsole.ts +50 -0
  113. package/src/diagnostics/logs/logGitHashes.ts +30 -0
  114. package/src/diagnostics/managementPages.tsx +289 -0
  115. package/src/diagnostics/periodic.ts +89 -0
  116. package/src/diagnostics/runSaturationTest.ts +416 -0
  117. package/src/diagnostics/satSchema.ts +64 -0
  118. package/src/diagnostics/trackResources.ts +82 -0
  119. package/src/diagnostics/watchdog.ts +55 -0
  120. package/src/errors.ts +132 -0
  121. package/src/forceProduction.ts +3 -0
  122. package/src/fs.ts +72 -0
  123. package/src/heapDumps.ts +666 -0
  124. package/src/https.ts +2 -0
  125. package/src/inject.ts +1 -0
  126. package/src/library-components/ATag.tsx +84 -0
  127. package/src/library-components/Button.tsx +344 -0
  128. package/src/library-components/ButtonSelector.tsx +64 -0
  129. package/src/library-components/DropdownCustom.tsx +151 -0
  130. package/src/library-components/DropdownSelector.tsx +32 -0
  131. package/src/library-components/Input.tsx +334 -0
  132. package/src/library-components/InputLabel.tsx +198 -0
  133. package/src/library-components/InputPicker.tsx +125 -0
  134. package/src/library-components/LazyComponent.tsx +62 -0
  135. package/src/library-components/MeasureHeightCSS.tsx +48 -0
  136. package/src/library-components/MeasuredDiv.tsx +47 -0
  137. package/src/library-components/ShowMore.tsx +51 -0
  138. package/src/library-components/SyncedController.ts +171 -0
  139. package/src/library-components/TimeRangeSelector.tsx +407 -0
  140. package/src/library-components/URLParam.ts +263 -0
  141. package/src/library-components/colors.tsx +14 -0
  142. package/src/library-components/drag.ts +114 -0
  143. package/src/library-components/icons.tsx +692 -0
  144. package/src/library-components/niceStringify.ts +50 -0
  145. package/src/library-components/renderToString.ts +52 -0
  146. package/src/misc/PromiseRace.ts +101 -0
  147. package/src/misc/color.ts +30 -0
  148. package/src/misc/getParentProcessId.cs +53 -0
  149. package/src/misc/getParentProcessId.ts +53 -0
  150. package/src/misc/hash.ts +83 -0
  151. package/src/misc/ipPong.js +13 -0
  152. package/src/misc/networking.ts +2 -0
  153. package/src/misc/random.ts +45 -0
  154. package/src/misc.ts +19 -0
  155. package/src/noserverhotreload.flag +0 -0
  156. package/src/path.ts +226 -0
  157. package/src/persistentLocalStore.ts +37 -0
  158. package/src/promise.ts +15 -0
  159. package/src/server.ts +73 -0
  160. package/src/src.d.ts +1 -0
  161. package/src/test/heapProcess.ts +36 -0
  162. package/src/test/mongoSatTest.tsx +55 -0
  163. package/src/test/satTest.ts +193 -0
  164. package/src/test/test.tsx +552 -0
  165. package/src/zip.ts +92 -0
  166. package/src/zipThreaded.ts +106 -0
  167. package/src/zipThreadedWorker.js +19 -0
  168. package/tsconfig.json +27 -0
  169. package/yarnSpec.txt +56 -0
@@ -0,0 +1,913 @@
1
+ import { getStorageDir, getSubFolder } from "../fs";
2
+ import { Archives } from "./archives";
3
+ import { diskEscapeFileName, getArchivesLocal } from "./archivesDisk";
4
+ import fs from "fs";
5
+
6
+ import diskusage from "diskusage";
7
+ import { binarySearchBasic, binarySearchIndex, list, nextId, sort, timeInHour, timeInMinute } from "socket-function/src/misc";
8
+ import { cache, lazy } from "socket-function/src/caching";
9
+ import { delay, runInSerial, runInfinitePoll } from "socket-function/src/batching";
10
+ import { sha256 } from "js-sha256";
11
+ import { devDebugbreak } from "../config";
12
+ import child_process from "child_process";
13
+ import os from "os";
14
+ import fsext, { flock } from "fs-ext";
15
+ import debugbreak from "debugbreak";
16
+ import readline from "readline";
17
+ import { getPPID } from "../misc/getParentProcessId";
18
+ import { Args } from "socket-function/src/types";
19
+ import { getArchivesBackblaze } from "./archivesBackBlaze";
20
+ import { logErrors } from "../errors";
21
+ import { formatNumber, formatTime } from "socket-function/src/formatting/format";
22
+ import { SizeLimiter } from "../diagnostics/SizeLimiter";
23
+
24
+ const SIZE_LIMIT = new SizeLimiter({
25
+ diskRoot: getStorageDir(),
26
+ maxBytes: 1024 * 1024 * 1024 * 50,
27
+ // Anything less than this and we can't even load enough weights models for a single task
28
+ minBytes: 1024 * 1024 * 1024 * 8,
29
+ maxDiskFraction: 0.3,
30
+ maxTotalDiskFraction: 0.95,
31
+ maxFiles: 1000 * 25,
32
+ });
33
+
34
+ const UPDATE_METRICS_INTERVAL = timeInMinute * 30;
35
+ const cacheArchives2 = getSubFolder("cache");
36
+
37
+ const LOCK_SUFFIX = ".lock";
38
+ const TEMP_SUFFIX = ".tmp";
39
+ const TEMP_THRESHOLD = timeInHour * 3;
40
+
41
+ const LARGE_FILE_CHUNK = 1024 * 1024 * 32;
42
+ const CACHE_SUFFIX = ".cache";
43
+ export function getArchiveCachePath(archives: Archives, key: string): string {
44
+ // Remove all "/backblaze/" values from the path, as they make equivalent paths
45
+ // hash differently, which we don't want.
46
+ let fullFileName = (archives.getDebugName() + "/" + key).replaceAll("//", "/").replaceAll("backblaze/", "/");
47
+ let hash = sha256(fullFileName);
48
+ let name = fullFileName.replace(/[<>:"\/\\|?*\x00-\x1F]/g, "_");
49
+ // Remove spaces, as I think they are causing some issues if they are at the end
50
+ name = name.replaceAll(" ", "");
51
+ return cacheArchives2 + hash.slice(0, 16) + "." + name.slice(0, 128) + CACHE_SUFFIX;
52
+ }
53
+
54
+ const getDiskMetricsBase = async () => {
55
+ ensureDiskMetricsUpdated();
56
+
57
+ let fileSizes: {
58
+ path: string;
59
+ time: number;
60
+ bytes: number;
61
+ }[] = [];
62
+
63
+ let usedCacheBytes = 0;
64
+ let usedCacheFiles = 0;
65
+ let cacheFiles = await fs.promises.readdir(cacheArchives2);
66
+ for (let file of cacheFiles) {
67
+ if (file.endsWith(LOCK_SUFFIX)) {
68
+ let base = file.slice(0, -LOCK_SUFFIX.length);
69
+ if (!fs.existsSync(base) && !await isLocked(cacheArchives2 + base)) {
70
+ try {
71
+ // NOTE: This races, it might not be locked when we check, but a lock
72
+ // might be added right here. But... what else can we do? We need
73
+ // to cleanup unused locks at some point...
74
+ await fs.promises.unlink(cacheArchives2 + file);
75
+ } catch { }
76
+ }
77
+ } else if (file.endsWith(CACHE_SUFFIX)) {
78
+ let info: fs.Stats | undefined;
79
+ try {
80
+ info = await fs.promises.stat(file);
81
+ } catch { }
82
+ if (!info) continue;
83
+ fileSizes.push({
84
+ path: file,
85
+ time: info.atimeMs,
86
+ bytes: info.size,
87
+ });
88
+ usedCacheBytes += info.size;
89
+ usedCacheFiles++;
90
+ } else {
91
+ // TEMP files, and... any files?
92
+ // If it's too old, delete it
93
+ let stat = await fs.promises.stat(cacheArchives2 + file);
94
+ let threshold = Date.now() - TEMP_THRESHOLD;
95
+ if (stat.mtimeMs < threshold) {
96
+ try {
97
+ await fs.promises.unlink(cacheArchives2 + file);
98
+ } catch { }
99
+ }
100
+ }
101
+ }
102
+
103
+ let { remove, availableBytes, availableFiles } = await SIZE_LIMIT.limit(fileSizes);
104
+ for (let file of remove) {
105
+ try {
106
+ await fs.promises.unlink(cacheArchives2 + file.path);
107
+ } catch { }
108
+ }
109
+
110
+ function updateAccessTime(path: string) {
111
+ let index = fileSizes.findIndex(x => x.path === path);
112
+ if (index < 0) return;
113
+ let obj = fileSizes[index];
114
+ obj.time = Date.now();
115
+ }
116
+
117
+ function removeFile(path: string) {
118
+ let index = fileSizes.findIndex(x => x.path === path);
119
+ if (index < 0) return false;
120
+ let obj = fileSizes[index];
121
+ availableBytes += obj.bytes;
122
+ availableFiles++;
123
+ fileSizes.splice(index, 1);
124
+ return true;
125
+ }
126
+
127
+ // We require our input to be a temp file, so our update can be atomic
128
+ async function addCacheFile(archives: Archives, key: string, sourceTempFile: string): Promise<void> {
129
+ let path = getArchiveCachePath(archives, key);
130
+ let hasFile = fileSizes.some(x => x.path === path);
131
+ if (hasFile) {
132
+ removeFile(path);
133
+ }
134
+ let sourceTempFileStat = await fs.promises.stat(sourceTempFile);
135
+ availableBytes -= sourceTempFileStat.size;
136
+ availableFiles--;
137
+ while ((availableBytes < 0 || availableFiles < 0) && fileSizes.length > 0) {
138
+ let leastRecentlyAccessed = fileSizes[0];
139
+ for (let file of fileSizes) {
140
+ if (file.time < leastRecentlyAccessed.time) {
141
+ leastRecentlyAccessed = file;
142
+ }
143
+ }
144
+ let size = 0;
145
+ try {
146
+ size = (await fs.promises.stat(leastRecentlyAccessed.path)).size;
147
+ } catch { }
148
+ if (await deleteCacheFile(leastRecentlyAccessed.path)) {
149
+ removeFile(leastRecentlyAccessed.path);
150
+ }
151
+ }
152
+ fileSizes.push({
153
+ path,
154
+ bytes: sourceTempFileStat.size,
155
+ time: Date.now(),
156
+ });
157
+ availableBytes -= sourceTempFileStat.size;
158
+ availableFiles--;
159
+ try {
160
+ await fs.promises.rename(sourceTempFile, path);
161
+ } catch (e) {
162
+ let destExists = fs.existsSync(path);
163
+ if (!destExists) {
164
+ console.error("Error renaming file", sourceTempFile, path, e);
165
+ }
166
+ }
167
+ }
168
+ async function getCacheFile(
169
+ archives: Archives,
170
+ key: string,
171
+ config?: { range?: { start: number; end: number } }
172
+ ): Promise<Buffer | undefined> {
173
+ let path = getArchiveCachePath(archives, key);
174
+ let buffer: Buffer | undefined;
175
+ let handle: fs.promises.FileHandle | undefined;
176
+ try {
177
+ handle = await fs.promises.open(path, "r");
178
+ let stats = await handle.stat();
179
+ let start = config?.range?.start ?? 0;
180
+ let end = config?.range?.end ?? stats.size;
181
+ let readBuffer = Buffer.alloc(end - start);
182
+ let read = await handle.read(readBuffer, 0, end - start, start);
183
+ buffer = readBuffer.slice(0, read.bytesRead);
184
+ }
185
+ catch { }
186
+ finally {
187
+ if (handle) {
188
+ await handle.close();
189
+ }
190
+ }
191
+ if (buffer) {
192
+ updateAccessTime(path);
193
+ }
194
+ return buffer;
195
+ }
196
+ async function getPathAndLock(archives: Archives, key: string): Promise<string> {
197
+ let path = getArchiveCachePath(archives, key);
198
+ await lockFile(path);
199
+ updateAccessTime(path);
200
+ return path;
201
+ }
202
+ async function delCacheFile(archives: Archives, key: string): Promise<void> {
203
+ let path = getArchiveCachePath(archives, key);
204
+ if (removeFile(path)) {
205
+ try {
206
+ await fs.promises.unlink(path);
207
+ } catch { }
208
+ }
209
+ }
210
+
211
+ // Keep track of an in memory access list, to make freeing and space estimation faster
212
+ // (that way we can find least recently accessed files without having to scan the disk
213
+ // every time... and it will be accurate, as long as we don't have multiple processes)
214
+ return {
215
+ addCacheFile: runInSerial(addCacheFile),
216
+ getCacheFile,
217
+ delCacheFile,
218
+ getPathAndLock,
219
+ };
220
+ };
221
+ let curSeqNum = 1;
222
+ const getDiskMetricSeqNum = cache((seqNum: number) => getDiskMetricsBase());
223
+ async function getDiskMetrics() {
224
+ return getDiskMetricSeqNum(curSeqNum);
225
+ }
226
+
227
+ const ensureDiskMetricsUpdated = lazy(() => {
228
+ runInfinitePoll(UPDATE_METRICS_INTERVAL, async () => {
229
+ await getDiskMetricSeqNum(curSeqNum + 1);
230
+ curSeqNum++;
231
+ getDiskMetricSeqNum.clear(curSeqNum - 1);
232
+ });
233
+ });
234
+
235
+ export type LockFncs = {
236
+ // Get a path to the file locally, locking it so it won't be garbage collected until lockRegion completes
237
+ // - If the file isn't in the archives, returns undefined, and doesn't lock it
238
+ getPathAndLock(fileName: string): Promise<string | undefined>;
239
+ // Gets it, and only checks the cache, not the archives. Faster, but means it might be deleted in
240
+ // the archives and we will still use it locally.
241
+ getPathAndLockFast(fileName: string): Promise<string | undefined>;
242
+ getPathAndLockCacheOnly(fileName: string): Promise<string | undefined>;
243
+ // MOVES the sourceFileName to the archives (so sourceFileName will be deleted after this runs),
244
+ // and locks the archives file until lockRegion completes
245
+ moveFileAndLock(config: {
246
+ archivesFilename: string;
247
+ sourceFileName: string;
248
+ onlyCache?: boolean;
249
+ }): Promise<string>;
250
+ };
251
+
252
+ let cacheArchivesSymbol = Symbol("cacheArchives");
253
+ /** IMPORTANT! The cache assumes the files contents immutable, and they will only be created
254
+ * and deleted, never mutated.
255
+ */
256
+ export function wrapArchivesWithCache(archives: Archives): Archives & {
257
+ // NOTE: lockRegion / path based functions are preferred for external accesses, as they ensure files
258
+ // won't be garbage collected, and uses paths, which will be required for external processes.
259
+ // - Locks only protect the local cache. The values can still be deleted explicitly.
260
+ lockRegion<T>(
261
+ code: (
262
+ fncs: LockFncs
263
+ ) => Promise<T>
264
+ ): Promise<T>;
265
+ debugGetPath(key: string): string;
266
+ } {
267
+ if (cacheArchivesSymbol in archives) {
268
+ return archives as any;
269
+ }
270
+ async function setLargeFile(config: Args<Archives["setLargeFile"]>[0]) {
271
+ const tempPath = cacheArchives2 + nextId() + TEMP_SUFFIX;
272
+ let handle: fs.promises.FileHandle | undefined;
273
+ try {
274
+ handle = await fs.promises.open(tempPath, "w");
275
+ let pos = 0;
276
+ while (true) {
277
+ let data = await config.getNextData();
278
+ if (!data?.length) break;
279
+ await handle.write(data, 0, data.length, pos);
280
+ pos += data.length;
281
+ }
282
+ } finally {
283
+ if (handle) {
284
+ try {
285
+ await handle.close();
286
+ } catch { }
287
+ }
288
+ }
289
+
290
+ let metrics = await getDiskMetrics();
291
+ await metrics.addCacheFile(archives, config.path, tempPath);
292
+ let cachePath = getArchiveCachePath(archives, config.path);
293
+
294
+ await lockFile(cachePath);
295
+ let pos = 0;
296
+ let cacheHandle: fs.promises.FileHandle = await fs.promises.open(cachePath, "r");
297
+ let data = Buffer.alloc(LARGE_FILE_CHUNK);
298
+ async function getNextData(): Promise<Buffer | undefined> {
299
+ try {
300
+ let read = await cacheHandle.read(data, 0, LARGE_FILE_CHUNK, pos);
301
+ if (read.bytesRead === 0) return undefined;
302
+ let curData = data;
303
+ if (read.bytesRead < LARGE_FILE_CHUNK) {
304
+ curData = curData.slice(0, read.bytesRead);
305
+ }
306
+ pos += read.bytesRead;
307
+ return curData;
308
+ } catch {
309
+ return undefined;
310
+ }
311
+ }
312
+ try {
313
+ await archives.setLargeFile({
314
+ ...config,
315
+ path: config.path,
316
+ getNextData,
317
+ });
318
+ } finally {
319
+ await cacheHandle.close();
320
+ await unlockFile(cachePath);
321
+ }
322
+ }
323
+ function createGetPathAndLock(locked: string[]) {
324
+ return async function getPathAndLock(fileName: string) {
325
+ const info = await archives.getInfo(fileName);
326
+ if (!info) return undefined;
327
+
328
+ let path = getArchiveCachePath(archives, fileName);
329
+ await lockFile(path);
330
+ locked.push(path);
331
+
332
+ try {
333
+ let cacheStat = await fs.promises.stat(path);
334
+ // NOTE: We check the size, and not just the existence, in case the file
335
+ // is partially populated? Even though this isn't a real thing, as we rename
336
+ // when we copy it...
337
+ if (cacheStat.size === info.size) {
338
+ return path;
339
+ }
340
+ } catch {
341
+ // It doesn't exist, so read it in
342
+ }
343
+
344
+ let startRead = Date.now();
345
+
346
+ let readPos = 0;
347
+ let getNextDataBase = async (): Promise<Buffer | undefined> => {
348
+ if (readPos >= info.size) return undefined;
349
+ let curPos = readPos;
350
+ readPos += LARGE_FILE_CHUNK;
351
+ let end = Math.min(readPos, info.size);
352
+ let data = await archives.get(fileName, { range: { start: curPos, end } });
353
+ if (!data?.length) return undefined;
354
+ return data;
355
+ };
356
+
357
+ let nextWriteIndex = 0;
358
+ let buffers: (Buffer | { error: Error } | null | undefined | (() => void))[] = [];
359
+
360
+ async function getThread() {
361
+ while (true) {
362
+ let index = nextWriteIndex++;
363
+ let next: typeof buffers[number];
364
+ try {
365
+ next = await getNextDataBase();
366
+ } catch (e: any) {
367
+ next = { error: e };
368
+ }
369
+ let prev = buffers[index];
370
+ buffers[index] = next || null;
371
+ if (typeof prev === "function") {
372
+ prev();
373
+ }
374
+ if (!next || "error" in next) break;
375
+ }
376
+ }
377
+ // Read in parallel, for faster read times, and so we can read from backblaze
378
+ // while writing to disk.
379
+ void list(8).map(getThread);
380
+
381
+ let nextReadIndex = 0;
382
+ let getNextData = async (): Promise<Buffer | undefined> => {
383
+ let index = nextReadIndex++;
384
+ if (buffers[index] === null) return undefined;
385
+ if (!buffers[index]) {
386
+ await new Promise<void>(resolve => {
387
+ buffers[index] = resolve;
388
+ });
389
+ }
390
+ let result = buffers[index] as any;
391
+ // Clear it, so we don't store all buffers in memory
392
+ buffers[index] = undefined;
393
+ return result;
394
+ };
395
+
396
+ let size = 0;
397
+ const tempPath = cacheArchives2 + nextId() + TEMP_SUFFIX;
398
+ let handle: fs.promises.FileHandle | undefined;
399
+ try {
400
+ handle = await fs.promises.open(tempPath, "w");
401
+ while (true) {
402
+ let data = await getNextData();
403
+ if (!data?.length) break;
404
+ await handle.write(data, 0, data.length, size);
405
+ size += data.length;
406
+ }
407
+ } finally {
408
+ if (handle) {
409
+ try {
410
+ await handle.close();
411
+ } catch { }
412
+ }
413
+ }
414
+ let totalRead = Date.now() - startRead;
415
+ console.log(`Read ${formatNumber(info.size)}B at ${formatNumber(info.size / totalRead)}B/s into cache (${fileName})`);
416
+
417
+ let metrics = await getDiskMetrics();
418
+ await metrics.addCacheFile(archives, fileName, tempPath);
419
+
420
+ return path;
421
+ };
422
+ }
423
+ function createGetPathAndLockFast(locked: string[]) {
424
+ return async function getPathAndLockFast(fileName: string) {
425
+ let path = getArchiveCachePath(archives, fileName);
426
+ await lockFile(path);
427
+ locked.push(path);
428
+ try {
429
+ // If it exists in the cache, just return the path. Otherwise, we might have to read it in
430
+ await fs.promises.stat(path);
431
+ return path;
432
+ } catch {
433
+ return createGetPathAndLock(locked)(fileName);
434
+ }
435
+ };
436
+ }
437
+ function createMovePathFromFileAndLock(locked: string[]) {
438
+ return async function movePathFromFileAndLock(config: {
439
+ archivesFilename: string;
440
+ sourceFileName: string;
441
+ onlyCache?: boolean;
442
+ }): Promise<string> {
443
+ let { archivesFilename, sourceFileName } = config;
444
+ if (!config.onlyCache) {
445
+ // NOTE: While we COULD use a rename to quickly move the file... we have to at least copy
446
+ // it to the underlying archives, so... moving it piece by piece is fine...
447
+
448
+ let handle: fs.promises.FileHandle = await fs.promises.open(sourceFileName, "r");
449
+ let data = Buffer.alloc(LARGE_FILE_CHUNK);
450
+ let pos = 0;
451
+ async function getNextData(): Promise<Buffer | undefined> {
452
+ try {
453
+ let read = await handle.read(data, 0, LARGE_FILE_CHUNK, pos);
454
+ if (read.bytesRead === 0) return undefined;
455
+ if (read.bytesRead < LARGE_FILE_CHUNK) {
456
+ data = data.slice(0, read.bytesRead);
457
+ }
458
+ pos += read.bytesRead;
459
+ return data;
460
+ } catch {
461
+ return undefined;
462
+ }
463
+ }
464
+ try {
465
+ await setLargeFile({
466
+ path: archivesFilename,
467
+ getNextData,
468
+ });
469
+ await handle.close();
470
+ handle = undefined as any;
471
+ await fs.promises.unlink(sourceFileName);
472
+ } finally {
473
+ if (handle) {
474
+ await handle.close();
475
+ }
476
+ }
477
+ } else {
478
+ await fs.promises.rename(sourceFileName, getArchiveCachePath(archives, archivesFilename));
479
+ }
480
+ let path = getArchiveCachePath(archives, archivesFilename);
481
+ await lockFile(path);
482
+ locked.push(path);
483
+ return path;
484
+ };
485
+ }
486
+ function createGetPathAndLockCacheOnly(locked: string[]) {
487
+ return async function getPathAndLockCacheOnly(fileName: string) {
488
+ let path = getArchiveCachePath(archives, fileName);
489
+ await lockFile(path);
490
+ locked.push(path);
491
+ try {
492
+ await fs.promises.stat(path);
493
+ return path;
494
+ } catch {
495
+ return undefined;
496
+ }
497
+ };
498
+ }
499
+ function debugGetPath(key: string) {
500
+ return getArchiveCachePath(archives, key);
501
+ }
502
+ return {
503
+ [cacheArchivesSymbol]: true,
504
+ getDebugName: () => archives.getDebugName(),
505
+ debugGetPath,
506
+
507
+ get: async (fileName: string, config) => {
508
+ let metrics = await getDiskMetrics();
509
+ // Get info to at least see if it is removed or not. This makes sure we don't get in
510
+ // a bad state, at least for immutable files (for mutable files we don't have an easy
511
+ // way to check)
512
+ // - TODO: Set hash in file metadata (maybe for all archive writes?), and use this to
513
+ // compare it against our cache file (which can have the hash in the file name).
514
+ let info = await archives.getInfo(fileName);
515
+ if (!info) {
516
+ // If it is gone remotely, remove it from the cache, to save space.
517
+ await metrics.delCacheFile(archives, fileName);
518
+ return;
519
+ }
520
+ let buffer = await metrics.getCacheFile(archives, fileName, config);
521
+ if (buffer) return buffer;
522
+ let result = await archives.get(fileName);
523
+ if (result) {
524
+ const tempPath = cacheArchives2 + nextId() + TEMP_SUFFIX;
525
+ await fs.promises.writeFile(tempPath, result);
526
+ await metrics.addCacheFile(archives, fileName, tempPath);
527
+ let range = config?.range;
528
+ if (range) {
529
+ result = result.slice(range.start, range.end);
530
+ }
531
+ }
532
+ return result;
533
+ },
534
+ set: async (fileName: string, data: Buffer) => {
535
+ let metrics = await getDiskMetrics();
536
+ const tempPath = cacheArchives2 + nextId() + TEMP_SUFFIX;
537
+ await fs.promises.writeFile(tempPath, data);
538
+ await metrics.addCacheFile(archives, fileName, tempPath);
539
+ await archives.set(fileName, data);
540
+ },
541
+ setLargeFile,
542
+ del: async (fileName: string) => {
543
+ let metrics = await getDiskMetrics();
544
+ await metrics.delCacheFile(archives, fileName);
545
+ await archives.del(fileName);
546
+ },
547
+ getInfo: (fileName: string) => archives.getInfo(fileName),
548
+ find: async (prefix: string, config) => archives.find(prefix, config),
549
+ findInfo: async (prefix: string, config) => archives.findInfo(prefix, config),
550
+ enableLogging: () => archives.enableLogging(),
551
+ move: async (config) => {
552
+ // Delete the file from ourselves, even if it is a self move, as not having
553
+ // values is in the cache is the safest thing, but having extra values
554
+ // breaks a lot of code (because then delete doesn't really delete!)
555
+ let metrics = await getDiskMetrics();
556
+ await metrics.delCacheFile(archives, config.path);
557
+ // IMPORTANT. Unwrap, otherwise move will get a get, which might cause our value to be cached again
558
+ if (cacheArchivesSymbol in config.target) {
559
+ config = { ...config };
560
+ config.target = config.target.getBaseArchives!()!.archives;
561
+ }
562
+ await archives.move(config);
563
+ },
564
+ copy: config => archives.copy(config),
565
+ assertPathValid: (path: string) => archives.assertPathValid(path),
566
+ getBaseArchives: () => ({ archives: archives, parentPath: "" }),
567
+
568
+ async lockRegion(code) {
569
+ let locked: string[] = [];
570
+ let fncs = {
571
+ getPathAndLock: createGetPathAndLock(locked),
572
+ getPathAndLockFast: createGetPathAndLockFast(locked),
573
+ moveFileAndLock: createMovePathFromFileAndLock(locked),
574
+ getPathAndLockCacheOnly: createGetPathAndLockCacheOnly(locked),
575
+ };
576
+ try {
577
+ return await code(fncs);
578
+ } finally {
579
+ for (let path of locked) {
580
+ await unlockFile(path);
581
+ }
582
+ }
583
+ },
584
+ };
585
+ }
586
+
587
+
588
+ function lockFilePath(path: string): string {
589
+ return path + LOCK_SUFFIX;
590
+ }
591
+
592
+ function getUniqueId() {
593
+ return process.pid + " " + process.ppid;
594
+ }
595
+
596
+ let lockCache = new Map<string, number>();
597
+ async function lockFile(path: string): Promise<void> {
598
+ // NOTE: If we don't stop ourself from locking it multiple times, a single process
599
+ // could use up the fill lock file limit, which would break the system.
600
+ let prevCount = lockCache.get(path);
601
+ if (prevCount) {
602
+ lockCache.set(path, prevCount + 1);
603
+ return;
604
+ }
605
+ if (!prevCount) {
606
+ const lockPath = lockFilePath(path);
607
+ // NOTE: Locking is taking WAY too long, so... we're not going to wait. This should still
608
+ // be mostly fine, due to how we maintain transactions files.
609
+ lockRetryLoop(async function appendFile() {
610
+ await fs.promises.appendFile(lockPath, "lock " + getUniqueId() + "\n");
611
+ }).catch(e => console.error("Error appending file lock for", path, e));
612
+ }
613
+ prevCount = lockCache.get(path) || 0;
614
+ lockCache.set(path, prevCount + 1);
615
+ }
616
+
617
+ async function unlockFile(path: string): Promise<void> {
618
+ let prevCount = lockCache.get(path);
619
+ if (!prevCount) {
620
+ console.warn(`Unlocking a file that wasn't locked: ${path}`);
621
+ } else {
622
+ prevCount--;
623
+ lockCache.set(path, prevCount);
624
+ if (prevCount <= 0) {
625
+ lockCache.delete(path);
626
+ }
627
+ }
628
+ if (!prevCount) {
629
+ await isLocked(path, { type: "unlock", lock: "lock " + getUniqueId() });
630
+ }
631
+ }
632
+
633
+ async function lockRetryLoop<T>(code: () => Promise<T>): Promise<T> {
634
+ while (true) {
635
+ try {
636
+ return await code();
637
+ } catch (error: any) {
638
+ if (error.code === "EBUSY") {
639
+ console.log("Lock file busy, retrying in 1 second");
640
+ await new Promise(r => setTimeout(r, 1000));
641
+ continue;
642
+ }
643
+ throw error;
644
+ }
645
+ }
646
+ }
647
+
648
+ async function getHandle(path: string) {
649
+ while (true) {
650
+ try {
651
+ return await fs.promises.open(path, "r+");
652
+ } catch (error: any) {
653
+ if (error.code === "ENOENT") {
654
+ try {
655
+ return await fs.promises.open(path, "wx+");
656
+ } catch (error: any) {
657
+ if (error.code === "EEXIST") {
658
+ continue;
659
+ }
660
+ throw error;
661
+ }
662
+ }
663
+ throw error;
664
+ }
665
+ }
666
+ }
667
+
668
+
669
+ /** NOTE: This inherently has a race condition, as anything you do based on isLocked
670
+ * could be invalidated by the time you do it. But... this should be good enough
671
+ * for our use case...
672
+ */
673
+ async function isLocked(path: string, operation?: {
674
+ type: "delete";
675
+ } | {
676
+ type: "unlock";
677
+ lock: string;
678
+ }): Promise<number> {
679
+ const lockPath = lockFilePath(path);
680
+
681
+ async function filterToValidState(locks: string[]): Promise<string[]> {
682
+ let existingLocks: string[] = [];
683
+ for (let lock of locks) {
684
+ if (lock.startsWith("lock ")) {
685
+ let [_, pid, ppid] = lock.split(" ");
686
+ const invalid = (
687
+ !await isProcessAlive(+pid)
688
+ || !await isProcessAlive(+ppid)
689
+ || await getPPID(+pid) !== +ppid
690
+ );
691
+ await getPPID(+pid);
692
+ if (!invalid) {
693
+ existingLocks.push(lock);
694
+ }
695
+ } else {
696
+ continue;
697
+ }
698
+ }
699
+ return Array.from(existingLocks);
700
+ }
701
+
702
+ return await lockRetryLoop(async function isLocked() {
703
+ let handle = await getHandle(lockPath);
704
+ try {
705
+ await new Promise<void>((r, e) => flock(handle.fd, "ex", err => err ? e(err) : r()));
706
+ let contents = (await handle.readFile()).toString();
707
+ let locks = contents.toString().replaceAll("\0", "").trim().split("\n").filter(x => x);
708
+ let validLocks = await filterToValidState(locks);
709
+ if (operation?.type === "unlock") {
710
+ let index = validLocks.indexOf(operation.lock);
711
+ if (index >= 0) {
712
+ validLocks.splice(index, 1);
713
+ }
714
+ }
715
+ if (validLocks.length !== locks.length) {
716
+ //console.log(`Lock count changed from ${locks.length} to ${validLocks.length}`);
717
+ let newContents = Buffer.from(validLocks.map(x => x + "\n").join(""));
718
+ if (newContents.length < contents.length) {
719
+ newContents = Buffer.concat([newContents, Buffer.alloc(contents.length - newContents.length)]);
720
+ }
721
+ await handle.write(newContents, 0, newContents.length, 0);
722
+ } else {
723
+ //console.log(`Lock count unchanged: ${validLocks.length}`);
724
+ }
725
+ if (operation?.type === "delete") {
726
+ if (validLocks.length === 0) {
727
+ await fs.promises.unlink(path);
728
+ }
729
+ }
730
+ return validLocks.length;
731
+ } catch (error: any) {
732
+ if (error.code === "ENOENT") {
733
+ return 0; // Lock file doesn't exist
734
+ }
735
+ throw error; // Unexpected error, rethrow
736
+ } finally {
737
+ await handle.close();
738
+ }
739
+ });
740
+ }
741
+ async function execPromise(command: string, args: string[]): Promise<string> {
742
+ return new Promise((resolve, reject) => {
743
+ child_process.execFile(command, args, (error, stdout, stderr) => {
744
+ if (error) {
745
+ reject(error);
746
+ return;
747
+ }
748
+ resolve(stdout);
749
+ });
750
+ });
751
+ }
752
+
753
+
754
+ async function isProcessAlive(pid: number): Promise<boolean> {
755
+ try {
756
+ process.kill(pid, 0);
757
+ return true;
758
+ } catch (error) {
759
+ return false;
760
+ }
761
+ }
762
+
763
+
764
+ async function atomicRead(path: string) {
765
+ while (true) {
766
+ let stat0 = fs.statSync(path);
767
+ let contents = fs.readFileSync(path);
768
+ let stat1 = fs.statSync(path);
769
+ if (stat0.mtimeMs === stat1.mtimeMs) {
770
+ return contents;
771
+ }
772
+ }
773
+ };
774
+
775
+ async function lockCacheFile(archives: Archives, key: string): Promise<void> {
776
+ await lockFile(getArchiveCachePath(archives, key));
777
+ }
778
+ async function unlockCacheFile(archives: Archives, key: string): Promise<void> {
779
+ await unlockFile(getArchiveCachePath(archives, key));
780
+ }
781
+ // Returns true if it was deleted
782
+ async function deleteCacheFile(path: string): Promise<boolean> {
783
+ let cannotDelete = await isLocked(path, { type: "delete" });
784
+ return !cannotDelete;
785
+ }
786
+
787
+ // for i in {1..100}; do yarn typenode src/-b-archives/archiveCache.ts & done
788
+
789
+ async function testLocks() {
790
+ let testFiles = ["a", "b", "c"].map(x => cacheArchives2 + x);
791
+ function getAFile() {
792
+ let file = testFiles[Math.floor(Math.random() * testFiles.length)];
793
+ console.log(file);
794
+ return file;
795
+ }
796
+
797
+ function assert(condition: unknown, message: string) {
798
+ if (!condition) {
799
+ throw new Error(message);
800
+ }
801
+ }
802
+
803
+ async function test1() {
804
+ let file = getAFile();
805
+ await lockFile(file);
806
+ let locked = await isLocked(file);
807
+ assert(locked, "File should be locked");
808
+ await new Promise(r => setTimeout(r, 100));
809
+ await unlockFile(file);
810
+ console.log(await fs.promises.readFile(file + LOCK_SUFFIX, "utf8"));
811
+ }
812
+ async function test2() {
813
+ let file = getAFile();
814
+ let notDeleted = await isLocked(file, { type: "delete" });
815
+ if (notDeleted) {
816
+ console.log("File was not deleted, as it was locked");
817
+ } else {
818
+ console.log("File was deleted");
819
+ }
820
+ }
821
+ async function test3() {
822
+ let count = 10;
823
+ let file = getAFile();
824
+ console.log(`Lock count before: ${await isLocked(file)}`);
825
+ for (let i = 0; i < count; i++) {
826
+ await lockFile(file);
827
+ }
828
+ for (let i = 0; i < count; i++) {
829
+ await unlockFile(file);
830
+ }
831
+ console.log(`Lock count after: ${await isLocked(file)}`);
832
+ }
833
+ async function test4() {
834
+ let file = getAFile();
835
+ await lockFile(file);
836
+ let notDeleted = await isLocked(file, { type: "delete" });
837
+ assert(notDeleted, "File should not be deleted if it is locked");
838
+ await unlockFile(file);
839
+ }
840
+
841
+
842
+ async function runRandomTest() {
843
+ let testFn = [test1, test2, test3, test4][Math.floor(Math.random() * 4)];
844
+ console.log(`Running test: ${testFn.name}`);
845
+ await testFn();
846
+ }
847
+ for (let i = 0; i < 1000; i++) {
848
+ await runRandomTest();
849
+ }
850
+ }
851
+ //testLocks().catch(console.error).finally(() => process.exit(0));
852
+
853
+
854
+ async function testLargeFiles() {
855
+ const largeFile = "E:/downloads/Weird.Science.1985.EXTENDED.1080p.BluRay.H264.AAC-RARBG/weirdscience.mp4";
856
+ let test = wrapArchivesWithCache(getArchivesBackblaze("querysub.com-testbucket"));
857
+
858
+ /*
859
+ let cacheHandle: fs.promises.FileHandle = await fs.promises.open(largeFile, "r");
860
+ let data = Buffer.alloc(LARGE_FILE_CHUNK);
861
+ let pos = 0;
862
+ async function getNextData(): Promise<Buffer | undefined> {
863
+ try {
864
+ let read = await cacheHandle.read(data, 0, LARGE_FILE_CHUNK, pos);
865
+ if (read.bytesRead === 0) return undefined;
866
+ if (read.bytesRead < LARGE_FILE_CHUNK) {
867
+ data = data.slice(0, read.bytesRead);
868
+ }
869
+ pos += read.bytesRead;
870
+ return data;
871
+ } catch {
872
+ return undefined;
873
+ }
874
+ }
875
+
876
+ await test.setLargeFile({
877
+ path: "test.mp4",
878
+ getNextData,
879
+ });
880
+ */
881
+
882
+
883
+ await test.lockRegion(async fncs => {
884
+ let newPath = await fncs.getPathAndLock("copy.mp4");
885
+ console.log({ newPath });
886
+ });
887
+
888
+
889
+ /*
890
+ logErrors((async () => {
891
+ while (true) {
892
+ let first100Bytes = await test.get("copy.mp4", { range: { start: 0, end: 100 } });
893
+ console.log({ first100Bytes });
894
+ await delay(5000);
895
+ }
896
+ })());
897
+
898
+ {
899
+ let pos = 0;
900
+ async function getNextData(): Promise<Buffer | undefined> {
901
+ let data = await test.get("test.mp4", { range: { start: pos, end: pos + LARGE_FILE_CHUNK } });
902
+ if (!data?.length) return undefined;
903
+ pos += data.length;
904
+ return data;
905
+ }
906
+ await test.setLargeFile({
907
+ path: "copy.mp4",
908
+ getNextData,
909
+ });
910
+ }
911
+ */
912
+ }
913
+ //testLargeFiles().catch(console.error).finally(() => process.exit(0));