querysub 0.403.0 → 0.405.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 (108) hide show
  1. package/.cursorrules +2 -0
  2. package/bin/audit-imports.js +4 -0
  3. package/bin/join.js +1 -1
  4. package/package.json +7 -4
  5. package/spec.txt +77 -0
  6. package/src/-a-archives/archiveCache.ts +9 -4
  7. package/src/-a-archives/archivesBackBlaze.ts +1039 -1039
  8. package/src/-a-auth/certs.ts +0 -12
  9. package/src/-c-identity/IdentityController.ts +12 -3
  10. package/src/-f-node-discovery/NodeDiscovery.ts +32 -26
  11. package/src/-g-core-values/NodeCapabilities.ts +12 -2
  12. package/src/0-path-value-core/AuthorityLookup.ts +239 -0
  13. package/src/0-path-value-core/LockWatcher2.ts +150 -0
  14. package/src/0-path-value-core/PathRouter.ts +543 -0
  15. package/src/0-path-value-core/PathRouterRouteOverride.ts +72 -0
  16. package/src/0-path-value-core/PathRouterServerAuthoritySpec.tsx +73 -0
  17. package/src/0-path-value-core/PathValueCommitter.ts +222 -488
  18. package/src/0-path-value-core/PathValueController.ts +277 -239
  19. package/src/0-path-value-core/PathWatcher.ts +534 -0
  20. package/src/0-path-value-core/ShardPrefixes.ts +31 -0
  21. package/src/0-path-value-core/ValidStateComputer.ts +303 -0
  22. package/src/0-path-value-core/archiveLocks/ArchiveLocks.ts +1 -1
  23. package/src/0-path-value-core/archiveLocks/ArchiveLocks2.ts +80 -44
  24. package/src/0-path-value-core/archiveLocks/archiveSnapshots.ts +13 -16
  25. package/src/0-path-value-core/auditLogs.ts +2 -0
  26. package/src/0-path-value-core/hackedPackedPathParentFiltering.ts +97 -0
  27. package/src/0-path-value-core/pathValueArchives.ts +491 -492
  28. package/src/0-path-value-core/pathValueCore.ts +195 -1496
  29. package/src/0-path-value-core/startupAuthority.ts +74 -0
  30. package/src/1-path-client/RemoteWatcher.ts +90 -82
  31. package/src/1-path-client/pathValueClientWatcher.ts +808 -815
  32. package/src/2-proxy/PathValueProxyWatcher.ts +10 -8
  33. package/src/2-proxy/archiveMoveHarness.ts +182 -214
  34. package/src/2-proxy/garbageCollection.ts +9 -8
  35. package/src/2-proxy/schema2.ts +21 -1
  36. package/src/3-path-functions/PathFunctionHelpers.ts +206 -180
  37. package/src/3-path-functions/PathFunctionRunner.ts +943 -766
  38. package/src/3-path-functions/PathFunctionRunnerMain.ts +5 -3
  39. package/src/3-path-functions/pathFunctionLoader.ts +2 -2
  40. package/src/3-path-functions/syncSchema.ts +596 -521
  41. package/src/4-deploy/deployFunctions.ts +19 -4
  42. package/src/4-deploy/deployGetFunctionsInner.ts +8 -2
  43. package/src/4-deploy/deployMain.ts +51 -68
  44. package/src/4-deploy/edgeClientWatcher.tsx +6 -1
  45. package/src/4-deploy/edgeNodes.ts +2 -2
  46. package/src/4-dom/qreact.tsx +2 -4
  47. package/src/4-dom/qreactTest.tsx +7 -13
  48. package/src/4-querysub/Querysub.ts +21 -8
  49. package/src/4-querysub/QuerysubController.ts +45 -29
  50. package/src/4-querysub/permissions.ts +2 -2
  51. package/src/4-querysub/querysubPrediction.ts +80 -70
  52. package/src/4-querysub/schemaHelpers.ts +5 -1
  53. package/src/5-diagnostics/GenericFormat.tsx +14 -9
  54. package/src/archiveapps/archiveGCEntry.tsx +9 -2
  55. package/src/archiveapps/archiveJoinEntry.ts +96 -84
  56. package/src/bits.ts +19 -0
  57. package/src/config.ts +21 -3
  58. package/src/config2.ts +23 -48
  59. package/src/deployManager/components/DeployPage.tsx +7 -3
  60. package/src/deployManager/machineSchema.ts +4 -1
  61. package/src/diagnostics/ActionsHistory.ts +3 -8
  62. package/src/diagnostics/AuditLogPage.tsx +2 -3
  63. package/src/diagnostics/FunctionCallInfo.tsx +141 -0
  64. package/src/diagnostics/FunctionCallInfoState.ts +162 -0
  65. package/src/diagnostics/MachineThreadInfo.tsx +1 -1
  66. package/src/diagnostics/NodeViewer.tsx +37 -48
  67. package/src/diagnostics/SyncTestPage.tsx +241 -0
  68. package/src/diagnostics/auditImportViolations.ts +185 -0
  69. package/src/diagnostics/listenOnDebugger.ts +3 -3
  70. package/src/diagnostics/logs/IndexedLogs/BufferUnitSet.ts +10 -4
  71. package/src/diagnostics/logs/IndexedLogs/IndexedLogs.ts +2 -2
  72. package/src/diagnostics/logs/IndexedLogs/LogViewer3.tsx +24 -22
  73. package/src/diagnostics/logs/IndexedLogs/moveIndexLogsToPublic.ts +1 -1
  74. package/src/diagnostics/logs/diskLogGlobalContext.ts +1 -0
  75. package/src/diagnostics/logs/errorNotifications2/logWatcher.ts +1 -3
  76. package/src/diagnostics/logs/lifeCycleAnalysis/LifeCycleEntryEditor.tsx +34 -16
  77. package/src/diagnostics/logs/lifeCycleAnalysis/LifeCycleEntryReadMode.tsx +4 -6
  78. package/src/diagnostics/logs/lifeCycleAnalysis/LifeCycleInstanceTableView.tsx +36 -5
  79. package/src/diagnostics/logs/lifeCycleAnalysis/LifeCyclePage.tsx +19 -5
  80. package/src/diagnostics/logs/lifeCycleAnalysis/LifeCycleRenderer.tsx +15 -7
  81. package/src/diagnostics/logs/lifeCycleAnalysis/NestedLifeCycleInfo.tsx +28 -106
  82. package/src/diagnostics/logs/lifeCycleAnalysis/lifeCycleMatching.ts +2 -0
  83. package/src/diagnostics/logs/lifeCycleAnalysis/lifeCycleMisc.ts +0 -0
  84. package/src/diagnostics/logs/lifeCycleAnalysis/lifeCycleSearch.tsx +18 -7
  85. package/src/diagnostics/logs/lifeCycleAnalysis/lifeCycles.tsx +3 -0
  86. package/src/diagnostics/managementPages.tsx +10 -3
  87. package/src/diagnostics/misc-pages/ArchiveViewer.tsx +20 -26
  88. package/src/diagnostics/misc-pages/ArchiveViewerTree.tsx +6 -4
  89. package/src/diagnostics/misc-pages/ComponentSyncStats.tsx +2 -2
  90. package/src/diagnostics/misc-pages/LocalWatchViewer.tsx +7 -9
  91. package/src/diagnostics/misc-pages/SnapshotViewer.tsx +23 -12
  92. package/src/diagnostics/misc-pages/archiveViewerShared.tsx +1 -1
  93. package/src/diagnostics/pathAuditer.ts +486 -0
  94. package/src/diagnostics/pathAuditerCallback.ts +20 -0
  95. package/src/diagnostics/watchdog.ts +8 -1
  96. package/src/library-components/URLParam.ts +1 -1
  97. package/src/misc/hash.ts +1 -0
  98. package/src/path.ts +21 -7
  99. package/src/server.ts +54 -47
  100. package/src/user-implementation/loginEmail.tsx +1 -1
  101. package/tempnotes.txt +65 -0
  102. package/test.ts +298 -97
  103. package/src/0-path-value-core/NodePathAuthorities.ts +0 -1057
  104. package/src/0-path-value-core/PathController.ts +0 -1
  105. package/src/5-diagnostics/diskValueAudit.ts +0 -218
  106. package/src/5-diagnostics/memoryValueAudit.ts +0 -438
  107. package/src/archiveapps/archiveMergeEntry.tsx +0 -48
  108. package/src/archiveapps/lockTest.ts +0 -127
