diff --git a/.gitignore b/.gitignore
index 9fe7206..201514c 100644
--- a/.gitignore
+++ b/.gitignore
@@ -46,3 +46,4 @@ $RECYCLE.BIN/
IlluminatedCloud/
.idea/
+/.localdev/
diff --git a/README.md b/README.md
index 7bbc674..872f6cd 100644
--- a/README.md
+++ b/README.md
@@ -720,7 +720,8 @@ The following storage helpers are available by default:
- `useEventListener(eventName: string)`: Dispatches a CustomEvent to the `window` object with the given event name
whenever the signal changes. It also listens for events with the given name and updates the signal when the event is
received. This is useful for when you want to communicate changes to components that for some reason don't
- have access to the signal (for example, a component that cannot import the signal because it lives in a different namespace).
+ have access to the signal (for example, a component that cannot import the signal because it lives in a different
+ namespace).
The event sent and expected to be received has the following format:
@@ -754,6 +755,67 @@ The following storage helpers are available by default:
}
```
+- `useEventBus(channel: string, toValue: (response?: object) => T, options: object)`: Subscribes to the event bus
+ channel (e.g. platform event, change data capture, etc.).
+
+ - The `channel` parameter is the event bus channel to subscribe to.
+ - The `toValue` function is used to convert the response from the event bus to the desired value.
+
+ ```javascript
+ import { $signal, useEventBus } from "c/signals";
+ export const receivedEvent = $signal(undefined, {
+ storage: useEventBus("/event/PlatEvent__e", ({ data }) => ({
+ message: data.payload.Message__c,
+ sender: data.payload.Sender__c,
+ time: data.payload.Time__c
+ }))
+ });
+ ```
+
+ The passed in argument will be the message received from the event bus, which
+ is of the following shape:
+
+ ```javascript
+ {
+ channel: string;
+ data: {
+ event: {
+ replayId: number;
+ },
+ payload: object,
+ };
+ }
+ ```
+
+ The `payload` key will contain the actual data of the event. For example,
+ if using a platform event, this will contain the fields of the platform event.
+
+ - The `options` (optional) parameter is an object that can contain the following properties (all of them optional):
+ - `replayId` The replay ID to start from, defaults to -1. When -2 is passed, it will replay from the last saved event.
+ - `onSubscribe` A callback function called when the subscription is successful.
+ - `onError` A callback function called when an error response is received from the server for
+ handshake, connect, subscribe, and unsubscribe meta channels.
+
+ **Unsubscribing from the event bus**
+
+ When using the `useEventBus` storage, the signal will hold a special function called `unsubscribe` that you can call
+ to unsubscribe from the event bus.
+
+ ```javascript
+ import { $signal, useEventBus } from "c/signals";
+
+ const receivedEvent = $signal(undefined, {
+ storage: useEventBus("/event/PlatEvent__e", ({ data }) => ({
+ message: data.payload.Message__c,
+ sender: data.payload.Sender__c,
+ time: data.payload.Time__c
+ }))
+ });
+
+ // Unsubscribe from the event bus
+ receivedEvent.unsubscribe();
+ ```
+
### Creating a custom storage
The `storage` option receives a function that defines the behavior for where the data should be stored.
diff --git a/examples/counter/lwc/countChanger/countChanger.js-meta.xml b/examples/counter/lwc/countChanger/countChanger.js-meta.xml
index c5a6356..0fdc56d 100644
--- a/examples/counter/lwc/countChanger/countChanger.js-meta.xml
+++ b/examples/counter/lwc/countChanger/countChanger.js-meta.xml
@@ -7,5 +7,7 @@
lightningCommunity__DefaultlightningCommunity__Page
+ lightning__HomePage
+ lightning__RecordPage
-
\ No newline at end of file
+
diff --git a/examples/counter/lwc/countTracker/countTracker.js-meta.xml b/examples/counter/lwc/countTracker/countTracker.js-meta.xml
index e01574c..bc7ea48 100644
--- a/examples/counter/lwc/countTracker/countTracker.js-meta.xml
+++ b/examples/counter/lwc/countTracker/countTracker.js-meta.xml
@@ -7,5 +7,6 @@
lightningCommunity__DefaultlightningCommunity__Page
+ lightning__RecordPage
-
\ No newline at end of file
+
diff --git a/examples/demo-signals/lwc/demoSignals/chat-data-source.js b/examples/demo-signals/lwc/demoSignals/chat-data-source.js
new file mode 100644
index 0000000..17203ef
--- /dev/null
+++ b/examples/demo-signals/lwc/demoSignals/chat-data-source.js
@@ -0,0 +1,16 @@
+import { $signal, useEventBus } from "c/signals";
+
+export const messageEvent = $signal(undefined, {
+ storage: useEventBus(
+ "/event/ChatMessage__e",
+ ({ data }) => ({
+ message: data.payload.Message__c,
+ sender: data.payload.Sender__c,
+ time: data.payload.Time__c
+ }),
+ {
+ replayId: -2,
+ onSubscribe: (message) => console.log("Subscribed to message", message)
+ }
+ )
+});
diff --git a/examples/demo-signals/lwc/demoSignals/counter.js b/examples/demo-signals/lwc/demoSignals/counter.js
index 931e145..3e9221e 100644
--- a/examples/demo-signals/lwc/demoSignals/counter.js
+++ b/examples/demo-signals/lwc/demoSignals/counter.js
@@ -1,4 +1,4 @@
-import { $signal, $effect, $computed, useLocalStorage, useEventListener } from "c/signals";
+import { $signal, $effect, $computed, useEventListener } from "c/signals";
// EXAMPLE OF DEFAULT COUNTER
diff --git a/examples/demo-signals/lwc/demoSignals/demoSignals.js b/examples/demo-signals/lwc/demoSignals/demoSignals.js
index e1fac1d..4917cf7 100644
--- a/examples/demo-signals/lwc/demoSignals/demoSignals.js
+++ b/examples/demo-signals/lwc/demoSignals/demoSignals.js
@@ -2,3 +2,4 @@ export * from "./counter";
export * from "./contact-info";
export * from "./apex-fetcher";
export * from "./shopping-cart";
+export * from "./chat-data-source";
diff --git a/examples/main/default/objects/ChatMessage__e/ChatMessage__e.object-meta.xml b/examples/main/default/objects/ChatMessage__e/ChatMessage__e.object-meta.xml
new file mode 100644
index 0000000..705e337
--- /dev/null
+++ b/examples/main/default/objects/ChatMessage__e/ChatMessage__e.object-meta.xml
@@ -0,0 +1,8 @@
+
+
+ Deployed
+ HighVolume
+
+ ChatMessages
+ PublishImmediately
+
diff --git a/examples/main/default/objects/ChatMessage__e/fields/Message__c.field-meta.xml b/examples/main/default/objects/ChatMessage__e/fields/Message__c.field-meta.xml
new file mode 100644
index 0000000..72065ab
--- /dev/null
+++ b/examples/main/default/objects/ChatMessage__e/fields/Message__c.field-meta.xml
@@ -0,0 +1,13 @@
+
+
+ Message__c
+ false
+ false
+ false
+ false
+
+ 255
+ true
+ Text
+ false
+
diff --git a/examples/main/default/objects/ChatMessage__e/fields/Sender__c.field-meta.xml b/examples/main/default/objects/ChatMessage__e/fields/Sender__c.field-meta.xml
new file mode 100644
index 0000000..29f719a
--- /dev/null
+++ b/examples/main/default/objects/ChatMessage__e/fields/Sender__c.field-meta.xml
@@ -0,0 +1,13 @@
+
+
+ Sender__c
+ false
+ false
+ false
+ false
+
+ 255
+ true
+ Text
+ false
+
diff --git a/examples/main/default/objects/ChatMessage__e/fields/Time__c.field-meta.xml b/examples/main/default/objects/ChatMessage__e/fields/Time__c.field-meta.xml
new file mode 100644
index 0000000..7bc9e89
--- /dev/null
+++ b/examples/main/default/objects/ChatMessage__e/fields/Time__c.field-meta.xml
@@ -0,0 +1,12 @@
+
+
+ Time__c
+ NOW()
+ false
+ false
+ false
+ false
+
+ true
+ DateTime
+
diff --git a/examples/real-time-through-event-bus/controllers/classes/ChatController.cls b/examples/real-time-through-event-bus/controllers/classes/ChatController.cls
new file mode 100644
index 0000000..22e2c70
--- /dev/null
+++ b/examples/real-time-through-event-bus/controllers/classes/ChatController.cls
@@ -0,0 +1,9 @@
+public with sharing class ChatController {
+ @AuraEnabled
+ public static void sendMessage(String message, String sender) {
+ EventBus.publish(new ChatMessage__e(
+ Message__c = message,
+ Sender__c = sender
+ ));
+ }
+}
diff --git a/examples/real-time-through-event-bus/controllers/classes/ChatController.cls-meta.xml b/examples/real-time-through-event-bus/controllers/classes/ChatController.cls-meta.xml
new file mode 100644
index 0000000..df13efa
--- /dev/null
+++ b/examples/real-time-through-event-bus/controllers/classes/ChatController.cls-meta.xml
@@ -0,0 +1,5 @@
+
+
+ 60.0
+ Active
+
diff --git a/examples/real-time-through-event-bus/lwc/chat/chat.html b/examples/real-time-through-event-bus/lwc/chat/chat.html
new file mode 100644
index 0000000..8083b42
--- /dev/null
+++ b/examples/real-time-through-event-bus/lwc/chat/chat.html
@@ -0,0 +1,49 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {message.message}
+
+
+ {message.sender} • {message.time}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/examples/real-time-through-event-bus/lwc/chat/chat.js b/examples/real-time-through-event-bus/lwc/chat/chat.js
new file mode 100644
index 0000000..288954e
--- /dev/null
+++ b/examples/real-time-through-event-bus/lwc/chat/chat.js
@@ -0,0 +1,65 @@
+import { LightningElement } from "lwc";
+import { $effect } from "c/signals";
+import { messageEvent } from "c/demoSignals";
+import sendMessage from "@salesforce/apex/ChatController.sendMessage";
+
+export default class Chat extends LightningElement {
+ sender = "User";
+ message = "";
+
+ messages = [];
+
+ connectedCallback() {
+ $effect(() => {
+ if (messageEvent.value) {
+ console.log("Message received in the component", messageEvent.value);
+ this.messages = [...this.messages, messageEvent.value];
+ }
+ });
+ }
+
+ get formattedMessages() {
+ return this.messages.map((message) => {
+ return {
+ ...message,
+ listClasses:
+ message.sender === this.sender
+ ? "slds-chat-listitem slds-chat-listitem_outbound"
+ : "slds-chat-listitem slds-chat-listitem_inbound",
+ messageClasses:
+ message.sender === this.sender
+ ? "slds-chat-message__text slds-chat-message__text_outbound"
+ : "slds-chat-message__text slds-chat-message__text_inbound"
+ };
+ });
+ }
+
+ handleNameChange(event) {
+ this.sender = event.detail.value;
+ }
+
+ handleMessageChange(event) {
+ this.message = event.detail.value;
+ }
+
+ get isMessageBoxDisabled() {
+ return !this.sender;
+ }
+
+ get isSendDisabled() {
+ return !this.message || !this.sender;
+ }
+
+ sendMessage() {
+ sendMessage({
+ message: this.message,
+ sender: this.sender
+ });
+ }
+
+ unsub() {
+ messageEvent.unsubscribe((res) =>
+ console.log("Unsubscribed from message channel", res)
+ );
+ }
+}
diff --git a/examples/real-time-through-event-bus/lwc/chat/chat.js-meta.xml b/examples/real-time-through-event-bus/lwc/chat/chat.js-meta.xml
new file mode 100644
index 0000000..034e3c7
--- /dev/null
+++ b/examples/real-time-through-event-bus/lwc/chat/chat.js-meta.xml
@@ -0,0 +1,10 @@
+
+
+ 60.0
+ Chat
+ true
+ Chat
+
+ lightning__RecordPage
+
+
diff --git a/examples/server-communication/lwc/serverFetcher/serverFetcher.js b/examples/server-communication/lwc/serverFetcher/serverFetcher.js
index 4c2eb48..3ed29d6 100644
--- a/examples/server-communication/lwc/serverFetcher/serverFetcher.js
+++ b/examples/server-communication/lwc/serverFetcher/serverFetcher.js
@@ -4,4 +4,4 @@ import { fetchContacts } from "c/demoSignals";
export default class ServerFetcher extends LightningElement {
contacts = $computed(() => (this.contacts = fetchContacts.value)).value;
-}
\ No newline at end of file
+}
diff --git a/force-app/lwc/signals/use.js b/force-app/lwc/signals/use.js
index dd08143..42db942 100644
--- a/force-app/lwc/signals/use.js
+++ b/force-app/lwc/signals/use.js
@@ -1,5 +1,11 @@
-export function createStorage(get, set, registerOnChange) {
- return { get, set, registerOnChange };
+import {
+ subscribe,
+ unsubscribe as empApiUnsubscribe,
+ isEmpEnabled,
+ onError as empApiOnError
+} from "lightning/empApi";
+export function createStorage(get, set, registerOnChange, unsubscribe) {
+ return { get, set, registerOnChange, unsubscribe };
}
export function useInMemoryStorage(value) {
let _value = value;
@@ -104,3 +110,84 @@ export function useEventListener(type) {
return createStorage(getter, setter, registerOnChange);
};
}
+/**
+ * Subscribes to the event bus channel (e.g. platform event, change data capture, etc.).
+ * Usage:
+ * Pass to the `storage` option of a signal, e.g.:
+ * ```javascript
+ * import { $signal, useEventBus } from "c/signals";
+ * export const receivedEvent = $signal(undefined, {
+ * storage: useEventBus(
+ * "/event/PlatEvent__e",
+ * ({ data }) => ({
+ * message: data.payload.Message__c,
+ * sender: data.payload.Sender__c,
+ * time: data.payload.Time__c
+ * })
+ * )
+ * });
+ * ```
+ * @param channel The event bus channel to subscribe to.
+ * @param toValue A function that converts the received message to the desired value.
+ * The passed in argument will be the message received from the event bus, which
+ * is of the following shape:
+ * ```
+ * {
+ * channel: string;
+ * data: {
+ * event: {
+ * replayId: number;
+ * },
+ * payload: Record & { CreatedById: string; CreatedDate: string },
+ * };
+ * }
+ * ```
+ *
+ * The `payload` will contain the actual data of the event. For example,
+ * if using a platform event, this will contain the fields of the platform event.
+ * @param options (Optional) Additional options.
+ * @param options.replayId (Optional) The replay ID to start from. Defaults to -1.
+ * When -2 is passed, it will replay from the last saved event.
+ * @param options.onSubscribe (Optional) A callback function that's called when the subscription is successful.
+ * @param options.onError (Optional) A callback function that's called when an error response is received from the server for
+ * handshake, connect, subscribe, and unsubscribe meta channels.
+ */
+export function useEventBus(channel, toValue, options) {
+ return function (value) {
+ let _value = value;
+ let _onChange;
+ let subscription = {};
+ const replayId = options?.replayId ?? -1;
+ isEmpEnabled().then((enabled) => {
+ if (!enabled) {
+ console.error(
+ `EMP API is not enabled, cannot subscribe to channel ${channel}`
+ );
+ return;
+ }
+ subscribe(channel, replayId, (response) => {
+ _value = toValue(response);
+ _onChange?.();
+ }).then((sub) => {
+ subscription = sub;
+ options?.onSubscribe?.(sub);
+ });
+ empApiOnError((error) => {
+ options?.onError?.(error);
+ });
+ });
+ function getter() {
+ return _value;
+ }
+ function setter(newValue) {
+ _value = newValue;
+ }
+ function registerOnChange(onChange) {
+ _onChange = onChange;
+ }
+ function unsubscribe(callback) {
+ return empApiUnsubscribe(subscription, callback);
+ }
+ return createStorage(getter, setter, registerOnChange, unsubscribe);
+ };
+}
diff --git a/jest.config.ts b/jest.config.ts
index a8c0c61..7e0697e 100644
--- a/jest.config.ts
+++ b/jest.config.ts
@@ -12,7 +12,8 @@ const config: JestConfigWithTsJest = {
},
extensionsToTreatAsEsm: [".ts"],
moduleNameMapper: {
- "^(\\.{1,2}/.*)\\.js$": "$1"
+ "^(\\.{1,2}/.*)\\.js$": "$1",
+ "lightning/empApi": "/src/__mocks__/lightning/empApi.ts",
},
testEnvironment: "jsdom",
};
diff --git a/src/__mocks__/lightning/empApi.ts b/src/__mocks__/lightning/empApi.ts
new file mode 100644
index 0000000..4234a58
--- /dev/null
+++ b/src/__mocks__/lightning/empApi.ts
@@ -0,0 +1,25 @@
+const _channels: Record void }> = {};
+
+export const subscribe = jest.fn((channel: string, replayId: number, onMessageCallback: (response?: unknown) => void) => {
+ _channels[channel] = { onMessageCallback };
+ return Promise.resolve({
+ id: "_" + Date.now(),
+ channel: channel,
+ replayId: replayId
+ });
+});
+
+// A Jest-specific function for "publishing" your Platform Event
+export const jestMockPublish = jest.fn((channel, message) => {
+ if (
+ _channels[channel] &&
+ _channels[channel].onMessageCallback instanceof Function
+ ) {
+ _channels[channel].onMessageCallback(message);
+ }
+ return Promise.resolve(true);
+});
+
+export const unsubscribe = jest.fn().mockResolvedValue({});
+export const onError = jest.fn().mockResolvedValue(jest.fn());
+export const isEmpEnabled = jest.fn().mockResolvedValue(true);
diff --git a/src/global.d.ts b/src/global.d.ts
new file mode 100644
index 0000000..0a1ad41
--- /dev/null
+++ b/src/global.d.ts
@@ -0,0 +1,65 @@
+declare module "lightning/empApi" {
+ export interface SubscribeResponse {
+ channel: string;
+ successful: boolean;
+ subscription: string;
+ error: string;
+ clientId: string;
+ id: string;
+ }
+
+ export interface Message {
+ channel: string;
+ data: {
+ event: {
+ replayId: number;
+ },
+ payload: Record & { CreatedById: string; CreatedDate: string },
+ };
+ }
+
+ /**
+ * Subscribes to a given channel and returns a promise that holds a subscription object, which you use to
+ * unsubscribe later.
+ *
+ * @param channel The channel name to subscribe to.
+ * @param replayId Indicates what point in the stream to replay events from. Specify -1 to get new events from the
+ * tip of the stream, -2 to replay from the last-saved event, or a specific event replay ID to get all saved and
+ * new events after that ID.
+ * @param onMessageCallback A callback function that's invoked for every event received.
+ */
+ export function subscribe(channel: string, replayId: number, onMessageCallback: (response?: Message) => void): Promise;
+
+ export interface UnsubscribeResponse {
+ channel: string;
+ successful: boolean;
+ error: string;
+ clientId: string;
+ id: string;
+ }
+
+ /**
+ * Unsubscribes from the channel using the given subscription object and returns a promise. The result of this
+ * operation is passed in to the callback function. The result object holds the successful boolean field which
+ * indicates whether the unsubscribe operation was successful. The result fields are based on the CometD protocol
+ * for the unsubscribe operation.
+ *
+ * @param subscription Subscription object that the subscribe call returned.
+ * @param callback A callback function that's called with a server response for the unsubscribe call.
+ */
+ export function unsubscribe(subscription: object, callback?: (response?: UnsubscribeResponse) => void): Promise;
+
+ /**
+ * Registers a listener to errors that the server returns.
+ *
+ * @param callback A callback function that's called when an error response is received from the server for
+ * handshake, connect, subscribe, and unsubscribe meta channels.
+ */
+ export function onError(callback: (error: unknown) => void): void;
+
+ /**
+ * Returns a promise that holds a Boolean value. The value is true if the EmpJs Streaming API library can be used
+ * in this context; otherwise false.
+ */
+ export function isEmpEnabled(): Promise;
+}
diff --git a/src/lwc/signals/__tests__/signals.test.ts b/src/lwc/signals/__tests__/signals.test.ts
index a293075..0985b30 100644
--- a/src/lwc/signals/__tests__/signals.test.ts
+++ b/src/lwc/signals/__tests__/signals.test.ts
@@ -1,5 +1,6 @@
import { $signal, $computed, $effect, $resource, Signal } from "../core";
-import { createStorage, useCookies, useLocalStorage, useSessionStorage } from "../use";
+import { createStorage, useCookies, useEventBus, useLocalStorage, useSessionStorage } from "../use";
+import { jestMockPublish } from "../../../__mocks__/lightning/empApi";
describe("signals", () => {
describe("core functionality", () => {
@@ -14,7 +15,7 @@ describe("signals", () => {
expect(signal.value).toBe(1);
});
- test('can debounce setting a signal value', async () => {
+ test("can debounce setting a signal value", async () => {
const debouncedSignal = $signal(0, {
debounce: 100
});
@@ -380,8 +381,8 @@ describe("signals", () => {
const source = $signal("changed");
const { data: resource } = $resource(asyncFunction, () => ({
- source: source.value
- }),
+ source: source.value
+ }),
{
initialValue: "initial",
fetchWhen: () => false
@@ -570,3 +571,31 @@ describe("storing values in cookies", () => {
expect(signal.value).toBe(1);
});
});
+
+describe("when receiving a value from the empApi", () => {
+ it("should update the signal when the message is received", async () => {
+ function handleEvent(event?: { data: { payload: Record } }) {
+ return event?.data.payload.Message__c ?? "";
+ }
+
+ const signal = $signal("", {
+ storage: useEventBus("/event/TestChannel__e", handleEvent)
+ });
+
+ await new Promise(process.nextTick);
+
+ expect(signal.value).toBe("");
+
+ await jestMockPublish("/event/TestChannel__e", {
+ data: {
+ payload: {
+ Message__c: "Hello World!"
+ }
+ }
+ });
+
+ await new Promise(process.nextTick);
+
+ expect(signal.value).toBe("Hello World!");
+ });
+});
diff --git a/src/lwc/signals/use.ts b/src/lwc/signals/use.ts
index fa21382..71812d1 100644
--- a/src/lwc/signals/use.ts
+++ b/src/lwc/signals/use.ts
@@ -1,15 +1,26 @@
+import {
+ Message,
+ subscribe,
+ unsubscribe as empApiUnsubscribe,
+ UnsubscribeResponse,
+ isEmpEnabled, SubscribeResponse,
+ onError as empApiOnError
+} from "lightning/empApi";
+
export type State = {
get: () => T;
set: (newValue: T) => void;
registerOnChange?: (f: VoidFunction) => void;
+ unsubscribe?: () => void;
}
export function createStorage(
get: () => T,
set: (newValue: T) => void,
- registerOnChange?: (f: VoidFunction) => void
+ registerOnChange?: (f: VoidFunction) => void,
+ unsubscribe?: () => void
): State {
- return { get, set, registerOnChange };
+ return { get, set, registerOnChange, unsubscribe };
}
export function useInMemoryStorage(value: T): State {
@@ -139,3 +150,96 @@ export function useEventListener(
return createStorage(getter, setter, registerOnChange);
};
}
+
+/**
+ * Subscribes to the event bus channel (e.g. platform event, change data capture, etc.).
+ * Usage:
+ * Pass to the `storage` option of a signal, e.g.:
+ * ```javascript
+ * import { $signal, useEventBus } from "c/signals";
+ * export const receivedEvent = $signal(undefined, {
+ * storage: useEventBus(
+ * "/event/PlatEvent__e",
+ * ({ data }) => ({
+ * message: data.payload.Message__c,
+ * sender: data.payload.Sender__c,
+ * time: data.payload.Time__c
+ * })
+ * )
+ * });
+ * ```
+ * @param channel The event bus channel to subscribe to.
+ * @param toValue A function that converts the received message to the desired value.
+ * The passed in argument will be the message received from the event bus, which
+ * is of the following shape:
+ * ```
+ * {
+ * channel: string;
+ * data: {
+ * event: {
+ * replayId: number;
+ * },
+ * payload: Record & { CreatedById: string; CreatedDate: string },
+ * };
+ * }
+ * ```
+ *
+ * The `payload` will contain the actual data of the event. For example,
+ * if using a platform event, this will contain the fields of the platform event.
+ * @param options (Optional) Additional options.
+ * @param options.replayId (Optional) The replay ID to start from. Defaults to -1.
+ * When -2 is passed, it will replay from the last saved event.
+ * @param options.onSubscribe (Optional) A callback function that's called when the subscription is successful.
+ * @param options.onError (Optional) A callback function that's called when an error response is received from the server for
+ * handshake, connect, subscribe, and unsubscribe meta channels.
+ */
+export function useEventBus(channel: string, toValue: (response?: Message) => T, options?: {
+ replayId?: number,
+ onSubscribe?: (response: SubscribeResponse) => void,
+ onError?: (error: unknown) => void
+}) {
+ return function(value: T) {
+ let _value: T = value;
+ let _onChange: VoidFunction | undefined;
+ let subscription = {};
+
+ const replayId = options?.replayId ?? -1;
+
+ isEmpEnabled().then((enabled) => {
+ if (!enabled) {
+ console.error(`EMP API is not enabled, cannot subscribe to channel ${channel}`);
+ return;
+ }
+
+ subscribe(channel, replayId, (response?: Message) => {
+ _value = toValue(response);
+ _onChange?.();
+ }).then((sub) => {
+ subscription = sub;
+ options?.onSubscribe?.(sub);
+ });
+
+ empApiOnError((error) => {
+ options?.onError?.(error);
+ });
+ });
+
+ function getter() {
+ return _value;
+ }
+
+ function setter(newValue: T) {
+ _value = newValue;
+ }
+
+ function registerOnChange(onChange: VoidFunction) {
+ _onChange = onChange;
+ }
+
+ function unsubscribe(callback?: (response?: UnsubscribeResponse) => void) {
+ return empApiUnsubscribe(subscription, callback);
+ }
+
+ return createStorage(getter, setter, registerOnChange, unsubscribe);
+ };
+}
diff --git a/tsconfig.json b/tsconfig.json
index 0464ec8..2d0ac48 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -9,5 +9,5 @@
"forceConsistentCasingInFileNames": true,
"moduleResolution": "Bundler"
},
- "exclude": ["node_modules", "./*.ts", "src/**/*.test.ts"]
+ "exclude": ["node_modules", "./*.ts", "src/**/*.test.ts", "**/__mocks__/*"]
}