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,7 @@
1
+ export { useArSyncFetch } from '../core/hooksBase'
2
+ import { useArSyncModelWithClass, Request, DataAndStatus } from '../core/hooksBase'
3
+ import ArSyncModel from './ArSyncModel'
4
+
5
+ export function useArSyncModel<T>(request: Request | null): DataAndStatus<T> {
6
+ return useArSyncModelWithClass<T>(ArSyncModel, request)
7
+ }
@@ -0,0 +1,2 @@
1
+ import ArSyncModel from './ArSyncModel'
2
+ export default ArSyncModel
@@ -0,0 +1,145 @@
1
+ import ArSyncStore from './ArSyncStore'
2
+ import ArSyncAPI from '../core/ArSyncApi'
3
+ import ArSyncConnectionManager from '../core/ConnectionManager'
4
+ import ArSyncModelBase from '../core/ArSyncModelBase'
5
+ import ConnectionAdapter from '../core/ConnectionAdapter'
6
+
7
+ class ArSyncRecord {
8
+ immutable
9
+ request
10
+ subscriptions
11
+ store
12
+ retryLoadTimer
13
+ data
14
+ bufferTimer
15
+ bufferedPatches
16
+ eventListeners
17
+ networkSubscription
18
+ complete: boolean
19
+ notfound?: boolean
20
+ static connectionManager
21
+ constructor(request, option = {} as { immutable?: boolean }) {
22
+ this.immutable = option.immutable ? true : false
23
+ this.request = request
24
+ this.subscriptions = []
25
+ this.store = null
26
+ this.data = null
27
+ this.complete = false
28
+ this.bufferedPatches = []
29
+ this.eventListeners = { events: {}, serial: 0 }
30
+ this.networkSubscription = ArSyncRecord.connectionManager.subscribeNetwork((status) => {
31
+ if (this.notfound) {
32
+ this.trigger('connection', status)
33
+ return
34
+ }
35
+ if (status) {
36
+ this.load(() => {
37
+ this.trigger('connection', status)
38
+ this.trigger('change', { path: [], value: this.data })
39
+ })
40
+ } else {
41
+ this.unsubscribeAll()
42
+ this.trigger('connection', false)
43
+ }
44
+ })
45
+ this.load(() => {
46
+ this.trigger('load')
47
+ this.trigger('change', { path: [], value: this.data })
48
+ })
49
+ }
50
+ release() {
51
+ this.unsubscribeAll()
52
+ this.networkSubscription.unsubscribe()
53
+ }
54
+ unsubscribeAll() {
55
+ if (this.retryLoadTimer) clearTimeout(this.retryLoadTimer)
56
+ for (const s of this.subscriptions) s.unsubscribe()
57
+ this.subscriptions = []
58
+ }
59
+ load(callback, retryCount = 0) {
60
+ ArSyncAPI.syncFetch(this.request).then(syncData => {
61
+ const { keys, data, limit, order } = syncData as any
62
+ this.initializeStore(keys, data, { limit, order, immutable: this.immutable })
63
+ if (callback) callback(true, this.data)
64
+ }).catch(e => {
65
+ console.error(e)
66
+ if (e.retry) {
67
+ this.retryLoad(callback, retryCount + 1)
68
+ } else {
69
+ this.initializeStore(null, null, null)
70
+ if (callback) callback(false, this.data)
71
+ }
72
+ })
73
+ }
74
+ retryLoad(callback, retryCount) {
75
+ const sleepSeconds = Math.min(Math.pow(2, retryCount), 30)
76
+ this.retryLoadTimer = setTimeout(() => {
77
+ this.retryLoadTimer = null
78
+ this.load(callback, retryCount)
79
+ }, sleepSeconds * 1000)
80
+ }
81
+ patchReceived(patch) {
82
+ const buffer = this.bufferedPatches
83
+ buffer.push(patch)
84
+ if (this.bufferTimer) return
85
+ this.bufferTimer = setTimeout(() => {
86
+ this.bufferTimer = null
87
+ this.bufferedPatches
88
+ const buf = this.bufferedPatches
89
+ this.bufferedPatches = []
90
+ const { changes, events } = this.store.batchUpdate(buf)
91
+ this.data = this.store.data
92
+ changes.forEach(change => this.trigger('change', change))
93
+ events.forEach(event => {
94
+ this.trigger(event.type, event.data)
95
+ })
96
+ }, 16)
97
+ }
98
+ subscribe(event, callback) {
99
+ let listeners = this.eventListeners.events[event]
100
+ if (!listeners) this.eventListeners.events[event] = listeners = {}
101
+ const id = this.eventListeners.serial++
102
+ listeners[id] = callback
103
+ return { unsubscribe: () => { delete listeners[id] } }
104
+ }
105
+ trigger(event, arg?) {
106
+ const listeners = this.eventListeners.events[event]
107
+ if (!listeners) return
108
+ for (const id in listeners) listeners[id](arg)
109
+ }
110
+ initializeStore(keys, data, option) {
111
+ this.complete = true
112
+ if (!keys) {
113
+ this.notfound = true
114
+ return
115
+ }
116
+ this.notfound = false
117
+ const query = this.request.query
118
+ if (this.store) {
119
+ this.store.replaceData(data)
120
+ } else {
121
+ this.store = new ArSyncStore(query, data, option)
122
+ this.data = this.store.data
123
+ }
124
+ this.subscriptions = keys.map(key => {
125
+ return ArSyncRecord.connectionManager.subscribe(key, patch => this.patchReceived(patch))
126
+ })
127
+ }
128
+ }
129
+
130
+ export default class ArSyncModel<T> extends ArSyncModelBase<T> {
131
+ static setConnectionAdapter(adapter: ConnectionAdapter) {
132
+ ArSyncRecord.connectionManager = new ArSyncConnectionManager(adapter)
133
+ }
134
+ static createRefModel(request, option) {
135
+ return new ArSyncRecord(request, option)
136
+ }
137
+ refManagerClass() {
138
+ return ArSyncModel
139
+ }
140
+ connectionManager() {
141
+ return ArSyncRecord.connectionManager
142
+ }
143
+ static _cache = {}
144
+ static cacheTimeout = 10 * 1000
145
+ }
@@ -0,0 +1,323 @@
1
+ class Updator {
2
+ changes
3
+ markedForFreezeObjects
4
+ immutable
5
+ data
6
+ constructor(immutable) {
7
+ this.changes = []
8
+ this.markedForFreezeObjects = []
9
+ this.immutable = immutable
10
+ }
11
+ static createFrozenObject(obj) {
12
+ if (!obj) return obj
13
+ if (obj.constructor === Array) {
14
+ obj = obj.map(el => Updator.createFrozenObject(el))
15
+ } else if (typeof obj === 'object') {
16
+ obj = Object.assign({}, obj)
17
+ for (const key in obj) {
18
+ obj[key] = Updator.createFrozenObject(obj[key])
19
+ }
20
+ }
21
+ Object.freeze(obj)
22
+ return obj
23
+ }
24
+ replaceData(data, newData) {
25
+ if (this.immutable) return Updator.createFrozenObject(newData)
26
+ return this.recursivelyReplaceData(data, newData)
27
+ }
28
+ recursivelyReplaceData(data, newData) {
29
+ const replaceArray = (as, bs) => {
30
+ const aids = {}
31
+ for (const a of as) {
32
+ if (!a.id) return false
33
+ aids[a.id] = a
34
+ }
35
+ const order = {}
36
+ bs.forEach((b, i) => {
37
+ if (!b.id) return false
38
+ if (aids[b.id]) {
39
+ replaceObject(aids[b.id], b)
40
+ } else {
41
+ as.push(b)
42
+ }
43
+ order[b.id] = i + 1
44
+ })
45
+ as.sort((a, b) => {
46
+ const oa = order[a.id] || Infinity
47
+ const ob = order[b.id] || Infinity
48
+ return oa > ob ? +1 : oa < ob ? -1 : 0
49
+ })
50
+ while (as.length && !order[as[as.length - 1].id]) as.pop()
51
+ return true
52
+ }
53
+ const replaceObject = (aobj, bobj) => {
54
+ const keys = {}
55
+ for (const key in aobj) keys[key] = true
56
+ for (const key in bobj) keys[key] = true
57
+ for (const key in keys) {
58
+ const a = aobj[key]
59
+ const b = bobj[key]
60
+ if ((a instanceof Array) && (b instanceof Array)) {
61
+ if (!replaceArray(a, b)) aobj[key] = b
62
+ } else if(a && b && (typeof a === 'object') && (typeof b === 'object') && !(a instanceof Array) && !(b instanceof Array)) {
63
+ replaceObject(a, b)
64
+ } else if (a !== b) {
65
+ aobj[key] = b
66
+ }
67
+ }
68
+ }
69
+ replaceObject(data, newData)
70
+ return data
71
+ }
72
+ mark(obj) {
73
+ if (!this.immutable) return obj
74
+ if (!Object.isFrozen(this.data)) return obj
75
+ const mobj = (obj.constructor === Array) ? [...obj] : { ...obj }
76
+ this.markedForFreezeObjects.push(mobj)
77
+ return mobj
78
+ }
79
+ trace(data, path) {
80
+ path.forEach(key => {
81
+ if (this.immutable) data[key] = this.mark(data[key])
82
+ data = data[key]
83
+ })
84
+ return data
85
+ }
86
+ assign(el, path, column, value, orderParam) {
87
+ if (this.immutable) value = Updator.createFrozenObject(value)
88
+ if (el.constructor === Array && !el[column]) {
89
+ this.changes.push({
90
+ path: path.concat([value.id]),
91
+ target: el,
92
+ id: value.id,
93
+ valueWas: null,
94
+ value
95
+ })
96
+ const limitReached = orderParam && orderParam.limit != null && el.length === orderParam.limit
97
+ let removed
98
+ if (orderParam && orderParam.order == 'desc') {
99
+ el.unshift(value)
100
+ if (limitReached) removed = el.pop()
101
+ } else {
102
+ el.push(value)
103
+ if (limitReached) removed = el.pop()
104
+ }
105
+ if (removed) this.changes.push({
106
+ path: path.concat([removed.id]),
107
+ target: el,
108
+ id: removed.id,
109
+ valueWas: removed,
110
+ value: null
111
+ })
112
+ } else if (!this.valueEquals(el[column], value)) {
113
+ this.changes.push({
114
+ path: path.concat([column]),
115
+ target: el,
116
+ column: column,
117
+ valueWas: el[column],
118
+ value
119
+ })
120
+ el[column] = value
121
+ }
122
+ }
123
+ valueEquals(a, b) {
124
+ if (a === b) return true
125
+ if (!a || !b) return a == b
126
+ if (typeof a !== 'object') return false
127
+ if (typeof b !== 'object') return false
128
+ const ja = JSON.stringify(a)
129
+ const jb = JSON.stringify(b)
130
+ return ja === jb
131
+ }
132
+ add(tree, accessKeys, path, column, value, orderParam) {
133
+ const root = this.mark(tree)
134
+ const data = this.trace(root, accessKeys)
135
+ if (data) this.assign(data, path, column, value, orderParam)
136
+ return root
137
+ }
138
+ remove(tree, accessKeys, path, column) {
139
+ const root = this.mark(tree)
140
+ let data = this.trace(root, accessKeys)
141
+ if (!data) return root
142
+ if (data.constructor === Array) {
143
+ this.changes.push({
144
+ path: path.concat([data[column].id]),
145
+ target: data,
146
+ id: data[column].id,
147
+ valueWas: data[column],
148
+ value: null
149
+ })
150
+ data.splice(column, 1)
151
+ } else if (data[column] !== null) {
152
+ this.changes.push({
153
+ path: path.concat([column]),
154
+ target: data,
155
+ column: column,
156
+ valueWas: data[column],
157
+ value: null
158
+ })
159
+ data[column] = null
160
+ }
161
+ return root
162
+ }
163
+ cleanup() {
164
+ this.markedForFreezeObjects.forEach(mobj => Object.freeze(mobj))
165
+ }
166
+ }
167
+
168
+ export default class ArSyncStore {
169
+ data
170
+ query
171
+ immutable
172
+ constructor(query, data, option = {} as { immutable?: boolean }) {
173
+ this.data = option.immutable ? Updator.createFrozenObject(data) : data
174
+ this.query = ArSyncStore.parseQuery(query)
175
+ this.immutable = option.immutable
176
+ }
177
+ replaceData(data) {
178
+ this.data = new Updator(this.immutable).replaceData(this.data, data)
179
+ }
180
+ batchUpdate(patches) {
181
+ const events = []
182
+ const updator = new Updator(this.immutable)
183
+ patches.forEach(patch => this._update(patch, updator, events))
184
+ updator.cleanup()
185
+ return { changes: updator.changes, events }
186
+ }
187
+ update(patch) {
188
+ return this.batchUpdate([patch])
189
+ }
190
+ _slicePatch(patchData, query) {
191
+ const obj = {}
192
+ for (const key in patchData) {
193
+ if (key === 'id' || query.attributes['*']) {
194
+ obj[key] = patchData[key]
195
+ } else {
196
+ const subq = query.attributes[key]
197
+ if (subq) {
198
+ obj[subq.column || key] = patchData[key]
199
+ }
200
+ }
201
+ }
202
+ return obj
203
+ }
204
+ _applyPatch(data, accessKeys, actualPath, updator, query, patchData) {
205
+ for (const key in patchData) {
206
+ const subq = query.attributes[key]
207
+ const value = patchData[key]
208
+ if (subq || query.attributes['*']) {
209
+ const subcol = (subq && subq.column) || key
210
+ if (data[subcol] !== value) {
211
+ this.data = updator.add(this.data, accessKeys, actualPath, subcol, value)
212
+ }
213
+ }
214
+ }
215
+ }
216
+ _update(patch, updator, events) {
217
+ const { action, path } = patch
218
+ const patchData = patch.data
219
+ let query = this.query
220
+ let data = this.data
221
+ const actualPath: (string | number)[] = []
222
+ const accessKeys: (string | number)[] = []
223
+ for (let i = 0; i < path.length - 1; i++) {
224
+ const nameOrId = path[i]
225
+ if (typeof(nameOrId) === 'number') {
226
+ const idx = data.findIndex(o => o.id === nameOrId)
227
+ if (idx < 0) return
228
+ actualPath.push(nameOrId)
229
+ accessKeys.push(idx)
230
+ data = data[idx]
231
+ } else {
232
+ const { attributes } = query
233
+ if (!attributes[nameOrId]) return
234
+ const column = attributes[nameOrId].column || nameOrId
235
+ query = attributes[nameOrId]
236
+ actualPath.push(column)
237
+ accessKeys.push(column)
238
+ data = data[column]
239
+ }
240
+ if (!data) return
241
+ }
242
+ const nameOrId = path[path.length - 1]
243
+ let id, idx, column, target = data
244
+ if (typeof(nameOrId) === 'number') {
245
+ id = nameOrId
246
+ idx = data.findIndex(o => o.id === id)
247
+ target = data[idx]
248
+ } else if (nameOrId) {
249
+ const { attributes } = query
250
+ if (!attributes[nameOrId]) return
251
+ column = attributes[nameOrId].column || nameOrId
252
+ query = attributes[nameOrId]
253
+ target = data[column]
254
+ }
255
+ if (action === 'create') {
256
+ const obj = this._slicePatch(patchData, query)
257
+ if (column) {
258
+ this.data = updator.add(this.data, accessKeys, actualPath, column, obj)
259
+ } else if (!target) {
260
+ const ordering = Object.assign({}, patch.ordering)
261
+ const limitOverride = query.params && query.params.limit
262
+ ordering.order = query.params && query.params.order || ordering.order
263
+ if (ordering.limit == null || limitOverride != null && limitOverride < ordering.limit) ordering.limit = limitOverride
264
+ this.data = updator.add(this.data, accessKeys, actualPath, data.length, obj, ordering)
265
+ }
266
+ return
267
+ }
268
+ if (action === 'destroy') {
269
+ if (column) {
270
+ this.data = updator.remove(this.data, accessKeys, actualPath, column)
271
+ } else if (idx >= 0) {
272
+ this.data = updator.remove(this.data, accessKeys, actualPath, idx)
273
+ }
274
+ return
275
+ }
276
+ if (!target) return
277
+ if (column) {
278
+ actualPath.push(column)
279
+ accessKeys.push(column)
280
+ } else if (id) {
281
+ actualPath.push(id)
282
+ accessKeys.push(idx)
283
+ }
284
+ if (action === 'update') {
285
+ this._applyPatch(target, accessKeys, actualPath, updator, query, patchData)
286
+ } else {
287
+ const eventData = { target, path: actualPath, data: patchData.data }
288
+ events.push({ type: patchData.type, data: eventData })
289
+ }
290
+ }
291
+
292
+ static parseQuery(query, attrsonly?){
293
+ const attributes = {}
294
+ let column = null
295
+ let params = null
296
+ if (query.constructor !== Array) query = [query]
297
+ for (const arg of query) {
298
+ if (typeof(arg) === 'string') {
299
+ attributes[arg] = {}
300
+ } else if (typeof(arg) === 'object') {
301
+ for (const key in arg){
302
+ const value = arg[key]
303
+ if (attrsonly) {
304
+ attributes[key] = this.parseQuery(value)
305
+ continue
306
+ }
307
+ if (key === 'attributes') {
308
+ const child = this.parseQuery(value, true)
309
+ for (const k in child) attributes[k] = child[k]
310
+ } else if (key === 'as') {
311
+ column = value
312
+ } else if (key === 'params') {
313
+ params = value
314
+ } else {
315
+ attributes[key] = this.parseQuery(value)
316
+ }
317
+ }
318
+ }
319
+ }
320
+ if (attrsonly) return attributes
321
+ return { attributes, column, params }
322
+ }
323
+ }