mirror of
https://github.com/gristlabs/grist-core.git
synced 2024-10-27 20:44:07 +00:00
124 lines
4.0 KiB
TypeScript
124 lines
4.0 KiB
TypeScript
|
/**
|
||
|
* RefCountMap maintains a reference-counted key-value map. Its sole method is use(key) which
|
||
|
* increments the counter for the key, and returns a disposable object which exposes the value via
|
||
|
* the get() method, and decrements the counter back on disposal.
|
||
|
*
|
||
|
* The value is constructed on first reference using options.create(key) callback. After the last
|
||
|
* reference is gone, and an optional gracePeriodMs elapsed, the value is cleaned up using
|
||
|
* options.dispose(key, value) callback.
|
||
|
*/
|
||
|
import {IDisposable} from 'grainjs';
|
||
|
|
||
|
export interface IRefCountSub<Value> extends IDisposable {
|
||
|
get(): Value;
|
||
|
dispose(): void;
|
||
|
}
|
||
|
|
||
|
export class RefCountMap<Key, Value> implements IDisposable {
|
||
|
private _map: Map<Key, RefCountValue<Value>> = new Map();
|
||
|
private _createKey: (key: Key) => Value;
|
||
|
private _disposeKey: (key: Key, value: Value) => void;
|
||
|
private _gracePeriodMs: number;
|
||
|
|
||
|
/**
|
||
|
* Values are created using options.create(key) on first use. They are disposed after last use,
|
||
|
* using options.dispose(key, value). If options.gracePeriodMs is greater than zero, values
|
||
|
* stick around for this long after last use.
|
||
|
*/
|
||
|
constructor(options: {
|
||
|
create: (key: Key) => Value,
|
||
|
dispose: (key: Key, value: Value) => void,
|
||
|
gracePeriodMs: number,
|
||
|
}) {
|
||
|
this._createKey = options.create;
|
||
|
this._disposeKey = options.dispose;
|
||
|
this._gracePeriodMs = options.gracePeriodMs;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Use a value, constructing it if needed, or only incrementing the reference count if this key
|
||
|
* is already in the map. The returned subscription object has a get() method which returns the
|
||
|
* actual value, and a dispose() method, which must be called to release this subscription (i.e.
|
||
|
* decrement back the reference count).
|
||
|
*/
|
||
|
public use(key: Key): IRefCountSub<Value> {
|
||
|
const rcValue = this._useKey(key);
|
||
|
return {
|
||
|
get: () => rcValue.value,
|
||
|
dispose: () => this._releaseKey(rcValue, key),
|
||
|
};
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Purge a key by immediately removing it from the map. Disposing the remaining IRefCountSub
|
||
|
* values will be no-ops.
|
||
|
*/
|
||
|
public purgeKey(key: Key): void {
|
||
|
// Note that we must be careful that disposing stale IRefCountSub values is a no-op even when
|
||
|
// the same key gets re-added to the map after purgeKey.
|
||
|
this._doDisposeKey(key);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Disposing clears the map immediately, and calls options.dispose on all values.
|
||
|
*/
|
||
|
public dispose(): void {
|
||
|
// Note that a clear() method like this one would not be OK. If the map were to continue being
|
||
|
// used after clear(), subscriptions created before clear() would wreak havoc when disposed.
|
||
|
for (const [key, r] of this._map) {
|
||
|
r.count = 0;
|
||
|
this._disposeKey.call(null, key, r.value);
|
||
|
}
|
||
|
this._map.clear();
|
||
|
}
|
||
|
|
||
|
private _useKey(key: Key): RefCountValue<Value> {
|
||
|
const r = this._map.get(key);
|
||
|
if (r) {
|
||
|
r.count += 1;
|
||
|
if (r.disposeTimeout) {
|
||
|
clearTimeout(r.disposeTimeout);
|
||
|
r.disposeTimeout = undefined;
|
||
|
}
|
||
|
return r;
|
||
|
}
|
||
|
const value = this._createKey.call(null, key);
|
||
|
const rcValue = new RefCountValue(value);
|
||
|
this._map.set(key, rcValue);
|
||
|
return rcValue;
|
||
|
}
|
||
|
|
||
|
private _releaseKey(r: RefCountValue<Value>, key: Key): void {
|
||
|
if (r.count > 0) {
|
||
|
r.count -= 1;
|
||
|
if (r.count === 0) {
|
||
|
if (this._gracePeriodMs > 0) {
|
||
|
if (!r.disposeTimeout) {
|
||
|
r.disposeTimeout = setTimeout(() => this._doDisposeKey(key), this._gracePeriodMs);
|
||
|
}
|
||
|
} else {
|
||
|
this._doDisposeKey(key);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
private _doDisposeKey(key: Key): void {
|
||
|
const r = this._map.get(key);
|
||
|
if (r) {
|
||
|
this._map.delete(key);
|
||
|
r.count = 0;
|
||
|
this._disposeKey.call(null, key, r.value);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* This is an implementation detail of the RefCountMap, which represents a single item.
|
||
|
*/
|
||
|
class RefCountValue<Value> {
|
||
|
public count: number = 1;
|
||
|
public disposeTimeout?: ReturnType<typeof setTimeout> = undefined;
|
||
|
constructor(public value: Value) {}
|
||
|
}
|