parent
8a68e93e52
commit
af681f8b25
@ -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…
Reference in new issue