Big-bang: remove legacy translators, App.createActiveX(), APP_EVENT_*, autoInvalidate*

Completes step 3 of the active-app refactor. The orca-based service
schema model is now the single supported path; the legacy
auto-reactive ecosystem is gone.

Removed:

  - arts/active-app/integrations/session-translator.ts and auth-cache.ts
    (their purpose is replaced by orca presets in arts/active-app/presets/).
  - arts/connection/bus-session-source.ts (canonical session events
    flow through SESSION_EVENT_* directly).
  - libs/active-app/ entirely. Its contents (consts, errors, events
    reduced to APP_EVENT_DISPOSE_STARTING) consolidated into
    arts/active-app/{consts,errors,events}.ts. The libs layer no longer
    has anything app-specific.
  - From arts/cache:
      - bus / autoInvalidateOn options on ActiveCacheOptions.
      - wireAutoInvalidation internal subscription.
      - CACHE_AUTO_INVALIDATE_* constants and types.
  - From arts/perm:
      - bus / autoInvalidateOn options on ActivePermsOptions.
      - wireAutoInvalidation internal subscription.
      - PERM_AUTO_INVALIDATE_* constants and types.
  - From arts/connection:
      - bus option in EngineConnectionsOptions.
      - shouldWireBusSessionSource helper.
      - The "reacts to canonical app identity bus events" test that
        depended on the deleted bus-session-source.
  - From arts/active-app/active-app.svelte.ts:
      - createSiumEngine() / createActiveSession() / createActiveConnections() /
        createActivePerms() / createActiveAuth() factory methods.
      - App.Sess / App.Perms / App.Auth getters and the singleton
        guards (Sess !== undefined etc.).
      - APP_ORCHESTRATION_* preset system, resolveActiveAppOrchestration,
        STANDARD_ORCHESTRATION_TRANSLATORS, all five translator handles.
      - APP_ERROR_ALREADY_CREATED_* / APP_ERROR_CREATE_PERM_ENDPOINT_REQUIRED
        constants (orphan after factory removal).
  - From arts/active-app/test:
      - ecosystem.integration.test.ts (9 monolithic tests, ~1900 lines).
      - session-translator.test.ts.
      - create-sium-engine.test.ts.
  - From libs/active-app/test:
      - events.test.ts (covered the deleted publishers and the legacy
        APP_USER_IDENTITY_* causes).
  - APP_EVENT_USER_IDENTITY_CHANGED, APP_EVENT_TENANT_SWITCHED,
    APP_EVENT_PERMISSIONS_REFRESH_REQUESTED,
    APP_EVENT_CACHE_INVALIDATE_REQUESTED, APP_EVENT_CONNECTIVITY_CHANGED
    (only DISPOSE_STARTING survives).
  - Their associated payload interfaces and the
    APP_USER_IDENTITY_CAUSE_* constants.
  - publishAppUserIdentityChanged / publishAppPermsRefreshRequested /
    publishAppCacheInvalidateRequested / publishAppTenantSwitched /
    publishAppConnectivityChanged. publishAppDisposeStarting and the
    new onAppDisposeStarting helper remain.

Final shape of arts/active-app/:
  - active-app.svelte.ts: builds Logger, Lang, Format, Frontend, Dom,
    Storage, Http, Timers, Bus, Orca, Cache, then the schema services
    via buildServiceBuilders. dispose() publishes DISPOSE_STARTING and
    tears everything down in reverse construction order.
  - services.ts, service-builder.ts, service-factories/, presets/,
    bus-context.svelte.ts, consts.ts, errors.ts, events.ts, types.ts,
    index.ts (public barrel exposing every define*, applyStandardOrca,
    types and errors).

Suite: 1374 tests pass. The reduction from the prior 1408 reflects the
deleted legacy tests; coverage of the new model is comprehensive
(schema-declarative.test.ts, service-builder.test.ts,
service-factories.test.ts, presets.test.ts, active-app.test.ts core,
plus the existing per-art suites).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
master
dev 5 months ago
parent 01a85ad299
commit 64ab1f0b63