@@ -0,0 +1,303 @@
1
+ import { keyByArray, binarySearchIndex } from "socket-function/src/misc";
2
+ import { measureFnc } from "socket-function/src/profiling/measure";
3
+ import { isNode } from "typesafecss";
4
+ import { isDiskAudit } from "../config";
5
+ import { getPathFromStr } from "../path";
6
+ import { auditLog } from "./auditLogs";
7
+ import { PathRouter } from "./PathRouter";
8
+ import { PathValue, authorityStorage, compareTime, debugPathValuePath, ReadLock, byLockGroup, isCoreQuiet, debugRejections, debugTime, debugPathValue } from "./pathValueCore";
9
+ import { pathWatcher } from "./PathWatcher";
10
+ import { lockWatcher2 } from "./LockWatcher2";
11
+ import { onPathInteracted } from "../diagnostics/pathAuditerCallback";
12
+
13
+ class ValidStateComputer {
14
+
15
+ private capturePrevValidStates(valuePaths: PathValue[]): Map<PathValue, boolean | undefined> {
16
+ let prevValidStates = new Map<PathValue, boolean | undefined>();
17
+ for (let value of valuePaths) {
18
+ let prevValidState = authorityStorage.getValueAtTime(value.path, value.time)?.valid;
19
+ prevValidStates.set(value, prevValidState);
20
+ }
21
+ return prevValidStates;
22
+ }
23
+
24
+ @measureFnc
25
+ public ingestValuesAndValidStates(config: {
26
+ pathValues: PathValue[];
27
+ parentSyncs: { parentPath: string; sourceNodeId: string }[];
28
+ initialTriggers: { values: Set<string>; parentPaths: Set<string> };
29
+ doNotArchive?: boolean;
30
+ }) {
31
+ let { pathValues, parentSyncs, initialTriggers, doNotArchive } = config;
32
+
33
+ // TODO: We might want to add back optimizations for "no watches and no locks"?
34
+ // TODO: If we find it is a serious performance problem, we could just send the valid state instead of sending the full path value and values are rejected (this is what we used to do).
35
+
36
+ let now = Date.now();
37
+
38
+ authorityStorage.addParentSyncs(parentSyncs);
39
+
40
+ let ourValues: PathValue[] = [];
41
+ let notOurValues: PathValue[] = [];
42
+ for (let value of pathValues) {
43
+ if (!PathRouter.isSelfAuthority(value.path)) {
44
+ notOurValues.push(value);
45
+ } else {
46
+ ourValues.push(value);
47
+ }
48
+ }
49
+ authorityStorage.ingestValues(notOurValues, { doNotArchive });
50
+
51
+
52
+ // Use initial triggers, at least for all the values that we aren't the authority of, to clobber the old values, creating new shallow copies of the old values with a valid state false if we don't have the corresponding value in our new values, including for all parent paths.
53
+ {
54
+ // TODO: This code is similar to the code in trigger values change, but it's also dealing with watchers and other stuff, so I think it makes sense to have it also here.
55
+ let fullInitialPaths = new Set<string>();
56
+ for (let path of initialTriggers?.values ?? []) {
57
+ fullInitialPaths.add(path);
58
+ }
59
+
60
+ // initialTriggers.parentPaths => triggerPaths
61
+ for (let parentPath of initialTriggers.parentPaths) {
62
+ let valuePaths = authorityStorage.getPathsFromParent(parentPath);
63
+ for (let valuePath of valuePaths || []) {
64
+ fullInitialPaths.add(valuePath);
65
+ }
66
+ }
67
+
68
+ let fullInitialPathsArr = Array.from(fullInitialPaths);
69
+ fullInitialPathsArr = fullInitialPathsArr.filter(x => !PathRouter.isSelfAuthority(x));
70
+
71
+ let valueByPath = keyByArray(notOurValues, x => x.path);
72
+ for (let path of fullInitialPathsArr) {
73
+ let newValues = valueByPath.get(path) ?? [];
74
+ let prevValues = authorityStorage.getValuePlusHistory(path);
75
+
76
+ newValues.sort((a, b) => compareTime(a.time, b.time));
77
+
78
+ for (let value of prevValues) {
79
+ let newIndex = binarySearchIndex(newValues.length, i => compareTime(newValues[i].time, value.time));
80
+ if (newIndex < 0) {
81
+ let newValue = newValues.at(-1);
82
+ console.info(`Rejecting past value due to initial sync: ${debugPathValuePath(value)}`, {
83
+ path: value.path,
84
+ timeId: value.time.time,
85
+ newTimeId: newValue?.time.time,
86
+ });
87
+ ourValues.push({ ...value, valid: false, });
88
+ }
89
+ }
90
+ }
91
+ }
92
+
93
+
94
+
95
+ let initialPrevValidStates: Map<PathValue, boolean | undefined> | undefined = this.capturePrevValidStates(ourValues);
96
+ authorityStorage.ingestValues(ourValues, { doNotArchive });
97
+
98
+ // NOTE: Watch value locks, will internally watch remote locks if necessary.
99
+ lockWatcher2.watchValueLocks(ourValues, now);
100
+
101
+ // NOTE: Initial values always get put into computeValidStates, which then ingests, but does not compute values we are not the authority for.
102
+ let valuesChanged = new Set<PathValue>(pathValues);
103
+ let dependenciesChanged = new Set<PathValue>(ourValues);
104
+ let alreadyTriggered = new Set<PathValue>(dependenciesChanged);
105
+
106
+ let loopCount = 0;
107
+
108
+ while (dependenciesChanged.size > 0) {
109
+ loopCount++;
110
+ if (loopCount > 3000) {
111
+ console.error("Too many loops in valid state loop. aborting.", {
112
+ loopCount,
113
+ changedCount: dependenciesChanged.size,
114
+ totalTriggered: alreadyTriggered.size,
115
+ exampleChanged: Array.from(dependenciesChanged)[0].path,
116
+ });
117
+ break;
118
+ }
119
+ // Just because the dependencies change doesn't mean the valid state has changed. We actually have to compute to see if it has changed. Of course, if we're not the authority, we won't compute the valid site at all. It'll just ignore it, which is good. If we're not the authority, we don't want to be doing recursive computation. Because we prepopulate it, the values change with all of the input values, we'll still get a trigger. We just won't get a nested trigger.
120
+ let validStateChanged = validStateComputer.computeValidStates(
121
+ Array.from(dependenciesChanged),
122
+ now,
123
+ initialPrevValidStates,
124
+ ).changed;
125
+ initialPrevValidStates = undefined;
126
+ dependenciesChanged.clear();
127
+
128
+ for (let validStateChange of validStateChanged) {
129
+ valuesChanged.add(validStateChange);
130
+ // IMPORTANT! The consequence of this is that if someone predicts a value, sends it to us, it's not valid, we will see it's not valid, and we will tell all of the nodes that are watching that path about all of the values on that path. This is inefficient. However, rejected values should be quite rare, so it shouldn't be too inefficient.
131
+ if (!validStateChange.valid) {
132
+ // If it's not valid, we need to send the next available valid value. But we haven't done all the fullness computation yet, so the current next available valid value might be recomputed, it might be invalid. So we'll just do the initial trigger. It's not going to be that expensive as the GCing should mean that most paths have a small history, and rejections should be quite rare.
133
+ initialTriggers.values.add(validStateChange.path);
134
+ }
135
+ }
136
+ // And then for everything that had a valid state change, go check if it has a dependency that was watching it.
137
+ for (let value of validStateChanged) {
138
+ let watchers = lockWatcher2.getValuePathWatchers(value);
139
+ for (let watcher of watchers) {
140
+ if (alreadyTriggered.has(watcher)) continue;
141
+ dependenciesChanged.add(watcher);
142
+ alreadyTriggered.add(watcher);
143
+ }
144
+ }
145
+ }
146
+
147
+ pathWatcher.triggerValuesChanged({
148
+ valuesChanged,
149
+ initialTriggers
150
+ });
151
+ }
152
+
153
+ private isLockValid(lock: ReadLock): boolean {
154
+ let value = authorityStorage.getValueAtTime(lock.path, lock.endTime);
155
+ if (!value) {
156
+ // If not synced, we don't want to reject it yet
157
+ return !authorityStorage.isSynced(lock.path);
158
+ }
159
+ return !!value.valid;
160
+ }
161
+ private isLockContentionFree(lock: ReadLock): PathValue | undefined {
162
+ // sorted by -time (newest are first)
163
+ let history = authorityStorage.getValuePlusHistory(lock.path);
164
+
165
+ let index = binarySearchIndex(history.length, i => compareTime(lock.endTime, history[i].time));
166
+ if (index < 0) index = ~index;
167
+ else index++;
168
+ for (let i = index; i < history.length; i++) {
169
+ let value = history[i];
170
+ if (!value.valid) continue;
171
+ // It's fine, they have equivalent values. It was probably gced by the writer, which is fine.
172
+ if (value.isTransparent && lock.readIsTransparent) continue;
173
+ // If value.time is within our range, there's contention!
174
+ if (compareTime(lock.startTime, value.time) < 0 && compareTime(value.time, lock.endTime) < 0) {
175
+ return value;
176
+ }
177
+ }
178
+ return undefined;
179
+ }
180
+
181
+ @measureFnc
182
+ public computeValidStates(
183
+ valuePaths: PathValue[],
184
+ now: number,
185
+ prevValidStates = this.capturePrevValidStates(valuePaths)
186
+ ): { changed: PathValue[] } {
187
+ let changed: PathValue[] = [];
188
+
189
+ let lockGroups = byLockGroup(valuePaths);
190
+
191
+ for (let [locks, valueGroup] of lockGroups) {
192
+ if (valueGroup.length === 0) continue;
193
+
194
+ let valid = true;
195
+ for (let lock of locks) {
196
+ if (!this.isLockValid(lock)) {
197
+ onPathInteracted(lock.path, 2);
198
+ valid = false;
199
+ this.logLockInvalid(valueGroup, lock, now);
200
+ break;
201
+ }
202
+ let contention = this.isLockContentionFree(lock);
203
+ if (contention) {
204
+ onPathInteracted(lock.path, 2);
205
+ valid = false;
206
+ this.logLockContention(valueGroup, lock, contention, now);
207
+ break;
208
+ }
209
+ }
210
+ for (let pathValue of valueGroup) {
211
+ let prevValidState = prevValidStates.get(pathValue);
212
+ // IMPORTANT! This means if it didn't previously exist and it's presently rejected, we count that as a change, as it this is going from undefined to false. This is actually very useful, as a lot of places want to know if a write is rejected, so they can display it in the UI for the developer.
213
+ if (valid === prevValidState) continue;
214
+
215
+ changed.push(pathValue);
216
+
217
+ // NOTE: This should be the only place we set it, and the read-only flag is only for us to prevent anyone from setting it.
218
+ (pathValue as any).valid = valid;
219
+ }
220
+ }
221
+
222
+ return { changed };
223
+ }
224
+
225
+ private logLockInvalid(valueGroup: PathValue[], lock: ReadLock, now: number) {
226
+ // This is good... unless it happens A LOT. Then the app needs to change (unless it
227
+ // is a game, or something with realtime competition, in which case this is probably unavoidable).
228
+ if (valueGroup.length > 0 && lock.endTime.version !== Number.MAX_SAFE_INTEGER && lock.startTime.version !== -2) {
229
+ if ((!isCoreQuiet || !isNode()) && debugRejections) {
230
+ let timeToReject = now - valueGroup[0].time.time;
231
+ let message = `!!! (VALUE REJECTED) VALUE REJECTED DUE TO USING MISSING / REJECTED READ!!!(rejected after ${timeToReject}ms at ${Date.now()})`;
232
+ message += `\n${debugTime(valueGroup[0].time)} (write)`;
233
+ message += `\n rejected as the server could not find: `;
234
+ if (lock.readIsTransparent) {
235
+ message += `\n${debugTime(lock.startTime)} to ${debugTime(lock.endTime)} ${getPathFromStr(lock.path).join(".")} `;
236
+ } else {
237
+ message += `\n${debugTime(lock.startTime)} ${getPathFromStr(lock.path).join(".")} `;
238
+ }
239
+ if (lock.readIsTransparent) {
240
+ message += `\n (read was undefined, so presumably a value exists which the writer missed)`;
241
+ }
242
+ message += `\nFull list of writes rejected: `;
243
+ for (let pathValue of valueGroup) {
244
+ message += `\n${debugPathValue(pathValue)}`;
245
+ }
246
+ console.error(message);
247
+ }
248
+
249
+ if (isDiskAudit()) {
250
+ for (let pathValue of valueGroup) {
251
+ auditLog("VALUE REJECTED, we did not see a required value (we are missing their value, or their value was rejected)", {
252
+ path: pathValue.path,
253
+ timeId: pathValue.time.time,
254
+ missingPath: lock.path,
255
+ missingPathTimeId: lock.startTime.time,
256
+ transparent: lock.readIsTransparent,
257
+ });
258
+ }
259
+ }
260
+ }
261
+ }
262
+ private logLockContention(valueGroup: PathValue[], lock: ReadLock, conflictingValue: PathValue, now: number) {
263
+
264
+ // This special version indicates clientside prediction (which SHOULD be rejected).
265
+ if (lock.endTime.version !== Number.MAX_SAFE_INTEGER) {
266
+
267
+ let changed = valueGroup.filter(x => x.valid);
268
+ if (changed.length > 0) {
269
+ if ((!isCoreQuiet || !isNode())
270
+ || debugRejections
271
+ ) {
272
+ let timeToReject = now - changed[0].time.time;
273
+ let redMessage = `!!! (VALUE REJECTED) LOCK CONTENTION FIXED VIA REJECTION OF VALUE!!!(rejected after ${timeToReject}ms)`;
274
+ for (let pathValue of changed) {
275
+ redMessage += `\n${debugPathValue(pathValue)}`;
276
+ }
277
+ redMessage += `\n (write rejected due to original read not noticing value at: ${lock.path})`;
278
+ redMessage += `\n (original read from: ${debugTime(lock.startTime)}`;
279
+ redMessage += `\n (at time: ${debugTime(lock.endTime)}`;
280
+ redMessage += `\n (current time: ${Date.now()})`;
281
+ redMessage += `\n (conflict write at: ${debugTime(conflictingValue.time)}`;
282
+ console.error(redMessage);
283
+ }
284
+ if (isDiskAudit()) {
285
+ for (let pathValue of changed) {
286
+ auditLog("VALUE REJECTED, remote write did not see a conflicting value (remote is missing a value)", {
287
+ lock,
288
+ path: pathValue.path,
289
+ timeId: pathValue.time.time,
290
+ lockedPath: lock.path,
291
+ transparent: lock.readIsTransparent,
292
+ missingPath: lock.path,
293
+ conflictTimeId0: conflictingValue.time.time,
294
+ conflictingWrites: conflictingValue,
295
+ hadTimeId: lock.startTime.time,
296
+ });
297
+ }
298
+ }
299
+ }
300
+ }
301
+ }
302
+ }
303
+ export const validStateComputer = new ValidStateComputer();
@@ -149,6 +149,6 @@ export interface ArchiveLocker {
149
149
  * this will probably work. AND if you are reverting to an old snapshot, something that will
150
150
  * probably work is better than a data state which is definitely broken.
151
151
  */
152
- unsafeSetFiles(files: string[]): Promise<void>;
152
+ unsafeSetFiles(files: string[], config?: { hackNoDelete?: boolean }): Promise<void>;
153
153
  unsafeGetFileLocation(file: string): Promise<"live" | "zombie" | "recycled" | "missing">;
154
154
  }
