@fozy-labs/rx-toolkit 0.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +131 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +3 -0
- package/dist/query/SKIP_TOKEN.d.ts +1 -0
- package/dist/query/SKIP_TOKEN.js +1 -0
- package/dist/query/api/DefaultOptions.d.ts +9 -0
- package/dist/query/api/DefaultOptions.js +9 -0
- package/dist/query/api/createDevtools.d.ts +1 -0
- package/dist/query/api/createDevtools.js +6 -0
- package/dist/query/api/createOperation.d.ts +7 -0
- package/dist/query/api/createOperation.js +6 -0
- package/dist/query/api/createResource.d.ts +3 -0
- package/dist/query/api/createResource.js +2 -0
- package/dist/query/api/createSubResource.d.ts +0 -0
- package/dist/query/api/createSubResource.js +1 -0
- package/dist/query/core/Opertation/Operation.d.ts +34 -0
- package/dist/query/core/Opertation/Operation.js +176 -0
- package/dist/query/core/Opertation/OperationAgent.d.ts +20 -0
- package/dist/query/core/Opertation/OperationAgent.js +54 -0
- package/dist/query/core/QueriesCache.d.ts +8 -0
- package/dist/query/core/QueriesCache.js +31 -0
- package/dist/query/core/Resource/Resource.d.ts +40 -0
- package/dist/query/core/Resource/Resource.js +154 -0
- package/dist/query/core/Resource/ResourceAgent.d.ts +34 -0
- package/dist/query/core/Resource/ResourceAgent.js +87 -0
- package/dist/query/core/Resource/ResourceRef.d.ts +18 -0
- package/dist/query/core/Resource/ResourceRef.js +52 -0
- package/dist/query/core/SharedOptions.d.ts +5 -0
- package/dist/query/core/SharedOptions.js +4 -0
- package/dist/query/index.d.ts +7 -0
- package/dist/query/index.js +7 -0
- package/dist/query/lib/IndirectMap.d.ts +18 -0
- package/dist/query/lib/IndirectMap.js +85 -0
- package/dist/query/lib/ReactiveCache.d.ts +77 -0
- package/dist/query/lib/ReactiveCache.js +96 -0
- package/dist/query/lib/shallowEqual.d.ts +1 -0
- package/dist/query/lib/shallowEqual.js +21 -0
- package/dist/query/react/useOperationAgent.d.ts +9 -0
- package/dist/query/react/useOperationAgent.js +22 -0
- package/dist/query/react/useResourceAgent.d.ts +9 -0
- package/dist/query/react/useResourceAgent.js +25 -0
- package/dist/query/types/Operation.types.d.ts +91 -0
- package/dist/query/types/Operation.types.js +1 -0
- package/dist/query/types/Resource.types.d.ts +90 -0
- package/dist/query/types/Resource.types.js +1 -0
- package/dist/query/types/shared.types.d.ts +4 -0
- package/dist/query/types/shared.types.js +1 -0
- package/dist/react-hooks/index.d.ts +5 -0
- package/dist/react-hooks/index.js +5 -0
- package/dist/react-hooks/useConstant.d.ts +4 -0
- package/dist/react-hooks/useConstant.js +19 -0
- package/dist/react-hooks/useEventHandler.d.ts +1 -0
- package/dist/react-hooks/useEventHandler.js +6 -0
- package/dist/react-hooks/useObservable.d.ts +12 -0
- package/dist/react-hooks/useObservable.js +48 -0
- package/dist/react-hooks/useSignal.d.ts +2 -0
- package/dist/react-hooks/useSignal.js +12 -0
- package/dist/react-hooks/useSyncObservable.d.ts +16 -0
- package/dist/react-hooks/useSyncObservable.js +52 -0
- package/dist/signal/base/Batcher.d.ts +7 -0
- package/dist/signal/base/Batcher.js +21 -0
- package/dist/signal/base/Computed.d.ts +10 -0
- package/dist/signal/base/Computed.js +26 -0
- package/dist/signal/base/Effect.d.ts +12 -0
- package/dist/signal/base/Effect.js +69 -0
- package/dist/signal/base/ReadonlySignal.d.ts +12 -0
- package/dist/signal/base/ReadonlySignal.js +20 -0
- package/dist/signal/base/Signal.d.ts +30 -0
- package/dist/signal/base/Signal.js +44 -0
- package/dist/signal/base/SyncObservable.d.ts +5 -0
- package/dist/signal/base/SyncObservable.js +18 -0
- package/dist/signal/base/Tracker.d.ts +5 -0
- package/dist/signal/base/Tracker.js +7 -0
- package/dist/signal/base/index.d.ts +8 -0
- package/dist/signal/base/index.js +8 -0
- package/dist/signal/base/types.d.ts +15 -0
- package/dist/signal/base/types.js +1 -0
- package/dist/signal/extends/LocalSignal.d.ts +24 -0
- package/dist/signal/extends/LocalSignal.js +66 -0
- package/dist/signal/extends/index.d.ts +1 -0
- package/dist/signal/extends/index.js +1 -0
- package/dist/signal/index.d.ts +3 -0
- package/dist/signal/index.js +3 -0
- package/dist/signal/operators/batch.d.ts +2 -0
- package/dist/signal/operators/batch.js +27 -0
- package/dist/signal/operators/filterUpdates.d.ts +5 -0
- package/dist/signal/operators/filterUpdates.js +18 -0
- package/dist/signal/operators/index.d.ts +4 -0
- package/dist/signal/operators/index.js +4 -0
- package/dist/signal/operators/mapSignals.d.ts +3 -0
- package/dist/signal/operators/mapSignals.js +10 -0
- package/dist/signal/operators/signalize.d.ts +3 -0
- package/dist/signal/operators/signalize.js +6 -0
- package/docs/query/README.md +224 -0
- package/docs/signals/README.md +119 -0
- package/docs/usage/react/README.md +144 -0
- package/package.json +57 -0
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { BehaviorSubject, Observable } from "rxjs";
|
|
2
|
+
import { type ReadableSignalLike, UnaryFunction } from "./types";
|
|
3
|
+
export declare class Signal<T> extends BehaviorSubject<T> implements ReadableSignalLike<T> {
|
|
4
|
+
constructor(initialValue: T);
|
|
5
|
+
protected _onChange(value: T): void;
|
|
6
|
+
get value(): T;
|
|
7
|
+
set value(value: T);
|
|
8
|
+
next(value: T): void;
|
|
9
|
+
peek(): T;
|
|
10
|
+
/**
|
|
11
|
+
* @deprecated use `value` instead.
|
|
12
|
+
*/
|
|
13
|
+
get(): T;
|
|
14
|
+
/**
|
|
15
|
+
* @deprecated use `next(value)` instead.
|
|
16
|
+
*/
|
|
17
|
+
set(value: T): void;
|
|
18
|
+
pipe(): Signal<T>;
|
|
19
|
+
pipe<A extends Observable<any>>(op1: UnaryFunction<ReadableSignalLike<T>, A>): A;
|
|
20
|
+
pipe<A extends Observable<any>, B extends Observable<any>>(op1: UnaryFunction<Signal<T>, A>, op2: UnaryFunction<A, B>): B;
|
|
21
|
+
pipe<A extends Observable<any>, B extends Observable<any>, C extends Observable<any>>(op1: UnaryFunction<Signal<T>, A>, op2: UnaryFunction<A, B>, op3: UnaryFunction<B, C>): C;
|
|
22
|
+
pipe<A extends Observable<any>, B extends Observable<any>, C extends Observable<any>, D extends Observable<any>>(op1: UnaryFunction<Signal<T>, A>, op2: UnaryFunction<A, B>, op3: UnaryFunction<B, C>, op4: UnaryFunction<C, D>): D;
|
|
23
|
+
pipe<A extends Observable<any>, B extends Observable<any>, C extends Observable<any>, D extends Observable<any>, E extends Observable<any>>(op1: UnaryFunction<Signal<T>, A>, op2: UnaryFunction<A, B>, op3: UnaryFunction<B, C>, op4: UnaryFunction<C, D>, op5: UnaryFunction<D, E>): E;
|
|
24
|
+
pipe<A extends Observable<any>, B extends Observable<any>, C extends Observable<any>, D extends Observable<any>, E extends Observable<any>, F extends Observable<any>>(op1: UnaryFunction<Signal<T>, A>, op2: UnaryFunction<A, B>, op3: UnaryFunction<B, C>, op4: UnaryFunction<C, D>, op5: UnaryFunction<D, E>, op6: UnaryFunction<E, F>): F;
|
|
25
|
+
pipe<A extends Observable<any>, B extends Observable<any>, C extends Observable<any>, D extends Observable<any>, E extends Observable<any>, F extends Observable<any>, G extends Observable<any>>(op1: UnaryFunction<Signal<T>, A>, op2: UnaryFunction<A, B>, op3: UnaryFunction<B, C>, op4: UnaryFunction<C, D>, op5: UnaryFunction<D, E>, op6: UnaryFunction<E, F>, op7: UnaryFunction<F, G>): G;
|
|
26
|
+
pipe<A extends Observable<any>, B extends Observable<any>, C extends Observable<any>, D extends Observable<any>, E extends Observable<any>, F extends Observable<any>, G extends Observable<any>, H extends Observable<any>>(op1: UnaryFunction<Signal<T>, A>, op2: UnaryFunction<A, B>, op3: UnaryFunction<B, C>, op4: UnaryFunction<C, D>, op5: UnaryFunction<D, E>, op6: UnaryFunction<E, F>, op7: UnaryFunction<F, G>, op8: UnaryFunction<G, H>): H;
|
|
27
|
+
pipe<A extends Observable<any>, B extends Observable<any>, C extends Observable<any>, D extends Observable<any>, E extends Observable<any>, F extends Observable<any>, G extends Observable<any>, H extends Observable<any>, I extends Observable<any>>(op1: UnaryFunction<Signal<T>, A>, op2: UnaryFunction<A, B>, op3: UnaryFunction<B, C>, op4: UnaryFunction<C, D>, op5: UnaryFunction<D, E>, op6: UnaryFunction<E, F>, op7: UnaryFunction<F, G>, op8: UnaryFunction<G, H>, op9: UnaryFunction<H, I>): I;
|
|
28
|
+
pipe<A extends Observable<any>, B extends Observable<any>, C extends Observable<any>, D extends Observable<any>, E extends Observable<any>, F extends Observable<any>, G extends Observable<any>, H extends Observable<any>, I extends Observable<any>>(op1: UnaryFunction<Signal<T>, A>, op2: UnaryFunction<A, B>, op3: UnaryFunction<B, C>, op4: UnaryFunction<C, D>, op5: UnaryFunction<D, E>, op6: UnaryFunction<E, F>, op7: UnaryFunction<F, G>, op8: UnaryFunction<G, H>, op9: UnaryFunction<H, I>, ...operations: UnaryFunction<Observable<any>, Observable<any>>[]): Observable<unknown>;
|
|
29
|
+
asReadonly(): ReadableSignalLike<T>;
|
|
30
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { BehaviorSubject } from "rxjs";
|
|
2
|
+
import { Tracker } from "./Tracker";
|
|
3
|
+
export class Signal extends BehaviorSubject {
|
|
4
|
+
constructor(initialValue) {
|
|
5
|
+
super(initialValue);
|
|
6
|
+
}
|
|
7
|
+
_onChange(value) {
|
|
8
|
+
super.next(value);
|
|
9
|
+
}
|
|
10
|
+
get value() {
|
|
11
|
+
Tracker.next(this);
|
|
12
|
+
return super.value;
|
|
13
|
+
}
|
|
14
|
+
set value(value) {
|
|
15
|
+
this._onChange(value);
|
|
16
|
+
}
|
|
17
|
+
next(value) {
|
|
18
|
+
this._onChange(value);
|
|
19
|
+
}
|
|
20
|
+
peek() {
|
|
21
|
+
return super.value;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* @deprecated use `value` instead.
|
|
25
|
+
*/
|
|
26
|
+
get() {
|
|
27
|
+
return this.value;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* @deprecated use `next(value)` instead.
|
|
31
|
+
*/
|
|
32
|
+
set(value) {
|
|
33
|
+
return this.next(value);
|
|
34
|
+
}
|
|
35
|
+
pipe(...operations) {
|
|
36
|
+
return operations.reduce(pipeReducer, this);
|
|
37
|
+
}
|
|
38
|
+
asReadonly() {
|
|
39
|
+
return this;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
function pipeReducer(prev, fn) {
|
|
43
|
+
return fn(prev);
|
|
44
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { Observable } from "rxjs";
|
|
2
|
+
const empty = Symbol('EMPTY');
|
|
3
|
+
export class SyncObservable extends Observable {
|
|
4
|
+
constructor(subscribe) {
|
|
5
|
+
super(subscribe);
|
|
6
|
+
}
|
|
7
|
+
get value() {
|
|
8
|
+
let value = empty;
|
|
9
|
+
const sub = this.subscribe((v) => {
|
|
10
|
+
value = v;
|
|
11
|
+
});
|
|
12
|
+
sub.unsubscribe();
|
|
13
|
+
if (value === empty) {
|
|
14
|
+
throw new Error('No value emitted');
|
|
15
|
+
}
|
|
16
|
+
return value;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { Observable } from "rxjs";
|
|
2
|
+
export interface ReadableSignalLike<T = unknown> extends Observable<T> {
|
|
3
|
+
get value(): T;
|
|
4
|
+
peek(): T;
|
|
5
|
+
/**
|
|
6
|
+
* @deprecated use `value` instead.
|
|
7
|
+
*/
|
|
8
|
+
get(): T;
|
|
9
|
+
}
|
|
10
|
+
export interface UnaryFunction<T, R> {
|
|
11
|
+
(source: T): R;
|
|
12
|
+
}
|
|
13
|
+
export type SignalOperatorFn<T, R> = UnaryFunction<ReadableSignalLike<T>, ReadableSignalLike<R>>;
|
|
14
|
+
export type MonoTypeSignalOperatorFn<T> = SignalOperatorFn<T, T>;
|
|
15
|
+
export type ObservableToSignalFn<T> = UnaryFunction<Observable<T>, ReadableSignalLike<T>>;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { ZodType } from "zod/v4";
|
|
2
|
+
import { Observable } from "rxjs";
|
|
3
|
+
import { Computed } from "../../signal/base";
|
|
4
|
+
type Options<T> = {
|
|
5
|
+
zodSchema?: ZodType<T>;
|
|
6
|
+
key: string;
|
|
7
|
+
userId?: string;
|
|
8
|
+
/**
|
|
9
|
+
* @deprecated use checkEffect instead
|
|
10
|
+
*/
|
|
11
|
+
validator$?: Observable<(value: T) => boolean>;
|
|
12
|
+
checkEffect?: (value: T) => boolean;
|
|
13
|
+
defaultValue: T;
|
|
14
|
+
};
|
|
15
|
+
export declare class LocalSignal<T = string | null | number | undefined> extends Computed<T> {
|
|
16
|
+
private _options;
|
|
17
|
+
static KEY_PREFIX: string;
|
|
18
|
+
static DRIVER: Storage;
|
|
19
|
+
private static _getStorageValue;
|
|
20
|
+
private static _setStorageValue;
|
|
21
|
+
constructor(_options: Options<T>);
|
|
22
|
+
protected _onChange(value: T): void;
|
|
23
|
+
}
|
|
24
|
+
export {};
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { signalize } from "../../signal/operators";
|
|
2
|
+
import { z } from "zod/v4";
|
|
3
|
+
import { Computed } from "../../signal/base";
|
|
4
|
+
const NullOrString = z.string().nullable();
|
|
5
|
+
const NONE = Symbol('NONE');
|
|
6
|
+
export class LocalSignal extends Computed {
|
|
7
|
+
_options;
|
|
8
|
+
static KEY_PREFIX = '__LSValue__';
|
|
9
|
+
static DRIVER = localStorage;
|
|
10
|
+
static _getStorageValue(options) {
|
|
11
|
+
const storageKey = `${LocalSignal.KEY_PREFIX}:${options.key}`;
|
|
12
|
+
const item = LocalSignal.DRIVER.getItem(storageKey);
|
|
13
|
+
if (!item)
|
|
14
|
+
return NONE;
|
|
15
|
+
const schema = z.record(z.string(), options.zodSchema || NullOrString);
|
|
16
|
+
const parsed = schema.safeParse(JSON.parse(item));
|
|
17
|
+
if (!parsed.success) {
|
|
18
|
+
console.warn(`Invalid value for key "${options.key}" in localStorage`, parsed.error);
|
|
19
|
+
return NONE;
|
|
20
|
+
}
|
|
21
|
+
const subKey = options.userId ? `user:${options.userId}` : 'common';
|
|
22
|
+
if (!(subKey in parsed.data)) {
|
|
23
|
+
return NONE;
|
|
24
|
+
}
|
|
25
|
+
return parsed.data[subKey];
|
|
26
|
+
}
|
|
27
|
+
static _setStorageValue(options, value) {
|
|
28
|
+
const storageKey = `${LocalSignal.KEY_PREFIX}:${options.key}`;
|
|
29
|
+
const item = LocalSignal.DRIVER.getItem(storageKey) || '{}';
|
|
30
|
+
const schema = z.record(z.string(), options.zodSchema || NullOrString);
|
|
31
|
+
const parsed = schema.safeParse(JSON.parse(item));
|
|
32
|
+
let data = parsed.data ?? {};
|
|
33
|
+
if (!parsed.success) {
|
|
34
|
+
data = {};
|
|
35
|
+
}
|
|
36
|
+
const subKey = options.userId ? `user:${options.userId}` : 'common';
|
|
37
|
+
data[subKey] = value;
|
|
38
|
+
LocalSignal.DRIVER.setItem(storageKey, JSON.stringify(data));
|
|
39
|
+
}
|
|
40
|
+
constructor(_options) {
|
|
41
|
+
const validator$ = _options.validator$;
|
|
42
|
+
const checkEffect = _options.checkEffect;
|
|
43
|
+
const defaultValue = _options.defaultValue;
|
|
44
|
+
const validatorSignal$ = validator$ && signalize(validator$);
|
|
45
|
+
super(() => {
|
|
46
|
+
const value = LocalSignal._getStorageValue(_options);
|
|
47
|
+
if (value === NONE)
|
|
48
|
+
return defaultValue;
|
|
49
|
+
if (validatorSignal$) {
|
|
50
|
+
const validator = validatorSignal$.value;
|
|
51
|
+
if (!validator(value)) {
|
|
52
|
+
return defaultValue;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
if (checkEffect) {
|
|
56
|
+
return checkEffect(value) ? value : defaultValue;
|
|
57
|
+
}
|
|
58
|
+
return value;
|
|
59
|
+
});
|
|
60
|
+
this._options = _options;
|
|
61
|
+
}
|
|
62
|
+
_onChange(value) {
|
|
63
|
+
LocalSignal._setStorageValue(this._options, value);
|
|
64
|
+
return super._onChange(value);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './LocalSignal';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './LocalSignal';
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { createOperatorSubscriber } from "rxjs/internal/operators/OperatorSubscriber";
|
|
2
|
+
import { ReadonlySignal, Batcher } from "../base";
|
|
3
|
+
export function batched() {
|
|
4
|
+
return (source) => {
|
|
5
|
+
let lockSubscription = null;
|
|
6
|
+
let lastResult = undefined;
|
|
7
|
+
return new ReadonlySignal((destination) => source.subscribe(createOperatorSubscriber(destination, (value) => {
|
|
8
|
+
if (lockSubscription) {
|
|
9
|
+
lastResult = value;
|
|
10
|
+
return;
|
|
11
|
+
}
|
|
12
|
+
if (Batcher.isLocked$.value) {
|
|
13
|
+
lastResult = value;
|
|
14
|
+
lockSubscription = Batcher.isLocked$.subscribe((isLocked) => {
|
|
15
|
+
if (!isLocked) {
|
|
16
|
+
lockSubscription.unsubscribe();
|
|
17
|
+
lockSubscription = null;
|
|
18
|
+
destination.next(lastResult);
|
|
19
|
+
lastResult = undefined;
|
|
20
|
+
}
|
|
21
|
+
});
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
destination.next(value);
|
|
25
|
+
})));
|
|
26
|
+
};
|
|
27
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { type TruthyTypesOf } from "rxjs";
|
|
2
|
+
import type { MonoTypeSignalOperatorFn, SignalOperatorFn } from "../base";
|
|
3
|
+
export declare function filterUpdates<T, S extends T>(predicate: (value: T, index: number) => value is S): SignalOperatorFn<T, S>;
|
|
4
|
+
export declare function filterUpdates<T>(predicate: BooleanConstructor): SignalOperatorFn<T, TruthyTypesOf<T>>;
|
|
5
|
+
export declare function filterUpdates<T>(predicate: (value: T, index: number) => boolean): MonoTypeSignalOperatorFn<T>;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { createOperatorSubscriber } from "rxjs/internal/operators/OperatorSubscriber";
|
|
2
|
+
import { ReadonlySignal } from "../../signal/base/ReadonlySignal";
|
|
3
|
+
export function filterUpdates(predicate, thisArg) {
|
|
4
|
+
return (source) => new ReadonlySignal((destination) => {
|
|
5
|
+
let index = 0;
|
|
6
|
+
let isFirst = true;
|
|
7
|
+
source.subscribe(createOperatorSubscriber(destination, (value) => {
|
|
8
|
+
if (isFirst) {
|
|
9
|
+
isFirst = false;
|
|
10
|
+
destination.next(value);
|
|
11
|
+
return;
|
|
12
|
+
}
|
|
13
|
+
const result = predicate.call(thisArg, value, index++);
|
|
14
|
+
if (result)
|
|
15
|
+
destination.next(value);
|
|
16
|
+
}));
|
|
17
|
+
});
|
|
18
|
+
}
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import type { SignalOperatorFn } from "../base";
|
|
2
|
+
export declare function mapSignals<T, R, A>(project: (this: A, value: T, index: number) => R): SignalOperatorFn<T, R>;
|
|
3
|
+
export declare function mapSignals<T, R, A>(project: (this: A, value: T, index: number) => R, thisArg: A): SignalOperatorFn<T, R>;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { createOperatorSubscriber } from "rxjs/internal/operators/OperatorSubscriber";
|
|
2
|
+
import { ReadonlySignal } from "../../signal/base/ReadonlySignal";
|
|
3
|
+
export function mapSignals(project, thisArg) {
|
|
4
|
+
return (source) => new ReadonlySignal((destination) => {
|
|
5
|
+
let index = 0;
|
|
6
|
+
source.subscribe(createOperatorSubscriber(destination, (value) => {
|
|
7
|
+
destination.next(project.call(thisArg, value, index++));
|
|
8
|
+
}));
|
|
9
|
+
});
|
|
10
|
+
}
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
# RxQuery
|
|
2
|
+
|
|
3
|
+
RxQuery - система для управления запросами и кэшированием данных в RxToolkit. Она состоит из двух основных компонентов: **Resources** и **Operations**.
|
|
4
|
+
|
|
5
|
+
## Основные концепции
|
|
6
|
+
|
|
7
|
+
### Resources (Ресурсы)
|
|
8
|
+
|
|
9
|
+
Resources предназначены для реактивного кэширования повторяемых запросов. Они автоматически управляют состоянием загрузки, кэшируют результаты и обеспечивают эффективную инвалидацию данных.
|
|
10
|
+
|
|
11
|
+
**Ключевые особенности:**
|
|
12
|
+
- Автоматическое кэширование по аргументам запроса
|
|
13
|
+
- Поддержка AbortController для отмены запросов
|
|
14
|
+
- Реактивные обновления состояния
|
|
15
|
+
- Оптимистичные обновления
|
|
16
|
+
|
|
17
|
+
### Operations (Операции)
|
|
18
|
+
|
|
19
|
+
Operations представляют одноразовые операции или мутации. Они не кэшируются, но предоставляют состояние выполнения и могут связываться с ресурсами для их обновления.
|
|
20
|
+
|
|
21
|
+
**Ключевые особенности:**
|
|
22
|
+
- Отслеживание состояния выполнения
|
|
23
|
+
- Связывание с ресурсами для их обновления
|
|
24
|
+
- Поддержка оптимистичных обновлений
|
|
25
|
+
- Возможность блокировки связанных ресурсов
|
|
26
|
+
|
|
27
|
+
### Agents (Агенты)
|
|
28
|
+
Agents представляют собой интеллектуальные обертки над ресурсами (или операциями),
|
|
29
|
+
которые обеспечивают более удобную работу с состояниями запросов для потребителей.
|
|
30
|
+
|
|
31
|
+
#### Основная проблема, которую решают агенты
|
|
32
|
+
|
|
33
|
+
Кэш ресурсов (или операций) содержит "сырые" состояния отдельных запросов, но потребителям нужна более высокоуровневая логика.
|
|
34
|
+
Например:
|
|
35
|
+
- `isInitialLoading` должно быть true только при первой загрузке ресурса, но не при переключении между разными аргументами
|
|
36
|
+
- При смене аргументов запроса нужно показывать данные предыдущего запроса, пока загружается новый
|
|
37
|
+
- Состояние загрузки должно отражать контекст использования, а не просто состояние кэша
|
|
38
|
+
|
|
39
|
+
## API
|
|
40
|
+
|
|
41
|
+
### createResource
|
|
42
|
+
|
|
43
|
+
Создает новый ресурс для кэширования данных.
|
|
44
|
+
|
|
45
|
+
```typescript
|
|
46
|
+
const userResource = createResource<{ id: string }, User>({
|
|
47
|
+
async queryFn(args, tools) {
|
|
48
|
+
const response = await fetch(`/api/users/${args.id}`, {
|
|
49
|
+
signal: tools.abortSignal
|
|
50
|
+
});
|
|
51
|
+
return response.json();
|
|
52
|
+
},
|
|
53
|
+
select: (data) => ({
|
|
54
|
+
id: data.id,
|
|
55
|
+
name: data.name,
|
|
56
|
+
email: data.email
|
|
57
|
+
})
|
|
58
|
+
});
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
**Параметры:**
|
|
62
|
+
- `queryFn(args, tools)` — функция выполнения запроса
|
|
63
|
+
- `select(data)` — опциональная функция трансформации данных
|
|
64
|
+
|
|
65
|
+
### createOperation
|
|
66
|
+
|
|
67
|
+
Создает новую операцию для выполнения мутаций.
|
|
68
|
+
|
|
69
|
+
```typescript
|
|
70
|
+
const updateUser = createOperation<
|
|
71
|
+
{ id: string; data: Partial<User> },
|
|
72
|
+
User
|
|
73
|
+
>({
|
|
74
|
+
async queryFn(args) {
|
|
75
|
+
const response = await fetch(`/api/users/${args.id}`, {
|
|
76
|
+
method: 'PATCH',
|
|
77
|
+
headers: { 'Content-Type': 'application/json' },
|
|
78
|
+
body: JSON.stringify(args.data)
|
|
79
|
+
});
|
|
80
|
+
return response.json();
|
|
81
|
+
},
|
|
82
|
+
link(add) {
|
|
83
|
+
add({
|
|
84
|
+
resource: userResource,
|
|
85
|
+
forwardArgs: (args) => ({ id: args.id }),
|
|
86
|
+
optimisticUpdate(draft, args) {
|
|
87
|
+
Object.assign(draft, args.data);
|
|
88
|
+
},
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
**Параметры:**
|
|
95
|
+
- `queryFn(args)` — функция выполнения операции
|
|
96
|
+
- `link(ref)` — опциональная функция связывания с ресурсами
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
## Свойства Link
|
|
100
|
+
```typescript
|
|
101
|
+
/**
|
|
102
|
+
* Настройки связи операции с ресурсом
|
|
103
|
+
*/
|
|
104
|
+
export type LinkOptions<D extends OperationDefinition, RD extends ResourceDefinition> = {
|
|
105
|
+
/**
|
|
106
|
+
* Целевой ресурс, с которым связывается операция
|
|
107
|
+
* @required
|
|
108
|
+
*/
|
|
109
|
+
resource: ResourceInstance<RD>;
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Функция для получения аргументов ресурса из аргументов операции
|
|
113
|
+
* Используется для определения какой именно элемент в кэше ресурса нужно обновить
|
|
114
|
+
* @required
|
|
115
|
+
*/
|
|
116
|
+
forwardArgs: (args: D["Args"]) => RD["Args"];
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Флаг для инвалидации (очистки) кэша ресурса после выполнения операции
|
|
120
|
+
* При true - кэш будет очищен и ресурс будет перезагружен при следующем обращении
|
|
121
|
+
* @optional @default false
|
|
122
|
+
*/
|
|
123
|
+
invalidate?: boolean;
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Флаг для блокировки ресурса во время выполнения операции
|
|
127
|
+
* При true - ресурс будет заблокирован и не сможет выполнять новые запросы
|
|
128
|
+
* @optional @default false
|
|
129
|
+
*/
|
|
130
|
+
lock?: boolean;
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Функция для обновления кэша ресурса после успешного выполнения операции
|
|
134
|
+
* Получает draft объект для мутации, аргументы операции и результат операции
|
|
135
|
+
* @optional
|
|
136
|
+
*/
|
|
137
|
+
update?: (tools: {
|
|
138
|
+
/** Immer draft объект для мутации кэша ресурса */
|
|
139
|
+
draft: RD["Data"];
|
|
140
|
+
/** Аргументы, с которыми была вызвана операция */
|
|
141
|
+
args: D["Args"];
|
|
142
|
+
/** Результат выполнения операции */
|
|
143
|
+
data: D["Data"];
|
|
144
|
+
}) => void | RD["Data"] | Promise<RD["Data"]>;
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Функция для оптимистичного обновления кэша ресурса ДО выполнения операции
|
|
148
|
+
* Позволяет обновить UI немедленно, до получения ответа от сервера
|
|
149
|
+
* @optional
|
|
150
|
+
*/
|
|
151
|
+
optimisticUpdate?: (tools: {
|
|
152
|
+
/** Immer draft объект для мутации кэша ресурса */
|
|
153
|
+
draft: RD["Data"];
|
|
154
|
+
/** Аргументы, с которыми была вызвана операция */
|
|
155
|
+
args: D["Args"];
|
|
156
|
+
}) => void | RD["Data"] | Promise<D["Data"]>;
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Функция для создания нового элемента в кэше ресурса
|
|
160
|
+
* Используется когда операция создает новую сущность, которую нужно добавить в кэш
|
|
161
|
+
* @optional
|
|
162
|
+
*/
|
|
163
|
+
create?: (tools: {
|
|
164
|
+
/** Аргументы, с которыми была вызвана операция */
|
|
165
|
+
args: D["Args"];
|
|
166
|
+
/** Результат выполнения операции */
|
|
167
|
+
data: D["Data"];
|
|
168
|
+
}) => RD["Data"] | Promise<RD["Data"]>;
|
|
169
|
+
};
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
## Состояния запросов
|
|
174
|
+
|
|
175
|
+
Каждый ресурс и операция предоставляют следующие состояния:
|
|
176
|
+
|
|
177
|
+
```typescript
|
|
178
|
+
/**
|
|
179
|
+
* Состояние запроса ресурса
|
|
180
|
+
*/
|
|
181
|
+
export type ResourceQueryState<D extends ResourceDefinition> = {
|
|
182
|
+
/** Инициализирован ли хотя бы один запрос */
|
|
183
|
+
isInitiated: boolean;
|
|
184
|
+
/** Первая загрузка */
|
|
185
|
+
isLoading: boolean;
|
|
186
|
+
/** Завершен ли запрос */
|
|
187
|
+
isDone: boolean;
|
|
188
|
+
/** Успешно ли завершен последний запрос (false по умолчанию) */
|
|
189
|
+
isSuccess: boolean;
|
|
190
|
+
/** Произошла ли ошибка последнего запроса (false по умолчанию) */
|
|
191
|
+
isError: boolean;
|
|
192
|
+
/** Заблокирован ли ресурс */
|
|
193
|
+
isLocked: boolean;
|
|
194
|
+
/** Перезагружается ли ресурс */
|
|
195
|
+
isReloading: boolean;
|
|
196
|
+
/** Оригинал ошибки, если есть */
|
|
197
|
+
error: unknown | undefined;
|
|
198
|
+
/** Данные, полученные в результате запроса (или select данных) */
|
|
199
|
+
data: D["Data"] | undefined;
|
|
200
|
+
/** Аргументы запроса */
|
|
201
|
+
args: D["Args"] | undefined; // TODO undefined - это костыль для сведения типов, его быть не должно
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Состояние выполнения операции
|
|
206
|
+
*/
|
|
207
|
+
export type OperationQueryState<D extends OperationDefinition> = {
|
|
208
|
+
/** Выполняется ли операция в данный момент */
|
|
209
|
+
isLoading: boolean;
|
|
210
|
+
/** Завершена ли операция */
|
|
211
|
+
isDone: boolean;
|
|
212
|
+
/** Успешно ли завершена операция (false по умолчанию) */
|
|
213
|
+
isSuccess: boolean;
|
|
214
|
+
/** Произошла ли ошибка при выполнении операции (false по умолчанию) */
|
|
215
|
+
isError: boolean;
|
|
216
|
+
/** Оригинал ошибки, если есть */
|
|
217
|
+
error: unknown | undefined;
|
|
218
|
+
/** Результат выполнения операции */
|
|
219
|
+
data: D["Data"] | undefined;
|
|
220
|
+
/** Аргументы операции */
|
|
221
|
+
args: D["Args"];
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
```
|