git-documentdb
Version:
Offline-first database that syncs with Git
371 lines • 13.5 kB
JavaScript
"use strict";
/**
* GitDocumentDB
* Copyright (c) Hidekazu Kubota
*
* This source code is licensed under the Mozilla Public License Version 2.0
* found in the LICENSE file in the root directory of this source tree.
*/
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.TaskQueue = void 0;
/**
* ! Must import both clearInterval and setInterval from 'timers'
*/
const timers_1 = require("timers");
const ulid_1 = require("ulid");
const async_lock_1 = __importDefault(require("async-lock"));
const utils_1 = require("./utils");
const error_1 = require("./error");
/**
* TaskQueue
*
* @public
*/
class TaskQueue {
/**
* Constructor
*
* @remarks
* Must call start() after new TaskQueue()
*
* @public
*/
constructor(logger) {
// Monotonic counter
this._ulid = (0, ulid_1.monotonicFactory)();
this._lock = new async_lock_1.default();
this._taskQueue = [];
this._isTaskQueueWorking = false;
/**
* Task Statistics
*
* (Just for test purposes)
*/
this._statistics = {
put: 0,
insert: 0,
update: 0,
delete: 0,
push: 0,
sync: 0,
cancel: 0,
};
this._currentTask = undefined;
this._checkTimerInterval = 100;
this._logger = logger;
}
/**
* Set logger
*
* @internal
*/
setLogger(logger) {
this._logger = logger;
}
/**
* Get current task ID
*
* @public
*/
currentTaskId() {
var _a;
return (_a = this._currentTask) === null || _a === void 0 ? void 0 : _a.taskId;
}
/**
* Get default task ID
*
* @remarks ID monotonically increases. It does not ensures the task order in _taskQueue.
*
* @internal
*/
newTaskId() {
return this._ulid(Date.now());
}
/**
* Get enqueue time
*
* @remarks It ensures the task order in _taskQueue.
*
* @public
*/
getEnqueueTime() {
return this._ulid(Date.now());
}
/**
* Push task to TaskQueue
*
* @internal
*/
// eslint-disable-next-line complexity
pushToTaskQueue(task) {
// Critical section
this._lock // eslint-disable-next-line complexity
.acquire('TaskQueue', () => {
var _a, _b, _c;
// Skip consecutive sync/push events
this._logger.silly(`Try to push ${task.label}@${task.taskId} into ${JSON.stringify(this._taskQueue)}`);
if ((this._taskQueue.length === 0 &&
((_a = this._currentTask) === null || _a === void 0 ? void 0 : _a.syncRemoteName) === task.syncRemoteName &&
((((_b = this._currentTask) === null || _b === void 0 ? void 0 : _b.label) === 'sync' && task.label === 'sync') ||
(((_c = this._currentTask) === null || _c === void 0 ? void 0 : _c.label) === 'push' && task.label === 'push'))) ||
(this._taskQueue.length > 0 &&
this._taskQueue[this._taskQueue.length - 1].syncRemoteName ===
task.syncRemoteName &&
((this._taskQueue[this._taskQueue.length - 1].label === 'sync' &&
task.label === 'sync') ||
(this._taskQueue[this._taskQueue.length - 1].label === 'push' &&
task.label === 'push')))) {
task.cancel();
this._statistics.cancel++;
throw new error_1.Err.ConsecutiveSyncSkippedError(task.label, task.taskId);
}
// eslint-disable-next-line require-atomic-updates
task.enqueueTime = this.getEnqueueTime();
this._taskQueue.push(task);
})
.then(() => {
const taskMetadata = {
label: task.label,
taskId: task.taskId,
shortId: task.shortId,
shortName: task.shortName,
collectionPath: task.collectionPath,
enqueueTime: task.enqueueTime,
syncRemoteName: task.syncRemoteName,
debounceTime: task.debounceTime,
};
if (task.enqueueCallback) {
try {
task.enqueueCallback(taskMetadata);
}
catch (e) {
this._logger.debug(`Error in enqueueCallback (fullDocPath: ${task.collectionPath + task.shortName}) ${e}`, utils_1.CONSOLE_STYLE.bgGreen().fgRed().tag);
}
}
})
.catch(e => {
if (e instanceof error_1.Err.ConsecutiveSyncSkippedError) {
this._logger.debug(e.message, utils_1.CONSOLE_STYLE.bgGreen().fgRed().tag);
}
else {
throw e;
}
});
}
/**
* Start TaskQueue
*
* @public
*/
start() {
if (this._lock === undefined) {
this._lock = new async_lock_1.default();
}
if (this._checkTimer === undefined) {
this._checkTimer = (0, timers_1.setInterval)(() => {
this._checkTaskQueue();
}, this._checkTimerInterval);
}
}
/**
* Stop TaskQueue
*
* @public
*/
stop() {
// Clear not queued job
if (this._checkTimer !== undefined) {
(0, timers_1.clearInterval)(this._checkTimer);
this._checkTimer = undefined;
}
if (this._lock !== undefined) {
// @ts-ignore
this._lock.queues.taskQueue = null;
}
this._lock = undefined;
// Cancel queued tasks
this._taskQueue.forEach(task => task.cancel());
this._taskQueue.length = 0;
this._isTaskQueueWorking = false;
this._currentTask = undefined;
this._statistics = {
put: 0,
insert: 0,
update: 0,
delete: 0,
push: 0,
sync: 0,
cancel: 0,
};
}
/**
* Get length of TaskQueue
*
* @public
*/
length() {
return this._taskQueue.length;
}
/**
* Get current statistics
*
* @public
*/
currentStatistics() {
return JSON.parse(JSON.stringify(this._statistics));
}
/**
* @internal
*/
async waitCompletion(timeoutMsec) {
const startMsec = Date.now();
let isTimeout = false;
while ((this._taskQueue.length > 0 || this._isTaskQueueWorking) && !isTimeout) {
if (Date.now() - startMsec > timeoutMsec) {
isTimeout = true;
}
// eslint-disable-next-line no-await-in-loop
await (0, utils_1.sleep)(100);
}
return isTimeout;
}
_pullTargetTask(targetIndex) {
if (targetIndex >= this._taskQueue.length) {
return undefined;
}
if (targetIndex === 0) {
return this._taskQueue.shift();
}
const [task] = this._taskQueue.splice(targetIndex, 1);
return task;
}
/**
* checkTaskQueue
* @internal
*/
_checkTaskQueue() {
if (this._lock === undefined)
return;
if (this._lock.isBusy())
return;
// eslint-disable-next-line complexity
this._lock.acquire('TaskQueue', () => {
if (this._taskQueue.length === 0 || this._isTaskQueueWorking)
return;
let taskIndex = 0;
while (taskIndex < this._taskQueue.length) {
const targetTask = this._taskQueue[taskIndex];
if (targetTask.debounceTime === undefined || targetTask.debounceTime < 0) {
this._currentTask = this._pullTargetTask(taskIndex);
if (this._currentTask !== undefined)
this._execTask();
return;
}
if (targetTask.label !== 'put' && targetTask.label !== 'update') {
this._currentTask = this._pullTargetTask(taskIndex);
if (this._currentTask !== undefined)
this._execTask();
return;
}
const targetFullDocPath = targetTask.collectionPath + targetTask.shortName;
const expiredTime = (0, ulid_1.decodeTime)(targetTask.enqueueTime) + targetTask.debounceTime;
const current = Date.now();
let nextPutUpdateExist = false;
let nextDeleteExist = false;
let nextInsertExist = false;
for (let i = taskIndex + 1; i < this._taskQueue.length; i++) {
const tmpTask = this._taskQueue[i];
if ((0, ulid_1.decodeTime)(tmpTask.enqueueTime) > expiredTime)
break;
if ((tmpTask.label === 'put' ||
tmpTask.label === 'insert' ||
tmpTask.label === 'update' ||
tmpTask.label === 'delete') &&
targetFullDocPath === tmpTask.collectionPath + tmpTask.shortName) {
// eslint-disable-next-line max-depth
if (tmpTask.label === 'delete') {
nextDeleteExist = true;
break;
}
if (tmpTask.label === 'insert') {
nextInsertExist = true;
break;
}
nextPutUpdateExist = true;
break;
}
}
if (nextPutUpdateExist) {
const cancelTask = this._pullTargetTask(taskIndex);
cancelTask === null || cancelTask === void 0 ? void 0 : cancelTask.cancel();
continue;
}
else if (nextDeleteExist) {
this._currentTask = this._pullTargetTask(taskIndex);
if (this._currentTask !== undefined)
this._execTask();
return;
}
else if (nextInsertExist) {
this._currentTask = this._pullTargetTask(taskIndex);
if (this._currentTask !== undefined)
this._execTask();
return;
}
if (expiredTime <= current) {
this._currentTask = this._pullTargetTask(taskIndex);
if (this._currentTask !== undefined)
this._execTask();
return;
}
taskIndex++;
}
});
}
/**
* execTask
* @internal
*/
// eslint-disable-next-line complexity
_execTask() {
if (this._currentTask !== undefined && this._currentTask.func !== undefined) {
this._isTaskQueueWorking = true;
const label = this._currentTask.label;
const shortId = this._currentTask.shortId;
const shortName = this._currentTask.shortName;
const collectionPath = this._currentTask.collectionPath;
const fullDocPath = collectionPath ? collectionPath + shortName : '';
const taskId = this._currentTask.taskId;
const syncRemoteName = this._currentTask.syncRemoteName;
this._logger.debug(`Start: ${label}(${fullDocPath})@${taskId}`, utils_1.CONSOLE_STYLE.bgYellow().fgBlack().tag);
const beforeResolve = () => {
this._logger.debug(`End: ${label}(${fullDocPath})@${taskId}`, utils_1.CONSOLE_STYLE.bgGreen().fgBlack().tag);
this._statistics[label]++;
};
const beforeReject = () => {
this._logger.debug(`End with error: ${label}(${fullDocPath})@${taskId}`, utils_1.CONSOLE_STYLE.bgGreen().fgRed().tag);
this._statistics[label]++;
};
const taskMetadata = {
label,
taskId,
shortId,
shortName,
collectionPath,
enqueueTime: this._currentTask.enqueueTime,
syncRemoteName,
};
this._currentTask.func(beforeResolve, beforeReject, taskMetadata).finally(() => {
var _a, _b;
this._logger.silly(`Clear currentTask: ${(_a = this._currentTask) === null || _a === void 0 ? void 0 : _a.label}@${(_b = this._currentTask) === null || _b === void 0 ? void 0 : _b.taskId}`);
this._isTaskQueueWorking = false;
this._currentTask = undefined;
});
}
}
}
exports.TaskQueue = TaskQueue;
//# sourceMappingURL=task_queue.js.map