@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.
@@ -1,2 +1,16 @@
1
1
  import { DevtoolsLike } from "./types";
2
- export declare function reduxDevtools(): DevtoolsLike;
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
- // @ts-ignore
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,7 @@
1
+ export declare class PromiseResolver<T> {
2
+ private _resolve;
3
+ private _reject;
4
+ promise: Promise<T>;
5
+ resolve(value: T): void;
6
+ reject(reason?: any): void;
7
+ }
@@ -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
- ref.update((draft) => {
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
- if (stateDevtools) {
20
- const key = `${this._logname}:${JSON.stringify(args)}:i=${Indexer.getIndex()}`;
21
- let devtools = stateDevtools(key, initialState);
22
- cache.spy$.subscribe((state) => {
23
- if (state === initialState)
24
- return;
25
- devtools(state);
26
- });
27
- cache.onClean$.subscribe(() => {
28
- devtools('$CLEANED');
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: QueriesCache<D["Args"], CoreResourceQueryState<D>>;
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 = new QueriesCache('Resource');
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
- return this._queriesCache.createQueryCache(args, ResourceQueryState.create());
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._queriesCache.getQueryCache(args);
209
+ let cache = options?.cache ?? this.getQueryCache(args);
158
210
  const state = ResourceQueryState.load(cache?.value, args);
159
211
  if (!cache) {
160
- cache = this._queriesCache.createQueryCache(args, state);
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
- SharedOptions.onError?.(error);
233
+ hookResolvers.fulfilledError(error);
234
+ SharedOptions.onError?.(error); // TODO перенести в хуки
182
235
  cache.next(ResourceQueryState.error(state, error));
183
236
  });
184
237
  return cache;
@@ -30,5 +30,5 @@ export declare class ResourceAgent<D extends ResourceDefinition> implements Reso
30
30
  constructor(_resource: Resource<D>);
31
31
  private _next;
32
32
  initiate(args: D["Args"], force?: boolean): void;
33
- createAgent: () => ResourceAgentInstance<D>;
33
+ complete(): void;
34
34
  }
@@ -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
- createAgent = () => {
85
- return this;
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.forEach((transaction) => {
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
- currentData = applyPatches(currentData, transaction.inversePatches);
81
- remainingTransactions.push(transaction);
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$: Observable<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 { Signal } from "../../signals";
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
- this._cacheLifeTime = options.cacheLifeTime || 60_000;
47
- this._state$ = new Signal(options.initialState, { disableDevtools: true });
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._value);
73
+ this.onClean$.next(this._state$.value);
94
74
  this.onClean$.complete();
95
75
  }
96
76
  }
@@ -1,8 +1,5 @@
1
- import { Prettify, ResourceAgentInstance, ResourceDefinition, ResourceQueryState } from "../../query/types";
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: WithAgent<D>, ...argss: D['Args'] extends void ? [] | [typeof SKIP] : [D['Args'] | typeof SKIP]): Result<D>;
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 {};
@@ -22,5 +22,8 @@ export function useResourceAgent(res, ...argss) {
22
22
  }
23
23
  agent.initiate(args);
24
24
  }, [args]);
25
+ React.useEffect(() => () => {
26
+ agent.complete();
27
+ }, []);
25
28
  return useSignal(agent.state$);
26
29
  }
@@ -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
- createAgent(): ResourceAgentInstance<D>;
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;
@@ -20,8 +20,7 @@ export class Computed extends Signal {
20
20
  this._effect = effect;
21
21
  }
22
22
  unsubscribe() {
23
- this._effect.unsubscribe();
24
- super.unsubscribe();
23
+ this.complete();
25
24
  }
26
25
  complete() {
27
26
  this._effect.unsubscribe();
@@ -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.forEach((sub) => sub.unsubscribe());
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);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fozy-labs/rx-toolkit",
3
- "version": "0.4.3",
3
+ "version": "0.4.4",
4
4
  "main": "./dist/index.js",
5
5
  "types": "./dist/index.d.ts",
6
6
  "type": "module",