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.
Files changed (39) hide show
  1. package/bin/stop-machine.js +4 -0
  2. package/package.json +5 -2
  3. package/src/-a-archives/archiveCache.ts +28 -7
  4. package/src/-a-archives/archiveCache2.ts +5 -2
  5. package/src/-a-archives/archives.ts +2 -63
  6. package/src/-a-archives/archivesDisk.ts +2 -13
  7. package/src/-a-archives/archivesMemoryCache.ts +27 -17
  8. package/src/-a-archives/archivesMemoryCache2.ts +23 -0
  9. package/src/-d-trust/NetworkTrust2.ts +2 -2
  10. package/src/-e-certs/certAuthority.ts +2 -2
  11. package/src/-f-node-discovery/NodeDiscovery.ts +2 -2
  12. package/src/0-path-value-core/AuthorityLookup.ts +0 -4
  13. package/src/0-path-value-core/ShardPrefixes.ts +2 -4
  14. package/src/0-path-value-core/archiveLocks/ArchiveLocks2.ts +23 -10
  15. package/src/0-path-value-core/archiveLocks/archiveSnapshots.ts +0 -1
  16. package/src/0-path-value-core/pathValueArchives.ts +1 -0
  17. package/src/4-deploy/edgeBootstrap.ts +50 -11
  18. package/src/4-deploy/edgeNodes.ts +7 -13
  19. package/src/4-querysub/Querysub.ts +2 -2
  20. package/src/config2.ts +1 -7
  21. package/src/deployManager/components/ServiceMeasureBars.tsx +60 -50
  22. package/src/deployManager/components/ServicesListPage.tsx +29 -13
  23. package/src/deployManager/machineDaemonShared.ts +2 -0
  24. package/src/deployManager/machineSchema.ts +5 -6
  25. package/src/deployManager/setupMachineMain.ts +1 -2
  26. package/src/deployManager/stopMachineMain.ts +45 -0
  27. package/src/diagnostics/logs/IndexedLogs/IndexedLogs.ts +34 -21
  28. package/src/diagnostics/logs/IndexedLogs/MCPIndexedLogs.ts +3 -3
  29. package/src/diagnostics/logs/IndexedLogs/TimeFileTree.ts +11 -2
  30. package/src/diagnostics/logs/IndexedLogs/moveIndexLogsToPublic.ts +3 -4
  31. package/src/diagnostics/logs/errorTickets/tickets.ts +3 -4
  32. package/src/diagnostics/logs/lifeCycleAnalysis/lifeCycles.tsx +2 -4
  33. package/src/diagnostics/watchdog.ts +0 -5
  34. package/src/-a-archives/archivesBackBlaze.ts +0 -156
  35. package/src/-a-archives/archivesCborT.ts +0 -52
  36. package/src/-a-archives/archivesLimitedCache.ts +0 -307
  37. package/src/-a-archives/archivesPrivateFileSystem.ts +0 -326
  38. package/src/-a-archives/copyLocalToBackblaze.ts +0 -24
  39. package/src/-b-authorities/cdnAuthority.ts +0 -53
@@ -0,0 +1,4 @@
1
+ #!/usr/bin/env node
2
+
3
+ require("typenode");
4
+ require("../src/deployManager/stopMachineMain");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "querysub",
3
- "version": "0.590.0",
3
+ "version": "0.592.0",
4
4
  "main": "index.js",
5
5
  "license": "MIT",
6
6
  "note1": "note on node-forge fork, see https://github.com/digitalbazaar/forge/issues/744 for details",
@@ -26,6 +26,8 @@
26
26
  "mc2": "yarn typenode ./src/diagnostics/debugger/mcp-server.ts --cwd D:/repos/qs-cyoa/",
27
27
  "autofix": "yarn typenode ./src/diagnostics/logs/errorTickets/autoFixerEntry.ts",
28
28
  "parameters-timeline": "yarn typenode ./src/deployManager/parametersTimelineMain.ts",
29
+ "setup-machine": "yarn typenode ./src/deployManager/setupMachineMain.ts",
30
+ "stop-machine": "yarn typenode ./src/deployManager/stopMachineMain.ts",
29
31
  "ssh-a-claude": "ssh root@a.querysubtest.com"
30
32
  },
