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

155 lines (154 loc) 6.07 kB
import createSignal from "./index16.mjs"; import ReplicatedCollection from "./index26.mjs"; //#region src/AutoFetchCollection.ts /** * A special collection that automatically fetches items when they are needed. */ var AutoFetchCollection = class extends ReplicatedCollection { activeObservers = /* @__PURE__ */ new Map(); observerTimeouts = /* @__PURE__ */ new Map(); purgeDelay; idQueryCache = /* @__PURE__ */ new Map(); itemsCache = /* @__PURE__ */ new Map(); fetchQueryItems; triggerReload = null; reactivityAdapter = null; loadingSignals = /* @__PURE__ */ new Map(); isFetchingSignal; mergeItems; /** * @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; } }); 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) 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"); this.triggerReload(); }).catch((error) => { this.emit("persistence.error", error); }).finally(() => { this.isFetchingSignal.set(false); }); } handleObserverCreation(selector) { const activeObservers = this.activeObservers.get(this.getKeyForSelector(selector))?.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) this.fetchSelector(selector); } handleObserverDisposal(selector) { const activeObservers = (this.activeObservers.get(this.getKeyForSelector(selector))?.count ?? 0) - 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"); 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) { this.ensureSignal(selector).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; return this.ensureSignal(selector).get() || isPushing; } }; //#endregion export { AutoFetchCollection as default };