@fozy-labs/rx-toolkit 0.4.4 → 0.4.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE CHANGED
@@ -1,6 +1,6 @@
1
1
  MIT License
2
2
 
3
- Copyright (c) 2025 fozykit
3
+ Copyright (c) 2025 Vladimir Panev
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
@@ -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,54 @@ export class Operation {
95
119
  });
96
120
  }
97
121
  });
98
- const query = this._options.queryFn(args);
99
122
  query
100
123
  .then((result) => {
101
- 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
+ if (link.invalidate) {
141
+ ref.invalidate();
142
+ }
143
+ state.patch?.commit();
144
+ });
145
+ hookResolvers.fulfilledSuccess(data);
146
+ /**
147
+ * Обновляем связанные ресурсы
148
+ */
149
+ linksMeta.forEach(({ state }) => {
150
+ state.unlocker?.unlock();
151
+ });
117
152
  });
118
153
  })
119
154
  .catch((error) => {
120
- 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();
155
+ Batcher.batch(() => {
156
+ cache.next(OperationQueryState.error(state, error));
157
+ /**
158
+ * Обновляем связанные ресурсы
159
+ */
160
+ linksMeta.forEach(({ state }) => {
161
+ state.patch?.abort();
162
+ });
163
+ hookResolvers.fulfilledError(error);
164
+ /**
165
+ * Обновляем связанные ресурсы
166
+ */
167
+ linksMeta.forEach(({ state }) => {
168
+ state.unlocker?.unlock();
169
+ });
135
170
  });
136
171
  });
137
172
  return cache;
@@ -1,9 +1,8 @@
1
1
  import { ReactiveCache } from "../../query/lib/ReactiveCache";
2
2
  export declare class QueriesCache<KEY, VALUE> {
3
3
  private _cacheLifeTime;
4
- private _logname;
5
4
  private readonly _cache;
6
- constructor(_cacheLifeTime?: number | false, _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>;
@@ -40,6 +41,9 @@ export declare class Resource<D extends ResourceDefinition> implements ResourceI
40
41
  }, options?: {
41
42
  cache?: CoreResourceQueryCache<D>;
42
43
  }): CoreResourceQueryCache<D> | null;
44
+ createWithData(args: D['Args'], data: D['Data'], options?: {
45
+ cache?: CoreResourceQueryCache<D>;
46
+ }): CoreResourceQueryCache<D>;
43
47
  initiate(args: D['Args'], options?: {
44
48
  cache?: CoreResourceQueryCache<D>;
45
49
  }): CoreResourceQueryCache<D>;
