querysub 0.591.0 → 0.593.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 (44) hide show
  1. package/.claude/settings.local.json +5 -1
  2. package/bin/stop-machine.js +4 -0
  3. package/package.json +6 -3
  4. package/spec.txt +12 -0
  5. package/src/-a-archives/archiveCache.ts +28 -7
  6. package/src/-a-archives/archiveCache2.ts +4 -1
  7. package/src/-a-archives/archives.ts +2 -63
  8. package/src/-a-archives/archives2.ts +21 -17
  9. package/src/-a-archives/archivesDisk.ts +2 -13
  10. package/src/-a-archives/archivesMemoryCache.ts +27 -17
  11. package/src/-a-archives/archivesMemoryCache2.ts +23 -0
  12. package/src/-d-trust/NetworkTrust2.ts +3 -3
  13. package/src/-e-certs/certAuthority.ts +2 -2
  14. package/src/-f-node-discovery/NodeDiscovery.ts +3 -3
  15. package/src/0-path-value-core/AuthorityLookup.ts +0 -4
  16. package/src/0-path-value-core/PathRouter.ts +17 -16
  17. package/src/0-path-value-core/ShardPrefixes.ts +2 -4
  18. package/src/0-path-value-core/archiveLocks/ArchiveLocks2.ts +133 -38
  19. package/src/0-path-value-core/archiveLocks/archiveSnapshots.ts +0 -1
  20. package/src/0-path-value-core/pathValueArchives.ts +9 -5
  21. package/src/2-proxy/archiveMoveHarness.ts +1 -1
  22. package/src/4-deploy/edgeBootstrap.ts +50 -11
  23. package/src/4-deploy/edgeNodes.ts +7 -13
  24. package/src/4-querysub/Querysub.ts +2 -2
  25. package/src/archiveapps/archiveJoinEntry.ts +1 -1
  26. package/src/config2.ts +1 -7
  27. package/src/deployManager/components/ServicesListPage.tsx +27 -8
  28. package/src/deployManager/machineDaemonShared.ts +2 -0
  29. package/src/deployManager/machineSchema.ts +5 -6
  30. package/src/deployManager/setupMachineMain.ts +1 -2
  31. package/src/deployManager/stopMachineMain.ts +45 -0
  32. package/src/diagnostics/logs/IndexedLogs/IndexedLogs.ts +34 -21
  33. package/src/diagnostics/logs/IndexedLogs/MCPIndexedLogs.ts +3 -3
  34. package/src/diagnostics/logs/IndexedLogs/TimeFileTree.ts +11 -2
  35. package/src/diagnostics/logs/IndexedLogs/moveIndexLogsToPublic.ts +3 -4
  36. package/src/diagnostics/logs/errorTickets/tickets.ts +3 -4
  37. package/src/diagnostics/logs/lifeCycleAnalysis/lifeCycles.tsx +2 -4
  38. package/src/storageSetup.ts +1 -1
  39. package/src/-a-archives/archivesBackBlaze.ts +0 -156
  40. package/src/-a-archives/archivesCborT.ts +0 -52
  41. package/src/-a-archives/archivesLimitedCache.ts +0 -307
  42. package/src/-a-archives/archivesPrivateFileSystem.ts +0 -326
  43. package/src/-a-archives/copyLocalToBackblaze.ts +0 -24
  44. package/src/-b-authorities/cdnAuthority.ts +0 -53
@@ -31,7 +31,11 @@
31
31
  "WebFetch(domain:developers.cloudflare.com)",
32
32
  "PowerShell(yarn type *)",
33
33
  "PowerShell(Remove-Item -Confirm:$false \"C:\\\\Users\\\\quent\\\\.claude\\\\projects\\\\D--repos-querysub\\\\memory\\\\no-naming-new-bins.md\")",
34
- "Bash(python -)"
34
+ "Bash(python -)",
35
+ "PowerShell(yarn upreal *)",
36
+ "PowerShell($env:COMPUTERNAME; $env:USERNAME)",
37
+ "PowerShell($env:COMPUTERNAME)",
38
+ "PowerShell($env:USERNAME)"
35
39
  ]