31
33
  "bin": {
@@ -38,6 +40,7 @@
38
40
  "machine": "./bin/machine.js",
39
41
  "machine-alwaysup": "./bin/machine-alwaysup.js",
40
42
  "setup-machine": "./bin/setup-machine.js",
43
+ "stop-machine": "./bin/stop-machine.js",
41
44
  "gc": "./bin/gc.js",
42
45
  "gc-watch": "./bin/gc-watch.js",
43
46
  "gc-watch-public": "./bin/gc-watch-public.js",
@@ -71,7 +74,7 @@
71
74
  "node-forge": "https://github.com/sliftist/forge#e618181b469b07bdc70b968b0391beb8ef5fecd6",
72
75
  "pako": "^2.1.0",
73
76
  "peggy": "^5.0.6",
74
- "sliftutils": "^1.7.65",
77
+ "sliftutils": "^1.7.69",
75
78
  "socket-function": "^1.2.27",
76
79
  "terser": "^5.31.0",
77
80
  "typenode": "^6.6.1",
@@ -138,11 +138,17 @@ const getDiskMetricsBase = async () => {
138
138
  // We require our input to be a temp file, so our update can be atomic
139
139
  async function addCacheFile(archives: CacheKeySource, key: string, sourceTempFile: string): Promise<void> {
140
140
  let path = getArchiveCachePath(archives, key);
141
+ let sourceTempFileStat = await fs.promises.stat(sourceTempFile);
142
+ if (sourceTempFileStat.size === 0) {
143
+ try {
144
+ await fs.promises.unlink(sourceTempFile);
145
+ } catch { }
146
+ throw new Error(`Expected non-empty data when caching ${key}, was 0 bytes. Empty files are treated as deletes by the storage layer, so they must never be written or cached.`);
147
+ }
141
148
  let hasFile = fileSizes.some(x => x.path === path);
142
149
  if (hasFile) {
143
150
  removeFile(path);
144
151
  }
145
- let sourceTempFileStat = await fs.promises.stat(sourceTempFile);
146
152
  availableBytes -= sourceTempFileStat.size;
147
153
  availableFiles--;
148
154
  sort(fileSizes, x => x.time);
@@ -196,14 +202,19 @@ const getDiskMetricsBase = async () => {
196
202
  let path = getArchiveCachePath(archives, key);
197
203
  let buffer: Buffer | undefined;
198
204
  let handle: fs.promises.FileHandle | undefined;
205
+ let isEmptyFile = false;
199
206
  try {
200
207
  handle = await fs.promises.open(path, "r");
201
208
  let stats = await handle.stat();
202
- let start = config?.range?.start ?? 0;
203
- let end = config?.range?.end ?? stats.size;
204
- let readBuffer = Buffer.alloc(end - start);
205
- let read = await handle.read(readBuffer, 0, end - start, start);
206
- buffer = readBuffer.slice(0, read.bytesRead);
209
+ if (stats.size === 0) {
210
+ isEmptyFile = true;
211
+ } else {
212
+ let start = config?.range?.start ?? 0;
213
+ let end = config?.range?.end ?? stats.size;
214
+ let readBuffer = Buffer.alloc(end - start);
215
+ let read = await handle.read(readBuffer, 0, end - start, start);
216
+ buffer = readBuffer.slice(0, read.bytesRead);
217
+ }
207
218
  }
208
219
  catch { }
209
220
  finally {
@@ -211,6 +222,13 @@ const getDiskMetricsBase = async () => {
211
222
  await handle.close();
212
223
  }
213
224
  }
225
+ if (isEmptyFile) {
226
+ removeFile(path);
227
+ try {
228
+ await fs.promises.unlink(path);
229
+ } catch { }
230
+ return undefined;
231
+ }
214
232
  if (buffer) {
215
233
  updateAccessTime(path);
216
234
  bumpAtimeIfStale(path);
@@ -355,7 +373,9 @@ export function wrapArchivesWithCache(archives: Archives, rootConfig?: {
355
373
  if (rootConfig?.immutable) {
356
374
  try {
357
375
  let stat = await fs.promises.stat(getArchiveCachePath(archives, fileName));
358
- return { writeTime: stat.mtimeMs, size: stat.size };
376
+ if (stat.size > 0) {
377
+ return { writeTime: stat.mtimeMs, size: stat.size };
378
+ }
359
379
  } catch {
360
380
  // Not in our cache yet — fall through to the source.
361
381
  }
@@ -363,6 +383,7 @@ export function wrapArchivesWithCache(archives: Archives, rootConfig?: {
363
383
  return archives.getInfo(fileName);
364
384
  },
365
385
  set: async (fileName: string, data: Buffer) => {
386
+ if (data.length === 0) throw new Error(`Expected non-empty data when setting ${fileName}, was 0 bytes. Empty writes are treated as deletes by the storage layer.`);
366
387
  let metrics = await getDiskMetrics();
367
388
  const tempPath = getTempFilePath();
368
389
  await fs.promises.writeFile(tempPath, data);
@@ -116,7 +116,7 @@ export function wrapArchivesWithCache2(archives: IArchives, rootConfig?: {
116
116
  get2: async (fileName: string, config?: GetConfig) => {
117
117
  let result = await archives.get2(fileName, config);
118
118
  // Range reads are partial, so they can't populate the cache.
119
- if (result && !config?.range) {
119
+ if (result?.data && !config?.range) {
120
120
  await cacheBuffer(fileName, result.data);
121
121
  }
122
122
  return result;
@@ -127,7 +127,9 @@ export function wrapArchivesWithCache2(archives: IArchives, rootConfig?: {
127
127
  if (rootConfig?.immutable) {
128
128
  try {
129
129
  let stat = await fs.promises.stat(getArchiveCachePath(archives, fileName));
130
- return { writeTime: stat.mtimeMs, size: stat.size };
130
+ if (stat.size > 0) {
131
+ return { writeTime: stat.mtimeMs, size: stat.size };
132
+ }
131
133
  } catch {
132
134
  // Not in our cache yet — fall through to the source.
133
135
  }
@@ -135,6 +137,7 @@ export function wrapArchivesWithCache2(archives: IArchives, rootConfig?: {
135
137
  return archives.getInfo(fileName);
136
138
  },
137
139
  set: async (fileName: string, data: Buffer, config?: SetConfig) => {
140
+ if (data.length === 0) throw new Error(`Expected non-empty data when setting ${fileName}, was 0 bytes. Empty writes are treated as deletes by the storage layer.`);
138
141
  let writtenKey = await archives.set(fileName, data, config);
139
142
  // VARIABLE_SHARD keys materialize into a different key on write, in which case
140
143
  // fileName isn't the readable key, so caching under it would be wrong.
@@ -1,14 +1,4 @@
1
- import { isNode } from "socket-function/src/misc";
2
- import { getDomain, isNoNetwork } from "../config";
3
- import { getStorageDir, getSubFolder } from "../fs";
4
- import { getArchivesBackblaze, hasBackblazePermissions } from "./archivesBackBlaze";
5
- import { getArchivesLocal } from "./archivesDisk";
6
- import { magenta } from "socket-function/src/formatting/logColors";
7
- import debugbreak from "debugbreak";
8
- import { isClient } from "../config2";
9
- import { wrapArchivesWithCache } from "./archiveCache";
10
1
  import { Args } from "socket-function/src/types";
11
- import { IArchives } from "sliftutils/storage/IArchives";
12
2
 
13
3
  export interface Archives {
14
4
  getDebugName(): string;
@@ -105,8 +95,8 @@ export function createArchivesOverride<T extends Partial<Archives>>(
105
95
  };
106
96
  }
107
97
 
108
- export function nestArchives(path: string, archives: Archives): Archives & IArchives {
109
- if (!path) return archives as any;
98
+ export function nestArchives(path: string, archives: Archives): Archives {
99
+ if (!path) return archives;
110
100
  if (!path.endsWith("/")) {
111
101
  path = path + "/";
112
102
  }
@@ -116,7 +106,6 @@ export function nestArchives(path: string, archives: Archives): Archives & IArch
116
106
  }
117
107
  return file;
118
108
  }
119
- // NOTE: Just ignore the errors in here. We're going to be getting rid of this soon enough.
120
109
  return {
121
110
  getDebugName: () => archives.getDebugName() + "/" + path,
122
111
  get: (fileName: string, config) => archives.get(path + stripFilePrefix(fileName), config),
@@ -153,53 +142,3 @@ export function nestArchives(path: string, archives: Archives): Archives & IArch
153
142
  };
154
143
  }
155
144
 
156
- let archivesLogging = false;
157
- export function enableArchivesLogging() {
158
- archivesLogging = true;
159
- }
160
-
161
- let cached = new Map<string, Archives>();
162
- export function getArchives(rootPath = ""): Archives {
163
- let value = cached.get(rootPath);
164
- if (!value) {
165
- value = getArchivesBase(rootPath);
166
- cached.set(rootPath, value);
167
- }
168
- return value;
169
- }
170
- export function getArchivesBase(rootPath = ""): Archives {
171
- let archives: Archives;
172
- let domain = getDomain();
173
- if (isNoNetwork()) {
174
- archives = getArchivesLocal(domain);
175
- archives = nestArchives(rootPath, archives);
176
- } else {
177
- archives = getArchivesBackblaze(domain);
178
- archives = nestArchives(rootPath, archives);
179
- // Why did we think we should always cache? We absolutely should not. Even if we were checking
180
- // file hashes on every read (which we do not), it would still potentially fill the cache up
181
- // with large temporary files we have no intention of using (which might evict entries we
182
- // intend to use).
183
- //archives = wrapArchivesWithCache(archives);
184
- }
185
- if (archivesLogging) {
186
- archives.enableLogging();
187
- }
188
- return archives;
189
- }
190
-
191
- export function hasArchivesPermissions() {
192
- if (isNoNetwork()) {
193
- return true;
194
- }
195
- return hasBackblazePermissions();
196
- }
197
-
198
- if (isNode()) {
199
- if (isNoNetwork()) {
200
- console.log("Primary data storage: " + magenta(getStorageDir()));
201
- } else {
202
- console.log("Config storage path: " + magenta(getStorageDir()));
203
- console.log("Primary data storage: " + magenta(`backblaze:${getDomain()}`));
204
- }
205
- }
@@ -1,12 +1,12 @@
1
1
  import { isNode } from "socket-function/src/misc";
2
2
  import os from "os";
3
- import { fsExistsAsync, getSubFolder } from "../fs";
3
+ import { fsExistsAsync } from "../fs";
4
4
  import fs from "fs";
5
5
  import { blue, red, yellow } from "socket-function/src/formatting/logColors";
6
6
  import { measureFnc } from "socket-function/src/profiling/measure";
7
7
  import { delay } from "socket-function/src/batching";
8
8
  import debugbreak from "debugbreak";
9
- import { Archives, getArchives } from "./archives";
9
+ import { Archives } from "./archives";
10
10
  import { cache, lazy } from "socket-function/src/caching";
11
11
  import { isDefined } from "../misc";
12
12
 
@@ -433,17 +433,6 @@ function unescapeFileName(fileName: string): string {
433
433
  return fileName;
434
434
  }
435
435
 
436
- export const getArchivesLocal = cache((domain: string): Archives => {
437
-
438
- const archivesLocal = new ArchivesDisk();
439
-
440
- if (isNode()) {
441
- archivesLocal.LOCAL_ARCHIVE_FOLDER = getSubFolder(domain).replaceAll("\\", "/");
442
- }
443
-
444
- return archivesLocal;
445
- });
446
-
447
436
  export const getArchivesHome = cache((domain: string): Archives => {
448
437
  const archivesLocal = new ArchivesDisk();
449
438
 
@@ -24,21 +24,29 @@ export function testDisableCache() {
24
24
  cacheDisabled = true;
25
25
  }
26
26
 
27
+ export type MemoryCacheConfig = {
28
+ extraReadSize?: number;
29
+ maxSize?: number;
30
+ maxCount?: number;
31
+ // If the files are guaranteed to be immutable, then all reads, including reading entire file and reading any range, can be cached.
32
+ // - Otherwise, we can't cache when the entire file is read, as there might be further appends after it.
33
+ // - Also, it means if there's a range read that happens, if that range read doesn't return enough bytes, for example, we try to read from 0 to 100, but we only get 50 bytes back, we can only cache the 0 to 50, and then if they read from 0 to 100 again, we can use the cache, but we have to also read 50 to 100 from the source, because the file might have gotten larger.
34
+ fullyImmutable?: boolean;
35
+ stats?: ArchivesMemoryCacheStats;
36
+ sizeCache?: Map<string, number>;
37
+ };
38
+
39
+ /** The operations the cache actually needs from its source, so any archives flavor (old Archives or IArchives) can be cached. */
40
+ export type MemoryCacheSource = {
41
+ get(path: string, config?: { range?: { start: number; end: number; } }): Promise<Buffer | undefined>;
42
+ getInfo(path: string): Promise<{ size: number } | undefined>;
43
+ };
44
+
27
45
  /** Creates an in-memory cache for the archives. We expect that the values will only have values appended to them or be deleted. Also, if a value is deleted, we won't erase our in-memory cache, and so we will still allow reading from deleted values. */
28
- export function createArchivesMemoryCache(
29
- archives: Archives,
30
- config?: {
31
- extraReadSize?: number;
32
- maxSize?: number;
33
- maxCount?: number;
34
- // If the files are guaranteed to be immutable, then all reads, including reading entire file and reading any range, can be cached.
35
- // - Otherwise, we can't cache when the entire file is read, as there might be further appends after it.
36
- // - Also, it means if there's a range read that happens, if that range read doesn't return enough bytes, for example, we try to read from 0 to 100, but we only get 50 bytes back, we can only cache the 0 to 50, and then if they read from 0 to 100 again, we can use the cache, but we have to also read 50 to 100 from the source, because the file might have gotten larger.
37
- fullyImmutable?: boolean;
38
- stats?: ArchivesMemoryCacheStats;
39
- sizeCache?: Map<string, number>;
40
- }
41
- ): Archives {
46
+ export function createMemoryCachedGet(
47
+ archives: MemoryCacheSource,
48
+ config?: MemoryCacheConfig
49
+ ) {
42
50
  let {
43
51
  maxSize = 1024 * 1024 * 1024 * 4,
44
52
  maxCount = 1000 * 1000,
@@ -221,8 +229,6 @@ export function createArchivesMemoryCache(
221
229
 
222
230
  async function cachedGet(path: string, getConfig?: {
223
231
  range?: { start: number; end: number; };
224
- retryCount?: number;
225
- fastRead?: boolean;
226
232
  }): Promise<Buffer | undefined> {
227
233
  if (cacheDisabled) {
228
234
  let result = await archives.get(path, getConfig);
@@ -374,7 +380,11 @@ export function createArchivesMemoryCache(
374
380
  return result;
375
381
  }
376
382
 
383
+ return cachedGet;
384
+ }
385
+
386
+ export function createArchivesMemoryCache(archives: Archives, config?: MemoryCacheConfig): Archives {
377
387
  return createArchivesOverride(archives, {
378
- get: cachedGet
388
+ get: createMemoryCachedGet(archives, config),
379
389
  });
380
390
  }
@@ -0,0 +1,23 @@
1
+ import { IArchives } from "sliftutils/storage/IArchives";
2
+ import { createMemoryCachedGet, MemoryCacheConfig } from "./archivesMemoryCache";
3
+
4
+ /** See createMemoryCachedGet — the same in-memory range cache, wrapping an IArchives (archives2). */
5
+ export function createArchivesMemoryCache2(archives: IArchives, config?: MemoryCacheConfig): IArchives {
6
+ let cachedGet = createMemoryCachedGet(archives, config);
7
+ return {
8
+ get: cachedGet,
9
+ getDebugName: () => archives.getDebugName(),
10
+ hasWriteAccess: () => archives.hasWriteAccess(),
11
+ get2: (fileName, getConfig) => archives.get2(fileName, getConfig),
12
+ set: (fileName, data, setConfig) => archives.set(fileName, data, setConfig),
13
+ del: (fileName) => archives.del(fileName),
14
+ setLargeFile: (largeConfig) => archives.setLargeFile(largeConfig),
15
+ getInfo: (fileName) => archives.getInfo(fileName),
16
+ find: (prefix, findConfig) => archives.find(prefix, findConfig),
17
+ findInfo: (prefix, findConfig) => archives.findInfo(prefix, findConfig),
18
+ getURL: (path) => archives.getURL(path),
19
+ getConfig: () => archives.getConfig(),
20
+ getChangesAfter2: (changesConfig) => archives.getChangesAfter2(changesConfig),
21
+ getSyncStatus: archives.getSyncStatus && (() => archives.getSyncStatus!()),
22
+ };
23
+ }
@@ -1,6 +1,6 @@
1
1
  import { measureWrap } from "socket-function/src/profiling/measure";
2
2
  import { getIdentityCA, getMachineId, getOwnMachineId } from "sliftutils/misc/https/certs";
3
- import { getArchives } from "../-a-archives/archives";
3
+ import { getArchives2 } from "../-a-archives/archives2";
4
4
  import { isNode, throttleFunction, timeInHour, timeInSecond } from "socket-function/src/misc";
5
5
  import { SocketFunctionHook } from "socket-function/SocketFunctionTypes";
6
6
  import { SocketFunction } from "socket-function/SocketFunction";
@@ -21,7 +21,7 @@ import { magenta } from "socket-function/src/formatting/logColors";
21
21
  const UNTRUST_CACHE_TIME = 30 * timeInSecond;
22
22
  const TRUSTED_CACHE_RESET_INTERVAL = timeInHour;
23
23
 
24
- const archives = lazy(() => getArchives("trust2/"));
24
+ const archives = lazy(() => getArchives2("trust2/"));
25
25
 
26
26
  export const requiresNetworkTrustHook: SocketFunctionHook = async config => {
27
27
  // HACK: On the clientside we strip the domain process and machine id, so we can no longer determine
@@ -5,13 +5,13 @@ import { cache, lazy } from "socket-function/src/caching";
5
5
  import { hasDNSWritePermissions, setRecord } from "../-b-authorities/dnsAuthority";
6
6
  import { magenta } from "socket-function/src/formatting/logColors";
7
7
  import { createKeyStore } from "../persistentLocalStore";
8
- import { getArchives } from "../-a-archives/archives";
8
+ import { getArchives2 } from "../-a-archives/archives2";
9
9
  import { isNoNetwork } from "../config";
10
10
  import { formatDateTime, formatTime } from "socket-function/src/formatting/format";
11
11
  import { delay } from "socket-function/src/batching";
12
12
  import { timeInMinute } from "socket-function/src/misc";
13
13
 
14
- const archives = lazy(() => getArchives(`https_certs_3/`));
14
+ const archives = lazy(() => getArchives2(`https_certs_3/`));
15
15
  // Expire EXPIRATION_THRESHOLD% of the way through the certificate's lifetime
16
16
  const EXPIRATION_THRESHOLD = 0.4;
17
17
  // Let's Encrypt only accepts RSA or ECDSA account keys (not Ed25519, which is all sliftutils/certs generates).
@@ -1,5 +1,5 @@
1
1
  import { SocketFunction } from "socket-function/SocketFunction";
2
- import { getArchives } from "../-a-archives/archives";
2
+ import { getArchives2 } from "../-a-archives/archives2";
3
3
  import { getDomain, isDevDebugbreak, isNoNetwork, isPublic } from "../config";
4
4
  import { measureBlock } from "socket-function/src/profiling/measure";
5
5
  import { isNode, keyByArray, sha256Hash, throttleFunction, timeInMinute, timeInSecond } from "socket-function/src/misc";
@@ -56,7 +56,7 @@ let DEAD_NODE_POLL_COOLDOWN = timeInMinute * 5;
56
56
 
57
57
  let shutdown = false;
58
58
 
59
- const archives = lazy(() => getArchives("nodes/"));
59
+ const archives = lazy(() => getArchives2("nodes/"));
60
60
 
61
61
  let logging = true;
62
62
  export function enableNodeDiscoveryLogging() {
@@ -1,8 +1,4 @@
1
1
  import { sort, timeInMinute, timeInSecond } from "socket-function/src/misc";
2
- import { nestArchives } from "../-a-archives/archives";
3
- import { getArchivesBackblaze } from "../-a-archives/archivesBackBlaze";
4
- import { archiveJSONT } from "../-a-archives/archivesJSONT";
5
- import { getDomain, isPublic } from "../config";
6
2
  import { cache, lazy } from "socket-function/src/caching";
7
3
  import { SocketFunction } from "socket-function/SocketFunction";
8
4
  import { delay, runInSerial, runInfinitePollCallAtStart } from "socket-function/src/batching";
@@ -10,10 +10,8 @@
10
10
  */
11
11
 
12
12
  import { isNode } from "typesafecss";
13
- import { nestArchives } from "../-a-archives/archives";
14
- import { getArchivesBackblaze } from "../-a-archives/archivesBackBlaze";
13
+ import { getArchives2 } from "../-a-archives/archives2";
15
14
  import { archiveJSONT } from "../-a-archives/archivesJSONT";
16
- import { getDomain } from "../config";
17
15
  import { sort } from "socket-function/src/misc";
18
16
  import { getPathFromStr, getPathStr } from "../path";
19
17
  import { green, red } from "socket-function/src/formatting/logColors";
@@ -22,7 +20,7 @@ type PrefixObj = {
22
20
  prefixes: string[];
23
21
  };
24
22
 
25
- let prefixes = archiveJSONT<PrefixObj>(() => nestArchives("shard-prefixes/", getArchivesBackblaze(getDomain())));
23
+ let prefixes = archiveJSONT<PrefixObj>(() => getArchives2("shard-prefixes/"));
26
24
  // We don't actually need a collection, we only need a single value. However, the collection interface is a lot more convenient than manually dealing with backplays files.
27
25
  const key = "all";
28
26
 
@@ -32,6 +32,7 @@ const ARCHIVE_PROPAGATION_TIME = timeInSecond * 5;
32
32
  const MAX_APPLY_TRIES = 10;
33
33
  const CONCURRENT_READ_COUNT = 32;
34
34
  const CONCURRENT_WRITE_COUNT = 16;
35
+ const CONCURRENT_TRANSACTION_READS = 8;
35
36
 
36
37
  /** Moves by copy-then-delete (IArchives has no move). copyArchiveFile stamps the target with the source's write time, so ordering-by-writeTime survives the move. A missing source means another process already moved it, which is fine. */
37
38
  async function moveArchiveFile(from: IArchives, to: IArchives, path: string): Promise<void> {
@@ -391,7 +392,7 @@ class TransactionLocker {
391
392
  let path = this.getConfirmKey(key);
392
393
  console.info(`Creating confirmation for ${key}, ${countLeft} left`);
393
394
  if (!await this.storage.getValue(path)) {
394
- await this.storage.setValue(path, Buffer.from(""));
395
+ await this.storage.setValue(path, Buffer.from(JSON.stringify({ time: Date.now(), node: getOwnNodeId() })));
395
396
  }
396
397
  return path;
397
398
  }
@@ -410,7 +411,11 @@ class TransactionLocker {
410
411
  if (!op) return;
411
412
  if (op.type === "create") {
412
413
  let key = op.key;
413
- await this.storage.setValue(key, op.value || Buffer.from(""));
414
+ const value = op.value;
415
+ if (!value || value.length === 0) {
416
+ throw new Error(`Create op for ${key} has an empty value. Empty files are treated as deletes by the storage layer, so this create would vanish and the transaction could never be confirmed.`);
417
+ }
418
+ await this.storage.setValue(key, value);
414
419
  }
415
420
  }
416
421
  };
@@ -494,11 +499,16 @@ class TransactionLocker {
494
499
  createTime: number;
495
500
  source: FileInfo;
496
501
  })[] = [];
497
- for (let file of files) {
498
- if (!file.file.endsWith(".transaction")) continue;
499
- let tFile = file;
502
+ let transactionFiles = files.filter(x => x.file.endsWith(".transaction"));
503
+ // Set when any reader finds a missing value, so the other parallel readers stop early.
504
+ let missingValue = false;
505
+ const readTransaction = async (tFile: FileInfo) => {
506
+ if (missingValue) return;
500
507
  let buffer = await this.storage.getValue(tFile.file);
501
- if (!buffer) return false;
508
+ if (!buffer) {
509
+ missingValue = true;
510
+ return;
511
+ }
502
512
  bufferCache.set(tFile.file, buffer);
503
513
  let transaction: Transaction | undefined;
504
514
  try {
@@ -507,7 +517,7 @@ class TransactionLocker {
507
517
  // Maybe it is actively being written to? Or it is just broken. Skip it.
508
518
  console.error(`Failed to parse transaction. Ignoring, ${tFile.file}, ${e.stack}`);
509
519
  }
510
- if (!transaction) continue;
520
+ if (!transaction) return;
511
521
 
512
522
  let { name } = parsePath(tFile.file);
513
523
  let kvp = parseFileNameKVP(name);
@@ -518,8 +528,11 @@ class TransactionLocker {
518
528
  createTime: tFile.createTime,
519
529
  source: tFile,
520
530
  });
521
- }
531
+ };
532
+ let readTransactionParallel = runInParallel({ parallelCount: CONCURRENT_TRANSACTION_READS }, readTransaction);
533
+ await Promise.all(transactionFiles.map(readTransactionParallel));
522
534
  this.perf.checkpoint("read transaction files");
535
+ if (missingValue) return false;
523
536
 
524
537
  // Check all of our files to see if any have changed (or if there a new files).
525
538
  // Because we don't reuse file names, or change them, it means if a file exists
@@ -664,7 +677,7 @@ class TransactionLocker {
664
677
  }
665
678
  return true;
666
679
  }
667
- private async applyTransaction(transaction: Transaction): Promise<void> {
680
+ private async applyTransaction(transaction: Transaction & { source: FileInfo }): Promise<void> {
668
681
  let createCount = transaction.ops.filter(a => a.type === "create").length;
669
682
  let deleteCount = transaction.ops.filter(a => a.type === "delete").length;
670
683
  let lockedFiles = transaction.lockedFilesMustEqual?.length;
@@ -685,7 +698,7 @@ class TransactionLocker {
685
698
  if (op.type === "create") {
686
699
  await this.createConfirm(op.key, opsRemaining.length);
687
700
  } else if (op.type === "delete") {
688
- await this.deleteDataFile(op.key, `transaction (${getOwnNodeId()})`, opsRemaining.length);
701
+ await this.deleteDataFile(op.key, `transaction for ${transaction.source.file}`, opsRemaining.length);
689
702
  } else {
690
703
  let unhandled: never = op;
691
704
  throw new Error(`Unhandled type: ${unhandled}`);
@@ -1,5 +1,4 @@
1
1
  import { sha256 } from "js-sha256";
2
- import { getArchives } from "../../-a-archives/archives";
3
2
  import { getAllNodeIds, getOwnThreadId } from "../../-f-node-discovery/NodeDiscovery";
4
3
  import { archives, pathValueArchives } from "../pathValueArchives";
5
4
  import { ignoreErrors, logErrors, timeoutToUndefinedSilent } from "../../errors";
@@ -139,6 +139,7 @@ export class PathValueArchives {
139
139
  if (!config?.noFilter) {
140
140
  values = await this.filterForDiskStorage(values);
141
141
  }
142
+ if (values.length === 0) return undefined;
142
143
 
143
144
  let buffers = await pathValueSerializer.serialize(values, {
144
145
  noLocks: true,
@@ -18,26 +18,28 @@ declare global {
18
18
  var getEdgeNodeConfig: undefined | (() => Promise<EdgeNodeConfig>);
19
19
  }
20
20
 
21
- let getCachedConfig = cache(async (url: string): Promise<EdgeNodesIndex | undefined> => {
21
+ let getCachedConfig = cache(async (urlsKey: string): Promise<EdgeNodesIndex | undefined> => {
22
22
  setTimeout(() => {
23
- getCachedConfig.clear(url);
23
+ getCachedConfig.clear(urlsKey);
24
24
  // NOTE: We want deploying to be fast, so we don't cache for long. We also don't
25
25
  // get the new value every 30 seconds, as we don't want to create too many requests.
26
26
  }, timeInSecond * 30);
27
27
 
28
- try {
29
- let response = await fetch(url);
30
- return await response.json();
31
- } catch {
32
- return undefined;
28
+ for (let url of urlsKey.split("\n")) {
29
+ try {
30
+ let response = await fetch(url);
31
+ if (!response.ok) continue;
32
+ return await response.json();
33
+ } catch { }
33
34
  }
35
+ return undefined;
34
36
  });
35
37
 
36
38
  export async function getEdgeBootstrapScript(config: {
37
- edgeNodeConfigURL: string;
39
+ edgeNodeConfigURLs: string[];
38
40
  }): Promise<string> {
39
41
  return await measureBlock(async function getEdgeBootstrapScript() {
40
- let cachedConfig = await getCachedConfig(config.edgeNodeConfigURL);
42
+ let cachedConfig = await getCachedConfig(config.edgeNodeConfigURLs.join("\n"));
41
43
  let allowedArgs = ["--local"];
42
44
  // TODO: Be smarter about this (getting values and not just flags)
43
45
  let argv = process.argv.filter(x => allowedArgs.includes(x));
@@ -72,7 +74,7 @@ declare global {
72
74
  */
73
75
 
74
76
  async function edgeNodeFunction(config: {
75
- edgeNodeConfigURL: string;
77
+ edgeNodeConfigURLs: string[];
76
78
  cachedConfig: EdgeNodesIndex | undefined;
77
79
  argv: string[];
78
80
  }) {
@@ -302,8 +304,45 @@ async function edgeNodeFunction(config: {
302
304
  return false;
303
305
  }
304
306
  }
307
+ // Fetches all the URLs at the same time and uses whichever responds first, aborting the rest (so slow sources don't hold sockets — chrome limits us to 6 per domain). Racing redundant sources is normally wasteful, but this is the very first request of the page load, so minimizing its latency is a huge win.
308
+ async function fetchFirstEdgeIndex(urls: string[]): Promise<EdgeNodesIndex> {
309
+ let controllers = urls.map(() => new AbortController());
310
+ return await new Promise<EdgeNodesIndex>((resolve, reject) => {
311
+ let resolved = false;
312
+ let pendingCount = urls.length;
313
+ let errors: string[] = [];
314
+ if (!pendingCount) {
315
+ reject(new Error(`No edge node config URLs to fetch`));
316
+ return;
317
+ }
318
+ urls.forEach((url, index) => {
319
+ void (async () => {
320
+ try {
321
+ let response = await fetch(url, { signal: controllers[index].signal });
322
+ if (!response.ok) throw new Error(`Bad status ${response.status} from ${url}`);
323
+ let edgeIndex = await response.json() as EdgeNodesIndex;
324
+ if (resolved) return;
325
+ resolved = true;
326
+ for (let otherIndex = 0; otherIndex < controllers.length; otherIndex++) {
327
+ if (otherIndex !== index) {
328
+ controllers[otherIndex].abort();
329
+ }
330
+ }
331
+ resolve(edgeIndex);
332
+ } catch (e: any) {
333
+ errors.push(`${url}: ${e?.stack ?? e}`);
334
+ pendingCount--;
335
+ if (!resolved && pendingCount === 0) {
336
+ reject(new Error(`All edge node config URLs failed: ${errors.join(" | ")}`));
337
+ }
338
+ }
339
+ })();
340
+ });
341
+ });
342
+ }
343
+
305
344
  async function getEdgeNodeConfig(): Promise<EdgeNodeConfig> {
306
- let edgeIndex = cachedConfig || await (await fetch(config.edgeNodeConfigURL)).json() as EdgeNodesIndex;
345
+ let edgeIndex = cachedConfig || await fetchFirstEdgeIndex(config.edgeNodeConfigURLs);
307
346
  cachedConfig = undefined;
308
347
 
309
348
  edgeIndex.edgeNodes.sort((a, b) => -(a.bootTime - b.bootTime));