querysub 0.352.0 → 0.354.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 +3 -3
- package/src/-a-archives/archiveCache.ts +7 -5
- package/src/-a-archives/archives.ts +6 -1
- package/src/-a-archives/archivesBackBlaze.ts +4 -0
- package/src/0-path-value-core/pathValueArchives.ts +3 -2
- package/src/0-path-value-core/pathValueCore.ts +4 -3
- package/src/1-path-client/RemoteWatcher.ts +1 -5
- package/src/1-path-client/pathValueClientWatcher.ts +88 -65
- package/src/2-proxy/PathValueProxyWatcher.ts +69 -24
- package/src/3-path-functions/PathFunctionRunner.ts +10 -2
- package/src/4-dom/qreact.tsx +39 -15
- package/src/4-querysub/Querysub.ts +8 -6
- package/src/5-diagnostics/Modal.tsx +1 -1
- package/src/diagnostics/logs/injectFileLocationToConsole.ts +1 -1
- package/src/errors.ts +1 -0
- package/src/functional/throttleRender.ts +8 -1
- package/src/library-components/Button.tsx +141 -128
- package/src/library-components/drag.ts +11 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "querysub",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.354.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.
|
|
53
|
+
"socket-function": "^1.0.4",
|
|
54
54
|
"terser": "^5.31.0",
|
|
55
|
-
"typesafecss": "^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
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
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?: {
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
540
|
-
//
|
|
541
|
-
|
|
542
|
-
// NOTE:
|
|
543
|
-
//
|
|
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
|
-
|
|
550
|
-
|
|
551
|
-
|
|
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
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
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
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
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
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
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 (
|
|
619
|
+
if (watchSpec.paths.size === 0 && watchSpec.parentPaths.size === 0) {
|
|
600
620
|
return;
|
|
601
621
|
}
|
|
602
622
|
|
|
603
|
-
measureBlock(() => {
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
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
|
-
|
|
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
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
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
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
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
|
|
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 | remember, components have 3 watchers`);
|
|
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) {
|
package/src/4-dom/qreact.tsx
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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
|
|
2762
|
+
let pendingGlobalAfterRenderWatches: QRenderClass[] = [];
|
|
2739
2763
|
|
|
2740
|
-
let
|
|
2741
|
-
function
|
|
2742
|
-
|
|
2764
|
+
let globalAfterRenderWatches: ((component: QRenderClass[]) => void)[] = [];
|
|
2765
|
+
function globalAfterRenderWatch(callback: (component: ExternalRenderClass[]) => void) {
|
|
2766
|
+
globalAfterRenderWatches.push(callback);
|
|
2743
2767
|
return () => {
|
|
2744
|
-
let index =
|
|
2768
|
+
let index = globalAfterRenderWatches.indexOf(callback);
|
|
2745
2769
|
if (index !== -1) {
|
|
2746
|
-
|
|
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 =
|
|
2754
|
-
|
|
2755
|
-
for (let callback of
|
|
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
|
|
2785
|
+
console.error(`Error in globalAfterRenderWatch callback: ${error}`, error);
|
|
2762
2786
|
}
|
|
2763
2787
|
}
|
|
2764
2788
|
});
|
|
2765
|
-
function
|
|
2789
|
+
function triggerGlobalAfterRenderWatch(component: QRenderClass) {
|
|
2766
2790
|
qreact.domUpdateCount++;
|
|
2767
|
-
|
|
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
|
|
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({
|
|
@@ -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
|
-
|
|
199
|
-
|
|
200
|
-
|
|
199
|
+
render(): preact.ComponentChildren {
|
|
200
|
+
return ButtonInline(this.props, this, true);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
201
204
|
|
|
202
|
-
|
|
205
|
+
qreact.inline(ButtonInline);
|
|
203
206
|
|
|
204
|
-
|
|
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 = "
|
|
228
|
+
flavorOverrides.padding = "10px";
|
|
207
229
|
}
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
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
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
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
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
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
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
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
|
-
|
|
271
|
-
|
|
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
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
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
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
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
|
|