querysub 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (169) hide show
  1. package/.dependency-cruiser.js +304 -0
  2. package/.eslintrc.js +51 -0
  3. package/.github/copilot-instructions.md +1 -0
  4. package/.vscode/settings.json +25 -0
  5. package/bin/deploy.js +4 -0
  6. package/bin/function.js +4 -0
  7. package/bin/server.js +4 -0
  8. package/costsBenefits.txt +112 -0
  9. package/deploy.ts +3 -0
  10. package/inject.ts +1 -0
  11. package/package.json +60 -0
  12. package/prompts.txt +54 -0
  13. package/spec.txt +820 -0
  14. package/src/-a-archives/archiveCache.ts +913 -0
  15. package/src/-a-archives/archives.ts +148 -0
  16. package/src/-a-archives/archivesBackBlaze.ts +792 -0
  17. package/src/-a-archives/archivesDisk.ts +418 -0
  18. package/src/-a-archives/copyLocalToBackblaze.ts +24 -0
  19. package/src/-a-auth/certs.ts +517 -0
  20. package/src/-a-auth/der.ts +122 -0
  21. package/src/-a-auth/ed25519.ts +1015 -0
  22. package/src/-a-auth/node-forge-ed25519.d.ts +17 -0
  23. package/src/-b-authorities/dnsAuthority.ts +203 -0
  24. package/src/-b-authorities/emailAuthority.ts +57 -0
  25. package/src/-c-identity/IdentityController.ts +200 -0
  26. package/src/-d-trust/NetworkTrust2.ts +150 -0
  27. package/src/-e-certs/EdgeCertController.ts +288 -0
  28. package/src/-e-certs/certAuthority.ts +192 -0
  29. package/src/-f-node-discovery/NodeDiscovery.ts +543 -0
  30. package/src/-g-core-values/NodeCapabilities.ts +134 -0
  31. package/src/-g-core-values/oneTimeForward.ts +91 -0
  32. package/src/-h-path-value-serialize/PathValueSerializer.ts +769 -0
  33. package/src/-h-path-value-serialize/stringSerializer.ts +176 -0
  34. package/src/0-path-value-core/LoggingClient.tsx +24 -0
  35. package/src/0-path-value-core/NodePathAuthorities.ts +978 -0
  36. package/src/0-path-value-core/PathController.ts +1 -0
  37. package/src/0-path-value-core/PathValueCommitter.ts +565 -0
  38. package/src/0-path-value-core/PathValueController.ts +231 -0
  39. package/src/0-path-value-core/archiveLocks/ArchiveLocks.ts +154 -0
  40. package/src/0-path-value-core/archiveLocks/ArchiveLocks2.ts +820 -0
  41. package/src/0-path-value-core/archiveLocks/archiveSnapshots.ts +180 -0
  42. package/src/0-path-value-core/debugLogs.ts +90 -0
  43. package/src/0-path-value-core/pathValueArchives.ts +483 -0
  44. package/src/0-path-value-core/pathValueCore.ts +2217 -0
  45. package/src/1-path-client/RemoteWatcher.ts +558 -0
  46. package/src/1-path-client/pathValueClientWatcher.ts +702 -0
  47. package/src/2-proxy/PathValueProxyWatcher.ts +1857 -0
  48. package/src/2-proxy/archiveMoveHarness.ts +376 -0
  49. package/src/2-proxy/garbageCollection.ts +753 -0
  50. package/src/2-proxy/pathDatabaseProxyBase.ts +37 -0
  51. package/src/2-proxy/pathValueProxy.ts +139 -0
  52. package/src/2-proxy/schema2.ts +518 -0
  53. package/src/3-path-functions/PathFunctionHelpers.ts +129 -0
  54. package/src/3-path-functions/PathFunctionRunner.ts +619 -0
  55. package/src/3-path-functions/PathFunctionRunnerMain.ts +67 -0
  56. package/src/3-path-functions/deployBlock.ts +10 -0
  57. package/src/3-path-functions/deployCheck.ts +7 -0
  58. package/src/3-path-functions/deployMain.ts +160 -0
  59. package/src/3-path-functions/pathFunctionLoader.ts +282 -0
  60. package/src/3-path-functions/syncSchema.ts +475 -0
  61. package/src/3-path-functions/tests/functionsTest.ts +135 -0
  62. package/src/3-path-functions/tests/rejectTest.ts +77 -0
  63. package/src/4-dom/css.tsx +29 -0
  64. package/src/4-dom/cssTypes.d.ts +212 -0
  65. package/src/4-dom/qreact.tsx +2322 -0
  66. package/src/4-dom/qreactTest.tsx +417 -0
  67. package/src/4-querysub/Querysub.ts +877 -0
  68. package/src/4-querysub/QuerysubController.ts +620 -0
  69. package/src/4-querysub/copyEvent.ts +0 -0
  70. package/src/4-querysub/permissions.ts +289 -0
  71. package/src/4-querysub/permissionsShared.ts +1 -0
  72. package/src/4-querysub/querysubPrediction.ts +525 -0
  73. package/src/5-diagnostics/FullscreenModal.tsx +67 -0
  74. package/src/5-diagnostics/GenericFormat.tsx +165 -0
  75. package/src/5-diagnostics/Modal.tsx +79 -0
  76. package/src/5-diagnostics/Table.tsx +183 -0
  77. package/src/5-diagnostics/TimeGrouper.tsx +114 -0
  78. package/src/5-diagnostics/diskValueAudit.ts +216 -0
  79. package/src/5-diagnostics/memoryValueAudit.ts +442 -0
  80. package/src/5-diagnostics/nodeMetadata.ts +135 -0
  81. package/src/5-diagnostics/qreactDebug.tsx +309 -0
  82. package/src/5-diagnostics/shared.ts +26 -0
  83. package/src/5-diagnostics/synchronousLagTracking.ts +47 -0
  84. package/src/TestController.ts +35 -0
  85. package/src/allowclient.flag +0 -0
  86. package/src/bits.ts +86 -0
  87. package/src/buffers.ts +69 -0
  88. package/src/config.ts +53 -0
  89. package/src/config2.ts +48 -0
  90. package/src/diagnostics/ActionsHistory.ts +56 -0
  91. package/src/diagnostics/NodeViewer.tsx +503 -0
  92. package/src/diagnostics/SizeLimiter.ts +62 -0
  93. package/src/diagnostics/TimeDebug.tsx +18 -0
  94. package/src/diagnostics/benchmark.ts +139 -0
  95. package/src/diagnostics/errorLogs/ErrorLogController.ts +515 -0
  96. package/src/diagnostics/errorLogs/ErrorLogCore.ts +274 -0
  97. package/src/diagnostics/errorLogs/LogClassifiers.tsx +302 -0
  98. package/src/diagnostics/errorLogs/LogFilterUI.tsx +84 -0
  99. package/src/diagnostics/errorLogs/LogNotify.tsx +101 -0
  100. package/src/diagnostics/errorLogs/LogTimeSelector.tsx +724 -0
  101. package/src/diagnostics/errorLogs/LogViewer.tsx +757 -0
  102. package/src/diagnostics/errorLogs/hookErrors.ts +60 -0
  103. package/src/diagnostics/errorLogs/logFiltering.tsx +149 -0
  104. package/src/diagnostics/heapTag.ts +13 -0
  105. package/src/diagnostics/listenOnDebugger.ts +77 -0
  106. package/src/diagnostics/logs/DiskLoggerPage.tsx +572 -0
  107. package/src/diagnostics/logs/ObjectDisplay.tsx +165 -0
  108. package/src/diagnostics/logs/ansiFormat.ts +108 -0
  109. package/src/diagnostics/logs/diskLogGlobalContext.ts +38 -0
  110. package/src/diagnostics/logs/diskLogger.ts +305 -0
  111. package/src/diagnostics/logs/diskShimConsoleLogs.ts +32 -0
  112. package/src/diagnostics/logs/injectFileLocationToConsole.ts +50 -0
  113. package/src/diagnostics/logs/logGitHashes.ts +30 -0
  114. package/src/diagnostics/managementPages.tsx +289 -0
  115. package/src/diagnostics/periodic.ts +89 -0
  116. package/src/diagnostics/runSaturationTest.ts +416 -0
  117. package/src/diagnostics/satSchema.ts +64 -0
  118. package/src/diagnostics/trackResources.ts +82 -0
  119. package/src/diagnostics/watchdog.ts +55 -0
  120. package/src/errors.ts +132 -0
  121. package/src/forceProduction.ts +3 -0
  122. package/src/fs.ts +72 -0
  123. package/src/heapDumps.ts +666 -0
  124. package/src/https.ts +2 -0
  125. package/src/inject.ts +1 -0
  126. package/src/library-components/ATag.tsx +84 -0
  127. package/src/library-components/Button.tsx +344 -0
  128. package/src/library-components/ButtonSelector.tsx +64 -0
  129. package/src/library-components/DropdownCustom.tsx +151 -0
  130. package/src/library-components/DropdownSelector.tsx +32 -0
  131. package/src/library-components/Input.tsx +334 -0
  132. package/src/library-components/InputLabel.tsx +198 -0
  133. package/src/library-components/InputPicker.tsx +125 -0
  134. package/src/library-components/LazyComponent.tsx +62 -0
  135. package/src/library-components/MeasureHeightCSS.tsx +48 -0
  136. package/src/library-components/MeasuredDiv.tsx +47 -0
  137. package/src/library-components/ShowMore.tsx +51 -0
  138. package/src/library-components/SyncedController.ts +171 -0
  139. package/src/library-components/TimeRangeSelector.tsx +407 -0
  140. package/src/library-components/URLParam.ts +263 -0
  141. package/src/library-components/colors.tsx +14 -0
  142. package/src/library-components/drag.ts +114 -0
  143. package/src/library-components/icons.tsx +692 -0
  144. package/src/library-components/niceStringify.ts +50 -0
  145. package/src/library-components/renderToString.ts +52 -0
  146. package/src/misc/PromiseRace.ts +101 -0
  147. package/src/misc/color.ts +30 -0
  148. package/src/misc/getParentProcessId.cs +53 -0
  149. package/src/misc/getParentProcessId.ts +53 -0
  150. package/src/misc/hash.ts +83 -0
  151. package/src/misc/ipPong.js +13 -0
  152. package/src/misc/networking.ts +2 -0
  153. package/src/misc/random.ts +45 -0
  154. package/src/misc.ts +19 -0
  155. package/src/noserverhotreload.flag +0 -0
  156. package/src/path.ts +226 -0
  157. package/src/persistentLocalStore.ts +37 -0
  158. package/src/promise.ts +15 -0
  159. package/src/server.ts +73 -0
  160. package/src/src.d.ts +1 -0
  161. package/src/test/heapProcess.ts +36 -0
  162. package/src/test/mongoSatTest.tsx +55 -0
  163. package/src/test/satTest.ts +193 -0
  164. package/src/test/test.tsx +552 -0
  165. package/src/zip.ts +92 -0
  166. package/src/zipThreaded.ts +106 -0
  167. package/src/zipThreadedWorker.js +19 -0
  168. package/tsconfig.json +27 -0
  169. package/yarnSpec.txt +56 -0