@ -1,11 +1,6 @@
import { createActiveDom } from '$adom/active-dom.svelte';
import { createActiveAuth } from '$auth/active-auth.svelte';
import { createEngineHttpAuthClient } from '$auth/client';
import type { ActiveAuth, ActiveAuthOptions } from '$auth/types';
import { createSvelteEngineBus } from '$bus';
import { createActiveCache } from '$cache/active-cache.svelte';
import { createActiveConnections as createActiveConnectionsRegistry } from '$connection/active-connections.svelte';
import type { ActiveConnections, ActiveConnectionsOptions, ConnectionMap } from '$connection/types';
import { createActiveFrontend } from '$frontend/active-frontend.svelte';
import { createActiveFormat } from '$format/active-formats.svelte';
import { createEngineHttp } from '$http/engine-http';
@ -14,49 +9,24 @@ import type { ActiveLang } from '$lang';
import { createActiveLang } from '$lang/active-lang.svelte';
import { createActiveMonoLang } from '$lang/mono-lang.svelte';
import { createEngineLogger } from '$logger/engine-logger';
import { createActivePerms as createActivePermsClient } from '$perm/active-permissions.svelte';
import { PermInvalidEndpointError } from '$perm/errors';
import type { ActivePerms, ActivePermsOptions } from '$perm/types';
import { createActiveSession } from '$session/active-session.svelte';
import { SessionAlreadyCreatedError } from '$session/errors';
import type { ActiveSession, EngineSessionOptions } from '$session/types';
import { createEngineSium } from '$sium/engine-sium';
import { createActiveStorage } from '$storage/active-storage.svelte';
import { createActiveTimers } from '$timer/active-timers.svelte';
import { createEngineOrca } from '$orca';
import { publishAppDisposeStarting } from '$libs/active-app/events';
import { publishAppDisposeStarting } from './events.ts';
import {
applyFrontendPreferenceSnapshot,
bindFrontendStorage,
loadPersistedFrontendPreferences
} from './integrations/frontend-storage';
import { createAuthCacheInvalidator } from './integrations/auth-cache';
import { wireSessionTranslator } from './integrations/session-translator';
import {
APP_ERROR_ALREADY_CREATED_PERMISSIONS,
APP_ERROR_ALREADY_CREATED_AUTH,
APP_ERROR_ALREADY_CREATED_SESSION,
APP_ERROR_CREATE_PERM_ENDPOINT_REQUIRED,
APP_ORCHESTRATION_SILENT,
APP_ORCHESTRATION_STANDARD,
APP_ORCHESTRATION_TRANSLATOR_CONNECTIVITY,
APP_ORCHESTRATION_TRANSLATOR_DISPOSE,
APP_ORCHESTRATION_TRANSLATOR_IDENTITY,
APP_ORCHESTRATION_TRANSLATOR_PERM_REFRESH,
APP_ORCHESTRATION_TRANSLATOR_TENANT_SWITCHED,
APP_MODULE
} from './consts.ts';
import { AappAlreadyCreatedError } from './errors.ts';
import { APP_MODULE } from './consts.ts';
import { buildServiceBuilders } from './service-builder.ts';
import type { AppServiceSchema, CoreServices } from './services.ts';
import type {
ActiveApp,
ActiveAppBusEvents,
ActiveAppLegacy,
ActiveAppOptions,
ActiveAppOrchestrationOptions,
ActiveAppOrchestrationTranslator
ActiveAppOptions
} from './types.ts';
import type { EngineBus } from '$bus';
@ -145,23 +115,12 @@ export function createActiveApp<
? buildServiceBuilders(options.services as AppServiceSchema, coreForServices)
: undefined;
const orchestration = resolveActiveAppOrchestration(options.orchestration);
const detachSessionTranslator = orchestration.has(APP_ORCHESTRATION_TRANSLATOR_IDENTITY)
? wireSessionTranslator(Bus)
: undefined;
const Cache = createActiveCache({
...options.cache,
logger: Logger,
bus: Bus
logger: Logger
});
let disposed = false;
let Sess: ActiveSession<unknown, unknown, unknown> | undefined;
let Perms: ActivePerms | undefined;
let Auth: ActiveAuth | undefined;
// eslint-disable-next-line svelte/prefer-svelte-reactivity -- disposal registry is not rendered state.
const connectionRegistries = new Set<ActiveConnections>();
const baseApp: ActiveAppLegacy<S> = {
Logger,
@ -176,124 +135,17 @@ export function createActiveApp<
Orca,
Cache,
get Sess() {
return Sess;
},
get Perms() {
return Perms;
},
get Auth() {
return Auth;
},
getLocale: () => Lang.getLocale(),
setLocale: (locale) => Lang.setLocale(locale),
onLocaleChange: (fn) => Lang.onLocaleChange(fn),
createSiumEngine() {
return createEngineSium({
lang: Lang,
logger: Logger,
locale: Lang.getLocale()
});
},
createActiveSession<TUser, TCredential = undefined, TData = undefined>(
sessOptions: Omit<EngineSessionOptions<TUser, TCredential, TData>, 'logger' | 'bus'> = {}
): ActiveSession<TUser, TCredential, TData> {
if (Sess !== undefined) {
throw new SessionAlreadyCreatedError(APP_ERROR_ALREADY_CREATED_SESSION);
}
const built = createActiveSession<TUser, TCredential, TData>({
...sessOptions,
logger: Logger,
bus: Bus
});
Sess = built as ActiveSession<unknown, unknown, unknown>;
return built;
},
createActiveConnections<TConnections extends ConnectionMap = ConnectionMap>(
connectionOptions: Omit<
ActiveConnectionsOptions,
'logger' | 'timers' | 'session' | 'bus'
> = {}
): ActiveConnections<TConnections> {
const built = createActiveConnectionsRegistry<TConnections>({
...options.connections,
...connectionOptions,
logger: Logger,
timers: Timers,
bus: Bus
});
connectionRegistries.add(built as ActiveConnections);
return built;
},
createActivePerms(
permissionOptions: Partial<Omit<ActivePermsOptions, 'logger' | 'http' | 'bus'>> = {}
): ActivePerms {
if (Perms !== undefined) {
throw new AappAlreadyCreatedError(APP_ERROR_ALREADY_CREATED_PERMISSIONS);
}
const merged = {
...options.permissions,
...permissionOptions
};
if (!merged.endpoint) {
throw new PermInvalidEndpointError(APP_ERROR_CREATE_PERM_ENDPOINT_REQUIRED);
}
const built = createActivePermsClient({
...merged,
endpoint: merged.endpoint,
logger: Logger,
http: Http,
bus: Bus
});
Perms = built;
return built;
},
createActiveAuth(
authOptions: Omit<ActiveAuthOptions, 'http' | 'cache' | 'logger'> = {}
): ActiveAuth {
if (Auth !== undefined) {
throw new AappAlreadyCreatedError(APP_ERROR_ALREADY_CREATED_AUTH);
}
const built = createActiveAuth({
...options.auth,
...authOptions,
logger: Logger,
http: createEngineHttpAuthClient(Http),
cache: createAuthCacheInvalidator({
cache: Cache,
permissions: () => Perms
})
});
Auth = built;
return built;
},
dispose() {
if (disposed) return;
disposed = true;
if (orchestration.has(APP_ORCHESTRATION_TRANSLATOR_DISPOSE)) {
publishAppDisposeStarting(Bus, { cause: APP_MODULE });
}
publishAppDisposeStarting(Bus, { cause: APP_MODULE });
// Schema-declared services first (reverse construction order
// is handled by the builder).
serviceBuilders?.disposeAll();
Auth?.dispose();
Auth = undefined;
Perms?.dispose();
Perms = undefined;
for (const registry of connectionRegistries) registry.dispose();
connectionRegistries.clear();
detachSessionTranslator?.();
Sess?.dispose();
Sess = undefined;
Cache.dispose();
Orca.dispose();
Bus.dispose();
@ -339,23 +191,3 @@ export function createActiveApp<
return app;
}
function resolveActiveAppOrchestration(
orchestration: ActiveAppOrchestrationOptions | undefined
): ReadonlySet<ActiveAppOrchestrationTranslator> {
if (orchestration === false || orchestration === APP_ORCHESTRATION_SILENT) {
return new Set();
}
if (orchestration === undefined || orchestration === APP_ORCHESTRATION_STANDARD) {
return new Set(STANDARD_ORCHESTRATION_TRANSLATORS);
}
return new Set(orchestration);
}
const STANDARD_ORCHESTRATION_TRANSLATORS = [
APP_ORCHESTRATION_TRANSLATOR_IDENTITY,
APP_ORCHESTRATION_TRANSLATOR_PERM_REFRESH,
APP_ORCHESTRATION_TRANSLATOR_TENANT_SWITCHED,
APP_ORCHESTRATION_TRANSLATOR_CONNECTIVITY,
APP_ORCHESTRATION_TRANSLATOR_DISPOSE
] as const;

@ -13,8 +13,8 @@
import { getContext, setContext } from 'svelte';
import type { BusEventMap } from '$libs/bus';
import type { EngineBus } from '$bus';
import { APP_BUS_CONTEXT_KEY } from '$libs/active-app/consts';
import { AappBusNoContextError } from '$libs/active-app/errors';
import { APP_BUS_CONTEXT_KEY } from './consts.ts';
import { AappBusNoContextError } from './errors.ts';
const BUS_CONTEXT = Symbol(APP_BUS_CONTEXT_KEY);

@ -1,26 +1,37 @@
export { APP_MODULE } from '$libs/active-app/consts';
import { APP_MODULE } from '$libs/active-app/consts';
/**
* Constants for `arts/active-app`. Holds the module identifier, the
* Svelte context key for the bus, the safe-publish guards (sensitive
* key parts, runtime gates) and the canonical app-event names.
*/
export const APP_LOG_MSG_SESSION_CACHE_CLEAR_FAILED =
'session change cache clear failed';
export const APP_CONNECTION_CLOSE_REASON_IDENTITY_CLEARED = 'identity_cleared';
export const APP_MODULE = 'app';
export const APP_ORCHESTRATION_STANDARD = 'standard';
export const APP_ORCHESTRATION_SILENT = 'silent';
export const APP_ORCHESTRATION_TRANSLATOR_IDENTITY = 'identity';
export const APP_ORCHESTRATION_TRANSLATOR_PERM_REFRESH = 'permissions-refresh';
export const APP_ORCHESTRATION_TRANSLATOR_TENANT_SWITCHED = 'tenant-switched';
export const APP_ORCHESTRATION_TRANSLATOR_CONNECTIVITY = 'connectivity';
export const APP_ORCHESTRATION_TRANSLATOR_DISPOSE = 'dispose';
export const APP_BUS_CONTEXT_KEY = 'arts.app.bus';
export const APP_ERROR_ALREADY_CREATED_SESSION =
`[${APP_MODULE}] App.createActiveSession() called twice — only one session per App.`;
export const APP_EVENT_SENSITIVE_KEY_PARTS = [
'authorization',
'cookie',
'credential',
'csrf',
'hash',
'header',
'password',
'secret',
'token'
] as const;
export const APP_ERROR_ALREADY_CREATED_PERMISSIONS =
`[${APP_MODULE}] App.createActivePerms() called twice — only one permissions client per App.`;
/**
* Where a given app event is allowed to be published. `'both'` is the
* default for facts that exist in both server-rendered and browser-
* mounted contexts; `'client'` is reserved for browser-only signals;
* `'server'` for events that only make sense in the request lifecycle.
*/
export const APP_EVENT_RUNTIME_BOTH = 'both';
export const APP_EVENT_RUNTIME_CLIENT = 'client';
export const APP_EVENT_RUNTIME_SERVER = 'server';
export const APP_ERROR_ALREADY_CREATED_AUTH =
`[${APP_MODULE}] App.createActiveAuth() called twice — only one active auth client per App.`;
export const APP_ERROR_CREATE_PERM_ENDPOINT_REQUIRED =
`[${APP_MODULE}] App.createActivePerms() requires endpoint via options.permissions or argument.`;
export const APP_EVENT_RUNTIMES_VALUES = [
APP_EVENT_RUNTIME_BOTH,
APP_EVENT_RUNTIME_CLIENT,
APP_EVENT_RUNTIME_SERVER
] as const;

@ -1,8 +1,16 @@
import { CodeError, errCode, type ErrCode, type ErrorMessages } from '$libs/errs';
import { APP_ERR } from '$libs/active-app/errors';
import {
CodeError,
errCode,
moduleSeed,
type ErrCode,
type ErrorMessages,
type ModuleSeed
} from '$libs/errs';
import { APP_MODULE } from './consts.ts';
// ── Error codes ────────────────────────────────────────────────────────
export const APP_ERR: ModuleSeed = moduleSeed(APP_MODULE);
export const APP_ERR_ALREADY_CREATED: ErrCode = errCode(APP_ERR, 'already_created');
export const APP_ERR_SERVICE_NAME_MISMATCH: ErrCode = errCode(APP_ERR, 'service_name_mismatch');
export const APP_ERR_SERVICE_DEPENDENCY_CYCLE: ErrCode = errCode(
@ -13,10 +21,16 @@ export const APP_ERR_SERVICE_CONSTRUCTION_FAILED: ErrCode = errCode(
APP_ERR,
'service_construction_failed'
);
export const APP_ERR_BUS: ErrCode = errCode(APP_ERR, 'bus');
export const APP_ERR_BUS_NO_CONTEXT: ErrCode = errCode(APP_ERR_BUS, 'no_context');
export const APP_ERR_EVENT: ErrCode = errCode(APP_ERR, 'event');
export const APP_ERR_EVENT_INVALID_RUNTIME: ErrCode = errCode(APP_ERR_EVENT, 'invalid_runtime');
export const APP_ERR_EVENT_UNSAFE_PAYLOAD: ErrCode = errCode(APP_ERR_EVENT, 'unsafe_payload');
// ── Error message builders ─────────────────────────────────────────────
export const APP_ERROR_MSG_ALREADY_CREATED = 'App factory called more than once.';
export const APP_ERROR_MSG_BUS_NO_CONTEXT = `[${APP_MODULE}] no bus in context — call setBus(App.Bus) in a layout before getBus()`;
export const appServiceNameMismatchMessage = (key: string, factoryName: string): string =>
`[active-app] service factory name "${factoryName}" must match schema key "${key}"`;
@ -27,13 +41,26 @@ export const appServiceDependencyCycleMessage = (cycle: readonly string[]): stri
export const appServiceConstructionFailedMessage = (name: string): string =>
`[active-app] service "${name}" failed to construct`;
export const appInvalidEventRuntimeMessage = (
type: string,
allowed: string,
where: string
): string =>
`[${APP_MODULE}] event cannot fire in this runtime (type=${type}, allowed=${allowed}, where=${where})`;
export const appUnsafeEventPayloadMessage = (type: string, path: string): string =>
`[${APP_MODULE}] app event payload contains a sensitive field (type=${type}, path=${path})`;
// ── Error messages ─────────────────────────────────────────────────────
export const APP_ERROR_MESSAGES: ErrorMessages = {
[APP_ERR_ALREADY_CREATED]: APP_ERROR_MSG_ALREADY_CREATED,
[APP_ERR_SERVICE_NAME_MISMATCH]: appServiceNameMismatchMessage,
[APP_ERR_SERVICE_DEPENDENCY_CYCLE]: appServiceDependencyCycleMessage,
[APP_ERR_SERVICE_CONSTRUCTION_FAILED]: appServiceConstructionFailedMessage
[APP_ERR_SERVICE_CONSTRUCTION_FAILED]: appServiceConstructionFailedMessage,
[APP_ERR_BUS_NO_CONTEXT]: APP_ERROR_MSG_BUS_NO_CONTEXT,
[APP_ERR_EVENT_INVALID_RUNTIME]: appInvalidEventRuntimeMessage,
[APP_ERR_EVENT_UNSAFE_PAYLOAD]: appUnsafeEventPayloadMessage
};
// ── Error classes ──────────────────────────────────────────────────────
@ -77,6 +104,38 @@ export class AappServiceConstructionFailedError extends CodeError {
}
}
export class AappBusNoContextError extends CodeError {
constructor(message = APP_ERROR_MSG_BUS_NO_CONTEXT) {
super(APP_ERR_BUS_NO_CONTEXT, { message });
}
}
export class AappInvalidEventRuntimeError extends CodeError {
readonly type: string;
readonly allowed: string;
readonly where: string;
constructor(type: string, allowed: string, where: string) {
super(APP_ERR_EVENT_INVALID_RUNTIME, {
message: appInvalidEventRuntimeMessage(type, allowed, where)
});
this.type = type;
this.allowed = allowed;
this.where = where;
}
}
export class AappUnsafeEventPayloadError extends CodeError {
readonly type: string;
readonly path: string;
constructor(type: string, path: string) {
super(APP_ERR_EVENT_UNSAFE_PAYLOAD, {
message: appUnsafeEventPayloadMessage(type, path)
});
this.type = type;
this.path = path;
}
}
// ── Type guards ────────────────────────────────────────────────────────
export function isAappAlreadyCreatedError(error: unknown): error is AappAlreadyCreatedError {
@ -100,3 +159,19 @@ export function isAappServiceConstructionFailedError(
): error is AappServiceConstructionFailedError {
return error instanceof AappServiceConstructionFailedError;
}
export function isAappBusNoContextError(error: unknown): error is AappBusNoContextError {
return error instanceof AappBusNoContextError;
}
export function isAappInvalidEventRuntimeError(
error: unknown
): error is AappInvalidEventRuntimeError {
return error instanceof AappInvalidEventRuntimeError;
}
export function isAappUnsafeEventPayloadError(
error: unknown
): error is AappUnsafeEventPayloadError {
return error instanceof AappUnsafeEventPayloadError;
}

@ -0,0 +1,106 @@
import type { BusPublishOptions, EventPublisher, EventSubscriber } from '$libs/bus';
import {
APP_EVENT_RUNTIME_BOTH,
APP_EVENT_RUNTIME_CLIENT,
APP_EVENT_SENSITIVE_KEY_PARTS
} from './consts.ts';
import { AappInvalidEventRuntimeError, AappUnsafeEventPayloadError } from './errors.ts';
/**
* The single App-owned event that survives the orca-based migration.
* Apps that need to react to teardown subscribe via `App.Bus.on(...)` or
* register an orca action.
*/
export const APP_EVENT_DISPOSE_STARTING = 'app.dispose.starting';
/**
* Where each `APP_EVENT_*` is allowed to fire. `'both'` is the default;
* `'client'` is reserved for facts that only exist in the browser.
*/
export type AppEventRuntime = typeof APP_EVENT_RUNTIME_BOTH | typeof APP_EVENT_RUNTIME_CLIENT;
export const APP_EVENT_RUNTIMES: Readonly<Record<string, AppEventRuntime>> = {
[APP_EVENT_DISPOSE_STARTING]: APP_EVENT_RUNTIME_BOTH
};
/**
* Throws `AappInvalidEventRuntimeError` if `type` is an `APP_EVENT_*`
* declared as client-only and the current runtime is the server (or
* vice-versa). Module events are not registered in `APP_EVENT_RUNTIMES`
* and are accepted unconditionally.
*/
export function assertEventCanFire(
type: string,
where: typeof APP_EVENT_RUNTIME_CLIENT | 'server'
): void {
const allowed = APP_EVENT_RUNTIMES[type];
if (allowed === undefined) return;
if (allowed === APP_EVENT_RUNTIME_BOTH) return;
if (allowed === where) return;
throw new AappInvalidEventRuntimeError(type, allowed, where);
}
export function assertAppEventPayloadSafe(type: string, payload: unknown): void {
const unsafePath = findSensitivePayloadPath(payload);
if (unsafePath !== undefined) throw new AappUnsafeEventPayloadError(type, unsafePath);
}
function findSensitivePayloadPath(value: unknown, path = '$'): string | undefined {
if (value === null || typeof value !== 'object') return undefined;
if (Array.isArray(value)) {
for (let index = 0; index < value.length; index += 1) {
const unsafe = findSensitivePayloadPath(value[index], `${path}[${index}]`);
if (unsafe !== undefined) return unsafe;
}
return undefined;
}
for (const [key, child] of Object.entries(value)) {
const childPath = `${path}.${key}`;
if (isSensitivePayloadKey(key)) return childPath;
const unsafe = findSensitivePayloadPath(child, childPath);
if (unsafe !== undefined) return unsafe;
}
return undefined;
}
function isSensitivePayloadKey(key: string): boolean {
const normalized = key.toLowerCase();
return APP_EVENT_SENSITIVE_KEY_PARTS.some((part) => normalized.includes(part));
}
export interface AppDisposeStartingPayload {
readonly cause: string;
}
export interface AppEventMap {
[APP_EVENT_DISPOSE_STARTING]: AppDisposeStartingPayload;
}
/**
* Read-only contract for App's event bus. App publishes only
* `DISPOSE_STARTING` directly; everything else flows through the bus by
* other modules (session, connection, etc.) and is orchestrated via
* `App.Orca`.
*/
export type AppEventBus = EventSubscriber<AppEventMap>;
function currentRuntime(): typeof APP_EVENT_RUNTIME_CLIENT | 'server' {
return typeof window === 'undefined' ? 'server' : APP_EVENT_RUNTIME_CLIENT;
}
export function publishAppDisposeStarting(
bus: EventPublisher<AppEventMap>,
payload: AppDisposeStartingPayload,
options?: BusPublishOptions
) {
assertEventCanFire(APP_EVENT_DISPOSE_STARTING, currentRuntime());
assertAppEventPayloadSafe(APP_EVENT_DISPOSE_STARTING, payload);
return bus.publish(APP_EVENT_DISPOSE_STARTING, payload, options);
}
export function onAppDisposeStarting(
bus: AppEventBus,
listener: (payload: AppDisposeStartingPayload) => void | Promise<void>
) {
return bus.on(APP_EVENT_DISPOSE_STARTING, (envelope) => listener(envelope.payload));
}

@ -1,30 +1,62 @@
export { createActiveApp } from './active-app.svelte';
export { APP_MODULE } from './consts';
export {
APP_ORCHESTRATION_SILENT,
APP_ORCHESTRATION_STANDARD,
APP_ORCHESTRATION_TRANSLATOR_CONNECTIVITY,
APP_ORCHESTRATION_TRANSLATOR_DISPOSE,
APP_ORCHESTRATION_TRANSLATOR_IDENTITY,
APP_ORCHESTRATION_TRANSLATOR_PERM_REFRESH,
APP_ORCHESTRATION_TRANSLATOR_TENANT_SWITCHED,
APP_MODULE
} from './consts';
export { AappAlreadyCreatedError, isAappAlreadyCreatedError } from './errors';
AappAlreadyCreatedError,
AappServiceConstructionFailedError,
AappServiceDependencyCycleError,
AappServiceNameMismatchError,
isAappAlreadyCreatedError,
isAappServiceConstructionFailedError,
isAappServiceDependencyCycleError,
isAappServiceNameMismatchError
} from './errors';
export type {
ActiveApp,
ActiveAppBusEvents,
ActiveAppLegacy,
ActiveAppOptions,
ActiveAppOrchestrationOptions,
ActiveAppOrchestrationPreset,
ActiveAppOrchestrationTranslator,
ActiveAppServicesIntrospection,
ActiveAppStorageOptions,
FrontendPersist,
FrontendPersistKey,
FrontendPersistKeyOverride
} from './types';
// `createTestApp` lives at `$active-app/testing` so it does not ship with bundles
// that import the production barrel. See `aapp/testing/index.ts`.
export type {
AppServiceFactory,
AppServiceSchema,
CoreServices,
CoreServiceKey,
ResolveServiceInstances,
ServiceInitMode,
ServiceStatus
} from './services';
export { buildServiceBuilders } from './service-builder';
// Service factories — declarative `services: { … }` schema entries.
export {
defineActiveAuth,
defineActiveCache,
defineActiveConnections,
defineActiveDom,
defineActiveFormat,
defineActiveFrontend,
defineActiveLang,
defineActivePerm,
defineActiveSession,
defineActiveStorage,
defineEngineHttp,
defineEngineSium
} from './service-factories';
// Orchestration presets — opt-in reactions registered on `App.Orca`.
export {
applyCacheClearOnIdentityChange,
applyCacheClearOnRevoke,
applyPermInvalidateOnIdentityChange,
applyStandardOrca
} from './presets';
// Common storage adapters re-exported for ergonomic single import.
// `createMemoryAdapter` lives in `$storage` only — testing-specific.
@ -35,12 +67,3 @@ export type {
BroadcastAdapter,
BroadcastOptions
} from '$storage';
// `createActiveMonoLang` is intentionally NOT re-exported — callers that
// compose without `createActiveApp` should import it directly from
// `$lang/mono-lang.svelte`. Re-exporting bloated the surface for a path
// no caller actually used.
// EngineSium type is re-exported so consumers can type variables holding
// `App.createSiumEngine()` without importing `$sium` directly.
export type { EngineSium } from '$sium';

@ -1,23 +0,0 @@
import { CACHE_SCOPE_PUBLIC } from '$libs/cache';
import type { AuthClientCachePort } from '$libs/auth/contracts';
import type { ActiveCache } from '$cache';
import type { ActivePerms } from '$perm';
export function createAuthCacheInvalidator(input: {
readonly cache: ActiveCache;
readonly permissions: () => ActivePerms | undefined;
}): AuthClientCachePort {
return {
async invalidate(event) {
input.permissions()?.invalidate();
await Promise.all(
event.tags.map((tag) =>
input.cache.invalidate({
tag,
scope: CACHE_SCOPE_PUBLIC
})
)
);
}
};
}

@ -1,64 +0,0 @@
import type { AppEventMap, AppUserIdentityChangeCause } from '$libs/active-app/events';
import {
APP_USER_IDENTITY_CAUSE_SESSION_ADOPTED,
APP_USER_IDENTITY_CAUSE_SESSION_ADOPTED_SERVER,
APP_USER_IDENTITY_CAUSE_SESSION_EXPIRED,
APP_USER_IDENTITY_CAUSE_SESSION_EXTERNAL_CHANGED,
APP_USER_IDENTITY_CAUSE_SESSION_REFRESHED,
APP_USER_IDENTITY_CAUSE_SESSION_REVOKED,
publishAppUserIdentityChanged
} from '$libs/active-app/events';
import {
SESSION_EVENT_LIFECYCLE_ADOPTED,
SESSION_EVENT_LIFECYCLE_ADOPTED_SERVER,
SESSION_EVENT_LIFECYCLE_EXPIRED,
SESSION_EVENT_LIFECYCLE_EXTERNAL_CHANGED,
SESSION_EVENT_LIFECYCLE_REFRESHED,
SESSION_EVENT_LIFECYCLE_REVOKED,
SESSION_EVENT_CHANGED
} from '$session/consts';
import type { SessEventMap, SessLifecyclePayload, SessionEvent } from '$session/types';
import type { EngineBus } from '$bus';
import { APP_MODULE } from '../consts.ts';
type SessionTranslatorBusEvents = AppEventMap & SessEventMap;
export function wireSessionTranslator(bus: EngineBus<SessionTranslatorBusEvents>): () => void {
const subscription = bus.on(SESSION_EVENT_CHANGED, (event) => {
publishAppIdentityFromSessionLifecycleEvent(bus, event.payload);
});
return () => subscription.unsubscribe();
}
export function publishAppIdentityFromSessionLifecycleEvent(
bus: EngineBus<SessionTranslatorBusEvents>,
payload: SessLifecyclePayload
): void {
const cause = resolveAppIdentityCause(payload.event);
if (cause === undefined) return;
publishAppUserIdentityChanged(
bus,
{
event: payload.event,
generation: payload.generation,
identity: {
from: payload.identity.from,
to: payload.identity.to
},
cause
},
{
source: APP_MODULE
}
);
}
function resolveAppIdentityCause(event: SessionEvent): AppUserIdentityChangeCause | undefined {
if (event === SESSION_EVENT_LIFECYCLE_ADOPTED) return APP_USER_IDENTITY_CAUSE_SESSION_ADOPTED;
if (event === SESSION_EVENT_LIFECYCLE_ADOPTED_SERVER) return APP_USER_IDENTITY_CAUSE_SESSION_ADOPTED_SERVER;
if (event === SESSION_EVENT_LIFECYCLE_REFRESHED) return APP_USER_IDENTITY_CAUSE_SESSION_REFRESHED;
if (event === SESSION_EVENT_LIFECYCLE_REVOKED) return APP_USER_IDENTITY_CAUSE_SESSION_REVOKED;
if (event === SESSION_EVENT_LIFECYCLE_EXPIRED) return APP_USER_IDENTITY_CAUSE_SESSION_EXPIRED;
if (event === SESSION_EVENT_LIFECYCLE_EXTERNAL_CHANGED) return APP_USER_IDENTITY_CAUSE_SESSION_EXTERNAL_CHANGED;
return undefined;
}

@ -1,70 +0,0 @@
/**
* App.createSiumEngine() — page-scoped Sium construction smoke tests.
*
* Verifies that the method:
* - returns a working EngineSium wired to App.Lang and App.Logger
* - routes validation diagnostics through the App's Logger
* - returns a fresh engine per call
*/
import { describe, it, expect } from 'vitest';
import { createTestApp } from '../testing';
import { object, pipe, string, email, min, meta } from '$sium/core';
import { siumLangs } from '$sium';
import { LogLevel } from '$logger';
import type { LangNode } from '$lang';
const langSchema = {
auth: { loginCta: { es: 'Entrar', en: 'Sign in' } },
sium: siumLangs
} satisfies LangNode;
const Login = object({
email: pipe(string(), email(), meta({ label: 'Email' })),
password: pipe(string(), min(8), meta({ label: 'Password' }))
});
describe('App.createSiumEngine()', () => {
it('returns a working EngineSium wired to App.Lang', async () => {
const App = createTestApp({ lang: { schema: langSchema, defaultLocale: 'es' } });
const sium = App.createSiumEngine();
const result = await sium.validate(Login, { email: 'no', password: '123' });
expect(result.ok).toBe(false);
// Issues are translated via App.Lang because the method passed it in.
if (!result.ok) {
const messages = result.issues.map((i) => sium.resolveIssue(i, App.getLocale()));
expect(messages.every((m) => typeof m === 'string' && m.length > 0)).toBe(true);
}
App.dispose();
});
it('routes validation diagnostics through App.Logger', async () => {
const App = createTestApp({
captureLogs: true,
lang: { schema: langSchema, defaultLocale: 'es' }
});
const sium = App.createSiumEngine();
await sium.validate(Login, { email: 'no', password: '123' });
// Sium emits a debug entry under category 'sium' on validation failure.
const debug = App.entries.find(
(e) => e.category === 'sium' && e.level === LogLevel.DEBUG
);
expect(debug).toBeDefined();
expect(debug?.context).toMatchObject({ issueCount: expect.any(Number) });
App.dispose();
});
it('returns a fresh sium instance per call', () => {
const App = createTestApp({ lang: { schema: langSchema, defaultLocale: 'es' } });
const a = App.createSiumEngine();
const b = App.createSiumEngine();
expect(a).not.toBe(b);
App.dispose();
});
});

@ -1,31 +1,18 @@
import type { ActiveDom, ActiveDomProps } from '$adom';
import type { ActiveAuth, ActiveAuthOptions } from '$auth';
import type { EngineBus, EngineBusOptions } from '$bus';
import type { ActiveCache, ActiveCacheOptions } from '$cache';
import type { ActiveConnections, ActiveConnectionsOptions, ConnectionMap } from '$connection';
import type { ActiveFrontend, ActiveFrontendOptions } from '$frontend';
import type { FrontendPreferenceKey } from '$frontend';
import type { ActiveFormat, ActiveFormatOptions } from '$format';
import type { EngineHttp, EngineHttpOptions } from '$http';
import type { AppEventMap } from '$libs/active-app/events';
import type { AppEventMap } from './events.ts';
import type { LangNode, SupportedLocale } from '$libs/lang';
import type { ActiveLang } from '$lang';
import type { EngineLogger, LoggerOptions } from '$logger';
import type { EngineOrca } from '$orca';
import type { ActivePerms, ActivePermsOptions } from '$perm';
import type { ActiveSession, EngineSessionOptions, SessEventMap } from '$session';
import type { EngineSium } from '$sium';
import type { SessEventMap } from '$session';
import type { ActiveStorage, SyncStorageAdapter } from '$storage';
import type { ActiveTimers, EngineTimersOptions } from '$timer';
import type {
APP_ORCHESTRATION_SILENT,
APP_ORCHESTRATION_STANDARD,
APP_ORCHESTRATION_TRANSLATOR_CONNECTIVITY,
APP_ORCHESTRATION_TRANSLATOR_DISPOSE,
APP_ORCHESTRATION_TRANSLATOR_IDENTITY,
APP_ORCHESTRATION_TRANSLATOR_PERM_REFRESH,
APP_ORCHESTRATION_TRANSLATOR_TENANT_SWITCHED
} from './consts.ts';
import type {
AppServiceSchema,
ResolveServiceInstances,
@ -77,22 +64,6 @@ export interface ActiveAppStorageOptions {
namespace?: string;
}
export type ActiveAppOrchestrationPreset =
| false
| typeof APP_ORCHESTRATION_SILENT
| typeof APP_ORCHESTRATION_STANDARD;
export type ActiveAppOrchestrationTranslator =
| typeof APP_ORCHESTRATION_TRANSLATOR_IDENTITY
| typeof APP_ORCHESTRATION_TRANSLATOR_PERM_REFRESH
| typeof APP_ORCHESTRATION_TRANSLATOR_TENANT_SWITCHED
| typeof APP_ORCHESTRATION_TRANSLATOR_CONNECTIVITY
| typeof APP_ORCHESTRATION_TRANSLATOR_DISPOSE;
export type ActiveAppOrchestrationOptions =
| ActiveAppOrchestrationPreset
| readonly ActiveAppOrchestrationTranslator[];
/**
* Options for `createActiveApp()`. Every section is optional.
*
@ -175,29 +146,7 @@ export interface ActiveAppOptions<
* resolver when data should persist, share across layers or segment by
* tenant/actor/permission.
*/
cache?: Omit<ActiveCacheOptions, 'logger' | 'bus'>;
/**
* @deprecated Pair with the deprecated `App.createActiveConnections()`.
* Use `services: { connections: defineActiveConnections(...) }` instead.
*/
connections?: Omit<ActiveConnectionsOptions, 'logger' | 'timers' | 'session' | 'bus'>;
/**
* @deprecated Pair with the deprecated `App.createActivePerms()`.
* Use `services: { perm: defineActivePerm(...) }` instead.
*/
permissions?: Omit<ActivePermsOptions, 'logger' | 'http' | 'bus'>;
/**
* @deprecated Pair with the deprecated `App.createActiveAuth()`.
* Use `services: { auth: defineActiveAuth(...) }` instead.
*/
auth?: Omit<ActiveAuthOptions, 'http' | 'cache' | 'logger'>;
/**
* @deprecated The legacy translator system is being replaced by the
* orca-based presets in `arts/active-app/presets/`. Apply
* `applyStandardOrca(App)` after construction to register equivalent
* reactions, or cherry-pick individual presets.
*/
orchestration?: ActiveAppOrchestrationOptions;
cache?: Omit<ActiveCacheOptions, 'logger'>;
}
/**
@ -229,6 +178,12 @@ export interface ActiveAppServicesIntrospection {
readonly services: Readonly<Record<string, ServiceStatus>>;
}
/**
* The fixed core surface present on every App: Logger, Lang, Format,
* Frontend, Dom, Storage, Http, Timers, Bus, Orca, Cache, locale flow
* and `dispose`. Service-schema instances live alongside on
* `ActiveApp<S, TSchema>` (lowercase keys).
*/
export interface ActiveAppLegacy<S extends LangNode = LangNode> {
readonly Logger: EngineLogger;
readonly Lang: ActiveLang<S>;
@ -252,63 +207,6 @@ export interface ActiveAppLegacy<S extends LangNode = LangNode> {
setLocale: (locale: SupportedLocale) => void;
onLocaleChange: (fn: (locale: SupportedLocale) => void) => () => void;
/**
* Build an `EngineSium` wired to this App's `Lang`, `Logger` and current
* locale.
*
* @deprecated Declare `sium: defineEngineSium()` in the App service
* schema and use `App.sium` instead. Each `App.sium` access returns the
* same instance; cleanup runs on `App.dispose()`.
*/
createSiumEngine: () => EngineSium;
/**
* Build the App-scoped `ActiveSession`.
*
* @deprecated Declare `session: defineActiveSession<TUser, ...>({ ... })`
* in the App service schema and use `App.session` instead. The schema-
* driven session is built lazily on first access, gets `App.Logger` and
* `App.Bus` injected automatically, and is disposed by `App.dispose()`.
*/
createActiveSession<TUser, TCredential = undefined, TData = undefined>(
options?: Omit<EngineSessionOptions<TUser, TCredential, TData>, 'logger' | 'bus'>
): ActiveSession<TUser, TCredential, TData>;
/**
* @deprecated Declare `connections: defineActiveConnections({ ... })`
* in the App service schema and use `App.connections` instead.
*/
createActiveConnections<TConnections extends ConnectionMap = ConnectionMap>(
options?: Omit<ActiveConnectionsOptions, 'logger' | 'timers' | 'session' | 'bus'>
): ActiveConnections<TConnections>;
/**
* @deprecated Declare `perm: defineActivePerm({ endpoint, ... })` in the
* App service schema and use `App.perm` instead.
*/
createActivePerms(
options?: Partial<Omit<ActivePermsOptions, 'logger' | 'http' | 'bus'>>
): ActivePerms;
/**
* @deprecated Declare `auth: defineActiveAuth({ http, ... })` in the
* App service schema and use `App.auth` instead.
*/
createActiveAuth(options?: Omit<ActiveAuthOptions, 'http' | 'cache' | 'logger'>): ActiveAuth;
/**
* The active session, when one has been built via
* `App.createActiveSession(...)`. `undefined` until then.
*
* @deprecated Use `App.session` (lowercase) declared via
* `services: { session: defineActiveSession(...) }` instead.
*/
readonly Sess: ActiveSession<unknown, unknown, unknown> | undefined;
/** @deprecated Use `App.perm` (lowercase) declared via the schema. */
readonly Perms: ActivePerms | undefined;
/** @deprecated Use `App.auth` (lowercase) declared via the schema. */
readonly Auth: ActiveAuth | undefined;
/** Tear down owned instances in reverse construction order. Idempotent. */
dispose: () => void;
}

@ -1,15 +1,6 @@
import type { BusSubscription } from '$libs/bus';
import {
APP_EVENT_TENANT_SWITCHED,
APP_EVENT_USER_IDENTITY_CHANGED
} from '$libs/active-app/events';
import { CACHE_EVENT_ALL, type CacheClock, type CacheEvent } from '$libs/cache';
import { createEngineCache } from '$svrs/cache';
import {
CACHE_AUTO_INVALIDATE_NONE,
CACHE_AUTO_INVALIDATE_STANDARD,
CACHE_AUTO_INVALIDATE_TENANT_SWITCHED,
CACHE_AUTO_INVALIDATE_USER_IDENTITY_CHANGE,
CACHE_ACTIVE_DIAGNOSTIC_EVENTS,
CACHE_METHOD_CLEAR,
CACHE_METHOD_ENTRY,
@ -56,7 +47,6 @@ export function createActiveCache(options: ActiveCacheOptions = {}): ActiveCache
eventCountCell += 1;
notify();
});
const busSubscriptions = wireAutoInvalidation();
function updateLoading(delta: number): void {
loadingCount = Math.max(0, loadingCount + delta);
@ -192,7 +182,6 @@ export function createActiveCache(options: ActiveCacheOptions = {}): ActiveCache
dispose() {
if (disposed) return;
disposed = true;
for (const subscription of busSubscriptions) subscription.unsubscribe();
for (const activeEntry of entries) activeEntry.dispose();
entries.clear();
offEvents();
@ -200,47 +189,8 @@ export function createActiveCache(options: ActiveCacheOptions = {}): ActiveCache
listeners.clear();
}
};
function wireAutoInvalidation(): readonly BusSubscription[] {
const bus = options.bus;
if (bus === undefined) return [];
const targets = resolveAutoInvalidateTargets(options.autoInvalidateOn);
const subscriptions: BusSubscription[] = [];
if (targets.has(CACHE_AUTO_INVALIDATE_USER_IDENTITY_CHANGE)) {
subscriptions.push(
bus.on(APP_EVENT_USER_IDENTITY_CHANGED, () => {
if (disposed) return;
void track(CACHE_METHOD_CLEAR, () => engine.clear()).catch(() => undefined);
})
);
}
if (targets.has(CACHE_AUTO_INVALIDATE_TENANT_SWITCHED)) {
subscriptions.push(
bus.on(APP_EVENT_TENANT_SWITCHED, () => {
if (disposed) return;
void track(CACHE_METHOD_CLEAR, () => engine.clear()).catch(() => undefined);
})
);
}
return subscriptions;
}
}
const systemClock: CacheClock = {
now: () => Date.now()
};
function resolveAutoInvalidateTargets(
autoInvalidateOn: ActiveCacheOptions['autoInvalidateOn']
): ReadonlySet<string> {
if (autoInvalidateOn === undefined || autoInvalidateOn === CACHE_AUTO_INVALIDATE_NONE) {
return new Set();
}
if (autoInvalidateOn === CACHE_AUTO_INVALIDATE_STANDARD) {
return new Set([
CACHE_AUTO_INVALIDATE_USER_IDENTITY_CHANGE,
CACHE_AUTO_INVALIDATE_TENANT_SWITCHED
]);
}
return new Set(autoInvalidateOn);
}

