@xube/kit-aws 0.2.14 → 0.2.16
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"put.d.ts","sourceRoot":"","sources":["../../../src/resources/kinesis/put.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,aAAa,EAEb,sBAAsB,EACvB,MAAM,yBAAyB,CAAC;
|
|
1
|
+
{"version":3,"file":"put.d.ts","sourceRoot":"","sources":["../../../src/resources/kinesis/put.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,aAAa,EAEb,sBAAsB,EACvB,MAAM,yBAAyB,CAAC;AAQjC,eAAO,MAAM,UAAU,eACT,MAAM,WACT,sBAAsB,EAAE,WACxB,aAAa,kBA0BvB,CAAC"}
|
|
@@ -2,11 +2,28 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.putRecords = void 0;
|
|
4
4
|
const client_kinesis_1 = require("@aws-sdk/client-kinesis");
|
|
5
|
+
const MAX_RECORDS_PER_CALL = 500;
|
|
6
|
+
const MAX_BATCH_PAYLOAD_BYTES = 4_500_000;
|
|
7
|
+
const recordSize = (r) => (r.Data?.byteLength ?? 0) + (r.PartitionKey?.length ?? 0);
|
|
5
8
|
const putRecords = async (streamName, records, kinesis) => {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
9
|
+
let batch = [];
|
|
10
|
+
let batchBytes = 0;
|
|
11
|
+
const flush = async () => {
|
|
12
|
+
if (batch.length === 0)
|
|
13
|
+
return;
|
|
14
|
+
await kinesis.send(new client_kinesis_1.PutRecordsCommand({ StreamName: streamName, Records: batch }));
|
|
15
|
+
batch = [];
|
|
16
|
+
batchBytes = 0;
|
|
17
|
+
};
|
|
18
|
+
for (const record of records) {
|
|
19
|
+
const size = recordSize(record);
|
|
20
|
+
if (batch.length >= MAX_RECORDS_PER_CALL ||
|
|
21
|
+
(batch.length > 0 && batchBytes + size > MAX_BATCH_PAYLOAD_BYTES)) {
|
|
22
|
+
await flush();
|
|
23
|
+
}
|
|
24
|
+
batch.push(record);
|
|
25
|
+
batchBytes += size;
|
|
26
|
+
}
|
|
27
|
+
await flush();
|
|
11
28
|
};
|
|
12
29
|
exports.putRecords = putRecords;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xube/kit-aws",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.16",
|
|
4
4
|
"description": "",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"scripts": {
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
"homepage": "https://github.com/XubeLtd/dev-kit#readme",
|
|
19
19
|
"devDependencies": {
|
|
20
20
|
"@types/aws-lambda": "^8.10.119",
|
|
21
|
-
"@xube/kit-build": "^0.2.
|
|
21
|
+
"@xube/kit-build": "^0.2.16"
|
|
22
22
|
},
|
|
23
23
|
"dependencies": {
|
|
24
24
|
"@aws-sdk/client-athena": "^3.656.0",
|
|
@@ -29,10 +29,10 @@
|
|
|
29
29
|
"@aws-sdk/client-ssm": "^3.654.0",
|
|
30
30
|
"@aws-sdk/lib-dynamodb": "^3.656.0",
|
|
31
31
|
"@aws-sdk/util-dynamodb": "^3.656.0",
|
|
32
|
-
"@xube/kit-aws-schema": "^0.2.
|
|
33
|
-
"@xube/kit-log": "^0.2.
|
|
34
|
-
"@xube/kit-request": "^0.2.
|
|
35
|
-
"@xube/kit-schema": "^0.2.
|
|
32
|
+
"@xube/kit-aws-schema": "^0.2.16",
|
|
33
|
+
"@xube/kit-log": "^0.2.16",
|
|
34
|
+
"@xube/kit-request": "^0.2.16",
|
|
35
|
+
"@xube/kit-schema": "^0.2.16",
|
|
36
36
|
"zod": "^4.4.3"
|
|
37
37
|
}
|
|
38
38
|
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import {
|
|
2
|
+
KinesisClient,
|
|
3
|
+
PutRecordsCommand,
|
|
4
|
+
PutRecordsRequestEntry,
|
|
5
|
+
} from "@aws-sdk/client-kinesis";
|
|
6
|
+
import { putRecords } from "./put";
|
|
7
|
+
|
|
8
|
+
const makeRecord = (size: number, key = "p"): PutRecordsRequestEntry => ({
|
|
9
|
+
Data: Buffer.alloc(size),
|
|
10
|
+
PartitionKey: key,
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
const captureCalls = () => {
|
|
14
|
+
const calls: PutRecordsRequestEntry[][] = [];
|
|
15
|
+
const send = jest.fn(async (cmd: PutRecordsCommand) => {
|
|
16
|
+
calls.push(cmd.input.Records ?? []);
|
|
17
|
+
return {};
|
|
18
|
+
});
|
|
19
|
+
const client = { send } as unknown as KinesisClient;
|
|
20
|
+
return { calls, send, client };
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
describe("putRecords (batched)", () => {
|
|
24
|
+
it("sends a single call when records fit in one batch", async () => {
|
|
25
|
+
const { calls, client } = captureCalls();
|
|
26
|
+
const records = Array.from({ length: 100 }, () => makeRecord(100));
|
|
27
|
+
await putRecords("stream", records, client);
|
|
28
|
+
expect(calls).toHaveLength(1);
|
|
29
|
+
expect(calls[0]).toHaveLength(100);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it("splits when record count exceeds 500", async () => {
|
|
33
|
+
const { calls, client } = captureCalls();
|
|
34
|
+
const records = Array.from({ length: 1200 }, () => makeRecord(100));
|
|
35
|
+
await putRecords("stream", records, client);
|
|
36
|
+
expect(calls.map((c) => c.length)).toEqual([500, 500, 200]);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it("splits exactly at the 500 boundary", async () => {
|
|
40
|
+
const { calls, client } = captureCalls();
|
|
41
|
+
const records = Array.from({ length: 500 }, () => makeRecord(100));
|
|
42
|
+
await putRecords("stream", records, client);
|
|
43
|
+
expect(calls).toHaveLength(1);
|
|
44
|
+
expect(calls[0]).toHaveLength(500);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it("splits when cumulative payload approaches 4.5 MB", async () => {
|
|
48
|
+
const { calls, client } = captureCalls();
|
|
49
|
+
const records = Array.from({ length: 100 }, () => makeRecord(100_000));
|
|
50
|
+
await putRecords("stream", records, client);
|
|
51
|
+
expect(calls.length).toBeGreaterThan(1);
|
|
52
|
+
for (const c of calls) {
|
|
53
|
+
const bytes = c.reduce(
|
|
54
|
+
(sum, r) => sum + (r.Data?.byteLength ?? 0) + (r.PartitionKey?.length ?? 0),
|
|
55
|
+
0,
|
|
56
|
+
);
|
|
57
|
+
expect(bytes).toBeLessThanOrEqual(4_500_000);
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it("does nothing when records array is empty", async () => {
|
|
62
|
+
const { send, client } = captureCalls();
|
|
63
|
+
await putRecords("stream", [], client);
|
|
64
|
+
expect(send).not.toHaveBeenCalled();
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it("does not orphan oversized first record into an empty flush", async () => {
|
|
68
|
+
const { calls, client } = captureCalls();
|
|
69
|
+
const records = [makeRecord(4_400_000), makeRecord(100), makeRecord(100)];
|
|
70
|
+
await putRecords("stream", records, client);
|
|
71
|
+
expect(calls.length).toBeGreaterThan(0);
|
|
72
|
+
const allRecords = calls.flat();
|
|
73
|
+
expect(allRecords).toHaveLength(3);
|
|
74
|
+
});
|
|
75
|
+
});
|
|
@@ -4,15 +4,39 @@ import {
|
|
|
4
4
|
PutRecordsRequestEntry,
|
|
5
5
|
} from "@aws-sdk/client-kinesis";
|
|
6
6
|
|
|
7
|
+
const MAX_RECORDS_PER_CALL = 500;
|
|
8
|
+
const MAX_BATCH_PAYLOAD_BYTES = 4_500_000;
|
|
9
|
+
|
|
10
|
+
const recordSize = (r: PutRecordsRequestEntry): number =>
|
|
11
|
+
(r.Data?.byteLength ?? 0) + (r.PartitionKey?.length ?? 0);
|
|
12
|
+
|
|
7
13
|
export const putRecords = async (
|
|
8
14
|
streamName: string,
|
|
9
15
|
records: PutRecordsRequestEntry[],
|
|
10
16
|
kinesis: KinesisClient
|
|
11
17
|
) => {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
18
|
+
let batch: PutRecordsRequestEntry[] = [];
|
|
19
|
+
let batchBytes = 0;
|
|
20
|
+
|
|
21
|
+
const flush = async () => {
|
|
22
|
+
if (batch.length === 0) return;
|
|
23
|
+
await kinesis.send(
|
|
24
|
+
new PutRecordsCommand({ StreamName: streamName, Records: batch })
|
|
25
|
+
);
|
|
26
|
+
batch = [];
|
|
27
|
+
batchBytes = 0;
|
|
28
|
+
};
|
|
16
29
|
|
|
17
|
-
|
|
30
|
+
for (const record of records) {
|
|
31
|
+
const size = recordSize(record);
|
|
32
|
+
if (
|
|
33
|
+
batch.length >= MAX_RECORDS_PER_CALL ||
|
|
34
|
+
(batch.length > 0 && batchBytes + size > MAX_BATCH_PAYLOAD_BYTES)
|
|
35
|
+
) {
|
|
36
|
+
await flush();
|
|
37
|
+
}
|
|
38
|
+
batch.push(record);
|
|
39
|
+
batchBytes += size;
|
|
40
|
+
}
|
|
41
|
+
await flush();
|
|
18
42
|
};
|