030b7e90bf
* Enable `@typescript-eslint/explicit-member-accessibility` on /src * Prettier * Enable `@typescript-eslint/explicit-function-return-type` in /src * Fix types * tsc strict fixes * Delint * Fix test * Fix bad merge
124 lines
4.9 KiB
TypeScript
124 lines
4.9 KiB
TypeScript
/*
|
|
Copyright 2021 The Matrix.org Foundation C.I.C.
|
|
|
|
Licensed under the Apache License, Version 2.0 (the "License");
|
|
you may not use this file except in compliance with the License.
|
|
You may obtain a copy of the License at
|
|
|
|
http://www.apache.org/licenses/LICENSE-2.0
|
|
|
|
Unless required by applicable law or agreed to in writing, software
|
|
distributed under the License is distributed on an "AS IS" BASIS,
|
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
See the License for the specific language governing permissions and
|
|
limitations under the License.
|
|
*/
|
|
|
|
import { EnhancedMap } from "./maps";
|
|
|
|
// Inspired by https://pkg.go.dev/golang.org/x/sync/singleflight
|
|
|
|
const keyMap = new EnhancedMap<Object, EnhancedMap<string, unknown>>();
|
|
|
|
/**
|
|
* Access class to get a singleflight context. Singleflights execute a
|
|
* function exactly once, unless instructed to forget about a result.
|
|
*
|
|
* Typically this is used to de-duplicate an action, such as a save button
|
|
* being pressed, without having to track state internally for an operation
|
|
* already being in progress. This doesn't expose a flag which can be used
|
|
* to disable a button, however it would be capable of returning a Promise
|
|
* from the first call.
|
|
*
|
|
* The result of the function call is cached indefinitely, just in case a
|
|
* second call comes through late. There are various functions named "forget"
|
|
* to have the cache be cleared of a result.
|
|
*
|
|
* Singleflights in our use case are tied to an instance of something, combined
|
|
* with a string key to differentiate between multiple possible actions. This
|
|
* means that a "save" key will be scoped to the instance which defined it and
|
|
* not leak between other instances. This is done to avoid having to concatenate
|
|
* variables to strings to essentially namespace the field, for most cases.
|
|
*/
|
|
export class Singleflight {
|
|
private constructor() {}
|
|
|
|
/**
|
|
* A void marker to help with returning a value in a singleflight context.
|
|
* If your code doesn't return anything, return this instead.
|
|
*/
|
|
public static Void = Symbol("void");
|
|
|
|
/**
|
|
* Acquire a singleflight context.
|
|
* @param {Object} instance An instance to associate the context with. Can be any object.
|
|
* @param {string} key A string key relevant to that instance to namespace under.
|
|
* @returns {SingleflightContext} Returns the context to execute the function.
|
|
*/
|
|
public static for(instance: Object, key: string): SingleflightContext {
|
|
if (!instance || !key) throw new Error("An instance and key must be supplied");
|
|
return new SingleflightContext(instance, key);
|
|
}
|
|
|
|
/**
|
|
* Forgets all results for a given instance.
|
|
* @param {Object} instance The instance to forget about.
|
|
*/
|
|
public static forgetAllFor(instance: Object): void {
|
|
keyMap.delete(instance);
|
|
}
|
|
|
|
/**
|
|
* Forgets all cached results for all instances. Intended for use by tests.
|
|
*/
|
|
public static forgetAll(): void {
|
|
for (const k of keyMap.keys()) {
|
|
keyMap.remove(k);
|
|
}
|
|
}
|
|
}
|
|
|
|
class SingleflightContext {
|
|
public constructor(private instance: Object, private key: string) {}
|
|
|
|
/**
|
|
* Forget this particular instance and key combination, discarding the result.
|
|
*/
|
|
public forget(): void {
|
|
const map = keyMap.get(this.instance);
|
|
if (!map) return;
|
|
map.remove(this.key);
|
|
if (!map.size) keyMap.remove(this.instance);
|
|
}
|
|
|
|
/**
|
|
* Execute a function. If a result is already known, that will be returned instead
|
|
* of executing the provided function. However, if no result is known then the function
|
|
* will be called, with its return value cached. The function must return a value
|
|
* other than `undefined` - take a look at Singleflight.Void if you don't have a return
|
|
* to make.
|
|
*
|
|
* Note that this technically allows the caller to provide a different function each time:
|
|
* this is largely considered a bad idea and should not be done. Singleflights work off the
|
|
* premise that something needs to happen once, so duplicate executions will be ignored.
|
|
*
|
|
* For ideal performance and behaviour, functions which return promises are preferred. If
|
|
* a function is not returning a promise, it should return as soon as possible to avoid a
|
|
* second call potentially racing it. The promise returned by this function will be that
|
|
* of the first execution of the function, even on duplicate calls.
|
|
* @param {Function} fn The function to execute.
|
|
* @returns The recorded value.
|
|
*/
|
|
public do<T>(fn: () => T): T {
|
|
const map = keyMap.getOrCreate(this.instance, new EnhancedMap<string, unknown>());
|
|
|
|
// We have to manually getOrCreate() because we need to execute the fn
|
|
let val = <T>map.get(this.key);
|
|
if (val === undefined) {
|
|
val = fn();
|
|
map.set(this.key, val);
|
|
}
|
|
|
|
return val;
|
|
}
|
|
}
|