Skip to content

Commit 9ba012d

Browse files
feat(event-stream): implement event stream sra (#4695)
1 parent e91addb commit 9ba012d

25 files changed

+629
-178
lines changed

packages/eventstream-codec/src/EventStreamCodec.ts

+51-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,12 @@
11
import { Crc32 } from "@aws-crypto/crc32";
2-
import { Message, MessageHeaders } from "@aws-sdk/types";
2+
import {
3+
AvailableMessage,
4+
AvailableMessages,
5+
Message,
6+
MessageDecoder,
7+
MessageEncoder,
8+
MessageHeaders,
9+
} from "@aws-sdk/types";
310
import { Decoder, Encoder } from "@aws-sdk/types";
411

512
import { HeaderMarshaller } from "./HeaderMarshaller";
@@ -9,11 +16,53 @@ import { splitMessage } from "./splitMessage";
916
* A Codec that can convert binary-packed event stream messages into
1017
* JavaScript objects and back again into their binary format.
1118
*/
12-
export class EventStreamCodec {
19+
export class EventStreamCodec implements MessageEncoder, MessageDecoder {
1320
private readonly headerMarshaller: HeaderMarshaller;
21+
private messageBuffer: Message[];
22+
23+
private isEndOfStream: boolean;
1424

1525
constructor(toUtf8: Encoder, fromUtf8: Decoder) {
1626
this.headerMarshaller = new HeaderMarshaller(toUtf8, fromUtf8);
27+
this.messageBuffer = [];
28+
this.isEndOfStream = false;
29+
}
30+
31+
feed(message: ArrayBufferView): void {
32+
this.messageBuffer.push(this.decode(message));
33+
}
34+
35+
endOfStream(): void {
36+
this.isEndOfStream = true;
37+
}
38+
39+
getMessage(): AvailableMessage {
40+
const message = this.messageBuffer.pop();
41+
const isEndOfStream = this.isEndOfStream;
42+
43+
return {
44+
getMessage(): Message | undefined {
45+
return message;
46+
},
47+
isEndOfStream(): boolean {
48+
return isEndOfStream;
49+
},
50+
};
51+
}
52+
53+
getAvailableMessages(): AvailableMessages {
54+
const messages = this.messageBuffer;
55+
this.messageBuffer = [];
56+
const isEndOfStream = this.isEndOfStream;
57+
58+
return {
59+
getMessages(): Message[] {
60+
return messages;
61+
},
62+
isEndOfStream(): boolean {
63+
return isEndOfStream;
64+
},
65+
};
1766
}
1867

1968
/**
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import { Message } from "@aws-sdk/types";
2+
3+
import { MessageDecoderStream } from "./MessageDecoderStream";
4+
5+
describe("MessageDecoderStream", () => {
6+
it("returns decoded messages", async () => {
7+
const message1 = {
8+
headers: {},
9+
body: new Uint8Array(1),
10+
};
11+
12+
const message2 = {
13+
headers: {},
14+
body: new Uint8Array(2),
15+
};
16+
17+
const messageDecoderMock = {
18+
decode: jest.fn().mockReturnValueOnce(message1).mockReturnValueOnce(message2),
19+
feed: jest.fn(),
20+
endOfStream: jest.fn(),
21+
getMessage: jest.fn(),
22+
getAvailableMessages: jest.fn(),
23+
};
24+
25+
const inputStream = async function* () {
26+
yield new Uint8Array(0);
27+
yield new Uint8Array(1);
28+
};
29+
30+
const messageDecoderStream = new MessageDecoderStream({
31+
decoder: messageDecoderMock,
32+
inputStream: inputStream(),
33+
});
34+
35+
const messages: Array<Message> = [];
36+
for await (const message of messageDecoderStream) {
37+
messages.push(message);
38+
}
39+
expect(messages.length).toEqual(2);
40+
expect(messages[0]).toEqual(message1);
41+
expect(messages[1]).toEqual(message2);
42+
});
43+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import { Message, MessageDecoder } from "@aws-sdk/types";
2+
3+
/**
4+
* @internal
5+
*/
6+
export interface MessageDecoderStreamOptions {
7+
inputStream: AsyncIterable<Uint8Array>;
8+
decoder: MessageDecoder;
9+
}
10+
11+
/**
12+
* @internal
13+
*/
14+
export class MessageDecoderStream implements AsyncIterable<Message> {
15+
constructor(private readonly options: MessageDecoderStreamOptions) {}
16+
17+
[Symbol.asyncIterator](): AsyncIterator<Message> {
18+
return this.asyncIterator();
19+
}
20+
21+
private async *asyncIterator() {
22+
for await (const bytes of this.options.inputStream) {
23+
const decoded = this.options.decoder.decode(bytes);
24+
yield decoded;
25+
}
26+
}
27+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
import { MessageEncoderStream } from "./MessageEncoderStream";
2+
3+
describe("MessageEncoderStream", () => {
4+
it("returns encoded stream with end frame", async () => {
5+
const message1 = {
6+
headers: {},
7+
body: new Uint8Array(1),
8+
};
9+
10+
const message2 = {
11+
headers: {},
12+
body: new Uint8Array(2),
13+
};
14+
15+
const messageEncoderMock = {
16+
encode: jest.fn().mockReturnValueOnce(new Uint8Array(1)).mockReturnValueOnce(new Uint8Array(2)),
17+
};
18+
19+
const inputStream = async function* () {
20+
yield message1;
21+
yield message2;
22+
};
23+
24+
const messageEncoderStream = new MessageEncoderStream({
25+
encoder: messageEncoderMock,
26+
messageStream: inputStream(),
27+
includeEndFrame: true,
28+
});
29+
30+
const messages: Array<Uint8Array> = [];
31+
for await (const encoded of messageEncoderStream) {
32+
messages.push(encoded);
33+
}
34+
expect(messages.length).toEqual(3);
35+
expect(messages[0]).toEqual(new Uint8Array(1));
36+
expect(messages[1]).toEqual(new Uint8Array(2));
37+
expect(messages[2]).toEqual(new Uint8Array(0));
38+
});
39+
40+
it("returns encoded stream without end frame", async () => {
41+
const message1 = {
42+
headers: {},
43+
body: new Uint8Array(1),
44+
};
45+
46+
const message2 = {
47+
headers: {},
48+
body: new Uint8Array(2),
49+
};
50+
51+
const messageEncoderMock = {
52+
encode: jest.fn().mockReturnValueOnce(new Uint8Array(1)).mockReturnValueOnce(new Uint8Array(2)),
53+
};
54+
55+
const inputStream = async function* () {
56+
yield message1;
57+
yield message2;
58+
};
59+
60+
const messageEncoderStream = new MessageEncoderStream({
61+
encoder: messageEncoderMock,
62+
messageStream: inputStream(),
63+
});
64+
65+
const messages: Array<Uint8Array> = [];
66+
for await (const encoded of messageEncoderStream) {
67+
messages.push(encoded);
68+
}
69+
expect(messages.length).toEqual(2);
70+
expect(messages[0]).toEqual(new Uint8Array(1));
71+
expect(messages[1]).toEqual(new Uint8Array(2));
72+
});
73+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import { Message, MessageEncoder } from "@aws-sdk/types";
2+
3+
/**
4+
* @internal
5+
*/
6+
export interface MessageEncoderStreamOptions {
7+
messageStream: AsyncIterable<Message>;
8+
encoder: MessageEncoder;
9+
includeEndFrame?: Boolean;
10+
}
11+
12+
/**
13+
* @internal
14+
*/
15+
export class MessageEncoderStream implements AsyncIterable<Uint8Array> {
16+
constructor(private readonly options: MessageEncoderStreamOptions) {}
17+
18+
[Symbol.asyncIterator](): AsyncIterator<Uint8Array> {
19+
return this.asyncIterator();
20+
}
21+
22+
private async *asyncIterator() {
23+
for await (const msg of this.options.messageStream) {
24+
const encoded = this.options.encoder.encode(msg);
25+
yield encoded;
26+
}
27+
if (this.options.includeEndFrame) {
28+
yield new Uint8Array(0);
29+
}
30+
}
31+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import { SmithyMessageDecoderStream } from "./SmithyMessageDecoderStream";
2+
3+
describe("SmithyMessageDecoderStream", () => {
4+
it("returns decoded stream", async () => {
5+
const message1 = {
6+
headers: {},
7+
body: new Uint8Array(1),
8+
};
9+
10+
const message2 = {
11+
headers: {},
12+
body: new Uint8Array(2),
13+
};
14+
15+
const deserializer = jest
16+
.fn()
17+
.mockReturnValueOnce(Promise.resolve("first"))
18+
.mockReturnValueOnce(Promise.resolve("second"));
19+
20+
const inputStream = async function* () {
21+
yield message1;
22+
yield message2;
23+
};
24+
25+
const stream = new SmithyMessageDecoderStream<String>({
26+
messageStream: inputStream(),
27+
deserializer: deserializer,
28+
});
29+
30+
const messages: Array<String> = [];
31+
for await (const str of stream) {
32+
messages.push(str);
33+
}
34+
expect(messages.length).toEqual(2);
35+
expect(messages[0]).toEqual("first");
36+
expect(messages[1]).toEqual("second");
37+
});
38+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import { Message } from "@aws-sdk/types";
2+
3+
/**
4+
* @internal
5+
*/
6+
export interface SmithyMessageDecoderStreamOptions<T> {
7+
readonly messageStream: AsyncIterable<Message>;
8+
readonly deserializer: (input: Message) => Promise<T | undefined>;
9+
}
10+
11+
/**
12+
* @internal
13+
*/
14+
export class SmithyMessageDecoderStream<T> implements AsyncIterable<T> {
15+
constructor(private readonly options: SmithyMessageDecoderStreamOptions<T>) {}
16+
17+
[Symbol.asyncIterator](): AsyncIterator<T> {
18+
return this.asyncIterator();
19+
}
20+
21+
private async *asyncIterator() {
22+
for await (const message of this.options.messageStream) {
23+
const deserialized = await this.options.deserializer(message);
24+
if (deserialized === undefined) continue;
25+
yield deserialized;
26+
}
27+
}
28+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import { Message } from "@aws-sdk/types";
2+
3+
import { SmithyMessageEncoderStream } from "./SmithyMessageEncoderStream";
4+
5+
describe("SmithyMessageEncoderStream", () => {
6+
it("returns encoded stream", async () => {
7+
const message1 = {
8+
headers: {},
9+
body: new Uint8Array(1),
10+
};
11+
12+
const message2 = {
13+
headers: {},
14+
body: new Uint8Array(2),
15+
};
16+
17+
const serializer = jest.fn().mockReturnValueOnce(message1).mockReturnValueOnce(message2);
18+
19+
const inputStream = async function* () {
20+
yield "first";
21+
yield "second";
22+
};
23+
24+
const stream = new SmithyMessageEncoderStream<String>({
25+
inputStream: inputStream(),
26+
serializer: serializer,
27+
});
28+
29+
const messages: Array<Message> = [];
30+
for await (const str of stream) {
31+
messages.push(str);
32+
}
33+
expect(messages.length).toEqual(2);
34+
expect(messages[0]).toEqual(message1);
35+
expect(messages[1]).toEqual(message2);
36+
});
37+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import { Message } from "@aws-sdk/types";
2+
3+
/**
4+
* @internal
5+
*/
6+
export interface SmithyMessageEncoderStreamOptions<T> {
7+
inputStream: AsyncIterable<T>;
8+
serializer: (event: T) => Message;
9+
}
10+
11+
/**
12+
* @internal
13+
*/
14+
export class SmithyMessageEncoderStream<T> implements AsyncIterable<Message> {
15+
constructor(private readonly options: SmithyMessageEncoderStreamOptions<T>) {}
16+
17+
[Symbol.asyncIterator](): AsyncIterator<Message> {
18+
return this.asyncIterator();
19+
}
20+
21+
private async *asyncIterator() {
22+
for await (const chunk of this.options.inputStream) {
23+
const payloadBuf = this.options.serializer(chunk);
24+
yield payloadBuf;
25+
}
26+
}
27+
}
+5
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,8 @@
11
export * from "./EventStreamCodec";
2+
export * from "./HeaderMarshaller";
23
export * from "./Int64";
34
export * from "./Message";
5+
export * from "./MessageDecoderStream";
6+
export * from "./MessageEncoderStream";
7+
export * from "./SmithyMessageDecoderStream";
8+
export * from "./SmithyMessageEncoderStream";

0 commit comments

Comments
 (0)