semantic-network
Version:
A utility library for manipulating a list of links that form a semantic interface to a network of resources.
222 lines (221 loc) • 11 kB
JavaScript
import { __asyncValues, __awaiter } from "tslib";
import anylogger from 'anylogger';
import { LinkUtil } from 'semantic-link';
import { LinkRelation } from '../linkRelation';
import { instanceOfCollection } from '../utils/instanceOf/instanceOfCollection';
import cloneDeep from 'lodash.clonedeep';
import { defaultEqualityOperators } from '../utils/comparators/defaultEqualityOperators';
const log = anylogger('test');
const noopResolver = () => __awaiter(void 0, void 0, void 0, function* () {
return undefined;
});
const noopVoidResolver = () => __awaiter(void 0, void 0, void 0, function* () {
});
export class Differencer {
/**
* A default set of comparisons made to check if two resource
* representation refer to the same resource in a collection.
*
* The most specific and robust equality check is first, with the most vague and
* optimistic last.
*/
static get defaultEqualityOperators() {
return defaultEqualityOperators;
}
/**
* Processes difference sets (create, update, delete) for between two client-side collections {@Link CollectionRepresentation}
*
* WARNING: this is a differencing set and update can be misleading. What is means is that we have 'matched' these
* two resource as both 'existing' and thus may or may not require some form of update on them. The decision on
* whether there is an actual difference is up to some other decision that know about the internals of the resource
* (such as an edit form merger).
*
* set one - current collection
* set two - new collection
* add/create - not in set one but in set two
* match/update - intersection of both sets (may or may not require a change)
* remove/delete - in set one and not in set two
*
* set one: current
* +-----------------------------+
* +-----------------------------------------+
* | | | |
* | add | match | remove |
* | | | |
* +-----------------------------------------+
* +--------------------------+
* set two: new
*
*
* @param resource an existing resource collection that is
* synchronised with the server (network of data).
*
* @param document a document with a collection CollectionRepresentation
* format that describes the state of the resources.
*
* @param options a document with a collection CollectionRepresentation
* format that describes the state of the resources.
*/
static difference(resource, document, options) {
var _a, e_1, _b, _c, _d, e_2, _e, _f, _g, e_3, _h, _j;
return __awaiter(this, void 0, void 0, function* () {
if (!instanceOfCollection(resource)) {
throw new Error(`[Differencer] collection resource '${LinkUtil.getUri(resource, LinkRelation.Self)}' has no items`);
}
if (!instanceOfCollection(document)) {
throw new Error(`[Differencer] collection document '${LinkUtil.getUri(document, LinkRelation.Self)}' has no items`);
}
const { createStrategy = noopResolver, updateStrategy = noopVoidResolver, deleteStrategy = noopResolver, } = Object.assign({}, options);
let { comparators = Differencer.defaultEqualityOperators, } = Object.assign({}, options);
// provide a default comparator and normalise a single comparator to an array of comparators
if (typeof comparators === 'function') {
comparators = [comparators];
}
/**
* tuple of collection item and and document item
*/
const updateItems = [];
// clone the items
const deleteItems = [...resource.items];
// this needs a deep copy otherwise recursive structures won't work
// TODO: node17 and later browsers allow 'structuredClone'
// @see https://developer.mozilla.org/en-US/docs/Web/API/structuredClone
const createItems = cloneDeep(document.items);
for (const comparator of comparators) {
// Get a list of items that need to be updated.
// create an array of indexes, eg
// if the first two match return [[0,0]]
const itemsToMove = deleteItems
.map((item, index) => {
const docIndex = createItems.findIndex(createItem => comparator(item, createItem));
return docIndex >= 0 ? { lIndex: index, rIndex: docIndex } : undefined;
})
.filter(item => !!item);
// Remove those items that are to be updated from the 'delete' list
// on any that are removed, add reference for later processing onto the pair
// if there is a match return [0,0,{item}]
itemsToMove
.sort((lVal, rVal) => lVal.lIndex - rVal.lIndex)
.reverse()
.forEach((pair) => {
const index = pair.lIndex;
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const [representation, _] = deleteItems.splice(index, 1);
pair.lVal = representation;
});
// Remove those items that are to be updated from the 'create' list
// on any that are removed, add reference for later processing onto the pair
itemsToMove
.sort((lVal, rVal) => lVal.rIndex - rVal.rIndex)
.reverse()
.forEach((pair) => {
const index = pair.rIndex;
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const [representation, _] = createItems.splice(index, 1);
pair.rVal = representation;
});
// Append to the 'update' list the items removed from the 'delete' and 'create' lists
const argArray = itemsToMove.map(item => ({ lVal: item.lVal, rVal: item.rVal }));
// TODO: use reduce
// eslint-disable-next-line prefer-spread
updateItems.push.apply(updateItems, argArray);
}
const { batchSize = undefined } = Object.assign({}, options);
//
// 1. Delete all resource first
//
log.debug('delete strategy sequential: count \'%s\'', deleteItems.length);
try {
for (var _k = true, deleteItems_1 = __asyncValues(deleteItems), deleteItems_1_1; deleteItems_1_1 = yield deleteItems_1.next(), _a = deleteItems_1_1.done, !_a; _k = true) {
_c = deleteItems_1_1.value;
_k = false;
const item = _c;
yield deleteStrategy(item);
}
}
catch (e_1_1) { e_1 = { error: e_1_1 }; }
finally {
try {
if (!_k && !_a && (_b = deleteItems_1.return)) yield _b.call(deleteItems_1);
}
finally { if (e_1) throw e_1.error; }
}
//
// 2. Then update the existing resources
//
if (batchSize === 0 || !batchSize) {
log.debug('update strategy parallel: count \'%s\'', updateItems.length);
yield Promise.all(updateItems.map((item) => __awaiter(this, void 0, void 0, function* () { return yield updateStrategy(item.lVal, item.rVal); })));
}
else {
log.debug('update strategy sequential: count \'%s\'', updateItems.length);
try {
for (var _l = true, updateItems_1 = __asyncValues(updateItems), updateItems_1_1; updateItems_1_1 = yield updateItems_1.next(), _d = updateItems_1_1.done, !_d; _l = true) {
_f = updateItems_1_1.value;
_l = false;
const item = _f;
yield updateStrategy(item.lVal, item.rVal);
}
}
catch (e_2_1) { e_2 = { error: e_2_1 }; }
finally {
try {
if (!_l && !_d && (_e = updateItems_1.return)) yield _e.call(updateItems_1);
}
finally { if (e_2) throw e_2.error; }
}
}
//
// 3. Then create the new resources
//
let createResults = [];
if (batchSize === 0 || !batchSize) {
log.debug('create strategy parallel: count \'%s\'', createItems.length);
createResults = yield Promise.all(createItems
.map((item) => __awaiter(this, void 0, void 0, function* () {
const resource = yield createStrategy(item);
return { lVal: item, rVal: resource };
})));
}
else {
log.debug('create strategy sequential: count \'%s\'', createItems.length);
try {
for (var _m = true, createItems_1 = __asyncValues(createItems), createItems_1_1; createItems_1_1 = yield createItems_1.next(), _g = createItems_1_1.done, !_g; _m = true) {
_j = createItems_1_1.value;
_m = false;
const item = _j;
const resource = yield createStrategy(item);
createResults.push({ lVal: item, rVal: resource });
}
}
catch (e_3_1) { e_3 = { error: e_3_1 }; }
finally {
try {
if (!_m && !_g && (_h = createItems_1.return)) yield _h.call(createItems_1);
}
finally { if (e_3) throw e_3.error; }
}
}
const infos = [
...createResults.map(({ lVal, rVal }) => ({
document: lVal,
resource: rVal,
action: 'create',
})),
...updateItems.map(({ lVal, rVal }) => ({
document: lVal,
resource: rVal,
action: 'update',
})),
];
log.debug('[add, matched, remove] (%s %s %s) on %s', createResults.length, updateItems.length, deleteItems.length, LinkUtil.getUri(resource, LinkRelation.Self));
return {
info: infos,
created: createResults,
updated: updateItems,
deleted: deleteItems,
};
});
}
}
//# sourceMappingURL=differencer.js.map