@fedify/fedify 2.3.0 → 2.3.2-dev.1556
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/README.md +10 -7
- package/dist/{builder-DYXTKqvC.mjs → builder-DosLp_w5.mjs} +2 -2
- package/dist/{circuit-breaker-CSWsyoef.mjs → circuit-breaker-DLS_n1x5.mjs} +190 -7
- package/dist/circuit-breaker-test-utils-BVnv3T76.mjs +14 -0
- package/dist/compat/mod.d.cts +1 -1
- package/dist/compat/mod.d.ts +1 -1
- package/dist/compat/transformers.test.mjs +1 -1
- package/dist/{context-BBVLF7lx.d.cts → context-Bjswu3eC.d.cts} +18 -1
- package/dist/{context-BU6jSQdo.d.ts → context-COxc_O-c.d.ts} +18 -1
- package/dist/{deno-ChfuM30K.mjs → deno-Bg8I1L40.mjs} +1 -1
- package/dist/{docloader-Bu-Oecom.mjs → docloader-Dxsk3R8v.mjs} +2 -2
- package/dist/federation/builder.test.mjs +1 -1
- package/dist/federation/circuit-breaker.test.mjs +622 -3
- package/dist/federation/handler.test.mjs +3 -3
- package/dist/federation/idempotency.test.mjs +2 -2
- package/dist/federation/metrics.test.mjs +1 -1
- package/dist/federation/middleware.test.mjs +16 -8
- package/dist/federation/mod.cjs +1 -1
- package/dist/federation/mod.d.cts +2 -2
- package/dist/federation/mod.d.ts +2 -2
- package/dist/federation/mod.js +1 -1
- package/dist/federation/send.test.mjs +3 -3
- package/dist/federation/temporal.test.mjs +1 -1
- package/dist/federation/webfinger.test.mjs +1 -1
- package/dist/{http-Cm_e9dY7.mjs → http-BaUm6xbz.mjs} +3 -3
- package/dist/{http-CiPBp7Ax.js → http-Bw8wN_Uk.js} +17 -7
- package/dist/{http-Q2nW0DHO.cjs → http-y8Cigpuq.cjs} +17 -7
- package/dist/{key-nrqRBwjx.mjs → key-DOnwUeY9.mjs} +18 -8
- package/dist/{kv-cache-QkLiue07.cjs → kv-cache-B9AAXEOS.cjs} +1 -1
- package/dist/{kv-cache-B3Na5DMI.mjs → kv-cache-CE_QUNa6.mjs} +1 -1
- package/dist/{kv-cache-BQLjC1KN.js → kv-cache-DilyIIfz.js} +1 -1
- package/dist/{ld-CaRoGhYH.mjs → ld-CiONXo2U.mjs} +3 -3
- package/dist/{metrics-BSUZSyQU.mjs → metrics-CmX7KEfY.mjs} +1 -1
- package/dist/{middleware-BIj0BBxb.js → middleware-BEhF8uxY.js} +214 -14
- package/dist/{middleware-BIctkZ9S.mjs → middleware-CRMohX8-.mjs} +1 -1
- package/dist/{middleware-CwJv5KxX.mjs → middleware-Cf0dFqr9.mjs} +34 -17
- package/dist/{middleware-DXLo-UOS.cjs → middleware-DZf3vcun.cjs} +214 -14
- package/dist/{mod-C0F6kvgS.d.cts → mod-0bIqGrKQ.d.cts} +1 -1
- package/dist/{mod-vPYVoa5n.d.ts → mod-3H9Gym5G.d.ts} +1 -1
- package/dist/mod.cjs +4 -4
- package/dist/mod.d.cts +2 -2
- package/dist/mod.d.ts +2 -2
- package/dist/mod.js +4 -4
- package/dist/nodeinfo/handler.test.mjs +1 -1
- package/dist/{owner-CyK8YW7-.mjs → owner-C7h5_D1P.mjs} +2 -2
- package/dist/{proof-CIUSLcHg.cjs → proof-BKOUYRRh.cjs} +1 -1
- package/dist/{proof-DXHo4Yon.mjs → proof-BVuILGqP.mjs} +3 -3
- package/dist/{proof-Ba14ORbL.js → proof-DNlVt3rz.js} +1 -1
- package/dist/{send-B4Ld91pX.mjs → send-7kSXrsl4.mjs} +3 -3
- package/dist/sig/http.test.mjs +2 -2
- package/dist/sig/key.test.mjs +51 -1
- package/dist/sig/ld.test.mjs +2 -2
- package/dist/sig/mod.cjs +2 -2
- package/dist/sig/mod.js +2 -2
- package/dist/sig/owner.test.mjs +1 -1
- package/dist/sig/proof.test.mjs +1 -1
- package/dist/{temporal-E7cF2Hsh.mjs → temporal-D3Bspq6d.mjs} +1 -1
- package/dist/utils/docloader.test.mjs +2 -2
- package/dist/utils/kv-cache.test.mjs +1 -1
- package/dist/utils/mod.cjs +1 -1
- package/dist/utils/mod.js +1 -1
- package/package.json +6 -6
package/README.md
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
[![npm][npm badge]][npm]
|
|
8
8
|
[![GitHub Actions][GitHub Actions badge]][GitHub Actions]
|
|
9
9
|
[![Matrix][Matrix badge]][Matrix]
|
|
10
|
-
[![@fedify@
|
|
10
|
+
[![@fedify@hackers.pub][@fedify@hackers.pub badge]][@fedify@hackers.pub]
|
|
11
11
|
|
|
12
12
|
> [!NOTE]
|
|
13
13
|
> Looking for a quick demo? Here it is: [Fedify Demo] on Deno Playground.
|
|
@@ -61,8 +61,8 @@ in the fediverse!
|
|
|
61
61
|
[GitHub Actions]: https://github.com/fedify-dev/fedify/actions/workflows/build.yaml
|
|
62
62
|
[Matrix badge]: https://img.shields.io/matrix/fedify%3Amatrix.org?logo=matrix
|
|
63
63
|
[Matrix]: https://matrix.to/#/#fedify:matrix.org
|
|
64
|
-
[@fedify@
|
|
65
|
-
[@fedify@
|
|
64
|
+
[@fedify@hackers.pub badge]: https://fedi-badge.minhee.org/@fedify@hackers.pub/followers.svg
|
|
65
|
+
[@fedify@hackers.pub]: https://hackers.pub/@fedify
|
|
66
66
|
[Fedify Demo]: https://dash.deno.com/playground/fedify-demo
|
|
67
67
|
[ActivityPub]: https://www.w3.org/TR/activitypub/
|
|
68
68
|
[fediverse]: https://en.wikipedia.org/wiki/Fediverse
|
|
@@ -215,22 +215,25 @@ financial contributors:[^2]
|
|
|
215
215
|
|
|
216
216
|
### Corporate sponsors
|
|
217
217
|
|
|
218
|
-
|
|
218
|
+
| Avatar | Name | Description |
|
|
219
|
+
| ------ | ---- | ----------- |
|
|
220
|
+
| <img src="https://images.opencollective.com/ghost/avatar/128.png" width="64" height="64"> | [Ghost](https://ghost.org) | Fiercely independent, professional publishing |
|
|
221
|
+
| <img src="https://images.opencollective.com/altstore/avatar/128.png" width="64" height="64"> | [AltStore](https://altstore.io) | |
|
|
219
222
|
|
|
220
223
|
### Supporters
|
|
221
224
|
|
|
222
|
-
- [Daniel Supernault](https://pixelfed.org/)
|
|
223
225
|
- [tkgka](https://opencollective.com/tkgka)
|
|
224
226
|
- [Blaine](https://opencollective.com/blaine)
|
|
225
227
|
- [Erick González Aguilar](https://opencollective.com/erick-gonzalez-aguilar)
|
|
228
|
+
- [Daniel Supernault](https://pixelfed.org/)
|
|
226
229
|
|
|
227
230
|
### Backers
|
|
228
231
|
|
|
229
|
-
Robin Riley,
|
|
232
|
+
Robin Riley, Encyclia, taye, box464, Evan Prodromou, okin, yamanoku, malte, Rafael Goulart, Andy Piper
|
|
230
233
|
|
|
231
234
|
### One-time donations
|
|
232
235
|
|
|
233
|
-
Robin Riley,
|
|
236
|
+
Robin Riley, malte, Nils Bergmann, Markus P, Rameez
|
|
234
237
|
|
|
235
238
|
<!-- /DO NOT EDIT -->
|
|
236
239
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import "@js-temporal/polyfill";
|
|
2
2
|
import "urlpattern-polyfill";
|
|
3
3
|
globalThis.addEventListener = () => {};
|
|
4
|
-
import { n as version, t as name } from "./deno-
|
|
4
|
+
import { n as version, t as name } from "./deno-Bg8I1L40.mjs";
|
|
5
5
|
import { t as ActivityListenerSet } from "./activity-listener-tztVvlNb.mjs";
|
|
6
6
|
import { getLogger } from "@logtape/logtape";
|
|
7
7
|
import { Router, RouterError, assertPath } from "@fedify/uri-template";
|
|
@@ -80,7 +80,7 @@ var FederationBuilderImpl = class {
|
|
|
80
80
|
* specified.
|
|
81
81
|
*/
|
|
82
82
|
async build(options) {
|
|
83
|
-
const { FederationImpl } = await import("./middleware-
|
|
83
|
+
const { FederationImpl } = await import("./middleware-CRMohX8-.mjs");
|
|
84
84
|
const f = new FederationImpl(options);
|
|
85
85
|
const trailingSlashInsensitiveValue = f.router.trailingSlashInsensitive;
|
|
86
86
|
f.router = this.router.clone();
|
|
@@ -4,6 +4,13 @@ globalThis.addEventListener = () => {};
|
|
|
4
4
|
import { getLogger } from "@logtape/logtape";
|
|
5
5
|
//#region src/federation/circuit-breaker.ts
|
|
6
6
|
const MAX_CUSTOM_FAILURE_HISTORY = 100;
|
|
7
|
+
const LEGACY_SWEEP_MARKER = ["__fedify_meta", "circuit_breaker_state_ttl_sweep_v1"];
|
|
8
|
+
const LEGACY_SWEEP_DELETING_MARKER = { __fedifyDeletingCircuitBreakerLegacyState: true };
|
|
9
|
+
const CIRCUIT_BREAKER_STATE_VERSION = 1;
|
|
10
|
+
const LEGACY_SWEEP_LOCK_TTL = Temporal.Duration.from({ minutes: 5 });
|
|
11
|
+
const LEGACY_SWEEP_RETRY_WINDOW = Temporal.Duration.from({ hours: 168 });
|
|
12
|
+
const LEGACY_SWEEP_WAIT_INTERVAL = 10;
|
|
13
|
+
const LEGACY_SWEEP_CAS_ATTEMPTS = 3;
|
|
7
14
|
/**
|
|
8
15
|
* Tracks reachability state for remote outbox delivery hosts.
|
|
9
16
|
* @since 2.3.0
|
|
@@ -13,6 +20,8 @@ var CircuitBreaker = class {
|
|
|
13
20
|
#prefix;
|
|
14
21
|
#options;
|
|
15
22
|
#now;
|
|
23
|
+
#legacySweep;
|
|
24
|
+
#legacySweepDone;
|
|
16
25
|
#stateChangeObserver;
|
|
17
26
|
constructor(options) {
|
|
18
27
|
this.#kv = options.kv;
|
|
@@ -29,6 +38,7 @@ var CircuitBreaker = class {
|
|
|
29
38
|
return now.until(this.#capHeldRetryAt(now, heldSince, now.add(delay)));
|
|
30
39
|
}
|
|
31
40
|
async beforeSend(remoteHost, message) {
|
|
41
|
+
this.#sweepLegacyStates();
|
|
32
42
|
const heldSince = parseHeldSince(message.circuitHeldSince);
|
|
33
43
|
const now = this.#now();
|
|
34
44
|
if (heldSince != null && Temporal.Instant.compare(heldSince.add(this.#options.heldActivityTtl), now) <= 0) return {
|
|
@@ -110,6 +120,7 @@ var CircuitBreaker = class {
|
|
|
110
120
|
throw new Error(`Failed to update circuit breaker state for ${remoteHost}`);
|
|
111
121
|
}
|
|
112
122
|
async recordSuccess(remoteHost) {
|
|
123
|
+
this.#sweepLegacyStates();
|
|
113
124
|
for (let attempt = 0; attempt < 10; attempt++) {
|
|
114
125
|
const oldState = await this.#get(remoteHost);
|
|
115
126
|
if (oldState == null) return void 0;
|
|
@@ -130,6 +141,7 @@ var CircuitBreaker = class {
|
|
|
130
141
|
return await this.recordSuccess(remoteHost);
|
|
131
142
|
}
|
|
132
143
|
async recordFailure(remoteHost) {
|
|
144
|
+
this.#sweepLegacyStates();
|
|
133
145
|
const now = this.#now();
|
|
134
146
|
for (let attempt = 0; attempt < 10; attempt++) {
|
|
135
147
|
const oldState = await this.#get(remoteHost);
|
|
@@ -177,26 +189,122 @@ var CircuitBreaker = class {
|
|
|
177
189
|
}
|
|
178
190
|
}
|
|
179
191
|
async getState(remoteHost) {
|
|
180
|
-
|
|
192
|
+
this.#sweepLegacyStates();
|
|
193
|
+
return stripStoredCircuitBreakerState(await this.#get(remoteHost));
|
|
194
|
+
}
|
|
195
|
+
/**
|
|
196
|
+
* The currently running background legacy sweep, if any.
|
|
197
|
+
* @internal
|
|
198
|
+
*/
|
|
199
|
+
get pendingSweep() {
|
|
200
|
+
return this.#legacySweep;
|
|
181
201
|
}
|
|
182
202
|
#key(remoteHost) {
|
|
183
203
|
return [...this.#prefix, remoteHost];
|
|
184
204
|
}
|
|
205
|
+
#legacySweepMarkerKey() {
|
|
206
|
+
return [...this.#prefix, ...LEGACY_SWEEP_MARKER];
|
|
207
|
+
}
|
|
208
|
+
#sweepLegacyStates() {
|
|
209
|
+
if (this.#kv.cas == null) return;
|
|
210
|
+
if (this.#options.stateTtl == null) return;
|
|
211
|
+
if (this.#legacySweep != null) return;
|
|
212
|
+
if (this.#isLegacySweepDone()) return;
|
|
213
|
+
this.#legacySweep = this.#sweepLegacyStatesImpl().catch((error) => {
|
|
214
|
+
getLogger([
|
|
215
|
+
"fedify",
|
|
216
|
+
"federation",
|
|
217
|
+
"circuit"
|
|
218
|
+
]).warn("Failed to sweep legacy circuit breaker state:\n{error}", { error });
|
|
219
|
+
}).finally(() => {
|
|
220
|
+
this.#legacySweep = void 0;
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
async #sweepLegacyStatesImpl() {
|
|
224
|
+
const markerKey = this.#legacySweepMarkerKey();
|
|
225
|
+
const marker = await this.#acquireLegacySweep(markerKey);
|
|
226
|
+
if (marker === "done") return;
|
|
227
|
+
try {
|
|
228
|
+
for await (const { key, value } of this.#kv.list(this.#prefix)) {
|
|
229
|
+
if (key.length !== this.#prefix.length + 1) continue;
|
|
230
|
+
await this.#migrateLegacyState(key, value);
|
|
231
|
+
}
|
|
232
|
+
} catch (error) {
|
|
233
|
+
await this.#deleteIfUnchanged(markerKey, marker);
|
|
234
|
+
throw error;
|
|
235
|
+
}
|
|
236
|
+
const finishedMarker = this.#finishLegacySweepMarker(marker);
|
|
237
|
+
if (await this.#kv.cas(markerKey, marker, finishedMarker)) this.#rememberLegacySweepMarker(finishedMarker);
|
|
238
|
+
}
|
|
239
|
+
#finishLegacySweepMarker(marker) {
|
|
240
|
+
const retryUntil = getLegacySweepRetryUntil(marker);
|
|
241
|
+
if (retryUntil != null && Temporal.Instant.compare(this.#now(), Temporal.Instant.from(retryUntil)) < 0) return {
|
|
242
|
+
state: "done",
|
|
243
|
+
retryUntil
|
|
244
|
+
};
|
|
245
|
+
return { state: "final" };
|
|
246
|
+
}
|
|
247
|
+
async #migrateLegacyState(key, value) {
|
|
248
|
+
if (isCurrentCircuitBreakerState(value)) return;
|
|
249
|
+
const state = parseCircuitBreakerKvState(value);
|
|
250
|
+
if (state != null) {
|
|
251
|
+
await this.#kv.cas(key, value, markCircuitBreakerState(state), this.#setOptions());
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
await this.#deleteIfUnchanged(key, value);
|
|
255
|
+
}
|
|
256
|
+
async #deleteIfUnchanged(key, value) {
|
|
257
|
+
if (await this.#kv.cas(key, value, LEGACY_SWEEP_DELETING_MARKER, { ttl: LEGACY_SWEEP_LOCK_TTL })) await this.#kv.delete(key);
|
|
258
|
+
}
|
|
259
|
+
async #acquireLegacySweep(markerKey) {
|
|
260
|
+
for (let attempt = 0; attempt < LEGACY_SWEEP_CAS_ATTEMPTS; attempt++) {
|
|
261
|
+
const marker = await this.#kv.get(markerKey);
|
|
262
|
+
const now = this.#now();
|
|
263
|
+
if (isLegacySweepActive(marker, now)) {
|
|
264
|
+
this.#rememberLegacySweepMarker(marker);
|
|
265
|
+
return "done";
|
|
266
|
+
}
|
|
267
|
+
const retryUntil = isLegacySweepRetryDue(marker, now) ? marker.retryUntil : getLegacySweepRetryUntil(marker) ?? now.add(LEGACY_SWEEP_RETRY_WINDOW).toString();
|
|
268
|
+
const sweeping = {
|
|
269
|
+
state: "sweeping",
|
|
270
|
+
started: now.toString(),
|
|
271
|
+
retryUntil
|
|
272
|
+
};
|
|
273
|
+
if (await this.#kv.cas(markerKey, marker ?? void 0, sweeping, { ttl: LEGACY_SWEEP_LOCK_TTL })) return sweeping;
|
|
274
|
+
await delay(LEGACY_SWEEP_WAIT_INTERVAL);
|
|
275
|
+
}
|
|
276
|
+
return "done";
|
|
277
|
+
}
|
|
278
|
+
#isLegacySweepDone() {
|
|
279
|
+
if (this.#legacySweepDone === "final") return true;
|
|
280
|
+
if (this.#legacySweepDone == null) return false;
|
|
281
|
+
if (Temporal.Instant.compare(this.#now(), this.#legacySweepDone) < 0) return true;
|
|
282
|
+
this.#legacySweepDone = void 0;
|
|
283
|
+
return false;
|
|
284
|
+
}
|
|
285
|
+
#rememberLegacySweepMarker(marker) {
|
|
286
|
+
const doneUntil = getLegacySweepDoneUntil(marker, this.#now());
|
|
287
|
+
if (doneUntil != null) this.#legacySweepDone = doneUntil;
|
|
288
|
+
}
|
|
185
289
|
#capHeldRetryAt(now, heldSince, retryAt) {
|
|
186
290
|
const expiresAt = (heldSince ?? now).add(this.#options.heldActivityTtl);
|
|
187
291
|
return Temporal.Instant.compare(expiresAt, retryAt) < 0 ? expiresAt : retryAt;
|
|
188
292
|
}
|
|
189
293
|
async #get(remoteHost) {
|
|
190
|
-
return
|
|
294
|
+
return parseStoredCircuitBreakerKvState(await this.#kv.get(this.#key(remoteHost)));
|
|
191
295
|
}
|
|
192
296
|
async #replace(remoteHost, oldState, newState) {
|
|
193
297
|
const key = this.#key(remoteHost);
|
|
298
|
+
const storedState = newState == null ? void 0 : markCircuitBreakerState(newState);
|
|
194
299
|
if (this.#kv.cas == null) {
|
|
195
|
-
if (
|
|
196
|
-
else await this.#kv.set(key,
|
|
300
|
+
if (storedState == null) await this.#kv.delete(key);
|
|
301
|
+
else await this.#kv.set(key, storedState, this.#setOptions());
|
|
197
302
|
return true;
|
|
198
303
|
}
|
|
199
|
-
return await this.#kv.cas(key, oldState,
|
|
304
|
+
return await this.#kv.cas(key, oldState, storedState, storedState == null ? void 0 : this.#setOptions());
|
|
305
|
+
}
|
|
306
|
+
#setOptions() {
|
|
307
|
+
return this.#options.stateTtl == null ? void 0 : { ttl: this.#options.stateTtl };
|
|
200
308
|
}
|
|
201
309
|
async #notifyStateChange(remoteHost, previousState, newState) {
|
|
202
310
|
try {
|
|
@@ -236,18 +344,25 @@ var CircuitBreaker = class {
|
|
|
236
344
|
* @param options The public circuit breaker options supplied to Fedify.
|
|
237
345
|
* @returns The normalized failure predicate, failure pruning function,
|
|
238
346
|
* duration values, and optional callbacks with defaults applied.
|
|
239
|
-
* @throws {RangeError} If any configured duration is not positive
|
|
347
|
+
* @throws {RangeError} If any configured duration is not positive, or if
|
|
348
|
+
* `stateTtl` is shorter than `recoveryDelay`.
|
|
240
349
|
* @throws {TypeError} If `failureThreshold` is not a positive integer.
|
|
241
350
|
*/
|
|
242
351
|
function normalizeCircuitBreakerOptions(options) {
|
|
243
352
|
const recoveryDelay = toInstantDuration(options.recoveryDelay ?? { minutes: 30 });
|
|
244
353
|
const heldActivityTtl = toInstantDuration(options.heldActivityTtl ?? { hours: 168 });
|
|
245
354
|
const releaseInterval = toInstantDuration(options.releaseInterval ?? { seconds: 1 });
|
|
355
|
+
const configuredStateTtl = options.stateTtl == null ? void 0 : toInstantDuration(options.stateTtl);
|
|
246
356
|
assertPositiveDuration(recoveryDelay, "recoveryDelay");
|
|
247
357
|
assertPositiveDuration(heldActivityTtl, "heldActivityTtl");
|
|
248
358
|
assertPositiveDuration(releaseInterval, "releaseInterval");
|
|
359
|
+
if (configuredStateTtl != null) {
|
|
360
|
+
assertPositiveDuration(configuredStateTtl, "stateTtl");
|
|
361
|
+
if (Temporal.Duration.compare(configuredStateTtl, recoveryDelay) < 0) throw new RangeError("stateTtl must be at least recoveryDelay.");
|
|
362
|
+
}
|
|
249
363
|
let failure;
|
|
250
364
|
let pruneFailures;
|
|
365
|
+
let stateTtl;
|
|
251
366
|
if (options.failure == null) {
|
|
252
367
|
const failureThreshold = options.failureThreshold ?? 5;
|
|
253
368
|
if (!Number.isInteger(failureThreshold) || failureThreshold <= 0) throw new TypeError("failureThreshold must be a positive integer.");
|
|
@@ -263,9 +378,11 @@ function normalizeCircuitBreakerOptions(options) {
|
|
|
263
378
|
const last = timestamps[timestamps.length - 1];
|
|
264
379
|
return Temporal.Duration.compare(first.until(last), failureWindow) <= 0;
|
|
265
380
|
};
|
|
381
|
+
stateTtl = configuredStateTtl ?? maxDuration(recoveryDelay.add(failureWindow), recoveryDelay.add(heldActivityTtl));
|
|
266
382
|
} else {
|
|
267
383
|
failure = options.failure;
|
|
268
384
|
pruneFailures = (timestamps) => timestamps.slice(-MAX_CUSTOM_FAILURE_HISTORY);
|
|
385
|
+
stateTtl = configuredStateTtl;
|
|
269
386
|
}
|
|
270
387
|
return {
|
|
271
388
|
failure,
|
|
@@ -273,10 +390,72 @@ function normalizeCircuitBreakerOptions(options) {
|
|
|
273
390
|
recoveryDelay,
|
|
274
391
|
heldActivityTtl,
|
|
275
392
|
releaseInterval,
|
|
393
|
+
stateTtl,
|
|
276
394
|
onStateChange: options.onStateChange,
|
|
277
395
|
onActivityDrop: options.onActivityDrop
|
|
278
396
|
};
|
|
279
397
|
}
|
|
398
|
+
function maxDuration(duration, ...durations) {
|
|
399
|
+
return durations.reduce((max, candidate) => Temporal.Duration.compare(candidate, max) > 0 ? candidate : max, duration);
|
|
400
|
+
}
|
|
401
|
+
function isLegacySweepDone(value, now) {
|
|
402
|
+
return getLegacySweepDoneUntil(value, now) != null;
|
|
403
|
+
}
|
|
404
|
+
function getLegacySweepDoneUntil(value, now) {
|
|
405
|
+
if (typeof value !== "object" || value == null || !("state" in value)) return;
|
|
406
|
+
if (value.state === "final") return "final";
|
|
407
|
+
if (value.state === "done" && "retryUntil" in value && typeof value.retryUntil === "string") try {
|
|
408
|
+
const retryUntil = Temporal.Instant.from(value.retryUntil);
|
|
409
|
+
return Temporal.Instant.compare(now, retryUntil) < 0 ? retryUntil : void 0;
|
|
410
|
+
} catch {
|
|
411
|
+
return;
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
function isLegacySweepRetryDue(value, now) {
|
|
415
|
+
if (typeof value !== "object" || value == null || !("state" in value) || value.state !== "done" || !("retryUntil" in value) || typeof value.retryUntil !== "string") return false;
|
|
416
|
+
try {
|
|
417
|
+
return Temporal.Instant.compare(now, Temporal.Instant.from(value.retryUntil)) >= 0;
|
|
418
|
+
} catch {
|
|
419
|
+
return false;
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
function isLegacySweepActive(value, now) {
|
|
423
|
+
return isLegacySweepDone(value, now) || isLegacySweepInProgress(value, now);
|
|
424
|
+
}
|
|
425
|
+
function isLegacySweepInProgress(value, now) {
|
|
426
|
+
if (typeof value !== "object" || value == null || !("state" in value) || value.state !== "sweeping" || !("started" in value) || typeof value.started !== "string") return false;
|
|
427
|
+
try {
|
|
428
|
+
return Temporal.Instant.compare(now, Temporal.Instant.from(value.started).add(LEGACY_SWEEP_LOCK_TTL)) < 0;
|
|
429
|
+
} catch {
|
|
430
|
+
return false;
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
function getLegacySweepRetryUntil(value) {
|
|
434
|
+
if (typeof value !== "object" || value == null || !("retryUntil" in value) || typeof value.retryUntil !== "string") return;
|
|
435
|
+
try {
|
|
436
|
+
Temporal.Instant.from(value.retryUntil);
|
|
437
|
+
return value.retryUntil;
|
|
438
|
+
} catch {
|
|
439
|
+
return;
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
function isCurrentCircuitBreakerState(value) {
|
|
443
|
+
return typeof value === "object" && value != null && "__fedifyCircuitBreakerStateVersion" in value && value.__fedifyCircuitBreakerStateVersion === CIRCUIT_BREAKER_STATE_VERSION;
|
|
444
|
+
}
|
|
445
|
+
function markCircuitBreakerState(state) {
|
|
446
|
+
return {
|
|
447
|
+
...state,
|
|
448
|
+
__fedifyCircuitBreakerStateVersion: CIRCUIT_BREAKER_STATE_VERSION
|
|
449
|
+
};
|
|
450
|
+
}
|
|
451
|
+
function stripStoredCircuitBreakerState(state) {
|
|
452
|
+
if (state == null) return void 0;
|
|
453
|
+
const { __fedifyCircuitBreakerStateVersion: _, ...publicState } = state;
|
|
454
|
+
return publicState;
|
|
455
|
+
}
|
|
456
|
+
function delay(milliseconds) {
|
|
457
|
+
return new Promise((resolve) => setTimeout(resolve, milliseconds));
|
|
458
|
+
}
|
|
280
459
|
function toInstantDuration(duration) {
|
|
281
460
|
const parsed = Temporal.Duration.from(duration);
|
|
282
461
|
return Temporal.Duration.from({ milliseconds: Math.trunc(parsed.total({
|
|
@@ -311,6 +490,9 @@ function parseHeldSince(value) {
|
|
|
311
490
|
* valid instant strings, or `undefined` when the stored value is malformed.
|
|
312
491
|
*/
|
|
313
492
|
function parseCircuitBreakerKvState(value) {
|
|
493
|
+
return stripStoredCircuitBreakerState(parseStoredCircuitBreakerKvState(value));
|
|
494
|
+
}
|
|
495
|
+
function parseStoredCircuitBreakerKvState(value) {
|
|
314
496
|
const isInstantString = (v) => {
|
|
315
497
|
if (typeof v !== "string") return false;
|
|
316
498
|
try {
|
|
@@ -330,7 +512,8 @@ function parseCircuitBreakerKvState(value) {
|
|
|
330
512
|
state: record.state,
|
|
331
513
|
failures: record.failures,
|
|
332
514
|
...record.opened == null ? {} : { opened: record.opened },
|
|
333
|
-
...record.halfOpened == null ? {} : { halfOpened: record.halfOpened }
|
|
515
|
+
...record.halfOpened == null ? {} : { halfOpened: record.halfOpened },
|
|
516
|
+
...record.__fedifyCircuitBreakerStateVersion === CIRCUIT_BREAKER_STATE_VERSION ? { __fedifyCircuitBreakerStateVersion: CIRCUIT_BREAKER_STATE_VERSION } : {}
|
|
334
517
|
};
|
|
335
518
|
}
|
|
336
519
|
//#endregion
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import "@js-temporal/polyfill";
|
|
2
|
+
import "urlpattern-polyfill";
|
|
3
|
+
globalThis.addEventListener = () => {};
|
|
4
|
+
//#region src/federation/circuit-breaker-test-utils.ts
|
|
5
|
+
function markCircuitBreakerLegacySweepDone(kv) {
|
|
6
|
+
return kv.set([
|
|
7
|
+
"_fedify",
|
|
8
|
+
"circuit",
|
|
9
|
+
"__fedify_meta",
|
|
10
|
+
"circuit_breaker_state_ttl_sweep_v1"
|
|
11
|
+
], { state: "final" });
|
|
12
|
+
}
|
|
13
|
+
//#endregion
|
|
14
|
+
export { markCircuitBreakerLegacySweepDone as t };
|
package/dist/compat/mod.d.cts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/// <reference lib="esnext.temporal" />
|
|
2
|
-
import { jt as ActivityTransformer, n as Context } from "../context-
|
|
2
|
+
import { jt as ActivityTransformer, n as Context } from "../context-Bjswu3eC.cjs";
|
|
3
3
|
import { Activity } from "@fedify/vocab";
|
|
4
4
|
|
|
5
5
|
//#region src/compat/transformers.d.ts
|
package/dist/compat/mod.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/// <reference lib="esnext.temporal" />
|
|
2
|
-
import { jt as ActivityTransformer, n as Context } from "../context-
|
|
2
|
+
import { jt as ActivityTransformer, n as Context } from "../context-COxc_O-c.js";
|
|
3
3
|
import { Activity } from "@fedify/vocab";
|
|
4
4
|
|
|
5
5
|
//#region src/compat/transformers.d.ts
|
|
@@ -5,7 +5,7 @@ import { t as assertEquals } from "../assert_equals-C-ZRDbaf.mjs";
|
|
|
5
5
|
import { t as assertInstanceOf } from "../assert_instance_of-DBC5X09g.mjs";
|
|
6
6
|
import { t as assert } from "../assert-OguE97r2.mjs";
|
|
7
7
|
import { t as MemoryKvStore } from "../kv-rV3vodCc.mjs";
|
|
8
|
-
import { b as autoIdAssigner, n as FederationImpl, y as actorDehydrator } from "../middleware-
|
|
8
|
+
import { b as autoIdAssigner, n as FederationImpl, y as actorDehydrator } from "../middleware-Cf0dFqr9.mjs";
|
|
9
9
|
import { Follow, Person } from "@fedify/vocab";
|
|
10
10
|
import { test } from "@fedify/fixture";
|
|
11
11
|
//#region src/compat/transformers.test.ts
|
|
@@ -87,6 +87,16 @@ type CircuitBreakerOptions = CircuitBreakerFailurePolicy & {
|
|
|
87
87
|
*/
|
|
88
88
|
readonly releaseInterval?: Temporal.Duration | Temporal.DurationLike;
|
|
89
89
|
/**
|
|
90
|
+
* How long Fedify keeps circuit breaker state in the configured key-value
|
|
91
|
+
* store.
|
|
92
|
+
*
|
|
93
|
+
* When omitted, Fedify derives this from `failureWindow`, `recoveryDelay`, and
|
|
94
|
+
* `heldActivityTtl` for the default numeric failure policy. Custom `failure`
|
|
95
|
+
* callbacks do not have an inspectable time window, so custom policies do not
|
|
96
|
+
* expire their stored state unless this option is provided.
|
|
97
|
+
*/
|
|
98
|
+
readonly stateTtl?: Temporal.Duration | Temporal.DurationLike;
|
|
99
|
+
/**
|
|
90
100
|
* Called whenever the circuit state changes.
|
|
91
101
|
*/
|
|
92
102
|
readonly onStateChange?: (remoteHost: string, previousState: CircuitBreakerState, newState: CircuitBreakerState) => void | Promise<void>;
|
|
@@ -105,6 +115,7 @@ interface NormalizedCircuitBreakerOptions {
|
|
|
105
115
|
readonly recoveryDelay: Temporal.Duration;
|
|
106
116
|
readonly heldActivityTtl: Temporal.Duration;
|
|
107
117
|
readonly releaseInterval: Temporal.Duration;
|
|
118
|
+
readonly stateTtl: Temporal.Duration | undefined;
|
|
108
119
|
readonly onStateChange?: CircuitBreakerOptions["onStateChange"];
|
|
109
120
|
readonly onActivityDrop?: CircuitBreakerOptions["onActivityDrop"];
|
|
110
121
|
}
|
|
@@ -165,6 +176,11 @@ declare class CircuitBreaker {
|
|
|
165
176
|
recordFailure(remoteHost: string): Promise<CircuitBreakerStateChange | undefined>;
|
|
166
177
|
dropActivity(remoteHost: string, details: CircuitBreakerActivityDrop): Promise<void>;
|
|
167
178
|
getState(remoteHost: string): Promise<CircuitBreakerKvState | undefined>;
|
|
179
|
+
/**
|
|
180
|
+
* The currently running background legacy sweep, if any.
|
|
181
|
+
* @internal
|
|
182
|
+
*/
|
|
183
|
+
get pendingSweep(): Promise<void> | undefined;
|
|
168
184
|
}
|
|
169
185
|
/**
|
|
170
186
|
* Normalizes user-provided circuit breaker options into the internal policy
|
|
@@ -173,7 +189,8 @@ declare class CircuitBreaker {
|
|
|
173
189
|
* @param options The public circuit breaker options supplied to Fedify.
|
|
174
190
|
* @returns The normalized failure predicate, failure pruning function,
|
|
175
191
|
* duration values, and optional callbacks with defaults applied.
|
|
176
|
-
* @throws {RangeError} If any configured duration is not positive
|
|
192
|
+
* @throws {RangeError} If any configured duration is not positive, or if
|
|
193
|
+
* `stateTtl` is shorter than `recoveryDelay`.
|
|
177
194
|
* @throws {TypeError} If `failureThreshold` is not a positive integer.
|
|
178
195
|
*/
|
|
179
196
|
declare function normalizeCircuitBreakerOptions(options: CircuitBreakerOptions): NormalizedCircuitBreakerOptions;
|
|
@@ -90,6 +90,16 @@ type CircuitBreakerOptions = CircuitBreakerFailurePolicy & {
|
|
|
90
90
|
*/
|
|
91
91
|
readonly releaseInterval?: Temporal.Duration | Temporal.DurationLike;
|
|
92
92
|
/**
|
|
93
|
+
* How long Fedify keeps circuit breaker state in the configured key-value
|
|
94
|
+
* store.
|
|
95
|
+
*
|
|
96
|
+
* When omitted, Fedify derives this from `failureWindow`, `recoveryDelay`, and
|
|
97
|
+
* `heldActivityTtl` for the default numeric failure policy. Custom `failure`
|
|
98
|
+
* callbacks do not have an inspectable time window, so custom policies do not
|
|
99
|
+
* expire their stored state unless this option is provided.
|
|
100
|
+
*/
|
|
101
|
+
readonly stateTtl?: Temporal.Duration | Temporal.DurationLike;
|
|
102
|
+
/**
|
|
93
103
|
* Called whenever the circuit state changes.
|
|
94
104
|
*/
|
|
95
105
|
readonly onStateChange?: (remoteHost: string, previousState: CircuitBreakerState, newState: CircuitBreakerState) => void | Promise<void>;
|
|
@@ -108,6 +118,7 @@ interface NormalizedCircuitBreakerOptions {
|
|
|
108
118
|
readonly recoveryDelay: Temporal.Duration;
|
|
109
119
|
readonly heldActivityTtl: Temporal.Duration;
|
|
110
120
|
readonly releaseInterval: Temporal.Duration;
|
|
121
|
+
readonly stateTtl: Temporal.Duration | undefined;
|
|
111
122
|
readonly onStateChange?: CircuitBreakerOptions["onStateChange"];
|
|
112
123
|
readonly onActivityDrop?: CircuitBreakerOptions["onActivityDrop"];
|
|
113
124
|
}
|
|
@@ -168,6 +179,11 @@ declare class CircuitBreaker {
|
|
|
168
179
|
recordFailure(remoteHost: string): Promise<CircuitBreakerStateChange | undefined>;
|
|
169
180
|
dropActivity(remoteHost: string, details: CircuitBreakerActivityDrop): Promise<void>;
|
|
170
181
|
getState(remoteHost: string): Promise<CircuitBreakerKvState | undefined>;
|
|
182
|
+
/**
|
|
183
|
+
* The currently running background legacy sweep, if any.
|
|
184
|
+
* @internal
|
|
185
|
+
*/
|
|
186
|
+
get pendingSweep(): Promise<void> | undefined;
|
|
171
187
|
}
|
|
172
188
|
/**
|
|
173
189
|
* Normalizes user-provided circuit breaker options into the internal policy
|
|
@@ -176,7 +192,8 @@ declare class CircuitBreaker {
|
|
|
176
192
|
* @param options The public circuit breaker options supplied to Fedify.
|
|
177
193
|
* @returns The normalized failure predicate, failure pruning function,
|
|
178
194
|
* duration values, and optional callbacks with defaults applied.
|
|
179
|
-
* @throws {RangeError} If any configured duration is not positive
|
|
195
|
+
* @throws {RangeError} If any configured duration is not positive, or if
|
|
196
|
+
* `stateTtl` is shorter than `recoveryDelay`.
|
|
180
197
|
* @throws {TypeError} If `failureThreshold` is not a positive integer.
|
|
181
198
|
*/
|
|
182
199
|
declare function normalizeCircuitBreakerOptions(options: CircuitBreakerOptions): NormalizedCircuitBreakerOptions;
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import "@js-temporal/polyfill";
|
|
2
2
|
import "urlpattern-polyfill";
|
|
3
3
|
globalThis.addEventListener = () => {};
|
|
4
|
-
import { o as validateCryptoKey } from "./key-
|
|
5
|
-
import { n as doubleKnock } from "./http-
|
|
4
|
+
import { o as validateCryptoKey } from "./key-DOnwUeY9.mjs";
|
|
5
|
+
import { n as doubleKnock } from "./http-BaUm6xbz.mjs";
|
|
6
6
|
import { getLogger } from "@logtape/logtape";
|
|
7
7
|
import { UrlError, createActivityPubRequest, getRemoteDocument, logRequest, validatePublicUrl } from "@fedify/vocab-runtime";
|
|
8
8
|
import { curry } from "es-toolkit";
|
|
@@ -5,7 +5,7 @@ import { t as assertEquals } from "../assert_equals-C-ZRDbaf.mjs";
|
|
|
5
5
|
import { r as assertExists } from "../std__assert-BBjXFNOb.mjs";
|
|
6
6
|
import { t as assertThrows } from "../assert_throws-BOkhLGYc.mjs";
|
|
7
7
|
import { t as MemoryKvStore } from "../kv-rV3vodCc.mjs";
|
|
8
|
-
import { r as createFederationBuilder } from "../builder-
|
|
8
|
+
import { r as createFederationBuilder } from "../builder-DosLp_w5.mjs";
|
|
9
9
|
import { DisallowedOperatorError, DisallowedVarSpecModifierError, DuplicateRouteVariableError, RouteTemplateOptionsNotMatchedError, RouterError } from "@fedify/uri-template";
|
|
10
10
|
import { Activity, Note, Person } from "@fedify/vocab";
|
|
11
11
|
import { test } from "@fedify/fixture";
|