querysub 0.130.0 → 0.131.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.130.0",
3
+ "version": "0.131.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",
@@ -1,3 +1,6 @@
1
+ import type { EdgeNodeConfig } from "../4-deploy/edgeNodes";
2
+ import type { ExtraMetadata } from "../5-diagnostics/nodeMetadata";
3
+
1
4
  // Hooks, to allow function implementations to be declared after their first call
2
5
  // (so static calls work).
3
6
 
@@ -89,7 +92,6 @@ export const onTimeProfile = createHookFunction<
89
92
  ) => void
90
93
  >("onTimeProfile");
91
94
 
92
- import type { ExtraMetadata } from "../5-diagnostics/nodeMetadata";
93
95
  export const registerNodeMetadata = createHookFunction<
94
96
  (
95
97
  metadata: ExtraMetadata
@@ -116,3 +118,16 @@ export const logNodeStateStats = createHookFunction<
116
118
  export const isManagementUser = createHookFunctionReturn<
117
119
  () => Promise<boolean>
118
120
  >("isManagementUser");
121
+
122
+
123
+
124
+ declare global {
125
+ var BOOTED_EDGE_NODE: EdgeNodeConfig | undefined;
126
+ }
127
+
128
+
129
+ export function getBootedEdgeNode(): EdgeNodeConfig {
130
+ let edgeNode = globalThis.BOOTED_EDGE_NODE;
131
+ if (!edgeNode) throw new Error(`No edge node booted? This should be impossible.`);
132
+ return edgeNode;
133
+ }
@@ -23,7 +23,7 @@ import dns from "dns/promises";
23
23
  import { isDefined } from "../misc";
24
24
  import { diskLog, noDiskLogPrefix } from "../diagnostics/logs/diskLogger";
25
25
  import { getDebuggerUrl } from "../diagnostics/listenOnDebugger";
26
- import { getBootedEdgeNode } from "../4-deploy/edgeBootstrap";
26
+ import { getBootedEdgeNode } from "../-0-hooks/hooks";
27
27
 
28
28
  let HEARTBEAT_INTERVAL = timeInMinute * 2;
29
29
  // Interval which we check other heartbeats
@@ -665,9 +665,9 @@ export async function preloadFunctions(specs: FunctionSpec[]) {
665
665
  let nodeIds = await getControllerNodeIdList(FunctionPreloadController);
666
666
  await Promise.allSettled(nodeIds.map(async nodeId => {
667
667
  let controller = FunctionPreloadController.nodes[nodeId.nodeId];
668
- console.log(blue(`Preloading functions on ${nodeId}`));
668
+ console.log(blue(`Preloading functions on ${String(nodeId)}`));
669
669
  await errorToUndefined(controller.preloadFunctions(specs));
670
- console.log(blue(`Finished preloading functions on ${nodeId}`));
670
+ console.log(blue(`Finished preloading functions on ${String(nodeId)}`));
671
671
  }));
672
672
  }
673
673
 
@@ -38,36 +38,4 @@ export function setLiveDeployedHash(config: {
38
38
  deploySchema()[getDomain()].deploy.history[nextId()] = hashObj;
39
39
  },
40
40
  });
41
- }
42
-
43
- //todonext
44
- // - Function to dynamically deploy edge service based on hash (try on old hashes)
45
- // - Watch live hash, and when it changes, deploy new edge service
46
- // - Add flag to deploy script which sets it (with all the flags, so we can just deploy it)
47
- // - Update edge node code to pass live hash, and try to connect to a service with that
48
-
49
- //todonext
50
- /*
51
- Force client refresh mechanism / update notification ON `yarn deploy`
52
- - deployonlycode / deployonlyui / deploynotifyforced
53
- - OH! Actually...
54
- - This HAS to use a synced value!
55
- - Add a new synced value which specifies the UI hash!
56
- - THEN! Have front-end servers dynamically launch this services with this hash when it's set (loading the code and then serving the specific path, with the hash, etc)
57
- - Also, serve this to clients!
58
- - Clients will watch this value (normally, like any other value), and notify the user when it is out of date, letting them ignore it, but eventually forcing a refresh
59
- - AND, support preloading it
60
- - So our order will be
61
- (git push)
62
- preload functions
63
- preload UI
64
- atomically set function hashes + ui hashes
65
- clients are notified they should refresh
66
- new clients use services with the latest hash (which servers are already hosting)
67
- functions will run this hash
68
- - Ah, also... add a flag which tells us NOT to use the service HTML cache
69
- - Remove it when used
70
- - Required on refresh, otherwise we might refresh and use an outdated service
71
-
72
- ALSO, have FunctionRunner watch the hash using the get/watch mechanism (for preloading, as it will dynamically load the code depending on what is really deployed anyways...)
73
- */
41
+ }
@@ -3,23 +3,17 @@ import { isServer } from "../config2";
3
3
  import { EdgeNodeConfig, EdgeNodesIndex } from "./edgeNodes";
