@fedify/fedify 2.1.29 → 2.1.31

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 (86) hide show
  1. package/dist/{builder-Mg2G3qj8.mjs → builder-isMRh_I6.mjs} +3 -3
  2. package/dist/{client-CAM_bQXx.d.cts → client-BIBzqEIp.d.cts} +0 -1
  3. package/dist/{client-CjDMsn4a.mjs → client-BZr7TGyu.mjs} +113 -64
  4. package/dist/{client-CSddvgWN.d.ts → client-jE7oGvRh.d.ts} +0 -1
  5. package/dist/compat/mod.d.cts +1 -1
  6. package/dist/compat/mod.d.ts +1 -1
  7. package/dist/compat/transformers.test.mjs +1 -1
  8. package/dist/{context-DLLaDau8.d.ts → context-C0uGVzgk.d.ts} +28 -2
  9. package/dist/{context-BNwv9Q1v.d.cts → context-CLaLxi49.d.cts} +28 -2
  10. package/dist/{deno-BiYv5RT5.mjs → deno-CiaJqTcu.mjs} +1 -1
  11. package/dist/docloader-DOrwedT7.mjs +124 -0
  12. package/dist/federation/builder.test.mjs +1 -1
  13. package/dist/federation/handler.test.mjs +6 -6
  14. package/dist/federation/idempotency.test.mjs +2 -2
  15. package/dist/federation/inbox.test.mjs +1 -1
  16. package/dist/federation/keycache.test.mjs +18 -7
  17. package/dist/federation/middleware.test.mjs +380 -24
  18. package/dist/federation/mod.cjs +1 -1
  19. package/dist/federation/mod.d.cts +2 -2
  20. package/dist/federation/mod.d.ts +2 -2
  21. package/dist/federation/mod.js +1 -1
  22. package/dist/federation/send.test.mjs +445 -70
  23. package/dist/federation/temporal.test.mjs +1 -1
  24. package/dist/federation/webfinger.test.mjs +1 -1
  25. package/dist/{http-Dl00HXr4.d.ts → http-9IFUxdkp.d.cts} +1 -2
  26. package/dist/{http-C1ga9WQu.mjs → http-A9mP2uM7.mjs} +194 -96
  27. package/dist/{http-Cginz7L0.cjs → http-CRiVlZd5.cjs} +188 -105
  28. package/dist/{http-B3rSb2GQ.js → http-Crml3gei.js} +187 -104
  29. package/dist/{http-DqZhyEso.d.cts → http-SbfezERn.d.ts} +1 -2
  30. package/dist/{inbox-BWzL0xMU.mjs → inbox-KESLudq4.mjs} +1 -1
  31. package/dist/{key-O-ATwIGS.mjs → key-Dg6-DV_O.mjs} +17 -3
  32. package/dist/{keycache-RFlZObU3.mjs → keycache-DahCaG_m.mjs} +1 -1
  33. package/dist/{kv-cache-BdylAE9w.cjs → kv-cache-Cqmm-yQa.cjs} +59 -15
  34. package/dist/{kv-cache-8Kg2CHiD.js → kv-cache-DQcyZL5T.js} +60 -16
  35. package/dist/{ld-C-fCGM-z.mjs → ld-COjDN6Nb.mjs} +2 -2
  36. package/dist/{middleware-CVRFS7PZ.cjs → middleware-BbgR7xOI.cjs} +185 -60
  37. package/dist/{middleware-VvrqPwHL.cjs → middleware-BtDQUCcz.cjs} +1 -1
  38. package/dist/{middleware-C9UCWOqw.mjs → middleware-COFlKeYn.mjs} +79 -39
  39. package/dist/{middleware-CxgFiKI3.mjs → middleware-CuQvczZd.mjs} +1 -1
  40. package/dist/{middleware-41MkquTV.js → middleware-DIW5S_5L.js} +185 -60
  41. package/dist/{mod-DNEgTI8M.d.ts → mod-BRnwh2oy.d.ts} +2 -7
  42. package/dist/{mod-BLDHhuW0.d.ts → mod-C7w-QVUv.d.ts} +1 -1
  43. package/dist/{mod-CWJNbyju.d.ts → mod-CFRocFeQ.d.ts} +1 -1
  44. package/dist/{mod-BmlfRBmU.d.cts → mod-CGUH8Ujp.d.cts} +1 -1
  45. package/dist/{mod-Cn9GJ5ex.d.cts → mod-Co7LEK9X.d.cts} +2 -7
  46. package/dist/{mod-DiuFTYUt.d.cts → mod-LOEOKbYd.d.cts} +1 -1
  47. package/dist/mod.cjs +5 -5
  48. package/dist/mod.d.cts +6 -6
  49. package/dist/mod.d.ts +6 -6
  50. package/dist/mod.js +5 -5
  51. package/dist/nodeinfo/client.test.mjs +337 -19
  52. package/dist/nodeinfo/handler.test.mjs +1 -1
  53. package/dist/nodeinfo/mod.cjs +1 -1
  54. package/dist/nodeinfo/mod.d.cts +1 -1
  55. package/dist/nodeinfo/mod.d.ts +1 -1
  56. package/dist/nodeinfo/mod.js +1 -1
  57. package/dist/otel/exporter.test.mjs +292 -30
  58. package/dist/otel/mod.cjs +36 -30
  59. package/dist/otel/mod.d.cts +13 -0
  60. package/dist/otel/mod.d.ts +13 -0
  61. package/dist/otel/mod.js +36 -30
  62. package/dist/{owner-C8IVASD3.mjs → owner-CFHq61cq.mjs} +2 -2
  63. package/dist/{proof-BbHMAkPG.cjs → proof-0JkSC_pT.cjs} +1 -1
  64. package/dist/{proof-cGfRPTiV.js → proof-DzksJFK7.js} +1 -1
  65. package/dist/{proof-CUdcBgvg.mjs → proof-KSHlbPCG.mjs} +2 -2
  66. package/dist/{send-BoV4dyVO.mjs → send-DaDe2YCb.mjs} +117 -32
  67. package/dist/sig/http.test.mjs +507 -3
  68. package/dist/sig/key.test.mjs +197 -3
  69. package/dist/sig/ld.test.mjs +47 -2
  70. package/dist/sig/mod.cjs +2 -2
  71. package/dist/sig/mod.d.cts +2 -2
  72. package/dist/sig/mod.d.ts +2 -2
  73. package/dist/sig/mod.js +2 -2
  74. package/dist/sig/owner.test.mjs +46 -1
  75. package/dist/sig/proof.test.mjs +52 -1
  76. package/dist/{temporal-BJlnku_d.mjs → temporal-DNGH8o2E.mjs} +1 -1
  77. package/dist/testing/mod.d.mts +26 -1
  78. package/dist/{types-miAYfiGO.js → types-B1_pRFE_.js} +112 -63
  79. package/dist/{types-DRPjmHVV.cjs → types-DOvyymM4.cjs} +111 -62
  80. package/dist/utils/docloader.test.mjs +380 -32
  81. package/dist/utils/mod.cjs +1 -1
  82. package/dist/utils/mod.d.cts +1 -1
  83. package/dist/utils/mod.d.ts +1 -1
  84. package/dist/utils/mod.js +1 -1
  85. package/package.json +7 -7
  86. package/dist/docloader-DrxXBFL8.mjs +0 -80
