@photon-grove/evt 0.0.0 → 0.1.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.
Files changed (149) hide show
  1. package/LICENSE +176 -0
  2. package/README.md +164 -1
  3. package/dist/aggregate.d.ts +83 -0
  4. package/dist/aggregate.d.ts.map +1 -0
  5. package/dist/aggregate.js +92 -0
  6. package/dist/aggregate.js.map +1 -0
  7. package/dist/base64.d.ts +8 -0
  8. package/dist/base64.d.ts.map +1 -0
  9. package/dist/base64.js +67 -0
  10. package/dist/base64.js.map +1 -0
  11. package/dist/codec.d.ts +56 -0
  12. package/dist/codec.d.ts.map +1 -0
  13. package/dist/codec.js +90 -0
  14. package/dist/codec.js.map +1 -0
  15. package/dist/commit-id.d.ts +3 -0
  16. package/dist/commit-id.d.ts.map +1 -0
  17. package/dist/commit-id.js +43 -0
  18. package/dist/commit-id.js.map +1 -0
  19. package/dist/dynamodb/client.d.ts +83 -0
  20. package/dist/dynamodb/client.d.ts.map +1 -0
  21. package/dist/dynamodb/client.js +6 -0
  22. package/dist/dynamodb/client.js.map +1 -0
  23. package/dist/dynamodb/head.d.ts +34 -0
  24. package/dist/dynamodb/head.d.ts.map +1 -0
  25. package/dist/dynamodb/head.js +62 -0
  26. package/dist/dynamodb/head.js.map +1 -0
  27. package/dist/dynamodb/index.d.ts +6 -0
  28. package/dist/dynamodb/index.d.ts.map +1 -0
  29. package/dist/dynamodb/index.js +8 -0
  30. package/dist/dynamodb/index.js.map +1 -0
  31. package/dist/dynamodb/item.d.ts +70 -0
  32. package/dist/dynamodb/item.d.ts.map +1 -0
  33. package/dist/dynamodb/item.js +264 -0
  34. package/dist/dynamodb/item.js.map +1 -0
  35. package/dist/dynamodb/marker.d.ts +11 -0
  36. package/dist/dynamodb/marker.d.ts.map +1 -0
  37. package/dist/dynamodb/marker.js +56 -0
  38. package/dist/dynamodb/marker.js.map +1 -0
  39. package/dist/dynamodb/repository.d.ts +61 -0
  40. package/dist/dynamodb/repository.d.ts.map +1 -0
  41. package/dist/dynamodb/repository.js +731 -0
  42. package/dist/dynamodb/repository.js.map +1 -0
  43. package/dist/dynamodb/requests.d.ts +65 -0
  44. package/dist/dynamodb/requests.d.ts.map +1 -0
  45. package/dist/dynamodb/requests.js +179 -0
  46. package/dist/dynamodb/requests.js.map +1 -0
  47. package/dist/envelope.d.ts +15 -0
  48. package/dist/envelope.d.ts.map +1 -0
  49. package/dist/envelope.js +59 -0
  50. package/dist/envelope.js.map +1 -0
  51. package/dist/errors.d.ts +205 -0
  52. package/dist/errors.d.ts.map +1 -0
  53. package/dist/errors.js +240 -0
  54. package/dist/errors.js.map +1 -0
  55. package/dist/event.d.ts +47 -0
  56. package/dist/event.d.ts.map +1 -0
  57. package/dist/event.js +64 -0
  58. package/dist/event.js.map +1 -0
  59. package/dist/features.d.ts +55 -0
  60. package/dist/features.d.ts.map +1 -0
  61. package/dist/features.js +126 -0
  62. package/dist/features.js.map +1 -0
  63. package/dist/fields.d.ts +31 -0
  64. package/dist/fields.d.ts.map +1 -0
  65. package/dist/fields.js +105 -0
  66. package/dist/fields.js.map +1 -0
  67. package/dist/index.d.ts +15 -0
  68. package/dist/index.d.ts.map +1 -0
  69. package/dist/index.js +17 -0
  70. package/dist/index.js.map +1 -0
  71. package/dist/json.d.ts +47 -0
  72. package/dist/json.d.ts.map +1 -0
  73. package/dist/json.js +458 -0
  74. package/dist/json.js.map +1 -0
  75. package/dist/memory/index.d.ts +2 -0
  76. package/dist/memory/index.d.ts.map +1 -0
  77. package/dist/memory/index.js +3 -0
  78. package/dist/memory/index.js.map +1 -0
  79. package/dist/memory/repository.d.ts +70 -0
  80. package/dist/memory/repository.d.ts.map +1 -0
  81. package/dist/memory/repository.js +308 -0
  82. package/dist/memory/repository.js.map +1 -0
  83. package/dist/metadata.d.ts +47 -0
  84. package/dist/metadata.d.ts.map +1 -0
  85. package/dist/metadata.js +75 -0
  86. package/dist/metadata.js.map +1 -0
  87. package/dist/repository.d.ts +131 -0
  88. package/dist/repository.d.ts.map +1 -0
  89. package/dist/repository.js +130 -0
  90. package/dist/repository.js.map +1 -0
  91. package/dist/retry.d.ts +25 -0
  92. package/dist/retry.d.ts.map +1 -0
  93. package/dist/retry.js +50 -0
  94. package/dist/retry.js.map +1 -0
  95. package/dist/snapshots.d.ts +21 -0
  96. package/dist/snapshots.d.ts.map +1 -0
  97. package/dist/snapshots.js +35 -0
  98. package/dist/snapshots.js.map +1 -0
  99. package/dist/store.d.ts +103 -0
  100. package/dist/store.d.ts.map +1 -0
  101. package/dist/store.js +246 -0
  102. package/dist/store.js.map +1 -0
  103. package/dist/testing/equal.d.ts +22 -0
  104. package/dist/testing/equal.d.ts.map +1 -0
  105. package/dist/testing/equal.js +121 -0
  106. package/dist/testing/equal.js.map +1 -0
  107. package/dist/testing/index.d.ts +3 -0
  108. package/dist/testing/index.d.ts.map +1 -0
  109. package/dist/testing/index.js +5 -0
  110. package/dist/testing/index.js.map +1 -0
  111. package/dist/testing/scenario.d.ts +136 -0
  112. package/dist/testing/scenario.d.ts.map +1 -0
  113. package/dist/testing/scenario.js +307 -0
  114. package/dist/testing/scenario.js.map +1 -0
  115. package/dist/utf8.d.ts +17 -0
  116. package/dist/utf8.d.ts.map +1 -0
  117. package/dist/utf8.js +82 -0
  118. package/dist/utf8.js.map +1 -0
  119. package/package.json +58 -2
  120. package/src/aggregate.ts +196 -0
  121. package/src/base64.ts +80 -0
  122. package/src/codec.ts +139 -0
  123. package/src/commit-id.ts +50 -0
  124. package/src/dynamodb/client.ts +102 -0
  125. package/src/dynamodb/head.ts +93 -0
  126. package/src/dynamodb/index.ts +45 -0
  127. package/src/dynamodb/item.ts +334 -0
  128. package/src/dynamodb/marker.ts +64 -0
  129. package/src/dynamodb/repository.ts +1030 -0
  130. package/src/dynamodb/requests.ts +256 -0
  131. package/src/envelope.ts +67 -0
  132. package/src/errors.ts +365 -0
  133. package/src/event.ts +117 -0
  134. package/src/features.ts +175 -0
  135. package/src/fields.ts +134 -0
  136. package/src/globals.d.ts +39 -0
  137. package/src/index.ts +98 -0
  138. package/src/json.ts +546 -0
  139. package/src/memory/index.ts +11 -0
  140. package/src/memory/repository.ts +463 -0
  141. package/src/metadata.ts +120 -0
  142. package/src/repository.ts +275 -0
  143. package/src/retry.ts +74 -0
  144. package/src/snapshots.ts +42 -0
  145. package/src/store.ts +377 -0
  146. package/src/testing/equal.ts +146 -0
  147. package/src/testing/index.ts +14 -0
  148. package/src/testing/scenario.ts +406 -0
  149. package/src/utf8.ts +92 -0
