From cdce5de04e307f2d41efd4a7c2f6fa72734b2714 Mon Sep 17 00:00:00 2001 From: dev Date: Wed, 29 Apr 2026 18:08:30 +0200 Subject: [PATCH] Move timer entry runtime helpers --- NEXT_STEPS.md | 1 + src/arts/timr/engine-timers.ts | 46 +++++++--------------------------- src/arts/timr/timer-entry.ts | 37 ++++++++++++++++++++++++++- 3 files changed, 46 insertions(+), 38 deletions(-) diff --git a/NEXT_STEPS.md b/NEXT_STEPS.md index fbfcb08..2baecf5 100644 --- a/NEXT_STEPS.md +++ b/NEXT_STEPS.md @@ -27,6 +27,7 @@ Estado al cierre: - `arts/timr/engine-timers.ts` delega fan-out de listeners y diagnostico de listeners en `timer-events.ts`. - `arts/timr/engine-timers.ts` delega la construccion de entradas internas en `timer-entry.ts`. - `arts/timr/engine-timers.ts` delega cancelacion y seleccion por scope en `timer-cancel.ts`. + - `arts/timr/engine-timers.ts` delega entrada viva, contexto de task y max-runs en `timer-entry.ts`. - Integracion total ampliada: `Auth.signOut()` valida anonimizacion, invalidacion de `Permissions` y evento `Cache.invalidate`. - Tanda focalizada verde: `npx vitest run src/arts/conn src/libs/cach src/arts/cach src/svrs/cach src/arts/auth src/svrs/auth src/libs/auth src/arts/aapp/test/ecosystem.integration.test.ts` -> 19 archivos, 82 tests. - `/test/ecosystem` revisado en navegador: carga sin errores de consola, `ar` cambia a `rtl`, Formats se actualiza por locale, Perm cambia con rol `viewer`, Cach re-scopea por locale y Conn loopback publica/recibe. diff --git a/src/arts/timr/engine-timers.ts b/src/arts/timr/engine-timers.ts index 5fae6fa..e1972db 100644 --- a/src/arts/timr/engine-timers.ts +++ b/src/arts/timr/engine-timers.ts @@ -48,6 +48,9 @@ import { TimrDuplicateKeyError } from './errors.ts'; import { cancelTimerEntry, collectTimerKeysInScope } from './timer-cancel.ts'; import { createInternalTimerEntry, + createTimerTaskContext, + getLiveTimerEntry, + hasTimerReachedMaxRuns, timerEntrySnapshot, type InternalTimerEntry } from './timer-entry.ts'; @@ -68,8 +71,7 @@ import type { TimerIntervalOptions, TimerKind, TimerOptions, - TimerTask, - TimerTaskContext + TimerTask } from './types.ts'; export function createEngineTimers(options: EngineTimersOptions = {}): EngineTimers { @@ -94,19 +96,6 @@ export function createEngineTimers(options: EngineTimersOptions = {}): EngineTim // ── Entry lifecycle ───────────────────────────────────────────────────── - /** - * Look up an entry only if the captured tuple still matches. This is - * the core of the race-safety contract — every callback uses it. - */ - function getLiveEntry(id: number, key: string, version: number): InternalTimerEntry | null { - const current = entries.get(key); - if (current === undefined) return null; - if (current.id !== id) return null; - if (current.version !== version) return null; - if (current.controller.signal.aborted) return null; - return current; - } - /** * Arm the native timeout for the given entry. Captures the tuple in * the closure — `runEntry` will reject the callback if the entry has @@ -122,21 +111,8 @@ export function createEngineTimers(options: EngineTimersOptions = {}): EngineTim }, delayMs); } - function makeContext(entry: InternalTimerEntry, firedAt: number): TimerTaskContext { - return { - key: entry.key, - kind: entry.kind, - scheduledAt: entry.scheduledAt, - dueAt: entry.dueAt, - firedAt, - driftMs: firedAt - entry.dueAt, - runCount: entry.runCount, - signal: entry.controller.signal - }; - } - async function runEntry(id: number, key: string, version: number): Promise { - const entry = getLiveEntry(id, key, version); + const entry = getLiveTimerEntry(entries, id, key, version); if (entry === null) return; if (entry.status !== TIMER_STATUS_PENDING) return; @@ -145,7 +121,7 @@ export function createEngineTimers(options: EngineTimersOptions = {}): EngineTim entry.lastFiredAt = firedAt; entry.runCount += 1; entry.native = null; - const ctx = makeContext(entry, firedAt); + const ctx = createTimerTaskContext(entry, firedAt); emit({ type: TIMER_EVENT_RUNNING, entry: timerEntrySnapshot(entry) }); const isInterval = entry.kind === TIMER_KIND_INTERVAL; @@ -154,7 +130,7 @@ export function createEngineTimers(options: EngineTimersOptions = {}): EngineTim // Branch A — interval with awaitTask=false: arm the next tick BEFORE // running so we honour cadence regardless of how long the task // takes. Stale-callback guard still applies to next tick. - if (isInterval && !awaitTask && !hasReachedMaxRuns(entry)) { + if (isInterval && !awaitTask && !hasTimerReachedMaxRuns(entry)) { scheduleNextIntervalTick(entry); } @@ -204,7 +180,7 @@ export function createEngineTimers(options: EngineTimersOptions = {}): EngineTim // `entry.status` may already be TIMER_STATUS_PENDING if branch A // already armed the next tick; otherwise schedule it now. if (entry.status === TIMER_STATUS_RUNNING) { - if (hasReachedMaxRuns(entry)) { + if (hasTimerReachedMaxRuns(entry)) { entry.status = error === null ? TIMER_STATUS_COMPLETED : TIMER_STATUS_FAILED; entries.delete(entry.key); return; @@ -221,7 +197,7 @@ export function createEngineTimers(options: EngineTimersOptions = {}): EngineTim function scheduleNextIntervalTick(entry: InternalTimerEntry): void { if (entry.intervalMs === null) return; - if (hasReachedMaxRuns(entry)) { + if (hasTimerReachedMaxRuns(entry)) { entry.status = TIMER_STATUS_COMPLETED; entries.delete(entry.key); return; @@ -242,10 +218,6 @@ export function createEngineTimers(options: EngineTimersOptions = {}): EngineTim // ── Public scheduling ─────────────────────────────────────────────────── - function hasReachedMaxRuns(entry: InternalTimerEntry): boolean { - return entry.maxRuns !== undefined && entry.runCount >= entry.maxRuns; - } - function scheduleCommon( key: string, delayMs: number, diff --git a/src/arts/timr/timer-entry.ts b/src/arts/timr/timer-entry.ts index f205f74..65d158d 100644 --- a/src/arts/timr/timer-entry.ts +++ b/src/arts/timr/timer-entry.ts @@ -4,7 +4,8 @@ import type { TimerKind, TimerNativeHandle, TimerStatus, - TimerTask + TimerTask, + TimerTaskContext } from './types.ts'; export interface InternalTimerEntry { @@ -71,6 +72,40 @@ export function createInternalTimerEntry(input: CreateInternalTimerEntryInput): }; } +export function getLiveTimerEntry( + entries: Map, + id: number, + key: string, + version: number +): InternalTimerEntry | null { + const current = entries.get(key); + if (current === undefined) return null; + if (current.id !== id) return null; + if (current.version !== version) return null; + if (current.controller.signal.aborted) return null; + return current; +} + +export function createTimerTaskContext( + entry: InternalTimerEntry, + firedAt: number +): TimerTaskContext { + return { + key: entry.key, + kind: entry.kind, + scheduledAt: entry.scheduledAt, + dueAt: entry.dueAt, + firedAt, + driftMs: firedAt - entry.dueAt, + runCount: entry.runCount, + signal: entry.controller.signal + }; +} + +export function hasTimerReachedMaxRuns(entry: InternalTimerEntry): boolean { + return entry.maxRuns !== undefined && entry.runCount >= entry.maxRuns; +} + export function scopeOfTimerKey(key: string): string { const idx = key.lastIndexOf(DEFAULT_TIMER_SCOPE_SEPARATOR); return idx === -1 ? key : key.slice(0, idx);