Skip to content

Commit d8382a1

Browse files
authored
Merge pull request #52 from dunky-dev/docs/navigation-spec-scenarios
docs(browser-navigation): add a Scenarios section to the SPEC
2 parents e02edb4 + 60c4b47 commit d8382a1

3 files changed

Lines changed: 104 additions & 2 deletions

File tree

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
---
2+
'@dunky.dev/browser-navigation': patch
3+
---
4+
5+
`SPEC.md` (shipped with the package) gains a Scenarios section: 26 compact
6+
traces of `interceptBackNavigation` and `watchSpentEntry` behavior, grouped
7+
by one layer, release and consumption, forward/claims/reload, stacked
8+
layers, and timing edges.
9+
10+
The behavior contract was already fully stated, but as one mechanism per
11+
prose bullet — nothing let a reader replay a concrete flow end to end.
12+
Each trace is a replayable episode in the module's own vocabulary (`arm`,
13+
`release`, `Back`, `Forward`), e.g.:
14+
15+
```
16+
arm A -> arm B -> arm C -> Back -> release B -> Forward -> Forward
17+
=> the Back closes C; the first Forward soaks into B's abandoned entry;
18+
the second asks C, which reopens only if it still can without B.
19+
```
20+
21+
Two outcomes the prose previously left implicit are now stated outright: a
22+
deliberately released entry absorbs one Forward press (it can't be deleted,
23+
only left to soak the traversal), and any re-plant — a new layer or a
24+
veto's re-arm — truncates every parked Forward watch above it.

packages/dom/utils/navigation/SPEC.md

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,84 @@ URL, so it isn't shareable. A layer that must survive reload in its own right
8888
(or be linkable) keeps its open-state in the URL and derives itself from it —
8989
Back then closes for free and needs no interceptor.
9090

91+
## Scenarios
92+
93+
Compact traces of the behavior above, in the module's own vocabulary.
94+
`arm` registers a guard; `release` is a close this module didn't cause —
95+
external, imperative, or similar — reaching it as the release call;
96+
`release(keepClaim)` is a teardown. `watch` registers a spent-entry
97+
watcher. `Back` and `Forward` are user traversals; `=>` is the observable
98+
outcome.
99+
100+
### One layer
101+
102+
- `arm -> Back` => the layer closes; the page stays.
103+
- `arm -> release` => the entry is consumed; the next Back leaves the page.
104+
- `arm -> Back -> Forward` => the layer reopens, re-armed on the entry in
105+
place.
106+
- `arm -> Back -> Forward (reopen declined)` => stays closed, stays parked;
107+
a later Forward offers again.
108+
- `arm -> Back (onBack declines)` => re-armed; the next Back reaches the
109+
same layer again.
110+
- `arm -> Back (onBack throws)` => counts as a decline: re-armed, and the
111+
error propagates.
112+
113+
### Release and consumption
114+
115+
- `arm -> release -> arm (same turn)` => the re-registration adopts the
116+
entry in place; zero traversals.
117+
- `arm (onForward) -> Back -> release` => the Forward watch ends; the spent
118+
entry stays in the forward stack, not the module's to spend.
119+
- `arm -> Back -> Forward (declined) -> release` => the declined reopen
120+
left the entry current, so it is consumed.
121+
- `arm A -> arm B -> release both (any order)` => both entries consumed,
122+
one traversal at a time; the next Back leaves the page.
123+
- `arm A -> arm B -> release A` => A's entry is buried under B: left
124+
alone. The Back that closes B surfaces it, and it is consumed alongside.
125+
- `arm -> Back -> release` => the Back already unwound the guard; the
126+
release is a no-op.
127+
128+
### Forward, claims, reload
129+
130+
- `arm (claim) -> Back -> release(keepClaim) -> watch -> Forward` => the
131+
sole watcher reopens; the new registration adopts the entry.
132+
- `arm (claim) -> Back -> release -> Forward` => nothing. The close was
133+
deliberate (abandoned), and the dead entry absorbs one Forward press —
134+
an entry can't be deleted, only left to soak the traversal.
135+
- Two watchers, one claim, `Forward` => neither answers: they can't be
136+
told apart, and reopening the wrong layer is worse than reopening none.
137+
- `arm (claim) -> Back -> reload -> watch -> Forward` => reopens. The
138+
abandoned memory did not survive the reload: surrendered and lost ground
139+
now look the same.
140+
- `arm -> reload -> Back` => spends on nothing — the entry outlived the
141+
open-state; a Forward back onto it offers the claim.
142+
- `Forward` onto marked ground with no owner and no watcher => nothing —
143+
residue never unwinds anything.
144+
- `arm A -> Back -> arm B` => B's plant truncates the forward stack: A's
145+
spent entry and its watch are gone. A veto's re-plant does the same — a
146+
decline costs every parked watch above.
147+
148+
### Stacked layers
149+
150+
- `arm A -> arm B -> Back -> Back` => closes B, then A — one per press.
151+
- `arm A -> arm B -> release B -> Back` => closes A; B's entry was
152+
consumed in between.
153+
- `arm A -> arm B -> arm C -> Back -> release B -> Forward -> Forward` =>
154+
the Back closes C; the first Forward soaks into B's abandoned entry; the
155+
second asks C, which reopens only if it still can without B.
156+
- `history.go(-2)` across A and B => unwinds topmost first; a decline
157+
midway re-arms and stops the unwind there.
158+
- `history.go(+2)` across two spent entries => offers each crossed layer
159+
its reopen, lowest first.
160+
161+
### Timing edges
162+
163+
- A registration adopts an entry while a self-caused pop is in flight =>
164+
the swallow accounting re-plants the live guard (self-heal).
165+
- Parent and child arm in one commit (child lifecycle runs first) => the
166+
child arms beneath the parent: the first Back closes the outer layer. A
167+
layer armed after its parent unwinds inside-out as expected.
168+
91169
## API
92170

93171
| Export | Description |

packages/dom/utils/navigation/src/intercept-back-navigation.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@ function currentClaim(): string | undefined {
7474
// Offers a spent entry to the layer that has taken the planter's place. Only a
7575
// sole candidate may answer: two layers claiming the same ground can't be told
7676
// apart, and reopening the wrong one is worse than reopening none.
77-
function offerToClaimant(): void {
77+
function resolveClaim(): void {
7878
const id = currentGuardId()
7979
if (id !== undefined && abandoned.has(id)) return
8080
const claim = currentClaim()
@@ -169,7 +169,7 @@ function onPopState(): void {
169169
}
170170
}
171171
} else {
172-
offerToClaimant()
172+
resolveClaim()
173173
}
174174
detachWhenIdle()
175175
return

0 commit comments

Comments
 (0)