4
4
  import { timeInMinute, timeInSecond } from "socket-function/src/misc";
5
5
  import { measureBlock } from "socket-function/src/profiling/measure";
6
+ import { URLParam } from "../library-components/URLParam";
6
7
 
7
8
  module.hotreload = true;
8
9
  module.noserverhotreload = false;
9
10
 
11
+ const liveHashOverrideParam = "liveHashOverride";
12
+ export const liveHashOverrideURL = new URLParam(liveHashOverrideParam, undefined as undefined | {
13
+ liveHash: string;
14
+ time: number;
15
+ });
10
16
 
11
- declare global {
12
- var BOOTED_EDGE_NODE: EdgeNodeConfig | undefined;
13
- }
14
-
15
-
16
- export function getBootedEdgeNode(): EdgeNodeConfig {
17
- if (isServer()) throw new Error(`getBootedEdgeNode is not available on the server`);
18
-
19
- let edgeNode = globalThis.BOOTED_EDGE_NODE;
20
- if (!edgeNode) throw new Error(`No edge node booted? This should be impossible.`);
21
- return edgeNode;
22
- }
23
17
 
24
18
  let getCachedConfig = cache(async (url: string): Promise<EdgeNodesIndex | undefined> => {
25
19
  setTimeout(() => {
@@ -217,6 +211,26 @@ async function edgeNodeFunction(config: {
217
211
 
218
212
  let cachedConfig = config.cachedConfig;
219
213
 
214
+ let liveHashOverride = "";
215
+ let liveHashOverrideExpiryTime = 0;
216
+
217
+ const liveHashOverrideParam2: typeof liveHashOverrideParam = "liveHashOverride";
218
+ let searchParams = new URLSearchParams(document.location.search);
219
+ let liveHashOverrideObj = searchParams.get(liveHashOverrideParam2);
220
+ // JUST use it on the first loop, so if we can't find any matches, we ignore it.
221
+ if (liveHashOverrideObj) {
222
+ try {
223
+ let liveHashOverrideObject = JSON.parse(liveHashOverrideObj);
224
+ liveHashOverrideExpiryTime = liveHashOverrideObject.time + 1000 * 30;
225
+ liveHashOverride = liveHashOverrideObject.liveHash;
226
+ } catch (e: any) {
227
+ console.error(`Error parsing liveHashOverride ${liveHashOverride}: ${e.stack}`);
228
+ }
229
+ // Remove it from the search params
230
+ searchParams.delete(liveHashOverrideParam2);
231
+ document.location.search = searchParams.toString();
232
+ }
233
+
220
234
  let progressUI = createProgressUI();
221
235
  progressUI.setMessage("Finding available server...");
222
236
  while (true) {
@@ -226,9 +240,12 @@ async function edgeNodeFunction(config: {
226
240
  } catch (e) {
227
241
  console.error(e);
228
242
  }
229
- progressUI.setMessage("No available servers, retrying in 15s");
230
- console.log(`Failing to boot, trying in 15 seconds`);
231
- await new Promise(resolve => setTimeout(resolve, 1000 * 15));
243
+ progressUI.setMessage("No available servers, retrying soon");
244
+ let time = 1000 * 15;
245
+ if (Date.now() < liveHashOverrideExpiryTime) {
246
+ time = 1000 * 3;
247
+ }
248
+ await new Promise(resolve => setTimeout(resolve, time));
232
249
  }
233
250
  progressUI.stop();
234
251
 
@@ -260,6 +277,16 @@ async function edgeNodeFunction(config: {
260
277
  async function getEdgeNodeConfig(): Promise<EdgeNodeConfig> {
261
278
  let edgeIndex = cachedConfig || await (await fetch(config.edgeNodeConfigURL)).json() as EdgeNodesIndex;
262
279
  cachedConfig = undefined;
280
+ let liveHashForced = false;
281
+ if (Date.now() < liveHashOverrideExpiryTime) {
282
+ liveHashForced = true;
283
+ edgeIndex.liveHash = liveHashOverride;
284
+ }
285
+
286
+ // export const liveHashOverrideURL = new URLParam(liveHashOverrideParam, undefined as undefined | {
287
+ // liveHash: string;
288
+ // time: number;
289
+ // });
263
290
 
264
291
  console.group(`Found edge nodes`);
265
292
 
@@ -309,6 +336,9 @@ async function edgeNodeFunction(config: {
309
336
 
310
337
  let liveNodes = edgeNodes.filter(x => x.gitHash === edgeIndex.liveHash);
311
338
  if (liveNodes.length === 0) {
339
+ if (liveHashForced) {
340
+ throw new Error(`Could not find any live nodes (${edgeIndex.liveHash}), and the live hash is forced.`);
341
+ }
312
342
  let latestHash = edgeNodes[0].gitHash;
313
343
  console.warn(`Could not find any live nodes (${edgeIndex.liveHash}), falling back to latest hash: ${latestHash}`);
314
344
  liveNodes = edgeNodes.filter(x => x.gitHash === latestHash);
@@ -0,0 +1,187 @@
1
+ import { SocketFunction } from "socket-function/SocketFunction";
2
+ import { Querysub } from "../4-querysub/QuerysubController";
3
+ import { deploySchema } from "./deploySchema";
4
+ import { getDomain } from "../config";
5
+ import { throttleFunction, timeInMinute } from "socket-function/src/misc";
6
+ import { isNode } from "typesafecss";
7
+ import { logErrors, timeoutToError } from "../errors";
8
+ import { blue, red } from "socket-function/src/formatting/logColors";
9
+ import { showModal } from "../5-diagnostics/Modal";
10
+ import { qreact } from "../4-dom/qreact";
11
+ import { liveHashOverrideURL } from "./edgeBootstrap";
12
+ import { css } from "typesafecss";
13
+ import { formatTime } from "socket-function/src/formatting/format";
14
+ import { delay } from "socket-function/src/batching";
15
+
16
+ export function startEdgeNotifier() {
17
+ SocketFunction.expose(EdgeNotifierController);
18
+ let lastHash = "";
19
+ Querysub.createWatcher(() => {
20
+ let liveHash = deploySchema()[getDomain()].deploy.live.hash;
21
+ if (!Querysub.isAllSynced()) return;
22
+ if (liveHash === lastHash) return;
23
+ let refreshThresholdTime = deploySchema()[getDomain()].deploy.live.refreshThresholdTime;
24
+ lastHash = liveHash;
25
+ void notifyClients(liveHash, refreshThresholdTime);
26
+ });
27
+ }
28
+
29
+ let watchingClientNodes = new Set<string>();
30
+ const notifyClients = throttleFunction(100, async function notifyClients(liveHash: string, refreshThresholdTime: number) {
31
+ await Promise.allSettled(Array.from(watchingClientNodes).map(async clientNodeId => {
32
+ try {
33
+ await timeoutToError(5000, EdgeNotifierClientController.nodes[clientNodeId].onLiveHashChange(liveHash, refreshThresholdTime), () => new Error(`Timeout for watch updates on node ${clientNodeId}`));
34
+ } catch (e) {
35
+ console.log(blue(`Client unwatching liveHash ${clientNodeId}`), e);
36
+ watchingClientNodes.delete(clientNodeId);
37
+ }
38
+ }));
39
+ });
40
+
41
+ // Track current notification state
42
+ let currentNotification: { close: () => void } | null = null;
43
+ let curHash = "";
44
+ function onLiveHashChange(liveHash: string, refreshThresholdTime: number) {
45
+ let notifyIntervals = [0, 0.1, 0.5, 1];
46
+ console.log(blue(`Client liveHash changed ${liveHash}`));
47
+ let skipFirst = false;
48
+ if (currentNotification) {
49
+ currentNotification.close();
50
+ currentNotification = null;
51
+ skipFirst = true;
52
+ }
53
+ curHash = liveHash;
54
+
55
+ // Start notification loop
56
+ void (async () => {
57
+ // Show notifications at intervals
58
+ for (let i = 0; i < notifyIntervals.length; i++) {
59
+ // Don't show if a newer notification is active
60
+ if (curHash !== liveHash) return;
61
+
62
+ let waitDuration = (notifyIntervals[i + 1] - notifyIntervals[i]) * refreshThresholdTime;
63
+ if (i >= notifyIntervals.length - 1 && waitDuration <= 30 * 1000) continue;
64
+
65
+ // Update the URL override for manual refreshes
66
+ Querysub.localCommit(() => {
67
+ liveHashOverrideURL.value = { liveHash, time: Date.now() };
68
+ });
69
+ if (!skipFirst) {
70
+ // Show notification modal
71
+ currentNotification = {
72
+ close: showModal({
73
+ content: (
74
+ <div
75
+ title={`Live Hash: ${liveHash}`}
76
+ className={
77
+ css.vbox(10).pad(20)
78
+ .hsla(0, 0, 0, 0.8)
79
+ .position("fixed")
80
+ .bottom(10)
81
+ .right(10)
82
+ .zIndex(1000)
83
+ }
84
+ onClick={() => {
85
+ if (currentNotification) {
86
+ currentNotification.close();
87
+ currentNotification = null;
88
+ }
89
+ }}
90
+ >
91
+ <div className={css.vbox(10).maxWidth(250)}>
92
+ <h3 className={css.margin(0)}>Server Update Available</h3>
93
+ <div>The server has been updated. Please refresh the page to ensure you don't experience incompatibility issues.</div>
94
+ </div>
95
+ <div className={css.hbox(10).justifyContent("flex-end")}>
96
+ <button
97
+ className={css.pad(8, 16).hsl(200, 50, 50).color("white").pointer}
98
+ onClick={() => {
99
+ Querysub.localCommit(() => {
100
+ liveHashOverrideURL.value = { liveHash, time: Date.now() };
101
+ });
102
+ window.location.reload();
103
+ }}
104
+ >
105
+ Refresh Now
106
+ </button>
107
+ <button
108
+ className={css.pad(8, 16).pointer}
109
+ onClick={() => {
110
+ if (currentNotification) {
111
+ currentNotification.close();
112
+ currentNotification = null;
113
+ }
114
+ }}
115
+ >
116
+ Dismiss
117
+ </button>
118
+ </div>
119
+ </div>
120
+ )
121
+ }).close
122
+ };
123
+ }
124
+ skipFirst = false;
125
+
126
+ console.log(red(`Notify again in ${formatTime(waitDuration)}`));
127
+ await delay(waitDuration);
128
+ }
129
+
130
+ console.log(red(`Force refresh after 30 seconds`));
131
+ await delay(30 * 1000);
132
+
133
+ // Only force refresh if this is still the current notification
134
+ if (curHash === liveHash) {
135
+ window.location.reload();
136
+ }
137
+ })();
138
+ }
139
+
140
+ const EdgeNotifierClientController = SocketFunction.register(
141
+ "EdgeNotifierClientController-ed122e41-6ad2-4161-9492-a3f8414bd4f0",
142
+ new class EdgeNotifierClient {
143
+ public async onLiveHashChange(liveHash: string, refreshThresholdTime: number) {
144
+ onLiveHashChange(liveHash, refreshThresholdTime);
145
+ }
146
+ },
147
+ () => ({
148
+ onLiveHashChange: {},
149
+ }),
150
+ () => ({}),
151
+ {
152
+ noAutoExpose: true,
153
+ }
154
+ );
155
+
156
+ if (!isNode()) {
157
+ setImmediate(() => {
158
+ void Querysub.optionalStartupWait().finally(() => {
159
+ logErrors(EdgeNotifierController.nodes[SocketFunction.browserNodeId()].watchUpdates());
160
+ });
161
+ SocketFunction.expose(EdgeNotifierClientController);
162
+ });
163
+ }
164
+
165
+ class EdgeNotifierControllerBase {
166
+ public async watchUpdates() {
167
+ let nodeId = SocketFunction.getCaller().nodeId;
168
+ console.log(blue(`Client watching liveHash ${nodeId}`));
169
+ watchingClientNodes.add(nodeId);
170
+ Querysub.onNextDisconnect(nodeId, () => {
171
+ watchingClientNodes.delete(nodeId);
172
+ });
173
+ }
174
+ }
175
+
176
+ const EdgeNotifierController = SocketFunction.register(
177
+ "EdgeNotifierController-b11fbbf0-ef5e-4ba3-80ae-d5b1b8124f2d",
178
+ new EdgeNotifierControllerBase(),
179
+ () => ({
180
+ watchUpdates: {},
181
+ }),
182
+ () => ({}),
183
+ {
184
+ noAutoExpose: true,
185
+ }
186
+ );
187
+
@@ -24,6 +24,7 @@ import { deploySchema } from "./deploySchema";
24
24
  import { proxyWatcher } from "../2-proxy/PathValueProxyWatcher";
25
25
  import { Querysub } from "../4-querysub/QuerysubController";
26
26
  import { onEdgeNodesChanged } from "./edgeBootstrap";
27
+ import { startEdgeNotifier } from "./edgeClientWatcher";
27
28
 
28
29
  const UPDATE_POLL_INTERVAL = timeInMinute;
29
30
  const DEAD_NODE_COUNT_THRESHOLD = 15;
@@ -177,8 +178,6 @@ const loadEntryPointsByHash = runInSerial(async function loadEntryPointsByHash(c
177
178
  await edgeNodeStorage.set(getNextNodePath(), Buffer.from(JSON.stringify(edgeNodeConfig)));
178
179
  console.log(magenta(`Deployed edge node`), edgeNodeConfig);
179
180
 
180
- SocketFunction.expose(EdgeNodeController);
181
-
182
181
  await updateEdgeNodesFile();
183
182
 
184
183
  onEdgeNodesChanged();
@@ -196,6 +195,8 @@ export const getEdgeNodeConfigURL = lazy(async () => {
196
195
  const startUpdateLoop = lazy(async () => {
197
196
  await getEdgeNodeConfigURL();
198
197
  await runInfinitePollCallAtStart(UPDATE_POLL_INTERVAL, updateLoop);
198
+ SocketFunction.expose(EdgeNodeController);
199
+ startEdgeNotifier();
199
200
  });
200
201
 
201
202
  async function updateLoop() {
@@ -272,9 +273,9 @@ export async function preloadUI(hash: string) {
272
273
  let nodeIds = await getControllerNodeIdList(EdgeNodeController);
273
274
  await Promise.allSettled(nodeIds.map(async nodeId => {
274
275
  let controller = EdgeNodeController.nodes[nodeId.nodeId];
275
- console.log(blue(`Preloading UI on ${nodeId}, hash: ${hash}`));
276
+ console.log(blue(`Preloading UI on ${String(nodeId)}, hash: ${hash}`));
276
277
  await errorToUndefined(controller.preloadUI({ hash }));
277
- console.log(blue(`Finished preloading UI on ${nodeId}`));
278
+ console.log(blue(`Finished preloading UI on ${String(nodeId)}`));
278
279
  }));
279
280
  }
280
281
 
@@ -309,23 +310,3 @@ const EdgeNodeController = SocketFunction.register(
309
310
  }
310
311
  );
311
312
 
312
- // async function main() {
313
- // await loadEntryPointsByHash({
314
- // host: "127-0-0-1.querysub.com:1111",
315
- // entryPaths: ["./src/browser.ts"],
316
- // hash: "7b1eb4934d5bdfe6d0f7076049c4b7decad4e645",
317
- // });
318
- // }
319
- // main().catch(console.error).finally(() => process.exit());
320
-
321
-
322
-
323
- //todonext
324
- // 3) Verify the deploy code works on the live site
325
- // - I think it isn't? But maybe we just need to do-update again
326
-
327
-
328
- // 7) Client live hash watch code + notify + refresh + flag to ignore the baked in live hash + edge node
329
- // - Test by pushing, deploying, and then we should immediately see a notify to refresh
330
-
331
- // 8) VERIFY writing and permissions still works
@@ -290,6 +290,9 @@ export class Querysub {
290
290
  public static isAllSynced = Querysub.allSynced;
291
291
  public static isAnyUnsynced = Querysub.anyUnsynced;
292
292
 
293
+ public static onNextDisconnect = SocketFunction.onNextDisconnect;
294
+ public static isNodeConnected = SocketFunction.isNodeConnected;
295
+
293
296
  public static pathHasAnyWatchers(get: () => unknown) {
294
297
  return clientWatcher.pathHasAnyWatchers(getProxyPath(get));
295
298
  }
@@ -63,11 +63,13 @@ export function createURLSync<T>(urlKey: string, defaultValue: T, config?: {
63
63
  if (config?.storage === "localStorage") {
64
64
  localStorageKeys.add(urlKey);
65
65
  }
66
- Querysub.localCommit(() => {
67
- data().defaults[urlKey] = defaultValue;
68
- if (!(urlKey in loadSearchCache)) {
69
- data().params[urlKey] = deepCloneJSON(defaultValue);
70
- }
66
+ setImmediate(() => {
67
+ Querysub.localCommit(() => {
68
+ data().defaults[urlKey] = defaultValue;
69
+ if (!(urlKey in loadSearchCache)) {
70
+ data().params[urlKey] = deepCloneJSON(defaultValue);
71
+ }
72
+ });
71
73
  });
72
74
  function deleteKeys(obj: any) {
73
75
  if (!canHaveChildren(obj)) return;
@@ -222,9 +224,11 @@ function watchURLForUpdates() {
222
224
  }
223
225
 
224
226
  if (!isNode()) {
225
- loadParamsFromURL();
226
- watchURLForUpdates();
227
- syncStateToURL();
227
+ setImmediate(() => {
228
+ loadParamsFromURL();
229
+ watchURLForUpdates();
230
+ syncStateToURL();
231
+ });
228
232
  }
229
233
 
230
234
  export function parseSearchString(search: string): { [key: string]: unknown } {