127 lines
		
	
	
		
			4.9 KiB
		
	
	
	
		
			TypeScript
		
	
	
			
		
		
	
	
			127 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 usecase 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) {
 | |
|         keyMap.delete(instance);
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * Forgets all cached results for all instances. Intended for use by tests.
 | |
|      */
 | |
|     public static forgetAll() {
 | |
|         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() {
 | |
|         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;
 | |
|     }
 | |
| }
 |