UNPKG

@hf-chimera/store

Version:

Cross-end reactivity API

1,476 lines (1,463 loc) 57.7 kB
import { D as ChimeraInternalError, E as ChimeraError, S as ChimeraQueryUnsuccessfulDeletionError, _ as ChimeraQueryNotReadyError, a as chimeraDefaultFilterConfig, b as ChimeraQueryTrustFetchedCollectionError, d as ChimeraQueryDeletedItemError, f as ChimeraQueryDeletingError, g as ChimeraQueryNotCreatedError, h as ChimeraQueryIdMismatchError, i as chimeraDefaultOrderConfig, l as chimeraDefaultDebugConfig, m as ChimeraQueryFetchingError, t as chimeraDefaultQueryConfig, u as ChimeraQueryAlreadyRunningError, x as ChimeraQueryTrustIdMismatchError } from "./defaults-CLUQg2zK.js"; //#region src/filter/errors.ts var ChimeraFilterError = class extends ChimeraError {}; var ChimeraFilterOperatorError = class extends ChimeraFilterError { constructor(operator, message) { super(`Operator "${operator}" ${message}`); } }; var ChimeraFilterOperatorNotFoundError = class extends ChimeraFilterOperatorError { constructor(operator) { super(operator, "not found"); } }; //#endregion //#region src/shared/shared.ts const deepObjectAssign = (dst, srcObj, visited = /* @__PURE__ */ new WeakSet()) => { for (const { 0: key, 1: srcVal } of Object.entries(srcObj)) { if (srcVal === null || typeof srcVal !== "object" || Array.isArray(srcVal)) { dst[key] = srcVal; continue; } if (visited.has(srcVal)) { dst[key] = srcVal; continue; } visited.add(srcVal); const destVal = dst[key]; dst[key] = destVal === null || typeof destVal !== "object" || Array.isArray(destVal) ? {} : destVal; deepObjectAssign(dst[key], srcVal, visited); visited.delete(srcVal); } return dst; }; const deepObjectFreeze = (obj, frozenObjects = /* @__PURE__ */ new WeakSet()) => { if (obj === null || typeof obj !== "object" || Object.isFrozen(obj) || frozenObjects.has(obj)) return obj; frozenObjects.add(obj); for (const value of Object.values(obj)) if (value && typeof value === "object") deepObjectFreeze(value, frozenObjects); return Object.freeze(obj); }; const TypedArray = Object.getPrototypeOf(Int8Array); const deepObjectClone = (value, refs) => { if (value === null) return null; if (value === void 0) return void 0; if (typeof value !== "object") return value; if (refs) { const ref = refs.get(value); if (ref !== void 0) return ref; } if (value.constructor === Object) { const keys$1 = Object.keys(value).concat(Object.getOwnPropertySymbols(value)); const length$1 = keys$1.length; const clone$1 = {}; refs ??= /* @__PURE__ */ new Map(); refs.set(value, clone$1); for (let i = 0; i < length$1; i++) clone$1[keys$1[i]] = deepObjectClone(value[keys$1[i]], refs); return clone$1; } if (Array.isArray(value)) { const length$1 = value.length; const clone$1 = new Array(length$1); refs ??= /* @__PURE__ */ new Map(); refs.set(value, clone$1); for (let i = 0; i < length$1; i++) clone$1[i] = deepObjectClone(value[i], refs); return clone$1; } if (value instanceof Date) return new value.constructor(value.valueOf()); if (value instanceof RegExp) return value.constructor; if (value instanceof Map) { const clone$1 = new value.constructor(); refs ??= /* @__PURE__ */ new Map(); refs.set(value, clone$1); for (const entry of value.entries()) clone$1.set(entry[0], deepObjectClone(entry[1], refs)); return clone$1; } if (value instanceof Set) { const clone$1 = new value.constructor(); refs ??= /* @__PURE__ */ new Map(); refs.set(value, clone$1); for (const entry of value.values()) clone$1.add(deepObjectClone(entry, refs)); return clone$1; } if (value instanceof Error) { const clone$1 = new value.constructor(value.message); const keys$1 = Object.keys(value).concat(Object.getOwnPropertySymbols(value)); const length$1 = keys$1.length; refs ??= /* @__PURE__ */ new Map(); refs.set(value, clone$1); for (let i = 0; i < length$1; i++) clone$1[keys$1[i]] = deepObjectClone(value[keys$1[i]], refs); return clone$1; } if (value instanceof ArrayBuffer) return value.slice(); if (value instanceof TypedArray) return value.slice(); if (value instanceof DataView) return new DataView(value.buffer.slice()); if (value instanceof WeakMap) return value; if (value instanceof WeakSet) return value; const clone = Object.create(value.constructor.prototype); const keys = Object.keys(value).concat(Object.getOwnPropertySymbols(value)); const length = keys.length; refs ??= /* @__PURE__ */ new Map(); refs.set(value, clone); for (let i = 0; i < length; i++) clone[keys[i]] = deepObjectClone(value[keys[i]], refs); return clone; }; const compilePropertyGetter = ({ get }) => typeof get === "function" ? get : (e) => e[get]; const simplifyPropertyGetter = ({ key }) => key; const makeCancellablePromise = (promise, controller = new AbortController()) => { const signal = controller.signal; const newPromise = promise.then((v) => signal.aborted ? new Promise(() => null) : v, (err) => { return signal.aborted ? new Promise(() => null) : Promise.reject(err); }); newPromise.cancel = () => controller.abort(); newPromise.cancelled = (cb) => signal.aborted ? queueMicrotask(cb) : signal.addEventListener("abort", cb); if ("cancelled" in promise) { promise.cancelled(() => newPromise.cancel()); controller.signal.addEventListener("abort", () => promise.cancel()); } return newPromise; }; //#endregion //#region src/filter/constants.ts const ChimeraOperatorSymbol = Symbol("ChimeraOperatorSymbol"); const ChimeraConjunctionSymbol = Symbol("ChimeraConjunctionSymbol"); //#endregion //#region src/filter/filter.ts const filterConjunctions = { and: (operations) => operations.every((op) => op()), not: (operations) => !operations.every((op) => op()), or: (operations) => operations.some((op) => op()) }; const compileOperator = (config, { op, value, test }) => { const operatorFunc = config.operators[op]; if (!operatorFunc) throw new ChimeraFilterOperatorNotFoundError(op); const getter = compilePropertyGetter(value); return (entity) => operatorFunc(getter(entity), test); }; const compileConjunction = (config, { kind, operations }) => { const conjunction = filterConjunctions[kind]; const compiledOperations = operations.map((operation) => { switch (operation.type) { case ChimeraOperatorSymbol: return compileOperator(config, operation); case ChimeraConjunctionSymbol: return compileConjunction(config, operation); default: throw new ChimeraInternalError(`Invalid filter operation ${operation.type}`); } }).filter(Boolean); return (entity) => conjunction(compiledOperations.map((op) => () => op(entity))); }; const simplifyOperator = ({ op, value, test }) => ({ key: simplifyPropertyGetter(value), op, test, type: ChimeraOperatorSymbol }); const compareSimplifiedOperator = (a, b) => a.key.localeCompare(b.key) || a.op.localeCompare(b.op) || JSON.stringify(a.test).localeCompare(JSON.stringify(b.test)); const compareSimplifiedOperation = (a, b) => { if (a.type !== b.type) return a.type === ChimeraOperatorSymbol ? -1 : 1; if (a.type === ChimeraOperatorSymbol && b.type === ChimeraOperatorSymbol) return compareSimplifiedOperator(a, b); if (a.type === ChimeraConjunctionSymbol && b.type === ChimeraConjunctionSymbol) return compareSimplifiedConjunction(a, b); return 0; }; const compareSimplifiedConjunction = (a, b) => { const kindCompare = a.kind.localeCompare(b.kind); if (kindCompare !== 0) return kindCompare; const aOps = a.operations; const bOps = b.operations; const minLength = Math.min(aOps.length, bOps.length); for (let i = 0; i < minLength; i++) { const aOp = aOps[i]; const bOp = bOps[i]; if (aOp && bOp) { const compare = compareSimplifiedOperation(aOp, bOp); if (compare !== 0) return compare; } } return aOps.length - bOps.length; }; const simplifyConjunction = ({ kind, operations }) => { return { kind, operations: operations.map((op) => { switch (op.type) { case ChimeraOperatorSymbol: return simplifyOperator(op); case ChimeraConjunctionSymbol: return simplifyConjunction(op); default: throw new ChimeraInternalError(`Invalid filter operation ${op.type}`); } }).filter(Boolean).sort((a, b) => compareSimplifiedOperation(a, b)), type: ChimeraConjunctionSymbol }; }; const chimeraCreateOperator = (op, value, test) => ({ op, test, type: ChimeraOperatorSymbol, value: typeof value === "string" ? { get: value, key: value } : value }); const chimeraCreateConjunction = (kind, operations) => ({ kind, operations, type: ChimeraConjunctionSymbol }); const chimeraCreateNot = (operation) => ({ kind: "not", operations: [operation], type: ChimeraConjunctionSymbol }); function chimeraIsConjunction(item) { return item.type === ChimeraConjunctionSymbol; } function chimeraIsOperator(item) { return item.type === ChimeraOperatorSymbol; } const compileFilter = (config, descriptor) => descriptor ? compileConjunction(config, descriptor) : () => true; const simplifyFilter = (descriptor) => descriptor ? simplifyConjunction(descriptor) : null; const isOperationSubset = (candidateOp, targetOp, getOperatorKey) => { if (candidateOp.type !== targetOp.type) return false; if (candidateOp.type === ChimeraOperatorSymbol && targetOp.type === ChimeraOperatorSymbol) return candidateOp.key === targetOp.key && candidateOp.op === targetOp.op && getOperatorKey(candidateOp) === getOperatorKey(targetOp); if (candidateOp.type === ChimeraConjunctionSymbol && targetOp.type === ChimeraConjunctionSymbol) return isConjunctionSubset(candidateOp, targetOp, getOperatorKey); return false; }; const isConjunctionSubset = (candidate, target, getOperatorKey) => { if (candidate.kind !== target.kind) return false; switch (candidate.kind) { case "and": case "not": return candidate.operations.every((candidateOp) => target.operations.some((targetOp) => isOperationSubset(candidateOp, targetOp, getOperatorKey))); case "or": return target.operations.every((targetOp) => candidate.operations.some((candidateOp) => isOperationSubset(candidateOp, targetOp, getOperatorKey))); } }; const isFilterSubset = (candidate, target, getOperatorKey) => { if (candidate === null) return true; if (target === null) return false; return isConjunctionSubset(candidate, target, getOperatorKey); }; //#endregion //#region src/order/types.ts let ChimeraOrderNulls = /* @__PURE__ */ function(ChimeraOrderNulls$1) { ChimeraOrderNulls$1["First"] = "first"; ChimeraOrderNulls$1["Last"] = "last"; return ChimeraOrderNulls$1; }({}); //#endregion //#region src/order/order.ts const compileOrderDescriptor = ({ key, desc, nulls }) => ({ desc, get: compilePropertyGetter(key), nulls }); const chimeraCreateOrderBy = (key, desc = false, nulls = ChimeraOrderNulls.Last) => ({ desc, key: typeof key === "string" ? { get: key, key } : key, nulls }); const nullsComparator = (a, b, nulls) => { return a == b ? 0 : (a == null ? -1 : 1) * (nulls === ChimeraOrderNulls.First ? 1 : -1); }; const buildComparator = (comparator, orderBy) => { if (!orderBy) return () => 0; const compiledPriority = orderBy.map((ob) => compileOrderDescriptor(ob)); return (a, b) => { let result = 0; for (const descriptor of compiledPriority) { const vA = descriptor.get(a); const vB = descriptor.get(b); if (vA == null || vB == null) { result = nullsComparator(vA, vB, descriptor.nulls); if (result) break; continue; } result = comparator(descriptor.get(a), descriptor.get(b)); descriptor.desc && (result *= -1); if (result) break; } return result; }; }; const simplifyOrderBy = (orderBy) => orderBy ? orderBy.map((ob) => ({ desc: ob.desc, field: ob.key.key, nulls: ob.nulls })) : null; //#endregion //#region src/shared/ChimeraEventEmitter/ChimeraEventEmitter.ts var Events = function Events$1() {}; Events.prototype = Object.create(null); var ChimeraEventEmitter = class { _events; _eventsCount; constructor() { this._events = new Events(); this._eventsCount = 0; } #addListener(event, fn, once) { var listener = { fn, once }; if (!this._events[event]) { this._events[event] = listener; this._eventsCount++; } else if (!this._events[event].fn) this._events[event].push(listener); else this._events[event] = [this._events[event], listener]; return this; } #clearEvent(event) { if (--this._eventsCount === 0) this._events = new Events(); else delete this._events[event]; } eventNames() { return Object.keys(this._events); } listeners(event) { var handlers = this._events[event]; if (!handlers) return []; if (handlers.fn) return [handlers.fn]; for (var i = 0, l = handlers.length, ee = new Array(l); i < l; i++) ee[i] = handlers[i].fn; return ee; } listenerCount(event) { var listeners = this._events[event]; if (!listeners) return 0; if (listeners.fn) return 1; return listeners.length; } removeListener(event, fn, once) { if (!this._events[event]) return this; if (!fn) { this.#clearEvent(event); return this; } var listeners = this._events[event]; if (listeners.fn) { if (listeners.fn === fn && (!once || listeners.once)) this.#clearEvent(event); } else { for (var i = 0, events = [], length = listeners.length; i < length; i++) if (listeners[i].fn !== fn || once && !listeners[i].once) events.push(listeners[i]); if (events.length) this._events[event] = events.length === 1 ? events[0] : events; else this.#clearEvent(event); } return this; } emit(event, arg) { if (!this._events[event]) return false; var listeners = this._events[event]; if (listeners.fn) { if (listeners.once) this.removeListener(event, listeners.fn, true); listeners.fn.call(this, arg); } else for (var i = 0, length = listeners.length; i < length; i++) { if (listeners[i].once) this.removeListener(event, listeners[i].fn, true); listeners[i].fn.call(this, arg); } return true; } on(event, fn) { return this.#addListener(event, fn, false); } once(event, fn) { return this.#addListener(event, fn, true); } removeAllListeners(event) { if (event) { if (this._events[event]) this.#clearEvent(event); } else { this._events = new Events(); this._eventsCount = 0; } return this; } off = this.removeListener; addListener = this.on; }; //#endregion //#region src/query/types.ts let ChimeraQueryFetchingState = /* @__PURE__ */ function(ChimeraQueryFetchingState$1) { /** Query just initialized. */ ChimeraQueryFetchingState$1["Initialized"] = "initialized"; /** Not used yet. */ ChimeraQueryFetchingState$1["Scheduled"] = "scheduled"; /** Fetching in progress. */ ChimeraQueryFetchingState$1["Fetching"] = "fetching"; /** Creating in progress */ ChimeraQueryFetchingState$1["Creating"] = "creating"; /** Updating in progress. */ ChimeraQueryFetchingState$1["Updating"] = "updating"; /** Deleting in progress. */ ChimeraQueryFetchingState$1["Deleting"] = "deleting"; /** Fetch requested after reaching the Fetched, Errored, or Prefetched states. */ ChimeraQueryFetchingState$1["Refetching"] = "refetching"; /** Data retrieved from existing queries without initiating a fetch. */ ChimeraQueryFetchingState$1["Prefetched"] = "prefetched"; /** Fetch ended successfully; data is ready for use. */ ChimeraQueryFetchingState$1["Fetched"] = "fetched"; /** Fetch ended with an error; no data is available. */ ChimeraQueryFetchingState$1["Errored"] = "errored"; /** Refetch ended with an error, but old data is still available. */ ChimeraQueryFetchingState$1["ReErrored"] = "reErrored"; /** * Only for the item query, data is deleted, but the local value is still present, * no longer allows updates, but `refetch` still works (in case of strange errors, allows recovering state) */ ChimeraQueryFetchingState$1["Deleted"] = "deleted"; /** Only for the item query, data was actualized from an external event */ ChimeraQueryFetchingState$1["Actualized"] = "actualized"; return ChimeraQueryFetchingState$1; }({}); //#endregion //#region src/query/constants.ts const ChimeraGetParamsSym = Symbol("ChimeraGetParamsSym"); const ChimeraSetOneSym = Symbol("ChimeraSetOneSym"); const ChimeraSetManySym = Symbol("ChimeraSetManySym"); const ChimeraDeleteOneSym = Symbol("ChimeraDeleteOneSym"); const ChimeraDeleteManySym = Symbol("ChimeraDeleteManySym"); const ChimeraUpdateMixedSym = Symbol("ChimeraUpdateMixedSym"); const IN_PROGRESS_STATES = [ ChimeraQueryFetchingState.Scheduled, ChimeraQueryFetchingState.Creating, ChimeraQueryFetchingState.Fetching, ChimeraQueryFetchingState.Refetching, ChimeraQueryFetchingState.Updating, ChimeraQueryFetchingState.Deleting ]; //#endregion //#region src/query/ChimeraCollectionQuery.ts var ChimeraCollectionQuery = class extends ChimeraEventEmitter { #state; #promise; #lastError; #items; #config; #idGetter; #params; #order; #filter; #emit(event, arg) { queueMicrotask(() => super.emit(event, arg)); } emit() { throw new ChimeraInternalError("External events dispatching is not supported."); } #prepareRequestParams() { return { controller: new AbortController() }; } #readyItems(internalMessage) { if (this.#items) return this.#items; throw internalMessage ? new ChimeraInternalError(internalMessage) : new ChimeraQueryNotReadyError(this.#config.name); } #addItem(item) { const items = this.#readyItems("Trying to update not ready collection"); const foundIndex = items.findIndex((el) => this.#order(el, item) > 0); items.splice(foundIndex !== -1 ? foundIndex : items.length, 0, item); this.#emit("itemAdded", { instance: this, item }); } #setItems(items) { !this.#items && this.#emit("ready", { instance: this }); const oldItems = this.#items; this.#items = items; this.#emit("updated", { instance: this, items, oldItems }); } #setNewItems(items) { items.forEach((i) => void deepObjectFreeze(i)); this.#emit("selfUpdated", { instance: this, items, oldItems: this.#items }); this.#setItems(items); } #setPromise(promise) { this.#promise?.cancel(); this.#promise = promise; return promise; } #deleteAtIndex(index) { if (index === -1) return; const { 0: old } = this.#readyItems("Trying to update not ready collection").splice(index, 1); this.#emit("itemDeleted", { instance: this, item: old }); } #deleteItem(item) { this.#deleteAtIndex(this.#readyItems("Trying to update not ready collection").indexOf(item)); } #deleteById(id) { this.#deleteAtIndex(this.#readyItems("Trying to update not ready collection").findIndex((item) => this.#idGetter(item) === id)); } #replaceItem(oldItem, newItem) { const items = this.#readyItems("Trying to update not ready collection"); const index = items.indexOf(oldItem); const old = items[index]; items[index] = newItem; this.#emit("itemUpdated", { instance: this, newItem, oldItem: old }); } #getById(id) { return this.#readyItems("Trying to update not ready collection").find((item) => this.#idGetter(item) === id); } #setOne(item) { const existingItem = this.#getById(this.#idGetter(item)); const nowMatches = this.#filter(item); if (!(nowMatches || existingItem)) return; if (existingItem) { if (this.#order(existingItem, item) === 0) { this.#replaceItem(existingItem, item); return; } this.#deleteItem(existingItem); } nowMatches && this.#addItem(item); } #setNewOne(item) { deepObjectFreeze(item); this.#setOne(item); } #apply(input) { return input.filter((item) => this.#filter(item)).sort((a, b) => this.#order(a, b)); } #validate(input) { if (this.#config.trustQuery && !this.#config.devMode) return input; const prepared = this.#apply(input); if (!this.#config.trustQuery) return prepared; if (this.#config.devMode) { for (let i = 0; i < input.length; i++) if (input[i] !== prepared[i]) { console.warn(new ChimeraQueryTrustFetchedCollectionError(this.#config.name, input, prepared)); break; } } return input; } #setError(error, source) { this.#state = this.#items ? ChimeraQueryFetchingState.ReErrored : ChimeraQueryFetchingState.Errored; this.#lastError = error; this.#emit("error", { error, instance: this }); throw source; } #watchPromise(promise, controller) { return makeCancellablePromise(promise.then((response) => { this.#setNewItems(this.#validate(response.data)); this.#state = ChimeraQueryFetchingState.Fetched; return response; }).catch((error) => this.#setError(error, new ChimeraQueryFetchingError(this.#config.name, error))), controller); } constructor(config, params, existingItems, order, filter, alreadyValid) { super(); this.#config = config; this.#params = params; this.#promise = null; this.#items = null; this.#state = ChimeraQueryFetchingState.Initialized; this.#idGetter = config.idGetter; this.#filter = filter; this.#order = order; if (existingItems) { const input = Array.from(existingItems); this.#setItems(alreadyValid ? this.#validate(input) : this.#apply(input)); this.#state = ChimeraQueryFetchingState.Prefetched; } else { this.#state = ChimeraQueryFetchingState.Fetching; const { controller } = this.#prepareRequestParams(); this.#setPromise(this.#watchPromise(makeCancellablePromise(config.collectionFetcher(params, { signal: controller.signal }), controller), controller)); } this.#emit("initialized", { instance: this }); } get [ChimeraGetParamsSym]() { return this.#params; } [ChimeraSetOneSym](item) { this.#items && this.#setOne(item); } [ChimeraDeleteOneSym](id) { this.#items && this.#deleteById(id); } [ChimeraSetManySym](items) { if (this.#items) for (const item of items) this.#setOne(item); } [ChimeraDeleteManySym](ids) { if (this.#items) for (const id of ids) this.#deleteById(id); } [ChimeraUpdateMixedSym](toAdd, toDelete) { if (this.#items) { for (const id of toDelete) this.#deleteById(id); for (const item of toAdd) this.#setOne(item); } } get state() { return this.#state; } get inProgress() { return IN_PROGRESS_STATES.includes(this.#state); } get ready() { return this.#items !== null; } get lastError() { return this.#lastError; } /** * Wait for the current progress process to complete (both success or error) */ get progress() { return new Promise((res) => { const resolve = () => queueMicrotask(() => res()); if (this.#promise) { this.#promise.then(resolve, resolve); this.#promise.cancelled(() => this.progress.then(resolve, resolve)); } else resolve(); }); } /** * Wait for the current progress process to complete, throw an error if it fails */ get result() { return new Promise((res, rej) => { const resolve = () => queueMicrotask(() => res()); if (this.#promise) { this.#promise.then(resolve, rej); this.#promise.cancelled(() => this.#promise ? this.result.then(res, rej) : rej("cancelled")); } else resolve(); }); } /** Return an item if it is ready, throw error otherwise */ getById(id) { return this.#readyItems().find((item) => this.#idGetter(item) === id); } /** Return mutable ref to item by idx if it is ready, throw error otherwise */ mutableAt(idx) { return deepObjectClone(this.#readyItems().at(idx)); } /** Return mutable ref to item by [id] if it is ready, throw error otherwise */ mutableGetById(id) { return deepObjectClone(this.#readyItems().find((item) => this.#idGetter(item) === id)); } /** * Trigger refetch, return existing refetch promise if already running * @param force If true cancels any running process and starts a new one * @throws {ChimeraQueryAlreadyRunningError} If deleting or updating already in progress */ refetch(force = false) { if (!force && this.#promise && [ChimeraQueryFetchingState.Fetching, ChimeraQueryFetchingState.Refetching].includes(this.#state)) return this.#promise; if (!force && [ChimeraQueryFetchingState.Updating, ChimeraQueryFetchingState.Deleting].includes(this.#state)) throw new ChimeraQueryAlreadyRunningError(this.#config.name, this.#state); this.#state = ChimeraQueryFetchingState.Refetching; const { controller } = this.#prepareRequestParams(); return this.#setPromise(this.#watchPromise(makeCancellablePromise(this.#config.collectionFetcher(this.#params, { signal: controller.signal }), controller), controller)); } /** * Update item using updated copy * @param newItem new item to update */ update(newItem) { const { controller } = this.#prepareRequestParams(); return this.#config.itemUpdater(newItem, { signal: controller.signal }).then((response) => { const { data } = response; this.#items && this.#setNewOne(data); this.#emit("selfItemUpdated", { instance: this, item: data }); return response; }); } /** * Update item using updated copy * @param newItems array of items to update */ batchedUpdate(newItems) { const { controller } = this.#prepareRequestParams(); return this.#config.batchedUpdater(Array.from(newItems), { signal: controller.signal }).then((response) => { const ready = this.ready; response.data.forEach((item) => { ready && this.#setNewOne(item); this.#emit("selfItemUpdated", { instance: this, item }); }); return response; }); } /** * Delete item using its [id] * @param id id of item to delete */ delete(id) { const { controller } = this.#prepareRequestParams(); return this.#config.itemDeleter(id, { signal: controller.signal }).then((response) => { const { result: { id: newId, success } } = response; if (!this.#items) { success && this.#emit("selfItemDeleted", { id: newId, instance: this }); return response; } if (this.#config.trustQuery && !this.#config.devMode && success) { this.#deleteById(newId); this.#emit("selfItemDeleted", { id: newId, instance: this }); return response; } if (id !== newId) { this.#config.devMode && this.#config.trustQuery && console.warn(new ChimeraQueryTrustIdMismatchError(this.#config.name, id, newId)); if (!this.#config.trustQuery) { success && this.#emit("selfItemDeleted", { id: newId, instance: this }); throw new ChimeraQueryTrustIdMismatchError(this.#config.name, id, newId); } } if (success) { this.#deleteById(newId); this.#emit("selfItemDeleted", { id: newId, instance: this }); return response; } const error = new ChimeraQueryUnsuccessfulDeletionError(this.#config.name, id); this.#state = ChimeraQueryFetchingState.ReErrored; this.#lastError = error; throw error; }, (error) => this.#setError(error, new ChimeraQueryDeletingError(this.#config.name, error))); } /** * Delete a list of items by their [id]s * @param ids array of items to delete */ batchedDelete(ids) { const { controller } = this.#prepareRequestParams(); return this.#config.batchedDeleter(Array.from(ids), { signal: controller.signal }).then((response) => { this.#items && response.result.forEach(({ id: newId, success }) => { if (success) { this.#deleteById(newId); this.#emit("selfItemDeleted", { id: newId, instance: this }); } else { const error = new ChimeraQueryUnsuccessfulDeletionError(this.#config.name, newId); this.#state = ChimeraQueryFetchingState.ReErrored; this.#lastError = error; throw error; } }); return response; }, (error) => this.#setError(error, new ChimeraQueryDeletingError(this.#config.name, error))); } /** * Create new item using partial data * @param item partial item data to create new item */ create(item) { const { controller } = this.#prepareRequestParams(); return this.#config.itemCreator(item, { signal: controller.signal }).then((response) => { const { data } = response; this.#items && this.#setNewOne(data); this.#emit("selfItemCreated", { instance: this, item: data }); return response; }, (error) => this.#setError(error, new ChimeraQueryFetchingError(this.#config.name, error))); } /** * Create multiple items using partial data * @param items array of partial item data to create new items */ batchedCreate(items) { const { controller } = this.#prepareRequestParams(); return this.#config.batchedCreator(Array.from(items), { signal: controller.signal }).then((response) => { this.#items && response.data.forEach((item) => { this.#setNewOne(item); this.#emit("selfItemCreated", { instance: this, item }); }); return response; }, (error) => this.#setError(error, new ChimeraQueryFetchingError(this.#config.name, error))); } /** * Standard Array API without mutations */ get length() { return this.#readyItems().length; } [Symbol.iterator]() { return this.#readyItems()[Symbol.iterator](); } at(idx) { return this.#readyItems().at(idx); } entries() { return this.#readyItems().entries(); } values() { return this.#readyItems().values(); } keys() { return this.#readyItems().keys(); } every(predicate) { return this.#readyItems().every((item, idx) => predicate(item, idx, this)); } some(predicate) { return this.#readyItems().some((item, idx) => predicate(item, idx, this)); } filter(predicate) { return this.#readyItems().filter((item, idx) => predicate(item, idx, this)); } find(predicate) { return this.#readyItems().find((item, idx) => predicate(item, idx, this)); } findIndex(predicate) { return this.#readyItems().findIndex((item, idx) => predicate(item, idx, this)); } findLast(predicate) { return this.#readyItems().findLast((item, idx) => predicate(item, idx, this)); } findLastIndex(predicate) { return this.#readyItems().findLastIndex((item, idx) => predicate(item, idx, this)); } forEach(cb) { this.#readyItems().forEach((item, idx) => void cb(item, idx, this)); } includes(item) { return this.#readyItems().includes(item); } indexOf(item) { return this.#readyItems().indexOf(item); } map(cb) { return this.#readyItems().map((item, idx) => cb(item, idx, this)); } reduce(cb, initialValue) { return this.#readyItems().reduce((prev, cur, idx) => cb(prev, cur, idx, this), initialValue); } reduceRight(cb, initialValue) { return this.#readyItems().reduceRight((prev, cur, idx) => cb(prev, cur, idx, this), initialValue); } slice(start, end) { return this.#readyItems().slice(start, end); } toSorted(compareFn) { return this.#readyItems().toSorted(compareFn); } toSpliced(start, deleteCount, ...items) { return this.#readyItems().toSpliced(start, deleteCount, ...items); } toJSON() { return Array.from(this.#readyItems()); } toString() { return this.#readyItems().toString(); } }; //#endregion //#region src/query/ChimeraItemQuery.ts var ChimeraItemQuery = class extends ChimeraEventEmitter { #item; #mutable; #state; #promise; #lastError; #params; #config; #idGetter; #emit(event, arg) { queueMicrotask(() => super.emit(event, arg)); } emit() { throw new ChimeraInternalError("External events dispatching is not supported."); } #prepareRequestParams() { return { controller: new AbortController() }; } #setPromise(promise) { this.#promise?.cancel(); this.#promise = promise; return promise; } #readyItem(internalMessage) { if (this.#item) return this.#item; throw internalMessage ? new ChimeraInternalError(internalMessage) : new ChimeraQueryNotReadyError(this.#config.name); } #mutableItem(internalMessage) { if (this.#state === ChimeraQueryFetchingState.Deleted) throw internalMessage ? new ChimeraInternalError(internalMessage) : new ChimeraQueryDeletedItemError(this.#config.name, this.#params.id); return this.#readyItem(internalMessage); } #setMutable(item) { if (item != null) if (this.#mutable) deepObjectAssign(this.#mutable, item); else this.#mutable = deepObjectClone(item); else this.#mutable = item; } #resetMutable() { this.#setMutable(this.#readyItem(`Trying to reset mutable ref for empty item (${this.#config.name}[${this.#params.id}])`)); } #setItem(item) { !this.#item && this.#emit("ready", { instance: this }); const oldItem = this.#item; this.#item = item; this.#resetMutable(); this.#emit("updated", { instance: this, item, oldItem }); } #setNewItem(item) { deepObjectFreeze(item); const oldItem = this.#item; this.#setItem(item); this.#emit("selfUpdated", { instance: this, item, oldItem }); } #deleteItem() { this.#state = ChimeraQueryFetchingState.Deleted; this.#emit("deleted", { id: this.#params.id, instance: this }); } #setError(error, source) { this.#state = this.#item ? ChimeraQueryFetchingState.ReErrored : ChimeraQueryFetchingState.Errored; this.#lastError = error; this.#emit("error", { error, instance: this }); throw source; } #watchPromise(promise, controller) { return makeCancellablePromise(promise.then(({ data }) => { if (this.#config.trustQuery && !this.#config.devMode) { this.#setNewItem(data); this.#state = ChimeraQueryFetchingState.Fetched; return { data }; } const localId = this.#params.id; const newId = this.#idGetter(data); if (localId === newId || this.#state === ChimeraQueryFetchingState.Creating) { this.#setNewItem(data); if (this.#state === ChimeraQueryFetchingState.Creating) this.#emit("selfCreated", { instance: this, item: data }); this.#state = ChimeraQueryFetchingState.Fetched; } else { this.#config.devMode && this.#config.trustQuery && console.warn(new ChimeraQueryTrustIdMismatchError(this.#config.name, localId, newId)); if (!this.#config.trustQuery) throw new ChimeraQueryTrustIdMismatchError(this.#config.name, localId, newId); this.#setNewItem(data); this.#params.id = newId; this.#state = ChimeraQueryFetchingState.Fetched; } return { data }; }).catch((error) => this.#setError(error, new ChimeraQueryFetchingError(this.#config.name, error))), controller); } #updateItem(newItem) { const newId = this.#idGetter(newItem); const oldId = this.#idGetter(this.#readyItem(`Trying to update not ready item (${this.#config.name}[${this.#params.id}])`)); if (newId !== oldId && !this.#config.trustQuery) { this.#resetMutable(); throw new ChimeraQueryIdMismatchError(this.#config.name, oldId, newId); } this.#state = ChimeraQueryFetchingState.Updating; const { controller } = this.#prepareRequestParams(); return this.#setPromise(this.#watchPromise(makeCancellablePromise(this.#config.itemUpdater(newItem, { signal: controller.signal }), controller), controller)); } #requestDelete() { this.#state = ChimeraQueryFetchingState.Deleting; const { controller } = this.#prepareRequestParams(); return this.#setPromise(makeCancellablePromise(makeCancellablePromise(this.#config.itemDeleter(this.#params.id, { signal: controller.signal }), controller).then(({ result }) => { const { id, success } = result; if (this.#config.trustQuery && !this.#config.devMode && success) { this.#deleteItem(); return { result }; } const localId = this.#params.id; if (localId !== id) { this.#config.devMode && this.#config.trustQuery && console.warn(new ChimeraQueryTrustIdMismatchError(this.#config.name, localId, id)); if (!this.#config.trustQuery) throw new ChimeraQueryTrustIdMismatchError(this.#config.name, localId, id); } if (success) { this.#deleteItem(); this.#emit("selfDeleted", { id, instance: this }); } else { const error = new ChimeraQueryUnsuccessfulDeletionError(this.#config.name, this.#params.id); this.#state = ChimeraQueryFetchingState.ReErrored; this.#lastError = error; throw error; } return { result }; }, (error) => this.#setError(error, new ChimeraQueryDeletingError(this.#config.name, error))))); } constructor(config, params, existingItem, toCreateItem) { super(); this.#config = config; this.#idGetter = config.idGetter; this.#params = params; this.#promise = null; this.#item = null; this.#mutable = null; this.#state = ChimeraQueryFetchingState.Initialized; if (existingItem) { const item = existingItem; this.#setItem(item); if (config.devMode && this.#idGetter(item) !== params.id) { this.#state = ChimeraQueryFetchingState.Errored; throw new ChimeraInternalError(`Invalid item query [id] (changed from "${params.id}" to "${this.#idGetter(item)}")`); } this.#state = ChimeraQueryFetchingState.Prefetched; } else if (toCreateItem) { this.#state = ChimeraQueryFetchingState.Creating; const { controller } = this.#prepareRequestParams(); this.#setPromise(this.#watchPromise(makeCancellablePromise(config.itemCreator(toCreateItem, { signal: controller.signal }), controller).then(({ data }) => { this.#params.id = this.#idGetter(data); return { data }; }), controller)); } else { this.#state = ChimeraQueryFetchingState.Fetching; const { controller } = this.#prepareRequestParams(); this.#setPromise(this.#watchPromise(makeCancellablePromise(config.itemFetcher(params, { signal: controller.signal }), controller), controller)); } this.#emit("initialized", { instance: this }); } get [ChimeraGetParamsSym]() { return this.#params; } [ChimeraSetOneSym](item) { this.#setItem(item); !this.inProgress && (this.#state = ChimeraQueryFetchingState.Actualized); } [ChimeraDeleteOneSym](id) { if (id === this.#params.id) { this.#promise?.cancel(); this.#promise = null; this.#deleteItem(); } } get state() { return this.#state; } get inProgress() { return IN_PROGRESS_STATES.includes(this.#state); } get ready() { return this.#item !== null; } get lastError() { return this.#lastError; } get id() { return this.#params.id; } /** Return an item if it is ready, throw error otherwise */ get data() { return this.#readyItem(); } /** Get ref for an item that can be changed as a regular object. To send changes to updater, use <commit> method */ get mutable() { this.#readyItem(); return this.#mutable; } get promise() { return this.#promise; } /** * Wait for the current progress process to complete (both success or error) */ get progress() { return new Promise((res) => { const resolve = () => queueMicrotask(() => res()); if (this.#promise) { this.#promise.then(resolve, resolve); this.#promise.cancelled(() => this.progress.then(resolve, resolve)); } else resolve(); }); } /** * Wait for the current progress process to complete, throw an error if it fails */ get result() { return new Promise((res, rej) => { const resolve = () => queueMicrotask(() => res()); if (this.#promise) { this.#promise.then(resolve, rej); this.#promise.cancelled(() => this.#promise ? this.result.then(res, rej) : rej("cancelled")); } else resolve(); }); } /** * Trigger refetch, return existing refetch promise if already running * @param force If true cancels any running process and starts a new one * @throws {ChimeraQueryAlreadyRunningError} If deleting or updating already in progress */ refetch(force = false) { if (!force && this.#promise && [ChimeraQueryFetchingState.Fetching, ChimeraQueryFetchingState.Refetching].includes(this.#state)) return this.#promise; if (this.#state === ChimeraQueryFetchingState.Creating) throw new ChimeraQueryNotCreatedError(this.#config.name); if (!force && [ChimeraQueryFetchingState.Updating, ChimeraQueryFetchingState.Deleting].includes(this.#state)) throw new ChimeraQueryAlreadyRunningError(this.#config.name, this.#state); this.#state = ChimeraQueryFetchingState.Refetching; const { controller } = this.#prepareRequestParams(); return this.#setPromise(this.#watchPromise(makeCancellablePromise(this.#config.itemFetcher(this.#params, { signal: controller.signal }), controller), controller)); } /** * Update item using updated copy, a running update process will be cancelled * @param newItem new item to replace existing * @param force if true cancels any running process including fetch and delete * @throws {ChimeraQueryAlreadyRunningError} If deleting or updating already in progress */ update(newItem, force = false) { if (this.#state === ChimeraQueryFetchingState.Creating) throw new ChimeraQueryNotCreatedError(this.#config.name); if (!force && [ ChimeraQueryFetchingState.Fetching, ChimeraQueryFetchingState.Refetching, ChimeraQueryFetchingState.Deleting ].includes(this.#state)) throw new ChimeraQueryAlreadyRunningError(this.#config.name, this.#state); this.#mutableItem(); return this.#updateItem(newItem); } /** * Update item using function with draft item as argument * that can be used to patch item in place or return a patched value, * a running update process will be cancelled * @param mutator mutator function * @param force if true cancels any running process including fetch and delete * @throws {ChimeraQueryAlreadyRunningError} If deleting or updating already in progress */ mutate(mutator, force = false) { if (this.#state === ChimeraQueryFetchingState.Creating) throw new ChimeraQueryNotCreatedError(this.#config.name); if (!force && [ ChimeraQueryFetchingState.Fetching, ChimeraQueryFetchingState.Refetching, ChimeraQueryFetchingState.Deleting ].includes(this.#state)) throw new ChimeraQueryAlreadyRunningError(this.#config.name, this.#state); const item = deepObjectClone(this.#mutableItem()); return this.#updateItem(mutator(item) ?? item); } /** * Commit updated value from mutable ref, a running update process will be canceled * @param force if true cancels any running process including fetch and delete * @throws {ChimeraQueryAlreadyRunningError} If deleting or updating already in progress */ commit(force = false) { if (this.#state === ChimeraQueryFetchingState.Creating) throw new ChimeraQueryNotCreatedError(this.#config.name); if (!force && [ ChimeraQueryFetchingState.Fetching, ChimeraQueryFetchingState.Refetching, ChimeraQueryFetchingState.Deleting ].includes(this.#state)) throw new ChimeraQueryAlreadyRunningError(this.#config.name, this.#state); this.#mutableItem(); return this.#updateItem(this.#mutable); } /** * Request to delete the value. * Local copy will still be available if it was present. * A running delete process will be canceled * @param force if true cancels any running process including fetch and update * @throws {ChimeraQueryAlreadyRunningError} If deleting or updating already in progress */ delete(force = false) { if (this.#state === ChimeraQueryFetchingState.Creating) throw new ChimeraQueryNotCreatedError(this.#config.name); if (!force && [ ChimeraQueryFetchingState.Fetching, ChimeraQueryFetchingState.Refetching, ChimeraQueryFetchingState.Updating ].includes(this.#state)) throw new ChimeraQueryAlreadyRunningError(this.#config.name, this.#state); return this.#requestDelete(); } toJSON() { return this.#readyItem(); } toString() { return `${this.#readyItem()}`; } }; //#endregion //#region src/shared/ChimeraWeakValueMap/ChimeraWeakValueMap.ts var ChimeraWeakValueMap = class extends ChimeraEventEmitter { #map; #registry; #cleanupScheduled = false; #emit(event, arg) { queueMicrotask(() => super.emit(event, arg)); } emit() { throw new ChimeraInternalError("External events dispatching is not supported."); } #scheduleCleanup() { if (this.#cleanupScheduled) return; this.#cleanupScheduled = true; (typeof requestIdleCallback !== "undefined" ? requestIdleCallback : (cb) => setTimeout(cb, 0))(() => { this.#cleanup(); this.#cleanupScheduled = false; }); } #cleanup() { for (const [key, weakRef] of this.#map.entries()) if (weakRef.deref() === void 0) { this.#map.delete(key); this.#emit("finalize", { instance: this, key }); } } constructor(values) { super(); this.#registry = new FinalizationRegistry((key) => { const weakRef = this.#map.get(key); if (weakRef && weakRef.deref() === void 0) { this.#map.delete(key); this.#emit("finalize", { instance: this, key }); } }); this.#map = new Map(values ? values.map(([k, v]) => { this.#registry.register(v, k, v); return [k, new WeakRef(v)]; }) : null); } set(key, value) { const existingRef = this.#map.get(key); if (existingRef) { const existingValue = existingRef.deref(); if (existingValue) this.#registry.unregister(existingValue); } this.#registry.register(value, key, value); this.#map.set(key, new WeakRef(value)); this.#emit("set", { instance: this, key, value }); return this; } delete(key) { if (!this.#map.has(key)) return false; const value = this.#map.get(key)?.deref(); if (value === void 0) { this.#map.delete(key); this.#emit("finalize", { instance: this, key }); return true; } this.#map.delete(key); this.#registry.unregister(value); this.#emit("delete", { instance: this, key, value }); return true; } has(key) { const weakRef = this.#map.get(key); const value = weakRef?.deref(); if (value === void 0 && weakRef) { this.#map.delete(key); this.#emit("finalize", { instance: this, key }); this.#scheduleCleanup(); } return value !== void 0; } forEach(callbackFn, thisArg) { this.#map.forEach((weakRef, k) => { const value = weakRef.deref(); if (value !== void 0) callbackFn.call(thisArg, value, k, this); else { this.#map.delete(k); this.#emit("finalize", { instance: this, key: k }); } }); if (this.#map.size > 0) this.#scheduleCleanup(); } get(key) { const weakRef = this.#map.get(key); const value = weakRef?.deref(); if (value === void 0 && weakRef) { this.#map.delete(key); this.#emit("finalize", { instance: this, key }); this.#scheduleCleanup(); } return value; } get size() { this.#cleanup(); return this.#map.size; } *entries() { for (const [k, weakRef] of this.#map.entries()) { const value = weakRef.deref(); if (value !== void 0) yield [k, value]; else { this.#map.delete(k); this.#emit("finalize", { instance: this, key: k }); } } if (this.#map.size > 0) this.#scheduleCleanup(); } *keys() { for (const [k, weakRef] of this.#map.entries()) if (weakRef.deref() !== void 0) yield k; else { this.#map.delete(k); this.#emit("finalize", { instance: this, key: k }); } if (this.#map.size > 0) this.#scheduleCleanup(); } *values() { for (const weakRef of this.#map.values()) { const value = weakRef.deref(); if (value !== void 0) yield value; } this.#cleanup(); } *[Symbol.iterator]() { yield* this.entries(); } clear() { for (const weakRef of this.#map.values()) { const value = weakRef.deref(); if (value !== void 0) this.#registry.unregister(value); } this.#map.clear(); this.#emit("clear", { instance: this }); } cleanup() { this.#cleanup(); } get rawSize() { return this.#map.size; } }; //#endregion //#region src/store/ChimeraEntityRepository.ts var ChimeraEntityRepository = class extends ChimeraEventEmitter { #entityConfig; #filterConfig; #orderConfig; #idGetter; #itemsMap; #collectionQueryMap; #itemQueryMap; #emit(event, arg) { queueMicrotask(() => super.emit(event, arg)); } emit() { throw new ChimeraInternalError("External events dispatching is not supported."); } #registerUpdate(item, skipItem) { const id = this.#idGetter(item); const oldItem = this.#itemsMap.get(id); this.#itemsMap.set(id, item); const itemQuery = this.#itemQueryMap.get(id); itemQuery && skipItem !== itemQuery && itemQuery[ChimeraSetOneSym](item); !oldItem && this.#emit("itemAdded", { instance: this, item }); this.#emit("itemUpdated", { instance: this, item, oldItem: oldItem ?? null }); } #registerDelete(id, skipItem) { const oldItem = this.#itemsMap.get(id); if (!oldItem) return; this.#itemsMap.delete(id); const itemQuery = this.#itemQueryMap.get(id); itemQuery && skipItem !== itemQuery && itemQuery[ChimeraDeleteOneSym](id); this.#emit("itemDeleted", { instance: this, oldItem: oldItem ?? null }); } #propagateUpdateOne(item, { item: skipItem, collection: skipCollection } = {}) { this.#registerUpdate(item, skipItem); for (const c of this.#collectionQueryMap.values()) c !== skipCollection && c[ChimeraSetOneSym](item); } #propagateDeleteOne(id, { item: skipItem, collection: skipCollection } = {}) { this.#registerDelete(id, skipItem); for (const c of this.#collectionQueryMap.values()) c !== skipCollection && c[ChimeraDeleteOneSym](id); } #propagateUpdateMany(items, { item: skipItem, collection: skipCollection } = {}) { for (const item of items) this.#registerUpdate(item, skipItem); this.#emit("updated", { instance: this, items }); for (const c of this.#collectionQueryMap.values()) c !== skipCollection && c[ChimeraSetManySym](items); } #propagateDeleteMany(ids, { item: skipItem, collection: skipCollection } = {}) { for (const id of ids) this.#registerDelete(id, skipItem); this.#emit("deleted", { ids, instance: this }); for (const c of this.#collectionQueryMap.values()) c !== skipCollection && c[ChimeraDeleteManySym](ids); } #itemUpdateHandler(query, item) { this.#propagateUpdateOne(item, { item: query }); } #itemDeleteHandler(query, id) { this.#itemQueryMap.delete(id); this.#propagateDeleteOne(id, { item: query }); } #prepareItemQuery(query) { if (query.id !== "") this.#itemQueryMap.set(query.id, query); query.on("selfCreated", ({ instance }) => this.#itemQueryMap.set(instance.id, instance)); query.on("selfUpdated", ({ instance, item }) => this.#itemUpdateHandler(instance, item)); query.on("selfDeleted", ({ instance, id }) => this.#itemDeleteHandler(instance, id)); return query; } #simplifyCollectionParams(params) { return { filter: simplifyFilter(params.filter), meta: params.meta, order: simplifyOrderBy(params.order) }; } #getCollectionKey({ order, filter }) { return `ORDER<${order ? this.#orderConfig.getKey(order) : ""}>\nFILTER