@prsm/realtime 1.0.0 → 1.0.1

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.
@@ -1,16 +0,0 @@
1
- <!DOCTYPE html>
2
- <html lang="en">
3
- <head>
4
- <meta charset="UTF-8">
5
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
- <title>mesh devtools</title>
7
- <style>
8
- body { margin: 0; background: #0a0a0a; }
9
- </style>
10
- <script type="module" crossorigin src="./assets/index-w2FI7RvC.js"></script>
11
- <link rel="stylesheet" crossorigin href="./assets/index-CGm1NqOQ.css">
12
- </head>
13
- <body>
14
- <div id="app"></div>
15
- </body>
16
- </html>
@@ -1,15 +0,0 @@
1
- <!DOCTYPE html>
2
- <html lang="en">
3
- <head>
4
- <meta charset="UTF-8">
5
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
- <title>mesh devtools</title>
7
- <style>
8
- body { margin: 0; background: #0a0a0a; }
9
- </style>
10
- </head>
11
- <body>
12
- <div id="app"></div>
13
- <script type="module" src="/src/main.js"></script>
14
- </body>
15
- </html>
@@ -1,17 +0,0 @@
1
- {
2
- "name": "@prsm/realtime-devtools-client",
3
- "private": true,
4
- "type": "module",
5
- "scripts": {
6
- "dev": "vite",
7
- "build": "vite build"
8
- },
9
- "dependencies": {
10
- "vue": "^3.5.13",
11
- "shiki": "^3.4.2"
12
- },
13
- "devDependencies": {
14
- "@vitejs/plugin-vue": "^5.2.4",
15
- "vite": "^6.3.5"
16
- }
17
- }
@@ -1,173 +0,0 @@
1
- <template>
2
- <div class="app">
3
- <div class="top-bar">
4
- <div class="top-bar-left">
5
- <span class="logo">mesh <span>devtools</span></span>
6
- </div>
7
- <div class="top-bar-right">
8
- <span class="pulse" :class="{ disconnected: !!error }"></span>
9
- <span class="instance-id" v-if="state">instance {{ state.instanceId?.slice(0, 8) }}</span>
10
- </div>
11
- </div>
12
-
13
- <div class="tab-bar">
14
- <div
15
- v-for="tab in tabs"
16
- :key="tab.id"
17
- class="tab"
18
- :class="{ active: activeTab === tab.id }"
19
- @click="activeTab = tab.id"
20
- >
21
- {{ tab.label }}<span class="count" v-if="tab.count !== undefined">{{ tab.count }}</span>
22
- </div>
23
- </div>
24
-
25
- <div class="main">
26
- <div class="sidebar">
27
- <ConnectionPicker
28
- :connections="state?.connections || []"
29
- :selected-id="selectedConnectionId"
30
- @select="selectConnection"
31
- />
32
- <div class="sidebar-section" v-if="state?.exposed">
33
- <div class="sidebar-header" style="cursor: pointer; user-select: none;" @click="showExposed = !showExposed">
34
- <span>registered</span>
35
- <span style="float: right; font-size: 9px;">{{ showExposed ? '−' : '+' }}</span>
36
- </div>
37
- <template v-if="showExposed">
38
- <div
39
- v-for="(patterns, type) in nonEmptyExposed"
40
- :key="type"
41
- class="exposed-row"
42
- >
43
- <span class="exposed-label">{{ formatExposedLabel(type) }}</span>
44
- <span class="tag" v-for="p in patterns" :key="p">{{ p }}</span>
45
- </div>
46
- </template>
47
- </div>
48
- </div>
49
-
50
- <div class="content">
51
- <RoomsView
52
- v-if="activeTab === 'rooms'"
53
- :rooms="filteredRooms"
54
- @navigate="navigateToConnection"
55
- />
56
- <ChannelsView
57
- v-if="activeTab === 'channels'"
58
- :channels="filteredChannels"
59
- @navigate="navigateToConnection"
60
- />
61
- <CollectionsView
62
- v-if="activeTab === 'collections'"
63
- :collections="filteredCollections"
64
- :fetch-records="fetchCollectionRecords"
65
- @navigate="navigateToConnection"
66
- />
67
- <RecordsView
68
- v-if="activeTab === 'records'"
69
- :records="filteredRecords"
70
- @navigate="navigateToConnection"
71
- />
72
- <MetadataView
73
- v-if="activeTab === 'metadata'"
74
- :detail="connectionDetail"
75
- :connections="state?.connections || []"
76
- />
77
- </div>
78
- </div>
79
- </div>
80
- </template>
81
-
82
- <script setup>
83
- import { ref, computed } from 'vue'
84
- import { useApi } from './composables/useApi.js'
85
- import ConnectionPicker from './components/ConnectionPicker.vue'
86
- import RoomsView from './views/RoomsView.vue'
87
- import ChannelsView from './views/ChannelsView.vue'
88
- import CollectionsView from './views/CollectionsView.vue'
89
- import RecordsView from './views/RecordsView.vue'
90
- import MetadataView from './views/MetadataView.vue'
91
-
92
- const activeTab = ref('rooms')
93
- const showExposed = ref(true)
94
-
95
- const exposedLabels = {
96
- channels: 'channels',
97
- records: 'records',
98
- writableRecords: 'writable',
99
- collections: 'collections',
100
- presence: 'presence',
101
- commands: 'commands'
102
- }
103
-
104
- function formatExposedLabel(key) {
105
- return exposedLabels[key] || key
106
- }
107
-
108
- function navigateToConnection(connId) {
109
- selectConnection(connId)
110
- activeTab.value = 'metadata'
111
- }
112
- const {
113
- state,
114
- connectionDetail,
115
- selectedConnectionId,
116
- error,
117
- selectConnection,
118
- fetchCollectionRecords
119
- } = useApi()
120
-
121
- const filteredRooms = computed(() => {
122
- if (!state.value?.rooms) return []
123
- if (!selectedConnectionId.value) return state.value.rooms
124
- return state.value.rooms.filter(r => r.members.includes(selectedConnectionId.value))
125
- })
126
-
127
- const filteredChannels = computed(() => {
128
- if (!state.value?.channels) return {}
129
- if (!selectedConnectionId.value) return state.value.channels
130
- const out = {}
131
- for (const [ch, subs] of Object.entries(state.value.channels)) {
132
- if (subs.includes(selectedConnectionId.value)) out[ch] = subs
133
- }
134
- return out
135
- })
136
-
137
- const filteredCollections = computed(() => {
138
- if (!state.value?.collections) return {}
139
- if (!selectedConnectionId.value) return state.value.collections
140
- const out = {}
141
- for (const [id, info] of Object.entries(state.value.collections)) {
142
- if (info.subscribers[selectedConnectionId.value]) out[id] = info
143
- }
144
- return out
145
- })
146
-
147
- const filteredRecords = computed(() => {
148
- if (!state.value?.records) return {}
149
- if (!selectedConnectionId.value) return state.value.records
150
- const out = {}
151
- for (const [id, info] of Object.entries(state.value.records)) {
152
- if (info.subscribers[selectedConnectionId.value]) out[id] = info
153
- }
154
- return out
155
- })
156
-
157
- const nonEmptyExposed = computed(() => {
158
- if (!state.value?.exposed) return {}
159
- const out = {}
160
- for (const [k, v] of Object.entries(state.value.exposed)) {
161
- if (v.length) out[k] = v
162
- }
163
- return out
164
- })
165
-
166
- const tabs = computed(() => [
167
- { id: 'rooms', label: 'rooms', count: filteredRooms.value.length },
168
- { id: 'channels', label: 'channels', count: Object.keys(filteredChannels.value).length },
169
- { id: 'collections', label: 'collections', count: Object.keys(filteredCollections.value).length },
170
- { id: 'records', label: 'records', count: Object.keys(filteredRecords.value).length },
171
- { id: 'metadata', label: 'metadata' }
172
- ])
173
- </script>
@@ -1,38 +0,0 @@
1
- <template>
2
- <div class="sidebar-section">
3
- <div class="sidebar-header">connections</div>
4
- <div
5
- class="sidebar-item"
6
- :class="{ active: !selectedId }"
7
- @click="$emit('select', null)"
8
- >
9
- <span class="label">all connections</span>
10
- <span class="badge" v-if="connections.length">{{ connections.length }}</span>
11
- </div>
12
- <div
13
- v-for="conn in connections"
14
- :key="conn.id"
15
- class="sidebar-item"
16
- :class="{ active: selectedId === conn.id }"
17
- @click="$emit('select', conn.id)"
18
- >
19
- <span class="label" :title="conn.id">{{ conn.id.slice(0, 8) }}</span>
20
- <span class="meta" v-if="conn.latency !== null">{{ conn.latency }}ms</span>
21
- </div>
22
- </div>
23
- </template>
24
-
25
- <script setup>
26
- defineProps({
27
- connections: { type: Array, default: () => [] },
28
- selectedId: { type: String, default: null }
29
- })
30
-
31
- defineEmits(['select'])
32
-
33
- function label(conn) {
34
- if (conn.metadata?.name) return conn.metadata.name
35
- if (conn.metadata?.username) return conn.metadata.username
36
- return conn.id.slice(0, 8)
37
- }
38
- </script>
@@ -1,18 +0,0 @@
1
- <template>
2
- <div class="json-view" v-html="html"></div>
3
- </template>
4
-
5
- <script setup>
6
- import { computed } from 'vue'
7
- import { useHighlight } from '../composables/useHighlight.js'
8
-
9
- const props = defineProps({ data: { default: null } })
10
- const { highlight } = useHighlight()
11
-
12
- const html = computed(() => {
13
- if (props.data === null || props.data === undefined) {
14
- return '<span style="color: var(--syn-null)">null</span>'
15
- }
16
- return highlight(props.data)
17
- })
18
- </script>
@@ -1,71 +0,0 @@
1
- import { ref, onMounted, onUnmounted } from 'vue'
2
-
3
- export function useApi(pollInterval = 1000) {
4
- const state = ref(null)
5
- const connectionDetail = ref(null)
6
- const selectedConnectionId = ref(null)
7
- const error = ref(null)
8
- let timer = null
9
-
10
- const basePath = import.meta.env.DEV ? '' : '.'
11
-
12
- async function fetchState() {
13
- try {
14
- const res = await fetch(`${basePath}/api/state`)
15
- if (!res.ok) throw new Error(`${res.status}`)
16
- state.value = await res.json()
17
- error.value = null
18
- } catch (e) {
19
- error.value = e.message
20
- }
21
- }
22
-
23
- async function fetchConnectionDetail(id) {
24
- try {
25
- const res = await fetch(`${basePath}/api/connection/${id}`)
26
- if (!res.ok) throw new Error(`${res.status}`)
27
- connectionDetail.value = await res.json()
28
- } catch {}
29
- }
30
-
31
- async function fetchCollectionRecords(collId, connId) {
32
- try {
33
- const res = await fetch(`${basePath}/api/collection/${encodeURIComponent(collId)}/records?connId=${encodeURIComponent(connId)}`)
34
- if (!res.ok) throw new Error(`${res.status}`)
35
- return await res.json()
36
- } catch {
37
- return { recordIds: [], records: [] }
38
- }
39
- }
40
-
41
- async function selectConnection(id) {
42
- selectedConnectionId.value = id
43
- if (id) await fetchConnectionDetail(id)
44
- else connectionDetail.value = null
45
- }
46
-
47
- async function poll() {
48
- await fetchState()
49
- if (selectedConnectionId.value) {
50
- await fetchConnectionDetail(selectedConnectionId.value)
51
- }
52
- }
53
-
54
- onMounted(() => {
55
- poll()
56
- timer = setInterval(poll, pollInterval)
57
- })
58
-
59
- onUnmounted(() => {
60
- if (timer) clearInterval(timer)
61
- })
62
-
63
- return {
64
- state,
65
- connectionDetail,
66
- selectedConnectionId,
67
- error,
68
- selectConnection,
69
- fetchCollectionRecords
70
- }
71
- }
@@ -1,57 +0,0 @@
1
- import { ref, onMounted } from 'vue'
2
- import { createHighlighterCore } from 'shiki/core'
3
- import { createJavaScriptRegexEngine } from 'shiki/engine/javascript'
4
- import json from 'shiki/langs/json.mjs'
5
-
6
- const highlighterRef = ref(null)
7
- const ready = ref(false)
8
- let initPromise = null
9
-
10
- const theme = {
11
- name: 'mesh-devtools',
12
- type: 'dark',
13
- bg: 'transparent',
14
- fg: '#d4d4d4',
15
- settings: [
16
- { scope: ['string'], settings: { foreground: '#a5d6a7' } },
17
- { scope: ['constant.numeric'], settings: { foreground: '#4dd0e1' } },
18
- { scope: ['constant.language.boolean', 'constant.language'], settings: { foreground: '#ce93d8' } },
19
- { scope: ['constant.language.null'], settings: { foreground: '#666666' } },
20
- { scope: ['support.type.property-name'], settings: { foreground: '#b0b0b0' } },
21
- { scope: ['punctuation'], settings: { foreground: '#555555' } },
22
- ]
23
- }
24
-
25
- function init() {
26
- if (initPromise) return initPromise
27
- initPromise = createHighlighterCore({
28
- themes: [theme],
29
- langs: [json],
30
- engine: createJavaScriptRegexEngine()
31
- }).then(h => {
32
- highlighterRef.value = h
33
- ready.value = true
34
- return h
35
- })
36
- return initPromise
37
- }
38
-
39
- export function useHighlight() {
40
- onMounted(init)
41
-
42
- function highlight(value) {
43
- const json = typeof value === 'string' ? value : JSON.stringify(value, null, 2)
44
- if (!json) return ''
45
- if (!highlighterRef.value) return escapeHtml(json)
46
- return highlighterRef.value.codeToHtml(json, {
47
- lang: 'json',
48
- theme: 'mesh-devtools'
49
- })
50
- }
51
-
52
- return { highlight, ready }
53
- }
54
-
55
- function escapeHtml(str) {
56
- return str.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')
57
- }
@@ -1,5 +0,0 @@
1
- import { createApp } from 'vue'
2
- import App from './App.vue'
3
- import './style.css'
4
-
5
- createApp(App).mount('#app')