Skip to content

fix(session): merge a save into the stored session instead of overwriting it - #108

Merged
tina4stack merged 3 commits into
tina4stack:v3from
MichaelC8E:fix/session-save-keeps-a-concurrent-logout
Oct 1, 2026
Merged

tina4stack merged 3 commits into
tina4stack:v3from
MichaelC8E:fix/session-save-keeps-a-concurrent-logout

Conversation

@MichaelC8E

@MichaelC8E MichaelC8E commented Oct 1, 2026 •

Copy link
Copy Markdown
Contributor

A session save wrote back the whole session as the request loaded it. When requests on one session overlap, the one that finishes later puts its stale copy back over the other's changes, and re-creates the session if the other request destroyed it. regenerate() moved the same stale copy to the new id.

Change. A save re-reads the stored session and applies only the keys this request set, changed or removed since it loaded; after clear() it replaces the session. If the stored session is gone, it stays gone: nothing is written and no cookie is sent. If the store cannot be read, nothing is written and the change is kept for a retry. regenerate() re-reads the same way before moving the session, and returns null instead of minting an id for a session that has ended.

SessionLike.regenerate() in sso.ts is typed string | null to match.

Test. test/sessionConcurrentRequests.test.ts: two sessions on one store stand in for overlapping requests (48 checks, including the documented login flow and an SSO callback). 22 fail on v3 without the change. Each part of the change, reverted on its own, turns a case red. The full suite shows no new failures against v3 on the same machine. Also run end to end over HTTP on the built-in server, with the file and the database (SQLite) session backends.

Parity. The same change in all four frameworks: tina4stack/tina4-python#189, tina4stack/tina4-php#260, tina4stack/tina4-ruby#94, #108.

…ting it

A save wrote back the whole session as this request loaded it. When
requests on one session overlapped, the one that finished later put its
stale copy back over the other's changes, and re-created the session if
the other request had destroyed it. regenerate() moved the same stale
copy to the new id.

A save now re-reads the stored session and applies only the keys this
request set, changed or removed since it loaded; after clear() it
replaces the session. If the stored session is gone, it stays gone:
nothing is written and no cookie is sent. If the store cannot be read,
nothing is written and the change is kept for a retry. regenerate()
re-reads the same way before moving the session, and mints no id for a
session that has ended.

regenerate() returns null when there is no session to carry, and the
SessionLike type in sso.ts now says so.

Signed-off-by: Michael <[email protected]>
@MichaelC8E

Copy link
Copy Markdown
Contributor Author

I have read the Tina4 Contributor Licence Agreement and I agree to it.

@MichaelC8E MichaelC8E closed this Oct 1, 2026
@MichaelC8E MichaelC8E reopened this Oct 1, 2026
MichaelC8E and others added 2 commits October 1, 2026 16:44
…ion merge

The merge on save adds complexity to this one file. Only its entry in the baseline changes.

Signed-off-by: Michael <[email protected]>
A request that started a session the store already holds now re-writes the
re-read, merged record on save() even when nothing changed, so the backend
deadline is re-stamped to now + TINA4_SESSION_TTL. A session therefore expires
after that span of INACTIVITY rather than that long after its last change,
matching tina4-php (the reference for ADR-0087) and closing the per-framework
session-lifetime divergence.

The slide preserves every concurrent-save guard and its precedence: no session
writes nothing; a record another request ended is not re-created (the session
ends for this request too); a failed backend read keeps the dirty flag for a
later retry; a cleared session replaces rather than merges; and
TINA4_SESSION_TTL=0 still re-stamps a never-expires deadline (ADR-0027). The
re-write is the merged current record, so sliding is concurrent-safe.

The read-only test now reads the stored deadline off disk before and after a
read-only save on a real FileSessionHandler and asserts it advanced while the
stored data is unchanged, mirroring php's
testAnUnchangedSessionIsStillWrittenSoExpiryCountsFromTheLastRequest. Proven by
mutation: it fails on the old write-nothing behaviour. The real dead-port Redis
retry scenario and every other case are unchanged.

Signed-off-by: Andre van Zuydam <[email protected]>
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Co-Authored-By: Tina4 <[email protected]>
@tina4stack
tina4stack merged commit edf2db5 into tina4stack:v3 Oct 1, 2026
10 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants