| ... | ... | @@ -5,31 +5,87 @@ import * as Crypto from "effect/Crypto"; |
| 5 | 5 | import * as Effect from "effect/Effect"; |
| 6 | 6 | import * as Encoding from "effect/Encoding"; |
| 7 | 7 | import * as Layer from "effect/Layer"; |
| 8 | import * as PlatformError from "effect/PlatformError"; |
| 8 | 9 | import * as Schema from "effect/Schema"; |
| 9 | 10 | import * as SynchronizedRef from "effect/SynchronizedRef"; |
| 10 | 11 | |
| 11 | 12 | const PREVIEW_PARTITION_PREFIX = "persist:t3code-preview-"; |
| 12 | 13 | |
| 13 | | export class BrowserSessionError extends Schema.TaggedErrorClass<BrowserSessionError>()( |
| 14 | | "BrowserSessionError", |
| 14 | export class BrowserSessionPartitionDerivationError extends Schema.TaggedErrorClass<BrowserSessionPartitionDerivationError>()( |
| 15 | "BrowserSessionPartitionDerivationError", |
| 15 | 16 | { |
| 16 | | operation: Schema.Literals(["getPartition", "getSession", "clearCookies", "clearCache"]), |
| 17 | scope: Schema.String, |
| 18 | cause: Schema.instanceOf(PlatformError.PlatformError), |
| 19 | }, |
| 20 | ) { |
| 21 | override get message(): string { |
| 22 | return `Failed to derive a desktop preview browser partition for scope ${this.scope}.`; |
| 23 | } |
| 24 | } |
| 25 | |
| 26 | export class BrowserSessionCreationError extends Schema.TaggedErrorClass<BrowserSessionCreationError>()( |
| 27 | "BrowserSessionCreationError", |
| 28 | { |
| 29 | scope: Schema.String, |
| 30 | partition: Schema.String, |
| 31 | cause: Schema.Defect(), |
| 32 | }, |
| 33 | ) { |
| 34 | override get message(): string { |
| 35 | return `Failed to create a desktop preview browser session for scope ${this.scope} (partition ${this.partition}).`; |
| 36 | } |
| 37 | } |
| 38 | |
| 39 | export class BrowserSessionStorageClearError extends Schema.TaggedErrorClass<BrowserSessionStorageClearError>()( |
| 40 | "BrowserSessionStorageClearError", |
| 41 | { |
| 42 | partition: Schema.String, |
| 17 | 43 | cause: Schema.Defect(), |
| 18 | 44 | }, |
| 19 | 45 | ) { |
| 20 | 46 | override get message(): string { |
| 21 | | return `Desktop preview browser session operation failed: ${this.operation}`; |
| 47 | return `Failed to clear desktop preview browser storage for partition ${this.partition}.`; |
| 22 | 48 | } |
| 23 | 49 | } |
| 24 | 50 | |
| 51 | export class BrowserSessionCacheClearError extends Schema.TaggedErrorClass<BrowserSessionCacheClearError>()( |
| 52 | "BrowserSessionCacheClearError", |
| 53 | { |
| 54 | partition: Schema.String, |
| 55 | cause: Schema.Defect(), |
| 56 | }, |
| 57 | ) { |
| 58 | override get message(): string { |
| 59 | return `Failed to clear the desktop preview browser cache for partition ${this.partition}.`; |
| 60 | } |
| 61 | } |
| 62 | |
| 63 | export const BrowserSessionGetSessionError = Schema.Union([ |
| 64 | BrowserSessionPartitionDerivationError, |
| 65 | BrowserSessionCreationError, |
| 66 | ]); |
| 67 | export type BrowserSessionGetSessionError = typeof BrowserSessionGetSessionError.Type; |
| 68 | export const isBrowserSessionGetSessionError = Schema.is(BrowserSessionGetSessionError); |
| 69 | |
| 70 | export const BrowserSessionError = Schema.Union([ |
| 71 | BrowserSessionPartitionDerivationError, |
| 72 | BrowserSessionCreationError, |
| 73 | BrowserSessionStorageClearError, |
| 74 | BrowserSessionCacheClearError, |
| 75 | ]); |
| 76 | export type BrowserSessionError = typeof BrowserSessionError.Type; |
| 77 | export const isBrowserSessionError = Schema.is(BrowserSessionError); |
| 78 | |
| 25 | 79 | export class BrowserSession extends Context.Service< |
| 26 | 80 | BrowserSession, |
| 27 | 81 | { |
| 28 | | readonly getPartition: (scope?: string) => Effect.Effect<string, BrowserSessionError>; |
| 82 | readonly getPartition: ( |
| 83 | scope?: string, |
| 84 | ) => Effect.Effect<string, BrowserSessionPartitionDerivationError>; |
| 29 | 85 | readonly isPartition: (partition: string) => boolean; |
| 30 | | readonly getSession: (scope?: string) => Effect.Effect<Session, BrowserSessionError>; |
| 31 | | readonly clearCookies: () => Effect.Effect<void, BrowserSessionError>; |
| 32 | | readonly clearCache: () => Effect.Effect<void, BrowserSessionError>; |
| 86 | readonly getSession: (scope?: string) => Effect.Effect<Session, BrowserSessionGetSessionError>; |
| 87 | readonly clearCookies: () => Effect.Effect<void, BrowserSessionStorageClearError>; |
| 88 | readonly clearCache: () => Effect.Effect<void, BrowserSessionCacheClearError>; |
| 33 | 89 | } |
| 34 | 90 | >()("@t3tools/desktop/preview/BrowserSession") {} |
| 35 | 91 | |
| ... | ... | @@ -38,11 +94,15 @@ export const make = Effect.gen(function* BrowserSessionMake() { |
| 38 | 94 | const sessionsRef = yield* SynchronizedRef.make<ReadonlyMap<string, Session>>(new Map()); |
| 39 | 95 | |
| 40 | 96 | const getPartition = Effect.fn("BrowserSession.getPartition")(function* (scope = "shared") { |
| 41 | | const digest = yield* crypto |
| 42 | | .digest("SHA-256", new TextEncoder().encode(scope)) |
| 43 | | .pipe( |
| 44 | | Effect.mapError((cause) => new BrowserSessionError({ operation: "getPartition", cause })), |
| 45 | | ); |
| 97 | const digest = yield* crypto.digest("SHA-256", new TextEncoder().encode(scope)).pipe( |
| 98 | Effect.mapError( |
| 99 | (cause) => |
| 100 | new BrowserSessionPartitionDerivationError({ |
| 101 | scope, |
| 102 | cause, |
| 103 | }), |
| 104 | ), |
| 105 | ); |
| 46 | 106 | return `${PREVIEW_PARTITION_PREFIX}${Encoding.encodeHex(digest).slice(0, 20)}`; |
| 47 | 107 | }); |
| 48 | 108 | |
| ... | ... | @@ -67,7 +127,12 @@ export const make = Effect.gen(function* BrowserSessionMake() { |
| 67 | 127 | next.set(partition, browserSession); |
| 68 | 128 | return [browserSession, next] as const; |
| 69 | 129 | }, |
| 70 | | catch: (cause) => new BrowserSessionError({ operation: "getSession", cause }), |
| 130 | catch: (cause) => |
| 131 | new BrowserSessionCreationError({ |
| 132 | scope, |
| 133 | partition, |
| 134 | cause, |
| 135 | }), |
| 71 | 136 | }); |
| 72 | 137 | }); |
| 73 | 138 | }); |
| ... | ... | @@ -79,13 +144,17 @@ export const make = Effect.gen(function* BrowserSessionMake() { |
| 79 | 144 | clearCookies: Effect.fn("BrowserSession.clearCookies")(function* () { |
| 80 | 145 | const sessions = yield* SynchronizedRef.get(sessionsRef); |
| 81 | 146 | yield* Effect.all( |
| 82 | | [...sessions.values()].map((browserSession) => |
| 147 | [...sessions.entries()].map(([partition, browserSession]) => |
| 83 | 148 | Effect.tryPromise({ |
| 84 | 149 | try: () => |
| 85 | 150 | browserSession.clearStorageData({ |
| 86 | 151 | storages: ["cookies", "localstorage", "indexdb", "websql", "serviceworkers"], |
| 87 | 152 | }), |
| 88 | | catch: (cause) => new BrowserSessionError({ operation: "clearCookies", cause }), |
| 153 | catch: (cause) => |
| 154 | new BrowserSessionStorageClearError({ |
| 155 | partition, |
| 156 | cause, |
| 157 | }), |
| 89 | 158 | }), |
| 90 | 159 | ), |
| 91 | 160 | { concurrency: "unbounded", discard: true }, |
| ... | ... | @@ -94,10 +163,14 @@ export const make = Effect.gen(function* BrowserSessionMake() { |
| 94 | 163 | clearCache: Effect.fn("BrowserSession.clearCache")(function* () { |
| 95 | 164 | const sessions = yield* SynchronizedRef.get(sessionsRef); |
| 96 | 165 | yield* Effect.all( |
| 97 | | [...sessions.values()].map((browserSession) => |
| 166 | [...sessions.entries()].map(([partition, browserSession]) => |
| 98 | 167 | Effect.tryPromise({ |
| 99 | 168 | try: () => browserSession.clearCache(), |
| 100 | | catch: (cause) => new BrowserSessionError({ operation: "clearCache", cause }), |
| 169 | catch: (cause) => |
| 170 | new BrowserSessionCacheClearError({ |
| 171 | partition, |
| 172 | cause, |
| 173 | }), |
| 101 | 174 | }), |
| 102 | 175 | ), |
| 103 | 176 | { concurrency: "unbounded", discard: true }, |