@eleven-am/pondsocket 0.1.191 → 0.1.192

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.
Files changed (2) hide show
  1. package/abstracts/redisClient.js +325 -151
  2. package/package.json +20 -16
@@ -33,7 +33,7 @@ var __rest = (this && this.__rest) || function (s, e) {
33
33
  var __importDefault = (this && this.__importDefault) || function (mod) {
34
34
  return (mod && mod.__esModule) ? mod : { "default": mod };
35
35
  };
36
- var _RedisClient_instances, _RedisClient_heartbeatInterval, _RedisClient_cleanupInterval, _RedisClient_instanceTtl, _RedisClient_redisClient, _RedisClient_pubClient, _RedisClient_subClient, _RedisClient_instanceId, _RedisClient_assignsPublisher, _RedisClient_presencePublisher, _RedisClient_userLeavesPublisher, _RedisClient_channelMessagePublisher, _RedisClient_stateSyncPublisher, _RedisClient_heartbeatTimer, _RedisClient_cleanupTimer, _RedisClient_presence_changes_channel_get, _RedisClient_assigns_changes_channel_get, _RedisClient_channel_messages_channel_get, _RedisClient_user_leaves_channel_get, _RedisClient_handleErrors, _RedisClient_getPresenceCacheChannel, _RedisClient_getAssignsCacheChannel, _RedisClient_publishPresenceChange, _RedisClient_publishAssignsChange, _RedisClient_publishChannelMessage, _RedisClient_publishUserLeave, _RedisClient_registerInstance, _RedisClient_unregisterInstance, _RedisClient_unsubscribeFromChannels, _RedisClient_subscribeToChannels, _RedisClient_startHeartbeat, _RedisClient_startPeriodicCleanup, _RedisClient_performConsistencyCheck, _RedisClient_handleRedisMessage, _RedisClient_publishCacheMessage, _RedisClient_emitStateSyncEvent, _RedisClient_subscribeToCacheChanges, _RedisClient_subscribeToChannelMessages, _RedisClient_subscribeToUserLeaves, _RedisClient_subscribeToStateSync, _RedisClient_handleExit, _RedisClient_deleteKeysByPattern, _RedisClient_generateCache;
36
+ var _RedisClient_instances, _RedisClient_heartbeatInterval, _RedisClient_cleanupInterval, _RedisClient_instanceTtl, _RedisClient_redisClient, _RedisClient_pubClient, _RedisClient_subClient, _RedisClient_instanceId, _RedisClient_assignsPublisher, _RedisClient_presencePublisher, _RedisClient_userLeavesPublisher, _RedisClient_channelMessagePublisher, _RedisClient_stateSyncPublisher, _RedisClient_heartbeatTimer, _RedisClient_cleanupTimer, _RedisClient_presence_changes_channel_get, _RedisClient_assigns_changes_channel_get, _RedisClient_channel_messages_channel_get, _RedisClient_user_leaves_channel_get, _RedisClient_handleErrors, _RedisClient_registerInstance, _RedisClient_unregisterInstance, _RedisClient_cleanupDisconnectedClients, _RedisClient_getAllInstanceIds, _RedisClient_startPeriodicCleanup, _RedisClient_startHeartbeat, _RedisClient_performHeartbeat, _RedisClient_unsubscribeFromChannels, _RedisClient_subscribeToChannels, _RedisClient_handleRedisMessage, _RedisClient_getPresenceCacheChannel, _RedisClient_getAssignsCacheChannel, _RedisClient_subscribeToCacheChanges, _RedisClient_subscribeToChannelMessages, _RedisClient_subscribeToUserLeaves, _RedisClient_subscribeToStateSync, _RedisClient_publishPresenceChange, _RedisClient_publishAssignsChange, _RedisClient_publishChannelMessage, _RedisClient_publishUserLeave, _RedisClient_publishCacheMessage, _RedisClient_emitStateSyncEvent, _RedisClient_generateCache, _RedisClient_handleExit;
37
37
  Object.defineProperty(exports, "__esModule", { value: true });
38
38
  exports.RedisClient = void 0;
39
39
  const pondsocket_common_1 = require("@eleven-am/pondsocket-common");
@@ -97,12 +97,10 @@ class RedisClient {
97
97
  }
98
98
  shutdown() {
99
99
  return __awaiter(this, void 0, void 0, function* () {
100
- const batchSize = 1000;
101
100
  clearInterval(__classPrivateFieldGet(this, _RedisClient_heartbeatTimer, "f"));
102
101
  clearInterval(__classPrivateFieldGet(this, _RedisClient_cleanupTimer, "f"));
103
102
  yield __classPrivateFieldGet(this, _RedisClient_instances, "m", _RedisClient_unsubscribeFromChannels).call(this);
104
103
  yield __classPrivateFieldGet(this, _RedisClient_instances, "m", _RedisClient_unregisterInstance).call(this);
105
- yield __classPrivateFieldGet(this, _RedisClient_instances, "m", _RedisClient_deleteKeysByPattern).call(this, `*_cache:${__classPrivateFieldGet(this, _RedisClient_instanceId, "f")}:*`, batchSize);
106
104
  });
107
105
  }
108
106
  }
@@ -119,53 +117,14 @@ _RedisClient_heartbeatInterval = new WeakMap(), _RedisClient_cleanupInterval = n
119
117
  __classPrivateFieldGet(this, _RedisClient_redisClient, "f").on('error', (err) => console.error('Redis client error:', err));
