@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
package/package.json ADDED
@@ -0,0 +1,49 @@
1
+ {
2
+ "name": "@prsm/realtime",
3
+ "version": "1.0.0",
4
+ "description": "Distributed WebSocket framework with Redis-backed rooms, records, presence, channels, collections, and persistence",
5
+ "type": "module",
6
+ "license": "ISC",
7
+ "exports": {
8
+ ".": "./src/index.js",
9
+ "./client": "./src/client/index.js",
10
+ "./sqlite": "./src/adapters/sqlite.js",
11
+ "./postgres": "./src/adapters/postgres.js",
12
+ "./devtools": "./src/devtools/index.js"
13
+ },
14
+ "files": [
15
+ "src"
16
+ ],
17
+ "scripts": {
18
+ "test": "vitest run --reporter verbose"
19
+ },
20
+ "engines": {
21
+ "node": ">=20"
22
+ },
23
+ "dependencies": {
24
+ "eventemitter3": "^5.0.1",
25
+ "fast-json-patch": "^3.1.1",
26
+ "ioredis": "^5.6.1",
27
+ "ws": "^8.18.0"
28
+ },
29
+ "peerDependencies": {
30
+ "express": "^4.0.0 || ^5.0.0",
31
+ "pg": "^8.0.0",
32
+ "sqlite3": "^5.0.0"
33
+ },
34
+ "peerDependenciesMeta": {
35
+ "express": {
36
+ "optional": true
37
+ },
38
+ "sqlite3": {
39
+ "optional": true
40
+ },
41
+ "pg": {
42
+ "optional": true
43
+ }
44
+ },
45
+ "devDependencies": {
46
+ "ioredis": "^5.6.1",
47
+ "vitest": "^3.2.4"
48
+ }
49
+ }
@@ -0,0 +1,139 @@
1
+ import { convertToSqlPattern } from "../server/utils/pattern-conversion.js"
2
+ import { serverLogger } from "../shared/index.js"
3
+
4
+ export function createPostgresAdapter(options = {}) {
5
+ const opts = {
6
+ host: "localhost",
7
+ port: 5432,
8
+ database: "mesh_test",
9
+ user: "mesh",
10
+ password: "mesh_password",
11
+ max: 10,
12
+ ...options,
13
+ }
14
+ let pool = null
15
+ let initialized = false
16
+
17
+ async function createTables() {
18
+ if (!pool) throw new Error("Database not initialized")
19
+ const client = await pool.connect()
20
+ try {
21
+ await client.query(`
22
+ CREATE TABLE IF NOT EXISTS channel_messages (
23
+ id TEXT PRIMARY KEY, channel TEXT NOT NULL, message TEXT NOT NULL,
24
+ instance_id TEXT NOT NULL, timestamp BIGINT NOT NULL, metadata JSONB
25
+ )
26
+ `)
27
+ await client.query(`CREATE INDEX IF NOT EXISTS idx_channel_timestamp ON channel_messages (channel, timestamp)`)
28
+ await client.query(`
29
+ CREATE TABLE IF NOT EXISTS records (
30
+ record_id TEXT PRIMARY KEY, version INTEGER NOT NULL,
31
+ value JSONB NOT NULL, timestamp BIGINT NOT NULL
32
+ )
33
+ `)
34
+ await client.query(`CREATE INDEX IF NOT EXISTS idx_records_timestamp ON records (timestamp)`)
35
+ } finally { client.release() }
36
+ }
37
+
38
+ return {
39
+ async initialize() {
40
+ if (initialized) return
41
+ try {
42
+ const pg = await import("pg")
43
+ const Pool = pg.default?.Pool || pg.Pool
44
+ pool = new Pool(
45
+ opts.connectionString
46
+ ? { connectionString: opts.connectionString, max: opts.max }
47
+ : { host: opts.host, port: opts.port, database: opts.database, user: opts.user, password: opts.password, ssl: opts.ssl, max: opts.max }
48
+ )
49
+ await createTables()
50
+ initialized = true
51
+ } catch (err) {
52
+ serverLogger.error("Error initializing PostgreSQL database:", err)
53
+ throw err
54
+ }
55
+ },
56
+
57
+ async storeMessages(messages) {
58
+ if (!pool) throw new Error("Database not initialized")
59
+ if (messages.length === 0) return
60
+ const client = await pool.connect()
61
+ try {
62
+ await client.query("BEGIN")
63
+ for (const msg of messages) {
64
+ await client.query(
65
+ `INSERT INTO channel_messages (id, channel, message, instance_id, timestamp, metadata) VALUES ($1, $2, $3, $4, $5, $6)`,
66
+ [msg.id, msg.channel, msg.message, msg.instanceId, msg.timestamp, msg.metadata || null]
67
+ )
68
+ }
69
+ await client.query("COMMIT")
70
+ } catch (err) { await client.query("ROLLBACK"); throw err }
71
+ finally { client.release() }
72
+ },
73
+
74
+ async getMessages(channel, since, limit = 50) {
75
+ if (!pool) throw new Error("Database not initialized")
76
+ let query = "SELECT * FROM channel_messages WHERE channel = $1"
77
+ const params = [channel]
78
+ let paramIndex = 2
79
+ if (since !== undefined) {
80
+ if (typeof since === "number") {
81
+ query += ` AND timestamp > $${paramIndex}`
82
+ params.push(since)
83
+ paramIndex++
84
+ } else {
85
+ const timestampResult = await pool.query("SELECT timestamp FROM channel_messages WHERE id = $1", [since])
86
+ const timestamp = timestampResult.rows[0]?.timestamp || 0
87
+ query += ` AND timestamp > $${paramIndex}`
88
+ params.push(timestamp)
89
+ paramIndex++
90
+ }
91
+ }
92
+ query += ` ORDER BY timestamp ASC LIMIT $${paramIndex}`
93
+ params.push(limit)
94
+ const result = await pool.query(query, params)
95
+ return result.rows.map((row) => ({
96
+ id: row.id, channel: row.channel, message: row.message,
97
+ instanceId: row.instance_id, timestamp: parseInt(row.timestamp), metadata: row.metadata,
98
+ }))
99
+ },
100
+
101
+ async storeRecords(records) {
102
+ if (!pool) throw new Error("Database not initialized")
103
+ if (records.length === 0) return
104
+ const client = await pool.connect()
105
+ try {
106
+ await client.query("BEGIN")
107
+ for (const record of records) {
108
+ await client.query(
109
+ `INSERT INTO records (record_id, version, value, timestamp) VALUES ($1, $2, $3, $4)
110
+ ON CONFLICT (record_id) DO UPDATE SET version = $2, value = $3, timestamp = $4`,
111
+ [record.recordId, record.version, record.value, record.timestamp]
112
+ )
113
+ }
114
+ await client.query("COMMIT")
115
+ } catch (err) { await client.query("ROLLBACK"); throw err }
116
+ finally { client.release() }
117
+ },
118
+
119
+ async getRecords(pattern) {
120
+ if (!pool) throw new Error("Database not initialized")
121
+ const sqlPattern = convertToSqlPattern(pattern)
122
+ const result = await pool.query(
123
+ `SELECT record_id, version, value, timestamp FROM records WHERE record_id LIKE $1 ORDER BY timestamp DESC`,
124
+ [sqlPattern]
125
+ )
126
+ return result.rows.map((row) => ({
127
+ recordId: row.record_id, version: row.version,
128
+ value: row.value, timestamp: parseInt(row.timestamp),
129
+ }))
130
+ },
131
+
132
+ async close() {
133
+ if (!pool) return
134
+ await pool.end()
135
+ pool = null
136
+ initialized = false
137
+ },
138
+ }
139
+ }
@@ -0,0 +1,177 @@
1
+ import { convertToSqlPattern } from "../server/utils/pattern-conversion.js"
2
+ import { serverLogger } from "../shared/index.js"
3
+
4
+ export function createSqliteAdapter(options = {}) {
5
+ const opts = { filename: ":memory:", ...options }
6
+ let db = null
7
+ let initialized = false
8
+
9
+ async function createTables() {
10
+ if (!db) throw new Error("Database not initialized")
11
+ return new Promise((resolve, reject) => {
12
+ db.run(
13
+ `CREATE TABLE IF NOT EXISTS channel_messages (
14
+ id TEXT PRIMARY KEY,
15
+ channel TEXT NOT NULL,
16
+ message TEXT NOT NULL,
17
+ instance_id TEXT NOT NULL,
18
+ timestamp INTEGER NOT NULL,
19
+ metadata TEXT
20
+ )`,
21
+ (err) => {
22
+ if (err) return reject(err)
23
+ db.run("CREATE INDEX IF NOT EXISTS idx_channel_timestamp ON channel_messages (channel, timestamp)", (err) => {
24
+ if (err) return reject(err)
25
+ db.run(
26
+ `CREATE TABLE IF NOT EXISTS records (
27
+ record_id TEXT PRIMARY KEY,
28
+ version INTEGER NOT NULL,
29
+ value TEXT NOT NULL,
30
+ timestamp INTEGER NOT NULL
31
+ )`,
32
+ (err) => {
33
+ if (err) return reject(err)
34
+ db.run("CREATE INDEX IF NOT EXISTS idx_records_timestamp ON records (timestamp)", (err) => {
35
+ if (err) return reject(err)
36
+ resolve()
37
+ })
38
+ }
39
+ )
40
+ })
41
+ }
42
+ )
43
+ })
44
+ }
45
+
46
+ return {
47
+ async initialize() {
48
+ if (initialized) return
49
+ const sqlite3 = await import("sqlite3")
50
+ const { Database } = sqlite3.default || sqlite3
51
+ return new Promise((resolve, reject) => {
52
+ try {
53
+ db = new Database(opts.filename, async (err) => {
54
+ if (err) return reject(err)
55
+ try {
56
+ await createTables()
57
+ initialized = true
58
+ resolve()
59
+ } catch (e) { reject(e) }
60
+ })
61
+ } catch (err) { reject(err) }
62
+ })
63
+ },
64
+
65
+ async storeMessages(messages) {
66
+ if (!db) throw new Error("Database not initialized")
67
+ if (messages.length === 0) return
68
+ return new Promise((resolve, reject) => {
69
+ db.serialize(() => {
70
+ db.run("BEGIN TRANSACTION")
71
+ const stmt = db.prepare(
72
+ `INSERT INTO channel_messages (id, channel, message, instance_id, timestamp, metadata) VALUES (?, ?, ?, ?, ?, ?)`
73
+ )
74
+ try {
75
+ for (const msg of messages) {
76
+ const metadata = msg.metadata ? JSON.stringify(msg.metadata) : null
77
+ stmt.run(msg.id, msg.channel, msg.message, msg.instanceId, msg.timestamp, metadata)
78
+ }
79
+ stmt.finalize()
80
+ db.run("COMMIT", (err) => { if (err) reject(err); else resolve() })
81
+ } catch (err) { db.run("ROLLBACK"); reject(err) }
82
+ })
83
+ })
84
+ },
85
+
86
+ async getMessages(channel, since, limit = 50) {
87
+ if (!db) throw new Error("Database not initialized")
88
+ let query = "SELECT * FROM channel_messages WHERE channel = ?"
89
+ const params = [channel]
90
+ if (since !== undefined) {
91
+ if (typeof since === "number") {
92
+ query += " AND timestamp > ?"
93
+ params.push(since)
94
+ } else {
95
+ const timestampQuery = await new Promise((resolve, reject) => {
96
+ db.get("SELECT timestamp FROM channel_messages WHERE id = ?", [since], (err, row) => {
97
+ if (err) reject(err)
98
+ else resolve(row ? row.timestamp : 0)
99
+ })
100
+ })
101
+ query += " AND timestamp > ?"
102
+ params.push(timestampQuery)
103
+ }
104
+ }
105
+ query += " ORDER BY timestamp ASC LIMIT ?"
106
+ params.push(limit)
107
+ return new Promise((resolve, reject) => {
108
+ db.all(query, params, (err, rows) => {
109
+ if (err) return reject(err)
110
+ resolve(rows.map((row) => ({
111
+ id: row.id,
112
+ channel: row.channel,
113
+ message: row.message,
114
+ instanceId: row.instance_id,
115
+ timestamp: row.timestamp,
116
+ metadata: row.metadata ? JSON.parse(row.metadata) : undefined,
117
+ })))
118
+ })
119
+ })
120
+ },
121
+
122
+ async storeRecords(records) {
123
+ if (!db) throw new Error("Database not initialized")
124
+ if (records.length === 0) return
125
+ return new Promise((resolve, reject) => {
126
+ db.serialize(() => {
127
+ db.run("BEGIN TRANSACTION")
128
+ const stmt = db.prepare(
129
+ `INSERT OR REPLACE INTO records (record_id, version, value, timestamp) VALUES (?, ?, ?, ?)`
130
+ )
131
+ try {
132
+ for (const record of records) {
133
+ stmt.run(record.recordId, record.version, record.value, record.timestamp)
134
+ }
135
+ stmt.finalize()
136
+ db.run("COMMIT", (err) => {
137
+ if (err) { db.run("ROLLBACK"); reject(err) }
138
+ else resolve()
139
+ })
140
+ } catch (err) { db.run("ROLLBACK"); reject(err) }
141
+ })
142
+ })
143
+ },
144
+
145
+ async getRecords(pattern) {
146
+ if (!db) throw new Error("Database not initialized")
147
+ const sqlPattern = convertToSqlPattern(pattern)
148
+ return new Promise((resolve, reject) => {
149
+ db.all(
150
+ `SELECT record_id, version, value, timestamp FROM records WHERE record_id LIKE ? ORDER BY timestamp DESC`,
151
+ [sqlPattern],
152
+ (err, rows) => {
153
+ if (err) return reject(err)
154
+ resolve(rows.map((row) => ({
155
+ recordId: row.record_id,
156
+ version: row.version,
157
+ value: row.value,
158
+ timestamp: row.timestamp,
159
+ })))
160
+ }
161
+ )
162
+ })
163
+ },
164
+
165
+ async close() {
166
+ if (!db) return
167
+ return new Promise((resolve, reject) => {
168
+ db.close((err) => {
169
+ if (err) return reject(err)
170
+ db = null
171
+ initialized = false
172
+ resolve()
173
+ })
174
+ })
175
+ },
176
+ }
177
+ }