UNPKG

@statezero/core

Version:

The type-safe frontend client for StateZero - connect directly to your backend models with zero boilerplate

211 lines (210 loc) 7.09 kB
import Pusher from 'pusher-js'; /** * Structure of events received from the server. * @typedef {Object} ModelEvent * @property {string} [type] - Support both frontend (type) and backend (event) naming conventions. * @property {string} [event] * @property {string} model * @property {any} [data] * @property {string} [operationId] * @property {string} [namespace] * @property {(string|number)[]} [instances] - For bulk events. * @property {string} [pk_field_name] * @property {string} [configKey] - The backend configuration key this event is associated with. * @property {any} [key] - Additional open-ended keys. */ /** * Event types that can be received from the server. * @readonly * @enum {string} */ export const EventType = { CREATE: 'create', UPDATE: 'update', DELETE: 'delete', BULK_UPDATE: 'bulk_update', BULK_DELETE: 'bulk_delete' }; /** * Callback for handling model events. * @callback EventHandler * @param {ModelEvent} event - The event object. */ /** * A namespace resolver function. * @callback NamespaceResolver * @param {string} modelName - The model name. * @returns {string} The namespace. */ /** * Options for instantiating a Pusher client. * @typedef {Object} PusherClientOptions * @property {string} appKey * @property {string} cluster * @property {boolean} [forceTLS] * @property {string} authEndpoint * @property {function(): Object<string, string>} [getAuthHeaders] */ /** * Configuration options for Pusher event receivers. * @typedef {Object} PusherReceiverOptions * @property {PusherClientOptions} clientOptions * @property {function(string): string} [formatChannelName] - Optional channel name formatter. Default: (namespace) => `private-${namespace}` * @property {NamespaceResolver} [namespaceResolver] - Optional namespace resolver. Default: (modelName) => modelName. */ /** * Implementation of EventReceiver that uses Pusher. */ export class PusherEventReceiver { /** * @param {PusherReceiverOptions} options * @param {string} configKey - The backend configuration key */ constructor(options, configKey) { const { clientOptions, formatChannelName, namespaceResolver } = options; this.configKey = configKey; this.pusherClient = new Pusher(clientOptions.appKey, { cluster: clientOptions.cluster, forceTLS: clientOptions.forceTLS ?? true, authEndpoint: clientOptions.authEndpoint, auth: { headers: clientOptions.getAuthHeaders?.() || {} } }); this.formatChannelName = formatChannelName ?? (ns => `private-${ns}`); this.namespaceResolver = namespaceResolver ?? (modelName => modelName); this.channels = new Map(); this.eventHandlers = new Set(); } /** * Set the namespace resolver function. * @param {NamespaceResolver} resolver */ setNamespaceResolver(resolver) { this.namespaceResolver = resolver; } /** * Connect to Pusher (no-op since Pusher handles connection automatically). */ connect() { } /** * Subscribe to events for a specific namespace. * @param {string} namespace */ subscribe(namespace) { if (this.channels.has(namespace)) return; const channelName = namespace.startsWith('private-') ? namespace : this.formatChannelName(namespace); console.log(`Subscribing to channel: ${channelName} for backend: ${this.configKey}`); const channel = this.pusherClient.subscribe(channelName); channel.bind('pusher:subscription_succeeded', () => { console.log(`Subscription succeeded for channel: ${channelName}`); }); channel.bind('pusher:subscription_error', status => { console.error(`Subscription error for channel: ${channelName}. Status:`, status); }); // Listen for CRUD events Object.values(EventType).forEach(eventType => { channel.bind(eventType, data => { const event = { ...data, type: data.event || eventType, namespace, configKey: this.configKey }; this.eventHandlers.forEach(handler => handler(event)); }); }); this.channels.set(namespace, channel); } unsubscribe(namespace) { const channel = this.channels.get(namespace); if (!channel) return; Object.values(EventType).forEach(eventType => { channel.unbind(eventType); }); const channelName = namespace.startsWith('private-') ? namespace : this.formatChannelName(namespace); this.pusherClient.unsubscribe(channelName); this.channels.delete(namespace); } /** * Disconnect from Pusher. */ disconnect() { [...this.channels.keys()].forEach(ns => this.unsubscribe(ns)); this.pusherClient.disconnect(); } /** * Add handler for model events * @param {EventHandler} handler */ addModelEventHandler(handler) { this.eventHandlers.add(handler); } /** * Legacy method - adds event handler for backwards compatibility * @param {EventHandler} handler */ addEventHandler(handler) { this.eventHandlers.add(handler); } /** * Remove an event handler callback. * @param {EventHandler} handler */ removeEventHandler(handler) { this.eventHandlers.delete(handler); } /** * Get namespace from model name using the resolver. * @param {string} modelName * @returns {string} */ getNamespace(modelName) { return this.namespaceResolver(modelName); } } // Map of event receivers by backend key const eventReceivers = new Map(); /** * Set an event receiver for a specific backend. * @param {string} configKey - The backend configuration key * @param {EventReceiver} receiver - The event receiver instance */ export function setEventReceiver(configKey, receiver) { const currentReceiver = eventReceivers.get(configKey); if (currentReceiver) { currentReceiver.disconnect(); } eventReceivers.set(configKey, receiver); receiver.connect(); } /** * Get the event receiver for a specific backend. * @param {string} configKey - The backend configuration key * @returns {EventReceiver|null} */ export function getEventReceiver(configKey = 'default') { return eventReceivers.get(configKey); } /** * Get all registered event receivers. * @returns {Map<string, EventReceiver>} */ export function getAllEventReceivers() { return eventReceivers; } /** * Set a custom namespace resolver function for a specific backend. * @param {string} configKey - The backend configuration key * @param {NamespaceResolver} resolver */ export function setNamespaceResolver(configKey, resolver) { const receiver = getEventReceiver(configKey); if (receiver) { receiver.setNamespaceResolver(resolver); } }