"use client"; import { useAtomValue } from "@effect/atom-react"; import { parseScopedThreadKey, scopedThreadKey } from "@t3tools/client-runtime/environment"; import { runAtomCommand } from "@t3tools/client-runtime/state/runtime"; import type { ScopedThreadRef } from "@t3tools/contracts"; import * as Schema from "effect/Schema"; import { AsyncResult, Atom } from "effect/unstable/reactivity"; import { applyPreviewServerEvent, applyPreviewServerSnapshot, readThreadPreviewState, reconcilePreviewServerSessions, } from "~/previewStateStore"; import { previewEnvironment } from "~/state/preview"; class PreviewSessionThreadKeyParseError extends Schema.TaggedErrorClass()( "PreviewSessionThreadKeyParseError", { threadKey: Schema.String }, ) { override get message(): string { return `Invalid scoped preview thread key: ${this.threadKey}`; } } const previewSessionSyncAtom = Atom.family((threadKey: string) => { const threadRef = parseScopedThreadKey(threadKey); if (threadRef === null) { throw new PreviewSessionThreadKeyParseError({ threadKey }); } const sessionsAtom = previewEnvironment.list({ environmentId: threadRef.environmentId, input: { threadId: threadRef.threadId }, }); const eventsAtom = previewEnvironment.events({ environmentId: threadRef.environmentId, input: {}, }); return Atom.make((get) => { let disposed = false; let recoveryId = 0; let recoveringUrl: string | null = null; let sessionsVersion = 0; let eventsVersion = 0; const reconcileSessions = (result: Atom.Type) => { if (!AsyncResult.isSuccess(result)) return; if (result.value.sessions.length > 0) { recoveringUrl = null; recoveryId += 1; reconcilePreviewServerSessions(threadRef, result.value.sessions); return; } const localSnapshot = readThreadPreviewState(threadRef).snapshot; const recoverableUrl = localSnapshot && localSnapshot.navStatus._tag !== "Idle" ? localSnapshot.navStatus.url : null; if (!recoverableUrl) { applyPreviewServerSnapshot(threadRef, null); return; } if (recoveringUrl === recoverableUrl) return; recoveringUrl = recoverableUrl; const currentRecoveryId = ++recoveryId; void runAtomCommand( get.registry, previewEnvironment.open, { environmentId: threadRef.environmentId, input: { threadId: threadRef.threadId, url: recoverableUrl }, }, { reportDefect: false, reportFailure: false }, ).then((openResult) => { if (disposed || currentRecoveryId !== recoveryId) return; recoveringUrl = null; if (openResult._tag === "Failure") return; applyPreviewServerSnapshot(threadRef, openResult.value); get.refresh(sessionsAtom); }); }; const applyLatestEvent = (result: Atom.Type) => { if (!AsyncResult.isSuccess(result) || result.value.threadId !== threadRef.threadId) return; applyPreviewServerEvent(threadRef, result.value); if (result.value.type === "opened" || result.value.type === "closed") { get.refresh(sessionsAtom); } }; get.addFinalizer(() => { disposed = true; recoveryId += 1; }); const initialSessions = get.once(sessionsAtom); const initialEvent = get.once(eventsAtom); get.subscribe(sessionsAtom, (result) => { sessionsVersion += 1; reconcileSessions(result); }); get.subscribe(eventsAtom, (result) => { eventsVersion += 1; applyLatestEvent(result); }); queueMicrotask(() => { if (disposed) return; if (sessionsVersion === 0) reconcileSessions(initialSessions); if (eventsVersion === 0) applyLatestEvent(initialEvent); }); }).pipe(Atom.setIdleTTL(1_000), Atom.withLabel(`preview:session-sync:${threadKey}`)); }); export function usePreviewSession(threadRef: ScopedThreadRef): void { useAtomValue(previewSessionSyncAtom(scopedThreadKey(threadRef))); }