import type { Session } from "electron"; import { session } from "electron"; import * as Context from "effect/Context"; import * as Crypto from "effect/Crypto"; import * as Effect from "effect/Effect"; import * as Encoding from "effect/Encoding"; import * as Layer from "effect/Layer"; import * as PlatformError from "effect/PlatformError"; import * as Schema from "effect/Schema"; import * as SynchronizedRef from "effect/SynchronizedRef"; const PREVIEW_PARTITION_PREFIX = "persist:t3code-preview-"; export class BrowserSessionPartitionDerivationError extends Schema.TaggedErrorClass()( "BrowserSessionPartitionDerivationError", { scope: Schema.String, cause: Schema.instanceOf(PlatformError.PlatformError), }, ) { override get message(): string { return `Failed to derive a desktop preview browser partition for scope ${this.scope}.`; } } export class BrowserSessionCreationError extends Schema.TaggedErrorClass()( "BrowserSessionCreationError", { scope: Schema.String, partition: Schema.String, cause: Schema.Defect(), }, ) { override get message(): string { return `Failed to create a desktop preview browser session for scope ${this.scope} (partition ${this.partition}).`; } } export class BrowserSessionStorageClearError extends Schema.TaggedErrorClass()( "BrowserSessionStorageClearError", { partition: Schema.String, cause: Schema.Defect(), }, ) { override get message(): string { return `Failed to clear desktop preview browser storage for partition ${this.partition}.`; } } export class BrowserSessionCacheClearError extends Schema.TaggedErrorClass()( "BrowserSessionCacheClearError", { partition: Schema.String, cause: Schema.Defect(), }, ) { override get message(): string { return `Failed to clear the desktop preview browser cache for partition ${this.partition}.`; } } export const BrowserSessionGetSessionError = Schema.Union([ BrowserSessionPartitionDerivationError, BrowserSessionCreationError, ]); export type BrowserSessionGetSessionError = typeof BrowserSessionGetSessionError.Type; export const isBrowserSessionGetSessionError = Schema.is(BrowserSessionGetSessionError); export const BrowserSessionError = Schema.Union([ BrowserSessionPartitionDerivationError, BrowserSessionCreationError, BrowserSessionStorageClearError, BrowserSessionCacheClearError, ]); export type BrowserSessionError = typeof BrowserSessionError.Type; export const isBrowserSessionError = Schema.is(BrowserSessionError); export class BrowserSession extends Context.Service< BrowserSession, { readonly getPartition: ( scope?: string, ) => Effect.Effect; readonly isPartition: (partition: string) => boolean; readonly getSession: (scope?: string) => Effect.Effect; readonly clearCookies: () => Effect.Effect; readonly clearCache: () => Effect.Effect; } >()("@t3tools/desktop/preview/BrowserSession") {} export const make = Effect.gen(function* BrowserSessionMake() { const crypto = yield* Crypto.Crypto; const sessionsRef = yield* SynchronizedRef.make>(new Map()); const getPartition = Effect.fn("BrowserSession.getPartition")(function* (scope = "shared") { const digest = yield* crypto.digest("SHA-256", new TextEncoder().encode(scope)).pipe( Effect.mapError( (cause) => new BrowserSessionPartitionDerivationError({ scope, cause, }), ), ); return `${PREVIEW_PARTITION_PREFIX}${Encoding.encodeHex(digest).slice(0, 20)}`; }); const getSession = Effect.fn("BrowserSession.getSession")(function* (scope = "shared") { const partition = yield* getPartition(scope); return yield* SynchronizedRef.modifyEffect(sessionsRef, (sessions) => { const existing = sessions.get(partition); if (existing) return Effect.succeed([existing, sessions] as const); return Effect.try({ try: () => { const browserSession = session.fromPartition(partition); const userAgent = browserSession .getUserAgent() .replace(/Electron\/[\d.]+ /, "") .replace(/\s*t3code\/[\d.]+/, ""); browserSession.setUserAgent(userAgent); browserSession.setPermissionRequestHandler((_webContents, permission, callback) => { const allowed = ["clipboard-read", "clipboard-write", "notifications", "geolocation"]; callback(allowed.includes(permission)); }); const next = new Map(sessions); next.set(partition, browserSession); return [browserSession, next] as const; }, catch: (cause) => new BrowserSessionCreationError({ scope, partition, cause, }), }); }); }); return BrowserSession.of({ getPartition, isPartition: (partition) => partition.startsWith(PREVIEW_PARTITION_PREFIX), getSession, clearCookies: Effect.fn("BrowserSession.clearCookies")(function* () { const sessions = yield* SynchronizedRef.get(sessionsRef); yield* Effect.all( [...sessions.entries()].map(([partition, browserSession]) => Effect.tryPromise({ try: () => browserSession.clearStorageData({ storages: ["cookies", "localstorage", "indexdb", "websql", "serviceworkers"], }), catch: (cause) => new BrowserSessionStorageClearError({ partition, cause, }), }), ), { concurrency: "unbounded", discard: true }, ); }), clearCache: Effect.fn("BrowserSession.clearCache")(function* () { const sessions = yield* SynchronizedRef.get(sessionsRef); yield* Effect.all( [...sessions.entries()].map(([partition, browserSession]) => Effect.tryPromise({ try: () => browserSession.clearCache(), catch: (cause) => new BrowserSessionCacheClearError({ partition, cause, }), }), ), { concurrency: "unbounded", discard: true }, ); }), }); }).pipe(Effect.withSpan("BrowserSession.make")); export const layer = Layer.effect(BrowserSession, make);