import { Temporal } from "@js-temporal/polyfill";
import "urlpattern-polyfill";
globalThis.addEventListener = () => {};
import { t as test } from "../testing-BQeTO4ss.mjs";
import { t as assertEquals } from "../assert_equals-Ew3jOFa3.mjs";
import "../std__assert-2v7gYiZp.mjs";
import { t as assertThrows } from "../assert_throws-BIL7gChy.mjs";
import { t as MemoryKvStore } from "../kv-OuT2IU64.mjs";
import { getLogger } from "@logtape/logtape";
import { SpanKind, SpanStatusCode, TraceFlags } from "@opentelemetry/api";
import { ExportResultCode } from "@opentelemetry/core";
//#region src/otel/exporter.ts
/**
* A SpanExporter that persists ActivityPub activity traces to a
* {@link KvStore}.  This enables distributed tracing across multiple
* nodes in a Fedify deployment.
*
* The exporter captures activity data from OpenTelemetry span events
* (`activitypub.activity.received` and `activitypub.activity.sent`)
* and stores them in the KvStore with trace context preserved.
*
* @example Basic usage with MemoryKvStore
* ```typescript ignore
* import { MemoryKvStore } from "@fedify/fedify";
* import { FedifySpanExporter } from "@fedify/fedify/otel";
* import {
*   BasicTracerProvider,
*   SimpleSpanProcessor,
* } from "@opentelemetry/sdk-trace-base";
*
* const kv = new MemoryKvStore();
* const exporter = new FedifySpanExporter(kv, {
*   ttl: Temporal.Duration.from({ hours: 1 }),
* });
*
* const provider = new BasicTracerProvider();
* provider.addSpanProcessor(new SimpleSpanProcessor(exporter));
* ```
*
* @example Querying stored traces
* ```typescript ignore
* import { MemoryKvStore } from "@fedify/fedify";
* import { FedifySpanExporter } from "@fedify/fedify/otel";
*
* const kv = new MemoryKvStore();
* const exporter = new FedifySpanExporter(kv);
* const traceId = "abc123";
*
* // Get all activities for a specific trace
* const activities = await exporter.getActivitiesByTraceId(traceId);
*
* // Get recent traces
* const recentTraces = await exporter.getRecentTraces({ limit: 100 });
* ```
*
* @since 1.10.0
*/
var FedifySpanExporter = class {
	#kv;
	#ttl;
	#keyPrefix;
	#useList;
	/**
	* Creates a new FedifySpanExporter.
	*
	* @param kv The KvStore to persist trace data to.  Must support either
	*           `list()` or `cas()` method.
	* @param options Configuration options.
	* @throws {Error} If the KvStore supports neither `list()` nor `cas()`.
	*/
	constructor(kv, options) {
		if (kv.list == null && kv.cas == null) throw new Error("KvStore must support either list() or cas() method to use FedifySpanExporter. Consider using a KvStore implementation that supports these operations, such as MemoryKvStore, RedisKvStore, PostgresKvStore, or SqliteKvStore.");
		this.#kv = kv;
		this.#ttl = options?.ttl;
		this.#keyPrefix = options?.keyPrefix ?? ["fedify", "traces"];
		this.#useList = kv.list != null;
	}
	/**
	* Exports spans to the KvStore.
	*
	* @param spans The spans to export.
	* @param resultCallback Callback to invoke with the export result.
	*/
	export(spans, resultCallback) {
		this.#exportAsync(spans).then(() => resultCallback({ code: ExportResultCode.SUCCESS })).catch((error) => {
			getLogger([
				"fedify",
				"otel",
				"exporter"
			]).error("Failed to export spans to KvStore: {error}", { error });
			resultCallback({ code: ExportResultCode.FAILED });
		});
	}
	async #exportAsync(spans) {
		const storeOperations = [];
		for (const span of spans) {
			const records = this.#extractRecords(span);
			for (const record of records) storeOperations.push(this.#storeRecord(record));
		}
		const rejected = (await Promise.allSettled(storeOperations)).filter((r) => r.status === "rejected");
		if (rejected.length > 0) throw new AggregateError(rejected.map((r) => r.reason), "Failed to store one or more trace activity records.");
	}
	#extractRecords(span) {
		const records = [];
		const spanContext = span.spanContext();
		const traceId = spanContext.traceId;
		const spanId = spanContext.spanId;
		const parentSpanId = span.parentSpanId;
		for (const event of span.events) if (event.name === "activitypub.activity.received") {
			const record = this.#extractInboundRecord(event, traceId, spanId, parentSpanId);
			if (record != null) records.push(record);
		} else if (event.name === "activitypub.activity.sent") {
			const record = this.#extractOutboundRecord(event, traceId, spanId, parentSpanId);
			if (record != null) records.push(record);
		}
		return records;
	}
	#extractInboundRecord(event, traceId, spanId, parentSpanId) {
		const attrs = event.attributes;
		if (attrs == null) return null;
		const activityJson = attrs["activitypub.activity.json"];
		if (typeof activityJson !== "string") return null;
		let activityType = "Unknown";
		let activityId;
		let actorId;
		try {
			const activity = JSON.parse(activityJson);
			activityType = activity.type ?? "Unknown";
			activityId = activity.id;
			if (typeof activity.actor === "string") actorId = activity.actor;
			else if (activity.actor != null && typeof activity.actor.id === "string") actorId = activity.actor.id;
		} catch {}
		const verified = attrs["activitypub.activity.verified"];
		const httpSigVerified = attrs["http_signatures.verified"];
		const httpSigKeyId = attrs["http_signatures.key_id"];
		const ldSigVerified = attrs["ld_signatures.verified"];
		let signatureDetails;
		if (typeof httpSigVerified === "boolean" || typeof ldSigVerified === "boolean") signatureDetails = {
			httpSignaturesVerified: httpSigVerified === true,
			httpSignaturesKeyId: typeof httpSigKeyId === "string" && httpSigKeyId !== "" ? httpSigKeyId : void 0,
			ldSignaturesVerified: ldSigVerified === true
		};
		return {
			traceId,
			spanId,
			parentSpanId,
			direction: "inbound",
			activityType,
			activityId,
			actorId,
			activityJson,
			verified: typeof verified === "boolean" ? verified : void 0,
			signatureDetails,
			timestamp: (/* @__PURE__ */ new Date(event.time[0] * 1e3 + event.time[1] / 1e6)).toISOString()
		};
	}
	#extractOutboundRecord(event, traceId, spanId, parentSpanId) {
		const attrs = event.attributes;
		if (attrs == null) return null;
		const activityJson = attrs["activitypub.activity.json"];
		if (typeof activityJson !== "string") return null;
		let activityType = "Unknown";
		let activityId;
		let actorId;
		try {
			const activity = JSON.parse(activityJson);
			activityType = activity.type ?? "Unknown";
			activityId = activity.id;
			if (typeof activity.actor === "string") actorId = activity.actor;
			else if (activity.actor != null && typeof activity.actor.id === "string") actorId = activity.actor.id;
		} catch {}
		const inboxUrl = attrs["activitypub.inbox.url"];
		const explicitActivityId = attrs["activitypub.activity.id"];
		return {
			traceId,
			spanId,
			parentSpanId,
			direction: "outbound",
			activityType,
			activityId: activityId ?? (typeof explicitActivityId === "string" && explicitActivityId !== "" ? explicitActivityId : void 0),
			actorId,
			activityJson,
			timestamp: (/* @__PURE__ */ new Date(event.time[0] * 1e3 + event.time[1] / 1e6)).toISOString(),
			inboxUrl: typeof inboxUrl === "string" ? inboxUrl : void 0
		};
	}
	async #storeRecord(record) {
		const options = this.#ttl != null ? { ttl: this.#ttl } : void 0;
		if (this.#useList) {
			const key = [
				...this.#keyPrefix,
				record.traceId,
				record.spanId
			];
			await this.#kv.set(key, record, options);
			await this.#updateTraceSummary(record, options);
		} else await this.#storeWithCas(record, options);
	}
	async #setWithCasRetry(key, transform, options) {
		if (this.#kv.cas != null) for (let attempt = 0; attempt < 3; attempt++) {
			const existing = await this.#kv.get(key);
			const newValue = transform(existing);
			if (await this.#kv.cas(key, existing, newValue, options)) return;
		}
		const newValue = transform(await this.#kv.get(key));
		await this.#kv.set(key, newValue, options);
	}
	async #updateTraceSummary(record, options) {
		const summaryKey = [
			...this.#keyPrefix,
			"_summaries",
			record.traceId
		];
		await this.#setWithCasRetry(summaryKey, (existing) => {
			const summary = existing != null ? {
				traceId: existing.traceId,
				timestamp: existing.timestamp,
				activityCount: existing.activityCount,
				activityTypes: [...existing.activityTypes]
			} : {
				traceId: record.traceId,
				timestamp: record.timestamp,
				activityCount: 0,
				activityTypes: []
			};
			summary.activityCount += 1;
			if (!summary.activityTypes.includes(record.activityType)) summary.activityTypes.push(record.activityType);
			return summary;
		}, options);
	}
	async #storeWithCas(record, options) {
		const key = [...this.#keyPrefix, record.traceId];
		await this.#setWithCasRetry(key, (existing) => existing ? [...existing, record] : [record], options);
		await this.#updateTraceSummary(record, options);
	}
	/**
	* Gets all activity records for a specific trace ID.
	*
	* @param traceId The trace ID to query.
	* @returns An array of activity records belonging to the trace.
	*/
	async getActivitiesByTraceId(traceId) {
		if (this.#useList) {
			const prefix = [...this.#keyPrefix, traceId];
			const records = [];
			for await (const entry of this.#kv.list(prefix)) records.push(entry.value);
			return records;
		} else {
			const key = [...this.#keyPrefix, traceId];
			return await this.#kv.get(key) ?? [];
		}
	}
	/**
	* Gets recent traces with summary information.
	*
	* @param options Options for the query.
	* @returns An array of trace summaries.
	*/
	async getRecentTraces(options) {
		const summaryPrefix = [...this.#keyPrefix, "_summaries"];
		const summaries = [];
		if (this.#kv.list != null) for await (const entry of this.#kv.list(summaryPrefix)) summaries.push(entry.value);
		summaries.sort((a, b) => b.timestamp.localeCompare(a.timestamp));
		if (options?.limit != null) return summaries.slice(0, options.limit);
		return summaries;
	}
	/**
	* Forces the exporter to flush any buffered data.
	* This is a no-op because we write directly to the KvStore without buffering.
	*/
	async forceFlush() {}
	/**
	* Shuts down the exporter.
	*/
	async shutdown() {}
};
//#endregion
//#region src/otel/exporter.test.ts
function createMockSpan(options) {
	const spanContext = {
		traceId: options.traceId ?? "0123456789abcdef0123456789abcdef",
		spanId: options.spanId ?? "0123456789abcdef",
		traceFlags: TraceFlags.SAMPLED
	};
	return {
		name: options.name ?? "test-span",
		kind: SpanKind.INTERNAL,
		spanContext: () => spanContext,
		parentSpanId: options.parentSpanId,
		startTime: [17e8, 0],
		endTime: [1700000001, 0],
		status: { code: SpanStatusCode.OK },
		attributes: {},
		links: [],
		events: options.events ?? [],
		duration: [1, 0],
		ended: true,
		resource: {
			attributes: {},
			merge: () => ({
				attributes: {},
				merge: () => null
			})
		},
		instrumentationLibrary: { name: "test" },
		droppedAttributesCount: 0,
		droppedEventsCount: 0,
		droppedLinksCount: 0
	};
}
function createActivityReceivedEvent(options) {
	return {
		name: "activitypub.activity.received",
		time: [17e8, 5e8],
		attributes: {
			"activitypub.activity.json": options.activityJson,
			"activitypub.activity.verified": options.verified ?? true,
			"ld_signatures.verified": options.ldSigVerified ?? false,
			"http_signatures.verified": options.httpSigVerified ?? true,
			"http_signatures.key_id": options.httpSigKeyId ?? ""
		}
	};
}
function createActivitySentEvent(options) {
	return {
		name: "activitypub.activity.sent",
		time: [17e8, 5e8],
		attributes: {
			"activitypub.activity.json": options.activityJson,
			"activitypub.inbox.url": options.inboxUrl,
			"activitypub.activity.id": options.activityId ?? ""
		}
	};
}
test("FedifySpanExporter", async (t) => {
	await t.step("constructor throws if KvStore has neither list() nor cas()", () => {
		const kv = {
			get: () => Promise.resolve(void 0),
			set: () => Promise.resolve(),
			delete: () => Promise.resolve()
		};
		assertThrows(() => new FedifySpanExporter(kv), Error, "KvStore must support either list() or cas()");
	});
	await t.step("constructor accepts KvStore with list()", () => {
		assertEquals(new FedifySpanExporter(new MemoryKvStore()) instanceof FedifySpanExporter, true);
	});
	await t.step("constructor accepts KvStore with cas() only", () => {
		assertEquals(new FedifySpanExporter({
			get: () => Promise.resolve(void 0),
			set: () => Promise.resolve(),
			delete: () => Promise.resolve(),
			cas: () => Promise.resolve(true)
		}) instanceof FedifySpanExporter, true);
	});
	await t.step("export() stores inbound activity from span event", async () => {
		const exporter = new FedifySpanExporter(new MemoryKvStore());
		const traceId = "trace123";
		const spanId = "span456";
		const activity = {
			"@context": "https://www.w3.org/ns/activitystreams",
			type: "Create",
			id: "https://example.com/activities/123",
			actor: "https://example.com/users/alice",
			object: {
				type: "Note",
				content: "Hello!"
			}
		};
		const activityJson = JSON.stringify(activity);
		const span = createMockSpan({
			traceId,
			spanId,
			name: "activitypub.inbox",
			events: [createActivityReceivedEvent({
				activityJson,
				verified: true,
				httpSigVerified: true
			})]
		});
		await new Promise((resolve) => {
			exporter.export([span], (result) => {
				assertEquals(result.code, 0);
				resolve();
			});
		});
		const activities = await exporter.getActivitiesByTraceId(traceId);
		assertEquals(activities.length, 1);
		assertEquals(activities[0].traceId, traceId);
		assertEquals(activities[0].spanId, spanId);
		assertEquals(activities[0].direction, "inbound");
		assertEquals(activities[0].activityType, activity.type);
		assertEquals(activities[0].activityId, activity.id);
		assertEquals(activities[0].activityJson, activityJson);
		assertEquals(activities[0].verified, true);
	});
	await t.step("export() stores outbound activity from span event", async () => {
		const exporter = new FedifySpanExporter(new MemoryKvStore());
		const traceId = "trace789";
		const spanId = "span012";
		const inboxUrl = "https://example.com/users/alice/inbox";
		const activity = {
			"@context": "https://www.w3.org/ns/activitystreams",
			type: "Follow",
			id: "https://myserver.com/activities/789",
			actor: "https://myserver.com/users/bob",
			object: "https://example.com/users/alice"
		};
		const span = createMockSpan({
			traceId,
			spanId,
			name: "activitypub.send_activity",
			events: [createActivitySentEvent({
				activityJson: JSON.stringify(activity),
				inboxUrl,
				activityId: activity.id
			})]
		});
		await new Promise((resolve) => {
			exporter.export([span], (result) => {
				assertEquals(result.code, 0);
				resolve();
			});
		});
		const activities = await exporter.getActivitiesByTraceId(traceId);
		assertEquals(activities.length, 1);
		assertEquals(activities[0].traceId, traceId);
		assertEquals(activities[0].spanId, spanId);
		assertEquals(activities[0].direction, "outbound");
		assertEquals(activities[0].activityType, activity.type);
		assertEquals(activities[0].activityId, activity.id);
		assertEquals(activities[0].inboxUrl, inboxUrl);
	});
	await t.step("export() ignores spans without activity events", async () => {
		const exporter = new FedifySpanExporter(new MemoryKvStore());
		const span = createMockSpan({
			traceId: "trace999",
			spanId: "span999",
			name: "some-other-span",
			events: []
		});
		await new Promise((resolve) => {
			exporter.export([span], (result) => {
				assertEquals(result.code, 0);
				resolve();
			});
		});
		assertEquals((await exporter.getActivitiesByTraceId("trace999")).length, 0);
	});
	await t.step("export() stores multiple activities from same trace", async () => {
		const exporter = new FedifySpanExporter(new MemoryKvStore());
		const inboundActivity = JSON.stringify({
			"@context": "https://www.w3.org/ns/activitystreams",
			type: "Create",
			id: "https://example.com/activities/1"
		});
		const outboundActivity = JSON.stringify({
			"@context": "https://www.w3.org/ns/activitystreams",
			type: "Accept",
			id: "https://myserver.com/activities/2"
		});
		const span1 = createMockSpan({
			traceId: "multitrace",
			spanId: "span1",
			name: "activitypub.inbox",
			events: [createActivityReceivedEvent({ activityJson: inboundActivity })]
		});
		const span2 = createMockSpan({
			traceId: "multitrace",
			spanId: "span2",
			parentSpanId: "span1",
			name: "activitypub.send_activity",
			events: [createActivitySentEvent({
				activityJson: outboundActivity,
				inboxUrl: "https://example.com/inbox"
			})]
		});
		await new Promise((resolve) => {
			exporter.export([span1, span2], (result) => {
				assertEquals(result.code, 0);
				resolve();
			});
		});
		const activities = await exporter.getActivitiesByTraceId("multitrace");
		assertEquals(activities.length, 2);
		const inbound = activities.find((a) => a.direction === "inbound");
		const outbound = activities.find((a) => a.direction === "outbound");
		assertEquals(inbound?.activityType, "Create");
		assertEquals(outbound?.activityType, "Accept");
		assertEquals(outbound?.parentSpanId, "span1");
	});
	await t.step("getRecentTraces() returns recent traces", async () => {
		const exporter = new FedifySpanExporter(new MemoryKvStore());
		for (let i = 0; i < 5; i++) {
			const activityJson = JSON.stringify({
				"@context": "https://www.w3.org/ns/activitystreams",
				type: "Create",
				id: `https://example.com/activities/${i}`
			});
			const span = createMockSpan({
				traceId: `trace-${i}`,
				spanId: `span-${i}`,
				name: "activitypub.inbox",
				events: [createActivityReceivedEvent({ activityJson })]
			});
			await new Promise((resolve) => {
				exporter.export([span], () => resolve());
			});
		}
		assertEquals((await exporter.getRecentTraces({ limit: 3 })).length, 3);
	});
	await t.step("getRecentTraces() returns all traces when limit not specified", async () => {
		const exporter = new FedifySpanExporter(new MemoryKvStore());
		for (let i = 0; i < 3; i++) {
			const activityJson = JSON.stringify({
				"@context": "https://www.w3.org/ns/activitystreams",
				type: "Create",
				id: `https://example.com/activities/${i}`
			});
			const span = createMockSpan({
				traceId: `all-trace-${i}`,
				spanId: `span-${i}`,
				name: "activitypub.inbox",
				events: [createActivityReceivedEvent({ activityJson })]
			});
			await new Promise((resolve) => {
				exporter.export([span], () => resolve());
			});
		}
		assertEquals((await exporter.getRecentTraces()).length >= 3, true);
	});
	await t.step("forceFlush() returns resolved promise", async () => {
		await new FedifySpanExporter(new MemoryKvStore()).forceFlush();
	});
	await t.step("shutdown() completes successfully", async () => {
		await new FedifySpanExporter(new MemoryKvStore()).shutdown();
	});
	await t.step("works with cas()-only KvStore", async () => {
		const storedData = {};
		const exporter = new FedifySpanExporter({
			get: (key) => {
				const k = JSON.stringify(key);
				return Promise.resolve(storedData[k]);
			},
			set: (key, value) => {
				const k = JSON.stringify(key);
				storedData[k] = value;
				return Promise.resolve();
			},
			delete: (key) => {
				const k = JSON.stringify(key);
				delete storedData[k];
				return Promise.resolve();
			},
			cas: (key, expected, newValue) => {
				const k = JSON.stringify(key);
				const current = storedData[k];
				if (JSON.stringify(current) === JSON.stringify(expected)) {
					storedData[k] = newValue;
					return Promise.resolve(true);
				}
				return Promise.resolve(false);
			}
		});
		const span = createMockSpan({
			traceId: "cas-trace",
			spanId: "cas-span",
			name: "activitypub.inbox",
			events: [createActivityReceivedEvent({ activityJson: JSON.stringify({
				"@context": "https://www.w3.org/ns/activitystreams",
				type: "Like",
				id: "https://example.com/activities/like"
			}) })]
		});
		await new Promise((resolve) => {
			exporter.export([span], (result) => {
				assertEquals(result.code, 0);
				resolve();
			});
		});
		const activities = await exporter.getActivitiesByTraceId("cas-trace");
		assertEquals(activities.length, 1);
		assertEquals(activities[0].activityType, "Like");
	});
	await t.step("TTL option is respected", async () => {
		const exporter = new FedifySpanExporter(new MemoryKvStore(), { ttl: Temporal.Duration.from({ hours: 1 }) });
		const span = createMockSpan({
			traceId: "ttl-trace",
			spanId: "ttl-span",
			name: "activitypub.inbox",
			events: [createActivityReceivedEvent({ activityJson: JSON.stringify({
				"@context": "https://www.w3.org/ns/activitystreams",
				type: "Create"
			}) })]
		});
		await new Promise((resolve) => {
			exporter.export([span], () => resolve());
		});
		assertEquals((await exporter.getActivitiesByTraceId("ttl-trace")).length, 1);
	});
	await t.step("keyPrefix option customizes storage keys", async () => {
		const exporter = new FedifySpanExporter(new MemoryKvStore(), { keyPrefix: ["custom", "prefix"] });
		const span = createMockSpan({
			traceId: "prefix-trace",
			spanId: "prefix-span",
			name: "activitypub.inbox",
			events: [createActivityReceivedEvent({ activityJson: JSON.stringify({
				"@context": "https://www.w3.org/ns/activitystreams",
				type: "Announce"
			}) })]
		});
		await new Promise((resolve) => {
			exporter.export([span], () => resolve());
		});
		const activities = await exporter.getActivitiesByTraceId("prefix-trace");
		assertEquals(activities.length, 1);
		assertEquals(activities[0].activityType, "Announce");
	});
	await t.step("separate exporter instances share state via same KvStore (distributed simulation)", async () => {
		const sharedKv = new MemoryKvStore();
		const webServerExporter = new FedifySpanExporter(sharedKv);
		const workerExporter = new FedifySpanExporter(sharedKv);
		const dashboardExporter = new FedifySpanExporter(sharedKv);
		const inboxSpan = createMockSpan({
			traceId: "distributed-trace-001",
			spanId: "inbox-span",
			name: "activitypub.inbox",
			events: [createActivityReceivedEvent({
				activityJson: JSON.stringify({
					"@context": "https://www.w3.org/ns/activitystreams",
					type: "Follow",
					id: "https://remote.example/activities/follow-1",
					actor: "https://remote.example/users/alice",
					object: "https://local.example/users/bob"
				}),
				verified: true
			})]
		});
		await new Promise((resolve) => {
			webServerExporter.export([inboxSpan], () => resolve());
		});
		const sendSpan = createMockSpan({
			traceId: "distributed-trace-001",
			spanId: "send-span",
			parentSpanId: "inbox-span",
			name: "activitypub.send_activity",
			events: [createActivitySentEvent({
				activityJson: JSON.stringify({
					"@context": "https://www.w3.org/ns/activitystreams",
					type: "Accept",
					id: "https://local.example/activities/accept-1",
					actor: "https://local.example/users/bob",
					object: "https://remote.example/activities/follow-1"
				}),
				inboxUrl: "https://remote.example/users/alice/inbox",
				activityId: "https://local.example/activities/accept-1"
			})]
		});
		await new Promise((resolve) => {
			workerExporter.export([sendSpan], () => resolve());
		});
		const activities = await dashboardExporter.getActivitiesByTraceId("distributed-trace-001");
		assertEquals(activities.length, 2);
		const follow = activities.find((a) => a.activityType === "Follow");
		const accept = activities.find((a) => a.activityType === "Accept");
		assertEquals(follow != null, true);
		assertEquals(follow?.direction, "inbound");
		assertEquals(follow?.verified, true);
		assertEquals(accept != null, true);
		assertEquals(accept?.direction, "outbound");
		assertEquals(accept?.inboxUrl, "https://remote.example/users/alice/inbox");
		assertEquals(accept?.parentSpanId, "inbox-span");
		const ourTrace = (await dashboardExporter.getRecentTraces()).find((t) => t.traceId === "distributed-trace-001");
		assertEquals(ourTrace != null, true);
		assertEquals(ourTrace?.activityCount, 2);
		assertEquals(ourTrace?.activityTypes.includes("Follow"), true);
		assertEquals(ourTrace?.activityTypes.includes("Accept"), true);
	});
	await t.step("multiple workers writing to same trace concurrently", async () => {
		const sharedKv = new MemoryKvStore();
		const workers = [
			new FedifySpanExporter(sharedKv),
			new FedifySpanExporter(sharedKv),
			new FedifySpanExporter(sharedKv)
		];
		const traceId = "concurrent-fanout-trace";
		const exportPromises = workers.map((worker, i) => {
			const activityJson = JSON.stringify({
				"@context": "https://www.w3.org/ns/activitystreams",
				type: "Create",
				id: "https://local.example/activities/post-1"
			});
			const span = createMockSpan({
				traceId,
				spanId: `worker-${i}-span`,
				name: "activitypub.send_activity",
				events: [createActivitySentEvent({
					activityJson,
					inboxUrl: `https://follower-${i}.example/inbox`
				})]
			});
			return new Promise((resolve) => {
				worker.export([span], () => resolve());
			});
		});
		await Promise.all(exportPromises);
		const activities = await new FedifySpanExporter(sharedKv).getActivitiesByTraceId(traceId);
		assertEquals(activities.length, 3);
		assertEquals(activities.map((a) => a.inboxUrl).sort(), [
			"https://follower-0.example/inbox",
			"https://follower-1.example/inbox",
			"https://follower-2.example/inbox"
		]);
	});
	await t.step("extracts actorId from activity with string actor", async () => {
		const exporter = new FedifySpanExporter(new MemoryKvStore());
		const span = createMockSpan({
			traceId: "actor-string-trace",
			spanId: "span1",
			name: "activitypub.inbox",
			events: [createActivityReceivedEvent({ activityJson: JSON.stringify({
				"@context": "https://www.w3.org/ns/activitystreams",
				type: "Create",
				id: "https://example.com/activities/123",
				actor: "https://example.com/users/alice",
				object: {
					type: "Note",
					content: "Hello!"
				}
			}) })]
		});
		await new Promise((resolve) => {
			exporter.export([span], () => resolve());
		});
		const activities = await exporter.getActivitiesByTraceId("actor-string-trace");
		assertEquals(activities.length, 1);
		assertEquals(activities[0].actorId, "https://example.com/users/alice");
	});
	await t.step("extracts actorId from activity with object actor", async () => {
		const exporter = new FedifySpanExporter(new MemoryKvStore());
		const span = createMockSpan({
			traceId: "actor-object-trace",
			spanId: "span1",
			name: "activitypub.inbox",
			events: [createActivityReceivedEvent({ activityJson: JSON.stringify({
				"@context": "https://www.w3.org/ns/activitystreams",
				type: "Create",
				id: "https://example.com/activities/456",
				actor: {
					type: "Person",
					id: "https://example.com/users/bob",
					name: "Bob"
				},
				object: {
					type: "Note",
					content: "Hello!"
				}
			}) })]
		});
		await new Promise((resolve) => {
			exporter.export([span], () => resolve());
		});
		const activities = await exporter.getActivitiesByTraceId("actor-object-trace");
		assertEquals(activities.length, 1);
		assertEquals(activities[0].actorId, "https://example.com/users/bob");
	});
	await t.step("extracts actorId from outbound activity", async () => {
		const exporter = new FedifySpanExporter(new MemoryKvStore());
		const span = createMockSpan({
			traceId: "outbound-actor-trace",
			spanId: "span1",
			name: "activitypub.send_activity",
			events: [createActivitySentEvent({
				activityJson: JSON.stringify({
					"@context": "https://www.w3.org/ns/activitystreams",
					type: "Follow",
					id: "https://myserver.com/activities/789",
					actor: "https://myserver.com/users/charlie",
					object: "https://example.com/users/alice"
				}),
				inboxUrl: "https://example.com/users/alice/inbox"
			})]
		});
		await new Promise((resolve) => {
			exporter.export([span], () => resolve());
		});
		const activities = await exporter.getActivitiesByTraceId("outbound-actor-trace");
		assertEquals(activities.length, 1);
		assertEquals(activities[0].actorId, "https://myserver.com/users/charlie");
	});
	await t.step("extracts signature verification details for inbound activity", async () => {
		const exporter = new FedifySpanExporter(new MemoryKvStore());
		const span = createMockSpan({
			traceId: "sig-details-trace",
			spanId: "span1",
			name: "activitypub.inbox",
			events: [createActivityReceivedEvent({
				activityJson: JSON.stringify({
					"@context": "https://www.w3.org/ns/activitystreams",
					type: "Create",
					id: "https://example.com/activities/sig-test",
					actor: "https://example.com/users/alice"
				}),
				verified: true,
				httpSigVerified: true,
				httpSigKeyId: "https://example.com/users/alice#main-key",
				ldSigVerified: false
			})]
		});
		await new Promise((resolve) => {
			exporter.export([span], () => resolve());
		});
		const activities = await exporter.getActivitiesByTraceId("sig-details-trace");
		assertEquals(activities.length, 1);
		assertEquals(activities[0].verified, true);
		assertEquals(activities[0].signatureDetails != null, true);
		assertEquals(activities[0].signatureDetails?.httpSignaturesVerified, true);
		assertEquals(activities[0].signatureDetails?.httpSignaturesKeyId, "https://example.com/users/alice#main-key");
		assertEquals(activities[0].signatureDetails?.ldSignaturesVerified, false);
	});
	await t.step("signature details with LD signatures verified", async () => {
		const exporter = new FedifySpanExporter(new MemoryKvStore());
		const span = createMockSpan({
			traceId: "ld-sig-trace",
			spanId: "span1",
			name: "activitypub.inbox",
			events: [createActivityReceivedEvent({
				activityJson: JSON.stringify({
					"@context": "https://www.w3.org/ns/activitystreams",
					type: "Delete",
					id: "https://example.com/activities/ld-sig-test",
					actor: "https://example.com/users/alice"
				}),
				verified: true,
				httpSigVerified: false,
				ldSigVerified: true
			})]
		});
		await new Promise((resolve) => {
			exporter.export([span], () => resolve());
		});
		const activities = await exporter.getActivitiesByTraceId("ld-sig-trace");
		assertEquals(activities.length, 1);
		assertEquals(activities[0].signatureDetails?.httpSignaturesVerified, false);
		assertEquals(activities[0].signatureDetails?.ldSignaturesVerified, true);
	});
	await t.step("handles activity without actor field", async () => {
		const exporter = new FedifySpanExporter(new MemoryKvStore());
		const span = createMockSpan({
			traceId: "no-actor-trace",
			spanId: "span1",
			name: "activitypub.inbox",
			events: [createActivityReceivedEvent({ activityJson: JSON.stringify({
				"@context": "https://www.w3.org/ns/activitystreams",
				type: "Delete",
				id: "https://example.com/activities/no-actor",
				object: "https://example.com/posts/123"
			}) })]
		});
		await new Promise((resolve) => {
			exporter.export([span], () => resolve());
		});
		const activities = await exporter.getActivitiesByTraceId("no-actor-trace");
		assertEquals(activities.length, 1);
		assertEquals(activities[0].actorId, void 0);
	});
});
//#endregion
export {};
