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.
- package/dist/Aggregate.d.ts +1 -1
- package/dist/Aggregate.d.ts.map +1 -1
- package/dist/Aggregate.js +22 -6
- package/dist/Aggregate.js.map +1 -1
- package/dist/Batch.d.ts +1 -1
- package/dist/Batch.d.ts.map +1 -1
- package/dist/Batch.js +7 -5
- package/dist/Batch.js.map +1 -1
- package/dist/ConsumedCapacity.d.ts +107 -0
- package/dist/ConsumedCapacity.d.ts.map +1 -0
- package/dist/ConsumedCapacity.js +86 -0
- package/dist/ConsumedCapacity.js.map +1 -0
- package/dist/DynamoClient.d.ts.map +1 -1
- package/dist/DynamoClient.js +6 -1
- package/dist/DynamoClient.js.map +1 -1
- package/dist/Entity.d.ts +1 -1
- package/dist/Entity.d.ts.map +1 -1
- package/dist/Entity.js +76 -42
- package/dist/Entity.js.map +1 -1
- package/dist/EventStore.d.ts +1 -1
- package/dist/EventStore.d.ts.map +1 -1
- package/dist/EventStore.js +35 -18
- package/dist/EventStore.js.map +1 -1
- package/dist/Query.d.ts +15 -0
- package/dist/Query.d.ts.map +1 -1
- package/dist/Query.js +23 -8
- package/dist/Query.js.map +1 -1
- package/dist/Transaction.d.ts +1 -1
- package/dist/Transaction.d.ts.map +1 -1
- package/dist/Transaction.js +7 -5
- package/dist/Transaction.js.map +1 -1
- package/dist/change-feed/ChangeFeed.d.ts.map +1 -1
- package/dist/change-feed/ChangeFeed.js +7 -4
- package/dist/change-feed/ChangeFeed.js.map +1 -1
- package/dist/change-feed/CheckpointStore.d.ts +1 -1
- package/dist/change-feed/CheckpointStore.d.ts.map +1 -1
- package/dist/change-feed/CheckpointStore.js +11 -3
- package/dist/change-feed/CheckpointStore.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/internal/BoundCrud.js +3 -3
- package/dist/internal/BoundCrud.js.map +1 -1
- package/dist/internal/BoundQuery.d.ts +16 -0
- package/dist/internal/BoundQuery.d.ts.map +1 -1
- package/dist/internal/BoundQuery.js +21 -4
- package/dist/internal/BoundQuery.js.map +1 -1
- package/dist/internal/BoundVectorQuery.d.ts +1 -1
- package/dist/internal/BoundVectorQuery.d.ts.map +1 -1
- package/dist/internal/BoundVectorQuery.js +4 -3
- package/dist/internal/BoundVectorQuery.js.map +1 -1
- package/dist/internal/Capacity.d.ts +254 -0
- package/dist/internal/Capacity.d.ts.map +1 -0
- package/dist/internal/Capacity.js +653 -0
- package/dist/internal/Capacity.js.map +1 -0
- package/dist/internal/EntityCombinators.d.ts.map +1 -1
- package/dist/internal/EntityCombinators.js +4 -4
- package/dist/internal/EntityCombinators.js.map +1 -1
- package/dist/internal/EntityOps.d.ts +10 -1
- package/dist/internal/EntityOps.d.ts.map +1 -1
- package/dist/internal/EntityOps.js +22 -4
- package/dist/internal/EntityOps.js.map +1 -1
- package/dist/internal/EventProjection.d.ts.map +1 -1
- package/dist/internal/EventProjection.js +12 -8
- package/dist/internal/EventProjection.js.map +1 -1
- package/dist/internal/MultiProjection.d.ts.map +1 -1
- package/dist/internal/MultiProjection.js +11 -8
- package/dist/internal/MultiProjection.js.map +1 -1
- package/dist/internal/ProjectionEngine.d.ts.map +1 -1
- package/dist/internal/ProjectionEngine.js +5 -5
- package/dist/internal/ProjectionEngine.js.map +1 -1
- package/dist/internal/TransactPlan.d.ts.map +1 -1
- package/dist/internal/TransactPlan.js +5 -3
- package/dist/internal/TransactPlan.js.map +1 -1
- 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
|