Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions packages/laravel-echo/src/channel/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,6 @@ export * from "./null-channel";
export * from "./null-private-channel";
export * from "./null-encrypted-private-channel";
export * from "./null-presence-channel";
export * from "./poll-channel";
export * from "./poll-private-channel";
export * from "./poll-presence-channel";
139 changes: 139 additions & 0 deletions packages/laravel-echo/src/channel/poll-channel.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
import { EventFormatter } from "../util";
import { Channel } from "./channel";
import type { EchoOptionsWithDefaults } from "../connector";

/**
* This class represents a poll channel.
*/
export class PollChannel extends Channel {
/**
* The name of the channel.
*/
name: string;

/**
* The event formatter.
*/
eventFormatter: EventFormatter;

/**
* Local event listener registry.
*/
private listeners: Map<string, Set<CallableFunction>> = new Map();

/**
* Subscription success callbacks.
*/
private subscribedCallbacks: Set<CallableFunction> = new Set();

/**
* Error callbacks.
*/
private errorCallbacks: Set<CallableFunction> = new Set();

/**
* Create a new class instance.
*/
constructor(name: string, options: EchoOptionsWithDefaults<"poll">) {
super();
this.name = name;
this.options = options;
this.eventFormatter = new EventFormatter(this.options.namespace);
}

/**
* Dispatch an event to local listeners.
*/
dispatch(event: string, data: any): void {
const callbacks = this.listeners.get(event);
if (callbacks) {
callbacks.forEach((cb) => cb(data));
}
}

/**
* Notify that subscription succeeded.
*/
notifySubscribed(): void {
this.subscribedCallbacks.forEach((cb) => cb());
}

/**
* Notify that an error occurred.
*/
notifyError(error: any): void {
this.errorCallbacks.forEach((cb) => cb(error));
}

/**
* Subscribe to a channel.
*/
subscribe(): void {
//
}

/**
* Unsubscribe from a channel.
*/
unsubscribe(): void {
this.listeners.clear();
this.subscribedCallbacks.clear();
this.errorCallbacks.clear();
}

/**
* Listen for an event on the channel instance.
*/
listen(event: string, callback: CallableFunction): this {
this.on(this.eventFormatter.format(event), callback);
return this;
}

/**
* Listen for all events on the channel instance.
*/
listenToAll(callback: CallableFunction): this {
this.on("*", callback);
return this;
}

/**
* Stop listening for an event on the channel instance.
*/
stopListening(event: string, callback?: CallableFunction): this {
const formatted = this.eventFormatter.format(event);
if (callback) {
this.listeners.get(formatted)?.delete(callback);
} else {
this.listeners.delete(formatted);
}
return this;
}

/**
* Register a callback to be called anytime a subscription succeeds.
*/
subscribed(callback: CallableFunction): this {
this.subscribedCallbacks.add(callback);
return this;
}

/**
* Register a callback to be called anytime an error occurs.
*/
error(callback: CallableFunction): this {
this.errorCallbacks.add(callback);
return this;
}

/**
* Bind a channel to an event.
*/
on(event: string, callback: CallableFunction): this {
if (!this.listeners.has(event)) {
this.listeners.set(event, new Set());
}
this.listeners.get(event)!.add(callback);
return this;
}
}
111 changes: 111 additions & 0 deletions packages/laravel-echo/src/channel/poll-presence-channel.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
import type { PresenceChannel } from "./presence-channel";
import { PollPrivateChannel } from "./poll-private-channel";

/**
* This class represents a poll presence channel.
*/
export class PollPresenceChannel
extends PollPrivateChannel
implements PresenceChannel
{
/**
* Callbacks for the here event.
*/
private hereCallbacks: Set<CallableFunction> = new Set();

/**
* Callbacks for the joining event.
*/
private joiningCallbacks: Set<CallableFunction> = new Set();

/**
* Callbacks for the leaving event.
*/
private leavingCallbacks: Set<CallableFunction> = new Set();

/**
* Track known member IDs for client-side join/leave detection.
*
* The server returns only the current members list. Each client
* diffs against its own known members to detect joins and leaves,
* ensuring every client sees every change regardless of poll timing.
*/
private knownMembers: Map<string | number, any> = new Map();

/**
* Register a callback to be called anytime the member list changes.
*/
here(callback: CallableFunction): this {
this.hereCallbacks.add(callback);
return this;
}

/**
* Listen for someone joining the channel.
*/
joining(callback: CallableFunction): this {
this.joiningCallbacks.add(callback);
return this;
}

/**
* Listen for someone leaving the channel.
*/
leaving(callback: CallableFunction): this {
this.leavingCallbacks.add(callback);
return this;
}

/**
* Send a whisper event to other clients in the channel.
*/
whisper(_eventName: string, _data: Record<any, any>): this {
return this;
}

/**
* Update presence data from the poll response.
*
* The server returns only { members: [...] }. This method computes
* joined/left by diffing against the client's own known members.
*/
updatePresence(data: { members: any[] }): void {
const currentMembers = data.members ?? [];
const currentIds = new Set(
currentMembers.map((m: any) => m.user_id),
);

// Detect newly joined members
for (const member of currentMembers) {
if (!this.knownMembers.has(member.user_id)) {
this.joiningCallbacks.forEach((cb) => cb(member));
}
}

// Detect members who left
for (const [id, member] of this.knownMembers) {
if (!currentIds.has(id)) {
this.leavingCallbacks.forEach((cb) => cb(member));
}
}

// Update known members
this.knownMembers = new Map(
currentMembers.map((m: any) => [m.user_id, m]),
);

// Fire here() with current member list
this.hereCallbacks.forEach((cb) => cb(currentMembers));
}

/**
* Unsubscribe from a channel.
*/
unsubscribe(): void {
super.unsubscribe();
this.hereCallbacks.clear();
this.joiningCallbacks.clear();
this.leavingCallbacks.clear();
this.knownMembers.clear();
}
}
13 changes: 13 additions & 0 deletions packages/laravel-echo/src/channel/poll-private-channel.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { PollChannel } from "./poll-channel";

/**
* This class represents a poll private channel.
*/
export class PollPrivateChannel extends PollChannel {
/**
* Send a whisper event to other clients in the channel.
*/
whisper(_eventName: string, _data: Record<any, any>): this {
return this;
}
}
1 change: 1 addition & 0 deletions packages/laravel-echo/src/connector/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@ export * from "./connector";
export * from "./pusher-connector";
export * from "./socketio-connector";
export * from "./null-connector";
export * from "./poll-connector";
Loading
Loading