You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
svelte-kit-vice/src/arts/prefs/engine-prefs.ts

279 lines
8.9 KiB

import {
resolvePrefs,
sanitizeIntent,
type PrefsChangeCause,
type PrefsChangeEvent,
type PrefsChangeHandler,
type PrefsEffectiveOf,
type PrefsEnvironment,
type PrefsIntentOf,
type PrefsSchema,
type PrefsSnapshot,
type PrefsUnsubscribe
} from '$libs/prefs';
import {
PREFS_ENGINE_METHOD_CLEAR_INTENT,
PREFS_ENGINE_METHOD_PATCH_ENVIRONMENT,
PREFS_ENGINE_METHOD_REFRESH_ENVIRONMENT,
PREFS_ENGINE_METHOD_RESET_INTENT,
PREFS_ENGINE_METHOD_SET_INTENT,
PREFS_KIND
} from './consts.ts';
import {
PrefsDisposedError,
PrefsIntentInvalidError,
PrefsUnknownDimensionError,
disposedErrorMessage,
intentInvalidErrorMessage,
unknownDimensionErrorMessage
} from './errors.ts';
import type { EnginePrefs, EnginePrefsOptions } from './types.ts';
/**
* Build a fresh `EnginePrefs<S>` for the given schema. Runes-free —
* safe to import from server-only modules. The Active wrapper layers
* reactive state and the per-dimension surface on top.
*
* Internally maintains:
* - The frozen `schema` reference (immutable for the engine's lifetime).
* - Two layer cells (`environment`, `intent`) replaced by reference on
* every commit.
* - A frozen `PrefsSnapshot<S>` cell with the resolved `effective`
* view; rebuilt only on commits so reads return structurally-shared
* references.
* - A monotonic `version` counter bumped per commit. Stable across
* no-op writes (see `commit()` short-circuit).
* - A listener set notified with `{previous, next, effectiveDiff,
* cause}` only when at least one layer changed by reference.
*/
export function createEnginePrefs<S extends PrefsSchema>(
options: EnginePrefsOptions<S>
): EnginePrefs<S> {
const schema = options.schema;
let environment: PrefsEnvironment = options.environment ?? {};
let intent: PrefsIntentOf<S> = sanitiseInitialIntent(schema, options.intent);
let version = 0;
let snapshot = buildSnapshot(schema, environment, intent, version);
let disposed = false;
const listeners = new Set<PrefsChangeHandler<S>>();
function ensureLive(method: string): void {
if (disposed) {
throw new PrefsDisposedError(disposedErrorMessage(method));
}
}
function commit(
nextEnvironment: PrefsEnvironment,
nextIntent: PrefsIntentOf<S>,
cause: PrefsChangeCause
): PrefsSnapshot<S> {
// No-op short-circuit: if every mutable layer is the same
// reference, the resolved view is by definition unchanged. Skip
// the recompute, the version bump and the listener walk.
if (nextEnvironment === environment && nextIntent === intent) {
return snapshot;
}
const previous = snapshot;
environment = nextEnvironment;
intent = nextIntent;
version += 1;
snapshot = buildSnapshot(schema, environment, intent, version);
const event: PrefsChangeEvent<S> = {
previous,
next: snapshot,
effectiveDiff: diffEffective(previous.effective, snapshot.effective),
cause
};
// Snapshot the listener set so a handler that mutates the engine
// (e.g. unsubscribes itself) doesn't skip its peers.
for (const listener of [...listeners]) {
listener(event);
}
return snapshot;
}
const engine: EnginePrefs<S> = {
kind: PREFS_KIND,
schema,
snapshot() {
return snapshot;
},
environment() {
return environment;
},
intent() {
return intent;
},
effective() {
return snapshot.effective;
},
setIntent<K extends keyof S>(key: K, value: unknown): PrefsSnapshot<S> {
ensureLive(PREFS_ENGINE_METHOD_SET_INTENT);
const stringKey = key as string;
const dim = schema[stringKey];
if (dim === undefined) {
throw new PrefsUnknownDimensionError(stringKey, unknownDimensionErrorMessage(stringKey));
}
const result = dim.validate(value);
if (!result.ok) {
throw new PrefsIntentInvalidError(
stringKey,
result.reason,
intentInvalidErrorMessage(stringKey, result.reason)
);
}
const current = (intent as Record<string, unknown>)[stringKey];
if (current === result.value) return snapshot;
const next = { ...(intent as Record<string, unknown>), [stringKey]: result.value };
return commit(environment, next as PrefsIntentOf<S>, 'intent:set');
},
clearIntent<K extends keyof S>(key: K): PrefsSnapshot<S> {
ensureLive(PREFS_ENGINE_METHOD_CLEAR_INTENT);
const stringKey = key as string;
if (schema[stringKey] === undefined) {
throw new PrefsUnknownDimensionError(stringKey, unknownDimensionErrorMessage(stringKey));
}
if ((intent as Record<string, unknown>)[stringKey] === undefined) return snapshot;
const next = { ...(intent as Record<string, unknown>) };
delete next[stringKey];
return commit(environment, next as PrefsIntentOf<S>, 'intent:clear');
},
resetIntent(next?: PrefsIntentOf<S>): PrefsSnapshot<S> {
ensureLive(PREFS_ENGINE_METHOD_RESET_INTENT);
const sanitised = sanitizeIntent(schema, (next ?? {}) as Record<string, unknown>);
if (intentEqual(intent, sanitised)) return snapshot;
return commit(environment, sanitised as PrefsIntentOf<S>, 'intent:reset');
},
refreshEnvironment(next: PrefsEnvironment): PrefsSnapshot<S> {
ensureLive(PREFS_ENGINE_METHOD_REFRESH_ENVIRONMENT);
if (environmentEqual(environment, next)) return snapshot;
return commit(next, intent, 'environment:refresh');
},
patchEnvironment(patch: Partial<PrefsEnvironment>): PrefsSnapshot<S> {
ensureLive(PREFS_ENGINE_METHOD_PATCH_ENVIRONMENT);
const merged: PrefsEnvironment = { ...environment, ...patch };
if (environmentEqual(environment, merged)) return snapshot;
return commit(merged, intent, 'environment:refresh');
},
subscribe(handler) {
if (disposed) return () => {};
listeners.add(handler);
const unsubscribe: PrefsUnsubscribe = () => {
listeners.delete(handler);
};
return unsubscribe;
},
dispose() {
if (disposed) return;
disposed = true;
listeners.clear();
}
};
return engine;
}
// ─────────────────────────────────────────────────────────────────────
// Helpers
// ─────────────────────────────────────────────────────────────────────
function sanitiseInitialIntent<S extends PrefsSchema>(
schema: S,
intent: PrefsIntentOf<S> | undefined
): PrefsIntentOf<S> {
if (intent === undefined) return {} as PrefsIntentOf<S>;
return sanitizeIntent(schema, intent as Record<string, unknown>) as PrefsIntentOf<S>;
}
function buildSnapshot<S extends PrefsSchema>(
schema: S,
environment: PrefsEnvironment,
intent: PrefsIntentOf<S>,
version: number
): PrefsSnapshot<S> {
const effective = resolvePrefs({ schema, environment, intent }) as PrefsEffectiveOf<S>;
return Object.freeze({
environment,
intent,
effective,
version
}) as PrefsSnapshot<S>;
}
/**
* Shallow per-key diff over `effective`. Returns only keys whose value
* changed — `Object.keys(diff).length === 0` is the "effective
* unchanged" signal subscribers can branch on.
*/
function diffEffective<S extends PrefsSchema>(
previous: PrefsEffectiveOf<S>,
next: PrefsEffectiveOf<S>
): Partial<PrefsEffectiveOf<S>> {
const diff: Record<string, unknown> = {};
const previousMap = previous as unknown as Record<string, unknown>;
const nextMap = next as unknown as Record<string, unknown>;
for (const key of Object.keys(nextMap)) {
if (previousMap[key] !== nextMap[key]) {
diff[key] = nextMap[key];
}
}
return diff as Partial<PrefsEffectiveOf<S>>;
}
/**
* Sparse-map equality. Keys with `undefined` values are treated as
* absent so callers can write `{ locale: undefined }` without
* spuriously committing — the canonical "clear" path is still
* `clearIntent(key)`.
*/
function intentEqual(a: object, b: object): boolean {
const aMap = a as Record<string, unknown>;
const bMap = b as Record<string, unknown>;
const aKeys = Object.keys(aMap).filter((k) => aMap[k] !== undefined);
const bKeys = Object.keys(bMap).filter((k) => bMap[k] !== undefined);
if (aKeys.length !== bKeys.length) return false;
for (const key of aKeys) {
if (aMap[key] !== bMap[key]) return false;
}
return true;
}
function environmentEqual(a: PrefsEnvironment, b: PrefsEnvironment): boolean {
if (a === b) return true;
const aMap = a as Record<string, unknown>;
const bMap = b as Record<string, unknown>;
const aKeys = Object.keys(aMap);
const bKeys = Object.keys(bMap);
if (aKeys.length !== bKeys.length) return false;
for (const key of aKeys) {
if (key === 'locales') {
if (!arrayEqual(a.locales, b.locales)) return false;
} else if (aMap[key] !== bMap[key]) {
return false;
}
}
return true;
}
function arrayEqual<T>(a: readonly T[] | undefined, b: readonly T[] | undefined): boolean {
if (a === b) return true;
if (a === undefined || b === undefined) return false;
if (a.length !== b.length) return false;
for (let i = 0; i < a.length; i += 1) {
if (a[i] !== b[i]) return false;
}
return true;
}

Powered by TurnKey Linux.