ar_sync 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
+
}
|