import { EnvironmentRegistry } from "@t3tools/client-runtime/connection"; import { EnvironmentRpcSubscriptionObserver, request } from "@t3tools/client-runtime/rpc"; import { type BackgroundScope, type ClientActivityReportInput, type EnvironmentId, WS_METHODS, } from "@t3tools/contracts"; import * as Clock from "effect/Clock"; import * as DateTime from "effect/DateTime"; import * as Effect from "effect/Effect"; import * as Layer from "effect/Layer"; import * as Queue from "effect/Queue"; import * as Schedule from "effect/Schedule"; import * as Stream from "effect/Stream"; import * as SubscriptionRef from "effect/SubscriptionRef"; import { AppState, type AppStateStatus } from "react-native"; import * as MobileStorage from "../persistence/mobile-storage"; import { observeMobileBackgroundActivitySubscription, onRetainedMobileBackgroundScopesChange, retainedMobileBackgroundScopes, } from "./background-activity-scopes"; const REPORT_INTERVAL_MS = 25_000; const LEASE_TTL_MS = 45_000; const BASELINE_SCOPES: ReadonlyArray = [{ type: "provider-status" }]; function normalizeAppState( state: AppStateStatus, ): NonNullable { if (state === "active" || state === "inactive" || state === "background") return state; return "unknown"; } export const mobileBackgroundActivityObserverLayer = Layer.succeed( EnvironmentRpcSubscriptionObserver, EnvironmentRpcSubscriptionObserver.of({ observe: observeMobileBackgroundActivitySubscription, }), ); export const mobileBackgroundActivityReporterLayer = Layer.effectDiscard( Effect.gen(function* () { const registry = yield* EnvironmentRegistry; const storage = yield* MobileStorage.MobileStorage; const clientId = yield* storage.loadOrCreateAgentAwarenessDeviceId.pipe( Effect.map((deviceId) => `mobile-${deviceId}`), Effect.orElseSucceed(() => "ephemeral-mobile-client"), ); const reportRequests = yield* Queue.sliding(1); const requestReport = () => Queue.offerUnsafe(reportRequests, undefined); let appState = AppState.currentState; const report = Effect.gen(function* () { const observedAtMs = yield* Clock.currentTimeMillis; const active = appState === "active"; const entries = yield* SubscriptionRef.get(registry.entries); yield* Effect.forEach( entries.keys(), (environmentId) => registry .run( environmentId, request(WS_METHODS.serverReportClientActivity, { environmentId: environmentId as EnvironmentId, clientId, clientKind: "mobile", visible: active, focused: active, recentlyInteracted: active, appState: normalizeAppState(appState), scopes: [ ...BASELINE_SCOPES, ...retainedMobileBackgroundScopes(environmentId as EnvironmentId), ], ttlMs: LEASE_TTL_MS, observedAt: DateTime.makeUnsafe(observedAtMs), }), ) .pipe(Effect.ignore), { concurrency: "unbounded", discard: true }, ); }).pipe(Effect.withSpan("mobile.backgroundActivity.report")); yield* Effect.acquireRelease( Effect.sync(() => { const removeScopeListener = onRetainedMobileBackgroundScopesChange(requestReport); const subscription = AppState.addEventListener("change", (nextState) => { appState = nextState; requestReport(); }); return { removeScopeListener, subscription }; }), ({ removeScopeListener, subscription }) => Effect.sync(() => { removeScopeListener(); subscription.remove(); }), ); yield* SubscriptionRef.changes(registry.entries).pipe( Stream.runForEach(() => Effect.sync(requestReport)), Effect.forkScoped, ); yield* Stream.fromQueue(reportRequests).pipe( Stream.debounce("250 millis"), Stream.runForEach(() => report), Effect.forkScoped, ); yield* Effect.sync(requestReport).pipe( Effect.repeat(Schedule.spaced(`${REPORT_INTERVAL_MS} millis`)), Effect.forkScoped, ); }), );