querysub 0.406.0 → 0.408.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 (55) hide show
  1. package/bin/audit-disk-values.js +7 -0
  2. package/bin/deploy-prefixes.js +7 -0
  3. package/package.json +5 -3
  4. package/src/-a-archives/archiveCache.ts +12 -9
  5. package/src/-a-auth/certs.ts +1 -1
  6. package/src/-c-identity/IdentityController.ts +9 -1
  7. package/src/-f-node-discovery/NodeDiscovery.ts +63 -10
  8. package/src/0-path-value-core/AuthorityLookup.ts +14 -4
  9. package/src/0-path-value-core/PathRouter.ts +247 -117
  10. package/src/0-path-value-core/PathRouterRouteOverride.ts +1 -1
  11. package/src/0-path-value-core/PathRouterServerAuthoritySpec.tsx +4 -2
  12. package/src/0-path-value-core/PathValueCommitter.ts +68 -31
  13. package/src/0-path-value-core/PathValueController.ts +77 -8
  14. package/src/0-path-value-core/PathWatcher.ts +46 -4
  15. package/src/0-path-value-core/ShardPrefixes.ts +6 -0
  16. package/src/0-path-value-core/ValidStateComputer.ts +20 -8
  17. package/src/0-path-value-core/hackedPackedPathParentFiltering.ts +18 -55
  18. package/src/0-path-value-core/pathValueArchives.ts +19 -8
  19. package/src/0-path-value-core/pathValueCore.ts +75 -27
  20. package/src/0-path-value-core/startupAuthority.ts +9 -9
  21. package/src/1-path-client/RemoteWatcher.ts +217 -178
  22. package/src/1-path-client/pathValueClientWatcher.ts +6 -11
  23. package/src/2-proxy/pathValueProxy.ts +2 -3
  24. package/src/3-path-functions/PathFunctionRunner.ts +3 -1
  25. package/src/3-path-functions/syncSchema.ts +6 -2
  26. package/src/4-deploy/deployGetFunctionsInner.ts +1 -1
  27. package/src/4-deploy/deployPrefixes.ts +14 -0
  28. package/src/4-deploy/edgeNodes.ts +1 -1
  29. package/src/4-querysub/Querysub.ts +17 -5
  30. package/src/4-querysub/QuerysubController.ts +21 -10
  31. package/src/4-querysub/predictionQueue.tsx +3 -0
  32. package/src/4-querysub/querysubPrediction.ts +27 -20
  33. package/src/5-diagnostics/nodeMetadata.ts +17 -0
  34. package/src/diagnostics/NodeConnectionsPage.tsx +167 -0
  35. package/src/diagnostics/NodeViewer.tsx +11 -15
  36. package/src/diagnostics/PathDistributionInfo.tsx +102 -0
  37. package/src/diagnostics/SyncTestPage.tsx +19 -8
  38. package/src/diagnostics/auditDiskValues.ts +221 -0
  39. package/src/diagnostics/auditDiskValuesEntry.ts +43 -0
  40. package/src/diagnostics/logs/IndexedLogs/LogViewer3.tsx +5 -1
  41. package/src/diagnostics/logs/TimeRangeSelector.tsx +3 -3
  42. package/src/diagnostics/logs/lifeCycleAnalysis/LifeCycleRenderer.tsx +2 -0
  43. package/src/diagnostics/managementPages.tsx +10 -1
  44. package/src/diagnostics/misc-pages/ArchiveViewer.tsx +3 -2
  45. package/src/diagnostics/pathAuditer.ts +21 -0
  46. package/src/path.ts +9 -2
  47. package/src/rangeMath.ts +41 -0
  48. package/tempnotes.txt +5 -58
  49. package/test.ts +13 -295
  50. package/src/diagnostics/benchmark.ts +0 -139
  51. package/src/diagnostics/runSaturationTest.ts +0 -416
  52. package/src/diagnostics/satSchema.ts +0 -64
  53. package/src/test/mongoSatTest.tsx +0 -55
  54. package/src/test/satTest.ts +0 -193
  55. package/src/test/test.tsx +0 -552
@@ -9,6 +9,7 @@ import { unique } from "../misc";
9
9
  import { measureFnc } from "socket-function/src/profiling/measure";
10
10
  import { getRoutingOverride, hasPrefixHash } from "./PathRouterRouteOverride";