@@ -15,6 +15,12 @@ import { getNodeId } from "socket-function/src/nodeCache";
15
15
  import { logNodeStateStats, logNodeStats } from "../../-0-hooks/hooks";
16
16
  import { parsePath, toFileNameKVP, parseFileNameKVP } from "../../misc";
17
17
  import { logDisk } from "../../diagnostics/logs/diskLogger";
18
+ import { retryFunctional, runInParallel } from "socket-function/src/batching";
19
+
20
+ /*
21
+ Data starts a .data files, from servers creating files.
22
+ Then we create .locked files, and .transactions, And when we apply these transactions, it will create .confirm files which make the .locked files count.
23
+ */
18
24
 
19
25
  /** Clean up old files after a while */
20
26
  const DEAD_CREATE_THRESHOLD = timeInHour * 12;
@@ -90,60 +96,83 @@ export function createArchiveLocker2(config: {
90
96
  }
91
97
  },
92
98
  };
93
- async function unsafeSetFiles(files: string[]): Promise<void> {
99
+ async function unsafeSetFiles(files: string[], config?: { hackNoDelete?: boolean }): Promise<void> {
94
100
  let valuePaths = new Set((await archiveValues.findInfo("")).map(x => x.path));
95
101
  let correctConfirms = new Set<string>();
96
- let correctFiles = new Set<string>();
102
+ let correctFiles = new Set<string>(files);
97
103
 
98
104
  async function deleteAllOtherFiles() {
105
+ if (config?.hackNoDelete) return;
99
106
  // Delete all old confirms / transactions / etc
100
107
  let allConfirms = (await archiveLocks.findInfo("")).map(x => x.path);
101
- await Promise.all(allConfirms.map(async confirm => {
108
+ console.log(`Found ${allConfirms.length} confirms to delete`);
109
+ let deleteCount = 0;
110
+ async function deleteConfirm(confirm: string) {
111
+ deleteCount++;
102
112
  if (correctConfirms.has(confirm)) return;
103
113
  await storage.deleteKey(confirm);
104
- }));
114
+ console.log(`Deleted confirm ${confirm}, ${deleteCount}/${allConfirms.length}`);
115
+ }
116
+ let deleteConfirmParallel = runInParallel({ parallelCount: 8 }, deleteConfirm);
117
+ await Promise.all(allConfirms.map(deleteConfirmParallel));
118
+ console.log(`Deleted ${allConfirms.length} confirms`);
105
119
 
106
120
  valuePaths = new Set((await archiveValues.findInfo("")).map(x => x.path));
121
+ console.log(`Found ${valuePaths.size} files to delete`);
107
122
  // Delete all files we didn't just set (move to recycle bin)
108
- await Promise.all(Array.from(valuePaths).map(async file => {
123
+ let moveCount = 0;
124
+ async function moveFile(file: string) {
125
+ moveCount++;
109
126
  if (correctFiles.has(file)) return;
127
+ let info = await archiveValues.getInfo(file);
128
+ console.log(`Moving file ${file}, ${moveCount}/${valuePaths.size}, ${formatNumber(info?.size || 0)} bytes`);
110
129
  await archiveValues.move({
111
130
  path: file,
112
131
  targetPath: file,
113
132
  target: archiveRecycleBin,
114
133
  });
115
- }));
134
+ console.log(`Moved file ${file}, ${moveCount}/${valuePaths.size}`);
135
+ }
136
+ let moveFileParallel = runInParallel({ parallelCount: 8 }, moveFile);
137
+ await Promise.all(Array.from(valuePaths).map(moveFileParallel));
138
+ console.log(`Deleted ${valuePaths.size} files`);
116
139
  }
117
140
 
118
- console.log(magenta(`Deleting all other files`));
141
+ console.log(magenta(`Deleting all other files (this may take a while)`));
119
142
  // Deleting early deletes any transactions, which might be reapplied due to our non-transaction changes
120
143
  await deleteAllOtherFiles();
121
144
 
122
- // Undelete, by retrieving all files from the recycle bin
123
- let promises: Promise<void>[] = [];
124
- for (let file of files) {
125
- if (valuePaths.has(file)) {
126
- correctFiles.add(file);
145
+ let restoreCount = 0;
146
+ async function restoreFile(file: string): Promise<void> {
147
+ if (await archiveValues.get(file)) {
148
+ console.log(`File ${file} is already in the archive values, skipping, ${restoreCount++}/${files.length}`);
149
+ return;
127
150
  }
128
151
  let isInRecycleBin = !!(await archiveRecycleBin.getInfo(file));
129
152
  if (isInRecycleBin) {
130
- console.log(`Restoring ${file}`);
131
- promises.push(archiveRecycleBin.move({
153
+ console.log(`Restoring ${file}, ${restoreCount++}/${files.length}`);
154
+ await archiveRecycleBin.move({
132
155
  path: file,
133
156
  targetPath: file,
134
157
  target: archiveValues,
135
- }));
136
- correctFiles.add(file);
158
+ });
159
+ } else {
160
+ console.log(`File ${file} is not in the recycle bin, skipping, ${restoreCount++}/${files.length}`);
137
161
  }
138
162
  }
139
- await Promise.all(promises);
163
+ const restoreFileParallel = runInParallel({ parallelCount: 16 }, restoreFile);
164
+ await Promise.all(files.map(restoreFileParallel));
140
165
 
141
166
  console.log(magenta(`Creating confirms`));
167
+ let createCount = 0;
142
168
  // Confirm all files
143
- await Promise.all(Array.from(correctFiles).map(async file => {
144
- let confirm = await locker.createConfirm(file);
169
+ async function createConfirm(file: string): Promise<void> {
170
+ let confirm = await locker.createConfirm(file, correctFiles.size - createCount);
145
171
  correctConfirms.add(confirm);
146
- }));
172
+ console.log(`Created confirm ${confirm}, ${createCount++}/${correctFiles.size}`);
173
+ }
174
+ let createConfirmParallel = runInParallel({ parallelCount: 4 }, retryFunctional(createConfirm));
175
+ await Promise.all(Array.from(correctFiles).map(createConfirmParallel));
147
176
 
148
177
  console.log(magenta(`Deleting all other files again`));
149
178
  // Delete again, in case anything else was created while we were restoring. Not great, but... should
@@ -184,6 +213,7 @@ export function createArchiveLocker2(config: {
184
213
  ) => Promise<ArchiveTransaction[]>
185
214
  ): Promise<"accepted" | "rejected"> {
186
215
  let files = await locker.getFiles();
216
+ await saveSnapshot({ files: files.map(a => a.file) });
187
217
  let readFiles = async (files: FileInfo[]) => {
188
218
  let pendingFiles = files.slice();
189
219
  let readResults = new Map<string, Buffer | undefined>();
@@ -231,7 +261,6 @@ export function createArchiveLocker2(config: {
231
261
  let unhandled: never = type;
232
262
  }
233
263
  }
234
- logErrors(saveSnapshot({ files: Array.from(newFiles) }));
235
264
  }
236
265
  return status;
237
266
  }
@@ -259,7 +288,7 @@ type StorageType = {
259
288
  * all writes should now be readable.
260
289
  */
261
290
  };
262
- type Transaction = {
291
+ export type Transaction = {
263
292
  ops: ({
264
293
  type: "create";
265
294
  key: string;
@@ -307,15 +336,16 @@ class TransactionLocker {
307
336
  let { dir, name } = parsePath(key);
308
337
  return `${dir}confirm_${name}.confirm`;
309
338
  }
310
- public async createConfirm(key: string) {
339
+ public async createConfirm(key: string, countLeft: number) {
311
340
  let path = this.getConfirmKey(key);
312
- console.info("Creating confirmation for ${key}");
313
- await this.storage.setValue(path, Buffer.from(""));
341
+ console.info(`Creating confirmation for ${key}, ${countLeft} left`);
342
+ if (!await this.storage.getValue(path)) {
343
+ await this.storage.setValue(path, Buffer.from(""));
344
+ }
314
345
  return path;
315
346
  }
316
- private async deleteDataFile(key: string, reason: string): Promise<void> {
317
- console.log(red(`Deleting data file ${key}, because ${reason}`));
318
- //await this.storage.setValue(key + ".reason", Buffer.from(reason));
347
+ private async deleteDataFile(key: string, reason: string, countLeft: number): Promise<void> {
348
+ console.log(red(`Deleting data file ${key}, because ${reason}, ${countLeft} left`));
319
349
  // Delete file, and confirmation as well
320
350
  await this.storage.deleteKey(key);
321
351
  await this.storage.deleteKey(this.getConfirmKey(key));
@@ -472,13 +502,22 @@ class TransactionLocker {
472
502
 
473
503
  let existingFiles = new Map(files.map(a => [a.file, a]));
474
504
 
505
+ let uncomfirmedCount = 0;
506
+
475
507
  let currentDataFiles = new Map<string, FileInfo>();
508
+ let countByExtension = new Map<string, { value: number }>();
476
509
  for (let file of files) {
510
+ let extension = file.file.split(".").pop() || "";
511
+ let existing = countByExtension.get(extension);
512
+ if (!existing) {
513
+ existing = { value: 0 };
514
+ countByExtension.set(extension, existing);
515
+ }
516
+ existing.value++;
477
517
  if (!(
478
518
  file.file.endsWith(".locked")
479
519
  || file.file.endsWith(".confirm")
480
520
  || file.file.endsWith(".transaction")
481
- || file.file.endsWith(".reason")
482
521
  )) {
483
522
  currentDataFiles.set(file.file, file);
484
523
  continue;
@@ -488,14 +527,17 @@ class TransactionLocker {
488
527
  let confirmFile = existingFiles.get(confirmKey);
489
528
  if (confirmFile) {
490
529
  currentDataFiles.set(file.file, file);
530
+ } else {
531
+ uncomfirmedCount++;
491
532
  }
492
533
  }
493
-
494
- console.info("Read archive state", {
534
+ console.log("Read archive state", {
495
535
  rawFilesCount: files.length,
496
536
  confirmedCount: currentDataFiles.size,
497
- rawFiles: files.map(a => a.file),
498
- confirmedFiles: Array.from(currentDataFiles.values()).map(a => a.file),
537
+ uncomfirmedCount,
538
+ countByExtention: Array.from(countByExtension.entries()).map(([extension, value]) => `${extension}: ${value.value}`).join(" | "),
539
+ //rawFiles: files.map(a => a.file),
540
+ //confirmedFiles: Array.from(currentDataFiles.values()).map(a => a.file),
499
541
  });
500
542
 
501
543
  return {
@@ -586,9 +628,9 @@ class TransactionLocker {
586
628
  let op = opsRemaining.pop();
587
629
  if (!op) return;
588
630
  if (op.type === "create") {
589
- await this.createConfirm(op.key);
631
+ await this.createConfirm(op.key, opsRemaining.length);
590
632
  } else if (op.type === "delete") {
591
- await this.deleteDataFile(op.key, `transaction (${getOwnNodeId()})`);
633
+ await this.deleteDataFile(op.key, `transaction (${getOwnNodeId()})`, opsRemaining.length);
592
634
  } else {
593
635
  let unhandled: never = op;
594
636
  throw new Error(`Unhandled type: ${unhandled}`);
@@ -706,8 +748,9 @@ class TransactionLocker {
706
748
  console.warn(red(`Deleted ${unconfirmedOldFiles2.length} very old unconfirmed files`), { files: unconfirmedOldFiles2.map(x => x.file) });
707
749
  logNodeStats(`archives|TΔ Delete Old Rejected File`, formatNumber, unconfirmedOldFiles2.length);
708
750
  // At the point the file was very old when we started reading, not part of the active transaction.
709
- for (let file of unconfirmedOldFiles2) {
710
- await this.deleteDataFile(file.file, `old unconfirmed file`);
751
+ for (let i = 0; i < unconfirmedOldFiles2.length; i++) {
752
+ let file = unconfirmedOldFiles2[i];
753
+ await this.deleteDataFile(file.file, `old unconfirmed file`, unconfirmedOldFiles2.length - i);
711
754
  }
712
755
  } else {
713
756
  console.warn(`Almost deleted ${unconfirmedOldFiles.length} very old unconfirmed files. This is bad, did we miss their confirmations that first time? If we missed them twice in a row, we might literally delete the database, and need to enter recovery mode to fix it...`, { files: unconfirmedOldFiles });
@@ -740,13 +783,6 @@ class TransactionLocker {
740
783
  }
741
784
  }
742
785
 
743
- // Delete some debug files
744
- {
745
- for (let file of dataState.rawDataFiles.filter(x => x.file.endsWith(".reason"))) {
746
- await this.storage.deleteKey(file.file);
747
- }
748
- }
749
-
750
786
  return {
751
787
  dataFiles: Array.from(dataState.confirmedDataFiles),
752
788
  rawDataFiles: dataState.rawDataFiles,
@@ -4,7 +4,7 @@ import { getAllNodeIds, getOwnThreadId } from "../../-f-node-discovery/NodeDisco
4
4
  import { pathValueArchives } from "../pathValueArchives";
5
5
  import { ignoreErrors, logErrors, timeoutToUndefinedSilent } from "../../errors";
6
6
  import { green, magenta } from "socket-function/src/formatting/logColors";
7
- import { devDebugbreak } from "../../config";
7
+ import { devDebugbreak, isPublic } from "../../config";
8
8
  import { sort } from "socket-function/src/misc";
9
9
  import { lazy } from "socket-function/src/caching";
10
10
 
@@ -56,6 +56,7 @@ export async function saveSnapshot(config: {
56
56
  let { files } = config;
57
57
  let overview = getSnapshotOverview(files);
58
58
  await snapshots().set(overview.file, Buffer.from(files.join("\n")));
59
+ return overview;
59
60
  }
60
61
  function overviewToFileName(overview: Omit<ArchiveSnapshotOverview, "file">): string {
61
62
  return Object.entries({
@@ -152,10 +153,11 @@ async function getSnapshotBase(snapshotFile: string | "live") {
152
153
  // although you will still have to take most of the servers down to get back
153
154
  // to a consistent server state.
154
155
  export async function loadSnapshot(config: {
155
- overview: ArchiveSnapshotOverview;
156
- audit?: (nodeId: string) => Promise<void>;
156
+ overview: { file: string; };
157
+ noExit?: boolean;
158
+ hackNoDelete?: boolean;
157
159
  }): Promise<void> {
158
- let { overview, audit } = config;
160
+ let { overview } = config;
159
161
  console.log(magenta(`Loading snapshot: ${overview.file}`));
160
162
  let locker = await pathValueArchives.getArchiveLocker();
161
163
 
@@ -166,20 +168,15 @@ export async function loadSnapshot(config: {
166
168
  let files = snapshotData.toString().split("\n");
167
169
 
168
170
  console.log(magenta(`Unsafe setting snapshot files: ${overview.file}`));
169
- await locker.unsafeSetFiles(files);
171
+ await locker.unsafeSetFiles(files, { hackNoDelete: config.hackNoDelete });
170
172
  console.log(magenta(`Finished unsafe setting snapshot files: ${overview.file}`));
171
173
 
172
- let allNodes = await getAllNodeIds();
173
-
174
- // Logging the errors, not ignoring them
175
- if (audit) {
176
- await timeoutToUndefinedSilent(10_000, Promise.all(allNodes.map(async nodeId => {
177
- if (!audit) return;
178
- await audit(nodeId);
179
- })));
180
- }
181
-
182
174
  for (let i = 0; i < 10; i++) {
183
175
  console.log(green(`!!!Finished loading snapshot!!!`));
184
176
  }
185
- }
177
+
178
+ if (!config.noExit && !isPublic()) {
179
+ // Exit so we know when this is done. Really, all the servers need to be restarted after we load the snapshot, anyway.
180
+ process.exit();
181
+ }
182
+ }
@@ -47,7 +47,9 @@ export function getLogHistoryEquals(value: string) {
47
47
  (globalThis as any).getLogHistoryEquals = getLogHistoryEquals;
48
48
  (globalThis as any).getPathAuditLogs = getLogHistoryEquals;
49
49
 
50
+ // NOTE: It's good practice to check isDebugLogEnabled before calling this, as it's expensive to create an object if we're just going to throw it away.
50
51
  export function auditLog(type: string, values: { [key: string]: unknown }) {
52
+ if (!ENABLED_LOGGING) return;
51
53
  debugLogFnc(type, values);
52
54
  }
53
55
  let debugLogFnc = debugLogBase;