36
40
  }
37
41
  }
@@ -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.591.0",
3
+ "version": "0.593.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,8 +74,8 @@
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.66",
75
- "socket-function": "^1.2.27",
77
+ "sliftutils": "^1.7.72",
78
+ "socket-function": "^1.2.29",
76
79
  "terser": "^5.31.0",
77
80
  "typenode": "^6.6.1",
78
81
  "typesafecss": "^0.32.0",
package/spec.txt CHANGED
@@ -1,3 +1,15 @@
1
+ It could be it's just taking a while to get valid. Let's check it two times to see. Although if it is, that's a bug, but it's probably an easy bug to fix.
2
+
3
+ Wait, what transaction is it applying now? If it's not a valid transaction and we don't even write it, so what?
4
+
5
+ Well, we might have to restore from a snapshot if it breaks it, but I'm pretty confident we're not going to lose all of our data.
6
+
7
+ Ugh... I mean it seems like our storage is just really out of sync. Like, we're reading files and it changes every time, etc.
8
+
9
+
10
+
11
+
12
+
1
13
  Trigger values
2
14
  1) validStateComputer.ingestValuesAndValidStates
3
15
  1.5) authorityStorage.ingestValues
@@ -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);
@@ -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
- }
@@ -4,6 +4,7 @@ import { cache, cacheJSONArgsEqual } from "socket-function/src/caching";
4
4
  import { formatDateTime } from "socket-function/src/formatting/format";
5
5
  import { getDomain } from "../config";
6
6
  import { timeInMinute } from "socket-function/src/misc";
7
+ import { FindConfig } from "sliftutils/storage/IArchives";
7
8
 
8
9
  export const STORAGE_ACCOUNT = "root";
9
10
 
