@@ -10,9 +10,11 @@ const KEYS = {
1010 workflowMeta : ( wfId : string ) => `workflow:${ wfId } :meta` ,
1111 socketWorkflow : ( socketId : string ) => `socket:${ socketId } :workflow` ,
1212 socketSession : ( socketId : string ) => `socket:${ socketId } :session` ,
13+ socketPresenceWorkflow : ( socketId : string ) => `socket:${ socketId } :presence-workflow` ,
1314} as const
1415
1516const SOCKET_KEY_TTL = 3600
17+ const SOCKET_PRESENCE_WORKFLOW_KEY_TTL = 24 * 60 * 60
1618
1719/**
1820 * Lua script for atomic user removal from room.
@@ -22,11 +24,21 @@ const SOCKET_KEY_TTL = 3600
2224const REMOVE_USER_SCRIPT = `
2325local socketWorkflowKey = KEYS[1]
2426local socketSessionKey = KEYS[2]
27+ local socketPresenceWorkflowKey = KEYS[3]
2528local workflowUsersPrefix = ARGV[1]
2629local workflowMetaPrefix = ARGV[2]
2730local socketId = ARGV[3]
31+ local workflowIdHint = ARGV[4]
2832
2933local workflowId = redis.call('GET', socketWorkflowKey)
34+ if not workflowId then
35+ workflowId = redis.call('GET', socketPresenceWorkflowKey)
36+ end
37+
38+ if not workflowId and workflowIdHint ~= '' then
39+ workflowId = workflowIdHint
40+ end
41+
3042if not workflowId then
3143 return nil
3244end
@@ -35,7 +47,7 @@ local workflowUsersKey = workflowUsersPrefix .. workflowId .. ':users'
3547local workflowMetaKey = workflowMetaPrefix .. workflowId .. ':meta'
3648
3749redis.call('HDEL', workflowUsersKey, socketId)
38- redis.call('DEL', socketWorkflowKey, socketSessionKey)
50+ redis.call('DEL', socketWorkflowKey, socketSessionKey, socketPresenceWorkflowKey )
3951
4052local remaining = redis.call('HLEN', workflowUsersKey)
4153if remaining == 0 then
@@ -54,11 +66,13 @@ const UPDATE_ACTIVITY_SCRIPT = `
5466local workflowUsersKey = KEYS[1]
5567local socketWorkflowKey = KEYS[2]
5668local socketSessionKey = KEYS[3]
69+ local socketPresenceWorkflowKey = KEYS[4]
5770local socketId = ARGV[1]
5871local cursorJson = ARGV[2]
5972local selectionJson = ARGV[3]
6073local lastActivity = ARGV[4]
6174local ttl = tonumber(ARGV[5])
75+ local presenceWorkflowTtl = tonumber(ARGV[6])
6276
6377local existingJson = redis.call('HGET', workflowUsersKey, socketId)
6478if not existingJson then
@@ -78,6 +92,7 @@ existing.lastActivity = tonumber(lastActivity)
7892redis.call('HSET', workflowUsersKey, socketId, cjson.encode(existing))
7993redis.call('EXPIRE', socketWorkflowKey, ttl)
8094redis.call('EXPIRE', socketSessionKey, ttl)
95+ redis.call('EXPIRE', socketPresenceWorkflowKey, presenceWorkflowTtl)
8196return 1
8297`
8398
@@ -164,6 +179,8 @@ export class RedisRoomManager implements IRoomManager {
164179 pipeline . hSet ( KEYS . workflowMeta ( workflowId ) , 'lastModified' , Date . now ( ) . toString ( ) )
165180 pipeline . set ( KEYS . socketWorkflow ( socketId ) , workflowId )
166181 pipeline . expire ( KEYS . socketWorkflow ( socketId ) , SOCKET_KEY_TTL )
182+ pipeline . set ( KEYS . socketPresenceWorkflow ( socketId ) , workflowId )
183+ pipeline . expire ( KEYS . socketPresenceWorkflow ( socketId ) , SOCKET_PRESENCE_WORKFLOW_KEY_TTL )
167184 pipeline . hSet ( KEYS . socketSession ( socketId ) , {
168185 userId : presence . userId ,
169186 userName : presence . userName ,
@@ -187,35 +204,50 @@ export class RedisRoomManager implements IRoomManager {
187204 }
188205 }
189206
190- async removeUserFromRoom ( socketId : string , retried = false ) : Promise < string | null > {
207+ async removeUserFromRoom (
208+ socketId : string ,
209+ workflowIdHint ?: string ,
210+ retried = false
211+ ) : Promise < string | null > {
191212 if ( ! this . removeUserScriptSha ) {
192213 logger . error ( 'removeUserFromRoom called before initialize()' )
193214 return null
194215 }
195216
196217 try {
197218 const workflowId = await this . redis . evalSha ( this . removeUserScriptSha , {
198- keys : [ KEYS . socketWorkflow ( socketId ) , KEYS . socketSession ( socketId ) ] ,
199- arguments : [ 'workflow:' , 'workflow:' , socketId ] ,
219+ keys : [
220+ KEYS . socketWorkflow ( socketId ) ,
221+ KEYS . socketSession ( socketId ) ,
222+ KEYS . socketPresenceWorkflow ( socketId ) ,
223+ ] ,
224+ arguments : [ 'workflow:' , 'workflow:' , socketId , workflowIdHint ?? '' ] ,
200225 } )
201226
202- if ( workflowId ) {
227+ if ( typeof workflowId === 'string' && workflowId . length > 0 ) {
203228 logger . debug ( `Removed socket ${ socketId } from workflow ${ workflowId } ` )
229+ return workflowId
204230 }
205- return workflowId as string | null
231+
232+ return null
206233 } catch ( error ) {
207234 if ( ( error as Error ) . message ?. includes ( 'NOSCRIPT' ) && ! retried ) {
208235 logger . warn ( 'Lua script not found, reloading...' )
209236 this . removeUserScriptSha = await this . redis . scriptLoad ( REMOVE_USER_SCRIPT )
210- return this . removeUserFromRoom ( socketId , true )
237+ return this . removeUserFromRoom ( socketId , workflowIdHint , true )
211238 }
212239 logger . error ( `Failed to remove user from room: ${ socketId } ` , error )
213240 return null
214241 }
215242 }
216243
217244 async getWorkflowIdForSocket ( socketId : string ) : Promise < string | null > {
218- return this . redis . get ( KEYS . socketWorkflow ( socketId ) )
245+ const workflowId = await this . redis . get ( KEYS . socketWorkflow ( socketId ) )
246+ if ( workflowId ) {
247+ return workflowId
248+ }
249+
250+ return this . redis . get ( KEYS . socketPresenceWorkflow ( socketId ) )
219251 }
220252
221253 async getUserSession ( socketId : string ) : Promise < UserSession | null > {
@@ -278,13 +310,15 @@ export class RedisRoomManager implements IRoomManager {
278310 KEYS . workflowUsers ( workflowId ) ,
279311 KEYS . socketWorkflow ( socketId ) ,
280312 KEYS . socketSession ( socketId ) ,
313+ KEYS . socketPresenceWorkflow ( socketId ) ,
281314 ] ,
282315 arguments : [
283316 socketId ,
284317 updates . cursor !== undefined ? JSON . stringify ( updates . cursor ) : '' ,
285318 updates . selection !== undefined ? JSON . stringify ( updates . selection ) : '' ,
286319 ( updates . lastActivity ?? Date . now ( ) ) . toString ( ) ,
287320 SOCKET_KEY_TTL . toString ( ) ,
321+ SOCKET_PRESENCE_WORKFLOW_KEY_TTL . toString ( ) ,
288322 ] ,
289323 } )
290324 } catch ( error ) {
@@ -348,7 +382,7 @@ export class RedisRoomManager implements IRoomManager {
348382
349383 // Remove all users from Redis state
350384 for ( const user of users ) {
351- await this . removeUserFromRoom ( user . socketId )
385+ await this . removeUserFromRoom ( user . socketId , workflowId )
352386 }
353387
354388 // Clean up room data
0 commit comments