UNPKG

wacom

Version:

Module which has common services, pipes, directives and interfaces which can be used on all projects.

1 lines 309 kB
{"version":3,"file":"wacom.mjs","sources":["../../../projects/wacom/src/core/core.prototype.ts","../../../projects/wacom/src/core/core.service.ts","../../../projects/wacom/src/crud/crud.component.ts","../../../projects/wacom/src/services/emitter.service.ts","../../../projects/wacom/src/interfaces/config.interface.ts","../../../projects/wacom/src/interfaces/http.interface.ts","../../../projects/wacom/src/services/http.service.ts","../../../projects/wacom/src/interfaces/network.interface.ts","../../../projects/wacom/src/services/network.service.ts","../../../projects/wacom/src/store/store.service.ts","../../../projects/wacom/src/crud/crud.service.ts","../../../projects/wacom/src/theme/theme.service.ts","../../../projects/wacom/src/meta/meta.const.ts","../../../projects/wacom/src/meta/meta.service.ts","../../../projects/wacom/src/meta/meta.guard.ts","../../../projects/wacom/src/translate/translate.service.ts","../../../projects/wacom/src/translate/provide-translate.ts","../../../projects/wacom/src/translate/translate.directive.ts","../../../projects/wacom/src/translate/translate.pipe.ts","../../../projects/wacom/src/directives/click-outside.directive.ts","../../../projects/wacom/src/directives/manual-disabled.directive.ts","../../../projects/wacom/src/directives/manual-name.directive.ts","../../../projects/wacom/src/directives/manual-readonly.directive.ts","../../../projects/wacom/src/directives/manual-type.directive.ts","../../../projects/wacom/src/pipes/arr.pipe.ts","../../../projects/wacom/src/pipes/mongodate.pipe.ts","../../../projects/wacom/src/pipes/number.pipe.ts","../../../projects/wacom/src/pipes/pagination.pipe.ts","../../../projects/wacom/src/pipes/safe.pipe.ts","../../../projects/wacom/src/pipes/search.pipe.ts","../../../projects/wacom/src/pipes/splice.pipe.ts","../../../projects/wacom/src/pipes/split.pipe.ts","../../../projects/wacom/src/services/dom.service.ts","../../../projects/wacom/src/services/rtc.service.ts","../../../projects/wacom/src/services/socket.service.ts","../../../projects/wacom/src/services/time.service.ts","../../../projects/wacom/src/services/util.service.ts","../../../projects/wacom/src/provide-wacom.ts","../../../projects/wacom/src/wacom.module.ts","../../../projects/wacom/public-api.ts","../../../projects/wacom/wacom.ts"],"sourcesContent":["export {};\r\n\r\n// Add capitalize method to String prototype if it doesn't already exist\r\nif (!String.prototype.capitalize) {\r\n\tString.prototype.capitalize = function (): string {\r\n\t\tif (this.length > 0) {\r\n\t\t\treturn this.charAt(0).toUpperCase() + this.slice(1).toLowerCase();\r\n\t\t}\r\n\t\treturn '';\r\n\t};\r\n}\r\n\r\n// Extend the String interface to include the new method\r\ndeclare global {\r\n\tinterface String {\r\n\t\tcapitalize(): string;\r\n\t}\r\n}\r\n","// Core utilities and helpers for the Wacom app\r\nimport { isPlatformBrowser } from '@angular/common';\r\nimport {\r\n\tDestroyRef,\r\n\tInjectable,\r\n\tPLATFORM_ID,\r\n\tSignal,\r\n\tWritableSignal,\r\n\tcomputed,\r\n\tinject,\r\n\tsignal,\r\n} from '@angular/core';\r\nimport { Viewport } from './core.type';\r\n\r\n@Injectable({\r\n\tprovidedIn: 'root',\r\n})\r\nexport class CoreService {\r\n\tprivate readonly _platformId = inject(PLATFORM_ID);\r\n\tprivate readonly _isBrowser = isPlatformBrowser(this._platformId);\r\n\tprivate readonly _destroyRef = inject(DestroyRef);\r\n\r\n\tdeviceID = '';\r\n\r\n\tconstructor() {\r\n\t\tif (this._isBrowser) {\r\n\t\t\tconst stored = localStorage.getItem('deviceID');\r\n\t\t\tthis.deviceID =\r\n\t\t\t\tstored ||\r\n\t\t\t\t(typeof crypto?.randomUUID === 'function' ? crypto.randomUUID() : this.UUID());\r\n\r\n\t\t\tlocalStorage.setItem('deviceID', this.deviceID);\r\n\r\n\t\t\tthis.detectDevice();\r\n\t\t\tthis.detectViewport();\r\n\t\t} else {\r\n\t\t\tthis.deviceID = this.UUID();\r\n\t\t}\r\n\t}\r\n\r\n\t/**\r\n\t * Generates a UUID (Universally Unique Identifier) version 4.\r\n\t *\r\n\t * This implementation uses `Math.random()` to generate random values,\r\n\t * making it suitable for general-purpose identifiers, but **not** for\r\n\t * cryptographic or security-sensitive use cases.\r\n\t *\r\n\t * The format follows the UUID v4 standard: `xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx`\r\n\t * where:\r\n\t * - `x` is a random hexadecimal digit (0–f)\r\n\t * - `4` indicates UUID version 4\r\n\t * - `y` is one of 8, 9, A, or B\r\n\t *\r\n\t * Example: `f47ac10b-58cc-4372-a567-0e02b2c3d479`\r\n\t *\r\n\t * @returns A string containing a UUID v4.\r\n\t */\r\n\tUUID(): string {\r\n\t\treturn 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c: string) => {\r\n\t\t\tconst r = (Math.random() * 16) | 0;\r\n\t\t\tconst v = c === 'x' ? r : (r & 0x3) | 0x8;\r\n\t\t\treturn v.toString(16);\r\n\t\t});\r\n\t}\r\n\r\n\t/**\r\n\t * Converts an object to an array. Optionally holds keys instead of values.\r\n\t *\r\n\t * @param {any} obj - The object to be converted.\r\n\t * @param {boolean} [holder=false] - If true, the keys will be held in the array; otherwise, the values will be held.\r\n\t * @returns {any[]} The resulting array.\r\n\t */\r\n\tota(obj: any, holder: boolean = false): any[] {\r\n\t\tif (Array.isArray(obj)) return obj;\r\n\t\tif (typeof obj !== 'object' || obj === null) return [];\r\n\t\tconst arr = [];\r\n\t\tfor (const each in obj) {\r\n\t\t\tif (\r\n\t\t\t\tobj.hasOwnProperty(each) &&\r\n\t\t\t\t(obj[each] || typeof obj[each] === 'number' || typeof obj[each] === 'boolean')\r\n\t\t\t) {\r\n\t\t\t\tif (holder) {\r\n\t\t\t\t\tarr.push(each);\r\n\t\t\t\t} else {\r\n\t\t\t\t\tarr.push(obj[each]);\r\n\t\t\t\t}\r\n\t\t\t}\r\n\t\t}\r\n\t\treturn arr;\r\n\t}\r\n\r\n\t/**\r\n\t * Removes elements from `fromArray` that are present in `removeArray` based on a comparison field.\r\n\t *\r\n\t * @param {any[]} removeArray - The array of elements to remove.\r\n\t * @param {any[]} fromArray - The array from which to remove elements.\r\n\t * @param {string} [compareField='_id'] - The field to use for comparison.\r\n\t * @returns {any[]} The modified `fromArray` with elements removed.\r\n\t */\r\n\tsplice(removeArray: any[], fromArray: any[], compareField: string = '_id'): any[] {\r\n\t\tif (!Array.isArray(removeArray) || !Array.isArray(fromArray)) {\r\n\t\t\treturn fromArray;\r\n\t\t}\r\n\r\n\t\tconst removeSet = new Set(removeArray.map(item => item[compareField]));\r\n\t\treturn fromArray.filter(item => !removeSet.has(item[compareField]));\r\n\t}\r\n\r\n\t/**\r\n\t * Unites multiple _id values into a single unique _id.\r\n\t * The resulting _id is unique regardless of the order of the input _id values.\r\n\t *\r\n\t * @param {...string[]} args - The _id values to be united.\r\n\t * @returns {string} The unique combined _id.\r\n\t */\r\n\tids2id(...args: string[]): string {\r\n\t\targs.sort((a, b) => {\r\n\t\t\tif (Number(a.toString().substring(0, 8)) > Number(b.toString().substring(0, 8))) {\r\n\t\t\t\treturn 1;\r\n\t\t\t}\r\n\t\t\treturn -1;\r\n\t\t});\r\n\r\n\t\treturn args.join();\r\n\t}\r\n\r\n\t// After While\r\n\tprivate _afterWhile: Record<string, number> = {};\r\n\t/**\r\n\t * Delays the execution of a callback function for a specified amount of time.\r\n\t * If called again within that time, the timer resets.\r\n\t *\r\n\t * @param {string | object | (() => void)} doc - A unique identifier for the timer, an object to host the timer, or the callback function.\r\n\t * @param {() => void} [cb] - The callback function to execute after the delay.\r\n\t * @param {number} [time=1000] - The delay time in milliseconds.\r\n\t */\r\n\tafterWhile(doc: string | object | (() => void), cb?: () => void, time: number = 1000): void {\r\n\t\tif (typeof doc === 'function') {\r\n\t\t\tcb = doc as () => void;\r\n\t\t\tdoc = 'common';\r\n\t\t}\r\n\r\n\t\tif (typeof cb === 'function' && typeof time === 'number') {\r\n\t\t\tif (typeof doc === 'string') {\r\n\t\t\t\tclearTimeout(this._afterWhile[doc]);\r\n\t\t\t\tthis._afterWhile[doc] = setTimeout(cb, time);\r\n\t\t\t} else if (typeof doc === 'object') {\r\n\t\t\t\tclearTimeout((doc as { __afterWhile: number }).__afterWhile);\r\n\t\t\t\t(doc as { __afterWhile: number }).__afterWhile = setTimeout(cb, time);\r\n\t\t\t} else {\r\n\t\t\t\tconsole.warn('badly configured after while');\r\n\t\t\t}\r\n\t\t}\r\n\t}\r\n\r\n\t/**\r\n\t * Recursively copies properties from one object to another.\r\n\t * Handles nested objects, arrays, and Date instances appropriately.\r\n\t *\r\n\t * @param from - The source object from which properties are copied.\r\n\t * @param to - The target object to which properties are copied.\r\n\t */\r\n\tcopy(from: any, to: any) {\r\n\t\tfor (const each in from) {\r\n\t\t\tif (\r\n\t\t\t\ttypeof from[each] !== 'object' ||\r\n\t\t\t\tfrom[each] instanceof Date ||\r\n\t\t\t\tArray.isArray(from[each]) ||\r\n\t\t\t\tfrom[each] === null\r\n\t\t\t) {\r\n\t\t\t\tto[each] = from[each];\r\n\t\t\t} else {\r\n\t\t\t\tif (\r\n\t\t\t\t\ttypeof to[each] !== 'object' ||\r\n\t\t\t\t\tto[each] instanceof Date ||\r\n\t\t\t\t\tArray.isArray(to[each]) ||\r\n\t\t\t\t\tto[each] === null\r\n\t\t\t\t) {\r\n\t\t\t\t\tto[each] = {};\r\n\t\t\t\t}\r\n\r\n\t\t\t\tthis.copy(from[each], to[each]);\r\n\t\t\t}\r\n\t\t}\r\n\t}\r\n\r\n\t// Device management\r\n\tdevice = '';\r\n\t/**\r\n\t * Detects the device type based on the user agent.\r\n\t */\r\n\tdetectDevice(): void {\r\n\t\tif (!this._isBrowser) return;\r\n\r\n\t\tconst userAgent = navigator.userAgent || navigator.vendor || (window as any).opera;\r\n\t\tif (/windows phone/i.test(userAgent)) {\r\n\t\t\tthis.device = 'Windows Phone';\r\n\t\t} else if (/android/i.test(userAgent)) {\r\n\t\t\tthis.device = 'Android';\r\n\t\t} else if (/iPad|iPhone|iPod/.test(userAgent) && !(window as any).MSStream) {\r\n\t\t\tthis.device = 'iOS';\r\n\t\t} else {\r\n\t\t\tthis.device = 'Web';\r\n\t\t}\r\n\t}\r\n\r\n\t/**\r\n\t * Checks if the device is a mobile device.\r\n\t * @returns {boolean} - Returns true if the device is a mobile device.\r\n\t */\r\n\tisMobile(): boolean {\r\n\t\treturn (\r\n\t\t\tthis.device === 'Windows Phone' || this.device === 'Android' || this.device === 'iOS'\r\n\t\t);\r\n\t}\r\n\r\n\t/**\r\n\t * Checks if the device is a tablet.\r\n\t * @returns {boolean} - Returns true if the device is a tablet.\r\n\t */\r\n\tisTablet(): boolean {\r\n\t\tif (!this._isBrowser) return false;\r\n\r\n\t\treturn this.device === 'iOS' && /iPad/.test(navigator.userAgent);\r\n\t}\r\n\r\n\t/**\r\n\t * Checks if the device is a web browser.\r\n\t * @returns {boolean} - Returns true if the device is a web browser.\r\n\t */\r\n\tisWeb(): boolean {\r\n\t\treturn this.device === 'Web';\r\n\t}\r\n\r\n\t/**\r\n\t * Checks if the device is an Android device.\r\n\t * @returns {boolean} - Returns true if the device is an Android device.\r\n\t */\r\n\tisAndroid(): boolean {\r\n\t\treturn this.device === 'Android';\r\n\t}\r\n\r\n\t/**\r\n\t * Checks if the device is an iOS device.\r\n\t * @returns {boolean} - Returns true if the device is an iOS device.\r\n\t */\r\n\tisIos(): boolean {\r\n\t\treturn this.device === 'iOS';\r\n\t}\r\n\r\n\t// Viewport management (responsive breakpoint)\r\n\treadonly viewport = signal<Viewport>('desktop');\r\n\r\n\treadonly isViewportMobile = computed(() => this.viewport() === 'mobile');\r\n\treadonly isViewportTablet = computed(() => this.viewport() === 'tablet');\r\n\treadonly isViewportDesktop = computed(() => this.viewport() === 'desktop');\r\n\r\n\tdetectViewport(): void {\r\n\t\tif (!this._isBrowser) return;\r\n\r\n\t\tconst mqMobile = window.matchMedia('(max-width: 767.98px)');\r\n\t\tconst mqTablet = window.matchMedia('(min-width: 768px) and (max-width: 1023.98px)');\r\n\t\tconst mqDesktop = window.matchMedia('(min-width: 1024px)');\r\n\r\n\t\tconst update = () => {\r\n\t\t\tif (mqMobile.matches) return this.viewport.set('mobile');\r\n\t\t\tif (mqTablet.matches) return this.viewport.set('tablet');\r\n\t\t\treturn this.viewport.set('desktop');\r\n\t\t};\r\n\r\n\t\tupdate();\r\n\r\n\t\tmqMobile.addEventListener('change', update);\r\n\t\tmqTablet.addEventListener('change', update);\r\n\t\tmqDesktop.addEventListener('change', update);\r\n\r\n\t\tthis._destroyRef.onDestroy(() => {\r\n\t\t\tmqMobile.removeEventListener('change', update);\r\n\t\t\tmqTablet.removeEventListener('change', update);\r\n\t\t\tmqDesktop.removeEventListener('change', update);\r\n\t\t});\r\n\t}\r\n\r\n\t// Version management\r\n\tversion = '1.0.0';\r\n\r\n\tappVersion = '';\r\n\r\n\tdateVersion = '';\r\n\r\n\t/**\r\n\t * Sets the combined version string based on appVersion and dateVersion.\r\n\t */\r\n\tsetVersion(): void {\r\n\t\tthis.version = this.appVersion || '';\r\n\r\n\t\tthis.version += this.version && this.dateVersion ? ' ' : '';\r\n\r\n\t\tthis.version += this.dateVersion || '';\r\n\t}\r\n\r\n\t/**\r\n\t * Sets the app version and updates the combined version string.\r\n\t *\r\n\t * @param {string} appVersion - The application version to set.\r\n\t */\r\n\tsetAppVersion(appVersion: string): void {\r\n\t\tthis.appVersion = appVersion;\r\n\r\n\t\tthis.setVersion();\r\n\t}\r\n\r\n\t/**\r\n\t * Sets the date version and updates the combined version string.\r\n\t *\r\n\t * @param {string} dateVersion - The date version to set.\r\n\t */\r\n\tsetDateVersion(dateVersion: string): void {\r\n\t\tthis.dateVersion = dateVersion;\r\n\r\n\t\tthis.setVersion();\r\n\t}\r\n\r\n\t// Locking management\r\n\tprivate _locked: Record<string, boolean> = {};\r\n\tprivate _unlockResolvers: Record<string, (() => void)[]> = {};\r\n\r\n\t/**\r\n\t * Locks a resource to prevent concurrent access.\r\n\t * @param which - The resource to lock, identified by a string.\r\n\t */\r\n\tlock(which: string): void {\r\n\t\tthis._locked[which] = true;\r\n\r\n\t\tif (!this._unlockResolvers[which]) {\r\n\t\t\tthis._unlockResolvers[which] = [];\r\n\t\t}\r\n\t}\r\n\r\n\t/**\r\n\t * Unlocks a resource, allowing access.\r\n\t * @param which - The resource to unlock, identified by a string.\r\n\t */\r\n\tunlock(which: string): void {\r\n\t\tthis._locked[which] = false;\r\n\r\n\t\tif (this._unlockResolvers[which]) {\r\n\t\t\tthis._unlockResolvers[which].forEach(resolve => resolve());\r\n\r\n\t\t\tthis._unlockResolvers[which] = [];\r\n\t\t}\r\n\t}\r\n\r\n\t/**\r\n\t * Returns a Promise that resolves when the specified resource is unlocked.\r\n\t * @param which - The resource to watch for unlocking, identified by a string.\r\n\t * @returns A Promise that resolves when the resource is unlocked.\r\n\t */\r\n\tonUnlock(which: string): Promise<void> {\r\n\t\tif (!this._locked[which]) {\r\n\t\t\treturn Promise.resolve();\r\n\t\t}\r\n\r\n\t\treturn new Promise(resolve => {\r\n\t\t\tif (!this._unlockResolvers[which]) {\r\n\t\t\t\tthis._unlockResolvers[which] = [];\r\n\t\t\t}\r\n\r\n\t\t\tthis._unlockResolvers[which].push(resolve);\r\n\t\t});\r\n\t}\r\n\r\n\t/**\r\n\t * Checks if a resource is locked.\r\n\t * @param which - The resource to check, identified by a string.\r\n\t * @returns True if the resource is locked, false otherwise.\r\n\t */\r\n\tlocked(which: string): boolean {\r\n\t\treturn !!this._locked[which];\r\n\t}\r\n\r\n\t// Angular Signals //\r\n\t/**\r\n\t * Converts a plain object into a signal-wrapped object.\r\n\t * Optionally wraps specific fields of the object as individual signals,\r\n\t * and merges them into the returned signal for fine-grained reactivity.\r\n\t *\r\n\t * @template Document - The type of the object being wrapped.\r\n\t * @param {Document} document - The plain object to wrap into a signal.\r\n\t * @param {Record<string, (doc: Document) => unknown>} [signalFields={}] -\r\n\t * Optional map where each key is a field name and the value is a function\r\n\t * to extract the initial value for that field. These fields will be wrapped\r\n\t * as separate signals and embedded in the returned object.\r\n\t *\r\n\t * @returns {WritableSignal<Document>} A signal-wrapped object, possibly containing\r\n\t * nested field signals for more granular control.\r\n\t *\r\n\t * @example\r\n\t * const user = { _id: '1', name: 'Alice', score: 42 };\r\n\t * const sig = toSignal(user, { score: (u) => u.score });\r\n\t * console.log(sig().name); // 'Alice'\r\n\t * console.log(sig().score()); // 42 — field is now a signal\r\n\t */\r\n\ttoSignal<Document>(\r\n\t\tdocument: Document,\r\n\t\tsignalFields: Record<string, (doc: Document) => unknown> = {},\r\n\t): WritableSignal<Document> {\r\n\t\tif (Object.keys(signalFields).length) {\r\n\t\t\tconst fields: Record<string, Signal<unknown>> = {};\r\n\r\n\t\t\tfor (const key in signalFields) {\r\n\t\t\t\tfields[key] = signal(signalFields[key](document));\r\n\t\t\t}\r\n\r\n\t\t\treturn signal({ ...document, ...fields });\r\n\t\t} else {\r\n\t\t\treturn signal(document);\r\n\t\t}\r\n\t}\r\n\r\n\t/**\r\n\t * Converts an array of objects into an array of Angular signals.\r\n\t * Optionally wraps specific fields of each object as individual signals.\r\n\t *\r\n\t * @template Document - The type of each object in the array.\r\n\t * @param {Document[]} arr - Array of plain objects to convert into signals.\r\n\t * @param {Record<string, (doc: Document) => unknown>} [signalFields={}] -\r\n\t * Optional map where keys are field names and values are functions that extract the initial value\r\n\t * from the object. These fields will be turned into separate signals.\r\n\t *\r\n\t * @returns {WritableSignal<Document>[]} An array where each item is a signal-wrapped object,\r\n\t * optionally with individual fields also wrapped in signals.\r\n\t *\r\n\t * @example\r\n\t * toSignalsArray(users, {\r\n\t * name: (u) => u.name,\r\n\t * score: (u) => u.score,\r\n\t * });\r\n\t */\r\n\ttoSignalsArray<Document>(\r\n\t\tarr: Document[],\r\n\t\tsignalFields: Record<string, (doc: Document) => unknown> = {},\r\n\t): WritableSignal<Document>[] {\r\n\t\treturn arr.map(obj => this.toSignal(obj, signalFields));\r\n\t}\r\n\r\n\t/**\r\n\t * Adds a new object to the signals array.\r\n\t * Optionally wraps specific fields of the object as individual signals before wrapping the whole object.\r\n\t *\r\n\t * @template Document - The type of the object being added.\r\n\t * @param {WritableSignal<Document>[]} signals - The signals array to append to.\r\n\t * @param {Document} item - The object to wrap and push as a signal.\r\n\t * @param {Record<string, (doc: Document) => unknown>} [signalFields={}] -\r\n\t * Optional map of fields to be wrapped as signals within the object.\r\n\t *\r\n\t * @returns {void}\r\n\t */\r\n\tpushSignal<Document>(\r\n\t\tsignals: WritableSignal<Document>[],\r\n\t\titem: Document,\r\n\t\tsignalFields: Record<string, (doc: Document) => unknown> = {},\r\n\t): void {\r\n\t\tsignals.push(this.toSignal(item, signalFields));\r\n\t}\r\n\r\n\t/**\r\n\t * Removes the first signal from the array whose object's field matches the provided value.\r\n\t * @template Document\r\n\t * @param {WritableSignal<Document>[]} signals - The signals array to modify.\r\n\t * @param {unknown} value - The value to match.\r\n\t * @param {string} [field='_id'] - The object field to match against.\r\n\t * @returns {void}\r\n\t */\r\n\tremoveSignalByField<Document extends Record<string, unknown>>(\r\n\t\tsignals: WritableSignal<Document>[],\r\n\t\tvalue: unknown,\r\n\t\tfield: string = '_id',\r\n\t): void {\r\n\t\tconst idx = signals.findIndex(sig => sig()[field] === value);\r\n\r\n\t\tif (idx > -1) signals.splice(idx, 1);\r\n\t}\r\n\r\n\t/**\r\n\t * Returns a generic trackBy function for *ngFor, tracking by the specified object field.\r\n\t * @template Document\r\n\t * @param {string} field - The object field to use for tracking (e.g., '_id').\r\n\t * @returns {(index: number, sig: Signal<Document>) => unknown} TrackBy function for Angular.\r\n\t */\r\n\ttrackBySignalField<Document extends Record<string, unknown>>(field: string) {\r\n\t\treturn (_: number, sig: Signal<Document>) => sig()[field];\r\n\t}\r\n\r\n\t/**\r\n\t * Finds the first signal in the array whose object's field matches the provided value.\r\n\t * @template Document\r\n\t * @param {Signal<Document>[]} signals - Array of signals to search.\r\n\t * @param {unknown} value - The value to match.\r\n\t * @param {string} [field='_id'] - The object field to match against.\r\n\t * @returns {Signal<Document> | undefined} The found signal or undefined if not found.\r\n\t */\r\n\tfindSignalByField<Document extends Record<string, unknown>>(\r\n\t\tsignals: WritableSignal<Document>[],\r\n\t\tvalue: unknown,\r\n\t\tfield = '_id',\r\n\t): WritableSignal<Document> | undefined {\r\n\t\treturn signals.find(sig => sig()[field] === value) as WritableSignal<Document>;\r\n\t}\r\n\r\n\t/**\r\n\t * Updates the first writable signal in the array whose object's field matches the provided value.\r\n\t * @template Document\r\n\t * @param {WritableSignal<Document>[]} signals - Array of writable signals to search.\r\n\t * @param {unknown} value - The value to match.\r\n\t * @param {(val: Document) => Document} updater - Function to produce the updated object.\r\n\t * @param {string} field - The object field to match against.\r\n\t * @returns {void}\r\n\t */\r\n\tupdateSignalByField<Document extends Record<string, unknown>>(\r\n\t\tsignals: WritableSignal<Document>[],\r\n\t\tvalue: unknown,\r\n\t\tupdater: (val: Document) => Document,\r\n\t\tfield: string,\r\n\t): void {\r\n\t\tconst sig = this.findSignalByField<Document>(\r\n\t\t\tsignals,\r\n\t\t\tvalue,\r\n\t\t\tfield,\r\n\t\t) as WritableSignal<Document>;\r\n\r\n\t\tif (sig) sig.update(updater);\r\n\t}\r\n}\r\n","import { ChangeDetectorRef, inject, Signal, signal, WritableSignal } from '@angular/core';\r\nimport { firstValueFrom, take } from 'rxjs';\r\nimport { CoreService } from '../core/core.service';\r\nimport { CrudDocument, CrudOptions, CrudServiceInterface, TableConfig } from './crud.interface';\r\n\r\n/**\r\n * Interface representing the shape of a form service used by the CrudComponent.\r\n * The consuming app must provide a service that implements this structure.\r\n */\r\ninterface FormServiceInterface<FormInterface> {\r\n\tmodal: <T>(\r\n\t\tform: FormInterface,\r\n\t\tbuttons?: unknown | unknown[],\r\n\t\tsubmition?: unknown,\r\n\t\tchange?: (update: T) => void | Promise<(update: T) => void>,\r\n\t\tmodalOptions?: unknown,\r\n\t) => Promise<T>;\r\n\tmodalDocs: <T>(docs: T[]) => Promise<T[]>;\r\n\tmodalUnique: <T>(collection: string, key: string, doc: T) => void;\r\n}\r\n\r\n/**\r\n * Abstract reusable base class for CRUD list views.\r\n * It encapsulates pagination, modals, and document handling logic.\r\n *\r\n * @template Service - A service implementing CrudServiceInterface for a specific document type\r\n * @template Document - The data model extending CrudDocument\r\n */\r\nexport abstract class CrudComponent<\r\n\tService extends CrudServiceInterface<Document>,\r\n\tDocument extends CrudDocument<Document>,\r\n\tFormInterface,\r\n> {\r\n\t/** Service responsible for data fetching, creating, updating, deleting */\r\n\tprotected crudService: Service;\r\n\r\n\t/** The array of documents currently loaded and shown */\r\n\tprotected documents = signal<Signal<Document>[]>([]);\r\n\r\n\t/** Form schema/config used by the FormService modals */\r\n\tprotected form: FormInterface;\r\n\r\n\t/** Current pagination page */\r\n\tprotected page = 1;\r\n\r\n\t/** CoreService handles timing and copying helpers */\r\n\tprivate __core = inject(CoreService);\r\n\r\n\t/** ChangeDetectorRef handles on push strategy */\r\n\tprivate __cdr = inject(ChangeDetectorRef);\r\n\r\n\t/** Internal reference to form service matching FormServiceInterface */\r\n\tprivate __formService: FormServiceInterface<FormInterface>;\r\n\r\n\t/**\r\n\t * Constructor\r\n\t *\r\n\t * @param formConfig - Object describing form title and its component structure\r\n\t * @param formService - Any service that conforms to FormServiceInterface (usually casted)\r\n\t * @param crudService - CRUD service implementing get/create/update/delete\r\n\t */\r\n\tconstructor(formConfig: unknown, formService: unknown, crudService: Service, module = '') {\r\n\t\tconst form = formConfig as FormInterface;\r\n\r\n\t\tthis.__formService = formService as FormServiceInterface<FormInterface>;\r\n\r\n\t\tthis.form = form;\r\n\r\n\t\tthis.crudService = crudService;\r\n\r\n\t\tthis._module = module;\r\n\t}\r\n\r\n\tprotected localDocumentsFilter: (doc: Document) => boolean = () => true;\r\n\r\n\t/**\r\n\t * Allow set query customization\r\n\t */\r\n\tprotected setDocumentsQuery(query: string) {\r\n\t\treturn query;\r\n\t}\r\n\r\n\t/**\r\n\t * Loads documents for a given page.\r\n\t */\r\n\tprotected setDocuments(page = this.page, query = ''): Promise<void> {\r\n\t\tquery = this.setDocumentsQuery(query);\r\n\r\n\t\treturn new Promise(resolve => {\r\n\t\t\tif (this.configType === 'server') {\r\n\t\t\t\tthis.page = page;\r\n\r\n\t\t\t\tthis.__core.afterWhile(\r\n\t\t\t\t\tthis,\r\n\t\t\t\t\t() => {\r\n\t\t\t\t\t\tthis.crudService\r\n\t\t\t\t\t\t\t.get({ page, query }, this.getOptions())\r\n\t\t\t\t\t\t\t.subscribe((docs: Document[]) => {\r\n\t\t\t\t\t\t\t\tthis.documents.update(() =>\r\n\t\t\t\t\t\t\t\t\t(docs || []).map(doc => this.crudService.getSignal(doc)),\r\n\t\t\t\t\t\t\t\t);\r\n\r\n\t\t\t\t\t\t\t\tresolve();\r\n\r\n\t\t\t\t\t\t\t\tthis.__cdr.markForCheck();\r\n\t\t\t\t\t\t\t});\r\n\t\t\t\t\t},\r\n\t\t\t\t\t250,\r\n\t\t\t\t);\r\n\t\t\t} else {\r\n\t\t\t\tthis.documents.update(() =>\r\n\t\t\t\t\tthis.crudService\r\n\t\t\t\t\t\t.getDocs()\r\n\t\t\t\t\t\t.filter(this.localDocumentsFilter)\r\n\t\t\t\t\t\t.map(doc => this.crudService.getSignal(doc)),\r\n\t\t\t\t);\r\n\r\n\t\t\t\tthis.crudService.loaded.pipe(take(1)).subscribe(() => {\r\n\t\t\t\t\tresolve();\r\n\r\n\t\t\t\t\tthis.__cdr.markForCheck();\r\n\t\t\t\t});\r\n\t\t\t}\r\n\t\t});\r\n\t}\r\n\r\n\t/** Fields considered when performing bulk updates. */\r\n\tprotected updatableFields = ['_id', 'name', 'description', 'data'];\r\n\r\n\t/**\r\n\t * Clears temporary metadata before document creation.\r\n\t */\r\n\tprotected preCreate(doc: Document): void {\r\n\t\tdelete doc.__creating;\r\n\t}\r\n\r\n\t/**\r\n\t * Funciton which controls whether the create functionality is available.\r\n\t */\r\n\tprotected allowCreate(): boolean {\r\n\t\treturn true;\r\n\t}\r\n\r\n\t/**\r\n\t * Funciton which controls whether the update and delete functionality is available.\r\n\t */\r\n\tprotected allowMutate(): boolean {\r\n\t\treturn true;\r\n\t}\r\n\r\n\t/**\r\n\t * Funciton which controls whether the unique url functionality is available.\r\n\t */\r\n\tprotected allowUrl(): boolean {\r\n\t\treturn true;\r\n\t}\r\n\r\n\t/** Determines whether manual sorting controls are available. */\r\n\tprotected allowSort(): boolean {\r\n\t\treturn false;\r\n\t}\r\n\r\n\t/**\r\n\t * Funciton which prepare get crud options.\r\n\t */\r\n\tprotected getOptions(): CrudOptions<Document> {\r\n\t\treturn {} as CrudOptions<Document>;\r\n\t}\r\n\r\n\t/**\r\n\t * Handles bulk creation and updating of documents.\r\n\t * In creation mode, adds new documents.\r\n\t * In update mode, syncs changes and deletes removed entries.\r\n\t */\r\n\tprotected bulkManagement(isCreateFlow = true): () => void {\r\n\t\treturn (): void => {\r\n\t\t\tthis.__formService\r\n\t\t\t\t.modalDocs<Document>(\r\n\t\t\t\t\tisCreateFlow\r\n\t\t\t\t\t\t? []\r\n\t\t\t\t\t\t: this.documents().map(\r\n\t\t\t\t\t\t\t\t(obj: any) =>\r\n\t\t\t\t\t\t\t\t\tObject.fromEntries(\r\n\t\t\t\t\t\t\t\t\t\tthis.updatableFields.map(key => [key, obj()[key]]),\r\n\t\t\t\t\t\t\t\t\t) as Document,\r\n\t\t\t\t\t\t\t),\r\n\t\t\t\t)\r\n\t\t\t\t.then(async (docs: Document[]) => {\r\n\t\t\t\t\tif (isCreateFlow) {\r\n\t\t\t\t\t\tfor (const doc of docs) {\r\n\t\t\t\t\t\t\tthis.preCreate(doc);\r\n\r\n\t\t\t\t\t\t\tawait firstValueFrom(this.crudService.create(doc));\r\n\t\t\t\t\t\t}\r\n\t\t\t\t\t} else {\r\n\t\t\t\t\t\tfor (const document of this.documents()) {\r\n\t\t\t\t\t\t\tif (!docs.find(d => d._id === document()._id)) {\r\n\t\t\t\t\t\t\t\tawait firstValueFrom(this.crudService.delete(document()));\r\n\t\t\t\t\t\t\t}\r\n\t\t\t\t\t\t}\r\n\r\n\t\t\t\t\t\tfor (const doc of docs) {\r\n\t\t\t\t\t\t\tconst local = this.documents().find(\r\n\t\t\t\t\t\t\t\tdocument => document()._id === doc._id,\r\n\t\t\t\t\t\t\t);\r\n\r\n\t\t\t\t\t\t\tif (local) {\r\n\t\t\t\t\t\t\t\t(local as WritableSignal<Document>).update(document => {\r\n\t\t\t\t\t\t\t\t\tthis.__core.copy(doc, document);\r\n\r\n\t\t\t\t\t\t\t\t\treturn document;\r\n\t\t\t\t\t\t\t\t});\r\n\r\n\t\t\t\t\t\t\t\tawait firstValueFrom(this.crudService.update(local()));\r\n\t\t\t\t\t\t\t} else {\r\n\t\t\t\t\t\t\t\tthis.preCreate(doc);\r\n\r\n\t\t\t\t\t\t\t\tawait firstValueFrom(this.crudService.create(doc));\r\n\t\t\t\t\t\t\t}\r\n\t\t\t\t\t\t}\r\n\t\t\t\t\t}\r\n\r\n\t\t\t\t\tthis.setDocuments();\r\n\t\t\t\t});\r\n\t\t};\r\n\t}\r\n\r\n\t/** Opens a modal to create a new document. */\r\n\tprotected create() {\r\n\t\tthis.__formService.modal<Document>(\r\n\t\t\tthis.form,\r\n\t\t\t{\r\n\t\t\t\tlabel: 'Create',\r\n\t\t\t\tclick: async (created: unknown, close: () => void) => {\r\n\t\t\t\t\tclose();\r\n\r\n\t\t\t\t\tthis.preCreate(created as Document);\r\n\r\n\t\t\t\t\tawait firstValueFrom(this.crudService.create(created as Document));\r\n\r\n\t\t\t\t\tthis.setDocuments();\r\n\t\t\t\t},\r\n\t\t\t},\r\n\t\t\t{ data: {} },\r\n\t\t\t() => {},\r\n\t\t\t{\r\n\t\t\t\tresetOnSubmit: true,\r\n\t\t\t},\r\n\t\t);\r\n\t}\r\n\r\n\t/** Displays a modal to edit an existing document. */\r\n\tprotected update(doc: Document) {\r\n\t\tthis.__formService.modal<Document>(\r\n\t\t\tthis.form,\r\n\t\t\t{\r\n\t\t\t\tlabel: 'Update',\r\n\t\t\t\tclick: (updated: unknown, close: () => void) => {\r\n\t\t\t\t\tclose();\r\n\r\n\t\t\t\t\tthis.__core.copy(updated, doc);\r\n\r\n\t\t\t\t\tthis.crudService.update(doc);\r\n\r\n\t\t\t\t\tthis.__cdr.markForCheck();\r\n\t\t\t\t},\r\n\t\t\t},\r\n\t\t\tdoc,\r\n\t\t);\r\n\t}\r\n\r\n\t/** Requests confirmation before deleting the provided document. */\r\n\tprotected async delete(doc: Document) {\r\n\t\tthis.crudService.delete(doc).subscribe(() => {\r\n\t\t\tthis.setDocuments();\r\n\t\t});\r\n\t}\r\n\r\n\t/** Opens a modal to edit the document's unique URL. */\r\n\tprotected mutateUrl(doc: Document) {\r\n\t\tthis.__formService.modalUnique<Document>(this._module, 'url', doc);\r\n\t}\r\n\r\n\t/** Moves the given document one position up and updates ordering. */\r\n\tprotected moveUp(doc: Document) {\r\n\t\tconst index = this.documents().findIndex(document => document()._id === doc._id);\r\n\r\n\t\tif (index) {\r\n\t\t\tthis.documents.update(documents => {\r\n\t\t\t\tdocuments.splice(index, 1);\r\n\r\n\t\t\t\tdocuments.splice(index - 1, 0, this.crudService.getSignal(doc));\r\n\r\n\t\t\t\treturn documents;\r\n\t\t\t});\r\n\t\t}\r\n\r\n\t\tfor (let i = 0; i < this.documents().length; i++) {\r\n\t\t\tif (this.documents()[i]().order !== i) {\r\n\t\t\t\tthis.documents()[i]().order = i;\r\n\r\n\t\t\t\tthis.crudService.update(this.documents()[i]());\r\n\t\t\t}\r\n\t\t}\r\n\r\n\t\tthis.__cdr.markForCheck();\r\n\t}\r\n\r\n\t/** Data source mode used for document retrieval. */\r\n\tprotected configType: 'server' | 'local' = 'server';\r\n\r\n\t/** Number of documents fetched per page when paginating. */\r\n\tprotected perPage = 20;\r\n\r\n\t/**\r\n\t * Configuration object used by the UI for rendering table and handling actions.\r\n\t */\r\n\tprotected getConfig(): TableConfig<Document> {\r\n\t\tconst config: TableConfig<Document> = {\r\n\t\t\tcreate: this.allowCreate()\r\n\t\t\t\t? (): void => {\r\n\t\t\t\t\t\tthis.create();\r\n\t\t\t\t\t}\r\n\t\t\t\t: null,\r\n\r\n\t\t\tupdate: this.allowMutate()\r\n\t\t\t\t? (doc: Document): void => {\r\n\t\t\t\t\t\tthis.update(doc);\r\n\t\t\t\t\t}\r\n\t\t\t\t: null,\r\n\r\n\t\t\tdelete: this.allowMutate()\r\n\t\t\t\t? (doc: Document): void => {\r\n\t\t\t\t\t\tthis.delete(doc);\r\n\t\t\t\t\t}\r\n\t\t\t\t: null,\r\n\r\n\t\t\tbuttons: [],\r\n\t\t\theaderButtons: [],\r\n\t\t\tallDocs: true,\r\n\t\t};\r\n\r\n\t\tif (this.allowUrl()) {\r\n\t\t\tconfig.buttons.push({\r\n\t\t\t\ticon: 'cloud_download',\r\n\t\t\t\tclick: (doc: Document) => {\r\n\t\t\t\t\tthis.mutateUrl(doc);\r\n\t\t\t\t},\r\n\t\t\t});\r\n\t\t}\r\n\r\n\t\tif (this.allowSort()) {\r\n\t\t\tconfig.buttons.push({\r\n\t\t\t\ticon: 'arrow_upward',\r\n\t\t\t\tclick: (doc: Document) => {\r\n\t\t\t\t\tthis.moveUp(doc);\r\n\t\t\t\t},\r\n\t\t\t});\r\n\t\t}\r\n\r\n\t\tif (this.allowCreate()) {\r\n\t\t\tconfig.headerButtons.push({\r\n\t\t\t\ticon: 'playlist_add',\r\n\t\t\t\tclick: this.bulkManagement(),\r\n\t\t\t\tclass: 'playlist',\r\n\t\t\t});\r\n\t\t}\r\n\t\tif (this.allowMutate()) {\r\n\t\t\tconfig.headerButtons.push({\r\n\t\t\t\ticon: 'edit_note',\r\n\t\t\t\tclick: this.bulkManagement(false),\r\n\t\t\t\tclass: 'edit',\r\n\t\t\t});\r\n\t\t}\r\n\r\n\t\treturn this.configType === 'server'\r\n\t\t\t? {\r\n\t\t\t\t\t...config,\r\n\t\t\t\t\tpaginate: this.setDocuments.bind(this),\r\n\t\t\t\t\tperPage: this.perPage,\r\n\t\t\t\t\tsetPerPage: this.crudService.setPerPage?.bind(this.crudService),\r\n\t\t\t\t\tallDocs: false,\r\n\t\t\t\t}\r\n\t\t\t: config;\r\n\t}\r\n\r\n\t/** Name of the collection or module used for contextual actions. */\r\n\tprivate _module = '';\r\n}\r\n","import { Injectable, WritableSignal, signal } from '@angular/core';\r\nimport { toObservable } from '@angular/core/rxjs-interop';\r\nimport {\r\n\tObservable,\r\n\tSubject,\r\n\tcombineLatest,\r\n\tfilter,\r\n\tmap,\r\n\tmerge,\r\n\tshare,\r\n\tskip,\r\n\ttake,\r\n\ttakeUntil,\r\n\ttimeout,\r\n} from 'rxjs';\r\n\r\ntype Any = unknown;\r\n\r\n@Injectable({ providedIn: 'root' })\r\nexport class EmitterService {\r\n\tprivate _signals = new Map<string, WritableSignal<Any>>();\r\n\tprivate _closers = new Map<string, Subject<void>>();\r\n\tprivate _streams = new Map<string, Observable<Any>>();\r\n\r\n\tprivate _getSignal(id: string): WritableSignal<Any> {\r\n\t\tlet s = this._signals.get(id);\r\n\t\tif (!s) {\r\n\t\t\t// emit even if same payload repeats\r\n\t\t\ts = signal<Any>(undefined, { equal: () => false });\r\n\t\t\tthis._signals.set(id, s);\r\n\t\t}\r\n\t\treturn s;\r\n\t}\r\n\r\n\tprivate _getCloser(id: string): Subject<void> {\r\n\t\tlet c = this._closers.get(id);\r\n\t\tif (!c) {\r\n\t\t\tc = new Subject<void>();\r\n\t\t\tthis._closers.set(id, c);\r\n\t\t}\r\n\t\treturn c;\r\n\t}\r\n\r\n\tprivate _getStream(id: string): Observable<Any> {\r\n\t\tlet obs$ = this._streams.get(id);\r\n\t\tif (!obs$) {\r\n\t\t\tconst sig = this._getSignal(id);\r\n\t\t\tconst closed$ = this._getCloser(id);\r\n\t\t\tobs$ = toObservable(sig).pipe(\r\n\t\t\t\t// Subject-like: don't replay the current value on subscribe\r\n\t\t\t\tskip(1),\r\n\t\t\t\ttakeUntil(closed$),\r\n\t\t\t\tshare(),\r\n\t\t\t);\r\n\t\t\tthis._streams.set(id, obs$);\r\n\t\t}\r\n\t\treturn obs$;\r\n\t}\r\n\r\n\t/** Emit an event */\r\n\temit<T = Any>(id: string, data?: T): void {\r\n\t\tthis._getSignal(id).set(data as Any);\r\n\t}\r\n\r\n\t/** Listen for events (hot, completes when off(id) is called) */\r\n\ton<T = Any>(id: string): Observable<T> {\r\n\t\treturn this._getStream(id) as Observable<T>;\r\n\t}\r\n\r\n\t/** Complete and remove a channel */\r\n\toff(id: string): void {\r\n\t\tconst closer = this._closers.get(id);\r\n\t\tif (closer) {\r\n\t\t\tcloser.next();\r\n\t\t\tcloser.complete();\r\n\t\t\tthis._closers.delete(id);\r\n\t\t}\r\n\t\tthis._signals.delete(id);\r\n\t\tthis._streams.delete(id);\r\n\t}\r\n\r\n\toffAll(): void {\r\n\t\tfor (const id of Array.from(this._closers.keys())) this.off(id);\r\n\t}\r\n\r\n\thas(id: string): boolean {\r\n\t\treturn this._signals.has(id);\r\n\t}\r\n\r\n\tprivate _done = new Map<string, WritableSignal<Any | undefined>>();\r\n\r\n\tprivate _getDoneSignal(id: string): WritableSignal<Any | undefined> {\r\n\t\tlet s = this._done.get(id);\r\n\t\tif (!s) {\r\n\t\t\ts = signal<Any | undefined>(undefined);\r\n\t\t\tthis._done.set(id, s);\r\n\t\t}\r\n\t\treturn s;\r\n\t}\r\n\r\n\t/** Mark task as completed with a payload (default: true) */\r\n\tcomplete<T = Any>(task: string, value: T = true as unknown as T): void {\r\n\t\tthis._getDoneSignal(task).set(value as Any);\r\n\t}\r\n\r\n\t/** Clear completion so it can be awaited again */\r\n\tclearCompleted(task: string): void {\r\n\t\tconst s = this._done.get(task) ?? this._getDoneSignal(task);\r\n\t\ts.set(undefined);\r\n\t}\r\n\r\n\t/** Read current completion payload (undefined => not completed) */\r\n\tcompleted(task: string): Any | undefined {\r\n\t\treturn this._getDoneSignal(task)();\r\n\t}\r\n\r\n\tisCompleted(task: string): boolean {\r\n\t\treturn this._getDoneSignal(task)() !== undefined;\r\n\t}\r\n\r\n\tonComplete(\r\n\t\ttasks: string | string[],\r\n\t\topts?: {\r\n\t\t\tmode?: 'all' | 'any';\r\n\t\t\ttimeoutMs?: number;\r\n\t\t\tabort?: AbortSignal;\r\n\t\t},\r\n\t): Observable<Any | Any[]> {\r\n\t\tconst list = (Array.isArray(tasks) ? tasks : [tasks]).filter(Boolean);\r\n\t\tconst streams = list.map(id =>\r\n\t\t\ttoObservable(this._getDoneSignal(id)).pipe(\r\n\t\t\t\tfilter((v): v is Any => v !== undefined),\r\n\t\t\t\tmap(v => v as Any),\r\n\t\t\t),\r\n\t\t);\r\n\r\n\t\tlet source$: Observable<Any | Any[]>;\r\n\r\n\t\tif (list.length <= 1) {\r\n\t\t\t// single-task await\r\n\t\t\tsource$ = streams[0]?.pipe(take(1)) ?? new Observable<never>();\r\n\t\t} else if (opts?.mode === 'any') {\r\n\t\t\tsource$ = merge(...streams).pipe(take(1));\r\n\t\t} else {\r\n\t\t\tsource$ = combineLatest(streams).pipe(take(1));\r\n\t\t}\r\n\r\n\t\tif (opts?.timeoutMs && Number.isFinite(opts.timeoutMs)) {\r\n\t\t\tsource$ = source$.pipe(timeout({ first: opts.timeoutMs }));\r\n\t\t}\r\n\r\n\t\tif (opts?.abort) {\r\n\t\t\tconst abort$ = new Observable<void>(sub => {\r\n\t\t\t\tconst handler = () => {\r\n\t\t\t\t\tsub.next();\r\n\t\t\t\t\tsub.complete();\r\n\t\t\t\t};\r\n\t\t\t\topts.abort!.addEventListener('abort', handler);\r\n\t\t\t\treturn () => opts.abort!.removeEventListener('abort', handler);\r\n\t\t\t});\r\n\t\t\tsource$ = source$.pipe(takeUntil(abort$));\r\n\t\t}\r\n\r\n\t\treturn source$;\r\n\t}\r\n}\r\n","import { InjectionToken } from '@angular/core';\r\nimport { MetaConfig } from '../meta/meta.interface';\r\nimport { StoreConfig } from '../store/store.interface';\r\nimport { HttpConfig } from './http.interface';\r\nimport { NetworkConfig } from './network.interface';\r\n\r\n/**\r\n * Root configuration object used to initialize the library.\r\n * Each property allows consumers to override the default\r\n * behavior of the corresponding service.\r\n */\r\nexport interface Config {\r\n\t/** Options for the key‑value storage service. */\r\n\tstore?: StoreConfig;\r\n\t/** Defaults applied to page metadata handling. */\r\n\tmeta?: MetaConfig;\r\n\t/** Base HTTP settings such as API URL and headers. */\r\n\thttp?: HttpConfig;\r\n\t/** Optional socket connection configuration. */\r\n\tsocket?: any;\r\n\t/** Raw Socket.IO client instance, if used. */\r\n\tio?: any;\r\n\tnetwork?: NetworkConfig;\r\n}\r\n\r\nexport const CONFIG_TOKEN = new InjectionToken<Config>('config');\r\n\r\nexport const DEFAULT_CONFIG: Config = {\r\n\tstore: {\r\n\t\tprefix: 'waStore',\r\n\t},\r\n\tmeta: {\r\n\t\tuseTitleSuffix: false,\r\n\t\tdefaults: { links: {} },\r\n\t},\r\n\tsocket: false,\r\n\thttp: {\r\n\t\turl: '',\r\n\t\theaders: {},\r\n\t},\r\n};\r\n","export type HttpHeaderType = string | number | (string | number)[];\r\n\r\n/**\r\n * Configuration values used by the HTTP service when\r\n * issuing requests to a backend API.\r\n */\r\nexport interface HttpConfig {\r\n\t/** Map of default headers appended to each request. */\r\n\theaders?: Record<string, HttpHeaderType>;\r\n\t/** Base URL for all HTTP requests. */\r\n\turl?: string;\r\n}\r\n\r\nexport const DEFAULT_HTTP_CONFIG: HttpConfig = {\r\n\theaders: {},\r\n\turl: '',\r\n};\r\n","import { isPlatformBrowser } from '@angular/common';\r\nimport { HttpClient, HttpErrorResponse, HttpHeaders } from '@angular/common/http';\r\nimport { Inject, Injectable, Optional, PLATFORM_ID, inject } from '@angular/core';\r\nimport { EMPTY, Observable, ReplaySubject } from 'rxjs';\r\nimport { catchError, first } from 'rxjs/operators';\r\nimport { CONFIG_TOKEN, Config } from '../interfaces/config.interface';\r\nimport { DEFAULT_HTTP_CONFIG, HttpConfig, HttpHeaderType } from '../interfaces/http.interface';\r\n\r\n@Injectable({\r\n\tprovidedIn: 'root',\r\n})\r\nexport class HttpService {\r\n\tprivate readonly _isBrowser = isPlatformBrowser(inject(PLATFORM_ID));\r\n\r\n\t// An array of error handling callbacks\r\n\terrors: ((err: HttpErrorResponse, retry?: () => void) => {})[] = [];\r\n\r\n\t// Base URL for HTTP requests\r\n\turl = '';\r\n\r\n\t// Flag to lock the service to prevent multiple requests\r\n\tlocked = false;\r\n\r\n\t// Array to store setTimeout IDs for managing request locks\r\n\tawaitLocked: ReturnType<typeof setTimeout>[] = [];\r\n\r\n\t// Configuration object for HTTP settings\r\n\tprivate _config: HttpConfig;\r\n\r\n\t// Object to store HTTP headers\r\n\tprivate _headers: {\r\n\t\t[name: string]: HttpHeaderType;\r\n\t} = {};\r\n\r\n\t// Instance of HttpHeaders with current headers\r\n\tprivate _http_headers = new HttpHeaders(this._headers);\r\n\r\n\tconstructor(\r\n\t\t@Inject(CONFIG_TOKEN) @Optional() config: Config,\r\n\t\tprivate _http: HttpClient,\r\n\t) {\r\n\t\t// Initialize HTTP configuration and headers from injected config\r\n\t\tthis._config = {\r\n\t\t\t...DEFAULT_HTTP_CONFIG,\r\n\t\t\t...(config.http || {}),\r\n\t\t};\r\n\r\n\t\tif (typeof this._config.url === 'string') {\r\n\t\t\tthis.setUrl(this._config.url);\r\n\t\t}\r\n\r\n\t\tif (this._isBrowser) {\r\n\t\t\tthis.url = localStorage.getItem('wacom-http.url') || this.url;\r\n\r\n\t\t\tconst raw = localStorage.getItem('wacom-http.headers');\r\n\t\t\tthis._headers = raw ? JSON.parse(raw) : this._headers;\r\n\t\t\tthis._http_headers = new HttpHeaders(this._headers);\r\n\t\t}\r\n\r\n\t\tif (typeof this._config.headers === 'object') {\r\n\t\t\tfor (const header in this._config.headers) {\r\n\t\t\t\tthis._headers[header] = this._config.headers[header];\r\n\t\t\t}\r\n\r\n\t\t\tthis._http_headers = new HttpHeaders(this._headers);\r\n\t\t}\r\n\t}\r\n\r\n\t// Set a new base URL and save it in the store\r\n\tsetUrl(url: string) {\r\n\t\tthis.url = url;\r\n\r\n\t\tif (this._isBrowser) {\r\n\t\t\tlocalStorage.setItem('wacom-http.url', url);\r\n\t\t}\r\n\t}\r\n\r\n\t// Remove the base URL and revert to the default or stored one\r\n\tremoveUrl() {\r\n\t\tthis.url = this._config.url || '';\r\n\r\n\t\tif (this._isBrowser) {\r\n\t\t\tlocalStorage.removeItem('wacom-http.url');\r\n\t\t}\r\n\t}\r\n\r\n\t// Set a new HTTP header and update the stored headers\r\n\tset(key: any, value: any) {\r\n\t\tthis._headers[key] = value;\r\n\r\n\t\tif (this._isBrowser) {\r\n\t\t\tlocalStorage.setItem('wacom-http.headers', JSON.stringify(this._headers));\r\n\t\t}\r\n\r\n\t\tthis._http_headers = new HttpHeaders(this._headers);\r\n\t}\r\n\r\n\t// Get the value of a specific HTTP header\r\n\theader(key: any) {\r\n\t\treturn this._headers[key];\r\n\t}\r\n\r\n\t// Remove a specific HTTP header and update the stored headers\r\n\tremove(key: any) {\r\n\t\tdelete this._headers[key];\r\n\r\n\t\tif (this._isBrowser) {\r\n\t\t\tlocalStorage.setItem('wacom-http.headers', JSON.stringify(this._headers));\r\n\t\t}\r\n\r\n\t\tthis._http_headers = new HttpHeaders(this._headers);\r\n\t}\r\n\r\n\t// Internal method to make HTTP requests based on the method type\r\n\tprivate _httpMethod(method: string, _url: string, doc: unknown, headers: any): Observable<any> {\r\n\t\tif (method === 'post') {\r\n\t\t\treturn this._http.post<any>(_url, doc, headers);\r\n\t\t} else if (method === 'put') {\r\n\t\t\treturn this._http.put<any>(_url, doc, headers);\r\n\t\t} else if (method === 'patch') {\r\n\t\t\treturn this._http.patch<any>(_url, doc, headers);\r\n\t\t} else if (method === 'delete') {\r\n\t\t\treturn this._http.delete<any>(_url, headers);\r\n\t\t} else {\r\n\t\t\treturn this._http.get<any>(_url, headers);\r\n\t\t}\r\n\t}\r\n\r\n\t/**\r\n\t * Internal method to handle HTTP requests for various methods (POST, PUT, PATCH, DELETE, GET).\r\n\t *\r\n\t * Features:\r\n\t * - **Request Locking**: Manages request locking to prevent simultaneous requests.\r\n\t * - **Acceptance Check**: Validates the server response against a user-defined `acceptance` function.\r\n\t * If the check fails, the response is rejected with an error.\r\n\t * - **Replace Logic**: Allows modification of specific parts of the response object, determined by a user-defined `replace` function.\r\n\t * Can handle both objects and arrays within the response.\r\n\t * - **Field Filtering**: Supports extracting specific fields from response objects or arrays.\r\n\t * - **Legacy Support**: Compatible with callback-based usage alongside Observables.\r\n\t * - **ReplaySubject**: Ensures that the response can be shared across multiple subscribers.\r\n\t *\r\n\t * @param url - The endpoint to send the HTTP request to (relative to the base URL).\r\n\t * @param doc - The request payload for methods like POST, PUT, and PATCH.\r\n\t * @param callback - A legacy callback function to handle the response.\r\n\t * @param opts - Additional options:\r\n\t * - `err`: Error handling callback.\r\n\t * - `acceptance`: Function to validate the server response. Should return `true` for valid responses.\r\n\t * - `replace`: Function to modify specific parts of the response data.\r\n\t * - `fields`: Array of fields to extract from the response object(s).\r\n\t * - `data`: Path in the response where the data resides for `replace` and `fields` operations.\r\n\t * - `skipLock`: If `true`, bypasses request locking.\r\n\t * - `url`: Overrides the base URL for this request.\r\n\t * @param method - The HTTP method (e.g., 'post', 'put', 'patch', 'delete', 'get').\r\n\t * @returns An Observable that emits the processed HTTP response or an error.\r\n\t */\r\n\tprivate _post(\r\n\t\turl: string,\r\n\t\tdoc: unknown,\r\n\t\tcallback = (resp: unknown) => {},\r\n\t\topts: any = {},\r\n\t\tmethod = 'post',\r\n\t): Observable<any> {\r\n\t\tif (typeof opts === 'function') {\r\n\t\t\topts = { err: opts };\r\n\t\t}\r\n\r\n\t\tif (!opts.err) {\r\n\t\t\topts.err = (err: HttpErrorResponse) => {};\r\n\t\t}\r\n\r\n\t\t// Handle request locking to avoid multiple simultaneous requests\r\n\t\tif (this.locked && !opts.skipLock) {\r\n\t\t\treturn new Observable(observer => {\r\n\t\t\t\tconst wait = setTimeout(() => {\r\n\t\t\t\t\tthis._post(url, doc, callback, opts, method).subscribe(observer);\r\n\t\t\t\t}, 100);\r\n\r\n\t\t\t\tthis.awaitLocked.push(wait);\r\n\t\t\t});\r\n\t\t}\r\n\r\n\t\tconst _url = (opts.url || this.url) + url;\r\n\r\n\t\tthis.prepare_handle(_url, doc);\r\n\r\n\t\t// Using ReplaySubject to allow multiple subscriptions without re-triggering the HTTP request\r\n\t\tconst responseSubject = new ReplaySubject(1);\r\n\r\n\t\tthis._httpMethod(method, _url, doc, { headers: this._http_headers })\r\n\t\t\t.pipe(\r\n\t\t\t\tfirst(),\r\n\t\t\t\tcatchError((error: HttpErrorResponse) => {\r\n\t\t\t\t\tthis.handleError(opts.err, () => {\r\n\t\t\t\t\t\tthis._post(url, doc, callback, opts, method).subscribe(responseSubject);\r\n\t\t\t\t\t})(error);\r\n\r\n\t\t\t\t\tresponseSubject.error(error);\r\n\r\n\t\t\t\t\treturn EMPTY;\r\n\t\t\t\t}),\r\n\t\t\t)\r\n\t\t\t.subscribe({\r\n\t\t\t\tnext: (resp: unknown) => {\r\n\t\t\t\t\tif (opts.acceptance && typeof opts.acceptance === 'function') {\r\n\t\t\t\t\t\tif (!opts.acceptance(resp)) {\r\n\t\t\t\t\t\t\tconst error = new HttpErrorResponse({\r\n\t\t\t\t\t\t\t\terror: 'Acceptance failed',\r\n\t\t\t\t\t\t\t\tstatus: 400,\r\n\t\t\t\t\t\t\t});\r\n\r\n\t\t\t\t\t\t\tthis.handleError(opts.err, () => {})(error);\r\n\r\n\t\t\t\t\t\t\tresponseSubject.error(error);\r\n\r\n\t\t\t\t\t\t\treturn;\r\n\t\t\t\t\t\t}\r\n\t\t\t\t\t}\r\n\r\n\t\t\t\t\tif (opts.replace && typeof opts.replace === 'function') {\r\n\t\t\t\t\t\tif (Array.isArray(this._getObjectToReplace(resp, opts.data))) {\r\n\t\t\t\t\t\t\t(this._getObjectToReplace(resp, opts.data) as Array<unknown>).map(\r\n\t\t\t\t\t\t\t\t(item: unknown) => opts.replace(item),\r\n\t\t\t\t\t\t\t);\r\n\t\t\t\t\t\t} else if (this._getObjectToReplace(resp, opts.data)) {\r\n\t\t\t\t\t\t\topts.replace(this._getObjectToReplace(resp, opts.data));\r\n\t\t\t\t\t\t}\r\n\t\t\t\t\t}\r\n\r\n\t\t\t\t\tif (Array.isArray(opts.fields)) {\r\n\t\t\t\t\t\tif (Array.isArray(this._getObjectToReplace(resp, opts.data))) {\r\n\t\t\t\t\t\t\t(this._getObjectToReplace(resp, opts.data) as Array<unknown>).map(\r\n\t\t\t\t\t\t\t\t(item: unknown) => {\r\n\t\t\t\t\t\t\t\t\treturn this._newDoc(item, opts.fields);\r\n\t\t\t\t\t\t\t\t},\r\n\t\t\t\t\t\t\t);\r\n\t\t\t\t\t\t} else if (this._getObjectToReplace(resp, opts.data)) {\r\n\t\t\t\t\t\t\tconst newDoc = this._newDoc(\r\n\t\t\t\t\t\t\t\tthis._getObjectToReplace(resp, opts.data),\r\n\t\t\t\t\t\t\t\topts.fields,\r\n\t\t\t\t\t\t\t);\r\n\r\n\t\t\t\t\t\t\tif (opts.data) {\r\n\t\t\t\t\t\t\t\tthis._setObjectToReplace(resp, opts.data, newDoc);\r\n\t\t\t\t\t\t\t} else {\r\n\t\t\t\t\t\t\t\tresp = newDoc;\r\n\t\t\t\t\t\t\t}\r\n\t\t\t\t\t\t}\r\n\t\t\t\t\t}\r\n\r\n\t\t\t\t\tthis.response_handle(_url, resp, () => callback(resp));\r\n\r\n\t\t\t\t\tresponseSubject.next(resp);\r\n\r\n\t\t\t\t\tresponseSubject.complete();\r\n\t\t\t\t},\r\n\t\t\t\terror: err => responseSubject.error(err),\r\n\t\t\t\tcomplete: () => responseSubject.complete(),\r\n\t\t\t});\r\n\r\n\t\treturn responseSubject.asObservable();\r\n\t}\r\n\r\n\t/**\r\n\t * Public method to perform a POST request.\r\n\t * - Supports legacy callback usage.\r\n\t * - Returns an Observable for reactive programming.\r\n\t */\r\n\tpost(url: string, doc: any, callback = (resp: any) => {}, opts: any = {}): Observable<any> {\r\n\t\treturn this._post(url, doc, callback, opts);\r\n\t}\r\n\r\n\t/**\r\n\t * Public method to perform a PUT request.\r\n\t * - Supports legacy callback usage.\r\n\t * - Returns an Observa