@ricardojbd/ngrx-signals-broadcast-sync
Version:
Enables seamless synchronization of state between browser contexts
103 lines (99 loc) • 4.48 kB
JavaScript
import { signalStoreFeature, withMethods, getState, patchState, withHooks } from '@ngrx/signals';
import { inject, PLATFORM_ID, effect } from '@angular/core';
import { isPlatformServer } from '@angular/common';
import isEqual from 'lodash.isequal';
const NOOP = () => null;
const BroadcastSyncStub = {
getBroadcastChannel: NOOP,
broadcastState: NOOP,
_requestBroadcastState: NOOP,
_patchStateFromBroadcast: NOOP
};
/**
* Adds broadcast synchronization capabilities to a Signal Store.
*
* This function enables state synchronization between multiple clients using the `BroadcastChannel` API.
* It can be invoked either with a broadcast channel name or with a configuration object (`Options`).
* It allows clients to request, send, and patch the state across multiple contexts.
*
* @template Input The type of the store feature result, extending `SignalStoreFeatureResult`.
* @param channelOrOptions Either the name of the broadcast channel or a configuration object.
* If a string is provided, it is treated as the broadcast channel name.
* If an `Options` object is provided, it allows further configuration of the broadcast behavior.
* @returns A Signal Store feature that adds methods for broadcast synchronization.
*/
function withBroadcastSync(channelOrOptions) {
const { channel, requestState = false, select = (state) => state, messageEventInterceptor = (event) => event.data, broadcastStateInterceptor = (state) => state, onMessageError = () => null } = typeof channelOrOptions === 'string' ? { channel: channelOrOptions } : channelOrOptions;
const runGuard = (platformId) => isPlatformServer(platformId) || typeof window?.BroadcastChannel === 'undefined';
let stateChannel;
let lastSyncedState = null;
return signalStoreFeature(withMethods((store, platformId = inject(PLATFORM_ID)) => {
if (runGuard(platformId)) {
console.warn(`'withBroadcastSync' provides non-functional implementation due to server-side execution`);
return BroadcastSyncStub;
}
return {
getBroadcastChannel() {
return stateChannel;
},
broadcastState() {
let state = select(getState(store));
if (isEqual(state, lastSyncedState)) {
return;
}
state = broadcastStateInterceptor(state);
if (stateChannel != null) {
lastSyncedState = state; // Avoid broadcast duplicated state
stateChannel.postMessage({ type: 'UPDATE_STATE', state });
}
},
_requestBroadcastState() {
lastSyncedState = select(getState(store)); // Avoid broadcast initial state
stateChannel?.postMessage({ type: 'REQUEST_STATE' });
},
_patchStateFromBroadcast(state) {
lastSyncedState = state; // Avoid broadcast this state change
patchState(store, state);
}
};
}), withHooks({
onInit(store, platformId = inject(PLATFORM_ID)) {
if (runGuard(platformId)) {
return;
}
stateChannel = new BroadcastChannel(channel);
if (requestState) {
store._requestBroadcastState();
}
stateChannel.onmessage = (event) => {
const message = messageEventInterceptor(event);
switch (message.type) {
case 'REQUEST_STATE': {
lastSyncedState = null; // Force broadcast state
store.broadcastState();
break;
}
case 'UPDATE_STATE': {
store._patchStateFromBroadcast(message.state);
break;
}
default: {
console.warn(`Unknown message type "${message}" in the broadcast channel "${channel}"`);
}
}
};
stateChannel.onmessageerror = (event) => onMessageError(event);
effect(() => {
store.broadcastState();
});
},
onDestroy() {
stateChannel?.close();
}
}));
}
/**
* Generated bundle index. Do not edit.
*/
export { withBroadcastSync };
//# sourceMappingURL=ricardojbd-ngrx-signals-broadcast-sync.mjs.map