@fedify/fedify 2.0.35-dev.2469 → 2.0.35-dev.2480

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. package/dist/{builder-vEXq-lKC.mjs → builder-cEjyBwpD.mjs} +3 -3
  2. package/dist/compat/transformers.test.mjs +1 -1
  3. package/dist/{deno-HyjpIMD2.mjs → deno-BcnGeGr2.mjs} +1 -1
  4. package/dist/{docloader-C3V1CA__.mjs → docloader-BqDq3fDF.mjs} +2 -2
  5. package/dist/federation/builder.test.mjs +1 -1
  6. package/dist/federation/handler.test.mjs +6 -6
  7. package/dist/federation/idempotency.test.mjs +2 -2
  8. package/dist/federation/inbox.test.mjs +1 -1
  9. package/dist/federation/middleware.test.mjs +6 -6
  10. package/dist/federation/mod.cjs +1 -1
  11. package/dist/federation/mod.js +1 -1
  12. package/dist/federation/send.test.mjs +3 -3
  13. package/dist/federation/temporal.test.mjs +1 -1
  14. package/dist/federation/webfinger.test.mjs +1 -1
  15. package/dist/{http-B_JXxsg0.cjs → http-BD9izRp9.cjs} +1 -1
  16. package/dist/{http-xlEvsjp2.mjs → http-D0cjeL8a.mjs} +2 -2
  17. package/dist/{http-B9yJ1OGG.js → http-IYRDhhnT.js} +1 -1
  18. package/dist/{inbox-Ca4SopAP.mjs → inbox-D2fsxjqC.mjs} +1 -1
  19. package/dist/{key-CLtu73m3.mjs → key-BFzQ3Hef.mjs} +1 -1
  20. package/dist/{kv-cache-BzxVosc_.cjs → kv-cache-B4sDuLjb.cjs} +1 -1
  21. package/dist/{kv-cache-ibNSFaaR.js → kv-cache-C-RyNMt6.js} +1 -1
  22. package/dist/{ld-CwZHzx6E.mjs → ld-D2bkYBIz.mjs} +2 -2
  23. package/dist/{middleware-BE2KpCCl.mjs → middleware-3mc-kq3v.mjs} +1 -1
  24. package/dist/{middleware-BfYt2ccy.cjs → middleware-B2UC8mtN.cjs} +1 -1
  25. package/dist/{middleware-B-PC2OBb.mjs → middleware-CXIBpN4y.mjs} +11 -11
  26. package/dist/{middleware-DKzhsZ6a.js → middleware-DRZp7Odv.js} +3 -3
  27. package/dist/{middleware-DegTN2kB.cjs → middleware-DxX8ZL-q.cjs} +4 -4
  28. package/dist/mod.cjs +4 -4
  29. package/dist/mod.js +4 -4
  30. package/dist/nodeinfo/handler.test.mjs +1 -1
  31. package/dist/otel/exporter.test.mjs +292 -30
  32. package/dist/otel/mod.cjs +36 -30
  33. package/dist/otel/mod.d.cts +13 -0
  34. package/dist/otel/mod.d.ts +13 -0
  35. package/dist/otel/mod.js +36 -30
  36. package/dist/{owner-CI6RnH-j.mjs → owner-BuJXJDhl.mjs} +2 -2
  37. package/dist/{proof-C7JS7QMS.js → proof-BJ_6nKhb.js} +1 -1
  38. package/dist/{proof-BIaI6Y0n.cjs → proof-DfmHAmtr.cjs} +1 -1
  39. package/dist/{proof-CDgXDfMX.mjs → proof-JxVhLZA3.mjs} +2 -2
  40. package/dist/{send-7rCQ8WKw.mjs → send-CDaWmeqV.mjs} +2 -2
  41. package/dist/sig/http.test.mjs +2 -2
  42. package/dist/sig/key.test.mjs +2 -2
  43. package/dist/sig/ld.test.mjs +2 -2
  44. package/dist/sig/mod.cjs +2 -2
  45. package/dist/sig/mod.js +2 -2
  46. package/dist/sig/owner.test.mjs +1 -1
  47. package/dist/sig/proof.test.mjs +1 -1
  48. package/dist/{temporal-DsnplmHX.mjs → temporal-Bv6Zn1C5.mjs} +1 -1
  49. package/dist/utils/docloader.test.mjs +2 -2
  50. package/dist/utils/mod.cjs +1 -1
  51. package/dist/utils/mod.js +1 -1
  52. package/package.json +6 -6
@@ -3,6 +3,7 @@ import "urlpattern-polyfill";
3
3
  globalThis.addEventListener = () => {};
4
4
  import { t as assertEquals } from "../assert_equals-Ew3jOFa3.mjs";
5
5
  import "../std__assert-DflMZha5.mjs";
6
+ import { t as assert } from "../assert-BhQd8QHP.mjs";
6
7
  import { t as MemoryKvStore } from "../kv-mI4Ix2EU.mjs";
7
8
  import { test } from "@fedify/fixture";
8
9
  import { SpanKind, SpanStatusCode, TraceFlags } from "@opentelemetry/api";
@@ -187,33 +188,6 @@ var FedifySpanExporter = class {
187
188
  record.spanId
188
189
  ];
