@fozy-labs/rx-toolkit 0.4.3 → 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.d.ts +15 -1
- package/dist/common/devtools/reduxDevtools.js +20 -6
- package/dist/common/utils/PromiseResolver.d.ts +7 -0
- package/dist/common/utils/PromiseResolver.js +14 -0
- package/dist/common/utils/index.d.ts +1 -0
- package/dist/common/utils/index.js +1 -0
- package/dist/query/core/Opertation/Operation.d.ts +5 -3
- package/dist/query/core/Opertation/Operation.js +71 -51
- package/dist/query/core/Opertation/OperationAgent.js +1 -1
- package/dist/query/core/QueriesCache.d.ts +2 -2
- package/dist/query/core/QueriesCache.js +19 -18
- 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 +4 -9
- package/dist/query/core/Resource/Resource.js +31 -27
- package/dist/query/core/Resource/ResourceAgent.d.ts +1 -1
- package/dist/query/core/Resource/ResourceAgent.js +5 -4
- package/dist/query/core/Resource/ResourceRef.d.ts +0 -3
- package/dist/query/core/Resource/ResourceRef.js +10 -20
- package/dist/query/lib/ReactiveCache.d.ts +6 -22
- package/dist/query/lib/ReactiveCache.js +19 -39
- package/dist/query/react/useResourceAgent.d.ts +2 -5
- package/dist/query/react/useResourceAgent.js +3 -0
- package/dist/query/types/Operation.types.d.ts +19 -1
- package/dist/query/types/Resource.types.d.ts +21 -9
- package/dist/query/types/shared.types.d.ts +22 -0
- package/dist/signals/base/Computed.js +5 -2
- package/dist/signals/base/Effect.d.ts +3 -1
- package/dist/signals/base/Effect.js +31 -13
- package/dist/signals/base/ReadonlySignal.d.ts +0 -2
- package/dist/signals/base/ReadonlySignal.js +0 -25
- package/dist/signals/base/Signal.d.ts +1 -0
- package/dist/signals/base/Signal.js +4 -0
- package/package.json +1 -1
|
@@ -1,2 +1,16 @@
|
|
|
1
1
|
import { DevtoolsLike } from "./types";
|
|
2
|
-
|
|
2
|
+
interface ReduxDevtoolsExtension {
|
|
3
|
+
connect(options: {
|
|
4
|
+
name: string;
|
|
5
|
+
}): ReduxDevtoolsConnection;
|
|
6
|
+
}
|
|
7
|
+
interface ReduxDevtoolsConnection {
|
|
8
|
+
init(state: any): void;
|
|
9
|
+
send(action: any, state: any): void;
|
|
10
|
+
}
|
|
11
|
+
type Options = {
|
|
12
|
+
name?: string;
|
|
13
|
+
driver?: ReduxDevtoolsExtension;
|
|
14
|
+
};
|
|
15
|
+
export declare function reduxDevtools(options?: Options): DevtoolsLike;
|
|
16
|
+
export {};
|
|
@@ -1,21 +1,35 @@
|
|
|
1
1
|
import { Batcher } from "../../signals";
|
|
2
|
-
export function reduxDevtools() {
|
|
2
|
+
export function reduxDevtools(options = {}) {
|
|
3
|
+
const devtools = options.driver ?? window.__REDUX_DEVTOOLS_EXTENSION__;
|
|
4
|
+
if (!devtools) {
|
|
5
|
+
throw new Error('Redux Devtools extension is not installed');
|
|
6
|
+
}
|
|
3
7
|
let state = {};
|
|
4
|
-
|
|
5
|
-
const reduxDevtools = window.__REDUX_DEVTOOLS_EXTENSION__.connect({ name: 'RxToolkit' });
|
|
8
|
+
const reduxDevtools = devtools.connect({ name: options.name ?? 'RxToolkit' });
|
|
6
9
|
reduxDevtools.init(state);
|
|
7
10
|
const scheduler = Batcher.scheduler(Infinity);
|
|
11
|
+
let isCreated = false;
|
|
8
12
|
const updateFn = () => {
|
|
9
|
-
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);
|
|
10
18
|
};
|
|
11
19
|
const createFn = () => {
|
|
12
|
-
|
|
20
|
+
isCreated = true;
|
|
21
|
+
return updateFn;
|
|
13
22
|
};
|
|
14
23
|
return {
|
|
15
24
|
state(name, initState) {
|
|
16
25
|
state = { ...state, [name]: initState };
|
|
17
|
-
scheduler.schedule(createFn);
|
|
26
|
+
scheduler.schedule(createFn());
|
|
18
27
|
return (newState) => {
|
|
28
|
+
if (newState === '$COMPLETE' || newState === '$CLEANED') {
|
|
29
|
+
delete state[name];
|
|
30
|
+
clearFn();
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
19
33
|
state = { ...state, [name]: newState };
|
|
20
34
|
scheduler.schedule(updateFn);
|
|
21
35
|
};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export class PromiseResolver {
|
|
2
|
+
_resolve;
|
|
3
|
+
_reject;
|
|
4
|
+
promise = new Promise((resolve, reject) => {
|
|
5
|
+
this._resolve = resolve;
|
|
6
|
+
this._reject = reject;
|
|
7
|
+
});
|
|
8
|
+
resolve(value) {
|
|
9
|
+
this._resolve(value);
|
|
10
|
+
}
|
|
11
|
+
reject(reason) {
|
|
12
|
+
this._reject(reason);
|
|
13
|
+
}
|
|
14
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './PromiseResolver';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './PromiseResolver';
|
|
@@ -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,5 +1,7 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { PromiseResolver } from "../../../common/utils";
|
|
2
|
+
import { Batcher } from "../../../signals";
|
|
2
3
|
import { QueriesCache } from "../QueriesCache";
|
|
4
|
+
import { QueriesLifetimeHooks } from "../QueriesLifetimeHooks";
|
|
3
5
|
import { OperationAgent } from "./OperationAgent";
|
|
4
6
|
class OperationQueryState {
|
|
5
7
|
static create() {
|
|
@@ -50,10 +52,14 @@ class OperationQueryState {
|
|
|
50
52
|
}
|
|
51
53
|
export class Operation {
|
|
52
54
|
_options;
|
|
53
|
-
_queriesCache
|
|
55
|
+
_queriesCache;
|
|
56
|
+
_hooks;
|
|
54
57
|
_links = [];
|
|
58
|
+
_DEFAULT_CACHE_LIFETIME = 1_000;
|
|
55
59
|
constructor(_options) {
|
|
56
60
|
this._options = _options;
|
|
61
|
+
this._queriesCache = new QueriesCache(this._options.cacheLifetime ?? this._DEFAULT_CACHE_LIFETIME);
|
|
62
|
+
this._hooks = new QueriesLifetimeHooks(_options, 'Operation');
|
|
57
63
|
this._createLinks();
|
|
58
64
|
}
|
|
59
65
|
_createLinks() {
|
|
@@ -67,14 +73,31 @@ export class Operation {
|
|
|
67
73
|
getQueryCache(args) {
|
|
68
74
|
return this._queriesCache.getQueryCache(args);
|
|
69
75
|
}
|
|
70
|
-
createQueryCache(args) {
|
|
71
|
-
|
|
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;
|
|
72
92
|
}
|
|
73
93
|
initiate(args, options) {
|
|
74
|
-
|
|
94
|
+
return Batcher.batch(() => this._initiate(args, options));
|
|
95
|
+
}
|
|
96
|
+
_initiate(args, options) {
|
|
97
|
+
let cache = options?.cache ?? this.getQueryCache(args);
|
|
75
98
|
const state = OperationQueryState.load(cache?.value, args);
|
|
76
99
|
if (!cache) {
|
|
77
|
-
cache = this.
|
|
100
|
+
cache = this.createQueryCache(args, state);
|
|
78
101
|
}
|
|
79
102
|
else {
|
|
80
103
|
cache.next(state);
|
|
@@ -84,6 +107,8 @@ export class Operation {
|
|
|
84
107
|
const ref = link.resource.createRef(forwardedArgs);
|
|
85
108
|
return { link, ref, state: {} };
|
|
86
109
|
});
|
|
110
|
+
const query = this._options.queryFn(args);
|
|
111
|
+
const hookResolvers = this._hooks.onQueryStarted(args);
|
|
87
112
|
linksMeta.forEach(({ link, ref, state }) => {
|
|
88
113
|
if (link.lock) {
|
|
89
114
|
state.unlocker = ref.lock();
|
|
@@ -94,42 +119,51 @@ export class Operation {
|
|
|
94
119
|
});
|
|
95
120
|
}
|
|
96
121
|
});
|
|
97
|
-
const query = this._options.queryFn(args);
|
|
98
122
|
query
|
|
99
123
|
.then((result) => {
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
ref.
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
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
|
+
});
|
|
115
149
|
});
|
|
116
150
|
})
|
|
117
151
|
.catch((error) => {
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
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
|
+
});
|
|
133
167
|
});
|
|
134
168
|
});
|
|
135
169
|
return cache;
|
|
@@ -161,17 +195,3 @@ export class Operation {
|
|
|
161
195
|
return resolver.promise;
|
|
162
196
|
}
|
|
163
197
|
}
|
|
164
|
-
class PromiseResolver {
|
|
165
|
-
_resolve;
|
|
166
|
-
_reject;
|
|
167
|
-
promise = new Promise((resolve, reject) => {
|
|
168
|
-
this._resolve = resolve;
|
|
169
|
-
this._reject = reject;
|
|
170
|
-
});
|
|
171
|
-
resolve(value) {
|
|
172
|
-
this._resolve(value);
|
|
173
|
-
}
|
|
174
|
-
reject(reason) {
|
|
175
|
-
this._reject(reason);
|
|
176
|
-
}
|
|
177
|
-
}
|
|
@@ -6,7 +6,7 @@ export class OperationAgent {
|
|
|
6
6
|
}, { disableDevtools: true });
|
|
7
7
|
state$ = new Computed(() => {
|
|
8
8
|
const operations = this._operations$.value;
|
|
9
|
-
const currState = operations.current$?.value;
|
|
9
|
+
const currState = operations.current$?.value$.value;
|
|
10
10
|
// Нет текущего состояния — дефолт
|
|
11
11
|
if (!currState) {
|
|
12
12
|
return {
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { ReactiveCache } from "../../query/lib/ReactiveCache";
|
|
2
2
|
export declare class QueriesCache<KEY, VALUE> {
|
|
3
|
-
private
|
|
3
|
+
private _cacheLifeTime;
|
|
4
4
|
private readonly _cache;
|
|
5
|
-
constructor(
|
|
5
|
+
constructor(_cacheLifeTime?: number | false);
|
|
6
6
|
getQueryCache(args: KEY): ReactiveCache<VALUE> | undefined;
|
|
7
7
|
createQueryCache(args: KEY, initialState: VALUE): ReactiveCache<VALUE>;
|
|
8
8
|
}
|
|
@@ -1,12 +1,10 @@
|
|
|
1
|
-
import { SharedOptions } from "../../common/options/SharedOptions";
|
|
2
|
-
import { Indexer } from "../../signals/base/Indexer";
|
|
3
1
|
import { IndirectMap } from "../../query/lib/IndirectMap";
|
|
4
2
|
import { ReactiveCache } from "../../query/lib/ReactiveCache";
|
|
5
3
|
export class QueriesCache {
|
|
6
|
-
|
|
4
|
+
_cacheLifeTime;
|
|
7
5
|
_cache = new IndirectMap();
|
|
8
|
-
constructor(
|
|
9
|
-
this.
|
|
6
|
+
constructor(_cacheLifeTime = 60_000) {
|
|
7
|
+
this._cacheLifeTime = _cacheLifeTime;
|
|
10
8
|
}
|
|
11
9
|
getQueryCache(args) {
|
|
12
10
|
return this._cache.get(args);
|
|
@@ -14,20 +12,23 @@ export class QueriesCache {
|
|
|
14
12
|
createQueryCache(args, initialState) {
|
|
15
13
|
const cache = new ReactiveCache({
|
|
16
14
|
initialState,
|
|
15
|
+
cacheLifeTime: this._cacheLifeTime,
|
|
17
16
|
});
|
|
18
|
-
const stateDevtools = SharedOptions.DEVTOOLS?.state;
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
17
|
+
// const stateDevtools = SharedOptions.DEVTOOLS?.state;
|
|
18
|
+
//
|
|
19
|
+
// if (stateDevtools) {
|
|
20
|
+
// const key = `${this._logname}:${JSON.stringify(args)}:i=${Indexer.getIndex()}`;
|
|
21
|
+
// let devtools = stateDevtools(key, initialState);
|
|
22
|
+
//
|
|
23
|
+
// cache.spy$.subscribe((state) => {
|
|
24
|
+
// if (state === initialState) return;
|
|
25
|
+
// devtools(state);
|
|
26
|
+
// });
|
|
27
|
+
//
|
|
28
|
+
// cache.onClean$.subscribe(() => {
|
|
29
|
+
// devtools('$CLEANED' as any);
|
|
30
|
+
// });
|
|
31
|
+
// }
|
|
31
32
|
cache.onClean$.subscribe(() => {
|
|
32
33
|
this._cache.delete(args);
|
|
33
34
|
});
|
|
@@ -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
|
+
}
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { ReactiveCache } from "../../../query/lib/ReactiveCache";
|
|
2
2
|
import type { ResourceCreateOptions, ResourceDefinition, ResourceInstance, ResourceRefInstanse, ResourceTransaction } from "../../../query/types";
|
|
3
|
-
import { QueriesCache } from "../../../query/core/QueriesCache";
|
|
4
3
|
import { ResourceAgent } from "./ResourceAgent";
|
|
5
4
|
export type CoreResourceQueryState<D extends ResourceDefinition> = {
|
|
6
5
|
transactions: ResourceTransaction[] | null;
|
|
@@ -21,24 +20,20 @@ export type CoreResourceQueryState<D extends ResourceDefinition> = {
|
|
|
21
20
|
export type CoreResourceQueryCache<D extends ResourceDefinition> = ReactiveCache<CoreResourceQueryState<D>>;
|
|
22
21
|
export declare class Resource<D extends ResourceDefinition> implements ResourceInstance<D> {
|
|
23
22
|
private readonly _options;
|
|
24
|
-
readonly _queriesCache
|
|
23
|
+
private readonly _queriesCache;
|
|
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>;
|
|
33
34
|
decrementLock(args: D['Args'], options?: {
|
|
34
35
|
cache?: CoreResourceQueryCache<D>;
|
|
35
36
|
}): CoreResourceQueryCache<D> | null;
|
|
36
|
-
/**
|
|
37
|
-
* @deprecated
|
|
38
|
-
*/
|
|
39
|
-
updateData_legacy(args: D['Args'], updateFn: (data: D['Data']) => D['Data'], options?: {
|
|
40
|
-
cache?: CoreResourceQueryCache<D>;
|
|
41
|
-
}): CoreResourceQueryCache<D> | null;
|
|
42
37
|
update(args: D['Args'], updateFn: (data: D['Data'], savedData: D['Data'] | null, transactions: ResourceTransaction[] | null) => {
|
|
43
38
|
data: D['Data'];
|
|
44
39
|
transactions: ResourceTransaction[] | null;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { QueriesCache } from "../QueriesCache";
|
|
2
|
+
import { QueriesLifetimeHooks } from "../QueriesLifetimeHooks";
|
|
3
3
|
import { ResourceAgent } from "./ResourceAgent";
|
|
4
4
|
import { ResourceRef } from "./ResourceRef";
|
|
5
5
|
class ResourceQueryState {
|
|
@@ -92,9 +92,13 @@ class ResourceQueryState {
|
|
|
92
92
|
}
|
|
93
93
|
export class Resource {
|
|
94
94
|
_options;
|
|
95
|
-
_queriesCache
|
|
95
|
+
_queriesCache;
|
|
96
|
+
_hooks;
|
|
97
|
+
_DEFAULT_CACHE_LIFETIME = 60_000;
|
|
96
98
|
constructor(_options) {
|
|
97
99
|
this._options = _options;
|
|
100
|
+
this._hooks = new QueriesLifetimeHooks(_options, 'Resource');
|
|
101
|
+
this._queriesCache = new QueriesCache(_options.cacheLifetime ?? this._DEFAULT_CACHE_LIFETIME);
|
|
98
102
|
}
|
|
99
103
|
createAgent = () => {
|
|
100
104
|
return new ResourceAgent(this);
|
|
@@ -105,8 +109,22 @@ export class Resource {
|
|
|
105
109
|
getQueryCache(args) {
|
|
106
110
|
return this._queriesCache.getQueryCache(args);
|
|
107
111
|
}
|
|
108
|
-
createQueryCache(args) {
|
|
109
|
-
|
|
112
|
+
createQueryCache(args, state = ResourceQueryState.create()) {
|
|
113
|
+
const cache = this._queriesCache.createQueryCache(args, state);
|
|
114
|
+
const hookResolvers = this._hooks.onCacheEntryAdded(args);
|
|
115
|
+
const spySub = cache.spy$.subscribe((state) => {
|
|
116
|
+
if (!state.isDone)
|
|
117
|
+
return;
|
|
118
|
+
hookResolvers.cacheDataLoaded();
|
|
119
|
+
spySub.unsubscribe();
|
|
120
|
+
});
|
|
121
|
+
cache.spy$.subscribe((data) => {
|
|
122
|
+
hookResolvers.dataChanged$.next(data);
|
|
123
|
+
});
|
|
124
|
+
cache.onClean$.subscribe(() => {
|
|
125
|
+
hookResolvers.cacheEntryRemoved();
|
|
126
|
+
});
|
|
127
|
+
return cache;
|
|
110
128
|
}
|
|
111
129
|
incrementLock(args, options) {
|
|
112
130
|
let cache = options?.cache ?? this.getQueryCache(args);
|
|
@@ -124,22 +142,6 @@ export class Resource {
|
|
|
124
142
|
cache.next(ResourceQueryState.decrementLock(cache.value));
|
|
125
143
|
return cache;
|
|
126
144
|
}
|
|
127
|
-
/**
|
|
128
|
-
* @deprecated
|
|
129
|
-
*/
|
|
130
|
-
updateData_legacy(args, updateFn, options) {
|
|
131
|
-
let cache = options?.cache ?? this.getQueryCache(args);
|
|
132
|
-
if (!cache) {
|
|
133
|
-
return null;
|
|
134
|
-
}
|
|
135
|
-
const cacheValue = cache.value;
|
|
136
|
-
if (!cacheValue.isDone) {
|
|
137
|
-
return cache;
|
|
138
|
-
}
|
|
139
|
-
const newData = updateFn(cacheValue.data);
|
|
140
|
-
cache.next(ResourceQueryState.setData(cache.value, newData));
|
|
141
|
-
return cache;
|
|
142
|
-
}
|
|
143
145
|
update(args, updateFn, options) {
|
|
144
146
|
let cache = options?.cache ?? this.getQueryCache(args);
|
|
145
147
|
if (!cache) {
|
|
@@ -154,10 +156,10 @@ export class Resource {
|
|
|
154
156
|
return cache;
|
|
155
157
|
}
|
|
156
158
|
initiate(args, options) {
|
|
157
|
-
let cache = options?.cache ?? this.
|
|
159
|
+
let cache = options?.cache ?? this.getQueryCache(args);
|
|
158
160
|
const state = ResourceQueryState.load(cache?.value, args);
|
|
159
161
|
if (!cache) {
|
|
160
|
-
cache = this.
|
|
162
|
+
cache = this.createQueryCache(args, state);
|
|
161
163
|
}
|
|
162
164
|
else {
|
|
163
165
|
cache.next(state);
|
|
@@ -166,20 +168,22 @@ export class Resource {
|
|
|
166
168
|
abortController?.abort();
|
|
167
169
|
abortController = new AbortController();
|
|
168
170
|
const query = this._options.queryFn(args, { abortSignal: abortController.signal });
|
|
171
|
+
const hookResolvers = this._hooks.onQueryStarted(args);
|
|
169
172
|
query
|
|
170
|
-
.then((
|
|
173
|
+
.then((result) => {
|
|
171
174
|
if (abortController.signal.aborted) {
|
|
172
175
|
return;
|
|
173
176
|
}
|
|
174
|
-
const
|
|
175
|
-
cache.next(ResourceQueryState.success(state,
|
|
177
|
+
const data = this._options.select ? result._options.select(result) : result;
|
|
178
|
+
cache.next(ResourceQueryState.success(state, data));
|
|
179
|
+
hookResolvers.fulfilledSuccess(data);
|
|
176
180
|
})
|
|
177
181
|
.catch((error) => {
|
|
178
182
|
if (abortController.signal.aborted) {
|
|
179
183
|
return;
|
|
180
184
|
}
|
|
181
|
-
SharedOptions.onError?.(error);
|
|
182
185
|
cache.next(ResourceQueryState.error(state, error));
|
|
186
|
+
hookResolvers.fulfilledError(error);
|
|
183
187
|
});
|
|
184
188
|
return cache;
|
|
185
189
|
}
|
|
@@ -8,7 +8,7 @@ export class ResourceAgent {
|
|
|
8
8
|
state$ = new Computed(() => {
|
|
9
9
|
const resources = this._resources$.value;
|
|
10
10
|
let prevState;
|
|
11
|
-
const currState = resources.current$?.value;
|
|
11
|
+
const currState = resources.current$?.value$.value;
|
|
12
12
|
if (!currState?.isDone) {
|
|
13
13
|
prevState = resources.previous$?.value;
|
|
14
14
|
}
|
|
@@ -81,7 +81,8 @@ export class ResourceAgent {
|
|
|
81
81
|
this._next(cache);
|
|
82
82
|
}
|
|
83
83
|
}
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
84
|
+
complete() {
|
|
85
|
+
this.state$.complete();
|
|
86
|
+
this._resources$.complete();
|
|
87
|
+
}
|
|
87
88
|
}
|
|
@@ -10,9 +10,6 @@ export declare class ResourceRef<D extends ResourceDefinition> implements Resour
|
|
|
10
10
|
unlock: () => void;
|
|
11
11
|
};
|
|
12
12
|
unlockOne(): void;
|
|
13
|
-
update(updateFn: (data: D["Data"]) => D["Data"]): {
|
|
14
|
-
rollback: () => void;
|
|
15
|
-
};
|
|
16
13
|
patch(patchFn: (data: D["Data"]) => void): ResourceTransaction | null;
|
|
17
14
|
create(data: D["Data"]): void;
|
|
18
15
|
invalidate(): void;
|
|
@@ -29,22 +29,6 @@ export class ResourceRef {
|
|
|
29
29
|
unlockOne() {
|
|
30
30
|
this._cacheItem = this._resource.decrementLock(this._args, { cache: this._cacheItem });
|
|
31
31
|
}
|
|
32
|
-
update(updateFn) {
|
|
33
|
-
const cacheItem = this._cacheItem ?? this._resource.getQueryCache(this._args);
|
|
34
|
-
if (!cacheItem) {
|
|
35
|
-
console.warn('Trying to update non-existing cache item');
|
|
36
|
-
return {
|
|
37
|
-
rollback: () => { }
|
|
38
|
-
};
|
|
39
|
-
}
|
|
40
|
-
const value = cacheItem.value;
|
|
41
|
-
this._resource.updateData_legacy(this._args, updateFn, { cache: cacheItem });
|
|
42
|
-
return {
|
|
43
|
-
rollback: () => {
|
|
44
|
-
this._resource.updateData_legacy(this._args, () => value.data, { cache: cacheItem });
|
|
45
|
-
}
|
|
46
|
-
};
|
|
47
|
-
}
|
|
48
32
|
patch(patchFn) {
|
|
49
33
|
let isSkipped = true;
|
|
50
34
|
const reapplyFn = (data, savedData, transactions) => {
|
|
@@ -56,12 +40,14 @@ export class ResourceRef {
|
|
|
56
40
|
// Все pending - применяем и оставляем в очереди
|
|
57
41
|
// Все commited (которые после pending) - применяем, но оставляем в очереди
|
|
58
42
|
// Все aborted (которые после pending) - откатываем, но оставляем в очереди
|
|
43
|
+
// Если после aborted нет pending - пропускаем и убираем из очереди
|
|
59
44
|
// Те после применения всех транзакций, очередь должна начинаться с первой pending транзакции (если есть), включая все, что после неё.
|
|
60
45
|
let newSavedData = savedData ?? data;
|
|
61
46
|
let currentData = savedData ?? data;
|
|
62
47
|
const remainingTransactions = [];
|
|
63
48
|
let foundPending = false;
|
|
64
|
-
transactions.
|
|
49
|
+
const lastPendingIndex = transactions.findLastIndex(t => t.status === 'pending');
|
|
50
|
+
transactions.forEach((transaction, index) => {
|
|
65
51
|
if (transaction.status === 'pending') {
|
|
66
52
|
foundPending = true;
|
|
67
53
|
// Применяем pending транзакцию и оставляем в очереди
|
|
@@ -76,9 +62,13 @@ export class ResourceRef {
|
|
|
76
62
|
remainingTransactions.push(transaction);
|
|
77
63
|
}
|
|
78
64
|
else if (transaction.status === 'aborted') {
|
|
79
|
-
//
|
|
80
|
-
|
|
81
|
-
|
|
65
|
+
// Проверяем, есть ли pending после текущей aborted
|
|
66
|
+
const hasPendingAfter = index < lastPendingIndex;
|
|
67
|
+
if (hasPendingAfter) {
|
|
68
|
+
currentData = applyPatches(currentData, transaction.inversePatches);
|
|
69
|
+
remainingTransactions.push(transaction);
|
|
70
|
+
}
|
|
71
|
+
// Если pending нет - пропускаем и убираем из очереди
|
|
82
72
|
}
|
|
83
73
|
}
|
|
84
74
|
else {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { Observable, Subject } from "rxjs";
|
|
2
|
+
import { ReadableSignalLike } from "../../signals";
|
|
2
3
|
type Options<VALUE> = {
|
|
3
4
|
/**
|
|
4
5
|
* Начальное состояние кэша
|
|
@@ -6,9 +7,11 @@ type Options<VALUE> = {
|
|
|
6
7
|
initialState: VALUE;
|
|
7
8
|
/**
|
|
8
9
|
* Время жизни кэша в миллисекундах (пока нет подписок на кеш)
|
|
10
|
+
* Если указано `false`, кэш не будет очищаться автоматически.
|
|
11
|
+
* Если указано `0` или меньше, кэш будет очищаться сразу после отписки от него.
|
|
9
12
|
* @default 60_000 (1 минута)
|
|
10
13
|
*/
|
|
11
|
-
cacheLifeTime?: number;
|
|
14
|
+
cacheLifeTime?: number | false;
|
|
12
15
|
};
|
|
13
16
|
/**
|
|
14
17
|
* Класс `ReactiveCache` представляет собой реактивный кэш,
|
|
@@ -17,26 +20,15 @@ type Options<VALUE> = {
|
|
|
17
20
|
* @template VALUE Тип значения, хранимого в кэше.
|
|
18
21
|
*/
|
|
19
22
|
export declare class ReactiveCache<VALUE> {
|
|
20
|
-
/**
|
|
21
|
-
* Время жизни кэша в миллисекундах.
|
|
22
|
-
* Если значение больше 0, то кэш очищается через указанное время после отписки.
|
|
23
|
-
* @private
|
|
24
|
-
*/
|
|
25
|
-
private readonly _cacheLifeTime;
|
|
26
23
|
/**
|
|
27
24
|
* Внутренний `BehaviorSubject`, хранящий текущее состояние кэша.
|
|
28
25
|
* @private
|
|
29
26
|
*/
|
|
30
27
|
private _state$;
|
|
31
|
-
/**
|
|
32
|
-
* Текущее значение.
|
|
33
|
-
* @private
|
|
34
|
-
*/
|
|
35
|
-
private _value;
|
|
36
28
|
/**
|
|
37
29
|
* Реактивное значене (Observable)
|
|
38
30
|
*/
|
|
39
|
-
value$:
|
|
31
|
+
value$: ReadableSignalLike<VALUE>;
|
|
40
32
|
/**
|
|
41
33
|
* Значение без сайд-эффектов (для использования в DevTools)
|
|
42
34
|
*/
|
|
@@ -53,16 +45,8 @@ export declare class ReactiveCache<VALUE> {
|
|
|
53
45
|
* @param options.cacheLifeTime Время жизни кэша в миллисекундах (по умолчанию 60_000).
|
|
54
46
|
*/
|
|
55
47
|
constructor(options: Options<VALUE>);
|
|
56
|
-
|
|
57
|
-
* Возвращает текущее значение кэша.
|
|
58
|
-
* @returns {VALUE} Текущее значение кэша.
|
|
59
|
-
*/
|
|
48
|
+
private _getOnRefCountZero;
|
|
60
49
|
get value(): VALUE;
|
|
61
|
-
/**
|
|
62
|
-
* Возвращает текущее значение кэша.
|
|
63
|
-
* @returns {VALUE} Текущее значение кэша.
|
|
64
|
-
*/
|
|
65
|
-
peek(): VALUE;
|
|
66
50
|
/**
|
|
67
51
|
* Устанавливает новое значение в кэш и обновляет поток состояния.
|
|
68
52
|
*
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { finalize, ReplaySubject, share, Subject, takeUntil, timer } from "rxjs";
|
|
2
|
-
import {
|
|
1
|
+
import { BehaviorSubject, finalize, ReplaySubject, share, Subject, takeUntil, timer } from "rxjs";
|
|
2
|
+
import { signalize } from "../../signals";
|
|
3
3
|
/**
|
|
4
4
|
* Класс `ReactiveCache` представляет собой реактивный кэш,
|
|
5
5
|
* который позволяет управлять состоянием и временем жизни кэшированных данных.
|
|
@@ -7,22 +7,11 @@ import { Signal } from "../../signals";
|
|
|
7
7
|
* @template VALUE Тип значения, хранимого в кэше.
|
|
8
8
|
*/
|
|
9
9
|
export class ReactiveCache {
|
|
10
|
-
/**
|
|
11
|
-
* Время жизни кэша в миллисекундах.
|
|
12
|
-
* Если значение больше 0, то кэш очищается через указанное время после отписки.
|
|
13
|
-
* @private
|
|
14
|
-
*/
|
|
15
|
-
_cacheLifeTime;
|
|
16
10
|
/**
|
|
17
11
|
* Внутренний `BehaviorSubject`, хранящий текущее состояние кэша.
|
|
18
12
|
* @private
|
|
19
13
|
*/
|
|
20
14
|
_state$;
|
|
21
|
-
/**
|
|
22
|
-
* Текущее значение.
|
|
23
|
-
* @private
|
|
24
|
-
*/
|
|
25
|
-
_value;
|
|
26
15
|
/**
|
|
27
16
|
* Реактивное значене (Observable)
|
|
28
17
|
*/
|
|
@@ -43,46 +32,37 @@ export class ReactiveCache {
|
|
|
43
32
|
* @param options.cacheLifeTime Время жизни кэша в миллисекундах (по умолчанию 60_000).
|
|
44
33
|
*/
|
|
45
34
|
constructor(options) {
|
|
46
|
-
|
|
47
|
-
this._state$ = new
|
|
48
|
-
this._value = options.initialState;
|
|
35
|
+
const cacheLifeTime = options.cacheLifeTime ?? 60_000;
|
|
36
|
+
this._state$ = new BehaviorSubject(options.initialState);
|
|
49
37
|
this.spy$ = this._state$.pipe(takeUntil(this.onClean$));
|
|
50
|
-
this.value$ = this._state$.pipe(finalize(() => {
|
|
38
|
+
this.value$ = signalize(this._state$.pipe(finalize(() => {
|
|
51
39
|
this.complete();
|
|
52
40
|
}), share({
|
|
53
41
|
connector: () => new ReplaySubject(1),
|
|
54
|
-
|
|
55
|
-
* Если lifetime больше 0,
|
|
56
|
-
* то очистим кэш значения по истечении этого времени,
|
|
57
|
-
* иначе очищаем сразу после отписки.
|
|
58
|
-
*/
|
|
59
|
-
resetOnRefCountZero: this._cacheLifeTime > 0
|
|
60
|
-
? () => timer(this._cacheLifeTime)
|
|
61
|
-
: true,
|
|
42
|
+
resetOnRefCountZero: this._getOnRefCountZero(cacheLifeTime),
|
|
62
43
|
resetOnComplete: true,
|
|
63
|
-
}));
|
|
44
|
+
})));
|
|
45
|
+
}
|
|
46
|
+
_getOnRefCountZero(cacheLifeTime) {
|
|
47
|
+
if (cacheLifeTime === false) {
|
|
48
|
+
return false;
|
|
49
|
+
}
|
|
50
|
+
if (cacheLifeTime <= 0) {
|
|
51
|
+
return true;
|
|
52
|
+
}
|
|
53
|
+
return () => {
|
|
54
|
+
return timer(cacheLifeTime);
|
|
55
|
+
};
|
|
64
56
|
}
|
|
65
|
-
/**
|
|
66
|
-
* Возвращает текущее значение кэша.
|
|
67
|
-
* @returns {VALUE} Текущее значение кэша.
|
|
68
|
-
*/
|
|
69
57
|
get value() {
|
|
70
58
|
return this._state$.value;
|
|
71
59
|
}
|
|
72
|
-
/**
|
|
73
|
-
* Возвращает текущее значение кэша.
|
|
74
|
-
* @returns {VALUE} Текущее значение кэша.
|
|
75
|
-
*/
|
|
76
|
-
peek() {
|
|
77
|
-
return this._value;
|
|
78
|
-
}
|
|
79
60
|
/**
|
|
80
61
|
* Устанавливает новое значение в кэш и обновляет поток состояния.
|
|
81
62
|
*
|
|
82
63
|
* @param value Новое значение для кэша.
|
|
83
64
|
*/
|
|
84
65
|
next(value) {
|
|
85
|
-
this._value = value;
|
|
86
66
|
this._state$.next(value);
|
|
87
67
|
}
|
|
88
68
|
/**
|
|
@@ -90,7 +70,7 @@ export class ReactiveCache {
|
|
|
90
70
|
*/
|
|
91
71
|
complete() {
|
|
92
72
|
this._state$.complete();
|
|
93
|
-
this.onClean$.next(this.
|
|
73
|
+
this.onClean$.next(this._state$.value);
|
|
94
74
|
this.onClean$.complete();
|
|
95
75
|
}
|
|
96
76
|
}
|
|
@@ -1,8 +1,5 @@
|
|
|
1
|
-
import { Prettify,
|
|
1
|
+
import { Prettify, ResourceDefinition, ResourceInstance, ResourceQueryState } from "../../query/types";
|
|
2
2
|
import { SKIP } from "../../query/SKIP_TOKEN";
|
|
3
|
-
type WithAgent<D extends ResourceDefinition> = {
|
|
4
|
-
createAgent: () => ResourceAgentInstance<D>;
|
|
5
|
-
};
|
|
6
3
|
type Result<D extends ResourceDefinition> = Prettify<ResourceQueryState<D>>;
|
|
7
|
-
export declare function useResourceAgent<D extends ResourceDefinition>(res:
|
|
4
|
+
export declare function useResourceAgent<D extends ResourceDefinition>(res: ResourceInstance<D>, ...argss: D['Args'] extends void ? [] | [typeof SKIP] : [D['Args'] | typeof SKIP]): Result<D>;
|
|
8
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
|
*/
|
|
@@ -13,6 +13,24 @@ export type ResourceCreateOptions<D extends ResourceDefinition> = {
|
|
|
13
13
|
select?: (data: D["Result"]) => D["Selected"];
|
|
14
14
|
/** Функция запроса данных */
|
|
15
15
|
queryFn: (args: D["Args"], tools: ResourceQueryFnTools) => Promise<D["Result"]>;
|
|
16
|
+
/**
|
|
17
|
+
* Время жизни кеша в миллисекундах. По умолчанию 60_000 (1 минута).
|
|
18
|
+
* Если указано false - кеш не удаляется автоматически.
|
|
19
|
+
*/
|
|
20
|
+
cacheLifetime?: number | false;
|
|
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"]>;
|
|
16
34
|
};
|
|
17
35
|
/**
|
|
18
36
|
* Определение типов ресурса
|
|
@@ -45,8 +63,8 @@ export type ResourceAgentInstance<D extends ResourceDefinition> = {
|
|
|
45
63
|
state$: ReadableSignalLike<ResourceQueryState<D>>;
|
|
46
64
|
/** Инициирует запрос с указанными аргументами */
|
|
47
65
|
initiate(args: D["Args"], force?: boolean): void;
|
|
48
|
-
/**
|
|
49
|
-
|
|
66
|
+
/** Завершает работу агента, позволяя освободить ресурсы */
|
|
67
|
+
complete(): void;
|
|
50
68
|
};
|
|
51
69
|
/**
|
|
52
70
|
* Состояние запроса ресурса
|
|
@@ -93,12 +111,6 @@ export type ResourceRefInstanse<D extends ResourceDefinition> = {
|
|
|
93
111
|
unlock: () => void;
|
|
94
112
|
};
|
|
95
113
|
unlockOne(): void;
|
|
96
|
-
/**
|
|
97
|
-
* @deprecated
|
|
98
|
-
*/
|
|
99
|
-
update(updateFn: (data: D['Data']) => D['Data']): {
|
|
100
|
-
rollback: () => void;
|
|
101
|
-
};
|
|
102
114
|
patch(patchFn: (data: D['Data']) => void): ResourceTransaction | null;
|
|
103
115
|
invalidate(): void;
|
|
104
116
|
create(data: D['Data']): void;
|
|
@@ -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',
|
|
@@ -20,10 +22,11 @@ export class Computed extends Signal {
|
|
|
20
22
|
this._effect = effect;
|
|
21
23
|
}
|
|
22
24
|
unsubscribe() {
|
|
23
|
-
this.
|
|
24
|
-
super.unsubscribe();
|
|
25
|
+
this.complete();
|
|
25
26
|
}
|
|
26
27
|
complete() {
|
|
28
|
+
if (this.closed)
|
|
29
|
+
return;
|
|
27
30
|
this._effect.unsubscribe();
|
|
28
31
|
super.complete();
|
|
29
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,20 +1,22 @@
|
|
|
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
|
/**
|
|
11
13
|
* Выполняет функцию в tracked-контексте, подписываясь на Tracker.
|
|
12
14
|
*/
|
|
13
15
|
_runInTrackedContext(effectFn, isAsyncRun = false) {
|
|
14
|
-
|
|
16
|
+
let prevSubscriptions;
|
|
15
17
|
if (!isAsyncRun) {
|
|
16
18
|
this._rang = 0;
|
|
17
|
-
this._subscriptions
|
|
19
|
+
prevSubscriptions = this._subscriptions;
|
|
18
20
|
this._subscriptions = [];
|
|
19
21
|
}
|
|
20
22
|
let isTrackedContext = true;
|
|
@@ -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);
|
|
@@ -43,9 +54,16 @@ export class Effect {
|
|
|
43
54
|
trackerSub.unsubscribe();
|
|
44
55
|
isTrackedContext = false;
|
|
45
56
|
scheduler = Batcher.scheduler(this._rang);
|
|
57
|
+
prevSubscriptions?.forEach((sub) => sub.unsubscribe());
|
|
46
58
|
}
|
|
47
59
|
unsubscribe() {
|
|
60
|
+
this.complete();
|
|
61
|
+
}
|
|
62
|
+
complete() {
|
|
63
|
+
if (this.closed)
|
|
64
|
+
return;
|
|
48
65
|
this.closed = true;
|
|
49
66
|
this._subscriptions.forEach((sub) => sub.unsubscribe());
|
|
67
|
+
this._onComplete?.();
|
|
50
68
|
}
|
|
51
69
|
}
|
|
@@ -3,8 +3,6 @@ import type { ReadableSignalLike } from "./types";
|
|
|
3
3
|
import { SyncObservable } from "./SyncObservable";
|
|
4
4
|
export declare class ReadonlySignal<T> extends SyncObservable<T> implements ReadableSignalLike<T> {
|
|
5
5
|
protected rang: number;
|
|
6
|
-
private readonly _devtools;
|
|
7
|
-
private static _logIdIndex;
|
|
8
6
|
constructor(subscribe?: (this: Observable<T>, subscriber: Subscriber<T>) => TeardownLogic);
|
|
9
7
|
get value(): T;
|
|
10
8
|
peek(): T;
|
|
@@ -1,34 +1,9 @@
|
|
|
1
|
-
import { Subscriber } from "rxjs";
|
|
2
|
-
import { SharedOptions } from "../../common/options/SharedOptions";
|
|
3
1
|
import { SyncObservable } from "./SyncObservable";
|
|
4
2
|
import { Tracker } from "./Tracker";
|
|
5
3
|
export class ReadonlySignal extends SyncObservable {
|
|
6
4
|
rang = 0;
|
|
7
|
-
_devtools;
|
|
8
|
-
static _logIdIndex = 0;
|
|
9
5
|
constructor(subscribe) {
|
|
10
|
-
const stateDevtools = SharedOptions.DEVTOOLS?.state;
|
|
11
|
-
const originalSubscribe = subscribe;
|
|
12
|
-
if (stateDevtools && originalSubscribe) {
|
|
13
|
-
subscribe = (subscriber) => {
|
|
14
|
-
const wrappedSubscriber = new Subscriber({
|
|
15
|
-
next: (value) => {
|
|
16
|
-
this._devtools?.(value);
|
|
17
|
-
subscriber.next(value);
|
|
18
|
-
},
|
|
19
|
-
error: (err) => subscriber.error(err),
|
|
20
|
-
complete: () => subscriber.complete()
|
|
21
|
-
});
|
|
22
|
-
return originalSubscribe.call(this, wrappedSubscriber);
|
|
23
|
-
};
|
|
24
|
-
}
|
|
25
6
|
super(subscribe);
|
|
26
|
-
if (stateDevtools) {
|
|
27
|
-
const id = ReadonlySignal._logIdIndex++;
|
|
28
|
-
const key = `ReadonlySignal:i=${id}`;
|
|
29
|
-
const initialValue = this.peek();
|
|
30
|
-
this._devtools = stateDevtools(key, initialValue);
|
|
31
|
-
}
|
|
32
7
|
}
|
|
33
8
|
get value() {
|
|
34
9
|
Tracker.next(this.rang, this);
|
|
@@ -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;
|