querysub 0.429.0 → 0.430.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.429.0",
3
+ "version": "0.430.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",
@@ -78,6 +78,8 @@ interface DataSettings {
78
78
  undefined
79
79
  // Split strings into multiple buffers
80
80
  | 1
81
+ // Add transactionPaths support
82
+ | 2
81
83
  );
82
84
  }
83
85
 
@@ -92,6 +94,7 @@ interface PathValueStructure {
92
94
  source?: string;
93
95
  lockCount: number;
94
96
  updateCount: number;
97
+ transactionPaths?: string[];
95
98
  // IMPORTANT! All of these values must be explicitly copied to PathValue
96
99
  // (we don't spread it), so if you add a new value here, make sure to
97
100
  // update places that use these values (search for canGCValue in this file).
@@ -264,6 +267,69 @@ class PathValueSerializer {
264
267
  return readLocks;
265
268
  }
266
269
 
270
+ @measureFnc
271
+ private transactionPathsWrite(writer: Writer, transactionPaths: (string[] | undefined)[]) {
272
+ let transactionPathsIndexes = new Map<string[] | undefined, number>();
273
+ for (let pathArray of transactionPaths) {
274
+ if (transactionPathsIndexes.has(pathArray)) continue;
275
+ if (!pathArray || pathArray.length === 0) {
276
+ transactionPathsIndexes.set(pathArray, -1);
277
+ continue;
278
+ }
279
+ transactionPathsIndexes.set(pathArray, transactionPathsIndexes.size);
280
+ }
281
+
282
+ let uniquePathArrays = Array.from(transactionPathsIndexes.keys());
283
+ writer.writeFloat64(uniquePathArrays.length);
284
+ for (let pathArray of uniquePathArrays) {
285
+ if (!pathArray || pathArray.length === 0) {
286
+ writer.writeFloat64(0);
287
+ } else {
288
+ writer.writeFloat64(pathArray.length);
289
+ for (let pathHash of pathArray) {
290
+ writer.writeString(pathHash);
291
+ }
292
+ }
293
+ }
294
+
295
+ let indexes = transactionPaths.map(x => transactionPathsIndexes.get(x)!);
296
+ writer.writeBuffer(asBuffer(new Int32Array(indexes)));
297
+ }
298
+
299
+ @measureFnc
300
+ private transactionPathsRead(reader: Reader): (string[] | undefined)[] {
301
+ let uniquePathArrayCount = reader.readFloat64();
302
+ let uniquePathArrays: (string[] | undefined)[] = [];
303
+ for (let i = 0; i < uniquePathArrayCount; i++) {
304
+ let pathCount = reader.readFloat64();
305
+ if (pathCount === 0) {
306
+ uniquePathArrays.push(undefined);
307
+ } else {
308
+ let paths: string[] = [];
309
+ for (let j = 0; j < pathCount; j++) {
310
+ paths.push(reader.readString());
311
+ }
312
+ uniquePathArrays.push(paths);
313
+ }
314
+ }
315
+
316
+ let indexesBuf = reader.readBuffer();
317
+ let indexes: number[] = [];
318
+ for (let i = 0; i < indexesBuf.length; i += 4) {
319
+ indexes.push(indexesBuf.readInt32LE(i));
320
+ }
321
+ let transactionPaths = Array.from(indexes).map(index => {
322
+ if (index === -1) return undefined;
323
+ if (index < 0 || index >= uniquePathArrays.length) {
324
+ debugbreak(2);
325
+ debugger;
326
+ throw new Error(`Invalid transaction paths index ${index}, expected 0 <= index < ${uniquePathArrays.length}`);
327
+ }
328
+ return uniquePathArrays[index];
329
+ });
330
+ return transactionPaths;
331
+ }
332
+
267
333
 
268
334
  private async valuesWrite(writer: Writer, valuesGroups: PathValue[][]) {
269
335
  let totalLength = valuesGroups.reduce((total, x) => total + x.length, 0);
@@ -389,7 +455,7 @@ class PathValueSerializer {
389
455
  singleBuffer?: boolean;
390
456
  stripSource?: boolean;
391
457
  }): Promise<Buffer[]> {
392
- const version = 1;
458
+ const version = 2;
393
459
  let settings: DataSettings = {
394
460
  valueCount: values.length,
395
461
  noLocks: config?.noLocks,
@@ -479,6 +545,7 @@ class PathValueSerializer {
479
545
  // TODO: Support breaking locks up
480
546
  this.readLocksWrite(writer, values.map(x => x.locks));
481
547
  }
548
+ this.transactionPathsWrite(writer, values.map(x => x.transactionPaths));
482
549
  await this.valuesWrite(writer, valueGroups);
483
550
 
484
551
  if (currentBufferPos > 0) {
@@ -560,6 +627,8 @@ class PathValueSerializer {
560
627
  // All values become undefined. If also skipping strings OR if you have large values,
561
628
  // this can make deserialization significantly faster.
562
629
  skipValues?: boolean;
630
+ // Skip deserializing transaction paths.
631
+ skipTransactionPaths?: boolean;
563
632
  }): Promise<PathValue[]> {
564
633
  buffers = buffers.slice();
565
634
 
@@ -684,6 +753,13 @@ class PathValueSerializer {
684
753
  let partialValues = this.pathValuesRead(reader, settings.valueCount);
685
754
  let readLocks = settings.noLocks ? Array(settings.valueCount).fill([]) : this.readLocksRead(reader);
686
755
 
756
+ let transactionPaths: (string[] | undefined)[] = [];
757
+ // Skip transactions if they at all seem like they want to skip anything, as transactions are not very important. And how are you going to use transaction paths if you don't have values or if you don't have strings?
758
+ // - Oh, but I think skip values just makes the values lazy. So that's normal. We still want transactions even if the values are lazy.
759
+ if (version > 1 && !config?.skipStrings && !config?.skipTransactionPaths) {
760
+ transactionPaths = this.transactionPathsRead(reader);
761
+ }
762
+
687
763
  let values: unknown[] = [];
688
764
  let valuesAreLazy: boolean[] = [];
689
765
  if (!config?.skipValues) {
@@ -710,6 +786,7 @@ class PathValueSerializer {
710
786
  source: partialValue.source,
711
787
  lockCount: partialValue.lockCount,
712
788
  updateCount: partialValue.updateCount,
789
+ transactionPaths: transactionPaths[i],
713
790
  });
714
791
  }
715
792
 
@@ -11,6 +11,7 @@ import { getRoutingOverride, hasPrefixHash } from "./PathRouterRouteOverride";
11
11
  import { sha256 } from "js-sha256";
12
12
  import { rangesOverlap, removeRange } from "../rangeMath";
13
13
  import { decodeParentFilter } from "./hackedPackedPathParentFiltering";
14
+ import { getBufferInt } from "../bits";
14
15
 
15
16
 
16
17
  // Cases
@@ -190,7 +191,9 @@ export class PathRouter {
190
191
  this.lastKeyRoute.route = override.route;
191
192
  return override.route;
192
193
  }
193
- let hash = fastHash(key);
194
+ //let hash = fastHash(key);
195
+ // NOTE: Having an even distribution is more important than hashing quickly, and it makes testing a lot easier.
196
+ let hash = getBufferInt(Buffer.from(sha256(key), "hex"));
194
197
  let route = hash % (1000 * 1000 * 1000) / (1000 * 1000 * 1000);
195
198
  this.lastKeyRoute.key = key;
196
199
  this.lastKeyRoute.route = route;
@@ -9,7 +9,7 @@ import { pathValueSerializer } from "../-h-path-value-serialize/PathValueSeriali
9
9
  import { pathValueCommitter } from "./PathValueCommitter";
10
10
  import { auditLog, isDebugLogEnabled } from "./auditLogs";
11
11
  import { debugNodeId, debugNodeThread } from "../-c-identity/IdentityController";
12
- import { isDiskAudit } from "../config";
12
+ import { getSlowdown, isDiskAudit } from "../config";
13
13
  import { decodeNodeId } from "../-a-auth/certs";
14
14
  import { areNodeIdsEqual, isOwnNodeId } from "../-f-node-discovery/NodeDiscovery";
15
15
  import { getNodeIdIP } from "socket-function/src/nodeCache";
@@ -20,6 +20,7 @@ import { LZ4 } from "../storage/LZ4";
20
20
  import { Time } from "./pathValueCore";
21
21
  import { encodeCborx, decodeCborx } from "../misc/cloneHelpers";
22
22
  import { debugGetAllCallFactories } from "socket-function/src/nodeCache";
23
+ import { delay } from "socket-function/src/batching";
23
24
  export { pathValueCommitter };
24
25
 
25
26
  // ONLY returns non-canGCValues that are valid
@@ -127,6 +128,11 @@ export class PathValueControllerBase {
127
128
  let callerId = SocketFunction.getCaller().nodeId;
128
129
  let { initialCreation, valueBuffers } = config;
129
130
 
131
+ let slowdown = getSlowdown();
132
+ if (slowdown) {
133
+ await delay(slowdown);
134
+ }
135
+
130
136
  let values: PathValue[] = [];
131
137
  if (valueBuffers) {
132
138
  values = await pathValueSerializer.deserialize(valueBuffers);
@@ -1,5 +1,5 @@
1
1
  import { SocketFunction } from "socket-function/SocketFunction";
2
- import { lazy } from "socket-function/src/caching";
2
+ import { cacheLimited, lazy } from "socket-function/src/caching";
3
3
  import { addEpsilons, minusEpsilon } from "../bits";
4
4
  import { logErrors } from "../errors";
5
5
  import { appendToPathStr, getParentPathStr, getPathDepth, getPathIndexAssert, hack_getPackedPathSuffix, hack_setPackedPathSuffix, hack_stripPackedPath } from "../path";
@@ -26,6 +26,8 @@ import { onPathInteracted } from "../diagnostics/pathAuditerCallback";
26
26
  import { decodeParentFilter, filterChildPathsBase } from "./hackedPackedPathParentFiltering";
27
27
  import { isDiskAudit } from "../config";
28
28
  import { removeRange } from "../rangeMath";
29
+ import { remoteWatcher } from "../1-path-client/RemoteWatcher";
30
+ import { sha256 } from "js-sha256";
29
31
 
30
32
  setImmediate(async () => {
31
33
  // Import everything will dynamically import, so the client side can tell that it's required.
@@ -291,6 +293,10 @@ export type PathValue = {
291
293
  /** Used internally by archive management services. */
292
294
  updateCount?: number;
293
295
 
296
+ /** This is the hashes of all the paths that were written at the same time, as in that have the same time as this path value.
297
+ - By using this, you can determine if you've only partially read a transaction by looking at the paths which you have read and comparing them against these path values. And if you see that some of them have a time equal to our time, but some of them have a time before our time, it means that you are missing part of our transaction. This allows the client to wait for the rest of the transaction to be received. */
298
+ transactionPaths?: string[];
299
+
294
300
  // #endregion
295
301
  };
296
302
 
@@ -374,6 +380,10 @@ export function getLockHash(lock: ReadLock) {
374
380
  return timeHash(lock.startTime) + "_" + timeHash(lock.endTime) + "_" + lock.path;
375
381
  }
376
382
 
383
+ export const hashPathForTransaction = cacheLimited(1000 * 1000, function hashPathForTransaction(path: string): string {
384
+ return Buffer.from(sha256(path), "hex").toString("base64").slice(0, 8);
385
+ });
386
+
377
387
  export function debugPathValuePath(pathValue: { time: Time; path: string }): string {
378
388
  // Log the raw path, so it can show up in searches
379
389
  return `(${debugTime(pathValue.time)}) ${pathValue.path}`;
@@ -582,7 +592,7 @@ class AuthorityPathValueStorage {
582
592
  }
583
593
  return true;
584
594
  }
585
- public isSynced(path: string, ignoreParentSync = false) {
595
+ public isSynced(path: string) {
586
596
  if (PathRouter.isSelfAuthority(path)) return true;
587
597
 
588
598
  if (this.isSyncedCache.has(path)) return true;
@@ -597,24 +607,42 @@ class AuthorityPathValueStorage {
597
607
  return true;
598
608
  }
599
609
 
600
- // NOTE: Parent syncs sends child values. This helps with speed, and is used in RemoteWatcher
601
- // to prevent extra sends. HOWEVER, we also have to accept a value is synced if only the
602
- // parent is synced
603
- // - See PathWatcher.watchPath, where when !noInitialTrigger (aka, initialTrigger),
604
- // we get all paths via authorityStorage.getPathsFromParent, and send their values.
605
- if (!ignoreParentSync && this.isParentSynced(getParentPathStr(path))) {
606
- return true;
610
+ {
611
+ // We are always going to prefer our own route. So if we can hash with our own route and match our route range, then it's going to be picked 100% of the time. And so it's always going to be synced.
612
+ let ourSpec = authorityLookup.getOurSpec();
613
+ let ourRoute = PathRouter.getRouteFull({ path, spec: ourSpec });
614
+ if (ourSpec.routeStart <= ourRoute && ourRoute < ourSpec.routeEnd) {
615
+ this.isSyncedCache.add(path);
616
+ return true;
617
+ }
618
+ }
619
+
620
+ let remoteWatchRoute = remoteWatcher.getRemoteParentWatchRoute(path);
621
+ if (remoteWatchRoute !== undefined) {
622
+ let parent = getParentPathStr(path);
623
+ let ranges = this.parentsSynced.get(parent);
624
+ // NOTE: We can't cache this because when we stop watching the parent, the remote watch will remove it, which will cause us to be no longer synced. However, it's not going to know to go into our synced cache and remove it.
625
+ if (ranges === true) return true;
626
+ if (ranges) {
627
+ // NOTE: This code is unfortunately fragile. It depends on the ranges that we're watching on the remote to match up with the synced ranges that we're keeping track of.
628
+ // HOWEVER I think it works fine because if we disconnect from a remote and so we stop watching them, the remote watcher will remove it, and so it will no longer match the route. And then it'll try to find something that matches that previous route exactly. Perhaps redoing all of the remote watchers if to in order to find something that hashes consistently. So I think it should be safe.
629
+ for (let range of ranges) {
630
+ if (range.start <= remoteWatchRoute && remoteWatchRoute < range.end) {
631
+ return true;
632
+ }
633
+ }
634
+ }
607
635
  }
608
636
 
609
637
  return false;
610
638
  }
611
- public isParentSynced(path: string) {
612
- let originalPath = path;
613
- let range = decodeParentFilter(path);
614
- path = hack_stripPackedPath(path);
615
- if (PathRouter.isLocalPath(path)) return true;
639
+ public isParentSynced(parentPath: string) {
640
+ let originalPath = parentPath;
641
+ let range = decodeParentFilter(parentPath);
642
+ parentPath = hack_stripPackedPath(parentPath);
643
+ if (PathRouter.isLocalPath(parentPath)) return true;
616
644
 
617
- let synced = this.parentsSynced.get(originalPath) || this.parentsSynced.get(path);
645
+ let synced = this.parentsSynced.get(originalPath) || this.parentsSynced.get(parentPath);
618
646
  if (synced === true) return true;
619
647
  // See if the ranges received so far cover the requested range. If we only request a partial range, then this will always be the case. We'll never fully synchronize the path.
620
648
  if (synced && range) {
@@ -143,6 +143,19 @@ export class RemoteWatcher {
143
143
  return x.start <= route && route < x.end;
144
144
  });
145
145
  }
146
+ public getRemoteParentWatchRoute(path: string) {
147
+ let parentPath = getParentPathStr(path);
148
+ let watchObj = this.remoteWatchParents2.get(parentPath);
149
+ if (!watchObj) return undefined;
150
+ // NOTE: PathRouter.getChildReadNodes PROMISES That all of the nodes will hash in the same way, and if they all hash in the same way, and they have different ranges, then every path will uniquely map to a single value.
151
+ for (let range of watchObj.ranges) {
152
+ let route = PathRouter.getRouteFull({ path, spec: range.authoritySpec });
153
+ if (range.start <= route && route < range.end) {
154
+ return route;
155
+ }
156
+ }
157
+ return undefined;
158
+ }
146
159
 
147
160
 
148
161
  public debugIsWatchingPath(path: string) {
@@ -20,7 +20,7 @@ import { logErrors } from "../errors";
20
20
  import { getParentPathStr, getPathFromStr, hack_stripPackedPath } from "../path";
21
21
  import { measureBlock, measureFnc } from "socket-function/src/profiling/measure";
22
22
  import { pathValueCommitter } from "../0-path-value-core/PathValueController";
23
- import { PathValue, Value, getNextTime, Time, ReadLock, MAX_CHANGE_AGE, authorityStorage, getCreatorId, WatchConfig } from "../0-path-value-core/pathValueCore";
23
+ import { PathValue, Value, getNextTime, Time, ReadLock, MAX_CHANGE_AGE, authorityStorage, getCreatorId, WatchConfig, hashPathForTransaction } from "../0-path-value-core/pathValueCore";
24
24
  import { pathWatcher } from "../0-path-value-core/PathWatcher";
25
25
  import { blue, green, red } from "socket-function/src/formatting/logColors";
26
26
  import { runInfinitePoll } from "socket-function/src/batching";
@@ -681,6 +681,8 @@ export class ClientWatcher {
681
681
  /** Causes us to NOT ACTUALLY write values, but just return what we would write, if !dryRun */
682
682
  dryRun?: boolean;
683
683
  source?: string;
684
+ // If we read values in the past, not taking the latest value, we need to be told about this because this changes how we will be creating the transaction.
685
+ historyBasedReads?: boolean;
684
686
  }
685
687
  ): PathValue[] {
686
688
  const { values, locks, eventPaths } = config;
@@ -704,6 +706,13 @@ export class ClientWatcher {
704
706
  source = debugName;
705
707
  }
706
708
 
709
+ // Create transaction paths array - hash all paths and reuse the same array for all PathValues
710
+ // - Sorting doesn't matter. I guess it just makes it so it's technically going to encode more efficiently... or something?
711
+ let transactionPaths: string[] | undefined = Array.from(values.keys()).map(hashPathForTransaction).sort();
712
+ if (config.historyBasedReads) {
713
+ transactionPaths = undefined;
714
+ }
715
+
707
716
  let pathValues: PathValue[] = [];
708
717
  for (let [path, value] of values) {
709
718
  let valueIsEvent = event;
@@ -711,6 +720,8 @@ export class ClientWatcher {
711
720
  valueIsEvent = true;
712
721
  }
713
722
  let isTransparent = value === undefined || config.forcedUndefinedWrites?.has(path);
723
+ // IMPORTANT! With the exception of epoch values, this is really the only place that actually creates path values. All path values will be created on this specific line.
724
+ // - In the future, we could create PathValues in different places. However, that would require PathValue generation that doesn't use ClientWatcher, which would be complex, and probably not worth it. Manually creating path values. While technically faster, the only cases in which you would need that speed would then overload the path value server and all the watchers, and so they're not really viable. Not to mention the fact that using ClientWatcher and Proxy PathWatcher are very fast anyway and can easily create probably hundreds of thousands of writes a second.
714
725
  let pathValue: PathValue = pathValueCtor({
715
726
  path,
716
727
  valid: true,
@@ -722,6 +733,7 @@ export class ClientWatcher {
722
733
  event: valueIsEvent,
723
734
  isTransparent,
724
735
  source,
736
+ transactionPaths,
725
737
  });
726
738
  pathValues.push(pathValue);
727
739
  }
@@ -234,6 +234,17 @@ export interface WatcherOptions<Result> {
234
234
  /** AKA, isAllSynced is always true. Useful for cases when we want to show partially loaded values. */
235
235
  commitAllRuns?: boolean;
236
236
 
237
+
238
+ /** Path values contain some information about all the paths that were written at once, which we call a transaction. We can use this to detect when we are missing part of a transaction and wait until we receive that part of the transaction.
239
+ - If we don't wait, our writes will get rejected, and so the system will still be valid, however, there are a few reasons we want to avoid this:
240
+ - If we are trying to trigger external data, such as API calls, we want to reduce raise conditions. The values still might be rejected entirely, however, at least there won't be a partial read, whereas partial reads are significantly more common than rejections.
241
+ - Rejections can cause view stuttering, where the view goes to a bad state before it goes to a good state. If we just wait for the transaction to complete before trying to commit a value, such as in function runner, we can remove this stutter.
242
+ - Waiting can be more efficient than committing it and having to reject it, especially if something else depends on our value and then it has to be rejected and rerun.
243
+ - Not all writers will automatically rerun on rejections. They should be able to handle rejections. However, it might cause downtime and it might have to put the system into a partial recovery state. It's better to avoid this and just wait for the full transaction.
244
+ - Because this is so useful and so cheap to calculate, this flag will be set by default by most commit-style templates. It should be safe to set in watch style templates, However, it's not on by default there as, technically speaking, waiting for incomplete transactions will cause higher latency to render, And by default we prefer to render fast and usually get it right rather than render slow and always render the right thing.
245
+ */
246
+ waitForIncompleteTransactions?: boolean;
247
+
237
248
  // NOTE: The reason there isn't throttle support here is very frequently when you want to throttle one component rendering, it's because you have many components. So you actually want to throttle many components and have them throttle in conjunction with each other, which results in the logic becoming complicated.
238
249
  // - But maybe we should support the single throttle case anyways?
239
250
  }
@@ -1176,6 +1187,9 @@ export class PathValueProxyWatcher {
1176
1187
  }
1177
1188
  };
1178
1189
  watcher.hasAnyUnsyncedAccesses = () => {
1190
+ if (watcher.options.waitForIncompleteTransactions) {
1191
+ waitIfReceivedIncompleteTransaction();
1192
+ }
1179
1193
  return (
1180
1194
  watcher.pendingUnsyncedAccesses.size > 0
1181
1195
  || watcher.pendingUnsyncedParentAccesses.size > 0
@@ -1474,6 +1488,8 @@ export class PathValueProxyWatcher {
1474
1488
  }
1475
1489
  watcher.countSinceLastFullSync++;
1476
1490
  if (watcher.countSinceLastFullSync > 10) {
1491
+ require("debugbreak")(2);
1492
+ debugger;
1477
1493
  console.warn(`Watcher ${watcher.debugName} has been unsynced for ${watcher.countSinceLastFullSync} times. This is fine, but maybe optimize it. Why is it cascading?`, watcher.lastUnsyncedAccesses, watcher.lastUnsyncedParentAccesses, watcher.options.watchFunction);
1478
1494
  }
1479
1495
  if (watcher.countSinceLastFullSync > 500) {
@@ -1552,6 +1568,15 @@ export class PathValueProxyWatcher {
1552
1568
 
1553
1569
  if (watcher.pendingWrites.size > 0) {
1554
1570
  let locks: ReadLock[] = [];
1571
+ let historyBasedReads = false;
1572
+ for (let time of watcher.pendingAccesses.keys()) {
1573
+ if (time === undefined) continue;
1574
+ // If we're reading from a specific time, and it isn't just our write time, then we're reading in the past.
1575
+ if (compareTime(time, writeTime) !== 0) {
1576
+ historyBasedReads = true;
1577
+ break;
1578
+ }
1579
+ }
1555
1580
 
1556
1581
  if (!watcher.options.noLocks && !watcher.options.unsafeNoLocks) {
1557
1582
  for (let [readTime, values] of watcher.pendingAccesses) {
@@ -1651,6 +1676,7 @@ export class PathValueProxyWatcher {
1651
1676
  dryRun: options.dryRun,
1652
1677
  forcedUndefinedWrites,
1653
1678
  source: options.source,
1679
+ historyBasedReads,
1654
1680
  });
1655
1681
  }
1656
1682
 
@@ -1966,6 +1992,7 @@ export class PathValueProxyWatcher {
1966
1992
  ): Promise<Result> {
1967
1993
  options = {
1968
1994
  canWrite: true,
1995
+ waitForIncompleteTransactions: true,
1969
1996
  ...options,
1970
1997
  };
1971
1998
 
@@ -2507,4 +2534,5 @@ registerPeriodic(function logWatcherTypes() {
2507
2534
  // #endregion Proxy Ordering
2508
2535
 
2509
2536
  import { inlineNestedCalls } from "../3-path-functions/syncSchema"; import { LOCAL_DOMAIN_PATH } from "../0-path-value-core/PathRouter";
2537
+ import { waitIfReceivedIncompleteTransaction } from "./TransactionDelayer";
2510
2538
 
@@ -0,0 +1,95 @@
1
+ import { delay } from "socket-function/src/batching";
2
+ import { PathValue, hashPathForTransaction, compareTime, Time } from "../0-path-value-core/pathValueCore";
3
+ import { proxyWatcher } from "./PathValueProxyWatcher";
4
+
5
+ const MISSING_TRANSACTION_PART_TIMEOUT = 5000;
6
+
7
+ export function isMissingTransactionPart(pathValues: PathValue[]): { time: number; path: string; } | undefined {
8
+ // Create a map of path hash -> time for quick lookups
9
+ const pathHashToTime = new Map<string, {
10
+ time: Time;
11
+ path: string;
12
+ }>();
13
+ for (const pv of pathValues) {
14
+ const hash = hashPathForTransaction(pv.path);
15
+ const existing = pathHashToTime.get(hash);
16
+ // Keep the latest time for this path
17
+ if (!existing || compareTime(pv.time, existing.time) > 0) {
18
+ pathHashToTime.set(hash, {
19
+ time: pv.time,
20
+ path: pv.path,
21
+ });
22
+ }
23
+ }
24
+
25
+ // Group PathValues by their transactionPaths array reference
26
+ const uniqueTransactions = new Map<string[], Time>();
27
+ for (const pathValue of pathValues) {
28
+ if (!pathValue.transactionPaths || pathValue.transactionPaths.length === 0) {
29
+ continue;
30
+ }
31
+
32
+ const existing = uniqueTransactions.get(pathValue.transactionPaths);
33
+ if (!existing) {
34
+ uniqueTransactions.set(pathValue.transactionPaths, pathValue.time);
35
+ }
36
+ }
37
+
38
+ let newestWaitTime: {
39
+ time: number;
40
+ path: string;
41
+ } | undefined = undefined;
42
+
43
+ // Check each unique transaction only once
44
+ for (const [transactionPaths, transactionTime] of uniqueTransactions) {
45
+ for (const transactionPathHash of transactionPaths) {
46
+ const pathTime = pathHashToTime.get(transactionPathHash);
47
+ if (!pathTime) continue;
48
+
49
+ // We know that there's a value with this path hash at the transaction time. And so if the value that we've read is before that, then we haven't read the latest value. So we know that we're missing part of the transaction.
50
+ const timeComparison = compareTime(pathTime.time, transactionTime);
51
+ if (timeComparison < 0) {
52
+ if (!newestWaitTime || transactionTime.time > newestWaitTime.time) {
53
+ newestWaitTime = {
54
+ time: transactionTime.time,
55
+ path: pathTime.path,
56
+ };
57
+ }
58
+ }
59
+ }
60
+ }
61
+ if (!newestWaitTime) return undefined;
62
+ return newestWaitTime;
63
+ }
64
+
65
+ export function getAllPendingPathValueReads() {
66
+ let values: PathValue[] = [];
67
+ let watcher = proxyWatcher.getTriggeredWatcher();
68
+ for (let map of watcher.pendingAccesses.values()) {
69
+ for (let pathValue of map.values()) {
70
+ values.push(pathValue.pathValue);
71
+ }
72
+ }
73
+ return values;
74
+ }
75
+
76
+
77
+ export function waitIfReceivedIncompleteTransaction() {
78
+ // 1) Keep track of the last proxy watcher we just registered it for. And then if the proxy watcher is the same as that one and that proxy watcher is waiting on a promise, don't register it again.
79
+ let watcher = proxyWatcher.getTriggeredWatcher();
80
+ // We don't want to register a whole bunch of duplicate promises. So if somebody's waiting, there's no need to even do our check.
81
+ if (watcher.specialPromiseUnsynced) return;
82
+
83
+ let newestTime = isMissingTransactionPart(getAllPendingPathValueReads());
84
+ if (!newestTime) return;
85
+
86
+ let timeout = newestTime.time + MISSING_TRANSACTION_PART_TIMEOUT;
87
+ let now = Date.now();
88
+ if (timeout < now) return;
89
+
90
+ let promise = delay(timeout - now);
91
+ // This really nicely both blocks it from finishing because of the waiting for the promise and also triggers it automatically when the delay finishes. HOWEVER, In practice, the promise should never be required to trigger it. It should trigger when we receive the missing parts of the transaction, which we will be, of course, watching as they're in the path values that we're accessing. The timeout is just in case something goes wrong and we incorrectly think we should receive the values, but we won't. That way, eventually, we do commit the value.
92
+ // ALSO! Plus the hash I think only stores like 48 bits. So the chance of collision is somewhat high, especially if we're accessing thousands of paths. So sometimes this will trigger even though we're not missing any part just because we had a collision between the path hashes.
93
+ proxyWatcher.triggerOnPromiseFinish(promise, { waitReason: "Missing transaction part" });
94
+ console.error(`(NOT an error, convert this to a warning after we finish testing). Waiting for missing transaction part ${newestTime.path} which was written at time ${newestTime.time} (now is ${now}). We have parts of this transaction, but we are missing this specific path.`);
95
+ }
@@ -131,6 +131,7 @@ function predictCallBase(config: {
131
131
  let result = pathValueSerializer.getPathValue(resultObj) as FunctionResult | undefined;
132
132
  if (!result) return;
133
133
  if (result.lastInternalLoopCount === -1) return;
134
+ cleanupPrediction();
134
135
  actualValueFinished.resolve();
135
136
  for (let callback of onPredictionFinishedCallbacks) {
136
137
  callback({ callId: call.CallId, result, functionId: call.FunctionId });
@@ -306,7 +307,6 @@ function predictCallBase(config: {
306
307
 
307
308
  let didCancel = false;
308
309
  function rejectPrediction() {
309
- predictions;
310
310
  if (didCancel) return;
311
311
  didCancel = true;
312
312
  if (!predictions) return;
package/src/config.ts CHANGED
@@ -30,6 +30,7 @@ let yargObj = parseArgsFactory()
30
30
  desc: "Track all audit logs to disk. This might end up writing A LOT of data."
31
31
  })
32
32
  .option("logbackblaze", { type: "boolean", desc: "Log all backblaze activity to disk." })
33
+ .option("slowdown", { type: "number", desc: "Delay all input data values by this amount of time, pretending like we didn't even receive it until this time is up." })
33
34
  .argv
34
35
  ;
35
36
  type QuerysubConfig = {
@@ -51,6 +52,10 @@ let querysubConfig = lazy((): QuerysubConfig => {
51
52
  }
52
53
  });
53
54
 
55
+ export function getSlowdown() {
56
+ return yargObj.slowdown;
57
+ }
58
+
54
59
  export function isLogBackblaze() {
55
60
  return !!yargObj.logbackblaze;
56
61
  }
@@ -12,15 +12,19 @@ import { authorityStorage } from "../0-path-value-core/pathValueCore";
12
12
  import { getAllNodeIds } from "../-f-node-discovery/NodeDiscovery";
13
13
  import { MachineThreadInfoController } from "./MachineThreadInfo";
14
14
  import { pathValueSerializer } from "../-h-path-value-serialize/PathValueSerializer";
15
- import { sort } from "socket-function/src/misc";
15
+ import { list, sort } from "socket-function/src/misc";
16
16
  import { PathAuditerController } from "./pathAuditer";
17
17
  import { t } from "../2-proxy/schema2";
18
+ import { proxyWatcher } from "../2-proxy/PathValueProxyWatcher";
19
+ import { remoteWatcher } from "../1-path-client/RemoteWatcher";
20
+ import { getAllPendingPathValueReads, isMissingTransactionPart, waitIfReceivedIncompleteTransaction } from "../2-proxy/TransactionDelayer";
21
+
22
+ module.hotreload = true;
18
23
 
19
- //todonext
20
- // So... We need to double write, to write to multiple keys at once, in something that's not a lookup, or no, is a lookup, but before we deploy, so it doesn't have the proper or no, a nested lookup, even better.
21
24
 
22
25
  let { data, functions } = Querysub.createSchema({
23
- values: t.lookup(t.number)
26
+ values: t.lookup(t.number),
27
+ cardinality: t.number,
24
28
  })({
25
29
  functions: {
26
30
  setValue(key: string, value: number) {
@@ -29,6 +33,37 @@ let { data, functions } = Querysub.createSchema({
29
33
  incrementValue(key: string) {
30
34
  data().values[key]++;
31
35
  },
36
+ deleteValue(key: string) {
37
+ delete data().values[key];
38
+ },
39
+ syncValues(keys: string[], removeKeys: string[]) {
40
+ Querysub.noLocks(() => {
41
+ for (let key of removeKeys) {
42
+ delete data().values[key];
43
+ }
44
+ let max = Math.max(...keys.map(x => data().values[x])) + 1;
45
+ for (let key of keys) {
46
+ data().values[key] = max;
47
+ }
48
+ });
49
+ },
50
+ checkCardinality(keys: string[]) {
51
+ Querysub.noLocks(() => {
52
+ let cardinality = new Set(keys.map(x => data().values[x]));
53
+ let max = Math.max(...keys.map(x => data().values[x]));
54
+ data().cardinality = cardinality.size + max / 1000;
55
+ let watcher = proxyWatcher.getTriggeredWatcher();
56
+ if (watcher.pendingUnsyncedAccesses.size > 0) {
57
+ console.log("Unsynced accesses", [...watcher.pendingUnsyncedAccesses]);
58
+ }
59
+ for (let path of watcher.pendingWatches.paths) {
60
+ if (!path.includes("syncText")) continue;
61
+ let pathValue = authorityStorage.getValueAtTime(path, undefined, false, "noAudit");
62
+ let source = remoteWatcher.getExistingWatchRemoteNodeId(path);
63
+ console.log("Path value", pathValue?.time.time, pathValueSerializer.getPathValue(pathValue), path, source);
64
+ }
65
+ });
66
+ },
32
67
  },
33
68
  module,
34
69
  moduleId: "syncText",
@@ -45,6 +80,26 @@ let { data, functions } = Querysub.createSchema({
45
80
  getKey: (key: string) => key,
46
81
  },
47
82
  },
83
+ deleteValue: {
84
+ keyOverride: {
85
+ getPrefix: data => data().values,
86
+ getKey: (key: string) => key,
87
+ },
88
+ },
89
+ syncValues: {
90
+ nopredict: true,
91
+ keyOverride: {
92
+ getPrefix: data => data().values,
93
+ getKey: () => "f1.2",
94
+ },
95
+ },
96
+ checkCardinality: {
97
+ nopredict: true,
98
+ keyOverride: {
99
+ getPrefix: data => data().values,
100
+ getKey: () => "f2",
101
+ },
102
+ },
48
103
  },
49
104
  permissions: {
50
105
  PERMISSIONS: isSuperUserPERMISSIONS
@@ -98,6 +153,8 @@ export class SyncTestPage extends qreact.Component {
98
153
  });
99
154
  }
100
155
 
156
+ let keys = Object.keys(values);
157
+
101
158
  return (
102
159
  <div className={css.pad2(20).vbox(40)}>
103
160
  <h1>Sync Test Page</h1>
@@ -138,6 +195,7 @@ export class SyncTestPage extends qreact.Component {
138
195
  functions.setValue(key, +value);
139
196
  refreshDelayed();
140
197
  }} />
198
+ <Button onClick={() => { functions.deleteValue(key); refreshDelayed(); }}>Delete</Button>
141
199
  </div>
142
200
  <div className={css.hbox(10, 2).wrap.marginLeft(20)}>
143
201
  {allThreads.map(thread => {
@@ -184,11 +242,49 @@ export class SyncTestPage extends qreact.Component {
184
242
  refreshDelayed();
185
243
  }}
186
244
  />
187
- <Button onClick={() => {
188
- syncTestController.refreshAll();
189
- }}>
190
- Refresh
191
- </Button>
245
+ <div>
246
+ Cardinality: {data().cardinality}
247
+ </div>
248
+ <div className={css.hbox(10)}>
249
+ <Button onClick={() => {
250
+ syncTestController.refreshAll();
251
+ }}>
252
+ Refresh
253
+ </Button>
254
+
255
+ <Button onClick={() => {
256
+ let randKeys = list(4).map(x => Math.random() + "");
257
+ functions.syncValues(randKeys, keys);
258
+ refreshDelayed();
259
+ }}>
260
+ Set All Values
261
+ </Button>
262
+ <Button onClick={() => {
263
+ functions.syncValues(keys, []);
264
+ refreshDelayed();
265
+ }}>
266
+ Inc All Values
267
+ </Button>
268
+ <Button onClick={() => {
269
+ functions.checkCardinality(keys);
270
+ }}>
271
+ Check Cardinality
272
+ </Button>
273
+ <Button onClick={() => {
274
+ functions.syncValues(keys, []);
275
+ setTimeout(() => {
276
+ Querysub.commit(() => {
277
+ functions.checkCardinality(keys);
278
+ });
279
+ }, 500);
280
+ refreshDelayed();
281
+ }}>
282
+ Both
283
+ </Button>
284
+ </div>
285
+ <div>
286
+ {isMissingTransactionPart(getAllPendingPathValueReads()) && "Missing transaction part" || "No missing transaction part"}
287
+ </div>
192
288
  </div>
193
289
  );
194
290
  }