UNPKG

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
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