querysub 0.185.0 → 0.187.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.185.0",
3
+ "version": "0.187.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",
@@ -54,4 +54,4 @@
54
54
  "resolutions": {
55
55
  "node-forge": "https://github.com/sliftist/forge#e618181b469b07bdc70b968b0391beb8ef5fecd6"
56
56
  }
57
- }
57
+ }
package/spec.txt CHANGED
@@ -51,6 +51,7 @@ More corruption resistance files
51
51
  - Value types will be string, float64, byte, Buffer[]
52
52
  - and... we might as well add support for short, int, float, and uint (uint is a good way to store a guid, via storing 8 uint variables).
53
53
 
54
+
54
55
  Schema/binary PathValues accesses
55
56
 
56
57
  Base code
@@ -461,7 +461,7 @@ class PathValueCommitter {
461
461
  // NOTE: specialInitialParentCausedRejections is a bit dangerous, because it can reject golden values.
462
462
  // This means we have to ingest it immediately, otherwise authorityStorage might gc path values
463
463
  // (that are before a golden value), when rejections might remove that golden value.
464
- if (specialInitialParentCausedRejections) {
464
+ if (specialInitialParentCausedRejections?.length) {
465
465
  this.ingestValidStates(specialInitialParentCausedRejections);
466
466
  }
467
467
 
@@ -34,13 +34,15 @@ export function getLogHistoryIncludes(includes: string) {
34
34
  return Object.values(x.values).some(y => String(y).includes(includes));
35
35
  });
36
36
  }
37
- (globalThis as any).getLogHistoryEquals = getLogHistoryEquals;
38
- (globalThis as any).getPathAuditLogs = getLogHistoryEquals;
37
+ (globalThis as any).auditPath = getLogHistoryIncludes;
38
+ (globalThis as any).auditValue = getLogHistoryIncludes;
39
39
  export function getLogHistoryEquals(value: string) {
40
40
  return logHistory.getAllUnordered().filter(x => {
41
41
  return Object.values(x.values).some(y => y === value);
42
42
  });
43
43
  }
44
+ (globalThis as any).getLogHistoryEquals = getLogHistoryEquals;
45
+ (globalThis as any).getPathAuditLogs = getLogHistoryEquals;
44
46
 
