@statezero/core 0.1.67 → 0.1.68

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -69,11 +69,17 @@ export class PusherEventReceiver {
69
69
  */
70
70
  constructor(options: PusherReceiverOptions, configKey: string);
71
71
  configKey: string;
72
+ connectionTimeoutId: NodeJS.Timeout;
72
73
  pusherClient: Pusher;
73
74
  formatChannelName: (ns: string) => string;
74
75
  namespaceResolver: (modelName: string) => string;
75
76
  channels: Map<any, any>;
76
77
  eventHandlers: Set<any>;
78
+ /**
79
+ * @private
80
+ * @param {string} reason
81
+ */
82
+ private _logConnectionError;
77
83
  /**
78
84
  * Set the namespace resolver function.
79
85
  * @param {NamespaceResolver} resolver
@@ -176,4 +182,4 @@ export type PusherReceiverOptions = {
176
182
  */
177
183
  namespaceResolver?: NamespaceResolver | undefined;
178
184
  };
179
- import Pusher from 'pusher-js';
185
+ import Pusher from "pusher-js";
@@ -1,4 +1,4 @@
1
- import Pusher from 'pusher-js';
1
+ import Pusher from "pusher-js";
2
2
  /**
3
3
  * Structure of events received from the server.
4
4
  * @typedef {Object} ModelEvent
@@ -19,11 +19,11 @@ import Pusher from 'pusher-js';
19
19
  * @enum {string}
20
20
  */
21
21
  export const EventType = {
22
- CREATE: 'create',
23
- UPDATE: 'update',
24
- DELETE: 'delete',
25
- BULK_UPDATE: 'bulk_update',
26
- BULK_DELETE: 'bulk_delete'
22
+ CREATE: "create",
23
+ UPDATE: "update",
24
+ DELETE: "delete",
25
+ BULK_UPDATE: "bulk_update",
26
+ BULK_DELETE: "bulk_delete",
27
27
  };
28
28
  /**
29
29
  * Callback for handling model events.
@@ -62,18 +62,67 @@ export class PusherEventReceiver {
62
62
  */
63
63
  constructor(options, configKey) {
64
64
  const { clientOptions, formatChannelName, namespaceResolver } = options;
65
+ const CONNECTION_TIMEOUT = 10000; // 10 seconds
65
66
  this.configKey = configKey;
67
+ this.connectionTimeoutId = null;
68
+ if (clientOptions.appKey &&
69
+ /^\d+$/.test(clientOptions.appKey) &&
70
+ clientOptions.appKey.length < 15) {
71
+ console.warn(`%c[Pusher Warning] The provided appKey ("${clientOptions.appKey}") looks like a numeric app_id. Pusher requires the alphanumeric key, not the ID. Please verify your configuration for backend: "${this.configKey}".`, "color: orange; font-weight: bold; font-size: 14px;");
72
+ }
66
73
  this.pusherClient = new Pusher(clientOptions.appKey, {
67
74
  cluster: clientOptions.cluster,
68
75
  forceTLS: clientOptions.forceTLS ?? true,
69
76
  authEndpoint: clientOptions.authEndpoint,
70
- auth: { headers: clientOptions.getAuthHeaders?.() || {} }
77
+ auth: { headers: clientOptions.getAuthHeaders?.() || {} },
71
78
  });
72
- this.formatChannelName = formatChannelName ?? (ns => `private-${ns}`);
73
- this.namespaceResolver = namespaceResolver ?? (modelName => modelName);
79
+ this.pusherClient.connection.bind("connected", () => {
80
+ console.log(`Pusher client connected successfully for backend: ${this.configKey}.`);
81
+ if (this.connectionTimeoutId) {
82
+ clearTimeout(this.connectionTimeoutId);
83
+ this.connectionTimeoutId = null;
84
+ }
85
+ });
86
+ this.pusherClient.connection.bind("failed", () => {
87
+ this._logConnectionError("Pusher connection explicitly failed.");
88
+ if (this.connectionTimeoutId) {
89
+ clearTimeout(this.connectionTimeoutId);
90
+ this.connectionTimeoutId = null;
91
+ }
92
+ });
93
+ this.connectionTimeoutId = setTimeout(() => {
94
+ if (this.pusherClient.connection.state !== "connected") {
95
+ this._logConnectionError(`Pusher connection timed out after ${CONNECTION_TIMEOUT / 1000} seconds.`);
96
+ }
97
+ }, CONNECTION_TIMEOUT);
98
+ this.formatChannelName = formatChannelName ?? ((ns) => `private-${ns}`);
99
+ this.namespaceResolver = namespaceResolver ?? ((modelName) => modelName);
74
100
  this.channels = new Map();
75
101
  this.eventHandlers = new Set();
76
102
  }
