Skip to content

Commit

Permalink
Merge pull request #26 from poly-state/feat/batch
Browse files Browse the repository at this point in the history
transaction support
  • Loading branch information
shahriar-shojib committed Oct 24, 2023
2 parents cd21854 + 3c742fb commit c7db05a
Show file tree
Hide file tree
Showing 22 changed files with 2,257 additions and 1,615 deletions.
21 changes: 12 additions & 9 deletions .changeset/config.json
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
{
"$schema": "https://unpkg.com/@changesets/config@2.0.1/schema.json",
"changelog": ["@changesets/changelog-github", { "repo": "poly-state/poly-state" }],
"commit": false,
"fixed": [],
"linked": [],
"access": "public",
"baseBranch": "master",
"updateInternalDependencies": "minor",
"ignore": []
"$schema": "https://unpkg.com/@changesets/config@2.0.1/schema.json",
"changelog": [
"@changesets/changelog-github",
{ "repo": "poly-state/poly-state" }
],
"commit": false,
"fixed": [],
"linked": [],
"access": "public",
"baseBranch": "master",
"updateInternalDependencies": "patch",
"ignore": []
}
27 changes: 27 additions & 0 deletions .changeset/healthy-balloons-perform.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
---
'@poly-state/core': minor
'@poly-state/react': patch
---

## @poly-state/core:

- Global transactions support

```js
import { transact } from '@poly-state/core';

transact(() => {
store1.setState({ count: 1 });
store2.setState({ name: test });
store2.setState({ address: 'London' });
});
```

> store 1 and store 2 will be updated only once!
## @poly-state/react:

- fixed a peer dependency issue for react
- use `useSyncExternalStore`
- use `shallowCompare`
- upgrade packages and bundling
12 changes: 12 additions & 0 deletions .changeset/pre.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"mode": "pre",
"tag": "next",
"initialVersions": {
"preact-example": "0.0.1",
"react-example": "0.0.1",
"@poly-state/core": "1.3.0",
"@poly-state/preact": "3.0.0",
"@poly-state/react": "3.0.0"
},
"changesets": []
}
2 changes: 1 addition & 1 deletion .github/workflows/pr.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ jobs:
main-branch-name: 'master'
- uses: pnpm/action-setup@v2.2.2
with:
version: 7
version: 8
- uses: actions/setup-node@v2
with:
cache: 'pnpm'
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ jobs:
- uses: nrwl/nx-set-shas@v2
- uses: pnpm/action-setup@v2.2.2
with:
version: 7
version: 8
- uses: actions/setup-node@v2
with:
cache: 'pnpm'
Expand Down
8 changes: 4 additions & 4 deletions examples/preact/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,9 @@
},
"devDependencies": {
"@preact/preset-vite": "^2.1.5",
"typescript": "^4.5.4",
"typescript": "^5.2.2",
"vite": "^2.9.9",
"@poly-state/core":"*",
"@poly-state/preact":"*"
"@poly-state/core": "*",
"@poly-state/preact": "*"
}
}
}
44 changes: 22 additions & 22 deletions examples/react/package.json
Original file line number Diff line number Diff line change
@@ -1,23 +1,23 @@
{
"name": "react-example",
"private": true,
"version": "0.0.1",
"scripts": {
"dev": "vite",
"build": "tsc && vite build",
"preview": "vite preview"
},
"dependencies": {
"react": "^18.0.0",
"react-dom": "^18.0.0"
},
"devDependencies": {
"@poly-state/core": "*",
"@poly-state/react": "*",
"@types/react": "^18.0.0",
"@types/react-dom": "^18.0.0",
"@vitejs/plugin-react": "^1.3.0",
"typescript": "^4.6.3",
"vite": "^2.9.9"
}
}
"name": "react-example",
"private": true,
"version": "0.0.1",
"scripts": {
"dev": "vite",
"build": "tsc && vite build",
"preview": "vite preview"
},
"dependencies": {
"react": "^18.0.0",
"react-dom": "^18.0.0"
},
"devDependencies": {
"@poly-state/core": "*",
"@poly-state/react": "*",
"@types/react": "^18.0.0",
"@types/react-dom": "^18.0.0",
"@vitejs/plugin-react": "^1.3.0",
"typescript": "^5.2.2",
"vite": "^2.9.9"
}
}
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,6 @@
},
"devDependencies": {
"@changesets/changelog-github": "^0.4.5"
}
},
"packageManager": "pnpm@8.6.1"
}

