@prsm/realtime 1.0.0

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.
Files changed (60) hide show
  1. package/package.json +49 -0
  2. package/src/adapters/postgres.js +139 -0
  3. package/src/adapters/sqlite.js +177 -0
  4. package/src/client/client.js +441 -0
  5. package/src/client/connection.js +178 -0
  6. package/src/client/ids.js +26 -0
  7. package/src/client/index.js +5 -0
  8. package/src/client/queue.js +44 -0
  9. package/src/client/subscriptions/channels.js +79 -0
  10. package/src/client/subscriptions/collections.js +96 -0
  11. package/src/client/subscriptions/presence.js +98 -0
  12. package/src/client/subscriptions/records.js +123 -0
  13. package/src/client/subscriptions/rooms.js +69 -0
  14. package/src/devtools/client/dist/assets/index-CGm1NqOQ.css +1 -0
  15. package/src/devtools/client/dist/assets/index-w2FI7RvC.js +168 -0
  16. package/src/devtools/client/dist/index.html +16 -0
  17. package/src/devtools/client/index.html +15 -0
  18. package/src/devtools/client/package.json +17 -0
  19. package/src/devtools/client/src/App.vue +173 -0
  20. package/src/devtools/client/src/components/ConnectionPicker.vue +38 -0
  21. package/src/devtools/client/src/components/JsonView.vue +18 -0
  22. package/src/devtools/client/src/composables/useApi.js +71 -0
  23. package/src/devtools/client/src/composables/useHighlight.js +57 -0
  24. package/src/devtools/client/src/main.js +5 -0
  25. package/src/devtools/client/src/style.css +440 -0
  26. package/src/devtools/client/src/views/ChannelsView.vue +27 -0
  27. package/src/devtools/client/src/views/CollectionsView.vue +61 -0
  28. package/src/devtools/client/src/views/MetadataView.vue +108 -0
  29. package/src/devtools/client/src/views/RecordsView.vue +30 -0
  30. package/src/devtools/client/src/views/RoomsView.vue +39 -0
  31. package/src/devtools/client/vite.config.js +17 -0
  32. package/src/devtools/demo/server.js +144 -0
  33. package/src/devtools/index.js +186 -0
  34. package/src/index.js +9 -0
  35. package/src/server/connection.js +116 -0
  36. package/src/server/context.js +22 -0
  37. package/src/server/managers/broadcast.js +94 -0
  38. package/src/server/managers/channels.js +118 -0
  39. package/src/server/managers/collections.js +127 -0
  40. package/src/server/managers/commands.js +55 -0
  41. package/src/server/managers/connections.js +111 -0
  42. package/src/server/managers/instance.js +125 -0
  43. package/src/server/managers/persistence.js +371 -0
  44. package/src/server/managers/presence.js +217 -0
  45. package/src/server/managers/pubsub.js +242 -0
  46. package/src/server/managers/record-subscriptions.js +123 -0
  47. package/src/server/managers/records.js +110 -0
  48. package/src/server/managers/redis.js +61 -0
  49. package/src/server/managers/rooms.js +129 -0
  50. package/src/server/message-stream.js +20 -0
  51. package/src/server/server.js +878 -0
  52. package/src/server/utils/constants.js +4 -0
  53. package/src/server/utils/ids.js +5 -0
  54. package/src/server/utils/pattern-conversion.js +14 -0
  55. package/src/shared/errors.js +7 -0
  56. package/src/shared/index.js +5 -0
  57. package/src/shared/logger.js +53 -0
  58. package/src/shared/merge.js +17 -0
  59. package/src/shared/message.js +17 -0
  60. package/src/shared/status.js +7 -0
