workcraft-js
Version:
Node module for Workcraft Workers
223 lines • 6.61 kB
JavaScript
// src/index.ts
import { v4 as uuidv4 } from "uuid";
import { SignJWT } from "jose";
var TokenExpiration = 24 * 60 * 60;
var createDefaultTaskPayload = (partial = {}) => ({
task_args: partial.task_args ?? [],
task_kwargs: partial.task_kwargs ?? {},
prerun_handler_args: partial.prerun_handler_args ?? [],
prerun_handler_kwargs: partial.prerun_handler_kwargs ?? {},
postrun_handler_args: partial.postrun_handler_args ?? [],
postrun_handler_kwargs: partial.postrun_handler_kwargs ?? {}
});
var TaskStatus = /* @__PURE__ */ ((TaskStatus2) => {
TaskStatus2["PENDING"] = "PENDING";
TaskStatus2["RUNNING"] = "RUNNING";
TaskStatus2["SUCCESS"] = "SUCCESS";
TaskStatus2["FAILURE"] = "FAILURE";
TaskStatus2["INVALID"] = "INVALID";
TaskStatus2["CANCELLED"] = "CANCELLED";
return TaskStatus2;
})(TaskStatus || {});
var PeonStatus = /* @__PURE__ */ ((PeonStatus2) => {
PeonStatus2["IDLE"] = "IDLE";
PeonStatus2["PREPARING"] = "PREPARING";
PeonStatus2["WORKING"] = "WORKING";
PeonStatus2["OFFLINE"] = "OFFLINE";
return PeonStatus2;
})(PeonStatus || {});
var UpdateType = /* @__PURE__ */ ((UpdateType2) => {
UpdateType2["TASK_UPDATE"] = "task_update";
UpdateType2["PEON_UPDATE"] = "peon_update";
return UpdateType2;
})(UpdateType || {});
var WorkcraftClient = class {
config;
strongholdUrl;
apiKey = null;
fetchWithApiKey = async (input, init) => {
if (this.apiKey === null) throw new Error("No API key provided");
return fetch(input, {
...init,
headers: {
...init?.headers,
WORKCRAFT_API_KEY: this.apiKey
}
});
};
sse = null;
subscribers = /* @__PURE__ */ new Map();
reconnectDelay = 5e3;
// Start with 1 second
constructor(config) {
this.config = config;
this.strongholdUrl = config.strongholdUrl;
}
async createJWT() {
const now = Math.floor(Date.now() / 1e3);
const secret = new TextEncoder().encode(this.config.apiKey);
const jwt = await new SignJWT({ api_key: this.config.apiKey }).setProtectedHeader({ alg: "HS256" }).setIssuedAt(now).setExpirationTime(now + TokenExpiration).setNotBefore(now).sign(secret);
return jwt;
}
async setupSSE() {
if (this.apiKey === null) {
throw new Error("Client must be initialized before subscribing");
}
const url = `${this.strongholdUrl}/events?type=chieftain`;
try {
this.sse = new EventSource(url, { withCredentials: true });
} catch (error) {
console.error("Failed to create EventSource:", error);
return;
}
this.sse.onopen = (event) => {
console.log("SSE connection established");
};
this.sse.onmessage = (event) => {
try {
const update = JSON.parse(event.data);
this.notifySubscribers(update);
} catch (error) {
console.error("Failed to parse SSE message:", error);
}
};
this.sse.onerror = (error) => {
console.error("SSE error:", error);
};
}
notifySubscribers(update) {
this.subscribers.forEach((subscriber) => {
try {
subscriber(update);
} catch (error) {
console.error("Error in subscriber:", error);
}
});
}
async subscribe(callback) {
if (!this.sse) {
await this.setupSSE();
}
const id = uuidv4();
this.subscribers.set(id, callback);
return () => {
this.subscribers.delete(id);
if (this.subscribers.size === 0 && this.sse) {
this.sse.close();
this.sse = null;
}
};
}
async disconnect() {
if (this.sse) {
this.sse.close();
this.sse = null;
this.subscribers.clear();
}
}
async init() {
this.apiKey = this.config.apiKey;
try {
console.log("fetching", this.strongholdUrl + "/api/test");
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), 5e3);
const res = await this.fetchWithApiKey(this.strongholdUrl + "/api/test", {
signal: controller.signal
});
clearTimeout(timeout);
if (res.status !== 200) {
console.error(`Server responded with status ${res.status}`);
}
} catch (error) {
if (error instanceof Error) {
if ("name" in error && error.name === "AbortError") {
console.error("Connection timeout - server might be offline");
} else {
console.error(
"Failed to connect to the stronghold server:",
error.message
);
}
} else {
console.error("Failed to connect to the stronghold server:", error);
}
}
}
async deleteTaskOrThrow(taskId) {
const res = await this.fetchWithApiKey(
this.strongholdUrl + "/api/task/" + taskId + "/delete",
{
method: "DELETE",
headers: {
"Content-Type": "application/json"
}
}
);
if (res.status < 200 || res.status > 299) {
throw new Error("Failed to delete the task: " + await res.text());
}
return res.json();
}
async createTaskOrThrow({
taskName,
taskPayload = {},
queue = "DEFAULT",
retryOnFailure = false,
retryLimit = 0
}) {
const res = await this.fetchWithApiKey(this.strongholdUrl + "/api/task", {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({
id: uuidv4(),
task_name: taskName,
queue,
retry_on_failure: retryOnFailure,
retry_limit: retryLimit,
payload: createDefaultTaskPayload(taskPayload)
})
});
if (res.status !== 201) {
throw new Error("Failed to create a task: " + await res.text());
}
return res.json();
}
async getTaskByIdOrThrow(id) {
const res = await this.fetchWithApiKey(
this.strongholdUrl + "/api/task/" + id
);
if (res.status !== 200) {
throw new Error("Failed to get task by id: " + await res.text());
}
return res.json();
}
async getPeonByIdOrThrow(id) {
const res = await this.fetchWithApiKey(
this.strongholdUrl + "/api/peon/" + id
);
if (res.status !== 200) {
throw new Error("Failed to get peon by id: " + await res.text());
}
return res.json();
}
async cancelTaskOrThrow(id) {
const res = await this.fetchWithApiKey(
this.strongholdUrl + "/api/task/" + id + "/cancel",
{
method: "POST"
}
);
if (res.status !== 200) {
throw new Error("Failed to cancel task: " + await res.text());
}
}
};
export {
PeonStatus,
TaskStatus,
UpdateType,
WorkcraftClient
};
//# sourceMappingURL=index.js.map