Skip to content

Commit 236fa39

Browse files
committed
Claude.md updates
1 parent 3e949c9 commit 236fa39

3 files changed

Lines changed: 120 additions & 112 deletions

File tree

CLAUDE.md

Lines changed: 12 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,9 @@
22

33
## Project Overview
44

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.
66

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.
88

99
## Monorepo Structure
1010

@@ -34,7 +34,7 @@ esp-js (no esp deps)
3434
- **Package manager**: npm (workspaces)
3535
- **Monorepo orchestration**: Lerna 8 (`lerna.json`)
3636
- **Per-package build scripts**: delegated to `nps` (`package-scripts.js` at root defines shared scripts)
37-
- **Versioning**: Lerna fixed-mode — all packages share the same version (`8.1.0`)
37+
- **Versioning**: Lerna fixed-mode — all packages share the same version (`8.1.0`; next release targets `9.0.0`)
3838
- **Publishing**: `forcePublish: true` — every package is always published together
3939

4040
### Key root commands
@@ -101,9 +101,9 @@ npm run test-ci # jest (no watch, CI mode)
101101
5. `npm test` from inside a package for watch-mode tests
102102
6. `npm run build-prod && npm test` from root before committing
103103

104-
**Adding a new package**: use `yarn create-package` (invokes `nps create-package`).
104+
**Adding a new package**: invoke `nps create-package` from the repo root.
105105

106-
**Local linking**: Yarn workspaces handle symlinking automatically — no `yarn link` required.
106+
**Local linking**: npm workspaces handle symlinking automatically — no manual linking required.
107107

108108
## Key Architectural Patterns
109109

@@ -115,13 +115,15 @@ npm run test-ci # jest (no watch, CI mode)
115115
- `normal` — primary mutation stage
116116
- `committed` — only fires if `eventContext.commit()` was called during `normal`
117117
- `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)`)
119119

120-
### OO model pattern (esp-js core)
120+
### Functional model pattern (esp-js core)
121121

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
125127

126128
### React integration (esp-js-react)
127129

@@ -130,13 +132,6 @@ npm run test-ci # jest (no watch, CI mode)
130132
- `useSyncModelWithSelector` — hook-based subscription with selector and equality function
131133
- `@viewBinding(MyView)` decorator on model class — declaratively binds a React component to the model
132134

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-
140135
## Coding Conventions
141136

142137
- **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)
156151
| `__jest__/jest.config.js` | Shared Jest configuration |
157152
| `__jest__/typeScriptPreprocessor.ts` | Jest TypeScript transform |
158153
| `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 |
161154
| `webpack/peerDepsExternalsPlugin.js` | Auto-externalizes peer dependencies in webpack bundles |

packages/esp-js-react/CLAUDE.md

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ const MyConnectedView = connect<MyModel, MyPublishProps>(
7373

7474
### useSyncModelWithSelector
7575

76-
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.
7777

7878
```tsx
7979
const orders = useSyncModelWithSelector<OrdersState>(
@@ -85,8 +85,6 @@ const orders = useSyncModelWithSelector<OrdersState>(
8585
);
8686
```
8787

88-
If the model exposes a `getEspPolimerImmutableModel()` method, `tryPreSelectPolimerImmutableModel: true` (default) automatically calls it before passing the result to the selector.
89-
9088
### EspModelContextProvider
9189

9290
For cases where a subtree shares a single `modelId`:
@@ -139,10 +137,13 @@ export { EspModelContextProvider, useGetModelId, useGetModel, usePublishModelEve
139137
- Test files: `tests/**/*Tests.tsx`
140138
- Uses `@testing-library/react` for rendering and interaction
141139
- `tests/testApi/` — shared test fixtures including mock router and model helpers
140+
- `testModel.ts``createTestModel(router, modelId)` registers a model via `ModelBuilder`
141+
- `testApi.tsx``setupTestModel(modelId)` and `setupModel(modelId, model)` helpers
142+
- `routerSpy.ts``RouterSpy extends Router`, wraps `getModelObservable()` to count subscriptions (does **not** use `Observable.create` — reactive module is internal)
142143
- Notable test files:
143-
- `connectableComponentTests.tsx` — HOC subscription and re-render behaviour
144+
- `connectableComponentTests.tsx` — HOC subscription and re-render behaviour; models use `[immerable] = true`
144145
- `useSyncModelWithSelectorTests.tsx` — hook selector and equality function behaviour
145-
- `espModelContextProviderTests.tsx`, `espRouterContextProviderTests.tsx` — context hook tests
146+
- `espModelContextProviderTests.tsx`, `espRouterContextProviderTests.tsx` — context hook tests; use `router.getModel()` to read current snapshot after events
146147
- `viewBinderTests.tsx``@viewBinding` resolution logic
147148

148149
## Common Tasks
@@ -188,3 +189,5 @@ class MyModel extends ModelBase { ... }
188189
- `useSyncModelWithSelector` uses `useSyncExternalStoreWithSelector` from React 18 — requires React 18 or the `use-sync-external-store` shim
189190
- `ConnectableComponent` re-renders on every model update regardless of selector — for performance-sensitive cases prefer `useSyncModelWithSelector` with an equality function
190191
- `@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

Comments
 (0)