From 5373c8b4a3492eb72f0a856ad8b483ac2bdc5799 Mon Sep 17 00:00:00 2001 From: George Fu Date: Thu, 13 Aug 2026 15:53:30 +0000 Subject: [PATCH] feat(server-common): event stream support --- .changeset/beige-dancers-fix.md | 5 + .changeset/quiet-tables-lay.md | 5 + .changeset/red-cooks-think.md | 5 + .../cbor/SmithyRpcV2CborProtocol.ts | 6 +- .../event-streams/EventStreamSerde.spec.ts | 63 ++ .../event-streams/EventStreamSerde.ts | 18 +- .../eventstream-cbor.integ.spec.ts | 48 +- .../src/submodules/protocols/RpcProtocol.ts | 7 +- .../contentLengthMiddleware.ts | 10 +- packages/server-common/README.md | 213 ++++++ .../HttpServerProtocol.ts | 94 +++ .../RestServerProtocol.spec.ts | 52 +- .../layer-1-abstracts/RestServerProtocol.ts | 34 +- .../layer-1-abstracts/RpcServerProtocol.ts | 76 +- .../SmithyRpcV2CborServerProtocol.ts | 21 + .../test/schema-server.integ.spec.ts | 411 ++++++++++- packages/server-node/README.md | 104 +++ .../server-node/src/node-http-converters.ts | 15 +- .../src/models/models_0.ts | 173 ++++- .../src/schemas/schemas_0.ts | 132 +++- .../src/server/XYZServiceHandler.ts | 10 + .../my-local-model-schema/src/XYZService.ts | 48 ++ .../src/XYZServiceClient.ts | 9 + .../src/commands/PublishEventsCommand.ts | 88 +++ .../src/commands/SubscribeToEventsCommand.ts | 87 +++ .../src/commands/TradeEventStreamCommand.ts | 22 +- .../src/commands/index.ts | 2 + .../src/models/models_0.ts | 173 ++++- .../src/runtimeConfig.ts | 7 +- .../src/schemas/schemas_0.ts | 132 +++- .../test/index-objects.spec.mjs | 32 + .../my-local-model-schema/test/index-types.ts | 18 + .../test/snapshots.integ.spec.ts | 6 + .../test/snapshots/req/PublishEvents.txt | 70 ++ .../test/snapshots/req/SubscribeToEvents.txt | 21 + .../test/snapshots/req/TradeEventStream.txt | 29 +- .../test/snapshots/res/PublishEvents.txt | 56 ++ .../test/snapshots/res/SubscribeToEvents.txt | 101 +++ .../test/snapshots/res/TradeEventStream.txt | 29 +- private/my-local-model/package.json | 2 +- private/my-local-model/src/XYZService.ts | 48 ++ .../my-local-model/src/XYZServiceClient.ts | 9 + .../src/commands/PublishEventsCommand.ts | 118 +++ .../src/commands/SubscribeToEventsCommand.ts | 117 +++ .../src/commands/TradeEventStreamCommand.ts | 22 +- private/my-local-model/src/commands/index.ts | 2 + private/my-local-model/src/models/models_0.ts | 229 +++++- .../my-local-model/src/protocols/Rpcv2cbor.ts | 669 ++++++++++++------ private/my-local-model/src/runtimeConfig.ts | 7 +- .../integration/AddHttp2Dependency.java | 103 +++ ....codegen.integration.TypeScriptIntegration | 1 + .../my-local-model/my-local-model.smithy | 106 ++- 52 files changed, 3513 insertions(+), 352 deletions(-) create mode 100644 .changeset/beige-dancers-fix.md create mode 100644 .changeset/quiet-tables-lay.md create mode 100644 .changeset/red-cooks-think.md create mode 100644 private/my-local-model-schema/src/commands/PublishEventsCommand.ts create mode 100644 private/my-local-model-schema/src/commands/SubscribeToEventsCommand.ts create mode 100644 private/my-local-model-schema/test/snapshots/req/PublishEvents.txt create mode 100644 private/my-local-model-schema/test/snapshots/req/SubscribeToEvents.txt create mode 100644 private/my-local-model-schema/test/snapshots/res/PublishEvents.txt create mode 100644 private/my-local-model-schema/test/snapshots/res/SubscribeToEvents.txt create mode 100644 private/my-local-model/src/commands/PublishEventsCommand.ts create mode 100644 private/my-local-model/src/commands/SubscribeToEventsCommand.ts create mode 100644 smithy-typescript-codegen/src/main/java/software/amazon/smithy/typescript/codegen/integration/AddHttp2Dependency.java diff --git a/.changeset/beige-dancers-fix.md b/.changeset/beige-dancers-fix.md new file mode 100644 index 00000000000..dff5f961fc8 --- /dev/null +++ b/.changeset/beige-dancers-fix.md @@ -0,0 +1,5 @@ +--- +"@smithy/core": patch +--- + +fix for RPC protocol event stream initial messages diff --git a/.changeset/quiet-tables-lay.md b/.changeset/quiet-tables-lay.md new file mode 100644 index 00000000000..5bdee549575 --- /dev/null +++ b/.changeset/quiet-tables-lay.md @@ -0,0 +1,5 @@ +--- +"@smithy/server-node": patch +--- + +handle streaming bodies in node-http-converters diff --git a/.changeset/red-cooks-think.md b/.changeset/red-cooks-think.md new file mode 100644 index 00000000000..7ca2f4886e2 --- /dev/null +++ b/.changeset/red-cooks-think.md @@ -0,0 +1,5 @@ +--- +"@smithy/server-common": minor +--- + +event stream support for schema-based server SDK diff --git a/packages/core/src/submodules/cbor/SmithyRpcV2CborProtocol.ts b/packages/core/src/submodules/cbor/SmithyRpcV2CborProtocol.ts index 04584b0d7d4..3b9f9597ad1 100644 --- a/packages/core/src/submodules/cbor/SmithyRpcV2CborProtocol.ts +++ b/packages/core/src/submodules/cbor/SmithyRpcV2CborProtocol.ts @@ -67,9 +67,9 @@ export class SmithyRpcV2CborProtocol extends RpcProtocol { this.serializer.write(15, {}); request.body = this.serializer.flush(); } - try { - request.headers["content-length"] = String((request.body as Uint8Array).byteLength); - } catch (ignored) {} + if (request.body instanceof Uint8Array) { + request.headers["content-length"] = String(request.body.byteLength); + } } const { service, operation } = getSmithyContext(context) as { service: string; diff --git a/packages/core/src/submodules/event-streams/EventStreamSerde.spec.ts b/packages/core/src/submodules/event-streams/EventStreamSerde.spec.ts index c30f69d3881..ad466e8d804 100644 --- a/packages/core/src/submodules/event-streams/EventStreamSerde.spec.ts +++ b/packages/core/src/submodules/event-streams/EventStreamSerde.spec.ts @@ -16,6 +16,7 @@ import type { import { describe, expect, test as it } from "vitest"; import { EventStreamSerde } from "./EventStreamSerde"; +import { EventStreamCodec } from "./eventstream-codec/EventStreamCodec"; import { EventStreamMarshaller } from "./eventstream-serde/EventStreamMarshaller"; describe(EventStreamSerde.name, () => { @@ -763,4 +764,66 @@ describe(EventStreamSerde.name, () => { }); }); }); + + describe("initial-request serialization correctness", () => { + it("serializes initial-request string members as strings, not pre-encoded bytes", async () => { + // This test verifies that when raw values (not pre-serialized bytes) are + // passed as initialRequest, the CBOR serializer correctly encodes them as + // their proper types. This catches a regression where pre-serialized + // Uint8Array values were passed, causing strings to be double-encoded as blobs. + const cborCodec = new CborCodec(); + const marshaller = new EventStreamMarshaller({ utf8Encoder: toUtf8, utf8Decoder: fromUtf8 }); + const serde = new EventStreamSerde({ + marshaller, + serializer: cborCodec.createSerializer(), + deserializer: cborCodec.createDeserializer(), + defaultContentType: "application/cbor", + }); + + // Schema with a string member and a streaming union member. + const streamingUnion: StaticStructureSchema = [ + 4, + "ns", + "Events", + { streaming: 1 }, + ["alpha"], + [[3, "ns", "Alpha", 0, ["id"], [0 satisfies StringSchema]] satisfies StaticStructureSchema], + ] as any; + const containerSchema: StaticStructureSchema = [ + 3, + "ns", + "Container", + 0, + ["sessionId", "eventStream"], + [0 satisfies StringSchema, [() => streamingUnion, 0]], + ]; + + const requestBody = await serde.serializeEventStream({ + eventStream: (async function* () {})(), + requestSchema: NormalizedSchema.of(containerSchema), + initialRequest: { + sessionId: "my-session-value", + }, + }); + + // Collect and decode the initial-request message. + const chunks: Uint8Array[] = []; + for await (const chunk of requestBody as AsyncIterable) { + chunks.push(chunk); + } + const fullBody = Buffer.concat(chunks); + + // Read the first message (initial-request). + const totalLength = fullBody.readUInt32BE(0); + const codec = new EventStreamCodec(toUtf8, fromUtf8); + const firstMessage = codec.decode(fullBody.subarray(0, totalLength)); + + expect(firstMessage.headers[":event-type"]?.value).toBe("initial-request"); + + // Decode the CBOR payload and verify sessionId is a string, not bytes. + const decoded = cbor.deserialize(firstMessage.body); + expect(typeof decoded.sessionId).toBe("string"); + expect(decoded.sessionId).toBe("my-session-value"); + }); + }); }); diff --git a/packages/core/src/submodules/event-streams/EventStreamSerde.ts b/packages/core/src/submodules/event-streams/EventStreamSerde.ts index 4620ee96b1e..296be7163c4 100644 --- a/packages/core/src/submodules/event-streams/EventStreamSerde.ts +++ b/packages/core/src/submodules/event-streams/EventStreamSerde.ts @@ -67,10 +67,17 @@ export class EventStreamSerde { eventStream, requestSchema, initialRequest, + initialMessageType, }: { eventStream: AsyncIterable; requestSchema: NormalizedSchema; initialRequest?: any; + /** + * The :event-type header value for the initial message. + * Defaults to "initial-request" (client→server). + * Server→client should pass "initial-response". + */ + initialMessageType?: string; }): Promise { const marshaller = this.marshaller; const eventStreamMember = requestSchema.getEventStreamMember(); @@ -85,7 +92,7 @@ export class EventStreamSerde { async *[Symbol.asyncIterator]() { if (initialRequest) { const headers: MessageHeaders = { - ":event-type": { type: "string", value: "initial-request" }, + ":event-type": { type: "string", value: initialMessageType ?? "initial-request" }, ":message-type": { type: "string", value: "event" }, ":content-type": { type: "string", value: defaultContentType }, }; @@ -150,10 +157,17 @@ export class EventStreamSerde { response, responseSchema, initialResponseContainer, + initialMessageType, }: { response: IHttpResponse; responseSchema: NormalizedSchema; initialResponseContainer?: any; + /** + * The :event-type header value to match as the initial message. + * Defaults to "initial-response" (server→client). + * Server-side deserialization of client requests should pass "initial-request". + */ + initialMessageType?: string; }): Promise> { const marshaller = this.marshaller; const eventStreamMember = responseSchema.getEventStreamMember(); @@ -173,7 +187,7 @@ export class EventStreamSerde { const body = event[unionMember].body; - if (unionMember === "initial-response") { + if (unionMember === (initialMessageType ?? "initial-response")) { const dataObject = await this.deserializer.read(responseSchema, body); delete dataObject[eventStreamMember]; return { diff --git a/packages/core/src/submodules/event-streams/eventstream-serde-universal/eventstream-cbor.integ.spec.ts b/packages/core/src/submodules/event-streams/eventstream-serde-universal/eventstream-cbor.integ.spec.ts index 4654a3b41cd..a364ad6c750 100644 --- a/packages/core/src/submodules/event-streams/eventstream-serde-universal/eventstream-cbor.integ.spec.ts +++ b/packages/core/src/submodules/event-streams/eventstream-serde-universal/eventstream-cbor.integ.spec.ts @@ -1,6 +1,7 @@ // oxlint-disable no-useless-spread import { Readable } from "node:stream"; import { cbor, dateToTag } from "@smithy/core/cbor"; +import { EventStreamCodec } from "../eventstream-codec/EventStreamCodec"; import { HttpResponse } from "@smithy/core/protocols"; import { requireRequestsFrom } from "@smithy/util-test/src"; import { describe, expect, test as it } from "vitest"; @@ -28,6 +29,10 @@ describe("local model integration test for cbor eventstreams", () => { return [...uint32]; } + const toUtf8 = (input: Uint8Array): string => new TextDecoder().decode(input); + const fromUtf8 = (input: string): Uint8Array => new TextEncoder().encode(input); + const codec = new EventStreamCodec(toUtf8, fromUtf8); + requireRequestsFrom(client) .toMatch({ hostname: /localhost/, @@ -37,25 +42,30 @@ describe("local model integration test for cbor eventstreams", () => { outgoing.push(chunk); } expect(outgoing).toEqual([ - new Uint8Array([ - 0, 0, 0, 101, 0, 0, 0, 75, 213, 254, 191, 76, 11, 58, 101, 118, 101, 110, 116, 45, 116, 121, 112, 101, 7, - 0, 5, 97, 108, 112, 104, 97, 13, 58, 109, 101, 115, 115, 97, 103, 101, 45, 116, 121, 112, 101, 7, 0, 5, - 101, 118, 101, 110, 116, 13, 58, 99, 111, 110, 116, 101, 110, 116, 45, 116, 121, 112, 101, 7, 0, 16, 97, - 112, 112, 108, 105, 99, 97, 116, 105, 111, 110, 47, 99, 98, 111, 114, 161, 98, 105, 100, 101, 97, 108, - 112, 104, 97, 32, 93, 69, 236, - ]), - new Uint8Array([ - 0, 0, 0, 91, 0, 0, 0, 74, 188, 232, 137, 61, 11, 58, 101, 118, 101, 110, 116, 45, 116, 121, 112, 101, 7, - 0, 4, 98, 101, 116, 97, 13, 58, 109, 101, 115, 115, 97, 103, 101, 45, 116, 121, 112, 101, 7, 0, 5, 101, - 118, 101, 110, 116, 13, 58, 99, 111, 110, 116, 101, 110, 116, 45, 116, 121, 112, 101, 7, 0, 16, 97, 112, - 112, 108, 105, 99, 97, 116, 105, 111, 110, 47, 99, 98, 111, 114, 160, 195, 209, 62, 47, - ]), - new Uint8Array([ - 0, 0, 0, 91, 0, 0, 0, 74, 188, 232, 137, 61, 11, 58, 101, 118, 101, 110, 116, 45, 116, 121, 112, 101, 7, - 0, 4, 98, 101, 116, 97, 13, 58, 109, 101, 115, 115, 97, 103, 101, 45, 116, 121, 112, 101, 7, 0, 5, 101, - 118, 101, 110, 116, 13, 58, 99, 111, 110, 116, 101, 110, 116, 45, 116, 121, 112, 101, 7, 0, 16, 97, 112, - 112, 108, 105, 99, 97, 116, 105, 111, 110, 47, 99, 98, 111, 114, 160, 195, 209, 62, 47, - ]), + codec.encode({ + headers: { + ":event-type": { type: "string", value: "alpha" }, + ":message-type": { type: "string", value: "event" }, + ":content-type": { type: "string", value: "application/cbor" }, + }, + body: cbor.serialize({ id: "alpha" }), + }), + codec.encode({ + headers: { + ":event-type": { type: "string", value: "beta" }, + ":message-type": { type: "string", value: "event" }, + ":content-type": { type: "string", value: "application/cbor" }, + }, + body: cbor.serialize({}), + }), + codec.encode({ + headers: { + ":event-type": { type: "string", value: "gamma" }, + ":message-type": { type: "string", value: "event" }, + ":content-type": { type: "string", value: "application/cbor" }, + }, + body: new Uint8Array(), + }), new Uint8Array(), ]); }, diff --git a/packages/core/src/submodules/protocols/RpcProtocol.ts b/packages/core/src/submodules/protocols/RpcProtocol.ts index eb68b1e9b3b..4c866531df4 100644 --- a/packages/core/src/submodules/protocols/RpcProtocol.ts +++ b/packages/core/src/submodules/protocols/RpcProtocol.ts @@ -64,10 +64,9 @@ export abstract class RpcProtocol extends HttpProtocol { if (eventStreamMember) { if (input[eventStreamMember]) { const initialRequest = {} as any; - for (const [memberName, memberSchema] of ns.structIterator()) { - if (memberName !== eventStreamMember && input[memberName]) { - serializer.write(memberSchema, input[memberName]); - initialRequest[memberName] = serializer.flush(); + for (const [memberName] of ns.structIterator()) { + if (memberName !== eventStreamMember && input[memberName] != null) { + initialRequest[memberName] = input[memberName]; } } diff --git a/packages/core/src/submodules/protocols/middleware-content-length/contentLengthMiddleware.ts b/packages/core/src/submodules/protocols/middleware-content-length/contentLengthMiddleware.ts index d292dae7d4c..bb50433e069 100644 --- a/packages/core/src/submodules/protocols/middleware-content-length/contentLengthMiddleware.ts +++ b/packages/core/src/submodules/protocols/middleware-content-length/contentLengthMiddleware.ts @@ -26,10 +26,12 @@ export function contentLengthMiddleware(bodyLengthChecker: BodyLengthCalculator) ) { try { const length = bodyLengthChecker(body); - request.headers = { - ...request.headers, - [CONTENT_LENGTH_HEADER]: String(length), - }; + if (length != null) { + request.headers = { + ...request.headers, + [CONTENT_LENGTH_HEADER]: String(length), + }; + } } catch (ignored) { // ToDo: Add 'transfer-encoding' as chunked only for HTTP/1.1 request // Refs: https://github.com/aws/aws-sdk-js-v3/pull/3403 diff --git a/packages/server-common/README.md b/packages/server-common/README.md index bfb8fc8d68c..802481a92a7 100644 --- a/packages/server-common/README.md +++ b/packages/server-common/README.md @@ -379,6 +379,219 @@ export async function handler(event: APIGatewayProxyEvent): Promise` in both input and output +types. To consume an incoming stream, iterate it with `for await`. To produce +an outgoing stream, return an async generator. + +```typescript +const serviceHandler = new MyServiceHandler({ + protocols: [/* ... */], + handlers: { + // Output-only: return an async generator for the response stream. + async SubscribeToEvents(input) { + const channel = input.channel ?? "default"; + return { + subscriptionId: `sub-${channel}`, + events: (async function* () { + for (let i = 0; i < 10; i++) { + yield { notification: { topic: channel, payload: `event-${i}` } }; + await sleep(1000); + } + })(), + }; + }, + + // Input-only: consume the incoming stream, return a normal response. + async PublishEvents(input) { + let count = 0; + for await (const event of input.events) { + count++; + processEvent(event); + } + return { eventCount: count, message: `Processed ${count} events` }; + }, + + // Bidirectional: consume input stream and produce output stream. + async Chat(input) { + const inputMessages = input.messages; + return { + sessionId: `ack-${input.sessionId}`, + messages: (async function* () { + for await (const msg of inputMessages) { + // Echo back a response for each incoming message. + yield { response: { text: `Got: ${msg.message?.text}` } }; + } + })(), + }; + }, + }, +}); +``` + +#### Client-side usage + +From the client SDK, event stream operations use the same async iterable +pattern: + +```typescript +import { MyServiceClient, SubscribeToEventsCommand, PublishEventsCommand } from "@example/my-client"; + +const client = new MyServiceClient({ endpoint: "http://localhost:8080" }); + +// Output-only: iterate the response stream. +const response = await client.send(new SubscribeToEventsCommand({ channel: "news" })); +console.log(response.subscriptionId); +for await (const event of response.events) { + console.log(event.notification?.payload); +} + +// Input-only: pass an async generator as the request stream. +await client.send( + new PublishEventsCommand({ + channel: "metrics", + events: (async function* () { + yield { metric: { name: "cpu", value: 0.85 } }; + yield { metric: { name: "mem", value: 0.6 } }; + })(), + }) +); +``` + +#### HTTP transport requirements + +Event streams use the `application/vnd.amazon.eventstream` binary framing +format. + +- **Output-only streams** work over HTTP/1.1 using chunked transfer encoding + on the response. +- **Input-only streams** can work over HTTP/1.1 (chunked request body) or + HTTP/2, depending on the service's `eventStreamHttp` trait. +- **Bidirectional streams** require HTTP/2 for full-duplex communication. + +See the [`@smithy/server-node` README](../server-node/README.md) for an example +of setting up an HTTP/2 server that supports bidirectional event streams. + +When the service's protocol trait includes `eventStreamHttp: ["h2"]`, the +generated client automatically uses `NodeHttp2Handler`: + +```smithy +@rpcv2Cbor( + http: ["h2", "http/1.1"] + eventStreamHttp: ["h2"] +) +service MyService { /* ... */ } +``` + +Output-only event streams work over HTTP/1.1 (the request is normal; only the +response body streams). + +#### RPC vs REST protocol differences + +For **RPC protocols** (Smithy RPC v2 CBOR, AWS JSON 1.0/1.1), non-stream +members of the input/output are serialized as an `initial-request` or +`initial-response` message — the first event in the stream. + +For **REST protocols** (AWS restJson1), non-stream members are bound to HTTP +headers, URI path labels, or query parameters. They do not appear in the +event stream itself. The event stream member must carry `@httpPayload`. + +#### Lambda / API Gateway limitations + +> **Important:** AWS Lambda does not support incoming request streams or +> bidirectional event streams. The Lambda execution model buffers the full +> request body before invoking the handler, and does not support streaming +> the request. +> +> - **Output-only streams** (server→client) are supported via Lambda response +> streaming (`awslambda.streamifyResponse`) with HTTP API (API Gateway v2). +> - **Input-only and bidirectional streams** are **not supported** on Lambda. +> These require a long-lived connection (e.g., a Node.js HTTP/2 server on +> EC2, ECS, or Fargate). +> +> The `@smithy/server-apigateway` adapter does not support event stream +> operations. Use `@smithy/server-node` with an HTTP/2 server for full +> event stream support. + ### Passing User Context The `handle` method's second argument is a user-defined context object that diff --git a/packages/server-common/src/protocols-schema/layer-0-interface-and-base/HttpServerProtocol.ts b/packages/server-common/src/protocols-schema/layer-0-interface-and-base/HttpServerProtocol.ts index e70fdf8d6f4..e4e80534de9 100644 --- a/packages/server-common/src/protocols-schema/layer-0-interface-and-base/HttpServerProtocol.ts +++ b/packages/server-common/src/protocols-schema/layer-0-interface-and-base/HttpServerProtocol.ts @@ -9,6 +9,8 @@ import type { StaticOperationSchema, } from "@smithy/types"; import { HttpResponse } from "@smithy/core/protocols"; +import type { NormalizedSchema } from "@smithy/core/schema"; +import type { EventStreamSerde } from "@smithy/core/event-streams"; import type { SmithyFrameworkException } from "../../validation/errors"; import { isFrameworkException } from "../../validation/errors"; import { ServiceException } from "../../validation/errors"; @@ -165,4 +167,96 @@ export abstract class HttpServerProtocol } return (output as any).$fault === "client" || (output as any).$fault === "server"; } + + /** + * Serializes an AsyncIterable of events into a binary event stream body. + * + * @param eventStream - the iterable of events provided by the handler. + * @param responseSchema - the schema of the output structure containing the event stream member. + * @param initialResponse - for RPC protocols, non-stream members serialized as initial-response. + * + * @returns an AsyncIterable of Uint8Array chunks suitable for the HTTP response body. + * + * @internal + */ + protected async serializeEventStream({ + eventStream, + responseSchema, + initialResponse, + }: { + eventStream: AsyncIterable; + responseSchema: NormalizedSchema; + initialResponse?: any; + }): Promise> { + const eventStreamSerde = await this.loadEventStreamCapability(); + // Reuse serializeEventStream which produces a marshalled binary stream. + // Pass initialMessageType="initial-response" because on the server we + // emit the initial message as a response, not a request. + const body = await eventStreamSerde.serializeEventStream({ + eventStream, + requestSchema: responseSchema, + initialRequest: initialResponse, + initialMessageType: "initial-response", + }); + return body as AsyncIterable; + } + + /** + * Deserializes a binary event stream body into an AsyncIterable of typed events. + * + * @param request - the HTTP request whose body contains the event stream. + * @param requestSchema - the schema of the input structure containing the event stream member. + * @param initialRequestContainer - for RPC protocols, populated with initial-request members. + * + * @returns the AsyncIterable of deserialized events. + * + * @internal + */ + protected async deserializeEventStream({ + request, + requestSchema, + initialRequestContainer, + }: { + request: IHttpRequest; + requestSchema: NormalizedSchema; + initialRequestContainer?: any; + }): Promise> { + const eventStreamSerde = await this.loadEventStreamCapability(); + // Reuse deserializeEventStream. It operates on an IHttpResponse shape + // but only reads `.body`, so we can adapt the request body. + // Pass initialMessageType="initial-request" because on the server we + // receive the initial message as a request, not a response. + const pseudoResponse = new HttpResponse({ + statusCode: 200, + headers: {}, + body: request.body, + }); + return eventStreamSerde.deserializeEventStream({ + response: pseudoResponse, + responseSchema: requestSchema, + initialResponseContainer: initialRequestContainer, + initialMessageType: "initial-request", + }); + } + + /** + * Lazily loads the EventStreamSerde capability. + * + * @internal + */ + private async loadEventStreamCapability(): Promise { + const { EventStreamSerde, UniversalEventStreamMarshaller } = await import("@smithy/core/event-streams"); + const { fromUtf8, toUtf8 } = await import("@smithy/core/serde"); + const marshaller = new UniversalEventStreamMarshaller({ + utf8Encoder: this.serdeContext?.utf8Encoder ?? toUtf8, + utf8Decoder: this.serdeContext?.utf8Decoder ?? fromUtf8, + }); + return new EventStreamSerde({ + marshaller, + serializer: this.serializer as ShapeSerializer, + deserializer: this.deserializer as ShapeDeserializer, + serdeContext: this.serdeContext, + defaultContentType: this.getDefaultContentType(), + }); + } } diff --git a/packages/server-common/src/protocols-schema/layer-1-abstracts/RestServerProtocol.spec.ts b/packages/server-common/src/protocols-schema/layer-1-abstracts/RestServerProtocol.spec.ts index 53ce706d33d..c8a796121e7 100644 --- a/packages/server-common/src/protocols-schema/layer-1-abstracts/RestServerProtocol.spec.ts +++ b/packages/server-common/src/protocols-schema/layer-1-abstracts/RestServerProtocol.spec.ts @@ -2,7 +2,6 @@ import { beforeEach, describe, expect, it, vi } from "vitest"; import type * as schema from "@smithy/core/schema"; import { NormalizedSchema } from "@smithy/core/schema"; import { RestServerProtocol } from "./RestServerProtocol"; -import { SerializationException } from "../../validation/errors"; import type { HttpRequest as IHttpRequest, HttpResponse as IHttpResponse, @@ -306,7 +305,7 @@ describe("RestServerProtocol", () => { }); describe("deserializeRequest - httpPayload", () => { - it("throws SerializationException for streaming event stream", async () => { + it("deserializes streaming event stream via event stream serde", async () => { // A streaming union with @httpPayload — the input struct has one member // "events" targeting a union schema with { streaming: 1 }, and the member // itself carries { httpPayload: 1 }. @@ -322,10 +321,13 @@ describe("RestServerProtocol", () => { "unit", ] satisfies StaticOperationSchema; - const request = makeRequest({ path: "/test", method: "POST" }); - await expect(protocol.deserializeRequest(opSchema, makeContext(), request)).rejects.toBeInstanceOf( - SerializationException - ); + // Provide an empty async iterable as the body (simulates binary event stream). + const fakeBody = (async function* () {})(); + const request = makeRequest({ path: "/test", method: "POST", body: fakeBody } as any); + const result: any = await protocol.deserializeRequest(opSchema, makeContext(), request); + // The event stream member is populated with an async iterable. + expect(result.events).toBeDefined(); + expect(result.events[Symbol.asyncIterator]).toBeDefined(); }); it("passes streaming blob body through directly", async () => { @@ -379,28 +381,26 @@ describe("RestServerProtocol", () => { spy.mockRestore(); }); - it("throws SerializationException for event stream in response payload", async () => { - const outputNs = { - structIterator: function* () { - yield [ - "events", - { - getMergedTraits: () => ({ httpPayload: 1 }), - isStreaming: () => true, - isStructSchema: () => true, - isBlobSchema: () => false, - }, - ]; - }, - }; + it("serializes event stream in response payload as binary stream", async () => { + // A streaming union with @httpPayload — the output struct has one member + // "events" targeting a union schema with { streaming: 1 }, and the member + // itself carries { httpPayload: 1 }. + const streamingUnion: any = [4, "test", "Events", { streaming: 1 }, ["a"], [0]]; + const outputSchema: any = [3, "test", "Output", 0, ["events"], [[() => streamingUnion, { httpPayload: 1 }]]]; - const spy = vi.spyOn(NormalizedSchema, "of").mockReturnValue(outputNs as any); + const opSchema = [ + 9, + "test", + "Op", + { http: ["POST", "/test", 200] }, + "unit", + () => outputSchema, + ] satisfies StaticOperationSchema; - const opSchema = { input: {}, output: {}, traits: {} } as unknown as $OperationSchema; - await expect((protocol as any).serializeSuccess(opSchema, makeContext(), { events: {} })).rejects.toBeInstanceOf( - SerializationException - ); - spy.mockRestore(); + const fakeEvents = (async function* () {})(); + const response = await (protocol as any).serializeSuccess(opSchema, makeContext(), { events: fakeEvents }); + // The response should have event stream content type. + expect(response.headers["content-type"]).toBe("application/vnd.amazon.eventstream"); }); it("passes streaming blob body through to response", async () => { diff --git a/packages/server-common/src/protocols-schema/layer-1-abstracts/RestServerProtocol.ts b/packages/server-common/src/protocols-schema/layer-1-abstracts/RestServerProtocol.ts index 9bd666e8e2c..4f6492c0f14 100644 --- a/packages/server-common/src/protocols-schema/layer-1-abstracts/RestServerProtocol.ts +++ b/packages/server-common/src/protocols-schema/layer-1-abstracts/RestServerProtocol.ts @@ -7,7 +7,6 @@ import type { StaticOperationSchema, } from "@smithy/types"; import { HttpServerProtocol } from "../layer-0-interface-and-base/HttpServerProtocol"; -import { SerializationException } from "../../validation/errors"; /** * Abstract base for REST (HTTP binding) server protocols. @@ -94,12 +93,17 @@ export abstract class RestServerProtocol extends HttpServerProtocol { // This member is the entire body. if (memberSchema.isStreaming()) { if (memberSchema.isStructSchema()) { - // Event stream (streaming union) — not yet supported on the server. - // TODO: implement event stream deserialization for server requests. - throw new SerializationException(); + // Event stream (streaming union). + // In REST protocols, initial-request members are bound to HTTP + // headers/URI/query — they are NOT part of the event stream. + callerInput[memberName] = await this.deserializeEventStream({ + request, + requestSchema: ns, + }); + } else { + // Data stream (streaming blob) — pass body through to the handler. + callerInput[memberName] = request.body; } - // Data stream (streaming blob) — pass body through to the handler. - callerInput[memberName] = request.body; } else if (memberSchema.isBlobSchema()) { callerInput[memberName] = await collectBody(request.body, context); } else { @@ -185,12 +189,20 @@ export abstract class RestServerProtocol extends HttpServerProtocol { payloadMember = memberName; if (memberSchema.isStreaming()) { if (memberSchema.isStructSchema()) { - // Event stream (streaming union) — not yet supported on the server. - // TODO: implement event stream serialization for server responses. - throw new SerializationException(); + // Event stream (streaming union). + // In REST protocols, initial-response members are bound to HTTP + // headers — they are NOT part of the event stream. + const eventIterable = value as AsyncIterable; + const eventBody = await this.serializeEventStream({ + eventStream: eventIterable, + responseSchema: ns, + }); + body = eventBody as any; + headers["content-type"] = "application/vnd.amazon.eventstream"; + } else { + // Data stream (streaming blob) — pass through to the response body. + body = value; } - // Data stream (streaming blob) — pass through to the response body. - body = value; } else if (memberSchema.isBlobSchema()) { body = value; } else { diff --git a/packages/server-common/src/protocols-schema/layer-1-abstracts/RpcServerProtocol.ts b/packages/server-common/src/protocols-schema/layer-1-abstracts/RpcServerProtocol.ts index 4dada3c14a5..d3ea432403a 100644 --- a/packages/server-common/src/protocols-schema/layer-1-abstracts/RpcServerProtocol.ts +++ b/packages/server-common/src/protocols-schema/layer-1-abstracts/RpcServerProtocol.ts @@ -22,10 +22,15 @@ export abstract class RpcServerProtocol extends HttpServerProtocol { context: SerdeFunctions, request: IHttpRequest ): Promise { - this.validateContentType(request); - this.validateAccept(request); - const ns = NormalizedSchema.of(operationSchema[4]); + const eventStreamMember = ns.getEventStreamMember(); + + // For event stream operations, the Content-Type is + // application/vnd.amazon.eventstream, not the protocol's default. + if (!eventStreamMember) { + this.validateContentType(request); + this.validateAccept(request); + } if (ns.getSchema() === "unit") { // discard body stream. @@ -33,6 +38,21 @@ export abstract class RpcServerProtocol extends HttpServerProtocol { return {} as Input; } + if (eventStreamMember) { + // RPC event stream input: the body is a binary event stream. + // The initial-request message contains non-stream members. + const initialRequestContainer: Record = {}; + const eventIterable = await this.deserializeEventStream({ + request, + requestSchema: ns, + initialRequestContainer, + }); + + const input: any = { ...initialRequestContainer }; + input[eventStreamMember] = eventIterable; + return input as Input; + } + const bytes = await collectBody(request.body, context); if (bytes.byteLength === 0) { @@ -44,7 +64,13 @@ export abstract class RpcServerProtocol extends HttpServerProtocol { } /** - * Serializes a successful RPC response. The entire output is in the body. + * Serializes a successful RPC response. + * + * For event stream operations (output has a streaming union member): + * - The response body is a binary event stream. + * - The first message is `initial-response` containing non-stream members. + * - The remaining messages are the event stream from the handler. + * - The response Content-Type is `application/vnd.amazon.eventstream`. */ protected override async serializeSuccess( operationSchema: StaticOperationSchema, @@ -54,6 +80,48 @@ export abstract class RpcServerProtocol extends HttpServerProtocol { const ns = NormalizedSchema.of(operationSchema[5]); const schema = ns.getSchema(); + const eventStreamMember = ns.getEventStreamMember(); + + if (eventStreamMember) { + // RPC event stream output: serialize as binary event stream. + // Non-stream members go into the initial-response message. + const eventStream = (output as any)[eventStreamMember] as AsyncIterable; + if (!eventStream) { + // No event stream provided by handler — return empty body. + return new HttpResponse({ + statusCode: 200, + headers: { + "content-type": "application/vnd.amazon.eventstream", + }, + body: undefined, + }); + } + + // Collect non-stream members for the initial-response. + const initialResponse: Record = {}; + let hasInitialResponse = false; + for (const [memberName] of ns.structIterator()) { + if (memberName !== eventStreamMember && (output as any)[memberName] !== undefined) { + initialResponse[memberName] = (output as any)[memberName]; + hasInitialResponse = true; + } + } + + const body = await this.serializeEventStream({ + eventStream, + responseSchema: ns, + initialResponse: hasInitialResponse ? initialResponse : undefined, + }); + + return new HttpResponse({ + statusCode: 200, + headers: { + "content-type": "application/vnd.amazon.eventstream", + }, + body, + }); + } + this.serializer.write(schema, output); const body = this.serializer.flush(); diff --git a/packages/server-common/src/protocols-schema/layer-2-protocols/SmithyRpcV2CborServerProtocol.ts b/packages/server-common/src/protocols-schema/layer-2-protocols/SmithyRpcV2CborServerProtocol.ts index b86983ff5f6..019a4656b65 100644 --- a/packages/server-common/src/protocols-schema/layer-2-protocols/SmithyRpcV2CborServerProtocol.ts +++ b/packages/server-common/src/protocols-schema/layer-2-protocols/SmithyRpcV2CborServerProtocol.ts @@ -45,7 +45,14 @@ export class SmithyRpcV2CborServerProtocol extends RpcServerProtocol { */ protected override validateContentType(request: IHttpRequest): void { super.validateContentType(request); + this.validateProtocolHeaders(request); + } + /** + * Validates protocol identity headers independently of Content-Type. + * Called for all requests including event stream operations. + */ + private validateProtocolHeaders(request: IHttpRequest): void { const smithyProtocol = this.getHeaderValue(request, "smithy-protocol"); if (smithyProtocol !== "rpc-v2-cbor") { throw new SerializationException(); @@ -59,6 +66,20 @@ export class SmithyRpcV2CborServerProtocol extends RpcServerProtocol { } } + /** + * @override - For event stream operations, skip content-type validation but + * still validate protocol identity headers. + */ + public override async deserializeRequest( + operationSchema: any, + context: any, + request: IHttpRequest + ): Promise { + // Always validate protocol identity headers regardless of event stream. + this.validateProtocolHeaders(request); + return super.deserializeRequest(operationSchema, context, request); + } + /** * @override - Adds the smithy-protocol header to responses. */ diff --git a/packages/server-common/test/schema-server.integ.spec.ts b/packages/server-common/test/schema-server.integ.spec.ts index 14313ed4789..5d784cdcabe 100644 --- a/packages/server-common/test/schema-server.integ.spec.ts +++ b/packages/server-common/test/schema-server.integ.spec.ts @@ -1,4 +1,5 @@ import http from "node:http"; +import http2 from "node:http2"; import { afterAll, beforeAll, describe, expect, it } from "vitest"; import { XYZServiceHandler } from "xyz-schema-server"; import { @@ -7,6 +8,7 @@ import { CamelCaseOperationCommand, HttpLabelCommandCommand, ValidatedOperationCommand, + TradeEventStreamCommand, } from "xyz-schema"; import { SmithyRpcV2CborServerProtocol, @@ -18,6 +20,7 @@ import { HttpRequest } from "@smithy/core/protocols"; import { AwsRestJsonProtocol, AwsJson1_0Protocol } from "@aws-sdk/core/protocols"; import { GetNumbers$, camelCaseOperation$ } from "xyz-schema-server"; import { convertRequest, writeResponse } from "@smithy/server-node"; +import { NodeHttpHandler } from "@smithy/node-http-handler"; /** * End-to-end integration test that stands up a real Node.js HTTP server @@ -41,7 +44,6 @@ describe("Multi-protocol schema SSDK over HTTP", () => { new AwsRestJsonServerProtocol({ defaultNamespace: "org.xyz.v1" }), new AwsJsonRpcServerProtocol({ defaultNamespace: "org.xyz.v1" }), ], - // logger: console, handlers: { async GetNumbers(input) { const inputNumbers = input.numbers ?? {}; @@ -65,8 +67,35 @@ describe("Multi-protocol schema SSDK over HTTP", () => { async HostPrefixOperation(_input) { return {}; }, - async TradeEventStream(_input) { - return {} as any; + async TradeEventStream(input) { + // Echo the event stream back, prefixed with the sessionId from the initial message. + const prefix = input.sessionId ?? "no-session"; + const inputEvents = input.eventStream; + const outputEvents = (async function* () { + if (inputEvents) { + for await (const event of inputEvents) { + if (event.alpha) { + yield { alpha: { ...event.alpha, id: `${prefix}:${event.alpha.id}` } }; + } else if (event.gamma) { + yield { gamma: event.gamma }; + } else if (event.delta) { + yield { delta: { ...event.delta, name: `${prefix}:${event.delta.name}` } }; + } else { + yield event; + } + } + } + })(); + return { + sessionId: `ack-${prefix}`, + eventStream: outputEvents, + }; + }, + async PublishEvents() { + return { eventCount: 0, message: "not tested over h1" }; + }, + async SubscribeToEvents() { + return { subscriptionId: "n/a", events: (async function* () {})() }; }, async ValidatedOperation(input) { return { @@ -90,10 +119,12 @@ describe("Multi-protocol schema SSDK over HTTP", () => { const addr = server.address() as { port: number }; baseUrl = `http://127.0.0.1:${addr.port}`; - // CBOR client — uses default protocol. + // CBOR client — uses default protocol. Override requestHandler to HTTP/1.1 + // since the main test server is H1 (event stream tests use a separate H2 server). cborClient = new XYZServiceClient({ endpoint: baseUrl, apiKey: { apiKey: "test-key" }, + requestHandler: new NodeHttpHandler(), }); // restJson1 client — overrides protocol. @@ -104,6 +135,7 @@ describe("Multi-protocol schema SSDK over HTTP", () => { protocolSettings: { defaultNamespace: "org.xyz.v1", }, + requestHandler: new NodeHttpHandler(), }); // AWS JSON 1.0 RPC client — overrides protocol. @@ -115,6 +147,7 @@ describe("Multi-protocol schema SSDK over HTTP", () => { defaultNamespace: "org.xyz.v1", serviceTarget: "XYZService", }, + requestHandler: new NodeHttpHandler(), }); }); @@ -342,6 +375,364 @@ describe("Multi-protocol schema SSDK over HTTP", () => { }); }); + describe("Event stream (bidirectional)", () => { + let h2Server: http2.Http2Server; + let h2CborClient: XYZServiceClient; + let h2JsonRpcClient: XYZServiceClient; + let h2RestJsonClient: XYZServiceClient; + + async function collectEvents(iterable: AsyncIterable): Promise { + const events: any[] = []; + for await (const event of iterable) { + events.push(event); + } + return events; + } + + beforeAll(async () => { + const h2Handler = new XYZServiceHandler({ + protocols: [ + new SmithyRpcV2CborServerProtocol({ defaultNamespace: "org.xyz.v1" }), + new AwsJsonRpcServerProtocol({ defaultNamespace: "org.xyz.v1" }), + new AwsRestJsonServerProtocol({ defaultNamespace: "org.xyz.v1" }), + ], + validationEnabled: false, + handlers: { + async GetNumbers() { + return {}; + }, + async camelCaseOperation() { + return {}; + }, + async HttpLabelCommand() { + return {}; + }, + async HostPrefixOperation() { + return {}; + }, + async TradeEventStream(input) { + const prefix = input.sessionId ?? "no-session"; + const inputEvents = input.eventStream; + const outputEvents = (async function* () { + if (inputEvents) { + for await (const event of inputEvents) { + if (event.alpha) { + yield { alpha: { ...event.alpha, id: `${prefix}:${event.alpha.id}` } }; + } else if (event.gamma) { + yield { gamma: event.gamma }; + } else if (event.delta) { + yield { delta: { ...event.delta, name: `${prefix}:${event.delta.name}` } }; + } else { + yield event; + } + } + } + })(); + return { + sessionId: `ack-${prefix}`, + eventStream: outputEvents, + }; + }, + async PublishEvents(input) { + // Input-only stream: consume events and return a summary. + const events = await collectEvents(input.events ?? (async function* () {})()); + return { + eventCount: events.length, + message: `Received ${events.length} events on channel ${input.channel ?? "default"}`, + }; + }, + async SubscribeToEvents(input) { + // Output-only stream: return a stream of events based on the request. + const max = input.maxEvents ?? 3; + const channel = input.channel ?? "default"; + const outputEvents = (async function* () { + for (let i = 0; i < max; ++i) { + yield { notification: { topic: channel, payload: `event-${i}` } }; + } + })(); + return { + subscriptionId: `sub-${channel}`, + events: outputEvents, + }; + }, + async ValidatedOperation(input) { + return { message: `Hello, ${input.username}!` }; + }, + }, + }); + + h2Server = http2.createServer(); + h2Server.on("stream", async (stream, headers) => { + stream.on("error", () => {}); + const method = headers[":method"] as string; + const path = headers[":path"] as string; + const reqHeaders: Record = {}; + for (const [key, value] of Object.entries(headers)) { + if (!key.startsWith(":") && value !== undefined) { + reqHeaders[key] = Array.isArray(value) ? value.join(", ") : value; + } + } + + const httpRequest = new HttpRequest({ + method, + path, + headers: reqHeaders, + body: stream, + }); + + try { + const httpResponse = await h2Handler.handle(httpRequest, {}); + + const responseHeaders: Record = { + ":status": httpResponse.statusCode, + }; + for (const [key, value] of Object.entries(httpResponse.headers)) { + responseHeaders[key] = value; + } + stream.respond(responseHeaders); + + if (httpResponse.body) { + if (typeof httpResponse.body[Symbol.asyncIterator] === "function") { + for await (const chunk of httpResponse.body as AsyncIterable) { + stream.write(chunk); + } + stream.end(); + } else { + stream.end(httpResponse.body); + } + } else { + stream.end(); + } + } catch (err: any) { + if (!stream.destroyed) { + stream.respond({ ":status": 500 }); + stream.end(err.message); + } + } + }); + + await new Promise((resolve) => { + h2Server.listen(0, "127.0.0.1", () => resolve()); + }); + const h2Port = (h2Server.address() as { port: number }).port; + const h2BaseUrl = `http://127.0.0.1:${h2Port}`; + + h2CborClient = new XYZServiceClient({ + endpoint: h2BaseUrl, + apiKey: { apiKey: "test-key" }, + }); + + h2JsonRpcClient = new XYZServiceClient({ + endpoint: h2BaseUrl, + apiKey: { apiKey: "test-key" }, + protocol: AwsJson1_0Protocol, + protocolSettings: { + defaultNamespace: "org.xyz.v1", + serviceTarget: "XYZService", + }, + }); + + h2RestJsonClient = new XYZServiceClient({ + endpoint: h2BaseUrl, + apiKey: { apiKey: "test-key" }, + protocol: AwsRestJsonProtocol, + protocolSettings: { + defaultNamespace: "org.xyz.v1", + }, + }); + }); + + afterAll(async () => { + h2CborClient.destroy(); + h2JsonRpcClient.destroy(); + h2RestJsonClient.destroy(); + await new Promise((resolve, reject) => { + h2Server.close((err) => (err ? reject(err) : resolve())); + }); + }); + + // --- Bidirectional (TradeEventStream) --- + + it("CBOR: bidirectional event stream with initial message", async () => { + const response = await h2CborClient.send( + new TradeEventStreamCommand({ + sessionId: "cbor-session", + eventStream: (async function* () { + yield { alpha: { id: "evt-1" } }; + yield { delta: { name: "trade-1", number: 42 } }; + })(), + }) + ); + expect(response.sessionId).toBe("ack-cbor-session"); + const events = await collectEvents(response.eventStream!); + expect(events).toHaveLength(2); + expect(events[0].alpha?.id).toBe("cbor-session:evt-1"); + expect(events[1].delta?.name).toBe("cbor-session:trade-1"); + expect(events[1].delta?.number).toBe(42); + }); + + it("CBOR: eventHeader and eventPayload (gamma)", async () => { + const response = await h2CborClient.send( + new TradeEventStreamCommand({ + sessionId: "gamma-test", + eventStream: (async function* () { + yield { gamma: { sequenceNumber: 7, payload: { message: "hello", values: [1, 2, 3] } } }; + })(), + }) + ); + expect(response.sessionId).toBe("ack-gamma-test"); + const events = await collectEvents(response.eventStream!); + expect(events).toHaveLength(1); + expect(events[0].gamma?.sequenceNumber).toBe(7); + expect(events[0].gamma?.payload?.message).toBe("hello"); + expect(events[0].gamma?.payload?.values).toEqual([1, 2, 3]); + }); + + it("JSON RPC: bidirectional event stream with initial message", async () => { + const response = await h2JsonRpcClient.send( + new TradeEventStreamCommand({ + sessionId: "json-rpc-session", + eventStream: (async function* () { + yield { alpha: { id: "json-evt-1" } }; + yield { delta: { name: "json-trade", number: 99 } }; + })(), + }) + ); + expect(response.sessionId).toBe("ack-json-rpc-session"); + const events = await collectEvents(response.eventStream!); + expect(events).toHaveLength(2); + expect(events[0].alpha?.id).toBe("json-rpc-session:json-evt-1"); + expect(events[1].delta?.name).toBe("json-rpc-session:json-trade"); + }); + + it("REST JSON: bidirectional event stream with initial message in headers", async () => { + const response = await h2RestJsonClient.send( + new TradeEventStreamCommand({ + sessionId: "rest-session", + eventStream: (async function* () { + yield { alpha: { id: "rest-evt-1" } }; + yield { delta: { name: "rest-trade", number: 77 } }; + })(), + }) + ); + // REST protocol: sessionId is in HTTP headers, not initial-response event. + expect(response.sessionId).toBe("ack-rest-session"); + const events = await collectEvents(response.eventStream!); + expect(events).toHaveLength(2); + expect(events[0].alpha?.id).toBe("rest-session:rest-evt-1"); + expect(events[1].delta?.name).toBe("rest-session:rest-trade"); + expect(events[1].delta?.number).toBe(77); + }); + + it("CBOR: empty event stream returns no events", async () => { + const response = await h2CborClient.send( + new TradeEventStreamCommand({ + sessionId: "empty-stream", + eventStream: (async function* () {})(), + }) + ); + expect(response.sessionId).toBe("ack-empty-stream"); + const events = await collectEvents(response.eventStream!); + expect(events).toHaveLength(0); + }); + + // --- Input-only stream (PublishEvents) --- + + it("CBOR: input-only event stream", async () => { + const { PublishEventsCommand } = await import("xyz-schema"); + const response = await h2CborClient.send( + new PublishEventsCommand({ + channel: "metrics", + events: (async function* () { + yield { log: { level: "INFO", message: "started" } }; + yield { metric: { name: "cpu", value: 0.75 } }; + yield { log: { level: "WARN", message: "high load" } }; + })(), + }) + ); + expect(response.eventCount).toBe(3); + expect(response.message).toBe("Received 3 events on channel metrics"); + }); + + it("JSON RPC: input-only event stream", async () => { + const { PublishEventsCommand } = await import("xyz-schema"); + const response = await h2JsonRpcClient.send( + new PublishEventsCommand({ + channel: "logs", + events: (async function* () { + yield { log: { level: "ERROR", message: "oops" } }; + })(), + }) + ); + expect(response.eventCount).toBe(1); + expect(response.message).toBe("Received 1 events on channel logs"); + }); + + it("REST JSON: input-only event stream", async () => { + const { PublishEventsCommand } = await import("xyz-schema"); + const response = await h2RestJsonClient.send( + new PublishEventsCommand({ + channel: "telemetry", + events: (async function* () { + yield { metric: { name: "latency", value: 123.4 } }; + yield { metric: { name: "errors", value: 0 } }; + })(), + }) + ); + expect(response.eventCount).toBe(2); + expect(response.message).toBe("Received 2 events on channel telemetry"); + }); + + // --- Output-only stream (SubscribeToEvents) --- + + it("CBOR: output-only event stream", async () => { + const { SubscribeToEventsCommand } = await import("xyz-schema"); + const response = await h2CborClient.send( + new SubscribeToEventsCommand({ + channel: "news", + maxEvents: 3, + }) + ); + expect(response.subscriptionId).toBe("sub-news"); + const events = await collectEvents(response.events!); + expect(events).toHaveLength(3); + expect(events[0].notification?.topic).toBe("news"); + expect(events[0].notification?.payload).toBe("event-0"); + expect(events[2].notification?.payload).toBe("event-2"); + }); + + it("JSON RPC: output-only event stream", async () => { + const { SubscribeToEventsCommand } = await import("xyz-schema"); + const response = await h2JsonRpcClient.send( + new SubscribeToEventsCommand({ + channel: "alerts", + maxEvents: 2, + }) + ); + expect(response.subscriptionId).toBe("sub-alerts"); + const events = await collectEvents(response.events!); + expect(events).toHaveLength(2); + expect(events[0].notification?.topic).toBe("alerts"); + expect(events[1].notification?.payload).toBe("event-1"); + }); + + it("REST JSON: output-only event stream", async () => { + const { SubscribeToEventsCommand } = await import("xyz-schema"); + const response = await h2RestJsonClient.send( + new SubscribeToEventsCommand({ + channel: "updates", + maxEvents: 4, + }) + ); + // REST protocol: subscriptionId is in HTTP header. + expect(response.subscriptionId).toBe("sub-updates"); + const events = await collectEvents(response.events!); + expect(events).toHaveLength(4); + expect(events[0].notification?.topic).toBe("updates"); + expect(events[3].notification?.payload).toBe("event-3"); + }); + }); + describe("interceptor modify hooks", () => { let interceptorServer: http.Server; let interceptorClient: XYZServiceClient; @@ -366,8 +757,14 @@ describe("Multi-protocol schema SSDK over HTTP", () => { async HostPrefixOperation() { return {}; }, - async TradeEventStream() { - return {} as any; + async TradeEventStream(input) { + return { sessionId: input.sessionId, eventStream: (async function* () {})() }; + }, + async PublishEvents() { + return { eventCount: 0, message: "" }; + }, + async SubscribeToEvents() { + return { subscriptionId: "", events: (async function* () {})() }; }, async ValidatedOperation(input) { return { message: `Hello, ${input.username}!` }; @@ -420,6 +817,7 @@ describe("Multi-protocol schema SSDK over HTTP", () => { interceptorClient = new XYZServiceClient({ endpoint: interceptorBaseUrl, apiKey: { apiKey: "test-key" }, + requestHandler: new NodeHttpHandler(), }); }); @@ -494,6 +892,7 @@ describe("Multi-protocol schema SSDK over HTTP", () => { directClient = new XYZServiceClient({ endpoint: directBaseUrl, apiKey: { apiKey: "test-key" }, + requestHandler: new NodeHttpHandler(), }); }); diff --git a/packages/server-node/README.md b/packages/server-node/README.md index d61887681ec..8401f872dee 100644 --- a/packages/server-node/README.md +++ b/packages/server-node/README.md @@ -33,3 +33,107 @@ const server = createServer(async (req, res) => { server.listen(3000); console.log("Listening on port 3000"); ``` + +## HTTP/2 server for event streams + +Bidirectional and input event streams require HTTP/2 for full-duplex +communication. Use Node.js `http2.createServer()` (or `createSecureServer` for +TLS) and handle the `stream` event directly. + +The `writeResponse` helper supports `AsyncIterable` bodies (used by +event stream responses), automatically piping chunks to the response. + +```typescript +import { createServer } from "node:http2"; +import { HttpRequest } from "@smithy/core/protocols"; + +// Generated server SDK handler — supports event stream operations. +import { MyServiceHandler } from "@example/my-service-server"; + +const serviceHandler = new MyServiceHandler({ + handlers: { + // Output-only stream example. + async SubscribeToEvents(input) { + return { + subscriptionId: `sub-${input.channel}`, + events: (async function* () { + for (let i = 0; i < 100; i++) { + yield { notification: { topic: input.channel, payload: `msg-${i}` } }; + await new Promise((r) => setTimeout(r, 100)); + } + })(), + }; + }, + + // Bidirectional stream example. + async Chat(input) { + return { + sessionId: `session-${input.sessionId}`, + messages: (async function* () { + for await (const msg of input.messages) { + yield { reply: { text: `Echo: ${msg.message?.text}` } }; + } + })(), + }; + }, + + // ... other operation handlers + }, +}); + +const server = createServer(); + +server.on("stream", async (stream, headers) => { + stream.on("error", () => {}); // Prevent unhandled error crashes. + + // Extract standard headers (skip HTTP/2 pseudo-headers). + const reqHeaders: Record = {}; + for (const [key, value] of Object.entries(headers)) { + if (!key.startsWith(":") && value !== undefined) { + reqHeaders[key] = Array.isArray(value) ? value.join(", ") : value; + } + } + + const httpRequest = new HttpRequest({ + method: headers[":method"] as string, + path: headers[":path"] as string, + headers: reqHeaders, + body: stream, // The H2 stream IS the request body (AsyncIterable). + }); + + try { + const httpResponse = await serviceHandler.handle(httpRequest, {}); + + // Send response headers. + const responseHeaders: Record = { + ":status": httpResponse.statusCode, + }; + for (const [key, value] of Object.entries(httpResponse.headers)) { + responseHeaders[key] = value; + } + stream.respond(responseHeaders); + + // Write response body — may be an async iterable (event stream). + if (httpResponse.body) { + if (typeof httpResponse.body[Symbol.asyncIterator] === "function") { + for await (const chunk of httpResponse.body) { + stream.write(chunk); + } + stream.end(); + } else { + stream.end(httpResponse.body); + } + } else { + stream.end(); + } + } catch (err) { + if (!stream.destroyed) { + stream.respond({ ":status": 500 }); + stream.end(); + } + } +}); + +server.listen(3000); +console.log("HTTP/2 server listening on port 3000"); +``` diff --git a/packages/server-node/src/node-http-converters.ts b/packages/server-node/src/node-http-converters.ts index 2dab5f77c5c..091f64cdc7c 100644 --- a/packages/server-node/src/node-http-converters.ts +++ b/packages/server-node/src/node-http-converters.ts @@ -53,7 +53,20 @@ export function writeResponse(httpResponse: HttpResponse, res: ServerResponse) { res.setHeader(key, value); } if (httpResponse.body) { - res.end(httpResponse.body); + if (typeof httpResponse.body[Symbol.asyncIterator] === "function") { + // Streaming body (e.g. event stream) — pipe chunks to the response. + const iterable = httpResponse.body as AsyncIterable; + (async () => { + for await (const chunk of iterable) { + res.write(chunk); + } + res.end(); + })().catch(() => { + res.destroy(); + }); + } else { + res.end(httpResponse.body); + } } else { res.end(); } diff --git a/private/my-local-model-schema-server/src/models/models_0.ts b/private/my-local-model-schema-server/src/models/models_0.ts index 094b7df81d4..bc2cd3b9dd1 100644 --- a/private/my-local-model-schema-server/src/models/models_0.ts +++ b/private/my-local-model-schema-server/src/models/models_0.ts @@ -62,6 +62,22 @@ export interface DifferentShapeName { number?: number | undefined; } +/** + * @public + */ +export interface GammaPayload { + message?: string | undefined; + values?: number[] | undefined; +} + +/** + * @public + */ +export interface Gamma { + sequenceNumber?: number | undefined; + payload?: GammaPayload | undefined; +} + /** * @public */ @@ -129,6 +145,13 @@ export interface GetNumbersResponse { inexplicablyDeprecatedNumbers?: number[] | undefined; } +/** + * @public + */ +export interface HeartbeatEvent { + timestamp?: Date | undefined; +} + /** * @public */ @@ -136,6 +159,150 @@ export interface HostPrefixOperationInput { AccountId: string | undefined; } +/** + * @public + */ +export interface LogEvent { + level?: string | undefined; + message?: string | undefined; +} + +/** + * @public + */ +export interface MetricEvent { + name?: string | undefined; + value?: number | undefined; +} + +/** + * @public + */ +export interface NotificationEvent { + topic?: string | undefined; + payload?: string | undefined; +} + +/** + * @public + */ +export type PublishEventStream = + | PublishEventStream.LogMember + | PublishEventStream.MetricMember + | PublishEventStream.$UnknownMember; + +/** + * @public + */ +export namespace PublishEventStream { + export interface LogMember { + log: LogEvent; + metric?: never; + $unknown?: never; + } + + export interface MetricMember { + log?: never; + metric: MetricEvent; + $unknown?: never; + } + + /** + * @public + */ + export interface $UnknownMember { + log?: never; + metric?: never; + $unknown: [string, any]; + } + + /** + * @deprecated unused in schema-serde mode. + * + */ + export interface Visitor { + log: (value: LogEvent) => T; + metric: (value: MetricEvent) => T; + _: (name: string, value: any) => T; + } +} + +/** + * @public + */ +export interface PublishEventsRequest { + channel?: string | undefined; + events?: AsyncIterable | undefined; +} + +/** + * @public + */ +export interface PublishEventsResponse { + eventCount?: number | undefined; + message?: string | undefined; +} + +/** + * @public + */ +export type SubscribeEventStream = + | SubscribeEventStream.HeartbeatMember + | SubscribeEventStream.NotificationMember + | SubscribeEventStream.$UnknownMember; + +/** + * @public + */ +export namespace SubscribeEventStream { + export interface NotificationMember { + notification: NotificationEvent; + heartbeat?: never; + $unknown?: never; + } + + export interface HeartbeatMember { + notification?: never; + heartbeat: HeartbeatEvent; + $unknown?: never; + } + + /** + * @public + */ + export interface $UnknownMember { + notification?: never; + heartbeat?: never; + $unknown: [string, any]; + } + + /** + * @deprecated unused in schema-serde mode. + * + */ + export interface Visitor { + notification: (value: NotificationEvent) => T; + heartbeat: (value: HeartbeatEvent) => T; + _: (name: string, value: any) => T; + } +} + +/** + * @public + */ +export interface SubscribeToEventsRequest { + channel?: string | undefined; + maxEvents?: number | undefined; +} + +/** + * @public + */ +export interface SubscribeToEventsResponse { + subscriptionId?: string | undefined; + events?: AsyncIterable | undefined; +} + /** * @public */ @@ -174,7 +341,7 @@ export namespace TradeEvents { export interface GammaMember { alpha?: never; beta?: never; - gamma: Unit; + gamma: Gamma; delta?: never; $unknown?: never; } @@ -205,7 +372,7 @@ export namespace TradeEvents { export interface Visitor { alpha: (value: Alpha) => T; beta: (value: Unit) => T; - gamma: (value: Unit) => T; + gamma: (value: Gamma) => T; delta: (value: DifferentShapeName) => T; _: (name: string, value: any) => T; } @@ -215,6 +382,7 @@ export namespace TradeEvents { * @public */ export interface TradeEventStreamRequest { + sessionId?: string | undefined; eventStream?: AsyncIterable | undefined; } @@ -222,6 +390,7 @@ export interface TradeEventStreamRequest { * @public */ export interface TradeEventStreamResponse { + sessionId?: string | undefined; eventStream?: AsyncIterable | undefined; } diff --git a/private/my-local-model-schema-server/src/schemas/schemas_0.ts b/private/my-local-model-schema-server/src/schemas/schemas_0.ts index 911a8b20ef1..08ed67fd936 100644 --- a/private/my-local-model-schema-server/src/schemas/schemas_0.ts +++ b/private/my-local-model-schema-server/src/schemas/schemas_0.ts @@ -4,21 +4,35 @@ const _AI = "AccountId"; const _CA = "ConstrainedAddress"; const _CTE = "CodedThrottlingError"; const _DSN = "DifferentShapeName"; +const _G = "Gamma"; const _GN = "GetNumbers"; const _GNR = "GetNumbersRequest"; const _GNRe = "GetNumbersResponse"; +const _GP = "GammaPayload"; const _HE = "HaltError"; +const _HEe = "HeartbeatEvent"; const _HLC = "HttpLabelCommand"; const _HLCI = "HttpLabelCommandInput"; const _HLCO = "HttpLabelCommandOutput"; const _HPO = "HostPrefixOperation"; const _HPOI = "HostPrefixOperationInput"; const _LDNATRP = "LabelDoesNotApplyToRpcProtocol"; +const _LE = "LogEvent"; +const _ME = "MetricEvent"; const _MSLE = "MainServiceLinkedError"; const _MTE = "MysteryThrottlingError"; +const _NE = "NotificationEvent"; +const _PE = "PublishEvents"; +const _PER = "PublishEventsRequest"; +const _PERu = "PublishEventsResponse"; +const _PES = "PublishEventStream"; const _RE = "RetryableError"; +const _SES = "SubscribeEventStream"; const _SIL = "SparseIntegerList"; const _SIM = "SparseIntegerMap"; +const _STE = "SubscribeToEvents"; +const _STER = "SubscribeToEventsRequest"; +const _STERu = "SubscribeToEventsResponse"; const _T = "Tag"; const _TE = "TradeEvents"; const _TES = "TradeEventStream"; @@ -41,35 +55,51 @@ const _cCO = "camelCaseOperation"; const _cCOI = "camelCaseOperationInput"; const _cCOO = "camelCaseOperationOutput"; const _cHI = "customHeaderInput"; +const _ch = "channel"; const _d = "delta"; const _dN = "deprecatedNumbers"; const _dNWC = "deprecatedNumbersWithoutChronology"; const _dNWE = "deprecatedNumbersWithoutExplanation"; const _e = "error"; +const _eC = "eventCount"; +const _eH = "eventHeader"; +const _eP = "eventPayload"; const _eS = "eventStream"; const _em = "email"; const _en = "endpoint"; +const _ev = "events"; const _fWM = "fieldWithoutMessage"; const _fWMi = "fieldWithMessage"; const _g = "gamma"; -const _h = "http"; +const _h = "heartbeat"; const _hE = "httpError"; +const _hH = "httpHeader"; const _hL = "hostLabel"; +const _ht = "http"; const _i = "id"; const _iDN = "inexplicablyDeprecatedNumbers"; const _l = "length"; +const _le = "level"; +const _lo = "log"; const _m = "message"; +const _mE = "maxEvents"; const _mR = "maxResults"; +const _me = "metric"; const _n = "name"; const _nT = "nextToken"; +const _no = "notification"; const _nu = "number"; const _num = "numbers"; const _oP = "overloadedParam"; const _p = "pattern"; +const _pa = "payload"; const _r = "results"; const _ra = "range"; const _s = "smithy.ts.sdk.synthetic.org.xyz.v1"; -const _sN = "sparseNumbers"; +const _sI = "subscriptionId"; +const _sIe = "sessionId"; +const _sN = "sequenceNumber"; +const _sNp = "sparseNumbers"; const _sT = "startToken"; const _sp = "sparse"; const _st = "state"; @@ -77,9 +107,17 @@ const _str = "streaming"; const _t = "timestamp"; const _ta = "tags"; const _to = "token"; +const _top = "topic"; const _u = "username"; const _uI = "uniqueItems"; const _uT = "uniqueTags"; +const _v = "values"; +const _va = "value"; +const _xc = "x-channel"; +const _xec = "x-event-count"; +const _xme = "x-max-events"; +const _xsi = "x-subscription-id"; +const _xsi_ = "x-session-id"; const _zC = "zipCode"; const _zZzZzZ = "^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$"; const n0 = "org.xyz.v1"; @@ -193,30 +231,80 @@ export var DifferentShapeName$: StaticStructureSchema = [3, n0, _DSN, [_n, _nu], [0, 1] ]; +export var Gamma$: StaticStructureSchema = [3, n0, _G, + 0, + [_sN, _pa], + [[1, { [_eH]: 1 }], [() => GammaPayload$, { [_eP]: 1 }]] +]; +export var GammaPayload$: StaticStructureSchema = [3, n0, _GP, + 0, + [_m, _v], + [0, 64 | 1] +]; export var GetNumbersRequest$: StaticStructureSchema = [3, n0, _GNR, 0, - [_bD, _bI, _fWM, _fWMi, _sT, _mR, _cHI, _num, _sN], + [_bD, _bI, _fWM, _fWMi, _sT, _mR, _cHI, _num, _sNp], [19, 17, 0, 0, 0, 1, 0, 128 | 1, [() => SparseIntegerMap, 0]] ]; export var GetNumbersResponse$: StaticStructureSchema = [3, n0, _GNRe, 0, - [_bD, _bI, _num, _sN, _nT, _dN, _dNWE, _dNWC, _iDN], + [_bD, _bI, _num, _sNp, _nT, _dN, _dNWE, _dNWC, _iDN], [19, 17, 64 | 1, [() => SparseIntegerList, 0], 0, 64 | 1, 64 | 1, 64 | 1, 64 | 1] ]; +export var HeartbeatEvent$: StaticStructureSchema = [3, n0, _HEe, + 0, + [_t], + [4] +]; export var HostPrefixOperationInput$: StaticStructureSchema = [3, n0, _HPOI, 0, [_AI], [[0, { [_hL]: 1 }]], 1 ]; +export var LogEvent$: StaticStructureSchema = [3, n0, _LE, + 0, + [_le, _m], + [0, 0] +]; +export var MetricEvent$: StaticStructureSchema = [3, n0, _ME, + 0, + [_n, _va], + [0, 1] +]; +export var NotificationEvent$: StaticStructureSchema = [3, n0, _NE, + 0, + [_top, _pa], + [0, 0] +]; +export var PublishEventsRequest$: StaticStructureSchema = [3, n0, _PER, + 0, + [_ch, _ev], + [[0, { [_hH]: _xc }], [() => PublishEventStream$, 16]] +]; +export var PublishEventsResponse$: StaticStructureSchema = [3, n0, _PERu, + 0, + [_eC, _m], + [[1, { [_hH]: _xec }], 0] +]; +export var SubscribeToEventsRequest$: StaticStructureSchema = [3, n0, _STER, + 0, + [_ch, _mE], + [[0, { [_hH]: _xc }], [1, { [_hH]: _xme }]] +]; +export var SubscribeToEventsResponse$: StaticStructureSchema = [3, n0, _STERu, + 0, + [_sI, _ev], + [[0, { [_hH]: _xsi }], [() => SubscribeEventStream$, 16]] +]; export var TradeEventStreamRequest$: StaticStructureSchema = [3, n0, _TESR, 0, - [_eS], - [[() => TradeEvents$, 0]] + [_sIe, _eS], + [[0, { [_hH]: _xsi_ }], [() => TradeEvents$, 16]] ]; export var TradeEventStreamResponse$: StaticStructureSchema = [3, n0, _TESRr, 0, - [_eS], - [[() => TradeEvents$, 0]] + [_sIe, _eS], + [[0, { [_hH]: _xsi_ }], [() => TradeEvents$, 16]] ]; export var ValidatedInput$: StaticStructureSchema = [3, n0, _VI, 0, @@ -246,26 +334,42 @@ var IntegerMap = 128 | 1; var SparseIntegerMap: StaticMapSchema = [2, n0, _SIM, { [_sp]: 1 }, 0, 1 ]; +export var PublishEventStream$: StaticUnionSchema = [4, n0, _PES, + { [_str]: 1 }, + [_lo, _me], + [() => LogEvent$, () => MetricEvent$] +]; +export var SubscribeEventStream$: StaticUnionSchema = [4, n0, _SES, + { [_str]: 1 }, + [_no, _h], + [() => NotificationEvent$, () => HeartbeatEvent$] +]; export var TradeEvents$: StaticUnionSchema = [4, n0, _TE, { [_str]: 1 }, [_al, _b, _g, _d], - [() => Alpha$, () => __Unit, () => __Unit, () => DifferentShapeName$] + [() => Alpha$, () => __Unit, [() => Gamma$, 0], () => DifferentShapeName$] ]; export var HttpLabelCommand$: StaticOperationSchema = [9, n1, _HLC, - { [_h]: ["POST", "/{LabelDoesNotApplyToRpcProtocol}", 200] }, () => HttpLabelCommandInput$, () => HttpLabelCommandOutput$ + { [_ht]: ["POST", "/{LabelDoesNotApplyToRpcProtocol}", 200] }, () => HttpLabelCommandInput$, () => HttpLabelCommandOutput$ ]; export var camelCaseOperation$: StaticOperationSchema = [9, n0, _cCO, - { [_h]: ["POST", "/camel-case", 200] }, () => camelCaseOperationInput$, () => camelCaseOperationOutput$ + { [_ht]: ["POST", "/camel-case", 200] }, () => camelCaseOperationInput$, () => camelCaseOperationOutput$ ]; export var GetNumbers$: StaticOperationSchema = [9, n0, _GN, - { [_h]: ["POST", "/get-numbers", 200] }, () => GetNumbersRequest$, () => GetNumbersResponse$ + { [_ht]: ["POST", "/get-numbers", 200] }, () => GetNumbersRequest$, () => GetNumbersResponse$ ]; export var HostPrefixOperation$: StaticOperationSchema = [9, n0, _HPO, { [_en]: ["{AccountId}."] }, () => HostPrefixOperationInput$, () => __Unit ]; +export var PublishEvents$: StaticOperationSchema = [9, n0, _PE, + { [_ht]: ["POST", "/publish-events", 200] }, () => PublishEventsRequest$, () => PublishEventsResponse$ +]; +export var SubscribeToEvents$: StaticOperationSchema = [9, n0, _STE, + { [_ht]: ["POST", "/subscribe-to-events", 200] }, () => SubscribeToEventsRequest$, () => SubscribeToEventsResponse$ +]; export var TradeEventStream$: StaticOperationSchema = [9, n0, _TES, - { [_h]: ["POST", "/trade-event-stream", 200] }, () => TradeEventStreamRequest$, () => TradeEventStreamResponse$ + { [_ht]: ["POST", "/trade-event-stream", 200] }, () => TradeEventStreamRequest$, () => TradeEventStreamResponse$ ]; export var ValidatedOperation$: StaticOperationSchema = [9, n0, _VOa, - { [_h]: ["POST", "/validated", 200] }, () => ValidatedInput$, () => ValidatedOutput$ + { [_ht]: ["POST", "/validated", 200] }, () => ValidatedInput$, () => ValidatedOutput$ ]; diff --git a/private/my-local-model-schema-server/src/server/XYZServiceHandler.ts b/private/my-local-model-schema-server/src/server/XYZServiceHandler.ts index f7cb15e7208..5b4f4225338 100644 --- a/private/my-local-model-schema-server/src/server/XYZServiceHandler.ts +++ b/private/my-local-model-schema-server/src/server/XYZServiceHandler.ts @@ -14,6 +14,10 @@ import type { HostPrefixOperationInput, HttpLabelCommandInput, HttpLabelCommandOutput, + PublishEventsRequest, + PublishEventsResponse, + SubscribeToEventsRequest, + SubscribeToEventsResponse, TradeEventStreamRequest, TradeEventStreamResponse, Unit, @@ -25,6 +29,8 @@ import { GetNumbers$, HostPrefixOperation$, HttpLabelCommand$, + PublishEvents$, + SubscribeToEvents$, TradeEventStream$, ValidatedOperation$, } from "../schemas/schemas_0"; @@ -35,6 +41,8 @@ const OPERATION_SCHEMAS: StaticOperationSchema[] = [ camelCaseOperation$, GetNumbers$, HostPrefixOperation$, + PublishEvents$, + SubscribeToEvents$, TradeEventStream$, ValidatedOperation$, ]; @@ -52,6 +60,8 @@ export class XYZServiceHandler extends SchemaServiceHandler Promise; GetNumbers: (input: GetNumbersRequest, context: ServerRequestContext, userContext: Context) => Promise; HostPrefixOperation: (input: HostPrefixOperationInput, context: ServerRequestContext, userContext: Context) => Promise; + PublishEvents: (input: PublishEventsRequest, context: ServerRequestContext, userContext: Context) => Promise; + SubscribeToEvents: (input: SubscribeToEventsRequest, context: ServerRequestContext, userContext: Context) => Promise; TradeEventStream: (input: TradeEventStreamRequest, context: ServerRequestContext, userContext: Context) => Promise; ValidatedOperation: (input: ValidatedInput, context: ServerRequestContext, userContext: Context) => Promise; }; diff --git a/private/my-local-model-schema/src/XYZService.ts b/private/my-local-model-schema/src/XYZService.ts index 270da0dd083..8deb3cba1b8 100644 --- a/private/my-local-model-schema/src/XYZService.ts +++ b/private/my-local-model-schema/src/XYZService.ts @@ -27,6 +27,16 @@ import { type HttpLabelCommandCommandOutput, HttpLabelCommandCommand, } from "./commands/HttpLabelCommandCommand"; +import { + type PublishEventsCommandInput, + type PublishEventsCommandOutput, + PublishEventsCommand, +} from "./commands/PublishEventsCommand"; +import { + type SubscribeToEventsCommandInput, + type SubscribeToEventsCommandOutput, + SubscribeToEventsCommand, +} from "./commands/SubscribeToEventsCommand"; import { type TradeEventStreamCommandInput, type TradeEventStreamCommandOutput, @@ -51,6 +61,8 @@ const commands = { CamelCaseOperationCommand, GetNumbersCommand, HostPrefixOperationCommand, + PublishEventsCommand, + SubscribeToEventsCommand, TradeEventStreamCommand, ValidatedOperationCommand, }; @@ -135,6 +147,42 @@ export interface XYZService { cb: (err: any, data?: HostPrefixOperationCommandOutput) => void ): void; + /** + * @see {@link PublishEventsCommand} + */ + publishEvents(): Promise; + publishEvents( + args: PublishEventsCommandInput, + options?: __HttpHandlerOptions + ): Promise; + publishEvents( + args: PublishEventsCommandInput, + cb: (err: any, data?: PublishEventsCommandOutput) => void + ): void; + publishEvents( + args: PublishEventsCommandInput, + options: __HttpHandlerOptions, + cb: (err: any, data?: PublishEventsCommandOutput) => void + ): void; + + /** + * @see {@link SubscribeToEventsCommand} + */ + subscribeToEvents(): Promise; + subscribeToEvents( + args: SubscribeToEventsCommandInput, + options?: __HttpHandlerOptions + ): Promise; + subscribeToEvents( + args: SubscribeToEventsCommandInput, + cb: (err: any, data?: SubscribeToEventsCommandOutput) => void + ): void; + subscribeToEvents( + args: SubscribeToEventsCommandInput, + options: __HttpHandlerOptions, + cb: (err: any, data?: SubscribeToEventsCommandOutput) => void + ): void; + /** * @see {@link TradeEventStreamCommand} */ diff --git a/private/my-local-model-schema/src/XYZServiceClient.ts b/private/my-local-model-schema/src/XYZServiceClient.ts index 6da0c7b6d1a..62d2496fb3f 100644 --- a/private/my-local-model-schema/src/XYZServiceClient.ts +++ b/private/my-local-model-schema/src/XYZServiceClient.ts @@ -55,6 +55,11 @@ import type { HostPrefixOperationCommandOutput, } from "./commands/HostPrefixOperationCommand"; import type { HttpLabelCommandCommandInput, HttpLabelCommandCommandOutput } from "./commands/HttpLabelCommandCommand"; +import type { PublishEventsCommandInput, PublishEventsCommandOutput } from "./commands/PublishEventsCommand"; +import type { + SubscribeToEventsCommandInput, + SubscribeToEventsCommandOutput, +} from "./commands/SubscribeToEventsCommand"; import type { TradeEventStreamCommandInput, TradeEventStreamCommandOutput } from "./commands/TradeEventStreamCommand"; import type { ValidatedOperationCommandInput, @@ -79,6 +84,8 @@ export type ServiceInputTypes = | GetNumbersCommandInput | HostPrefixOperationCommandInput | HttpLabelCommandCommandInput + | PublishEventsCommandInput + | SubscribeToEventsCommandInput | TradeEventStreamCommandInput | ValidatedOperationCommandInput; @@ -90,6 +97,8 @@ export type ServiceOutputTypes = | GetNumbersCommandOutput | HostPrefixOperationCommandOutput | HttpLabelCommandCommandOutput + | PublishEventsCommandOutput + | SubscribeToEventsCommandOutput | TradeEventStreamCommandOutput | ValidatedOperationCommandOutput; diff --git a/private/my-local-model-schema/src/commands/PublishEventsCommand.ts b/private/my-local-model-schema/src/commands/PublishEventsCommand.ts new file mode 100644 index 00000000000..67ff5becf32 --- /dev/null +++ b/private/my-local-model-schema/src/commands/PublishEventsCommand.ts @@ -0,0 +1,88 @@ +// smithy-typescript generated code +import type { MetadataBearer as __MetadataBearer } from "@smithy/types"; + +import { _ep3, _mw0, command } from "../commandBuilder"; +import type { PublishEventsRequest, PublishEventsResponse } from "../models/models_0"; +import { PublishEvents$ } from "../schemas/schemas_0"; + +/** + * @public + */ +export type { __MetadataBearer }; +/** + * @public + * + * The input for {@link PublishEventsCommand}. + */ +export interface PublishEventsCommandInput extends PublishEventsRequest {} +/** + * @public + * + * The output of {@link PublishEventsCommand}. + */ +export interface PublishEventsCommandOutput extends PublishEventsResponse, __MetadataBearer {} + +/** + * Input-only event stream: client sends events, server responds with a summary. + * @example + * Use a bare-bones client and the command you need to make an API call. + * ```javascript + * import { XYZServiceClient, PublishEventsCommand } from "xyz-schema"; // ES Modules import + * // const { XYZServiceClient, PublishEventsCommand } = require("xyz-schema"); // CommonJS import + * // import type { XYZServiceClientConfig } from "xyz-schema"; + * const config = {}; // type is XYZServiceClientConfig + * const client = new XYZServiceClient(config); + * const input = { // PublishEventsRequest + * channel: "STRING_VALUE", + * events: { // PublishEventStream Union: only one key present + * log: { // LogEvent + * level: "STRING_VALUE", + * message: "STRING_VALUE", + * }, + * metric: { // MetricEvent + * name: "STRING_VALUE", + * value: Number("double"), + * }, + * }, + * }; + * const command = new PublishEventsCommand(input); + * const response = await client.send(command); + * // { // PublishEventsResponse + * // eventCount: Number("int"), + * // message: "STRING_VALUE", + * // }; + * + * ``` + * + * @param PublishEventsCommandInput - {@link PublishEventsCommandInput} + * @returns {@link PublishEventsCommandOutput} + * @see {@link PublishEventsCommandInput} for command's `input` shape. + * @see {@link PublishEventsCommandOutput} for command's `response` shape. + * @see {@link XYZServiceClientResolvedConfig | config} for XYZServiceClient's `config` shape. + * + * @throws {@link MainServiceLinkedError} (client fault) + * + * @throws {@link XYZServiceSyntheticServiceException} + *

Base exception class for all service exceptions from XYZService service.

+ * + * + * @public + */ +export class PublishEventsCommand extends command( + _ep3, + _mw0, + "PublishEvents", + PublishEvents$ +) { + /** @internal type navigation helper, not in runtime. */ + protected declare static __types: { + api: { + input: PublishEventsRequest; + output: PublishEventsResponse; + }; + sdk: { + input: PublishEventsCommandInput; + output: PublishEventsCommandOutput; + }; + }; +} diff --git a/private/my-local-model-schema/src/commands/SubscribeToEventsCommand.ts b/private/my-local-model-schema/src/commands/SubscribeToEventsCommand.ts new file mode 100644 index 00000000000..bc8e390cef2 --- /dev/null +++ b/private/my-local-model-schema/src/commands/SubscribeToEventsCommand.ts @@ -0,0 +1,87 @@ +// smithy-typescript generated code +import type { MetadataBearer as __MetadataBearer } from "@smithy/types"; + +import { _ep3, _mw0, command } from "../commandBuilder"; +import type { SubscribeToEventsRequest, SubscribeToEventsResponse } from "../models/models_0"; +import { SubscribeToEvents$ } from "../schemas/schemas_0"; + +/** + * @public + */ +export type { __MetadataBearer }; +/** + * @public + * + * The input for {@link SubscribeToEventsCommand}. + */ +export interface SubscribeToEventsCommandInput extends SubscribeToEventsRequest {} +/** + * @public + * + * The output of {@link SubscribeToEventsCommand}. + */ +export interface SubscribeToEventsCommandOutput extends SubscribeToEventsResponse, __MetadataBearer {} + +/** + * Output-only event stream: client sends a subscription request, server streams events. + * @example + * Use a bare-bones client and the command you need to make an API call. + * ```javascript + * import { XYZServiceClient, SubscribeToEventsCommand } from "xyz-schema"; // ES Modules import + * // const { XYZServiceClient, SubscribeToEventsCommand } = require("xyz-schema"); // CommonJS import + * // import type { XYZServiceClientConfig } from "xyz-schema"; + * const config = {}; // type is XYZServiceClientConfig + * const client = new XYZServiceClient(config); + * const input = { // SubscribeToEventsRequest + * channel: "STRING_VALUE", + * maxEvents: Number("int"), + * }; + * const command = new SubscribeToEventsCommand(input); + * const response = await client.send(command); + * // { // SubscribeToEventsResponse + * // subscriptionId: "STRING_VALUE", + * // events: { // SubscribeEventStream Union: only one key present + * // notification: { // NotificationEvent + * // topic: "STRING_VALUE", + * // payload: "STRING_VALUE", + * // }, + * // heartbeat: { // HeartbeatEvent + * // timestamp: new Date("TIMESTAMP"), + * // }, + * // }, + * // }; + * + * ``` + * + * @param SubscribeToEventsCommandInput - {@link SubscribeToEventsCommandInput} + * @returns {@link SubscribeToEventsCommandOutput} + * @see {@link SubscribeToEventsCommandInput} for command's `input` shape. + * @see {@link SubscribeToEventsCommandOutput} for command's `response` shape. + * @see {@link XYZServiceClientResolvedConfig | config} for XYZServiceClient's `config` shape. + * + * @throws {@link MainServiceLinkedError} (client fault) + * + * @throws {@link XYZServiceSyntheticServiceException} + *

Base exception class for all service exceptions from XYZService service.

+ * + * + * @public + */ +export class SubscribeToEventsCommand extends command( + _ep3, + _mw0, + "SubscribeToEvents", + SubscribeToEvents$ +) { + /** @internal type navigation helper, not in runtime. */ + protected declare static __types: { + api: { + input: SubscribeToEventsRequest; + output: SubscribeToEventsResponse; + }; + sdk: { + input: SubscribeToEventsCommandInput; + output: SubscribeToEventsCommandOutput; + }; + }; +} diff --git a/private/my-local-model-schema/src/commands/TradeEventStreamCommand.ts b/private/my-local-model-schema/src/commands/TradeEventStreamCommand.ts index 5162d7c1d5e..32c63a9daa0 100644 --- a/private/my-local-model-schema/src/commands/TradeEventStreamCommand.ts +++ b/private/my-local-model-schema/src/commands/TradeEventStreamCommand.ts @@ -34,13 +34,22 @@ export interface TradeEventStreamCommandOutput extends TradeEventStreamResponse, * const config = {}; // type is XYZServiceClientConfig * const client = new XYZServiceClient(config); * const input = { // TradeEventStreamRequest + * sessionId: "STRING_VALUE", * eventStream: { // TradeEvents Union: only one key present * alpha: { // Alpha * id: "STRING_VALUE", * timestamp: new Date("TIMESTAMP"), * }, * beta: {}, - * gamma: {}, + * gamma: { // Gamma + * sequenceNumber: Number("int"), + * payload: { // GammaPayload + * message: "STRING_VALUE", + * values: [ // IntegerList + * Number("int"), + * ], + * }, + * }, * delta: { // DifferentShapeName * name: "STRING_VALUE", * number: Number("int"), @@ -50,13 +59,22 @@ export interface TradeEventStreamCommandOutput extends TradeEventStreamResponse, * const command = new TradeEventStreamCommand(input); * const response = await client.send(command); * // { // TradeEventStreamResponse + * // sessionId: "STRING_VALUE", * // eventStream: { // TradeEvents Union: only one key present * // alpha: { // Alpha * // id: "STRING_VALUE", * // timestamp: new Date("TIMESTAMP"), * // }, * // beta: {}, - * // gamma: {}, + * // gamma: { // Gamma + * // sequenceNumber: Number("int"), + * // payload: { // GammaPayload + * // message: "STRING_VALUE", + * // values: [ // IntegerList + * // Number("int"), + * // ], + * // }, + * // }, * // delta: { // DifferentShapeName * // name: "STRING_VALUE", * // number: Number("int"), diff --git a/private/my-local-model-schema/src/commands/index.ts b/private/my-local-model-schema/src/commands/index.ts index 72947904e64..6be90e24808 100644 --- a/private/my-local-model-schema/src/commands/index.ts +++ b/private/my-local-model-schema/src/commands/index.ts @@ -3,5 +3,7 @@ export * from "./CamelCaseOperationCommand"; export * from "./GetNumbersCommand"; export * from "./HostPrefixOperationCommand"; export * from "./HttpLabelCommandCommand"; +export * from "./PublishEventsCommand"; +export * from "./SubscribeToEventsCommand"; export * from "./TradeEventStreamCommand"; export * from "./ValidatedOperationCommand"; diff --git a/private/my-local-model-schema/src/models/models_0.ts b/private/my-local-model-schema/src/models/models_0.ts index 51ce537fd7f..09adc0c1335 100644 --- a/private/my-local-model-schema/src/models/models_0.ts +++ b/private/my-local-model-schema/src/models/models_0.ts @@ -62,6 +62,22 @@ export interface DifferentShapeName { number?: number | undefined; } +/** + * @public + */ +export interface GammaPayload { + message?: string | undefined; + values?: number[] | undefined; +} + +/** + * @public + */ +export interface Gamma { + sequenceNumber?: number | undefined; + payload?: GammaPayload | undefined; +} + /** * @public */ @@ -129,6 +145,13 @@ export interface GetNumbersResponse { inexplicablyDeprecatedNumbers?: number[] | undefined; } +/** + * @public + */ +export interface HeartbeatEvent { + timestamp?: Date | undefined; +} + /** * @public */ @@ -136,6 +159,150 @@ export interface HostPrefixOperationInput { AccountId: string | undefined; } +/** + * @public + */ +export interface LogEvent { + level?: string | undefined; + message?: string | undefined; +} + +/** + * @public + */ +export interface MetricEvent { + name?: string | undefined; + value?: number | undefined; +} + +/** + * @public + */ +export interface NotificationEvent { + topic?: string | undefined; + payload?: string | undefined; +} + +/** + * @public + */ +export type PublishEventStream = + | PublishEventStream.LogMember + | PublishEventStream.MetricMember + | PublishEventStream.$UnknownMember; + +/** + * @public + */ +export namespace PublishEventStream { + export interface LogMember { + log: LogEvent; + metric?: never; + $unknown?: never; + } + + export interface MetricMember { + log?: never; + metric: MetricEvent; + $unknown?: never; + } + + /** + * @public + */ + export interface $UnknownMember { + log?: never; + metric?: never; + $unknown: [string, any]; + } + + /** + * @deprecated unused in schema-serde mode. + * + */ + export interface Visitor { + log: (value: LogEvent) => T; + metric: (value: MetricEvent) => T; + _: (name: string, value: any) => T; + } +} + +/** + * @public + */ +export interface PublishEventsRequest { + channel?: string | undefined; + events?: AsyncIterable | undefined; +} + +/** + * @public + */ +export interface PublishEventsResponse { + eventCount?: number | undefined; + message?: string | undefined; +} + +/** + * @public + */ +export type SubscribeEventStream = + | SubscribeEventStream.HeartbeatMember + | SubscribeEventStream.NotificationMember + | SubscribeEventStream.$UnknownMember; + +/** + * @public + */ +export namespace SubscribeEventStream { + export interface NotificationMember { + notification: NotificationEvent; + heartbeat?: never; + $unknown?: never; + } + + export interface HeartbeatMember { + notification?: never; + heartbeat: HeartbeatEvent; + $unknown?: never; + } + + /** + * @public + */ + export interface $UnknownMember { + notification?: never; + heartbeat?: never; + $unknown: [string, any]; + } + + /** + * @deprecated unused in schema-serde mode. + * + */ + export interface Visitor { + notification: (value: NotificationEvent) => T; + heartbeat: (value: HeartbeatEvent) => T; + _: (name: string, value: any) => T; + } +} + +/** + * @public + */ +export interface SubscribeToEventsRequest { + channel?: string | undefined; + maxEvents?: number | undefined; +} + +/** + * @public + */ +export interface SubscribeToEventsResponse { + subscriptionId?: string | undefined; + events?: AsyncIterable | undefined; +} + /** * @public */ @@ -174,7 +341,7 @@ export namespace TradeEvents { export interface GammaMember { alpha?: never; beta?: never; - gamma: Unit; + gamma: Gamma; delta?: never; $unknown?: never; } @@ -205,7 +372,7 @@ export namespace TradeEvents { export interface Visitor { alpha: (value: Alpha) => T; beta: (value: Unit) => T; - gamma: (value: Unit) => T; + gamma: (value: Gamma) => T; delta: (value: DifferentShapeName) => T; _: (name: string, value: any) => T; } @@ -215,6 +382,7 @@ export namespace TradeEvents { * @public */ export interface TradeEventStreamRequest { + sessionId?: string | undefined; eventStream?: AsyncIterable | undefined; } @@ -222,6 +390,7 @@ export interface TradeEventStreamRequest { * @public */ export interface TradeEventStreamResponse { + sessionId?: string | undefined; eventStream?: AsyncIterable | undefined; } diff --git a/private/my-local-model-schema/src/runtimeConfig.ts b/private/my-local-model-schema/src/runtimeConfig.ts index bb710e832c6..ff5f241bb5d 100644 --- a/private/my-local-model-schema/src/runtimeConfig.ts +++ b/private/my-local-model-schema/src/runtimeConfig.ts @@ -8,7 +8,7 @@ import { NODE_RETRY_MODE_CONFIG_OPTIONS, } from "@smithy/core/retry"; import { calculateBodyLength } from "@smithy/core/serde"; -import { NodeHttpHandler as RequestHandler, streamCollector } from "@smithy/node-http-handler"; +import { NodeHttp2Handler as RequestHandler, streamCollector } from "@smithy/node-http-handler"; import { getRuntimeConfig as getSharedRuntimeConfig } from "./runtimeConfig.shared"; import type { XYZServiceClientConfig } from "./XYZServiceClient"; @@ -29,7 +29,10 @@ export const getRuntimeConfig = (config: XYZServiceClientConfig) => { bodyLengthChecker: config?.bodyLengthChecker ?? calculateBodyLength, eventStreamSerdeProvider: config?.eventStreamSerdeProvider ?? eventStreamSerdeProvider, maxAttempts: config?.maxAttempts ?? loadNodeConfig(NODE_MAX_ATTEMPT_CONFIG_OPTIONS, config), - requestHandler: RequestHandler.create(config?.requestHandler ?? defaultConfigProvider), + requestHandler: RequestHandler.create(config?.requestHandler ?? (async () => ({ + ...await defaultConfigProvider(), + disableConcurrentStreams: true + }))), retryMode: config?.retryMode ?? loadNodeConfig( diff --git a/private/my-local-model-schema/src/schemas/schemas_0.ts b/private/my-local-model-schema/src/schemas/schemas_0.ts index 8d5aec1df88..c86dc7eead1 100644 --- a/private/my-local-model-schema/src/schemas/schemas_0.ts +++ b/private/my-local-model-schema/src/schemas/schemas_0.ts @@ -3,21 +3,35 @@ const _AI = "AccountId"; const _CA = "ConstrainedAddress"; const _CTE = "CodedThrottlingError"; const _DSN = "DifferentShapeName"; +const _G = "Gamma"; const _GN = "GetNumbers"; const _GNR = "GetNumbersRequest"; const _GNRe = "GetNumbersResponse"; +const _GP = "GammaPayload"; const _HE = "HaltError"; +const _HEe = "HeartbeatEvent"; const _HLC = "HttpLabelCommand"; const _HLCI = "HttpLabelCommandInput"; const _HLCO = "HttpLabelCommandOutput"; const _HPO = "HostPrefixOperation"; const _HPOI = "HostPrefixOperationInput"; const _LDNATRP = "LabelDoesNotApplyToRpcProtocol"; +const _LE = "LogEvent"; +const _ME = "MetricEvent"; const _MSLE = "MainServiceLinkedError"; const _MTE = "MysteryThrottlingError"; +const _NE = "NotificationEvent"; +const _PE = "PublishEvents"; +const _PER = "PublishEventsRequest"; +const _PERu = "PublishEventsResponse"; +const _PES = "PublishEventStream"; const _RE = "RetryableError"; +const _SES = "SubscribeEventStream"; const _SIL = "SparseIntegerList"; const _SIM = "SparseIntegerMap"; +const _STE = "SubscribeToEvents"; +const _STER = "SubscribeToEventsRequest"; +const _STERu = "SubscribeToEventsResponse"; const _TE = "TradeEvents"; const _TES = "TradeEventStream"; const _TESR = "TradeEventStreamRequest"; @@ -37,32 +51,48 @@ const _cCO = "camelCaseOperation"; const _cCOI = "camelCaseOperationInput"; const _cCOO = "camelCaseOperationOutput"; const _cHI = "customHeaderInput"; +const _ch = "channel"; const _d = "delta"; const _dN = "deprecatedNumbers"; const _dNWC = "deprecatedNumbersWithoutChronology"; const _dNWE = "deprecatedNumbersWithoutExplanation"; const _e = "error"; +const _eC = "eventCount"; +const _eH = "eventHeader"; +const _eP = "eventPayload"; const _eS = "eventStream"; const _em = "email"; const _en = "endpoint"; +const _ev = "events"; const _fWM = "fieldWithoutMessage"; const _fWMi = "fieldWithMessage"; const _g = "gamma"; -const _h = "http"; +const _h = "heartbeat"; const _hE = "httpError"; +const _hH = "httpHeader"; const _hL = "hostLabel"; +const _ht = "http"; const _i = "id"; const _iDN = "inexplicablyDeprecatedNumbers"; +const _l = "level"; +const _lo = "log"; const _m = "message"; +const _mE = "maxEvents"; const _mR = "maxResults"; +const _me = "metric"; const _n = "name"; const _nT = "nextToken"; +const _no = "notification"; const _nu = "number"; const _num = "numbers"; const _oP = "overloadedParam"; +const _p = "payload"; const _r = "results"; const _s = "smithy.ts.sdk.synthetic.org.xyz.v1"; -const _sN = "sparseNumbers"; +const _sI = "subscriptionId"; +const _sIe = "sessionId"; +const _sN = "sequenceNumber"; +const _sNp = "sparseNumbers"; const _sT = "startToken"; const _sp = "sparse"; const _st = "state"; @@ -70,8 +100,16 @@ const _str = "streaming"; const _t = "timestamp"; const _ta = "tags"; const _to = "token"; +const _top = "topic"; const _u = "username"; const _uT = "uniqueTags"; +const _v = "values"; +const _va = "value"; +const _xc = "x-channel"; +const _xec = "x-event-count"; +const _xme = "x-max-events"; +const _xsi = "x-subscription-id"; +const _xsi_ = "x-session-id"; const _zC = "zipCode"; const n0 = "org.xyz.v1"; const n1 = "org.xyz.secondary"; @@ -182,30 +220,80 @@ export var DifferentShapeName$: StaticStructureSchema = [3, n0, _DSN, [_n, _nu], [0, 1] ]; +export var Gamma$: StaticStructureSchema = [3, n0, _G, + 0, + [_sN, _p], + [[1, { [_eH]: 1 }], [() => GammaPayload$, { [_eP]: 1 }]] +]; +export var GammaPayload$: StaticStructureSchema = [3, n0, _GP, + 0, + [_m, _v], + [0, 64 | 1] +]; export var GetNumbersRequest$: StaticStructureSchema = [3, n0, _GNR, 0, - [_bD, _bI, _fWM, _fWMi, _sT, _mR, _cHI, _num, _sN], + [_bD, _bI, _fWM, _fWMi, _sT, _mR, _cHI, _num, _sNp], [19, 17, 0, 0, 0, 1, 0, 128 | 1, [() => SparseIntegerMap, 0]] ]; export var GetNumbersResponse$: StaticStructureSchema = [3, n0, _GNRe, 0, - [_bD, _bI, _num, _sN, _nT, _dN, _dNWE, _dNWC, _iDN], + [_bD, _bI, _num, _sNp, _nT, _dN, _dNWE, _dNWC, _iDN], [19, 17, 64 | 1, [() => SparseIntegerList, 0], 0, 64 | 1, 64 | 1, 64 | 1, 64 | 1] ]; +export var HeartbeatEvent$: StaticStructureSchema = [3, n0, _HEe, + 0, + [_t], + [4] +]; export var HostPrefixOperationInput$: StaticStructureSchema = [3, n0, _HPOI, 0, [_AI], [[0, { [_hL]: 1 }]], 1 ]; +export var LogEvent$: StaticStructureSchema = [3, n0, _LE, + 0, + [_l, _m], + [0, 0] +]; +export var MetricEvent$: StaticStructureSchema = [3, n0, _ME, + 0, + [_n, _va], + [0, 1] +]; +export var NotificationEvent$: StaticStructureSchema = [3, n0, _NE, + 0, + [_top, _p], + [0, 0] +]; +export var PublishEventsRequest$: StaticStructureSchema = [3, n0, _PER, + 0, + [_ch, _ev], + [[0, { [_hH]: _xc }], [() => PublishEventStream$, 16]] +]; +export var PublishEventsResponse$: StaticStructureSchema = [3, n0, _PERu, + 0, + [_eC, _m], + [[1, { [_hH]: _xec }], 0] +]; +export var SubscribeToEventsRequest$: StaticStructureSchema = [3, n0, _STER, + 0, + [_ch, _mE], + [[0, { [_hH]: _xc }], [1, { [_hH]: _xme }]] +]; +export var SubscribeToEventsResponse$: StaticStructureSchema = [3, n0, _STERu, + 0, + [_sI, _ev], + [[0, { [_hH]: _xsi }], [() => SubscribeEventStream$, 16]] +]; export var TradeEventStreamRequest$: StaticStructureSchema = [3, n0, _TESR, 0, - [_eS], - [[() => TradeEvents$, 0]] + [_sIe, _eS], + [[0, { [_hH]: _xsi_ }], [() => TradeEvents$, 16]] ]; export var TradeEventStreamResponse$: StaticStructureSchema = [3, n0, _TESRr, 0, - [_eS], - [[() => TradeEvents$, 0]] + [_sIe, _eS], + [[0, { [_hH]: _xsi_ }], [() => TradeEvents$, 16]] ]; export var ValidatedInput$: StaticStructureSchema = [3, n0, _VI, 0, @@ -229,26 +317,42 @@ var IntegerMap = 128 | 1; var SparseIntegerMap: StaticMapSchema = [2, n0, _SIM, { [_sp]: 1 }, 0, 1 ]; +export var PublishEventStream$: StaticUnionSchema = [4, n0, _PES, + { [_str]: 1 }, + [_lo, _me], + [() => LogEvent$, () => MetricEvent$] +]; +export var SubscribeEventStream$: StaticUnionSchema = [4, n0, _SES, + { [_str]: 1 }, + [_no, _h], + [() => NotificationEvent$, () => HeartbeatEvent$] +]; export var TradeEvents$: StaticUnionSchema = [4, n0, _TE, { [_str]: 1 }, [_al, _b, _g, _d], - [() => Alpha$, () => __Unit, () => __Unit, () => DifferentShapeName$] + [() => Alpha$, () => __Unit, [() => Gamma$, 0], () => DifferentShapeName$] ]; export var HttpLabelCommand$: StaticOperationSchema = [9, n1, _HLC, - { [_h]: ["POST", "/{LabelDoesNotApplyToRpcProtocol}", 200] }, () => HttpLabelCommandInput$, () => HttpLabelCommandOutput$ + { [_ht]: ["POST", "/{LabelDoesNotApplyToRpcProtocol}", 200] }, () => HttpLabelCommandInput$, () => HttpLabelCommandOutput$ ]; export var camelCaseOperation$: StaticOperationSchema = [9, n0, _cCO, - { [_h]: ["POST", "/camel-case", 200] }, () => camelCaseOperationInput$, () => camelCaseOperationOutput$ + { [_ht]: ["POST", "/camel-case", 200] }, () => camelCaseOperationInput$, () => camelCaseOperationOutput$ ]; export var GetNumbers$: StaticOperationSchema = [9, n0, _GN, - { [_h]: ["POST", "/get-numbers", 200] }, () => GetNumbersRequest$, () => GetNumbersResponse$ + { [_ht]: ["POST", "/get-numbers", 200] }, () => GetNumbersRequest$, () => GetNumbersResponse$ ]; export var HostPrefixOperation$: StaticOperationSchema = [9, n0, _HPO, { [_en]: ["{AccountId}."] }, () => HostPrefixOperationInput$, () => __Unit ]; +export var PublishEvents$: StaticOperationSchema = [9, n0, _PE, + { [_ht]: ["POST", "/publish-events", 200] }, () => PublishEventsRequest$, () => PublishEventsResponse$ +]; +export var SubscribeToEvents$: StaticOperationSchema = [9, n0, _STE, + { [_ht]: ["POST", "/subscribe-to-events", 200] }, () => SubscribeToEventsRequest$, () => SubscribeToEventsResponse$ +]; export var TradeEventStream$: StaticOperationSchema = [9, n0, _TES, - { [_h]: ["POST", "/trade-event-stream", 200] }, () => TradeEventStreamRequest$, () => TradeEventStreamResponse$ + { [_ht]: ["POST", "/trade-event-stream", 200] }, () => TradeEventStreamRequest$, () => TradeEventStreamResponse$ ]; export var ValidatedOperation$: StaticOperationSchema = [9, n0, _VOa, - { [_h]: ["POST", "/validated", 200] }, () => ValidatedInput$, () => ValidatedOutput$ + { [_ht]: ["POST", "/validated", 200] }, () => ValidatedInput$, () => ValidatedOutput$ ]; diff --git a/private/my-local-model-schema/test/index-objects.spec.mjs b/private/my-local-model-schema/test/index-objects.spec.mjs index 7ca5f77f06c..f4f941bf3c4 100644 --- a/private/my-local-model-schema/test/index-objects.spec.mjs +++ b/private/my-local-model-schema/test/index-objects.spec.mjs @@ -8,12 +8,15 @@ import { CodedThrottlingError$, ConstrainedAddress$, DifferentShapeName$, + Gamma$, + GammaPayload$, GetNumbers$, GetNumbersCommand, GetNumbersRequest$, GetNumbersResponse$, HaltError, HaltError$, + HeartbeatEvent$, HostPrefixOperation$, HostPrefixOperationCommand, HostPrefixOperationInput$, @@ -21,14 +24,27 @@ import { HttpLabelCommandCommand, HttpLabelCommandInput$, HttpLabelCommandOutput$, + LogEvent$, MainServiceLinkedError, MainServiceLinkedError$, + MetricEvent$, MysteryThrottlingError, MysteryThrottlingError$, + NotificationEvent$, paginatecamelCaseOperation, paginateGetNumbers, + PublishEvents$, + PublishEventsCommand, + PublishEventsRequest$, + PublishEventsResponse$, + PublishEventStream$, RetryableError, RetryableError$, + SubscribeEventStream$, + SubscribeToEvents$, + SubscribeToEventsCommand, + SubscribeToEventsRequest$, + SubscribeToEventsResponse$, TradeEvents$, TradeEventStream$, TradeEventStreamCommand, @@ -63,6 +79,10 @@ assert(typeof GetNumbersCommand === "function"); assert(typeof GetNumbers$ === "object"); assert(typeof HostPrefixOperationCommand === "function"); assert(typeof HostPrefixOperation$ === "object"); +assert(typeof PublishEventsCommand === "function"); +assert(typeof PublishEvents$ === "object"); +assert(typeof SubscribeToEventsCommand === "function"); +assert(typeof SubscribeToEvents$ === "object"); assert(typeof TradeEventStreamCommand === "function"); assert(typeof TradeEventStream$ === "object"); assert(typeof ValidatedOperationCommand === "function"); @@ -75,9 +95,21 @@ assert(typeof camelCaseOperationInput$ === "object"); assert(typeof camelCaseOperationOutput$ === "object"); assert(typeof ConstrainedAddress$ === "object"); assert(typeof DifferentShapeName$ === "object"); +assert(typeof Gamma$ === "object"); +assert(typeof GammaPayload$ === "object"); assert(typeof GetNumbersRequest$ === "object"); assert(typeof GetNumbersResponse$ === "object"); +assert(typeof HeartbeatEvent$ === "object"); assert(typeof HostPrefixOperationInput$ === "object"); +assert(typeof LogEvent$ === "object"); +assert(typeof MetricEvent$ === "object"); +assert(typeof NotificationEvent$ === "object"); +assert(typeof PublishEventsRequest$ === "object"); +assert(typeof PublishEventsResponse$ === "object"); +assert(typeof PublishEventStream$ === "object"); +assert(typeof SubscribeEventStream$ === "object"); +assert(typeof SubscribeToEventsRequest$ === "object"); +assert(typeof SubscribeToEventsResponse$ === "object"); assert(typeof TradeEvents$ === "object"); assert(typeof TradeEventStreamRequest$ === "object"); assert(typeof TradeEventStreamResponse$ === "object"); diff --git a/private/my-local-model-schema/test/index-types.ts b/private/my-local-model-schema/test/index-types.ts index 6c092003e06..1b84c836f5f 100644 --- a/private/my-local-model-schema/test/index-types.ts +++ b/private/my-local-model-schema/test/index-types.ts @@ -14,6 +14,12 @@ export type { HostPrefixOperationCommand, HostPrefixOperationCommandInput, HostPrefixOperationCommandOutput, + PublishEventsCommand, + PublishEventsCommandInput, + PublishEventsCommandOutput, + SubscribeToEventsCommand, + SubscribeToEventsCommandInput, + SubscribeToEventsCommandOutput, TradeEventStreamCommand, TradeEventStreamCommandInput, TradeEventStreamCommandOutput, @@ -27,9 +33,21 @@ export type { CamelCaseOperationOutput, ConstrainedAddress, DifferentShapeName, + Gamma, + GammaPayload, GetNumbersRequest, GetNumbersResponse, + HeartbeatEvent, HostPrefixOperationInput, + LogEvent, + MetricEvent, + NotificationEvent, + PublishEventsRequest, + PublishEventsResponse, + PublishEventStream, + SubscribeEventStream, + SubscribeToEventsRequest, + SubscribeToEventsResponse, TradeEvents, TradeEventStreamRequest, TradeEventStreamResponse, diff --git a/private/my-local-model-schema/test/snapshots.integ.spec.ts b/private/my-local-model-schema/test/snapshots.integ.spec.ts index 9ea801e11a2..9343fca0c50 100644 --- a/private/my-local-model-schema/test/snapshots.integ.spec.ts +++ b/private/my-local-model-schema/test/snapshots.integ.spec.ts @@ -16,7 +16,11 @@ import { HttpLabelCommandCommand, MainServiceLinkedError$, MysteryThrottlingError$, + PublishEvents$, + PublishEventsCommand, RetryableError$, + SubscribeToEvents$, + SubscribeToEventsCommand, TradeEventStream$, TradeEventStreamCommand, ValidatedOperation$, @@ -47,6 +51,8 @@ describe("XYZServiceClient" + ` (${mode})`, () => { [camelCaseOperation$, CamelCaseOperationCommand], [GetNumbers$, GetNumbersCommand], [HostPrefixOperation$, HostPrefixOperationCommand], + [PublishEvents$, PublishEventsCommand], + [SubscribeToEvents$, SubscribeToEventsCommand], [TradeEventStream$, TradeEventStreamCommand], [ValidatedOperation$, ValidatedOperationCommand], ]), diff --git a/private/my-local-model-schema/test/snapshots/req/PublishEvents.txt b/private/my-local-model-schema/test/snapshots/req/PublishEvents.txt new file mode 100644 index 00000000000..80bbb413173 --- /dev/null +++ b/private/my-local-model-schema/test/snapshots/req/PublishEvents.txt @@ -0,0 +1,70 @@ +POST https://localhost +/mock-required-endpoint/service/XYZService/operation/PublishEvents + +x-api-key: [object Object] +content-type: application/cbor +smithy-protocol: rpc-v2-cbor +accept: application/cbor +amz-sdk-invocation-id: 1111abcd-uuid-uuid-uuid-000000001111 +amz-sdk-request: attempt=1; max=3 +X-Api-Key: MOCK_api_key + +[async_iterable (Readable)] + +[chunk (event-stream object view)] + [total-size] 122 [header-size] 85 [prelude-crc] 3443622524 +:event-type: initial-request +:message-type: event +:content-type: application/cbor + +[cbor object view] +{ + "channel": "__channel__" +} + +[actual bytes] +161, 103, 99, 104, 97, 110, 110, 101, 108, 107, 95, 95, 99, 104, 97, 110, 110, 101, 108, 95, 95 + +[message-crc] 1729834264 +============================================================ + +[chunk (event-stream object view)] + [total-size] 126 [header-size] 73 [prelude-crc] 750811379 +:event-type: log +:message-type: event +:content-type: application/cbor + +[cbor object view] +{ + "level": "__level__", + "message": "__message__" +} + +[actual bytes] +162, 101, 108, 101, 118, 101, 108, 105, 95, 95, 108, 101, 118, 101, 108, 95, 95, 103, 109, 101, 115, 115, 97, 103, +101, 107, 95, 95, 109, 101, 115, 115, 97, 103, 101, 95, 95 + +[message-crc] 1425231269 +============================================================ + +[chunk (event-stream object view)] + [total-size] 114 [header-size] 76 [prelude-crc] 2572837245 +:event-type: metric +:message-type: event +:content-type: application/cbor + +[cbor object view] +{ + "name": "__name__", + "value": 0 +} + +[actual bytes] +162, 100, 110, 97, 109, 101, 104, 95, 95, 110, 97, 109, 101, 95, 95, 101, 118, 97, 108, 117, 101, 0 + +[message-crc] 2310491168 +============================================================ + +[chunk (b64)] +Cg== + diff --git a/private/my-local-model-schema/test/snapshots/req/SubscribeToEvents.txt b/private/my-local-model-schema/test/snapshots/req/SubscribeToEvents.txt new file mode 100644 index 00000000000..615087259dd --- /dev/null +++ b/private/my-local-model-schema/test/snapshots/req/SubscribeToEvents.txt @@ -0,0 +1,21 @@ +POST https://localhost +/mock-required-endpoint/service/XYZService/operation/SubscribeToEvents + +x-api-key: [object Object] +content-type: application/cbor +smithy-protocol: rpc-v2-cbor +accept: application/cbor +content-length: 32 +amz-sdk-invocation-id: 1111abcd-uuid-uuid-uuid-000000001111 +amz-sdk-request: attempt=1; max=3 +X-Api-Key: MOCK_api_key + +[Uint8Array (cbor object view)] +{ + "channel": "__channel__", + "maxEvents": 0 +} + +[actual bytes] +162, 103, 99, 104, 97, 110, 110, 101, 108, 107, 95, 95, 99, 104, 97, 110, 110, 101, 108, 95, 95, 105, 109, 97, +120, 69, 118, 101, 110, 116, 115, 0 diff --git a/private/my-local-model-schema/test/snapshots/req/TradeEventStream.txt b/private/my-local-model-schema/test/snapshots/req/TradeEventStream.txt index f870d9145e5..e150e875bc4 100644 --- a/private/my-local-model-schema/test/snapshots/req/TradeEventStream.txt +++ b/private/my-local-model-schema/test/snapshots/req/TradeEventStream.txt @@ -5,7 +5,6 @@ x-api-key: [object Object] content-type: application/cbor smithy-protocol: rpc-v2-cbor accept: application/cbor -content-length: undefined amz-sdk-invocation-id: 1111abcd-uuid-uuid-uuid-000000001111 amz-sdk-request: attempt=1; max=3 X-Api-Key: MOCK_api_key @@ -13,18 +12,21 @@ X-Api-Key: MOCK_api_key [async_iterable (Readable)] [chunk (event-stream object view)] - [total-size] 102 [header-size] 85 [prelude-crc] 1750202623 + [total-size] 126 [header-size] 85 [prelude-crc] 952181948 :event-type: initial-request :message-type: event :content-type: application/cbor [cbor object view] -{} +{ + "sessionId": "__sessionId__" +} [actual bytes] -160 +161, 105, 115, 101, 115, 115, 105, 111, 110, 73, 100, 109, 95, 95, 115, 101, 115, 115, 105, 111, 110, 73, 100, 95, +95 -[message-crc] 802441068 +[message-crc] 3879059039 ============================================================ [chunk (event-stream object view)] @@ -65,18 +67,27 @@ X-Api-Key: MOCK_api_key ============================================================ [chunk (event-stream object view)] - [total-size] 92 [header-size] 75 [prelude-crc] 2043635131 + [total-size] 143 [header-size] 95 [prelude-crc] 2906698831 :event-type: gamma :message-type: event :content-type: application/cbor +sequenceNumber: 0 (integer) [cbor object view] -{} +{ + "message": "__message__", + "values": [ + 0, + 0, + 0 + ] +} [actual bytes] -160 +162, 103, 109, 101, 115, 115, 97, 103, 101, 107, 95, 95, 109, 101, 115, 115, 97, 103, 101, 95, 95, 102, 118, 97, +108, 117, 101, 115, 131, 0, 0, 0 -[message-crc] 2477278713 +[message-crc] 587089113 ============================================================ [chunk (event-stream object view)] diff --git a/private/my-local-model-schema/test/snapshots/res/PublishEvents.txt b/private/my-local-model-schema/test/snapshots/res/PublishEvents.txt new file mode 100644 index 00000000000..75ac15367e1 --- /dev/null +++ b/private/my-local-model-schema/test/snapshots/res/PublishEvents.txt @@ -0,0 +1,56 @@ +======================== minimal response ======================== +[status] 200 + +smithy-protocol: rpc-v2-cbor +content-type: application/cbor + +[Uint8Array (cbor object view)] +{} + +[actual bytes] +160 + + +--- [output object] --- +{ + $metadata: { + httpStatusCode: (number) 200, + requestId: (undefined), + extendedRequestId: (undefined), + cfId: (undefined), + attempts: (number) 1, + totalRetryDelay: (number) 0 + } +} + +======================== w/ optional fields ======================== +[status] 200 + +smithy-protocol: rpc-v2-cbor +content-type: application/cbor + +[Uint8Array (cbor object view)] +{ + "eventCount": 0, + "message": "__message__" +} + +[actual bytes] +162, 106, 101, 118, 101, 110, 116, 67, 111, 117, 110, 116, 0, 103, 109, 101, 115, 115, 97, 103, 101, 107, 95, 95, +109, 101, 115, 115, 97, 103, 101, 95, 95 + + +--- [output object] --- +{ + eventCount: (number) 0, + message: "__message__", + $metadata: { + httpStatusCode: (number) 200, + requestId: (undefined), + extendedRequestId: (undefined), + cfId: (undefined), + attempts: (number) 1, + totalRetryDelay: (number) 0 + } +} + diff --git a/private/my-local-model-schema/test/snapshots/res/SubscribeToEvents.txt b/private/my-local-model-schema/test/snapshots/res/SubscribeToEvents.txt new file mode 100644 index 00000000000..2a1942705ac --- /dev/null +++ b/private/my-local-model-schema/test/snapshots/res/SubscribeToEvents.txt @@ -0,0 +1,101 @@ +======================== minimal response ======================== +[status] 200 + +smithy-protocol: rpc-v2-cbor +content-type: application/cbor + +[async_iterable (Object)] + + + + +--- [output object] --- +{ + events: async_it[], + $metadata: { + httpStatusCode: (number) 200, + requestId: (undefined), + extendedRequestId: (undefined), + cfId: (undefined), + attempts: (number) 1, + totalRetryDelay: (number) 0 + } +} + +======================== w/ optional fields ======================== +[status] 200 + +smithy-protocol: rpc-v2-cbor +content-type: application/cbor + +[async_iterable (Readable)] + +[chunk (event-stream object view)] + [total-size] 135 [header-size] 82 [prelude-crc] 3816915763 +:event-type: notification +:message-type: event +:content-type: application/cbor + +[cbor object view] +{ + "topic": "__topic__", + "payload": "__payload__" +} + +[actual bytes] +162, 101, 116, 111, 112, 105, 99, 105, 95, 95, 116, 111, 112, 105, 99, 95, 95, 103, 112, 97, 121, 108, 111, 97, +100, 107, 95, 95, 112, 97, 121, 108, 111, 97, 100, 95, 95 + +[message-crc] 2400364377 +============================================================ + +[chunk (event-stream object view)] + [total-size] 116 [header-size] 79 [prelude-crc] 2400437607 +:event-type: heartbeat +:message-type: event +:content-type: application/cbor + +[cbor object view] +{ + "timestamp": { + "tag": 1, + "value": 946702799.999 + } +} + +[actual bytes] +161, 105, 116, 105, 109, 101, 115, 116, 97, 109, 112, 193, 251, 65, 204, 54, 196, 231, 255, 223, 59 + +[message-crc] 3042832380 +============================================================ + +[chunk (b64)] +Cg== + + + +--- [output object] --- +{ + events: async_it[ + { + notification: { + topic: "__topic__", + payload: "__payload__" + } + }, + { + heartbeat: { + timestamp: (Date) Fri, Dec 31, 1999, 20:59:59 Pacific Standard Time + } + } + ], + $metadata: { + httpStatusCode: (number) 200, + requestId: (undefined), + extendedRequestId: (undefined), + cfId: (undefined), + attempts: (number) 1, + totalRetryDelay: (number) 0 + } +} + diff --git a/private/my-local-model-schema/test/snapshots/res/TradeEventStream.txt b/private/my-local-model-schema/test/snapshots/res/TradeEventStream.txt index 766e5d5338c..9a4fee679c8 100644 --- a/private/my-local-model-schema/test/snapshots/res/TradeEventStream.txt +++ b/private/my-local-model-schema/test/snapshots/res/TradeEventStream.txt @@ -68,18 +68,27 @@ content-type: application/cbor ============================================================ [chunk (event-stream object view)] - [total-size] 92 [header-size] 75 [prelude-crc] 2043635131 + [total-size] 143 [header-size] 95 [prelude-crc] 2906698831 :event-type: gamma :message-type: event :content-type: application/cbor +sequenceNumber: 0 (integer) [cbor object view] -{} +{ + "message": "__message__", + "values": [ + 0, + 0, + 0 + ] +} [actual bytes] -160 +162, 103, 109, 101, 115, 115, 97, 103, 101, 107, 95, 95, 109, 101, 115, 115, 97, 103, 101, 95, 95, 102, 118, 97, +108, 117, 101, 115, 131, 0, 0, 0 -[message-crc] 2477278713 +[message-crc] 587089113 ============================================================ [chunk (event-stream object view)] @@ -118,7 +127,17 @@ Cg== beta: {} }, { - gamma: {} + gamma: { + sequenceNumber: (number) 0, + payload: { + message: "__message__", + values: [ + (number) 0, + (number) 0, + (number) 0 + ] + } + } }, { delta: { diff --git a/private/my-local-model/package.json b/private/my-local-model/package.json index 840c8256560..b51cb2857a7 100644 --- a/private/my-local-model/package.json +++ b/private/my-local-model/package.json @@ -1,7 +1,7 @@ { "name": "xyz", "description": "xyz client", - "version": "3.31.1", + "version": "3.32.0", "scripts": { "build": "concurrently 'npm:build:cjs' 'npm:build:es' 'npm:build:types'", "build:cjs": "tsc -p tsconfig.cjs.json", diff --git a/private/my-local-model/src/XYZService.ts b/private/my-local-model/src/XYZService.ts index 270da0dd083..8deb3cba1b8 100644 --- a/private/my-local-model/src/XYZService.ts +++ b/private/my-local-model/src/XYZService.ts @@ -27,6 +27,16 @@ import { type HttpLabelCommandCommandOutput, HttpLabelCommandCommand, } from "./commands/HttpLabelCommandCommand"; +import { + type PublishEventsCommandInput, + type PublishEventsCommandOutput, + PublishEventsCommand, +} from "./commands/PublishEventsCommand"; +import { + type SubscribeToEventsCommandInput, + type SubscribeToEventsCommandOutput, + SubscribeToEventsCommand, +} from "./commands/SubscribeToEventsCommand"; import { type TradeEventStreamCommandInput, type TradeEventStreamCommandOutput, @@ -51,6 +61,8 @@ const commands = { CamelCaseOperationCommand, GetNumbersCommand, HostPrefixOperationCommand, + PublishEventsCommand, + SubscribeToEventsCommand, TradeEventStreamCommand, ValidatedOperationCommand, }; @@ -135,6 +147,42 @@ export interface XYZService { cb: (err: any, data?: HostPrefixOperationCommandOutput) => void ): void; + /** + * @see {@link PublishEventsCommand} + */ + publishEvents(): Promise; + publishEvents( + args: PublishEventsCommandInput, + options?: __HttpHandlerOptions + ): Promise; + publishEvents( + args: PublishEventsCommandInput, + cb: (err: any, data?: PublishEventsCommandOutput) => void + ): void; + publishEvents( + args: PublishEventsCommandInput, + options: __HttpHandlerOptions, + cb: (err: any, data?: PublishEventsCommandOutput) => void + ): void; + + /** + * @see {@link SubscribeToEventsCommand} + */ + subscribeToEvents(): Promise; + subscribeToEvents( + args: SubscribeToEventsCommandInput, + options?: __HttpHandlerOptions + ): Promise; + subscribeToEvents( + args: SubscribeToEventsCommandInput, + cb: (err: any, data?: SubscribeToEventsCommandOutput) => void + ): void; + subscribeToEvents( + args: SubscribeToEventsCommandInput, + options: __HttpHandlerOptions, + cb: (err: any, data?: SubscribeToEventsCommandOutput) => void + ): void; + /** * @see {@link TradeEventStreamCommand} */ diff --git a/private/my-local-model/src/XYZServiceClient.ts b/private/my-local-model/src/XYZServiceClient.ts index 4f391d53e2a..447111235d2 100644 --- a/private/my-local-model/src/XYZServiceClient.ts +++ b/private/my-local-model/src/XYZServiceClient.ts @@ -54,6 +54,11 @@ import type { HostPrefixOperationCommandOutput, } from "./commands/HostPrefixOperationCommand"; import type { HttpLabelCommandCommandInput, HttpLabelCommandCommandOutput } from "./commands/HttpLabelCommandCommand"; +import type { PublishEventsCommandInput, PublishEventsCommandOutput } from "./commands/PublishEventsCommand"; +import type { + SubscribeToEventsCommandInput, + SubscribeToEventsCommandOutput, +} from "./commands/SubscribeToEventsCommand"; import type { TradeEventStreamCommandInput, TradeEventStreamCommandOutput } from "./commands/TradeEventStreamCommand"; import type { ValidatedOperationCommandInput, @@ -78,6 +83,8 @@ export type ServiceInputTypes = | GetNumbersCommandInput | HostPrefixOperationCommandInput | HttpLabelCommandCommandInput + | PublishEventsCommandInput + | SubscribeToEventsCommandInput | TradeEventStreamCommandInput | ValidatedOperationCommandInput; @@ -89,6 +96,8 @@ export type ServiceOutputTypes = | GetNumbersCommandOutput | HostPrefixOperationCommandOutput | HttpLabelCommandCommandOutput + | PublishEventsCommandOutput + | SubscribeToEventsCommandOutput | TradeEventStreamCommandOutput | ValidatedOperationCommandOutput; diff --git a/private/my-local-model/src/commands/PublishEventsCommand.ts b/private/my-local-model/src/commands/PublishEventsCommand.ts new file mode 100644 index 00000000000..902f2d256dd --- /dev/null +++ b/private/my-local-model/src/commands/PublishEventsCommand.ts @@ -0,0 +1,118 @@ +// smithy-typescript generated code +import { Command as $Command } from "@smithy/core/client"; +import { getEndpointPlugin } from "@smithy/core/endpoints"; +import { getSerdePlugin } from "@smithy/core/serde"; +import type { MetadataBearer as __MetadataBearer } from "@smithy/types"; + +import { commonParams } from "../endpoint/EndpointParameters"; +import { + type PublishEventsRequest, + type PublishEventsResponse, + PublishEventsRequestFilterSensitiveLog, +} from "../models/models_0"; +import { de_PublishEventsCommand, se_PublishEventsCommand } from "../protocols/Rpcv2cbor"; +import type { ServiceInputTypes, ServiceOutputTypes, XYZServiceClientResolvedConfig } from "../XYZServiceClient"; + +/** + * @public + */ +export type { __MetadataBearer }; +/** + * @public + * + * The input for {@link PublishEventsCommand}. + */ +export interface PublishEventsCommandInput extends PublishEventsRequest {} +/** + * @public + * + * The output of {@link PublishEventsCommand}. + */ +export interface PublishEventsCommandOutput extends PublishEventsResponse, __MetadataBearer {} + +/** + * Input-only event stream: client sends events, server responds with a summary. + * @example + * Use a bare-bones client and the command you need to make an API call. + * ```javascript + * import { XYZServiceClient, PublishEventsCommand } from "xyz"; // ES Modules import + * // const { XYZServiceClient, PublishEventsCommand } = require("xyz"); // CommonJS import + * // import type { XYZServiceClientConfig } from "xyz"; + * const config = {}; // type is XYZServiceClientConfig + * const client = new XYZServiceClient(config); + * const input = { // PublishEventsRequest + * channel: "STRING_VALUE", + * events: { // PublishEventStream Union: only one key present + * log: { // LogEvent + * level: "STRING_VALUE", + * message: "STRING_VALUE", + * }, + * metric: { // MetricEvent + * name: "STRING_VALUE", + * value: Number("double"), + * }, + * }, + * }; + * const command = new PublishEventsCommand(input); + * const response = await client.send(command); + * // { // PublishEventsResponse + * // eventCount: Number("int"), + * // message: "STRING_VALUE", + * // }; + * + * ``` + * + * @param PublishEventsCommandInput - {@link PublishEventsCommandInput} + * @returns {@link PublishEventsCommandOutput} + * @see {@link PublishEventsCommandInput} for command's `input` shape. + * @see {@link PublishEventsCommandOutput} for command's `response` shape. + * @see {@link XYZServiceClientResolvedConfig | config} for XYZServiceClient's `config` shape. + * + * @throws {@link MainServiceLinkedError} (client fault) + * + * @throws {@link XYZServiceSyntheticServiceException} + *

Base exception class for all service exceptions from XYZService service.

+ * + * + * @public + */ +export class PublishEventsCommand extends $Command + .classBuilder< + PublishEventsCommandInput, + PublishEventsCommandOutput, + XYZServiceClientResolvedConfig, + ServiceInputTypes, + ServiceOutputTypes + >() + .ep(commonParams) + .m(function (this: any, Command: any, cs: any, config: XYZServiceClientResolvedConfig, o: any) { + return [ + getSerdePlugin(config, this.serialize, this.deserialize), + getEndpointPlugin(config, Command.getEndpointParameterInstructions()), + ]; + }) + .s("XYZService", "PublishEvents", { + /** + * @internal + */ + eventStream: { + input: true, + }, + }) + .n("XYZServiceClient", "PublishEventsCommand") + .f(PublishEventsRequestFilterSensitiveLog, void 0) + .ser(se_PublishEventsCommand) + .de(de_PublishEventsCommand) + .build() { + /** @internal type navigation helper, not in runtime. */ + protected declare static __types: { + api: { + input: PublishEventsRequest; + output: PublishEventsResponse; + }; + sdk: { + input: PublishEventsCommandInput; + output: PublishEventsCommandOutput; + }; + }; +} diff --git a/private/my-local-model/src/commands/SubscribeToEventsCommand.ts b/private/my-local-model/src/commands/SubscribeToEventsCommand.ts new file mode 100644 index 00000000000..b9ea4641e82 --- /dev/null +++ b/private/my-local-model/src/commands/SubscribeToEventsCommand.ts @@ -0,0 +1,117 @@ +// smithy-typescript generated code +import { Command as $Command } from "@smithy/core/client"; +import { getEndpointPlugin } from "@smithy/core/endpoints"; +import { getSerdePlugin } from "@smithy/core/serde"; +import type { MetadataBearer as __MetadataBearer } from "@smithy/types"; + +import { commonParams } from "../endpoint/EndpointParameters"; +import { + type SubscribeToEventsRequest, + type SubscribeToEventsResponse, + SubscribeToEventsResponseFilterSensitiveLog, +} from "../models/models_0"; +import { de_SubscribeToEventsCommand, se_SubscribeToEventsCommand } from "../protocols/Rpcv2cbor"; +import type { ServiceInputTypes, ServiceOutputTypes, XYZServiceClientResolvedConfig } from "../XYZServiceClient"; + +/** + * @public + */ +export type { __MetadataBearer }; +/** + * @public + * + * The input for {@link SubscribeToEventsCommand}. + */ +export interface SubscribeToEventsCommandInput extends SubscribeToEventsRequest {} +/** + * @public + * + * The output of {@link SubscribeToEventsCommand}. + */ +export interface SubscribeToEventsCommandOutput extends SubscribeToEventsResponse, __MetadataBearer {} + +/** + * Output-only event stream: client sends a subscription request, server streams events. + * @example + * Use a bare-bones client and the command you need to make an API call. + * ```javascript + * import { XYZServiceClient, SubscribeToEventsCommand } from "xyz"; // ES Modules import + * // const { XYZServiceClient, SubscribeToEventsCommand } = require("xyz"); // CommonJS import + * // import type { XYZServiceClientConfig } from "xyz"; + * const config = {}; // type is XYZServiceClientConfig + * const client = new XYZServiceClient(config); + * const input = { // SubscribeToEventsRequest + * channel: "STRING_VALUE", + * maxEvents: Number("int"), + * }; + * const command = new SubscribeToEventsCommand(input); + * const response = await client.send(command); + * // { // SubscribeToEventsResponse + * // subscriptionId: "STRING_VALUE", + * // events: { // SubscribeEventStream Union: only one key present + * // notification: { // NotificationEvent + * // topic: "STRING_VALUE", + * // payload: "STRING_VALUE", + * // }, + * // heartbeat: { // HeartbeatEvent + * // timestamp: new Date("TIMESTAMP"), + * // }, + * // }, + * // }; + * + * ``` + * + * @param SubscribeToEventsCommandInput - {@link SubscribeToEventsCommandInput} + * @returns {@link SubscribeToEventsCommandOutput} + * @see {@link SubscribeToEventsCommandInput} for command's `input` shape. + * @see {@link SubscribeToEventsCommandOutput} for command's `response` shape. + * @see {@link XYZServiceClientResolvedConfig | config} for XYZServiceClient's `config` shape. + * + * @throws {@link MainServiceLinkedError} (client fault) + * + * @throws {@link XYZServiceSyntheticServiceException} + *

Base exception class for all service exceptions from XYZService service.

+ * + * + * @public + */ +export class SubscribeToEventsCommand extends $Command + .classBuilder< + SubscribeToEventsCommandInput, + SubscribeToEventsCommandOutput, + XYZServiceClientResolvedConfig, + ServiceInputTypes, + ServiceOutputTypes + >() + .ep(commonParams) + .m(function (this: any, Command: any, cs: any, config: XYZServiceClientResolvedConfig, o: any) { + return [ + getSerdePlugin(config, this.serialize, this.deserialize), + getEndpointPlugin(config, Command.getEndpointParameterInstructions()), + ]; + }) + .s("XYZService", "SubscribeToEvents", { + /** + * @internal + */ + eventStream: { + output: true, + }, + }) + .n("XYZServiceClient", "SubscribeToEventsCommand") + .f(void 0, SubscribeToEventsResponseFilterSensitiveLog) + .ser(se_SubscribeToEventsCommand) + .de(de_SubscribeToEventsCommand) + .build() { + /** @internal type navigation helper, not in runtime. */ + protected declare static __types: { + api: { + input: SubscribeToEventsRequest; + output: SubscribeToEventsResponse; + }; + sdk: { + input: SubscribeToEventsCommandInput; + output: SubscribeToEventsCommandOutput; + }; + }; +} diff --git a/private/my-local-model/src/commands/TradeEventStreamCommand.ts b/private/my-local-model/src/commands/TradeEventStreamCommand.ts index 716fe5bcfaa..784b20a6e70 100644 --- a/private/my-local-model/src/commands/TradeEventStreamCommand.ts +++ b/private/my-local-model/src/commands/TradeEventStreamCommand.ts @@ -43,13 +43,22 @@ export interface TradeEventStreamCommandOutput extends TradeEventStreamResponse, * const config = {}; // type is XYZServiceClientConfig * const client = new XYZServiceClient(config); * const input = { // TradeEventStreamRequest + * sessionId: "STRING_VALUE", * eventStream: { // TradeEvents Union: only one key present * alpha: { // Alpha * id: "STRING_VALUE", * timestamp: new Date("TIMESTAMP"), * }, * beta: {}, - * gamma: {}, + * gamma: { // Gamma + * sequenceNumber: Number("int"), + * payload: { // GammaPayload + * message: "STRING_VALUE", + * values: [ // IntegerList + * Number("int"), + * ], + * }, + * }, * delta: { // DifferentShapeName * name: "STRING_VALUE", * number: Number("int"), @@ -59,13 +68,22 @@ export interface TradeEventStreamCommandOutput extends TradeEventStreamResponse, * const command = new TradeEventStreamCommand(input); * const response = await client.send(command); * // { // TradeEventStreamResponse + * // sessionId: "STRING_VALUE", * // eventStream: { // TradeEvents Union: only one key present * // alpha: { // Alpha * // id: "STRING_VALUE", * // timestamp: new Date("TIMESTAMP"), * // }, * // beta: {}, - * // gamma: {}, + * // gamma: { // Gamma + * // sequenceNumber: Number("int"), + * // payload: { // GammaPayload + * // message: "STRING_VALUE", + * // values: [ // IntegerList + * // Number("int"), + * // ], + * // }, + * // }, * // delta: { // DifferentShapeName * // name: "STRING_VALUE", * // number: Number("int"), diff --git a/private/my-local-model/src/commands/index.ts b/private/my-local-model/src/commands/index.ts index 72947904e64..6be90e24808 100644 --- a/private/my-local-model/src/commands/index.ts +++ b/private/my-local-model/src/commands/index.ts @@ -3,5 +3,7 @@ export * from "./CamelCaseOperationCommand"; export * from "./GetNumbersCommand"; export * from "./HostPrefixOperationCommand"; export * from "./HttpLabelCommandCommand"; +export * from "./PublishEventsCommand"; +export * from "./SubscribeToEventsCommand"; export * from "./TradeEventStreamCommand"; export * from "./ValidatedOperationCommand"; diff --git a/private/my-local-model/src/models/models_0.ts b/private/my-local-model/src/models/models_0.ts index 8bcc9d9f734..51bf47cc177 100644 --- a/private/my-local-model/src/models/models_0.ts +++ b/private/my-local-model/src/models/models_0.ts @@ -62,6 +62,22 @@ export interface DifferentShapeName { number?: number | undefined; } +/** + * @public + */ +export interface GammaPayload { + message?: string | undefined; + values?: number[] | undefined; +} + +/** + * @public + */ +export interface Gamma { + sequenceNumber?: number | undefined; + payload?: GammaPayload | undefined; +} + /** * @public */ @@ -129,6 +145,13 @@ export interface GetNumbersResponse { inexplicablyDeprecatedNumbers?: number[] | undefined; } +/** + * @public + */ +export interface HeartbeatEvent { + timestamp?: Date | undefined; +} + /** * @public */ @@ -136,6 +159,206 @@ export interface HostPrefixOperationInput { AccountId: string | undefined; } +/** + * @public + */ +export interface LogEvent { + level?: string | undefined; + message?: string | undefined; +} + +/** + * @public + */ +export interface MetricEvent { + name?: string | undefined; + value?: number | undefined; +} + +/** + * @public + */ +export interface NotificationEvent { + topic?: string | undefined; + payload?: string | undefined; +} + +/** + * @public + */ +export type PublishEventStream = + | PublishEventStream.LogMember + | PublishEventStream.MetricMember + | PublishEventStream.$UnknownMember; + +/** + * @public + */ +export namespace PublishEventStream { + export interface LogMember { + log: LogEvent; + metric?: never; + $unknown?: never; + } + + export interface MetricMember { + log?: never; + metric: MetricEvent; + $unknown?: never; + } + + /** + * @public + */ + export interface $UnknownMember { + log?: never; + metric?: never; + $unknown: [string, any]; + } + + export interface Visitor { + log: (value: LogEvent) => T; + metric: (value: MetricEvent) => T; + _: (name: string, value: any) => T; + } + + export const visit = (value: PublishEventStream, visitor: Visitor): T => { + if (value.log !== undefined) return visitor.log(value.log); + if (value.metric !== undefined) return visitor.metric(value.metric); + return visitor._(value.$unknown[0], value.$unknown[1]); + }; +} +/** + * @internal + */ +export const PublishEventStreamFilterSensitiveLog = (obj: PublishEventStream): any => { + if (obj.log !== undefined) { + return { + log: obj.log + }; + } + if (obj.metric !== undefined) { + return { + metric: obj.metric + }; + } + if (obj.$unknown !== undefined) return { [obj.$unknown[0]]: "UNKNOWN" }; +} + +/** + * @public + */ +export interface PublishEventsRequest { + channel?: string | undefined; + events?: AsyncIterable | undefined; +} + +/** + * @internal + */ +export const PublishEventsRequestFilterSensitiveLog = (obj: PublishEventsRequest): any => ({ + ...obj, + ...(obj.events && { events: + 'STREAMING_CONTENT' + }), +}) + +/** + * @public + */ +export interface PublishEventsResponse { + eventCount?: number | undefined; + message?: string | undefined; +} + +/** + * @public + */ +export type SubscribeEventStream = + | SubscribeEventStream.HeartbeatMember + | SubscribeEventStream.NotificationMember + | SubscribeEventStream.$UnknownMember; + +/** + * @public + */ +export namespace SubscribeEventStream { + export interface NotificationMember { + notification: NotificationEvent; + heartbeat?: never; + $unknown?: never; + } + + export interface HeartbeatMember { + notification?: never; + heartbeat: HeartbeatEvent; + $unknown?: never; + } + + /** + * @public + */ + export interface $UnknownMember { + notification?: never; + heartbeat?: never; + $unknown: [string, any]; + } + + export interface Visitor { + notification: (value: NotificationEvent) => T; + heartbeat: (value: HeartbeatEvent) => T; + _: (name: string, value: any) => T; + } + + export const visit = (value: SubscribeEventStream, visitor: Visitor): T => { + if (value.notification !== undefined) return visitor.notification(value.notification); + if (value.heartbeat !== undefined) return visitor.heartbeat(value.heartbeat); + return visitor._(value.$unknown[0], value.$unknown[1]); + }; +} +/** + * @internal + */ +export const SubscribeEventStreamFilterSensitiveLog = (obj: SubscribeEventStream): any => { + if (obj.notification !== undefined) { + return { + notification: obj.notification + }; + } + if (obj.heartbeat !== undefined) { + return { + heartbeat: obj.heartbeat + }; + } + if (obj.$unknown !== undefined) return { [obj.$unknown[0]]: "UNKNOWN" }; +} + +/** + * @public + */ +export interface SubscribeToEventsRequest { + channel?: string | undefined; + maxEvents?: number | undefined; +} + +/** + * @public + */ +export interface SubscribeToEventsResponse { + subscriptionId?: string | undefined; + events?: AsyncIterable | undefined; +} + +/** + * @internal + */ +export const SubscribeToEventsResponseFilterSensitiveLog = (obj: SubscribeToEventsResponse): any => ({ + ...obj, + ...(obj.events && { events: + 'STREAMING_CONTENT' + }), +}) + /** * @public */ @@ -174,7 +397,7 @@ export namespace TradeEvents { export interface GammaMember { alpha?: never; beta?: never; - gamma: Unit; + gamma: Gamma; delta?: never; $unknown?: never; } @@ -201,7 +424,7 @@ export namespace TradeEvents { export interface Visitor { alpha: (value: Alpha) => T; beta: (value: Unit) => T; - gamma: (value: Unit) => T; + gamma: (value: Gamma) => T; delta: (value: DifferentShapeName) => T; _: (name: string, value: any) => T; } @@ -245,6 +468,7 @@ export const TradeEventsFilterSensitiveLog = (obj: TradeEvents): any => { * @public */ export interface TradeEventStreamRequest { + sessionId?: string | undefined; eventStream?: AsyncIterable | undefined; } @@ -262,6 +486,7 @@ export const TradeEventStreamRequestFilterSensitiveLog = (obj: TradeEventStreamR * @public */ export interface TradeEventStreamResponse { + sessionId?: string | undefined; eventStream?: AsyncIterable | undefined; } diff --git a/private/my-local-model/src/protocols/Rpcv2cbor.ts b/private/my-local-model/src/protocols/Rpcv2cbor.ts index 5653fc9ebc6..d3857cdefeb 100644 --- a/private/my-local-model/src/protocols/Rpcv2cbor.ts +++ b/private/my-local-model/src/protocols/Rpcv2cbor.ts @@ -47,6 +47,11 @@ import type { HostPrefixOperationCommandOutput, } from "../commands/HostPrefixOperationCommand"; import type { HttpLabelCommandCommandInput, HttpLabelCommandCommandOutput } from "../commands/HttpLabelCommandCommand"; +import type { PublishEventsCommandInput, PublishEventsCommandOutput } from "../commands/PublishEventsCommand"; +import type { + SubscribeToEventsCommandInput, + SubscribeToEventsCommandOutput, +} from "../commands/SubscribeToEventsCommand"; import type { TradeEventStreamCommandInput, TradeEventStreamCommandOutput } from "../commands/TradeEventStreamCommand"; import type { ValidatedOperationCommandInput, @@ -66,12 +71,21 @@ import { type CamelCaseOperationOutput, type ConstrainedAddress, type DifferentShapeName, + type Gamma, + type GammaPayload, type GetNumbersRequest, type GetNumbersResponse, + type HeartbeatEvent, type HostPrefixOperationInput, type HttpLabelCommandInput, + type LogEvent, + type MetricEvent, + type NotificationEvent, + type SubscribeEventStream, + type SubscribeToEventsRequest, type Unit, type ValidatedInput, + PublishEventStream, TradeEvents, } from "../models/models_0"; import { XYZServiceSyntheticServiceException as __BaseException } from "../models/XYZServiceSyntheticServiceException"; @@ -139,6 +153,36 @@ export const se_HostPrefixOperationCommand = async ( return buildHttpRpcRequest(context, headers, "/service/XYZService/operation/HostPrefixOperation", resolvedHostname, body); }; +/** + * serializeRpcv2cborPublishEventsCommand + */ +export const se_PublishEventsCommand = async ( + input: PublishEventsCommandInput, + context: __SerdeContext & __EventStreamSerdeContext +): Promise<__HttpRequest> => { + const headers: __HeaderBag = { ...SHARED_HEADERS }; + headers["content-type"] = "application/vnd.amazon.eventstream"; + + let body: any; + body = se_PublishEventStream(input.events, context); + return buildHttpRpcRequest(context, headers, "/service/XYZService/operation/PublishEvents", undefined, body); +}; + +/** + * serializeRpcv2cborSubscribeToEventsCommand + */ +export const se_SubscribeToEventsCommand = async ( + input: SubscribeToEventsCommandInput, + context: __SerdeContext +): Promise<__HttpRequest> => { + const headers: __HeaderBag = { ...SHARED_HEADERS }; + headers.accept = "application/vnd.amazon.eventstream"; + + let body: any; + body = cbor.serialize(_json(input)); + return buildHttpRpcRequest(context, headers, "/service/XYZService/operation/SubscribeToEvents", undefined, body); +}; + /** * serializeRpcv2cborTradeEventStreamCommand */ @@ -255,6 +299,48 @@ export const de_HostPrefixOperationCommand = async ( }; +/** + * deserializeRpcv2cborPublishEventsCommand + */ +export const de_PublishEventsCommand = async ( + output: __HttpResponse, + context: __SerdeContext +): Promise => { + cr(output); + if (output.statusCode >= 300) { + return de_CommandError(output, context); + } + + const data: any = await parseBody(output.body, context) + let contents: any = {}; + contents = _json(data); + const response: PublishEventsCommandOutput = { + $metadata: deserializeMetadata(output), ...contents, + }; + return response; + +}; + +/** + * deserializeRpcv2cborSubscribeToEventsCommand + */ +export const de_SubscribeToEventsCommand = async ( + output: __HttpResponse, + context: __SerdeContext & __EventStreamSerdeContext +): Promise => { + cr(output); + if (output.statusCode >= 300) { + return de_CommandError(output, context); + } + + const contents = { events: de_SubscribeEventStream(output.body, context) }; + const response: SubscribeToEventsCommandOutput = { + $metadata: deserializeMetadata(output), ...contents, + }; + return response; + +}; + /** * deserializeRpcv2cborTradeEventStreamCommand */ @@ -434,6 +520,20 @@ const de_XYZServiceServiceExceptionRes = async ( return __decorateServiceException(exception, body); }; +/** + * serializeRpcv2cborPublishEventStream + */ +const se_PublishEventStream = ( + input: any, + context: __SerdeContext & __EventStreamSerdeContext +): any => { + const eventMarshallingVisitor = (event: any): __Message => PublishEventStream.visit(event, { + log: value => se_LogEvent_event(value, context), + metric: value => se_MetricEvent_event(value, context), + _: value => value as any + }); + return context.eventStreamMarshaller.serialize(input, eventMarshallingVisitor); +} /** * serializeRpcv2cborTradeEvents */ @@ -444,7 +544,7 @@ const se_TradeEvents = ( const eventMarshallingVisitor = (event: any): __Message => TradeEvents.visit(event, { alpha: value => se_Alpha_event(value, context), beta: value => se_Unit_event(value, context), - gamma: value => se_Unit_event(value, context), + gamma: value => se_Gamma_event(value, context), delta: value => se_DifferentShapeName_event(value, context), _: value => value as any }); @@ -478,260 +578,403 @@ const se_Alpha_event = ( body = cbor.serialize(body); return { headers, body }; } - const se_Unit_event = ( - input: Unit, + const se_Gamma_event = ( + input: Gamma, context: __SerdeContext ): __Message => { const headers: __MessageHeaders = { - ":event-type": { type: "string", value: "beta" }, + ":event-type": { type: "string", value: "gamma" }, ":message-type": { type: "string", value: "event" }, ":content-type": { type: "string", value: "application/cbor" }, } + if (input.sequenceNumber != null) { + headers["sequenceNumber"] = { type: "integer", value: input.sequenceNumber } + } let body: Uint8Array = new Uint8Array(); - body = _json(input); - body = cbor.serialize(body); + if (input.payload != null) { + body = _json(input.payload); + body = cbor.serialize(body); + } return { headers, body }; } - /** - * deserializeRpcv2cborTradeEvents - */ - const de_TradeEvents = ( - output: any, - context: __SerdeContext & __EventStreamSerdeContext - ): AsyncIterable => { - return context.eventStreamMarshaller.deserialize( - output, - async event => { - if (event["alpha"] != null) { - return { - alpha: await de_Alpha_event(event["alpha"], context), - }; + const se_LogEvent_event = ( + input: LogEvent, + context: __SerdeContext + ): __Message => { + const headers: __MessageHeaders = { + ":event-type": { type: "string", value: "log" }, + ":message-type": { type: "string", value: "event" }, + ":content-type": { type: "string", value: "application/cbor" }, + } + let body: Uint8Array = new Uint8Array(); + body = _json(input); + body = cbor.serialize(body); + return { headers, body }; + } + const se_MetricEvent_event = ( + input: MetricEvent, + context: __SerdeContext + ): __Message => { + const headers: __MessageHeaders = { + ":event-type": { type: "string", value: "metric" }, + ":message-type": { type: "string", value: "event" }, + ":content-type": { type: "string", value: "application/cbor" }, + } + let body: Uint8Array = new Uint8Array(); + body = se_MetricEvent(input, context); + body = cbor.serialize(body); + return { headers, body }; + } + const se_Unit_event = ( + input: Unit, + context: __SerdeContext + ): __Message => { + const headers: __MessageHeaders = { + ":event-type": { type: "string", value: "beta" }, + ":message-type": { type: "string", value: "event" }, + ":content-type": { type: "string", value: "application/cbor" }, } - if (event["beta"] != null) { - return { - beta: await de_Unit_event(event["beta"], context), - }; + let body: Uint8Array = new Uint8Array(); + body = _json(input); + body = cbor.serialize(body); + return { headers, body }; } - if (event["gamma"] != null) { - return { - gamma: await de_Unit_event(event["gamma"], context), - }; + /** + * deserializeRpcv2cborSubscribeEventStream + */ + const de_SubscribeEventStream = ( + output: any, + context: __SerdeContext & __EventStreamSerdeContext + ): AsyncIterable => { + return context.eventStreamMarshaller.deserialize( + output, + async event => { + if (event["notification"] != null) { + return { + notification: await de_NotificationEvent_event(event["notification"], context), + }; + } + if (event["heartbeat"] != null) { + return { + heartbeat: await de_HeartbeatEvent_event(event["heartbeat"], context), + }; + } + return {$unknown: event as any}; + } + ); } - if (event["delta"] != null) { - return { - delta: await de_DifferentShapeName_event(event["delta"], context), - }; + /** + * deserializeRpcv2cborTradeEvents + */ + const de_TradeEvents = ( + output: any, + context: __SerdeContext & __EventStreamSerdeContext + ): AsyncIterable => { + return context.eventStreamMarshaller.deserialize( + output, + async event => { + if (event["alpha"] != null) { + return { + alpha: await de_Alpha_event(event["alpha"], context), + }; + } + if (event["beta"] != null) { + return { + beta: await de_Unit_event(event["beta"], context), + }; + } + if (event["gamma"] != null) { + return { + gamma: await de_Gamma_event(event["gamma"], context), + }; + } + if (event["delta"] != null) { + return { + delta: await de_DifferentShapeName_event(event["delta"], context), + }; + } + return {$unknown: event as any}; + } + ); + } + const de_Alpha_event = async ( + output: any, + context: __SerdeContext + ): Promise => { + const contents: Alpha = {} as any; + const data: any = await parseBody(output.body, context); + Object.assign(contents, de_Alpha(data, context)); + return contents; + } + const de_DifferentShapeName_event = async ( + output: any, + context: __SerdeContext + ): Promise => { + const contents: DifferentShapeName = {} as any; + const data: any = await parseBody(output.body, context); + Object.assign(contents, _json(data)); + return contents; + } + const de_Gamma_event = async ( + output: any, + context: __SerdeContext + ): Promise => { + const contents: Gamma = {} as any; + if (output.headers[_sN] !== undefined) { + contents[_sN] = output.headers[_sN].value; + } + + const data: any = await parseBody(output.body, context); + contents.payload = _json(data); + return contents; + } + const de_HeartbeatEvent_event = async ( + output: any, + context: __SerdeContext + ): Promise => { + const contents: HeartbeatEvent = {} as any; + const data: any = await parseBody(output.body, context); + Object.assign(contents, de_HeartbeatEvent(data, context)); + return contents; + } + const de_NotificationEvent_event = async ( + output: any, + context: __SerdeContext + ): Promise => { + const contents: NotificationEvent = {} as any; + const data: any = await parseBody(output.body, context); + Object.assign(contents, _json(data)); + return contents; + } + const de_Unit_event = async ( + output: any, + context: __SerdeContext + ): Promise => { + const contents: Unit = {} as any; + const data: any = await parseBody(output.body, context); + Object.assign(contents, _json(data)); + return contents; + } + // se_HttpLabelCommandInput omitted. + + /** + * serializeRpcv2cborAlpha + */ + const se_Alpha = ( + input: Alpha, + context: __SerdeContext + ): any => { + return take(input, { + 'id': [], + 'timestamp': __dateToTag, + }); } - return {$unknown: event as any}; - } - ); - } - const de_Alpha_event = async ( - output: any, - context: __SerdeContext - ): Promise => { - const contents: Alpha = {} as any; - const data: any = await parseBody(output.body, context); - Object.assign(contents, de_Alpha(data, context)); - return contents; - } - const de_DifferentShapeName_event = async ( - output: any, - context: __SerdeContext - ): Promise => { - const contents: DifferentShapeName = {} as any; - const data: any = await parseBody(output.body, context); - Object.assign(contents, _json(data)); - return contents; - } - const de_Unit_event = async ( - output: any, - context: __SerdeContext - ): Promise => { - const contents: Unit = {} as any; - const data: any = await parseBody(output.body, context); - Object.assign(contents, _json(data)); - return contents; - } - // se_HttpLabelCommandInput omitted. - - /** - * serializeRpcv2cborAlpha - */ - const se_Alpha = ( - input: Alpha, - context: __SerdeContext - ): any => { - return take(input, { - 'id': [], - 'timestamp': __dateToTag, - }); - } - // se_CamelCaseOperationInput omitted. + // se_CamelCaseOperationInput omitted. + + // se_ConstrainedAddress omitted. + + // se_DifferentShapeName omitted. + + // se_GammaPayload omitted. + + /** + * serializeRpcv2cborGetNumbersRequest + */ + const se_GetNumbersRequest = ( + input: GetNumbersRequest, + context: __SerdeContext + ): any => { + return take(input, { + 'bigDecimal': __nv, + 'bigInteger': [], + 'customHeaderInput': [], + 'fieldWithMessage': [], + 'fieldWithoutMessage': [], + 'maxResults': [], + 'numbers': _json, + 'sparseNumbers': _ => se_SparseIntegerMap(_, context), + 'startToken': [], + }); + } - // se_ConstrainedAddress omitted. + // se_HostPrefixOperationInput omitted. - // se_DifferentShapeName omitted. + // se_IntegerList omitted. - /** - * serializeRpcv2cborGetNumbersRequest - */ - const se_GetNumbersRequest = ( - input: GetNumbersRequest, - context: __SerdeContext - ): any => { - return take(input, { - 'bigDecimal': __nv, - 'bigInteger': [], - 'customHeaderInput': [], - 'fieldWithMessage': [], - 'fieldWithoutMessage': [], - 'maxResults': [], - 'numbers': _json, - 'sparseNumbers': _ => se_SparseIntegerMap(_, context), - 'startToken': [], - }); - } + // se_IntegerMap omitted. - // se_HostPrefixOperationInput omitted. + // se_LogEvent omitted. - // se_IntegerMap omitted. + /** + * serializeRpcv2cborMetricEvent + */ + const se_MetricEvent = ( + input: MetricEvent, + context: __SerdeContext + ): any => { + return take(input, { + 'name': [], + 'value': [], + }); + } - /** - * serializeRpcv2cborSparseIntegerMap - */ - const se_SparseIntegerMap = ( - input: Record, - context: __SerdeContext - ): any => { - return Object.entries(input).reduce((acc: Record, [key, value]: [string, any]) => { - if (value !== null) { - acc[key] = value; - } + /** + * serializeRpcv2cborSparseIntegerMap + */ + const se_SparseIntegerMap = ( + input: Record, + context: __SerdeContext + ): any => { + return Object.entries(input).reduce((acc: Record, [key, value]: [string, any]) => { + if (value !== null) { + acc[key] = value; + } + + else { + acc[key] = null as any; + } + + return acc; + }, {}); + } - else { - acc[key] = null as any; - } + // se_SubscribeToEventsRequest omitted. - return acc; - }, {}); - } + // se_TagList omitted. - // se_TagList omitted. + // se_UniqueTagList omitted. - // se_UniqueTagList omitted. + // se_ValidatedInput omitted. - // se_ValidatedInput omitted. + // se_Unit omitted. - // se_Unit omitted. + // de_HttpLabelCommandOutput omitted. - // de_HttpLabelCommandOutput omitted. + /** + * deserializeRpcv2cborAlpha + */ + const de_Alpha = ( + output: any, + context: __SerdeContext + ): Alpha => { + return take(output, { + 'id': __expectString, + 'timestamp': (_: any) => __expectNonNull(__parseEpochTimestamp(_)), + }) as any; + } - /** - * deserializeRpcv2cborAlpha - */ - const de_Alpha = ( - output: any, - context: __SerdeContext - ): Alpha => { - return take(output, { - 'id': __expectString, - 'timestamp': (_: any) => __expectNonNull(__parseEpochTimestamp(_)), - }) as any; - } + /** + * deserializeRpcv2cborBlobs + */ + const de_Blobs = ( + output: any, + context: __SerdeContext + ): Uint8Array[] => { + const collection = (output || []).filter((e: any) => e != null) + return collection; + } - /** - * deserializeRpcv2cborBlobs - */ - const de_Blobs = ( - output: any, - context: __SerdeContext - ): Uint8Array[] => { - const collection = (output || []).filter((e: any) => e != null) - return collection; - } + /** + * deserializeRpcv2cborCamelCaseOperationOutput + */ + const de_CamelCaseOperationOutput = ( + output: any, + context: __SerdeContext + ): CamelCaseOperationOutput => { + return take(output, { + 'results': (_: any) => de_Blobs(_, context), + 'token': __expectString, + }) as any; + } - /** - * deserializeRpcv2cborCamelCaseOperationOutput - */ - const de_CamelCaseOperationOutput = ( - output: any, - context: __SerdeContext - ): CamelCaseOperationOutput => { - return take(output, { - 'results': (_: any) => de_Blobs(_, context), - 'token': __expectString, - }) as any; - } + // de_CodedThrottlingError omitted. + + // de_DifferentShapeName omitted. + + // de_GammaPayload omitted. + + /** + * deserializeRpcv2cborGetNumbersResponse + */ + const de_GetNumbersResponse = ( + output: any, + context: __SerdeContext + ): GetNumbersResponse => { + return take(output, { + 'bigDecimal': [], + 'bigInteger': [], + 'deprecatedNumbers': _json, + 'deprecatedNumbersWithoutChronology': _json, + 'deprecatedNumbersWithoutExplanation': _json, + 'inexplicablyDeprecatedNumbers': _json, + 'nextToken': __expectString, + 'numbers': _json, + 'sparseNumbers': (_: any) => de_SparseIntegerList(_, context), + }) as any; + } - // de_CodedThrottlingError omitted. + // de_HaltError omitted. + + /** + * deserializeRpcv2cborHeartbeatEvent + */ + const de_HeartbeatEvent = ( + output: any, + context: __SerdeContext + ): HeartbeatEvent => { + return take(output, { + 'timestamp': (_: any) => __expectNonNull(__parseEpochTimestamp(_)), + }) as any; + } - // de_DifferentShapeName omitted. + // de_IntegerList omitted. - /** - * deserializeRpcv2cborGetNumbersResponse - */ - const de_GetNumbersResponse = ( - output: any, - context: __SerdeContext - ): GetNumbersResponse => { - return take(output, { - 'bigDecimal': [], - 'bigInteger': [], - 'deprecatedNumbers': _json, - 'deprecatedNumbersWithoutChronology': _json, - 'deprecatedNumbersWithoutExplanation': _json, - 'inexplicablyDeprecatedNumbers': _json, - 'nextToken': __expectString, - 'numbers': _json, - 'sparseNumbers': (_: any) => de_SparseIntegerList(_, context), - }) as any; - } + // de_MainServiceLinkedError omitted. - // de_HaltError omitted. + // de_MysteryThrottlingError omitted. - // de_IntegerList omitted. + // de_NotificationEvent omitted. - // de_MainServiceLinkedError omitted. + // de_PublishEventsResponse omitted. - // de_MysteryThrottlingError omitted. + // de_RetryableError omitted. - // de_RetryableError omitted. + /** + * deserializeRpcv2cborSparseIntegerList + */ + const de_SparseIntegerList = ( + output: any, + context: __SerdeContext + ): (number | null)[] => { + const collection = (output || []).map((entry: any) => { + if (entry === null) { + return null as any; + } + return __expectInt32(entry) as any; + }); + return collection; + } - /** - * deserializeRpcv2cborSparseIntegerList - */ - const de_SparseIntegerList = ( - output: any, - context: __SerdeContext - ): (number | null)[] => { - const collection = (output || []).map((entry: any) => { - if (entry === null) { - return null as any; - } - return __expectInt32(entry) as any; - }); - return collection; - } + // de_ValidatedOutput omitted. - // de_ValidatedOutput omitted. + // de_XYZServiceServiceException omitted. - // de_XYZServiceServiceException omitted. + // de_Unit omitted. - // de_Unit omitted. + const deserializeMetadata = (output: __HttpResponse): __ResponseMetadata => ({ + httpStatusCode: output.statusCode, + requestId: output.headers["x-amzn-requestid"] ?? output.headers["x-amzn-request-id"] ?? output.headers["x-amz-request-id"], + extendedRequestId: output.headers["x-amz-id-2"], + cfId: output.headers["x-amz-cf-id"], + }); - const deserializeMetadata = (output: __HttpResponse): __ResponseMetadata => ({ - httpStatusCode: output.statusCode, - requestId: output.headers["x-amzn-requestid"] ?? output.headers["x-amzn-request-id"] ?? output.headers["x-amz-request-id"], - extendedRequestId: output.headers["x-amz-id-2"], - cfId: output.headers["x-amz-cf-id"], - }); + const throwDefaultError = withBaseException(__BaseException); + const SHARED_HEADERS: __HeaderBag = { + 'content-type': "application/cbor", + "smithy-protocol": "rpc-v2-cbor", + "accept": "application/cbor", - const throwDefaultError = withBaseException(__BaseException); - const SHARED_HEADERS: __HeaderBag = { - 'content-type': "application/cbor", - "smithy-protocol": "rpc-v2-cbor", - "accept": "application/cbor", + }; - }; + const _sN = "sequenceNumber"; diff --git a/private/my-local-model/src/runtimeConfig.ts b/private/my-local-model/src/runtimeConfig.ts index bb710e832c6..ff5f241bb5d 100644 --- a/private/my-local-model/src/runtimeConfig.ts +++ b/private/my-local-model/src/runtimeConfig.ts @@ -8,7 +8,7 @@ import { NODE_RETRY_MODE_CONFIG_OPTIONS, } from "@smithy/core/retry"; import { calculateBodyLength } from "@smithy/core/serde"; -import { NodeHttpHandler as RequestHandler, streamCollector } from "@smithy/node-http-handler"; +import { NodeHttp2Handler as RequestHandler, streamCollector } from "@smithy/node-http-handler"; import { getRuntimeConfig as getSharedRuntimeConfig } from "./runtimeConfig.shared"; import type { XYZServiceClientConfig } from "./XYZServiceClient"; @@ -29,7 +29,10 @@ export const getRuntimeConfig = (config: XYZServiceClientConfig) => { bodyLengthChecker: config?.bodyLengthChecker ?? calculateBodyLength, eventStreamSerdeProvider: config?.eventStreamSerdeProvider ?? eventStreamSerdeProvider, maxAttempts: config?.maxAttempts ?? loadNodeConfig(NODE_MAX_ATTEMPT_CONFIG_OPTIONS, config), - requestHandler: RequestHandler.create(config?.requestHandler ?? defaultConfigProvider), + requestHandler: RequestHandler.create(config?.requestHandler ?? (async () => ({ + ...await defaultConfigProvider(), + disableConcurrentStreams: true + }))), retryMode: config?.retryMode ?? loadNodeConfig( diff --git a/smithy-typescript-codegen/src/main/java/software/amazon/smithy/typescript/codegen/integration/AddHttp2Dependency.java b/smithy-typescript-codegen/src/main/java/software/amazon/smithy/typescript/codegen/integration/AddHttp2Dependency.java new file mode 100644 index 00000000000..af3d0a80158 --- /dev/null +++ b/smithy-typescript-codegen/src/main/java/software/amazon/smithy/typescript/codegen/integration/AddHttp2Dependency.java @@ -0,0 +1,103 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +package software.amazon.smithy.typescript.codegen.integration; + +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.function.Consumer; +import software.amazon.smithy.codegen.core.SymbolProvider; +import software.amazon.smithy.model.Model; +import software.amazon.smithy.model.knowledge.ServiceIndex; +import software.amazon.smithy.model.node.ArrayNode; +import software.amazon.smithy.model.node.Node; +import software.amazon.smithy.model.shapes.ServiceShape; +import software.amazon.smithy.model.shapes.ShapeId; +import software.amazon.smithy.model.traits.Trait; +import software.amazon.smithy.typescript.codegen.LanguageTarget; +import software.amazon.smithy.typescript.codegen.TypeScriptDependency; +import software.amazon.smithy.typescript.codegen.TypeScriptSettings; +import software.amazon.smithy.typescript.codegen.TypeScriptWriter; +import software.amazon.smithy.utils.MapUtils; +import software.amazon.smithy.utils.SmithyInternalApi; + +/** + * Configures the generated client to use NodeHttp2Handler when the service's + * protocol trait specifies eventStreamHttp containing "h2". + * + *

This mirrors the behavior of AddHttp2Dependency in smithy-aws-typescript-codegen + * but operates on any protocol trait that has an eventStreamHttp property, + * not just AWS protocol traits. + */ +@SmithyInternalApi +public final class AddHttp2Dependency implements TypeScriptIntegration { + + @Override + public List runAfter() { + return List.of(new AddEventStreamDependency().name()); + } + + @Override + public Map> getRuntimeConfigWriters( + TypeScriptSettings settings, + Model model, + SymbolProvider symbolProvider, + LanguageTarget target + ) { + ServiceShape service = settings.getService(model); + if (!requiresHttp2ForEventStreams(model, service)) { + return Collections.emptyMap(); + } + switch (target) { + case NODE: + return MapUtils.of("requestHandler", writer -> { + writer.addImport( + "NodeHttp2Handler", + "RequestHandler", + TypeScriptDependency.AWS_SDK_NODE_HTTP_HANDLER + ); + writer.openBlock( + "RequestHandler.create(config?.requestHandler ?? (async () => ({", + "})))", + () -> { + writer.write("...await defaultConfigProvider(),"); + writer.write("disableConcurrentStreams: true"); + } + ); + }); + default: + return Collections.emptyMap(); + } + } + + /** + * Checks whether the service's protocol trait has eventStreamHttp containing "h2". + */ + private static boolean requiresHttp2ForEventStreams(Model model, ServiceShape service) { + ServiceIndex serviceIndex = ServiceIndex.of(model); + for (ShapeId protocolId : serviceIndex.getProtocols(service).keySet()) { + Trait protocolTrait = service.findTrait(protocolId).orElse(null); + if (protocolTrait == null) { + continue; + } + Node traitNode = protocolTrait.toNode(); + if (!traitNode.isObjectNode()) { + continue; + } + ArrayNode eventStreamHttp = traitNode.expectObjectNode() + .getArrayMember("eventStreamHttp") + .orElse(null); + if (eventStreamHttp == null) { + continue; + } + for (Node entry : eventStreamHttp) { + if (entry.isStringNode() && entry.expectStringNode().getValue().equals("h2")) { + return true; + } + } + } + return false; + } +} diff --git a/smithy-typescript-codegen/src/main/resources/META-INF/services/software.amazon.smithy.typescript.codegen.integration.TypeScriptIntegration b/smithy-typescript-codegen/src/main/resources/META-INF/services/software.amazon.smithy.typescript.codegen.integration.TypeScriptIntegration index ba1af5a1af0..ecc631aceee 100644 --- a/smithy-typescript-codegen/src/main/resources/META-INF/services/software.amazon.smithy.typescript.codegen.integration.TypeScriptIntegration +++ b/smithy-typescript-codegen/src/main/resources/META-INF/services/software.amazon.smithy.typescript.codegen.integration.TypeScriptIntegration @@ -17,3 +17,4 @@ software.amazon.smithy.typescript.codegen.integration.AddSdkStreamMixinDependenc software.amazon.smithy.typescript.codegen.integration.DefaultReadmeGenerator software.amazon.smithy.typescript.codegen.integration.AddCompressionDependency software.amazon.smithy.typescript.codegen.protocols.AddProtocols +software.amazon.smithy.typescript.codegen.integration.AddHttp2Dependency diff --git a/smithy-typescript-protocol-test-codegen/model/my-local-model/my-local-model.smithy b/smithy-typescript-protocol-test-codegen/model/my-local-model/my-local-model.smithy index d114a6443a3..14db8ecc125 100644 --- a/smithy-typescript-protocol-test-codegen/model/my-local-model/my-local-model.smithy +++ b/smithy-typescript-protocol-test-codegen/model/my-local-model/my-local-model.smithy @@ -12,7 +12,10 @@ use smithy.test#httpRequestTests use smithy.test#httpResponseTests use smithy.waiters#waitable -@rpcv2Cbor +@rpcv2Cbor( + http: ["h2", "http/1.1"] + eventStreamHttp: ["h2"] +) @documentation("xyz interfaces") @httpApiKeyAuth(name: "X-Api-Key", in: "header") @clientContextParams( @@ -134,6 +137,8 @@ service XYZService { operations: [ GetNumbers TradeEventStream + PublishEvents + SubscribeToEvents camelCaseOperation HttpLabelCommand HostPrefixOperation @@ -337,10 +342,18 @@ operation TradeEventStream { } structure TradeEventStreamRequest { + @httpHeader("x-session-id") + sessionId: String + + @httpPayload eventStream: TradeEvents } structure TradeEventStreamResponse { + @httpHeader("x-session-id") + sessionId: String + + @httpPayload eventStream: TradeEvents } @@ -348,7 +361,7 @@ structure TradeEventStreamResponse { union TradeEvents { alpha: Alpha beta: Unit - gamma: Unit + gamma: Gamma delta: DifferentShapeName } @@ -357,6 +370,19 @@ structure Alpha { timestamp: Timestamp } +structure Gamma { + @eventHeader + sequenceNumber: Integer + + @eventPayload + payload: GammaPayload +} + +structure GammaPayload { + message: String + values: IntegerList +} + // this tests that the event stream member associated with it // generates using :event-type: delta rather than :event-type: DifferentShapeName. structure DifferentShapeName { @@ -364,6 +390,82 @@ structure DifferentShapeName { number: Integer } +/// Input-only event stream: client sends events, server responds with a summary. +@http(method: "POST", uri: "/publish-events", code: 200) +operation PublishEvents { + input: PublishEventsRequest + output: PublishEventsResponse +} + +structure PublishEventsRequest { + @httpHeader("x-channel") + channel: String + + @httpPayload + events: PublishEventStream +} + +structure PublishEventsResponse { + @httpHeader("x-event-count") + eventCount: Integer + + message: String +} + +@streaming +union PublishEventStream { + log: LogEvent + metric: MetricEvent +} + +structure LogEvent { + level: String + message: String +} + +structure MetricEvent { + name: String + value: Double +} + +/// Output-only event stream: client sends a subscription request, server streams events. +@http(method: "POST", uri: "/subscribe-to-events", code: 200) +operation SubscribeToEvents { + input: SubscribeToEventsRequest + output: SubscribeToEventsResponse +} + +structure SubscribeToEventsRequest { + @httpHeader("x-channel") + channel: String + + @httpHeader("x-max-events") + maxEvents: Integer +} + +structure SubscribeToEventsResponse { + @httpHeader("x-subscription-id") + subscriptionId: String + + @httpPayload + events: SubscribeEventStream +} + +@streaming +union SubscribeEventStream { + notification: NotificationEvent + heartbeat: HeartbeatEvent +} + +structure NotificationEvent { + topic: String + payload: String +} + +structure HeartbeatEvent { + timestamp: Timestamp +} + @rpcv2Cbor @documentation("a second service in the same model, unused.") service UnusedService {