@@ -44,24 +45,27 @@ function archiveBuilder(bucket: string, overrides: Partial<RemoteConfigBase>) {
44
45
  const ONTARIO = `https://99-250-124-91.querysubtest.com:5234/file/${STORAGE_ACCOUNT}/${bucket}/storage/storagerouting.json`;
45
46
  const HETZNER = `https://65-109-93-113.querysubtest.com:5234/file/${STORAGE_ACCOUNT}/${bucket}/storage/storagerouting.json`;
46
47
  const BACKBLAZE = `https://f002.backblazeb2.com/file/${bucket}/storage/storagerouting.json`;
48
+
49
+ function stripeSources(urls: string[]) {
50
+ let sources: Omit<Exclude<RemoteConfigBase, string>, "validWindow">[] = [];
51
+ for (let j = 0; j < urls.length; j++) {
52
+ // Shift by j
53
+ let currentUrls = urls.slice(j).concat(urls.slice(0, j));
54
+ for (let i = 0; i < urls.length; i++) {
55
+ sources.push({ type: "remote", url: currentUrls[i], route: [i / urls.length, (i + 1) / urls.length], });
56
+ }
57
+ }
58
+ return sources;
59
+ }
60
+
61
+
47
62
  let sources = createSourceWindows(overrides, [
48
63
  {
49
- startTime: +new Date("2026-07-20 18:00:00-04:00"),
50
- sources: [
51
- { type: "remote", url: ONTARIO, route: [0.5, 1], },
52
- { type: "remote", url: HETZNER, route: [0, 0.5], },
53
- { type: "remote", url: ONTARIO, },
54
- { type: "remote", url: HETZNER, },
55
- { type: "backblaze", url: BACKBLAZE, },
56
- ]
57
- },
58
- {
59
- startTime: +new Date("2026-07-21 02:59:00-04:00"),
64
+ startTime: +new Date("2026-07-20 00:00:00-04:00"),
60
65
  sources: [
61
- { type: "remote", url: ONTARIO, route: [0, 0.5], },
62
- { type: "remote", url: HETZNER, route: [0.5, 1], },
63
- { type: "remote", url: ONTARIO, },
64
- { type: "remote", url: HETZNER, },
66
+ // NOTE: Once we add new sources, we need to add something at a new start time in the future so we have time to move the files.
67
+ // TODO: EVENTUALLY, Once we have enough data, instead of restriping, we need to bisect the data. That way we can slowly move over the data instead of trying to rewrite all of our data all at once. However, for the moment, moving all the data is probably fine. On the data center, we should be able to move terabytes of data every hour, so we should be able to handle quite a bit, especially if we have a few days to run the restriping.
68
+ ...stripeSources([ONTARIO, HETZNER]),
65
69
  { type: "backblaze", url: BACKBLAZE, },
66
70
  ]
67
71
  },
@@ -89,8 +93,8 @@ function nestBucket(folder: string, archives: ReturnType<typeof createArchives>)
89
93
  get: (path: string, config?: GetConfig) => archives.get(folder + path, config),
90
94
  set: (path: string, data: Buffer, config?: SetConfig) => archives.set(folder + path, data, config),
91
95
  del: (path: string) => archives.del(folder + path),
92
- find: async (prefix: string, config?: { shallow?: boolean; type: "files" | "folders" }) => (await archives.find(folder + prefix, config)).map(x => x.slice(folder.length)),
93
- findInfo: async (prefix: string, config?: { shallow?: boolean; type: "files" | "folders" }) => (await archives.findInfo(folder + prefix, config)).map(x => ({ ...x, path: x.path.slice(folder.length) })),
96
+ find: async (prefix: string, config?: FindConfig) => (await archives.find(folder + prefix, config)).map(x => x.slice(folder.length)),
97
+ findInfo: async (prefix: string, config?: FindConfig) => (await archives.findInfo(folder + prefix, config)).map(x => ({ ...x, path: x.path.slice(folder.length) })),
94
98
  get2: (path: string, config?: GetConfig) => archives.get2(folder + path, config),
95
99
  setLargeFile: (config: { path: string; lastModified?: number; getNextData(): Promise<Buffer | undefined>; }) => archives.setLargeFile({ ...config, path: folder + config.path }),
96
100
  getInfo: (path: string) => archives.getInfo(folder + path),
@@ -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
@@ -87,7 +87,7 @@ export const isTrusted = measureWrap(async function isTrusted(machineId: string)
87
87
  }
88
88
  });
89
89
  let populateTrustedCache = lazy(async () => {
90
- let trustedMachineIds = await archives().find("");
90
+ let trustedMachineIds = await archives().find("", { fallbacks: true });
91
91
  lastArchivesTrusted = trustedMachineIds.slice();
92
92
  for (let trustedMachineId of trustedMachineIds) {
93
93
  trustedCache.add(trustedMachineId);
@@ -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() {
@@ -342,7 +342,7 @@ async function syncArchives() {
342
342
  if (isServer()) {
343
343
  // Make sure we are present
344
344
  await writeHeartbeat();
345
- let nodeIds = await archives().find("");
345
+ let nodeIds = await archives().find("", { fallbacks: true });
346
346
  console.info(`Synced node ids from archives`, { nodeIds });
347
347
  await setNodeIds(nodeIds);
348
348
  } else {
@@ -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";
@@ -13,6 +13,7 @@ import { sha256 } from "js-sha256";
13
13
  import { rangesOverlap, removeRange } from "../rangeMath";
14
14
  import { decodeParentFilter } from "./hackedPackedPathParentFiltering";
15
15
  import { getBufferInt } from "socket-function/src/bits";
16
+ import { safeLoop } from "socket-function/src/batching";
16
17
 
17
18
  import { LOCAL_DOMAIN, LOCAL_DOMAIN_PATH } from "./PathRouterConstants";
18
19
  export { LOCAL_DOMAIN, LOCAL_DOMAIN_PATH };
@@ -320,7 +321,7 @@ export class PathRouter {
320
321
  // NOTE: Encodes all the data, even if it matches our spec or not. If you don't want this, you have to filter first
321
322
  // - If this becomes an issue we COULD filter, as we can do it quickly, but I don't think it is required, as all the present usecases prefilter anyways.
322
323
  @measureFnc
323
- public static getPathIdentifierTargets(values: PathValue[], ourSpec: AuthoritySpec): Map<string, PathValue[]> {
324
+ public static async getPathIdentifierTargets(values: PathValue[], ourSpec: AuthoritySpec): Promise<Map<string, PathValue[]>> {
324
325
  // NOTE: The file size limit is 1024 bytes. But we also have our folder, etc, so we want to add enough buffer
325
326
  // - Shorter hashes means we can store more, but there's a point when the collisions make it less useful.
326
327
  const MAX_PREFIXES_PER_FILE = 50;
@@ -333,17 +334,17 @@ export class PathRouter {
333
334
  let prefixes = ourSpec.prefixes.slice();
334
335
  sort(prefixes, x => x.prefixPartLength);
335
336
 
336
- // NOTE: If there are few enough path values for a prefix, we don't even need to calculate the routing hash.
337
+ // NOTE: If there are few enough path values for a prefix, we don't even need to calculate the routing hash.
337
338
  let byPrefix = new Map<PrefixMatcher | undefined, PathValue[]>();
338
- for (let value of values) {
339
+ await safeLoop({ data: values, name: "PathRouter.getPathIdentifierTargets|groupByPrefix" }, value => {
339
340
  let prefix = getMatchingPrefix(ourSpec, value.path);
340
- let values = byPrefix.get(prefix);
341
- if (!values) {
342
- values = [];
343
- byPrefix.set(prefix, values);
341
+ let prefixValues = byPrefix.get(prefix);
342
+ if (!prefixValues) {
343
+ prefixValues = [];
344
+ byPrefix.set(prefix, prefixValues);
344
345
  }
345
- values.push(value);
346
- }
346
+ prefixValues.push(value);
347
+ });
347
348
 
348
349
  let prefixGroups = Array.from(byPrefix.entries()).map(([prefix, values]) => ({
349
350
  prefix,
@@ -391,17 +392,17 @@ export class PathRouter {
391
392
  }
392
393
 
393
394
  let finalFiles = new Map<string, PathValue[]>();
394
- for (let group of groups) {
395
+ await safeLoop({ data: groups, name: "PathRouter.getPathIdentifierTargets|shardGroups" }, async group => {
395
396
  if (group.count < SHARD_THRESHOLD) {
396
397
  let identifier = this.encodeIdentifier({ prefixes: group.prefixes, rangeStart: 0, rangeEnd: 1 });
397
398
  finalFiles.set(identifier, group.values.flat());
398
- continue;
399
+ return;
399
400
  }
400
401
  // Split by routing hash
401
- let values = group.values.flat();
402
- let splitCount = Math.max(MIN_SHARD_FILE_COUNT, Math.ceil(values.length / TARGET_SHARD_SIZE));
402
+ let groupValues = group.values.flat();
403
+ let splitCount = Math.max(MIN_SHARD_FILE_COUNT, Math.ceil(groupValues.length / TARGET_SHARD_SIZE));
403
404
  let byRouteGroup = new Map<number, PathValue[]>();
404
- for (let value of values) {
405
+ await safeLoop({ data: groupValues, name: "PathRouter.getPathIdentifierTargets|routeValues" }, value => {
405
406
  let route = this.getRouteFull({
406
407
  path: value.path,
407
408
  spec: {
@@ -418,14 +419,14 @@ export class PathRouter {
418
419
  byRouteGroup.set(routeIndex, routeValues);
419
420
  }
420
421
  routeValues.push(value);
421
- }
422
+ });
422
423
  for (let [routeIndex, routeValues] of byRouteGroup) {
423
424
  let rangeStart = routeIndex / splitCount;
424
425
  let rangeEnd = (routeIndex + 1) / splitCount;
425
426
  let identifier = this.encodeIdentifier({ prefixes: group.prefixes, rangeStart, rangeEnd });
426
427
  finalFiles.set(identifier, routeValues);
427
428
  }
428
- }
429
+ });
429
430
 
430
431
  // NOTE: There could be a huge number of prefixes and we can't pack them all into one file because of the prefix limit, so this will write any remaining values.
431
432
  if (remainingValues.length > 0) {
@@ -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