You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Copy file name to clipboardExpand all lines: CLAUDE.md
+12-19Lines changed: 12 additions & 19 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -2,9 +2,9 @@
2
2
3
3
## Project Overview
4
4
5
-
ESP is a TypeScript/JavaScript framework for managing model state changes in a deterministic, event-driven manner. A central `Router` sits between event publishers and models: publishers call `router.publishEvent(modelId, eventType, event)`, the router queues and dispatches events through ordered observation stages, and the mutated model is then pushed to model observers. The framework supports both OO and immutable (Redux-like) modeling patterns and is designed for complex composite single-page applications.
5
+
ESP is a TypeScript/JavaScript framework for managing model state changes in a deterministic, event-driven manner. A central `Router` sits between event publishers and models: publishers call `router.publishEvent(modelId, eventType, event)`, the router queues and dispatches events through ordered observation stages, and a frozen immutable snapshot of the mutated model is then pushed to model observers. The framework uses an immer-based functional modeling pattern (`ModelBuilder`) and is designed for complex composite single-page applications.
6
6
7
-
The monorepo contains the core router, dependency injection container, React integration, immutable model support, RxJS utilities, a composite-app UI framework, and supporting packages.
7
+
The monorepo contains the core router, dependency injection container, and React integration.
@@ -115,13 +115,15 @@ npm run test-ci # jest (no watch, CI mode)
115
115
-`normal` — primary mutation stage
116
116
-`committed` — only fires if `eventContext.commit()` was called during `normal`
117
117
-`final` — fires regardless of commit; observe after all mutation
118
-
3. After all events for a model are processed, the model is pushed to model observers (`router.getModelObservable(modelId)`)
118
+
3. After all events for a model are processed, a frozen immutable snapshot of the model is pushed to model observers (`router.getModelObservable(modelId)`)
119
119
120
-
### OO model pattern (esp-js core)
120
+
### Functional model pattern (esp-js core)
121
121
122
-
- Extend `ModelBase` (from `esp-js`, re-exported by `esp-js-ui`)
123
-
- Decorate handler methods with `@observeEvent(eventType)` or `@observeEvent(eventType, ObservationStage.preview)`
124
-
- Call `this.observeEvents()` in the constructor to register the model and wire decorators
122
+
- Create a plain object or class instance as the initial state
123
+
- Register with `ModelBuilder`: `new ModelBuilder(router, modelId, initialState).withEventHandler(...).registerWithRouter()`
124
+
- Event handlers receive an immer `draft` — mutate it directly; the router produces a new frozen snapshot after all handlers run
125
+
- Preview handlers and effect handlers receive `Readonly<TModel>` (no draft)
126
+
- Class instances used as model state must include `[immerable] = true` (from `immer`) for immer to handle them
125
127
126
128
### React integration (esp-js-react)
127
129
@@ -130,13 +132,6 @@ npm run test-ci # jest (no watch, CI mode)
130
132
-`useSyncModelWithSelector` — hook-based subscription with selector and equality function
131
133
-`@viewBinding(MyView)` decorator on model class — declaratively binds a React component to the model
132
134
133
-
### Composite app (esp-js-ui)
134
-
135
-
-`Shell` — bootstraps the container, router, RegionManager, ViewRegistryModel; loads modules
136
-
-`ModuleBase` — abstract base for feature modules; decorated with `@espModule(key, name)`; each module gets a child DI container
137
-
-`ViewFactoryBase` — abstract base for view factories; decorated with `@viewFactory(viewKey, shortName)`; creates model+view pairs
138
-
-`RegionManager` — manages named regions; views are added/removed dynamically via `regionManager.addRegionItem(regionName, item)`
139
-
140
135
## Coding Conventions
141
136
142
137
-**TypeScript strictness**: `noImplicitAny: false`, `strictNullChecks: false` — the codebase does not use strict mode
@@ -156,6 +151,4 @@ npm run test-ci # jest (no watch, CI mode)
156
151
|`__jest__/jest.config.js`| Shared Jest configuration |
157
152
|`__jest__/typeScriptPreprocessor.ts`| Jest TypeScript transform |
158
153
|`tslint.json`| Lint rules applied during webpack build |
159
-
|`packages/esp-js-ui/src/ui/dependencyInjection/systemContainerConst.ts`| Well-known DI container key constants |
160
-
|`packages/esp-js-ui/src/ui/dependencyInjection/systemContainerConfiguration.ts`| Registers all framework services into the root container |
161
154
|`webpack/peerDepsExternalsPlugin.js`| Auto-externalizes peer dependencies in webpack bundles |
Hook-based approach using React 18's `useSyncExternalStore`:
76
+
Hook-based approach using React 18's `useSyncExternalStore`. The model emitted by the router is always a frozen immutable snapshot (produced by immer via `ModelBuilder`) — no unwrapping required.
If the model exposes a `getEspPolimerImmutableModel()` method, `tryPreSelectPolimerImmutableModel: true` (default) automatically calls it before passing the result to the selector.
89
-
90
88
### EspModelContextProvider
91
89
92
90
For cases where a subtree shares a single `modelId`:
-`espModelContextProviderTests.tsx`, `espRouterContextProviderTests.tsx` — context hook tests; use `router.getModel()` to read current snapshot after events
-`useSyncModelWithSelector` uses `useSyncExternalStoreWithSelector` from React 18 — requires React 18 or the `use-sync-external-store` shim
189
190
-`ConnectableComponent` re-renders on every model update regardless of selector — for performance-sensitive cases prefer `useSyncModelWithSelector` with an equality function
190
191
-`@viewBinding` metadata is stored on the **constructor function**, not the instance — `createViewForModel` must receive the model instance (it reads `model.constructor`)
192
+
- The model received by hooks and `ConnectableComponent` is always a **frozen immutable snapshot** from immer — never mutate it directly, and do not hold references across dispatches (the reference becomes stale)
193
+
- Class instances used as model state must include `[immerable] = true` from `immer`; subclasses inherit `@viewBinding` metadata via ES6 prototype chain (`class B extends A` means `B._viewMetadata` resolves via `Object.getPrototypeOf(B) === A`)
0 commit comments