From bf3891ca91a762c9165dd108aa1efb3714bbc0aa Mon Sep 17 00:00:00 2001 From: dev Date: Fri, 1 May 2026 04:50:35 +0200 Subject: [PATCH] Decouple cross-artifact runtime imports and finalize bus contract MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Layer-boundary cleanup (audit-1-5.md section 1): - fend → adom: move apply.ts to libs/dom; FrontendDom is now DomApplier; fend falls back to bare applyChange instead of constructing ActiveDom - sium → lang: full split — pure code (consts, types, guards, helpers, errors, json, plural, plural_rules, diagnostics) moves to libs/lang; arts/lang keeps engine + active wrappers and re-exports for back-compat - conn → timr: minimum split — types and public constants move to libs/timers; conn imports types from $libs/timers; EngineConnectionsOptions.timers is now required (no more silent createEngineTimers fallback). Tests updated to construct shared timers per beforeEach Bus contract finishing touches (continued from prior session): - subscribe() returns the unsubscribe function directly - publishCausedBy() propagates correlationId/causationId - invokeListener hook + Svelte adapter wraps listeners in untrack - maxReentrancyDepth guard with BusReentrancyLimitError (fatal — bypasses listener-error trap) - DEV-mode structuredClone payload check raising BusInvalidPayloadError - BusListenerFailure carries envelopeId/correlationId/causationId - createBusRecent active wrapper, bus-context.svelte.ts, APP_EVENT_RUNTIMES table with assertEventCanFire wired into every publishApp* helper - App switches to createSvelteEngineBus audit-1-5.md captures the full ecosystem audit (5 axes); section 1 is now closed by these changes. Sections 2-5 remain open. Verification: svelte-check 1395/0 errors; server 1230 tests passed; client 19 tests passed. Co-Authored-By: Claude Opus 4.7 (1M context) --- audit-1-5.md | 259 ++++++++++++++++++ src/arts/aapp/active-app.svelte.ts | 2 +- src/arts/aapp/testing/index.ts | 2 +- src/arts/aapp/types.ts | 5 +- src/arts/adom/active-dom.svelte.ts | 6 +- src/arts/adom/index.ts | 4 +- src/arts/buss/engine-bus.ts | 12 +- src/arts/conn/acks.ts | 2 +- src/arts/conn/active-connections.svelte.ts | 2 +- src/arts/conn/connection-timers.ts | 2 +- src/arts/conn/connection.ts | 2 +- src/arts/conn/engine-connections.ts | 12 +- src/arts/conn/test/active-connections.test.ts | 16 +- src/arts/conn/test/connection.test.ts | 52 ++-- src/arts/conn/test/engine-connections.test.ts | 19 +- src/arts/conn/types.ts | 12 +- src/arts/fend/active-frontend.svelte.ts | 18 +- src/arts/fend/test/active-frontend.test.ts | 2 +- src/arts/lang/active-lang.svelte.ts | 18 +- src/arts/lang/engine-lang.ts | 40 +-- src/arts/lang/index.ts | 110 ++++---- src/arts/lang/mono-lang.svelte.ts | 29 +- src/arts/lang/test/lang.test.ts | 25 +- src/arts/sium/engine-resolver.ts | 9 +- src/arts/sium/engine-sium.ts | 2 +- src/arts/sium/langs/langs.ts | 3 +- src/arts/timr/active-timers.svelte.ts | 2 +- src/arts/timr/clock.ts | 2 +- src/arts/timr/consts.ts | 87 ++---- src/arts/timr/diagnostics.ts | 2 +- src/arts/timr/engine-timers.ts | 2 +- src/arts/timr/index.ts | 2 +- src/arts/timr/test/engine-timers.test.ts | 2 +- src/arts/timr/timer-cancel.ts | 2 +- src/arts/timr/timer-entry.ts | 2 +- src/arts/timr/timer-events.ts | 2 +- src/arts/timr/timer-handle.ts | 2 +- src/arts/timr/timer-runner.ts | 2 +- src/libs/buss/diagnostics.ts | 91 ++++-- src/libs/buss/index.ts | 13 +- src/{arts/adom => libs/dom}/apply.ts | 19 +- src/libs/dom/index.ts | 1 + .../adom => libs/dom}/test/apply.test.ts | 2 +- src/{arts => libs}/lang/consts.ts | 0 src/{arts => libs}/lang/diagnostics.ts | 0 src/{arts => libs}/lang/errors.ts | 0 src/{arts => libs}/lang/guards.ts | 0 src/{arts => libs}/lang/helpers.ts | 0 src/libs/lang/index.ts | 79 ++++++ src/{arts => libs}/lang/json.ts | 0 src/{arts => libs}/lang/plural.ts | 0 src/{arts => libs}/lang/plural_rules.ts | 0 src/{arts => libs}/lang/types.ts | 0 src/libs/timers/consts.ts | 66 +++++ src/libs/timers/index.ts | 4 +- src/{arts/timr => libs/timers}/types.ts | 19 +- 56 files changed, 786 insertions(+), 282 deletions(-) create mode 100644 audit-1-5.md rename src/{arts/adom => libs/dom}/apply.ts (64%) rename src/{arts/adom => libs/dom}/test/apply.test.ts (97%) rename src/{arts => libs}/lang/consts.ts (100%) rename src/{arts => libs}/lang/diagnostics.ts (100%) rename src/{arts => libs}/lang/errors.ts (100%) rename src/{arts => libs}/lang/guards.ts (100%) rename src/{arts => libs}/lang/helpers.ts (100%) create mode 100644 src/libs/lang/index.ts rename src/{arts => libs}/lang/json.ts (100%) rename src/{arts => libs}/lang/plural.ts (100%) rename src/{arts => libs}/lang/plural_rules.ts (100%) rename src/{arts => libs}/lang/types.ts (100%) create mode 100644 src/libs/timers/consts.ts rename src/{arts/timr => libs/timers}/types.ts (93%) diff --git a/audit-1-5.md b/audit-1-5.md new file mode 100644 index 0000000..cabb4ac --- /dev/null +++ b/audit-1-5.md @@ -0,0 +1,259 @@ +# Active framework — ecosystem audit (arts / libs / svrs) + +Scope: `src/arts/`, `src/libs/`, `src/svrs/`. The `src/web/` folder is +explicitly excluded. Findings are grouped by audit axis. Severity tags: + +- **bug** — incorrect runtime behaviour +- **violation** — breaks an established framework rule +- **drift** — code or docs out of sync with the rest +- **opportunity** — refactor / clean-up that is not a bug today + +Each item includes file paths and line ranges so the fix can be applied +without re-searching. Items are ordered roughly by priority within each +section. + +--- + +## 1. Layer-boundary violations + +Rule: `arts/` may import from `$libs/*` and from its own internal +files; cross-artifact imports (`$`) are reserved for the +composition root `arts/aapp`. `libs/*` may not import from any +`$` alias. `svrs/*` is similarly restricted. + +### Real coupling (runtime, not test) — **violation** + +- `src/arts/fend/active-frontend.svelte.ts:2` — + `import { createActiveDom, type ActiveDom } from '$adom';` + `arts/fend` reaches into `arts/adom`. The DOM port should live in + `libs/dom` and `fend` should accept an interface, leaving the DOM + engine wiring to `aapp`. +- `src/arts/sium/engine-resolver.ts:1-2` — + `import { ID_FALLBACK_SEPARATOR, parseLangRef } from '$lang';` + `arts/sium` imports runtime symbols from `arts/lang`. These two + values are pure helpers and belong in `libs/lang` so `sium` can + consume them without crossing artifacts. +- `src/arts/conn/engine-connections.ts:1` — + `import { createEngineTimers } from '$timr';` + `arts/conn` constructs its own timer engine instead of accepting a + `TimerScheduler` through options. The pattern elsewhere (sess, cach, + perm) is "App injects Timers"; conn should follow it. + +### Type-only cross-artifact imports — **opportunity** + +These compile away but still couple the two artifacts at the type +level. They should be promoted to `libs//types.ts` so consumers +import the contract without naming the engine. + +- `src/arts/auth/client.ts:2` — `import type { EngineHttp, HttpBodyInit, HttpResult } from '$http';` +- `src/arts/sess/types.ts:33` — `import type { SyncStorageAdapter } from '$stor';` +- `src/arts/sium/engine-resolver.ts:1` — `import type { EngineLang, LangParams, SupportedLocale } from '$lang';` +- `src/arts/conn/types.ts` and related — `import type { TimerScheduler, ... } from '$timr';` + +### `svrs/*` and `aapp` — clean + +`src/svrs/*` does not import any `$` alias. `arts/aapp` is +the composition root and is allowed to import every engine; the +imports there are intentional. + +--- + +## 2. Naming convention drift + +Rule: every module-event / method / diagnostic value must be a +**module-scoped lowercase string** (`'sess.lifecycle.adopted'`, +`'cach.delete'`, `'conn.connected'`, `'buss.event.published'`). Bare +names (`'delete'`, `'hit'`, `'auth_failed'`) collide across artifacts +once aggregated in logs or wired through the bus. + +### Unscoped diagnostic event values — **violation** + +- `src/arts/conn/consts.ts:4-20` — `CONNECTION_DIAGNOSTIC_EVENTS` has + 15 bare values: `'auth_failed'`, `'browser_reconnect'`, + `'connect_failed'`, `'frame_decode_failed'`, `'frame_encode_failed'`, + `'heartbeat_timeout'`, `'listener_threw'`, `'reauth_failed'`, + `'reconnect_exhausted'`, `'send_failed'`, `'session_changed'`, + `'session_expired'`, `'session_refreshed'`, `'session_revoked'`, + `'transport_error'`. All should carry the `'conn.'` prefix. +- `src/arts/sess/consts.ts:4-20` — `SESSION_DIAGNOSTIC_EVENTS` has 15 + bare values: `'adopt_server_invalid'`, `'disposed_access'`, + `'listener_threw'`, `'refresh_*'` (5×), `'revoke_global_*'` (3×), + `'storage_*'` (3×). All need `'sess.'`. +- `src/arts/perm/consts.ts:41-45` — + `PERMISSION_CLIENT_DIAGNOSTIC_EVENTS` has `'remote_batch_failed'`, + `'remote_check_failed'`, `'remote_what_failed'`. All need `'perm.'`. +- `src/svrs/perm/consts.ts:3-7` — `PERMISSION_DIAGNOSTIC_EVENTS` has + `'decision'`, `'denied'`, `'indeterminate'`. All need a `'perm.'` + prefix (or a server-specific scope, e.g. `'perm.server.decision'`). + +### Unscoped method constants — **violation** + +- `src/svrs/perm/consts.ts:19-25` — `PERMISSION_METHOD_*` constants + hold bare names (`'check'`, `'can'`, `'assert'`, `'explain'`, + `'what'`, `'who'`, `'filter'`). The client-side counterpart in + `arts/perm` already uses `'perm.check'`, `'perm.can'`, … — server + must align. + +### Why this matters + +`Logger.warn(category, message)` aggregates across artifacts. With +unscoped diagnostic strings, a value like `'listener_threw'` appears +under both `category: 'conn'` and `category: 'sess'` and is +indistinguishable in any flat log search. + +--- + +## 3. Error-class and dispose-pattern consistency + +### Hard-coded error `name` strings — **violation** + +The convention is `this.name = _ERROR_NAME_*` (read from +`consts.ts`). + +- `src/arts/sium/core/types.ts:302` — `SiumValidationError` sets + `this.name = 'SiumValidationError'` literally. +- `src/arts/sium/core/types.ts:322` — `SiumAsyncSchemaError` same + pattern. + +### Error classes without matching `is*Error` guards — **violation** + +The pattern is "one class, one guard". Missing guards make `instanceof` +checks leak into call sites. + +- `src/arts/conn/errors.ts:49` — `ConnChannelAlreadyExistsError` no + `isConnChannelAlreadyExistsError`. +- `src/arts/conn/errors.ts:58` — `ConnChannelNotFoundError` no + `isConnChannelNotFoundError`. +- `src/libs/auth/errors.ts:24-66` — 10 auth error classes have no + guards: `AuthAccountNotLinkedError`, `AuthSessionRevokedError`, + `AuthAssuranceRequiredError`, `AuthRateLimitedError`, + `AuthTenantBoundaryError`, `AuthOAuthStateInvalidError`, + `AuthOAuthProviderError`, `AuthOtpInvalidError`, + `AuthMfaRequiredError`, `AuthWebAuthnError`. + +### `dispose()` without idempotency guard — **bug** + +The convention is `if (disposed) return; disposed = true; …`. Calling +`dispose()` twice should be a no-op. + +- `src/arts/conn/active-connections.svelte.ts:118` — second call would + re-traverse `detachers` (empty by then) and call `engine.dispose()` + again. +- `src/arts/fend/active-frontend.svelte.ts:185` — second call would + re-invoke `ownedDom?.dispose()` if the inner DOM was created here. + +### `Logger` option not defaulting to `SILENT_LOGGER` — **violation** + +Pattern in the codebase: `const logger = options.logger ?? SILENT_LOGGER;` +(see `arts/buss/engine-bus.ts:54`). + +- `src/arts/stor/engine-storage.ts` — `createStorageDiagnostics(options.logger)` + passes the `undefined` straight through. +- `src/arts/timr/engine-timers.ts` — same issue with + `createTimerDiagnostics`. +- `src/arts/http/engine-options.ts` — same issue with + `createHttpDiagnostics`. + +If diagnostics ever calls a method on the logger without guarding for +`undefined`, these three engines crash when used without a logger +(e.g. in unit tests or smoke harnesses). Verify each diagnostics +constructor; if it already null-checks internally, demote to +**opportunity** for consistency only. + +### Generic `throw new Error(...)` in runtime code — **opportunity** + +Typed error classes carry the discriminating `name` plus optional +fields (cause, code). Generic throws lose that. + +- `src/arts/lang/engine-lang.ts:124, 133` — circular-reference message + thrown as plain `Error`. +- `src/libs/perm/runtime.ts:154, 181` — throws + `Error(PERMISSION_ERROR_MSG_FILTER_REQUIRES_ACTOR)` where a + `PermissionInvalidFilterError` would fit. + +--- + +## 4. Dead code, casts, and `any` + +Codebase is largely clean. Specific findings worth acting on: + +- `src/svrs/auth/adapters/db.ts:40, 64, 72, 79, 111, 134` — six + repetitions of `as unknown as Readonly>`. + Consolidate into a `DbRow` type alias or a `coerceRow()` helper. + **opportunity**. +- `src/libs/buss/silent-bus.ts:137` — `} as unknown as EngineBus;`. + Acceptable because the no-op surface is intentionally generic, but + worth replacing with proper generic constraints when the object + literal grows. **opportunity**. +- `src/arts/lang/types.ts:83` — `HasParams` uses `any` deliberately + (contravariant variance). Comment is in place. **no action**. +- `src/libs/days/_vendor/**` — multiple `@ts-ignore` and `TODO` + comments. Vendor code, leave as-is. **no action**. + +No dead exports were found in the spot checks of `libs/buss`, +`libs/auth`, `arts/conn`, `libs/aapp/events.ts`. No duplicate type +definitions detected across the audited modules. + +--- + +## 5. Documentation drift + +Only the high-traffic READMEs were sampled. + +### `arts/buss/README.md` + +- The session-translator example (around line 563) hardcodes `cause: + APP_USER_IDENTITY_CAUSE_SESSION_ADOPTED` for every event. The actual + translator (`arts/aapp/integrations/session-translator.ts:37`) calls + `resolveAppIdentityCause(payload.event)` to map each `EVENT_*` to + the correct cause. The example should mirror that mapping or it + teaches the wrong pattern. **drift**. +- The README was already partially refreshed earlier in this session; + re-check the "Implementation gaps → Done" list now that + `subscribe()`, `publishCausedBy()`, `invokeListener`, the depth + guard, the payload-cloneable check, the Svelte adapter, the + `createBusRecent` wrapper, the `bus-context.svelte.ts`, and the + `APP_EVENT_RUNTIMES` table all landed. + +### `arts/sess/README.md` + +- The README still references a `publishSessIdentityChanged` helper. + Actual export is `publishSessLifecycleEvent` in + `arts/sess/bus-helpers.ts`. Either rename in code or update the + README. **drift**. + +### `arts/cach/README.md` + +- README written in Spanish (a deliberate choice; not flagged as a + rule break). At lines 11–12 it suggests importing + `createEngineCache` from `$svrs/cach`, but the artifact's barrel + (`arts/cach/index.ts`) only exports `createActiveCache`. Add a one- + line note clarifying the engine vs active split. **drift**. + +### Other artifacts (aapp, adom, auth, conn, fend, fmts, http, lang, logr, perm, sium, stor, timr) + +Spot checks did not surface drift — claims match exports. + +--- + +## Suggested fix order + +1. **Coupling fixes** (Section 1, runtime violations). They constrain + every other refactor: until `fend → adom`, `sium → lang`, and + `conn → timr` are decoupled, those artifacts cannot be tested in + isolation. +2. **Naming convention pass** (Section 2). Mechanical, scoped to four + `consts.ts` files, and unblocks coherent log aggregation. Update + any test that hard-codes the literal values. +3. **Error guards + idempotent `dispose`** (Section 3). Low-risk and + localised; prevents subtle bugs if any consumer ever calls + `dispose()` twice or tries to discriminate auth errors. +4. **Logger defaults** (Section 3). Verify the three diagnostics + constructors and add `?? SILENT_LOGGER` where missing. +5. **Documentation refresh** (Section 5). Easiest after the code + changes above so the README reflects the final shape. +6. **Cast consolidation** (Section 4). Pure refactor, ship it whenever + convenient. + +Items in Section 4 (vendor code, justified `any`, intentional cast) +require **no action**. diff --git a/src/arts/aapp/active-app.svelte.ts b/src/arts/aapp/active-app.svelte.ts index fd2dba8..2404e85 100644 --- a/src/arts/aapp/active-app.svelte.ts +++ b/src/arts/aapp/active-app.svelte.ts @@ -9,7 +9,7 @@ import type { ActiveConnections, ActiveConnectionsOptions, ConnectionMap } from import { createActiveFrontend } from '$fend/active-frontend.svelte'; import { createActiveFormats } from '$fmts/active-formats.svelte'; import { createEngineHttp } from '$http/engine-http'; -import type { ActiveLang, LangNode } from '$lang/types'; +import type { ActiveLang, LangNode } from '$libs/lang'; import { createActiveLang } from '$lang/active-lang.svelte'; import { createActiveMonoLang } from '$lang/mono-lang.svelte'; import { createEngineLogger } from '$logr/engine-logger'; diff --git a/src/arts/aapp/testing/index.ts b/src/arts/aapp/testing/index.ts index 025d01e..eb6d060 100644 --- a/src/arts/aapp/testing/index.ts +++ b/src/arts/aapp/testing/index.ts @@ -1,5 +1,5 @@ import { LogLevel, type LogEntry, type LoggerOptions, type Transport } from '$logr'; -import type { LangNode } from '$lang'; +import type { LangNode } from '$libs/lang'; import { createActiveApp } from '../active-app.svelte'; import type { ActiveApp, ActiveAppOptions } from '../types'; diff --git a/src/arts/aapp/types.ts b/src/arts/aapp/types.ts index f30157c..4930f30 100644 --- a/src/arts/aapp/types.ts +++ b/src/arts/aapp/types.ts @@ -8,13 +8,14 @@ import type { FrontendPreferenceKey } from '$fend'; import type { ActiveFormats, ActiveFormatsOptions } from '$fmts'; import type { EngineHttp, EngineHttpOptions } from '$http'; import type { AppEventMap } from '$libs/aapp/events'; -import type { ActiveLang, LangNode, SupportedLocale } from '$lang'; +import type { ActiveLang, LangNode, SupportedLocale } from '$libs/lang'; import type { EngineLogger, LoggerOptions } from '$logr'; import type { ActivePermissions, ActivePermissionsOptions } from '$perm'; import type { ActiveSession, EngineSessionOptions, SessEventMap } from '$sess'; import type { EngineSium } from '$sium'; import type { ActiveStorage, SyncStorageAdapter } from '$stor'; -import type { ActiveTimers, EngineTimersOptions } from '$timr'; +import type { ActiveTimers } from '$timr'; +import type { EngineTimersOptions } from '$libs/timers'; import type { APP_ORCHESTRATION_SILENT, APP_ORCHESTRATION_STANDARD, diff --git a/src/arts/adom/active-dom.svelte.ts b/src/arts/adom/active-dom.svelte.ts index 5f1e66a..c7a266d 100644 --- a/src/arts/adom/active-dom.svelte.ts +++ b/src/arts/adom/active-dom.svelte.ts @@ -1,13 +1,15 @@ import { readableActive, type Active } from '$reactive'; import { + applyChange, BREAKPOINTS_DEFAULT, getCurrentBreakpoint, + removeAttrs, resolveResponsiveProp, type Breakpoint, type Breakpoints, - type ResponsiveProp + type ResponsiveProp, + type StructuralChange } from '$libs/dom'; -import { applyChange, removeAttrs, type StructuralChange } from './apply.js'; import { createViewportTracker, getSharedViewport, diff --git a/src/arts/adom/index.ts b/src/arts/adom/index.ts index aa40ba5..39f7ec4 100644 --- a/src/arts/adom/index.ts +++ b/src/arts/adom/index.ts @@ -7,8 +7,8 @@ export { createActiveDom } from './active-dom.svelte.js'; export type { ActiveDom, ActiveDomProps } from './active-dom.svelte.js'; -export { applyChange, removeAttrs } from './apply.js'; -export type { DomAttrValue, StructuralChange } from './apply.js'; +export { applyChange, removeAttrs } from '$libs/dom'; +export type { DomApplier, DomAttrValue, StructuralChange } from '$libs/dom'; export { BodyScrollLock } from './body-scroll-lock.svelte.js'; export type { BodyScrollLockOption } from './body-scroll-lock.svelte.js'; diff --git a/src/arts/buss/engine-bus.ts b/src/arts/buss/engine-bus.ts index 929b4a6..cea9e1d 100644 --- a/src/arts/buss/engine-bus.ts +++ b/src/arts/buss/engine-bus.ts @@ -12,8 +12,9 @@ import { BusDisposedError, BusInvalidPayloadError, BusReentrancyLimitError, - logBusListenerFailure, - logBusListenerLeakWarning + createBusDiagnostics, + emitBusListenerFailureDiagnostic, + emitBusListenerLeakWarningDiagnostic } from '$libs/buss'; import type { BusEnvelope, @@ -52,6 +53,7 @@ export function createEngineBus( options: EngineBusOptions = {} ): EngineBus { const logger = options.logger ?? SILENT_LOGGER; + const diagnostics = createBusDiagnostics(logger); const clock = options.clock ?? DEFAULT_CLOCK; const idFactory = options.idFactory ?? createIncrementalIdFactory(BUS_ID_PREFIX); const maxListenersPerEvent = @@ -150,8 +152,8 @@ export function createEngineBus( function warnIfListenerLeak(type: string | typeof BUS_EVENT_ALL): void { const count = listenerBucket(type).length; if (count <= maxListenersPerEvent) return; - logBusListenerLeakWarning({ - logger, + emitBusListenerLeakWarningDiagnostic({ + diagnostics, type, count, max: maxListenersPerEvent @@ -316,7 +318,7 @@ export function createEngineBus( }; errors.push(failure); if (mode === BUS_LISTENER_ERROR_MODE_LOG_AND_CONTINUE) { - logBusListenerFailure({ logger, envelope, failure }); + emitBusListenerFailureDiagnostic({ diagnostics, envelope, failure }); } } diff --git a/src/arts/conn/acks.ts b/src/arts/conn/acks.ts index d016ca0..bcdef67 100644 --- a/src/arts/conn/acks.ts +++ b/src/arts/conn/acks.ts @@ -1,4 +1,4 @@ -import type { TimerScheduler } from '$timr'; +import type { TimerScheduler } from '$libs/timers'; import { CONNECTION_ACK_REASON_CLOSED, CONNECTION_ACK_REASON_REJECTED, diff --git a/src/arts/conn/active-connections.svelte.ts b/src/arts/conn/active-connections.svelte.ts index 448ecbc..a391b8c 100644 --- a/src/arts/conn/active-connections.svelte.ts +++ b/src/arts/conn/active-connections.svelte.ts @@ -17,7 +17,7 @@ import type { } from './types.ts'; export function createActiveConnections( - options: ActiveConnectionsOptions = {} + options: ActiveConnectionsOptions ): ActiveConnections { const engine = createEngineConnections(options); // eslint-disable-next-line svelte/prefer-svelte-reactivity -- detach callbacks are bookkeeping, not rendered state. diff --git a/src/arts/conn/connection-timers.ts b/src/arts/conn/connection-timers.ts index 2fe2738..08835e4 100644 --- a/src/arts/conn/connection-timers.ts +++ b/src/arts/conn/connection-timers.ts @@ -1,4 +1,4 @@ -import type { TimerScheduler } from '$timr'; +import type { TimerScheduler } from '$libs/timers'; import { timerKey } from './helpers.ts'; export interface ConnectionTimerControls { diff --git a/src/arts/conn/connection.ts b/src/arts/conn/connection.ts index 5736096..2c22b1f 100644 --- a/src/arts/conn/connection.ts +++ b/src/arts/conn/connection.ts @@ -1,4 +1,4 @@ -import type { TimerScheduler } from '$timr'; +import type { TimerScheduler } from '$libs/timers'; import { createConnectionAckRegistry } from './acks.ts'; import { wireBrowserReconnect } from './browser-reconnect.ts'; import { createConnectionChannelRegistry } from './channel-registry.ts'; diff --git a/src/arts/conn/engine-connections.ts b/src/arts/conn/engine-connections.ts index d58408b..ac8c553 100644 --- a/src/arts/conn/engine-connections.ts +++ b/src/arts/conn/engine-connections.ts @@ -1,4 +1,3 @@ -import { createEngineTimers } from '$timr'; import { createBusSessionSource } from './bus-session-source.ts'; import { createConnection } from './connection.ts'; import { @@ -81,16 +80,10 @@ function mergeConnectionOptions( } export function createEngineConnections( - options: EngineConnectionsOptions = {} + options: EngineConnectionsOptions ): EngineConnections { const connections = new Map(); - const timers = - options.timers ?? - createEngineTimers({ - clock: options.clock, - logger: options.logger - }); - const ownsTimers = options.timers === undefined; + const { timers } = options; const sessionSource = options.session ?? (options.bus === undefined || !shouldWireBusSessionSource(options.autoReauthOn) @@ -182,7 +175,6 @@ export function createEngineConnections { + timers = createEngineTimers(); +}); +afterEach(() => { + timers.dispose(); +}); describe('createActiveConnections', () => { it('tracks active names and connection state buckets', async () => { - const Connections = createActiveConnections(); + const Connections = createActiveConnections({ timers }); const Main = Connections.createConnection('main', { transport: createMockTransport(), heartbeat: false, @@ -48,7 +58,7 @@ describe('createActiveConnections', () => { }); it('keeps active state updated when operations go through the active root', async () => { - const Connections = createActiveConnections(); + const Connections = createActiveConnections({ timers }); Connections.createConnection('main', { transport: createMockTransport(), heartbeat: false, diff --git a/src/arts/conn/test/connection.test.ts b/src/arts/conn/test/connection.test.ts index 0c0ac1b..49d61ce 100644 --- a/src/arts/conn/test/connection.test.ts +++ b/src/arts/conn/test/connection.test.ts @@ -1,4 +1,4 @@ -import { afterEach, describe, expect, it, vi } from 'vitest'; +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import { CONNECTION_ACK_REASON_CLOSED, CONNECTION_ACK_REASON_TIMEOUT, @@ -26,7 +26,8 @@ import { publishAppUserIdentityChanged, type AppEventMap } from '$libs/aapp/events'; -import type { TimerClock } from '$timr'; +import { createEngineTimers } from '$timr'; +import type { EngineTimers, TimerClock } from '$libs/timers'; import type { ConnectionFrame, MockConnectionTransport } from '../index.ts'; function latestFrame(transport: MockConnectionTransport): ConnectionFrame { @@ -38,13 +39,18 @@ function emitFrame(transport: MockConnectionTransport, frame: ConnectionFrame): transport.emitMessage(JSON.stringify(createFrame(frame))); } +let timers: EngineTimers; +beforeEach(() => { + timers = createEngineTimers(); +}); afterEach(() => { + timers.dispose(); vi.useRealTimers(); }); describe('Connection lifecycle', () => { it('connects and disconnects through the transport', async () => { - const Connections = createEngineConnections(); + const Connections = createEngineConnections({ timers }); const transport = createMockTransport(); const Main = Connections.createConnection('main', { transport, @@ -71,7 +77,8 @@ describe('Connection lifecycle', () => { setTimeout: () => 0, clearTimeout: () => {} }; - const Connections = createEngineConnections({ clock }); + const localTimers = createEngineTimers({ clock }); + const Connections = createEngineConnections({ timers: localTimers }); const Main = Connections.createConnection('main', { transport: createMockTransport(), heartbeat: false, @@ -86,6 +93,7 @@ describe('Connection lifecycle', () => { expect(Main.closedAt).toBe(4242); Connections.dispose(); + localTimers.dispose(); }); it('does not update closedAt when the transport repeats an intentional close', async () => { @@ -95,7 +103,8 @@ describe('Connection lifecycle', () => { setTimeout: () => 0, clearTimeout: () => {} }; - const Connections = createEngineConnections({ clock }); + const localTimers = createEngineTimers({ clock }); + const Connections = createEngineConnections({ timers: localTimers }); const transport = createMockTransport(); const Main = Connections.createConnection('main', { transport, @@ -112,11 +121,12 @@ describe('Connection lifecycle', () => { expect(Main.closedAt).toBe(100); Connections.dispose(); + localTimers.dispose(); }); it('does not reconnect after an intentional disconnect', async () => { vi.useFakeTimers(); - const Connections = createEngineConnections(); + const Connections = createEngineConnections({ timers }); const transport = createMockTransport(); const Main = Connections.createConnection('main', { transport, @@ -135,7 +145,7 @@ describe('Connection lifecycle', () => { it('reconnects after an unexpected close', async () => { vi.useFakeTimers(); - const Connections = createEngineConnections(); + const Connections = createEngineConnections({ timers }); const transport = createMockTransport(); const Main = Connections.createConnection('main', { transport, @@ -159,7 +169,7 @@ describe('Connection lifecycle', () => { it('closes the transport when connect-time auth fails', async () => { vi.useFakeTimers(); - const Connections = createEngineConnections(); + const Connections = createEngineConnections({ timers }); const transport = createMockTransport(); const Main = Connections.createConnection('main', { transport, @@ -188,7 +198,7 @@ describe('Connection lifecycle', () => { describe('Connection send/request', () => { it('sends a serialized frame when open', async () => { - const Connections = createEngineConnections(); + const Connections = createEngineConnections({ timers }); const transport = createMockTransport(); const Main = Connections.createConnection('main', { transport, @@ -210,7 +220,7 @@ describe('Connection send/request', () => { }); it('fails fire-and-forget sends while closed unless buffering is enabled', async () => { - const Connections = createEngineConnections(); + const Connections = createEngineConnections({ timers }); const Main = Connections.createConnection('main', { transport: createMockTransport(), heartbeat: false, @@ -225,7 +235,7 @@ describe('Connection send/request', () => { }); it('buffers fire-and-forget frames and flushes on connect', async () => { - const Connections = createEngineConnections(); + const Connections = createEngineConnections({ timers }); const transport = createMockTransport(); const Main = Connections.createConnection('main', { transport, @@ -249,7 +259,7 @@ describe('Connection send/request', () => { }); it('resolves a request from an incoming replyTo frame', async () => { - const Connections = createEngineConnections(); + const Connections = createEngineConnections({ timers }); const transport = createMockTransport(); const Main = Connections.createConnection('main', { transport, @@ -277,7 +287,7 @@ describe('Connection send/request', () => { it('times out unanswered requests', async () => { vi.useFakeTimers(); - const Connections = createEngineConnections(); + const Connections = createEngineConnections({ timers }); const Main = Connections.createConnection('main', { transport: createMockTransport(), heartbeat: false, @@ -294,7 +304,7 @@ describe('Connection send/request', () => { }); it('closes pending requests on disconnect', async () => { - const Connections = createEngineConnections(); + const Connections = createEngineConnections({ timers }); const Main = Connections.createConnection('main', { transport: createMockTransport(), heartbeat: false, @@ -313,7 +323,7 @@ describe('Connection send/request', () => { describe('Connection channels', () => { it('caches channel instances by name', () => { - const Connections = createEngineConnections(); + const Connections = createEngineConnections({ timers }); const Main = Connections.createConnection('main', { transport: createMockTransport(), heartbeat: false, @@ -326,7 +336,7 @@ describe('Connection channels', () => { }); it('routes topic frames to the matching channel', async () => { - const Connections = createEngineConnections(); + const Connections = createEngineConnections({ timers }); const transport = createMockTransport(); const Main = Connections.createConnection('main', { transport, @@ -353,7 +363,7 @@ describe('Connection channels', () => { }); it('join sends the standard channel join frame', async () => { - const Connections = createEngineConnections(); + const Connections = createEngineConnections({ timers }); const transport = createMockTransport(); const Main = Connections.createConnection('main', { transport, @@ -376,7 +386,7 @@ describe('Connection channels', () => { }); it('marks a disposed channel as left', async () => { - const Connections = createEngineConnections(); + const Connections = createEngineConnections({ timers }); const Main = Connections.createConnection('main', { transport: createMockTransport(), heartbeat: false, @@ -399,7 +409,8 @@ describe('Connection session bridge', () => { const Bus = createEngineBus(); const Connections = createEngineConnections({ bus: Bus, - autoReauthOn: CONNECTION_AUTO_REAUTH_STANDARD + autoReauthOn: CONNECTION_AUTO_REAUTH_STANDARD, + timers }); const Main = Connections.createConnection('main', { transport: createMockTransport(), @@ -432,7 +443,8 @@ describe('Connection session bridge', () => { listener = undefined; }; } - } + }, + timers }); const Main = Connections.createConnection('main', { transport: createMockTransport(), diff --git a/src/arts/conn/test/engine-connections.test.ts b/src/arts/conn/test/engine-connections.test.ts index cba8f36..a2a67fb 100644 --- a/src/arts/conn/test/engine-connections.test.ts +++ b/src/arts/conn/test/engine-connections.test.ts @@ -1,4 +1,4 @@ -import { afterEach, describe, expect, it, vi } from 'vitest'; +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import { CONNECTION_STATE_CLOSED, CONNECTION_STATE_OPEN, @@ -7,14 +7,21 @@ import { isConnConnectionAlreadyExistsError, isConnConnectionNotFoundError } from '../index.ts'; +import { createEngineTimers } from '$timr'; +import type { EngineTimers } from '$libs/timers'; +let timers: EngineTimers; +beforeEach(() => { + timers = createEngineTimers(); +}); afterEach(() => { + timers.dispose(); vi.useRealTimers(); }); describe('createEngineConnections', () => { it('creates, retrieves and lists named connections', () => { - const Connections = createEngineConnections(); + const Connections = createEngineConnections({ timers }); const transport = createMockTransport(); const Main = Connections.createConnection('main', { transport, @@ -30,7 +37,7 @@ describe('createEngineConnections', () => { }); it('rejects duplicate names with a typed error', () => { - const Connections = createEngineConnections(); + const Connections = createEngineConnections({ timers }); Connections.createConnection('main', { transport: createMockTransport(), heartbeat: false, @@ -51,7 +58,7 @@ describe('createEngineConnections', () => { }); it('throws a typed error for missing names', () => { - const Connections = createEngineConnections(); + const Connections = createEngineConnections({ timers }); try { Connections.connection('missing'); @@ -63,7 +70,7 @@ describe('createEngineConnections', () => { }); it('opens and closes every registered connection', async () => { - const Connections = createEngineConnections(); + const Connections = createEngineConnections({ timers }); Connections.createConnection('main', { transport: createMockTransport(), heartbeat: false, @@ -89,7 +96,7 @@ describe('createEngineConnections', () => { }); it('keeps close() safe when the method is destructured', async () => { - const Connections = createEngineConnections(); + const Connections = createEngineConnections({ timers }); const Main = Connections.createConnection('main', { transport: createMockTransport(), heartbeat: false, diff --git a/src/arts/conn/types.ts b/src/arts/conn/types.ts index 89aa0e0..4eedf8b 100644 --- a/src/arts/conn/types.ts +++ b/src/arts/conn/types.ts @@ -1,6 +1,6 @@ import type { Logger } from '$libs/logr'; import type { AppEventBus } from '$libs/aapp/events'; -import type { TimerClock, TimerScheduler } from '$timr'; +import type { TimerScheduler } from '$libs/timers'; import type { CONNECTION_ACK_REASON_CLOSED, CONNECTION_ACK_REASON_INVALID_REPLY, @@ -357,8 +357,14 @@ export interface Connection; readonly bus?: AppEventBus; readonly autoReauthOn?: ConnectionAutoReauthOn; diff --git a/src/arts/fend/active-frontend.svelte.ts b/src/arts/fend/active-frontend.svelte.ts index a53a5e0..563d2f4 100644 --- a/src/arts/fend/active-frontend.svelte.ts +++ b/src/arts/fend/active-frontend.svelte.ts @@ -1,5 +1,5 @@ import type { LocaleSource } from '$locale'; -import { createActiveDom, type ActiveDom } from '$adom'; +import { applyChange, type DomApplier } from '$libs/dom'; import { DEFAULT_DENSITY, DEFAULT_MODE, DEFAULT_THEME, FRONTEND_ATTRS } from './consts'; import { resolveDir } from './locale-defaults'; import type { Direction } from './locale-defaults'; @@ -16,7 +16,15 @@ export type FrontendDirMode = Direction | 'auto'; export type FrontendLocaleSource = LocaleSource; export type FrontendTarget = HTMLElement | (() => HTMLElement | null); -export type FrontendDom = Pick; + +/** + * Anything that absorbs a `StructuralChange`. The full `ActiveDom` + * engine satisfies this; so does a plain object with an `apply()` + * method (test spies, custom batchers). When `Frontend` is used + * standalone without an external applier, it falls back to the bare + * `applyChange` helper from `$libs/dom`. + */ +export type FrontendDom = DomApplier; export interface ActiveFrontendOptions { locale?: string; @@ -79,9 +87,8 @@ function subscribeMedia(query: string, fn: (matches: boolean) => void): () => vo export function createActiveFrontend(options: ActiveFrontendOptions = {}): ActiveFrontend { const localeSource = options.localeSource; - const ownedDom = - options.applyDom === false || options.dom !== undefined ? undefined : createActiveDom(); - const dom = options.applyDom === false ? undefined : (options.dom ?? ownedDom); + const dom: FrontendDom | undefined = + options.applyDom === false ? undefined : (options.dom ?? { apply: applyChange }); let currentLocale = $state(localeSource?.getLocale() ?? options.locale ?? ''); let dirOverride = $state( @@ -227,7 +234,6 @@ export function createActiveFrontend(options: ActiveFrontendOptions = {}): Activ unsubscribeLocale?.(); unsubscribeDarkMode(); unsubscribeReducedMotion(); - ownedDom?.dispose(); listeners.clear(); } }; diff --git a/src/arts/fend/test/active-frontend.test.ts b/src/arts/fend/test/active-frontend.test.ts index 3cf2f25..d0414b9 100644 --- a/src/arts/fend/test/active-frontend.test.ts +++ b/src/arts/fend/test/active-frontend.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from 'vitest'; +import type { StructuralChange } from '$libs/dom'; import { FRONTEND_ATTRS, createActiveFrontend } from '..'; -import type { StructuralChange } from '../../adom'; describe('createActiveFrontend()', () => { it('keeps auto and explicit values consistent across locale changes', () => { diff --git a/src/arts/lang/active-lang.svelte.ts b/src/arts/lang/active-lang.svelte.ts index 7a50b9a..1938717 100644 --- a/src/arts/lang/active-lang.svelte.ts +++ b/src/arts/lang/active-lang.svelte.ts @@ -1,13 +1,13 @@ import { SvelteSet } from 'svelte/reactivity'; -import { - type ActiveLang, - createEngineLang, - type EngineLang, - type LangNode, - type LangParams, - type LangString, - type SupportedLocale -} from './index'; +import type { + ActiveLang, + EngineLang, + LangNode, + LangParams, + LangString, + SupportedLocale +} from '$libs/lang'; +import { createEngineLang } from './engine-lang.ts'; import type { Logger } from '$libs/logr'; export type { LangNode, LangParams, LangString, SupportedLocale, ActiveLang }; diff --git a/src/arts/lang/engine-lang.ts b/src/arts/lang/engine-lang.ts index b2ec320..0fbb0f1 100644 --- a/src/arts/lang/engine-lang.ts +++ b/src/arts/lang/engine-lang.ts @@ -1,26 +1,28 @@ -import type { - SupportedLocale, - LangBranch, - LangFn, - LangParams, - LangRecord, - LangString, - EngineLang, - LangNode -} from './types.ts'; -import { LANG_ERRORS } from './errors.ts'; -import { ID_PREFIX, LANG_DIAGNOSTIC_EVENTS, MAX_RESOLVE_DEPTH } from './consts.ts'; -import { createLangDiagnostics, emitLangDiagnostic } from './diagnostics.ts'; -import { isLangRef, isLangRecord } from './guards.ts'; import { + createLangDiagnostics, + deepMerge, + emitLangDiagnostic, + ID_PREFIX, interpolateTemplate, - resolvePath, + isLangBranch, + isLangRef, + isLangRecord, + LANG_DIAGNOSTIC_EVENTS, + LANG_ERRORS, + MAX_RESOLVE_DEPTH, parseLangRef, parsePathFallback, - deepMerge, - isLangBranch, - resolveLocaleInRecord -} from './helpers.ts'; + resolveLocaleInRecord, + resolvePath, + type EngineLang, + type LangBranch, + type LangFn, + type LangNode, + type LangParams, + type LangRecord, + type LangString, + type SupportedLocale +} from '$libs/lang'; import { isDev as DEV } from '$libs/env'; import type { Logger } from '$libs/logr'; diff --git a/src/arts/lang/index.ts b/src/arts/lang/index.ts index a1ca177..57fda93 100644 --- a/src/arts/lang/index.ts +++ b/src/arts/lang/index.ts @@ -1,71 +1,73 @@ -// Public surface of the lang artifact. Named re-exports (not `export *`) so -// the bundler can statically prove which symbols are reached from a given -// import and tree-shake the rest. Active variants (`createActiveLang`, -// `createActiveMonoLang`) live in their own `.svelte.ts` files and must be -// imported from those paths directly so this barrel stays runes-free. +// Public surface of the lang artifact. The pure contracts (types, +// constants, helpers, plural rules, JSON serialization, diagnostics +// builders, error catalog) live in `$libs/lang`. This barrel mirrors +// them for back-compat — existing `import … from '$lang'` imports +// continue to resolve identically — and adds the runtime engine +// (`createEngineLang`). +// +// Active wrappers (`createActiveLang`, `createActiveMonoLang`) are not +// re-exported here on purpose; they live in `.svelte.ts` files and must +// be imported from those paths directly so this barrel stays +// runes-free. export { - ID_PREFIX, + asLangString, + createLangDiagnostics, + deepMerge, + emitLangDiagnostic, + getPluralConfig, ID_FALLBACK_SEPARATOR, + ID_PREFIX, + interpolateTemplate, + isLangBranch, + isLangRef, + isLangRecord, + isLangString, + JSONToLangNode, LANG_DIAGNOSTIC_EVENTS, + LANG_ERRORS, + langNodeToJSON, LOGGER_CATEGORY, + makeLangRecord, MAX_RESOLVE_DEPTH, - MONO_LANG_CATEGORY -} from './consts.ts'; -export { createLangDiagnostics, emitLangDiagnostic } from './diagnostics.ts'; - -export { isLangRef, isLangRecord, isLangString } from './guards.ts'; + MONO_LANG_CATEGORY, + p, + parseLangRef, + parsePathFallback, + pluralRule, + resolveLocaleInRecord, + resolvePath, + resolveRecordFallback +} from '$libs/lang'; export type { + ActiveLang, + EngineLang, + GetTypeAtPath, + HasParams, LangBase, - SupportedLocale, - LangRef, - LangRecord, - LangParams, - LangFn, - LangPluralFn, - LangValue, LangBranch, + LangDiagnosticEvent, + LangDiagnosticMeta, + LangDiagnostics, + LangDiagnosticType, + LangFn, + LangJSONObject, + LangJSONValue, LangNode, + LangParams, + LangPluralFn, + LangRecord, + LangRef, LangString, + LangValue, LeafPaths, - GetTypeAtPath, ParamsFor, - HasParams, - PluralForms, + PluralCategory, PluralConfig, - EngineLang, - ActiveLang -} from './types.ts'; -export type { - LangDiagnosticEvent, - LangDiagnosticMeta, - LangDiagnostics, - LangDiagnosticType -} from './diagnostics.ts'; - -export { pluralRule } from './plural_rules.ts'; -export type { PluralCategory } from './plural_rules.ts'; - -export { p, getPluralConfig } from './plural.ts'; - -export { - resolvePath, - parseLangRef, - parsePathFallback, - makeLangRecord, - asLangString, - interpolateTemplate, - resolveRecordFallback, - resolveLocaleInRecord, - deepMerge, - isLangBranch -} from './helpers.ts'; -export type { RecordLookup } from './helpers.ts'; + PluralForms, + RecordLookup, + SupportedLocale +} from '$libs/lang'; export { createEngineLang } from './engine-lang.ts'; - -export { langNodeToJSON, JSONToLangNode } from './json.ts'; -export type { LangJSONValue, LangJSONObject } from './json.ts'; - -export { LANG_ERRORS } from './errors.ts'; diff --git a/src/arts/lang/mono-lang.svelte.ts b/src/arts/lang/mono-lang.svelte.ts index 9c1ce8f..c161559 100644 --- a/src/arts/lang/mono-lang.svelte.ts +++ b/src/arts/lang/mono-lang.svelte.ts @@ -1,19 +1,24 @@ import { SvelteSet } from 'svelte/reactivity'; -import { interpolateTemplate, parseLangRef, parsePathFallback } from './helpers.ts'; -import { isLangRecord, isLangRef } from './guards.ts'; +import { + createLangDiagnostics, + emitLangDiagnostic, + interpolateTemplate, + isLangRecord, + isLangRef, + LANG_DIAGNOSTIC_EVENTS, + MONO_LANG_CATEGORY, + parseLangRef, + parsePathFallback, + type ActiveLang, + type LangNode, + type LangParams, + type LangString, + type SupportedLocale +} from '$libs/lang'; import { isDev as DEV } from '$libs/env'; -import { LANG_DIAGNOSTIC_EVENTS, MONO_LANG_CATEGORY } from './consts.ts'; -import { createLangDiagnostics, emitLangDiagnostic } from './diagnostics.ts'; -import type { - ActiveLang, - LangNode, - LangParams, - LangString, - SupportedLocale -} from './types.ts'; import type { Logger } from '$libs/logr'; -export { MONO_LANG_CATEGORY } from './consts.ts'; +export { MONO_LANG_CATEGORY } from '$libs/lang'; /** * Default initial locale for `createActiveMonoLang()`. The mono lang does not diff --git a/src/arts/lang/test/lang.test.ts b/src/arts/lang/test/lang.test.ts index 337fa59..d52f935 100644 --- a/src/arts/lang/test/lang.test.ts +++ b/src/arts/lang/test/lang.test.ts @@ -13,22 +13,27 @@ */ import { describe, it, expect, vi } from 'vitest'; -import { isLangRef, isLangRecord, isLangString } from '../guards.ts'; import { + deepMerge, + getPluralConfig, + interpolateTemplate, + isLangRef, + isLangRecord, + isLangString, + JSONToLangNode, + langNodeToJSON, + makeLangRecord, + p, parseLangRef, parsePathFallback, - resolvePath, - makeLangRecord, - interpolateTemplate, + pluralRule, resolveLocaleInRecord, + resolvePath, resolveRecordFallback, - deepMerge -} from '../helpers.ts'; -import { pluralRule } from '../plural_rules.ts'; -import { p, getPluralConfig } from '../plural.ts'; + type LangBranch, + type LangNode +} from '$libs/lang'; import { createEngineLang } from '../engine-lang.ts'; -import { langNodeToJSON, JSONToLangNode } from '../json.ts'; -import type { LangNode, LangBranch } from '../types.ts'; import type { Logger } from '$libs/logr'; function createTestLogger(overrides: Partial = {}): Logger { diff --git a/src/arts/sium/engine-resolver.ts b/src/arts/sium/engine-resolver.ts index 3ac187f..20716e6 100644 --- a/src/arts/sium/engine-resolver.ts +++ b/src/arts/sium/engine-resolver.ts @@ -1,5 +1,10 @@ -import type { EngineLang, LangParams, SupportedLocale } from '$lang'; -import { ID_FALLBACK_SEPARATOR, parseLangRef } from '$lang'; +import { + ID_FALLBACK_SEPARATOR, + parseLangRef, + type EngineLang, + type LangParams, + type SupportedLocale +} from '$libs/lang'; import { SIUM_DIAGNOSTIC_EVENTS } from './consts'; import { emitSiumDiagnostic, type SiumDiagnostics } from './diagnostics'; import { resolve as resolveFallbackTemplate } from './langs'; diff --git a/src/arts/sium/engine-sium.ts b/src/arts/sium/engine-sium.ts index 80214d5..d9eadab 100644 --- a/src/arts/sium/engine-sium.ts +++ b/src/arts/sium/engine-sium.ts @@ -1,4 +1,4 @@ -import type { EngineLang, LangParams, SupportedLocale } from '$lang'; +import type { EngineLang, LangParams, SupportedLocale } from '$libs/lang'; import type { Logger } from '$libs/logr'; import { createSiumDiagnostics } from './diagnostics'; import { createSiumResolver } from './engine-resolver'; diff --git a/src/arts/sium/langs/langs.ts b/src/arts/sium/langs/langs.ts index d88136e..b0fef2c 100644 --- a/src/arts/sium/langs/langs.ts +++ b/src/arts/sium/langs/langs.ts @@ -1,5 +1,4 @@ -import type { LangNode } from '$lang'; -import { p } from '$lang/plural'; +import { p, type LangNode } from '$libs/lang'; const lengthPlural = p({ en: { diff --git a/src/arts/timr/active-timers.svelte.ts b/src/arts/timr/active-timers.svelte.ts index f170832..b7278dc 100644 --- a/src/arts/timr/active-timers.svelte.ts +++ b/src/arts/timr/active-timers.svelte.ts @@ -15,7 +15,7 @@ */ import { createEngineTimers } from './engine-timers.ts'; -import type { EngineTimers, EngineTimersOptions, TimerEntrySnapshot } from './types.ts'; +import type { EngineTimers, EngineTimersOptions, TimerEntrySnapshot } from '$libs/timers'; export interface ActiveTimers extends EngineTimers { readonly size: number; diff --git a/src/arts/timr/clock.ts b/src/arts/timr/clock.ts index a7e621a..7308b40 100644 --- a/src/arts/timr/clock.ts +++ b/src/arts/timr/clock.ts @@ -1,4 +1,4 @@ -import type { TimerClock, TimerNativeHandle } from './types.ts'; +import type { TimerClock, TimerNativeHandle } from '$libs/timers'; /** * Default `TimerClock` backed by `globalThis.setTimeout` / diff --git a/src/arts/timr/consts.ts b/src/arts/timr/consts.ts index 222e5b5..6b114ae 100644 --- a/src/arts/timr/consts.ts +++ b/src/arts/timr/consts.ts @@ -1,74 +1,39 @@ /** - * Public constants for `arts/timr`. + * Engine-internal constants for `arts/timr`. * - * Per the locked-in rule (no hardcoded literals in engine bodies), every - * status / kind / discriminator string the engine returns lives here as - * a named export. The corresponding TypeScript types in `types.ts` - * derive from these via `typeof CONST` so the literal lives in one - * place. + * Public values that appear in the artifact's type contract live in + * `$libs/timers` (re-exported here for back-compat so existing + * `import … from './consts.ts'` calls within the engine keep + * resolving). Engine-local strings (method names, error names, error + * message prefixes, log message prefixes) stay in this file. */ -/** Logger category emitted by the engine. */ -export const LOGGER_CATEGORY = 'timr'; - -export const TIMER_DIAGNOSTIC_EVENTS = { - LISTENER_THREW: 'listener_threw', - SCHEDULE_AT_PAST: 'schedule_at_past', - TASK_FAILED: 'task_failed' -} as const; +export { + DEFAULT_TIMER_SCOPE_SEPARATOR, + LOGGER_CATEGORY, + TIMER_DIAGNOSTIC_EVENTS, + TIMER_EVENT_CANCELLED, + TIMER_EVENT_COMPLETED, + TIMER_EVENT_DISPOSED, + TIMER_EVENT_FAILED, + TIMER_EVENT_RUNNING, + TIMER_EVENT_SCHEDULED, + TIMER_KIND_INTERVAL, + TIMER_KIND_TIMEOUT, + TIMER_KINDS, + TIMER_STATUS_CANCELLED, + TIMER_STATUS_COMPLETED, + TIMER_STATUS_FAILED, + TIMER_STATUS_PENDING, + TIMER_STATUS_RUNNING, + TIMER_STATUSES +} from '$libs/timers'; export const ENGINE_METHOD_SCHEDULE = 'schedule'; export const ENGINE_METHOD_SCHEDULE_AT = 'scheduleAt'; export const ENGINE_METHOD_CANCEL = 'cancel'; export const ENGINE_METHOD_CANCEL_ALL = 'cancelAll'; -// ── Timer status ─────────────────────────────────────────────────────────── -// -// Discrete values of `TimerStatus`. The engine moves an entry through -// `pending → running → (completed | failed)` for one-shots, and back to -// `pending` for intervals between ticks. `cancelled` is terminal. - -export const TIMER_STATUS_PENDING = 'pending'; -export const TIMER_STATUS_RUNNING = 'running'; -export const TIMER_STATUS_CANCELLED = 'cancelled'; -export const TIMER_STATUS_COMPLETED = 'completed'; -export const TIMER_STATUS_FAILED = 'failed'; - -export const TIMER_STATUSES = [ - TIMER_STATUS_PENDING, - TIMER_STATUS_RUNNING, - TIMER_STATUS_CANCELLED, - TIMER_STATUS_COMPLETED, - TIMER_STATUS_FAILED -] as const; - -// ── Timer kind ───────────────────────────────────────────────────────────── - -export const TIMER_KIND_TIMEOUT = 'timeout'; -export const TIMER_KIND_INTERVAL = 'interval'; - -export const TIMER_KINDS = [TIMER_KIND_TIMEOUT, TIMER_KIND_INTERVAL] as const; - -// ── Event types ──────────────────────────────────────────────────────────── -// -// Events emitted via `EngineTimers.onChange`. Every state transition -// surfaces as exactly one of these. The Active wrapper subscribes to -// the same channel external listeners use — no parallel notification -// path. - -// Timer event names. Scoped with the `timr.` prefix for log/devtool -// disambiguation against cache, connection or any other emitter. -export const TIMER_EVENT_SCHEDULED = 'timr.scheduled'; -export const TIMER_EVENT_RUNNING = 'timr.running'; -export const TIMER_EVENT_COMPLETED = 'timr.completed'; -export const TIMER_EVENT_FAILED = 'timr.failed'; -export const TIMER_EVENT_CANCELLED = 'timr.cancelled'; -export const TIMER_EVENT_DISPOSED = 'timr.disposed'; - -// ── Scope helpers ────────────────────────────────────────────────────────── - -export const DEFAULT_TIMER_SCOPE_SEPARATOR = ':'; - // ── Backoff defaults ─────────────────────────────────────────────────────── export { diff --git a/src/arts/timr/diagnostics.ts b/src/arts/timr/diagnostics.ts index cd7e573..ded13cc 100644 --- a/src/arts/timr/diagnostics.ts +++ b/src/arts/timr/diagnostics.ts @@ -14,7 +14,7 @@ import { taskFailedMessage } from './consts.ts'; import type { Logger } from '$libs/logr'; -import type { TimerEvent } from './types.ts'; +import type { TimerEvent } from '$libs/timers'; export type TimerDiagnosticType = (typeof TIMER_DIAGNOSTIC_EVENTS)[keyof typeof TIMER_DIAGNOSTIC_EVENTS]; diff --git a/src/arts/timr/engine-timers.ts b/src/arts/timr/engine-timers.ts index a864834..6c22b2b 100644 --- a/src/arts/timr/engine-timers.ts +++ b/src/arts/timr/engine-timers.ts @@ -62,7 +62,7 @@ import type { TimerKind, TimerOptions, TimerTask -} from './types.ts'; +} from '$libs/timers'; export function createEngineTimers(options: EngineTimersOptions = {}): EngineTimers { const clock: TimerClock = options.clock ?? createSystemTimerClock(); diff --git a/src/arts/timr/index.ts b/src/arts/timr/index.ts index 032d2b1..1115f41 100644 --- a/src/arts/timr/index.ts +++ b/src/arts/timr/index.ts @@ -70,4 +70,4 @@ export type { TimerStatus, TimerTask, TimerTaskContext -} from './types.ts'; +} from '$libs/timers'; diff --git a/src/arts/timr/test/engine-timers.test.ts b/src/arts/timr/test/engine-timers.test.ts index c760652..d3556cb 100644 --- a/src/arts/timr/test/engine-timers.test.ts +++ b/src/arts/timr/test/engine-timers.test.ts @@ -21,7 +21,7 @@ import { isTimrInvalidDelayError, isTimrInvalidKeyError } from '../errors.ts'; -import type { TimerClock, TimerEvent, TimerNativeHandle, TimerTaskContext } from '../types.ts'; +import type { TimerClock, TimerEvent, TimerNativeHandle, TimerTaskContext } from '$libs/timers'; // ── Fake clock ────────────────────────────────────────────────────────────── diff --git a/src/arts/timr/timer-cancel.ts b/src/arts/timr/timer-cancel.ts index a2dd423..553e896 100644 --- a/src/arts/timr/timer-cancel.ts +++ b/src/arts/timr/timer-cancel.ts @@ -4,7 +4,7 @@ import { timerEntrySnapshot, type InternalTimerEntry } from './timer-entry.ts'; -import type { TimerClock, TimerEvent } from './types.ts'; +import type { TimerClock, TimerEvent } from '$libs/timers'; export interface CancelTimerEntryInput { readonly entry: InternalTimerEntry; diff --git a/src/arts/timr/timer-entry.ts b/src/arts/timr/timer-entry.ts index 65d158d..addcc97 100644 --- a/src/arts/timr/timer-entry.ts +++ b/src/arts/timr/timer-entry.ts @@ -6,7 +6,7 @@ import type { TimerStatus, TimerTask, TimerTaskContext -} from './types.ts'; +} from '$libs/timers'; export interface InternalTimerEntry { id: number; diff --git a/src/arts/timr/timer-events.ts b/src/arts/timr/timer-events.ts index 3214e00..8ba052e 100644 --- a/src/arts/timr/timer-events.ts +++ b/src/arts/timr/timer-events.ts @@ -1,6 +1,6 @@ import { TIMER_DIAGNOSTIC_EVENTS } from './consts.ts'; import { emitTimerDiagnostic, type TimerDiagnostics } from './diagnostics.ts'; -import type { TimerEvent, TimerListener } from './types.ts'; +import type { TimerEvent, TimerListener } from '$libs/timers'; export interface TimerEventBus { emit(event: TimerEvent): void; diff --git a/src/arts/timr/timer-handle.ts b/src/arts/timr/timer-handle.ts index b8aee18..61d6680 100644 --- a/src/arts/timr/timer-handle.ts +++ b/src/arts/timr/timer-handle.ts @@ -2,7 +2,7 @@ import { inactiveTimerErrorMessage, TIMER_STATUS_PENDING, TIMER_STATUS_RUNNING } import { TimrInactiveTimerError } from './errors.ts'; import type { InternalTimerEntry } from './timer-entry.ts'; import { assertTimerDelay } from './timer-validation.ts'; -import type { TimerClock, TimerHandle } from './types.ts'; +import type { TimerClock, TimerHandle } from '$libs/timers'; export interface TimerHandleRuntime { readonly entries: Map; diff --git a/src/arts/timr/timer-runner.ts b/src/arts/timr/timer-runner.ts index 011a0ab..471a5a8 100644 --- a/src/arts/timr/timer-runner.ts +++ b/src/arts/timr/timer-runner.ts @@ -18,7 +18,7 @@ import { timerEntrySnapshot, type InternalTimerEntry } from './timer-entry.ts'; -import type { TimerClock, TimerEvent } from './types.ts'; +import type { TimerClock, TimerEvent } from '$libs/timers'; interface TimerRunnerOptions { readonly entries: Map; diff --git a/src/libs/buss/diagnostics.ts b/src/libs/buss/diagnostics.ts index 31fc5d4..cca3285 100644 --- a/src/libs/buss/diagnostics.ts +++ b/src/libs/buss/diagnostics.ts @@ -1,37 +1,92 @@ import { + BUS_DIAGNOSTIC_EVENTS, BUS_LOG_CATEGORY, BUS_LOG_MSG_LISTENER_FAILED, BUS_LOG_MSG_LISTENER_LEAK_WARNING } from './consts.ts'; import type { BusEnvelope, BusListenerFailure } from './types.ts'; -import type { Logger } from '$libs/logr'; +import { + LogLevel, + createCatalogDiagnostics, + type DiagnosticCatalog, + type DiagnosticEvent, + type Diagnostics, + type Logger +} from '$libs/logr'; + +export type BusDiagnosticType = + | typeof BUS_DIAGNOSTIC_EVENTS.LISTENER_FAILED + | typeof BUS_DIAGNOSTIC_EVENTS.LISTENER_LEAK_WARNING; + +export type BusDiagnosticMeta = + | { + readonly envelope: BusEnvelope; + readonly listenerId?: string; + readonly eventType: string; + readonly error: unknown; + } + | { + readonly type: string; + readonly count: number; + readonly max: number; + }; + +export type BusDiagnosticEvent = DiagnosticEvent; +export type BusDiagnostics = Diagnostics; + +const BUS_DIAGNOSTIC_LOGS: DiagnosticCatalog = { + [BUS_DIAGNOSTIC_EVENTS.LISTENER_FAILED]: { + level: LogLevel.ERROR, + message: BUS_LOG_MSG_LISTENER_FAILED + }, + [BUS_DIAGNOSTIC_EVENTS.LISTENER_LEAK_WARNING]: { + level: LogLevel.WARN, + message: BUS_LOG_MSG_LISTENER_LEAK_WARNING + } +}; + +export function createBusDiagnostics(logger?: Logger): BusDiagnostics { + return createCatalogDiagnostics({ + logger, + defaultCategory: BUS_LOG_CATEGORY, + catalog: BUS_DIAGNOSTIC_LOGS + }); +} + +export function emitBusDiagnostic( + diagnostics: BusDiagnostics, + type: BusDiagnosticType, + meta: BusDiagnosticMeta +): void { + diagnostics.emit({ + artifact: BUS_LOG_CATEGORY, + type, + meta + }); +} -export function logBusListenerFailure(input: { - readonly logger: Logger; +export function emitBusListenerFailureDiagnostic(input: { + readonly diagnostics: BusDiagnostics; readonly envelope: BusEnvelope; readonly failure: BusListenerFailure; }): void { - input.logger.error(BUS_LOG_CATEGORY, BUS_LOG_MSG_LISTENER_FAILED, { - error: input.failure.error, - context: { - envelope: input.envelope, - listenerId: input.failure.listenerId, - eventType: input.failure.type - } + emitBusDiagnostic(input.diagnostics, BUS_DIAGNOSTIC_EVENTS.LISTENER_FAILED, { + envelope: input.envelope, + ...(input.failure.listenerId !== undefined ? { listenerId: input.failure.listenerId } : {}), + eventType: input.failure.type, + error: input.failure.error }); } -export function logBusListenerLeakWarning(input: { - readonly logger: Logger; +export function emitBusListenerLeakWarningDiagnostic(input: { + readonly diagnostics: BusDiagnostics; readonly type: string; readonly count: number; readonly max: number; }): void { - input.logger.warn(BUS_LOG_CATEGORY, BUS_LOG_MSG_LISTENER_LEAK_WARNING, { - context: { - type: input.type, - count: input.count, - max: input.max - } + emitBusDiagnostic(input.diagnostics, BUS_DIAGNOSTIC_EVENTS.LISTENER_LEAK_WARNING, { + type: input.type, + count: input.count, + max: input.max }); } diff --git a/src/libs/buss/index.ts b/src/libs/buss/index.ts index b2a33f9..d7d13ba 100644 --- a/src/libs/buss/index.ts +++ b/src/libs/buss/index.ts @@ -49,7 +49,18 @@ export { isBusReentrancyLimitError } from './errors.ts'; -export { logBusListenerFailure, logBusListenerLeakWarning } from './diagnostics.ts'; +export { + createBusDiagnostics, + emitBusDiagnostic, + emitBusListenerFailureDiagnostic, + emitBusListenerLeakWarningDiagnostic +} from './diagnostics.ts'; +export type { + BusDiagnosticEvent, + BusDiagnosticMeta, + BusDiagnostics, + BusDiagnosticType +} from './diagnostics.ts'; export { isBusAnyEvent } from './matching.ts'; export { SILENT_BUS } from './silent-bus.ts'; diff --git a/src/arts/adom/apply.ts b/src/libs/dom/apply.ts similarity index 64% rename from src/arts/adom/apply.ts rename to src/libs/dom/apply.ts index 3052ba5..1a4e693 100644 --- a/src/arts/adom/apply.ts +++ b/src/libs/dom/apply.ts @@ -1,8 +1,10 @@ /** - * `apply` / `remove` — the only mutation surface of `ActiveDom`. + * `apply` / `removeAttrs` — pure DOM attribute writers. * - * `ActiveDom` doesn't know product frameworks. It receives instructions - * already resolved by upper layers and applies them to the DOM. Nothing else. + * Lives in `libs/dom` because it has no runtime state, no Svelte + * reactivity, and no dependency on other artifacts. Both the `arts/adom` + * engine (for its `apply()` method) and `arts/fend` (when no + * `ActiveDom` is wired) consume these helpers directly. * * Contract: * - `string | number` → `setAttribute(name, String(value))` @@ -12,7 +14,7 @@ * No event dispatch, no batching, no observation. Pure mutations. */ -import { isBrowser } from '$libs/dom'; +import { isBrowser } from './core.ts'; export type DomAttrValue = string | number | boolean | null | undefined; @@ -21,6 +23,15 @@ export type StructuralChange = { attrs: Record; }; +/** + * Minimal applier contract. Anything that can absorb a + * `StructuralChange` qualifies — the full `ActiveDom` engine, a test + * spy, a no-op stub, or the bare `applyChange` helper exposed below. + */ +export interface DomApplier { + apply(change: StructuralChange): void; +} + function writeAttr(target: HTMLElement, name: string, value: DomAttrValue): void { if (value === false || value === null || value === undefined) { target.removeAttribute(name); diff --git a/src/libs/dom/index.ts b/src/libs/dom/index.ts index 1330846..8c00bb6 100644 --- a/src/libs/dom/index.ts +++ b/src/libs/dom/index.ts @@ -1,3 +1,4 @@ +export * from './apply' export * from './core' export * from './errors' export * from './focus' diff --git a/src/arts/adom/test/apply.test.ts b/src/libs/dom/test/apply.test.ts similarity index 97% rename from src/arts/adom/test/apply.test.ts rename to src/libs/dom/test/apply.test.ts index e55a9ab..0b56343 100644 --- a/src/arts/adom/test/apply.test.ts +++ b/src/libs/dom/test/apply.test.ts @@ -2,7 +2,7 @@ import { beforeEach, describe, expect, it } from 'vitest'; -import { applyChange, removeAttrs } from '../apply'; +import { applyChange, removeAttrs } from '$libs/dom'; describe('apply', () => { let target: HTMLElement; diff --git a/src/arts/lang/consts.ts b/src/libs/lang/consts.ts similarity index 100% rename from src/arts/lang/consts.ts rename to src/libs/lang/consts.ts diff --git a/src/arts/lang/diagnostics.ts b/src/libs/lang/diagnostics.ts similarity index 100% rename from src/arts/lang/diagnostics.ts rename to src/libs/lang/diagnostics.ts diff --git a/src/arts/lang/errors.ts b/src/libs/lang/errors.ts similarity index 100% rename from src/arts/lang/errors.ts rename to src/libs/lang/errors.ts diff --git a/src/arts/lang/guards.ts b/src/libs/lang/guards.ts similarity index 100% rename from src/arts/lang/guards.ts rename to src/libs/lang/guards.ts diff --git a/src/arts/lang/helpers.ts b/src/libs/lang/helpers.ts similarity index 100% rename from src/arts/lang/helpers.ts rename to src/libs/lang/helpers.ts diff --git a/src/libs/lang/index.ts b/src/libs/lang/index.ts new file mode 100644 index 0000000..56b39c1 --- /dev/null +++ b/src/libs/lang/index.ts @@ -0,0 +1,79 @@ +/** + * `libs/lang` — pure contracts and helpers for the lang artifact. + * + * Mirrors the `libs/buss` / `libs/logr` / `libs/dom` pattern: this module + * exports interfaces, generic constants, error catalogs, pure helpers, + * plural rules, JSON serialization, and diagnostics builders. It does + * NOT include the engine implementation; that lives in + * `arts/lang/engine-lang.ts` and the active wrappers in + * `arts/lang/active-lang.svelte.ts` / `arts/lang/mono-lang.svelte.ts`. + * + * Modules that need types or pure helpers from lang import from + * `$libs/lang`. Only the composition root (`aapp`) and tests touch + * `$lang` to reach the engine factories. + */ + +export { + ID_FALLBACK_SEPARATOR, + ID_PREFIX, + LANG_DIAGNOSTIC_EVENTS, + LOGGER_CATEGORY, + MAX_RESOLVE_DEPTH, + MONO_LANG_CATEGORY +} from './consts.ts'; + +export { createLangDiagnostics, emitLangDiagnostic } from './diagnostics.ts'; +export type { + LangDiagnosticEvent, + LangDiagnosticMeta, + LangDiagnostics, + LangDiagnosticType +} from './diagnostics.ts'; + +export { LANG_ERRORS } from './errors.ts'; + +export { isLangRef, isLangRecord, isLangString } from './guards.ts'; + +export { + asLangString, + deepMerge, + interpolateTemplate, + isLangBranch, + makeLangRecord, + parseLangRef, + parsePathFallback, + resolveLocaleInRecord, + resolvePath, + resolveRecordFallback +} from './helpers.ts'; +export type { RecordLookup } from './helpers.ts'; + +export { JSONToLangNode, langNodeToJSON } from './json.ts'; +export type { LangJSONObject, LangJSONValue } from './json.ts'; + +export { getPluralConfig, p } from './plural.ts'; + +export { pluralRule } from './plural_rules.ts'; +export type { PluralCategory } from './plural_rules.ts'; + +export type { + ActiveLang, + EngineLang, + GetTypeAtPath, + HasParams, + LangBase, + LangBranch, + LangFn, + LangNode, + LangParams, + LangPluralFn, + LangRecord, + LangRef, + LangString, + LangValue, + LeafPaths, + ParamsFor, + PluralConfig, + PluralForms, + SupportedLocale +} from './types.ts'; diff --git a/src/arts/lang/json.ts b/src/libs/lang/json.ts similarity index 100% rename from src/arts/lang/json.ts rename to src/libs/lang/json.ts diff --git a/src/arts/lang/plural.ts b/src/libs/lang/plural.ts similarity index 100% rename from src/arts/lang/plural.ts rename to src/libs/lang/plural.ts diff --git a/src/arts/lang/plural_rules.ts b/src/libs/lang/plural_rules.ts similarity index 100% rename from src/arts/lang/plural_rules.ts rename to src/libs/lang/plural_rules.ts diff --git a/src/arts/lang/types.ts b/src/libs/lang/types.ts similarity index 100% rename from src/arts/lang/types.ts rename to src/libs/lang/types.ts diff --git a/src/libs/timers/consts.ts b/src/libs/timers/consts.ts new file mode 100644 index 0000000..a762ab3 --- /dev/null +++ b/src/libs/timers/consts.ts @@ -0,0 +1,66 @@ +/** + * Public constants of the timers contract. + * + * Mirrors the `libs/buss` / `libs/lang` pattern: the discrete values + * exposed in public types live here so consumers can import them + * without crossing into `arts/timr`. Engine-internal constants (error + * messages, method names, log message prefixes) stay in + * `arts/timr/consts.ts`. + */ + +/** Logger category emitted by the engine. */ +export const LOGGER_CATEGORY = 'timr'; + +export const TIMER_DIAGNOSTIC_EVENTS = { + LISTENER_THREW: 'listener_threw', + SCHEDULE_AT_PAST: 'schedule_at_past', + TASK_FAILED: 'task_failed' +} as const; + +// ── Timer status ─────────────────────────────────────────────────────────── +// +// Discrete values of `TimerStatus`. The engine moves an entry through +// `pending → running → (completed | failed)` for one-shots, and back to +// `pending` for intervals between ticks. `cancelled` is terminal. + +export const TIMER_STATUS_PENDING = 'pending'; +export const TIMER_STATUS_RUNNING = 'running'; +export const TIMER_STATUS_CANCELLED = 'cancelled'; +export const TIMER_STATUS_COMPLETED = 'completed'; +export const TIMER_STATUS_FAILED = 'failed'; + +export const TIMER_STATUSES = [ + TIMER_STATUS_PENDING, + TIMER_STATUS_RUNNING, + TIMER_STATUS_CANCELLED, + TIMER_STATUS_COMPLETED, + TIMER_STATUS_FAILED +] as const; + +// ── Timer kind ───────────────────────────────────────────────────────────── + +export const TIMER_KIND_TIMEOUT = 'timeout'; +export const TIMER_KIND_INTERVAL = 'interval'; + +export const TIMER_KINDS = [TIMER_KIND_TIMEOUT, TIMER_KIND_INTERVAL] as const; + +// ── Event types ──────────────────────────────────────────────────────────── +// +// Events emitted via `EngineTimers.onChange`. Every state transition +// surfaces as exactly one of these. The Active wrapper subscribes to +// the same channel external listeners use — no parallel notification +// path. +// +// Names are scoped with the `timr.` prefix for log/devtool +// disambiguation against cache, connection or any other emitter. + +export const TIMER_EVENT_SCHEDULED = 'timr.scheduled'; +export const TIMER_EVENT_RUNNING = 'timr.running'; +export const TIMER_EVENT_COMPLETED = 'timr.completed'; +export const TIMER_EVENT_FAILED = 'timr.failed'; +export const TIMER_EVENT_CANCELLED = 'timr.cancelled'; +export const TIMER_EVENT_DISPOSED = 'timr.disposed'; + +// ── Scope helpers ────────────────────────────────────────────────────────── + +export const DEFAULT_TIMER_SCOPE_SEPARATOR = ':'; diff --git a/src/libs/timers/index.ts b/src/libs/timers/index.ts index 7e546c2..181e410 100644 --- a/src/libs/timers/index.ts +++ b/src/libs/timers/index.ts @@ -1,2 +1,4 @@ -export * from './debounce.ts'; export * from './backoff.ts'; +export * from './consts.ts'; +export * from './debounce.ts'; +export * from './types.ts'; diff --git a/src/arts/timr/types.ts b/src/libs/timers/types.ts similarity index 93% rename from src/arts/timr/types.ts rename to src/libs/timers/types.ts index df848d7..07fa32b 100644 --- a/src/arts/timr/types.ts +++ b/src/libs/timers/types.ts @@ -1,14 +1,13 @@ /** - * Public type contract of `arts/timr`. + * Public type contract of the timers ecosystem. * - * `arts/timr` is a runtime timer scheduler. Singleton-per-App, never - * module-global. Engine layer is runes-free; the Active wrapper layers - * `$state` on top in `active-timers.svelte.ts`. - * - * Other artifacts (`sess`, `conn`, future `cache`/`authn`) consume the - * minimal `TimerScheduler` interface so they accept either an - * `EngineTimers`, an `ActiveTimers`, or a fake clock-driven double in - * tests — none of them depend on Svelte runtime. + * `arts/timr` is a runtime timer scheduler. The engine layer is + * runes-free; the Active wrapper layers `$state` on top in + * `arts/timr/active-timers.svelte.ts`. Consumers (`sess`, `conn`, + * future engines) depend on the minimal `TimerScheduler` interface + * exported here so they accept any compliant implementation — + * `EngineTimers`, `ActiveTimers`, or a fake clock-driven double in + * tests — without depending on Svelte runtime. */ import type { Logger } from '$libs/logr'; @@ -28,7 +27,7 @@ import type { TIMER_STATUS_RUNNING } from './consts.ts'; -export type { BackoffOptions } from '$libs/timers'; +export type { BackoffOptions } from './backoff.ts'; // ============================================================================ // CLOCK