armature 1.0.0
armature: ^1.0.0 copied to clipboard
Feature-based app framework with dependency-graph resolution, reactive stores, typed ports (pipes / behaviors / slots), and tasks.
1.0.0 #
First stable release. Public API surface in
package:armature/armature.dartis committed: every symbol exported from that barrel follows semver from here on. Code written against0.4.0continues to compile —1.0.0is additive on top of it.
Added — DX #
StoreListener/TaskBuilder(Flutter side) — new widgets inarmature_flutterthat pair with this release. See that package's changelog for details.TaskStateExtensionsonTaskState<TParams, TResult, TError>:.when({idle, pending, done, failed})— exhaustive pattern match..maybeWhen({...}, orElse:)— partial match with fallback..isIdle/.isPending/.isDone/.isFailed— branch booleans..paramsOrNull/.resultOrNull/.errorOrNull— branch payload getters.
- Task awaiters —
Task.firstWhere(predicate),Task.awaitDone(),Task.awaitFailed(),Task.awaitSettled(). Useful in tests and orchestration scripts that need to gate on a transition without spinning up a reaction. Store.subscribeSelect(selector, listener, {fireImmediately, equals})— equality-filtered projection with optional custom equality (e.g.listEqualsfor collection-typed selectors). Replaces theStoreSubscribeExtensionsproposal — lives directly onStore<T>.package:armature/framework.dart— new barrel for sibling-package plumbing. ExposesPort,AnyPort,PortType,PortSubscriptionforarmature_flutterand customRendererimplementations. Application code should not import it — the typed APIs inarmature.dartcover every end-user scenario.
Fixed #
- Listener errors in
Stateare now isolated. Each listener call is wrapped intry/catch; a single captured error rethrows with its original stack trace, multiple errors surface through the new internalStateListenerErrorsaggregate. A throwing listener no longer aborts its siblings or the surroundingstate =setter. Store.dispose()isolates eachtask.dispose()call so a misbehaving task can't prevent siblings from being torn down. Same single/aggregate semantics via internalTaskDisposeErrors.FeatureOrchestrator._onToggleswitched to a batched FIFO queue with microtask-deferred drain. Eliminates re-entrant cascade chains from inside lifecycle callbacks — resolves the deadlock undermaxResolveConcurrency: 1and the fixed-point livelock when a reactive subscription toggles during an active cascade. Thearmature_graphpackage was not touched.FeatureRuntime.teardownclears_cleanupOnErrorso prior-cycle closures don't pin the container's error handler / logger references acrossstart/stopcycles.PrintLogger.logwrapsJsonEncoder.convertin try/catch with atoString()fallback so a non-encodabledebugInfomap can no longer crash the framework.
Performance #
- State
_notifyListenersfast path — single-listener writes skip the try/catch + error list allocation entirely; the listener throws propagate naturally. - Atom
_propagateChangedfast path — single-observer atoms skip iterator allocation on everyreportChanged(). Reaction._clearAtomsreuses the atom set in place of allocating a fresh empty set on every clear; early-return when the set is already empty.Task._flushLatestPendingsingle-completer fast path — the common.latestcase (one pending caller) skips the defensiveList.from()snapshot.
Internal #
PortType/AnyPort/Port/PortSubscriptionmoved out ofpackage:armature/advanced.dartintopackage:armature/framework.dart.advanced.dartnow holds only application-facing escape-hatch types (handler typedefs, individualTaskStrategy*constructors, debug-overlay mirrors).
0.4.0 #
Breaking release: container lifecycle reshaped around
stop().dispose()is gone — see Migration below.
Removed #
AppContainer.dispose()andContainerStatus.disposed— the container is no longer a one-shot disposable.AppContainer.onDispose(...)and the internal_disposeCallbacksplumbing — per-container resources should now live inside a feature'ssetup+ cleanup bag, where they're recreated each cycle.- The internal
forRestartflag on_teardownFeatures— there is now a single teardown path.
Added #
AppContainer.stop()— returns the container to.idlewith the same coalesce / re-entrance guardsdispose()had. Afterstop(),start()can be called again to spin up a fresh cycle: new stores, fresh status stores, re-armed lifetime cleanup, re-registered port handlers.Events.clearListeners()— invoked from everystopand from rollback paths so event subscribers don't leak across cycles.- Restart-cycle correctness tests in
container_test.dartand a same-container restart RSS leak benchmark inleak_benchmark_test.dart.
Migration #
- Replace
container.dispose()withcontainer.stop(). - Move any
onDispose(...)work into a feature's cleanup bag (cleanup.add(...)) so it runs per cycle rather than once at the end of the container's life.
Documentation #
- Expanded doc-comments on enum members of
ContainerStatus,LogLevel, andThrottleEdge. - Per-strategy doc-comments on
TaskStrategyOnce/TaskStrategyQueue/TaskStrategyLatest/TaskStrategyDebounce/TaskStrategyThrottledescribing the behaviour each one selects. - Field-level doc-comments on
PortDebugInfoandFeatureDependency. - README aligned with the current API surface (keyed slots,
.queue/ascdefaults,package:armature/advanced.dartbarrel, explicitFeatureStatus/ToggleState).
Depends on #
armature_graph: ^1.0.0(was^0.1.0) — first stable release.armature_reactive: ^1.0.0(was^0.1.0) — first stable release.
Public API of both transitive packages is unchanged, so this is a
constraint-only bump for armature consumers.
0.3.1 #
Additive release: new public APIs on
TaskandCleanup. No breaking changes —^0.3.0consumers can adopt without code modifications.
Added #
Task.reset()— public method that returns a task toTaskIdlefrom any sticky state (TaskDone/TaskFailed). Supersedes any in-flight run via a generation token (state writes from older runs drop), cancels strategy-internal timers (debounce quiet timer, throttle cooldown / window), rejects coalesced callers from.latest/.debounce/.throttle(trailing)withTaskError, and clears the.oncecache so the next call re-executes the fn. Silent no-op afterdisposeand fromTaskIdle.autoReset: Duration?parameter onStore.createTask/Store.createVoidTask— schedules an automatic transition back toTaskIdleafter the given duration inTaskDone/TaskFailed. The internal timer cancels and re-arms on every state transition, so a freshcall()while the timer is waiting starts a new lifecycle without flickering throughTaskIdle. Cancelled indispose()and on manualreset().Cleanup.subscribe(store, listener, {fireImmediately})— sugar forcleanup.add(store.subscribe(listener, ...)).Cleanup.periodic(duration, callback)— wrapsTimer.periodicand auto-cancels the timer on deactivation.Cleanup.listen(stream, onData, {onError, onDone, cancelOnError})— wrapsStream.listen(...)and auto-cancels the subscription on deactivation.
Changed #
CleanupBagnowextends Cleanup(wasimplements Cleanup) so the new sugar helpers are inherited. Late-add semantics on the sealed bag are preserved — sugar methods route throughadd().TaskErrordoc clarifies that the same error is also surfaced through pending futures whenTask.reset()cancels coalesced callers (previously documented only fordispose).- README and
Store.createTaskdocstring gain a "Picking TError" guide (Exception/ domain class /Never/Object) explaining which thrown values land inTaskFailedvs propagate.
Internal #
_latestRunIdgeneralised into a cross-strategy_generationsupersession token. Every async path (_executeFn/_runLatest/_fireDebounced/_fireThrottleTrailing) captures the token at entry and skips state writes / completer settles when the captured value no longer matches.- Extracted
_supersedeInFlighthelper shared betweendisposeandresetfor strategy-state teardown (timer cancel, completer rejection, queue / debounce / throttle buffer clear).
0.3.0 #
Note: This release has breaking internal changes. User-facing API is unchanged; internal (
@internal) paths have moved.
Fixed #
- Per-container feature runtime — top-level
final feature = createFeature(...)instances no longer carry mutable runtime state. Scope API, status store, cleanup bags,ownActive, toggle callable, and the boundFeatureParentApinow live in a newFeatureRuntimeowned by each [AppContainer]. Two concurrent or sequentially-remounted containers backed by the same feature list hold independent runtime state — fixes a race where async dispose of one container corrupted another's stores, port handlers, and status subscriptions.
BREAKING (internal) #
Feature.internalgetter replaced byFeature.config— exposesFeatureConfig(immutable after cascade): name, deps, factories, activation setup, onStart callback, port bindings. Runtime state is reached viacontainer.runtimeOf(feature).Port._handlersremoved — port handler registration lives on the container ascontainer.handlersOf<THandler>(port)/container.addPortHandler(...)/container.removePortHandler(...). Ports themselves become stateless across containers (only_ownerremains, set-once and stable).Port.addHandler/removeHandler/hasHandlerFor/handlerCount/handlerFeatureNames/handlersgetter deleted.Port.checksignature now takescontainer: AppContaineralongsideapplyingFeature:— the lookup of pre-registered handlers reads from the container's per-container map.FeatureParentApiis instantiated per-container via the new internalfeatureParentApiForContainer(...)helper;.of/.statusOfresolve throughcontainer.runtimeOf(feature).FeatureHandlerContext/FeatureScopeApigain an@internal containerfield (set by the container during scope construction).feature.storeOf<T>()intest_utils.dartnow takes an explicitAppContainerargument:feature.storeOf<T>(container).
Behavioural changes #
useStoresthrowsFeatureResolutionErrorafter any container has constructed the feature (previously: after_scopeApiwas set on a single shared slot). Semantics are the same for the single-container case; multi-container now also guards.container.dispose()clears the per-container port handler map — no cross-container deregistration needed. Handlers are reinstalled on the nextcontainer.start()from each feature's recordedportBindings.
0.2.0 #
Note: This release has breaking changes.
- BREAKING FEAT(website): add interactive docs and examples site. (f30d28c1)
0.1.0 #
- Initial release — the core
armatureframework:Feature— a modular unit with typed stores, exports, activation, and ports. Built viacreateFeature(...)with a records-basedstores:/exports:/ports:surface.AppContainer— orchestrates feature lifecycle, dependency-graph resolution, and port application. Single error sink viaContainerErrorHandler({source, error, meta}).Store<T>/State<T>— reactive state primitives built onarmature_reactive, with subscribe / fireImmediately / update semantics.Task— strategy-backed async action runner (.once,.queue,.latest,.debounce,.throttle).- Ports:
Pipe,Behavior— owner/handler contract with eager or lazy owner binding; slot ports live inarmature_flutter. - Activation helpers:
manualActivation,whenStoreState,whenActive,whenInactive,whenAllActive. - Reactive feature-status observation via
parentApi.statusOf(feature)returning aStore<FeatureStatus>. CleanupBagwith LIFO disposal, late-add semantics, and async error routing.- Sealed
ArmatureErrorhierarchy (ContainerError,FeatureResolutionError,HandlerError,ListenerError,PortError,RenderError,StoreLookupError,TaskError, …).