@effect-agent/platform-cloudflare 0.0.1-beta.6 → 0.1.0-beta.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.mts +24 -57
- package/dist/index.mjs +63 -368
- package/dist/index.mjs.map +1 -1
- package/package.json +20 -19
- package/src/config.ts +0 -3
- package/src/conversation-object.ts +139 -218
- package/src/index.ts +0 -1
- package/src/layers.ts +5 -14
- package/src/telemetry-internal.ts +0 -432
- package/src/telemetry.ts +0 -37
|
@@ -1,432 +0,0 @@
|
|
|
1
|
-
import { Cause, Duration, Effect, Exit } from "effect";
|
|
2
|
-
|
|
3
|
-
import { CloudflareRuntimeTelemetry, type CloudflareTelemetryExportError } from "./telemetry.ts";
|
|
4
|
-
|
|
5
|
-
/** Bounded typed control signal used only while a native entrypoint span is active. */
|
|
6
|
-
class CloudflareNativeSpanFailure extends Error {
|
|
7
|
-
override readonly name = "ConversationObjectDeliveryFailed";
|
|
8
|
-
|
|
9
|
-
constructor() {
|
|
10
|
-
super("Cloudflare Conversation Object delivery failed");
|
|
11
|
-
}
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
const stripCloudflareNativeSpanFailures = <E>(
|
|
15
|
-
cause: Cause.Cause<E | CloudflareNativeSpanFailure>,
|
|
16
|
-
): { readonly found: boolean; readonly residual: Cause.Cause<E> } => {
|
|
17
|
-
const found = cause.reasons.some(
|
|
18
|
-
(reason) => Cause.isFailReason(reason) && reason.error instanceof CloudflareNativeSpanFailure,
|
|
19
|
-
);
|
|
20
|
-
if (!found) {
|
|
21
|
-
// Effect v4 Cause is flat. Preserve the exact marker-free Cause object and narrow only its
|
|
22
|
-
// covariant error parameter after the complete Reason scan.
|
|
23
|
-
return { found: false, residual: cause as Cause.Cause<E> };
|
|
24
|
-
}
|
|
25
|
-
const reasons = cause.reasons.filter((reason): reason is Cause.Reason<E> => {
|
|
26
|
-
if (Cause.isFailReason(reason) && reason.error instanceof CloudflareNativeSpanFailure) {
|
|
27
|
-
return false;
|
|
28
|
-
}
|
|
29
|
-
return true;
|
|
30
|
-
});
|
|
31
|
-
return { found: true, residual: Cause.fromReasons(reasons) };
|
|
32
|
-
};
|
|
33
|
-
|
|
34
|
-
/**
|
|
35
|
-
* @internal Replace an entrypoint Cause with a bounded typed marker inside a span, then remove
|
|
36
|
-
* only that marker and restore the exact invocation-local Cause plus any newly composed reasons.
|
|
37
|
-
*/
|
|
38
|
-
export const withCloudflareNativeSpanFailure = <A, E, R>(
|
|
39
|
-
effect: Effect.Effect<A, E, R>,
|
|
40
|
-
trace: (
|
|
41
|
-
masked: Effect.Effect<A, E | CloudflareNativeSpanFailure, R>,
|
|
42
|
-
) => Effect.Effect<A, E | CloudflareNativeSpanFailure, R>,
|
|
43
|
-
): Effect.Effect<A, E, R> =>
|
|
44
|
-
Effect.suspend(() => {
|
|
45
|
-
let original: Cause.Cause<E> | undefined;
|
|
46
|
-
return trace(
|
|
47
|
-
effect.pipe(
|
|
48
|
-
Effect.catchCause((cause) => {
|
|
49
|
-
original = cause;
|
|
50
|
-
return Effect.fail(new CloudflareNativeSpanFailure());
|
|
51
|
-
}),
|
|
52
|
-
),
|
|
53
|
-
).pipe(
|
|
54
|
-
Effect.catchCause((cause) => {
|
|
55
|
-
const { found, residual } = stripCloudflareNativeSpanFailures(cause);
|
|
56
|
-
if (!found) return Effect.failCause(residual);
|
|
57
|
-
const restored = original === undefined ? residual : Cause.combine(original, residual);
|
|
58
|
-
return Effect.failCause(restored);
|
|
59
|
-
}),
|
|
60
|
-
);
|
|
61
|
-
});
|
|
62
|
-
|
|
63
|
-
const logCloudflareTelemetryCause = (
|
|
64
|
-
cause: Cause.Cause<CloudflareTelemetryExportError | Cause.TimeoutError>,
|
|
65
|
-
): Effect.Effect<void> => {
|
|
66
|
-
type FailureKind = "exporter" | "timeout" | "defect" | "interrupted";
|
|
67
|
-
const kinds = new Set<FailureKind>();
|
|
68
|
-
for (const reason of cause.reasons) {
|
|
69
|
-
if (Cause.isFailReason(reason)) {
|
|
70
|
-
kinds.add(Cause.isTimeoutError(reason.error) ? "timeout" : "exporter");
|
|
71
|
-
} else if (Cause.isDieReason(reason)) {
|
|
72
|
-
kinds.add("defect");
|
|
73
|
-
} else {
|
|
74
|
-
kinds.add("interrupted");
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
return Effect.forEach(
|
|
78
|
-
kinds,
|
|
79
|
-
(kind) => {
|
|
80
|
-
const diagnostic =
|
|
81
|
-
kind === "defect"
|
|
82
|
-
? Effect.logError("Cloudflare telemetry flush defect")
|
|
83
|
-
: Effect.logWarning(
|
|
84
|
-
kind === "exporter"
|
|
85
|
-
? "Cloudflare telemetry exporter failed"
|
|
86
|
-
: kind === "timeout"
|
|
87
|
-
? "Cloudflare telemetry flush exceeded its time budget"
|
|
88
|
-
: "Cloudflare telemetry flush interrupted",
|
|
89
|
-
);
|
|
90
|
-
// The typed error retains its foreign Cause for explicit host-controlled inspection. Never
|
|
91
|
-
// pass untrusted exporter, defect, interrupt, or platform values to the configured Logger.
|
|
92
|
-
return diagnostic.pipe(
|
|
93
|
-
Effect.annotateLogs({
|
|
94
|
-
"effect_agent.cloudflare.telemetry.failure_kind": kind,
|
|
95
|
-
}),
|
|
96
|
-
);
|
|
97
|
-
},
|
|
98
|
-
{ discard: true },
|
|
99
|
-
);
|
|
100
|
-
};
|
|
101
|
-
|
|
102
|
-
/**
|
|
103
|
-
* @internal Cooperative background flush budget. Interruptible exporters stop at the configured
|
|
104
|
-
* timeout; uninterruptible exporters remain bounded by Cloudflare's waitUntil lifecycle instead
|
|
105
|
-
* of holding the native delivery open.
|
|
106
|
-
*/
|
|
107
|
-
export const flushCloudflareRuntimeTelemetry = (
|
|
108
|
-
timeoutMillis: number,
|
|
109
|
-
): Effect.Effect<
|
|
110
|
-
void,
|
|
111
|
-
CloudflareTelemetryExportError | Cause.TimeoutError,
|
|
112
|
-
CloudflareRuntimeTelemetry
|
|
113
|
-
> =>
|
|
114
|
-
Effect.flatMap(CloudflareRuntimeTelemetry, ({ flush }) => flush).pipe(
|
|
115
|
-
Effect.interruptible,
|
|
116
|
-
Effect.timeout(Duration.millis(timeoutMillis)),
|
|
117
|
-
Effect.onExit((exit) =>
|
|
118
|
-
Exit.isFailure(exit) ? logCloudflareTelemetryCause(exit.cause) : Effect.void,
|
|
119
|
-
),
|
|
120
|
-
Effect.asVoid,
|
|
121
|
-
);
|
|
122
|
-
|
|
123
|
-
/** @internal Convert the shared exporter result into the always-fulfilled waitUntil bridge. */
|
|
124
|
-
export const fulfillCloudflareTelemetryBackground = (result: Promise<unknown>): Promise<void> =>
|
|
125
|
-
result.then(
|
|
126
|
-
() => undefined,
|
|
127
|
-
() => undefined,
|
|
128
|
-
);
|
|
129
|
-
|
|
130
|
-
export interface CloudflareTelemetryFlushReservation {
|
|
131
|
-
/** Shared, always-fulfilled background Promise registered by this batch's first owner only. */
|
|
132
|
-
readonly background: Promise<void>;
|
|
133
|
-
/** Shared raw cycle result used by deterministic coordinator tests and diagnostics. */
|
|
134
|
-
readonly cycle: Promise<void>;
|
|
135
|
-
/** True only for the first delivery reserved into this bounded batch. */
|
|
136
|
-
readonly owner: boolean;
|
|
137
|
-
/** True when this delivery was coalesced without retaining its settlement Promise. */
|
|
138
|
-
readonly dropped: boolean;
|
|
139
|
-
/** Cancel this batch when its synchronous waitUntil registration fails. */
|
|
140
|
-
readonly cancel: () => void;
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
/** @internal One active exporter attempt plus bounded trailing/queued batches. */
|
|
144
|
-
export interface CloudflareTelemetryFlushCoordinator {
|
|
145
|
-
readonly reserve: <A>(delivery: Promise<A>) => CloudflareTelemetryFlushReservation;
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
type FlushCyclePhase = "pending" | "first" | "trailing" | "settling";
|
|
149
|
-
type FlushBatchPhase = "pending" | "waiting" | "running" | "settled" | "cancelled";
|
|
150
|
-
|
|
151
|
-
/** @internal Hard cap on delivery Promises retained by one exporter batch. */
|
|
152
|
-
export const MAX_CLOUDFLARE_TELEMETRY_BATCH_DELIVERIES = 64;
|
|
153
|
-
|
|
154
|
-
interface FlushBatch {
|
|
155
|
-
readonly result: Promise<void>;
|
|
156
|
-
readonly background: Promise<void>;
|
|
157
|
-
readonly resolveResult: () => void;
|
|
158
|
-
readonly rejectResult: (cause: unknown) => void;
|
|
159
|
-
readonly ready: Promise<void>;
|
|
160
|
-
readonly resolveReady: () => void;
|
|
161
|
-
phase: FlushBatchPhase;
|
|
162
|
-
reservations: number;
|
|
163
|
-
unsettledDeliveries: number;
|
|
164
|
-
dropDiagnosed: boolean;
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
interface FlushCycle {
|
|
168
|
-
readonly promise: Promise<void>;
|
|
169
|
-
readonly resolve: () => void;
|
|
170
|
-
readonly reject: (cause: unknown) => void;
|
|
171
|
-
readonly first: FlushBatch;
|
|
172
|
-
phase: FlushCyclePhase;
|
|
173
|
-
trailing: FlushBatch | undefined;
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
const makeFlushBatch = (): FlushBatch => {
|
|
177
|
-
let resolveResult = (): void => undefined;
|
|
178
|
-
let rejectResult = (_cause: unknown): void => undefined;
|
|
179
|
-
const result = new Promise<void>((resolve, reject) => {
|
|
180
|
-
resolveResult = resolve;
|
|
181
|
-
rejectResult = reject;
|
|
182
|
-
});
|
|
183
|
-
let resolveReady = (): void => undefined;
|
|
184
|
-
const ready = new Promise<void>((resolve) => {
|
|
185
|
-
resolveReady = resolve;
|
|
186
|
-
});
|
|
187
|
-
return {
|
|
188
|
-
result,
|
|
189
|
-
background: fulfillCloudflareTelemetryBackground(result),
|
|
190
|
-
resolveResult,
|
|
191
|
-
rejectResult,
|
|
192
|
-
ready,
|
|
193
|
-
resolveReady,
|
|
194
|
-
phase: "pending",
|
|
195
|
-
reservations: 0,
|
|
196
|
-
unsettledDeliveries: 0,
|
|
197
|
-
dropDiagnosed: false,
|
|
198
|
-
};
|
|
199
|
-
};
|
|
200
|
-
|
|
201
|
-
const makeFlushCycle = (): FlushCycle => {
|
|
202
|
-
let resolveCycle = (): void => undefined;
|
|
203
|
-
let rejectCycle = (_cause: unknown): void => undefined;
|
|
204
|
-
const promise = new Promise<void>((resolve, reject) => {
|
|
205
|
-
resolveCycle = resolve;
|
|
206
|
-
rejectCycle = reject;
|
|
207
|
-
});
|
|
208
|
-
// Runtime delivery does not consume the raw cycle result. Attach a bounded rejection observer
|
|
209
|
-
// immediately while retaining the original Promise for deterministic tests/host diagnostics.
|
|
210
|
-
void fulfillCloudflareTelemetryBackground(promise);
|
|
211
|
-
return {
|
|
212
|
-
promise,
|
|
213
|
-
resolve: resolveCycle,
|
|
214
|
-
reject: rejectCycle,
|
|
215
|
-
first: makeFlushBatch(),
|
|
216
|
-
phase: "pending",
|
|
217
|
-
trailing: undefined,
|
|
218
|
-
};
|
|
219
|
-
};
|
|
220
|
-
|
|
221
|
-
/**
|
|
222
|
-
* @internal Coordinate exporter attempts for one Conversation Object incarnation. Each cycle is
|
|
223
|
-
* capped at a first attempt plus one trailing attempt. Each batch is reserved synchronously and
|
|
224
|
-
* waits for every delivery assigned to it before exporting. Only the first owner of the pending,
|
|
225
|
-
* trailing, or queued batch registers that batch's shared background Promise, so a stalled
|
|
226
|
-
* exporter cannot accumulate per-delivery waitUntil registrations. Each batch retains at most
|
|
227
|
-
* `MAX_CLOUDFLARE_TELEMETRY_BATCH_DELIVERIES` settlement Promises; excess deliveries are lossy-
|
|
228
|
-
* coalesced into the already-requested export without settlement ownership and diagnosed once for
|
|
229
|
-
* that batch. Failures remain rejected by the raw cycle after every requested attempt runs; each
|
|
230
|
-
* shared waitUntil bridge always fulfills.
|
|
231
|
-
*/
|
|
232
|
-
export const makeCloudflareTelemetryFlushCoordinator = (
|
|
233
|
-
flush: () => Promise<void>,
|
|
234
|
-
options: {
|
|
235
|
-
/** @internal Synchronous test hook for the exact final-settlement boundary. */
|
|
236
|
-
readonly onSettling?: () => void;
|
|
237
|
-
/** Bounded synchronous diagnostic invoked once for each batch that reaches its hard cap. */
|
|
238
|
-
readonly onReservationDropped?: () => void;
|
|
239
|
-
} = {},
|
|
240
|
-
): CloudflareTelemetryFlushCoordinator => {
|
|
241
|
-
let active: FlushCycle | undefined;
|
|
242
|
-
let queued: FlushCycle | undefined;
|
|
243
|
-
|
|
244
|
-
const closeBatch = (batch: FlushBatch): void => {
|
|
245
|
-
if (batch.phase !== "pending") return;
|
|
246
|
-
batch.phase = "waiting";
|
|
247
|
-
if (batch.unsettledDeliveries === 0) batch.resolveReady();
|
|
248
|
-
};
|
|
249
|
-
|
|
250
|
-
const cancelBatch = (batch: FlushBatch): void => {
|
|
251
|
-
if (batch.phase === "settled" || batch.phase === "cancelled") return;
|
|
252
|
-
batch.phase = "cancelled";
|
|
253
|
-
batch.resolveReady();
|
|
254
|
-
batch.resolveResult();
|
|
255
|
-
};
|
|
256
|
-
|
|
257
|
-
const reserveDelivery = <A>(
|
|
258
|
-
batch: FlushBatch,
|
|
259
|
-
delivery: Promise<A>,
|
|
260
|
-
): { readonly owner: boolean; readonly dropped: boolean } => {
|
|
261
|
-
if (batch.reservations >= MAX_CLOUDFLARE_TELEMETRY_BATCH_DELIVERIES) {
|
|
262
|
-
if (!batch.dropDiagnosed) {
|
|
263
|
-
batch.dropDiagnosed = true;
|
|
264
|
-
try {
|
|
265
|
-
options.onReservationDropped?.();
|
|
266
|
-
} catch {
|
|
267
|
-
// A derivative diagnostic sink cannot change native delivery or exporter coordination.
|
|
268
|
-
}
|
|
269
|
-
}
|
|
270
|
-
return { owner: false, dropped: true };
|
|
271
|
-
}
|
|
272
|
-
const owner = batch.reservations === 0;
|
|
273
|
-
batch.reservations += 1;
|
|
274
|
-
batch.unsettledDeliveries += 1;
|
|
275
|
-
const settled = (): void => {
|
|
276
|
-
batch.unsettledDeliveries -= 1;
|
|
277
|
-
if (batch.phase === "waiting" && batch.unsettledDeliveries === 0) {
|
|
278
|
-
batch.resolveReady();
|
|
279
|
-
}
|
|
280
|
-
};
|
|
281
|
-
void delivery.then(settled, settled);
|
|
282
|
-
return { owner, dropped: false };
|
|
283
|
-
};
|
|
284
|
-
|
|
285
|
-
const runCycle = async (cycle: FlushCycle): Promise<void> => {
|
|
286
|
-
const failures: Array<unknown> = [];
|
|
287
|
-
|
|
288
|
-
const runAttempt = async (): Promise<void> => {
|
|
289
|
-
try {
|
|
290
|
-
await flush();
|
|
291
|
-
} catch (cause) {
|
|
292
|
-
// Each cycle is capped at two attempts, so retaining every cause is bounded.
|
|
293
|
-
failures.push(cause);
|
|
294
|
-
}
|
|
295
|
-
};
|
|
296
|
-
|
|
297
|
-
const runBatch = async (batch: FlushBatch): Promise<void> => {
|
|
298
|
-
closeBatch(batch);
|
|
299
|
-
await batch.ready;
|
|
300
|
-
if (batch.phase === "cancelled") return;
|
|
301
|
-
batch.phase = "running";
|
|
302
|
-
const failureStart = failures.length;
|
|
303
|
-
await runAttempt();
|
|
304
|
-
batch.phase = "settled";
|
|
305
|
-
if (failures.length === failureStart) {
|
|
306
|
-
batch.resolveResult();
|
|
307
|
-
} else {
|
|
308
|
-
batch.rejectResult(failures[failures.length - 1]);
|
|
309
|
-
}
|
|
310
|
-
};
|
|
311
|
-
|
|
312
|
-
try {
|
|
313
|
-
// Requests reserved before this first exporter microtask starts share the first batch.
|
|
314
|
-
cycle.phase = "first";
|
|
315
|
-
await runBatch(cycle.first);
|
|
316
|
-
const trailing = cycle.trailing;
|
|
317
|
-
if (trailing !== undefined) {
|
|
318
|
-
cycle.phase = "trailing";
|
|
319
|
-
await runBatch(trailing);
|
|
320
|
-
}
|
|
321
|
-
// Once no more attempts can join this capped cycle, later requests must route to the queued
|
|
322
|
-
// cycle. The synchronous hook lets tests re-enter `reserve` at this exact boundary without
|
|
323
|
-
// relying on Promise reaction ordering.
|
|
324
|
-
cycle.phase = "settling";
|
|
325
|
-
options.onSettling?.();
|
|
326
|
-
} finally {
|
|
327
|
-
// Advance in the same continuation that completes the capped cycle. A request queued on the
|
|
328
|
-
// just-completed exporter Promise therefore either joined the next cycle while this one was
|
|
329
|
-
// trailing or sees the promoted pending cycle; it can never attach to a settling old Promise.
|
|
330
|
-
const next = queued;
|
|
331
|
-
queued = undefined;
|
|
332
|
-
active = next;
|
|
333
|
-
if (next !== undefined) startCycle(next);
|
|
334
|
-
}
|
|
335
|
-
|
|
336
|
-
if (failures.length === 0) return;
|
|
337
|
-
if (failures.length === 1) throw failures[0];
|
|
338
|
-
throw new AggregateError(
|
|
339
|
-
failures,
|
|
340
|
-
`${failures.length} Cloudflare telemetry flush attempts failed`,
|
|
341
|
-
);
|
|
342
|
-
};
|
|
343
|
-
|
|
344
|
-
const startCycle = (cycle: FlushCycle): void => {
|
|
345
|
-
// Start in a microtask after active is assigned, including when flush throws synchronously.
|
|
346
|
-
void Promise.resolve()
|
|
347
|
-
.then(() => runCycle(cycle))
|
|
348
|
-
.then(cycle.resolve, cycle.reject);
|
|
349
|
-
};
|
|
350
|
-
|
|
351
|
-
const reserve = <A>(delivery: Promise<A>): CloudflareTelemetryFlushReservation => {
|
|
352
|
-
let cycle: FlushCycle;
|
|
353
|
-
let batch: FlushBatch;
|
|
354
|
-
let shouldStart = false;
|
|
355
|
-
|
|
356
|
-
if (active === undefined) {
|
|
357
|
-
cycle = makeFlushCycle();
|
|
358
|
-
active = cycle;
|
|
359
|
-
batch = cycle.first;
|
|
360
|
-
shouldStart = true;
|
|
361
|
-
} else if (active.phase === "pending" && active.first.phase !== "cancelled") {
|
|
362
|
-
cycle = active;
|
|
363
|
-
batch = active.first;
|
|
364
|
-
} else if (active.phase === "first") {
|
|
365
|
-
cycle = active;
|
|
366
|
-
batch = active.trailing ??= makeFlushBatch();
|
|
367
|
-
} else {
|
|
368
|
-
// The current cycle has spent its trailing attempt, entered settlement, or had its pending
|
|
369
|
-
// registration cancelled. Route later traffic to one pending next cycle.
|
|
370
|
-
if (queued === undefined) queued = makeFlushCycle();
|
|
371
|
-
cycle = queued;
|
|
372
|
-
batch = queued.first;
|
|
373
|
-
}
|
|
374
|
-
|
|
375
|
-
const { dropped, owner } = reserveDelivery(batch, delivery);
|
|
376
|
-
if (shouldStart) startCycle(cycle);
|
|
377
|
-
|
|
378
|
-
return {
|
|
379
|
-
background: batch.background,
|
|
380
|
-
cycle: cycle.promise,
|
|
381
|
-
owner,
|
|
382
|
-
dropped,
|
|
383
|
-
cancel: () => {
|
|
384
|
-
if (!owner) return;
|
|
385
|
-
cancelBatch(batch);
|
|
386
|
-
if (cycle.trailing === batch && cycle.phase === "first") {
|
|
387
|
-
cycle.trailing = undefined;
|
|
388
|
-
} else if (queued === cycle && active !== cycle) {
|
|
389
|
-
queued = undefined;
|
|
390
|
-
cycle.resolve();
|
|
391
|
-
}
|
|
392
|
-
},
|
|
393
|
-
};
|
|
394
|
-
};
|
|
395
|
-
|
|
396
|
-
return { reserve };
|
|
397
|
-
};
|
|
398
|
-
|
|
399
|
-
/**
|
|
400
|
-
* @internal Reserve telemetry before registration without allowing a synchronous platform failure
|
|
401
|
-
* to replace the already-running native delivery Promise. Only a batch owner calls waitUntil; its
|
|
402
|
-
* shared bridge is already always fulfilled, and registration failure cancels export for that
|
|
403
|
-
* unowned batch.
|
|
404
|
-
*/
|
|
405
|
-
export const registerCloudflareTelemetryAfterNativeSettlement = <A>(
|
|
406
|
-
waitUntil: (promise: Promise<void>) => void,
|
|
407
|
-
delivery: Promise<A>,
|
|
408
|
-
reserve: (delivery: Promise<A>) => CloudflareTelemetryFlushReservation,
|
|
409
|
-
diagnoseRegistrationFailure: (cause: unknown) => void,
|
|
410
|
-
): Promise<A> => {
|
|
411
|
-
const reservation = reserve(delivery);
|
|
412
|
-
if (!reservation.owner) return delivery;
|
|
413
|
-
try {
|
|
414
|
-
waitUntil(reservation.background);
|
|
415
|
-
} catch (cause) {
|
|
416
|
-
reservation.cancel();
|
|
417
|
-
try {
|
|
418
|
-
diagnoseRegistrationFailure(cause);
|
|
419
|
-
} catch {
|
|
420
|
-
// Even a broken diagnostic sink is derivative and cannot create an uncertain native outcome.
|
|
421
|
-
}
|
|
422
|
-
}
|
|
423
|
-
return delivery;
|
|
424
|
-
};
|
|
425
|
-
|
|
426
|
-
/** @internal Content-free diagnostic for a rejected Cloudflare waitUntil registration. */
|
|
427
|
-
export const logCloudflareWaitUntilRegistrationFailure = (_cause: unknown): Effect.Effect<void> =>
|
|
428
|
-
Effect.logError("Cloudflare telemetry waitUntil registration failed").pipe(
|
|
429
|
-
Effect.annotateLogs({
|
|
430
|
-
"effect_agent.cloudflare.telemetry.failure_kind": "wait_until_registration",
|
|
431
|
-
}),
|
|
432
|
-
);
|
package/src/telemetry.ts
DELETED
|
@@ -1,37 +0,0 @@
|
|
|
1
|
-
import { Context, Effect, Layer, Schema } from "effect";
|
|
2
|
-
|
|
3
|
-
/** A host exporter failed during one coalesced background export attempt. */
|
|
4
|
-
export class CloudflareTelemetryExportError extends Schema.TaggedError<CloudflareTelemetryExportError>()(
|
|
5
|
-
"CloudflareTelemetryExportError",
|
|
6
|
-
{
|
|
7
|
-
/** The original exporter failure, retained for host-side diagnostics. */
|
|
8
|
-
cause: Schema.Defect(),
|
|
9
|
-
},
|
|
10
|
-
) {}
|
|
11
|
-
|
|
12
|
-
/**
|
|
13
|
-
* Host-owned exporter lifecycle for one Cloudflare Conversation Object incarnation.
|
|
14
|
-
*
|
|
15
|
-
* The platform package creates measurements but deliberately does not select an exporter or
|
|
16
|
-
* vendor. Native entrypoint instrumentation consumes this service at the Worker composition edge;
|
|
17
|
-
* applications pass its provider as the second `makeConversationObjectClass` argument together
|
|
18
|
-
* with their Effect Logger, Tracer, and Metric layers. That provider may read `DurableObjectContext` or
|
|
19
|
-
* `ConversationObjectNamespace`, and its typed acquisition error remains visible in construction.
|
|
20
|
-
* `flush` must attempt to export every configured signal. After the native span closes, the
|
|
21
|
-
* Conversation Object reserves it into a shared post-settlement batch before `ctx.waitUntil` under
|
|
22
|
-
* a cooperative timeout. Only each pending/trailing/queued batch's first owner registers its
|
|
23
|
-
* shared Promise. Cycles remain capped at one first exporter attempt plus at most one trailing
|
|
24
|
-
* attempt, with one queued cycle while trailing runs; exporter completion never holds endpoint
|
|
25
|
-
* delivery open and exporter failure never replaces endpoint behavior.
|
|
26
|
-
*/
|
|
27
|
-
export class CloudflareRuntimeTelemetry extends Context.Service<
|
|
28
|
-
CloudflareRuntimeTelemetry,
|
|
29
|
-
{
|
|
30
|
-
readonly flush: Effect.Effect<void, CloudflareTelemetryExportError>;
|
|
31
|
-
}
|
|
32
|
-
>()("@effect-agent/platform-cloudflare/CloudflareRuntimeTelemetry") {
|
|
33
|
-
/** Content-free default selected by `makeConversationObjectClass` when no host Layer is passed. */
|
|
34
|
-
static readonly layerNoop = Layer.succeed(CloudflareRuntimeTelemetry)({
|
|
35
|
-
flush: Effect.void,
|
|
36
|
-
});
|
|
37
|
-
}
|