@fozy-labs/rx-toolkit 0.4.15 → 0.4.16

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 (32) hide show
  1. package/dist/common/devtools/reduxDevtools.js +0 -1
  2. package/dist/common/options/SharedOptions.d.ts +3 -1
  3. package/dist/common/options/SharedOptions.js +2 -0
  4. package/dist/common/utils/deepEqual.d.ts +1 -0
  5. package/dist/common/utils/deepEqual.js +21 -0
  6. package/dist/common/utils/index.d.ts +2 -0
  7. package/dist/common/utils/index.js +2 -0
  8. package/dist/index.d.ts +2 -0
  9. package/dist/index.js +2 -0
  10. package/dist/query/api/cleanAllResources.d.ts +1 -0
  11. package/dist/query/api/cleanAllResources.js +4 -0
  12. package/dist/query/core/CleanAllResourcesSignal.d.ts +6 -0
  13. package/dist/query/core/CleanAllResourcesSignal.js +11 -0
  14. package/dist/query/core/QueriesCache.d.ts +3 -1
  15. package/dist/query/core/QueriesCache.js +11 -17
  16. package/dist/query/core/Resource/Resource.d.ts +2 -1
  17. package/dist/query/core/Resource/Resource.js +14 -4
  18. package/dist/query/core/Resource/ResourceAgent.d.ts +3 -1
  19. package/dist/query/core/Resource/ResourceAgent.js +39 -20
  20. package/dist/query/index.d.ts +1 -0
  21. package/dist/query/index.js +1 -0
  22. package/dist/query/lib/IndirectMap.d.ts +1 -0
  23. package/dist/query/lib/IndirectMap.js +4 -1
  24. package/dist/query/lib/ReactiveCache.d.ts +1 -0
  25. package/dist/query/lib/ReactiveCache.js +4 -0
  26. package/dist/query/react/useResourceAgent.js +1 -2
  27. package/dist/query/types/Resource.types.d.ts +6 -0
  28. package/dist/signals/signals/Effect.d.ts +2 -1
  29. package/dist/signals/signals/Effect.js +12 -1
  30. package/package.json +1 -1
  31. /package/dist/{query/lib → common/utils}/shallowEqual.d.ts +0 -0
  32. /package/dist/{query/lib → common/utils}/shallowEqual.js +0 -0
@@ -76,6 +76,5 @@ function deleteState(keys, state) {
76
76
  return true;
77
77
  };
78
78
  deleteRecursive(acc, keys, 0);
79
- console.log('deleteState', keys, state, acc);
80
79
  return acc;
81
80
  }
@@ -1,8 +1,10 @@
1
- import { DevtoolsLike } from "../../common/devtools";
2
1
  import { Observable } from "rxjs";
2
+ import { DevtoolsLike } from "../../common/devtools";
3
+ import { shallowEqual } from "../../common/utils";
3
4
  export declare class SharedOptions {
4
5
  static DEVTOOLS: DevtoolsLike | null;
5
6
  static onQueryError: ((error: unknown) => void) | null;
6
7
  static getScopeName: (() => string | null) | null;
7
8
  static getScopeDestroyed$: (() => Observable<void> | null) | null;
9
+ static defaultCompareArgs: typeof shallowEqual;
8
10
  }
@@ -1,6 +1,8 @@
1
+ import { shallowEqual } from "../../common/utils";
1
2
  export class SharedOptions {
2
3
  static DEVTOOLS = null;
3
4
  static onQueryError = null;
4
5
  static getScopeName = null;
5
6
  static getScopeDestroyed$ = null;
7
+ static defaultCompareArgs = shallowEqual;
6
8
  }
