querysub 0.649.0 → 0.651.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/overview.md ADDED
@@ -0,0 +1,46 @@
1
+ # Querysub Overview
2
+
3
+ The system is fundamentally a data synchronization system.
4
+
5
+ ## Path values and time
6
+
7
+ Data exists in path values, which have a path (a structured list of strings) and a value. These are time based: they have the time that they occur at, which is a globally unique time. This time includes a somewhat unique identifier for the server that created the time (best effort — it cannot be relied on, but is usually unique), the time itself, and also a version value that our system uses to create something that happens at the same time, but just an epsilon amount after.
8
+
9
+ ## Locks and validity
10
+
11
+ Path values also include what are referred to as locks. Locks are essentially a list of reads that were done at the time the path value was created.
12
+
13
+ Values are evaluated in an eventually consistent manner. A value can be rejected if its locks are rejected. This can mean that the value it read has itself become rejected, or it can mean that the value it read was not the latest value at the time — which we know if we find another value between the creation time of the value that was read and the time that it was read at. That's why it's called a lock: it's a kind of locking of that value between its creation time and the time we read it. Of course, it's a reversed lock — instead of having the writer fail because of the lock, we have the reader fail.
14
+
15
+ ## Synchronization and watchers
16
+
17
+ Synchronization is very important. We synchronize values between different watchers in a client, and between machines. This is done at a low level in PathWatcher and RemoteWatcher. We have the ability to watch a path, or watch the direct children of a path. There is no recursive watching — if values need to be recursively watched, the watcher has to recursively access more and more each time it receives values, watching more each time.
18
+
19
+ ## Schema
20
+
21
+ We have a schema system which can simplify accesses and make it so that if values aren't provided, we know whether to default to a primitive value or use a proxy so the code can drill down further.
22
+
23
+ ## Proxy watcher
24
+
25
+ Most accesses are done using the proxy watcher. This is a system that sits on top of our other systems, letting you write regular JavaScript code to access fields and write to fields as if it were plain JavaScript, with all of the reads and writes being converted to path value reads and writes.
26
+
27
+ ## Client writes, function calls, and predictions
28
+
29
+ On the client, anything that needs to impact a remote value is mutated by making a special socket function call to the server, asking it to add a function call write — which writes a path value to a specific location. We then have a function runner server that reads from that location, running those functions. The client side also runs these functions, but only as a prediction. That way most actions feel immediate. When the server function runner does eventually run, it updates with the actual results.
30
+
31
+ ## Sharding
32
+
33
+ The servers are sharded — they are split up over different values. We generally don't do global path hashing. Instead, there are specific paths that a server reads on startup, and it does its hashing by looking at the child key of those paths when they match. These generally match up with lookups, so the hashes are usually the lookup keys. This allows an object's fields to be on the same server — minimizing the number of different servers we access — while still sharding the data.
34
+
35
+ ## Geographic routing
36
+
37
+ Almost everything has some level of sharding or geographic control. For example, the client side tries to connect to the querysub server — the server that actually adds the function calls and interacts with the client — and it usually connects to the closest one, or the one with the lowest latency. Function runners are also sharded between geographic locations, and the client usually requests one that is close to it.
38
+
39
+ ## Non-core systems
40
+
41
+ - The binary format of path values, which involves compression, etc.
42
+ - The error notification system.
43
+ - The logging and log search system.
44
+ - The MCP server.
45
+ - QReact, which implements a JSX component renderer using our synchronization system.
46
+ - A machine and service management system.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "querysub",
3
- "version": "0.649.0",
3
+ "version": "0.651.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",
@@ -79,8 +79,8 @@
79
79
  "node-forge": "https://github.com/sliftist/forge#e618181b469b07bdc70b968b0391beb8ef5fecd6",
80
80
  "pako": "^2.1.0",
81
81
  "peggy": "^5.0.6",
82
- "sliftutils": "^1.7.120",
83
- "socket-function": "^1.2.33",
82
+ "sliftutils": "^1.7.124",
83
+ "socket-function": "^1.2.34",
84
84
  "terser": "^5.31.0",
85
85
  "typenode": "^6.6.1",
86
86
  "typesafecss": "^0.32.0",
@@ -55,6 +55,13 @@ export function getRoutingOverridePart(part: string): {
55
55
  };
56
56
  }
57
57
 
