@fozy-labs/rx-toolkit 0.5.0-rc.3 → 0.5.0

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/README.md CHANGED
@@ -46,7 +46,7 @@ RxToolkit решает эти проблемы, предоставляя сво
46
46
  // Описываем логику в обычном JavaScript
47
47
  const count$ = Signal.create(0);
48
48
  const doubled$ = Signal.compute(() => count$() * 2);
49
- const increment = () => count$.set(count$.peek() + 1);
49
+ const increment = () => count$.set(count$() + 1);
50
50
  ```
51
51
 
52
52
  ###### Подключаем к фреймворку
@@ -81,7 +81,7 @@ const clicker$ = fromEvent(document, 'click').pipe(
81
81
  const clickCount$ = signalize(clicker$);
82
82
  const doubled$ = Signal.compute(() => clickCount$() * 2);
83
83
 
84
- console.log(doubled$.peek()); // Всегда актуальное значение
84
+ console.log(doubled$()); // Всегда актуальное значение
85
85
 
86
86
  // Или наоборот, получаем событие из сигнала
87
87
  const on10click$ = doubled$.obs.pipe(
@@ -0,0 +1,4 @@
1
+ import { DuplicatorOptions, ResourceDuplicator, DuplicatorDefinition } from "../../query/core/Resource/ResourceDuplicator";
2
+ import { ResourceDefinition } from "../../query/types";
3
+ export declare const createResourceDuplicator: <ARGS, RESULT, SELECTED = never>(options: DuplicatorOptions<DuplicatorDefinition<ResourceDefinition<ARGS, RESULT, SELECTED>>>) => ResourceDuplicator<DuplicatorDefinition<ResourceDefinition<ARGS, RESULT, SELECTED>>>;
4
+ export type ResourceDuplicatorCreateFn<ARGS, RESULT, SELECTED = never> = (options: DuplicatorOptions<DuplicatorDefinition<ResourceDefinition<ARGS, RESULT, SELECTED>>>) => ResourceDuplicator<DuplicatorDefinition<ResourceDefinition<ARGS, RESULT, SELECTED>>>;
@@ -0,0 +1,2 @@
1
+ import { ResourceDuplicator } from "../../query/core/Resource/ResourceDuplicator";
2
+ export const createResourceDuplicator = ((options) => new ResourceDuplicator(options));
@@ -1,10 +1,9 @@
1
1
  import { OperationAgentInstanse, OperationDefinition } from "../../../query/types";
2
- import { Computed } from "../../../signals";
3
2
  import type { Operation } from "./Operation";
4
3
  export declare class OperationAgent<D extends OperationDefinition> implements OperationAgentInstanse<D> {
5
4
  private _operation;
6
5
  private _operations$;
7
- state$: Computed<{
6
+ state$: import("../../../signals/types").ComputeFn<{
8
7
  isLoading: boolean;
9
8
  isDone: boolean;
10
9
  isSuccess: boolean;
@@ -1,10 +1,10 @@
1
1
  import { Computed, Signal } from "../../../signals";
2
2
  export class OperationAgent {
3
3
  _operation;
4
- _operations$ = new Signal({
4
+ _operations$ = Signal.create({
5
5
  current$: null,
6
6
  }, { isDisabled: true });
7
- state$ = new Computed(() => {
7
+ state$ = Computed.create(() => {
8
8
  const operations = this._operations$.get();
9
9
  const currState = operations.current$?.value$.get();
10
10
  // Нет текущего состояния — дефолт
@@ -1,9 +1,8 @@
1
1
  import { ReactiveCache } from "../../query/lib/ReactiveCache";
2
- import { shallowEqual } from "../../common/utils";
3
2
  export declare class QueriesCache<KEY, VALUE> {
4
3
  private _cacheLifeTime;
5
4
  private readonly _cache;
6
- constructor(_cacheLifeTime?: number | false, compareArgsFn?: typeof shallowEqual);
5
+ constructor(_cacheLifeTime?: number | false, compareArgsFn?: ((a: KEY, b: KEY) => boolean));
7
6
  getQueryCache(args: KEY): ReactiveCache<VALUE> | undefined;
8
7
  createQueryCache(args: KEY, initialState: VALUE): ReactiveCache<VALUE>;
9
8
  values(): MapIterator<ReactiveCache<VALUE>>;
@@ -1,10 +1,9 @@
1
- import { Computed } from "../../../signals";
2
- import { ResourceAgentInstance, ResourceDefinition, ResourceQueryState } from "../../../query/types";
1
+ import { ResourceAgentInstance, ResourceDefinition } from "../../../query/types";
3
2
  import type { Resource } from "./Resource";
4
3
  export declare class ResourceAgent<D extends ResourceDefinition> implements ResourceAgentInstance<D> {
5
4
  private _resource;
6
5
  private _resources$;
7
- state$: Computed<{
6
+ state$: import("../../../signals/types").ComputeFn<{
8
7
  isInitiated: boolean;
9
8
  isLoading: boolean;
10
9
  isInitialLoading: boolean;
@@ -30,7 +29,6 @@ export declare class ResourceAgent<D extends ResourceDefinition> implements Reso
30
29
  args: NonNullable<D["Args"]> | undefined;
31
30
  }>;
32
31
  constructor(_resource: Resource<D>);
33
- getState(values: D["Args"]): ResourceQueryState<D>;
34
32
  initiate(args: D["Args"], force?: boolean): void;
35
33
  compareArgs(args: D["Args"], otherArgs: D["Args"]): boolean;
36
34
  private _next;
@@ -1,11 +1,11 @@
1
1
  import { Computed, Signal } from "../../../signals";
2
2
  export class ResourceAgent {
3
3
  _resource;
4
- _resources$ = new Signal({
4
+ _resources$ = Signal.create({
5
5
  previous$: null,
6
6
  current$: null,
7
7
  }, { isDisabled: true });
8
- state$ = new Computed(() => {
8
+ state$ = Computed.create(() => {
9
9
  const resources = this._resources$.get();
10
10
  let prevState;
11
11
  const currState = resources.current$?.value$.get();
@@ -68,38 +68,6 @@ export class ResourceAgent {
68
68
  constructor(_resource) {
69
69
  this._resource = _resource;
70
70
  }
71
- getState(values) {
72
- const cache = this._resource.getQueryCache(values);
73
- if (!cache) {
74
- return {
75
- isInitiated: false,
76
- isLoading: false,
77
- isInitialLoading: false,
78
- isDone: false,
79
- isSuccess: false,
80
- isError: false,
81
- isLocked: false,
82
- isReloading: false,
83
- error: undefined,
84
- data: undefined,
85
- args: undefined,
86
- };
87
- }
88
- const state = cache.value;
89
- return {
90
- isInitiated: state.isInitiated,
91
- isLoading: state.isLoading,
92
- isInitialLoading: state.isLoading && !state.isDone,
93
- isDone: state.isDone,
94
- isSuccess: state.isSuccess,
95
- isError: state.isError,
96
- isLocked: state.isLocked,
97
- isReloading: state.isReloading,
98
- error: state.error ?? undefined,
99
- data: state.data ?? undefined,
100
- args: state.args ?? undefined,
101
- };
102
- }
103
71
  initiate(args, force = false) {
104
72
  const current = this._resources$.peek().current$;
105
73
  const cache = this._resource.getQueryCache(args);
@@ -0,0 +1,73 @@
1
+ import { ResourceDefinition } from "../../../query/types";
2
+ import { CoreResourceQueryState, Resource } from "./Resource";
3
+ import { ReadableSignalLike } from "../../../signals/types";
4
+ import { Observable, Subject } from "rxjs";
5
+ import { ResourceDuplicatorAgent } from "./ResourceDuplicatorAgent";
6
+ export type DuplicatorOptions<D extends DuplicatorDefinition> = {
7
+ resource: Resource<D['RESOURCE_DEFINITION']>;
8
+ getArgKey: (item: D['ARGS_ITEM']) => string | number;
9
+ getDataKey: (item: D['DATA_ITEM']) => string | number;
10
+ cacheLifetime?: number | false;
11
+ };
12
+ export type DuplicatorDefinition<D extends ResourceDefinition = ResourceDefinition> = {
13
+ ARGS_ITEM: D['Args'] extends Array<any> ? D['Args'][number] : never;
14
+ DATA_ITEM: D['Data'] extends Array<any> ? D['Data'][number] : never;
15
+ RESOURCE_DEFINITION: D;
16
+ };
17
+ type State<D extends DuplicatorDefinition> = CoreResourceQueryState<D['RESOURCE_DEFINITION']> & {
18
+ unreleasedArgs?: D['ARGS_ITEM'][];
19
+ };
20
+ type Cache<D extends DuplicatorDefinition> = ComputedReactiveCache<State<D>>;
21
+ export type CoreResourceDuplicatorCache<D extends DuplicatorDefinition> = Cache<D>;
22
+ export declare class ResourceDuplicator<D extends DuplicatorDefinition> {
23
+ private _options;
24
+ private _fis;
25
+ private _caches;
26
+ private get _resource();
27
+ constructor(_options: DuplicatorOptions<D>);
28
+ getQueryCache(args: D['ARGS_ITEM'][]): Cache<D> | undefined;
29
+ createCache(args: D['ARGS_ITEM'][]): Cache<D>;
30
+ initiate(args: D['ARGS_ITEM'][], cache?: Cache<D>): Cache<D>;
31
+ serialize(args: D['ARGS_ITEM'][]): string;
32
+ compareArgs(a: D['ARGS_ITEM'][], b: D['ARGS_ITEM'][]): boolean;
33
+ createAgent: () => ResourceDuplicatorAgent<D>;
34
+ /** @deprecated */
35
+ d_init(args: D['ARGS_ITEM'][]): {
36
+ value$: import("../../../signals/types").ComputeFn<State<D>>;
37
+ };
38
+ }
39
+ export declare class ComputedReactiveCache<T> {
40
+ /**
41
+ * Реактивное значене (Observable)
42
+ */
43
+ value$: ReadableSignalLike<T>;
44
+ /**
45
+ * Значение без сайд-эффектов (для использования в DevTools)
46
+ */
47
+ spy$: Observable<T>;
48
+ /**
49
+ * Subject, уведомляющий об очистке кэша.
50
+ */
51
+ onClean$: Subject<T>;
52
+ closed: boolean;
53
+ private _getValue;
54
+ /**
55
+ * Создает новый экземпляр `ReactiveCacheItem`.
56
+ *
57
+ * @param options Параметры для настройки элемента кэша.
58
+ * @param options.initialState Начальное состояние кэша.
59
+ * @param options.cacheLifeTime Время жизни кэша в миллисекундах (по умолчанию 60_000).
60
+ */
61
+ constructor(options: {
62
+ obs: Observable<T>;
63
+ getValue: () => T;
64
+ cacheLifeTime: number | false;
65
+ });
66
+ private _getOnRefCountZero;
67
+ get value(): T;
68
+ /**
69
+ * Завершает работу кэша, закрывая все потоки и уведомляя об очистке.
70
+ */
71
+ complete(): void;
72
+ }
73
+ export {};
@@ -0,0 +1,227 @@
1
+ import { Signal, signalize } from "../../../signals";
2
+ import { finalize, ReplaySubject, share, Subject, takeUntil, timer } from "rxjs";
3
+ import { ResourceDuplicatorAgent } from "./ResourceDuplicatorAgent";
4
+ export class ResourceDuplicator {
5
+ _options;
6
+ _fis = new Map();
7
+ _caches;
8
+ get _resource() {
9
+ return this._options.resource;
10
+ }
11
+ constructor(_options) {
12
+ this._options = _options;
13
+ this._caches = new Map();
14
+ }
15
+ getQueryCache(args) {
16
+ const key = this.serialize(args);
17
+ return this._caches.get(key);
18
+ }
19
+ createCache(args) {
20
+ const key = this.serialize(args);
21
+ const { value$ } = this.d_init(args);
22
+ const cache = new ComputedReactiveCache({
23
+ cacheLifeTime: this._options.cacheLifetime ?? 60_000,
24
+ getValue: () => value$.get(),
25
+ obs: value$.obs,
26
+ });
27
+ cache.onClean$.subscribe(() => {
28
+ args.forEach(arg => {
29
+ const argKey = this._options.getArgKey(arg);
30
+ const fi = this._fis.get(argKey);
31
+ if (!fi)
32
+ return;
33
+ fi.k--;
34
+ if (fi.k <= 0) {
35
+ this._fis.delete(argKey);
36
+ }
37
+ });
38
+ this._caches.delete(key);
39
+ });
40
+ this._caches.set(key, cache);
41
+ return cache;
42
+ }
43
+ initiate(args, cache) {
44
+ const cacheInstance = cache ?? this.getQueryCache(args) ?? this.createCache(args);
45
+ const unreleasedArgs = cacheInstance.value.unreleasedArgs;
46
+ if (unreleasedArgs && unreleasedArgs.length !== 0) {
47
+ this._resource.initiate(unreleasedArgs);
48
+ }
49
+ const uninitiatedCaches = new Set();
50
+ console.log({ uninitiatedCaches, fis: this._fis });
51
+ args.forEach(arg => {
52
+ const argKey = this._options.getArgKey(arg);
53
+ let fi = this._fis.get(argKey);
54
+ if (fi && !fi.cache.value.isInitiated) {
55
+ uninitiatedCaches.add(fi.cache);
56
+ }
57
+ });
58
+ uninitiatedCaches.forEach((c) => {
59
+ this._resource.initiate(c.value.args, { cache: c });
60
+ });
61
+ return cacheInstance;
62
+ }
63
+ serialize(args) {
64
+ if (!args)
65
+ return '';
66
+ const argsKeys = args.map(a => this._options.getArgKey(a));
67
+ return argsKeys.join('|');
68
+ }
69
+ compareArgs(a, b) {
70
+ return this.serialize(a) === this.serialize(b);
71
+ }
72
+ createAgent = () => {
73
+ return new ResourceDuplicatorAgent(this);
74
+ };
75
+ /** @deprecated */
76
+ d_init(args) {
77
+ const argsKeys = args.map(a => this._options.getArgKey(a));
78
+ const releasedCaches = new Set();
79
+ const unreleasedArgs = [];
80
+ args.forEach(arg => {
81
+ const argKey = this._options.getArgKey(arg);
82
+ let fi = this._fis.get(argKey);
83
+ if (!fi || !fi.cache.value.isInitiated) {
84
+ unreleasedArgs.push(arg);
85
+ return;
86
+ }
87
+ fi.k++;
88
+ releasedCaches.add(fi.cache);
89
+ });
90
+ const queryCache = this._resource.createQueryCache(unreleasedArgs);
91
+ unreleasedArgs.forEach(arg => {
92
+ const argKey = this._options.getArgKey(arg);
93
+ let fi = this._fis.get(argKey);
94
+ if (!fi) {
95
+ fi = {
96
+ k: 1,
97
+ cache: queryCache,
98
+ };
99
+ this._fis.set(argKey, fi);
100
+ }
101
+ });
102
+ return {
103
+ value$: Signal.compute(() => {
104
+ const itemsAcc = [
105
+ queryCache.value$.get(),
106
+ ];
107
+ for (const rc of releasedCaches) {
108
+ itemsAcc.push(rc.value$.get());
109
+ }
110
+ const isNotInitiated = itemsAcc.some(i => !i.isInitiated);
111
+ const baseReturn = {
112
+ transactions: null,
113
+ abortController: null,
114
+ args,
115
+ savedData: null,
116
+ data: null,
117
+ error: null,
118
+ isError: false,
119
+ isLoading: false,
120
+ isReloading: false,
121
+ isDone: false,
122
+ isSuccess: false,
123
+ isLocked: false,
124
+ isInitiated: true,
125
+ lockCount: 0,
126
+ unreleasedArgs,
127
+ };
128
+ if (isNotInitiated)
129
+ return {
130
+ ...baseReturn,
131
+ isInitiated: false,
132
+ };
133
+ const isError = itemsAcc.some(i => i.isError);
134
+ if (isError) {
135
+ const firstError = itemsAcc.find(i => i.isError);
136
+ return {
137
+ ...baseReturn,
138
+ isError: true,
139
+ isDone: true,
140
+ error: firstError.error,
141
+ };
142
+ }
143
+ const isLoading = itemsAcc.some(i => i.isLoading);
144
+ if (isLoading)
145
+ return {
146
+ ...baseReturn,
147
+ isLoading: true,
148
+ };
149
+ const dataAcc = [];
150
+ itemsAcc.forEach(item => {
151
+ item.data?.forEach((d) => {
152
+ const dataKey = this._options.getDataKey(d);
153
+ const index = argsKeys.findIndex(ak => ak === dataKey);
154
+ if (index === -1)
155
+ return;
156
+ dataAcc[index] = d;
157
+ });
158
+ });
159
+ return {
160
+ ...baseReturn,
161
+ isSuccess: true,
162
+ isDone: true,
163
+ data: dataAcc,
164
+ };
165
+ }, { isDisabled: true }),
166
+ };
167
+ }
168
+ }
169
+ export class ComputedReactiveCache {
170
+ /**
171
+ * Реактивное значене (Observable)
172
+ */
173
+ value$;
174
+ /**
175
+ * Значение без сайд-эффектов (для использования в DevTools)
176
+ */
177
+ spy$;
178
+ /**
179
+ * Subject, уведомляющий об очистке кэша.
180
+ */
181
+ onClean$ = new Subject();
182
+ closed = false;
183
+ _getValue;
184
+ /**
185
+ * Создает новый экземпляр `ReactiveCacheItem`.
186
+ *
187
+ * @param options Параметры для настройки элемента кэша.
188
+ * @param options.initialState Начальное состояние кэша.
189
+ * @param options.cacheLifeTime Время жизни кэша в миллисекундах (по умолчанию 60_000).
190
+ */
191
+ constructor(options) {
192
+ const cacheLifeTime = options.cacheLifeTime ?? 60_000;
193
+ this.spy$ = options.obs.pipe(takeUntil(this.onClean$));
194
+ this.value$ = signalize(options.obs.pipe(finalize(() => {
195
+ this.complete();
196
+ }), share({
197
+ connector: () => new ReplaySubject(1),
198
+ resetOnRefCountZero: this._getOnRefCountZero(cacheLifeTime),
199
+ resetOnComplete: true,
200
+ })));
201
+ this._getValue = options.getValue;
202
+ }
203
+ _getOnRefCountZero(cacheLifeTime) {
204
+ if (cacheLifeTime === false) {
205
+ return false;
206
+ }
207
+ if (cacheLifeTime <= 0) {
208
+ return true;
209
+ }
210
+ return () => {
211
+ return timer(cacheLifeTime);
212
+ };
213
+ }
214
+ get value() {
215
+ return this._getValue();
216
+ }
217
+ /**
218
+ * Завершает работу кэша, закрывая все потоки и уведомляя об очистке.
219
+ */
220
+ complete() {
221
+ if (this.closed)
222
+ return;
223
+ this.closed = true;
224
+ this.onClean$.next(this._getValue());
225
+ this.onClean$.complete();
226
+ }
227
+ }
@@ -0,0 +1,35 @@
1
+ import { ResourceAgentInstance } from "../../../query/types";
2
+ import { ResourceDuplicator, DuplicatorDefinition } from "../../../query/core/Resource/ResourceDuplicator";
3
+ export declare class ResourceDuplicatorAgent<D extends DuplicatorDefinition> implements ResourceAgentInstance<D['RESOURCE_DEFINITION']> {
4
+ private _resource;
5
+ private _resources$;
6
+ state$: import("../../../signals/types").ComputeFn<{
7
+ isInitiated: boolean;
8
+ isLoading: boolean;
9
+ isInitialLoading: boolean;
10
+ isDone: boolean;
11
+ isSuccess: boolean;
12
+ isError: boolean;
13
+ isLocked: boolean;
14
+ isReloading: boolean;
15
+ error: undefined;
16
+ data: undefined;
17
+ args: D["ARGS_ITEM"][];
18
+ } | {
19
+ isInitiated: boolean;
20
+ isLoading: boolean;
21
+ isInitialLoading: boolean;
22
+ isDone: boolean;
23
+ isSuccess: boolean;
24
+ isError: boolean;
25
+ isLocked: boolean;
26
+ isReloading: boolean;
27
+ error: {} | undefined;
28
+ data: NonNullable<D["RESOURCE_DEFINITION"]["Data"]> | undefined;
29
+ args: NonNullable<D["RESOURCE_DEFINITION"]["Args"]> | undefined;
30
+ }>;
31
+ constructor(_resource: ResourceDuplicator<D>);
32
+ initiate(args: D['ARGS_ITEM'][], force?: boolean): void;
33
+ compareArgs(args: D['ARGS_ITEM'][], otherArgs: D['ARGS_ITEM'][]): boolean;
34
+ private _next;
35
+ }
@@ -0,0 +1,110 @@
1
+ import { Computed, Signal } from "../../../signals";
2
+ export class ResourceDuplicatorAgent {
3
+ _resource;
4
+ _resources$ = Signal.create({
5
+ previous$: null,
6
+ current$: null,
7
+ }, { isDisabled: true });
8
+ state$ = Computed.create(() => {
9
+ const resources = this._resources$.get();
10
+ let prevState;
11
+ const currState = resources.current$?.value$.get();
12
+ // Отлавливаем кейс, когда ресурс был спрошен.
13
+ // На данные момент единсвенная причина сброса - resetAllQueriesCache(),
14
+ // но в будущем могут быть и другие причины, что потребует доработку.
15
+ if (currState && !currState.isInitiated) {
16
+ this._resource.initiate(currState.args, resources.current$);
17
+ return {
18
+ isInitiated: true,
19
+ isLoading: true,
20
+ isInitialLoading: true,
21
+ isDone: false,
22
+ isSuccess: false,
23
+ isError: false,
24
+ isReloading: false,
25
+ error: undefined,
26
+ data: undefined,
27
+ // TODO вообще нет точного представлния, как блокировака доложна работать.
28
+ // Мб тут стоит брать currState.isLocked.
29
+ isLocked: false,
30
+ args: currState.args,
31
+ };
32
+ }
33
+ if (!currState?.isDone) {
34
+ prevState = resources.previous$?.value;
35
+ }
36
+ // Нет текущего состояния — дефолт
37
+ if (!currState) {
38
+ return {
39
+ isInitiated: false,
40
+ isLoading: false,
41
+ isInitialLoading: false,
42
+ isDone: false,
43
+ isSuccess: false,
44
+ isError: false,
45
+ isLocked: false,
46
+ isReloading: false,
47
+ error: undefined,
48
+ data: undefined,
49
+ args: undefined,
50
+ };
51
+ }
52
+ // Если идёт загрузка, но есть успешные данные из прошлого запроса — показываем их
53
+ const isShowPrev = currState.isLoading && prevState && prevState.isSuccess;
54
+ return {
55
+ isInitiated: currState.isInitiated || !!prevState,
56
+ isLoading: currState.isLoading,
57
+ isInitialLoading: currState.isLoading && !currState.isDone && !prevState?.isDone,
58
+ isDone: currState.isDone,
59
+ isSuccess: currState.isSuccess,
60
+ isError: currState.isError,
61
+ isLocked: currState.isLocked,
62
+ isReloading: currState.isReloading,
63
+ error: isShowPrev ? prevState.error ?? undefined : currState.error ?? undefined,
64
+ data: isShowPrev ? prevState.data ?? undefined : currState.data ?? undefined,
65
+ args: currState.args ?? undefined,
66
+ };
67
+ }, { isDisabled: true });
68
+ constructor(_resource) {
69
+ this._resource = _resource;
70
+ }
71
+ initiate(args, force = false) {
72
+ const current = this._resources$.peek().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
+ compareArgs(args, otherArgs) {
87
+ return this._resource.compareArgs(args, otherArgs);
88
+ }
89
+ _next(newCache) {
90
+ const { previous$, current$ } = this._resources$.peek();
91
+ if (!current$) {
92
+ this._resources$.set({
93
+ previous$: null,
94
+ current$: newCache,
95
+ });
96
+ return;
97
+ }
98
+ if (!current$.value$.peek().isDone && previous$?.value$.peek().isDone) {
99
+ this._resources$.set({
100
+ previous$: previous$,
101
+ current$: newCache,
102
+ });
103
+ return;
104
+ }
105
+ this._resources$.set({
106
+ previous$: current$,
107
+ current$: newCache,
108
+ });
109
+ }
110
+ }
@@ -1,6 +1,7 @@
1
1
  export * from './api/createResource';
2
2
  export * from './api/createOperation';
3
3
  export * from './api/resetAllQueriesCache';
4
+ export * from './api/createResourceDuplicator';
4
5
  export * from './SKIP_TOKEN';
5
6
  export * from './react/useResourceAgent';
6
7
  export * from './react/useResourceRef';
@@ -1,6 +1,7 @@
1
1
  export * from './api/createResource';
2
2
  export * from './api/createOperation';
3
3
  export * from './api/resetAllQueriesCache';
4
+ export * from './api/createResourceDuplicator';
4
5
  export * from './SKIP_TOKEN';
5
6
  export * from './react/useResourceAgent';
6
7
  export * from './react/useResourceRef';
@@ -1,5 +1,10 @@
1
- import { Prettify, ResourceDefinition, ResourceInstance, ResourceQueryState } from "../../query/types";
1
+ import { Prettify, ResourceAgentInstance, ResourceDefinition, ResourceQueryState } from "../../query/types";
2
2
  import { SKIP } from "../../query/SKIP_TOKEN";
3
+ import { ResourceDuplicatorAgent } from "../../query/core/Resource/ResourceDuplicatorAgent";
4
+ import { DuplicatorDefinition } from "../../query/core/Resource/ResourceDuplicator";
3
5
  type Result<D extends ResourceDefinition> = Prettify<ResourceQueryState<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>;
6
+ type WithCreateAgent<D extends ResourceDefinition> = {
7
+ createAgent: () => ResourceAgentInstance<D> | ResourceDuplicatorAgent<DuplicatorDefinition<D>>;
8
+ };
9
+ export declare function useResourceAgent<D extends ResourceDefinition>(res: WithCreateAgent<D>, ...argss: D['Args'] extends void ? [] | [typeof SKIP] : [D['Args'] | typeof SKIP]): Result<D>;
5
10
  export {};
@@ -12,9 +12,18 @@ export function useResourceAgent(res, ...argss) {
12
12
  }
13
13
  return agent;
14
14
  });
15
- if (args !== SKIP && !agent.compareArgs(args, prevArgsRef.current)) {
15
+ if (!compare(args, prevArgsRef.current, agent)) {
16
16
  prevArgsRef.current = args;
17
17
  agent.initiate(args);
18
18
  }
19
19
  return useSignal(agent.state$);
20
20
  }
21
+ function compare(args, prevArgs, agent) {
22
+ if (args === SKIP && prevArgs === SKIP) {
23
+ return true;
24
+ }
25
+ if (args === SKIP || prevArgs === SKIP) {
26
+ return false;
27
+ }
28
+ return agent.compareArgs(args, prevArgs);
29
+ }
@@ -72,7 +72,7 @@ export type ResourceAgentInstance<D extends ResourceDefinition> = {
72
72
  /** Инициирует запрос с указанными аргументами */
73
73
  initiate(args: D["Args"], force?: boolean): void;
74
74
  /** Сравнивает аргументы между собой */
75
- compareArgs(args1: D["Args"], args2: D["Args"]): unknown;
75
+ compareArgs(args1: D["Args"], args2: D["Args"]): boolean;
76
76
  };
77
77
  /**
78
78
  * Состояние запроса ресурса
@@ -74,7 +74,7 @@ DefaultOptions.update({
74
74
  ```
