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

Configure Feed

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

feat: $type utilities

+298 -22
+30
lex/core/$type.ts
··· 1 + import type { OmitKey, Simplify } from "./types.ts"; 1 2 import type { NsidString } from "./string-format.ts"; 2 3 3 4 export type $Type< ··· 16 17 ): $Type<N, H> { 17 18 return (hash === "main" ? nsid : `${nsid}#${hash}`) as $Type<N, H>; 18 19 } 20 + 21 + export type $Typed<V, T extends string = string> = Simplify< 22 + V & { 23 + $type: T; 24 + } 25 + >; 26 + 27 + export function $typed<V extends Record<string, unknown>, T extends string>( 28 + value: V, 29 + $type: T, 30 + ): $Typed<Un$Typed<V & { $type?: unknown }>, T> { 31 + return (value as { $type?: unknown }).$type === $type 32 + ? value as unknown as $Typed<Un$Typed<V & { $type?: unknown }>, T> 33 + : { ...value, $type }; 34 + } 35 + 36 + export type $TypedMaybe<V, T extends string = string> = Simplify< 37 + V & { 38 + $type?: T; 39 + } 40 + >; 41 + 42 + export type Un$Typed<V extends { $type?: unknown }> = OmitKey<V, "$type">; 43 + 44 + declare const unknown$TypeSymbol: unique symbol; 45 + 46 + export type Unknown$Type = string & { [unknown$TypeSymbol]: true }; 47 + 48 + export type Unknown$TypedObject = { $type: Unknown$Type };
+4
lex/core/types.ts
··· 2 2 3 3 export type Simplify<T> = { [K in keyof T]: T[K] } & NonNullable<unknown>; 4 4 5 + export type OmitKey<T, K extends keyof T> = { 6 + [K2 in keyof T as K2 extends K ? never : K2]: T[K2]; 7 + }; 8 + 5 9 declare const __restricted: unique symbol; 6 10 export type Restricted<Message extends string> = typeof __restricted & { 7 11 [__restricted]: Message;
+19 -7
lex/schema/typed-object.ts
··· 1 1 import { isPlainObject } from "../data/object.ts"; 2 - import type { $Type, Simplify } from "../core.ts"; 2 + import type { 3 + $Type, 4 + $Typed, 5 + $TypedMaybe, 6 + Un$Typed, 7 + } from "../core.ts"; 8 + import { $typed } from "../core.ts"; 3 9 import { 4 10 type Infer, 5 11 Schema, ··· 8 14 type ValidatorContext, 9 15 } from "../validation.ts"; 10 16 17 + export type MaybeTypedObject< 18 + T extends $Type, 19 + V extends { $type?: unknown } = { $type?: unknown }, 20 + > = V extends { $type?: T } ? V 21 + : $TypedMaybe<V, T>; 22 + 11 23 export type TypedObjectSchemaOutput< 12 24 T extends $Type, 13 25 S extends Validator<{ [_ in string]?: unknown }>, 14 - > = Simplify<Infer<S> & { $type?: T }>; 26 + > = $TypedMaybe<Infer<S>, T>; 15 27 16 28 export class TypedObjectSchema< 17 29 const T extends $Type = any, ··· 26 38 27 39 isTypeOf<X extends Record<string, unknown>>( 28 40 value: X, 29 - ): value is X extends { $type?: T } ? X : X & { $type?: T } { 41 + ): value is MaybeTypedObject<T, X> { 30 42 return value.$type === undefined || value.$type === this.$type; 31 43 } 32 44 33 45 build<X extends Omit<Infer<S>, "$type">>( 34 46 input: X, 35 - ): Simplify<Omit<X, "$type"> & { $type: T }> { 36 - return { ...input, $type: this.$type }; 47 + ): $Typed<Un$Typed<X & { $type?: unknown }>, T> { 48 + return $typed(input, this.$type); 37 49 } 38 50 39 51 $isTypeOf<X extends Record<string, unknown>>( 40 52 value: X, 41 - ): value is X extends { $type?: T } ? X : X & { $type?: T } { 53 + ): value is MaybeTypedObject<T, X> { 42 54 return this.isTypeOf(value); 43 55 } 44 56 45 57 $build<X extends Omit<Infer<S>, "$type">>( 46 58 input: X, 47 - ): Simplify<Omit<X, "$type"> & { $type: T }> { 59 + ): $Typed<Un$Typed<X & { $type?: unknown }>, T> { 48 60 return this.build<X>(input); 49 61 } 50 62
+2 -1
lex/schema/typed-ref.ts
··· 1 + import type { $Typed } from "../core.ts"; 1 2 import { 2 3 Schema, 3 4 type ValidationResult, ··· 14 15 TypedRefSchemaValidator<V>; 15 16 16 17 export type TypedRefSchemaOutput<V extends { $type?: string } = any> = V extends 17 - { $type?: infer T extends string } ? V & { $type: T } : never; 18 + { $type?: infer T extends string } ? $Typed<V, T> : never; 18 19 19 20 export class TypedRefSchema<V extends { $type?: string } = any> extends Schema< 20 21 TypedRefSchemaOutput<V>
+64 -6
lex/schema/typed-union.ts
··· 1 + import { isCid } from "../data/cid.ts"; 1 2 import { isPlainObject } from "../data/object.ts"; 2 - import type { Restricted, UnknownString } from "../core/types.ts"; 3 + import type { Unknown$TypedObject } from "../core.ts"; 3 4 import { lazyProperty } from "../util/lazy-property.ts"; 4 5 import { 5 6 type Infer, 7 + IssueInvalidType, 8 + type PropertyKey, 6 9 Schema, 7 10 type ValidationResult, 8 11 type ValidatorContext, ··· 11 14 12 15 export type TypedRef<T extends { $type?: string }> = TypedRefSchemaOutput<T>; 13 16 14 - export type TypedObject = 15 - & { $type: UnknownString } 16 - & { 17 - [K in string]: Restricted<"Unknown property">; 18 - }; 17 + export type TypedObject = Unknown$TypedObject; 19 18 20 19 type TypedRefSchemasToUnion<T extends readonly TypedRefSchema[]> = { 21 20 [K in keyof T]: Infer<T[K]>; ··· 27 26 > = Closed extends true ? TypedRefSchemasToUnion<TypedRefs> 28 27 : TypedRefSchemasToUnion<TypedRefs> | TypedObject; 29 28 29 + const LEX_VALUE_TYPES = [ 30 + "integer", 31 + "string", 32 + "boolean", 33 + "null", 34 + "array", 35 + "object", 36 + "bytes", 37 + "cid", 38 + ] as const; 39 + 30 40 export class TypedUnionSchema< 31 41 TypedRefs extends readonly TypedRefSchema[] = any, 32 42 Closed extends boolean = any, ··· 73 83 return ctx.issueInvalidPropertyType(input, "$type", "string"); 74 84 } 75 85 86 + const invalidLexValue = findInvalidLexValue(input); 87 + if (invalidLexValue) { 88 + return ctx.failure( 89 + new IssueInvalidType( 90 + ctx.concatPath(invalidLexValue.path), 91 + invalidLexValue.value, 92 + LEX_VALUE_TYPES, 93 + ), 94 + ); 95 + } 96 + 76 97 return ctx.success( 77 98 input as TypedUnionSchemaOutput<TypedRefs, Closed>, 78 99 ); 79 100 } 80 101 } 102 + 103 + function findInvalidLexValue( 104 + value: unknown, 105 + path: PropertyKey[] = [], 106 + ): { path: PropertyKey[]; value: unknown } | undefined { 107 + switch (typeof value) { 108 + case "number": 109 + return Number.isInteger(value) ? undefined : { path, value }; 110 + case "string": 111 + case "boolean": 112 + return undefined; 113 + case "object": 114 + if (value === null || value instanceof Uint8Array || isCid(value)) { 115 + return undefined; 116 + } 117 + if (Array.isArray(value)) { 118 + for (let i = 0; i < value.length; i++) { 119 + const invalid = findInvalidLexValue(value[i], path.concat(i)); 120 + if (invalid) return invalid; 121 + } 122 + return undefined; 123 + } 124 + if (isPlainObject(value)) { 125 + for (const key in value as Record<string, unknown>) { 126 + const invalid = findInvalidLexValue( 127 + (value as Record<string, unknown>)[key], 128 + path.concat(key), 129 + ); 130 + if (invalid) return invalid; 131 + } 132 + return undefined; 133 + } 134 + return { path, value }; 135 + default: 136 + return { path, value }; 137 + } 138 + }
+141
lex/tests/typed-utils_test.ts
··· 1 + import { assert, assertEquals, assertStrictEquals } from "@std/assert"; 2 + import { l } from "@atp/lex"; 3 + import type { LexMap } from "@atp/lex/data"; 4 + 5 + const unknownTypedObject: l.Unknown$TypedObject = { 6 + $type: "com.example.unknown" as l.Unknown$Type, 7 + }; 8 + 9 + const lexMap: LexMap = unknownTypedObject; 10 + const nestedLexMap: LexMap = { 11 + arr: [unknownTypedObject], 12 + val: unknownTypedObject, 13 + }; 14 + 15 + Deno.test("$typed adds $type when missing", () => { 16 + assertEquals( 17 + l.$typed({ text: "hello" }, "com.example.post"), 18 + { 19 + $type: "com.example.post", 20 + text: "hello", 21 + }, 22 + ); 23 + }); 24 + 25 + Deno.test("$typed reuses typed values with the same $type", () => { 26 + const value: l.$Typed<{ text: string }, "com.example.post"> = { 27 + $type: "com.example.post", 28 + text: "hello", 29 + }; 30 + 31 + assertStrictEquals(l.$typed(value, "com.example.post"), value); 32 + }); 33 + 34 + Deno.test("$typed retags typed values without collapsing the result type", () => { 35 + const value: l.$Typed<{ text: string }, "com.example.old"> = { 36 + $type: "com.example.old", 37 + text: "hello", 38 + }; 39 + 40 + const retagged: l.$Typed<{ text: string }, "com.example.new"> = l.$typed( 41 + value, 42 + "com.example.new", 43 + ); 44 + 45 + assertEquals(retagged, { 46 + $type: "com.example.new", 47 + text: "hello", 48 + }); 49 + }); 50 + 51 + Deno.test("Un$Typed removes $type at the type level", () => { 52 + const value: l.Un$Typed<l.$Typed<{ text: string }, "com.example.post">> = { 53 + text: "hello", 54 + }; 55 + 56 + assertEquals(value, { text: "hello" }); 57 + }); 58 + 59 + Deno.test("$TypedMaybe allows omitting $type", () => { 60 + const value: l.$TypedMaybe<{ text: string }, "com.example.post"> = { 61 + text: "hello", 62 + }; 63 + 64 + assertEquals(value, { text: "hello" }); 65 + }); 66 + 67 + Deno.test("Unknown$TypedObject is assignable to LexMap", () => { 68 + assertEquals(lexMap, unknownTypedObject); 69 + assertEquals(nestedLexMap, { 70 + arr: [unknownTypedObject], 71 + val: unknownTypedObject, 72 + }); 73 + }); 74 + 75 + Deno.test("typedObject isTypeOf narrows Unknown$TypedObject", () => { 76 + const known = l.typedObject( 77 + "com.example.post", 78 + "main", 79 + l.object({ 80 + text: l.string(), 81 + }), 82 + ); 83 + const value: l.Unknown$TypedObject = { 84 + $type: "com.example.post" as l.Unknown$Type, 85 + }; 86 + 87 + if (known.isTypeOf(value)) { 88 + const narrowed: { $type?: "com.example.post" } = value; 89 + assertEquals(narrowed.$type, "com.example.post"); 90 + return; 91 + } 92 + 93 + assert(false); 94 + }); 95 + 96 + Deno.test("open typed unions accept unknown payloads that are LexMap values", () => { 97 + const known = l.typedObject( 98 + "com.example.post", 99 + "main", 100 + l.object({ 101 + text: l.string(), 102 + }), 103 + ); 104 + const union = l.typedUnion([l.typedRef(() => known)], false); 105 + const parsed = union.parse({ 106 + $type: "com.example.unknown", 107 + nested: { 108 + text: "hello", 109 + }, 110 + }); 111 + 112 + assertEquals( 113 + parsed as unknown, 114 + { 115 + $type: "com.example.unknown", 116 + nested: { 117 + text: "hello", 118 + }, 119 + }, 120 + ); 121 + }); 122 + 123 + Deno.test("open typed unions reject unknown payloads with non-LexMap values", () => { 124 + const known = l.typedObject( 125 + "com.example.post", 126 + "main", 127 + l.object({ 128 + text: l.string(), 129 + }), 130 + ); 131 + const union = l.typedUnion([l.typedRef(() => known)], false); 132 + const result = union.safeParse({ 133 + $type: "com.example.unknown", 134 + nested: { 135 + bad: () => "nope", 136 + }, 137 + }); 138 + 139 + assert(!result.success); 140 + assertEquals(result.error.issues[0]?.path, ["nested", "bad"]); 141 + });
+11 -8
xrpc-server/server.ts
··· 79 79 type XrpcMux, 80 80 } from "./stream/adapters.ts"; 81 81 82 - type LexAddConfig<M extends Procedure | Query | Subscription> = M extends 83 - Procedure | Query ? LexMethodConfig<M, Auth> | LexMethodHandler<M, void> 82 + type LexAddConfig< 83 + M extends Procedure | Query | Subscription, 84 + A extends Auth = Auth, 85 + > = M extends Procedure | Query 86 + ? LexMethodConfig<M, A> | LexMethodHandler<M, void> 84 87 : M extends Subscription 85 - ? LexSubscriptionConfig<M, Auth> | LexSubscriptionHandler<M, void> 88 + ? LexSubscriptionConfig<M, A> | LexSubscriptionHandler<M, void> 86 89 : never; 87 90 88 91 /** ··· 213 216 214 217 // handlers 215 218 216 - add<M extends Procedure | Query | Subscription>( 219 + add<M extends Procedure | Query | Subscription, A extends Auth = Auth>( 217 220 method: LexMethodLike<M>, 218 - configOrHandler: LexAddConfig<M>, 221 + configOrHandler: LexAddConfig<M, A>, 219 222 ): void { 220 223 const schema = getLexMethod(method); 221 224 const config = typeof configOrHandler === "function" ··· 225 228 if (schema instanceof Procedure) { 226 229 return this.addProcedureSchema( 227 230 schema, 228 - config as LexMethodConfig<Procedure, Auth>, 231 + config as LexMethodConfig<Procedure, A>, 229 232 ); 230 233 } 231 234 232 235 if (schema instanceof Query) { 233 236 return this.addQuerySchema( 234 237 schema, 235 - config as LexMethodConfig<Query, Auth>, 238 + config as LexMethodConfig<Query, A>, 236 239 ); 237 240 } 238 241 239 242 return this.addSubscriptionSchema( 240 243 schema, 241 - config as LexSubscriptionConfig<Subscription, Auth>, 244 + config as LexSubscriptionConfig<Subscription, A>, 242 245 ); 243 246 } 244 247
+27
xrpc-server/tests/lex_compat_test.ts
··· 121 121 }), 122 122 ); 123 123 124 + const inferredAuthQuery = l.query( 125 + "io.example.inferredAuthQuery", 126 + l.params(), 127 + l.jsonPayload({ 128 + authenticated: l.boolean(), 129 + }), 130 + ); 131 + 124 132 let server: xrpcServer.Server; 125 133 let httpServer: Deno.HttpServer; 126 134 let client: Client; ··· 184 192 handler: () => ({ 185 193 encoding: "application/json", 186 194 body: {} as unknown as { message: string }, 195 + }), 196 + }); 197 + 198 + server.add(inferredAuthQuery, { 199 + auth: () => ({ 200 + credentials: { type: "custom" as const }, 201 + }), 202 + handler: ({ auth }) => ({ 203 + encoding: "application/json", 204 + body: { authenticated: auth.credentials.type === "custom" }, 187 205 }), 188 206 }); 189 207 ··· 317 335 const response = await client.call(defaultedQuery); 318 336 319 337 assertEquals(response.data, { message: "hello default" }); 338 + }); 339 + 340 + Deno.test("infers auth types for lex sdk methods", { 341 + sanitizeOps: false, 342 + sanitizeResources: false, 343 + }, async () => { 344 + const response = await client.call(inferredAuthQuery); 345 + 346 + assertEquals(response.data, { authenticated: true }); 320 347 }); 321 348 322 349 Deno.test("registers subscriptions from lex sdk methods", {