189
190
  await this.#kv.set(key, record, options);
190
- await this.#updateTraceSummary(record, options);
191
- }
192
- async #setWithCasRetry(key, transform, options) {
193
- if (this.#kv.cas != null) for (let attempt = 0; attempt < 3; attempt++) {
194
- const existing = await this.#kv.get(key);
195
- const newValue = transform(existing);
196
- if (await this.#kv.cas(key, existing, newValue, options)) return;
197
- }
198
- const newValue = transform(await this.#kv.get(key));
199
- await this.#kv.set(key, newValue, options);
200
- }
201
- async #updateTraceSummary(record, options) {
202
- const summaryKey = [
203
- ...this.#keyPrefix,
204
- "_summaries",
205
- record.traceId
206
- ];
207
- await this.#setWithCasRetry(summaryKey, (existing) => {
208
- const activityCount = existing != null ? existing.activityCount + 1 : 1;
209
- const activityTypes = existing != null ? existing.activityTypes.includes(record.activityType) ? existing.activityTypes : [...existing.activityTypes, record.activityType] : [record.activityType];
210
- return {
211
- traceId: existing?.traceId ?? record.traceId,
212
- timestamp: existing?.timestamp ?? record.timestamp,
213
- activityCount,
214
- activityTypes
215
- };
216
- }, options);
217
191
  }
218
192
  /**
219
193
  * Gets all activity records for a specific trace ID.
@@ -230,13 +204,46 @@ var FedifySpanExporter = class {
230
204
  /**
231
205
  * Gets recent traces with summary information.
232
206
  *
207
+ * Each summary is computed directly from the activity records currently
208
+ * stored for each trace, rather than from a separately maintained
209
+ * counter. This guarantees that a trace is discoverable, and its
210
+ * `activityCount` accurate, as soon as its records are stored, even if
211
+ * an earlier attempt at summarizing it (in a previous version of this
212
+ * exporter) never completed, and that the count always reflects
213
+ * records that have not expired. It costs a full scan of every stored
214
+ * record on every call, including full inbound activity JSON. The
215
+ * `limit` option only truncates the returned summaries and does not
216
+ * reduce the scan. Callers that poll this method frequently should
217
+ * account for the total retained data when choosing a TTL and polling
218
+ * interval.
219
+ *
233
220
  * @param options Options for the query.
234
221
  * @returns An array of trace summaries.
235
222
  */
