@fozy-labs/rx-toolkit 0.4.4 → 0.4.5
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.
- package/dist/common/devtools/reduxDevtools.js +14 -3
- package/dist/query/core/Opertation/Operation.d.ts +5 -3
- package/dist/query/core/Opertation/Operation.js +70 -38
- package/dist/query/core/QueriesCache.d.ts +1 -2
- package/dist/query/core/QueriesCache.js +1 -3
- package/dist/query/core/QueriesLifetimeHooks.d.ts +21 -0
- package/dist/query/core/QueriesLifetimeHooks.js +89 -0
- package/dist/query/core/Resource/Resource.d.ts +2 -1
- package/dist/query/core/Resource/Resource.js +18 -67
- package/dist/query/types/Operation.types.d.ts +19 -1
- package/dist/query/types/Resource.types.d.ts +14 -21
- package/dist/query/types/shared.types.d.ts +22 -0
- package/dist/signals/base/Computed.js +4 -0
- package/dist/signals/base/Effect.d.ts +3 -1
- package/dist/signals/base/Effect.js +28 -11
- package/dist/signals/base/Signal.d.ts +1 -0
- package/dist/signals/base/Signal.js +4 -0
- package/package.json +1 -1
|
@@ -8,17 +8,28 @@ export function reduxDevtools(options = {}) {
|
|
|
8
8
|
const reduxDevtools = devtools.connect({ name: options.name ?? 'RxToolkit' });
|
|
9
9
|
reduxDevtools.init(state);
|
|
10
10
|
const scheduler = Batcher.scheduler(Infinity);
|
|
11
|
+
let isCreated = false;
|
|
11
12
|
const updateFn = () => {
|
|
12
|
-
reduxDevtools.send({ type: 'update' }, state);
|
|
13
|
+
reduxDevtools.send({ type: isCreated ? 'create' : 'update' }, state);
|
|
14
|
+
isCreated = false;
|
|
15
|
+
};
|
|
16
|
+
const clearFn = () => {
|
|
17
|
+
reduxDevtools.send({ type: 'clear' }, state);
|
|
13
18
|
};
|
|
14
19
|
const createFn = () => {
|
|
15
|
-
|
|
20
|
+
isCreated = true;
|
|
21
|
+
return updateFn;
|
|
16
22
|
};
|
|
17
23
|
return {
|
|
18
24
|
state(name, initState) {
|
|
19
25
|
state = { ...state, [name]: initState };
|
|
20
|
-
scheduler.schedule(createFn);
|
|
26
|
+
scheduler.schedule(createFn());
|
|
21
27
|
return (newState) => {
|
|
28
|
+
if (newState === '$COMPLETE' || newState === '$CLEANED') {
|
|
29
|
+
delete state[name];
|
|
30
|
+
clearFn();
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
22
33
|
state = { ...state, [name]: newState };
|
|
23
34
|
scheduler.schedule(updateFn);
|
|
24
35
|
};
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import type { ReactiveCache } from "../../../query/lib/ReactiveCache";
|
|
2
2
|
import type { FallbackOnNever, OperationCreateOptions, OperationDefinition, OperationInstance } from "../../../query/types";
|
|
3
|
-
import { QueriesCache } from "../QueriesCache";
|
|
4
3
|
import { OperationAgent } from "./OperationAgent";
|
|
5
4
|
export type CoreOperationQueryState<D extends OperationDefinition> = {
|
|
6
5
|
arg: D['Args'] | null;
|
|
@@ -15,16 +14,19 @@ export type CoreOperationQueryState<D extends OperationDefinition> = {
|
|
|
15
14
|
};
|
|
16
15
|
export declare class Operation<D extends OperationDefinition> implements OperationInstance<D> {
|
|
17
16
|
private readonly _options;
|
|
18
|
-
|
|
17
|
+
private _queriesCache;
|
|
18
|
+
private _hooks;
|
|
19
19
|
private _links;
|
|
20
|
+
private _DEFAULT_CACHE_LIFETIME;
|
|
20
21
|
constructor(_options: OperationCreateOptions<D>);
|
|
21
22
|
private _createLinks;
|
|
22
23
|
createAgent(): OperationAgent<D>;
|
|
23
24
|
getQueryCache(args: D['Args']): ReactiveCache<CoreOperationQueryState<D>> | undefined;
|
|
24
|
-
createQueryCache(args: D['Args']): ReactiveCache<CoreOperationQueryState<D>>;
|
|
25
|
+
createQueryCache(args: D['Args'], state?: CoreOperationQueryState<D>): ReactiveCache<CoreOperationQueryState<D>>;
|
|
25
26
|
initiate(args: D['Args'], options?: {
|
|
26
27
|
cache?: ReactiveCache<CoreOperationQueryState<D>>;
|
|
27
28
|
}): ReactiveCache<CoreOperationQueryState<D>>;
|
|
29
|
+
private _initiate;
|
|
28
30
|
/**
|
|
29
31
|
* Используеются для обртной совместимости, а надо ли менять что-то - хз
|
|
30
32
|
* @deprecated
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { PromiseResolver } from "../../../common/utils";
|
|
2
|
-
import {
|
|
2
|
+
import { Batcher } from "../../../signals";
|
|
3
3
|
import { QueriesCache } from "../QueriesCache";
|
|
4
|
+
import { QueriesLifetimeHooks } from "../QueriesLifetimeHooks";
|
|
4
5
|
import { OperationAgent } from "./OperationAgent";
|
|
5
6
|
class OperationQueryState {
|
|
6
7
|
static create() {
|
|
@@ -51,10 +52,14 @@ class OperationQueryState {
|
|
|
51
52
|
}
|
|
52
53
|
export class Operation {
|
|
53
54
|
_options;
|
|
54
|
-
_queriesCache
|
|
55
|
+
_queriesCache;
|
|
56
|
+
_hooks;
|
|
55
57
|
_links = [];
|
|
58
|
+
_DEFAULT_CACHE_LIFETIME = 1_000;
|
|
56
59
|
constructor(_options) {
|
|
57
60
|
this._options = _options;
|
|
61
|
+
this._queriesCache = new QueriesCache(this._options.cacheLifetime ?? this._DEFAULT_CACHE_LIFETIME);
|
|
62
|
+
this._hooks = new QueriesLifetimeHooks(_options, 'Operation');
|
|
58
63
|
this._createLinks();
|
|
59
64
|
}
|
|
60
65
|
_createLinks() {
|
|
@@ -68,14 +73,31 @@ export class Operation {
|
|
|
68
73
|
getQueryCache(args) {
|
|
69
74
|
return this._queriesCache.getQueryCache(args);
|
|
70
75
|
}
|
|
71
|
-
createQueryCache(args) {
|
|
72
|
-
|
|
76
|
+
createQueryCache(args, state = OperationQueryState.create()) {
|
|
77
|
+
const cache = this._queriesCache.createQueryCache(args, state);
|
|
78
|
+
const hookResolvers = this._hooks.onCacheEntryAdded(args);
|
|
79
|
+
const spySub = cache.spy$.subscribe((state) => {
|
|
80
|
+
if (!state.isDone)
|
|
81
|
+
return;
|
|
82
|
+
hookResolvers.cacheDataLoaded();
|
|
83
|
+
spySub.unsubscribe();
|
|
84
|
+
});
|
|
85
|
+
cache.spy$.subscribe((state) => {
|
|
86
|
+
hookResolvers.dataChanged$.next(state);
|
|
87
|
+
});
|
|
88
|
+
cache.onClean$.subscribe(() => {
|
|
89
|
+
hookResolvers.cacheEntryRemoved();
|
|
90
|
+
});
|
|
91
|
+
return cache;
|
|
73
92
|
}
|
|
74
93
|
initiate(args, options) {
|
|
75
|
-
|
|
94
|
+
return Batcher.batch(() => this._initiate(args, options));
|
|
95
|
+
}
|
|
96
|
+
_initiate(args, options) {
|
|
97
|
+
let cache = options?.cache ?? this.getQueryCache(args);
|
|
76
98
|
const state = OperationQueryState.load(cache?.value, args);
|
|
77
99
|
if (!cache) {
|
|
78
|
-
cache = this.
|
|
100
|
+
cache = this.createQueryCache(args, state);
|
|
79
101
|
}
|
|
80
102
|
else {
|
|
81
103
|
cache.next(state);
|
|
@@ -85,6 +107,8 @@ export class Operation {
|
|
|
85
107
|
const ref = link.resource.createRef(forwardedArgs);
|
|
86
108
|
return { link, ref, state: {} };
|
|
87
109
|
});
|
|
110
|
+
const query = this._options.queryFn(args);
|
|
111
|
+
const hookResolvers = this._hooks.onQueryStarted(args);
|
|
88
112
|
linksMeta.forEach(({ link, ref, state }) => {
|
|
89
113
|
if (link.lock) {
|
|
90
114
|
state.unlocker = ref.lock();
|
|
@@ -95,43 +119,51 @@ export class Operation {
|
|
|
95
119
|
});
|
|
96
120
|
}
|
|
97
121
|
});
|
|
98
|
-
const query = this._options.queryFn(args);
|
|
99
122
|
query
|
|
100
123
|
.then((result) => {
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
124
|
+
Batcher.batch(() => {
|
|
125
|
+
const data = this._options.select ? this._options.select(result) : result;
|
|
126
|
+
cache.next(OperationQueryState.success(state, data));
|
|
127
|
+
/**
|
|
128
|
+
* Обновляем связанные ресурсы
|
|
129
|
+
*/
|
|
130
|
+
linksMeta.forEach(({ link, ref, state }) => {
|
|
131
|
+
if (link.update && ref.has) {
|
|
132
|
+
// TODO подумать, нужно ли добавлять обработку, если patch() -> null (и в принце про работу patch)
|
|
133
|
+
ref.patch((draft) => {
|
|
134
|
+
return link.update({ draft, args, data });
|
|
135
|
+
})?.commit();
|
|
136
|
+
}
|
|
137
|
+
if (link.create && !ref.has) {
|
|
138
|
+
ref.create(link.create({ args, data }));
|
|
139
|
+
}
|
|
140
|
+
state.patch?.commit();
|
|
141
|
+
});
|
|
142
|
+
hookResolvers.fulfilledSuccess(data);
|
|
143
|
+
/**
|
|
144
|
+
* Обновляем связанные ресурсы
|
|
145
|
+
*/
|
|
146
|
+
linksMeta.forEach(({ state }) => {
|
|
147
|
+
state.unlocker?.unlock();
|
|
148
|
+
});
|
|
117
149
|
});
|
|
118
150
|
})
|
|
119
151
|
.catch((error) => {
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
152
|
+
Batcher.batch(() => {
|
|
153
|
+
cache.next(OperationQueryState.error(state, error));
|
|
154
|
+
/**
|
|
155
|
+
* Обновляем связанные ресурсы
|
|
156
|
+
*/
|
|
157
|
+
linksMeta.forEach(({ state }) => {
|
|
158
|
+
state.patch?.abort();
|
|
159
|
+
});
|
|
160
|
+
hookResolvers.fulfilledError(error);
|
|
161
|
+
/**
|
|
162
|
+
* Обновляем связанные ресурсы
|
|
163
|
+
*/
|
|
164
|
+
linksMeta.forEach(({ state }) => {
|
|
165
|
+
state.unlocker?.unlock();
|
|
166
|
+
});
|
|
135
167
|
});
|
|
136
168
|
});
|
|
137
169
|
return cache;
|
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
import { ReactiveCache } from "../../query/lib/ReactiveCache";
|
|
2
2
|
export declare class QueriesCache<KEY, VALUE> {
|
|
3
3
|
private _cacheLifeTime;
|
|
4
|
-
private _logname;
|
|
5
4
|
private readonly _cache;
|
|
6
|
-
constructor(_cacheLifeTime?: number | false
|
|
5
|
+
constructor(_cacheLifeTime?: number | false);
|
|
7
6
|
getQueryCache(args: KEY): ReactiveCache<VALUE> | undefined;
|
|
8
7
|
createQueryCache(args: KEY, initialState: VALUE): ReactiveCache<VALUE>;
|
|
9
8
|
}
|
|
@@ -2,11 +2,9 @@ import { IndirectMap } from "../../query/lib/IndirectMap";
|
|
|
2
2
|
import { ReactiveCache } from "../../query/lib/ReactiveCache";
|
|
3
3
|
export class QueriesCache {
|
|
4
4
|
_cacheLifeTime;
|
|
5
|
-
_logname;
|
|
6
5
|
_cache = new IndirectMap();
|
|
7
|
-
constructor(_cacheLifeTime = 60_000
|
|
6
|
+
constructor(_cacheLifeTime = 60_000) {
|
|
8
7
|
this._cacheLifeTime = _cacheLifeTime;
|
|
9
|
-
this._logname = _logname;
|
|
10
8
|
}
|
|
11
9
|
getQueryCache(args) {
|
|
12
10
|
return this._cache.get(args);
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { Subject } from "rxjs";
|
|
2
|
+
import { OnCacheEntryAdded, OnQueryStarted } from "../../query/types";
|
|
3
|
+
type Options<ARGS, DATA> = {
|
|
4
|
+
onCacheEntryAdded?: OnCacheEntryAdded<ARGS, DATA>;
|
|
5
|
+
onQueryStarted?: OnQueryStarted<ARGS, DATA>;
|
|
6
|
+
};
|
|
7
|
+
export declare class QueriesLifetimeHooks<ARGS, DATA> {
|
|
8
|
+
private onCacheEntryAddedListeners;
|
|
9
|
+
private onQueryStartedListeners;
|
|
10
|
+
constructor(options: Options<ARGS, DATA> | undefined, devtoolName: string | undefined);
|
|
11
|
+
onCacheEntryAdded: (args: ARGS) => {
|
|
12
|
+
cacheDataLoaded: () => void;
|
|
13
|
+
cacheEntryRemoved: () => void;
|
|
14
|
+
dataChanged$: Subject<DATA>;
|
|
15
|
+
};
|
|
16
|
+
onQueryStarted: (args: ARGS) => {
|
|
17
|
+
fulfilledSuccess: (data: DATA) => void;
|
|
18
|
+
fulfilledError: (error: unknown) => void;
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
export {};
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { Subject } from "rxjs";
|
|
2
|
+
import { SharedOptions } from "../../common/options/SharedOptions";
|
|
3
|
+
import { PromiseResolver } from "../../common/utils";
|
|
4
|
+
import { Indexer } from "../../signals/base/Indexer";
|
|
5
|
+
export class QueriesLifetimeHooks {
|
|
6
|
+
onCacheEntryAddedListeners = [];
|
|
7
|
+
onQueryStartedListeners = [];
|
|
8
|
+
constructor(options, devtoolName) {
|
|
9
|
+
if (options?.onCacheEntryAdded) {
|
|
10
|
+
this.onCacheEntryAddedListeners.push(options.onCacheEntryAdded);
|
|
11
|
+
}
|
|
12
|
+
if (options?.onQueryStarted) {
|
|
13
|
+
this.onQueryStartedListeners.push(options.onQueryStarted);
|
|
14
|
+
}
|
|
15
|
+
if (devtoolName) {
|
|
16
|
+
const stateDevtools = SharedOptions.DEVTOOLS?.state;
|
|
17
|
+
if (stateDevtools) {
|
|
18
|
+
this.onCacheEntryAddedListeners.push(async (args, { $cacheEntryRemoved, dataChanged$ }) => {
|
|
19
|
+
const key = `${devtoolName}:${JSON.stringify(args)}:i=${Indexer.getIndex()}`;
|
|
20
|
+
let devtools = null;
|
|
21
|
+
dataChanged$.subscribe((state) => {
|
|
22
|
+
if (!devtools) {
|
|
23
|
+
devtools = stateDevtools(key, state);
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
devtools(state);
|
|
27
|
+
});
|
|
28
|
+
$cacheEntryRemoved.then(() => {
|
|
29
|
+
devtools('$CLEANED');
|
|
30
|
+
});
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
if (SharedOptions.onError) {
|
|
35
|
+
this.onQueryStartedListeners.push(async (args, { $queryFulfilled }) => {
|
|
36
|
+
try {
|
|
37
|
+
await $queryFulfilled;
|
|
38
|
+
}
|
|
39
|
+
catch (error) {
|
|
40
|
+
SharedOptions.onError(error);
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
onCacheEntryAdded = (args) => {
|
|
46
|
+
const cacheDataLoadedResolver = new PromiseResolver();
|
|
47
|
+
const cacheEntryRemovedResolver = new PromiseResolver();
|
|
48
|
+
const dataChanged$ = new Subject(); // TODO не нравится мне это, мб передавать $spy в аргументы?
|
|
49
|
+
cacheEntryRemovedResolver.promise.finally(() => {
|
|
50
|
+
dataChanged$.complete();
|
|
51
|
+
});
|
|
52
|
+
this.onCacheEntryAddedListeners.forEach((listener) => {
|
|
53
|
+
listener(args, {
|
|
54
|
+
$cacheDataLoaded: cacheDataLoadedResolver.promise,
|
|
55
|
+
$cacheEntryRemoved: cacheEntryRemovedResolver.promise,
|
|
56
|
+
dataChanged$,
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
return {
|
|
60
|
+
cacheDataLoaded: () => cacheDataLoadedResolver.resolve(),
|
|
61
|
+
cacheEntryRemoved: () => cacheEntryRemovedResolver.resolve(),
|
|
62
|
+
dataChanged$,
|
|
63
|
+
};
|
|
64
|
+
};
|
|
65
|
+
onQueryStarted = (args) => {
|
|
66
|
+
const queryFulfilledResolver = new PromiseResolver();
|
|
67
|
+
this.onQueryStartedListeners.forEach((listener) => {
|
|
68
|
+
listener(args, {
|
|
69
|
+
$queryFulfilled: queryFulfilledResolver.promise
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
return {
|
|
73
|
+
fulfilledSuccess: (data) => {
|
|
74
|
+
queryFulfilledResolver.resolve({
|
|
75
|
+
data,
|
|
76
|
+
error: undefined,
|
|
77
|
+
isError: false
|
|
78
|
+
});
|
|
79
|
+
},
|
|
80
|
+
fulfilledError: (error) => {
|
|
81
|
+
queryFulfilledResolver.resolve({
|
|
82
|
+
data: undefined,
|
|
83
|
+
error,
|
|
84
|
+
isError: true
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
};
|
|
88
|
+
};
|
|
89
|
+
}
|
|
@@ -22,11 +22,12 @@ export declare class Resource<D extends ResourceDefinition> implements ResourceI
|
|
|
22
22
|
private readonly _options;
|
|
23
23
|
private readonly _queriesCache;
|
|
24
24
|
private readonly _hooks;
|
|
25
|
+
private _DEFAULT_CACHE_LIFETIME;
|
|
25
26
|
constructor(_options: ResourceCreateOptions<D>);
|
|
26
27
|
createAgent: () => ResourceAgent<D>;
|
|
27
28
|
createRef: (args: D["Args"]) => ResourceRefInstanse<D>;
|
|
28
29
|
getQueryCache(args: D['Args']): CoreResourceQueryCache<D> | undefined;
|
|
29
|
-
createQueryCache(args: D['Args']): CoreResourceQueryCache<D>;
|
|
30
|
+
createQueryCache(args: D['Args'], state?: CoreResourceQueryState<D>): CoreResourceQueryCache<D>;
|
|
30
31
|
incrementLock(args: D['Args'], options?: {
|
|
31
32
|
cache?: CoreResourceQueryCache<D>;
|
|
32
33
|
}): CoreResourceQueryCache<D>;
|
|
@@ -1,6 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import { QueriesCache } from "../../../query/core/QueriesCache";
|
|
1
|
+
import { QueriesCache } from "../QueriesCache";
|
|
2
|
+
import { QueriesLifetimeHooks } from "../QueriesLifetimeHooks";
|
|
4
3
|
import { ResourceAgent } from "./ResourceAgent";
|
|
5
4
|
import { ResourceRef } from "./ResourceRef";
|
|
6
5
|
class ResourceQueryState {
|
|
@@ -91,67 +90,15 @@ class ResourceQueryState {
|
|
|
91
90
|
};
|
|
92
91
|
}
|
|
93
92
|
}
|
|
94
|
-
// TODO вынести и унифицировать; как-то организовать глобальные хуки и devtools через хуки
|
|
95
|
-
class QueryHooks {
|
|
96
|
-
_options;
|
|
97
|
-
onCacheEntryAddedListeners = [];
|
|
98
|
-
onQueryStartedListeners = [];
|
|
99
|
-
constructor(_options) {
|
|
100
|
-
this._options = _options;
|
|
101
|
-
if (_options?.onCacheEntryAdded) {
|
|
102
|
-
this.onCacheEntryAddedListeners.push(_options.onCacheEntryAdded);
|
|
103
|
-
}
|
|
104
|
-
if (_options?.onQueryStarted) {
|
|
105
|
-
this.onQueryStartedListeners.push(_options.onQueryStarted);
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
onCacheEntryAdded = (args) => {
|
|
109
|
-
const cacheDataLoadedResolver = new PromiseResolver();
|
|
110
|
-
const cacheEntryRemovedResolver = new PromiseResolver();
|
|
111
|
-
this.onCacheEntryAddedListeners.forEach((listener) => {
|
|
112
|
-
listener(args, {
|
|
113
|
-
$cacheDataLoaded: cacheDataLoadedResolver.promise,
|
|
114
|
-
$cacheEntryRemoved: cacheEntryRemovedResolver.promise,
|
|
115
|
-
});
|
|
116
|
-
});
|
|
117
|
-
return {
|
|
118
|
-
cacheDataLoaded: () => cacheDataLoadedResolver.resolve(),
|
|
119
|
-
cacheEntryRemoved: () => cacheEntryRemovedResolver.resolve(),
|
|
120
|
-
};
|
|
121
|
-
};
|
|
122
|
-
onQueryStarted = (args) => {
|
|
123
|
-
const queryFulfilledResolver = new PromiseResolver();
|
|
124
|
-
this.onQueryStartedListeners.forEach((listener) => {
|
|
125
|
-
listener(args, {
|
|
126
|
-
$queryFulfilled: queryFulfilledResolver.promise
|
|
127
|
-
});
|
|
128
|
-
});
|
|
129
|
-
return {
|
|
130
|
-
fulfilledSuccess: (data) => {
|
|
131
|
-
queryFulfilledResolver.resolve({
|
|
132
|
-
data,
|
|
133
|
-
error: undefined,
|
|
134
|
-
isError: false
|
|
135
|
-
});
|
|
136
|
-
},
|
|
137
|
-
fulfilledError: (error) => {
|
|
138
|
-
queryFulfilledResolver.resolve({
|
|
139
|
-
data: undefined,
|
|
140
|
-
error,
|
|
141
|
-
isError: true
|
|
142
|
-
});
|
|
143
|
-
}
|
|
144
|
-
};
|
|
145
|
-
};
|
|
146
|
-
}
|
|
147
93
|
export class Resource {
|
|
148
94
|
_options;
|
|
149
95
|
_queriesCache;
|
|
150
96
|
_hooks;
|
|
97
|
+
_DEFAULT_CACHE_LIFETIME = 60_000;
|
|
151
98
|
constructor(_options) {
|
|
152
99
|
this._options = _options;
|
|
153
|
-
this._hooks = new
|
|
154
|
-
this._queriesCache = new QueriesCache(_options.cacheLifetime
|
|
100
|
+
this._hooks = new QueriesLifetimeHooks(_options, 'Resource');
|
|
101
|
+
this._queriesCache = new QueriesCache(_options.cacheLifetime ?? this._DEFAULT_CACHE_LIFETIME);
|
|
155
102
|
}
|
|
156
103
|
createAgent = () => {
|
|
157
104
|
return new ResourceAgent(this);
|
|
@@ -162,8 +109,8 @@ export class Resource {
|
|
|
162
109
|
getQueryCache(args) {
|
|
163
110
|
return this._queriesCache.getQueryCache(args);
|
|
164
111
|
}
|
|
165
|
-
createQueryCache(args) {
|
|
166
|
-
const cache = this._queriesCache.createQueryCache(args,
|
|
112
|
+
createQueryCache(args, state = ResourceQueryState.create()) {
|
|
113
|
+
const cache = this._queriesCache.createQueryCache(args, state);
|
|
167
114
|
const hookResolvers = this._hooks.onCacheEntryAdded(args);
|
|
168
115
|
const spySub = cache.spy$.subscribe((state) => {
|
|
169
116
|
if (!state.isDone)
|
|
@@ -171,6 +118,9 @@ export class Resource {
|
|
|
171
118
|
hookResolvers.cacheDataLoaded();
|
|
172
119
|
spySub.unsubscribe();
|
|
173
120
|
});
|
|
121
|
+
cache.spy$.subscribe((data) => {
|
|
122
|
+
hookResolvers.dataChanged$.next(data);
|
|
123
|
+
});
|
|
174
124
|
cache.onClean$.subscribe(() => {
|
|
175
125
|
hookResolvers.cacheEntryRemoved();
|
|
176
126
|
});
|
|
@@ -209,30 +159,31 @@ export class Resource {
|
|
|
209
159
|
let cache = options?.cache ?? this.getQueryCache(args);
|
|
210
160
|
const state = ResourceQueryState.load(cache?.value, args);
|
|
211
161
|
if (!cache) {
|
|
212
|
-
cache = this.createQueryCache(args);
|
|
162
|
+
cache = this.createQueryCache(args, state);
|
|
163
|
+
}
|
|
164
|
+
else {
|
|
165
|
+
cache.next(state);
|
|
213
166
|
}
|
|
214
|
-
cache.next(state);
|
|
215
167
|
let abortController = state.abortController;
|
|
216
168
|
abortController?.abort();
|
|
217
169
|
abortController = new AbortController();
|
|
218
170
|
const query = this._options.queryFn(args, { abortSignal: abortController.signal });
|
|
219
171
|
const hookResolvers = this._hooks.onQueryStarted(args);
|
|
220
172
|
query
|
|
221
|
-
.then((
|
|
173
|
+
.then((result) => {
|
|
222
174
|
if (abortController.signal.aborted) {
|
|
223
175
|
return;
|
|
224
176
|
}
|
|
177
|
+
const data = this._options.select ? result._options.select(result) : result;
|
|
178
|
+
cache.next(ResourceQueryState.success(state, data));
|
|
225
179
|
hookResolvers.fulfilledSuccess(data);
|
|
226
|
-
const selectedData = this._options.select ? this._options.select(data) : data;
|
|
227
|
-
cache.next(ResourceQueryState.success(state, selectedData));
|
|
228
180
|
})
|
|
229
181
|
.catch((error) => {
|
|
230
182
|
if (abortController.signal.aborted) {
|
|
231
183
|
return;
|
|
232
184
|
}
|
|
233
|
-
hookResolvers.fulfilledError(error);
|
|
234
|
-
SharedOptions.onError?.(error); // TODO перенести в хуки
|
|
235
185
|
cache.next(ResourceQueryState.error(state, error));
|
|
186
|
+
hookResolvers.fulfilledError(error);
|
|
236
187
|
});
|
|
237
188
|
return cache;
|
|
238
189
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { ReadableSignalLike } from "../../signals";
|
|
2
|
-
import { FallbackOnNever } from "./shared.types";
|
|
2
|
+
import { FallbackOnNever, OnCacheEntryAdded, OnQueryStarted } from "./shared.types";
|
|
3
3
|
import { ResourceDefinition, ResourceInstance } from "./Resource.types";
|
|
4
4
|
/**
|
|
5
5
|
* Функция создания операции
|
|
@@ -15,6 +15,24 @@ export type OperationCreateOptions<D extends OperationDefinition> = {
|
|
|
15
15
|
queryFn: (args: D["Args"]) => Promise<D["Result"]>;
|
|
16
16
|
/** Связанные ресурсы */
|
|
17
17
|
link?: (link: <RD extends ResourceDefinition>(options: LinkOptions<D, RD>) => void) => void;
|
|
18
|
+
/**
|
|
19
|
+
* Время жизни кеша в миллисекундах. По умолчанию 1_000 (1 секунда).
|
|
20
|
+
* Если указано false - кеш не удаляется автоматически.
|
|
21
|
+
*/
|
|
22
|
+
cacheLifetime?: number | false;
|
|
23
|
+
/**
|
|
24
|
+
* Хук, вызываемый при добавлении нового элемента в кеш.
|
|
25
|
+
* Также позволяет отследить:
|
|
26
|
+
* - когда данные были загружены (впервые)
|
|
27
|
+
* - когда элемент был удален из кеша
|
|
28
|
+
*/
|
|
29
|
+
onCacheEntryAdded?: OnCacheEntryAdded<D["Args"], D["Data"]>;
|
|
30
|
+
/**
|
|
31
|
+
* Хук, вызываемый при старте запроса.
|
|
32
|
+
* Также позволяет отследить:
|
|
33
|
+
* - завершение запроса с результатом или ошибкой
|
|
34
|
+
*/
|
|
35
|
+
onQueryStarted?: OnQueryStarted<D["Args"], D["Result"]>;
|
|
18
36
|
};
|
|
19
37
|
/**
|
|
20
38
|
* Настройки связи операции с ресурсом
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Patch as ImmerPatch } from "immer";
|
|
2
2
|
import { ReadableSignalLike } from "../../signals";
|
|
3
|
-
import { FallbackOnNever } from "./shared.types";
|
|
3
|
+
import { FallbackOnNever, OnCacheEntryAdded, OnQueryStarted } from "./shared.types";
|
|
4
4
|
/**
|
|
5
5
|
* Функция создания ресурса
|
|
6
6
|
*/
|
|
@@ -18,26 +18,19 @@ export type ResourceCreateOptions<D extends ResourceDefinition> = {
|
|
|
18
18
|
* Если указано false - кеш не удаляется автоматически.
|
|
19
19
|
*/
|
|
20
20
|
cacheLifetime?: number | false;
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
error: undefined;
|
|
35
|
-
isError: false;
|
|
36
|
-
} | {
|
|
37
|
-
data: undefined;
|
|
38
|
-
error: unknown;
|
|
39
|
-
isError: true;
|
|
40
|
-
}>;
|
|
21
|
+
/**
|
|
22
|
+
* Хук, вызываемый при добавлении нового элемента в кеш.
|
|
23
|
+
* Также позволяет отследить:
|
|
24
|
+
* - когда данные были загружены (впервые)
|
|
25
|
+
* - когда элемент был удален из кеша
|
|
26
|
+
*/
|
|
27
|
+
onCacheEntryAdded?: OnCacheEntryAdded<D["Args"], D["Data"]>;
|
|
28
|
+
/**
|
|
29
|
+
* Хук, вызываемый при старте запроса.
|
|
30
|
+
* Также позволяет отследить:
|
|
31
|
+
* - завершение запроса с результатом или ошибкой
|
|
32
|
+
*/
|
|
33
|
+
onQueryStarted?: OnQueryStarted<D["Args"], D["Result"]>;
|
|
41
34
|
};
|
|
42
35
|
/**
|
|
43
36
|
* Определение типов ресурса
|
|
@@ -1,4 +1,26 @@
|
|
|
1
|
+
import { Subject } from "rxjs";
|
|
1
2
|
export type Prettify<T> = {
|
|
2
3
|
[KeyType in keyof T]: T[KeyType];
|
|
3
4
|
} & {};
|
|
4
5
|
export type FallbackOnNever<T, F> = [T] extends [never] ? F : T;
|
|
6
|
+
export type CacheEntryAddedTools<DATA> = {
|
|
7
|
+
/** Функция для ожидания загрузки данных в кеш */
|
|
8
|
+
$cacheDataLoaded: Promise<void>;
|
|
9
|
+
/** Функция для ожидания удаления кеша */
|
|
10
|
+
$cacheEntryRemoved: Promise<void>;
|
|
11
|
+
dataChanged$: Subject<DATA>;
|
|
12
|
+
};
|
|
13
|
+
export type QueryStartedTools<DATA> = {
|
|
14
|
+
/** Функция для уведомления об успешном завершении запроса */
|
|
15
|
+
$queryFulfilled: Promise<{
|
|
16
|
+
data: DATA;
|
|
17
|
+
error: undefined;
|
|
18
|
+
isError: false;
|
|
19
|
+
} | {
|
|
20
|
+
data: undefined;
|
|
21
|
+
error: unknown;
|
|
22
|
+
isError: true;
|
|
23
|
+
}>;
|
|
24
|
+
};
|
|
25
|
+
export type OnCacheEntryAdded<ARGS, DATA> = (args: ARGS, tools: CacheEntryAddedTools<DATA>) => void;
|
|
26
|
+
export type OnQueryStarted<ARGS, DATA> = (args: ARGS, tools: QueryStartedTools<DATA>) => void;
|
|
@@ -12,6 +12,8 @@ export class Computed extends Signal {
|
|
|
12
12
|
}
|
|
13
13
|
this._rang = effect._rang;
|
|
14
14
|
this.value = computeFn();
|
|
15
|
+
}, () => {
|
|
16
|
+
this.complete();
|
|
15
17
|
});
|
|
16
18
|
super(initialValue, {
|
|
17
19
|
devtoolsName: 'Computed',
|
|
@@ -23,6 +25,8 @@ export class Computed extends Signal {
|
|
|
23
25
|
this.complete();
|
|
24
26
|
}
|
|
25
27
|
complete() {
|
|
28
|
+
if (this.closed)
|
|
29
|
+
return;
|
|
26
30
|
this._effect.unsubscribe();
|
|
27
31
|
super.complete();
|
|
28
32
|
}
|
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
import { SubscriptionLike } from "rxjs";
|
|
2
2
|
export declare class Effect implements SubscriptionLike {
|
|
3
|
+
private _onComplete?;
|
|
3
4
|
private _subscriptions;
|
|
4
5
|
closed: boolean;
|
|
5
6
|
_rang: number;
|
|
6
|
-
constructor(effectFn: (ctx: (fn: () => void) => void) => void);
|
|
7
|
+
constructor(effectFn: (ctx: (fn: () => void) => void) => void, _onComplete?: (() => void) | undefined);
|
|
7
8
|
/**
|
|
8
9
|
* Выполняет функцию в tracked-контексте, подписываясь на Tracker.
|
|
9
10
|
*/
|
|
10
11
|
private _runInTrackedContext;
|
|
11
12
|
unsubscribe(): void;
|
|
13
|
+
complete(): void;
|
|
12
14
|
}
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import { Batcher } from "./Batcher";
|
|
2
2
|
import { Tracker } from "./Tracker";
|
|
3
3
|
export class Effect {
|
|
4
|
+
_onComplete;
|
|
4
5
|
_subscriptions = [];
|
|
5
6
|
closed = false;
|
|
6
7
|
_rang = 0;
|
|
7
|
-
constructor(effectFn) {
|
|
8
|
+
constructor(effectFn, _onComplete) {
|
|
9
|
+
this._onComplete = _onComplete;
|
|
8
10
|
this._runInTrackedContext(effectFn, false);
|
|
9
11
|
}
|
|
10
12
|
/**
|
|
@@ -24,18 +26,27 @@ export class Effect {
|
|
|
24
26
|
this._runInTrackedContext(effectFn);
|
|
25
27
|
};
|
|
26
28
|
// Подписываемся на Tracker. Во время выполнения подпишемся на все tracked наблюдатели.
|
|
27
|
-
const trackerSub = Tracker.tracked$.subscribe(
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
if (tracked.rang <= this._rang) {
|
|
31
|
-
this._rang = tracked.rang + 1;
|
|
32
|
-
}
|
|
33
|
-
this._subscriptions.push(tracked.obsv$.subscribe(() => {
|
|
34
|
-
if (isTrackedContext) {
|
|
29
|
+
const trackerSub = Tracker.tracked$.subscribe({
|
|
30
|
+
next: (tracked) => {
|
|
31
|
+
if (!isTrackedContext)
|
|
35
32
|
return;
|
|
33
|
+
if (tracked.rang <= this._rang) {
|
|
34
|
+
this._rang = tracked.rang + 1;
|
|
36
35
|
}
|
|
37
|
-
|
|
38
|
-
|
|
36
|
+
this._subscriptions.push(tracked.obsv$.subscribe(() => {
|
|
37
|
+
if (isTrackedContext) {
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
scheduler.schedule(scheduledFn);
|
|
41
|
+
}));
|
|
42
|
+
},
|
|
43
|
+
complete: () => {
|
|
44
|
+
this.unsubscribe();
|
|
45
|
+
},
|
|
46
|
+
error: (err) => {
|
|
47
|
+
console.error(err);
|
|
48
|
+
this.unsubscribe();
|
|
49
|
+
},
|
|
39
50
|
});
|
|
40
51
|
effectFn((fn) => {
|
|
41
52
|
this._runInTrackedContext(fn, true);
|
|
@@ -46,7 +57,13 @@ export class Effect {
|
|
|
46
57
|
prevSubscriptions?.forEach((sub) => sub.unsubscribe());
|
|
47
58
|
}
|
|
48
59
|
unsubscribe() {
|
|
60
|
+
this.complete();
|
|
61
|
+
}
|
|
62
|
+
complete() {
|
|
63
|
+
if (this.closed)
|
|
64
|
+
return;
|
|
49
65
|
this.closed = true;
|
|
50
66
|
this._subscriptions.forEach((sub) => sub.unsubscribe());
|
|
67
|
+
this._onComplete?.();
|
|
51
68
|
}
|
|
52
69
|
}
|
|
@@ -25,6 +25,7 @@ export declare class Signal<T> extends BehaviorSubject<T> implements SignalLike<
|
|
|
25
25
|
* @deprecated use `next(value)` instead.
|
|
26
26
|
*/
|
|
27
27
|
set(value: T): void;
|
|
28
|
+
complete(): void;
|
|
28
29
|
pipe(): Signal<T>;
|
|
29
30
|
pipe<A extends Observable<any>>(op1: UnaryFunction<ReadableSignalLike<T>, A>): A;
|
|
30
31
|
pipe<A extends Observable<any>, B extends Observable<any>>(op1: UnaryFunction<Signal<T>, A>, op2: UnaryFunction<A, B>): B;
|