graphddb 0.7.10 → 0.8.1
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/README.md +6 -6
- package/dist/cdc/index.d.ts +389 -5
- package/dist/cdc/index.js +4 -4
- package/dist/{chunk-AD6ZQTTE.js → chunk-GS4C5VGO.js} +2 -6
- package/dist/{chunk-DFUKGU2Q.js → chunk-HNY2EJPV.js} +216 -229
- package/dist/{chunk-3ZU2VW3L.js → chunk-L2NEDS7U.js} +582 -781
- package/dist/chunk-L4QRCHRQ.js +278 -0
- package/dist/chunk-LAT64YCZ.js +1987 -0
- package/dist/chunk-S2NI4PBW.js +187 -0
- package/dist/{chunk-EOJDN3SA.js → chunk-T44OB5GU.js} +3757 -6138
- package/dist/{chunk-PDUVTYC5.js → chunk-XTWXMOHD.js} +0 -1
- package/dist/cli.js +63 -254
- package/dist/index.d.ts +23 -1550
- package/dist/index.js +94 -1850
- package/dist/internal/index.d.ts +84 -0
- package/dist/internal/index.js +701 -0
- package/dist/{maintenance-view-adapter-BAZ9uBGe.d.ts → key-DZtjAQDh.d.ts} +573 -1817
- package/dist/linter/index.d.ts +39 -7
- package/dist/linter/index.js +22 -4
- package/dist/{registry-LWE54Sdc.d.ts → linter-DQY7gUEk.d.ts} +22 -22
- package/dist/prepared-artifact-HFealr1q.d.ts +281 -0
- package/dist/spec/index.d.ts +506 -5
- package/dist/spec/index.js +22 -18
- package/dist/testing/index.d.ts +2 -3
- package/dist/testing/index.js +4 -4
- package/dist/transform/index.d.ts +1 -1
- package/dist/transform/index.js +4 -4
- package/dist/{types-BQLzTEqh.d.ts → types-2PMXEn5x.d.ts} +8 -10
- package/dist/types-DW__-Icc.d.ts +450 -0
- package/docs/cdc-projection.md +5 -5
- package/docs/class-hydration.md +1 -1
- package/docs/cqrs-contract.md +79 -20
- package/docs/design-patterns.md +5 -5
- package/docs/docs-generation.md +6 -6
- package/docs/middleware.md +15 -15
- package/docs/mutation-command-derivation.md +52 -42
- package/docs/prepared-statements.md +14 -14
- package/docs/python-bridge.md +96 -65
- package/docs/spec.md +153 -124
- package/docs/testing.md +9 -8
- package/package.json +14 -4
- package/dist/chunk-3UD3XIF2.js +0 -860
- package/dist/chunk-MMVHOUM4.js +0 -24
- package/dist/from-change-Ty95KA8C.d.ts +0 -327
- package/dist/index-Dc7d8mWI.d.ts +0 -1089
- package/dist/relation-depth-BRS513Tq.d.ts +0 -36
package/README.md
CHANGED
|
@@ -74,13 +74,13 @@ const User = UserModel.asModel();
|
|
|
74
74
|
|
|
75
75
|
// Connect the client once. GraphDDB owns throttle / transient retries, so set
|
|
76
76
|
// `maxAttempts: 1` to avoid double retries (SDK × library) — see "Retry & throttling" below.
|
|
77
|
-
|
|
77
|
+
graphddb.config.client(new DynamoDBClient({ maxAttempts: 1 }));
|
|
78
78
|
|
|
79
79
|
// READ —— only the fields you select appear in the result type.
|
|
80
80
|
const user = await User.query({ userId: 'alice' }, { name: true });
|
|
81
81
|
|
|
82
82
|
// WRITE —— a unified envelope that writes multiple items atomically (default mode: 'transaction').
|
|
83
|
-
await
|
|
83
|
+
await graphddb.mutate({
|
|
84
84
|
user: { update: User, key: { userId: 'alice' }, input: { name: 'Alice B.' } },
|
|
85
85
|
});
|
|
86
86
|
|
|
@@ -89,7 +89,7 @@ await User.putItem({ userId: 'alice', name: 'Alice' });
|
|
|
89
89
|
```
|
|
90
90
|
|
|
91
91
|
Reads center on `query` (a single PK / unique GSI lookup) / `list` (multiple items, with a cursor).
|
|
92
|
-
Writes center on `
|
|
92
|
+
Writes center on `graphddb.mutate` (a unified envelope), while `putItem` / `updateItem` / `deleteItem` are
|
|
93
93
|
positioned secondarily as raw base operations named after the DynamoDB API. For the full API and options,
|
|
94
94
|
see [`docs/spec.md`](./docs/spec.md).
|
|
95
95
|
|
|
@@ -196,7 +196,7 @@ Each capability is summarized here only. For details, operator lists, and proced
|
|
|
196
196
|
- **Filtering** —— a declarative, type-safe server-side `filter` (compatible with `FilterExpression` /
|
|
197
197
|
AppSync `ModelFilterInput`). `Model.col` + the `cond` raw escape hatch are shared by read filters / write
|
|
198
198
|
`condition`s. For the operator list, see [`docs/spec.md`](./docs/spec.md).
|
|
199
|
-
- **Middleware / Hooks** —— `
|
|
199
|
+
- **Middleware / Hooks** —— `graphddb.config.use` for host-side hooks on reads / writes (logging, metrics, tenant /
|
|
200
200
|
authorization scoping). Host-only and non-serialized. For hook points, see [`docs/middleware.md`](./docs/middleware.md).
|
|
201
201
|
- **Class hydration** —— `options.hydrate` loads read results into host-language domain objects (opt-in,
|
|
202
202
|
Phase 1: `query` top level). [`docs/class-hydration.md`](./docs/class-hydration.md).
|
|
@@ -209,7 +209,7 @@ Each capability is summarized here only. For details, operator lists, and proced
|
|
|
209
209
|
aggregation. [`docs/cdc-emulator.md`](./docs/cdc-emulator.md).
|
|
210
210
|
- **CDC projection** —— a typed-consumer-IF that parses the CDC change events of a `@cdcProjected()`
|
|
211
211
|
source model into typed records. `Model.fromChange(event)` returns `[old, new]`, and
|
|
212
|
-
`
|
|
212
|
+
`graphddb.subscribe(handlers)` (the sibling of `query` / `mutate`) produces a `ChangeHandler` the
|
|
213
213
|
consumer mounts on its own stream. graphddb owns parse→typed record only; subscription, sink
|
|
214
214
|
delivery, and idempotency are the consumer's. [`docs/cdc-projection.md`](./docs/cdc-projection.md).
|
|
215
215
|
- **CloudFormation generation** —— `graphddb generate cloudformation` emits a CloudFormation template for
|
|
@@ -231,7 +231,7 @@ documentation available offline at hand. On GitHub, follow the links in the tabl
|
|
|
231
231
|
|----------|-------------|
|
|
232
232
|
| [Specification](./docs/spec.md) | Core API: entities, structured keys/GSIs, query/filter, relations, batch/transaction, design rules, runtime behavior. |
|
|
233
233
|
| [Design patterns](./docs/design-patterns.md) | Mapping representative DynamoDB design patterns onto graphddb features (how to declare `pattern` / `@model({ kind })` + `@maintainedFrom` / `@aggregate`, read & write maintenance, per-phase coverage). |
|
|
234
|
-
| [Middleware / Hooks](./docs/middleware.md) | The `
|
|
234
|
+
| [Middleware / Hooks](./docs/middleware.md) | The `graphddb.config.use` host-side middleware: hook points read R1–R5 / write W1–W5, `{ context }`, ordering. |
|
|
235
235
|
| [CQRS contract layer](./docs/cqrs-contract.md) | Public Query/Command contracts, cardinality matrix, N+1 safety, composition across contracts, context boundaries. |
|
|
236
236
|
| [Mutation → command derivation](./docs/mutation-command-derivation.md) | The internal write-plan composition DSL behind the Command IF (`entityWrites`, fragment composition, derivation of atomic `TransactWriteItems`). |
|
|
237
237
|
| [Class hydration](./docs/class-hydration.md) | Opt-in `options.hydrate` that loads read results into host objects (Phase 1: `query` top level). |
|
package/dist/cdc/index.d.ts
CHANGED
|
@@ -1,7 +1,351 @@
|
|
|
1
|
-
|
|
2
|
-
export {
|
|
3
|
-
import '
|
|
4
|
-
import '../types-
|
|
1
|
+
import { w as CdcEmulatorOptions, x as ChangeHandler, y as Unsubscribe, F as FaultSpec, z as ConcurrentRecomputeRef, v as ChangeEvent, A as EventLog, G as ReplayOptions, H as ShardId, I as MaintainTrigger, E as EffectPath, g as ProjectionMap, j as MembershipPredicate } from '../key-DZtjAQDh.js';
|
|
2
|
+
export { J as BatchResult, K as CdcMode, L as ChangeBatch, N as ChangeEventName, O as ClockMode, V as StartingPosition, X as StreamViewType, Y as SubscribeHandler, Z as SubscribeHandlers, _ as buildSubscribeHandler } from '../key-DZtjAQDh.js';
|
|
3
|
+
import { E as EntityMetadata } from '../types-DW__-Icc.js';
|
|
4
|
+
import '../types-2PMXEn5x.js';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* CDC emulator (issue #72). Subscribes to the core write-capture seam
|
|
8
|
+
* ({@link ChangeCaptureRegistry}) and turns each {@link RawWriteRecord} into a
|
|
9
|
+
* DynamoDB-Streams-equivalent {@link ChangeEvent}, then delivers events to a
|
|
10
|
+
* consumer under a deterministic delivery contract (spec §6): per-shard ordered,
|
|
11
|
+
* batched, at-least-once, with checkpoint/resume, partial-batch failure, and a
|
|
12
|
+
* dead-letter queue. Faults (duplicate / reorder / delay / dropThenRedeliver /
|
|
13
|
+
* partialBatchFailure) and a concurrent-recompute race are injectable and
|
|
14
|
+
* seeded for deterministic reproduction (spec §8).
|
|
15
|
+
*
|
|
16
|
+
* The emulator is dev/test only. The {@link ChangeEvent} type and delivery
|
|
17
|
+
* contract match real Streams so a consumer is portable to production (spec §1).
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* The CDC emulator (spec §10 API). Construct via {@link createCdcEmulator}.
|
|
22
|
+
*/
|
|
23
|
+
declare class CdcEmulator {
|
|
24
|
+
private readonly mode;
|
|
25
|
+
private readonly clockMode;
|
|
26
|
+
private readonly batchSize;
|
|
27
|
+
private readonly maxRetries;
|
|
28
|
+
private readonly startingPosition;
|
|
29
|
+
private readonly shardCount;
|
|
30
|
+
private readonly seed;
|
|
31
|
+
private rng;
|
|
32
|
+
private faultSpec;
|
|
33
|
+
private concurrentRecompute;
|
|
34
|
+
private concurrentRecomputeHook;
|
|
35
|
+
/** Per-shard monotonic sequence counter. */
|
|
36
|
+
private seqCounters;
|
|
37
|
+
/** Per-shard delivery queue (queued/replay modes). */
|
|
38
|
+
private queues;
|
|
39
|
+
/** Per-shard last successfully checkpointed sequence number. */
|
|
40
|
+
private checkpointMap;
|
|
41
|
+
/**
|
|
42
|
+
* Per-shard set of acked sequence numbers (as ints) not yet folded into the
|
|
43
|
+
* checkpoint. The checkpoint advances over the contiguous prefix of acked
|
|
44
|
+
* sequences (ReportBatchItemFailures semantics): a sequence is checkpointed
|
|
45
|
+
* only once every sequence at or before it has been acked.
|
|
46
|
+
*/
|
|
47
|
+
private ackedSeqs;
|
|
48
|
+
/** Per-shard count of sequences ever assigned, to bound the prefix scan. */
|
|
49
|
+
private maxSeqSeen;
|
|
50
|
+
/** Recorded event log (record mode and always, for replay()). */
|
|
51
|
+
private log;
|
|
52
|
+
/** Dead-letter buffer. */
|
|
53
|
+
private dlq;
|
|
54
|
+
private handler;
|
|
55
|
+
/** Virtual clock (ms since EPOCH). */
|
|
56
|
+
private clockMs;
|
|
57
|
+
/** Pending async work from inline-mode captures, awaited by callers. */
|
|
58
|
+
private inflight;
|
|
59
|
+
private detachCapture;
|
|
60
|
+
private readonly models;
|
|
61
|
+
constructor(opts?: CdcEmulatorOptions);
|
|
62
|
+
private onRawWrite;
|
|
63
|
+
private toChangeEvent;
|
|
64
|
+
/** Parse a zero-padded sequence string back to its integer value. */
|
|
65
|
+
private seqToInt;
|
|
66
|
+
/** Render an integer sequence as the zero-padded string form. */
|
|
67
|
+
private intToSeq;
|
|
68
|
+
/**
|
|
69
|
+
* Advance a shard's checkpoint over the contiguous prefix of acked sequence
|
|
70
|
+
* numbers. Starting just past the current checkpoint, consume every
|
|
71
|
+
* consecutive acked sequence; stop at the first gap (an un-acked sequence
|
|
72
|
+
* still pending redelivery). Consumed sequences are removed from the acked
|
|
73
|
+
* set to bound its size.
|
|
74
|
+
*/
|
|
75
|
+
private advanceCheckpoint;
|
|
76
|
+
private deriveEventName;
|
|
77
|
+
private nowIso;
|
|
78
|
+
private enqueue;
|
|
79
|
+
/**
|
|
80
|
+
* Deliver buffered, due records to the consumer, advancing per-shard
|
|
81
|
+
* checkpoints on success and redelivering failures. In `queued`/`replay`
|
|
82
|
+
* modes call this (or {@link advanceClock}) to drive delivery; `inline` calls
|
|
83
|
+
* it automatically after each capture.
|
|
84
|
+
*/
|
|
85
|
+
pump(maxBatches?: number): Promise<void>;
|
|
86
|
+
/**
|
|
87
|
+
* Pull up to `batchSize` due records from a shard, preserving ascending
|
|
88
|
+
* sequence order (spec §6). When the `reorder` fault is on, the within-shard
|
|
89
|
+
* order is deliberately shuffled to exercise invariant detection.
|
|
90
|
+
*/
|
|
91
|
+
private collectBatch;
|
|
92
|
+
private deliverBatch;
|
|
93
|
+
/** Register the consumer. Returns an unsubscribe handle. */
|
|
94
|
+
subscribe(handler: ChangeHandler): Unsubscribe;
|
|
95
|
+
/** Await any inline-mode async deliveries triggered by writes. */
|
|
96
|
+
flush(): Promise<void>;
|
|
97
|
+
/**
|
|
98
|
+
* Advance the virtual clock by `ms` and deliver records whose due-time has
|
|
99
|
+
* arrived (spec §7). Reproduces throttle T / sweep period / staleness S time
|
|
100
|
+
* behavior deterministically (AC4).
|
|
101
|
+
*/
|
|
102
|
+
advanceClock(ms: number): Promise<void>;
|
|
103
|
+
/** Current virtual time (ms since the emulator epoch). */
|
|
104
|
+
now(): number;
|
|
105
|
+
/** Configure fault injection (spec §8). Merges into the current spec. */
|
|
106
|
+
fault(spec: FaultSpec): void;
|
|
107
|
+
/**
|
|
108
|
+
* Force a recompute to race a mark for the given ref (spec §8, §7). The
|
|
109
|
+
* harness supplies the actual recompute via {@link onConcurrentRecompute};
|
|
110
|
+
* this records which ref to race and arms the hook.
|
|
111
|
+
*/
|
|
112
|
+
injectConcurrentRecompute(ref: ConcurrentRecomputeRef): void;
|
|
113
|
+
/**
|
|
114
|
+
* Register the callback the emulator invokes (before each record reaches the
|
|
115
|
+
* consumer) when a concurrent recompute has been injected. This is how the
|
|
116
|
+
* test harness wires its recompute into the delivery race.
|
|
117
|
+
*/
|
|
118
|
+
onConcurrentRecompute(hook: (ref: ConcurrentRecomputeRef, event: ChangeEvent) => void | Promise<void>): void;
|
|
119
|
+
/** Snapshot the recorded event log (spec §10). */
|
|
120
|
+
record(): EventLog;
|
|
121
|
+
/**
|
|
122
|
+
* Replay an event log through the delivery pipeline, optionally shuffled and
|
|
123
|
+
* duplicated (spec §10, AC5). Used to prove **replay equivalence**: starting
|
|
124
|
+
* from any global order plus duplicates, a consumer that respects the
|
|
125
|
+
* per-shard ordering contract (spec §6) reaches a final aggregate equal to a
|
|
126
|
+
* full recompute from source.
|
|
127
|
+
*
|
|
128
|
+
* The shuffle models cross-shard reordering and at-least-once chaos at the
|
|
129
|
+
* delivery boundary. It must NOT, however, break the within-shard ordering
|
|
130
|
+
* guarantee that real DynamoDB Streams provides and that incremental
|
|
131
|
+
* aggregation depends on — so after shuffling/duplicating, each shard's queue
|
|
132
|
+
* is re-sorted into ascending original `sequenceNumber` order before delivery.
|
|
133
|
+
* The original `sequenceNumber` (assigned at record time) is preserved so it
|
|
134
|
+
* remains the source-of-truth ordering the consumer can rely on.
|
|
135
|
+
*/
|
|
136
|
+
replay(log: EventLog, opts?: ReplayOptions): Promise<void>;
|
|
137
|
+
/** Per-shard checkpoints (last acked sequence number). */
|
|
138
|
+
checkpoints(): Record<ShardId, string>;
|
|
139
|
+
/** Dead-lettered events (exceeded maxRetries). */
|
|
140
|
+
deadLetters(): ChangeEvent[];
|
|
141
|
+
/** Reset delivery state and re-seed (keeps the subscription + model opt-in). */
|
|
142
|
+
reset(): void;
|
|
143
|
+
/** Tear down: detach from the core seam and disable model streaming. */
|
|
144
|
+
close(): void;
|
|
145
|
+
}
|
|
146
|
+
/** Create a CDC emulator (spec §10). */
|
|
147
|
+
declare function createCdcEmulator(opts?: CdcEmulatorOptions): CdcEmulator;
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Decorator-metadata → maintenance-graph `ViewDefinition` adapter (issue #152).
|
|
151
|
+
*
|
|
152
|
+
* ## What this replaces
|
|
153
|
+
*
|
|
154
|
+
* The old `defineView` / `defineVersioned` builders (deleted) registered
|
|
155
|
+
* `ViewDefinition`s into a side `ViewRegistry`, which the maintenance graph read.
|
|
156
|
+
* That builder carried a registration side-effect + a `generation` counter (a
|
|
157
|
+
* silent-drop guard). This adapter removes that whole path: it derives the SAME
|
|
158
|
+
* `ViewDefinition` IR purely from the declarative `MetadataRegistry` — the
|
|
159
|
+
* Contract-layer SSoT, whose own `generation` counter is the single
|
|
160
|
+
* invalidation source — so a view declared via `@model({ kind })` + `@maintainedFrom`
|
|
161
|
+
* (and a versioned relation declared via `@hasOne/@hasMany` `pattern`) is discovered
|
|
162
|
+
* with no separate registry.
|
|
163
|
+
*
|
|
164
|
+
* ## IR invariance
|
|
165
|
+
*
|
|
166
|
+
* The `ViewDefinition` / `ViewSourceSlice` shapes are UNCHANGED. The maintenance
|
|
167
|
+
* graph, the K (#130) stream/drain, and the L (#131) rebuild consume the adapter's
|
|
168
|
+
* output exactly as they consumed `ViewRegistry.getAll()`. Only the SOURCE of the
|
|
169
|
+
* definitions moved from a builder side-effect to declarative metadata.
|
|
170
|
+
*
|
|
171
|
+
* ## Order independence (issue #152 hardening)
|
|
172
|
+
*
|
|
173
|
+
* Stacked `@maintainedFrom` declarations carry no order meaning. The adapter rejects
|
|
174
|
+
* — symmetrically, so the error is identical whichever order the decorators are
|
|
175
|
+
* stacked — any two declarations on the same view that:
|
|
176
|
+
* 1. write the SAME projection target attribute (ambiguous duplicate projection);
|
|
177
|
+
* 2. maintain the SAME `collection.field`;
|
|
178
|
+
* 3. bind the SAME view-key field to a DIFFERENT source path (the view-row identity
|
|
179
|
+
* would be inconsistent across sources).
|
|
180
|
+
* A legitimate multi-source view (different sources filling DIFFERENT fields, all
|
|
181
|
+
* binding the SAME key the SAME way) is fine.
|
|
182
|
+
*/
|
|
183
|
+
|
|
184
|
+
type AnyModelClass = abstract new (...args: any[]) => any;
|
|
185
|
+
/** The view source slice IR the maintenance graph consumes (one per source × view). */
|
|
186
|
+
interface ViewSourceSlice {
|
|
187
|
+
readonly sourceClass: AnyModelClass;
|
|
188
|
+
readonly maintainedOn: readonly MaintainTrigger[];
|
|
189
|
+
readonly keys: Readonly<Record<string, EffectPath>>;
|
|
190
|
+
readonly project: ProjectionMap;
|
|
191
|
+
readonly collection?: {
|
|
192
|
+
readonly field: string;
|
|
193
|
+
readonly maxItems?: number;
|
|
194
|
+
readonly orderBy?: EffectPath;
|
|
195
|
+
readonly orderDir?: 'ASC' | 'DESC';
|
|
196
|
+
};
|
|
197
|
+
readonly predicate?: MembershipPredicate;
|
|
198
|
+
}
|
|
199
|
+
/** The view definition IR — an independent view row maintained from one or more sources. */
|
|
200
|
+
interface ViewDefinition {
|
|
201
|
+
readonly name: string;
|
|
202
|
+
readonly viewClass: AnyModelClass;
|
|
203
|
+
readonly pattern: 'materializedView' | 'sparseView';
|
|
204
|
+
readonly slices: readonly ViewSourceSlice[];
|
|
205
|
+
readonly updateMode?: 'mutation' | 'stream';
|
|
206
|
+
readonly consistency?: 'transactional' | 'eventual';
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* CDC **maintenance drain** (issue #130) — the asynchronous lower for
|
|
211
|
+
* `updateMode: 'stream'` maintainers (Epic #118 §5.1).
|
|
212
|
+
*
|
|
213
|
+
* A synchronous (`updateMode: 'mutation'`) maintainer composes its owner-row write
|
|
214
|
+
* into the SAME atomic `TransactWriteItems` as the source write (#127). A
|
|
215
|
+
* `updateMode: 'stream'` maintainer instead emits a **maintenance-outbox** marker row
|
|
216
|
+
* (`OUTBOX#MAINT#…`) ATOMICALLY with the source write (the #130 compile lowering in
|
|
217
|
+
* `src/spec/mutation-command.ts`), and THIS consumer — driven by the {@link
|
|
218
|
+
* import('./emulator.js').CdcEmulator} (or, in production, real DynamoDB Streams +
|
|
219
|
+
* a Lambda built around {@link createMaintenanceDrainHandler}) — applies the owner-row
|
|
220
|
+
* write asynchronously.
|
|
221
|
+
*
|
|
222
|
+
* ## Why a separate async path exists (not just "do it later synchronously")
|
|
223
|
+
*
|
|
224
|
+
* The async path realizes the maintenance operations a single synchronous
|
|
225
|
+
* `UpdateExpression` cannot express against an unread item:
|
|
226
|
+
*
|
|
227
|
+
* - **collection `maxItems` trim** — keeping a bounded, ordered list requires reading
|
|
228
|
+
* the current list, appending, sorting, and trimming (a read-modify-write); a
|
|
229
|
+
* synchronous `list_append` can only append (Phase 1 was append-only, #127).
|
|
230
|
+
* - **`removed`-driven splice** — removing an entry from a maintained collection on a
|
|
231
|
+
* source `removed` event likewise needs the current list.
|
|
232
|
+
* - **running `max(field)`** — a conditional `SET` (`#a = :v` guarded by
|
|
233
|
+
* `attribute_not_exists(#a) OR #a < :v`) whose failed guard, if run synchronously,
|
|
234
|
+
* would roll back the legitimate source write.
|
|
235
|
+
*
|
|
236
|
+
* ## Delivery guarantees (reused from the CDC substrate)
|
|
237
|
+
*
|
|
238
|
+
* The maintenance-outbox row is recorded ATOMICALLY with the source write, so the
|
|
239
|
+
* intent can never be lost ("wrote the row, lost the maintenance" is impossible). The
|
|
240
|
+
* CDC substrate then delivers it **at-least-once**, **per-shard ordered** (a shard is
|
|
241
|
+
* `hash(pk)`, and the outbox row's `pk` is keyed off the SOURCE row's key, so all
|
|
242
|
+
* maintenance events for one source entity land on one shard in source-write order),
|
|
243
|
+
* with a **DLQ** for poison events. This consumer is therefore written to be
|
|
244
|
+
* **idempotent / commutative** wherever at-least-once redelivery could double-apply:
|
|
245
|
+
*
|
|
246
|
+
* - a `snapshot` `SET` is naturally idempotent (re-applying the same projection is a
|
|
247
|
+
* no-op);
|
|
248
|
+
* - a `collection` rebuild de-duplicates by the projected item's identity key, so a
|
|
249
|
+
* redelivered append does not grow the list twice;
|
|
250
|
+
* - a `max` conditional `SET` is idempotent (the guard rejects a non-greater value);
|
|
251
|
+
* - a `count` `ADD` is the ONE non-idempotent op — but the source-row create guard
|
|
252
|
+
* (`attribute_not_exists`) means a given source `created` event is emitted at most
|
|
253
|
+
* once into the outbox, and the per-shard checkpoint advances only over acked events,
|
|
254
|
+
* so a `count` is applied once per genuine source lifecycle. (A duplicate DELIVERY of
|
|
255
|
+
* the same event is folded by the consumer's per-event de-dup set within a drain.)
|
|
256
|
+
*/
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* The PK prefix a maintenance-outbox row keys on — the single source of truth shared
|
|
260
|
+
* with the compiler ({@link import('../spec/mutation-command.js')}'s
|
|
261
|
+
* `MAINT_OUTBOX_PK_PREFIX`). The drain selects its events by `keys.pk.startsWith` of
|
|
262
|
+
* this. Kept as a local literal (rather than importing a non-exported compiler const)
|
|
263
|
+
* so the cdc module does not depend on the spec module; the value is asserted equal by
|
|
264
|
+
* the integration tests that round-trip a real stream maintainer through both.
|
|
265
|
+
*/
|
|
266
|
+
declare const MAINT_OUTBOX_PK_PREFIX = "OUTBOX#MAINT#";
|
|
267
|
+
/** Options for {@link createMaintenanceDrain}. */
|
|
268
|
+
interface MaintenanceDrainOptions {
|
|
269
|
+
/**
|
|
270
|
+
* The model classes (or `ModelStatic`s) whose stream maintainers this drain
|
|
271
|
+
* applies. The drain builds a scoped {@link MaintenanceGraph} over them to resolve
|
|
272
|
+
* each maintenance-outbox event back to its declared effect. Defaults to the GLOBAL
|
|
273
|
+
* registry when omitted.
|
|
274
|
+
*/
|
|
275
|
+
readonly models?: readonly Function[];
|
|
276
|
+
/**
|
|
277
|
+
* The {@link ViewDefinition}s (`defineView`) whose maintainers this drain applies
|
|
278
|
+
* (issue #132). A view's destination is its dedicated view model; its source
|
|
279
|
+
* classes should also be in {@link models} so the drain can resolve their
|
|
280
|
+
* maintenance-outbox events. Defaults to none.
|
|
281
|
+
*/
|
|
282
|
+
readonly views?: readonly ViewDefinition[];
|
|
283
|
+
}
|
|
284
|
+
/**
|
|
285
|
+
* A maintenance drain: a CDC consumer ({@link ChangeHandler}) that applies the
|
|
286
|
+
* owner-row writes for `updateMode: 'stream'` maintainers. Construct via
|
|
287
|
+
* {@link createMaintenanceDrain}, then `subscribe` its {@link handler} to a
|
|
288
|
+
* {@link import('./emulator.js').CdcEmulator} (dev/test) or wire it into a real
|
|
289
|
+
* Streams Lambda (production).
|
|
290
|
+
*/
|
|
291
|
+
declare class MaintenanceDrain {
|
|
292
|
+
private readonly graph;
|
|
293
|
+
/** Owner entity name → its model class (for the owner-row key derivation). */
|
|
294
|
+
private readonly ownerByName;
|
|
295
|
+
/** Per-event-id de-dup within a single handler invocation (at-least-once). */
|
|
296
|
+
private appliedSeq;
|
|
297
|
+
/** Count of owner-row writes applied (test/diagnostic). */
|
|
298
|
+
private applied;
|
|
299
|
+
constructor(opts?: MaintenanceDrainOptions);
|
|
300
|
+
/** The CDC {@link ChangeHandler} to subscribe to an emulator / Streams source. */
|
|
301
|
+
readonly handler: ChangeHandler;
|
|
302
|
+
/** Resolve one maintenance-outbox event to its effect and apply the owner-row write. */
|
|
303
|
+
private applyOne;
|
|
304
|
+
/** Resolve a maintenance-outbox event back to the declared {@link MaintainItem}. */
|
|
305
|
+
private resolve;
|
|
306
|
+
/** A `snapshot` SET of each projected attribute onto the owner row (idempotent). */
|
|
307
|
+
private applySnapshot;
|
|
308
|
+
/**
|
|
309
|
+
* A sparse-view **membership** write (#133): evaluate the membership predicate against
|
|
310
|
+
* the source image; PUT the view row (its projection + key fields) when the predicate
|
|
311
|
+
* holds, DELETE it when it flips false. A source `removed` event always deletes (the
|
|
312
|
+
* source no longer exists, so its view row must disappear regardless of the predicate).
|
|
313
|
+
*
|
|
314
|
+
* Both ops are idempotent under at-least-once redelivery: a repeated PUT writes the same
|
|
315
|
+
* row, a repeated DELETE is a no-op. The view row is keyed by the source identity
|
|
316
|
+
* (`effect.keys`), so the PUT/DELETE always targets the SAME physical row the predicate
|
|
317
|
+
* gates.
|
|
318
|
+
*/
|
|
319
|
+
private applyMembership;
|
|
320
|
+
/**
|
|
321
|
+
* A running `max`: read the owner row, and `SET` the attribute to the source value
|
|
322
|
+
* only when it is greater (or absent). Implemented as a conditional update so a
|
|
323
|
+
* redelivered / out-of-order older value never regresses the stored max — idempotent
|
|
324
|
+
* and commutative under at-least-once delivery.
|
|
325
|
+
*/
|
|
326
|
+
private applyMax;
|
|
327
|
+
/**
|
|
328
|
+
* A bounded `collection`: read the current list, apply the event (append for a
|
|
329
|
+
* `created`/`updated` source, splice for a `removed`), de-duplicate by the projected
|
|
330
|
+
* identity key, order by `orderBy` (in `orderDir`, default DESC), trim to `maxItems`, and write the whole
|
|
331
|
+
* list back. The read-modify-write is exactly what a single synchronous
|
|
332
|
+
* `UpdateExpression` cannot do — the reason a bounded/ordered collection is a stream
|
|
333
|
+
* maintainer (Phase 1 sync was append-only).
|
|
334
|
+
*/
|
|
335
|
+
private applyCollection;
|
|
336
|
+
/** Owner-row writes applied so far (test/diagnostic). */
|
|
337
|
+
appliedCount(): number;
|
|
338
|
+
/** Clear the per-delivery de-dup set + counters (test reset). */
|
|
339
|
+
reset(): void;
|
|
340
|
+
}
|
|
341
|
+
/** Create a {@link MaintenanceDrain} (issue #130). */
|
|
342
|
+
declare function createMaintenanceDrain(opts?: MaintenanceDrainOptions): MaintenanceDrain;
|
|
343
|
+
/**
|
|
344
|
+
* Create just the CDC {@link ChangeHandler} for a maintenance drain — the form to
|
|
345
|
+
* subscribe to an emulator or wire into a production Streams Lambda. A thin wrapper
|
|
346
|
+
* over {@link createMaintenanceDrain} for callers that only need the handler.
|
|
347
|
+
*/
|
|
348
|
+
declare function createMaintenanceDrainHandler(opts?: MaintenanceDrainOptions): ChangeHandler;
|
|
5
349
|
|
|
6
350
|
/**
|
|
7
351
|
* Deterministic primitives for the CDC emulator (issue #72). All fault
|
|
@@ -35,4 +379,44 @@ declare function hashString(value: string): number;
|
|
|
35
379
|
/** Compute the shard id for a partition key over `shardCount` shards. */
|
|
36
380
|
declare function shardIdFor(pk: string, shardCount: number): string;
|
|
37
381
|
|
|
38
|
-
|
|
382
|
+
/**
|
|
383
|
+
* `fromChange` — the pure `(event) => [old, new]` typed mapper (issue #153).
|
|
384
|
+
*
|
|
385
|
+
* This is graphddb's HALF of the CDC-projection boundary (see
|
|
386
|
+
* `docs/cdc-projection.md`): it parses a raw {@link ChangeEvent}'s `oldImage` /
|
|
387
|
+
* `newImage` into typed model instances and does nothing downstream of that (no
|
|
388
|
+
* sink write, no dedup, no subscription). `Model.fromChange(event)` on `DDBModel`
|
|
389
|
+
* delegates here.
|
|
390
|
+
*
|
|
391
|
+
* The two responsibilities packed into one call are **routing** and **parse**:
|
|
392
|
+
*
|
|
393
|
+
* - Routing — an event that is not for `modelClass` yields `[null, null]`. A valid
|
|
394
|
+
* event for the class always has at least one non-null image, so `[null, null]`
|
|
395
|
+
* is an unambiguous "not for this model" signal and no separate `owns()` guard is
|
|
396
|
+
* needed. The match is by `event.model` (the resolved model name the write path /
|
|
397
|
+
* emulator stamps) when present, falling back to the model's PK prefix
|
|
398
|
+
* (`keys.pk` begins with `<prefix>`) when the event carries no `model`.
|
|
399
|
+
* - Parse — each present image is hydrated via the existing {@link hydrate}
|
|
400
|
+
* (raw item → typed value, INCLUDING ISO 8601 → `Date` for `@datetime` and
|
|
401
|
+
* embedded reconstruction), returning ALL fields (no projection — narrowing does
|
|
402
|
+
* not reduce network cost since the image is already on the stream). The hydrated
|
|
403
|
+
* plain record is then loaded onto a fresh model instance so the result is a
|
|
404
|
+
* genuine `InstanceType` (like the read path's `{ hydrate }` factory option).
|
|
405
|
+
*
|
|
406
|
+
* The image side present per event kind mirrors DynamoDB Streams:
|
|
407
|
+
*
|
|
408
|
+
* - INSERT → `[null, new ]`
|
|
409
|
+
* - MODIFY → `[old, new ]`
|
|
410
|
+
* - REMOVE → `[old, null]`
|
|
411
|
+
*/
|
|
412
|
+
|
|
413
|
+
/**
|
|
414
|
+
* Parse a {@link ChangeEvent} into the `[oldRecord, newRecord]` tuple for the given
|
|
415
|
+
* model. Returns `[null, null]` when the event is not for this model (routing). The
|
|
416
|
+
* caller (`DDBModel.fromChange`) supplies the resolved metadata + class; this
|
|
417
|
+
* function is the runtime core and is deliberately model-name-string driven so
|
|
418
|
+
* `DDBModel.subscribe` can reuse it when routing a batch.
|
|
419
|
+
*/
|
|
420
|
+
declare function parseChange<T extends object>(event: ChangeEvent, metadata: EntityMetadata, modelName: string, modelClass: new () => T): [T | null, T | null];
|
|
421
|
+
|
|
422
|
+
export { CdcEmulator, CdcEmulatorOptions, ChangeEvent, ChangeHandler, ConcurrentRecomputeRef, EventLog, FaultSpec, MAINT_OUTBOX_PK_PREFIX, MaintenanceDrain, type MaintenanceDrainOptions, ReplayOptions, SeededRandom, ShardId, Unsubscribe, createCdcEmulator, createMaintenanceDrain, createMaintenanceDrainHandler, hashString, parseChange, shardIdFor };
|
package/dist/cdc/index.js
CHANGED
|
@@ -8,13 +8,13 @@ import {
|
|
|
8
8
|
createMaintenanceDrainHandler,
|
|
9
9
|
hashString,
|
|
10
10
|
shardIdFor
|
|
11
|
-
} from "../chunk-
|
|
11
|
+
} from "../chunk-GS4C5VGO.js";
|
|
12
12
|
import {
|
|
13
13
|
buildSubscribeHandler,
|
|
14
14
|
parseChange
|
|
15
|
-
} from "../chunk-
|
|
16
|
-
import "../chunk-
|
|
17
|
-
import "../chunk-
|
|
15
|
+
} from "../chunk-HNY2EJPV.js";
|
|
16
|
+
import "../chunk-L2NEDS7U.js";
|
|
17
|
+
import "../chunk-XTWXMOHD.js";
|
|
18
18
|
export {
|
|
19
19
|
CdcEmulator,
|
|
20
20
|
MAINT_OUTBOX_PK_PREFIX,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
buildMaintenanceGraph
|
|
3
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-HNY2EJPV.js";
|
|
4
4
|
import {
|
|
5
5
|
ChangeCaptureRegistry,
|
|
6
6
|
ClientManager,
|
|
@@ -9,7 +9,7 @@ import {
|
|
|
9
9
|
buildDeleteInput,
|
|
10
10
|
buildPutInput,
|
|
11
11
|
resolveModelClass
|
|
12
|
-
} from "./chunk-
|
|
12
|
+
} from "./chunk-L2NEDS7U.js";
|
|
13
13
|
|
|
14
14
|
// src/cdc/prng.ts
|
|
15
15
|
var SeededRandom = class {
|
|
@@ -800,10 +800,6 @@ export {
|
|
|
800
800
|
shardIdFor,
|
|
801
801
|
CdcEmulator,
|
|
802
802
|
createCdcEmulator,
|
|
803
|
-
pathField,
|
|
804
|
-
projectFrom,
|
|
805
|
-
compareDesc,
|
|
806
|
-
orderAndTrimCollection,
|
|
807
803
|
MAINT_OUTBOX_PK_PREFIX,
|
|
808
804
|
MaintenanceDrain,
|
|
809
805
|
createMaintenanceDrain,
|