@defer/client
Advanced tools
Comparing version 2.0.0-alpha-20230225204614-f9e6d83 to 2.0.0-alpha-20230628211612-9c2d07c
@@ -10,8 +10,9 @@ "use strict"; | ||
schedule_for: request.scheduleFor, | ||
metadata: request.metadata, | ||
}); | ||
return client("POST", "/api/v1/enqueue", data); | ||
return client("POST", "/public/v1/enqueue", data); | ||
} | ||
exports.enqueueExecution = enqueueExecution; | ||
function fetchExecution(client, request) { | ||
return client("GET", `/api/v1/executions/${request.id}`); | ||
return client("GET", `/public/v1/executions/${request.id}`); | ||
} | ||
@@ -18,0 +19,0 @@ exports.fetchExecution = fetchExecution; |
"use strict"; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
exports.INTERNAL_VERSION = void 0; | ||
exports.INTERNAL_VERSION = 3; | ||
exports.RETRY_MAX_ATTEMPTS_PLACEHOLDER = exports.INTERNAL_VERSION = void 0; | ||
exports.INTERNAL_VERSION = 5; | ||
exports.RETRY_MAX_ATTEMPTS_PLACEHOLDER = 13; |
"use strict"; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
exports.APIError = exports.HTTPRequestError = exports.ClientError = exports.DeferError = void 0; | ||
exports.APIError = exports.HTTPRequestError = exports.ClientError = exports.DeferError = exports.errorMessage = void 0; | ||
const errorMessage = (error) => { | ||
let message = error.message; | ||
if (error.cause instanceof Error) { | ||
message = `${message}: ${(0, exports.errorMessage)(error.cause)}`; | ||
} | ||
else { | ||
message = `${message}: ${String(error.cause)}`; | ||
} | ||
return message; | ||
}; | ||
exports.errorMessage = errorMessage; | ||
class DeferError extends Error { | ||
@@ -5,0 +16,0 @@ constructor(msg) { |
"use strict"; | ||
var __importDefault = (this && this.__importDefault) || function (mod) { | ||
return (mod && mod.__esModule) ? mod : { "default": mod }; | ||
}; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
@@ -7,2 +10,3 @@ exports.makeHTTPClient = void 0; | ||
const errors_js_1 = require("./errors.js"); | ||
const version_js_1 = __importDefault(require("./version.js")); | ||
const basicAuth = (username, password) => { | ||
@@ -21,3 +25,3 @@ const credentials = Buffer.from(`${username}:${password}`).toString("base64"); | ||
if (error instanceof Error) | ||
message = `invalid endpoint url: ${error.message}`; | ||
message = `invalid endpoint url: ${(0, errors_js_1.errorMessage)(error)}`; | ||
else | ||
@@ -35,2 +39,3 @@ message = `unknown error: ${String(error)}`; | ||
"Content-type": "application/json", | ||
"User-Agent": `defer/${version_js_1.default} (source: https://github.com/defer-run/defer.client)`, | ||
Authorization: basicAuth("", accessToken), | ||
@@ -47,3 +52,3 @@ }, | ||
if (error instanceof Error) | ||
message = `cannot execute http request: ${error.message}`; | ||
message = `cannot execute http request: ${(0, errors_js_1.errorMessage)(error)}`; | ||
else | ||
@@ -59,3 +64,3 @@ message = `unknown error: ${String(error)}`; | ||
if (error instanceof Error) | ||
message = `cannot decode http response: ${error.message}`; | ||
message = `cannot decode http response: ${(0, errors_js_1.errorMessage)(error)}`; | ||
else | ||
@@ -75,3 +80,3 @@ message = `unknown error: ${String(error)}`; | ||
const e = error; | ||
throw new errors_js_1.ClientError(`cannot decode http response body: ${e.message}`); | ||
throw new errors_js_1.ClientError(`cannot decode http response body: ${(0, errors_js_1.errorMessage)(e)}`); | ||
} | ||
@@ -89,3 +94,3 @@ } | ||
const e = error; | ||
throw new errors_js_1.ClientError(`cannot decode http response body: ${e.message}`); | ||
throw new errors_js_1.ClientError(`cannot decode http response body: ${(0, errors_js_1.errorMessage)(e)}`); | ||
} | ||
@@ -92,0 +97,0 @@ throw new errors_js_1.APIError(decodedData.message, decodedData.code); |
304
cjs/index.js
@@ -6,9 +6,6 @@ "use strict"; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
exports.delay = exports.defer = exports.configure = void 0; | ||
/* eslint-disable @typescript-eslint/no-explicit-any */ | ||
exports.awaitResult = exports.addMetadata = exports.delay = exports.defer = exports.getExecution = exports.deferEnabled = exports.configure = void 0; | ||
const parse_duration_1 = __importDefault(require("parse-duration")); | ||
// @ts-expect-error untyped dep | ||
const natural_cron_js_1 = __importDefault(require("@darkeyedevelopers/natural-cron.js")); | ||
const uuid_1 = require("uuid"); | ||
const constants_js_1 = require("./constants.js"); | ||
const FakeID = "00000000000000000000000000000000"; | ||
const client_js_1 = require("./client.js"); | ||
@@ -18,4 +15,5 @@ const errors_js_1 = require("./errors.js"); | ||
const withDelay = (dt, delay) => new Date(dt.getTime() + (0, parse_duration_1.default)(delay)); | ||
const __database = new Map(); | ||
let __accessToken = process.env["DEFER_TOKEN"]; | ||
let __endpoint = "https://api.defer.run"; | ||
let __endpoint = process.env["DEFER_ENDPOINT"] || "https://api.defer.run"; | ||
let __verbose = false; | ||
@@ -37,2 +35,50 @@ let __httpClient; | ||
exports.configure = configure; | ||
const deferEnabled = () => !!__accessToken; | ||
exports.deferEnabled = deferEnabled; | ||
async function execLocally(id, fn, args) { | ||
let state = "succeed"; | ||
let originalResult; | ||
try { | ||
originalResult = await fn(...args); | ||
} | ||
catch (error) { | ||
const e = error; | ||
state = "failed"; | ||
originalResult = { | ||
name: e.name, | ||
message: e.message, | ||
cause: e.cause, | ||
stack: e.stack, | ||
}; | ||
} | ||
let result; | ||
try { | ||
result = JSON.parse(JSON.stringify(originalResult || "")); | ||
} | ||
catch (error) { | ||
const e = error; | ||
throw new errors_js_1.DeferError(`cannot serialize function return: ${e.message}`); | ||
} | ||
const response = { id, state, result }; | ||
__database.set(id, response); | ||
return response; | ||
} | ||
async function getExecution(id) { | ||
if (__httpClient) | ||
return (0, client_js_1.fetchExecution)(__httpClient, { id }); | ||
const response = __database.get(id); | ||
if (response) | ||
return Promise.resolve(response); | ||
throw new errors_js_1.APIError("execution not found", ""); | ||
} | ||
exports.getExecution = getExecution; | ||
function defaultRetryPolicy() { | ||
return { | ||
maxAttempts: 0, | ||
initialInterval: 30, | ||
randomizationFactor: 0.5, | ||
multiplier: 1.5, | ||
maxInterval: 60 * 10, | ||
}; | ||
} | ||
const defer = (fn, options) => { | ||
@@ -55,2 +101,3 @@ const ret = async (...args) => { | ||
scheduleFor: new Date(), | ||
metadata: {}, | ||
}); | ||
@@ -60,15 +107,74 @@ } | ||
console.log(`[defer.run][${fn.name}] defer ignore, no token found.`); | ||
await fn(...functionArguments); | ||
return { id: FakeID }; | ||
const id = (0, uuid_1.v4)(); | ||
__database.set(id, { id: id, state: "running" }); | ||
execLocally(id, fn, functionArguments); | ||
return { id }; | ||
}; | ||
ret.__fn = fn; | ||
let retryPolicy = 0; | ||
if (options?.retry === true) { | ||
retryPolicy = 12; | ||
const retryPolicy = defaultRetryPolicy(); | ||
switch (typeof options?.retry) { | ||
case "boolean": { | ||
if (options.retry) { | ||
retryPolicy.maxAttempts = constants_js_1.RETRY_MAX_ATTEMPTS_PLACEHOLDER; | ||
} | ||
break; | ||
} | ||
case "number": { | ||
retryPolicy.maxAttempts = options.retry; | ||
break; | ||
} | ||
case "object": { | ||
if (options.retry.maxAttempts) { | ||
retryPolicy.maxAttempts = options.retry.maxAttempts; | ||
} | ||
else { | ||
options.retry.maxAttempts = constants_js_1.RETRY_MAX_ATTEMPTS_PLACEHOLDER; | ||
} | ||
if (options.retry.initialInterval) | ||
retryPolicy.initialInterval = options.retry.initialInterval; | ||
if (options.retry.randomizationFactor) | ||
retryPolicy.randomizationFactor = options.retry.randomizationFactor; | ||
if (options.retry.multiplier) | ||
retryPolicy.multiplier = options.retry.multiplier; | ||
if (options.retry.maxInterval) | ||
retryPolicy.maxInterval = options.retry.maxInterval; | ||
break; | ||
} | ||
case "undefined": { | ||
retryPolicy.maxAttempts = 0; | ||
break; | ||
} | ||
default: { | ||
throw new Error("invalid retry options"); | ||
} | ||
} | ||
if (typeof options?.retry === "number") { | ||
retryPolicy = options.retry; | ||
} | ||
ret.__metadata = { version: constants_js_1.INTERNAL_VERSION, retry: retryPolicy }; | ||
ret.await = async (...args) => { | ||
ret.__metadata = { | ||
version: constants_js_1.INTERNAL_VERSION, | ||
retry: retryPolicy, | ||
concurrency: options?.concurrency, | ||
}; | ||
return ret; | ||
}; | ||
exports.defer = defer; | ||
exports.defer.cron = (fn, schedule) => { | ||
const ret = () => { | ||
throw new Error("`defer.cron()` functions should not be invoked."); | ||
}; | ||
ret.__fn = fn; | ||
ret.__metadata = { | ||
version: constants_js_1.INTERNAL_VERSION, | ||
cron: schedule, | ||
}; | ||
return ret; | ||
}; | ||
/** | ||
* Delay the execution of a background function | ||
* @constructor | ||
* @param {Function} deferFn - A background function (`defer(...)` result) | ||
* @param {string|Date} delay - The delay (ex: "1h" or a Date object) | ||
* @returns Function | ||
*/ | ||
const delay = (deferFn, delay) => { | ||
const delayedDeferFn = async (...args) => { | ||
const fn = deferFn.__fn; | ||
let functionArguments; | ||
@@ -82,60 +188,101 @@ try { | ||
} | ||
if (__verbose) | ||
console.log(`[defer.run][${fn.name}] invoked.`); | ||
if (__httpClient) { | ||
const { id } = await (0, client_js_1.enqueueExecution)(__httpClient, { | ||
let scheduleFor; | ||
if (delay instanceof Date) { | ||
scheduleFor = delay; | ||
} | ||
else { | ||
const now = new Date(); | ||
scheduleFor = withDelay(now, delay); | ||
} | ||
return (0, client_js_1.enqueueExecution)(__httpClient, { | ||
name: fn.name, | ||
arguments: functionArguments, | ||
scheduleFor: new Date(), | ||
scheduleFor, | ||
metadata: deferFn.__execOptions?.metadata || {}, | ||
}); | ||
const response = await (0, client_js_1.waitExecutionResult)(__httpClient, { id: id }); | ||
if (response.state === "failed") { | ||
let error = new Error("Defer execution failed"); | ||
if (response.result?.message) { | ||
error = new Error(response.result.message); | ||
error.stack = response.result.stack; | ||
} | ||
else if (response.result) { | ||
error = response.result; | ||
} | ||
throw error; | ||
} | ||
return response.result; | ||
} | ||
if (__verbose) | ||
console.log(`[defer.run][${fn.name}] defer ignore, no token found.`); | ||
const id = (0, uuid_1.v4)(); | ||
__database.set(id, { id: id, state: "running" }); | ||
execLocally(id, fn, functionArguments); | ||
return { id }; | ||
}; | ||
delayedDeferFn.__fn = deferFn.__fn; | ||
delayedDeferFn.__metadata = deferFn.__metadata; | ||
delayedDeferFn.__execOptions = { | ||
...deferFn.__execOptions, | ||
delay, | ||
}; | ||
return delayedDeferFn; | ||
}; | ||
exports.delay = delay; | ||
/** | ||
* Add metadata to the the execution of a background function | ||
* @constructor | ||
* @param {Function} deferFn - A background function (`defer(...)` result) | ||
* @param {Metadata} metadata - The metadata (ex: `{ foo: "bar" }`) | ||
* @returns Function | ||
*/ | ||
const addMetadata = (deferFn, metadata) => { | ||
const newMetadata = { ...deferFn.__execOptions?.metadata, ...metadata }; | ||
const deferFnWithMetadata = async (...args) => { | ||
const fn = deferFn.__fn; | ||
let functionArguments; | ||
try { | ||
return Promise.resolve(await fn(...functionArguments)); | ||
functionArguments = JSON.parse(JSON.stringify(args)); | ||
} | ||
catch (error) { | ||
// const e = error as Error; | ||
let deferError = new Error("Defer execution failed"); | ||
if (error instanceof Error) { | ||
deferError = new Error(error.message); | ||
deferError.stack = error.stack || ""; | ||
const e = error; | ||
throw new errors_js_1.DeferError(`cannot serialize argument: ${e.message}`); | ||
} | ||
if (__verbose) | ||
console.log(`[defer.run][${fn.name}] invoked.`); | ||
if (__httpClient) { | ||
let scheduleFor; | ||
const delay = deferFn.__execOptions?.delay; | ||
if (delay instanceof Date) { | ||
scheduleFor = delay; | ||
} | ||
else if (delay) { | ||
const now = new Date(); | ||
scheduleFor = withDelay(now, delay); | ||
} | ||
else { | ||
deferError = error; | ||
scheduleFor = new Date(); | ||
} | ||
throw error; | ||
return (0, client_js_1.enqueueExecution)(__httpClient, { | ||
name: fn.name, | ||
arguments: functionArguments, | ||
scheduleFor, | ||
metadata: newMetadata, | ||
}); | ||
} | ||
if (__verbose) | ||
console.log(`[defer.run][${fn.name}] defer ignore, no token found.`); | ||
const id = (0, uuid_1.v4)(); | ||
__database.set(id, { id: id, state: "running" }); | ||
execLocally(id, fn, functionArguments); | ||
return { id }; | ||
}; | ||
return ret; | ||
}; | ||
exports.defer = defer; | ||
exports.defer.schedule = (fn, schedule) => { | ||
const ret = () => { | ||
throw new Error("`defer.scheduled()` functions should not be invoked."); | ||
deferFnWithMetadata.__fn = deferFn.__fn; | ||
deferFnWithMetadata.__metadata = deferFn.__metadata; | ||
deferFnWithMetadata.__execOptions = { | ||
...deferFn.__execOptions, | ||
metadata: newMetadata, | ||
}; | ||
ret.__fn = fn; | ||
ret.__metadata = { | ||
version: constants_js_1.INTERNAL_VERSION, | ||
cron: (0, natural_cron_js_1.default)(schedule), | ||
}; | ||
return ret; | ||
return deferFnWithMetadata; | ||
}; | ||
exports.addMetadata = addMetadata; | ||
/** | ||
* Delay the execution of a background function | ||
* Trigger the execution of a background function and waits for its result | ||
* @constructor | ||
* @param {Function} deferFn - A background function (`defer(...)` result) | ||
* @param {string|Date} delay - The delay (ex: "1h" or a Date object) | ||
* @returns Function | ||
*/ | ||
const delay = (deferFn, delay) => async (...args) => { | ||
const awaitResult = (deferFn) => async (...args) => { | ||
const fnName = deferFn.__fn.name; | ||
const fn = deferFn.__fn; | ||
@@ -150,25 +297,31 @@ let functionArguments; | ||
} | ||
if (__verbose) | ||
console.log(`[defer.run][${fn.name}] invoked.`); | ||
let response; | ||
if (__httpClient) { | ||
let scheduleFor; | ||
if (delay instanceof Date) { | ||
scheduleFor = delay; | ||
} | ||
else { | ||
const now = new Date(); | ||
scheduleFor = withDelay(now, delay); | ||
} | ||
return (0, client_js_1.enqueueExecution)(__httpClient, { | ||
name: fn.name, | ||
const { id } = await (0, client_js_1.enqueueExecution)(__httpClient, { | ||
name: fnName, | ||
arguments: functionArguments, | ||
scheduleFor: scheduleFor, | ||
scheduleFor: new Date(), | ||
metadata: {}, | ||
}); | ||
response = await (0, client_js_1.waitExecutionResult)(__httpClient, { id: id }); | ||
} | ||
if (__verbose) | ||
console.log(`[defer.run][${fn.name}] defer ignore, no token found.`); | ||
await fn(...functionArguments); | ||
return { id: FakeID }; | ||
else { | ||
const id = (0, uuid_1.v4)(); | ||
__database.set(id, { id: id, state: "running" }); | ||
response = await execLocally(id, fn, functionArguments); | ||
} | ||
if (response.state === "failed") { | ||
let error = new errors_js_1.DeferError("Defer execution failed"); | ||
if (response.result?.message) { | ||
error = new errors_js_1.DeferError(response.result.message); | ||
error.stack = response.result.stack; | ||
} | ||
else if (response.result) { | ||
error = response.result; | ||
} | ||
throw error; | ||
} | ||
return response.result; | ||
}; | ||
exports.delay = delay; | ||
exports.awaitResult = awaitResult; | ||
// EXAMPLES: | ||
@@ -193,7 +346,8 @@ // interface Contact { | ||
// } | ||
// defer.schedule(myFunction, "every day"); | ||
// defer.cron(myFunction, "every day"); | ||
// async function test() { | ||
// await importContactsD("1", []); // fire and forget | ||
// await importContactsD.await("1", []); // wait for execution result | ||
// await importContactsD.delayed("1", [], { delay: "2 days" }); // scheduled | ||
// const r = await importContactsD.await("1", []); // wait for execution result | ||
// const awaitImportContact = awaitResult(importContactsD); | ||
// const result = await awaitImportContact("1", []); | ||
// } | ||
@@ -200,0 +354,0 @@ // // Delayed |
@@ -7,7 +7,8 @@ import { jitter } from "./jitter.js"; | ||
schedule_for: request.scheduleFor, | ||
metadata: request.metadata, | ||
}); | ||
return client("POST", "/api/v1/enqueue", data); | ||
return client("POST", "/public/v1/enqueue", data); | ||
} | ||
export function fetchExecution(client, request) { | ||
return client("GET", `/api/v1/executions/${request.id}`); | ||
return client("GET", `/public/v1/executions/${request.id}`); | ||
} | ||
@@ -14,0 +15,0 @@ export async function waitExecutionResult(client, request) { |
@@ -1,1 +0,2 @@ | ||
export const INTERNAL_VERSION = 3; | ||
export const INTERNAL_VERSION = 5; | ||
export const RETRY_MAX_ATTEMPTS_PLACEHOLDER = 13; |
@@ -0,1 +1,11 @@ | ||
export const errorMessage = (error) => { | ||
let message = error.message; | ||
if (error.cause instanceof Error) { | ||
message = `${message}: ${errorMessage(error.cause)}`; | ||
} | ||
else { | ||
message = `${message}: ${String(error.cause)}`; | ||
} | ||
return message; | ||
}; | ||
export class DeferError extends Error { | ||
@@ -2,0 +12,0 @@ constructor(msg) { |
import { fetch } from "@whatwg-node/fetch"; | ||
import { URL } from "node:url"; | ||
import { APIError, ClientError, HTTPRequestError } from "./errors.js"; | ||
import { APIError, ClientError, HTTPRequestError, errorMessage, } from "./errors.js"; | ||
import VERSION from "./version.js"; | ||
const basicAuth = (username, password) => { | ||
@@ -17,3 +18,3 @@ const credentials = Buffer.from(`${username}:${password}`).toString("base64"); | ||
if (error instanceof Error) | ||
message = `invalid endpoint url: ${error.message}`; | ||
message = `invalid endpoint url: ${errorMessage(error)}`; | ||
else | ||
@@ -31,2 +32,3 @@ message = `unknown error: ${String(error)}`; | ||
"Content-type": "application/json", | ||
"User-Agent": `defer/${VERSION} (source: https://github.com/defer-run/defer.client)`, | ||
Authorization: basicAuth("", accessToken), | ||
@@ -43,3 +45,3 @@ }, | ||
if (error instanceof Error) | ||
message = `cannot execute http request: ${error.message}`; | ||
message = `cannot execute http request: ${errorMessage(error)}`; | ||
else | ||
@@ -55,3 +57,3 @@ message = `unknown error: ${String(error)}`; | ||
if (error instanceof Error) | ||
message = `cannot decode http response: ${error.message}`; | ||
message = `cannot decode http response: ${errorMessage(error)}`; | ||
else | ||
@@ -71,3 +73,3 @@ message = `unknown error: ${String(error)}`; | ||
const e = error; | ||
throw new ClientError(`cannot decode http response body: ${e.message}`); | ||
throw new ClientError(`cannot decode http response body: ${errorMessage(e)}`); | ||
} | ||
@@ -85,3 +87,3 @@ } | ||
const e = error; | ||
throw new ClientError(`cannot decode http response body: ${e.message}`); | ||
throw new ClientError(`cannot decode http response body: ${errorMessage(e)}`); | ||
} | ||
@@ -88,0 +90,0 @@ throw new APIError(decodedData.message, decodedData.code); |
300
esm/index.js
@@ -1,13 +0,11 @@ | ||
/* eslint-disable @typescript-eslint/no-explicit-any */ | ||
import parseDuration from "parse-duration"; | ||
// @ts-expect-error untyped dep | ||
import getCronString from "@darkeyedevelopers/natural-cron.js"; | ||
import { INTERNAL_VERSION } from "./constants.js"; | ||
const FakeID = "00000000000000000000000000000000"; | ||
import { enqueueExecution, waitExecutionResult, } from "./client.js"; | ||
import { DeferError } from "./errors.js"; | ||
import { v4 as randomUUID } from "uuid"; | ||
import { INTERNAL_VERSION, RETRY_MAX_ATTEMPTS_PLACEHOLDER, } from "./constants.js"; | ||
import { enqueueExecution, fetchExecution, waitExecutionResult, } from "./client.js"; | ||
import { APIError, DeferError } from "./errors.js"; | ||
import { makeHTTPClient } from "./httpClient.js"; | ||
const withDelay = (dt, delay) => new Date(dt.getTime() + parseDuration(delay)); | ||
const __database = new Map(); | ||
let __accessToken = process.env["DEFER_TOKEN"]; | ||
let __endpoint = "https://api.defer.run"; | ||
let __endpoint = process.env["DEFER_ENDPOINT"] || "https://api.defer.run"; | ||
let __verbose = false; | ||
@@ -28,2 +26,48 @@ let __httpClient; | ||
} | ||
export const deferEnabled = () => !!__accessToken; | ||
async function execLocally(id, fn, args) { | ||
let state = "succeed"; | ||
let originalResult; | ||
try { | ||
originalResult = await fn(...args); | ||
} | ||
catch (error) { | ||
const e = error; | ||
state = "failed"; | ||
originalResult = { | ||
name: e.name, | ||
message: e.message, | ||
cause: e.cause, | ||
stack: e.stack, | ||
}; | ||
} | ||
let result; | ||
try { | ||
result = JSON.parse(JSON.stringify(originalResult || "")); | ||
} | ||
catch (error) { | ||
const e = error; | ||
throw new DeferError(`cannot serialize function return: ${e.message}`); | ||
} | ||
const response = { id, state, result }; | ||
__database.set(id, response); | ||
return response; | ||
} | ||
export async function getExecution(id) { | ||
if (__httpClient) | ||
return fetchExecution(__httpClient, { id }); | ||
const response = __database.get(id); | ||
if (response) | ||
return Promise.resolve(response); | ||
throw new APIError("execution not found", ""); | ||
} | ||
function defaultRetryPolicy() { | ||
return { | ||
maxAttempts: 0, | ||
initialInterval: 30, | ||
randomizationFactor: 0.5, | ||
multiplier: 1.5, | ||
maxInterval: 60 * 10, | ||
}; | ||
} | ||
export const defer = (fn, options) => { | ||
@@ -46,2 +90,3 @@ const ret = async (...args) => { | ||
scheduleFor: new Date(), | ||
metadata: {}, | ||
}); | ||
@@ -51,15 +96,73 @@ } | ||
console.log(`[defer.run][${fn.name}] defer ignore, no token found.`); | ||
await fn(...functionArguments); | ||
return { id: FakeID }; | ||
const id = randomUUID(); | ||
__database.set(id, { id: id, state: "running" }); | ||
execLocally(id, fn, functionArguments); | ||
return { id }; | ||
}; | ||
ret.__fn = fn; | ||
let retryPolicy = 0; | ||
if (options?.retry === true) { | ||
retryPolicy = 12; | ||
const retryPolicy = defaultRetryPolicy(); | ||
switch (typeof options?.retry) { | ||
case "boolean": { | ||
if (options.retry) { | ||
retryPolicy.maxAttempts = RETRY_MAX_ATTEMPTS_PLACEHOLDER; | ||
} | ||
break; | ||
} | ||
case "number": { | ||
retryPolicy.maxAttempts = options.retry; | ||
break; | ||
} | ||
case "object": { | ||
if (options.retry.maxAttempts) { | ||
retryPolicy.maxAttempts = options.retry.maxAttempts; | ||
} | ||
else { | ||
options.retry.maxAttempts = RETRY_MAX_ATTEMPTS_PLACEHOLDER; | ||
} | ||
if (options.retry.initialInterval) | ||
retryPolicy.initialInterval = options.retry.initialInterval; | ||
if (options.retry.randomizationFactor) | ||
retryPolicy.randomizationFactor = options.retry.randomizationFactor; | ||
if (options.retry.multiplier) | ||
retryPolicy.multiplier = options.retry.multiplier; | ||
if (options.retry.maxInterval) | ||
retryPolicy.maxInterval = options.retry.maxInterval; | ||
break; | ||
} | ||
case "undefined": { | ||
retryPolicy.maxAttempts = 0; | ||
break; | ||
} | ||
default: { | ||
throw new Error("invalid retry options"); | ||
} | ||
} | ||
if (typeof options?.retry === "number") { | ||
retryPolicy = options.retry; | ||
} | ||
ret.__metadata = { version: INTERNAL_VERSION, retry: retryPolicy }; | ||
ret.await = async (...args) => { | ||
ret.__metadata = { | ||
version: INTERNAL_VERSION, | ||
retry: retryPolicy, | ||
concurrency: options?.concurrency, | ||
}; | ||
return ret; | ||
}; | ||
defer.cron = (fn, schedule) => { | ||
const ret = () => { | ||
throw new Error("`defer.cron()` functions should not be invoked."); | ||
}; | ||
ret.__fn = fn; | ||
ret.__metadata = { | ||
version: INTERNAL_VERSION, | ||
cron: schedule, | ||
}; | ||
return ret; | ||
}; | ||
/** | ||
* Delay the execution of a background function | ||
* @constructor | ||
* @param {Function} deferFn - A background function (`defer(...)` result) | ||
* @param {string|Date} delay - The delay (ex: "1h" or a Date object) | ||
* @returns Function | ||
*/ | ||
export const delay = (deferFn, delay) => { | ||
const delayedDeferFn = async (...args) => { | ||
const fn = deferFn.__fn; | ||
let functionArguments; | ||
@@ -73,59 +176,99 @@ try { | ||
} | ||
if (__verbose) | ||
console.log(`[defer.run][${fn.name}] invoked.`); | ||
if (__httpClient) { | ||
const { id } = await enqueueExecution(__httpClient, { | ||
let scheduleFor; | ||
if (delay instanceof Date) { | ||
scheduleFor = delay; | ||
} | ||
else { | ||
const now = new Date(); | ||
scheduleFor = withDelay(now, delay); | ||
} | ||
return enqueueExecution(__httpClient, { | ||
name: fn.name, | ||
arguments: functionArguments, | ||
scheduleFor: new Date(), | ||
scheduleFor, | ||
metadata: deferFn.__execOptions?.metadata || {}, | ||
}); | ||
const response = await waitExecutionResult(__httpClient, { id: id }); | ||
if (response.state === "failed") { | ||
let error = new Error("Defer execution failed"); | ||
if (response.result?.message) { | ||
error = new Error(response.result.message); | ||
error.stack = response.result.stack; | ||
} | ||
else if (response.result) { | ||
error = response.result; | ||
} | ||
throw error; | ||
} | ||
return response.result; | ||
} | ||
if (__verbose) | ||
console.log(`[defer.run][${fn.name}] defer ignore, no token found.`); | ||
const id = randomUUID(); | ||
__database.set(id, { id: id, state: "running" }); | ||
execLocally(id, fn, functionArguments); | ||
return { id }; | ||
}; | ||
delayedDeferFn.__fn = deferFn.__fn; | ||
delayedDeferFn.__metadata = deferFn.__metadata; | ||
delayedDeferFn.__execOptions = { | ||
...deferFn.__execOptions, | ||
delay, | ||
}; | ||
return delayedDeferFn; | ||
}; | ||
/** | ||
* Add metadata to the the execution of a background function | ||
* @constructor | ||
* @param {Function} deferFn - A background function (`defer(...)` result) | ||
* @param {Metadata} metadata - The metadata (ex: `{ foo: "bar" }`) | ||
* @returns Function | ||
*/ | ||
export const addMetadata = (deferFn, metadata) => { | ||
const newMetadata = { ...deferFn.__execOptions?.metadata, ...metadata }; | ||
const deferFnWithMetadata = async (...args) => { | ||
const fn = deferFn.__fn; | ||
let functionArguments; | ||
try { | ||
return Promise.resolve(await fn(...functionArguments)); | ||
functionArguments = JSON.parse(JSON.stringify(args)); | ||
} | ||
catch (error) { | ||
// const e = error as Error; | ||
let deferError = new Error("Defer execution failed"); | ||
if (error instanceof Error) { | ||
deferError = new Error(error.message); | ||
deferError.stack = error.stack || ""; | ||
const e = error; | ||
throw new DeferError(`cannot serialize argument: ${e.message}`); | ||
} | ||
if (__verbose) | ||
console.log(`[defer.run][${fn.name}] invoked.`); | ||
if (__httpClient) { | ||
let scheduleFor; | ||
const delay = deferFn.__execOptions?.delay; | ||
if (delay instanceof Date) { | ||
scheduleFor = delay; | ||
} | ||
else if (delay) { | ||
const now = new Date(); | ||
scheduleFor = withDelay(now, delay); | ||
} | ||
else { | ||
deferError = error; | ||
scheduleFor = new Date(); | ||
} | ||
throw error; | ||
return enqueueExecution(__httpClient, { | ||
name: fn.name, | ||
arguments: functionArguments, | ||
scheduleFor, | ||
metadata: newMetadata, | ||
}); | ||
} | ||
if (__verbose) | ||
console.log(`[defer.run][${fn.name}] defer ignore, no token found.`); | ||
const id = randomUUID(); | ||
__database.set(id, { id: id, state: "running" }); | ||
execLocally(id, fn, functionArguments); | ||
return { id }; | ||
}; | ||
return ret; | ||
}; | ||
defer.schedule = (fn, schedule) => { | ||
const ret = () => { | ||
throw new Error("`defer.scheduled()` functions should not be invoked."); | ||
deferFnWithMetadata.__fn = deferFn.__fn; | ||
deferFnWithMetadata.__metadata = deferFn.__metadata; | ||
deferFnWithMetadata.__execOptions = { | ||
...deferFn.__execOptions, | ||
metadata: newMetadata, | ||
}; | ||
ret.__fn = fn; | ||
ret.__metadata = { | ||
version: INTERNAL_VERSION, | ||
cron: getCronString(schedule), | ||
}; | ||
return ret; | ||
return deferFnWithMetadata; | ||
}; | ||
/** | ||
* Delay the execution of a background function | ||
* Trigger the execution of a background function and waits for its result | ||
* @constructor | ||
* @param {Function} deferFn - A background function (`defer(...)` result) | ||
* @param {string|Date} delay - The delay (ex: "1h" or a Date object) | ||
* @returns Function | ||
*/ | ||
export const delay = (deferFn, delay) => async (...args) => { | ||
export const awaitResult = (deferFn) => async (...args) => { | ||
const fnName = deferFn.__fn.name; | ||
const fn = deferFn.__fn; | ||
@@ -140,23 +283,29 @@ let functionArguments; | ||
} | ||
if (__verbose) | ||
console.log(`[defer.run][${fn.name}] invoked.`); | ||
let response; | ||
if (__httpClient) { | ||
let scheduleFor; | ||
if (delay instanceof Date) { | ||
scheduleFor = delay; | ||
} | ||
else { | ||
const now = new Date(); | ||
scheduleFor = withDelay(now, delay); | ||
} | ||
return enqueueExecution(__httpClient, { | ||
name: fn.name, | ||
const { id } = await enqueueExecution(__httpClient, { | ||
name: fnName, | ||
arguments: functionArguments, | ||
scheduleFor: scheduleFor, | ||
scheduleFor: new Date(), | ||
metadata: {}, | ||
}); | ||
response = await waitExecutionResult(__httpClient, { id: id }); | ||
} | ||
if (__verbose) | ||
console.log(`[defer.run][${fn.name}] defer ignore, no token found.`); | ||
await fn(...functionArguments); | ||
return { id: FakeID }; | ||
else { | ||
const id = randomUUID(); | ||
__database.set(id, { id: id, state: "running" }); | ||
response = await execLocally(id, fn, functionArguments); | ||
} | ||
if (response.state === "failed") { | ||
let error = new DeferError("Defer execution failed"); | ||
if (response.result?.message) { | ||
error = new DeferError(response.result.message); | ||
error.stack = response.result.stack; | ||
} | ||
else if (response.result) { | ||
error = response.result; | ||
} | ||
throw error; | ||
} | ||
return response.result; | ||
}; | ||
@@ -182,7 +331,8 @@ // EXAMPLES: | ||
// } | ||
// defer.schedule(myFunction, "every day"); | ||
// defer.cron(myFunction, "every day"); | ||
// async function test() { | ||
// await importContactsD("1", []); // fire and forget | ||
// await importContactsD.await("1", []); // wait for execution result | ||
// await importContactsD.delayed("1", [], { delay: "2 days" }); // scheduled | ||
// const r = await importContactsD.await("1", []); // wait for execution result | ||
// const awaitImportContact = awaitResult(importContactsD); | ||
// const result = await awaitImportContact("1", []); | ||
// } | ||
@@ -189,0 +339,0 @@ // // Delayed |
{ | ||
"name": "@defer/client", | ||
"version": "2.0.0-alpha-20230225204614-f9e6d83", | ||
"version": "2.0.0-alpha-20230628211612-9c2d07c", | ||
"description": "Zero infrastructure NodeJS background jobs", | ||
"dependencies": { | ||
"@darkeyedevelopers/natural-cron.js": "^1.1.0", | ||
"@whatwg-node/fetch": "^0.2.9", | ||
"parse-duration": "^1.0.2" | ||
"parse-duration": "^1.0.2", | ||
"uuid": "^9.0.0" | ||
}, | ||
@@ -10,0 +10,0 @@ "repository": "git@github.com:defer-run/defer.client.git", |
@@ -1,54 +0,18 @@ | ||
# `@defer.run/client` | ||
# `@defer/client` | ||
`defer` is your favorite background tasks handler to offload your JavaScript API. | ||
Zero infrastructure Node.js background jobs. | ||
---- | ||
## Install | ||
## Get started | ||
- [Next.js Quickstart](https://docs.defer.run/quickstart/next/) | ||
- [Express/Koa/hapi Quickstart](https://docs.defer.run/quickstart/express-koa-hapi/) | ||
```sh | ||
yarn add @defer.run/client | ||
# or | ||
## API documentation | ||
npm i @defer.run/client | ||
``` | ||
## Configuration | ||
Make sure to define the `DEFER_TOKEN` environment variable in production/staging environments. | ||
## Usage | ||
### 1. Define your background function | ||
A background function should be a unique default export of a file placed in `background-functions/` folder. | ||
```ts | ||
import { defer } from "@defer.run/client"; | ||
function importContacts(intercomId: string) { | ||
// import contacts from Hubspot and insert them in the database | ||
} | ||
export default defer(importContacts) | ||
``` | ||
### 2. Call your background function | ||
For a seamless integration with your project, we recommend to install our `@defer.run/babel` babel plugin. | ||
Our babel plugin will replace all usage of defer functions to client pushes: | ||
```ts | ||
import type { NextApiRequest, NextApiResponse } from "next"; | ||
import sentToIntercom from "../../background-functions/importContacts"; | ||
export default function handler(req: NextApiRequest, res: NextApiResponse) { | ||
// will be executed in the background | ||
await importContacts(currentUser.intercomId); | ||
res.status(200).json({ name: "John Doe" }); | ||
} | ||
``` | ||
- [Configuration options: retries, concurrency](https://docs.defer.run/features/retries-concurrency/) | ||
- [Delayed Function](https://docs.defer.run/features/delay/) | ||
- [CRON](https://docs.defer.run/features/cron/) | ||
- [Workflows](https://docs.defer.run/features/workflows/) |
import type { HTTPClient } from "./httpClient.js"; | ||
import type { Metadata } from "./index.js"; | ||
export interface EnqueueExecutionRequest { | ||
@@ -6,2 +7,3 @@ name: string; | ||
scheduleFor: Date; | ||
metadata: Metadata; | ||
} | ||
@@ -8,0 +10,0 @@ export interface EnqueueExecutionResponse { |
@@ -1,1 +0,2 @@ | ||
export declare const INTERNAL_VERSION = 3; | ||
export declare const INTERNAL_VERSION = 5; | ||
export declare const RETRY_MAX_ATTEMPTS_PLACEHOLDER = 13; |
@@ -0,1 +1,2 @@ | ||
export declare const errorMessage: (error: Error) => string; | ||
export declare class DeferError extends Error { | ||
@@ -2,0 +3,0 @@ constructor(msg: string); |
import { Units } from "parse-duration"; | ||
import { EnqueueExecutionResponse } from "./client.js"; | ||
import { EnqueueExecutionResponse, FetchExecutionResponse } from "./client.js"; | ||
interface Options { | ||
@@ -9,10 +9,17 @@ accessToken?: string; | ||
export declare function configure(opts?: Options): void; | ||
export declare const deferEnabled: () => boolean; | ||
export declare function getExecution(id: string): Promise<FetchExecutionResponse>; | ||
export declare type UnPromise<F> = F extends Promise<infer R> ? R : F; | ||
export declare type DelayString = `${string}${Units}`; | ||
export interface Metadata { | ||
[key: string]: string; | ||
} | ||
export interface DeferExecutionOptions { | ||
delay: DelayString | Date; | ||
delay?: DelayString | Date; | ||
metadata?: Metadata; | ||
} | ||
export declare type DeferRetFnParameters<F extends (...args: any | undefined) => Promise<any>> = [...first: Parameters<F>, options: DeferExecutionOptions]; | ||
export declare type RetryNumber = 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12; | ||
export declare type Concurrency = 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50; | ||
declare type Enumerate<N extends number, Acc extends number[] = []> = Acc["length"] extends N ? Acc[number] : Enumerate<N, [...Acc, Acc["length"]]>; | ||
declare type Range<F extends number, T extends number> = Exclude<Enumerate<T>, Enumerate<F>>; | ||
export declare type Concurrency = Range<0, 51>; | ||
export interface HasDeferMetadata { | ||
@@ -22,4 +29,5 @@ __metadata: { | ||
cron?: string; | ||
retry?: RetryNumber; | ||
concurrency?: Concurrency; | ||
retry?: RetryPolicy; | ||
concurrency?: Concurrency | undefined; | ||
maxDuration?: number | undefined; | ||
}; | ||
@@ -30,3 +38,3 @@ } | ||
__fn: F; | ||
await: DeferAwaitRetFn<F>; | ||
__execOptions?: DeferExecutionOptions; | ||
} | ||
@@ -42,6 +50,13 @@ export interface DeferScheduledFn<F extends (...args: never) => Promise<any>> extends HasDeferMetadata { | ||
<F extends (...args: any | undefined) => Promise<any>>(fn: F, options?: DeferOptions): DeferRetFn<F>; | ||
schedule: <F extends (args: never[]) => Promise<any>>(fn: F, schedule: string) => DeferScheduledFn<F>; | ||
cron: <F extends (args: never[]) => Promise<any>>(fn: F, schedule: string) => DeferScheduledFn<F>; | ||
} | ||
export interface RetryPolicy { | ||
maxAttempts: number; | ||
initialInterval: number; | ||
randomizationFactor: number; | ||
multiplier: number; | ||
maxInterval: number; | ||
} | ||
export interface DeferOptions { | ||
retry?: boolean | RetryNumber; | ||
retry?: boolean | number | Partial<RetryPolicy>; | ||
concurrency?: Concurrency; | ||
@@ -51,3 +66,3 @@ } | ||
interface DeferDelay { | ||
<F extends (...args: any | undefined) => Promise<any>>(deferFn: DeferRetFn<F>, delay: DelayString | Date): (...args: Parameters<F>) => Promise<EnqueueExecutionResponse>; | ||
<F extends (...args: any | undefined) => Promise<any>>(deferFn: DeferRetFn<F>, delay: DelayString | Date): DeferRetFn<F>; | ||
} | ||
@@ -62,2 +77,23 @@ /** | ||
export declare const delay: DeferDelay; | ||
interface DeferAddMetadata { | ||
<F extends (...args: any | undefined) => Promise<any>>(deferFn: DeferRetFn<F>, metadata: Metadata): DeferRetFn<F>; | ||
} | ||
/** | ||
* Add metadata to the the execution of a background function | ||
* @constructor | ||
* @param {Function} deferFn - A background function (`defer(...)` result) | ||
* @param {Metadata} metadata - The metadata (ex: `{ foo: "bar" }`) | ||
* @returns Function | ||
*/ | ||
export declare const addMetadata: DeferAddMetadata; | ||
interface DeferAwaitResult { | ||
<F extends (...args: any | undefined) => Promise<any>>(deferFn: DeferRetFn<F>): DeferAwaitRetFn<F>; | ||
} | ||
/** | ||
* Trigger the execution of a background function and waits for its result | ||
* @constructor | ||
* @param {Function} deferFn - A background function (`defer(...)` result) | ||
* @returns Function | ||
*/ | ||
export declare const awaitResult: DeferAwaitResult; | ||
export {}; |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Environment variable access
Supply chain riskPackage accesses environment variables, which may be a sign of credential stuffing or data theft.
Found 1 instance in 1 package
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
50643
32
1206
1
19
5
+ Addeduuid@^9.0.0
+ Addeduuid@9.0.1(transitive)
- Removed@darkeyedevelopers/natural-cron.js@1.1.0(transitive)