@fedify/fedify 1.3.0-dev.570 → 1.3.0-dev.575

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.
package/CHANGES.md CHANGED
@@ -59,6 +59,9 @@ To be released.
59
59
 
60
60
  - Added `getTypeId()` function.
61
61
 
62
+ - `Context.sendActivity()` and `InboxContext.forwardActivity()` methods now
63
+ reject when they fail to enqueue the task. [[#192]]
64
+
62
65
  - Fedify now supports OpenTelemetry for tracing. [[#170]]
63
66
 
64
67
  - Added `Context.tracerProvider` property.
@@ -103,6 +106,7 @@ To be released.
103
106
  [#173]: https://github.com/dahlia/fedify/issues/173
104
107
  [#183]: https://github.com/dahlia/fedify/pull/183
105
108
  [#186]: https://github.com/dahlia/fedify/pull/186
109
+ [#192]: https://github.com/dahlia/fedify/issues/192
106
110
 
107
111
 
108
112
  Version 1.2.8
package/esm/deno.js CHANGED
@@ -1,6 +1,6 @@
1
1
  export default {
2
2
  "name": "@fedify/fedify",
3
- "version": "1.3.0-dev.570+61d94afc",
3
+ "version": "1.3.0-dev.575+9cfc4531",
4
4
  "license": "MIT",
5
5
  "exports": {
6
6
  ".": "./mod.ts",
@@ -71,28 +71,12 @@ export async function handleObject(request, { values, context, objectDispatcher,
71
71
  },
72
72
  });
73
73
  }
74
- export function handleCollection(request, params) {
75
- const name = params.name.trim().replace(/\s+/g, "_");
76
- const tracerProvider = params.tracerProvider ?? trace.getTracerProvider();
74
+ export async function handleCollection(request, { name, identifier, uriGetter, filter, filterPredicate, context, collectionCallbacks, tracerProvider, onUnauthorized, onNotFound, onNotAcceptable, }) {
75
+ const spanName = name.trim().replace(/\s+/g, "_");
76
+ tracerProvider = tracerProvider ?? trace.getTracerProvider();
77
77
  const tracer = tracerProvider.getTracer(metadata.name, metadata.version);
78
78
  const url = new URL(request.url);
79
79
  const cursor = url.searchParams.get("cursor");
80
- return tracer.startActiveSpan(cursor == null
81
- ? `activitypub.dispatch_collection ${name}`
82
- : `activitypub.dispatch_collection_page ${name}`, { kind: SpanKind.SERVER }, async (span) => {
83
- try {
84
- return await handleCollectionInternal(request, cursor, params, span);
85
- }
86
- catch (e) {
87
- span.setStatus({ code: SpanStatusCode.ERROR, message: String(e) });
88
- throw e;
89
- }
90
- finally {
91
- span.end();
92
- }
93
- });
94
- }
95
- async function handleCollectionInternal(request, cursor, { name, identifier, uriGetter, filter, filterPredicate, context, collectionCallbacks, onUnauthorized, onNotFound, onNotAcceptable, }, span) {
96
80
  if (collectionCallbacks == null)
97
81
  return await onNotFound(request);
98
82
  let collection;
@@ -100,21 +84,41 @@ async function handleCollectionInternal(request, cursor, { name, identifier, uri
100
84
  if (cursor == null) {
101
85
  const firstCursor = await collectionCallbacks.firstCursor?.(context, identifier);
102
86
  const totalItems = await collectionCallbacks.counter?.(context, identifier);
103
- if (totalItems != null) {
104
- span.setAttribute("activitypub.collection.total_items", Number(totalItems));
105
- }
106
87
  if (firstCursor == null) {
107
- const page = await collectionCallbacks.dispatcher(context, identifier, null, filter);
108
- if (page == null) {
109
- span.setStatus({ code: SpanStatusCode.ERROR });
110
- return await onNotFound(request);
111
- }
112
- const { items } = page;
113
- span.setAttribute("fedify.collection.items", items.length);
88
+ const itemsOrResponse = await tracer.startActiveSpan(`activitypub.dispatch_collection ${spanName}`, {
89
+ kind: SpanKind.SERVER,
90
+ attributes: {
91
+ "activitypub.collection.id": baseUri.href,
92
+ "activitypub.collection.type": OrderedCollection.typeId.href,
93
+ },
94
+ }, async (span) => {
95
+ if (totalItems != null) {
96
+ span.setAttribute("activitypub.collection.total_items", Number(totalItems));
97
+ }
98
+ try {
99
+ const page = await collectionCallbacks.dispatcher(context, identifier, null, filter);
100
+ if (page == null) {
101
+ span.setStatus({ code: SpanStatusCode.ERROR });
102
+ return await onNotFound(request);
103
+ }
104
+ const { items } = page;
105
+ span.setAttribute("fedify.collection.items", items.length);
106
+ return items;
107
+ }
108
+ catch (e) {
109
+ span.setStatus({ code: SpanStatusCode.ERROR, message: String(e) });
110
+ throw e;
111
+ }
112
+ finally {
113
+ span.end();
114
+ }
115
+ });
116
+ if (itemsOrResponse instanceof Response)
117
+ return itemsOrResponse;
114
118
  collection = new OrderedCollection({
115
119
  id: baseUri,
116
120
  totalItems: totalItems == null ? null : Number(totalItems),
117
- items: filterCollectionItems(items, name, filterPredicate),
121
+ items: filterCollectionItems(itemsOrResponse, name, filterPredicate),
118
122
  });
119
123
  }
120
124
  else {
@@ -135,16 +139,36 @@ async function handleCollectionInternal(request, cursor, { name, identifier, uri
135
139
  }
136
140
  }
137
141
  else {
138
- span.setAttribute("fedify.collection.cursor", cursor);
139
142
  const uri = new URL(baseUri);
140
143
  uri.searchParams.set("cursor", cursor);
141
- const page = await collectionCallbacks.dispatcher(context, identifier, cursor, filter);
142
- if (page == null) {
143
- span.setStatus({ code: SpanStatusCode.ERROR });
144
- return await onNotFound(request);
145
- }
146
- const { items, prevCursor, nextCursor } = page;
147
- span.setAttribute("fedify.collection.items", items.length);
144
+ const pageOrResponse = await tracer.startActiveSpan(`activitypub.dispatch_collection_page ${name}`, {
145
+ kind: SpanKind.SERVER,
146
+ attributes: {
147
+ "activitypub.collection.id": uri.href,
148
+ "activitypub.collection.type": OrderedCollectionPage.typeId.href,
149
+ "fedify.collection.cursor": cursor,
150
+ },
151
+ }, async (span) => {
152
+ try {
153
+ const page = await collectionCallbacks.dispatcher(context, identifier, cursor, filter);
154
+ if (page == null) {
155
+ span.setStatus({ code: SpanStatusCode.ERROR });
156
+ return await onNotFound(request);
157
+ }
158
+ span.setAttribute("fedify.collection.items", page.items.length);
159
+ return page;
160
+ }
161
+ catch (e) {
162
+ span.setStatus({ code: SpanStatusCode.ERROR, message: String(e) });
163
+ throw e;
164
+ }
165
+ finally {
166
+ span.end();
167
+ }
168
+ });
169
+ if (pageOrResponse instanceof Response)
170
+ return pageOrResponse;
171
+ const { items, prevCursor, nextCursor } = pageOrResponse;
148
172
  let prev = null;
149
173
  if (prevCursor != null) {
150
174
  prev = new URL(context.url);
@@ -174,10 +198,6 @@ async function handleCollectionInternal(request, cursor, { name, identifier, uri
174
198
  return await onUnauthorized(request);
175
199
  }
176
200
  }
177
- if (collection.id != null) {
178
- span.setAttribute("activitypub.collection.id", collection.id.href);
179
- }
180
- span.setAttribute("activitypub.collection.type", getTypeId(collection).href);
181
201
  const jsonLd = await collection.toJsonLd(context);
182
202
  return new Response(JSON.stringify(jsonLd), {
183
203
  headers: {
@@ -425,7 +445,7 @@ export async function handleInbox(request, { recipient, context, inboxContextFac
425
445
  });
426
446
  }
427
447
  try {
428
- await listener(inboxContextFactory(recipient, json), activity);
448
+ await listener(inboxContextFactory(recipient, json, activity.id?.href, getTypeId(activity).href), activity);
429
449
  }
430
450
  catch (error) {
431
451
  try {
@@ -1,6 +1,6 @@
1
1
  import * as dntShim from "../_dnt.shims.js";
2
2
  import { getLogger, withContext } from "@logtape/logtape";
3
- import { SpanKind, SpanStatusCode, trace, } from "@opentelemetry/api";
3
+ import { context, propagation, SpanKind, SpanStatusCode, trace, } from "@opentelemetry/api";
4
4
  import { ATTR_HTTP_REQUEST_HEADER, ATTR_HTTP_REQUEST_METHOD, ATTR_HTTP_RESPONSE_HEADER, ATTR_HTTP_RESPONSE_STATUS_CODE, ATTR_URL_FULL, } from "@opentelemetry/semantic-conventions";
5
5
  import metadata from "../deno.js";
6
6
  import { handleNodeInfo, handleNodeInfoJrd } from "../nodeinfo/handler.js";
@@ -157,9 +157,31 @@ export class FederationImpl {
157
157
  await Promise.all(promises);
158
158
  }
159
159
  #listenQueue(ctxData, message) {
160
+ const tracer = this.#getTracer();
160
161
  return withContext({ messageId: message.id }, async () => {
161
162
  if (message.type === "outbox") {
162
- await this.#listenOutboxMessage(ctxData, message);
163
+ const extractedContext = propagation.extract(context.active(), message.traceContext);
164
+ await tracer.startActiveSpan("activitypub.outbox", {
165
+ kind: SpanKind.CONSUMER,
166
+ attributes: { "activitypub.activity.type": message.activityType },
167
+ }, extractedContext, async (span) => {
168
+ if (message.activityId != null) {
169
+ span.setAttribute("activitypub.activity.id", message.activityId);
170
+ }
171
+ try {
172
+ await this.#listenOutboxMessage(ctxData, message);
173
+ }
174
+ catch (e) {
175
+ span.setStatus({
176
+ code: SpanStatusCode.ERROR,
177
+ message: String(e),
178
+ });
179
+ throw e;
180
+ }
181
+ finally {
182
+ span.end();
183
+ }
184
+ });
163
185
  }
164
186
  else if (message.type === "inbox") {
165
187
  await this.#listenInboxMessage(ctxData, message);
@@ -194,7 +216,9 @@ export class FederationImpl {
194
216
  keys,
195
217
  activity: message.activity,
196
218
  activityId: message.activityId,
219
+ activityType: message.activityType,
197
220
  inbox: new URL(message.inbox),
221
+ sharedInbox: message.sharedInbox,
198
222
  headers: new Headers(message.headers),
199
223
  tracerProvider: this.tracerProvider,
200
224
  });
@@ -220,7 +244,7 @@ export class FederationImpl {
220
244
  if (delay != null) {
221
245
  logger.error("Failed to send activity {activityId} to {inbox} (attempt " +
222
246
  "#{attempt}); retry...:\n{error}", { ...logData, error });
223
- this.outboxQueue?.enqueue({
247
+ await this.outboxQueue?.enqueue({
224
248
  ...message,
225
249
  attempt: message.attempt + 1,
226
250
  }, {
@@ -286,7 +310,7 @@ export class FederationImpl {
286
310
  return;
287
311
  }
288
312
  try {
289
- await listener(context.toInboxContext(message.identifier, message.activity), activity);
313
+ await listener(context.toInboxContext(message.identifier, message.activity, activity.id?.href, getTypeId(activity).href), activity);
290
314
  }
291
315
  catch (error) {
292
316
  try {
@@ -314,7 +338,7 @@ export class FederationImpl {
314
338
  activity: message.activity,
315
339
  recipient: message.identifier,
316
340
  });
317
- this.inboxQueue?.enqueue({
341
+ await this.inboxQueue?.enqueue({
318
342
  ...message,
319
343
  attempt: message.attempt + 1,
320
344
  }, {
@@ -960,7 +984,7 @@ export class FederationImpl {
960
984
  };
961
985
  return setters;
962
986
  }
963
- async sendActivity(keys, recipients, activity, options) {
987
+ async sendActivity(keys, recipients, activity, options, span) {
964
988
  const logger = getLogger(["fedify", "federation", "outbox"]);
965
989
  const { preferSharedInbox, immediate, excludeBaseUris, collectionSync, contextData, } = options;
966
990
  if (keys.length < 1) {
@@ -974,9 +998,9 @@ export class FederationImpl {
974
998
  throw new TypeError("The activity to send must have at least one actor property.");
975
999
  }
976
1000
  if (activity.id == null) {
977
- activity = activity.clone({
978
- id: new URL(`urn:uuid:${dntShim.crypto.randomUUID()}`),
979
- });
1001
+ const id = new URL(`urn:uuid:${dntShim.crypto.randomUUID()}`);
1002
+ activity = activity.clone({ id });
1003
+ span?.setAttribute("activitypub.activity.id", id.href);
980
1004
  }
981
1005
  const inboxes = extractInboxes({
982
1006
  recipients: Array.isArray(recipients) ? recipients : [recipients],
@@ -1061,9 +1085,11 @@ export class FederationImpl {
1061
1085
  keys,
1062
1086
  activity: jsonLd,
1063
1087
  activityId: activity.id?.href,
1088
+ activityType: getTypeId(activity).href,
1064
1089
  inbox: new URL(inbox),
1090
+ sharedInbox: inboxes[inbox].sharedInbox,
1065
1091
  headers: collectionSync == null ? undefined : new Headers({
1066
- "Collection-Synchronization": await buildCollectionSynchronizationHeader(collectionSync, inboxes[inbox]),
1092
+ "Collection-Synchronization": await buildCollectionSynchronizationHeader(collectionSync, inboxes[inbox].actorIds),
1067
1093
  }),
1068
1094
  tracerProvider: this.tracerProvider,
1069
1095
  }));
@@ -1079,6 +1105,9 @@ export class FederationImpl {
1079
1105
  }
1080
1106
  if (!this.manuallyStartQueue)
1081
1107
  this.#startQueue(contextData);
1108
+ const carrier = {};
1109
+ propagation.inject(context.active(), carrier);
1110
+ const promises = [];
1082
1111
  for (const inbox in inboxes) {
1083
1112
  const message = {
1084
1113
  type: "outbox",
@@ -1086,14 +1115,28 @@ export class FederationImpl {
1086
1115
  keys: keyJwkPairs,
1087
1116
  activity: jsonLd,
1088
1117
  activityId: activity.id?.href,
1118
+ activityType: getTypeId(activity).href,
1089
1119
  inbox,
1120
+ sharedInbox: inboxes[inbox].sharedInbox,
1090
1121
  started: new Date().toISOString(),
1091
1122
  attempt: 0,
1092
1123
  headers: collectionSync == null ? {} : {
1093
- "Collection-Synchronization": await buildCollectionSynchronizationHeader(collectionSync, inboxes[inbox]),
1124
+ "Collection-Synchronization": await buildCollectionSynchronizationHeader(collectionSync, inboxes[inbox].actorIds),
1094
1125
  },
1126
+ traceContext: carrier,
1095
1127
  };
1096
- this.outboxQueue.enqueue(message);
1128
+ promises.push(this.outboxQueue.enqueue(message));
1129
+ }
1130
+ const results = await Promise.allSettled(promises);
1131
+ const errors = results
1132
+ .filter((r) => r.status === "rejected")
1133
+ .map((r) => r.reason);
1134
+ if (errors.length > 0) {
1135
+ logger.error("Failed to enqueue activity {activityId} to send later: {errors}", { activityId: activity.id.href, errors });
1136
+ if (errors.length > 1) {
1137
+ throw new AggregateError(errors, `Failed to enqueue activity ${activityId} to send later.`);
1138
+ }
1139
+ throw errors[0];
1097
1140
  }
1098
1141
  }
1099
1142
  fetch(request, options) {
@@ -1368,8 +1411,8 @@ export class ContextImpl {
1368
1411
  this.invokedFromActorKeyPairsDispatcher =
1369
1412
  invokedFromActorKeyPairsDispatcher;
1370
1413
  }
1371
- toInboxContext(recipient, activity) {
1372
- return new InboxContextImpl(recipient, activity, {
1414
+ toInboxContext(recipient, activity, activityId, activityType) {
1415
+ return new InboxContextImpl(recipient, activity, activityId, activityType, {
1373
1416
  url: this.url,
1374
1417
  federation: this.federation,
1375
1418
  data: this.data,
@@ -1738,7 +1781,36 @@ export class ContextImpl {
1738
1781
  contextLoader: options.contextLoader ?? this.contextLoader,
1739
1782
  });
1740
1783
  }
1741
- async sendActivity(sender, recipients, activity, options = {}) {
1784
+ sendActivity(sender, recipients, activity, options = {}) {
1785
+ const tracer = this.tracerProvider.getTracer(metadata.name, metadata.version);
1786
+ return tracer.startActiveSpan("activitypub.outbox", {
1787
+ kind: this.federation.outboxQueue == null || options.immediate
1788
+ ? SpanKind.CLIENT
1789
+ : SpanKind.PRODUCER,
1790
+ attributes: {
1791
+ "activitypub.activity.type": getTypeId(activity).href,
1792
+ "activitypub.activity.to": activity.toIds.map((to) => to.href),
1793
+ "activitypub.activity.cc": activity.toIds.map((cc) => cc.href),
1794
+ "activitypub.activity.bto": activity.btoIds.map((bto) => bto.href),
1795
+ "activitypub.activity.bcc": activity.toIds.map((bcc) => bcc.href),
1796
+ },
1797
+ }, async (span) => {
1798
+ try {
1799
+ if (activity.id != null) {
1800
+ span.setAttribute("activitypub.activity.id", activity.id.href);
1801
+ }
1802
+ await this.sendActivityInternal(sender, recipients, activity, options, span);
1803
+ }
1804
+ catch (e) {
1805
+ span.setStatus({ code: SpanStatusCode.ERROR, message: String(e) });
1806
+ throw e;
1807
+ }
1808
+ finally {
1809
+ span.end();
1810
+ }
1811
+ });
1812
+ }
1813
+ async sendActivityInternal(sender, recipients, activity, options = {}, span) {
1742
1814
  let keys;
1743
1815
  let identifier = null;
1744
1816
  if ("identifier" in sender || "username" in sender || "handle" in sender) {
@@ -1766,6 +1838,7 @@ export class ContextImpl {
1766
1838
  identifier = mapped;
1767
1839
  }
1768
1840
  }
1841
+ span.setAttribute("fedify.actor.identifier", identifier);
1769
1842
  keys = await this.getKeyPairsFromIdentifier(identifier);
1770
1843
  if (keys.length < 1) {
1771
1844
  throw new Error(`No key pair found for actor ${JSON.stringify(identifier)}.`);
@@ -1805,7 +1878,8 @@ export class ContextImpl {
1805
1878
  else {
1806
1879
  expandedRecipients = [recipients];
1807
1880
  }
1808
- return await this.federation.sendActivity(keys, expandedRecipients, activity, opts);
1881
+ span.setAttribute("activitypub.inboxes", expandedRecipients.length);
1882
+ return await this.federation.sendActivity(keys, expandedRecipients, activity, opts, span);
1809
1883
  }
1810
1884
  async *getFollowers(identifier) {
1811
1885
  if (this.federation.followersCallbacks == null) {
@@ -1912,12 +1986,39 @@ class RequestContextImpl extends ContextImpl {
1912
1986
  export class InboxContextImpl extends ContextImpl {
1913
1987
  recipient;
1914
1988
  activity;
1915
- constructor(recipient, activity, options) {
1989
+ activityId;
1990
+ activityType;
1991
+ constructor(recipient, activity, activityId, activityType, options) {
1916
1992
  super(options);
1917
1993
  this.recipient = recipient;
1918
1994
  this.activity = activity;
1995
+ this.activityId = activityId;
1996
+ this.activityType = activityType;
1919
1997
  }
1920
- async forwardActivity(forwarder, recipients, options) {
1998
+ forwardActivity(forwarder, recipients, options) {
1999
+ const tracer = this.tracerProvider.getTracer(metadata.name, metadata.version);
2000
+ return tracer.startActiveSpan("activitypub.outbox", {
2001
+ kind: this.federation.outboxQueue == null || options?.immediate
2002
+ ? SpanKind.CLIENT
2003
+ : SpanKind.PRODUCER,
2004
+ attributes: { "activitypub.activity.type": this.activityType },
2005
+ }, async (span) => {
2006
+ try {
2007
+ if (this.activityId != null) {
2008
+ span.setAttribute("activitypub.activity.id", this.activityId);
2009
+ }
2010
+ await this.forwardActivityInternal(forwarder, recipients, options);
2011
+ }
2012
+ catch (e) {
2013
+ span.setStatus({ code: SpanStatusCode.ERROR, message: String(e) });
2014
+ throw e;
2015
+ }
2016
+ finally {
2017
+ span.end();
2018
+ }
2019
+ });
2020
+ }
2021
+ async forwardActivityInternal(forwarder, recipients, options) {
1921
2022
  const logger = getLogger(["fedify", "federation", "inbox"]);
1922
2023
  let keys;
1923
2024
  let identifier = null;
@@ -1961,12 +2062,10 @@ export class InboxContextImpl extends ContextImpl {
1961
2062
  else {
1962
2063
  keys = [forwarder];
1963
2064
  }
1964
- let activityId = undefined;
1965
2065
  if (!hasSignature(this.activity)) {
1966
2066
  let hasProof;
1967
2067
  try {
1968
2068
  const activity = await Activity.fromJsonLd(this.activity, this);
1969
- activityId = activity.id?.href;
1970
2069
  hasProof = await activity.getProof() != null;
1971
2070
  }
1972
2071
  catch {
@@ -1980,15 +2079,6 @@ export class InboxContextImpl extends ContextImpl {
1980
2079
  "them due to the lack of a signature/proof.");
1981
2080
  }
1982
2081
  }
1983
- if (activityId == null && typeof this.activity === "object" &&
1984
- this.activity != null) {
1985
- activityId =
1986
- "@id" in this.activity && typeof this.activity["@id"] === "string"
1987
- ? this.activity["@id"]
1988
- : "id" in this.activity && typeof this.activity.id === "string"
1989
- ? this.activity.id
1990
- : undefined;
1991
- }
1992
2082
  if (recipients === "followers") {
1993
2083
  if (identifier == null) {
1994
2084
  throw new Error('If recipients is "followers", ' +
@@ -2007,7 +2097,7 @@ export class InboxContextImpl extends ContextImpl {
2007
2097
  });
2008
2098
  logger.debug("Forwarding activity {activityId} to inboxes:\n{inboxes}", {
2009
2099
  inboxes: globalThis.Object.keys(inboxes),
2010
- activityId,
2100
+ activityId: this.activityId,
2011
2101
  activity: this.activity,
2012
2102
  });
2013
2103
  if (options?.immediate || this.federation.outboxQueue == null) {
@@ -2024,33 +2114,52 @@ export class InboxContextImpl extends ContextImpl {
2024
2114
  promises.push(sendActivity({
2025
2115
  keys,
2026
2116
  activity: this.activity,
2027
- activityId: activityId,
2117
+ activityId: this.activityId,
2118
+ activityType: this.activityType,
2028
2119
  inbox: new URL(inbox),
2120
+ sharedInbox: inboxes[inbox].sharedInbox,
2029
2121
  tracerProvider: this.tracerProvider,
2030
2122
  }));
2031
2123
  }
2032
2124
  await Promise.all(promises);
2033
2125
  return;
2034
2126
  }
2035
- logger.debug("Enqueuing activity {activityId} to forward later.", { activityId, activity: this.activity });
2127
+ logger.debug("Enqueuing activity {activityId} to forward later.", { activityId: this.activityId, activity: this.activity });
2036
2128
  const keyJwkPairs = [];
2037
2129
  for (const { keyId, privateKey } of keys) {
2038
2130
  const privateKeyJwk = await exportJwk(privateKey);
2039
2131
  keyJwkPairs.push({ keyId: keyId.href, privateKey: privateKeyJwk });
2040
2132
  }
2133
+ const carrier = {};
2134
+ propagation.inject(context.active(), carrier);
2135
+ const promises = [];
2041
2136
  for (const inbox in inboxes) {
2042
2137
  const message = {
2043
2138
  type: "outbox",
2044
2139
  id: dntShim.crypto.randomUUID(),
2045
2140
  keys: keyJwkPairs,
2046
2141
  activity: this.activity,
2047
- activityId,
2142
+ activityId: this.activityId,
2143
+ activityType: this.activityType,
2048
2144
  inbox,
2145
+ sharedInbox: inboxes[inbox].sharedInbox,
2049
2146
  started: new Date().toISOString(),
2050
2147
  attempt: 0,
2051
2148
  headers: {},
2149
+ traceContext: carrier,
2052
2150
  };
2053
- this.federation.outboxQueue.enqueue(message);
2151
+ promises.push(this.federation.outboxQueue.enqueue(message));
2152
+ }
2153
+ const results = await Promise.allSettled(promises);
2154
+ const errors = results
2155
+ .filter((r) => r.status === "rejected")
2156
+ .map((r) => r.reason);
2157
+ if (errors.length > 0) {
2158
+ logger.error("Failed to enqueue activity {activityId} to forward later:\n{errors}", { activityId: this.activityId, errors });
2159
+ if (errors.length > 1) {
2160
+ throw new AggregateError(errors, `Failed to enqueue activity ${this.activityId} to forward later.`);
2161
+ }
2162
+ throw errors[0];
2054
2163
  }
2055
2164
  }
2056
2165
  }
@@ -1,4 +1,6 @@
1
1
  import { getLogger } from "@logtape/logtape";
2
+ import { SpanKind, SpanStatusCode, trace, } from "@opentelemetry/api";
3
+ import metadata from "../deno.js";
2
4
  import { signRequest } from "../sig/http.js";
3
5
  /**
4
6
  * Extracts the inbox URLs from recipients.
@@ -9,16 +11,22 @@ import { signRequest } from "../sig/http.js";
9
11
  export function extractInboxes({ recipients, preferSharedInbox, excludeBaseUris }) {
10
12
  const inboxes = {};
11
13
  for (const recipient of recipients) {
12
- const inbox = preferSharedInbox
13
- ? recipient.endpoints?.sharedInbox ?? recipient.inboxId
14
- : recipient.inboxId;
14
+ let inbox;
15
+ let sharedInbox = false;
16
+ if (preferSharedInbox && recipient.endpoints?.sharedInbox != null) {
17
+ inbox = recipient.endpoints.sharedInbox;
18
+ sharedInbox = true;
19
+ }
20
+ else {
21
+ inbox = recipient.inboxId;
22
+ }
15
23
  if (inbox != null && recipient.id != null) {
16
24
  if (excludeBaseUris != null &&
17
- excludeBaseUris.some((u) => u.origin == inbox.origin)) {
25
+ excludeBaseUris.some((u) => u.origin === inbox?.origin)) {
18
26
  continue;
19
27
  }
20
- inboxes[inbox.href] ??= new Set();
21
- inboxes[inbox.href].add(recipient.id.href);
28
+ inboxes[inbox.href] ??= { actorIds: new Set(), sharedInbox };
29
+ inboxes[inbox.href].actorIds.add(recipient.id.href);
22
30
  }
23
31
  }
24
32
  return inboxes;
@@ -30,7 +38,34 @@ export function extractInboxes({ recipients, preferSharedInbox, excludeBaseUris
30
38
  * See also {@link SendActivityParameters}.
31
39
  * @throws {Error} If the activity fails to send.
32
40
  */
33
- export async function sendActivity({ activity, activityId, keys, inbox, headers, tracerProvider, }) {
41
+ export function sendActivity(options) {
42
+ const tracerProvider = options.tracerProvider ?? trace.getTracerProvider();
43
+ const tracer = tracerProvider.getTracer(metadata.name, metadata.version);
44
+ return tracer.startActiveSpan("activitypub.send_activity", {
45
+ kind: SpanKind.CLIENT,
46
+ attributes: {
47
+ "activitypub.shared_inbox": options.sharedInbox ?? false,
48
+ },
49
+ }, async (span) => {
50
+ if (options.activityId != null) {
51
+ span.setAttribute("activitypub.activity.id", options.activityId);
52
+ }
53
+ if (options.activityType != null) {
54
+ span.setAttribute("activitypub.activity.type", options.activityType);
55
+ }
56
+ try {
57
+ await sendActivityInternal({ ...options, tracerProvider });
58
+ }
59
+ catch (e) {
60
+ span.setStatus({ code: SpanStatusCode.ERROR, message: String(e) });
61
+ throw e;
62
+ }
63
+ finally {
64
+ span.end();
65
+ }
66
+ });
67
+ }
68
+ async function sendActivityInternal({ activity, activityId, keys, inbox, headers, tracerProvider, }) {
34
69
  const logger = getLogger(["fedify", "federation", "outbox"]);
35
70
  headers = new Headers(headers);
36
71
  headers.set("Content-Type", "application/activity+json");