effect-dynamodb 1.27.0 → 1.29.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/Batch.d.ts.map +1 -1
- package/dist/Batch.js +3 -2
- package/dist/Batch.js.map +1 -1
- package/dist/Entity.d.ts +24 -1
- package/dist/Entity.d.ts.map +1 -1
- package/dist/Entity.js +33 -17
- package/dist/Entity.js.map +1 -1
- package/dist/EventStore.d.ts +66 -5
- package/dist/EventStore.d.ts.map +1 -1
- package/dist/EventStore.js +100 -2
- package/dist/EventStore.js.map +1 -1
- package/dist/Transaction.d.ts.map +1 -1
- package/dist/Transaction.js +5 -2
- package/dist/Transaction.js.map +1 -1
- package/dist/change-feed/ChangeFeed.d.ts +203 -0
- package/dist/change-feed/ChangeFeed.d.ts.map +1 -0
- package/dist/change-feed/ChangeFeed.js +498 -0
- package/dist/change-feed/ChangeFeed.js.map +1 -0
- package/dist/change-feed/CheckpointStore.d.ts +113 -0
- package/dist/change-feed/CheckpointStore.d.ts.map +1 -0
- package/dist/change-feed/CheckpointStore.js +247 -0
- package/dist/change-feed/CheckpointStore.js.map +1 -0
- package/dist/change-feed/DynamoStreamsClient.d.ts +71 -0
- package/dist/change-feed/DynamoStreamsClient.d.ts.map +1 -0
- package/dist/change-feed/DynamoStreamsClient.js +92 -0
- package/dist/change-feed/DynamoStreamsClient.js.map +1 -0
- package/dist/change-feed/Errors.d.ts +84 -0
- package/dist/change-feed/Errors.d.ts.map +1 -0
- package/dist/change-feed/Errors.js +40 -0
- package/dist/change-feed/Errors.js.map +1 -0
- package/dist/change-feed/index.d.ts +36 -0
- package/dist/change-feed/index.d.ts.map +1 -0
- package/dist/change-feed/index.js +36 -0
- package/dist/change-feed/index.js.map +1 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/internal/EntityOps.d.ts +7 -2
- package/dist/internal/EntityOps.d.ts.map +1 -1
- package/dist/internal/EntityOps.js.map +1 -1
- package/dist/internal/EventProjection.d.ts +782 -0
- package/dist/internal/EventProjection.d.ts.map +1 -0
- package/dist/internal/EventProjection.js +608 -0
- package/dist/internal/EventProjection.js.map +1 -0
- package/dist/internal/MultiProjection.d.ts +255 -0
- package/dist/internal/MultiProjection.d.ts.map +1 -0
- package/dist/internal/MultiProjection.js +377 -0
- package/dist/internal/MultiProjection.js.map +1 -0
- package/dist/internal/ProjectionEngine.d.ts +253 -0
- package/dist/internal/ProjectionEngine.d.ts.map +1 -0
- package/dist/internal/ProjectionEngine.js +837 -0
- package/dist/internal/ProjectionEngine.js.map +1 -0
- package/dist/internal/TransactWriteOps.d.ts.map +1 -1
- package/dist/internal/TransactWriteOps.js +12 -3
- package/dist/internal/TransactWriteOps.js.map +1 -1
- package/dist/internal/TransactableOps.d.ts +44 -1
- package/dist/internal/TransactableOps.d.ts.map +1 -1
- package/dist/internal/TransactableOps.js +52 -2
- package/dist/internal/TransactableOps.js.map +1 -1
- package/package.json +14 -3
|
@@ -0,0 +1,782 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* EventStore projections (#151) — read models folded from a change feed that
|
|
3
|
+
* take their order from the event store, not from delivery.
|
|
4
|
+
*
|
|
5
|
+
* DynamoDB Streams orders records per item only, and a transaction's records
|
|
6
|
+
* carry no grouping or order, so a stream's events reach a consumer in any
|
|
7
|
+
* order. A projection groups a batch's events by row, sorts and
|
|
8
|
+
* deduplicates them, reads from the store only the versions the batch lacks
|
|
9
|
+
* (one strongly consistent `readFrom`), folds, and writes the read-model row
|
|
10
|
+
* once, conditioned on the checkpoint it read. See
|
|
11
|
+
* `docs/designs/eventstore-projections.md`.
|
|
12
|
+
*
|
|
13
|
+
* Re-exported from `EventStore`; this module only type-imports it.
|
|
14
|
+
*
|
|
15
|
+
* @internal
|
|
16
|
+
*/
|
|
17
|
+
import { type ConcurrentModification, type ConditionalCheckFailed, type ItemNotFound, StreamRecordIdentifierMissing, type ValidationError } from "@effect-dynamodb/schema/Errors.js";
|
|
18
|
+
import { Duration, Effect, Option, Pipeable, type Schedule, type Schema } from "effect";
|
|
19
|
+
import type { DynamoClient, DynamoClientError } from "../DynamoClient.js";
|
|
20
|
+
import type { EventStream, StreamEvent, StreamIdInput, StreamMetadata } from "../EventStore.js";
|
|
21
|
+
import type { Table, TableConfig } from "../Table.js";
|
|
22
|
+
import type { EntityGet, EntityPut } from "./EntityOps.js";
|
|
23
|
+
import { type StreamImage } from "./Packed.js";
|
|
24
|
+
import { type DrainResult, type OutboxMeta, type RemoveRow } from "./ProjectionEngine.js";
|
|
25
|
+
/**
|
|
26
|
+
* The read model a projection writes: an unbound `Entity`. Its row is read
|
|
27
|
+
* with a strongly consistent `get` and written whole with `put` (or `create`
|
|
28
|
+
* for a new row), so the projection owns the row.
|
|
29
|
+
*/
|
|
30
|
+
export interface ProjectionReadModel {
|
|
31
|
+
readonly entityType: string;
|
|
32
|
+
readonly model: Schema.Top;
|
|
33
|
+
readonly versioned: unknown;
|
|
34
|
+
readonly softDelete: unknown;
|
|
35
|
+
readonly timeSeries: unknown;
|
|
36
|
+
readonly get: (key: any) => EntityGet<any, any, any, any>;
|
|
37
|
+
readonly put: (input: any) => EntityPut<any, any, any, any>;
|
|
38
|
+
readonly create: (input: any) => EntityPut<any, any, any, any>;
|
|
39
|
+
readonly systemFields: {
|
|
40
|
+
readonly createdAt: string | null;
|
|
41
|
+
};
|
|
42
|
+
readonly _attachPrototype?: ((decoded: any) => any) | undefined;
|
|
43
|
+
}
|
|
44
|
+
/** The state a projection folds: the read model's model type. */
|
|
45
|
+
export type ProjectionState<RM> = RM extends {
|
|
46
|
+
readonly get: (key: any) => EntityGet<infer A, any, any, any>;
|
|
47
|
+
} ? A : never;
|
|
48
|
+
/** The read model's primary key, as its `get` takes it. */
|
|
49
|
+
export type ProjectionKey<RM> = RM extends {
|
|
50
|
+
readonly get: (key: infer K) => any;
|
|
51
|
+
} ? K : never;
|
|
52
|
+
/**
|
|
53
|
+
* What reading and writing the read-model row can fail with — its `get` (a
|
|
54
|
+
* missing row is not an error) and its `put`. The conditional write's
|
|
55
|
+
* `ConditionalCheckFailed` is the projection's own race signal, never surfaced.
|
|
56
|
+
*/
|
|
57
|
+
export type ProjectionReadModelError<RM> = (RM extends {
|
|
58
|
+
readonly put: (input: any) => EntityPut<any, any, infer E, any>;
|
|
59
|
+
} ? Exclude<E, ConditionalCheckFailed> : never) | (RM extends {
|
|
60
|
+
readonly get: (key: any) => EntityGet<any, any, infer E, any>;
|
|
61
|
+
} ? Exclude<E, ItemNotFound> : never);
|
|
62
|
+
/** The read model's numeric fields — those a checkpoint may name. */
|
|
63
|
+
export type CheckpointField<S> = {
|
|
64
|
+
[K in keyof S]-?: S[K] extends number ? K : never;
|
|
65
|
+
}[keyof S] & string;
|
|
66
|
+
/**
|
|
67
|
+
* The read model's field holding the last applied stream version. With
|
|
68
|
+
* `{ field, offset }` the field stores `version + offset`: a read model that
|
|
69
|
+
* numbered versions from 0 adopts a projection with `offset: -1`, its stored
|
|
70
|
+
* values and readers unchanged.
|
|
71
|
+
*/
|
|
72
|
+
export type ProjectionCheckpoint<S> = CheckpointField<S> | {
|
|
73
|
+
readonly field: CheckpointField<S>;
|
|
74
|
+
readonly offset?: number | undefined;
|
|
75
|
+
};
|
|
76
|
+
/** Configuration of {@link projection}. */
|
|
77
|
+
export interface ProjectionConfig<TEvent, TStreamIdFields extends ReadonlyArray<string>, TMetadata, RM extends ProjectionReadModel, E, R, KR extends ProjectionKey<RM> | Option.Option<ProjectionKey<RM>> = ProjectionKey<RM>, O = never> {
|
|
78
|
+
/** The source stream. One projection reads one stream. */
|
|
79
|
+
readonly stream: EventStream<TEvent, TStreamIdFields, TMetadata, any, any, any, any>;
|
|
80
|
+
/** The read model (an unbound `Entity`). */
|
|
81
|
+
readonly readModel: RM;
|
|
82
|
+
/**
|
|
83
|
+
* The row an event folds into. A function of the stream id alone gives one
|
|
84
|
+
* row per stream instance. One that also reads the event lets a stream feed
|
|
85
|
+
* several rows — each row is still fed by that one stream instance (a row
|
|
86
|
+
* fed by several streams needs a checkpoint per source, which projections
|
|
87
|
+
* don't keep). When a later event addresses a different row, the earlier
|
|
88
|
+
* row keeps what it had, and the new row folds only its own events.
|
|
89
|
+
*
|
|
90
|
+
* Return an `Option` to let an event address no row: `Option.none()`
|
|
91
|
+
* acknowledges it — no row is read or written, no checkpoint moves, and it
|
|
92
|
+
* is in no outcome (`ignored` lists its position). `apply` then returns an
|
|
93
|
+
* `Option` too.
|
|
94
|
+
*/
|
|
95
|
+
readonly key: (streamId: StreamIdInput<TStreamIdFields>, event: StreamEvent<TEvent, StreamMetadata<TMetadata>>) => KR;
|
|
96
|
+
/**
|
|
97
|
+
* The state a row starts from, before `event` — the first event folded into
|
|
98
|
+
* a row that does not exist yet.
|
|
99
|
+
*/
|
|
100
|
+
readonly initial: (streamId: StreamIdInput<TStreamIdFields>, event: StreamEvent<TEvent, StreamMetadata<TMetadata>>) => ProjectionState<RM>;
|
|
101
|
+
/**
|
|
102
|
+
* Apply one event. Return the state unchanged to ignore an event (the
|
|
103
|
+
* checkpoint still advances), or `EventStore.removeRow` to remove the row
|
|
104
|
+
* (with `removal`). A failure fails this row only, unless `terminal`
|
|
105
|
+
* accepts it. The library sets the checkpoint (and revision) after the
|
|
106
|
+
* fold; `evolve` need not. It runs again on every lost-race retry, so keep
|
|
107
|
+
* it free of side effects.
|
|
108
|
+
*/
|
|
109
|
+
readonly evolve: (state: ProjectionState<RM>, event: StreamEvent<TEvent, StreamMetadata<TMetadata>>) => Effect.Effect<ProjectionState<RM> | RemoveRow, E, R>;
|
|
110
|
+
/** See {@link ProjectionCheckpoint}. */
|
|
111
|
+
readonly checkpoint: ProjectionCheckpoint<ProjectionState<RM>>;
|
|
112
|
+
/**
|
|
113
|
+
* The row as written, from the folded state (checkpoint set) — to recompute
|
|
114
|
+
* attributes kept for other readers (`Hidden` index fields, say) on every
|
|
115
|
+
* write. Default: the state as folded.
|
|
116
|
+
*/
|
|
117
|
+
readonly toItem?: ((state: ProjectionState<RM>, context: {
|
|
118
|
+
readonly streamId: StreamIdInput<TStreamIdFields>;
|
|
119
|
+
readonly key: ProjectionKey<RM>;
|
|
120
|
+
}) => ProjectionState<RM>) | undefined;
|
|
121
|
+
/**
|
|
122
|
+
* The `evolve` failures that reject their event rather than fail the row —
|
|
123
|
+
* an update to a row that was never created, say. A rejected event is
|
|
124
|
+
* skipped and reported in the outcome's `rejected`; the fold continues, and
|
|
125
|
+
* a row that does not exist is not created by rejected events alone. The
|
|
126
|
+
* decision must be deterministic: a rejected event can be read again (to
|
|
127
|
+
* fill a later gap) and is rejected again. Any other failure fails the row,
|
|
128
|
+
* and keeps failing it on every redelivery.
|
|
129
|
+
*
|
|
130
|
+
* A row that is never created (every one of its events rejected) has no
|
|
131
|
+
* checkpoint, so each later delivery of its events gap-reads from version 1
|
|
132
|
+
* and reports the earlier rejections again — dedupe `rejected` by version
|
|
133
|
+
* when counting or logging them; the read grows with the stream.
|
|
134
|
+
*/
|
|
135
|
+
readonly terminal?: ((error: E) => boolean) | undefined;
|
|
136
|
+
/**
|
|
137
|
+
* Retries after a lost race — another invocation advanced the row between
|
|
138
|
+
* this one's read and its conditional write. Each retry re-reads the row, so
|
|
139
|
+
* it usually finds the events applied and has nothing left to write. A
|
|
140
|
+
* number `n` is `Schedule.recurs(n)`. Default: up to 12 retries after a
|
|
141
|
+
* jittered delay growing from 5 ms by 1.5× (about 1.3 s in all). Exhausted,
|
|
142
|
+
* the row fails with `ConcurrentModification`.
|
|
143
|
+
*/
|
|
144
|
+
readonly retry?: number | Schedule.Schedule<unknown, ConcurrentModification> | undefined;
|
|
145
|
+
/**
|
|
146
|
+
* A numeric model field the library owns: every write sets it one higher
|
|
147
|
+
* and is conditioned on the value it read (`attribute_not_exists` when the
|
|
148
|
+
* row has none). It may be an existing write counter readers already use.
|
|
149
|
+
* With it, a write is guarded by the revision instead of the checkpoint, so
|
|
150
|
+
* a `rebuild` can't be undone by a concurrent writer. Required by `outbox`.
|
|
151
|
+
* `{ field, initial }` sets the value a row without one is written with
|
|
152
|
+
* (default `1`) — `0`, say, to match a counter readers already see.
|
|
153
|
+
*/
|
|
154
|
+
readonly revision?: ProjectionRevision<ProjectionState<RM>> | undefined;
|
|
155
|
+
/**
|
|
156
|
+
* Allow `evolve` to remove the row (`EventStore.removeRow`): the library
|
|
157
|
+
* writes a tombstone at the row's key that keeps its checkpoint and
|
|
158
|
+
* revision — read as no row, by this projection and by the entity — so a
|
|
159
|
+
* redelivery can't recreate it. `ttl` lets DynamoDB expire tombstones; it
|
|
160
|
+
* must exceed the change feed's 24 hours plus any redelivery or replay
|
|
161
|
+
* window, or a very late event recreates the row.
|
|
162
|
+
*/
|
|
163
|
+
readonly removal?: {
|
|
164
|
+
readonly ttl?: Duration.Input | undefined;
|
|
165
|
+
} | undefined;
|
|
166
|
+
/** Side effects committed with the row — see {@link ProjectionOutboxConfig}. */
|
|
167
|
+
readonly outbox?: ProjectionOutboxConfig<O, StreamEvent<TEvent, StreamMetadata<TMetadata>>, ProjectionState<RM>> | undefined;
|
|
168
|
+
}
|
|
169
|
+
/** A projection's `revision` — the field, or the field and its starting value. */
|
|
170
|
+
export type ProjectionRevision<S> = CheckpointField<S> | {
|
|
171
|
+
readonly field: CheckpointField<S>;
|
|
172
|
+
readonly initial?: number | undefined;
|
|
173
|
+
};
|
|
174
|
+
/** @internal `revision`, resolved. */
|
|
175
|
+
export declare const resolveRevision: (revision: ProjectionRevision<any> | undefined) => {
|
|
176
|
+
readonly field: string;
|
|
177
|
+
readonly initial: number;
|
|
178
|
+
} | undefined;
|
|
179
|
+
/**
|
|
180
|
+
* An ordered, at-least-once outbox (#155). `effects(event, next, context)`
|
|
181
|
+
* returns the side effects an applied event produces (`next` is the row's
|
|
182
|
+
* state after it, `undefined` when it removed the row); they are written —
|
|
183
|
+
* encoded through `schema` — as outbox items in the same transaction as the
|
|
184
|
+
* row, so state and effects commit together. `drain` (or `deliver` on
|
|
185
|
+
* `applyBatch` / `processBatch`) hands them to a `deliver` function strictly
|
|
186
|
+
* in order per row, removing each only after it succeeds.
|
|
187
|
+
*
|
|
188
|
+
* Outbox items live in the row's own partition (sort key
|
|
189
|
+
* `<row sk>#outbox#<revision>#<n>`, `__edd_e__` `<entityType>#outbox`) — or
|
|
190
|
+
* in `table` when given (keyed by `keys`, default `pk` / `sk`). In the read
|
|
191
|
+
* model's table they show in its change stream and in full-table scans (not
|
|
192
|
+
* in the entity's reads, which filter on `__edd_e__`); a separate table keeps
|
|
193
|
+
* them out of both. `index` (a sparse index whose key attributes only outbox
|
|
194
|
+
* items carry) enables `drainPending`. A claim lasts `claimFor` (default 30
|
|
195
|
+
* seconds): a drainer that crashes after delivering leaves it to expire, and
|
|
196
|
+
* the effect is delivered again.
|
|
197
|
+
*/
|
|
198
|
+
export interface ProjectionOutboxConfig<O, Ev, S> {
|
|
199
|
+
readonly schema: Schema.Codec<O, any>;
|
|
200
|
+
readonly effects: (event: Ev, next: S | undefined, context: {
|
|
201
|
+
readonly source: string;
|
|
202
|
+
readonly streamId: string;
|
|
203
|
+
}) => ReadonlyArray<O>;
|
|
204
|
+
readonly table?: Table<any, any> | undefined;
|
|
205
|
+
readonly keys?: {
|
|
206
|
+
readonly pk: string;
|
|
207
|
+
readonly sk: string;
|
|
208
|
+
} | undefined;
|
|
209
|
+
readonly index?: {
|
|
210
|
+
readonly name: string;
|
|
211
|
+
readonly pk: string;
|
|
212
|
+
readonly sk: string;
|
|
213
|
+
} | undefined;
|
|
214
|
+
readonly claimFor?: Duration.Input | undefined;
|
|
215
|
+
}
|
|
216
|
+
/** An event `evolve` rejected — see {@link ProjectionConfig.terminal}. */
|
|
217
|
+
export interface ProjectionRejected<A, M, E> {
|
|
218
|
+
readonly event: StreamEvent<A, M>;
|
|
219
|
+
readonly error: E;
|
|
220
|
+
}
|
|
221
|
+
/** One row applied by {@link EventProjection.applyBatch} / `apply`. */
|
|
222
|
+
export interface ProjectionApplied<S, A, M, K = unknown, E = unknown> {
|
|
223
|
+
/** The stream instance (`StreamEvent.streamId`). */
|
|
224
|
+
readonly streamId: string;
|
|
225
|
+
/** The row's key. */
|
|
226
|
+
readonly key: K;
|
|
227
|
+
/**
|
|
228
|
+
* The events this call applied to the row, in version order — including
|
|
229
|
+
* any it read from the store to fill a gap. Each event is in exactly one
|
|
230
|
+
* call's `applied`: the one whose conditional write won. Side effects driven
|
|
231
|
+
* from it are at most once (a crash after the write loses them).
|
|
232
|
+
*/
|
|
233
|
+
readonly applied: ReadonlyArray<StreamEvent<A, M>>;
|
|
234
|
+
/**
|
|
235
|
+
* The events `evolve` rejected (see {@link ProjectionConfig.terminal}), in
|
|
236
|
+
* version order. Reported by every call that reads them — again, if a row
|
|
237
|
+
* that does not exist is not created before the next read.
|
|
238
|
+
*/
|
|
239
|
+
readonly rejected: ReadonlyArray<ProjectionRejected<A, M, E>>;
|
|
240
|
+
/**
|
|
241
|
+
* The batch's events this row had already applied (version at or below the
|
|
242
|
+
* checkpoint it read), in version order — a redelivery, or a late
|
|
243
|
+
* out-of-order delivery. Republishing their side effects gives at-least-once
|
|
244
|
+
* delivery, for consumers that tolerate duplicates.
|
|
245
|
+
*/
|
|
246
|
+
readonly alreadyApplied: ReadonlyArray<StreamEvent<A, M>>;
|
|
247
|
+
/**
|
|
248
|
+
* The row as written, or as read when nothing was left to apply.
|
|
249
|
+
* `Option.none()` when there is no row: every event was rejected, or the
|
|
250
|
+
* row is removed (`removed`).
|
|
251
|
+
*/
|
|
252
|
+
readonly state: Option.Option<S>;
|
|
253
|
+
/**
|
|
254
|
+
* The row's checkpoint, in store numbering (offset removed): every stream
|
|
255
|
+
* version up to it has been considered for this row.
|
|
256
|
+
*/
|
|
257
|
+
readonly version: number;
|
|
258
|
+
/** The row is removed: a tombstone keeps its checkpoint (see `removal`). */
|
|
259
|
+
readonly removed: boolean;
|
|
260
|
+
/** Outbox effects committed with this write. */
|
|
261
|
+
readonly enqueued: number;
|
|
262
|
+
/**
|
|
263
|
+
* With `deliver`: the effect delivery stopped at — it stays pending, at the
|
|
264
|
+
* head of the row's outbox, for the next drain. The write itself succeeded.
|
|
265
|
+
*/
|
|
266
|
+
readonly undelivered: Option.Option<{
|
|
267
|
+
readonly meta: OutboxMeta;
|
|
268
|
+
readonly error: unknown;
|
|
269
|
+
}>;
|
|
270
|
+
}
|
|
271
|
+
/** The outcome of one row in {@link ProjectionBatchResult}. */
|
|
272
|
+
export type ProjectionOutcome<S, A, M, Err, K = unknown, E = unknown> = ({
|
|
273
|
+
readonly _tag: "Applied";
|
|
274
|
+
/** Positions of this row's events in the `applyBatch` input, ascending. */
|
|
275
|
+
readonly positions: ReadonlyArray<number>;
|
|
276
|
+
} & ProjectionApplied<S, A, M, K, E>) | {
|
|
277
|
+
readonly _tag: "Failed";
|
|
278
|
+
readonly streamId: string;
|
|
279
|
+
/** The row's key — `undefined` when the stream id could not be parsed. */
|
|
280
|
+
readonly key: K | undefined;
|
|
281
|
+
/** Positions of this row's events in the `applyBatch` input, ascending. */
|
|
282
|
+
readonly positions: ReadonlyArray<number>;
|
|
283
|
+
readonly error: Err;
|
|
284
|
+
};
|
|
285
|
+
/** Result of {@link EventProjection.applyBatch}. */
|
|
286
|
+
export interface ProjectionBatchResult<S, A, M, Err, K = unknown, E = unknown> {
|
|
287
|
+
/** One outcome per row, in order of first delivery. */
|
|
288
|
+
readonly outcomes: ReadonlyArray<ProjectionOutcome<S, A, M, Err, K, E>>;
|
|
289
|
+
/**
|
|
290
|
+
* The earliest input position of each failed row, ascending — the records
|
|
291
|
+
* to report in a partial batch response.
|
|
292
|
+
*/
|
|
293
|
+
readonly failedPositions: ReadonlyArray<number>;
|
|
294
|
+
/** Input positions of the events whose `key` returned `Option.none()`, ascending. */
|
|
295
|
+
readonly ignored: ReadonlyArray<number>;
|
|
296
|
+
}
|
|
297
|
+
/** Result of {@link EventProjection.catchUp}. */
|
|
298
|
+
export interface ProjectionCaughtUp<S, A, M, K = unknown, E = unknown> {
|
|
299
|
+
/** The row of the stream's latest event — `Option.none()` for a stream with no events. */
|
|
300
|
+
readonly key: Option.Option<K>;
|
|
301
|
+
/** The events folded, in version order. Empty when the row was current. */
|
|
302
|
+
readonly applied: ReadonlyArray<StreamEvent<A, M>>;
|
|
303
|
+
/** The events `evolve` rejected, in version order. */
|
|
304
|
+
readonly rejected: ReadonlyArray<ProjectionRejected<A, M, E>>;
|
|
305
|
+
/** The row — `Option.none()` when there is none, or it is removed. */
|
|
306
|
+
readonly state: Option.Option<S>;
|
|
307
|
+
readonly version: number;
|
|
308
|
+
/** The row is removed (a tombstone). */
|
|
309
|
+
readonly removed: boolean;
|
|
310
|
+
}
|
|
311
|
+
/** One row re-derived by {@link EventProjection.rebuild}. */
|
|
312
|
+
export interface ProjectionRebuiltRow<S, A, M, K = unknown, E = unknown> {
|
|
313
|
+
readonly key: K;
|
|
314
|
+
/** The row as written — as read, when every one of its events was rejected. */
|
|
315
|
+
readonly state: Option.Option<S>;
|
|
316
|
+
/** The version of the row's last event. */
|
|
317
|
+
readonly version: number;
|
|
318
|
+
readonly rejected: ReadonlyArray<ProjectionRejected<A, M, E>>;
|
|
319
|
+
/** The fold ended removed: a tombstone was written. */
|
|
320
|
+
readonly removed: boolean;
|
|
321
|
+
}
|
|
322
|
+
/** Result of {@link EventProjection.rebuild}. */
|
|
323
|
+
export interface ProjectionRebuilt<S, A = unknown, M = unknown, K = unknown, E = unknown> {
|
|
324
|
+
/** Every row the stream's events address, in order of their first event. Empty for a stream with no events. */
|
|
325
|
+
readonly rows: ReadonlyArray<ProjectionRebuiltRow<S, A, M, K, E>>;
|
|
326
|
+
}
|
|
327
|
+
/** What a projection's operations fail with: `evolve`'s `E`, the read model's errors, and the store's. */
|
|
328
|
+
export type ProjectionError<RM, E> = E | ProjectionReadModelError<RM> | DynamoClientError | ValidationError | ConcurrentModification;
|
|
329
|
+
/** Options of {@link EventProjection.applyBatch}. */
|
|
330
|
+
export interface ApplyBatchOptions {
|
|
331
|
+
/** Rows applied concurrently. Default `10`. */
|
|
332
|
+
readonly concurrency?: number | "unbounded" | undefined;
|
|
333
|
+
/**
|
|
334
|
+
* Drain each row that committed outbox effects right after its write,
|
|
335
|
+
* through this function (see {@link EventProjection.drain}). A delivery
|
|
336
|
+
* failure is reported in the outcome's `undelivered`; it never fails the
|
|
337
|
+
* row — the effects are committed, and stay pending for the next drain.
|
|
338
|
+
*/
|
|
339
|
+
readonly deliver?: ((effect: any, meta: OutboxMeta) => Effect.Effect<void, any, any>) | undefined;
|
|
340
|
+
}
|
|
341
|
+
/**
|
|
342
|
+
* A DynamoDB Streams record as a Lambda event source mapping or an
|
|
343
|
+
* EventBridge Pipe delivers it — only the members the projection reads.
|
|
344
|
+
* Structural, so no `aws-lambda` types are needed. A record of another shape
|
|
345
|
+
* (a Pipe input template's) is read through `image` / `identifier` accessors.
|
|
346
|
+
*/
|
|
347
|
+
export interface StreamRecord {
|
|
348
|
+
readonly eventID?: string | undefined;
|
|
349
|
+
readonly eventName?: string | undefined;
|
|
350
|
+
readonly dynamodb?: {
|
|
351
|
+
readonly SequenceNumber?: string | undefined;
|
|
352
|
+
readonly NewImage?: StreamImage | undefined;
|
|
353
|
+
} | undefined;
|
|
354
|
+
}
|
|
355
|
+
/**
|
|
356
|
+
* Who reads the partial batch response. A Lambda event source mapping on a
|
|
357
|
+
* DynamoDB stream identifies a record by `dynamodb.SequenceNumber`; an
|
|
358
|
+
* EventBridge Pipe by `eventID`.
|
|
359
|
+
*/
|
|
360
|
+
export type BatchResponseSource = "lambda" | "pipe";
|
|
361
|
+
/** The `ReportBatchItemFailures` response. */
|
|
362
|
+
export interface BatchItemFailuresResponse {
|
|
363
|
+
readonly batchItemFailures: Array<{
|
|
364
|
+
readonly itemIdentifier: string;
|
|
365
|
+
}>;
|
|
366
|
+
}
|
|
367
|
+
/** Identify records by their source's member — see {@link BatchResponseSource}. */
|
|
368
|
+
export interface IdentifyBySource {
|
|
369
|
+
readonly source: BatchResponseSource;
|
|
370
|
+
}
|
|
371
|
+
/** Identify records with an accessor — for a record shape of your own. */
|
|
372
|
+
export interface IdentifyWith<Rec> {
|
|
373
|
+
readonly identifier: (record: Rec) => string | undefined;
|
|
374
|
+
}
|
|
375
|
+
/** Options of {@link EventProjection.processBatch}, beside how records are identified. */
|
|
376
|
+
export interface ProcessBatchOptions<Rec> extends ApplyBatchOptions {
|
|
377
|
+
/**
|
|
378
|
+
* A record's image, `undefined` to skip it. Default: the `NewImage` of an
|
|
379
|
+
* `INSERT` (a record without an `eventName` counts as one).
|
|
380
|
+
*/
|
|
381
|
+
readonly image?: ((record: Rec) => StreamImage | undefined) | undefined;
|
|
382
|
+
/**
|
|
383
|
+
* What to do with a record whose image does not decode. A decode failure is
|
|
384
|
+
* deterministic, so `"report"` (the default) makes the source redeliver it
|
|
385
|
+
* — and everything after it on its shard — until its retries or age run out.
|
|
386
|
+
* `"skip"` acknowledges it, to log and dead-letter it yourself. Either way it
|
|
387
|
+
* is in `decodeFailures`.
|
|
388
|
+
*
|
|
389
|
+
* Skipping acknowledges the record, not the stored event: the next event of
|
|
390
|
+
* the same row gap-fills through it, fails to decode it, and fails the row
|
|
391
|
+
* (that record is reported). It helps when the bad event is its stream's
|
|
392
|
+
* last, or once the stored item is repaired or removed.
|
|
393
|
+
*/
|
|
394
|
+
readonly onDecodeFailure?: "report" | "skip" | undefined;
|
|
395
|
+
}
|
|
396
|
+
/** Result of {@link EventProjection.processBatch}. */
|
|
397
|
+
export interface ProcessBatchResult<S, A, M, Err, Rec, K = unknown, E = unknown> {
|
|
398
|
+
/**
|
|
399
|
+
* See {@link ProjectionBatchResult.outcomes} — with `positions` indexing the
|
|
400
|
+
* **records** passed in.
|
|
401
|
+
*/
|
|
402
|
+
readonly outcomes: ReadonlyArray<ProjectionOutcome<S, A, M, Err, K, E>>;
|
|
403
|
+
/** Records whose image failed to decode. */
|
|
404
|
+
readonly decodeFailures: ReadonlyArray<{
|
|
405
|
+
readonly record: Rec;
|
|
406
|
+
readonly error: ValidationError;
|
|
407
|
+
}>;
|
|
408
|
+
/**
|
|
409
|
+
* The records the response reports, in delivery order: the earliest record
|
|
410
|
+
* of each failed row, and (with `onDecodeFailure: "report"`) every record
|
|
411
|
+
* that failed to decode.
|
|
412
|
+
*/
|
|
413
|
+
readonly failedRecords: ReadonlyArray<Rec>;
|
|
414
|
+
/**
|
|
415
|
+
* The partial batch response for `failedRecords`. To fail more records (a
|
|
416
|
+
* side effect that failed, say), pass them with these to {@link batchResponse}.
|
|
417
|
+
*/
|
|
418
|
+
readonly response: BatchItemFailuresResponse;
|
|
419
|
+
}
|
|
420
|
+
/**
|
|
421
|
+
* The partial batch response (`ReportBatchItemFailures`) for `failedRecords`:
|
|
422
|
+
* each identified by `dynamodb.SequenceNumber` for a Lambda event source
|
|
423
|
+
* mapping, by `eventID` for an EventBridge Pipe, or by your own `identifier`
|
|
424
|
+
* accessor.
|
|
425
|
+
*
|
|
426
|
+
* Lambda checkpoints at the lowest reported sequence number and redelivers
|
|
427
|
+
* every record from there, so reporting the earliest failed record of each
|
|
428
|
+
* row is enough. A record without an identifier fails with
|
|
429
|
+
* `StreamRecordIdentifierMissing` — an empty one would fail the whole batch
|
|
430
|
+
* silently — so fail the invocation, and the source redelivers the batch.
|
|
431
|
+
*/
|
|
432
|
+
export declare const batchResponse: {
|
|
433
|
+
(failedRecords: ReadonlyArray<StreamRecord>, options: IdentifyBySource): Effect.Effect<BatchItemFailuresResponse, StreamRecordIdentifierMissing>;
|
|
434
|
+
<Rec>(failedRecords: ReadonlyArray<Rec>, options: IdentifyWith<Rec>): Effect.Effect<BatchItemFailuresResponse, StreamRecordIdentifierMissing>;
|
|
435
|
+
};
|
|
436
|
+
/** A stream {@link decoder} can dispatch to — an `EventStream` or a `BoundEventStream`. */
|
|
437
|
+
export interface DecodableStream {
|
|
438
|
+
readonly streamName: string;
|
|
439
|
+
readonly eventMarker: string;
|
|
440
|
+
decodeStreamImage(image: StreamImage): Effect.Effect<Option.Option<unknown>, ValidationError, any>;
|
|
441
|
+
}
|
|
442
|
+
/** The event a decodable stream decodes to. */
|
|
443
|
+
type DecodedEventOf<S> = S extends {
|
|
444
|
+
decodeStreamImage(image: any, options?: any): Effect.Effect<Option.Option<infer Ev>, any, any>;
|
|
445
|
+
} ? Ev : never;
|
|
446
|
+
/** What {@link decoder} returns for one of its streams' event images. */
|
|
447
|
+
export type DecodedStreamEvent<Streams extends {
|
|
448
|
+
readonly [name: string]: DecodableStream;
|
|
449
|
+
}> = {
|
|
450
|
+
readonly [K in keyof Streams & string]: {
|
|
451
|
+
readonly stream: K;
|
|
452
|
+
readonly event: DecodedEventOf<Streams[K]>;
|
|
453
|
+
};
|
|
454
|
+
}[keyof Streams & string];
|
|
455
|
+
/**
|
|
456
|
+
* One decoder for several streams' change-feed images. Name the streams in a
|
|
457
|
+
* record; `decode(image)` dispatches on the image's `__edd_e__` and returns
|
|
458
|
+
* `{ stream: <name>, event }`, or `Option.none()` for anything that is not one
|
|
459
|
+
* of their events (snapshots, idempotency sentinels, entities, other streams).
|
|
460
|
+
* The image is decoded by that stream's `decodeStreamImage`, so it accepts the
|
|
461
|
+
* same shapes (DynamoDB JSON with `B` as base64 or bytes, or unmarshalled).
|
|
462
|
+
*
|
|
463
|
+
* Two streams storing events under the same marker throw `[EDD-9082]`.
|
|
464
|
+
*
|
|
465
|
+
* @example
|
|
466
|
+
* ```ts
|
|
467
|
+
* const decode = EventStore.decoder({ Order: OrderEvents, Payment: PaymentEvents })
|
|
468
|
+
* const decoded = yield* decode(record.dynamodb.NewImage)
|
|
469
|
+
* if (Option.isSome(decoded) && decoded.value.stream === "Order") { ... }
|
|
470
|
+
* ```
|
|
471
|
+
*/
|
|
472
|
+
export declare const decoder: <const Streams extends {
|
|
473
|
+
readonly [name: string]: DecodableStream;
|
|
474
|
+
}>(streams: Streams) => ((image: StreamImage) => Effect.Effect<Option.Option<DecodedStreamEvent<Streams>>, ValidationError, never>);
|
|
475
|
+
/** A Lambda event source mapping / EventBridge Pipe filter pattern. */
|
|
476
|
+
export interface EventFilterPattern {
|
|
477
|
+
readonly eventName: ReadonlyArray<"INSERT">;
|
|
478
|
+
readonly dynamodb: {
|
|
479
|
+
readonly NewImage: {
|
|
480
|
+
readonly __edd_e__: {
|
|
481
|
+
readonly S: ReadonlyArray<string>;
|
|
482
|
+
};
|
|
483
|
+
};
|
|
484
|
+
};
|
|
485
|
+
}
|
|
486
|
+
/**
|
|
487
|
+
* The filter pattern that selects the `INSERT`s of these streams' event items
|
|
488
|
+
* — `JSON.stringify` it into `FilterCriteria.Filters[].Pattern` of a Lambda
|
|
489
|
+
* event source mapping or an EventBridge Pipe. Snapshots, idempotency
|
|
490
|
+
* sentinels and other items are filtered out before the function runs.
|
|
491
|
+
*/
|
|
492
|
+
export declare const eventFilterPattern: (streams: ReadonlyArray<DecodableStream> | {
|
|
493
|
+
readonly [name: string]: DecodableStream;
|
|
494
|
+
}) => EventFilterPattern;
|
|
495
|
+
declare const EventProjectionTypeId: unique symbol;
|
|
496
|
+
export type EventProjectionTypeId = typeof EventProjectionTypeId;
|
|
497
|
+
/**
|
|
498
|
+
* @internal How a projection takes part in a multi-projection batch: one
|
|
499
|
+
* route per source stream — its decode (under the bound context for a bound
|
|
500
|
+
* projection) and its `applyBatch`.
|
|
501
|
+
*/
|
|
502
|
+
export declare const ProjectionRoutes: unique symbol;
|
|
503
|
+
/**
|
|
504
|
+
* A change-feed projection: one source stream folded into read-model rows —
|
|
505
|
+
* see {@link projection}. Its operations need `DynamoClient | TableConfig`
|
|
506
|
+
* (the stream's table and the read model's) and `evolve`'s requirements;
|
|
507
|
+
* {@link bindProjection} resolves them (`R = never`).
|
|
508
|
+
*/
|
|
509
|
+
export interface EventProjection<S, A, TStreamIdFields extends ReadonlyArray<string>, M, Err, R, K = unknown, E = unknown, I extends boolean = false, O = never> extends Pipeable.Pipeable {
|
|
510
|
+
readonly [EventProjectionTypeId]: EventProjectionTypeId;
|
|
511
|
+
/** The source stream. */
|
|
512
|
+
readonly stream: EventStream<A, TStreamIdFields, any, any, any, any, any>;
|
|
513
|
+
/** @internal See {@link ProjectionRoutes}. */
|
|
514
|
+
readonly [ProjectionRoutes]: (name: string) => ReadonlyArray<Route>;
|
|
515
|
+
/** The read model's entity type. */
|
|
516
|
+
readonly entityType: string;
|
|
517
|
+
/**
|
|
518
|
+
* Apply one stream's decoded change-feed events — a batch, in delivery
|
|
519
|
+
* order. Per row (`key(streamId, event)`, concurrently across rows):
|
|
520
|
+
*
|
|
521
|
+
* 1. sort and deduplicate the row's events by version;
|
|
522
|
+
* 2. read the row (strongly consistent) and its checkpoint `c`; the events
|
|
523
|
+
* at or below `c` are `alreadyApplied`, and with none left the row is
|
|
524
|
+
* done;
|
|
525
|
+
* 3. if the rest are not exactly `c + 1 …` their highest version, read
|
|
526
|
+
* `c + 1 …` that version from the store with one strongly consistent
|
|
527
|
+
* `readFrom` and keep the row's events — so nothing waits for a delivery;
|
|
528
|
+
* 4. fold through `evolve` (skipping `terminal` rejections), set the
|
|
529
|
+
* checkpoint, and write the row once, conditioned on the checkpoint still
|
|
530
|
+
* being `c` (`create` for a new row; nothing when a new row's events were
|
|
531
|
+
* all rejected);
|
|
532
|
+
* 5. on a lost race, re-read and continue, under the `retry` policy.
|
|
533
|
+
*
|
|
534
|
+
* Never fails: each row's failure is its outcome, so one row's error can't
|
|
535
|
+
* hide the others' `applied`.
|
|
536
|
+
*/
|
|
537
|
+
applyBatch(events: ReadonlyArray<StreamEvent<A, M>>, options?: ApplyBatchOptions | undefined): Effect.Effect<ProjectionBatchResult<S, A, M, Err, K, E>, never, R>;
|
|
538
|
+
/** {@link EventProjection.applyBatch} of one event, its failure in the error channel. */
|
|
539
|
+
apply(event: StreamEvent<A, M>): Effect.Effect<I extends true ? Option.Option<ProjectionApplied<S, A, M, K, E>> : ProjectionApplied<S, A, M, K, E>, Err, R>;
|
|
540
|
+
/**
|
|
541
|
+
* Bring the row of the stream's latest event up to the head: read that
|
|
542
|
+
* event (one strongly consistent `Query`), then the row's events after its
|
|
543
|
+
* checkpoint (one strongly consistent `readFrom`) — to repair a row whose
|
|
544
|
+
* last event never arrived (a dead-lettered record, say). An earlier missing
|
|
545
|
+
* event is filled in by the next delivery anyway. With one row per stream
|
|
546
|
+
* instance that is the only row; {@link EventProjection.rebuild} re-derives
|
|
547
|
+
* every row.
|
|
548
|
+
*/
|
|
549
|
+
catchUp(streamId: StreamIdInput<TStreamIdFields>): Effect.Effect<ProjectionCaughtUp<S, A, M, K, E>, Err, R>;
|
|
550
|
+
/**
|
|
551
|
+
* Re-derive every row of a stream instance from version 1: fold the whole
|
|
552
|
+
* stream (strongly consistent) from `initial`, row by row, ignoring the rows'
|
|
553
|
+
* state, and write each conditioned on the checkpoint read. Returns no
|
|
554
|
+
* applied events, so a rebuild never replays side effects. A stream with no
|
|
555
|
+
* events writes nothing.
|
|
556
|
+
*
|
|
557
|
+
* Pause live delivery for the stream instance while it runs. The condition
|
|
558
|
+
* is the checkpoint, and a rebuild of a row already at the stream's head
|
|
559
|
+
* writes the same checkpoint back — so an `applyBatch` that read the row
|
|
560
|
+
* before the rebuild still holds a matching condition, and its later write
|
|
561
|
+
* (folded from the old state) replaces the rebuilt row.
|
|
562
|
+
*/
|
|
563
|
+
rebuild(streamId: StreamIdInput<TStreamIdFields>): Effect.Effect<ProjectionRebuilt<S, A, M, K, E>, Err, R>;
|
|
564
|
+
/**
|
|
565
|
+
* Process a raw change-feed batch: decode each `INSERT`'s `NewImage` (other
|
|
566
|
+
* records, and images that are not this stream's events, are skipped), apply
|
|
567
|
+
* the events with {@link EventProjection.applyBatch}, and build the partial
|
|
568
|
+
* batch response — the earliest record of each failed row and every record
|
|
569
|
+
* that failed to decode (unless `onDecodeFailure: "skip"`), identified per
|
|
570
|
+
* `options.source` or by `options.identifier`. Outcome `positions` index
|
|
571
|
+
* `records`. A failed record without an identifier fails the call with
|
|
572
|
+
* `StreamRecordIdentifierMissing`: fail the invocation, and the source
|
|
573
|
+
* redelivers the batch.
|
|
574
|
+
*/
|
|
575
|
+
processBatch<Rec extends StreamRecord>(records: ReadonlyArray<Rec>, options: ProcessBatchOptions<Rec> & (IdentifyBySource | IdentifyWith<Rec>)): Effect.Effect<ProcessBatchResult<S, A, M, Err, Rec, K, E>, StreamRecordIdentifierMissing, R>;
|
|
576
|
+
/** A record shape of your own, read through `image` and `identifier`. */
|
|
577
|
+
processBatch<Rec>(records: ReadonlyArray<Rec>, options: ProcessBatchOptions<Rec> & {
|
|
578
|
+
readonly image: (record: Rec) => StreamImage | undefined;
|
|
579
|
+
} & IdentifyWith<Rec>): Effect.Effect<ProcessBatchResult<S, A, M, Err, Rec, K, E>, StreamRecordIdentifierMissing, R>;
|
|
580
|
+
/**
|
|
581
|
+
* Deliver a row's pending outbox effects (see `outbox`), strictly in order:
|
|
582
|
+
* claim the head item, `deliver` it, delete it, repeat — stopping at a
|
|
583
|
+
* failure (reported in `undelivered`, the effect left pending) or at a head
|
|
584
|
+
* another drainer holds. A crash after delivering leaves a claim that
|
|
585
|
+
* expires (`claimFor`), and the effect is delivered again: at least once.
|
|
586
|
+
*/
|
|
587
|
+
drain<DE, DR>(key: K, deliver: (effect: O, meta: OutboxMeta) => Effect.Effect<void, DE, DR>): Effect.Effect<DrainResult<DE>, DynamoClientError | ValidationError, R | DR>;
|
|
588
|
+
/**
|
|
589
|
+
* Drain every row with pending effects, found through the outbox's sparse
|
|
590
|
+
* `index` — the sweeper for rows that receive no further events.
|
|
591
|
+
*/
|
|
592
|
+
drainPending<DE, DR>(deliver: (effect: O, meta: OutboxMeta) => Effect.Effect<void, DE, DR>): Effect.Effect<{
|
|
593
|
+
readonly delivered: number;
|
|
594
|
+
readonly undelivered: ReadonlyArray<{
|
|
595
|
+
readonly meta: OutboxMeta;
|
|
596
|
+
readonly error: DE;
|
|
597
|
+
}>;
|
|
598
|
+
}, DynamoClientError | ValidationError, R | DR>;
|
|
599
|
+
}
|
|
600
|
+
/** @internal Refusals shared by `projection` and `multiProjection` (`[EDD-9081]`). */
|
|
601
|
+
export declare const refuseReadModel: (label: string, readModel: ProjectionReadModel, options: {
|
|
602
|
+
readonly fields: ReadonlyArray<string>;
|
|
603
|
+
readonly removal: boolean;
|
|
604
|
+
readonly outbox: boolean;
|
|
605
|
+
readonly revision: string | undefined;
|
|
606
|
+
}) => void;
|
|
607
|
+
/** @internal An outbox config, resolved. */
|
|
608
|
+
export declare const resolveOutbox: (outbox: ProjectionOutboxConfig<any, any, any> | undefined) => {
|
|
609
|
+
schema: Schema.Codec<any, any, never, never>;
|
|
610
|
+
effects: (event: any, next: any, context: {
|
|
611
|
+
readonly source: string;
|
|
612
|
+
readonly streamId: string;
|
|
613
|
+
}) => readonly any[];
|
|
614
|
+
table: Table<any, any> | undefined;
|
|
615
|
+
pk: string;
|
|
616
|
+
sk: string;
|
|
617
|
+
index: {
|
|
618
|
+
readonly name: string;
|
|
619
|
+
readonly pk: string;
|
|
620
|
+
readonly sk: string;
|
|
621
|
+
} | undefined;
|
|
622
|
+
claimFor: Duration.Duration;
|
|
623
|
+
} | undefined;
|
|
624
|
+
/**
|
|
625
|
+
* Define a change-feed projection of `stream` into `readModel` — rows keyed by
|
|
626
|
+
* `key(streamId, event)` (one per stream instance when the key ignores the
|
|
627
|
+
* event), each folded from `initial(streamId, event)` through `evolve`, its
|
|
628
|
+
* checkpoint kept in `checkpoint`. See {@link EventProjection} for the
|
|
629
|
+
* operations.
|
|
630
|
+
*
|
|
631
|
+
* The read model must be a plain entity: `versioned` (the checkpoint is the
|
|
632
|
+
* version), `softDelete` (removal is `removal` tombstones) and `timeSeries`
|
|
633
|
+
* read models throw `[EDD-9081]`, as do a field the model lacks, a
|
|
634
|
+
* non-integer offset, `removal` / `outbox` on a read model with `unique`
|
|
635
|
+
* constraints, and `outbox` without `revision`.
|
|
636
|
+
*
|
|
637
|
+
* @example
|
|
638
|
+
* ```ts
|
|
639
|
+
* const OrderSummaryProjection = EventStore.projection({
|
|
640
|
+
* stream: OrderEvents,
|
|
641
|
+
* readModel: OrderSummaries,
|
|
642
|
+
* key: (id) => ({ orderId: id.orderId }),
|
|
643
|
+
* initial: (id) => ({ orderId: id.orderId, status: "open", total: 0, version: 0 }),
|
|
644
|
+
* evolve: (state, event) => Effect.succeed(next(state, event.data)),
|
|
645
|
+
* checkpoint: "version",
|
|
646
|
+
* })
|
|
647
|
+
* ```
|
|
648
|
+
*/
|
|
649
|
+
export declare const projection: <TEvent, TStreamIdFields extends ReadonlyArray<string>, TMetadata, RM extends ProjectionReadModel, E = never, R = never, KR extends ProjectionKey<RM> | Option.Option<ProjectionKey<RM>> = ProjectionKey<RM>, O = never>(config: ProjectionConfig<TEvent, TStreamIdFields, TMetadata, RM, E, R, KR, O>) => EventProjection<ProjectionState<RM>, TEvent, TStreamIdFields, StreamMetadata<TMetadata>, ProjectionError<RM, E>, R | DynamoClient | TableConfig, ProjectionKey<RM>, E, [Extract<KR, Option.Option<unknown>>] extends [never] ? false : true, O>;
|
|
650
|
+
/** Any projection, bound or unbound — single-source, or multi-source. */
|
|
651
|
+
export type AnyEventProjection = EventProjection<any, any, any, any, any, any, any, any, any, any> | {
|
|
652
|
+
readonly [ProjectionRoutes]: (name: string) => ReadonlyArray<Route>;
|
|
653
|
+
applyBatch(events: ReadonlyArray<any>, options?: any): Effect.Effect<any, never, any>;
|
|
654
|
+
};
|
|
655
|
+
/** A projection's outcomes. */
|
|
656
|
+
export type ProjectionOutcomesOf<P> = P extends EventProjection<infer S, infer A, any, infer M, infer Err, any, infer K, infer E, any, any> ? ReadonlyArray<ProjectionOutcome<S, A, M, Err, K, E>> : P extends {
|
|
657
|
+
applyBatch(events: any, options?: any): Effect.Effect<{
|
|
658
|
+
readonly outcomes: infer Outcomes;
|
|
659
|
+
}, never, any>;
|
|
660
|
+
} ? Outcomes : never;
|
|
661
|
+
/** A projection's requirements. */
|
|
662
|
+
export type ProjectionRequirementsOf<P> = P extends EventProjection<any, any, any, any, any, infer R, any, any, any, any> ? R : P extends {
|
|
663
|
+
applyBatch(events: any, options?: any): Effect.Effect<any, never, infer R>;
|
|
664
|
+
} ? R : never;
|
|
665
|
+
/** Result of {@link processBatch}. */
|
|
666
|
+
export interface ProcessBatchesResult<Projections extends {
|
|
667
|
+
readonly [name: string]: AnyEventProjection;
|
|
668
|
+
}, Rec, Streams extends {
|
|
669
|
+
readonly [name: string]: DecodableStream;
|
|
670
|
+
} = {}> {
|
|
671
|
+
/**
|
|
672
|
+
* The events of the `streams` named without a projection, in delivery order,
|
|
673
|
+
* each with its record's position — so a caller acting on them needs no
|
|
674
|
+
* second decode.
|
|
675
|
+
*/
|
|
676
|
+
readonly unrouted: ReadonlyArray<{
|
|
677
|
+
readonly position: number;
|
|
678
|
+
} & DecodedStreamEvent<Streams>>;
|
|
679
|
+
/**
|
|
680
|
+
* Each projection's outcomes — `positions` index the **records** passed in.
|
|
681
|
+
* A projection whose stream had no events in the batch has none.
|
|
682
|
+
*/
|
|
683
|
+
readonly outcomes: {
|
|
684
|
+
readonly [K in keyof Projections]: ProjectionOutcomesOf<Projections[K]>;
|
|
685
|
+
};
|
|
686
|
+
/** Records whose image failed to decode. */
|
|
687
|
+
readonly decodeFailures: ReadonlyArray<{
|
|
688
|
+
readonly record: Rec;
|
|
689
|
+
readonly error: ValidationError;
|
|
690
|
+
}>;
|
|
691
|
+
/**
|
|
692
|
+
* The records the response reports, in delivery order: the earliest record
|
|
693
|
+
* of each failed row of every projection, and (with
|
|
694
|
+
* `onDecodeFailure: "report"`) every record that failed to decode.
|
|
695
|
+
*/
|
|
696
|
+
readonly failedRecords: ReadonlyArray<Rec>;
|
|
697
|
+
/** The partial batch response for `failedRecords`. */
|
|
698
|
+
readonly response: BatchItemFailuresResponse;
|
|
699
|
+
}
|
|
700
|
+
/** @internal One projection behind a feed: the stream it decodes and its `applyBatch`. */
|
|
701
|
+
export interface Route {
|
|
702
|
+
readonly name: string;
|
|
703
|
+
/**
|
|
704
|
+
* A multi-source projection's name for this stream: its events reach
|
|
705
|
+
* `applyBatch` as `{ stream: tag, event }`. Undefined: as the bare event.
|
|
706
|
+
*/
|
|
707
|
+
readonly tag?: string | undefined;
|
|
708
|
+
/** The (unbound) stream — its identity groups the projections that read it. */
|
|
709
|
+
readonly stream: DecodableStream;
|
|
710
|
+
/** How its images decode — under the bound context for a bound projection. */
|
|
711
|
+
readonly decode: (image: StreamImage) => Effect.Effect<Option.Option<unknown>, ValidationError, any>;
|
|
712
|
+
readonly applyBatch: (events: ReadonlyArray<any>, options?: ApplyBatchOptions | undefined) => Effect.Effect<ProjectionBatchResult<any, any, any, any, any, any>, never, any>;
|
|
713
|
+
}
|
|
714
|
+
/**
|
|
715
|
+
* @internal Decode a batch once, route each event to the projections of its
|
|
716
|
+
* stream, apply each projection's events, and build one partial batch
|
|
717
|
+
* response — shared by {@link processBatch} and `projection.processBatch`.
|
|
718
|
+
*/
|
|
719
|
+
export declare const runBatch: <Rec>(operation: string, records: ReadonlyArray<Rec>, options: ProcessBatchOptions<Rec> & (IdentifyBySource | IdentifyWith<Rec>), routes: ReadonlyArray<Route>,
|
|
720
|
+
/** Streams decoded without a projection: their events are returned as `unrouted`. */
|
|
721
|
+
decodeOnly?: ReadonlyArray<readonly [string, DecodableStream]>) => Effect.Effect<{
|
|
722
|
+
readonly unrouted: ReadonlyArray<{
|
|
723
|
+
readonly position: number;
|
|
724
|
+
readonly stream: string;
|
|
725
|
+
readonly event: unknown;
|
|
726
|
+
}>;
|
|
727
|
+
readonly outcomes: Record<string, ReadonlyArray<ProjectionOutcome<any, any, any, any, any, any>>>;
|
|
728
|
+
readonly decodeFailures: ReadonlyArray<{
|
|
729
|
+
readonly record: Rec;
|
|
730
|
+
readonly error: ValidationError;
|
|
731
|
+
}>;
|
|
732
|
+
readonly failedRecords: ReadonlyArray<Rec>;
|
|
733
|
+
readonly response: BatchItemFailuresResponse;
|
|
734
|
+
}, StreamRecordIdentifierMissing, any>;
|
|
735
|
+
/**
|
|
736
|
+
* Process one change-feed batch for several projections — the usual shape
|
|
737
|
+
* when one table's feed carries several streams. Each record is decoded once
|
|
738
|
+
* and routed to every projection of its stream; records of other streams and
|
|
739
|
+
* items are skipped, as acknowledged. Each projection applies its events
|
|
740
|
+
* (see {@link EventProjection.applyBatch}), and one partial batch response
|
|
741
|
+
* names the earliest record of every failed row across all of them, plus the
|
|
742
|
+
* records that failed to decode (unless `onDecodeFailure: "skip"`). Outcome
|
|
743
|
+
* `positions` index `records`.
|
|
744
|
+
*
|
|
745
|
+
* Records are read as `StreamRecord`s, identified per `source`, or through
|
|
746
|
+
* `image` / `identifier` accessors for a shape of your own (a Pipe input
|
|
747
|
+
* template's). A failed record without an identifier fails the call with
|
|
748
|
+
* `StreamRecordIdentifierMissing`. `streams` names streams to decode without a
|
|
749
|
+
* projection: their events come back as `unrouted`. Two streams storing
|
|
750
|
+
* events under one marker die with `[EDD-9082]`.
|
|
751
|
+
*
|
|
752
|
+
* @example
|
|
753
|
+
* ```ts
|
|
754
|
+
* const result = yield* EventStore.processBatch(event.Records, {
|
|
755
|
+
* projections: { summaries: OrderSummaryProjection, balances: BalanceProjection },
|
|
756
|
+
* source: "lambda",
|
|
757
|
+
* })
|
|
758
|
+
* return result.response
|
|
759
|
+
* ```
|
|
760
|
+
*/
|
|
761
|
+
export declare const processBatch: {
|
|
762
|
+
<const Projections extends {
|
|
763
|
+
readonly [name: string]: AnyEventProjection;
|
|
764
|
+
}, Rec extends StreamRecord, const Streams extends {
|
|
765
|
+
readonly [name: string]: DecodableStream;
|
|
766
|
+
} = {}>(records: ReadonlyArray<Rec>, options: ProcessBatchOptions<Rec> & (IdentifyBySource | IdentifyWith<Rec>) & {
|
|
767
|
+
readonly projections: Projections;
|
|
768
|
+
readonly streams?: Streams | undefined;
|
|
769
|
+
}): Effect.Effect<ProcessBatchesResult<Projections, Rec, Streams>, StreamRecordIdentifierMissing, ProjectionRequirementsOf<Projections[keyof Projections]>>;
|
|
770
|
+
<const Projections extends {
|
|
771
|
+
readonly [name: string]: AnyEventProjection;
|
|
772
|
+
}, Rec, const Streams extends {
|
|
773
|
+
readonly [name: string]: DecodableStream;
|
|
774
|
+
} = {}>(records: ReadonlyArray<Rec>, options: ProcessBatchOptions<Rec> & {
|
|
775
|
+
readonly image: (record: Rec) => StreamImage | undefined;
|
|
776
|
+
} & IdentifyWith<Rec> & {
|
|
777
|
+
readonly projections: Projections;
|
|
778
|
+
readonly streams?: Streams | undefined;
|
|
779
|
+
}): Effect.Effect<ProcessBatchesResult<Projections, Rec, Streams>, StreamRecordIdentifierMissing, ProjectionRequirementsOf<Projections[keyof Projections]>>;
|
|
780
|
+
};
|
|
781
|
+
export {};
|
|
782
|
+
//# sourceMappingURL=EventProjection.d.ts.map
|