@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
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@prsm/realtime",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.3",
|
|
4
4
|
"description": "Distributed WebSocket framework with Redis-backed rooms, records, presence, channels, collections, and persistence",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "ISC",
|
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
"node": ">=20"
|
|
21
21
|
},
|
|
22
22
|
"dependencies": {
|
|
23
|
+
"@prsm/log": "^1.0.1",
|
|
23
24
|
"eventemitter3": "^5.0.1",
|
|
24
25
|
"fast-json-patch": "^3.1.1",
|
|
25
26
|
"ioredis": "^5.6.1",
|
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
|
}
|
|
@@ -49,7 +49,7 @@ export function createPostgresAdapter(options = {}) {
|
|
|
49
49
|
await createTables()
|
|
50
50
|
initialized = true
|
|
51
51
|
} catch (err) {
|
|
52
|
-
serverLogger.error("
|
|
52
|
+
serverLogger.error("error initializing postgresql database", { err })
|
|
53
53
|
throw err
|
|
54
54
|
}
|
|
55
55
|
},
|
package/src/client/client.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { EventEmitter } from "eventemitter3"
|
|
2
2
|
import { Connection } from "./connection.js"
|
|
3
|
-
import { clientLogger, CodeError, LogLevel, Status } from "../shared/index.js"
|
|
3
|
+
import { clientLogger, configureLogLevel, CodeError, LogLevel, Status } from "../shared/index.js"
|
|
4
4
|
import { createRecordSubscriptions } from "./subscriptions/records.js"
|
|
5
5
|
import { createChannelSubscriptions } from "./subscriptions/channels.js"
|
|
6
6
|
import { createPresenceSubscriptions } from "./subscriptions/presence.js"
|
|
@@ -43,7 +43,7 @@ export class RealtimeClient extends EventEmitter {
|
|
|
43
43
|
logLevel: opts.logLevel ?? LogLevel.ERROR,
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
-
|
|
46
|
+
configureLogLevel(this.options.logLevel)
|
|
47
47
|
|
|
48
48
|
this.recordSubscriptions = new Map()
|
|
49
49
|
this.collectionSubscriptions = new Map()
|
|
@@ -169,13 +169,13 @@ 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
|
-
clientLogger.error(
|
|
178
|
+
clientLogger.error("failed to get metadata for connection", { err: error })
|
|
179
179
|
return null
|
|
180
180
|
}
|
|
181
181
|
}
|
|
@@ -187,10 +187,10 @@ 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
|
-
clientLogger.error(
|
|
193
|
+
clientLogger.error("failed to set metadata for connection", { err: error })
|
|
194
194
|
return false
|
|
195
195
|
}
|
|
196
196
|
}
|
|
@@ -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,9 +238,9 @@ 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("
|
|
242
|
-
.then(() => { clientLogger.info("
|
|
243
|
-
.catch(() => { clientLogger.info("
|
|
241
|
+
this.command("rt/noop", {}, 5000)
|
|
242
|
+
.then(() => { clientLogger.info("tab visible, connection ok"); this.emit("republish") })
|
|
243
|
+
.catch(() => { clientLogger.info("tab visible, forcing reconnect"); this._forceReconnect() })
|
|
244
244
|
}
|
|
245
245
|
})
|
|
246
246
|
})
|
|
@@ -253,8 +253,8 @@ 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("
|
|
257
|
-
clientLogger.info(
|
|
256
|
+
this.command("rt/noop", {}, 5000).catch(() => {
|
|
257
|
+
clientLogger.info("no activity, forcing reconnect", { timeSinceActivity })
|
|
258
258
|
this._forceReconnect()
|
|
259
259
|
})
|
|
260
260
|
}
|
|
@@ -325,7 +325,7 @@ export class RealtimeClient extends EventEmitter {
|
|
|
325
325
|
this.missedPings++
|
|
326
326
|
if (this.missedPings > this.options.maxMissedPings) {
|
|
327
327
|
if (this.options.shouldReconnect) {
|
|
328
|
-
clientLogger.warn(
|
|
328
|
+
clientLogger.warn("missed pings, reconnecting", { missedPings: this.missedPings })
|
|
329
329
|
this.reconnect()
|
|
330
330
|
}
|
|
331
331
|
} else {
|
|
@@ -418,7 +418,7 @@ export class RealtimeClient extends EventEmitter {
|
|
|
418
418
|
}
|
|
419
419
|
|
|
420
420
|
async _resubscribeAll() {
|
|
421
|
-
clientLogger.info("
|
|
421
|
+
clientLogger.info("resubscribing to all subscriptions after reconnect")
|
|
422
422
|
try {
|
|
423
423
|
const successfulRooms = await this._rooms.resubscribe()
|
|
424
424
|
await Promise.allSettled([
|
|
@@ -430,12 +430,12 @@ export class RealtimeClient extends EventEmitter {
|
|
|
430
430
|
if (successfulRooms.length > 0) {
|
|
431
431
|
for (const roomName of successfulRooms) {
|
|
432
432
|
try { await this._presence.forceUpdate(roomName) }
|
|
433
|
-
catch (err) { clientLogger.error(
|
|
433
|
+
catch (err) { clientLogger.error("error refreshing presence for room", { roomName, err }) }
|
|
434
434
|
await new Promise((resolve) => setTimeout(resolve, 50))
|
|
435
435
|
}
|
|
436
436
|
}
|
|
437
437
|
} catch (error) {
|
|
438
|
-
clientLogger.error("
|
|
438
|
+
clientLogger.error("error during resubscription", { err: error })
|
|
439
439
|
}
|
|
440
440
|
}
|
|
441
441
|
}
|
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") {
|
|
@@ -10,7 +10,7 @@ export function createChannelSubscriptions(client) {
|
|
|
10
10
|
try {
|
|
11
11
|
await subscription.callback(message)
|
|
12
12
|
} catch (error) {
|
|
13
|
-
clientLogger.error(
|
|
13
|
+
clientLogger.error("error in channel callback", { channel, err: error })
|
|
14
14
|
}
|
|
15
15
|
}
|
|
16
16
|
}
|
|
@@ -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,14 +50,14 @@ 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,
|
|
57
57
|
})
|
|
58
58
|
return { success: result.success, history: result.history || [] }
|
|
59
59
|
} catch (error) {
|
|
60
|
-
clientLogger.error(
|
|
60
|
+
clientLogger.error("failed to get history for channel", { channel, err: error })
|
|
61
61
|
return { success: false, history: [] }
|
|
62
62
|
}
|
|
63
63
|
}
|
|
@@ -68,7 +68,7 @@ export function createChannelSubscriptions(client) {
|
|
|
68
68
|
await subscribe(channel, callback, { historyLimit })
|
|
69
69
|
return true
|
|
70
70
|
} catch (error) {
|
|
71
|
-
clientLogger.error(
|
|
71
|
+
clientLogger.error("failed to resubscribe to channel", { channel, err: error })
|
|
72
72
|
return false
|
|
73
73
|
}
|
|
74
74
|
})
|
|
@@ -9,9 +9,7 @@ export function createCollectionSubscriptions(client) {
|
|
|
9
9
|
if (!subscription) return
|
|
10
10
|
|
|
11
11
|
if (version !== subscription.version + 1) {
|
|
12
|
-
clientLogger.warn(
|
|
13
|
-
`Desync detected for collection ${collectionId}. Expected version ${subscription.version + 1}, got ${version}. Resubscribing.`
|
|
14
|
-
)
|
|
12
|
+
clientLogger.warn("desync detected for collection, resubscribing", { collectionId, expected: subscription.version + 1, got: version })
|
|
15
13
|
await unsubscribe(collectionId)
|
|
16
14
|
await subscribe(collectionId, { onDiff: subscription.onDiff })
|
|
17
15
|
return
|
|
@@ -30,7 +28,7 @@ export function createCollectionSubscriptions(client) {
|
|
|
30
28
|
version,
|
|
31
29
|
})
|
|
32
30
|
} catch (error) {
|
|
33
|
-
clientLogger.error(
|
|
31
|
+
clientLogger.error("error in collection diff callback", { collectionId, err: error })
|
|
34
32
|
}
|
|
35
33
|
}
|
|
36
34
|
}
|
|
@@ -42,7 +40,7 @@ export function createCollectionSubscriptions(client) {
|
|
|
42
40
|
*/
|
|
43
41
|
async function subscribe(collectionId, options = {}) {
|
|
44
42
|
try {
|
|
45
|
-
const result = await client.command("
|
|
43
|
+
const result = await client.command("rt/subscribe-collection", { collectionId })
|
|
46
44
|
if (result.success) {
|
|
47
45
|
subscriptions.set(collectionId, {
|
|
48
46
|
ids: new Set(result.ids),
|
|
@@ -53,13 +51,13 @@ export function createCollectionSubscriptions(client) {
|
|
|
53
51
|
try {
|
|
54
52
|
await options.onDiff({ added: result.records, removed: [], changed: [], version: result.version })
|
|
55
53
|
} catch (error) {
|
|
56
|
-
clientLogger.error(
|
|
54
|
+
clientLogger.error("error in initial collection diff callback", { collectionId, err: error })
|
|
57
55
|
}
|
|
58
56
|
}
|
|
59
57
|
}
|
|
60
58
|
return { success: result.success, ids: result.ids || [], records: result.records || [], version: result.version || 0 }
|
|
61
59
|
} catch (error) {
|
|
62
|
-
clientLogger.error(
|
|
60
|
+
clientLogger.error("failed to subscribe to collection", { collectionId, err: error })
|
|
63
61
|
return { success: false, ids: [], records: [], version: 0 }
|
|
64
62
|
}
|
|
65
63
|
}
|
|
@@ -70,11 +68,11 @@ export function createCollectionSubscriptions(client) {
|
|
|
70
68
|
*/
|
|
71
69
|
async function unsubscribe(collectionId) {
|
|
72
70
|
try {
|
|
73
|
-
const success = await client.command("
|
|
71
|
+
const success = await client.command("rt/unsubscribe-collection", { collectionId })
|
|
74
72
|
if (success) subscriptions.delete(collectionId)
|
|
75
73
|
return success
|
|
76
74
|
} catch (error) {
|
|
77
|
-
clientLogger.error(
|
|
75
|
+
clientLogger.error("failed to unsubscribe from collection", { collectionId, err: error })
|
|
78
76
|
return false
|
|
79
77
|
}
|
|
80
78
|
}
|
|
@@ -85,7 +83,7 @@ export function createCollectionSubscriptions(client) {
|
|
|
85
83
|
await subscribe(collectionId, { onDiff: subscription.onDiff })
|
|
86
84
|
return true
|
|
87
85
|
} catch (error) {
|
|
88
|
-
clientLogger.error(
|
|
86
|
+
clientLogger.error("failed to resubscribe to collection", { collectionId, err: error })
|
|
89
87
|
return false
|
|
90
88
|
}
|
|
91
89
|
})
|
|
@@ -16,14 +16,14 @@ 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)
|
|
23
23
|
}
|
|
24
24
|
return { success: result.success, present: result.present || [], states: result.states || {} }
|
|
25
25
|
} catch (error) {
|
|
26
|
-
clientLogger.error(
|
|
26
|
+
clientLogger.error("failed to subscribe to presence for room", { roomName, err: error })
|
|
27
27
|
return { success: false, present: [] }
|
|
28
28
|
}
|
|
29
29
|
}
|
|
@@ -34,11 +34,11 @@ 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) {
|
|
41
|
-
clientLogger.error(
|
|
41
|
+
clientLogger.error("failed to unsubscribe from presence for room", { roomName, err: error })
|
|
42
42
|
return false
|
|
43
43
|
}
|
|
44
44
|
}
|
|
@@ -50,14 +50,14 @@ 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,
|
|
57
57
|
silent: options.silent,
|
|
58
58
|
})
|
|
59
59
|
} catch (error) {
|
|
60
|
-
clientLogger.error(
|
|
60
|
+
clientLogger.error("failed to publish presence state for room", { roomName, err: error })
|
|
61
61
|
return false
|
|
62
62
|
}
|
|
63
63
|
}
|
|
@@ -68,9 +68,9 @@ 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
|
-
clientLogger.error(
|
|
73
|
+
clientLogger.error("failed to clear presence state for room", { roomName, err: error })
|
|
74
74
|
return false
|
|
75
75
|
}
|
|
76
76
|
}
|
|
@@ -79,8 +79,8 @@ 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("
|
|
83
|
-
clientLogger.error(
|
|
82
|
+
const result = await client.command("rt/get-presence-state", { roomName }, 5000).catch((err) => {
|
|
83
|
+
clientLogger.error("failed to get presence state for room", { roomName, err })
|
|
84
84
|
return { success: false }
|
|
85
85
|
})
|
|
86
86
|
if (!result.success) return false
|
|
@@ -89,7 +89,7 @@ export function createPresenceSubscriptions(client) {
|
|
|
89
89
|
}
|
|
90
90
|
return true
|
|
91
91
|
} catch (error) {
|
|
92
|
-
clientLogger.error(
|
|
92
|
+
clientLogger.error("failed to force presence update for room", { roomName, err: error })
|
|
93
93
|
return false
|
|
94
94
|
}
|
|
95
95
|
}
|
|
@@ -16,7 +16,7 @@ export function createRecordSubscriptions(client) {
|
|
|
16
16
|
version,
|
|
17
17
|
})
|
|
18
18
|
} catch (error) {
|
|
19
|
-
clientLogger.error(
|
|
19
|
+
clientLogger.error("error in collection record update callback", { collectionId, err: error })
|
|
20
20
|
}
|
|
21
21
|
}
|
|
22
22
|
}
|
|
@@ -26,9 +26,7 @@ export function createRecordSubscriptions(client) {
|
|
|
26
26
|
|
|
27
27
|
if (patch) {
|
|
28
28
|
if (version !== subscription.localVersion + 1) {
|
|
29
|
-
clientLogger.warn(
|
|
30
|
-
`Desync detected for record ${recordId}. Expected version ${subscription.localVersion + 1}, got ${version}. Resubscribing to request full record.`
|
|
31
|
-
)
|
|
29
|
+
clientLogger.warn("desync detected for record, resubscribing", { recordId, expected: subscription.localVersion + 1, got: version })
|
|
32
30
|
await unsubscribe(recordId)
|
|
33
31
|
await subscribe(recordId, subscription.callback, { mode: subscription.mode })
|
|
34
32
|
return
|
|
@@ -48,7 +46,7 @@ export function createRecordSubscriptions(client) {
|
|
|
48
46
|
try {
|
|
49
47
|
await subscription.callback({ recordId, deleted: true, version })
|
|
50
48
|
} catch (error) {
|
|
51
|
-
clientLogger.error(
|
|
49
|
+
clientLogger.error("error in record deletion callback", { recordId, err: error })
|
|
52
50
|
}
|
|
53
51
|
}
|
|
54
52
|
subscriptions.delete(recordId)
|
|
@@ -63,14 +61,14 @@ export function createRecordSubscriptions(client) {
|
|
|
63
61
|
async function subscribe(recordId, callback, options) {
|
|
64
62
|
const mode = options?.mode ?? "full"
|
|
65
63
|
try {
|
|
66
|
-
const result = await client.command("
|
|
64
|
+
const result = await client.command("rt/subscribe-record", { recordId, mode })
|
|
67
65
|
if (result.success) {
|
|
68
66
|
subscriptions.set(recordId, { callback, localVersion: result.version, mode })
|
|
69
67
|
if (callback) await callback({ recordId, full: result.record, version: result.version })
|
|
70
68
|
}
|
|
71
69
|
return { success: result.success, record: result.record ?? null, version: result.version ?? 0 }
|
|
72
70
|
} catch (error) {
|
|
73
|
-
clientLogger.error(
|
|
71
|
+
clientLogger.error("failed to subscribe to record", { recordId, err: error })
|
|
74
72
|
return { success: false, record: null, version: 0 }
|
|
75
73
|
}
|
|
76
74
|
}
|
|
@@ -81,11 +79,11 @@ export function createRecordSubscriptions(client) {
|
|
|
81
79
|
*/
|
|
82
80
|
async function unsubscribe(recordId) {
|
|
83
81
|
try {
|
|
84
|
-
const success = await client.command("
|
|
82
|
+
const success = await client.command("rt/unsubscribe-record", { recordId })
|
|
85
83
|
if (success) subscriptions.delete(recordId)
|
|
86
84
|
return success
|
|
87
85
|
} catch (error) {
|
|
88
|
-
clientLogger.error(
|
|
86
|
+
clientLogger.error("failed to unsubscribe from record", { recordId, err: error })
|
|
89
87
|
return false
|
|
90
88
|
}
|
|
91
89
|
}
|
|
@@ -98,10 +96,10 @@ export function createRecordSubscriptions(client) {
|
|
|
98
96
|
*/
|
|
99
97
|
async function write(recordId, newValue, options) {
|
|
100
98
|
try {
|
|
101
|
-
const result = await client.command("
|
|
99
|
+
const result = await client.command("rt/publish-record-update", { recordId, newValue, options })
|
|
102
100
|
return result.success === true
|
|
103
101
|
} catch (error) {
|
|
104
|
-
clientLogger.error(
|
|
102
|
+
clientLogger.error("failed to publish update for record", { recordId, err: error })
|
|
105
103
|
return false
|
|
106
104
|
}
|
|
107
105
|
}
|
|
@@ -112,7 +110,7 @@ export function createRecordSubscriptions(client) {
|
|
|
112
110
|
await subscribe(recordId, callback, { mode })
|
|
113
111
|
return true
|
|
114
112
|
} catch (error) {
|
|
115
|
-
clientLogger.error(
|
|
113
|
+
clientLogger.error("failed to resubscribe to record", { recordId, err: error })
|
|
116
114
|
return false
|
|
117
115
|
}
|
|
118
116
|
})
|
|
@@ -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,10 +41,10 @@ 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
|
-
clientLogger.error(
|
|
47
|
+
clientLogger.error("failed to get metadata for room", { roomName, err: error })
|
|
48
48
|
return null
|
|
49
49
|
}
|
|
50
50
|
}
|
|
@@ -55,7 +55,7 @@ export function createRoomSubscriptions(client, presence) {
|
|
|
55
55
|
await join(roomName, presenceCallback)
|
|
56
56
|
return { roomName, success: true }
|
|
57
57
|
} catch (error) {
|
|
58
|
-
clientLogger.error(
|
|
58
|
+
clientLogger.error("failed to rejoin room", { roomName, err: error })
|
|
59
59
|
return { roomName, success: false }
|
|
60
60
|
}
|
|
61
61
|
})
|
package/src/server/connection.js
CHANGED
|
@@ -39,7 +39,7 @@ export class Connection extends EventEmitter {
|
|
|
39
39
|
this.missedPongs++
|
|
40
40
|
const maxMissedPongs = this.connectionOptions.maxMissedPongs ?? 1
|
|
41
41
|
if (this.missedPongs > maxMissedPongs) {
|
|
42
|
-
serverLogger.info(
|
|
42
|
+
serverLogger.info("closing connection due to missed pongs", { connectionId: this.id })
|
|
43
43
|
this.close()
|
|
44
44
|
this.server.cleanupConnection(this)
|
|
45
45
|
return
|
|
@@ -59,7 +59,7 @@ export class Connection extends EventEmitter {
|
|
|
59
59
|
|
|
60
60
|
_applyListeners() {
|
|
61
61
|
this.socket.on("close", () => {
|
|
62
|
-
serverLogger.info("
|
|
62
|
+
serverLogger.info("client socket closed", { connectionId: this.id })
|
|
63
63
|
this.status = Status.OFFLINE
|
|
64
64
|
this.emit("close")
|
|
65
65
|
})
|
|
@@ -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 }) {
|