dantian 0.0.2-beta.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/.eslintignore ADDED
@@ -0,0 +1 @@
1
+ .eslintrc.cjs
package/.eslintrc.cjs ADDED
@@ -0,0 +1,28 @@
1
+ module.exports = {
2
+ env: {
3
+ browser: true,
4
+ es2021: true,
5
+ },
6
+ extends: [
7
+ 'standard-with-typescript',
8
+ 'plugin:react/recommended',
9
+ 'plugin:storybook/recommended',
10
+ 'prettier',
11
+ ],
12
+ plugins: ['prettier'],
13
+ overrides: [],
14
+ parserOptions: {
15
+ ecmaVersion: 'latest',
16
+ sourceType: 'module',
17
+ },
18
+ plugins: ['react'],
19
+ rules: {
20
+ '@typescript-eslint/semi': 'off',
21
+ '@typescript-eslint/explicit-function-return-type': 'off',
22
+ },
23
+ settings: {
24
+ react: {
25
+ version: 'detect',
26
+ },
27
+ },
28
+ };
@@ -0,0 +1,74 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: [main, master]
6
+ pull_request:
7
+ branches: [main, master]
8
+
9
+ jobs:
10
+ ci-checks-and-publish:
11
+ timeout-minutes: 60
12
+ runs-on: ubuntu-latest
13
+
14
+ steps:
15
+ - uses: actions/checkout@v3
16
+ - name: Configure Git
17
+ run: |
18
+ git config --global user.name "JuliusKoronciCH"
19
+ git config --global user.email "julius.koronci@gmail.com"
20
+ - uses: actions/checkout@v3
21
+ - name: Set up Node.js
22
+ uses: actions/setup-node@v3
23
+ with:
24
+ node-version: 20
25
+ registry-url: https://registry.npmjs.org/
26
+
27
+ - name: Install Dependencies
28
+ run: corepack enable
29
+ if: ${{ !steps.cache.outputs.cache-hit }}
30
+
31
+ - name: Cache Dependencies (Yarn Berry)
32
+ uses: actions/cache@v3
33
+ with:
34
+ path: ~/.yarn/cache # Cache location for Yarn Berry
35
+ key: ${{ runner.os }}-yarn-berry-${{ hashFiles('**/yarn.lock') }}
36
+ restore-keys: |
37
+ ${{ runner.os }}-yarn-berry-
38
+
39
+ - name: Install Dependencies
40
+ run: yarn install --immutable
41
+
42
+ - name: Install Playwright Browsers
43
+ run: yarn playwright install --with-deps
44
+
45
+ - name: Run Linters
46
+ run: yarn lint
47
+
48
+ - name: Run Prettier
49
+ run: yarn format
50
+
51
+ - name: Run Tests
52
+ run: yarn test
53
+
54
+ - name: Start Storybook
55
+ run: yarn storybook &
56
+
57
+ - name: Run Playwright tests
58
+ run: yarn playwright test
59
+
60
+ - uses: actions/upload-artifact@v3
61
+ if: always()
62
+ with:
63
+ name: playwright-report
64
+ path: playwright-report/
65
+ retention-days: 30
66
+
67
+ - name: Publish to npm (on merge to main)
68
+ if: ${{ github.event_name == 'push' && github.ref == 'refs/heads/main' }}
69
+ run: |
70
+ yarn standard-version
71
+ npm publish --tag beta
72
+ env:
73
+ NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
74
+ GITHUB_TOKEN: ${{ secrets.CHANGELOG_TOKEN }}
package/.prettierrc ADDED
@@ -0,0 +1,6 @@
1
+ {
2
+ "singleQuote": true,
3
+ "trailingComma": "all",
4
+ "tabWidth": 2,
5
+ "semi": true
6
+ }
@@ -0,0 +1,18 @@
1
+ import type { StorybookConfig } from '@storybook/react-vite';
2
+
3
+ const config: StorybookConfig = {
4
+ stories: ['../stories/**/*.stories.@(js|jsx|mjs|ts|tsx)'],
5
+ addons: [
6
+ '@storybook/addon-links',
7
+ '@storybook/addon-essentials',
8
+ '@storybook/addon-interactions',
9
+ ],
10
+ framework: {
11
+ name: '@storybook/react-vite',
12
+ options: {},
13
+ },
14
+ docs: {
15
+ autodocs: 'tag',
16
+ },
17
+ };
18
+ export default config;
@@ -0,0 +1,24 @@
1
+ import type { Preview } from '@storybook/react';
2
+ import React from 'react';
3
+ import { ThemeProvider } from './theme';
4
+
5
+ const preview: Preview = {
6
+ decorators: [
7
+ (Story) => (
8
+ <ThemeProvider>
9
+ <Story />
10
+ </ThemeProvider>
11
+ ),
12
+ ],
13
+ parameters: {
14
+ actions: { argTypesRegex: '^on[A-Z].*' },
15
+ controls: {
16
+ matchers: {
17
+ color: /(background|color)$/i,
18
+ date: /Date$/i,
19
+ },
20
+ },
21
+ },
22
+ };
23
+
24
+ export default preview;
@@ -0,0 +1,7 @@
1
+ import '@radix-ui/themes/styles.css';
2
+ import React, { PropsWithChildren } from 'react';
3
+ import { Theme } from '@radix-ui/themes';
4
+
5
+ export const ThemeProvider = ({ children }: PropsWithChildren) => {
6
+ return <Theme>{children}</Theme>;
7
+ };
@@ -0,0 +1,9 @@
1
+ {
2
+ "bumpFiles": [
3
+ {
4
+ "filename": "package.json",
5
+ "type": "json"
6
+ }
7
+ ],
8
+ "prerelease": "beta"
9
+ }
Binary file
package/.yarnrc.yml ADDED
@@ -0,0 +1 @@
1
+ nodeLinker: node-modules
package/CHANGELOG.md ADDED
@@ -0,0 +1,5 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
4
+
5
+ ### 0.0.2-beta.0 (2024-04-04)
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 Igsem
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,55 @@
1
+ **Dantian - React State Management Reimagined**
2
+
3
+ npm version: [replace with package version badge]
4
+
5
+ Dantian is an event-based state management library for React applications that delivers pinpoint performance and effortless integration with forms. Say goodbye to unnecessary rerenders and complex state comparisons – Dantian ensures that components only update when they subscribe to specific state changes.
6
+
7
+ **The Problem**
8
+
9
+ Traditional state management solutions in React often lead to performance issues, especially in applications with frequent updates or large forms. Unnecessary rerenders can slow down the user experience.
10
+
11
+ **Our Solution**
12
+
13
+ Dantian's event-driven approach focuses on precision. State updates are triggered by events, and only components subscribed to those events rerender. This provides granular control and optimizes performance where it matters most.
14
+
15
+ **Features**
16
+
17
+ - **useState-like hooks:** Familiar interface for ease of use.
18
+ - **Custom events:** Power and flexibility for complex scenarios.
19
+ - **Asynchronous state updates:** Handle async patterns gracefully.
20
+ - **Optimized form integration:** Tackle large forms without performance compromises.
21
+
22
+ **Installation**
23
+
24
+ ```bash
25
+ npm install dantian
26
+ ```
27
+
28
+ **Basic usage**
29
+
30
+ ```TypeScript
31
+ import { useEventState } from 'dantian';
32
+
33
+ function MyComponent() {
34
+ const [count, setCount] = useEventState('counter', 0);
35
+
36
+ const increment = () => setCount(count + 1);
37
+
38
+ return (
39
+ <div>
40
+ <p>Count: {count}</p>
41
+ <button onClick={increment}>Increment</button>
42
+ </div>
43
+ );
44
+ }
45
+ ```
46
+
47
+ **Getting Started**
48
+
49
+ **Why Dantian?**
50
+
51
+ Performance by design: Avoid unnecessary rerenders, especially in complex forms.
52
+ Developer-friendly: Intuitive API and seamless form integration.
53
+ **License**
54
+
55
+ MIT
@@ -0,0 +1 @@
1
+ export default { extends: ['@commitlint/config-conventional'] };
@@ -0,0 +1,85 @@
1
+ import { test, expect } from '@playwright/test';
2
+ import { getStorybookLocator } from './getLocator';
3
+
4
+ test.beforeEach(async ({ page }) => {
5
+ await page.goto(
6
+ 'http://localhost:6006/?path=/story/dantian-counter--primary',
7
+ );
8
+ });
9
+
10
+ test('page title', async ({ page }) => {
11
+ await expect(page).toHaveTitle(/Dantian \/ Counter.*/);
12
+ });
13
+
14
+ test('test counter with classic store', async ({ page }) => {
15
+ const counter1Text = getStorybookLocator(page).locator(
16
+ 'text=We are counting first: 0',
17
+ );
18
+ const counter2Text = getStorybookLocator(page).locator(
19
+ 'text=We are counting second: 0',
20
+ );
21
+ const counter3Text = getStorybookLocator(page).locator(
22
+ 'text=We are counting third: 0',
23
+ );
24
+ const counterPromiseText = getStorybookLocator(page).locator(
25
+ 'text=We are counting hydrating default: 88',
26
+ );
27
+
28
+ await expect(counter1Text).toBeVisible();
29
+ await expect(counter2Text).toBeVisible();
30
+ await expect(counter3Text).toBeVisible();
31
+ await expect(counterPromiseText).toBeVisible();
32
+
33
+ const button3 = getStorybookLocator(page).locator(
34
+ 'button:has-text("Let\'s go, third counter, reusing second store")',
35
+ );
36
+ await button3.click();
37
+ await expect(
38
+ getStorybookLocator(page).locator('text=We are counting first: 0'),
39
+ ).toBeVisible();
40
+ await expect(
41
+ getStorybookLocator(page).locator('text=We are counting third: 1'),
42
+ ).toBeVisible();
43
+ await expect(
44
+ getStorybookLocator(page).locator('text=We are counting second: 1'),
45
+ ).toBeVisible();
46
+
47
+ const button2 = getStorybookLocator(page).locator(
48
+ 'button:has-text("Let\'s go, second counter")',
49
+ );
50
+ await button2.click();
51
+ await expect(
52
+ getStorybookLocator(page).locator('text=We are counting first: 0'),
53
+ ).toBeVisible();
54
+ await expect(
55
+ getStorybookLocator(page).locator('text=We are counting third: 2'),
56
+ ).toBeVisible();
57
+ await expect(
58
+ getStorybookLocator(page).locator('text=We are counting second: 2'),
59
+ ).toBeVisible();
60
+
61
+ const button1 = getStorybookLocator(page).locator(
62
+ 'button:has-text("Let\'s go, first counter")',
63
+ );
64
+ await button1.click();
65
+
66
+ await expect(
67
+ getStorybookLocator(page).locator('text=We are counting first: 1'),
68
+ ).toBeVisible();
69
+ await expect(
70
+ getStorybookLocator(page).locator('text=We are counting third: 2'),
71
+ ).toBeVisible();
72
+ await expect(
73
+ getStorybookLocator(page).locator('text=We are counting second: 2'),
74
+ ).toBeVisible();
75
+
76
+ const buttonPromise = getStorybookLocator(page).locator(
77
+ 'button:has-text("Let\'s go, promise counter")',
78
+ );
79
+ await buttonPromise.click();
80
+ await expect(
81
+ getStorybookLocator(page).locator(
82
+ 'text=We are counting hydrating default: 89',
83
+ ),
84
+ ).toBeVisible();
85
+ });
@@ -0,0 +1,4 @@
1
+ import { Page } from '@playwright/test';
2
+
3
+ export const getStorybookLocator = (page: Page) =>
4
+ page.frameLocator('[id="storybook-preview-iframe"]');
@@ -0,0 +1,8 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import * as publicApi from '../index';
3
+
4
+ describe('Public api', () => {
5
+ it('Should have a test export', () => {
6
+ expect(publicApi.buildClassicStore).toBeDefined();
7
+ });
8
+ });
@@ -0,0 +1,86 @@
1
+ import { BehaviorSubject } from 'rxjs';
2
+ import { useMemo, useSyncExternalStore } from 'react';
3
+
4
+ type UpdateFunction<T> = (state: T) => T;
5
+ type Updater<T> = (cb: UpdateFunction<T>) => void;
6
+
7
+ type DefaultState<T> =
8
+ | T
9
+ | {
10
+ hydrator: () => Promise<T>;
11
+ beforeLoadState: T;
12
+ persist?: (state: T) => Promise<void>;
13
+ };
14
+
15
+ interface StoreBuilder<T> {
16
+ defaultState: DefaultState<T>;
17
+ useStore: () => [T, Updater<T>];
18
+ useSelector: <S>(selector: (state: T) => S) => S;
19
+ subject$: BehaviorSubject<T>;
20
+ update: Updater<T>;
21
+ getValue: () => T;
22
+ }
23
+
24
+ const isDefaultState = <T>(
25
+ defaultState: DefaultState<T>,
26
+ ): defaultState is T => {
27
+ if (
28
+ typeof defaultState === 'object' &&
29
+ defaultState !== null &&
30
+ 'hydrator' in defaultState
31
+ ) {
32
+ return false;
33
+ }
34
+ return true;
35
+ };
36
+
37
+ export const buildClassicStore = async <T>(
38
+ defaultState: DefaultState<T>,
39
+ ): Promise<StoreBuilder<T>> => {
40
+ const initialState = isDefaultState<T>(defaultState)
41
+ ? defaultState
42
+ : await defaultState.hydrator();
43
+
44
+ const store = new BehaviorSubject<T>(initialState);
45
+
46
+ const update: Updater<T> = (updater) => {
47
+ store.next(updater(store.getValue()));
48
+ };
49
+
50
+ const useStore = () => {
51
+ const subscribe = (onStoreChange: () => void) => {
52
+ const subscription = store.subscribe({
53
+ next: onStoreChange,
54
+ error: console.error,
55
+ });
56
+
57
+ return () => {
58
+ subscription.unsubscribe();
59
+ };
60
+ };
61
+ const state: T = useSyncExternalStore(
62
+ subscribe,
63
+ () => store.getValue(),
64
+ () => initialState,
65
+ );
66
+
67
+ return [state, update] satisfies [T, Updater<T>];
68
+ };
69
+
70
+ const useSelector = <S>(selector: (state: T) => S) => {
71
+ const [value] = useStore();
72
+
73
+ const selectedValue = selector(value);
74
+
75
+ return useMemo(() => selectedValue, [selectedValue]);
76
+ };
77
+
78
+ return {
79
+ defaultState,
80
+ useStore,
81
+ useSelector,
82
+ subject$: store,
83
+ update,
84
+ getValue: () => store.getValue(),
85
+ };
86
+ };
package/lib/dao.ts ADDED
@@ -0,0 +1,3 @@
1
+ import { createEventStore } from '.';
2
+
3
+ export const wuji = createEventStore;
@@ -0,0 +1,169 @@
1
+ import { BehaviorSubject, type Observable } from 'rxjs';
2
+ import {
3
+ distinctUntilChanged,
4
+ filter,
5
+ map,
6
+ scan,
7
+ startWith,
8
+ tap,
9
+ } from 'rxjs/operators';
10
+ import {
11
+ type GetValueType,
12
+ type NestedEvent,
13
+ type PropertyPath,
14
+ type SystemEvent,
15
+ } from './types';
16
+ import { set, get } from 'lodash/fp';
17
+
18
+ import { useCallback, useEffect, useState } from 'react';
19
+
20
+ export function createEventStore<T extends object>(
21
+ initialState: T,
22
+ options?: {
23
+ debug?: boolean;
24
+ hydrator: () => Promise<T>;
25
+ persist?: (state: T) => Promise<void>;
26
+ },
27
+ ) {
28
+ const debug = options?.debug === true ? options.debug : false;
29
+ const globalEventStore = new BehaviorSubject<NestedEvent<T> | SystemEvent<T>>(
30
+ {
31
+ type: '@@INIT',
32
+ payload: initialState,
33
+ },
34
+ );
35
+ const publish = <
36
+ TType extends PropertyPath<T>,
37
+ TPayload extends GetValueType<T, TType>,
38
+ >(
39
+ type: TType,
40
+ payload: TPayload,
41
+ ) => {
42
+ // eslint-disable-next-line
43
+ const event = { type, payload } as NestedEvent<T>;
44
+ globalEventStore.next(event);
45
+ };
46
+
47
+ const getPropertyObservable = <K extends PropertyPath<T>>(
48
+ eventType: K,
49
+ ): Observable<GetValueType<T, K>> => {
50
+ return globalEventStore.pipe(
51
+ filter((event) => event.type.startsWith(eventType)),
52
+ map((event) => event.payload as GetValueType<T, K>),
53
+ scan((__, curr) => curr),
54
+ distinctUntilChanged(),
55
+ );
56
+ };
57
+
58
+ const getHydrationObservable$ = (): Observable<T> => {
59
+ return globalEventStore.pipe(
60
+ filter((event) => event.type === '@@HYDRATED'),
61
+ map((event) => event.payload as T),
62
+ scan((__, curr) => curr),
63
+ distinctUntilChanged(),
64
+ );
65
+ };
66
+
67
+ const state$ = new BehaviorSubject<T>(initialState);
68
+
69
+ globalEventStore
70
+ .pipe(
71
+ tap((event) => {
72
+ if (debug) {
73
+ console.info(event);
74
+ }
75
+ }),
76
+ scan((state, event) => {
77
+ if (event.type === '@@INIT' || event.type === '@@HYDRATED') {
78
+ return event.payload as T;
79
+ }
80
+ return set(event.type, event.payload, state);
81
+ }, initialState),
82
+ tap((state) => {
83
+ if (debug) {
84
+ console.info('State after update', state);
85
+ }
86
+ }),
87
+ startWith(initialState),
88
+ )
89
+ .subscribe(state$);
90
+
91
+ options
92
+ ?.hydrator?.()
93
+ .then((payload) => {
94
+ globalEventStore.next({ type: '@@HYDRATED', payload });
95
+ })
96
+ .catch((error) => {
97
+ console.error('Failed to hydrate store', error);
98
+ });
99
+
100
+ state$.subscribe({ next: options?.persist });
101
+
102
+ const useStoreValue = <K extends PropertyPath<T>>(
103
+ type: K,
104
+ ): [GetValueType<T, K>, (payload: GetValueType<T, K>) => void] => {
105
+ const defaultValue: GetValueType<T, K> = get(type, state$.getValue());
106
+ const [value, setValue] = useState<GetValueType<T, K>>(defaultValue);
107
+ const handleUpdate = useCallback((payload: GetValueType<T, K>) => {
108
+ publish(type, payload);
109
+ }, []);
110
+
111
+ useEffect(() => {
112
+ const subscription = getHydrationObservable$().subscribe({
113
+ next: (nextState) => {
114
+ setValue(get(type, nextState) as GetValueType<T, K>);
115
+ },
116
+ });
117
+
118
+ return () => {
119
+ subscription.unsubscribe();
120
+ };
121
+ }, []);
122
+
123
+ useEffect(() => {
124
+ const subscription = getPropertyObservable(type).subscribe({
125
+ next: (value) => {
126
+ setValue(value);
127
+ },
128
+ });
129
+ return () => {
130
+ subscription.unsubscribe();
131
+ };
132
+ }, []);
133
+
134
+ return [value, handleUpdate];
135
+ };
136
+ const useHydrateStore = () => {
137
+ return (payload: T) => {
138
+ globalEventStore.next({ type: '@@HYDRATED', payload });
139
+ };
140
+ };
141
+ const useIsHydrated = () => {
142
+ const [isHydrated, setIsHydrated] = useState(false);
143
+ useEffect(() => {
144
+ const subscription = getHydrationObservable$().subscribe({
145
+ next: () => {
146
+ setIsHydrated(true);
147
+ },
148
+ });
149
+
150
+ return () => {
151
+ subscription.unsubscribe();
152
+ };
153
+ }, []);
154
+ return isHydrated;
155
+ };
156
+ const systemEvents$ = globalEventStore.pipe(
157
+ filter((event) => event.type.startsWith('@@')),
158
+ );
159
+
160
+ return {
161
+ useStoreValue,
162
+ useHydrateStore,
163
+ useIsHydrated,
164
+ publish,
165
+ getPropertyObservable,
166
+ state$,
167
+ systemEvents$,
168
+ };
169
+ }
package/lib/index.ts ADDED
@@ -0,0 +1,3 @@
1
+ export * from './classic-store';
2
+ export * from './event-store';
3
+ export * from './dao';
package/lib/types.ts ADDED
@@ -0,0 +1,28 @@
1
+ export type PropertyPath<T> = {
2
+ [K in keyof T]: K extends string
3
+ ? T[K] extends object
4
+ ? `${K}.${PropertyPath<T[K]>}` | K
5
+ : K
6
+ : never;
7
+ }[keyof T];
8
+
9
+ export type GetValueType<
10
+ T,
11
+ Path extends PropertyPath<T>,
12
+ > = Path extends `${infer Key}.${infer Rest}`
13
+ ? Key extends keyof T
14
+ ? Rest extends PropertyPath<T[Key]>
15
+ ? GetValueType<T[Key], Rest>
16
+ : never
17
+ : never
18
+ : Path extends keyof T
19
+ ? T[Path]
20
+ : never;
21
+
22
+ export interface NestedEvent<T> {
23
+ type: PropertyPath<T>;
24
+ payload: GetValueType<T, PropertyPath<T>>;
25
+ }
26
+ export type SystemEvent<T> =
27
+ | { type: '@@INIT'; payload: T }
28
+ | { type: '@@HYDRATED'; payload: T };
package/package.json ADDED
@@ -0,0 +1,73 @@
1
+ {
2
+ "name": "dantian",
3
+ "version": "0.0.2-beta.0",
4
+ "type": "module",
5
+ "main": "./dist/dantian.cjs",
6
+ "module": "./dist/dantian.js",
7
+ "types": "./dist/index.d.ts",
8
+ "scripts": {
9
+ "dev": "vite",
10
+ "test": "vitest",
11
+ "build": "tsc && vite build",
12
+ "preview": "vite preview",
13
+ "format": "prettier --write .",
14
+ "lint": "eslint ./lib/**/*.ts",
15
+ "prepare": "husky install",
16
+ "storybook": "storybook dev -p 6006",
17
+ "build-storybook": "storybook build",
18
+ "e2e": "playwright test"
19
+ },
20
+ "lint-staged": {
21
+ "**/*.{js,ts,tsx}": [
22
+ "eslint --fix"
23
+ ],
24
+ "**/*": "prettier --write --ignore-unknown"
25
+ },
26
+ "peerDependencies": {
27
+ "react": ">=18"
28
+ },
29
+ "devDependencies": {
30
+ "@commitlint/cli": "^18.6.1",
31
+ "@commitlint/config-conventional": "^18.6.2",
32
+ "@playwright/test": "^1.41.2",
33
+ "@radix-ui/themes": "^2.0.3",
34
+ "@storybook/addon-essentials": "^7.6.17",
35
+ "@storybook/addon-interactions": "^7.6.17",
36
+ "@storybook/addon-links": "^7.6.17",
37
+ "@storybook/blocks": "^7.6.17",
38
+ "@storybook/react": "^7.6.17",
39
+ "@storybook/react-vite": "^7.6.17",
40
+ "@storybook/test": "^7.6.17",
41
+ "@storybook/testing-library": "^0.2.2",
42
+ "@types/lodash.set": "^4",
43
+ "@types/node": "^20.11.20",
44
+ "@typescript-eslint/eslint-plugin": "^6.4.0",
45
+ "@typescript-eslint/parser": "^7.0.2",
46
+ "@vitest/ui": "^1.3.1",
47
+ "eslint": "^8.0.1",
48
+ "eslint-config-prettier": "^9.1.0",
49
+ "eslint-config-standard-with-typescript": "^43.0.1",
50
+ "eslint-plugin-import": "^2.25.2",
51
+ "eslint-plugin-n": "^15.0.0 || ^16.0.0 ",
52
+ "eslint-plugin-promise": "^6.0.0",
53
+ "eslint-plugin-react": "^7.33.2",
54
+ "eslint-plugin-storybook": "^0.8.0",
55
+ "husky": "^9.0.11",
56
+ "jsdom": "^24.0.0",
57
+ "lint-staged": "^15.2.2",
58
+ "prettier": "^3.2.5",
59
+ "react": "18.2.0",
60
+ "react-dom": "^18.2.0",
61
+ "standard-version": "^9.5.0",
62
+ "storybook": "^7.6.17",
63
+ "typescript": "*",
64
+ "vite": "^5.1.4",
65
+ "vite-plugin-dts": "^3.7.3",
66
+ "vitest": "^1.3.1"
67
+ },
68
+ "packageManager": "yarn@4.1.0",
69
+ "dependencies": {
70
+ "lodash.set": "^4.3.2",
71
+ "rxjs": "^7.8.1"
72
+ }
73
+ }
@@ -0,0 +1,77 @@
1
+ import { defineConfig, devices } from '@playwright/test';
2
+
3
+ /**
4
+ * Read environment variables from file.
5
+ * https://github.com/motdotla/dotenv
6
+ */
7
+ // require('dotenv').config();
8
+
9
+ /**
10
+ * See https://playwright.dev/docs/test-configuration.
11
+ */
12
+ export default defineConfig({
13
+ testDir: './e2e',
14
+ /* Run tests in files in parallel */
15
+ fullyParallel: true,
16
+ /* Fail the build on CI if you accidentally left test.only in the source code. */
17
+ forbidOnly: !!process.env.CI,
18
+ /* Retry on CI only */
19
+ retries: process.env.CI ? 2 : 0,
20
+ /* Opt out of parallel tests on CI. */
21
+ workers: process.env.CI ? 1 : undefined,
22
+ /* Reporter to use. See https://playwright.dev/docs/test-reporters */
23
+ reporter: 'html',
24
+ /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
25
+ use: {
26
+ /* Base URL to use in actions like `await page.goto('/')`. */
27
+ // baseURL: 'http://127.0.0.1:3000',
28
+
29
+ /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
30
+ trace: 'on-first-retry',
31
+ },
32
+
33
+ /* Configure projects for major browsers */
34
+ projects: [
35
+ {
36
+ name: 'chromium',
37
+ use: { ...devices['Desktop Chrome'] },
38
+ },
39
+
40
+ {
41
+ name: 'firefox',
42
+ use: { ...devices['Desktop Firefox'] },
43
+ },
44
+
45
+ {
46
+ name: 'webkit',
47
+ use: { ...devices['Desktop Safari'] },
48
+ },
49
+
50
+ /* Test against mobile viewports. */
51
+ // {
52
+ // name: 'Mobile Chrome',
53
+ // use: { ...devices['Pixel 5'] },
54
+ // },
55
+ // {
56
+ // name: 'Mobile Safari',
57
+ // use: { ...devices['iPhone 12'] },
58
+ // },
59
+
60
+ /* Test against branded browsers. */
61
+ // {
62
+ // name: 'Microsoft Edge',
63
+ // use: { ...devices['Desktop Edge'], channel: 'msedge' },
64
+ // },
65
+ // {
66
+ // name: 'Google Chrome',
67
+ // use: { ...devices['Desktop Chrome'], channel: 'chrome' },
68
+ // },
69
+ ],
70
+
71
+ /* Run your local dev server before starting the tests */
72
+ // webServer: {
73
+ // command: 'npm run start',
74
+ // url: 'http://127.0.0.1:3000',
75
+ // reuseExistingServer: !process.env.CI,
76
+ // },
77
+ });
@@ -0,0 +1,22 @@
1
+ import React from 'react';
2
+ import { Flex, Text, Button } from '@radix-ui/themes';
3
+ import useStore, { useCounterSelector } from './counterStore';
4
+
5
+ const CountText = () => {
6
+ const count = useCounterSelector((state) => state.count);
7
+
8
+ return <Text>We are counting first: {count}</Text>;
9
+ };
10
+
11
+ export const Counter = () => {
12
+ const [state, updateCount] = useStore();
13
+ const increment = () =>
14
+ updateCount((prevState) => ({ ...prevState, count: prevState.count + 1 }));
15
+
16
+ return (
17
+ <Flex direction="column" gap="2">
18
+ <CountText />
19
+ <Button onClick={increment}>Let's go, first counter</Button>
20
+ </Flex>
21
+ );
22
+ };
@@ -0,0 +1,23 @@
1
+ import React from 'react';
2
+ import { Flex, Text, Button } from '@radix-ui/themes';
3
+
4
+ import useStore, { useCounter2Selector } from './counterStore2';
5
+
6
+ const CountText = () => {
7
+ const count = useCounter2Selector((state) => state.count);
8
+
9
+ return <Text>We are counting second: {count}</Text>;
10
+ };
11
+
12
+ export const Counter2 = () => {
13
+ const [state, updateCount] = useStore();
14
+ const increment = () =>
15
+ updateCount((prevState) => ({ ...prevState, count: prevState.count + 1 }));
16
+
17
+ return (
18
+ <Flex direction="column" gap="2">
19
+ <CountText />
20
+ <Button onClick={increment}>Let's go, second counter</Button>
21
+ </Flex>
22
+ );
23
+ };
@@ -0,0 +1,18 @@
1
+ import React from 'react';
2
+ import { Flex, Text, Button } from '@radix-ui/themes';
3
+ import useStore from './counterStore2';
4
+
5
+ export const Counter3 = () => {
6
+ const [state, updateCount] = useStore();
7
+ const increment = () =>
8
+ updateCount((prevState) => ({ ...prevState, count: prevState.count + 1 }));
9
+
10
+ return (
11
+ <Flex direction="column" gap="2">
12
+ <Text>We are counting third: {state.count}</Text>
13
+ <Button onClick={increment}>
14
+ Let's go, third counter, reusing second store
15
+ </Button>
16
+ </Flex>
17
+ );
18
+ };
@@ -0,0 +1,16 @@
1
+ import { Button, Flex, Text } from '@radix-ui/themes';
2
+ import React from 'react';
3
+ import useStore, { useCounterPromiseSelector } from './counterStorePromise';
4
+
5
+ export const DefaultPromise = () => {
6
+ const [, updateCount] = useStore();
7
+ const count = useCounterPromiseSelector((state) => state.count);
8
+ const increment = () =>
9
+ updateCount((prevState) => ({ ...prevState, count: prevState.count + 1 }));
10
+ return (
11
+ <Flex direction="column" gap="2">
12
+ <Text>We are counting hydrating default: {count}</Text>
13
+ <Button onClick={increment}>Let's go, promise counter</Button>
14
+ </Flex>
15
+ );
16
+ };
@@ -0,0 +1,17 @@
1
+ import { Flex } from '@radix-ui/themes';
2
+ import { Counter } from './Counter';
3
+ import React from 'react';
4
+ import { Counter2 } from './Counter2';
5
+ import { Counter3 } from './Counter3';
6
+ import { DefaultPromise } from './DefaultPromise';
7
+
8
+ export const Root = () => {
9
+ return (
10
+ <Flex direction="column" gap="2">
11
+ <Counter />
12
+ <Counter2 />
13
+ <Counter3 />
14
+ <DefaultPromise />
15
+ </Flex>
16
+ );
17
+ };
@@ -0,0 +1,11 @@
1
+ import { buildClassicStore } from '../../lib';
2
+
3
+ export interface CounterState {
4
+ count: number;
5
+ }
6
+
7
+ const counterStoreBuilder = await buildClassicStore<CounterState>({ count: 0 });
8
+ const { useStore, useSelector: useCounterSelector } = counterStoreBuilder;
9
+
10
+ export { useCounterSelector };
11
+ export default useStore;
@@ -0,0 +1,13 @@
1
+ import { buildClassicStore } from '../../lib';
2
+
3
+ export interface Counter2State {
4
+ count: number;
5
+ }
6
+
7
+ const counter2StoreBuilder = await buildClassicStore<Counter2State>({
8
+ count: 0,
9
+ });
10
+ const { useStore, useSelector: useCounter2Selector } = counter2StoreBuilder;
11
+
12
+ export { useCounter2Selector };
13
+ export default useStore;
@@ -0,0 +1,15 @@
1
+ import { buildClassicStore } from '../../lib';
2
+
3
+ export interface CounterState {
4
+ count: number;
5
+ }
6
+
7
+ const counterStore3Builder = await buildClassicStore<CounterState>({
8
+ hydrator: () => Promise.resolve({ count: 88 }),
9
+ beforeLoadState: { count: 0 },
10
+ });
11
+ const { useStore, useSelector: useCounterPromiseSelector } =
12
+ counterStore3Builder;
13
+
14
+ export { useCounterPromiseSelector };
15
+ export default useStore;
@@ -0,0 +1,23 @@
1
+ import type { Meta, StoryObj } from '@storybook/react';
2
+ import { Root } from './counter/Root';
3
+
4
+ // More on how to set up stories at: https://storybook.js.org/docs/writing-stories#default-export
5
+ const meta = {
6
+ title: 'Dantian/Counter',
7
+ component: Root,
8
+ parameters: {
9
+ layout: 'centered',
10
+ },
11
+
12
+ tags: ['autodocs'],
13
+
14
+ argTypes: {},
15
+ } satisfies Meta<typeof Root>;
16
+
17
+ export default meta;
18
+ type Story = StoryObj<typeof meta>;
19
+
20
+ // More on writing stories with args: https://storybook.js.org/docs/writing-stories/args
21
+ export const Primary: Story = {
22
+ args: {},
23
+ };
@@ -0,0 +1,19 @@
1
+ import { Flex } from '@radix-ui/themes';
2
+
3
+ import React from 'react';
4
+ import { state$, useStoreValue } from './store';
5
+
6
+ export const Root = () => {
7
+ const [name] = useStoreValue('user.name');
8
+ return (
9
+ <Flex direction="column" gap="2">
10
+ <Flex direction="row" gap="2" align={'center'}>
11
+ State: <pre>{JSON.stringify(state$.getValue())}</pre>
12
+ </Flex>
13
+
14
+ <Flex direction="row" gap="2" align={'center'}>
15
+ User name: {name}
16
+ </Flex>
17
+ </Flex>
18
+ );
19
+ };
@@ -0,0 +1,9 @@
1
+ interface State {
2
+ user: {
3
+ name: string;
4
+ address: {
5
+ city: string;
6
+ street: string;
7
+ };
8
+ };
9
+ }
@@ -0,0 +1,28 @@
1
+ import { wuji } from '../../lib';
2
+
3
+ export const { state$, useStoreValue, useHydrateStore, useIsHydrated } =
4
+ wuji<State>(
5
+ {
6
+ user: { address: { city: 'n/a', street: 'n/a' }, name: 'n/a' },
7
+ },
8
+ {
9
+ hydrator: () => {
10
+ return new Promise((resolve) => {
11
+ setTimeout(
12
+ () =>
13
+ resolve({
14
+ user: {
15
+ address: {
16
+ city: 'Aubonne',
17
+ street: 'Chemin du Mont-Blanc 16',
18
+ },
19
+ name: 'Julius',
20
+ },
21
+ }),
22
+ 3000,
23
+ );
24
+ });
25
+ },
26
+ debug: true,
27
+ },
28
+ );
@@ -0,0 +1,23 @@
1
+ import type { Meta, StoryObj } from '@storybook/react';
2
+ import { Root } from './event-store/Root';
3
+
4
+ // More on how to set up stories at: https://storybook.js.org/docs/writing-stories#default-export
5
+ const meta = {
6
+ title: 'Dantian/Event Store',
7
+ component: Root,
8
+ parameters: {
9
+ layout: 'centered',
10
+ },
11
+
12
+ tags: ['autodocs'],
13
+
14
+ argTypes: {},
15
+ } satisfies Meta<typeof Root>;
16
+
17
+ export default meta;
18
+ type Story = StoryObj<typeof meta>;
19
+
20
+ // More on writing stories with args: https://storybook.js.org/docs/writing-stories/args
21
+ export const Primary: Story = {
22
+ args: {},
23
+ };
package/tsconfig.json ADDED
@@ -0,0 +1,24 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2020",
4
+ "useDefineForClassFields": true,
5
+ "module": "ESNext",
6
+ "lib": ["ES2020", "DOM", "DOM.Iterable"],
7
+ "skipLibCheck": true,
8
+ "jsx": "react-jsx",
9
+
10
+ /* Bundler mode */
11
+ "moduleResolution": "bundler",
12
+ "allowImportingTsExtensions": true,
13
+ "resolveJsonModule": true,
14
+ "isolatedModules": true,
15
+ "noEmit": true,
16
+
17
+ /* Linting */
18
+ "strict": true,
19
+ "noUnusedLocals": true,
20
+ "noUnusedParameters": true,
21
+ "noFallthroughCasesInSwitch": true
22
+ },
23
+ "include": ["lib"]
24
+ }
package/vite.config.js ADDED
@@ -0,0 +1,21 @@
1
+ import { defineConfig } from 'vite';
2
+ import dts from 'vite-plugin-dts';
3
+ export default defineConfig({
4
+ build: {
5
+ copyPublicDir: false,
6
+ lib: {
7
+ entry: 'lib/index.ts',
8
+ name: 'dantian',
9
+ formats: ['es', 'cjs'],
10
+ },
11
+ rollupOptions: {
12
+ external: ['react', 'react-dom'],
13
+ output: {
14
+ globals: {
15
+ react: 'React',
16
+ },
17
+ },
18
+ },
19
+ },
20
+ plugins: [dts({ include: ['lib'], insertTypesEntry: true })],
21
+ });
@@ -0,0 +1,9 @@
1
+ import { defineConfig } from 'vitest/config';
2
+
3
+ export default defineConfig({
4
+ test: {
5
+ environment: 'jsdom',
6
+ globals: true,
7
+ include: ['lib/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'],
8
+ },
9
+ });