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.
337 lines
14 KiB
337 lines
14 KiB
<script lang="ts">
|
|
import CodeBlock from '../../_components/CodeBlock.svelte';
|
|
import Callout from '../../_components/Callout.svelte';
|
|
import AiAgentsBox from '../../_components/AiAgentsBox.svelte';
|
|
import ModuleHeader from '../../_components/ModuleHeader.svelte';
|
|
import PageNav from '../../_components/PageNav.svelte';
|
|
|
|
const composition = `import { createActiveApp } from '$active-app';
|
|
import {
|
|
defineActiveLangs,
|
|
defineActiveFormat,
|
|
defineActiveDom,
|
|
defineActiveCache,
|
|
defineActiveSession,
|
|
defineActivePerm,
|
|
defineActiveConnections
|
|
} from '$active-app/services';
|
|
import { createActivePrefsDomProjection } from '$prefs';
|
|
import { applyStandardOrca } from '$active-app/presets';
|
|
import { LogLevel, consoleTransport } from '$logger';
|
|
|
|
export const App = createActiveApp({
|
|
logger: { level: LogLevel.INFO, transports: [consoleTransport()] },
|
|
orca: { maxDepth: 24 },
|
|
prefs: { capabilities, environment, intent },
|
|
services: {
|
|
dom: defineActiveDom(),
|
|
langs: defineActiveLangs({ schema, defaultLocale: 'es', fallbackChain: ['en'] }),
|
|
format: defineActiveFormat(),
|
|
cache: defineActiveCache(),
|
|
session: defineActiveSession({ schemas, storage, onRefresh, onRevoke }),
|
|
perm: defineActivePerm({ endpoint: '/api/perm' }),
|
|
connections: defineActiveConnections()
|
|
}
|
|
});
|
|
|
|
export const prefsProjection = createActivePrefsDomProjection({
|
|
prefs: App.prefs,
|
|
dom: App.dom
|
|
});
|
|
|
|
applyStandardOrca(App);`;
|
|
|
|
const access = `App.logger.info('checkout.paid', { orderId });
|
|
App.prefs.locale.set('es-MX');
|
|
App.langs.t('common.ok');
|
|
App.format.currency.format(99.5);
|
|
App.cache.clear();`;
|
|
|
|
const factories = `import {
|
|
defineActiveLangs,
|
|
defineActiveFormat,
|
|
defineActiveDom,
|
|
defineActiveStorage,
|
|
defineEngineHttp,
|
|
defineEngineSium,
|
|
defineActiveCache,
|
|
defineActiveSession,
|
|
defineActiveAuth,
|
|
defineActivePerm,
|
|
defineActiveConnections
|
|
} from '$active-app/services';
|
|
|
|
const App = createActiveApp({
|
|
prefs: { capabilities, environment },
|
|
services: {
|
|
storage: defineActiveStorage(),
|
|
http: defineEngineHttp({ baseUrl: '/api' }),
|
|
dom: defineActiveDom(),
|
|
langs: defineActiveLangs({ schema }),
|
|
format: defineActiveFormat(),
|
|
sium: defineEngineSium({}),
|
|
cache: defineActiveCache(),
|
|
session: defineActiveSession({ schemas, storage, onRefresh, onRevoke }),
|
|
auth: defineActiveAuth({ endpoint: '/api/auth' }),
|
|
perm: defineActivePerm({ endpoint: '/api/perm' }),
|
|
connections: defineActiveConnections()
|
|
}
|
|
});`;
|
|
|
|
const eventFlow = `App.session publishes SESSION_EVENT_IDENTITY_CHANGED on App.bus
|
|
App.orca receives the event through registered presets
|
|
-> applyCacheClearOnIdentityChange calls App.cache.clear()
|
|
-> applyPermInvalidateOnIdentityChange calls App.perm.invalidate()
|
|
-> applyConnectionsReauthOnIdentityChange calls App.connections.reauthenticateAll()
|
|
|
|
No preset registered, no destructive reaction runs.`;
|
|
|
|
const disposal = `import { onDestroy } from 'svelte';
|
|
|
|
onDestroy(() => App.dispose());`;
|
|
|
|
const commonMistakes = [
|
|
{
|
|
name: 'Treating App as a domain service',
|
|
why: 'The composition root becomes a mixed business object and module ownership disappears.',
|
|
fix: 'Keep business behavior inside the artifact that owns it; App only wires runtime pieces.'
|
|
},
|
|
{
|
|
name: 'Expecting undeclared services to exist',
|
|
why: 'Only core members are always present. Services are exposed only when declared in the schema.',
|
|
fix: 'Declare each required module under services and let TypeScript enforce the App shape.'
|
|
},
|
|
{
|
|
name: 'Bypassing prefs for user intent',
|
|
why: 'Lang, Format and DOM projection can disagree about language, locale or direction.',
|
|
fix: 'Use App.prefs.<dim>.set(value) (e.g. App.prefs.locale.set("es-ES")); downstream services follow the effective snapshot.'
|
|
},
|
|
{
|
|
name: 'Putting reactions inside Bus listeners by hand',
|
|
why: 'Lifecycle behavior becomes invisible and hard to test.',
|
|
fix: 'Register orca presets in $active-app/presets or add explicit App.orca actions.'
|
|
},
|
|
{
|
|
name: 'Importing $active-app as server authority',
|
|
why: 'App is client/runtime composition; server trust belongs to engines and $svrs.',
|
|
fix: 'Use $svrs/auth, $svrs/perm, $svrs/cache and pure Engine factories from server files.'
|
|
}
|
|
] as const;
|
|
|
|
const aiAgentRows = [
|
|
{
|
|
step: 'Composition surface',
|
|
where: 'src/arts/active-app/types.ts and src/arts/active-app/active-app.svelte.ts',
|
|
rule: 'Update root options, core getters and service-schema typing together.'
|
|
},
|
|
{
|
|
step: 'Fixed core',
|
|
where: 'createActiveApp()',
|
|
rule: 'Logger, Bus, Timers, Orca and Prefs are always present and never declared as services.'
|
|
},
|
|
{
|
|
step: 'Service factories',
|
|
where: 'src/arts/active-app/service-factories/*.ts',
|
|
rule: 'Each factory adapts one artifact to App; artifacts must not import App.'
|
|
},
|
|
{
|
|
step: 'Orchestration',
|
|
where: 'src/arts/active-app/presets/*.ts',
|
|
rule: 'Cross-module reactions belong to Orca presets, not hidden auto-subscribers inside Cache, Perm or Connections.'
|
|
},
|
|
{
|
|
step: 'Server boundary',
|
|
where: 'src/svrs/*',
|
|
rule: '$active-app is the client composition root. Server authority belongs in $svrs and shared contracts belong in $libs.'
|
|
},
|
|
{
|
|
step: 'Tests',
|
|
where: 'src/arts/active-app/test',
|
|
rule: 'Composition, service ordering, prefs consumer wiring and orca presets need integration coverage.'
|
|
}
|
|
] as const;
|
|
</script>
|
|
|
|
<svelte:head>
|
|
<title>App ($active-app) - Active</title>
|
|
</svelte:head>
|
|
|
|
<article class="article">
|
|
<ModuleHeader
|
|
section="Composition"
|
|
title="App"
|
|
alias="$active-app"
|
|
summary="Client composition root: fixed Logger, Bus, Timers, Orca and Prefs core plus a typed opt-in service schema."
|
|
factories={['createActiveApp']}
|
|
dependsOn={['$logger', '$bus', '$timer', '$orca', '$prefs', '$active-app/services']}
|
|
layer="ActiveApp"
|
|
/>
|
|
|
|
<h2>Overview</h2>
|
|
<p>
|
|
<code>$active-app</code> is the runtime composition root. It always builds five core
|
|
members - <code>App.logger</code>, <code>App.bus</code>, <code>App.timers</code> and
|
|
<code>App.orca</code>, plus <code>App.prefs</code> - and then exposes only the services declared by the application.
|
|
The old model where Lang, Format, Dom, Storage, Http, Cache and Prefs were
|
|
always-present roots is gone.
|
|
</p>
|
|
|
|
<h2>Mental model</h2>
|
|
<p>
|
|
App is wiring, not business logic. It creates the core, adapts artifacts through service
|
|
factories, computes the service dependency order, exposes typed getters, and owns teardown.
|
|
Cross-module behavior is explicit: modules publish events on <code>App.bus</code>, while
|
|
<code>App.orca</code> runs the registered reactions.
|
|
</p>
|
|
|
|
<Callout variant="info" title="The schema is the App contract">
|
|
<p>
|
|
If a service is not declared in <code>services</code>, it is not part of the App type.
|
|
This keeps feature surfaces honest: a checkout app can declare cache and session, while
|
|
a static marketing page can keep only the fixed core.
|
|
</p>
|
|
</Callout>
|
|
|
|
<h2>Quick start</h2>
|
|
<CodeBlock code={composition} lang="ts" title="src/lib/app.ts" />
|
|
<p>Then read or mutate declared services directly:</p>
|
|
<CodeBlock code={access} lang="ts" />
|
|
|
|
<h2>Fixed core</h2>
|
|
<table>
|
|
<thead>
|
|
<tr>
|
|
<th>Member</th>
|
|
<th>Configured from</th>
|
|
<th>Notes</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<tr><td><code>App.logger</code></td><td><code>logger</code></td><td>Engine logger. Injected into core-aware service factories.</td></tr>
|
|
<tr><td><code>App.bus</code></td><td><code>bus</code></td><td>Typed Svelte-safe event bus. App injects logger and clock.</td></tr>
|
|
<tr><td><code>App.timers</code></td><td><code>timers</code></td><td>Active timer scheduler. Used by services and Orca.</td></tr>
|
|
<tr><td><code>App.orca</code></td><td><code>orca</code></td><td>Orchestration engine. App injects bus, timers and logger.</td></tr>
|
|
<tr><td><code>App.prefs</code></td><td><code>prefs</code></td><td>Preference engine. Always present; configured from root <code>prefs</code> options or neutral defaults.</td></tr>
|
|
</tbody>
|
|
</table>
|
|
|
|
<h2>Service schema</h2>
|
|
<p>
|
|
Services are adapted by <code>$active-app/services</code>. Factories declare the core
|
|
dependencies they consume, the services they can read, and whether they build lazily or
|
|
immediately. The builder validates names, detects cycles, builds dependencies first and
|
|
disposes constructed services in reverse construction order.
|
|
</p>
|
|
<CodeBlock code={factories} lang="ts" />
|
|
|
|
<table>
|
|
<thead>
|
|
<tr>
|
|
<th>Slot</th>
|
|
<th>Factory</th>
|
|
<th>Dependency behavior</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<tr><td><code>lang</code></td><td><code>defineActiveLangs()</code></td><td>Consumes <code>logger</code> and <code>prefs</code> from the core; follows <code>App.prefs.language.get()</code>.</td></tr>
|
|
<tr><td><code>format</code></td><td><code>defineActiveFormat()</code></td><td>Consumes <code>timers</code> and <code>prefs</code> from the core; resolves locale from explicit source or prefs.</td></tr>
|
|
<tr><td><code>dom</code></td><td><code>defineActiveDom()</code></td><td>DOM integration, isolated as a service.</td></tr>
|
|
<tr><td><code>storage</code></td><td><code>defineActiveStorage()</code></td><td>Storage runtime, typically used by session and prefs persistence bridges.</td></tr>
|
|
<tr><td><code>http</code></td><td><code>defineEngineHttp()</code></td><td>Pure HTTP engine adapted into the schema.</td></tr>
|
|
<tr><td><code>sium</code></td><td><code>defineEngineSium()</code></td><td>Validation engine; uses lang when present.</td></tr>
|
|
<tr><td><code>cache</code></td><td><code>defineActiveCache()</code></td><td>Cache runtime. Identity clears are Orca presets.</td></tr>
|
|
<tr><td><code>session</code></td><td><code>defineActiveSession()</code></td><td>Publishes typed session lifecycle events on App.bus.</td></tr>
|
|
<tr><td><code>auth</code></td><td><code>defineActiveAuth()</code></td><td>Client auth reflector for server-backed flows.</td></tr>
|
|
<tr><td><code>perm</code></td><td><code>defineActivePerm()</code></td><td>Permission reflector. Identity invalidation is an Orca preset.</td></tr>
|
|
<tr><td><code>connections</code></td><td><code>defineActiveConnections()</code></td><td>Realtime connection registry. Identity reauth and revoke close are Orca presets.</td></tr>
|
|
</tbody>
|
|
</table>
|
|
|
|
<h2>Prefs propagation</h2>
|
|
<p>
|
|
<code>App.prefs</code> is always present and is the core source for user intent.
|
|
<code>lang</code> follows <code>App.prefs.language.get()</code>,
|
|
<code>format</code> follows <code>App.prefs.locale.get()</code>, and
|
|
shared prefs such as direction, motion, sound and haptic can be projected explicitly
|
|
through <code>createActivePrefsDomProjection()</code>.
|
|
</p>
|
|
|
|
<h2>Bus and Orca</h2>
|
|
<p>
|
|
<code>App.bus</code> is the event transport. <code>App.orca</code> is the policy runner.
|
|
This separation matters: a module can publish a lifecycle event without silently clearing
|
|
cache, invalidating permissions or reconnecting sockets. Those reactions exist only when
|
|
the application registers presets from <code>$active-app/presets</code>.
|
|
</p>
|
|
<CodeBlock code={eventFlow} lang="txt" title="Identity event flow" />
|
|
|
|
<table>
|
|
<thead>
|
|
<tr>
|
|
<th>Preset</th>
|
|
<th>Event</th>
|
|
<th>Effect</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<tr><td><code>applyCacheClearOnIdentityChange</code></td><td><code>SESSION_EVENT_IDENTITY_CHANGED</code></td><td><code>App.cache.clear()</code></td></tr>
|
|
<tr><td><code>applyCacheClearOnRevoke</code></td><td><code>SESSION_EVENT_REVOKED</code></td><td><code>App.cache.clear()</code></td></tr>
|
|
<tr><td><code>applyPermInvalidateOnIdentityChange</code></td><td><code>SESSION_EVENT_IDENTITY_CHANGED</code></td><td><code>App.perm.invalidate()</code></td></tr>
|
|
<tr><td><code>applyConnectionsReauthOnIdentityChange</code></td><td><code>SESSION_EVENT_IDENTITY_CHANGED</code></td><td><code>App.connections.reauthenticateAll()</code></td></tr>
|
|
<tr><td><code>applyConnectionsCloseOnRevoke</code></td><td><code>SESSION_EVENT_REVOKED</code></td><td><code>App.connections.closeAll()</code></td></tr>
|
|
<tr><td><code>applySessionAutoRefresh</code></td><td>Timer action</td><td>Schedules session refresh through <code>App.orca</code>.</td></tr>
|
|
</tbody>
|
|
</table>
|
|
|
|
<Callout variant="warn" title="Public payloads only">
|
|
<p>
|
|
Bus payloads are observable contracts. Do not put tokens, passwords, authorization
|
|
headers, refresh secrets or sensitive hashes in them. Use actor ids, tenant ids,
|
|
causes and correlation ids.
|
|
</p>
|
|
</Callout>
|
|
|
|
<h2>Disposal</h2>
|
|
<p>
|
|
<code>App.dispose()</code> publishes the dispose-starting event, disposes all constructed
|
|
services in reverse construction order, tears down the prefs storage bridge, then disposes
|
|
<code>Prefs</code>, <code>Orca</code>, <code>Bus</code>, <code>Timers</code> and
|
|
<code>Logger</code>. Subsequent calls are no-ops.
|
|
</p>
|
|
<CodeBlock code={disposal} lang="ts" />
|
|
|
|
<h2>Common mistakes</h2>
|
|
<table>
|
|
<thead>
|
|
<tr>
|
|
<th>Mistake</th>
|
|
<th>Why it hurts</th>
|
|
<th>Correct pattern</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{#each commonMistakes as mistake (mistake.name)}
|
|
<tr>
|
|
<td><code>{mistake.name}</code></td>
|
|
<td>{mistake.why}</td>
|
|
<td>{mistake.fix}</td>
|
|
</tr>
|
|
{/each}
|
|
</tbody>
|
|
</table>
|
|
|
|
<h2>Testing</h2>
|
|
<p>
|
|
Suites under <code>src/arts/active-app/test</code> cover schema construction, service
|
|
ordering, prefs consumer wiring, orca presets, session auto refresh and cross-actor
|
|
isolation.
|
|
</p>
|
|
|
|
<AiAgentsBox
|
|
intro="Before editing $active-app, verify the fixed core, the service schema and the Orca preset boundary."
|
|
rows={aiAgentRows}
|
|
/>
|
|
|
|
<PageNav />
|
|
</article>
|