ar_sync 1.0.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.
Files changed (75) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +13 -0
  3. data/.travis.yml +5 -0
  4. data/Gemfile +6 -0
  5. data/Gemfile.lock +53 -0
  6. data/LICENSE.txt +21 -0
  7. data/README.md +128 -0
  8. data/Rakefile +10 -0
  9. data/ar_sync.gemspec +28 -0
  10. data/bin/console +12 -0
  11. data/bin/setup +8 -0
  12. data/core/ActioncableAdapter.d.ts +10 -0
  13. data/core/ActioncableAdapter.js +29 -0
  14. data/core/ArSyncApi.d.ts +5 -0
  15. data/core/ArSyncApi.js +74 -0
  16. data/core/ArSyncModelBase.d.ts +71 -0
  17. data/core/ArSyncModelBase.js +110 -0
  18. data/core/ConnectionAdapter.d.ts +7 -0
  19. data/core/ConnectionAdapter.js +2 -0
  20. data/core/ConnectionManager.d.ts +19 -0
  21. data/core/ConnectionManager.js +75 -0
  22. data/core/DataType.d.ts +60 -0
  23. data/core/DataType.js +2 -0
  24. data/core/hooksBase.d.ts +29 -0
  25. data/core/hooksBase.js +80 -0
  26. data/graph/ArSyncModel.d.ts +10 -0
  27. data/graph/ArSyncModel.js +22 -0
  28. data/graph/ArSyncStore.d.ts +28 -0
  29. data/graph/ArSyncStore.js +593 -0
  30. data/graph/hooks.d.ts +3 -0
  31. data/graph/hooks.js +10 -0
  32. data/graph/index.d.ts +2 -0
  33. data/graph/index.js +4 -0
  34. data/lib/ar_sync.rb +25 -0
  35. data/lib/ar_sync/class_methods.rb +215 -0
  36. data/lib/ar_sync/collection.rb +83 -0
  37. data/lib/ar_sync/config.rb +18 -0
  38. data/lib/ar_sync/core.rb +138 -0
  39. data/lib/ar_sync/field.rb +96 -0
  40. data/lib/ar_sync/instance_methods.rb +130 -0
  41. data/lib/ar_sync/rails.rb +155 -0
  42. data/lib/ar_sync/type_script.rb +80 -0
  43. data/lib/ar_sync/version.rb +3 -0
  44. data/lib/generators/ar_sync/install/install_generator.rb +87 -0
  45. data/lib/generators/ar_sync/types/types_generator.rb +11 -0
  46. data/package-lock.json +1115 -0
  47. data/package.json +19 -0
  48. data/src/core/ActioncableAdapter.ts +30 -0
  49. data/src/core/ArSyncApi.ts +75 -0
  50. data/src/core/ArSyncModelBase.ts +126 -0
  51. data/src/core/ConnectionAdapter.ts +5 -0
  52. data/src/core/ConnectionManager.ts +69 -0
  53. data/src/core/DataType.ts +73 -0
  54. data/src/core/hooksBase.ts +86 -0
  55. data/src/graph/ArSyncModel.ts +21 -0
  56. data/src/graph/ArSyncStore.ts +567 -0
  57. data/src/graph/hooks.ts +7 -0
  58. data/src/graph/index.ts +2 -0
  59. data/src/tree/ArSyncModel.ts +145 -0
  60. data/src/tree/ArSyncStore.ts +323 -0
  61. data/src/tree/hooks.ts +7 -0
  62. data/src/tree/index.ts +2 -0
  63. data/tree/ArSyncModel.d.ts +39 -0
  64. data/tree/ArSyncModel.js +143 -0
  65. data/tree/ArSyncStore.d.ts +21 -0
  66. data/tree/ArSyncStore.js +365 -0
  67. data/tree/hooks.d.ts +3 -0
  68. data/tree/hooks.js +10 -0
  69. data/tree/index.d.ts +2 -0
  70. data/tree/index.js +4 -0
  71. data/tsconfig.json +15 -0
  72. data/vendor/assets/javascripts/ar_sync_actioncable_adapter.js.erb +7 -0
  73. data/vendor/assets/javascripts/ar_sync_graph.js.erb +17 -0
  74. data/vendor/assets/javascripts/ar_sync_tree.js.erb +17 -0
  75. metadata +187 -0
