@logux/server 0.14.0 → 0.16.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.
@@ -5,17 +5,47 @@ import { nanoid } from 'nanoid'
5
5
  import { readFile } from 'node:fs/promises'
6
6
  import { dirname, join } from 'node:path'
7
7
  import { fileURLToPath } from 'node:url'
8
- import UrlPattern from 'url-pattern'
9
8
  import { WebSocketServer } from 'ws'
10
9
 
11
10
  import { addHttpPages } from '../add-http-pages/index.js'
12
11
  import { Context } from '../context/index.js'
13
12
  import { createHttpServer } from '../create-http-server/index.js'
14
13
  import { ServerClient } from '../server-client/index.js'
14
+ import { createPattern } from '../url-pattern/index.js'
15
15
 
16
16
  const SKIP_PROCESS = Symbol('skipProcess')
17
17
  const RESEND_META = ['channels', 'users', 'clients', 'nodes']
18
18
 
19
+ function addTarget(targets, client, action, meta) {
20
+ let entries = targets.get(client)
21
+ if (entries) {
22
+ entries.push([action, meta])
23
+ } else {
24
+ targets.set(client, [[action, meta]])
25
+ }
26
+ }
27
+
28
+ function sendTargets(app, targets, settled) {
29
+ for (let [client, entries] of targets) {
30
+ let sending = client.node.onAdd(entries)
31
+ // Only `ServerClient` tracks sending for `Server#drain()`
32
+ if (client.track) client.track(sending)
33
+ }
34
+ if (settled > 0) {
35
+ for (let client of app.connected.values()) {
36
+ if (!targets.has(client) && client.node.lastAddedCache < settled) {
37
+ client.node.lastAddedCache = settled
38
+ }
39
+ }
40
+ }
41
+ targets.clear()
42
+ }
43
+
44
+ function maxAdded(settled, meta) {
45
+ if (typeof meta.added === 'undefined' || meta.added < settled) return settled
46
+ return meta.added
47
+ }
48
+
19
49
  function optionError(msg) {
20
50
  let error = new Error(msg)
21
51
  error.logux = true
@@ -110,7 +140,7 @@ export class BaseServer {
110
140
  if (!this.options.host) this.options.host = '127.0.0.1'
111
141
  }
112
142
 
113
- this.nodeId = `server:${this.options.id || nanoid(8)}`
143
+ this.nodeId = `server:${this.options.id || nanoid(6)}`
114
144
 
115
145
  if (this.options.fileUrl) {
116
146
  this.options.root = dirname(fileURLToPath(this.options.fileUrl))
@@ -159,89 +189,64 @@ export class BaseServer {
159
189
  }
160
190
  this.replaceResendShortcuts(meta)
161
191
  })
