@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.
- package/package.json +49 -0
- package/src/adapters/postgres.js +139 -0
- package/src/adapters/sqlite.js +177 -0
- package/src/client/client.js +441 -0
- package/src/client/connection.js +178 -0
- package/src/client/ids.js +26 -0
- package/src/client/index.js +5 -0
- package/src/client/queue.js +44 -0
- package/src/client/subscriptions/channels.js +79 -0
- package/src/client/subscriptions/collections.js +96 -0
- package/src/client/subscriptions/presence.js +98 -0
- package/src/client/subscriptions/records.js +123 -0
- package/src/client/subscriptions/rooms.js +69 -0
- package/src/devtools/client/dist/assets/index-CGm1NqOQ.css +1 -0
- package/src/devtools/client/dist/assets/index-w2FI7RvC.js +168 -0
- package/src/devtools/client/dist/index.html +16 -0
- package/src/devtools/client/index.html +15 -0
- package/src/devtools/client/package.json +17 -0
- package/src/devtools/client/src/App.vue +173 -0
- package/src/devtools/client/src/components/ConnectionPicker.vue +38 -0
- package/src/devtools/client/src/components/JsonView.vue +18 -0
- package/src/devtools/client/src/composables/useApi.js +71 -0
- package/src/devtools/client/src/composables/useHighlight.js +57 -0
- package/src/devtools/client/src/main.js +5 -0
- package/src/devtools/client/src/style.css +440 -0
- package/src/devtools/client/src/views/ChannelsView.vue +27 -0
- package/src/devtools/client/src/views/CollectionsView.vue +61 -0
- package/src/devtools/client/src/views/MetadataView.vue +108 -0
- package/src/devtools/client/src/views/RecordsView.vue +30 -0
- package/src/devtools/client/src/views/RoomsView.vue +39 -0
- package/src/devtools/client/vite.config.js +17 -0
- package/src/devtools/demo/server.js +144 -0
- package/src/devtools/index.js +186 -0
- package/src/index.js +9 -0
- package/src/server/connection.js +116 -0
- package/src/server/context.js +22 -0
- package/src/server/managers/broadcast.js +94 -0
- package/src/server/managers/channels.js +118 -0
- package/src/server/managers/collections.js +127 -0
- package/src/server/managers/commands.js +55 -0
- package/src/server/managers/connections.js +111 -0
- package/src/server/managers/instance.js +125 -0
- package/src/server/managers/persistence.js +371 -0
- package/src/server/managers/presence.js +217 -0
- package/src/server/managers/pubsub.js +242 -0
- package/src/server/managers/record-subscriptions.js +123 -0
- package/src/server/managers/records.js +110 -0
- package/src/server/managers/redis.js +61 -0
- package/src/server/managers/rooms.js +129 -0
- package/src/server/message-stream.js +20 -0
- package/src/server/server.js +878 -0
- package/src/server/utils/constants.js +4 -0
- package/src/server/utils/ids.js +5 -0
- package/src/server/utils/pattern-conversion.js +14 -0
- package/src/shared/errors.js +7 -0
- package/src/shared/index.js +5 -0
- package/src/shared/logger.js +53 -0
- package/src/shared/merge.js +17 -0
- package/src/shared/message.js +17 -0
- 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)}
|