@angular-architects/ngrx-toolkit 0.0.7 → 0.1.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.
Files changed (62) hide show
  1. package/.eslintrc.json +43 -0
  2. package/jest.config.ts +22 -0
  3. package/ng-package.json +7 -0
  4. package/package.json +3 -16
  5. package/project.json +37 -0
  6. package/{index.d.ts → src/index.ts} +3 -2
  7. package/src/lib/assertions/assertions.ts +9 -0
  8. package/src/lib/redux-connector/create-redux.ts +94 -0
  9. package/{lib/redux-connector/index.d.ts → src/lib/redux-connector/index.ts} +1 -0
  10. package/src/lib/redux-connector/model.ts +67 -0
  11. package/{lib/redux-connector/rxjs-interop/index.d.ts → src/lib/redux-connector/rxjs-interop/index.ts} +1 -0
  12. package/src/lib/redux-connector/rxjs-interop/redux-method.ts +61 -0
  13. package/src/lib/redux-connector/signal-redux-store.ts +54 -0
  14. package/src/lib/redux-connector/util.ts +22 -0
  15. package/src/lib/shared/empty.ts +2 -0
  16. package/src/lib/with-call-state.spec.ts +24 -0
  17. package/src/lib/with-call-state.ts +136 -0
  18. package/src/lib/with-data-service.ts +312 -0
  19. package/src/lib/with-devtools.spec.ts +157 -0
  20. package/src/lib/with-devtools.ts +128 -0
  21. package/src/lib/with-redux.spec.ts +100 -0
  22. package/src/lib/with-redux.ts +261 -0
  23. package/src/lib/with-storage-sync.spec.ts +237 -0
  24. package/src/lib/with-storage-sync.ts +160 -0
  25. package/src/lib/with-undo-redo.ts +184 -0
  26. package/src/test-setup.ts +8 -0
  27. package/tsconfig.json +29 -0
  28. package/tsconfig.lib.json +17 -0
  29. package/tsconfig.lib.prod.json +9 -0
  30. package/tsconfig.spec.json +16 -0
  31. package/esm2022/angular-architects-ngrx-toolkit.mjs +0 -5
  32. package/esm2022/index.mjs +0 -9
  33. package/esm2022/lib/assertions/assertions.mjs +0 -6
  34. package/esm2022/lib/redux-connector/create-redux.mjs +0 -41
  35. package/esm2022/lib/redux-connector/index.mjs +0 -2
  36. package/esm2022/lib/redux-connector/model.mjs +0 -2
  37. package/esm2022/lib/redux-connector/rxjs-interop/index.mjs +0 -2
  38. package/esm2022/lib/redux-connector/rxjs-interop/redux-method.mjs +0 -22
  39. package/esm2022/lib/redux-connector/signal-redux-store.mjs +0 -43
  40. package/esm2022/lib/redux-connector/util.mjs +0 -13
  41. package/esm2022/lib/shared/empty.mjs +0 -2
  42. package/esm2022/lib/with-call-state.mjs +0 -58
  43. package/esm2022/lib/with-data-service.mjs +0 -161
  44. package/esm2022/lib/with-devtools.mjs +0 -79
  45. package/esm2022/lib/with-redux.mjs +0 -95
  46. package/esm2022/lib/with-storage-sync.mjs +0 -56
  47. package/esm2022/lib/with-undo-redo.mjs +0 -93
  48. package/fesm2022/angular-architects-ngrx-toolkit.mjs +0 -653
  49. package/fesm2022/angular-architects-ngrx-toolkit.mjs.map +0 -1
  50. package/lib/assertions/assertions.d.ts +0 -2
  51. package/lib/redux-connector/create-redux.d.ts +0 -13
  52. package/lib/redux-connector/model.d.ts +0 -36
  53. package/lib/redux-connector/rxjs-interop/redux-method.d.ts +0 -11
  54. package/lib/redux-connector/signal-redux-store.d.ts +0 -11
  55. package/lib/redux-connector/util.d.ts +0 -5
  56. package/lib/shared/empty.d.ts +0 -1
  57. package/lib/with-call-state.d.ts +0 -56
  58. package/lib/with-data-service.d.ts +0 -115
  59. package/lib/with-devtools.d.ts +0 -32
  60. package/lib/with-redux.d.ts +0 -57
  61. package/lib/with-storage-sync.d.ts +0 -58
  62. package/lib/with-undo-redo.d.ts +0 -55
