@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
JavaScript
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
};