querysub 0.352.0 → 0.353.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.352.0",
3
+ "version": "0.353.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",
@@ -50,9 +50,9 @@
50
50
  "js-sha512": "^0.9.0",
51
51
  "node-forge": "https://github.com/sliftist/forge#e618181b469b07bdc70b968b0391beb8ef5fecd6",
52
52
  "pako": "^2.1.0",
53
- "socket-function": "^1.0.1",
53
+ "socket-function": "^1.0.4",
54
54
  "terser": "^5.31.0",
55
- "typesafecss": "^0.25.0",
55
+ "typesafecss": "^0.28.0",
56
56
  "yaml": "^2.5.0",
57
57
  "yargs": "^15.3.1"
58
58
  },
@@ -504,11 +504,13 @@ export function wrapArchivesWithCache(archives: Archives): Archives & {
504
504
  // way to check)
505
505
  // - TODO: Set hash in file metadata (maybe for all archive writes?), and use this to
506
506
  // compare it against our cache file (which can have the hash in the file name).
507
- let info = await archives.getInfo(fileName);
508
- if (!info) {
509
- // If it is gone remotely, remove it from the cache, to save space.
510
- await metrics.delCacheFile(archives, fileName);
511
- return;
507
+ if (!config?.fastRead) {
508
+ let info = await archives.getInfo(fileName);
509
+ if (!info) {
510
+ // If it is gone remotely, remove it from the cache, to save space.
511
+ await metrics.delCacheFile(archives, fileName);
512
+ return;
513
+ }
512
514
  }
513
515
  let buffer = await metrics.getCacheFile(archives, fileName, config);
514
516
  if (buffer) return buffer;
@@ -12,7 +12,12 @@ import { Args } from "socket-function/src/types";
12
12
  export interface Archives {
13
13
  getDebugName(): string;
14
14
 
15
- get(path: string, config?: { range?: { start: number; end: number; }; retryCount?: number }): Promise<Buffer | undefined>;
15
+ get(path: string, config?: {
16
+ range?: { start: number; end: number; };
17
+ retryCount?: number;
18
+ // Skips checking if the file exists on the remote. Only appropriate when the file name is fresh (recently obtained from find).
19
+ fastRead?: boolean;
20
+ }): Promise<Buffer | undefined>;
16
21
  set(path: string, data: Buffer): Promise<void>;
17
22
  del(path: string): Promise<void>;
18
23
 
@@ -520,7 +520,9 @@ export class ArchivesBackblaze {
520
520
  (err.stack.includes(`503`)
521
521
  || err.stack.includes(`"service_unavailable"`)
522
522
  || err.stack.includes(`"internal_error"`)
523
+ || err.stack.includes(`ENOBUFS`)
523
524
  ) && Date.now() - this.last503Reset > 60 * 1000) {
525
+ console.error("503 error, waiting a minute and resetting: " + err.message);
524
526
  this.log("503 error, waiting a minute and resetting: " + err.message);
525
527
  await delay(10 * 1000);
526
528
  // We check again in case, and in the very likely case that this is being run in parallel, we only want to reset once.
@@ -552,7 +554,9 @@ export class ArchivesBackblaze {
552
554
  || err.stack.includes(`getaddrinfo ENOTFOUND`)
553
555
  || err.stack.includes(`ECONNRESET`)
554
556
  || err.stack.includes(`ECONNREFUSED`)
557
+ || err.stack.includes(`ENOBUFS`)
555
558
  ) {
559
+ console.error("Retrying in 5s: " + err.message);
556
560
  this.log(err.message + " retrying in 5s");
557
561
  await delay(5000);
558
562
  return this.apiRetryLogic(fnc, retries - 1);
@@ -232,8 +232,9 @@ export class PathValueArchives {
232
232
  // (we read the target dest, find no file, read a bunch of other files over a few minutes, get to
233
233
  // the source file, but by now it has moved, so we don't see either file, even though at all times
234
234
  // both files existed).
235
+ let time = Date.now();
235
236
  dataPaths = await this.getValuePaths(authority);
236
- console.log(blue(` ${dataPaths.length} data paths`));
237
+ console.log(green(`${dataPaths.length} data paths in ${formatTime(Date.now() - time)}`));
237
238
  // NOTE: If the notMatched count is high enough, it is possible NodePathAuthorities.ts:authoritiesMightOverlap is
238
239
  // too loose, and should be removing more cases.
239
240
 
@@ -244,7 +245,7 @@ export class PathValueArchives {
244
245
  while (pendingDataPaths.length > 0) {
245
246
  let dataPath = pendingDataPaths.pop()!;
246
247
  if (readCache.has(dataPath)) continue;
247
- let data = await archives().get(dataPath);
248
+ let data = await archives().get(dataPath, { fastRead: true });
248
249
  if (!data) continue;
249
250
  readCache.set(dataPath, data);
250
251
  }
@@ -696,7 +696,7 @@ class AuthorityPathValueStorage {
696
696
  }
697
697
  return true;
698
698
  }
699
- public isSynced(path: string) {
699
+ public isSynced(path: string, ignoreParentSync = false) {
700
700
  if (pathValueAuthority2.isSelfAuthority(path)) return true;
701
701
 
702
702
  if (this.isSyncedCache.has(path)) return true;
@@ -716,8 +716,7 @@ class AuthorityPathValueStorage {
716
716
  // parent is synced
717
717
  // - See PathWatcher.watchPath, where when !noInitialTrigger (aka, initialTrigger),
718
718
  // we get all paths via authorityStorage.getPathsFromParent, and send their values.
719
- let parentPath = getParentPathStr(path);
720
- if (this.isParentSynced(parentPath)) {
719
+ if (!ignoreParentSync && this.isParentSynced(getParentPathStr(path))) {
721
720
  return true;
722
721
  }
723
722
 
@@ -970,6 +969,7 @@ class AuthorityPathValueStorage {
970
969
  // and while the values are undefined and so we are deleting them... we have definitely
971
970
  // received values!
972
971
  let path = values[0].path;
972
+
973
973
  this.isSyncedCache.add(path);
974
974
  this.removePathFromStorage(path, "value is undefined and golden");
975
975
  values.splice(0, values.length);
@@ -1493,6 +1493,7 @@ class PathWatcher {
1493
1493
  changes = new Set();
1494
1494
  changedPerCallbacks.set(watcher, changes);
1495
1495
  }
1496
+ // @ts-ignore
1496
1497
  changes.add(value);
1497
1498
  }
1498
1499
 
@@ -19,11 +19,7 @@ import debugbreak from "debugbreak";
19
19
  import { isDevDebugbreak } from "../config";
20
20
  import { auditLog } from "../0-path-value-core/auditLogs";
21
21
 
22
- // NOTE: In some cases this can half our the PathValues sent. AND, it can reduce our sync requests
23
- // by N, and as those are uploads those are event more expensive. However, it also complicates
24
- // other code, requiring not just checking if a value is synced, but also the parent path. We should
25
- // try to make this stable, but worst case scenario, we can set this to false, and it will make
26
- // our synchronization a lot easier to verify.
22
+ // NOTE: In some cases this can half our the PathValues sent. AND, it can reduce our sync requests by N, and as those are uploads those are event more expensive. However, it also complicates other code, requiring not just checking if a value is synced, but also the parent path. We should try to make this stable, but worst case scenario, we can set this to false, and it will make our synchronization a lot easier to verify.
27
23
  const STOP_KEYS_DOUBLE_SENDS = true;
28
24
 
29
25
  export class RemoteWatcher {
@@ -175,9 +175,9 @@ export class ClientWatcher {
175
175
  };
176
176
  const triggerWatcherParent = (spec: WatchSpec, parent: string) => {
177
177
  if (this.ignoreWatch?.(spec, parent)) return;
178
- onTrigger(spec, parent);
179
178
  let trigger = this.triggerWatcher(spec);
180
179
  trigger.newParentsSynced.add(parent);
180
+ onTrigger(spec, parent);
181
181
  };
182
182
  for (let value of values) {
183
183
  let watchers = this.valueFunctionWatchers.get(value.path);
@@ -214,6 +214,25 @@ export class ClientWatcher {
214
214
  }
215
215
  }
216
216
  }
217
+ // Trigger all children of parents synced
218
+ // NOTE: This is the counterpointer to RemoteWatcher's removal of extra requests when we are already syncing the parent path. If it's not going to request them, we're not going to get the paths, so we need to trigger based on parents.
219
+ // - We ignore hack paths here (that path in range requests in parent paths), because RemoteWatcher also ignores them (so hacked parent paths won't stop value path syncs there), so they will be received normally.
220
+ // NOTE: This will probably be slow, but we shouldn't be receiving parent syncs very frequently. Or maybe we will, and maybe this will just be incredibly slow.
221
+ if (parentsSynced.length > 0) {
222
+ measureBlock(() => {
223
+ for (let parentPath of parentsSynced) {
224
+ for (let [path, lookup] of this.valueFunctionWatchers.entries()) {
225
+ if (!path.startsWith(parentPath)) continue;
226
+ if (getParentPathStr(path) !== parentPath) continue;
227
+ for (let watch of lookup.values()) {
228
+ let trigger = this.triggerWatcher(watch);
229
+ trigger.paths.add(path);
230
+ onTrigger(watch, path);
231
+ }
232
+ }
233
+ }
234
+ }, "ClientWatcher|triggerChildrenOfParentsSynced");
235
+ }
217
236
  }
218
237
 
219
238
  public debugBreakOnNextTrigger() {
@@ -536,85 +555,89 @@ export class ClientWatcher {
536
555
  });
537
556
  });
538
557
 
539
- // NOTE: The promise USUALLY doesn't resolve until all the watches have synced (except in certain error cases).
540
- // NOTE: The callback is only called when synced values change (so if they are already synced, and don't change,
541
- // the callback won't be called).
542
- // NOTE: Doesn't call the callback until all requested values have finished their initial sync, to prevent
543
- // too many callback callbacks.
558
+
559
+ // TODO: EVENTUALLY! We should accept a delta on the watches, which should allow for more efficient updates.
560
+
561
+ // NOTE: The callback is only called when synced values change (so if they are already synced, and don't change, the callback won't be called).
562
+ // NOTE: Doesn't call the callback until all requested values have finished their initial sync, to prevent too many callback callbacks.
544
563
  // - You can just fragment your writes into different components to isolate any slow loading parts, so partial
545
564
  // loading of data really isn't needed.
546
565
  // NOTE: Takes ownership of paths and parentPaths, so... don't mutate them after calling this!
547
566
  @measureFnc
548
567
  public setWatches(watchSpec: WatchSpec) {
549
- let callback = watchSpec.callback;
550
- let paths = watchSpec.paths;
551
- let parentPaths = watchSpec.parentPaths;
568
+ // NOTE: Yes, setWatches shows up as being slow in the profiler. I added measureBlock to every single section, and none of them showed up as being slow.
569
+ // - My best guess is that after we clobber values in various lookups, but BEFORE we return, the destructor for those old resources runs. So their destruct time gets put in our profiler?
570
+ // - Or maybe hack_stripPackedPath, decodeParentFilter, or other functions with heavy temporary allocations.
571
+ // - measureBlock also has overhead (which is why I commented them all out), but... It's not nearly enough to account for this.
572
+
573
+
574
+ //measureBlock(() => {
552
575
  watchSpec.order = watchSpec.order ?? nextWatchSpecOrder++;
576
+ //}, "ClientWatcher()|watchSpec.order = ...");
553
577
 
578
+ //measureBlock(() => {
554
579
  this.updateUnwatches(watchSpec);
580
+ //}, "ClientWatcher()|updateUnwatches wrapper?");
555
581
 
556
- this.allWatchers.set(callback, watchSpec);
557
- measureBlock(() => {
558
- for (let path of paths) {
559
- let watchers = this.valueFunctionWatchers.get(path);
560
- if (!watchers) {
561
- watchers = new Map();
562
- this.valueFunctionWatchers.set(path, watchers);
563
- }
564
- watchers.set(callback, watchSpec);
565
- this.pendingUnwatches.delete(path);
582
+ //measureBlock(() => {
583
+ this.allWatchers.set(watchSpec.callback, watchSpec);
584
+ //}, "ClientWatcher()|setWatches|this.allWatcher.set");
585
+ //measureBlock(() => {
586
+ for (let path of watchSpec.paths) {
587
+ let watchers = this.valueFunctionWatchers.get(path);
588
+ if (!watchers) {
589
+ watchers = new Map();
590
+ this.valueFunctionWatchers.set(path, watchers);
566
591
  }
567
- }, "ClientWatcher()|setWatches|paths");
568
- measureBlock(() => {
569
- for (let path of parentPaths) {
570
- let basePath = hack_stripPackedPath(path);
571
- let watchersBase = this.parentValueFunctionWatchers.get(basePath);
572
- if (!watchersBase) {
573
- watchersBase = new Map();
574
- this.parentValueFunctionWatchers.set(basePath, watchersBase);
575
- }
576
- let watchers = watchersBase.get(path);
577
- if (!watchers) {
578
- let range = decodeParentFilter(path) || { start: 0, end: 1 };
579
- watchers = {
580
- start: range.start,
581
- end: range.end,
582
- lookup: new Map()
583
- };
584
- watchersBase.set(path, watchers);
585
- }
586
- watchers.lookup.set(callback, watchSpec);
587
- this.pendingParentUnwatches.delete(path);
592
+ watchers.set(watchSpec.callback, watchSpec);
593
+ this.pendingUnwatches.delete(path);
594
+ }
595
+ //}, "ClientWatcher()|setWatches|paths");
596
+ //measureBlock(() => {
597
+ for (let path of watchSpec.parentPaths) {
598
+ let basePath = hack_stripPackedPath(path);
599
+ let watchersBase = this.parentValueFunctionWatchers.get(basePath);
600
+ if (!watchersBase) {
601
+ watchersBase = new Map();
602
+ this.parentValueFunctionWatchers.set(basePath, watchersBase);
588
603
  }
589
- }, "ClientWatcher()|setWatches|parentPaths");
590
-
591
- let pathsArray!: string[];
592
- let parentPathsArray!: string[];
593
- measureBlock(() => {
594
- pathsArray = Array.from(paths);
595
- parentPathsArray = Array.from(parentPaths);
596
- }, "ClientWatcher()|setWatches|Array.from");
597
- let debugName = watchSpec.callback.name;
604
+ let watchers = watchersBase.get(path);
605
+ if (!watchers) {
606
+ let range = decodeParentFilter(path) || { start: 0, end: 1 };
607
+ watchers = {
608
+ start: range.start,
609
+ end: range.end,
610
+ lookup: new Map()
611
+ };
612
+ watchersBase.set(path, watchers);
613
+ }
614
+ watchers.lookup.set(watchSpec.callback, watchSpec);
615
+ this.pendingParentUnwatches.delete(path);
616
+ }
617
+ //}, "ClientWatcher()|setWatches|parentPaths");
598
618
 
599
- if (pathsArray.length === 0 && parentPathsArray.length === 0) {
619
+ if (watchSpec.paths.size === 0 && watchSpec.parentPaths.size === 0) {
600
620
  return;
601
621
  }
602
622
 
603
- measureBlock(() => {
604
- // Watch it locally as well, so when we get the value we know to trigger the client callback
605
- pathWatcher.watchPath({
606
- callback: getOwnNodeId(),
607
- paths: pathsArray,
608
- parentPaths: parentPathsArray,
609
- debugName,
610
- noInitialTrigger: watchSpec.noInitialTrigger,
611
- });
612
- remoteWatcher.watchLatest({
613
- paths: pathsArray,
614
- parentPaths: parentPathsArray,
615
- debugName,
616
- });
617
- }, "ClientWatcher()|setWatches|watchCalls");
623
+ //measureBlock(() => {
624
+ let pathsArray = Array.from(watchSpec.paths);
625
+ let parentPathsArray = Array.from(watchSpec.parentPaths);
626
+ let debugName = watchSpec.callback.name;
627
+ // Watch it locally as well, so when we get the value we know to trigger the client callback
628
+ pathWatcher.watchPath({
629
+ callback: getOwnNodeId(),
630
+ paths: pathsArray,
631
+ parentPaths: parentPathsArray,
632
+ debugName,
633
+ noInitialTrigger: watchSpec.noInitialTrigger,
634
+ });
635
+ remoteWatcher.watchLatest({
636
+ paths: pathsArray,
637
+ parentPaths: parentPathsArray,
638
+ debugName,
639
+ });
640
+ //}, "ClientWatcher()|setWatches|watchCalls");
618
641
  }
619
642
 
620
643
  private lastVersions = registerResource("paths|lastVersion", new Map<number, number>());
@@ -1,6 +1,6 @@
1
1
  import { measureCode, measureWrap, registerMeasureInfo } from "socket-function/src/profiling/measure";
2
2
  import { SocketFunction } from "socket-function/SocketFunction";
3
- import { binarySearchBasic, binarySearchBasic2, binarySearchIndex, deepCloneJSON, getKeys, insertIntoSortedList, isNode, recursiveFreeze, timeInMinute, timeInSecond } from "socket-function/src/misc";
3
+ import { binarySearchBasic, binarySearchBasic2, binarySearchIndex, deepCloneJSON, getKeys, insertIntoSortedList, isNode, recursiveFreeze, sort, timeInMinute, timeInSecond } from "socket-function/src/misc";
4
4
  import { canHaveChildren, MaybePromise } from "socket-function/src/types";
5
5
  import { blue, green, magenta, red, yellow } from "socket-function/src/formatting/logColors";
6
6
  import { cache, cacheLimited, lazy } from "socket-function/src/caching";
@@ -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, formatTime } from "socket-function/src/formatting/format";
35
+ import { formatNumber, 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
  import { isAsyncFunction } from "../misc";
@@ -142,6 +142,9 @@ export interface WatcherOptions<Result> {
142
142
  */
143
143
  runImmediately?: boolean;
144
144
 
145
+ /** By default we call pathValueCommitter.waitForValuesToCommit. This skips that check. */
146
+ noWaitForCommit?: boolean;
147
+
145
148
  /** HACK: Used in conjuction with specific callers to prevent disposing, so the watcher
146
149
  * can be reused for specific cases.
147
150
  */
@@ -1183,7 +1186,9 @@ export class PathValueProxyWatcher {
1183
1186
  );
1184
1187
  };
1185
1188
  const getReadyToCommit = () => {
1186
- let blocked = isProxyBlockedByOrder(watcher);
1189
+ if (watcher.options.commitAllRuns) {
1190
+ return true;
1191
+ }
1187
1192
  return (
1188
1193
  watcher.lastUnsyncedAccesses.size === 0
1189
1194
  && watcher.lastUnsyncedParentAccesses.size === 0
@@ -1192,10 +1197,17 @@ export class PathValueProxyWatcher {
1192
1197
  );
1193
1198
  };
1194
1199
  function afterTrigger() {
1195
- let callbacks = watcher.onAfterTriggered;
1196
- watcher.onAfterTriggered = [];
1197
- for (let callback of callbacks) {
1198
- logErrors(((async () => { await callback(); }))());
1200
+ for (let i = 0; i < 10; i++) {
1201
+ let callbacks = watcher.onAfterTriggered;
1202
+ watcher.onAfterTriggered = [];
1203
+ for (let callback of callbacks) {
1204
+ logErrors(((async () => { await callback(); }))());
1205
+ }
1206
+ if (watcher.onAfterTriggered.length === 0) break;
1207
+ }
1208
+ if (watcher.onAfterTriggered.length > 0) {
1209
+ console.warn(`Watcher ${watcher.debugName} keeps adding after trigger callbacks during after trigger. We reached our iteration limit
1210
+ and will be ignoring triggers remaining triggers.`);
1199
1211
  }
1200
1212
  }
1201
1213
 
@@ -1204,16 +1216,25 @@ export class PathValueProxyWatcher {
1204
1216
  watcher.disposed = true;
1205
1217
  clientWatcher.unwatch(trigger);
1206
1218
  self.allWatchers.delete(watcher);
1207
- let disposeCallbacks = watcher.onInnerDisposed;
1208
- watcher.onInnerDisposed = [];
1209
- for (let callback of disposeCallbacks) {
1210
- logErrors(((async () => { await callback(); }))());
1211
- }
1212
-
1213
- self.allWatchersLookup.delete(trigger);
1214
1219
 
1220
+ // NOTE: We trigger next before onInnerDisposed, as onInnerDispose often run more commits, and if we proxy block it causes proxies to run twice.
1215
1221
  // TODO: Is this guaranteed to be called all the time? What about nested proxy watchers?
1216
1222
  void finishProxyAndTriggerNext(watcher);
1223
+
1224
+ for (let i = 0; i < 10; i++) {
1225
+ let disposeCallbacks = watcher.onInnerDisposed;
1226
+ watcher.onInnerDisposed = [];
1227
+ for (let callback of disposeCallbacks) {
1228
+ logErrors(((async () => { await callback(); }))());
1229
+ }
1230
+ if (watcher.onInnerDisposed.length === 0) break;
1231
+ }
1232
+ if (watcher.onInnerDisposed.length > 0) {
1233
+ console.warn(`Watcher ${watcher.debugName} keeps adding inner disposed callbacks during dispose. We reached our iteration limit
1234
+ and will be ignoring dispose callbacks remaining dispose callbacks.`);
1235
+ }
1236
+
1237
+ self.allWatchersLookup.delete(trigger);
1217
1238
  }
1218
1239
  function runWatcher() {
1219
1240
  watcher.pendingWrites.clear();
@@ -1485,12 +1506,13 @@ export class PathValueProxyWatcher {
1485
1506
  parentPaths: watcher.pendingWatches.parentPaths,
1486
1507
  };
1487
1508
 
1509
+ // NOTE: I don't see the reason for this. How many watchers do we have that aren't watching anything?
1488
1510
  // If nothing is being watched, and nothing was... then don't bother updating the watchers
1489
- if (
1490
- !initialTriggerWatch
1491
- && watcher.lastWatches.paths.size === 0 && watcher.lastWatches.parentPaths.size === 0
1492
- && watcher.pendingWatches.paths.size === 0 && watcher.pendingWatches.parentPaths.size === 0
1493
- ) return;
1511
+ // if (
1512
+ // !initialTriggerWatch
1513
+ // && watcher.lastWatches.paths.size === 0 && watcher.lastWatches.parentPaths.size === 0
1514
+ // && watcher.pendingWatches.paths.size === 0 && watcher.pendingWatches.parentPaths.size === 0
1515
+ // ) return;
1494
1516
 
1495
1517
  clientWatcher.setWatches({
1496
1518
  debugName: watcher.debugName,
@@ -1649,6 +1671,7 @@ export class PathValueProxyWatcher {
1649
1671
 
1650
1672
  }
1651
1673
  const hasUnsyncedBefore = measureWrap(function proxyWatchHasUnsyncedBefore() {
1674
+ if (watcher.options.commitAllRuns) return false;
1652
1675
  // NOTE: We COULD remove any synced values from lastUnsyncedAccesses, however... we will generally sync all values at once, so we don't really need to optimize the cascading case here. Also... deleting values requires cloning while we iterate, as well as mutating the set, which probably makes the non-cascading case slower.
1653
1676
  for (let path of watcher.lastUnsyncedAccesses) {
1654
1677
  if (!authorityStorage.isSynced(path)) {
@@ -1840,11 +1863,10 @@ export class PathValueProxyWatcher {
1840
1863
  }
1841
1864
 
1842
1865
  if (SHOULD_TRACE) {
1843
- console.log(green(`Committing ${values?.length || 0} watcher writes ${watcher.debugName} at ${time}`), watcher.triggeredByChanges);
1866
+ console.log(green(`Committing ${values?.length || 0} watcher writes ${watcher.debugName} at ${time}`), watcher.triggeredByChanges, watcher.lastUnsyncedAccesses, watcher.lastUnsyncedParentAccesses);
1844
1867
  }
1845
1868
 
1846
- // ONLY clear triggeredByChanges when we have a succesful run, otherwise delta based
1847
- // watchers won't work!
1869
+ // ONLY clear triggeredByChanges when we have a succesful run, otherwise delta based watchers won't work!
1848
1870
  watcher.triggeredByChanges = undefined;
1849
1871
  options.onResultUpdated?.(result, values, watcher);
1850
1872
  };
@@ -1978,7 +2000,7 @@ export class PathValueProxyWatcher {
1978
2000
  let result = await resultPromise;
1979
2001
  // NOTE: Now we ALWAYS wait, to prevent services from terminating their process before their writes finish.
1980
2002
  // It is just too hard to remember to call pathValueCommitter.waitForValuesToCommit in every service.
1981
- if (options.canWrite && anyWrites && !options.dryRun) {
2003
+ if (options.canWrite && anyWrites && !options.dryRun && !options.noWaitForCommit) {
1982
2004
  await pathValueCommitter.waitForValuesToCommit();
1983
2005
  }
1984
2006
  return result;
@@ -2414,7 +2436,7 @@ const finishProxyAndTriggerNext = runInSerial(
2414
2436
  return;
2415
2437
  }
2416
2438
 
2417
- // NOTE: We have to wait to the next tick, as finish order code is most often on for async functions, in which case, even though we will have called onResultUpdated, the caller He's waiting on a promise, so we need to give it a tick to actually receive the value.
2439
+ // NOTE: We have to wait to the next tick, as finish order code is most often on for async functions, in which case, even though we will have called onResultUpdated, the caller's waiting on a promise, so we need to give it a tick to actually receive the value.
2418
2440
  // - This shouldn't cause any actual slowdown because people are calling the functions and usually awaiting them, so they won't notice a difference.
2419
2441
  // HACK: We wait 10 times because I guess there's a lot of change promises. I found five works, but adding more is fine. This is pretty bad, but... I don't think there's anything that can possibly be expecting an ordered function to finish synchronously. Anything that is waiting on the ordered function is going to actually be waiting with an await, so we could iterate 100 times here and it wouldn't break anything.
2420
2442
  for (let i = 0; i < 10; i++) {
@@ -2460,4 +2482,27 @@ export function getCurrentCallCreationProxy() {
2460
2482
  return currentCallCreationProxy;
2461
2483
  }
2462
2484
 
2485
+
2486
+ registerPeriodic(function logWatcherTypes() {
2487
+ let allWatchers = proxyWatcher.getAllWatchers();
2488
+ let counts = new Map<string, { count: number; }>();
2489
+ for (let watcher of allWatchers) {
2490
+ let type = watcher.debugName.split("|")[0] || "no debug name";
2491
+ let countObj = counts.get(type);
2492
+ if (!countObj) {
2493
+ countObj = { count: 0 };
2494
+ counts.set(type, countObj);
2495
+ }
2496
+ countObj.count++;
2497
+ }
2498
+ let sorted = sort(Array.from(counts.entries()), x => -x[1].count);
2499
+
2500
+ console.log(`${formatNumber(allWatchers.size)} watchers | ${formatNumber(sorted.length)} types`);
2501
+ // Log the top 5
2502
+ for (let [type, countObj] of sorted.slice(0, 5)) {
2503
+ let countText = `${formatPercent(countObj.count / allWatchers.size)} (${formatNumber(countObj.count)})`;
2504
+ console.log(` ${countText.padEnd(15, " ")} ${type}`);
2505
+ }
2506
+ });
2507
+
2463
2508
  // #endregion Proxy Ordering
@@ -15,7 +15,7 @@ import { __getRoutingHash, authorityStorage, compareTime, debugTime, epochTime,
15
15
  import { getModuleFromSpec, watchModuleHotreloads } from "./pathFunctionLoader";
16
16
  import debugbreak from "debugbreak";
17
17
  import { parseArgs } from "./PathFunctionHelpers";
18
- import { PERMISSIONS_FUNCTION_ID, getAllDevelopmentModulesIds, getDevelopmentModule, getExportPath, getModuleRelativePath, getSchemaObject } from "./syncSchema";
18
+ import { FunctionMetadata, PERMISSIONS_FUNCTION_ID, getAllDevelopmentModulesIds, getDevelopmentModule, getExportPath, getModuleRelativePath, getSchemaObject } from "./syncSchema";
19
19
  import { formatTime } from "socket-function/src/formatting/format";
20
20
  import { getControllerNodeIdList, set_debug_getFunctionRunnerShards } from "../-g-core-values/NodeCapabilities";
21
21
  import { FilterSelector, Filterable, doesMatch } from "../misc/filterable";
@@ -511,6 +511,14 @@ export class PathFunctionRunner {
511
511
  throw new Error(`Export at ${JSON.stringify(exportPath.join("."))} was not a function (it was ${typeof baseFunction})`);
512
512
  }
513
513
 
514
+ let devFunctionMetadata: FunctionMetadata | undefined;
515
+
516
+ let devModule = getDevelopmentModule(functionSpec.ModuleId);
517
+ if (devModule) {
518
+ let schemaObj = getSchemaObject(devModule);
519
+ devFunctionMetadata = schemaObj?.functionMetadata?.[functionSpec.FunctionId];
520
+ }
521
+
514
522
  await proxyWatcher.commitFunction({
515
523
  canWrite: true,
516
524
  debugName: getDebugName(callPath, functionSpec),
@@ -518,7 +526,7 @@ export class PathFunctionRunner {
518
526
  getPermissionsCheck: PermissionsChecker && (() => new PermissionsChecker(callPath)),
519
527
  nestedCalls: "inline",
520
528
  temporary: true,
521
- maxLocksOverride: functionSpec.maxLocksOverride,
529
+ maxLocksOverride: devFunctionMetadata ? devFunctionMetadata.maxLocksOverride : functionSpec.maxLocksOverride,
522
530
  watchFunction: function runCallWatcher() {
523
531
  runCount++;
524
532
  if (PathFunctionRunner.DEBUG_CALLS) {
@@ -164,6 +164,7 @@ export class qreact {
164
164
  public static Component = Component;
165
165
  public static createElement = createElement;
166
166
  public static Fragment = Fragment;
167
+ public static inline = setInlineRenderFlag;
167
168
 
168
169
  /** See QComponentStatic.jsonComparePropUpdates */
169
170
  public static ComponentJSONCompare = ComponentJSONCompare;
@@ -208,7 +209,7 @@ export class qreact {
208
209
 
209
210
  public static cancelRender = cancelRender;
210
211
  public static isInRender = isInRender;
211
- public static globalOnMountWatch = globalOnMountWatch;
212
+ public static globalAfterRenderWatch = globalAfterRenderWatch;
212
213
  public static allComponents = new Set<ExternalRenderClass>();
213
214
 
214
215
  /** The threshold at which we log component render times
@@ -222,7 +223,6 @@ export class qreact {
222
223
  (globalThis as any).qreact = qreact;
223
224
 
224
225
 
225
-
226
226
  export namespace qreact {
227
227
  export type VNode = preact.ComponentChildren;
228
228
  export type ComponentChildren = preact.ComponentChildren;
@@ -300,6 +300,23 @@ module.remapExports = (exports, callingModule) => {
300
300
  };
301
301
 
302
302
 
303
+
304
+ type InlineFunction = {
305
+ /** NOTE: TypeScript will get unhappy if the function returns just component children, as in a string or something. Just forcefully cast it if this happens. It'll work fine, an inline function which returns a string, undefined, etc, will render without issue. */
306
+ (props: any, parentContext: any | undefined): preact.VNode;
307
+ isInlineFunction?: true;
308
+ }
309
+ /** Used to set a flag on a function which will cause it to render inline, which means it will be evaluated immediately, the same as if you called a function.
310
+ */
311
+ export function setInlineRenderFlag(fnc: InlineFunction) {
312
+ fnc.isInlineFunction = true;
313
+ return fnc;
314
+ }
315
+ function isInlineFunction(fnc: Function): fnc is InlineFunction {
316
+ return (fnc as InlineFunction).isInlineFunction === true;
317
+ }
318
+
319
+
303
320
  const elementCtor = heapTagObj("JSXElement");
304
321
  function createElement(
305
322
  type: qreact.ComponentClass | string,
@@ -317,8 +334,15 @@ function createElementBase(
317
334
  ): preact.VNode<{ [key: string]: unknown }> {
318
335
  props = props || {};
319
336
  if (children.length) {
337
+ if (children.length === 1 && !Array.isArray(children[0])) {
338
+ children = [children[0]];
339
+ }
320
340
  props.children = children;
321
341
  }
342
+ // TODO: Support nested inline calls as well
343
+ if (typeof type === "function" && isInlineFunction(type)) {
344
+ return type(props, getRenderingComponent());
345
+ }
322
346
  return elementCtor({
323
347
  type,
324
348
  props: props as any,
@@ -1432,7 +1456,7 @@ class QRenderClass {
1432
1456
  contextCommit(() => props.ref?.(self.instance as any));
1433
1457
  }
1434
1458
 
1435
- triggerGlobalOnMountWatch(self);
1459
+ triggerGlobalAfterRenderWatch(self);
1436
1460
 
1437
1461
  if (Querysub.anyUnsynced()) {
1438
1462
  debugger;
@@ -2735,36 +2759,36 @@ registerMeasureInfo(() => {
2735
2759
  // #endregion
2736
2760
 
2737
2761
 
2738
- let pendingGlobalOnMountWatches: QRenderClass[] = [];
2762
+ let pendingGlobalAfterRenderWatches: QRenderClass[] = [];
2739
2763
 
2740
- let globalOnMountWatches: ((component: QRenderClass[]) => void)[] = [];
2741
- function globalOnMountWatch(callback: (component: ExternalRenderClass[]) => void) {
2742
- globalOnMountWatches.push(callback);
2764
+ let globalAfterRenderWatches: ((component: QRenderClass[]) => void)[] = [];
2765
+ function globalAfterRenderWatch(callback: (component: ExternalRenderClass[]) => void) {
2766
+ globalAfterRenderWatches.push(callback);
2743
2767
  return () => {
2744
- let index = globalOnMountWatches.indexOf(callback);
2768
+ let index = globalAfterRenderWatches.indexOf(callback);
2745
2769
  if (index !== -1) {
2746
- globalOnMountWatches.splice(index, 1);
2770
+ globalAfterRenderWatches.splice(index, 1);
2747
2771
  }
2748
2772
  };
2749
2773
  }
2750
2774
  let baseTrigger = lazy(async () => {
2751
2775
  await clientWatcher.waitForTriggerFinished();
2752
2776
  baseTrigger.reset();
2753
- let current = pendingGlobalOnMountWatches;
2754
- pendingGlobalOnMountWatches = [];
2755
- for (let callback of globalOnMountWatches) {
2777
+ let current = pendingGlobalAfterRenderWatches;
2778
+ pendingGlobalAfterRenderWatches = [];
2779
+ for (let callback of globalAfterRenderWatches) {
2756
2780
  try {
2757
2781
  Querysub.commit(() => {
2758
2782
  callback(current);
2759
2783
  });
2760
2784
  } catch (error) {
2761
- console.error(`Error in globalOnMountWatch callback: ${error}`, error);
2785
+ console.error(`Error in globalAfterRenderWatch callback: ${error}`, error);
2762
2786
  }
2763
2787
  }
2764
2788
  });
2765
- function triggerGlobalOnMountWatch(component: QRenderClass) {
2789
+ function triggerGlobalAfterRenderWatch(component: QRenderClass) {
2766
2790
  qreact.domUpdateCount++;
2767
- pendingGlobalOnMountWatches.push(component);
2791
+ pendingGlobalAfterRenderWatches.push(component);
2768
2792
  void baseTrigger();
2769
2793
  }
2770
2794
 
@@ -270,7 +270,7 @@ export class Querysub {
270
270
  /** Calls the callback any time components are mounted to do the DOM (which happens after
271
271
  * time they render, not just the first time).
272
272
  */
273
- public static globalOnMountWatch = (callback: (components: ExternalRenderClass[]) => void) => qreact.globalOnMountWatch(callback);
273
+ public static globalAfterRenderWatch = (callback: (components: ExternalRenderClass[]) => void) => qreact.globalAfterRenderWatch(callback);
274
274
 
275
275
  public static doAtomicWrites = <T>(callback: () => T): T => doAtomicWrites(callback);
276
276
 
@@ -319,10 +319,14 @@ export class Querysub {
319
319
  public static runSynced = Querysub.serviceWriteDetached;
320
320
  public static commit = Querysub.serviceWriteDetached;
321
321
  public static write = Querysub.serviceWriteDetached;
322
+
323
+ // NOTE: We might want to add a kind of commit function that has finish and start order set by default, as this is kind of what most people assume. However, it adds so much lag that it definitely shouldn't be the default, even when we're client-side.
322
324
  public static serviceWriteDetached(fnc: () => unknown, options?: Partial<WatcherOptions<unknown>>) {
323
325
  logErrors(proxyWatcher.commitFunction({
324
326
  canWrite: true,
325
327
  watchFunction: fnc,
328
+ //finishInStartOrder: isClient(),
329
+ noWaitForCommit: isClient(),
326
330
  ...options,
327
331
  }));
328
332
  }
@@ -342,18 +346,16 @@ export class Querysub {
342
346
  public static syncedCommit = Querysub.serviceWrite;
343
347
  public static commitSynced = Querysub.serviceWrite;
344
348
  public static commitAsync = Querysub.serviceWrite;
345
- /** IMPORTANT! Functions called with commitAsync will finish in their call order, allowing you to use this to read remote values and commit to either remote state or local state, and have the writes be applied in the same order as if they all happened in serial. If you don't want this, or don't need this, you can either use Querysub.commit (If you don't need to wait for it to finish or access the result), or you can pass { finishInStartOrder: false }
346
- * - ONLY DEFAULTED WHEN isClient()
347
- */
348
349
  public static async serviceWrite<T>(fnc: () => T, options?: Partial<WatcherOptions<T>>) {
349
350
  let calls: CallSpec[] = [];
350
351
  let result = await proxyWatcher.commitFunction({
351
352
  canWrite: true,
352
353
  watchFunction: fnc,
353
- finishInStartOrder: isClient(),
354
354
  onCallCommit(call, metadata) {
355
355
  calls.push(call);
356
356
  },
357
+ // The only reason we really wait for the commit is for server side so we don't terminate the process before the write finishes. But if we are client side this isn't an issue.
358
+ noWaitForCommit: isClient(),
357
359
  ...options,
358
360
  });
359
361
  // Wait for calls to predict. This SHOULDN'T take very long. This allows us to use serviceWrite
@@ -373,7 +375,7 @@ export class Querysub {
373
375
  });
374
376
  }
375
377
  public static fastReadAsync<T>(fnc: () => T, options?: Partial<WatcherOptions<T>>) {
376
- return Querysub.serviceWrite(fnc, { ...options, allowProxyResults: true });
378
+ return Querysub.serviceWrite(fnc, { ...options, allowProxyResults: true, noWaitForCommit: true });
377
379
  }
378
380
  public static localRead<T>(fnc: () => T, options?: Partial<WatcherOptions<T>>) {
379
381
  return proxyWatcher.runOnce({
@@ -102,7 +102,7 @@ export function closeAllModals() {
102
102
  });
103
103
  }
104
104
  export function isShowingModal() {
105
- return Object.values(data().modals).length > 0;
105
+ return Object.values(data().modals).filter(x => !x.onlyExplicitClose).length > 0;
106
106
  }
107
107
 
108
108
 
@@ -36,7 +36,7 @@ if (isNode()) {
36
36
  }
37
37
 
38
38
  // IMPORTANT! No newlines, so we don't break sourcemaps
39
- return `var console = (${shimConsole.toString().replaceAll("\n", " ")})(); ${contents}`;
39
+ return `"use strict"; var console = (${shimConsole.toString().replaceAll("\n", " ")})(); ${contents}`;
40
40
  });
41
41
 
42
42
  let diskShimConsoleLogs = require("./diskShimConsoleLogs") as typeof import("./diskShimConsoleLogs");
package/src/errors.ts CHANGED
@@ -110,6 +110,7 @@ export function errorify(error: any, messageOverride?: string) {
110
110
  let errorObj = new Error();
111
111
  if (typeof error === "string" && error.includes("\n")) {
112
112
  errorObj.stack = error;
113
+ errorObj.message = error.split("\n")[0];
113
114
  } else {
114
115
  errorObj.message = error;
115
116
  errorObj.stack = error + "\n" + errorObj.stack;
@@ -2,10 +2,16 @@
2
2
 
3
3
  // NOTE: Many cases we don't know if we want to throttle, or how long we want to throttle for, until we start the watcher, so this has to be done inside our render, instead of in ProxyWatcher.
4
4
 
5
- import { cache } from "socket-function/src/caching";
5
+ import { cache, lazy } from "socket-function/src/caching";
6
6
  import { proxyWatcher, SyncWatcher } from "../2-proxy/PathValueProxyWatcher";
7
7
  import { qreact } from "../4-dom/qreact";
8
8
  import { onNextPaint } from "./onNextPaint";
9
+ import { Querysub } from "../4-querysub/QuerysubController";
10
+
11
+ let disabled = false;
12
+ export function setThrottleRenderDisabled(value: boolean) {
13
+ disabled = value;
14
+ }
9
15
 
10
16
  // Throttles calls that have the same throttleKey
11
17
  /** Used near the start of your render, like so:
@@ -26,6 +32,7 @@ export function throttleRender(config: {
26
32
  // Delays for additional frames. Ex, set frameDelay to 0, smear to 60, and count to 30. The first delay is 0, then 30, then 60
27
33
  frameDelayCount?: number;
28
34
  }): boolean {
35
+ if (disabled) return false;
29
36
  let watcher = proxyWatcher.getTriggeredWatcher();
30
37
  // Never throttle the first render, as that would be noticeable and always unintended
31
38
  if (watcher.syncRunCount === 0) return false;
@@ -186,8 +186,9 @@ export function getAnimationFrames() {
186
186
  return frames;
187
187
  }
188
188
 
189
+
190
+
189
191
  export class Button extends qreact.Component<ButtonProps> {
190
- onFocusText = "";
191
192
  element: HTMLButtonElement | null = null;
192
193
  componentDidMount(): void {
193
194
  registeredWatches.add(this);
@@ -195,150 +196,162 @@ export class Button extends qreact.Component<ButtonProps> {
195
196
  componentWillUnmount(): void {
196
197
  registeredWatches.delete(this);
197
198
  }
198
- static renderInline(props: ButtonProps) { return true; }
199
- render() {
200
- let { square, flavor, children, className, style, hue, showHotkeys, hotkeys, hotkeyPosition: typeHotkeyPosition, ...props } = this.props;
199
+ render(): preact.ComponentChildren {
200
+ return ButtonInline(this.props, this, true);
201
+ }
202
+ }
203
+
201
204
 
202
- delete props["class"];
205
+ qreact.inline(ButtonInline);
203
206
 
204
- let flavorOverrides: preact.JSX.CSSProperties = {};
207
+ export function ButtonInline(inputProps: ButtonProps, parentContext: Button | undefined, isNotInline?: boolean): preact.VNode {
208
+ let { square, flavor, children, className, style, hue, showHotkeys, hotkeys, hotkeyPosition: typeHotkeyPosition, ...props } = inputProps;
209
+ let context = isNotInline ? parentContext as Button : undefined;
210
+
211
+ if (hotkeys?.length && !isNotInline) {
212
+ debugger;
213
+ throw new Error(`Hotkeys are not supported for inline buttons. Tried to use ${JSON.stringify(hotkeys)}`);
214
+ }
215
+
216
+ delete props["class"];
217
+
218
+ let flavorOverrides: preact.JSX.CSSProperties = {};
219
+ if (square) {
220
+ flavorOverrides.padding = "1px";
221
+ }
222
+ if (flavor === "large") {
223
+ flavorOverrides = {
224
+ fontSize: 18,
225
+ padding: "10px 15px",
226
+ };
205
227
  if (square) {
206
- flavorOverrides.padding = "1px";
228
+ flavorOverrides.padding = "10px";
207
229
  }
208
- if (flavor === "large") {
209
- flavorOverrides = {
210
- fontSize: 18,
211
- padding: "10px 15px",
212
- };
213
- if (square) {
214
- flavorOverrides.padding = "10px";
215
- }
216
- }
217
- if (flavor === "small") {
218
- flavorOverrides = {
219
- fontSize: 12,
220
- padding: "5px 10px",
221
- };
222
- if (square) {
223
- flavorOverrides.padding = "5px";
224
- }
230
+ }
231
+ if (flavor === "small") {
232
+ flavorOverrides = {
233
+ fontSize: 12,
234
+ padding: "5px 10px",
235
+ };
236
+ if (square) {
237
+ flavorOverrides.padding = "5px";
225
238
  }
226
- if (flavor === "tiny") {
227
- flavorOverrides = {
228
- fontSize: 10,
229
- padding: "3px 6px",
230
- };
231
- if (square) {
232
- flavorOverrides.padding = "3px";
233
- }
239
+ }
240
+ if (flavor === "tiny") {
241
+ flavorOverrides = {
242
+ fontSize: 10,
243
+ padding: "3px 6px",
244
+ };
245
+ if (square) {
246
+ flavorOverrides.padding = "3px";
234
247
  }
248
+ }
235
249
 
236
- let colorStyle = (
237
- hue !== undefined &&
238
- css.background(`hsl(${hue}, 50%, 50%)`, "soft")
239
- .background(`hsl(${hue}, 50%, 40%)`, "active", "soft")
240
- .border(`1px solid hsl(${hue}, 0%, 50%)`, "soft")
241
- .outline(`3px solid hsl(204, 100%, 50%)`, "focus", "soft")
242
- .color("white")
243
- || css.background("hsl(0, 0%, 39%)", "soft")
244
- .background("hsl(0, 0%, 50%)", "hover", "soft")
245
- .background("hsl(0, 0%, 50%)", "active", "soft")
246
- .border("1px solid hsl(0, 0%, 50%)", "soft")
247
- .outline("3px solid hsl(204, 100%, 50%)", "focus", "soft")
248
- .color("white")
249
- );
250
+ let colorStyle = (
251
+ hue !== undefined &&
252
+ css.background(`hsl(${hue}, 50%, 50%)`, "soft")
253
+ .background(`hsl(${hue}, 50%, 40%)`, "active", "soft")
254
+ .border(`1px solid hsl(${hue}, 0%, 50%)`, "soft")
255
+ .outline(`3px solid hsl(204, 100%, 50%)`, "focus", "soft")
256
+ .color("white")
257
+ || css.background("hsl(0, 0%, 39%)", "soft")
258
+ .background("hsl(0, 0%, 50%)", "hover", "soft")
259
+ .background("hsl(0, 0%, 50%)", "active", "soft")
260
+ .border("1px solid hsl(0, 0%, 50%)", "soft")
261
+ .outline("3px solid hsl(204, 100%, 50%)", "focus", "soft")
262
+ .color("white")
263
+ );
250
264
 
251
265
 
252
- let type = this.props.typeOverride || this.props.flavor === "noui" && "div" || "button";
253
- return qreact.createElement(
254
- type,
255
- {
256
- ...props,
257
- className: (
258
- (className || this.props.class || "")
259
- + " trigger-hover"
260
- + css.zIndex(1, "hover").position("relative", "soft")
261
- + (
262
- flavor !== "noui" && (
263
- colorStyle.display("flex", "soft")
264
- .cursor("pointer")
265
- .padding("4px 6px" as "1px")
266
- .filter("brightness(1.1)", "hover", "soft")
267
- )
266
+ let type = inputProps.typeOverride || inputProps.flavor === "noui" && "div" || "button";
267
+ return qreact.createElement(
268
+ type,
269
+ {
270
+ ...props,
271
+ className: (
272
+ (className || inputProps.class || "")
273
+ + " trigger-hover"
274
+ + css.zIndex(1, "hover").position("relative", "soft")
275
+ + (
276
+ flavor !== "noui" && (
277
+ colorStyle.display("flex", "soft")
278
+ .cursor("pointer")
279
+ .padding("4px 6px" as "1px")
280
+ .filter("brightness(1.1)", "hover", "soft")
268
281
  )
282
+ )
283
+ ),
284
+ style: flavor === "noui" ? {} : {
285
+ alignItems: "center",
286
+ userSelect: inputProps.useLegacySelection ? "none" : undefined,
287
+ flexDirection: (
288
+ inputProps.showHotkeys === "vertical" && "column"
289
+ || inputProps.showHotkeys === "reverse" && "row-reverse"
290
+ || "row"
269
291
  ),
270
- style: flavor === "noui" ? {} : {
271
- alignItems: "center",
272
- userSelect: this.props.useLegacySelection ? "none" : undefined,
273
- flexDirection: (
274
- this.props.showHotkeys === "vertical" && "column"
275
- || this.props.showHotkeys === "reverse" && "row-reverse"
276
- || "row"
277
- ),
278
- ...flavorOverrides,
279
- ...style as any,
280
- },
281
- ref: (x: any) => this.element = x,
282
- onMouseDown: ((e: MouseEvent) => {
283
- // NOTE: THIS is the correct way to prevent selection. This prevent clicking
284
- // on a button from accidentally selecting text (EVEN other text), while
285
- // still allowing the user to select the button text if they want to.
286
- if (!this.props.useLegacySelection) {
287
- e.preventDefault();
288
- }
289
- return this.props.onMouseDown?.(e as any);
290
- })
292
+ ...flavorOverrides,
293
+ ...style as any,
291
294
  },
292
- <>
293
- {children}
294
- {
295
- <div class={
296
- ((this.props.hideHotkeys && " ")
297
- || showHotkeys && hotkeys?.length && css.marginLeft(10).vbox(6).pointerEvents("none")
298
- || (
299
- css.absolute.zIndex(1).pointerEvents("none")
295
+ ref: (x: any) => context && (context.element = x),
296
+ onMouseDown: ((e: MouseEvent) => {
297
+ // NOTE: THIS is the correct way to prevent selection. This prevent clicking
298
+ // on a button from accidentally selecting text (EVEN other text), while
299
+ // still allowing the user to select the button text if they want to.
300
+ if (!inputProps.useLegacySelection) {
301
+ e.preventDefault();
302
+ }
303
+ return inputProps.onMouseDown?.(e as any);
304
+ })
305
+ },
306
+ <>
307
+ {children}
308
+ {
309
+ <div class={
310
+ ((inputProps.hideHotkeys && " ")
311
+ || showHotkeys && hotkeys?.length && css.marginLeft(10).vbox(6).pointerEvents("none")
312
+ || (
313
+ css.absolute.zIndex(1).pointerEvents("none")
300
314
 
315
+ .whiteSpace("nowrap")
316
+ + " show-on-hover"
317
+ + (
318
+ typeHotkeyPosition === "left" && css.pos("0%", "50%").offset("-100%", "-50%")
319
+ || typeHotkeyPosition === "above" && css.pos("50%", "0%").offset("-50%", "-100%")
320
+ || css.pos("100%", "50%").offset("6px", "-50%")
321
+ )
322
+ )
323
+ ) + (inputProps.hotkeyClassname || "")
324
+ }>
325
+ {inputProps.hotkeys?.map((x, i) =>
326
+ <div class={
327
+ "" + (
328
+ css
329
+ .center
330
+ .height(showHotkeys ? 20 : 35)
331
+ .minWidth(showHotkeys ? 20 : 35)
332
+ .pad(0, 10)
333
+ .fontSize(14)
334
+ .color("hsl(0, 0%, 20%)")
335
+ .background("hsl(0, 0%, 90%)")
336
+ .background("hsl(0, 0%, 70%)", "hover")
337
+ .background("hsl(0, 0%, 60%)", "active")
338
+ .borderRadius(3)
339
+ .boxShadow("0px 2px 4px rgba(0, 0, 0, 0.5)")
340
+ .boxShadow("0px 2px 4px rgba(0, 0, 0, 0.9)", "active")
341
+ .transition("all 0.2s ease-in-out")
301
342
  .whiteSpace("nowrap")
302
- + " show-on-hover"
303
- + (
304
- typeHotkeyPosition === "left" && css.pos("0%", "50%").offset("-100%", "-50%")
305
- || typeHotkeyPosition === "above" && css.pos("50%", "0%").offset("-50%", "-100%")
306
- || css.pos("100%", "50%").offset("6px", "-50%")
307
- )
308
343
  )
309
- ) + (this.props.hotkeyClassname || "")
310
- }>
311
- {this.props.hotkeys?.map((x, i) =>
312
- <div class={
313
- "" + (
314
- css
315
- .center
316
- .height(showHotkeys ? 20 : 35)
317
- .minWidth(showHotkeys ? 20 : 35)
318
- .pad(0, 10)
319
- .fontSize(14)
320
- .color("hsl(0, 0%, 20%)")
321
- .background("hsl(0, 0%, 90%)")
322
- .background("hsl(0, 0%, 70%)", "hover")
323
- .background("hsl(0, 0%, 60%)", "active")
324
- .borderRadius(3)
325
- .boxShadow("0px 2px 4px rgba(0, 0, 0, 0.5)")
326
- .boxShadow("0px 2px 4px rgba(0, 0, 0, 0.9)", "active")
327
- .transition("all 0.2s ease-in-out")
328
- .whiteSpace("nowrap")
329
- )
330
344
 
331
- }
332
- >
333
- {formatHotkey(x)}
334
- </div>
335
- )}
336
- </div>
337
- }
338
- </>
345
+ }
346
+ >
347
+ {formatHotkey(x)}
348
+ </div>
349
+ )}
350
+ </div>
351
+ }
352
+ </>
339
353
 
340
- );
341
- }
354
+ );
342
355
  }
343
356
 
344
357
  function formatHotkey(key: string) {
@@ -69,6 +69,10 @@ export function performDrag2(
69
69
 
70
70
  const onMouseMove = (e: MouseEvent) => {
71
71
  if (finished) return;
72
+ if (e.buttons === 0) {
73
+ finish();
74
+ return;
75
+ }
72
76
  if (lastMouseX === e.clientX && lastMouseY === e.clientY) return;
73
77
  lastMouseX = e.clientX;
74
78
  lastMouseY = e.clientY;
@@ -82,6 +86,7 @@ export function performDrag2(
82
86
  dragCount--;
83
87
  window.removeEventListener("mousemove", onMouseMove);
84
88
  window.removeEventListener("mouseup", finish, { capture: true });
89
+ window.removeEventListener("blur", onBlur);
85
90
  document.removeEventListener("keydown", keyDown);
86
91
  // Call trigger, to reset the state back to the original state.
87
92
  triggerMove();
@@ -116,8 +121,14 @@ export function performDrag2(
116
121
  }
117
122
  };
118
123
 
124
+ const onBlur = () => {
125
+ if (finished) return;
126
+ cancel();
127
+ };
128
+
119
129
  window.addEventListener("mousemove", onMouseMove);
120
130
  window.addEventListener("mouseup", finish, { capture: true });
131
+ window.addEventListener("blur", onBlur);
121
132
  document.addEventListener("keydown", keyDown);
122
133
  selfCancelCallback.add(cancel);
123
134