mirror of
https://github.com/gristlabs/grist-core.git
synced 2024-10-27 20:44:07 +00:00
142 lines
5.1 KiB
TypeScript
142 lines
5.1 KiB
TypeScript
import {DocumentMetadata, HomeDBManager} from 'app/gen-server/lib/homedb/HomeDBManager';
|
|
import log from 'app/server/lib/log';
|
|
|
|
/**
|
|
* HostedMetadataManager handles pushing document metadata changes to the Home database when
|
|
* a doc is updated. Currently updates doc updatedAt time and usage.
|
|
*/
|
|
export class HostedMetadataManager {
|
|
|
|
// Document metadata mapped by docId.
|
|
private _metadata: {[docId: string]: DocumentMetadata} = {};
|
|
|
|
// Set if the class holder is closing and no further pushes should be scheduled.
|
|
private _closing: boolean = false;
|
|
|
|
// Last push time in ms since epoch.
|
|
private _lastPushTime: number = 0.0;
|
|
|
|
// Callback for next opportunity to push changes.
|
|
private _timeout: NodeJS.Timeout|null = null;
|
|
|
|
// Maintains the update Promise to wait on it if the class is closing.
|
|
private _push: Promise<void>|null;
|
|
|
|
// The default delay in milliseconds between metadata pushes to the database.
|
|
private readonly _minPushDelayMs: number;
|
|
|
|
/**
|
|
* Create an instance of HostedMetadataManager.
|
|
* The minPushDelay is the default delay in seconds between metadata pushes to the database.
|
|
*/
|
|
constructor(private _dbManager: HomeDBManager, minPushDelay: number = 60) {
|
|
this._minPushDelayMs = minPushDelay * 1000;
|
|
}
|
|
|
|
/**
|
|
* Close the manager. Send out any pending updates and prevent more from being scheduled.
|
|
*/
|
|
public async close(): Promise<void> {
|
|
// Pushes will no longer be scheduled.
|
|
this._closing = true;
|
|
// Wait for outgoing pushes to finish before proceeding.
|
|
if (this._push) { await this._push; }
|
|
if (this._timeout) {
|
|
// Since an update was scheduled, perform one final update now.
|
|
this._update();
|
|
if (this._push) { await this._push; }
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Schedule a call to _update some time from now. When the update is made, it will
|
|
* store the given metadata in the updated_at and usage columns of the docs table for
|
|
* the specified document.
|
|
*
|
|
* If `minimizeDelay` is true, the push will be scheduled with minimum delay (0ms) and
|
|
* will cancel/overwrite an already scheduled push (if present).
|
|
*/
|
|
public scheduleUpdate(docId: string, metadata: DocumentMetadata, minimizeDelay = false): void {
|
|
if (this._closing) { return; }
|
|
|
|
// Update metadata even if an update is already scheduled - if the update has not yet occurred,
|
|
// the more recent metadata will be used.
|
|
this._setOrUpdateMetadata(docId, metadata);
|
|
if (this._timeout && !minimizeDelay) { return; }
|
|
|
|
this._schedulePush(minimizeDelay ? 0 : undefined);
|
|
}
|
|
|
|
public setDocsMetadata(docUpdateMap: {[docId: string]: DocumentMetadata}): Promise<any> {
|
|
return this._dbManager.setDocsMetadata(docUpdateMap);
|
|
}
|
|
|
|
/**
|
|
* Push all metadata updates to the database.
|
|
*/
|
|
private _update(): void {
|
|
if (this._timeout) {
|
|
clearTimeout(this._timeout);
|
|
this._timeout = null;
|
|
}
|
|
if (this._push) { return; }
|
|
this._push = this._performUpdate()
|
|
.catch(err => {
|
|
log.error("HostedMetadataManager error performing update: ", err);
|
|
})
|
|
.then(() => {
|
|
this._push = null;
|
|
if (!this._closing && !this._timeout && Object.keys(this._metadata).length !== 0) {
|
|
// If we have metadata that hasn't been pushed up yet, but no push scheduled,
|
|
// go ahead and schedule an immediate push. This can happen if `scheduleUpdate`
|
|
// is called frequently with minimizeDelay set to true, particularly when
|
|
// _performUpdate is taking a bit longer than normal to complete.
|
|
this._schedulePush(0);
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* This is called by the update function to actually perform the update. This should not
|
|
* be called unless to force an immediate update.
|
|
*/
|
|
private async _performUpdate(): Promise<void> {
|
|
// Await the database if it is not yet connected.
|
|
const docUpdates = this._metadata;
|
|
this._metadata = {};
|
|
this._lastPushTime = Date.now();
|
|
await this.setDocsMetadata(docUpdates);
|
|
}
|
|
|
|
/**
|
|
* Schedule a metadata push.
|
|
*
|
|
* If `delayMs` is specified, the push will be scheduled to occur at least that
|
|
* number of milliseconds in the future. If `delayMs` is unspecified, the push
|
|
* will be scheduled to occur at least `_minPushDelayMs` after the last push time.
|
|
*
|
|
* If called while a push is already scheduled, that push will be cancelled and
|
|
* replaced with this one.
|
|
*/
|
|
private _schedulePush(delayMs?: number): void {
|
|
if (delayMs === undefined) {
|
|
delayMs = Math.round(this._minPushDelayMs - (Date.now() - this._lastPushTime));
|
|
}
|
|
if (this._timeout) { clearTimeout(this._timeout); }
|
|
this._timeout = setTimeout(() => this._update(), delayMs < 0 ? 0 : delayMs);
|
|
}
|
|
|
|
/**
|
|
* Adds `docId` and its `metadata` to the list of queued updates, merging any existing values.
|
|
*/
|
|
private _setOrUpdateMetadata(docId: string, metadata: DocumentMetadata): void {
|
|
if (!this._metadata[docId]) {
|
|
this._metadata[docId] = metadata;
|
|
} else {
|
|
const {updatedAt, usage} = metadata;
|
|
if (updatedAt) { this._metadata[docId].updatedAt = updatedAt; }
|
|
if (usage !== undefined) { this._metadata[docId].usage = usage; }
|
|
}
|
|
}
|
|
}
|