Add a throttle
This commit is contained in:
@@ -46,7 +46,7 @@ The root `package.json` also has a convenience dev dependency:
|
||||
- [concurrently](https://github.com/open-cli-tools/concurrently) for spawning
|
||||
parallel tasks when we invoke various yarn scripts.
|
||||
|
||||
## Crypto
|
||||
## Cryptography
|
||||
|
||||
We use [libsodium](https://libsodium.gitbook.io/doc/) for encryption, key
|
||||
generation etc. Specifically, we use its WebAssembly and JS wrappers made using
|
||||
@@ -85,9 +85,8 @@ our apps are regular React SPAs, and are not particularly tied to Next.
|
||||
### Vite
|
||||
|
||||
For some of our newer code, we have started to use [Vite](https://vitejs.dev).
|
||||
It is more lower level than Next, but the bells and whistles it doesn't have are
|
||||
the bells and whistles (and the accompanying complexity) that we don't need in
|
||||
some cases.
|
||||
It is likely the future (both generally, and for our code) since Next is
|
||||
becoming less suitable for SSR and SPAs with their push towards SSG.
|
||||
|
||||
## UI
|
||||
|
||||
@@ -179,6 +178,11 @@ For more details, see [translations.md](translations.md).
|
||||
- [zod](https://github.com/colinhacks/zod) is used for runtime typechecking
|
||||
(e.g. verifying that API responses match the expected TypeScript shape).
|
||||
|
||||
- [debounce](https://github.com/sindresorhus/debounce) and its
|
||||
promise-supporting sibling
|
||||
[pDebounce](https://github.com/sindresorhus/p-debounce) are used for
|
||||
debouncing operations (See also: `[Note: Throttle and debounce]`).
|
||||
|
||||
## Media
|
||||
|
||||
- [jszip](https://github.com/Stuk/jszip) is used for reading zip files in
|
||||
|
||||
@@ -9,6 +9,7 @@ import { blobCache } from "@/next/blob-cache";
|
||||
import { ensureElectron } from "@/next/electron";
|
||||
import log from "@/next/log";
|
||||
import { ComlinkWorker } from "@/next/worker/comlink-worker";
|
||||
import { throttled } from "@/utils/promise";
|
||||
import { proxy } from "comlink";
|
||||
import { isBetaUser, isInternalUser } from "../feature-flags";
|
||||
import { getRemoteFlag, updateRemoteFlag } from "../remote-store";
|
||||
@@ -421,7 +422,7 @@ const setInterimScheduledStatus = () => {
|
||||
setMLStatusSnapshot({ phase: "scheduled", nSyncedFiles, nTotalFiles });
|
||||
};
|
||||
|
||||
const workerDidProcessFile = triggerStatusUpdate;
|
||||
const workerDidProcessFile = throttled(updateMLStatusSnapshot, 2000);
|
||||
|
||||
/**
|
||||
* Return the IDs of all the faces in the given {@link enteFile} that are not
|
||||
|
||||
@@ -7,6 +7,82 @@
|
||||
export const wait = (ms: number) =>
|
||||
new Promise((resolve) => setTimeout(resolve, ms));
|
||||
|
||||
/**
|
||||
* Throttle invocations of an underlying function, coalescing pending calls.
|
||||
*
|
||||
* Take a function that returns a promise, and return a new function that can be
|
||||
* called an any number of times while still ensuring that the underlying
|
||||
* function is only called a maximum of once per the specified period.
|
||||
*
|
||||
* The underlying function is immediately called if there were no calls to the
|
||||
* throttled function in the last period.
|
||||
*
|
||||
* Otherwise we start waiting. Multiple calls to the throttled function while
|
||||
* we're waiting (either for the original promise to resolve, or after that, for
|
||||
* the specified cooldown period to elapse) will all be coalesced into a single
|
||||
* call to the underlying function when we're done waiting.
|
||||
*
|
||||
* ---
|
||||
*
|
||||
* [Note: Throttle and debounce]
|
||||
*
|
||||
* There are many throttle/debounce libraries, and ideally I'd have liked to
|
||||
* just use one of them instead of reinventing such a basic and finicky wheel.
|
||||
* Then why write a bespoke one?
|
||||
*
|
||||
* - "debounce" means that the underlying function will only be called when a
|
||||
* particular wait time has elapsed since the last call to the _debounced_
|
||||
* function.
|
||||
*
|
||||
* - This behaviour, while useful sometimes, is not what we want always. If
|
||||
* the debounced function is continuously being called, then the underlying
|
||||
* function might never get called (since the wait time does not elapse).
|
||||
*
|
||||
* - To avoid this starvation, some debounce implementations like lodash
|
||||
* provide a "maxWait" option, which tells the debounced function to always
|
||||
* call the underlying function if maxWait has elapsed.
|
||||
*
|
||||
* - The debounced functions can trigger the underlying in two ways: leading
|
||||
* (aka immediate) and trailing which control if the underlying should be
|
||||
* called at the leading or the trailing edge of the time period.
|
||||
*
|
||||
* - "throttle" can be conceptually thought of as just maxWait + leading. In
|
||||
* fact, this is how lodash actually implements it too. So we could've used
|
||||
* lodash, except that is a big dependency to pull for a small function.
|
||||
*
|
||||
* - Alternatively, pThrottle is a micro-library that provide such a
|
||||
* "throttle" primitive. However, its implementation enqueues all incoming
|
||||
* requests to the throttled function: it still calls the underlying once
|
||||
* per period, but eventually underlying will get called once for each call
|
||||
* to the throttled function.
|
||||
*
|
||||
* - There are circumstances where that would be the appropriate behaviour,
|
||||
* but that's not what we want. We wish to trigger an async action,
|
||||
* coalescing multiple triggers into a single one, one per period.
|
||||
*
|
||||
* - Perhaps there are other focused and standard library that'd have what we
|
||||
* want, but instead of spending more time searching I just wrote it from
|
||||
* scratch for now. Indeed, I've spent more time writing about the function
|
||||
* than the function itself.
|
||||
*/
|
||||
export const throttled = (underlying: () => Promise<void>, period: number) => {
|
||||
let pending = 0;
|
||||
|
||||
const f = () => {
|
||||
pending += 1;
|
||||
if (pending > 1) return;
|
||||
void underlying()
|
||||
.then(() => wait(period))
|
||||
.then(() => {
|
||||
const retrigger = pending > 1;
|
||||
pending = 0;
|
||||
if (retrigger) f();
|
||||
});
|
||||
};
|
||||
|
||||
return f;
|
||||
};
|
||||
|
||||
/**
|
||||
* Await the given {@link promise} for {@link timeoutMS} milliseconds. If it
|
||||
* does not resolve within {@link timeoutMS}, then reject with a timeout error.
|
||||
|
||||
Reference in New Issue
Block a user