Composable Workspace #
Composable Workspace is a small TCA26 foundation for applications that authenticate a person, authorize editing, maintain a local reference copy, and publish edits through proposals.
Warning
This package is under active development. Its core workspace lifecycle is exercised by a complete test suite and demo application, but its public API may still change as it is adopted by more applications.
Installation #
Add Composable Workspace to a Swift package:
dependencies: [
.package(
url: "https://source.woody.fm/swift-composable-workspace",
branch: "main"
)
]
Then add the products that match the ownership boundaries your application needs:
.target(
name: "EventApplication",
dependencies: [
.product(
name: "ComposableAuthoringWorkspace",
package: "swift-composable-workspace"
)
]
)
| Product | Purpose |
|---|---|
ComposableAuthoring |
Authoring admission and validated capability state |
ComposableWorkspace |
Source preparation, exact baseline selection, and proposal publication |
ComposableAuthoringWorkspace |
Composition of authoring admission with workspace lifecycle |
WorkspaceDemoFeature |
In-memory integration and executable application stories |
Composable Workspace requires Swift 6.1 and supports iOS 17 and macOS 14.
The package deliberately separates four owners:
| Owner | Knows | Does not know |
|---|---|---|
| Application authentication | Session, token, sign-in, sign-out, network dependencies | Authoring policy or proposal state |
Authoring |
Admission policy and the currently validated authoring capability | Source copies, branches, or proposals |
Workspace |
The editable source, exact branch baseline, and proposal publication | Login UI or application-specific admission policy |
AuthoringWorkspace |
The two transitions between Authoring and Workspace | Any additional business state |
AuthoringWorkspace is intentionally one real TCA26 feature layer. TCA26 needs a concrete state and
action identity when the feature is scoped. The layer owns no duplicate state machine: a newly
validated grant requests workspace preparation, and an invalidated grant interrupts authorized
workspace work. Authoring posts those lifecycle events after it changes the validated grant;
AuthoringWorkspace handles the events explicitly. The composition does not infer authorization
transitions from a later feature remount.
Composition #
Features receive their clients from Dependencies at the application composition root. Call sites do
not pass infrastructure into Workspace or AuthoringWorkspace:
Scope(\.editing, action: \.editing) {
AuthoringWorkspace {
EventViewer()
}
}
The application installs its concrete clients once:
EventApplication()
.dependency(authoringClient)
.dependency(workspaceClient)
The application also owns the lifecycle connections that are specific to it:
- When the application starts, send
restoreAuthorizationso persisted authorization is revalidated. Incoming shared grant storage is cleared before wrapped content can observe it. - When the session identity changes, send
authorizationInvalidatedand await its returned task. If the new session is valid, then sendrefreshAdmissionand await that task too. This prevents a late result from the previous identity from restoring stale admission or capability state. - Implement network clients using the current session/token and the
AuthoringContextcapability exposed to wrapped content.
Auth therefore remains an application feature. An OME application can compose its own Auth,
AuthoringWorkspace, and EventViewer without this package knowing OME policy.
Workspace lifecycle #
The reference copy and writable content are different things. The writable content remains the editor's source of truth throughout publication.
The complete durable model is deliberately small:
struct WorkspaceState<Source: Sendable> {
let id: WorkspaceID
var availability: WorkspaceAvailability<Source>
}
enum WorkspaceAvailability<Source: Sendable> {
case inactive
case preparing
case preparationFailed(WorkspaceFailure)
case ready(WorkspaceSession<Source>)
}
struct WorkspaceSession<Source: Sendable> {
var source: Source
var baseline: WorkspaceBranchReference
}
ready always contains one coherent pair: the editable source and the exact branch revision against
which its working edits are compared. Proposals are remote review records, not workspace modes and
not mounted state.
prepareSource creates or opens that coherent session. It must not silently advance the installed
baseline. refreshBaseline may update only the revision of the current branch. selectBranch
explicitly installs another branch baseline. Refresh and selection are operations tracked by task
IDs; they are not additional durable workspace states. A failed operation leaves the existing ready
session intact.
Publication is one user operation:
captureChangescompares writable content with the installed baseline and returns one in-memoryChangeSet.publishProposalcreates one new remote proposal from that captured change set.- Remote record creation is the publication commit point. Before that point, an error belongs to the caller's proposal form and the workspace remains in its existing ready session.
- After that point, projection lag must not turn publication into a failed workspace state. The integration may durably record a delivery receipt, but the workspace baseline does not change.
The ChangeSet generic is the captured difference understood by an integrator. It can be a database
change manifest, a file patch, or a document snapshot. It is an ephemeral publication-attempt value,
not another database and not durable workspace state.
Workspace state is scoped to workspaceID, not to the lifetime of an authoring grant. Losing a grant
cancels active tasks but does not discard an already ready local session. Removing local workspace
data is a separate operation owned by the integrating application.
For database-backed integrations, Source is the coherent database resource used for the entire
editable workspace. It can contain the writable application database and a stable read-only
reference reader. Descendants use that exact resource; queries must never locate or attach a
reference database themselves, and applications must not reload a query to compensate for using a
different writer.
Integration contract #
Every WorkspaceClient implementation must preserve these rules:
prepareSourcereturns a complete, coherentWorkspaceSessionand never replaces writable content.Workspaceowns the returned source. Every later operation receives that exact value.captureChangescaptures against the session's installed baseline.publishProposalis retry-safe and returns the exact proposal record identity, content revision, and target revision.- Once the remote record exists,
publishProposalreports its identity without installing it into workspace state. refreshBaselinepreserves the selected branch identity.selectBranchreturns the exact requested branch only after it has been installed coherently.- Authorization is checked at every external operation. Throw
WorkspaceAuthorizationError.invalidatedwhen the current capability is no longer valid. - Throw
WorkspaceHeadMismatchbefore creating a proposal record when its target has moved.
These are semantic requirements rather than assumptions hidden inside a Git, database, or network implementation, so different integrators can implement them in their own storage systems.
Failure model #
Preparation failure is workspace availability state because no coherent session exists yet. Capture or pre-record publication failure is reported to the proposal form while the ready session remains usable. Context refresh or selection failure is reported to the requesting UI and likewise preserves the session.
After remote record creation, delivery receipts belong to the integration's persistence layer. They bridge server-projection lag without inventing a user-visible "half published" workspace state.
Invalid actions and invalid client output are programmer or protocol errors. They are reported with
withErrorReporting and do not become persistent UI state. Async operations mutate the store
directly after returning; the package has no Result response actions.
Standalone proof #
The package exposes WorkspaceDemoFeature as the reusable application feature and in-memory demo
system. The executable SwiftUI host lives in a separate Xcode application, so this package has no
application target or duplicate UI surface. Open the companion application's WorkspaceDemo.xcworkspace
and run the WorkspaceDemoApp scheme on an iPhone simulator.
The UI exercises sign-in, authoring entry, reference preparation, editing, proposal creation, creating independent proposals, refreshing Published after merge, and creating a fresh proposal. It intentionally does not inject synthetic backend failures.
Run the complete story and domain suite with:
swift test
The application stories mount makeWorkspaceDemoStore(), the same live feature graph and in-memory
system as the Xcode host. They cover entry, the full create-create-merge-create path, and grant loss
and re-entry without inventing proposal workspace state. Focused feature stories replace only
external client operations to cover preparation, no-change publication, record failure, baseline
selection, refresh, authorization loss, remote head movement, and invalid integrator output.