@prsm/realtime 1.0.1 → 1.0.3
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.
- package/package.json +2 -1
- package/src/adapters/postgres.js +4 -4
- package/src/client/client.js +21 -21
- package/src/client/connection.js +1 -1
- package/src/client/subscriptions/channels.js +6 -6
- package/src/client/subscriptions/collections.js +8 -10
- package/src/client/subscriptions/presence.js +11 -11
- package/src/client/subscriptions/records.js +10 -12
- package/src/client/subscriptions/rooms.js +5 -5
- package/src/server/connection.js +2 -2
- package/src/server/managers/channels.js +4 -4
- package/src/server/managers/collections.js +5 -5
- package/src/server/managers/connections.js +3 -3
- package/src/server/managers/instance.js +26 -26
- package/src/server/managers/persistence.js +17 -17
- package/src/server/managers/presence.js +10 -10
- package/src/server/managers/pubsub.js +15 -15
- package/src/server/managers/records.js +4 -4
- package/src/server/managers/rooms.js +8 -8
- package/src/server/server.js +38 -38
- package/src/server/utils/constants.js +4 -4
- package/src/shared/index.js +1 -1
- package/src/shared/logger.js +9 -42
|
@@ -28,45 +28,45 @@ export class InstanceManager {
|
|
|
28
28
|
}
|
|
29
29
|
|
|
30
30
|
async _registerInstance() {
|
|
31
|
-
await this.redis.sadd("
|
|
31
|
+
await this.redis.sadd("rt:instances", this.instanceId)
|
|
32
32
|
}
|
|
33
33
|
|
|
34
34
|
async _deregisterInstance() {
|
|
35
|
-
await this.redis.srem("
|
|
36
|
-
await this.redis.del(`
|
|
35
|
+
await this.redis.srem("rt:instances", this.instanceId)
|
|
36
|
+
await this.redis.del(`rt:instance:${this.instanceId}:heartbeat`)
|
|
37
37
|
}
|
|
38
38
|
|
|
39
39
|
async _updateHeartbeat() {
|
|
40
|
-
await this.redis.set(`
|
|
40
|
+
await this.redis.set(`rt:instance:${this.instanceId}:heartbeat`, Date.now().toString(), "EX", this.heartbeatTTL)
|
|
41
41
|
}
|
|
42
42
|
|
|
43
43
|
async _acquireCleanupLock() {
|
|
44
|
-
const result = await this.redis.set("
|
|
44
|
+
const result = await this.redis.set("rt:cleanup:lock", this.instanceId, "EX", this.cleanupLockTTL, "NX")
|
|
45
45
|
return result === "OK"
|
|
46
46
|
}
|
|
47
47
|
|
|
48
48
|
async _releaseCleanupLock() {
|
|
49
49
|
const script = 'if redis.call("get", KEYS[1]) == ARGV[1] then return redis.call("del", KEYS[1]) else return 0 end'
|
|
50
|
-
await this.redis.eval(script, 1, "
|
|
50
|
+
await this.redis.eval(script, 1, "rt:cleanup:lock", this.instanceId)
|
|
51
51
|
}
|
|
52
52
|
|
|
53
53
|
async _performCleanup() {
|
|
54
54
|
try {
|
|
55
55
|
const lockAcquired = await this._acquireCleanupLock()
|
|
56
56
|
if (!lockAcquired) return
|
|
57
|
-
const registeredInstances = await this.redis.smembers("
|
|
58
|
-
const allConnections = await this.redis.hgetall("
|
|
57
|
+
const registeredInstances = await this.redis.smembers("rt:instances")
|
|
58
|
+
const allConnections = await this.redis.hgetall("rt:connections")
|
|
59
59
|
const instanceIds = new Set([...registeredInstances, ...Object.values(allConnections)])
|
|
60
60
|
for (const instanceId of instanceIds) {
|
|
61
61
|
if (instanceId === this.instanceId) continue
|
|
62
|
-
const heartbeat = await this.redis.get(`
|
|
62
|
+
const heartbeat = await this.redis.get(`rt:instance:${instanceId}:heartbeat`)
|
|
63
63
|
if (!heartbeat) {
|
|
64
|
-
serverLogger.info(
|
|
64
|
+
serverLogger.info("found dead instance", { instanceId })
|
|
65
65
|
await this._cleanupDeadInstance(instanceId)
|
|
66
66
|
}
|
|
67
67
|
}
|
|
68
68
|
} catch (error) {
|
|
69
|
-
serverLogger.error("
|
|
69
|
+
serverLogger.error("error during cleanup", { err: error })
|
|
70
70
|
} finally {
|
|
71
71
|
await this._releaseCleanupLock()
|
|
72
72
|
}
|
|
@@ -74,20 +74,20 @@ export class InstanceManager {
|
|
|
74
74
|
|
|
75
75
|
async _cleanupDeadInstance(instanceId) {
|
|
76
76
|
try {
|
|
77
|
-
const connectionsKey = `
|
|
77
|
+
const connectionsKey = `rt:connections:${instanceId}`
|
|
78
78
|
const connections = await this.redis.smembers(connectionsKey)
|
|
79
79
|
for (const connectionId of connections) {
|
|
80
80
|
await this._cleanupConnection(connectionId)
|
|
81
81
|
}
|
|
82
|
-
const allConnections = await this.redis.hgetall("
|
|
82
|
+
const allConnections = await this.redis.hgetall("rt:connections")
|
|
83
83
|
for (const [connectionId, connInstanceId] of Object.entries(allConnections)) {
|
|
84
84
|
if (connInstanceId === instanceId) await this._cleanupConnection(connectionId)
|
|
85
85
|
}
|
|
86
|
-
await this.redis.srem("
|
|
86
|
+
await this.redis.srem("rt:instances", instanceId)
|
|
87
87
|
await this.redis.del(connectionsKey)
|
|
88
|
-
serverLogger.info(
|
|
88
|
+
serverLogger.info("cleaned up dead instance", { instanceId })
|
|
89
89
|
} catch (error) {
|
|
90
|
-
serverLogger.error(
|
|
90
|
+
serverLogger.error("error cleaning up instance", { instanceId, err: error })
|
|
91
91
|
}
|
|
92
92
|
}
|
|
93
93
|
|
|
@@ -103,23 +103,23 @@ export class InstanceManager {
|
|
|
103
103
|
|
|
104
104
|
async _cleanupConnection(connectionId) {
|
|
105
105
|
try {
|
|
106
|
-
const roomsKey = `
|
|
106
|
+
const roomsKey = `rt:connection:${connectionId}:rooms`
|
|
107
107
|
const rooms = await this.redis.smembers(roomsKey)
|
|
108
108
|
const pipeline = this.redis.pipeline()
|
|
109
109
|
for (const room of rooms) {
|
|
110
|
-
pipeline.srem(`
|
|
111
|
-
pipeline.srem(`
|
|
112
|
-
pipeline.del(`
|
|
113
|
-
pipeline.del(`
|
|
110
|
+
pipeline.srem(`rt:room:${room}`, connectionId)
|
|
111
|
+
pipeline.srem(`rt:presence:room:${room}`, connectionId)
|
|
112
|
+
pipeline.del(`rt:presence:room:${room}:conn:${connectionId}`)
|
|
113
|
+
pipeline.del(`rt:presence:state:${room}:conn:${connectionId}`)
|
|
114
114
|
}
|
|
115
115
|
pipeline.del(roomsKey)
|
|
116
|
-
pipeline.hdel("
|
|
117
|
-
pipeline.hdel("
|
|
118
|
-
await this._deleteMatchingKeys(`
|
|
116
|
+
pipeline.hdel("rt:connections", connectionId)
|
|
117
|
+
pipeline.hdel("rt:connection-meta", connectionId)
|
|
118
|
+
await this._deleteMatchingKeys(`rt:collection:*:${connectionId}`)
|
|
119
119
|
await pipeline.exec()
|
|
120
|
-
serverLogger.debug(
|
|
120
|
+
serverLogger.debug("cleaned up stale connection", { connectionId })
|
|
121
121
|
} catch (error) {
|
|
122
|
-
serverLogger.error(
|
|
122
|
+
serverLogger.error("error cleaning up connection", { connectionId, err: error })
|
|
123
123
|
}
|
|
124
124
|
}
|
|
125
125
|
}
|
|
@@ -34,7 +34,7 @@ export class PersistenceManager extends EventEmitter {
|
|
|
34
34
|
|
|
35
35
|
async _processPendingRecordUpdates() {
|
|
36
36
|
if (this.pendingRecordUpdates.length === 0) return
|
|
37
|
-
serverLogger.info(
|
|
37
|
+
serverLogger.info("processing pending record updates", { count: this.pendingRecordUpdates.length })
|
|
38
38
|
const updates = [...this.pendingRecordUpdates]
|
|
39
39
|
this.pendingRecordUpdates = []
|
|
40
40
|
for (const { recordId, value, version } of updates) {
|
|
@@ -54,25 +54,25 @@ export class PersistenceManager extends EventEmitter {
|
|
|
54
54
|
await this._processPendingRecordUpdates()
|
|
55
55
|
this.emit("initialized")
|
|
56
56
|
} catch (err) {
|
|
57
|
-
serverLogger.error("
|
|
57
|
+
serverLogger.error("failed to initialize persistence manager", { err })
|
|
58
58
|
throw err
|
|
59
59
|
}
|
|
60
60
|
}
|
|
61
61
|
|
|
62
62
|
async restorePersistedRecords() {
|
|
63
63
|
if (!this.recordManager) {
|
|
64
|
-
serverLogger.warn("
|
|
64
|
+
serverLogger.warn("cannot restore persisted records: record manager not available")
|
|
65
65
|
return
|
|
66
66
|
}
|
|
67
67
|
const redis = this.recordManager.getRedis()
|
|
68
68
|
if (!redis) {
|
|
69
|
-
serverLogger.warn("
|
|
69
|
+
serverLogger.warn("cannot restore records: redis not available")
|
|
70
70
|
return
|
|
71
71
|
}
|
|
72
72
|
try {
|
|
73
|
-
serverLogger.info("
|
|
73
|
+
serverLogger.info("restoring persisted records")
|
|
74
74
|
if (this.recordPatterns.length === 0) {
|
|
75
|
-
serverLogger.info("
|
|
75
|
+
serverLogger.info("no record patterns to restore")
|
|
76
76
|
return
|
|
77
77
|
}
|
|
78
78
|
for (const config of this.recordPatterns) {
|
|
@@ -93,7 +93,7 @@ export class PersistenceManager extends EventEmitter {
|
|
|
93
93
|
}))
|
|
94
94
|
}
|
|
95
95
|
if (records.length > 0) {
|
|
96
|
-
serverLogger.info(
|
|
96
|
+
serverLogger.info("restoring records for pattern", { count: records.length, pattern: patternLabel })
|
|
97
97
|
for (const record of records) {
|
|
98
98
|
try {
|
|
99
99
|
const { recordId, value, version } = record
|
|
@@ -104,17 +104,17 @@ export class PersistenceManager extends EventEmitter {
|
|
|
104
104
|
pipeline.set(versionKey, version.toString())
|
|
105
105
|
await pipeline.exec()
|
|
106
106
|
} catch (parseErr) {
|
|
107
|
-
serverLogger.error(
|
|
107
|
+
serverLogger.error("failed to restore record", { recordId: record.recordId, err: parseErr })
|
|
108
108
|
}
|
|
109
109
|
}
|
|
110
110
|
}
|
|
111
111
|
} catch (patternErr) {
|
|
112
|
-
serverLogger.error(
|
|
112
|
+
serverLogger.error("error restoring records for pattern", { pattern: patternLabel, err: patternErr })
|
|
113
113
|
}
|
|
114
114
|
}
|
|
115
|
-
serverLogger.info("
|
|
115
|
+
serverLogger.info("finished restoring persisted records")
|
|
116
116
|
} catch (err) {
|
|
117
|
-
serverLogger.error("
|
|
117
|
+
serverLogger.error("failed to restore persisted records", { err })
|
|
118
118
|
}
|
|
119
119
|
}
|
|
120
120
|
|
|
@@ -133,7 +133,7 @@ export class PersistenceManager extends EventEmitter {
|
|
|
133
133
|
}
|
|
134
134
|
if (fullOptions.adapter !== this.defaultAdapter && !this.isShuttingDown) {
|
|
135
135
|
fullOptions.adapter.initialize().catch((err) => {
|
|
136
|
-
serverLogger.error(
|
|
136
|
+
serverLogger.error("failed to initialize adapter for pattern", { pattern, err })
|
|
137
137
|
})
|
|
138
138
|
}
|
|
139
139
|
this.channelPatterns.push({ pattern, options: fullOptions })
|
|
@@ -148,7 +148,7 @@ export class PersistenceManager extends EventEmitter {
|
|
|
148
148
|
resolvedAdapter = { adapter: adapterInstance, restorePattern: adapter.restorePattern }
|
|
149
149
|
if (adapterInstance !== this.defaultAdapter && !this.isShuttingDown) {
|
|
150
150
|
adapterInstance.initialize().catch((err) => {
|
|
151
|
-
serverLogger.error(
|
|
151
|
+
serverLogger.error("failed to initialize adapter for record pattern", { pattern, err })
|
|
152
152
|
})
|
|
153
153
|
}
|
|
154
154
|
}
|
|
@@ -220,7 +220,7 @@ export class PersistenceManager extends EventEmitter {
|
|
|
220
220
|
await options.adapter.storeMessages(messages)
|
|
221
221
|
this.emit("flushed", { channel, count: messages.length })
|
|
222
222
|
} catch (err) {
|
|
223
|
-
serverLogger.error(
|
|
223
|
+
serverLogger.error("failed to flush messages for channel", { channel, err })
|
|
224
224
|
if (!this.isShuttingDown) {
|
|
225
225
|
const currentMessages = this.messageBuffer.get(channel) || []
|
|
226
226
|
this.messageBuffer.set(channel, [...messages, ...currentMessages])
|
|
@@ -295,7 +295,7 @@ export class PersistenceManager extends EventEmitter {
|
|
|
295
295
|
}
|
|
296
296
|
}
|
|
297
297
|
const handleFlushError = (failedRecords, err) => {
|
|
298
|
-
serverLogger.error("
|
|
298
|
+
serverLogger.error("failed to flush records", { err })
|
|
299
299
|
if (!this.isShuttingDown) {
|
|
300
300
|
for (const record of failedRecords) this.recordBuffer.set(record.recordId, record)
|
|
301
301
|
if (!this.recordFlushTimer) {
|
|
@@ -337,7 +337,7 @@ export class PersistenceManager extends EventEmitter {
|
|
|
337
337
|
return await this.defaultAdapter.getRecords(pattern)
|
|
338
338
|
}
|
|
339
339
|
} catch (err) {
|
|
340
|
-
serverLogger.error(
|
|
340
|
+
serverLogger.error("failed to get persisted records for pattern", { pattern, err })
|
|
341
341
|
}
|
|
342
342
|
return []
|
|
343
343
|
}
|
|
@@ -364,7 +364,7 @@ export class PersistenceManager extends EventEmitter {
|
|
|
364
364
|
}
|
|
365
365
|
for (const adapter of adapters) {
|
|
366
366
|
try { await adapter.close() }
|
|
367
|
-
catch (err) { serverLogger.error("
|
|
367
|
+
catch (err) { serverLogger.error("error closing persistence adapter", { err }) }
|
|
368
368
|
}
|
|
369
369
|
this.initialized = false
|
|
370
370
|
}
|
|
@@ -7,8 +7,8 @@ export class PresenceManager {
|
|
|
7
7
|
this.redisManager = redisManager
|
|
8
8
|
this.presenceExpirationEventsEnabled = enableExpirationEvents
|
|
9
9
|
|
|
10
|
-
this.PRESENCE_KEY_PATTERN = /^
|
|
11
|
-
this.PRESENCE_STATE_KEY_PATTERN = /^
|
|
10
|
+
this.PRESENCE_KEY_PATTERN = /^rt:presence:room:(.+):conn:(.+)$/
|
|
11
|
+
this.PRESENCE_STATE_KEY_PATTERN = /^rt:presence:state:(.+):conn:(.+)$/
|
|
12
12
|
this.trackedRooms = []
|
|
13
13
|
this.roomGuards = new Map()
|
|
14
14
|
this.roomTTLs = new Map()
|
|
@@ -47,7 +47,7 @@ export class PresenceManager {
|
|
|
47
47
|
await this._publishPresenceStateUpdate(match[1], match[2], null)
|
|
48
48
|
}
|
|
49
49
|
} catch (err) {
|
|
50
|
-
serverLogger.error("
|
|
50
|
+
serverLogger.error("failed to handle expired key", { key, err })
|
|
51
51
|
}
|
|
52
52
|
}
|
|
53
53
|
|
|
@@ -89,9 +89,9 @@ export class PresenceManager {
|
|
|
89
89
|
return this.defaultTTL
|
|
90
90
|
}
|
|
91
91
|
|
|
92
|
-
presenceRoomKey(roomName) { return `
|
|
93
|
-
presenceConnectionKey(roomName, connectionId) { return `
|
|
94
|
-
presenceStateKey(roomName, connectionId) { return `
|
|
92
|
+
presenceRoomKey(roomName) { return `rt:presence:room:${roomName}` }
|
|
93
|
+
presenceConnectionKey(roomName, connectionId) { return `rt:presence:room:${roomName}:conn:${connectionId}` }
|
|
94
|
+
presenceStateKey(roomName, connectionId) { return `rt:presence:state:${roomName}:conn:${connectionId}` }
|
|
95
95
|
|
|
96
96
|
async markOnline(connectionId, roomName) {
|
|
97
97
|
const roomKey = this.presenceRoomKey(roomName)
|
|
@@ -137,7 +137,7 @@ export class PresenceManager {
|
|
|
137
137
|
}
|
|
138
138
|
|
|
139
139
|
async _publishPresenceUpdate(roomName, connectionId, type) {
|
|
140
|
-
const channel = `
|
|
140
|
+
const channel = `rt:presence:updates:${roomName}`
|
|
141
141
|
const message = JSON.stringify({ type, connectionId, roomName, timestamp: Date.now() })
|
|
142
142
|
await this.redis.publish(channel, message)
|
|
143
143
|
}
|
|
@@ -167,7 +167,7 @@ export class PresenceManager {
|
|
|
167
167
|
const value = await this.redis.get(key)
|
|
168
168
|
if (!value) return null
|
|
169
169
|
try { return JSON.parse(value) }
|
|
170
|
-
catch (e) { serverLogger.error(
|
|
170
|
+
catch (e) { serverLogger.error("failed to parse presence state", { err: e }); return null }
|
|
171
171
|
}
|
|
172
172
|
|
|
173
173
|
async getAllPresenceStates(roomName) {
|
|
@@ -186,13 +186,13 @@ export class PresenceManager {
|
|
|
186
186
|
const [err, value] = responses[i] || []
|
|
187
187
|
if (err || !value) continue
|
|
188
188
|
try { result.set(connectionId, JSON.parse(value)) }
|
|
189
|
-
catch (e) { serverLogger.error(
|
|
189
|
+
catch (e) { serverLogger.error("failed to parse presence state", { err: e }) }
|
|
190
190
|
}
|
|
191
191
|
return result
|
|
192
192
|
}
|
|
193
193
|
|
|
194
194
|
async _publishPresenceStateUpdate(roomName, connectionId, state) {
|
|
195
|
-
const channel = `
|
|
195
|
+
const channel = `rt:presence:updates:${roomName}`
|
|
196
196
|
const message = JSON.stringify({ type: "state", connectionId, roomName, state, timestamp: Date.now() })
|
|
197
197
|
await this.redis.publish(channel, message)
|
|
198
198
|
}
|
|
@@ -23,8 +23,8 @@ export class PubSubManager {
|
|
|
23
23
|
subscribeToInstanceChannel() {
|
|
24
24
|
const channel = `${PUB_SUB_CHANNEL_PREFIX}${this.instanceId}`
|
|
25
25
|
this._subscriptionPromise = new Promise((resolve, reject) => {
|
|
26
|
-
this.subClient.subscribe(channel, RECORD_PUB_SUB_CHANNEL, "
|
|
27
|
-
this.subClient.psubscribe("
|
|
26
|
+
this.subClient.subscribe(channel, RECORD_PUB_SUB_CHANNEL, "rt:collection:record-change")
|
|
27
|
+
this.subClient.psubscribe("rt:presence:updates:*", (err) => {
|
|
28
28
|
if (err) {
|
|
29
29
|
this.emitError(new Error(`Failed to subscribe to channels/patterns: ${JSON.stringify({ cause: err })}`))
|
|
30
30
|
reject(err)
|
|
@@ -43,14 +43,14 @@ export class PubSubManager {
|
|
|
43
43
|
this._handleInstancePubSubMessage(channel, message)
|
|
44
44
|
} else if (channel === RECORD_PUB_SUB_CHANNEL) {
|
|
45
45
|
this._handleRecordUpdatePubSubMessage(message)
|
|
46
|
-
} else if (channel === "
|
|
46
|
+
} else if (channel === "rt:collection:record-change") {
|
|
47
47
|
this._handleCollectionRecordChange(message)
|
|
48
48
|
} else {
|
|
49
49
|
const subscribers = this.getChannelSubscriptions(channel)
|
|
50
50
|
if (subscribers) {
|
|
51
51
|
for (const connection of subscribers) {
|
|
52
52
|
if (!connection.isDead) {
|
|
53
|
-
connection.send({ command: "
|
|
53
|
+
connection.send({ command: "rt/subscription-message", payload: { channel, message } })
|
|
54
54
|
}
|
|
55
55
|
}
|
|
56
56
|
}
|
|
@@ -58,14 +58,14 @@ export class PubSubManager {
|
|
|
58
58
|
})
|
|
59
59
|
|
|
60
60
|
this.subClient.on("pmessage", async (pattern, channel, message) => {
|
|
61
|
-
if (pattern === "
|
|
61
|
+
if (pattern === "rt:presence:updates:*") {
|
|
62
62
|
const subscribers = this.getChannelSubscriptions(channel)
|
|
63
63
|
if (subscribers) {
|
|
64
64
|
try {
|
|
65
65
|
const payload = JSON.parse(message)
|
|
66
66
|
subscribers.forEach((connection) => {
|
|
67
67
|
if (!connection.isDead) {
|
|
68
|
-
connection.send({ command: "
|
|
68
|
+
connection.send({ command: "rt/presence-update", payload })
|
|
69
69
|
} else {
|
|
70
70
|
subscribers.delete(connection)
|
|
71
71
|
}
|
|
@@ -105,11 +105,11 @@ export class PubSubManager {
|
|
|
105
105
|
const connection = this.connectionManager.getLocalConnection(connectionId)
|
|
106
106
|
if (connection && !connection.isDead) {
|
|
107
107
|
if (deleted) {
|
|
108
|
-
connection.send({ command: "
|
|
108
|
+
connection.send({ command: "rt/record-deleted", payload: { recordId, version } })
|
|
109
109
|
} else if (mode === "patch" && patch) {
|
|
110
|
-
connection.send({ command: "
|
|
110
|
+
connection.send({ command: "rt/record-update", payload: { recordId, patch, version } })
|
|
111
111
|
} else if (mode === "full" && newValue !== undefined) {
|
|
112
|
-
connection.send({ command: "
|
|
112
|
+
connection.send({ command: "rt/record-update", payload: { recordId, full: newValue, version } })
|
|
113
113
|
}
|
|
114
114
|
} else if (!connection || connection.isDead) {
|
|
115
115
|
subscribers.delete(connectionId)
|
|
@@ -168,7 +168,7 @@ export class PubSubManager {
|
|
|
168
168
|
|
|
169
169
|
const newRecords = await this.collectionManager.resolveCollection(collectionId, connection)
|
|
170
170
|
const newRecordIds = newRecords.map((record) => record.id)
|
|
171
|
-
const previousRecordIdsKey = `
|
|
171
|
+
const previousRecordIdsKey = `rt:collection:${collectionId}:${connectionId}`
|
|
172
172
|
const previousRecordIdsStr = await this.pubClient.get(previousRecordIdsKey)
|
|
173
173
|
const previousRecordIds = previousRecordIdsStr ? JSON.parse(previousRecordIdsStr) : []
|
|
174
174
|
|
|
@@ -199,7 +199,7 @@ export class PubSubManager {
|
|
|
199
199
|
this.collectionManager.updateSubscriptionVersion(collectionId, connectionId, newCollectionVersion)
|
|
200
200
|
await this.pubClient.set(previousRecordIdsKey, JSON.stringify(newRecordIds))
|
|
201
201
|
connection.send({
|
|
202
|
-
command: "
|
|
202
|
+
command: "rt/collection-diff",
|
|
203
203
|
payload: { collectionId, added, removed, version: newCollectionVersion },
|
|
204
204
|
})
|
|
205
205
|
}
|
|
@@ -209,10 +209,10 @@ export class PubSubManager {
|
|
|
209
209
|
try {
|
|
210
210
|
const { record, version } = await this.recordManager.getRecordAndVersion(recordId)
|
|
211
211
|
if (record) {
|
|
212
|
-
connection.send({ command: "
|
|
212
|
+
connection.send({ command: "rt/record-update", payload: { recordId, version, full: record } })
|
|
213
213
|
}
|
|
214
214
|
} catch (recordError) {
|
|
215
|
-
serverLogger.info(
|
|
215
|
+
serverLogger.info("record not found during collection update, likely deleted", { recordId })
|
|
216
216
|
}
|
|
217
217
|
}
|
|
218
218
|
}
|
|
@@ -234,8 +234,8 @@ export class PubSubManager {
|
|
|
234
234
|
if (this.subClient && this.subClient.status !== "end") {
|
|
235
235
|
const channel = `${PUB_SUB_CHANNEL_PREFIX}${this.instanceId}`
|
|
236
236
|
await Promise.all([
|
|
237
|
-
new Promise((resolve) => { this.subClient.unsubscribe(channel, RECORD_PUB_SUB_CHANNEL, "
|
|
238
|
-
new Promise((resolve) => { this.subClient.punsubscribe("
|
|
237
|
+
new Promise((resolve) => { this.subClient.unsubscribe(channel, RECORD_PUB_SUB_CHANNEL, "rt:collection:record-change", () => resolve()) }),
|
|
238
|
+
new Promise((resolve) => { this.subClient.punsubscribe("rt:presence:updates:*", () => resolve()) }),
|
|
239
239
|
])
|
|
240
240
|
}
|
|
241
241
|
}
|
|
@@ -65,10 +65,10 @@ export class RecordManager {
|
|
|
65
65
|
Promise.all(
|
|
66
66
|
this.recordUpdateCallbacks.map(async (callback) => {
|
|
67
67
|
try { await callback({ recordId, value: finalValue }) }
|
|
68
|
-
catch (error) { serverLogger.error(
|
|
68
|
+
catch (error) { serverLogger.error("error in record update callback", { recordId, err: error }) }
|
|
69
69
|
})
|
|
70
70
|
).catch((error) => {
|
|
71
|
-
serverLogger.error(
|
|
71
|
+
serverLogger.error("error in record update callbacks", { recordId, err: error })
|
|
72
72
|
})
|
|
73
73
|
}
|
|
74
74
|
|
|
@@ -88,10 +88,10 @@ export class RecordManager {
|
|
|
88
88
|
Promise.all(
|
|
89
89
|
this.recordRemovedCallbacks.map(async (callback) => {
|
|
90
90
|
try { await callback({ recordId, value: record }) }
|
|
91
|
-
catch (error) { serverLogger.error(
|
|
91
|
+
catch (error) { serverLogger.error("error in record removed callback", { recordId, err: error }) }
|
|
92
92
|
})
|
|
93
93
|
).catch((error) => {
|
|
94
|
-
serverLogger.error(
|
|
94
|
+
serverLogger.error("error in record removed callbacks", { recordId, err: error })
|
|
95
95
|
})
|
|
96
96
|
}
|
|
97
97
|
|
|
@@ -5,9 +5,9 @@ export class RoomManager {
|
|
|
5
5
|
this.redis = redis
|
|
6
6
|
}
|
|
7
7
|
|
|
8
|
-
roomKey(roomName) { return `
|
|
9
|
-
connectionsRoomKey(connectionId) { return `
|
|
10
|
-
roomMetadataKey(roomName) { return `
|
|
8
|
+
roomKey(roomName) { return `rt:room:${roomName}` }
|
|
9
|
+
connectionsRoomKey(connectionId) { return `rt:connection:${connectionId}:rooms` }
|
|
10
|
+
roomMetadataKey(roomName) { return `rt:roommeta:${roomName}` }
|
|
11
11
|
|
|
12
12
|
async getRoomConnectionIds(roomName) {
|
|
13
13
|
return this.redis.smembers(this.roomKey(roomName))
|
|
@@ -30,8 +30,8 @@ export class RoomManager {
|
|
|
30
30
|
}
|
|
31
31
|
|
|
32
32
|
async getAllRooms() {
|
|
33
|
-
const keys = await this.redis.keys("
|
|
34
|
-
return keys.map((key) => key.replace("
|
|
33
|
+
const keys = await this.redis.keys("rt:room:*")
|
|
34
|
+
return keys.map((key) => key.replace("rt:room:", ""))
|
|
35
35
|
}
|
|
36
36
|
|
|
37
37
|
async removeFromRoom(roomName, connection) {
|
|
@@ -110,18 +110,18 @@ export class RoomManager {
|
|
|
110
110
|
}
|
|
111
111
|
|
|
112
112
|
async getAllMetadata() {
|
|
113
|
-
const keys = await this.redis.keys("
|
|
113
|
+
const keys = await this.redis.keys("rt:roommeta:*")
|
|
114
114
|
const result = []
|
|
115
115
|
if (keys.length === 0) return result
|
|
116
116
|
const pipeline = this.redis.pipeline()
|
|
117
117
|
keys.forEach((key) => pipeline.hget(key, "data"))
|
|
118
118
|
const results = await pipeline.exec()
|
|
119
119
|
keys.forEach((key, index) => {
|
|
120
|
-
const roomName = key.replace("
|
|
120
|
+
const roomName = key.replace("rt:roommeta:", "")
|
|
121
121
|
const data = results?.[index]?.[1]
|
|
122
122
|
if (data) {
|
|
123
123
|
try { result.push({ id: roomName, metadata: JSON.parse(data) }) }
|
|
124
|
-
catch (e) { serverLogger.error(
|
|
124
|
+
catch (e) { serverLogger.error("failed to parse metadata for room", { roomName, err: e }) }
|
|
125
125
|
}
|
|
126
126
|
})
|
|
127
127
|
return result
|