querysub 0.9.0 → 0.10.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "querysub",
3
- "version": "0.9.0",
3
+ "version": "0.10.0",
4
4
  "main": "index.js",
5
5
  "license": "MIT",
6
6
  "note1": "note on node-forge fork, see https://github.com/digitalbazaar/forge/issues/744 for details",
@@ -9,7 +9,6 @@ import debugbreak from "debugbreak";
9
9
  import cborx from "cbor-x";
10
10
  import { lazy } from "socket-function/src/caching";
11
11
  import { secureRandom } from "../misc/random";
12
- import { onCallPredict } from "../4-querysub/QuerysubController";
13
12
  import { isDefined } from "../misc";
14
13
  import { blue, green, red } from "socket-function/src/formatting/logColors";
15
14
  import { getPathStr2 } from "../path";
@@ -50,6 +49,7 @@ export async function replaceFunctions(config: {
50
49
  let currentHashes = new Set(currentFunctions.map(x => JSON.stringify(x)));
51
50
 
52
51
  let prevFunctionNames = new Set(previousFunctions.map(getFunctionName));
52
+ let currentFunctionNames = new Set(currentFunctions.map(getFunctionName));
53
53
 
54
54
  let messages: {
55
55
  text: string;
@@ -57,10 +57,10 @@ export async function replaceFunctions(config: {
57
57
  }[] = [];
58
58
 
59
59
  for (let previous of previousFunctions) {
60
- let hash = JSON.stringify(previous);
61
- if (currentHashes.has(hash)) continue;
62
- messages.push({ text: `Removing ${red(debugFunction(previous))}`, order: 3 });
63
- base[previous.ModuleId].Sources[previous.FunctionId] = undefined;
60
+ if (!currentFunctionNames.has(getFunctionName(previous))) {
61
+ messages.push({ text: `Removing ${red(debugFunction(previous))}`, order: 3 });
62
+ base[previous.ModuleId].Sources[previous.FunctionId] = undefined;
63
+ }
64
64
  }
65
65
 
66
66
  console.log();
@@ -78,9 +78,11 @@ export async function replaceFunctions(config: {
78
78
  base[func.ModuleId].Sources[func.FunctionId] = atomicObjectWrite(func);
79
79
  }
80
80
 
81
- sort(messages, x => -x.order);
82
- for (let message of messages) {
83
- console.log(message.text);
81
+ if (proxyWatcher.isAllSynced()) {
82
+ sort(messages, x => -x.order);
83
+ for (let message of messages) {
84
+ console.log(message.text);
85
+ }
84
86
  }
85
87
  }
86
88
  });
@@ -16,6 +16,8 @@ import yargs from "yargs";
16
16
  import { isPublic } from "../config";
17
17
  import { deployBlock } from "./deployBlock";
18
18
  import { LOCAL_DOMAIN } from "../0-path-value-core/PathController";
19
+ import { preloadFunctions } from "./pathFunctionLoader";
20
+ import { magenta } from "socket-function/src/formatting/logColors";
19
21
  let yargObj = yargs(process.argv)
20
22
  .option("domain", { type: "string", required: true, desc: "Domain to deploy to" })
21
23
  .option("nogit", { type: "boolean", default: false, desc: `Disable git operations. Useful for debugging, when using --local` })
@@ -107,7 +109,17 @@ export async function deployMain() {
107
109
  }
108
110
  }
109
111
 
112
+ console.log();
113
+ console.log();
114
+ console.log(magenta(`Preloading ${currentFunctions.length} functions`));
115
+ await preloadFunctions(currentFunctions);
116
+
117
+ console.log();
118
+ console.log();
119
+ console.log(magenta(`Deploying ${currentFunctions.length} functions`));
110
120
  await replaceFunctions({ domainName, functions: currentFunctions, });
121
+
122
+ console.log(magenta(`FINISHED DEPLOY`));
111
123
  }
112
124
  function getGitURL(gitDir: string) {
113
125
  if (yargObj.nogit) return "git@github.com:nogit/nogit.git";
@@ -5,19 +5,22 @@ import fs from "fs";
5
5
  import { blue, magenta, red } from "socket-function/src/formatting/logColors";
6
6
  import debugbreak from "debugbreak";
7
7
  import { cache, lazy } from "socket-function/src/caching";
8
- import { batchFunction, runInfinitePoll } from "socket-function/src/batching";
9
- import { logErrors } from "../errors";
8
+ import { batchFunction, delay, runInfinitePoll } from "socket-function/src/batching";
9
+ import { errorToUndefined, logErrors } from "../errors";
10
10
  import crypto from "crypto";
11
11
  import { MaybePromise } from "socket-function/src/types";
12
12
  import yargs from "yargs";
13
13
  import { SyncWatcher } from "../2-proxy/PathValueProxyWatcher";
14
14
  import { MAX_CHANGE_AGE } from "../0-path-value-core/pathValueCore";
15
- import { isNode, isNodeTrue } from "socket-function/src/misc";
15
+ import { isNode, isNodeTrue, nextId, timeInSecond } from "socket-function/src/misc";
16
16
  import { getPathStr2, getPathStr3 } from "../path";
17
17
  import { consistentHash } from "../misc/hash";
18
18
  import { setExternalHotReloading } from "socket-function/hot/HotReloadController";
19
19
  import { isPublic } from "../config";
20
20
  import { SocketFunction } from "socket-function/SocketFunction";
21
+ import { requiresNetworkTrustHook } from "../-d-trust/NetworkTrust2";
22
+ import { getControllerNodeId, getControllerNodeIdList } from "../-g-core-values/NodeCapabilities";
23
+ import { sha256 } from "js-sha256";
21
24
 
22
25
  // Get localPathRemappings using yargs, so it is easy to configure in multiple entry points
23
26
  let yargObj = isNodeTrue() && yargs(process.argv)
@@ -138,13 +141,31 @@ let moduleResolver = async (spec: FunctionSpec) => {
138
141
  urlForPath = urlForPath.slice(0, -".git".length);
139
142
  }
140
143
  let repoPath = getSubFolder("synced_repos") + urlForPath + "/" + spec.gitRef + "/";
141
- if (!fs.existsSync(repoPath)) {
142
- await executeCommand("git", ["clone", gitURL, repoPath]);
143
- await executeCommand("git", ["reset", "--hard", spec.gitRef], { cwd: repoPath });
144
+
145
+ const lockFolder = getSubFolder("synced_repos_locks");
146
+ let lockPath = lockFolder + sha256(repoPath).slice(0, 16) + ".loadinglock";
147
+ let exists = fs.existsSync(repoPath);
148
+ if (exists) {
149
+ if (!await isRepoCloned(repoPath)) {
150
+ exists = false;
151
+ }
144
152
  }
145
- if (!fs.existsSync(repoPath + "node_modules")) {
146
- await executeCommand("yarn", ["install"], { cwd: repoPath });
153
+ if (!exists) {
154
+ await getFileLock(lockPath, async () => {
155
+ if (fs.existsSync(repoPath) && !await isRepoCloned(repoPath)) {
156
+ // Just move it, and try again
157
+ await fs.promises.rename(repoPath, repoPath + "_" + Date.now());
158
+ }
159
+ if (!fs.existsSync(repoPath)) {
160
+ await executeCommand("git", ["clone", gitURL, repoPath]);
161
+ await executeCommand("git", ["reset", "--hard", spec.gitRef], { cwd: repoPath });
162
+ }
163
+ if (!fs.existsSync(repoPath + "node_modules")) {
164
+ await executeCommand("yarn", ["install"], { cwd: repoPath });
165
+ }
166
+ });
147
167
  }
168
+
148
169
  let querysubPath = repoPath + "node_modules/querysub";
149
170
  if (fs.existsSync(querysubPath)) {
150
171
  // By moving querysub, it forces the repo to use the parent querysub. This is nice, as it even
@@ -157,6 +178,57 @@ let moduleResolver = async (spec: FunctionSpec) => {
157
178
  return repoPath;
158
179
  };
159
180
 
181
+ const lockHeartbeatInterval = timeInSecond * 15;
182
+ const lockExpiryTime = timeInSecond * 60;
183
+ const lockCheckTime = 500;
184
+ async function tryGetFileLock(file: string) {
185
+ let expiryTime = Date.now() - lockExpiryTime;
186
+ try {
187
+ let lastHeartbeat = Number((await fs.promises.readFile(file, "utf8")).split("-")[0]) || 0;
188
+ if (lastHeartbeat > expiryTime) return false;
189
+ } catch { }
190
+ let heartbeatWrite = Date.now() + "-" + nextId();
191
+ await fs.promises.writeFile(file, heartbeatWrite);
192
+ await delay(lockCheckTime);
193
+ let readBack = await fs.promises.readFile(file, "utf8");
194
+ if (readBack !== heartbeatWrite) return false;
195
+ return true;
196
+ }
197
+ async function getFileLock(file: string, callback: () => Promise<void>) {
198
+ console.log(magenta(`Getting file lock: ${file}`));
199
+ while (true) {
200
+ if (await tryGetFileLock(file)) {
201
+ let done = false;
202
+ logErrors((async () => {
203
+ while (!done) {
204
+ await delay(lockHeartbeatInterval);
205
+ console.log(magenta(`Not done with file lock, writing heartbeat: ${file}`));
206
+ await fs.promises.writeFile(file, Date.now() + "-" + nextId());
207
+ }
208
+ })());
209
+ try {
210
+ await callback();
211
+ } finally {
212
+ await fs.promises.unlink(file);
213
+ done = true;
214
+ console.log(magenta(`Releasing file lock: ${file}`));
215
+ }
216
+ return;
217
+ }
218
+ console.log(magenta(`Waiting for lock file lock: ${file}`));
219
+ await delay(lockHeartbeatInterval);
220
+ }
221
+ }
222
+
223
+ async function isRepoCloned(path: string): Promise<boolean> {
224
+ try {
225
+ await executeCommand("git", ["-C", path, "rev-parse", "--is-inside-work-tree"]);
226
+ return true;
227
+ } catch {
228
+ return false;
229
+ }
230
+ }
231
+
160
232
  export function isDynamicModule(module: NodeJS.Module): boolean {
161
233
  return isDynamicModulePath(module.filename);
162
234
  }
@@ -185,6 +257,7 @@ function getCallstackFiles(): string[] {
185
257
  async function getModuleFromSpecBase(
186
258
  spec: FunctionSpec
187
259
  ): Promise<NodeJS.Module> {
260
+ everLoadedRepos.add(spec.gitURL);
188
261
  let hotReloadPackagePath = "";
189
262
  let path = gitURLRefMappings.get(getSpecKey(spec));
190
263
  let deployPath = "";
@@ -333,4 +406,39 @@ async function executeCommand(command: string, args: string[], options?: {
333
406
  stdio: "inherit",
334
407
  cwd: options?.cwd
335
408
  });
336
- }
409
+ }
410
+
411
+
412
+ // Helps us exclude non-function loaders (PathValueServer), which really shouldn't
413
+ // be loading code anyways!
414
+ let everLoadedRepos = new Set<string>();
415
+
416
+ export async function preloadFunctions(specs: FunctionSpec[]) {
417
+ let nodeIds = await getControllerNodeIdList(functionPreloadController);
418
+ await Promise.all(nodeIds.map(async nodeId => {
419
+ let controller = functionPreloadController.nodes[nodeId];
420
+ console.log(blue(`Preloading functions on ${nodeId}`));
421
+ await errorToUndefined(controller.preloadFunctions(specs));
422
+ console.log(blue(`Finished preloading functions on ${nodeId}`));
423
+ }));
424
+ }
425
+
426
+ class FunctionPreloaderBase {
427
+ async preloadFunctions(specs: FunctionSpec[]) {
428
+ specs = specs.filter(spec => everLoadedRepos.has(spec.gitURL));
429
+ for (let spec of specs) {
430
+ await getModuleFromSpec(spec);
431
+ }
432
+ }
433
+ }
434
+
435
+ const functionPreloadController = SocketFunction.register(
436
+ "FunctionPreloader-c966a1a6-5d3f-4453-bd03-ae84b574ec00",
437
+ new FunctionPreloaderBase(),
438
+ () => ({
439
+ preloadFunctions: {},
440
+ }),
441
+ () => ({
442
+ hooks: [requiresNetworkTrustHook],
443
+ })
444
+ );