12 changes: 6 additions & 6 deletions packages/core/.eslintrc.js
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
module.exports = {
env: {
browser: true,
es2021: true,
},
extends: ['eslint:recommended', 'plugin:@typescript-eslint/recommended'],
env: { browser: true, es2020: true },
ignorePatterns: ['dist', '.eslintrc.js'],
parser: '@typescript-eslint/parser',
parserOptions: {
ecmaVersion: 12,
ecmaVersion: 'latest',
sourceType: 'module',
project: ['./tsconfig.json', './tsconfig.node.json'],
tsconfigRootDir: __dirname,
},
extends: ['eslint:recommended', 'plugin:@typescript-eslint/recommended'],
plugins: ['@typescript-eslint'],
rules: {
indent: ['off', 'tab'],
Expand Down
16 changes: 8 additions & 8 deletions packages/core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,21 +38,21 @@
"scripts": {
"build": "tsup",
"test": "jest",
"lint": "eslint src/**/*.{ts,tsx}",
"lint": "eslint --fix",
"format:check": "prettier --check ./",
"format": "prettier --check --write ./"
},
"devDependencies": {
"@swc/core": "^1.2.246",
"@swc/jest": "^0.2.22",
"@swc/core": "^1.3.94",
"@swc/jest": "^0.2.29",
"@types/jest": "^29.0.0",
"@typescript-eslint/eslint-plugin": "^5.36.1",
"@typescript-eslint/parser": "^5.36.1",
"eslint": "^8.23.0",
"@typescript-eslint/eslint-plugin": "^6.9.0",
"@typescript-eslint/parser": "^6.9.0",
"eslint": "^8.52.0",
"jest": "^29.0.2",
"prettier": "^2.7.1",
"tsup": "^6.2.3",
"typescript": "^4.8.2"
"tsup": "^7.2.0",
"typescript": "^5.2.2"
},
"keywords": [
"react",
Expand Down
2 changes: 2 additions & 0 deletions packages/core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,6 @@ export const createStore = <T extends StateConstraint>(

export * from './devtools';
export * from './store.class';
export * from './transact';
export * from './types';
export * from './utils';
31 changes: 25 additions & 6 deletions packages/core/src/store.class.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import { GLOBAL_TRANSACT } from './transact';
import {
AllSettersMiddlewareCallback,
CallBack,
Expand All @@ -12,7 +13,7 @@ import {
StoreMiddleWareFunction,
StoreType,
} from './types';
import { capitalize, deleteFromArray, isFunction } from './utils';
import { capitalize, isFunction } from './utils';

type StoreFactory<T extends StateConstraint> = new (
initialState: T,
Expand All @@ -27,9 +28,12 @@ export const getStoreClass = <T extends StateConstraint>(): StoreFactory<T> => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
[key: string]: any;
}
// eslint-disable-next-line @typescript-eslint/no-unsafe-declaration-merging
class Store<State extends StateConstraint> implements StoreType<State> {
private id = `${Math.random()}${Date.now()}`;

private isHydrated = false;
private listeners: CallBack<State>[] = [];
private listeners = new Set<CallBack<Readonly<State>>>();
private isEqual: EqualityComparatorFunction;

private SET_STATE_MIDDLEWARES: MiddlewareCallback<State>[] = [];
Expand Down Expand Up @@ -75,9 +79,9 @@ export const getStoreClass = <T extends StateConstraint>(): StoreFactory<T> => {
}

subscribe(callback: CallBack<Readonly<State>>) {
this.listeners.push(callback);
this.listeners.add(callback);
return () => {
deleteFromArray(this.listeners, callback);
this.listeners.delete(callback);
};
}

Expand Down Expand Up @@ -107,8 +111,23 @@ export const getStoreClass = <T extends StateConstraint>(): StoreFactory<T> => {
}

private notifySubscribers() {
for (let i = 0; i < this.listeners.length; i++) {
this.listeners[i](this.state);
if (GLOBAL_TRANSACT.running) {
if (GLOBAL_TRANSACT.callBacks.has(this.id)) {
return;
}

GLOBAL_TRANSACT.callBacks.set(this.id, () => {
this.flush();
});
return;
}

this.flush();
}

private flush() {
for (const listener of this.listeners) {
listener(this.state);
}
}

Expand Down
45 changes: 45 additions & 0 deletions packages/core/src/tests/store-transact.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { createStore, transact } from '..';

describe('Poly Stat Transact functionality', () => {
it('should perform batch updates', () => {
const store = createStore({ count: 0, name: 'test', other: {} });

const listener = jest.fn();

store.subscribe(listener);

transact(() => {
store.setCount(1);
store.setName('test2');
store.setOther({ test: true });
});

expect(listener).toBeCalledTimes(1);
expect(listener).toHaveBeenCalledWith({ count: 1, name: 'test2', other: { test: true } });
});

it('should perform batched updates across multiple stores', () => {
const store1 = createStore({ count: 0, name: 'test' });
const store2 = createStore({ count: 0, name: 'test' });

const listener1 = jest.fn();
const listener2 = jest.fn();

store1.subscribe(listener1);
store2.subscribe(listener2);

transact(() => {
store1.setCount(1);
store1.setName('test2');

store2.setCount(1);
store2.setName('test2');
});

expect(listener1).toBeCalledTimes(1);
expect(listener1).toHaveBeenCalledWith({ count: 1, name: 'test2' });

expect(listener2).toBeCalledTimes(1);
expect(listener2).toHaveBeenCalledWith({ count: 1, name: 'test2' });
});
});
11 changes: 11 additions & 0 deletions packages/core/src/transact.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
export const GLOBAL_TRANSACT = {
callBacks: new Map<string, () => void>(),
running: false,
};

export const transact = (cb: () => void) => {
GLOBAL_TRANSACT.running = true;
cb();
GLOBAL_TRANSACT.callBacks.forEach((cb) => cb());
GLOBAL_TRANSACT.running = false;
};
25 changes: 25 additions & 0 deletions packages/core/src/utils/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -147,3 +147,28 @@ export const deepClone = <T>(a: T): T => {
export const isFunction = (obj: any): obj is CallableFunction => {
return typeof obj === 'function';
};

export const shallowCompare = <T>(objA: T, objB: T) => {
if (Object.is(objA, objB)) {
return true;
}

if (typeof objA !== 'object' || objA === null || typeof objB !== 'object' || objB === null) {
return false;
}

const keysA = Object.keys(objA);
if (keysA.length !== Object.keys(objB).length) {
return false;
}

for (let i = 0; i < keysA.length; i++) {
if (
!Object.prototype.hasOwnProperty.call(objB, keysA[i] as string) ||
!Object.is(objA[keysA[i] as keyof T], objB[keysA[i] as keyof T])
) {
return false;
}
}
return true;
};
12 changes: 6 additions & 6 deletions packages/preact/.eslintrc.js
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
module.exports = {
env: {
browser: true,
es2021: true,
},
extends: ['eslint:recommended', 'plugin:@typescript-eslint/recommended'],
env: { browser: true, es2020: true },
ignorePatterns: ['dist', '.eslintrc.js'],
parser: '@typescript-eslint/parser',
parserOptions: {
ecmaVersion: 12,
ecmaVersion: 'latest',
sourceType: 'module',
project: ['./tsconfig.json', './tsconfig.node.json'],
tsconfigRootDir: __dirname,
},
extends: ['eslint:recommended', 'plugin:@typescript-eslint/recommended'],
plugins: ['@typescript-eslint'],
rules: {
indent: ['off', 'tab'],
Expand Down
Loading

0 comments on commit c7db05a

Please sign in to comment.