ar_sync 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
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
+ }