package/.eslintrc.json ADDED
@@ -0,0 +1,43 @@
1
+ {
2
+ "extends": ["../../.eslintrc.json"],
3
+ "ignorePatterns": ["!**/*"],
4
+ "overrides": [
5
+ {
6
+ "files": ["*.ts"],
7
+ "extends": [
8
+ "plugin:@nx/angular",
9
+ "plugin:@angular-eslint/template/process-inline-templates"
10
+ ],
11
+ "rules": {
12
+ "@angular-eslint/directive-selector": [
13
+ "error",
14
+ {
15
+ "type": "attribute",
16
+ "prefix": "ngrxToolkit",
17
+ "style": "camelCase"
18
+ }
19
+ ],
20
+ "@angular-eslint/component-selector": [
21
+ "error",
22
+ {
23
+ "type": "element",
24
+ "prefix": "ngrx-toolkit",
25
+ "style": "kebab-case"
26
+ }
27
+ ]
28
+ }
29
+ },
30
+ {
31
+ "files": ["*.html"],
32
+ "extends": ["plugin:@nx/angular-template"],
33
+ "rules": {}
34
+ },
35
+ {
36
+ "files": ["*.json"],
37
+ "parser": "jsonc-eslint-parser",
38
+ "rules": {
39
+ "@nx/dependency-checks": "error"
40
+ }
41
+ }
42
+ ]
43
+ }
package/jest.config.ts ADDED
@@ -0,0 +1,22 @@
1
+ /* eslint-disable */
2
+ export default {
3
+ displayName: 'ngrx-toolkit',
4
+ preset: '../../jest.preset.js',
5
+ setupFilesAfterEnv: ['<rootDir>/src/test-setup.ts'],
6
+ coverageDirectory: '../../coverage/libs/ngrx-toolkit',
7
+ transform: {
8
+ '^.+\\.(ts|mjs|js|html)$': [
9
+ 'jest-preset-angular',
10
+ {
11
+ tsconfig: '<rootDir>/tsconfig.spec.json',
12
+ stringifyContentPathRegex: '\\.(html|svg)$',
13
+ },
14
+ ],
15
+ },
16
+ transformIgnorePatterns: ['node_modules/(?!.*\\.mjs$)'],
17
+ snapshotSerializers: [
18
+ 'jest-preset-angular/build/serializers/no-ng-attributes',
19
+ 'jest-preset-angular/build/serializers/ng-snapshot',
20
+ 'jest-preset-angular/build/serializers/html-comment',
21
+ ],
22
+ };
@@ -0,0 +1,7 @@
1
+ {
2
+ "$schema": "../../node_modules/ng-packagr/ng-package.schema.json",
3
+ "dest": "../../dist/libs/ngrx-toolkit",
4
+ "lib": {
5
+ "entryFile": "src/index.ts"
6
+ }
7
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@angular-architects/ngrx-toolkit",
3
- "version": "0.0.7",
3
+ "version": "0.1.0",
4
4
  "license": "MIT",
5
5
  "repository": {
6
6
  "type": "GitHub",
@@ -14,18 +14,5 @@
14
14
  "dependencies": {
15
15
  "tslib": "^2.3.0"
16
16
  },
17
- "sideEffects": false,
18
- "module": "fesm2022/angular-architects-ngrx-toolkit.mjs",
19
- "typings": "index.d.ts",
20
- "exports": {
21
- "./package.json": {
22
- "default": "./package.json"
23
- },
24
- ".": {
25
- "types": "./index.d.ts",
26
- "esm2022": "./esm2022/angular-architects-ngrx-toolkit.mjs",
27
- "esm": "./esm2022/angular-architects-ngrx-toolkit.mjs",
28
- "default": "./fesm2022/angular-architects-ngrx-toolkit.mjs"
29
- }
30
- }
31
- }
17
+ "sideEffects": false
18
+ }
package/project.json ADDED
@@ -0,0 +1,37 @@
1
+ {
2
+ "name": "ngrx-toolkit",
3
+ "$schema": "../../node_modules/nx/schemas/project-schema.json",
4
+ "sourceRoot": "libs/ngrx-toolkit/src",
5
+ "prefix": "ngrx-toolkit",
6
+ "tags": [],
7
+ "projectType": "library",
8
+ "targets": {
9
+ "build": {
10
+ "executor": "@nx/angular:package",
11
+ "outputs": ["{workspaceRoot}/dist/{projectRoot}"],
12
+ "options": {
13
+ "project": "libs/ngrx-toolkit/ng-package.json"
14
+ },
15
+ "configurations": {
16
+ "production": {
17
+ "tsConfig": "libs/ngrx-toolkit/tsconfig.lib.prod.json"
18
+ },
19
+ "development": {
20
+ "tsConfig": "libs/ngrx-toolkit/tsconfig.lib.json"
21
+ }
22
+ },
23
+ "defaultConfiguration": "production"
24
+ },
25
+ "test": {
26
+ "executor": "@nx/jest:jest",
27
+ "outputs": ["{workspaceRoot}/coverage/{projectRoot}"],
28
+ "options": {
29
+ "jestConfig": "libs/ngrx-toolkit/jest.config.ts"
30
+ }
31
+ },
32
+ "lint": {
33
+ "executor": "@nx/eslint:lint",
34
+ "outputs": ["{options.outputFile}"]
35
+ }
36
+ }
37
+ }
@@ -1,8 +1,9 @@
1
1
  export { withDevtools, patchState, Action } from './lib/with-devtools';
