graphddb 0.4.2 → 0.5.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/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- import { S as SelectableOf, g as PrimaryKeyOf, h as RequestContext, i as Middleware, j as ReadRequestKind, k as CtxModel, l as ReadParams, m as ReadRequestCtx, D as DynamoDBOperation, I as Item, E as Executor, n as RetryPolicy, o as ExecutionPlanSpec, p as EntityMetadata, K as KeyDefinition, G as GsiDefinition, q as ModelKind, F as FieldOptions, r as DynamoType, s as ProjectionTransform, t as MaintainEvent, u as MembershipPredicate, v as MaintainConsistency, w as MaintainUpdateMode, x as MembershipPredicateOp, y as RelationOptions, A as AggregateOptions, z as AggregateValue, H as SelectBuilderSpec, J as RawCondition, L as TransactionSpec, N as Manifest, O as RetryOverride, Q as ExecutionPlan, V as FieldMetadata, X as ResolvedKey, R as ReadExecOptions, a as ExecutorResult, B as BatchGetExecInput, P as PutInput, W as WriteExecOptions, b as WriteResult, U as UpdateInput, c as DeleteInput, d as BatchWriteExecItem, e as BatchExecOptions, T as TransactWriteExecItem, Y as CdcEmulatorOptions, Z as ChangeHandler, _ as Unsubscribe, $ as FaultSpec, a0 as ConcurrentRecomputeRef, C as ChangeEvent, a1 as EventLog, a2 as ReplayOptions, a3 as ShardId, a4 as ViewDefinition, a5 as RelationMetadata, a6 as TransactionItemSpec, a7 as MaintainEffect, M as ModelStatic, f as DDBModel, a8 as Param, a9 as ParamDescriptor, aa as DefinitionMap, ab as OperationDefinition, ac as WriteDefinitionOptions, ad as PartialQueryKeyOf, ae as StrictSelectSpec, af as ReadDefinitionOptions, ag as EntityInput, ah as UniqueQueryKeyOf, ai as EntityRef, aj as ConditionInput, ak as QueryModelContract, al as QueryMethodSpec, am as CommandModelContract, an as CommandMethodSpec, ao as ContractSpec, ap as QuerySpec, aq as CommandSpec, ar as ContextSpec, as as OperationsDocument, at as AnyOperationDefinition, au as BridgeBundle, av as ConditionSpec } from './types-BWOrWcbd.js';
2
- export { aw as AggregateMetadata, ax as BatchDeleteRequest, ay as BatchGetOptions, az as BatchGetRequest, aA as BatchGetResult, aB as BatchPutRequest, aC as BatchResult, aD as BatchWriteRequest, aE as CONTRACT_RANGE_FANOUT_CONCURRENCY, aF as CdcMode, aG as Change, aH as ChangeBatch, aI as ChangeEventName, aJ as ClockMode, aK as CollectionEffect, aL as CollectionOptions, aM as Column, aN as ColumnMap, aO as CommandContractMethodSpec, aP as CommandInputShape, aQ as CommandMethod, aR as CommandPlan, aS as CommandResolutionTarget, aT as CommandResultKind, aU as CommandSelectShape, aV as CompiledFragment, aW as ComposeSpec, aX as CondSlot, aY as ConditionCheckInput, aZ as Connection, a_ as ContractCallSignature, a$ as ContractCardinality, b0 as ContractCommandParams, b1 as ContractCommandResult, b2 as ContractComposeNode, b3 as ContractFromRef, b4 as ContractInputArity, b5 as ContractItem, b6 as ContractKeyFieldRef, b7 as ContractKeyInput, b8 as ContractKeyRef, b9 as ContractKeySpec, ba as ContractKind, bb as ContractMethodOp, bc as ContractParamRef, bd as ContractQueryParams, be as ContractResolution, bf as CounterAggregate, bg as CounterEffect, bh as CtxBase, bi as DEFAULT_MAX_ATTEMPTS, bj as DEFAULT_RETRY_POLICY, bk as DeleteOptions, bl as DeriveEffect, bm as DerivedEdgeWrite, bn as DerivedUpdate, bo as DescriptorBinding, bp as ENTITY_WRITES_MARKER, bq as EdgeEffect, br as EffectPath, bs as EmbeddedMetadata, bt as EmitEffect, bu as EntityWritesDefinition, bv as EntityWritesShape, bw as ExecutableCommandContract, bx as ExecutableQueryContract, by as FilterInput, bz as FilterSpec, bA as FragmentInput, bB as GsiDefinitionMarker, bC as GsiOptions, bD as IdempotencyEffect, bE as InProcessWriteDescriptor, bF as InputArity, bG as KeyDefinitionMarker, bH as KeySegment, bI as KeySlot, bJ as KeyStructure, bK as KeyedResult, bL as LIFECYCLE_CONTRACT_MARKER, bM as LifecycleContract, bN as LifecycleEffects, bO as LiteralParam, bP as MaintainItem, bQ as MaintainTrigger, bR as MaintenanceGraph, bS as ManifestEntity, bT as ManifestField, bU as ManifestFieldType, bV as ManifestGsi, bW as ManifestKey, bX as ManifestRelation, bY as ManifestTable, bZ as MembershipEffect, b_ as ModelRef, b$ as MutateMode, c0 as MutateOptions, c1 as MutateParallelResult, c2 as MutateTransactionResult, c3 as MutationBody, c4 as MutationDescriptorMap, c5 as MutationFragment, c6 as MutationInputProxy, c7 as MutationInputRef, c8 as MutationIntent, c9 as NumberParam, ca as OperationKind, cb as OperationSpec, cc as ParallelOpResult, cd as ParamKind, ce as ParamSpec, cf as ParamStructure, cg as PersistCtx, ch as PersistOrigin, ci as PlannedCommandMethod, cj as ProjectionMap, ck as ProjectionTransformOp, cl as PutOptions, cm as QueryContractMethodSpec, cn as QueryEnvelopeResult, co as QueryKeyOf, cp as QueryMethod, cq as QueryResult, cr as RangeConditionSpec, cs as ReadEnvelope, ct as ReadOpCtx, cu as ReadOpKind, cv as ReadOperationType, cw as ReadRouteDescriptor, cx as ReadRouteOptions, cy as ReadRouteResult, cz as RecordedCompose, cA as RelationBuilder, cB as RelationConsistency, cC as RelationLimitOptions, cD as RelationPattern, cE as RelationProjection, cF as RelationReadOptions, cG as RelationSelect, cH as RelationSpec, cI as RelationUpdateMode, cJ as RelationWriteOptions, cK as RequiresEffect, cL as Resolution, cM as RetryInfo, cN as RetryOperationKind, cO as SPEC_VERSION, cP as SegmentSpec, cQ as SegmentedKey, cR as SelectBuilder, cS as SelectOf, cT as SnapshotEffect, cU as StartingPosition, cV as StreamViewType, cW as StringParam, cX as TransactionContext, cY as TransactionItemType, cZ as UniqueEffect, c_ as Updatable, c$ as UpdateOptions, d0 as ViewSourceSlice, d1 as WhenSpec, d2 as WriteCtx, d3 as WriteDescriptor, d4 as WriteEnvelope, d5 as WriteInput, d6 as WriteKind, d7 as WriteLifecyclePhase, d8 as WriteMiddleware, d9 as WriteOperationType, da as WriteRecorder, db as WriteResultProjection, dc as attachModelClass, dd as buildDeleteInput, de as buildMaintenanceGraph, df as buildPutInput, dg as buildUpdateInput, dh as collectViewDefinitions, di as compileFragment, dj as compileMutationPlan, dk as compileSingleFragmentPlan, dl as cond, dm as contractOfMethodSpec, dn as definePlan, dp as entityWrites, dq as executeBatchGet, dr as executeBatchWrite, ds as executeCommandMethod, dt as executeDelete, du as executeKeyedBatchGet, dv as executePut, dw as executeQueryMethod, dx as executeRangeFanout, dy as executeTransaction, dz as executeUpdate, dA as from, dB as getEntityWrites, dC as gsi, dD as identity, dE as isColumn, dF as isCommandModelContract, dG as isCommandPlan, dH as isContractComposeNode, dI as isContractFromRef, dJ as isContractKeyFieldRef, dK as isContractKeyRef, dL as isContractParamRef, dM as isEntityWritesDefinition, dN as isKeySegment, dO as isLifecycleContract, dP as isMaintainTrigger, dQ as isMutationFragment, dR as isMutationInputRef, dS as isParam, dT as isPlannedCommandMethod, dU as isQueryModelContract, dV as isRetryableError, dW as isRetryableTransactionCancellation, dX as k, dY as key, dZ as lifecyclePhaseForIntent, d_ as maintainTrigger, d$ as mintContractKeyFieldRef, e0 as mintContractParamRef, e1 as mutation, e2 as param, e3 as preview, e4 as publicCommandModel, e5 as publicQueryModel, e6 as query, e7 as resolveLifecycle, e8 as wholeKeysSentinel } from './types-BWOrWcbd.js';
1
+ import { S as SelectableOf, g as PrimaryKeyOf, h as RequestContext, i as Middleware, j as ReadRequestKind, k as CtxModel, l as ReadParams, m as ReadRequestCtx, D as DynamoDBOperation, I as Item, E as Executor, n as RetryPolicy, o as ExecutionPlanSpec, p as EntityMetadata, K as KeyDefinition, G as GsiDefinition, q as ModelKind, F as FieldOptions, r as DynamoType, s as ProjectionTransform, t as MaintainEvent, u as MembershipPredicate, v as MaintainConsistency, w as MaintainUpdateMode, x as MembershipPredicateOp, y as RelationOptions, A as AggregateOptions, z as AggregateValue, H as SelectBuilderSpec, J as RawCondition, L as TransactionSpec, N as Manifest, O as RetryOverride, Q as ExecutionPlan, V as FieldMetadata, X as ResolvedKey, R as ReadExecOptions, a as ExecutorResult, B as BatchGetExecInput, P as PutInput, W as WriteExecOptions, b as WriteResult, U as UpdateInput, c as DeleteInput, d as BatchWriteExecItem, e as BatchExecOptions, T as TransactWriteExecItem, Y as CdcEmulatorOptions, Z as ChangeHandler, _ as Unsubscribe, $ as FaultSpec, a0 as ConcurrentRecomputeRef, C as ChangeEvent, a1 as EventLog, a2 as ReplayOptions, a3 as ShardId, a4 as ViewDefinition, a5 as RelationMetadata, a6 as TransactionItemSpec, a7 as MaintainEffect, M as ModelStatic, f as DDBModel, a8 as Param, a9 as ParamDescriptor, aa as DefinitionMap, ab as OperationDefinition, ac as WriteDefinitionOptions, ad as PartialQueryKeyOf, ae as StrictSelectSpec, af as ReadDefinitionOptions, ag as EntityInput, ah as UniqueQueryKeyOf, ai as EntityRef, aj as ConditionInput, ak as QueryModelContract, al as QueryMethodSpec, am as CommandModelContract, an as CommandMethodSpec, ao as ContractSpec, ap as QuerySpec, aq as CommandSpec, ar as ContextSpec, as as OperationsDocument, at as AnyOperationDefinition, au as BridgeBundle, av as ConditionSpec } from './types-B-F7jw9f.js';
2
+ export { aw as AggregateMetadata, ax as BatchDeleteRequest, ay as BatchGetOptions, az as BatchGetRequest, aA as BatchGetResult, aB as BatchPutRequest, aC as BatchResult, aD as BatchWriteRequest, aE as CONTRACT_RANGE_FANOUT_CONCURRENCY, aF as CdcMode, aG as Change, aH as ChangeBatch, aI as ChangeEventName, aJ as ClockMode, aK as CollectionEffect, aL as CollectionOptions, aM as Column, aN as ColumnMap, aO as CommandContractMethodSpec, aP as CommandInputShape, aQ as CommandMethod, aR as CommandPlan, aS as CommandResolutionTarget, aT as CommandResultKind, aU as CommandSelectShape, aV as CompiledFragment, aW as ComposeSpec, aX as CondSlot, aY as ConditionCheckInput, aZ as Connection, a_ as ContractCallSignature, a$ as ContractCardinality, b0 as ContractCommandParams, b1 as ContractCommandResult, b2 as ContractComposeNode, b3 as ContractFromRef, b4 as ContractInputArity, b5 as ContractItem, b6 as ContractKeyFieldRef, b7 as ContractKeyInput, b8 as ContractKeyRef, b9 as ContractKeySpec, ba as ContractKind, bb as ContractMethodOp, bc as ContractParamRef, bd as ContractQueryParams, be as ContractResolution, bf as CounterAggregate, bg as CounterEffect, bh as CtxBase, bi as DEFAULT_MAX_ATTEMPTS, bj as DEFAULT_RETRY_POLICY, bk as DeleteOptions, bl as DeriveEffect, bm as DerivedEdgeWrite, bn as DerivedUpdate, bo as DescriptorBinding, bp as ENTITY_WRITES_MARKER, bq as EdgeEffect, br as EffectPath, bs as EmbeddedMetadata, bt as EmitEffect, bu as EntityWritesDefinition, bv as EntityWritesShape, bw as ExecutableCommandContract, bx as ExecutableQueryContract, by as FilterInput, bz as FilterSpec, bA as FragmentInput, bB as GsiDefinitionMarker, bC as GsiOptions, bD as IdempotencyEffect, bE as InProcessWriteDescriptor, bF as InputArity, bG as KeyDefinitionMarker, bH as KeySegment, bI as KeySlot, bJ as KeyStructure, bK as KeyedResult, bL as LIFECYCLE_CONTRACT_MARKER, bM as LifecycleContract, bN as LifecycleEffects, bO as LiteralParam, bP as MaintainItem, bQ as MaintainTrigger, bR as MaintenanceGraph, bS as ManifestEntity, bT as ManifestField, bU as ManifestFieldType, bV as ManifestGsi, bW as ManifestKey, bX as ManifestRelation, bY as ManifestTable, bZ as MembershipEffect, b_ as ModelRef, b$ as MutateMode, c0 as MutateOptions, c1 as MutateParallelResult, c2 as MutateTransactionResult, c3 as MutationBody, c4 as MutationDescriptorMap, c5 as MutationFragment, c6 as MutationInputProxy, c7 as MutationInputRef, c8 as MutationIntent, c9 as NumberParam, ca as OperationKind, cb as OperationSpec, cc as ParallelOpResult, cd as ParamKind, ce as ParamSpec, cf as ParamStructure, cg as PersistCtx, ch as PersistOrigin, ci as PlannedCommandMethod, cj as ProjectionMap, ck as ProjectionTransformOp, cl as PutOptions, cm as QueryContractMethodSpec, cn as QueryEnvelopeResult, co as QueryKeyOf, cp as QueryMethod, cq as QueryResult, cr as RangeConditionSpec, cs as ReadEnvelope, ct as ReadOpCtx, cu as ReadOpKind, cv as ReadOperationType, cw as ReadRouteDescriptor, cx as ReadRouteOptions, cy as ReadRouteResult, cz as RecordedCompose, cA as RelationBuilder, cB as RelationConsistency, cC as RelationLimitOptions, cD as RelationPattern, cE as RelationProjection, cF as RelationReadOptions, cG as RelationSelect, cH as RelationSpec, cI as RelationUpdateMode, cJ as RelationWriteOptions, cK as RequiresEffect, cL as Resolution, cM as RetryInfo, cN as RetryOperationKind, cO as SPEC_VERSION, cP as SegmentSpec, cQ as SegmentedKey, cR as SelectBuilder, cS as SelectOf, cT as SnapshotEffect, cU as StartingPosition, cV as StreamViewType, cW as StringParam, cX as TransactionContext, cY as TransactionItemType, cZ as UniqueEffect, c_ as Updatable, c$ as UpdateOptions, d0 as ViewSourceSlice, d1 as WhenSpec, d2 as WriteCtx, d3 as WriteDescriptor, d4 as WriteEnvelope, d5 as WriteInput, d6 as WriteKind, d7 as WriteLifecyclePhase, d8 as WriteMiddleware, d9 as WriteOperationType, da as WriteRecorder, db as WriteResultProjection, dc as attachModelClass, dd as buildDeleteInput, de as buildMaintenanceGraph, df as buildPutInput, dg as buildUpdateInput, dh as collectViewDefinitions, di as compileFragment, dj as compileMutationPlan, dk as compileSingleFragmentPlan, dl as cond, dm as contractOfMethodSpec, dn as definePlan, dp as entityWrites, dq as executeBatchGet, dr as executeBatchWrite, ds as executeCommandMethod, dt as executeDelete, du as executeKeyedBatchGet, dv as executePut, dw as executeQueryMethod, dx as executeRangeFanout, dy as executeTransaction, dz as executeUpdate, dA as from, dB as getEntityWrites, dC as gsi, dD as identity, dE as isColumn, dF as isCommandModelContract, dG as isCommandPlan, dH as isContractComposeNode, dI as isContractFromRef, dJ as isContractKeyFieldRef, dK as isContractKeyRef, dL as isContractParamRef, dM as isEntityWritesDefinition, dN as isKeySegment, dO as isLifecycleContract, dP as isMaintainTrigger, dQ as isMutationFragment, dR as isMutationInputRef, dS as isParam, dT as isPlannedCommandMethod, dU as isQueryModelContract, dV as isRetryableError, dW as isRetryableTransactionCancellation, dX as k, dY as key, dZ as lifecyclePhaseForIntent, d_ as maintainTrigger, d$ as mintContractKeyFieldRef, e0 as mintContractParamRef, e1 as mutation, e2 as param, e3 as preview, e4 as publicCommandModel, e5 as publicQueryModel, e6 as query, e7 as resolveLifecycle, e8 as wholeKeysSentinel } from './types-B-F7jw9f.js';
3
3
  import { DynamoDBClient } from '@aws-sdk/client-dynamodb';
