Skip to content

feat(state): enable application state management - #3286

Merged
odinr merged 37 commits into
mainfrom
feat/state/add-state-module
Aug 5, 2026
Merged

feat(state): enable application state management#3286
odinr merged 37 commits into
mainfrom
feat/state/add-state-module

Conversation

@odinr

@odinr odinr commented Aug 25, 2025

Copy link
Copy Markdown
Collaborator

Overview

Adds persistent, optionally-replicated application state to Fusion Framework: a new
@equinor/fusion-framework-module-state package, framework- and React-level enablers, a
useAppState hook that behaves like useState but persists and syncs across components, and a
useStateSyncEvents hook for observing replication status. Includes a cookbook
(cookbooks/app-react-state) demonstrating all of it, from zero-config local storage to CouchDB
replication.

Related

  • Delivers the "pilot app integration" blocker on equinor/fusion-core-tasks#538
    Persistent User State).
  • closes equinor/fusion-core-tasks#1669

What's new

@equinor/fusion-framework-module-state (new package)

  • StateProvider - reactive state management backed by a pluggable storage interface.
  • PouchDbStorage - local, per-app persistent storage (PouchDB), zero configuration required.
  • PouchDbSyncStorage - bidirectional replication with a remote CouchDB, with configurable
    PouchDB.Replication.SyncOptions (live, retry, heartbeat, timeout).
  • A typed event system (StateEntryCreatedEvent/UpdatedEvent/DeletedEvent,
    StateSyncStatusEvent/ChangeEvent/CompleteEvent/ErrorEvent) dispatched through the app's
    event module.

@equinor/fusion-framework-app

  • enableState - a thin, app-scoped convenience wrapper around enableStateModule.

@equinor/fusion-framework-react-app

  • enableAppState - configures the app to use the state module, with app-scoped storage key
    prefixing to prevent collisions between apps.
  • useAppState - a persistent, cross-component-synchronized alternative to useState.
  • useStateSyncEvents - subscribes to onStateSync.status/.change/.complete/.error and
    returns the most recent events, bounded to a given limit.
import { enableAppState } from '@equinor/fusion-framework-react-app/state';

export const configure = (configurator) => {
  enableAppState(configurator);
};
import { useAppState, useStateSyncEvents } from '@equinor/fusion-framework-react-app/state';

const [count, setCount] = useAppState('counter', { defaultValue: 0 });
const events = useStateSyncEvents(20);

Cookbook: cookbooks/app-react-state

Merges the earlier app-react-state and app-react-state-replication cookbooks into one, covering:

  • Basics - useAppState fundamentals (boolean, string, optional state).
  • Profile - object state with immutable updates, no reducer/action-creator library needed.
  • Todos - array-in-object state (add/toggle/remove/filter).
  • Sync Status Monitor - a live view of useStateSyncEvents.

Runs with zero setup (local storage only); pnpm couchdb:start opts into a local CouchDB for the
replication demo.

Checklist

@changeset-bot

