@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@prsm/realtime",
3
- "version": "1.0.2",
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",
@@ -49,7 +49,7 @@ export function createPostgresAdapter(options = {}) {
49
49
  await createTables()
50
50
  initialized = true
51
51
  } catch (err) {
52
- serverLogger.error("Error initializing PostgreSQL database:", err)
52
+ serverLogger.error("error initializing postgresql database", { err })
53
53
  throw err
54
54
  }
55
55
  },
@@ -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
- clientLogger.configure({ level: this.options.logLevel, styling: true })
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(`Failed to get metadata for connection:`, 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(`Failed to set metadata for connection:`, 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("Tab visible, connection ok"); this.emit("republish") })
243
- .catch(() => { clientLogger.info("Tab visible, forcing reconnect"); this._forceReconnect() })
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(`No activity for ${timeSinceActivity}ms, forcing reconnect`)
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(`Missed ${this.missedPings} pings, reconnecting...`)
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("Resubscribing to all subscriptions after reconnect")
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(`Error refreshing presence for room ${roomName}:`, err) }
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("Error during resubscription:", 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(`Error in channel callback for ${channel}:`, 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(`Failed to get history for channel ${channel}:`, 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(`Failed to resubscribe to channel ${channel}:`, 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(`Error in collection diff callback for ${collectionId}:`, 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(`Error in initial collection diff callback for ${collectionId}:`, 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(`Failed to subscribe to collection ${collectionId}:`, 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(`Failed to unsubscribe from collection ${collectionId}:`, 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(`Failed to resubscribe to collection ${collectionId}:`, 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(`Failed to subscribe to presence for room ${roomName}:`, 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(`Failed to unsubscribe from presence for room ${roomName}:`, 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(`Failed to publish presence state for room ${roomName}:`, 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(`Failed to clear presence state for room ${roomName}:`, 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(`Failed to get presence state for room ${roomName}:`, 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(`Failed to force presence update for room ${roomName}:`, 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(`Error in collection record update callback for ${collectionId}:`, 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(`Error in record deletion callback for ${recordId}:`, 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(`Failed to subscribe to record ${recordId}:`, 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(`Failed to unsubscribe from record ${recordId}:`, 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(`Failed to publish update for record ${recordId}:`, 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(`Failed to resubscribe to record ${recordId}:`, 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(`Failed to get metadata for room ${roomName}:`, 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(`Failed to rejoin room ${roomName}:`, error)
58
+ clientLogger.error("failed to rejoin room", { roomName, err: error })
59
59
  return { roomName, success: false }
60
60
  }
61
61
  })
@@ -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(`Closing connection (${this.id}) due to missed pongs`)
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("Client's socket closed:", this.id)
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(`Found dead instance: ${instanceId}`)
64
+ serverLogger.info("found dead instance", { instanceId })
65
65
  await this._cleanupDeadInstance(instanceId)
66
66
  }
67
67
  }
68
68
  } catch (error) {
69
- serverLogger.error("Error during cleanup:", 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(`Cleaned up dead instance: ${instanceId}`)
88
+ serverLogger.info("cleaned up dead instance", { instanceId })
89
89
  } catch (error) {
90
- serverLogger.error(`Error cleaning up instance ${instanceId}:`, 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(`Cleaned up stale connection: ${connectionId}`)
120
+ serverLogger.debug("cleaned up stale connection", { connectionId })
121
121
  } catch (error) {
122
- serverLogger.error(`Error cleaning up connection ${connectionId}:`, 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(`Processing ${this.pendingRecordUpdates.length} pending record updates`)
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("Failed to initialize persistence manager:", err)
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("Cannot restore persisted records: record manager not available")
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("Cannot restore records: Redis not available")
69
+ serverLogger.warn("cannot restore records: redis not available")
70
70
  return
71
71
  }
72
72
  try {
73
- serverLogger.info("Restoring persisted records...")
73
+ serverLogger.info("restoring persisted records")
74
74
  if (this.recordPatterns.length === 0) {
75
- serverLogger.info("No record patterns to restore")
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(`Restoring ${records.length} records for pattern ${patternLabel}`)
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(`Failed to restore record ${record.recordId}: ${parseErr}`)
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(`Error restoring records for pattern ${patternLabel}: ${patternErr}`)
112
+ serverLogger.error("error restoring records for pattern", { pattern: patternLabel, err: patternErr })
113
113
  }
114
114
  }
115
- serverLogger.info("Finished restoring persisted records")
115
+ serverLogger.info("finished restoring persisted records")
116
116
  } catch (err) {
117
- serverLogger.error("Failed to restore persisted records:", err)
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(`Failed to initialize adapter for pattern ${pattern}:`, err)
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(`Failed to initialize adapter for record pattern ${pattern}:`, err)
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(`Failed to flush messages for channel ${channel}:`, err)
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("Failed to flush records:", err)
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(`Failed to get persisted records for pattern ${pattern}:`, err)
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("Error closing persistence adapter:", err) }
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("[PresenceManager] Failed to handle expired key:", err)
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(`[PresenceManager] Failed to parse presence state: ${e}`); return null }
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(`[PresenceManager] Failed to parse presence state: ${e}`) }
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(`Record ${recordId} not found during collection update (likely deleted).`)
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(`Error in record update callback for ${recordId}:`, error) }
68
+ catch (error) { serverLogger.error("error in record update callback", { recordId, err: error }) }
69
69
  })
70
70
  ).catch((error) => {
71
- serverLogger.error(`Error in record update callbacks for ${recordId}:`, 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(`Error in record removed callback for ${recordId}:`, error) }
91
+ catch (error) { serverLogger.error("error in record removed callback", { recordId, err: error }) }
92
92
  })
93
93
  ).catch((error) => {
94
- serverLogger.error(`Error in record removed callbacks for ${recordId}:`, 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(`Failed to parse metadata for room ${roomName}:`, e) }
124
+ catch (e) { serverLogger.error("failed to parse metadata for room", { roomName, err: e }) }
125
125
  }
126
126
  })
127
127
  return result
@@ -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
- serverLogger.configure({ level: this.serverOptions.logLevel, styling: false })
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("Received shutdown signal, closing...")
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(`Error: ${err}`)
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(`Failed to subscribe to record ${recordId}:`, e)
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(`Failed to subscribe to presence for room ${roomName}:`, e)
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(`Failed to publish presence state for room ${roomName}:`, e)
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(`Failed to clear presence state for room ${roomName}:`, e)
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(`Failed to get presence state for room ${roomName}:`, e)
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(`Failed to subscribe to collection ${collectionId}:`, e)
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("Cleaning up connection:", connection.id)
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("Error shutting down persistence manager:", err) }
861
+ catch (err) { serverLogger.error("error shutting down persistence manager", { err }) }
862
862
  }
863
863
 
864
864
  await this.channelManager.cleanupAllSubscriptions()
@@ -1,5 +1,5 @@
1
1
  export { CodeError } from "./errors.js"
2
- export { LogLevel, Logger, clientLogger, serverLogger, logger } from "./logger.js"
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"
@@ -1,4 +1,5 @@
1
- /** @enum {number} */
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 isBrowser = typeof window !== "undefined" && typeof window.document !== "undefined"
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
- _log(method, ...args) {
42
- if (this.config.styling && isBrowser) {
43
- const style = "background: #000; color: #FFA07A; padding: 2px 4px; border-radius: 2px;"
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 clientLogger = new Logger({ level: LogLevel.ERROR, styling: true })
52
- export const serverLogger = new Logger({ level: LogLevel.ERROR, styling: false })
53
- export const logger = isBrowser ? clientLogger : serverLogger
18
+ export const serverLogger = log.child({ sys: "server" })
19
+ export const clientLogger = log.child({ sys: "client" })
20
+ export const logger = log