Extract session state runtime

master
dev 5 months ago
parent 8a68e93e52
commit af681f8b25

@ -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.

@ -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<TUser, TCredential = undefined, TData = unde
const { schemas, storage, onRefresh, onRevoke, logger } = options;
const diagnostics = createSessionDiagnostics(logger);
type Snap = Session<TUser, TCredential, TData>;
let snapshot: Snap | null = null;
let generation = 0;
let refreshing: Promise<RefreshResult<TUser, TCredential, TData>> | null = null;
let disposed = false;
const listeners = new Set<SessionListener<TUser, TCredential, TData>>();
const detachers: Array<() => void> = [];
const initialSnapshot =
storage === undefined
? null
: hydrateSessionStorage<TUser, TCredential, TData>(storage, diagnostics);
let broadcastSync:
| {
post(event: SessionChange<TUser, TCredential, TData>['event'], generation: number): void;
detach(): void;
}
| undefined;
const state = createSessionStateRuntime<TUser, TCredential, TData>({
initial: initialSnapshot,
storage,
diagnostics,
broadcast: (event, nextGeneration) => {
broadcastSync?.post(event, nextGeneration);
}
});
// ── Storage sync ────────────────────────────────────────────────────────
if (storage !== undefined) {
const promoted = hydrateSessionStorage<TUser, TCredential, TData>(storage, diagnostics);
if (promoted !== null) snapshot = promoted;
const detachStorage = wireSessionStorageSync<TUser, TCredential, TData>({
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<TUser, TCredential = undefined, TData = unde
// ── Cross-tab broadcast (event + generation only, never tokens) ─────────
const channelName = options.broadcastChannel ?? DEFAULT_BROADCAST_CHANNEL;
const broadcastSync = createSessionBroadcastSync<
broadcastSync = createSessionBroadcastSync<
TUser,
TCredential,
TData,
@ -135,108 +140,11 @@ export function createEngineSession<TUser, TCredential = undefined, TData = unde
channelName,
storage,
applyExternalChange: (next) => {
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<TUser, TCredential, TData>['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<TUser, TCredential, TData>): 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<TUser, TCredential = undefined, TData = unde
const engine: EngineSession<TUser, TCredential, TData> = {
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<TUser, TCredential = undefined, TData = unde
}
const next = validated.session;
commitAdoption(next, EVENT_ADOPTED);
state.commitAdoption(next, EVENT_ADOPTED);
return { ok: true, session: next };
},
@ -301,7 +209,7 @@ export function createEngineSession<TUser, TCredential = undefined, TData = unde
issuedAt: input.issuedAt,
expiresAt: input.expiresAt
});
commitAdoption(next, EVENT_ADOPTED_SERVER);
state.commitAdoption(next, EVENT_ADOPTED_SERVER);
return next;
},
@ -309,21 +217,21 @@ export function createEngineSession<TUser, TCredential = undefined, TData = unde
ensureLive(ENGINE_METHOD_REFRESH);
if (refreshing !== null) return refreshing;
if (snapshot === null) {
const current = state.current;
if (current === null) {
return { status: REFRESH_STATUS_SKIPPED, reason: SKIP_REASON_NO_SESSION };
}
if (onRefresh === undefined) {
return { status: REFRESH_STATUS_SKIPPED, reason: SKIP_REASON_NO_REFRESH_FN };
}
const startedAt = generation;
const current = snapshot;
const startedAt = state.generation;
refreshing = (async (): Promise<RefreshResult<TUser, TCredential, TData>> => {
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<TUser, TCredential = undefined, TData = unde
}
if (next === null) {
commitRevocation(EVENT_EXPIRED);
state.commitRevocation(EVENT_EXPIRED);
return { status: REFRESH_STATUS_EXPIRED };
}
@ -363,17 +271,17 @@ export function createEngineSession<TUser, TCredential = undefined, TData = unde
};
}
if (startedAt !== generation) {
if (startedAt !== state.generation) {
return {
status: REFRESH_STATUS_SKIPPED,
reason: SKIP_REASON_STALE_GENERATION
};
}
commitAdoption(validated.session, EVENT_REFRESHED);
state.commitAdoption(validated.session, EVENT_REFRESHED);
return { status: REFRESH_STATUS_REFRESHED, session: validated.session };
} catch (err) {
if (startedAt !== generation) {
if (startedAt !== state.generation) {
return {
status: REFRESH_STATUS_SKIPPED,
reason: SKIP_REASON_STALE_GENERATION
@ -382,15 +290,13 @@ export function createEngineSession<TUser, TCredential = undefined, TData = unde
emitSessionDiagnostic(diagnostics, SESSION_DIAGNOSTIC_EVENTS.REFRESH_TRANSIENT_FAILURE, {
error: toError(err)
});
dispatch({
const identity = state.identity;
state.dispatch({
event: EVENT_REFRESH_FAILED,
current: snapshot,
previous: snapshot,
generation,
identity: {
from: computeSessionIdentity(snapshot),
to: computeSessionIdentity(snapshot)
},
current: state.current,
previous: state.current,
generation: state.generation,
identity: { from: identity, to: identity },
error: err
});
return { status: REFRESH_STATUS_FAILED, error: err, session: current };
@ -405,7 +311,8 @@ export function createEngineSession<TUser, TCredential = undefined, TData = unde
async revoke(opts: RevokeOptions = {}) {
ensureLive(ENGINE_METHOD_REVOKE);
if (snapshot === null) {
const current = state.current;
if (current === null) {
return {
localRevoked: true,
globalRevoked: false,
@ -434,7 +341,7 @@ export function createEngineSession<TUser, TCredential = undefined, TData = unde
});
} else {
try {
const ok = await onRevoke(snapshot, { logger });
const ok = await onRevoke(current, { logger });
if (ok) {
result = {
localRevoked: true,
@ -476,14 +383,14 @@ export function createEngineSession<TUser, TCredential = undefined, TData = unde
};
}
commitRevocation(EVENT_REVOKED, result);
state.commitRevocation(EVENT_REVOKED, result);
return result;
},
clearLocal() {
ensureLive(ENGINE_METHOD_CLEAR_LOCAL);
const result: RevokeResult =
snapshot === null
state.current === null
? {
localRevoked: true,
globalRevoked: false,
@ -495,29 +402,13 @@ export function createEngineSession<TUser, TCredential = undefined, TData = unde
globalRevoked: false,
scope: REVOKE_SCOPE_LOCAL
};
if (snapshot !== null) commitRevocation(EVENT_REVOKED, result);
if (state.current !== null) state.commitRevocation(EVENT_REVOKED, result);
return result;
},
onChange(listener) {
if (disposed) return () => {};
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<TUser, TCredential = undefined, TData = unde
// best-effort
}
}
listeners.clear();
state.clearListeners();
}
};

@ -0,0 +1,174 @@
import {
EVENT_ADOPTED,
EVENT_ADOPTED_SERVER,
EVENT_EXPIRED,
EVENT_EXTERNAL_CHANGED,
EVENT_INITIAL_SESSION,
EVENT_REFRESHED,
EVENT_REVOKED,
SESSION_DIAGNOSTIC_EVENTS
} from './consts.ts';
import { emitSessionDiagnostic, type SessionDiagnostics } from './diagnostics.ts';
import { sameSnapshot, toError } from './session-helpers.ts';
import { computeSessionIdentity } from './session-identity.ts';
import type {
EngineSessionOptions,
RevokeResult,
Session,
SessionChange,
SessionIdentityState,
SessionListener
} from './types.ts';
type AdoptionEvent = typeof EVENT_ADOPTED | typeof EVENT_ADOPTED_SERVER | typeof EVENT_REFRESHED;
type RevocationEvent = typeof EVENT_REVOKED | typeof EVENT_EXPIRED;
export interface SessionStateRuntime<TUser, TCredential = undefined, TData = undefined> {
readonly current: Session<TUser, TCredential, TData> | null;
readonly generation: number;
readonly identity: SessionIdentityState;
applyExternalChange(next: Session<TUser, TCredential, TData> | null): void;
commitAdoption(next: Session<TUser, TCredential, TData>, event: AdoptionEvent): void;
commitRevocation(event: RevocationEvent, revoke?: RevokeResult): void;
dispatch(change: SessionChange<TUser, TCredential, TData>): void;
onChange(listener: SessionListener<TUser, TCredential, TData>): () => void;
clearListeners(): void;
}
export interface SessionStateRuntimeOptions<TUser, TCredential = undefined, TData = undefined> {
readonly initial?: Session<TUser, TCredential, TData> | null;
readonly storage?: EngineSessionOptions<TUser, TCredential, TData>['storage'];
readonly diagnostics: SessionDiagnostics;
readonly broadcast?: (
event: SessionChange<TUser, TCredential, TData>['event'],
generation: number
) => void;
}
export function createSessionStateRuntime<TUser, TCredential = undefined, TData = undefined>(
options: SessionStateRuntimeOptions<TUser, TCredential, TData>
): SessionStateRuntime<TUser, TCredential, TData> {
type Snap = Session<TUser, TCredential, TData>;
let snapshot: Snap | null = options.initial ?? null;
let generation = 0;
const listeners = new Set<SessionListener<TUser, TCredential, TData>>();
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<TUser, TCredential, TData>): 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<TUser, TCredential, TData>): () => 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();
}
};
}
Loading…
Cancel
Save

Powered by TurnKey Linux.