2
2
  export * from './lib/with-redux';
3
+
3
4
  export * from './lib/with-call-state';
4
5
  export * from './lib/with-undo-redo';
5
- export * from './lib/with-data-service';
6
+ export * from './lib/with-data-service'
6
7
  export { withStorageSync, SyncConfig } from './lib/with-storage-sync';
7
8
  export * from './lib/redux-connector';
8
- export * from './lib/redux-connector/rxjs-interop';
9
+ export * from './lib/redux-connector/rxjs-interop';
@@ -0,0 +1,9 @@
1
+ import { ActionsFnSpecs } from '../with-redux';
2
+
3
+ export function assertActionFnSpecs(
4
+ obj: unknown
5
+ ): asserts obj is ActionsFnSpecs {
6
+ if (!obj || typeof obj !== 'object') {
7
+ throw new Error('%o is not an Action Specification');
8
+ }
9
+ }
@@ -0,0 +1,94 @@
1
+ import { ENVIRONMENT_INITIALIZER, inject, makeEnvironmentProviders } from "@angular/core";
2
+ import { ActionCreator, ActionType } from "@ngrx/store/src/models";
3
+ import { CreateReduxState, ExtractActionTypes, MapperTypes, Store } from "./model";
4
+ import { SignalReduxStore, injectReduxDispatch } from "./signal-redux-store";
5
+ import { capitalize, isActionCreator } from "./util";
6
+
7
+
8
+ export function mapAction<
9
+ Creators extends readonly ActionCreator[]
10
+ >(
11
+ ...args: [
12
+ ...creators: Creators,
13
+ storeMethod: (action: ActionType<Creators[number]>) => unknown
14
+ ]
15
+ ): MapperTypes<Creators>;
16
+ export function mapAction<
17
+ Creators extends readonly ActionCreator[],
18
+ T
19
+ >(
20
+ ...args: [
21
+ ...creators: Creators,
22
+ storeMethod: (action: ActionType<Creators[number]>, resultMethod: (input: T) => unknown) => unknown,
23
+ resultMethod: (input: T) => unknown
24
+ ]
25
+ ): MapperTypes<Creators>;
26
+ export function mapAction<
27
+ Creators extends readonly ActionCreator[]
28
+ >(
29
+ ...args: [
30
+ ...creators: Creators,
31
+ storeMethod: (action: ActionType<Creators[number]>) => unknown,
32
+ resultMethod?: (input: unknown) => unknown
33
+ ]
34
+ ): MapperTypes<Creators> {
35
+ let resultMethod = args.pop() as unknown as ((input: unknown) => unknown ) | undefined;
36
+ let storeMethod = args.pop() as unknown as (action: ActionType<Creators[number]>) => unknown;
37
+
38
+ if (isActionCreator(storeMethod)) {
39
+ args.push(storeMethod);
40
+ storeMethod = resultMethod || storeMethod;
41
+ resultMethod = undefined;
42
+ }
43
+
44
+ const types = (args as unknown as Creators).map(
45
+ (creator) => creator.type
46
+ ) as unknown as ExtractActionTypes<Creators>;
47
+
48
+ return {
49
+ types,
50
+ storeMethod,
51
+ resultMethod
52
+ };
53
+ }
54
+
55
+ export function withActionMappers(
56
+ ...mappers: MapperTypes<ActionCreator<any, any>[]>[]
57
+ ): MapperTypes<ActionCreator<any, any>[]>[] {
58
+ return mappers;
59
+ }
60
+
61
+ export function createReduxState<
62
+ StoreName extends string,
63
+ STORE extends Store
64
+ >(
65
+ storeName: StoreName,
66
+ signalStore: STORE,
67
+ withActionMappers: (store: InstanceType<STORE>) => MapperTypes<ActionCreator<any, any>[]>[],
68
+ ): CreateReduxState<StoreName, STORE> {
69
+ const isRootProvider = (signalStore as any)?.ɵprov?.providedIn === 'root';
70
+ return {
71
+ [`provide${capitalize(storeName)}Store`]: (connectReduxDevtools = false) => makeEnvironmentProviders([
72
+ isRootProvider? [] : signalStore,
73
+ {
74
+ provide: ENVIRONMENT_INITIALIZER,
75
+ multi: true,
76
+ useFactory: (
77
+ signalReduxStore = inject(SignalReduxStore),
78
+ store = inject(signalStore)
79
+ ) => () => {
80
+ if (connectReduxDevtools) {
81
+ // addStoreToReduxDevtools(store, storeName, false);
82
+ }
83
+ signalReduxStore.connectFeatureStore(
84
+ withActionMappers(store)
85
+ );
86
+ }
87
+ }
88
+ ]),
89
+ [`inject${capitalize(storeName)}Store`]: () => Object.assign(
90
+ inject(signalStore),
91
+ { dispatch: injectReduxDispatch() }
92
+ )
93
+ } as CreateReduxState<StoreName, STORE>;
94
+ }
@@ -1 +1,2 @@
1
+
1
2
  export { createReduxState, mapAction, withActionMappers } from './create-redux';