@ -59,11 +59,6 @@ export const CACHE_ACTIVE_DIAGNOSTIC_EVENTS = {
export const CACHE_ACTIVE_LOG_MESSAGE_OPERATION_FAILED = 'active cache operation failed';
export const CACHE_AUTO_INVALIDATE_NONE = 'none';
export const CACHE_AUTO_INVALIDATE_STANDARD = 'standard';
export const CACHE_AUTO_INVALIDATE_USER_IDENTITY_CHANGE = 'userIdentityChange';
export const CACHE_AUTO_INVALIDATE_TENANT_SWITCHED = 'tenantSwitched';
export const CACHE_METHOD_ENTRY = 'cache.entry';
export const CACHE_ACTIVE_ENTRY_EVENTS = [

@ -16,10 +16,6 @@ export {
CACHE_ACTIVE_STATUS_STALE,
CACHE_ACTIVE_STATUS_SUCCESS,
CACHE_ACTIVE_STATUSES,
CACHE_AUTO_INVALIDATE_NONE,
CACHE_AUTO_INVALIDATE_STANDARD,
CACHE_AUTO_INVALIDATE_TENANT_SWITCHED,
CACHE_AUTO_INVALIDATE_USER_IDENTITY_CHANGE,
CACHE_LOG_MESSAGE_ADAPTER_ERROR,
CACHE_LOG_MESSAGE_BY_EVENT,
CACHE_LOG_MESSAGE_DELETE,
@ -72,8 +68,6 @@ export type {
ActiveCacheEntryOptions,
ActiveCacheEntrySnapshot,
ActiveCacheEntryStatus,
ActiveCacheAutoInvalidateOn,
ActiveCacheAutoInvalidateTarget,
ActiveCacheOptions,
EngineCache,
EngineCacheOptions

@ -1,12 +1,4 @@
import type {
CACHE_ACTIVE_ENTRY_EVENTS,
CACHE_ACTIVE_STATUSES,
CACHE_AUTO_INVALIDATE_NONE,
CACHE_AUTO_INVALIDATE_STANDARD,
CACHE_AUTO_INVALIDATE_TENANT_SWITCHED,
CACHE_AUTO_INVALIDATE_USER_IDENTITY_CHANGE
} from './consts.ts';
import type { AppEventBus } from '$libs/active-app/events';
import type { CACHE_ACTIVE_ENTRY_EVENTS, CACHE_ACTIVE_STATUSES } from './consts.ts';
import type { CacheClock, CacheEvent, CacheKey, QueryOptions, SetOptions } from '$libs/cache';
import type { EngineCache, EngineCacheOptions } from '$svrs/cache';
import type { ActiveChangeListener, ActiveEngine } from '$libs/active';
@ -48,32 +40,7 @@ export interface ActiveCacheEntryOptions<T> extends QueryOptions<T> {
clock?: CacheClock;
}
export type ActiveCacheAutoInvalidateTarget =
| typeof CACHE_AUTO_INVALIDATE_USER_IDENTITY_CHANGE
| typeof CACHE_AUTO_INVALIDATE_TENANT_SWITCHED;
export type ActiveCacheAutoInvalidateOn =
| typeof CACHE_AUTO_INVALIDATE_NONE
| typeof CACHE_AUTO_INVALIDATE_STANDARD
| readonly ActiveCacheAutoInvalidateTarget[];
export interface ActiveCacheOptions extends EngineCacheOptions {
/**
* @deprecated Pass through `arts/active-app/presets/applyStandardOrca`
* (or the individual preset `applyCacheClearOnIdentityChange`) instead.
* The internal subscription mechanism is kept temporarily for back-
* compat and will be removed when the ecosystem tests migrate to
* orca-based reactions.
*/
readonly bus?: AppEventBus;
/**
* @deprecated Use the orca preset
* `arts/active-app/presets/applyCacheClearOnIdentityChange` (or the
* aggregator `applyStandardOrca`) instead. The recommended path is to
* leave this option unset and register reactions on `App.Orca`.
*/
readonly autoInvalidateOn?: ActiveCacheAutoInvalidateOn;
}
export interface ActiveCacheOptions extends EngineCacheOptions {}
export interface ActiveCacheSnapshot {
readonly lastEvent: CacheEvent | null;

@ -1,18 +0,0 @@
import {
APP_EVENT_USER_IDENTITY_CHANGED,
type AppEventBus
} from '$libs/active-app/events';
import type { ConnectionSessionSource } from './types.ts';
export function createBusSessionSource(bus: AppEventBus): ConnectionSessionSource {
return {
onChange(listener) {
const subscription = bus.on(APP_EVENT_USER_IDENTITY_CHANGED, (event) => {
listener(event.payload);
});
return () => {
subscription.unsubscribe();
};
}
};
}

@ -1,9 +1,5 @@
import { createBusSessionSource } from './bus-session-source.ts';
import { createConnection } from './connection.ts';
import {
CONNECTION_AUTO_REAUTH_NONE,
CONNECTION_AUTO_REAUTH_STANDARD,
CONNECTION_AUTO_REAUTH_USER_IDENTITY_CHANGE,
CONNECTION_METHOD_CLOSE_ALL,
CONNECTION_METHOD_CLOSE_CONNECTION,
CONNECTION_METHOD_CONNECTION,
@ -84,11 +80,7 @@ export function createEngineConnections<TConnections extends ConnectionMap = Con
): EngineConnections<TConnections> {
const connections = new Map<string, Connection>();
const { timers } = options;
const sessionSource =
options.session ??
(options.bus === undefined || !shouldWireBusSessionSource(options.autoReauthOn)
? undefined
: createBusSessionSource(options.bus));
const sessionSource = options.session;
let disposed = false;
function ensureLive(method: string): void {
@ -180,11 +172,3 @@ export function createEngineConnections<TConnections extends ConnectionMap = Con
return engine;
}
function shouldWireBusSessionSource(
autoReauthOn: EngineConnectionsOptions['autoReauthOn']
): boolean {
if (autoReauthOn === undefined || autoReauthOn === CONNECTION_AUTO_REAUTH_NONE) return false;
if (autoReauthOn === CONNECTION_AUTO_REAUTH_STANDARD) return true;
return autoReauthOn.includes(CONNECTION_AUTO_REAUTH_USER_IDENTITY_CHANGE);
}

@ -1,5 +1,4 @@
export { createActiveConnections } from './active-connections.svelte.ts';
export { createBusSessionSource } from './bus-session-source.ts';
export { createConnectionChannel } from './channel.ts';
export { createConnection } from './connection.ts';
export { createEngineConnections } from './engine-connections.ts';

@ -18,14 +18,6 @@ import {
createFrame,
createMockTransport
} from '../index.ts';
import {
createEngineBus
} from '$bus';
import {
APP_USER_IDENTITY_CAUSE_SESSION_REVOKED,
publishAppUserIdentityChanged,
type AppEventMap
} from '$libs/active-app/events';
import { createEngineTimers, type EngineTimers } from '$timer';
import type { TimerClock } from '$libs/timer';
import type { ConnectionFrame, MockConnectionTransport } from '../index.ts';
@ -405,34 +397,6 @@ describe('Connection channels', () => {
});
describe('Connection session bridge', () => {
it('reacts to canonical app identity bus events when auto reauth is enabled', async () => {
const Bus = createEngineBus<AppEventMap>();
const Connections = createEngineConnections({
bus: Bus,
autoReauthOn: CONNECTION_AUTO_REAUTH_STANDARD,
timers
});
const Main = Connections.createConnection('main', {
transport: createMockTransport(),
heartbeat: false,
reconnect: false,
session: { enabled: true }
});
await Main.connect();
publishAppUserIdentityChanged(Bus, {
event: CONNECTION_SESSION_EVENT_REVOKED,
generation: 1,
identity: { from: 'identified', to: 'none' },
cause: APP_USER_IDENTITY_CAUSE_SESSION_REVOKED
});
expect(Main.state).toBe(CONNECTION_STATE_CLOSED);
Connections.dispose();
Bus.dispose();
});
it('disconnects opted-in connections when session expires', async () => {
let listener: ((change: { readonly event: string }) => void) | undefined;
const Connections = createEngineConnections({

@ -1,5 +1,4 @@
import type { Logger } from '$libs/logger';
import type { AppEventBus } from '$libs/active-app/events';
import type { TimerScheduler } from '$libs/timer';
import type {
CONNECTION_ACK_REASON_CLOSED,
@ -366,7 +365,6 @@ export interface EngineConnectionsOptions {
*/
readonly timers: TimerScheduler;
readonly defaults?: Partial<ConnectionOptions>;
readonly bus?: AppEventBus;
readonly autoReauthOn?: ConnectionAutoReauthOn;
readonly session?: ConnectionSessionSource;
}

@ -1,16 +1,5 @@
import type { BusSubscription } from '$libs/bus';
import {
APP_EVENT_PERMISSIONS_REFRESH_REQUESTED,
APP_EVENT_TENANT_SWITCHED,
APP_EVENT_USER_IDENTITY_CHANGED
} from '$libs/active-app/events';
import { createPermClient } from './client.ts';
import {
PERM_AUTO_INVALIDATE_NONE,
PERM_AUTO_INVALIDATE_PERMISSIONS_REFRESH,
PERM_AUTO_INVALIDATE_STANDARD,
PERM_AUTO_INVALIDATE_TENANT_SWITCHED,
PERM_AUTO_INVALIDATE_USER_IDENTITY_CHANGE,
PERM_METHOD_BATCH,
PERM_METHOD_CAN,
PERM_METHOD_CHECK,
@ -48,7 +37,6 @@ export function createActivePerms(options: ActivePermsOptions): ActivePerms {
const off = client.subscribe((snapshot) => {
snapshotCell = snapshot;
});
const busSubscriptions = wireAutoInvalidation();
function updateLoading(delta: number): void {
loadingCount = Math.max(0, loadingCount + delta);
@ -135,61 +123,12 @@ export function createActivePerms(options: ActivePermsOptions): ActivePerms {
dispose() {
if (disposed) return;
disposed = true;
for (const subscription of busSubscriptions) subscription.unsubscribe();
off();
client.dispose();
}
};
function wireAutoInvalidation(): readonly BusSubscription[] {
const bus = options.bus;
if (bus === undefined) return [];
const targets = resolveAutoInvalidateTargets(options.autoInvalidateOn);
const subscriptions: BusSubscription[] = [];
if (targets.has(PERM_AUTO_INVALIDATE_USER_IDENTITY_CHANGE)) {
subscriptions.push(
bus.on(APP_EVENT_USER_IDENTITY_CHANGED, () => {
if (!disposed) client.invalidate();
})
);
}
if (targets.has(PERM_AUTO_INVALIDATE_PERMISSIONS_REFRESH)) {
subscriptions.push(
bus.on(APP_EVENT_PERMISSIONS_REFRESH_REQUESTED, () => {
if (!disposed) client.invalidate();
})
);
}
if (targets.has(PERM_AUTO_INVALIDATE_TENANT_SWITCHED)) {
subscriptions.push(
bus.on(APP_EVENT_TENANT_SWITCHED, () => {
if (!disposed) client.invalidate();
})
);
}
return subscriptions;
}
}
function normalizeActivePermError(error: unknown): ActivePermError {
return error instanceof Error ? error : new Error(String(error));
}
function resolveAutoInvalidateTargets(
autoInvalidateOn: ActivePermsOptions['autoInvalidateOn']
): ReadonlySet<string> {
if (
autoInvalidateOn === undefined ||
autoInvalidateOn === PERM_AUTO_INVALIDATE_NONE
) {
return new Set();
}
if (autoInvalidateOn === PERM_AUTO_INVALIDATE_STANDARD) {
return new Set([
PERM_AUTO_INVALIDATE_USER_IDENTITY_CHANGE,
PERM_AUTO_INVALIDATE_PERMISSIONS_REFRESH,
PERM_AUTO_INVALIDATE_TENANT_SWITCHED
]);
}
return new Set(autoInvalidateOn);
}

@ -59,12 +59,6 @@ export const PERM_METHOD_DECISION_KEY = 'perm.decisionKey';
export const PERM_METHOD_HYDRATE = 'perm.hydrate';
export const PERM_METHOD_INVALIDATE = 'perm.invalidate';
export const PERM_AUTO_INVALIDATE_NONE = 'none';
export const PERM_AUTO_INVALIDATE_STANDARD = 'standard';
export const PERM_AUTO_INVALIDATE_USER_IDENTITY_CHANGE = 'userIdentityChange';
export const PERM_AUTO_INVALIDATE_PERMISSIONS_REFRESH = 'permissionsRefresh';
export const PERM_AUTO_INVALIDATE_TENANT_SWITCHED = 'tenantSwitched';
export const PERM_LOG_MSG_REMOTE_CHECK_FAILED = 'remote authorization check failed';
export const PERM_LOG_MSG_REMOTE_BATCH_FAILED = 'remote authorization batch failed';
export const PERM_LOG_MSG_REMOTE_WHAT_FAILED = 'remote authorization what failed';

@ -10,11 +10,6 @@ export { permDecisionKey, stablePermStringify } from '$libs/svrs/perm';
export {
PERM_MODULE,
PERM_AUTO_INVALIDATE_NONE,
PERM_AUTO_INVALIDATE_PERMISSIONS_REFRESH,
PERM_AUTO_INVALIDATE_STANDARD,
PERM_AUTO_INVALIDATE_TENANT_SWITCHED,
PERM_AUTO_INVALIDATE_USER_IDENTITY_CHANGE,
PERM_CLIENT_DIAGNOSTIC_EVENTS,
PERM_CLIENT_CONTEXT_EMPTY,
PERM_CLIENT_DEFAULT_CACHE_TTL_MS,
@ -111,8 +106,6 @@ export type {
PermClientClock,
PermClientCheckInput,
PermClientOptions,
PermAutoInvalidateOn,
PermAutoInvalidateTarget,
PermDecision,
PermEffect,
PermFallback,

@ -1,15 +1,7 @@
import type { Logger } from '$libs/logger';
import type { AppEventBus } from '$libs/active-app/events';
import type { ExplainResult, PermDecision, ResourceRef, SubjectRef } from '$libs/perm';
import type { EngineHttp } from '$http';
import type { ActiveChangeListener, ActiveEngine } from '$libs/active';
import type {
PERM_AUTO_INVALIDATE_NONE,
PERM_AUTO_INVALIDATE_PERMISSIONS_REFRESH,
PERM_AUTO_INVALIDATE_STANDARD,
PERM_AUTO_INVALIDATE_TENANT_SWITCHED,
PERM_AUTO_INVALIDATE_USER_IDENTITY_CHANGE
} from './consts.ts';
import type {
PermDisposedError,
PermInvalidEndpointError,
@ -66,16 +58,6 @@ export interface PermClientOptions {
readonly onError?: (error: unknown) => void;
}
export type PermAutoInvalidateTarget =
| typeof PERM_AUTO_INVALIDATE_USER_IDENTITY_CHANGE
| typeof PERM_AUTO_INVALIDATE_PERMISSIONS_REFRESH
| typeof PERM_AUTO_INVALIDATE_TENANT_SWITCHED;
export type PermAutoInvalidateOn =
| typeof PERM_AUTO_INVALIDATE_NONE
| typeof PERM_AUTO_INVALIDATE_STANDARD
| readonly PermAutoInvalidateTarget[];
export interface PermClientCheckInput {
readonly action: string;
readonly resource?: ResourceRef;
@ -123,16 +105,4 @@ export interface ActivePerms
onChange(listener: ActiveChangeListener<PermSnapshot>): () => void;
}
export interface ActivePermsOptions extends PermClientOptions {
/**
* @deprecated Use the orca preset `applyPermInvalidateOnIdentityChange`
* (or the aggregator `applyStandardOrca`) instead. Kept for back-compat
* until the ecosystem tests migrate to orca.
*/
readonly bus?: AppEventBus;
/**
* @deprecated Use the orca preset
* `arts/active-app/presets/applyPermInvalidateOnIdentityChange`.
*/
readonly autoInvalidateOn?: PermAutoInvalidateOn;
}
export interface ActivePermsOptions extends PermClientOptions {}

@ -1,41 +0,0 @@
/**
* Generic constants for `libs/active-app`. Public app-event names and payload
* types live in `./events.ts`; error codes, messages and classes live in
* `./errors.ts`; constants here are framework-internal (context keys,
* runtime gates).
*/
export const APP_MODULE = 'app';
export const APP_BUS_CONTEXT_KEY = 'libs.app.bus';
export const APP_EVENT_SENSITIVE_KEY_PARTS = [
'authorization',
'cookie',
'credential',
'csrf',
'hash',
'header',
'password',
'secret',
'token'
] as const;
/**
* Where a given app event is allowed to be published. `'both'` is the
* default for facts that exist in both server-rendered and
* browser-mounted contexts; `'client'` is reserved for events that
* depend on browser-only signals (online/offline, focus, etc.);
* `'server'` is reserved for events that only make sense in the
* server's request lifecycle (currently none, but the slot is here so
* the contract scales).
*/
export const APP_EVENT_RUNTIME_BOTH = 'both';
export const APP_EVENT_RUNTIME_CLIENT = 'client';
export const APP_EVENT_RUNTIME_SERVER = 'server';
export const APP_EVENT_RUNTIMES_VALUES = [
APP_EVENT_RUNTIME_BOTH,
APP_EVENT_RUNTIME_CLIENT,
APP_EVENT_RUNTIME_SERVER
] as const;

@ -1,81 +0,0 @@
import {
CodeError,
errCode,
moduleSeed,
type ErrCode,
type ErrorMessages,
type ModuleSeed
} from '$libs/errs';
import { APP_MODULE } from './consts.ts';
// ── Error codes ────────────────────────────────────────────────────────
export const APP_ERR: ModuleSeed = moduleSeed(APP_MODULE);
export const APP_ERR_BUS: ErrCode = errCode(APP_ERR, 'bus');
export const APP_ERR_BUS_NO_CONTEXT: ErrCode = errCode(APP_ERR_BUS, 'no_context');
export const APP_ERR_EVENT: ErrCode = errCode(APP_ERR, 'event');
export const APP_ERR_EVENT_INVALID_RUNTIME: ErrCode = errCode(APP_ERR_EVENT, 'invalid_runtime');
export const APP_ERR_EVENT_UNSAFE_PAYLOAD: ErrCode = errCode(APP_ERR_EVENT, 'unsafe_payload');
// ── Error messages ─────────────────────────────────────────────────────
export const APP_ERROR_MESSAGES: ErrorMessages = {
[APP_ERR_BUS_NO_CONTEXT]:
`[${APP_MODULE}] no bus in context — call setBus(App.Bus) in a layout before getBus()`,
[APP_ERR_EVENT_INVALID_RUNTIME]: `[${APP_MODULE}] event cannot fire in this runtime`,
[APP_ERR_EVENT_UNSAFE_PAYLOAD]: `[${APP_MODULE}] app event payload contains a sensitive field`
};
// ── Error classes ──────────────────────────────────────────────────────
export class AappBusNoContextError extends CodeError {
constructor(message = APP_ERROR_MESSAGES[APP_ERR_BUS_NO_CONTEXT] as string) {
super(APP_ERR_BUS_NO_CONTEXT, { message });
}
}
export class AappInvalidEventRuntimeError extends CodeError {
readonly type: string;
readonly allowed: string;
readonly where: string;
constructor(type: string, allowed: string, where: string) {
super(APP_ERR_EVENT_INVALID_RUNTIME, {
message: `${APP_ERROR_MESSAGES[APP_ERR_EVENT_INVALID_RUNTIME] as string} (type=${type}, allowed=${allowed}, where=${where})`
});
this.type = type;
this.allowed = allowed;
this.where = where;
}
}
export class AappUnsafeEventPayloadError extends CodeError {
readonly type: string;
readonly path: string;
constructor(type: string, path: string) {
super(APP_ERR_EVENT_UNSAFE_PAYLOAD, {
message: `${APP_ERROR_MESSAGES[APP_ERR_EVENT_UNSAFE_PAYLOAD] as string} (type=${type}, path=${path})`
});
this.type = type;
this.path = path;
}
}
// ── Type guards ────────────────────────────────────────────────────────
export function isAappBusNoContextError(error: unknown): error is AappBusNoContextError {
return error instanceof AappBusNoContextError;
}
export function isAappInvalidEventRuntimeError(
error: unknown
): error is AappInvalidEventRuntimeError {
return error instanceof AappInvalidEventRuntimeError;
}
export function isAappUnsafeEventPayloadError(
error: unknown
): error is AappUnsafeEventPayloadError {
return error instanceof AappUnsafeEventPayloadError;
}

@ -1,295 +0,0 @@
import type {
BusEnvelope,
BusPublishOptions,
BusSubscription,
EventPublisher,
EventSubscriber
} from '$libs/bus';
import {
APP_EVENT_RUNTIME_BOTH,
APP_EVENT_RUNTIME_CLIENT,
APP_EVENT_SENSITIVE_KEY_PARTS
} from './consts.ts';
import {
AappInvalidEventRuntimeError,
AappUnsafeEventPayloadError
} from './errors.ts';
export const APP_EVENT_USER_IDENTITY_CHANGED = 'app.user.identity.changed';
export const APP_EVENT_TENANT_SWITCHED = 'app.tenant.switched';
export const APP_EVENT_PERMISSIONS_REFRESH_REQUESTED =
'app.permissions.refresh.requested';
export const APP_EVENT_CONNECTIVITY_CHANGED = 'app.connectivity.changed';
export const APP_EVENT_CACHE_INVALIDATE_REQUESTED = 'app.cache.invalidate.requested';
export const APP_EVENT_DISPOSE_STARTING = 'app.dispose.starting';
/**
* Where each `APP_EVENT_*` is allowed to fire. `'both'` is the default;
* `'client'` is reserved for facts that only exist in the browser
* (connectivity is the canonical example). `assertEventCanFire` reads
* this table.
*/
export type AppEventRuntime =
| typeof APP_EVENT_RUNTIME_BOTH
| typeof APP_EVENT_RUNTIME_CLIENT;
export const APP_EVENT_RUNTIMES: Readonly<Record<string, AppEventRuntime>> = {
[APP_EVENT_USER_IDENTITY_CHANGED]: APP_EVENT_RUNTIME_BOTH,
[APP_EVENT_TENANT_SWITCHED]: APP_EVENT_RUNTIME_BOTH,
[APP_EVENT_PERMISSIONS_REFRESH_REQUESTED]: APP_EVENT_RUNTIME_BOTH,
[APP_EVENT_CONNECTIVITY_CHANGED]: APP_EVENT_RUNTIME_CLIENT,
[APP_EVENT_CACHE_INVALIDATE_REQUESTED]: APP_EVENT_RUNTIME_BOTH,
[APP_EVENT_DISPOSE_STARTING]: APP_EVENT_RUNTIME_BOTH
};
/**
* Throws `AappInvalidEventRuntimeError` if `type` is an `APP_EVENT_*`
* declared as client-only and the current runtime is the server (or
* vice-versa). Module events are not registered in `APP_EVENT_RUNTIMES`
* and are accepted unconditionally — only the public app-event surface
* is gated.
*/
export function assertEventCanFire(
type: string,
where: typeof APP_EVENT_RUNTIME_CLIENT | 'server'
): void {
const allowed = APP_EVENT_RUNTIMES[type];
if (allowed === undefined) return;
if (allowed === APP_EVENT_RUNTIME_BOTH) return;
if (allowed === where) return;
throw new AappInvalidEventRuntimeError(type, allowed, where);
}
export function assertAppEventPayloadSafe(type: string, payload: unknown): void {
const unsafePath = findSensitivePayloadPath(payload);
if (unsafePath !== undefined) throw new AappUnsafeEventPayloadError(type, unsafePath);
}
function findSensitivePayloadPath(value: unknown, path = '$'): string | undefined {
if (value === null || typeof value !== 'object') return undefined;
if (Array.isArray(value)) {
for (let index = 0; index < value.length; index += 1) {
const unsafe = findSensitivePayloadPath(value[index], `${path}[${index}]`);
if (unsafe !== undefined) return unsafe;
}
return undefined;
}
for (const [key, child] of Object.entries(value)) {
const childPath = `${path}.${key}`;
if (isSensitivePayloadKey(key)) return childPath;
const unsafe = findSensitivePayloadPath(child, childPath);
if (unsafe !== undefined) return unsafe;
}
return undefined;
}
function isSensitivePayloadKey(key: string): boolean {
const normalized = key.toLowerCase();
return APP_EVENT_SENSITIVE_KEY_PARTS.some((part) => normalized.includes(part));
}
export const APP_USER_IDENTITY_CAUSE_SESSION_ADOPTED = 'session-adopted';
export const APP_USER_IDENTITY_CAUSE_SESSION_ADOPTED_SERVER = 'session-adopted-server';
export const APP_USER_IDENTITY_CAUSE_SESSION_REFRESHED = 'session-refreshed';
export const APP_USER_IDENTITY_CAUSE_SESSION_REVOKED = 'session-revoked';
export const APP_USER_IDENTITY_CAUSE_SESSION_EXPIRED = 'session-expired';
export const APP_USER_IDENTITY_CAUSE_SESSION_EXTERNAL_CHANGED =
'session-external-changed';
export type AppUserIdentityChangeCause =
| typeof APP_USER_IDENTITY_CAUSE_SESSION_ADOPTED
| typeof APP_USER_IDENTITY_CAUSE_SESSION_ADOPTED_SERVER
| typeof APP_USER_IDENTITY_CAUSE_SESSION_REFRESHED
| typeof APP_USER_IDENTITY_CAUSE_SESSION_REVOKED
| typeof APP_USER_IDENTITY_CAUSE_SESSION_EXPIRED
| typeof APP_USER_IDENTITY_CAUSE_SESSION_EXTERNAL_CHANGED;
export interface AppUserIdentityChangedPayload {
readonly event: string;
readonly generation: number;
readonly identity: {
readonly from: string;
readonly to: string;
};
readonly cause: AppUserIdentityChangeCause;
readonly previousActorId?: string | null;
readonly nextActorId?: string | null;
readonly tenantId?: string | null;
}
export interface AppPermsRefreshRequestedPayload {
readonly cause: string;
readonly generation?: number;
}
export interface AppTenantSwitchedPayload {
readonly previousTenantId: string | null;
readonly nextTenantId: string | null;
readonly cause: string;
}
export interface AppConnectivityChangedPayload {
readonly online: boolean;
readonly cause: string;
}
export interface AppCacheInvalidateRequestedPayload {
readonly cause: string;
readonly tags?: readonly string[];
}
export interface AppDisposeStartingPayload {
readonly cause: string;
}
export interface AppEventMap {
[APP_EVENT_USER_IDENTITY_CHANGED]: AppUserIdentityChangedPayload;
[APP_EVENT_TENANT_SWITCHED]: AppTenantSwitchedPayload;
[APP_EVENT_PERMISSIONS_REFRESH_REQUESTED]: AppPermsRefreshRequestedPayload;
[APP_EVENT_CONNECTIVITY_CHANGED]: AppConnectivityChangedPayload;
[APP_EVENT_CACHE_INVALIDATE_REQUESTED]: AppCacheInvalidateRequestedPayload;
[APP_EVENT_DISPOSE_STARTING]: AppDisposeStartingPayload;
}
export type AppEventBus = EventSubscriber<AppEventMap>;
function currentRuntime(): typeof APP_EVENT_RUNTIME_CLIENT | 'server' {
return typeof window === 'undefined' ? 'server' : APP_EVENT_RUNTIME_CLIENT;
}
/**
* @deprecated The session art publishes `SESSION_EVENT_IDENTITY_CHANGED`
* directly. Subscribe to that event (or use the orca preset
* `applyCacheClearOnIdentityChange` / `applyPermInvalidateOnIdentityChange`)
* instead of republishing as `APP_EVENT_USER_IDENTITY_CHANGED`.
*/
export function publishAppUserIdentityChanged(
bus: EventPublisher<AppEventMap>,
payload: AppUserIdentityChangedPayload,
options?: BusPublishOptions
) {
assertEventCanFire(APP_EVENT_USER_IDENTITY_CHANGED, currentRuntime());
assertAppEventPayloadSafe(APP_EVENT_USER_IDENTITY_CHANGED, payload);
return bus.publish(APP_EVENT_USER_IDENTITY_CHANGED, payload, options);
}
/**
* @deprecated `PERMISSIONS_REFRESH_REQUESTED` is a command disguised as an
* event. Call `App.perm.refresh()` (or the legacy `App.Perms?.refresh()`)
* directly instead of routing through the bus.
*/
export function publishAppPermsRefreshRequested(
bus: EventPublisher<AppEventMap>,
payload: AppPermsRefreshRequestedPayload,
options?: BusPublishOptions
) {
assertEventCanFire(APP_EVENT_PERMISSIONS_REFRESH_REQUESTED, currentRuntime());
assertAppEventPayloadSafe(APP_EVENT_PERMISSIONS_REFRESH_REQUESTED, payload);
return bus.publish(APP_EVENT_PERMISSIONS_REFRESH_REQUESTED, payload, options);
}
/**
* @deprecated Tenant switching does not currently have a module owner.
* Apps that switch tenant should expose their own event or call
* `App.cache.clear()` / `App.perm.invalidate()` directly. Slated for
* removal in the orca-based migration.
*/
export function publishAppTenantSwitched(
bus: EventPublisher<AppEventMap>,
payload: AppTenantSwitchedPayload,
options?: BusPublishOptions
) {
assertEventCanFire(APP_EVENT_TENANT_SWITCHED, currentRuntime());
assertAppEventPayloadSafe(APP_EVENT_TENANT_SWITCHED, payload);
return bus.publish(APP_EVENT_TENANT_SWITCHED, payload, options);
}
/**
* @deprecated `arts/connection` should publish its own
* `CONNECTION_EVENT_*` and the application orchestrates from there.
* Slated for removal once consumers migrate.
*/
export function publishAppConnectivityChanged(
bus: EventPublisher<AppEventMap>,
payload: AppConnectivityChangedPayload,
options?: BusPublishOptions
) {
assertEventCanFire(APP_EVENT_CONNECTIVITY_CHANGED, currentRuntime());
assertAppEventPayloadSafe(APP_EVENT_CONNECTIVITY_CHANGED, payload);
return bus.publish(APP_EVENT_CONNECTIVITY_CHANGED, payload, options);
}
/**
* @deprecated `CACHE_INVALIDATE_REQUESTED` is a command disguised as an
* event. Call `App.cache.clear()` / `App.Cache.clear()` directly
* instead of routing through the bus.
*/
export function publishAppCacheInvalidateRequested(
bus: EventPublisher<AppEventMap>,
payload: AppCacheInvalidateRequestedPayload,
options?: BusPublishOptions
) {
assertEventCanFire(APP_EVENT_CACHE_INVALIDATE_REQUESTED, currentRuntime());
assertAppEventPayloadSafe(APP_EVENT_CACHE_INVALIDATE_REQUESTED, payload);
return bus.publish(APP_EVENT_CACHE_INVALIDATE_REQUESTED, payload, options);
}
export function publishAppDisposeStarting(
bus: EventPublisher<AppEventMap>,
payload: AppDisposeStartingPayload,
options?: BusPublishOptions
) {
assertEventCanFire(APP_EVENT_DISPOSE_STARTING, currentRuntime());
assertAppEventPayloadSafe(APP_EVENT_DISPOSE_STARTING, payload);
return bus.publish(APP_EVENT_DISPOSE_STARTING, payload, options);
}
export function onAppUserIdentityChanged(
bus: AppEventBus,
listener: (
event: BusEnvelope<string, AppUserIdentityChangedPayload>
) => void | Promise<void>
): BusSubscription {
return bus.on(APP_EVENT_USER_IDENTITY_CHANGED, listener);
}
export function onAppPermsRefreshRequested(
bus: AppEventBus,
listener: (
event: BusEnvelope<string, AppPermsRefreshRequestedPayload>
) => void | Promise<void>
): BusSubscription {
return bus.on(APP_EVENT_PERMISSIONS_REFRESH_REQUESTED, listener);
}
export function onAppTenantSwitched(
bus: AppEventBus,
listener: (event: BusEnvelope<string, AppTenantSwitchedPayload>) => void | Promise<void>
): BusSubscription {
return bus.on(APP_EVENT_TENANT_SWITCHED, listener);
}
export function onAppConnectivityChanged(
bus: AppEventBus,
listener: (
event: BusEnvelope<string, AppConnectivityChangedPayload>
) => void | Promise<void>
): BusSubscription {
return bus.on(APP_EVENT_CONNECTIVITY_CHANGED, listener);
}
export function onAppCacheInvalidateRequested(
bus: AppEventBus,
listener: (
event: BusEnvelope<string, AppCacheInvalidateRequestedPayload>
) => void | Promise<void>
): BusSubscription {
return bus.on(APP_EVENT_CACHE_INVALIDATE_REQUESTED, listener);
}
export function onAppDisposeStarting(
bus: AppEventBus,
listener: (event: BusEnvelope<string, AppDisposeStartingPayload>) => void | Promise<void>
): BusSubscription {
return bus.on(APP_EVENT_DISPOSE_STARTING, listener);
}

@ -1,93 +0,0 @@
import { describe, expect, it } from 'vitest';
import { createEngineBus } from '$bus';
import {
APP_EVENT_CONNECTIVITY_CHANGED,
APP_EVENT_USER_IDENTITY_CHANGED,
APP_USER_IDENTITY_CAUSE_SESSION_REVOKED,
assertEventCanFire,
onAppDisposeStarting,
onAppUserIdentityChanged,
publishAppDisposeStarting,
publishAppUserIdentityChanged,
type AppEventMap
} from '../events.ts';
import {
isAappInvalidEventRuntimeError,
isAappUnsafeEventPayloadError
} from '../errors.ts';
describe('libs/active-app/events', () => {
it('publishes and subscribes to public app events through helpers', () => {
const Bus = createEngineBus<AppEventMap>();
const identityPayloads: unknown[] = [];
const disposePayloads: unknown[] = [];
const offIdentity = onAppUserIdentityChanged(Bus, (event) => {
identityPayloads.push(event.payload);
});
const offDispose = onAppDisposeStarting(Bus, (event) => {
disposePayloads.push(event.payload);
});
publishAppUserIdentityChanged(Bus, {
event: 'REVOKED',
generation: 1,
identity: { from: 'identified', to: 'none' },
cause: APP_USER_IDENTITY_CAUSE_SESSION_REVOKED
});
publishAppDisposeStarting(Bus, { cause: 'test' });
expect(identityPayloads).toEqual([
{
event: 'REVOKED',
generation: 1,
identity: { from: 'identified', to: 'none' },
cause: APP_USER_IDENTITY_CAUSE_SESSION_REVOKED
}
]);
expect(disposePayloads).toEqual([{ cause: 'test' }]);
offIdentity.unsubscribe();
offDispose.unsubscribe();
Bus.dispose();
});
it('assertEventCanFire allows both-runtime events anywhere', () => {
expect(() => assertEventCanFire(APP_EVENT_USER_IDENTITY_CHANGED, 'server')).not.toThrow();
expect(() => assertEventCanFire(APP_EVENT_USER_IDENTITY_CHANGED, 'client')).not.toThrow();
});
it('assertEventCanFire throws when a client-only event is fired on the server', () => {
try {
assertEventCanFire(APP_EVENT_CONNECTIVITY_CHANGED, 'server');
expect.fail('expected assertion to throw');
} catch (error) {
expect(isAappInvalidEventRuntimeError(error)).toBe(true);
}
});
it('assertEventCanFire ignores unregistered (module) events', () => {
expect(() => assertEventCanFire('session.lifecycle.adopted', 'server')).not.toThrow();
});
it('publish helpers reject sensitive app-event payload keys', () => {
const Bus = createEngineBus<AppEventMap>();
try {
publishAppUserIdentityChanged(
Bus,
{
event: 'REVOKED',
generation: 1,
identity: { from: 'identified', to: 'none' },
cause: APP_USER_IDENTITY_CAUSE_SESSION_REVOKED,
credential: { token: 'secret-token' }
} as unknown as Parameters<typeof publishAppUserIdentityChanged>[1]
);
expect.fail('expected unsafe payload assertion to throw');
} catch (error) {
expect(isAappUnsafeEventPayloadError(error)).toBe(true);
} finally {
Bus.dispose();
}
});
});
Loading…
Cancel
Save

Powered by TurnKey Linux.