236
223
  async getRecentTraces(options) {
237
- const summaryPrefix = [...this.#keyPrefix, "_summaries"];
238
- const summaries = [];
239
- for await (const entry of this.#kv.list(summaryPrefix)) summaries.push(entry.value);
224
+ const aggregates = /* @__PURE__ */ new Map();
225
+ const traceIdIndex = this.#keyPrefix.length;
226
+ for await (const entry of this.#kv.list(this.#keyPrefix)) {
227
+ const traceId = entry.key[traceIdIndex];
228
+ if (traceId == null || traceId === "_summaries") continue;
229
+ const record = entry.value;
230
+ if (record == null || typeof record.timestamp !== "string") continue;
231
+ const aggregate = aggregates.get(traceId);
232
+ if (aggregate == null) aggregates.set(traceId, {
233
+ timestamp: record.timestamp,
234
+ activityCount: 1,
235
+ activityTypes: [record.activityType]
236
+ });
237
+ else {
238
+ aggregate.activityCount++;
239
+ if (record.timestamp < aggregate.timestamp) aggregate.timestamp = record.timestamp;
240
+ if (!aggregate.activityTypes.includes(record.activityType)) aggregate.activityTypes.push(record.activityType);
241
+ }
242
+ }
243
+ const summaries = [...aggregates].map(([traceId, aggregate]) => ({
244
+ traceId,
245
+ ...aggregate
246
+ }));
240
247
  summaries.sort((a, b) => b.timestamp.localeCompare(a.timestamp));
241
248
  if (options?.limit != null) return summaries.slice(0, options.limit);
242
249
  return summaries;
@@ -497,6 +504,261 @@ test("FedifySpanExporter", async (t) => {
497
504
  }
498
505
  assertEquals((await exporter.getRecentTraces()).length >= 3, true);
499
506
  });
507
+ function createOutboundSpan(traceId, spanId) {
508
+ return createMockSpan({
509
+ traceId,
510
+ spanId,
511
+ name: "activitypub.send_activity",
512
+ events: [createActivitySentEvent({
513
+ activityJson: JSON.stringify({
514
+ "@context": "https://www.w3.org/ns/activitystreams",
515
+ type: "Create",
516
+ id: `https://local.example/activities/${spanId}`
517
+ }),
518
+ inboxUrl: "https://follower.example/inbox"
519
+ })]
520
+ });
521
+ }
522
+ async function exportSpans(exporter, spans) {
523
+ assertEquals((await new Promise((resolve) => {
524
+ exporter.export(spans, resolve);
525
+ })).code, 0);
526
+ }
527
+ for (const withCas of [true, false]) {
528
+ const createStore = () => {
529
+ const memory = new MemoryKvStore();
530
+ return withCas ? memory : {
531
+ get: memory.get.bind(memory),
532
+ set: memory.set.bind(memory),
533
+ delete: memory.delete.bind(memory),
534
+ list: memory.list.bind(memory)
535
+ };
536
+ };
537
+ await t.step(`re-export counts each retained span once (CAS=${withCas})`, async () => {
538
+ const kv = createStore();
539
+ const exporters = [new FedifySpanExporter(kv), new FedifySpanExporter(kv)];
540
+ const span = createOutboundSpan("reexport-trace", "span-a");
541
+ await exportSpans(exporters[0], [span]);
542
+ await exportSpans(exporters[0], [span]);
543
+ assertEquals((await exporters[1].getRecentTraces())[0].activityCount, 1);
544
+ await Promise.all(exporters.map((exporter) => exportSpans(exporter, [span])));
545
+ assertEquals((await exporters[0].getActivitiesByTraceId("reexport-trace")).length, 1);
546
+ assertEquals((await exporters[1].getRecentTraces())[0].activityCount, 1);
547
+ });
548
+ await t.step(`concurrent distinct spans are all counted (CAS=${withCas})`, async () => {
549
+ const kv = createStore();
550
+ const exporters = [new FedifySpanExporter(kv), new FedifySpanExporter(kv)];
551
+ await Promise.all(exporters.map((exporter, i) => exportSpans(exporter, [createOutboundSpan("concurrent-count-trace", `span-${i}`)])));
552
+ const reader = new FedifySpanExporter(kv);
553
+ assertEquals((await reader.getActivitiesByTraceId("concurrent-count-trace")).length, 2);
554
+ assertEquals((await reader.getRecentTraces())[0].activityCount, 2);
555
+ });
556
+ await t.step(`legacy summaries cannot hide or inflate retained records (CAS=${withCas})`, async () => {
557
+ const kv = createStore();
558
+ const exporter = new FedifySpanExporter(kv);
559
+ await exportSpans(exporter, [createOutboundSpan("legacy-trace", "span-a")]);
560
+ await kv.delete([
561
+ "fedify",
562
+ "traces",
563
+ "_summaries",
564
+ "legacy-trace"
565
+ ]);
566
+ const originalSet = kv.set.bind(kv);
567
+ let writes = 0;
568
+ kv.set = (...args) => {
569
+ writes++;
570
+ return originalSet(...args);
571
+ };
572
+ assertEquals((await exporter.getRecentTraces())[0].activityCount, 1);
573
+ assertEquals(writes, 0);
574
+ const stale = {
575
+ traceId: "legacy-trace",
576
+ timestamp: "2000-01-01T00:00:00.000Z",
577
+ activityCount: 99,
578
+ activityTypes: ["Bogus"]
579
+ };
580
+ await kv.set([
581
+ "fedify",
582
+ "traces",
583
+ "_summaries",
584
+ "legacy-trace"
585
+ ], stale);
586
+ await kv.set([
587
+ "fedify",
588
+ "traces",
589
+ "_summaries",
590
+ "ghost-trace"
591
+ ], {
592
+ ...stale,
593
+ traceId: "ghost-trace"
594
+ });
595
+ const records = await exporter.getActivitiesByTraceId("legacy-trace");
596
+ assertEquals(await exporter.getRecentTraces(), [{
597
+ traceId: "legacy-trace",
598
+ timestamp: records[0].timestamp,
599
+ activityCount: 1,
600
+ activityTypes: ["Create"]
601
+ }]);
602
+ await kv.delete([
603
+ "fedify",
604
+ "traces",
605
+ "legacy-trace",
606
+ "span-a"
607
+ ]);
608
+ assertEquals(await exporter.getRecentTraces(), []);
609
+ });
610
+ await t.step(`summary metadata follows retained records (CAS=${withCas})`, async () => {
611
+ const kv = createStore();
612
+ const exporter = new FedifySpanExporter(kv, { keyPrefix: ["custom"] });
613
+ function span(traceId, spanId, type, seconds) {
614
+ return createMockSpan({
615
+ traceId,
616
+ spanId,
617
+ events: [{
618
+ ...createActivitySentEvent({
619
+ activityJson: JSON.stringify({ type }),
620
+ inboxUrl: "https://example.com/inbox"
621
+ }),
622
+ time: [seconds, 0]
623
+ }]
624
+ });
625
+ }
626
+ await exportSpans(exporter, [
627
+ span("older-trace", "late", "Like", 1700000002),
628
+ span("older-trace", "early", "Create", 17e8),
629
+ span("older-trace", "duplicate-type", "Like", 1700000003),
630
+ span("newer-trace", "only", "Follow", 1700000001)
631
+ ]);
632
+ const recent = await exporter.getRecentTraces();
633
+ assertEquals(recent.map((summary) => summary.traceId), ["newer-trace", "older-trace"]);
634
+ assertEquals(recent[1], {
635
+ traceId: "older-trace",
636
+ timestamp: (/* @__PURE__ */ new Date(17e11)).toISOString(),
637
+ activityCount: 3,
638
+ activityTypes: ["Like", "Create"]
639
+ });
640
+ assertEquals(await exporter.getRecentTraces({ limit: 1 }), [recent[0]]);
641
+ await kv.delete([
642
+ "custom",
643
+ "older-trace",
644
+ "early"
645
+ ]);
646
+ assertEquals((await exporter.getRecentTraces())[0], {
647
+ traceId: "older-trace",
648
+ timestamp: (/* @__PURE__ */ new Date(1700000002e3)).toISOString(),
649
+ activityCount: 2,
650
+ activityTypes: ["Like"]
651
+ });
652
+ });
653
+ }
654
+ await t.step("a trace with only one record is discoverable via getRecentTraces(), with no further write", async () => {
655
+ const exporter = new FedifySpanExporter(new MemoryKvStore());
656
+ const traceId = "first-write-discoverable-trace";
657
+ await new Promise((resolve) => {
658
+ exporter.export([createOutboundSpan(traceId, "span-a")], () => resolve());
659
+ });
660
+ assertEquals((await exporter.getRecentTraces()).find((s) => s.traceId === traceId)?.activityCount, 1);
661
+ });
662
+ await t.step("getRecentTraces() needs no additional writes to discover a trace or correct its activityCount", async () => {
663
+ const kv = new MemoryKvStore();
664
+ const exporter = new FedifySpanExporter(kv);
665
+ const traceId = "query-time-correction-trace";
666
+ await new Promise((resolve) => {
667
+ exporter.export([createOutboundSpan(traceId, "span-a")], () => resolve());
668
+ });
669
+ await new Promise((resolve) => {
670
+ exporter.export([createOutboundSpan(traceId, "span-b")], () => resolve());
671
+ });
672
+ const originalSet = kv.set.bind(kv);
673
+ let writeCalls = 0;
674
+ kv.set = (key, value, options) => {
675
+ writeCalls++;
676
+ return originalSet(key, value, options);
677
+ };
678
+ assertEquals((await exporter.getRecentTraces()).find((s) => s.traceId === traceId)?.activityCount, 2);
679
+ assertEquals(writeCalls, 0);
680
+ });
681
+ await t.step("getRecentTraces() reflects only currently-retained records, e.g., after some have expired", async () => {
682
+ const kv = new MemoryKvStore();
683
+ const exporter = new FedifySpanExporter(kv);
684
+ const traceId = "expiry-reflects-retained-trace";
685
+ await new Promise((resolve) => {
686
+ exporter.export([createOutboundSpan(traceId, "span-a"), createOutboundSpan(traceId, "span-b")], () => resolve());
687
+ });
688
+ let recent = await exporter.getRecentTraces();
689
+ assertEquals(recent.find((s) => s.traceId === traceId)?.activityCount, 2);
690
+ await kv.delete([
691
+ "fedify",
692
+ "traces",
693
+ traceId,
694
+ "span-a"
695
+ ]);
696
+ recent = await exporter.getRecentTraces();
697
+ assertEquals(recent.find((s) => s.traceId === traceId)?.activityCount, 1);
698
+ });
699
+ await t.step("getRecentTraces() polling cost scales with every retained record, not just records changed since the last poll, and limit does not reduce it", async () => {
700
+ const kv = new MemoryKvStore();
701
+ const exporter = new FedifySpanExporter(kv);
702
+ const traceCount = 20;
703
+ const recordsPerTrace = 5;
704
+ for (let i = 0; i < traceCount; i++) {
705
+ const traceId = `poll-cost-trace-${i}`;
706
+ const spans = Array.from({ length: recordsPerTrace }, (_, j) => createOutboundSpan(traceId, `span-${j}`));
707
+ await new Promise((resolve) => {
708
+ exporter.export(spans, () => resolve());
709
+ });
710
+ }
711
+ const originalList = kv.list.bind(kv);
712
+ let entriesRead = 0;
713
+ let bytesRead = 0;
714
+ kv.list = async function* (prefix) {
715
+ for await (const entry of originalList(prefix)) {
716
+ entriesRead++;
717
+ bytesRead += new TextEncoder().encode(JSON.stringify(entry.value)).byteLength;
718
+ yield entry;
719
+ }
720
+ };
721
+ assertEquals((await exporter.getRecentTraces()).length, traceCount);
722
+ assertEquals(entriesRead, traceCount * recordsPerTrace);
723
+ assert(bytesRead > 0);
724
+ const entriesReadForFullScan = entriesRead;
725
+ const bytesReadForFullScan = bytesRead;
726
+ entriesRead = 0;
727
+ bytesRead = 0;
728
+ assertEquals((await exporter.getRecentTraces({ limit: 3 })).length, 3);
729
+ assertEquals(entriesRead, entriesReadForFullScan);
730
+ assertEquals(bytesRead, bytesReadForFullScan);
731
+ });
732
+ await t.step("recent-list scans include full inbound payloads even with limit", async () => {
733
+ const kv = new MemoryKvStore();
734
+ const exporter = new FedifySpanExporter(kv);
735
+ const activityJson = JSON.stringify({
736
+ type: "Create",
737
+ object: {
738
+ type: "Note",
739
+ content: "한".repeat(64 * 1024)
740
+ }
741
+ });
742
+ await exportSpans(exporter, Array.from({ length: 10 }, (_, i) => createMockSpan({
743
+ traceId: `large-inbound-${i}`,
744
+ spanId: "inbound",
745
+ events: [createActivityReceivedEvent({ activityJson })]
746
+ })));
747
+ const originalList = kv.list.bind(kv);
748
+ let entriesRead = 0;
749
+ let bytesRead = 0;
750
+ kv.list = async function* (prefix) {
751
+ for await (const entry of originalList(prefix)) {
752
+ entriesRead++;
753
+ bytesRead += new TextEncoder().encode(JSON.stringify(entry.value)).byteLength;
754
+ yield entry;
755
+ }
756
+ };
757
+ assertEquals((await exporter.getRecentTraces({ limit: 1 })).length, 1);
758
+ assertEquals(entriesRead, 10);
759
+ assert(bytesRead >= new TextEncoder().encode(activityJson).byteLength * 10);
760
+ assert(bytesRead > 1.8 * 1024 * 1024);
761
+ });
500
762
  await t.step("forceFlush() returns resolved promise", async () => {
501
763
  await new FedifySpanExporter(new MemoryKvStore()).forceFlush();
502
764
  });
package/dist/otel/mod.cjs CHANGED
@@ -183,33 +183,6 @@ var FedifySpanExporter = class {
183
183
  record.spanId
184
184
  ];
185
185
  await this.#kv.set(key, record, options);
186
- await this.#updateTraceSummary(record, options);
187
- }
188
- async #setWithCasRetry(key, transform, options) {
189
- if (this.#kv.cas != null) for (let attempt = 0; attempt < 3; attempt++) {
190
- const existing = await this.#kv.get(key);
191
- const newValue = transform(existing);
192
- if (await this.#kv.cas(key, existing, newValue, options)) return;
193
- }
194
- const newValue = transform(await this.#kv.get(key));
195
- await this.#kv.set(key, newValue, options);
196
- }
197
- async #updateTraceSummary(record, options) {
198
- const summaryKey = [
199
- ...this.#keyPrefix,
200
- "_summaries",
201
- record.traceId
202
- ];
203
- await this.#setWithCasRetry(summaryKey, (existing) => {
204
- const activityCount = existing != null ? existing.activityCount + 1 : 1;
205
- const activityTypes = existing != null ? existing.activityTypes.includes(record.activityType) ? existing.activityTypes : [...existing.activityTypes, record.activityType] : [record.activityType];
206
- return {
207
- traceId: existing?.traceId ?? record.traceId,
208
- timestamp: existing?.timestamp ?? record.timestamp,
209
- activityCount,
210
- activityTypes
211
- };
212
- }, options);
213
186
  }
