From 48a8124e505914ca4c7675625f1f92f2fb9fc361 Mon Sep 17 00:00:00 2001 From: Joe Tannenbaum Date: Mon, 5 May 2025 14:40:36 -0400 Subject: [PATCH 01/10] better model types --- packages/react/src/types.ts | 3 +++ packages/vue/src/types.ts | 3 +++ 2 files changed, 6 insertions(+) diff --git a/packages/react/src/types.ts b/packages/react/src/types.ts index c0bea5f7..38f1293a 100644 --- a/packages/react/src/types.ts +++ b/packages/react/src/types.ts @@ -23,6 +23,9 @@ export type ConfigDefaults = Record< export type ModelPayload = { model: T; + connection: string | null; + queue: string | null; + afterCommit: boolean; }; export type ChannelReturnType< diff --git a/packages/vue/src/types.ts b/packages/vue/src/types.ts index c0bea5f7..38f1293a 100644 --- a/packages/vue/src/types.ts +++ b/packages/vue/src/types.ts @@ -23,6 +23,9 @@ export type ConfigDefaults = Record< export type ModelPayload = { model: T; + connection: string | null; + queue: string | null; + afterCommit: boolean; }; export type ChannelReturnType< From 67dea8d676a07544e2a39914cf7fe63ac364514f Mon Sep 17 00:00:00 2001 From: Joe Tannenbaum Date: Mon, 5 May 2025 14:41:00 -0400 Subject: [PATCH 02/10] Update use-echo.ts --- packages/react/src/hooks/use-echo.ts | 31 ++++++++++++++++++++++++---- 1 file changed, 27 insertions(+), 4 deletions(-) diff --git a/packages/react/src/hooks/use-echo.ts b/packages/react/src/hooks/use-echo.ts index 9a839903..f983d70a 100644 --- a/packages/react/src/hooks/use-echo.ts +++ b/packages/react/src/hooks/use-echo.ts @@ -81,11 +81,17 @@ export const useEcho = < >( channelName: string, event: string | string[], - callback: (payload: TPayload) => void, + callback: (payload: TPayload, eventName: string) => void, dependencies: any[] = [], visibility: TVisibility = "private" as TVisibility, ) => { - const callbackFunc = useCallback(callback, dependencies); + const callbacks = useRef< + Record void> + >({}); + const allCallbackFunc = useCallback( + (eventName: string, payload: TPayload) => callback(payload, eventName), + dependencies, + ); const subscription = useRef | null>(null); const listening = useRef(false); @@ -104,7 +110,11 @@ export const useEcho = < } events.forEach((e) => { - subscription.current!.stopListening(e, callbackFunc); + if (e !== "*") { + subscription.current!.stopListening(e, callbacks.current[e]); + } else if ("stopListeningToAll" in subscription.current!) { + subscription.current.stopListeningToAll(allCallbackFunc); + } }); listening.current = false; @@ -116,7 +126,20 @@ export const useEcho = < } events.forEach((e) => { - subscription.current!.listen(e, callbackFunc); + if (e !== "*") { + const cb = + callbacks.current[e] ?? + ((payload: TPayload) => callback(payload, e)); + + subscription.current!.listen(e, cb); + } else if ("listenToAll" in subscription.current!) { + subscription.current.listenToAll(allCallbackFunc); + } else { + // eslint-disable-next-line no-console + console.warn( + "listenToAll is not supported for this channel type", + ); + } }); listening.current = true; From 1380ac54760a4b2554df038c0bdc5dffb9c8d0ee Mon Sep 17 00:00:00 2001 From: Joe Tannenbaum Date: Mon, 5 May 2025 15:29:35 -0400 Subject: [PATCH 03/10] alter callback to include event name --- packages/react/src/hooks/use-echo.ts | 10 +- packages/react/tests/use-echo.test.ts | 195 +++++++++++++++++++------- 2 files changed, 148 insertions(+), 57 deletions(-) diff --git a/packages/react/src/hooks/use-echo.ts b/packages/react/src/hooks/use-echo.ts index f983d70a..7187fc1b 100644 --- a/packages/react/src/hooks/use-echo.ts +++ b/packages/react/src/hooks/use-echo.ts @@ -104,6 +104,10 @@ export const useEcho = < visibility, }; + events.forEach((e) => { + callbacks.current[e] = (payload: TPayload) => callback(payload, e); + }); + const stopListening = useCallback(() => { if (!listening.current) { return; @@ -127,11 +131,7 @@ export const useEcho = < events.forEach((e) => { if (e !== "*") { - const cb = - callbacks.current[e] ?? - ((payload: TPayload) => callback(payload, e)); - - subscription.current!.listen(e, cb); + subscription.current!.listen(e, callbacks.current[e]); } else if ("listenToAll" in subscription.current!) { subscription.current.listenToAll(allCallbackFunc); } else { diff --git a/packages/react/tests/use-echo.test.ts b/packages/react/tests/use-echo.test.ts index e1849619..d0da2f2e 100644 --- a/packages/react/tests/use-echo.test.ts +++ b/packages/react/tests/use-echo.test.ts @@ -122,18 +122,37 @@ describe("useEcho hook", async () => { const channel = echoInstance.private(channelName); - expect(channel.listen).toHaveBeenCalledWith(events[0], mockCallback); - expect(channel.listen).toHaveBeenCalledWith(events[1], mockCallback); + const firstCallback = ( + channel.listen as unknown as ReturnType + ).mock.calls[0][1]; + const secondCallback = ( + channel.listen as unknown as ReturnType + ).mock.calls[1][1]; + + expect(channel.listen).toHaveBeenCalledWith( + events[0], + expect.any(Function), + ); + expect(channel.listen).toHaveBeenCalledWith( + events[1], + expect.any(Function), + ); + + firstCallback({ data: "test" }); + expect(mockCallback).toHaveBeenCalledWith({ data: "test" }, events[0]); + + secondCallback({ data: "test" }); + expect(mockCallback).toHaveBeenCalledWith({ data: "test" }, events[1]); expect(() => unmount()).not.toThrow(); expect(channel.stopListening).toHaveBeenCalledWith( events[0], - mockCallback, + firstCallback, ); expect(channel.stopListening).toHaveBeenCalledWith( events[1], - mockCallback, + secondCallback, ); }); @@ -188,10 +207,16 @@ describe("useEcho hook", async () => { expect(echoInstance.private).toHaveBeenCalledWith(channelName); - expect(echoInstance.private(channelName).listen).toHaveBeenCalledWith( + const channel = echoInstance.private(channelName); + const callback = (channel.listen as any).mock.calls[0][1]; + + expect(channel.listen).toHaveBeenCalledWith( event, - mockCallback, + expect.any(Function), ); + + callback({ data: "test" }); + expect(mockCallback).toHaveBeenCalledWith({ data: "test" }, event); }); it("can leave a channel", async () => { @@ -250,16 +275,27 @@ describe("useEcho hook", async () => { ); const channel = echoInstance.private(channelName); + const callback = (channel.listen as any).mock.calls[0][1]; - expect(channel.listen).toHaveBeenCalledWith(event, mockCallback); + expect(channel.listen).toHaveBeenCalledWith( + event, + expect.any(Function), + ); result.current.stopListening(); - expect(channel.stopListening).toHaveBeenCalledWith(event, mockCallback); + expect(channel.stopListening).toHaveBeenCalledWith(event, callback); result.current.listen(); - expect(channel.listen).toHaveBeenCalledWith(event, mockCallback); + const newCallback = (channel.listen as any).mock.calls[1][1]; + expect(channel.listen).toHaveBeenCalledWith( + event, + expect.any(Function), + ); + + newCallback({ data: "test" }); + expect(mockCallback).toHaveBeenCalledWith({ data: "test" }, event); }); it("can manually stop listening to events", async () => { @@ -271,10 +307,12 @@ describe("useEcho hook", async () => { echoModule.useEcho(channelName, event, mockCallback), ); + const channel = echoInstance.private(channelName); + const callback = (channel.listen as any).mock.calls[0][1]; + result.current.stopListening(); - const channel = echoInstance.private(channelName); - expect(channel.stopListening).toHaveBeenCalledWith(event, mockCallback); + expect(channel.stopListening).toHaveBeenCalledWith(event, callback); }); it("stopListening is a no-op when not listening", async () => { @@ -361,10 +399,10 @@ describe("useEchoModel hook", async () => { const events = ["UserCreated", "UserUpdated"]; const { result, unmount } = renderHook(() => - echoModule.useEchoModel( + echoModule.useEchoModel( model, identifier, - ["UserCreated", "UserUpdated"], + events, mockCallback, ), ); @@ -375,25 +413,39 @@ describe("useEchoModel hook", async () => { expect(echoInstance.private).toHaveBeenCalledWith(expectedChannelName); const channel = echoInstance.private(expectedChannelName); + const firstCallback = (channel.listen as any).mock.calls[0][1]; + const secondCallback = (channel.listen as any).mock.calls[1][1]; expect(channel.listen).toHaveBeenCalledWith( `.${events[0]}`, - mockCallback, + expect.any(Function), ); expect(channel.listen).toHaveBeenCalledWith( `.${events[1]}`, - mockCallback, + expect.any(Function), + ); + + firstCallback({ data: "test" }); + expect(mockCallback).toHaveBeenCalledWith( + { data: "test" }, + `.${events[0]}`, + ); + + secondCallback({ data: "test" }); + expect(mockCallback).toHaveBeenCalledWith( + { data: "test" }, + `.${events[1]}`, ); expect(() => unmount()).not.toThrow(); expect(channel.stopListening).toHaveBeenCalledWith( `.${events[0]}`, - mockCallback, + firstCallback, ); expect(channel.stopListening).toHaveBeenCalledWith( `.${events[1]}`, - mockCallback, + secondCallback, ); }); @@ -522,7 +574,18 @@ describe("useEchoModel hook", async () => { expect(echoInstance.private).toHaveBeenCalledWith(expectedChannelName); const channel = echoInstance.private(expectedChannelName); - expect(channel.listen).toHaveBeenCalledWith(`.${event}`, mockCallback); + const callback = (channel.listen as any).mock.calls[0][1]; + + expect(channel.listen).toHaveBeenCalledWith( + `.${event}`, + expect.any(Function), + ); + + callback({ data: "test" }); + expect(mockCallback).toHaveBeenCalledWith( + { data: "test" }, + `.${event}`, + ); }); }); @@ -579,19 +642,33 @@ describe("useEchoPublic hook", async () => { expect(echoInstance.channel).toHaveBeenCalledWith(channelName); const channel = echoInstance.channel(channelName); + const firstCallback = (channel.listen as any).mock.calls[0][1]; + const secondCallback = (channel.listen as any).mock.calls[1][1]; + + expect(channel.listen).toHaveBeenCalledWith( + events[0], + expect.any(Function), + ); + expect(channel.listen).toHaveBeenCalledWith( + events[1], + expect.any(Function), + ); + + firstCallback({ data: "test" }); + expect(mockCallback).toHaveBeenCalledWith({ data: "test" }, events[0]); - expect(channel.listen).toHaveBeenCalledWith(events[0], mockCallback); - expect(channel.listen).toHaveBeenCalledWith(events[1], mockCallback); + secondCallback({ data: "test" }); + expect(mockCallback).toHaveBeenCalledWith({ data: "test" }, events[1]); expect(() => unmount()).not.toThrow(); expect(channel.stopListening).toHaveBeenCalledWith( events[0], - mockCallback, + firstCallback, ); expect(channel.stopListening).toHaveBeenCalledWith( events[1], - mockCallback, + secondCallback, ); }); @@ -708,36 +785,6 @@ describe("useEchoPresence hook", async () => { expect(typeof result.current.channel().whisper).toBe("function"); }); - it("handles multiple events", async () => { - const mockCallback = vi.fn(); - const channelName = "test-channel"; - const events = ["event1", "event2"]; - - const { result, unmount } = renderHook(() => - echoModule.useEchoPresence(channelName, events, mockCallback), - ); - - expect(result.current).toHaveProperty("leaveChannel"); - - expect(echoInstance.join).toHaveBeenCalledWith(channelName); - - const channel = echoInstance.join(channelName); - - expect(channel.listen).toHaveBeenCalledWith(events[0], mockCallback); - expect(channel.listen).toHaveBeenCalledWith(events[1], mockCallback); - - expect(() => unmount()).not.toThrow(); - - expect(channel.stopListening).toHaveBeenCalledWith( - events[0], - mockCallback, - ); - expect(channel.stopListening).toHaveBeenCalledWith( - events[1], - mockCallback, - ); - }); - it("cleans up subscriptions on unmount", async () => { const mockCallback = vi.fn(); const channelName = "test-channel"; @@ -810,4 +857,48 @@ describe("useEchoPresence hook", async () => { expect(echoInstance.leave).toHaveBeenCalledWith(channelName); }); + + it("handles multiple events", async () => { + const mockCallback = vi.fn(); + const channelName = "test-channel"; + const events = ["event1", "event2"]; + + const { result, unmount } = renderHook(() => + echoModule.useEchoPresence(channelName, events, mockCallback), + ); + + expect(result.current).toHaveProperty("leaveChannel"); + + expect(echoInstance.join).toHaveBeenCalledWith(channelName); + + const channel = echoInstance.join(channelName); + const firstCallback = (channel.listen as any).mock.calls[0][1]; + const secondCallback = (channel.listen as any).mock.calls[1][1]; + + expect(channel.listen).toHaveBeenCalledWith( + events[0], + expect.any(Function), + ); + expect(channel.listen).toHaveBeenCalledWith( + events[1], + expect.any(Function), + ); + + firstCallback({ data: "test" }); + expect(mockCallback).toHaveBeenCalledWith({ data: "test" }, events[0]); + + secondCallback({ data: "test" }); + expect(mockCallback).toHaveBeenCalledWith({ data: "test" }, events[1]); + + expect(() => unmount()).not.toThrow(); + + expect(channel.stopListening).toHaveBeenCalledWith( + events[0], + firstCallback, + ); + expect(channel.stopListening).toHaveBeenCalledWith( + events[1], + secondCallback, + ); + }); }); From 5f6374e484fd244e8d433dd9c67dbf62d0da9170 Mon Sep 17 00:00:00 2001 From: Joe Tannenbaum Date: Mon, 5 May 2025 15:36:47 -0400 Subject: [PATCH 04/10] Update use-echo.ts --- packages/react/src/hooks/use-echo.ts | 50 +++++++++++++++++----------- 1 file changed, 30 insertions(+), 20 deletions(-) diff --git a/packages/react/src/hooks/use-echo.ts b/packages/react/src/hooks/use-echo.ts index 7187fc1b..4f984859 100644 --- a/packages/react/src/hooks/use-echo.ts +++ b/packages/react/src/hooks/use-echo.ts @@ -6,7 +6,6 @@ import type { ChannelData, ChannelReturnType, Connection, - ModelEvents, ModelPayload, } from "../types"; import { toArray } from "../util"; @@ -78,24 +77,36 @@ export const useEcho = < TPayload, TDriver extends BroadcastDriver = BroadcastDriver, TVisibility extends Channel["visibility"] = "private", + TEvent extends string = string, >( channelName: string, - event: string | string[], - callback: (payload: TPayload, eventName: string) => void, + event: TEvent | TEvent[], + callback: (payload: TPayload, eventName: TEvent) => void, dependencies: any[] = [], visibility: TVisibility = "private" as TVisibility, ) => { + const events = toArray(event); const callbacks = useRef< - Record void> - >({}); + Record void> + >( + events.reduce( + (acc, e) => { + acc[e] = (payload: TPayload) => callback(payload, e); + return acc; + }, + {} as Record< + TEvent, + (payload: TPayload, eventName: TEvent) => void + >, + ), + ); const allCallbackFunc = useCallback( - (eventName: string, payload: TPayload) => callback(payload, eventName), + (eventName: TEvent, payload: TPayload) => callback(payload, eventName), dependencies, ); const subscription = useRef | null>(null); const listening = useRef(false); - const events = toArray(event); const channel: Channel = { name: channelName, id: ["private", "presence"].includes(visibility) @@ -104,10 +115,6 @@ export const useEcho = < visibility, }; - events.forEach((e) => { - callbacks.current[e] = (payload: TPayload) => callback(payload, e); - }); - const stopListening = useCallback(() => { if (!listening.current) { return; @@ -194,13 +201,14 @@ export const useEcho = < export const useEchoPresence = < TPayload, TDriver extends BroadcastDriver = BroadcastDriver, + TEvent extends string = string, >( channelName: string, - event: string | string[], - callback: (payload: TPayload) => void, + event: TEvent | TEvent[], + callback: (payload: TPayload, eventName: TEvent) => void, dependencies: any[] = [], ) => { - return useEcho( + return useEcho( channelName, event, callback, @@ -212,13 +220,14 @@ export const useEchoPresence = < export const useEchoPublic = < TPayload, TDriver extends BroadcastDriver = BroadcastDriver, + TEvent extends string = string, >( channelName: string, - event: string | string[], - callback: (payload: TPayload) => void, + event: TEvent | TEvent[], + callback: (payload: TPayload, eventName: TEvent) => void, dependencies: any[] = [], ) => { - return useEcho( + return useEcho( channelName, event, callback, @@ -231,14 +240,15 @@ export const useEchoModel = < TPayload, TModel extends string, TDriver extends BroadcastDriver = BroadcastDriver, + TEvent extends string = string, >( model: TModel, identifier: string | number, - event: ModelEvents | ModelEvents[], - callback: (payload: ModelPayload) => void, + event: TEvent | TEvent[], + callback: (payload: ModelPayload, eventName: TEvent) => void, dependencies: any[] = [], ) => { - return useEcho, TDriver, "private">( + return useEcho, TDriver, "private", TEvent>( `${model}.${identifier}`, toArray(event).map((e) => (e.startsWith(".") ? e : `.${e}`)), callback, From 49df0e052c8e29cd15934dd42c47f0680a40d15c Mon Sep 17 00:00:00 2001 From: Joe Tannenbaum Date: Mon, 5 May 2025 15:48:06 -0400 Subject: [PATCH 05/10] Update use-echo.ts --- packages/react/src/hooks/use-echo.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/react/src/hooks/use-echo.ts b/packages/react/src/hooks/use-echo.ts index 4f984859..e55c8ef0 100644 --- a/packages/react/src/hooks/use-echo.ts +++ b/packages/react/src/hooks/use-echo.ts @@ -250,7 +250,9 @@ export const useEchoModel = < ) => { return useEcho, TDriver, "private", TEvent>( `${model}.${identifier}`, - toArray(event).map((e) => (e.startsWith(".") ? e : `.${e}`)), + toArray(event).map((e) => + e.startsWith(".") ? e : `.${e}`, + ) as TEvent[], callback, dependencies, "private", From add5e355cf9523f91c231ca061d05576eb4ad0fc Mon Sep 17 00:00:00 2001 From: Joe Tannenbaum Date: Mon, 5 May 2025 16:05:15 -0400 Subject: [PATCH 06/10] better event typing --- packages/react/src/hooks/use-echo.ts | 3 +- packages/vue/src/composables/useEcho.ts | 54 +++++++++++++++++++------ 2 files changed, 43 insertions(+), 14 deletions(-) diff --git a/packages/react/src/hooks/use-echo.ts b/packages/react/src/hooks/use-echo.ts index e55c8ef0..7cdeff99 100644 --- a/packages/react/src/hooks/use-echo.ts +++ b/packages/react/src/hooks/use-echo.ts @@ -6,6 +6,7 @@ import type { ChannelData, ChannelReturnType, Connection, + ModelEvents, ModelPayload, } from "../types"; import { toArray } from "../util"; @@ -240,7 +241,7 @@ export const useEchoModel = < TPayload, TModel extends string, TDriver extends BroadcastDriver = BroadcastDriver, - TEvent extends string = string, + TEvent extends ModelEvents = ModelEvents, >( model: TModel, identifier: string | number, diff --git a/packages/vue/src/composables/useEcho.ts b/packages/vue/src/composables/useEcho.ts index 1cf0f8e7..a2f5bde9 100644 --- a/packages/vue/src/composables/useEcho.ts +++ b/packages/vue/src/composables/useEcho.ts @@ -78,14 +78,29 @@ export const useEcho = < TPayload, TDriver extends BroadcastDriver = BroadcastDriver, TVisibility extends Channel["visibility"] = "private", + TEvent extends string = string, >( channelName: string, - event: string | string[], - callback: (payload: TPayload) => void, + event: TEvent | TEvent[], + callback: (payload: TPayload, eventName: TEvent) => void, dependencies: any[] = [], visibility: TVisibility = "private" as TVisibility, ) => { - const eventCallback = ref(callback); + const events = toArray(event); + const eventCallback = ref< + Record void> + >( + events.reduce( + (acc, e) => { + acc[e] = (payload: TPayload) => callback(payload, e); + return acc; + }, + {} as Record< + TEvent, + (payload: TPayload, eventName: TEvent) => void + >, + ), + ); const listening = ref(false); watch( @@ -96,7 +111,7 @@ export const useEcho = < ); let subscription: Connection | null = null; - const events = Array.isArray(event) ? event : [event]; + const channel: Channel = { name: channelName, id: ["private", "presence"].includes(visibility) @@ -121,7 +136,11 @@ export const useEcho = < } events.forEach((e) => { - subscription!.listen(e, eventCallback.value); + if (e !== "*") { + subscription!.listen(e, eventCallback.value[e]); + } else if ("listenToAll" in subscription!) { + subscription.listenToAll(eventCallback.value[e]); + } }); listening.value = true; @@ -133,7 +152,11 @@ export const useEcho = < } events.forEach((e) => { - subscription!.stopListening(e, eventCallback.value); + if (e !== "*") { + subscription!.stopListening(e, eventCallback.value[e]); + } else if ("stopListeningToAll" in subscription!) { + subscription.stopListeningToAll(eventCallback.value[e]); + } }); listening.value = false; @@ -191,10 +214,11 @@ export const useEcho = < export const useEchoPresence = < TPayload, TDriver extends BroadcastDriver = BroadcastDriver, + TEvent extends string = string, >( channelName: string, - event: string | string[], - callback: (payload: TPayload) => void, + event: TEvent | TEvent[], + callback: (payload: TPayload, eventName: TEvent) => void, dependencies: any[] = [], ) => { return useEcho( @@ -209,10 +233,11 @@ export const useEchoPresence = < export const useEchoPublic = < TPayload, TDriver extends BroadcastDriver = BroadcastDriver, + TEvent extends string = string, >( channelName: string, - event: string | string[], - callback: (payload: TPayload) => void, + event: TEvent | TEvent[], + callback: (payload: TPayload, eventName: TEvent) => void, dependencies: any[] = [], ) => { return useEcho( @@ -228,16 +253,19 @@ export const useEchoModel = < TPayload, TModel extends string, TDriver extends BroadcastDriver = BroadcastDriver, + TEvent extends ModelEvents = ModelEvents, >( model: TModel, identifier: string | number, - event: ModelEvents | ModelEvents[], - callback: (payload: ModelPayload) => void, + event: TEvent | TEvent[], + callback: (payload: ModelPayload, eventName: TEvent) => void, dependencies: any[] = [], ) => { return useEcho, TDriver, "private">( `${model}.${identifier}`, - toArray(event).map((e) => (e.startsWith(".") ? e : `.${e}`)), + toArray(event).map((e) => + e.startsWith(".") ? e : `.${e}`, + ) as TEvent[], callback, dependencies, "private", From 507a960734242ad0550515f18fc7cbc241d61217 Mon Sep 17 00:00:00 2001 From: Joe Tannenbaum Date: Mon, 5 May 2025 16:13:40 -0400 Subject: [PATCH 07/10] Update tasks.json --- .vscode/tasks.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.vscode/tasks.json b/.vscode/tasks.json index 43e5a27f..fa2185c7 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -4,13 +4,13 @@ { "type": "npm", "script": "build", - "path": "packages/react", + "path": ".", "group": { "kind": "build", "isDefault": true }, "problemMatcher": [], - "label": "npm: build - packages/react", + "label": "pnpm: build all packages", "detail": "vite build && FORMAT=iife vite build" } ] From 1791cca877d3f6253e4399ff9d1e90becf371bcd Mon Sep 17 00:00:00 2001 From: Joe Tannenbaum Date: Mon, 5 May 2025 16:13:45 -0400 Subject: [PATCH 08/10] Update useEcho.ts --- packages/vue/src/composables/useEcho.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/vue/src/composables/useEcho.ts b/packages/vue/src/composables/useEcho.ts index a2f5bde9..7596643d 100644 --- a/packages/vue/src/composables/useEcho.ts +++ b/packages/vue/src/composables/useEcho.ts @@ -221,7 +221,7 @@ export const useEchoPresence = < callback: (payload: TPayload, eventName: TEvent) => void, dependencies: any[] = [], ) => { - return useEcho( + return useEcho( channelName, event, callback, @@ -240,7 +240,7 @@ export const useEchoPublic = < callback: (payload: TPayload, eventName: TEvent) => void, dependencies: any[] = [], ) => { - return useEcho( + return useEcho( channelName, event, callback, @@ -261,7 +261,7 @@ export const useEchoModel = < callback: (payload: ModelPayload, eventName: TEvent) => void, dependencies: any[] = [], ) => { - return useEcho, TDriver, "private">( + return useEcho, TDriver, "private", TEvent>( `${model}.${identifier}`, toArray(event).map((e) => e.startsWith(".") ? e : `.${e}`, From 0c7858dd398a7c5c978cec6db49e97bf0610a114 Mon Sep 17 00:00:00 2001 From: Joe Tannenbaum Date: Mon, 5 May 2025 16:16:13 -0400 Subject: [PATCH 09/10] Update useEcho.test.ts --- packages/vue/tests/useEcho.test.ts | 120 ++++++++++++++++++++++------- 1 file changed, 93 insertions(+), 27 deletions(-) diff --git a/packages/vue/tests/useEcho.test.ts b/packages/vue/tests/useEcho.test.ts index d90fe647..3f1f3359 100644 --- a/packages/vue/tests/useEcho.test.ts +++ b/packages/vue/tests/useEcho.test.ts @@ -206,19 +206,33 @@ describe("useEcho hook", async () => { expect(echoInstance.private).toHaveBeenCalledWith(channelName); const channel = echoInstance.private(channelName); + const firstCallback = (channel.listen as any).mock.calls[0][1]; + const secondCallback = (channel.listen as any).mock.calls[1][1]; - expect(channel.listen).toHaveBeenCalledWith(events[0], mockCallback); - expect(channel.listen).toHaveBeenCalledWith(events[1], mockCallback); + expect(channel.listen).toHaveBeenCalledWith( + events[0], + expect.any(Function), + ); + expect(channel.listen).toHaveBeenCalledWith( + events[1], + expect.any(Function), + ); + + firstCallback({ data: "test" }); + expect(mockCallback).toHaveBeenCalledWith({ data: "test" }, events[0]); + + secondCallback({ data: "test" }); + expect(mockCallback).toHaveBeenCalledWith({ data: "test" }, events[1]); wrapper.unmount(); expect(channel.stopListening).toHaveBeenCalledWith( events[0], - mockCallback, + firstCallback, ); expect(channel.stopListening).toHaveBeenCalledWith( events[1], - mockCallback, + secondCallback, ); }); @@ -265,10 +279,16 @@ describe("useEcho hook", async () => { expect(echoInstance.private).toHaveBeenCalledWith(channelName); - expect(echoInstance.private(channelName).listen).toHaveBeenCalledWith( + const channel = echoInstance.private(channelName); + const callback = (channel.listen as any).mock.calls[0][1]; + + expect(channel.listen).toHaveBeenCalledWith( event, - mockCallback, + expect.any(Function), ); + + callback({ data: "test" }); + expect(mockCallback).toHaveBeenCalledWith({ data: "test" }, event); }); it("can leave a channel", async () => { @@ -325,18 +345,24 @@ describe("useEcho hook", async () => { wrapper = getTestComponent(channelName, event, mockCallback); const mockChannel = echoInstance.private(channelName); - expect(mockChannel.listen).toHaveBeenCalledWith(event, mockCallback); + const callback = (mockChannel.listen as any).mock.calls[0][1]; + + expect(mockChannel.listen).toHaveBeenCalledWith(event, callback); wrapper.vm.stopListening(); - expect(mockChannel.stopListening).toHaveBeenCalledWith( - event, - mockCallback, - ); + expect(mockChannel.stopListening).toHaveBeenCalledWith(event, callback); wrapper.vm.listen(); - expect(mockChannel.listen).toHaveBeenCalledWith(event, mockCallback); + const newCallback = (mockChannel.listen as any).mock.calls[1][1]; + expect(mockChannel.listen).toHaveBeenCalledWith( + event, + expect.any(Function), + ); + + newCallback({ data: "test" }); + expect(mockCallback).toHaveBeenCalledWith({ data: "test" }, event); }); it("listen method is a no-op when already listening", async () => { @@ -360,12 +386,11 @@ describe("useEcho hook", async () => { wrapper = getTestComponent(channelName, event, mockCallback); const mockChannel = echoInstance.private(channelName); + const callback = (mockChannel.listen as any).mock.calls[0][1]; + wrapper.vm.stopListening(); - expect(mockChannel.stopListening).toHaveBeenCalledWith( - event, - mockCallback, - ); + expect(mockChannel.stopListening).toHaveBeenCalledWith(event, callback); }); it("stopListening method is a no-op when not listening", async () => { @@ -391,28 +416,41 @@ describe("useEcho hook", async () => { const mockChannel = echoInstance.private(channelName); events.forEach((event) => { + const callback = (mockChannel.listen as any).mock.calls[ + events.indexOf(event) + ][1]; expect(mockChannel.listen).toHaveBeenCalledWith( event, - mockCallback, + expect.any(Function), ); + callback({ data: "test" }); + expect(mockCallback).toHaveBeenCalledWith({ data: "test" }, event); }); wrapper.vm.stopListening(); wrapper.vm.listen(); events.forEach((event) => { + const callback = (mockChannel.listen as any).mock.calls[ + events.indexOf(event) + ][1]; expect(mockChannel.listen).toHaveBeenCalledWith( event, - mockCallback, + expect.any(Function), ); + callback({ data: "test" }); + expect(mockCallback).toHaveBeenCalledWith({ data: "test" }, event); }); wrapper.vm.stopListening(); events.forEach((event) => { + const callback = (mockChannel.stopListening as any).mock.calls[ + events.indexOf(event) + ][1]; expect(mockChannel.stopListening).toHaveBeenCalledWith( event, - mockCallback, + callback, ); }); }); @@ -462,19 +500,33 @@ describe("useEchoPublic hook", async () => { expect(echoInstance.channel).toHaveBeenCalledWith(channelName); const channel = echoInstance.channel(channelName); + const firstCallback = (channel.listen as any).mock.calls[0][1]; + const secondCallback = (channel.listen as any).mock.calls[1][1]; + + expect(channel.listen).toHaveBeenCalledWith( + events[0], + expect.any(Function), + ); + expect(channel.listen).toHaveBeenCalledWith( + events[1], + expect.any(Function), + ); + + firstCallback({ data: "test" }); + expect(mockCallback).toHaveBeenCalledWith({ data: "test" }, events[0]); - expect(channel.listen).toHaveBeenCalledWith(events[0], mockCallback); - expect(channel.listen).toHaveBeenCalledWith(events[1], mockCallback); + secondCallback({ data: "test" }); + expect(mockCallback).toHaveBeenCalledWith({ data: "test" }, events[1]); wrapper.unmount(); expect(channel.stopListening).toHaveBeenCalledWith( events[0], - mockCallback, + firstCallback, ); expect(channel.stopListening).toHaveBeenCalledWith( events[1], - mockCallback, + secondCallback, ); }); @@ -591,19 +643,33 @@ describe("useEchoPresence hook", async () => { expect(echoInstance.join).toHaveBeenCalledWith(channelName); const channel = echoInstance.join(channelName); + const firstCallback = (channel.listen as any).mock.calls[0][1]; + const secondCallback = (channel.listen as any).mock.calls[1][1]; + + expect(channel.listen).toHaveBeenCalledWith( + events[0], + expect.any(Function), + ); + expect(channel.listen).toHaveBeenCalledWith( + events[1], + expect.any(Function), + ); + + firstCallback({ data: "test" }); + expect(mockCallback).toHaveBeenCalledWith({ data: "test" }, events[0]); - expect(channel.listen).toHaveBeenCalledWith(events[0], mockCallback); - expect(channel.listen).toHaveBeenCalledWith(events[1], mockCallback); + secondCallback({ data: "test" }); + expect(mockCallback).toHaveBeenCalledWith({ data: "test" }, events[1]); wrapper.unmount(); expect(channel.stopListening).toHaveBeenCalledWith( events[0], - mockCallback, + firstCallback, ); expect(channel.stopListening).toHaveBeenCalledWith( events[1], - mockCallback, + secondCallback, ); }); From 5df2ec6542fd0872f9f249c167b5ee5b81df406b Mon Sep 17 00:00:00 2001 From: Joe Tannenbaum Date: Mon, 5 May 2025 16:35:28 -0400 Subject: [PATCH 10/10] dots before model events --- packages/react/src/types.ts | 30 +++++++++++++++--------------- packages/vue/src/types.ts | 30 +++++++++++++++--------------- 2 files changed, 30 insertions(+), 30 deletions(-) diff --git a/packages/react/src/types.ts b/packages/react/src/types.ts index 38f1293a..e7566749 100644 --- a/packages/react/src/types.ts +++ b/packages/react/src/types.ts @@ -43,18 +43,18 @@ export type ModelName = T extends `${infer _}.${infer U}` : T; export type ModelEvents = - | `${ModelName}Retrieved` - | `${ModelName}Creating` - | `${ModelName}Created` - | `${ModelName}Updating` - | `${ModelName}Updated` - | `${ModelName}Saving` - | `${ModelName}Saved` - | `${ModelName}Deleting` - | `${ModelName}Deleted` - | `${ModelName}Trashed` - | `${ModelName}ForceDeleting` - | `${ModelName}ForceDeleted` - | `${ModelName}Restoring` - | `${ModelName}Restored` - | `${ModelName}Replicating`; + | `.${ModelName}Retrieved` + | `.${ModelName}Creating` + | `.${ModelName}Created` + | `.${ModelName}Updating` + | `.${ModelName}Updated` + | `.${ModelName}Saving` + | `.${ModelName}Saved` + | `.${ModelName}Deleting` + | `.${ModelName}Deleted` + | `.${ModelName}Trashed` + | `.${ModelName}ForceDeleting` + | `.${ModelName}ForceDeleted` + | `.${ModelName}Restoring` + | `.${ModelName}Restored` + | `.${ModelName}Replicating`; diff --git a/packages/vue/src/types.ts b/packages/vue/src/types.ts index 38f1293a..e7566749 100644 --- a/packages/vue/src/types.ts +++ b/packages/vue/src/types.ts @@ -43,18 +43,18 @@ export type ModelName = T extends `${infer _}.${infer U}` : T; export type ModelEvents = - | `${ModelName}Retrieved` - | `${ModelName}Creating` - | `${ModelName}Created` - | `${ModelName}Updating` - | `${ModelName}Updated` - | `${ModelName}Saving` - | `${ModelName}Saved` - | `${ModelName}Deleting` - | `${ModelName}Deleted` - | `${ModelName}Trashed` - | `${ModelName}ForceDeleting` - | `${ModelName}ForceDeleted` - | `${ModelName}Restoring` - | `${ModelName}Restored` - | `${ModelName}Replicating`; + | `.${ModelName}Retrieved` + | `.${ModelName}Creating` + | `.${ModelName}Created` + | `.${ModelName}Updating` + | `.${ModelName}Updated` + | `.${ModelName}Saving` + | `.${ModelName}Saved` + | `.${ModelName}Deleting` + | `.${ModelName}Deleted` + | `.${ModelName}Trashed` + | `.${ModelName}ForceDeleting` + | `.${ModelName}ForceDeleted` + | `.${ModelName}Restoring` + | `.${ModelName}Restored` + | `.${ModelName}Replicating`;