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