@logux/server 0.16.2 → 0.16.3

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.
@@ -238,6 +238,13 @@ export interface BaseServerOptions {
238
238
  */
239
239
  subprotocol?: number
240
240
 
241
+ /**
242
+ * How long an action can stay in the processing queue of the client
243
+ * before the server answers `logux/undo` and moves to the next action.
244
+ * `300000` (5 minutes) by default, `0` disables it.
245
+ */
246
+ queueTimeout?: number
247
+
241
248
  /**
242
249
  * How many actions could be sent in a single message. `100` by default.
243
250
  *
@@ -602,6 +609,7 @@ interface ReportersArguments {
602
609
  }
603
610
  unsubscribed: SubscriptionReporter
604
611
  useless: ActionReporter
612
+ duplicate: ActionReporter
605
613
  wrongChannel: SubscriptionReporter
606
614
  zombie: {
607
615
  nodeId: string
@@ -120,6 +120,9 @@ export class BaseServer {
120
120
  constructor(opts = {}) {
121
121
  this.options = opts
122
122
  this.env = this.options.env || process.env.NODE_ENV || 'development'
123
+ if (typeof this.options.queueTimeout === 'undefined') {
124
+ this.options.queueTimeout = 5 * 60 * 1000
125
+ }
123
126
 
124
127
  if (typeof this.options.subprotocol === 'undefined') {
125
128
  throw optionError('Missed `subprotocol` option in server constructor')
@@ -326,6 +329,7 @@ export class BaseServer {
326
329
  this.typeToQueue = new Map()
327
330
  this.queues = new Map()
328
331
  this.actionToQueue = new Map()
332
+ this.queueTimers = new Map()
329
333
 
330
334
  this.httpListeners = {}
331
335
  this.httpAllListeners = []
@@ -334,6 +338,8 @@ export class BaseServer {
334
338
  this.listenNotes = {}
335
339
 
336
340
  let end = (actionId, queue, queueKey, ...args) => {
341
+ clearTimeout(this.queueTimers.get(actionId))
342
+ this.queueTimers.delete(actionId)
337
343
  this.actionToQueue.delete(actionId)
338
344
  if (queue.length() === 0) {
339
345
  this.queues.delete(queueKey)
@@ -346,17 +352,21 @@ export class BaseServer {
346
352
  for (let task of remainingTasks) {
347
353
  this.undo(task.action, task.meta, 'error')
348
354
  this.actionToQueue.delete(task.meta.id)
355
+ // The core waits for it to answer `synced`
356
+ task.onReceiveResolve(false)
349
357
  }
350
358
  }
351
359
  queue.killAndDrain()
352
360
  }
361
+ this.releaseQueue = (actionId, error) => {
362
+ let queueKey = this.actionToQueue.get(actionId)
363
+ if (!queueKey) return
364
+ let queue = this.queues.get(queueKey)
365
+ if (error) undoRemainingTasks(queue)
366
+ end(actionId, queue, queueKey, error ?? null)
367
+ }
353
368
  this.on('error', (e, action, meta) => {
354
- let queueKey = this.actionToQueue.get(meta?.id)
355
- if (queueKey) {
356
- let queue = this.queues.get(queueKey)
357
- undoRemainingTasks(queue)
358
- end(meta.id, queue, queueKey, e)
359
- }
369
+ if (meta) this.releaseQueue(meta.id, e)
360
370
  })
361
371
  this.on('processed', (action, meta) => {
362
372
  if (action.type === 'logux/undo') {
@@ -654,6 +664,26 @@ export class BaseServer {
654
664
  })
655
665
  }
656
666
 
667
+ /**
668
+ * The client re-sent the action, which the log already has: the server
669
+ * stored it, but the client did not get the answer. Answer again
670
+ * and release the queue of the client.
671
+ */
672
+ async resolveDuplicate(action, meta) {
673
+ let [stored, storedMeta] = await this.log.byId(meta.id)
674
+ this.emitter.emit('report', 'duplicate', {
675
+ actionId: meta.id,
676
+ status: storedMeta ? storedMeta.status : undefined
677
+ })
678
+ if (stored && storedMeta.status === 'processed') {
679
+ // The `processed` event of the answer releases the queue
680
+ this.markAsProcessed(storedMeta)
681
+ } else {
682
+ // Still processing or already answered by `logux/undo`
683
+ this.releaseQueue(meta.id)
684
+ }
685
+ }
686
+
657
687
  markAsProcessed(meta) {
658
688
  this.log.changeMeta(meta.id, { status: 'processed' })
659
689
  let data = parseId(meta.id)
@@ -79,6 +79,8 @@ const REPORTERS = {
79
79
 
80
80
  destroy: () => ({ msg: 'Shutting down Logux server' }),
81
81
 
82
+ duplicate: () => ({ level: 'warn', msg: 'Action was already received' }),
83
+
82
84
  disconnect: () => ({ msg: 'Client was disconnected' }),
83
85
 
84
86
  error: record => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@logux/server",
3
- "version": "0.16.2",
3
+ "version": "0.16.3",
4
4
  "description": "Build own Logux server",
5
5
  "keywords": [
6
6
  "collaborative",
@@ -25,7 +25,7 @@
25
25
  },
26
26
  "dependencies": {
27
27
  "@logux/actions": "^0.7.3",
28
- "@logux/core": "^0.11.7",
28
+ "@logux/core": "^0.11.8",
29
29
  "cookie": "^2.0.1",
30
30
  "fastq": "^1.20.3",
31
31
  "nanoevents": "^10.0.0",
@@ -30,9 +30,21 @@ function denyBack(app, clientId, action, meta) {
30
30
  }
31
31
 
32
32
  async function queueWorker(task, next) {
33
- let { action, app, clientId, meta, onReceiveResolve, queue } = task
33
+ let { action, app, client, clientId, meta, onReceiveResolve, queue } = task
34
34
  queue.next = next
35
35
 
36
+ if (app.options.queueTimeout) {
37
+ let timer = setTimeout(() => {
38
+ let error = new Error(
39
+ `Action "${meta.id}" was not processed in ${app.options.queueTimeout} ms`
40
+ )
41
+ app.undo(action, meta, 'error')
42
+ app.emitter.emit('error', error, action, meta)
43
+ }, app.options.queueTimeout)
44
+ if (timer.unref) timer.unref()
45
+ app.queueTimers.set(meta.id, timer)
46
+ }
47
+
36
48
  let type = action.type
37
49
  if (type === 'logux/subscribe' || type === 'logux/unsubscribe') {
38
50
  return onReceiveResolve([action, meta])
@@ -56,7 +68,14 @@ async function queueWorker(task, next) {
56
68
  denyBack(app, clientId, action, meta)
57
69
  return onReceiveResolve(false)
58
70
  } else {
59
- return onReceiveResolve([action, meta])
71
+ // The queue is released by the `processed` event of the action,
72
+ // which never comes for the action the log already has: `log.add()`
73
+ // returns `false` and no `batch` event fires. The core ignores
74
+ // `false`, so the action is added here to see the result
75
+ if (client.node.received) client.node.received[meta.id] = true
76
+ let added = await app.log.add(action, meta)
77
+ if (added === false) await app.resolveDuplicate(action, meta)
78
+ return onReceiveResolve(false)
60
79
  }
61
80
  } catch (e) {
62
81
  app.undo(action, meta, 'error')
@@ -315,6 +334,7 @@ export class ServerClient {
315
334
  queue.push({
316
335
  action,
317
336
  app: this.app,
337
+ client: this,
318
338
  clientId,
319
339
  meta,
320
340
  onReceiveResolve: result => {
package/vite.config.ts DELETED
@@ -1,25 +0,0 @@
1
- import { defineConfig } from 'vitest/config'
2
-
3
- export default defineConfig({
4
- test: {
5
- coverage: {
6
- exclude: [
7
- '**/node_modules/**',
8
- 'server/index.js',
9
- 'test/**',
10
- '**/*.d.ts',
11
- '**/*.test.ts',
12
- '**/errors.ts',
13
- '**/types.ts',
14
- '*.config.*',
15
- 'human-formatter/**'
16
- ],
17
- provider: 'v8',
18
- thresholds: {
19
- lines: 100
20
- }
21
- },
22
- environment: 'node',
23
- exclude: ['**/node_modules/**', 'test/servers/**']
24
- }
25
- })