214
187
  /**
215
188
  * Gets all activity records for a specific trace ID.
@@ -226,13 +199,46 @@ var FedifySpanExporter = class {
226
199
  /**
227
200
  * Gets recent traces with summary information.
228
201
  *
202
+ * Each summary is computed directly from the activity records currently
203
+ * stored for each trace, rather than from a separately maintained
204
+ * counter. This guarantees that a trace is discoverable, and its
205
+ * `activityCount` accurate, as soon as its records are stored, even if
206
+ * an earlier attempt at summarizing it (in a previous version of this
207
+ * exporter) never completed, and that the count always reflects
208
+ * records that have not expired. It costs a full scan of every stored
209
+ * record on every call, including full inbound activity JSON. The
210
+ * `limit` option only truncates the returned summaries and does not
211
+ * reduce the scan. Callers that poll this method frequently should
212
+ * account for the total retained data when choosing a TTL and polling
213
+ * interval.
214
+ *
229
215
  * @param options Options for the query.
230
216
  * @returns An array of trace summaries.
231
217
  */
232
218
  async getRecentTraces(options) {
233
- const summaryPrefix = [...this.#keyPrefix, "_summaries"];
234
- const summaries = [];
235
- for await (const entry of this.#kv.list(summaryPrefix)) summaries.push(entry.value);
219
+ const aggregates = /* @__PURE__ */ new Map();
220
+ const traceIdIndex = this.#keyPrefix.length;
221
+ for await (const entry of this.#kv.list(this.#keyPrefix)) {
222
+ const traceId = entry.key[traceIdIndex];
223
+ if (traceId == null || traceId === "_summaries") continue;
224
+ const record = entry.value;
225
+ if (record == null || typeof record.timestamp !== "string") continue;
226
+ const aggregate = aggregates.get(traceId);
227
+ if (aggregate == null) aggregates.set(traceId, {
228
+ timestamp: record.timestamp,
229
+ activityCount: 1,
230
+ activityTypes: [record.activityType]
231
+ });
232
+ else {
233
+ aggregate.activityCount++;
234
+ if (record.timestamp < aggregate.timestamp) aggregate.timestamp = record.timestamp;
235
+ if (!aggregate.activityTypes.includes(record.activityType)) aggregate.activityTypes.push(record.activityType);
236
+ }
237
+ }
238
+ const summaries = [...aggregates].map(([traceId, aggregate]) => ({
239
+ traceId,
240
+ ...aggregate
241
+ }));
236
242
  summaries.sort((a, b) => b.timestamp.localeCompare(a.timestamp));
237
243
  if (options?.limit != null) return summaries.slice(0, options.limit);
238
244
  return summaries;
@@ -213,6 +213,19 @@ declare class FedifySpanExporter implements SpanExporter {
213
213
  /**
214
214
  * Gets recent traces with summary information.
215
215
  *
216
+ * Each summary is computed directly from the activity records currently
217
+ * stored for each trace, rather than from a separately maintained
218
+ * counter. This guarantees that a trace is discoverable, and its
219
+ * `activityCount` accurate, as soon as its records are stored, even if
220
+ * an earlier attempt at summarizing it (in a previous version of this
221
+ * exporter) never completed, and that the count always reflects
222
+ * records that have not expired. It costs a full scan of every stored
223
+ * record on every call, including full inbound activity JSON. The
224
+ * `limit` option only truncates the returned summaries and does not
225
+ * reduce the scan. Callers that poll this method frequently should
226
+ * account for the total retained data when choosing a TTL and polling
227
+ * interval.
228
+ *
216
229
  * @param options Options for the query.
217
230
  * @returns An array of trace summaries.
218
231
  */
@@ -213,6 +213,19 @@ declare class FedifySpanExporter implements SpanExporter {
213
213
  /**
214
214
  * Gets recent traces with summary information.
215
215
  *
216
+ * Each summary is computed directly from the activity records currently
217
+ * stored for each trace, rather than from a separately maintained
218
+ * counter. This guarantees that a trace is discoverable, and its
219
+ * `activityCount` accurate, as soon as its records are stored, even if
220
+ * an earlier attempt at summarizing it (in a previous version of this
221
+ * exporter) never completed, and that the count always reflects
222
+ * records that have not expired. It costs a full scan of every stored
223
+ * record on every call, including full inbound activity JSON. The
224
+ * `limit` option only truncates the returned summaries and does not
225
+ * reduce the scan. Callers that poll this method frequently should
226
+ * account for the total retained data when choosing a TTL and polling
227
+ * interval.
228
+ *
216
229
  * @param options Options for the query.
217
230
  * @returns An array of trace summaries.
218
231
  */
package/dist/otel/mod.js CHANGED
@@ -181,33 +181,6 @@ var FedifySpanExporter = class {
181
181
  record.spanId
182
182
  ];
183
183
  await this.#kv.set(key, record, options);
184
- await this.#updateTraceSummary(record, options);
185
- }
186
- async #setWithCasRetry(key, transform, options) {
187
- if (this.#kv.cas != null) for (let attempt = 0; attempt < 3; attempt++) {
188
- const existing = await this.#kv.get(key);
189
- const newValue = transform(existing);
190
- if (await this.#kv.cas(key, existing, newValue, options)) return;
191
- }
192
- const newValue = transform(await this.#kv.get(key));
193
- await this.#kv.set(key, newValue, options);
194
- }
195
- async #updateTraceSummary(record, options) {
196
- const summaryKey = [
197
- ...this.#keyPrefix,
198
- "_summaries",
199
- record.traceId
200
- ];
201
- await this.#setWithCasRetry(summaryKey, (existing) => {
202
- const activityCount = existing != null ? existing.activityCount + 1 : 1;
203
- const activityTypes = existing != null ? existing.activityTypes.includes(record.activityType) ? existing.activityTypes : [...existing.activityTypes, record.activityType] : [record.activityType];
204
- return {
205
- traceId: existing?.traceId ?? record.traceId,
206
- timestamp: existing?.timestamp ?? record.timestamp,
207
- activityCount,
208
- activityTypes
209
- };
210
- }, options);
211
184
  }
212
185
  /**
213
186
  * Gets all activity records for a specific trace ID.
@@ -224,13 +197,46 @@ var FedifySpanExporter = class {
224
197
  /**
225
198
  * Gets recent traces with summary information.
226
199
  *
200
+ * Each summary is computed directly from the activity records currently
201
+ * stored for each trace, rather than from a separately maintained
202
+ * counter. This guarantees that a trace is discoverable, and its
203
+ * `activityCount` accurate, as soon as its records are stored, even if
204
+ * an earlier attempt at summarizing it (in a previous version of this
205
+ * exporter) never completed, and that the count always reflects
206
+ * records that have not expired. It costs a full scan of every stored
207
+ * record on every call, including full inbound activity JSON. The
208
+ * `limit` option only truncates the returned summaries and does not
209
+ * reduce the scan. Callers that poll this method frequently should
210
+ * account for the total retained data when choosing a TTL and polling
211
+ * interval.
212
+ *
227
213
  * @param options Options for the query.
228
214
  * @returns An array of trace summaries.
229
215
  */
230
216
  async getRecentTraces(options) {
231
- const summaryPrefix = [...this.#keyPrefix, "_summaries"];
232
- const summaries = [];
233
- for await (const entry of this.#kv.list(summaryPrefix)) summaries.push(entry.value);
217
+ const aggregates = /* @__PURE__ */ new Map();
218
+ const traceIdIndex = this.#keyPrefix.length;
219
+ for await (const entry of this.#kv.list(this.#keyPrefix)) {
220
+ const traceId = entry.key[traceIdIndex];
221
+ if (traceId == null || traceId === "_summaries") continue;
222
+ const record = entry.value;
223
+ if (record == null || typeof record.timestamp !== "string") continue;
224
+ const aggregate = aggregates.get(traceId);
225
+ if (aggregate == null) aggregates.set(traceId, {
226
+ timestamp: record.timestamp,
227
+ activityCount: 1,
228
+ activityTypes: [record.activityType]
229
+ });
230
+ else {
231
+ aggregate.activityCount++;
232
+ if (record.timestamp < aggregate.timestamp) aggregate.timestamp = record.timestamp;
233
+ if (!aggregate.activityTypes.includes(record.activityType)) aggregate.activityTypes.push(record.activityType);
234
+ }
235
+ }
236
+ const summaries = [...aggregates].map(([traceId, aggregate]) => ({
237
+ traceId,
238
+ ...aggregate
239
+ }));
234
240
  summaries.sort((a, b) => b.timestamp.localeCompare(a.timestamp));
235
241
  if (options?.limit != null) return summaries.slice(0, options.limit);
236
242
  return summaries;
@@ -1,8 +1,8 @@
1
1
  import "@js-temporal/polyfill";
2
2
  import "urlpattern-polyfill";
3
3
  globalThis.addEventListener = () => {};
4
- import { n as version, t as name } from "./deno-HyjpIMD2.mjs";
5
- import { n as fetchActorDocument, s as verifyKeyOwnership, t as exportJwk } from "./key-CLtu73m3.mjs";
4
+ import { n as version, t as name } from "./deno-BcnGeGr2.mjs";
5
+ import { n as fetchActorDocument, s as verifyKeyOwnership, t as exportJwk } from "./key-BFzQ3Hef.mjs";
6
6
  import { CryptographicKey, Object as Object$1, isActor } from "@fedify/vocab";
7
7
  import { SpanKind, SpanStatusCode, trace } from "@opentelemetry/api";
8
8
  import { getDocumentLoader } from "@fedify/vocab-runtime";
@@ -1,6 +1,6 @@
1
1
  import { Temporal } from "@js-temporal/polyfill";
2
2
  import { URLPattern } from "urlpattern-polyfill";
3
- import { a as fetchActorDocument, d as name, f as version, i as exportJwk, l as validateCryptoKey, o as fetchKey, u as verifyKeyOwnership } from "./http-B9yJ1OGG.js";
3
+ import { a as fetchActorDocument, d as name, f as version, i as exportJwk, l as validateCryptoKey, o as fetchKey, u as verifyKeyOwnership } from "./http-IYRDhhnT.js";
4
4
  import { getLogger } from "@logtape/logtape";
5
5
  import { Activity, CryptographicKey, DataIntegrityProof, Multikey, Object as Object$1, getTypeId, isActor } from "@fedify/vocab";
6
6
  import { SpanKind, SpanStatusCode, trace } from "@opentelemetry/api";
@@ -1,7 +1,7 @@
1
1
  const { Temporal } = require("@js-temporal/polyfill");
2
2
  const { URLPattern } = require("urlpattern-polyfill");
3
3
  const require_chunk = require("./chunk-DDcVe30Y.cjs");
4
- const require_http = require("./http-B_JXxsg0.cjs");
4
+ const require_http = require("./http-BD9izRp9.cjs");
5
5
  let _logtape_logtape = require("@logtape/logtape");
6
6
  let _fedify_vocab = require("@fedify/vocab");
7
7
  let _opentelemetry_api = require("@opentelemetry/api");
@@ -1,8 +1,8 @@
1
1
  import { Temporal } from "@js-temporal/polyfill";
2
2
  import "urlpattern-polyfill";
3
3
  globalThis.addEventListener = () => {};
4
- import { n as version, t as name } from "./deno-HyjpIMD2.mjs";
5
- import { o as validateCryptoKey, r as fetchKey } from "./key-CLtu73m3.mjs";
4
+ import { n as version, t as name } from "./deno-BcnGeGr2.mjs";
5
+ import { o as validateCryptoKey, r as fetchKey } from "./key-BFzQ3Hef.mjs";
6
6
  import { Activity, DataIntegrityProof, Multikey, getTypeId } from "@fedify/vocab";
7
7
  import { SpanStatusCode, trace } from "@opentelemetry/api";
8
8
  import { getLogger } from "@logtape/logtape";
@@ -1,8 +1,8 @@
1
1
  import "@js-temporal/polyfill";
2
2
  import "urlpattern-polyfill";
3
3
  globalThis.addEventListener = () => {};
4
- import { n as version, t as name } from "./deno-HyjpIMD2.mjs";
5
- import { n as doubleKnock } from "./http-xlEvsjp2.mjs";
4
+ import { n as version, t as name } from "./deno-BcnGeGr2.mjs";
5
+ import { n as doubleKnock } from "./http-D0cjeL8a.mjs";
6
6
  import { SpanKind, SpanStatusCode, trace } from "@opentelemetry/api";
7
7
  import { FetchError, UrlError, validatePublicUrl } from "@fedify/vocab-runtime";
8
8
  import { getLogger } from "@logtape/logtape";
@@ -7,8 +7,8 @@ import { i as assertExists, t as assertStringIncludes } from "../std__assert-Dfl
7
7
  import { r as assertFalse, t as assertThrows } from "../assert_throws-D4DNWrux.mjs";
8
8
  import { t as assert } from "../assert-BhQd8QHP.mjs";
9
9
  import { t as assertRejects } from "../assert_rejects-WyYSTDDX.mjs";
10
- import { t as exportJwk } from "../key-CLtu73m3.mjs";
11
- import { a as parseRfc9421Signature, c as timingSafeEqual, i as formatRfc9421SignatureParameters, l as verifyRequest, n as doubleKnock, o as parseRfc9421SignatureInput, r as formatRfc9421Signature, s as signRequest, t as createRfc9421SignatureBase } from "../http-xlEvsjp2.mjs";
10
+ import { t as exportJwk } from "../key-BFzQ3Hef.mjs";
11
+ import { a as parseRfc9421Signature, c as timingSafeEqual, i as formatRfc9421SignatureParameters, l as verifyRequest, n as doubleKnock, o as parseRfc9421SignatureInput, r as formatRfc9421Signature, s as signRequest, t as createRfc9421SignatureBase } from "../http-D0cjeL8a.mjs";
12
12
  import { i as rsaPrivateKey2, l as rsaPublicKey5, o as rsaPublicKey1, s as rsaPublicKey2 } from "../keys-CCxRJhWa.mjs";
13
13
  import { mockDocumentLoader, test } from "@fedify/fixture";
14
14
  import { FetchError, exportSpki } from "@fedify/vocab-runtime";
@@ -6,9 +6,9 @@ import { n as assertStrictEquals } from "../std__assert-DflMZha5.mjs";
6
6
  import { r as assertFalse, t as assertThrows } from "../assert_throws-D4DNWrux.mjs";
7
7
  import { t as assert } from "../assert-BhQd8QHP.mjs";
8
8
  import { t as assertRejects } from "../assert_rejects-WyYSTDDX.mjs";
9
- import { a as importJwk, i as generateCryptoKeyPair, o as validateCryptoKey, r as fetchKey, t as exportJwk } from "../key-CLtu73m3.mjs";
9
+ import { a as importJwk, i as generateCryptoKeyPair, o as validateCryptoKey, r as fetchKey, t as exportJwk } from "../key-BFzQ3Hef.mjs";
10
10
  import { c as rsaPublicKey3, i as rsaPrivateKey2, o as rsaPublicKey1, s as rsaPublicKey2, t as ed25519Multikey } from "../keys-CCxRJhWa.mjs";
11
- import { g as wrapContextLoaderForJsonLd } from "../ld-CwZHzx6E.mjs";
11
+ import { g as wrapContextLoaderForJsonLd } from "../ld-D2bkYBIz.mjs";
12
12
  import { mockDocumentLoader, test } from "@fedify/fixture";
13
13
  import { CryptographicKey, Multikey, Person } from "@fedify/vocab";
14
14
  import { FetchError, UrlError, getDocumentLoader } from "@fedify/vocab-runtime";
@@ -5,9 +5,9 @@ import { t as assertEquals } from "../assert_equals-Ew3jOFa3.mjs";
5
5
  import { r as assertFalse, t as assertThrows } from "../assert_throws-D4DNWrux.mjs";
6
6
  import { n as assertInstanceOf, t as assert } from "../assert-BhQd8QHP.mjs";
7
7
  import { t as assertRejects } from "../assert_rejects-WyYSTDDX.mjs";
8
- import { i as generateCryptoKeyPair } from "../key-CLtu73m3.mjs";
8
+ import { i as generateCryptoKeyPair } from "../key-BFzQ3Hef.mjs";
9
9
  import { a as rsaPrivateKey3, c as rsaPublicKey3, i as rsaPrivateKey2, n as ed25519PrivateKey, s as rsaPublicKey2, t as ed25519Multikey } from "../keys-CCxRJhWa.mjs";
10
- import { a as compactJsonLd, d as isInvalidUrlTypeError, f as signJsonLd, h as verifySignature, i as attachSignature, m as verifyJsonLd, n as UnsafeJsonLdError, o as createSignature, s as detachSignature } from "../ld-CwZHzx6E.mjs";
10
+ import { a as compactJsonLd, d as isInvalidUrlTypeError, f as signJsonLd, h as verifySignature, i as attachSignature, m as verifyJsonLd, n as UnsafeJsonLdError, o as createSignature, s as detachSignature } from "../ld-D2bkYBIz.mjs";
11
11
  import { mockDocumentLoader, test } from "@fedify/fixture";
12
12
  import { CryptographicKey } from "@fedify/vocab";
13
13
  import { encodeBase64 } from "byte-encodings/base64";
package/dist/sig/mod.cjs CHANGED
@@ -1,8 +1,8 @@
1
1
  const { Temporal } = require("@js-temporal/polyfill");
2
2
  const { URLPattern } = require("urlpattern-polyfill");
3
3
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
4
- const require_http = require("../http-B_JXxsg0.cjs");
5
- const require_proof = require("../proof-BIaI6Y0n.cjs");
4
+ const require_http = require("../http-BD9izRp9.cjs");
5
+ const require_proof = require("../proof-DfmHAmtr.cjs");
6
6
  exports.attachSignature = require_proof.attachSignature;
7
7
  exports.createProof = require_proof.createProof;
8
8
  exports.createSignature = require_proof.createSignature;