@node-elion/syncron
Version:
Provides a simple way to delivery models between sender and receiver
654 lines • 31.9 kB
JavaScript
"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