@sekiban/dcb-runtime 0.2.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/LICENSE +86 -0
- package/README.md +13 -0
- package/dist/allocator/AllocatorDurableObject.d.ts +36 -0
- package/dist/allocator/AllocatorDurableObject.js +658 -0
- package/dist/allocator/IssuanceLedger.d.ts +188 -0
- package/dist/allocator/IssuanceLedger.js +608 -0
- package/dist/allocator/IssuanceReconciler.d.ts +41 -0
- package/dist/allocator/IssuanceReconciler.js +191 -0
- package/dist/allocator/OrderClock.d.ts +41 -0
- package/dist/allocator/OrderClock.js +92 -0
- package/dist/allocator/SortableUniqueId.d.ts +46 -0
- package/dist/allocator/SortableUniqueId.js +156 -0
- package/dist/allocator/types.d.ts +52 -0
- package/dist/allocator/types.js +1 -0
- package/dist/bootstrap/BootstrapCoordinatorDurableObject.d.ts +28 -0
- package/dist/bootstrap/BootstrapCoordinatorDurableObject.js +357 -0
- package/dist/bootstrap/BootstrapStoreAdapter.d.ts +51 -0
- package/dist/bootstrap/BootstrapStoreAdapter.js +162 -0
- package/dist/bootstrap/OperatorBootstrap.d.ts +20 -0
- package/dist/bootstrap/OperatorBootstrap.js +104 -0
- package/dist/bootstrap/manifest.d.ts +9 -0
- package/dist/bootstrap/manifest.js +115 -0
- package/dist/bootstrap/types.d.ts +75 -0
- package/dist/bootstrap/types.js +1 -0
- package/dist/cli/OperatorRepairCli.d.ts +11 -0
- package/dist/cli/OperatorRepairCli.js +108 -0
- package/dist/cloudflare.d.ts +164 -0
- package/dist/cloudflare.js +19487 -0
- package/dist/commit/CommitWorker.d.ts +101 -0
- package/dist/commit/CommitWorker.js +921 -0
- package/dist/commit/types.d.ts +25 -0
- package/dist/commit/types.js +13 -0
- package/dist/completeness/GlobalCompletenessReconciler.d.ts +77 -0
- package/dist/completeness/GlobalCompletenessReconciler.js +572 -0
- package/dist/completeness/types.d.ts +85 -0
- package/dist/completeness/types.js +6 -0
- package/dist/composition.d.ts +123 -0
- package/dist/composition.js +237 -0
- package/dist/cosmos.d.ts +11 -0
- package/dist/cosmos.js +1411 -0
- package/dist/d1-mv.d.ts +8 -0
- package/dist/d1-mv.js +2001 -0
- package/dist/d1.d.ts +16 -0
- package/dist/d1.js +3418 -0
- package/dist/diagnostics/G60DurableHop.d.ts +99 -0
- package/dist/diagnostics/G60DurableHop.js +248 -0
- package/dist/diagnostics/G65Admission.d.ts +23 -0
- package/dist/diagnostics/G65Admission.js +43 -0
- package/dist/diagnostics/G65DirectRing.d.ts +33 -0
- package/dist/diagnostics/G65DirectRing.js +85 -0
- package/dist/diagnostics/G69AdmissionAttempt.d.ts +43 -0
- package/dist/diagnostics/G69AdmissionAttempt.js +22 -0
- package/dist/downstream/DeliveryCore.d.ts +127 -0
- package/dist/downstream/DeliveryCore.js +304 -0
- package/dist/downstream/Doorbell.d.ts +43 -0
- package/dist/downstream/Doorbell.js +137 -0
- package/dist/downstream/DownstreamAdapter.d.ts +52 -0
- package/dist/downstream/DownstreamAdapter.js +299 -0
- package/dist/downstream/ExclusionLookup.d.ts +36 -0
- package/dist/downstream/ExclusionLookup.js +50 -0
- package/dist/downstream/InconsistencyDetector.d.ts +27 -0
- package/dist/downstream/InconsistencyDetector.js +82 -0
- package/dist/downstream/OutboxDrain.d.ts +35 -0
- package/dist/downstream/OutboxDrain.js +126 -0
- package/dist/downstream/types.d.ts +77 -0
- package/dist/downstream/types.js +64 -0
- package/dist/eventIdentity.d.ts +30 -0
- package/dist/eventIdentity.js +49 -0
- package/dist/eventRecord.d.ts +26 -0
- package/dist/eventRecord.js +53 -0
- package/dist/g42-probe.d.ts +9 -0
- package/dist/g42-probe.js +9 -0
- package/dist/http/SerializedQueryWorker.d.ts +58 -0
- package/dist/http/SerializedQueryWorker.js +497 -0
- package/dist/http/commitResponse.d.ts +46 -0
- package/dist/http/commitResponse.js +80 -0
- package/dist/http/testServiceId.d.ts +17 -0
- package/dist/http/testServiceId.js +14 -0
- package/dist/index.d.ts +112 -0
- package/dist/index.js +19926 -0
- package/dist/journal/JournalDurableObject.d.ts +30 -0
- package/dist/journal/JournalDurableObject.js +481 -0
- package/dist/journal/JournalFirstTouchProbe.d.ts +190 -0
- package/dist/journal/JournalFirstTouchProbe.js +375 -0
- package/dist/journal/types.d.ts +116 -0
- package/dist/journal/types.js +52 -0
- package/dist/mv/MaterializedViewCatchUp.d.ts +60 -0
- package/dist/mv/MaterializedViewCatchUp.js +353 -0
- package/dist/mv/MaterializedViewRuntime.d.ts +80 -0
- package/dist/mv/MaterializedViewRuntime.js +328 -0
- package/dist/mv/MaterializedViewStore.d.ts +257 -0
- package/dist/mv/MaterializedViewStore.js +875 -0
- package/dist/mv/UnsafeWindowMaterializedView.d.ts +112 -0
- package/dist/mv/UnsafeWindowMaterializedView.js +495 -0
- package/dist/mv/index.d.ts +8 -0
- package/dist/mv/index.js +2357 -0
- package/dist/projection/LiveProjectionWorker.d.ts +86 -0
- package/dist/projection/LiveProjectionWorker.js +240 -0
- package/dist/projection/ProjectionRuntime.d.ts +113 -0
- package/dist/projection/ProjectionRuntime.js +346 -0
- package/dist/projection/ProjectorRegistry.d.ts +51 -0
- package/dist/projection/ProjectorRegistry.js +116 -0
- package/dist/query/ProjectionQueryStore.d.ts +102 -0
- package/dist/query/ProjectionQueryStore.js +185 -0
- package/dist/query/QueryRegistry.d.ts +22 -0
- package/dist/query/QueryRegistry.js +36 -0
- package/dist/read/SerializedReadWorker.d.ts +36 -0
- package/dist/read/SerializedReadWorker.js +225 -0
- package/dist/repair/RepairWorker.d.ts +72 -0
- package/dist/repair/RepairWorker.js +461 -0
- package/dist/safeWindow.d.ts +20 -0
- package/dist/safeWindow.js +36 -0
- package/dist/scope/ControlRouteScope.d.ts +22 -0
- package/dist/scope/ControlRouteScope.js +36 -0
- package/dist/scope/ScopeName.d.ts +34 -0
- package/dist/scope/ScopeName.js +67 -0
- package/dist/service/ServiceIdentityProvider.d.ts +35 -0
- package/dist/service/ServiceIdentityProvider.js +56 -0
- package/dist/store/CosmosEventStore.d.ts +126 -0
- package/dist/store/CosmosEventStore.js +950 -0
- package/dist/store/D1EventStore.d.ts +111 -0
- package/dist/store/D1EventStore.js +1111 -0
- package/dist/store/PostgresEventStore.d.ts +35 -0
- package/dist/store/PostgresEventStore.js +811 -0
- package/dist/store/provider.d.ts +25 -0
- package/dist/store/provider.js +32 -0
- package/dist/store/types.d.ts +205 -0
- package/dist/store/types.js +18 -0
- package/dist/tag/EventDigest.d.ts +40 -0
- package/dist/tag/EventDigest.js +158 -0
- package/dist/tag/TagDurableObject.d.ts +286 -0
- package/dist/tag/TagDurableObject.js +4071 -0
- package/dist/tag/TagSqlMeasurement.d.ts +26 -0
- package/dist/tag/TagSqlMeasurement.js +44 -0
- package/dist/tag/TagSqlSchema.d.ts +29 -0
- package/dist/tag/TagSqlSchema.js +211 -0
- package/dist/tag/types.d.ts +173 -0
- package/dist/tag/types.js +3 -0
- package/dist/tagstate/TagStateDurableObject.d.ts +57 -0
- package/dist/tagstate/TagStateDurableObject.js +499 -0
- package/dist/trace/AttributionRatio.d.ts +12 -0
- package/dist/trace/AttributionRatio.js +68 -0
- package/dist/trace/CloudflareTracing.d.ts +7 -0
- package/dist/trace/CloudflareTracing.js +9 -0
- package/dist/trace/CommitTrace.d.ts +405 -0
- package/dist/trace/CommitTrace.js +1008 -0
- package/dist/trace/CommitTraceConsoleSink.d.ts +68 -0
- package/dist/trace/CommitTraceConsoleSink.js +114 -0
- package/dist/trace/CommitTraceVerifier.d.ts +22 -0
- package/dist/trace/CommitTraceVerifier.js +295 -0
- package/dist/trace/ObservationStream.d.ts +118 -0
- package/dist/trace/ObservationStream.js +148 -0
- package/package.json +71 -0
package/dist/cosmos.js
ADDED
|
@@ -0,0 +1,1411 @@
|
|
|
1
|
+
// ../dcb-core/dist/index.js
|
|
2
|
+
var DcbDefinitionError = class extends Error {
|
|
3
|
+
code;
|
|
4
|
+
constructor(code, message, options) {
|
|
5
|
+
super(message, options);
|
|
6
|
+
this.name = "DcbDefinitionError";
|
|
7
|
+
this.code = code;
|
|
8
|
+
}
|
|
9
|
+
};
|
|
10
|
+
var CanonicalEventIdentityError = class extends DcbDefinitionError {
|
|
11
|
+
code = "CANONICAL_EVENT_IDENTITY_INVALID";
|
|
12
|
+
constructor(message) {
|
|
13
|
+
super("CANONICAL_EVENT_IDENTITY_INVALID", message);
|
|
14
|
+
this.name = "CanonicalEventIdentityError";
|
|
15
|
+
}
|
|
16
|
+
};
|
|
17
|
+
function canonicalEventKey(eventPayloadName) {
|
|
18
|
+
if (typeof eventPayloadName !== "string" || eventPayloadName.length === 0 || eventPayloadName.includes(":")) {
|
|
19
|
+
throw new CanonicalEventIdentityError("Event payload names must be non-empty and must not contain ':'");
|
|
20
|
+
}
|
|
21
|
+
return eventPayloadName;
|
|
22
|
+
}
|
|
23
|
+
function parseCanonicalEventKey(key) {
|
|
24
|
+
const canonical2 = canonicalEventKey(key);
|
|
25
|
+
if (canonical2 !== key) {
|
|
26
|
+
throw new CanonicalEventIdentityError("Event identity key is not canonical");
|
|
27
|
+
}
|
|
28
|
+
return Object.freeze({ eventPayloadName: key, key });
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// src/eventIdentity.ts
|
|
32
|
+
var DeliveryIdentityError = class extends Error {
|
|
33
|
+
code = "DELIVERY_IDENTITY_INVALID";
|
|
34
|
+
retryable = false;
|
|
35
|
+
constructor(message) {
|
|
36
|
+
super(message);
|
|
37
|
+
this.name = "DeliveryIdentityError";
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
var MissingCanonicalEventIdentityError = class extends DeliveryIdentityError {
|
|
41
|
+
code = "MISSING_CANONICAL_EVENT_IDENTITY";
|
|
42
|
+
constructor(source) {
|
|
43
|
+
super(`G32 ${source} delivery is missing canonical event identity`);
|
|
44
|
+
this.name = "MissingCanonicalEventIdentityError";
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
function provenanceFrom(value) {
|
|
48
|
+
if (value === void 0) return void 0;
|
|
49
|
+
if (value === "g32") return value;
|
|
50
|
+
throw new DeliveryIdentityError("Unknown event identity provenance");
|
|
51
|
+
}
|
|
52
|
+
function resolveDeliveryIdentity(message, source) {
|
|
53
|
+
const provenance = provenanceFrom(message.provenance);
|
|
54
|
+
if (message.eventType === void 0) {
|
|
55
|
+
throw new MissingCanonicalEventIdentityError(source);
|
|
56
|
+
}
|
|
57
|
+
const identity = parseCanonicalEventKey(message.eventType);
|
|
58
|
+
if (provenance !== "g32") {
|
|
59
|
+
throw new DeliveryIdentityError("G32 delivery provenance is required");
|
|
60
|
+
}
|
|
61
|
+
return {
|
|
62
|
+
...identity,
|
|
63
|
+
provenance: "g32",
|
|
64
|
+
legacy: false
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// src/allocator/SortableUniqueId.ts
|
|
69
|
+
var SORTABLE_UNIQUE_ID_TICKS_DIGITS = 19;
|
|
70
|
+
var SORTABLE_UNIQUE_ID_RANDOM_DIGITS = 11;
|
|
71
|
+
var SORTABLE_UNIQUE_ID_DIGITS = SORTABLE_UNIQUE_ID_TICKS_DIGITS + SORTABLE_UNIQUE_ID_RANDOM_DIGITS;
|
|
72
|
+
var DOTNET_UNIX_EPOCH_TICKS = 621355968000000000n;
|
|
73
|
+
var DOTNET_MAX_TICKS = 3155378975999999999n;
|
|
74
|
+
var DOTNET_TICKS_PER_MILLISECOND = 10000n;
|
|
75
|
+
var SORTABLE_UNIQUE_ID_RANDOM_LIMIT = 10n ** BigInt(SORTABLE_UNIQUE_ID_RANDOM_DIGITS);
|
|
76
|
+
var MAX_UNIX_MILLISECONDS = (DOTNET_MAX_TICKS - DOTNET_UNIX_EPOCH_TICKS) / DOTNET_TICKS_PER_MILLISECOND;
|
|
77
|
+
var SortableUniqueIdError = class extends Error {
|
|
78
|
+
constructor(code, message, options) {
|
|
79
|
+
super(message, options);
|
|
80
|
+
this.code = code;
|
|
81
|
+
this.name = "SortableUniqueIdError";
|
|
82
|
+
}
|
|
83
|
+
code;
|
|
84
|
+
};
|
|
85
|
+
var LEGACY_PREFIXED_SUID = /^suid-[0-9]+$/;
|
|
86
|
+
var LEGACY_DECISION_OBSERVER = /* @__PURE__ */ Symbol.for("@sekiban/dcb-runtime/legacy-sortable-unique-id-observer");
|
|
87
|
+
function legacyDecisionObserver() {
|
|
88
|
+
return globalThis[LEGACY_DECISION_OBSERVER];
|
|
89
|
+
}
|
|
90
|
+
function assertDotNetTicks(ticks) {
|
|
91
|
+
if (ticks < 0n || ticks > DOTNET_MAX_TICKS) {
|
|
92
|
+
throw new SortableUniqueIdError("SUID_TICKS_INVALID", "SortableUniqueId ticks are outside the .NET DateTime range");
|
|
93
|
+
}
|
|
94
|
+
return ticks;
|
|
95
|
+
}
|
|
96
|
+
function assertSortableUniqueId(value) {
|
|
97
|
+
if (typeof value === "string" && LEGACY_PREFIXED_SUID.test(value)) {
|
|
98
|
+
legacyDecisionObserver()?.(value);
|
|
99
|
+
throw new SortableUniqueIdError(
|
|
100
|
+
"SUID_LEGACY_FORMAT_RETIRED",
|
|
101
|
+
"Legacy prefixed SortableUniqueId is retired; G32 requires exactly 30 ASCII decimal digits"
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
if (typeof value !== "string" || !new RegExp(`^[0-9]{${SORTABLE_UNIQUE_ID_DIGITS}}$`).test(value)) {
|
|
105
|
+
throw new SortableUniqueIdError("SUID_INVALID", "SortableUniqueId must be exactly 30 ASCII decimal digits");
|
|
106
|
+
}
|
|
107
|
+
const ticksText = value.slice(0, SORTABLE_UNIQUE_ID_TICKS_DIGITS);
|
|
108
|
+
const randomText = value.slice(SORTABLE_UNIQUE_ID_TICKS_DIGITS);
|
|
109
|
+
const ticks = assertDotNetTicks(BigInt(ticksText));
|
|
110
|
+
const random = BigInt(randomText);
|
|
111
|
+
if (random < 0n || random >= SORTABLE_UNIQUE_ID_RANDOM_LIMIT) {
|
|
112
|
+
throw new SortableUniqueIdError("SUID_RANDOM_INVALID", "SortableUniqueId random suffix is outside its 11-digit range");
|
|
113
|
+
}
|
|
114
|
+
return Object.freeze({ value, ticks, random });
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// src/eventRecord.ts
|
|
118
|
+
var SERIALIZED_COMMIT_CORRELATION_ID = "SerializedCommit";
|
|
119
|
+
var SERIALIZED_SEKIBAN_EXECUTOR = "SerializedSekibanExecutor";
|
|
120
|
+
var CANONICAL_UTC_TIMESTAMP_PATTERN = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{1,7}Z$/;
|
|
121
|
+
function serializedEventMetadata(eventId) {
|
|
122
|
+
return Object.freeze({
|
|
123
|
+
causationId: eventId,
|
|
124
|
+
correlationId: SERIALIZED_COMMIT_CORRELATION_ID,
|
|
125
|
+
executedUser: SERIALIZED_SEKIBAN_EXECUTOR
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
function isRfc4122Uuid(value) {
|
|
129
|
+
return typeof value === "string" && /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(value);
|
|
130
|
+
}
|
|
131
|
+
function isUuidV7(value) {
|
|
132
|
+
return typeof value === "string" && /^[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/.test(value);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// src/safeWindow.ts
|
|
136
|
+
function decayedLagEstimateMs(estimateMs, observedAtMs, nowMs) {
|
|
137
|
+
const elapsed = Math.max(0, nowMs - observedAtMs);
|
|
138
|
+
return Math.max(0, estimateMs - elapsed);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// src/store/types.ts
|
|
142
|
+
var CanonicalEventIdentityConflictError = class extends Error {
|
|
143
|
+
constructor(provider, eventId, message) {
|
|
144
|
+
super(message ?? `${provider} EventId ${eventId} conflicts with its canonical event identity`);
|
|
145
|
+
this.provider = provider;
|
|
146
|
+
this.eventId = eventId;
|
|
147
|
+
this.name = "CanonicalEventIdentityConflictError";
|
|
148
|
+
}
|
|
149
|
+
provider;
|
|
150
|
+
eventId;
|
|
151
|
+
code = "CANONICAL_EVENT_IDENTITY_CONFLICT";
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
// src/store/CosmosEventStore.ts
|
|
155
|
+
var COSMOS_EVENT_PARTITION_KEY_PATH = "/pk";
|
|
156
|
+
var COSMOS_AUXILIARY_PARTITION_KEY_PATH = "/serviceId";
|
|
157
|
+
var DEFAULT_COSMOS_CONTAINERS = Object.freeze({
|
|
158
|
+
events: "dcb-events",
|
|
159
|
+
lagEstimates: "dcb-lag-estimates",
|
|
160
|
+
pendingArrivals: "dcb-pending-arrivals",
|
|
161
|
+
findings: "dcb-findings",
|
|
162
|
+
checkpoints: "dcb-projection-checkpoints"
|
|
163
|
+
});
|
|
164
|
+
function cosmosContainerDefinitions(containers = DEFAULT_COSMOS_CONTAINERS) {
|
|
165
|
+
return [
|
|
166
|
+
{
|
|
167
|
+
name: containers.events,
|
|
168
|
+
partitionKeyPath: COSMOS_EVENT_PARTITION_KEY_PATH,
|
|
169
|
+
// The C# CosmosEvent is partitioned by the exact serviceId|id pair.
|
|
170
|
+
// Refuse a missing id here so an introspection fixture cannot mistake
|
|
171
|
+
// an auxiliary service partition for the logical event partition.
|
|
172
|
+
partitionKeyValue: (serviceId, id) => {
|
|
173
|
+
if (typeof id !== "string" || id.length === 0) throw new Error("Cosmos event partition key requires an event id");
|
|
174
|
+
return eventPk(serviceId, id);
|
|
175
|
+
}
|
|
176
|
+
},
|
|
177
|
+
...[containers.lagEstimates, containers.pendingArrivals, containers.findings, containers.checkpoints].map((name) => ({
|
|
178
|
+
name,
|
|
179
|
+
partitionKeyPath: COSMOS_AUXILIARY_PARTITION_KEY_PATH,
|
|
180
|
+
partitionKeyValue: (serviceId) => serviceId
|
|
181
|
+
}))
|
|
182
|
+
];
|
|
183
|
+
}
|
|
184
|
+
var CosmosClientError = class extends Error {
|
|
185
|
+
constructor(status, message) {
|
|
186
|
+
super(message);
|
|
187
|
+
this.status = status;
|
|
188
|
+
this.name = "CosmosClientError";
|
|
189
|
+
}
|
|
190
|
+
status;
|
|
191
|
+
};
|
|
192
|
+
function asString(value, name) {
|
|
193
|
+
if (typeof value !== "string") throw new Error(`Cosmos ${name} was not a string`);
|
|
194
|
+
return value;
|
|
195
|
+
}
|
|
196
|
+
function nullableString(value, name) {
|
|
197
|
+
return value === null ? null : asString(value, name);
|
|
198
|
+
}
|
|
199
|
+
function asNumber(value, name) {
|
|
200
|
+
const number = typeof value === "number" ? value : Number(value);
|
|
201
|
+
if (!Number.isSafeInteger(number)) throw new Error(`Cosmos ${name} was not a safe integer`);
|
|
202
|
+
return number;
|
|
203
|
+
}
|
|
204
|
+
function asStringArray(value, name) {
|
|
205
|
+
if (!Array.isArray(value) || !value.every((entry) => typeof entry === "string")) {
|
|
206
|
+
throw new Error(`Cosmos ${name} was not a string array`);
|
|
207
|
+
}
|
|
208
|
+
return [...value];
|
|
209
|
+
}
|
|
210
|
+
function sortedUnique(values) {
|
|
211
|
+
return [...new Set(values)].sort(binaryCompare);
|
|
212
|
+
}
|
|
213
|
+
function binaryCompare(left, right) {
|
|
214
|
+
const leftBytes = new TextEncoder().encode(left);
|
|
215
|
+
const rightBytes = new TextEncoder().encode(right);
|
|
216
|
+
const shared = Math.min(leftBytes.length, rightBytes.length);
|
|
217
|
+
for (let index = 0; index < shared; index += 1) {
|
|
218
|
+
const difference = leftBytes[index] - rightBytes[index];
|
|
219
|
+
if (difference !== 0) return difference;
|
|
220
|
+
}
|
|
221
|
+
return leftBytes.length - rightBytes.length;
|
|
222
|
+
}
|
|
223
|
+
function assertUtcTimestamp(value, eventId) {
|
|
224
|
+
if (!CANONICAL_UTC_TIMESTAMP_PATTERN.test(value) || !Number.isFinite(Date.parse(value))) {
|
|
225
|
+
throw new CanonicalEventIdentityConflictError("cosmos", eventId, "Cosmos Timestamp must be canonical UTC ISO-8601");
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
function compareCosmosSuid(left, right) {
|
|
229
|
+
const leftBytes = new TextEncoder().encode(left);
|
|
230
|
+
const rightBytes = new TextEncoder().encode(right);
|
|
231
|
+
const shared = Math.min(leftBytes.length, rightBytes.length);
|
|
232
|
+
for (let index = 0; index < shared; index += 1) {
|
|
233
|
+
const difference = leftBytes[index] - rightBytes[index];
|
|
234
|
+
if (difference !== 0) return difference;
|
|
235
|
+
}
|
|
236
|
+
return leftBytes.length - rightBytes.length;
|
|
237
|
+
}
|
|
238
|
+
function safeId(...parts) {
|
|
239
|
+
return parts.map((part) => encodeURIComponent(part)).join("~");
|
|
240
|
+
}
|
|
241
|
+
function eventPk(serviceId, id) {
|
|
242
|
+
return `${serviceId}|${id}`;
|
|
243
|
+
}
|
|
244
|
+
function auxiliaryPk(serviceId) {
|
|
245
|
+
return eventPk(serviceId, "__dcb_event_ops__");
|
|
246
|
+
}
|
|
247
|
+
function isStatus(error, status) {
|
|
248
|
+
return error instanceof CosmosClientError && error.status === status;
|
|
249
|
+
}
|
|
250
|
+
function eventFrom(document, ops) {
|
|
251
|
+
const arrivals = Array.isArray(ops?.arrivals) ? ops.arrivals : [];
|
|
252
|
+
const id = asString(document.id, "id");
|
|
253
|
+
const tags = asStringArray(document.tags, "tags");
|
|
254
|
+
return {
|
|
255
|
+
serviceId: asString(document.serviceId, "serviceId"),
|
|
256
|
+
id,
|
|
257
|
+
eventId: id,
|
|
258
|
+
sortableUniqueId: asString(document.sortableUniqueId, "sortableUniqueId"),
|
|
259
|
+
suid: asString(document.sortableUniqueId, "sortableUniqueId"),
|
|
260
|
+
payload: asString(document.payload, "payload"),
|
|
261
|
+
tags,
|
|
262
|
+
eventTags: tags,
|
|
263
|
+
eventType: asString(document.eventType, "eventType"),
|
|
264
|
+
timestamp: asString(document.timestamp, "timestamp"),
|
|
265
|
+
causationId: nullableString(document.causationId, "causationId"),
|
|
266
|
+
correlationId: nullableString(document.correlationId, "correlationId"),
|
|
267
|
+
executedUser: nullableString(document.executedUser, "executedUser"),
|
|
268
|
+
provenance: "g32",
|
|
269
|
+
firstArrivedAt: ops === void 0 ? 0 : asNumber(ops.firstArrivedAt, "firstArrivedAt"),
|
|
270
|
+
lastArrivedAt: ops === void 0 ? 0 : asNumber(ops.lastArrivedAt, "lastArrivedAt"),
|
|
271
|
+
maxDeliveryLagMs: ops === void 0 ? 0 : asNumber(ops.maxDeliveryLagMs, "maxDeliveryLagMs"),
|
|
272
|
+
arrivals: arrivals.map((arrival) => {
|
|
273
|
+
if (typeof arrival !== "object" || arrival === null || Array.isArray(arrival)) {
|
|
274
|
+
throw new Error("Cosmos arrival was not an object");
|
|
275
|
+
}
|
|
276
|
+
const value = arrival;
|
|
277
|
+
return {
|
|
278
|
+
serviceId: asString(value.serviceId, "arrival.serviceId"),
|
|
279
|
+
eventId: asString(value.eventId, "arrival.eventId"),
|
|
280
|
+
tag: asString(value.tag, "arrival.tag"),
|
|
281
|
+
enqueuedAt: asNumber(value.enqueuedAt, "arrival.enqueuedAt"),
|
|
282
|
+
arrivedAt: asNumber(value.arrivedAt, "arrival.arrivedAt"),
|
|
283
|
+
lagMs: asNumber(value.lagMs, "arrival.lagMs")
|
|
284
|
+
};
|
|
285
|
+
})
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
function pendingFrom(document) {
|
|
289
|
+
return {
|
|
290
|
+
serviceId: asString(document.serviceId, "serviceId"),
|
|
291
|
+
attemptId: asString(document.attemptId, "attemptId"),
|
|
292
|
+
eventId: asString(document.eventId, "eventId"),
|
|
293
|
+
suid: asString(document.suid, "suid"),
|
|
294
|
+
expectedPaths: sortedUnique(asStringArray(document.expectedPaths, "expectedPaths")),
|
|
295
|
+
observedPaths: sortedUnique(asStringArray(document.observedPaths, "observedPaths")),
|
|
296
|
+
firstObservedAt: asNumber(document.firstObservedAt, "firstObservedAt"),
|
|
297
|
+
lagBoundMs: asNumber(document.lagBoundMs, "lagBoundMs")
|
|
298
|
+
};
|
|
299
|
+
}
|
|
300
|
+
function findingFrom(document) {
|
|
301
|
+
const classification = asString(document.classification, "classification");
|
|
302
|
+
if (classification !== "MISSING_STABLE" && classification !== "EXCLUDED_AUDITED" && classification !== "RESOLVED_LATE") {
|
|
303
|
+
throw new Error("Cosmos classification was invalid");
|
|
304
|
+
}
|
|
305
|
+
return {
|
|
306
|
+
serviceId: asString(document.serviceId, "serviceId"),
|
|
307
|
+
eventId: asString(document.eventId, "eventId"),
|
|
308
|
+
path: asString(document.path, "path"),
|
|
309
|
+
classification,
|
|
310
|
+
firstObservedAt: asNumber(document.firstObservedAt, "firstObservedAt"),
|
|
311
|
+
lagBoundMs: asNumber(document.lagBoundMs, "lagBoundMs"),
|
|
312
|
+
observedAt: asNumber(document.observedAt, "observedAt")
|
|
313
|
+
};
|
|
314
|
+
}
|
|
315
|
+
function incidentClassification(value) {
|
|
316
|
+
const classification = asString(value, "classification");
|
|
317
|
+
if (classification !== "SUID_COLLISION" && classification !== "ORDER_VIOLATION" && classification !== "LINEAGE_MISMATCH" && classification !== "ORDERING_DETECTOR_UNKNOWN") {
|
|
318
|
+
throw new Error("Cosmos delivery incident classification was invalid");
|
|
319
|
+
}
|
|
320
|
+
return classification;
|
|
321
|
+
}
|
|
322
|
+
function optionalString(value, name) {
|
|
323
|
+
if (value === null || value === void 0 || value === "") return void 0;
|
|
324
|
+
return asString(value, name);
|
|
325
|
+
}
|
|
326
|
+
function incidentFrom(document) {
|
|
327
|
+
return {
|
|
328
|
+
serviceId: asString(document.serviceId, "serviceId"),
|
|
329
|
+
identityKey: asString(document.identityKey, "identityKey"),
|
|
330
|
+
classification: incidentClassification(document.classification),
|
|
331
|
+
suid: optionalString(document.suid, "suid"),
|
|
332
|
+
existingEventId: optionalString(document.existingEventId, "existingEventId"),
|
|
333
|
+
incomingEventId: optionalString(document.incomingEventId, "incomingEventId"),
|
|
334
|
+
eventId: optionalString(document.eventId, "eventId"),
|
|
335
|
+
boundLineageId: optionalString(document.boundLineageId, "boundLineageId"),
|
|
336
|
+
incomingLineageId: optionalString(document.incomingLineageId, "incomingLineageId"),
|
|
337
|
+
observedAt: asNumber(document.observedAt, "observedAt")
|
|
338
|
+
};
|
|
339
|
+
}
|
|
340
|
+
function collisionIdentity(serviceId, suid, existingEventId, incomingEventId) {
|
|
341
|
+
const [first, second] = [existingEventId, incomingEventId].sort(binaryCompare);
|
|
342
|
+
return `SUID_COLLISION|${serviceId}|${suid}|${first}|${second}`;
|
|
343
|
+
}
|
|
344
|
+
function lineageIdentity(serviceId, boundLineageId, incomingLineageId) {
|
|
345
|
+
return `LINEAGE_MISMATCH|${serviceId}|${boundLineageId}|${incomingLineageId}`;
|
|
346
|
+
}
|
|
347
|
+
function metadataForDelivery(message, deliverySource) {
|
|
348
|
+
const serialized = serializedEventMetadata(message.eventId);
|
|
349
|
+
if (deliverySource !== "import") {
|
|
350
|
+
if (message.causationId !== serialized.causationId || message.correlationId !== serialized.correlationId || message.executedUser !== serialized.executedUser) {
|
|
351
|
+
throw new CanonicalEventIdentityConflictError("cosmos", message.eventId, "Cosmos metadata must use the serialized C# constants");
|
|
352
|
+
}
|
|
353
|
+
return serialized;
|
|
354
|
+
}
|
|
355
|
+
const values = [message.causationId, message.correlationId, message.executedUser];
|
|
356
|
+
if (!values.every((value) => value === null || typeof value === "string")) {
|
|
357
|
+
throw new CanonicalEventIdentityConflictError("cosmos", message.eventId, "Cosmos import metadata must be string or null");
|
|
358
|
+
}
|
|
359
|
+
const allNull = values.every((value) => value === null);
|
|
360
|
+
const allSerialized = message.causationId === serialized.causationId && message.correlationId === serialized.correlationId && message.executedUser === serialized.executedUser;
|
|
361
|
+
if (!allNull && !allSerialized) {
|
|
362
|
+
throw new CanonicalEventIdentityConflictError("cosmos", message.eventId, "Cosmos import metadata must be all null or serialized constants");
|
|
363
|
+
}
|
|
364
|
+
return {
|
|
365
|
+
causationId: message.causationId,
|
|
366
|
+
correlationId: message.correlationId,
|
|
367
|
+
executedUser: message.executedUser
|
|
368
|
+
};
|
|
369
|
+
}
|
|
370
|
+
function checkpointFrom(document) {
|
|
371
|
+
return {
|
|
372
|
+
serviceId: asString(document.serviceId, "serviceId"),
|
|
373
|
+
projectionId: asString(document.projectionId, "projectionId"),
|
|
374
|
+
lastSuid: asString(document.lastSuid, "lastSuid"),
|
|
375
|
+
stateJson: asString(document.stateJson, "stateJson"),
|
|
376
|
+
version: asNumber(document.version, "version"),
|
|
377
|
+
updatedAt: asNumber(document.updatedAt, "updatedAt")
|
|
378
|
+
};
|
|
379
|
+
}
|
|
380
|
+
function emulatorKeyBytes(key) {
|
|
381
|
+
const binary = atob(key);
|
|
382
|
+
return Uint8Array.from(binary, (character) => character.charCodeAt(0));
|
|
383
|
+
}
|
|
384
|
+
function base64(value) {
|
|
385
|
+
const bytes = new Uint8Array(value);
|
|
386
|
+
let binary = "";
|
|
387
|
+
for (const byte of bytes) binary += String.fromCharCode(byte);
|
|
388
|
+
return btoa(binary);
|
|
389
|
+
}
|
|
390
|
+
var CosmosRestClient = class {
|
|
391
|
+
initialized = false;
|
|
392
|
+
endpoint;
|
|
393
|
+
key;
|
|
394
|
+
database;
|
|
395
|
+
fetcher;
|
|
396
|
+
containers;
|
|
397
|
+
constructor(options) {
|
|
398
|
+
this.endpoint = options.endpoint.endsWith("/") ? options.endpoint : `${options.endpoint}/`;
|
|
399
|
+
this.key = options.key;
|
|
400
|
+
this.database = options.database;
|
|
401
|
+
this.fetcher = options.fetcher ?? fetch;
|
|
402
|
+
this.containers = { ...DEFAULT_COSMOS_CONTAINERS, ...options.containers };
|
|
403
|
+
}
|
|
404
|
+
async initialize() {
|
|
405
|
+
if (this.initialized) return;
|
|
406
|
+
await this.ensureResource("dbs", { id: this.database });
|
|
407
|
+
for (const container of cosmosContainerDefinitions(this.containers)) {
|
|
408
|
+
await this.ensureResource(`dbs/${encodeURIComponent(this.database)}/colls`, {
|
|
409
|
+
id: container.name,
|
|
410
|
+
partitionKey: { paths: [container.partitionKeyPath], kind: "Hash" }
|
|
411
|
+
});
|
|
412
|
+
}
|
|
413
|
+
this.initialized = true;
|
|
414
|
+
}
|
|
415
|
+
async read(container, id, partitionKey) {
|
|
416
|
+
const response = await this.request(
|
|
417
|
+
"GET",
|
|
418
|
+
`dbs/${encodeURIComponent(this.database)}/colls/${encodeURIComponent(container)}/docs/${encodeURIComponent(id)}`,
|
|
419
|
+
void 0,
|
|
420
|
+
{ "x-ms-documentdb-partitionkey": JSON.stringify([partitionKey]) }
|
|
421
|
+
);
|
|
422
|
+
if (response.status === 404) return void 0;
|
|
423
|
+
if (!response.ok) throw await this.errorFrom(response, "Cosmos document read failed");
|
|
424
|
+
const document = await response.json();
|
|
425
|
+
return { document, etag: response.headers.get("etag") ?? void 0 };
|
|
426
|
+
}
|
|
427
|
+
async create(container, document, partitionKey) {
|
|
428
|
+
const response = await this.request(
|
|
429
|
+
"POST",
|
|
430
|
+
`dbs/${encodeURIComponent(this.database)}/colls/${encodeURIComponent(container)}/docs`,
|
|
431
|
+
document,
|
|
432
|
+
{ "x-ms-documentdb-partitionkey": JSON.stringify([partitionKey]) }
|
|
433
|
+
);
|
|
434
|
+
if (!response.ok) throw await this.errorFrom(response, "Cosmos document create failed");
|
|
435
|
+
return { document: await response.json(), etag: response.headers.get("etag") ?? void 0 };
|
|
436
|
+
}
|
|
437
|
+
async replace(container, document, partitionKey, etag) {
|
|
438
|
+
const response = await this.request(
|
|
439
|
+
"PUT",
|
|
440
|
+
`dbs/${encodeURIComponent(this.database)}/colls/${encodeURIComponent(container)}/docs/${encodeURIComponent(String(document.id))}`,
|
|
441
|
+
document,
|
|
442
|
+
{
|
|
443
|
+
"x-ms-documentdb-partitionkey": JSON.stringify([partitionKey]),
|
|
444
|
+
...etag === void 0 ? {} : { "if-match": etag }
|
|
445
|
+
}
|
|
446
|
+
);
|
|
447
|
+
if (response.status === 412) return false;
|
|
448
|
+
if (!response.ok) throw await this.errorFrom(response, "Cosmos document replace failed");
|
|
449
|
+
return true;
|
|
450
|
+
}
|
|
451
|
+
async query(container, query, parameters, partitionKey) {
|
|
452
|
+
const results = [];
|
|
453
|
+
let continuation;
|
|
454
|
+
do {
|
|
455
|
+
const response = await this.request(
|
|
456
|
+
"POST",
|
|
457
|
+
`dbs/${encodeURIComponent(this.database)}/colls/${encodeURIComponent(container)}/docs`,
|
|
458
|
+
{ query, parameters },
|
|
459
|
+
{
|
|
460
|
+
"x-ms-documentdb-isquery": "true",
|
|
461
|
+
"content-type": "application/query+json",
|
|
462
|
+
"x-ms-max-item-count": "1000",
|
|
463
|
+
...partitionKey === void 0 ? {} : { "x-ms-documentdb-partitionkey": JSON.stringify([partitionKey]) },
|
|
464
|
+
...continuation === void 0 ? {} : { "x-ms-continuation": continuation }
|
|
465
|
+
}
|
|
466
|
+
);
|
|
467
|
+
if (!response.ok) throw await this.errorFrom(response, "Cosmos query failed");
|
|
468
|
+
const body = await response.json();
|
|
469
|
+
for (const document of body.Documents ?? []) {
|
|
470
|
+
results.push({ document, etag: response.headers.get("etag") ?? void 0 });
|
|
471
|
+
}
|
|
472
|
+
continuation = response.headers.get("x-ms-continuation") ?? void 0;
|
|
473
|
+
} while (continuation !== void 0 && continuation.length > 0);
|
|
474
|
+
return results;
|
|
475
|
+
}
|
|
476
|
+
async ensureResource(path, body) {
|
|
477
|
+
const response = await this.request("POST", path, body);
|
|
478
|
+
if (response.status !== 201 && response.status !== 409) {
|
|
479
|
+
throw await this.errorFrom(response, "Cosmos resource initialization failed");
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
async request(method, path, body, extraHeaders = {}) {
|
|
483
|
+
const date = (/* @__PURE__ */ new Date()).toUTCString().toLowerCase();
|
|
484
|
+
const resourceType = path.includes("/docs/") || path.endsWith("/docs") ? "docs" : path.includes("/colls/") ? "colls" : "dbs";
|
|
485
|
+
const normalizedPath = path.replace(/^\//, "");
|
|
486
|
+
const resourceLink = resourceType === "dbs" && method === "POST" && normalizedPath === "dbs" ? "" : resourceType === "colls" && method === "POST" && normalizedPath.endsWith("/colls") ? normalizedPath.slice(0, -"/colls".length) : resourceType === "docs" && normalizedPath.endsWith("/docs") ? normalizedPath.slice(0, -"/docs".length) : normalizedPath;
|
|
487
|
+
const signaturePayload = `${method.toLowerCase()}
|
|
488
|
+
${resourceType}
|
|
489
|
+
${resourceLink}
|
|
490
|
+
${date}
|
|
491
|
+
|
|
492
|
+
`;
|
|
493
|
+
const cryptoKey = await crypto.subtle.importKey(
|
|
494
|
+
"raw",
|
|
495
|
+
(() => {
|
|
496
|
+
const bytes = emulatorKeyBytes(this.key);
|
|
497
|
+
const buffer = new ArrayBuffer(bytes.byteLength);
|
|
498
|
+
new Uint8Array(buffer).set(bytes);
|
|
499
|
+
return buffer;
|
|
500
|
+
})(),
|
|
501
|
+
{ name: "HMAC", hash: "SHA-256" },
|
|
502
|
+
false,
|
|
503
|
+
["sign"]
|
|
504
|
+
);
|
|
505
|
+
const signature = encodeURIComponent(`type=master&ver=1.0&sig=${base64(await crypto.subtle.sign("HMAC", cryptoKey, new TextEncoder().encode(signaturePayload)))}`);
|
|
506
|
+
const response = await this.fetcher(new URL(path, this.endpoint), {
|
|
507
|
+
method,
|
|
508
|
+
headers: {
|
|
509
|
+
accept: "application/json",
|
|
510
|
+
"content-type": "application/json",
|
|
511
|
+
"x-ms-date": date,
|
|
512
|
+
"x-ms-version": "2018-12-31",
|
|
513
|
+
authorization: signature,
|
|
514
|
+
...extraHeaders
|
|
515
|
+
},
|
|
516
|
+
body: body === void 0 ? void 0 : JSON.stringify(body)
|
|
517
|
+
});
|
|
518
|
+
return response;
|
|
519
|
+
}
|
|
520
|
+
async errorFrom(response, prefix) {
|
|
521
|
+
return new CosmosClientError(response.status, `${prefix} (HTTP ${response.status})`);
|
|
522
|
+
}
|
|
523
|
+
};
|
|
524
|
+
var CosmosEventStore = class {
|
|
525
|
+
initialized = false;
|
|
526
|
+
client;
|
|
527
|
+
containers;
|
|
528
|
+
beforeWrite;
|
|
529
|
+
constructor(options) {
|
|
530
|
+
if (options.client !== void 0) {
|
|
531
|
+
this.client = options.client;
|
|
532
|
+
} else {
|
|
533
|
+
if (options.endpoint === void 0 || options.key === void 0 || options.database === void 0) {
|
|
534
|
+
throw new Error("Cosmos endpoint, key, and database are required for the Cosmos store");
|
|
535
|
+
}
|
|
536
|
+
this.client = new CosmosRestClient({
|
|
537
|
+
endpoint: options.endpoint,
|
|
538
|
+
key: options.key,
|
|
539
|
+
database: options.database,
|
|
540
|
+
fetcher: options.fetcher,
|
|
541
|
+
containers: options.containers
|
|
542
|
+
});
|
|
543
|
+
}
|
|
544
|
+
this.containers = { ...DEFAULT_COSMOS_CONTAINERS, ...options.containers };
|
|
545
|
+
this.beforeWrite = options.beforeWrite;
|
|
546
|
+
}
|
|
547
|
+
async initialize() {
|
|
548
|
+
if (this.initialized) return;
|
|
549
|
+
await this.client.initialize();
|
|
550
|
+
this.initialized = true;
|
|
551
|
+
}
|
|
552
|
+
async recordDelivery(message, arrivedAt, deliverySource = "queue") {
|
|
553
|
+
this.ready();
|
|
554
|
+
const identity = resolveDeliveryIdentity(message, deliverySource);
|
|
555
|
+
assertSortableUniqueId(message.suid);
|
|
556
|
+
const acceptsImportedId = deliverySource === "import";
|
|
557
|
+
if (!(acceptsImportedId ? isRfc4122Uuid(message.eventId) : isUuidV7(message.eventId))) {
|
|
558
|
+
throw new CanonicalEventIdentityConflictError(
|
|
559
|
+
"cosmos",
|
|
560
|
+
message.eventId,
|
|
561
|
+
`Cosmos EventId must be an ${acceptsImportedId ? "RFC 4122 UUID" : "UUID v7"}`
|
|
562
|
+
);
|
|
563
|
+
}
|
|
564
|
+
try {
|
|
565
|
+
JSON.parse(message.payload);
|
|
566
|
+
} catch {
|
|
567
|
+
throw new CanonicalEventIdentityConflictError("cosmos", message.eventId, "Cosmos Payload must be UTF-8 JSON text");
|
|
568
|
+
}
|
|
569
|
+
assertUtcTimestamp(message.timestamp ?? new Date(arrivedAt).toISOString(), message.eventId);
|
|
570
|
+
const metadata = metadataForDelivery(message, deliverySource);
|
|
571
|
+
const lagMs = Math.max(0, arrivedAt - message.enqueuedAt);
|
|
572
|
+
const eventTags = [...message.eventTags];
|
|
573
|
+
const binding = await this.readLineageBinding(message.serviceId);
|
|
574
|
+
if (binding !== void 0 && binding.document.allocatorLineageId !== message.allocatorLineageId) {
|
|
575
|
+
const incident = {
|
|
576
|
+
serviceId: message.serviceId,
|
|
577
|
+
identityKey: lineageIdentity(message.serviceId, binding.document.allocatorLineageId, message.allocatorLineageId),
|
|
578
|
+
classification: "LINEAGE_MISMATCH",
|
|
579
|
+
boundLineageId: binding.document.allocatorLineageId,
|
|
580
|
+
incomingLineageId: message.allocatorLineageId,
|
|
581
|
+
observedAt: arrivedAt
|
|
582
|
+
};
|
|
583
|
+
await this.persistIncident(incident);
|
|
584
|
+
return { outcome: "lineage-mismatch", kind: "lineage-mismatch", incident };
|
|
585
|
+
}
|
|
586
|
+
if (binding === void 0) {
|
|
587
|
+
await this.bindLineage(message.serviceId, message.allocatorLineageId, arrivedAt);
|
|
588
|
+
const racedBinding = await this.readLineageBinding(message.serviceId);
|
|
589
|
+
if (racedBinding === void 0) {
|
|
590
|
+
throw new Error("Cosmos allocator lineage binding was not durable");
|
|
591
|
+
}
|
|
592
|
+
if (racedBinding.document.allocatorLineageId !== message.allocatorLineageId) {
|
|
593
|
+
const incident = {
|
|
594
|
+
serviceId: message.serviceId,
|
|
595
|
+
identityKey: lineageIdentity(message.serviceId, racedBinding.document.allocatorLineageId, message.allocatorLineageId),
|
|
596
|
+
classification: "LINEAGE_MISMATCH",
|
|
597
|
+
boundLineageId: racedBinding.document.allocatorLineageId,
|
|
598
|
+
incomingLineageId: message.allocatorLineageId,
|
|
599
|
+
observedAt: arrivedAt
|
|
600
|
+
};
|
|
601
|
+
await this.persistIncident(incident);
|
|
602
|
+
return { outcome: "lineage-mismatch", kind: "lineage-mismatch", incident };
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
const currentEvents = await this.eventDocuments(message.serviceId);
|
|
606
|
+
const existingBySuid = currentEvents.find((entry) => entry.document.sortableUniqueId === message.suid);
|
|
607
|
+
if (existingBySuid !== void 0 && existingBySuid.document.id !== message.eventId) {
|
|
608
|
+
const incident = {
|
|
609
|
+
serviceId: message.serviceId,
|
|
610
|
+
identityKey: collisionIdentity(
|
|
611
|
+
message.serviceId,
|
|
612
|
+
message.suid,
|
|
613
|
+
existingBySuid.document.id,
|
|
614
|
+
message.eventId
|
|
615
|
+
),
|
|
616
|
+
classification: "SUID_COLLISION",
|
|
617
|
+
suid: message.suid,
|
|
618
|
+
existingEventId: existingBySuid.document.id,
|
|
619
|
+
incomingEventId: message.eventId,
|
|
620
|
+
observedAt: arrivedAt
|
|
621
|
+
};
|
|
622
|
+
await this.persistIncident(incident);
|
|
623
|
+
return { outcome: "suid-collision", kind: "suid-collision", incident };
|
|
624
|
+
}
|
|
625
|
+
const reservedEventId = await this.reserveSuid(message.serviceId, message.suid, message.eventId);
|
|
626
|
+
if (reservedEventId !== void 0 && reservedEventId !== message.eventId) {
|
|
627
|
+
const incident = {
|
|
628
|
+
serviceId: message.serviceId,
|
|
629
|
+
identityKey: collisionIdentity(message.serviceId, message.suid, reservedEventId, message.eventId),
|
|
630
|
+
classification: "SUID_COLLISION",
|
|
631
|
+
suid: message.suid,
|
|
632
|
+
existingEventId: reservedEventId,
|
|
633
|
+
incomingEventId: message.eventId,
|
|
634
|
+
observedAt: arrivedAt
|
|
635
|
+
};
|
|
636
|
+
await this.persistIncident(incident);
|
|
637
|
+
return { outcome: "suid-collision", kind: "suid-collision", incident };
|
|
638
|
+
}
|
|
639
|
+
const currentHead = currentEvents.reduce((head, entry) => head === void 0 || compareCosmosSuid(entry.document.sortableUniqueId, head) > 0 ? entry.document.sortableUniqueId : head, void 0);
|
|
640
|
+
const recoveryBacklogSample = currentHead !== void 0 && compareCosmosSuid(message.suid, currentHead) < 0;
|
|
641
|
+
const event = await this.mutateEvent(message, eventTags, identity.key, message.timestamp ?? new Date(arrivedAt).toISOString(), metadata);
|
|
642
|
+
const ops = await this.mutateEventOps(message, arrivedAt, lagMs);
|
|
643
|
+
if (!recoveryBacklogSample && deliverySource !== "fast") await this.updateLag(message.serviceId, lagMs, arrivedAt);
|
|
644
|
+
return { outcome: "stored", kind: "stored", event: eventFrom(event, ops) };
|
|
645
|
+
}
|
|
646
|
+
async readAllEvents(serviceId, since) {
|
|
647
|
+
this.ready();
|
|
648
|
+
if (since.length !== 0) assertSortableUniqueId(since);
|
|
649
|
+
const documents = await this.eventDocuments(serviceId);
|
|
650
|
+
const events = await Promise.all(documents.map(async (entry) => eventFrom(entry.document, await this.eventOps(serviceId, entry.document.id))));
|
|
651
|
+
return events.filter((event) => compareCosmosSuid(event.suid, since) > 0).sort((left, right) => compareCosmosSuid(left.suid, right.suid) || binaryCompare(left.eventId, right.eventId));
|
|
652
|
+
}
|
|
653
|
+
async currentLagBound(serviceId, nowMs) {
|
|
654
|
+
this.ready();
|
|
655
|
+
const row = await this.client.read(this.containers.lagEstimates, serviceId, serviceId);
|
|
656
|
+
if (row === void 0) return 0;
|
|
657
|
+
const estimate = asNumber(row.document.estimateMs, "estimateMs");
|
|
658
|
+
const observedAt = asNumber(row.document.observedAt, "observedAt");
|
|
659
|
+
const decayNow = nowMs !== void 0 && nowMs >= 1e11 && observedAt >= 1e11 ? nowMs : observedAt;
|
|
660
|
+
return decayedLagEstimateMs(estimate, observedAt, decayNow);
|
|
661
|
+
}
|
|
662
|
+
async listProjectionTags(serviceId) {
|
|
663
|
+
this.ready();
|
|
664
|
+
const tags = /* @__PURE__ */ new Set();
|
|
665
|
+
for (const entry of await this.eventDocuments(serviceId)) {
|
|
666
|
+
for (const tag of asStringArray(entry.document.tags, "tags")) tags.add(tag);
|
|
667
|
+
}
|
|
668
|
+
return [...tags].sort(binaryCompare);
|
|
669
|
+
}
|
|
670
|
+
async readProjectionCheckpoint(serviceId, projectionId) {
|
|
671
|
+
this.ready();
|
|
672
|
+
const row = await this.client.read(this.containers.checkpoints, safeId(projectionId), serviceId);
|
|
673
|
+
return row === void 0 ? void 0 : checkpointFrom(row.document);
|
|
674
|
+
}
|
|
675
|
+
async advanceProjectionCheckpoint(input) {
|
|
676
|
+
this.ready();
|
|
677
|
+
const id = safeId(input.projectionId);
|
|
678
|
+
const document = {
|
|
679
|
+
id,
|
|
680
|
+
serviceId: input.serviceId,
|
|
681
|
+
projectionId: input.projectionId,
|
|
682
|
+
lastSuid: input.lastSuid,
|
|
683
|
+
stateJson: input.stateJson,
|
|
684
|
+
version: input.version,
|
|
685
|
+
updatedAt: input.updatedAt
|
|
686
|
+
};
|
|
687
|
+
const existing = await this.client.read(this.containers.checkpoints, id, input.serviceId);
|
|
688
|
+
if (existing === void 0) {
|
|
689
|
+
if (input.expectedLastSuid !== null) return false;
|
|
690
|
+
await this.beforeWrite?.("checkpoint");
|
|
691
|
+
try {
|
|
692
|
+
await this.client.create(this.containers.checkpoints, document, input.serviceId);
|
|
693
|
+
return true;
|
|
694
|
+
} catch (error) {
|
|
695
|
+
if (isStatus(error, 409)) return false;
|
|
696
|
+
throw error;
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
if (existing.document.lastSuid !== input.expectedLastSuid) return false;
|
|
700
|
+
await this.beforeWrite?.("checkpoint");
|
|
701
|
+
return this.client.replace(this.containers.checkpoints, document, input.serviceId, existing.etag);
|
|
702
|
+
}
|
|
703
|
+
async projectionLag(serviceId, projectionId, tag) {
|
|
704
|
+
this.ready();
|
|
705
|
+
const checkpoint = await this.readProjectionCheckpoint(serviceId, projectionId);
|
|
706
|
+
const checkpointSuid = checkpoint?.lastSuid ?? "";
|
|
707
|
+
const candidates = await this.eventDocuments(serviceId);
|
|
708
|
+
const matching = (await Promise.all(
|
|
709
|
+
candidates.map(async (entry) => eventFrom(entry.document, await this.eventOps(serviceId, entry.document.id)))
|
|
710
|
+
)).filter((event) => event.eventTags.includes(tag));
|
|
711
|
+
const headSuid = matching.reduce((head, event) => head === "" || compareCosmosSuid(event.suid, head) > 0 ? event.suid : head, "");
|
|
712
|
+
return {
|
|
713
|
+
serviceId,
|
|
714
|
+
projectionId,
|
|
715
|
+
tag,
|
|
716
|
+
checkpointSuid,
|
|
717
|
+
headSuid,
|
|
718
|
+
behindEvents: matching.filter((event) => compareCosmosSuid(event.suid, checkpointSuid) > 0).length
|
|
719
|
+
};
|
|
720
|
+
}
|
|
721
|
+
async upsertPending(message, firstObservedAt, lagBoundMs) {
|
|
722
|
+
this.ready();
|
|
723
|
+
const id = safeId(message.eventId);
|
|
724
|
+
for (let attempt = 0; attempt < 8; attempt += 1) {
|
|
725
|
+
const existing = await this.client.read(this.containers.pendingArrivals, id, message.serviceId);
|
|
726
|
+
if (existing === void 0) {
|
|
727
|
+
const document2 = {
|
|
728
|
+
id,
|
|
729
|
+
serviceId: message.serviceId,
|
|
730
|
+
eventId: message.eventId,
|
|
731
|
+
attemptId: message.attemptId,
|
|
732
|
+
suid: message.suid,
|
|
733
|
+
expectedPaths: sortedUnique(message.eventTags),
|
|
734
|
+
observedPaths: [message.tag],
|
|
735
|
+
firstObservedAt,
|
|
736
|
+
lagBoundMs
|
|
737
|
+
};
|
|
738
|
+
await this.beforeWrite?.("pending");
|
|
739
|
+
try {
|
|
740
|
+
await this.client.create(this.containers.pendingArrivals, document2, message.serviceId);
|
|
741
|
+
return pendingFrom(document2);
|
|
742
|
+
} catch (error) {
|
|
743
|
+
if (isStatus(error, 409)) continue;
|
|
744
|
+
throw error;
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
const pending = pendingFrom(existing.document);
|
|
748
|
+
if (pending.attemptId !== message.attemptId || pending.suid !== message.suid) {
|
|
749
|
+
throw new Error(`EventId ${message.eventId} has contradictory pending-arrival identity`);
|
|
750
|
+
}
|
|
751
|
+
const document = {
|
|
752
|
+
...existing.document,
|
|
753
|
+
expectedPaths: sortedUnique([...pending.expectedPaths, ...message.eventTags]),
|
|
754
|
+
observedPaths: sortedUnique([...pending.observedPaths, message.tag]),
|
|
755
|
+
lagBoundMs: Math.max(pending.lagBoundMs, lagBoundMs)
|
|
756
|
+
};
|
|
757
|
+
await this.beforeWrite?.("pending");
|
|
758
|
+
if (await this.client.replace(this.containers.pendingArrivals, document, message.serviceId, existing.etag)) {
|
|
759
|
+
return pendingFrom(document);
|
|
760
|
+
}
|
|
761
|
+
}
|
|
762
|
+
throw new Error("Cosmos pending-arrival CAS did not converge");
|
|
763
|
+
}
|
|
764
|
+
async listPending(serviceId) {
|
|
765
|
+
this.ready();
|
|
766
|
+
const rows = await this.client.query(
|
|
767
|
+
this.containers.pendingArrivals,
|
|
768
|
+
serviceId === void 0 ? "SELECT * FROM c" : "SELECT * FROM c WHERE c.serviceId = @serviceId",
|
|
769
|
+
serviceId === void 0 ? [] : [{ name: "@serviceId", value: serviceId }],
|
|
770
|
+
serviceId
|
|
771
|
+
);
|
|
772
|
+
return rows.map((row) => pendingFrom(row.document)).sort((left, right) => binaryCompare(left.serviceId, right.serviceId) || binaryCompare(left.eventId, right.eventId));
|
|
773
|
+
}
|
|
774
|
+
async appendFinding(finding) {
|
|
775
|
+
this.ready();
|
|
776
|
+
const document = {
|
|
777
|
+
id: safeId(finding.eventId, finding.path, finding.classification),
|
|
778
|
+
...finding
|
|
779
|
+
};
|
|
780
|
+
await this.beforeWrite?.("finding");
|
|
781
|
+
try {
|
|
782
|
+
await this.client.create(this.containers.findings, document, finding.serviceId);
|
|
783
|
+
} catch (error) {
|
|
784
|
+
if (!isStatus(error, 409)) throw error;
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
|
+
async appendDeliveryIncident(incident) {
|
|
788
|
+
this.ready();
|
|
789
|
+
await this.persistIncident(incident);
|
|
790
|
+
await this.projectIncident(incident);
|
|
791
|
+
}
|
|
792
|
+
async hasDeliveryIncident(serviceId, identityKey) {
|
|
793
|
+
this.ready();
|
|
794
|
+
return await this.client.read(
|
|
795
|
+
this.containers.events,
|
|
796
|
+
safeId("incident", identityKey),
|
|
797
|
+
auxiliaryPk(serviceId)
|
|
798
|
+
) !== void 0;
|
|
799
|
+
}
|
|
800
|
+
async listDeliveryIncidents(serviceId) {
|
|
801
|
+
this.ready();
|
|
802
|
+
const rows = await this.client.query(
|
|
803
|
+
this.containers.events,
|
|
804
|
+
serviceId === void 0 ? "SELECT * FROM c" : "SELECT * FROM c WHERE c.serviceId = @serviceId",
|
|
805
|
+
serviceId === void 0 ? [] : [{ name: "@serviceId", value: serviceId }]
|
|
806
|
+
);
|
|
807
|
+
return rows.filter((row) => row.document.kind === "delivery-incident").map((row) => incidentFrom(row.document)).sort((left, right) => left.observedAt - right.observedAt || binaryCompare(left.identityKey, right.identityKey));
|
|
808
|
+
}
|
|
809
|
+
/** Retryable async projection from the same-partition incident landing. */
|
|
810
|
+
async projectDeliveryIncidents(serviceId) {
|
|
811
|
+
const incidents = await this.listDeliveryIncidents(serviceId);
|
|
812
|
+
let projected = 0;
|
|
813
|
+
for (const incident of incidents) {
|
|
814
|
+
await this.projectIncident(incident);
|
|
815
|
+
projected += 1;
|
|
816
|
+
}
|
|
817
|
+
return projected;
|
|
818
|
+
}
|
|
819
|
+
async readLineageBinding(serviceId) {
|
|
820
|
+
return this.client.read(
|
|
821
|
+
this.containers.events,
|
|
822
|
+
safeId("allocator-lineage-binding"),
|
|
823
|
+
auxiliaryPk(serviceId)
|
|
824
|
+
);
|
|
825
|
+
}
|
|
826
|
+
async bindLineage(serviceId, allocatorLineageId, boundAt) {
|
|
827
|
+
const document = {
|
|
828
|
+
id: safeId("allocator-lineage-binding"),
|
|
829
|
+
pk: auxiliaryPk(serviceId),
|
|
830
|
+
serviceId,
|
|
831
|
+
kind: "allocator-lineage-binding",
|
|
832
|
+
allocatorLineageId,
|
|
833
|
+
boundAt
|
|
834
|
+
};
|
|
835
|
+
try {
|
|
836
|
+
await this.client.create(this.containers.events, document, auxiliaryPk(serviceId));
|
|
837
|
+
} catch (error) {
|
|
838
|
+
if (!isStatus(error, 409)) throw error;
|
|
839
|
+
}
|
|
840
|
+
}
|
|
841
|
+
/**
|
|
842
|
+
* Cosmos has no cross-document unique index. A same-partition SUID binding
|
|
843
|
+
* is the equivalent guard: the first EventId reserves the opaque SUID and
|
|
844
|
+
* every later delivery reuses or rejects that reservation before event
|
|
845
|
+
* mutation. Historical rows are still checked by eventDocuments above.
|
|
846
|
+
*/
|
|
847
|
+
async reserveSuid(serviceId, suid, eventId) {
|
|
848
|
+
const id = safeId("suid-binding", suid);
|
|
849
|
+
const existing = await this.client.read(this.containers.events, id, auxiliaryPk(serviceId));
|
|
850
|
+
if (existing !== void 0) return existing.document.eventId;
|
|
851
|
+
const document = {
|
|
852
|
+
id,
|
|
853
|
+
pk: auxiliaryPk(serviceId),
|
|
854
|
+
serviceId,
|
|
855
|
+
kind: "suid-binding",
|
|
856
|
+
suid,
|
|
857
|
+
eventId
|
|
858
|
+
};
|
|
859
|
+
await this.beforeWrite?.("event");
|
|
860
|
+
try {
|
|
861
|
+
await this.client.create(this.containers.events, document, auxiliaryPk(serviceId));
|
|
862
|
+
return eventId;
|
|
863
|
+
} catch (error) {
|
|
864
|
+
if (!isStatus(error, 409)) throw error;
|
|
865
|
+
const raced = await this.client.read(this.containers.events, id, auxiliaryPk(serviceId));
|
|
866
|
+
return raced?.document.eventId;
|
|
867
|
+
}
|
|
868
|
+
}
|
|
869
|
+
async persistIncident(incident) {
|
|
870
|
+
const document = {
|
|
871
|
+
id: safeId("incident", incident.identityKey),
|
|
872
|
+
pk: auxiliaryPk(incident.serviceId),
|
|
873
|
+
serviceId: incident.serviceId,
|
|
874
|
+
kind: "delivery-incident",
|
|
875
|
+
identityKey: incident.identityKey,
|
|
876
|
+
classification: incident.classification,
|
|
877
|
+
...incident.suid === void 0 ? {} : { suid: incident.suid },
|
|
878
|
+
...incident.existingEventId === void 0 ? {} : { existingEventId: incident.existingEventId },
|
|
879
|
+
...incident.incomingEventId === void 0 ? {} : { incomingEventId: incident.incomingEventId },
|
|
880
|
+
...incident.eventId === void 0 ? {} : { eventId: incident.eventId },
|
|
881
|
+
...incident.boundLineageId === void 0 ? {} : { boundLineageId: incident.boundLineageId },
|
|
882
|
+
...incident.incomingLineageId === void 0 ? {} : { incomingLineageId: incident.incomingLineageId },
|
|
883
|
+
observedAt: incident.observedAt
|
|
884
|
+
};
|
|
885
|
+
await this.beforeWrite?.("incident");
|
|
886
|
+
try {
|
|
887
|
+
await this.client.create(this.containers.events, document, auxiliaryPk(incident.serviceId));
|
|
888
|
+
} catch (error) {
|
|
889
|
+
if (!isStatus(error, 409)) throw error;
|
|
890
|
+
}
|
|
891
|
+
}
|
|
892
|
+
async projectIncident(incident) {
|
|
893
|
+
const document = {
|
|
894
|
+
id: safeId("incident", incident.identityKey),
|
|
895
|
+
serviceId: incident.serviceId,
|
|
896
|
+
eventId: incident.eventId ?? incident.incomingEventId ?? incident.identityKey,
|
|
897
|
+
path: incident.classification,
|
|
898
|
+
classification: incident.classification,
|
|
899
|
+
firstObservedAt: incident.observedAt,
|
|
900
|
+
lagBoundMs: 0,
|
|
901
|
+
observedAt: incident.observedAt,
|
|
902
|
+
kind: "delivery-incident",
|
|
903
|
+
identityKey: incident.identityKey
|
|
904
|
+
};
|
|
905
|
+
await this.beforeWrite?.("incident-projection");
|
|
906
|
+
try {
|
|
907
|
+
await this.client.create(this.containers.findings, document, incident.serviceId);
|
|
908
|
+
} catch (error) {
|
|
909
|
+
if (!isStatus(error, 409)) throw error;
|
|
910
|
+
}
|
|
911
|
+
}
|
|
912
|
+
async hasFinding(serviceId, eventId, path, classification) {
|
|
913
|
+
this.ready();
|
|
914
|
+
return await this.client.read(
|
|
915
|
+
this.containers.findings,
|
|
916
|
+
safeId(eventId, path, classification),
|
|
917
|
+
serviceId
|
|
918
|
+
) !== void 0;
|
|
919
|
+
}
|
|
920
|
+
async listFindings(serviceId, eventId) {
|
|
921
|
+
this.ready();
|
|
922
|
+
const predicates = [];
|
|
923
|
+
const parameters = [];
|
|
924
|
+
if (serviceId !== void 0) {
|
|
925
|
+
predicates.push("c.serviceId = @serviceId");
|
|
926
|
+
parameters.push({ name: "@serviceId", value: serviceId });
|
|
927
|
+
}
|
|
928
|
+
if (eventId !== void 0) {
|
|
929
|
+
predicates.push("c.eventId = @eventId");
|
|
930
|
+
parameters.push({ name: "@eventId", value: eventId });
|
|
931
|
+
}
|
|
932
|
+
const rows = await this.client.query(
|
|
933
|
+
this.containers.findings,
|
|
934
|
+
`SELECT * FROM c${predicates.length === 0 ? "" : ` WHERE ${predicates.join(" AND ")}`}`,
|
|
935
|
+
parameters,
|
|
936
|
+
serviceId
|
|
937
|
+
);
|
|
938
|
+
return rows.filter((row) => row.document.kind === void 0).map((row) => findingFrom(row.document)).sort((left, right) => left.observedAt - right.observedAt || binaryCompare(left.eventId, right.eventId) || binaryCompare(left.path, right.path));
|
|
939
|
+
}
|
|
940
|
+
async eventDocuments(serviceId) {
|
|
941
|
+
const rows = await this.client.query(
|
|
942
|
+
this.containers.events,
|
|
943
|
+
"SELECT * FROM c WHERE c.serviceId = @serviceId AND IS_DEFINED(c.sortableUniqueId)",
|
|
944
|
+
[{ name: "@serviceId", value: serviceId }]
|
|
945
|
+
);
|
|
946
|
+
return rows;
|
|
947
|
+
}
|
|
948
|
+
async eventOps(serviceId, eventId) {
|
|
949
|
+
const row = await this.client.read(
|
|
950
|
+
this.containers.events,
|
|
951
|
+
safeId("event-ops", eventId),
|
|
952
|
+
eventPk(serviceId, eventId)
|
|
953
|
+
);
|
|
954
|
+
return row?.document;
|
|
955
|
+
}
|
|
956
|
+
async mutateEvent(message, eventTags, eventType, timestamp, metadata) {
|
|
957
|
+
const id = message.eventId;
|
|
958
|
+
const pk = eventPk(message.serviceId, id);
|
|
959
|
+
for (let attempt = 0; attempt < 8; attempt += 1) {
|
|
960
|
+
const existing = await this.client.read(this.containers.events, id, pk);
|
|
961
|
+
if (existing === void 0) {
|
|
962
|
+
const document = {
|
|
963
|
+
id,
|
|
964
|
+
pk,
|
|
965
|
+
serviceId: message.serviceId,
|
|
966
|
+
sortableUniqueId: message.suid,
|
|
967
|
+
eventType,
|
|
968
|
+
payload: message.payload,
|
|
969
|
+
tags: eventTags,
|
|
970
|
+
timestamp,
|
|
971
|
+
causationId: metadata.causationId,
|
|
972
|
+
correlationId: metadata.correlationId,
|
|
973
|
+
executedUser: metadata.executedUser
|
|
974
|
+
};
|
|
975
|
+
await this.beforeWrite?.("event");
|
|
976
|
+
try {
|
|
977
|
+
await this.client.create(this.containers.events, document, pk);
|
|
978
|
+
return document;
|
|
979
|
+
} catch (error) {
|
|
980
|
+
if (isStatus(error, 409)) continue;
|
|
981
|
+
throw error;
|
|
982
|
+
}
|
|
983
|
+
}
|
|
984
|
+
const prior = eventFrom(existing.document);
|
|
985
|
+
if (prior.eventType !== eventType) {
|
|
986
|
+
throw new CanonicalEventIdentityConflictError("cosmos", message.eventId);
|
|
987
|
+
}
|
|
988
|
+
if (prior.suid !== message.suid || prior.payload !== message.payload || JSON.stringify(prior.eventTags) !== JSON.stringify(eventTags) || prior.timestamp !== timestamp || prior.causationId !== metadata.causationId || prior.correlationId !== metadata.correlationId || prior.executedUser !== metadata.executedUser) {
|
|
989
|
+
throw new Error(`EventId ${message.eventId} conflicts with its durable Cosmos row`);
|
|
990
|
+
}
|
|
991
|
+
return existing.document;
|
|
992
|
+
}
|
|
993
|
+
throw new Error("Cosmos event CAS did not converge");
|
|
994
|
+
}
|
|
995
|
+
async mutateEventOps(message, arrivedAt, lagMs) {
|
|
996
|
+
const id = safeId("event-ops", message.eventId);
|
|
997
|
+
const pk = eventPk(message.serviceId, message.eventId);
|
|
998
|
+
const nextArrival = {
|
|
999
|
+
serviceId: message.serviceId,
|
|
1000
|
+
eventId: message.eventId,
|
|
1001
|
+
tag: message.tag,
|
|
1002
|
+
enqueuedAt: message.enqueuedAt,
|
|
1003
|
+
arrivedAt,
|
|
1004
|
+
lagMs
|
|
1005
|
+
};
|
|
1006
|
+
for (let attempt = 0; attempt < 8; attempt += 1) {
|
|
1007
|
+
const existing = await this.client.read(this.containers.events, id, pk);
|
|
1008
|
+
if (existing === void 0) {
|
|
1009
|
+
const document2 = {
|
|
1010
|
+
id,
|
|
1011
|
+
pk,
|
|
1012
|
+
serviceId: message.serviceId,
|
|
1013
|
+
kind: "event-ops",
|
|
1014
|
+
eventId: message.eventId,
|
|
1015
|
+
attemptId: message.attemptId,
|
|
1016
|
+
allocatorLineageId: message.allocatorLineageId,
|
|
1017
|
+
firstArrivedAt: arrivedAt,
|
|
1018
|
+
lastArrivedAt: arrivedAt,
|
|
1019
|
+
maxDeliveryLagMs: lagMs,
|
|
1020
|
+
arrivals: [nextArrival]
|
|
1021
|
+
};
|
|
1022
|
+
await this.beforeWrite?.("event");
|
|
1023
|
+
try {
|
|
1024
|
+
await this.client.create(this.containers.events, document2, pk);
|
|
1025
|
+
return document2;
|
|
1026
|
+
} catch (error) {
|
|
1027
|
+
if (isStatus(error, 409)) continue;
|
|
1028
|
+
throw error;
|
|
1029
|
+
}
|
|
1030
|
+
}
|
|
1031
|
+
if (existing.document.allocatorLineageId !== message.allocatorLineageId) {
|
|
1032
|
+
throw new CanonicalEventIdentityConflictError("cosmos", message.eventId, "Cosmos event sidecar lineage conflicts");
|
|
1033
|
+
}
|
|
1034
|
+
const arrivals = [...existing.document.arrivals];
|
|
1035
|
+
const index = arrivals.findIndex((arrival) => arrival.tag === message.tag);
|
|
1036
|
+
if (index < 0) arrivals.push(nextArrival);
|
|
1037
|
+
else {
|
|
1038
|
+
const prior = arrivals[index];
|
|
1039
|
+
arrivals[index] = {
|
|
1040
|
+
...prior,
|
|
1041
|
+
enqueuedAt: Math.min(prior.enqueuedAt, nextArrival.enqueuedAt),
|
|
1042
|
+
arrivedAt: Math.max(prior.arrivedAt, nextArrival.arrivedAt),
|
|
1043
|
+
lagMs: Math.max(prior.lagMs, nextArrival.lagMs)
|
|
1044
|
+
};
|
|
1045
|
+
}
|
|
1046
|
+
const document = {
|
|
1047
|
+
...existing.document,
|
|
1048
|
+
firstArrivedAt: Math.min(existing.document.firstArrivedAt, arrivedAt),
|
|
1049
|
+
lastArrivedAt: Math.max(existing.document.lastArrivedAt, arrivedAt),
|
|
1050
|
+
maxDeliveryLagMs: Math.max(existing.document.maxDeliveryLagMs, lagMs),
|
|
1051
|
+
arrivals
|
|
1052
|
+
};
|
|
1053
|
+
await this.beforeWrite?.("event");
|
|
1054
|
+
if (await this.client.replace(this.containers.events, document, pk, existing.etag)) return document;
|
|
1055
|
+
}
|
|
1056
|
+
throw new Error("Cosmos event sidecar CAS did not converge");
|
|
1057
|
+
}
|
|
1058
|
+
async updateLag(serviceId, lagMs, observedAt) {
|
|
1059
|
+
for (let attempt = 0; attempt < 8; attempt += 1) {
|
|
1060
|
+
const existing = await this.client.read(this.containers.lagEstimates, serviceId, serviceId);
|
|
1061
|
+
const currentEstimate = existing === void 0 ? 0 : decayedLagEstimateMs(asNumber(existing.document.estimateMs, "estimateMs"), asNumber(existing.document.observedAt, "observedAt"), observedAt);
|
|
1062
|
+
const document = {
|
|
1063
|
+
id: serviceId,
|
|
1064
|
+
serviceId,
|
|
1065
|
+
estimateMs: Math.max(currentEstimate, lagMs),
|
|
1066
|
+
observedAt
|
|
1067
|
+
};
|
|
1068
|
+
await this.beforeWrite?.("lag");
|
|
1069
|
+
if (existing === void 0) {
|
|
1070
|
+
try {
|
|
1071
|
+
await this.client.create(this.containers.lagEstimates, document, serviceId);
|
|
1072
|
+
return;
|
|
1073
|
+
} catch (error) {
|
|
1074
|
+
if (isStatus(error, 409)) continue;
|
|
1075
|
+
throw error;
|
|
1076
|
+
}
|
|
1077
|
+
} else if (await this.client.replace(this.containers.lagEstimates, document, serviceId, existing.etag)) {
|
|
1078
|
+
return;
|
|
1079
|
+
}
|
|
1080
|
+
}
|
|
1081
|
+
throw new Error("Cosmos lag estimate CAS did not converge");
|
|
1082
|
+
}
|
|
1083
|
+
ready() {
|
|
1084
|
+
if (!this.initialized) throw new Error("CosmosEventStore.initialize() must complete before use");
|
|
1085
|
+
}
|
|
1086
|
+
};
|
|
1087
|
+
|
|
1088
|
+
// src/bootstrap/manifest.ts
|
|
1089
|
+
var isObject = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1090
|
+
var canonical = (value) => {
|
|
1091
|
+
if (Array.isArray(value)) return `[${value.map(canonical).join(",")}]`;
|
|
1092
|
+
if (isObject(value)) return `{${Object.keys(value).sort().map((key) => `${JSON.stringify(key)}:${canonical(value[key])}`).join(",")}}`;
|
|
1093
|
+
return JSON.stringify(value);
|
|
1094
|
+
};
|
|
1095
|
+
function bootstrapDigest(value) {
|
|
1096
|
+
let hash = 2166136261;
|
|
1097
|
+
for (const char of canonical(value)) {
|
|
1098
|
+
hash ^= char.charCodeAt(0);
|
|
1099
|
+
hash = Math.imul(hash, 16777619);
|
|
1100
|
+
}
|
|
1101
|
+
return `fnv1a32:${(hash >>> 0).toString(16).padStart(8, "0")}`;
|
|
1102
|
+
}
|
|
1103
|
+
|
|
1104
|
+
// src/tag/EventDigest.ts
|
|
1105
|
+
var EVENT_DIGEST_DOMAIN_SEPARATOR = "sekiban-dcb-ts/eventDigest/v2";
|
|
1106
|
+
var EventDigestValidationError = class extends TypeError {
|
|
1107
|
+
constructor(message) {
|
|
1108
|
+
super(message);
|
|
1109
|
+
this.name = "EventDigestValidationError";
|
|
1110
|
+
}
|
|
1111
|
+
};
|
|
1112
|
+
var encoder = new TextEncoder();
|
|
1113
|
+
var requiredFieldNames = [
|
|
1114
|
+
"serviceId",
|
|
1115
|
+
"eventId",
|
|
1116
|
+
"sortableUniqueId",
|
|
1117
|
+
"eventType",
|
|
1118
|
+
"timestamp",
|
|
1119
|
+
"attemptId",
|
|
1120
|
+
"declaredTagSet",
|
|
1121
|
+
"payload"
|
|
1122
|
+
];
|
|
1123
|
+
var optionalFieldNames = ["allocatorLineageId"];
|
|
1124
|
+
var excludedFieldNames = ["causationId", "correlationId", "executedUser"];
|
|
1125
|
+
var allowedFieldNames = /* @__PURE__ */ new Set([...requiredFieldNames, ...optionalFieldNames, ...excludedFieldNames]);
|
|
1126
|
+
function byteCompare(left, right) {
|
|
1127
|
+
const length = Math.min(left.byteLength, right.byteLength);
|
|
1128
|
+
for (let index = 0; index < length; index += 1) {
|
|
1129
|
+
const delta = left[index] - right[index];
|
|
1130
|
+
if (delta !== 0) return delta;
|
|
1131
|
+
}
|
|
1132
|
+
return left.byteLength - right.byteLength;
|
|
1133
|
+
}
|
|
1134
|
+
function concat(parts) {
|
|
1135
|
+
const length = parts.reduce((total, part) => total + part.byteLength, 0);
|
|
1136
|
+
const bytes = new Uint8Array(length);
|
|
1137
|
+
let offset = 0;
|
|
1138
|
+
for (const part of parts) {
|
|
1139
|
+
bytes.set(part, offset);
|
|
1140
|
+
offset += part.byteLength;
|
|
1141
|
+
}
|
|
1142
|
+
return bytes;
|
|
1143
|
+
}
|
|
1144
|
+
function lengthPrefix(length) {
|
|
1145
|
+
if (!Number.isSafeInteger(length) || length < 0 || length > 4294967295) {
|
|
1146
|
+
throw new EventDigestValidationError("eventDigest field length is outside uint32 range");
|
|
1147
|
+
}
|
|
1148
|
+
return Uint8Array.of(
|
|
1149
|
+
length >>> 24 & 255,
|
|
1150
|
+
length >>> 16 & 255,
|
|
1151
|
+
length >>> 8 & 255,
|
|
1152
|
+
length & 255
|
|
1153
|
+
);
|
|
1154
|
+
}
|
|
1155
|
+
function utf8(value) {
|
|
1156
|
+
return encoder.encode(value);
|
|
1157
|
+
}
|
|
1158
|
+
function hasOwn(input, name) {
|
|
1159
|
+
return Object.prototype.hasOwnProperty.call(input, name);
|
|
1160
|
+
}
|
|
1161
|
+
function requiredUtf8(input, name) {
|
|
1162
|
+
const value = input[name];
|
|
1163
|
+
if (typeof value !== "string") {
|
|
1164
|
+
throw new EventDigestValidationError(`eventDigest required field ${name} is absent or not a string`);
|
|
1165
|
+
}
|
|
1166
|
+
return utf8(value);
|
|
1167
|
+
}
|
|
1168
|
+
function tagSetBytes(tags) {
|
|
1169
|
+
const canonical2 = canonicalDeclaredTagSet(tags);
|
|
1170
|
+
return concat(canonical2.map((tag) => {
|
|
1171
|
+
const bytes = utf8(tag);
|
|
1172
|
+
return concat([lengthPrefix(bytes.byteLength), bytes]);
|
|
1173
|
+
}));
|
|
1174
|
+
}
|
|
1175
|
+
function canonicalDeclaredTagSet(tags) {
|
|
1176
|
+
if (!Array.isArray(tags) || !tags.every((tag) => typeof tag === "string")) {
|
|
1177
|
+
throw new EventDigestValidationError("eventDigest declaredTagSet must contain only strings");
|
|
1178
|
+
}
|
|
1179
|
+
const encoded = tags.map((tag) => ({ tag, bytes: utf8(tag) })).sort((left, right) => byteCompare(left.bytes, right.bytes));
|
|
1180
|
+
const unique = [];
|
|
1181
|
+
let previous;
|
|
1182
|
+
for (const item of encoded) {
|
|
1183
|
+
if (previous !== void 0 && byteCompare(previous, item.bytes) === 0) continue;
|
|
1184
|
+
unique.push(item.tag);
|
|
1185
|
+
previous = item.bytes;
|
|
1186
|
+
}
|
|
1187
|
+
return Object.freeze(unique);
|
|
1188
|
+
}
|
|
1189
|
+
function assertClosedProjection(input) {
|
|
1190
|
+
for (const name of Object.keys(input)) {
|
|
1191
|
+
if (!allowedFieldNames.has(name)) {
|
|
1192
|
+
throw new EventDigestValidationError(`eventDigest field ${name} is neither included nor explicitly excluded`);
|
|
1193
|
+
}
|
|
1194
|
+
}
|
|
1195
|
+
for (const name of requiredFieldNames) {
|
|
1196
|
+
if (!hasOwn(input, name)) {
|
|
1197
|
+
throw new EventDigestValidationError(`eventDigest required field ${name} is absent`);
|
|
1198
|
+
}
|
|
1199
|
+
}
|
|
1200
|
+
if (!Array.isArray(input.declaredTagSet)) {
|
|
1201
|
+
throw new EventDigestValidationError("eventDigest required field declaredTagSet is absent or invalid");
|
|
1202
|
+
}
|
|
1203
|
+
if (!(input.payload instanceof Uint8Array)) {
|
|
1204
|
+
throw new EventDigestValidationError("eventDigest required field payload is absent or not raw bytes");
|
|
1205
|
+
}
|
|
1206
|
+
if (hasOwn(input, "allocatorLineageId") && typeof input.allocatorLineageId !== "string") {
|
|
1207
|
+
throw new EventDigestValidationError("eventDigest optional field allocatorLineageId must be absent or a string");
|
|
1208
|
+
}
|
|
1209
|
+
}
|
|
1210
|
+
function encodeField(name, value) {
|
|
1211
|
+
const nameBytes = utf8(name);
|
|
1212
|
+
const present = value === void 0 ? Uint8Array.of(0) : Uint8Array.of(1);
|
|
1213
|
+
const valueBytes = value ?? new Uint8Array();
|
|
1214
|
+
return concat([
|
|
1215
|
+
present,
|
|
1216
|
+
lengthPrefix(nameBytes.byteLength),
|
|
1217
|
+
nameBytes,
|
|
1218
|
+
lengthPrefix(valueBytes.byteLength),
|
|
1219
|
+
valueBytes
|
|
1220
|
+
]);
|
|
1221
|
+
}
|
|
1222
|
+
function eventDigestBytes(input) {
|
|
1223
|
+
assertClosedProjection(input);
|
|
1224
|
+
const fields = [
|
|
1225
|
+
["serviceId", requiredUtf8(input, "serviceId")],
|
|
1226
|
+
["eventId", requiredUtf8(input, "eventId")],
|
|
1227
|
+
["sortableUniqueId", requiredUtf8(input, "sortableUniqueId")],
|
|
1228
|
+
["eventType", requiredUtf8(input, "eventType")],
|
|
1229
|
+
["timestamp", requiredUtf8(input, "timestamp")],
|
|
1230
|
+
["allocatorLineageId", hasOwn(input, "allocatorLineageId") ? utf8(input.allocatorLineageId) : void 0],
|
|
1231
|
+
["attemptId", requiredUtf8(input, "attemptId")],
|
|
1232
|
+
["declaredTagSet", tagSetBytes(input.declaredTagSet)],
|
|
1233
|
+
["payload", input.payload]
|
|
1234
|
+
];
|
|
1235
|
+
return concat([
|
|
1236
|
+
utf8(EVENT_DIGEST_DOMAIN_SEPARATOR),
|
|
1237
|
+
Uint8Array.of(0),
|
|
1238
|
+
...fields.map(([name, value]) => encodeField(name, value))
|
|
1239
|
+
]);
|
|
1240
|
+
}
|
|
1241
|
+
async function eventDigestHex(input) {
|
|
1242
|
+
const bytes = eventDigestBytes(input);
|
|
1243
|
+
const source = bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength);
|
|
1244
|
+
const digest = await crypto.subtle.digest("SHA-256", source);
|
|
1245
|
+
return [...new Uint8Array(digest)].map((byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
1246
|
+
}
|
|
1247
|
+
|
|
1248
|
+
// src/bootstrap/BootstrapStoreAdapter.ts
|
|
1249
|
+
function arrayBufferBase64(bytes) {
|
|
1250
|
+
let binary = "";
|
|
1251
|
+
for (const byte of bytes) binary += String.fromCharCode(byte);
|
|
1252
|
+
return btoa(binary);
|
|
1253
|
+
}
|
|
1254
|
+
var BootstrapIdentityConflictError = class extends Error {
|
|
1255
|
+
constructor(provider, eventId) {
|
|
1256
|
+
super(`${provider} bootstrap EventId ${eventId} conflicts with the target identity`);
|
|
1257
|
+
this.provider = provider;
|
|
1258
|
+
this.eventId = eventId;
|
|
1259
|
+
this.name = "BootstrapIdentityConflictError";
|
|
1260
|
+
}
|
|
1261
|
+
provider;
|
|
1262
|
+
eventId;
|
|
1263
|
+
code = "BOOTSTRAP_EVENT_IDENTITY_CONFLICT";
|
|
1264
|
+
};
|
|
1265
|
+
function compareOpaque(left, right) {
|
|
1266
|
+
const a = new TextEncoder().encode(left);
|
|
1267
|
+
const b = new TextEncoder().encode(right);
|
|
1268
|
+
for (let index = 0; index < Math.min(a.length, b.length); index += 1) if (a[index] !== b[index]) return a[index] - b[index];
|
|
1269
|
+
return a.length - b.length;
|
|
1270
|
+
}
|
|
1271
|
+
function bootstrapEventIdentityMatches(event, record) {
|
|
1272
|
+
return event.suid === record.suid && event.payload === record.payload && event.eventType === record.eventType && event.timestamp === record.timestamp && event.causationId === record.causationId && event.correlationId === record.correlationId && event.executedUser === record.executedUser && JSON.stringify(event.eventTags) === JSON.stringify(record.eventTags);
|
|
1273
|
+
}
|
|
1274
|
+
function toRecord(event) {
|
|
1275
|
+
return {
|
|
1276
|
+
eventId: event.eventId,
|
|
1277
|
+
suid: event.suid,
|
|
1278
|
+
payload: event.payload,
|
|
1279
|
+
eventTags: [...event.eventTags],
|
|
1280
|
+
eventType: event.eventType,
|
|
1281
|
+
provenance: { origin: "g32" },
|
|
1282
|
+
timestamp: event.timestamp,
|
|
1283
|
+
causationId: event.causationId,
|
|
1284
|
+
correlationId: event.correlationId,
|
|
1285
|
+
executedUser: event.executedUser
|
|
1286
|
+
};
|
|
1287
|
+
}
|
|
1288
|
+
var BootstrapStoreAdapter = class {
|
|
1289
|
+
constructor(provider, store, identityGuard = bootstrapEventIdentityMatches) {
|
|
1290
|
+
this.provider = provider;
|
|
1291
|
+
this.store = store;
|
|
1292
|
+
this.identityGuard = identityGuard;
|
|
1293
|
+
}
|
|
1294
|
+
provider;
|
|
1295
|
+
store;
|
|
1296
|
+
identityGuard;
|
|
1297
|
+
async exportPage(input) {
|
|
1298
|
+
if (!Number.isSafeInteger(input.pageSize) || input.pageSize <= 0) throw new Error("bootstrap export pageSize must be positive");
|
|
1299
|
+
await this.store.initialize();
|
|
1300
|
+
const observed = await this.store.readAllEvents(input.sourceServiceId, "");
|
|
1301
|
+
const highWatermark = input.cursor?.highWatermark ?? observed.at(-1)?.suid ?? null;
|
|
1302
|
+
const snapshot = observed.filter((event) => highWatermark === null || compareOpaque(event.suid, highWatermark) <= 0).map(toRecord);
|
|
1303
|
+
const start = input.cursor?.nextEventId === null || input.cursor?.nextEventId === void 0 ? 0 : snapshot.findIndex((event) => event.eventId === input.cursor.nextEventId) + 1;
|
|
1304
|
+
const page = snapshot.slice(Math.max(0, start), Math.max(0, start) + input.pageSize);
|
|
1305
|
+
const tagCounts = {};
|
|
1306
|
+
for (const event of snapshot) for (const tag of event.eventTags) tagCounts[tag] = (tagCounts[tag] ?? 0) + 1;
|
|
1307
|
+
const draft = {
|
|
1308
|
+
format: "sekiban-dcb-bootstrap",
|
|
1309
|
+
version: 1,
|
|
1310
|
+
source: { serviceId: input.sourceServiceId, lineageId: input.sourceAllocatorLineageId },
|
|
1311
|
+
target: { serviceId: input.targetServiceId, allocatorLineageId: input.allocatorLineageId },
|
|
1312
|
+
highWatermark,
|
|
1313
|
+
eventCount: snapshot.length,
|
|
1314
|
+
tagCounts,
|
|
1315
|
+
canonicalization: "utf8-json-sorted-keys-v1"
|
|
1316
|
+
};
|
|
1317
|
+
const fullDump = { manifest: { ...draft, contentDigest: "" }, events: snapshot };
|
|
1318
|
+
const manifest = { ...draft, contentDigest: bootstrapDigest(fullDump) };
|
|
1319
|
+
const pageEnd = Math.max(0, start) + page.length;
|
|
1320
|
+
return {
|
|
1321
|
+
dump: { manifest, events: snapshot },
|
|
1322
|
+
page,
|
|
1323
|
+
cursor: { highWatermark, nextEventId: pageEnd >= snapshot.length ? null : snapshot[pageEnd - 1].eventId },
|
|
1324
|
+
complete: pageEnd >= snapshot.length
|
|
1325
|
+
};
|
|
1326
|
+
}
|
|
1327
|
+
async admitBootstrap(input) {
|
|
1328
|
+
await this.store.initialize();
|
|
1329
|
+
const existing = new Map((await this.store.readAllEvents(input.manifest.target.serviceId, "")).map((event) => [event.eventId, event]));
|
|
1330
|
+
for (const record of input.events) {
|
|
1331
|
+
const prior = existing.get(record.eventId);
|
|
1332
|
+
if (prior !== void 0 && !this.identityGuard(prior, record)) throw new BootstrapIdentityConflictError(this.provider, record.eventId);
|
|
1333
|
+
for (const tag of record.eventTags) {
|
|
1334
|
+
const attemptId = `bootstrap:${input.importId}:${input.leaseEpoch}`;
|
|
1335
|
+
const digestInput = {
|
|
1336
|
+
serviceId: input.manifest.target.serviceId,
|
|
1337
|
+
eventId: record.eventId,
|
|
1338
|
+
sortableUniqueId: record.suid,
|
|
1339
|
+
eventType: record.eventType,
|
|
1340
|
+
timestamp: record.timestamp,
|
|
1341
|
+
allocatorLineageId: input.manifest.target.allocatorLineageId,
|
|
1342
|
+
attemptId,
|
|
1343
|
+
declaredTagSet: canonicalDeclaredTagSet(record.eventTags),
|
|
1344
|
+
payload: new TextEncoder().encode(record.payload)
|
|
1345
|
+
};
|
|
1346
|
+
const delivered = await this.store.recordDelivery({
|
|
1347
|
+
version: 1,
|
|
1348
|
+
serviceId: input.manifest.target.serviceId,
|
|
1349
|
+
allocatorLineageId: input.manifest.target.allocatorLineageId,
|
|
1350
|
+
tag,
|
|
1351
|
+
attemptId,
|
|
1352
|
+
eventId: record.eventId,
|
|
1353
|
+
suid: record.suid,
|
|
1354
|
+
payload: record.payload,
|
|
1355
|
+
eventTags: [...record.eventTags],
|
|
1356
|
+
eventType: record.eventType,
|
|
1357
|
+
provenance: "g32",
|
|
1358
|
+
timestamp: record.timestamp,
|
|
1359
|
+
causationId: record.causationId,
|
|
1360
|
+
correlationId: record.correlationId,
|
|
1361
|
+
executedUser: record.executedUser,
|
|
1362
|
+
enqueuedAt: 0,
|
|
1363
|
+
completeness: {
|
|
1364
|
+
canonicalBytesBase64: arrayBufferBase64(eventDigestBytes(digestInput)),
|
|
1365
|
+
eventDigest: await eventDigestHex(digestInput),
|
|
1366
|
+
declaredTagSet: [...digestInput.declaredTagSet],
|
|
1367
|
+
localCommittedMembership: [{ serviceId: input.manifest.target.serviceId, eventId: record.eventId, tag }],
|
|
1368
|
+
// Bootstrap is not a source outbox replay path. The deterministic
|
|
1369
|
+
// per-event/tag ordinal is only enough to satisfy the D1 receipt
|
|
1370
|
+
// key while G44's source scanner remains explicitly out of scope.
|
|
1371
|
+
obligationSequence: Math.max(1, [...new TextEncoder().encode(`${record.eventId}|${tag}`)].reduce((total, byte) => (total * 33 + byte) % 1e9, 5381))
|
|
1372
|
+
}
|
|
1373
|
+
// A C# import may contain a historical RFC 4122 Id. It bypasses
|
|
1374
|
+
// Queue/doorbell and is admitted only through this fenced path;
|
|
1375
|
+
// normal deliveries still require UUID v7.
|
|
1376
|
+
}, 0, "import");
|
|
1377
|
+
if (delivered.outcome !== "stored") throw new Error(`${this.provider} bootstrap admission rejected ${record.eventId}`);
|
|
1378
|
+
}
|
|
1379
|
+
}
|
|
1380
|
+
}
|
|
1381
|
+
async verifyBootstrap(input) {
|
|
1382
|
+
await this.store.initialize();
|
|
1383
|
+
const actual = await this.store.readAllEvents(input.manifest.target.serviceId, "");
|
|
1384
|
+
if (actual.length !== input.manifest.eventCount) throw new Error(`${this.provider} bootstrap store count differs for ${input.importId}`);
|
|
1385
|
+
}
|
|
1386
|
+
};
|
|
1387
|
+
function createBootstrapStoreAdapter(provider, store, identityGuard) {
|
|
1388
|
+
return new BootstrapStoreAdapter(provider, store, identityGuard);
|
|
1389
|
+
}
|
|
1390
|
+
|
|
1391
|
+
// src/cosmos.ts
|
|
1392
|
+
function createCosmosStoreProvider(config) {
|
|
1393
|
+
return Object.freeze({
|
|
1394
|
+
name: "cosmos",
|
|
1395
|
+
isConfigured: () => true,
|
|
1396
|
+
create: () => new CosmosEventStore(config),
|
|
1397
|
+
createBootstrapAdapter: () => createCosmosBootstrapAdapter(new CosmosEventStore(config))
|
|
1398
|
+
});
|
|
1399
|
+
}
|
|
1400
|
+
function createCosmosBootstrapAdapter(store) {
|
|
1401
|
+
return createBootstrapStoreAdapter("cosmos", store, (event, record) => bootstrapEventIdentityMatches(event, record));
|
|
1402
|
+
}
|
|
1403
|
+
export {
|
|
1404
|
+
CosmosClientError,
|
|
1405
|
+
CosmosEventStore,
|
|
1406
|
+
CosmosRestClient,
|
|
1407
|
+
DEFAULT_COSMOS_CONTAINERS,
|
|
1408
|
+
compareCosmosSuid,
|
|
1409
|
+
createCosmosBootstrapAdapter,
|
|
1410
|
+
createCosmosStoreProvider
|
|
1411
|
+
};
|