@@ -0,0 +1 @@
1
+ export declare function deepEqual(a: unknown, b: unknown): boolean;
@@ -0,0 +1,21 @@
1
+ export function deepEqual(a, b) {
2
+ if (a === b) {
3
+ return true;
4
+ }
5
+ if (typeof a !== "object" || a === null || typeof b !== "object" || b === null) {
6
+ return false;
7
+ }
8
+ const keysA = Object.keys(a);
9
+ const keysB = Object.keys(b);
10
+ if (keysA.length !== keysB.length) {
11
+ return false;
12
+ }
13
+ for (let i = 0; i < keysA.length; i++) {
14
+ const key = keysA[i];
15
+ // @ts-ignore
16
+ if (!Object.prototype.hasOwnProperty.call(b, key) || !deepEqual(a[key], b[key])) {
17
+ return false;
18
+ }
19
+ }
20
+ return true;
21
+ }
@@ -1 +1,3 @@
1
1
  export * from './PromiseResolver';
2
+ export * from './deepEqual';
3
+ export * from './shallowEqual';
@@ -1 +1,3 @@
1
1
  export * from './PromiseResolver';
2
+ export * from './deepEqual';
3
+ export * from './shallowEqual';
package/dist/index.d.ts CHANGED
@@ -1,5 +1,7 @@
1
1
  export * from './common/devtools';
2
2
  export * from './common/options';
3
3
  export * from './common/react';
4
+ export * from './common/utils/deepEqual';
5
+ export * from './common/utils/shallowEqual';
4
6
  export * from './query';
5
7
  export * from './signals';
package/dist/index.js CHANGED
@@ -1,5 +1,7 @@
1
1
  export * from './common/devtools';
2
2
  export * from './common/options';
3
3
  export * from './common/react';
4
+ export * from './common/utils/deepEqual';
5
+ export * from './common/utils/shallowEqual';
4
6
  export * from './query';
5
7
  export * from './signals';
@@ -0,0 +1 @@
1
+ export declare function cleanAllResources(): void;
@@ -0,0 +1,4 @@
1
+ import { CleanAllResourcesSignal } from "../core/CleanAllResourcesSignal";
2
+ export function cleanAllResources() {
3
+ CleanAllResourcesSignal.clean();
4
+ }
@@ -0,0 +1,6 @@
1
+ import { Subject } from "rxjs";
2
+ export declare class CleanAllResourcesSignal {
3
+ private static subject$;
4
+ static clean$: Subject<void>;
5
+ static clean(): void;
6
+ }
@@ -0,0 +1,11 @@
1
+ import { Subject } from "rxjs";
2
+ import { Batcher } from "../../signals";
3
+ export class CleanAllResourcesSignal {
4
+ static subject$ = new Subject();
5
+ static clean$ = CleanAllResourcesSignal.subject$;
6
+ static clean() {
7
+ Batcher.batch(() => {
8
+ CleanAllResourcesSignal.subject$.next();
9
+ });
10
+ }
11
+ }
@@ -1,8 +1,10 @@
1
1
  import { ReactiveCache } from "../../query/lib/ReactiveCache";
2
+ import { shallowEqual } from "../../common/utils";
2
3
  export declare class QueriesCache<KEY, VALUE> {
3
4
  private _cacheLifeTime;
4
5
  private readonly _cache;
5
- constructor(_cacheLifeTime?: number | false);
6
+ constructor(_cacheLifeTime?: number | false, compareArgsFn?: typeof shallowEqual);
6
7
  getQueryCache(args: KEY): ReactiveCache<VALUE> | undefined;
7
8
  createQueryCache(args: KEY, initialState: VALUE): ReactiveCache<VALUE>;
9
+ clear(): void;
8
10
  }
@@ -1,10 +1,12 @@
1
1
  import { IndirectMap } from "../../query/lib/IndirectMap";
2
2
  import { ReactiveCache } from "../../query/lib/ReactiveCache";
