@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
@@ -2,10 +2,10 @@ import { Temporal } from "@js-temporal/polyfill";
2
2
  import { URLPattern } from "urlpattern-polyfill";
3
3
  import { t as __exportAll } from "./chunk-CRNNMoPX.js";
4
4
  import { r as getDefaultActivityTransformers } from "./transformers-BGMIq1cs.js";
5
- import { C as recordWebFingerHandle, O as name, S as recordOutboxEnqueue, a as verifyRequestDetailed, b as recordInboxActivity, d as validateCryptoKey, f as getDurationMs, g as isAbortError, h as instrumentDocumentLoader, i as verifyRequest, k as version, m as getRemoteHost, n as parseRfc9421SignatureInput, o as exportJwk, p as getFederationMetrics, t as doubleKnock, u as importJwk, w as formatAcceptSignature, x as recordOutboxActivity, y as recordFanoutRecipients } from "./http-CFP8WMMv.js";
6
- import { _ as hasSignatureLike, b as signJsonLd, c as getKeyOwner, f as compactJsonLd, g as hasSignature, h as getNormalizationContextLoader, i as verifyObject, l as InvalidContextReferenceError, m as detachSignature, n as hasProofLike, o as normalizeOutgoingActivityJsonLd, r as signObject, s as doesActorOwnKey, u as assertSafeJsonLd, v as isClearlyMalformedContextReference, w as wrapContextLoaderForJsonLd, x as verifyCompactJsonLd, y as isInvalidUrlTypeError } from "./proof-B_6gAVQ2.js";
5
+ import { C as recordFanoutRecipients, D as recordWebFingerHandle, E as recordOutboxEnqueue, M as name, N as version, O as formatAcceptSignature, T as recordOutboxActivity, a as verifyRequestDetailed, b as recordCollectionRequest, d as validateCryptoKey, f as getDurationMs, g as isAbortError, h as instrumentDocumentLoader, i as verifyRequest, m as getRemoteHost, n as parseRfc9421SignatureInput, o as exportJwk, p as getFederationMetrics, t as doubleKnock, u as importJwk, v as recordCollectionDispatchDuration, w as recordInboxActivity, x as recordCollectionTotalItems, y as recordCollectionPageItems } from "./http-C0XZv7iH.js";
6
+ import { _ as hasSignatureLike, b as signJsonLd, c as getKeyOwner, f as compactJsonLd, g as hasSignature, h as getNormalizationContextLoader, i as verifyObject, l as InvalidContextReferenceError, m as detachSignature, n as hasProofLike, o as normalizeOutgoingActivityJsonLd, r as signObject, s as doesActorOwnKey, u as assertSafeJsonLd, v as isClearlyMalformedContextReference, w as wrapContextLoaderForJsonLd, x as verifyCompactJsonLd, y as isInvalidUrlTypeError } from "./proof-CmS6yxgt.js";
7
7
  import { n as getNodeInfo, t as nodeInfoToJson } from "./types-CAY3OdLq.js";
8
- import { n as getAuthenticatedDocumentLoader, t as kvCache } from "./kv-cache-dH0biV98.js";
8
+ import { n as getAuthenticatedDocumentLoader, t as kvCache } from "./kv-cache-DsbVBK7Y.js";
9
9
  import { getLogger, withContext } from "@logtape/logtape";
10
10
  import { Router, RouterError, assertPath } from "@fedify/uri-template";
11
11
  import { Activity, Collection, CollectionPage, CryptographicKey, Link, Multikey, Object as Object$1, OrderedCollection, OrderedCollectionPage, Tombstone, getTypeId, lookupObject, traverseCollection } from "@fedify/vocab";
@@ -1211,6 +1211,33 @@ async function handleObject(request, { values, context, objectDispatcher, author
1211
1211
  Vary: "Accept"
1212
1212
  } });
1213
1213
  }