75
75
 
76
76
  **Может пригодиться:**
77
- - Если в вашей среде неудобно или проблематично установить браузерное расширение
77
+ - Если в вашей среде невозможно установить браузерное расширение
78
78
  - Для мобильной отладки
79
79
 
80
80
  ---
@@ -206,7 +206,7 @@ interface DevtoolsLike {
206
206
  }
207
207
 
208
208
  interface DevtoolsStateLike<T = any> {
209
- (newState: T): void;
209
+ (newState: T | '$COMPLETED' | '$CLEANED'): void;
210
210
  }
211
211
  ```
212
212
 
@@ -13,7 +13,7 @@ signal.value += 1;
13
13
  // Стало
14
14
  const signal = Signal.create(0); // ✅
15
15
  const computed = Signal.compute(() => signal() * 2); // ✅
16
- signal.set(signal.peek() + 1);
16
+ signal.set(signal() + 1);
17
17
  ```
18
18
 
19
19
 
@@ -42,7 +42,7 @@ DefaultOptions.update({
42
42
  // Логирование
43
43
  console.error('[RxToolkit Query Error]', error);
44
44
 
45
- // Отправка в систему мониторинга
45
+ // Отправка в абстакную систему мониторинга
46
46
  Sentry.captureException(error, {
47
47
  tags: { source: 'rx-toolkit-query' }
48
48
  });
@@ -64,26 +64,27 @@ DefaultOptions.update({
64
64
  **Тип:** `(() => string | null) | null`
65
65
  **По умолчанию:** `null`
66
66
 
67
- Функция для получения имени текущего scope. Полезно для SSR, изоляции данных между запросами или многопользовательских приложений.
67
+ Функция для получения имени текущего scope.
68
+ Можено, например, подключить к DI систему, для раширенного devtools нейминга.
68
69
 
69
70
  ```typescript
70
71
  import { DefaultOptions } from '@fozy-labs/rx-toolkit';
71
72
 
72
- // SSR: изоляция данных между запросами
73
- let currentRequestId: string | null = null;
74
-
75
73
  DefaultOptions.update({
76
- getScopeName: () => currentRequestId
74
+ getScopeName: () => MyDiAbsractDi.getCurrentScopeName(),
77
75
  });
78
76
 
79
- // В middleware сервера
80
- app.use((req, res, next) => {
81
- currentRequestId = req.id;
82
- res.on('finish', () => {
83
- currentRequestId = null;
84
- });
85
- next();
86
- });
77
+ // Объявляем класс
78
+ class Counter {
79
+ value$ = Signal.create(0, '{scope}/Counter/value$');
80
+ }
81
+
82
+ // В другом месте приложения
83
+ function ChannelCounter() {
84
+ const counter = MyDiAbsractDi.resolve<Counter>('Counter', 'ChannelScope');
85
+ console.log(counter.value$()); // Devtools покажет имя сигнала как "ChannelScope/Counter/value$"
86
+ return null;
87
+ }
87
88
  ```
88
89
 
89
90
  ---
@@ -19,11 +19,11 @@ RxSignals — это реактивная система управления с
19
19
  ```typescript
20
20
  import { Signal } from '@fozy-labs/rx-toolkit';
21
21
 
22
- const name = new Signal('John');
23
- const age = new Signal(25);
22
+ const name = Signal.create('John');
23
+ const age = Signal.create(25);
24
24
 
25
25
  // Чтение значения (с отслеживанием зависимостей)
26
- console.log(name.get()); // "John"
26
+ console.log(name()); // "John"
27
27
 
28
28
  // Чтение значения без отслеживания
29
29
  console.log(name.peek()); // "John"
@@ -41,7 +41,7 @@ subscription.unsubscribe();
41
41
  ```
42
42
 
43
43
  **API Signal:**
44
- - `get()` — получить значение и зарегистрировать зависимость (для использования внутри Computed/Effect)
44
+ - `()`|`get()` — получить значение и зарегистрировать зависимость (для использования внутри Computed/Effect)
45
45
  - `peek()` — получить значение без регистрации зависимости
46
46
  - `set(value)` — установить новое значение
47
47
  - `obs` — RxJS Observable для подписки на изменения
@@ -51,58 +51,42 @@ subscription.unsubscribe();
51
51
  Создает вычисляемое значение, которое автоматически обновляется при изменении зависимостей.
52
52
 
53
53
  ```typescript
54
- import { Signal, Computed } from '@fozy-labs/rx-toolkit';
54
+ import { Signal } from '@fozy-labs/rx-toolkit';
55
55
 
56
- const firstName = new Signal('John');
57
- const lastName = new Signal('Doe');
56
+ const firstName = Signal.create('John');
57
+ const lastName = Signal.create('Doe');
58
58
 
59
- const fullName = new Computed(() => `${firstName.get()} ${lastName.get()}`);
59
+ const fullName = Signal.compute(() => `${firstName()} ${lastName()}`);
60
60
 
61
- console.log(fullName.get()); // "John Doe"
61
+ console.log(fullName()); // "John Doe"
62
62
 
63
63
  firstName.set('Jane');
64
- console.log(fullName.get()); // "Jane Doe"
64
+ console.log(fullName()); // "Jane Doe"
65
65
 
66
66
  // Подписка на изменения
67
67
  fullName.obs.subscribe(name => console.log(name));
68
68
  ```
69
69
 
70
70
  **API Computed:**
71
- - `get()` — получить вычисленное значение с регистрацией зависимости
71
+ - `()`|`get()` — получить вычисленное значение с регистрацией зависимости
72
72
  - `peek()` — получить значение без регистрации зависимости
73
73
  - `obs` — RxJS Observable для подписки на изменения
74
74
 
75
75
  Также на данный момент Computed
76
76
 
77
- +Важно: отличие от RxJS
78
- +
79
- + - Computed: по умолчанию `Computed.obs` применяет `distinctUntilChanged()` — это значит, что подписчики не будут получать повторных эмиссий, если новое значение строго равно (`===`) предыдущему. Такое поведение предотвращает лишние рендеры и обработки, когда значение фактически не изменилось.
80
- + - Signal: базовый `Signal` использует `BehaviorSubject` и при вызове `set()` будет эмитить значение независимо от того, изменилось оно или нет (если вам нужно предотвратить повторные эмиссии для сигнала, применяйте операторы RxJS к `signal.obs`, например `signal.obs.pipe(distinctUntilChanged())`).
81
- +
82
- +Пример: если у вас есть `const total = Signal.compute(() => a.get() + b.get())`, то при изменении `a` или `b` `total.obs` сработает только если новое значение суммы отличается от предыдущего (по `===`). Если нужен особый критерий сравнения, используйте `distinctUntilChanged` с собственной функцией сравнения:
83
- +
84
- +```ts
85
- +import { distinctUntilChanged } from 'rxjs';
86
- +
87
- +total.obs.pipe(distinctUntilChanged((prev, next) => /* ваша логика */));
88
- +```
89
- +
90
- +Рекомендация: рассчитывайте на то, что `Computed` избавляет от лишних эмиссий по-умолчанию; если вам нужно другое поведение, применяйте RxJS-операторы к `obs` или создавайте `Computed`, возвращающий другую форму данных (например объект с версией) для более тонкого контроля.
91
- +
92
-
93
77
  ### Effect
94
78
 
95
79
  Создает побочный эффект, который автоматически выполняется при изменении используемых сигналов.
96
80
 
97
81
  ```typescript
98
- import { Signal, Effect } from '@fozy-labs/rx-toolkit';
82
+ import { Signal } from '@fozy-labs/rx-toolkit';
99
83
 
100
- const count = new Signal(0);
101
- const message = new Signal('Hello');
84
+ const count = Signal.create(0);
85
+ const message = Signal.create('Hello');
102
86
 
103
- const effect = new Effect(() => {
87
+ const effect = Signal.effect(() => {
104
88
  // Выведет: "Hello: 0" при инициализации
105
- console.log(`${message.get()}: ${count.get()}`);
89
+ console.log(`${message()}: ${count()}`);
106
90
  });
107
91
 
108
92
  count.set(1); // Выведет: "Hello: 1"
@@ -117,8 +101,9 @@ effect.unsubscribe();
117
101
  Effect поддерживает возврат функции очистки, которая вызывается перед следующим выполнением или при отписке:
118
102
 
119
103
  ```typescript
120
- const effect = new Effect(() => {
121
- const timer = setInterval(() => console.log(count.get()), 1000);
104
+ const effect = Signal.effect(() => {
105
+ count(); // Создаем подписку на count (тк не работает при асинхронных операциях)
106
+ const timer = setInterval(() => count(), 1000);
122
107
 
123
108
  // Cleanup - вызывается перед повторным выполнением эффекта
124
109
  return () => {
@@ -127,43 +112,35 @@ const effect = new Effect(() => {
127
112
  });
128
113
  ```
129
114
 
130
- ## Функциональный стиль API
115
+ ## Функциональный vs классовый стиль
131
116
 
132
- Для более компактного синтаксиса доступны статические методы `Signal.create()`, `Signal.compute()` и `Signal.effect()`:
117
+ RxSignals поддерживает как функциональный, так и классовый стили создания сигналов, позволяя выбрать подход в зависимости от предпочтений и архитектуры приложения.
118
+ #### Функциональный стиль (рекомендуемый)
133
119
 
134
- ```typescript
120
+ Используйте статические методы `Signal.create`,`Signal.compute` и `Signal.effect` для создания сигналов.
121
+ Этот стиль лаконичен, похож на SolidJS и подходит для большинства случаев:
122
+
123
+ ```tszz
135
124
  import { Signal } from '@fozy-labs/rx-toolkit';
136
125
 
137
- class CounterStore {
138
- // Создание сигнала в функциональном стиле (вызывается как функция)
139
- count$ = Signal.create(0);
140
-
141
- // Computed в функциональном стиле
142
- doubled$ = Signal.compute(() => this.count$() * 2);
143
- squared$ = Signal.compute(() => (this.doubled$() / 2) ** 2);
144
-
145
- increment = () => this.count$.set(this.count$.peek() + 1);
146
- decrement = () => this.count$.set(this.count$.peek() - 1);
147
- reset = () => this.count$.set(0);
148
- }
126
+ const count = Signal.create(0);
127
+ const doubled = Signal.compute(() => count() * 2);
128
+ const logEffect = Signal.effect(() => console.log(doubled()));
129
+ ```
149
130
 
150
- const store = new CounterStore();
131
+ #### Классовый стиль
151
132
 
152
- // Чтение значения - вызов как функции
153
- console.log(store.count$()); // 0
154
- console.log(store.doubled$()); // 0
133
+ Создавайте экземпляры классов Signal, Computed и Effect напрямую.
134
+ Этот стиль более явный, похож на RxJs и полезен для наследования или сложной логики,
135
+ учтите, что вызов `()` недоступен и нужно использовать `get()`:
155
136
 
156
- store.increment();
157
- console.log(store.count$()); // 1
158
- console.log(store.doubled$()); // 2
159
- console.log(store.squared$()); // 1
160
- ```
137
+ ```ts
138
+ import { Signal, Computed, Effect } from '@fozy-labs/rx-toolkit';
161
139
 
162
- **API функциональных сигналов:**
163
- - `signal$()` вызов как функции возвращает значение (аналог `get()`)
164
- - `signal$.peek()` получить значение без отслеживания
165
- - `signal$.set(value)` — установить значение
166
- - `signal$.obs` — RxJS Observable
140
+ const count = new Signal(0);
141
+ const doubled = new Computed(() => count.get() * 2);
142
+ const logEffect = new Effect(() => console.log(doubled.get()));
143
+ ```
167
144
 
168
145
  ### ReadonlySignal
169
146
 
@@ -246,8 +246,8 @@ class CounterStore {
246
246
  count$ = Signal.create(0, 'counter');
247
247
  doubled$ = Signal.compute(() => this.count$() * 2);
248
248
 
249
- increment = () => this.count$.set(this.count$.peek() + 1);
250
- decrement = () => this.count$.set(this.count$.peek() - 1);
249
+ increment = () => this.count$.set(this.count$() + 1);
250
+ decrement = () => this.count$.set(this.count$() - 1);
251
251
  reset = () => this.count$.set(0);
252
252
  }
253
253
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fozy-labs/rx-toolkit",
3
- "version": "0.5.0-rc.3",
3
+ "version": "0.5.0",
4
4
  "main": "./dist/index.js",
5
5
  "types": "./dist/index.d.ts",
6
6
  "type": "module",