@@ -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-HvthQAHr.mjs";
7
8
  import { test } from "@fedify/fixture";
8
9
  import { SpanKind, SpanStatusCode, TraceFlags } from "@opentelemetry/api";
@@ -193,33 +194,6 @@ var FedifySpanExporter = class {
193
194
  record.spanId
194
195
  ];
195
196
  await this.#kv.set(key, record, options);
196
- await this.#updateTraceSummary(record, options);
197
- }
198
- async #setWithCasRetry(key, transform, options) {
199
- if (this.#kv.cas != null) for (let attempt = 0; attempt < 3; attempt++) {
200
- const existing = await this.#kv.get(key);
201
- const newValue = transform(existing);
202
- if (await this.#kv.cas(key, existing, newValue, options)) return;
203
- }
204
- const newValue = transform(await this.#kv.get(key));
205
- await this.#kv.set(key, newValue, options);
206
- }
207
- async #updateTraceSummary(record, options) {
208
- const summaryKey = [
209
- ...this.#keyPrefix,
210
- "_summaries",
211
- record.traceId
212
- ];
213
- await this.#setWithCasRetry(summaryKey, (existing) => {
214
- const activityCount = existing != null ? existing.activityCount + 1 : 1;
215
- const activityTypes = existing != null ? existing.activityTypes.includes(record.activityType) ? existing.activityTypes : [...existing.activityTypes, record.activityType] : [record.activityType];
216
- return {
217
- traceId: existing?.traceId ?? record.traceId,
218
- timestamp: existing?.timestamp ?? record.timestamp,
219
- activityCount,
220
- activityTypes
221
- };
222
- }, options);
223
197
  }
