| ... | @@ -5,31 +5,87 @@ import * as Crypto from "effect/Crypto"; | ... | @@ -5,31 +5,87 @@ import * as Crypto from "effect/Crypto"; |
| 5 | import * as Effect from "effect/Effect"; | 5 | import * as Effect from "effect/Effect"; |
| 6 | import * as Encoding from "effect/Encoding"; | 6 | import * as Encoding from "effect/Encoding"; |
| 7 | import * as Layer from "effect/Layer"; | 7 | import * as Layer from "effect/Layer"; |
| | 8 | import * as PlatformError from "effect/PlatformError"; |
| 8 | import * as Schema from "effect/Schema"; | 9 | import * as Schema from "effect/Schema"; |
| 9 | import * as SynchronizedRef from "effect/SynchronizedRef"; | 10 | import * as SynchronizedRef from "effect/SynchronizedRef"; |
| 10 | | 11 | |
| 11 | const PREVIEW_PARTITION_PREFIX = "persist:t3code-preview-"; | 12 | const PREVIEW_PARTITION_PREFIX = "persist:t3code-preview-"; |
| 12 | | 13 | |
| 13 | export class BrowserSessionError extends Schema.TaggedErrorClass<BrowserSessionError>()( | 14 | export class BrowserSessionPartitionDerivationError extends Schema.TaggedErrorClass<BrowserSessionPartitionDerivationError>()( |
| 14 | "BrowserSessionError", | 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 | cause: Schema.Defect(), | 43 | cause: Schema.Defect(), |
| 18 | }, | 44 | }, |
| 19 | ) { | 45 | ) { |
| 20 | override get message(): string { | 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 | export class BrowserSession extends Context.Service< | 79 | export class BrowserSession extends Context.Service< |
| 26 | BrowserSession, | 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 | readonly isPartition: (partition: string) => boolean; | 85 | readonly isPartition: (partition: string) => boolean; |
| 30 | readonly getSession: (scope?: string) => Effect.Effect<Session, BrowserSessionError>; | 86 | readonly getSession: (scope?: string) => Effect.Effect<Session, BrowserSessionGetSessionError>; |
| 31 | readonly clearCookies: () => Effect.Effect<void, BrowserSessionError>; | 87 | readonly clearCookies: () => Effect.Effect<void, BrowserSessionStorageClearError>; |
| 32 | readonly clearCache: () => Effect.Effect<void, BrowserSessionError>; | 88 | readonly clearCache: () => Effect.Effect<void, BrowserSessionCacheClearError>; |
| 33 | } | 89 | } |
| 34 | >()("@t3tools/desktop/preview/BrowserSession") {} | 90 | >()("@t3tools/desktop/preview/BrowserSession") {} |
| 35 | | 91 | |
| ... | @@ -38,11 +94,15 @@ export const make = Effect.gen(function* BrowserSessionMake() { | ... | @@ -38,11 +94,15 @@ export const make = Effect.gen(function* BrowserSessionMake() { |
| 38 | const sessionsRef = yield* SynchronizedRef.make<ReadonlyMap<string, Session>>(new Map()); | 94 | const sessionsRef = yield* SynchronizedRef.make<ReadonlyMap<string, Session>>(new Map()); |
| 39 | | 95 | |
| 40 | const getPartition = Effect.fn("BrowserSession.getPartition")(function* (scope = "shared") { | 96 | const getPartition = Effect.fn("BrowserSession.getPartition")(function* (scope = "shared") { |
| 41 | const digest = yield* crypto | 97 | const digest = yield* crypto.digest("SHA-256", new TextEncoder().encode(scope)).pipe( |
| 42 | .digest("SHA-256", new TextEncoder().encode(scope)) | 98 | Effect.mapError( |
| 43 | .pipe( | 99 | (cause) => |
| 44 | Effect.mapError((cause) => new BrowserSessionError({ operation: "getPartition", cause })), | 100 | new BrowserSessionPartitionDerivationError({ |
| 45 | ); | 101 | scope, |
| | 102 | cause, |
| | 103 | }), |
| | 104 | ), |
| | 105 | ); |
| 46 | return `${PREVIEW_PARTITION_PREFIX}${Encoding.encodeHex(digest).slice(0, 20)}`; | 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,7 +127,12 @@ export const make = Effect.gen(function* BrowserSessionMake() { |
| 67 | next.set(partition, browserSession); | 127 | next.set(partition, browserSession); |
| 68 | return [browserSession, next] as const; | 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,13 +144,17 @@ export const make = Effect.gen(function* BrowserSessionMake() { |
| 79 | clearCookies: Effect.fn("BrowserSession.clearCookies")(function* () { | 144 | clearCookies: Effect.fn("BrowserSession.clearCookies")(function* () { |
| 80 | const sessions = yield* SynchronizedRef.get(sessionsRef); | 145 | const sessions = yield* SynchronizedRef.get(sessionsRef); |
| 81 | yield* Effect.all( | 146 | yield* Effect.all( |
| 82 | [...sessions.values()].map((browserSession) => | 147 | [...sessions.entries()].map(([partition, browserSession]) => |
| 83 | Effect.tryPromise({ | 148 | Effect.tryPromise({ |
| 84 | try: () => | 149 | try: () => |
| 85 | browserSession.clearStorageData({ | 150 | browserSession.clearStorageData({ |
| 86 | storages: ["cookies", "localstorage", "indexdb", "websql", "serviceworkers"], | 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 | { concurrency: "unbounded", discard: true }, | 160 | { concurrency: "unbounded", discard: true }, |
| ... | @@ -94,10 +163,14 @@ export const make = Effect.gen(function* BrowserSessionMake() { | ... | @@ -94,10 +163,14 @@ export const make = Effect.gen(function* BrowserSessionMake() { |
| 94 | clearCache: Effect.fn("BrowserSession.clearCache")(function* () { | 163 | clearCache: Effect.fn("BrowserSession.clearCache")(function* () { |
| 95 | const sessions = yield* SynchronizedRef.get(sessionsRef); | 164 | const sessions = yield* SynchronizedRef.get(sessionsRef); |
| 96 | yield* Effect.all( | 165 | yield* Effect.all( |
| 97 | [...sessions.values()].map((browserSession) => | 166 | [...sessions.entries()].map(([partition, browserSession]) => |
| 98 | Effect.tryPromise({ | 167 | Effect.tryPromise({ |
| 99 | try: () => browserSession.clearCache(), | 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 | { concurrency: "unbounded", discard: true }, | 176 | { concurrency: "unbounded", discard: true }, |