1214
+ const BUILT_IN_COLLECTION_METRIC_KINDS = new Set([
1215
+ "inbox",
1216
+ "outbox",
1217
+ "following",
1218
+ "followers",
1219
+ "liked",
1220
+ "featured",
1221
+ "featured_tags"
1222
+ ]);
1223
+ function getCollectionMetricKind(name) {
1224
+ const normalized = name.trim().replace(/([a-z0-9])([A-Z])/g, "$1_$2").toLowerCase().replace(/\s+/g, "_");
1225
+ return BUILT_IN_COLLECTION_METRIC_KINDS.has(normalized) ? normalized : "custom";
1226
+ }
1227
+ function collectionAttributes(base, result, response) {
1228
+ return {
1229
+ ...base,
1230
+ result,
1231
+ ...response == null ? {} : { statusCode: response.status }
1232
+ };
1233
+ }
1234
+ function recordCollectionMetrics(meterProvider, base, result, options = {}) {
1235
+ const attrs = collectionAttributes(base, result, options.response);
1236
+ recordCollectionRequest(meterProvider, attrs);
1237
+ if (options.dispatchDurationMs != null) recordCollectionDispatchDuration(meterProvider, options.dispatchDurationMs, attrs);
1238
+ if (options.itemCount != null) recordCollectionPageItems(meterProvider, options.itemCount, attrs);
1239
+ if (options.totalItems != null) recordCollectionTotalItems(meterProvider, options.totalItems, attrs);
1240
+ }
1214
1241
  /**
1215
1242
  * Handles a collection request.
1216
1243
  * @template TItem The type of items in the collection.
@@ -1221,36 +1248,118 @@ async function handleObject(request, { values, context, objectDispatcher, author
1221
1248
  * @param parameters The parameters for handling the collection.
1222
1249
  * @returns A promise that resolves to an HTTP response.
1223
1250
  */