224
198
  /**
225
199
  * Gets all activity records for a specific trace ID.
@@ -236,13 +210,46 @@ var FedifySpanExporter = class {
236
210
  /**
237
211
  * Gets recent traces with summary information.
238
212
  *
213
+ * Each summary is computed directly from the activity records currently
214
+ * stored for each trace, rather than from a separately maintained
215
+ * counter. This guarantees that a trace is discoverable, and its
216
+ * `activityCount` accurate, as soon as its records are stored, even if
217
+ * an earlier attempt at summarizing it (in a previous version of this
218
+ * exporter) never completed, and that the count always reflects
219
+ * records that have not expired. It costs a full scan of every stored
220
+ * record on every call, including full inbound activity JSON. The
221
+ * `limit` option only truncates the returned summaries and does not
222
+ * reduce the scan. Callers that poll this method frequently should
223
+ * account for the total retained data when choosing a TTL and polling
224
+ * interval.
225
+ *
239
226
  * @param options Options for the query.
240
227
  * @returns An array of trace summaries.
241
228
  */
242
229
  async getRecentTraces(options) {
243
- const summaryPrefix = [...this.#keyPrefix, "_summaries"];
244
- const summaries = [];
245
- for await (const entry of this.#kv.list(summaryPrefix)) summaries.push(entry.value);
230
+ const aggregates = /* @__PURE__ */ new Map();
231
+ const traceIdIndex = this.#keyPrefix.length;
232
+ for await (const entry of this.#kv.list(this.#keyPrefix)) {
233
+ const traceId = entry.key[traceIdIndex];
234
+ if (traceId == null || traceId === "_summaries") continue;
235
+ const record = entry.value;
236
+ if (record == null || typeof record.timestamp !== "string") continue;
237
+ const aggregate = aggregates.get(traceId);
238
+ if (aggregate == null) aggregates.set(traceId, {
239
+ timestamp: record.timestamp,
240
+ activityCount: 1,
241
+ activityTypes: [record.activityType]
242
+ });
243
+ else {
244
+ aggregate.activityCount++;
245
+ if (record.timestamp < aggregate.timestamp) aggregate.timestamp = record.timestamp;
246
+ if (!aggregate.activityTypes.includes(record.activityType)) aggregate.activityTypes.push(record.activityType);
247
+ }
248
+ }
249
+ const summaries = [...aggregates].map(([traceId, aggregate]) => ({
250
+ traceId,
251
+ ...aggregate
252
+ }));
246
253
  summaries.sort((a, b) => b.timestamp.localeCompare(a.timestamp));
247
254
  if (options?.limit != null) return summaries.slice(0, options.limit);
248
255
  return summaries;
@@ -506,6 +513,261 @@ test("FedifySpanExporter", async (t) => {
506
513
  }
507
514
  assertEquals((await exporter.getRecentTraces()).length >= 3, true);
508
515
  });
516
+ function createOutboundSpan(traceId, spanId) {
517
+ return createMockSpan({
518
+ traceId,
519
+ spanId,
520
+ name: "activitypub.send_activity",
521
+ events: [createActivitySentEvent({
522
+ activityJson: JSON.stringify({
523
+ "@context": "https://www.w3.org/ns/activitystreams",
524
+ type: "Create",
525
+ id: `https://local.example/activities/${spanId}`
526
+ }),
527
+ inboxUrl: "https://follower.example/inbox"
528
+ })]
529
+ });
530
+ }
531
+ async function exportSpans(exporter, spans) {
532
+ assertEquals((await new Promise((resolve) => {
533
+ exporter.export(spans, resolve);
534
+ })).code, 0);
535
+ }
536
+ for (const withCas of [true, false]) {
537
+ const createStore = () => {
538
+ const memory = new MemoryKvStore();
539
+ return withCas ? memory : {
540
+ get: memory.get.bind(memory),
541
+ set: memory.set.bind(memory),
542
+ delete: memory.delete.bind(memory),
543
+ list: memory.list.bind(memory)
544
+ };
545
+ };
546
+ await t.step(`re-export counts each retained span once (CAS=${withCas})`, async () => {
547
+ const kv = createStore();
548
+ const exporters = [new FedifySpanExporter(kv), new FedifySpanExporter(kv)];
549
+ const span = createOutboundSpan("reexport-trace", "span-a");
550
+ await exportSpans(exporters[0], [span]);
551
+ await exportSpans(exporters[0], [span]);
552
+ assertEquals((await exporters[1].getRecentTraces())[0].activityCount, 1);
553
+ await Promise.all(exporters.map((exporter) => exportSpans(exporter, [span])));
554
+ assertEquals((await exporters[0].getActivitiesByTraceId("reexport-trace")).length, 1);
555
+ assertEquals((await exporters[1].getRecentTraces())[0].activityCount, 1);
556
+ });
557
+ await t.step(`concurrent distinct spans are all counted (CAS=${withCas})`, async () => {
558
+ const kv = createStore();
559
+ const exporters = [new FedifySpanExporter(kv), new FedifySpanExporter(kv)];
560
+ await Promise.all(exporters.map((exporter, i) => exportSpans(exporter, [createOutboundSpan("concurrent-count-trace", `span-${i}`)])));
561
+ const reader = new FedifySpanExporter(kv);
562
+ assertEquals((await reader.getActivitiesByTraceId("concurrent-count-trace")).length, 2);
563
+ assertEquals((await reader.getRecentTraces())[0].activityCount, 2);
564
+ });
565
+ await t.step(`legacy summaries cannot hide or inflate retained records (CAS=${withCas})`, async () => {
566
+ const kv = createStore();
567
+ const exporter = new FedifySpanExporter(kv);
568
+ await exportSpans(exporter, [createOutboundSpan("legacy-trace", "span-a")]);
569
+ await kv.delete([
570
+ "fedify",
571
+ "traces",
572
+ "_summaries",
573
+ "legacy-trace"
574
+ ]);
575
+ const originalSet = kv.set.bind(kv);
576
+ let writes = 0;
577
+ kv.set = (...args) => {
578
+ writes++;
579
+ return originalSet(...args);
580
+ };
581
+ assertEquals((await exporter.getRecentTraces())[0].activityCount, 1);
582
+ assertEquals(writes, 0);
583
+ const stale = {
584
+ traceId: "legacy-trace",
585
+ timestamp: "2000-01-01T00:00:00.000Z",
586
+ activityCount: 99,
587
+ activityTypes: ["Bogus"]
588
+ };
589
+ await kv.set([
590
+ "fedify",
591
+ "traces",
592
+ "_summaries",
593
+ "legacy-trace"
594
+ ], stale);
595
+ await kv.set([
596
+ "fedify",
597
+ "traces",
598
+ "_summaries",
599
+ "ghost-trace"
600
+ ], {
601
+ ...stale,
602
+ traceId: "ghost-trace"
603
+ });
604
+ const records = await exporter.getActivitiesByTraceId("legacy-trace");
605
+ assertEquals(await exporter.getRecentTraces(), [{
606
+ traceId: "legacy-trace",
607
+ timestamp: records[0].timestamp,
608
+ activityCount: 1,
609
+ activityTypes: ["Create"]
610
+ }]);
611
+ await kv.delete([
612
+ "fedify",
613
+ "traces",
614
+ "legacy-trace",
615
+ "span-a"
616
+ ]);
617
+ assertEquals(await exporter.getRecentTraces(), []);
618
+ });
619
+ await t.step(`summary metadata follows retained records (CAS=${withCas})`, async () => {
620
+ const kv = createStore();
621
+ const exporter = new FedifySpanExporter(kv, { keyPrefix: ["custom"] });
622
+ function span(traceId, spanId, type, seconds) {
623
+ return createMockSpan({
624
+ traceId,
625
+ spanId,
626
+ events: [{
627
+ ...createActivitySentEvent({
628
+ activityJson: JSON.stringify({ type }),
629
+ inboxUrl: "https://example.com/inbox"
630
+ }),
631
+ time: [seconds, 0]
632
+ }]
633
+ });
634
+ }
635
+ await exportSpans(exporter, [
636
+ span("older-trace", "late", "Like", 1700000002),
637
+ span("older-trace", "early", "Create", 17e8),
638
+ span("older-trace", "duplicate-type", "Like", 1700000003),
639
+ span("newer-trace", "only", "Follow", 1700000001)
640
+ ]);
641
+ const recent = await exporter.getRecentTraces();
642
+ assertEquals(recent.map((summary) => summary.traceId), ["newer-trace", "older-trace"]);
643
+ assertEquals(recent[1], {
644
+ traceId: "older-trace",
645
+ timestamp: (/* @__PURE__ */ new Date(17e11)).toISOString(),
646
+ activityCount: 3,
647
+ activityTypes: ["Like", "Create"]
648
+ });
649
+ assertEquals(await exporter.getRecentTraces({ limit: 1 }), [recent[0]]);
650
+ await kv.delete([
651
+ "custom",
652
+ "older-trace",
653
+ "early"
654
+ ]);
655
+ assertEquals((await exporter.getRecentTraces())[0], {
656
+ traceId: "older-trace",
657
+ timestamp: (/* @__PURE__ */ new Date(1700000002e3)).toISOString(),
658
+ activityCount: 2,
659
+ activityTypes: ["Like"]
660
+ });
661
+ });
662
+ }
663
+ await t.step("a trace with only one record is discoverable via getRecentTraces(), with no further write", async () => {
664
+ const exporter = new FedifySpanExporter(new MemoryKvStore());
665
+ const traceId = "first-write-discoverable-trace";
666
+ await new Promise((resolve) => {
667
+ exporter.export([createOutboundSpan(traceId, "span-a")], () => resolve());
668
+ });
669
+ assertEquals((await exporter.getRecentTraces()).find((s) => s.traceId === traceId)?.activityCount, 1);
670
+ });
671
+ await t.step("getRecentTraces() needs no additional writes to discover a trace or correct its activityCount", async () => {
672
+ const kv = new MemoryKvStore();
673
+ const exporter = new FedifySpanExporter(kv);
674
+ const traceId = "query-time-correction-trace";
675
+ await new Promise((resolve) => {
676
+ exporter.export([createOutboundSpan(traceId, "span-a")], () => resolve());
677
+ });
678
+ await new Promise((resolve) => {
679
+ exporter.export([createOutboundSpan(traceId, "span-b")], () => resolve());
680
+ });
681
+ const originalSet = kv.set.bind(kv);
682
+ let writeCalls = 0;
683
+ kv.set = (key, value, options) => {
684
+ writeCalls++;
685
+ return originalSet(key, value, options);
686
+ };
687
+ assertEquals((await exporter.getRecentTraces()).find((s) => s.traceId === traceId)?.activityCount, 2);
688
+ assertEquals(writeCalls, 0);
689
+ });
690
+ await t.step("getRecentTraces() reflects only currently-retained records, e.g., after some have expired", async () => {
691
+ const kv = new MemoryKvStore();
692
+ const exporter = new FedifySpanExporter(kv);
693
+ const traceId = "expiry-reflects-retained-trace";
694
+ await new Promise((resolve) => {
695
+ exporter.export([createOutboundSpan(traceId, "span-a"), createOutboundSpan(traceId, "span-b")], () => resolve());
696
+ });
697
+ let recent = await exporter.getRecentTraces();
698
+ assertEquals(recent.find((s) => s.traceId === traceId)?.activityCount, 2);
699
+ await kv.delete([
700
+ "fedify",
701
+ "traces",
702
+ traceId,
703
+ "span-a"
704
+ ]);
705
+ recent = await exporter.getRecentTraces();
706
+ assertEquals(recent.find((s) => s.traceId === traceId)?.activityCount, 1);
707
+ });
708
+ 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 () => {
709
+ const kv = new MemoryKvStore();
710
+ const exporter = new FedifySpanExporter(kv);
711
+ const traceCount = 20;
712
+ const recordsPerTrace = 5;
713
+ for (let i = 0; i < traceCount; i++) {
714
+ const traceId = `poll-cost-trace-${i}`;
715
+ const spans = Array.from({ length: recordsPerTrace }, (_, j) => createOutboundSpan(traceId, `span-${j}`));
716
+ await new Promise((resolve) => {
717
+ exporter.export(spans, () => resolve());
718
+ });
719
+ }
720
+ const originalList = kv.list.bind(kv);
721
+ let entriesRead = 0;
722
+ let bytesRead = 0;
723
+ kv.list = async function* (prefix) {
724
+ for await (const entry of originalList(prefix)) {
725
+ entriesRead++;
726
+ bytesRead += new TextEncoder().encode(JSON.stringify(entry.value)).byteLength;
727
+ yield entry;
728
+ }
729
+ };
730
+ assertEquals((await exporter.getRecentTraces()).length, traceCount);
731
+ assertEquals(entriesRead, traceCount * recordsPerTrace);
732
+ assert(bytesRead > 0);
733
+ const entriesReadForFullScan = entriesRead;
734
+ const bytesReadForFullScan = bytesRead;
735
+ entriesRead = 0;
736
+ bytesRead = 0;
737
+ assertEquals((await exporter.getRecentTraces({ limit: 3 })).length, 3);
738
+ assertEquals(entriesRead, entriesReadForFullScan);
739
+ assertEquals(bytesRead, bytesReadForFullScan);
740
+ });
741
+ await t.step("recent-list scans include full inbound payloads even with limit", async () => {
742
+ const kv = new MemoryKvStore();
743
+ const exporter = new FedifySpanExporter(kv);
744
+ const activityJson = JSON.stringify({
745
+ type: "Create",
746
+ object: {
747
+ type: "Note",
748
+ content: "한".repeat(64 * 1024)
749
+ }
750
+ });
751
+ await exportSpans(exporter, Array.from({ length: 10 }, (_, i) => createMockSpan({
752
+ traceId: `large-inbound-${i}`,
753
+ spanId: "inbound",
754
+ events: [createActivityReceivedEvent({ activityJson })]
755
+ })));
756
+ const originalList = kv.list.bind(kv);
757
+ let entriesRead = 0;
758
+ let bytesRead = 0;
759
+ kv.list = async function* (prefix) {
760
+ for await (const entry of originalList(prefix)) {
761
+ entriesRead++;
762
+ bytesRead += new TextEncoder().encode(JSON.stringify(entry.value)).byteLength;
763
+ yield entry;
764
+ }
765
+ };
766
+ assertEquals((await exporter.getRecentTraces({ limit: 1 })).length, 1);
767
+ assertEquals(entriesRead, 10);
768
+ assert(bytesRead >= new TextEncoder().encode(activityJson).byteLength * 10);
769
+ assert(bytesRead > 1.8 * 1024 * 1024);
770
+ });
509
771
  await t.step("forceFlush() returns resolved promise", async () => {
510
772
  await new FedifySpanExporter(new MemoryKvStore()).forceFlush();
511
773
  });
package/dist/otel/mod.cjs CHANGED
@@ -189,33 +189,6 @@ var FedifySpanExporter = class {
189
189
  record.spanId
190
190
  ];
191
191
  await this.#kv.set(key, record, options);
192
- await this.#updateTraceSummary(record, options);
193
- }
194
- async #setWithCasRetry(key, transform, options) {
195
- if (this.#kv.cas != null) for (let attempt = 0; attempt < 3; attempt++) {
196
- const existing = await this.#kv.get(key);
197
- const newValue = transform(existing);
198
- if (await this.#kv.cas(key, existing, newValue, options)) return;
199
- }
200
- const newValue = transform(await this.#kv.get(key));
201
- await this.#kv.set(key, newValue, options);
202
- }
203
- async #updateTraceSummary(record, options) {
204
- const summaryKey = [
205
- ...this.#keyPrefix,
206
- "_summaries",
207
- record.traceId
208
- ];
209
- await this.#setWithCasRetry(summaryKey, (existing) => {
210
- const activityCount = existing != null ? existing.activityCount + 1 : 1;
211
- const activityTypes = existing != null ? existing.activityTypes.includes(record.activityType) ? existing.activityTypes : [...existing.activityTypes, record.activityType] : [record.activityType];
212
- return {
213
- traceId: existing?.traceId ?? record.traceId,
214
- timestamp: existing?.timestamp ?? record.timestamp,
215
- activityCount,
216
- activityTypes
217
- };
218
- }, options);
219
192
  }
