@fozy-labs/rx-toolkit 0.4.3 → 0.4.4
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 +6 -3
- 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.js +5 -17
- package/dist/query/core/Opertation/OperationAgent.js +1 -1
- package/dist/query/core/QueriesCache.d.ts +2 -1
- package/dist/query/core/QueriesCache.js +19 -16
- package/dist/query/core/Resource/Resource.d.ts +2 -8
- package/dist/query/core/Resource/Resource.js +77 -24
- 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/Resource.types.d.ts +27 -8
- package/dist/signals/base/Computed.js +1 -2
- package/dist/signals/base/Effect.js +3 -2
- package/dist/signals/base/ReadonlySignal.d.ts +0 -2
- package/dist/signals/base/ReadonlySignal.js +0 -25
- 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,8 +1,11 @@
|
|
|
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);
|
|
8
11
|
const updateFn = () => {
|
|
@@ -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,3 +1,4 @@
|
|
|
1
|
+
import { PromiseResolver } from "../../../common/utils";
|
|
1
2
|
import { SharedOptions } from "../../../common/options/SharedOptions";
|
|
2
3
|
import { QueriesCache } from "../QueriesCache";
|
|
3
4
|
import { OperationAgent } from "./OperationAgent";
|
|
@@ -50,7 +51,7 @@ class OperationQueryState {
|
|
|
50
51
|
}
|
|
51
52
|
export class Operation {
|
|
52
53
|
_options;
|
|
53
|
-
_queriesCache = new QueriesCache('Operation');
|
|
54
|
+
_queriesCache = new QueriesCache(60_000, 'Operation');
|
|
54
55
|
_links = [];
|
|
55
56
|
constructor(_options) {
|
|
56
57
|
this._options = _options;
|
|
@@ -104,9 +105,10 @@ export class Operation {
|
|
|
104
105
|
*/
|
|
105
106
|
linksMeta.forEach(({ link, ref, state }) => {
|
|
106
107
|
if (link.update && ref.has) {
|
|
107
|
-
|
|
108
|
+
// TODO подумать, нужно ли добавлять обработку, если patch() -> null (и в принце про работу patch)
|
|
109
|
+
ref.patch((draft) => {
|
|
108
110
|
return link.update({ draft, args, data });
|
|
109
|
-
});
|
|
111
|
+
})?.commit();
|
|
110
112
|
}
|
|
111
113
|
if (link.create && !ref.has) {
|
|
112
114
|
ref.create(link.create({ args, data }));
|
|
@@ -161,17 +163,3 @@ export class Operation {
|
|
|
161
163
|
return resolver.promise;
|
|
162
164
|
}
|
|
163
165
|
}
|
|
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,9 @@
|
|
|
1
1
|
import { ReactiveCache } from "../../query/lib/ReactiveCache";
|
|
2
2
|
export declare class QueriesCache<KEY, VALUE> {
|
|
3
|
+
private _cacheLifeTime;
|
|
3
4
|
private _logname;
|
|
4
5
|
private readonly _cache;
|
|
5
|
-
constructor(_logname?: string);
|
|
6
|
+
constructor(_cacheLifeTime?: number | false, _logname?: string);
|
|
6
7
|
getQueryCache(args: KEY): ReactiveCache<VALUE> | undefined;
|
|
7
8
|
createQueryCache(args: KEY, initialState: VALUE): ReactiveCache<VALUE>;
|
|
8
9
|
}
|
|
@@ -1,11 +1,11 @@
|
|
|
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 {
|
|
4
|
+
_cacheLifeTime;
|
|
6
5
|
_logname;
|
|
7
6
|
_cache = new IndirectMap();
|
|
8
|
-
constructor(_logname = 'query') {
|
|
7
|
+
constructor(_cacheLifeTime = 60_000, _logname = 'query') {
|
|
8
|
+
this._cacheLifeTime = _cacheLifeTime;
|
|
9
9
|
this._logname = _logname;
|
|
10
10
|
}
|
|
11
11
|
getQueryCache(args) {
|
|
@@ -14,20 +14,23 @@ export class QueriesCache {
|
|
|
14
14
|
createQueryCache(args, initialState) {
|
|
15
15
|
const cache = new ReactiveCache({
|
|
16
16
|
initialState,
|
|
17
|
+
cacheLifeTime: this._cacheLifeTime,
|
|
17
18
|
});
|
|
18
|
-
const stateDevtools = SharedOptions.DEVTOOLS?.state;
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
19
|
+
// const stateDevtools = SharedOptions.DEVTOOLS?.state;
|
|
20
|
+
//
|
|
21
|
+
// if (stateDevtools) {
|
|
22
|
+
// const key = `${this._logname}:${JSON.stringify(args)}:i=${Indexer.getIndex()}`;
|
|
23
|
+
// let devtools = stateDevtools(key, initialState);
|
|
24
|
+
//
|
|
25
|
+
// cache.spy$.subscribe((state) => {
|
|
26
|
+
// if (state === initialState) return;
|
|
27
|
+
// devtools(state);
|
|
28
|
+
// });
|
|
29
|
+
//
|
|
30
|
+
// cache.onClean$.subscribe(() => {
|
|
31
|
+
// devtools('$CLEANED' as any);
|
|
32
|
+
// });
|
|
33
|
+
// }
|
|
31
34
|
cache.onClean$.subscribe(() => {
|
|
32
35
|
this._cache.delete(args);
|
|
33
36
|
});
|
|
@@ -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,7 +20,8 @@ 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
25
|
constructor(_options: ResourceCreateOptions<D>);
|
|
26
26
|
createAgent: () => ResourceAgent<D>;
|
|
27
27
|
createRef: (args: D["Args"]) => ResourceRefInstanse<D>;
|
|
@@ -33,12 +33,6 @@ export declare class Resource<D extends ResourceDefinition> implements ResourceI
|
|
|
33
33
|
decrementLock(args: D['Args'], options?: {
|
|
34
34
|
cache?: CoreResourceQueryCache<D>;
|
|
35
35
|
}): 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
36
|
update(args: D['Args'], updateFn: (data: D['Data'], savedData: D['Data'] | null, transactions: ResourceTransaction[] | null) => {
|
|
43
37
|
data: D['Data'];
|
|
44
38
|
transactions: ResourceTransaction[] | null;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { PromiseResolver } from "../../../common/utils";
|
|
1
2
|
import { SharedOptions } from "../../../common/options/SharedOptions";
|
|
2
3
|
import { QueriesCache } from "../../../query/core/QueriesCache";
|
|
3
4
|
import { ResourceAgent } from "./ResourceAgent";
|
|
@@ -90,11 +91,67 @@ class ResourceQueryState {
|
|
|
90
91
|
};
|
|
91
92
|
}
|
|
92
93
|
}
|
|
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
|
+
}
|
|
93
147
|
export class Resource {
|
|
94
148
|
_options;
|
|
95
|
-
_queriesCache
|
|
149
|
+
_queriesCache;
|
|
150
|
+
_hooks;
|
|
96
151
|
constructor(_options) {
|
|
97
152
|
this._options = _options;
|
|
153
|
+
this._hooks = new QueryHooks(_options);
|
|
154
|
+
this._queriesCache = new QueriesCache(_options.cacheLifetime, 'Resource');
|
|
98
155
|
}
|
|
99
156
|
createAgent = () => {
|
|
100
157
|
return new ResourceAgent(this);
|
|
@@ -106,7 +163,18 @@ export class Resource {
|
|
|
106
163
|
return this._queriesCache.getQueryCache(args);
|
|
107
164
|
}
|
|
108
165
|
createQueryCache(args) {
|
|
109
|
-
|
|
166
|
+
const cache = this._queriesCache.createQueryCache(args, ResourceQueryState.create());
|
|
167
|
+
const hookResolvers = this._hooks.onCacheEntryAdded(args);
|
|
168
|
+
const spySub = cache.spy$.subscribe((state) => {
|
|
169
|
+
if (!state.isDone)
|
|
170
|
+
return;
|
|
171
|
+
hookResolvers.cacheDataLoaded();
|
|
172
|
+
spySub.unsubscribe();
|
|
173
|
+
});
|
|
174
|
+
cache.onClean$.subscribe(() => {
|
|
175
|
+
hookResolvers.cacheEntryRemoved();
|
|
176
|
+
});
|
|
177
|
+
return cache;
|
|
110
178
|
}
|
|
111
179
|
incrementLock(args, options) {
|
|
112
180
|
let cache = options?.cache ?? this.getQueryCache(args);
|
|
@@ -124,22 +192,6 @@ export class Resource {
|
|
|
124
192
|
cache.next(ResourceQueryState.decrementLock(cache.value));
|
|
125
193
|
return cache;
|
|
126
194
|
}
|
|
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
195
|
update(args, updateFn, options) {
|
|
144
196
|
let cache = options?.cache ?? this.getQueryCache(args);
|
|
145
197
|
if (!cache) {
|
|
@@ -154,23 +206,23 @@ export class Resource {
|
|
|
154
206
|
return cache;
|
|
155
207
|
}
|
|
156
208
|
initiate(args, options) {
|
|
157
|
-
let cache = options?.cache ?? this.
|
|
209
|
+
let cache = options?.cache ?? this.getQueryCache(args);
|
|
158
210
|
const state = ResourceQueryState.load(cache?.value, args);
|
|
159
211
|
if (!cache) {
|
|
160
|
-
cache = this.
|
|
161
|
-
}
|
|
162
|
-
else {
|
|
163
|
-
cache.next(state);
|
|
212
|
+
cache = this.createQueryCache(args);
|
|
164
213
|
}
|
|
214
|
+
cache.next(state);
|
|
165
215
|
let abortController = state.abortController;
|
|
166
216
|
abortController?.abort();
|
|
167
217
|
abortController = new AbortController();
|
|
168
218
|
const query = this._options.queryFn(args, { abortSignal: abortController.signal });
|
|
219
|
+
const hookResolvers = this._hooks.onQueryStarted(args);
|
|
169
220
|
query
|
|
170
221
|
.then((data) => {
|
|
171
222
|
if (abortController.signal.aborted) {
|
|
172
223
|
return;
|
|
173
224
|
}
|
|
225
|
+
hookResolvers.fulfilledSuccess(data);
|
|
174
226
|
const selectedData = this._options.select ? this._options.select(data) : data;
|
|
175
227
|
cache.next(ResourceQueryState.success(state, selectedData));
|
|
176
228
|
})
|
|
@@ -178,7 +230,8 @@ export class Resource {
|
|
|
178
230
|
if (abortController.signal.aborted) {
|
|
179
231
|
return;
|
|
180
232
|
}
|
|
181
|
-
|
|
233
|
+
hookResolvers.fulfilledError(error);
|
|
234
|
+
SharedOptions.onError?.(error); // TODO перенести в хуки
|
|
182
235
|
cache.next(ResourceQueryState.error(state, error));
|
|
183
236
|
});
|
|
184
237
|
return cache;
|
|
@@ -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 {};
|
|
@@ -13,6 +13,31 @@ 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
|
+
onCacheEntryAdded?: (args: D["Args"], tools: CacheEntryAddedTools<D>) => void;
|
|
22
|
+
onQueryStarted?: (args: D["Args"], tools: QueryStartedTools<D>) => void;
|
|
23
|
+
};
|
|
24
|
+
export type CacheEntryAddedTools<D extends ResourceDefinition> = {
|
|
25
|
+
/** Функция для ожидания загрузки данных в кеш */
|
|
26
|
+
$cacheDataLoaded: Promise<void>;
|
|
27
|
+
/** Функция для ожидания удаления кеша */
|
|
28
|
+
$cacheEntryRemoved: Promise<void>;
|
|
29
|
+
};
|
|
30
|
+
export type QueryStartedTools<D extends ResourceDefinition> = {
|
|
31
|
+
/** Функция для уведомления об успешном завершении запроса */
|
|
32
|
+
$queryFulfilled: Promise<{
|
|
33
|
+
data: D["Result"];
|
|
34
|
+
error: undefined;
|
|
35
|
+
isError: false;
|
|
36
|
+
} | {
|
|
37
|
+
data: undefined;
|
|
38
|
+
error: unknown;
|
|
39
|
+
isError: true;
|
|
40
|
+
}>;
|
|
16
41
|
};
|
|
17
42
|
/**
|
|
18
43
|
* Определение типов ресурса
|
|
@@ -45,8 +70,8 @@ export type ResourceAgentInstance<D extends ResourceDefinition> = {
|
|
|
45
70
|
state$: ReadableSignalLike<ResourceQueryState<D>>;
|
|
46
71
|
/** Инициирует запрос с указанными аргументами */
|
|
47
72
|
initiate(args: D["Args"], force?: boolean): void;
|
|
48
|
-
/**
|
|
49
|
-
|
|
73
|
+
/** Завершает работу агента, позволяя освободить ресурсы */
|
|
74
|
+
complete(): void;
|
|
50
75
|
};
|
|
51
76
|
/**
|
|
52
77
|
* Состояние запроса ресурса
|
|
@@ -93,12 +118,6 @@ export type ResourceRefInstanse<D extends ResourceDefinition> = {
|
|
|
93
118
|
unlock: () => void;
|
|
94
119
|
};
|
|
95
120
|
unlockOne(): void;
|
|
96
|
-
/**
|
|
97
|
-
* @deprecated
|
|
98
|
-
*/
|
|
99
|
-
update(updateFn: (data: D['Data']) => D['Data']): {
|
|
100
|
-
rollback: () => void;
|
|
101
|
-
};
|
|
102
121
|
patch(patchFn: (data: D['Data']) => void): ResourceTransaction | null;
|
|
103
122
|
invalidate(): void;
|
|
104
123
|
create(data: D['Data']): void;
|
|
@@ -11,10 +11,10 @@ export class Effect {
|
|
|
11
11
|
* Выполняет функцию в tracked-контексте, подписываясь на Tracker.
|
|
12
12
|
*/
|
|
13
13
|
_runInTrackedContext(effectFn, isAsyncRun = false) {
|
|
14
|
-
|
|
14
|
+
let prevSubscriptions;
|
|
15
15
|
if (!isAsyncRun) {
|
|
16
16
|
this._rang = 0;
|
|
17
|
-
this._subscriptions
|
|
17
|
+
prevSubscriptions = this._subscriptions;
|
|
18
18
|
this._subscriptions = [];
|
|
19
19
|
}
|
|
20
20
|
let isTrackedContext = true;
|
|
@@ -43,6 +43,7 @@ export class Effect {
|
|
|
43
43
|
trackerSub.unsubscribe();
|
|
44
44
|
isTrackedContext = false;
|
|
45
45
|
scheduler = Batcher.scheduler(this._rang);
|
|
46
|
+
prevSubscriptions?.forEach((sub) => sub.unsubscribe());
|
|
46
47
|
}
|
|
47
48
|
unsubscribe() {
|
|
48
49
|
this.closed = true;
|
|
@@ -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);
|