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.
- checksums.yaml +7 -0
- data/.gitignore +13 -0
- data/.travis.yml +5 -0
- data/Gemfile +6 -0
- data/Gemfile.lock +53 -0
- data/LICENSE.txt +21 -0
- data/README.md +128 -0
- data/Rakefile +10 -0
- data/ar_sync.gemspec +28 -0
- data/bin/console +12 -0
- data/bin/setup +8 -0
- data/core/ActioncableAdapter.d.ts +10 -0
- data/core/ActioncableAdapter.js +29 -0
- data/core/ArSyncApi.d.ts +5 -0
- data/core/ArSyncApi.js +74 -0
- data/core/ArSyncModelBase.d.ts +71 -0
- data/core/ArSyncModelBase.js +110 -0
- data/core/ConnectionAdapter.d.ts +7 -0
- data/core/ConnectionAdapter.js +2 -0
- data/core/ConnectionManager.d.ts +19 -0
- data/core/ConnectionManager.js +75 -0
- data/core/DataType.d.ts +60 -0
- data/core/DataType.js +2 -0
- data/core/hooksBase.d.ts +29 -0
- data/core/hooksBase.js +80 -0
- data/graph/ArSyncModel.d.ts +10 -0
- data/graph/ArSyncModel.js +22 -0
- data/graph/ArSyncStore.d.ts +28 -0
- data/graph/ArSyncStore.js +593 -0
- data/graph/hooks.d.ts +3 -0
- data/graph/hooks.js +10 -0
- data/graph/index.d.ts +2 -0
- data/graph/index.js +4 -0
- data/lib/ar_sync.rb +25 -0
- data/lib/ar_sync/class_methods.rb +215 -0
- data/lib/ar_sync/collection.rb +83 -0
- data/lib/ar_sync/config.rb +18 -0
- data/lib/ar_sync/core.rb +138 -0
- data/lib/ar_sync/field.rb +96 -0
- data/lib/ar_sync/instance_methods.rb +130 -0
- data/lib/ar_sync/rails.rb +155 -0
- data/lib/ar_sync/type_script.rb +80 -0
- data/lib/ar_sync/version.rb +3 -0
- data/lib/generators/ar_sync/install/install_generator.rb +87 -0
- data/lib/generators/ar_sync/types/types_generator.rb +11 -0
- data/package-lock.json +1115 -0
- data/package.json +19 -0
- data/src/core/ActioncableAdapter.ts +30 -0
- data/src/core/ArSyncApi.ts +75 -0
- data/src/core/ArSyncModelBase.ts +126 -0
- data/src/core/ConnectionAdapter.ts +5 -0
- data/src/core/ConnectionManager.ts +69 -0
- data/src/core/DataType.ts +73 -0
- data/src/core/hooksBase.ts +86 -0
- data/src/graph/ArSyncModel.ts +21 -0
- data/src/graph/ArSyncStore.ts +567 -0
- data/src/graph/hooks.ts +7 -0
- data/src/graph/index.ts +2 -0
- data/src/tree/ArSyncModel.ts +145 -0
- data/src/tree/ArSyncStore.ts +323 -0
- data/src/tree/hooks.ts +7 -0
- data/src/tree/index.ts +2 -0
- data/tree/ArSyncModel.d.ts +39 -0
- data/tree/ArSyncModel.js +143 -0
- data/tree/ArSyncStore.d.ts +21 -0
- data/tree/ArSyncStore.js +365 -0
- data/tree/hooks.d.ts +3 -0
- data/tree/hooks.js +10 -0
- data/tree/index.d.ts +2 -0
- data/tree/index.js +4 -0
- data/tsconfig.json +15 -0
- data/vendor/assets/javascripts/ar_sync_actioncable_adapter.js.erb +7 -0
- data/vendor/assets/javascripts/ar_sync_graph.js.erb +17 -0
- data/vendor/assets/javascripts/ar_sync_tree.js.erb +17 -0
- 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
|
+
}
|