3
+ import { shallowEqual } from "../../common/utils";
3
4
  export class QueriesCache {
4
5
  _cacheLifeTime;
5
- _cache = new IndirectMap();
6
- constructor(_cacheLifeTime = 60_000) {
6
+ _cache;
7
+ constructor(_cacheLifeTime = 60_000, compareArgsFn = shallowEqual) {
7
8
  this._cacheLifeTime = _cacheLifeTime;
9
+ this._cache = new IndirectMap(compareArgsFn);
8
10
  }
9
11
  getQueryCache(args) {
10
12
  return this._cache.get(args);
@@ -14,25 +16,17 @@ export class QueriesCache {
14
16
  initialState,
15
17
  cacheLifeTime: this._cacheLifeTime,
16
18
  });
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
- // }
32
19
  cache.onClean$.subscribe(() => {
33
20
  this._cache.delete(args);
34
21
  });
35
22
  this._cache.set(args, cache);
36
23
  return cache;
37
24
  }
25
+ clear() {
26
+ // Делаем именно так, тк при очистке могут синхронно добавляться новые кеши
27
+ const values = Array.from(this._cache.values);
28
+ values.forEach(c => {
29
+ c.complete();
30
+ });
31
+ }
38
32
  }
@@ -4,7 +4,7 @@ import { ResourceAgent } from "./ResourceAgent";
4
4
  export type CoreResourceQueryState<D extends ResourceDefinition> = {
5
5
  transactions: ResourceTransaction[] | null;
6
6
  abortController: AbortController | null;
7
- args: D['Args'] | null;
7
+ args: D['Args'];
8
8
  savedData: D['Data'] | null;
9
9
  data: D['Data'] | null;
10
10
  error: unknown | null;
@@ -47,4 +47,5 @@ export declare class Resource<D extends ResourceDefinition> implements ResourceI
47
47
  initiate(args: D['Args'], options?: {
48
48
  cache?: CoreResourceQueryCache<D>;
49
49
  }): CoreResourceQueryCache<D>;
50
+ compareArgs(args1: D['Args'], args2: D['Args']): boolean;
50
51
  }
@@ -1,14 +1,16 @@
1
1
  import { QueriesCache } from "../QueriesCache";
2
2
  import { QueriesLifetimeHooks } from "../QueriesLifetimeHooks";
3
+ import { CleanAllResourcesSignal } from "../CleanAllResourcesSignal";
3
4
  import { ResourceAgent } from "./ResourceAgent";
4
5
  import { ResourceRef } from "./ResourceRef";
