@fedify/fedify 2.3.0-dev.1158 → 2.3.0-dev.1172

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 (48) hide show
  1. package/dist/{builder-B66L9i5E.mjs → builder-JoFBmqfM.mjs} +2 -2
  2. package/dist/compat/transformers.test.mjs +1 -1
  3. package/dist/{deno-O_rwum1q.mjs → deno-Cb_y5qEi.mjs} +1 -1
  4. package/dist/{docloader-Ct8PhKFS.mjs → docloader-Bv4TW6eo.mjs} +2 -2
  5. package/dist/federation/builder.test.mjs +1 -1
  6. package/dist/federation/handler.test.mjs +305 -3
  7. package/dist/federation/idempotency.test.mjs +2 -2
  8. package/dist/federation/metrics.test.mjs +80 -1
  9. package/dist/federation/middleware.test.mjs +20 -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-BoRhhcgB.mjs → http--aE0vk2u.mjs} +3 -3
  16. package/dist/{http-CFP8WMMv.js → http-C0XZv7iH.js} +92 -2
  17. package/dist/{http-DlPd_LYM.cjs → http-D_HNhC57.cjs} +115 -1
  18. package/dist/{key-DyATZSWG.mjs → key-Cl_bixZo.mjs} +2 -2
  19. package/dist/{kv-cache-BJo6COYN.cjs → kv-cache-CdOuPFgC.cjs} +1 -1
  20. package/dist/{kv-cache-DeJE8EeD.mjs → kv-cache-DQUblF4f.mjs} +1 -1
  21. package/dist/{kv-cache-dH0biV98.js → kv-cache-DsbVBK7Y.js} +1 -1
  22. package/dist/{ld-B8wjsKDJ.mjs → ld-xVq6y31b.mjs} +3 -3
  23. package/dist/{metrics-oMUWaw6W.mjs → metrics-CKticT28.mjs} +92 -2
  24. package/dist/{middleware-DwZ1ofL9.js → middleware-BSuEI4Qf.js} +318 -107
  25. package/dist/{middleware-BzOa0ncb.mjs → middleware-BmPIKmb4.mjs} +1 -1
  26. package/dist/{middleware-xtTRaiJL.mjs → middleware-DHM2Pjqf.mjs} +327 -116
  27. package/dist/{middleware-CcJyVEpv.cjs → middleware-hxnyAewn.cjs} +318 -107
  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/{owner-BxjgK8PG.mjs → owner-DmU2qEh_.mjs} +2 -2
  32. package/dist/{proof-42Q9NiqN.mjs → proof-1XBgQ0Z0.mjs} +3 -3
  33. package/dist/{proof-B_6gAVQ2.js → proof-CmS6yxgt.js} +1 -1
  34. package/dist/{proof-CfttNzWW.cjs → proof-wm6UxUoM.cjs} +1 -1
  35. package/dist/{send-R1_K46CH.mjs → send-CmtB8w5D.mjs} +3 -3
  36. package/dist/sig/http.test.mjs +2 -2
  37. package/dist/sig/key.test.mjs +1 -1
  38. package/dist/sig/ld.test.mjs +2 -2
  39. package/dist/sig/mod.cjs +2 -2
  40. package/dist/sig/mod.js +2 -2
  41. package/dist/sig/owner.test.mjs +1 -1
  42. package/dist/sig/proof.test.mjs +1 -1
  43. package/dist/{temporal-8kDX3E4q.mjs → temporal-DE9_a2nI.mjs} +1 -1
  44. package/dist/utils/docloader.test.mjs +2 -2
  45. package/dist/utils/kv-cache.test.mjs +1 -1
  46. package/dist/utils/mod.cjs +1 -1
  47. package/dist/utils/mod.js +1 -1
  48. package/package.json +6 -6
@@ -1,26 +1,26 @@
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-O_rwum1q.mjs";
5
- import { a as instrumentDocumentLoader, d as recordInboxActivity, h as recordWebFingerHandle, i as getRemoteHost, m as recordOutboxEnqueue, n as getDurationMs, o as isAbortError, p as recordOutboxActivity, r as getFederationMetrics, u as recordFanoutRecipients } from "./metrics-oMUWaw6W.mjs";
4
+ import { n as version, t as name } from "./deno-Cb_y5qEi.mjs";
5
+ import { _ as recordOutboxActivity, a as instrumentDocumentLoader, c as recordCollectionDispatchDuration, d as recordCollectionTotalItems, h as recordInboxActivity, i as getRemoteHost, l as recordCollectionPageItems, m as recordFanoutRecipients, n as getDurationMs, o as isAbortError, r as getFederationMetrics, u as recordCollectionRequest, v as recordOutboxEnqueue, y as recordWebFingerHandle } from "./metrics-CKticT28.mjs";
6
6
  import { t as formatAcceptSignature } from "./accept-CceiKpCy.mjs";
