Closes the second half of step 2 of the active-app refactor: every
service in the schema now has a defineActiveX/defineEngineX factory,
and the orchestration that lived inside arts/cache and arts/perm as
internal bus subscriptions moves to opt-in presets registered via
App.Orca.
Service factories added (4):
- defineActiveCache — passes only logger from core. The legacy
autoInvalidateOn / bus options are still accepted for back-compat
but no longer recommended.
- defineActivePerm — same shape as cache; perm endpoint required
via options.
- defineActiveSession — wires core.bus into the session so
SESSION_EVENT_LIFECYCLE_* events flow without per-app config.
- defineActiveConnections — requires logger + timers from core.
Presets added in arts/active-app/presets/ (4):
- applyCacheClearOnIdentityChange — listens to
SESSION_EVENT_IDENTITY_CHANGED, calls cache.clear().
- applyCacheClearOnRevoke — listens to SESSION_EVENT_REVOKED,
calls cache.clear().
- applyPermInvalidateOnIdentityChange — listens to
SESSION_EVENT_IDENTITY_CHANGED, calls perm.invalidate().
- applyStandardOrca — aggregator that registers every preset whose
services are present on App. Returns a single detacher.
Each preset is a function (App: AppShape) => () => void, where
AppShape is structurally typed against the orca instance and the
specific services the preset needs. Presets cherry-pick what they
need from each service via Pick<…, 'clear' | 'invalidate'>.
Tests: 8 cases for the presets (publish event → assert imperative
API called, detacher unregisters, aggregator skips absent services,
errors recorded in run trace). Total suite: 1402 tests pass
(1394 + 8).
Pending for the big-bang merge in a separate session:
- Rewrite createActiveApp() to consume the schema and remove the
legacy App.createActiveX() factories.
- Delete wireSessionTranslator, createAuthCacheInvalidator and
APP_ORCHESTRATION_*.
- Delete APP_EVENT_USER_IDENTITY_CHANGED / TENANT_SWITCHED /
PERMISSIONS_REFRESH_REQUESTED / CACHE_INVALIDATE_REQUESTED /
CONNECTIVITY_CHANGED. Keep only DISPOSE_STARTING.
- Migrate web/routes/* and the ecosystem integration tests.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
master
parent
d528652640
commit
a04fa67152
@ -0,0 +1,41 @@
|
||||
import { ORCA_ON_ERROR_CONTINUE, ORCA_STAGE_MAIN, orcaError, orcaSuccess } from '$orca';
|
||||
import type { EngineOrca } from '$orca';
|
||||
import { SESSION_EVENT_IDENTITY_CHANGED } from '$session';
|
||||
import type { ActiveCache } from '$cache/types';
|
||||
|
||||
const ACTION_ID = 'cache.clear-on-identity-change';
|
||||
const TOKEN_CLEARED = 'cache:cleared-on-identity';
|
||||
|
||||
interface AppShape {
|
||||
readonly Orca: EngineOrca;
|
||||
readonly cache: Pick<ActiveCache, 'clear'>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers an orca action that clears the active cache when the
|
||||
* session's actor identity changes.
|
||||
*
|
||||
* Replaces the legacy `wireAutoInvalidation` subscription inside
|
||||
* `arts/cache`. Listens to `SESSION_EVENT_IDENTITY_CHANGED` (the
|
||||
* canonical event from `arts/session`), not the deprecated
|
||||
* `APP_EVENT_USER_IDENTITY_CHANGED` re-publication.
|
||||
*
|
||||
* Returns a detach function. Calling it unregisters the action; the
|
||||
* engine then stops reacting to the event.
|
||||
*/
|
||||
export function applyCacheClearOnIdentityChange(App: AppShape): () => void {
|
||||
return App.Orca.onEvent(SESSION_EVENT_IDENTITY_CHANGED, {
|
||||
id: ACTION_ID,
|
||||
stage: ORCA_STAGE_MAIN,
|
||||
provides: [TOKEN_CLEARED],
|
||||
onError: ORCA_ON_ERROR_CONTINUE,
|
||||
action: async () => {
|
||||
try {
|
||||
await App.cache.clear();
|
||||
return orcaSuccess({ emits: [TOKEN_CLEARED] });
|
||||
} catch (error) {
|
||||
return orcaError(error);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
@ -0,0 +1,35 @@
|
||||
import { ORCA_ON_ERROR_CONTINUE, ORCA_STAGE_MAIN, orcaError, orcaSuccess } from '$orca';
|
||||
import type { EngineOrca } from '$orca';
|
||||
import { SESSION_EVENT_REVOKED } from '$session';
|
||||
import type { ActiveCache } from '$cache/types';
|
||||
|
||||
const ACTION_ID = 'cache.clear-on-revoke';
|
||||
const TOKEN_CLEARED = 'cache:cleared-on-revoke';
|
||||
|
||||
interface AppShape {
|
||||
readonly Orca: EngineOrca;
|
||||
readonly cache: Pick<ActiveCache, 'clear'>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers an orca action that clears the active cache when the
|
||||
* session is revoked. Pairs with `applyCacheClearOnIdentityChange` for
|
||||
* apps where revoke is independent of identity change (e.g. logout
|
||||
* without login of another actor).
|
||||
*/
|
||||
export function applyCacheClearOnRevoke(App: AppShape): () => void {
|
||||
return App.Orca.onEvent(SESSION_EVENT_REVOKED, {
|
||||
id: ACTION_ID,
|
||||
stage: ORCA_STAGE_MAIN,
|
||||
provides: [TOKEN_CLEARED],
|
||||
onError: ORCA_ON_ERROR_CONTINUE,
|
||||
action: async () => {
|
||||
try {
|
||||
await App.cache.clear();
|
||||
return orcaSuccess({ emits: [TOKEN_CLEARED] });
|
||||
} catch (error) {
|
||||
return orcaError(error);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
@ -0,0 +1,22 @@
|
||||
/**
|
||||
* Orchestration presets for `arts/active-app`. Each `apply*` function
|
||||
* registers one or more orca actions on `App.Orca` that react to
|
||||
* canonical lifecycle events (`SESSION_EVENT_*`, future
|
||||
* `CONNECTION_EVENT_*`, etc.) and call the imperative API of the
|
||||
* affected service.
|
||||
*
|
||||
* **Presets live here, not inside the arts.** An art (`arts/cache`,
|
||||
* `arts/perm`, etc.) does not know about `arts/session` or
|
||||
* `arts/orca` — that knowledge belongs to the composition layer
|
||||
* (`arts/active-app`). Putting the presets here keeps the inter-art
|
||||
* dependency inversion clean.
|
||||
*
|
||||
* Each preset is opt-in: applications register only the orchestration
|
||||
* they need. `applyStandardOrca(App)` is a convenience aggregator that
|
||||
* registers every preset whose required services are declared in `App`.
|
||||
*/
|
||||
|
||||
export { applyCacheClearOnIdentityChange } from './cache-clear-on-identity-change.ts';
|
||||
export { applyCacheClearOnRevoke } from './cache-clear-on-revoke.ts';
|
||||
export { applyPermInvalidateOnIdentityChange } from './perm-invalidate-on-identity-change.ts';
|
||||
export { applyStandardOrca } from './standard.ts';
|
||||
@ -0,0 +1,36 @@
|
||||
import { ORCA_ON_ERROR_CONTINUE, ORCA_STAGE_MAIN, orcaError, orcaSuccess } from '$orca';
|
||||
import type { EngineOrca } from '$orca';
|
||||
import { SESSION_EVENT_IDENTITY_CHANGED } from '$session';
|
||||
import type { ActivePerms } from '$perm/types';
|
||||
|
||||
const ACTION_ID = 'perm.invalidate-on-identity-change';
|
||||
const TOKEN_INVALIDATED = 'perm:invalidated-on-identity';
|
||||
|
||||
interface AppShape {
|
||||
readonly Orca: EngineOrca;
|
||||
readonly perm: Pick<ActivePerms, 'invalidate'>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers an orca action that invalidates the local permissions cache
|
||||
* when the session's actor identity changes.
|
||||
*
|
||||
* Replaces the legacy `wireAutoInvalidation` subscription inside
|
||||
* `arts/perm`. Listens to `SESSION_EVENT_IDENTITY_CHANGED` directly.
|
||||
*/
|
||||
export function applyPermInvalidateOnIdentityChange(App: AppShape): () => void {
|
||||
return App.Orca.onEvent(SESSION_EVENT_IDENTITY_CHANGED, {
|
||||
id: ACTION_ID,
|
||||
stage: ORCA_STAGE_MAIN,
|
||||
provides: [TOKEN_INVALIDATED],
|
||||
onError: ORCA_ON_ERROR_CONTINUE,
|
||||
action: () => {
|
||||
try {
|
||||
App.perm.invalidate();
|
||||
return orcaSuccess({ emits: [TOKEN_INVALIDATED] });
|
||||
} catch (error) {
|
||||
return orcaError(error);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
@ -0,0 +1,36 @@
|
||||
import type { EngineOrca } from '$orca';
|
||||
import type { ActiveCache } from '$cache/types';
|
||||
import type { ActivePerms } from '$perm/types';
|
||||
import { applyCacheClearOnIdentityChange } from './cache-clear-on-identity-change.ts';
|
||||
import { applyCacheClearOnRevoke } from './cache-clear-on-revoke.ts';
|
||||
import { applyPermInvalidateOnIdentityChange } from './perm-invalidate-on-identity-change.ts';
|
||||
|
||||
interface AppShape {
|
||||
readonly Orca: EngineOrca;
|
||||
readonly cache?: Pick<ActiveCache, 'clear'>;
|
||||
readonly perm?: Pick<ActivePerms, 'invalidate'>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convenience aggregator: registers every standard preset whose required
|
||||
* services are declared on `App`. Apps that want a tailored set of
|
||||
* reactions can cherry-pick individual `apply*` functions instead.
|
||||
*
|
||||
* Returns a single detach function that unregisters everything in
|
||||
* reverse order — convenient for tests and hot reloading.
|
||||
*/
|
||||
export function applyStandardOrca(App: AppShape): () => void {
|
||||
const detachers: Array<() => void> = [];
|
||||
|
||||
if (App.cache !== undefined) {
|
||||
detachers.push(applyCacheClearOnIdentityChange(App as AppShape & { cache: NonNullable<AppShape['cache']> }));
|
||||
detachers.push(applyCacheClearOnRevoke(App as AppShape & { cache: NonNullable<AppShape['cache']> }));
|
||||
}
|
||||
if (App.perm !== undefined) {
|
||||
detachers.push(applyPermInvalidateOnIdentityChange(App as AppShape & { perm: NonNullable<AppShape['perm']> }));
|
||||
}
|
||||
|
||||
return () => {
|
||||
for (let i = detachers.length - 1; i >= 0; i--) detachers[i]();
|
||||
};
|
||||
}
|
||||
@ -0,0 +1,37 @@
|
||||
import { createActiveCache } from '$cache/active-cache.svelte';
|
||||
import type { ActiveCache, ActiveCacheOptions } from '$cache/types';
|
||||
import type { AppServiceFactory } from '../services.ts';
|
||||
|
||||
/**
|
||||
* `defineActiveCache(options)` produces a service factory for the `cache`
|
||||
* slot.
|
||||
*
|
||||
* **Auto-invalidation is OFF by default** when registered via the schema.
|
||||
* The legacy `autoInvalidateOn` and `bus` options on `ActiveCacheOptions`
|
||||
* are still honored if the application explicitly passes them, but the
|
||||
* recommended path is to omit them and use an orca preset
|
||||
* (`applyCacheInvalidateOnIdentityChange` etc.) to react to events.
|
||||
*
|
||||
* The art still has `bus.on(APP_EVENT_*)` subscriptions internally
|
||||
* gated by `autoInvalidateOn`. Step 3 of the active-app refactor will
|
||||
* remove those entirely; until then, leaving the option unset keeps
|
||||
* the behavior clean.
|
||||
*/
|
||||
export function defineActiveCache(
|
||||
options: Omit<ActiveCacheOptions, 'logger'> = {}
|
||||
): AppServiceFactory<'cache', readonly ['logger'], readonly [], ActiveCache> {
|
||||
return {
|
||||
name: 'cache',
|
||||
coreDependencies: ['logger'],
|
||||
initMode: 'lazy',
|
||||
create({ core }): ActiveCache {
|
||||
return createActiveCache({
|
||||
...options,
|
||||
logger: core.logger
|
||||
});
|
||||
},
|
||||
dispose(instance) {
|
||||
instance.dispose();
|
||||
}
|
||||
};
|
||||
}
|
||||
@ -0,0 +1,36 @@
|
||||
import { createActiveConnections } from '$connection/active-connections.svelte';
|
||||
import type { ActiveConnections, ActiveConnectionsOptions } from '$connection/types';
|
||||
import type { AppServiceFactory } from '../services.ts';
|
||||
|
||||
/**
|
||||
* `defineActiveConnections(options)` produces a service factory for the
|
||||
* `connections` slot.
|
||||
*
|
||||
* Requires `timers` from the core. Identity tracking (formerly via
|
||||
* `bus-session-source`) becomes an orca preset in step 3 — until then
|
||||
* applications can pass `session` in options manually.
|
||||
*/
|
||||
export function defineActiveConnections(
|
||||
options: Omit<ActiveConnectionsOptions, 'logger' | 'timers'> = {}
|
||||
): AppServiceFactory<
|
||||
'connections',
|
||||
readonly ['logger', 'timers'],
|
||||
readonly [],
|
||||
ActiveConnections
|
||||
> {
|
||||
return {
|
||||
name: 'connections',
|
||||
coreDependencies: ['logger', 'timers'],
|
||||
initMode: 'lazy',
|
||||
create({ core }): ActiveConnections {
|
||||
return createActiveConnections({
|
||||
...options,
|
||||
logger: core.logger,
|
||||
timers: core.timers
|
||||
});
|
||||
},
|
||||
dispose(instance) {
|
||||
instance.dispose();
|
||||
}
|
||||
};
|
||||
}
|
||||
@ -0,0 +1,34 @@
|
||||
import { createActivePerms } from '$perm/active-permissions.svelte';
|
||||
import type { ActivePerms, ActivePermsOptions } from '$perm/types';
|
||||
import type { AppServiceFactory } from '../services.ts';
|
||||
|
||||
/**
|
||||
* `defineActivePerm(options)` produces a service factory for the `perm`
|
||||
* slot.
|
||||
*
|
||||
* **Auto-invalidation is OFF by default** when registered via the schema
|
||||
* — same reasoning as `defineActiveCache`. Use orca presets
|
||||
* (`applyPermInvalidateOnIdentityChange`) to react to events.
|
||||
*
|
||||
* The factory still requires the application to provide `endpoint` (via
|
||||
* the underlying `ActivePermsOptions`); the perm client cannot work
|
||||
* without a backend.
|
||||
*/
|
||||
export function defineActivePerm(
|
||||
options: Omit<ActivePermsOptions, 'logger'>
|
||||
): AppServiceFactory<'perm', readonly ['logger'], readonly [], ActivePerms> {
|
||||
return {
|
||||
name: 'perm',
|
||||
coreDependencies: ['logger'],
|
||||
initMode: 'lazy',
|
||||
create({ core }): ActivePerms {
|
||||
return createActivePerms({
|
||||
...options,
|
||||
logger: core.logger
|
||||
});
|
||||
},
|
||||
dispose(instance) {
|
||||
instance.dispose();
|
||||
}
|
||||
};
|
||||
}
|
||||
@ -0,0 +1,45 @@
|
||||
import { createActiveSession } from '$session/active-session.svelte';
|
||||
import type { ActiveSession, EngineSessionOptions } from '$session/types';
|
||||
import type { AppServiceFactory } from '../services.ts';
|
||||
|
||||
/**
|
||||
* `defineActiveSession<TUser, TCredential?, TData?>(options)` produces a
|
||||
* service factory for the `session` slot.
|
||||
*
|
||||
* The session art publishes its own `SESSION_EVENT_LIFECYCLE_*` events
|
||||
* via `options.bus`. The factory wires `core.bus` into the session
|
||||
* automatically so consumers (and orca presets) can subscribe to those
|
||||
* events without extra configuration.
|
||||
*
|
||||
* Application-level `APP_EVENT_USER_IDENTITY_CHANGED` re-publishing
|
||||
* (formerly handled by `wireSessionTranslator`) moves to an orca preset
|
||||
* in step 3.
|
||||
*/
|
||||
export function defineActiveSession<
|
||||
TUser,
|
||||
TCredential = undefined,
|
||||
TData = undefined
|
||||
>(
|
||||
options: Omit<EngineSessionOptions<TUser, TCredential, TData>, 'logger' | 'bus'>
|
||||
): AppServiceFactory<
|
||||
'session',
|
||||
readonly ['logger', 'bus'],
|
||||
readonly [],
|
||||
ActiveSession<TUser, TCredential, TData>
|
||||
> {
|
||||
return {
|
||||
name: 'session',
|
||||
coreDependencies: ['logger', 'bus'],
|
||||
initMode: 'lazy',
|
||||
create({ core }): ActiveSession<TUser, TCredential, TData> {
|
||||
return createActiveSession<TUser, TCredential, TData>({
|
||||
...(options as EngineSessionOptions<TUser, TCredential, TData>),
|
||||
logger: core.logger,
|
||||
bus: core.bus
|
||||
});
|
||||
},
|
||||
dispose(instance) {
|
||||
instance.dispose();
|
||||
}
|
||||
};
|
||||
}
|
||||
@ -0,0 +1,224 @@
|
||||
/**
|
||||
* End-to-end tests for the orca-based presets in
|
||||
* `arts/active-app/presets/`. Validate that:
|
||||
* - Publishing a SESSION_EVENT_* on the bus triggers the registered
|
||||
* orca action.
|
||||
* - The action calls the imperative API on the relevant service.
|
||||
* - Detachers actually unregister the action.
|
||||
* - The aggregator only registers presets whose services are present.
|
||||
*/
|
||||
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import {
|
||||
applyCacheClearOnIdentityChange,
|
||||
applyCacheClearOnRevoke,
|
||||
applyPermInvalidateOnIdentityChange,
|
||||
applyStandardOrca
|
||||
} from '../presets/index.ts';
|
||||
import { createSvelteEngineBus } from '$bus';
|
||||
import { createEngineLogger } from '$logger/engine-logger';
|
||||
import { createEngineOrca, type EngineOrca } from '$orca';
|
||||
import { createActiveTimers } from '$timer/active-timers.svelte';
|
||||
import {
|
||||
SESSION_EVENT_IDENTITY_CHANGED,
|
||||
SESSION_EVENT_LIFECYCLE_REVOKED,
|
||||
SESSION_EVENT_REVOKED
|
||||
} from '$session';
|
||||
import type { EngineLogger } from '$logger';
|
||||
import type { EngineBus } from '$bus';
|
||||
import type { ActiveTimers } from '$timer';
|
||||
import type { ActiveCache } from '$cache/types';
|
||||
import type { ActivePerms } from '$perm/types';
|
||||
|
||||
interface CoreState {
|
||||
logger: EngineLogger;
|
||||
bus: EngineBus;
|
||||
timers: ActiveTimers;
|
||||
orca: EngineOrca;
|
||||
}
|
||||
|
||||
function buildCore(): CoreState {
|
||||
const logger = createEngineLogger({});
|
||||
const timers = createActiveTimers({ logger });
|
||||
const bus = createSvelteEngineBus({ logger, clock: timers.clock });
|
||||
const orca = createEngineOrca({ bus, timers, logger });
|
||||
return { logger, bus, timers, orca };
|
||||
}
|
||||
|
||||
function disposeCore(core: CoreState): void {
|
||||
core.orca.dispose();
|
||||
core.bus.dispose();
|
||||
core.timers.dispose();
|
||||
core.logger.dispose();
|
||||
}
|
||||
|
||||
const samplePayload = {
|
||||
event: 'session.lifecycle.adopted',
|
||||
generation: 1,
|
||||
identity: { from: 'anon', to: 'user-42' },
|
||||
previousActorId: null,
|
||||
nextActorId: 'user-42'
|
||||
};
|
||||
|
||||
async function flush(rounds = 4) {
|
||||
for (let i = 0; i < rounds; i++) {
|
||||
await new Promise((r) => queueMicrotask(() => r(undefined)));
|
||||
await new Promise((r) => setTimeout(r, 0));
|
||||
}
|
||||
}
|
||||
|
||||
describe('applyCacheClearOnIdentityChange', () => {
|
||||
let core: CoreState;
|
||||
|
||||
beforeEach(() => {
|
||||
core = buildCore();
|
||||
});
|
||||
afterEach(() => {
|
||||
disposeCore(core);
|
||||
});
|
||||
|
||||
it('clears the cache when SESSION_EVENT_IDENTITY_CHANGED is published', async () => {
|
||||
const clear = vi.fn(() => Promise.resolve());
|
||||
const cache = { clear } as unknown as ActiveCache;
|
||||
|
||||
applyCacheClearOnIdentityChange({ Orca: core.orca, cache });
|
||||
|
||||
core.bus.publish(SESSION_EVENT_IDENTITY_CHANGED, samplePayload);
|
||||
await flush();
|
||||
|
||||
expect(clear).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('detacher unregisters the action', async () => {
|
||||
const clear = vi.fn(() => Promise.resolve());
|
||||
const cache = { clear } as unknown as ActiveCache;
|
||||
|
||||
const detach = applyCacheClearOnIdentityChange({ Orca: core.orca, cache });
|
||||
detach();
|
||||
|
||||
core.bus.publish(SESSION_EVENT_IDENTITY_CHANGED, samplePayload);
|
||||
await flush();
|
||||
|
||||
expect(clear).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('records action failure in run trace when clear() rejects', async () => {
|
||||
const error = new Error('clear failed');
|
||||
const cache = { clear: vi.fn(() => Promise.reject(error)) } as unknown as ActiveCache;
|
||||
|
||||
applyCacheClearOnIdentityChange({ Orca: core.orca, cache });
|
||||
|
||||
core.bus.publish(SESSION_EVENT_IDENTITY_CHANGED, samplePayload);
|
||||
await flush();
|
||||
|
||||
const runs = core.orca.recentRuns();
|
||||
expect(runs).toHaveLength(1);
|
||||
expect(runs[0].actions[0].status).toBe('error');
|
||||
expect(runs[0].actions[0].error).toBe(error);
|
||||
});
|
||||
});
|
||||
|
||||
describe('applyPermInvalidateOnIdentityChange', () => {
|
||||
let core: CoreState;
|
||||
|
||||
beforeEach(() => {
|
||||
core = buildCore();
|
||||
});
|
||||
afterEach(() => {
|
||||
disposeCore(core);
|
||||
});
|
||||
|
||||
it('invalidates perms when SESSION_EVENT_IDENTITY_CHANGED is published', async () => {
|
||||
const invalidate = vi.fn();
|
||||
const perm = { invalidate } as unknown as ActivePerms;
|
||||
|
||||
applyPermInvalidateOnIdentityChange({ Orca: core.orca, perm });
|
||||
|
||||
core.bus.publish(SESSION_EVENT_IDENTITY_CHANGED, samplePayload);
|
||||
await flush();
|
||||
|
||||
expect(invalidate).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('applyCacheClearOnRevoke', () => {
|
||||
let core: CoreState;
|
||||
|
||||
beforeEach(() => {
|
||||
core = buildCore();
|
||||
});
|
||||
afterEach(() => {
|
||||
disposeCore(core);
|
||||
});
|
||||
|
||||
it('clears the cache when SESSION_EVENT_REVOKED is published', async () => {
|
||||
const clear = vi.fn(() => Promise.resolve());
|
||||
const cache = { clear } as unknown as ActiveCache;
|
||||
|
||||
applyCacheClearOnRevoke({ Orca: core.orca, cache });
|
||||
|
||||
const revokePayload = {
|
||||
...samplePayload,
|
||||
event: SESSION_EVENT_LIFECYCLE_REVOKED
|
||||
};
|
||||
core.bus.publish(SESSION_EVENT_REVOKED, revokePayload);
|
||||
await flush();
|
||||
|
||||
expect(clear).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('applyStandardOrca', () => {
|
||||
let core: CoreState;
|
||||
|
||||
beforeEach(() => {
|
||||
core = buildCore();
|
||||
});
|
||||
afterEach(() => {
|
||||
disposeCore(core);
|
||||
});
|
||||
|
||||
it('registers cache + perm reactions when both services are present', async () => {
|
||||
const cacheClear = vi.fn(() => Promise.resolve());
|
||||
const permInvalidate = vi.fn();
|
||||
const cache = { clear: cacheClear } as unknown as ActiveCache;
|
||||
const perm = { invalidate: permInvalidate } as unknown as ActivePerms;
|
||||
|
||||
applyStandardOrca({ Orca: core.orca, cache, perm });
|
||||
|
||||
core.bus.publish(SESSION_EVENT_IDENTITY_CHANGED, samplePayload);
|
||||
await flush();
|
||||
|
||||
expect(cacheClear).toHaveBeenCalledTimes(1);
|
||||
expect(permInvalidate).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('skips cache reactions when cache is absent', async () => {
|
||||
const permInvalidate = vi.fn();
|
||||
const perm = { invalidate: permInvalidate } as unknown as ActivePerms;
|
||||
|
||||
applyStandardOrca({ Orca: core.orca, perm });
|
||||
|
||||
// No cache action registered -> no error from publish
|
||||
core.bus.publish(SESSION_EVENT_IDENTITY_CHANGED, samplePayload);
|
||||
await flush();
|
||||
|
||||
expect(permInvalidate).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('detach removes every registered preset', async () => {
|
||||
const cacheClear = vi.fn(() => Promise.resolve());
|
||||
const permInvalidate = vi.fn();
|
||||
const cache = { clear: cacheClear } as unknown as ActiveCache;
|
||||
const perm = { invalidate: permInvalidate } as unknown as ActivePerms;
|
||||
|
||||
const detach = applyStandardOrca({ Orca: core.orca, cache, perm });
|
||||
detach();
|
||||
|
||||
core.bus.publish(SESSION_EVENT_IDENTITY_CHANGED, samplePayload);
|
||||
await flush();
|
||||
|
||||
expect(cacheClear).not.toHaveBeenCalled();
|
||||
expect(permInvalidate).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
Loading…
Reference in new issue