@mixd-id/web-scaffold 0.1.240411106 → 0.1.240411108

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@mixd-id/web-scaffold",
3
3
  "private": false,
4
- "version": "0.1.240411106",
4
+ "version": "0.1.240411108",
5
5
  "scripts": {
6
6
  "dev": "vite serve",
7
7
  "build": "vite build",
@@ -785,7 +785,7 @@ export default{
785
785
  const key = kk.substring(1)
786
786
  const presetColumn = this.preset.columns.find(_ => _.key === key)
787
787
  let value = vv
788
- switch(presetColumn.type){
788
+ switch(presetColumn?.type){
789
789
 
790
790
  case 'bool':
791
791
  case 'boolean':
package/src/utils/wss.js CHANGED
@@ -63,6 +63,22 @@ const revertDataUrlObj = (obj) => {
63
63
  return output
64
64
  }
65
65
 
66
+ // Expected rejections when a redis client is torn down while commands are
67
+ // still queued (e.g. socket closes mid-subscribe) — safe to ignore.
68
+ const isClosedError = (e) =>
69
+ e && (e.message === 'Disconnects client' || e.message === 'The client is closed')
70
+
71
+ const reportError = (e) => {
72
+ if(isClosedError(e)) return
73
+ '$report' in process ? process.$report(e) : console.error(e)
74
+ }
75
+
76
+ const disconnectQuietly = (client) => {
77
+ if(client && client.isOpen){
78
+ client.disconnect().catch(reportError)
79
+ }
80
+ }
81
+
66
82
  class WSS extends EventEmitter2{
67
83
 
68
84
  _instance
@@ -104,11 +120,23 @@ class WSS extends EventEmitter2{
104
120
  port: process.env.REDIS_PORT ?? 6379
105
121
  }
106
122
  })
107
- this._client.connect().then()
123
+ // Without an 'error' listener node-redis errors crash the process
124
+ this._client.on('error', reportError)
125
+ this._client.connect().catch(reportError)
126
+
127
+ this._instance.on('error', reportError)
108
128
 
109
129
  this._instance.on('connection', async (socket, req) => {
110
130
  socket.id = `${++this._connectionId}`
111
131
  socket._retryCount = 0
132
+ socket.path = req.url
133
+ socket.headers = req.headers
134
+
135
+ // Protocol errors (e.g. invalid close codes) emit 'error'; without a
136
+ // listener they crash the process. ws closes the socket afterwards.
137
+ socket.on('error', reportError)
138
+
139
+ let closed = false
112
140
 
113
141
  if(`${req.url}`.indexOf('debug-ws') > 0){
114
142
  this._debugIds[socket.id] = {}
@@ -117,9 +145,17 @@ class WSS extends EventEmitter2{
117
145
  let subscriber
118
146
 
119
147
  const listener = (message) => {
120
- const { model, event, items } = JSON.parse(message)
148
+ let payload
149
+ try{
150
+ payload = JSON.parse(message)
151
+ }
152
+ catch(e){
153
+ return reportError(e)
154
+ }
155
+ const { model, event, items } = payload ?? {}
121
156
  this.toBinaryData({ data:{ model, event, items } }, !!this._debugIds[socket.id])
122
157
  .then(data => socket.send(data))
158
+ .catch(reportError)
123
159
  }
124
160
 
125
161
  socket.leave = (channel) => {
@@ -128,7 +164,7 @@ class WSS extends EventEmitter2{
128
164
  }*/
129
165
 
130
166
  if(subscriber){
131
- subscriber.unsubscribe(channel, listener)
167
+ subscriber.unsubscribe(channel, listener).catch(reportError)
132
168
  }
133
169
  }
134
170
 
@@ -139,83 +175,25 @@ class WSS extends EventEmitter2{
139
175
  socket.channels[channel] = 1*/
140
176
 
141
177
  if(subscriber) {
142
- subscriber.subscribe(channel, listener)
178
+ subscriber.subscribe(channel, listener).catch(reportError)
143
179
  }
144
180
  }
145
181
 
146
182
  socket.to = (channel) => {
147
183
  return {
148
184
  emit: (model, event, items) => {
149
- this.broadcast(channel, { model, event, items }).then()
185
+ this.broadcast(channel, { model, event, items }).catch(reportError)
150
186
  }
151
187
  }
152
188
  }
153
189
 
154
- socket.on('message', async (binaryData) => {
155
- if(!socket.isAuth) return
156
-
157
- let obj
158
- try{
159
- obj = await this.fromBinaryData(binaryData, !!this._debugIds[socket.id]);
160
- }
161
- catch(e){
162
- return socket.close(1002, e.message);
163
- }
164
-
165
- const { _requestId, path, params, ping, pong } = obj
166
-
167
- if(pong){
168
- delete socket.pinging
169
- socket._retryCount = 0
170
- }
171
- else if(ping){
172
- socket.send(await this.toBinaryData({ pong: 1 }, !!this._debugIds[socket.id]))
173
- }
174
- else{
175
- let status = 200
176
- let data
177
- switch(path){
178
-
179
- default:
180
- try{
181
- const arr = (await Promise.all(this._reqFn.map(fn => fn({ path, params }, socket))))
182
- .filter(_ => _)
183
-
184
- data = arr.length > 0 ? JSON.parse(JSON.stringify(arr.pop())) : data
185
- }
186
- catch(e){
187
-
188
- if(e.response){
189
- '$report' in process ? process.$report(e) : console.error(e.response.status, e.response.data)
190
- }
191
- else{
192
- '$report' in process ? process.$report(e) : console.error(e)
193
- }
194
-
195
- status = 500
196
- data = {
197
- name: e.name,
198
- message: e.message,
199
- errors: e.response ? e.response.data : e.errors,
200
- stack: process.env.APP_DEBUG === 'true' ?
201
- e.stack : undefined,
202
- }
203
- }
204
- break
205
- }
206
-
207
- socket.send(await this.toBinaryData({
208
- _requestId,
209
- status,
210
- data
211
- }, !!this._debugIds[socket.id]))
212
- }
190
+ socket.on('message', (binaryData) => {
191
+ this._onMessage(socket, binaryData).catch(reportError)
213
192
  });
214
193
 
215
194
  socket.on('close', (e) => {
216
- if(subscriber){
217
- subscriber.disconnect()
218
- }
195
+ closed = true
196
+ disconnectQuietly(subscriber)
219
197
  if(this._debugIds[socket.id]){
220
198
  delete this._debugIds[socket.id]
221
199
  }
@@ -232,45 +210,64 @@ class WSS extends EventEmitter2{
232
210
  await fn(token, socket)
233
211
  }
234
212
 
235
- subscriber = this._client.duplicate()
236
- await subscriber.connect()
213
+ const client = this._client.duplicate()
214
+ client.on('error', reportError)
215
+ subscriber = client
216
+ await client.connect()
217
+
218
+ // Socket closed while authenticating/connecting: the close handler may
219
+ // have run before the client was open, so release it here.
220
+ if(closed){
221
+ disconnectQuietly(client)
222
+ return
223
+ }
237
224
 
238
225
  socket.isAuth = true
239
226
  socket.send(await this.toBinaryData({ auth:true }, !!this._debugIds[socket.id]))
240
227
  }
241
228
  catch(e){
229
+ disconnectQuietly(subscriber)
230
+ if(closed) return
242
231
  return socket.close(1002, (e.message ?? '').substring(0, 123));
243
232
  }
244
233
 
245
- if(this._opt.ping !== false){
234
+ if(this._opt.ping !== false && !closed){
246
235
  let pingTimer
247
236
 
237
+ const schedule = (fn, ms) => {
238
+ if(!closed) pingTimer = setTimeout(fn, ms)
239
+ }
240
+
248
241
  const ping = async() => {
249
- if(socket.readyState !== WebSocket.OPEN) return
242
+ if(closed || socket.readyState !== WebSocket.OPEN) return
250
243
 
251
- socket.send(await this.toBinaryData({ ping:1 }, !!this._debugIds[socket.id]))
244
+ try{
245
+ socket.send(await this.toBinaryData({ ping:1 }, !!this._debugIds[socket.id]))
246
+ }
247
+ catch(e){
248
+ return reportError(e)
249
+ }
252
250
  socket.pinging = true
253
251
 
254
- pingTimer = setTimeout(() => {
252
+ schedule(() => {
255
253
  if(socket.pinging){
256
254
  socket._retryCount++
257
255
 
258
256
  if(socket._retryCount > 10){
259
- socket.close(1002, 'ping timeout')
260
- if(subscriber){
261
- subscriber.disconnect()
262
- }
257
+ // Peer is unresponsive, a close handshake would just hang;
258
+ // terminate so 'close' fires now and releases the subscriber.
259
+ socket.terminate()
263
260
  }
264
261
  else{
265
- pingTimer = setTimeout(ping, 3000)
262
+ schedule(ping, 3000)
266
263
  }
267
264
  }
268
265
  else{
269
- pingTimer = setTimeout(ping, (Math.round(Math.random() * 10) % 5) * 5000)
266
+ schedule(ping, (Math.round(Math.random() * 10) % 5) * 5000)
270
267
  }
271
268
  }, 3000)
272
269
  }
273
- pingTimer = setTimeout(ping, (Math.round(Math.random() * 10) % 5) * 5000)
270
+ schedule(ping, (Math.round(Math.random() * 10) % 5) * 5000)
274
271
 
275
272
  socket.on('close', () => {
276
273
  clearTimeout(pingTimer)
@@ -279,6 +276,68 @@ class WSS extends EventEmitter2{
279
276
  })
280
277
  }
281
278
 
279
+ async _onMessage(socket, binaryData){
280
+ if(!socket.isAuth) return
281
+
282
+ let obj
283
+ try{
284
+ obj = await this.fromBinaryData(binaryData, !!this._debugIds[socket.id]);
285
+ if(typeof obj !== 'object' || obj === null) throw new Error('Invalid message')
286
+ }
287
+ catch(e){
288
+ return socket.close(1002, (e.message ?? '').substring(0, 123));
289
+ }
290
+
291
+ const { _requestId, path, params, ping, pong } = obj
292
+
293
+ if(pong){
294
+ delete socket.pinging
295
+ socket._retryCount = 0
296
+ }
297
+ else if(ping){
298
+ socket.send(await this.toBinaryData({ pong: 1 }, !!this._debugIds[socket.id]))
299
+ }
300
+ else{
301
+ let status = 200
302
+ let data
303
+ switch(path){
304
+
305
+ default:
306
+ try{
307
+ const arr = (await Promise.all(this._reqFn.map(fn => fn({ path, params }, socket))))
308
+ .filter(_ => _)
309
+
310
+ data = arr.length > 0 ? JSON.parse(JSON.stringify(arr.pop())) : data
311
+ }
312
+ catch(e){
313
+
314
+ if(e.response){
315
+ '$report' in process ? process.$report(e) : console.error(e.response.status, e.response.data)
316
+ }
317
+ else{
318
+ '$report' in process ? process.$report(e) : console.error(e)
319
+ }
320
+
321
+ status = 500
322
+ data = {
323
+ name: e.name,
324
+ message: e.message,
325
+ errors: e.response ? e.response.data : e.errors,
326
+ stack: process.env.APP_DEBUG === 'true' ?
327
+ e.stack : undefined,
328
+ }
329
+ }
330
+ break
331
+ }
332
+
333
+ socket.send(await this.toBinaryData({
334
+ _requestId,
335
+ status,
336
+ data
337
+ }, !!this._debugIds[socket.id]))
338
+ }
339
+ }
340
+
282
341
  stats(){
283
342
  let authenticated = 0
284
343
  this._instance.clients.forEach(socket => {
@@ -296,25 +355,34 @@ class WSS extends EventEmitter2{
296
355
 
297
356
  clusterStats(){
298
357
  return new Promise((resolve) => {
299
- const workers = {}
300
- const timeout = setTimeout(() => resolve(this._aggregateStats(workers)), 3000)
358
+ // Include own stats
359
+ const workers = { [process.pid]: this.stats() }
301
360
 
302
- process.on('message', (packet) => {
303
- if(packet.type === 'process:msg' && packet.data?.type === 'wss:stats:response'){
361
+ if(!process.send){
362
+ return resolve(this._aggregateStats(workers))
363
+ }
364
+
365
+ // Listener must be removed once done, otherwise every call leaks one
366
+ const finish = () => {
367
+ clearTimeout(timeout)
368
+ process.removeListener('message', onMessage)
369
+ resolve(this._aggregateStats(workers))
370
+ }
371
+
372
+ const onMessage = (packet) => {
373
+ if(packet?.type === 'process:msg' && packet.data?.type === 'wss:stats:response'){
304
374
  workers[packet.data.pid] = packet.data.stats
305
375
  }
306
376
 
307
377
  const expected = parseInt(process.env.instances || '0', 10)
308
378
  if(expected > 0 && Object.keys(workers).length >= expected){
309
- clearTimeout(timeout)
310
- resolve(this._aggregateStats(workers))
379
+ finish()
311
380
  }
312
- })
381
+ }
313
382
 
383
+ const timeout = setTimeout(finish, 3000)
384
+ process.on('message', onMessage)
314
385
  process.send({ type: 'process:msg', data: { type: 'wss:stats:request' } })
315
-
316
- // Include own stats
317
- workers[process.pid] = this.stats()
318
386
  })
319
387
  }
320
388
 
@@ -331,9 +399,10 @@ class WSS extends EventEmitter2{
331
399
  }
332
400
 
333
401
  initClusterStats(){
334
- if(process.send){
402
+ if(process.send && !this._clusterStatsInit){
403
+ this._clusterStatsInit = true
335
404
  process.on('message', (packet) => {
336
- if(packet.type === 'process:msg' && packet.data?.type === 'wss:stats:request'){
405
+ if(packet?.type === 'process:msg' && packet.data?.type === 'wss:stats:request'){
337
406
  process.send({
338
407
  type: 'process:msg',
339
408
  data: { type: 'wss:stats:response', pid: process.pid, stats: this.stats() }