7
- import { a as importJwk, o as validateCryptoKey, t as exportJwk } from "./key-DyATZSWG.mjs";
8
- import { l as verifyRequest, o as parseRfc9421SignatureInput, u as verifyRequestDetailed } from "./http-BoRhhcgB.mjs";
9
- import { t as getAuthenticatedDocumentLoader } from "./docloader-Ct8PhKFS.mjs";
10
- import { n as kvCache } from "./kv-cache-DeJE8EeD.mjs";
11
- import { _ as wrapContextLoaderForJsonLd, a as compactJsonLd, c as getNormalizationContextLoader, d as isClearlyMalformedContextReference, f as isInvalidUrlTypeError, l as hasSignature, m as verifyCompactJsonLd, p as signJsonLd, r as assertSafeJsonLd, s as detachSignature, t as InvalidContextReferenceError, u as hasSignatureLike } from "./ld-B8wjsKDJ.mjs";
12
- import { n as getKeyOwner, t as doesActorOwnKey } from "./owner-BxjgK8PG.mjs";
7
+ import { a as importJwk, o as validateCryptoKey, t as exportJwk } from "./key-Cl_bixZo.mjs";
8
+ import { l as verifyRequest, o as parseRfc9421SignatureInput, u as verifyRequestDetailed } from "./http--aE0vk2u.mjs";
9
+ import { t as getAuthenticatedDocumentLoader } from "./docloader-Bv4TW6eo.mjs";
10
+ import { n as kvCache } from "./kv-cache-DQUblF4f.mjs";
11
+ import { _ as wrapContextLoaderForJsonLd, a as compactJsonLd, c as getNormalizationContextLoader, d as isClearlyMalformedContextReference, f as isInvalidUrlTypeError, l as hasSignature, m as verifyCompactJsonLd, p as signJsonLd, r as assertSafeJsonLd, s as detachSignature, t as InvalidContextReferenceError, u as hasSignatureLike } from "./ld-xVq6y31b.mjs";
12
+ import { n as getKeyOwner, t as doesActorOwnKey } from "./owner-DmU2qEh_.mjs";
13
13
  import { r as normalizeOutgoingActivityJsonLd } from "./outgoing-jsonld-BgFLCJQ_.mjs";
14
- import { i as verifyObject, n as hasProofLike, r as signObject } from "./proof-42Q9NiqN.mjs";
14
+ import { i as verifyObject, n as hasProofLike, r as signObject } from "./proof-1XBgQ0Z0.mjs";
15
15
  import { t as getNodeInfo } from "./client-B_A6mfn3.mjs";
16
16
  import { t as nodeInfoToJson } from "./types-BFowWFTT.mjs";
17
- import { n as FederationBuilderImpl, t as ACTOR_ALIAS_PREFIX } from "./builder-B66L9i5E.mjs";
17
+ import { n as FederationBuilderImpl, t as ACTOR_ALIAS_PREFIX } from "./builder-JoFBmqfM.mjs";
18
18
  import { t as buildCollectionSynchronizationHeader } from "./collection-CA3V5zyK.mjs";
19
19
  import { t as KvKeyCache } from "./keycache-BYMd8q7F.mjs";
20
20
  import { t as acceptsJsonLd } from "./negotiation-CDW-_gUU.mjs";
21
- import { t as hasMalformedKnownTemporalLiteral } from "./temporal-8kDX3E4q.mjs";
21
+ import { t as hasMalformedKnownTemporalLiteral } from "./temporal-DE9_a2nI.mjs";
22
22
  import { t as createExponentialBackoffPolicy } from "./retry-_VvV0h9f.mjs";
23
- import { n as extractInboxes, r as sendActivity, t as SendActivityError } from "./send-R1_K46CH.mjs";
23
+ import { n as extractInboxes, r as sendActivity, t as SendActivityError } from "./send-CmtB8w5D.mjs";
24
24
  import { getLogger, withContext } from "@logtape/logtape";
25
25
  import { RouterError } from "@fedify/uri-template";
26
26
  import { Activity, Collection, CollectionPage, CryptographicKey, Link, Multikey, Object as Object$1, OrderedCollection, OrderedCollectionPage, Tombstone, getTypeId, lookupObject, traverseCollection } from "@fedify/vocab";
@@ -402,6 +402,33 @@ async function handleObject(request, { values, context, objectDispatcher, author
402
402
  Vary: "Accept"
403
403
  } });
404
404
  }
405
+ const BUILT_IN_COLLECTION_METRIC_KINDS = new Set([
406
+ "inbox",
407
+ "outbox",
408
+ "following",
409
+ "followers",
410
+ "liked",
411
+ "featured",
412
+ "featured_tags"
413
+ ]);
414
+ function getCollectionMetricKind(name) {
415
+ const normalized = name.trim().replace(/([a-z0-9])([A-Z])/g, "$1_$2").toLowerCase().replace(/\s+/g, "_");
416
+ return BUILT_IN_COLLECTION_METRIC_KINDS.has(normalized) ? normalized : "custom";
417
+ }
418
+ function collectionAttributes(base, result, response) {
419
+ return {
420
+ ...base,
421
+ result,
422
+ ...response == null ? {} : { statusCode: response.status }
423
+ };
424
+ }
425
+ function recordCollectionMetrics(meterProvider, base, result, options = {}) {
426
+ const attrs = collectionAttributes(base, result, options.response);
427
+ recordCollectionRequest(meterProvider, attrs);
428
+ if (options.dispatchDurationMs != null) recordCollectionDispatchDuration(meterProvider, options.dispatchDurationMs, attrs);
429
+ if (options.itemCount != null) recordCollectionPageItems(meterProvider, options.itemCount, attrs);
430
+ if (options.totalItems != null) recordCollectionTotalItems(meterProvider, options.totalItems, attrs);
431
+ }
405
432
  /**
406
433
  * Handles a collection request.
407
434
  * @template TItem The type of items in the collection.
@@ -412,36 +439,118 @@ async function handleObject(request, { values, context, objectDispatcher, author
412
439
  * @param parameters The parameters for handling the collection.
413
440
  * @returns A promise that resolves to an HTTP response.
414
441
  */
