@fozy-labs/rx-toolkit 0.4.4 → 0.4.6
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/LICENSE +1 -1
- 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 +73 -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 +5 -1
- package/dist/query/core/Resource/Resource.js +51 -76
- package/dist/query/core/Resource/ResourceRef.js +2 -2
- package/dist/query/react/useResourceRef.d.ts +1 -1
- 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
package/LICENSE
CHANGED
|
@@ -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,54 @@ 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
|
+
if (link.invalidate) {
|
|
141
|
+
ref.invalidate();
|
|
142
|
+
}
|
|
143
|
+
state.patch?.commit();
|
|
144
|
+
});
|
|
145
|
+
hookResolvers.fulfilledSuccess(data);
|
|
146
|
+
/**
|
|
147
|
+
* Обновляем связанные ресурсы
|
|
148
|
+
*/
|
|
149
|
+
linksMeta.forEach(({ state }) => {
|
|
150
|
+
state.unlocker?.unlock();
|
|
151
|
+
});
|
|
117
152
|
});
|
|
118
153
|
})
|
|
119
154
|
.catch((error) => {
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
155
|
+
Batcher.batch(() => {
|
|
156
|
+
cache.next(OperationQueryState.error(state, error));
|
|
157
|
+
/**
|
|
158
|
+
* Обновляем связанные ресурсы
|
|
159
|
+
*/
|
|
160
|
+
linksMeta.forEach(({ state }) => {
|
|
161
|
+
state.patch?.abort();
|
|
162
|
+
});
|
|
163
|
+
hookResolvers.fulfilledError(error);
|
|
164
|
+
/**
|
|
165
|
+
* Обновляем связанные ресурсы
|
|
166
|
+
*/
|
|
167
|
+
linksMeta.forEach(({ state }) => {
|
|
168
|
+
state.unlocker?.unlock();
|
|
169
|
+
});
|
|
135
170
|
});
|
|
136
171
|
});
|
|
137
172
|
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>;
|
|
@@ -40,6 +41,9 @@ export declare class Resource<D extends ResourceDefinition> implements ResourceI
|
|
|
40
41
|
}, options?: {
|
|
41
42
|
cache?: CoreResourceQueryCache<D>;
|
|
42
43
|
}): CoreResourceQueryCache<D> | null;
|
|
44
|
+
createWithData(args: D['Args'], data: D['Data'], options?: {
|
|
45
|
+
cache?: CoreResourceQueryCache<D>;
|
|
46
|
+
}): CoreResourceQueryCache<D>;
|
|
43
47
|
initiate(args: D['Args'], options?: {
|
|
44
48
|
cache?: CoreResourceQueryCache<D>;
|
|
45
49
|
}): 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 {
|
|
@@ -25,6 +24,7 @@ class ResourceQueryState {
|
|
|
25
24
|
static load(state = ResourceQueryState.create(), args) {
|
|
26
25
|
return {
|
|
27
26
|
...state,
|
|
27
|
+
abortController: new AbortController(),
|
|
28
28
|
args: args,
|
|
29
29
|
isLoading: !state.isDone,
|
|
30
30
|
isReloading: state.isDone,
|
|
@@ -34,6 +34,7 @@ class ResourceQueryState {
|
|
|
34
34
|
static success(state, data) {
|
|
35
35
|
return {
|
|
36
36
|
...state,
|
|
37
|
+
abortController: null,
|
|
37
38
|
savedData: null,
|
|
38
39
|
transactions: null,
|
|
39
40
|
data,
|
|
@@ -48,6 +49,7 @@ class ResourceQueryState {
|
|
|
48
49
|
static error(state, error) {
|
|
49
50
|
return {
|
|
50
51
|
...state,
|
|
52
|
+
abortController: null,
|
|
51
53
|
isLoading: false,
|
|
52
54
|
isReloading: false,
|
|
53
55
|
isDone: true,
|
|
@@ -80,78 +82,34 @@ class ResourceQueryState {
|
|
|
80
82
|
data
|
|
81
83
|
};
|
|
82
84
|
}
|
|
83
|
-
|
|
84
|
-
* @deprecated
|
|
85
|
-
*/
|
|
86
|
-
static setData(state, data) {
|
|
85
|
+
static createWithData(data, args) {
|
|
87
86
|
return {
|
|
88
|
-
|
|
87
|
+
savedData: null,
|
|
89
88
|
transactions: null,
|
|
90
|
-
data
|
|
89
|
+
data,
|
|
90
|
+
isLoading: false,
|
|
91
|
+
isReloading: false,
|
|
92
|
+
isDone: true,
|
|
93
|
+
isSuccess: true,
|
|
94
|
+
isError: false,
|
|
95
|
+
error: null,
|
|
96
|
+
abortController: null,
|
|
97
|
+
args,
|
|
98
|
+
isInitiated: false,
|
|
99
|
+
isLocked: false,
|
|
100
|
+
lockCount: 0
|
|
91
101
|
};
|
|
92
102
|
}
|
|
93
103
|
}
|
|
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
104
|
export class Resource {
|
|
148
105
|
_options;
|
|
149
106
|
_queriesCache;
|
|
150
107
|
_hooks;
|
|
108
|
+
_DEFAULT_CACHE_LIFETIME = 60_000;
|
|
151
109
|
constructor(_options) {
|
|
152
110
|
this._options = _options;
|
|
153
|
-
this._hooks = new
|
|
154
|
-
this._queriesCache = new QueriesCache(_options.cacheLifetime
|
|
111
|
+
this._hooks = new QueriesLifetimeHooks(_options, 'Resource');
|
|
112
|
+
this._queriesCache = new QueriesCache(_options.cacheLifetime ?? this._DEFAULT_CACHE_LIFETIME);
|
|
155
113
|
}
|
|
156
114
|
createAgent = () => {
|
|
157
115
|
return new ResourceAgent(this);
|
|
@@ -162,8 +120,8 @@ export class Resource {
|
|
|
162
120
|
getQueryCache(args) {
|
|
163
121
|
return this._queriesCache.getQueryCache(args);
|
|
164
122
|
}
|
|
165
|
-
createQueryCache(args) {
|
|
166
|
-
const cache = this._queriesCache.createQueryCache(args,
|
|
123
|
+
createQueryCache(args, state = ResourceQueryState.create()) {
|
|
124
|
+
const cache = this._queriesCache.createQueryCache(args, state);
|
|
167
125
|
const hookResolvers = this._hooks.onCacheEntryAdded(args);
|
|
168
126
|
const spySub = cache.spy$.subscribe((state) => {
|
|
169
127
|
if (!state.isDone)
|
|
@@ -171,6 +129,9 @@ export class Resource {
|
|
|
171
129
|
hookResolvers.cacheDataLoaded();
|
|
172
130
|
spySub.unsubscribe();
|
|
173
131
|
});
|
|
132
|
+
cache.spy$.subscribe((data) => {
|
|
133
|
+
hookResolvers.dataChanged$.next(data);
|
|
134
|
+
});
|
|
174
135
|
cache.onClean$.subscribe(() => {
|
|
175
136
|
hookResolvers.cacheEntryRemoved();
|
|
176
137
|
});
|
|
@@ -205,34 +166,48 @@ export class Resource {
|
|
|
205
166
|
cache.next(ResourceQueryState.update(cache.value, data, savedData, transactions));
|
|
206
167
|
return cache;
|
|
207
168
|
}
|
|
169
|
+
createWithData(args, data, options) {
|
|
170
|
+
let cache = options?.cache ?? this.getQueryCache(args);
|
|
171
|
+
const state = ResourceQueryState.createWithData(data, args);
|
|
172
|
+
if (!cache) {
|
|
173
|
+
cache = this.createQueryCache(args, state);
|
|
174
|
+
// Только обновляем кэш новыми данными, если он еще не был инициализирован.
|
|
175
|
+
// Это предотвращает перезапись уже инициализированного кэша.
|
|
176
|
+
}
|
|
177
|
+
else if (!cache.value.isInitiated) {
|
|
178
|
+
cache.next(state);
|
|
179
|
+
}
|
|
180
|
+
return cache;
|
|
181
|
+
}
|
|
208
182
|
initiate(args, options) {
|
|
209
183
|
let cache = options?.cache ?? this.getQueryCache(args);
|
|
184
|
+
const prevAbortController = cache?.value.abortController ?? null;
|
|
210
185
|
const state = ResourceQueryState.load(cache?.value, args);
|
|
211
186
|
if (!cache) {
|
|
212
|
-
cache = this.createQueryCache(args);
|
|
187
|
+
cache = this.createQueryCache(args, state);
|
|
188
|
+
}
|
|
189
|
+
else {
|
|
190
|
+
cache.next(state);
|
|
213
191
|
}
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
abortController?.abort();
|
|
217
|
-
abortController = new AbortController();
|
|
192
|
+
prevAbortController?.abort();
|
|
193
|
+
const abortController = state.abortController;
|
|
218
194
|
const query = this._options.queryFn(args, { abortSignal: abortController.signal });
|
|
219
195
|
const hookResolvers = this._hooks.onQueryStarted(args);
|
|
220
196
|
query
|
|
221
|
-
.then((
|
|
197
|
+
.then((result) => {
|
|
222
198
|
if (abortController.signal.aborted) {
|
|
223
199
|
return;
|
|
224
200
|
}
|
|
201
|
+
const data = this._options.select ? this._options.select(result) : result;
|
|
202
|
+
cache.next(ResourceQueryState.success(state, data));
|
|
225
203
|
hookResolvers.fulfilledSuccess(data);
|
|
226
|
-
const selectedData = this._options.select ? this._options.select(data) : data;
|
|
227
|
-
cache.next(ResourceQueryState.success(state, selectedData));
|
|
228
204
|
})
|
|
229
205
|
.catch((error) => {
|
|
230
206
|
if (abortController.signal.aborted) {
|
|
231
207
|
return;
|
|
232
208
|
}
|
|
233
|
-
hookResolvers.fulfilledError(error);
|
|
234
|
-
SharedOptions.onError?.(error); // TODO перенести в хуки
|
|
235
209
|
cache.next(ResourceQueryState.error(state, error));
|
|
210
|
+
hookResolvers.fulfilledError(error);
|
|
236
211
|
});
|
|
237
212
|
return cache;
|
|
238
213
|
}
|
|
@@ -128,9 +128,9 @@ export class ResourceRef {
|
|
|
128
128
|
return transaction;
|
|
129
129
|
}
|
|
130
130
|
create(data) {
|
|
131
|
-
|
|
131
|
+
this._cacheItem = this._resource.createWithData(this._args, data, { cache: this._cacheItem ?? undefined });
|
|
132
132
|
}
|
|
133
133
|
invalidate() {
|
|
134
|
-
|
|
134
|
+
this._cacheItem = this._resource.initiate(this._args, { cache: this._cacheItem ?? undefined });
|
|
135
135
|
}
|
|
136
136
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { SKIP } from "../../query/SKIP_TOKEN";
|
|
2
2
|
import type { Prettify, ResourceDefinition, ResourceInstance, ResourceRefInstanse } from "../../query/types";
|
|
3
|
-
type Result<D extends ResourceDefinition> = Prettify<ResourceRefInstanse<D
|
|
3
|
+
type Result<D extends ResourceDefinition> = Prettify<ResourceRefInstanse<D>>;
|
|
4
4
|
export declare function useResourceRef<D extends ResourceDefinition>(res: ResourceInstance<D>, ...argss: D['Args'] extends void ? [] | [typeof SKIP] : [D['Args'] | typeof SKIP]): Result<D>;
|
|
5
5
|
export {};
|
|
@@ -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;
|