querysub 0.340.0 → 0.341.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.
@@ -531,6 +531,7 @@ class QRenderClass {
531
531
  void Promise.resolve().finally(() => {
532
532
  logErrors(proxyWatcher.commitFunction({
533
533
  canWrite: true,
534
+ baseFunction: callback,
534
535
  watchFunction() {
535
536
  QRenderClass.renderingComponentId = self.id;
536
537
  try {
@@ -555,7 +556,13 @@ class QRenderClass {
555
556
 
556
557
  let statics = vNode.type as QComponentStatic;
557
558
  // NOTE: This used to exist in an `atomicWrites: true` runOnce, but... I don't think that is required anymore
558
- let instance = new vNode.type(config.props) as preact.Component<{ [key: string]: unknown }>;
559
+ let instance: preact.Component<{ [key: string]: unknown }>;
560
+ QRenderClass.renderingComponentId = this.id;
561
+ try {
562
+ instance = new vNode.type(config.props) as preact.Component<{ [key: string]: unknown }>;
563
+ } finally {
564
+ QRenderClass.renderingComponentId = undefined;
565
+ }
559
566
  (instance as any)[idSymbol] = this.id;
560
567
  if (isValueProxy(instance)) {
561
568
  // This will probably result in an error later on. It is usually due to us failing to identify fragments.
@@ -616,6 +623,8 @@ class QRenderClass {
616
623
 
617
624
  self.instance.componentWillMount && logErrors(proxyWatcher.commitFunction({
618
625
  debugName: getDebugName("componentWillMount"),
626
+ // eslint-disable-next-line @typescript-eslint/unbound-method
627
+ baseFunction: self.instance.componentWillMount,
619
628
  watchFunction() {
620
629
  contextCommit(() => {
621
630
  self.instance.componentWillMount?.();
@@ -1175,6 +1184,7 @@ class QRenderClass {
1175
1184
  void Promise.resolve().finally(() => {
1176
1185
  logErrors(proxyWatcher.commitFunction({
1177
1186
  canWrite: true,
1187
+ baseFunction: ref,
1178
1188
  watchFunction() {
1179
1189
  ref(node);
1180
1190
  },
@@ -1219,6 +1229,7 @@ class QRenderClass {
1219
1229
  void Promise.resolve().finally(() => {
1220
1230
  logErrors(proxyWatcher.commitFunction({
1221
1231
  canWrite: true,
1232
+ baseFunction: ref2,
1222
1233
  watchFunction() {
1223
1234
  ref2(node);
1224
1235
  },
@@ -190,6 +190,7 @@ export class Querysub {
190
190
  * - NOTE: If this is too short, predictions stop before the server can finish. If it's too long,
191
191
  * server issues (such as the FunctionRunner being missing) won't be noticed. So...
192
192
  * we SHOULD increase this when our setup is more stable (60 seconds is probably good).
193
+ * SHOULD be > DELAY_COMMIT_DELAY, Otherwise delayed commits are going to cause predictions to be rejected before we even submit the actual call.
193
194
  */
194
195
  public static PREDICTION_MAX_LIFESPAN = 1000 * 15;
195
196
 
@@ -292,7 +293,7 @@ export class Querysub {
292
293
  explicitlyTrigger: () => void;
293
294
  } & SyncWatcher {
294
295
  return proxyWatcher.createWatcher({
295
- debugName: watcher.name,
296
+ baseFunction: watcher,
296
297
  canWrite: true,
297
298
  ...options,
298
299
  watchFunction: () => {
@@ -336,15 +337,18 @@ export class Querysub {
336
337
  process.exit();
337
338
  }
338
339
  };
339
-
340
340
  public static syncedCommit = Querysub.serviceWrite;
341
341
  public static commitSynced = Querysub.serviceWrite;
342
342
  public static commitAsync = Querysub.serviceWrite;
343
+ /** 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 }
344
+ * - ONLY DEFAULTED WHEN isClient()
345
+ */
343
346
  public static async serviceWrite<T>(fnc: () => T, options?: Partial<WatcherOptions<T>>) {
344
347
  let calls: CallSpec[] = [];
345
348
  let result = await proxyWatcher.commitFunction({
346
349
  canWrite: true,
347
350
  watchFunction: fnc,
351
+ finishInStartOrder: isClient(),
348
352
  onCallCommit(call, metadata) {
349
353
  calls.push(call);
350
354
  },
@@ -403,6 +407,8 @@ export class Querysub {
403
407
  public static isAllSynced = Querysub.allSynced;
404
408
  public static isAnyUnsynced = Querysub.anyUnsynced;
405
409
 
410
+ public static isInWatcher = () => !!proxyWatcher.getTriggeredWatcherMaybeUndefined();
411
+
406
412
  public static onNextDisconnect = SocketFunction.onNextDisconnect;
407
413
  public static isNodeConnected = SocketFunction.isNodeConnected;
408
414
 
@@ -499,6 +505,20 @@ export class Querysub {
499
505
  });
500
506
  }
501
507
 
508
+ /** Returns true if any predictions are running. In which case, we will correctly rerun the function and consider it non-synced until the predictions finish. This allows you to check for predictions of the in your function and return if you have any. This is useful for functions that need a stable state before they run. However, if many functions use this and you trigger them at once, it might result in an n-squared situation, so this should be used with caution.
509
+ * - The best use case is if something is triggering maybe on focus and on blur, and you want to make sure the on blur changes happen before your on focus changes happen.
510
+ */
511
+ public static syncAnyPredictionsPending(): boolean {
512
+ let waitPromise = onAllPredictionsFinished();
513
+ if (waitPromise) {
514
+ proxyWatcher.triggerOnPromiseFinish(waitPromise, {
515
+ waitReason: "Waiting for predictions to finish",
516
+ });
517
+ return true;
518
+ }
519
+ return false;
520
+ }
521
+
502
522
  public static onCallPredict = (x: CallSpec | undefined) => onCallPredict(x);
503
523
  /** NOTE: USUALLY you don't have to wait for predictions, as functions will run in order anyways.
504
524
  * BUT, if you run a synced function in the UI, then somewhere unrelated try to read values
@@ -1127,11 +1147,15 @@ let initInterval = cache((interval: number) => {
1127
1147
  });
1128
1148
 
1129
1149
  function getSetTime() {
1150
+ let watcher = proxyWatcher.getTriggeredWatcherMaybeUndefined();
1151
+ if (watcher?.options.predictMetadata?.delayCommit) {
1152
+ debugger;
1153
+ console.warn(`Accessed the function time inside of a delayed commit call in ${watcher.debugName}. This means the prediction is almost certainly going to be wrong. You should provide Date.now() as an argument to the function instead. `);
1154
+ }
1130
1155
  let call = getCurrentCallAllowUndefined();
1131
1156
  if (call) {
1132
1157
  return call.runAtTime.time;
1133
1158
  }
1134
- let watcher = proxyWatcher.getTriggeredWatcherMaybeUndefined();
1135
1159
  if (watcher?.options.temporary) {
1136
1160
  return watcher.createTime;
1137
1161
  }
@@ -1313,4 +1337,5 @@ import { MachineController } from "../deployManager/machineController";
1313
1337
  import { getRecords, setRecord } from "../-b-authorities/dnsAuthority";
1314
1338
  import { testTCPIsListening } from "socket-function/src/networking";
1315
1339
  import { getNodeId, getNodeIdLocation } from "socket-function/src/nodeCache";
1340
+ import { onAllPredictionsFinished } from "../-0-hooks/hooks";
1316
1341
 
@@ -44,6 +44,7 @@ import { mergeFilterables, parseFilterable, serializeFilterable } from "../misc/
44
44
  import { isManagementUser, onAllPredictionsFinished } from "../-0-hooks/hooks";
45
45
  import { getDomain, isBootstrapOnly, isLocal } from "../config";
46
46
  import { logDisk } from "../diagnostics/logs/diskLogger";
47
+ import { flushPredictionQueueBase, runInPredictionQueue, syncHasPendingPredictionsBase } from "./predictionQueue";
47
48
 
48
49
  let yargObj = isNodeTrue() && yargs(process.argv)
49
50
  .option("fncfilter", { type: "string", default: "", desc: `Sets the filterable state for function calls, causing them to target specific FunctionRunners. If no FunctionRunner matches, all functions will fail to run. For example: "devtestserver" will match a FunctionRunner that uses the "devtestserver" filter. Merges with the existing filterable state if a client sets it explicitly.` })
@@ -148,7 +149,7 @@ let pendingPredictedCalls = new Map<string, {
148
149
  }>();
149
150
  export const debug_pendingPredictedCalls = pendingPredictedCalls;
150
151
 
151
- export function callWaitOn(callId: string, promise: Promise<unknown>) {
152
+ export function registerPredictionBlocker(callId: string, promise: Promise<unknown>) {
152
153
  let waitObj = pendingPredictedCalls.get(callId);
153
154
  if (!waitObj) return;
154
155
  let seqNum = ++waitObj.seqNum;
@@ -164,34 +165,72 @@ writeCall.value = async (callSpec: CallSpec, metadata: FunctionMetadata) => {
164
165
  obj: new PromiseObj(),
165
166
  seqNum: 0,
166
167
  });
167
- let promise = (async () => {
168
- // IMPORTANT! While it may seem like we can use addCall even if we are trusted, so we can
169
- // have function calling prediction, we can't. If did then we would depend on these
170
- // predictions with our writes, which could end up on other nodes, which would then
171
- // be rejected (as our predictions use a Time.version that is only on our
172
- // node). The in unavoidable, as it is too difficult to have predictions, but also not have
173
- // predictions, on the same node.
174
- // - AND, we can't just use the same Time.version as the FunctionRunner will use, as our
175
- // ValuePaths might differ, which would sometimes work, BUT, other times our ReadLocks
176
- // would be valid, BUT, the actual read value would be different (as we might compute
177
- // a different value than FunctionRunner). So... we would lose locking, at which point,
178
- // why even use FunctionRunner in the first place! Just write the writes directly!
179
- // TODO: Add the ability to use direct writes BUT if we have a rejection, commit it as a regular function call
180
- // - This will be a lot more efficient, although, if the committed dies and the function is rejected, the
181
- // value will be lost, which isn't great. However... trusted nodes shouldn't be dying so easily!
182
- let isTrusted = isServer() && await isTrustedByDomain(callSpec.DomainName);
183
- if (isTrusted) {
184
- // NOTE: Direct writes should probably be used for calls from trusted nodes, which will
185
- // be a lot faster.
186
- await baseWriteCall(callSpec, metadata);
187
- } else {
188
- await addCall(callSpec, metadata);
168
+ // IMPORTANT! While it may seem like we can use addCall even if we are trusted, so we can
169
+ // have function calling prediction, we can't. If did then we would depend on these
170
+ // predictions with our writes, which could end up on other nodes, which would then
171
+ // be rejected (as our predictions use a Time.version that is only on our
172
+ // node). The in unavoidable, as it is too difficult to have predictions, but also not have
173
+ // predictions, on the same node.
174
+ // - AND, we can't just use the same Time.version as the FunctionRunner will use, as our
175
+ // ValuePaths might differ, which would sometimes work, BUT, other times our ReadLocks
176
+ // would be valid, BUT, the actual read value would be different (as we might compute
177
+ // a different value than FunctionRunner). So... we would lose locking, at which point,
178
+ // why even use FunctionRunner in the first place! Just write the writes directly!
179
+ // TODO: Add the ability to use direct writes BUT if we have a rejection, commit it as a regular function call
180
+ // - This will be a lot more efficient, although, if the committed dies and the function is rejected, the
181
+ // value will be lost, which isn't great. However... trusted nodes shouldn't be dying so easily!
182
+ let isTrusted = isServer() && await isTrustedByDomain(callSpec.DomainName);
183
+ if (isTrusted) {
184
+ // NOTE: Direct writes should probably be used for calls from trusted nodes, which will
185
+ // be a lot faster.
186
+ let promise = baseWriteCall(callSpec, metadata);
187
+ registerPredictionBlocker(callSpec.CallId, promise);
188
+ await promise;
189
+ } else if (!Querysub.PREDICT_CALLS || metadata.nopredict) {
190
+ let promise = baseAddCall(callSpec);
191
+ registerPredictionBlocker(callSpec.CallId, promise);
192
+ await promise;
193
+ } else {
194
+ let success = false;
195
+ let cancel = () => { };
196
+ try {
197
+ let obj = prediction.predictCall(callSpec, metadata);
198
+ cancel = obj.cancel;
199
+
200
+ // NOTE: All functions go through the prediction queue, because all functions have to be in order whether or not they're delayed. If we add a non-delayed function to the queue, it's going to cause the other functions to finish as soon as possible, which is going to be when their predictions are ready.
201
+ let result = await runInPredictionQueue({
202
+ callSpec,
203
+ metadata,
204
+ prediction: obj.predictPromise,
205
+ });
206
+
207
+ if (result === "run") {
208
+ // NOTE: I think awaiting here is fine, as the the prediction blockers don't wait based on the right call finishing, and I don't think anything really waits for this to finish.
209
+ await baseAddCall(callSpec);
210
+ } else {
211
+ cancel();
212
+ }
213
+ success = true;
214
+ } finally {
215
+ if (!success) {
216
+ // Cancelling is important, as if it never got committed to the server, we won't get a rejection for the prediction. So we just have to abort the prediction right away, or otherwise it will stay forever and cause bugs.
217
+ cancel();
218
+ }
189
219
  }
190
- })();
191
- callWaitOn(callSpec.CallId, promise);
192
- await promise;
220
+ }
193
221
  };
194
222
 
223
+
224
+ export async function flushDelayedFunctions() {
225
+ await flushPredictionQueueBase();
226
+ }
227
+ export function syncHasPendingPredictions() {
228
+ return syncHasPendingPredictionsBase();
229
+ }
230
+
231
+
232
+
233
+
195
234
  export async function onCallPredict(call: CallSpec | undefined) {
196
235
  if (!call) return;
197
236
  await pendingPredictedCalls.get(call.CallId)?.obj.promise;
@@ -215,17 +254,10 @@ onAllPredictionsFinished.declare(() => {
215
254
  });
216
255
 
217
256
 
218
- export async function flushDelayedFunctions() {
219
- await prediction.flushDelayedFunctions();
220
- }
221
-
222
- export async function addCall(call: CallSpec, metadata: FunctionMetadata) {
223
- await prediction.addCall(call, metadata);
224
- }
225
257
 
226
- export async function baseAddCall(call: CallSpec, nodeId: string, cancel: () => void, type: string) {
258
+ export async function baseAddCall(call: CallSpec) {
227
259
  if (Querysub.DEBUG_CALLS) {
228
- console.log(`[Querysub] ${magenta(type)} ${green(debugCallSpec(call))} @${debugTime(call.runAtTime)}`);
260
+ console.log(`[Querysub] ${magenta("call")} ${green(debugCallSpec(call))} @${debugTime(call.runAtTime)}`);
229
261
  }
230
262
  let errorWatcher = Querysub.createWatcher(() => {
231
263
  let callResult = atomic(prediction.getCallResult(call));
@@ -239,14 +271,10 @@ export async function baseAddCall(call: CallSpec, nodeId: string, cancel: () =>
239
271
  setTimeout(() => {
240
272
  errorWatcher.dispose();
241
273
  }, MAX_CHANGE_AGE);
242
- try {
243
- await QuerysubController.nodes[nodeId].addCall(call);
244
- } catch (e) {
245
- // Cancellation is IMPORTANT! Otherwise the prediction might never be rejected,
246
- // and so the client will keep stale data!
247
- cancel();
248
- throw e;
249
- }
274
+
275
+ const nodeId = await querysubNodeId();
276
+ if (!nodeId) throw new Error("No querysub node found");
277
+ await QuerysubController.nodes[nodeId].addCall(call);
250
278
  }
251
279
 
252
280
  export class QuerysubControllerBase {
@@ -0,0 +1,183 @@
1
+ import { timeInSecond } from "socket-function/src/misc";
2
+ import { CallSpec, debugCallSpec, functionSchema } from "../3-path-functions/PathFunctionRunner";
3
+ import { FunctionMetadata } from "../3-path-functions/syncSchema";
4
+ import { getParentPathStr, getPathFromStr, getPathStr2 } from "../path";
5
+ import { PromiseObj } from "../promise";
6
+ import { Querysub } from "./QuerysubController";
7
+ import { PredictResult } from "./querysubPrediction";
8
+ import { delay } from "socket-function/src/batching";
9
+ import { blue, green, magenta, yellow } from "socket-function/src/formatting/logColors";
10
+ import { formatTime } from "socket-function/src/formatting/format";
11
+ import { measureBlock, measureWrap } from "socket-function/src/profiling/measure";
12
+ import { debugTime } from "../0-path-value-core/pathValueCore";
13
+ import { t } from "./Querysub";
14
+ import { getProxyPath } from "../2-proxy/pathValueProxy";
15
+
16
+ let commitQueues: Map<string, {
17
+ callSpec: CallSpec;
18
+ metadata: FunctionMetadata;
19
+ prediction: Promise<PredictResult | undefined>;
20
+ predictionResult: PredictResult | undefined;
21
+ commitTime: PromiseObj<void>;
22
+ resolveOrderedPromise: PromiseObj<void>;
23
+ canBeClobbered: boolean;
24
+ }[]> = new Map();
25
+ let commitQueueCount = Querysub.createLocalSchema("commitQueueCount", {
26
+ count: t.number,
27
+ });
28
+
29
+ export async function runInPredictionQueue(config: {
30
+ callSpec: CallSpec;
31
+ metadata: FunctionMetadata;
32
+ prediction: Promise<PredictResult | undefined>;
33
+ }): Promise<"cancel" | "run"> {
34
+ let { callSpec, prediction, metadata } = config;
35
+
36
+ let namespace = getPathStr2(callSpec.DomainName, callSpec.ModuleId);
37
+ // NOTE: While in an ideal world every single function ID would be unique, realistically functions in the same module are likely to access the same data. So by breaking up by module ID, we get a lot of usefulness by making it so that delays often can queue up despite unrelated functions running. But there aren't also many issues due to the delays because things still operate in a reasonable order.
38
+ let commitQueue = commitQueues.get(namespace);
39
+ if (!commitQueue) {
40
+ commitQueue = [];
41
+ commitQueues.set(namespace, commitQueue);
42
+ }
43
+
44
+ let commitTime = new PromiseObj<void>();
45
+ if (metadata.delayCommit) {
46
+ setTimeout(() => {
47
+ commitTime.resolve();
48
+ }, Querysub.DELAY_COMMIT_DELAY);
49
+ } else if (commitQueue.length > 0) {
50
+ // If we have anything that's delaying, always wait a few milliseconds. This might become very annoying later. However, I assume due to network latency, five milliseconds is not going to matter. And this will help batch values more. So even if we do have many non-delayed values that are running, hopefully we should get the predictions done pretty fast, and we should still be able to remove some clobbered values.
51
+ setTimeout(() => {
52
+ commitTime.resolve();
53
+ }, 5);
54
+ } else {
55
+ commitTime.resolve();
56
+ }
57
+
58
+
59
+ let obj = {
60
+ callSpec,
61
+ metadata,
62
+ prediction,
63
+ predictionResult: undefined as PredictResult | undefined,
64
+ commitTime,
65
+ resolveOrderedPromise: new PromiseObj<void>(),
66
+ canBeClobbered: !!metadata.delayCommit,
67
+ };
68
+ void prediction.then(result => {
69
+ obj.predictionResult = result;
70
+ });
71
+ commitQueue.push(obj);
72
+ Querysub.localCommit(() => commitQueueCount().count++);
73
+
74
+ let stillWaiting = true;
75
+ void (async () => {
76
+ let startTime = Date.now();
77
+ while (stillWaiting) {
78
+ await delay(timeInSecond * 5);
79
+ if (!stillWaiting) break;
80
+ let duration = Date.now() - startTime;
81
+ console.warn(`VERY SLOW PREDICTION FUNCTION! While this is pending, we cannot commit other writes in this namespace ${debugCallSpec(callSpec)} has been running for ${formatTime(duration)}`);
82
+ }
83
+ })();
84
+ let ourPredictBase: PredictResult | undefined;
85
+ try {
86
+ ourPredictBase = await prediction;
87
+ } finally {
88
+ stillWaiting = false;
89
+ }
90
+ const ourPredict = ourPredictBase;
91
+
92
+ await commitTime.promise;
93
+
94
+ // NOTE: I believe, due to the commit time, this should result in batches where we have many values clobbered by one value, and then after that many values clobbered by a new value, etc. etc. But I guess we'll find out...
95
+
96
+ let status: "cancel" | "run" = "run";
97
+ let index = commitQueue.indexOf(obj);
98
+ if (index >= 0 && obj.canBeClobbered && ourPredict) {
99
+ // NOTE: Usually, the predictions should be much faster than the times, so most of the predictions after us should be ready.
100
+
101
+ let isIncrementTypeValue = (
102
+ ourPredict.writes.find(x => ourPredict.readPaths.has(x.path))
103
+ || ourPredict.writes.find(x => ourPredict.readParentPaths.has(getParentPathStr(x.path)))
104
+ );
105
+ // if (isIncrementTypeValue) {
106
+ // console.warn(`A delayCommit call Is writing a value that is also reading, which means it can never clobber itself. Suggest it that you either remove the delayCommit flag as it's not doing anything, or change it so that you set the value instead of incrementing it. Fnc: ${debugCallSpec(callSpec)}`, { isIncrementTypeValue });
107
+ // }
108
+ // NOTE: Actually, we always end up reading and writing to the same values, because we check values before we write to them, so we don't write identical values. And even if we don't, there's too much code which is focused on initialization, so... We just won't do this check, and if it causes problems, we'll probably notice pretty quickly in the UI.
109
+ isIncrementTypeValue = undefined;
110
+
111
+ if (!isIncrementTypeValue) {
112
+ let examplePath0 = getProxyPath(() => functionSchema()[""].PathFunctionRunner);
113
+ let examplePath2 = getProxyPath(() => functionSchema()[""].PathFunctionRunner[""].Results);
114
+ let path0Array = getPathFromStr(examplePath0);
115
+ let path2Array = getPathFromStr(examplePath2);
116
+ let value0 = path0Array[path0Array.length - 1];
117
+ let index0 = path0Array.length - 1;
118
+ let value2 = path2Array[path2Array.length - 1];
119
+ let index2 = path2Array.length - 1;
120
+
121
+ // We only need to see if word clobbered values should unwind in relative order.
122
+ const doesValueClobberUs = measureWrap(function doesValueClobberUs(value: PredictResult): boolean {
123
+ // NOTE: See the delay commit notes for the cases that this doesn't handle. However, it should handle most cases, so it's probably fine. And if it doesn't handle a case, then we will see fairly quickly because the when the predictions are cancelled, we'll see values change that shouldn't (in the UI... hopefully).
124
+ let otherWrites = new Set(value.writes.map(write => write.path));
125
+ let nonFunctionWrites = ourPredict.writes.filter(write => {
126
+ let arr = getPathFromStr(write.path);
127
+ let isFunctionWrite = (
128
+ arr[index0] === value0 && arr[index2] === value2
129
+ );
130
+ return !isFunctionWrite;
131
+ });
132
+ let differentWrite = nonFunctionWrites.find(write => !otherWrites.has(write.path));
133
+ return (
134
+ // If it fully writes all the values we write to, then it eclipses us. And if it eclipses us And we aren't an increment type commit, then it likely clobbers us (As if we aren't an increment type commit, Then it is possible we are fully isolated with none of our reads changing, In which case, if we are eclipsed then by a value that also is like that, then clobbering is safe).
135
+ !differentWrite
136
+ );
137
+ });
138
+
139
+ // NOTE: This is quadratic, but I think it should be fine. If not, we have the measure wrap, so we'll know.
140
+ for (let i = commitQueue.length - 1; i > index; i--) {
141
+ let value = commitQueue[i].predictionResult;
142
+ if (!value) continue;
143
+ if (doesValueClobberUs(value)) {
144
+ // Don't allow it to be clobbered. Otherwise, we might result in a case where the actual written value keeps getting pushed off and off forever, and we never actually write to the server.
145
+ commitQueue[i].canBeClobbered = false;
146
+ status = "cancel";
147
+ if (Querysub.DEBUG_CALLS) {
148
+ console.log(`[Querysub] ${blue("removed redundant call")} ${green(debugCallSpec(commitQueue[i].callSpec))} has all of it's writes handled by ${green(debugCallSpec(commitQueue[index].callSpec))}`);
149
+ }
150
+ break;
151
+ }
152
+ }
153
+ }
154
+ }
155
+
156
+ // Resolve all the values before us as well, before even our own promise, and then wait on that promise. That way, everything finishes in the same order it starts.
157
+ if (index >= 0) {
158
+ for (let i = 0; i <= index; i++) {
159
+ commitQueue[i].resolveOrderedPromise.resolve();
160
+ }
161
+ commitQueue.splice(0, index + 1);
162
+
163
+ Querysub.localCommit(() => commitQueueCount().count -= index + 1);
164
+ }
165
+ await obj.resolveOrderedPromise.promise;
166
+
167
+
168
+ return status;
169
+ }
170
+
171
+ export async function flushPredictionQueueBase() {
172
+ let promises: Promise<void>[] = [];
173
+ for (let commitQueue of commitQueues.values()) {
174
+ for (let obj of commitQueue) {
175
+ obj.commitTime.resolve();
176
+ promises.push(obj.resolveOrderedPromise.promise);
177
+ }
178
+ }
179
+ await Promise.all(promises);
180
+ }
181
+ export function syncHasPendingPredictionsBase(): boolean {
182
+ return commitQueueCount().count > 0;
183
+ }