querysub 0.166.0 → 0.167.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/-0-hooks/hooks.ts +13 -6
- package/src/-a-auth/ed25519.ts +1 -1
- package/src/-f-node-discovery/NodeDiscovery.ts +3 -3
- package/src/0-path-value-core/PathValueCommitter.ts +0 -2
- package/src/2-proxy/PathValueProxyWatcher.ts +22 -4
- package/src/2-proxy/schema2.ts +1 -3
- package/src/3-path-functions/PathFunctionHelpers.ts +6 -2
- package/src/3-path-functions/syncSchema.ts +6 -0
- package/src/4-dom/qreact.tsx +29 -13
- package/src/4-querysub/Querysub.ts +14 -1
- package/src/4-querysub/QuerysubController.ts +17 -4
- package/src/4-querysub/querysubPrediction.ts +2 -0
- package/src/library-components/Input.tsx +4 -16
- package/src/library-components/InputLabel.tsx +1 -1
- package/src/library-components/InputPicker.tsx +31 -23
- package/src/library-components/loadingIndicator.tsx +9 -1
- package/src/promise.ts +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "querysub",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.167.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",
|
|
@@ -24,9 +24,9 @@
|
|
|
24
24
|
"node-forge": "https://github.com/sliftist/forge#e618181b469b07bdc70b968b0391beb8ef5fecd6",
|
|
25
25
|
"pako": "^2.1.0",
|
|
26
26
|
"preact": "^10.11.3",
|
|
27
|
-
"socket-function": "^0.
|
|
27
|
+
"socket-function": "^0.105.0",
|
|
28
28
|
"terser": "^5.31.0",
|
|
29
|
-
"typesafecss": "^0.
|
|
29
|
+
"typesafecss": "^0.15.0",
|
|
30
30
|
"yaml": "^2.5.0",
|
|
31
31
|
"yargs": "^15.3.1"
|
|
32
32
|
},
|
package/src/-0-hooks/hooks.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { MaybePromise } from "socket-function/src/types";
|
|
2
|
+
import { CallInterceptor } from "../3-path-functions/PathFunctionHelpers";
|
|
1
3
|
import type { EdgeNodeConfig } from "../4-deploy/edgeNodes";
|
|
2
4
|
import type { ExtraMetadata } from "../5-diagnostics/nodeMetadata";
|
|
3
5
|
|
|
@@ -121,13 +123,18 @@ export const isManagementUser = createHookFunctionReturn<
|
|
|
121
123
|
|
|
122
124
|
|
|
123
125
|
|
|
124
|
-
declare global {
|
|
125
|
-
var BOOTED_EDGE_NODE: EdgeNodeConfig | undefined;
|
|
126
|
-
}
|
|
127
|
-
|
|
128
126
|
|
|
129
127
|
export function getBootedEdgeNode(): EdgeNodeConfig {
|
|
130
128
|
let edgeNode = globalThis.BOOTED_EDGE_NODE;
|
|
131
129
|
if (!edgeNode) throw new Error(`No edge node booted? This should be impossible.`);
|
|
132
|
-
return edgeNode;
|
|
133
|
-
}
|
|
130
|
+
return edgeNode as EdgeNodeConfig;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
export const interceptCalls = createHookFunctionReturn<
|
|
135
|
+
<T>(interceptor: CallInterceptor<T>) => T
|
|
136
|
+
>("interceptCalls");
|
|
137
|
+
|
|
138
|
+
export const onAllPredictionsFinished = createHookFunctionReturn<
|
|
139
|
+
() => MaybePromise<void>
|
|
140
|
+
>("onAllPredictionsFinished");
|
package/src/-a-auth/ed25519.ts
CHANGED
|
@@ -21,7 +21,7 @@ export async function signWithPEM(config: {
|
|
|
21
21
|
let signature = await signED25519(key, inputMessage);
|
|
22
22
|
return signature;
|
|
23
23
|
} else {
|
|
24
|
-
let key = await globalThis.crypto.subtle.importKey("spki", Buffer.from(pem), { name: "RSASSA-PKCS1-v1_5", hash: "SHA-256" }, false, ["sign"]);
|
|
24
|
+
let key = await globalThis.crypto.subtle.importKey("spki", Buffer.from(pem as any), { name: "RSASSA-PKCS1-v1_5", hash: "SHA-256" }, false, ["sign"]);
|
|
25
25
|
let signatureBuffer = await globalThis.crypto.subtle.sign("RSASSA-PKCS1-v1_5", key, inputMessage);
|
|
26
26
|
return Buffer.from(signatureBuffer);
|
|
27
27
|
}
|
|
@@ -523,9 +523,9 @@ const NodeDiscoveryController = SocketFunction.register(
|
|
|
523
523
|
getAllNodesHash: { hooks: [requiresNetworkTrustHook] },
|
|
524
524
|
// Skip client hooks, so we don't block on authentication (IdentityController), as some of these functions
|
|
525
525
|
// are needed for authentication to finish!
|
|
526
|
-
getAllNodeIds: { noClientHooks: true },
|
|
527
|
-
getNodeId: { noClientHooks: true },
|
|
528
|
-
isNoNetwork: { noClientHooks: true },
|
|
526
|
+
getAllNodeIds: { noClientHooks: true, noDefaultHooks: true },
|
|
527
|
+
getNodeId: { noClientHooks: true, noDefaultHooks: true },
|
|
528
|
+
isNoNetwork: { noClientHooks: true, noDefaultHooks: true },
|
|
529
529
|
}),
|
|
530
530
|
() => ({
|
|
531
531
|
|
|
@@ -371,8 +371,6 @@ class PathValueCommitter {
|
|
|
371
371
|
});
|
|
372
372
|
if (values.length === 0) continue;
|
|
373
373
|
|
|
374
|
-
debugbreak(2);
|
|
375
|
-
debugger;
|
|
376
374
|
console.log(self.getExistingWatchRemoteNodeId(childPath));
|
|
377
375
|
for (let value of values) {
|
|
378
376
|
specialInitialParentCausedRejections.push({
|
|
@@ -30,10 +30,10 @@ import type { FunctionMetadata } from "../3-path-functions/syncSchema";
|
|
|
30
30
|
|
|
31
31
|
import { DEPTH_TO_DATA, MODULE_INDEX, getCurrentCall, getCurrentCallObj } from "../3-path-functions/PathFunctionRunner";
|
|
32
32
|
import { inlineNestedCalls } from "../3-path-functions/syncSchema";
|
|
33
|
-
import {
|
|
33
|
+
import { interceptCallsBase, runCall } from "../3-path-functions/PathFunctionHelpers";
|
|
34
34
|
import { deepCloneCborx } from "../misc/cloneHelpers";
|
|
35
35
|
import { formatPercent } from "socket-function/src/formatting/format";
|
|
36
|
-
import { addStatPeriodic, onTimeProfile } from "../-0-hooks/hooks";
|
|
36
|
+
import { addStatPeriodic, interceptCalls, onAllPredictionsFinished, onTimeProfile } from "../-0-hooks/hooks";
|
|
37
37
|
|
|
38
38
|
// TODO: Break this into two parts:
|
|
39
39
|
// 1) Run and get accesses
|
|
@@ -1032,7 +1032,11 @@ export class PathValueProxyWatcher {
|
|
|
1032
1032
|
}
|
|
1033
1033
|
};
|
|
1034
1034
|
watcher.hasAnyUnsyncedAccesses = () => {
|
|
1035
|
-
return
|
|
1035
|
+
return (
|
|
1036
|
+
watcher.pendingUnsyncedAccesses.size > 0
|
|
1037
|
+
|| watcher.pendingUnsyncedParentAccesses.size > 0
|
|
1038
|
+
|| watcher.specialPromiseUnsynced
|
|
1039
|
+
);
|
|
1036
1040
|
};
|
|
1037
1041
|
|
|
1038
1042
|
function dispose() {
|
|
@@ -1110,6 +1114,20 @@ export class PathValueProxyWatcher {
|
|
|
1110
1114
|
// to do the permissions checks if they want them
|
|
1111
1115
|
throw new Error(`Nested synced function calls are not allowed. Call the function directly, or use Querysub.onCommitFinished to wait for the function to finish.`);
|
|
1112
1116
|
} else if (handling === "after" || handling === undefined) {
|
|
1117
|
+
|
|
1118
|
+
// We need to wait for predictions to finish, otherwise we run into situations
|
|
1119
|
+
// where we call a function which should change a parameter we want to pass
|
|
1120
|
+
// to another function, but because the first call didn't predict, the second
|
|
1121
|
+
// call gets a different values, causing all kinds of issues.
|
|
1122
|
+
if (watcher.pendingCalls.length === 0) {
|
|
1123
|
+
let waitPromise = onAllPredictionsFinished();
|
|
1124
|
+
if (waitPromise) {
|
|
1125
|
+
proxyWatcher.triggerOnPromiseFinish(waitPromise, {
|
|
1126
|
+
waitReason: "Waiting for predictions to finish",
|
|
1127
|
+
});
|
|
1128
|
+
}
|
|
1129
|
+
}
|
|
1130
|
+
|
|
1113
1131
|
watcher.pendingCalls.push({ call, metadata });
|
|
1114
1132
|
} else if (handling === "ignore") {
|
|
1115
1133
|
} else {
|
|
@@ -1122,7 +1140,7 @@ export class PathValueProxyWatcher {
|
|
|
1122
1140
|
runCodeWithDatabase(proxy, baseFunction)
|
|
1123
1141
|
);
|
|
1124
1142
|
},
|
|
1125
|
-
});
|
|
1143
|
+
}) as Result;
|
|
1126
1144
|
}
|
|
1127
1145
|
|
|
1128
1146
|
// We use atomic object read, as our callers don't want a proxy.
|
package/src/2-proxy/schema2.ts
CHANGED
|
@@ -75,9 +75,7 @@ type TypeDefType<T = never, Optional = false> = {
|
|
|
75
75
|
/** An object (so not atomic), but optional (so you can delete it), without automatic GCing of deleted objects. */
|
|
76
76
|
optionalObjectNoGC<ObjectT>(object: ObjectT): TypeDefType<TypeDefToT<ObjectT>, true>;
|
|
77
77
|
|
|
78
|
-
/**
|
|
79
|
-
* However there are some cases when this is useful.
|
|
80
|
-
* - An object (so not atomic)
|
|
78
|
+
/** @deprecated Not really needed, just use an object directly ex, { a: { value: t.string } }. UNLESS this object is optional, then use "optionalObject"
|
|
81
79
|
*/
|
|
82
80
|
object<ObjectT>(object: ObjectT): TypeDefType<TypeDefToT<ObjectT>, true>;
|
|
83
81
|
|
|
@@ -15,6 +15,7 @@ import { getPathStr2 } from "../path";
|
|
|
15
15
|
import { isNode, sort } from "socket-function/src/misc";
|
|
16
16
|
import { decodeCborx, encodeCborx } from "../misc/cloneHelpers";
|
|
17
17
|
import { parseFilterable } from "../misc/filterable";
|
|
18
|
+
import { interceptCalls } from "../-0-hooks/hooks";
|
|
18
19
|
|
|
19
20
|
// NOTE: We could deploy single functions, but... we will almost always be updating all functions at
|
|
20
21
|
// once, because keeping everything on the same git hash reduces a lot of potential bugs.
|
|
@@ -101,13 +102,14 @@ export async function runCall(call: CallSpec, metadata: FunctionMetadata) {
|
|
|
101
102
|
return await writeCall.value(call, metadata);
|
|
102
103
|
}
|
|
103
104
|
|
|
104
|
-
|
|
105
|
+
|
|
106
|
+
export interface CallInterceptor<T> {
|
|
105
107
|
onCall: (call: CallSpec, metadata: FunctionMetadata) => void;
|
|
106
108
|
code: () => T;
|
|
107
109
|
}
|
|
108
110
|
|
|
109
111
|
let curInterceptor: CallInterceptor<unknown> | undefined = undefined;
|
|
110
|
-
export function
|
|
112
|
+
export function interceptCallsBase<T>(
|
|
111
113
|
interceptor: CallInterceptor<T>
|
|
112
114
|
) {
|
|
113
115
|
let prev = curInterceptor;
|
|
@@ -118,6 +120,8 @@ export function interceptCalls<T>(
|
|
|
118
120
|
curInterceptor = prev;
|
|
119
121
|
}
|
|
120
122
|
}
|
|
123
|
+
interceptCalls.declare(interceptCallsBase);
|
|
124
|
+
|
|
121
125
|
|
|
122
126
|
export function writeFunctionCall(config: {
|
|
123
127
|
domainName: string;
|
|
@@ -137,6 +137,12 @@ export type PermissionsParameters = {
|
|
|
137
137
|
```
|
|
138
138
|
* */
|
|
139
139
|
callerMachineId: string;
|
|
140
|
+
|
|
141
|
+
// IMPORTANT! DO NOT add "matchedValue" here. It is convenient, BUT, it is better for the user
|
|
142
|
+
// to access it themselves using pathWildcards. This makes it more typesafe, allowing
|
|
143
|
+
// them to find all references and find that reference (otherwise the reference will be hidden).
|
|
144
|
+
// - Also, I don't think we could make matchedValue typesafe at all, so it would have to be any,
|
|
145
|
+
// which is terrible.
|
|
140
146
|
};
|
|
141
147
|
/** A false of false will deny read permissions, resulting in all reads being given value with a value
|
|
142
148
|
* of undefined, and a time of 0.
|
package/src/4-dom/qreact.tsx
CHANGED
|
@@ -11,7 +11,7 @@ import { measureBlock, measureCode, measureFnc } from "socket-function/src/profi
|
|
|
11
11
|
import { canHaveChildren } from "socket-function/src/types";
|
|
12
12
|
import { errorify, logErrors } from "../errors";
|
|
13
13
|
import { cache, lazy } from "socket-function/src/caching";
|
|
14
|
-
import { getPathStr1, getPathIndexAssert } from "../path";
|
|
14
|
+
import { getPathStr1, getPathIndexAssert, getPathStr2 } from "../path";
|
|
15
15
|
import { blue, green, red, yellow } from "socket-function/src/formatting/logColors";
|
|
16
16
|
import { heapTagObj } from "../diagnostics/heapTag";
|
|
17
17
|
import { onHotReload } from "socket-function/hot/HotReloadController";
|
|
@@ -1342,19 +1342,24 @@ class QRenderClass {
|
|
|
1342
1342
|
if (Array.isArray(node)) return 0;
|
|
1343
1343
|
if (isVNode(node)) {
|
|
1344
1344
|
if (node.key) {
|
|
1345
|
-
|
|
1345
|
+
// The type HAS to match as WELL as the type. Otherwise the key can force
|
|
1346
|
+
// spans to match divs, or... differently component types to match, which is bad.
|
|
1347
|
+
return getPathStr2(String(node.key), getSubKey(node));
|
|
1346
1348
|
}
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1349
|
+
return getSubKey(node);
|
|
1350
|
+
function getSubKey(node: VirtualDOMElement) {
|
|
1351
|
+
// So... if you change the value of a select option, it changes the parent select.value. Which is...
|
|
1352
|
+
// odd. We can easily fix this by just using the value as the key, which prevents us from ever
|
|
1353
|
+
// changing the .value
|
|
1354
|
+
if (node.type === "option") {
|
|
1355
|
+
return String(node.props.value);
|
|
1356
|
+
}
|
|
1357
|
+
let type = node.type;
|
|
1358
|
+
if (typeof type === "function") {
|
|
1359
|
+
return QRenderClass.componentIdDedupe(type);
|
|
1360
|
+
}
|
|
1361
|
+
return type;
|
|
1356
1362
|
}
|
|
1357
|
-
return type;
|
|
1358
1363
|
}
|
|
1359
1364
|
return typeof node;
|
|
1360
1365
|
}
|
|
@@ -1749,7 +1754,7 @@ function updateDOMNodeFields(domNode: DOMNode, vNode: VirtualDOM, prevVNode: Vir
|
|
|
1749
1754
|
ready: false,
|
|
1750
1755
|
wrappedCallback: wrapEventCallback(`${owner.debugName}.eventHandlers.${key}`, name,
|
|
1751
1756
|
function (this: any, ...args: any[]) {
|
|
1752
|
-
if (!eventNode._eventHandlers?.[name].ready
|
|
1757
|
+
if (name === "click" && !eventNode._eventHandlers?.[name].ready) {
|
|
1753
1758
|
// UPDATE: ONLY for clicks, otherwise we break blur handlers.
|
|
1754
1759
|
// IMPORTANT NOTE: IF you are inside of a click handler and add a handler to
|
|
1755
1760
|
// the parent, the parent's handler will be triggered. An event might
|
|
@@ -1763,6 +1768,17 @@ function updateDOMNodeFields(domNode: DOMNode, vNode: VirtualDOM, prevVNode: Vir
|
|
|
1763
1768
|
// before we start
|
|
1764
1769
|
return;
|
|
1765
1770
|
}
|
|
1771
|
+
// Ignore blurs for disconnected elements. This frequently happens when state changes
|
|
1772
|
+
// inside of a keydown (ex, enter+shift) trigger a complete re-render, which unmounts
|
|
1773
|
+
// an input. Because we already triggered a state change, we usually don't want to also
|
|
1774
|
+
// blur (which will trigger change handlers for inputs).
|
|
1775
|
+
if (name === "blur") {
|
|
1776
|
+
let target = args[0].currentTarget as HTMLElement;
|
|
1777
|
+
if (!target.getAttribute("data-blur-on-unmount") && !target.isConnected) {
|
|
1778
|
+
console.info("Ignoring blur for disconnected element. You can use data-blur-on-unmount to re-enable blurs on this element.", target);
|
|
1779
|
+
return;
|
|
1780
|
+
}
|
|
1781
|
+
}
|
|
1766
1782
|
let prevEvent = QRenderClass.eventComponentId;
|
|
1767
1783
|
QRenderClass.eventComponentId = owner.id;
|
|
1768
1784
|
try {
|
|
@@ -21,7 +21,7 @@ import { getCurrentCallAllowUndefined, getCurrentCall, CallSpec, PathFunctionRun
|
|
|
21
21
|
import { listenOnDebugger } from "../diagnostics/listenOnDebugger";
|
|
22
22
|
import { logErrors } from "../errors";
|
|
23
23
|
import { getLastPathPart, getPathIndexAssert, getPathStr2, hack_setPackedPathSuffix } from "../path";
|
|
24
|
-
import { QuerysubController, flushDelayedFunctions, onCallPredict, waitUntilAllPredictionsFinish } from "./QuerysubController";
|
|
24
|
+
import { QuerysubController, anyPredictionsPending, flushDelayedFunctions, onCallPredict, waitUntilAllPredictionsFinish } from "./QuerysubController";
|
|
25
25
|
import { PermissionsCheck } from "./permissions";
|
|
26
26
|
import { inlineNestedCalls, syncSchema } from "../3-path-functions/syncSchema";
|
|
27
27
|
import type { identityStorageKey, IdentityStorageType } from "../-a-auth/certs";
|
|
@@ -344,6 +344,11 @@ export class Querysub {
|
|
|
344
344
|
public static onCommitFinished(callback: () => void) {
|
|
345
345
|
proxyWatcher.getTriggeredWatcher().onInnerDisposed.push(callback);
|
|
346
346
|
}
|
|
347
|
+
public static onCommitFinishedCommit(callback: () => void) {
|
|
348
|
+
this.onCommitFinished(() => {
|
|
349
|
+
Querysub.commit(callback);
|
|
350
|
+
});
|
|
351
|
+
}
|
|
347
352
|
|
|
348
353
|
/** A more powerful version of omCommitFinished, which even waits for call predictions (or tries to).
|
|
349
354
|
* - Also see afterPredictionsSynced, which runs the callback in a write.
|
|
@@ -385,6 +390,14 @@ export class Querysub {
|
|
|
385
390
|
* (such as if code is still loading).
|
|
386
391
|
*/
|
|
387
392
|
public static waitUntilAllPredictionsFinished = () => waitUntilAllPredictionsFinish();
|
|
393
|
+
/** Useful to prevent in-progress state from showing temporarily. */
|
|
394
|
+
public static waitForPredictionsSynced = () => {
|
|
395
|
+
if (anyPredictionsPending()) {
|
|
396
|
+
proxyWatcher.triggerOnPromiseFinish(Querysub.waitUntilAllPredictionsFinished(), {
|
|
397
|
+
waitReason: "Waiting for predictions to finish",
|
|
398
|
+
});
|
|
399
|
+
}
|
|
400
|
+
};
|
|
388
401
|
public static onCommitPredictFinished = this.onCallPredict;
|
|
389
402
|
|
|
390
403
|
public static getOwnMachineId = getOwnMachineId;
|
|
@@ -42,7 +42,7 @@ setFlag(require, "preact", "allowclient", true);
|
|
|
42
42
|
|
|
43
43
|
import yargs from "yargs";
|
|
44
44
|
import { mergeFilterables, parseFilterable, serializeFilterable } from "../misc/filterable";
|
|
45
|
-
import { isManagementUser } from "../-0-hooks/hooks";
|
|
45
|
+
import { isManagementUser, onAllPredictionsFinished } from "../-0-hooks/hooks";
|
|
46
46
|
import { isLocal } from "../config";
|
|
47
47
|
|
|
48
48
|
let yargObj = isNodeTrue() && yargs(process.argv)
|
|
@@ -157,6 +157,8 @@ let pendingPredictedCalls = new Map<string, {
|
|
|
157
157
|
obj: PromiseObj;
|
|
158
158
|
seqNum: number;
|
|
159
159
|
}>();
|
|
160
|
+
export const debug_pendingPredictedCalls = pendingPredictedCalls;
|
|
161
|
+
|
|
160
162
|
export function callWaitOn(callId: string, promise: Promise<unknown>) {
|
|
161
163
|
let waitObj = pendingPredictedCalls.get(callId);
|
|
162
164
|
if (!waitObj) return;
|
|
@@ -205,13 +207,24 @@ export async function onCallPredict(call: CallSpec | undefined) {
|
|
|
205
207
|
if (!call) return;
|
|
206
208
|
await pendingPredictedCalls.get(call.CallId)?.obj.promise;
|
|
207
209
|
}
|
|
210
|
+
export function anyPredictionsPending() {
|
|
211
|
+
return Array.from(pendingPredictedCalls.values()).some(obj => !obj.obj.resolved && !obj.obj.rejected);
|
|
212
|
+
}
|
|
213
|
+
export const debug_anyPredictionsPending = anyPredictionsPending;
|
|
208
214
|
export async function waitUntilAllPredictionsFinish() {
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
215
|
+
while (anyPredictionsPending()) {
|
|
216
|
+
try {
|
|
217
|
+
await Promise.allSettled(Array.from(pendingPredictedCalls.values()).map(obj => obj.obj.promise));
|
|
218
|
+
} catch {
|
|
219
|
+
}
|
|
212
220
|
}
|
|
213
221
|
}
|
|
214
222
|
|
|
223
|
+
onAllPredictionsFinished.declare(() => {
|
|
224
|
+
if (!anyPredictionsPending()) return undefined;
|
|
225
|
+
return waitUntilAllPredictionsFinish();
|
|
226
|
+
});
|
|
227
|
+
|
|
215
228
|
|
|
216
229
|
export async function flushDelayedFunctions() {
|
|
217
230
|
await prediction.flushDelayedFunctions();
|
|
@@ -21,6 +21,7 @@ import { FunctionMetadata } from "../3-path-functions/syncSchema";
|
|
|
21
21
|
import { isNode, nextId, sort } from "socket-function/src/misc";
|
|
22
22
|
import { getBrowserUrlNode } from "../-f-node-discovery/NodeDiscovery";
|
|
23
23
|
import { isLocal } from "../config";
|
|
24
|
+
import { onAllPredictionsFinished } from "../-0-hooks/hooks";
|
|
24
25
|
setFlag(require, "cbor-x", "allowclient", true);
|
|
25
26
|
const cborEncoder = lazy(() => new cbor.Encoder({ structuredClone: true }));
|
|
26
27
|
|
|
@@ -98,6 +99,7 @@ export function flushDelayedFunctions() {
|
|
|
98
99
|
}
|
|
99
100
|
}
|
|
100
101
|
|
|
102
|
+
|
|
101
103
|
export const addCall = runInSerial(async function addCall(call: CallSpec, metadata: FunctionMetadata) {
|
|
102
104
|
const nodeId = await querysubNodeId();
|
|
103
105
|
if (!nodeId) throw new Error("No querysub node found");
|
|
@@ -23,6 +23,8 @@ export type InputProps = (
|
|
|
23
23
|
inputRef?: (x: HTMLInputElement | null) => void;
|
|
24
24
|
/** Don't blur on enter key */
|
|
25
25
|
noEnterKeyBlur?: boolean;
|
|
26
|
+
/** Don't commit on shift+enter key */
|
|
27
|
+
noEnterKeyCommit?: boolean;
|
|
26
28
|
noFocusSelect?: boolean;
|
|
27
29
|
inputKey?: string;
|
|
28
30
|
fillWidth?: boolean;
|
|
@@ -212,7 +214,7 @@ export class Input extends qreact.Component<InputProps> {
|
|
|
212
214
|
callback?.(e as unknown as preact.JSX.TargetedInputEvent<HTMLInputElement>);
|
|
213
215
|
}
|
|
214
216
|
}
|
|
215
|
-
let { noEnterKeyBlur, onInput, onChange } = props;
|
|
217
|
+
let { noEnterKeyBlur, noEnterKeyCommit, onInput, onChange } = props;
|
|
216
218
|
// Detach from the synced function, to prevent double calls. This is important, as apparently .blur()
|
|
217
219
|
// synchronously triggers onChange, BUT, only if the input is changing the first time. Which means
|
|
218
220
|
// if this function reruns, it won't trigger the change again. Detaching it causes any triggered
|
|
@@ -236,21 +238,7 @@ export class Input extends qreact.Component<InputProps> {
|
|
|
236
238
|
}
|
|
237
239
|
if (!noEnterKeyBlur && e.code === "Enter" && (!textarea || e.shiftKey || e.ctrlKey)) {
|
|
238
240
|
e.currentTarget.blur();
|
|
239
|
-
} else if (
|
|
240
|
-
e.ctrlKey && (
|
|
241
|
-
// No need to commit on on paste or cut. If they want to commit, they can commit like they usually
|
|
242
|
-
// do. This shortcut only marginally affects a few workflows, and has the potential to break many others,
|
|
243
|
-
// so... let's not.
|
|
244
|
-
// // Pasting and cutting might not mean commit, but... they probably mean for it to...
|
|
245
|
-
// e.code === "KeyV" ||
|
|
246
|
-
// e.code === "KeyX" ||
|
|
247
|
-
|
|
248
|
-
// Ctrl+enter means "commit"
|
|
249
|
-
e.code === "Enter"
|
|
250
|
-
)
|
|
251
|
-
// Shift+enter means "commit"
|
|
252
|
-
|| e.shiftKey && e.code === "Enter"
|
|
253
|
-
) {
|
|
241
|
+
} else if (!noEnterKeyCommit && e.code === "Enter" && (e.ctrlKey || e.shiftKey)) {
|
|
254
242
|
Querysub.serviceWriteDetached(() => {
|
|
255
243
|
onChange?.(e);
|
|
256
244
|
});
|
|
@@ -137,7 +137,7 @@ export class InputLabel extends qreact.Component<InputLabelProps> {
|
|
|
137
137
|
style={style}
|
|
138
138
|
/>;
|
|
139
139
|
if (props.edit && !this.state.editting) {
|
|
140
|
-
input = <span class={css.hbox(2) + " trigger-hover"}>
|
|
140
|
+
input = <span class={css.hbox(2).overflowHidden + " trigger-hover"}>
|
|
141
141
|
<span class={props.editClass}>
|
|
142
142
|
{props.editValue ?? props.value}
|
|
143
143
|
</span>
|
|
@@ -27,6 +27,7 @@ export class InputPicker<T> extends qreact.Component<{
|
|
|
27
27
|
addPicked: (value: T) => void;
|
|
28
28
|
removePicked: (value: T) => void;
|
|
29
29
|
allowNonOptions?: boolean;
|
|
30
|
+
paletteOptions?: boolean;
|
|
30
31
|
}> {
|
|
31
32
|
state = {
|
|
32
33
|
pendingText: "",
|
|
@@ -61,31 +62,38 @@ export class InputPicker<T> extends qreact.Component<{
|
|
|
61
62
|
pendingMatches = pendingMatches.slice(0, 10);
|
|
62
63
|
extra -= pendingMatches.length;
|
|
63
64
|
return (
|
|
64
|
-
<div class={css.hbox(10).alignItems("start")}>
|
|
65
|
-
{
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
this.state.
|
|
74
|
-
this.state.
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
65
|
+
<div class={css.hbox(10).alignItems("start").relative}>
|
|
66
|
+
<div class={css.hbox(10).pad2(0, 3)}>
|
|
67
|
+
<div className={css.flexShrink0}>
|
|
68
|
+
{this.props.label}
|
|
69
|
+
</div>
|
|
70
|
+
<Input
|
|
71
|
+
value={this.state.pendingText}
|
|
72
|
+
hot
|
|
73
|
+
alwaysUseLatestValueWhenFocused
|
|
74
|
+
onChangeValue={(x) => this.state.pendingText = x}
|
|
75
|
+
onFocus={() => this.state.focused = true}
|
|
76
|
+
onBlur={() => {
|
|
77
|
+
this.state.focused = false;
|
|
78
|
+
this.state.pendingText = "";
|
|
79
|
+
}}
|
|
80
|
+
onKeyDown={e => {
|
|
81
|
+
// On enter, add first in pendingMatches
|
|
82
|
+
if (e.key === "Enter") {
|
|
83
|
+
e.preventDefault();
|
|
84
|
+
if (pendingMatches.length > 0) {
|
|
85
|
+
this.props.addPicked(pendingMatches[0].value);
|
|
86
|
+
this.state.pendingText = "";
|
|
87
|
+
}
|
|
83
88
|
}
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
|
|
89
|
+
}}
|
|
90
|
+
/>
|
|
91
|
+
</div>
|
|
87
92
|
{pendingMatches.length > 0 && (
|
|
88
|
-
<div class={
|
|
93
|
+
<div class={
|
|
94
|
+
css.hbox(4).wrap
|
|
95
|
+
+ (this.props.paletteOptions && css.absolute.pos(0, "100%").zIndex(1).hsla(0, 0, 0, 0.5).pad2(4))
|
|
96
|
+
}>
|
|
89
97
|
{pendingMatches.map((option) => (
|
|
90
98
|
<Button
|
|
91
99
|
key={`add-${option.matchText}`}
|
|
@@ -5,14 +5,22 @@ import { formatNumber, formatTime } from "socket-function/src/formatting/format"
|
|
|
5
5
|
|
|
6
6
|
export class UnsyncedIndicator extends qreact.Component {
|
|
7
7
|
lastSynced = Date.now();
|
|
8
|
+
wasSyncedLast = false;
|
|
8
9
|
render() {
|
|
9
10
|
let unsynced = Querysub.watchUnsyncedComponents();
|
|
10
11
|
let unsyncedCount = unsynced.size;
|
|
11
12
|
if (unsyncedCount === 0) {
|
|
12
13
|
this.lastSynced = Date.now();
|
|
14
|
+
this.wasSyncedLast = true;
|
|
13
15
|
return undefined;
|
|
14
16
|
}
|
|
15
|
-
|
|
17
|
+
// If we were synced in the last render, then we actually were just synced, so...
|
|
18
|
+
// set synced to now.
|
|
19
|
+
if (this.wasSyncedLast) {
|
|
20
|
+
this.wasSyncedLast = false;
|
|
21
|
+
this.lastSynced = Date.now();
|
|
22
|
+
}
|
|
23
|
+
let timeSinceLastSync = Querysub.timeDelayed(2000) - this.lastSynced;
|
|
16
24
|
if (timeSinceLastSync < 1500) {
|
|
17
25
|
return undefined;
|
|
18
26
|
}
|
package/src/promise.ts
CHANGED
|
@@ -9,7 +9,7 @@ export class PromiseObj<T = void> {
|
|
|
9
9
|
this.resolve = resolve;
|
|
10
10
|
this.reject = reject;
|
|
11
11
|
});
|
|
12
|
-
this.promise.finally(() => this.resolved = true);
|
|
12
|
+
void this.promise.finally(() => this.resolved = true);
|
|
13
13
|
this.promise.catch(() => this.rejected = true);
|
|
14
14
|
}
|
|
15
15
|
}
|