4
4
  import { DynamoDBDocumentClient } from '@aws-sdk/lib-dynamodb';
5
5
 
@@ -356,6 +356,19 @@ declare class MetadataRegistry {
356
356
  static get(target: Function): EntityMetadata;
357
357
  static has(target: Function): boolean;
358
358
  static getAll(): Map<Function, EntityMetadata>;
359
+ /**
360
+ * Snapshot of the entities that are **already finalized**, WITHOUT triggering
361
+ * finalize on any pending entity (unlike {@link getAll}). This is the safe view
362
+ * for a cross-entity lint rule that runs *inside* `finalize`: eagerly
363
+ * finalizing the rest of the registry from within a finalize pass would
364
+ * re-enter (and duplicate) the in-progress entity's finalize. A whole-registry
365
+ * rule instead reasons over the already-finalized peers plus the entity
366
+ * currently being checked; any conflicting pair is caught when the later of the
367
+ * two finalizes (both are present by then).
368
+ *
369
+ * @internal — for cross-entity linter rules.
370
+ */
371
+ static getFinalized(): Map<Function, EntityMetadata>;
359
372
  static get linter(): Linter;
360
373
  static set linter(linter: Linter);
361
374
  /** Resets linter to the default built-in rule set. */
@@ -484,6 +497,48 @@ interface FieldDecorator<T> extends FieldDecoratorFn<T> {
484
497
  * `literals` set.
485
498
  */
486
499
  declare function literal<const L extends string>(...values: readonly [L, ...L[]]): FieldDecoratorFn<L>;
500
+ /**
501
+ * `@ttl` — the **DynamoDB Time-To-Live** field decorator (issue #172, Epic #167 —
502
+ * CFn generator C4). It marks the model's TTL attribute: the Number field holding
503
+ * the **epoch-seconds** expiry timestamp DynamoDB reads to auto-delete the item.
504
+ *
505
+ * ## Standalone, number-pinned (not composed with `@number`)
506
+ *
507
+ * `@ttl` is a **standalone** semantic decorator — it records the field's storage
508
+ * type (`dynamoType: 'N'`) itself, exactly as `@datetime` records `'S'` + a format
509
+ * and `@literal` records `'S'` + a literal set. It is NOT composed with `@number`
510
+ * (a `@number @ttl` stack). Two reasons:
511
+ *
512
+ * 1. **A valid TTL attribute is always a Number** (epoch seconds) — there is no
513
+ * string/boolean TTL — so the storage type is not a free choice the author
514
+ * should have to (re)state. Folding `'N'` into `@ttl` removes the redundant
515
+ * second decorator and the ordering/consistency question a two-decorator stack
516
+ * would raise (which wins if they disagreed?).
517
+ * 2. It matches the module idiom: every semantic decorator here is self-contained
518
+ * and pins exactly one storage type; none composes with another.
519
+ *
520
+ * ## Loud-reject (the most-feared silent-drop class in this repo)
521
+ *
522
+ * - **Non-number field → compile error.** The decorator is pinned to `number`
523
+ * (its `context` is `ClassFieldDecoratorContext<unknown, number | undefined>`),
524
+ * so `@ttl expiresAt!: string` fails to type-check — the SAME cross-type
525
+ * mechanism that makes `@number age!: string` an error. A TTL on a non-Number
526
+ * attribute can never be declared (DynamoDB would silently never expire it).
527
+ * - **More than one `@ttl` per (physical) table → throws.** DynamoDB allows
528
+ * exactly one TTL attribute per table. A second `@ttl` on the same model throws
529
+ * at `@model` registration; two `@ttl` fields on DIFFERENT entities sharing one
530
+ * physical table throw at manifest build (where all table siblings are known).
531
+ *
532
+ * ## Runtime behaviour
533
+ *
534
+ * A `@ttl` field is a plain stored Number at runtime — no special serialization,
535
+ * hydration, or key handling (it is not a key attribute). The decorator only
536
+ * records metadata (`dynamoType: 'N'`, `ttl: true`); the TTL *effect* is a table
537
+ * property DynamoDB applies, emitted into the generated CloudFormation
538
+ * `TimeToLiveSpecification` (never into `AttributeDefinitions`, which holds key
539
+ * attributes only). So it needs no runtime-serialization conformance change.
540
+ */
541
+ declare const ttl: FieldDecoratorFn<number>;
487
542
  declare const string: FieldDecorator<string>;
488
543
  declare const number: FieldDecorator<number>;
489
544
  declare const boolean: FieldDecorator<boolean>;
@@ -1608,6 +1663,13 @@ declare function decodePerKeyCursor(cursor: string, expectedKey: Record<string,
1608
1663
  * unaffected. query-boundary is excluded because it flags non-unique GSIs which
1609
1664
  * are valid for list() operations; boundary enforcement is handled at runtime by
1610
1665
  * the planner's boundary-check.
1666
+ *
1667
+ * Also includes cfn-schema-consistency (issue #169): a whole-table validator
1668
+ * that rejects entities sharing a physical table with an ambiguous base
1669
+ * KeySchema, inconsistently-shared GSI index names, or more than 20 unioned
1670
+ * GSIs — the CFn deploy-consistency user-error class, caught statically at
1671
+ * finalize. It is additive: a single entity on its own table with ≤ 20 GSIs is
1672
+ * never affected.
1611
1673
  */
1612
1674
  declare function createDefaultLinter(): Linter;
1613
1675
 
@@ -3003,4 +3065,4 @@ declare function assertBundleSerializable(bundle: BridgeBundle): void;
3003
3065
  */
3004
3066
  declare function buildBridgeBundle(queries?: DefinitionMap, commands?: DefinitionMap, registry?: typeof MetadataRegistry, transactions?: Record<string, TransactionDefinition>, contractInputs?: ContractInputs): BridgeBundle;
3005
3067
 
3006
- export { AggregateOptions, AggregateValue, type AnyModelContract, AnyOperationDefinition, BATCH_GET_MAX_KEYS, BATCH_WRITE_MAX_ITEMS, BatchExecOptions, BatchGetExecInput, BatchWriteExecItem, BridgeBundle, type BuiltContracts, CdcEmulator, CdcEmulatorOptions, ChangeEvent, ChangeHandler, ClientManager, type CollectParams, CommandMethodSpec, CommandModelContract, CommandSpec, ConcurrentRecomputeRef, type ConditionExpressionResult, ConditionInput, ConditionSpec, type ContextOwnership, type ContextOwnershipMap, ContextSpec, type ContractBoundaryViolation, type ContractInputs, type ContractMap, type ContractN1Violation, ContractSpec, CtxModel, DDBModel, DefinitionMap, DeleteInput, type DriftReport, DynamoDBOperation, DynamoExecutor, DynamoType, EDGE_WRITES_MARKER, type EdgeLifecycle, type EdgeWriteDeclaration, type EdgeWriteRecorder, type EdgeWritesDefinition, EntityMetadata, EntityRef, EventLog, ExecutionPlan, Executor, ExecutorResult, type ExplainInput, FaultSpec, FieldMetadata, FieldOptions, type FilterExpressionResult, GsiDefinition, type HasManyVersionedCallback, type HasManyVersionedOptions, type HasOneVersionedCallback, type HasOneVersionedOptions, Item, KeyDefinition, type LintResult, type LintRule, Linter, type ListInput, type ListOptions, MAINT_OUTBOX_PK_PREFIX, MAX_TRANSACT_ITEMS, MaintainConsistency, MaintainEffect, MaintainEvent, MaintainUpdateMode, type MaintainedFromCallback, type MaintainedFromOptions, MaintenanceDrain, type MaintenanceDrainOptions, type MaintenanceRebuildOptions, MaintenanceRebuilder, Manifest, MembershipPredicate, MembershipPredicateOp, MetadataRegistry, Middleware, type ModelOptions, ModelStatic, OLD_VALUE_NAMESPACE, OperationDefinition, OperationsDocument, Param, ParamDescriptor, type Parameterize, PartialQueryKeyOf, type PerKeyCursorEnvelope, type PlanInput, PrimaryKeyOf, type ProjectionResult, ProjectionTransform, type ProjectionValue, PutInput, QueryMethodSpec, QueryModelContract, type QueryOptions$1 as QueryOptions, QuerySpec, RawCondition, ReadExecOptions, ReadParams, ReadRequestCtx, ReadRequestKind, type RebuildResult, RelationMetadata, RelationOptions, type RelationTraversalOptions, ReplayOptions, RequestContext, ResolvedKey, RetryOverride, RetryPolicy, RetryingExecutor, SelectableOf, type SelfProxy, type SelfRef, ShardId, type SourceProxy, type SourceRef, TableMapping, TransactWriteExecItem, type TransactionDefinition, TransactionItemSpec, type TransactionParamShape, type TransactionRef, TransactionSpec, type TxConditionCheckOptions, type TxForEachInstruction, type TxForEachOptions, type TxInstruction, type TxRecorder, type TxWriteInstruction, type TxWriteOptions, UniqueQueryKeyOf, type UpdateExpressionResult, UpdateInput, type VersionedHistoryOptions, type VersionedLatestOptions, ViewDefinition, type WhenComparison, WriteDefinitionOptions, WriteExecOptions, WriteResult, aggregate, assertBundleSerializable, assertContractBoundaries, assertContractN1Safe, assertJsonSerializable, assertSupportedCondition, belongsTo, binary, boolean, buildBridgeBundle, buildConditionExpression, buildContexts, buildContracts, buildManifest, buildOperations, buildProjection, buildQuerySpec, buildTransactionSpec, buildTransactions, buildUpdateExpression, collectContractBoundaryViolations, collectContractN1Violations, compileFilterExpression, count, createCdcEmulator, createDefaultLinter, createMaintenanceDrain, createMaintenanceDrainHandler, createMaintenanceRebuilder, datetime, decodeCursor, decodePerKeyCursor, defineCommands, defineDelete, defineList, definePut, defineQueries, defineQuery, defineTransaction, defineTransactions, defineUpdate, deriveEdgeWriteItems, deriveEdgeWriteItemsFor, deriveModelEdgeWriteItems, derivePrefix, detectRelationFields, edgeWrites, embedded, encodeCursor, encodePerKeyCursor, evaluateFilter, execute, executeDeclarativeTransaction, executeExplain, executeList, executeQuery, expandTransaction, field, getEdgeWrites, getImplicitKeyFields, gsiAmbiguityRule, hasMany, hasOne, hydrate, isEdgeWritesDefinition, isSelectBuilder, isTransactionRef, list, literal, maintainedFrom, map, max, missingGsiRule, model, noScanRule, number, numberSet, plan, queryBoundaryRule, relationDepthRule, requireLimitRule, resolveKey, resolveRelations, serializeContractKey, serializeFieldValue, string, stringSet, validateDepth, validateGsiAmbiguity, when, whenMember };
3068
+ export { AggregateOptions, AggregateValue, type AnyModelContract, AnyOperationDefinition, BATCH_GET_MAX_KEYS, BATCH_WRITE_MAX_ITEMS, BatchExecOptions, BatchGetExecInput, BatchWriteExecItem, BridgeBundle, type BuiltContracts, CdcEmulator, CdcEmulatorOptions, ChangeEvent, ChangeHandler, ClientManager, type CollectParams, CommandMethodSpec, CommandModelContract, CommandSpec, ConcurrentRecomputeRef, type ConditionExpressionResult, ConditionInput, ConditionSpec, type ContextOwnership, type ContextOwnershipMap, ContextSpec, type ContractBoundaryViolation, type ContractInputs, type ContractMap, type ContractN1Violation, ContractSpec, CtxModel, DDBModel, DefinitionMap, DeleteInput, type DriftReport, DynamoDBOperation, DynamoExecutor, DynamoType, EDGE_WRITES_MARKER, type EdgeLifecycle, type EdgeWriteDeclaration, type EdgeWriteRecorder, type EdgeWritesDefinition, EntityMetadata, EntityRef, EventLog, ExecutionPlan, Executor, ExecutorResult, type ExplainInput, FaultSpec, FieldMetadata, FieldOptions, type FilterExpressionResult, GsiDefinition, type HasManyVersionedCallback, type HasManyVersionedOptions, type HasOneVersionedCallback, type HasOneVersionedOptions, Item, KeyDefinition, type LintResult, type LintRule, Linter, type ListInput, type ListOptions, MAINT_OUTBOX_PK_PREFIX, MAX_TRANSACT_ITEMS, MaintainConsistency, MaintainEffect, MaintainEvent, MaintainUpdateMode, type MaintainedFromCallback, type MaintainedFromOptions, MaintenanceDrain, type MaintenanceDrainOptions, type MaintenanceRebuildOptions, MaintenanceRebuilder, Manifest, MembershipPredicate, MembershipPredicateOp, MetadataRegistry, Middleware, type ModelOptions, ModelStatic, OLD_VALUE_NAMESPACE, OperationDefinition, OperationsDocument, Param, ParamDescriptor, type Parameterize, PartialQueryKeyOf, type PerKeyCursorEnvelope, type PlanInput, PrimaryKeyOf, type ProjectionResult, ProjectionTransform, type ProjectionValue, PutInput, QueryMethodSpec, QueryModelContract, type QueryOptions$1 as QueryOptions, QuerySpec, RawCondition, ReadExecOptions, ReadParams, ReadRequestCtx, ReadRequestKind, type RebuildResult, RelationMetadata, RelationOptions, type RelationTraversalOptions, ReplayOptions, RequestContext, ResolvedKey, RetryOverride, RetryPolicy, RetryingExecutor, SelectableOf, type SelfProxy, type SelfRef, ShardId, type SourceProxy, type SourceRef, TableMapping, TransactWriteExecItem, type TransactionDefinition, TransactionItemSpec, type TransactionParamShape, type TransactionRef, TransactionSpec, type TxConditionCheckOptions, type TxForEachInstruction, type TxForEachOptions, type TxInstruction, type TxRecorder, type TxWriteInstruction, type TxWriteOptions, UniqueQueryKeyOf, type UpdateExpressionResult, UpdateInput, type VersionedHistoryOptions, type VersionedLatestOptions, ViewDefinition, type WhenComparison, WriteDefinitionOptions, WriteExecOptions, WriteResult, aggregate, assertBundleSerializable, assertContractBoundaries, assertContractN1Safe, assertJsonSerializable, assertSupportedCondition, belongsTo, binary, boolean, buildBridgeBundle, buildConditionExpression, buildContexts, buildContracts, buildManifest, buildOperations, buildProjection, buildQuerySpec, buildTransactionSpec, buildTransactions, buildUpdateExpression, collectContractBoundaryViolations, collectContractN1Violations, compileFilterExpression, count, createCdcEmulator, createDefaultLinter, createMaintenanceDrain, createMaintenanceDrainHandler, createMaintenanceRebuilder, datetime, decodeCursor, decodePerKeyCursor, defineCommands, defineDelete, defineList, definePut, defineQueries, defineQuery, defineTransaction, defineTransactions, defineUpdate, deriveEdgeWriteItems, deriveEdgeWriteItemsFor, deriveModelEdgeWriteItems, derivePrefix, detectRelationFields, edgeWrites, embedded, encodeCursor, encodePerKeyCursor, evaluateFilter, execute, executeDeclarativeTransaction, executeExplain, executeList, executeQuery, expandTransaction, field, getEdgeWrites, getImplicitKeyFields, gsiAmbiguityRule, hasMany, hasOne, hydrate, isEdgeWritesDefinition, isSelectBuilder, isTransactionRef, list, literal, maintainedFrom, map, max, missingGsiRule, model, noScanRule, number, numberSet, plan, queryBoundaryRule, relationDepthRule, requireLimitRule, resolveKey, resolveRelations, serializeContractKey, serializeFieldValue, string, stringSet, ttl, validateDepth, validateGsiAmbiguity, when, whenMember };
package/dist/index.js CHANGED
@@ -79,7 +79,7 @@ import {
79
79
  validateDepth,
80
80
  when,
81
81
  wholeKeysSentinel
82
- } from "./chunk-G5RWWBAL.js";
82
+ } from "./chunk-J5A665UW.js";
83
83
  import {
84
84
  CdcEmulator,
85
85
  MAINT_OUTBOX_PK_PREFIX,
@@ -91,7 +91,7 @@ import {
91
91
  orderAndTrimCollection,
92
92
  pathField,
93
93
  projectFrom
94
- } from "./chunk-BROCT574.js";
94
+ } from "./chunk-7NPG5R7O.js";
95
95
  import {
96
96
  BATCH_GET_MAX_KEYS,
97
97
  BATCH_WRITE_MAX_ITEMS,
@@ -154,7 +154,7 @@ import {
154
154
  resolveModelClass,
155
155
  segmentFieldNames,
156
156
  serializeFieldValue
157
- } from "./chunk-CPTV3H2U.js";
157
+ } from "./chunk-FMJIWFIS.js";
158
158
 
159
159
  // src/metadata/prefix.ts
160
160
  function derivePrefix(customPrefix, className) {
@@ -267,6 +267,13 @@ function model(options) {
267
267
  `@model on '${String(context.name)}': \`kind: '${kind}'\` declares a maintained view but no \`@maintainedFrom(...)\` source is stacked. A view with no source is maintained by nothing \u2014 add at least one \`@maintainedFrom\` decorator.`
268
268
  );
269
269
  }
270
+ const ttlFields = fields.filter((f) => f.ttl === true);
271
+ if (ttlFields.length > 1) {
272
+ throw new Error(
273
+ `@model on '${String(context.name)}': ${ttlFields.length} \`@ttl\` fields (${ttlFields.map((f) => `'${f.propertyName}'`).join(", ")}) are declared, but DynamoDB allows exactly one TTL attribute per table. Mark a single field \`@ttl\`.`
274
+ );
275
+ }
276
+ const ttlAttribute = ttlFields.length === 1 ? ttlFields[0].propertyName : void 0;
270
277
  MetadataRegistry.register(target, {
271
278
  tableName: options.table,
272
279
  prefix,
@@ -278,7 +285,8 @@ function model(options) {
278
285
  embeddedFields,
279
286
  kind,
280
287
  ...options.description !== void 0 ? { description: options.description } : {},
281
- maintainedFrom: maintainedFrom2
288
+ maintainedFrom: maintainedFrom2,
289
+ ...ttlAttribute !== void 0 ? { ttlAttribute } : {}
282
290
  });
283
291
  };
284
292
  }
@@ -328,6 +336,13 @@ function literal(...values) {
328
336
  });
329
337
  };
