Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .codegraph/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# CodeGraph data files — local to each machine, not for committing.
# Ignore everything in .codegraph/ except this file itself, so transient
# files (the database, daemon.pid, sockets, logs) never show up in git.
*
!.gitignore
47 changes: 27 additions & 20 deletions .github/workflows/release-plugin.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ name: Publish Plugin to Portal
on:
push:
tags:
- '*'
- 'v*'

permissions:
contents: read
Expand All @@ -14,30 +14,37 @@ jobs:
runs-on: ubuntu-latest
environment: release

env:
GRADLE_PUBLISH_KEY: ${{ secrets.GRADLE_PUBLISH_KEY }}
GRADLE_PUBLISH_SECRET: ${{ secrets.GRADLE_PUBLISH_SECRET }}
if: ${{ !contains(github.event.head_commit.message, 'ci skip') }}
steps:
- name: Checkout Repo
uses: actions/checkout@v2
- name: Cache Gradle Caches
uses: actions/cache@v3
with:
path: ~/.gradle/caches/
key: cache-gradle-cache
- name: Cache Gradle Wrapper
uses: actions/cache@v3
with:
path: ~/.gradle/wrapper/
key: cache-gradle-wrapper
- name: Setup java
uses: actions/setup-java@v3
uses: actions/checkout@v4

- name: Setup Java
uses: actions/setup-java@v4
with:
distribution: 'corretto'
distribution: temurin
java-version: '17'

- name: Setup Gradle
uses: gradle/actions/setup-gradle@v4

- name: Run tests
run: ./gradlew --no-daemon --stacktrace :way-gradle-plugin:test

- name: Validate tag matches version
run: |
TAG="${GITHUB_REF#refs/tags/}"
VERSION=$(grep '^versionName=' gradle.properties | cut -d'=' -f2)
if [ "v${VERSION}" != "${TAG}" ] && [ "${VERSION}" != "${TAG}" ]; then
echo "Tag '${TAG}' does not match versionName '${VERSION}' in gradle.properties"
exit 1
fi

- name: Publish on Plugin Portal
run: ./gradlew --project-dir plugin-build setupPluginUploadFromEnvironment publishPlugins
env:
GRADLE_PUBLISH_KEY: ${{ secrets.GRADLE_PUBLISH_KEY }}
GRADLE_PUBLISH_SECRET: ${{ secrets.GRADLE_PUBLISH_SECRET }}
run: ./gradlew setupPluginUploadFromEnvironment :way-gradle-plugin:publishPlugins
if: success()

- name: Stop Gradle
run: ./gradlew --stop
17 changes: 16 additions & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ name: Release Libraries
on:
push:
tags:
- '*'
- 'v*'

permissions:
contents: read
Expand Down Expand Up @@ -37,6 +37,21 @@ jobs:
sdkmanager --install \
"platforms;android-36" \
"build-tools;36.0.0"
- name: Run tests
run: |
./gradlew --no-daemon --stacktrace \
:way-gradle-plugin:test \
:way:jvmTest

- name: Validate tag matches version
run: |
TAG="${GITHUB_REF#refs/tags/}"
VERSION=$(grep '^versionName=' gradle.properties | cut -d'=' -f2)
if [ "v${VERSION}" != "${TAG}" ] && [ "${VERSION}" != "${TAG}" ]; then
echo "Tag '${TAG}' does not match versionName '${VERSION}' in gradle.properties"
exit 1
fi

- name: Prepare publishing credentials
env:
NEXUS_USERTOKEN_NAME: ${{ secrets.NEXUS_USERTOKEN_NAME }}
Expand Down
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,8 @@ tmp/
build
.gradle
local.properties

/courses/
/docs/
AGENTS.md
CLAUDE.md
30 changes: 30 additions & 0 deletions .idea/inspectionProfiles/Project_Default.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

112 changes: 112 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,117 @@
# Changelog

## 0.9.8 - 2026-07-07

The headline of this release is **parallel navigation**, built on the W3C SCXML statechart
model, together with node lifecycle management, transaction-style transitions, and an expanded
Compose integration. Parallel support is entirely new — nothing about it changes existing
single-flow code. A few non-parallel APIs do change; see **Breaking** at the end.

### Parallel navigation (new)

- `ParallelFlowNode<R : Any>` — a new node type (an `abstract class`) whose sub-regions are all
**active concurrently** (SCXML AND-states). It can be the schema root or nested inside a
region, and returns `FlowTransition<R>` — the same transition set as a regular flow.
- DOT: declare one with `type = "parallelFlow"` (omitted `resultType` defaults to `kotlin.Unit`).
- **App-owned Back / focus.** The library stores no "current"/"focused" region — a
`ParallelFlowNode` subclass holds its own field and uses it for both rendering and Back. Return
`DispatchBackTo(regionId): FlowTransition<Nothing>` from `transition(Event.Back)` to route the
structural back-pop into a region; return `Ignore` for the deepest-region default. A
stale/unknown region id soft-falls-back to the deepest region — Back never throws.
- Relative `FlowTarget` / `ScreenTarget` returned from a parallel node resolve against the
**first** declared sub-region; target a specific region with `AbsoluteTarget`.
- `NavigationState` gains `rootNode` / `rootNodePath` so consumers (notably `way-compose`) can
find the active root — including a parallel root — without re-walking the schema.
- New `@CrossRegionEvent` annotation marks events every region must react to (auth expired, deep
link, config change); child flows must `Ignore` such events so they bubble to the parallel.