120
118
  __classPrivateFieldGet(this, _RedisClient_pubClient, "f").on('error', (err) => console.error('Redis pub client error:', err));
121
119
  __classPrivateFieldGet(this, _RedisClient_subClient, "f").on('error', (err) => console.error('Redis sub client error:', err));
122
- }, _RedisClient_getPresenceCacheChannel = function _RedisClient_getPresenceCacheChannel(endpointId, channelId) {
123
- return `presence_cache:${__classPrivateFieldGet(this, _RedisClient_instanceId, "f")}:${endpointId}:${channelId}`;
124
- }, _RedisClient_getAssignsCacheChannel = function _RedisClient_getAssignsCacheChannel(endpointId, channelId) {
125
- return `assigns_cache:${__classPrivateFieldGet(this, _RedisClient_instanceId, "f")}:${endpointId}:${channelId}`;
126
- }, _RedisClient_publishPresenceChange = function _RedisClient_publishPresenceChange(endpointId, channelId, userId, state) {
127
- const message = {
128
- userId,
129
- channelId,
130
- endpointId,
131
- state,
132
- };
133
- const key = __classPrivateFieldGet(this, _RedisClient_instances, "a", _RedisClient_presence_changes_channel_get);
134
- const cacheKey = __classPrivateFieldGet(this, _RedisClient_instances, "m", _RedisClient_getPresenceCacheChannel).call(this, message.endpointId, message.channelId);
135
- return __classPrivateFieldGet(this, _RedisClient_instances, "m", _RedisClient_publishCacheMessage).call(this, key, cacheKey, message);
136
- }, _RedisClient_publishAssignsChange = function _RedisClient_publishAssignsChange(endpointId, channelId, userId, state) {
137
- const message = {
138
- userId,
139
- channelId,
140
- endpointId,
141
- state,
142
- };
143
- const key = __classPrivateFieldGet(this, _RedisClient_instances, "a", _RedisClient_assigns_changes_channel_get);
144
- const cacheKey = __classPrivateFieldGet(this, _RedisClient_instances, "m", _RedisClient_getAssignsCacheChannel).call(this, message.endpointId, message.channelId);
145
- return __classPrivateFieldGet(this, _RedisClient_instances, "m", _RedisClient_publishCacheMessage).call(this, key, cacheKey, message);
146
- }, _RedisClient_publishChannelMessage = function _RedisClient_publishChannelMessage(endpointId, channelId, message) {
147
- return __awaiter(this, void 0, void 0, function* () {
148
- const messageData = JSON.stringify({
149
- endpointId,
150
- channelId,
151
- message,
152
- });
153
- yield __classPrivateFieldGet(this, _RedisClient_pubClient, "f").publish(__classPrivateFieldGet(this, _RedisClient_instances, "a", _RedisClient_channel_messages_channel_get), messageData);
154
- });
155
- }, _RedisClient_publishUserLeave = function _RedisClient_publishUserLeave(endpointId, channelId, userId) {
156
- return __awaiter(this, void 0, void 0, function* () {
157
- const message = JSON.stringify({
158
- endpointId,
159
- channelId,
160
- userId,
161
- });
162
- yield __classPrivateFieldGet(this, _RedisClient_pubClient, "f").publish(__classPrivateFieldGet(this, _RedisClient_instances, "a", _RedisClient_user_leaves_channel_get), message);
163
- });
164
120
  }, _RedisClient_registerInstance = function _RedisClient_registerInstance() {
165
121
  return __awaiter(this, void 0, void 0, function* () {
166
122
  const multi = __classPrivateFieldGet(this, _RedisClient_redisClient, "f").multi();
167
- multi.sadd('distributed_instances', __classPrivateFieldGet(this, _RedisClient_instanceId, "f"));
168
- multi.set(`heartbeat:${__classPrivateFieldGet(this, _RedisClient_instanceId, "f")}`, Date.now().toString(), 'EX', __classPrivateFieldGet(this, _RedisClient_instanceTtl, "f"));
123
+ const now = Date.now().toString();
124
+ multi
125
+ .sadd('distributed_instances', __classPrivateFieldGet(this, _RedisClient_instanceId, "f"))
126
+ .set(`heartbeat:${__classPrivateFieldGet(this, _RedisClient_instanceId, "f")}`, now, 'EX', __classPrivateFieldGet(this, _RedisClient_instanceTtl, "f"))
127
+ .hset(`instance_metadata:${__classPrivateFieldGet(this, _RedisClient_instanceId, "f")}`, 'last_seen', now);
169
128
  try {
170
129
  yield multi.exec();
171
130
  }
@@ -174,73 +133,233 @@ _RedisClient_heartbeatInterval = new WeakMap(), _RedisClient_cleanupInterval = n
174
133
  }
175
134
  });
