| 83 | } |
| 84 | |
| 85 | export function fetch<T extends { name: string }>( |
| 86 | clientName: string, |
| 87 | client: Client, |
| 88 | list: (client: Client) => Promise<T[]>, |
| 89 | label: string, |
| 90 | key?: (item: T) => string, |
| 91 | ) { |
| 92 | return Effect.tryPromise({ |
| 93 | try: () => list(client), |
| 94 | catch: (error) => error, |
| 95 | }).pipe( |
| 96 | Effect.tapError((error) => |
| 97 | Effect.logWarning(`failed to get ${label}`, { |
| 98 | clientName, |
| 99 | error: error instanceof Error ? error.message : String(error), |
| 100 | }), |
| 101 | ), |
| 102 | Effect.map((items) => { |
| 103 | const sanitizedClient = sanitize(clientName) |
| 104 | // Escape both the separator and escape marker so `server:uri` keys remain unambiguous. |
| 105 | const resourceClient = clientName.replaceAll("%", "%25").replaceAll(":", "%3A") |
| 106 | return Object.fromEntries( |
| 107 | items.map((item) => [ |
| 108 | key ? resourceClient + ":" + key(item) : sanitizedClient + ":" + sanitize(item.name), |
| 109 | { ...item, client: clientName }, |
| 110 | ]), |
| 111 | ) |
| 112 | }), |
| 113 | Effect.orElseSucceed(() => undefined), |
| 114 | ) |
| 115 | } |
| 116 | |
| 117 | export const sanitize = (value: string) => value.replace(/[^a-zA-Z0-9_-]/g, "_") |
| 118 | |