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
@@ -1,522 +1,597 @@
1
- import fs from "fs";
2
- import { cache, lazy } from "socket-function/src/caching";
3
- import { getStringKeys, sort } from "socket-function/src/misc";
4
- import { Args } from "socket-function/src/types";
5
- import { appendToPathStr, getPathFromStr, getPathStr, rootPathStr } from "../path";
6
- import { writeFunctionCall } from "./PathFunctionHelpers";
7
- import { CallSpec, functionSchema } from "./PathFunctionRunner";
8
- import { getDomain, isLocal } from "../config";
9
- import { isHotReloading } from "socket-function/hot/HotReloadController";
10
- import { Schema2, Schema2Fncs, Schema2T, SchemaPath } from "../2-proxy/schema2";
11
- import { PathValueProxyWatcher, atomic, proxyWatcher, registerSchema } from "../2-proxy/PathValueProxyWatcher";
12
- import { registerAliveChecker } from "../2-proxy/garbageCollection";
13
- import { rawSchema } from "../2-proxy/pathDatabaseProxyBase";
14
- import { ARCHIVE_FLUSH_LIMIT } from "../0-path-value-core/pathValueCore";
15
- import debugbreak from "debugbreak";
16
- import { getPathFromProxy, getProxyPath } from "../2-proxy/pathValueProxy";
17
- import { CALL_PERMISSIONS_KEY } from "../4-querysub/permissionsShared";
18
- import { LOCAL_DOMAIN } from "../0-path-value-core/PathController";
19
- import path from "path";
20
- import { isEmpty } from "../misc";
21
- import { getSpecFromModule } from "./pathFunctionLoader";
22
- import { isNode } from "typesafecss";
23
- import { Querysub } from "../4-querysub/QuerysubController";
24
-
25
- // This is the the function id which should be used when creating the FunctionSpec (in order to load the module),
26
- // to access the permissions in the schema.
27
- export const PERMISSIONS_FUNCTION_ID = "PERMISSIONS_FUNCTION_ID";
28
-
29
- const SCHEMA_EXPORT_KEY = "schema-00c87ab5-10bb-4795-8f6b-e187a7e45841";
30
- export function getSchemaObject(module: NodeJS.Module): SchemaObject | undefined {
31
- return module.exports[SCHEMA_EXPORT_KEY];
32
- }
33
-
34
- export function getExportPath(functionId: string): string {
35
- return getPathStr([SCHEMA_EXPORT_KEY, "rawFunctions", functionId]);
36
- }
37
-
38
- export type FunctionMetadata<F = unknown> = {
39
- nopredict?: boolean;
40
- /** Removes unnecessary function calls by first delaying by Querysub.DELAY_COMMIT_DELAY, Then looking at their predictions and removing ones where their predictions completely clobber the other ones.
41
- * - Determines unnecessary calls via our function prediction. If our function is prediction is wrong, we might remove calls that shouldn't be removed.
42
- * - This is quite dangerous, but extremely useful for things such as editors when you want to type and have things update quickly, but you don't want to spam the server with calls.
43
- * - Only applied for clients.
44
- * - We will always preserve the order of function calls in the same schema, so if you make another function call that doesn't have delay commit in the same schema, then we will forcefully commit all the delayed commit functions first.
45
- * - This shouldn't cause an excessive amount of function commits. The delayed commit time is very small, maybe around five seconds. And if you are regularly having commits that are faster than that, that aren't delayed commit, then you're already having a lot of commits. And we will still remove unnecessary calls, even if we have to commit them earlier than the delay.
46
- * - IMPORTANT! Our check for if values should be removed isn't perfect. If you write to a path that another value reads from, and then it writes that value to a path that we then read from... This would mean our value can't be clobbered as it has side effects which would affect values that could regularly clobber us. However, we don't detect this case, and we will just allow it to be clobbered.
47
- * - However, when we clobber values, we will cancel them client-side fairly quickly, so it should be apparent if this bug happens, in which case the writes you're doing are too complicated and you shouldn't be using delay commit. Delay commit is only really intended for simple cases, such as the user presses a key and then you set some text equal to a result based on that key (It doesn't work with deltas, You need to be forcefully setting the value).
48
- */
49
- delayCommit?: boolean;
50
-
51
- /** By default, we try to finish calls in the start order when using Querysub.commitAsync, unless the caller explicitly says not to. However, if this is set, then we won't have this call finish or cause other calls to be finished in the start order. This makes locking less safe, but can be useful for long-running functions that shouldn't block other functions and which the order doesn't matter. */
52
- noFinishInStartOrder?: boolean;
53
-
54
- /** Too many locks can lag the server, and eventually cause crashes. Consider using Querysub.noLocks(() => ...) around code that is accessing too many values, assuming you don't want to lock them. However, if absolutely required, you can override max locks to allow as many locks to be created as you want until the server crashses... */
55
- maxLocksOverride?: number;
56
- };
57
-
58
- export interface SchemaObject<Schema = any, Functions = any> {
59
- data: () => Schema;
60
- domainName: string;
61
- moduleId: string;
62
- functions: {
63
- [key in keyof Functions]: (
64
- //Functions[key] &
65
- // NOTE: Functions don't have results, if you want a result write to the database
66
- // and then watch it for the result.
67
- // - The promise resolves once the function has started the commit process, NOT when
68
- // it has finished. Once it resolves you can terminate your process and the function
69
- // will run (but it might still error out due to taking too long to reach the server,
70
- // or too long to run, or insufficient privileges, etc, etc).
71
- ((...args: Args<Functions[key]>) => CallSpec) & {
72
- // Add a "debugger;" when the holding function is called, to make it easy to debug the function
73
- debug(): void;
74
- }
75
- )
76
- };
77
- functionMetadata: {
78
- [key in keyof Functions]?: FunctionMetadata<Functions[key]> | undefined;
79
- };
80
- rawFunctions: Functions;
81
- permissions: SchemaToPermissions<Schema> | undefined;
82
- permissionsNonWildcards: Map<string, PermissionsCallback>;
83
- // Sorted by most nested first
84
- permissionsWildcards: {
85
- pathParts: string[];
86
- callback: PermissionsCallback;
87
- }[];
88
- }
89
-
90
- let existsCached = cache((path: string) => {
91
- return fs.existsSync(path);
92
- });
93
-
94
-
95
- export function getModuleRelativePath(module: NodeJS.Module): string {
96
- let root = path.resolve(".").replaceAll("\\", "/");
97
- if (!existsCached(root + "/package.json")) {
98
- throw new Error(`Expected to find package.json at ${root}. The current directory must be the root of the project when deploying, so we can resolve relative file references.`);
99
- }
100
- // Get the relative path
101
- let relativePath = path.relative(root, module.filename).replaceAll("\\", "/");
102
- if (relativePath.startsWith("../")) {
103
- let otherFolder = relativePath.split("/")[1];
104
- if (existsCached("node_modules/" + otherFolder)) {
105
- relativePath = "node_modules/" + otherFolder + "/" + relativePath.split("/").slice(2).join("/");
106
- }
107
- }
108
- return "/" + relativePath;
109
- }
110
-
111
- export type PermissionsParameters = {
112
- // Each part of the path that matched with a wildcard ("*") is added to this array (in order).
113
- pathWildcards: string[];
114
- /**
115
- * The path the permissions check matched.
116
- * NOTE: The full path matched is not provided for optimization purposes. Otherwise for large
117
- * writes the permissions check would be needlessly run. IF further depth is required,
118
- * use "*" wildcards. If the depth is dynamic... your structure is probably too complex,
119
- * or something similar to a tree. Destructure it to a list, and all of your
120
- * code will run significantly faster.
121
- *
122
- * TODO: If we ABSOLUTELY need it, we COULD conditionally expose fullPath, and just disable
123
- * optimizations when we do. We could even have a parameter which replaces matchedPath with
124
- * fullPath. But, ideally we don't, as it is just very inefficient...
125
- * NOTE: Usually pathWildcards should be used instead. This is only useful if you want to create
126
- * a very generic permission check which can be used in many hierarchies, and depends on the
127
- * hierarchy (ex, if it checks path.slice(0, 3).users for the users table).
128
- **/
129
- matchedPath: string;
130
- /** The callerMachineId will be unique to the calling machine (but a machine may have multiple,
131
- ex, 1 per browser, another a fresh one if they are in incognito mode).
132
- - To tie machineIds to userIds simple create a lookup in your database, for example:
133
- `{ machineToUserId: { [machineId: string]: string | undefined } }`
134
- Logging out from a machine is simply:
135
- `machineToUserId[machineId] = undefined`
136
- A reverse index may also be useful, to efficiently log out everywhere (or just
137
- show all machines that a user is logged into)
138
- ```
139
- { userIdToMachineIds: { [userId: string]: { [machineId: string]: 1 } } }
140
- funcion logoutEverywhere(userId: string) {
141
- for (let machineId in userIdToMachineIds[userId]) {
142
- userIdToMachineIds[userId][machineId] = undefined;
143
- machineToUserId[machineId] = undefined;
144
- }
145
- }
146
- ```
147
- * */
148
- callerMachineId: string;
149
-
150
- // IMPORTANT! DO NOT add "matchedValue" here. It is convenient, BUT, it is better for the user
151
- // to access it themselves using pathWildcards. This makes it more typesafe, allowing
152
- // them to find all references and find that reference (otherwise the reference will be hidden).
153
- // - Also, I don't think we could make matchedValue typesafe at all, so it would have to be any,
154
- // which is terrible.
155
- };
156
- /** A false of false will deny read permissions, resulting in all reads being given value with a value
157
- * of undefined, and a time of 0.
158
-
159
- */
160
- export type PermissionsCallback = (config: PermissionsParameters) => PermissionsCheckResult;
161
- export type PermissionsCheckResult = boolean | { allowed: boolean; skipParentChecks?: boolean; };
162
- /*
163
- NOTE: All ancestor permissions checks are applied as well.
164
- - This means if the ancestor disallows, it blocks any descendant checks even if they are allowed.
165
-
166
- NOTE: If no permissions match, then access is disallowed by default.
167
-
168
- NOTE: Permissions checks at the root (permissions: { READ_PERMISSIONS(config) { } }) will
169
- be used to verify calls are allowed as well, even if the call doesn't read or write any data
170
- that is under a permissions check.
171
- - THIS MEANS that if there is no permission check at the root, calls won't be allowed by any user
172
- (as we default to disallow).
173
-
174
- NOTE: Servers watches ignore permissions, as they directly communicate with the underlying PathValueServer,
175
- skipping the ProxyServer.
176
- - However function calls by servers are still run by FunctionRunner, which will apply permission checks.
177
-
178
- IMPORTANT! Wildcards are tested via the "" path. This means if the "" key is provided access to a user, they can run
179
- Object.keys()/Object.values() on the data. In order to not accidentally provide list access, never allow "" to be
180
- a valid path, by never providing access by exclusion. For example:
181
- BROKEN: `{ ["*"]: { PERMISSIONS(){ return config.pathWildcards?.[0] !== "admin" || users()[config.callerId].isAdmin; } } }`
182
- - This will allow users to access the "admin" data via Object.values(), because the "" !== "admin"
183
- BROKEN: `{ admin: { PERMISSIONS(){ return users()[config.callerId].isAdmin; } } }`
184
- - Because "" isn't checked
185
- CORRECT: `{ ["*"]: { PERMISSIONS(){ return config.pathWildcards?.[0] === config.callerId; } } }`
186
- CORRECT: `{ ["*"]: { PERMISSIONS(){ return users()[config.callerId].isAdmin || config.pathWildcards?.[0] === config.callerId; } } }`
187
-
188
- NOTE: Permission checks lag a bit behind data. This means not only will a write that removes checks of itself
189
- still be sent (ex, if a user removes their admin access, and only admins can see who is an admin or not,
190
- they will be told about their admin change). BUT, a user will receive writes for a bit after that as well!
191
- The delay is equal to the network propagation time between two server, which is small (ideally milliseconds),
192
- but in the case of excess lag could easily be 10s of seconds (in which case everything on the site would
193
- be lagging by 10s of seconds as well).
194
-
195
- For example
196
- permissions: {
197
- userSecretData: {
198
- ["*"]: {
199
- PERMISSIONS(pathWildcards) {
200
- return config.pathWildcards[0] === config.callerMachineId;
201
- }
202
- }
203
- }
204
- }
205
- */
206
- export type SchemaToPermissions<Schema> = {
207
- [key in keyof Schema]?: SchemaToPermissions<Schema[key]>;
208
- } & {
209
- PERMISSIONS?: PermissionsCallback;
210
- } | {
211
- PERMISSIONS?: PermissionsCallback;
212
- };
213
-
214
- /** A special key which is prefixed (along with a random value) before special keys used to check child
215
- * permissions. This allows us to explicitly disallow parent syncing (ex Object.keys()) of values.
216
- */
217
- export const CHILD_CHECK_PREFIX = "";
218
-
219
- export function hasWildcardMatch(permissionsPath: string[], valuePathParts: string[]): boolean {
220
- for (let i = 0; i < permissionsPath.length; i++) {
221
- let part = permissionsPath[i];
222
- if (part === "*") {
223
-
224
- } else if (part !== valuePathParts[i]) {
225
- return false;
226
- }
227
- }
228
- return true;
229
- }
230
- export function getWildcardMatches(permissionsPath: string[], valuePathParts: string[]): string[] | undefined {
231
- if (permissionsPath.length > valuePathParts.length) return undefined;
232
- let wildcardMatches: string[] | undefined;
233
- for (let i = 0; i < permissionsPath.length; i++) {
234
- let part = permissionsPath[i];
235
- if (part === "*") {
236
- if (!wildcardMatches) wildcardMatches = [];
237
- wildcardMatches.push(valuePathParts[i]);
238
- } else if (part !== valuePathParts[i]) {
239
- return undefined;
240
- }
241
- }
242
- return wildcardMatches;
243
- }
244
-
245
- let inliningCalls = false;
246
- export function inlineNestedCalls<T>(code: () => T) {
247
- let prev = inliningCalls;
248
- inliningCalls = true;
249
- try {
250
- return code();
251
- } finally {
252
- inliningCalls = prev;
253
- }
254
- }
255
-
256
- type SyncSchemaResult<Schema> = {
257
- // NOTE: The functions are over the network, AND, eventually consistent, so it is not possible
258
- // for them to return a result. If a result is required, pass an id which will instruct them where
259
- // to write their result to, and then watch that location.
260
- <Functions extends { [name: string]: ((...args: any) => void) }>(config: {
261
- domainName?: string;
262
- /** NOTE: moduleId is used to store your data in a unique location, that doesn't change
263
- * if you move this file around. Any unique identifier is allowed, as the deploy command
264
- * will warn you if you accidentally reused a moduleId.
265
- */
266
- moduleId: string;
267
- module: NodeJS.Module;
268
- functions: Functions;
269
- functionMetadata?: { [name in keyof Functions]?: FunctionMetadata<Functions[name]> };
270
- /** Provide access to [Querysub.CALL_PERMISSIONS_KEY] to allow function calls (or provide access at the root). */
271
- permissions?: SchemaToPermissions<Schema> & {
272
- [CALL_PERMISSIONS_KEY]?: {
273
- PERMISSIONS: PermissionsCallback;
274
- };
275
- };
276
- }): SchemaObject<Schema, Functions>;
277
- };
278
-
279
- export function syncSchema<Schema>(): SyncSchemaResult<Schema>;
280
- export function syncSchema<SchemaDef extends Schema2>(schema: SchemaDef): SyncSchemaResult<Schema2T<SchemaDef>>;
281
- export function syncSchema<Schema>(schema?: Schema2): SyncSchemaResult<Schema> {
282
- return function getFunctions(config) {
283
- const { moduleId, functions, module, permissions } = config;
284
- const domainName = config.domainName ?? getDomain();
285
-
286
- const data = () => functionSchema()[domainName].PathFunctionRunner[moduleId].Data;
287
-
288
- let fncs = { ...functions };
289
-
290
- let undoChecksAdded: {
291
- name: string;
292
- path: SchemaPath;
293
- }[] = [];
294
- if (schema) {
295
- let dataPrefixPath = getPathFromStr(getProxyPath(data));
296
- let fnc = getSpecFromModule(module);
297
- // NOTE: We call syncSchema a lot, so we default the git hash. However, for modules that provide
298
- // functions we call as synced functions, we SHOULD have a real hash here.
299
- let gitHash = fnc?.gitRef || "ambient";
300
- registerSchema({ domainName, moduleId, schema, gitHash });
301
-
302
- let gcDefs = Schema2Fncs.getGCObjects(schema);
303
- for (let { path, gcDelay } of gcDefs) {
304
- createObjectAliveChecker(data, path, gcDelay);
305
- }
306
-
307
- let undoChecks = Schema2Fncs.getAllUndoChecks(schema);
308
- for (let checkObj of undoChecks) {
309
- // Need at least one check
310
- if (checkObj.undoChecks.length === 0) continue;
311
- function niceSchemaPath(path: SchemaPath) {
312
- return path.map(x => x === Schema2Fncs.WildCard ? "*" : x).join(".");
313
- }
314
- let name = `__undoer__${niceSchemaPath(checkObj.path)}`;
315
- let fullPath = (dataPrefixPath as SchemaPath).concat(checkObj.path);
316
- undoChecksAdded.push({
317
- name,
318
- path: fullPath,
319
- });
320
- (fncs as any)[name] = function undoCallback(path: string[], value: unknown) {
321
- if (!isChildPath(fullPath, path)) {
322
- throw new Error(`Tried to access undo function with path not under undo spec ${niceSchemaPath(fullPath)} . Tried to use path ${path.join(".")}`);
323
- }
324
-
325
- let permissionsChecker = proxyWatcher.getTriggeredWatcher().permissionsChecker;
326
- if (permissionsChecker) {
327
- let permissionsCheck = permissionsChecker.checkPermissions(getPathStr(path));
328
- if (!permissionsCheck?.allowed) {
329
- throw new Error(`Failed read permission check for undo call at path ${path.join(".")}`);
330
- }
331
- }
332
- for (let check of checkObj.undoChecks) {
333
- let nestedPath = path.slice(0, fullPath.length);
334
- let key = nestedPath[nestedPath.length - 1];
335
- if (!atomic(check(key, nestedPath))) {
336
- throw new Error(`Failed undo permission check at path ${nestedPath.join(".")}, check ${check.name || check.toString().slice(0, 100)}`);
337
- }
338
- }
339
- let parent = rawSchema()() as any;
340
- for (let i = 0; i < path.length - 1; i++) {
341
- parent = parent[path[i]];
342
- }
343
- let key = path[path.length - 1];
344
-
345
- // NOTE: This isn't an atomic set, because we leave it up proxyWatcher (and schema2)
346
- // to correctly handle assigning the values atomically.
347
- parent[key] = value;
348
- };
349
- }
350
- }
351
-
352
- let wrappedFunctions: { [name: string]: (...args: any) => CallSpec } = Object.create(null);
353
- let functionMetadata = config.functionMetadata;
354
- for (let [name, fnc] of Object.entries(fncs)) {
355
- let metadata = functionMetadata?.[name] ?? {};
356
- let debugging = false;
357
- wrappedFunctions[name] = Object.assign((...args: any[]): any => {
358
- if (inliningCalls) {
359
- return fnc(...args);
360
- }
361
- if (debugging) {
362
- debugger;
363
- }
364
- return writeFunctionCall({
365
- domainName,
366
- moduleId,
367
- functionId: name,
368
- args,
369
- metadata: metadata
370
- });
371
- }, {
372
- debug() {
373
- debugging = true;
374
- }
375
- });
376
- }
377
-
378
- for (let check of undoChecksAdded) {
379
- registeredUndoChecks.push({
380
- path: check.path,
381
- undoSet: wrappedFunctions[check.name] as any,
382
- });
383
- }
384
-
385
- let permissionsNonWildcards = new Map<string, PermissionsCallback>();
386
- let permissionsWildcards: { pathParts: string[]; callback: PermissionsCallback }[] = [];
387
- addPermissions(rootPathStr, permissions);
388
- function addPermissions(path: string, permissions: SchemaToPermissions<Schema> | undefined) {
389
- if (!permissions) return;
390
- for (let key of getStringKeys(permissions)) {
391
- if (key === "PERMISSIONS") {
392
- let value = permissions.PERMISSIONS;
393
- if (value) {
394
- if (path.includes("*")) {
395
- permissionsWildcards.push({
396
- pathParts: getPathFromStr(path),
397
- callback: value,
398
- });
399
- } else {
400
- permissionsNonWildcards.set(path, value);
401
- }
402
- }
403
- } else {
404
- addPermissions(appendToPathStr(path, key), permissions[key]);
405
- }
406
- }
407
- }
408
- // Sort reversed, so the most specific checks are first
409
- permissionsWildcards.sort((b, a) => a.pathParts.length - b.pathParts.length);
410
-
411
- // NOTE: We cache the root with lazy, to save time evaluating each proxy on each access. Also, it reduces the extra paths accesses that will never have data.
412
- // - Permissions should be fine, as we check them directly (if the user wants a path we check it), so we don't throw on accessing the root data and then allow all child accesses (each path is checked independently).
413
- let dataCached = lazy(() => data());
414
- let result: SchemaObject = {
415
- data: dataCached,
416
- domainName,
417
- moduleId,
418
- functions: wrappedFunctions as any,
419
- functionMetadata: functionMetadata as any,
420
- rawFunctions: fncs,
421
- permissions,
422
- permissionsNonWildcards,
423
- permissionsWildcards,
424
- };
425
-
426
- if (!isHotReloading() && module.exports[SCHEMA_EXPORT_KEY]) {
427
- throw new Error(`Module ${moduleId} already has a schema. Only 1 schema is allowed per module.`);
428
- }
429
-
430
- // NOTE: We clobber the previous value, as we might just be hotreloading, which
431
- // could mean the exports value has our previous value.
432
- module.exports[SCHEMA_EXPORT_KEY] = result;
433
- developmentModules.set(moduleId, module);
434
- return result;
435
- };
436
- }
437
- let developmentModules = new Map<string, NodeJS.Module>();
438
-
439
- export function getDevelopmentModule(moduleId: string): NodeJS.Module | undefined {
440
- return developmentModules.get(moduleId);
441
- }
442
- export function getAllDevelopmentModulesIds(): string[] {
443
- return Array.from(developmentModules.keys());
444
- }
445
-
446
- function isChildPath(spec: SchemaPath, path: string[]): boolean {
447
- if (spec.length > path.length) return false;
448
- for (let i = 0; i < spec.length; i++) {
449
- if (spec[i] === Schema2Fncs.WildCard) continue;
450
- if (spec[i] !== path[i]) return false;
451
- }
452
- return true;
453
- }
454
-
455
- // pathStr => ...
456
- let registeredUndoChecks: {
457
- path: SchemaPath;
458
- undoSet: (nestedPath: string[], value: unknown) => void;
459
- }[] = [];
460
-
461
- export function registerUndoCallback(path: SchemaPath, undoSet: (nestedPath: string[], value: unknown) => void) {
462
- registeredUndoChecks.push({ path, undoSet });
463
- }
464
-
465
- /** Throws if there is no undo callback available at this path. */
466
- export function getUndoCallback(path: string[]): {
467
- setValue(path: string[], value: unknown): void;
468
- } {
469
- // Local undos are always allowed
470
- if (path[0] === LOCAL_DOMAIN) {
471
- return {
472
- setValue(path, value) {
473
- if (path[0] !== LOCAL_DOMAIN) throw new Error(`Cannot set non-local value with local undo function. Tried to use path ${path.join(".")}`);
474
- let parent = rawSchema()() as any;
475
- for (let i = 0; i < path.length - 1; i++) {
476
- parent = parent[path[i]];
477
- }
478
- let key = path[path.length - 1];
479
- parent[key] = value;
480
- }
481
- };
482
- }
483
- // Any callback should be fine?
484
- let undoCallbacks = registeredUndoChecks.filter(x => isChildPath(x.path, path));
485
- sort(undoCallbacks, x => -x.path.length);
486
- if (undoCallbacks.length === 0) {
487
- throw new Error(`No undo callback available at path ${path.join(".")}. There needs to be an undo region at or above this path, via t.undoRegion`);
488
- }
489
- return { setValue: undoCallbacks[0].undoSet };
490
- }
491
-
492
- function createObjectAliveChecker(data: () => any, path: SchemaPath, delay: number | undefined) {
493
- registerAliveChecker({
494
- object(wildcard) {
495
- let obj = data();
496
- for (let part of path) {
497
- if (part === Schema2Fncs.WildCard) {
498
- obj = obj[wildcard];
499
- } else {
500
- obj = obj[part];
501
- }
502
- }
503
- return obj;
504
- },
505
- isAlive(value, path) {
506
- let parent = rawSchema()() as any;
507
- for (let i = 0; i < path.length - 1; i++) {
508
- parent = parent[path[i]];
509
- }
510
- let key = path[path.length - 1];
511
- let isAlive = key in parent;
512
- return isAlive;
513
- },
514
- deleteDelayTime: delay,
515
- // NOTE: This is actually safe, as we only read a single value, and are really
516
- // just looking to see if ANYTHING has created the value. Without this there is
517
- // a large gap when the value could be deleted, then undeleted, and then
518
- // as we only read old values we might not see the undelete. WITH this the gap
519
- // is much smaller (but still there).
520
- unsafeLiveReads: true,
521
- });
1
+ import fs from "fs";
2
+ import { cache, lazy } from "socket-function/src/caching";
3
+ import { getStringKeys, sort } from "socket-function/src/misc";
4
+ import { Args } from "socket-function/src/types";
5
+ import { appendToPathStr, getPathFromStr, getPathStr, joinPathStres, rootPathStr } from "../path";
6
+ import { writeFunctionCall } from "./PathFunctionHelpers";
7
+ import { CallSpec, functionSchema } from "./PathFunctionRunner";
8
+ import { getDomain, isLocal } from "../config";
9
+ import { isHotReloading } from "socket-function/hot/HotReloadController";
10
+ import { Schema2, Schema2Fncs, Schema2T, SchemaPath } from "../2-proxy/schema2";
11
+ import { PathValueProxyWatcher, atomic, proxyWatcher, registerSchema } from "../2-proxy/PathValueProxyWatcher";
12
+ import { registerAliveChecker } from "../2-proxy/garbageCollection";
13
+ import { rawSchema } from "../2-proxy/pathDatabaseProxyBase";
14
+ import { ARCHIVE_FLUSH_LIMIT } from "../0-path-value-core/pathValueCore";
15
+ import debugbreak from "debugbreak";
16
+ import { getPathFromProxy, getProxyPath } from "../2-proxy/pathValueProxy";
17
+ import type { CALL_PERMISSIONS_KEY } from "../4-querysub/permissionsShared";
18
+ import path from "path";
19
+ import { isEmpty } from "../misc";
20
+ import { getSpecFromModule } from "./pathFunctionLoader";
21
+ import { isNode } from "typesafecss";
22
+ import { LOCAL_DOMAIN } from "../0-path-value-core/PathRouter";
23
+ import { createRoutingOverrideKey } from "../0-path-value-core/PathRouterRouteOverride";
24
+
25
+ // This is the the function id which should be used when creating the FunctionSpec (in order to load the module),
26
+ // to access the permissions in the schema.
27
+ export const PERMISSIONS_FUNCTION_ID = "PERMISSIONS_FUNCTION_ID";
28
+
29
+ const SCHEMA_EXPORT_KEY = "schema-00c87ab5-10bb-4795-8f6b-e187a7e45841";
30
+ export function getSchemaObject(module: NodeJS.Module): SchemaObject | undefined {
31
+ return module.exports[SCHEMA_EXPORT_KEY];
32
+ }
33
+
34
+ export function getExportPath(functionId: string): string {
35
+ return getPathStr([SCHEMA_EXPORT_KEY, "rawFunctions", functionId]);
36
+ }
37
+
38
+ export type FunctionMetadata<Schema = unknown, F = unknown> = {
39
+ nopredict?: boolean;
40
+ /** Removes unnecessary function calls by first delaying by Querysub.DELAY_COMMIT_DELAY, Then looking at their predictions and removing ones where their predictions completely clobber the other ones.
41
+ * - Determines unnecessary calls via our function prediction. If our function is prediction is wrong, we might remove calls that shouldn't be removed.
42
+ * - This is quite dangerous, but extremely useful for things such as editors when you want to type and have things update quickly, but you don't want to spam the server with calls.
43
+ * - Only applied for clients.
44
+ * - We will always preserve the order of function calls in the same schema, so if you make another function call that doesn't have delay commit in the same schema, then we will forcefully commit all the delayed commit functions first.
45
+ * - This shouldn't cause an excessive amount of function commits. The delayed commit time is very small, maybe around five seconds. And if you are regularly having commits that are faster than that, that aren't delayed commit, then you're already having a lot of commits. And we will still remove unnecessary calls, even if we have to commit them earlier than the delay.
46
+ * - IMPORTANT! Our check for if values should be removed isn't perfect. If you write to a path that another value reads from, and then it writes that value to a path that we then read from... This would mean our value can't be clobbered as it has side effects which would affect values that could regularly clobber us. However, we don't detect this case, and we will just allow it to be clobbered.
47
+ * - However, when we clobber values, we will cancel them client-side fairly quickly, so it should be apparent if this bug happens, in which case the writes you're doing are too complicated and you shouldn't be using delay commit. Delay commit is only really intended for simple cases, such as the user presses a key and then you set some text equal to a result based on that key (It doesn't work with deltas, You need to be forcefully setting the value).
48
+ */
49
+ delayCommit?: boolean;
50
+
51
+ /** By default, we try to finish calls in the start order when using Querysub.commitAsync, unless the caller explicitly says not to. However, if this is set, then we won't have this call finish or cause other calls to be finished in the start order. This makes locking less safe, but can be useful for long-running functions that shouldn't block other functions and which the order doesn't matter. */
52
+ noFinishInStartOrder?: boolean;
53
+
54
+ /** Too many locks can lag the server, and eventually cause crashes. Consider using Querysub.noLocks(() => ...) around code that is accessing too many values, assuming you don't want to lock them. However, if absolutely required, you can override max locks to allow as many locks to be created as you want until the server crashses... */
55
+ maxLocksOverride?: number;
56
+
57
+ // NOTE: Eventually we might want to support dynamic paths per call, so we can supported nested lookups.
58
+ // - We also need to support specifying on the lookup that the key is in the nested lookup, which means hashing has to support wildcards, etc. All-in-all the whole change is very complicated, and only needed for hypothetical cases (data().worlds[region][id]...), which could probably be flattened anyways to just a single lookup (data().worlds[region + "-" + id]...)
59
+ /** This should identify the most common lookup accessed, and then give the path to that lookup and the key that we're using to access it. */
60
+ keyOverride?: {
61
+ getPrefix: (data: () => Schema) => unknown;
62
+ getKey: (...args: Args<F>) => string;
63
+ };
64
+ };
65
+
66
+ export interface SchemaObject<Schema = any, Functions = any> {
67
+ data: () => Schema;
68
+ domainName: string;
69
+ moduleId: string;
70
+ functions: {
71
+ [key in keyof Functions]: (
72
+ //Functions[key] &
73
+ // NOTE: Functions don't have results, if you want a result write to the database
74
+ // and then watch it for the result.
75
+ // - The promise resolves once the function has started the commit process, NOT when
76
+ // it has finished. Once it resolves you can terminate your process and the function
77
+ // will run (but it might still error out due to taking too long to reach the server,
78
+ // or too long to run, or insufficient privileges, etc, etc).
79
+ ((...args: Args<Functions[key]>) => CallSpec) & {
80
+ // Add a "debugger;" when the holding function is called, to make it easy to debug the function
81
+ debug(): void;
82
+ }
83
+ )
84
+ };
85
+ functionMetadata: {
86
+ [key in keyof Functions]?: FunctionMetadata<Schema, Functions[key]> | undefined;
87
+ };
88
+ rawFunctions: Functions;
89
+ permissions: SchemaToPermissions<Schema> | undefined;
90
+ permissionsNonWildcards: Map<string, PermissionsCallback>;
91
+ // Sorted by most nested first
92
+ permissionsWildcards: {
93
+ pathParts: string[];
94
+ callback: PermissionsCallback;
95
+ }[];
96
+ }
97
+
98
+ let existsCached = cache((path: string) => {
99
+ return fs.existsSync(path);
100
+ });
101
+
102
+
103
+ export function getModuleRelativePath(module: NodeJS.Module): string {
104
+ let root = path.resolve(".").replaceAll("\\", "/");
105
+ if (!existsCached(root + "/package.json")) {
106
+ throw new Error(`Expected to find package.json at ${root}. The current directory must be the root of the project when deploying, so we can resolve relative file references.`);
107
+ }
108
+ // Get the relative path
109
+ let relativePath = path.relative(root, module.filename).replaceAll("\\", "/");
110
+ if (relativePath.startsWith("../")) {
111
+ let otherFolder = relativePath.split("/")[1];
112
+ if (existsCached("node_modules/" + otherFolder)) {
113
+ relativePath = "node_modules/" + otherFolder + "/" + relativePath.split("/").slice(2).join("/");
114
+ }
115
+ }
116
+ return "/" + relativePath;
117
+ }
118
+
119
+ export type PermissionsParameters = {
120
+ // Each part of the path that matched with a wildcard ("*") is added to this array (in order).
121
+ pathWildcards: string[];
122
+ /**
123
+ * The path the permissions check matched.
124
+ * NOTE: The full path matched is not provided for optimization purposes. Otherwise for large
125
+ * writes the permissions check would be needlessly run. IF further depth is required,
126
+ * use "*" wildcards. If the depth is dynamic... your structure is probably too complex,
127
+ * or something similar to a tree. Destructure it to a list, and all of your
128
+ * code will run significantly faster.
129
+ *
130
+ * TODO: If we ABSOLUTELY need it, we COULD conditionally expose fullPath, and just disable
131
+ * optimizations when we do. We could even have a parameter which replaces matchedPath with
132
+ * fullPath. But, ideally we don't, as it is just very inefficient...
133
+ * NOTE: Usually pathWildcards should be used instead. This is only useful if you want to create
134
+ * a very generic permission check which can be used in many hierarchies, and depends on the
135
+ * hierarchy (ex, if it checks path.slice(0, 3).users for the users table).
136
+ **/
137
+ matchedPath: string;
138
+ /** The callerMachineId will be unique to the calling machine (but a machine may have multiple,
139
+ ex, 1 per browser, another a fresh one if they are in incognito mode).
140
+ - To tie machineIds to userIds simple create a lookup in your database, for example:
141
+ `{ machineToUserId: { [machineId: string]: string | undefined } }`
142
+ Logging out from a machine is simply:
143
+ `machineToUserId[machineId] = undefined`
144
+ A reverse index may also be useful, to efficiently log out everywhere (or just
145
+ show all machines that a user is logged into)
146
+ ```
147
+ { userIdToMachineIds: { [userId: string]: { [machineId: string]: 1 } } }
148
+ funcion logoutEverywhere(userId: string) {
149
+ for (let machineId in userIdToMachineIds[userId]) {
150
+ userIdToMachineIds[userId][machineId] = undefined;
151
+ machineToUserId[machineId] = undefined;
152
+ }
153
+ }
154
+ ```
155
+ * */
156
+ callerMachineId: string;
157
+
158
+ // IMPORTANT! DO NOT add "matchedValue" here. It is convenient, BUT, it is better for the user
159
+ // to access it themselves using pathWildcards. This makes it more typesafe, allowing
160
+ // them to find all references and find that reference (otherwise the reference will be hidden).
161
+ // - Also, I don't think we could make matchedValue typesafe at all, so it would have to be any,
162
+ // which is terrible.
163
+ };
164
+ /** A false of false will deny read permissions, resulting in all reads being given value with a value
165
+ * of undefined, and a time of 0.
166
+
167
+ */
168
+ export type PermissionsCallback = (config: PermissionsParameters) => PermissionsCheckResult;
169
+ export type PermissionsCheckResult = boolean | { allowed: boolean; skipParentChecks?: boolean; };
170
+ /*
171
+ NOTE: All ancestor permissions checks are applied as well.
172
+ - This means if the ancestor disallows, it blocks any descendant checks even if they are allowed.
173
+
174
+ NOTE: If no permissions match, then access is disallowed by default.
175
+
176
+ NOTE: Permissions checks at the root (permissions: { READ_PERMISSIONS(config) { } }) will
177
+ be used to verify calls are allowed as well, even if the call doesn't read or write any data
178
+ that is under a permissions check.
179
+ - THIS MEANS that if there is no permission check at the root, calls won't be allowed by any user
180
+ (as we default to disallow).
181
+
182
+ NOTE: Servers watches ignore permissions, as they directly communicate with the underlying PathValueServer,
183
+ skipping the ProxyServer.
184
+ - However function calls by servers are still run by FunctionRunner, which will apply permission checks.
185
+
186
+ IMPORTANT! Wildcards are tested via the "" path. This means if the "" key is provided access to a user, they can run
187
+ Object.keys()/Object.values() on the data. In order to not accidentally provide list access, never allow "" to be
188
+ a valid path, by never providing access by exclusion. For example:
189
+ BROKEN: `{ ["*"]: { PERMISSIONS(){ return config.pathWildcards?.[0] !== "admin" || users()[config.callerId].isAdmin; } } }`
190
+ - This will allow users to access the "admin" data via Object.values(), because the "" !== "admin"
191
+ BROKEN: `{ admin: { PERMISSIONS(){ return users()[config.callerId].isAdmin; } } }`
192
+ - Because "" isn't checked
193
+ CORRECT: `{ ["*"]: { PERMISSIONS(){ return config.pathWildcards?.[0] === config.callerId; } } }`
194
+ CORRECT: `{ ["*"]: { PERMISSIONS(){ return users()[config.callerId].isAdmin || config.pathWildcards?.[0] === config.callerId; } } }`
195
+
196
+ NOTE: Permission checks lag a bit behind data. This means not only will a write that removes checks of itself
197
+ still be sent (ex, if a user removes their admin access, and only admins can see who is an admin or not,
198
+ they will be told about their admin change). BUT, a user will receive writes for a bit after that as well!
199
+ The delay is equal to the network propagation time between two server, which is small (ideally milliseconds),
200
+ but in the case of excess lag could easily be 10s of seconds (in which case everything on the site would
201
+ be lagging by 10s of seconds as well).
202
+
203
+ For example
204
+ permissions: {
205
+ userSecretData: {
206
+ ["*"]: {
207
+ PERMISSIONS(pathWildcards) {
208
+ return config.pathWildcards[0] === config.callerMachineId;
209
+ }
210
+ }
211
+ }
212
+ }
213
+ */
214
+ export type SchemaToPermissions<Schema> = {
215
+ [key in keyof Schema]?: SchemaToPermissions<Schema[key]>;
216
+ } & {
217
+ PERMISSIONS?: PermissionsCallback;
218
+ } | {
219
+ PERMISSIONS?: PermissionsCallback;
220
+ };
221
+
222
+ /** A special key which is prefixed (along with a random value) before special keys used to check child
223
+ * permissions. This allows us to explicitly disallow parent syncing (ex Object.keys()) of values.
224
+ */
225
+ export const CHILD_CHECK_PREFIX = "";
226
+
227
+ export function hasWildcardMatch(permissionsPath: string[], valuePathParts: string[]): boolean {
228
+ for (let i = 0; i < permissionsPath.length; i++) {
229
+ let part = permissionsPath[i];
230
+ if (part === "*") {
231
+
232
+ } else if (part !== valuePathParts[i]) {
233
+ return false;
234
+ }
235
+ }
236
+ return true;
237
+ }
238
+ export function getWildcardMatches(permissionsPath: string[], valuePathParts: string[]): string[] | undefined {
239
+ if (permissionsPath.length > valuePathParts.length) return undefined;
240
+ let wildcardMatches: string[] | undefined;
241
+ for (let i = 0; i < permissionsPath.length; i++) {
242
+ let part = permissionsPath[i];
243
+ if (part === "*") {
244
+ if (!wildcardMatches) wildcardMatches = [];
245
+ wildcardMatches.push(valuePathParts[i]);
246
+ } else if (part !== valuePathParts[i]) {
247
+ return undefined;
248
+ }
249
+ }
250
+ return wildcardMatches;
251
+ }
252
+
253
+ let inliningCalls = false;
254
+ export function inlineNestedCalls<T>(code: () => T) {
255
+ let prev = inliningCalls;
256
+ inliningCalls = true;
257
+ try {
258
+ return code();
259
+ } finally {
260
+ inliningCalls = prev;
261
+ }
262
+ }
263
+
264
+ type SyncSchemaResult<Schema> = {
265
+ // NOTE: The functions are over the network, AND, eventually consistent, so it is not possible
266
+ // for them to return a result. If a result is required, pass an id which will instruct them where
267
+ // to write their result to, and then watch that location.
268
+ <Functions extends { [name: string]: ((...args: any) => void) }>(config: {
269
+ domainName?: string;
270
+ /** NOTE: moduleId is used to store your data in a unique location, that doesn't change
271
+ * if you move this file around. Any unique identifier is allowed, as the deploy command
272
+ * will warn you if you accidentally reused a moduleId.
273
+ */
274
+ moduleId: string;
275
+ module: NodeJS.Module;
276
+ functions: Functions;
277
+ functionMetadata?: { [name in keyof Functions]?: FunctionMetadata<Schema, Functions[name]> };
278
+ /** Provide access to [Querysub.CALL_PERMISSIONS_KEY] to allow function calls (or provide access at the root). */
279
+ permissions?: SchemaToPermissions<Schema> & {
280
+ [CALL_PERMISSIONS_KEY]?: {
281
+ PERMISSIONS: PermissionsCallback;
282
+ };
283
+ };
284
+ }): SchemaObject<Schema, Functions>;
285
+ };
286
+
287
+ let prefixes: string[] = [];
288
+ // ONLY used by the deploy process. Path value server is pretty much the only one who needs this, and they aren't including client-side code, and they definitely aren't updating client-side code, so it doesn't make sense to use this directly.
289
+ export function getPrefixesForDeploy(): string[] {
290
+ return prefixes;
291
+ }
292
+ // Used for some old non syncSchema schema cases
293
+ export function addRoutingPrefixForDeploy(prefix: string) {
294
+ prefixes.push(prefix);
295
+ }
296
+
297
+ export function syncSchema<Schema>(): SyncSchemaResult<Schema>;
298
+ export function syncSchema<SchemaDef extends Schema2>(schema: SchemaDef): SyncSchemaResult<Schema2T<SchemaDef>>;
299
+ export function syncSchema<Schema>(schema?: Schema2): SyncSchemaResult<Schema> {
300
+ return function getFunctions(config) {
301
+ const { moduleId, functions, module, permissions } = config;
302
+ const domainName = config.domainName ?? getDomain();
303
+
304
+ const data = () => functionSchema()[domainName].PathFunctionRunner[moduleId].Data;
305
+ let dataCached = lazy(() => data()) as () => Schema;
306
+
307
+ let fncs = { ...functions };
308
+
309
+ let undoChecksAdded: {
310
+ name: string;
311
+ path: SchemaPath;
312
+ }[] = [];
313
+ if (schema) {
314
+ let dataPrefixPath = getPathFromStr(getProxyPath(data));
315
+ let fnc = getSpecFromModule(module);
316
+ // NOTE: We call syncSchema a lot, so we default the git hash. However, for modules that provide
317
+ // functions we call as synced functions, we SHOULD have a real hash here.
318
+ let gitHash = fnc?.gitRef || "ambient";
319
+ registerSchema({ domainName, moduleId, schema, gitHash });
320
+
321
+ let gcDefs = Schema2Fncs.getGCObjects(schema);
322
+ for (let { path, gcDelay } of gcDefs) {
323
+ createObjectAliveChecker(data, path, gcDelay);
324
+ }
325
+
326
+
327
+ let undoChecks = Schema2Fncs.getAllUndoChecks(schema);
328
+ for (let checkObj of undoChecks) {
329
+ // Need at least one check
330
+ if (checkObj.undoChecks.length === 0) continue;
331
+ function niceSchemaPath(path: SchemaPath) {
332
+ return path.map(x => x === Schema2Fncs.WildCard ? "*" : x).join(".");
333
+ }
334
+ let name = `__undoer__${niceSchemaPath(checkObj.path)}`;
335
+ let fullPath = (dataPrefixPath as SchemaPath).concat(checkObj.path);
336
+ undoChecksAdded.push({
337
+ name,
338
+ path: fullPath,
339
+ });
340
+ (fncs as any)[name] = function undoCallback(path: string[], value: unknown) {
341
+ if (!isChildPath(fullPath, path)) {
342
+ throw new Error(`Tried to access undo function with path not under undo spec ${niceSchemaPath(fullPath)} . Tried to use path ${path.join(".")}`);
343
+ }
344
+
345
+ let permissionsChecker = proxyWatcher.getTriggeredWatcher().permissionsChecker;
346
+ if (permissionsChecker) {
347
+ let permissionsCheck = permissionsChecker.checkPermissions(getPathStr(path));
348
+ if (!permissionsCheck?.allowed) {
349
+ throw new Error(`Failed read permission check for undo call at path ${path.join(".")}`);
350
+ }
351
+ }
352
+ for (let check of checkObj.undoChecks) {
353
+ let nestedPath = path.slice(0, fullPath.length);
354
+ let key = nestedPath[nestedPath.length - 1];
355
+ if (!atomic(check(key, nestedPath))) {
356
+ throw new Error(`Failed undo permission check at path ${nestedPath.join(".")}, check ${check.name || check.toString().slice(0, 100)}`);
357
+ }
358
+ }
359
+ let parent = rawSchema()() as any;
360
+ for (let i = 0; i < path.length - 1; i++) {
361
+ parent = parent[path[i]];
362
+ }
363
+ let key = path[path.length - 1];
364
+
365
+ // NOTE: This isn't an atomic set, because we leave it up proxyWatcher (and schema2)
366
+ // to correctly handle assigning the values atomically.
367
+ parent[key] = value;
368
+ };
369
+ }
370
+
371
+ // Register all root lookups as prefixes.
372
+ let dataRoot = getProxyPath(() => functionSchema()[getDomain()].PathFunctionRunner[moduleId].Data);
373
+ let paths = Schema2Fncs.getLookups(schema).map(x =>
374
+ joinPathStres(dataRoot, getPathStr(x))
375
+ );
376
+ // NOTE: This will break with wildcards, but... we also take only the highest level lookups, so it shouldn't be an issue
377
+ sort(paths, x => x.length);
378
+ let rootLookups = new Set<string>();
379
+ for (let path of paths) {
380
+ let parts = getPathFromStr(path);
381
+ // Don't add if any of our ancestors are added
382
+ function hasAncestor() {
383
+ for (let i = 0; i < parts.length - 1; i++) {
384
+ let ancestorPath = getPathStr(parts.slice(0, i));
385
+ if (rootLookups.has(ancestorPath)) return true;
386
+ }
387
+ }
388
+ if (!hasAncestor()) {
389
+ rootLookups.add(path);
390
+ prefixes.push(path);
391
+ }
392
+ }
393
+ }
394
+
395
+ let wrappedFunctions: { [name: string]: (...args: any) => CallSpec } = Object.create(null);
396
+ let functionMetadata = config.functionMetadata;
397
+ for (let [name, fnc] of Object.entries(fncs)) {
398
+ let metadata = functionMetadata?.[name] ?? {};
399
+ let debugging = false;
400
+ wrappedFunctions[name] = Object.assign((...args: any[]): any => {
401
+ if (inliningCalls) {
402
+ return fnc(...args);
403
+ }
404
+ if (debugging) {
405
+ debugger;
406
+ }
407
+ return writeFunctionCall({
408
+ domainName,
409
+ moduleId,
410
+ functionId: name,
411
+ args,
412
+ metadata: metadata as FunctionMetadata
413
+ });
414
+ }, {
415
+ debug() {
416
+ debugging = true;
417
+ }
418
+ });
419
+ if (metadata.keyOverride) {
420
+ let { getPrefix, getKey } = metadata.keyOverride;
421
+ let prefix = getProxyPath(() => getPrefix(dataCached));
422
+ if (!prefix) throw new Error(`Key override getPrefix returned a non-proxy, ${getPrefix.toString()}`);
423
+ let objs = callIdOverrideFncs.get(config.moduleId);
424
+ if (!objs) {
425
+ objs = new Map();
426
+ callIdOverrideFncs.set(config.moduleId, objs);
427
+ }
428
+ objs.set(name, {
429
+ prefix,
430
+ getKey: getKey as any,
431
+ });
432
+ }
433
+ }
434
+
435
+ for (let check of undoChecksAdded) {
436
+ registeredUndoChecks.push({
437
+ path: check.path,
438
+ undoSet: wrappedFunctions[check.name] as any,
439
+ });
440
+ }
441
+
442
+ let permissionsNonWildcards = new Map<string, PermissionsCallback>();
443
+ let permissionsWildcards: { pathParts: string[]; callback: PermissionsCallback }[] = [];
444
+ addPermissions(rootPathStr, permissions);
445
+ function addPermissions(path: string, permissions: SchemaToPermissions<Schema> | undefined) {
446
+ if (!permissions) return;
447
+ for (let key of getStringKeys(permissions)) {
448
+ if (key === "PERMISSIONS") {
449
+ let value = permissions.PERMISSIONS;
450
+ if (value) {
451
+ if (path.includes("*")) {
452
+ permissionsWildcards.push({
453
+ pathParts: getPathFromStr(path),
454
+ callback: value,
455
+ });
456
+ } else {
457
+ permissionsNonWildcards.set(path, value);
458
+ }
459
+ }
460
+ } else {
461
+ addPermissions(appendToPathStr(path, key), permissions[key]);
462
+ }
463
+ }
464
+ }
465
+ // Sort reversed, so the most specific checks are first
466
+ permissionsWildcards.sort((b, a) => a.pathParts.length - b.pathParts.length);
467
+
468
+ // NOTE: We cache the root with lazy, to save time evaluating each proxy on each access. Also, it reduces the extra paths accesses that will never have data.
469
+ // - Permissions should be fine, as we check them directly (if the user wants a path we check it), so we don't throw on accessing the root data and then allow all child accesses (each path is checked independently).
470
+ let result: SchemaObject = {
471
+ data: dataCached,
472
+ domainName,
473
+ moduleId,
474
+ functions: wrappedFunctions as any,
475
+ functionMetadata: functionMetadata as any,
476
+ rawFunctions: fncs,
477
+ permissions,
478
+ permissionsNonWildcards,
479
+ permissionsWildcards,
480
+ };
481
+
482
+ if (!isHotReloading() && module.exports[SCHEMA_EXPORT_KEY]) {
483
+ throw new Error(`Module ${moduleId} already has a schema. Only 1 schema is allowed per module.`);
484
+ }
485
+
486
+ // NOTE: We clobber the previous value, as we might just be hotreloading, which
487
+ // could mean the exports value has our previous value.
488
+ module.exports[SCHEMA_EXPORT_KEY] = result;
489
+ developmentModules.set(moduleId, module);
490
+ return result;
491
+ };
492
+ }
493
+ let developmentModules = new Map<string, NodeJS.Module>();
494
+
495
+ export function getDevelopmentModule(moduleId: string): NodeJS.Module | undefined {
496
+ return developmentModules.get(moduleId);
497
+ }
498
+ export function getAllDevelopmentModulesIds(): string[] {
499
+ return Array.from(developmentModules.keys());
500
+ }
501
+
502
+ function isChildPath(spec: SchemaPath, path: string[]): boolean {
503
+ if (spec.length > path.length) return false;
504
+ for (let i = 0; i < spec.length; i++) {
505
+ if (spec[i] === Schema2Fncs.WildCard) continue;
506
+ if (spec[i] !== path[i]) return false;
507
+ }
508
+ return true;
509
+ }
510
+
511
+ // pathStr => ...
512
+ let registeredUndoChecks: {
513
+ path: SchemaPath;
514
+ undoSet: (nestedPath: string[], value: unknown) => void;
515
+ }[] = [];
516
+
517
+ export function registerUndoCallback(path: SchemaPath, undoSet: (nestedPath: string[], value: unknown) => void) {
518
+ registeredUndoChecks.push({ path, undoSet });
519
+ }
520
+
521
+ /** Throws if there is no undo callback available at this path. */
522
+ export function getUndoCallback(path: string[]): {
523
+ setValue(path: string[], value: unknown): void;
524
+ } {
525
+ // Local undos are always allowed
526
+ if (path[0] === LOCAL_DOMAIN) {
527
+ return {
528
+ setValue(path, value) {
529
+ if (path[0] !== LOCAL_DOMAIN) throw new Error(`Cannot set non-local value with local undo function. Tried to use path ${path.join(".")}`);
530
+ let parent = rawSchema()() as any;
531
+ for (let i = 0; i < path.length - 1; i++) {
532
+ parent = parent[path[i]];
533
+ }
534
+ let key = path[path.length - 1];
535
+ parent[key] = value;
536
+ }
537
+ };
538
+ }
539
+ // Any callback should be fine?
540
+ let undoCallbacks = registeredUndoChecks.filter(x => isChildPath(x.path, path));
541
+ sort(undoCallbacks, x => -x.path.length);
542
+ if (undoCallbacks.length === 0) {
543
+ throw new Error(`No undo callback available at path ${path.join(".")}. There needs to be an undo region at or above this path, via t.undoRegion`);
544
+ }
545
+ return { setValue: undoCallbacks[0].undoSet };
546
+ }
547
+
548
+ function createObjectAliveChecker(data: () => any, path: SchemaPath, delay: number | undefined) {
549
+ registerAliveChecker({
550
+ object(wildcard) {
551
+ let obj = data();
552
+ for (let part of path) {
553
+ if (part === Schema2Fncs.WildCard) {
554
+ obj = obj[wildcard];
555
+ } else {
556
+ obj = obj[part];
557
+ }
558
+ }
559
+ return obj;
560
+ },
561
+ isAlive(value, path) {
562
+ let parent = rawSchema()() as any;
563
+ for (let i = 0; i < path.length - 1; i++) {
564
+ parent = parent[path[i]];
565
+ }
566
+ let key = path[path.length - 1];
567
+ let isAlive = key in parent;
568
+ return isAlive;
569
+ },
570
+ deleteDelayTime: delay,
571
+ // NOTE: This is actually safe, as we only read a single value, and are really
572
+ // just looking to see if ANYTHING has created the value. Without this there is
573
+ // a large gap when the value could be deleted, then undeleted, and then
574
+ // as we only read old values we might not see the undelete. WITH this the gap
575
+ // is much smaller (but still there).
576
+ unsafeLiveReads: true,
577
+ });
578
+ }
579
+
580
+ let callIdOverrideFncs: Map<string, Map<string, {
581
+ prefix: string;
582
+ getKey: (...args: unknown[]) => string;
583
+ }>> = new Map();
584
+ export function getCallIdOverride(config: {
585
+ moduleId: string;
586
+ functionId: string;
587
+ callId: string;
588
+ args: unknown[];
589
+ }): string {
590
+ let def = callIdOverrideFncs.get(config.moduleId)?.get(config.functionId);
591
+ if (!def) return config.callId;
592
+ return createRoutingOverrideKey({
593
+ remappedPrefix: def.prefix,
594
+ originalKey: config.callId,
595
+ routeKey: def.getKey(...config.args),
596
+ });
522
597
  }