Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,4 +24,5 @@ jobs:
cache: npm
- run: npm ci
- run: npm test
- run: npm run test:types
- run: npm pack --dry-run
9 changes: 8 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,14 @@

All notable changes to `hy-event-store` are documented in this file.

## 1.4.0 (unreleased)
## Unreleased

### Added

- Add first-party TypeScript declarations for event payloads, state keys,
actions, and action return values, plus compile-time regression coverage.

## 1.4.0 - 2026-08-19

### Added

Expand Down
26 changes: 22 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,12 +26,30 @@ Mini Program build workflows.
npm install hy-event-store
~~~

The package currently exposes a CommonJS entry point:
The package exposes a CommonJS entry point and ships first-party TypeScript
declarations:

~~~js
const { HYEventBus, HYEventStore } = require("hy-event-store")
~~~

TypeScript users can add event and state types without changing the runtime API:

~~~ts
const bus = new HYEventBus<{ ready: []; signedIn: [user: { name: string }] }>()
bus.on("signedIn", user => console.log(user.name))

const store = new HYEventStore({
state: { count: 0 },
actions: {
increment(state, amount: number) {
state.count += amount
return state.count
}
}
})
~~~

## Quick start

### Event bus
Expand Down Expand Up @@ -181,9 +199,9 @@ The distributed package is CommonJS and has no runtime dependencies. CI
validates the package on Node.js 18 through 26. Browser and Mini Program
projects should use their normal npm-package build or bundling workflow.

Native ESM exports, TypeScript declarations, a read-only state selector, and
an opt-in full snapshot mode for <code>onStates</code> are planned design
improvements; see the [roadmap](ROADMAP.md).
Native ESM exports, a read-only state selector, and an opt-in full snapshot
mode for <code>onStates</code> are planned design improvements; see the
[roadmap](ROADMAP.md).

## Development

Expand Down
21 changes: 19 additions & 2 deletions README.zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,12 +22,29 @@
npm install hy-event-store
~~~

当前包提供 CommonJS 入口:
当前包提供 CommonJS 入口,并内置第一方 TypeScript 声明

~~~js
const { HYEventBus, HYEventStore } = require("hy-event-store")
~~~

TypeScript 用户无需改变运行时 API,即可补充事件和状态类型:

~~~ts
const bus = new HYEventBus<{ ready: []; signedIn: [user: { name: string }] }>()
bus.on("signedIn", user => console.log(user.name))

const store = new HYEventStore({
state: { count: 0 },
actions: {
increment(state, amount: number) {
state.count += amount
return state.count
}
}
})
~~~

## 快速开始

### 事件总线
Expand Down Expand Up @@ -164,7 +181,7 @@ store.offStates(["count", "status"], renderProfile)

发布包为 CommonJS,且没有运行时依赖。CI 在 Node.js 18 至 26 上验证。浏览器和小程序项目应通过各自的 npm 包构建或打包流程使用本包。

原生 ESM 导出、TypeScript 声明、只读状态选择器与 <code>onStates</code> 的完整快照模式属于后续设计项,详见[路线图](ROADMAP.md)。
原生 ESM 导出、只读状态选择器与 <code>onStates</code> 的完整快照模式属于后续设计项,详见[路线图](ROADMAP.md)。

## 开发

Expand Down
2 changes: 1 addition & 1 deletion ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ keeping the public API compatible.

These items need API design or compatibility research before implementation:

- Add first-party TypeScript declarations
- Add first-party TypeScript declarations and compile-time coverage
([#25](https://github.com/coderwhy/hy-event-store/issues/25)).
- Document and test CommonJS, ESM, and Mini Program import paths
([#27](https://github.com/coderwhy/hy-event-store/issues/27)).
Expand Down
57 changes: 57 additions & 0 deletions Test/types.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { HYEventBus, HYEventStore } from ".."

type AppEvents = {
ready: []
signedIn: [user: { id: string; name: string }]
}

const bus = new HYEventBus<AppEvents>()

bus.on("signedIn", user => {
user.id.toUpperCase()
user.name.toUpperCase()
})
bus.emit("ready")
bus.emit("signedIn", { id: "1", name: "coderwhy" })

// @ts-expect-error An event payload must match its declared event tuple.
bus.emit("signedIn", "coderwhy")

const store = new HYEventStore({
state: {
count: 0,
status: "idle" as "idle" | "ready"
},
actions: {
increment(state, amount: number) {
state.count += amount
return state.count
},
async load(state, label: string) {
state.status = "ready"
return `${label}:${state.status}`
}
}
})

store.onState("count", count => {
count.toFixed()
})

store.onStates(["count", "status"], change => {
change.count?.toFixed()
change.status?.toUpperCase()
})

store.setState("count", 1)
store.setState("status", "ready")

const nextCount: number = store.dispatch("increment", 2)
const loaded: Promise<string> = store.dispatch("load", "home")

// @ts-expect-error State keys must exist in the declared state object.
store.setState("missing", 1)
// @ts-expect-error State values must match their declared key type.
store.setState("count", "1")
// @ts-expect-error Action arguments are inferred from the action signature.
store.dispatch("increment", "2")
106 changes: 106 additions & 0 deletions index.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
export type EventMap = Record<string, unknown[]>;

export type EventCallback<Payload extends unknown[] = unknown[]> = (
...payload: Payload
) => void;

export class HYEventBus<Events extends EventMap = EventMap> {
on<EventName extends Extract<keyof Events, string>>(
eventName: EventName,
eventCallback: EventCallback<Events[EventName]>,
thisArg?: unknown
): this;

once<EventName extends Extract<keyof Events, string>>(
eventName: EventName,
eventCallback: EventCallback<Events[EventName]>,
thisArg?: unknown
): this;

emit<EventName extends Extract<keyof Events, string>>(
eventName: EventName,
...payload: Events[EventName]
): this;

off<EventName extends Extract<keyof Events, string>>(
eventName: EventName,
eventCallback: EventCallback<Events[EventName]>
): this;

clear(): this;

hasEvent(eventName: string): boolean;
}

export type StoreAction<
State extends object,
Args extends unknown[] = unknown[],
Result = unknown
> = (state: State, ...args: Args) => Result;

export type StoreActions<State extends object> = Record<
string,
StoreAction<State, any[], any>
>;

export interface HYEventStoreOptions<
State extends object,
Actions extends StoreActions<State> = Record<never, never>
> {
state: State;
actions?: Actions & StoreActions<State>;
}

type StateKey<State extends object> = Extract<keyof State, string>;
type StateChange<State extends object, Key extends StateKey<State>> = Partial<
Pick<State, Key>
>;
type ActionArguments<State extends object, Action> = Action extends (
state: State,
...args: infer Args
) => unknown
? Args
: never;
type ActionResult<Action> = Action extends (...args: any[]) => infer Result
? Result
: never;

export class HYEventStore<
State extends object,
Actions extends StoreActions<State> = Record<never, never>
> {
constructor(options: HYEventStoreOptions<State, Actions>);

state: State;
actions: Actions;

onState<Key extends StateKey<State>>(
stateKey: Key,
stateCallback: (value: State[Key]) => void
): void;

offState<Key extends StateKey<State>>(
stateKey: Key,
stateCallback: (value: State[Key]) => void
): void;

onStates<Keys extends readonly StateKey<State>[]>(
stateKeys: Keys,
stateCallback: (change: StateChange<State, Keys[number]>) => void
): void;

offStates<Keys extends readonly StateKey<State>[]>(
stateKeys: Keys,
stateCallback: (change: StateChange<State, Keys[number]>) => void
): void;

setState<Key extends StateKey<State>>(
stateKey: Key,
stateValue: State[Key]
): void;

dispatch<ActionName extends Extract<keyof Actions, string>>(
actionName: ActionName,
...args: ActionArguments<State, Actions[ActionName]>
): ActionResult<Actions[ActionName]>;
}
19 changes: 18 additions & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

10 changes: 8 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,20 @@
"version": "1.4.0",
"description": "A zero-dependency event bus and global state store for Vue, React, Mini Programs, and vanilla JavaScript.",
"main": "src/index.js",
"types": "index.d.ts",
"files": [
"src",
"CHANGELOG.md",
"README.zh-CN.md"
"README.zh-CN.md",
"index.d.ts"
],
"scripts": {
"test": "node --test Test/*.test.js",
"test:coverage": "node --test --experimental-test-coverage Test/*.test.js"
"test:coverage": "node --test --experimental-test-coverage Test/*.test.js",
"test:types": "tsc --noEmit --strict --skipLibCheck --module commonjs --moduleResolution node --target es2020 Test/types.test.ts"
},
"devDependencies": {
"typescript": "^5.7.3"
},
"author": "coderwhy",
"license": "MIT",
Expand Down