effect-dynamodb 1.35.0 → 1.36.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.
Files changed (76) hide show
  1. package/dist/Aggregate.d.ts +1 -1
  2. package/dist/Aggregate.d.ts.map +1 -1
  3. package/dist/Aggregate.js +22 -6
  4. package/dist/Aggregate.js.map +1 -1
  5. package/dist/Batch.d.ts +1 -1
  6. package/dist/Batch.d.ts.map +1 -1
  7. package/dist/Batch.js +7 -5
  8. package/dist/Batch.js.map +1 -1
  9. package/dist/ConsumedCapacity.d.ts +107 -0
  10. package/dist/ConsumedCapacity.d.ts.map +1 -0
  11. package/dist/ConsumedCapacity.js +86 -0
  12. package/dist/ConsumedCapacity.js.map +1 -0
  13. package/dist/DynamoClient.d.ts.map +1 -1
  14. package/dist/DynamoClient.js +6 -1
  15. package/dist/DynamoClient.js.map +1 -1
  16. package/dist/Entity.d.ts +1 -1
  17. package/dist/Entity.d.ts.map +1 -1
  18. package/dist/Entity.js +76 -42
  19. package/dist/Entity.js.map +1 -1
  20. package/dist/EventStore.d.ts +1 -1
  21. package/dist/EventStore.d.ts.map +1 -1
  22. package/dist/EventStore.js +35 -18
  23. package/dist/EventStore.js.map +1 -1
  24. package/dist/Query.d.ts +15 -0
  25. package/dist/Query.d.ts.map +1 -1
  26. package/dist/Query.js +23 -8
  27. package/dist/Query.js.map +1 -1
  28. package/dist/Transaction.d.ts +1 -1
  29. package/dist/Transaction.d.ts.map +1 -1
  30. package/dist/Transaction.js +7 -5
  31. package/dist/Transaction.js.map +1 -1
  32. package/dist/change-feed/ChangeFeed.d.ts.map +1 -1
  33. package/dist/change-feed/ChangeFeed.js +7 -4
  34. package/dist/change-feed/ChangeFeed.js.map +1 -1
  35. package/dist/change-feed/CheckpointStore.d.ts +1 -1
  36. package/dist/change-feed/CheckpointStore.d.ts.map +1 -1
  37. package/dist/change-feed/CheckpointStore.js +11 -3
  38. package/dist/change-feed/CheckpointStore.js.map +1 -1
  39. package/dist/index.d.ts +1 -0
  40. package/dist/index.d.ts.map +1 -1
  41. package/dist/index.js +1 -0
  42. package/dist/index.js.map +1 -1
  43. package/dist/internal/BoundCrud.js +3 -3
  44. package/dist/internal/BoundCrud.js.map +1 -1
  45. package/dist/internal/BoundQuery.d.ts +16 -0
  46. package/dist/internal/BoundQuery.d.ts.map +1 -1
  47. package/dist/internal/BoundQuery.js +21 -4
  48. package/dist/internal/BoundQuery.js.map +1 -1
  49. package/dist/internal/BoundVectorQuery.d.ts +1 -1
  50. package/dist/internal/BoundVectorQuery.d.ts.map +1 -1
  51. package/dist/internal/BoundVectorQuery.js +4 -3
  52. package/dist/internal/BoundVectorQuery.js.map +1 -1
  53. package/dist/internal/Capacity.d.ts +254 -0
  54. package/dist/internal/Capacity.d.ts.map +1 -0
  55. package/dist/internal/Capacity.js +653 -0
  56. package/dist/internal/Capacity.js.map +1 -0
  57. package/dist/internal/EntityCombinators.d.ts.map +1 -1
  58. package/dist/internal/EntityCombinators.js +4 -4
  59. package/dist/internal/EntityCombinators.js.map +1 -1
  60. package/dist/internal/EntityOps.d.ts +10 -1
  61. package/dist/internal/EntityOps.d.ts.map +1 -1
  62. package/dist/internal/EntityOps.js +22 -4
  63. package/dist/internal/EntityOps.js.map +1 -1
  64. package/dist/internal/EventProjection.d.ts.map +1 -1
  65. package/dist/internal/EventProjection.js +12 -8
  66. package/dist/internal/EventProjection.js.map +1 -1
  67. package/dist/internal/MultiProjection.d.ts.map +1 -1
  68. package/dist/internal/MultiProjection.js +11 -8
  69. package/dist/internal/MultiProjection.js.map +1 -1
  70. package/dist/internal/ProjectionEngine.d.ts.map +1 -1
  71. package/dist/internal/ProjectionEngine.js +5 -5
  72. package/dist/internal/ProjectionEngine.js.map +1 -1
  73. package/dist/internal/TransactPlan.d.ts.map +1 -1
  74. package/dist/internal/TransactPlan.js +5 -3
  75. package/dist/internal/TransactPlan.js.map +1 -1
  76. package/package.json +2 -2
@@ -0,0 +1,653 @@
1
+ /**
2
+ * @internal Consumed-capacity reporting (#174) — the choke point and the
3
+ * operation scopes. The public surface is `ConsumedCapacity.ts`.
4
+ *
5
+ * - {@link client} is how every library code path gets the DynamoDB client: an
6
+ * instrumented view ({@link instrument}) of whatever `DynamoClient` is in
7
+ * context — the built-in layer, a decorator, a mock. With reporting off the
8
+ * view calls the underlying method with the very same input and nothing else
9
+ * happens; with it on, each data-plane request asks for `ConsumedCapacity`
10
+ * and records what came back into the current operation scope.
11
+ * - {@link operation} / {@link operationStream} open a named scope at a
12
+ * library operation's entry point. Requests record into their scope and
13
+ * every enclosing one; the outermost (root) scope delivers one report to the
14
+ * observer, so reports never double count. See
15
+ * `docs/designs/consumed-capacity.md`.
16
+ */
17
+ import { Cause, Context, Effect, Exit, Stream, Tracer } from "effect";
18
+ import { DynamoClient } from "../DynamoClient.js";
19
+ /** The "off" setting — the default. */
20
+ export const off = Object.freeze({
21
+ enabled: false,
22
+ level: "total",
23
+ spans: false,
24
+ onOperation: undefined,
25
+ });
26
+ /** Build an enabled setting. */
27
+ export const make = (options) => ({
28
+ enabled: true,
29
+ level: options?.level ?? "total",
30
+ spans: options?.spans ?? true,
31
+ onOperation: options?.onOperation,
32
+ });
33
+ /** The reporting setting. Default: off. */
34
+ export const Reporting = Context.Reference("effect-dynamodb/ConsumedCapacity/Reporting", { defaultValue: () => off });
35
+ // ---------------------------------------------------------------------------
36
+ // Span attribute names
37
+ // ---------------------------------------------------------------------------
38
+ export const Attributes = {
39
+ /** `"aws.dynamodb"` (OTel semconv). */
40
+ dbSystem: "db.system.name",
41
+ operation: "effect_dynamodb.operation",
42
+ target: "effect_dynamodb.target",
43
+ index: "effect_dynamodb.index",
44
+ /** `true` when the operation runs inside another library operation. */
45
+ nested: "effect_dynamodb.nested",
46
+ /** OTel semconv: every table a request named. */
47
+ tableNames: "aws.dynamodb.table_names",
48
+ /** Σ capacity units — always present when reporting is on. */
49
+ total: "aws.dynamodb.consumed_capacity.total",
50
+ read: "aws.dynamodb.consumed_capacity.read",
51
+ write: "aws.dynamodb.consumed_capacity.write",
52
+ requestCount: "effect_dynamodb.request_count",
53
+ unreportedRequests: "effect_dynamodb.unreported_requests",
54
+ vectorSearchRequestBytes: "effect_dynamodb.vector.search_request_bytes",
55
+ vectorWriteRequestBytes: "effect_dynamodb.vector.write_request_bytes",
56
+ };
57
+ /** Span name of an operation: `effect-dynamodb.<operation>`. */
58
+ export const spanName = (operation) => `effect-dynamodb.${operation}`;
59
+ /**
60
+ * Span kind of an operation: `client` for a root, `internal` for a nested one,
61
+ * so tracing backends that count client spans as database calls count each
62
+ * root once (OTel advises against nesting client spans).
63
+ */
64
+ const spanKind = (scope) => scope.parent === undefined ? "client" : "internal";
65
+ /** Set only while an observer runs: everything inside it runs unreported. */
66
+ export const SUPPRESSED = "suppressed";
67
+ /**
68
+ * The operation the current fiber is inside. Never captured by a bind site
69
+ * ({@link detachContext}), so a bound client cannot override it.
70
+ */
71
+ export const CurrentOperation = Context.Reference("effect-dynamodb/ConsumedCapacity/CurrentOperation", { defaultValue: () => undefined });
72
+ const zeroTotals = () => ({
73
+ capacityUnits: 0,
74
+ readCapacityUnits: 0,
75
+ writeCapacityUnits: 0,
76
+ requests: 0,
77
+ unreportedRequests: 0,
78
+ });
79
+ const addTotals = (into, from) => {
80
+ into.capacityUnits += from.capacityUnits;
81
+ into.readCapacityUnits += from.readCapacityUnits;
82
+ into.writeCapacityUnits += from.writeCapacityUnits;
83
+ into.requests += from.requests;
84
+ into.unreportedRequests += from.unreportedRequests;
85
+ };
86
+ /**
87
+ * The nearest open scope at or above `current`. A fiber that outlived its
88
+ * operation still finds the operation's enclosing scope when that one is
89
+ * open; `undefined` only when every enclosing scope has closed (the work
90
+ * then reports as a root of its own).
91
+ */
92
+ const liveScope = (current) => {
93
+ if (current === SUPPRESSED)
94
+ return undefined;
95
+ let s = current;
96
+ while (s?.closed)
97
+ s = s.parent;
98
+ return s;
99
+ };
100
+ const newScope = (name, options, settings, parent) => {
101
+ const scope = {
102
+ name,
103
+ target: options?.target,
104
+ index: options?.index,
105
+ parent,
106
+ settings,
107
+ closed: false,
108
+ outcome: "success",
109
+ totals: zeroTotals(),
110
+ byCommand: {},
111
+ tables: new Map(),
112
+ vectorSearchRequestBytes: 0,
113
+ vectorWriteRequestBytes: 0,
114
+ children: new Map(),
115
+ carried: {
116
+ totals: zeroTotals(),
117
+ tables: new Set(),
118
+ vectorSearchRequestBytes: 0,
119
+ vectorWriteRequestBytes: 0,
120
+ },
121
+ openChildren: new Set(),
122
+ span: undefined,
123
+ };
124
+ parent?.openChildren.add(scope);
125
+ return scope;
126
+ };
127
+ const startAttributes = (scope) => ({
128
+ [Attributes.dbSystem]: "aws.dynamodb",
129
+ [Attributes.operation]: scope.name,
130
+ ...(scope.target !== undefined && { [Attributes.target]: scope.target }),
131
+ ...(scope.index !== undefined && { [Attributes.index]: scope.index }),
132
+ [Attributes.nested]: scope.parent !== undefined,
133
+ });
134
+ /** The span covers the scope's whole life: what it reports plus what was cleared from it. */
135
+ const annotateEnd = (span, scope) => {
136
+ const { carried } = scope;
137
+ const totals = zeroTotals();
138
+ addTotals(totals, scope.totals);
139
+ addTotals(totals, carried.totals);
140
+ const vectorSearch = scope.vectorSearchRequestBytes + carried.vectorSearchRequestBytes;
141
+ const vectorWrite = scope.vectorWriteRequestBytes + carried.vectorWriteRequestBytes;
142
+ span.attribute(Attributes.tableNames, [...new Set([...carried.tables, ...scope.tables.keys()])].sort());
143
+ span.attribute(Attributes.total, totals.capacityUnits);
144
+ span.attribute(Attributes.read, totals.readCapacityUnits);
145
+ span.attribute(Attributes.write, totals.writeCapacityUnits);
146
+ span.attribute(Attributes.requestCount, totals.requests);
147
+ if (totals.unreportedRequests > 0)
148
+ span.attribute(Attributes.unreportedRequests, totals.unreportedRequests);
149
+ if (vectorSearch > 0)
150
+ span.attribute(Attributes.vectorSearchRequestBytes, vectorSearch);
151
+ if (vectorWrite > 0)
152
+ span.attribute(Attributes.vectorWriteRequestBytes, vectorWrite);
153
+ };
154
+ const freezeTotals = (t) => ({
155
+ capacityUnits: t.capacityUnits,
156
+ readCapacityUnits: t.readCapacityUnits,
157
+ writeCapacityUnits: t.writeCapacityUnits,
158
+ requests: t.requests,
159
+ unreportedRequests: t.unreportedRequests,
160
+ });
161
+ const freezeIndex = (c) => ({
162
+ capacityUnits: c.capacityUnits,
163
+ readCapacityUnits: c.readCapacityUnits,
164
+ writeCapacityUnits: c.writeCapacityUnits,
165
+ });
166
+ const freezeIndexes = (m) => m === undefined
167
+ ? undefined
168
+ : Object.fromEntries([...m.entries()].map(([name, c]) => [name, freezeIndex(c)]));
169
+ /** @internal Build the observer report of a (closed) scope. */
170
+ export const buildReport = (scope) => ({
171
+ operation: scope.name,
172
+ target: scope.target,
173
+ index: scope.index,
174
+ ...freezeTotals(scope.totals),
175
+ tables: [...scope.tables.keys()].sort(),
176
+ requestsByCommand: { ...scope.byCommand },
177
+ byTable: [...scope.tables.entries()]
178
+ .sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0))
179
+ .map(([tableName, acc]) => ({
180
+ tableName,
181
+ ...freezeTotals(acc),
182
+ ...(acc.table !== undefined && { table: freezeIndex(acc.table) }),
183
+ ...(acc.gsi !== undefined && { globalSecondaryIndexes: freezeIndexes(acc.gsi) }),
184
+ ...(acc.lsi !== undefined && { localSecondaryIndexes: freezeIndexes(acc.lsi) }),
185
+ })),
186
+ vectorSearchRequestBytes: scope.vectorSearchRequestBytes,
187
+ vectorWriteRequestBytes: scope.vectorWriteRequestBytes,
188
+ nested: [...scope.children.entries()].map(([operation, child]) => ({
189
+ operation,
190
+ calls: child.calls,
191
+ ...freezeTotals(child.totals),
192
+ })),
193
+ outcome: scope.outcome,
194
+ });
195
+ const outcomeOf = (exit) => Exit.isSuccess(exit) ? "success" : Cause.hasInterruptsOnly(exit.cause) ? "interrupted" : "failure";
196
+ /**
197
+ * Clear what `scope` (and every open scope under it) reports, moving it to
198
+ * the span-only `carried` totals. Called when an enclosing scope closes while
199
+ * `scope` is still running: everything recorded so far is already in that
200
+ * scope's totals, so only later work may be reported again.
201
+ */
202
+ const clearReported = (scope) => {
203
+ const { carried } = scope;
204
+ addTotals(carried.totals, scope.totals);
205
+ for (const name of scope.tables.keys())
206
+ carried.tables.add(name);
207
+ carried.vectorSearchRequestBytes += scope.vectorSearchRequestBytes;
208
+ carried.vectorWriteRequestBytes += scope.vectorWriteRequestBytes;
209
+ Object.assign(scope.totals, zeroTotals());
210
+ for (const command of Object.keys(scope.byCommand))
211
+ delete scope.byCommand[command];
212
+ scope.tables.clear();
213
+ scope.vectorSearchRequestBytes = 0;
214
+ scope.vectorWriteRequestBytes = 0;
215
+ scope.children.clear();
216
+ for (const child of scope.openChildren)
217
+ clearReported(child);
218
+ };
219
+ /**
220
+ * A scope is closing while some of its children still run (forked into an
221
+ * outer scope, or detached): hand them to its parent — or, when it is the
222
+ * root, make each a root of its own. Their later requests are then charged to
223
+ * the nearest open scope, and late work under a closed root is reported in a
224
+ * later root report instead of being lost.
225
+ */
226
+ const handOver = (scope) => {
227
+ const parent = scope.parent;
228
+ parent?.openChildren.delete(scope);
229
+ for (const child of scope.openChildren) {
230
+ child.parent = parent;
231
+ parent?.openChildren.add(child);
232
+ clearReported(child);
233
+ }
234
+ scope.openChildren.clear();
235
+ };
236
+ /**
237
+ * Close a scope: annotate its span, hand over its still-running children,
238
+ * add it to its parent's `nested` breakdown, and — for a root — deliver the
239
+ * report. The observer runs with everything it does suppressed; its failures
240
+ * are logged and dropped.
241
+ */
242
+ const finish = (scope, exit) => Effect.suspend(() => {
243
+ if (scope.closed)
244
+ return Effect.void;
245
+ scope.closed = true;
246
+ scope.outcome = outcomeOf(exit);
247
+ if (scope.span !== undefined)
248
+ annotateEnd(scope.span, scope);
249
+ handOver(scope);
250
+ const parent = scope.parent;
251
+ if (parent !== undefined) {
252
+ const entry = parent.children.get(scope.name);
253
+ if (entry === undefined) {
254
+ const totals = zeroTotals();
255
+ addTotals(totals, scope.totals);
256
+ parent.children.set(scope.name, { calls: 1, totals });
257
+ }
258
+ else {
259
+ entry.calls++;
260
+ addTotals(entry.totals, scope.totals);
261
+ }
262
+ return Effect.void;
263
+ }
264
+ const observer = scope.settings.onOperation;
265
+ if (observer === undefined)
266
+ return Effect.void;
267
+ const report = buildReport(scope);
268
+ return Effect.suspend(() => observer(report)).pipe(Effect.provideService(CurrentOperation, SUPPRESSED), Effect.exit, Effect.flatMap((observed) => Exit.isFailure(observed) && !Cause.hasInterruptsOnly(observed.cause)
269
+ ? Effect.logWarning(`effect-dynamodb: the ConsumedCapacity observer failed for ${scope.name}; the report was dropped`, observed.cause)
270
+ : Effect.void));
271
+ });
272
+ const runScope = (name, options, settings, parent, effect) => {
273
+ const scope = newScope(name, options, settings, parent);
274
+ const body = Effect.provideService(effect, CurrentOperation, scope);
275
+ // Success and failure report after the effect, as ordinary (interruptible)
276
+ // code; only an interrupted operation reports from the exit finalizer.
277
+ const finished = Effect.onExit(Effect.flatMap(Effect.exit(body), (exit) => Effect.flatMap(finish(scope, exit), () => exit)), (exit) => (scope.closed ? Effect.void : finish(scope, exit)));
278
+ if (!settings.spans)
279
+ return finished;
280
+ return Effect.useSpan(spanName(name), { kind: spanKind(scope), attributes: startAttributes(scope) }, (span) => {
281
+ scope.span = span;
282
+ return Effect.withParentSpan(finished, span);
283
+ });
284
+ };
285
+ /**
286
+ * Run `effect` as the named library operation. With reporting off — or
287
+ * inside an observer — `effect` runs unchanged.
288
+ */
289
+ export const operation = (name, options, effect,
290
+ /**
291
+ * The setting a bound client captured, for an operation that composes bound
292
+ * calls but does not itself run inside the bound `provide` (a bound stream's
293
+ * command handler). It wins over the run-time one, as captured settings do.
294
+ */
295
+ captured) => Effect.withFiber((fiber) => {
296
+ const settings = captured ?? fiber.getRef(Reporting);
297
+ if (!settings.enabled)
298
+ return effect;
299
+ const current = fiber.getRef(CurrentOperation);
300
+ if (current === SUPPRESSED)
301
+ return effect;
302
+ return runScope(name, options, settings, liveScope(current), effect);
303
+ });
304
+ /**
305
+ * Like {@link operation}, but only when no operation is current: a delegate
306
+ * (a `Query` terminal run by a library accessor) adds its requests to the
307
+ * accessor's scope rather than opening one of its own.
308
+ */
309
+ export const delegate = (name, options, effect) => Effect.withFiber((fiber) => {
310
+ const settings = fiber.getRef(Reporting);
311
+ if (!settings.enabled)
312
+ return effect;
313
+ const current = fiber.getRef(CurrentOperation);
314
+ if (current === SUPPRESSED || liveScope(current) !== undefined)
315
+ return effect;
316
+ return runScope(name, options, settings, undefined, effect);
317
+ });
318
+ /**
319
+ * The stream form, as an effect to run where the stream is BUILT — inside a
320
+ * bound client's `provide`, so the captured setting is the one read. The
321
+ * returned stream decides at its start whether to open a scope (a delegate
322
+ * does not when an operation is already current there), opens it for that
323
+ * run (each run is one operation), and carries the resolved setting and
324
+ * scope to every page pull, which happens later from the consumer's fiber.
325
+ */
326
+ export const operationStream = (name, options, stream, mode = "operation") => Effect.withFiber((fiber) => {
327
+ const settings = fiber.getRef(Reporting);
328
+ if (!settings.enabled)
329
+ return Effect.succeed(Stream.provideService(stream, Reporting, settings));
330
+ return Effect.succeed(Stream.unwrap(Effect.withFiber((startFiber) => {
331
+ const current = startFiber.getRef(CurrentOperation);
332
+ const withSettings = Stream.provideService(stream, Reporting, settings);
333
+ if (current === SUPPRESSED)
334
+ return Effect.succeed(Stream.provideService(withSettings, CurrentOperation, SUPPRESSED));
335
+ const parent = liveScope(current);
336
+ if (mode === "delegate" && parent !== undefined)
337
+ return Effect.succeed(Stream.provideService(withSettings, CurrentOperation, parent));
338
+ return Effect.gen(function* () {
339
+ const scope = newScope(name, options, settings, parent);
340
+ let scoped = Stream.provideService(withSettings, CurrentOperation, scope);
341
+ if (settings.spans) {
342
+ const span = yield* Effect.makeSpanScoped(spanName(name), {
343
+ kind: spanKind(scope),
344
+ attributes: startAttributes(scope),
345
+ });
346
+ scope.span = span;
347
+ scoped = Stream.provideService(scoped, Tracer.ParentSpan, span);
348
+ }
349
+ // Added after the span, so it runs first: the span is annotated
350
+ // before it ends.
351
+ yield* Effect.addFinalizer((exit) => finish(scope, exit));
352
+ return scoped;
353
+ });
354
+ })));
355
+ });
356
+ /**
357
+ * Strip per-call state from a context captured for later use: the current
358
+ * operation scope and the tracer's parent span.
359
+ */
360
+ export const detachContext = (context) => Context.omit(CurrentOperation, Tracer.ParentSpan)(context);
361
+ /**
362
+ * `(name, fn)` → `fn` as the operation `<prefix>.<name>` with `target`, for
363
+ * the methods of an object whose every method is an operation.
364
+ */
365
+ export const namedOperation = (prefix, target) => (name, fn) => (...args) => operation(`${prefix}.${name}`, { target }, fn(...args));
366
+ /** The setting stored in a captured context, if one was provided there (no default). */
367
+ export const capturedSettings = (context) => Context.getOrUndefined(context, Reporting);
368
+ /** Where a bound object keeps the setting captured at bind time. */
369
+ export const CapturedReporting = Symbol.for("effect-dynamodb/Capacity/CapturedReporting");
370
+ /** Run `effect` outside any current operation (a long-lived forked fiber). */
371
+ export const detached = (effect) => Effect.withFiber((fiber) => fiber.getRef(CurrentOperation) === SUPPRESSED
372
+ ? effect
373
+ : Effect.provideService(effect, CurrentOperation, undefined));
374
+ // ---------------------------------------------------------------------------
375
+ // Recording one request
376
+ // ---------------------------------------------------------------------------
377
+ const READ_COMMANDS = new Set([
378
+ "GetItem",
379
+ "Query",
380
+ "Scan",
381
+ "BatchGetItem",
382
+ "TransactGetItems",
383
+ "SearchVectors",
384
+ ]);
385
+ /** Failures of requests that were never sent (a transaction plan's recorded write). */
386
+ const notSentErrors = new WeakSet();
387
+ /** @internal Mark a client failure as local: no request reached DynamoDB. */
388
+ export const markNotSent = (error) => {
389
+ notSentErrors.add(error);
390
+ return error;
391
+ };
392
+ const tablesOf = (command, input) => {
393
+ const names = new Set();
394
+ if (typeof input.TableName === "string")
395
+ names.add(input.TableName);
396
+ if (command === "BatchGetItem" || command === "BatchWriteItem") {
397
+ const requestItems = input.RequestItems;
398
+ if (requestItems !== null && typeof requestItems === "object")
399
+ for (const name of Object.keys(requestItems))
400
+ names.add(name);
401
+ }
402
+ if (command === "TransactGetItems" || command === "TransactWriteItems") {
403
+ const items = input.TransactItems;
404
+ if (Array.isArray(items))
405
+ for (const item of items) {
406
+ if (item === null || typeof item !== "object")
407
+ continue;
408
+ for (const part of Object.values(item)) {
409
+ const name = part?.TableName;
410
+ if (typeof name === "string")
411
+ names.add(name);
412
+ }
413
+ }
414
+ }
415
+ return [...names];
416
+ };
417
+ const toCapacityList = (value) => {
418
+ if (value === undefined || value === null)
419
+ return undefined;
420
+ if (Array.isArray(value))
421
+ return value;
422
+ if (typeof value === "object")
423
+ return [value];
424
+ return undefined;
425
+ };
426
+ /** `ConsumedCapacity` from a failure: on the error, or on the SDK exception it wraps. */
427
+ const capacityOfError = (error) => {
428
+ if (error === null || typeof error !== "object")
429
+ return undefined;
430
+ const direct = error.ConsumedCapacity;
431
+ if (direct !== undefined)
432
+ return direct;
433
+ const cause = error.cause;
434
+ if (cause === null || typeof cause !== "object")
435
+ return undefined;
436
+ return cause.ConsumedCapacity;
437
+ };
438
+ const split = (command, c) => {
439
+ const explicit = c.ReadCapacityUnits !== undefined || c.WriteCapacityUnits !== undefined;
440
+ const units = c.CapacityUnits ?? (c.ReadCapacityUnits ?? 0) + (c.WriteCapacityUnits ?? 0);
441
+ if (explicit)
442
+ return { units, read: c.ReadCapacityUnits ?? 0, write: c.WriteCapacityUnits ?? 0 };
443
+ return READ_COMMANDS.has(command)
444
+ ? { units, read: units, write: 0 }
445
+ : { units, read: 0, write: units };
446
+ };
447
+ const addSplit = (into, s) => {
448
+ into.capacityUnits += s.units;
449
+ into.readCapacityUnits += s.read;
450
+ into.writeCapacityUnits += s.write;
451
+ };
452
+ const addIndexes = (existing, command, indexes) => {
453
+ if (indexes === undefined)
454
+ return existing;
455
+ const map = existing ?? new Map();
456
+ for (const [name, capacity] of Object.entries(indexes)) {
457
+ let acc = map.get(name);
458
+ if (acc === undefined) {
459
+ acc = { capacityUnits: 0, readCapacityUnits: 0, writeCapacityUnits: 0 };
460
+ map.set(name, acc);
461
+ }
462
+ addSplit(acc, split(command, capacity));
463
+ }
464
+ return map;
465
+ };
466
+ const tableAccumulator = (scope, name) => {
467
+ let acc = scope.tables.get(name);
468
+ if (acc === undefined) {
469
+ acc = { ...zeroTotals(), table: undefined, gsi: undefined, lsi: undefined };
470
+ scope.tables.set(name, acc);
471
+ }
472
+ return acc;
473
+ };
474
+ const describeRequest = (command, input, exit) => {
475
+ let raw;
476
+ if (Exit.isSuccess(exit)) {
477
+ raw = exit.value?.ConsumedCapacity;
478
+ }
479
+ else {
480
+ const error = Cause.findErrorOption(exit.cause);
481
+ if (error._tag === "Some") {
482
+ if (typeof error.value === "object" && error.value !== null && notSentErrors.has(error.value))
483
+ return undefined;
484
+ raw = capacityOfError(error.value);
485
+ }
486
+ }
487
+ const tables = tablesOf(command, input);
488
+ if (command === "SearchVectors") {
489
+ const bytes = raw
490
+ ?.VectorSearchRequestBytes;
491
+ const searchBytes = typeof bytes === "number" ? bytes : 0;
492
+ return {
493
+ command,
494
+ tables,
495
+ capacities: [],
496
+ reported: typeof bytes === "number",
497
+ vectorSearchRequestBytes: searchBytes,
498
+ vectorWriteRequestBytes: 0,
499
+ };
500
+ }
501
+ const capacities = toCapacityList(raw) ?? [];
502
+ let writeBytes = 0;
503
+ for (const c of capacities)
504
+ for (const v of Object.values(c.VectorIndexes ?? {}))
505
+ writeBytes += v.VectorWriteRequestBytes ?? 0;
506
+ return {
507
+ command,
508
+ tables,
509
+ capacities,
510
+ reported: capacities.length > 0,
511
+ vectorSearchRequestBytes: 0,
512
+ vectorWriteRequestBytes: writeBytes,
513
+ };
514
+ };
515
+ const applyRequest = (scope, record) => {
516
+ const { command } = record;
517
+ scope.totals.requests++;
518
+ scope.byCommand[command] = (scope.byCommand[command] ?? 0) + 1;
519
+ if (!record.reported)
520
+ scope.totals.unreportedRequests++;
521
+ scope.vectorSearchRequestBytes += record.vectorSearchRequestBytes;
522
+ scope.vectorWriteRequestBytes += record.vectorWriteRequestBytes;
523
+ for (const name of record.tables) {
524
+ const acc = tableAccumulator(scope, name);
525
+ acc.requests++;
526
+ if (!record.reported)
527
+ acc.unreportedRequests++;
528
+ }
529
+ for (const c of record.capacities) {
530
+ const s = split(command, c);
531
+ scope.totals.capacityUnits += s.units;
532
+ scope.totals.readCapacityUnits += s.read;
533
+ scope.totals.writeCapacityUnits += s.write;
534
+ const tableName = c.TableName ?? (record.tables.length === 1 ? record.tables[0] : undefined) ?? "unknown";
535
+ const acc = tableAccumulator(scope, tableName);
536
+ addSplit(acc, s);
537
+ if (c.Table !== undefined) {
538
+ acc.table ??= { capacityUnits: 0, readCapacityUnits: 0, writeCapacityUnits: 0 };
539
+ addSplit(acc.table, split(command, c.Table));
540
+ }
541
+ acc.gsi = addIndexes(acc.gsi, command, c.GlobalSecondaryIndexes);
542
+ acc.lsi = addIndexes(acc.lsi, command, c.LocalSecondaryIndexes);
543
+ }
544
+ };
545
+ /**
546
+ * Charge one request to the nearest open scope at or above the one it was
547
+ * sent in, and every ancestor of that (all open, by the parent invariant).
548
+ * When every one of them has closed while the request was in flight, the
549
+ * request is reported as an implicit root of its own, so it is never lost.
550
+ */
551
+ const record = (scope, command, input, exit) => Effect.suspend(() => {
552
+ const described = describeRequest(command, input, exit);
553
+ if (described === undefined)
554
+ return Effect.void;
555
+ const open = liveScope(scope);
556
+ if (open === undefined) {
557
+ // No span: the request ran under the (now ended) span of its operation.
558
+ const late = newScope(`DynamoClient.${command}`, undefined, scope.settings, undefined);
559
+ applyRequest(late, described);
560
+ return finish(late, exit);
561
+ }
562
+ for (let s = open; s !== undefined; s = s.parent) {
563
+ if (!s.closed)
564
+ applyRequest(s, described);
565
+ }
566
+ return Effect.void;
567
+ });
568
+ // ---------------------------------------------------------------------------
569
+ // The choke point
570
+ // ---------------------------------------------------------------------------
571
+ /** The data-plane methods and their command names. Table management passes through. */
572
+ export const DATA_PLANE = [
573
+ ["getItem", "GetItem"],
574
+ ["putItem", "PutItem"],
575
+ ["updateItem", "UpdateItem"],
576
+ ["deleteItem", "DeleteItem"],
577
+ ["query", "Query"],
578
+ ["scan", "Scan"],
579
+ ["batchGetItem", "BatchGetItem"],
580
+ ["batchWriteItem", "BatchWriteItem"],
581
+ ["transactGetItems", "TransactGetItems"],
582
+ ["transactWriteItems", "TransactWriteItems"],
583
+ ["searchVectors", "SearchVectors"],
584
+ ];
585
+ const InstrumentedTypeId = Symbol.for("effect-dynamodb/Capacity/Instrumented");
586
+ const instrumentedCache = new WeakMap();
587
+ const send = (command, raw, input) => Effect.withFiber((fiber) => {
588
+ const settings = fiber.getRef(Reporting);
589
+ if (!settings.enabled)
590
+ return raw(input);
591
+ const current = fiber.getRef(CurrentOperation);
592
+ if (current === SUPPRESSED)
593
+ return raw(input);
594
+ const flagged = input.ReturnConsumedCapacity !== undefined
595
+ ? input
596
+ : { ...input, ReturnConsumedCapacity: settings.level === "indexes" ? "INDEXES" : "TOTAL" };
597
+ const recorded = Effect.withFiber((inner) => {
598
+ const scope = liveScope(inner.getRef(CurrentOperation));
599
+ return raw(flagged).pipe(Effect.onExit((exit) => scope !== undefined ? record(scope, command, input, exit) : Effect.void));
600
+ });
601
+ // A request outside every operation is an implicit root, named after the
602
+ // command: its cost is still reported, under a name that shows the gap.
603
+ return liveScope(current) !== undefined
604
+ ? recorded
605
+ : runScope(`DynamoClient.${command}`, undefined, settings, undefined, recorded);
606
+ });
607
+ /**
608
+ * The instrumented view of a client: the data-plane methods record their
609
+ * capacity, everything else is the client's own. Memoised per client; an
610
+ * already-instrumented client is returned as is.
611
+ */
612
+ export const instrument = (service) => {
613
+ if (service[InstrumentedTypeId] === true)
614
+ return service;
615
+ const cached = instrumentedCache.get(service);
616
+ if (cached !== undefined)
617
+ return cached;
618
+ // Prototype-backed, so a class-based client keeps its other members.
619
+ const view = Object.create(service);
620
+ for (const [method, command] of DATA_PLANE) {
621
+ if (typeof service[method] !== "function")
622
+ continue;
623
+ // Looked up per call, so a client whose methods are swapped later is honoured.
624
+ const raw = (input) => service[method].call(service, input);
625
+ view[method] = (input) => send(command, raw, input);
626
+ }
627
+ Object.defineProperty(view, InstrumentedTypeId, { value: true, enumerable: false });
628
+ const instrumented = view;
629
+ instrumentedCache.set(service, instrumented);
630
+ return instrumented;
631
+ };
632
+ /** The client every library code path uses: the instrumented view of the one in context. */
633
+ export const client = Effect.suspend(() => Effect.map(Effect.service(DynamoClient), instrument));
634
+ // ---------------------------------------------------------------------------
635
+ // Operation names carried by op builders
636
+ // ---------------------------------------------------------------------------
637
+ const OperationNameTypeId = Symbol.for("effect-dynamodb/Capacity/OperationName");
638
+ /**
639
+ * Name the operation an entity op builder implements (`Entity.getVersion`
640
+ * on a get-shaped op). The name travels with the builder function, so the
641
+ * combinators that rebuild an op from its builder keep it.
642
+ */
643
+ export const nameBuilder = (builder, name) => {
644
+ Object.defineProperty(builder, OperationNameTypeId, { value: name, enumerable: false });
645
+ return builder;
646
+ };
647
+ /** The name {@link nameBuilder} gave a builder, if any. */
648
+ export const builderName = (builder) => builder[OperationNameTypeId];
649
+ /** The `target` of an entity op: its entity type. */
650
+ export const entityTarget = (entity) => ({
651
+ target: entity.entityType,
652
+ });
653
+ //# sourceMappingURL=Capacity.js.map