UNPKG

stem-core

Version:

Frontend and core-library framework

339 lines (283 loc) 9.33 kB
import {Dispatchable, CleanupJobs} from "../base/Dispatcher"; import {DefaultState} from "./State"; // The store information is kept in a symbol, to not interfere with serialization/deserialization export const StoreSymbol = Symbol("Store"); export const EventDispatcherSymbol = Symbol("EventDispatcher"); class StoreObject extends Dispatchable { constructor(obj, event, store) { super(); Object.assign(this, obj); this.setStore(store); }; static getStoreName() { return this.name; } setStore(store) { this[StoreSymbol] = store; } getStore() { return this[StoreSymbol]; } // By default, applying an event just shallow copies the fields from event.data applyEvent(event) { Object.assign(this, event.data); }; // Add a listener for all updates, callback will receive the events after they were applied addUpdateListener(callback) { return this.addListener("update", callback); } addDeleteListener(callback) { return this.addListener("delete", callback); } // Add a listener on updates from events with this specific type. // Can accept an array as eventType // Returns an object that implements the Cleanup interface. addEventListener(eventType, callback) { if (Array.isArray(eventType)) { const handlers = eventType.map(e => this.addEventListener(e, callback)); return new CleanupJobs(handlers); } // Ensure the private event dispatcher exists if (!this[EventDispatcherSymbol]) { this[EventDispatcherSymbol] = new Dispatchable(); this.addUpdateListener((event) => { this[EventDispatcherSymbol].dispatch(event.type, event, this); }); } return this[EventDispatcherSymbol].addListener(eventType, callback); } getStreamName() { throw "getStreamName not implemented"; } // TODO: this should not be here by default registerToStream() { this.getStore().getState().registerStream(this.getStreamName()); } toJSON() { const obj = {}; for (const key in this) { if (this.hasOwnProperty(key)) { obj[key] = this[key]; } } return obj; } } class BaseStore extends Dispatchable { constructor(objectType, ObjectWrapper=StoreObject, options={}) { super(); this.options = options; this.objectType = objectType.toLowerCase(); this.ObjectWrapper = ObjectWrapper; this.attachToState(); } attachToState() { if (this.getState()) { this.getState().addStore(this); } } getState() { // Allow explicit no state if (this.options.hasOwnProperty("state")) { return this.options.state; } else { return DefaultState; } } // Is used by the state object to see which stores need to be loaded first getDependencies() { return this.options.dependencies || []; } } // Store type primarily intended to store objects that come from a server DB, and have a unique numeric .id field class GenericObjectStore extends BaseStore { objects = new Map(); has(id) { return !!this.get(id); } get(id) { if (id == null) { return null; } return this.objects.get(String(id)); } addObject(id, obj) { this.objects.set(String(id), obj); } clear() { this.objects.clear(); this.dispatch("update", null, null); } getObjectIdForEvent(event) { return String(event.objectId || event.data.id); } getObjectForEvent(event) { let objectId = this.getObjectIdForEvent(event); return this.get(objectId); } all(asIterable) { let values = this.objects.values(); if (!asIterable) { values = Array.from(values); } return values; } find(callback) { return this.all(true).find(callback); } toJSON() { return this.all().map(entry => entry.toJSON()); } createObject(event) { const obj = new this.ObjectWrapper(event.data, event, this); obj.setStore(this); return obj; } applyCreateEvent(event, sendDispatch=true) { let obj = this.getObjectForEvent(event); let dispatchType = "create"; if (obj) { let refreshEvent = Object.assign({}, event); dispatchType = "update"; refreshEvent.type = "refresh"; obj.applyEvent(refreshEvent); obj.dispatch("update", event); } else { obj = this.createObject(event); this.addObject(this.getObjectIdForEvent(event), obj); } if (sendDispatch) { this.dispatch(dispatchType, obj, event); } return obj; } applyUpdateOrCreateEvent(event) { let obj = this.getObjectForEvent(event); if (!obj) { obj = this.applyCreateEvent(event, false); this.dispatch("create", obj, event); } else { this.applyEventToObject(obj, event); } this.dispatch("updateOrCreate", obj, event); return obj; } applyDeleteEvent(event) { let objDeleted = this.getObjectForEvent(event); if (objDeleted) { this.objects.delete(this.getObjectIdForEvent(event)); objDeleted.dispatch("delete", event, objDeleted); this.dispatch("delete", objDeleted, event); } return objDeleted; } applyEventToObject(obj, event) { obj.applyEvent(event); obj.dispatch("update", event); this.dispatch("update", obj, event); return obj; } applyEvent(event) { event.data = event.data || {}; if (event.type === "create") { return this.applyCreateEvent(event); } else if (event.type === "delete") { return this.applyDeleteEvent(event); } else if (event.type === "updateOrCreate") { return this.applyUpdateOrCreateEvent(event); } else { var obj = this.getObjectForEvent(event); if (!obj) { console.error("I don't have object of type ", this.objectType, " ", event.objectId); return; } return this.applyEventToObject(obj, event); } } importState(objects) { objects = objects || []; for (let obj of objects) { this.fakeCreate(obj); } } // Create a fake creation event, to insert the raw object fakeCreate(obj, eventType="fakeCreate", dispatchEvent=true) { if (!obj) { return; } let event = { objectType: this.objectType, objectId: obj.id, type: eventType, data: obj, }; return this.applyCreateEvent(event, dispatchEvent); } // Add a listener on all object creation events // If fakeExisting, will also pass existing objects to your callback addCreateListener(callback, fakeExisting) { if (fakeExisting) { for (let object of this.objects.values()) { let event = { objectType: this.objectType, objectId: object.id, type: "fakeCreate", data: object, }; callback(object, event); } } return this.addListener("create", callback); } // Add a listener for any updates to objects in store // The callback will receive the object and the event addUpdateListener(callback) { return this.addListener("update", callback); } // Add a listener for any object deletions addDeleteListener(callback) { return this.addListener("delete", callback); } addChangeListener(callback) { return this.addListener(["create", "update", "delete"], callback); } } class SingletonStore extends BaseStore { constructor(objectType, options={}) { super(objectType, SingletonStore, options); } get() { return this; } all() { return [this]; } toJSON() { return JSON.stringify([this]); } applyEvent(event) { Object.assign(this, event.data); this.dispatch("update", event, this); } importState(obj) { Object.assign(this, obj); this.dispatch("update", obj, this); } addUpdateListener(callback) { return this.addListener("update", callback); } // Use the same logic as StoreObject when listening to events addEventListener = StoreObject.prototype.addEventListener.bind(this); } const ObjectStore = (objectType, ObjectWrapper, options={}) => class ObjectStore extends GenericObjectStore { constructor() { super(objectType, ObjectWrapper, options); } static getObjectType() { return objectType; } static getInstance(state=DefaultState) { return state.getStore(this.getObjectType()); } }; export {StoreObject, BaseStore, GenericObjectStore, SingletonStore, ObjectStore};