415
- async function handleCollection(request, { name: name$1, identifier, uriGetter, filter, filterPredicate, context, collectionCallbacks, tracerProvider, onUnauthorized, onNotFound }) {
442
+ async function handleCollection(request, { name: name$1, identifier, uriGetter, filter, filterPredicate, context, collectionCallbacks, tracerProvider, meterProvider, onUnauthorized, onNotFound }) {
416
443
  const spanName = name$1.trim().replace(/\s+/g, "_");
417
444
  tracerProvider = tracerProvider ?? trace.getTracerProvider();
418
445
  const tracer = tracerProvider.getTracer(name, version);
419
446
  const cursor = new URL(request.url).searchParams.get("cursor");
420
- if (collectionCallbacks == null) return await onNotFound(request);
421
- let collection;
422
- const baseUri = uriGetter(identifier);
423
- if (cursor == null) {
424
- const firstCursor = await collectionCallbacks.firstCursor?.(context, identifier);
425
- const totalItems = filter == null ? await collectionCallbacks.counter?.(context, identifier) : void 0;
426
- if (firstCursor == null) {
427
- const itemsOrResponse = await tracer.startActiveSpan(`activitypub.dispatch_collection ${spanName}`, {
447
+ const metricBase = {
448
+ kind: getCollectionMetricKind(name$1),
449
+ page: cursor != null,
450
+ dispatcher: "built_in"
451
+ };
452
+ let dispatchDurationMs;
453
+ let itemCount;
454
+ let totalItemCount;
455
+ const finish = (response, result) => {
456
+ recordCollectionMetrics(meterProvider, metricBase, result, {
457
+ response,
458
+ dispatchDurationMs,
459
+ itemCount,
460
+ totalItems: totalItemCount
461
+ });
462
+ return response;
463
+ };
464
+ try {
465
+ if (collectionCallbacks == null) return finish(await onNotFound(request), "not_found");
466
+ let collection;
467
+ const baseUri = uriGetter(identifier);
468
+ if (cursor == null) {
469
+ const firstCursor = await collectionCallbacks.firstCursor?.(context, identifier);
470
+ const totalItems = filter == null ? await collectionCallbacks.counter?.(context, identifier) : void 0;
471
+ totalItemCount = totalItems == null ? void 0 : Number(totalItems);
472
+ if (firstCursor == null) {
473
+ const itemsOrResponse = await tracer.startActiveSpan(`activitypub.dispatch_collection ${spanName}`, {
474
+ kind: SpanKind.SERVER,
475
+ attributes: {
476
+ "activitypub.collection.id": baseUri.href,
477
+ "activitypub.collection.type": OrderedCollection.typeId.href
478
+ }
479
+ }, async (span) => {
480
+ if (totalItemCount != null) span.setAttribute("activitypub.collection.total_items", totalItemCount);
481
+ const started = performance.now();
482
+ try {
483
+ const page = await collectionCallbacks.dispatcher(context, identifier, null, filter);
484
+ dispatchDurationMs = getDurationMs(started);
485
+ if (page == null) {
486
+ span.setStatus({ code: SpanStatusCode.ERROR });
487
+ return await onNotFound(request);
488
+ }
489
+ const items = filterCollectionItems(page.items, name$1, filterPredicate);
490
+ itemCount = items.length;
491
+ span.setAttribute("fedify.collection.items", itemCount);
492
+ return items;
493
+ } catch (e) {
494
+ if (dispatchDurationMs == null) dispatchDurationMs = getDurationMs(started);
495
+ span.setStatus({
496
+ code: SpanStatusCode.ERROR,
497
+ message: String(e)
498
+ });
499
+ throw e;
500
+ } finally {
501
+ span.end();
502
+ }
503
+ });
504
+ if (itemsOrResponse instanceof Response) return finish(itemsOrResponse, "not_found");
505
+ collection = new OrderedCollection({
506
+ id: baseUri,
507
+ totalItems: totalItemCount ?? null,
508
+ items: itemsOrResponse
509
+ });
510
+ } else {
511
+ const lastCursor = await collectionCallbacks.lastCursor?.(context, identifier);
512
+ const first = new URL(context.url);
513
+ first.searchParams.set("cursor", firstCursor);
514
+ let last = null;
515
+ if (lastCursor != null) {
516
+ last = new URL(context.url);
517
+ last.searchParams.set("cursor", lastCursor);
518
+ }
519
+ collection = new OrderedCollection({
520
+ id: baseUri,
521
+ totalItems: totalItemCount ?? null,
522
+ first,
523
+ last
524
+ });
525
+ }
526
+ } else {
527
+ const uri = new URL(baseUri);
528
+ uri.searchParams.set("cursor", cursor);
529
+ const pageOrResponse = await tracer.startActiveSpan(`activitypub.dispatch_collection_page ${name$1}`, {
428
530
  kind: SpanKind.SERVER,
429
531
  attributes: {
430
- "activitypub.collection.id": baseUri.href,
431
- "activitypub.collection.type": OrderedCollection.typeId.href
532
+ "activitypub.collection.id": uri.href,
533
+ "activitypub.collection.type": OrderedCollectionPage.typeId.href,
534
+ "fedify.collection.cursor": cursor
432
535
  }
433
536
  }, async (span) => {
434
- if (totalItems != null) span.setAttribute("activitypub.collection.total_items", Number(totalItems));
537
+ const started = performance.now();
435
538
  try {
436
- const page = await collectionCallbacks.dispatcher(context, identifier, null, filter);
539
+ const page = await collectionCallbacks.dispatcher(context, identifier, cursor, filter);
540
+ dispatchDurationMs = getDurationMs(started);
437
541
  if (page == null) {
438
542
  span.setStatus({ code: SpanStatusCode.ERROR });
439
543
  return await onNotFound(request);
440
544
  }
441
- const { items } = page;
442
- span.setAttribute("fedify.collection.items", items.length);
443
- return items;
545
+ const items = filterCollectionItems(page.items, name$1, filterPredicate);
546
+ itemCount = items.length;
547
+ span.setAttribute("fedify.collection.items", itemCount);
548
+ return {
549
+ ...page,
550
+ items
551
+ };
444
552
  } catch (e) {
553
+ if (dispatchDurationMs == null) dispatchDurationMs = getDurationMs(started);
445
554
  span.setStatus({
446
555
  code: SpanStatusCode.ERROR,
447
556
  message: String(e)
@@ -451,87 +560,44 @@ async function handleCollection(request, { name: name$1, identifier, uriGetter,
451
560
  span.end();
452
561
  }
453
562
  });
454
- if (itemsOrResponse instanceof Response) return itemsOrResponse;
455
- collection = new OrderedCollection({
456
- id: baseUri,
457
- totalItems: totalItems == null ? null : Number(totalItems),
458
- items: filterCollectionItems(itemsOrResponse, name$1, filterPredicate)
459
- });
460
- } else {
461
- const lastCursor = await collectionCallbacks.lastCursor?.(context, identifier);
462
- const first = new URL(context.url);
463
- first.searchParams.set("cursor", firstCursor);
464
- let last = null;
465
- if (lastCursor != null) {
466
- last = new URL(context.url);
467
- last.searchParams.set("cursor", lastCursor);
563
+ if (pageOrResponse instanceof Response) return finish(pageOrResponse, "not_found");
564
+ const { items, prevCursor, nextCursor } = pageOrResponse;
565
+ let prev = null;
566
+ if (prevCursor != null) {
567
+ prev = new URL(context.url);
568
+ prev.searchParams.set("cursor", prevCursor);
468
569
  }
469
- collection = new OrderedCollection({
470
- id: baseUri,
471
- totalItems: totalItems == null ? null : Number(totalItems),
472
- first,
473
- last
474
- });
475
- }
476
- } else {
477
- const uri = new URL(baseUri);
478
- uri.searchParams.set("cursor", cursor);
479
- const pageOrResponse = await tracer.startActiveSpan(`activitypub.dispatch_collection_page ${name$1}`, {
480
- kind: SpanKind.SERVER,
481
- attributes: {
482
- "activitypub.collection.id": uri.href,
483
- "activitypub.collection.type": OrderedCollectionPage.typeId.href,
484
- "fedify.collection.cursor": cursor
570
+ let next = null;
571
+ if (nextCursor != null) {
572
+ next = new URL(context.url);
573
+ next.searchParams.set("cursor", nextCursor);
485
574
  }
486
- }, async (span) => {
487
- try {
488
- const page = await collectionCallbacks.dispatcher(context, identifier, cursor, filter);
489
- if (page == null) {
490
- span.setStatus({ code: SpanStatusCode.ERROR });
491
- return await onNotFound(request);
492
- }
493
- span.setAttribute("fedify.collection.items", page.items.length);
494
- return page;
495
- } catch (e) {
496
- span.setStatus({
497
- code: SpanStatusCode.ERROR,
498
- message: String(e)
499
- });
500
- throw e;
501
- } finally {
502
- span.end();
503
- }
504
- });
505
- if (pageOrResponse instanceof Response) return pageOrResponse;
506
- const { items, prevCursor, nextCursor } = pageOrResponse;
507
- let prev = null;
508
- if (prevCursor != null) {
509
- prev = new URL(context.url);
510
- prev.searchParams.set("cursor", prevCursor);
575
+ const partOf = new URL(context.url);
576
+ partOf.searchParams.delete("cursor");
577
+ collection = new OrderedCollectionPage({
578
+ id: uri,
579
+ prev,
580
+ next,
581
+ items,
582
+ partOf
583
+ });
511
584
  }
512
- let next = null;
513
- if (nextCursor != null) {
514
- next = new URL(context.url);
515
- next.searchParams.set("cursor", nextCursor);
585
+ if (collectionCallbacks.authorizePredicate != null) {
586
+ if (!await collectionCallbacks.authorizePredicate(context, identifier)) return finish(await onUnauthorized(request), "unauthorized");
516
587
  }
517
- const partOf = new URL(context.url);
518
- partOf.searchParams.delete("cursor");
519
- collection = new OrderedCollectionPage({
520
- id: uri,
521
- prev,
522
- next,
523
- items: filterCollectionItems(items, name$1, filterPredicate),
524
- partOf
588
+ const jsonLd = await collection.toJsonLd(context);
589
+ return finish(new Response(JSON.stringify(jsonLd), { headers: {
590
+ "Content-Type": "application/activity+json",
591
+ Vary: "Accept"
592
+ } }), "served");
593
+ } catch (e) {
594
+ recordCollectionMetrics(meterProvider, metricBase, "error", {
595
+ dispatchDurationMs,
596
+ itemCount,
597
+ totalItems: totalItemCount
525
598
  });
599
+ throw e;
526
600
  }
527
- if (collectionCallbacks.authorizePredicate != null) {
528
- if (!await collectionCallbacks.authorizePredicate(context, identifier)) return await onUnauthorized(request);
529
- }
530
- const jsonLd = await collection.toJsonLd(context);
531
- return new Response(JSON.stringify(jsonLd), { headers: {
532
- "Content-Type": "application/activity+json",
533
- Vary: "Accept"
534
- } });
535
601
  }
536
602
  /**
537
603
  * Filters collection items based on the provided predicate.
@@ -1234,11 +1300,31 @@ async function handleInboxInternal(request, parameters, span) {
1234
1300
  * @since 1.8.0
1235
1301
  */
1236
1302
  const handleCustomCollection = exceptWrapper(_handleCustomCollection);
1237
- async function _handleCustomCollection(request, { name, values, context, tracerProvider, collectionCallbacks: callbacks, filterPredicate }) {
1303
+ const pendingCollectionMetricRecorders = /* @__PURE__ */ new WeakMap();
1304
+ function deferPendingCollectionMetrics(error, recorder) {
1305
+ if (error == null || typeof error !== "object" && typeof error !== "function") return false;
1306
+ pendingCollectionMetricRecorders.set(error, recorder);
1307
+ return true;
1308
+ }
1309
+ function recordDeferredPendingCollectionMetrics(error, result, response) {
1310
+ if (error == null || typeof error !== "object" && typeof error !== "function") return;
1311
+ const recorder = pendingCollectionMetricRecorders.get(error);
1312
+ pendingCollectionMetricRecorders.delete(error);
1313
+ recorder?.(result, response);
1314
+ }
1315
+ async function _handleCustomCollection(request, { name, values, context, tracerProvider, meterProvider, collectionCallbacks: callbacks, filterPredicate }) {
1238
1316
  verifyDefined(callbacks);
1239
1317
  await authIfNeeded(context, values, callbacks);
1240
1318
  const cursor = new URL(request.url).searchParams.get("cursor");
1241
- return await new CustomCollectionHandler(name, values, context, callbacks, tracerProvider, Collection, CollectionPage, filterPredicate).fetchCollection(cursor).toJsonLd().then(respondAsActivity);
1319
+ const handler = new CustomCollectionHandler(name, values, context, callbacks, tracerProvider, meterProvider, Collection, CollectionPage, filterPredicate).fetchCollection(cursor);
1320
+ try {
1321
+ const response = await handler.toJsonLd().then(respondAsActivity);
1322
+ handler.recordPendingCollectionMetrics("served", response);
1323
+ return response;
1324
+ } catch (e) {
1325
+ if (!deferPendingCollectionMetrics(e, (result, response) => handler.recordPendingCollectionMetrics(result, response))) handler.recordPendingCollectionMetrics("error");
1326
+ throw e;
1327
+ }
1242
1328
  }
1243
1329
  /**
1244
1330
  * Handles an ordered collection request.
@@ -1252,11 +1338,19 @@ async function _handleCustomCollection(request, { name, values, context, tracerP
1252
1338
  * @since 1.8.0
1253
1339
  */
1254
1340
  const handleOrderedCollection = exceptWrapper(_handleOrderedCollection);
1255
- async function _handleOrderedCollection(request, { name, values, context, tracerProvider, collectionCallbacks: callbacks, filterPredicate }) {
1341
+ async function _handleOrderedCollection(request, { name, values, context, tracerProvider, meterProvider, collectionCallbacks: callbacks, filterPredicate }) {
1256
1342
  verifyDefined(callbacks);
1257
1343
  await authIfNeeded(context, values, callbacks);
1258
1344
  const cursor = new URL(request.url).searchParams.get("cursor");
1259
- return await new CustomCollectionHandler(name, values, context, callbacks, tracerProvider, OrderedCollection, OrderedCollectionPage, filterPredicate).fetchCollection(cursor).toJsonLd().then(respondAsActivity);
1345
+ const handler = new CustomCollectionHandler(name, values, context, callbacks, tracerProvider, meterProvider, OrderedCollection, OrderedCollectionPage, filterPredicate).fetchCollection(cursor);
1346
+ try {
1347
+ const response = await handler.toJsonLd().then(respondAsActivity);
1348
+ handler.recordPendingCollectionMetrics("served", response);
1349
+ return response;
1350
+ } catch (e) {
1351
+ if (!deferPendingCollectionMetrics(e, (result, response) => handler.recordPendingCollectionMetrics(result, response))) handler.recordPendingCollectionMetrics("error");
1352
+ throw e;
1353
+ }
1260
1354
  }
1261
1355
  /**
1262
1356
  * Handling custom collections with support for pagination and filtering.
@@ -1276,6 +1370,7 @@ var CustomCollectionHandler = class {
1276
1370
  context;
1277
1371
  callbacks;
1278
1372
  tracerProvider;
1373
+ meterProvider;
1279
1374
  Collection;
1280
1375
  CollectionPage;
1281
1376
  filterPredicate;
@@ -1303,6 +1398,7 @@ var CustomCollectionHandler = class {
1303
1398
  */
1304
1399
  #dispatcher;
1305
1400
  #collection = null;
1401
+ #pendingCollectionMetrics = [];
1306
1402
  /**
1307
1403
  * Creates a new CustomCollection instance.
1308
1404
  * @param name The name of the collection.
@@ -1314,12 +1410,13 @@ var CustomCollectionHandler = class {
1314
1410
  * @param CollectionPage The CollectionPage constructor.
1315
1411
  * @param filterPredicate Optional filter predicate for items.
1316
1412
  */
1317
- constructor(name$2, values, context, callbacks, tracerProvider = trace.getTracerProvider(), Collection, CollectionPage, filterPredicate) {
1413
+ constructor(name$2, values, context, callbacks, tracerProvider = trace.getTracerProvider(), meterProvider, Collection, CollectionPage, filterPredicate) {
1318
1414
  this.name = name$2;
1319
1415
  this.values = values;
1320
1416
  this.context = context;
1321
1417
  this.callbacks = callbacks;
1322
1418
  this.tracerProvider = tracerProvider;
1419
+ this.meterProvider = meterProvider;
1323
1420
  this.Collection = Collection;
1324
1421
  this.CollectionPage = CollectionPage;
1325
1422
  this.filterPredicate = filterPredicate;
@@ -1372,10 +1469,12 @@ var CustomCollectionHandler = class {
1372
1469
  const { prevCursor, nextCursor } = pages;
1373
1470
  const partOf = new URL(id);
1374
1471
  partOf.searchParams.delete("cursor");
1472
+ const items = this.filterItems(pages.items);
1473
+ this.recordPendingCollectionItemCount(true, items.length);
1375
1474
  return {
1376
1475
  id,
1377
1476
  partOf,
1378
- items: this.filterItems(pages.items),
1477
+ items,
1379
1478
  prev: this.appendToUrl(prevCursor),
1380
1479
  next: this.appendToUrl(nextCursor)
1381
1480
  };
@@ -1388,11 +1487,16 @@ var CustomCollectionHandler = class {
1388
1487
  */
1389
1488
  async getProps(firstCursor) {
1390
1489
  const lastCursor = await this.callbacks.lastCursor?.(this.context, this.values);
1490
+ const totalItems = await this.totalItems;
1491
+ if (totalItems != null) this.#pendingCollectionMetrics.push({
1492
+ page: false,
1493
+ totalItems: Number(totalItems)
1494
+ });
1391
1495
  return {
1392
1496
  id: this.#id,
1393
1497
  first: this.appendToUrl(firstCursor),
1394
1498
  last: this.appendToUrl(lastCursor),
1395
- totalItems: await this.totalItems
1499
+ totalItems
1396
1500
  };
1397
1501
  }
1398
1502
  /**
@@ -1402,10 +1506,12 @@ var CustomCollectionHandler = class {
1402
1506
  async getPropsWithoutCursor() {
1403
1507
  const totalItems = await this.totalItems;
1404
1508
  const pages = await this.getPages({ totalItems });
1509
+ const items = this.filterItems(pages.items);
1510
+ this.recordPendingCollectionItemCount(false, items.length);
1405
1511
  return {
1406
1512
  id: this.#id,
1407
1513
  totalItems,
1408
- items: this.filterItems(pages.items)
1514
+ items
1409
1515
  };
1410
1516
  }
1411
1517
  /**
@@ -1440,12 +1546,24 @@ var CustomCollectionHandler = class {
1440
1546
  * @returns A function that handles the span operation.
1441
1547
  */
1442
1548
  spanPages = ({ totalItems = null, cursor = null }) => async (span) => {
1549
+ const pageMetricBase = this.metricBase(cursor !== null);
1550
+ const started = performance.now();
1443
1551
  try {
1444
1552
  if (totalItems !== null) span.setAttribute(this.ATTRS.TOTAL_ITEMS, totalItems);
1445
1553
  const page = await this.dispatch(cursor);
1554
+ const durationMs = getDurationMs(started);
1446
1555
  span.setAttribute(this.ATTRS.ITEMS, page.items.length);
1556
+ this.#pendingCollectionMetrics.push({
1557
+ page: pageMetricBase.page,
1558
+ dispatchDurationMs: durationMs,
1559
+ totalItems: totalItems == null ? void 0 : Number(totalItems)
1560
+ });
1447
1561
  return page;
1448
1562
  } catch (e) {
1563
+ this.#pendingCollectionMetrics.push({
1564
+ page: cursor !== null,
1565
+ dispatchDurationMs: getDurationMs(started)
1566
+ });
1449
1567
  const message = e instanceof Error ? e.message : String(e);
1450
1568
  span.setStatus({
1451
1569
  code: SpanStatusCode.ERROR,
@@ -1472,6 +1590,37 @@ var CustomCollectionHandler = class {
1472
1590
  filterItems(items) {
1473
1591
  return filterCollectionItems(items, this.name, this.filterPredicate);
1474
1592
  }
1593
+ metricBase(page) {
1594
+ return {
1595
+ kind: "custom",
1596
+ page,
1597
+ dispatcher: "custom"
1598
+ };
1599
+ }
1600
+ metricAttributes(page, result, response) {
1601
+ return collectionAttributes(this.metricBase(page), result, response);
1602
+ }
1603
+ recordPendingCollectionItemCount(page, itemCount) {
1604
+ for (let i = this.#pendingCollectionMetrics.length - 1; i >= 0; i--) {
1605
+ const measurement = this.#pendingCollectionMetrics[i];
1606
+ if (measurement.page === page && measurement.dispatchDurationMs != null && measurement.itemCount == null) {
1607
+ measurement.itemCount = itemCount;
1608
+ return;
1609
+ }
1610
+ }
1611
+ this.#pendingCollectionMetrics.push({
1612
+ page,
1613
+ itemCount
1614
+ });
1615
+ }
1616
+ recordPendingCollectionMetrics(result, response) {
1617
+ for (const measurement of this.#pendingCollectionMetrics.splice(0)) {
1618
+ const attrs = this.metricAttributes(measurement.page, result, response);
1619
+ if (measurement.dispatchDurationMs != null) recordCollectionDispatchDuration(this.meterProvider, measurement.dispatchDurationMs, attrs);
1620
+ if (measurement.itemCount != null) recordCollectionPageItems(this.meterProvider, measurement.itemCount, attrs);
1621
+ if (measurement.totalItems != null) recordCollectionTotalItems(this.meterProvider, measurement.totalItems, attrs);
1622
+ }
1623
+ }
1475
1624
  /**
1476
1625
  * Appends a cursor to the URL if it exists.
1477
1626
  * @param cursor The cursor to append, or null/undefined.
@@ -1535,14 +1684,36 @@ var CustomCollectionHandler = class {
1535
1684
  */
1536
1685
  function exceptWrapper(handler) {
1537
1686
  return async (request, handlerParams) => {
1687
+ const page = new URL(request.url).searchParams.get("cursor") != null;
1688
+ const { meterProvider } = handlerParams;
1689
+ const metricBase = {
1690
+ kind: "custom",
1691
+ page,
1692
+ dispatcher: "custom"
1693
+ };
1538
1694
  try {
1539
- return await handler(request, handlerParams);
1695
+ const response = await handler(request, handlerParams);
1696
+ recordCollectionRequest(meterProvider, collectionAttributes(metricBase, "served", response));
1697
+ return response;
1540
1698
  } catch (error) {
1541
1699
  const { onNotFound, onUnauthorized } = handlerParams;
1542
1700
  switch (error?.constructor) {
1543
- case ItemsNotFoundError: return await onNotFound(request);
1544
- case UnauthorizedError: return await onUnauthorized(request);
1545
- default: throw error;
1701
+ case ItemsNotFoundError: {
1702
+ const response = await onNotFound(request);
1703
+ recordDeferredPendingCollectionMetrics(error, "not_found", response);
1704
+ recordCollectionRequest(meterProvider, collectionAttributes(metricBase, "not_found", response));
1705
+ return response;
1706
+ }
1707
+ case UnauthorizedError: {
1708
+ const response = await onUnauthorized(request);
1709
+ recordDeferredPendingCollectionMetrics(error, "unauthorized", response);
1710
+ recordCollectionRequest(meterProvider, collectionAttributes(metricBase, "unauthorized", response));
1711
+ return response;
1712
+ }
1713
+ default:
1714
+ recordDeferredPendingCollectionMetrics(error, "error");
1715
+ recordCollectionRequest(meterProvider, collectionAttributes(metricBase, "error"));
1716
+ throw error;
1546
1717
  }
1547
1718
  }
1548
1719
  };
@@ -2930,7 +3101,15 @@ var FederationImpl = class extends FederationBuilderImpl {
2930
3101
  }
2931
3102
  if (request.method !== "POST" && !acceptsJsonLd(request)) {
2932
3103
  metricState.endpoint = "not_acceptable";
2933
- return await onNotAcceptable(request);
3104
+ const response = await onNotAcceptable(request);
3105
+ const collectionRoute = getCollectionMetricRoute(routeName);
3106
+ if (collectionRoute != null) recordCollectionRequest(this._meterProvider, {
3107
+ ...collectionRoute,
3108
+ page: url.searchParams.get("cursor") != null,
3109
+ result: "not_acceptable",
3110
+ statusCode: response.status
3111
+ });
3112
+ return response;
2934
3113
  }
2935
3114
  switch (routeName) {
2936
3115
  case "actor":
@@ -2991,6 +3170,7 @@ var FederationImpl = class extends FederationBuilderImpl {
2991
3170
  context,
2992
3171
  collectionCallbacks: this.outboxCallbacks,
2993
3172
  tracerProvider: this.tracerProvider,
3173
+ meterProvider: this._meterProvider,
2994
3174
  onUnauthorized,
2995
3175
  onNotFound
2996
3176
  });
@@ -3002,6 +3182,7 @@ var FederationImpl = class extends FederationBuilderImpl {
3002
3182
  context,
3003
3183
  collectionCallbacks: this.inboxCallbacks,
3004
3184
  tracerProvider: this.tracerProvider,
3185
+ meterProvider: this._meterProvider,
3005
3186
  onUnauthorized,
3006
3187
  onNotFound
3007
3188
  });
@@ -3041,6 +3222,7 @@ var FederationImpl = class extends FederationBuilderImpl {
3041
3222
  context,
3042
3223
  collectionCallbacks: this.followingCallbacks,
3043
3224
  tracerProvider: this.tracerProvider,
3225
+ meterProvider: this._meterProvider,
3044
3226
  onUnauthorized,
3045
3227
  onNotFound
3046
3228
  });
@@ -3064,6 +3246,7 @@ var FederationImpl = class extends FederationBuilderImpl {
3064
3246
  filterPredicate: baseUrl != null ? ((i) => (i instanceof URL ? i.href : i.id?.href ?? "").startsWith(baseUrl)) : void 0,
3065
3247
  collectionCallbacks: this.followersCallbacks,
3066
3248
  tracerProvider: this.tracerProvider,
3249
+ meterProvider: this._meterProvider,
3067
3250
  onUnauthorized,
3068
3251
  onNotFound
3069
3252
  });
@@ -3075,6 +3258,7 @@ var FederationImpl = class extends FederationBuilderImpl {
3075
3258
  context,
3076
3259
  collectionCallbacks: this.likedCallbacks,
3077
3260
  tracerProvider: this.tracerProvider,
3261
+ meterProvider: this._meterProvider,
3078
3262
  onUnauthorized,
3079
3263
  onNotFound
3080
3264
  });
@@ -3085,6 +3269,7 @@ var FederationImpl = class extends FederationBuilderImpl {
3085
3269
  context,
3086
3270
  collectionCallbacks: this.featuredCallbacks,
3087
3271
  tracerProvider: this.tracerProvider,
3272
+ meterProvider: this._meterProvider,
3088
3273
  onUnauthorized,
3089
3274
  onNotFound
3090
3275
  });
@@ -3095,6 +3280,7 @@ var FederationImpl = class extends FederationBuilderImpl {
3095
3280
  context,
3096
3281
  collectionCallbacks: this.featuredTagsCallbacks,
3097
3282
  tracerProvider: this.tracerProvider,
3283
+ meterProvider: this._meterProvider,
3098
3284
  onUnauthorized,
3099
3285
  onNotFound
3100
3286
  });
@@ -3107,6 +3293,7 @@ var FederationImpl = class extends FederationBuilderImpl {
3107
3293
  values: route.values,
3108
3294
  collectionCallbacks: callbacks,
3109
3295
  tracerProvider: this.tracerProvider,
3296
+ meterProvider: this._meterProvider,
3110
3297
  onUnauthorized,
3111
3298
  onNotFound
3112
3299
  });
@@ -3120,6 +3307,7 @@ var FederationImpl = class extends FederationBuilderImpl {
3120
3307
  values: route.values,
3121
3308
  collectionCallbacks: callbacks,
3122
3309
  tracerProvider: this.tracerProvider,
3310
+ meterProvider: this._meterProvider,
3123
3311
  onUnauthorized,
3124
3312
  onNotFound
3125
3313
  });
@@ -3152,6 +3340,29 @@ function getEndpointCategory(routeName) {
3152
3340
  default: return "not_found";
3153
3341
  }
3154
3342
  }
3343
+ function getCollectionMetricRoute(routeName) {
3344
+ switch (routeName) {
3345
+ case "inbox":
3346
+ case "outbox":
3347
+ case "following":
3348
+ case "followers":
3349
+ case "liked":
3350
+ case "featured": return {
3351
+ kind: routeName,
3352
+ dispatcher: "built_in"
3353
+ };
3354
+ case "featuredTags": return {
3355
+ kind: "featured_tags",
3356
+ dispatcher: "built_in"
3357
+ };
3358
+ case "collection":
3359
+ case "orderedCollection": return {
3360
+ kind: "custom",
3361
+ dispatcher: "custom"
3362
+ };
3363
+ default: return;
3364
+ }
3365
+ }
3155
3366
  const FANOUT_THRESHOLD = 5;
3156
3367
  var ContextImpl = class ContextImpl {
3157
3368
  url;