58
+ export function getRoutingOverrideOriginalKey(part: string): string | undefined {
59
+ if (!part.startsWith(keySpecialIdentifier)) return undefined;
60
+ let parts = part.split("!");
61
+ if (parts.length < 4) return undefined;
62
+ return parts.slice(3).join("!");
63
+ }
64
+
58
65
  export const hasPrefixHash = cacheLimited(1000 * 10,
59
66
  (config: { spec: AuthoritySpec, prefixHash: string }) => {
60
67
  let { spec, prefixHash } = config;
@@ -265,7 +265,7 @@ export class DeployPage extends qreact.Component {
265
265
 
266
266
  let liveGitRef = mostCommon(liveFunctions.map(x => x.gitRef)) || "";
267
267
  let pendingGitRef = gitInfo?.latestRef || "";
268
- let anyUncommitted = !!gitInfo?.uncommitted.length;
268
+ //let anyUncommitted = !!gitInfo?.uncommitted.length;
269
269
 
270
270
  return <div className={css.vbox(40).marginTop(20)}>
271
271
  <div className={css.hbox(10)}>
@@ -274,12 +274,12 @@ export class DeployPage extends qreact.Component {
274
274
  <button
275
275
  disabled={controller.isAnyLoading()}
276
276
  className={buttonStyle.alignSelf("stretch").center.hsl(240, 70, 50).colorhsl(0, 0, 100)
277
- + (anyUncommitted && css.opacity(0.5))
277
+ //+ (anyUncommitted && css.opacity(0.5))
278
278
  + (this.state.isDeploying && css.opacity(0.7))
279
279
  }
280
- title={anyUncommitted && `Must commit uncommitted changes before deploying` || this.state.isDeploying && "Deploying..." || ""}
280
+ title={this.state.isDeploying && "Deploying..." || ""}
281
281
  onClick={async () => {
282
- if (anyUncommitted || this.state.isDeploying) return;
282
+ if (this.state.isDeploying) return;
283
283
 
284
284
  this.state.isDeploying = true;
285
285
  this.state.deployStartTime = Date.now();
@@ -20,11 +20,14 @@ import { MCPIndexedLogs, normalizeTime } from "./MCPIndexedLogs";
20
20
  import { searchStorageLogsForMCP } from "../../../deployManager/components/storage/storageLogMCPSearch";
21
21
  import { getAllNodeIds } from "../../../-f-node-discovery/NodeDiscovery";
22
22
  import { NodeCapabilitiesController } from "../../../-g-core-values/NodeCapabilities";
23
- import { formatTime } from "socket-function/src/formatting/format";
23
+ import { formatDateTime, formatTime } from "socket-function/src/formatting/format";
24
24
  import { SocketFunction } from "socket-function/SocketFunction";
25
+ import { sort } from "socket-function/src/misc";
26
+ import { getSuppressionEntries } from "../errorNotifications2/errorNotifications";
25
27
 
26
28
  const DEFAULT_MCP_HTTP_PORT = 4487;
27
29
  const NODE_INFO_TIMEOUT_MS = 5000;
30
+ const DEFAULT_SUPPRESSION_LIMIT = 20;
28
31
 
29
32
  const PROTOCOL_VERSION = "2025-03-26";
30
33
  const SERVER_INFO = { name: "querysub-indexed-logs", version: "0.1.0" };
@@ -97,6 +100,19 @@ Query syntax (case-sensitive substring match against each entry's JSON):
97
100
  properties: {},
98
101
  },
99
102
  },
103
+ {
104
+ name: "getSuppressions",
105
+ description: `List error-suppression entries (patterns that stop matching errors from triggering notifications), sorted most-recently-updated first. There can be hundreds, so results are limited (default ${DEFAULT_SUPPRESSION_LIMIT}); use the optional query filter to narrow.
106
+
107
+ Returns { total, returned, results } — total is the entry count after filtering, results contains { id, pattern, notes, lastUpdated, created, timeout, expired }. All three times are formatted date strings. expired is present (true) when the entry's timeout has passed, meaning it no longer suppresses anything.`,
108
+ inputSchema: {
109
+ type: "object",
110
+ properties: {
111
+ limit: { type: "number", default: DEFAULT_SUPPRESSION_LIMIT, description: "Maximum entries to return, most recently updated first." },
112
+ query: { type: "string", description: "Optional case-insensitive substring filter, matched against id, pattern, and notes." },
113
+ },
114
+ },
115
+ },
100
116
  ];
101
117
 
102
118
  type JsonRpcRequest = {
@@ -184,6 +200,8 @@ async function dispatch(method: string, params: unknown, mcp: MCPIndexedLogs): P
184
200
  });
185
201
  } else if (toolName === "listNodes") {
186
202
  result = await getNodeInfos();
203
+ } else if (toolName === "getSuppressions") {
204
+ result = await getSuppressionsForMCP(args as { limit?: number; query?: string });
187
205
  } else {
188
206
  throw new Error(`Unknown tool ${toolName}`);
189
207
  }
@@ -220,6 +238,34 @@ async function getNodeInfos(): Promise<NodeInfo[]> {
220
238
  );
221
239
  }
222
240
 
241
+ async function getSuppressionsForMCP(config: { limit?: number; query?: string }) {
242
+ let limit = config.limit ?? DEFAULT_SUPPRESSION_LIMIT;
243
+ let entries = await getSuppressionEntries();
244
+ let query = config.query?.toLowerCase();
245
+ if (query) {
246
+ const q = query;
247
+ entries = entries.filter(x => (x.id + "\n" + x.pattern + "\n" + (x.notes || "")).toLowerCase().includes(q));
248
+ }
249
+ sort(entries, x => -x.lastUpdatedTime);
250
+ let now = Date.now();
251
+ let results = entries.slice(0, limit).map(x => {
252
+ let expired: true | undefined;
253
+ if (x.timeout < now) {
254
+ expired = true;
255
+ }
256
+ return {
257
+ id: x.id,
258
+ pattern: x.pattern,
259
+ notes: x.notes,
260
+ lastUpdated: formatDateTime(x.lastUpdatedTime),
261
+ created: formatDateTime(x.createdTime),
262
+ timeout: formatDateTime(x.timeout),
263
+ expired,
264
+ };
265
+ });
266
+ return { total: entries.length, returned: results.length, results };
267
+ }
268
+
223
269
  async function main() {
224
270
  let mcp = new MCPIndexedLogs();
225
271
 
@@ -27,7 +27,7 @@ export function getTimeRange(): { startTime: number; endTime: number; searchFrom
27
27
  };
28
28
  }
29
29
 
30
- export class TimeRangeSelector extends qreact.Component {
30
+ export class TimeRangeSelector extends qreact.Component<{ dark?: boolean }> {
31
31
  render() {
32
32
  const timeRange = getTimeRange();
33
33
 
@@ -51,8 +51,12 @@ export class TimeRangeSelector extends qreact.Component {
51
51
 
52
52
 
53
53
  let now = Querysub.nowDelayed(timeInMinute);
54
+ let containerCss = css.vbox(12).pad2(16).bord2(200, 20, 80).hsl(200, 10, 98);
55
+ if (this.props.dark) {
56
+ containerCss = css.vbox(12).pad2(16).bord2(200, 20, 30).hsl(200, 10, 12);
57
+ }
54
58
  return (
55
- <div className={css.vbox(12).pad2(16).bord2(200, 20, 80).hsl(200, 10, 98)}>
59
+ <div className={containerCss}>
56
60
  <div className={css.hbox(12).wrap}>
57
61
  <div className={css.hbox(8).wrap.opacity(0.8).width(200)}>
58
62
  From {formatTime(now - timeRange.startTime)} AGO to {timeRange.endTime > now ? "now" : `${formatTime(now - timeRange.endTime)} AGO`}
@@ -86,7 +90,7 @@ export class TimeRangeSelector extends qreact.Component {
86
90
  }}
87
91
  />
88
92
  <Button
89
- hue={110} onClick={() => {
93
+ hue={210} onClick={() => {
90
94
  let now = Date.now();
91
95
  startTimeParam.value = now;
92
96
  endTimeParam.value = undefined;
@@ -95,7 +99,7 @@ export class TimeRangeSelector extends qreact.Component {
95
99
  Set to future data
96
100
  </Button>
97
101
  <Button
98
- hue={110} onClick={() => {
102
+ hue={210} onClick={() => {
99
103
  startTimeParam.value = now - timeInHour;
100
104
  endTimeParam.value = now + timeInHour * 2;
101
105
  }}
@@ -103,7 +107,7 @@ export class TimeRangeSelector extends qreact.Component {
103
107
  Set to last hour
104
108
  </Button>
105
109
  <Button
106
- hue={110} onClick={() => {
110
+ hue={210} onClick={() => {
107
111
  startTimeParam.value = now - timeInDay;
108
112
  endTimeParam.value = now + timeInHour * 2;
109
113
  }}
@@ -111,7 +115,7 @@ export class TimeRangeSelector extends qreact.Component {
111
115
  Set to last day
112
116
  </Button>
113
117
  <Button
114
- hue={110} onClick={() => {
118
+ hue={210} onClick={() => {
115
119
  startTimeParam.value = now - timeInDay * 7;
116
120
  endTimeParam.value = now + timeInHour * 2;
117
121
  }}
@@ -119,12 +123,12 @@ export class TimeRangeSelector extends qreact.Component {
119
123
  Set to last 7 days
120
124
  </Button>
121
125
  {!!(endTimeParam.value || startTimeParam.value) && <Button
122
- hue={110} onClick={resetToLastDay}
126
+ hue={210} onClick={resetToLastDay}
123
127
  >
124
128
  Reset
125
129
  </Button>}
126
130
  {(!startTimeParam.value || !endTimeParam.value) && <Button
127
- hue={110}
131
+ hue={210}
128
132
  onClick={() => {
129
133
  startTimeParam.value = timeRange.startTime;
130
134
  endTimeParam.value = timeRange.endTime;
@@ -394,22 +394,27 @@ async function auditAuthority(nodeId: string, pathsToAudit: { path: string }[],
394
394
  continue;
395
395
  }
396
396
 
397
+ let responseMissing = !response.time || compareTime(response.time, epochTime) === 0;
398
+ if (responseMissing) {
399
+ response = { ...response, valid: true, isTransparent: true };
400
+ }
397
401
  if (ourValue.isTransparent && response.isTransparent) continue;
398
402
 
399
403
  if (request.time) {
400
404
  let ourExact = authorityStorage.getValueExactMaybeRejected(request.path, request.time);
401
405
  if (!ourExact || !ourExact.valid) continue;
402
406
  if (compareTime(ourExact.time, epochTime) === 0) continue;
407
+ if (ourExact.isTransparent && responseMissing) continue;
403
408
  // The exact value = server does not have
404
409
  // - Send it our value
405
- if (!response.time) {
410
+ if (!response.time || responseMissing) {
406
411
  valuesToSend.push(ourExact);
407
412
  trackSyncAge({
408
413
  path: request.path,
409
414
  ourTimeId: ourExact.time.time,
410
415
  remoteTimeId: undefined,
411
416
  ourValid: ourExact.valid,
412
- remoteValid: response.valid,
417
+ remoteValid: undefined,
413
418
  remoteNodeId: nodeId,
414
419
  reason: "Remote is missing our value, sending it to them",
415
420
  });
@@ -471,14 +476,14 @@ async function auditAuthority(nodeId: string, pathsToAudit: { path: string }[],
471
476
  }
472
477
  // Our latest valid = server does not have & it's latest valid is older than ours
473
478
  // - Send it our value
474
- else if (response.valid === undefined && (!response.time || compareTime(ourValue.time, response.time) > 0)) {
479
+ else if (responseMissing && compareTime(ourValue.time, epochTime) > 0) {
475
480
  valuesToSend.push(ourValue);
476
481
  trackSyncAge({
477
482
  path: response.path,
478
483
  ourTimeId: ourValue.time.time,
479
- remoteTimeId: response.time?.time,
484
+ remoteTimeId: undefined,
480
485
  ourValid: ourValue.valid ?? false,
481
- remoteValid: response.valid,
486
+ remoteValid: undefined,
482
487
  remoteNodeId: nodeId,
483
488
  reason: "Remote is missing our value, sending it to them",
484
489
  });
@@ -535,12 +540,13 @@ class PathAuditerService {
535
540
  } else {
536
541
  value = authorityStorage.getValueAtOrBeforeTime(request.path);
537
542
  }
543
+ value = value || createMissingEpochValue(request.path);
538
544
  results.push({
539
545
  path: request.path,
540
- time: value?.time,
541
- valid: value === undefined ? undefined : value.valid,
542
- isTransparent: !!value?.isTransparent,
543
- event: !!value?.event,
546
+ time: value.time,
547
+ valid: value.valid,
548
+ isTransparent: !!value.isTransparent,
549
+ event: !!value.event,
544
550
  });
545
551
  }
546
552
  return results;