@y/y 14.0.0-21 → 14.0.0-22

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 (2) hide show
  1. package/package.json +3 -2
  2. package/tests/testHelper.js +594 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@y/y",
3
- "version": "14.0.0-21",
3
+ "version": "14.0.0-22",
4
4
  "description": "Shared Editing Library",
5
5
  "types": "./dist/src/index.d.ts",
6
6
  "type": "module",
@@ -43,7 +43,8 @@
43
43
  },
44
44
  "files": [
45
45
  "src",
46
- "dist"
46
+ "dist",
47
+ "tests/testHelper.js"
47
48
  ],
48
49
  "dictionaries": {
49
50
  "test": "tests"
@@ -0,0 +1,594 @@
1
+ import * as t from 'lib0/testing'
2
+ import * as prng from 'lib0/prng'
3
+ import * as encoding from 'lib0/encoding'
4
+ import * as decoding from 'lib0/decoding'
5
+ import * as syncProtocol from '@y/protocols/sync'
6
+ import * as object from 'lib0/object'
7
+ import * as map from 'lib0/map'
8
+ import * as Y from '../src/index.js'
9
+ import * as math from 'lib0/math'
10
+ import * as list from 'lib0/list'
11
+ import * as delta from 'lib0/delta'
12
+ import {
13
+ createIdSet, createIdMap, addToIdSet, encodeIdMap
14
+ } from '../src/internals.js'
15
+
16
+ export * from '../src/index.js'
17
+
18
+ if (typeof window !== 'undefined') {
19
+ // @ts-ignore
20
+ window.Y = Y // eslint-disable-line
21
+ }
22
+
23
+ /**
24
+ * @param {TestYInstance} y // publish message created by `y` to all other online clients
25
+ * @param {Uint8Array} m
26
+ */
27
+ const broadcastMessage = (y, m) => {
28
+ if (y.tc.onlineConns.has(y)) {
29
+ y.tc.onlineConns.forEach(remoteYInstance => {
30
+ if (remoteYInstance !== y) {
31
+ remoteYInstance._receive(m, y)
32
+ }
33
+ })
34
+ }
35
+ }
36
+
37
+ export let useV2 = false
38
+
39
+ export const encV1 = {
40
+ encodeStateAsUpdate: Y.encodeStateAsUpdate,
41
+ mergeUpdates: Y.mergeUpdates,
42
+ applyUpdate: Y.applyUpdate,
43
+ logUpdate: Y.logUpdate,
44
+ updateEventName: /** @type {'update'} */ ('update'),
45
+ diffUpdate: Y.diffUpdate
46
+ }
47
+
48
+ export const encV2 = {
49
+ encodeStateAsUpdate: Y.encodeStateAsUpdateV2,
50
+ mergeUpdates: Y.mergeUpdatesV2,
51
+ applyUpdate: Y.applyUpdateV2,
52
+ logUpdate: Y.logUpdateV2,
53
+ updateEventName: /** @type {'updateV2'} */ ('updateV2'),
54
+ diffUpdate: Y.diffUpdateV2
55
+ }
56
+
57
+ export let enc = encV1
58
+
59
+ const useV1Encoding = () => {
60
+ useV2 = false
61
+ enc = encV1
62
+ }
63
+
64
+ const useV2Encoding = () => {
65
+ console.error('sync protocol doesnt support v2 protocol yet, fallback to v1 encoding') // @Todo
66
+ useV2 = false
67
+ enc = encV1
68
+ }
69
+
70
+ export class TestYInstance extends Y.Doc {
71
+ /**
72
+ * @param {TestConnector} testConnector
73
+ * @param {number} clientID
74
+ */
75
+ constructor (testConnector, clientID) {
76
+ super()
77
+ this.userID = clientID // overwriting clientID
78
+ /**
79
+ * @type {TestConnector}
80
+ */
81
+ this.tc = testConnector
82
+ /**
83
+ * @type {Map<TestYInstance, Array<Uint8Array>>}
84
+ */
85
+ this.receiving = new Map()
86
+ testConnector.allConns.add(this)
87
+ /**
88
+ * The list of received updates.
89
+ * We are going to merge them later using Y.mergeUpdates and check if the resulting document is correct.
90
+ * @type {Array<Uint8Array<ArrayBuffer>>}
91
+ */
92
+ this.updates = []
93
+ // set up observe on local model
94
+ this.on(enc.updateEventName, (update, origin) => {
95
+ if (origin !== testConnector) {
96
+ const encoder = encoding.createEncoder()
97
+ syncProtocol.writeUpdate(encoder, update)
98
+ broadcastMessage(this, encoding.toUint8Array(encoder))
99
+ }
100
+ this.updates.push(update)
101
+ })
102
+ this.connect()
103
+ }
104
+
105
+ /**
106
+ * Disconnect from TestConnector.
107
+ */
108
+ disconnect () {
109
+ this.receiving = new Map()
110
+ this.tc.onlineConns.delete(this)
111
+ }
112
+
113
+ /**
114
+ * Append yourself to the list of known Y instances in testconnector.
115
+ * Also initiate sync with all clients.
116
+ */
117
+ connect () {
118
+ if (!this.tc.onlineConns.has(this)) {
119
+ this.tc.onlineConns.add(this)
120
+ const encoder = encoding.createEncoder()
121
+ syncProtocol.writeSyncStep1(encoder, this)
122
+ // publish SyncStep1
123
+ broadcastMessage(this, encoding.toUint8Array(encoder))
124
+ this.tc.onlineConns.forEach(remoteYInstance => {
125
+ if (remoteYInstance !== this) {
126
+ // remote instance sends instance to this instance
127
+ const encoder = encoding.createEncoder()
128
+ syncProtocol.writeSyncStep1(encoder, remoteYInstance)
129
+ this._receive(encoding.toUint8Array(encoder), remoteYInstance)
130
+ }
131
+ })
132
+ }
133
+ }
134
+
135
+ /**
136
+ * Receive a message from another client. This message is only appended to the list of receiving messages.
137
+ * TestConnector decides when this client actually reads this message.
138
+ *
139
+ * @param {Uint8Array} message
140
+ * @param {TestYInstance} remoteClient
141
+ */
142
+ _receive (message, remoteClient) {
143
+ map.setIfUndefined(this.receiving, remoteClient, () => /** @type {Array<Uint8Array>} */ ([])).push(message)
144
+ }
145
+ }
146
+
147
+ /**
148
+ * Keeps track of TestYInstances.
149
+ *
150
+ * The TestYInstances add/remove themselves from the list of connections maiained in this object.
151
+ * I think it makes sense. Deal with it.
152
+ */
153
+ export class TestConnector {
154
+ /**
155
+ * @param {prng.PRNG} gen
156
+ */
157
+ constructor (gen) {
158
+ /**
159
+ * @type {Set<TestYInstance>}
160
+ */
161
+ this.allConns = new Set()
162
+ /**
163
+ * @type {Set<TestYInstance>}
164
+ */
165
+ this.onlineConns = new Set()
166
+ /**
167
+ * @type {prng.PRNG}
168
+ */
169
+ this.prng = gen
170
+ }
171
+
172
+ /**
173
+ * Create a new Y instance and add it to the list of connections
174
+ * @param {number} clientID
175
+ */
176
+ createY (clientID) {
177
+ return new TestYInstance(this, clientID)
178
+ }
179
+
180
+ /**
181
+ * Choose random connection and flush a random message from a random sender.
182
+ *
183
+ * If this function was unable to flush a message, because there are no more messages to flush, it returns false. true otherwise.
184
+ * @return {boolean}
185
+ */
186
+ flushRandomMessage () {
187
+ const gen = this.prng
188
+ const conns = Array.from(this.onlineConns).filter(conn => conn.receiving.size > 0)
189
+ if (conns.length > 0) {
190
+ const receiver = prng.oneOf(gen, conns)
191
+ const [sender, messages] = prng.oneOf(gen, Array.from(receiver.receiving))
192
+ const m = messages.shift()
193
+ if (messages.length === 0) {
194
+ receiver.receiving.delete(sender)
195
+ }
196
+ if (m === undefined) {
197
+ return this.flushRandomMessage()
198
+ }
199
+ const encoder = encoding.createEncoder()
200
+ // console.log('receive (' + sender.userID + '->' + receiver.userID + '):\n', syncProtocol.stringifySyncMessage(decoding.createDecoder(m), receiver))
201
+ // do not publish data created when this function is executed (could be ss2 or update message)
202
+ syncProtocol.readSyncMessage(decoding.createDecoder(m), encoder, receiver, receiver.tc)
203
+ if (encoding.length(encoder) > 0) {
204
+ // send reply message
205
+ sender._receive(encoding.toUint8Array(encoder), receiver)
206
+ }
207
+ return true
208
+ }
209
+ return false
210
+ }
211
+
212
+ /**
213
+ * @return {boolean} True iff this function actually flushed something
214
+ */
215
+ flushAllMessages () {
216
+ let didSomething = false
217
+ while (this.flushRandomMessage()) {
218
+ didSomething = true
219
+ }
220
+ return didSomething
221
+ }
222
+
223
+ reconnectAll () {
224
+ this.allConns.forEach(conn => conn.connect())
225
+ }
226
+
227
+ disconnectAll () {
228
+ this.allConns.forEach(conn => conn.disconnect())
229
+ }
230
+
231
+ syncAll () {
232
+ this.reconnectAll()
233
+ this.flushAllMessages()
234
+ }
235
+
236
+ /**
237
+ * @return {boolean} Whether it was possible to disconnect a random connection.
238
+ */
239
+ disconnectRandom () {
240
+ if (this.onlineConns.size === 0) {
241
+ return false
242
+ }
243
+ prng.oneOf(this.prng, Array.from(this.onlineConns)).disconnect()
244
+ return true
245
+ }
246
+
247
+ /**
248
+ * @return {boolean} Whether it was possible to reconnect a random connection.
249
+ */
250
+ reconnectRandom () {
251
+ /**
252
+ * @type {Array<TestYInstance>}
253
+ */
254
+ const reconnectable = []
255
+ this.allConns.forEach(conn => {
256
+ if (!this.onlineConns.has(conn)) {
257
+ reconnectable.push(conn)
258
+ }
259
+ })
260
+ if (reconnectable.length === 0) {
261
+ return false
262
+ }
263
+ prng.oneOf(this.prng, reconnectable).connect()
264
+ return true
265
+ }
266
+ }
267
+
268
+ /**
269
+ * @template T
270
+ * @param {t.TestCase} tc
271
+ * @param {{users?:number}} conf
272
+ * @param {InitTestObjectCallback<T>} [initTestObject]
273
+ * @return {{testObjects:Array<any>,testConnector:TestConnector,users:Array<TestYInstance>,array0:Y.Type<any>,array1:Y.Type<any>,array2:Y.Type<any>,map0:Y.Type<any>,map1:Y.Type<any>,map2:Y.Type<any>,map3:Y.Type<any>,text0:Y.Type,text1:Y.Type,text2:Y.Type,xml0:Y.Type,xml1:Y.Type,xml2:Y.Type}}
274
+ */
275
+ export const init = (tc, { users = 5 } = {}, initTestObject) => {
276
+ /**
277
+ * @type {Object<string,any>}
278
+ */
279
+ const result = {
280
+ users: []
281
+ }
282
+ const gen = tc.prng
283
+ // choose an encoding approach at random
284
+ if (prng.bool(gen)) {
285
+ useV2Encoding()
286
+ } else {
287
+ useV1Encoding()
288
+ }
289
+
290
+ const testConnector = new TestConnector(gen)
291
+ result.testConnector = testConnector
292
+ for (let i = 0; i < users; i++) {
293
+ const y = testConnector.createY(i)
294
+ y.clientID = i
295
+ result.users.push(y)
296
+ result['array' + i] = y.get('array')
297
+ result['map' + i] = y.get('map')
298
+ result['xml' + i] = y.get('xml')
299
+ result['text' + i] = y.get('text')
300
+ }
301
+ testConnector.syncAll()
302
+ result.testObjects = result.users.map(initTestObject || (() => null))
303
+ useV1Encoding()
304
+ return /** @type {any} */ (result)
305
+ }
306
+
307
+ /**
308
+ * @param {Y.IdSet} idSet1
309
+ * @param {Y.IdSet} idSet2
310
+ */
311
+ export const compareIdSets = (idSet1, idSet2) => {
312
+ t.assert(idSet1.clients.size === idSet2.clients.size)
313
+ for (const [client, _items1] of idSet1.clients.entries()) {
314
+ const items1 = _items1.getIds()
315
+ const items2 = idSet2.clients.get(client)?.getIds()
316
+ t.assert(items2 !== undefined && items1.length === items2.length)
317
+ for (let i = 0; i < items1.length; i++) {
318
+ const di1 = items1[i]
319
+ const di2 = /** @type {Array<import('../src/utils/IdSet.js').IdRange>} */ (items2)[i]
320
+ t.assert(di1.clock === di2.clock && di1.len === di2.len)
321
+ }
322
+ }
323
+ return true
324
+ }
325
+
326
+ /**
327
+ * only use for testing
328
+ *
329
+ * @template T
330
+ * @param {Array<Y.ContentAttribute<T>>} attrs
331
+ * @param {Y.ContentAttribute<T>} attr
332
+ *
333
+ */
334
+ const _idmapAttrsHas = (attrs, attr) => {
335
+ const hash = attr.hash()
336
+ return attrs.find(a => a.hash() === hash)
337
+ }
338
+
339
+ /**
340
+ * only use for testing
341
+ *
342
+ * @template T
343
+ * @param {Array<Y.ContentAttribute<T>>} a
344
+ * @param {Array<Y.ContentAttribute<T>>} b
345
+ */
346
+ export const _idmapAttrsEqual = (a, b) => a.length === b.length && a.every(v => _idmapAttrsHas(b, v))
347
+
348
+ /**
349
+ * Ensure that all attributes exist. Also create a copy and compare it to the original.
350
+ *
351
+ * @template T
352
+ * @param {Y.IdMap<T>} idmap
353
+ */
354
+ export const validateIdMap = idmap => {
355
+ const copy = Y.createIdMap()
356
+ idmap.clients.forEach((ranges, client) => {
357
+ ranges.getIds().forEach(range => {
358
+ range.attrs.forEach(attr => {
359
+ t.assert(idmap.attrs.has(attr))
360
+ t.assert(idmap.attrsH.get(attr.hash()) === attr)
361
+ copy.add(client, range.clock, range.len, range.attrs.slice())
362
+ })
363
+ })
364
+ t.assert(copy.clients.get(client)?.getIds().length === ranges.getIds().length)
365
+ })
366
+ t.assert(idmap.attrsH.size === idmap.attrs.size)
367
+ }
368
+
369
+ /**
370
+ * @template T
371
+ * @param {Y.IdMap<T>} idmap1
372
+ * @param {Y.IdMap<T>} idmap2
373
+ */
374
+ export const compareIdmaps = (idmap1, idmap2) => {
375
+ t.assert(idmap1.clients.size === idmap2.clients.size)
376
+ for (const [client, _items1] of idmap1.clients.entries()) {
377
+ const items1 = _items1.getIds()
378
+ const items2 = idmap2.clients.get(client)?.getIds()
379
+ t.assert(items2 !== undefined && items1.length === items2.length)
380
+ for (let i = 0; i < items1.length; i++) {
381
+ const di1 = items1[i]
382
+ const di2 = /** @type {Array<import('../src/utils/IdMap.js').AttrRange<T>>} */ (items2)[i]
383
+ t.assert(di1.clock === di2.clock && di1.len === di2.len && _idmapAttrsEqual(di1.attrs, di2.attrs))
384
+ }
385
+ }
386
+ validateIdMap(idmap1)
387
+ validateIdMap(idmap2)
388
+ }
389
+
390
+ /**
391
+ * @param {prng.PRNG} gen
392
+ * @param {number} clients
393
+ * @param {number} clockRange (max clock - exclusive - by each client)
394
+ */
395
+ export const createRandomIdSet = (gen, clients, clockRange) => {
396
+ const maxOpLen = 5
397
+ const numOfOps = math.ceil((clients * clockRange) / maxOpLen)
398
+ const idset = createIdSet()
399
+ for (let i = 0; i < numOfOps; i++) {
400
+ const client = prng.uint32(gen, 0, clients - 1)
401
+ const clockStart = prng.uint32(gen, 0, clockRange)
402
+ const len = prng.uint32(gen, 0, clockRange - clockStart)
403
+ addToIdSet(idset, client, clockStart, len)
404
+ }
405
+ if (idset.clients.size === clients && clients > 1 && prng.bool(gen)) {
406
+ idset.clients.delete(prng.uint32(gen, 0, clients))
407
+ }
408
+ return idset
409
+ }
410
+
411
+ /**
412
+ * @template T
413
+ * @param {prng.PRNG} gen
414
+ * @param {number} clients
415
+ * @param {number} clockRange (max clock - exclusive - by each client)
416
+ * @param {Array<T>} attrChoices (max clock - exclusive - by each client)
417
+ * @return {Y.IdMap<T>}
418
+ */
419
+ export const createRandomIdMap = (gen, clients, clockRange, attrChoices) => {
420
+ const maxOpLen = 5
421
+ const numOfOps = math.ceil((clients * clockRange) / maxOpLen)
422
+ const idMap = createIdMap()
423
+ for (let i = 0; i < numOfOps; i++) {
424
+ const client = prng.uint32(gen, 0, clients - 1)
425
+ const clockStart = prng.uint32(gen, 0, clockRange)
426
+ const len = prng.uint32(gen, 0, clockRange - clockStart)
427
+ const attrs = [prng.oneOf(gen, attrChoices)]
428
+ // maybe add another attr
429
+ if (prng.bool(gen)) {
430
+ const a = prng.oneOf(gen, attrChoices)
431
+ if (attrs.find(attr => attr === a) == null) {
432
+ attrs.push(a)
433
+ }
434
+ }
435
+ idMap.add(client, clockStart, len, attrs.map(v => Y.createContentAttribute('', v)))
436
+ }
437
+ t.info(`Created IdMap with ${numOfOps} ranges and ${attrChoices.length} different attributes. Encoded size: ${encodeIdMap(idMap).byteLength}`)
438
+ return idMap
439
+ }
440
+
441
+ /**
442
+ * 1. reconnect and flush all
443
+ * 2. user 0 gc
444
+ * 3. get type content
445
+ * 4. disconnect & reconnect all (so gc is propagated)
446
+ * 5. compare os, ds, ss
447
+ *
448
+ * @param {Array<TestYInstance>} users
449
+ */
450
+ export const compare = users => {
451
+ users.forEach(u => u.connect())
452
+ while (users[0].tc.flushAllMessages()) {} // eslint-disable-line
453
+ // For each document, merge all received document updates with Y.mergeUpdates and create a new document which will be added to the list of "users"
454
+ // This ensures that mergeUpdates works correctly
455
+ const mergedDocs = users.map(user => {
456
+ const ydoc = new Y.Doc()
457
+ enc.applyUpdate(ydoc, enc.mergeUpdates(user.updates))
458
+ return ydoc
459
+ })
460
+ users.push(.../** @type {any} */(mergedDocs))
461
+ const userArrayValues = users.map(u => u.get('array').toJSON().children ?? [])
462
+ const userMapValues = users.map(u => u.get('map').toJSON().attrs ?? {})
463
+ // @todo fix type error here
464
+ // @ts-ignore
465
+ const userXmlValues = users.map(u => u.get('xml').toString())
466
+ const userTextValues = users.map(u => u.get('text').toDeltaDeep())
467
+ for (const u of users) {
468
+ t.assert(u.store.pendingDs === null)
469
+ t.assert(u.store.pendingStructs === null)
470
+ }
471
+ // Test Map iterator
472
+ const ymapkeys = Array.from(users[0].get('map').attrKeys())
473
+ t.assert(ymapkeys.length === Object.keys(userMapValues[0]).length)
474
+ ymapkeys.forEach(key => t.assert(object.hasProperty(userMapValues[0], key)))
475
+ // Compare all users
476
+ for (let i = 0; i < users.length - 1; i++) {
477
+ t.compare(userArrayValues[i].length, users[i].get('array').length)
478
+ t.compare(userArrayValues[i], userArrayValues[i + 1])
479
+ t.compare(userMapValues[i], userMapValues[i + 1])
480
+ t.compare(userXmlValues[i], userXmlValues[i + 1])
481
+ t.compare(list.toArray(userTextValues[i].children).map(a => (delta.$textOp.check(a) || delta.$insertOp.check(a)) ? a.insert.length : 0).reduce((a, b) => a + b, 0), users[i].get('text').length)
482
+ t.compare(userTextValues[i], userTextValues[i + 1], '', (_constructor, a, b) => {
483
+ if (a instanceof Y.Type) {
484
+ t.compare(a.toJSON(), b.toJSON())
485
+ } else if (a !== b) {
486
+ t.fail('Deltas dont match')
487
+ }
488
+ return true
489
+ })
490
+ t.compare(Y.encodeStateVector(users[i]), Y.encodeStateVector(users[i + 1]))
491
+ Y.equalIdSets(Y.createDeleteSetFromStructStore(users[i].store), Y.createDeleteSetFromStructStore(users[i + 1].store))
492
+ compareStructStores(users[i].store, users[i + 1].store)
493
+ t.compare(Y.encodeSnapshot(Y.snapshot(users[i])), Y.encodeSnapshot(Y.snapshot(users[i + 1])))
494
+ }
495
+ users.forEach(user => {
496
+ compareIdSets(user.store.ds, Y.createDeleteSetFromStructStore(user.store))
497
+ })
498
+ users.map(u => u.destroy())
499
+ }
500
+
501
+ /**
502
+ * @param {Y.Item?} a
503
+ * @param {Y.Item?} b
504
+ * @return {boolean}
505
+ */
506
+ export const compareItemIDs = (a, b) => a === b || (a !== null && b != null && Y.compareIDs(a.id, b.id))
507
+
508
+ /**
509
+ * @param {import('../src/internals.js').StructStore} ss1
510
+ * @param {import('../src/internals.js').StructStore} ss2
511
+ */
512
+ export const compareStructStores = (ss1, ss2) => {
513
+ t.assert(ss1.clients.size === ss2.clients.size)
514
+ for (const [client, structs1] of ss1.clients) {
515
+ const structs2 = /** @type {Array<Y.AbstractStruct>} */ (ss2.clients.get(client))
516
+ t.assert(structs2 !== undefined && structs1.length === structs2.length)
517
+ for (let i = 0; i < structs1.length; i++) {
518
+ const s1 = structs1[i]
519
+ const s2 = structs2[i]
520
+ // checks for abstract struct
521
+ if (
522
+ s1.constructor !== s2.constructor ||
523
+ !Y.compareIDs(s1.id, s2.id) ||
524
+ s1.deleted !== s2.deleted ||
525
+ // @ts-ignore
526
+ s1.length !== s2.length
527
+ ) {
528
+ t.fail('Structs dont match')
529
+ }
530
+ if (s1 instanceof Y.Item) {
531
+ if (
532
+ !(s2 instanceof Y.Item) ||
533
+ !((s1.left === null && s2.left === null) || (s1.left !== null && s2.left !== null && Y.compareIDs(s1.left.lastId, s2.left.lastId))) ||
534
+ !compareItemIDs(s1.right, s2.right) ||
535
+ !Y.compareIDs(s1.origin, s2.origin) ||
536
+ !Y.compareIDs(s1.rightOrigin, s2.rightOrigin) ||
537
+ s1.parentSub !== s2.parentSub
538
+ ) {
539
+ return t.fail('Items dont match')
540
+ }
541
+ // make sure that items are connected correctly
542
+ t.assert(s1.left === null || s1.left.right === s1)
543
+ t.assert(s1.right === null || s1.right.left === s1)
544
+ t.assert(s2.left === null || s2.left.right === s2)
545
+ t.assert(s2.right === null || s2.right.left === s2)
546
+ }
547
+ }
548
+ }
549
+ }
550
+
551
+ /**
552
+ * @template T
553
+ * @callback InitTestObjectCallback
554
+ * @param {TestYInstance} y
555
+ * @return {T}
556
+ */
557
+
558
+ /**
559
+ * @template T
560
+ * @param {t.TestCase} tc
561
+ * @param {Array<function(Y.Doc,prng.PRNG,T):void>} mods
562
+ * @param {number} iterations
563
+ * @param {InitTestObjectCallback<T>} [initTestObject]
564
+ */
565
+ export const applyRandomTests = (tc, mods, iterations, initTestObject) => {
566
+ const gen = tc.prng
567
+ const result = init(tc, { users: 5 }, initTestObject)
568
+ const { testConnector, users } = result
569
+ for (let i = 0; i < iterations; i++) {
570
+ if (prng.int32(gen, 0, 100) <= 2) {
571
+ // 2% chance to disconnect/reconnect a random user
572
+ if (prng.bool(gen)) {
573
+ testConnector.disconnectRandom()
574
+ } else {
575
+ testConnector.reconnectRandom()
576
+ }
577
+ } else if (prng.int32(gen, 0, 100) <= 1) {
578
+ // 1% chance to flush all
579
+ testConnector.flushAllMessages()
580
+ } else if (prng.int32(gen, 0, 100) <= 50) {
581
+ // 50% chance to flush a random message
582
+ testConnector.flushRandomMessage()
583
+ }
584
+ const user = prng.int32(gen, 0, users.length - 1)
585
+ const test = prng.oneOf(gen, mods)
586
+ test(users[user], gen, result.testObjects[user])
587
+ }
588
+ compare(users)
589
+ return result
590
+ }
591
+
592
+ /**
593
+ * @typedef {ReturnType<typeof applyRandomTests>} ApplyRandomTestsResult
594
+ */