@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,17 +0,0 @@
1
- import { defineConfig } from 'vite'
2
- import vue from '@vitejs/plugin-vue'
3
-
4
- export default defineConfig({
5
- plugins: [vue()],
6
- base: './',
7
- build: {
8
- outDir: 'dist',
9
- emptyOutDir: true
10
- },
11
- server: {
12
- port: 4173,
13
- proxy: {
14
- '/api': 'http://localhost:3399/devtools'
15
- }
16
- }
17
- })
@@ -1,144 +0,0 @@
1
- import express from "express"
2
- import http from "node:http"
3
- import { RealtimeServer } from "../../../src/index.js"
4
- import { RealtimeClient } from "../../../src/client/index.js"
5
- import { createDevtools } from "../index.js"
6
-
7
- const PORT = 3399
8
-
9
- const app = express()
10
- const httpServer = http.createServer(app)
11
-
12
- const mesh = new RealtimeServer({
13
- redis: { host: "127.0.0.1", port: 6379, db: 15 }
14
- })
15
-
16
- mesh.exposeChannel(/^chat:.*/)
17
- mesh.exposeChannel("notifications")
18
- mesh.exposeRecord(/^user:.*/)
19
- mesh.exposeWritableRecord(/^doc:.*/)
20
- mesh.trackPresence(/^room:.*/)
21
-
22
- mesh.exposeCollection("users:online", async () => {
23
- return await mesh.listRecordsMatching("user:*")
24
- })
25
-
26
- mesh.exposeCollection("docs:shared", async () => {
27
- return await mesh.listRecordsMatching("doc:*")
28
- })
29
-
30
- mesh.exposeCollection(/^users-by-role:.*/, async (_conn, collectionId) => {
31
- const role = collectionId.split(":")[1]
32
- const users = await mesh.listRecordsMatching("user:*")
33
- return users.filter(u => u.role === role)
34
- })
35
-
36
- mesh.exposeCommand("echo", (ctx) => ctx.payload)
37
-
38
- app.use("/devtools", createDevtools(mesh))
39
-
40
- const redis = mesh.redisManager.redis
41
- await redis.flushdb()
42
-
43
- await mesh.attach(httpServer, { port: PORT })
44
-
45
- const names = ["alice", "bob", "charlie", "diana", "eve"]
46
- const statuses = ["online", "away", "busy", "idle"]
47
- const rooms = ["room:lobby", "room:general", "room:random"]
48
- const channels = ["chat:general", "chat:random", "notifications"]
49
-
50
- const clients = []
51
-
52
- for (let i = 0; i < names.length; i++) {
53
- const client = new RealtimeClient(`ws://localhost:${PORT}`)
54
- await client.connect()
55
-
56
- await mesh.setConnectionMetadata(client.connectionId, {
57
- name: names[i],
58
- role: i === 0 ? "admin" : "member",
59
- joinedAt: new Date().toISOString()
60
- })
61
-
62
- const role = i === 0 ? "admin" : "member"
63
- await mesh.writeRecord(`user:${names[i]}`, {
64
- id: `user:${names[i]}`,
65
- name: names[i],
66
- email: `${names[i]}@example.com`,
67
- role,
68
- active: true
69
- })
70
-
71
- if (i < 3) {
72
- await mesh.writeRecord(`doc:doc-${i}`, {
73
- id: `doc:doc-${i}`,
74
- title: `Document ${i}`,
75
- content: "lorem ipsum",
76
- author: names[i]
77
- })
78
- }
79
-
80
- const clientRooms = rooms.slice(0, 1 + (i % rooms.length))
81
- for (const room of clientRooms) {
82
- await client.joinRoom(room)
83
- await client.subscribePresence(room, () => {})
84
- }
85
- await new Promise(r => setTimeout(r, 100))
86
- for (const room of clientRooms) {
87
- await client.publishPresenceState(room, {
88
- state: {
89
- status: statuses[i % statuses.length],
90
- cursor: { x: Math.floor(Math.random() * 800), y: Math.floor(Math.random() * 600) }
91
- }
92
- })
93
- }
94
-
95
- const clientChannels = channels.slice(0, 1 + (i % channels.length))
96
- for (const ch of clientChannels) {
97
- await client.subscribeChannel(ch, () => {})
98
- }
99
-
100
- await client.subscribeRecord(`user:${names[i]}`, () => {})
101
- if (i < 3) await client.subscribeRecord(`doc:doc-${i}`, () => {})
102
-
103
- clients.push(client)
104
- }
105
-
106
- await new Promise(r => setTimeout(r, 300))
107
-
108
- for (const client of clients) {
109
- await client.subscribeCollection("users:online", { onDiff: () => {} })
110
- }
111
-
112
- for (const client of clients.slice(0, 3)) {
113
- await client.subscribeCollection("docs:shared", { onDiff: () => {} })
114
- }
115
-
116
- await clients[0].subscribeCollection("users-by-role:admin", { onDiff: () => {} })
117
- await clients[1].subscribeCollection("users-by-role:member", { onDiff: () => {} })
118
- await clients[2].subscribeCollection("users-by-role:admin", { onDiff: () => {} })
119
-
120
- setInterval(async () => {
121
- const i = Math.floor(Math.random() * clients.length)
122
- const room = rooms[Math.floor(Math.random() * rooms.length)]
123
- const clientRooms = rooms.slice(0, 1 + (i % rooms.length))
124
- if (!clientRooms.includes(room)) return
125
-
126
- await clients[i].publishPresenceState(room, {
127
- state: {
128
- status: statuses[Math.floor(Math.random() * statuses.length)],
129
- cursor: { x: Math.floor(Math.random() * 800), y: Math.floor(Math.random() * 600) }
130
- }
131
- })
132
- }, 3000)
133
-
134
- setInterval(async () => {
135
- const ch = channels[Math.floor(Math.random() * channels.length)]
136
- await mesh.writeChannel(ch, {
137
- from: names[Math.floor(Math.random() * names.length)],
138
- text: `message at ${new Date().toISOString()}`,
139
- }, 20)
140
- }, 5000)
141
-
142
- console.log(`mesh devtools demo running on http://localhost:${PORT}/devtools`)
143
- console.log(`${names.length} clients connected, ${rooms.length} rooms, ${channels.length} channels`)
144
- console.log(`vite dev server: cd packages/devtools/client && npm run dev`)
@@ -1,186 +0,0 @@
1
- import express from "express"
2
- import { fileURLToPath } from "node:url"
3
- import { dirname, join } from "node:path"
4
- import { existsSync } from "node:fs"
5
-
6
- function patternToString(p) {
7
- if (typeof p === "string") return p
8
- if (p instanceof RegExp) return p.toString()
9
- return String(p)
10
- }
11
-
12
- export function createDevtools(server) {
13
- const router = express.Router()
14
- const __dirname = dirname(fileURLToPath(import.meta.url))
15
- const distPath = join(__dirname, "client", "dist")
16
-
17
- router.get("/api/state", async (_req, res) => {
18
- try {
19
- const connIds = await server.connectionManager.getAllConnectionIds()
20
- const allMeta = await server.connectionManager._getMetadataForConnectionIds(connIds)
21
- const localConns = server.connectionManager.getLocalConnections()
22
- const localMap = {}
23
- for (const conn of localConns) {
24
- localMap[conn.id] = conn
25
- }
26
-
27
- const connections = allMeta.map(({ id, metadata }) => {
28
- const local = localMap[id]
29
- return {
30
- id,
31
- metadata,
32
- local: !!local,
33
- latency: local?.latency?.ms ?? null,
34
- alive: local?.alive ?? null,
35
- remoteAddress: local?.remoteAddress ?? null
36
- }
37
- })
38
-
39
- const roomNames = await server.roomManager.getAllRooms()
40
- const rooms = []
41
- for (const name of roomNames) {
42
- const members = await server.roomManager.getRoomConnectionIds(name)
43
- const statesMap = await server.presenceManager.getAllPresenceStates(name)
44
- const presence = {}
45
- statesMap.forEach((state, connId) => { presence[connId] = state })
46
- rooms.push({ name, members, presence })
47
- }
48
-
49
- const channels = {}
50
- for (const [channel, subscribers] of Object.entries(server.channelManager.channelSubscriptions)) {
51
- if (channel.startsWith("mesh:presence:updates:")) continue
52
- channels[channel] = [...subscribers].map(c => c.id)
53
- }
54
-
55
- const collections = {}
56
- server.collectionManager.collectionSubscriptions.forEach((subs, collId) => {
57
- const subscribers = {}
58
- subs.forEach((info, connId) => { subscribers[connId] = info })
59
- collections[collId] = { subscribers }
60
- })
61
-
62
- const records = {}
63
- server.recordSubscriptionManager.recordSubscriptions.forEach((subs, recordId) => {
64
- const subscribers = {}
65
- subs.forEach((mode, connId) => { subscribers[connId] = mode })
66
- records[recordId] = { subscribers }
67
- })
68
-
69
- const exposed = {
70
- channels: server.channelManager.exposedChannels.map(patternToString),
71
- records: server.recordSubscriptionManager.exposedRecords.map(patternToString),
72
- writableRecords: server.recordSubscriptionManager.exposedWritableRecords.map(patternToString),
73
- collections: server.collectionManager.exposedCollections.map(e => patternToString(e.pattern)),
74
- presence: server.presenceManager.trackedRooms.map(patternToString),
75
- commands: server.commandManager.commands
76
- ? Object.keys(server.commandManager.commands).filter(c => !c.startsWith("mesh/"))
77
- : []
78
- }
79
-
80
- res.json({
81
- instanceId: server.instanceId,
82
- connections,
83
- rooms,
84
- channels,
85
- collections,
86
- records,
87
- exposed
88
- })
89
- } catch (err) {
90
- res.status(500).json({ error: err.message })
91
- }
92
- })
93
-
94
- router.get("/api/connection/:id", async (req, res) => {
95
- try {
96
- const { id } = req.params
97
- const metadata = await server.connectionManager.getMetadata(id)
98
- const rooms = await server.roomManager.getRoomsForConnection(id)
99
-
100
- const presence = {}
101
- for (const room of rooms) {
102
- const state = await server.presenceManager.getPresenceState(id, room)
103
- if (state) presence[room] = state
104
- }
105
-
106
- const channels = []
107
- for (const [channel, subscribers] of Object.entries(server.channelManager.channelSubscriptions)) {
108
- if (channel.startsWith("mesh:presence:updates:")) continue
109
- for (const conn of subscribers) {
110
- if (conn.id === id) { channels.push(channel); break }
111
- }
112
- }
113
-
114
- const collections = []
115
- server.collectionManager.collectionSubscriptions.forEach((subs, collId) => {
116
- if (subs.has(id)) collections.push({ id: collId, ...subs.get(id) })
117
- })
118
-
119
- const records = []
120
- server.recordSubscriptionManager.recordSubscriptions.forEach((subs, recordId) => {
121
- if (subs.has(id)) records.push({ id: recordId, mode: subs.get(id) })
122
- })
123
-
124
- const local = server.connectionManager.getLocalConnection(id)
125
-
126
- res.json({
127
- id,
128
- metadata,
129
- rooms,
130
- presence,
131
- channels,
132
- collections,
133
- records,
134
- local: !!local,
135
- latency: local?.latency?.ms ?? null,
136
- alive: local?.alive ?? null,
137
- remoteAddress: local?.remoteAddress ?? null
138
- })
139
- } catch (err) {
140
- res.status(500).json({ error: err.message })
141
- }
142
- })
143
-
144
- router.get("/api/room/:name", async (req, res) => {
145
- try {
146
- const { name } = req.params
147
- const membersWithMeta = await server.getRoomMembersWithMetadata(name)
148
- const statesMap = await server.presenceManager.getAllPresenceStates(name)
149
- const presence = {}
150
- statesMap.forEach((state, connId) => { presence[connId] = state })
151
- res.json({ name, members: membersWithMeta, presence })
152
- } catch (err) {
153
- res.status(500).json({ error: err.message })
154
- }
155
- })
156
-
157
- router.get("/api/collection/:id/records", async (req, res) => {
158
- try {
159
- const collId = req.params.id
160
- const connId = req.query.connId
161
- if (!connId) return res.status(400).json({ error: "connId query param required" })
162
-
163
- const raw = await server.redisManager.redis.get(`mesh:collection:${collId}:${connId}`)
164
- if (!raw) return res.json({ recordIds: [], records: [] })
165
-
166
- const recordIds = JSON.parse(raw)
167
- const records = []
168
- for (const rid of recordIds) {
169
- const data = await server.recordManager.getRecord(rid)
170
- records.push({ id: rid, data })
171
- }
172
- res.json({ recordIds, records })
173
- } catch (err) {
174
- res.status(500).json({ error: err.message })
175
- }
176
- })
177
-
178
- if (existsSync(distPath)) {
179
- router.use(express.static(distPath))
180
- router.get("/{*splat}", (_req, res) => {
181
- res.sendFile(join(distPath, "index.html"))
182
- })
183
- }
184
-
185
- return router
186
- }