162
- this.on('add', async (action, meta) => {
163
- let start = Date.now()
192
+ // Reports are per action, `clean` for a reason-less action is emitted
193
+ // by the log before `batch` event
194
+ this.on('add', (action, meta) => {
164
195
  if (meta.reasons.length === 0) {
165
196
  cleaned[meta.id] = true
166
197
  this.emitter.emit('report', 'addClean', { action, meta })
167
198
  } else {
168
199
  this.emitter.emit('report', 'add', { action, meta })
169
200
  }
201
+ })
202
+ this.on('batch', entries => {
203
+ let start = Date.now()
204
+ let ready = []
170
205
 
171
- if (this.destroying && !this.actionToQueue.has(meta.id)) {
172
- return
173
- }
174
-
175
- if (action.type === 'logux/subscribe') {
176
- if (meta.server === this.nodeId) {
177
- this.subscribeAction(action, meta, start)
206
+ for (let [action, meta] of entries) {
207
+ if (this.destroying && !this.actionToQueue.has(meta.id)) {
208
+ continue
178
209
  }
179
- return
180
- }
181
210
 
182
- if (action.type === 'logux/unsubscribe') {
183
- if (meta.server === this.nodeId) {
184
- this.unsubscribeAction(action, meta)
211
+ if (action.type === 'logux/subscribe') {
212
+ if (meta.server === this.nodeId) {
213
+ void this.subscribeAction(action, meta, start)
214
+ }
215
+ continue
185
216
  }
186
- return
187
- }
188
217
 
189
- let processor = this.getProcessor(action.type)
190
- if (processor && processor.resend && meta.status === 'waiting') {
191
- let ctx = this.createContext(action, meta)
192
- let resend
193
- try {
194
- resend = await processor.resend(ctx, action, meta)
195
- } catch (e) {
196
- this.undo(action, meta, 'error')
197
- this.emitter.emit('error', e, action, meta)
198
- this.finally(processor, ctx, action, meta)
199
- return
200
- }
201
- if (resend) {
202
- if (typeof resend === 'string') {
203
- resend = { channels: [resend] }
204
- } else if (Array.isArray(resend)) {
205
- resend = { channels: resend }
206
- } else {
207
- this.replaceResendShortcuts(resend)
208
- }
209
- let diff = {}
210
- for (let i of RESEND_META) {
211
- if (resend[i]) diff[i] = resend[i]
218
+ if (action.type === 'logux/unsubscribe') {
219
+ if (meta.server === this.nodeId) {
220
+ this.unsubscribeAction(action, meta)
212
221
  }
213
- await this.log.changeMeta(meta.id, diff)
214
- meta = { ...meta, ...diff }
222
+ continue
215
223
  }
216
- }
217
-
218
- if (this.isUseless(action, meta)) {
219
- this.emitter.emit('report', 'useless', { action, meta })
220
- }
221
224
 
222
- await this.sendAction(action, meta)
225
+ let processor = this.getProcessor(action.type)
226
+ if (processor && processor.resend && meta.status === 'waiting') {
227
+ // Resending is asynchronous, so the action can’t wait for others
228
+ void this.resendAction(action, meta, processor, start)
229
+ continue
230
+ }
223
231
 
224
- if (meta.status === 'waiting') {
225
- if (!processor) {
226
- this.internalUnknownType(action, meta)
227
- return
232
+ if (this.isUseless(action, meta)) {
233
+ this.emitter.emit('report', 'useless', { action, meta })
228
234
  }
229
- if (processor.process) {
230
- this.processAction(processor, action, meta, start)
231
- } else {
232
- this.emitter.emit('processed', action, meta, 0)
233
- this.finally(
234
- processor,
235
- this.createContext(action, meta),
236
- action,
237
- meta
238
- )
239
- this.markAsProcessed(meta)
235
+ ready.push([action, meta, processor])
236
+ }
237
+
238
+ if (ready.length === 0) return
239
+ let runProcessors = () => {
240
+ for (let [action, meta, processor] of ready) {
241
+ this.runProcessor(action, meta, processor, start)
240
242
  }
241
- } else {
242
- this.emitter.emit('processed', action, meta, 0)
243
- this.finally(processor, this.createContext(action, meta), action, meta)
244
243
  }
244
+ // Actions are processed only after they were sent to the clients
245
+ void Promise.resolve(this.sendBatch(ready)).then(runProcessors, error => {
246
+ // Do not stop action processing on a broken channel filter
247
+ runProcessors()
248
+ throw error
249
+ })
245
250
  })
246
251
  this.on('clean', (action, meta) => {
247
252
  if (cleaned[meta.id]) {
@@ -445,9 +450,7 @@ export class BaseServer {
445
450
  normalizeChannelCallbacks(`Channel ${pattern}`, callbacks)
446
451
  let channel = Object.assign({}, callbacks)
447
452
  if (typeof pattern === 'string') {
448
- channel.pattern = new UrlPattern(pattern, {
449
- segmentValueCharset: '^/'
450
- })
453
+ channel.pattern = createPattern(pattern)
451
454
  } else {
452
455
  channel.regexp = pattern
453
456
  }
@@ -496,6 +499,12 @@ export class BaseServer {
496
499
  return Promise.all(this.unbind.map(i => i()))
497
500
  }
498
501
 
502
+ drain(clientId) {
503
+ let client = this.clientIds.get(clientId)
504
+ if (!client) return Promise.resolve(false)
505
+ return client.drain()
506
+ }
507
+
499
508
  finally(processor, ctx, action, meta) {
500
509
  this.contexts.delete(action)
501
510
  if (processor && processor.finally) {
@@ -522,6 +531,11 @@ export class BaseServer {
522
531
  return undefined
523
532
  }
524
533
 
534
+ handleClient(ws, req) {
535
+ ws.upgradeReq = req
536
+ this.addClient(new ServerConnection(ws))
537
+ }
538
+
525
539
  http(method, url, listener) {
526
540
  if (this.options.disableHttpServer) {
527
541
  throw new Error(
@@ -624,10 +638,7 @@ export class BaseServer {
624
638
  await readFile(join(import.meta.dirname, '..', 'package.json'))
625
639
  )
626
640
 
627
- this.ws.on('connection', (ws, req) => {
628
- ws.upgradeReq = req
629
- this.addClient(new ServerConnection(ws))
630
- })
641
+ this.ws.on('connection', (ws, req) => this.handleClient(ws, req))
631
642
  this.emitter.emit('report', 'listen', {
632
643
  cert: !!this.options.cert,
633
644
  environment: this.env,
@@ -655,7 +666,12 @@ export class BaseServer {
655
666
  }
656
667
 
657
668
  on(event, listener) {
658
- if (event === 'preadd' || event === 'add' || event === 'clean') {
669
+ if (
670
+ event === 'preadd' ||
671
+ event === 'add' ||
672
+ event === 'batch' ||
673
+ event === 'clean'
674
+ ) {
659
675
  return this.log.emitter.on(event, listener)
660
676
  } else {
661
677
  return this.emitter.on(event, listener)
@@ -668,11 +684,7 @@ export class BaseServer {
668
684
  throw new Error('Callbacks for unknown channel are already defined')
669
685
  }
670
686
  let channel = Object.assign({}, callbacks)
671
- channel.pattern = {
672
- match(name) {
673
- return [name]
674
- }
675
- }
687
+ channel.pattern = name => [name]
676
688
  this.otherSubscriber = channel
677
689
  }
678
690
 
@@ -685,6 +697,7 @@ export class BaseServer {
685
697
  }
686
698
 
687
699
  performUnsubscribe(clientNodeId, action, meta) {
700
+ if (action.channel === '__proto__' || clientNodeId === '__proto__') return
688
701
  if (this.subscribers[action.channel]) {
689
702
  let subscriber = this.subscribers[action.channel][clientNodeId]
690
703
  if (subscriber) {
@@ -806,7 +819,42 @@ export class BaseServer {
806
819
  }
807
820
  }
808
821
 
809
- async sendAction(action, meta) {
822
+ async resendAction(action, meta, processor, start) {
823
+ let ctx = this.createContext(action, meta)
824
+ let resend
825
+ try {
826
+ resend = await processor.resend(ctx, action, meta)
827
+ } catch (e) {
828
+ this.undo(action, meta, 'error')
829
+ this.emitter.emit('error', e, action, meta)
830
+ this.finally(processor, ctx, action, meta)
831
+ return
832
+ }
833
+ if (resend) {
834
+ if (typeof resend === 'string') {
835
+ resend = { channels: [resend] }
836
+ } else if (Array.isArray(resend)) {
837
+ resend = { channels: resend }
838
+ } else {
839
+ this.replaceResendShortcuts(resend)
840
+ }
841
+ let diff = {}
842
+ for (let i of RESEND_META) {
843
+ if (resend[i]) diff[i] = resend[i]
844
+ }
845
+ await this.log.changeMeta(meta.id, diff)
846
+ meta = { ...meta, ...diff }
847
+ }
848
+
849
+ if (this.isUseless(action, meta)) {
850
+ this.emitter.emit('report', 'useless', { action, meta })
851
+ }
852
+
853
+ await this.sendAction(action, meta)
854
+ this.runProcessor(action, meta, processor, start)
855
+ }
856
+
857
+ resolveTargets(action, meta, targets) {
810
858
  let from = parseId(meta.id).clientId
811
859
  let ignoreClients = new Set(meta.excludeClients || [])
812
860
  ignoreClients.add(from)
@@ -816,7 +864,7 @@ export class BaseServer {
816
864
  let client = this.nodeIds.get(id)
817
865
  if (client) {
818
866
  ignoreClients.add(client.clientId)
819
- client.node.onAdd(action, meta)
867
+ addTarget(targets, client, action, meta)
820
868
  }
821
869
  }
822
870
  }
@@ -826,7 +874,7 @@ export class BaseServer {
826
874
  if (this.clientIds.has(id)) {
827
875
  let client = this.clientIds.get(id)
828
876
  ignoreClients.add(client.clientId)
829
- client.node.onAdd(action, meta)
877
+ addTarget(targets, client, action, meta)
830
878
  }
831
879
  }
832
880
  }
@@ -838,32 +886,38 @@ export class BaseServer {
838
886
  for (let client of users) {
839
887
  if (!ignoreClients.has(client.clientId)) {
840
888
  ignoreClients.add(client.clientId)
841
- client.node.onAdd(action, meta)
889
+ addTarget(targets, client, action, meta)
842
890
  }
843
891
  }
844
892
  }
845
893
  }
846
894
  }
847
895
 
848
- if (meta.channels) {
849
- for (let channel of meta.channels) {
850
- if (this.subscribers[channel]) {
851
- for (let nodeId in this.subscribers[channel]) {
852
- let clientId = parseId(nodeId).clientId
853
- if (!ignoreClients.has(clientId)) {
854
- let subscriber = this.subscribers[channel][nodeId]
855
- if (subscriber) {
856
- let ctx = this.createContext(action, meta)
857
- let client = this.clientIds.get(clientId)
858
- for (let filter of Object.values(subscriber.filters)) {
859
- filter =
860
- typeof filter === 'function'
861
- ? await filter(ctx, action, meta)
862
- : filter
863
- if (filter && client) {
864
- ignoreClients.add(clientId)
865
- client.node.onAdd(action, meta)
866
- }
896
+ if (!meta.channels) return undefined
897
+
898
+ let waiting = []
899
+ for (let channel of meta.channels) {
900
+ if (this.subscribers[channel]) {
901
+ for (let nodeId in this.subscribers[channel]) {
902
+ let clientId = parseId(nodeId).clientId
903
+ if (!ignoreClients.has(clientId)) {
904
+ let subscriber = this.subscribers[channel][nodeId]
905
+ if (subscriber) {
906
+ let ctx = this.createContext(action, meta)
907
+ let client = this.clientIds.get(clientId)
908
+ for (let filter of Object.values(subscriber.filters)) {
909
+ if (typeof filter === 'function') {
910
+ waiting.push(
911
+ Promise.resolve(filter(ctx, action, meta)).then(result => {
912
+ if (result && client && !ignoreClients.has(clientId)) {
913
+ ignoreClients.add(clientId)
914
+ addTarget(targets, client, action, meta)
915
+ }
916
+ })
917
+ )
918
+ } else if (filter && client) {
919
+ ignoreClients.add(clientId)
920
+ addTarget(targets, client, action, meta)
867
921
  }
868
922
  }
869
923
  }
@@ -871,6 +925,51 @@ export class BaseServer {
871
925
  }
872
926
  }
873
927
  }
928
+ return waiting.length > 0 ? Promise.all(waiting) : undefined
929
+ }
930
+
931
+ runProcessor(action, meta, processor, start) {
932
+ if (meta.status === 'waiting') {
933
+ if (!processor) {
934
+ this.internalUnknownType(action, meta)
935
+ return
936
+ }
937
+ if (processor.process) {
938
+ void this.processAction(processor, action, meta, start)
939
+ } else {
940
+ this.emitter.emit('processed', action, meta, 0)
941
+ this.finally(processor, this.createContext(action, meta), action, meta)
942
+ this.markAsProcessed(meta)
943
+ }
944
+ } else {
945
+ this.emitter.emit('processed', action, meta, 0)
946
+ this.finally(processor, this.createContext(action, meta), action, meta)
947
+ }
948
+ }
949
+
950
+ async sendAction(action, meta) {
951
+ let targets = new Map()
952
+ let waiting = this.resolveTargets(action, meta, targets)
953
+ if (waiting) await waiting
954
+ sendTargets(this, targets, maxAdded(0, meta))
955
+ }
956
+
957
+ sendBatch(entries) {
958
+ let targets = new Map()
959
+ let waiting = []
960
+ let settled = 0
961
+ for (let [action, meta] of entries) {
962
+ settled = maxAdded(settled, meta)
963
+ let promise = this.resolveTargets(action, meta, targets)
964
+ if (promise) waiting.push(promise)
965
+ }
966
+ if (waiting.length === 0) {
967
+ sendTargets(this, targets, settled)
968
+ return undefined
969
+ }
970
+ return Promise.all(waiting).then(() => {
971
+ sendTargets(this, targets, settled)
972
+ })
874
973
  }
875
974
 
876
975
  sendOnConnect(loader) {
@@ -887,6 +986,7 @@ export class BaseServer {
887
986
  }
888
987
 
889
988
  subscribe(nodeId, channel) {
989
+ if (channel === '__proto__' || nodeId === '__proto__') return
890
990
  if (!this.subscribers[channel] || !this.subscribers[channel][nodeId]) {
891
991
  if (!this.subscribers[channel]) {
892
992
  this.subscribers[channel] = {}
@@ -897,7 +997,7 @@ export class BaseServer {
897
997
  }
898
998
 
899
999
  async subscribeAction(action, meta, start) {
900
- if (typeof action.channel !== 'string') {
1000
+ if (typeof action.channel !== 'string' || action.channel === '__proto__') {
901
1001
  this.wrongChannel(action, meta)
902
1002
  return
903
1003
  }
@@ -910,7 +1010,7 @@ export class BaseServer {
910
1010
  let match
911
1011
  for (let channel of channels) {
912
1012
  if (channel.pattern) {
913
- match = channel.pattern.match(action.channel)
1013
+ match = channel.pattern(action.channel)
914
1014
  } else {
915
1015
  match = action.channel.match(channel.regexp)
916
1016
  }
@@ -918,6 +1018,7 @@ export class BaseServer {
918
1018
  let subscribed = false
919
1019
  if (match) {
920
1020
  let ctx = this.createContext(action, meta)
1021
+ if (ctx.nodeId === '__proto__') return
921
1022
  ctx.params = match
922
1023
  try {
923
1024
  let access = await channel.access(ctx, action, meta)
@@ -967,16 +1068,9 @@ export class BaseServer {
967
1068
  subscribed = true
968
1069
 
969
1070
  if (channel.load) {
1071
+ // Actions from a single `load()` are sent in a single message
970
1072
  let sendBack = await channel.load(ctx, action, meta)
971
- if (Array.isArray(sendBack)) {
972
- await Promise.all(
973
- sendBack.map(i => {
974
- return Array.isArray(i) ? ctx.sendBack(...i) : ctx.sendBack(i)
975
- })
976
- )
977
- } else if (sendBack) {
978
- await ctx.sendBack(sendBack)
979
- }
1073
+ if (sendBack) await ctx.sendBack(sendBack)
980
1074
  }
981
1075
  this.emitter.emit('subscribed', action, meta, Date.now() - start)
982
1076
  this.markAsProcessed(meta)
@@ -1,4 +1,4 @@
1
- import type { AnyAction } from '@logux/core'
1
+ import type { Action, AnyAction } from '@logux/core'
2
2
 
3
3
  import type { ServerMeta } from '../base-server/index.js'
4
4
  import type { ServerClient } from '../server-client/index.js'
@@ -59,6 +59,24 @@ export class ConnectContext<Headers extends object = unknown> {
59
59
 
60
60
  constructor(server: Server, client: ServerClient)
61
61
 
62
+ /**
63
+ * Wait until the client will confirm all actions, which were sent to it.
64
+ *
65
+ * Use it to send a long history without loading it all into the memory:
66
+ * the client’s speed will limit how fast you read the database.
67
+ *
68
+ * ```js
69
+ * while (await ctx.drain()) {
70
+ * let page = await cursor.next(100)
71
+ * if (!page.length) break
72
+ * ctx.sendBack(page.map(i => i.action))
73
+ * }
74
+ * ```
75
+ *
76
+ * @returns Promise with `false` if the client was disconnected.
77
+ */
78
+ drain(): Promise<boolean>
79
+
62
80
  /**
63
81
  * Send action back to the client.
64
82
  *
@@ -66,13 +84,27 @@ export class ConnectContext<Headers extends object = unknown> {
66
84
  * ctx.sendBack({ type: 'login/success', token })
67
85
  * ```
68
86
  *
87
+ * An array of actions will be sent in a single message. Use it to send
88
+ * a big history page by page instead of a message per action.
89
+ *
90
+ * ```js
91
+ * ctx.sendBack(page.map(i => i.action))
92
+ * ```
93
+ *
94
+ * Every action in the array can have own meta as `[action, meta]`.
95
+ *
69
96
  * Action will not be processed by server’s callbacks from `Server#type`.
70
97
  *
71
- * @param action The action.
98
+ * @param action The action or the array of actions.
72
99
  * @param meta Action’s meta.
73
100
  * @returns Promise until action was added to the server log.
74
101
  */
75
- sendBack(action: AnyAction, meta?: Partial<ServerMeta>): Promise<void>
102
+ sendBack<TypeAction extends Action = AnyAction>(
103
+ action:
104
+ | (TypeAction | [TypeAction, Partial<ServerMeta>])[]
105
+ | TypeAction,
106
+ meta?: Partial<ServerMeta>
107
+ ): Promise<void>
76
108
  }
77
109
 
78
110
  /**
package/context/index.js CHANGED
@@ -33,11 +33,22 @@ export class Context {
33
33
  }
34
34
  }
35
35
 
36
- sendBack(action, meta = {}) {
37
- return this.server.log.add(action, {
38
- clients: [this.clientId],
39
- status: 'processed',
40
- ...meta
41
- })
36
+ drain() {
37
+ return this.server.drain(this.clientId)
38
+ }
39
+
40
+ sendBack(actions, meta = {}) {
41
+ let common = { clients: [this.clientId], status: 'processed', ...meta }
42
+ if (Array.isArray(actions)) {
43
+ return this.server.log.add(
44
+ actions.map(item => {
45
+ // Every action can have own meta as `[action, meta]`
46
+ return Array.isArray(item)
47
+ ? [item[0], { ...common, ...item[1] }]
48
+ : [item, { ...common }]
49
+ })
50
+ )
51
+ }
52
+ return this.server.log.add(actions, common)
42
53
  }
43
54
  }
@@ -2,13 +2,13 @@ import os from 'node:os'
2
2
  import { sep } from 'node:path'
3
3
 
4
4
  import humanFormatter from '../human-formatter/index.js'
5
+ import { BINARY, isBinary } from '../human-formatter/utils.js'
5
6
 
6
7
  const ERROR_CODES = {
7
8
  EACCES: (e, environment) => {
8
9
  let wayToFix = {
9
- development:
10
- 'In dev mode it can be done with sudo:\n' + '$ sudo npm start',
11
- production: '$ su - `<username>`\n' + `$ npm start -p ${e.port}`
10
+ development: 'In dev mode it can be done with sudo:\n$ sudo npm start',
11
+ production: `$ su - \`<username>\`\n$ npm start -p ${e.port}`
12
12
  }
13
13
 
14
14
  return {
@@ -22,7 +22,7 @@ const ERROR_CODES = {
22
22
  EADDRINUSE: e => {
23
23
  let port = String(e.port)
24
24
  let wayToFix = {
25
- darwin: `$ sudo lsof -i ${e.port}\n` + '$ sudo kill -9 `<processid>`',
25
+ darwin: `$ sudo lsof -i ${e.port}\n$ sudo kill -9 \`<processid>\``,
26
26
  linux:
27
27
  '$ su - root\n' +
28
28
  `# netstat -nlp | grep ${e.port}\n` +
@@ -176,23 +176,46 @@ const REPORTERS = {
176
176
  function cleanFromKeys(obj, regexp, seen) {
177
177
  let result = {}
178
178
  for (let key in obj) {
179
- let v = obj[key]
180
- if (typeof v === 'string') {
181
- result[key] = v.replace(regexp, '[SECRET]')
182
- } else if (typeof v === 'object' && !Array.isArray(v) && v !== null) {
183
- if (seen.includes(v)) {
184
- throw new Error('Circular reference in action')
185
- }
186
- seen.push(v)
187
- result[key] = cleanFromKeys(v, regexp, seen)
188
- seen.pop()
189
- } else {
190
- result[key] = v
191
- }
179
+ result[key] = cleanValue(obj[key], regexp, seen)
192
180
  }
193
181
  return result
194
182
  }
195
183
 
184
+ function cleanValue(value, regexp, seen) {
185
+ if (typeof value === 'string') {
186
+ return value.replace(regexp, '[SECRET]')
187
+ } else if (isBinary(value)) {
188
+ return BINARY
189
+ } else if (typeof value === 'object' && value !== null) {
190
+ if (seen.includes(value)) {
191
+ throw new Error('Circular reference in action')
192
+ }
193
+ seen.push(value)
194
+ let cleaned = Array.isArray(value)
195
+ ? value.map(i => cleanValue(i, regexp, seen))
196
+ : cleanFromKeys(value, regexp, seen)
197
+ seen.pop()
198
+ return cleaned
199
+ } else {
200
+ return value
201
+ }
202
+ }
203
+
204
+ function createRecord(level, details, msg) {
205
+ /* c8 ignore next 4 */
206
+ if (typeof details === 'string') {
207
+ msg = details
208
+ details = {}
209
+ }
210
+ return {
211
+ level,
212
+ time: new Date().toISOString(),
213
+ pid: process.pid,
214
+ ...details,
215
+ msg
216
+ }
217
+ }
218
+
196
219
  export function createReporter(options) {
197
220
  let cleanFromLog = options.cleanFromLog || /Bearer [^\s"]+/g
198
221
  function reporter(type, details) {
@@ -218,21 +241,6 @@ export function createReporter(options) {
218
241
  }
219
242
  let stream = options.logger?.stream ?? process.stderr
220
243
 
221
- function createRecord(level, details, msg) {
222
- /* c8 ignore next 4 */
223
- if (typeof details === 'string') {
224
- msg = details
225
- details = {}
226
- }
227
- return {
228
- level,
229
- time: new Date().toISOString(),
230
- pid: process.pid,
231
- ...details,
232
- msg
233
- }
234
- }
235
-
236
244
  reporter.logger = {
237
245
  /* c8 ignore next 3 */
238
246
  debug(details, msg) {