@@ -0,0 +1,131 @@
1
+ import { type SerializedEvent, type SerializedSnapshot } from './event.js';
2
+ /** A snapshot to write with a commit. Its event sequence is the commit's last event. */
3
+ export interface NewSnapshot {
4
+ entityType: string;
5
+ /** The stored snapshot's sequence plus one, or 1 for the stream's first snapshot. */
6
+ sequence: bigint;
7
+ /** The aggregate state after the commit's events. */
8
+ payload: Uint8Array;
9
+ }
10
+ /**
11
+ * Everything a repository writes atomically for one command. A commit with no events is a no-op,
12
+ * and its snapshot is ignored.
13
+ */
14
+ export interface Commit {
15
+ /** The stream the events are appended to. */
16
+ entityId: string;
17
+ /** The new events, in ascending sequence order. */
18
+ events: SerializedEvent[];
19
+ /** The snapshot to write, when the commit reaches a snapshot boundary. */
20
+ snapshot?: NewSnapshot | null | undefined;
21
+ }
22
+ /** Options every repository call takes. */
23
+ export interface CallOptions {
24
+ /**
25
+ * Aborts the call. An abort before the call starts rejects with the signal's reason and writes
26
+ * nothing. An abort while a live backend's write is in flight rejects too, but the write may
27
+ * have been applied: load the stream again before deciding it wasn't.
28
+ */
29
+ signal?: AbortSignal | undefined;
30
+ }
31
+ /**
32
+ * An append-only event log with one optional snapshot and one head per stream (ADR 0007). Every
33
+ * backend must:
34
+ *
35
+ * - refuse a commit built wrong ({@link validateCommit}) with `invalidCommit`;
36
+ * - keep a head per stream, the highest event sequence ever committed to it, which a commit
37
+ * raises to its last event and compaction never touches;
38
+ * - refuse a commit whose first event doesn't follow the stored head with a
39
+ * {@link ConflictError}, or with a {@link SequenceGapError} when it skips past the head;
40
+ * - report a stream whose head disagrees with its events or snapshot as a
41
+ * {@link StreamHeadDriftError}, ranked above a gap and any other conflict;
42
+ * - refuse a commit's snapshot whose sequence isn't the stored snapshot's plus one (1 with none
43
+ * stored) with a {@link ConflictError};
44
+ * - write a commit's events, head and snapshot atomically, or nothing;
45
+ * - refuse an entity ID in a reserved namespace on every write path, before any request;
46
+ * - refuse every write (a commit, a snapshot write, compaction) to a store whose feature manifest
47
+ * declares a pair this package doesn't model, with an {@link UnsupportedFeatureError}, and to
48
+ * one that doesn't declare stream-heads: with a {@link StreamHeadsNotMigratedError} when it
49
+ * holds data, and otherwise either by declaring stream-heads on the first commit or snapshot
50
+ * write or with a {@link StoreNotInitializedError}, as the backend initializes;
51
+ * - refuse a commit, but not a snapshot write or compaction, while a projection rebuild holds the
52
+ * store, with a {@link RebuildInProgressError}, after the manifest's refusals and before every
53
+ * stream's;
54
+ * - refuse compaction of an entity whose type the manifest declares history-dependent, with a
55
+ * {@link HistoryDependentProjectionError};
56
+ * - return events in ascending sequence order, and `latest` in descending order.
57
+ */
58
+ export interface Repository {
59
+ /** Appends a commit's events, and its snapshot if it has one, atomically. */
60
+ commit(commit: Commit, options?: CallOptions): Promise<void>;
61
+ /** Every stored event of the stream, ascending. */
62
+ events(entityId: string, options?: CallOptions): Promise<SerializedEvent[]>;
63
+ /** The stream's events after sequence `after`, ascending. */
64
+ eventsAfter(entityId: string, after: bigint, options?: CallOptions): Promise<SerializedEvent[]>;
65
+ /**
66
+ * The stream's last `n` events, newest first, never the snapshot. `n` must be a safe integer of
67
+ * at least 1, or the call fails with `invalidArgument` before any request.
68
+ */
69
+ latest(entityId: string, n: number, options?: CallOptions): Promise<SerializedEvent[]>;
70
+ /** The stream's snapshot, or null when it has none. */
71
+ snapshot(entityId: string, options?: CallOptions): Promise<SerializedSnapshot | null>;
72
+ /**
73
+ * Writes a snapshot outside a commit, such as a catch-up snapshot. It never lowers the stored
74
+ * snapshot's event sequence: it replaces a snapshot at or below it and resolves `true`, or
75
+ * leaves a newer one in place and resolves `false`. The stream's head must cover the snapshot,
76
+ * or the write fails with a {@link StreamHeadDriftError}. A snapshot at event sequence 0 needs no
77
+ * head.
78
+ */
79
+ putSnapshot(snapshot: SerializedSnapshot, options?: CallOptions): Promise<boolean>;
80
+ /**
81
+ * Deletes the stream's events with sequences 1 to `through`, but only when the stored snapshot
82
+ * covers them (its event sequence is at least `through`), and resolves to how many it deleted.
83
+ * It never deletes the snapshot or touches the head. A `through` below 1 deletes nothing.
84
+ */
85
+ compactBelow(entityId: string, through: bigint, options?: CallOptions): Promise<number>;
86
+ }
87
+ /**
88
+ * A repository whose store can be initialized explicitly: its feature manifest declares
89
+ * stream-heads before any writer writes. A strict backend, such as DynamoDB by default, refuses
90
+ * every write until then. One whose writers initialize a fresh store on their first write, such
91
+ * as the in-memory one, needs it only where code treats every backend alike.
92
+ */
93
+ export interface StoreInitializer {
94
+ /**
95
+ * Declares stream-heads on a store that holds no data, and resolves `true` when this call
96
+ * declared it. A store that already declares it is left alone and resolves `false`. One that
97
+ * holds data without it is refused with a {@link StreamHeadsNotMigratedError}, and one that
98
+ * declares an unsupported feature with an {@link UnsupportedFeatureError}.
99
+ */
100
+ initialize(options?: CallOptions): Promise<boolean>;
101
+ }
102
+ /** The most actions one commit may carry, DynamoDB's transaction limit. */
103
+ export declare const MAX_COMMIT_ACTIONS = 100;
104
+ /** The most bytes one commit may carry, summed over its actions: 95% of DynamoDB's 4 MB. */
105
+ export declare const MAX_COMMIT_BYTES = 3800000;
106
+ /** The prefixes of the entity IDs only the engine writes. */
107
+ export declare const RESERVED_PREFIXES: readonly string[];
108
+ /** Whether an entity ID is in a reserved namespace. */
109
+ export declare function isReserved(entityId: string): boolean;
110
+ /** Refuses an entity ID in a reserved namespace, on a write path, before any request. */
111
+ export declare function checkNotReserved(entityId: string): void;
112
+ /**
113
+ * Checks that a commit's events can be stored as written: each belongs to the commit's entity and
114
+ * has the ID `{entityId}:{sequence}`, they share one entity type, the first sequence is at least
115
+ * 1, and the sequences are contiguous. A snapshot must have the events' entity type and a
116
+ * sequence of at least 1. A commit with no events is valid, and its snapshot isn't checked.
117
+ */
118
+ export declare function validateCommit(commit: Commit): void;
119
+ /**
120
+ * The size of each action a commit's transaction carries, in order: the feature-marker and
121
+ * rebuild-lock checks, then the stream's head update, its event puts, and its snapshot put when
122
+ * it writes one. `ttl` gives the expiry an entity type's items carry, if it's retained. Every
123
+ * backend budgets a commit by these sizes, so an oversized commit fails the same way on each.
124
+ */
125
+ export declare function commitSizes(commit: Commit, ttl: (entityType: string) => number | undefined): number[];
126
+ /**
127
+ * Refuses a commit whose actions are over the budget: more than {@link MAX_COMMIT_ACTIONS}, more
128
+ * than {@link MAX_COMMIT_BYTES} in all, or one over {@link MAX_COMMIT_ITEM_BYTES}.
129
+ */
130
+ export declare function checkBudget(sizes: readonly number[]): void;
131
+ //# sourceMappingURL=repository.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"repository.d.ts","sourceRoot":"","sources":["../src/repository.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,130 @@
1
+ // The storage contract every backend implements, and the commit rules every backend applies the
2
+ // same way: validation, the reserved namespaces, and the transaction budget.
3
+ import { headActionSize, headUpdate } from './dynamodb/head.js';
4
+ import { eventItem, itemSize, snapshotItem } from './dynamodb/item.js';
5
+ import { FEATURES_PK, REBUILD_LOCK_PK } from './dynamodb/marker.js';
6
+ import { CommitTooLargeError, EvtError, ReservedEntityIdError } from './errors.js';
7
+ import { eventId } from './event.js';
8
+ import { MAX_COMMIT_ITEM_BYTES } from './snapshots.js';
9
+ /** The most actions one commit may carry, DynamoDB's transaction limit. */
10
+ export const MAX_COMMIT_ACTIONS = 100;
11
+ /** The most bytes one commit may carry, summed over its actions: 95% of DynamoDB's 4 MB. */
12
+ export const MAX_COMMIT_BYTES = 3_800_000;
13
+ /** The prefixes of the entity IDs only the engine writes. */
14
+ export const RESERVED_PREFIXES = Object.freeze([
15
+ 'SYSTEM#',
16
+ 'command-outcome#',
17
+ ]);
18
+ /** Whether an entity ID is in a reserved namespace. */
19
+ export function isReserved(entityId) {
20
+ return RESERVED_PREFIXES.some((prefix) => entityId.startsWith(prefix));
21
+ }
22
+ /** Refuses an entity ID in a reserved namespace, on a write path, before any request. */
23
+ export function checkNotReserved(entityId) {
24
+ if (isReserved(entityId)) {
25
+ throw new ReservedEntityIdError(entityId);
26
+ }
27
+ }
28
+ /**
29
+ * Checks that a commit's events can be stored as written: each belongs to the commit's entity and
30
+ * has the ID `{entityId}:{sequence}`, they share one entity type, the first sequence is at least
31
+ * 1, and the sequences are contiguous. A snapshot must have the events' entity type and a
32
+ * sequence of at least 1. A commit with no events is valid, and its snapshot isn't checked.
33
+ */
34
+ export function validateCommit(commit) {
35
+ const [first] = commit.events;
36
+ if (first === undefined) {
37
+ return;
38
+ }
39
+ for (const event of commit.events) {
40
+ if (event.entityType !== first.entityType) {
41
+ throw invalidCommit(`event ${event.id} has entity type ${event.entityType}, but the commit's first event ` +
42
+ `has ${first.entityType}`);
43
+ }
44
+ }
45
+ const snapshot = commit.snapshot;
46
+ if (snapshot && snapshot.entityType !== first.entityType) {
47
+ throw invalidCommit(`the snapshot has entity type ${snapshot.entityType}, but the events have ` +
48
+ first.entityType);
49
+ }
50
+ if (snapshot && snapshot.sequence < 1n) {
51
+ throw invalidCommit(`snapshot sequence ${snapshot.sequence} is below 1; snapshot sequences start at 1`);
52
+ }
53
+ for (const event of commit.events) {
54
+ if (event.entityId !== commit.entityId) {
55
+ throw invalidCommit(`event ${event.id} belongs to entity ${event.entityId}, not ${commit.entityId}`);
56
+ }
57
+ const id = eventId(event.entityId, event.sequence);
58
+ if (event.id !== id) {
59
+ throw invalidCommit(`event ${event.id} should have the ID ${id}`);
60
+ }
61
+ }
62
+ if (first.sequence < 1n) {
63
+ throw invalidCommit(`event sequence ${first.sequence} is below 1; sequences start at 1`);
64
+ }
65
+ for (let i = 1; i < commit.events.length; i++) {
66
+ const previous = commit.events[i - 1].sequence;
67
+ const next = commit.events[i].sequence;
68
+ if (next !== previous + 1n) {
69
+ throw invalidCommit(`event sequence ${next} follows ${previous}; sequences must be contiguous`);
70
+ }
71
+ }
72
+ }
73
+ /**
74
+ * The size of each action a commit's transaction carries, in order: the feature-marker and
75
+ * rebuild-lock checks, then the stream's head update, its event puts, and its snapshot put when
76
+ * it writes one. `ttl` gives the expiry an entity type's items carry, if it's retained. Every
77
+ * backend budgets a commit by these sizes, so an oversized commit fails the same way on each.
78
+ */
79
+ export function commitSizes(commit, ttl) {
80
+ // The control rows every commit checks: the feature marker and the rebuild lock.
81
+ const sizes = [controlCheckSize(FEATURES_PK), controlCheckSize(REBUILD_LOCK_PK)];
82
+ const first = commit.events[0];
83
+ const last = commit.events.at(-1);
84
+ if (first === undefined || last === undefined) {
85
+ return sizes;
86
+ }
87
+ const head = headUpdate(commit.entityId, first.sequence, last.sequence, ttl(first.entityType));
88
+ sizes.push(headActionSize(head));
89
+ for (const event of commit.events) {
90
+ sizes.push(itemSize(eventItem(event, ttl(event.entityType))));
91
+ }
92
+ const snapshot = commit.snapshot;
93
+ if (snapshot) {
94
+ const stored = {
95
+ entityType: snapshot.entityType,
96
+ entityId: commit.entityId,
97
+ sequence: snapshot.sequence,
98
+ eventSequence: last.sequence,
99
+ payload: snapshot.payload,
100
+ };
101
+ sizes.push(itemSize(snapshotItem(stored, ttl(snapshot.entityType))));
102
+ }
103
+ return sizes;
104
+ }
105
+ /**
106
+ * Refuses a commit whose actions are over the budget: more than {@link MAX_COMMIT_ACTIONS}, more
107
+ * than {@link MAX_COMMIT_BYTES} in all, or one over {@link MAX_COMMIT_ITEM_BYTES}.
108
+ */
109
+ export function checkBudget(sizes) {
110
+ let bytes = 0;
111
+ let largestItem = 0;
112
+ for (const size of sizes) {
113
+ bytes += size;
114
+ largestItem = Math.max(largestItem, size);
115
+ }
116
+ const tooLarge = sizes.length > MAX_COMMIT_ACTIONS ||
117
+ bytes > MAX_COMMIT_BYTES ||
118
+ largestItem > MAX_COMMIT_ITEM_BYTES;
119
+ if (tooLarge) {
120
+ throw new CommitTooLargeError(sizes.length, bytes, largestItem);
121
+ }
122
+ }
123
+ // A control check is sized by its key: `pk` and `sk = 0`.
124
+ function controlCheckSize(pk) {
125
+ return itemSize({ pk: { S: pk }, sk: { N: '0' } });
126
+ }
127
+ function invalidCommit(message) {
128
+ return new EvtError('invalidCommit', `invalid commit: ${message}`);
129
+ }
130
+ //# sourceMappingURL=repository.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"repository.js","sourceRoot":"","sources":["../src/repository.ts"],"names":[],"mappings":"AAAA,gGAAgG;AAChG,6EAA6E;AAE7E,OAAO,EAAC,cAAc,EAAE,UAAU,EAAC,MAAM,oBAAoB,CAAA;AAC7D,OAAO,EAAC,SAAS,EAAE,QAAQ,EAAE,YAAY,EAAC,MAAM,oBAAoB,CAAA;AACpE,OAAO,EAAC,WAAW,EAAE,eAAe,EAAC,MAAM,sBAAsB,CAAA;AACjE,OAAO,EAAC,mBAAmB,EAAE,QAAQ,EAAE,qBAAqB,EAAC,MAAM,aAAa,CAAA;AAChF,OAAO,EAAgD,OAAO,EAAC,MAAM,YAAY,CAAA;AACjF,OAAO,EAAC,qBAAqB,EAAC,MAAM,gBAAgB,CAAA;AAiHpD,2EAA2E;AAC3E,MAAM,CAAC,MAAM,kBAAkB,GAAG,GAAG,CAAA;AAErC,4FAA4F;AAC5F,MAAM,CAAC,MAAM,gBAAgB,GAAG,SAAS,CAAA;AAEzC,6DAA6D;AAC7D,MAAM,CAAC,MAAM,iBAAiB,GAAsB,MAAM,CAAC,MAAM,CAAC;IAChE,SAAS;IACT,kBAAkB;CACnB,CAAC,CAAA;AAEF,uDAAuD;AACvD,MAAM,UAAU,UAAU,CAAC,QAAgB;IACzC,OAAO,iBAAiB,CAAC,IAAI,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,QAAQ,CAAC,UAAU,CAAC,MAAM,CAAC,CAAC,CAAA;AACxE,CAAC;AAED,yFAAyF;AACzF,MAAM,UAAU,gBAAgB,CAAC,QAAgB;IAC/C,IAAI,UAAU,CAAC,QAAQ,CAAC,EAAE,CAAC;QACzB,MAAM,IAAI,qBAAqB,CAAC,QAAQ,CAAC,CAAA;IAC3C,CAAC;AACH,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,cAAc,CAAC,MAAc;IAC3C,MAAM,CAAC,KAAK,CAAC,GAAG,MAAM,CAAC,MAAM,CAAA;IAC7B,IAAI,KAAK,KAAK,SAAS,EAAE,CAAC;QACxB,OAAM;IACR,CAAC;IAED,KAAK,MAAM,KAAK,IAAI,MAAM,CAAC,MAAM,EAAE,CAAC;QAClC,IAAI,KAAK,CAAC,UAAU,KAAK,KAAK,CAAC,UAAU,EAAE,CAAC;YAC1C,MAAM,aAAa,CACjB,SAAS,KAAK,CAAC,EAAE,oBAAoB,KAAK,CAAC,UAAU,iCAAiC;gBACpF,OAAO,KAAK,CAAC,UAAU,EAAE,CAC5B,CAAA;QACH,CAAC;IACH,CAAC;IAED,MAAM,QAAQ,GAAG,MAAM,CAAC,QAAQ,CAAA;IAChC,IAAI,QAAQ,IAAI,QAAQ,CAAC,UAAU,KAAK,KAAK,CAAC,UAAU,EAAE,CAAC;QACzD,MAAM,aAAa,CACjB,gCAAgC,QAAQ,CAAC,UAAU,wBAAwB;YACzE,KAAK,CAAC,UAAU,CACnB,CAAA;IACH,CAAC;IACD,IAAI,QAAQ,IAAI,QAAQ,CAAC,QAAQ,GAAG,EAAE,EAAE,CAAC;QACvC,MAAM,aAAa,CACjB,qBAAqB,QAAQ,CAAC,QAAQ,4CAA4C,CACnF,CAAA;IACH,CAAC;IAED,KAAK,MAAM,KAAK,IAAI,MAAM,CAAC,MAAM,EAAE,CAAC;QAClC,IAAI,KAAK,CAAC,QAAQ,KAAK,MAAM,CAAC,QAAQ,EAAE,CAAC;YACvC,MAAM,aAAa,CACjB,SAAS,KAAK,CAAC,EAAE,sBAAsB,KAAK,CAAC,QAAQ,SAAS,MAAM,CAAC,QAAQ,EAAE,CAChF,CAAA;QACH,CAAC;QAED,MAAM,EAAE,GAAG,OAAO,CAAC,KAAK,CAAC,QAAQ,EAAE,KAAK,CAAC,QAAQ,CAAC,CAAA;QAClD,IAAI,KAAK,CAAC,EAAE,KAAK,EAAE,EAAE,CAAC;YACpB,MAAM,aAAa,CAAC,SAAS,KAAK,CAAC,EAAE,uBAAuB,EAAE,EAAE,CAAC,CAAA;QACnE,CAAC;IACH,CAAC;IAED,IAAI,KAAK,CAAC,QAAQ,GAAG,EAAE,EAAE,CAAC;QACxB,MAAM,aAAa,CAAC,kBAAkB,KAAK,CAAC,QAAQ,mCAAmC,CAAC,CAAA;IAC1F,CAAC;IAED,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,MAAM,CAAC,MAAM,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;QAC9C,MAAM,QAAQ,GAAG,MAAM,CAAC,MAAM,CAAC,CAAC,GAAG,CAAC,CAAE,CAAC,QAAQ,CAAA;QAC/C,MAAM,IAAI,GAAG,MAAM,CAAC,MAAM,CAAC,CAAC,CAAE,CAAC,QAAQ,CAAA;QACvC,IAAI,IAAI,KAAK,QAAQ,GAAG,EAAE,EAAE,CAAC;YAC3B,MAAM,aAAa,CACjB,kBAAkB,IAAI,YAAY,QAAQ,gCAAgC,CAC3E,CAAA;QACH,CAAC;IACH,CAAC;AACH,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,WAAW,CACzB,MAAc,EACd,GAA+C;IAE/C,iFAAiF;IACjF,MAAM,KAAK,GAAG,CAAC,gBAAgB,CAAC,WAAW,CAAC,EAAE,gBAAgB,CAAC,eAAe,CAAC,CAAC,CAAA;IAChF,MAAM,KAAK,GAAG,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC,CAAA;IAC9B,MAAM,IAAI,GAAG,MAAM,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,CAAA;IACjC,IAAI,KAAK,KAAK,SAAS,IAAI,IAAI,KAAK,SAAS,EAAE,CAAC;QAC9C,OAAO,KAAK,CAAA;IACd,CAAC;IAED,MAAM,IAAI,GAAG,UAAU,CAAC,MAAM,CAAC,QAAQ,EAAE,KAAK,CAAC,QAAQ,EAAE,IAAI,CAAC,QAAQ,EAAE,GAAG,CAAC,KAAK,CAAC,UAAU,CAAC,CAAC,CAAA;IAC9F,KAAK,CAAC,IAAI,CAAC,cAAc,CAAC,IAAI,CAAC,CAAC,CAAA;IAChC,KAAK,MAAM,KAAK,IAAI,MAAM,CAAC,MAAM,EAAE,CAAC;QAClC,KAAK,CAAC,IAAI,CAAC,QAAQ,CAAC,SAAS,CAAC,KAAK,EAAE,GAAG,CAAC,KAAK,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,CAAA;IAC/D,CAAC;IAED,MAAM,QAAQ,GAAG,MAAM,CAAC,QAAQ,CAAA;IAChC,IAAI,QAAQ,EAAE,CAAC;QACb,MAAM,MAAM,GAAuB;YACjC,UAAU,EAAE,QAAQ,CAAC,UAAU;YAC/B,QAAQ,EAAE,MAAM,CAAC,QAAQ;YACzB,QAAQ,EAAE,QAAQ,CAAC,QAAQ;YAC3B,aAAa,EAAE,IAAI,CAAC,QAAQ;YAC5B,OAAO,EAAE,QAAQ,CAAC,OAAO;SAC1B,CAAA;QACD,KAAK,CAAC,IAAI,CAAC,QAAQ,CAAC,YAAY,CAAC,MAAM,EAAE,GAAG,CAAC,QAAQ,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,CAAA;IACtE,CAAC;IAED,OAAO,KAAK,CAAA;AACd,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,WAAW,CAAC,KAAwB;IAClD,IAAI,KAAK,GAAG,CAAC,CAAA;IACb,IAAI,WAAW,GAAG,CAAC,CAAA;IACnB,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;QACzB,KAAK,IAAI,IAAI,CAAA;QACb,WAAW,GAAG,IAAI,CAAC,GAAG,CAAC,WAAW,EAAE,IAAI,CAAC,CAAA;IAC3C,CAAC;IAED,MAAM,QAAQ,GACZ,KAAK,CAAC,MAAM,GAAG,kBAAkB;QACjC,KAAK,GAAG,gBAAgB;QACxB,WAAW,GAAG,qBAAqB,CAAA;IACrC,IAAI,QAAQ,EAAE,CAAC;QACb,MAAM,IAAI,mBAAmB,CAAC,KAAK,CAAC,MAAM,EAAE,KAAK,EAAE,WAAW,CAAC,CAAA;IACjE,CAAC;AACH,CAAC;AAED,0DAA0D;AAC1D,SAAS,gBAAgB,CAAC,EAAU;IAClC,OAAO,QAAQ,CAAC,EAAC,EAAE,EAAE,EAAC,CAAC,EAAE,EAAE,EAAC,EAAE,EAAE,EAAE,EAAC,CAAC,EAAE,GAAG,EAAC,EAAC,CAAC,CAAA;AAC9C,CAAC;AAED,SAAS,aAAa,CAAC,OAAe;IACpC,OAAO,IAAI,QAAQ,CAAC,eAAe,EAAE,mBAAmB,OAAO,EAAE,CAAC,CAAA;AACpE,CAAC"}
@@ -0,0 +1,25 @@
1
+ /**
2
+ * How many times to run a command and how long to wait between attempts. Delays grow
3
+ * exponentially, starting at `baseDelayMs` and capped at `maxDelayMs`, with jitter in the upper
4
+ * half of each (`[delay / 2, delay]`), so writers that conflicted together don't retry together.
5
+ */
6
+ export interface RetryPolicy {
7
+ /** Attempts in total, the first included. */
8
+ readonly maxAttempts: number;
9
+ /** The delay before the first retry, before jitter. */
10
+ readonly baseDelayMs: number;
11
+ /** The upper bound of any delay. */
12
+ readonly maxDelayMs: number;
13
+ }
14
+ /** Five attempts, waiting from 25 ms up to at most 1 s, as Go and Rust do. */
15
+ export declare const DEFAULT_RETRY_POLICY: RetryPolicy;
16
+ /** Refuses a policy with fewer than one attempt or a delay that isn't a whole, finite number. */
17
+ export declare function checkRetryPolicy(policy: RetryPolicy): void;
18
+ /**
19
+ * The jittered delay in milliseconds before retry `retry` (0 for the first retry). `random`
20
+ * returns a number in [0, 1), as `Math.random` does.
21
+ */
22
+ export declare function retryDelay(policy: RetryPolicy, retry: number, random?: () => number): number;
23
+ /** Waits `ms` milliseconds, or rejects with the signal's reason when it aborts first. */
24
+ export declare function sleep(ms: number, signal?: AbortSignal): Promise<void>;
25
+ //# sourceMappingURL=retry.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"retry.d.ts","sourceRoot":"","sources":["../src/retry.ts"],"names":[],"mappings":"AAKA;;;;GAIG;AACH,MAAM,WAAW,WAAW;IAC1B,6CAA6C;IAC7C,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAA;IAC5B,uDAAuD;IACvD,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAA;IAC5B,oCAAoC;IACpC,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAA;CAC5B;AAED,8EAA8E;AAC9E,eAAO,MAAM,oBAAoB,EAAE,WAIjC,CAAA;AAEF,iGAAiG;AACjG,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,WAAW,GAAG,IAAI,CAe1D;AAED;;;GAGG;AACH,wBAAgB,UAAU,CAAC,MAAM,EAAE,WAAW,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,eAAc,GAAG,MAAM,CAK3F;AAED,yFAAyF;AACzF,wBAAgB,KAAK,CAAC,EAAE,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC,CAiBrE"}
package/dist/retry.js ADDED
@@ -0,0 +1,50 @@
1
+ // Retry policy: how many times a command runs and how long to wait between attempts, with the
2
+ // defaults the compat fixtures pin (outcomes.json, retry).
3
+ import { EvtError } from './errors.js';
4
+ /** Five attempts, waiting from 25 ms up to at most 1 s, as Go and Rust do. */
5
+ export const DEFAULT_RETRY_POLICY = Object.freeze({
6
+ maxAttempts: 5,
7
+ baseDelayMs: 25,
8
+ maxDelayMs: 1000,
9
+ });
10
+ /** Refuses a policy with fewer than one attempt or a delay that isn't a whole, finite number. */
11
+ export function checkRetryPolicy(policy) {
12
+ const valid = Number.isSafeInteger(policy.maxAttempts) &&
13
+ policy.maxAttempts >= 1 &&
14
+ Number.isSafeInteger(policy.baseDelayMs) &&
15
+ policy.baseDelayMs >= 0 &&
16
+ Number.isSafeInteger(policy.maxDelayMs) &&
17
+ policy.maxDelayMs >= 0;
18
+ if (!valid) {
19
+ throw new EvtError('invalidArgument', 'a retry policy needs at least one attempt and whole, non-negative delays: ' +
20
+ `${policy.maxAttempts}, ${policy.baseDelayMs}, ${policy.maxDelayMs}`);
21
+ }
22
+ }
23
+ /**
24
+ * The jittered delay in milliseconds before retry `retry` (0 for the first retry). `random`
25
+ * returns a number in [0, 1), as `Math.random` does.
26
+ */
27
+ export function retryDelay(policy, retry, random = Math.random) {
28
+ const ceiling = Math.min(policy.baseDelayMs * 2 ** retry, policy.maxDelayMs);
29
+ const floor = Math.floor(ceiling / 2);
30
+ return floor + Math.floor(random() * (ceiling - floor + 1));
31
+ }
32
+ /** Waits `ms` milliseconds, or rejects with the signal's reason when it aborts first. */
33
+ export function sleep(ms, signal) {
34
+ return new Promise((resolve, reject) => {
35
+ if (signal?.aborted) {
36
+ reject(signal.reason);
37
+ return;
38
+ }
39
+ const timer = setTimeout(() => {
40
+ signal?.removeEventListener('abort', aborted);
41
+ resolve();
42
+ }, ms);
43
+ const aborted = () => {
44
+ clearTimeout(timer);
45
+ reject(signal?.reason);
46
+ };
47
+ signal?.addEventListener('abort', aborted, { once: true });
48
+ });
49
+ }
50
+ //# sourceMappingURL=retry.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"retry.js","sourceRoot":"","sources":["../src/retry.ts"],"names":[],"mappings":"AAAA,8FAA8F;AAC9F,2DAA2D;AAE3D,OAAO,EAAC,QAAQ,EAAC,MAAM,aAAa,CAAA;AAgBpC,8EAA8E;AAC9E,MAAM,CAAC,MAAM,oBAAoB,GAAgB,MAAM,CAAC,MAAM,CAAC;IAC7D,WAAW,EAAE,CAAC;IACd,WAAW,EAAE,EAAE;IACf,UAAU,EAAE,IAAI;CACjB,CAAC,CAAA;AAEF,iGAAiG;AACjG,MAAM,UAAU,gBAAgB,CAAC,MAAmB;IAClD,MAAM,KAAK,GACT,MAAM,CAAC,aAAa,CAAC,MAAM,CAAC,WAAW,CAAC;QACxC,MAAM,CAAC,WAAW,IAAI,CAAC;QACvB,MAAM,CAAC,aAAa,CAAC,MAAM,CAAC,WAAW,CAAC;QACxC,MAAM,CAAC,WAAW,IAAI,CAAC;QACvB,MAAM,CAAC,aAAa,CAAC,MAAM,CAAC,UAAU,CAAC;QACvC,MAAM,CAAC,UAAU,IAAI,CAAC,CAAA;IACxB,IAAI,CAAC,KAAK,EAAE,CAAC;QACX,MAAM,IAAI,QAAQ,CAChB,iBAAiB,EACjB,4EAA4E;YAC1E,GAAG,MAAM,CAAC,WAAW,KAAK,MAAM,CAAC,WAAW,KAAK,MAAM,CAAC,UAAU,EAAE,CACvE,CAAA;IACH,CAAC;AACH,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,UAAU,CAAC,MAAmB,EAAE,KAAa,EAAE,MAAM,GAAG,IAAI,CAAC,MAAM;IACjF,MAAM,OAAO,GAAG,IAAI,CAAC,GAAG,CAAC,MAAM,CAAC,WAAW,GAAG,CAAC,IAAI,KAAK,EAAE,MAAM,CAAC,UAAU,CAAC,CAAA;IAC5E,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,OAAO,GAAG,CAAC,CAAC,CAAA;IAErC,OAAO,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,MAAM,EAAE,GAAG,CAAC,OAAO,GAAG,KAAK,GAAG,CAAC,CAAC,CAAC,CAAA;AAC7D,CAAC;AAED,yFAAyF;AACzF,MAAM,UAAU,KAAK,CAAC,EAAU,EAAE,MAAoB;IACpD,OAAO,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE;QACrC,IAAI,MAAM,EAAE,OAAO,EAAE,CAAC;YACpB,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,CAAA;YACrB,OAAM;QACR,CAAC;QAED,MAAM,KAAK,GAAG,UAAU,CAAC,GAAG,EAAE;YAC5B,MAAM,EAAE,mBAAmB,CAAC,OAAO,EAAE,OAAO,CAAC,CAAA;YAC7C,OAAO,EAAE,CAAA;QACX,CAAC,EAAE,EAAE,CAAC,CAAA;QACN,MAAM,OAAO,GAAG,GAAG,EAAE;YACnB,YAAY,CAAC,KAAK,CAAC,CAAA;YACnB,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;QACxB,CAAC,CAAA;QACD,MAAM,EAAE,gBAAgB,CAAC,OAAO,EAAE,OAAO,EAAE,EAAC,IAAI,EAAE,IAAI,EAAC,CAAC,CAAA;IAC1D,CAAC,CAAC,CAAA;AACJ,CAAC"}
@@ -0,0 +1,21 @@
1
+ import type { SerializedSnapshot } from './event.js';
2
+ /**
3
+ * The most bytes one action of a commit may carry: 95% of DynamoDB's 400 KB item limit. A commit
4
+ * with a larger item fails on every backend.
5
+ */
6
+ export declare const MAX_COMMIT_ITEM_BYTES = 380000;
7
+ /**
8
+ * Whether a commit takes a snapshot, given the stream's current sequence, the events in the
9
+ * commit and the snapshot interval: 0 for no snapshot, otherwise the events it folds in, which is
10
+ * always the whole commit. A commit snapshots when it reaches or crosses the next multiple of the
11
+ * interval, and an interval of 0 (or less, as in Go) disables snapshots.
12
+ */
13
+ export declare function snapshotEventsDue(current: bigint, events: number, interval: number): number;
14
+ /**
15
+ * The bytes the commit budget counts for a snapshot's item, with a `ttl` at its widest whether or
16
+ * not the entity type is retained. A store leaves out a snapshot over
17
+ * {@link MAX_COMMIT_ITEM_BYTES}, so an aggregate whose state outgrows the budget doesn't make
18
+ * every commit that reaches the interval fail.
19
+ */
20
+ export declare function snapshotCommitSize(snapshot: SerializedSnapshot): number;
21
+ //# sourceMappingURL=snapshots.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"snapshots.d.ts","sourceRoot":"","sources":["../src/snapshots.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAC,kBAAkB,EAAC,MAAM,YAAY,CAAA;AAGlD;;;GAGG;AACH,eAAO,MAAM,qBAAqB,SAAU,CAAA;AAK5C;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,CAU3F;AAED;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAAC,QAAQ,EAAE,kBAAkB,GAAG,MAAM,CAEvE"}
@@ -0,0 +1,35 @@
1
+ // The snapshot policy and the per-item budget a snapshot must fit, the same on every backend.
2
+ import { snapshotItem, itemSize } from './dynamodb/item.js';
3
+ import { safeIntegerArgument } from './fields.js';
4
+ /**
5
+ * The most bytes one action of a commit may carry: 95% of DynamoDB's 400 KB item limit. A commit
6
+ * with a larger item fails on every backend.
7
+ */
8
+ export const MAX_COMMIT_ITEM_BYTES = 380_000;
9
+ // A ttl attribute at its widest: the name and the 19 digits of the largest int64.
10
+ const WIDEST_TTL = { N: (2n ** 63n - 1n).toString() };
11
+ /**
12
+ * Whether a commit takes a snapshot, given the stream's current sequence, the events in the
13
+ * commit and the snapshot interval: 0 for no snapshot, otherwise the events it folds in, which is
14
+ * always the whole commit. A commit snapshots when it reaches or crosses the next multiple of the
15
+ * interval, and an interval of 0 (or less, as in Go) disables snapshots.
16
+ */
17
+ export function snapshotEventsDue(current, events, interval) {
18
+ safeIntegerArgument(events, 'events');
19
+ if (safeIntegerArgument(interval, 'interval') <= 0) {
20
+ return 0;
21
+ }
22
+ const divisor = BigInt(interval);
23
+ const nextSnapshotAt = interval - Number(((current % divisor) + divisor) % divisor);
24
+ return events < nextSnapshotAt ? 0 : events;
25
+ }
26
+ /**
27
+ * The bytes the commit budget counts for a snapshot's item, with a `ttl` at its widest whether or
28
+ * not the entity type is retained. A store leaves out a snapshot over
29
+ * {@link MAX_COMMIT_ITEM_BYTES}, so an aggregate whose state outgrows the budget doesn't make
30
+ * every commit that reaches the interval fail.
31
+ */
32
+ export function snapshotCommitSize(snapshot) {
33
+ return itemSize({ ...snapshotItem(snapshot), ttl: WIDEST_TTL });
34
+ }
35
+ //# sourceMappingURL=snapshots.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"snapshots.js","sourceRoot":"","sources":["../src/snapshots.ts"],"names":[],"mappings":"AAAA,8FAA8F;AAE9F,OAAO,EAAC,YAAY,EAAE,QAAQ,EAAC,MAAM,oBAAoB,CAAA;AAEzD,OAAO,EAAC,mBAAmB,EAAC,MAAM,aAAa,CAAA;AAE/C;;;GAGG;AACH,MAAM,CAAC,MAAM,qBAAqB,GAAG,OAAO,CAAA;AAE5C,kFAAkF;AAClF,MAAM,UAAU,GAAG,EAAC,CAAC,EAAE,CAAC,EAAE,IAAI,GAAG,GAAG,EAAE,CAAC,CAAC,QAAQ,EAAE,EAAC,CAAA;AAEnD;;;;;GAKG;AACH,MAAM,UAAU,iBAAiB,CAAC,OAAe,EAAE,MAAc,EAAE,QAAgB;IACjF,mBAAmB,CAAC,MAAM,EAAE,QAAQ,CAAC,CAAA;IACrC,IAAI,mBAAmB,CAAC,QAAQ,EAAE,UAAU,CAAC,IAAI,CAAC,EAAE,CAAC;QACnD,OAAO,CAAC,CAAA;IACV,CAAC;IAED,MAAM,OAAO,GAAG,MAAM,CAAC,QAAQ,CAAC,CAAA;IAChC,MAAM,cAAc,GAAG,QAAQ,GAAG,MAAM,CAAC,CAAC,CAAC,OAAO,GAAG,OAAO,CAAC,GAAG,OAAO,CAAC,GAAG,OAAO,CAAC,CAAA;IAEnF,OAAO,MAAM,GAAG,cAAc,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,MAAM,CAAA;AAC7C,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,kBAAkB,CAAC,QAA4B;IAC7D,OAAO,QAAQ,CAAC,EAAC,GAAG,YAAY,CAAC,QAAQ,CAAC,EAAE,GAAG,EAAE,UAAU,EAAC,CAAC,CAAA;AAC/D,CAAC"}
@@ -0,0 +1,103 @@
1
+ import { type Aggregate, type EventDefinitions, type Rejection } from './aggregate.js';
2
+ import { type SerializedEvent } from './event.js';
3
+ import { type Metadata } from './metadata.js';
4
+ import type { CallOptions, Repository } from './repository.js';
5
+ import { type RetryPolicy } from './retry.js';
6
+ /** The default number of events between snapshots, as in Go and Rust. */
7
+ export declare const DEFAULT_SNAPSHOT_INTERVAL = 5;
8
+ /** How a {@link Store} fills in what commands leave out. */
9
+ export interface StoreOptions {
10
+ /** The region metadata gets when a command's metadata has none. */
11
+ region?: string | undefined;
12
+ /** The clock, in Unix milliseconds, that stamps metadata timestamps. `Date.now` by default. */
13
+ clock?: (() => number) | undefined;
14
+ /** The number of events between snapshots (default 5). 0 disables them. */
15
+ snapshotInterval?: number | undefined;
16
+ /**
17
+ * Makes the commit ID of a commit whose metadata has neither a command ID nor a commit ID. ULID
18
+ * format by default.
19
+ */
20
+ commitId?: (() => string) | undefined;
21
+ }
22
+ /** An aggregate loaded from its stream, with the position it was loaded at. */
23
+ export interface Loaded<S> {
24
+ readonly entityId: string;
25
+ readonly state: S;
26
+ /** The sequence of the last event folded into the state, 0 for a new entity. */
27
+ readonly sequence: bigint;
28
+ /** The sequence of the snapshot the load started from, or the commit wrote, 0 with none. */
29
+ readonly snapshotSequence: bigint;
30
+ /** The command IDs of the events folded in since the snapshot: the dedupe window. */
31
+ readonly seenCommandIds: ReadonlySet<string>;
32
+ }
33
+ /** Options for {@link Store.execute}. */
34
+ export interface ExecuteOptions extends CallOptions {
35
+ /**
36
+ * The command's metadata. What it leaves out is empty, except the region and the timestamp,
37
+ * which the store fills in.
38
+ */
39
+ metadata?: Partial<Metadata> | undefined;
40
+ }
41
+ /** Options for {@link Store.executeWithRetry}. */
42
+ export interface RetryOptions extends ExecuteOptions {
43
+ /** {@link DEFAULT_RETRY_POLICY} by default. */
44
+ policy?: RetryPolicy | undefined;
45
+ /** Waits between attempts. A timer by default; inject one in tests. */
46
+ sleep?: ((ms: number, signal?: AbortSignal) => Promise<void>) | undefined;
47
+ }
48
+ /**
49
+ * What a command did:
50
+ *
51
+ * - `committed`: it was accepted, and its events (possibly none) are stored.
52
+ * - `duplicate`: an event since the latest snapshot already carries its command ID, so it didn't
53
+ * run.
54
+ * - `rejected`: the handler refused it. Nothing was written.
55
+ */
56
+ export type ExecuteResult<S> = {
57
+ kind: 'committed';
58
+ state: Loaded<S>;
59
+ events: SerializedEvent[];
60
+ } | {
61
+ kind: 'duplicate';
62
+ commandId: string;
63
+ } | {
64
+ kind: 'rejected';
65
+ rejection: Rejection;
66
+ state: Loaded<S>;
67
+ };
68
+ export declare class Store {
69
+ #private;
70
+ readonly repository: Repository;
71
+ constructor(repository: Repository, options?: StoreOptions);
72
+ /** The number of events between snapshots of `aggregate`: its own interval, or the store's. */
73
+ snapshotInterval(aggregate: {
74
+ readonly snapshotInterval?: number | undefined;
75
+ }): number;
76
+ /**
77
+ * Loads an aggregate from its snapshot and the events after it, or from every event when it has
78
+ * no snapshot. An entity with no events loads as the initial state at sequence 0.
79
+ *
80
+ * The events must continue the sequence. A gap means another writer snapshotted and compacted
81
+ * the stream between the two reads, so the load starts over, up to three times, and then fails
82
+ * with a {@link SequenceGapError}. When no events follow, the snapshot is read again to rule out
83
+ * a compaction that left no gap to see.
84
+ */
85
+ load<S, C, D extends EventDefinitions>(aggregate: Aggregate<S, C, D>, entityId: string, options?: CallOptions): Promise<Loaded<S>>;
86
+ /**
87
+ * Loads the aggregate, runs the command, and commits its events, with a snapshot when the
88
+ * commit reaches the snapshot interval. A snapshot whose item is over the per-item commit budget
89
+ * is left out, so the commit still succeeds and later loads replay from the one before.
90
+ *
91
+ * A command ID already carried by an event folded in since the latest snapshot makes the
92
+ * command a `duplicate`, and the handler doesn't run. A conflict with another writer throws;
93
+ * {@link executeWithRetry} runs the command again.
94
+ */
95
+ execute<S, C, D extends EventDefinitions>(aggregate: Aggregate<S, C, D>, entityId: string, command: C, options?: ExecuteOptions): Promise<ExecuteResult<S>>;
96
+ /**
97
+ * Like {@link execute}, but loads and runs the command again after a retryable error (see
98
+ * {@link isRetryable}), waiting {@link retryDelay} between attempts. When the attempts run out,
99
+ * the last error is thrown. A rejection or a duplicate is a result, never retried.
100
+ */
101
+ executeWithRetry<S, C, D extends EventDefinitions>(aggregate: Aggregate<S, C, D>, entityId: string, command: C, options?: RetryOptions): Promise<ExecuteResult<S>>;
102
+ }
103
+ //# sourceMappingURL=store.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"store.d.ts","sourceRoot":"","sources":["../src/store.ts"],"names":[],"mappings":"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"}