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,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
+ }