@prsm/realtime 1.0.1 → 1.0.2
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 +1 -1
- package/src/adapters/postgres.js +3 -3
- package/src/client/client.js +10 -10
- package/src/client/connection.js +1 -1
- package/src/client/subscriptions/channels.js +3 -3
- package/src/client/subscriptions/collections.js +2 -2
- package/src/client/subscriptions/presence.js +5 -5
- package/src/client/subscriptions/records.js +3 -3
- package/src/client/subscriptions/rooms.js +3 -3
- 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 +20 -20
- package/src/server/managers/presence.js +7 -7
- package/src/server/managers/pubsub.js +14 -14
- package/src/server/managers/rooms.js +7 -7
- package/src/server/server.js +26 -26
- package/src/server/utils/constants.js +4 -4
- package/src/shared/logger.js +1 -1
package/package.json
CHANGED
package/src/adapters/postgres.js
CHANGED
|
@@ -5,9 +5,9 @@ export function createPostgresAdapter(options = {}) {
|
|
|
5
5
|
const opts = {
|
|
6
6
|
host: "localhost",
|
|
7
7
|
port: 5432,
|
|
8
|
-
database: "
|
|
9
|
-
user: "
|
|
10
|
-
password: "
|
|
8
|
+
database: "realtime_test",
|
|
9
|
+
user: "realtime",
|
|
10
|
+
password: "realtime_password",
|
|
11
11
|
max: 10,
|
|
12
12
|
...options,
|
|
13
13
|
}
|
package/src/client/client.js
CHANGED
|
@@ -169,10 +169,10 @@ export class RealtimeClient extends EventEmitter {
|
|
|
169
169
|
async getConnectionMetadata(connectionId) {
|
|
170
170
|
try {
|
|
171
171
|
if (connectionId) {
|
|
172
|
-
const result = await this.command("
|
|
172
|
+
const result = await this.command("rt/get-connection-metadata", { connectionId })
|
|
173
173
|
return result.metadata
|
|
174
174
|
}
|
|
175
|
-
const result = await this.command("
|
|
175
|
+
const result = await this.command("rt/get-my-connection-metadata")
|
|
176
176
|
return result.metadata
|
|
177
177
|
} catch (error) {
|
|
178
178
|
clientLogger.error(`Failed to get metadata for connection:`, error)
|
|
@@ -187,7 +187,7 @@ export class RealtimeClient extends EventEmitter {
|
|
|
187
187
|
*/
|
|
188
188
|
async setConnectionMetadata(metadata, options) {
|
|
189
189
|
try {
|
|
190
|
-
const result = await this.command("
|
|
190
|
+
const result = await this.command("rt/set-my-connection-metadata", { metadata, options })
|
|
191
191
|
return result.success
|
|
192
192
|
} catch (error) {
|
|
193
193
|
clientLogger.error(`Failed to set metadata for connection:`, error)
|
|
@@ -199,11 +199,11 @@ export class RealtimeClient extends EventEmitter {
|
|
|
199
199
|
this.connection.on("message", (data) => {
|
|
200
200
|
this.emit("message", data)
|
|
201
201
|
|
|
202
|
-
if (data.command === "
|
|
203
|
-
else if (data.command === "
|
|
204
|
-
else if (data.command === "
|
|
205
|
-
else if (data.command === "
|
|
206
|
-
else if (data.command === "
|
|
202
|
+
if (data.command === "rt/record-update") this._records.handleUpdate(data.payload)
|
|
203
|
+
else if (data.command === "rt/record-deleted") this._records.handleDeleted(data.payload)
|
|
204
|
+
else if (data.command === "rt/presence-update") this._presence.handleUpdate(data.payload)
|
|
205
|
+
else if (data.command === "rt/subscription-message") this._channels.handleMessage(data.payload)
|
|
206
|
+
else if (data.command === "rt/collection-diff") this._collections.handleDiff(data.payload)
|
|
207
207
|
else {
|
|
208
208
|
const systemCommands = ["ping", "pong", "latency", "latency:request", "latency:response"]
|
|
209
209
|
if (data.command && !systemCommands.includes(data.command)) {
|
|
@@ -238,7 +238,7 @@ export class RealtimeClient extends EventEmitter {
|
|
|
238
238
|
this._lastActivityTime = Date.now()
|
|
239
239
|
if (eventName === "visibilitychange" && doc.visibilityState === "visible") {
|
|
240
240
|
if (this._status === Status.OFFLINE) return
|
|
241
|
-
this.command("
|
|
241
|
+
this.command("rt/noop", {}, 5000)
|
|
242
242
|
.then(() => { clientLogger.info("Tab visible, connection ok"); this.emit("republish") })
|
|
243
243
|
.catch(() => { clientLogger.info("Tab visible, forcing reconnect"); this._forceReconnect() })
|
|
244
244
|
}
|
|
@@ -253,7 +253,7 @@ export class RealtimeClient extends EventEmitter {
|
|
|
253
253
|
const now = Date.now()
|
|
254
254
|
const timeSinceActivity = now - this._lastActivityTime
|
|
255
255
|
if (timeSinceActivity > this.options.pingTimeout && this._status === Status.ONLINE) {
|
|
256
|
-
this.command("
|
|
256
|
+
this.command("rt/noop", {}, 5000).catch(() => {
|
|
257
257
|
clientLogger.info(`No activity for ${timeSinceActivity}ms, forcing reconnect`)
|
|
258
258
|
this._forceReconnect()
|
|
259
259
|
})
|
package/src/client/connection.js
CHANGED
|
@@ -91,7 +91,7 @@ export class Connection extends EventEmitter {
|
|
|
91
91
|
|
|
92
92
|
this.emit("message", data);
|
|
93
93
|
|
|
94
|
-
if (data.command === "
|
|
94
|
+
if (data.command === "rt/assign-id") {
|
|
95
95
|
this.connectionId = data.payload;
|
|
96
96
|
this.emit("id-assigned", data.payload);
|
|
97
97
|
} else if (data.command === "latency:request") {
|
|
@@ -23,7 +23,7 @@ export function createChannelSubscriptions(client) {
|
|
|
23
23
|
*/
|
|
24
24
|
async function subscribe(channel, callback, options) {
|
|
25
25
|
subscriptions.set(channel, { callback, historyLimit: options?.historyLimit })
|
|
26
|
-
const result = await client.command("
|
|
26
|
+
const result = await client.command("rt/subscribe-channel", {
|
|
27
27
|
channel,
|
|
28
28
|
historyLimit: options?.historyLimit,
|
|
29
29
|
since: options?.since,
|
|
@@ -40,7 +40,7 @@ export function createChannelSubscriptions(client) {
|
|
|
40
40
|
*/
|
|
41
41
|
function unsubscribe(channel) {
|
|
42
42
|
subscriptions.delete(channel)
|
|
43
|
-
return client.command("
|
|
43
|
+
return client.command("rt/unsubscribe-channel", { channel })
|
|
44
44
|
}
|
|
45
45
|
|
|
46
46
|
/**
|
|
@@ -50,7 +50,7 @@ export function createChannelSubscriptions(client) {
|
|
|
50
50
|
*/
|
|
51
51
|
async function getHistory(channel, options) {
|
|
52
52
|
try {
|
|
53
|
-
const result = await client.command("
|
|
53
|
+
const result = await client.command("rt/get-channel-history", {
|
|
54
54
|
channel,
|
|
55
55
|
limit: options?.limit,
|
|
56
56
|
since: options?.since,
|
|
@@ -42,7 +42,7 @@ export function createCollectionSubscriptions(client) {
|
|
|
42
42
|
*/
|
|
43
43
|
async function subscribe(collectionId, options = {}) {
|
|
44
44
|
try {
|
|
45
|
-
const result = await client.command("
|
|
45
|
+
const result = await client.command("rt/subscribe-collection", { collectionId })
|
|
46
46
|
if (result.success) {
|
|
47
47
|
subscriptions.set(collectionId, {
|
|
48
48
|
ids: new Set(result.ids),
|
|
@@ -70,7 +70,7 @@ export function createCollectionSubscriptions(client) {
|
|
|
70
70
|
*/
|
|
71
71
|
async function unsubscribe(collectionId) {
|
|
72
72
|
try {
|
|
73
|
-
const success = await client.command("
|
|
73
|
+
const success = await client.command("rt/unsubscribe-collection", { collectionId })
|
|
74
74
|
if (success) subscriptions.delete(collectionId)
|
|
75
75
|
return success
|
|
76
76
|
} catch (error) {
|
|
@@ -16,7 +16,7 @@ export function createPresenceSubscriptions(client) {
|
|
|
16
16
|
*/
|
|
17
17
|
async function subscribe(roomName, callback) {
|
|
18
18
|
try {
|
|
19
|
-
const result = await client.command("
|
|
19
|
+
const result = await client.command("rt/subscribe-presence", { roomName })
|
|
20
20
|
if (result.success) {
|
|
21
21
|
subscriptions.set(roomName, callback)
|
|
22
22
|
if (result.present && result.present.length > 0) await callback(result)
|
|
@@ -34,7 +34,7 @@ export function createPresenceSubscriptions(client) {
|
|
|
34
34
|
*/
|
|
35
35
|
async function unsubscribe(roomName) {
|
|
36
36
|
try {
|
|
37
|
-
const success = await client.command("
|
|
37
|
+
const success = await client.command("rt/unsubscribe-presence", { roomName })
|
|
38
38
|
if (success) subscriptions.delete(roomName)
|
|
39
39
|
return success
|
|
40
40
|
} catch (error) {
|
|
@@ -50,7 +50,7 @@ export function createPresenceSubscriptions(client) {
|
|
|
50
50
|
*/
|
|
51
51
|
async function publishState(roomName, options) {
|
|
52
52
|
try {
|
|
53
|
-
return await client.command("
|
|
53
|
+
return await client.command("rt/publish-presence-state", {
|
|
54
54
|
roomName,
|
|
55
55
|
state: options.state,
|
|
56
56
|
expireAfter: options.expireAfter,
|
|
@@ -68,7 +68,7 @@ export function createPresenceSubscriptions(client) {
|
|
|
68
68
|
*/
|
|
69
69
|
async function clearState(roomName) {
|
|
70
70
|
try {
|
|
71
|
-
return await client.command("
|
|
71
|
+
return await client.command("rt/clear-presence-state", { roomName })
|
|
72
72
|
} catch (error) {
|
|
73
73
|
clientLogger.error(`Failed to clear presence state for room ${roomName}:`, error)
|
|
74
74
|
return false
|
|
@@ -79,7 +79,7 @@ export function createPresenceSubscriptions(client) {
|
|
|
79
79
|
try {
|
|
80
80
|
const handler = subscriptions.get(roomName)
|
|
81
81
|
if (!handler) return false
|
|
82
|
-
const result = await client.command("
|
|
82
|
+
const result = await client.command("rt/get-presence-state", { roomName }, 5000).catch((err) => {
|
|
83
83
|
clientLogger.error(`Failed to get presence state for room ${roomName}:`, err)
|
|
84
84
|
return { success: false }
|
|
85
85
|
})
|
|
@@ -63,7 +63,7 @@ export function createRecordSubscriptions(client) {
|
|
|
63
63
|
async function subscribe(recordId, callback, options) {
|
|
64
64
|
const mode = options?.mode ?? "full"
|
|
65
65
|
try {
|
|
66
|
-
const result = await client.command("
|
|
66
|
+
const result = await client.command("rt/subscribe-record", { recordId, mode })
|
|
67
67
|
if (result.success) {
|
|
68
68
|
subscriptions.set(recordId, { callback, localVersion: result.version, mode })
|
|
69
69
|
if (callback) await callback({ recordId, full: result.record, version: result.version })
|
|
@@ -81,7 +81,7 @@ export function createRecordSubscriptions(client) {
|
|
|
81
81
|
*/
|
|
82
82
|
async function unsubscribe(recordId) {
|
|
83
83
|
try {
|
|
84
|
-
const success = await client.command("
|
|
84
|
+
const success = await client.command("rt/unsubscribe-record", { recordId })
|
|
85
85
|
if (success) subscriptions.delete(recordId)
|
|
86
86
|
return success
|
|
87
87
|
} catch (error) {
|
|
@@ -98,7 +98,7 @@ export function createRecordSubscriptions(client) {
|
|
|
98
98
|
*/
|
|
99
99
|
async function write(recordId, newValue, options) {
|
|
100
100
|
try {
|
|
101
|
-
const result = await client.command("
|
|
101
|
+
const result = await client.command("rt/publish-record-update", { recordId, newValue, options })
|
|
102
102
|
return result.success === true
|
|
103
103
|
} catch (error) {
|
|
104
104
|
clientLogger.error(`Failed to publish update for record ${recordId}:`, error)
|
|
@@ -9,7 +9,7 @@ export function createRoomSubscriptions(client, presence) {
|
|
|
9
9
|
* @returns {Promise<{success: boolean, present: string[]}>}
|
|
10
10
|
*/
|
|
11
11
|
async function join(roomName, onPresenceUpdate) {
|
|
12
|
-
const joinResult = await client.command("
|
|
12
|
+
const joinResult = await client.command("rt/join-room", { roomName })
|
|
13
13
|
if (!joinResult.success) return { success: false, present: [] }
|
|
14
14
|
|
|
15
15
|
joinedRooms.set(roomName, onPresenceUpdate)
|
|
@@ -25,7 +25,7 @@ export function createRoomSubscriptions(client, presence) {
|
|
|
25
25
|
* @returns {Promise<{success: boolean}>}
|
|
26
26
|
*/
|
|
27
27
|
async function leave(roomName) {
|
|
28
|
-
const result = await client.command("
|
|
28
|
+
const result = await client.command("rt/leave-room", { roomName })
|
|
29
29
|
if (result.success) {
|
|
30
30
|
joinedRooms.delete(roomName)
|
|
31
31
|
if (client.presenceSubscriptions.has(roomName)) {
|
|
@@ -41,7 +41,7 @@ export function createRoomSubscriptions(client, presence) {
|
|
|
41
41
|
*/
|
|
42
42
|
async function getMetadata(roomName) {
|
|
43
43
|
try {
|
|
44
|
-
const result = await client.command("
|
|
44
|
+
const result = await client.command("rt/get-room-metadata", { roomName })
|
|
45
45
|
return result.metadata
|
|
46
46
|
} catch (error) {
|
|
47
47
|
clientLogger.error(`Failed to get metadata for room ${roomName}:`, error)
|
|
@@ -36,8 +36,8 @@ export class ChannelManager {
|
|
|
36
36
|
const serialized = typeof message === "string" ? message : JSON.stringify(message)
|
|
37
37
|
const parsedHistory = parseInt(history, 10)
|
|
38
38
|
if (!isNaN(parsedHistory) && parsedHistory > 0) {
|
|
39
|
-
await this.pubClient.rpush(`
|
|
40
|
-
await this.pubClient.ltrim(`
|
|
39
|
+
await this.pubClient.rpush(`rt:history:${channel}`, serialized)
|
|
40
|
+
await this.pubClient.ltrim(`rt:history:${channel}`, -parsedHistory, -1)
|
|
41
41
|
}
|
|
42
42
|
this.messageStream.publishMessage(channel, serialized, instanceId)
|
|
43
43
|
await this.pubClient.publish(channel, serialized)
|
|
@@ -89,11 +89,11 @@ export class ChannelManager {
|
|
|
89
89
|
const messages = await this.persistenceManager.getMessages(channel, since, limit)
|
|
90
90
|
return messages.map((msg) => msg.message)
|
|
91
91
|
} catch {
|
|
92
|
-
const historyKey = `
|
|
92
|
+
const historyKey = `rt:history:${channel}`
|
|
93
93
|
return this.redis.lrange(historyKey, 0, limit - 1)
|
|
94
94
|
}
|
|
95
95
|
}
|
|
96
|
-
const historyKey = `
|
|
96
|
+
const historyKey = `rt:history:${channel}`
|
|
97
97
|
return this.redis.lrange(historyKey, 0, limit - 1)
|
|
98
98
|
}
|
|
99
99
|
|
|
@@ -38,7 +38,7 @@ export class CollectionManager {
|
|
|
38
38
|
const ids = records.map((record) => record.id)
|
|
39
39
|
const version = 1
|
|
40
40
|
this.collectionSubscriptions.get(collectionId).set(connectionId, { version })
|
|
41
|
-
await this.redis.set(`
|
|
41
|
+
await this.redis.set(`rt:collection:${collectionId}:${connectionId}`, JSON.stringify(ids))
|
|
42
42
|
return { ids, records, version }
|
|
43
43
|
}
|
|
44
44
|
|
|
@@ -47,7 +47,7 @@ export class CollectionManager {
|
|
|
47
47
|
if (collectionSubs?.has(connectionId)) {
|
|
48
48
|
collectionSubs.delete(connectionId)
|
|
49
49
|
if (collectionSubs.size === 0) this.collectionSubscriptions.delete(collectionId)
|
|
50
|
-
await this.redis.del(`
|
|
50
|
+
await this.redis.del(`rt:collection:${collectionId}:${connectionId}`)
|
|
51
51
|
return true
|
|
52
52
|
}
|
|
53
53
|
return false
|
|
@@ -55,7 +55,7 @@ export class CollectionManager {
|
|
|
55
55
|
|
|
56
56
|
async publishRecordChange(recordId) {
|
|
57
57
|
try {
|
|
58
|
-
await this.redis.publish("
|
|
58
|
+
await this.redis.publish("rt:collection:record-change", recordId)
|
|
59
59
|
} catch (error) {
|
|
60
60
|
this.emitError(new Error(`Failed to publish record change for ${recordId}: ${error}`))
|
|
61
61
|
}
|
|
@@ -69,7 +69,7 @@ export class CollectionManager {
|
|
|
69
69
|
subscribers.delete(connectionId)
|
|
70
70
|
if (subscribers.size === 0) this.collectionSubscriptions.delete(collectionId)
|
|
71
71
|
cleanupPromises.push(
|
|
72
|
-
this.redis.del(`
|
|
72
|
+
this.redis.del(`rt:collection:${collectionId}:${connectionId}`).then(() => {}).catch((err) => {
|
|
73
73
|
this.emitError(new Error(`Failed to clean up collection subscription for "${collectionId}": ${err}`))
|
|
74
74
|
})
|
|
75
75
|
)
|
|
@@ -79,7 +79,7 @@ export class CollectionManager {
|
|
|
79
79
|
|
|
80
80
|
async listRecordsMatching(pattern, options) {
|
|
81
81
|
try {
|
|
82
|
-
const recordKeyPrefix = "
|
|
82
|
+
const recordKeyPrefix = "rt:record:"
|
|
83
83
|
const keys = []
|
|
84
84
|
let cursor = "0"
|
|
85
85
|
do {
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { deepMerge, isObject } from "../../shared/index.js"
|
|
2
2
|
|
|
3
|
-
const CONNECTIONS_HASH_KEY = "
|
|
4
|
-
const CONNECTIONS_META_HASH_KEY = "
|
|
5
|
-
const INSTANCE_CONNECTIONS_KEY_PREFIX = "
|
|
3
|
+
const CONNECTIONS_HASH_KEY = "rt:connections"
|
|
4
|
+
const CONNECTIONS_META_HASH_KEY = "rt:connection-meta"
|
|
5
|
+
const INSTANCE_CONNECTIONS_KEY_PREFIX = "rt:connections:"
|
|
6
6
|
|
|
7
7
|
export class ConnectionManager {
|
|
8
8
|
constructor({ redis, instanceId, roomManager }) {
|
|
@@ -28,38 +28,38 @@ 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
64
|
serverLogger.info(`Found dead instance: ${instanceId}`)
|
|
65
65
|
await this._cleanupDeadInstance(instanceId)
|
|
@@ -74,16 +74,16 @@ 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
88
|
serverLogger.info(`Cleaned up dead instance: ${instanceId}`)
|
|
89
89
|
} catch (error) {
|
|
@@ -103,19 +103,19 @@ 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
120
|
serverLogger.debug(`Cleaned up stale connection: ${connectionId}`)
|
|
121
121
|
} catch (error) {
|
|
@@ -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()
|
|
@@ -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
|
}
|
|
@@ -192,7 +192,7 @@ export class PresenceManager {
|
|
|
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,7 +209,7 @@ 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
215
|
serverLogger.info(`Record ${recordId} not found during collection update (likely deleted).`)
|
|
@@ -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
|
}
|
|
@@ -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,14 +110,14 @@ 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) }) }
|
package/src/server/server.js
CHANGED
|
@@ -292,7 +292,7 @@ export class RealtimeServer {
|
|
|
292
292
|
pendingAuthDataStore.delete(req)
|
|
293
293
|
await this.connectionManager.setMetadata(connection, authData)
|
|
294
294
|
}
|
|
295
|
-
connection.send({ command: "
|
|
295
|
+
connection.send({ command: "rt/assign-id", payload: connection.id })
|
|
296
296
|
} catch (error) {
|
|
297
297
|
connection.close()
|
|
298
298
|
return
|
|
@@ -490,7 +490,7 @@ export class RealtimeServer {
|
|
|
490
490
|
if (connection) {
|
|
491
491
|
metadata = await this.connectionManager.getMetadata(connection)
|
|
492
492
|
} else {
|
|
493
|
-
const metadataString = await this.redisManager.redis.hget("
|
|
493
|
+
const metadataString = await this.redisManager.redis.hget("rt:connection-meta", connectionId)
|
|
494
494
|
metadata = metadataString ? JSON.parse(metadataString) : null
|
|
495
495
|
}
|
|
496
496
|
return { id: connectionId, metadata }
|
|
@@ -582,9 +582,9 @@ export class RealtimeServer {
|
|
|
582
582
|
}
|
|
583
583
|
|
|
584
584
|
_registerBuiltinCommands() {
|
|
585
|
-
this.exposeCommand("
|
|
585
|
+
this.exposeCommand("rt/noop", async () => true)
|
|
586
586
|
|
|
587
|
-
this.exposeCommand("
|
|
587
|
+
this.exposeCommand("rt/subscribe-channel", async (ctx) => {
|
|
588
588
|
const { channel, historyLimit, since } = ctx.payload
|
|
589
589
|
if (!(await this.channelManager.isChannelExposed(channel, ctx.connection))) {
|
|
590
590
|
return { success: false, history: [] }
|
|
@@ -601,7 +601,7 @@ export class RealtimeServer {
|
|
|
601
601
|
}
|
|
602
602
|
})
|
|
603
603
|
|
|
604
|
-
this.exposeCommand("
|
|
604
|
+
this.exposeCommand("rt/unsubscribe-channel", async (ctx) => {
|
|
605
605
|
const { channel } = ctx.payload
|
|
606
606
|
const wasSubscribed = this.channelManager.removeSubscription(channel, ctx.connection)
|
|
607
607
|
if (wasSubscribed && !this.channelManager.getSubscribers(channel)) {
|
|
@@ -610,7 +610,7 @@ export class RealtimeServer {
|
|
|
610
610
|
return wasSubscribed
|
|
611
611
|
})
|
|
612
612
|
|
|
613
|
-
this.exposeCommand("
|
|
613
|
+
this.exposeCommand("rt/get-channel-history", async (ctx) => {
|
|
614
614
|
const { channel, limit, since } = ctx.payload
|
|
615
615
|
if (!(await this.channelManager.isChannelExposed(channel, ctx.connection))) {
|
|
616
616
|
return { success: false, history: [] }
|
|
@@ -630,44 +630,44 @@ export class RealtimeServer {
|
|
|
630
630
|
}
|
|
631
631
|
})
|
|
632
632
|
|
|
633
|
-
this.exposeCommand("
|
|
633
|
+
this.exposeCommand("rt/join-room", async (ctx) => {
|
|
634
634
|
const { roomName } = ctx.payload
|
|
635
635
|
await this.addToRoom(roomName, ctx.connection)
|
|
636
636
|
const present = await this.getRoomMembersWithMetadata(roomName)
|
|
637
637
|
return { success: true, present }
|
|
638
638
|
})
|
|
639
639
|
|
|
640
|
-
this.exposeCommand("
|
|
640
|
+
this.exposeCommand("rt/leave-room", async (ctx) => {
|
|
641
641
|
const { roomName } = ctx.payload
|
|
642
642
|
await this.removeFromRoom(roomName, ctx.connection)
|
|
643
643
|
return { success: true }
|
|
644
644
|
})
|
|
645
645
|
|
|
646
|
-
this.exposeCommand("
|
|
646
|
+
this.exposeCommand("rt/get-connection-metadata", async (ctx) => {
|
|
647
647
|
const { connectionId } = ctx.payload
|
|
648
648
|
const connection = this.connectionManager.getLocalConnection(connectionId)
|
|
649
649
|
if (connection) {
|
|
650
650
|
const metadata = await this.connectionManager.getMetadata(connection)
|
|
651
651
|
return { metadata }
|
|
652
652
|
} else {
|
|
653
|
-
const metadata = await this.redisManager.redis.hget("
|
|
653
|
+
const metadata = await this.redisManager.redis.hget("rt:connection-meta", connectionId)
|
|
654
654
|
return { metadata: metadata ? JSON.parse(metadata) : null }
|
|
655
655
|
}
|
|
656
656
|
})
|
|
657
657
|
|
|
658
|
-
this.exposeCommand("
|
|
658
|
+
this.exposeCommand("rt/get-my-connection-metadata", async (ctx) => {
|
|
659
659
|
const connectionId = ctx.connection.id
|
|
660
660
|
const connection = this.connectionManager.getLocalConnection(connectionId)
|
|
661
661
|
if (connection) {
|
|
662
662
|
const metadata = await this.connectionManager.getMetadata(connection)
|
|
663
663
|
return { metadata }
|
|
664
664
|
} else {
|
|
665
|
-
const metadata = await this.redisManager.redis.hget("
|
|
665
|
+
const metadata = await this.redisManager.redis.hget("rt:connection-meta", connectionId)
|
|
666
666
|
return { metadata: metadata ? JSON.parse(metadata) : null }
|
|
667
667
|
}
|
|
668
668
|
})
|
|
669
669
|
|
|
670
|
-
this.exposeCommand("
|
|
670
|
+
this.exposeCommand("rt/set-my-connection-metadata", async (ctx) => {
|
|
671
671
|
const { metadata, options } = ctx.payload
|
|
672
672
|
const connectionId = ctx.connection.id
|
|
673
673
|
const connection = this.connectionManager.getLocalConnection(connectionId)
|
|
@@ -683,7 +683,7 @@ export class RealtimeServer {
|
|
|
683
683
|
}
|
|
684
684
|
})
|
|
685
685
|
|
|
686
|
-
this.exposeCommand("
|
|
686
|
+
this.exposeCommand("rt/get-room-metadata", async (ctx) => {
|
|
687
687
|
const { roomName } = ctx.payload
|
|
688
688
|
const metadata = await this.roomManager.getMetadata(roomName)
|
|
689
689
|
return { metadata }
|
|
@@ -691,7 +691,7 @@ export class RealtimeServer {
|
|
|
691
691
|
}
|
|
692
692
|
|
|
693
693
|
_registerRecordCommands() {
|
|
694
|
-
this.exposeCommand("
|
|
694
|
+
this.exposeCommand("rt/subscribe-record", async (ctx) => {
|
|
695
695
|
const { recordId, mode = "full" } = ctx.payload
|
|
696
696
|
const connectionId = ctx.connection.id
|
|
697
697
|
if (!(await this.recordSubscriptionManager.isRecordExposed(recordId, ctx.connection))) {
|
|
@@ -707,12 +707,12 @@ export class RealtimeServer {
|
|
|
707
707
|
}
|
|
708
708
|
})
|
|
709
709
|
|
|
710
|
-
this.exposeCommand("
|
|
710
|
+
this.exposeCommand("rt/unsubscribe-record", async (ctx) => {
|
|
711
711
|
const { recordId } = ctx.payload
|
|
712
712
|
return this.recordSubscriptionManager.removeSubscription(recordId, ctx.connection.id)
|
|
713
713
|
})
|
|
714
714
|
|
|
715
|
-
this.exposeCommand("
|
|
715
|
+
this.exposeCommand("rt/publish-record-update", async (ctx) => {
|
|
716
716
|
const { recordId, newValue, options } = ctx.payload
|
|
717
717
|
if (!(await this.recordSubscriptionManager.isRecordWritable(recordId, ctx.connection))) {
|
|
718
718
|
throw new Error(`Record "${recordId}" is not writable by this connection.`)
|
|
@@ -725,13 +725,13 @@ export class RealtimeServer {
|
|
|
725
725
|
}
|
|
726
726
|
})
|
|
727
727
|
|
|
728
|
-
this.exposeCommand("
|
|
728
|
+
this.exposeCommand("rt/subscribe-presence", async (ctx) => {
|
|
729
729
|
const { roomName } = ctx.payload
|
|
730
730
|
if (!(await this.presenceManager.isRoomTracked(roomName, ctx.connection))) {
|
|
731
731
|
return { success: false, present: [] }
|
|
732
732
|
}
|
|
733
733
|
try {
|
|
734
|
-
const presenceChannel = `
|
|
734
|
+
const presenceChannel = `rt:presence:updates:${roomName}`
|
|
735
735
|
this.channelManager.addSubscription(presenceChannel, ctx.connection)
|
|
736
736
|
if (!this.channelManager.getSubscribers(presenceChannel) || this.channelManager.getSubscribers(presenceChannel)?.size === 1) {
|
|
737
737
|
await this.channelManager.subscribeToRedisChannel(presenceChannel)
|
|
@@ -747,13 +747,13 @@ export class RealtimeServer {
|
|
|
747
747
|
}
|
|
748
748
|
})
|
|
749
749
|
|
|
750
|
-
this.exposeCommand("
|
|
750
|
+
this.exposeCommand("rt/unsubscribe-presence", async (ctx) => {
|
|
751
751
|
const { roomName } = ctx.payload
|
|
752
|
-
const presenceChannel = `
|
|
752
|
+
const presenceChannel = `rt:presence:updates:${roomName}`
|
|
753
753
|
return this.channelManager.removeSubscription(presenceChannel, ctx.connection)
|
|
754
754
|
})
|
|
755
755
|
|
|
756
|
-
this.exposeCommand("
|
|
756
|
+
this.exposeCommand("rt/publish-presence-state", async (ctx) => {
|
|
757
757
|
const { roomName, state, expireAfter, silent } = ctx.payload
|
|
758
758
|
const connectionId = ctx.connection.id
|
|
759
759
|
if (!state) return false
|
|
@@ -769,7 +769,7 @@ export class RealtimeServer {
|
|
|
769
769
|
}
|
|
770
770
|
})
|
|
771
771
|
|
|
772
|
-
this.exposeCommand("
|
|
772
|
+
this.exposeCommand("rt/clear-presence-state", async (ctx) => {
|
|
773
773
|
const { roomName } = ctx.payload
|
|
774
774
|
const connectionId = ctx.connection.id
|
|
775
775
|
if (!(await this.presenceManager.isRoomTracked(roomName, ctx.connection)) || !(await this.isInRoom(roomName, connectionId))) {
|
|
@@ -784,7 +784,7 @@ export class RealtimeServer {
|
|
|
784
784
|
}
|
|
785
785
|
})
|
|
786
786
|
|
|
787
|
-
this.exposeCommand("
|
|
787
|
+
this.exposeCommand("rt/get-presence-state", async (ctx) => {
|
|
788
788
|
const { roomName } = ctx.payload
|
|
789
789
|
if (!(await this.presenceManager.isRoomTracked(roomName, ctx.connection))) {
|
|
790
790
|
return { success: false, present: [] }
|
|
@@ -801,7 +801,7 @@ export class RealtimeServer {
|
|
|
801
801
|
}
|
|
802
802
|
})
|
|
803
803
|
|
|
804
|
-
this.exposeCommand("
|
|
804
|
+
this.exposeCommand("rt/subscribe-collection", async (ctx) => {
|
|
805
805
|
const { collectionId } = ctx.payload
|
|
806
806
|
const connectionId = ctx.connection.id
|
|
807
807
|
if (!(await this.collectionManager.isCollectionExposed(collectionId, ctx.connection))) {
|
|
@@ -817,7 +817,7 @@ export class RealtimeServer {
|
|
|
817
817
|
}
|
|
818
818
|
})
|
|
819
819
|
|
|
820
|
-
this.exposeCommand("
|
|
820
|
+
this.exposeCommand("rt/unsubscribe-collection", async (ctx) => {
|
|
821
821
|
const { collectionId } = ctx.payload
|
|
822
822
|
return this.collectionManager.removeSubscription(collectionId, ctx.connection.id)
|
|
823
823
|
})
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export const PUB_SUB_CHANNEL_PREFIX = "
|
|
2
|
-
export const RECORD_PUB_SUB_CHANNEL = "
|
|
3
|
-
export const RECORD_KEY_PREFIX = "
|
|
4
|
-
export const RECORD_VERSION_KEY_PREFIX = "
|
|
1
|
+
export const PUB_SUB_CHANNEL_PREFIX = "rt:pubsub:"
|
|
2
|
+
export const RECORD_PUB_SUB_CHANNEL = "rt:record-updates"
|
|
3
|
+
export const RECORD_KEY_PREFIX = "rt:record:"
|
|
4
|
+
export const RECORD_VERSION_KEY_PREFIX = "rt:record-version:"
|
package/src/shared/logger.js
CHANGED