durable-utils
Version:
Utilities for Cloudflare Durable Objects and Workers, including SQL migrations, sharding and retry utilities, and more.
405 lines (399 loc) • 15.4 kB
JavaScript
;
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __export = (target, all) => {
for (var name in all)
__defProp(target, name, { get: all[name], enumerable: true });
};
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") {
for (let key of __getOwnPropNames(from))
if (!__hasOwnProp.call(to, key) && key !== except)
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
}
return to;
};
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
// src/do-sharding.ts
var do_sharding_exports = {};
__export(do_sharding_exports, {
FixedShardedDO: () => FixedShardedDO,
StaticShardedDO: () => StaticShardedDO
});
module.exports = __toCommonJS(do_sharding_exports);
// node_modules/js-xxhash/dist/esm/xxHash32.js
var PRIME32_1 = 2654435761;
var PRIME32_2 = 2246822519;
var PRIME32_3 = 3266489917;
var PRIME32_4 = 668265263;
var PRIME32_5 = 374761393;
var encoder;
function xxHash32(input, seed = 0) {
const buffer = typeof input === "string" ? (encoder ??= new TextEncoder()).encode(input) : input;
const b = buffer;
let acc = seed + PRIME32_5 & 4294967295;
let offset = 0;
if (b.length >= 16) {
const accN = [
seed + PRIME32_1 + PRIME32_2 & 4294967295,
seed + PRIME32_2 & 4294967295,
seed + 0 & 4294967295,
seed - PRIME32_1 & 4294967295
];
const b2 = buffer;
const limit2 = b2.length - 16;
let lane = 0;
for (offset = 0; (offset & 4294967280) <= limit2; offset += 4) {
const i = offset;
const laneN0 = b2[i + 0] + (b2[i + 1] << 8);
const laneN1 = b2[i + 2] + (b2[i + 3] << 8);
const laneNP = laneN0 * PRIME32_2 + (laneN1 * PRIME32_2 << 16);
let acc2 = accN[lane] + laneNP & 4294967295;
acc2 = acc2 << 13 | acc2 >>> 19;
const acc0 = acc2 & 65535;
const acc1 = acc2 >>> 16;
accN[lane] = acc0 * PRIME32_1 + (acc1 * PRIME32_1 << 16) & 4294967295;
lane = lane + 1 & 3;
}
acc = (accN[0] << 1 | accN[0] >>> 31) + (accN[1] << 7 | accN[1] >>> 25) + (accN[2] << 12 | accN[2] >>> 20) + (accN[3] << 18 | accN[3] >>> 14) & 4294967295;
}
acc = acc + buffer.length & 4294967295;
const limit = buffer.length - 4;
for (; offset <= limit; offset += 4) {
const i = offset;
const laneN0 = b[i + 0] + (b[i + 1] << 8);
const laneN1 = b[i + 2] + (b[i + 3] << 8);
const laneP = laneN0 * PRIME32_3 + (laneN1 * PRIME32_3 << 16);
acc = acc + laneP & 4294967295;
acc = acc << 17 | acc >>> 15;
acc = (acc & 65535) * PRIME32_4 + ((acc >>> 16) * PRIME32_4 << 16) & 4294967295;
}
for (; offset < b.length; ++offset) {
const lane = b[offset];
acc = acc + lane * PRIME32_5;
acc = acc << 11 | acc >>> 21;
acc = (acc & 65535) * PRIME32_1 + ((acc >>> 16) * PRIME32_1 << 16) & 4294967295;
}
acc = acc ^ acc >>> 15;
acc = ((acc & 65535) * PRIME32_2 & 4294967295) + ((acc >>> 16) * PRIME32_2 << 16);
acc = acc ^ acc >>> 13;
acc = ((acc & 65535) * PRIME32_3 & 4294967295) + ((acc >>> 16) * PRIME32_3 << 16);
acc = acc ^ acc >>> 16;
return acc < 0 ? acc + 4294967296 : acc;
}
// src/do-utils.ts
function stubByName(doNamespace, name, options) {
const doId = doNamespace.idFromName(name);
return doNamespace.get(doId, options);
}
// src/retries.ts
function jitterBackoff(attempt, baseDelayMs, maxDelayMs) {
const attemptUpperBoundMs = Math.min(2 ** attempt * baseDelayMs, maxDelayMs);
return Math.floor(Math.random() * attemptUpperBoundMs);
}
async function tryWhile(fn, isRetryable, options) {
const baseDelayMs = Math.floor(options?.baseDelayMs ?? 100);
const maxDelayMs = Math.floor(options?.maxDelayMs ?? 3e3);
if (baseDelayMs <= 0 || maxDelayMs <= 0) {
throw new Error("baseDelayMs and maxDelayMs must be greater than 0");
}
if (baseDelayMs >= maxDelayMs) {
throw new Error("baseDelayMs must be less than maxDelayMs");
}
let attempt = 1;
while (true) {
try {
return await fn(attempt);
} catch (err) {
if (options?.verbose) {
console.info({
message: "tryWhile",
attempt,
error: String(err),
errorProps: err
});
}
attempt += 1;
if (!isRetryable(err, attempt)) {
throw err;
}
const delay = jitterBackoff(attempt, baseDelayMs, maxDelayMs);
await new Promise((resolve) => setTimeout(resolve, delay));
}
}
}
// src/do-sharding.ts
var GOLDEN_RATIO = 2654435761;
var StaticShardedDO = class {
#doNamespace;
#options;
constructor(doNamespace, options) {
if (options.numShards <= 0) {
throw new Error("Invalid number of shards, must be greater than 0");
}
if (options.concurrency !== void 0 && options.concurrency <= 0) {
throw new Error("Invalid number of subrequests, must be greater than 0");
}
if (options.shardGroupName !== void 0 || options.prefixName !== void 0) {
if (options.shardGroupName !== void 0 && options.prefixName !== void 0) {
throw new Error("Cannot use both shardGroupName and prefixName options together");
}
options.shardGroupName = (options.shardGroupName ?? options.prefixName ?? "").trim();
if (!options.shardGroupName) {
throw new Error("Invalid shard group name, must not be empty or only whitespace");
}
}
this.#doNamespace = doNamespace;
this.#options = {
...options,
concurrency: options.concurrency ?? 10,
// WARNING: Never change the default value of this otherwise there will be data loss!
shardGroupName: options.shardGroupName ?? "fixed-sharded-do"
};
}
/**
* The number of shards used to spread out the load. Immutable after creation.
*/
get N() {
return this.#options.numShards;
}
/**
* Execute a single request against the Durable Object responsible for the given partitionKey.
* @param partitionKey The partition key is used to determine the shard.
* @param doer The callback function to execute with the Durable Object stub for each shard.
* @param options The options to control the execution like retries.
* @returns An array of the results from the given `doer` callback function for each shard,
* Each item in the array will be a `TryResult` object with the `ok` property indicating success or failure.
*/
async tryOne(partitionKey, doer, options) {
const shard = this.#shardForPartitionKey(partitionKey);
const result = await this.#makeTryShard(doer, options)(shard);
return result;
}
/**
* Execute a single request against the Durable Object responsible for the given partitionKey.
* @param partitionKey The partition key is used to determine the shard.
* @param doer The callback function to execute with the Durable Object stub for the chosen shard.
* @param options The options to control the execution like retries.
* @returns The result of the given `doer` callback function. If `doer()` throws the error is propagated.
*/
async one(partitionKey, doer, options) {
const response = await this.tryOne(partitionKey, doer, options);
if (!response.ok) {
throw response.error;
}
return response.result;
}
/**
* Execute a request to the shards selected by the `options.filterFn` callback, concurrently.
* @param doer The callback function to execute with the Durable Object stub for each shard.
* @param options The options to control the execution like retries and filtering of shards.
* @returns An array of the results from the given `doer` callback function for each shard,
* Each item in the array will be a `TryResult` object with the `ok` property indicating success or failure.
* Only shards that pass the `options.filterFn` will be returned.
*/
async trySome(doer, options) {
const responses = await this.#pipelineRequests(
this.#options.concurrency,
this.#options.numShards,
{ ...options, earlyReturn: false },
this.#makeTryShard(doer, options)
);
return Array.from(responses.results.values(), (r) => r).sort((a, b) => a.shard - b.shard);
}
/**
* Execute a request to the shards selected by the `options.filterFn` callback, concurrently.
* @param doer The callback function to execute with the Durable Object stub for each shard.
* @param options The options to control the execution like retries and filtering of shards.
* @returns An array of results from the given `doer` callback function for each filtered shard.
* Only shards that pass the `options.filterFn` will be returned.
* In case of an error, the function will throw the error immediately.
*/
async some(doer, options) {
const responses = await this.#pipelineRequests(
this.#options.concurrency,
this.#options.numShards,
{ ...options, earlyReturn: true },
this.#makeTryShard(doer, options)
);
return [...responses.results.values()].sort((a, b) => a.shard - b.shard).map((r) => {
if (!r.ok) {
throw r.error;
}
return r.result;
});
}
/**
* Execute a request to each of the shards, concurrently.
* @param doer The callback function to execute with the Durable Object stub for each shard.
* @param options The options to control the execution like retries.
* @returns An array of the results from the given `doer` callback function for each shard,
* Each item in the array will be a `TryResult` object with the `ok` property indicating success or failure.
*/
async tryAll(doer, options) {
if (this.#options.numShards > 1e3) {
throw new Error(
`Too many shards [${this.#options.numShards}], Cloudflare Workers only supports up to 1000 subrequests.`
);
}
return await this.trySome(doer, { ...options, filterFn: (_shard) => true });
}
/**
* Execute a request to each of the shards, concurrently.
* @param doer The callback function to execute with the Durable Object stub for each shard.
* @param options The options to control the execution like retries.
* @returns An array of results from the given `doer` callback function for each shard.
* In case of an error, the function will throw the error immediately.
*/
async all(doer, options) {
if (this.#options.numShards > 1e3) {
throw new Error(
`Too many shards [${this.#options.numShards}], Cloudflare Workers only supports up to 1000 subrequests.`
);
}
return await this.some(doer, { ...options, filterFn: (_shard) => true });
}
//////////////////////////////////////////////////////
// DEPRECATED
/**
* Execute a request to each of the shards, concurrently.
* @deprecated Use `all` or `tryAll` instead.
* @param doer The callback function to execute with the Durable Object stub for each shard.
* @returns An async generator of results from the given `doer` callback function for each shard.
* In case of an error, the function will throw the error immediately.
*/
async *genAll(doer) {
if (this.#options.numShards > 1e3) {
throw new Error(
`Too many shards [${this.#options.numShards}], Cloudflare Workers only supports up to 1000 subrequests.`
);
}
for await (const result of this.#genPipelineRequests(
this.#options.concurrency,
this.#options.numShards,
async (shard) => {
const stub = this.#stub(shard);
return await doer(stub, shard);
}
)) {
yield result;
}
}
/**
* Execute a request to each of the shards, concurrently.
* @deprecated Use `tryAll` instead.
* @param doer The callback function to execute with the Durable Object stub for each shard.
* @returns An array of results from the given `doer` callback function for each shard,
* and an array of errors for each shard that failed.
* Items in the results array will be `undefined` if the shard failed, and similarly for the errors array.
*/
async allMaybe(doer) {
if (this.#options.numShards > 1e3) {
throw new Error(
`Too many shards [${this.#options.numShards}], Cloudflare Workers only supports up to 1000 subrequests.`
);
}
const results = await this.#pipelineRequests(
this.#options.concurrency,
this.#options.numShards,
{ filterFn: () => true, earlyReturn: false },
this.#makeTryShard(doer)
);
const finalResults = {
results: Array.from({ length: this.#options.numShards }),
errors: Array.from({ length: this.#options.numShards }),
hasErrors: results.hasErrors
};
results.results.forEach((r) => {
if (r.ok) {
finalResults.results[r.shard] = r.result;
} else {
finalResults.errors[r.shard] = r.error;
}
});
return finalResults;
}
//////////////////////////////////////////////////////
// INTERNAL IMPLEMENTATION
async #pipelineRequests(concurrency, n, tryOptions, tryShardDoer) {
const results = /* @__PURE__ */ new Map();
let i = 0;
let hasErrors = false;
const next = async () => {
if (i >= n) {
return;
}
const shard = i++;
if (!tryOptions.filterFn(shard)) {
return await next();
}
const shardResult = await tryShardDoer(shard);
results.set(shard, shardResult);
if (!shardResult.ok) {
if (tryOptions.earlyReturn) {
throw shardResult.error;
}
hasErrors = true;
}
return await next();
};
await Promise.all(
Array(concurrency).fill(0).map(() => next())
);
return { results, hasErrors };
}
async *#genPipelineRequests(concurrency, n, doer) {
const results = Array(n).fill(null);
let idxAwait = 0;
for (let shard = 0; shard < n; shard++) {
if (shard < concurrency) {
results[shard] = doer(shard);
} else {
yield await results[idxAwait];
idxAwait++;
results[shard] = doer(shard);
}
}
for (; idxAwait < n; idxAwait++) {
yield await results[idxAwait];
}
}
#makeTryShard(doer, tryOptions) {
return async (shard) => {
const stub = this.#stub(shard);
try {
const result = await tryWhile(
async () => await doer(stub, shard),
(e, attempt) => {
return !!tryOptions?.shouldRetry?.(e, attempt, shard);
}
);
return { ok: true, shard, result };
} catch (e) {
return { ok: false, shard, error: e };
}
};
}
#shardForPartitionKey(partitionKey) {
const shard = xxHash32(partitionKey, GOLDEN_RATIO) % this.#options.numShards;
return shard;
}
#stub(shard) {
return stubByName(this.#doNamespace, `${this.#options.shardGroupName}-${shard}`, this.#stubOptions(shard));
}
#stubOptions(shard) {
const options = {};
if (this.#options.shardLocationHintFn) {
options.locationHint = this.#options.shardLocationHintFn(shard);
}
return options;
}
};
var FixedShardedDO = StaticShardedDO;
// Annotate the CommonJS export names for ESM import in node:
0 && (module.exports = {
FixedShardedDO,
StaticShardedDO
});