UNPKG

@node-elion/syncron

Version:

Provides a simple way to delivery models between sender and receiver

654 lines 31.9 kB
"use strict"; var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); }; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.ModelEventSubscriber = exports.ModelRequestStrategy = void 0; const lodash_merge_1 = __importDefault(require("lodash.merge")); const types_1 = require("./types"); var ModelRequestStrategy; (function (ModelRequestStrategy) { ModelRequestStrategy[ModelRequestStrategy["parallel"] = 0] = "parallel"; ModelRequestStrategy[ModelRequestStrategy["sequence"] = 1] = "sequence"; })(ModelRequestStrategy || (exports.ModelRequestStrategy = ModelRequestStrategy = {})); class ModelEventSubscriber { static simpleSubscribe(params, onEvent) { params.track(params.trackName, (header, body) => __awaiter(this, void 0, void 0, function* () { if (header === params.trackName) { onEvent(body); } })); return { unsubscribe: () => params.removeTrack(params.trackName), }; } constructor(config) { var _a, _b, _c, _d, _e, _f, _g, _h; this.queue = []; this.sendQueue = []; this.queueTimeout = null; this.keepAliveTimeout = null; this.modelState = new Map(); this.modelStateIndexes = []; this.trackIdentifiers = []; this.generateTrackIdentifier = (action) => (0, types_1.generateTrackIdentifier)(this.config.trackModelName, action); this.config = Object.assign(Object.assign({}, config), { getAll: (_a = config.getAll) !== null && _a !== void 0 ? _a : (() => this.getBatchDefault.bind(this)([], this.config.modelRequest.strategy)), modelRequest: { strategy: (_c = (_b = config.modelRequest) === null || _b === void 0 ? void 0 : _b.strategy) !== null && _c !== void 0 ? _c : ModelRequestStrategy.parallel, }, batchSize: config.batchSize || types_1.ModelSubscribeEventBatchSize.auto, firstContentSend: typeof config.firstContentSend !== "object" ? false : { size: ((_d = config.firstContentSend) === null || _d === void 0 ? void 0 : _d.size) || types_1.ModelSubscriberEventFirstContentSendSize.auto, }, queWaitTime: config.queWaitTime || types_1.ModelSubscribeEventQueWaitTime.default, metaFields: config.metaFields || {}, customTriggers: config.customTriggers || {}, keepAlive: { period: config.keepAlive.period || types_1.ModelSubscribeEventKeepAlivePeriod.default, pendingPeriod: config.keepAlive.pendingPeriod || types_1.ModelSubscribeEventKeepAliveCheckPendingPeriod.default, onKeepAlive: config.keepAlive.onKeepAlive, }, optimization: { publisherModelEventOptimization: (_f = (_e = config.optimization) === null || _e === void 0 ? void 0 : _e.publisherModelEventOptimization) !== null && _f !== void 0 ? _f : true, subscriberPostModelEventOptimization: (_h = (_g = config.optimization) === null || _g === void 0 ? void 0 : _g.subscriberPostModelEventOptimization) !== null && _h !== void 0 ? _h : true, } }); this.init(); } getBatchDefault(idList, strategy) { return __awaiter(this, void 0, void 0, function* () { let ids = idList; if (!idList || idList.length === 0) { ids = yield this.config.getAllIds(); } const usedStrategy = strategy || this.config.modelRequest.strategy; if (usedStrategy === ModelRequestStrategy.parallel) { return Promise.all(ids.map((id) => this.config.getById(id))); } const result = []; for (let i = 0; i < ids.length; i++) { result.push(yield this.config.getById(ids[i])); } return result; }); } init() { return __awaiter(this, void 0, void 0, function* () { this.keepAliveTimeout = setTimeout(this.onKeepAliveCheck.bind(this), this.config.keepAlive.period); this.trackIdentifiers = [ this.generateTrackIdentifier(types_1.ModelEventAction.UPDATE), this.generateTrackIdentifier(types_1.ModelEventAction.DELETE), ...Object.keys(this.config.customTriggers).map((key) => this.generateTrackIdentifier(key)), ]; this.trackIdentifiers.forEach((trackIdentifier) => this.config.track(trackIdentifier, this.onTrackEvent.bind(this))); this.pushToSendQueue({ modelName: this.config.trackModelName, idParamName: this.config.idParamName, action: types_1.ModelEventAction.MANUAL_UPDATE, data: { id: types_1.ManualUpdateTypes.INIT, }, }, ...Object.entries((yield Promise.all(Object.entries(this.config.metaFields).map((_a) => __awaiter(this, [_a], void 0, function* ([key, item]) { return [ key, yield item.onModelChange(), ]; })))).reduce((acc, [key, value]) => { acc[key] = value; return acc; }, {})).map(([key, value]) => { const event = { modelName: this.config.trackModelName, idParamName: this.config.idParamName, action: types_1.ModelEventAction.META, data: { id: key, data: value, }, }; return event; })); const prepareModelCreateEvent = ([id, data, index]) => { const createEvent = { modelName: this.config.trackModelName, idParamName: this.config.idParamName, action: types_1.ModelEventAction.UPDATE, data: { id, data, index, updateStrategy: types_1.UpdateStrategy.REPLACE, }, }; return createEvent; }; if (this.config.firstContentSend !== false) { const allIds = yield this.config.getAllIds(); const idsWithIndexes = allIds.map((id, index) => [id, index]); const firstContentIds = idsWithIndexes.slice(0, this.config.firstContentSend.size === "auto" ? Math.ceil(allIds.length / 10) : this.config.firstContentSend.size); const firstPart = yield this.getBatchDefault(firstContentIds.map(([id]) => id)); firstContentIds.forEach(([id, posIndex], index) => { const item = firstPart[index]; this.setModel(id, item, posIndex); }); this.pushToSendQueue(...firstContentIds .map(([id, posIndex], index) => [ id, firstPart[index], posIndex, ]) .map(prepareModelCreateEvent)); const lastPartIds = idsWithIndexes.slice(firstContentIds.length); if (lastPartIds.length > 0) { const lastPart = yield this.getBatchDefault(lastPartIds.map(([id]) => id)); lastPartIds.forEach(([id, posIndex], index) => { const item = lastPart[index]; this.setModel(id, item, posIndex); }); this.pushToSendQueue(...lastPartIds .map(([id, posIndex], index) => [ id, lastPart[index], posIndex, ]) .map(prepareModelCreateEvent)); } } else { const allObjects = yield this.config.getAll(); const allObjectData = allObjects.map((item, index) => [ item[this.config.idParamName], item, index, ]); allObjectData.forEach((item) => { this.setModel(...item); }); this.pushToSendQueue(...allObjectData.map(prepareModelCreateEvent)); } }); } setModel(id, data, index = null) { this.modelState.set(id, data); if (index !== null) this.setIdToIndex(id, index); } deleteModel(id) { this.modelState.delete(id); this.modelStateIndexes.splice(this.modelStateIndexes.indexOf(id), 1); } setIdToIndex(id, index = -1) { const isModelPresent = this.modelState.has(id); if (!isModelPresent) throw new Error(`Model with id ${id} is not present in modelState`); const currentIndex = this.modelStateIndexes.indexOf(id); if (currentIndex !== -1) { this.modelStateIndexes.splice(currentIndex, 1); } if (index !== -1) { this.modelStateIndexes.push(id); } else { this.modelStateIndexes.splice(index, 0, id); } } getStateIdList() { return this.modelStateIndexes; } pushToQueue(...events) { if (events.length) { const currentQueLength = this.queue.length; const currentSendQueLength = this.sendQueue.length; this.queue.push(...events); if (currentQueLength === 0 && currentSendQueLength === 0) { setTimeout(this.onQueueStep.bind(this), 0); } } } pushToSendQueue(...events) { if (events.length) { const currentQueLength = this.queue.length; const currentSendQueLength = this.sendQueue.length; this.sendQueue.push(...events); if (currentQueLength === 0 && currentSendQueLength === 0) { setTimeout(this.onQueueStep.bind(this), 0); } } } onKeepAliveCheck() { return __awaiter(this, void 0, void 0, function* () { const isAlive = yield Promise.race([ this.config.keepAlive.onKeepAlive(), new Promise((resolve) => { setTimeout(() => { resolve(false); }, this.config.keepAlive.pendingPeriod); }), ]); if (!isAlive) { yield this.unsubscribe(); } else { this.keepAliveTimeout = setTimeout(this.onKeepAliveCheck.bind(this), this.config.keepAlive.period); } }); } optimizeQueue() { return __awaiter(this, void 0, void 0, function* () { this.queue.reduce((acc, item, index) => { const itemIndex = acc.findIndex((i) => i.id === item.body.id); if (itemIndex === -1) { return [ ...acc, { id: item.body.id, actions: [ { name: (0, types_1.getActionFromTrackIdentifier)(item.header), index, }, ], }, ]; } acc[itemIndex].actions.push({ name: (0, types_1.getActionFromTrackIdentifier)(item.header), index, }); return acc; }, []) .filter((item) => item.actions.length > 1) .forEach((item) => { var _a; if (Array.from(new Set(item.actions.map((action) => action.name))).length === 1 && (((_a = this.config.customTriggers[item.actions[0].name]) === null || _a === void 0 ? void 0 : _a.allowOptimization) || !this.config.customTriggers[item.actions[0].name])) { item.actions.slice(0, -1).forEach((action) => { this.queue.splice(action.index, 1); }); const updateItem = item.actions.slice(-1)[0]; if (this.queue[updateItem.index] .body.updateStrategy === types_1.UpdateStrategy.MERGE) { this.queue[updateItem.index] .body.data = undefined; this.queue[updateItem.index] .body.updateStrategy = types_1.UpdateStrategy.REPLACE; } } else if (item.actions.some((action) => action.name === types_1.ModelEventAction.DELETE) && !item.actions.some((action) => { var _a; return (_a = this.config.customTriggers[action.name]) === null || _a === void 0 ? void 0 : _a.allowOptimization; })) { const reverseActions = [...item.actions].reverse(); const currentDeleteIndex = reverseActions.findIndex((action) => action.name === types_1.ModelEventAction.DELETE); const currentUpdateIndex = reverseActions.findIndex((action) => action.name !== types_1.ModelEventAction.DELETE); if (currentUpdateIndex < currentDeleteIndex) { // since delete is last, we can just remove all besides delete reverseActions.splice(currentDeleteIndex, 1); reverseActions.forEach((action) => { this.queue.splice(action.index, 1); }); } else { // since update is last, we can just remove all besides update const updateItem = reverseActions.splice(currentUpdateIndex, 1); if (this.queue[updateItem[0].index] .body.updateStrategy === types_1.UpdateStrategy.MERGE) { // could cause issues if previous update was merge also // remove data and change updateStrategy to replace for right now this.queue[updateItem[0].index] .body.data = undefined; this.queue[updateItem[0].index] .body.updateStrategy = types_1.UpdateStrategy.REPLACE; } reverseActions.forEach((action) => { this.queue.splice(action.index, 1); }); } } }); }); } optimizeSendQueue() { return __awaiter(this, void 0, void 0, function* () { const metaEvents = Object.values(this.sendQueue .filter((event) => event.action === types_1.ModelEventAction.META) .reduce((acc, event) => (Object.assign(Object.assign({}, acc), { [event.data.id]: event })), {})).reduce((acc, value) => [...acc, value], []); const systemEvents = this.sendQueue.filter((event) => event.action !== types_1.ModelEventAction.MANUAL_UPDATE); const modelEvents = this.sendQueue.filter((event) => [types_1.ModelEventAction.MANUAL_UPDATE, types_1.ModelEventAction.META].indexOf(event.action) === -1); const uniqModelEvents = Array.from(new Set(modelEvents.map((event) => event.data.id))) .map((id) => { const events = modelEvents .filter((event) => event.data.id === id) .map((event, index) => ({ event, index, })); const deleteEvent = events .slice() .reverse() .find(({ event }) => event.action === types_1.ModelEventAction.DELETE); const updateEvents = events.filter(({ event }) => [ types_1.ModelEventAction.DELETE, types_1.ModelEventAction.META, types_1.ModelEventAction.MANUAL_UPDATE, ].indexOf(event.action) === -1); const excludedEvents = []; if (deleteEvent) { if (deleteEvent.index > Math.max(...updateEvents.map(({ index }) => index))) { return [deleteEvent.event]; } excludedEvents.push(...events .map(({ index }) => index) .filter((index) => deleteEvent.index >= index)); } const clearedUpdateEvents = updateEvents .filter(({ index }) => !excludedEvents.includes(index)) .map((item, index) => (Object.assign({ internalIndex: index }, item))); if (clearedUpdateEvents.length > 1) { const reversedEvents = clearedUpdateEvents .slice() .reverse(); const lastUpdateEvent = reversedEvents.find(({ event }) => event.action === types_1.ModelEventAction.UPDATE); const lastUpdateReturn = () => [ lastUpdateEvent.event, ...clearedUpdateEvents .filter(({ event }) => [ types_1.ModelEventAction.UPDATE_INDEX, types_1.ModelEventAction.UPDATE, ].indexOf(event.action) === -1) .map(({ event }) => event), ]; if (lastUpdateEvent.internalIndex === clearedUpdateEvents.length - 1) { if (lastUpdateEvent.event .data.updateStrategy === types_1.UpdateStrategy.REPLACE) { return lastUpdateReturn(); } } else { const lastUpdateIndexEvent = reversedEvents.find(({ event }) => event.action === types_1.ModelEventAction.UPDATE_INDEX); if (lastUpdateIndexEvent.internalIndex > lastUpdateEvent.internalIndex) { lastUpdateEvent.event.data.index = lastUpdateIndexEvent.event.data.index; } if (lastUpdateEvent.event .data.updateStrategy === types_1.UpdateStrategy.REPLACE) { return lastUpdateReturn(); } } excludedEvents.push(...clearedUpdateEvents .filter(({ event }) => event.action === types_1.ModelEventAction.UPDATE_INDEX) .map(({ index }) => index)); } return events .filter(({ index }) => !excludedEvents.includes(index)) .map(({ event }) => event); }) .flat(); this.sendQueue = [...metaEvents, ...systemEvents, ...uniqModelEvents]; }); } updateMetaFields(triggers_1) { return __awaiter(this, arguments, void 0, function* (triggers, doPush = true) { const doTriggerFilter = (items) => triggers ? items.filter(([, item]) => item.modelTriggers.some((trigger) => triggers.includes(trigger)) || (item.customTriggers || []).some((customTrigger) => triggers.includes(customTrigger))) : items; const items = (yield Promise.all(doTriggerFilter(Object.entries(this.config.metaFields)).map((_a) => __awaiter(this, [_a], void 0, function* ([key, item]) { return [key, yield item.onModelChange()]; })))).map(([key, value]) => ({ modelName: this.config.trackModelName, idParamName: this.config.idParamName, action: types_1.ModelEventAction.META, data: { id: key, data: value, }, })); return doPush ? this.pushToSendQueue(...items) : items; }); } onQueueStep() { return __awaiter(this, void 0, void 0, function* () { if (this.queue.length) { let shouldUpdateIndexes = false; let newIdList = null; if (this.config.optimization.publisherModelEventOptimization) yield this.optimizeQueue(); const triggers = this.queue.map((event) => (0, types_1.getActionFromTrackIdentifier)(event.header)); for (let i = 0; i < this.queue.length; i++) { const eventResponse = yield this.performQue(this.queue[i], newIdList); if (eventResponse.shouldUpdateIndexes) { shouldUpdateIndexes = true; } if (eventResponse.newIdList) { newIdList = eventResponse.newIdList; } } this.queue = []; if (shouldUpdateIndexes) { yield this.findIndexDiff(newIdList); } yield this.updateMetaFields(triggers); } if (this.sendQueue.length) { if (this.config.optimization.subscriberPostModelEventOptimization) yield this.optimizeSendQueue(); if (this.config.batchSize === types_1.ModelSubscribeEventBatchSize.auto) { this.config.onModelEvent(this.sendQueue); this.sendQueue = []; } else { const batch = this.sendQueue.splice(0, this.config.batchSize); this.config.onModelEvent(batch); } } if (this.queue.length || this.sendQueue.length) { this.queueTimeout = setTimeout(this.onQueueStep.bind(this), this.config.queWaitTime); } }); } performQue(event, newIdList) { return __awaiter(this, void 0, void 0, function* () { const action = (0, types_1.getActionFromTrackIdentifier)(event.header); const id = event.body[this.config.idParamName]; switch (action) { case types_1.ModelEventAction.UPDATE: return this.onUpdate(id, event.body, newIdList); case types_1.ModelEventAction.DELETE: return { shouldUpdateIndexes: true }; default: return this.config.customTriggers[event.header].on({ getAll: this.config.getAll, getAllIds: this.config.getAllIds, onUpdate: this.onUpdate, modelState: this.modelState, }, event.body); } }); } onTrackEvent(header, body) { return __awaiter(this, void 0, void 0, function* () { this.pushToQueue({ header, body }); }); } findIndexDiff(existedNewIdList_1) { return __awaiter(this, arguments, void 0, function* (existedNewIdList, forceMetaUpgrade = false) { const currentIdList = this.getStateIdList(); const newIdList = existedNewIdList || (yield this.config.getAllIds()); const eventsSet = new Set(); const lookForData = (id) => __awaiter(this, void 0, void 0, function* () { const model = this.modelState.get(id); return model || this.config.getById(id); }); const updateIndexConfig = (id, index) => ({ modelName: this.config.trackModelName, idParamName: this.config.idParamName, action: types_1.ModelEventAction.UPDATE_INDEX, data: { id, index, }, }); const que = [ ...currentIdList .filter((id) => !newIdList.includes(id)) .map((id) => { eventsSet.add(types_1.ModelEventAction.DELETE); const deleteEvent = { modelName: this.config.trackModelName, idParamName: this.config.idParamName, action: types_1.ModelEventAction.DELETE, data: { id }, }; return deleteEvent; }), ...(yield Promise.all(newIdList.map((id, index) => __awaiter(this, void 0, void 0, function* () { if (id !== currentIdList[index]) { if (currentIdList.indexOf(id) !== -1) { // already exists, do index update only eventsSet.add(types_1.ModelEventAction.UPDATE_INDEX); return updateIndexConfig(id, index); } const data = yield lookForData(id); this.setModel(id, data, index); eventsSet.add(types_1.ModelEventAction.UPDATE); return { modelName: this.config.trackModelName, idParamName: this.config.idParamName, action: types_1.ModelEventAction.UPDATE, data: { id, data, index, updateStrategy: types_1.UpdateStrategy.REPLACE, }, }; } eventsSet.add(types_1.ModelEventAction.UPDATE_INDEX); return updateIndexConfig(id, index); })))), ...(yield this.updateMetaFields(forceMetaUpgrade ? false : [...eventsSet], false)), ]; // remove deleted models from state currentIdList .filter((id) => !newIdList.includes(id)) .forEach((id) => { this.deleteModel(id); }); if (que.length) { this.pushToSendQueue(...que); } }); } onUpdate(id, body, existedNewIdList) { return __awaiter(this, void 0, void 0, function* () { const newIdList = existedNewIdList || (yield this.config.getAllIds()); const modelHasLocal = this.modelState.has(id); const indexInNew = newIdList.indexOf(id); const updateStrategy = body.updateStrategy || "replace"; const dataFromBody = !!body.data; const data = body.data; if (indexInNew !== -1) { if (updateStrategy === types_1.UpdateStrategy.REPLACE) { this.setModel(id, dataFromBody ? yield this.config.sanitizeModel(data) : yield this.config.getById(id), indexInNew); } else if (updateStrategy === types_1.UpdateStrategy.MERGE && body.data && modelHasLocal) { this.setModel(id, yield this.config.sanitizeModel((0, lodash_merge_1.default)(this.modelState.get(id), data)), indexInNew); } this.pushToSendQueue({ modelName: this.config.trackModelName, idParamName: this.config.idParamName, action: types_1.ModelEventAction.UPDATE, data: { id, data: this.modelState.get(id), index: indexInNew, updateStrategy: types_1.UpdateStrategy.REPLACE, }, }); return { shouldUpdateIndexes: true, newIdList, __stage: 1 }; } if (modelHasLocal) { return { shouldUpdateIndexes: true, newIdList, __stage: 2 }; } return { shouldUpdateIndexes: false, newIdList, __stage: 3 }; }); } regenerateState() { return __awaiter(this, void 0, void 0, function* () { yield this.unsubscribe(); const currentIdList = this.getStateIdList(); this.modelState.clear(); this.modelStateIndexes = []; this.pushToSendQueue(...[ { modelName: this.config.trackModelName, idParamName: this.config.idParamName, action: types_1.ModelEventAction.MANUAL_UPDATE, data: { id: types_1.ManualUpdateTypes.REGENERATE_STATE, }, }, ...currentIdList.map((id) => { const deleteEvent = { modelName: this.config.trackModelName, idParamName: this.config.idParamName, action: types_1.ModelEventAction.DELETE, data: { id }, }; return deleteEvent; }), ]); yield this.init(); }); } regenerateIndexes() { return __awaiter(this, arguments, void 0, function* (forceMetaUpgrade = false) { this.pushToSendQueue({ modelName: this.config.trackModelName, idParamName: this.config.idParamName, action: types_1.ModelEventAction.MANUAL_UPDATE, data: { id: types_1.ManualUpdateTypes.REGENERATE_INDEXES, }, }); yield this.findIndexDiff(null, forceMetaUpgrade); }); } regenerateMeta() { return __awaiter(this, void 0, void 0, function* () { this.pushToSendQueue({ modelName: this.config.trackModelName, idParamName: this.config.idParamName, action: types_1.ModelEventAction.MANUAL_UPDATE, data: { id: types_1.ManualUpdateTypes.REGENERATE_METADATA, }, }); yield this.updateMetaFields(false); }); } unsubscribe() { return __awaiter(this, void 0, void 0, function* () { if (this.keepAliveTimeout) clearTimeout(this.keepAliveTimeout); yield Promise.all(this.trackIdentifiers.map((trackIdentifier) => this.config.removeTrack(trackIdentifier))); }); } } exports.ModelEventSubscriber = ModelEventSubscriber; //# sourceMappingURL=subscriber.js.map