@@ -0,0 +1,67 @@
1
+ import { EnvironmentProviders, Signal, Type } from "@angular/core";
2
+ import { DeepSignal } from "@ngrx/signals/src/deep-signal";
3
+ import { SignalStoreFeatureResult } from "@ngrx/signals/src/signal-store-models";
4
+ import { StateSignal } from "@ngrx/signals/src/state-signal";
5
+ import { Action, ActionCreator, ActionType, Prettify } from "@ngrx/store/src/models";
6
+ import { Observable, Unsubscribable } from "rxjs";
7
+
8
+
9
+ export type IncludePropType<T, V, WithNevers =
10
+ { [K in keyof T]: Exclude<T[K], undefined> extends V
11
+ ? T[K] extends Record<string, unknown>
12
+ ? IncludePropType<T[K], V>
13
+ : T[K]
14
+ : never
15
+ }> = Prettify<
16
+ Pick<
17
+ WithNevers,
18
+ { [K in keyof WithNevers]: WithNevers[K] extends never
19
+ ? never
20
+ : K extends string
21
+ ? K
22
+ : never
23
+ }[keyof WithNevers]
24
+ >
25
+ >;
26
+
27
+ export type Store = Type<Record<string, unknown> & StateSignal<SignalStoreFeatureResult['state']>>;
28
+
29
+ export type CreateReduxState<
30
+ StoreName extends string,
31
+ STORE extends Store
32
+ > = {
33
+ [K in StoreName as `provide${Capitalize<K>}Store`]: (connectReduxDevtools?: boolean) => EnvironmentProviders
34
+ } & {
35
+ [K in StoreName as `inject${Capitalize<K>}Store`]: () => InjectableReduxSlice<STORE>
36
+ };
37
+
38
+ export type Selectors<STORE extends Store> = IncludePropType<InstanceType<STORE>, Signal<unknown> | DeepSignal<unknown>>;
39
+ export type Dispatch = { dispatch: (input: Action | Observable<Action> | Signal<Action>) => Unsubscribable };
40
+ export type InjectableReduxSlice<STORE extends Store> = Selectors<STORE> & Dispatch;
41
+
42
+ export type ExtractActionTypes<Creators extends readonly ActionCreator[]> = {
43
+ [Key in keyof Creators]: Creators[Key] extends ActionCreator<infer T>
44
+ ? T
45
+ : never;
46
+ };
47
+
48
+ export interface ActionMethod<T, V extends Action = Action> {
49
+ (action: V): T;
50
+ }
51
+
52
+ export interface StoreMethod<
53
+ Creators extends readonly ActionCreator[],
54
+ ResultState = unknown,
55
+ > {
56
+ (
57
+ action: ActionType<Creators[number]>
58
+ ): ResultState;
59
+ }
60
+
61
+ export interface MapperTypes<
62
+ Creators extends readonly ActionCreator[]
63
+ > {
64
+ types: ExtractActionTypes<Creators>,
65
+ storeMethod: StoreMethod<Creators>,
66
+ resultMethod?: (...args: unknown[]) => unknown
67
+ }
@@ -1 +1,2 @@
1
+
1
2
  export { reduxMethod } from './redux-method';
