@fozy-labs/rx-toolkit 0.4.4 → 0.4.5

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