11
11
  import { sha256 } from "js-sha256";
12
+ import { rangesOverlap, removeRange } from "../rangeMath";
12
13
 
13
14
 
14
15
  // Cases
@@ -36,23 +37,35 @@ export type AuthoritySpec = {
36
37
  excludeDefault?: boolean;
37
38
  };
38
39
 
40
+ export function debugSpec(spec: AuthoritySpec) {
41
+ return {
42
+ info: `${spec.routeStart}-${spec.routeEnd} (${spec.prefixes.length} prefixes${spec.excludeDefault ? " excluding default" : ""})`,
43
+ spec: { do: { not: { expand: { spec } } } },
44
+ };
45
+ }
46
+
47
+ function getMatchingPrefix(spec: AuthoritySpec, path: string): string | undefined {
48
+ let longestPrefix: string | undefined;
49
+ for (let prefix of spec.prefixes) {
50
+ if (path.startsWith(prefix) && prefix !== path) {
51
+ if (!longestPrefix || longestPrefix.length < prefix.length) {
52
+ longestPrefix = prefix;
53
+ }
54
+ }
55
+ }
56
+ return longestPrefix;
57
+ }
58
+
39
59
 
40
60
  export class PathRouter {
41
61
 
42
62
  public static async waitUntilReady() {
43
63
  await authorityLookup.startSyncing();
44
64
  }
45
- /** NOTE: Parent watches are a little bit special. If it's a parent watch, we always hash it, assuming the parent is a prefix. And as most of the watches are parent watches, we're usually going to do this, and so it actually is independent of the topology.
46
- - The topology is really only used for the initial sync, which will use matchesAuthoritySpec, which gets the full routing value, AND, for disk storage.
47
- */
48
- @measureFnc
49
- public static getRouteChildKey(path: string): number {
50
- let key = getLastPathPart(path);
51
- return this.getSingleKeyRoute(key);
52
- }
65
+
53
66
  // NOTE: For non-prefix values, breaking up by routes on the file system becomes complicated, and so we just all non-prefix values in the same file. However, in memory, in some places, we need route values for every single path, such as for FunctionRunner, so it can distribute the function running evenly, without overlap.
54
67
  @measureFnc
55
- private static getRouteFull(config: {
68
+ public static getRouteFull(config: {
56
69
  path: string;
57
70
  spec: AuthoritySpec;
58
71
  }): number {
@@ -61,22 +74,38 @@ export class PathRouter {
61
74
  path = hack_stripPackedPath(path);
62
75
  let override = getRoutingOverride(path);
63
76
  if (override) {
64
- if (!hasPrefixHash({ spec, prefixHash: override.prefixHash })) return -1;
77
+ if (spec.excludeDefault && !hasPrefixHash({ spec, prefixHash: override.prefixHash })) return -1;
78
+ if (override.route < spec.routeStart || override.route >= spec.routeEnd) return -1;
65
79
  return override.route;
66
80
  }
67
81
 
68
- let prefix = spec.prefixes.find(x => path.startsWith(x) && x !== path);
82
+ let prefix = getMatchingPrefix(spec, path);
69
83
  if (prefix) {
70
84
  let key = getPathIndex(path, getPathDepth(prefix));
71
85
  if (key === undefined) {
86
+ require("debugbreak")(2);
87
+ debugger;
72
88
  throw new Error(`Impossible, hash index ${getPathDepth(prefix)} is out of range for path ${path}, but it matched the prefix ${prefix}`);
73
89
  }
74
- return this.getSingleKeyRoute(key);
90
+ let route = this.getSingleKeyRoute(key);
91
+ if (route < spec.routeStart || route >= spec.routeEnd) return -1;
92
+ return route;
75
93
  }
76
94
  if (spec.excludeDefault) return -1;
77
- let hash = this.getSingleKeyRoute(path);
78
- if (hash < spec.routeStart || hash >= spec.routeEnd) return -1;
79
- return hash;
95
+ let route = this.getSingleKeyRoute(path);
96
+ if (route < spec.routeStart || route >= spec.routeEnd) return -1;
97
+ return route;
98
+ }
99
+
100
+ // Mostly for debugging
101
+ @measureFnc
102
+ public static getAllRoutes(path: string): number[] {
103
+ let routes: number[] = [];
104
+ for (let authority of authorityLookup.getTopologySync()) {
105
+ let route = this.getRouteFull({ path, spec: authority.authoritySpec });
106
+ routes.push(route);
107
+ }
108
+ return routes;
80
109
  }
81
110
 
82
111
  private static lastKeyRoute = {
@@ -86,6 +115,12 @@ export class PathRouter {
86
115
  // Takes a key which is a part of a path. Mostly used PathRouterRouteOverride, or other PathRouter helpers.
87
116
  public static getSingleKeyRoute(key: string): number {
88
117
  if (key && this.lastKeyRoute.key === key) return this.lastKeyRoute.route;
118
+ let override = getRoutingOverride(key);
119
+ if (override) {
120
+ this.lastKeyRoute.key = key;
121
+ this.lastKeyRoute.route = override.route;
122
+ return override.route;
123
+ }
89
124
  let hash = fastHash(key);
90
125
  let route = hash % (1000 * 1000 * 1000) / (1000 * 1000 * 1000);
91
126
  this.lastKeyRoute.key = key;
@@ -104,6 +139,9 @@ export class PathRouter {
104
139
  private static getPrefixHash(prefix: string): string {
105
140
  return Buffer.from(sha256(prefix), "hex").toString("base64").slice(0, 6);
106
141
  }
142
+ private static isPrefixHash(hash: string): boolean {
143
+ return hash.length === 6 && /^[a-zA-Z0-9]+$/.test(hash);
144
+ }
107
145
  private static encodeIdentifier(config: { prefixes: string[]; rangeStart: number; rangeEnd: number } | "remaining"): string {
108
146
  if (config === "remaining") return "P!REMAINING";
109
147
  let { prefixes, rangeStart, rangeEnd } = config;
@@ -125,7 +163,7 @@ export class PathRouter {
125
163
  return {
126
164
  rangeStart: parseFloat(parts[1]),
127
165
  rangeEnd: parseFloat(parts[2]),
128
- prefixHashes: parts.slice(3),
166
+ prefixHashes: parts.slice(3).filter(this.isPrefixHash),
129
167
  };
130
168
  }
131
169
 
@@ -137,26 +175,19 @@ export class PathRouter {
137
175
  // NOTE: The file size limit is 1024 bytes. But we also have our folder, etc, so we want to add enough buffer
138
176
  // - Shorter hashes means we can store more, but there's a point when the collisions make it less useful.
139
177
  const MAX_PREFIXES_PER_FILE = 50;
140
- const PREFIX_COVER_FRACTION = 0.95;
141
- const TARGET_VALUES_PER_FILE = 50 * 1000;
142
- if (values.length < TARGET_VALUES_PER_FILE) {
143
- return new Map([[this.encodeIdentifier("remaining"), values]]);
144
- }
178
+ const PREFIX_COVER_FRACTION = 0.99;
179
+ const TARGET_VALUES_PER_SHARD_GROUP = 10 * 1000 * 1000;
180
+ const TARGET_SHARD_SIZE = 50 * 1000;
181
+ const MIN_SHARD_FILE_COUNT = 10;
182
+ const SHARD_THRESHOLD = 1000;
145
183
 
146
184
  let prefixes = ourSpec.prefixes.slice();
147
185
  sort(prefixes, x => x.length);
148
- function getPrefix(path: string): string | undefined {
149
- for (let prefix of prefixes) {
150
- if (path.startsWith(prefix) && prefix !== path) return prefix;
151
- }
152
- return undefined;
153
- }
154
-
155
186
 
156
187
  // NOTE: If there are few enough path values for a prefix, we don't even need to calculate the routing hash.
157
188
  let byPrefix = new Map<string | undefined, PathValue[]>();
158
189
  for (let value of values) {
159
- let prefix = getPrefix(value.path);
190
+ let prefix = getMatchingPrefix(ourSpec, value.path);
160
191
  let values = byPrefix.get(prefix);
161
192
  if (!values) {
162
193
  values = [];
@@ -194,7 +225,7 @@ export class PathRouter {
194
225
  }
195
226
  let last = groups[groups.length - 1];
196
227
  if (
197
- last.count > 0 && last.count + prefixGroup.values.length > TARGET_VALUES_PER_FILE
228
+ last.count > 0 && last.count + prefixGroup.values.length > TARGET_VALUES_PER_SHARD_GROUP
198
229
  || last.prefixes.length >= MAX_PREFIXES_PER_FILE
199
230
  ) {
200
231
  groups.push({
@@ -210,42 +241,44 @@ export class PathRouter {
210
241
  prefixLeft -= prefixGroup.values.length;
211
242
  }
212
243
 
213
-
214
244
  let finalFiles = new Map<string, PathValue[]>();
215
245
  for (let group of groups) {
216
- if (group.prefixes.length === 1 && group.count > TARGET_VALUES_PER_FILE) {
217
- // Split by routing hash
218
- let values = group.values.flat();
219
- let splitCount = Math.ceil(values.length / TARGET_VALUES_PER_FILE);
220
- let byRouteGroup = new Map<number, PathValue[]>();
221
- let prefix = group.prefixes[0];
222
- let hashIndex = getPathDepth(prefix);
223
- for (let value of values) {
224
- let key = getPathIndex(value.path, hashIndex);
225
- if (key === undefined) {
226
- throw new Error(`Impossible, hash index ${hashIndex} is out of range for path ${value.path}, but it matched the prefix ${prefix}`);
227
- }
228
- let route = this.getSingleKeyRoute(key);
229
- let routeIndex = Math.floor(route * splitCount);
230
- let routeValues = byRouteGroup.get(routeIndex);
231
- if (!routeValues) {
232
- routeValues = [];
233
- byRouteGroup.set(routeIndex, routeValues);
234
- }
235
- routeValues.push(value);
236
- }
237
- for (let [routeIndex, routeValues] of byRouteGroup) {
238
- let rangeStart = routeIndex / splitCount;
239
- let rangeEnd = (routeIndex + 1) / splitCount;
240
- let identifier = this.encodeIdentifier({ prefixes: [prefix], rangeStart, rangeEnd });
241
- finalFiles.set(identifier, routeValues);
242
- }
243
- } else {
246
+ if (group.count < SHARD_THRESHOLD) {
244
247
  let identifier = this.encodeIdentifier({ prefixes: group.prefixes, rangeStart: 0, rangeEnd: 1 });
245
248
  finalFiles.set(identifier, group.values.flat());
249
+ continue;
250
+ }
251
+ // Split by routing hash
252
+ let values = group.values.flat();
253
+ let splitCount = Math.max(MIN_SHARD_FILE_COUNT, Math.ceil(values.length / TARGET_SHARD_SIZE));
254
+ let byRouteGroup = new Map<number, PathValue[]>();
255
+ for (let value of values) {
256
+ let route = this.getRouteFull({
257
+ path: value.path,
258
+ spec: {
259
+ nodeId: "",
260
+ prefixes: group.prefixes,
261
+ routeStart: 0,
262
+ routeEnd: 1,
263
+ }
264
+ });
265
+ let routeIndex = Math.floor(route * splitCount);
266
+ let routeValues = byRouteGroup.get(routeIndex);
267
+ if (!routeValues) {
268
+ routeValues = [];
269
+ byRouteGroup.set(routeIndex, routeValues);
270
+ }
271
+ routeValues.push(value);
272
+ }
273
+ for (let [routeIndex, routeValues] of byRouteGroup) {
274
+ let rangeStart = routeIndex / splitCount;
275
+ let rangeEnd = (routeIndex + 1) / splitCount;
276
+ let identifier = this.encodeIdentifier({ prefixes: group.prefixes, rangeStart, rangeEnd });
277
+ finalFiles.set(identifier, routeValues);
246
278
  }
247
279
  }
248
280
 
281
+ // NOTE: There could be a huge number of prefixes and we can't pack them all into one file because of the prefix limit, so this will write any remaining values.
249
282
  if (remainingValues.length > 0) {
250
283
  let identifier = this.encodeIdentifier("remaining");
251
284
  finalFiles.set(identifier, remainingValues.flat());
@@ -268,6 +301,46 @@ export class PathRouter {
268
301
  return decodeObj.rangeStart < authority.routeEnd && decodeObj.rangeEnd > authority.routeStart;
269
302
  }
270
303
 
304
+ @measureFnc
305
+ public static overlapsAuthority(authority1: AuthoritySpec, authority2: AuthoritySpec): boolean {
306
+ // TODO: This becomes complicated because of exclude default, although I feel like there has to be a way to simplify it? Eh... whatever.
307
+
308
+ // Normalize it so if only one excludes default, it's always going to be the second one.
309
+ if (authority1.excludeDefault && !authority2.excludeDefault) return this.overlapsAuthority(authority2, authority1);
310
+
311
+ let doRangesOverlap = rangesOverlap({ start: authority1.routeStart, end: authority1.routeEnd }, { start: authority2.routeStart, end: authority2.routeEnd });
312
+
313
+ // If their prefixes are identical, then it's purely a range check
314
+ if (authority1.prefixes.length === authority2.prefixes.length && authority1.prefixes.every(x => authority2.prefixes.includes(x))) {
315
+ return doRangesOverlap;
316
+ }
317
+
318
+ // If they have any prefixes which are identical and the ranges overlap, then they overlap.
319
+ if (doRangesOverlap) {
320
+ if (authority1.prefixes.some(x => authority2.prefixes.includes(x))) {
321
+ return true;
322
+ }
323
+ }
324
+ // If any of their prefixes are under the prefix and match it, then that's a match.
325
+ if (authority1.prefixes.some(x => this.matchesAuthoritySpec(authority2, x))) {
326
+ return true;
327
+ }
328
+ if (authority2.prefixes.some(x => this.matchesAuthoritySpec(authority1, x))) {
329
+ return true;
330
+ }
331
+ if (authority1.excludeDefault && authority2.excludeDefault) {
332
+ // No shared prefixes, and none of them are nested under each other, and we don't include defaults, so neither match.
333
+ return false;
334
+ }
335
+ // If their prefixes are entirely unrelated, it means they're going to hash differently, so they do overlap.
336
+ return true;
337
+ }
338
+ @measureFnc
339
+ public static getAllOverlappingAuthorities(authority: AuthoritySpec): AuthoritySpec[] {
340
+ let allAuthorities = authorityLookup.getTopologySync();
341
+ return allAuthorities.filter(x => this.overlapsAuthority(authority, x.authoritySpec)).map(x => x.authoritySpec);
342
+ }
343
+
271
344
 
272
345
 
273
346
  public static isLocalPath(path: string): boolean {
@@ -283,11 +356,12 @@ export class PathRouter {
283
356
 
284
357
 
285
358
  // NOTE: This might return overlapping specs. Presently, this is only used when we're loading our initial data, so it's fine. However, if we use this for another purpose in the future, it might cause problems. So we might need to implement a different function for that theoretical future purpose.
359
+ // NOTE: Only takes remote nodes as presently this is just used during startup.
286
360
  @measureFnc
287
361
  public static getAuthoritySources(config: {
288
362
  target: AuthoritySpec;
289
363
  preferredNodeIds?: string[];
290
- }): (AuthoritySpec & { useFullPathHash?: boolean })[] {
364
+ }): AuthoritySpec[] {
291
365
  let { target } = config;
292
366
  let allSources = authorityLookup.getTopologySync();
293
367
  allSources = allSources.filter(x => !isOwnNodeId(x.nodeId));
@@ -368,41 +442,21 @@ export class PathRouter {
368
442
  for (let source of group) {
369
443
  let s = source.routeStart;
370
444
  let e = source.routeEnd;
371
- // Find if it overlaps any missing range, and if so, cut out that part of the missing change, and add the nodeId to usedNodeIds
372
- for (let i = missingRanges.length - 1; i >= 0; i--) {
373
- let missingRange = missingRanges[i];
374
- if (s >= missingRange.end || e <= missingRange.start) continue;
375
- let startTaken = Math.max(missingRange.start, s);
376
- let endTaken = Math.min(missingRange.end, e);
377
- // NOTE: It's not ideal that we might have to fragment one node ID between multiple requests. However, in practice, there shouldn't be much fragmentation here. The ranges that our nodes are breaking down by should be consistent, so there's actually no overlap or subsets.
445
+ let { removedRanges } = removeRange(missingRanges, { start: s, end: e });
446
+ for (let removedRange of removedRanges) {
378
447
  usedParts.push({
379
448
  nodeId: source.nodeId,
380
- routeStart: startTaken,
381
- routeEnd: endTaken,
449
+ routeStart: removedRange.start,
450
+ routeEnd: removedRange.end,
382
451
  prefixes: target.prefixes,
383
452
  excludeDefault: target.excludeDefault,
384
- useFullPathHash,
385
453
  });
386
- missingRanges.splice(i, 1);
387
- // Add back the parts we didn't overlap
388
- if (missingRange.start < s) {
389
- missingRanges.push({
390
- start: missingRange.start,
391
- end: s,
392
- });
393
- }
394
- if (missingRange.end > e) {
395
- missingRanges.push({
396
- start: e,
397
- end: missingRange.end,
398
- });
399
- }
400
454
  }
401
455
  if (missingRanges.length === 0) break;
402
456
  }
403
457
  if (missingRanges.length === 0) {
404
458
  if (!hashesSameAsTarget(group[0])) {
405
- console.warn(`Found a cohesive group that doesn't hash the same way we do. Expanding our range to be the full range of the group. This will be slower than it needs to.`);
459
+ console.warn(`Could only match a cohesive group that doesn't hash the same way we do. Expanding our range to be the full range of the group. This will be slower than it needs to, and might cause issues that result in only partial synchronization. This should be fixable with a deploy.`, config.target);
406
460
  }
407
461
  return usedParts;
408
462
  }
@@ -414,67 +468,141 @@ export class PathRouter {
414
468
  }
415
469
 
416
470
 
471
+ // NOTE: The returned nodes are guaranteed to hash in the same way (either all child key hashing for the path children, or all path hashing). This is required, otherwise it would mean if you take a single child path, It might have two different routing values depending on which node it matches, which means even if those ranges don't overlap, different routing values mean it could match two ranges, which is impossible and would break things (and it would also mean there would be values that wouldn't match anything, which I guess is even worse).
417
472
  @measureFnc
418
473
  public static getChildReadNodes(path: string, config?: {
419
474
  preferredNodeIds?: string[];
475
+ onlyOwnNodes?: boolean;
420
476
  }): {
421
- // By default we hash the key directly under the path. However, If that is not how our nodes were sharded, as in they didn't use it as a prefix, then they will be sharded by hashing the full path, and so when we receive the data, we need to keep this in mind and filter the data based on the full path hash.
422
- useFullPathHash?: boolean;
423
-
424
477
  // NOTE: If at all possible, we will cover all ranges. Node of the returned nodes will be redundant.
425
478
  // - Sorted by range.start
426
479
  nodes: {
427
480
  nodeId: string;
481
+ authoritySpec: AuthoritySpec;
428
482
  // The range of hashes this node owns, for the child keys of path
429
483
  // (If the node doesn't restrict the range, it will just be { start: 0, end: 1 })
430
484
  range: { start: number; end: number };
431
485
  }[];
432
486
  } {
433
- if (this.isSelfAuthority(path)) {
434
- return { nodes: [{ nodeId: getOwnNodeId(), range: { start: 0, end: 1 } }] };
435
- }
487
+ let preferredNodeIds = new Set(config?.preferredNodeIds ?? []);
436
488
 
437
489
  // If a prefix is a parent of path, then it is the same as matching just the path directly
438
490
  // (If our prefix directly equals one of the other matches, then it's more complicated, As then, the child keys of path are what is hashed, and so all the children will have different routes, so we might match multiple nodes. The same thing if we're matching the remaining case, in which case it's a full path hash, so the child key matters, and again, different routes).
439
491
  // - The different route case is how the FuntionRunner works, and without it large databases couldn't run functions. However, most applications won't directly use it.
440
- let allSources = authorityLookup.getTopologySync();
441
- allSources = allSources.filter(x => !isOwnNodeId(x.nodeId));
442
- let nestedMatches = allSources.filter(x =>
443
- x.authoritySpec.prefixes.some(y => path.startsWith(y) && y !== path)
444
- && this.matchesAuthoritySpec(x.authoritySpec, path)
445
- );
492
+ let allSources = config?.onlyOwnNodes ? [{ nodeId: getOwnNodeId(), authoritySpec: authorityLookup.getOurSpec() }] : authorityLookup.getTopologySync();
493
+ // Prefer our own node
494
+ sort(allSources, x => isOwnNodeId(x.nodeId) ? -1 : 1);
495
+
496
+
497
+ // Direct prefixes always take priority, as almost everything is under a prefix anyways...
498
+
499
+
500
+ // Direct prefix. This happens for things like calls and functions, it requires more advanced routing as it means we're going to route between multiple servers, but... it is important
501
+ let hasPrefix = allSources.filter(x => x.authoritySpec.prefixes.some(y => y === path)).map(x => x.authoritySpec);
502
+ if (hasPrefix.length > 0) {
503
+ shuffle(hasPrefix, Math.random());
504
+ sort(hasPrefix, x => preferredNodeIds.has(x.nodeId) ? -1 : 1);
505
+
506
+ let missingRanges: { start: number; end: number }[] = [{
507
+ start: 0,
508
+ end: 1,
509
+ }];
510
+ let usedParts: {
511
+ nodeId: string;
512
+ authoritySpec: AuthoritySpec;
513
+ range: { start: number; end: number };
514
+ }[] = [];
515
+ for (let source of hasPrefix) {
516
+ let s = source.routeStart;
517
+ let e = source.routeEnd;
518
+ let { removedRanges } = removeRange(missingRanges, { start: s, end: e });
519
+ // NOTE: It's not ideal that we might have to fragment one node ID between multiple requests. However, in practice, there shouldn't be much fragmentation here. The ranges that our nodes are breaking down by should be consistent, so there's actually no overlap or subsets.
520
+ for (let removedRange of removedRanges) {
521
+ usedParts.push({
522
+ nodeId: source.nodeId,
523
+ range: removedRange,
524
+ authoritySpec: source,
525
+ });
526
+ }
527
+ if (missingRanges.length === 0) break;
528
+ }
529
+ if (missingRanges.length === 0) {
530
+ return { nodes: usedParts };
531
+ }
532
+ }
533
+
534
+ let nestedMatches = allSources.filter(x => {
535
+ // There's nested prefixes, so if we match any prefix explicitly, we can't just take one of the previous prefixes because that isn't how the hashing will work.
536
+ // - This happens if it's a direct match, but one of the shards is down, in which case we can't get a full match.
537
+ if (x.authoritySpec.prefixes.some(y => y === path)) return false;
538
+
539
+ // If our path, which we're going to read the children of, is the child of another path, then it means in that other path, the child key will be known to us constant, and so we're going to match exactly one authority.
540
+ return (
541
+ x.authoritySpec.prefixes.some(y => path.startsWith(y) && y !== path)
542
+ && this.matchesAuthoritySpec(x.authoritySpec, path)
543
+ );
544
+ });
446
545
  if (nestedMatches.length > 0) {
447
546
  shuffle(nestedMatches, Math.random());
448
- let preferredNodeIds = new Set(config?.preferredNodeIds ?? []);
449
547
  sort(nestedMatches, x => preferredNodeIds.has(x.nodeId) ? -1 : 1);
548
+ sort(allSources, x => isOwnNodeId(x.nodeId) ? -1 : 1);
450
549
  return {
451
- nodes: nestedMatches.map(x => ({
550
+ // Only need to take the first match. Our path is picked by the prefix, and the prefix only hashes the direct child, and we're more deeply nested than that, which means... the route for all of our children will be identical, so this node matches all of our children.
551
+ nodes: nestedMatches.slice(0, 1).map(x => ({
452
552
  nodeId: x.nodeId,
453
- // NOTE: Our path is picked by the prefix, and the prefix only hashes the direct child, and we're more deeply nested than that, which means... the route for all of our children will be identical, so this node matches all of our children.
553
+ authoritySpec: x.authoritySpec,
454
554
  range: { start: 0, end: 1 },
455
555
  })),
456
556
  };
457
557
  }
458
558
 
459
- let fullSources = this.getAuthoritySources({
460
- target: {
461
- nodeId: "",
462
- prefixes: [path],
463
- routeStart: 0,
464
- routeEnd: 1,
465
- excludeDefault: true,
466
- },
467
- preferredNodeIds: config?.preferredNodeIds,
559
+ // If we are not under any prefixes of it, then it will be a full path hash
560
+ let fullPathMatches = allSources.filter(x => {
561
+ return !x.authoritySpec.prefixes.some(y => path.startsWith(y) && y !== path) && !x.authoritySpec.excludeDefault;
468
562
  });
469
- if (fullSources.length === 0) return { nodes: [] };
563
+ // Same as prefix matches. Not preferred, and not preferred over being under a prefix, but required for some root data, or data with no prefixes.
564
+ if (fullPathMatches.length > 0) {
565
+ shuffle(fullPathMatches, Math.random());
566
+ sort(fullPathMatches, x => preferredNodeIds.has(x.nodeId) ? -1 : 1);
567
+ sort(allSources, x => isOwnNodeId(x.nodeId) ? -1 : 1);
568
+ let missingRanges: { start: number; end: number }[] = [{
569
+ start: 0,
570
+ end: 1,
571
+ }];
572
+ let usedParts: {
573
+ nodeId: string;
574
+ authoritySpec: AuthoritySpec;
575
+ range: { start: number; end: number };
576
+ }[] = [];
577
+ for (let source of fullPathMatches) {
578
+ let s = source.authoritySpec.routeStart;
579
+ let e = source.authoritySpec.routeEnd;
580
+ let { removedRanges } = removeRange(missingRanges, { start: s, end: e });
581
+ // NOTE: It's not ideal that we might have to fragment one node ID between multiple requests. However, in practice, there shouldn't be much fragmentation here. The ranges that our nodes are breaking down by should be consistent, so there's actually no overlap or subsets.
582
+ for (let removedRange of removedRanges) {
583
+ usedParts.push({
584
+ nodeId: source.nodeId,
585
+ authoritySpec: source.authoritySpec,
586
+ range: removedRange,
587
+ });
588
+ }
589
+ if (missingRanges.length === 0) break;
590
+ }
591
+ if (missingRanges.length === 0) {
592
+ return { nodes: usedParts };
593
+ }
594
+ }
470
595
 
471
- return {
472
- useFullPathHash: fullSources.some(x => x.useFullPathHash),
473
- nodes: fullSources.map(x => ({
474
- nodeId: x.nodeId,
475
- range: { start: x.routeStart, end: x.routeEnd },
476
- })),
477
- };
596
+
597
+
598
+
599
+ if (!config?.onlyOwnNodes) {
600
+ // TODO: We could maybe match a partial match. However, even that is suspect. The site being partially broken is almost worse than it being completely broken. We should just get ALL the shards running again...
601
+
602
+ // NOTE: We *could* actually synchronize it even if it doesn't have a prefix shard as we can fall back to just the full path sharding. However, it becomes very complicated if we want a specific range, and then it becomes complicated if it then switches to prefix hashing (With the nodes that were using the full path hashing slowly going away). AND... key synchronization IS slow, so it's good to discourage it in general.
603
+ console.error(`Want to sync a prefix which is not under an existing prefix, nor equal to a prefix. 1) The servers are down. 2) Don't access the .keys() 3) call addRoutingPrefixForDeploy to add a route/parent route explicitly (as is done in PathFunctionRunner.ts). Path: ${JSON.stringify(path)}`, { path, allSources });
604
+ }
605
+ return { nodes: [] };
478
606
  }
479
607
 
480
608
 
@@ -538,3 +666,5 @@ export class PathRouter {
538
666
 
539
667
  }
540
668
 
669
+
670
+ (globalThis as any).PathRouter = PathRouter;
@@ -22,7 +22,7 @@ let keySpecialIdentifier = (
22
22
  export function createRoutingOverrideKey(config: {
23
23
  originalKey: string;
24
24
  routeKey: string;
25
- // This is not the prefix that it has to match. This is the prefix we remap it to. And so anything which matches this remap prefix will match this routing key.
25
+ // This is the prefix it has the equivalent of. We need this, so if something excludes default, it doesn't automatically get every routing overridden value.
26
26
  remappedPrefix: string;
27
27
  }) {
28
28
  let { originalKey, routeKey, remappedPrefix } = config;
@@ -55,13 +55,15 @@ export function getEmptyAuthoritySpec(): AuthoritySpec {
55
55
  routeStart: -1,
56
56
  routeEnd: -1,
57
57
  prefixes: [],
58
+ excludeDefault: true,
58
59
  };
59
60
  }
60
- export function getAllAuthoritySpec(): AuthoritySpec {
61
+ export async function getAllAuthoritySpec(): Promise<AuthoritySpec> {
62
+ let prefixes = await getShardPrefixes();
61
63
  return {
62
64
  nodeId: "",
63
65
  routeStart: 0,
64
66
  routeEnd: 1,
65
- prefixes: [],
67
+ prefixes: prefixes,
66
68
  };
67
69
  }