UNPKG

@signaldb/core

Version:

SignalDB is a client-side database that provides a simple MongoDB-like interface to the data with first-class typescript support to achieve an optimistic UI. Data persistence can be achieved by using storage providers that store the data through a JSON in

174 lines (173 loc) 7.16 kB
var __defProp = Object.defineProperty; var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value; var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value); import ReplicatedCollection from "./index20.mjs"; import createSignal from "./index17.mjs"; class AutoFetchCollection extends ReplicatedCollection { /** * @param options {Object} - Options for the collection. * @param options.fetchQueryItems {Function} - A function that fetches items from the server. It takes the selector as an argument and returns a promise that resolves to an object with an `items` property. * @param options.purgeDelay {Number} - The delay in milliseconds before purging an item from the cache. */ constructor(options) { let triggerRemoteChange; super({ ...options, pull: () => Promise.resolve({ items: [...this.itemsCache.values()].reduce((memo, items) => { const newItems = [...memo]; items.forEach((item) => { const index = newItems.findIndex((i) => i.id === item.id); if (index === -1) { newItems.push(item); return; } newItems[index] = this.mergeItems(newItems[index], item); }); return newItems; }, []) }), registerRemoteChange: async (onChange) => { triggerRemoteChange = onChange; } }); __publicField(this, "activeObservers", /* @__PURE__ */ new Map()); __publicField(this, "observerTimeouts", /* @__PURE__ */ new Map()); __publicField(this, "purgeDelay"); __publicField(this, "idQueryCache", /* @__PURE__ */ new Map()); __publicField(this, "itemsCache", /* @__PURE__ */ new Map()); __publicField(this, "fetchQueryItems"); __publicField(this, "triggerReload", null); __publicField(this, "reactivityAdapter", null); __publicField(this, "loadingSignals", /* @__PURE__ */ new Map()); __publicField(this, "isFetchingSignal"); __publicField(this, "mergeItems"); this.mergeItems = options.mergeItems ?? ((itemA, itemB) => ({ ...itemA, ...itemB })); this.purgeDelay = options.purgeDelay ?? 1e4; this.isFetchingSignal = createSignal(options.reactivity, false); if (!triggerRemoteChange) throw new Error("No triggerRemoteChange method found. Looks like your persistence adapter was not registered"); this.triggerReload = triggerRemoteChange; this.reactivityAdapter = options.reactivity ?? null; this.fetchQueryItems = options.fetchQueryItems; this.on("observer.created", (selector) => this.handleObserverCreation(selector ?? {})); this.on("observer.disposed", (selector) => setTimeout(() => this.handleObserverDisposal(selector ?? {}), 100)); if (options.registerRemoteChange) { void options.registerRemoteChange(() => this.forceRefetch()); } } /** * Registers a query manually that items should be fetched for it * @param selector {Object} Selector of the query */ registerQuery(selector) { this.handleObserverCreation(selector); } /** * Unregisters a query manually that items are not fetched anymore for it * @param selector {Object} Selector of the query */ unregisterQuery(selector) { this.handleObserverDisposal(selector); } getKeyForSelector(selector) { return JSON.stringify(selector); } async forceRefetch() { return Promise.all([...this.activeObservers.values()].map(({ selector }) => this.fetchSelector(selector))).then(() => { }); } fetchSelector(selector) { this.isFetchingSignal.set(true); return this.fetchQueryItems(selector).then((response) => { if (!response.items) throw new Error("AutoFetchCollection currently only works with a full item response"); this.itemsCache.set(this.getKeyForSelector(selector), response.items); response.items.forEach((item) => { const queries = this.idQueryCache.get(item.id) ?? []; queries.push(selector); this.idQueryCache.set(item.id, queries); }); this.setLoading(selector, true); this.once("persistence.received", () => { this.setLoading(selector, false); }); if (!this.triggerReload) throw new Error("No triggerReload method found. Looks like your persistence adapter was not registered"); void this.triggerReload(); }).catch((error) => { this.emit("persistence.error", error); }).finally(() => { this.isFetchingSignal.set(false); }); } handleObserverCreation(selector) { var _a; const activeObservers = ((_a = this.activeObservers.get(this.getKeyForSelector(selector))) == null ? void 0 : _a.count) ?? 0; this.activeObservers.set(this.getKeyForSelector(selector), { selector, count: activeObservers + 1 }); const timeout = this.observerTimeouts.get(this.getKeyForSelector(selector)); if (timeout) clearTimeout(timeout); if (activeObservers === 0) void this.fetchSelector(selector); } handleObserverDisposal(selector) { var _a; const currentObservers = ((_a = this.activeObservers.get(this.getKeyForSelector(selector))) == null ? void 0 : _a.count) ?? 0; const activeObservers = currentObservers - 1; if (activeObservers > 0) { this.activeObservers.set(this.getKeyForSelector(selector), { selector, count: activeObservers }); return; } const timeout = this.observerTimeouts.get(this.getKeyForSelector(selector)); if (timeout) clearTimeout(timeout); const removeObserver = () => { this.activeObservers.delete(this.getKeyForSelector(selector)); this.itemsCache.delete(this.getKeyForSelector(selector)); if (!this.triggerReload) throw new Error("No triggerReload method found. Looks like your persistence adapter was not registered"); void this.triggerReload(); }; if (this.purgeDelay === 0) { removeObserver(); return; } this.observerTimeouts.set(this.getKeyForSelector(selector), setTimeout(removeObserver, this.purgeDelay)); } ensureSignal(selector) { if (!this.reactivityAdapter) throw new Error("No reactivity adapter found"); if (!this.loadingSignals.has(this.getKeyForSelector(selector))) { this.loadingSignals.set(this.getKeyForSelector(selector), createSignal(this.reactivityAdapter, false)); } return this.loadingSignals.get(this.getKeyForSelector(selector)); } setLoading(selector, value) { const signal = this.ensureSignal(selector); signal.set(value); } /** * Indicates wether a query is currently been loaded * ⚡️ this function is reactive! * @param selector {Object} Selector of the query * @returns The loading state */ isLoading(selector) { const isPushing = this.isPushing(); if (!selector) { return this.isFetchingSignal.get() || isPushing; } const signal = this.ensureSignal(selector); return signal.get() || isPushing; } } export { AutoFetchCollection as default };