176
135
  }, _RedisClient_unregisterInstance = function _RedisClient_unregisterInstance() {
177
- return __awaiter(this, void 0, void 0, function* () {
178
- const multi = __classPrivateFieldGet(this, _RedisClient_redisClient, "f").multi();
179
- multi.srem('distributed_instances', __classPrivateFieldGet(this, _RedisClient_instanceId, "f"));
180
- multi.del(`heartbeat:${__classPrivateFieldGet(this, _RedisClient_instanceId, "f")}`);
136
+ return __awaiter(this, arguments, void 0, function* (instanceId = __classPrivateFieldGet(this, _RedisClient_instanceId, "f")) {
137
+ const script = `
138
+ -- Remove instance registration
139
+ redis.call('SREM', 'distributed_instances', ARGV[1])
140
+ redis.call('DEL', 'heartbeat:' .. ARGV[1])
141
+ redis.call('DEL', 'instance_metadata:' .. ARGV[1])
142
+
143
+ -- Delete all cache keys in one SCAN operation
144
+ local cursor = '0'
145
+ repeat
146
+ local result = redis.call('SCAN', cursor, 'MATCH', '{presence_cache,assigns_cache}:' .. ARGV[1] .. ':*', 'COUNT', 100)
147
+ cursor = result[1]
148
+ local keys = result[2]
149
+ if #keys > 0 then
150
+ redis.call('DEL', unpack(keys))
151
+ end
152
+ until cursor == '0'
153
+
154
+ return 1
155
+ `;
181
156
  try {
182
- yield multi.exec();
157
+ yield __classPrivateFieldGet(this, _RedisClient_redisClient, "f").eval(script, 0, instanceId);
183
158
  }
184
159
  catch (_a) {
185
160
  // no-op as we're shutting down
186
161
  }
187
162
  });
188
- }, _RedisClient_unsubscribeFromChannels = function _RedisClient_unsubscribeFromChannels() {
163
+ }, _RedisClient_cleanupDisconnectedClients = function _RedisClient_cleanupDisconnectedClients() {
189
164
  return __awaiter(this, void 0, void 0, function* () {
190
- yield __classPrivateFieldGet(this, _RedisClient_subClient, "f").unsubscribe(__classPrivateFieldGet(this, _RedisClient_instances, "a", _RedisClient_presence_changes_channel_get), __classPrivateFieldGet(this, _RedisClient_instances, "a", _RedisClient_assigns_changes_channel_get), __classPrivateFieldGet(this, _RedisClient_instances, "a", _RedisClient_channel_messages_channel_get), __classPrivateFieldGet(this, _RedisClient_instances, "a", _RedisClient_user_leaves_channel_get));
191
- });
192
- }, _RedisClient_subscribeToChannels = function _RedisClient_subscribeToChannels() {
193
- return new Promise((resolve, reject) => {
194
- __classPrivateFieldGet(this, _RedisClient_subClient, "f").subscribe(__classPrivateFieldGet(this, _RedisClient_instances, "a", _RedisClient_presence_changes_channel_get), __classPrivateFieldGet(this, _RedisClient_instances, "a", _RedisClient_assigns_changes_channel_get), __classPrivateFieldGet(this, _RedisClient_instances, "a", _RedisClient_channel_messages_channel_get), __classPrivateFieldGet(this, _RedisClient_instances, "a", _RedisClient_user_leaves_channel_get), (err) => {
195
- if (err) {
196
- reject(err);
197
- }
198
- else {
199
- resolve();
165
+ const script = `
166
+ -- Helper function to check if instance is alive
167
+ local function is_instance_alive(instance_id)
168
+ local heartbeat = redis.call('GET', 'heartbeat:' .. instance_id)
169
+ local in_set = redis.call('SISMEMBER', 'distributed_instances', instance_id)
170
+ return heartbeat and in_set == 1
171
+ end
172
+
173
+ -- Get array of instance IDs passed as argument
174
+ local instance_ids = cjson.decode(ARGV[1])
175
+
176
+ -- Track affected channels and keys to delete
177
+ local affected_channels = {}
178
+ local keys_to_delete = {}
179
+ local batch_size = 100
180
+
181
+ -- Process each instance
182
+ for _, instance_id in ipairs(instance_ids) do
183
+ -- If instance is not alive, clean it up
184
+ if not is_instance_alive(instance_id) then
185
+ -- Queue instance metadata keys for deletion
186
+ table.insert(keys_to_delete, 'heartbeat:' .. instance_id)
187
+ table.insert(keys_to_delete, 'instance_metadata:' .. instance_id)
188
+
189
+ -- Remove from distributed_instances
190
+ redis.call('SREM', 'distributed_instances', instance_id)
191
+
192
+ -- Find and process all cache keys for this instance
193
+ local cache_cursor = '0'
194
+ repeat
195
+ local result = redis.call('SCAN', cache_cursor, 'MATCH', '*_cache:' .. instance_id .. ':*', 'COUNT', batch_size)
196
+ cache_cursor = result[1]
197
+
198
+ for _, key in ipairs(result[2]) do
199
+ -- Extract channel info before deletion
200
+ local _, _, endpoint_id, channel_id = string.match(key, '_cache:[^:]+:([^:]+):([^:]+)')
201
+ if endpoint_id and channel_id then
202
+ affected_channels[endpoint_id .. ":" .. channel_id] = true
203
+ end
204
+
205
+ -- Add to deletion batch
206
+ table.insert(keys_to_delete, key)
207
+
208
+ -- If batch is full, process it
209
+ if #keys_to_delete >= batch_size then
210
+ redis.call('DEL', unpack(keys_to_delete))
211
+ keys_to_delete = {}
212
+ end
213
+ end
214
+ until cache_cursor == '0'
215
+ end
216
+ end
217
+
218
+ -- Delete any remaining keys
219
+ if #keys_to_delete > 0 then
220
+ redis.call('DEL', unpack(keys_to_delete))
221
+ end
222
+
223
+ -- Convert affected_channels to array
224
+ local channels = {}
225
+ for pair in pairs(affected_channels) do
226
+ table.insert(channels, pair)
227
+ end
228
+
229
+ return channels
230
+ `;
231
+ try {
232
+ // Get all instance IDs first
233
+ const instanceIds = yield __classPrivateFieldGet(this, _RedisClient_instances, "m", _RedisClient_getAllInstanceIds).call(this);
234
+ // Run cleanup script with the instance IDs
235
+ const affectedChannels = yield __classPrivateFieldGet(this, _RedisClient_redisClient, "f").eval(script, 0, JSON.stringify(instanceIds));
236
+ // Process affected channels
237
+ if (affectedChannels.length > 0) {
238
+ const promises = affectedChannels.map((pair) => {
239
+ const [endpointId, channelId] = pair.split(':');
240
+ return __classPrivateFieldGet(this, _RedisClient_instances, "m", _RedisClient_emitStateSyncEvent).call(this, endpointId, channelId);
241
+ });
242
+ yield Promise.all(promises);
200
243
  }
201
- });
202
- __classPrivateFieldGet(this, _RedisClient_subClient, "f").on('message', __classPrivateFieldGet(this, _RedisClient_instances, "m", _RedisClient_handleRedisMessage).bind(this));
244
+ }
245
+ catch (error) {
246
+ console.error('Error cleaning up disconnected clients:', error);
247
+ }
203
248
  });
204
- }, _RedisClient_startHeartbeat = function _RedisClient_startHeartbeat() {
205
- __classPrivateFieldSet(this, _RedisClient_heartbeatTimer, setInterval(() => __awaiter(this, void 0, void 0, function* () {
249
+ }, _RedisClient_getAllInstanceIds = function _RedisClient_getAllInstanceIds() {
250
+ return __awaiter(this, void 0, void 0, function* () {
251
+ const script = `
252
+ local instance_ids = {}
253
+ local seen = {} -- For deduplication
254
+
255
+ -- 1. Get instances from distributed_instances set
256
+ local registered = redis.call('SMEMBERS', 'distributed_instances')
257
+ for _, id in ipairs(registered) do
258
+ if not seen[id] then
259
+ seen[id] = true
260
+ table.insert(instance_ids, id)
261
+ end
262
+ end
263
+
264
+ -- 2. Get instances from heartbeat keys
265
+ local cursor = '0'
266
+ repeat
267
+ local result = redis.call('SCAN', cursor, 'MATCH', 'heartbeat:*', 'COUNT', 100)
268
+ cursor = result[1]
269
+ local keys = result[2]
270
+
271
+ for _, key in ipairs(keys) do
272
+ local id = string.match(key, 'heartbeat:([^:]+)')
273
+ if id and not seen[id] then
274
+ seen[id] = true
275
+ table.insert(instance_ids, id)
276
+ end
277
+ end
278
+ until cursor == '0'
279
+
280
+ -- 3. Get instances from metadata keys
281
+ cursor = '0'
282
+ repeat
283
+ local result = redis.call('SCAN', cursor, 'MATCH', 'instance_metadata:*', 'COUNT', 100)
284
+ cursor = result[1]
285
+ local keys = result[2]
286
+
287
+ for _, key in ipairs(keys) do
288
+ local id = string.match(key, 'instance_metadata:([^:]+)')
289
+ if id and not seen[id] then
290
+ seen[id] = true
291
+ table.insert(instance_ids, id)
292
+ end
293
+ end
294
+ until cursor == '0'
295
+
296
+ -- 4. Get instances from cache keys
297
+ cursor = '0'
298
+ repeat
299
+ local result = redis.call('SCAN', cursor, 'MATCH', '*_cache:*', 'COUNT', 100)
300
+ cursor = result[1]
301
+ local keys = result[2]
302
+
303
+ for _, key in ipairs(keys) do
304
+ local id = string.match(key, '_cache:([^:]+)')
305
+ if id and not seen[id] then
306
+ seen[id] = true
307
+ table.insert(instance_ids, id)
308
+ end
309
+ end
310
+ until cursor == '0'
311
+
312
+ return instance_ids
313
+ `;
314
+ let response = [];
206
315
  try {
207
- yield __classPrivateFieldGet(this, _RedisClient_redisClient, "f").set(`heartbeat:${__classPrivateFieldGet(this, _RedisClient_instanceId, "f")}`, Date.now().toString(), 'EX', __classPrivateFieldGet(this, _RedisClient_instanceTtl, "f"));
316
+ response = (yield __classPrivateFieldGet(this, _RedisClient_redisClient, "f").eval(script, 0));
208
317
  }
209
318
  catch (error) {
210
- throw new redisError_1.RedisError('Error setting heartbeat');
319
+ console.error('Error getting all instance IDs:', error);
211
320
  }
212
- }), __classPrivateFieldGet(this, _RedisClient_heartbeatInterval, "f")), "f");
321
+ return response;
322
+ });
213
323
  }, _RedisClient_startPeriodicCleanup = function _RedisClient_startPeriodicCleanup() {
214
324
  __classPrivateFieldSet(this, _RedisClient_cleanupTimer, setInterval(() => __awaiter(this, void 0, void 0, function* () {
215
325
  try {
216
- yield __classPrivateFieldGet(this, _RedisClient_instances, "m", _RedisClient_performConsistencyCheck).call(this);
326
+ yield __classPrivateFieldGet(this, _RedisClient_instances, "m", _RedisClient_cleanupDisconnectedClients).call(this);
217
327
  }
218
328
  catch (error) {
219
- throw new redisError_1.RedisError('Error performing consistency check');
329
+ console.error('Error in periodic cleanup:', error);
220
330
  }
221
331
  }), __classPrivateFieldGet(this, _RedisClient_cleanupInterval, "f")), "f");
222
- }, _RedisClient_performConsistencyCheck = function _RedisClient_performConsistencyCheck() {
223
- return __awaiter(this, void 0, void 0, function* () {
224
- const activeInstances = yield __classPrivateFieldGet(this, _RedisClient_redisClient, "f").smembers('distributed_instances');
225
- const allKeys = yield __classPrivateFieldGet(this, _RedisClient_redisClient, "f").keys('*_cache:*');
226
- const activeSet = new Set(activeInstances);
227
- const inactiveKeys = [];
228
- const uniquePairs = new Set();
229
- for (const key of allKeys) {
230
- const [, instanceId, endpointId, channelId] = key.split(':');
231
- if (!activeSet.has(instanceId)) {
232
- inactiveKeys.push(key);
233
- uniquePairs.add(`${endpointId}:${channelId}`);
234
- }
332
+ }, _RedisClient_startHeartbeat = function _RedisClient_startHeartbeat() {
333
+ __classPrivateFieldSet(this, _RedisClient_heartbeatTimer, setInterval(() => __awaiter(this, void 0, void 0, function* () {
334
+ try {
335
+ yield __classPrivateFieldGet(this, _RedisClient_instances, "m", _RedisClient_performHeartbeat).call(this);
235
336
  }
236
- if (inactiveKeys.length > 0) {
237
- yield __classPrivateFieldGet(this, _RedisClient_redisClient, "f").del(...inactiveKeys);
337
+ catch (error) {
338
+ console.error('Error performing heartbeat:', error);
238
339
  }
239
- const promises = Array.from(uniquePairs).map((pair) => {
240
- const [endpointId, channelId] = pair.split(':');
241
- return __classPrivateFieldGet(this, _RedisClient_instances, "m", _RedisClient_emitStateSyncEvent).call(this, endpointId, channelId);
340
+ }), __classPrivateFieldGet(this, _RedisClient_heartbeatInterval, "f")), "f");
341
+ }, _RedisClient_performHeartbeat = function _RedisClient_performHeartbeat() {
342
+ const now = Date.now().toString();
343
+ const multi = __classPrivateFieldGet(this, _RedisClient_redisClient, "f").multi();
344
+ multi
345
+ .set(`heartbeat:${__classPrivateFieldGet(this, _RedisClient_instanceId, "f")}`, now, 'EX', __classPrivateFieldGet(this, _RedisClient_instanceTtl, "f"))
346
+ .hset(`instance_metadata:${__classPrivateFieldGet(this, _RedisClient_instanceId, "f")}`, 'last_seen', now);
347
+ return multi.exec();
348
+ }, _RedisClient_unsubscribeFromChannels = function _RedisClient_unsubscribeFromChannels() {
349
+ return __awaiter(this, void 0, void 0, function* () {
350
+ yield __classPrivateFieldGet(this, _RedisClient_subClient, "f").unsubscribe(__classPrivateFieldGet(this, _RedisClient_instances, "a", _RedisClient_presence_changes_channel_get), __classPrivateFieldGet(this, _RedisClient_instances, "a", _RedisClient_assigns_changes_channel_get), __classPrivateFieldGet(this, _RedisClient_instances, "a", _RedisClient_channel_messages_channel_get), __classPrivateFieldGet(this, _RedisClient_instances, "a", _RedisClient_user_leaves_channel_get));
351
+ });
352
+ }, _RedisClient_subscribeToChannels = function _RedisClient_subscribeToChannels() {
353
+ return new Promise((resolve, reject) => {
354
+ __classPrivateFieldGet(this, _RedisClient_subClient, "f").subscribe(__classPrivateFieldGet(this, _RedisClient_instances, "a", _RedisClient_presence_changes_channel_get), __classPrivateFieldGet(this, _RedisClient_instances, "a", _RedisClient_assigns_changes_channel_get), __classPrivateFieldGet(this, _RedisClient_instances, "a", _RedisClient_channel_messages_channel_get), __classPrivateFieldGet(this, _RedisClient_instances, "a", _RedisClient_user_leaves_channel_get), (err) => {
355
+ if (err) {
356
+ reject(err);
357
+ }
358
+ else {
359
+ resolve();
360
+ }
242
361
  });
243
- yield Promise.all(promises);
362
+ __classPrivateFieldGet(this, _RedisClient_subClient, "f").on('message', __classPrivateFieldGet(this, _RedisClient_instances, "m", _RedisClient_handleRedisMessage).bind(this));
244
363
  });
245
364
  }, _RedisClient_handleRedisMessage = function _RedisClient_handleRedisMessage(channel, message) {
246
365
  const data = JSON.parse(message);
@@ -260,6 +379,81 @@ _RedisClient_heartbeatInterval = new WeakMap(), _RedisClient_cleanupInterval = n
260
379
  default:
261
380
  throw new Error(`Unknown channel: ${channel}`);
262
381
  }
382
+ }, _RedisClient_getPresenceCacheChannel = function _RedisClient_getPresenceCacheChannel(endpointId, channelId) {
383
+ return `presence_cache:${__classPrivateFieldGet(this, _RedisClient_instanceId, "f")}:${endpointId}:${channelId}`;
384
+ }, _RedisClient_getAssignsCacheChannel = function _RedisClient_getAssignsCacheChannel(endpointId, channelId) {
385
+ return `assigns_cache:${__classPrivateFieldGet(this, _RedisClient_instanceId, "f")}:${endpointId}:${channelId}`;
386
+ }, _RedisClient_subscribeToCacheChanges = function _RedisClient_subscribeToCacheChanges(endpoint, channel, presence, callback) {
387
+ const subject = presence ? __classPrivateFieldGet(this, _RedisClient_presencePublisher, "f") : __classPrivateFieldGet(this, _RedisClient_assignsPublisher, "f");
388
+ return subject.subscribe((_a) => {
389
+ var { endpointId, channelId } = _a, data = __rest(_a, ["endpointId", "channelId"]);
390
+ if (endpointId === endpoint && channelId === channel) {
391
+ return callback(data);
392
+ }
393
+ });
394
+ }, _RedisClient_subscribeToChannelMessages = function _RedisClient_subscribeToChannelMessages(endpoint, channel, callback) {
395
+ return __classPrivateFieldGet(this, _RedisClient_channelMessagePublisher, "f").subscribe(({ endpointId, channelId, message }) => {
396
+ if (endpointId === endpoint && channelId === channel) {
397
+ return callback(message);
398
+ }
399
+ });
400
+ }, _RedisClient_subscribeToUserLeaves = function _RedisClient_subscribeToUserLeaves(endpoint, channel, callback) {
401
+ return __classPrivateFieldGet(this, _RedisClient_userLeavesPublisher, "f").subscribe(({ endpointId, channelId, userId }) => {
402
+ if (endpointId === endpoint && channelId === channel) {
403
+ return callback(userId);
404
+ }
405
+ });
406
+ }, _RedisClient_subscribeToStateSync = function _RedisClient_subscribeToStateSync(endpoint, channel, callback) {
407
+ const interval = setInterval(() => __classPrivateFieldGet(this, _RedisClient_instances, "m", _RedisClient_emitStateSyncEvent).call(this, endpoint, channel), __classPrivateFieldGet(this, _RedisClient_heartbeatInterval, "f") * 10);
408
+ const subscription = __classPrivateFieldGet(this, _RedisClient_stateSyncPublisher, "f").subscribe((_a) => {
409
+ var { endpointId, channelId } = _a, data = __rest(_a, ["endpointId", "channelId"]);
410
+ if (endpointId === endpoint && channelId === channel) {
411
+ return callback(data);
412
+ }
413
+ });
414
+ void __classPrivateFieldGet(this, _RedisClient_instances, "m", _RedisClient_emitStateSyncEvent).call(this, endpoint, channel, true);
415
+ return () => {
416
+ clearInterval(interval);
417
+ subscription();
418
+ };
419
+ }, _RedisClient_publishPresenceChange = function _RedisClient_publishPresenceChange(endpointId, channelId, userId, state) {
420
+ const message = {
421
+ userId,
422
+ channelId,
423
+ endpointId,
424
+ state,
425
+ };
426
+ const key = __classPrivateFieldGet(this, _RedisClient_instances, "a", _RedisClient_presence_changes_channel_get);
427
+ const cacheKey = __classPrivateFieldGet(this, _RedisClient_instances, "m", _RedisClient_getPresenceCacheChannel).call(this, message.endpointId, message.channelId);
428
+ return __classPrivateFieldGet(this, _RedisClient_instances, "m", _RedisClient_publishCacheMessage).call(this, key, cacheKey, message);
429
+ }, _RedisClient_publishAssignsChange = function _RedisClient_publishAssignsChange(endpointId, channelId, userId, state) {
430
+ const message = {
431
+ userId,
432
+ channelId,
433
+ endpointId,
434
+ state,
435
+ };
436
+ const key = __classPrivateFieldGet(this, _RedisClient_instances, "a", _RedisClient_assigns_changes_channel_get);
437
+ const cacheKey = __classPrivateFieldGet(this, _RedisClient_instances, "m", _RedisClient_getAssignsCacheChannel).call(this, message.endpointId, message.channelId);
438
+ return __classPrivateFieldGet(this, _RedisClient_instances, "m", _RedisClient_publishCacheMessage).call(this, key, cacheKey, message);
439
+ }, _RedisClient_publishChannelMessage = function _RedisClient_publishChannelMessage(endpointId, channelId, message) {
440
+ return __awaiter(this, void 0, void 0, function* () {
441
+ const messageData = JSON.stringify({
442
+ endpointId,
443
+ channelId,
444
+ message,
445
+ });
446
+ yield __classPrivateFieldGet(this, _RedisClient_pubClient, "f").publish(__classPrivateFieldGet(this, _RedisClient_instances, "a", _RedisClient_channel_messages_channel_get), messageData);
447
+ });
448
+ }, _RedisClient_publishUserLeave = function _RedisClient_publishUserLeave(endpointId, channelId, userId) {
449
+ return __awaiter(this, void 0, void 0, function* () {
450
+ const message = JSON.stringify({
451
+ endpointId,
452
+ channelId,
453
+ userId,
454
+ });
455
+ yield __classPrivateFieldGet(this, _RedisClient_pubClient, "f").publish(__classPrivateFieldGet(this, _RedisClient_instances, "a", _RedisClient_user_leaves_channel_get), message);
456
+ });
263
457
  }, _RedisClient_publishCacheMessage = function _RedisClient_publishCacheMessage(key, cacheKey, message) {
264
458
  return __awaiter(this, void 0, void 0, function* () {
265
459
  const script = `
@@ -269,6 +463,8 @@ _RedisClient_heartbeatInterval = new WeakMap(), _RedisClient_cleanupInterval = n
269
463
 
270
464
  if state ~= '' then
271
465
  redis.call('HSET', KEYS[1], userId, state)
466
+ -- Set TTL on cache key
467
+ redis.call('EXPIRE', KEYS[1], ${__classPrivateFieldGet(this, _RedisClient_instanceTtl, "f")})
272
468
  else
273
469
  redis.call('HDEL', KEYS[1], userId)
274
470
  end
@@ -284,23 +480,45 @@ _RedisClient_heartbeatInterval = new WeakMap(), _RedisClient_cleanupInterval = n
284
480
  }, _RedisClient_emitStateSyncEvent = function _RedisClient_emitStateSyncEvent(endpointId_1, channelId_1) {
285
481
  return __awaiter(this, arguments, void 0, function* (endpointId, channelId, initialFetch = false) {
286
482
  const script = `
483
+ -- Helper function to check if an instance is truly alive
484
+ -- Returns true only if:
485
+ -- 1. Instance has a valid heartbeat AND
486
+ -- 2. Instance is in the distributed_instances set
487
+ local function is_instance_alive(instance_id)
488
+ -- Both checks in one atomic operation
489
+ local exists = redis.call('EXISTS', 'heartbeat:' .. instance_id)
490
+ if exists == 0 then
491
+ return false
492
+ end
493
+
494
+ local is_registered = redis.call('SISMEMBER', 'distributed_instances', instance_id)
495
+ return is_registered == 1
496
+ end
497
+
498
+ -- Get all registered instances
287
499
  local active_instances = redis.call('SMEMBERS', 'distributed_instances')
288
500
  local presence_data = {}
289
501
  local assigns_data = {}
290
502
 
291
503
  for _, instance in ipairs(active_instances) do
292
- local presence_key = 'presence_cache:' .. instance .. ':' .. ARGV[1] .. ':' .. ARGV[2]
293
- local assigns_key = 'assigns_cache:' .. instance .. ':' .. ARGV[1] .. ':' .. ARGV[2]
294
-
295
- local presence = redis.call('HGETALL', presence_key)
296
- local assigns = redis.call('HGETALL', assigns_key)
297
-
298
- for i = 1, #presence, 2 do
299
- presence_data[presence[i]] = presence[i+1]
300
- end
301
-
302
- for i = 1, #assigns, 2 do
303
- assigns_data[assigns[i]] = assigns[i+1]
504
+ -- Only process data from instances that are truly alive
505
+ if is_instance_alive(instance) then
506
+ local presence_key = 'presence_cache:' .. instance .. ':' .. ARGV[1] .. ':' .. ARGV[2]
507
+ local assigns_key = 'assigns_cache:' .. instance .. ':' .. ARGV[1] .. ':' .. ARGV[2]
508
+
509
+ -- Fetch data atomically for this instance
510
+ local presence = redis.call('HGETALL', presence_key)
511
+ local assigns = redis.call('HGETALL', assigns_key)
512
+
513
+ -- Process presence data
514
+ for i = 1, #presence, 2 do
515
+ presence_data[presence[i]] = presence[i+1]
516
+ end
517
+
518
+ -- Process assigns data
519
+ for i = 1, #assigns, 2 do
520
+ assigns_data[assigns[i]] = assigns[i+1]
521
+ end
304
522
  end
305
523
  end
306
524
 
@@ -316,39 +534,10 @@ _RedisClient_heartbeatInterval = new WeakMap(), _RedisClient_cleanupInterval = n
316
534
  };
317
535
  __classPrivateFieldGet(this, _RedisClient_stateSyncPublisher, "f").publish(event);
318
536
  });
319
- }, _RedisClient_subscribeToCacheChanges = function _RedisClient_subscribeToCacheChanges(endpoint, channel, presence, callback) {
320
- const subject = presence ? __classPrivateFieldGet(this, _RedisClient_presencePublisher, "f") : __classPrivateFieldGet(this, _RedisClient_assignsPublisher, "f");
321
- return subject.subscribe((_a) => {
322
- var { endpointId, channelId } = _a, data = __rest(_a, ["endpointId", "channelId"]);
323
- if (endpointId === endpoint && channelId === channel) {
324
- return callback(data);
325
- }
326
- });
327
- }, _RedisClient_subscribeToChannelMessages = function _RedisClient_subscribeToChannelMessages(endpoint, channel, callback) {
328
- return __classPrivateFieldGet(this, _RedisClient_channelMessagePublisher, "f").subscribe(({ endpointId, channelId, message }) => {
329
- if (endpointId === endpoint && channelId === channel) {
330
- return callback(message);
331
- }
332
- });
333
- }, _RedisClient_subscribeToUserLeaves = function _RedisClient_subscribeToUserLeaves(endpoint, channel, callback) {
334
- return __classPrivateFieldGet(this, _RedisClient_userLeavesPublisher, "f").subscribe(({ endpointId, channelId, userId }) => {
335
- if (endpointId === endpoint && channelId === channel) {
336
- return callback(userId);
337
- }
338
- });
339
- }, _RedisClient_subscribeToStateSync = function _RedisClient_subscribeToStateSync(endpoint, channel, callback) {
340
- const interval = setInterval(() => __classPrivateFieldGet(this, _RedisClient_instances, "m", _RedisClient_emitStateSyncEvent).call(this, endpoint, channel), __classPrivateFieldGet(this, _RedisClient_heartbeatInterval, "f") * 10);
341
- const subscription = __classPrivateFieldGet(this, _RedisClient_stateSyncPublisher, "f").subscribe((_a) => {
342
- var { endpointId, channelId } = _a, data = __rest(_a, ["endpointId", "channelId"]);
343
- if (endpointId === endpoint && channelId === channel) {
344
- return callback(data);
345
- }
346
- });
347
- void __classPrivateFieldGet(this, _RedisClient_instances, "m", _RedisClient_emitStateSyncEvent).call(this, endpoint, channel, true);
348
- return () => {
349
- clearInterval(interval);
350
- subscription();
351
- };
537
+ }, _RedisClient_generateCache = function _RedisClient_generateCache(data) {
538
+ const first = Object.entries(JSON.parse(data));
539
+ const second = first.map(([key, value]) => [key, JSON.parse(String(value))]);
540
+ return new Map(second);
352
541
  }, _RedisClient_handleExit = function _RedisClient_handleExit() {
353
542
  return __awaiter(this, void 0, void 0, function* () {
354
543
  try {
@@ -359,19 +548,4 @@ _RedisClient_heartbeatInterval = new WeakMap(), _RedisClient_cleanupInterval = n
359
548
  process.exit(1);
360
549
  }
361
550
  });
362
- }, _RedisClient_deleteKeysByPattern = function _RedisClient_deleteKeysByPattern(pattern, batchSize) {
363
- return __awaiter(this, void 0, void 0, function* () {
364
- let cursor = '0';
365
- do {
366
- const [newCursor, keys] = yield __classPrivateFieldGet(this, _RedisClient_redisClient, "f").scan(cursor, 'MATCH', pattern, 'COUNT', batchSize);
367
- cursor = newCursor;
368
- if (keys.length > 0) {
369
- yield __classPrivateFieldGet(this, _RedisClient_redisClient, "f").del(...keys);
370
- }
371
- } while (cursor !== '0');
372
- });
373
- }, _RedisClient_generateCache = function _RedisClient_generateCache(data) {
374
- const first = Object.entries(JSON.parse(data));
375
- const second = first.map(([key, value]) => [key, JSON.parse(String(value))]);
376
- return new Map(second);
377
551
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@eleven-am/pondsocket",
3
- "version": "0.1.191",
3
+ "version": "0.1.192",
4
4
  "description": "PondSocket is a fast simple socket server",
5
5
  "keywords": [
6
6
  "socket",
@@ -35,32 +35,36 @@
35
35
  },
36
36
  "dependencies": {
37
37
  "@eleven-am/pondsocket-common": "^0.0.26",
38
- "ioredis": "^5.4.2",
39
- "ws": "^8.18.0"
38
+ "ioredis": "^5.5.0",
39
+ "ws": "^8.18.1"
40
40
  },
41
41
  "devDependencies": {
42
- "@stylistic/eslint-plugin-ts": "^2.10.1",
42
+ "@eslint/compat": "^1.2.7",
43
+ "@eslint/eslintrc": "^3.3.0",
44
+ "@eslint/js": "^9.21.0",
45
+ "@stylistic/eslint-plugin-ts": "^4.0.1",
43
46
  "@types/jest": "^29.5.14",
44
- "@types/node": "^22.8.7",
45
- "@types/ws": "^8.5.13",
46
- "@typescript-eslint/eslint-plugin": "^8.11.0",
47
- "@typescript-eslint/parser": "^8.11.0",
48
- "eslint": "^8.57.1",
49
- "eslint-config-prettier": "^9.1.0",
47
+ "@types/node": "^22.13.5",
48
+ "@types/ws": "^8.5.14",
49
+ "@typescript-eslint/eslint-plugin": "^8.24.1",
50
+ "@typescript-eslint/parser": "^8.24.1",
51
+ "eslint": "^9.21.0",
52
+ "eslint-config-prettier": "^10.0.1",
50
53
  "eslint-import-resolver-node": "^0.3.9",
51
- "eslint-plugin-file-progress": "^1.5.0",
54
+ "eslint-plugin-file-progress": "^3.0.1",
52
55
  "eslint-plugin-import": "^2.31.0",
53
- "eslint-plugin-prettier": "^5.2.1",
56
+ "eslint-plugin-prettier": "^5.2.3",
57
+ "globals": "^16.0.0",
54
58
  "jest": "^29.7.0",
55
- "nodemon": "^3.1.7",
56
- "prettier": "^3.3.3",
59
+ "nodemon": "^3.1.9",
60
+ "prettier": "^3.5.2",
57
61
  "source-map-support": "^0.5.21",
58
62
  "supertest": "^7.0.0",
59
63
  "ts-jest": "^29.2.5",
60
- "ts-loader": "^9.5.1",
64
+ "ts-loader": "^9.5.2",
61
65
  "ts-node": "^10.9.2",
62
66
  "tsconfig-paths": "^4.2.0",
63
- "typescript": "^5.6.3"
67
+ "typescript": "^5.7.3"
64
68
  },
65
69
  "jest": {
66
70
  "moduleFileExtensions": [