querysub 0.504.0 → 0.506.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.
@@ -79,6 +79,7 @@ class AuthorityNodeRow extends qreact.Component<{ info: NodeAuthorityInfo }> {
79
79
  <AuthorityRangeBar start={spec.routeStart} end={spec.routeEnd} />
80
80
  <span className={css.colorhsl(0, 0, 50)}>width {(spec.routeEnd - spec.routeStart).toFixed(4)}</span>
81
81
  {spec.excludeDefault && <span className={css.colorhsl(0, 70, 35)}>(excludes default)</span>}
82
+ {spec.networks && spec.networks.length > 0 && <span className={css.colorhsl(210, 60, 40)}>networks: {spec.networks === "all" && "all" || Array.isArray(spec.networks) && spec.networks.join(", ") || ""}</span>}
82
83
  <span className={css.colorhsl(0, 0, 40).ellipsis.flexFillWidth}>{info.entryPoint || "(no entry point)"}</span>
83
84
  </div>
84
85
  {expanded &&
@@ -98,16 +99,40 @@ export class AuthoritySpecPage extends qreact.Component {
98
99
  render() {
99
100
  let infos = AuthoritySpecSynced(getBrowserUrlNode()).getAllNodeAuthoritySpecs();
100
101
  if (!infos) {
101
- return <div className={css.pad2(16)}>Loading authority specs...</div>;
102
+ return <div className={css.pad2(16)}>Loading routing table...</div>;
102
103
  }
103
104
  infos = infos.filter(x => x.spec && x.spec.routeStart >= 0 && x.spec.routeEnd >= 0);
104
105
  sort(infos, x => x.spec!.routeStart);
105
106
 
107
+ let infosPerNetwork = new Map<string, NodeAuthorityInfo[]>();
108
+ for (let info of infos) {
109
+ let networks = info.spec!.networks;
110
+ if (networks === "all") {
111
+ networks = ["all"];
112
+ }
113
+ if (!networks || networks.length === 0) {
114
+ networks = ["default"];
115
+ }
116
+ for (let network of networks) {
117
+ let list = infosPerNetwork.get(network);
118
+ if (!list) {
119
+ list = [];
120
+ infosPerNetwork.set(network, list);
121
+ }
122
+ list.push(info);
123
+ }
124
+ }
125
+ let networkEntries = Array.from(infosPerNetwork.entries());
126
+ sort(networkEntries, entry => entry[0] === "default" && " " || entry[0]);
127
+
106
128
  return <div className={css.vbox(12).pad2(16).fillWidth}>
107
- <h2>Node Authority Specs ({infos.length})</h2>
108
- <div className={css.vbox(8).fillWidth}>
109
- {infos.map(info => <AuthorityNodeRow key={info.nodeId} info={info} />)}
110
- </div>
129
+ <h2>Routing Table ({infos.length})</h2>
130
+ {networkEntries.map(([network, networkInfos]) =>
131
+ <div className={css.vbox(8).fillWidth}>
132
+ <h3>{network} ({networkInfos.length})</h3>
133
+ {networkInfos.map(info => <AuthorityNodeRow key={info.nodeId} info={info} />)}
134
+ </div>
135
+ )}
111
136
  </div>;
112
137
  }
113
138
  }
@@ -1,395 +0,0 @@
1
- import { devDebugbreak } from "../config";
2
- import debugbreak from "debugbreak";
3
-
4
- export type Filterable = {
5
- values: string[];
6
- } | undefined;
7
- // undefined FilterSelector only matches empty Filterables.
8
- export type FilterSelector = {
9
- expression: ASTNode;
10
- } | undefined;
11
-
12
- type ASTNode = {
13
- type: "orList";
14
- parts: ASTNode[];
15
- } | {
16
- type: "andList";
17
- parts: ASTNode[];
18
- } | {
19
- type: "not";
20
- part: ASTNode;
21
- } | {
22
- type: "leaf";
23
- matcher: FilterMatcher;
24
- };
25
-
26
- export type FilterMatcher = {
27
- type: "includes";
28
- value: string;
29
- } | {
30
- type: "regex";
31
- value: string;
32
- };
33
-
34
- export function doesMatch(filterable: Filterable, filterSelector: FilterSelector): boolean {
35
- if (!filterSelector) {
36
- return !filterable;
37
- }
38
- return evaluateASTNode(filterSelector.expression, filterable);
39
- }
40
- function evaluateASTNode(node: ASTNode, filterable: Filterable): boolean {
41
- if (node.type === "orList") {
42
- return node.parts.some(part => evaluateASTNode(part, filterable));
43
- } else if (node.type === "andList") {
44
- return node.parts.every(part => evaluateASTNode(part, filterable));
45
- } else if (node.type === "not") {
46
- return !evaluateASTNode(node.part, filterable);
47
- } else if (node.type === "leaf") {
48
- return evaluateLeaf(node.matcher, filterable);
49
- } else {
50
- let unhandled: never = node;
51
- }
52
- return false;
53
- }
54
- function evaluateLeaf(matcher: FilterMatcher, filterable: Filterable): boolean {
55
- if (!filterable) return false;
56
- if (matcher.type === "includes") {
57
- return filterable.values.some(value => value.includes(matcher.value));
58
- } else if (matcher.type === "regex") {
59
- return filterable.values.some(value => new RegExp(matcher.value).test(value));
60
- } else {
61
- let unhandled: never = matcher;
62
- }
63
- return false;
64
- }
65
-
66
- export function mergeSelectors(selectors: FilterSelector[]): FilterSelector {
67
- if (selectors.length === 0) return undefined;
68
- if (selectors.length === 1) return selectors[0];
69
- let nodes: ASTNode[] = [];
70
- for (let selector of selectors) {
71
- if (!selector) continue;
72
- // Flatten orLists
73
- if (selector.expression.type === "orList") {
74
- nodes.push(...selector.expression.parts);
75
- } else {
76
- nodes.push(selector.expression);
77
- }
78
- }
79
- return {
80
- expression: {
81
- type: "orList",
82
- parts: nodes,
83
- },
84
- };
85
- }
86
- export function mergeFilterables(filterables: Filterable[]): Filterable {
87
- if (filterables.length === 0) return undefined;
88
- if (filterables.length === 1) return filterables[0];
89
- let values = Array.from(new Set(filterables.flatMap(filterable => filterable?.values ?? [])));
90
- if (values.length === 0) return undefined;
91
- return {
92
- values,
93
- };
94
- }
95
-
96
- // apple | b & "cats are | funny" | !(d | elephant)
97
- // " at the start and end allows using special characters in a value
98
- // "" inside of " is parsed as a single "
99
- // The values are trimmed
100
- export function parseFilterSelector(filterSelector: string): FilterSelector {
101
- if (!filterSelector.trim()) return undefined;
102
-
103
- let pos = 0;
104
- const input = filterSelector;
105
-
106
- function parseValue(): string {
107
- let value = "";
108
- if (input[pos] === "\"") {
109
- pos++; // Skip opening quote
110
- while (pos < input.length) {
111
- if (input[pos] === "\"") {
112
- if (input[pos + 1] === "\"") {
113
- value += "\"";
114
- pos += 2;
115
- continue;
116
- }
117
- pos++; // Skip closing quote
118
- break;
119
- }
120
- value += input[pos];
121
- pos++;
122
- }
123
- } else {
124
- while (pos < input.length && !/[\s|&!()]/.test(input[pos])) {
125
- value += input[pos];
126
- pos++;
127
- }
128
- }
129
- return value.trim();
130
- }
131
-
132
- function skipWhitespace() {
133
- while (pos < input.length && /\s/.test(input[pos])) pos++;
134
- }
135
-
136
- function parseExpression(): ASTNode {
137
- skipWhitespace();
138
-
139
- if (pos >= input.length) {
140
- throw new Error(`Expected expression at position ${pos} but reached end of input`);
141
- }
142
-
143
- // Handle NOT
144
- if (input[pos] === "!") {
145
- pos++;
146
- skipWhitespace();
147
-
148
- // Support both (!x) and !(x) forms
149
- const needsClosingParen = input[pos] === "(";
150
- if (needsClosingParen) {
151
- pos++;
152
- }
153
-
154
- const expr = parseOrExpression();
155
- skipWhitespace();
156
-
157
- if (needsClosingParen) {
158
- if (input[pos] !== ")") {
159
- throw new Error(`Expected ")" at position ${pos}, found "${input[pos]}"`);
160
- }
161
- pos++;
162
- }
163
-
164
- return { type: "not", part: expr };
165
- }
166
-
167
- // Handle parentheses
168
- if (input[pos] === "(") {
169
- pos++;
170
- const expr = parseOrExpression();
171
- skipWhitespace();
172
- if (input[pos] !== ")") {
173
- throw new Error(`Expected ")" at position ${pos}, found "${input[pos]}"`);
174
- }
175
- pos++;
176
- return expr;
177
- }
178
-
179
- // Handle leaf node (simple value)
180
- const value = parseValue();
181
- if (!value) {
182
- throw new Error(`Expected value at position ${pos}`);
183
- }
184
- return {
185
- type: "leaf",
186
- matcher: { type: "includes", value }
187
- };
188
- }
189
-
190
- function parseAndExpression(): ASTNode {
191
- let left = parseExpression();
192
- skipWhitespace();
193
-
194
- const parts: ASTNode[] = [left];
195
-
196
- while (pos < input.length && input[pos] === "&") {
197
- pos++;
198
- skipWhitespace();
199
- parts.push(parseExpression());
200
- skipWhitespace();
201
- }
202
-
203
- if (parts.length === 1) return parts[0];
204
- return { type: "andList", parts };
205
- }
206
-
207
- function parseOrExpression(): ASTNode {
208
- let left = parseAndExpression();
209
- skipWhitespace();
210
-
211
- const parts: ASTNode[] = [left];
212
-
213
- while (pos < input.length && input[pos] === "|") {
214
- pos++;
215
- skipWhitespace();
216
- parts.push(parseAndExpression());
217
- skipWhitespace();
218
- }
219
-
220
- if (parts.length === 1) return parts[0];
221
- return { type: "orList", parts };
222
- }
223
-
224
- try {
225
- const expression = parseOrExpression();
226
- skipWhitespace();
227
-
228
- if (pos < input.length) {
229
- throw new Error(`Unexpected character "${input[pos]}" at position ${pos}`);
230
- }
231
-
232
- return { expression };
233
- } catch (error: any) {
234
- throw new Error(`Failed to parse filter selector "${filterSelector}": ${error.message}`);
235
- }
236
- }
237
- export function serializeFilterSelector(filterSelector: FilterSelector): string {
238
- if (!filterSelector) return "";
239
-
240
- function serializeASTNode(node: ASTNode, parentPrecedence = 0): string {
241
- // Precedence: OR = 1, AND = 2, NOT = 3, LEAF = 4
242
- let result: string;
243
- let currentPrecedence: number;
244
-
245
- if (node.type === "orList") {
246
- currentPrecedence = 1;
247
- result = node.parts.map(part => serializeASTNode(part, currentPrecedence)).join(" | ");
248
- } else if (node.type === "andList") {
249
- currentPrecedence = 2;
250
- result = node.parts.map(part => serializeASTNode(part, currentPrecedence)).join(" & ");
251
- } else if (node.type === "not") {
252
- currentPrecedence = 3;
253
- if (
254
- (node.part.type === "andList" || node.part.type === "orList") && node.part.parts.length === 1
255
- || node.part.type === "leaf"
256
- ) {
257
- result = `!${serializeASTNode(node.part, 0)}`;
258
- } else {
259
- result = `!(${serializeASTNode(node.part, 0)})`;
260
- }
261
- } else if (node.type === "leaf") {
262
- currentPrecedence = 4;
263
- const value = node.matcher.value;
264
- // If the value contains special characters, wrap it in quotes
265
- if (/[\s|&!()]/.test(value)) {
266
- // Escape quotes by doubling them
267
- result = `"${value.replace(/"/g, "\"\"")}"`;
268
- } else {
269
- result = value;
270
- }
271
- } else {
272
- let unhandled: never = node;
273
- return "";
274
- }
275
-
276
- // Add parentheses if our precedence is lower than parent's
277
- if (currentPrecedence < parentPrecedence) {
278
- result = `(${result})`;
279
- }
280
- return result;
281
- }
282
-
283
- return serializeASTNode(filterSelector.expression);
284
- }
285
-
286
- // a | b => { values: ["a", "b"] }
287
- export function parseFilterable(filterable: string): Filterable {
288
- if (Array.isArray(filterable)) {
289
- filterable = filterable.join("|");
290
- }
291
- if (!filterable.trim()) return undefined;
292
- let values = filterable.split("|").map(value => value.trim());
293
- return {
294
- values,
295
- };
296
- }
297
- export function serializeFilterable(filterable: Filterable): string {
298
- if (!filterable) return "";
299
- return filterable.values.join("|");
300
- }
301
-
302
- async function main() {
303
- function assert(cond: () => boolean, message?: string) {
304
- if (!cond()) throw new Error(`Failed for ${cond.toString()}${message ? `: ${message}` : ""}`);
305
- }
306
-
307
- function testFilter(input: string, matches: string[][], nonMatches: string[][]) {
308
- console.log(`\nTesting filter: ${input}`);
309
- const filter = parseFilterSelector(input);
310
- const serialized = serializeFilterSelector(filter);
311
- console.log("Serialized:", serialized);
312
-
313
- // Test that parsing the serialized version gives same result
314
- const reparsed = parseFilterSelector(serialized);
315
- assert(() => JSON.stringify(filter) === JSON.stringify(reparsed),
316
- "Serialized form should parse to equivalent AST");
317
-
318
- // Test positive matches
319
- for (const values of matches) {
320
- assert(() => doesMatch({ values }, filter),
321
- `Should match values: [${values.join(", ")}]`);
322
- }
323
-
324
- // Test negative matches
325
- for (const values of nonMatches) {
326
- assert(() => !doesMatch({ values }, filter),
327
- `Should NOT match values: [${values.join(", ")}]`);
328
- }
329
- }
330
-
331
- // Test basic operators
332
- testFilter("a",
333
- [["a"], ["ab"], ["a", "b"]],
334
- [["b"], ["c"], []]);
335
-
336
- testFilter("a & b",
337
- [["a", "b"], ["ab", "b"], ["a", "ab"]],
338
- [["a"], ["b"], ["c", "d"], []]);
339
-
340
- testFilter("a | b",
341
- [["a"], ["b"], ["a", "b"], ["ab"]],
342
- [["c"], ["d"], []]);
343
-
344
- // Test NOT operator
345
- testFilter("!a",
346
- [["b"], ["c"], ["b", "c"]],
347
- [["a"], ["a", "b"]]);
348
-
349
- testFilter("!(a | b)",
350
- [["c"], ["d"], ["c", "d"]],
351
- [["a"], ["b"], ["a", "c"], ["b", "d"]]);
352
-
353
- // Test operator precedence
354
- testFilter("a & b | c",
355
- [["a", "b"], ["c"], ["a", "b", "c"]],
356
- [["a"], ["b"]]);
357
-
358
- testFilter("a | b & c",
359
- [["a"], ["b", "c"], ["a", "b", "c"]],
360
- [["b"], ["c"], ["b"]]);
361
-
362
- // Test complex expressions
363
- testFilter("(a | b & c) & !d",
364
- [["a"], ["b", "c"], ["a", "b", "c"]],
365
- [["a", "d"], ["b", "c", "d"], ["d"]]);
366
-
367
- // Test quoted strings and special characters
368
- testFilter("\"hello world\" & !\"good bye\"",
369
- [["hello world"], ["hello world", "hi"]],
370
- [["good bye"], ["hello world", "good bye"]]);
371
-
372
- testFilter("\"a & b\" | \"c | d\"",
373
- [["a & b"], ["c | d"], ["a & b", "e"]],
374
- [["a"], ["b"], ["c"], ["d"]]);
375
-
376
- // Test empty and undefined cases
377
- assert(() => doesMatch(undefined, undefined), "undefined should match undefined");
378
- assert(() => !doesMatch(undefined, parseFilterSelector("a")), "undefined should not match filter");
379
- assert(() => !doesMatch({ values: ["a"] }, undefined), "non-empty values should not match undefined");
380
-
381
- // Test mergeSelectors
382
- const merged = mergeSelectors([
383
- parseFilterSelector("a & b"),
384
- parseFilterSelector("c | d"),
385
- undefined,
386
- parseFilterSelector("!e")
387
- ]);
388
- assert(() => doesMatch({ values: ["a", "b"] }, merged), "should match first selector");
389
- assert(() => doesMatch({ values: ["c"] }, merged), "should match second selector");
390
- assert(() => doesMatch({ values: ["d"] }, merged), "should match second selector alternative");
391
- assert(() => !doesMatch({ values: ["e"] }, merged), "should respect NOT in merged selectors");
392
-
393
- console.log("All tests passed!");
394
- }
395
- //main().catch(console.error).finally(() => process.exit(0));