@atproto/ozone
Version:
Backend service for moderating the Bluesky network.
206 lines • 8 kB
JavaScript
;
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.formatLabelerHeader = exports.defaultLabelerHeader = exports.parseLabelerHeader = exports.LABELER_HEADER_NAME = exports.retryHttp = exports.RETRYABLE_HTTP_STATUS_CODES = exports.getSigningKeyId = void 0;
exports.startInterval = startInterval;
exports.isCausedBySignal = isCausedBySignal;
exports.boundAbortController = boundAbortController;
const node_assert_1 = __importDefault(require("node:assert"));
const structured_headers_1 = require("structured-headers");
const common_1 = require("@atproto/common");
const xrpc_1 = require("@atproto/xrpc");
const getSigningKeyId = async (db, signingKey) => {
const selectRes = await db.db
.selectFrom('signing_key')
.selectAll()
.where('key', '=', signingKey)
.executeTakeFirst();
if (selectRes) {
return selectRes.id;
}
const insertRes = await db.db
.insertInto('signing_key')
.values({ key: signingKey })
.returningAll()
.executeTakeFirstOrThrow();
return insertRes.id;
};
exports.getSigningKeyId = getSigningKeyId;
exports.RETRYABLE_HTTP_STATUS_CODES = new Set([
408, 425, 429, 500, 502, 503, 504, 522, 524,
]);
exports.retryHttp = (0, common_1.createRetryable)((err) => {
if (err instanceof xrpc_1.XRPCError) {
if (err.status === xrpc_1.ResponseType.Unknown)
return true;
return exports.RETRYABLE_HTTP_STATUS_CODES.has(err.status);
}
return false;
});
exports.LABELER_HEADER_NAME = 'atproto-accept-labelers';
const parseLabelerHeader = (header, ignoreDid) => {
if (!header)
return null;
const labelerDids = new Set();
const redactDids = new Set();
const parsed = (0, structured_headers_1.parseList)(header);
for (const item of parsed) {
const did = item[0].toString();
if (!did) {
return null;
}
if (did === ignoreDid) {
continue;
}
labelerDids.add(did);
const redact = item[1].get('redact')?.valueOf();
if (redact === true) {
redactDids.add(did);
}
}
return {
dids: [...labelerDids],
redact: redactDids,
};
};
exports.parseLabelerHeader = parseLabelerHeader;
const defaultLabelerHeader = (dids) => {
return {
dids,
redact: new Set(dids),
};
};
exports.defaultLabelerHeader = defaultLabelerHeader;
const formatLabelerHeader = (parsed) => {
const parts = parsed.dids.map((did) => parsed.redact.has(did) ? `${did};redact` : did);
return parts.join(',');
};
exports.formatLabelerHeader = formatLabelerHeader;
/**
* Utility function similar to `setInterval()`. The main difference is that the
* execution is controlled through a signal and that the function will wait for
* `interval` milliseconds *between* the end of the previous execution and the
* start of the next one (instead of starting the execution every `interval`
* milliseconds), ensuring that the function is not running concurrently.
*
* @param fn The function to execute. That function must not throw any error
* other than {@link signal}'s {@link AbortSignal.reason} or an {@link Error}
* that has the {@link signal}'s {@link AbortSignal.reason} as its
* {@link Error.cause}.
*
* @returns A promise that resolves when the signal is aborted, and the last
* execution is done.
*
* @throws {AbortSignal['reason']} if the {@link signal} is already aborted.
* @throws {TypeError} if {@link fn} throws an unexpected error (with the
* unexpected error as the {@link Error.cause}).
*/
async function startInterval(fn, interval, signal, runImmediately = false) {
signal.throwIfAborted();
// Renaming for clarity
const inputSignal = signal;
const intervalController = new AbortController();
const intervalSignal = intervalController.signal;
return new Promise((resolve, reject) => {
let timer;
const run = async () => {
// Cloning the signal for this particular run to prevent memory leaks
const runController = boundAbortController(intervalSignal);
const runSignal = runController.signal;
try {
await fn(runSignal);
}
catch (err) {
if (err != null && isCausedBySignal(err, runSignal)) {
// Silently ignore the error if it is caused by the signal. At this
// point, the interval controller was aborted, which will cause the
// promise to resolve in the "finally" block bellow.
}
else {
// Invalid behavior: stop the interval and reject the promise.
const error = new TypeError('Unexpected error', { cause: err });
// Rejecting here will make `resolve()` in the "finally" block to be a
// no-op. Rejecting before aborting the controller to ensure the
// promise does not get resolved by the `abort` event listeners.
reject(error);
// Using `error` as abort reason to avoid creating an AbortError.
intervalController.abort(error);
}
}
finally {
// Cleanup the listeners added by `boundAbortController`
runController.abort();
if (intervalSignal.aborted)
resolve();
else
schedule();
}
};
const schedule = () => {
(0, node_assert_1.default)(timer === undefined, 'unexpected state');
timer = setTimeout(() => {
timer = undefined; // "running" state
void run();
}, interval);
};
inputSignal.addEventListener('abort',
// This function will only be called if the `inputSignal` is aborted
// before the interval controller is aborted.
() => {
// Stop the interval, using the input signal's reason
intervalController.abort(inputSignal.reason);
if (timer === undefined) {
// `fn` is currently running; `run`'s finally block will resolve the
// promise.
}
else {
// The execution was scheduled but not started yet. Clear the timer
// and resolve the promise.
clearTimeout(timer);
resolve();
}
},
// Remove the listener whenever the interval is aborted.
{ signal: intervalSignal });
if (runImmediately)
void run();
else
schedule();
});
}
/**
* Determines whether the cause of an error is a signal's reason
*/
function isCausedBySignal(err, signal) {
if (!signal.aborted)
return false;
if (signal.reason == null)
return false; // Ignore nullish reasons
return (err === signal.reason ||
(err instanceof Error && err.cause === signal.reason));
}
/**
* Creates an AbortController that will be aborted when any of the given signals
* is aborted.
*
* @note Make sure to call `abortController.abort()` when you are done with
* the controller to avoid memory leaks.
*
* @throws if any of the input signals is already aborted.
*/
function boundAbortController(...signals) {
for (const signal of signals) {
signal?.throwIfAborted();
}
const abortController = new AbortController();
const abort = function (event) {
abortController.abort(event.target?.reason);
};
for (const signal of signals) {
signal?.addEventListener('abort', abort, { signal: abortController.signal });
}
return abortController;
}
//# sourceMappingURL=util.js.map