changeset-bot Bot commented Aug 25, 2025

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: bd4498e

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 23 packages
Name Type
@equinor/fusion-framework-app Major
@equinor/fusion-framework-cookbook-app-react-state Major
@equinor/fusion-framework-module-state Major
@equinor/fusion-framework-module Patch
@equinor/fusion-framework-react-app Major
@equinor/fusion-framework-dev-portal Major
portal-analytics Patch
@equinor/fusion-framework-cli-plugin-ai-base Patch
@equinor/fusion-framework-cli-plugin-ai-chat Patch
@equinor/fusion-framework-cli-plugin-ai-index Patch
@equinor/fusion-framework-cli Patch
@equinor/fusion-framework Patch
@equinor/fusion-framework-module-analytics Patch
@equinor/fusion-framework-module-telemetry Patch
@equinor/fusion-framework-module-widget Patch
@equinor/fusion-framework-vite-plugin-spa Patch
poc-portal Patch
@equinor/fusion-framework-cookbook-app-react-ag-grid Patch
@equinor/fusion-framework-cookbook-app-react-context-custom-error Patch
@equinor/fusion-framework-cookbook-app-react-context Patch
@equinor/fusion-framework-cookbook-app-react-feature-flag Patch
@equinor/fusion-framework-dev-server Patch
portal Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@github-actions github-actions Bot added 👨🏻‍🍳 cookbooks 👾 React 📚 documentation Improvements or additions to documentation 🚀 feature New feature or request 🧬 Modules 🐞 bug Something isn't working 💾 CLI fusion framework CLI 🚧 chore maintaines work, (update deps, workflos ...) labels Aug 25, 2025
Comment thread packages/app/src/enable-state.ts Outdated
@odinr
odinr force-pushed the feat/state/add-state-module branch 3 times, most recently from 30516d1 to 722aef3 Compare August 29, 2025 06:44
@github-actions github-actions Bot removed 💾 CLI fusion framework CLI 🐞 bug Something isn't working labels Aug 29, 2025
@odinr
odinr force-pushed the feat/state/add-state-module branch from 722aef3 to 286494f Compare August 29, 2025 06:48
@github-actions github-actions Bot removed the 🚧 chore maintaines work, (update deps, workflos ...) label Aug 29, 2025
@odinr
odinr force-pushed the feat/state/add-state-module branch 5 times, most recently from 9608c15 to caaa767 Compare September 1, 2025 11:37
@odinr
odinr force-pushed the feat/state/add-state-module branch from 4d0e955 to e68c6d8 Compare September 29, 2025 13:43
@alftore
alftore force-pushed the feat/state/add-state-module branch from e68c6d8 to b5097de Compare June 25, 2026 12:56
Comment thread cookbooks/app-react-state-replication/src/App.tsx Fixed
Comment thread packages/app/src/AppConfigurator.ts Fixed
Comment thread packages/modules/state/src/StateProvider.ts Fixed
Comment thread packages/modules/state/src/storage/PouchDbSyncStorage.ts Fixed
@github-actions github-actions Bot added the 🐞 bug Something isn't working label Jun 25, 2026
alftore added a commit that referenced this pull request Jun 25, 2026
- remove unused ProfileManager import in app-react-state-replication/App.tsx
- remove unused AppModulesInitializedEvent import in AppConfigurator.ts
- remove unused 'type' from event destructuring in StateProvider.ts
- export PouchDbSyncStorage class
- add revs_limit and size options to PouchDbStorageOptions
@alftore
alftore force-pushed the feat/state/add-state-module branch from e3b44fd to e6de866 Compare June 25, 2026 20:29
@alftore

alftore commented Jun 25, 2026

Copy link
Copy Markdown
Contributor

Added revs_limit and size to PouchDbStorageOptions with JSDoc in e6de866. Both are already present on PouchDB.Configuration.LocalDatabaseConfiguration (which DatabaseConfiguration unions over), but the explicit declaration makes them discoverable without needing to narrow the union type, and the valid size values (10/50/100/500/1000 MB) are now documented inline including the Safari note.

odinr pushed a commit that referenced this pull request Aug 4, 2026
- remove unused ProfileManager import in app-react-state-replication/App.tsx
- remove unused AppModulesInitializedEvent import in AppConfigurator.ts
- remove unused 'type' from event destructuring in StateProvider.ts
- export PouchDbSyncStorage class
- add revs_limit and size options to PouchDbStorageOptions
odinr added 22 commits August 5, 2026 12:15
Switch from the deprecated useRouter (@equinor/fusion-framework-react-app/navigation)
+ interop RouterProvider pattern to the framework's canonical Router
component from @equinor/fusion-framework-react-router, matching the
app-react-router cookbook. This also removes a react-router/@remix-run/router
type mismatch (RouteObject/Router) that showed up under tsc -b.
- route PouchDB replication fetch through the Fusion IHttpClient instead of native fetch + manual MSAL token acquisition
- remove the non-functional sync retry circuit breaker from PouchDbSyncStorage
- add missing TSDoc for StateModuleConfigurator.setName
…o app-react-state

Consolidate both cookbooks into one, covering the full module-state surface (basics, profile/todo replication via PouchDbSyncStorage, sync-status UI). Replace the cookbook's hand-rolled sync-event module with the framework's native onStateSync.* events.
…nt wiring

packages/react/app: add useStateSyncEvents hook subscribing to onStateSync.* events.

