stat-sync.ts 2.1 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950
  1. import * as NodeRuntime from "@effect/platform-node/NodeRuntime"
  2. import { Athena } from "@kirincode-ai/stats-core/athena"
  3. import { ModelStatRepo } from "@kirincode-ai/stats-core/domain/model"
  4. import { layer as statsLayer } from "@kirincode-ai/stats-core/runtime"
  5. import { syncStats } from "@kirincode-ai/stats-core/stat-sync"
  6. import { Cause, Duration, Effect, Layer, Schedule } from "effect"
  7. const SYNC_INTERVAL = "1 hour"
  8. const SYNC_INTERVAL_MS = 3_600_000
  9. const runtimeLayer = Layer.mergeAll(statsLayer, Athena.layer)
  10. const daemon = Effect.gen(function* () {
  11. yield* Effect.logInfo("stats sync daemon started")
  12. yield* initialDelay()
  13. // One full pass per UTC day (including the first pass after boot) refreshes the
  14. // whole display window; every other pass only recomputes the current ISO week.
  15. let lastFullDay = ""
  16. const pass = Effect.gen(function* () {
  17. const today = new Date().toISOString().slice(0, 10)
  18. const full = lastFullDay !== today
  19. yield* syncStats({ full })
  20. if (full) lastFullDay = today
  21. }).pipe(
  22. Effect.catchCause((cause) =>
  23. Effect.logWarning(`stats sync failed ${JSON.stringify({ cause: Cause.pretty(cause) })}`),
  24. ),
  25. )
  26. yield* pass.pipe(Effect.repeat(Schedule.fixed(SYNC_INTERVAL)))
  27. }).pipe(Effect.forkScoped)
  28. // A restarted daemon must not immediately re-run the expensive Athena pass; resume
  29. // the hourly cadence from the last completed sync instead. This caps the Athena
  30. // spend of a crash loop at one pass per interval.
  31. const initialDelay = Effect.fnUntraced(function* () {
  32. const modelStats = yield* ModelStatRepo
  33. const lastSynced = yield* modelStats.lastSyncedAt().pipe(Effect.catchCause(() => Effect.succeed(null)))
  34. if (!lastSynced) return
  35. const delayMs = Math.min(SYNC_INTERVAL_MS - (Date.now() - lastSynced.getTime()), SYNC_INTERVAL_MS)
  36. if (delayMs <= 0) return
  37. yield* Effect.logInfo(
  38. `stats sync delaying first pass ${JSON.stringify({ lastSyncedAt: lastSynced.toISOString(), delayMs })}`,
  39. )
  40. yield* Effect.sleep(Duration.millis(delayMs))
  41. })
  42. NodeRuntime.runMain(Layer.launch(Layer.effectDiscard(daemon).pipe(Layer.provide(runtimeLayer))), {
  43. disableErrorReporting: true,
  44. })