@mshick/dyno 0.1.0

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.
@@ -0,0 +1,225 @@
1
+ import createDebug from 'debug';
2
+ import { parseResponse } from "../responses.js";
3
+ import { getItemSize } from "../util.js";
4
+ import { SendAllBatch } from "./send-all.js";
5
+ import { SendCompletelyBatch } from "./send-completely.js";
6
+ const log = createDebug('dyno-requests');
7
+ function _batchGetItemRequests(params) {
8
+ const requestItems = params.RequestItems;
9
+ if (!requestItems) {
10
+ throw new Error('RequestItems is required');
11
+ }
12
+ const batchedItems = Object.keys(requestItems).reduce((paramSet, tableName) => {
13
+ const keys = requestItems[tableName]?.Keys;
14
+ if (!keys) {
15
+ return paramSet;
16
+ }
17
+ const toMake = [...keys];
18
+ return chop(toMake);
19
+ function chop(requestsToMake) {
20
+ // request set that we're building — paramSet is seeded with one entry
21
+ // and only grows, so paramSet[paramSet.length - 1] is always defined.
22
+ const lastParams = paramSet[paramSet.length - 1];
23
+ if (!lastParams) {
24
+ return paramSet;
25
+ }
26
+ const requests = lastParams.RequestItems ?? {};
27
+ const requestsTable = requests[tableName] ?? { Keys: [] };
28
+ requests[tableName] = requestsTable;
29
+ // count existing requests in the current params
30
+ const count = Object.keys(requests).reduce((count, tableName) => {
31
+ return count + (requests[tableName]?.Keys?.length ?? 0);
32
+ }, 0);
33
+ // gather more from the requested params
34
+ const more = requestsToMake.splice(0, 100 - count);
35
+ // add them to the request set
36
+ requestsTable.Keys = (requestsTable.Keys ?? []).concat(more);
37
+ // if there are no requests left, return the modified paramSet
38
+ if (!requestsToMake.length) {
39
+ return paramSet;
40
+ }
41
+ // otherwise start a new request set
42
+ paramSet.push({
43
+ RequestItems: {},
44
+ ReturnConsumedCapacity: params.ReturnConsumedCapacity,
45
+ });
46
+ return chop(requestsToMake);
47
+ }
48
+ }, [
49
+ {
50
+ RequestItems: {},
51
+ ReturnConsumedCapacity: params.ReturnConsumedCapacity,
52
+ },
53
+ ]);
54
+ return batchedItems;
55
+ }
56
+ function _batchWriteItemRequests(params, { maxLength = 25, maxSize = 16 * 1024 * 1024 } = {}) {
57
+ const requestItems = params.RequestItems;
58
+ if (!requestItems) {
59
+ throw new Error('RequestItems is required');
60
+ }
61
+ const batchedItems = Object.keys(requestItems).reduce((paramSet, tableName) => {
62
+ const reqs = requestItems[tableName];
63
+ if (!reqs) {
64
+ return paramSet;
65
+ }
66
+ return chop([...reqs]);
67
+ function chop(requestsToMake) {
68
+ // paramSet is seeded with one entry and only grows.
69
+ const lastParams = paramSet[paramSet.length - 1];
70
+ if (!lastParams) {
71
+ return paramSet;
72
+ }
73
+ const requests = lastParams.RequestItems ?? {};
74
+ const requestsTable = requests[tableName] ?? [];
75
+ requests[tableName] = requestsTable;
76
+ // count existing requests in the current params
77
+ const count = Object.values(requests).reduce((c, r) => {
78
+ return c + r.length;
79
+ }, 0);
80
+ // find existing requests size
81
+ let size = Object.values(requests).reduce((s, request) => {
82
+ return (s +
83
+ request.reduce((s, r) => {
84
+ return s + (r.PutRequest?.Item ? getItemSize(r.PutRequest.Item) : 0);
85
+ }, 0));
86
+ }, 0);
87
+ // Add requests one by one until it would put us over the size limit
88
+ const startingCount = count;
89
+ for (let i = 0; i < maxLength - count; i++) {
90
+ const next = requestsToMake[0];
91
+ if (!next) {
92
+ return paramSet;
93
+ }
94
+ const nextSize = next.PutRequest?.Item ? getItemSize(next.PutRequest.Item) : 0;
95
+ if (size + nextSize > maxSize) {
96
+ break;
97
+ }
98
+ size += nextSize;
99
+ const itemToPush = requestsToMake.shift();
100
+ if (itemToPush) {
101
+ requestsTable.push(itemToPush);
102
+ }
103
+ }
104
+ // if there are no requests left, return the modified paramSet
105
+ if (!requestsToMake.length) {
106
+ return paramSet;
107
+ }
108
+ // If the next item couldn't fit in an empty params set, it's larger than
109
+ // maxSize on its own — recursing would loop forever.
110
+ if (startingCount === 0 && requestsTable.length === 0) {
111
+ const next = requestsToMake[0];
112
+ const nextSize = next?.PutRequest?.Item ? getItemSize(next.PutRequest.Item) : 0;
113
+ throw new Error(`Item exceeds maxSize: item is ~${nextSize} bytes, limit is ${maxSize}`);
114
+ }
115
+ // otherwise start a new request set
116
+ paramSet.push({
117
+ RequestItems: {},
118
+ ReturnConsumedCapacity: params.ReturnConsumedCapacity,
119
+ });
120
+ return chop(requestsToMake);
121
+ }
122
+ }, [
123
+ {
124
+ RequestItems: {},
125
+ ReturnConsumedCapacity: params.ReturnConsumedCapacity,
126
+ },
127
+ ]);
128
+ return batchedItems;
129
+ }
130
+ export function batchWriteItemRequests(client, params, options = {}) {
131
+ const l = log.extend('batchWriteItemRequests');
132
+ l(params, options);
133
+ const requestFactory = (params) => ({
134
+ params,
135
+ async send() {
136
+ try {
137
+ const data = await client.batchWrite(params, options);
138
+ return { data };
139
+ }
140
+ catch (error) {
141
+ return { error };
142
+ }
143
+ },
144
+ });
145
+ const items = _batchWriteItemRequests(params, options);
146
+ return new SendAllBatch(requestFactory, items, options);
147
+ }
148
+ export function batchGetItemRequests(client, params, options = {}) {
149
+ const l = log.extend('batchGetItemRequests');
150
+ l(params, options);
151
+ const requestFactory = (params) => ({
152
+ params,
153
+ async send() {
154
+ try {
155
+ const data = parseResponse(await client.batchGet(params, options), options);
156
+ return { data };
157
+ }
158
+ catch (error) {
159
+ return { error };
160
+ }
161
+ },
162
+ });
163
+ const items = _batchGetItemRequests(params);
164
+ return new SendAllBatch(requestFactory, items, options);
165
+ }
166
+ export function batchWriteAll(client, params, options = {}) {
167
+ const l = log.extend('batchWriteAll');
168
+ l(params, options);
169
+ const requestFactory = (params) => ({
170
+ params,
171
+ async send() {
172
+ try {
173
+ const data = await client.batchWrite(params, options);
174
+ return { data };
175
+ }
176
+ catch (error) {
177
+ return { error };
178
+ }
179
+ },
180
+ });
181
+ const items = _batchWriteItemRequests(params);
182
+ return new SendCompletelyBatch(requestFactory, items, options);
183
+ }
184
+ export function batchPutAll(client, tableName, items, options = {}) {
185
+ const params = {
186
+ RequestItems: {
187
+ [tableName]: items.map((item) => ({
188
+ PutRequest: {
189
+ Item: item,
190
+ },
191
+ })),
192
+ },
193
+ };
194
+ return batchWriteAll(client, params, options);
195
+ }
196
+ export function batchDeleteAll(client, tableName, items, options = {}) {
197
+ const params = {
198
+ RequestItems: {
199
+ [tableName]: items.map((item) => ({
200
+ DeleteRequest: {
201
+ Key: item,
202
+ },
203
+ })),
204
+ },
205
+ };
206
+ return batchWriteAll(client, params, options);
207
+ }
208
+ export function batchGetAll(client, params, options = {}) {
209
+ const l = log.extend('batchGetAll');
210
+ l(params, options);
211
+ const requestFactory = (params) => ({
212
+ params,
213
+ async send() {
214
+ try {
215
+ const data = parseResponse(await client.batchGet(params, options), options);
216
+ return { data };
217
+ }
218
+ catch (error) {
219
+ return { error };
220
+ }
221
+ },
222
+ });
223
+ const items = _batchGetItemRequests(params);
224
+ return new SendCompletelyBatch(requestFactory, items, options);
225
+ }
@@ -0,0 +1,43 @@
1
+ import type { BatchGetCommandInput, BatchGetCommandOutput, BatchWriteCommandOutput } from '@aws-sdk/lib-dynamodb';
2
+ import type { BatchCommandInput, BatchCommandOutput, Fetcher, RequestFactory } from './types.ts';
3
+ type SendAllCallback<I extends BatchCommandInput, O extends BatchCommandOutput = I extends BatchGetCommandInput ? BatchGetCommandOutput : BatchWriteCommandOutput> = (error: Array<Error | null> | undefined, data: Array<SendAllResult<O> | null>, unprocessed?: SendAllBatch<I>) => void;
4
+ type SendAllCompactCallback<I extends BatchCommandInput> = (error: Array<Error | null> | undefined, data: Array<SendAllCompactResult<I> | null>, unprocessed?: SendAllBatch<I>) => void;
5
+ export type SendAllResult<O extends BatchCommandOutput> = Omit<O, '$metadata'>;
6
+ export type SendAllGetOutput = SendAllResult<BatchGetCommandOutput>;
7
+ export type SendAllWriteOutput = SendAllResult<BatchWriteCommandOutput>;
8
+ export type SendAllCompactGetOutput = Omit<SendAllResult<BatchGetCommandOutput>, 'Responses' | 'UnprocessedKeys'> & {
9
+ Responses?: NonNullable<BatchGetCommandOutput['Responses']>[string];
10
+ UnprocessedKeys?: NonNullable<BatchGetCommandOutput['UnprocessedKeys']>[string];
11
+ };
12
+ export type SendAllCompactWriteOutput = Omit<SendAllResult<BatchWriteCommandOutput>, 'UnprocessedItems'> & {
13
+ UnprocessedItems?: NonNullable<BatchWriteCommandOutput['UnprocessedItems']>[string];
14
+ };
15
+ export type SendAllCompactResult<I extends BatchCommandInput> = I extends BatchGetCommandInput ? SendAllCompactGetOutput : SendAllCompactWriteOutput;
16
+ export type SendAllOptions = {
17
+ concurrency?: number;
18
+ };
19
+ export declare class SendAllBatch<I extends BatchCommandInput, O extends BatchCommandOutput = I extends BatchGetCommandInput ? BatchGetCommandOutput : BatchWriteCommandOutput> {
20
+ private readonly requestFactory;
21
+ private readonly options;
22
+ readonly requests: Array<Fetcher<I> | null>;
23
+ constructor(requestFactory: RequestFactory<I>, items: Array<Pick<I, 'RequestItems' | 'ReturnConsumedCapacity'> | null>, options: SendAllOptions);
24
+ load(items: Array<Pick<I, 'RequestItems' | 'ReturnConsumedCapacity'> | null>): SendAllBatch<I, I extends BatchGetCommandInput ? BatchGetCommandOutput : BatchWriteCommandOutput>;
25
+ sendAll(options: SendAllOptions & {
26
+ compact: true;
27
+ }): Promise<{
28
+ error: Array<Error | null> | undefined;
29
+ data: Array<SendAllCompactResult<I> | null>;
30
+ unprocessed?: SendAllBatch<I>;
31
+ }>;
32
+ sendAll(options?: SendAllOptions): Promise<{
33
+ error: Array<Error | null> | undefined;
34
+ data: Array<SendAllResult<O> | null>;
35
+ unprocessed?: SendAllBatch<I>;
36
+ }>;
37
+ sendAll(cb: SendAllCallback<I>): void;
38
+ sendAll(options: SendAllOptions, cb: SendAllCallback<I>): void;
39
+ sendAll(options: SendAllOptions & {
40
+ compact: true;
41
+ }, cb: SendAllCompactCallback<I>): void;
42
+ }
43
+ export {};
@@ -0,0 +1,163 @@
1
+ import createDebug from 'debug';
2
+ import fastq from 'fastq';
3
+ import { ensureError } from "../util.js";
4
+ const log = createDebug('dyno-requests');
5
+ function _sendAll(requests, load, options, callback) {
6
+ const l = log.extend('sendAll');
7
+ const { concurrency = 1 } = options;
8
+ l('requests:', requests.length, 'concurrency:', concurrency);
9
+ const worker = (request, done) => {
10
+ if (!request) {
11
+ done(null);
12
+ return;
13
+ }
14
+ void request
15
+ .send()
16
+ .then((res) => {
17
+ done(null, res);
18
+ })
19
+ .catch((err) => {
20
+ done(null, { error: ensureError(err) });
21
+ });
22
+ };
23
+ const drain = (results) => {
24
+ return () => {
25
+ l('done:results', results?.length);
26
+ const errors = [];
27
+ const data = [];
28
+ const unprocessed = [];
29
+ for (const res of results) {
30
+ errors.push(res?.error ? ensureError(res.error) : null);
31
+ data.push(res?.data ? res.data : null);
32
+ if (!res?.data) {
33
+ unprocessed.push(null);
34
+ continue;
35
+ }
36
+ if ('UnprocessedItems' in res.data &&
37
+ res.data.UnprocessedItems &&
38
+ Object.keys(res.data.UnprocessedItems).length) {
39
+ unprocessed.push({
40
+ RequestItems: res.data.UnprocessedItems,
41
+ });
42
+ }
43
+ else if ('UnprocessedKeys' in res.data &&
44
+ res.data.UnprocessedKeys &&
45
+ Object.keys(res.data.UnprocessedKeys).length) {
46
+ unprocessed.push({
47
+ RequestItems: res.data.UnprocessedKeys,
48
+ });
49
+ }
50
+ else {
51
+ unprocessed.push(null);
52
+ }
53
+ }
54
+ if (options.compact === true) {
55
+ callback(errors.filter((e) => Boolean(e)).length ? errors : undefined, compactSendAll(data), unprocessed.filter((u) => Boolean(u)).length ? load(unprocessed) : undefined);
56
+ return;
57
+ }
58
+ callback(errors.filter((e) => Boolean(e)).length ? errors : undefined, data, unprocessed.filter((u) => Boolean(u)).length ? load(unprocessed) : undefined);
59
+ };
60
+ };
61
+ const q = fastq(worker, concurrency);
62
+ const results = [];
63
+ q.drain = drain(results);
64
+ for (const req of requests) {
65
+ q.push(req, (_err, res) => {
66
+ results.push(res ?? null);
67
+ });
68
+ }
69
+ }
70
+ function hasResponses(data) {
71
+ if (!data) {
72
+ return false;
73
+ }
74
+ if ('Responses' in data) {
75
+ return true;
76
+ }
77
+ return false;
78
+ }
79
+ function hasUnprocessedKeys(data) {
80
+ if (!data) {
81
+ return false;
82
+ }
83
+ if ('UnprocessedKeys' in data) {
84
+ return true;
85
+ }
86
+ return false;
87
+ }
88
+ function hasUnprocessedItems(data) {
89
+ if (!data) {
90
+ return false;
91
+ }
92
+ if ('UnprocessedItems' in data) {
93
+ return true;
94
+ }
95
+ return false;
96
+ }
97
+ function compactSendAll(data) {
98
+ if (data.some(hasResponses)) {
99
+ for (const d of data) {
100
+ if (d) {
101
+ const responses = d.Responses;
102
+ d.Responses = responses
103
+ ? Object.values(responses)[0]
104
+ : responses;
105
+ }
106
+ }
107
+ }
108
+ if (data.some(hasUnprocessedKeys)) {
109
+ for (const d of data) {
110
+ if (d) {
111
+ const unprocessed = d.UnprocessedKeys;
112
+ d.UnprocessedKeys = unprocessed
113
+ ? Object.values(unprocessed)[0]
114
+ : unprocessed;
115
+ }
116
+ }
117
+ }
118
+ if (data.some(hasUnprocessedItems)) {
119
+ for (const d of data) {
120
+ if (d) {
121
+ const unprocessed = d.UnprocessedItems;
122
+ d.UnprocessedItems = unprocessed
123
+ ? Object.values(unprocessed)[0]
124
+ : unprocessed;
125
+ }
126
+ }
127
+ }
128
+ return data;
129
+ }
130
+ async function _sendAllAsync(requests, load, options) {
131
+ return new Promise((resolve) => {
132
+ _sendAll(requests, load, options, (error, data, unprocessed) => {
133
+ resolve({ error, data, unprocessed });
134
+ });
135
+ });
136
+ }
137
+ export class SendAllBatch {
138
+ requestFactory;
139
+ options;
140
+ requests;
141
+ constructor(requestFactory, items, options) {
142
+ this.requestFactory = requestFactory;
143
+ this.options = options;
144
+ this.requests = items.map((params) => (params ? requestFactory(params) : null));
145
+ }
146
+ load(items) {
147
+ return new SendAllBatch(this.requestFactory, items, this.options);
148
+ }
149
+ sendAll(optionsOrCb, cb) {
150
+ if (typeof optionsOrCb === 'function') {
151
+ _sendAll(this.requests, this.load.bind(this), this.options, optionsOrCb);
152
+ }
153
+ else if (typeof cb === 'function') {
154
+ _sendAll(this.requests, this.load.bind(this), { ...this.options, ...optionsOrCb }, cb);
155
+ }
156
+ else {
157
+ return _sendAllAsync(this.requests, this.load.bind(this), {
158
+ ...this.options,
159
+ ...optionsOrCb,
160
+ });
161
+ }
162
+ }
163
+ }
@@ -0,0 +1,48 @@
1
+ import type { ConsumedCapacity } from '@aws-sdk/client-dynamodb';
2
+ import type { BatchGetCommandInput, BatchGetCommandOutput, BatchWriteCommandOutput } from '@aws-sdk/lib-dynamodb';
3
+ import type { BatchCommandInput, BatchCommandOutput, Fetcher, RequestFactory } from './types.ts';
4
+ type SendCompletelyCallback<I extends BatchCommandInput, O extends BatchCommandOutput = I extends BatchGetCommandInput ? BatchGetCommandOutput : BatchWriteCommandOutput> = (error: AggregateError | undefined, data: SendCompletelyResult<O>) => void;
5
+ type SendCompletelyCompactCallback<I extends BatchCommandInput> = (error: AggregateError | undefined, data: SendCompletelyCompactResult<I>) => void;
6
+ export type SendCompletelyResult<O extends BatchCommandOutput> = Omit<O, '$metadata' | 'ConsumedCapacity'> & {
7
+ ConsumedCapacity?: ConsumedCapacity;
8
+ };
9
+ export type SendCompletelyGetOutput = SendCompletelyResult<BatchGetCommandOutput>;
10
+ export type SendCompletelyWriteOutput = SendCompletelyResult<BatchWriteCommandOutput>;
11
+ export type SendCompletelyCompactGetOutput = Omit<SendCompletelyResult<BatchGetCommandOutput>, 'Responses' | 'UnprocessedKeys'> & {
12
+ Responses?: NonNullable<BatchGetCommandOutput['Responses']>[string];
13
+ UnprocessedKeys?: NonNullable<BatchGetCommandOutput['UnprocessedKeys']>[string];
14
+ ConsumedCapacity?: ConsumedCapacity;
15
+ };
16
+ export type SendCompletelyCompactWriteOutput = Omit<SendCompletelyResult<BatchWriteCommandOutput>, 'UnprocessedItems'> & {
17
+ UnprocessedItems?: NonNullable<BatchWriteCommandOutput['UnprocessedItems']>[string];
18
+ ConsumedCapacity?: ConsumedCapacity;
19
+ };
20
+ export type SendCompletelyCompactResult<I extends BatchCommandInput> = I extends BatchGetCommandInput ? SendCompletelyCompactGetOutput : SendCompletelyCompactWriteOutput;
21
+ export type SendCompletelyOptions = {
22
+ concurrency?: number;
23
+ maxRetries?: number;
24
+ };
25
+ export declare class SendCompletelyBatch<I extends BatchCommandInput, O extends BatchCommandOutput = I extends BatchGetCommandInput ? BatchGetCommandOutput : BatchWriteCommandOutput> {
26
+ private readonly requestFactory;
27
+ items: Array<Pick<I, 'RequestItems' | 'ReturnConsumedCapacity'>>;
28
+ private readonly options;
29
+ readonly requests: Array<Fetcher<I>>;
30
+ constructor(requestFactory: RequestFactory<I>, items: Array<Pick<I, 'RequestItems' | 'ReturnConsumedCapacity'>>, options: SendCompletelyOptions);
31
+ load(items: Array<Pick<I, 'RequestItems' | 'ReturnConsumedCapacity'>>): SendCompletelyBatch<I, I extends BatchGetCommandInput ? BatchGetCommandOutput : BatchWriteCommandOutput>;
32
+ sendAll(options: SendCompletelyOptions & {
33
+ compact: true;
34
+ }): Promise<{
35
+ data: SendCompletelyCompactResult<I>;
36
+ error: AggregateError | undefined;
37
+ }>;
38
+ sendAll(options?: SendCompletelyOptions): Promise<{
39
+ data: SendCompletelyResult<O>;
40
+ error: AggregateError | undefined;
41
+ }>;
42
+ sendAll(cb: SendCompletelyCallback<I>): void;
43
+ sendAll(options: SendCompletelyOptions, cb: SendCompletelyCallback<I>): void;
44
+ sendAll(options: SendCompletelyOptions & {
45
+ compact: true;
46
+ }, cb: SendCompletelyCompactCallback<I>): void;
47
+ }
48
+ export {};