@@ -0,0 +1,21 @@
1
+ import ArSyncStore from './ArSyncStore'
2
+ import ArSyncConnectionManager from '../core/ConnectionManager'
3
+ import ArSyncModelBase from '../core/ArSyncModelBase'
4
+ import ConnectionAdapter from '../core/ConnectionAdapter'
5
+
6
+ export default class ArSyncModel<T> extends ArSyncModelBase<T> {
7
+ static setConnectionAdapter(adapter: ConnectionAdapter) {
8
+ ArSyncStore.connectionManager = new ArSyncConnectionManager(adapter)
9
+ }
10
+ static createRefModel(request, option): any {
11
+ return new ArSyncStore(request, option)
12
+ }
13
+ refManagerClass() {
14
+ return ArSyncModel
15
+ }
16
+ connectionManager() {
17
+ return ArSyncStore.connectionManager
18
+ }
19
+ static _cache = {}
20
+ static cacheTimeout = 10 * 1000
21
+ }
@@ -0,0 +1,567 @@
1
+ import ArSyncAPI from '../core/ArSyncApi'
2
+
3
+ const ModelBatchRequest = {
4
+ timer: null,
5
+ apiRequests: {} as {
6
+ [api: string]: {
7
+ [queryJSON: string]: {
8
+ query
9
+ requests: {
10
+ [id: number]: {
11
+ id: number
12
+ model?
13
+ callbacks: ((model) => void)[]
14
+ }
15
+ }
16
+ }
17
+ }
18
+ },
19
+ fetch(api, query, id) {
20
+ this.setTimer()
21
+ return new Promise(resolve => {
22
+ const queryJSON = JSON.stringify(query)
23
+ const apiRequest = this.apiRequests[api] = this.apiRequests[api] || {}
24
+ const queryRequests = apiRequest[queryJSON] = apiRequest[queryJSON] || { query, requests: {} }
25
+ const request = queryRequests.requests[id] = queryRequests.requests[id] || { id, callbacks: [] }
26
+ request.callbacks.push(resolve)
27
+ })
28
+ },
29
+ batchFetch() {
30
+ const { apiRequests } = this as typeof ModelBatchRequest
31
+ for (const api in apiRequests) {
32
+ const apiRequest = apiRequests[api]
33
+ for (const { query, requests } of Object.values(apiRequest)) {
34
+ const ids = Object.values(requests).map(({ id }) => id)
35
+ ArSyncAPI.syncFetch({ api, query, params: { ids } }).then((models: any[]) => {
36
+ for (const model of models) requests[model.id].model = model
37
+ for (const { model, callbacks } of Object.values(requests)) {
38
+ for (const callback of callbacks) callback(model)
39
+ }
40
+ })
41
+ }
42
+ }
43
+ this.apiRequests = {}
44
+ },
45
+ setTimer() {
46
+ if (this.timer) return
47
+ this.timer = setTimeout(() => {
48
+ this.timer = null
49
+ this.batchFetch()
50
+ }, 20)
51
+ }
52
+ }
53
+
54
+ class ArSyncContainerBase {
55
+ data
56
+ listeners
57
+ networkSubscriber
58
+ parentModel
59
+ parentKey
60
+ children: ArSyncContainerBase[]
61
+ onConnectionChange
62
+ constructor() {
63
+ this.listeners = []
64
+ }
65
+ replaceData(_data, _sync_keys?) {}
66
+ initForReload(request) {
67
+ this.networkSubscriber = ArSyncStore.connectionManager.subscribeNetwork((state) => {
68
+ if (state) {
69
+ ArSyncAPI.syncFetch(request).then(data => {
70
+ if (this.data) {
71
+ this.replaceData(data)
72
+ if (this.onConnectionChange) this.onConnectionChange(true)
73
+ if (this.onChange) this.onChange([], this.data)
74
+ }
75
+ })
76
+ } else {
77
+ if (this.onConnectionChange) this.onConnectionChange(false)
78
+ }
79
+ })
80
+ }
81
+ release() {
82
+ if (this.networkSubscriber) this.networkSubscriber.unsubscribe()
83
+ this.unsubscribeAll()
84
+ for (const child of Object.values(this.children)) {
85
+ if (child) child.release()
86
+ }
87
+ this.data = null
88
+ }
89
+ onChange(path, data) {
90
+ if (this.parentModel) this.parentModel.onChange([this.parentKey, ...path], data)
91
+ }
92
+ subscribe(key, listener) {
93
+ this.listeners.push(ArSyncStore.connectionManager.subscribe(key, listener))
94
+ }
95
+ unsubscribeAll() {
96
+ for (const l of this.listeners) l.unsubscribe()
97
+ this.listeners = []
98
+ }
99
+ static parseQuery(query, attrsonly = false){
100
+ const attributes = {}
101
+ let column = null
102
+ let params = null
103
+ if (!query) query = []
104
+ if (query.constructor !== Array) query = [query]
105
+ for (const arg of query) {
106
+ if (typeof(arg) === 'string') {
107
+ attributes[arg] = {}
108
+ } else if (typeof(arg) === 'object') {
109
+ for (const key in arg){
110
+ const value = arg[key]
111
+ if (attrsonly) {
112
+ attributes[key] = this.parseQuery(value)
113
+ continue
114
+ }
115
+ if (key === 'attributes') {
116
+ const child = this.parseQuery(value, true)
117
+ for (const k in child) attributes[k] = child[k]
118
+ } else if (key === 'as') {
119
+ column = value
120
+ } else if (key === 'params') {
121
+ params = value
122
+ } else {
123
+ attributes[key] = this.parseQuery(value)
124
+ }
125
+ }
126
+ }
127
+ }
128
+ if (attrsonly) return attributes
129
+ return { attributes, as: column, params }
130
+ }
131
+ static _load({ api, id, params, query }, root) {
132
+ const parsedQuery = ArSyncRecord.parseQuery(query)
133
+ if (id) {
134
+ return ModelBatchRequest.fetch(api, query, id).then(data => new ArSyncRecord(parsedQuery, data[0], null, root))
135
+ } else {
136
+ const request = { api, query, params }
137
+ return ArSyncAPI.syncFetch(request).then((response: any) => {
138
+ if (response.collection && response.order) {
139
+ return new ArSyncCollection(response.sync_keys, 'collection', parsedQuery, response, request, root)
140
+ } else {
141
+ return new ArSyncRecord(parsedQuery, response, request, root)
142
+ }
143
+ })
144
+ }
145
+ }
146
+ static load(apiParams, root) {
147
+ if (!(apiParams instanceof Array)) return this._load(apiParams, root)
148
+ return new Promise((resolve, _reject) => {
149
+ const resultModels: any[] = []
150
+ let countdown = apiParams.length
151
+ apiParams.forEach((param, i) => {
152
+ this._load(param, root).then(model => {
153
+ resultModels[i] = model
154
+ countdown --
155
+ if (countdown === 0) resolve(resultModels)
156
+ })
157
+ })
158
+ })
159
+ }
160
+ }
161
+
162
+ class ArSyncRecord extends ArSyncContainerBase {
163
+ id
164
+ root
165
+ query
166
+ data
167
+ children
168
+ sync_keys
169
+ paths
170
+ reloadQueryCache
171
+ constructor(query, data, request, root) {
172
+ super()
173
+ this.root = root
174
+ if (request) this.initForReload(request)
175
+ this.query = query
176
+ this.data = {}
177
+ this.children = {}
178
+ this.replaceData(data)
179
+ }
180
+ setSyncKeys(sync_keys) {
181
+ this.sync_keys = sync_keys
182
+ if (!this.sync_keys) {
183
+ this.sync_keys = []
184
+ console.error('warning: no sync_keys')
185
+ }
186
+ }
187
+ replaceData(data) {
188
+ this.setSyncKeys(data.sync_keys)
189
+ this.unsubscribeAll()
190
+ if (this.data.id !== data.id) {
191
+ this.mark()
192
+ this.data.id = data.id
193
+ }
194
+ this.paths = []
195
+ for (const key in this.query.attributes) {
196
+ const subQuery = this.query.attributes[key]
197
+ const aliasName = subQuery.as || key
198
+ const subData = data[aliasName]
199
+ if (key === 'sync_keys') continue
200
+ if (subQuery.attributes && (subData instanceof Array || (subData && subData.collection && subData.order))) {
201
+ if (this.children[aliasName]) {
202
+ this.children[aliasName].replaceData(subData, this.sync_keys)
203
+ } else {
204
+ const collection = new ArSyncCollection(this.sync_keys, key, subQuery, subData, null, this.root)
205
+ this.mark()
206
+ this.children[aliasName] = collection
207
+ this.data[aliasName] = collection.data
208
+ collection.parentModel = this
209
+ collection.parentKey = aliasName
210
+ }
211
+ } else {
212
+ if (subQuery.attributes) this.paths.push(key);
213
+ if (subData && subData.sync_keys) {
214
+ if (this.children[aliasName]) {
215
+ this.children[aliasName].replaceData(subData)
216
+ } else {
217
+ const model = new ArSyncRecord(subQuery, subData, null, this.root)
218
+ this.mark()
219
+ this.children[aliasName] = model
220
+ this.data[aliasName] = model.data
221
+ model.parentModel = this
222
+ model.parentKey = aliasName
223
+ }
224
+ } else {
225
+ if(this.children[aliasName]) {
226
+ this.children[aliasName].release()
227
+ delete this.children[aliasName]
228
+ }
229
+ if (this.data[aliasName] !== subData) {
230
+ this.mark()
231
+ this.data[aliasName] = subData
232
+ }
233
+ }
234
+ }
235
+ }
236
+ this.subscribeAll()
237
+ }
238
+ onNotify(notifyData, path?) {
239
+ const { action, class_name, id } = notifyData
240
+ if (action === 'remove') {
241
+ this.children[path].release()
242
+ this.children[path] = null
243
+ this.mark()
244
+ this.data[path] = null
245
+ this.onChange([path], null)
246
+ } else if (action === 'add') {
247
+ if (this.data.id === id) return
248
+ const query = this.query.attributes[path]
249
+ ModelBatchRequest.fetch(class_name, query, id).then(data => {
250
+ if (!data) return
251
+ const model = new ArSyncRecord(query, data, null, this.root)
252
+ if (this.children[path]) this.children[path].release()
253
+ this.children[path] = model
254
+ this.mark()
255
+ this.data[path] = model.data
256
+ model.parentModel = this
257
+ model.parentKey = path
258
+ this.onChange([path], model.data)
259
+ })
260
+ } else {
261
+ ModelBatchRequest.fetch(class_name, this.reloadQuery(), id).then(data => {
262
+ this.update(data)
263
+ })
264
+ }
265
+ }
266
+ subscribeAll() {
267
+ const callback = data => this.onNotify(data)
268
+ for (const key of this.sync_keys) {
269
+ this.subscribe(key, callback)
270
+ }
271
+ for (const path of this.paths) {
272
+ const pathCallback = data => this.onNotify(data, path)
273
+ for (const key of this.sync_keys) this.subscribe(key + path, pathCallback)
274
+ }
275
+ }
276
+ reloadQuery() {
277
+ if (this.reloadQueryCache) return this.reloadQueryCache
278
+ const reloadQuery = this.reloadQueryCache = { attributes: [] as any[] }
279
+ for (const key in this.query.attributes) {
280
+ if (key === 'sync_keys') continue
281
+ const val = this.query.attributes[key]
282
+ if (!val || !val.attributes) {
283
+ reloadQuery.attributes.push(key)
284
+ } else if (!val.params && Object.keys(val.attributes).length === 0) {
285
+ reloadQuery.attributes.push({ [key]: val })
286
+ }
287
+ }
288
+ return reloadQuery
289
+ }
290
+ update(data) {
291
+ for (const key in data) {
292
+ if (this.data[key] === data[key]) continue
293
+ this.mark()
294
+ this.data[key] = data[key]
295
+ this.onChange([key], data[key])
296
+ }
297
+ }
298
+ markAndSet(key, data) {
299
+ this.mark()
300
+ this.data[key] = data
301
+ }
302
+ mark() {
303
+ if (!this.root || !this.root.immutable || !Object.isFrozen(this.data)) return
304
+ this.data = { ...this.data }
305
+ this.root.mark(this.data)
306
+ if (this.parentModel) this.parentModel.markAndSet(this.parentKey, this.data)
307
+ }
308
+ onChange(path, data) {
309
+ if (this.parentModel) this.parentModel.onChange([this.parentKey, ...path], data)
310
+ }
311
+ }
312
+ class ArSyncCollection extends ArSyncContainerBase {
313
+ root
314
+ path
315
+ order
316
+ query
317
+ data
318
+ children
319
+ sync_keys
320
+ constructor(sync_keys, path, query, data, request, root){
321
+ super()
322
+ this.root = root
323
+ this.path = path
324
+ if (request) this.initForReload(request)
325
+ if (query.params && (query.params.order || query.params.limit)) {
326
+ this.order = { limit: query.params.limit, mode: query.params.order || 'asc' }
327
+ } else {
328
+ this.order = { limit: null, mode: 'asc' }
329
+ }
330
+ this.query = query
331
+ this.data = []
332
+ this.children = []
333
+ this.replaceData(data, sync_keys)
334
+ }
335
+ setSyncKeys(sync_keys) {
336
+ if (sync_keys) {
337
+ this.sync_keys = sync_keys.map(key => key + this.path)
338
+ } else {
339
+ console.error('warning: no sync_keys')
340
+ this.sync_keys = []
341
+ }
342
+ }
343
+ replaceData(data, sync_keys) {
344
+ this.setSyncKeys(sync_keys)
345
+ const existings = {}
346
+ for (const child of this.children) existings[child.data.id] = child
347
+ let collection
348
+ if (data.collection && data.order) {
349
+ collection = data.collection
350
+ this.order = data.order
351
+ } else {
352
+ collection = data
353
+ }
354
+ const newChildren: any[] = []
355
+ const newData: any[] = []
356
+ for (const subData of collection) {
357
+ let model = existings[subData.id]
358
+ if (model) {
359
+ model.replaceData(subData)
360
+ } else {
361
+ model = new ArSyncRecord(this.query, subData, null, this.root)
362
+ model.parentModel = this
363
+ model.parentKey = subData.id
364
+ }
365
+ newChildren.push(model)
366
+ newData.push(model.data)
367
+ }
368
+ while (this.children.length) {
369
+ const child = this.children.pop()
370
+ if (!existings[child.data.id]) child.release()
371
+ }
372
+ if (this.data.length || newChildren.length) this.mark()
373
+ while (this.data.length) this.data.pop()
374
+ for (const child of newChildren) this.children.push(child)
375
+ for (const el of newData) this.data.push(el)
376
+ this.subscribeAll()
377
+ }
378
+ consumeAdd(className, id) {
379
+ if (this.data.findIndex(a => a.id === id) >= 0) return
380
+ if (this.order.limit === this.data.length) {
381
+ if (this.order.mode === 'asc') {
382
+ const last = this.data[this.data.length - 1]
383
+ if (last && last.id < id) return
384
+ } else {
385
+ const last = this.data[this.data.length - 1]
386
+ if (last && last.id > id) return
387
+ }
388
+ }
389
+ ModelBatchRequest.fetch(className, this.query, id).then((data) => {
390
+ if (!data) return
391
+ const model = new ArSyncRecord(this.query, data, null, this.root)
392
+ model.parentModel = this
393
+ model.parentKey = id
394
+ const overflow = this.order.limit && this.order.limit === this.data.length
395
+ let rmodel
396
+ this.mark()
397
+ if (this.order.mode === 'asc') {
398
+ const last = this.data[this.data.length - 1]
399
+ this.children.push(model)
400
+ this.data.push(model.data)
401
+ if (last && last.id > id) {
402
+ this.children.sort((a, b) => a.data.id < b.data.id ? -1 : +1)
403
+ this.data.sort((a, b) => a.id < b.id ? -1 : +1)
404
+ }
405
+ if (overflow) {
406
+ rmodel = this.children.shift()
407
+ rmodel.release()
408
+ this.data.shift()
409
+ }
410
+ } else {
411
+ const first = this.data[0]
412
+ this.children.unshift(model)
413
+ this.data.unshift(model.data)
414
+ if (first && first.id > id) {
415
+ this.children.sort((a, b) => a.data.id > b.data.id ? -1 : +1)
416
+ this.data.sort((a, b) => a.id > b.id ? -1 : +1)
417
+ }
418
+ if (overflow) {
419
+ rmodel = this.children.pop()
420
+ rmodel.release()
421
+ this.data.pop()
422
+ }
423
+ }
424
+ this.onChange([model.id], model.data)
425
+ if (rmodel) this.onChange([rmodel.id], null)
426
+ })
427
+ }
428
+ consumeRemove(id) {
429
+ const idx = this.data.findIndex(a => a.id === id)
430
+ if (idx < 0) return
431
+ this.mark()
432
+ this.children[idx].release()
433
+ this.children.splice(idx, 1)
434
+ this.data.splice(idx, 1)
435
+ this.onChange([id], null)
436
+ }
437
+ onNotify(notifyData) {
438
+ if (notifyData.action === 'add') {
439
+ this.consumeAdd(notifyData.class_name, notifyData.id)
440
+ } else if (notifyData.action === 'remove') {
441
+ this.consumeRemove(notifyData.id)
442
+ }
443
+ }
444
+ subscribeAll() {
445
+ const callback = data => this.onNotify(data)
446
+ for (const key of this.sync_keys) this.subscribe(key, callback)
447
+ }
448
+ markAndSet(id, data) {
449
+ this.mark()
450
+ const idx = this.data.findIndex(a => a.id === id)
451
+ if (idx >= 0) this.data[idx] = data
452
+ }
453
+ mark() {
454
+ if (!this.root || !this.root.immutable || !Object.isFrozen(this.data)) return
455
+ this.data = [...this.data]
456
+ this.root.mark(this.data)
457
+ if (this.parentModel) this.parentModel.markAndSet(this.parentKey, this.data)
458
+ }
459
+ }
460
+
461
+ export default class ArSyncStore {
462
+ immutable
463
+ markedForFreezeObjects
464
+ changes
465
+ eventListeners
466
+ markForRelease
467
+ container
468
+ request
469
+ complete: boolean
470
+ notfound?: boolean
471
+ data
472
+ changesBufferTimer: number | undefined | null
473
+ retryLoadTimer: number | undefined | null
474
+ static connectionManager
475
+ constructor(request, { immutable } = {} as { immutable?: boolean }) {
476
+ this.immutable = immutable
477
+ this.markedForFreezeObjects = []
478
+ this.changes = []
479
+ this.eventListeners = { events: {}, serial: 0 }
480
+ this.request = request
481
+ this.complete = false
482
+ this.data = null
483
+ this.load(0)
484
+ }
485
+ load(retryCount: number) {
486
+ ArSyncContainerBase.load(this.request, this).then((container: ArSyncContainerBase) => {
487
+ if (this.markForRelease) {
488
+ container.release()
489
+ return
490
+ }
491
+ this.container = container
492
+ this.data = container.data
493
+ if (this.immutable) this.freezeRecursive(this.data)
494
+ this.complete = true
495
+ this.notfound = false
496
+ this.trigger('load')
497
+ this.trigger('change', { path: [], value: this.data })
498
+ container.onChange = (path, value) => {
499
+ this.changes.push({ path, value })
500
+ this.setChangesBufferTimer()
501
+ }
502
+ container.onConnectionChange = state => {
503
+ this.trigger('connection', state)
504
+ }
505
+ }).catch(e => {
506
+ if (this.markForRelease) return
507
+ if (!e.retry) {
508
+ this.complete = true
509
+ this.notfound = true
510
+ this.trigger('load')
511
+ return
512
+ }
513
+ const sleepSeconds = Math.min(Math.pow(2, retryCount), 30)
514
+ this.retryLoadTimer = setTimeout(
515
+ () => {
516
+ this.retryLoadTimer = null
517
+ this.load(retryCount + 1)
518
+ },
519
+ sleepSeconds * 1000
520
+ )
521
+ })
522
+ }
523
+ setChangesBufferTimer() {
524
+ if (this.changesBufferTimer) return
525
+ this.changesBufferTimer = setTimeout(() => {
526
+ this.changesBufferTimer = null
527
+ const changes = this.changes
528
+ this.changes = []
529
+ this.freezeMarked()
530
+ this.data = this.container.data
531
+ changes.forEach(patch => this.trigger('change', patch))
532
+ }, 20)
533
+ }
534
+ subscribe(event, callback) {
535
+ let listeners = this.eventListeners.events[event]
536
+ if (!listeners) this.eventListeners.events[event] = listeners = {}
537
+ const id = this.eventListeners.serial++
538
+ listeners[id] = callback
539
+ return { unsubscribe: () => { delete listeners[id] } }
540
+ }
541
+ trigger(event, arg?) {
542
+ const listeners = this.eventListeners.events[event]
543
+ if (!listeners) return
544
+ for (const id in listeners) listeners[id](arg)
545
+ }
546
+ mark(object) {
547
+ this.markedForFreezeObjects.push(object)
548
+ }
549
+ freezeRecursive(obj) {
550
+ if (Object.isFrozen(obj)) return obj
551
+ for (const key in obj) this.freezeRecursive(obj[key])
552
+ Object.freeze(obj)
553
+ }
554
+ freezeMarked() {
555
+ this.markedForFreezeObjects.forEach(obj => this.freezeRecursive(obj))
556
+ this.markedForFreezeObjects = []
557
+ }
558
+ release() {
559
+ if (this.retryLoadTimer) clearTimeout(this.retryLoadTimer)
560
+ if (this.changesBufferTimer) clearTimeout(this.changesBufferTimer)
561
+ if (this.container) {
562
+ this.container.release()
563
+ } else {
564
+ this.markForRelease = true
565
+ }
566
+ }
567
+ }