An open family is itself queryable state. Its standing view gives one atomic answer about current membership, exact identities, and the outcomes already retained for those members.
Read an open family as one standing value.
Lifecycle channels report what changed. A family view reports the complete current membership, exact refs, and retained Exits as one atomic query result.
1 · Read the family as one atomic value
A standing view answers who is here now.
The Watching perspective reads Watchers directly. Its record contains guide and spec together, and each value pairs the member's exact ref with an optional terminal Exit.
import { Activity, Query, State, Statechart, States, Transitions } from "@motive/motive";
import * as Effect from "effect/Effect";
import { Schema } from "effect";
export const WatchDocument = Activity.make("WatchDocument", {
input: Schema.Struct({ path: Schema.String }),
success: Schema.String,
});
const Watchers = WatchDocument.each("Watchers");
export class Workspace extends State.Compound<Workspace>()("Workspace", {
documents: Schema.Record(Schema.String, Schema.String).pipe(
Schema.withConstructorDefault(Effect.succeed({ guide: "docs/guide.md", spec: "docs/spec.md" })),
),
}) {
static states = States.make(() => [Watching]);
}
export class Watching extends State.Atomic<Watching>()("Watching") {
static transitions = Transitions.make(this, ({ invoke, on }) => [
invoke(
Watchers,
Query.gen(function* () {
return Object.fromEntries(
Object.entries((yield* Workspace).documents).map(([key, path]) => [key, { path }]),
);
}),
),
on(Watchers.MemberDone),
on(Watchers.Settled),
]);
}
export const WorkspaceChart = Statechart.make(Workspace);
export const WatcherView = WorkspaceChart.at(Watching).select(
Query.gen(function* () {
return yield* Watchers;
}),
);
2 · Observe across the owner's lifetime
An optional view can honestly become absent.
Workspace outlives the Watching placement, so Query.option reads the family as Some while it is mounted and None after PauseWatching retires it. ResumeWatching creates a fresh view of fresh members.
import { Activity, Query, State, Statechart, States, Transitions } from "@motive/motive";
import * as Effect from "effect/Effect";
import { Schema } from "effect";
export class PauseWatching extends Schema.TaggedClass<PauseWatching>()("PauseWatching", {}) {}
export class ResumeWatching extends Schema.TaggedClass<ResumeWatching>()("ResumeWatching", {}) {}
export const WatchDocument = Activity.make("WatchDocument", {
input: Schema.Struct({ path: Schema.String }),
success: Schema.String,
});
const Watchers = WatchDocument.each("Watchers");
export class Workspace extends State.Compound<Workspace>()("Workspace", {
documents: Schema.Record(Schema.String, Schema.String).pipe(
Schema.withConstructorDefault(Effect.succeed({ guide: "docs/guide.md", spec: "docs/spec.md" })),
),
}) {
static states = States.make(() => [Watching, Paused]);
}
class Watching extends State.Atomic<Watching>()("Watching") {
static transitions = Transitions.make(this, ({ invoke, on }) => [
invoke(
Watchers,
Query.gen(function* () {
return Object.fromEntries(
Object.entries((yield* Workspace).documents).map(([key, path]) => [key, { path }]),
);
}),
),
on(Watchers.MemberDone),
on(Watchers.Settled),
on(PauseWatching, Paused),
]);
}
class Paused extends State.Atomic<Paused>()("Paused") {
static transitions = Transitions.make(this, ({ on }) => [
on(ResumeWatching, Watching),
]);
}
export const WorkspaceChart = Statechart.make(Workspace);
export const WatcherView = WorkspaceChart.at(Workspace).select(
Query.gen(function* () {
return yield* Query.option(Watchers);
}),
);
A family view is a record, not an event log
Watchers.MemberAdded, MemberRemoved, and the member-outcome channels describe edges. A query of
Watchers answers a different question: what is the complete standing family now?
const view = yield * Watchers;
The site is directly iterable inside Query.gen; yield* Query.each(Watchers) is the explicit
equivalent. Both return a read-only record keyed by current member key.
Because the record comes from one published snapshot, callers do not have to subscribe to every edge and reconstruct membership in a second mutable store. Additions, removals, and settlements have already been reconciled before the view is observed.
Every member exposes identity and settlement
Each record value contains two facts:
{
ref: Ref.Ref,
exit: Option.Option<Exit.Exit<Success, Error>>,
}
ref addresses the exact current incarnation. exit is None while that member is running and
Some(Exit) after it settles. The Exit preserves success, modeled failure, and a locally handled
defect without flattening them into one status string.
The first frame lets guide finish while spec keeps running. guide remains in the view with a
terminal Exit because it remains desired membership. Settlement does not remove an open-family
member; removal from the authored mapping does.
Strict reads require a proven owner
The first selection is placed at Watching, the state that mounts Watchers:
WorkspaceChart.at(Watching).select(
Query.gen(function* () {
return yield* Watchers;
}),
);
That perspective proves the occurrence exists, so the read is strict and returns the record directly. The same rule applies inside guards, updates, inputs, and other query consumers: strict family reads belong where the mount owner is provably active.
This is useful structure, not ceremony. A view of an unmounted occurrence is not an empty family. Empty means the family exists with zero members; absent means no current occurrence owns a view.
Optional reads preserve absence
The second frame observes from Workspace, which remains active across both Watching and
Paused. It cannot prove the nested family is mounted, so it asks explicitly:
WorkspaceChart.at(Workspace).select(
Query.gen(function* () {
return yield* Query.option(Watchers);
}),
);
The result is Some(view) while Watching owns the occurrence and None after PauseWatching
exits it. That None is not a failed lookup and should not be collapsed to {}: it preserves the
lifecycle distinction between absent occurrence and present, empty family.
Resuming enters Watching again. The mapping still contains guide and spec, but both receive
fresh refs because this is a new family occurrence with new member incarnations.
Views and channels serve different decisions
Use a standing view when behavior depends on the current whole: render a status table, count
running members, inspect retained outcomes, address an exact ref, or derive another family's
membership. Use lifecycle channels when behavior belongs to a particular edge: announce one
addition, persist one completion, or react exactly when the current membership becomes settled.
Do not rebuild the view from channels merely to learn current state, and do not repeatedly diff a view when the model cares about the edge itself. Motive supplies both surfaces because current truth and change are different information.
The same shape spans open family kinds
Activity, child-chart, and Timer .each sites all expose the same structural idea: a keyed record
whose values contain an exact ref and optional exit. Their Exit types retain the citizen's own
success and failure contract—Activity output, child output and Cause, or Timer completion.
That common shape lets one query compose views across different mounted citizens without erasing their typed outcomes. The site handle still owns which family is being read and the chart still checks that the chosen perspective may read it.
Test standing views as atomic observations
- Settle one member and verify its
exitbecomesSomewhile its key andrefremain present. - Remove a settled key and verify the entire record entry disappears.
- Compare the record after different edge orders and verify equivalent published state yields the same view.
- Exit the owner and distinguish optional
Nonefrom a mounted empty{}view. - Reenter with the same keys and verify their refs are fresh.
- Observe a multi-family selection and verify every constituent view came from the same committed snapshot.
Next, Resource Boundaries asks which values belong in durable chart state and which should remain behind scoped, typed access to the outside world.