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
data/package.json ADDED
@@ -0,0 +1,19 @@
1
+ {
2
+ "name": "ar_sync",
3
+ "version": "0.0.1",
4
+ "scripts": {
5
+ "prepare": "tsc",
6
+ "build": "tsc"
7
+ },
8
+ "dependencies": {},
9
+ "devDependencies": {
10
+ "react": "^16.8.6",
11
+ "@types/react": "^16.8.6",
12
+ "actioncable": "^5.2.0",
13
+ "@types/actioncable": "^5.2.0",
14
+ "eslint": "^5.16.0",
15
+ "typescript": "^3.4.5",
16
+ "@typescript-eslint/eslint-plugin": "^1.6.0",
17
+ "@typescript-eslint/parser": "^1.6.0"
18
+ }
19
+ }
@@ -0,0 +1,30 @@
1
+ import * as ActionCable from 'actioncable'
2
+ import ConnectionAdapter from './ConnectionAdapter'
3
+
4
+ export default class ActionCableAdapter implements ConnectionAdapter {
5
+ connected: boolean
6
+ _cable: ActionCable.Cable
7
+ constructor() {
8
+ this.connected = true
9
+ this.subscribe(Math.random().toString(), () => {})
10
+ }
11
+ subscribe(key: string, received: (data: any) => void) {
12
+ const disconnected = () => {
13
+ if (!this.connected) return
14
+ this.connected = false
15
+ this.ondisconnect()
16
+ }
17
+ const connected = () => {
18
+ if (this.connected) return
19
+ this.connected = true
20
+ this.onreconnect()
21
+ }
22
+ if (!this._cable) this._cable = ActionCable.createConsumer()
23
+ return this._cable.subscriptions.create(
24
+ { channel: 'SyncChannel', key },
25
+ { received, disconnected, connected }
26
+ )
27
+ }
28
+ ondisconnect() {}
29
+ onreconnect() {}
30
+ }
@@ -0,0 +1,75 @@
1
+ async function apiBatchFetch(endpoint: string, requests: object[]) {
2
+ const headers = {
3
+ 'Accept': 'application/json',
4
+ 'Content-Type': 'application/json'
5
+ }
6
+ const body = JSON.stringify({ requests })
7
+ const option = { credentials: 'include', method: 'POST', headers, body } as const
8
+ const res = await fetch(endpoint, option)
9
+ if (res.status === 200) return res.json()
10
+ throw new Error(res.statusText)
11
+ }
12
+
13
+ interface PromiseCallback {
14
+ resolve: (data: object) => void
15
+ reject: (error: object) => void
16
+ }
17
+
18
+ class ApiFetcher {
19
+ endpoint: string
20
+ batches: [object, PromiseCallback][] = []
21
+ batchFetchTimer: number | null = null
22
+ constructor(endpoint: string) {
23
+ this.endpoint = endpoint
24
+ }
25
+ fetch(request: object) {
26
+ return new Promise((resolve, reject) => {
27
+ this.batches.push([request, { resolve, reject }])
28
+ if (this.batchFetchTimer) return
29
+ this.batchFetchTimer = setTimeout(() => {
30
+ this.batchFetchTimer = null
31
+ const compacts: { [key: string]: PromiseCallback[] } = {}
32
+ const requests: object[] = []
33
+ const callbacksList: PromiseCallback[][] = []
34
+ for (const batch of this.batches) {
35
+ const request = batch[0]
36
+ const callback = batch[1]
37
+ const key = JSON.stringify(request)
38
+ if (compacts[key]) {
39
+ compacts[key].push(callback)
40
+ } else {
41
+ requests.push(request)
42
+ callbacksList.push(compacts[key] = [callback])
43
+ }
44
+ }
45
+ this.batches = []
46
+ apiBatchFetch(this.endpoint, requests).then((results) => {
47
+ for (const i in callbacksList) {
48
+ const result = results[i]
49
+ const callbacks = callbacksList[i]
50
+ for (const callback of callbacks) {
51
+ if (result.data) {
52
+ callback.resolve(result.data)
53
+ } else {
54
+ const error = result.error || { type: 'Unknown Error' }
55
+ callback.reject(error)
56
+ }
57
+ }
58
+ }
59
+ }).catch((e) => {
60
+ const error = { type: e.name, message: e.message, retry: true }
61
+ for (const callbacks of callbacksList) {
62
+ for (const callback of callbacks) callback.reject(error)
63
+ }
64
+ })
65
+ }, 16)
66
+ })
67
+ }
68
+ }
69
+
70
+ const staticFetcher = new ApiFetcher('/static_api')
71
+ const syncFetcher = new ApiFetcher('/sync_api')
72
+ export default {
73
+ fetch: (request: object) => staticFetcher.fetch(request),
74
+ syncFetch: (request: object) => syncFetcher.fetch(request),
75
+ }
@@ -0,0 +1,126 @@
1
+ interface Request { api: string; query: any; params?: any }
2
+ type Path = (string | number)[]
3
+ interface Change { path: Path; value: any }
4
+ type ChangeCallback = (change: Change) => void
5
+ type LoadCallback = () => void
6
+ type ConnectionCallback = (status: boolean) => void
7
+ type SubscriptionType = 'load' | 'change' | 'connection'
8
+ type SubscriptionCallback = ChangeCallback | LoadCallback | ConnectionCallback
9
+ interface Adapter {
10
+ subscribe: (key: string, received: (data: any) => void) => { unsubscribe: () => void }
11
+ ondisconnect: () => void
12
+ onreconnect: () => void
13
+ }
14
+
15
+ export default abstract class ArSyncModelBase<T> {
16
+ private _ref
17
+ private _listenerSerial: number
18
+ private _listeners
19
+ complete: boolean
20
+ notfound?: boolean
21
+ connected: boolean
22
+ data: T | null
23
+ static _cache: { [key: string]: { key: string; count: number; timer: number | null; model } }
24
+ static cacheTimeout: number
25
+ abstract refManagerClass(): any
26
+ abstract connectionManager(): { networkStatus: boolean }
27
+ constructor(request: Request, option?: { immutable: boolean }) {
28
+ this._ref = this.refManagerClass().retrieveRef(request, option)
29
+ this._listenerSerial = 0
30
+ this._listeners = {}
31
+ this.complete = false
32
+ this.connected = this.connectionManager().networkStatus
33
+ const setData = () => {
34
+ this.data = this._ref.model.data
35
+ this.complete = this._ref.model.complete
36
+ this.notfound = this._ref.model.notfound
37
+ }
38
+ setData()
39
+ this.subscribe('load', setData)
40
+ this.subscribe('change', setData)
41
+ this.subscribe('connection', (status: boolean) => {
42
+ this.connected = status
43
+ })
44
+ }
45
+ onload(callback: LoadCallback) {
46
+ this.subscribeOnce('load', callback)
47
+ }
48
+ subscribeOnce(event: SubscriptionType, callback: SubscriptionCallback) {
49
+ const subscription = this.subscribe(event, (arg) => {
50
+ (callback as (arg: any) => void)(arg)
51
+ subscription.unsubscribe()
52
+ })
53
+ return subscription
54
+ }
55
+ subscribe(event: SubscriptionType, callback: SubscriptionCallback): { unsubscribe: () => void } {
56
+ const id = this._listenerSerial++
57
+ const subscription = this._ref.model.subscribe(event, callback)
58
+ let unsubscribed = false
59
+ const unsubscribe = () => {
60
+ unsubscribed = true
61
+ subscription.unsubscribe()
62
+ delete this._listeners[id]
63
+ }
64
+ if (this.complete) {
65
+ if (event === 'load') setTimeout(() => {
66
+ if (!unsubscribed) (callback as LoadCallback)()
67
+ }, 0)
68
+ if (event === 'change') setTimeout(() => {
69
+ if (!unsubscribed) (callback as ChangeCallback)({ path: [], value: this.data })
70
+ }, 0)
71
+ }
72
+ return this._listeners[id] = { unsubscribe }
73
+ }
74
+ release() {
75
+ for (const id in this._listeners) this._listeners[id].unsubscribe()
76
+ this._listeners = {}
77
+ this.refManagerClass()._detach(this._ref)
78
+ this._ref = null
79
+ }
80
+ static retrieveRef(
81
+ request: Request,
82
+ option?: { immutable: boolean }
83
+ ): { key: string; count: number; timer: number | null; model } {
84
+ const key = JSON.stringify([request, option])
85
+ let ref = this._cache[key]
86
+ if (!ref) {
87
+ const model = this.createRefModel(request, option)
88
+ ref = this._cache[key] = { key, count: 0, timer: null, model }
89
+ }
90
+ this._attach(ref)
91
+ return ref
92
+ }
93
+ static createRefModel(_request: Request, _option?: { immutable: boolean }) {
94
+ throw 'abstract method'
95
+ }
96
+ static _detach(ref) {
97
+ ref.count--
98
+ const timeout = this.cacheTimeout
99
+ if (ref.count !== 0) return
100
+ const timedout = () => {
101
+ ref.model.release()
102
+ delete this._cache[ref.key]
103
+ }
104
+ if (timeout) {
105
+ ref.timer = setTimeout(timedout, timeout)
106
+ } else {
107
+ timedout()
108
+ }
109
+ }
110
+ private static _attach(ref) {
111
+ ref.count++
112
+ if (ref.timer) clearTimeout(ref.timer)
113
+ }
114
+ static setConnectionAdapter(_adapter: Adapter) {}
115
+ static waitForLoad(...models: ArSyncModelBase<{}>[]) {
116
+ return new Promise((resolve) => {
117
+ let count = 0
118
+ for (const model of models) {
119
+ model.onload(() => {
120
+ count++
121
+ if (models.length == count) resolve(models)
122
+ })
123
+ }
124
+ })
125
+ }
126
+ }
@@ -0,0 +1,5 @@
1
+ export default interface ConnectionAdapter {
2
+ ondisconnect: (() => void) | null
3
+ onreconnect: (() => void) | null
4
+ subscribe(key: string, callback: (data: any) => void): { unsubscribe: () => void }
5
+ }
@@ -0,0 +1,69 @@
1
+ export default class ConnectionManager {
2
+ subscriptions
3
+ adapter
4
+ networkListeners
5
+ networkListenerSerial
6
+ networkStatus
7
+ constructor(adapter) {
8
+ this.subscriptions = {}
9
+ this.adapter = adapter
10
+ this.networkListeners = {}
11
+ this.networkListenerSerial = 0
12
+ this.networkStatus = true
13
+ adapter.ondisconnect = () => {
14
+ this.unsubscribeAll()
15
+ this.triggerNetworkChange(false)
16
+ }
17
+ adapter.onreconnect = () => this.triggerNetworkChange(true)
18
+ }
19
+ triggerNetworkChange(status) {
20
+ if (this.networkStatus == status) return
21
+ this.networkStatus = status
22
+ for (const id in this.networkListeners) this.networkListeners[id](status)
23
+ }
24
+ unsubscribeAll() {
25
+ for (const id in this.subscriptions) {
26
+ const subscription = this.subscriptions[id]
27
+ subscription.listeners = {}
28
+ subscription.connection.unsubscribe()
29
+ }
30
+ this.subscriptions = {}
31
+ }
32
+ subscribeNetwork(func) {
33
+ const id = this.networkListenerSerial++
34
+ this.networkListeners[id] = func
35
+ const unsubscribe = () => {
36
+ delete this.networkListeners[id]
37
+ }
38
+ return { unsubscribe }
39
+ }
40
+ subscribe(key, func) {
41
+ const subscription = this.connect(key)
42
+ const id = subscription.serial++
43
+ subscription.ref++
44
+ subscription.listeners[id] = func
45
+ const unsubscribe = () => {
46
+ if (!subscription.listeners[id]) return
47
+ delete subscription.listeners[id]
48
+ subscription.ref--
49
+ if (subscription.ref === 0) this.disconnect(key)
50
+ }
51
+ return { unsubscribe }
52
+ }
53
+ connect(key) {
54
+ if (this.subscriptions[key]) return this.subscriptions[key]
55
+ const connection = this.adapter.subscribe(key, data => this.received(key, data))
56
+ return this.subscriptions[key] = { connection, listeners: {}, ref: 0, serial: 0 }
57
+ }
58
+ disconnect(key) {
59
+ const subscription = this.subscriptions[key]
60
+ if (!subscription || subscription.ref !== 0) return
61
+ delete this.subscriptions[key]
62
+ subscription.connection.unsubscribe()
63
+ }
64
+ received(key, data) {
65
+ const subscription = this.subscriptions[key]
66
+ if (!subscription) return
67
+ for (const id in subscription.listeners) subscription.listeners[id](data)
68
+ }
69
+ }
@@ -0,0 +1,73 @@
1
+ type RecordType = { _meta?: { query: any } }
2
+ type Values<T> = T extends { [K in keyof T]: infer U } ? U : never
3
+ type DataTypeExtractField<BaseType, Key extends keyof BaseType> = BaseType[Key] extends RecordType
4
+ ? (null extends BaseType[Key] ? {} | null : {})
5
+ : BaseType[Key] extends RecordType[]
6
+ ? {}[]
7
+ : BaseType[Key]
8
+
9
+ type DataTypeExtractFieldsFromQuery<BaseType, Fields> = '*' extends Fields
10
+ ? { [key in Exclude<keyof BaseType, '_meta'>]: DataTypeExtractField<BaseType, key> }
11
+ : { [key in Fields & keyof (BaseType)]: DataTypeExtractField<BaseType, key> }
12
+
13
+ interface ExtraFieldErrorType {
14
+ extraFieldError: any
15
+ }
16
+
17
+ type DataTypeExtractFromQueryHash<BaseType, QueryType> = '*' extends keyof QueryType
18
+ ? {
19
+ [key in Exclude<(keyof BaseType) | (keyof QueryType), '_meta' | '_params' | '*'>]: (key extends keyof BaseType
20
+ ? (key extends keyof QueryType
21
+ ? (QueryType[key] extends true
22
+ ? DataTypeExtractField<BaseType, key>
23
+ : DataTypeFromQuery<BaseType[key] & {}, QueryType[key]>)
24
+ : DataTypeExtractField<BaseType, key>)
25
+ : ExtraFieldErrorType)
26
+ }
27
+ : {
28
+ [key in keyof QueryType]: (key extends keyof BaseType
29
+ ? (QueryType[key] extends true
30
+ ? DataTypeExtractField<BaseType, key>
31
+ : DataTypeFromQuery<BaseType[key] & {}, QueryType[key]>)
32
+ : ExtraFieldErrorType)
33
+ }
34
+
35
+ type _DataTypeFromQuery<BaseType, QueryType> = QueryType extends keyof BaseType | '*'
36
+ ? DataTypeExtractFieldsFromQuery<BaseType, QueryType>
37
+ : QueryType extends Readonly<(keyof BaseType | '*')[]>
38
+ ? DataTypeExtractFieldsFromQuery<BaseType, Values<QueryType>>
39
+ : QueryType extends { as: string }
40
+ ? { error: 'type for alias field is not supported' } | undefined
41
+ : DataTypeExtractFromQueryHash<BaseType, QueryType>
42
+
43
+ export type DataTypeFromQuery<BaseType, QueryType> = BaseType extends any[]
44
+ ? CheckAttributesField<BaseType[0], QueryType>[]
45
+ : null extends BaseType
46
+ ? CheckAttributesField<BaseType & {}, QueryType> | null
47
+ : CheckAttributesField<BaseType & {}, QueryType>
48
+
49
+ type CheckAttributesField<P, Q> = Q extends { attributes: infer R }
50
+ ? _DataTypeFromQuery<P, R>
51
+ : _DataTypeFromQuery<P, Q>
52
+
53
+ type IsAnyCompareLeftType = { __any: never }
54
+
55
+ type CollectExtraFields<Type, Path> = ExtraFieldErrorType extends Type
56
+ ? (IsAnyCompareLeftType extends Type ? null : Path)
57
+ : _CollectExtraFields<Type extends (infer R)[] ? R : (Type extends object ? Type : null)>
58
+
59
+ type _CollectExtraFields<Type> = keyof (Type) extends never
60
+ ? null
61
+ : Values<{ [key in keyof Type]: CollectExtraFields<Type[key], [key]> }>
62
+
63
+ type SelectString<T> = T extends string ? T : never
64
+ type _ValidateDataTypeExtraFileds<Extra, Type> = SelectString<Values<Extra>> extends never
65
+ ? Type
66
+ : { error: { extraFields: SelectString<Values<Extra>> } }
67
+ type ValidateDataTypeExtraFileds<Type> = _ValidateDataTypeExtraFileds<CollectExtraFields<Type, []>, Type>
68
+
69
+ type RequestBase = { api: string; query: any; params?: any; _meta?: { data: any } }
70
+ type DataTypeBaseFromRequestType<R> = R extends { _meta?: { data: infer DataType } } ? DataType : never
71
+ export type DataTypeFromRequest<Req extends RequestBase, R extends RequestBase> = ValidateDataTypeExtraFileds<
72
+ DataTypeFromQuery<DataTypeBaseFromRequestType<Req>, R['query']>
73
+ >
@@ -0,0 +1,86 @@
1
+ import { useState, useEffect, useCallback } from 'react'
2
+ import ArSyncAPI from './ArSyncApi'
3
+
4
+ interface ModelStatus { complete: boolean; notfound?: boolean; connected: boolean }
5
+ export type DataAndStatus<T> = [T | null, ModelStatus]
6
+ export interface Request { api: string; params?: any; query: any }
7
+
8
+ interface ArSyncModel<T> {
9
+ data: T | null
10
+ complete: boolean
11
+ connected: boolean
12
+ notfound?: boolean
13
+ release(): void
14
+ subscribe(type: any, callback: any): any
15
+ }
16
+ export function useArSyncModelWithClass<T>(modelClass: { new<T>(req: Request, option?: any): ArSyncModel<T> }, request: Request | null): DataAndStatus<T> {
17
+ const [data, setData] = useState<T | null>(null)
18
+ const [status, setStatus] = useState<ModelStatus>({ complete: false, connected: true })
19
+ const updateStatus = (complete: boolean, notfound: boolean | undefined, connected: boolean) => {
20
+ if (complete === status.complete || notfound === status.notfound || connected === status.notfound) return
21
+ setStatus({ complete, notfound, connected })
22
+ }
23
+ useEffect(() => {
24
+ if (!request) return () => {}
25
+ const model = new modelClass<T>(request, { immutable: true })
26
+ if (model.complete) setData(model.data)
27
+ updateStatus(model.complete, model.notfound, model.connected)
28
+ model.subscribe('change', () => {
29
+ updateStatus(model.complete, model.notfound, model.connected)
30
+ setData(model.data)
31
+ })
32
+ model.subscribe('connection', () => {
33
+ updateStatus(model.complete, model.notfound, model.connected)
34
+ })
35
+ return () => model.release()
36
+ }, [JSON.stringify(request && request.params)])
37
+ return [data, status]
38
+ }
39
+
40
+
41
+ interface FetchStatus { complete: boolean; notfound?: boolean }
42
+ type DataAndStatusAndUpdater<T> = [T | null, FetchStatus, () => void]
43
+ export function useArSyncFetch<T>(request: Request | null): DataAndStatusAndUpdater<T> {
44
+ const [response, setResponse] = useState<T | null>(null)
45
+ const [status, setStatus] = useState<FetchStatus>({ complete: false })
46
+ const requestString = JSON.stringify(request && request.params)
47
+ let canceled = false
48
+ let timer: number | null = null
49
+ const update = useCallback(() => {
50
+ if (!request) {
51
+ setStatus({ complete: false, notfound: undefined })
52
+ return () => {}
53
+ }
54
+ canceled = false
55
+ timer = null
56
+ const fetch = (count: number) => {
57
+ if (timer) clearTimeout(timer)
58
+ timer = null
59
+ ArSyncAPI.fetch(request)
60
+ .then((response) => {
61
+ if (canceled) return
62
+ setResponse(response as T)
63
+ setStatus({ complete: true, notfound: false })
64
+ })
65
+ .catch(e => {
66
+ if (canceled) return
67
+ if (!e.retry) {
68
+ setResponse(null)
69
+ setStatus({ complete: true, notfound: true })
70
+ return
71
+ }
72
+ timer = setTimeout(() => fetch(count + 1), 1000 * Math.min(4 ** count, 30))
73
+ })
74
+ }
75
+ fetch(0)
76
+ }, [requestString])
77
+ useEffect(() => {
78
+ update()
79
+ return () => {
80
+ canceled = true
81
+ if (timer) clearTimeout(timer)
82
+ timer = null
83
+ }
84
+ }, [requestString])
85
+ return [response, status, update]
86
+ }