6
+ import { SharedOptions } from "../../../common/options/SharedOptions";
5
7
  class ResourceQueryState {
6
- static create() {
8
+ static create(args) {
7
9
  return {
8
10
  transactions: null,
9
11
  savedData: null,
10
12
  abortController: null,
11
- args: null,
13
+ args,
12
14
  data: null,
13
15
  error: null,
14
16
  isError: false,
@@ -21,7 +23,8 @@ class ResourceQueryState {
21
23
  lockCount: 0
22
24
  };
23
25
  }
24
- static load(state = ResourceQueryState.create(), args) {
26
+ static load(state, args) {
27
+ state = state ?? ResourceQueryState.create(args);
25
28
  return {
26
29
  ...state,
27
30
  abortController: new AbortController(),
@@ -114,6 +117,9 @@ export class Resource {
114
117
  devtoolsName: _options.devtoolsName,
115
118
  });
116
119
  this._queriesCache = new QueriesCache(_options.cacheLifetime ?? this._DEFAULT_CACHE_LIFETIME);
120
+ CleanAllResourcesSignal.clean$.subscribe(() => {
121
+ this._queriesCache.clear();
122
+ });
117
123
  }
118
124
  createAgent = () => {
119
125
  return new ResourceAgent(this);
@@ -124,7 +130,7 @@ export class Resource {
124
130
  getQueryCache(args) {
125
131
  return this._queriesCache.getQueryCache(args);
126
132
  }
127
- createQueryCache(args, state = ResourceQueryState.create()) {
133
+ createQueryCache(args, state = ResourceQueryState.create(args)) {
128
134
  const cache = this._queriesCache.createQueryCache(args, state);
129
135
  const hookResolvers = this._hooks.onCacheEntryAdded(args);
130
136
  const spySub = cache.spy$.subscribe((state) => {
@@ -215,4 +221,8 @@ export class Resource {
215
221
  });
216
222
  return cache;
217
223
  }
224
+ compareArgs(args1, args2) {
225
+ const compareFn = this._options.compareArgsFn ?? SharedOptions.defaultCompareArgs;
226
+ return compareFn(args1, args2);
227
+ }
218
228
  }
@@ -4,6 +4,7 @@ import type { Resource } from "./Resource";
4
4
  export declare class ResourceAgent<D extends ResourceDefinition> implements ResourceAgentInstance<D> {
5
5
  private _resource;
6
6
  private _resources$;
7
+ private _effect;
7
8
  state$: Computed<{
8
9
  isInitiated: boolean;
9
10
  isLoading: boolean;
@@ -30,7 +31,8 @@ export declare class ResourceAgent<D extends ResourceDefinition> implements Reso
30
31
  args: NonNullable<D["Args"]> | undefined;
31
32
  }>;
32
33
  constructor(_resource: Resource<D>);
33
- private _next;
34
34
  initiate(args: D["Args"], force?: boolean): void;
35
+ compareArgs(args: D["Args"], otherArgs: D["Args"]): boolean;
35
36
  complete(): void;
37
+ private _next;
36
38
  }
@@ -1,10 +1,25 @@
1
- import { Computed, Signal } from "../../../signals";
1
+ import { Computed, Effect, Signal } from "../../../signals";
2
2
  export class ResourceAgent {
3
3
  _resource;
4
4
  _resources$ = new Signal({
5
5
  previous$: null,
6
6
  current$: null,
7
7
  }, { isDisabled: true });
8
+ _effect = new Effect(() => {
9
+ const current$ = this._resources$.value.current$;
10
+ const args = current$?.value.args;
11
+ // Если ресурс который мы слушаем очистился, то инициируем его заново с теми же аргументами
12
+ const sub = current$?.onClean$.subscribe(() => {
13
+ this._resources$.next({
14
+ previous$: null,
15
+ current$: null,
16
+ });
17
+ this.initiate(args);
18
+ });
19
+ return () => {
20
+ sub?.unsubscribe();
21
+ };
22
+ });
8
23
  state$ = new Computed(() => {
9
24
  const resources = this._resources$.value;
10
25
  let prevState;
@@ -47,6 +62,29 @@ export class ResourceAgent {
47
62
  constructor(_resource) {
48
63
  this._resource = _resource;
49
64
  }
65
+ initiate(args, force = false) {
66
+ const current = this._resources$.value.current$;
67
+ const cache = this._resource.getQueryCache(args);
68
+ if (!cache) {
69
+ const newCache = this._resource.initiate(args);
70
+ this._next(newCache);
71
+ return;
72
+ }
73
+ if (force || !(cache.value.isDone || cache.value.isLoading)) {
74
+ this._resource.initiate(args, { cache });
75
+ }
76
+ if (current !== cache) {
77
+ this._next(cache);
78
+ }
79
+ }
80
+ compareArgs(args, otherArgs) {
81
+ return this._resource.compareArgs(args, otherArgs);
82
+ }
83
+ complete() {
84
+ this._effect.complete();
85
+ this.state$.complete();
86
+ this._resources$.complete();
87
+ }
50
88
  _next(newCache) {
51
89
  const { previous$, current$ } = this._resources$.value;
52
90
  if (!current$) {
@@ -68,23 +106,4 @@ export class ResourceAgent {
68
106
  current$: newCache,
69
107
  });
70
108
  }
71
- initiate(args, force = false) {
72
- const current = this._resources$.value.current$;
73
- const cache = this._resource.getQueryCache(args);
74
- if (!cache) {
75
- const newCache = this._resource.initiate(args);
76
- this._next(newCache);
77
- return;
78
- }
79
- if (force || !(cache.value.isDone || cache.value.isLoading)) {
80
- this._resource.initiate(args, { cache });
81
- }
82
- if (current !== cache) {
83
- this._next(cache);
84
- }
85
- }
86
- complete() {
87
- this.state$.complete();
88
- this._resources$.complete();
89
- }
90
109
  }
@@ -1,5 +1,6 @@
1
1
  export * from './api/createResource';
2
2
  export * from './api/createOperation';
3
+ export * from './api/cleanAllResources';
3
4
  export * from './SKIP_TOKEN';
4
5
  export * from './react/useResourceAgent';
5
6
  export * from './react/useResourceRef';
@@ -1,5 +1,6 @@
1
1
  export * from './api/createResource';
2
2
  export * from './api/createOperation';
3
+ export * from './api/cleanAllResources';
3
4
  export * from './SKIP_TOKEN';
4
5
  export * from './react/useResourceAgent';
5
6
  export * from './react/useResourceRef';
@@ -14,5 +14,6 @@ export declare class IndirectMap<KEY, VALUE> {
14
14
  */
15
15
  delete(key: KEY): void;
16
16
  has(key: KEY): boolean;
17
+ get values(): IterableIterator<VALUE>;
17
18
  }
18
19
  export {};
@@ -1,4 +1,4 @@
1
- import { shallowEqual } from "./shallowEqual";
1
+ import { shallowEqual } from "../../common/utils";
2
2
  export class IndirectMap {
3
3
  _compareObjectsFn;
4
4
  _compareCache = new WeakMap();
@@ -82,4 +82,7 @@ export class IndirectMap {
82
82
  }
83
83
  return has;
84
84
  }
85
+ get values() {
86
+ return this._map.values();
87
+ }
85
88
  }
@@ -37,6 +37,7 @@ export declare class ReactiveCache<VALUE> {
37
37
  * Subject, уведомляющий об очистке кэша.
38
38
  */
39
39
  onClean$: Subject<VALUE>;
40
+ closed: boolean;
40
41
  /**
41
42
  * Создает новый экземпляр `ReactiveCacheItem`.
42
43
  *
@@ -24,6 +24,7 @@ export class ReactiveCache {
24
24
  * Subject, уведомляющий об очистке кэша.
25
25
  */
26
26
  onClean$ = new Subject();
27
+ closed = false;
27
28
  /**
28
29
  * Создает новый экземпляр `ReactiveCacheItem`.
29
30
  *
@@ -69,6 +70,9 @@ export class ReactiveCache {
69
70
  * Завершает работу кэша, закрывая все потоки и уведомляя об очистке.
70
71
  */
71
72
  complete() {
73
+ if (this.closed)
74
+ return;
75
+ this.closed = true;
72
76
  this._state$.complete();
73
77
  this.onClean$.next(this._state$.value);
74
78
  this.onClean$.complete();
@@ -1,7 +1,6 @@
1
1
  import React from "react";
2
2
  import { useConstant } from "../../common/react";
3
3
  import { useSignal } from "../../signals/react";
4
- import { shallowEqual } from "../../query/lib/shallowEqual";
5
4
  import { SKIP } from "../../query/SKIP_TOKEN";
6
5
  export function useResourceAgent(res, ...argss) {
7
6
  const args = (argss[0] === SKIP ? SKIP : argss[0]);
@@ -13,7 +12,7 @@ export function useResourceAgent(res, ...argss) {
13
12
  }
14
13
  return agent;
15
14
  });
16
- if (args !== SKIP && !shallowEqual(args, prevArgsRef.current)) {
15
+ if (args !== SKIP && !agent.compareArgs(args, prevArgsRef.current)) {
17
16
  prevArgsRef.current = args;
18
17
  agent.initiate(args);
19
18
  }
@@ -35,6 +35,10 @@ export type ResourceCreateOptions<D extends ResourceDefinition> = {
35
35
  * Настройка отображения в devtools
36
36
  */
37
37
  devtoolsName?: string | false;
38
+ /**
39
+ * Сравнение аргументов между собой
40
+ */
41
+ compareArgsFn?: (args1: D["Args"], args2: D["Args"]) => boolean;
38
42
  };
39
43
  /**
40
44
  * Определение типов ресурса
@@ -69,6 +73,8 @@ export type ResourceAgentInstance<D extends ResourceDefinition> = {
69
73
  initiate(args: D["Args"], force?: boolean): void;
70
74
  /** Завершает работу агента, позволяя освободить ресурсы */
71
75
  complete(): void;
76
+ /** Сравнивает аргументы между собой */
77
+ compareArgs(args1: D["Args"], args2: D["Args"]): unknown;
72
78
  };
73
79
  /**
74
80
  * Состояние запроса ресурса
@@ -2,10 +2,11 @@ import { SubscriptionLike } from "rxjs";
2
2
  export declare class Effect implements SubscriptionLike {
3
3
  private _onComplete?;
4
4
  private _subscriptions;
5
+ private _teardown?;
5
6
  protected readonly _scopeDestroyedSub: import("rxjs").Subscription | undefined;
6
7
  closed: boolean;
7
8
  _rang: number;
8
- constructor(effectFn: (ctx: (fn: () => void) => void) => void, _onComplete?: (() => void) | undefined);
9
+ constructor(effectFn: (ctx: (fn: () => void) => void) => void | (() => void), _onComplete?: (() => void) | undefined);
9
10
  /**
10
11
  * Выполняет функцию в tracked-контексте, подписываясь на Tracker.
11
12
  */
@@ -3,6 +3,7 @@ import { SharedOptions } from "../../common/options/SharedOptions";
3
3
  export class Effect {
4
4
  _onComplete;
5
5
  _subscriptions = [];
6
+ _teardown;
6
7
  _scopeDestroyedSub = SharedOptions.getScopeDestroyed$?.()?.subscribe(() => {
7
8
  this.complete();
8
9
  });
@@ -18,6 +19,9 @@ export class Effect {
18
19
  _runInTrackedContext(effectFn, isAsyncRun = false) {
19
20
  let prevSubscriptions;
20
21
  if (!isAsyncRun) {
22
+ // Вызываем teardown перед перезапуском эффекта
23
+ this._teardown?.();
24
+ this._teardown = undefined;
21
25
  this._rang = 0;
22
26
  prevSubscriptions = this._subscriptions;
23
27
  this._subscriptions = [];
@@ -51,9 +55,13 @@ export class Effect {
51
55
  this.complete();
52
56
  },
53
57
  });
54
- effectFn((fn) => {
58
+ const teardown = effectFn((fn) => {
55
59
  this._runInTrackedContext(fn, true);
56
60
  });
61
+ // Сохраняем teardown функцию, если она была возвращена
62
+ if (typeof teardown === 'function') {
63
+ this._teardown = teardown;
64
+ }
57
65
  trackerSub.unsubscribe();
58
66
  isTrackedContext = false;
59
67
  scheduler = Batcher.scheduler(this._rang);
@@ -63,6 +71,9 @@ export class Effect {
63
71
  if (this.closed)
64
72
  return;
65
73
  this.closed = true;
74
+ // Вызываем teardown перед завершением эффекта
75
+ this._teardown?.();
76
+ this._teardown = undefined;
66
77
  this._subscriptions.forEach((sub) => sub.unsubscribe());
67
78
  this._scopeDestroyedSub?.unsubscribe();
68
79
  this._onComplete?.();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fozy-labs/rx-toolkit",
3
- "version": "0.4.15",
3
+ "version": "0.4.16",
4
4
  "main": "./dist/index.js",
5
5
  "types": "./dist/index.d.ts",
6
6
  "type": "module",