1224
- async function handleCollection(request, { name: name$1, identifier, uriGetter, filter, filterPredicate, context, collectionCallbacks, tracerProvider, onUnauthorized, onNotFound }) {
1251
+ async function handleCollection(request, { name: name$1, identifier, uriGetter, filter, filterPredicate, context, collectionCallbacks, tracerProvider, meterProvider, onUnauthorized, onNotFound }) {
1225
1252
  const spanName = name$1.trim().replace(/\s+/g, "_");
1226
1253
  tracerProvider = tracerProvider ?? trace.getTracerProvider();
1227
1254
  const tracer = tracerProvider.getTracer(name, version);
1228
1255
  const cursor = new URL(request.url).searchParams.get("cursor");
1229
- if (collectionCallbacks == null) return await onNotFound(request);
1230
- let collection;
1231
- const baseUri = uriGetter(identifier);
1232
- if (cursor == null) {
1233
- const firstCursor = await collectionCallbacks.firstCursor?.(context, identifier);
1234
- const totalItems = filter == null ? await collectionCallbacks.counter?.(context, identifier) : void 0;
1235
- if (firstCursor == null) {
1236
- const itemsOrResponse = await tracer.startActiveSpan(`activitypub.dispatch_collection ${spanName}`, {
1256
+ const metricBase = {
1257
+ kind: getCollectionMetricKind(name$1),
1258
+ page: cursor != null,
1259
+ dispatcher: "built_in"
1260
+ };
1261
+ let dispatchDurationMs;
1262
+ let itemCount;
1263
+ let totalItemCount;
1264
+ const finish = (response, result) => {
1265
+ recordCollectionMetrics(meterProvider, metricBase, result, {
1266
+ response,
1267
+ dispatchDurationMs,
1268
+ itemCount,
1269
+ totalItems: totalItemCount
1270
+ });
1271
+ return response;
1272
+ };
1273
+ try {
1274
+ if (collectionCallbacks == null) return finish(await onNotFound(request), "not_found");
1275
+ let collection;
1276
+ const baseUri = uriGetter(identifier);
1277
+ if (cursor == null) {
1278
+ const firstCursor = await collectionCallbacks.firstCursor?.(context, identifier);
1279
+ const totalItems = filter == null ? await collectionCallbacks.counter?.(context, identifier) : void 0;
1280
+ totalItemCount = totalItems == null ? void 0 : Number(totalItems);
1281
+ if (firstCursor == null) {
1282
+ const itemsOrResponse = await tracer.startActiveSpan(`activitypub.dispatch_collection ${spanName}`, {
1283
+ kind: SpanKind.SERVER,
1284
+ attributes: {
1285
+ "activitypub.collection.id": baseUri.href,
1286
+ "activitypub.collection.type": OrderedCollection.typeId.href
1287
+ }
1288
+ }, async (span) => {
1289
+ if (totalItemCount != null) span.setAttribute("activitypub.collection.total_items", totalItemCount);
1290
+ const started = performance.now();
1291
+ try {
1292
+ const page = await collectionCallbacks.dispatcher(context, identifier, null, filter);
1293
+ dispatchDurationMs = getDurationMs(started);
1294
+ if (page == null) {
1295
+ span.setStatus({ code: SpanStatusCode.ERROR });
1296
+ return await onNotFound(request);
1297
+ }
1298
+ const items = filterCollectionItems(page.items, name$1, filterPredicate);
1299
+ itemCount = items.length;
1300
+ span.setAttribute("fedify.collection.items", itemCount);
1301
+ return items;
1302
+ } catch (e) {
1303
+ if (dispatchDurationMs == null) dispatchDurationMs = getDurationMs(started);
1304
+ span.setStatus({
1305
+ code: SpanStatusCode.ERROR,
1306
+ message: String(e)
1307
+ });
1308
+ throw e;
1309
+ } finally {
1310
+ span.end();
1311
+ }
1312
+ });
1313
+ if (itemsOrResponse instanceof Response) return finish(itemsOrResponse, "not_found");
1314
+ collection = new OrderedCollection({
1315
+ id: baseUri,
1316
+ totalItems: totalItemCount ?? null,
1317
+ items: itemsOrResponse
1318
+ });
1319
+ } else {
1320
+ const lastCursor = await collectionCallbacks.lastCursor?.(context, identifier);
1321
+ const first = new URL(context.url);
1322
+ first.searchParams.set("cursor", firstCursor);
1323
+ let last = null;
1324
+ if (lastCursor != null) {
1325
+ last = new URL(context.url);
1326
+ last.searchParams.set("cursor", lastCursor);
1327
+ }
1328
+ collection = new OrderedCollection({
1329
+ id: baseUri,
1330
+ totalItems: totalItemCount ?? null,
1331
+ first,
1332
+ last
1333
+ });
1334
+ }
1335
+ } else {
1336
+ const uri = new URL(baseUri);
1337
+ uri.searchParams.set("cursor", cursor);
1338
+ const pageOrResponse = await tracer.startActiveSpan(`activitypub.dispatch_collection_page ${name$1}`, {
1237
1339
  kind: SpanKind.SERVER,
1238
1340
  attributes: {
1239
- "activitypub.collection.id": baseUri.href,
1240
- "activitypub.collection.type": OrderedCollection.typeId.href
1341
+ "activitypub.collection.id": uri.href,
1342
+ "activitypub.collection.type": OrderedCollectionPage.typeId.href,
1343
+ "fedify.collection.cursor": cursor
1241
1344
  }
1242
1345
  }, async (span) => {
1243
- if (totalItems != null) span.setAttribute("activitypub.collection.total_items", Number(totalItems));
1346
+ const started = performance.now();
1244
1347
  try {
1245
- const page = await collectionCallbacks.dispatcher(context, identifier, null, filter);
1348
+ const page = await collectionCallbacks.dispatcher(context, identifier, cursor, filter);
1349
+ dispatchDurationMs = getDurationMs(started);
1246
1350
  if (page == null) {
1247
1351
  span.setStatus({ code: SpanStatusCode.ERROR });
1248
1352
  return await onNotFound(request);
1249
1353
  }
1250
- const { items } = page;
1251
- span.setAttribute("fedify.collection.items", items.length);
1252
- return items;
1354
+ const items = filterCollectionItems(page.items, name$1, filterPredicate);
1355
+ itemCount = items.length;
1356
+ span.setAttribute("fedify.collection.items", itemCount);
1357
+ return {
1358
+ ...page,
1359
+ items
1360
+ };
1253
1361
  } catch (e) {
1362
+ if (dispatchDurationMs == null) dispatchDurationMs = getDurationMs(started);
1254
1363
  span.setStatus({
1255
1364
  code: SpanStatusCode.ERROR,
1256
1365
  message: String(e)
@@ -1260,87 +1369,44 @@ async function handleCollection(request, { name: name$1, identifier, uriGetter,
1260
1369
  span.end();
1261
1370
  }
1262
1371
  });
1263
- if (itemsOrResponse instanceof Response) return itemsOrResponse;
1264
- collection = new OrderedCollection({
1265
- id: baseUri,
1266
- totalItems: totalItems == null ? null : Number(totalItems),
1267
- items: filterCollectionItems(itemsOrResponse, name$1, filterPredicate)
1268
- });
1269
- } else {
1270
- const lastCursor = await collectionCallbacks.lastCursor?.(context, identifier);
1271
- const first = new URL(context.url);
1272
- first.searchParams.set("cursor", firstCursor);
1273
- let last = null;
1274
- if (lastCursor != null) {
1275
- last = new URL(context.url);
1276
- last.searchParams.set("cursor", lastCursor);
1277
- }
1278
- collection = new OrderedCollection({
1279
- id: baseUri,
1280
- totalItems: totalItems == null ? null : Number(totalItems),
1281
- first,
1282
- last
1283
- });
1284
- }
1285
- } else {
1286
- const uri = new URL(baseUri);
1287
- uri.searchParams.set("cursor", cursor);
1288
- const pageOrResponse = await tracer.startActiveSpan(`activitypub.dispatch_collection_page ${name$1}`, {
1289
- kind: SpanKind.SERVER,
1290
- attributes: {
1291
- "activitypub.collection.id": uri.href,
1292
- "activitypub.collection.type": OrderedCollectionPage.typeId.href,
1293
- "fedify.collection.cursor": cursor
1372
+ if (pageOrResponse instanceof Response) return finish(pageOrResponse, "not_found");
1373
+ const { items, prevCursor, nextCursor } = pageOrResponse;
1374
+ let prev = null;
1375
+ if (prevCursor != null) {
1376
+ prev = new URL(context.url);
1377
+ prev.searchParams.set("cursor", prevCursor);
1294
1378
  }
1295
- }, async (span) => {
1296
- try {
1297
- const page = await collectionCallbacks.dispatcher(context, identifier, cursor, filter);
1298
- if (page == null) {
1299
- span.setStatus({ code: SpanStatusCode.ERROR });
1300
- return await onNotFound(request);
1301
- }
1302
- span.setAttribute("fedify.collection.items", page.items.length);
1303
- return page;
1304
- } catch (e) {
1305
- span.setStatus({
1306
- code: SpanStatusCode.ERROR,
1307
- message: String(e)
1308
- });
1309
- throw e;
1310
- } finally {
1311
- span.end();
1379
+ let next = null;
1380
+ if (nextCursor != null) {
1381
+ next = new URL(context.url);
1382
+ next.searchParams.set("cursor", nextCursor);
1312
1383
  }
1313
- });
1314
- if (pageOrResponse instanceof Response) return pageOrResponse;
1315
- const { items, prevCursor, nextCursor } = pageOrResponse;
1316
- let prev = null;
1317
- if (prevCursor != null) {
1318
- prev = new URL(context.url);
1319
- prev.searchParams.set("cursor", prevCursor);
1384
+ const partOf = new URL(context.url);
1385
+ partOf.searchParams.delete("cursor");
1386
+ collection = new OrderedCollectionPage({
1387
+ id: uri,
1388
+ prev,
1389
+ next,
1390
+ items,
1391
+ partOf
1392
+ });
1320
1393
  }
1321
- let next = null;
1322
- if (nextCursor != null) {
1323
- next = new URL(context.url);
1324
- next.searchParams.set("cursor", nextCursor);
1394
+ if (collectionCallbacks.authorizePredicate != null) {
1395
+ if (!await collectionCallbacks.authorizePredicate(context, identifier)) return finish(await onUnauthorized(request), "unauthorized");
1325
1396
  }
1326
- const partOf = new URL(context.url);
1327
- partOf.searchParams.delete("cursor");
1328
- collection = new OrderedCollectionPage({
1329
- id: uri,
1330
- prev,
1331
- next,
1332
- items: filterCollectionItems(items, name$1, filterPredicate),
1333
- partOf
1397
+ const jsonLd = await collection.toJsonLd(context);
1398
+ return finish(new Response(JSON.stringify(jsonLd), { headers: {
1399
+ "Content-Type": "application/activity+json",
1400
+ Vary: "Accept"
1401
+ } }), "served");
1402
+ } catch (e) {
1403
+ recordCollectionMetrics(meterProvider, metricBase, "error", {
1404
+ dispatchDurationMs,
1405
+ itemCount,
1406
+ totalItems: totalItemCount
1334
1407
  });
1408
+ throw e;
1335
1409
  }
1336
- if (collectionCallbacks.authorizePredicate != null) {
1337
- if (!await collectionCallbacks.authorizePredicate(context, identifier)) return await onUnauthorized(request);
1338
- }
1339
- const jsonLd = await collection.toJsonLd(context);
1340
- return new Response(JSON.stringify(jsonLd), { headers: {
1341
- "Content-Type": "application/activity+json",
1342
- Vary: "Accept"
1343
- } });
1344
1410
  }
1345
1411
  /**
1346
1412
  * Filters collection items based on the provided predicate.
@@ -2043,11 +2109,31 @@ async function handleInboxInternal(request, parameters, span) {
2043
2109
  * @since 1.8.0
2044
2110
  */
2045
2111
  const handleCustomCollection = exceptWrapper(_handleCustomCollection);
2046
- async function _handleCustomCollection(request, { name, values, context, tracerProvider, collectionCallbacks: callbacks, filterPredicate }) {
2112
+ const pendingCollectionMetricRecorders = /* @__PURE__ */ new WeakMap();
2113
+ function deferPendingCollectionMetrics(error, recorder) {
2114
+ if (error == null || typeof error !== "object" && typeof error !== "function") return false;
2115
+ pendingCollectionMetricRecorders.set(error, recorder);
2116
+ return true;
2117
+ }
2118
+ function recordDeferredPendingCollectionMetrics(error, result, response) {
2119
+ if (error == null || typeof error !== "object" && typeof error !== "function") return;
2120
+ const recorder = pendingCollectionMetricRecorders.get(error);
2121
+ pendingCollectionMetricRecorders.delete(error);
2122
+ recorder?.(result, response);
2123
+ }
2124
+ async function _handleCustomCollection(request, { name, values, context, tracerProvider, meterProvider, collectionCallbacks: callbacks, filterPredicate }) {
2047
2125
  verifyDefined(callbacks);
2048
2126
  await authIfNeeded(context, values, callbacks);
2049
2127
  const cursor = new URL(request.url).searchParams.get("cursor");
2050
- return await new CustomCollectionHandler(name, values, context, callbacks, tracerProvider, Collection, CollectionPage, filterPredicate).fetchCollection(cursor).toJsonLd().then(respondAsActivity);
2128
+ const handler = new CustomCollectionHandler(name, values, context, callbacks, tracerProvider, meterProvider, Collection, CollectionPage, filterPredicate).fetchCollection(cursor);
2129
+ try {
2130
+ const response = await handler.toJsonLd().then(respondAsActivity);
2131
+ handler.recordPendingCollectionMetrics("served", response);
2132
+ return response;
2133
+ } catch (e) {
2134
+ if (!deferPendingCollectionMetrics(e, (result, response) => handler.recordPendingCollectionMetrics(result, response))) handler.recordPendingCollectionMetrics("error");
2135
+ throw e;
2136
+ }
2051
2137
  }
2052
2138
  /**
2053
2139
  * Handles an ordered collection request.
@@ -2061,11 +2147,19 @@ async function _handleCustomCollection(request, { name, values, context, tracerP
2061
2147
  * @since 1.8.0
2062
2148
  */
2063
2149
  const handleOrderedCollection = exceptWrapper(_handleOrderedCollection);
2064
- async function _handleOrderedCollection(request, { name, values, context, tracerProvider, collectionCallbacks: callbacks, filterPredicate }) {
2150
+ async function _handleOrderedCollection(request, { name, values, context, tracerProvider, meterProvider, collectionCallbacks: callbacks, filterPredicate }) {
2065
2151
  verifyDefined(callbacks);
2066
2152
  await authIfNeeded(context, values, callbacks);
2067
2153
  const cursor = new URL(request.url).searchParams.get("cursor");
2068
- return await new CustomCollectionHandler(name, values, context, callbacks, tracerProvider, OrderedCollection, OrderedCollectionPage, filterPredicate).fetchCollection(cursor).toJsonLd().then(respondAsActivity);
2154
+ const handler = new CustomCollectionHandler(name, values, context, callbacks, tracerProvider, meterProvider, OrderedCollection, OrderedCollectionPage, filterPredicate).fetchCollection(cursor);
2155
+ try {
2156
+ const response = await handler.toJsonLd().then(respondAsActivity);
2157
+ handler.recordPendingCollectionMetrics("served", response);
2158
+ return response;
2159
+ } catch (e) {
2160
+ if (!deferPendingCollectionMetrics(e, (result, response) => handler.recordPendingCollectionMetrics(result, response))) handler.recordPendingCollectionMetrics("error");
2161
+ throw e;
2162
+ }
2069
2163
  }
2070
2164
  /**
2071
2165
  * Handling custom collections with support for pagination and filtering.
@@ -2085,6 +2179,7 @@ var CustomCollectionHandler = class {
2085
2179
  context;
2086
2180
  callbacks;
2087
2181
  tracerProvider;
2182
+ meterProvider;
2088
2183
  Collection;
2089
2184
  CollectionPage;
2090
2185
  filterPredicate;
@@ -2112,6 +2207,7 @@ var CustomCollectionHandler = class {
2112
2207
  */
2113
2208
  #dispatcher;
2114
2209
  #collection = null;
2210
+ #pendingCollectionMetrics = [];
2115
2211
  /**
2116
2212
  * Creates a new CustomCollection instance.
2117
2213
  * @param name The name of the collection.
@@ -2123,12 +2219,13 @@ var CustomCollectionHandler = class {
2123
2219
  * @param CollectionPage The CollectionPage constructor.
2124
2220
  * @param filterPredicate Optional filter predicate for items.
2125
2221
  */
2126
- constructor(name$2, values, context, callbacks, tracerProvider = trace.getTracerProvider(), Collection, CollectionPage, filterPredicate) {
2222
+ constructor(name$2, values, context, callbacks, tracerProvider = trace.getTracerProvider(), meterProvider, Collection, CollectionPage, filterPredicate) {
2127
2223
  this.name = name$2;
2128
2224
  this.values = values;
2129
2225
  this.context = context;
2130
2226
  this.callbacks = callbacks;
2131
2227
  this.tracerProvider = tracerProvider;
2228
+ this.meterProvider = meterProvider;
2132
2229
  this.Collection = Collection;
2133
2230
  this.CollectionPage = CollectionPage;
2134
2231
  this.filterPredicate = filterPredicate;
@@ -2181,10 +2278,12 @@ var CustomCollectionHandler = class {
2181
2278
  const { prevCursor, nextCursor } = pages;
2182
2279
  const partOf = new URL(id);
2183
2280
  partOf.searchParams.delete("cursor");
2281
+ const items = this.filterItems(pages.items);
2282
+ this.recordPendingCollectionItemCount(true, items.length);
2184
2283
  return {
2185
2284
  id,
2186
2285
  partOf,
2187
- items: this.filterItems(pages.items),
2286
+ items,
2188
2287
  prev: this.appendToUrl(prevCursor),
2189
2288
  next: this.appendToUrl(nextCursor)
2190
2289
  };
@@ -2197,11 +2296,16 @@ var CustomCollectionHandler = class {
2197
2296
  */
2198
2297
  async getProps(firstCursor) {
2199
2298
  const lastCursor = await this.callbacks.lastCursor?.(this.context, this.values);
2299
+ const totalItems = await this.totalItems;
2300
+ if (totalItems != null) this.#pendingCollectionMetrics.push({
2301
+ page: false,
2302
+ totalItems: Number(totalItems)
2303
+ });
2200
2304
  return {
2201
2305
  id: this.#id,
2202
2306
  first: this.appendToUrl(firstCursor),
2203
2307
  last: this.appendToUrl(lastCursor),
2204
- totalItems: await this.totalItems
2308
+ totalItems
2205
2309
  };
2206
2310
  }
2207
2311
  /**
@@ -2211,10 +2315,12 @@ var CustomCollectionHandler = class {
2211
2315
  async getPropsWithoutCursor() {
2212
2316
  const totalItems = await this.totalItems;
2213
2317
  const pages = await this.getPages({ totalItems });
2318
+ const items = this.filterItems(pages.items);
2319
+ this.recordPendingCollectionItemCount(false, items.length);
2214
2320
  return {
2215
2321
  id: this.#id,
2216
2322
  totalItems,
2217
- items: this.filterItems(pages.items)
2323
+ items
2218
2324
  };
2219
2325
  }
2220
2326
  /**
@@ -2249,12 +2355,24 @@ var CustomCollectionHandler = class {
2249
2355
  * @returns A function that handles the span operation.
2250
2356
  */
2251
2357
  spanPages = ({ totalItems = null, cursor = null }) => async (span) => {
2358
+ const pageMetricBase = this.metricBase(cursor !== null);
2359
+ const started = performance.now();
2252
2360
  try {
2253
2361
  if (totalItems !== null) span.setAttribute(this.ATTRS.TOTAL_ITEMS, totalItems);
2254
2362
  const page = await this.dispatch(cursor);
2363
+ const durationMs = getDurationMs(started);
2255
2364
  span.setAttribute(this.ATTRS.ITEMS, page.items.length);
2365
+ this.#pendingCollectionMetrics.push({
2366
+ page: pageMetricBase.page,
2367
+ dispatchDurationMs: durationMs,
2368
+ totalItems: totalItems == null ? void 0 : Number(totalItems)
2369
+ });
2256
2370
  return page;
2257
2371
  } catch (e) {
2372
+ this.#pendingCollectionMetrics.push({
2373
+ page: cursor !== null,
2374
+ dispatchDurationMs: getDurationMs(started)
2375
+ });
2258
2376
  const message = e instanceof Error ? e.message : String(e);
2259
2377
  span.setStatus({
2260
2378
  code: SpanStatusCode.ERROR,
@@ -2281,6 +2399,37 @@ var CustomCollectionHandler = class {
2281
2399
  filterItems(items) {
2282
2400
  return filterCollectionItems(items, this.name, this.filterPredicate);
2283
2401
  }
2402
+ metricBase(page) {
2403
+ return {
2404
+ kind: "custom",
2405
+ page,
2406
+ dispatcher: "custom"
2407
+ };
2408
+ }
2409
+ metricAttributes(page, result, response) {
2410
+ return collectionAttributes(this.metricBase(page), result, response);
2411
+ }
2412
+ recordPendingCollectionItemCount(page, itemCount) {
2413
+ for (let i = this.#pendingCollectionMetrics.length - 1; i >= 0; i--) {
2414
+ const measurement = this.#pendingCollectionMetrics[i];
2415
+ if (measurement.page === page && measurement.dispatchDurationMs != null && measurement.itemCount == null) {
2416
+ measurement.itemCount = itemCount;
2417
+ return;
2418
+ }
2419
+ }
2420
+ this.#pendingCollectionMetrics.push({
2421
+ page,
2422
+ itemCount
2423
+ });
2424
+ }
2425
+ recordPendingCollectionMetrics(result, response) {
2426
+ for (const measurement of this.#pendingCollectionMetrics.splice(0)) {
2427
+ const attrs = this.metricAttributes(measurement.page, result, response);
2428
+ if (measurement.dispatchDurationMs != null) recordCollectionDispatchDuration(this.meterProvider, measurement.dispatchDurationMs, attrs);
2429
+ if (measurement.itemCount != null) recordCollectionPageItems(this.meterProvider, measurement.itemCount, attrs);
2430
+ if (measurement.totalItems != null) recordCollectionTotalItems(this.meterProvider, measurement.totalItems, attrs);
2431
+ }
2432
+ }
2284
2433
  /**
2285
2434
  * Appends a cursor to the URL if it exists.
2286
2435
  * @param cursor The cursor to append, or null/undefined.
@@ -2344,14 +2493,36 @@ var CustomCollectionHandler = class {
2344
2493
  */
2345
2494
  function exceptWrapper(handler) {
2346
2495
  return async (request, handlerParams) => {
2496
+ const page = new URL(request.url).searchParams.get("cursor") != null;
2497
+ const { meterProvider } = handlerParams;
2498
+ const metricBase = {
2499
+ kind: "custom",
2500
+ page,
2501
+ dispatcher: "custom"
2502
+ };
2347
2503
  try {
2348
- return await handler(request, handlerParams);
2504
+ const response = await handler(request, handlerParams);
2505
+ recordCollectionRequest(meterProvider, collectionAttributes(metricBase, "served", response));
2506
+ return response;
2349
2507
  } catch (error) {
2350
2508
  const { onNotFound, onUnauthorized } = handlerParams;
2351
2509
  switch (error?.constructor) {
2352
- case ItemsNotFoundError: return await onNotFound(request);
2353
- case UnauthorizedError: return await onUnauthorized(request);
2354
- default: throw error;
2510
+ case ItemsNotFoundError: {
2511
+ const response = await onNotFound(request);
2512
+ recordDeferredPendingCollectionMetrics(error, "not_found", response);
2513
+ recordCollectionRequest(meterProvider, collectionAttributes(metricBase, "not_found", response));
2514
+ return response;
2515
+ }
2516
+ case UnauthorizedError: {
2517
+ const response = await onUnauthorized(request);
2518
+ recordDeferredPendingCollectionMetrics(error, "unauthorized", response);
2519
+ recordCollectionRequest(meterProvider, collectionAttributes(metricBase, "unauthorized", response));
2520
+ return response;
2521
+ }
2522
+ default:
2523
+ recordDeferredPendingCollectionMetrics(error, "error");
2524
+ recordCollectionRequest(meterProvider, collectionAttributes(metricBase, "error"));
2525
+ throw error;
2355
2526
  }
2356
2527
  }
2357
2528
  };
@@ -4028,7 +4199,15 @@ var FederationImpl = class extends FederationBuilderImpl {
4028
4199
  }
4029
4200
  if (request.method !== "POST" && !acceptsJsonLd(request)) {
4030
4201
  metricState.endpoint = "not_acceptable";
4031
- return await onNotAcceptable(request);
4202
+ const response = await onNotAcceptable(request);
4203
+ const collectionRoute = getCollectionMetricRoute(routeName);
4204
+ if (collectionRoute != null) recordCollectionRequest(this._meterProvider, {
4205
+ ...collectionRoute,
4206
+ page: url.searchParams.get("cursor") != null,
4207
+ result: "not_acceptable",
4208
+ statusCode: response.status
4209
+ });
4210
+ return response;
4032
4211
  }
4033
4212
  switch (routeName) {
4034
4213
  case "actor":
@@ -4089,6 +4268,7 @@ var FederationImpl = class extends FederationBuilderImpl {
4089
4268
  context,
4090
4269
  collectionCallbacks: this.outboxCallbacks,
4091
4270
  tracerProvider: this.tracerProvider,
4271
+ meterProvider: this._meterProvider,
4092
4272
  onUnauthorized,
4093
4273
  onNotFound
4094
4274
  });
@@ -4100,6 +4280,7 @@ var FederationImpl = class extends FederationBuilderImpl {
4100
4280
  context,
4101
4281
  collectionCallbacks: this.inboxCallbacks,
4102
4282
  tracerProvider: this.tracerProvider,
4283
+ meterProvider: this._meterProvider,
4103
4284
  onUnauthorized,
4104
4285
  onNotFound
4105
4286
  });
@@ -4139,6 +4320,7 @@ var FederationImpl = class extends FederationBuilderImpl {
4139
4320
  context,
4140
4321
  collectionCallbacks: this.followingCallbacks,
4141
4322
  tracerProvider: this.tracerProvider,
4323
+ meterProvider: this._meterProvider,
4142
4324
  onUnauthorized,
4143
4325
  onNotFound
4144
4326
  });
@@ -4162,6 +4344,7 @@ var FederationImpl = class extends FederationBuilderImpl {
4162
4344
  filterPredicate: baseUrl != null ? ((i) => (i instanceof URL ? i.href : i.id?.href ?? "").startsWith(baseUrl)) : void 0,
4163
4345
  collectionCallbacks: this.followersCallbacks,
4164
4346
  tracerProvider: this.tracerProvider,
4347
+ meterProvider: this._meterProvider,
4165
4348
  onUnauthorized,
4166
4349
  onNotFound
4167
4350
  });
@@ -4173,6 +4356,7 @@ var FederationImpl = class extends FederationBuilderImpl {
4173
4356
  context,
4174
4357
  collectionCallbacks: this.likedCallbacks,
4175
4358
  tracerProvider: this.tracerProvider,
4359
+ meterProvider: this._meterProvider,
4176
4360
  onUnauthorized,
4177
4361
  onNotFound
4178
4362
  });
@@ -4183,6 +4367,7 @@ var FederationImpl = class extends FederationBuilderImpl {
4183
4367
  context,
4184
4368
  collectionCallbacks: this.featuredCallbacks,
4185
4369
  tracerProvider: this.tracerProvider,
4370
+ meterProvider: this._meterProvider,
4186
4371
  onUnauthorized,
4187
4372
  onNotFound
4188
4373
  });
@@ -4193,6 +4378,7 @@ var FederationImpl = class extends FederationBuilderImpl {
4193
4378
  context,
4194
4379
  collectionCallbacks: this.featuredTagsCallbacks,
4195
4380
  tracerProvider: this.tracerProvider,
4381
+ meterProvider: this._meterProvider,
4196
4382
  onUnauthorized,
4197
4383
  onNotFound
4198
4384
  });
@@ -4205,6 +4391,7 @@ var FederationImpl = class extends FederationBuilderImpl {
4205
4391
  values: route.values,
4206
4392
  collectionCallbacks: callbacks,
4207
4393
  tracerProvider: this.tracerProvider,
4394
+ meterProvider: this._meterProvider,
4208
4395
  onUnauthorized,
4209
4396
  onNotFound
4210
4397
  });
@@ -4218,6 +4405,7 @@ var FederationImpl = class extends FederationBuilderImpl {
4218
4405
  values: route.values,
4219
4406
  collectionCallbacks: callbacks,
4220
4407
  tracerProvider: this.tracerProvider,
4408
+ meterProvider: this._meterProvider,
4221
4409
  onUnauthorized,
4222
4410
  onNotFound
4223
4411
  });
@@ -4250,6 +4438,29 @@ function getEndpointCategory(routeName) {
4250
4438
  default: return "not_found";
4251
4439
  }
4252
4440
  }
4441
+ function getCollectionMetricRoute(routeName) {
4442
+ switch (routeName) {
4443
+ case "inbox":
4444
+ case "outbox":
4445
+ case "following":
4446
+ case "followers":
4447
+ case "liked":
4448
+ case "featured": return {
4449
+ kind: routeName,
4450
+ dispatcher: "built_in"
4451
+ };
4452
+ case "featuredTags": return {
4453
+ kind: "featured_tags",
4454
+ dispatcher: "built_in"
4455
+ };
4456
+ case "collection":
4457
+ case "orderedCollection": return {
4458
+ kind: "custom",
4459
+ dispatcher: "custom"
4460
+ };
4461
+ default: return;
4462
+ }
4463
+ }
4253
4464
  const FANOUT_THRESHOLD = 5;
4254
4465
  var ContextImpl = class ContextImpl {
4255
4466
  url;
@@ -1,5 +1,5 @@
1
1
  import "@js-temporal/polyfill";
2
2
  import "urlpattern-polyfill";
3
3
  globalThis.addEventListener = () => {};
4
- import { n as FederationImpl } from "./middleware-xtTRaiJL.mjs";
4
+ import { n as FederationImpl } from "./middleware-DHM2Pjqf.mjs";
5
5
  export { FederationImpl };