import { ClientPresentation, CloudSession, EnvironmentOwnedDataCleanup, PlatformConnectionSource, PrimaryEnvironmentAuth, RelayDeviceIdentity, SshEnvironmentGateway, } from "@t3tools/client-runtime/platform"; import { ConnectionBlockedError, ConnectionTransientError, Connectivity, Wakeups, } from "@t3tools/client-runtime/connection"; import { managedRelayAccountChanges, managedRelaySessionAtom } from "@t3tools/client-runtime/relay"; import { AuthStandardClientScopes } from "@t3tools/contracts"; import * as Context from "effect/Context"; import * as Effect from "effect/Effect"; import * as Layer from "effect/Layer"; import * as Option from "effect/Option"; import * as Queue from "effect/Queue"; import * as Stream from "effect/Stream"; import Constants from "expo-constants"; import * as Network from "expo-network"; import { AppState } from "react-native"; import { authClientMetadata } from "../lib/authClientMetadata"; import * as Runtime from "../lib/runtime"; import * as MobileStorage from "../persistence/mobile-storage"; import { appAtomRegistry } from "../state/atom-registry"; import { clearThreadOutboxEnvironment } from "../state/thread-outbox-removal"; import { clearComposerDraftsEnvironment } from "../state/use-composer-drafts"; import { mobileApplicationActiveWakeup } from "./app-state-wakeups"; import { connectionStorageLayer } from "./storage"; function networkStatus(state: Network.NetworkState): "unknown" | "offline" | "online" { if (state.isConnected === false) { return "offline"; } if (state.isConnected === true) { return "online"; } return "unknown"; } const connectivityLayer = Connectivity.layer({ status: Effect.tryPromise({ try: () => Network.getNetworkStateAsync(), catch: () => undefined, }).pipe( Effect.match({ onFailure: () => "unknown" as const, onSuccess: networkStatus, }), ), changes: Stream.callback((queue) => Effect.acquireRelease( Effect.sync(() => { let active = true; const networkSubscription = Network.addNetworkStateListener((state) => { Queue.offerUnsafe(queue, networkStatus(state)); }); const appStateSubscription = AppState.addEventListener("change", (state) => { if (state !== "active") { return; } void Network.getNetworkStateAsync() .then((current) => { if (active) { Queue.offerUnsafe(queue, networkStatus(current)); } }) .catch(() => undefined); }); return { close: () => { active = false; networkSubscription.remove(); appStateSubscription.remove(); }, }; }), ({ close }) => Effect.sync(close), ).pipe(Effect.asVoid), ), }); const wakeupsLayer = Wakeups.layer({ changes: Stream.merge( Stream.callback<"application-active-probe" | "application-active-reconnect">((queue) => Effect.acquireRelease( Effect.sync(() => { let backgroundedAtMs = AppState.currentState === "background" ? Date.now() : null; return AppState.addEventListener("change", (state) => { if (state === "background") { backgroundedAtMs = Date.now(); return; } if (state === "active") { Queue.offerUnsafe(queue, mobileApplicationActiveWakeup(backgroundedAtMs, Date.now())); backgroundedAtMs = null; } }); }), (subscription) => Effect.sync(() => subscription.remove()), ).pipe(Effect.asVoid), ), managedRelayAccountChanges(appAtomRegistry).pipe( Stream.map(() => "credentials-changed" as const), ), ), }); const capabilitiesLayer = Layer.effectContext( Effect.gen(function* () { const storage = yield* MobileStorage.MobileStorage; return Context.make( CloudSession, CloudSession.of({ identity: Effect.sync(() => Option.fromNullishOr(appAtomRegistry.get(managedRelaySessionAtom)), ), clerkToken: Effect.gen(function* () { const session = appAtomRegistry.get(managedRelaySessionAtom); if (session === null) { return yield* new ConnectionBlockedError({ reason: "authentication", detail: "Sign in to T3 Connect to connect this environment.", }); } const token = yield* session.readClerkToken().pipe( Effect.mapError( (error) => new ConnectionTransientError({ reason: "network", detail: error.message, }), ), ); if (token === null) { return yield* new ConnectionBlockedError({ reason: "authentication", detail: "The T3 Connect session is unavailable.", }); } return token; }), }), ).pipe( Context.add( PrimaryEnvironmentAuth, PrimaryEnvironmentAuth.of({ bearerToken: Effect.succeed(Option.none()) }), ), Context.add( RelayDeviceIdentity, RelayDeviceIdentity.of({ deviceId: storage.loadOrCreateAgentAwarenessDeviceId.pipe( Effect.mapError( (cause) => new ConnectionTransientError({ reason: "remote-unavailable", detail: `Could not load the mobile device identity: ${String(cause)}`, }), ), Effect.map(Option.some), ), }), ), Context.add( ClientPresentation, ClientPresentation.of({ metadata: authClientMetadata(Constants.expoConfig?.version), scopes: AuthStandardClientScopes, }), ), Context.add( SshEnvironmentGateway, SshEnvironmentGateway.of({ provision: () => Effect.fail( new ConnectionBlockedError({ reason: "unsupported", detail: "SSH environments are only available in the desktop app.", }), ), prepare: () => Effect.fail( new ConnectionBlockedError({ reason: "unsupported", detail: "SSH environments are only available in the desktop app.", }), ), disconnect: () => Effect.void, }), ), ); }), ); const platformConnectionSourceLayer = Layer.succeed( PlatformConnectionSource, PlatformConnectionSource.of({ registrations: Stream.empty, }), ); const providedConnectionStorageLayer = connectionStorageLayer.pipe( Layer.provide(Runtime.runtimeContextLayer), ); const providedCapabilitiesLayer = capabilitiesLayer.pipe( Layer.provide(Runtime.runtimeContextLayer), ); const environmentOwnedDataCleanupLayer = Layer.succeed( EnvironmentOwnedDataCleanup, EnvironmentOwnedDataCleanup.of({ clear: (environmentId) => Effect.all( [ Effect.promise(() => clearThreadOutboxEnvironment(environmentId)), Effect.promise(() => clearComposerDraftsEnvironment(environmentId)), ], { concurrency: "unbounded", discard: true }, ).pipe( Effect.catch((cause) => Effect.logWarning("Could not clear mobile environment-owned data.", { environmentId, cause, }), ), ), }), ); type ConnectionPlatformLayerSource = | typeof providedConnectionStorageLayer | typeof Runtime.runtimeContextLayer | typeof connectivityLayer | typeof wakeupsLayer | typeof providedCapabilitiesLayer | typeof platformConnectionSourceLayer | typeof environmentOwnedDataCleanupLayer; export const connectionPlatformLayer: Layer.Layer< Layer.Success, Layer.Error, Layer.Services > = Layer.mergeAll( providedConnectionStorageLayer, Runtime.runtimeContextLayer, connectivityLayer, wakeupsLayer, providedCapabilitiesLayer, platformConnectionSourceLayer, environmentOwnedDataCleanupLayer, );