@@ -0,0 +1,79 @@
1
+ import { clientLogger } from "../../shared/index.js"
2
+
3
+ export function createChannelSubscriptions(client) {
4
+ const subscriptions = client.channelSubscriptions
5
+
6
+ async function handleMessage(payload) {
7
+ const { channel, message } = payload
8
+ const subscription = subscriptions.get(channel)
9
+ if (subscription) {
10
+ try {
11
+ await subscription.callback(message)
12
+ } catch (error) {
13
+ clientLogger.error(`Error in channel callback for ${channel}:`, error)
14
+ }
15
+ }
16
+ }
17
+
18
+ /**
19
+ * @param {string} channel
20
+ * @param {(message: any) => void} callback
21
+ * @param {{historyLimit?: number, since?: string}} [options]
22
+ * @returns {Promise<{success: boolean, history: any[]}>}
23
+ */
24
+ async function subscribe(channel, callback, options) {
25
+ subscriptions.set(channel, { callback, historyLimit: options?.historyLimit })
26
+ const result = await client.command("mesh/subscribe-channel", {
27
+ channel,
28
+ historyLimit: options?.historyLimit,
29
+ since: options?.since,
30
+ })
31
+ if (result.success && result.history && result.history.length > 0) {
32
+ result.history.forEach((message) => callback(message))
33
+ }
34
+ return { success: result.success, history: result.history || [] }
35
+ }
36
+
37
+ /**
38
+ * @param {string} channel
39
+ * @returns {Promise<any>}
40
+ */
41
+ function unsubscribe(channel) {
42
+ subscriptions.delete(channel)
43
+ return client.command("mesh/unsubscribe-channel", { channel })
44
+ }
45
+
46
+ /**
47
+ * @param {string} channel
48
+ * @param {{limit?: number, since?: string}} [options]
49
+ * @returns {Promise<{success: boolean, history: any[]}>}
50
+ */
51
+ async function getHistory(channel, options) {
52
+ try {
53
+ const result = await client.command("mesh/get-channel-history", {
54
+ channel,
55
+ limit: options?.limit,
56
+ since: options?.since,
57
+ })
58
+ return { success: result.success, history: result.history || [] }
59
+ } catch (error) {
60
+ clientLogger.error(`Failed to get history for channel ${channel}:`, error)
61
+ return { success: false, history: [] }
62
+ }
63
+ }
64
+
65
+ async function resubscribe() {
66
+ const promises = Array.from(subscriptions.entries()).map(async ([channel, { callback, historyLimit }]) => {
67
+ try {
68
+ await subscribe(channel, callback, { historyLimit })
69
+ return true
70
+ } catch (error) {
71
+ clientLogger.error(`Failed to resubscribe to channel ${channel}:`, error)
72
+ return false
73
+ }
74
+ })
75
+ return Promise.allSettled(promises)
76
+ }
77
+
78
+ return { handleMessage, subscribe, unsubscribe, getHistory, resubscribe }
79
+ }
@@ -0,0 +1,96 @@
1
+ import { clientLogger } from "../../shared/index.js"
2
+
3
+ export function createCollectionSubscriptions(client) {
4
+ const subscriptions = client.collectionSubscriptions
5
+
6
+ async function handleDiff(payload) {
7
+ const { collectionId, added, removed, version } = payload
8
+ const subscription = subscriptions.get(collectionId)
9
+ if (!subscription) return
10
+
11
+ if (version !== subscription.version + 1) {
12
+ clientLogger.warn(
13
+ `Desync detected for collection ${collectionId}. Expected version ${subscription.version + 1}, got ${version}. Resubscribing.`
14
+ )
15
+ await unsubscribe(collectionId)
16
+ await subscribe(collectionId, { onDiff: subscription.onDiff })
17
+ return
18
+ }
19
+
20
+ subscription.version = version
21
+ for (const record of added) subscription.ids.add(record.id)
22
+ for (const record of removed) subscription.ids.delete(record.id)
23
+
24
+ if (subscription.onDiff) {
25
+ try {
26
+ await subscription.onDiff({
27
+ added: added.map((record) => ({ id: record.id, record })),
28
+ removed: removed.map((record) => ({ id: record.id, record })),
29
+ changed: [],
30
+ version,
31
+ })
32
+ } catch (error) {
33
+ clientLogger.error(`Error in collection diff callback for ${collectionId}:`, error)
34
+ }
35
+ }
36
+ }
37
+
38
+ /**
39
+ * @param {string} collectionId
40
+ * @param {{onDiff?: (diff: {added: Array<{id: string, record: any}>, removed: Array<{id: string, record: any}>, changed: Array<{id: string, record: any}>, version: number}) => void}} [options]
41
+ * @returns {Promise<{success: boolean, ids: string[], records: any[], version: number}>}
42
+ */
43
+ async function subscribe(collectionId, options = {}) {
44
+ try {
45
+ const result = await client.command("mesh/subscribe-collection", { collectionId })
46
+ if (result.success) {
47
+ subscriptions.set(collectionId, {
48
+ ids: new Set(result.ids),
49
+ version: result.version,
50
+ onDiff: options.onDiff,
51
+ })
52
+ if (options.onDiff) {
53
+ try {
54
+ await options.onDiff({ added: result.records, removed: [], changed: [], version: result.version })
55
+ } catch (error) {
56
+ clientLogger.error(`Error in initial collection diff callback for ${collectionId}:`, error)
57
+ }
58
+ }
59
+ }
60
+ return { success: result.success, ids: result.ids || [], records: result.records || [], version: result.version || 0 }
61
+ } catch (error) {
62
+ clientLogger.error(`Failed to subscribe to collection ${collectionId}:`, error)
63
+ return { success: false, ids: [], records: [], version: 0 }
64
+ }
65
+ }
66
+
67
+ /**
68
+ * @param {string} collectionId
69
+ * @returns {Promise<boolean>}
70
+ */
71
+ async function unsubscribe(collectionId) {
72
+ try {
73
+ const success = await client.command("mesh/unsubscribe-collection", { collectionId })
74
+ if (success) subscriptions.delete(collectionId)
75
+ return success
76
+ } catch (error) {
77
+ clientLogger.error(`Failed to unsubscribe from collection ${collectionId}:`, error)
78
+ return false
79
+ }
80
+ }
81
+
82
+ async function resubscribe() {
83
+ const promises = Array.from(subscriptions.entries()).map(async ([collectionId, subscription]) => {
84
+ try {
85
+ await subscribe(collectionId, { onDiff: subscription.onDiff })
86
+ return true
87
+ } catch (error) {
88
+ clientLogger.error(`Failed to resubscribe to collection ${collectionId}:`, error)
89
+ return false
90
+ }
91
+ })
92
+ return Promise.allSettled(promises)
93
+ }
94
+
95
+ return { handleDiff, subscribe, unsubscribe, resubscribe }
96
+ }
@@ -0,0 +1,98 @@
1
+ import { clientLogger } from "../../shared/index.js"
2
+
3
+ export function createPresenceSubscriptions(client) {
4
+ const subscriptions = client.presenceSubscriptions
5
+
6
+ async function handleUpdate(payload) {
7
+ const { roomName } = payload
8
+ const callback = subscriptions.get(roomName)
9
+ if (callback) await callback(payload)
10
+ }
11
+
12
+ /**
13
+ * @param {string} roomName
14
+ * @param {(update: {roomName: string, present: string[], states: Object<string, any>, joined?: string, left?: string}) => void} callback
15
+ * @returns {Promise<{success: boolean, present: string[], states?: Object<string, any>}>}
16
+ */
17
+ async function subscribe(roomName, callback) {
18
+ try {
19
+ const result = await client.command("mesh/subscribe-presence", { roomName })
20
+ if (result.success) {
21
+ subscriptions.set(roomName, callback)
22
+ if (result.present && result.present.length > 0) await callback(result)
23
+ }
24
+ return { success: result.success, present: result.present || [], states: result.states || {} }
25
+ } catch (error) {
26
+ clientLogger.error(`Failed to subscribe to presence for room ${roomName}:`, error)
27
+ return { success: false, present: [] }
28
+ }
29
+ }
30
+
31
+ /**
32
+ * @param {string} roomName
33
+ * @returns {Promise<boolean>}
34
+ */
35
+ async function unsubscribe(roomName) {
36
+ try {
37
+ const success = await client.command("mesh/unsubscribe-presence", { roomName })
38
+ if (success) subscriptions.delete(roomName)
39
+ return success
40
+ } catch (error) {
41
+ clientLogger.error(`Failed to unsubscribe from presence for room ${roomName}:`, error)
42
+ return false
43
+ }
44
+ }
45
+
46
+ /**
47
+ * @param {string} roomName
48
+ * @param {{state: any, expireAfter?: number, silent?: boolean}} options
49
+ * @returns {Promise<any>}
50
+ */
51
+ async function publishState(roomName, options) {
52
+ try {
53
+ return await client.command("mesh/publish-presence-state", {
54
+ roomName,
55
+ state: options.state,
56
+ expireAfter: options.expireAfter,
57
+ silent: options.silent,
58
+ })
59
+ } catch (error) {
60
+ clientLogger.error(`Failed to publish presence state for room ${roomName}:`, error)
61
+ return false
62
+ }
63
+ }
64
+
65
+ /**
66
+ * @param {string} roomName
67
+ * @returns {Promise<any>}
68
+ */
69
+ async function clearState(roomName) {
70
+ try {
71
+ return await client.command("mesh/clear-presence-state", { roomName })
72
+ } catch (error) {
73
+ clientLogger.error(`Failed to clear presence state for room ${roomName}:`, error)
74
+ return false
75
+ }
76
+ }
77
+
78
+ async function forceUpdate(roomName) {
79
+ try {
80
+ const handler = subscriptions.get(roomName)
81
+ if (!handler) return false
82
+ const result = await client.command("mesh/get-presence-state", { roomName }, 5000).catch((err) => {
83
+ clientLogger.error(`Failed to get presence state for room ${roomName}:`, err)
84
+ return { success: false }
85
+ })
86
+ if (!result.success) return false
87
+ if (handler.init && typeof handler.init === "function") {
88
+ handler.init(result.present, result.states || {})
89
+ }
90
+ return true
91
+ } catch (error) {
92
+ clientLogger.error(`Failed to force presence update for room ${roomName}:`, error)
93
+ return false
94
+ }
95
+ }
96
+
97
+ return { handleUpdate, subscribe, unsubscribe, publishState, clearState, forceUpdate }
98
+ }
@@ -0,0 +1,123 @@
1
+ import { clientLogger } from "../../shared/index.js"
2
+
3
+ export function createRecordSubscriptions(client) {
4
+ const subscriptions = client.recordSubscriptions
5
+
6
+ async function handleUpdate(payload) {
7
+ const { recordId, full, patch, version } = payload
8
+
9
+ for (const [collectionId, collectionSub] of client.collectionSubscriptions.entries()) {
10
+ if (collectionSub.ids.has(recordId) && collectionSub.onDiff) {
11
+ try {
12
+ await collectionSub.onDiff({
13
+ added: [],
14
+ removed: [],
15
+ changed: [{ id: recordId, record: full }],
16
+ version,
17
+ })
18
+ } catch (error) {
19
+ clientLogger.error(`Error in collection record update callback for ${collectionId}:`, error)
20
+ }
21
+ }
22
+ }
23
+
24
+ const subscription = subscriptions.get(recordId)
25
+ if (!subscription) return
26
+
27
+ if (patch) {
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
+ )
32
+ await unsubscribe(recordId)
33
+ await subscribe(recordId, subscription.callback, { mode: subscription.mode })
34
+ return
35
+ }
36
+ subscription.localVersion = version
37
+ if (subscription.callback) await subscription.callback({ recordId, patch, version })
38
+ } else if (full !== undefined) {
39
+ subscription.localVersion = version
40
+ if (subscription.callback) await subscription.callback({ recordId, full, version })
41
+ }
42
+ }
43
+
44
+ async function handleDeleted(payload) {
45
+ const { recordId, version } = payload
46
+ const subscription = subscriptions.get(recordId)
47
+ if (subscription && subscription.callback) {
48
+ try {
49
+ await subscription.callback({ recordId, deleted: true, version })
50
+ } catch (error) {
51
+ clientLogger.error(`Error in record deletion callback for ${recordId}:`, error)
52
+ }
53
+ }
54
+ subscriptions.delete(recordId)
55
+ }
56
+
57
+ /**
58
+ * @param {string} recordId
59
+ * @param {(update: {recordId: string, full?: any, patch?: import('fast-json-patch').Operation[], version: number, deleted?: boolean}) => void} callback
60
+ * @param {{mode?: 'full' | 'patch'}} [options]
61
+ * @returns {Promise<{success: boolean, record: any, version: number}>}
62
+ */
63
+ async function subscribe(recordId, callback, options) {
64
+ const mode = options?.mode ?? "full"
65
+ try {
66
+ const result = await client.command("mesh/subscribe-record", { recordId, mode })
67
+ if (result.success) {
68
+ subscriptions.set(recordId, { callback, localVersion: result.version, mode })
69
+ if (callback) await callback({ recordId, full: result.record, version: result.version })
70
+ }
71
+ return { success: result.success, record: result.record ?? null, version: result.version ?? 0 }
72
+ } catch (error) {
73
+ clientLogger.error(`Failed to subscribe to record ${recordId}:`, error)
74
+ return { success: false, record: null, version: 0 }
75
+ }
76
+ }
77
+
78
+ /**
79
+ * @param {string} recordId
80
+ * @returns {Promise<boolean>}
81
+ */
82
+ async function unsubscribe(recordId) {
83
+ try {
84
+ const success = await client.command("mesh/unsubscribe-record", { recordId })
85
+ if (success) subscriptions.delete(recordId)
86
+ return success
87
+ } catch (error) {
88
+ clientLogger.error(`Failed to unsubscribe from record ${recordId}:`, error)
89
+ return false
90
+ }
91
+ }
92
+
93
+ /**
94
+ * @param {string} recordId
95
+ * @param {any} newValue
96
+ * @param {Object} [options]
97
+ * @returns {Promise<boolean>}
98
+ */
99
+ async function write(recordId, newValue, options) {
100
+ try {
101
+ const result = await client.command("mesh/publish-record-update", { recordId, newValue, options })
102
+ return result.success === true
103
+ } catch (error) {
104
+ clientLogger.error(`Failed to publish update for record ${recordId}:`, error)
105
+ return false
106
+ }
107
+ }
108
+
109
+ async function resubscribe() {
110
+ const promises = Array.from(subscriptions.entries()).map(async ([recordId, { callback, mode }]) => {
111
+ try {
112
+ await subscribe(recordId, callback, { mode })
113
+ return true
114
+ } catch (error) {
115
+ clientLogger.error(`Failed to resubscribe to record ${recordId}:`, error)
116
+ return false
117
+ }
118
+ })
119
+ return Promise.allSettled(promises)
120
+ }
121
+
122
+ return { handleUpdate, handleDeleted, subscribe, unsubscribe, write, resubscribe }
123
+ }
@@ -0,0 +1,69 @@
1
+ import { clientLogger } from "../../shared/index.js"
2
+
3
+ export function createRoomSubscriptions(client, presence) {
4
+ const joinedRooms = client.joinedRooms
5
+
6
+ /**
7
+ * @param {string} roomName
8
+ * @param {(update: {roomName: string, present: string[], states: Object<string, any>, joined?: string, left?: string}) => void} [onPresenceUpdate]
9
+ * @returns {Promise<{success: boolean, present: string[]}>}
10
+ */
11
+ async function join(roomName, onPresenceUpdate) {
12
+ const joinResult = await client.command("mesh/join-room", { roomName })
13
+ if (!joinResult.success) return { success: false, present: [] }
14
+
15
+ joinedRooms.set(roomName, onPresenceUpdate)
16
+
17
+ if (!onPresenceUpdate) return { success: true, present: joinResult.present || [] }
18
+
19
+ const { success: subSuccess } = await presence.subscribe(roomName, onPresenceUpdate)
20
+ return { success: subSuccess, present: joinResult.present || [] }
21
+ }
22
+
23
+ /**
24
+ * @param {string} roomName
25
+ * @returns {Promise<{success: boolean}>}
26
+ */
27
+ async function leave(roomName) {
28
+ const result = await client.command("mesh/leave-room", { roomName })
29
+ if (result.success) {
30
+ joinedRooms.delete(roomName)
31
+ if (client.presenceSubscriptions.has(roomName)) {
32
+ await presence.unsubscribe(roomName)
33
+ }
34
+ }
35
+ return { success: result.success }
36
+ }
37
+
38
+ /**
39
+ * @param {string} roomName
40
+ * @returns {Promise<any>}
41
+ */
42
+ async function getMetadata(roomName) {
43
+ try {
44
+ const result = await client.command("mesh/get-room-metadata", { roomName })
45
+ return result.metadata
46
+ } catch (error) {
47
+ clientLogger.error(`Failed to get metadata for room ${roomName}:`, error)
48
+ return null
49
+ }
50
+ }
51
+
52
+ async function resubscribe(presenceModule) {
53
+ const promises = Array.from(joinedRooms.entries()).map(async ([roomName, presenceCallback]) => {
54
+ try {
55
+ await join(roomName, presenceCallback)
56
+ return { roomName, success: true }
57
+ } catch (error) {
58
+ clientLogger.error(`Failed to rejoin room ${roomName}:`, error)
59
+ return { roomName, success: false }
60
+ }
61
+ })
62
+ const results = await Promise.allSettled(promises)
63
+ return results
64
+ .filter((r) => r.status === "fulfilled" && r.value.success)
65
+ .map((r) => r.value.roomName)
66
+ }
67
+
68
+ return { join, leave, getMetadata, resubscribe }
69
+ }
@@ -0,0 +1 @@
1
+ :root{--bg: #0a0a0a;--bg-surface: #111111;--bg-raised: #1a1a1a;--bg-hover: #222222;--bg-active: #2a2a2a;--border: #2a2a2a;--border-subtle: #1e1e1e;--text: #d4d4d4;--text-bright: #e8e8e8;--text-muted: #555555;--accent: #34d399;--accent-dim: rgba(52, 211, 153, .12);--accent-text: #2dd4a2;--syn-string: #a5d6a7;--syn-number: #4dd0e1;--syn-boolean: #ce93d8;--syn-null: #666666;--syn-key: #b0b0b0;--syn-bracket: #555555}*{box-sizing:border-box;margin:0;padding:0}body{font-family:SF Mono,Fira Code,JetBrains Mono,Cascadia Code,monospace;font-size:12px;line-height:1.5;color:var(--text);background:var(--bg);-webkit-font-smoothing:antialiased}::selection{background:var(--accent-dim);color:var(--accent-text)}::-webkit-scrollbar{width:6px;height:6px}::-webkit-scrollbar-track{background:transparent}::-webkit-scrollbar-thumb{background:var(--border);border-radius:3px}::-webkit-scrollbar-thumb:hover{background:#3a3a3a}.app{display:flex;flex-direction:column;height:100vh;overflow:hidden}.top-bar{display:flex;align-items:center;justify-content:space-between;padding:0 16px;height:40px;border-bottom:1px solid var(--border);background:var(--bg-surface);flex-shrink:0}.top-bar-left{display:flex;align-items:center;gap:12px}.logo{font-size:11px;font-weight:600;letter-spacing:.5px;text-transform:uppercase;color:var(--text-muted)}.logo span{color:var(--accent)}.top-bar-right{display:flex;align-items:center;gap:12px}.pulse{width:6px;height:6px;border-radius:50%;background:var(--accent)}.pulse.disconnected{background:#ef4444}.instance-id{font-size:10px;color:var(--text-muted)}.tab-bar{display:flex;align-items:center;gap:0;border-bottom:1px solid var(--border);background:var(--bg-surface);flex-shrink:0;padding:0 16px}.tab{padding:8px 16px;font-size:11px;color:var(--text-muted);cursor:pointer;border-bottom:2px solid transparent;-webkit-user-select:none;user-select:none}.tab:hover{color:var(--text)}.tab.active{color:var(--accent-text);border-bottom-color:var(--accent)}.tab .count{margin-left:6px;font-size:10px;color:var(--text-muted)}.tab.active .count{color:var(--accent)}.main{display:flex;flex:1;overflow:hidden}.sidebar{width:280px;min-width:280px;border-right:1px solid var(--border);overflow-y:auto;background:var(--bg-surface)}.content{flex:1;overflow-y:auto;padding:16px}.sidebar-section{border-bottom:1px solid var(--border-subtle)}.sidebar-header{padding:8px 12px;font-size:10px;text-transform:uppercase;letter-spacing:.5px;color:var(--text-muted);background:var(--bg)}.sidebar-item{display:flex;align-items:center;justify-content:space-between;padding:6px 12px;cursor:pointer;border-left:2px solid transparent}.sidebar-item:hover{background:var(--bg-hover)}.sidebar-item.active{background:var(--accent-dim);border-left-color:var(--accent)}.sidebar-item .label{font-size:11px;color:var(--text);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.sidebar-item.active .label{color:var(--accent-text)}.sidebar-item .meta{font-size:10px;color:var(--text-muted);flex-shrink:0;margin-left:8px}.badge{display:inline-flex;align-items:center;justify-content:center;min-width:18px;height:18px;padding:0 5px;font-size:10px;border-radius:9px;background:#1f1f1f;color:#999}.badge.accent{background:var(--accent-dim);color:var(--accent)}.section{margin-bottom:20px}.section-title{font-size:10px;text-transform:uppercase;letter-spacing:.5px;color:var(--text-muted);margin-bottom:8px}.card{background:var(--bg-surface);border:1px solid var(--border);border-radius:4px;overflow:hidden}.card+.card{margin-top:8px}.card-header{display:flex;align-items:center;justify-content:space-between;padding:8px 12px;background:var(--bg);border-bottom:1px solid var(--border-subtle);font-size:11px}.card-header .name{color:var(--text-bright)}.card-body{padding:8px 12px}.kv-row{display:flex;align-items:baseline;padding:2px 0;font-size:11px}.kv-key{color:var(--text-muted);min-width:100px;flex-shrink:0}.kv-value{color:var(--text);word-break:break-all}.member-row{display:flex;align-items:center;justify-content:space-between;padding:4px 12px;font-size:11px;border-bottom:1px solid var(--border-subtle)}.member-row:last-child{border-bottom:none}.member-id{color:var(--text);font-size:11px}.member-presence{font-size:10px;color:var(--accent)}.tag{display:inline-block;padding:1px 6px;font-size:10px;border-radius:3px;background:#1f1f1f;color:#999;margin:1px 2px}.tag.accent{background:var(--accent-dim);color:var(--accent)}.empty{padding:24px;text-align:center;color:var(--text-muted);font-size:11px}.view-hint{font-size:11px;color:var(--text-muted);margin-bottom:12px;padding-bottom:8px;border-bottom:1px solid var(--border-subtle)}.no-presence{font-size:10px;color:#333}.pattern-list{display:flex;flex-wrap:wrap;gap:4px;margin-top:4px}.json-view{font-size:11px;line-height:1.6;white-space:pre-wrap;word-break:break-all}.json-view .shiki{background:transparent!important;padding:0;margin:0}.json-view .shiki code{font-family:inherit;font-size:inherit}.select{background:var(--bg-raised);border:1px solid var(--border);color:var(--text);font-family:inherit;font-size:11px;padding:4px 8px;border-radius:3px;outline:none;cursor:pointer}.select:focus{border-color:var(--accent)}.conn-link{cursor:pointer;text-decoration:underline;text-decoration-color:var(--border);text-underline-offset:2px}.conn-link:hover{color:var(--accent-text);text-decoration-color:var(--accent)}.exposed-row{display:flex;align-items:center;flex-wrap:wrap;gap:3px;padding:4px 12px;border-bottom:1px solid var(--border-subtle)}.exposed-row:last-child{border-bottom:none}.exposed-label{font-size:10px;color:var(--text-muted);min-width:70px;flex-shrink:0}.inline-json .s{color:var(--syn-string)}.inline-json .n{color:var(--syn-number)}.inline-json .b{color:var(--syn-boolean)}.inline-json .null{color:var(--syn-null)}.inline-json .k{color:var(--syn-key)}.inline-json .p{color:var(--syn-bracket)}