feat(sdk): restore session runtime operations (#33777)

This commit is contained in:
Kit Langton 2026-06-25 20:23:01 +02:00 committed by GitHub
commit f44423609b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
20 changed files with 1099 additions and 92 deletions

View file

@ -37,6 +37,6 @@ The existing public `generate(Api, { directory })` operation writes the rich Eff
Generation formats TypeScript with Prettier before writing. Output paths are flat, unique, and checked against traversal, reserved manifest names, and existing symbolic links.
Generated source starts with one self-contained module per `HttpApiGroup`, plus root client and index modules. Schema dependencies may be duplicated across group modules. Cross-group schema partitioning is deferred until measured output or bundle cost requires it.
Portable Effect output uses one self-contained module per `HttpApiGroup`, plus root client and index modules. Promise output uses shared type and client modules, while imported Effect output keeps adapters in the root client module. Schema dependencies may be duplicated across portable Effect group modules. Cross-group schema partitioning is deferred until measured output or bundle cost requires it.
Codegen preserves group and endpoint identifiers exactly. The composed remote `HttpApi` owns public names such as `session` and `get`; the generator performs no prefix stripping, casing conversion, or public-name annotation mapping.
Codegen preserves transport identifiers internally. `compile` may explicitly map consumer-facing group names, and endpoint operation IDs are projected to their final dot-delimited segment. The generator performs no other implicit product-specific naming or public-name annotation mapping.

View file

@ -69,6 +69,7 @@ type Slot = {
const resolveHttpApiStatus = SchemaAST.resolveAt<number>("httpApiStatus")
const resolveHttpApiEncoding = SchemaAST.resolveAt<HttpApiSchema.Encoding>("~httpApiEncoding")
const resolveContentSchema = SchemaAST.resolveAt<SchemaAST.AST>("contentSchema")
const Manifest = Schema.fromJsonString(Schema.Array(Schema.String))
const manifestName = ".httpapi-codegen.json"
@ -125,9 +126,10 @@ export function compile<Id extends string, Groups extends HttpApiGroup.Any>(
...responseSchemas(success.schema, `${name}.success`),
...errorSchemas.map((item) => [`${name}.error`, item.schema] as const),
]
const effectPortable = [params, query, headers, ...payloads, success, ...errorSchemas].every(
(item) => item?.effectPortable !== false,
)
const effectPortable =
[params, query, headers, ...payloads, success, ...errorSchemas].every(
(item) => item?.effectPortable !== false,
) && streamEffectPortable(success.schema)
if (effectPortable) {
for (const [path, schema] of schemaPaths) assertPortable(schema, path, portable)
}
@ -454,7 +456,7 @@ function renderPromiseTypes(groups: ReadonlyArray<Group>) {
const success = typeOf(
isStreamSchema(successSchema) && successSchema._tag === "StreamSse"
? successSchema.sseMode === "data"
? streamDataSchema(successSchema)
? streamEncodedDataSchema(successSchema)
: successSchema.events
: successSchema,
)
@ -782,17 +784,6 @@ function responseSchemas(schema: Schema.Top, path: string): Array<readonly [stri
if (!isStreamSchema(schema)) return [[path, schema]]
if (schema._tag === "StreamUint8Array") return []
const value = schema.sseMode === "data" ? streamDataSchema(schema) : schema.events
const rebuilt =
schema.sseMode === "data"
? HttpApiSchema.StreamSse({ data: value, error: schema.error, contentType: schema.contentType })
: HttpApiSchema.StreamSse({
events: schema.events,
error: schema.error,
contentType: schema.contentType,
})
if (!sameEncoding(schema.events.ast, rebuilt.events.ast)) {
throw new GenerationError({ reason: `Unportable schema: ${path}.${schema.sseMode}` })
}
return [
[`${path}.${schema.sseMode}`, value],
[`${path}.error`, schema.error],
@ -964,11 +955,33 @@ function isStreamSchema(schema: Schema.Top): schema is HttpApiSchema.StreamSchem
}
function streamDataSchema(schema: Extract<HttpApiSchema.StreamSchema, { readonly _tag: "StreamSse" }>) {
const ast = Schema.toType(schema.events).ast
return Schema.make(streamDataAst(Schema.toType(schema.events).ast))
}
function streamEncodedDataSchema(schema: Extract<HttpApiSchema.StreamSchema, { readonly _tag: "StreamSse" }>) {
const data = streamDataAst(schema.events.ast)
const encodedAst = data.encoding?.at(-1)?.to
if (encodedAst === undefined) throw new GenerationError({ reason: "Invalid SSE data schema" })
const encoded = resolveContentSchema(encodedAst)
if (!SchemaAST.isAST(encoded)) throw new GenerationError({ reason: "Invalid SSE data schema" })
return Schema.make(encoded)
}
function streamDataAst(ast: SchemaAST.AST) {
if (!SchemaAST.isObjects(ast)) throw new GenerationError({ reason: "Invalid SSE data schema" })
const data = ast.propertySignatures.find((field) => field.name === "data")?.type
if (data === undefined) throw new GenerationError({ reason: "Invalid SSE data schema" })
return Schema.make(data)
return data
}
function streamEffectPortable(schema: Schema.Top) {
if (!isStreamSchema(schema) || schema._tag === "StreamUint8Array" || schema.sseMode === "events") return true
const rebuilt = HttpApiSchema.StreamSse({
data: streamDataSchema(schema),
error: schema.error,
contentType: schema.contentType,
})
return sameEncoding(schema.events.ast, rebuilt.events.ast)
}
function renderGroup(group: Group, groupIndex: number) {

View file

@ -395,7 +395,9 @@ describe("HttpApiCodegen.generate", () => {
api(
HttpApiEndpoint.get("subscribe", "/event", {
query: { after: Schema.optional(Schema.Number) },
success: HttpApiSchema.StreamSse({ data: Schema.Struct({ type: Schema.String }) }),
success: HttpApiSchema.StreamSse({
data: Schema.Struct({ type: Schema.String, count: Schema.NumberFromString }),
}),
}),
),
),
@ -416,7 +418,7 @@ describe("HttpApiCodegen.generate", () => {
return new Response(
new ReadableStream({
start(controller) {
controller.enqueue(encoder.encode('data: {"type":"ready"}\r'))
controller.enqueue(encoder.encode('data: {"type":"ready","count":"1"}\r'))
controller.enqueue(encoder.encode("\n\r\n"))
controller.close()
},
@ -430,7 +432,7 @@ describe("HttpApiCodegen.generate", () => {
expect(requests).toBe(0)
const received = []
for await (const event of events) received.push(event)
expect(received).toEqual([{ type: "ready" }])
expect(received).toEqual([{ type: "ready", count: "1" }])
expect(requests).toBe(1)
expect(url).toBe("https://example.com/event?after=2")
} finally {