330
338
  }
339
+ var ttl = (_value, context) => {
340
+ collectField({
341
+ propertyName: String(context.name),
342
+ dynamoType: "N",
343
+ ttl: true
344
+ });
345
+ };
331
346
  var string = createFieldDecorator("S");
332
347
  var number = createFieldDecorator("N");
333
348
  var boolean = createFieldDecorator("BOOL");
@@ -1973,6 +1988,7 @@ export {
1973
1988
  serializeFieldValue,
1974
1989
  string,
1975
1990
  stringSet,
1991
+ ttl,
1976
1992
  validateDepth,
1977
1993
  validateGsiAmbiguity,
1978
1994
  when,
@@ -1,4 +1,4 @@
1
- import { E as Executor, D as DynamoDBOperation, R as ReadExecOptions, a as ExecutorResult, B as BatchGetExecInput, P as PutInput, W as WriteExecOptions, b as WriteResult, U as UpdateInput, c as DeleteInput, d as BatchWriteExecItem, e as BatchExecOptions, T as TransactWriteExecItem, M as ModelStatic, f as DDBModel, C as ChangeEvent } from '../types-BWOrWcbd.js';
1
+ import { E as Executor, D as DynamoDBOperation, R as ReadExecOptions, a as ExecutorResult, B as BatchGetExecInput, P as PutInput, W as WriteExecOptions, b as WriteResult, U as UpdateInput, c as DeleteInput, d as BatchWriteExecItem, e as BatchExecOptions, T as TransactWriteExecItem, M as ModelStatic, f as DDBModel, C as ChangeEvent } from '../types-B-F7jw9f.js';
2
2
  import '@aws-sdk/client-dynamodb';
3
3
 
4
4
  /**
@@ -1,12 +1,12 @@
1
1
  import {
2
2
  createCdcEmulator
3
- } from "../chunk-BROCT574.js";
3
+ } from "../chunk-7NPG5R7O.js";
4
4
  import {
5
5
  ClientManager,
6
6
  MetadataRegistry,
7
7
  TableMapping,
8
8
  resolveModelClass
9
- } from "../chunk-CPTV3H2U.js";
9
+ } from "../chunk-FMJIWFIS.js";
10
10
 
11
11
  // src/memory/memory-store.ts
12
12
  function deepClone(value) {
@@ -3126,6 +3126,21 @@ interface ManifestEntity {
3126
3126
  * manifest. Surfaces as the generated Python class docstring.
3127
3127
  */
3128
3128
  readonly description?: string;
3129
+ /**
3130
+ * The physical attribute name of the model's DynamoDB **Time-To-Live** field,
3131
+ * when one is declared via `@ttl` (issue #172, Epic #167 — CFn generator C4).
3132
+ * Unlike the maintenance/stream signals (a maintenance-IR concern kept OFF the
3133
+ * manifest), TTL is a **physical schema** fact, and the CloudFormation emitter
3134
+ * consumes the manifest — so the TTL attribute name flows here and the emitter
3135
+ * renders `TimeToLiveSpecification { AttributeName: <this>, Enabled: true }`. The
3136
+ * attribute is deliberately NOT added to the table's key `AttributeDefinitions`
3137
+ * (that list holds ONLY key/index attributes; `TimeToLiveSpecification` legitimately
3138
+ * names a non-key attribute). **Absent** when the model declares no `@ttl`, so a
3139
+ * TTL-free model serializes byte-identically to the pre-#172 manifest (backward
3140
+ * compatible). DynamoDB permits exactly one TTL attribute per physical table; the
3141
+ * single-per-table rule is enforced at manifest build.
3142
+ */
3143
+ readonly ttlAttribute?: string;
3129
3144
  }
