Skip to content

Commit cb74ce1

Browse files
authored
Decompose StateImpl class for tree-shaking (#35)
* perf(core): add benchmark suite * refactor(core): decompose StateImpl class for tree-shaking Replace monolithic StateImpl class with module-level functions and a createState closure factory. Bundlers can now eliminate unused exports — importing only `state` drops derive, select, lens and all lens helpers (87.5% size reduction for state-only consumers). * docs: update architecture docs for tree-shaking and closure factory * perf(bench): add per-benchmark heap usage reporting Measure heap delta per benchmark run using process.memoryUsage().heapUsed with forced GC before each run for a clean baseline. Reports median heap delta in KB alongside existing timing stats. Adds --expose-gc flag to the benchmark npm script. * chore(biome): bump schema to 2.3.14 and reorder file includes * chore(deps): bump biome 2.3.14, @types/node 25.2.2, npm 11.9.0 * chore(assets): backport v2 SVG logo from epoch-2
1 parent ef855b1 commit cb74ce1

10 files changed

Lines changed: 709 additions & 1466 deletions

File tree

.github/README.md

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
1-
# Beacon <img align="right" src="https://raw.githubusercontent.com/nerdalytics/beacon/refs/heads/trunk/assets/beacon-logo.svg" width="128px" alt="A stylized lighthouse beacon with golden light against a dark blue background, representing the reactive state library"/>
1+
# Beacon <img align="right" src="https://raw.githubusercontent.com/nerdalytics/beacon/refs/heads/trunk/assets/beacon-logo-v2.svg" width="128px" alt="A stylized lighthouse beacon with golden light against a dark blue background, representing the reactive state library"/>
22

33
> Lightweight reactive state management for Node.js backends
44
55
[![license:mit](https://flat.badgen.net/static/license/MIT/blue)](https://github.com/nerdalytics/beacon/blob/trunk/LICENSE)
66
[![registry:npm:version](https://img.shields.io/npm/v/@nerdalytics/beacon.svg)](https://www.npmjs.com/package/@nerdalytics/beacon)
7-
[![Socket Badge](https://badge.socket.dev/npm/package/@nerdalytics/beacon/1000.2.5)](https://socket.dev/npm/package/@nerdalytics/beacon/overview/1000.2.5)
7+
[![Socket Badge](https://badge.socket.dev/npm/package/@nerdalytics/beacon/1000.3.0)](https://socket.dev/npm/package/@nerdalytics/beacon/overview/1000.3.0)
88

99
[![tech:nodejs](https://img.shields.io/badge/Node%20js-339933?style=for-the-badge&logo=nodedotjs&logoColor=white)](https://nodejs.org/)
1010
[![language:typescript](https://img.shields.io/badge/TypeScript-007ACC?style=for-the-badge&logo=typescript&logoColor=white)](https://typescriptlang.org/)
@@ -58,6 +58,7 @@ A lightweight reactive state library for Node.js backends. Enables reactive stat
5858
- **Cycle handling** - Safely manages cyclic dependencies without crashing
5959
- **Infinite loop detection** - Automatically detects and prevents infinite update loops
6060
- **TypeScript-first** - Full TypeScript support with generics
61+
- **Tree-shakeable** - Module-level exports let bundlers eliminate unused code
6162
- **Lightweight** - Zero dependencies
6263
- **Node.js compatibility** - Works with Node.js LTS v20+ and v22+
6364

@@ -444,9 +445,10 @@ Beacon follows these key principles:
444445

445446
## Architecture
446447

447-
Beacon is built around a centralized reactivity system with fine-grained dependency tracking. Here's how it works:
448+
Beacon is built around module-level functions and a closure-based state factory with fine-grained dependency tracking. All exports are independent `const` declarations, so bundlers can tree-shake unused primitives. Here's how it works:
448449

449450
- **Automatic Dependency Collection**: When a state is read inside an effect, Beacon automatically records this dependency
451+
- **Closure-based State**: Each `state()` call returns a closure over its value and subscriber set
450452
- **WeakMap-based Tracking**: Uses WeakMaps for automatic garbage collection
451453
- **Topological Updates**: Updates flow through the dependency graph in the correct order
452454
- **Memory-Efficient**: Designed for long-running Node.js processes
@@ -531,7 +533,7 @@ app.listen(3000);
531533

532534
#### Can Beacon be used in browser applications?
533535

534-
While Beacon is optimized for Node.js server-side applications, its core principles would work in browser environments. However, the library is specifically designed for backend use cases and hasn't been optimized for browser bundle sizes or DOM integration patterns.
536+
While Beacon is designed for Node.js server-side applications, it works in browser environments too. The library is fully tree-shakeable (`sideEffects: false`), so bundlers like esbuild, Rollup, and webpack will eliminate any primitives you don't import. It has not been optimized for DOM integration patterns.
535537

536538
## License
537539

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
1-
# Beacon <img align="right" src="https://raw.githubusercontent.com/nerdalytics/beacon/refs/heads/trunk/assets/beacon-logo.svg" width="128px" alt="A stylized lighthouse beacon with golden light against a dark blue background, representing the reactive state library"/>
1+
# Beacon <img align="right" src="https://raw.githubusercontent.com/nerdalytics/beacon/refs/heads/trunk/assets/beacon-logo-v2.svg" width="128px" alt="A stylized lighthouse beacon with golden light against a dark blue background, representing the reactive state library"/>
22

33
> Lightweight reactive state management for Node.js backends
44
55
[![license:mit](https://flat.badgen.net/static/license/MIT/blue)](https://github.com/nerdalytics/beacon/blob/trunk/LICENSE)
66
[![registry:npm:version](https://img.shields.io/npm/v/@nerdalytics/beacon.svg)](https://www.npmjs.com/package/@nerdalytics/beacon)
7-
[![Socket Badge](https://badge.socket.dev/npm/package/@nerdalytics/beacon/1000.2.5)](https://socket.dev/npm/package/@nerdalytics/beacon/overview/1000.2.5)
7+
[![Socket Badge](https://badge.socket.dev/npm/package/@nerdalytics/beacon/1000.3.0)](https://socket.dev/npm/package/@nerdalytics/beacon/overview/1000.3.0)
88

99
[![tech:nodejs](https://img.shields.io/badge/Node%20js-339933?style=for-the-badge&logo=nodedotjs&logoColor=white)](https://nodejs.org/)
1010
[![language:typescript](https://img.shields.io/badge/TypeScript-007ACC?style=for-the-badge&logo=typescript&logoColor=white)](https://typescriptlang.org/)

TECHNICAL_DETAILS.md

Lines changed: 14 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,15 @@ This document describes the internal implementation details of the Beacon librar
44

55
## Reactive System Architecture
66

7-
Beacon uses a fine-grained reactivity system with automatic dependency tracking. Here's how the core architecture works:
7+
Beacon uses a fine-grained reactivity system with automatic dependency tracking. The entire library lives in a single module (`src/index.ts`) as independent `const` function declarations sharing module-level reactive state. This design makes every export tree-shakeable — bundlers can eliminate any primitive a consumer doesn't import.
88

9-
1. **State Primitives**: Base reactive values that can be read and modified
9+
Here's how the core architecture works:
10+
11+
1. **State Primitives**: Base reactive values created by a closure factory (`createState`)
1012
2. **Derived Values**: Computed values that depend on other reactive states
1113
3. **Effects**: Side effects that run when dependencies change
1214
4. **Batching**: Optimization for multiple state changes
13-
5. **Dependency Tracking**: Automatic tracking of dependencies
15+
5. **Dependency Tracking**: Automatic tracking via module-level WeakMaps
1416
6. **Selectors**: Targeted subscriptions to subsets of state objects
1517

1618
### Core API Components
@@ -27,13 +29,15 @@ Beacon's API consists of the following key functions:
2729

2830
### Dependency Tracking Mechanism
2931

32+
Reactive state is coordinated through 10 module-level variables (5 mutable, 5 WeakMap/Set constants). Each `state()` call returns a closure that captures its own `value`, `subscribers` Set, and `stateId` Symbol, while reading and writing the shared module-level tracking structures.
33+
3034
When an effect or derived state runs:
3135

32-
1. The global `currentSubscriber` variable is set to the current effect
36+
1. The module-level `currentSubscriber` variable is set to the current effect
3337
2. Reading any state during execution registers the state as a dependency
3438
3. A bidirectional relationship is established:
35-
- The state keeps track of its subscribers (effects that depend on it)
36-
- The effect keeps track of its dependencies (states it depends on)
39+
- The state's closure-scoped `subscribers` Set tracks which effects depend on it
40+
- The module-level `subscriberDependencies` WeakMap tracks which subscriber sets each effect belongs to
3741
4. When a state changes, it notifies all its subscribers
3842

3943
## Cyclical Dependencies
@@ -262,11 +266,12 @@ This system ensures there are no memory leaks from lingering effect subscription
262266

263267
Several optimizations make Beacon efficient:
264268

265-
1. Set-based dependency tracking for fast operations
266-
2. Value equality checks to prevent unnecessary updates
267-
3. Specialized handling for small subscriber sets
269+
1. Closure-based state factory — eliminates class instantiation and method dispatch overhead
270+
2. Set-based dependency tracking for fast operations
271+
3. Value equality checks to prevent unnecessary updates
268272
4. Efficient batching to minimize effect executions
269273
5. WeakMap for subscriber dependencies to allow garbage collection
274+
6. Module-level `const` exports with `sideEffects: false` — enables tree-shaking of unused primitives
270275

271276
---
272277

assets/beacon-logo-v2.svg

Lines changed: 1 addition & 0 deletions
Loading

biome.json

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
{
2-
"$schema": "https://biomejs.dev/schemas/2.3.13/schema.json",
2+
"$schema": "https://biomejs.dev/schemas/2.3.14/schema.json",
33
"assist": {
44
"actions": {
55
"source": {
@@ -12,9 +12,9 @@
1212
"files": {
1313
"ignoreUnknown": true,
1414
"includes": [
15-
"**/*.ts",
15+
"*.json",
1616
"**/*.js",
17-
"**/*.json",
17+
"**/*.ts",
1818
"!package-lock.json",
1919
"!**/tests/template.test.ts",
2020
"!**/dist/**/*",

node.config.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
{
2-
"$schema": "https://nodejs.org/dist/v24.10.0/docs/node-config-schema.json",
2+
"$schema": "https://nodejs.org/dist/v24.13.0/docs/node-config-schema.json",
33
"nodeOptions": {
44
"test-coverage-branches": 100,
55
"test-coverage-exclude": [

0 commit comments

Comments
 (0)