packages/modules/state: module.ts resolved the event module via init.ref, which races against the framework's concurrent module initialization and could leave StateProvider without a valid event module reference. Resolve it via requireInstance instead, matching the hasModule/requireInstance pattern used by every other module with an optional event dependency.
Merge react-app_useAppState.md and react-app_use-state-sync-events.md into a single react-app_state-support.md changeset (fixing an incorrect major bump for what is a purely additive change). Drop module-state_lint-cleanup.md, an internal-only note about an unreleased package. Also fix a formatting regression in packages/react/app/package.json introduced earlier in this branch.
- Collapse ProfileManager from 10 files (fusion-observable reducer +
  action creators + 7 single-field components) into one component
  using plain useAppState + immutable object spreads, matching the
  Basics/Todos pattern. Drops the now-unused fusion-observable dep.
- Remove leftover debug console.logs and a stray junk property in
  TodoListManager and SyncStatusMonitor.
- Remove dead SyncStatus/ReplicationSettings types left over from the
  pre-merge hand-rolled sync implementation.
- Fix stale comments/README wording implying replication or a reducer
  pattern is always active, when both are optional.
- Amend the existing merge changeset with these follow-up bullets.
Fixes JSX/formatting diagnostics reported by reviewdog (Biome) across the
app-react-state cookbook, StateModuleConfigurator, module-state's public
export barrel, and the useStateSyncEvents hook + its test.
…entType

StateErrorEvent (onState.error) is a general operation-error event, never
emitted by observePouchDbSync and never matched by StateSyncEvent.is(). Its
presence in the StateSyncEventType union misled consumers into thinking
sync-scoped filters (e.g. hooks) could observe it.
The "./events" export's "types" field pointed at the typo'd
"./dist/types/storeventsage/index.d.ts", breaking TypeScript resolution for
@equinor/fusion-framework-module-state/events. Also add the missing
"events" typesVersions entry so older TS module resolution modes resolve
the subpath too.
…_sync

_sync() only destructured live/retry/heartbeat/timeout and discarded any
other caller-provided SyncOptions (filter, since, batches_limit, etc.) when
building the options object passed to PouchDB. Spread the caller options
first so only the four defaulted fields are overridden.
- Effect that subscribes via stateProvider.observeItem depended on
  stateProvider.observeItem (a stable method reference) instead of
  stateProvider itself, so the effect could fail to re-run when the
  provider instance changed.
- setValue's updater branch received the raw, possibly-undefined
  value$.value instead of the defaulted value, deviating from useState-like
  behavior. getValue is now applied before invoking the updater, and added
  to the callback's dependency list.
