@fozy-labs/rx-toolkit 0.4.3 → 0.4.5

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