Suite of AT Protocol TypeScript libraries built on web standards
21
fork

Configure Feed

Select the types of activity you want to include in your feed.

at main 298 lines 7.9 kB view raw
1import { l, type Procedure, Query, type Validator } from "@atp/lex"; 2import type { LexiconDoc } from "@atp/lexicon"; 3import { 4 type Agent, 5 type AgentOptions, 6 Client as ModernClient, 7 ResponseType, 8 type XrpcCallOptions, 9 XRPCError, 10 XRPCInvalidResponseError, 11 type XRPCResponse, 12} from "@atp/xrpc"; 13 14type Method = Query | Procedure; 15 16type LegacyCallOptions = { 17 encoding?: string; 18 signal?: AbortSignal; 19 headers?: Record<string, string | undefined>; 20 validateRequest?: boolean; 21 validateResponse?: boolean; 22}; 23 24type LexRecord = Record<string, unknown>; 25 26export { ResponseType, XRPCError, XRPCInvalidResponseError }; 27 28export class Client { 29 readonly #client: ModernClient; 30 readonly #methods: Map<string, Method>; 31 32 constructor(agentOpts: Agent | AgentOptions, lexicons: LexiconDoc[] = []) { 33 this.#client = new ModernClient(agentOpts); 34 this.#methods = buildMethodMap(lexicons); 35 } 36 37 get did() { 38 return this.#client.did; 39 } 40 41 setHeader( 42 key: string, 43 value: string | null | (() => string | null), 44 ): void { 45 this.#client.setHeader(key, value); 46 } 47 48 unsetHeader(key: string): void { 49 this.#client.unsetHeader(key); 50 } 51 52 clearHeaders(): void { 53 this.#client.clearHeaders(); 54 } 55 56 async call( 57 nsid: string, 58 params?: Record<string, unknown>, 59 dataOrOptions?: unknown, 60 options?: LegacyCallOptions, 61 ): Promise<XRPCResponse> { 62 const method = this.#methods.get(nsid) ?? l.query( 63 nsid as `${string}.${string}.${string}`, 64 l.params(), 65 l.payload(), 66 ); 67 if (method instanceof Query) { 68 const callOptions = options ?? toLegacyCallOptions(dataOrOptions); 69 return await this.#client.call( 70 method, 71 { 72 params, 73 encoding: callOptions?.encoding, 74 signal: callOptions?.signal, 75 headers: callOptions?.headers, 76 validateRequest: callOptions?.validateRequest, 77 validateResponse: callOptions?.validateResponse, 78 } as XrpcCallOptions<typeof method>, 79 ); 80 } 81 82 return await this.#client.call( 83 method, 84 { 85 params, 86 body: dataOrOptions, 87 encoding: options?.encoding, 88 signal: options?.signal, 89 headers: options?.headers, 90 validateRequest: options?.validateRequest, 91 validateResponse: options?.validateResponse, 92 } as XrpcCallOptions<typeof method>, 93 ); 94 } 95} 96 97export { Client as XrpcClient }; 98 99function buildMethodMap(lexicons: LexiconDoc[]): Map<string, Method> { 100 const methods = new Map<string, Method>(); 101 102 for (const lexicon of lexicons) { 103 const defs = asRecord(lexicon.defs); 104 const main = asRecord(defs?.main); 105 if (main == null) { 106 continue; 107 } 108 109 const params = compileParams(main.parameters); 110 const errors = compileErrors(main.errors); 111 if (main.type === "query") { 112 methods.set( 113 lexicon.id, 114 l.query( 115 lexicon.id as `${string}.${string}.${string}`, 116 params, 117 compilePayload(main.output), 118 errors, 119 ), 120 ); 121 continue; 122 } 123 124 if (main.type === "procedure") { 125 methods.set( 126 lexicon.id, 127 l.procedure( 128 lexicon.id as `${string}.${string}.${string}`, 129 params, 130 compilePayload(main.input), 131 compilePayload(main.output), 132 errors, 133 ), 134 ); 135 } 136 } 137 138 return methods; 139} 140 141function compileErrors(definition: unknown): readonly string[] | undefined { 142 if (!Array.isArray(definition)) { 143 return undefined; 144 } 145 const errors: string[] = []; 146 for (const item of definition) { 147 const error = asRecord(item); 148 if (error == null || typeof error.name !== "string") { 149 continue; 150 } 151 errors.push(error.name); 152 } 153 return errors.length > 0 ? errors : undefined; 154} 155 156function compilePayload(definition: unknown) { 157 const payload = asRecord(definition); 158 const encoding = typeof payload?.encoding === "string" 159 ? payload.encoding 160 : undefined; 161 const schema = compileSchema(payload?.schema); 162 if (schema === undefined) { 163 return l.payload(encoding); 164 } 165 return l.payload(encoding, schema); 166} 167 168function compileParams(definition: unknown) { 169 const params = asRecord(definition); 170 const properties = asRecord(params?.properties); 171 if (properties == null) { 172 return l.params(); 173 } 174 175 const required = new Set(toStringArray(params?.required)); 176 const validators: Record<string, Validator> = {}; 177 for (const [key, value] of Object.entries(properties)) { 178 const schema = compileSchema(value); 179 if (schema === undefined) { 180 continue; 181 } 182 if (required.has(key) || hasDefault(value)) { 183 validators[key] = schema; 184 } else { 185 validators[key] = l.optional(schema); 186 } 187 } 188 return l.params(validators); 189} 190 191function compileSchema(definition: unknown): Validator | undefined { 192 const schema = asRecord(definition); 193 if (schema == null) { 194 return undefined; 195 } 196 197 switch (schema.type) { 198 case "boolean": 199 return l.boolean({ 200 default: getBoolean(schema.default), 201 const: getBoolean(schema.const), 202 }); 203 case "integer": 204 return l.integer({ 205 default: getNumber(schema.default), 206 minimum: getNumber(schema.minimum), 207 maximum: getNumber(schema.maximum), 208 const: getNumber(schema.const), 209 }); 210 case "string": 211 return l.string({ 212 default: getString(schema.default), 213 minLength: getNumber(schema.minLength), 214 maxLength: getNumber(schema.maxLength), 215 }); 216 case "array": { 217 const items = compileSchema(schema.items) ?? l.unknown(); 218 return l.array(items, { 219 minLength: getNumber(schema.minLength), 220 maxLength: getNumber(schema.maxLength), 221 }); 222 } 223 case "object": { 224 const properties = asRecord(schema.properties) ?? {}; 225 const required = new Set(toStringArray(schema.required)); 226 const fields: Record<string, Validator> = {}; 227 for (const [key, value] of Object.entries(properties)) { 228 const fieldSchema = compileSchema(value); 229 if (fieldSchema === undefined) { 230 continue; 231 } 232 if (required.has(key) || hasDefault(value)) { 233 fields[key] = fieldSchema; 234 } else { 235 fields[key] = l.optional(fieldSchema); 236 } 237 } 238 return l.object(fields); 239 } 240 case "bytes": 241 return l.bytes({ 242 minLength: getNumber(schema.minLength), 243 maxLength: getNumber(schema.maxLength), 244 }); 245 case "cid-link": 246 return l.cidLink(); 247 default: 248 return l.unknown(); 249 } 250} 251 252function toLegacyCallOptions(value: unknown): LegacyCallOptions | undefined { 253 const options = asRecord(value); 254 if (options == null) { 255 return undefined; 256 } 257 if ( 258 !("encoding" in options) && 259 !("signal" in options) && 260 !("headers" in options) && 261 !("validateRequest" in options) && 262 !("validateResponse" in options) 263 ) { 264 return undefined; 265 } 266 return options as LegacyCallOptions; 267} 268 269function hasDefault(value: unknown): boolean { 270 const schema = asRecord(value); 271 return schema != null && "default" in schema; 272} 273 274function toStringArray(value: unknown): string[] { 275 if (!Array.isArray(value)) { 276 return []; 277 } 278 return value.filter((item): item is string => typeof item === "string"); 279} 280 281function asRecord(value: unknown): LexRecord | undefined { 282 if (value == null || typeof value !== "object" || Array.isArray(value)) { 283 return undefined; 284 } 285 return value as LexRecord; 286} 287 288function getNumber(value: unknown): number | undefined { 289 return typeof value === "number" ? value : undefined; 290} 291 292function getString(value: unknown): string | undefined { 293 return typeof value === "string" ? value : undefined; 294} 295 296function getBoolean(value: unknown): boolean | undefined { 297 return typeof value === "boolean" ? value : undefined; 298}