The reduce<Record<string, any>, ...> generics added to fix inference under
stricter TS settings introduced new explicit any. unknown fixes the same
inference issue without weakening the type system.
The example imported enableState from @equinor/fusion-framework-app/state,
but the package only exports the enabler under ./enable-state - the
example would not resolve for consumers as written.
{\@link configureModules} rendered literally instead of linking because the
@ was escaped.
Set and Record keyed by symbol/number aren't JSON-serializable and don't
round-trip through the default PouchDB-backed storage, contradicting the
persistence contract documented for useAppState.
The file ended with a JSDoc comment describing a validation function that
was never implemented, plus a stray '@see' referencing a nonexistent Zod
schema. Removed both - neither documents anything that exists in the file.
Several files inside @equinor/fusion-framework-module-state imported
AllowedValue/StateEventType by importing the package's own name. Before
the package is built, that self-import has no dist/types/* to resolve
against and breaks a clean tsc -b. Switched all of them to relative
imports (../types.js, ../events/index.js).
Both packages share the same caution paragraph; 'it`s' should be 'it's'.
Added as a runtime dependency but never imported anywhere in the package.
- Example used useAppState<any[]>, undermining the type-safety message and
  leaking any into copy-pasted consumer code; changed to unknown[].
- A duplicate closing code fence after the Key Stability example broke
  Markdown rendering for the rest of the doc comment.
- Fixed a backtick-instead-of-apostrophe typo in an inline comment.
- split packages/modules/state/README.md into docs/{usage,storage,events,api-reference,performance}.md
- fix broken code fence in storage docs (Monitoring Sync Progress)
- drop fictional v1->v2 migration guide (package is pre-1.0)
- add vue-press pages + sidebar children for state module docs
- add react-app-state cookbook page and sort/rename cookbooks index
Noggling
Noggling previously approved these changes Aug 5, 2026
@odinr
odinr force-pushed the feat/state/add-state-module branch from e611582 to bd4498e Compare August 5, 2026 10:45
@odinr
odinr merged commit b92698d into main Aug 5, 2026
11 of 12 checks passed
@odinr
odinr deleted the feat/state/add-state-module branch August 5, 2026 10:47
@github-actions

github-actions Bot commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

Coverage Report

Status Category Percentage Covered / Total
🔵 Lines 65.13% 3602 / 5530
🔵 Statements 64.7% 4287 / 6625
🔵 Functions 51.51% 1239 / 2405
🔵 Branches 55.16% 1985 / 3598
File Coverage
File Stmts Branches Functions Lines Uncovered Lines
Changed Files
packages/app/src/AppConfigurator.ts 36.36% 33.33% 25% 36.36% 169-187, 201-274
packages/app/src/AppConfiguratorError.ts 0% 100% 0% 0% 28-30
packages/app/src/AppModulesConfiguredEvent.ts 0% 100% 0% 0% 34
packages/app/src/utils.ts 85.71% 85.71% 100% 85.71% 42
packages/modules/module/src/lib/configurator/phases/create-module-configs.ts 100% 100% 90% 100%
packages/modules/state/src/StateModule.ts 100% 100% 100% 100%
packages/modules/state/src/StateModuleConfig.ts 100% 100% 100% 100%
packages/modules/state/src/StateModuleConfigurator.ts 0% 0% 0% 0% 53-85
packages/modules/state/src/StateProvider.interface.ts 100% 100% 100% 100%
packages/modules/state/src/StateProvider.ts 90% 75% 65.21% 91.22% 85-92, 364, 241-251, 277-285
packages/modules/state/src/enable-state-module.ts 40% 100% 0% 33.33% 21-23, 23
packages/modules/state/src/index.ts 100% 100% 100% 100%
packages/modules/state/src/module.ts 45.45% 0% 0% 71.42% 23-22
packages/modules/state/src/name.ts 100% 100% 100% 100%
packages/modules/state/src/state-module-config-schema.ts 80% 100% 0% 66.66% 16
packages/modules/state/src/types.ts 100% 100% 100% 100%
packages/modules/state/src/validate-state-module-config.ts 50% 100% 0% 50% 6, 6
packages/modules/state/src/events/StateEntryCreatedEvent.ts 90% 75% 75% 87.5% 57
packages/modules/state/src/events/StateEntryDeletedEvent.ts 90% 75% 75% 87.5% 57
packages/modules/state/src/events/StateEntryUpdatedEvent.ts 90% 75% 75% 87.5% 57
packages/modules/state/src/events/StateErrorEvent.ts 11.11% 0% 50% 12.5% 40-45
packages/modules/state/src/events/StateOperationFailureEvent.ts 20% 0% 50% 12.5% 38-55, 43
packages/modules/state/src/events/StateOperationSuccessEvent.ts 30% 0% 75% 25% 40-48, 45
packages/modules/state/src/events/StateSyncChangeEvent.ts 70% 58.33% 50% 62.5% 43, 50-59
packages/modules/state/src/events/StateSyncCompleteEvent.ts 70% 58.33% 50% 62.5% 52, 59-68
packages/modules/state/src/events/StateSyncErrorEvent.ts 80% 66.66% 75% 75% 56, 65
packages/modules/state/src/events/StateSyncStatusEvent.ts 80% 66.66% 75% 75% 41, 48
packages/modules/state/src/events/index.ts 87.5% 55.55% 33.33% 83.33% 84
packages/modules/state/src/storage/PouchDbStorage.ts 54.95% 57.14% 60.46% 55.61% 119, 152-164, 206, 240-243, 266, 274-282, 298, 313, 315-407, 454-476, 510-521, 552-561, 627-695, 753-837, 889-898, 927-933, 959-970, 1032-1041, 1083, 1090-1096, 1130, 1151-1174
packages/modules/state/src/storage/StorageError.ts 0% 100% 0% 0% 31-33
packages/modules/state/src/storage/observe-pouch-db-change.ts 75% 28.57% 66.66% 72.72% 68-78, 83-84, 98-105, 120, 68
packages/modules/state/src/storage/observe-pouch-db-sync.ts 0% 0% 0% 0% 77-127
packages/react/app/src/state/useStateSyncEvents.ts 94.73% 75% 88.88% 93.75% 55
Generated in workflow #15121 for commit bd4498e by the Vitest Coverage Report Action

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

🐞 bug Something isn't working 🚧 chore maintaines work, (update deps, workflos ...) 👨🏻‍🍳 cookbooks 📚 documentation Improvements or additions to documentation 🚀 feature New feature or request 🧬 Modules 👾 React

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants