ar_sync 1.0.4 → 1.0.5
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/Gemfile.lock +1 -1
- data/core/ActionCableAdapter.js +19 -17
- data/core/ArSyncApi.js +112 -48
- data/core/ArSyncModel.js +69 -60
- data/core/ArSyncStore.js +401 -314
- data/core/ConnectionManager.js +43 -38
- data/core/hooks.d.ts +4 -0
- data/core/hooks.js +61 -36
- data/lib/ar_sync/type_script.rb +9 -8
- data/lib/ar_sync/version.rb +1 -1
- data/src/core/DataType.ts +1 -1
- data/src/core/hooks.ts +29 -5
- data/tsconfig.json +2 -2
- metadata +2 -2
data/core/ConnectionManager.js
CHANGED
@@ -1,75 +1,80 @@
|
|
1
1
|
"use strict";
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
3
|
-
|
4
|
-
|
3
|
+
var ConnectionManager = /** @class */ (function () {
|
4
|
+
function ConnectionManager(adapter) {
|
5
|
+
var _this = this;
|
5
6
|
this.subscriptions = {};
|
6
7
|
this.adapter = adapter;
|
7
8
|
this.networkListeners = {};
|
8
9
|
this.networkListenerSerial = 0;
|
9
10
|
this.networkStatus = true;
|
10
|
-
adapter.ondisconnect = ()
|
11
|
-
|
12
|
-
|
11
|
+
adapter.ondisconnect = function () {
|
12
|
+
_this.unsubscribeAll();
|
13
|
+
_this.triggerNetworkChange(false);
|
13
14
|
};
|
14
|
-
adapter.onreconnect = ()
|
15
|
+
adapter.onreconnect = function () { return _this.triggerNetworkChange(true); };
|
15
16
|
}
|
16
|
-
triggerNetworkChange(status) {
|
17
|
+
ConnectionManager.prototype.triggerNetworkChange = function (status) {
|
17
18
|
if (this.networkStatus == status)
|
18
19
|
return;
|
19
20
|
this.networkStatus = status;
|
20
|
-
for (
|
21
|
+
for (var id in this.networkListeners)
|
21
22
|
this.networkListeners[id](status);
|
22
|
-
}
|
23
|
-
unsubscribeAll() {
|
24
|
-
for (
|
25
|
-
|
23
|
+
};
|
24
|
+
ConnectionManager.prototype.unsubscribeAll = function () {
|
25
|
+
for (var id in this.subscriptions) {
|
26
|
+
var subscription = this.subscriptions[id];
|
26
27
|
subscription.listeners = {};
|
27
28
|
subscription.connection.unsubscribe();
|
28
29
|
}
|
29
30
|
this.subscriptions = {};
|
30
|
-
}
|
31
|
-
subscribeNetwork(func) {
|
32
|
-
|
31
|
+
};
|
32
|
+
ConnectionManager.prototype.subscribeNetwork = function (func) {
|
33
|
+
var _this = this;
|
34
|
+
var id = this.networkListenerSerial++;
|
33
35
|
this.networkListeners[id] = func;
|
34
|
-
|
35
|
-
delete
|
36
|
+
var unsubscribe = function () {
|
37
|
+
delete _this.networkListeners[id];
|
36
38
|
};
|
37
|
-
return { unsubscribe };
|
38
|
-
}
|
39
|
-
subscribe(key, func) {
|
40
|
-
|
41
|
-
|
39
|
+
return { unsubscribe: unsubscribe };
|
40
|
+
};
|
41
|
+
ConnectionManager.prototype.subscribe = function (key, func) {
|
42
|
+
var _this = this;
|
43
|
+
var subscription = this.connect(key);
|
44
|
+
var id = subscription.serial++;
|
42
45
|
subscription.ref++;
|
43
46
|
subscription.listeners[id] = func;
|
44
|
-
|
47
|
+
var unsubscribe = function () {
|
45
48
|
if (!subscription.listeners[id])
|
46
49
|
return;
|
47
50
|
delete subscription.listeners[id];
|
48
51
|
subscription.ref--;
|
49
52
|
if (subscription.ref === 0)
|
50
|
-
|
53
|
+
_this.disconnect(key);
|
51
54
|
};
|
52
|
-
return { unsubscribe };
|
53
|
-
}
|
54
|
-
connect(key) {
|
55
|
+
return { unsubscribe: unsubscribe };
|
56
|
+
};
|
57
|
+
ConnectionManager.prototype.connect = function (key) {
|
58
|
+
var _this = this;
|
55
59
|
if (this.subscriptions[key])
|
56
60
|
return this.subscriptions[key];
|
57
|
-
|
58
|
-
return this.subscriptions[key] = { connection, listeners: {}, ref: 0, serial: 0 };
|
59
|
-
}
|
60
|
-
disconnect(key) {
|
61
|
-
|
61
|
+
var connection = this.adapter.subscribe(key, function (data) { return _this.received(key, data); });
|
62
|
+
return this.subscriptions[key] = { connection: connection, listeners: {}, ref: 0, serial: 0 };
|
63
|
+
};
|
64
|
+
ConnectionManager.prototype.disconnect = function (key) {
|
65
|
+
var subscription = this.subscriptions[key];
|
62
66
|
if (!subscription || subscription.ref !== 0)
|
63
67
|
return;
|
64
68
|
delete this.subscriptions[key];
|
65
69
|
subscription.connection.unsubscribe();
|
66
|
-
}
|
67
|
-
received(key, data) {
|
68
|
-
|
70
|
+
};
|
71
|
+
ConnectionManager.prototype.received = function (key, data) {
|
72
|
+
var subscription = this.subscriptions[key];
|
69
73
|
if (!subscription)
|
70
74
|
return;
|
71
|
-
for (
|
75
|
+
for (var id in subscription.listeners)
|
72
76
|
subscription.listeners[id](data);
|
73
|
-
}
|
74
|
-
|
77
|
+
};
|
78
|
+
return ConnectionManager;
|
79
|
+
}());
|
75
80
|
exports.default = ConnectionManager;
|
data/core/hooks.d.ts
CHANGED
@@ -1,10 +1,14 @@
|
|
1
1
|
declare let useState: <T>(t: T | (() => T)) => [T, (t: T | ((t: T) => T)) => void];
|
2
2
|
declare let useEffect: (f: (() => void) | (() => (() => void)), deps: any[]) => void;
|
3
3
|
declare let useMemo: <T>(f: () => T, deps: any[]) => T;
|
4
|
+
declare let useRef: <T>(value: T) => {
|
5
|
+
current: T;
|
6
|
+
};
|
4
7
|
declare type InitializeHooksParams = {
|
5
8
|
useState: typeof useState;
|
6
9
|
useEffect: typeof useEffect;
|
7
10
|
useMemo: typeof useMemo;
|
11
|
+
useRef: typeof useRef;
|
8
12
|
};
|
9
13
|
export declare function initializeHooks(hooks: InitializeHooksParams): void;
|
10
14
|
interface ModelStatus {
|
data/core/hooks.js
CHANGED
@@ -1,37 +1,43 @@
|
|
1
1
|
"use strict";
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
3
|
+
var ArSyncApi_1 = require("./ArSyncApi");
|
4
|
+
var ArSyncModel_1 = require("./ArSyncModel");
|
5
|
+
var useState;
|
6
|
+
var useEffect;
|
7
|
+
var useMemo;
|
8
|
+
var useRef;
|
8
9
|
function initializeHooks(hooks) {
|
9
10
|
useState = hooks.useState;
|
10
11
|
useEffect = hooks.useEffect;
|
11
12
|
useMemo = hooks.useMemo;
|
13
|
+
useRef = hooks.useRef;
|
12
14
|
}
|
13
15
|
exports.initializeHooks = initializeHooks;
|
14
16
|
function checkHooks() {
|
15
17
|
if (!useState)
|
16
|
-
throw 'uninitialized. needs `initializeHooks({ useState, useEffect, useMemo })`';
|
18
|
+
throw 'uninitialized. needs `initializeHooks({ useState, useEffect, useMemo, useRef })`';
|
17
19
|
}
|
18
|
-
|
20
|
+
var initialResult = [null, { complete: false, notfound: undefined, connected: true }];
|
19
21
|
function useArSyncModel(request) {
|
20
22
|
checkHooks();
|
21
|
-
|
22
|
-
|
23
|
-
|
23
|
+
var _a = useState(initialResult), result = _a[0], setResult = _a[1];
|
24
|
+
var requestString = JSON.stringify(request && request.params);
|
25
|
+
var prevRequestStringRef = useRef(requestString);
|
26
|
+
useEffect(function () {
|
27
|
+
prevRequestStringRef.current = requestString;
|
24
28
|
if (!request) {
|
25
29
|
setResult(initialResult);
|
26
|
-
return ()
|
30
|
+
return function () { };
|
27
31
|
}
|
28
|
-
|
32
|
+
var model = new ArSyncModel_1.default(request, { immutable: true });
|
29
33
|
function update() {
|
30
|
-
|
31
|
-
setResult(resultWas
|
32
|
-
|
33
|
-
|
34
|
-
|
34
|
+
var complete = model.complete, notfound = model.notfound, connected = model.connected, data = model.data;
|
35
|
+
setResult(function (resultWas) {
|
36
|
+
var dataWas = resultWas[0], statusWas = resultWas[1];
|
37
|
+
var statusPersisted = statusWas.complete === complete && statusWas.notfound === notfound && statusWas.connected === connected;
|
38
|
+
if (dataWas === data && statusPersisted)
|
39
|
+
return resultWas;
|
40
|
+
var status = statusPersisted ? statusWas : { complete: complete, notfound: notfound, connected: connected };
|
35
41
|
return [data, status];
|
36
42
|
});
|
37
43
|
}
|
@@ -41,21 +47,38 @@ function useArSyncModel(request) {
|
|
41
47
|
else {
|
42
48
|
setResult(initialResult);
|
43
49
|
}
|
50
|
+
model.subscribe('load', update);
|
44
51
|
model.subscribe('change', update);
|
45
52
|
model.subscribe('connection', update);
|
46
|
-
return ()
|
53
|
+
return function () { return model.release(); };
|
47
54
|
}, [requestString]);
|
48
|
-
return result;
|
55
|
+
return prevRequestStringRef.current === requestString ? result : initialResult;
|
49
56
|
}
|
50
57
|
exports.useArSyncModel = useArSyncModel;
|
51
|
-
|
58
|
+
var initialFetchState = { data: null, status: { complete: false, notfound: undefined } };
|
59
|
+
function extractParams(query, output) {
|
60
|
+
if (output === void 0) { output = []; }
|
61
|
+
if (typeof (query) !== 'object' || query == null || Array.isArray(query))
|
62
|
+
return output;
|
63
|
+
if ('params' in query)
|
64
|
+
output.push(query.params);
|
65
|
+
for (var key in query) {
|
66
|
+
extractParams(query[key], output);
|
67
|
+
}
|
68
|
+
return output;
|
69
|
+
}
|
52
70
|
function useArSyncFetch(request) {
|
53
71
|
checkHooks();
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
72
|
+
var _a = useState(initialFetchState), state = _a[0], setState = _a[1];
|
73
|
+
var query = request && request.query;
|
74
|
+
var params = request && request.params;
|
75
|
+
var requestString = useMemo(function () {
|
76
|
+
return JSON.stringify(extractParams(query, [params]));
|
77
|
+
}, [query, params]);
|
78
|
+
var prevRequestStringRef = useRef(requestString);
|
79
|
+
var loader = useMemo(function () {
|
80
|
+
var lastLoadId = 0;
|
81
|
+
var timer = null;
|
59
82
|
function cancel() {
|
60
83
|
if (timer)
|
61
84
|
clearTimeout(timer);
|
@@ -64,28 +87,28 @@ function useArSyncFetch(request) {
|
|
64
87
|
}
|
65
88
|
function fetch(request, retryCount) {
|
66
89
|
cancel();
|
67
|
-
|
68
|
-
ArSyncApi_1.default.fetch(request).then((response)
|
90
|
+
var currentLoadingId = lastLoadId;
|
91
|
+
ArSyncApi_1.default.fetch(request).then(function (response) {
|
69
92
|
if (currentLoadingId !== lastLoadId)
|
70
93
|
return;
|
71
94
|
setState({ data: response, status: { complete: true, notfound: false } });
|
72
|
-
}).catch(e
|
95
|
+
}).catch(function (e) {
|
73
96
|
if (currentLoadingId !== lastLoadId)
|
74
97
|
return;
|
75
98
|
if (!e.retry) {
|
76
99
|
setState({ data: null, status: { complete: true, notfound: true } });
|
77
100
|
return;
|
78
101
|
}
|
79
|
-
timer = setTimeout(()
|
102
|
+
timer = setTimeout(function () { return fetch(request, retryCount + 1); }, 1000 * Math.min(Math.pow(4, retryCount), 30));
|
80
103
|
});
|
81
104
|
}
|
82
105
|
function update() {
|
83
106
|
if (request) {
|
84
|
-
setState(state
|
85
|
-
|
107
|
+
setState(function (state) {
|
108
|
+
var data = state.data, status = state.status;
|
86
109
|
if (!status.complete && status.notfound === undefined)
|
87
110
|
return state;
|
88
|
-
return { data, status: { complete: false, notfound: undefined } };
|
111
|
+
return { data: data, status: { complete: false, notfound: undefined } };
|
89
112
|
});
|
90
113
|
fetch(request, 0);
|
91
114
|
}
|
@@ -93,13 +116,15 @@ function useArSyncFetch(request) {
|
|
93
116
|
setState(initialFetchState);
|
94
117
|
}
|
95
118
|
}
|
96
|
-
return { update, cancel };
|
119
|
+
return { update: update, cancel: cancel };
|
97
120
|
}, [requestString]);
|
98
|
-
useEffect(()
|
121
|
+
useEffect(function () {
|
122
|
+
prevRequestStringRef.current = requestString;
|
99
123
|
setState(initialFetchState);
|
100
124
|
loader.update();
|
101
|
-
return ()
|
125
|
+
return function () { return loader.cancel(); };
|
102
126
|
}, [requestString]);
|
103
|
-
|
127
|
+
var responseState = prevRequestStringRef.current === requestString ? state : initialFetchState;
|
128
|
+
return [responseState.data, responseState.status, loader.update];
|
104
129
|
}
|
105
130
|
exports.useArSyncFetch = useArSyncFetch;
|
data/lib/ar_sync/type_script.rb
CHANGED
@@ -75,23 +75,24 @@ module ArSync::TypeScript
|
|
75
75
|
<<~CODE
|
76
76
|
import { TypeRequest, ApiNameRequests } from './types'
|
77
77
|
import { DataTypeFromRequest as DataTypeFromRequestPair } from 'ar_sync/core/DataType'
|
78
|
-
type
|
78
|
+
export type NeverMatchArgument = { __nevermatch: never }
|
79
|
+
type DataTypeFromRequest<R extends TypeRequest | NeverMatchArgument> = NeverMatchArgument extends R ? never : R extends TypeRequest ? DataTypeFromRequestPair<ApiNameRequests[R['api']], R> : never
|
79
80
|
export default DataTypeFromRequest
|
80
81
|
CODE
|
81
82
|
end
|
82
83
|
|
83
84
|
def self.generate_hooks_script
|
84
85
|
<<~CODE
|
85
|
-
import { useState, useEffect, useMemo } from 'react'
|
86
|
+
import { useState, useEffect, useMemo, useRef } from 'react'
|
86
87
|
import { TypeRequest } from './types'
|
87
|
-
import DataTypeFromRequest from './DataTypeFromRequest'
|
88
|
+
import DataTypeFromRequest, { NeverMatchArgument } from './DataTypeFromRequest'
|
88
89
|
import { initializeHooks, useArSyncModel as useArSyncModelBase, useArSyncFetch as useArSyncFetchBase } from 'ar_sync/core/hooks'
|
89
|
-
initializeHooks({ useState, useEffect, useMemo })
|
90
|
-
export function useArSyncModel<R extends TypeRequest>(request: R | null) {
|
91
|
-
return useArSyncModelBase<DataTypeFromRequest<R>>(request)
|
90
|
+
initializeHooks({ useState, useEffect, useMemo, useRef })
|
91
|
+
export function useArSyncModel<R extends TypeRequest | NeverMatchArgument>(request: R | null) {
|
92
|
+
return useArSyncModelBase<DataTypeFromRequest<R>>(request as TypeRequest)
|
92
93
|
}
|
93
|
-
export function useArSyncFetch<R extends TypeRequest>(request: R | null) {
|
94
|
-
return useArSyncFetchBase<DataTypeFromRequest<R>>(request)
|
94
|
+
export function useArSyncFetch<R extends TypeRequest | NeverMatchArgument>(request: R | null) {
|
95
|
+
return useArSyncFetchBase<DataTypeFromRequest<R>>(request as TypeRequest)
|
95
96
|
}
|
96
97
|
CODE
|
97
98
|
end
|
data/lib/ar_sync/version.rb
CHANGED
data/src/core/DataType.ts
CHANGED
@@ -83,4 +83,4 @@ type DataTypeBaseFromRequestType<R extends RequestBase, ID> = R extends { _meta?
|
|
83
83
|
: never
|
84
84
|
export type DataTypeFromRequest<Req extends RequestBase, R extends RequestBase> = ValidateDataTypeExtraFileds<
|
85
85
|
DataTypeFromQuery<DataTypeBaseFromRequestType<Req, R['id']>, R['query']>
|
86
|
-
>
|
86
|
+
>
|
data/src/core/hooks.ts
CHANGED
@@ -4,18 +4,21 @@ import ArSyncModel from './ArSyncModel'
|
|
4
4
|
let useState: <T>(t: T | (() => T)) => [T, (t: T | ((t: T) => T)) => void]
|
5
5
|
let useEffect: (f: (() => void) | (() => (() => void)), deps: any[]) => void
|
6
6
|
let useMemo: <T>(f: () => T, deps: any[]) => T
|
7
|
+
let useRef: <T>(value: T) => { current: T }
|
7
8
|
type InitializeHooksParams = {
|
8
9
|
useState: typeof useState
|
9
10
|
useEffect: typeof useEffect
|
10
11
|
useMemo: typeof useMemo
|
12
|
+
useRef: typeof useRef
|
11
13
|
}
|
12
14
|
export function initializeHooks(hooks: InitializeHooksParams) {
|
13
15
|
useState = hooks.useState
|
14
16
|
useEffect = hooks.useEffect
|
15
17
|
useMemo = hooks.useMemo
|
18
|
+
useRef = hooks.useRef
|
16
19
|
}
|
17
20
|
function checkHooks() {
|
18
|
-
if (!useState) throw 'uninitialized. needs `initializeHooks({ useState, useEffect, useMemo })`'
|
21
|
+
if (!useState) throw 'uninitialized. needs `initializeHooks({ useState, useEffect, useMemo, useRef })`'
|
19
22
|
}
|
20
23
|
|
21
24
|
interface ModelStatus { complete: boolean; notfound?: boolean; connected: boolean }
|
@@ -27,7 +30,9 @@ export function useArSyncModel<T>(request: Request | null): DataAndStatus<T> {
|
|
27
30
|
checkHooks()
|
28
31
|
const [result, setResult] = useState<DataAndStatus<T>>(initialResult)
|
29
32
|
const requestString = JSON.stringify(request && request.params)
|
33
|
+
const prevRequestStringRef = useRef(requestString)
|
30
34
|
useEffect(() => {
|
35
|
+
prevRequestStringRef.current = requestString
|
31
36
|
if (!request) {
|
32
37
|
setResult(initialResult)
|
33
38
|
return () => {}
|
@@ -36,8 +41,9 @@ export function useArSyncModel<T>(request: Request | null): DataAndStatus<T> {
|
|
36
41
|
function update() {
|
37
42
|
const { complete, notfound, connected, data } = model
|
38
43
|
setResult(resultWas => {
|
39
|
-
const [, statusWas] = resultWas
|
44
|
+
const [dataWas, statusWas] = resultWas
|
40
45
|
const statusPersisted = statusWas.complete === complete && statusWas.notfound === notfound && statusWas.connected === connected
|
46
|
+
if (dataWas === data && statusPersisted) return resultWas
|
41
47
|
const status = statusPersisted ? statusWas : { complete, notfound, connected }
|
42
48
|
return [data, status]
|
43
49
|
})
|
@@ -47,21 +53,37 @@ export function useArSyncModel<T>(request: Request | null): DataAndStatus<T> {
|
|
47
53
|
} else {
|
48
54
|
setResult(initialResult)
|
49
55
|
}
|
56
|
+
model.subscribe('load', update)
|
50
57
|
model.subscribe('change', update)
|
51
58
|
model.subscribe('connection', update)
|
52
59
|
return () => model.release()
|
53
60
|
}, [requestString])
|
54
|
-
return result
|
61
|
+
return prevRequestStringRef.current === requestString ? result : initialResult
|
55
62
|
}
|
56
63
|
|
57
64
|
interface FetchStatus { complete: boolean; notfound?: boolean }
|
58
65
|
type DataStatusUpdate<T> = [T | null, FetchStatus, () => void]
|
59
66
|
type FetchState<T> = { data: T | null; status: FetchStatus }
|
60
67
|
const initialFetchState: FetchState<any> = { data: null, status: { complete: false, notfound: undefined } }
|
68
|
+
|
69
|
+
function extractParams(query: unknown, output: any[] = []): any[] {
|
70
|
+
if (typeof(query) !== 'object' || query == null || Array.isArray(query)) return output
|
71
|
+
if ('params' in query) output.push((query as { params: any }).params)
|
72
|
+
for (const key in query) {
|
73
|
+
extractParams(query[key], output)
|
74
|
+
}
|
75
|
+
return output
|
76
|
+
}
|
77
|
+
|
61
78
|
export function useArSyncFetch<T>(request: Request | null): DataStatusUpdate<T> {
|
62
79
|
checkHooks()
|
63
80
|
const [state, setState] = useState<FetchState<T>>(initialFetchState)
|
64
|
-
const
|
81
|
+
const query = request && request.query
|
82
|
+
const params = request && request.params
|
83
|
+
const requestString = useMemo(() => {
|
84
|
+
return JSON.stringify(extractParams(query, [params]))
|
85
|
+
}, [query, params])
|
86
|
+
const prevRequestStringRef = useRef(requestString)
|
65
87
|
const loader = useMemo(() => {
|
66
88
|
let lastLoadId = 0
|
67
89
|
let timer: null | number = null
|
@@ -100,9 +122,11 @@ export function useArSyncFetch<T>(request: Request | null): DataStatusUpdate<T>
|
|
100
122
|
return { update, cancel }
|
101
123
|
}, [requestString])
|
102
124
|
useEffect(() => {
|
125
|
+
prevRequestStringRef.current = requestString
|
103
126
|
setState(initialFetchState)
|
104
127
|
loader.update()
|
105
128
|
return () => loader.cancel()
|
106
129
|
}, [requestString])
|
107
|
-
|
130
|
+
const responseState = prevRequestStringRef.current === requestString ? state : initialFetchState
|
131
|
+
return [responseState.data, responseState.status, loader.update]
|
108
132
|
}
|
data/tsconfig.json
CHANGED
@@ -1,10 +1,10 @@
|
|
1
1
|
{
|
2
2
|
"compilerOptions": {
|
3
|
-
"incremental": true,
|
4
3
|
"outDir": ".",
|
5
4
|
"rootDir": "./src",
|
6
5
|
"module": "commonjs",
|
7
|
-
"target": "
|
6
|
+
"target": "es5",
|
7
|
+
"lib": ["es2017", "dom"],
|
8
8
|
"strictNullChecks": true,
|
9
9
|
"noUnusedLocals": true,
|
10
10
|
"noUnusedParameters": true,
|