@@ -0,0 +1,61 @@
1
+ import { Injector, Signal, inject } from "@angular/core";
2
+ import { rxMethod } from "@ngrx/signals/rxjs-interop";
3
+ import { Observable, Unsubscribable, map, pipe } from "rxjs";
4
+
5
+
6
+ type RxMethodInput<Input> = Input | Observable<Input> | Signal<Input>;
7
+
8
+ type RxMethod<Input, MethodInput = Input, MethodResult = unknown> = ((
9
+ input: RxMethodInput<Input>,
10
+ resultMethod: (input: MethodInput) => MethodResult
11
+ ) => Unsubscribable) & Unsubscribable;
12
+
13
+ export function reduxMethod<Input, MethodInput = Input>(
14
+ generator: (source$: Observable<Input>) => Observable<MethodInput>,
15
+ config?: { injector?: Injector }
16
+ ): RxMethod<Input, MethodInput>;
17
+ export function reduxMethod<Input, MethodInput = Input, MethodResult = unknown>(
18
+ generator: (source$: Observable<Input>) => Observable<MethodInput>,
19
+ resultMethod: (input: MethodInput) => MethodResult,
20
+ config?: {
21
+ injector?: Injector
22
+ }
23
+ ): RxMethod<Input, MethodInput, MethodResult>;
24
+ export function reduxMethod<Input, MethodInput = Input, MethodResult = unknown>(
25
+ generator: (source$: Observable<Input>) => Observable<MethodInput>,
26
+ resultMethodOrConfig?: ((input: MethodInput) => MethodResult) | {
27
+ injector?: Injector
28
+ },
29
+ config?: {
30
+ injector?: Injector
31
+ }
32
+ ): RxMethod<Input, MethodInput, MethodResult> {
33
+ const injector = inject(Injector);
34
+
35
+ if (typeof resultMethodOrConfig === 'function') {
36
+ let unsubscribable: Unsubscribable;
37
+ const inputResultFn = ((
38
+ input: RxMethodInput<Input>,
39
+ resultMethod = resultMethodOrConfig
40
+ ) => {
41
+
42
+ const rxMethodWithResult = rxMethod<Input>(pipe(
43
+ generator,
44
+ map(resultMethod)
45
+ ), {
46
+ ...(config || {}),
47
+ injector: config?.injector || injector
48
+ });
49
+ const rxWithInput = rxMethodWithResult(input);
50
+ unsubscribable = { unsubscribe: rxWithInput.unsubscribe.bind(rxWithInput) };
51
+
52
+ return rxWithInput;
53
+ }) as RxMethod<Input, MethodInput, MethodResult>;
54
+
55
+ inputResultFn.unsubscribe = () => unsubscribable?.unsubscribe();
56
+
57
+ return inputResultFn;
58
+ }
59
+
60
+ return rxMethod<Input>(generator, resultMethodOrConfig);
61
+ }
@@ -0,0 +1,54 @@
1
+ import { Injectable, inject } from "@angular/core";
2
+ import { rxMethod } from "@ngrx/signals/rxjs-interop";
3
+ import { Action, ActionCreator } from "@ngrx/store";
4
+ import { pipe, tap } from "rxjs";
5
+ import { MapperTypes } from "./model";
6
+ import { isUnsubscribable } from "./util";
7
+
8
+
9
+ @Injectable({
10
+ providedIn: 'root'
11
+ })
12
+ export class SignalReduxStore {
13
+ private mapperDict: Record<string, {
14
+ storeMethod: (...args: unknown[]) => unknown,
15
+ resultMethod?: (...args: unknown[]) => unknown,
16
+ }> = {};
17
+
18
+ dispatch = rxMethod<Action>(pipe(
19
+ tap((action: Action) => {
20
+ const callbacks = this.mapperDict[action.type];
21
+ if (callbacks?.storeMethod) {
22
+ if (
23
+ isUnsubscribable(callbacks.storeMethod) &&
24
+ callbacks.resultMethod
25
+ ) {
26
+ return callbacks.storeMethod(action, (a: Action) => {
27
+ const resultAction = callbacks.resultMethod?.(a) as Action;
28
+ this.dispatch(resultAction);
29
+ });
30
+ }
31
+
32
+ return callbacks?.storeMethod(action);
33
+ }
34
+
35
+ return;
36
+ })
37
+ ));
38
+
39
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
40
+ connectFeatureStore(mappers: MapperTypes<ActionCreator<any, any>[]>[]): void {
41
+ mappers.forEach(
42
+ mapper => mapper.types.forEach(
43
+ action => this.mapperDict[action] = {
44
+ storeMethod: mapper.storeMethod,
45
+ resultMethod: mapper.resultMethod
46
+ }
47
+ )
48
+ );
49
+ }
50
+ }
51
+
52
+ export function injectReduxDispatch() {
53
+ return inject(SignalReduxStore).dispatch;
54
+ }
@@ -0,0 +1,22 @@
1
+ import { Action } from '@ngrx/store';
2
+ import { Unsubscribable } from 'rxjs';
3
+
4
+
5
+ export function isUnsubscribable<F extends (...args: unknown[]) => unknown>(
6
+ fn: F | (F & Unsubscribable)
7
+ ): fn is F & Unsubscribable {
8
+ return !!(fn as any as F & Unsubscribable)?.unsubscribe;
9
+ }
10
+
11
+ export function capitalize(str: string): string {
12
+ return str ? str[0].toUpperCase() + str.substring(1) : str;
13
+ }
14
+
15
+ export function isActionCreator(action: any): action is Action {
16
+ return (
17
+ typeof action === 'function' &&
18
+ action &&
19
+ action.type &&
20
+ typeof action.type === 'string'
21
+ );
22
+ }
@@ -0,0 +1,2 @@
1
+ // eslint-disable-next-line @typescript-eslint/ban-types
2
+ export type Emtpy = {};
@@ -0,0 +1,24 @@
1
+ import { patchState, signalStore } from '@ngrx/signals';
2
+ import { setLoaded, setLoading, withCallState } from 'ngrx-toolkit';
3
+
4
+ describe('withCallState', () => {
5
+ it('should use and update a callState', () => {
6
+ const DataStore = signalStore(withCallState());
7
+ const dataStore = new DataStore();
8
+
9
+ patchState(dataStore, setLoading());
10
+
11
+ expect(dataStore.callState()).toBe('loading');
12
+ expect(dataStore.loading()).toBe(true);
13
+ });
14
+
15
+ it('should use the callState for a collection', () => {
16
+ const DataStore = signalStore(withCallState({ collection: 'entities' }));
17
+ const dataStore = new DataStore();
18
+
19
+ patchState(dataStore, setLoaded('entities'));
20
+
21
+ expect(dataStore.entitiesCallState()).toBe('loaded');
22
+ expect(dataStore.entitiesLoaded()).toBe(true);
23
+ });
24
+ });
@@ -0,0 +1,136 @@
1
+ import { Signal, computed } from '@angular/core';
2
+ import {
3
+ SignalStoreFeature,
4
+ signalStoreFeature,
5
+ withComputed,
6
+ withState,
7
+ } from '@ngrx/signals';
8
+ import { Emtpy } from './shared/empty';
9
+
10
+ export type CallState = 'init' | 'loading' | 'loaded' | { error: string };
11
+
12
+ export type NamedCallStateSlice<Collection extends string> = {
13
+ [K in Collection as `${K}CallState`]: CallState;
14
+ };
15
+
16
+ export type CallStateSlice = {
17
+ callState: CallState
18
+ }
19
+
20
+ export type NamedCallStateSignals<Prop extends string> = {
21
+ [K in Prop as `${K}Loading`]: Signal<boolean>;
22
+ } & {
23
+ [K in Prop as `${K}Loaded`]: Signal<boolean>;
24
+ } & {
25
+ [K in Prop as `${K}Error`]: Signal<string | null>;
26
+ }
27
+
28
+ export type CallStateSignals = {
29
+ loading: Signal<boolean>;
30
+ loaded: Signal<boolean>;
31
+ error: Signal<string | null>
32
+ }
33
+
34
+ export type SetCallState<Prop extends string | undefined> = Prop extends string
35
+ ? NamedCallStateSlice<Prop>
36
+ : CallStateSlice;
37
+
38
+ export function getCallStateKeys(config?: { collection?: string }) {
39
+ const prop = config?.collection;
40
+ return {
41
+ callStateKey: prop ? `${config.collection}CallState` : 'callState',
42
+ loadingKey: prop ? `${config.collection}Loading` : 'loading',
43
+ loadedKey: prop ? `${config.collection}Loaded` : 'loaded',
44
+ errorKey: prop ? `${config.collection}Error` : 'error',
45
+ };
46
+ }
47
+
48
+ export function withCallState<Collection extends string>(config: {
49
+ collection: Collection;
50
+ }): SignalStoreFeature<
51
+ { state: Emtpy, signals: Emtpy, methods: Emtpy },
52
+ {
53
+ state: NamedCallStateSlice<Collection>,
54
+ signals: NamedCallStateSignals<Collection>,
55
+ methods: Emtpy
56
+ }
57
+ >;
58
+ export function withCallState(): SignalStoreFeature<
59
+ { state: Emtpy, signals: Emtpy, methods: Emtpy },
60
+ {
61
+ state: CallStateSlice,
62
+ signals: CallStateSignals,
63
+ methods: Emtpy
64
+ }
65
+ >;
66
+ export function withCallState<Collection extends string>(config?: {
67
+ collection: Collection;
68
+ }): SignalStoreFeature {
69
+ const { callStateKey, errorKey, loadedKey, loadingKey } =
70
+ getCallStateKeys(config);
71
+
72
+ return signalStoreFeature(
73
+ withState({ [callStateKey]: 'init' }),
74
+ withComputed((state: Record<string, Signal<unknown>>) => {
75
+
76
+ const callState = state[callStateKey] as Signal<CallState>;
77
+
78
+ return {
79
+ [loadingKey]: computed(() => callState() === 'loading'),
80
+ [loadedKey]: computed(() => callState() === 'loaded'),
81
+ [errorKey]: computed(() => {
82
+ const v = callState();
83
+ return typeof v === 'object' ? v.error : null;
84
+ })
85
+ }
86
+ })
87
+ );
88
+ }
89
+
90
+ export function setLoading<Prop extends string | undefined = undefined>(
91
+ prop?: Prop
92
+ ): SetCallState<Prop> {
93
+ if (prop) {
94
+ return { [`${prop}CallState`]: 'loading' } as SetCallState<Prop>;
95
+ }
96
+
97
+ return { callState: 'loading' } as SetCallState<Prop>;
98
+ }
99
+
100
+ export function setLoaded<Prop extends string | undefined = undefined>(
101
+ prop?: Prop
102
+ ): SetCallState<Prop> {
103
+
104
+ if (prop) {
105
+ return { [`${prop}CallState`]: 'loaded' } as SetCallState<Prop>;
106
+ }
107
+ else {
108
+ return { callState: 'loaded' } as SetCallState<Prop>;
109
+ }
110
+ }
111
+
112
+ export function setError<Prop extends string | undefined = undefined>(
113
+ error: unknown,
114
+ prop?: Prop,
115
+ ): SetCallState<Prop> {
116
+
117
+ let errorMessage = '';
118
+
119
+ if (!error) {
120
+ errorMessage = '';
121
+ }
122
+ else if (typeof error === 'object' && 'message' in error) {
123
+ errorMessage = String(error.message);
124
+ }
125
+ else {
126
+ errorMessage = String(error);
127
+ }
128
+
129
+
130
+ if (prop) {
131
+ return { [`${prop}CallState`]: { error: errorMessage } } as SetCallState<Prop>;
132
+ }
133
+ else {
134
+ return { callState: { error: errorMessage } } as SetCallState<Prop>;
135
+ }
136
+ }