semantic-network
Version:
A utility library for manipulating a list of links that form a semantic interface to a network of resources.
384 lines • 19.9 kB
JavaScript
import { __rest } from "tslib";
import { instanceOfLinkedRepresentation, LinkUtil, } from 'semantic-link';
import { state } from '../types/types';
import { State } from './state';
import { Status } from './status';
import anylogger from 'anylogger';
import { instanceOfFeed } from '../utils/instanceOf/instanceOfFeed';
import { instanceOfCollection } from '../utils/instanceOf/instanceOfCollection';
import { LinkRelation } from '../linkRelation';
import { CanonicalOrSelf } from '../utils/comparators/canonicalOrSelf';
import { TrackedRepresentationUtil } from '../utils/trackedRepresentationUtil';
import { instanceOfTrackedRepresentation } from '../utils/instanceOf/instanceOfTrackedRepresentation';
const log = anylogger('SparseRepresentationFactory');
/**
* A factory for performing the initial construction of tracked resources.
*
* This factory is responsible for performing a logical 'new' of the in memory object
* used to represent resources. These objects have a 'state' object of type {@link State}
* that is used to track and manage the resources.
*
* If the across the wire representation is available it can be provided via
* the {@link ResourceFactoryOptions.addStateOn} property to provide an initial values
* for the resource. If the values are not provided then the resource is marked
* as being in the {@link Status.locationOnly} state (i.e. sparely populated).
*
* This factory is a pluggable strategy via the {@link ResourceFactoryOptions.makeSparseStrategy}. By
* default, the strategy is to create new instances for every instance of a resource. Implementations
* are provided to allowed pooled instances.
*/
export class SparseRepresentationFactory {
/**
* A simple facade to allow the make() method to be provided by an alternative strategy.
*
* @see {defaultMakeStrategy}
*/
static make(options) {
// Get the optional makeSparse strategy defaulting to the standard default implementation below.
const { makeSparseStrategy = SparseRepresentationFactory.defaultMakeStrategy } = Object.assign({}, options);
return makeSparseStrategy(options);
}
/**
* Returns a {@link LinkedRepresentation} with {@link State} initialised. An initialised representation will
* be at worst sparse with a state ({@link Status.locationOnly}, {@link Status.virtual}). At best, the representation
* is {@link Status.hydrated} when a resource is presented that has been retrieved across the wire.
*/
static defaultMakeStrategy(options) {
const _a = Object.assign({}, options), { addStateOn } = _a, opts = __rest(_a, ["addStateOn"]);
if (addStateOn) {
return SparseRepresentationFactory.makeHydrated(addStateOn, opts);
}
else {
return SparseRepresentationFactory.makeSparse(opts);
}
}
/**
* Create sparse items from a 'pool'. The pool is a single collection resource, which is used
* as both a source of items and a location to store new (sparse) items.
*
* This strategy allows the caching of resources in a memory conservative way so that the same
* resource is not loaded twice. More importantly this also means that if the application/user
* has a view of those resources then the view will be the same.
*
* It is assumed that the pooled collection is logically backed/represented by the set of all
* possible items, whereas the specific collection is a subset of those items.
*/
static pooledCollectionMakeStrategy(pool, options) {
const { addStateOn } = Object.assign({}, options);
if (addStateOn) {
return SparseRepresentationFactory.makeHydratedPoolCollection(addStateOn, pool, options);
}
else {
return SparseRepresentationFactory.makeSparse(Object.assign(Object.assign({}, options), { addStateOn: undefined }));
}
}
/**
* Find the first matching item in a collection. Match by URI.
*
* At this stage, it is really unlikely that this will ever match on eTag for identity at this point. ETag
* checking is later on in the pipeline (ie versioning is later)
*/
static firstMatchingFeedItem(collection, id) {
return collection
.items
.find((anItem) => LinkUtil.getUri(anItem, CanonicalOrSelf) === id);
}
/**
* Create sparse item from a 'pool'. The pool is a single collection resource, which is used
* as both a source of items and a location to store new (sparse) items.
*
* This strategy allows the caching of resources in a memory conservative way so that the same
* resource is not loaded twice. More importantly this also means that if the application/user
* has a view of those resources then the view will be the same.
*
* It is assumed that the pooled collection is logically backed/represented by the set of all
* possible items, whereas the specific collection is a subset of those items.
*/
static pooledSingletonMakeStrategy(pool, options) {
const _a = Object.assign({}, options), { addStateOn } = _a, opts = __rest(_a, ["addStateOn"]);
if (addStateOn) {
return SparseRepresentationFactory.makeHydratedPoolSingleton(addStateOn, pool, opts);
}
else {
return SparseRepresentationFactory.makeSparsePooled(pool, opts);
}
}
static makeHydrated(resource, options) {
const { sparseType = 'singleton', status = Status.hydrated, eTag = undefined, } = Object.assign({}, options);
if (sparseType === 'feed') {
throw new Error('Feed type not implemented. Sparse representation must be singleton or collection');
}
/*
* The passed in resource cannot be mutated because in libraries like Vue2 the bindings will be lost
*/
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
resource[state] = new State(status, eTag);
if (instanceOfCollection(resource)) {
// collection requires feed items to be sparsely populated
// should be able to know from feedOnly state
// TODO: need to know this coming out of load
if (instanceOfFeed(resource)) {
// make collection items
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
resource.items = resource
.items
.map(x => this.makeSparse(Object.assign(Object.assign({}, options), { sparseType: 'feed', feedItem: x })));
}
else {
/*
* Case where the resources already exist and state is to be added after it has been added in-memory
*/
for (const item of resource.items) {
if (instanceOfLinkedRepresentation(resource) && !instanceOfTrackedRepresentation(resource)) {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
item[state] = new State(status);
}
}
}
} // else singleton
return resource;
}
/**
* The resource is to be made as a sparse resource as there is no data provided via
* the {@link ResourceFactoryOptions.addStateOn} property.
*/
static makeSparse(options) {
const { uri = '', // rather than populate with undefined, default to empty string (unclear why this is a good idea)
title = undefined, updated = undefined, eTag = undefined, status = (options === null || options === void 0 ? void 0 : options.uri) ? Status.locationOnly : Status.virtual, mappedTitle = this.defaultMappedTitleAttributeName, mappedTitleFrom = this.defaultMappedFromFeedItemFieldName, mappedUpdated = this.defaultMappedUpdatedAttributeName, mappedUpdatedFrom = this.defaultMappedFromFeedItemUpdatedFieldName, mappedETagFrom = this.defaultMappedFromFeedItemETagFieldName, sparseType = 'singleton', } = Object.assign({}, options);
const sparseResource = {
[state]: new State(status, eTag, updated),
links: [{
rel: LinkRelation.Self,
href: uri,
}],
};
if (sparseType === 'singleton') {
// feed items come back in on a singleton and have the 'title' mapped to an attribute
// note: 'name' isn't likely to be configured but could be (it also could be injected from global configuration)
return Object.assign(Object.assign(Object.assign({}, sparseResource), (title && { [mappedTitle]: title })), (updated && { [mappedUpdated]: updated }));
}
else if (sparseType === 'collection') {
const { defaultItems = [] } = Object.assign({}, options);
const items = defaultItems.map(item => {
if (typeof item === 'string' /* Uri */) {
return this.makeSparse({ uri: item });
}
else {
return this.makeSparse({
uri: item.id,
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
title: item[mappedTitleFrom],
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
updated: item[mappedUpdatedFrom],
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
eTag: item[mappedETagFrom],
});
}
});
return Object.assign(Object.assign({}, sparseResource), { items });
}
else if (sparseType === 'feed') /* feedItem */ {
// note: sparseType: 'feed' is an internal type generated from {@link makeHydrated} to populate items
const { feedItem } = Object.assign({}, options);
if (feedItem) {
return this.makeSparse({
uri: feedItem.id,
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
title: feedItem[mappedTitleFrom],
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
updated: feedItem[mappedUpdatedFrom],
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
eTag: feedItem[mappedETagFrom],
});
}
else {
log.error('Cannot create resource of type \'feedItem\' should be set - returning unknown');
return this.makeSparse({ status: Status.unknown });
}
}
else {
log.error('Unsupported type %s', sparseType);
return this.makeSparse({ status: Status.unknown });
}
}
/**
* Make a collection (that is not pools) from members that are pools.
*/
static makeHydratedPoolCollection(resource, pool, options) {
const { sparseType = 'singleton', status = Status.hydrated, eTag = undefined, } = Object.assign({}, options);
if (sparseType === 'feed') {
throw new Error('Feed type not implemented. Sparse representation must be singleton or collection');
}
// make up a tracked resource for both singleton and collection (and forms)
// this will include links
const tracked = Object.assign(Object.assign({}, resource), { [state]: new State(status, eTag /* currently no last modified passed through */) });
if (instanceOfCollection(resource)) {
// collection requires feed items to be sparsely populated
// should be able to know from feedOnly state
const items = this.onAsFeedRepresentation(resource)
.items
.map(x => this.makePooledFeedItemResource(pool, x, options));
// Make the collection, with the pooled items
return Object.assign(Object.assign({}, tracked), { items: [...items] });
}
else { // the resource is a singleton (or a feed, ...)
// make a singleton (or form)
return tracked;
}
}
/**
* Make an item for a collection using a pool as the source for items.
*/
static makePooledFeedItemResource(pool, item, options) {
const firstMatchingItem = this.firstMatchingFeedItem(pool, item.id);
// when found by id merge across the title and the etag
// which will then make it stale, etc
if (firstMatchingItem) {
const { mappedTitleFrom = this.defaultMappedFromFeedItemFieldName, mappedETagFrom = this.defaultMappedFromFeedItemETagFieldName, mappedUpdatedFrom = this.defaultMappedFromFeedItemUpdatedFieldName, } = Object.assign({}, options);
const etag = item[mappedETagFrom];
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
const title = item[mappedTitleFrom];
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
const lastModified = item[mappedUpdatedFrom];
// const resourceFactoryOptions = { title, eTag: etag } as ResourceFactoryOptions;
return this.mergeFeedItem(firstMatchingItem, Object.assign(Object.assign({}, options), { title, eTag: etag, lastModified })); // item from the pool
}
else {
const newItem = this.makeSparse(Object.assign(Object.assign({}, options), { sparseType: 'feed', feedItem: item }));
pool.items.unshift(newItem); // put the item at the beginning of the pool
return newItem;
}
}
/**
* if an incoming feed item is already in the collection AND the incoming has the eTag as part of the feed
* then check if the existing item is stale (ie stale if eTags don't match and requires fetch across the wire)
*
* note: combined with {@link includeItems} items with eTag changes should be refreshed
*/
static mergeFeedItemETag(resource, options) {
const { eTag, lastModified } = Object.assign({}, options);
if (instanceOfTrackedRepresentation(resource)) {
// factory passed in eTag on newly created resources
if (eTag) {
const previousFeedETag = TrackedRepresentationUtil.getFeedETag(resource);
if (previousFeedETag !== eTag) {
const state = TrackedRepresentationUtil.getState(resource);
state.previousStatus = state.status;
state.status = Status.staleFromETag;
// update the feed item eTag with the new incoming—which mostly is the same but can be updated
TrackedRepresentationUtil.setFeedETag(resource, eTag, lastModified);
}
// look inside the resource that may have been already hydrated and has an eTag value in the headers
}
else if (TrackedRepresentationUtil.hasStaleFeedETag(resource)) {
const state = TrackedRepresentationUtil.getState(resource);
state.previousStatus = state.status;
state.status = Status.staleFromETag;
} // else eTags match, don't update
}
else {
log.error('Matched feed item in collection should already be a tracked resource. Developer error');
}
return resource;
}
/**
* The resource is to be made as a sparse resource as there is no data provided via
* the {@link ResourceFactoryOptions.addStateOn} property. Iff a link has been provided
* can the item be fetched from or stored into the pool.
*/
static makeSparsePooled(pool, options) {
const { uri } = Object.assign({}, options);
if (uri) {
const firstMatchingItem = this.firstMatchingFeedItem(pool, uri);
if (firstMatchingItem) {
return this.mergeFeedItem(firstMatchingItem, options); // item from the pool
}
else {
const sparse = this.makeSparse(options);
pool.items.unshift(sparse); // add item to the pool
return sparse;
}
}
else {
// the URI is not known, so return a 'new' sparse item and do not store it in the pool
return this.makeSparse(options); // not eligible for the pool
}
}
/**
* Make a collection (that is not pools) from members that are pools.
*/
static makeHydratedPoolSingleton(resource, pool, options) {
const uri = LinkUtil.getUri(resource, CanonicalOrSelf);
if (uri) {
const firstMatchingItem = this.firstMatchingFeedItem(pool, uri);
if (firstMatchingItem) {
return this.mergeFeedItem(firstMatchingItem, options); // item from the pool
}
else {
const hydrated = this.makeHydrated(resource, options);
pool.items.unshift(hydrated); // add item to the pool
return hydrated;
}
}
else {
return this.makeHydrated(resource, options);
}
}
static mergeFeedItem(resource, options) {
// incoming changes are merged onto the existing: name, title and eTags (which change state)
this.mergeFeedItemFields(resource, options);
return this.mergeFeedItemETag(resource, options);
}
/**
* Any resource requires incoming sparse representation options to be merged into existing resources. This method
* will follow any options override mappings
*
* Note: in practice, this means that incoming feed will be mapped back onto the UI with new feed titles/updatedAt
*/
static mergeFeedItemFields(resource, options) {
const { title = undefined, updated = undefined, mappedTitle = this.defaultMappedTitleAttributeName, mappedUpdated = this.defaultMappedUpdatedAttributeName, } = Object.assign({}, options);
if (title) {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
resource[mappedTitle] = title;
}
if (updated) {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
resource[mappedUpdated] = updated;
}
return resource;
}
/**
* Get the {@link ResourceFactoryOptions.addStateOn} data as a {@link FeedRepresentation}.
*/
static onAsFeedRepresentation(resource) {
if (instanceOfFeed(resource)) {
return resource;
}
else {
log.warn('Resource does not look like a feed');
return resource; // return it anyway.
}
}
}
SparseRepresentationFactory.defaultMappedTitleAttributeName = 'name';
SparseRepresentationFactory.defaultMappedFromFeedItemFieldName = 'title';
SparseRepresentationFactory.defaultMappedUpdatedAttributeName = 'updatedAt';
SparseRepresentationFactory.defaultMappedFromFeedItemUpdatedFieldName = 'updated';
SparseRepresentationFactory.defaultMappedFromFeedItemETagFieldName = 'eTag';
export const defaultMakeStrategy = SparseRepresentationFactory.defaultMakeStrategy;
export const pooledCollectionMakeStrategy = SparseRepresentationFactory.pooledCollectionMakeStrategy;
export const pooledSingletonMakeStrategy = SparseRepresentationFactory.pooledSingletonMakeStrategy;
//# sourceMappingURL=sparseRepresentationFactory.js.map