103
+ /**
104
+ * @private
105
+ * @param {string} reason
106
+ */
107
+ _logConnectionError(reason) {
108
+ console.error(`%c
109
+ ████████████████████████████████████████████████████████████████
110
+ █ █
111
+ █ PUSHER CONNECTION FAILED for backend: "${this.configKey}" █
112
+ █ █
113
+ ████████████████████████████████████████████████████████████████
114
+ %c
115
+ Reason: ${reason}
116
+
117
+ CRITICAL: Real-time updates from the server will NOT be received.
118
+ This application will not reflect remote changes propagated via Pusher.
119
+
120
+ Common causes:
121
+ 1. Incorrect 'appKey' or 'cluster' in the configuration.
122
+ 2. The 'authEndpoint' is unreachable or returning an error (check network tab).
123
+ 3. Network connectivity issues (firewall, offline).
124
+ 4. Using an 'app_id' instead of the 'appKey'.`, "background-color: red; color: white; font-weight: bold; font-size: 16px; padding: 10px;", "color: red; font-size: 12px;");
125
+ }
77
126
  /**
78
127
  * Set the namespace resolver function.
79
128
  * @param {NamespaceResolver} resolver
@@ -92,27 +141,29 @@ export class PusherEventReceiver {
92
141
  subscribe(namespace) {
93
142
  if (this.channels.has(namespace))
94
143
  return;
95
- const channelName = namespace.startsWith('private-')
144
+ const channelName = namespace.startsWith("private-")
96
145
  ? namespace
97
146
  : this.formatChannelName(namespace);
98
147
  console.log(`Subscribing to channel: ${channelName} for backend: ${this.configKey}`);
99
148
  const channel = this.pusherClient.subscribe(channelName);
100
- channel.bind('pusher:subscription_succeeded', () => {
149
+ channel.bind("pusher:subscription_succeeded", () => {
101
150
  console.log(`Subscription succeeded for channel: ${channelName}`);
102
151
  });
103
- channel.bind('pusher:subscription_error', status => {
152
+ channel.bind("pusher:subscription_error", (status) => {
104
153
  console.error(`Subscription error for channel: ${channelName}. Status:`, status);
154
+ if (status.status === 401 || status.status === 403) {
155
+ console.error(`%cAuthentication failed for channel ${channelName}. Check your authEndpoint and server-side permissions.`, "color: orange; font-weight: bold;");
156
+ }
105
157
  });
106
- // Listen for CRUD events
107
- Object.values(EventType).forEach(eventType => {
108
- channel.bind(eventType, data => {
158
+ Object.values(EventType).forEach((eventType) => {
159
+ channel.bind(eventType, (data) => {
109
160
  const event = {
110
161
  ...data,
111
162
  type: data.event || eventType,
112
163
  namespace,
113
- configKey: this.configKey
164
+ configKey: this.configKey,
114
165
  };
115
- this.eventHandlers.forEach(handler => handler(event));
166
+ this.eventHandlers.forEach((handler) => handler(event));
116
167
  });
117
168
  });
118
169
  this.channels.set(namespace, channel);
@@ -121,10 +172,10 @@ export class PusherEventReceiver {
121
172
  const channel = this.channels.get(namespace);
122
173
  if (!channel)
123
174
  return;
124
- Object.values(EventType).forEach(eventType => {
175
+ Object.values(EventType).forEach((eventType) => {
125
176
  channel.unbind(eventType);
126
177
  });
127
- const channelName = namespace.startsWith('private-')
178
+ const channelName = namespace.startsWith("private-")
128
179
  ? namespace
129
180
  : this.formatChannelName(namespace);
130
181
  this.pusherClient.unsubscribe(channelName);
@@ -134,7 +185,11 @@ export class PusherEventReceiver {
134
185
  * Disconnect from Pusher.
135
186
  */
136
187
  disconnect() {
137
- [...this.channels.keys()].forEach(ns => this.unsubscribe(ns));
188
+ if (this.connectionTimeoutId) {
189
+ clearTimeout(this.connectionTimeoutId);
190
+ this.connectionTimeoutId = null;
191
+ }
192
+ [...this.channels.keys()].forEach((ns) => this.unsubscribe(ns));
138
193
  this.pusherClient.disconnect();
139
194
  }
140
195
  /**
@@ -187,7 +242,7 @@ export function setEventReceiver(configKey, receiver) {
187
242
  * @param {string} configKey - The backend configuration key
188
243
  * @returns {EventReceiver|null}
189
244
  */
190
- export function getEventReceiver(configKey = 'default') {
245
+ export function getEventReceiver(configKey = "default") {
191
246
  return eventReceivers.get(configKey);
192
247
  }
193
248
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@statezero/core",
3
- "version": "0.1.67",
3
+ "version": "0.1.68",
4
4
  "type": "module",
5
5
  "module": "ESNext",
6
6
  "description": "The type-safe frontend client for StateZero - connect directly to your backend models with zero boilerplate",