3130
3145
  interface ManifestTable {
3131
3146
  readonly physicalName: string;
@@ -6230,6 +6245,20 @@ interface FieldMetadata {
6230
6245
  * `@number`, …), so plain fields are byte-for-byte unchanged (backward compat).
6231
6246
  */
6232
6247
  literals?: readonly string[];
6248
+ /**
6249
+ * For an `@ttl` field (issue #172, Epic #167 — CFn generator C4) — marks this
6250
+ * field as the model's **DynamoDB Time-To-Live** attribute. `@ttl` is a
6251
+ * standalone semantic decorator pinned to `number` (it records `dynamoType: 'N'`
6252
+ * itself, so `@ttl expiresAt!: string` is a compile error), because DynamoDB TTL
6253
+ * requires a Number attribute holding **epoch seconds**. It is a **physical
6254
+ * schema** fact (unlike the maintenance signals kept off the manifest): the flag
6255
+ * flows through {@link EntityMetadata} into `manifest.json`
6256
+ * ({@link ManifestEntity.ttlAttribute}), which the CloudFormation emitter consumes
6257
+ * to render `TimeToLiveSpecification`. DynamoDB allows exactly one TTL attribute
6258
+ * per table, enforced loudly at model registration (`@model`). `undefined` for
6259
+ * every other field decorator, so non-TTL fields are byte-for-byte unchanged.
6260
+ */
6261
+ ttl?: boolean;
6233
6262
  }
