@prsm/realtime 1.0.2 → 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 +1 -1
- package/src/client/client.js +11 -11
- package/src/client/subscriptions/channels.js +3 -3
- package/src/client/subscriptions/collections.js +6 -8
- package/src/client/subscriptions/presence.js +6 -6
- package/src/client/subscriptions/records.js +7 -9
- package/src/client/subscriptions/rooms.js +2 -2
- package/src/server/connection.js +2 -2
- package/src/server/managers/instance.js +6 -6
- package/src/server/managers/persistence.js +17 -17
- package/src/server/managers/presence.js +3 -3
- package/src/server/managers/pubsub.js +1 -1
- package/src/server/managers/records.js +4 -4
- package/src/server/managers/rooms.js +1 -1
- package/src/server/server.js +12 -12
- 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
|
@@ -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()
|
|
@@ -175,7 +175,7 @@ export class RealtimeClient extends EventEmitter {
|
|
|
175
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
|
}
|
|
@@ -190,7 +190,7 @@ export class RealtimeClient extends EventEmitter {
|
|
|
190
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
|
}
|
|
@@ -239,8 +239,8 @@ export class RealtimeClient extends EventEmitter {
|
|
|
239
239
|
if (eventName === "visibilitychange" && doc.visibilityState === "visible") {
|
|
240
240
|
if (this._status === Status.OFFLINE) return
|
|
241
241
|
this.command("rt/noop", {}, 5000)
|
|
242
|
-
.then(() => { clientLogger.info("
|
|
243
|
-
.catch(() => { clientLogger.info("
|
|
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
|
})
|
|
@@ -254,7 +254,7 @@ export class RealtimeClient extends EventEmitter {
|
|
|
254
254
|
const timeSinceActivity = now - this._lastActivityTime
|
|
255
255
|
if (timeSinceActivity > this.options.pingTimeout && this._status === Status.ONLINE) {
|
|
256
256
|
this.command("rt/noop", {}, 5000).catch(() => {
|
|
257
|
-
clientLogger.info(
|
|
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
|
}
|
|
@@ -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
|
}
|
|
@@ -57,7 +57,7 @@ export function createChannelSubscriptions(client) {
|
|
|
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
|
}
|
|
@@ -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
|
}
|
|
@@ -74,7 +72,7 @@ export function createCollectionSubscriptions(client) {
|
|
|
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
|
})
|
|
@@ -23,7 +23,7 @@ export function createPresenceSubscriptions(client) {
|
|
|
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
|
}
|
|
@@ -38,7 +38,7 @@ export function createPresenceSubscriptions(client) {
|
|
|
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
|
}
|
|
@@ -57,7 +57,7 @@ export function createPresenceSubscriptions(client) {
|
|
|
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
|
}
|
|
@@ -70,7 +70,7 @@ export function createPresenceSubscriptions(client) {
|
|
|
70
70
|
try {
|
|
71
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
|
}
|
|
@@ -80,7 +80,7 @@ export function createPresenceSubscriptions(client) {
|
|
|
80
80
|
const handler = subscriptions.get(roomName)
|
|
81
81
|
if (!handler) return false
|
|
82
82
|
const result = await client.command("rt/get-presence-state", { roomName }, 5000).catch((err) => {
|
|
83
|
-
clientLogger.error(
|
|
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)
|
|
@@ -70,7 +68,7 @@ export function createRecordSubscriptions(client) {
|
|
|
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
|
}
|
|
@@ -85,7 +83,7 @@ export function createRecordSubscriptions(client) {
|
|
|
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
|
}
|
|
@@ -101,7 +99,7 @@ export function createRecordSubscriptions(client) {
|
|
|
101
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
|
})
|
|
@@ -44,7 +44,7 @@ export function createRoomSubscriptions(client, presence) {
|
|
|
44
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
|
})
|
|
@@ -61,12 +61,12 @@ export class InstanceManager {
|
|
|
61
61
|
if (instanceId === this.instanceId) continue
|
|
62
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
|
}
|
|
@@ -85,9 +85,9 @@ export class InstanceManager {
|
|
|
85
85
|
}
|
|
86
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
|
|
|
@@ -117,9 +117,9 @@ export class InstanceManager {
|
|
|
117
117
|
pipeline.hdel("rt:connection-meta", connectionId)
|
|
118
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
|
}
|
|
@@ -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
|
|
|
@@ -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,7 +186,7 @@ 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
|
}
|
|
@@ -212,7 +212,7 @@ export class PubSubManager {
|
|
|
212
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
|
}
|
|
@@ -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
|
|
|
@@ -121,7 +121,7 @@ export class RoomManager {
|
|
|
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
|
package/src/server/server.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { createServer as createHttpServer } from "node:http"
|
|
2
2
|
import { randomUUID } from "node:crypto"
|
|
3
3
|
import { WebSocketServer } from "ws"
|
|
4
|
-
import { LogLevel, Status, serverLogger, parseCommand } from "../shared/index.js"
|
|
4
|
+
import { LogLevel, configureLogLevel, Status, serverLogger, parseCommand } from "../shared/index.js"
|
|
5
5
|
import { Connection } from "./connection.js"
|
|
6
6
|
import { PUB_SUB_CHANNEL_PREFIX } from "./utils/constants.js"
|
|
7
7
|
import { ConnectionManager } from "./managers/connections.js"
|
|
@@ -54,7 +54,7 @@ export class RealtimeServer {
|
|
|
54
54
|
enablePresenceExpirationEvents: opts.enablePresenceExpirationEvents ?? true,
|
|
55
55
|
}
|
|
56
56
|
|
|
57
|
-
|
|
57
|
+
configureLogLevel(this.serverOptions.logLevel)
|
|
58
58
|
|
|
59
59
|
this.redisManager = new RedisManager()
|
|
60
60
|
this.redisManager.initialize(opts.redis, (err) => this._emitError(err))
|
|
@@ -159,7 +159,7 @@ export class RealtimeServer {
|
|
|
159
159
|
|
|
160
160
|
enableGracefulShutdown() {
|
|
161
161
|
const handler = () => {
|
|
162
|
-
serverLogger.info("
|
|
162
|
+
serverLogger.info("received shutdown signal, closing")
|
|
163
163
|
this.close().then(() => process.exit(0))
|
|
164
164
|
}
|
|
165
165
|
process.on("SIGTERM", handler)
|
|
@@ -177,7 +177,7 @@ export class RealtimeServer {
|
|
|
177
177
|
}
|
|
178
178
|
|
|
179
179
|
_emitError(err) {
|
|
180
|
-
serverLogger.error(
|
|
180
|
+
serverLogger.error("error", { err })
|
|
181
181
|
for (const handler of this._errorHandlers) handler(err)
|
|
182
182
|
}
|
|
183
183
|
|
|
@@ -702,7 +702,7 @@ export class RealtimeServer {
|
|
|
702
702
|
this.recordSubscriptionManager.addSubscription(recordId, connectionId, mode)
|
|
703
703
|
return { success: true, record, version }
|
|
704
704
|
} catch (e) {
|
|
705
|
-
serverLogger.error(
|
|
705
|
+
serverLogger.error("failed to subscribe to record", { recordId, err: e })
|
|
706
706
|
return { success: false }
|
|
707
707
|
}
|
|
708
708
|
})
|
|
@@ -742,7 +742,7 @@ export class RealtimeServer {
|
|
|
742
742
|
statesMap.forEach((state, connectionId) => { states[connectionId] = state })
|
|
743
743
|
return { success: true, present, states }
|
|
744
744
|
} catch (e) {
|
|
745
|
-
serverLogger.error(
|
|
745
|
+
serverLogger.error("failed to subscribe to presence for room", { roomName, err: e })
|
|
746
746
|
return { success: false, present: [] }
|
|
747
747
|
}
|
|
748
748
|
})
|
|
@@ -764,7 +764,7 @@ export class RealtimeServer {
|
|
|
764
764
|
await this.presenceManager.publishPresenceState(connectionId, roomName, state, expireAfter, silent)
|
|
765
765
|
return true
|
|
766
766
|
} catch (e) {
|
|
767
|
-
serverLogger.error(
|
|
767
|
+
serverLogger.error("failed to publish presence state for room", { roomName, err: e })
|
|
768
768
|
return false
|
|
769
769
|
}
|
|
770
770
|
})
|
|
@@ -779,7 +779,7 @@ export class RealtimeServer {
|
|
|
779
779
|
await this.presenceManager.clearPresenceState(connectionId, roomName)
|
|
780
780
|
return true
|
|
781
781
|
} catch (e) {
|
|
782
|
-
serverLogger.error(
|
|
782
|
+
serverLogger.error("failed to clear presence state for room", { roomName, err: e })
|
|
783
783
|
return false
|
|
784
784
|
}
|
|
785
785
|
})
|
|
@@ -796,7 +796,7 @@ export class RealtimeServer {
|
|
|
796
796
|
statesMap.forEach((state, connectionId) => { states[connectionId] = state })
|
|
797
797
|
return { success: true, present, states }
|
|
798
798
|
} catch (e) {
|
|
799
|
-
serverLogger.error(
|
|
799
|
+
serverLogger.error("failed to get presence state for room", { roomName, err: e })
|
|
800
800
|
return { success: false, present: [] }
|
|
801
801
|
}
|
|
802
802
|
})
|
|
@@ -812,7 +812,7 @@ export class RealtimeServer {
|
|
|
812
812
|
const recordsWithId = records.map((record) => ({ id: record.id, record }))
|
|
813
813
|
return { success: true, ids, records: recordsWithId, version }
|
|
814
814
|
} catch (e) {
|
|
815
|
-
serverLogger.error(
|
|
815
|
+
serverLogger.error("failed to subscribe to collection", { collectionId, err: e })
|
|
816
816
|
return { success: false, ids: [], records: [], version: 0 }
|
|
817
817
|
}
|
|
818
818
|
})
|
|
@@ -824,7 +824,7 @@ export class RealtimeServer {
|
|
|
824
824
|
}
|
|
825
825
|
|
|
826
826
|
async cleanupConnection(connection) {
|
|
827
|
-
serverLogger.info("
|
|
827
|
+
serverLogger.info("cleaning up connection", { connectionId: connection.id })
|
|
828
828
|
connection.stopIntervals()
|
|
829
829
|
try {
|
|
830
830
|
await this.presenceManager.cleanupConnection(connection)
|
|
@@ -858,7 +858,7 @@ export class RealtimeServer {
|
|
|
858
858
|
|
|
859
859
|
if (this.persistenceManager) {
|
|
860
860
|
try { await this.persistenceManager.shutdown() }
|
|
861
|
-
catch (err) { serverLogger.error("
|
|
861
|
+
catch (err) { serverLogger.error("error shutting down persistence manager", { err }) }
|
|
862
862
|
}
|
|
863
863
|
|
|
864
864
|
await this.channelManager.cleanupAllSubscriptions()
|
package/src/shared/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export { CodeError } from "./errors.js"
|
|
2
|
-
export { LogLevel,
|
|
2
|
+
export { LogLevel, configureLogLevel, clientLogger, serverLogger, logger } from "./logger.js"
|
|
3
3
|
export { deepMerge, isObject } from "./merge.js"
|
|
4
4
|
export { parseCommand, stringifyCommand } from "./message.js"
|
|
5
5
|
export { Status } from "./status.js"
|
package/src/shared/logger.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
|
|
1
|
+
import log from "@prsm/log"
|
|
2
|
+
|
|
2
3
|
export const LogLevel = {
|
|
3
4
|
NONE: 0,
|
|
4
5
|
ERROR: 1,
|
|
@@ -7,47 +8,13 @@ export const LogLevel = {
|
|
|
7
8
|
DEBUG: 4,
|
|
8
9
|
}
|
|
9
10
|
|
|
10
|
-
const
|
|
11
|
-
|
|
12
|
-
export class Logger {
|
|
13
|
-
constructor(config) {
|
|
14
|
-
this.config = {
|
|
15
|
-
level: config?.level ?? LogLevel.INFO,
|
|
16
|
-
prefix: config?.prefix ?? "[realtime]",
|
|
17
|
-
styling: config?.styling ?? isBrowser,
|
|
18
|
-
}
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
configure(config) {
|
|
22
|
-
this.config = { ...this.config, ...config }
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
info(...args) {
|
|
26
|
-
if (this.config.level >= LogLevel.INFO) this._log("log", ...args)
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
warn(...args) {
|
|
30
|
-
if (this.config.level >= LogLevel.WARN) this._log("warn", ...args)
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
error(...args) {
|
|
34
|
-
if (this.config.level >= LogLevel.ERROR) this._log("error", ...args)
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
debug(...args) {
|
|
38
|
-
if (this.config.level >= LogLevel.DEBUG) this._log("debug", ...args)
|
|
39
|
-
}
|
|
11
|
+
const levelMap = { 0: "none", 1: "error", 2: "warn", 3: "info", 4: "debug" }
|
|
40
12
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
console[method](`%c${this.config.prefix}%c`, style, "", ...args)
|
|
45
|
-
} else {
|
|
46
|
-
console[method](this.config.prefix, ...args)
|
|
47
|
-
}
|
|
48
|
-
}
|
|
13
|
+
export function configureLogLevel(level) {
|
|
14
|
+
const str = typeof level === "number" ? levelMap[level] ?? "info" : level
|
|
15
|
+
log.configure({ level: str })
|
|
49
16
|
}
|
|
50
17
|
|
|
51
|
-
export const
|
|
52
|
-
export const
|
|
53
|
-
export const logger =
|
|
18
|
+
export const serverLogger = log.child({ sys: "server" })
|
|
19
|
+
export const clientLogger = log.child({ sys: "client" })
|
|
20
|
+
export const logger = log
|