45
47
  export function auditLog(type: string, values: { [key: string]: unknown }) {
46
48
  debugLogFnc(type, values);
@@ -128,15 +128,19 @@ export class RemoteWatcher {
128
128
  public watchLatest(config: WatchConfig & { debugName?: string }) {
129
129
  logErrors(this.watchLatestPromise(config));
130
130
  }
131
+ // IMPORTANT! If this shows up as being slow in a profile, try again with devtools closed! It is often slow because async calls are many times slower when devtools is open...
131
132
  // NOTE: This is private, as who wants to wait for watch to finish?
132
133
  // 1) The value won't be ready when it finishes.
133
134
  // 2) If it errors out, they can't do anything to fix the error
134
135
  // 3) We retry internally anyways
135
136
  private watchLatestPromise(config: WatchConfig & { debugName?: string }) {
136
- // NOTE: If none of the values are remote... early out. This has been profile to save a bit of time,
137
+ // NOTE: If none of the values are remote... early out. This has been profiled to save a bit of time,
137
138
  // mostly due to avoiding the async call.
138
- if (config.parentPaths.length === 0 && config.paths.every(x => pathValueAuthority2.isSelfAuthority(x))) {
139
- return;
139
+ if (config.parentPaths.length === 0) {
140
+ let isSelf = measureBlock(() => config.paths.every(x => pathValueAuthority2.isSelfAuthority(x)), "RemoteWatcher()|watchLatestPromise|isSelfCheck");
141
+ if (isSelf) {
142
+ return;
143
+ }
140
144
  }
141
145
  return this.watchLatestBase(config);
142
146
  }
@@ -440,6 +440,7 @@ export class ClientWatcher {
440
440
  this.allWatchers.delete(callback);
441
441
  }
442
442
 
443
+ @measureFnc
443
444
  private updateUnwatches(watchSpec: WatchSpec) {
444
445
  const { callback, paths, parentPaths } = watchSpec;
445
446
  let prevSpec = this.allWatchers.get(callback);
@@ -488,14 +489,15 @@ export class ClientWatcher {
488
489
  watchSpec = prevSpec;
489
490
 
490
491
  let expiryTime = Date.now() + ClientWatcher.WATCH_STICK_TIME;
491
- if (isDevDebugbreak()) {
492
- for (let path of fullyUnwatchedPaths) {
493
- auditLog("clientWatcher UNWATCH", { path });
494
- }
495
- for (let path of fullyUnwatchedParents) {
496
- auditLog("clientWatcher UNWATCH PARENT", { path });
497
- }
498
- }
492
+ // Commented out, as this is showing up as a bit slow in profiling
493
+ // if (isDevDebugbreak()) {
494
+ // for (let path of fullyUnwatchedPaths) {
495
+ // auditLog("clientWatcher UNWATCH", { path });
496
+ // }
497
+ // for (let path of fullyUnwatchedParents) {
498
+ // auditLog("clientWatcher UNWATCH PARENT", { path });
499
+ // }
500
+ // }
499
501
  for (let path of fullyUnwatchedPaths) {
500
502
  this.pendingUnwatches.set(path, expiryTime);
501
503
  }
@@ -550,35 +552,39 @@ export class ClientWatcher {
550
552
  this.updateUnwatches(watchSpec);
551
553
 
552
554
  this.allWatchers.set(callback, watchSpec);
553
- for (let path of paths) {
554
- let watchers = this.valueFunctionWatchers.get(path);
555
- if (!watchers) {
556
- watchers = new Map();
557
- this.valueFunctionWatchers.set(path, watchers);
558
- }
559
- watchers.set(callback, watchSpec);
560
- this.pendingUnwatches.delete(path);
561
- }
562
- for (let path of parentPaths) {
563
- let basePath = hack_stripPackedPath(path);
564
- let watchersBase = this.parentValueFunctionWatchers.get(basePath);
565
- if (!watchersBase) {
566
- watchersBase = new Map();
567
- this.parentValueFunctionWatchers.set(basePath, watchersBase);
555
+ measureBlock(() => {
556
+ for (let path of paths) {
557
+ let watchers = this.valueFunctionWatchers.get(path);
558
+ if (!watchers) {
559
+ watchers = new Map();
560
+ this.valueFunctionWatchers.set(path, watchers);
561
+ }
562
+ watchers.set(callback, watchSpec);
563
+ this.pendingUnwatches.delete(path);
568
564
  }
569
- let watchers = watchersBase.get(path);
570
- if (!watchers) {
571
- let range = decodeParentFilter(path) || { start: 0, end: 1 };
572
- watchers = {
573
- start: range.start,
574
- end: range.end,
575
- lookup: new Map()
576
- };
577
- watchersBase.set(path, watchers);
565
+ }, "ClientWatcher()|setWatches|paths");
566
+ measureBlock(() => {
567
+ for (let path of parentPaths) {
568
+ let basePath = hack_stripPackedPath(path);
569
+ let watchersBase = this.parentValueFunctionWatchers.get(basePath);
570
+ if (!watchersBase) {
571
+ watchersBase = new Map();
572
+ this.parentValueFunctionWatchers.set(basePath, watchersBase);
573
+ }
574
+ let watchers = watchersBase.get(path);
575
+ if (!watchers) {
576
+ let range = decodeParentFilter(path) || { start: 0, end: 1 };
577
+ watchers = {
578
+ start: range.start,
579
+ end: range.end,
580
+ lookup: new Map()
581
+ };
582
+ watchersBase.set(path, watchers);
583
+ }
584
+ watchers.lookup.set(callback, watchSpec);
585
+ this.pendingParentUnwatches.delete(path);
578
586
  }
579
- watchers.lookup.set(callback, watchSpec);
580
- this.pendingParentUnwatches.delete(path);
581
- }
587
+ }, "ClientWatcher()|setWatches|parentPaths");
582
588
 
583
589
  // Audit code to check if we failed to correctly maintain our paths
584
590
  // OR (more likely), the watcher mutated the paths Set, which breaks our watching
@@ -604,27 +610,33 @@ export class ClientWatcher {
604
610
  }
605
611
  */
606
612
 
607
- let pathsArray = Array.from(paths);
608
- let parentPathsArray = Array.from(parentPaths);
613
+ let pathsArray!: string[];
614
+ let parentPathsArray!: string[];
615
+ measureBlock(() => {
616
+ pathsArray = Array.from(paths);
617
+ parentPathsArray = Array.from(parentPaths);
618
+ }, "ClientWatcher()|setWatches|Array.from");
609
619
  let debugName = watchSpec.callback.name;
610
620
 
611
621
  if (pathsArray.length === 0 && parentPathsArray.length === 0) {
612
622
  return;
613
623
  }
614
624
 
615
- // Watch it locally as well, so when we get the value we know to trigger the client callback
616
- pathWatcher.watchPath({
617
- callback: getOwnNodeId(),
618
- paths: pathsArray,
619
- parentPaths: parentPathsArray,
620
- debugName,
621
- noInitialTrigger: watchSpec.noInitialTrigger,
622
- });
623
- remoteWatcher.watchLatest({
624
- paths: pathsArray,
625
- parentPaths: parentPathsArray,
626
- debugName,
627
- });
625
+ measureBlock(() => {
626
+ // Watch it locally as well, so when we get the value we know to trigger the client callback
627
+ pathWatcher.watchPath({
628
+ callback: getOwnNodeId(),
629
+ paths: pathsArray,
630
+ parentPaths: parentPathsArray,
631
+ debugName,
632
+ noInitialTrigger: watchSpec.noInitialTrigger,
633
+ });
634
+ remoteWatcher.watchLatest({
635
+ paths: pathsArray,
636
+ parentPaths: parentPathsArray,
637
+ debugName,
638
+ });
639
+ }, "ClientWatcher()|setWatches|watchCalls");
628
640
  }
629
641
 
630
642
  private lastVersions = registerResource("paths|lastVersion", new Map<number, number>());
@@ -764,7 +776,30 @@ export class ClientWatcher {
764
776
  }
765
777
 
766
778
  public pathHasAnyWatchers(path: string) {
767
- return !!this.valueFunctionWatchers.get(path)?.size;
779
+ let parentPath = getParentPathStr(path);
780
+ return !!this.valueFunctionWatchers.get(path)?.size || !!this.parentValueFunctionWatchers.get(parentPath)?.size;
781
+ }
782
+ public getWatchersForPath(path: string) {
783
+ let matched = new Set<WatchSpec>();
784
+ let parentPath = getParentPathStr(path);
785
+ let watchers = this.valueFunctionWatchers.get(path);
786
+ if (watchers) {
787
+ for (let watcher of watchers.values()) {
788
+ matched.add(watcher);
789
+ }
790
+ }
791
+ let parentWatchers = this.parentValueFunctionWatchers.get(parentPath);
792
+ if (parentWatchers) {
793
+ for (let [_, watcherObj] of parentWatchers) {
794
+ for (let watcher of watcherObj.lookup.values()) {
795
+ if (!matchesParentRangeFilter({ parentPath, fullPath: path, start: watcherObj.start, end: watcherObj.end })) {
796
+ continue;
797
+ }
798
+ matched.add(watcher);
799
+ }
800
+ }
801
+ }
802
+ return matched;
768
803
  }
769
804
  }
770
805
  export const clientWatcher = new ClientWatcher();
@@ -32,7 +32,7 @@ import { DEPTH_TO_DATA, MODULE_INDEX, getCurrentCall, getCurrentCallObj } from "
32
32
  import { inlineNestedCalls } from "../3-path-functions/syncSchema";
33
33
  import { interceptCallsBase, runCall } from "../3-path-functions/PathFunctionHelpers";
34
34
  import { deepCloneCborx } from "../misc/cloneHelpers";
35
- import { formatPercent } from "socket-function/src/formatting/format";
35
+ import { formatPercent, formatTime } from "socket-function/src/formatting/format";
36
36
  import { addStatPeriodic, interceptCalls, onAllPredictionsFinished, onTimeProfile } from "../-0-hooks/hooks";
37
37
  import { onNextPaint } from "../functional/onNextPaint";
38
38
 
@@ -202,6 +202,12 @@ export interface WatcherOptions<Result> {
202
202
  * what you want, it depends on the usecase.
203
203
  */
204
204
  runOncePerPaint?: string;
205
+
206
+ /** Logs time to go from unsynced to synced, and the paths that were unloaded.
207
+ * - In order to tell what paths to pre-load, and how much time we are waiting to load data.
208
+ * - Also to tell us how long and how many paths are loading on startup.
209
+ */
210
+ logSyncTimings?: boolean;
205
211
  }
206
212
 
207
213
  let harvestableReadyLoopCount = 0;
@@ -330,6 +336,15 @@ export type SyncWatcher = {
330
336
  hackHistory: { message: string; time: number }[];
331
337
 
332
338
  createTime: number;
339
+
340
+ logSyncTimings?: {
341
+ startTime: number;
342
+ unsyncedPaths: Set<string>;
343
+ unsyncedStages: {
344
+ paths: string[];
345
+ time: number;
346
+ }[];
347
+ };
333
348
  }
334
349
  function addToHistory(watcher: SyncWatcher, message: string) {
335
350
  watcher.hackHistory.push({ message, time: Date.now() });
@@ -360,7 +375,7 @@ export function atomicObjectRead<T>(obj: T): T {
360
375
  return (obj as any)[readNoProxy];
361
376
  }
362
377
  export const atomic = atomicObjectRead;
363
- (global as any).atomic = atomic;
378
+ (globalThis as any).atomic = atomic;
364
379
 
365
380
  export function doUnatomicWrites<T>(callback: () => T): T {
366
381
  return doProxyOptions({ atomicWrites: false }, callback);
@@ -860,7 +875,6 @@ export class PathValueProxyWatcher {
860
875
  return getKeys(pathValue?.value) as string[];
861
876
  }
862
877
 
863
- let keys: string[] = [];
864
878
  let childPaths = authorityStorage.getPathsFromParent(pathStr);
865
879
 
866
880
  // We need to also get keys from pendingWrites
@@ -877,23 +891,26 @@ export class PathValueProxyWatcher {
877
891
  }
878
892
  }
879
893
 
894
+ let keys = new Set<string>();
880
895
  if (childPaths) {
881
896
  let targetDepth = getPathDepth(pathStr);
882
897
  for (let childPath of childPaths) {
883
898
  let key = getPathIndexAssert(childPath, targetDepth);
884
899
  let childValue = this.getCallback(childPath, undefined, "readTransparent");
885
900
  if (childValue?.value !== undefined) {
886
- keys.push(key);
901
+ keys.add(key);
887
902
  }
888
903
  }
889
904
  }
890
905
 
906
+ let keysArray = Array.from(keys);
907
+
891
908
  // NOTE: Because getPathsFromParent does not preserve order, we have to sort here to ensure
892
909
  // we provide a consistent order (which might not be the order the user wants, but at least
893
910
  // it will always fail if it does fail).
894
- keys.sort();
911
+ keysArray.sort();
895
912
 
896
- return keys;
913
+ return keysArray;
897
914
  };
898
915
 
899
916
  private getSymbol = (pathStr: string, symbol: symbol): { value: unknown } | undefined => {
@@ -1119,6 +1136,8 @@ export class PathValueProxyWatcher {
1119
1136
  for (let callback of disposeCallbacks) {
1120
1137
  logErrors(((async () => { await callback(); }))());
1121
1138
  }
1139
+
1140
+ self.allWatchersLookup.delete(trigger);
1122
1141
  }
1123
1142
  function runWatcher() {
1124
1143
  watcher.pendingWrites.clear();
@@ -1253,8 +1272,56 @@ export class PathValueProxyWatcher {
1253
1272
  const specialPromiseUnsynced = watcher.specialPromiseUnsynced;
1254
1273
  watcher.specialPromiseUnsynced = false;
1255
1274
 
1275
+ let anyUnsynced = watcher.hasAnyUnsyncedAccesses();
1276
+ if (watcher.options.logSyncTimings) {
1277
+ if (anyUnsynced) {
1278
+ watcher.logSyncTimings = watcher.logSyncTimings || {
1279
+ startTime: Date.now(),
1280
+ unsyncedPaths: new Set(),
1281
+ unsyncedStages: [],
1282
+ };
1283
+ let newPaths: string[] = [];
1284
+ let set = watcher.logSyncTimings.unsyncedPaths;
1285
+ function addPath(path: string) {
1286
+ if (set.has(path)) return;
1287
+ set.add(path);
1288
+ newPaths.push(path);
1289
+ }
1290
+ for (let path of watcher.lastUnsyncedAccesses) {
1291
+ addPath(path);
1292
+ }
1293
+ for (let path of watcher.lastUnsyncedParentAccesses) {
1294
+ addPath(path);
1295
+ }
1296
+ if (newPaths.length > 0) {
1297
+ watcher.logSyncTimings.unsyncedStages.push({
1298
+ paths: newPaths,
1299
+ time: Date.now(),
1300
+ });
1301
+ }
1302
+ } else {
1303
+ if (watcher.logSyncTimings) {
1304
+ let now = Date.now();
1305
+ console.groupCollapsed(`${watcher.debugName} synced in ${formatTime(now - watcher.logSyncTimings.startTime)}`);
1306
+ // Log the stages
1307
+ let stages = watcher.logSyncTimings.unsyncedStages;
1308
+ for (let i = 0; i < stages.length; i++) {
1309
+ let nextTime = stages[i + 1]?.time || now;
1310
+ let stage = stages[i];
1311
+ console.groupCollapsed(`${formatTime(nextTime - stage.time)}`);
1312
+ for (let path of stage.paths) {
1313
+ console.log(path);
1314
+ }
1315
+ console.groupEnd();
1316
+ }
1317
+ console.groupEnd();
1318
+ watcher.logSyncTimings = undefined;
1319
+ }
1320
+ }
1321
+ }
1322
+
1256
1323
  if (!watcher.options.static) {
1257
- if (!watcher.hasAnyUnsyncedAccesses()) {
1324
+ if (!anyUnsynced) {
1258
1325
  watcher.countSinceLastFullSync = 0;
1259
1326
  watcher.lastSyncTime = Date.now();
1260
1327
  watcher.syncRunCount++;
@@ -1701,6 +1768,8 @@ export class PathValueProxyWatcher {
1701
1768
  trigger = measureWrap(trigger, `(watcher) ${watcher.debugName}`);
1702
1769
  watcher.explicitlyTrigger = trigger;
1703
1770
 
1771
+ self.allWatchersLookup.set(trigger, watcher);
1772
+
1704
1773
  if (options.static) {
1705
1774
  // Do nothing, this will be explicitly triggered when needed
1706
1775
  } else if (options.runImmediately) {
@@ -1880,6 +1949,10 @@ export class PathValueProxyWatcher {
1880
1949
 
1881
1950
 
1882
1951
  private allWatchers = registerResource("paths|proxyWatcher.allWatchers", new Set<SyncWatcher>());
1952
+ private allWatchersLookup = registerResource("paths|proxyWatcher.allWatchersLookup", new Map<Function, SyncWatcher>());
1953
+ public getWatcherForTrigger(trigger: Function): SyncWatcher | undefined {
1954
+ return this.allWatchersLookup.get(trigger);
1955
+ }
1883
1956
  public getAllWatchers(): Set<SyncWatcher> {
1884
1957
  return this.allWatchers;
1885
1958
  }
@@ -2025,6 +2098,10 @@ export class PathValueProxyWatcher {
2025
2098
  }
2026
2099
  return getPathFromProxy(proxy);
2027
2100
  }
2101
+
2102
+ public ignoreWatches<T>(code: () => T) {
2103
+ return doProxyOptions({ noSyncing: true }, code);
2104
+ }
2028
2105
  }
2029
2106
 
2030
2107
  const PAINT_NOOP_FUNCTION = () => { };
@@ -2146,13 +2223,13 @@ export function noAtomicSchema<T>(code: () => T) {
2146
2223
 
2147
2224
 
2148
2225
  export const proxyWatcher = new PathValueProxyWatcher();
2149
- (global as any).proxyWatcher = proxyWatcher;
2150
- (global as any).ProxyWatcher = PathValueProxyWatcher;
2226
+ (globalThis as any).proxyWatcher = proxyWatcher;
2227
+ (globalThis as any).ProxyWatcher = PathValueProxyWatcher;
2151
2228
  // Probably the most useful debug function for debugging watch based functions.
2152
- (global as any).getCurrentTriggers = function () {
2229
+ (globalThis as any).getCurrentTriggers = function () {
2153
2230
  return proxyWatcher.getTriggeredWatcher().triggeredByChanges;
2154
2231
  };
2155
- (global as any).getAllWatchers = function () {
2232
+ (globalThis as any).getAllWatchers = function () {
2156
2233
  return proxyWatcher.getAllWatchers();
2157
2234
  };
2158
2235
 
@@ -134,6 +134,9 @@ export interface QComponentStatic {
134
134
  * UNLESS the child explicitly sets this to false.
135
135
  * */
136
136
  multiRendersPerPaint?: boolean;
137
+
138
+ /** If true, logs time it takes for component to load synced data (if at all), and the paths that were unloaded. */
139
+ logLoadTime?: boolean;
137
140
  }
138
141
 
139
142
  /** See QComponentStatic.jsonComparePropUpdates */
@@ -172,6 +175,8 @@ export class qreact {
172
175
 
173
176
  public static errorHandler: ErrorHandler;
174
177
 
178
+ public static domUpdateCount = 0;
179
+
175
180
  public static debug = async () => {
176
181
  const { enableDebugComponents } = await import("../5-diagnostics/qreactDebug");
177
182
  enableDebugComponents();
@@ -414,7 +419,14 @@ function getAncestor(
414
419
  }
415
420
  function onDispose(fnc: () => void) {
416
421
  let component = getRenderingComponent();
417
- if (!component) throw new Error(`Can only call onDispose if inside of a render (or componentDidMount) function. If an an event callback, call qreact.getRenderingComponent in render, and then call qreact.watchDispose(component, callback).`);
422
+ if (!component) {
423
+ if (QRenderClass.renderingComponentId !== undefined) {
424
+ // If it's already disposed... call the function immediately?
425
+ fnc();
426
+ return;
427
+ }
428
+ throw new Error(`Can only call onDispose if inside of a render (or componentDidMount) function. If an an event callback, call qreact.getRenderingComponent in render, and then call qreact.watchDispose(component, callback).`);
429
+ }
418
430
  watchDispose(component, fnc);
419
431
  }
420
432
  function watchDispose(renderClass: ExternalRenderClass, fnc: () => void) {
@@ -490,6 +502,23 @@ class QRenderClass {
490
502
 
491
503
  let vNode = config.vNode;
492
504
  let self = this;
505
+
506
+ const contextCommit = (callback: () => void) => {
507
+ void Promise.resolve().finally(() => {
508
+ logErrors(proxyWatcher.commitFunction({
509
+ canWrite: true,
510
+ watchFunction() {
511
+ QRenderClass.renderingComponentId = self.id;
512
+ try {
513
+ callback();
514
+ } finally {
515
+ QRenderClass.renderingComponentId = undefined;
516
+ }
517
+ },
518
+ }));
519
+ });
520
+ };
521
+
493
522
  // Set up watchers for virtual watching AND dom mounting
494
523
  let debugName = vNode.type.name + `|${this.id}|`;
495
524
  const getDebugName = (type: string) => {
@@ -564,7 +593,9 @@ class QRenderClass {
564
593
  self.instance.componentWillMount && logErrors(proxyWatcher.commitFunction({
565
594
  debugName: getDebugName("componentWillMount"),
566
595
  watchFunction() {
567
- self.instance.componentWillMount?.();
596
+ contextCommit(() => {
597
+ self.instance.componentWillMount?.();
598
+ });
568
599
  },
569
600
  }));
570
601
 
@@ -595,6 +626,7 @@ class QRenderClass {
595
626
  const renderWatcher = this.renderWatcher = proxyWatcher.createWatcher({
596
627
  debugName: getDebugName("render"),
597
628
  canWrite: true,
629
+ logSyncTimings: statics.logLoadTime,
598
630
  runOncePerPaint: !multiRendersPerPaint && `${this.debugName}|${self.id}` || undefined,
599
631
  watchFunction() {
600
632
 
@@ -702,7 +734,9 @@ class QRenderClass {
702
734
  // ALSO, this stops infinite loops caused by self triggering, which is really useful.
703
735
  if (!QRenderClass.areVNodesEqual(comparePrevVNode, vNode)) {
704
736
  self.data().vNodeForRender = frozen;
705
- comparePrevVNode = frozen.value;
737
+ if (Querysub.isAllSynced()) {
738
+ comparePrevVNode = frozen.value;
739
+ }
706
740
  } else {
707
741
  wastedRenders++;
708
742
  QRenderClass.areVNodesEqual(comparePrevVNode, vNode);
@@ -836,6 +870,10 @@ class QRenderClass {
836
870
  if ("data-break" in nextProps) {
837
871
  debugger;
838
872
  }
873
+ todonext;
874
+ // First of all, we are accessing deepProps on the WRONG value. We should be accessing it on the child statics, not our statics!
875
+ // Second of all... we need to check if our child is JSON comparing, and if so, do an atomic write of the props. Usually we don't want to do an atomic write, as this would force an update every render, but... if it is JSON comparing, it won't force an update.
876
+ // - Which is actually trivial. We just literally set .props = nextProps. Done.
839
877
  if (statics.deepProps) {
840
878
  // NOTE: By removing atomic we can leverage the proxy automatically decomposing
841
879
  // the props to each individual primitive value, and checking each individual
@@ -1111,22 +1149,6 @@ class QRenderClass {
1111
1149
  }
1112
1150
  }
1113
1151
 
1114
- const contextCommit = (callback: () => void) => {
1115
- void Promise.resolve().finally(() => {
1116
- logErrors(proxyWatcher.commitFunction({
1117
- canWrite: true,
1118
- watchFunction() {
1119
- QRenderClass.renderingComponentId = self.id;
1120
- try {
1121
- callback();
1122
- } finally {
1123
- QRenderClass.renderingComponentId = undefined;
1124
- }
1125
- },
1126
- }));
1127
- });
1128
- };
1129
-
1130
1152
  // NOTE: This is a bit inefficient as it requires running twice, because the first run our child
1131
1153
  // components will have no rootDOMNodes. But... it's probably fine...
1132
1154
  QRenderClass.diffVNodes({
@@ -1946,14 +1968,17 @@ function updateDOMNodeFields(domNode: DOMNode, vNode: VirtualDOM, prevVNode: Vir
1946
1968
  if (typeof v === "number" && !IS_NON_DIMENSIONAL.test(key)) {
1947
1969
  v = v + "px";
1948
1970
  }
1949
- (domNode as any).style[key] = v ?? "";
1971
+ if (String(v).endsWith("!important")) {
1972
+ (domNode as any).style.setProperty(key, String(v).slice(0, -"!important".length), "important");
1973
+ } else {
1974
+ (domNode as any).style[key] = v ?? "";
1975
+ }
1950
1976
  }
1951
1977
  if (canHaveChildren(prevValue)) {
1952
1978
  for (let key in prevValue) {
1953
1979
  if (!(key in value)) {
1954
1980
  (domNode as any).style[key] = "";
1955
1981
  }
1956
-
1957
1982
  }
1958
1983
  }
1959
1984
  }
@@ -2675,6 +2700,7 @@ let baseTrigger = lazy(async () => {
2675
2700
  }
2676
2701
  });
2677
2702
  function triggerGlobalOnMountWatch(component: QRenderClass) {
2703
+ qreact.domUpdateCount++;
2678
2704
  pendingGlobalOnMountWatches.push(component);
2679
2705
  void baseTrigger();
2680
2706
  }
@@ -38,13 +38,13 @@ import { sha256 } from "js-sha256";
38
38
  import { minify_sync } from "terser";
39
39
  import { isClient } from "../config2";
40
40
  import { waitForFirstTimeSync } from "socket-function/time/trueTimeShim";
41
- import { logMeasureTable, measureBlock, measureFnc, startMeasure } from "socket-function/src/profiling/measure";
41
+ import { logMeasureTable, measureBlock, measureFnc, measureWrap, startMeasure } from "socket-function/src/profiling/measure";
42
42
  import { delay } from "socket-function/src/batching";
43
43
  import { MaybePromise } from "socket-function/src/types";
44
44
  import { devDebugbreak, getDomain, isDynamicallyLoading, isPublic, noSyncing } from "../config";
45
45
  import { Schema2, Schema2T, t } from "../2-proxy/schema2";
46
46
  import { CALL_PERMISSIONS_KEY } from "./permissionsShared";
47
- import yargs from "yargs";
47
+ import yargs, { check } from "yargs";
48
48
  import { parseArgsFactory } from "../misc/rawParams";
49
49
 
50
50
  import * as typesafecss from "typesafecss";
@@ -211,6 +211,8 @@ export class Querysub {
211
211
  public static callRandom = () => hashRandom(Querysub.getCallId(), getNextCallIndex());
212
212
  public static nextId = () => Querysub.getCallId() + "_" + getNextCallIndex();
213
213
 
214
+ public static getNextCallIndex = getNextCallIndex;
215
+
214
216
  public static configRootDiscoveryLocation = configRootDiscoveryLocation;
215
217
 
216
218
 
@@ -280,13 +282,14 @@ export class Querysub {
280
282
  await nodePathAuthority.waitUntilRoutingIsReady();
281
283
  }
282
284
 
283
- public static createWatcher(watcher: (obj: SyncWatcher) => void): {
285
+ public static createWatcher(watcher: (obj: SyncWatcher) => void, options?: Partial<WatcherOptions<unknown>>): {
284
286
  dispose: () => void;
285
287
  explicitlyTrigger: () => void;
286
288
  } {
287
289
  return proxyWatcher.createWatcher({
288
290
  debugName: watcher.name,
289
291
  canWrite: true,
292
+ ...options,
290
293
  watchFunction: () => {
291
294
  return watcher(proxyWatcher.getTriggeredWatcher());
292
295
  },
@@ -358,6 +361,9 @@ export class Querysub {
358
361
  allowProxyResults: true,
359
362
  });
360
363
  }
364
+ public static fastReadAsync<T>(fnc: () => T, options?: Partial<WatcherOptions<T>>) {
365
+ return Querysub.serviceWrite(fnc, { ...options, allowProxyResults: true });
366
+ }
361
367
  public static localRead<T>(fnc: () => T, options?: Partial<WatcherOptions<T>>) {
362
368
  return proxyWatcher.runOnce({
363
369
  watchFunction: fnc,
@@ -417,6 +423,18 @@ export class Querysub {
417
423
  Querysub.commit(callback);
418
424
  });
419
425
  }
426
+ public static onSynced(callback: () => void) {
427
+ let watcher = proxyWatcher.getTriggeredWatcherMaybeUndefined();
428
+ Querysub.onCommitFinished(() => {
429
+ if (!watcher || !watcher.hasAnyUnsyncedAccesses()) {
430
+ callback();
431
+ }
432
+ });
433
+ }
434
+ /** Checks all recursive watches. However... if something writes to a value you watch (ex, DerivedCache), we can't track that, so this is of limited use. */
435
+ public static onNestedSynced(callback: () => void) {
436
+ onNestedSynced(callback);
437
+ }
420
438
 
421
439
  /** A more powerful version of omCommitFinished, which even waits for call predictions (or tries to).
422
440
  * - Also see afterPredictionsSynced, which runs the callback in a write.
@@ -432,7 +450,7 @@ export class Querysub {
432
450
  * you can't call this at the start of your function (as nothing will have been called yet).
433
451
  * NOTE: This can also be used to prevent
434
452
  */
435
- public static afterPredictions(callback: () => Promise<void>) {
453
+ public static afterPredictions(callback: () => MaybePromise<void>) {
436
454
  let calls = proxyWatcher.getTriggeredWatcher().pendingCalls.map(x => x.call);
437
455
  Querysub.onCommitFinished(async () => {
438
456
  await Promise.all(calls.map(x => Querysub.onCallPredict(x)));
@@ -496,6 +514,10 @@ export class Querysub {
496
514
  return isSynced(value);
497
515
  }
498
516
 
517
+ public static ignoreWatches<T>(code: () => T) {
518
+ return proxyWatcher.ignoreWatches(code);
519
+ }
520
+
499
521
  public static assertDomainAllowed(path: string) {
500
522
  let domain = getPathIndexAssert(path, 0);
501
523
  if (!Querysub.trustedDomains.has(domain)) {
@@ -1021,6 +1043,7 @@ function getNextCallIndex() {
1021
1043
  if (triggeredWatcher !== triggered) {
1022
1044
  triggered.onAfterTriggered.push(() => {
1023
1045
  nextCallIndex = 1;
1046
+ triggeredWatcher = undefined;
1024
1047
  });
1025
1048
  triggeredWatcher = triggered;
1026
1049
  }
@@ -1058,26 +1081,40 @@ let initInterval = cache((interval: number) => {
1058
1081
  }, interval);
1059
1082
  });
1060
1083
 
1084
+ function getSetTime() {
1085
+ let call = getCurrentCallAllowUndefined();
1086
+ if (call) {
1087
+ return call.runAtTime.time;
1088
+ }
1089
+ let watcher = proxyWatcher.getTriggeredWatcherMaybeUndefined();
1090
+ if (watcher?.options.temporary) {
1091
+ return watcher.createTime;
1092
+ }
1093
+ return undefined;
1094
+ }
1095
+
1061
1096
  let lastTime = 0;
1062
1097
  function getSyncedTimeUnique() {
1098
+ let setTime = getSetTime();
1099
+ if (setTime !== undefined) {
1100
+ return addEpsilons(setTime, getNextCallIndex());
1101
+ }
1102
+
1063
1103
  let time = getSyncedTime();
1064
1104
  if (time <= lastTime) {
1065
- time = addEpsilons(time, 1);
1105
+ time = addEpsilons(lastTime, 1);
1066
1106
  }
1067
1107
  lastTime = time;
1068
1108
  return time;
1069
1109
  }
1070
1110
 
1071
1111
  function getSyncedTime() {
1072
- let call = getCurrentCallAllowUndefined();
1073
- if (call) {
1074
- return call.runAtTime.time;
1075
- }
1076
- let watcher = proxyWatcher.getTriggeredWatcherMaybeUndefined();
1077
- if (watcher?.options.temporary) {
1078
- return watcher.createTime;
1112
+ let setTime = getSetTime();
1113
+ if (setTime !== undefined) {
1114
+ return setTime;
1079
1115
  }
1080
1116
  if (isNode()) {
1117
+ let watcher = proxyWatcher.getTriggeredWatcherMaybeUndefined();
1081
1118
  if (watcher) {
1082
1119
  throw new Error(`Trying to access time in a serverside non-temporary watcher. Clientside this is allowed, as infinite loops (render every frame) makes sense. Serverside this is not allowed. Did you try to run a call-type operatin in a watcher? If you manually created a watcher, you might want to set "temporary: true" if you will be immediately disposing it. You almost might want to fork your write logic with setImmediate to detach this from your watcher, so your write can access a single non-changing time. In ${watcher.debugName}`);
1083
1120
  }
@@ -1098,6 +1135,72 @@ function timeDelayed(interval: number) {
1098
1135
  return Date.now();
1099
1136
  }
1100
1137
 
1138
+ let nestedSyncDedupeHack = new Map<string, () => void>();
1139
+
1140
+ const onNestedSynced = measureWrap(function onNestedSynced(callback: () => void) {
1141
+ const watcher = proxyWatcher.getTriggeredWatcherMaybeUndefined();
1142
+ if (!watcher) {
1143
+ callback();
1144
+ return;
1145
+ }
1146
+
1147
+ let key = watcher.debugName + "_" + callback.toString();
1148
+ if (nestedSyncDedupeHack.has(key)) {
1149
+ nestedSyncDedupeHack.set(key, callback);
1150
+ return;
1151
+ }
1152
+ nestedSyncDedupeHack.set(key, callback);
1153
+
1154
+ const getPendingWatcher = (): SyncWatcher | undefined => {
1155
+ let checkWatchers: SyncWatcher[] = [];
1156
+ let checkedWatchers = new Set<SyncWatcher>();
1157
+ checkWatchers.push(watcher);
1158
+ while (true) {
1159
+ let watcher = checkWatchers.shift();
1160
+ if (!watcher) break;
1161
+ if (watcher.hasAnyUnsyncedAccesses()) return watcher;
1162
+ // Wait until it's had time to spawn children watchers
1163
+ if (watcher.syncRunCount === 0) return watcher;
1164
+ let writes = watcher.pendingWrites;
1165
+ for (let writePath of writes.keys()) {
1166
+ // Find any watchers of these writes
1167
+ let callbacks = clientWatcher.getWatchersForPath(writePath);
1168
+ for (let callback of callbacks) {
1169
+ let nestedWatcher = proxyWatcher.getWatcherForTrigger(callback.callback);
1170
+ if (!nestedWatcher) continue;
1171
+ if (checkedWatchers.has(nestedWatcher)) continue;
1172
+ checkedWatchers.add(nestedWatcher);
1173
+ checkWatchers.push(nestedWatcher);
1174
+ }
1175
+ }
1176
+ }
1177
+
1178
+ return undefined;
1179
+ };
1180
+ const checkAgain = () => {
1181
+ const pendingWatcher = getPendingWatcher();
1182
+ if (!pendingWatcher) {
1183
+ callback();
1184
+ nestedSyncDedupeHack.delete(key);
1185
+ } else {
1186
+ //console.log(`Waiting for ${pendingWatcher.debugName} to finish`);
1187
+ getPendingWatcher();
1188
+ let waitPromise = clientWatcher.waitForTriggerFinished();
1189
+ if (!waitPromise) {
1190
+ pendingWatcher.onAfterTriggered.push(checkAgain);
1191
+ } else {
1192
+ void waitPromise.finally(() => {
1193
+ checkAgain();
1194
+ });
1195
+ }
1196
+ }
1197
+ };
1198
+ Querysub.onCommitFinished(() => {
1199
+ checkAgain();
1200
+ });
1201
+ });
1202
+
1203
+
1101
1204
  // Returns a value between 0 and 1
1102
1205
  function hashRandom(base: string, offset: number): number {
1103
1206
  let hashBuf = Buffer.from(sha256(base + "_" + offset), "hex");
@@ -1160,4 +1263,5 @@ import { formatNumber, formatTime } from "socket-function/src/formatting/format"
1160
1263
  import { css } from "../4-dom/css";
1161
1264
  import { getCountPerPaint } from "../functional/onNextPaint";
1162
1265
  import { addEpsilons } from "../bits";
1266
+ import { blue } from "socket-function/src/formatting/logColors";
1163
1267
 
@@ -1,17 +1,56 @@
1
1
  import preact from "preact";
2
2
  import { qreact } from "../4-dom/qreact";
3
3
  import { Button } from "../library-components/Button";
4
- import { Modal } from "./Modal";
4
+ import { Modal, showModal } from "./Modal";
5
5
 
6
6
 
7
- export class FullscreenModal extends preact.Component<{
7
+ export class FullscreenModal extends qreact.Component<{
8
8
  parentState?: { open: boolean };
9
9
  onCancel?: () => void | "abortClose";
10
10
  style?: preact.JSX.CSSProperties;
11
11
  outerStyle?: preact.JSX.CSSProperties;
12
12
  onlyExplicitClose?: boolean;
13
+ noModalWrap?: boolean;
13
14
  }> {
14
15
  render() {
16
+ let base = <div
17
+ className="FullscreenModal"
18
+ style={{
19
+ position: "fixed",
20
+ top: 0,
21
+ left: 0,
22
+ width: "100vw",
23
+ height: "100vh",
24
+ background: "hsla(0, 0%, 30%, 0.5)",
25
+ padding: 100,
26
+ display: "flex",
27
+ alignItems: "center",
28
+ justifyContent: "center",
29
+ overflow: "auto",
30
+ cursor: "pointer",
31
+ ...this.props.outerStyle,
32
+ }}
33
+ >
34
+ <div
35
+ className="FullscreenModal-background keepModalsOpen"
36
+ style={{
37
+ background: "hsl(0, 0%, 100%)",
38
+ padding: 20,
39
+ color: "hsl(0, 0%, 7%)",
40
+ cursor: "default",
41
+ width: "100%",
42
+ display: "flex",
43
+ flexDirection: "column",
44
+ gap: 10,
45
+ maxHeight: "calc(100% - 200px)",
46
+ overflow: "auto",
47
+ ...this.props.style
48
+ }}
49
+ >
50
+ {this.props.children}
51
+ </div>
52
+ </div>;
53
+ if (this.props.noModalWrap) return base;
15
54
  return (
16
55
  <Modal
17
56
  onClose={() => {
@@ -19,43 +58,21 @@ export class FullscreenModal extends preact.Component<{
19
58
  return this.props.onCancel?.();
20
59
  }}
21
60
  >
22
- <div
23
- style={{
24
- position: "fixed",
25
- top: 0,
26
- left: 0,
27
- width: "100vw",
28
- height: "100vh",
29
- background: "hsla(0, 0%, 30%, 0.5)",
30
- padding: 100,
31
- display: "flex",
32
- alignItems: "center",
33
- justifyContent: "center",
34
- overflow: "auto",
35
- cursor: "pointer",
36
- ...this.props.outerStyle,
37
- }}
38
- >
39
- <div
40
- className="keepModalsOpen"
41
- style={{
42
- background: "hsl(0, 0%, 100%)",
43
- padding: 20,
44
- color: "hsl(0, 0%, 7%)",
45
- cursor: "default",
46
- width: "100%",
47
- display: "flex",
48
- flexDirection: "column",
49
- gap: 10,
50
- maxHeight: "calc(100% - 200px)",
51
- overflow: "auto",
52
- ...this.props.style
53
- }}
54
- >
55
- {this.props.children}
56
- </div>
57
- </div>
61
+ {base}
58
62
  </Modal>
59
63
  );
60
64
  }
61
- }
65
+ }
66
+
67
+ export function showFullscreenModal(config: {
68
+ content: preact.ComponentChild;
69
+ onClose?: () => void | "abortClose";
70
+ }): {
71
+ close: () => void;
72
+ } {
73
+ let { close } = showModal({
74
+ content: <FullscreenModal noModalWrap>{config.content}</FullscreenModal>,
75
+ onClose: config.onClose,
76
+ });
77
+ return { close };
78
+ }
@@ -1,10 +1,10 @@
1
- import preact from "preact";
2
1
  import { nextId } from "socket-function/src/misc";
3
2
  import { lazy } from "socket-function/src/caching";
4
3
  import { css } from "typesafecss";
5
4
  import { Querysub } from "../4-querysub/Querysub";
6
5
  import { atomicObjectWrite, atomicObjectWriteNoFreeze } from "../2-proxy/PathValueProxyWatcher";
7
6
  import { qreact } from "../4-dom/qreact";
7
+ import { FullscreenModal } from "./FullscreenModal";
8
8
 
9
9
  const data = Querysub.createLocalSchema<{
10
10
  modals: {
@@ -41,14 +41,16 @@ const ensureRendering = lazy(() => {
41
41
  });
42
42
 
43
43
  /** IMPORTANT! Use the .keepModalsOpen class to prevent clicks from closing the model */
44
- export class Modal extends preact.Component<{
44
+ export class Modal extends qreact.Component<{
45
45
  onClose?: () => void | "abortClose";
46
46
  }> {
47
47
  id = nextId();
48
48
  componentWillUnmount(): void {
49
+ console.log("Unmounting modal", this.id);
49
50
  delete data().modals[this.id];
50
51
  }
51
52
  render() {
53
+ console.log("Rendering modal", this.id);
52
54
  ensureRendering();
53
55
  data().modals[this.id] = atomicObjectWriteNoFreeze({
54
56
  value: this.props.children,
@@ -65,6 +67,7 @@ function closeModal(id: string) {
65
67
  if (result === "abortClose") return;
66
68
  delete data().modals[id];
67
69
  }
70
+
68
71
  export function showModal(config: {
69
72
  content: preact.ComponentChild;
70
73
  onClose?: () => void | "abortClose";
@@ -94,7 +97,7 @@ export function closeAllModals() {
94
97
  }
95
98
 
96
99
 
97
- export class ModalHolder extends preact.Component {
100
+ export class ModalHolder extends qreact.Component {
98
101
  render() {
99
102
  let modals = Object.values(data().modals);
100
103
  return (
@@ -186,10 +186,15 @@ class WatchModal extends qreact.Component<{
186
186
  || {}
187
187
  }
188
188
  style={
189
- pos === "fill" && {}
190
- || pos === "top" && { maxHeight: "100%" }
191
- || pos === "bottom" && { maxHeight: "100%" }
192
- || {}
189
+ {
190
+ ...(
191
+ pos === "fill" && {}
192
+ || pos === "top" && { maxHeight: "100%" }
193
+ || pos === "bottom" && { maxHeight: "100%" }
194
+ || {}
195
+ ),
196
+ background: "rgb(255, 255, 255)!important",
197
+ }
193
198
  }
194
199
  onCancel={() => {
195
200
  if (this.state.pos !== "fill") {
@@ -197,7 +202,7 @@ class WatchModal extends qreact.Component<{
197
202
  }
198
203
  }}
199
204
  >
200
- <div class={css.hsl(0, 0, 100).vbox(6)}>
205
+ <div class={css.hsl(0, 0, 100).hslcolor(0, 0, 10).vbox(6)}>
201
206
  <div class={css.hbox(10).fillWidth}>
202
207
  {component.debugName}
203
208
  {parent && <Button onClick={() => this.state.parentNavigate++}>
@@ -53,7 +53,10 @@ export class ArchiveViewerTree extends qreact.Component<{
53
53
  let { remainingWidth, nodePath, scaleType } = config;
54
54
  const selectedPath = archiveTreeSelected.value;
55
55
 
56
- const sumFormatter = this.props.sumFormatter || formatNumber;
56
+ let sumFormatter: (value: number) => string = formatNumber;
57
+ if (archiveTreeScale.value === "size") {
58
+ sumFormatter = this.props.sumFormatter || formatNumber;
59
+ }
57
60
 
58
61
  let isSelected = selectedPath === nodePath;
59
62
  let isDescendantSelected = selectedPath.startsWith(nodePath);
package/src/errors.ts CHANGED
@@ -112,6 +112,7 @@ export function errorify(error: any, messageOverride?: string) {
112
112
  errorObj.stack = error;
113
113
  } else {
114
114
  errorObj.message = error;
115
+ errorObj.stack = error + "\n" + errorObj.stack;
115
116
  }
116
117
  if (messageOverride) {
117
118
  errorObj.message = messageOverride;
@@ -28,6 +28,8 @@ export type ATagProps = (
28
28
  */
29
29
  rawLink?: boolean;
30
30
  lightMode?: boolean;
31
+ noStyles?: boolean;
32
+ onRef?: (element: HTMLAnchorElement | null) => void;
31
33
  }
32
34
  );
33
35
 
@@ -50,6 +52,7 @@ export class ATag extends qreact.Component<ATagProps> {
50
52
  <a
51
53
  tabIndex={0}
52
54
  {...props}
55
+ ref={props.onRef}
53
56
  className={
54
57
  (isCurrent ? css.color(`hsl(110, 75%, ${lightness}%)`) : css.color(`hsl(210, 100%, ${lightness}%)`))
55
58
  + css.textDecoration("none")
@@ -57,6 +60,12 @@ export class ATag extends qreact.Component<ATagProps> {
57
60
  .outline("3px solid hsl(204, 100%, 50%)", "focus")
58
61
  + (props.className ?? props.class)
59
62
 
63
+ + (props.noStyles && css
64
+ .textDecoration("none", "important")
65
+ .outline("none", "important")
66
+ .color("inherit", "important")
67
+ )
68
+
60
69
  }
61
70
  onClick={e => {
62
71
  if (this.props.rawLink) return;
@@ -688,5 +688,13 @@ export const Icon = {
688
688
  <path d="M15 15V14H14V15H15ZM20.2929 21.7071C20.6834 22.0976 21.3166 22.0976 21.7071 21.7071C22.0976 21.3166 22.0976 20.6834 21.7071 20.2929L20.2929 21.7071ZM15 9H14V10H15V9ZM21.7071 3.70711C22.0976 3.31658 22.0976 2.68342 21.7071 2.29289C21.3166 1.90237 20.6834 1.90237 20.2929 2.29289L21.7071 3.70711ZM9 15H10V14H9V15ZM2.29289 20.2929C1.90237 20.6834 1.90237 21.3166 2.29289 21.7071C2.68342 22.0976 3.31658 22.0976 3.70711 21.7071L2.29289 20.2929ZM9 9V10H10V9H9ZM3.70711 2.29289C3.31658 1.90237 2.68342 1.90237 2.29289 2.29289C1.90237 2.68342 1.90237 3.31658 2.29289 3.70711L3.70711 2.29289ZM16 20V15H14V20H16ZM15 16H20V14H15V16ZM14.2929 15.7071L20.2929 21.7071L21.7071 20.2929L15.7071 14.2929L14.2929 15.7071ZM14 4V9H16V4H14ZM15 10H20V8H15V10ZM15.7071 9.70711L21.7071 3.70711L20.2929 2.29289L14.2929 8.29289L15.7071 9.70711ZM10 20V15H8V20H10ZM9 14H4V16H9V14ZM8.29289 14.2929L2.29289 20.2929L3.70711 21.7071L9.70711 15.7071L8.29289 14.2929ZM8 4V9H10V4H8ZM9 8H4V10H9V8ZM9.70711 8.29289L3.70711 2.29289L2.29289 3.70711L8.29289 9.70711L9.70711 8.29289Z" fill="#33363F" />
689
689
  </svg>
690
690
 
691
+ ),
692
+ // TODO: Create a better warning (something that looks good at low sizes)
693
+ warning: fastSVG(
694
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 122.88 111.54" width="24" height="24">
695
+ <path fill="#cf1f25" d="M2.35,84.42,45.28,10.2l.17-.27h0A23,23,0,0,1,52.5,2.69,17,17,0,0,1,61.57,0a16.7,16.7,0,0,1,9.11,2.69,22.79,22.79,0,0,1,7,7.26q.19.32.36.63l42.23,73.34.24.44h0a22.48,22.48,0,0,1,2.37,10.19,17.63,17.63,0,0,1-2.17,8.35,15.94,15.94,0,0,1-6.93,6.6c-.19.1-.39.18-.58.26a21.19,21.19,0,0,1-9.11,1.75v0H17.61c-.22,0-.44,0-.65,0a18.07,18.07,0,0,1-6.2-1.15A16.42,16.42,0,0,1,3,104.24a17.53,17.53,0,0,1-3-9.57,23,23,0,0,1,1.57-8.74,7.66,7.66,0,0,1,.77-1.51Z" />
696
+ <path fill="#fec901" fill-rule="evenodd" d="M9,88.75,52.12,14.16c5.24-8.25,13.54-8.46,18.87,0l42.43,73.69c3.39,6.81,1.71,16-9.33,15.77H17.61C10.35,103.8,5.67,97.43,9,88.75Z" />
697
+ <path fill="#010101" d="M57.57,83.78A5.53,5.53,0,0,1,61,82.2a5.6,5.6,0,0,1,2.4.36,5.7,5.7,0,0,1,2,1.3,5.56,5.56,0,0,1,1.54,5,6.23,6.23,0,0,1-.42,1.35,5.57,5.57,0,0,1-5.22,3.26,5.72,5.72,0,0,1-2.27-.53A5.51,5.51,0,0,1,56.28,90a5.18,5.18,0,0,1-.36-1.27,5.83,5.83,0,0,1-.06-1.31h0a6.53,6.53,0,0,1,.57-2,4.7,4.7,0,0,1,1.14-1.56Zm8.15-10.24c-.19,4.79-8.31,4.8-8.49,0-.82-8.21-2.92-29.34-2.86-37.05.07-2.38,2-3.79,4.56-4.33a12.83,12.83,0,0,1,5,0c2.61.56,4.65,2,4.65,4.44v.24L65.72,73.54Z" />
698
+ </svg>
691
699
  )
692
700
  };
@@ -1,8 +1,24 @@
1
+ function canBeJSON(str: string) {
2
+ return (
3
+ str === "null"
4
+ || str === "true"
5
+ || str === "false"
6
+ || str[0] === `"` && str[str.length - 1] === `"`
7
+ || str[0] === `[` && str[str.length - 1] === `]`
8
+ || str[0] === `{` && str[str.length - 1] === `}`
9
+ || (48 <= str.charCodeAt(0) && str.charCodeAt(0) <= 57)
10
+ || str.length > 1 && str[0] === "-" && (48 <= str.charCodeAt(1) && str.charCodeAt(1) <= 57)
11
+ );
12
+ }
13
+
1
14
  export function parseNice(text: string): unknown {
2
15
  if (text === "undefined") return undefined;
3
- try {
4
- return JSON.parse(text);
5
- } catch { }
16
+ if (text === undefined) return undefined;
17
+ if (canBeJSON(text)) {
18
+ try {
19
+ return JSON.parse(text);
20
+ } catch { }
21
+ }
6
22
  return text;
7
23
  }
8
24
  export function stringifyNice(value: unknown): string {
@@ -251,16 +251,18 @@ export class UserPage extends qreact.Component {
251
251
  <table>
252
252
  <tr>
253
253
  <th>User ID</th>
254
+ <th>Email</th>
254
255
  <th>Time</th>
255
256
  </tr>
256
- {sort(Object.entries(userObj.invitedUsers), x => -x[1].time).map(([userId, inviteData]) => {
257
+ {sort(Object.entries(userObj.invitedUsers2), x => -x[1].time).map(([email, inviteData]) => {
257
258
  return (
258
259
  <tr>
259
260
  <td>
260
- <Anchor values={[{ param: viewingUserURL, value: userId }]}>
261
- {userId}
261
+ <Anchor values={[{ param: viewingUserURL, value: inviteData.userId }]}>
262
+ {inviteData.userId}
262
263
  </Anchor>
263
264
  </td>
265
+ <td>{inviteData.email}</td>
264
266
  <td>{inviteData.time}</td>
265
267
  </tr>
266
268
  );
@@ -133,8 +133,10 @@ export type User = {
133
133
  };
134
134
 
135
135
  invitesRemaining: number;
136
- invitedUsers: {
137
- [userId: string]: {
136
+ invitedUsers2: {
137
+ [email: string]: {
138
+ userId: string;
139
+ email: string;
138
140
  time: number;
139
141
  };
140
142
  };
@@ -427,7 +429,7 @@ function getLoadingUserObj() {
427
429
  email: "",
428
430
  settings: { displayName: "" }, userType: "anonymous" as const,
429
431
  createTime: 0,
430
- machineIds: {}, allowedIPs: {}, loginTokens: {}, lastPageLoadsIPs: {}, invitedUsers: {},
432
+ machineIds: {}, allowedIPs: {}, loginTokens: {}, lastPageLoadsIPs: {}, invitedUsers2: {},
431
433
  invitesRemaining: 0
432
434
  };
433
435
  }
@@ -452,7 +454,7 @@ export function getCurrentUserObj(): User | undefined {
452
454
  allowedIPs: {},
453
455
  loginTokens: {},
454
456
  lastPageLoadsIPs: {},
455
- invitedUsers: {},
457
+ invitedUsers2: {},
456
458
  invitesRemaining: 0,
457
459
  };
458
460
  }
@@ -537,9 +539,10 @@ function internalCreateUser(config: {
537
539
  allowedIPs: {},
538
540
  loginTokens: {},
539
541
  lastPageLoadsIPs: {},
540
- invitedUsers: {},
542
+ invitedUsers2: {},
541
543
  invitesRemaining: 0,
542
544
  };
545
+ data().secure.emailToUserId[email] = userId;
543
546
  }
544
547
  return data().users[userId];
545
548
  }
@@ -776,7 +779,7 @@ function registerPageLoadTime() {
776
779
  function inviteUser(config: { email: string }) {
777
780
  Querysub.ignorePermissionsChecks(() => {
778
781
  let curUserObj = getUserObjAssert();
779
- if (config.email in curUserObj.invitedUsers) {
782
+ if (config.email in curUserObj.invitedUsers2) {
780
783
  console.info(`User ${config.email} already invited`);
781
784
  return;
782
785
  }
@@ -793,7 +796,9 @@ function inviteUser(config: { email: string }) {
793
796
  throw new Error("No invites remaining");
794
797
  }
795
798
 
796
- curUserObj.invitedUsers[config.email] = atomicObjectWrite({
799
+ curUserObj.invitedUsers2[email] = atomicObjectWrite({
800
+ userId,
801
+ email,
797
802
  time: Querysub.getCallTime(),
798
803
  });
799
804
  curUserObj.invitesRemaining--;
@@ -923,7 +928,7 @@ export function scriptCreateUser(config: {
923
928
  allowedIPs: {},
924
929
  loginTokens: {},
925
930
  lastPageLoadsIPs: {},
926
- invitedUsers: {},
931
+ invitedUsers2: {},
927
932
  invitesRemaining: 0,
928
933
  };
929
934
  data().secure.emailToUserId[email] = userId;
@@ -980,7 +985,7 @@ export async function registerServicePermissions() {
980
985
  allowedIPs: {},
981
986
  loginTokens: {},
982
987
  lastPageLoadsIPs: {},
983
- invitedUsers: {},
988
+ invitedUsers2: {},
984
989
  invitesRemaining: 0,
985
990
  };
986
991
  }