220
193
  /**
221
194
  * Gets all activity records for a specific trace ID.
@@ -232,13 +205,46 @@ var FedifySpanExporter = class {
232
205
  /**
233
206
  * Gets recent traces with summary information.
234
207
  *
208
+ * Each summary is computed directly from the activity records currently
209
+ * stored for each trace, rather than from a separately maintained
210
+ * counter. This guarantees that a trace is discoverable, and its
211
+ * `activityCount` accurate, as soon as its records are stored, even if
212
+ * an earlier attempt at summarizing it (in a previous version of this
213
+ * exporter) never completed, and that the count always reflects
214
+ * records that have not expired. It costs a full scan of every stored
215
+ * record on every call, including full inbound activity JSON. The
216
+ * `limit` option only truncates the returned summaries and does not
217
+ * reduce the scan. Callers that poll this method frequently should
218
+ * account for the total retained data when choosing a TTL and polling
219
+ * interval.
220
+ *
235
221
  * @param options Options for the query.
236
222
  * @returns An array of trace summaries.
237
223
  */
238
224
  async getRecentTraces(options) {
239
- const summaryPrefix = [...this.#keyPrefix, "_summaries"];
240
- const summaries = [];
241
- for await (const entry of this.#kv.list(summaryPrefix)) summaries.push(entry.value);
225
+ const aggregates = /* @__PURE__ */ new Map();
226
+ const traceIdIndex = this.#keyPrefix.length;
227
+ for await (const entry of this.#kv.list(this.#keyPrefix)) {
228
+ const traceId = entry.key[traceIdIndex];
229
+ if (traceId == null || traceId === "_summaries") continue;
230
+ const record = entry.value;
231
+ if (record == null || typeof record.timestamp !== "string") continue;
232
+ const aggregate = aggregates.get(traceId);
233
+ if (aggregate == null) aggregates.set(traceId, {
234
+ timestamp: record.timestamp,
235
+ activityCount: 1,
236
+ activityTypes: [record.activityType]
237
+ });
238
+ else {
239
+ aggregate.activityCount++;
240
+ if (record.timestamp < aggregate.timestamp) aggregate.timestamp = record.timestamp;
241
+ if (!aggregate.activityTypes.includes(record.activityType)) aggregate.activityTypes.push(record.activityType);
242
+ }
243
+ }
244
+ const summaries = [...aggregates].map(([traceId, aggregate]) => ({
245
+ traceId,
246
+ ...aggregate
247
+ }));
242
248
  summaries.sort((a, b) => b.timestamp.localeCompare(a.timestamp));
243
249
  if (options?.limit != null) return summaries.slice(0, options.limit);
244
250
  return summaries;
@@ -225,6 +225,19 @@ declare class FedifySpanExporter implements SpanExporter {
225
225
  /**
226
226
  * Gets recent traces with summary information.
227
227
  *
228
+ * Each summary is computed directly from the activity records currently
229
+ * stored for each trace, rather than from a separately maintained
230
+ * counter. This guarantees that a trace is discoverable, and its
231
+ * `activityCount` accurate, as soon as its records are stored, even if
232
+ * an earlier attempt at summarizing it (in a previous version of this
233
+ * exporter) never completed, and that the count always reflects
234
+ * records that have not expired. It costs a full scan of every stored
235
+ * record on every call, including full inbound activity JSON. The
236
+ * `limit` option only truncates the returned summaries and does not
237
+ * reduce the scan. Callers that poll this method frequently should
238
+ * account for the total retained data when choosing a TTL and polling
239
+ * interval.
240
+ *
228
241
  * @param options Options for the query.
229
242
  * @returns An array of trace summaries.
230
243
  */
@@ -225,6 +225,19 @@ declare class FedifySpanExporter implements SpanExporter {
225
225
  /**
226
226
  * Gets recent traces with summary information.
227
227
  *
228
+ * Each summary is computed directly from the activity records currently
229
+ * stored for each trace, rather than from a separately maintained
230
+ * counter. This guarantees that a trace is discoverable, and its
231
+ * `activityCount` accurate, as soon as its records are stored, even if
232
+ * an earlier attempt at summarizing it (in a previous version of this
233
+ * exporter) never completed, and that the count always reflects
234
+ * records that have not expired. It costs a full scan of every stored
235
+ * record on every call, including full inbound activity JSON. The
236
+ * `limit` option only truncates the returned summaries and does not
237
+ * reduce the scan. Callers that poll this method frequently should
238
+ * account for the total retained data when choosing a TTL and polling
239
+ * interval.
240
+ *
228
241
  * @param options Options for the query.
229
242
  * @returns An array of trace summaries.
230
243
  */
package/dist/otel/mod.js CHANGED
@@ -187,33 +187,6 @@ var FedifySpanExporter = class {
187
187
  record.spanId
188
188
  ];
189
189
  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
190
  }
218
191
  /**
219
192
  * Gets all activity records for a specific trace ID.
@@ -230,13 +203,46 @@ var FedifySpanExporter = class {
230
203
  /**
231
204
  * Gets recent traces with summary information.
232
205
  *
206
+ * Each summary is computed directly from the activity records currently
207
+ * stored for each trace, rather than from a separately maintained
208
+ * counter. This guarantees that a trace is discoverable, and its
209
+ * `activityCount` accurate, as soon as its records are stored, even if
210
+ * an earlier attempt at summarizing it (in a previous version of this
211
+ * exporter) never completed, and that the count always reflects
212
+ * records that have not expired. It costs a full scan of every stored
213
+ * record on every call, including full inbound activity JSON. The
214
+ * `limit` option only truncates the returned summaries and does not
215
+ * reduce the scan. Callers that poll this method frequently should
216
+ * account for the total retained data when choosing a TTL and polling
217
+ * interval.
218
+ *
233
219
  * @param options Options for the query.
234
220
  * @returns An array of trace summaries.
235
221
  */
236
222
  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);
223
+ const aggregates = /* @__PURE__ */ new Map();
224
+ const traceIdIndex = this.#keyPrefix.length;
225
+ for await (const entry of this.#kv.list(this.#keyPrefix)) {
226
+ const traceId = entry.key[traceIdIndex];
227
+ if (traceId == null || traceId === "_summaries") continue;
228
+ const record = entry.value;
229
+ if (record == null || typeof record.timestamp !== "string") continue;
230
+ const aggregate = aggregates.get(traceId);
231
+ if (aggregate == null) aggregates.set(traceId, {
232
+ timestamp: record.timestamp,
233
+ activityCount: 1,
234
+ activityTypes: [record.activityType]
235
+ });
236
+ else {
237
+ aggregate.activityCount++;
238
+ if (record.timestamp < aggregate.timestamp) aggregate.timestamp = record.timestamp;
239
+ if (!aggregate.activityTypes.includes(record.activityType)) aggregate.activityTypes.push(record.activityType);
240
+ }
241
+ }
242
+ const summaries = [...aggregates].map(([traceId, aggregate]) => ({
243
+ traceId,
244
+ ...aggregate
245
+ }));
240
246
  summaries.sort((a, b) => b.timestamp.localeCompare(a.timestamp));
241
247
  if (options?.limit != null) return summaries.slice(0, options.limit);
242
248
  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-BiYv5RT5.mjs";
5
- import { c as verifyKeyOwnership, n as fetchActorDocument, t as exportJwk } from "./key-O-ATwIGS.mjs";
4
+ import { n as version, t as name } from "./deno-CiaJqTcu.mjs";
5
+ import { c as verifyKeyOwnership, n as fetchActorDocument, t as exportJwk } from "./key-Dg6-DV_O.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,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-Cginz7L0.cjs");
4
+ const require_http = require("./http-CRiVlZd5.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,6 +1,6 @@
1
1
  import { Temporal } from "@js-temporal/polyfill";
2
2
  import { URLPattern } from "urlpattern-polyfill";
3
- import { c as fetchKey, f as validateCryptoKey, o as exportJwk, p as verifyKeyOwnership, s as fetchActorDocument, v as name, y as version } from "./http-B3rSb2GQ.js";
3
+ import { c as fetchKey, f as validateCryptoKey, o as exportJwk, p as verifyKeyOwnership, s as fetchActorDocument, v as name, y as version } from "./http-Crml3gei.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,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-BiYv5RT5.mjs";
5
- import { r as fetchKey, s as validateCryptoKey } from "./key-O-ATwIGS.mjs";
4
+ import { n as version, t as name } from "./deno-CiaJqTcu.mjs";
5
+ import { r as fetchKey, s as validateCryptoKey } from "./key-Dg6-DV_O.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";