@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
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import type { DeliverySource, DownstreamOutboxMessage, PipelineClock } from "./types";
|
|
2
|
+
import type { StoreProvider } from "../store/provider";
|
|
3
|
+
import type { StoreProviderEnvironment } from "../store/provider";
|
|
4
|
+
import type { GlobalReceiptJoin, PipelineStore, StoredEvent } from "../store/types";
|
|
5
|
+
import type { DeliveryAttemptContext } from "../store/types";
|
|
6
|
+
import { type G60DurableHopObserver } from "../diagnostics/G60DurableHop";
|
|
7
|
+
export type DeliveryViewFailureClass = "retryable-transient" | "duplicate-race" | "nonretryable-definition-poison";
|
|
8
|
+
export type DeliveryViewApplyResult = "applied" | "duplicate-race" | void;
|
|
9
|
+
export interface DeliveryViewInput {
|
|
10
|
+
readonly message: DownstreamOutboxMessage;
|
|
11
|
+
readonly event: StoredEvent;
|
|
12
|
+
readonly arrivedAt: number;
|
|
13
|
+
readonly source: DeliverySource;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* A view handler owns one view's unsafe observation and atomic apply. The
|
|
17
|
+
* delivery core deliberately knows nothing about D1 rows, receipts, or kicks;
|
|
18
|
+
* this boundary is what lets the Queue and doorbell share exactly one order
|
|
19
|
+
* of operations.
|
|
20
|
+
*/
|
|
21
|
+
export interface DeliveryViewHandler {
|
|
22
|
+
readonly id: string;
|
|
23
|
+
/**
|
|
24
|
+
* The exceptional unsafe lane is independent of G44 completeness. It may
|
|
25
|
+
* only be used by a handler whose mutation never advances a safe
|
|
26
|
+
* checkpoint; omitted means the ordinary completeness-gated lane.
|
|
27
|
+
*/
|
|
28
|
+
readonly admission?: "independent-unsafe";
|
|
29
|
+
readonly apply: (input: DeliveryViewInput) => Promise<DeliveryViewApplyResult>;
|
|
30
|
+
readonly classifyError?: (error: unknown) => DeliveryViewFailureClass;
|
|
31
|
+
}
|
|
32
|
+
export interface DeliveryViewResult {
|
|
33
|
+
readonly id: string;
|
|
34
|
+
readonly status: "applied" | "duplicate-race" | "failed";
|
|
35
|
+
readonly failureClass?: DeliveryViewFailureClass;
|
|
36
|
+
readonly error?: string;
|
|
37
|
+
readonly durationMs: number;
|
|
38
|
+
}
|
|
39
|
+
export interface DeliveryCoreFailure {
|
|
40
|
+
readonly phase: "recordDelivery" | "detector" | "completeness" | "view" | "drain";
|
|
41
|
+
readonly class: DeliveryViewFailureClass | "retryable-transient";
|
|
42
|
+
readonly viewId?: string;
|
|
43
|
+
readonly error: string;
|
|
44
|
+
}
|
|
45
|
+
export interface DeliveryCoreResult {
|
|
46
|
+
readonly source: DeliverySource;
|
|
47
|
+
readonly correlationId: string;
|
|
48
|
+
readonly outcome: "stored" | "suid-collision" | "lineage-mismatch";
|
|
49
|
+
readonly arrivedAt: number;
|
|
50
|
+
readonly detectorApplied: boolean;
|
|
51
|
+
readonly views: readonly DeliveryViewResult[];
|
|
52
|
+
readonly failures: readonly DeliveryCoreFailure[];
|
|
53
|
+
readonly coreDurationMs: number;
|
|
54
|
+
/** The queue wrapper is the only caller that turns this into ack/retry. */
|
|
55
|
+
readonly queueDisposition: "ack" | "retry-once" | "retry-to-dlq";
|
|
56
|
+
/** A durable poison finding must reach bounded Queue/DLQ remediation. */
|
|
57
|
+
readonly queueDispositionReason: "none" | "duplicate-race-retry" | "retryable-transient" | "definition-poison-dlq";
|
|
58
|
+
/** The fast wrapper never retries or changes outbox state. */
|
|
59
|
+
readonly fastDisposition: "completed" | "failed";
|
|
60
|
+
}
|
|
61
|
+
export interface DeliveryCoreOptions {
|
|
62
|
+
readonly clock?: PipelineClock;
|
|
63
|
+
readonly store?: PipelineStore;
|
|
64
|
+
readonly storeProvider?: StoreProvider;
|
|
65
|
+
readonly views?: readonly DeliveryViewHandler[];
|
|
66
|
+
/** Internal G60 hop observation; it is never a delivery decision input. */
|
|
67
|
+
readonly durableHopObserver?: G60DurableHopObserver;
|
|
68
|
+
/** G69 diagnostic ownership only; core admission never awaits this work. */
|
|
69
|
+
readonly g69AdmissionAttempt?: DeliveryAttemptContext;
|
|
70
|
+
/** Compatibility hook for callers that have not adopted a view handler yet. */
|
|
71
|
+
readonly onStored?: (input: {
|
|
72
|
+
readonly message: DownstreamOutboxMessage;
|
|
73
|
+
readonly event: StoredEvent;
|
|
74
|
+
readonly arrivedAt: number;
|
|
75
|
+
readonly source: DeliverySource;
|
|
76
|
+
}) => Promise<void>;
|
|
77
|
+
readonly afterDelivery?: (input: {
|
|
78
|
+
readonly message: DownstreamOutboxMessage;
|
|
79
|
+
readonly event: StoredEvent;
|
|
80
|
+
readonly arrivedAt: number;
|
|
81
|
+
readonly source: DeliverySource;
|
|
82
|
+
readonly result: DeliveryCoreResult;
|
|
83
|
+
}) => Promise<void>;
|
|
84
|
+
/**
|
|
85
|
+
* Source acknowledgement is allowed only after D1 has read back an exact
|
|
86
|
+
* event + committed-membership + receipt join. Queue/doorbell handoff is
|
|
87
|
+
* intentionally not an acknowledgement authority.
|
|
88
|
+
*/
|
|
89
|
+
readonly afterGlobalReceipt?: (input: {
|
|
90
|
+
readonly message: DownstreamOutboxMessage;
|
|
91
|
+
readonly receipt: GlobalReceiptJoin;
|
|
92
|
+
readonly arrivedAt: number;
|
|
93
|
+
readonly source: DeliverySource;
|
|
94
|
+
}) => Promise<void>;
|
|
95
|
+
/**
|
|
96
|
+
* G44's one internal coverage gate. The receipt remains durable, but a
|
|
97
|
+
* materialized/live view cannot advance while global completeness is not
|
|
98
|
+
* proven by a FULL source scan.
|
|
99
|
+
*/
|
|
100
|
+
readonly beforeViews?: (input: {
|
|
101
|
+
readonly message: DownstreamOutboxMessage;
|
|
102
|
+
readonly event: StoredEvent;
|
|
103
|
+
readonly arrivedAt: number;
|
|
104
|
+
readonly source: DeliverySource;
|
|
105
|
+
}) => Promise<void>;
|
|
106
|
+
/**
|
|
107
|
+
* G44 records a detector fault before returning. The core still fails
|
|
108
|
+
* closed even when this observation authority itself is unavailable.
|
|
109
|
+
*/
|
|
110
|
+
readonly onDetectorFailure?: (input: {
|
|
111
|
+
readonly message: DownstreamOutboxMessage;
|
|
112
|
+
readonly arrivedAt: number;
|
|
113
|
+
readonly source: DeliverySource;
|
|
114
|
+
readonly error: unknown;
|
|
115
|
+
}) => Promise<void>;
|
|
116
|
+
readonly correlationId?: string;
|
|
117
|
+
}
|
|
118
|
+
export interface DeliveryCoreEnvironment extends StoreProviderEnvironment {
|
|
119
|
+
readonly REPAIR_EXCLUSION_LOOKUP?: Fetcher;
|
|
120
|
+
}
|
|
121
|
+
export type { DeliverySource };
|
|
122
|
+
export declare function deliveryCorrelationId(message: DownstreamOutboxMessage, source: DeliverySource, supplied?: string): string;
|
|
123
|
+
/**
|
|
124
|
+
* Shared transport-neutral delivery order. There are intentionally no Queue
|
|
125
|
+
* message methods, outbox marks, or retry decisions in this function.
|
|
126
|
+
*/
|
|
127
|
+
export declare function processDeliveryCore(message: DownstreamOutboxMessage, source: DeliverySource, env: DeliveryCoreEnvironment, options?: DeliveryCoreOptions): Promise<DeliveryCoreResult>;
|
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
import { BindingExclusionLedgerClient } from "./ExclusionLookup";
|
|
2
|
+
import { InconsistencyDetector } from "./InconsistencyDetector";
|
|
3
|
+
import { resolveDeliveryIdentity } from "../eventIdentity";
|
|
4
|
+
import { systemPipelineClock } from "./types";
|
|
5
|
+
import { observeG60PostAdmission } from "../diagnostics/G60DurableHop";
|
|
6
|
+
function observePostAdmission(observer, message, source, input) {
|
|
7
|
+
observeG60PostAdmission(observer, {
|
|
8
|
+
...input,
|
|
9
|
+
serviceId: message.serviceId,
|
|
10
|
+
eventId: message.eventId,
|
|
11
|
+
suid: message.suid,
|
|
12
|
+
attemptId: message.attemptId,
|
|
13
|
+
partitionTag: message.tag,
|
|
14
|
+
transport: source,
|
|
15
|
+
observedAt: Date.now(),
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
function errorText(error) {
|
|
19
|
+
if (error instanceof Error && error.message.length > 0)
|
|
20
|
+
return error.message;
|
|
21
|
+
return String(error);
|
|
22
|
+
}
|
|
23
|
+
export function deliveryCorrelationId(message, source, supplied) {
|
|
24
|
+
return supplied ?? `${source}:${message.serviceId}:${message.eventId}:${message.attemptId}`;
|
|
25
|
+
}
|
|
26
|
+
function defaultFailureClass(error) {
|
|
27
|
+
const candidate = error;
|
|
28
|
+
if (candidate.failureClass === "duplicate-race")
|
|
29
|
+
return "duplicate-race";
|
|
30
|
+
if (candidate.failureClass === "nonretryable-definition-poison")
|
|
31
|
+
return "nonretryable-definition-poison";
|
|
32
|
+
if (candidate.failureClass === "retryable-transient")
|
|
33
|
+
return "retryable-transient";
|
|
34
|
+
if (candidate.code === "UNSAFE_DUPLICATE_RACE")
|
|
35
|
+
return "duplicate-race";
|
|
36
|
+
if (candidate.retryable === false)
|
|
37
|
+
return "nonretryable-definition-poison";
|
|
38
|
+
return "retryable-transient";
|
|
39
|
+
}
|
|
40
|
+
function disposition(failures, source) {
|
|
41
|
+
// A duplicate race is a blocking Queue outcome: the loser must be delivered
|
|
42
|
+
// once more so the receipt guard can turn the replay into a durable no-op.
|
|
43
|
+
// The fast path remains silent-fallback, so its disposition is calculated
|
|
44
|
+
// from the non-duplicate failures separately below.
|
|
45
|
+
const blocking = failures.length > 0;
|
|
46
|
+
const duplicateRaceOnly = blocking && failures.every((failure) => failure.class === "duplicate-race");
|
|
47
|
+
const poisonOnly = blocking && failures.every((failure) => failure.class === "nonretryable-definition-poison");
|
|
48
|
+
const fastBlocking = failures.some((failure) => failure.class !== "duplicate-race");
|
|
49
|
+
return {
|
|
50
|
+
queueDisposition: !blocking ? "ack" : duplicateRaceOnly ? "retry-once" : "retry-to-dlq",
|
|
51
|
+
queueDispositionReason: !blocking ? "none" : duplicateRaceOnly ? "duplicate-race-retry" : poisonOnly ? "definition-poison-dlq" : "retryable-transient",
|
|
52
|
+
fastDisposition: source === "fast" && fastBlocking ? "failed" : "completed",
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
function result(source, message, outcome, arrivedAt, detectorApplied, views, failures, suppliedCorrelationId, startedAt = performance.now()) {
|
|
56
|
+
return {
|
|
57
|
+
source,
|
|
58
|
+
correlationId: deliveryCorrelationId(message, source, suppliedCorrelationId),
|
|
59
|
+
outcome,
|
|
60
|
+
arrivedAt,
|
|
61
|
+
detectorApplied,
|
|
62
|
+
views,
|
|
63
|
+
failures,
|
|
64
|
+
coreDurationMs: Math.max(0, performance.now() - startedAt),
|
|
65
|
+
...disposition(failures, source),
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
function providerStore(env, options) {
|
|
69
|
+
if (options.store !== undefined)
|
|
70
|
+
return options.store;
|
|
71
|
+
if (options.storeProvider === undefined)
|
|
72
|
+
throw new Error("A downstream store provider is not configured");
|
|
73
|
+
return options.storeProvider.create(env);
|
|
74
|
+
}
|
|
75
|
+
async function applyViewHandlers(views, message, storedEvent, arrivedAt, source, observer) {
|
|
76
|
+
// The branches are independent atomic batches, so starting them together
|
|
77
|
+
// bounds fast-path latency by the slowest view while preserving the input
|
|
78
|
+
// order in the returned oracle.
|
|
79
|
+
const outcomes = await Promise.all(views.map(async (view) => {
|
|
80
|
+
const viewStartedAt = performance.now();
|
|
81
|
+
observePostAdmission(observer, message, source, {
|
|
82
|
+
stage: "unsafe-view-apply",
|
|
83
|
+
boundary: "start",
|
|
84
|
+
outcome: "started",
|
|
85
|
+
viewId: view.id,
|
|
86
|
+
});
|
|
87
|
+
try {
|
|
88
|
+
const applied = await view.apply({ message, event: storedEvent, arrivedAt, source });
|
|
89
|
+
const status = applied === "duplicate-race" ? "duplicate-race" : "applied";
|
|
90
|
+
observePostAdmission(observer, message, source, {
|
|
91
|
+
stage: "unsafe-view-apply",
|
|
92
|
+
boundary: "end",
|
|
93
|
+
outcome: status,
|
|
94
|
+
viewId: view.id,
|
|
95
|
+
});
|
|
96
|
+
return {
|
|
97
|
+
result: { id: view.id, status, durationMs: Math.max(0, performance.now() - viewStartedAt) },
|
|
98
|
+
failure: undefined,
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
catch (error) {
|
|
102
|
+
const failureClass = view.classifyError?.(error) ?? defaultFailureClass(error);
|
|
103
|
+
observePostAdmission(observer, message, source, {
|
|
104
|
+
stage: "unsafe-view-apply",
|
|
105
|
+
boundary: "end",
|
|
106
|
+
outcome: `failed:${failureClass}`,
|
|
107
|
+
viewId: view.id,
|
|
108
|
+
});
|
|
109
|
+
return {
|
|
110
|
+
result: {
|
|
111
|
+
id: view.id,
|
|
112
|
+
status: "failed",
|
|
113
|
+
failureClass,
|
|
114
|
+
error: errorText(error),
|
|
115
|
+
durationMs: Math.max(0, performance.now() - viewStartedAt),
|
|
116
|
+
},
|
|
117
|
+
failure: { phase: "view", class: failureClass, viewId: view.id, error: errorText(error) },
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
}));
|
|
121
|
+
const results = outcomes.map((outcome) => outcome.result);
|
|
122
|
+
const failures = outcomes
|
|
123
|
+
.map((outcome) => outcome.failure)
|
|
124
|
+
.filter((failure) => failure !== undefined);
|
|
125
|
+
return { results, failures };
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Shared transport-neutral delivery order. There are intentionally no Queue
|
|
129
|
+
* message methods, outbox marks, or retry decisions in this function.
|
|
130
|
+
*/
|
|
131
|
+
export async function processDeliveryCore(message, source, env, options = {}) {
|
|
132
|
+
const startedAt = performance.now();
|
|
133
|
+
// Admission is deliberately before initialize/recordDelivery: a malformed
|
|
134
|
+
// post-G27 envelope cannot create an incident, detector row, or view state.
|
|
135
|
+
const resolvedIdentity = resolveDeliveryIdentity(message, source);
|
|
136
|
+
const store = providerStore(env, options);
|
|
137
|
+
await store.initialize();
|
|
138
|
+
const clock = options.clock ?? systemPipelineClock;
|
|
139
|
+
const arrivedAt = clock.now();
|
|
140
|
+
let outcome;
|
|
141
|
+
try {
|
|
142
|
+
// Normative step 1: durable EventStore record before any detector or view.
|
|
143
|
+
outcome = await store.recordDelivery(message, arrivedAt, source, options.g69AdmissionAttempt);
|
|
144
|
+
options.durableHopObserver?.observe({
|
|
145
|
+
stage: "record-delivery-batch-committed",
|
|
146
|
+
serviceId: message.serviceId,
|
|
147
|
+
eventId: message.eventId,
|
|
148
|
+
suid: message.suid,
|
|
149
|
+
attemptId: message.attemptId,
|
|
150
|
+
partitionTag: message.tag,
|
|
151
|
+
transport: source,
|
|
152
|
+
// D1 recordDelivery returns only after its batch and read-back have
|
|
153
|
+
// settled; this is the durable post-batch observation boundary.
|
|
154
|
+
observedAt: Date.now(),
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
catch (error) {
|
|
158
|
+
const failures = [{ phase: "recordDelivery", class: "retryable-transient", error: errorText(error) }];
|
|
159
|
+
return result(source, message, "stored", arrivedAt, false, [], failures, options.correlationId, startedAt);
|
|
160
|
+
}
|
|
161
|
+
// Normative step 2: typed non-stored outcome gate. Only incident
|
|
162
|
+
// projection is allowed beyond this point for a collision or lineage miss.
|
|
163
|
+
if (outcome.outcome !== "stored") {
|
|
164
|
+
try {
|
|
165
|
+
await store.projectDeliveryIncidents?.(message.serviceId);
|
|
166
|
+
}
|
|
167
|
+
catch (error) {
|
|
168
|
+
const failures = [{ phase: "detector", class: "retryable-transient", error: errorText(error) }];
|
|
169
|
+
return result(source, message, outcome.outcome, arrivedAt, false, [], failures, options.correlationId, startedAt);
|
|
170
|
+
}
|
|
171
|
+
return result(source, message, outcome.outcome, arrivedAt, false, [], [], options.correlationId, startedAt);
|
|
172
|
+
}
|
|
173
|
+
const storedEvent = resolvedIdentity.legacy && outcome.event.eventType === undefined
|
|
174
|
+
? outcome.event
|
|
175
|
+
: {
|
|
176
|
+
...outcome.event,
|
|
177
|
+
eventType: outcome.event.eventType ?? resolvedIdentity.key,
|
|
178
|
+
provenance: outcome.event.provenance ?? resolvedIdentity.provenance,
|
|
179
|
+
};
|
|
180
|
+
const failures = [];
|
|
181
|
+
const allViews = options.views ?? [];
|
|
182
|
+
const unsafeViews = allViews.filter((view) => view.admission === "independent-unsafe");
|
|
183
|
+
const gatedViews = allViews.filter((view) => view.admission !== "independent-unsafe");
|
|
184
|
+
// G44: D1's atomic batch is followed by an independent join read-back
|
|
185
|
+
// before a source Tag DO can mark its obligation acknowledged.
|
|
186
|
+
if (store.readGlobalReceiptJoin !== undefined) {
|
|
187
|
+
let receipt;
|
|
188
|
+
observePostAdmission(options.durableHopObserver, message, source, {
|
|
189
|
+
stage: "post-record-delivery-global-receipt-readback",
|
|
190
|
+
boundary: "start",
|
|
191
|
+
outcome: "started",
|
|
192
|
+
});
|
|
193
|
+
try {
|
|
194
|
+
receipt = await store.readGlobalReceiptJoin(message);
|
|
195
|
+
if (receipt === undefined)
|
|
196
|
+
throw new Error("global receipt/membership join is absent");
|
|
197
|
+
observePostAdmission(options.durableHopObserver, message, source, {
|
|
198
|
+
stage: "post-record-delivery-global-receipt-readback",
|
|
199
|
+
boundary: "end",
|
|
200
|
+
outcome: "available",
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
catch (error) {
|
|
204
|
+
observePostAdmission(options.durableHopObserver, message, source, {
|
|
205
|
+
stage: "post-record-delivery-global-receipt-readback",
|
|
206
|
+
boundary: "end",
|
|
207
|
+
outcome: "error",
|
|
208
|
+
});
|
|
209
|
+
failures.push({ phase: "recordDelivery", class: "retryable-transient", error: errorText(error) });
|
|
210
|
+
return result(source, message, "stored", arrivedAt, false, [], failures, options.correlationId, startedAt);
|
|
211
|
+
}
|
|
212
|
+
if (options.afterGlobalReceipt !== undefined) {
|
|
213
|
+
try {
|
|
214
|
+
await options.afterGlobalReceipt({ message, receipt, arrivedAt, source });
|
|
215
|
+
}
|
|
216
|
+
catch (error) {
|
|
217
|
+
failures.push({ phase: "drain", class: "retryable-transient", error: errorText(error) });
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
// G60's exceptional unsafe lane is deliberately after recordDelivery and
|
|
222
|
+
// the exact global-receipt/source acknowledgement, but before the G44
|
|
223
|
+
// completeness gate. It writes only unsafe-window rows and kicks; it never
|
|
224
|
+
// advances a safe checkpoint. Ordinary views remain below the gate.
|
|
225
|
+
const unsafeViewBatch = await applyViewHandlers(unsafeViews, message, storedEvent, arrivedAt, source, options.durableHopObserver);
|
|
226
|
+
const viewResults = [...unsafeViewBatch.results];
|
|
227
|
+
failures.push(...unsafeViewBatch.failures);
|
|
228
|
+
if (options.beforeViews !== undefined) {
|
|
229
|
+
try {
|
|
230
|
+
await options.beforeViews({ message, event: storedEvent, arrivedAt, source });
|
|
231
|
+
}
|
|
232
|
+
catch (error) {
|
|
233
|
+
failures.push({ phase: "completeness", class: "retryable-transient", error: errorText(error) });
|
|
234
|
+
return result(source, message, "stored", arrivedAt, false, viewResults, failures, options.correlationId, startedAt);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
// Normative step 3: detector only for a stored event.
|
|
238
|
+
let detectorApplied = false;
|
|
239
|
+
observePostAdmission(options.durableHopObserver, message, source, {
|
|
240
|
+
stage: "detector",
|
|
241
|
+
boundary: "start",
|
|
242
|
+
outcome: "started",
|
|
243
|
+
});
|
|
244
|
+
try {
|
|
245
|
+
const lagBound = await store.currentLagBound(message.serviceId, arrivedAt);
|
|
246
|
+
const detector = new InconsistencyDetector(store, new BindingExclusionLedgerClient(env.REPAIR_EXCLUSION_LOOKUP));
|
|
247
|
+
await detector.observe(message, arrivedAt, lagBound);
|
|
248
|
+
detectorApplied = true;
|
|
249
|
+
observePostAdmission(options.durableHopObserver, message, source, {
|
|
250
|
+
stage: "detector",
|
|
251
|
+
boundary: "end",
|
|
252
|
+
outcome: "applied",
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
catch (error) {
|
|
256
|
+
observePostAdmission(options.durableHopObserver, message, source, {
|
|
257
|
+
stage: "detector",
|
|
258
|
+
boundary: "end",
|
|
259
|
+
outcome: "error",
|
|
260
|
+
});
|
|
261
|
+
try {
|
|
262
|
+
await options.onDetectorFailure?.({ message, arrivedAt, source, error });
|
|
263
|
+
}
|
|
264
|
+
catch (healthError) {
|
|
265
|
+
failures.push({
|
|
266
|
+
phase: "detector",
|
|
267
|
+
class: "retryable-transient",
|
|
268
|
+
error: `detector_health_record_failed:${errorText(healthError)}`,
|
|
269
|
+
});
|
|
270
|
+
}
|
|
271
|
+
failures.push({ phase: "detector", class: "retryable-transient", error: errorText(error) });
|
|
272
|
+
// A detector failure makes global completeness unknown. Do not apply a
|
|
273
|
+
// view after that failure; an earlier version accumulated the error and
|
|
274
|
+
// then silently continued into views.apply.
|
|
275
|
+
return result(source, message, "stored", arrivedAt, false, [], failures, options.correlationId, startedAt);
|
|
276
|
+
}
|
|
277
|
+
const views = gatedViews;
|
|
278
|
+
const gatedViewBatch = await applyViewHandlers(views, message, storedEvent, arrivedAt, source, options.durableHopObserver);
|
|
279
|
+
viewResults.push(...gatedViewBatch.results);
|
|
280
|
+
failures.push(...gatedViewBatch.failures);
|
|
281
|
+
// Compatibility callers still receive their stored-only callback, but it
|
|
282
|
+
// is after all explicit view branches and before the drain trigger.
|
|
283
|
+
if (options.onStored !== undefined) {
|
|
284
|
+
try {
|
|
285
|
+
await options.onStored({ message, event: storedEvent, arrivedAt, source });
|
|
286
|
+
}
|
|
287
|
+
catch (error) {
|
|
288
|
+
failures.push({ phase: "view", class: "retryable-transient", viewId: "legacy-onStored", error: errorText(error) });
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
const preliminary = result(source, message, "stored", arrivedAt, detectorApplied, viewResults, failures, options.correlationId, startedAt);
|
|
292
|
+
// Normative step 5: the trigger is after all view branches. The trigger is
|
|
293
|
+
// not itself a kick-target mutation; that mutation belongs to each view's
|
|
294
|
+
// single MV-D1 apply batch.
|
|
295
|
+
if (options.afterDelivery !== undefined && failures.every((failure) => failure.class === "duplicate-race")) {
|
|
296
|
+
try {
|
|
297
|
+
await options.afterDelivery({ message, event: storedEvent, arrivedAt, source, result: preliminary });
|
|
298
|
+
}
|
|
299
|
+
catch (error) {
|
|
300
|
+
failures.push({ phase: "drain", class: "retryable-transient", error: errorText(error) });
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
return result(source, message, "stored", arrivedAt, detectorApplied, viewResults, failures, options.correlationId, startedAt);
|
|
304
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import type { DownstreamOutboxMessage } from "./types";
|
|
2
|
+
export type DeliveryClass = "immediate-preferred" | "queued";
|
|
3
|
+
export type DirectDoorbellDegradation = "fail-fast" | "queued-degraded";
|
|
4
|
+
export type DirectDoorbellReceiverMode = "separate" | "self";
|
|
5
|
+
export type DirectDoorbellFailureKind = "non-2xx" | "throw" | "timeout" | "cancel";
|
|
6
|
+
/** Structural RPC surface used by a non-public Worker service binding. */
|
|
7
|
+
export interface DownstreamDoorbellBinding {
|
|
8
|
+
deliver(message: DownstreamOutboxMessage): Promise<unknown>;
|
|
9
|
+
}
|
|
10
|
+
export interface DirectDoorbellDeploymentConfig {
|
|
11
|
+
readonly deliveryClass: DeliveryClass;
|
|
12
|
+
readonly domainViewDeliveryClasses?: Readonly<Record<string, DeliveryClass>>;
|
|
13
|
+
readonly enabled: boolean;
|
|
14
|
+
readonly allowedViews: readonly string[];
|
|
15
|
+
readonly maxServiceBindingInvocations: number;
|
|
16
|
+
readonly degradation: DirectDoorbellDegradation;
|
|
17
|
+
readonly receiverMode: DirectDoorbellReceiverMode;
|
|
18
|
+
readonly selfBindingPreflightProof: boolean;
|
|
19
|
+
}
|
|
20
|
+
export interface DirectDoorbellPreflightResult {
|
|
21
|
+
readonly status: "ready" | "queued-degraded" | "fail-fast" | "disabled";
|
|
22
|
+
readonly reason: string;
|
|
23
|
+
readonly estimatedInvocations: number;
|
|
24
|
+
readonly topologyInvocations: number;
|
|
25
|
+
readonly allowedViews: readonly string[];
|
|
26
|
+
}
|
|
27
|
+
export declare const MAX_SERVICE_BINDING_INVOCATIONS_PER_REQUEST = 32;
|
|
28
|
+
export declare function classifyDirectDoorbellFailure(error: unknown): DirectDoorbellFailureKind;
|
|
29
|
+
/** Resolve the domain-owned half of the two-layer opt-in. */
|
|
30
|
+
export declare function readDomainDeliveryClass(env: Record<string, unknown>, fallback?: DeliveryClass): DeliveryClass;
|
|
31
|
+
/** Resolve the deployment half of the two-layer opt-in without touching a V1 body. */
|
|
32
|
+
export declare function readDirectDoorbellConfig(env: Record<string, unknown>, domainDeliveryClass?: DeliveryClass, domainViewDeliveryClasses?: Readonly<Record<string, DeliveryClass>>): DirectDoorbellDeploymentConfig;
|
|
33
|
+
/**
|
|
34
|
+
* Deployment preflight is intentionally pure so CI can exercise it without a
|
|
35
|
+
* live Worker. The conservative topology count is retained even when the
|
|
36
|
+
* default separate receiver reduces actual per-request recursion risk.
|
|
37
|
+
*/
|
|
38
|
+
export declare function preflightDirectDoorbell(config: DirectDoorbellDeploymentConfig, configuredImmediateViewCount?: number): DirectDoorbellPreflightResult;
|
|
39
|
+
export declare function selectDirectDoorbellViews<T extends {
|
|
40
|
+
readonly id: string;
|
|
41
|
+
}>(views: readonly T[], config: DirectDoorbellDeploymentConfig): readonly T[];
|
|
42
|
+
/** Stable bytes used by local and deployed byte-equality oracles. */
|
|
43
|
+
export declare function downstreamEnvelopeBytes(message: DownstreamOutboxMessage): string;
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
export const MAX_SERVICE_BINDING_INVOCATIONS_PER_REQUEST = 32;
|
|
2
|
+
export function classifyDirectDoorbellFailure(error) {
|
|
3
|
+
const structured = typeof error === "object" && error !== null
|
|
4
|
+
? error
|
|
5
|
+
: {};
|
|
6
|
+
const status = typeof structured.status === "number"
|
|
7
|
+
? structured.status
|
|
8
|
+
: typeof structured.statusCode === "number" ? structured.statusCode : undefined;
|
|
9
|
+
if (status !== undefined && (status < 200 || status >= 300))
|
|
10
|
+
return "non-2xx";
|
|
11
|
+
const value = String(error).toLowerCase();
|
|
12
|
+
const code = `${String(structured.code ?? "")} ${String(structured.name ?? "")}`.toLowerCase();
|
|
13
|
+
if (value.includes("timeout") || value.includes("deadline") || code.includes("timeout"))
|
|
14
|
+
return "timeout";
|
|
15
|
+
if (value.includes("cancel") || value.includes("abort") || code.includes("abort"))
|
|
16
|
+
return "cancel";
|
|
17
|
+
if (value.includes("non-2xx") || value.includes("http 4") || value.includes("http 5") || value.includes("status 4") || value.includes("status 5"))
|
|
18
|
+
return "non-2xx";
|
|
19
|
+
return "throw";
|
|
20
|
+
}
|
|
21
|
+
function envString(env, key) {
|
|
22
|
+
const value = env[key];
|
|
23
|
+
return typeof value === "string" ? value : undefined;
|
|
24
|
+
}
|
|
25
|
+
function parseBoolean(value, fallback) {
|
|
26
|
+
if (value === undefined)
|
|
27
|
+
return fallback;
|
|
28
|
+
if (value === "true")
|
|
29
|
+
return true;
|
|
30
|
+
if (value === "false")
|
|
31
|
+
return false;
|
|
32
|
+
throw new Error(`${value} is not a boolean deployment setting`);
|
|
33
|
+
}
|
|
34
|
+
function parsePositiveInteger(value, fallback, name) {
|
|
35
|
+
if (value === undefined || value.length === 0)
|
|
36
|
+
return fallback;
|
|
37
|
+
const parsed = Number(value);
|
|
38
|
+
if (!Number.isSafeInteger(parsed) || parsed < 1)
|
|
39
|
+
throw new Error(`${name} must be a positive safe integer`);
|
|
40
|
+
return parsed;
|
|
41
|
+
}
|
|
42
|
+
function parseAllowedViews(value) {
|
|
43
|
+
if (value === undefined || value.trim().length === 0)
|
|
44
|
+
return [];
|
|
45
|
+
return [...new Set(value.split(",").map((entry) => entry.trim()).filter((entry) => entry.length > 0))];
|
|
46
|
+
}
|
|
47
|
+
function parseDeliveryClass(value, fallback) {
|
|
48
|
+
if (value === undefined || value.length === 0)
|
|
49
|
+
return fallback;
|
|
50
|
+
if (value === "queued" || value === "immediate-preferred")
|
|
51
|
+
return value;
|
|
52
|
+
throw new Error("DOMAIN_DELIVERY_CLASS must be queued or immediate-preferred");
|
|
53
|
+
}
|
|
54
|
+
/** Resolve the domain-owned half of the two-layer opt-in. */
|
|
55
|
+
export function readDomainDeliveryClass(env, fallback = "queued") {
|
|
56
|
+
return parseDeliveryClass(envString(env, "DOMAIN_DELIVERY_CLASS"), fallback);
|
|
57
|
+
}
|
|
58
|
+
/** Resolve the deployment half of the two-layer opt-in without touching a V1 body. */
|
|
59
|
+
export function readDirectDoorbellConfig(env, domainDeliveryClass, domainViewDeliveryClasses) {
|
|
60
|
+
// The deployment half starts at DIRECT_DOORBELL. A deployment variable must
|
|
61
|
+
// not silently override the domain-owned delivery class.
|
|
62
|
+
const deliveryClass = domainDeliveryClass ?? readDomainDeliveryClass(env);
|
|
63
|
+
const receiverModeValue = envString(env, "DIRECT_DOORBELL_RECEIVER_MODE") ?? "separate";
|
|
64
|
+
if (receiverModeValue !== "separate" && receiverModeValue !== "self") {
|
|
65
|
+
throw new Error("DIRECT_DOORBELL_RECEIVER_MODE must be separate or self");
|
|
66
|
+
}
|
|
67
|
+
const degradationValue = envString(env, "DIRECT_DOORBELL_DEGRADATION") ?? "fail-fast";
|
|
68
|
+
if (degradationValue !== "fail-fast" && degradationValue !== "queued-degraded") {
|
|
69
|
+
throw new Error("DIRECT_DOORBELL_DEGRADATION must be fail-fast or queued-degraded");
|
|
70
|
+
}
|
|
71
|
+
return {
|
|
72
|
+
deliveryClass,
|
|
73
|
+
...(domainViewDeliveryClasses === undefined ? {} : { domainViewDeliveryClasses }),
|
|
74
|
+
enabled: parseBoolean(envString(env, "DIRECT_DOORBELL"), false),
|
|
75
|
+
allowedViews: parseAllowedViews(envString(env, "DIRECT_DOORBELL_ALLOWED_VIEWS")),
|
|
76
|
+
maxServiceBindingInvocations: parsePositiveInteger(envString(env, "DIRECT_DOORBELL_MAX_INVOCATIONS"), MAX_SERVICE_BINDING_INVOCATIONS_PER_REQUEST, "DIRECT_DOORBELL_MAX_INVOCATIONS"),
|
|
77
|
+
degradation: degradationValue,
|
|
78
|
+
receiverMode: receiverModeValue,
|
|
79
|
+
selfBindingPreflightProof: parseBoolean(envString(env, "DIRECT_DOORBELL_SELF_BINDING_PROOF"), false),
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Deployment preflight is intentionally pure so CI can exercise it without a
|
|
84
|
+
* live Worker. The conservative topology count is retained even when the
|
|
85
|
+
* default separate receiver reduces actual per-request recursion risk.
|
|
86
|
+
*/
|
|
87
|
+
export function preflightDirectDoorbell(config, configuredImmediateViewCount = config.allowedViews.length) {
|
|
88
|
+
const topologyInvocations = Math.max(1, configuredImmediateViewCount + 1);
|
|
89
|
+
const estimatedInvocations = topologyInvocations;
|
|
90
|
+
const degrade = (reason) => ({
|
|
91
|
+
status: config.degradation === "queued-degraded" ? "queued-degraded" : "fail-fast",
|
|
92
|
+
reason,
|
|
93
|
+
estimatedInvocations,
|
|
94
|
+
topologyInvocations,
|
|
95
|
+
allowedViews: config.allowedViews,
|
|
96
|
+
});
|
|
97
|
+
if (config.deliveryClass === "queued") {
|
|
98
|
+
return {
|
|
99
|
+
status: "disabled",
|
|
100
|
+
reason: "domain_delivery_class_queued",
|
|
101
|
+
estimatedInvocations,
|
|
102
|
+
topologyInvocations,
|
|
103
|
+
allowedViews: config.allowedViews,
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
if (!config.enabled)
|
|
107
|
+
return degrade("deployment_direct_doorbell_disabled");
|
|
108
|
+
if (config.allowedViews.length === 0)
|
|
109
|
+
return degrade("immediate_preferred_requires_allowed_views");
|
|
110
|
+
if (config.maxServiceBindingInvocations > MAX_SERVICE_BINDING_INVOCATIONS_PER_REQUEST) {
|
|
111
|
+
return degrade("configured_service_binding_budget_exceeds_platform_limit");
|
|
112
|
+
}
|
|
113
|
+
if (topologyInvocations > config.maxServiceBindingInvocations || topologyInvocations > MAX_SERVICE_BINDING_INVOCATIONS_PER_REQUEST) {
|
|
114
|
+
return degrade("configured_immediate_view_topology_exceeds_service_binding_budget");
|
|
115
|
+
}
|
|
116
|
+
if (config.receiverMode === "self" && !config.selfBindingPreflightProof) {
|
|
117
|
+
return degrade("self_binding_requires_recursion_and_route_privacy_preflight_proof");
|
|
118
|
+
}
|
|
119
|
+
return {
|
|
120
|
+
status: "ready",
|
|
121
|
+
reason: "direct_doorbell_capability_ready",
|
|
122
|
+
estimatedInvocations,
|
|
123
|
+
topologyInvocations,
|
|
124
|
+
allowedViews: config.allowedViews,
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
export function selectDirectDoorbellViews(views, config) {
|
|
128
|
+
if (config.deliveryClass !== "immediate-preferred" || !config.enabled)
|
|
129
|
+
return [];
|
|
130
|
+
const allowed = new Set(config.allowedViews);
|
|
131
|
+
return views.filter((view) => allowed.has(view.id) &&
|
|
132
|
+
(config.domainViewDeliveryClasses?.[view.id] ?? config.deliveryClass) === "immediate-preferred");
|
|
133
|
+
}
|
|
134
|
+
/** Stable bytes used by local and deployed byte-equality oracles. */
|
|
135
|
+
export function downstreamEnvelopeBytes(message) {
|
|
136
|
+
return JSON.stringify(message);
|
|
137
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { type DeliveryCoreOptions, type DeliveryCoreResult } from "./DeliveryCore";
|
|
2
|
+
import { type DownstreamOutboxMessage } from "./types";
|
|
3
|
+
import { type ServiceIdentityProvider } from "../service/ServiceIdentityProvider";
|
|
4
|
+
export interface DownstreamAdapterEnv {
|
|
5
|
+
POSTGRES_URL?: string;
|
|
6
|
+
HYPERDRIVE?: Hyperdrive;
|
|
7
|
+
/** Optional explicit D1 provider binding; Postgres remains the default. */
|
|
8
|
+
D1?: D1Database;
|
|
9
|
+
REPAIR_EXCLUSION_LOOKUP?: Fetcher;
|
|
10
|
+
BOOTSTRAP?: DurableObjectNamespace;
|
|
11
|
+
/** Present on the primary/receiver runtime so G44 can acknowledge source. */
|
|
12
|
+
TAG?: DurableObjectNamespace;
|
|
13
|
+
/** A receiver service-binding entrypoint must never silently skip admission. */
|
|
14
|
+
G38_DOORBELL_DELIVERY_ROLE?: string;
|
|
15
|
+
SDT_SERVICE_ID?: string;
|
|
16
|
+
}
|
|
17
|
+
export interface AfterStoredQueueDeliveryInput {
|
|
18
|
+
readonly message: DownstreamOutboxMessage;
|
|
19
|
+
readonly result: DeliveryCoreResult;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Queue-only hooks run after the shared core has durably recorded the event.
|
|
23
|
+
* They are deliberately separate from DeliveryCore.afterDelivery: that hook
|
|
24
|
+
* is a post-view hook and is not reached when the G44 gate is BLOCK/UNSETTLED.
|
|
25
|
+
*/
|
|
26
|
+
export interface DownstreamAdapterOptions extends DeliveryCoreOptions {
|
|
27
|
+
/**
|
|
28
|
+
* Notification only. The Queue handler never awaits this callback: the
|
|
29
|
+
* callback must register any work with the active ExecutionContext.
|
|
30
|
+
*/
|
|
31
|
+
readonly afterStoredQueueDelivery?: (input: AfterStoredQueueDeliveryInput) => void;
|
|
32
|
+
/** Invocation owner for bounded G69 diagnostic retention. */
|
|
33
|
+
readonly g69AdmissionAttemptWaitUntil?: (promise: Promise<void>) => void;
|
|
34
|
+
}
|
|
35
|
+
export type AdapterOptions = DownstreamAdapterOptions;
|
|
36
|
+
/**
|
|
37
|
+
* Processes one direct delivery through the shared core. The caller receives a
|
|
38
|
+
* typed result; this function never acknowledges, retries, or marks an
|
|
39
|
+
* outbox row.
|
|
40
|
+
*/
|
|
41
|
+
export declare function processDownstreamDoorbell(message: unknown, env: DownstreamAdapterEnv, options?: AdapterOptions): Promise<DeliveryCoreResult>;
|
|
42
|
+
/** Processes one Queue delivery without exposing Queue state to the core. */
|
|
43
|
+
export declare function processDownstreamDelivery(message: unknown, env: DownstreamAdapterEnv, options?: AdapterOptions): Promise<void>;
|
|
44
|
+
/**
|
|
45
|
+
* Queue policy is deliberately limited to the wrapper: the shared core
|
|
46
|
+
* returns a disposition, and this is the only place that calls ack/retry.
|
|
47
|
+
*/
|
|
48
|
+
export declare function handleDownstreamQueue(batch: MessageBatch<unknown>, env: DownstreamAdapterEnv, options?: AdapterOptions): Promise<void>;
|
|
49
|
+
/** A scheduled/operator invocation can evaluate a stable pending record without new traffic. */
|
|
50
|
+
export declare function stabilizeDownstream(env: DownstreamAdapterEnv, options?: AdapterOptions, serviceId?: string, serviceIdentityProvider?: ServiceIdentityProvider): Promise<void>;
|
|
51
|
+
export { processDeliveryCore } from "./DeliveryCore";
|
|
52
|
+
export type { DeliveryCoreEnvironment, DeliveryCoreFailure, DeliveryCoreOptions, DeliveryCoreResult, DeliverySource, DeliveryViewApplyResult, DeliveryViewFailureClass, DeliveryViewHandler, DeliveryViewInput, DeliveryViewResult, } from "./DeliveryCore";
|