6234
6263
 
6235
6264
  /**
@@ -6530,6 +6559,19 @@ interface EntityMetadata {
6530
6559
  * Absent / empty for a plain entity.
6531
6560
  */
6532
6561
  maintainedFrom?: MaintainedFromMetadata[];
6562
+ /**
6563
+ * The physical attribute name of the model's `@ttl` field, when one is declared
6564
+ * (issue #172, Epic #167 — CFn generator C4). DynamoDB TTL requires a single
6565
+ * Number attribute holding epoch seconds; `@model` derives this from the (unique)
6566
+ * field carrying {@link FieldMetadata.ttl} and throws if more than one field on the
6567
+ * SAME model is `@ttl` (the cross-entity, same-physical-table single-TTL rule is
6568
+ * enforced downstream, at manifest build, where all entities on a table are known).
6569
+ * A non-key field's physical attribute name is its property name, so this is the
6570
+ * `@ttl` field's property name. Propagated to the manifest
6571
+ * ({@link ManifestEntity.ttlAttribute}) — the SSoT the CloudFormation emitter reads
6572
+ * to render `TimeToLiveSpecification`. Absent when the model declares no `@ttl`.
6573
+ */
6574
+ ttlAttribute?: string;
6533
6575
  }
6534
6576
 
6535
6577
  /**
@@ -0,0 +1,203 @@
1
+ # CDC Emulator
2
+
3
+ The CDC (Change Data Capture) emulator produces DynamoDB-Streams-equivalent change events from GraphDDB writes and delivers them to a consumer under a deterministic, fault-injectable delivery contract. It exists to drive and verify stream-based logic — incremental aggregation in particular (see the [Aggregate Tree Pattern](../examples/aggregate-tree-pattern/README.md)) — locally and in CI.
4
+
5
+ The emulator is **dev/test only**. DynamoDB Local does not support Streams, so there is no standard way to exercise stream consumers off-cloud. The emulator fills that gap. Its `ChangeEvent` type and delivery contract mirror real DynamoDB Streams (`NEW_AND_OLD_IMAGES`), so a consumer written against the emulator runs unchanged against real Streams in production.
6
+
7
+ The emulator is responsible only for **producing, delivering, faulting, and replaying change events**. It performs no aggregation; aggregate logic lives entirely in the consumer.
8
+
9
+ ## Scope
10
+
11
+ | In scope | Out of scope |
12
+ |---|---|
13
+ | Capturing writes and producing `ChangeEvent`s | Running aggregations (consumer's responsibility) |
14
+ | OldImage / NewImage attachment | Faithful reproduction of real Streams internals (shard splits, iterator age) |
15
+ | Per-shard ordering and batched delivery | Cross-region / GSI streams, TTL-delete events |
16
+ | Fault injection (duplicate, reorder, delay, drop, partial-batch failure) | A user-facing public write hook |
17
+ | Record / replay | Production use |
18
+
19
+ ## The `ChangeEvent` type
20
+
21
+ `ChangeEvent` is owned by the `cdc` module; `core` never references it. The shape mirrors a DynamoDB Streams record under the `NEW_AND_OLD_IMAGES` view, which is the only view supported — incremental aggregation requires both images.
22
+
23
+ ```ts
24
+ type StreamViewType = 'NEW_AND_OLD_IMAGES';
25
+ type ChangeEventName = 'INSERT' | 'MODIFY' | 'REMOVE';
26
+
27
+ interface ChangeEvent<T = Record<string, unknown>> {
28
+ eventName: ChangeEventName;
29
+ table: string;
30
+ model?: string; // resolved model name when known
31
+ keys: { pk: string; sk: string };
32
+ oldImage?: T; // present for MODIFY / REMOVE
33
+ newImage?: T; // present for INSERT / MODIFY
34
+ approximateCreationTime: string; // ISO 8601, derived from the clock
35
+ sequenceNumber: string; // monotonic within a shard, zero-padded
36
+ shardId: string; // partition = hash(pk)
37
+ }
38
+
39
+ interface ChangeBatch<T = Record<string, unknown>> {
40
+ records: ChangeEvent<T>[]; // same-shard records in sequenceNumber order
41
+ }
42
+
43
+ interface BatchResult {
44
+ batchItemFailures?: string[]; // sequenceNumbers the consumer failed to process
45
+ }
46
+
47
+ type ChangeHandler = (batch: ChangeBatch) => Promise<BatchResult | void>;
48
+ ```
49
+
50
+ `eventName` is derived from the write: a `delete` yields `REMOVE`; a `put`/`update` yields `MODIFY` when a prior image existed, otherwise `INSERT`. `sequenceNumber` is the per-shard monotonic counter rendered as a 20-digit zero-padded string so lexicographic order equals numeric order. `approximateCreationTime` is derived from the active clock (virtual or real).
51
+
52
+ A consumer derives a delta from `oldImage`/`newImage` and may use `approximateCreationTime` as the source change time.
53
+
54
+ ## Capture seam
55
+
56
+ GraphDDB captures writes through an internal, non-exported seam in `core`. The write paths (`executePut`/`Update`/`Delete`/`batch`/`transaction`) call `captureWrite(record)` **after a successful write**. The seam carries only a minimal `RawWriteRecord` (`table`, `model?`, `op`, `keys`, `newItem?`, `oldItem?`); `core` knows nothing of images, sequence numbers, or shards.
57
+
58
+ - The seam is `@internal` and is **not** re-exported from `src/index.ts`. It is the private internal mechanism, distinct from any user-facing hook (which is out of scope). The dependency direction is always `core ← cdc`; `core` does not import the cdc module, so there is no cycle.
59
+ - `ChangeCaptureRegistry` (core-owned, `@internal`) supports multiple subscribers via `register(subscriber): Unsubscribe`. With zero subscribers the seam is a no-op, so there is zero overhead when nothing is listening.
60
+ - A pre-write image (`ReturnValues: ALL_OLD`) is requested only when the model is stream-enabled **and** a subscriber is live, so the OldImage cost is never paid when nobody is listening.
61
+ - Writes that did not actually land (e.g. `ConditionalCheckFailedException`) are not captured.
62
+ - Capture failures are isolated: a throwing subscriber neither fails the originating write nor blocks other subscribers (errors are swallowed at the seam).
63
+
64
+ The cdc module registers as one subscriber and maps each `RawWriteRecord` to a `ChangeEvent`, assigning `shardId`, `sequenceNumber`, and `approximateCreationTime`. Stream enablement is opt-in per model: each class in `opts.models` is stream-enabled on construction (`ChangeCaptureRegistry.enableStream`) and disabled on `close()`. When `models` is provided, records whose resolved model name is not in the set are dropped; a record carrying no model name is emitted.
65
+
66
+ ## Delivery contract
67
+
68
+ - **Sharding.** `shardId = hash(pk) % shardCount` (FNV-1a, default `shardCount` 8). All events for one partition key land in the same shard.
69
+ - **Per-shard ordering.** Within a shard, records are delivered in ascending `sequenceNumber`. Order across shards is independent.
70
+ - **Batching.** Records are delivered in batches of up to `batchSize` (default 100) per shard.
71
+ - **At-least-once.** Duplicate delivery is permitted and is reproducible via fault injection.
72
+ - **Checkpoint / resume.** Each shard tracks the last successfully checkpointed sequence number, available via `checkpoints()`. The checkpoint advances only over the **contiguous prefix** of acked sequences (`ReportBatchItemFailures` semantics): it never moves past an un-acked record awaiting redelivery, so a resume cannot skip a record.
73
+ - **Partial-batch failure.** The consumer returns the `sequenceNumber`s it failed to process in `BatchResult.batchItemFailures`. Successful records are checkpointed; failed records are redelivered, re-sorted into ascending order so per-shard ordering is preserved.
74
+ - **Dead-letter queue.** A record that fails more than `maxRetries` times (default 3) is moved to the dead-letter buffer, available via `deadLetters()`.
75
+
76
+ ## Modes
77
+
78
+ The `mode` option selects when events are delivered.
79
+
80
+ | Mode | Use | Delivery timing |
81
+ |---|---|---|
82
+ | `inline` | Unit tests | Delivered automatically after each capture. The capture seam is synchronous; the async delivery is chained onto an internal promise that callers await via `flush()`. |
83
+ | `queued` | Time-dependent behavior (throttle, sweep period, staleness) | Buffered. Delivery is driven by `pump()` or by advancing the virtual clock with `advanceClock()`. |
84
+ | `record` | Capture a log without delivering | Events are recorded but not delivered; obtain the log with `record()`. |
85
+ | `replay` | Replay-equivalence verification | Delivery is driven by `replay(log, opts?)`. |
86
+
87
+ Every mode records events into an internal log, so `record()` and `replay()` are available regardless of mode.
88
+
89
+ ## Virtual clock
90
+
91
+ The `clock` option is `'real'` or `'virtual'`; the default is `'virtual'` for `queued` mode and `'real'` otherwise. Under the virtual clock, time advances only through `advanceClock(ms)`, which moves the clock forward and delivers every record whose due time has arrived. `now()` returns the current virtual time in milliseconds since the emulator epoch (`2020-01-01T00:00:00Z`), and `approximateCreationTime` is derived from it. This makes throttle period, sweep period, and staleness behavior deterministic without depending on wall-clock time. `advanceClock()` throws if the clock is not virtual.
92
+
93
+ ## Fault injection
94
+
95
+ Faults are configured via `fault(spec)` (the spec is merged into the current one). All randomness routes through a seeded PRNG (`seed`, default 1), so a given seed reproduces the exact same delivery — including duplicates, reorderings, delays, and failures.
96
+
97
+ ```ts
98
+ interface FaultSpec {
99
+ duplicate?: number; // probability a delivered record is delivered again
100
+ reorder?: boolean; // shuffle within-shard order on delivery
101
+ delay?: { min: number; max: number }; // per-record virtual-time delivery delay (ms); requires queued mode
102
+ dropThenRedeliver?: number; // probability a record is dropped on first delivery, then redelivered
103
+ partialBatchFailure?: number; // probability a record is reported as a partial-batch failure
104
+ }
105
+ ```
106
+
107
+ `reorder` deliberately violates the within-shard ordering guarantee to exercise a consumer's invariant detection. `delay` withholds a record until the virtual clock reaches its due time.
108
+
109
+ ### Concurrent-recompute injection
110
+
111
+ To stage a mark-versus-recompute race (the false-clean race), the harness arms two hooks:
112
+
113
+ ```ts
114
+ emulator.injectConcurrentRecompute({ ref: 'Parent#p1' }); // which ref to race
115
+ emulator.onConcurrentRecompute((ref, event) => { /* run the recompute */ });
116
+ ```
117
+
118
+ Once both are set, the emulator invokes the registered recompute callback for each record immediately before that record reaches the consumer, deterministically interleaving the recompute with the consumer's mark. `injectConcurrentRecompute` alone records the ref; `onConcurrentRecompute` supplies the actual recompute and arms the race.
119
+
120
+ ## API
121
+
122
+ ```ts
123
+ createCdcEmulator(opts?: CdcEmulatorOptions): CdcEmulator
124
+
125
+ class CdcEmulator {
126
+ subscribe(handler: ChangeHandler): Unsubscribe;
127
+ flush(): Promise<void>; // await inline-mode deliveries
128
+ pump(maxBatches?: number): Promise<void>;
129
+ advanceClock(ms: number): Promise<void>;
130
+ now(): number;
131
+ fault(spec: FaultSpec): void;
132
+ injectConcurrentRecompute(ref: ConcurrentRecomputeRef): void;
133
+ onConcurrentRecompute(hook: (ref, event) => void | Promise<void>): void;
134
+ record(): EventLog;
135
+ replay(log: EventLog, opts?: ReplayOptions): Promise<void>;
136
+ checkpoints(): Record<ShardId, string>;
137
+ deadLetters(): ChangeEvent[];
138
+ reset(): void; // reset delivery state and re-seed (keeps subscription + opt-in)
139
+ close(): void; // detach from the seam, disable model streaming
140
+ }
141
+ ```
142
+
143
+ `subscribe` registers the single consumer; when `startingPosition` is `'LATEST'`, records buffered before the subscription are dropped. `record()` returns `{ seed, events }`. `replay(log, opts)` re-shards the log by `hash(pk)`, optionally shuffles and duplicates it (`ReplayOptions = { shuffle?: boolean; duplicate?: number }`), then re-sorts each shard into ascending `sequenceNumber` order before delivering — so cross-shard reordering and at-least-once chaos are modelled while the within-shard ordering guarantee that aggregation depends on is preserved.
144
+
145
+ ## Options
146
+
147
+ ```ts
148
+ interface CdcEmulatorOptions {
149
+ models?: Function[]; // stream-enabled model classes (opt-in; default none)
150
+ mode?: CdcMode; // default 'inline'
151
+ clock?: ClockMode; // default 'virtual' for queued, 'real' otherwise
152
+ batchSize?: number; // default 100
153
+ maxRetries?: number; // default 3
154
+ startingPosition?: 'TRIM_HORIZON' | 'LATEST'; // default 'TRIM_HORIZON'
155
+ seed?: number; // fault / shard determinism; default 1
156
+ shardCount?: number; // default 8
157
+ }
158
+ ```
159
+
160
+ A `ModelStatic` (from `DDBModel.asModel()`) or a raw decorated class may be passed in `models`; a `ModelStatic` is unwrapped to its inner class so the stream opt-in and model-name filter align with the class the write paths key on.
161
+
162
+ ## Connecting to a consumer
163
+
164
+ The emulator emits generic `ChangeEvent`s; wiring them to an aggregate is the consumer's job. A consumer subscribes, derives the work to do from each event, and reports per-record outcomes. Below, a consumer rebuilds a leaf-sum aggregate idempotently from `newImage`:
165
+
166
+ ```ts
167
+ const cdc = createCdcEmulator({ models: [CounterModel], mode: 'queued', seed: 1 });
168
+
169
+ const latestByLeaf = new Map<string, number>();
170
+ cdc.subscribe(async (batch) => {
171
+ for (const e of batch.records) {
172
+ const key = `${e.keys.pk}|${e.keys.sk}`;
173
+ if (e.eventName === 'REMOVE') latestByLeaf.delete(key);
174
+ else latestByLeaf.set(key, Number((e.newImage as { value: number }).value));
175
+ }
176
+ // return { batchItemFailures: [...] } to redeliver specific records
177
+ });
178
+
179
+ await Counter.putItem({ siteId: 's1', leafId: 'a', value: 10 });
180
+ await cdc.advanceClock(1); // drive delivery under the virtual clock
181
+ ```
182
+
183
+ Sweep scheduling lives outside the emulator: the emulator handles only change production and delivery. In production, swap `createCdcEmulator` for a real Streams consumer; the consumer code is unchanged because the `ChangeEvent` type and delivery contract match.
184
+
185
+ ### Recording and replaying
186
+
187
+ ```ts
188
+ const recorder = createCdcEmulator({ models: [CounterModel], mode: 'record', seed: 4 });
189
+ await Counter.putItem({ siteId: 's1', leafId: 'a', value: 5 });
190
+ await Counter.updateItem({ siteId: 's1', leafId: 'a' }, { value: 9 });
191
+ const log = recorder.record();
192
+ recorder.close();
193
+
194
+ const replayer = createCdcEmulator({ mode: 'replay', seed: 4 });
195
+ replayer.subscribe(/* idempotent, key-addressed consumer */);
196
+ await replayer.replay(log, { shuffle: true, duplicate: 0.5 });
197
+ ```
198
+
199
+ Because the consumer is idempotent and the replay preserves per-shard ordering, the final aggregate equals a full recompute from source regardless of the global delivery order or duplicates (replay equivalence).
200
+
201
+ ## Related
202
+
203
+ - Pattern: [Aggregate Tree Pattern](../examples/aggregate-tree-pattern/README.md)