From af681f8b25b1cf8e3e96a9aa2b70285a94ea7234 Mon Sep 17 00:00:00 2001 From: dev Date: Wed, 29 Apr 2026 17:34:58 +0200 Subject: [PATCH] Extract session state runtime --- NEXT_STEPS.md | 1 + src/arts/sess/engine-session.ts | 209 ++++++++------------------------ src/arts/sess/session-state.ts | 174 ++++++++++++++++++++++++++ 3 files changed, 225 insertions(+), 159 deletions(-) create mode 100644 src/arts/sess/session-state.ts diff --git a/NEXT_STEPS.md b/NEXT_STEPS.md index e7294ef..ad6b725 100644 --- a/NEXT_STEPS.md +++ b/NEXT_STEPS.md @@ -17,6 +17,7 @@ Estado al cierre: - `arts/stor/engine-storage.ts` delega IDs de adapter, bus keys y suscripciones cross-tab en `adapter-registry.ts`. - `arts/stor/engine-storage.ts` delega el registro de defaults conflictivos en `defaults-registry.ts`. - `arts/sess/engine-session.ts` delega la clasificacion `none/anonymous/identified` en `session-identity.ts`. + - `arts/sess/engine-session.ts` delega snapshot, generation, dispatch, persistencia y commits en `session-state.ts`. - Integracion total ampliada: `Auth.signOut()` valida anonimizacion, invalidacion de `Permissions` y evento `Cache.invalidate`. - Tanda focalizada verde: `npx vitest run src/arts/conn src/libs/cach src/arts/cach src/svrs/cach src/arts/auth src/svrs/auth src/libs/auth src/arts/aapp/test/ecosystem.integration.test.ts` -> 19 archivos, 82 tests. - `/test/ecosystem` revisado en navegador: carga sin errores de consola, `ar` cambia a `rtl`, Formats se actualiza por locale, Perm cambia con rol `viewer`, Cach re-scopea por locale y Conn loopback publica/recibe. diff --git a/src/arts/sess/engine-session.ts b/src/arts/sess/engine-session.ts index a48e16c..d2006fc 100644 --- a/src/arts/sess/engine-session.ts +++ b/src/arts/sess/engine-session.ts @@ -10,8 +10,6 @@ import { EVENT_ADOPTED, EVENT_ADOPTED_SERVER, EVENT_EXPIRED, - EVENT_EXTERNAL_CHANGED, - EVENT_INITIAL_SESSION, EVENT_REFRESHED, EVENT_REFRESH_FAILED, EVENT_REVOKED, @@ -41,7 +39,6 @@ import { SessDisposedError, SessInvalidSessionError } from './errors.ts'; import { freezeSession, readField, - sameSnapshot, toError } from './session-helpers.ts'; import { @@ -49,7 +46,7 @@ import { hydrateSessionStorage, wireSessionStorageSync } from './session-sync.ts'; -import { computeSessionIdentity } from './session-identity.ts'; +import { createSessionStateRuntime } from './session-state.ts'; import { checkSessionInvariants, validateSessionCandidate } from './session-validation.ts'; import type { AdoptResultField, @@ -58,10 +55,8 @@ import type { RefreshResult, RevokeOptions, RevokeResult, - Session, SessionActor, - SessionChange, - SessionListener + SessionChange } from './types.ts'; function refreshValidationDiagnosticEvent(field: AdoptResultField) { @@ -96,28 +91,38 @@ export function createEngineSession; - - let snapshot: Snap | null = null; - let generation = 0; let refreshing: Promise> | null = null; let disposed = false; - const listeners = new Set>(); const detachers: Array<() => void> = []; + const initialSnapshot = + storage === undefined + ? null + : hydrateSessionStorage(storage, diagnostics); + let broadcastSync: + | { + post(event: SessionChange['event'], generation: number): void; + detach(): void; + } + | undefined; + const state = createSessionStateRuntime({ + initial: initialSnapshot, + storage, + diagnostics, + broadcast: (event, nextGeneration) => { + broadcastSync?.post(event, nextGeneration); + } + }); // ── Storage sync ──────────────────────────────────────────────────────── if (storage !== undefined) { - const promoted = hydrateSessionStorage(storage, diagnostics); - if (promoted !== null) snapshot = promoted; - const detachStorage = wireSessionStorageSync({ storage, diagnostics, warnNoOnChange: logger !== undefined, applyExternalChange: (next) => { - if (next !== null || snapshot !== null) applyExternalChange(next); + if (next !== null || state.current !== null) state.applyExternalChange(next); } }); if (detachStorage !== undefined) detachers.push(detachStorage); @@ -126,7 +131,7 @@ export function createEngineSession { - if (next !== null || snapshot !== null) applyExternalChange(next); + if (next !== null || state.current !== null) state.applyExternalChange(next); } }); if (broadcastSync !== undefined) detachers.push(() => broadcastSync.detach()); - function broadcast(event: SessionChange['event']): void { - broadcastSync?.post(event, generation); - } - - // ── State helpers ─────────────────────────────────────────────────────── - - /** - * Apply a snapshot that came from outside this engine (storage event, - * BroadcastChannel). Dedups against the current snapshot so a storage - * event + a broadcast event for the same change only fire one - * `EXTERNAL_CHANGED`. Increments `generation` so refresh promises - * started before the external change cannot resurrect an old snapshot. - */ - function applyExternalChange(next: Snap | null): void { - const prev = snapshot; - if (sameSnapshot(prev, next)) return; - const fromIdentity = computeSessionIdentity(prev); - generation += 1; - snapshot = next; - const toIdentity = computeSessionIdentity(next); - dispatch({ - event: EVENT_EXTERNAL_CHANGED, - current: next, - previous: prev, - generation, - identity: { from: fromIdentity, to: toIdentity } - }); - } - - function persist(value: Snap | null): void { - if (storage === undefined) return; - try { - if (value === null) { - storage.adapter.removeItem(storage.key); - } else { - storage.adapter.setItem(storage.key, JSON.stringify(value)); - } - } catch (err) { - emitSessionDiagnostic(diagnostics, SESSION_DIAGNOSTIC_EVENTS.STORAGE_PERSIST_FAILED, { - error: toError(err) - }); - } - } - - function dispatch(change: SessionChange): void { - for (const listener of listeners) { - try { - listener(change); - } catch (err) { - emitSessionDiagnostic(diagnostics, SESSION_DIAGNOSTIC_EVENTS.LISTENER_THREW, { - error: toError(err), - event: change.event - }); - } - } - } - - function commitAdoption( - next: Snap, - event: typeof EVENT_ADOPTED | typeof EVENT_ADOPTED_SERVER | typeof EVENT_REFRESHED - ): void { - const prev = snapshot; - const fromIdentity = computeSessionIdentity(prev); - generation += 1; - snapshot = next; - persist(next); - const toIdentity = computeSessionIdentity(next); - dispatch({ - event, - current: next, - previous: prev, - generation, - identity: { from: fromIdentity, to: toIdentity } - }); - broadcast(event); - } - - function commitRevocation( - event: typeof EVENT_REVOKED | typeof EVENT_EXPIRED, - revoke?: RevokeResult - ): void { - const prev = snapshot; - const fromIdentity = computeSessionIdentity(prev); - generation += 1; - snapshot = null; - persist(null); - dispatch({ - event, - current: null, - previous: prev, - generation, - identity: { from: fromIdentity, to: computeSessionIdentity(null) }, - revoke - }); - broadcast(event); - } - // ── Public API ────────────────────────────────────────────────────────── function ensureLive(method: string): void { @@ -251,13 +159,13 @@ export function createEngineSession = { get current() { - return snapshot; + return state.current; }, get generation() { - return generation; + return state.generation; }, get identity() { - return computeSessionIdentity(snapshot); + return state.identity; }, async adopt(input) { @@ -279,7 +187,7 @@ export function createEngineSession> => { try { const next = await onRefresh(current, { logger }); - if (startedAt !== generation) { + if (startedAt !== state.generation) { return { status: REFRESH_STATUS_SKIPPED, reason: SKIP_REASON_STALE_GENERATION @@ -331,7 +239,7 @@ export function createEngineSession {}; - listeners.add(listener); - try { - const identity = computeSessionIdentity(snapshot); - listener({ - event: EVENT_INITIAL_SESSION, - current: snapshot, - previous: snapshot, - generation, - identity: { from: identity, to: identity } - }); - } catch (err) { - emitSessionDiagnostic(diagnostics, SESSION_DIAGNOSTIC_EVENTS.LISTENER_THREW, { - error: toError(err), - event: EVENT_INITIAL_SESSION - }); - } - return () => listeners.delete(listener); + return state.onChange(listener); }, dispose() { @@ -530,7 +421,7 @@ export function createEngineSession { + readonly current: Session | null; + readonly generation: number; + readonly identity: SessionIdentityState; + applyExternalChange(next: Session | null): void; + commitAdoption(next: Session, event: AdoptionEvent): void; + commitRevocation(event: RevocationEvent, revoke?: RevokeResult): void; + dispatch(change: SessionChange): void; + onChange(listener: SessionListener): () => void; + clearListeners(): void; +} + +export interface SessionStateRuntimeOptions { + readonly initial?: Session | null; + readonly storage?: EngineSessionOptions['storage']; + readonly diagnostics: SessionDiagnostics; + readonly broadcast?: ( + event: SessionChange['event'], + generation: number + ) => void; +} + +export function createSessionStateRuntime( + options: SessionStateRuntimeOptions +): SessionStateRuntime { + type Snap = Session; + + let snapshot: Snap | null = options.initial ?? null; + let generation = 0; + const listeners = new Set>(); + + function persist(value: Snap | null): void { + if (options.storage === undefined) return; + try { + if (value === null) { + options.storage.adapter.removeItem(options.storage.key); + } else { + options.storage.adapter.setItem(options.storage.key, JSON.stringify(value)); + } + } catch (err) { + emitSessionDiagnostic(options.diagnostics, SESSION_DIAGNOSTIC_EVENTS.STORAGE_PERSIST_FAILED, { + error: toError(err) + }); + } + } + + function dispatch(change: SessionChange): void { + for (const listener of listeners) { + try { + listener(change); + } catch (err) { + emitSessionDiagnostic(options.diagnostics, SESSION_DIAGNOSTIC_EVENTS.LISTENER_THREW, { + error: toError(err), + event: change.event + }); + } + } + } + + function applyExternalChange(next: Snap | null): void { + const prev = snapshot; + if (sameSnapshot(prev, next)) return; + const fromIdentity = computeSessionIdentity(prev); + generation += 1; + snapshot = next; + const toIdentity = computeSessionIdentity(next); + dispatch({ + event: EVENT_EXTERNAL_CHANGED, + current: next, + previous: prev, + generation, + identity: { from: fromIdentity, to: toIdentity } + }); + } + + function commitAdoption(next: Snap, event: AdoptionEvent): void { + const prev = snapshot; + const fromIdentity = computeSessionIdentity(prev); + generation += 1; + snapshot = next; + persist(next); + const toIdentity = computeSessionIdentity(next); + dispatch({ + event, + current: next, + previous: prev, + generation, + identity: { from: fromIdentity, to: toIdentity } + }); + options.broadcast?.(event, generation); + } + + function commitRevocation(event: RevocationEvent, revoke?: RevokeResult): void { + const prev = snapshot; + const fromIdentity = computeSessionIdentity(prev); + generation += 1; + snapshot = null; + persist(null); + dispatch({ + event, + current: null, + previous: prev, + generation, + identity: { from: fromIdentity, to: computeSessionIdentity(null) }, + revoke + }); + options.broadcast?.(event, generation); + } + + function onChange(listener: SessionListener): () => void { + listeners.add(listener); + try { + const identity = computeSessionIdentity(snapshot); + listener({ + event: EVENT_INITIAL_SESSION, + current: snapshot, + previous: snapshot, + generation, + identity: { from: identity, to: identity } + }); + } catch (err) { + emitSessionDiagnostic(options.diagnostics, SESSION_DIAGNOSTIC_EVENTS.LISTENER_THREW, { + error: toError(err), + event: EVENT_INITIAL_SESSION + }); + } + return () => listeners.delete(listener); + } + + return { + get current() { + return snapshot; + }, + get generation() { + return generation; + }, + get identity() { + return computeSessionIdentity(snapshot); + }, + applyExternalChange, + commitAdoption, + commitRevocation, + dispatch, + onChange, + clearListeners() { + listeners.clear(); + } + }; +}