| 1 | /** |
| 2 | * Effect service for the browser Permissions API. |
| 3 | * |
| 4 | * This module defines a `Permissions` service backed by |
| 5 | * `navigator.permissions`. The service exposes `query`, which returns the |
| 6 | * browser `PermissionStatus` for a permission name. Failed browser operations |
| 7 | * are represented as `PermissionsError` values with `InvalidStateError` or |
| 8 | * `TypeError` reasons, and `layer` provides the browser-backed service. |
| 9 | * |
| 10 | * @since 4.0.0 |
| 11 | */ |
| 12 | import * as Context from "effect/Context" |
| 13 | import * as Data from "effect/Data" |
| 14 | import * as Effect from "effect/Effect" |
| 15 | import * as Layer from "effect/Layer" |
| 16 | |
| 17 | const TypeId = "~@effect/platform-browser/Permissions" |
| 18 | const ErrorTypeId = "~@effect/platform-browser/Permissions/PermissionsError" |
| 19 | |
| 20 | /** |
| 21 | * Wrapper on the Permission API (`navigator.permissions`) with methods for |
| 22 | * querying status of permissions. |
| 23 | * |
| 24 | * @category services |
| 25 | * @since 4.0.0 |
| 26 | */ |
| 27 | export interface Permissions { |
| 28 | readonly [TypeId]: typeof TypeId |
| 29 | |
| 30 | /** |
| 31 | * Returns the state of a user permission on the global scope. |
| 32 | */ |
| 33 | readonly query: <Name extends PermissionName>( |
| 34 | name: Name |
| 35 | ) => Effect.Effect< |
| 36 | // `name` is identical to the name passed to Permissions.query |
| 37 | // https://developer.mozilla.org/en-US/docs/Web/API/PermissionStatus |
| 38 | Omit<PermissionStatus, "name"> & { name: Name }, |
| 39 | PermissionsError |
| 40 | > |
| 41 | } |
| 42 | |
| 43 | /** |
| 44 | * Error reason for an `InvalidStateError` raised by the browser Permissions API. |
| 45 | * |
| 46 | * @category errors |
| 47 | * @since 4.0.0 |
| 48 | */ |
| 49 | export class PermissionsInvalidStateError extends Data.TaggedError("InvalidStateError")<{ |
| 50 | readonly cause: unknown |
| 51 | }> { |
| 52 | override get message(): string { |
| 53 | return this._tag |
| 54 | } |
| 55 | } |
| 56 | |
| 57 | /** |
| 58 | * Error reason for a `TypeError` raised by the browser Permissions API. |
| 59 | * |
| 60 | * @category errors |
| 61 | * @since 4.0.0 |
| 62 | */ |
| 63 | export class PermissionsTypeError extends Data.TaggedError("TypeError")<{ |
| 64 | readonly cause: unknown |
| 65 | }> { |
| 66 | override get message(): string { |
| 67 | return this._tag |
| 68 | } |
| 69 | } |
| 70 | |
| 71 | /** |
| 72 | * Union of browser Permissions API error reasons represented by the service. |
| 73 | * |
| 74 | * @category errors |
| 75 | * @since 4.0.0 |
| 76 | */ |
| 77 | export type PermissionsErrorReason = PermissionsInvalidStateError | PermissionsTypeError |
| 78 | |
| 79 | /** |
| 80 | * Tagged error wrapping a browser Permissions API failure reason. |
| 81 | * |
| 82 | * @category errors |
| 83 | * @since 4.0.0 |
| 84 | */ |
| 85 | export class PermissionsError extends Data.TaggedError("PermissionsError")<{ |
| 86 | readonly reason: PermissionsErrorReason |
| 87 | }> { |
| 88 | constructor(props: { readonly reason: PermissionsErrorReason }) { |
| 89 | super({ |
| 90 | ...props, |
| 91 | cause: props.reason.cause |
| 92 | } as any) |
| 93 | } |
| 94 | |
| 95 | readonly [ErrorTypeId] = ErrorTypeId |
| 96 | |
| 97 | override get message(): string { |
| 98 | return this.reason.message |
| 99 | } |
| 100 | } |
| 101 | |
| 102 | /** |
| 103 | * Service tag for browser permission querying. |
| 104 | * |
| 105 | * **When to use** |
| 106 | * |
| 107 | * Use when you need to require or provide browser permission querying through |
| 108 | * Effect's context. |
| 109 | * |
| 110 | * @category services |
| 111 | * @since 4.0.0 |
| 112 | */ |
| 113 | export const Permissions: Context.Service<Permissions, Permissions> = Context.Service<Permissions>(TypeId) |
| 114 | |
| 115 | /** |
| 116 | * Provides the `Permissions` service using the browser `navigator.permissions` API. |
| 117 | * |
| 118 | * **When to use** |
| 119 | * |
| 120 | * Use when you need a live browser `Permissions` service backed by the ambient |
| 121 | * `navigator.permissions` implementation. |
| 122 | * |
| 123 | * **Details** |
| 124 | * |
| 125 | * `query` delegates to `navigator.permissions.query({ name })` and wraps |
| 126 | * rejected browser operations in `PermissionsError`. |
| 127 | * |
| 128 | * @category layers |
| 129 | * @since 4.0.0 |
| 130 | */ |
| 131 | export const layer: Layer.Layer<Permissions> = Layer.succeed( |
| 132 | Permissions, |
| 133 | Permissions.of({ |
| 134 | [TypeId]: TypeId, |
| 135 | query: (name) => |
| 136 | Effect.tryPromise({ |
| 137 | try: () => navigator.permissions.query({ name }) as Promise<any>, |
| 138 | catch: (cause) => |
| 139 | new PermissionsError({ |
| 140 | reason: cause instanceof DOMException |
| 141 | ? new PermissionsInvalidStateError({ cause }) |
| 142 | : new PermissionsTypeError({ cause }) |
| 143 | }) |
| 144 | }) |
| 145 | }) |
| 146 | ) |