UNPKG

ngx-signal-hub

Version:

[![npm version](https://badge.fury.io/js/ngx-signal-hub.svg)](https://badge.fury.io/js/ngx-signal-hub) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)

276 lines (271 loc) 10.5 kB
import * as i0 from '@angular/core'; import { signal, effect, untracked, computed, Injectable } from '@angular/core'; class SignalHubService { injector; eventRegistry = signal(new Map(), { equal: (a, b) => a === b, }); subscribers = new Map(); constructor(injector) { this.injector = injector; } publish(key, data) { if (!key) throw new Error('Key cannot be empty'); const event = { key, data: data, timestamp: Date.now() }; this.eventRegistry.update((registry) => new Map(registry).set(key, event)); this.notifySubscribers(event); } on(options) { const { key, callback, destroyRef, replayLatest, onError, until } = options; if (!key) throw new Error('Key cannot be empty'); if (typeof callback !== 'function') throw new Error('Callback must be a function'); const unsubscribers = []; let mainSubscription = null; // Handle unsubscription conditions (from takeUntil) const unsubscribeAll = () => { unsubscribers.forEach((unsub) => unsub()); mainSubscription?.unsubscribe(); }; if (until) { const conditions = Array.isArray(until) ? until : [until]; // Check signals immediately const signals = conditions.filter((c) => typeof c !== 'string'); if (signals.some((signal) => signal())) { return { unsubscribe: () => { } }; } for (const condition of conditions) { if (typeof condition === 'string') { const sub = this.internalSubscribe(condition, unsubscribeAll, undefined, undefined, true); unsubscribers.push(sub.unsubscribe); } else { const signal = condition; const effectRef = effect(() => { if (signal()) unsubscribeAll(); }, { injector: this.injector }); unsubscribers.push(() => effectRef.destroy()); } } } // Replay latest event if requested if (replayLatest) { untracked(() => { const latestEvent = this.findLatestEventForKey(key); if (latestEvent) { try { callback(latestEvent); } catch (error) { this.handleError(error, latestEvent, onError, key); } } }); } mainSubscription = this.internalSubscribe(key, callback, destroyRef, onError); return { unsubscribe: unsubscribeAll }; } subscribe = this.on; once({ key, callback, replayLatest, onError, until }) { let subscription = null; const onceCallback = (event) => { subscription?.unsubscribe(); try { callback(event); } catch (error) { this.handleError(error, event, onError, key); } }; subscription = this.on({ key, callback: onceCallback, replayLatest, onError, until }); return subscription; } onCombineLatest(options) { const { keys, callback, destroyRef, replayLatest, onError, sortBy } = options; if (!keys?.length) throw new Error('Keys array cannot be empty'); const sourceSignals = keys.map((key) => this.toSignal(key)); const combinedSignal = computed(() => { const events = sourceSignals.map((s) => s()).filter((e) => e !== null); if (events.length === keys.length) { return sortBy === 'timestamp' ? events.sort((a, b) => b.timestamp - a.timestamp) : sortBy === 'key' ? events.sort((a, b) => a.key.localeCompare(b.key)) : events; } return null; }); const effectRef = effect(() => { const combinedEvents = combinedSignal(); if (!combinedEvents) return; if (!replayLatest && !this.eventRegistry().size) return; try { callback(combinedEvents); } catch (error) { onError ? onError(error, combinedEvents) : console.warn(`onCombineLatest error for keys [${keys.join(', ')}]:`, error); } }, { injector: this.injector }); const unsubscribe = () => effectRef.destroy(); if (destroyRef) { destroyRef.onDestroy(unsubscribe); } return { unsubscribe }; } toSignal(key) { if (!key) throw new Error('Key cannot be empty'); if (key.includes('*')) throw new Error('toSignal does not support wildcards. Use toSignalMultiple instead.'); return computed(() => this.eventRegistry().get(key) ?? null); } toSignalMultiple(keys, options = {}) { if (!keys?.length) throw new Error('Keys array cannot be empty'); return computed(() => { const registry = this.eventRegistry(); const matchingEvents = new Map(); for (const query of keys) { if (!query) continue; const isWildcard = query.includes('*'); if (!isWildcard) { const event = registry.get(query); if (event) { matchingEvents.set(event.key, event); } } else { const regex = this.buildWildcardRegex(query); for (const [eventKey, event] of registry.entries()) { if (regex.test(eventKey)) { matchingEvents.set(eventKey, event); } } } } const eventsArray = Array.from(matchingEvents.values()); if (options.sortBy === 'timestamp') { return eventsArray.sort((a, b) => b.timestamp - a.timestamp); } if (options.sortBy === 'key') { return eventsArray.sort((a, b) => a.key.localeCompare(b.key)); } return eventsArray; }); } clearEvent(key) { if (!key) throw new Error('Key cannot be empty'); this.eventRegistry.update((registry) => { const updated = new Map(registry); updated.delete(key); return updated; }); } reset(options = {}) { this.eventRegistry.set(new Map()); if (options.clearSubscribers) { this.subscribers.clear(); } } internalSubscribe(query, callback, destroyRef, onError, once = false) { const subscriber = { callback: callback, onError: onError, }; const subscriberSet = this.subscribers.get(query) ?? new Set(); subscriberSet.add(subscriber); this.subscribers.set(query, subscriberSet); const unsubscribe = () => { const currentSubscribers = this.subscribers.get(query); currentSubscribers?.delete(subscriber); if (currentSubscribers?.size === 0) { this.subscribers.delete(query); } }; if (once) { const originalCallback = subscriber.callback; subscriber.callback = (event) => { unsubscribe(); return originalCallback(event); }; } if (destroyRef) { destroyRef.onDestroy(unsubscribe); } return { unsubscribe }; } notifySubscribers(event) { this.subscribers.forEach((subscribers, query) => { if (this.matchQuery(event.key, query)) { subscribers.forEach(({ callback, onError }) => { try { Promise.resolve(callback(event)).catch((error) => { this.handleError(error, event, onError, query); }); } catch (error) { this.handleError(error, event, onError, query); } }); } }); } findLatestEventForKey(key) { if (!key.includes('*')) { return this.eventRegistry().get(key); } const regex = this.buildWildcardRegex(key); let latestEvent; for (const [eventKey, event] of this.eventRegistry().entries()) { if (regex.test(eventKey)) { if (!latestEvent || event.timestamp > latestEvent.timestamp) { latestEvent = event; } } } return latestEvent; } handleError(error, event, handler, query) { if (handler) { handler(error, event); } else { console.warn(`SignalHub error for query "${query}" on event "${event.key}":`, error); } } matchQuery(key, query) { if (query === '*' || key === query) return true; if (!query.includes('*')) return false; return this.buildWildcardRegex(query).test(key); } buildWildcardRegex(query) { const pattern = query.replace(/[.+?^${}()|[\]\\]/g, '\\$&').replace(/\*/g, '[^:]+'); return new RegExp(`^${pattern}$`); } static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.6", ngImport: i0, type: SignalHubService, deps: [{ token: i0.Injector }], target: i0.ɵɵFactoryTarget.Injectable }); static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.2.6", ngImport: i0, type: SignalHubService, providedIn: 'root' }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.6", ngImport: i0, type: SignalHubService, decorators: [{ type: Injectable, args: [{ providedIn: 'root' }] }], ctorParameters: () => [{ type: i0.Injector }] }); /* * Public API Surface of ngx-signal-hub */ /** * Generated bundle index. Do not edit. */ export { SignalHubService }; //# sourceMappingURL=ngx-signal-hub.mjs.map