Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c88033ff26 | |||
| a62fa83afc |
@@ -1,5 +1,13 @@
|
||||
# Changelog
|
||||
|
||||
## 2026-04-30 - 2.3.1 - fix(types,testing)
|
||||
tighten action context typing and update tests for stricter TypeScript checks
|
||||
|
||||
- enable noImplicitAny in TypeScript configuration and remove the build flag that allowed implicit any
|
||||
- require the action context parameter in IActionDef to reflect actual action usage
|
||||
- update tests to use the tstest tapbundle import and add explicit guards for possibly undefined state access
|
||||
- refresh dependency versions and remove the deprecated tapbundle dev dependency
|
||||
|
||||
## 2026-03-27 - 2.3.0 - feat(stateprocess)
|
||||
add managed state processes with lifecycle controls, scheduled actions, and disposal safety
|
||||
|
||||
|
||||
Generated
-16523
File diff suppressed because it is too large
Load Diff
+8
-8
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@push.rocks/smartstate",
|
||||
"version": "2.3.0",
|
||||
"version": "2.3.1",
|
||||
"private": false,
|
||||
"description": "A TypeScript-first reactive state management library with middleware, computed state, batching, persistence, and Web Component Context Protocol support.",
|
||||
"main": "dist_ts/index.js",
|
||||
@@ -10,20 +10,19 @@
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
"test": "(tstest test/ --verbose)",
|
||||
"build": "(tsbuild tsfolders --allowimplicitany && tsbundle npm)",
|
||||
"build": "(tsbuild tsfolders && tsbundle npm)",
|
||||
"buildDocs": "tsdoc"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@git.zone/tsbuild": "^4.4.0",
|
||||
"@git.zone/tsbundle": "^2.10.0",
|
||||
"@git.zone/tsrun": "^2.0.2",
|
||||
"@git.zone/tstest": "^3.6.1",
|
||||
"@push.rocks/tapbundle": "^6.0.3",
|
||||
"@types/node": "^25.5.0"
|
||||
"@git.zone/tstest": "^3.6.3",
|
||||
"@types/node": "^25.6.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@push.rocks/smarthash": "^3.2.6",
|
||||
"@push.rocks/smartjson": "^6.0.0",
|
||||
"@push.rocks/smarthash": "^3.2.7",
|
||||
"@push.rocks/smartjson": "^6.0.1",
|
||||
"@push.rocks/smartpromise": "^4.2.3",
|
||||
"@push.rocks/smartrx": "^3.0.10",
|
||||
"@push.rocks/webstore": "^2.0.21"
|
||||
@@ -38,6 +37,7 @@
|
||||
"assets/**/*",
|
||||
"cli.js",
|
||||
".smartconfig.json",
|
||||
"license",
|
||||
"readme.md"
|
||||
],
|
||||
"browserslist": [
|
||||
@@ -66,5 +66,5 @@
|
||||
"type": "git",
|
||||
"url": "https://code.foss.global/push.rocks/smartstate.git"
|
||||
},
|
||||
"packageManager": "pnpm@10.11.0+sha512.6540583f41cc5f628eb3d9773ecee802f4f9ef9923cc45b69890fb47991d4b092964694ec3a4f738a420c918a333062c8b925d312f42e4f0c263eb603551f977"
|
||||
"packageManager": "pnpm@10.28.2"
|
||||
}
|
||||
|
||||
Generated
+46
-2149
File diff suppressed because it is too large
Load Diff
+29
-21
@@ -1,4 +1,4 @@
|
||||
import { expect, tap } from '@push.rocks/tapbundle';
|
||||
import { expect, tap } from '@git.zone/tstest/tapbundle';
|
||||
import * as smartstate from '../ts/index.js';
|
||||
|
||||
type TTestStateParts = 'initTest' | 'persistTest' | 'forceTest';
|
||||
@@ -10,6 +10,14 @@ interface ITestState {
|
||||
};
|
||||
}
|
||||
|
||||
const getRequiredState = <TPayload>(statePart: smartstate.StatePart<any, TPayload>): TPayload => {
|
||||
const state = statePart.getState();
|
||||
if (state === undefined) {
|
||||
throw new Error('Expected state part to have initialized state');
|
||||
}
|
||||
return state;
|
||||
};
|
||||
|
||||
// ============================
|
||||
// Init mode tests
|
||||
// ============================
|
||||
@@ -245,7 +253,7 @@ tap.test('middleware should transform state', async () => {
|
||||
});
|
||||
|
||||
await statePart.setState({ value: 2, nested: { data: 'hello' } });
|
||||
expect(statePart.getState().nested.data).toEqual('HELLO');
|
||||
expect(getRequiredState(statePart).nested.data).toEqual('HELLO');
|
||||
});
|
||||
|
||||
tap.test('middleware should reject state changes on throw', async () => {
|
||||
@@ -272,7 +280,7 @@ tap.test('middleware should reject state changes on throw', async () => {
|
||||
expect(error).not.toBeNull();
|
||||
expect(error?.message).toEqual('Value must be non-negative');
|
||||
// State should be unchanged
|
||||
expect(statePart.getState().value).toEqual(1);
|
||||
expect(getRequiredState(statePart).value).toEqual(1);
|
||||
});
|
||||
|
||||
tap.test('multiple middlewares should run in order', async () => {
|
||||
@@ -297,7 +305,7 @@ tap.test('multiple middlewares should run in order', async () => {
|
||||
await statePart.setState({ value: 5, nested: { data: 'test' } });
|
||||
expect(order).toEqual([1, 2]);
|
||||
// (5 + 10) * 2 = 30
|
||||
expect(statePart.getState().value).toEqual(30);
|
||||
expect(getRequiredState(statePart).value).toEqual(30);
|
||||
});
|
||||
|
||||
tap.test('middleware removal should work', async () => {
|
||||
@@ -312,12 +320,12 @@ tap.test('middleware removal should work', async () => {
|
||||
});
|
||||
|
||||
await statePart.setState({ value: 2, nested: { data: 'test' } });
|
||||
expect(statePart.getState().value).toEqual(200);
|
||||
expect(getRequiredState(statePart).value).toEqual(200);
|
||||
|
||||
remove();
|
||||
|
||||
await statePart.setState({ value: 3, nested: { data: 'test' } });
|
||||
expect(statePart.getState().value).toEqual(3);
|
||||
expect(getRequiredState(statePart).value).toEqual(3);
|
||||
});
|
||||
|
||||
// ============================
|
||||
@@ -639,7 +647,7 @@ tap.test('concurrent dispatchAction should serialize (counter reaches exactly 10
|
||||
const counter = await state.getStatePart<{ count: number }>('counter', { count: 0 });
|
||||
|
||||
const increment = counter.createAction<void>(async (statePart) => {
|
||||
const current = statePart.getState();
|
||||
const current = getRequiredState(statePart);
|
||||
return { count: current.count + 1 };
|
||||
});
|
||||
|
||||
@@ -650,7 +658,7 @@ tap.test('concurrent dispatchAction should serialize (counter reaches exactly 10
|
||||
}
|
||||
await Promise.all(promises);
|
||||
|
||||
expect(counter.getState().count).toEqual(10);
|
||||
expect(getRequiredState(counter).count).toEqual(10);
|
||||
});
|
||||
|
||||
tap.test('concurrent setState should serialize (no lost updates)', async () => {
|
||||
@@ -669,7 +677,7 @@ tap.test('concurrent setState should serialize (no lost updates)', async () => {
|
||||
// At minimum, the final state should have been set 5 times without error
|
||||
// The exact values depend on serialization timing, but state should be valid
|
||||
expect(part.getState()).toBeTruthy();
|
||||
expect(part.getState().values).toBeInstanceOf(Array);
|
||||
expect(getRequiredState(part).values).toBeInstanceOf(Array);
|
||||
});
|
||||
|
||||
tap.test('dispose should complete the Subject and notify subscribers', async () => {
|
||||
@@ -737,7 +745,7 @@ tap.test('batch re-entrancy: setState during flush should not deadlock', async (
|
||||
// Wait for the fire-and-forget setState to complete
|
||||
await new Promise<void>((r) => setTimeout(r, 50));
|
||||
|
||||
expect(part.getState().v).toEqual(2);
|
||||
expect(getRequiredState(part).v).toEqual(2);
|
||||
});
|
||||
|
||||
tap.test('force mode should dispose old StatePart (Subject completes)', async () => {
|
||||
@@ -774,13 +782,13 @@ tap.test('context.dispatch should not deadlock on same StatePart', async () => {
|
||||
const part = await state.getStatePart<{ count: number }>('reentrantAction', { count: 0 });
|
||||
|
||||
const innerIncrement = part.createAction<void>(async (sp) => {
|
||||
return { count: sp.getState().count + 1 };
|
||||
return { count: getRequiredState(sp).count + 1 };
|
||||
});
|
||||
|
||||
const outerAction = part.createAction<void>(async (sp, _payload, context) => {
|
||||
// This would deadlock without the context.dispatch() mechanism
|
||||
await context.dispatch(innerIncrement, undefined);
|
||||
return sp.getState();
|
||||
return getRequiredState(sp);
|
||||
});
|
||||
|
||||
const result = await part.dispatchAction(outerAction, undefined);
|
||||
@@ -793,23 +801,23 @@ tap.test('deeply nested context.dispatch should work (3 levels)', async () => {
|
||||
const part = await state.getStatePart<{ steps: string[] }>('deepNested', { steps: [] });
|
||||
|
||||
const appendStep = part.createAction<string>(async (sp, step) => {
|
||||
return { steps: [...sp.getState().steps, step] };
|
||||
return { steps: [...getRequiredState(sp).steps, step] };
|
||||
});
|
||||
|
||||
const level2 = part.createAction<void>(async (sp, _payload, context) => {
|
||||
await context.dispatch(appendStep, 'level-2');
|
||||
return sp.getState();
|
||||
return getRequiredState(sp);
|
||||
});
|
||||
|
||||
const level1 = part.createAction<void>(async (sp, _payload, context) => {
|
||||
await context.dispatch(appendStep, 'level-1');
|
||||
await context.dispatch(level2, undefined);
|
||||
await context.dispatch(appendStep, 'level-1-after');
|
||||
return sp.getState();
|
||||
return getRequiredState(sp);
|
||||
});
|
||||
|
||||
await part.dispatchAction(level1, undefined);
|
||||
expect(part.getState().steps).toEqual(['level-1', 'level-2', 'level-1-after']);
|
||||
expect(getRequiredState(part).steps).toEqual(['level-1', 'level-2', 'level-1-after']);
|
||||
});
|
||||
|
||||
tap.test('circular context.dispatch should throw max depth error', async () => {
|
||||
@@ -820,12 +828,12 @@ tap.test('circular context.dispatch should throw max depth error', async () => {
|
||||
// Create a self-referencing action that will loop forever
|
||||
const circularAction: smartstate.StateAction<{ count: number }, void> = part.createAction<void>(
|
||||
async (sp, _payload, context) => {
|
||||
const current = sp.getState();
|
||||
const current = getRequiredState(sp);
|
||||
if (current.count < 100) {
|
||||
// This should eventually hit the depth limit
|
||||
await context.dispatch(circularAction, undefined);
|
||||
}
|
||||
return sp.getState();
|
||||
return getRequiredState(sp);
|
||||
}
|
||||
);
|
||||
|
||||
@@ -851,7 +859,7 @@ tap.test('actions without context arg should still work (backward compat)', asyn
|
||||
});
|
||||
|
||||
await part.dispatchAction(simpleAction, 42);
|
||||
expect(part.getState().value).toEqual(42);
|
||||
expect(getRequiredState(part).value).toEqual(42);
|
||||
});
|
||||
|
||||
tap.test('concurrent dispatches still serialize correctly with context feature', async () => {
|
||||
@@ -860,7 +868,7 @@ tap.test('concurrent dispatches still serialize correctly with context feature',
|
||||
const part = await state.getStatePart<{ count: number }>('concurrentWithContext', { count: 0 });
|
||||
|
||||
const increment = part.createAction<void>(async (sp) => {
|
||||
const current = sp.getState();
|
||||
const current = getRequiredState(sp);
|
||||
return { count: current.count + 1 };
|
||||
});
|
||||
|
||||
@@ -871,7 +879,7 @@ tap.test('concurrent dispatches still serialize correctly with context feature',
|
||||
}
|
||||
await Promise.all(promises);
|
||||
|
||||
expect(part.getState().count).toEqual(10);
|
||||
expect(getRequiredState(part).count).toEqual(10);
|
||||
});
|
||||
|
||||
// ── distinctUntilChanged on selectors ──────────────────────────────────
|
||||
|
||||
+3
-3
@@ -1,4 +1,4 @@
|
||||
import { expect, tap } from '@push.rocks/tapbundle';
|
||||
import { expect, tap } from '@git.zone/tstest/tapbundle';
|
||||
import * as smartstate from '../ts/index.js';
|
||||
|
||||
type TMyStateParts = 'testStatePart';
|
||||
@@ -39,7 +39,7 @@ tap.test('should select something', async () => {
|
||||
tap.test('should dispatch a state action', async (tools) => {
|
||||
const done = tools.defer();
|
||||
const addFavourite = testStatePart.createAction<string>(async (statePart, payload) => {
|
||||
const currentState = statePart.getState();
|
||||
const currentState = statePart.getState()!;
|
||||
return {
|
||||
...currentState,
|
||||
currentFavorites: [...currentState.currentFavorites, payload],
|
||||
@@ -53,7 +53,7 @@ tap.test('should dispatch a state action', async (tools) => {
|
||||
done.resolve();
|
||||
});
|
||||
await testStatePart.dispatchAction(addFavourite, 'my favourite things');
|
||||
expect(testStatePart.getState().currentFavorites).toContain('my favourite things');
|
||||
expect(testStatePart.getState()!.currentFavorites).toContain('my favourite things');
|
||||
await done.promise;
|
||||
});
|
||||
|
||||
|
||||
@@ -3,6 +3,6 @@
|
||||
*/
|
||||
export const commitinfo = {
|
||||
name: '@push.rocks/smartstate',
|
||||
version: '2.3.0',
|
||||
version: '2.3.1',
|
||||
description: 'A TypeScript-first reactive state management library with middleware, computed state, batching, persistence, and Web Component Context Protocol support.'
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ export interface IActionContext<TStateType> {
|
||||
}
|
||||
|
||||
export interface IActionDef<TStateType, TActionPayloadType> {
|
||||
(stateArg: StatePart<any, TStateType>, actionPayload: TActionPayloadType, context?: IActionContext<TStateType>): Promise<TStateType>;
|
||||
(stateArg: StatePart<any, TStateType>, actionPayload: TActionPayloadType, context: IActionContext<TStateType>): Promise<TStateType>;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
"target": "ES2022",
|
||||
"module": "NodeNext",
|
||||
"moduleResolution": "NodeNext",
|
||||
"noImplicitAny": true,
|
||||
"esModuleInterop": true,
|
||||
"verbatimModuleSyntax": true,
|
||||
"types": ["node"]
|
||||
|
||||
Reference in New Issue
Block a user