@@ -1,6 +1,5 @@
1
- import { 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 {
@@ -25,6 +24,7 @@ class ResourceQueryState {
25
24
  static load(state = ResourceQueryState.create(), args) {
26
25
  return {
27
26
  ...state,
27
+ abortController: new AbortController(),
28
28
  args: args,
29
29
  isLoading: !state.isDone,
30
30
  isReloading: state.isDone,
@@ -34,6 +34,7 @@ class ResourceQueryState {
34
34
  static success(state, data) {
35
35
  return {
36
36
  ...state,
37
+ abortController: null,
37
38
  savedData: null,
38
39
  transactions: null,
39
40
  data,
@@ -48,6 +49,7 @@ class ResourceQueryState {
48
49
  static error(state, error) {
49
50
  return {
50
51
  ...state,
52
+ abortController: null,
51
53
  isLoading: false,
52
54
  isReloading: false,
53
55
  isDone: true,
@@ -80,78 +82,34 @@ class ResourceQueryState {
80
82
  data
81
83
  };
82
84
  }
83
- /**
84
- * @deprecated
85
- */
86
- static setData(state, data) {
85
+ static createWithData(data, args) {
87
86
  return {
88
- ...state,
87
+ savedData: null,
89
88
  transactions: null,
90
- data
89
+ data,
90
+ isLoading: false,
91
+ isReloading: false,
92
+ isDone: true,
93
+ isSuccess: true,
94
+ isError: false,
95
+ error: null,
96
+ abortController: null,
97
+ args,
98
+ isInitiated: false,
99
+ isLocked: false,
100
+ lockCount: 0
91
101
  };
92
102
  }
93
103
  }
94
- // TODO вынести и унифицировать; как-то организовать глобальные хуки и devtools через хуки
95
- class QueryHooks {
96
- _options;
97
- onCacheEntryAddedListeners = [];
98
- onQueryStartedListeners = [];
99
- constructor(_options) {
100
- this._options = _options;
101
- if (_options?.onCacheEntryAdded) {
102
- this.onCacheEntryAddedListeners.push(_options.onCacheEntryAdded);
103
- }
104
- if (_options?.onQueryStarted) {
105
- this.onQueryStartedListeners.push(_options.onQueryStarted);
106
- }
107
- }
108
- onCacheEntryAdded = (args) => {
109
- const cacheDataLoadedResolver = new PromiseResolver();
110
- const cacheEntryRemovedResolver = new PromiseResolver();
111
- this.onCacheEntryAddedListeners.forEach((listener) => {
112
- listener(args, {
113
- $cacheDataLoaded: cacheDataLoadedResolver.promise,
114
- $cacheEntryRemoved: cacheEntryRemovedResolver.promise,
115
- });
116
- });
117
- return {
118
- cacheDataLoaded: () => cacheDataLoadedResolver.resolve(),
119
- cacheEntryRemoved: () => cacheEntryRemovedResolver.resolve(),
120
- };
121
- };
122
- onQueryStarted = (args) => {
123
- const queryFulfilledResolver = new PromiseResolver();
124
- this.onQueryStartedListeners.forEach((listener) => {
125
- listener(args, {
126
- $queryFulfilled: queryFulfilledResolver.promise
127
- });
128
- });
129
- return {
130
- fulfilledSuccess: (data) => {
131
- queryFulfilledResolver.resolve({
132
- data,
133
- error: undefined,
134
- isError: false
135
- });
136
- },
137
- fulfilledError: (error) => {
138
- queryFulfilledResolver.resolve({
139
- data: undefined,
140
- error,
141
- isError: true
142
- });
143
- }
144
- };
145
- };
146
- }
147
104
  export class Resource {
148
105
  _options;
149
106
  _queriesCache;
150
107
  _hooks;
108
+ _DEFAULT_CACHE_LIFETIME = 60_000;
151
109
  constructor(_options) {
152
110
  this._options = _options;
153
- this._hooks = new QueryHooks(_options);
154
- this._queriesCache = new QueriesCache(_options.cacheLifetime, 'Resource');
111
+ this._hooks = new QueriesLifetimeHooks(_options, 'Resource');
112
+ this._queriesCache = new QueriesCache(_options.cacheLifetime ?? this._DEFAULT_CACHE_LIFETIME);
155
113
  }
156
114
  createAgent = () => {
157
115
  return new ResourceAgent(this);
@@ -162,8 +120,8 @@ export class Resource {
162
120
  getQueryCache(args) {
163
121
  return this._queriesCache.getQueryCache(args);
164
122
  }
165
- createQueryCache(args) {
166
- const cache = this._queriesCache.createQueryCache(args, ResourceQueryState.create());
123
+ createQueryCache(args, state = ResourceQueryState.create()) {
124
+ const cache = this._queriesCache.createQueryCache(args, state);
167
125
  const hookResolvers = this._hooks.onCacheEntryAdded(args);
168
126
  const spySub = cache.spy$.subscribe((state) => {
169
127
  if (!state.isDone)
@@ -171,6 +129,9 @@ export class Resource {
171
129
  hookResolvers.cacheDataLoaded();
172
130
  spySub.unsubscribe();
173
131
  });
132
+ cache.spy$.subscribe((data) => {
133
+ hookResolvers.dataChanged$.next(data);
134
+ });
174
135
  cache.onClean$.subscribe(() => {
175
136
  hookResolvers.cacheEntryRemoved();
176
137
  });
@@ -205,34 +166,48 @@ export class Resource {
205
166
  cache.next(ResourceQueryState.update(cache.value, data, savedData, transactions));
206
167
  return cache;
207
168
  }
169
+ createWithData(args, data, options) {
170
+ let cache = options?.cache ?? this.getQueryCache(args);
171
+ const state = ResourceQueryState.createWithData(data, args);
172
+ if (!cache) {
173
+ cache = this.createQueryCache(args, state);
174
+ // Только обновляем кэш новыми данными, если он еще не был инициализирован.
175
+ // Это предотвращает перезапись уже инициализированного кэша.
176
+ }
177
+ else if (!cache.value.isInitiated) {
178
+ cache.next(state);
179
+ }
180
+ return cache;
181
+ }
208
182
  initiate(args, options) {
209
183
  let cache = options?.cache ?? this.getQueryCache(args);
184
+ const prevAbortController = cache?.value.abortController ?? null;
210
185
  const state = ResourceQueryState.load(cache?.value, args);
211
186
  if (!cache) {
212
- cache = this.createQueryCache(args);
187
+ cache = this.createQueryCache(args, state);
188
+ }
189
+ else {
190
+ cache.next(state);
213
191
  }
214
- cache.next(state);
215
- let abortController = state.abortController;
216
- abortController?.abort();
217
- abortController = new AbortController();
192
+ prevAbortController?.abort();
193
+ const abortController = state.abortController;
218
194
  const query = this._options.queryFn(args, { abortSignal: abortController.signal });
219
195
  const hookResolvers = this._hooks.onQueryStarted(args);
220
196
  query
221
- .then((data) => {
197
+ .then((result) => {
222
198
  if (abortController.signal.aborted) {
223
199
  return;
224
200
  }
201
+ const data = this._options.select ? this._options.select(result) : result;
202
+ cache.next(ResourceQueryState.success(state, data));
225
203
  hookResolvers.fulfilledSuccess(data);
226
- const selectedData = this._options.select ? this._options.select(data) : data;
227
- cache.next(ResourceQueryState.success(state, selectedData));
228
204
  })
229
205
  .catch((error) => {
230
206
  if (abortController.signal.aborted) {
231
207
  return;
232
208
  }
233
- hookResolvers.fulfilledError(error);
234
- SharedOptions.onError?.(error); // TODO перенести в хуки
235
209
  cache.next(ResourceQueryState.error(state, error));
210
+ hookResolvers.fulfilledError(error);
236
211
  });
237
212
  return cache;
238
213
  }
@@ -128,9 +128,9 @@ export class ResourceRef {
128
128
  return transaction;
129
129
  }
130
130
  create(data) {
131
- throw new Error("Method not implemented.");
131
+ this._cacheItem = this._resource.createWithData(this._args, data, { cache: this._cacheItem ?? undefined });
132
132
  }
133
133
  invalidate() {
134
- throw new Error("Method not implemented.");
134
+ this._cacheItem = this._resource.initiate(this._args, { cache: this._cacheItem ?? undefined });
135
135
  }
136
136
  }
@@ -1,5 +1,5 @@
1
1
  import { SKIP } from "../../query/SKIP_TOKEN";
2
2
  import type { Prettify, ResourceDefinition, ResourceInstance, ResourceRefInstanse } from "../../query/types";
3
- type Result<D extends ResourceDefinition> = Prettify<ResourceRefInstanse<D>> | null;
3
+ type Result<D extends ResourceDefinition> = Prettify<ResourceRefInstanse<D>>;
4
4
  export declare function useResourceRef<D extends ResourceDefinition>(res: ResourceInstance<D>, ...argss: D['Args'] extends void ? [] | [typeof SKIP] : [D['Args'] | typeof SKIP]): Result<D>;
5
5
  export {};
@@ -1,5 +1,5 @@
1
1
  import { ReadableSignalLike } from "../../signals";
2
- import { FallbackOnNever } from "./shared.types";
2
+ import { FallbackOnNever, OnCacheEntryAdded, OnQueryStarted } from "./shared.types";
3
3
  import { ResourceDefinition, ResourceInstance } from "./Resource.types";
4
4
  /**
5
5
  * Функция создания операции
@@ -15,6 +15,24 @@ export type OperationCreateOptions<D extends OperationDefinition> = {
15
15
  queryFn: (args: D["Args"]) => Promise<D["Result"]>;
16
16
  /** Связанные ресурсы */
17
17
  link?: (link: <RD extends ResourceDefinition>(options: LinkOptions<D, RD>) => void) => void;
18
+ /**
19
+ * Время жизни кеша в миллисекундах. По умолчанию 1_000 (1 секунда).
20
+ * Если указано false - кеш не удаляется автоматически.
21
+ */
22
+ cacheLifetime?: number | false;
23
+ /**
24
+ * Хук, вызываемый при добавлении нового элемента в кеш.
25
+ * Также позволяет отследить:
26
+ * - когда данные были загружены (впервые)
27
+ * - когда элемент был удален из кеша
28
+ */
29
+ onCacheEntryAdded?: OnCacheEntryAdded<D["Args"], D["Data"]>;
30
+ /**
31
+ * Хук, вызываемый при старте запроса.
32
+ * Также позволяет отследить:
33
+ * - завершение запроса с результатом или ошибкой
34
+ */
35
+ onQueryStarted?: OnQueryStarted<D["Args"], D["Result"]>;
18
36
  };
19
37
  /**
20
38
  * Настройки связи операции с ресурсом
@@ -1,6 +1,6 @@
1
1
  import { Patch as ImmerPatch } from "immer";
2
2
  import { ReadableSignalLike } from "../../signals";
3
- import { FallbackOnNever } from "./shared.types";
3
+ import { FallbackOnNever, OnCacheEntryAdded, OnQueryStarted } from "./shared.types";
4
4
  /**
5
5
  * Функция создания ресурса
6
6
  */
@@ -18,26 +18,19 @@ export type ResourceCreateOptions<D extends ResourceDefinition> = {
18
18
  * Если указано false - кеш не удаляется автоматически.
19
19
  */
20
20
  cacheLifetime?: number | false;
21
- 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.6",
4
4
  "main": "./dist/index.js",
5
5
  "types": "./dist/index.d.ts",
6
6
  "type": "module",