topazcube 0.0.3 → 0.1.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.
package/src/client.js DELETED
@@ -1,251 +0,0 @@
1
- import { applyOperation, applyPatch } from "fast-json-patch"
2
- import { encode, decode } from "@msgpack/msgpack"
3
- import { reactive, opmsg, msgop } from "./utils.js"
4
-
5
- export default class TopazCubeClient {
6
- CYCLE = 100 // update/patch rate in ms
7
- url = ""
8
- documents = {}
9
- autoReconnect = true
10
- allowSync = true
11
- isConnected = false
12
- isConnecting = false
13
- isPatched = false
14
- stats = {
15
- send: 0,
16
- rec: 0,
17
- sendBps: 0,
18
- recBps: 0,
19
- ping: 0,
20
- stdiff: 0, // server time difference
21
- }
22
- lastFullState = 0
23
- lastPatch = 0
24
- le = true // Server is little endian
25
- _documentChanges = {}
26
- constructor({
27
- url, // server url
28
- }) {
29
- this.url = url
30
- this.socket = null
31
- this._startLoop()
32
- }
33
-
34
- /*= UPDATE ===================================================================*/
35
-
36
- _startLoop() {
37
- if (this._loopiv) {
38
- clearInterval(this._loopiv)
39
- }
40
- this._loopiv = setInterval(() => {
41
- this._loop()
42
- }, this.CYCLE)
43
- }
44
-
45
- _loop() {
46
- if (!this.isConnected) {
47
- return
48
- }
49
- this._sendPatches()
50
- }
51
-
52
- _countLoop() {
53
- this._stats.sendBps = this._stats.send
54
- this._stats.recBps = this._stats.rec
55
- this._stats.send = 0
56
- this._stats.rec = 0
57
- }
58
-
59
- /*= CONNECTION ===============================================================*/
60
-
61
- subscribe(name) {
62
- this.documents[name] = {}
63
- this.send({ c: "sub", n: name })
64
- }
65
-
66
- unsubscribe(name) {
67
- this.send({ c: "unsub", n: name })
68
- delete this.documents[name]
69
- }
70
-
71
- connect() {
72
- if (this.isConnecting) {
73
- return
74
- }
75
- this.isConnecting = true
76
- this._clear()
77
- console.log("connecting...")
78
-
79
- this.socket = new WebSocket(this.url)
80
-
81
- // message received
82
- this.socket.onmessage = async (event) => {
83
- let buffer = await event.data.arrayBuffer()
84
- this.stats.rec += buffer.byteLength
85
- let message = decode(buffer)
86
- let time = Date.now()
87
- if (message.c == "full") {
88
- let name = message.n
89
- let doc = message.doc
90
- this.documents[name] = doc
91
- this.isPatched = false
92
- this.document = reactive(name, this.documents[name], this._onDocumentChange.bind(this))
93
- this.isPatched = false
94
- this.lastFullState = message.t
95
- this.le = message.le
96
- this.onChange(name)
97
- } else if (message.c == "patch") {
98
- // patch
99
- this.lastPatch = message.t
100
- let name = message.n
101
- if (message.doc) {
102
- this.isPatched = true
103
- for (let op of message.doc) {
104
- let dop = msgop(op)
105
- applyOperation(this.documents[name], dop)
106
- }
107
- this.isPatched = false
108
- }
109
- this.onChange(name)
110
- } else if (message.c == "hello") {
111
- time = Date.now()
112
- let lastct = message.ct
113
- let ping = time - lastct
114
- let stime = message.t
115
- this.stats.stdiff = stime + ping / 2 - time
116
- this.stats.ping = ping
117
- console.log("ping", ping, "ms", "stdiff", this.stats.stdiff, "ms")
118
- }
119
- }
120
-
121
- // connection closed
122
- this.socket.onclose = (event) => {
123
- this.isConnected = false
124
- this.isConnecting = false
125
- this.lastFullState = 0
126
- this.socket = null
127
- this.onDisconnect()
128
- if (this.autoReconnect) {
129
- setTimeout(() => {
130
- this._reconnect()
131
- }, 500 + Math.random() * 500)
132
- }
133
- }
134
-
135
- this.socket.onerror = (event) => {
136
- this.isConnected = false
137
- this.isConnecting = false
138
- this.lastFullState = 0
139
- this.socket = null
140
- this.onDisconnect()
141
-
142
- if (this.autoReconnect) {
143
- setTimeout(() => {
144
- this._reconnect()
145
- }, 500 + Math.random() * 500)
146
- }
147
- }
148
-
149
- this.socket.onopen = (event) => {
150
- this.isConnecting = false
151
- this.isConnected = true
152
- this.lastFullState = 0
153
- this._ping()
154
- this.onConnect()
155
- }
156
- }
157
-
158
- disconnect() {
159
- this.isConnected = false
160
- this.isConnecting = false
161
- this.lastFullState = 0
162
- this.socket.close()
163
- this.socket = null
164
- }
165
-
166
- destroy() {
167
- this.autoReconnect = false
168
- this.disconnect()
169
- this.socket = null
170
- }
171
-
172
- onConnect() {}
173
-
174
- onDisconnect() {}
175
-
176
- _clear() {
177
- this.stats.sendBps = 0
178
- this.stats.recBps = 0
179
- this.stats.send = 0
180
- this.stats.rec = 0
181
- this.documents = {}
182
- this._documentChanges = {}
183
- this.lastFullState = 0
184
- this.lastPatch = 0
185
- this.isPatched = false
186
- this.le = true
187
- }
188
-
189
- _reconnect() {
190
- if (!this.isConnected) {
191
- if (!this.isConnecting) {
192
- this.connect()
193
- }
194
- }
195
- }
196
-
197
- _ping() {
198
- if (this.isConnected) {
199
- this.send({ c: "hello", ct: Date.now() })
200
- }
201
- }
202
-
203
- /*= MESSAGES =================================================================*/
204
-
205
- onChange(name) {}
206
-
207
- send(operation) {
208
- try {
209
- let enc = encode(operation)
210
- this.stats.send += enc.byteLength
211
- this.socket.send(enc)
212
- } catch (e) {
213
- console.error("send failed", e)
214
- }
215
- }
216
-
217
- _onDocumentChange(name, op, target, path, value) {
218
- if (this.isPatched || !this.allowSync) {
219
- return
220
- }
221
- if (path.indexOf("/_") >= 0) {
222
- return
223
- }
224
- if (!this._documentChanges[name]) {
225
- this._documentChanges[name] = []
226
- }
227
- this._documentChanges[name].push(opmsg(op, target, path, value))
228
- }
229
-
230
- _sendPatches() {
231
- for (let name in this._documentChanges) {
232
- let dc = this._documentChanges[name]
233
- if (dc.length == 0) {
234
- continue
235
- }
236
- let record = {
237
- n: name,
238
- c: "sync",
239
- ct: Date.now(),
240
- }
241
-
242
- if (dc.length > 0) {
243
- record.p = dc
244
- }
245
- this.send(record)
246
- this._documentChanges[name].length = 0
247
- this.onChange(name)
248
- }
249
- }
250
-
251
- }
package/src/server.js DELETED
@@ -1,335 +0,0 @@
1
- import https from "https"
2
- import fs from "fs"
3
- import { reactive, clonewo_, opmsg, msgop, getUID } from "./utils.js"
4
- import fastjsonpatch from "fast-json-patch"
5
- import { encode, decode } from "@msgpack/msgpack"
6
- import WebSocket, { WebSocketServer } from "ws"
7
- import { MongoClient } from "mongodb"
8
-
9
- const { applyPatch, applyOperation, observe } = fastjsonpatch
10
-
11
- const LITTLE_ENDIAN = (() => {
12
- const buffer = new ArrayBuffer(2)
13
- new DataView(buffer).setInt16(0, 256, true)
14
- return new Int16Array(buffer)[0] === 256
15
- })()
16
-
17
- export default class TopazCubeServer {
18
- allowSync = true // allow clients to sync their changes (no server authorization)
19
- CYCLE = 100 // update/patch rate in ms
20
- clients = new Set()
21
- _documentChanges = {}
22
- documents = {}
23
- isLoading = {}
24
-
25
- constructor({ port = 8799, https = false }) {
26
- this.port = port
27
- if (https) {
28
- const httpsServer = https.createServer({
29
- key: fs.readFileSync("./.cert/privkey.pem"),
30
- cert: fs.readFileSync("./.cert/fullchain.pem"),
31
- })
32
- httpsServer.listen(this.port)
33
- this.wss = new WebSocketServer({ server: httpsServer })
34
- console.log("TopazCubeServer running on HTTPS port " + this.port)
35
- } else {
36
- this.wss = new WebSocketServer({ port: this.port })
37
- console.log("TopazCubeServer running on port " + this.port)
38
- }
39
- this.wss.on("connection", (client) => {
40
- this._onConnected(client)
41
- })
42
-
43
- this._initDB()
44
- this._exited = false
45
- process.stdin.resume()
46
- process.on("SIGINT", () => {
47
- this._exitSignal("SIGINT")
48
- })
49
- process.on("SIGQUIT", () => {
50
- this._exitSignal("SIGQUIT")
51
- })
52
- process.on("SIGTERM", () => {
53
- this._exitSignal("SIGTERM")
54
- })
55
- process.on("SIGUSR2", () => {
56
- this._exitSignal("SIGUSR2")
57
- })
58
- this._startLoop()
59
- }
60
-
61
- canCreate(client, name) {
62
- return true;
63
- }
64
-
65
- onCreate(name) {
66
- return {
67
- data: {}
68
- }
69
- }
70
-
71
- // to be redefined, to be called when a new document is hydrated
72
- // (created, or loaded from db)
73
- onHydrate(name, doc) {
74
- }
75
-
76
- _makeReactive(name) {
77
- //console.log(`Making document '${name}' reactive`, this.documents[name])
78
- this.documents[name] = reactive(name, this.documents[name], this._onDocumentChange.bind(this))
79
- }
80
-
81
- _createEmptyDocument(name) {
82
- let doc = this.onCreate(name)
83
- if (!doc) {
84
- return
85
- }
86
- this.documents[name] = doc
87
- }
88
-
89
- async _waitLoad(name) {
90
- if (this.isLoading[name]) {
91
- while (this.isLoading[name]) {
92
- await new Promise(resolve => setTimeout(resolve, 50));
93
- }
94
- }
95
- }
96
-
97
- async _checkDocument(name, client) {
98
- await this._waitLoad(name)
99
- if (!this.documents[name]) {
100
- this.isLoading[name] = true;
101
- await this._loadDocument(name);
102
- if (!this.documents[name] && this.canCreate(client, name)) {
103
- this._createEmptyDocument(name)
104
- }
105
- if (this.documents[name]) {
106
- if (!this._documentChanges[name]) {
107
- this._documentChanges[name] = []
108
- }
109
- this._makeReactive(name)
110
- this.onHydrate(name, this.documents[name])
111
- }
112
- this.isLoading[name] = false
113
- }
114
- }
115
-
116
- /*= UPDATE =================================================================*/
117
-
118
- // to be redefined. called every 1/20s
119
- onUpdate(name, doc, dt) {}
120
-
121
- _startLoop() {
122
- this.lastUpdate = Date.now()
123
- if (this._loopiv) {
124
- clearInterval(this._loopiv)
125
- }
126
- this._loopiv = setInterval(() => {
127
- this._loop()
128
- }, this.CYCLE)
129
- }
130
-
131
- _loop() {
132
- let dt = Date.now() - this.lastUpdate
133
- for (let name in this.documents) {
134
- this.onUpdate(name, this.documents[name], dt)
135
- }
136
- this.lastUpdate = Date.now()
137
- this._sendPatches()
138
- }
139
-
140
- /*= MESSAGES ===============================================================*/
141
-
142
- // to be redefined. Called on message (operation) from client
143
- onMessage(client, message) {}
144
-
145
- // to be redefined. Called when a client connects
146
- onConnect(client) {}
147
-
148
- // to be redefined. Called when a client disconnects
149
- onDisconnect(client) {}
150
-
151
- _onConnected(client) {
152
- client.ID = getUID()
153
- client.subscribed = {}
154
- console.log("client connected", client.ID)
155
- this.clients.add(client)
156
- client.on("error", () => {
157
- this._onError(client, arguments)
158
- })
159
- client.on("message", (message) => {
160
- let dec = decode(message)
161
- this._onMessage(client, dec)
162
- })
163
- client.on("close", (message) => {
164
- this._onDisconnected(client)
165
- this.onDisconnect(client)
166
- })
167
- this.onConnect(client)
168
- }
169
-
170
- async _onMessage(client, message) {
171
- if (message.c == "hello") {
172
- //console.log('client hello')
173
- this.send(client, { c: "hello", t: Date.now(), ct: message.ct })
174
- } else if (message.c == "sync" && this.allowSync && client.subscribed[message.n] && this.documents[message.n]) {
175
- let name = message.n
176
- if (!this._documentChanges[name]) {
177
- this._documentChanges[name] = []
178
- }
179
- for (let op of message.p) {
180
- this._documentChanges[name].push(op)
181
- let dop = msgop(op)
182
- applyOperation(this.documents[name], dop)
183
- }
184
- } else if (message.c == "sub") {
185
- await this._checkDocument(message.n, client)
186
- if (!this.documents[message.n]) {
187
- this.send(client, { c: "error", t: Date.now(), message: "Document not found" })
188
- return
189
- }
190
- client.subscribed[message.n] = true
191
- this._sendFullState(message.n, client)
192
- } else if (message.c == "unsub") {
193
- client.subscribed[message.n] = false
194
- } else {
195
- this.onMessage(client, message)
196
- }
197
- }
198
-
199
- _onError(client, args) {
200
- console.error("onError:", args)
201
- }
202
-
203
- _onDisconnected(client) {
204
- console.log("client disconnected")
205
- }
206
-
207
- send(client, message) {
208
- let enc = encode(message)
209
- client.send(enc)
210
- }
211
-
212
- broadcast(message) {
213
- let enc = encode(message)
214
- for (let client of this.clients) {
215
- client.send(enc)
216
- }
217
- }
218
-
219
- async _sendFullState(name, client) {
220
- await this._waitLoad(name)
221
- let fullState = {
222
- c: "full",
223
- le: LITTLE_ENDIAN,
224
- t: Date.now(),
225
- n: name,
226
- doc: clonewo_(this.documents[name]),
227
- }
228
- this.send(client, fullState)
229
- }
230
-
231
- _sendPatches() {
232
- let now = Date.now()
233
-
234
- for (let name in this._documentChanges) {
235
- let dc = this._documentChanges[name];
236
- if (dc.length > 0) {
237
- let record = {
238
- c: "patch",
239
- t: now, // server time
240
- n: name,
241
- doc: dc
242
- }
243
- for (let client of this.clients) {
244
- if (client.subscribed[name]) {
245
- this.send(client, record)
246
- }
247
- }
248
- this._documentChanges[name] = []
249
- }
250
- }
251
- }
252
-
253
- _onDocumentChange(name, op, target, path, value) {
254
- if (path.indexOf("/_") >= 0) {
255
- return
256
- }
257
- this._documentChanges[name].push(opmsg(op, target, path, value))
258
- }
259
-
260
- async _initDB() {
261
- await this._connectDB()
262
- }
263
-
264
- async _connectDB() {
265
- this.mongoClient = new MongoClient("mongodb://localhost:27017")
266
- try {
267
- await this.mongoClient.connect()
268
- console.log("Connected to MongoDB")
269
- const db = this.mongoClient.db("topazcube")
270
- this._DB = db.collection("documents")
271
- } catch (error) {
272
- console.error("Error connecting to MongoDB:", error)
273
- this.mongoClient = null
274
- }
275
- }
276
-
277
- async _loadDocument(name) {
278
- if (this._DB) {
279
- try {
280
- const doc = await this._DB.findOne({ name: name })
281
- if (doc) {
282
- delete doc._id
283
- this.documents[name] = doc
284
- }
285
- } catch (error) {
286
- console.error("Error loading document from MongoDB:", error)
287
- }
288
- } else {
289
- console.warn("MongoDB client not initialized. Document not loaded.")
290
- }
291
- }
292
-
293
- async _saveDocument(name) {
294
- if (this._DB) {
295
- try {
296
- const doc = this.documents[name]
297
- let newdoc = clonewo_(doc, "__")
298
- console.log(`Saving document '${name}' to MongoDB`)
299
- await this._DB.updateOne({ name: name }, { $set: newdoc }, { upsert: true })
300
- console.log("Document saved to MongoDB")
301
- } catch (error) {
302
- console.error("Error saving document to MongoDB:", error)
303
- }
304
- } else {
305
- console.warn("MongoDB client not initialized. Document not saved.")
306
- }
307
- }
308
-
309
- async _saveAllDocuments() {
310
- for (let name in this.documents) {
311
- await this._saveDocument(name)
312
- }
313
- }
314
-
315
- async onHydrate(name, document) {
316
- document._hydrated = true
317
- }
318
-
319
- _exitSignal(signal) {
320
- if (!this._exited) {
321
- console.log("\nEXIT: Caught interrupt signal " + signal)
322
- this._exited = true
323
- clearInterval(this._loopiv)
324
- this.onBeforeExit()
325
- this.broadcast({ server: "Going down" })
326
- this._saveAllDocuments()
327
- this.wss.close()
328
- setTimeout(() => process.exit(0), 1000)
329
- }
330
- }
331
-
332
- // Called on program exit
333
-
334
- onBeforeExit() {}
335
- }