@@ -0,0 +1,620 @@
1
+ import { SocketFunction } from "socket-function/SocketFunction";
2
+ import { cache, lazy } from "socket-function/src/caching";
3
+ import { appendToPathStr, getPathDepth, getPathFromStr, getPathIndex, getPathStr1, getPathStr2, getPathStr3, hack_getPackedPathSuffix, removePathLastPart } from "../path";
4
+ import { CHILD_CHECK_PREFIX, FunctionMetadata } from "../3-path-functions/syncSchema";
5
+ import { RemoteWatcher, remoteWatcher } from "../1-path-client/RemoteWatcher";
6
+ import { getProxyPath } from "../2-proxy/pathValueProxy";
7
+ import { atomic, atomicObjectWrite, proxyWatcher } from "../2-proxy/PathValueProxyWatcher";
8
+ import { CallSpec, DEPTH_TO_DATA, FunctionSpec, debugCallSpec, functionSchema } from "../3-path-functions/PathFunctionRunner";
9
+ import { PathValueController } from "../0-path-value-core/PathValueController";
10
+ import { epochTime, MAX_ACCEPTED_CHANGE_AGE, PathValue, pathWatcher, Time, WatchConfig, compareTime, debugTime, getNextTime, MAX_CHANGE_AGE } from "../0-path-value-core/pathValueCore";
11
+ import { IdentityController_getMachineId, IdentityController_getPubKeyShort, IdentityController_getSecureIP } from "../-c-identity/IdentityController";
12
+ import { getControllerNodeId } from "../-g-core-values/NodeCapabilities";
13
+ import { getModuleFromConfig, watchModuleHotreloads } from "../3-path-functions/pathFunctionLoader";
14
+ import { errorToUndefined, ignoreErrors, logErrors } from "../errors";
15
+ import { blue, green, magenta, red } from "socket-function/src/formatting/logColors";
16
+ import { PermissionsCheck } from "./permissions";
17
+ import { parseArgs, writeCall } from "../3-path-functions/PathFunctionHelpers";
18
+ import { delay } from "socket-function/src/batching";
19
+ import { isNode, timeInMinute, timeInSecond } from "socket-function/src/misc";
20
+ import { registerResource } from "../diagnostics/trackResources";
21
+
22
+ // Always whitelist preact, as most of the time we want it clientside
23
+ import "preact";
24
+ import { setFlag } from "socket-function/require/compileFlags";
25
+ import { CallerContextBase } from "socket-function/SocketFunctionTypes";
26
+ import { isTrustedByNode } from "../-d-trust/NetworkTrust2";
27
+ import { pathValueSerializer } from "../-h-path-value-serialize/PathValueSerializer";
28
+ import { Querysub, id } from "./Querysub";
29
+ import { measureBlock } from "socket-function/src/profiling/measure";
30
+ import debugbreak from "debugbreak";
31
+ import { isDefined } from "../misc";
32
+ import { isClient, isServer } from "../config2";
33
+ import { PromiseObj } from "../promise";
34
+ import { LoggingClient } from "../0-path-value-core/LoggingClient";
35
+ import * as prediction from "./querysubPrediction";
36
+ import { getCallResultPath } from "./querysubPrediction";
37
+ import { pathValueAuthority2 } from "../0-path-value-core/NodePathAuthorities";
38
+ import { diskLog } from "../diagnostics/logs/diskLogger";
39
+ setFlag(require, "preact", "allowclient", true);
40
+
41
+ export { Querysub, id };
42
+
43
+
44
+ // NOTE: Eventually this will be split by domain, because the underlying getAllNodeIds
45
+ // will be split by domain.
46
+ // NOTE: We don't need to shard querysub nodes within a domain, because... they don't
47
+ // store data, and don't run functions, and only really check permissions (which should be
48
+ // very lightweight, hopefully...)
49
+ // - I guess we track our watches, but even that... should be fairly light, and... there
50
+ // is little benefit to sharding that?
51
+ export const querysubNodeId = lazy(() => getControllerNodeId(QuerysubController));
52
+
53
+ setImmediate(() => {
54
+ async function querysubWatchLatest(config: WatchConfig) {
55
+ let nodeId = await querysubNodeId();
56
+ if (!nodeId) throw new Error("No querysub node found");
57
+ await QuerysubController.nodes[nodeId].watch(config);
58
+ }
59
+ async function querysubUnwatchLatest(config: WatchConfig) {
60
+ let nodeId = await querysubNodeId();
61
+ if (!nodeId) throw new Error("No querysub node found");
62
+ await QuerysubController.nodes[nodeId].unwatch(config);
63
+ }
64
+
65
+ let baseRemoteWatchFunction = RemoteWatcher.REMOTE_WATCH_FUNCTION;
66
+ RemoteWatcher.REMOTE_WATCH_FUNCTION = async (config, authorityId) => {
67
+ let weTrusted = isServer() && await errorToUndefined(isTrustedByNode(authorityId));
68
+ if (!weTrusted) {
69
+ await querysubWatchLatest(config);
70
+ } else {
71
+ await baseRemoteWatchFunction(config, authorityId);
72
+ }
73
+ };
74
+
75
+ let baseRemoteUnwatchFunction = RemoteWatcher.REMOTE_UNWATCH_FUNCTION;
76
+ RemoteWatcher.REMOTE_UNWATCH_FUNCTION = async (config, authorityIdBase) => {
77
+ let weTrusted = isServer() && await isTrustedByNode(authorityIdBase);
78
+ if (!weTrusted) {
79
+ logErrors(querysubUnwatchLatest(config));
80
+ } else {
81
+ logErrors(baseRemoteUnwatchFunction(config, authorityIdBase));
82
+ }
83
+ };
84
+ });
85
+
86
+
87
+
88
+ function permissionsHash(caller: Readonly<CallerContextBase>) {
89
+ let machineId = IdentityController_getMachineId(caller);
90
+ let ip = IdentityController_getSecureIP(caller);
91
+ // NOTE: We also need the nodeId, as while the permissions are the same per machineId + ip, each
92
+ // process needs to be sent it's own values!
93
+ return getPathStr3(machineId, ip, caller.nodeId);
94
+ }
95
+ interface PermissionsObj {
96
+ nodes: Set<string>;
97
+
98
+ newPaths: Set<string>;
99
+ newParentPaths: Set<string>;
100
+
101
+ // permissionsPath => Set<path>
102
+ permissionsPathGroups: Map<string, {
103
+ allowed: boolean;
104
+ outdated: boolean;
105
+ originalPaths: Set<string>;
106
+ // testPath => parentPath
107
+ originalParentPaths: Map<string, string>;
108
+ }>;
109
+ pathToPermissionsPath: Map<string, string>;
110
+
111
+
112
+ checkNow: () => void;
113
+ dispose: () => void;
114
+ }
115
+ let permissionsPerNode = new Map<string, PermissionsObj>();
116
+ registerResource("querysub|permissionsPerNode", permissionsPerNode);
117
+
118
+ const lastCallTime = cache((nodeId: string): { value: Time } => {
119
+ SocketFunction.onNextDisconnect(nodeId, () => {
120
+ setTimeout(() => {
121
+ if (SocketFunction.isNodeConnected(nodeId)) return;
122
+ lastCallTime.clear(nodeId);
123
+ }, 1000 * 60);
124
+ });
125
+ return { value: epochTime };
126
+ });
127
+
128
+ export const isTrustedByDomain = cache(async function isTrustedByDomain(domain: string): Promise<boolean> {
129
+ // NOTE: When getControllerNodeId accepts a domain we will pass them the domain we are using.
130
+
131
+ // If we are trusted by one, we should be trusted by call!
132
+ // TODO: We should probably handle trust revocation? Or... not? Because that would only happen
133
+ // if we take a server offline, in which case... it would be off anyways.
134
+ let pathValueNodeId = await getControllerNodeId(PathValueController);
135
+ if (!pathValueNodeId) {
136
+ return false;
137
+ //throw new Error(`No PathValueController found`);
138
+ }
139
+ return isTrustedByNode(pathValueNodeId);
140
+ });
141
+
142
+ // NOTE: The last promise (added) ends up being the call we wait on, so that predictions
143
+ // can extend the wait time as they do more work.
144
+ let pendingPredictedCalls = new Map<string, {
145
+ obj: PromiseObj;
146
+ seqNum: number;
147
+ }>();
148
+ export function callWaitOn(callId: string, promise: Promise<unknown>) {
149
+ let waitObj = pendingPredictedCalls.get(callId);
150
+ if (!waitObj) return;
151
+ let seqNum = ++waitObj.seqNum;
152
+ void promise.finally(() => {
153
+ if (waitObj?.seqNum === seqNum) {
154
+ waitObj.obj.resolve();
155
+ }
156
+ });
157
+ }
158
+ let baseWriteCall = writeCall.value;
159
+ writeCall.value = async (callSpec: CallSpec, metadata: FunctionMetadata) => {
160
+ pendingPredictedCalls.set(callSpec.CallId, {
161
+ obj: new PromiseObj(),
162
+ seqNum: 0,
163
+ });
164
+ let promise = (async () => {
165
+ // IMPORTANT! While it may seem like we can use addCall even if we are trusted, so we can
166
+ // have function calling prediction, we can't. If did then we would depend on these
167
+ // predictions with our writes, which could end up on other nodes, which would then
168
+ // be rejected (as our predictions use a Time.version that is only on our
169
+ // node). The in unavoidable, as it is too difficult to have predictions, but also not have
170
+ // predictions, on the same node.
171
+ // - AND, we can't just use the same Time.version as the FunctionRunner will use, as our
172
+ // ValuePaths might differ, which would sometimes work, BUT, other times our ReadLocks
173
+ // would be valid, BUT, the actual read value would be different (as we might compute
174
+ // a different value than FunctionRunner). So... we would lose locking, at which point,
175
+ // why even use FunctionRunner in the first place! Just write the writes directly!
176
+ // TODO: Add the ability to use direct writes BUT if we have a rejection, commit it as a regular function call
177
+ // - This will be a lot more efficient, although, if the committed dies and the function is rejected, the
178
+ // value will be lost, which isn't great. However... trusted nodes shouldn't be dying so easily!
179
+ let isTrusted = isServer() && await isTrustedByDomain(callSpec.DomainName);
180
+ if (isTrusted) {
181
+ // NOTE: Direct writes should probably be used for calls from trusted nodes, which will
182
+ // be a lot faster.
183
+ await baseWriteCall(callSpec, metadata);
184
+ } else {
185
+ await addCall(callSpec, metadata);
186
+ }
187
+ })();
188
+ callWaitOn(callSpec.CallId, promise);
189
+ await promise;
190
+ };
191
+
192
+ export async function onCallPredict(call: CallSpec | undefined) {
193
+ if (!call) return;
194
+ await pendingPredictedCalls.get(call.CallId)?.obj.promise;
195
+ }
196
+ export async function waitUntilAllPredictionsFinish() {
197
+ try {
198
+ await Promise.allSettled(Array.from(pendingPredictedCalls.values()).map(obj => obj.obj.promise));
199
+ } catch {
200
+ }
201
+ }
202
+
203
+
204
+ export async function flushDelayedFunctions() {
205
+ await prediction.flushDelayedFunctions();
206
+ }
207
+
208
+ export async function addCall(call: CallSpec, metadata: FunctionMetadata) {
209
+ await prediction.addCall(call, metadata);
210
+ }
211
+
212
+ export async function baseAddCall(call: CallSpec, nodeId: string, cancel: () => void, type: string) {
213
+ if (Querysub.DEBUG_CALLS) {
214
+ console.log(`[Querysub] ${magenta(type)} ${green(debugCallSpec(call))} @${debugTime(call.runAtTime)}`);
215
+ }
216
+ let errorWatcher = Querysub.createWatcher(() => {
217
+ let callResult = atomic(prediction.getCallResult(call));
218
+ if (callResult && "error" in callResult && Querysub.isAllSynced()) {
219
+ console.error(callResult.error.split("\n")[0] + `\n (${call.DomainName}.${call.FunctionId})\n ${callResult.error}`);
220
+ // Stop watching when we get the first error, otherwise other changes will trigger duplicate errors
221
+ // (such as when the prediction is removed).
222
+ errorWatcher.dispose();
223
+ }
224
+ });
225
+ setTimeout(() => {
226
+ errorWatcher.dispose();
227
+ }, MAX_CHANGE_AGE);
228
+ try {
229
+ await QuerysubController.nodes[nodeId].addCall(call);
230
+ } catch (e) {
231
+ // Cancellation is IMPORTANT! Otherwise the prediction might never be rejected,
232
+ // and so the client will keep stale data!
233
+ cancel();
234
+ throw e;
235
+ }
236
+ }
237
+
238
+ class QuerysubControllerBase {
239
+ public async watch(config: WatchConfig) {
240
+ for (let path of config.paths) {
241
+ Querysub.assertDomainAllowed(path);
242
+ }
243
+ for (let path of config.parentPaths) {
244
+ Querysub.assertDomainAllowed(path);
245
+ }
246
+
247
+ // Both watch it locally, AND remotely, so we will receive all the changes,
248
+ // AND forward them to the watcher
249
+ let caller = SocketFunction.getCaller();
250
+ let callerId = caller.nodeId;
251
+ let callerMachineId = IdentityController_getMachineId(caller);
252
+ let callerIP = IdentityController_getSecureIP(caller);
253
+ let pHash = permissionsHash(caller);
254
+
255
+ if (Querysub.SIMULATE_LAG) {
256
+ await delay(Querysub.SIMULATE_LAG);
257
+ }
258
+
259
+ let permissionsObjIn = permissionsPerNode.get(pHash);
260
+ if (!permissionsObjIn) {
261
+
262
+ permissionsObjIn = {
263
+ nodes: new Set(),
264
+
265
+ newPaths: new Set(),
266
+ newParentPaths: new Set(),
267
+
268
+ permissionsPathGroups: new Map(),
269
+ pathToPermissionsPath: new Map(),
270
+
271
+ checkNow: () => { },
272
+ dispose: () => { },
273
+ };
274
+ const permissionsObj = permissionsObjIn;
275
+
276
+
277
+ SocketFunction.onNextDisconnect(callerId, () => {
278
+ permissionsPerNode.delete(pHash);
279
+ permissionsObj?.dispose();
280
+ });
281
+ // TODO: Once we have fast and easy incremental watches, use that to make this faster
282
+ let watcher = proxyWatcher.createWatcher({
283
+ watchFunction: this.createPermissionsCheck({ permissionsObj, callerMachineId, callerIP, callerId }),
284
+ });
285
+ watchModuleHotreloads(watcher);
286
+ permissionsObjIn.dispose = watcher.dispose;
287
+ permissionsObjIn.checkNow = watcher.explicitlyTrigger;
288
+ permissionsPerNode.set(pHash, permissionsObjIn);
289
+ }
290
+
291
+ for (let path of config.paths) {
292
+ permissionsObjIn.newPaths.add(path);
293
+ }
294
+ for (let path of config.parentPaths) {
295
+ permissionsObjIn.newParentPaths.add(path);
296
+ }
297
+
298
+ permissionsObjIn.checkNow();
299
+ }
300
+ private createPermissionsCheck(config: {
301
+ permissionsObj: PermissionsObj;
302
+ callerMachineId: string;
303
+ callerIP: string;
304
+ callerId: string;
305
+ }) {
306
+ const { permissionsObj, callerMachineId, callerIP, callerId } = config;
307
+ return function querysubCheckPermissions() {
308
+ if (!permissionsObj) throw new Error(`Internal error`);
309
+
310
+ let permissions = new PermissionsCheck({ callerMachineId, callerIP });
311
+
312
+ let extraPathsToCheck = new Set<string>();
313
+ let allowedPaths = new Set<string>();
314
+ let notAllowedPaths = new Set<string>();
315
+ let removedPaths = new Set<string>();
316
+
317
+ // HACK: Maintain a recording of parent paths separately, to make some code simpler.
318
+ let parentPathsLookup = new Map<string, string>();
319
+
320
+ const newPaths = permissionsObj.newPaths;
321
+ const newParentPaths = permissionsObj.newParentPaths;
322
+
323
+ function addPermissionsPath(path: string, parentPath: string | undefined) {
324
+ let checkResult = permissions.checkPermissions(path);
325
+ if (Querysub.anyUnsynced()) return;
326
+ let permissionsPath = checkResult.permissionsPath;
327
+ let group = permissionsObj.permissionsPathGroups.get(permissionsPath);
328
+ if (!group) {
329
+ group = { allowed: checkResult.allowed, originalPaths: new Set(), originalParentPaths: new Map(), outdated: true };
330
+ permissionsObj.permissionsPathGroups.set(permissionsPath, group);
331
+ }
332
+ if (parentPath !== undefined) {
333
+ group.originalParentPaths.set(path, parentPath);
334
+ } else {
335
+ group.originalPaths.add(path);
336
+ }
337
+
338
+ permissionsObj.pathToPermissionsPath.set(path, permissionsPath);
339
+ }
340
+
341
+ for (let path of newPaths) {
342
+ extraPathsToCheck.add(path);
343
+ addPermissionsPath(path, undefined);
344
+ }
345
+ for (let parentPath of newParentPaths) {
346
+ // NOTE: checkPermissions handles "" keys specially, but we still need to
347
+ // turn them into a child key watch!
348
+ let testPath = appendToPathStr(parentPath, "");
349
+ parentPathsLookup.set(testPath, parentPath);
350
+ extraPathsToCheck.add(testPath);
351
+ addPermissionsPath(testPath, parentPath);
352
+ }
353
+
354
+ for (let [permissionsPath, obj] of permissionsObj.permissionsPathGroups) {
355
+ // NOTE: Even if it is already unsynced, the allowed flag might be wrong, so we have to check
356
+ // it irregardless.
357
+ let allowed = permissions.checkPermissions(permissionsPath).allowed;
358
+ if (allowed !== obj.allowed && !Querysub.anyUnsynced()) {
359
+ // NOTE: We don't reset outdated even if we return, that way if we run
360
+ // again we will check any previously outdated paths as well (otherwise we miss them).
361
+ obj.outdated = true;
362
+ obj.allowed = allowed;
363
+ }
364
+ }
365
+
366
+ // Don't commit anything if we have unsynced accesses after rechecking our permission paths
367
+ if (Querysub.anyUnsynced()) {
368
+ return;
369
+ }
370
+
371
+ // ONLY consume the paths if we are ready to apply them
372
+ permissionsObj.newPaths = new Set();
373
+ permissionsObj.newParentPaths = new Set();
374
+
375
+ let outdatedCount = 0;
376
+ for (let [permissionsPath, obj] of permissionsObj.permissionsPathGroups) {
377
+ if (!obj.outdated) continue;
378
+ // if (permissionsPath.startsWith(".,querysubtest._com.,PathFunctionRunner.,Page.,Data.,world.,grid")) {
379
+ // console.log(magenta(`Changed permission path ${permissionsPath}, ${obj.allowed ? "allowed" : "not allowed"}`), obj.originalPaths);
380
+ // }
381
+ outdatedCount++;
382
+ obj.outdated = false;
383
+ for (let path of obj.originalPaths) {
384
+ if (obj.allowed) allowedPaths.add(path);
385
+ else {
386
+ notAllowedPaths.add(path);
387
+ removedPaths.add(path);
388
+ }
389
+ }
390
+ for (let [path, parentPath] of obj.originalParentPaths) {
391
+ parentPathsLookup.set(path, parentPath);
392
+ if (obj.allowed) allowedPaths.add(path);
393
+ else {
394
+ notAllowedPaths.add(path);
395
+ removedPaths.add(path);
396
+ }
397
+ }
398
+ }
399
+
400
+ for (let path of extraPathsToCheck) {
401
+ let allowed = permissions.checkPermissions(path).allowed;
402
+ if (allowed) allowedPaths.add(path);
403
+ else notAllowedPaths.add(path);
404
+ }
405
+
406
+
407
+ function splitParentPaths(paths: Set<string>): [string[], string[]] {
408
+ let parentPaths = Array.from(paths).map(path => parentPathsLookup.get(path)).filter(isDefined);
409
+ // IMPORTANT! DO NOT filter parent paths out of regular paths. If we did this, then if they
410
+ // tried to sync any direct path that includes [""], they would never receive the value!
411
+ let justPaths = Array.from(paths);
412
+ return [parentPaths, justPaths];
413
+ }
414
+
415
+ if (notAllowedPaths.size > 0) {
416
+ if (PermissionsCheck.DEBUG && Querysub.isAllSynced()) {
417
+ let hackNotRootPaths = Array.from(notAllowedPaths).filter(path => getPathDepth(path) > DEPTH_TO_DATA);
418
+ if (hackNotRootPaths.length > 0) {
419
+ for (let path of hackNotRootPaths) {
420
+ let test = permissions.checkPermissions(path);
421
+ permissions.checkPermissions(test.permissionsPath);
422
+ }
423
+ console.log(red(`Disallowing watches due to disallowed permissions.`));
424
+ for (let path of hackNotRootPaths) console.log(` ${blue(path)}`);
425
+ logErrors(LoggingClient.nodes[callerId].onMessage({
426
+ type: "warn",
427
+ message: `Disallowing ${hackNotRootPaths.length} watches due to disallowed permissions.`,
428
+ context: (
429
+ hackNotRootPaths.map(path => ` ${path}`).join("\n")
430
+ ),
431
+ }));
432
+ }
433
+ }
434
+ let [newParentPathsNotAllowed, newPathsNotAllowed] = splitParentPaths(notAllowedPaths);
435
+ // Send them undefined values at epochTime
436
+ // NOTE: We COULD send at time Date.now(), which would allow clobbering any existing values. This might
437
+ // make the UI look cleaner (instead of showing stale values, it shows nothing)?
438
+ let undefinedValues: PathValue[] = newPathsNotAllowed.map(path => ({ path, value: undefined, canGCValue: true, time: epochTime, locks: [], lockCount: 0, valid: true, event: false }));
439
+
440
+ diskLog(`Disallowing PathValue watches due to disallowed permissions`, { count: newPathsNotAllowed.length, callerId });
441
+
442
+ ignoreErrors(pathValueSerializer.serialize(undefinedValues, { compress: Querysub.COMPRESS_NETWORK }).then(buffers =>
443
+ PathValueController.nodes[callerId].forwardWrites(
444
+ buffers,
445
+ Array.from(newParentPathsNotAllowed),
446
+ )
447
+ ));
448
+ }
449
+
450
+ if (removedPaths.size > 0) {
451
+ let [removedParentPaths, removedJustPaths] = splitParentPaths(removedPaths);
452
+ pathWatcher.unwatchPath({ callback: callerId, paths: removedJustPaths, parentPaths: removedParentPaths });
453
+ }
454
+ if (allowedPaths.size > 0) {
455
+ let [newParentPathsAllowed, newPathsAllowed] = splitParentPaths(allowedPaths);
456
+ let watchConfig: WatchConfig = { paths: newPathsAllowed, parentPaths: newParentPathsAllowed };
457
+ pathWatcher.watchPath({ callback: callerId, ...watchConfig });
458
+ remoteWatcher.watchLatest(watchConfig);
459
+ }
460
+ };
461
+ }
462
+ public async unwatch(config: WatchConfig) {
463
+ let pHash = permissionsHash(SocketFunction.getCaller());
464
+ let callerId = SocketFunction.getCaller().nodeId;
465
+ let permissionsObjIn = permissionsPerNode.get(pHash);
466
+ if (permissionsObjIn) {
467
+ const permissionsObj = permissionsObjIn;
468
+ function delPermissionsPath(path: string, parentPath: boolean) {
469
+ let permissionsPath = permissionsObj.pathToPermissionsPath.get(path);
470
+ if (!permissionsPath) return;
471
+ let group = permissionsObj.permissionsPathGroups.get(permissionsPath);
472
+ if (!group) return;
473
+ permissionsObj.pathToPermissionsPath.delete(path);
474
+ group.originalPaths.delete(path);
475
+ if (parentPath) {
476
+ group.originalParentPaths.delete(path);
477
+ }
478
+ if (group.originalPaths.size === 0 && group.originalParentPaths.size === 0) {
479
+ permissionsObj.permissionsPathGroups.delete(permissionsPath);
480
+ }
481
+ }
482
+
483
+ for (let path of config.paths) {
484
+ delPermissionsPath(path, false);
485
+ }
486
+ for (let path of config.parentPaths) {
487
+ delPermissionsPath(appendToPathStr(path, ""), true);
488
+ }
489
+ }
490
+ pathWatcher.unwatchPath({ callback: callerId, ...config });
491
+ }
492
+ public async addCall(call: CallSpec) {
493
+ if (Querysub.DEBUG_CALLS) {
494
+ console.log(`[Querysub] addCall @${debugTime(call.runAtTime)}: ${call.DomainName}.${call.ModuleId}.${call.FunctionId}`);
495
+ }
496
+ Querysub.assertDomainAllowed(getPathStr1(call.DomainName));
497
+
498
+ let caller = SocketFunction.getCaller();
499
+ let callerMachineId = IdentityController_getMachineId(caller);
500
+ let callerCreatorId = IdentityController_getPubKeyShort(caller);
501
+ call.callerIP = IdentityController_getSecureIP(caller);
502
+
503
+ if (Querysub.SIMULATE_LAG) {
504
+ await delay(Querysub.SIMULATE_LAG * 2);
505
+ }
506
+
507
+ // 0) Ensure the call is valid
508
+ // - Verifies callerMachineId
509
+ // - Uses MAX_ACCEPTED_CHANGE_AGE to check the write time
510
+ // - Rejects out of order times
511
+ // - Verify the time creatorId
512
+ if (call.callerMachineId !== callerMachineId) {
513
+ throw new Error(`Caller machineId does not match call's callerMachineId, ${call.callerMachineId} !== ${callerMachineId}`);
514
+ }
515
+ // NOTE: We use a lower threshold than FunctionRunner to account for extra time for FunctionRunner to receive the call.
516
+ // We want to reject slow calls from the client promptly, as at this point we know it is a client issue. If the call
517
+ // passes here FunctionRunner will actually update runAtTime for old calls, running them anyways. We would prefer
518
+ // if slow clients don't depend on this behavior, as it is only intended to smooth over backend server issues,
519
+ // not to adjust slow (or really, disconnected and then reconnected) clients.
520
+ let now = Date.now();
521
+ let callTimethreshold = now - MAX_ACCEPTED_CHANGE_AGE * 0.5;
522
+ if (call.runAtTime.time < callTimethreshold) {
523
+ throw new Error(`Call is too old, and will be dropped, ${debugCallSpec(call)}, ${call.runAtTime.time} < ${callTimethreshold}`);
524
+ }
525
+
526
+ let futureThreshold = now + Querysub.MAX_FUTURE_CALL_TIME;
527
+ if (call.runAtTime.time > futureThreshold) {
528
+ let futureTime = call.runAtTime.time - futureThreshold;
529
+ throw new Error(`Call is too far in the future, ${debugCallSpec(call)}, ${call.runAtTime.time}. Future ${futureTime} > ${Querysub.MAX_FUTURE_CALL_TIME}`);
530
+ }
531
+
532
+ // NOTE: There isn't really any reason to force this, as they could easily have multiple nodes/machines connected
533
+ // (if they were attempting to cheat). So while running out of order might be annoying, and we would like to prevent it...
534
+ // this check doesn't really accomplish that in any meaningful way (and instead breaks some valid use cases).
535
+ // let lastTime = lastCallTime(caller.nodeId);
536
+ // if (compareTime(call.runAtTime, lastTime.value) <= 0) {
537
+ // throw new Error(`Call is out of order, ${call.runAtTime.time} <= ${lastTime.value.time}`);
538
+ // }
539
+ // lastTime.value = call.runAtTime;
540
+
541
+ if (call.runAtTime.creatorId !== callerCreatorId) {
542
+ throw new Error(`Call creatorId does not match caller creatorId, ${call.runAtTime.creatorId} !== ${callerCreatorId}`);
543
+ }
544
+
545
+ // 1) Ensure the caller has at least permissions on the data for this module
546
+ // NOTE: We could use commitFunction in watch as well, but... I think watches will benefit
547
+ // more from the batching, where as calls need batching less (and commitFunction is easier to use).
548
+ let hasPermissions = await proxyWatcher.commitFunction({
549
+ watchFunction: function checkPermissions() {
550
+ let dataPath = getProxyPath(() => (functionSchema()[call.DomainName].PathFunctionRunner[call.ModuleId].Data as any)[Querysub.CALL_PERMISSIONS_KEY]);
551
+ let check = new PermissionsCheck(call);
552
+ return check.checkPermissions(dataPath).allowed;
553
+ },
554
+ });
555
+
556
+ if (!hasPermissions) {
557
+ throw new Error(`Caller does not have permission to call ${call.DomainName}.${call.ModuleId}`);
558
+ }
559
+
560
+ // NOTE: We don't wait for our writes to actually commit, because... there isn't any reason they shouldn't
561
+ // except the server being down, in which case, the client can figure that out anyways?
562
+ proxyWatcher.writeOnly({
563
+ canWrite: true,
564
+ eventWrite: true,
565
+ doNotStoreWritesAsPredictions: true,
566
+ watchFunction: function writeCall() {
567
+ functionSchema()[call.DomainName].PathFunctionRunner[call.ModuleId].Calls[call.CallId] = atomicObjectWrite(call);
568
+ },
569
+ });
570
+ }
571
+
572
+ public async getModulePath(config: {
573
+ functionSpec: FunctionSpec;
574
+ }): Promise<string> {
575
+ let spec = config.functionSpec;
576
+ Querysub.assertDomainAllowed(getPathStr1(spec.DomainName));
577
+ let moduleConfig = await proxyWatcher.commitFunction({
578
+ watchFunction: function getModuleConfig() {
579
+ return functionSchema()[spec.DomainName].PathFunctionRunner[spec.ModuleId].Sources[spec.FunctionId];
580
+ }
581
+ });
582
+ if (!moduleConfig) {
583
+ throw new Error(`Function ${spec.DomainName}.${spec.ModuleId}.${spec.FunctionId} does not exist (a deploy might be required).`);
584
+ }
585
+
586
+ let module = await getModuleFromConfig(moduleConfig);
587
+ return module.filename;
588
+ }
589
+
590
+
591
+ // NOTE: Maybe remove this? It's very useful for debugging though...
592
+ public async debugGetSingleReadNode(path: string) {
593
+ return pathValueAuthority2.getSingleReadNodeSync(path);
594
+ }
595
+
596
+ public async debugGetPathAuthorities(nodeId: string) {
597
+ return pathValueAuthority2.getAuthorityPaths(nodeId);
598
+ }
599
+ }
600
+
601
+ export const QuerysubController = SocketFunction.register(
602
+ "QuerysubController-6db5ef05-7563-4473-a440-2f64f03fe6ef",
603
+ new QuerysubControllerBase(),
604
+ () => ({
605
+ // TODO: Maybe allow compression to be enable (or disabled) at a per call / schema level?
606
+ watch: { compress: true, },
607
+ unwatch: { compress: true, },
608
+ addCall: { compress: true, },
609
+ debugGetSingleReadNode: {},
610
+ debugGetPathAuthorities: {},
611
+ getModulePath: {},
612
+ }),
613
+ () => ({
614
+
615
+ }),
616
+ {
617
+ noAutoExpose: true,
618
+ }
619
+ );
620
+
File without changes