@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,9 @@
|
|
|
1
|
+
import { OperationAgentInstanse, OperationDefinition, OperationQueryState } from "../types/Operation.types";
|
|
2
|
+
import { Prettify } from "../types/shared.types";
|
|
3
|
+
type WithAgent<D extends OperationDefinition> = {
|
|
4
|
+
createAgent: () => OperationAgentInstanse<D>;
|
|
5
|
+
};
|
|
6
|
+
type TriggerFn<D extends OperationDefinition> = (args: D['Args']) => Promise<D['Data']>;
|
|
7
|
+
type Result<D extends OperationDefinition> = [TriggerFn<D>, Prettify<OperationQueryState<D>>];
|
|
8
|
+
export declare function useOperationAgent<D extends OperationDefinition>(op: WithAgent<D>): Result<D>;
|
|
9
|
+
export {};
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { useConstant, useEventHandler, useSignal } from "../../react-hooks";
|
|
2
|
+
export function useOperationAgent(op) {
|
|
3
|
+
const agent = useConstant(() => op.createAgent());
|
|
4
|
+
const state = useSignal(agent.state$);
|
|
5
|
+
const trigger = useEventHandler((args) => {
|
|
6
|
+
agent.initiate(args);
|
|
7
|
+
return new Promise((resolve, reject) => {
|
|
8
|
+
const sub = agent.state$.subscribe((s) => {
|
|
9
|
+
if (s.isDone && !s.isLoading) {
|
|
10
|
+
sub.unsubscribe();
|
|
11
|
+
if (s.isSuccess) {
|
|
12
|
+
resolve(s.data);
|
|
13
|
+
}
|
|
14
|
+
else {
|
|
15
|
+
reject(s.error);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
});
|
|
19
|
+
});
|
|
20
|
+
});
|
|
21
|
+
return [trigger, state];
|
|
22
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { ResourceAgentInstance, ResourceDefinition, ResourceQueryState } from "../types/Resource.types";
|
|
2
|
+
import { Prettify } from "../types/shared.types";
|
|
3
|
+
import { SKIP } from "../SKIP_TOKEN";
|
|
4
|
+
type WithAgent<D extends ResourceDefinition> = {
|
|
5
|
+
createAgent: () => ResourceAgentInstance<D>;
|
|
6
|
+
};
|
|
7
|
+
type Result<D extends ResourceDefinition> = Prettify<ResourceQueryState<D>>;
|
|
8
|
+
export declare function useResourceAgent<D extends ResourceDefinition>(res: WithAgent<D>, ...argss: D['Args'] extends void ? [] | [typeof SKIP] : [D['Args'] | typeof SKIP]): Result<D>;
|
|
9
|
+
export {};
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { useConstant, useSignal } from "../../react-hooks";
|
|
3
|
+
import { shallowEqual } from "../lib/shallowEqual";
|
|
4
|
+
import { SKIP } from "../SKIP_TOKEN";
|
|
5
|
+
export function useResourceAgent(res, ...argss) {
|
|
6
|
+
const args = (argss[0] === SKIP ? SKIP : argss[0]);
|
|
7
|
+
const agent = useConstant(() => {
|
|
8
|
+
const agent = res.createAgent();
|
|
9
|
+
if (args !== SKIP) {
|
|
10
|
+
agent.initiate(args);
|
|
11
|
+
}
|
|
12
|
+
return agent;
|
|
13
|
+
});
|
|
14
|
+
React.useEffect(() => {
|
|
15
|
+
if (args === SKIP) {
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
const state = agent.state$.peek();
|
|
19
|
+
if (state.isInitiated && shallowEqual(args, state.args)) {
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
agent.initiate(args);
|
|
23
|
+
}, [args]);
|
|
24
|
+
return useSignal(agent.state$);
|
|
25
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { ReadableSignalLike } from "../../signal";
|
|
2
|
+
import { FallbackOnNever } from "../../query/types/shared.types";
|
|
3
|
+
import { ResourceDefinition, ResourceInstance } from "./Resource.types";
|
|
4
|
+
/**
|
|
5
|
+
* Функция создания операции
|
|
6
|
+
*/
|
|
7
|
+
export type OperationCreateFn<ARGS, RESULT, SELECTED = never> = (options: OperationCreateOptions<OperationDefinition<ARGS, RESULT, SELECTED>>) => OperationInstance<OperationDefinition<ARGS, RESULT, SELECTED>>;
|
|
8
|
+
/**
|
|
9
|
+
* Опции создания операции
|
|
10
|
+
*/
|
|
11
|
+
export type OperationCreateOptions<D extends OperationDefinition> = {
|
|
12
|
+
/** Функция селектора для преобразования результата операции */
|
|
13
|
+
select?: (data: D["Result"]) => D["Selected"];
|
|
14
|
+
/** Функция выполнения операции */
|
|
15
|
+
queryFn: (args: D["Args"]) => Promise<D["Result"]>;
|
|
16
|
+
/** Связанные ресурсы */
|
|
17
|
+
link?: (link: <RD extends ResourceDefinition>(options: LinkOptions<D, RD>) => void) => void;
|
|
18
|
+
};
|
|
19
|
+
/**
|
|
20
|
+
* Настройки связи операции с ресурсом
|
|
21
|
+
*/
|
|
22
|
+
export type LinkOptions<D extends OperationDefinition, RD extends ResourceDefinition> = {
|
|
23
|
+
resource: ResourceInstance<RD>;
|
|
24
|
+
forwardArgs: (args: D["Args"]) => RD["Args"];
|
|
25
|
+
invalidate?: boolean;
|
|
26
|
+
lock?: boolean;
|
|
27
|
+
update?: (tools: {
|
|
28
|
+
draft: RD["Data"];
|
|
29
|
+
args: D["Args"];
|
|
30
|
+
data: D["Data"];
|
|
31
|
+
}) => void | RD["Data"] | Promise<RD["Data"]>;
|
|
32
|
+
optimisticUpdate?: (tools: {
|
|
33
|
+
draft: RD["Data"];
|
|
34
|
+
args: D["Args"];
|
|
35
|
+
}) => void | RD["Data"] | Promise<D["Data"]>;
|
|
36
|
+
create?: (tools: {
|
|
37
|
+
args: D["Args"];
|
|
38
|
+
data: D["Data"];
|
|
39
|
+
}) => RD["Data"] | Promise<RD["Data"]>;
|
|
40
|
+
};
|
|
41
|
+
/**
|
|
42
|
+
* Определение типов операции
|
|
43
|
+
*/
|
|
44
|
+
export type OperationDefinition<A = any, R = any, S = any> = {
|
|
45
|
+
Args: A;
|
|
46
|
+
Result: R;
|
|
47
|
+
Selected: S;
|
|
48
|
+
Data: FallbackOnNever<S, R>;
|
|
49
|
+
};
|
|
50
|
+
/**
|
|
51
|
+
* Экземпляр операции
|
|
52
|
+
*/
|
|
53
|
+
export type OperationInstance<D extends OperationDefinition> = {
|
|
54
|
+
/** Создает агента для выполнения операции */
|
|
55
|
+
createAgent(): OperationAgentInstanse<D>;
|
|
56
|
+
/**
|
|
57
|
+
* Выполняет операцию с указанными аргументами
|
|
58
|
+
* @deprecated
|
|
59
|
+
*/
|
|
60
|
+
mutate: (args: D["Args"]) => Promise<D["Data"]>;
|
|
61
|
+
};
|
|
62
|
+
/**
|
|
63
|
+
* Агент для выполнения операции
|
|
64
|
+
*/
|
|
65
|
+
export type OperationAgentInstanse<D extends OperationDefinition> = {
|
|
66
|
+
/** Observable состояния выполнения операции */
|
|
67
|
+
state$: ReadableSignalLike<OperationQueryState<D>>;
|
|
68
|
+
/** Инициирует выполнение операции с указанными аргументами */
|
|
69
|
+
initiate(args: D["Args"]): void;
|
|
70
|
+
/** Создает новый агент операции */
|
|
71
|
+
createAgent(): OperationAgentInstanse<D>;
|
|
72
|
+
};
|
|
73
|
+
/**
|
|
74
|
+
* Состояние выполнения операции
|
|
75
|
+
*/
|
|
76
|
+
export type OperationQueryState<D extends OperationDefinition> = {
|
|
77
|
+
/** Выполняется ли операция в данный момент */
|
|
78
|
+
isLoading: boolean;
|
|
79
|
+
/** Завершена ли операция */
|
|
80
|
+
isDone: boolean;
|
|
81
|
+
/** Успешно ли завершена операция (false по умолчанию) */
|
|
82
|
+
isSuccess: boolean;
|
|
83
|
+
/** Произошла ли ошибка при выполнении операции (false по умолчанию) */
|
|
84
|
+
isError: boolean;
|
|
85
|
+
/** Оригинал ошибки, если есть */
|
|
86
|
+
error: unknown | undefined;
|
|
87
|
+
/** Результат выполнения операции */
|
|
88
|
+
data: D["Data"] | undefined;
|
|
89
|
+
/** Аргументы операции */
|
|
90
|
+
args: D["Args"];
|
|
91
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { ReadableSignalLike } from "../../signal";
|
|
2
|
+
import { FallbackOnNever } from "../../query/types/shared.types";
|
|
3
|
+
/**
|
|
4
|
+
* Функция создания ресурса
|
|
5
|
+
*/
|
|
6
|
+
export type ResourceCreateFn<ARGS, RESULT, SELECTED = never> = (options: ResourceCreateOptions<ResourceDefinition<ARGS, RESULT, SELECTED>>) => ResourceInstance<ResourceDefinition<ARGS, RESULT, SELECTED>>;
|
|
7
|
+
/**
|
|
8
|
+
* Опции создания ресурса
|
|
9
|
+
*/
|
|
10
|
+
export type ResourceCreateOptions<D extends ResourceDefinition> = {
|
|
11
|
+
/** Функция селектора для преобразования данных */
|
|
12
|
+
select?: (data: D["Result"]) => D["Selected"];
|
|
13
|
+
/** Функция запроса данных */
|
|
14
|
+
queryFn: (args: D["Args"], tools: ResourceQueryFnTools) => Promise<D["Result"]>;
|
|
15
|
+
};
|
|
16
|
+
/**
|
|
17
|
+
* Определение типов ресурса
|
|
18
|
+
*/
|
|
19
|
+
export type ResourceDefinition<A = any, R = any, S = any> = {
|
|
20
|
+
Args: A;
|
|
21
|
+
Result: R;
|
|
22
|
+
Selected: S;
|
|
23
|
+
Data: FallbackOnNever<S, R>;
|
|
24
|
+
};
|
|
25
|
+
/** Инструменты для функции запроса */
|
|
26
|
+
export type ResourceQueryFnTools = {
|
|
27
|
+
/** Сигнал для отмены запроса */
|
|
28
|
+
abortSignal?: AbortSignal;
|
|
29
|
+
};
|
|
30
|
+
/**
|
|
31
|
+
* Экземпляр ресурса
|
|
32
|
+
*/
|
|
33
|
+
export type ResourceInstance<D extends ResourceDefinition> = {
|
|
34
|
+
/** Создает агента для работы с ресурсом */
|
|
35
|
+
createAgent(): ResourceAgentInstance<D>;
|
|
36
|
+
/** Создает ссылку на ресурс с указанными аргументами */
|
|
37
|
+
createRef(args: D["Args"]): ResourceRefInstanse<D>;
|
|
38
|
+
};
|
|
39
|
+
/**
|
|
40
|
+
* Агент для работы с ресурсом
|
|
41
|
+
*/
|
|
42
|
+
export type ResourceAgentInstance<D extends ResourceDefinition> = {
|
|
43
|
+
/** Observable состояния запроса */
|
|
44
|
+
state$: ReadableSignalLike<ResourceQueryState<D>>;
|
|
45
|
+
/** Инициирует запрос с указанными аргументами */
|
|
46
|
+
initiate(args: D["Args"], force?: boolean): void;
|
|
47
|
+
/** Создает новый агент */
|
|
48
|
+
createAgent(): ResourceAgentInstance<D>;
|
|
49
|
+
};
|
|
50
|
+
/**
|
|
51
|
+
* Состояние запроса ресурса
|
|
52
|
+
*/
|
|
53
|
+
export type ResourceQueryState<D extends ResourceDefinition> = {
|
|
54
|
+
/** Инициализирован ли хотя бы один запрос */
|
|
55
|
+
isInitiated: boolean;
|
|
56
|
+
/** Первая загрузка */
|
|
57
|
+
isLoading: boolean;
|
|
58
|
+
/** Завершен ли запрос */
|
|
59
|
+
isDone: boolean;
|
|
60
|
+
/** Успешно ли завершен последний запрос (false по умолчанию) */
|
|
61
|
+
isSuccess: boolean;
|
|
62
|
+
/** Произошла ли ошибка последнего запроса (false по умолчанию) */
|
|
63
|
+
isError: boolean;
|
|
64
|
+
/** Заблокирован ли ресурс */
|
|
65
|
+
isLocked: boolean;
|
|
66
|
+
/** Перезагружается ли ресурс */
|
|
67
|
+
isReloading: boolean;
|
|
68
|
+
/** Оригинал ошибки, если есть */
|
|
69
|
+
error: unknown | undefined;
|
|
70
|
+
/** Данные, полученные в результате запроса (или select данных) */
|
|
71
|
+
data: D["Data"] | undefined;
|
|
72
|
+
/** Аргументы запроса */
|
|
73
|
+
args: D["Args"] | undefined;
|
|
74
|
+
};
|
|
75
|
+
/**
|
|
76
|
+
* Эте не ссылка в "классическом" понимании, а абстракция
|
|
77
|
+
* для работы с элементом кеша ресурса.
|
|
78
|
+
*/
|
|
79
|
+
export type ResourceRefInstanse<D extends ResourceDefinition> = {
|
|
80
|
+
get has(): boolean;
|
|
81
|
+
lock(): {
|
|
82
|
+
unlock: () => void;
|
|
83
|
+
};
|
|
84
|
+
unlockOne(): void;
|
|
85
|
+
update(updateFn: (data: D['Data']) => D['Data']): {
|
|
86
|
+
rollback: () => void;
|
|
87
|
+
};
|
|
88
|
+
invalidate(): void;
|
|
89
|
+
create(data: D['Data']): void;
|
|
90
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
/**
|
|
3
|
+
* Hook like useMemo, but without unloading.
|
|
4
|
+
*/
|
|
5
|
+
export function useConstant(fn, deps = []) {
|
|
6
|
+
const ref = React.useRef(null);
|
|
7
|
+
const depsRef = React.useRef(null);
|
|
8
|
+
if (ref.current === null) {
|
|
9
|
+
ref.current = { v: fn() };
|
|
10
|
+
}
|
|
11
|
+
if (depsRef.current === null) {
|
|
12
|
+
depsRef.current = deps;
|
|
13
|
+
}
|
|
14
|
+
if (depsRef.current.some((d, i) => d !== deps[i])) {
|
|
15
|
+
ref.current = { v: fn() };
|
|
16
|
+
depsRef.current = deps;
|
|
17
|
+
}
|
|
18
|
+
return ref.current.v;
|
|
19
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function useEventHandler<T extends (...args: any[]) => any>(fn: T): T;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { Observable } from 'rxjs';
|
|
2
|
+
/**
|
|
3
|
+
* Hook for automatically subscribing and unsubscribing from an Observable.
|
|
4
|
+
* If the Observable synchronously returns a value, it returns it.
|
|
5
|
+
* Else returns initialValue.
|
|
6
|
+
* If the value of the Observable changes, it resubscribes.
|
|
7
|
+
* If a value equal to the previous one arrives in the Observable,
|
|
8
|
+
* the previous value is returned without triggering a re-render.
|
|
9
|
+
* @param input$ Observable.
|
|
10
|
+
* @param initialValue Initial value that will be used before the Observable emits a value.
|
|
11
|
+
*/
|
|
12
|
+
export declare function useObservable<T>(input$: Observable<T>, initialValue: T): T;
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { useEventHandler } from "../react-hooks/useEventHandler";
|
|
3
|
+
import { BehaviorSubject } from 'rxjs';
|
|
4
|
+
import { useConstant } from "../react-hooks/useConstant";
|
|
5
|
+
const NONE = Symbol('NONE');
|
|
6
|
+
/**
|
|
7
|
+
* Hook for automatically subscribing and unsubscribing from an Observable.
|
|
8
|
+
* If the Observable synchronously returns a value, it returns it.
|
|
9
|
+
* Else returns initialValue.
|
|
10
|
+
* If the value of the Observable changes, it resubscribes.
|
|
11
|
+
* If a value equal to the previous one arrives in the Observable,
|
|
12
|
+
* the previous value is returned without triggering a re-render.
|
|
13
|
+
* @param input$ Observable.
|
|
14
|
+
* @param initialValue Initial value that will be used before the Observable emits a value.
|
|
15
|
+
*/
|
|
16
|
+
export function useObservable(input$, initialValue) {
|
|
17
|
+
const { subject$, subscription } = useConstant(() => {
|
|
18
|
+
const subject = new BehaviorSubject(NONE);
|
|
19
|
+
/**
|
|
20
|
+
* Check if the Observable synchronously returns a value,
|
|
21
|
+
* like BehaviorSubject or etc.
|
|
22
|
+
*/
|
|
23
|
+
const subscription = input$
|
|
24
|
+
.subscribe((value) => subject.next(value));
|
|
25
|
+
return {
|
|
26
|
+
subject$: subject,
|
|
27
|
+
subscription,
|
|
28
|
+
};
|
|
29
|
+
}, [input$]);
|
|
30
|
+
React.useEffect(() => {
|
|
31
|
+
return () => {
|
|
32
|
+
subscription.unsubscribe();
|
|
33
|
+
subject$.complete();
|
|
34
|
+
};
|
|
35
|
+
}, [subject$]);
|
|
36
|
+
const subscribe = React.useCallback((updateStore) => {
|
|
37
|
+
const subjectSubscription = subject$.subscribe(updateStore);
|
|
38
|
+
return () => {
|
|
39
|
+
subjectSubscription.unsubscribe();
|
|
40
|
+
};
|
|
41
|
+
}, [input$, subject$]);
|
|
42
|
+
const getSnapshot = useEventHandler(() => subject$.getValue());
|
|
43
|
+
let value = React.useSyncExternalStore(subscribe, getSnapshot);
|
|
44
|
+
if (value === NONE) {
|
|
45
|
+
value = initialValue;
|
|
46
|
+
}
|
|
47
|
+
return value;
|
|
48
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { useEventHandler } from "../react-hooks";
|
|
3
|
+
export function useSignal(signal$) {
|
|
4
|
+
const subscribe = React.useCallback((update) => {
|
|
5
|
+
const subscription = signal$.subscribe(update);
|
|
6
|
+
return () => {
|
|
7
|
+
subscription.unsubscribe();
|
|
8
|
+
};
|
|
9
|
+
}, [signal$]);
|
|
10
|
+
const getSnapshot = useEventHandler(() => signal$.peek());
|
|
11
|
+
return React.useSyncExternalStore(subscribe, getSnapshot);
|
|
12
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { Observable } from 'rxjs';
|
|
2
|
+
declare const NONE: unique symbol;
|
|
3
|
+
/**
|
|
4
|
+
* Hook for automatically subscribing and unsubscribing from an Observable.
|
|
5
|
+
* If the Observable synchronously returns a value, it returns it.
|
|
6
|
+
* Else if initialValue is provided, it returns it.
|
|
7
|
+
* Else (if initialValue is NONE and the Observable does not synchronously return a value),
|
|
8
|
+
* throws error.
|
|
9
|
+
* If the value of the Observable changes, it resubscribes.
|
|
10
|
+
* If a value equal to the previous one arrives in the Observable,
|
|
11
|
+
* the previous value is returned without triggering a re-render.
|
|
12
|
+
* @param input$ Observable.
|
|
13
|
+
* @param initialValue Initial value that will be used before the Observable emits a value.
|
|
14
|
+
*/
|
|
15
|
+
export declare function useSyncObservable<T>(input$: Observable<T>, initialValue?: T | typeof NONE): T;
|
|
16
|
+
export {};
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { BehaviorSubject } from 'rxjs';
|
|
3
|
+
import { useConstant, useEventHandler } from "../react-hooks";
|
|
4
|
+
const NONE = Symbol('NONE');
|
|
5
|
+
/**
|
|
6
|
+
* Hook for automatically subscribing and unsubscribing from an Observable.
|
|
7
|
+
* If the Observable synchronously returns a value, it returns it.
|
|
8
|
+
* Else if initialValue is provided, it returns it.
|
|
9
|
+
* Else (if initialValue is NONE and the Observable does not synchronously return a value),
|
|
10
|
+
* throws error.
|
|
11
|
+
* If the value of the Observable changes, it resubscribes.
|
|
12
|
+
* If a value equal to the previous one arrives in the Observable,
|
|
13
|
+
* the previous value is returned without triggering a re-render.
|
|
14
|
+
* @param input$ Observable.
|
|
15
|
+
* @param initialValue Initial value that will be used before the Observable emits a value.
|
|
16
|
+
*/
|
|
17
|
+
export function useSyncObservable(input$, initialValue = NONE) {
|
|
18
|
+
const { subject$, subscription } = useConstant(() => {
|
|
19
|
+
const subject = new BehaviorSubject(NONE);
|
|
20
|
+
/**
|
|
21
|
+
* Check if the Observable synchronously returns a value,
|
|
22
|
+
* like BehaviorSubject or etc.
|
|
23
|
+
*/
|
|
24
|
+
const subscription = input$
|
|
25
|
+
.subscribe((value) => subject.next(value));
|
|
26
|
+
return {
|
|
27
|
+
subject$: subject,
|
|
28
|
+
subscription,
|
|
29
|
+
};
|
|
30
|
+
}, [input$]);
|
|
31
|
+
React.useEffect(() => {
|
|
32
|
+
return () => {
|
|
33
|
+
subscription.unsubscribe();
|
|
34
|
+
subject$.complete();
|
|
35
|
+
};
|
|
36
|
+
}, [subject$]);
|
|
37
|
+
const subscribe = React.useCallback((updateStore) => {
|
|
38
|
+
const subjectSubscription = subject$.subscribe(updateStore);
|
|
39
|
+
return () => {
|
|
40
|
+
subjectSubscription.unsubscribe();
|
|
41
|
+
};
|
|
42
|
+
}, [input$, subject$]);
|
|
43
|
+
const getSnapshot = useEventHandler(() => subject$.getValue());
|
|
44
|
+
let value = React.useSyncExternalStore(subscribe, getSnapshot);
|
|
45
|
+
if (value === NONE) {
|
|
46
|
+
value = initialValue;
|
|
47
|
+
}
|
|
48
|
+
if (value === NONE) {
|
|
49
|
+
throw new Error('Observable did not return a value and no initial value provided');
|
|
50
|
+
}
|
|
51
|
+
return value;
|
|
52
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { BehaviorSubject } from "rxjs";
|
|
2
|
+
export const Batcher = {
|
|
3
|
+
isLocked$: new BehaviorSubject(false),
|
|
4
|
+
lock() {
|
|
5
|
+
if (this.isLocked$.value)
|
|
6
|
+
return;
|
|
7
|
+
this.isLocked$.next(true);
|
|
8
|
+
},
|
|
9
|
+
unlock() {
|
|
10
|
+
if (!this.isLocked$.value)
|
|
11
|
+
return;
|
|
12
|
+
this.isLocked$.next(false);
|
|
13
|
+
},
|
|
14
|
+
batch(fn) {
|
|
15
|
+
if (this.isLocked$.value)
|
|
16
|
+
return fn();
|
|
17
|
+
this.lock();
|
|
18
|
+
fn();
|
|
19
|
+
this.unlock();
|
|
20
|
+
},
|
|
21
|
+
};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { SubscriptionLike } from "rxjs";
|
|
2
|
+
import { ReadableSignalLike } from "../../signal/base/types";
|
|
3
|
+
import { Signal } from "./Signal";
|
|
4
|
+
export declare class Computed<T> extends Signal<T> implements SubscriptionLike, ReadableSignalLike<T> {
|
|
5
|
+
private static _EMPTY;
|
|
6
|
+
private _effect;
|
|
7
|
+
constructor(computeFn: () => T, doLog?: boolean);
|
|
8
|
+
unsubscribe(): void;
|
|
9
|
+
complete(): void;
|
|
10
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { Signal } from "./Signal";
|
|
2
|
+
import { Effect } from "./Effect";
|
|
3
|
+
export class Computed extends Signal {
|
|
4
|
+
static _EMPTY = Symbol('empty');
|
|
5
|
+
_effect;
|
|
6
|
+
constructor(computeFn, doLog = false) {
|
|
7
|
+
let initialValue = Computed._EMPTY;
|
|
8
|
+
const effect = new Effect(() => {
|
|
9
|
+
if (initialValue === Computed._EMPTY) {
|
|
10
|
+
initialValue = computeFn();
|
|
11
|
+
return;
|
|
12
|
+
}
|
|
13
|
+
this.value = computeFn();
|
|
14
|
+
}, doLog);
|
|
15
|
+
super(initialValue);
|
|
16
|
+
this._effect = effect;
|
|
17
|
+
}
|
|
18
|
+
unsubscribe() {
|
|
19
|
+
this._effect.unsubscribe();
|
|
20
|
+
super.unsubscribe();
|
|
21
|
+
}
|
|
22
|
+
complete() {
|
|
23
|
+
this._effect.unsubscribe();
|
|
24
|
+
super.complete();
|
|
25
|
+
}
|
|
26
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { SubscriptionLike } from "rxjs";
|
|
2
|
+
export declare class Effect implements SubscriptionLike {
|
|
3
|
+
private _doLog;
|
|
4
|
+
closed: boolean;
|
|
5
|
+
private _subscriptions;
|
|
6
|
+
constructor(effectFn: (runInTrackedContext: (fn: () => void) => void) => void, _doLog?: boolean);
|
|
7
|
+
/**
|
|
8
|
+
* Выполняет функцию в tracked-контексте, подписываясь на Tracker.
|
|
9
|
+
*/
|
|
10
|
+
private _runInTrackedContext;
|
|
11
|
+
unsubscribe(): void;
|
|
12
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { Batcher } from "../../signal/base/Batcher";
|
|
2
|
+
import { Tracker } from "./Tracker";
|
|
3
|
+
export class Effect {
|
|
4
|
+
_doLog;
|
|
5
|
+
closed = false;
|
|
6
|
+
_subscriptions = [];
|
|
7
|
+
constructor(effectFn, _doLog = false) {
|
|
8
|
+
this._doLog = _doLog;
|
|
9
|
+
if (this._doLog)
|
|
10
|
+
console.log("Run EffectFn. Reason: init");
|
|
11
|
+
this._runInTrackedContext(effectFn, false);
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Выполняет функцию в tracked-контексте, подписываясь на Tracker.
|
|
15
|
+
*/
|
|
16
|
+
_runInTrackedContext(effectFn, doUnsubscribe = true) {
|
|
17
|
+
// Отписываемся от предыдущих подписок
|
|
18
|
+
if (doUnsubscribe) {
|
|
19
|
+
this._subscriptions.forEach((sub) => sub.unsubscribe());
|
|
20
|
+
this._subscriptions = [];
|
|
21
|
+
}
|
|
22
|
+
let isTrackedContext = true;
|
|
23
|
+
let isWaitingBatching = false;
|
|
24
|
+
// Подписываемся на Tracker. Во время выполнения подпишемся на все tracked наблюдатели.
|
|
25
|
+
const trackerSub = Tracker.tracked$.subscribe((tracked$) => {
|
|
26
|
+
if (!isTrackedContext)
|
|
27
|
+
return;
|
|
28
|
+
this._subscriptions.push(tracked$.subscribe(() => {
|
|
29
|
+
if (isTrackedContext) {
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
if (isWaitingBatching) {
|
|
33
|
+
if (this._doLog)
|
|
34
|
+
console.log("Effect: still waiting for batching to finish", { tracked$ });
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
if (Batcher.isLocked$.value) {
|
|
38
|
+
// console.log("Effect: waiting for batching to finish");
|
|
39
|
+
isWaitingBatching = true;
|
|
40
|
+
const sub = Batcher.isLocked$
|
|
41
|
+
.subscribe((isLocked) => {
|
|
42
|
+
if (isLocked)
|
|
43
|
+
return;
|
|
44
|
+
sub.unsubscribe();
|
|
45
|
+
isWaitingBatching = false;
|
|
46
|
+
if (this._doLog)
|
|
47
|
+
console.log("Run EffectFn. Reason: tracked observable change after batching", { tracked$ });
|
|
48
|
+
this._runInTrackedContext(effectFn);
|
|
49
|
+
});
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
if (this._doLog)
|
|
53
|
+
console.log("Run EffectFn. Reason: tracked observable change", { tracked$ });
|
|
54
|
+
this._runInTrackedContext(effectFn);
|
|
55
|
+
}));
|
|
56
|
+
});
|
|
57
|
+
effectFn((fn) => {
|
|
58
|
+
if (this._doLog)
|
|
59
|
+
console.log("Run EffectFn. Reason: manual trigger sub context");
|
|
60
|
+
this._runInTrackedContext(fn, false);
|
|
61
|
+
});
|
|
62
|
+
trackerSub.unsubscribe();
|
|
63
|
+
isTrackedContext = false;
|
|
64
|
+
}
|
|
65
|
+
unsubscribe() {
|
|
66
|
+
this.closed = true;
|
|
67
|
+
this._subscriptions.forEach((sub) => sub.unsubscribe());
|
|
68
|
+
}
|
|
69
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { Observable, Subscriber, TeardownLogic } from "rxjs";
|
|
2
|
+
import { SyncObservable } from "./SyncObservable";
|
|
3
|
+
import type { ReadableSignalLike } from "./types";
|
|
4
|
+
export declare class ReadonlySignal<T> extends SyncObservable<T> implements ReadableSignalLike<T> {
|
|
5
|
+
constructor(subscribe?: (this: Observable<T>, subscriber: Subscriber<T>) => TeardownLogic);
|
|
6
|
+
get value(): T;
|
|
7
|
+
peek(): T;
|
|
8
|
+
/**
|
|
9
|
+
* @deprecated
|
|
10
|
+
*/
|
|
11
|
+
get(): T;
|
|
12
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { SyncObservable } from "./SyncObservable";
|
|
2
|
+
import { Tracker } from "./Tracker";
|
|
3
|
+
export class ReadonlySignal extends SyncObservable {
|
|
4
|
+
constructor(subscribe) {
|
|
5
|
+
super(subscribe);
|
|
6
|
+
}
|
|
7
|
+
get value() {
|
|
8
|
+
Tracker.next(this);
|
|
9
|
+
return super.value;
|
|
10
|
+
}
|
|
11
|
+
peek() {
|
|
12
|
+
return super.value;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* @deprecated
|
|
16
|
+
*/
|
|
17
|
+
get() {
|
|
18
|
+
return this.value;
|
|
19
|
+
}
|
|
20
|
+
}
|