```kotlin
class MainTabsNode : ParallelFlowNode<Unit>(), ComposableNode {
override val dismissResult = Unit
var currentTab: RegionId by mutableStateOf(homeRegionId); private set // app owns "current tab"
override fun transition(event: Event) = when (event) {
is TabSelected -> { currentTab = event.regionId; Stay }
is Event.Back -> DispatchBackTo(currentTab) // Back follows the visible tab
else -> Ignore
}
}
```

### Statechart / SCXML engine + history (new)

- A canonical SCXML entry/exit/LCCA transition engine underpins the runtime, with SCXML
conformance tests. Flows are compound (OR) states, parallels are AND-states, screens are atomic.
- **History** — new `HistoryTarget(path, deep = false)` target plus `type = "history"` (shallow)
and `type = "deepHistory"` DOT pseudostates. Shallow restores which child was active (reset to
its default leaf); deep restores the exact leaf. Works across parallel regions and cold
re-entry, with typed history accessors emitted by codegen.

### Node lifecycle (new)

- `Node.onDispose()` — optional teardown hook (default no-op) on every node type.
- `NavigationService.cleanDispose()` walks alive nodes leaf-to-root calling `onDispose()` before
shutting down; `dispose()` hard-cuts listeners/scheduler and marks the service disposed.
Post-dispose `sendEvent` is a safe no-op.

### Transaction-style transitions (new)

- Every transition snapshots `NavigationState` and rolls back atomically if any step (node build,
payload computation, schema validation) throws — no half-applied state is ever visible to
listeners.
- New `validateSchema: Boolean = true` flag validates the resulting state against the schema
after each transition; set `false` only in tests that intentionally build off-schema states.
- Payloads persist in navigation state and are pruned in lockstep with alive paths, so a
parameterized child flow rebuilt without a fresh `NavigateTo` no longer crashes with
`no payload for …`.

### Transitions

- `EnqueueEvent` gains `NavigateAndEnqueue` and the `NavigateTo(...) thenEnqueue event` infix DSL
— navigate and queue a follow-up event to dispatch after the navigation completes.

### Compose (`way-compose`)

- A parallel node renders by implementing `ComposableNode` — its `Content()` lays out the parallel
(tab bar, pager, …) and calls `NodeHost(regionId)` per sub-region, each in its own
`rememberSaveableStateHolder`, so per-node UI state survives sibling switches. `NodeHost` renders
parallel roots directly.
- `NodeHost(regionId)` overload renders a single region of a parallel node.
- `LocalNavigationService` (a `compositionLocalOf`) and `LocalNodePath` let descendants read the
current service and enclosing node path without prop-drilling.
- `NodeHost`'s `AnimatedContent` is keyed by node **`Path`**, avoiding a same-path re-entry crash
and a leak that would otherwise retain every exited node (its DI scope, view models, children)
in the SlotTable.
- `NodeHost(nodeBuilder, onFinishRequest)` now disposes the service it owns (previously leaked it)
and reads `onFinishRequest` through `rememberUpdatedState`; `SaveableStateProvider` keys use
full, cross-schema-unique segment ids.

### Codegen / DOT plugin

- Segment ids are unified as `nodeId@graphId:file` (via a `SchemaRegistry` that resolves imported
nodes to the child schema's identity), so parent and child emit the same id at a module
boundary and path comparisons work across Gradle modules.
- Generated `*Schema` / `*NodeBuilder` expose typed `val <name>RegionId: RegionId` accessors, and
parallel-rooted schemas emit a typed region `enum class` (`RegionEnumCodegen`).
- Build-time schema validation rejects unknown targets, mis-nested regions, duplicate segment ids,
disconnected cycles, and codegen output-filename collisions with a precise location.
- Nullable payload/result types (`parameterType = "kotlin.String?"`) now generate `String?`.

### Breaking (vs 0.9.7)

- `NavigateTo.targets` is now `List<Target>` (was `Set<Target>`) — replace `setOf(...)` with
`listOf(...)`; later targets in the same region win.
- `NavigationService.start()` throws on a second call; `sendEvent()` throws if called before
`start()` (previously an opaque NPE / silent no-op).
- `NodeBuilder.invalidateCache(path: Path)` → `invalidateCache(alivePaths: Set<Path>)` — retains
any cache entry that is a prefix of an alive path across **all** regions (fixes sibling-region
eviction). Affects custom NodeBuilder / test-double implementations only.
- `NavigationState` gains required `rootNode` / `rootNodePath` (affects code constructing it
directly — usually only tests).
- `way-compose`: the `NodeHost` `transitionSpec` receiver is now
`AnimatedContentTransitionScope<Path?>` (was over `NodeWithPath?`).
- Re-run the `way-gradle-plugin` codegen: the segment-id format and new region enums change
generated output.

## 0.9.7 - 2026-03-31

* Fix Windows-incompatible Android source discovery in `way-gradle-plugin` by avoiding `SourceDirectorySet.directories` snapshot/provider placeholder paths (e.g. `provider(?)`)
Expand Down
Loading
Loading