@corvohq/worker 0.2.0
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/dist/gen/corvo/v1/worker_pb.d.ts +689 -0
- package/dist/gen/corvo/v1/worker_pb.js +133 -0
- package/dist/index.d.ts +55 -0
- package/dist/index.js +405 -0
- package/dist/rpc.d.ts +78 -0
- package/dist/rpc.js +193 -0
- package/package.json +24 -0
package/dist/rpc.js
ADDED
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
import { createClient } from "@connectrpc/connect";
|
|
2
|
+
import { createConnectTransport } from "@connectrpc/connect-node";
|
|
3
|
+
import { create } from "@bufbuild/protobuf";
|
|
4
|
+
import { WorkerService, HeartbeatJobUpdateSchema, } from "./gen/corvo/v1/worker_pb.js";
|
|
5
|
+
// ---------------------------------------------------------------------------
|
|
6
|
+
// Auth interceptor
|
|
7
|
+
// ---------------------------------------------------------------------------
|
|
8
|
+
function authInterceptor(auth) {
|
|
9
|
+
return (next) => async (req) => {
|
|
10
|
+
if (auth.headers) {
|
|
11
|
+
for (const [k, v] of Object.entries(auth.headers)) {
|
|
12
|
+
req.header.set(k, v);
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
if (auth.apiKey) {
|
|
16
|
+
req.header.set(auth.apiKeyHeader || "X-API-Key", auth.apiKey);
|
|
17
|
+
}
|
|
18
|
+
let token = auth.bearerToken || "";
|
|
19
|
+
if (auth.tokenProvider) {
|
|
20
|
+
token = await auth.tokenProvider();
|
|
21
|
+
}
|
|
22
|
+
if (token) {
|
|
23
|
+
req.header.set("authorization", `Bearer ${token}`);
|
|
24
|
+
}
|
|
25
|
+
return next(req);
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
function makeClient(baseUrl, auth) {
|
|
29
|
+
const transport = createConnectTransport({
|
|
30
|
+
baseUrl,
|
|
31
|
+
httpVersion: "1.1",
|
|
32
|
+
useBinaryFormat: false, // use JSON for compatibility
|
|
33
|
+
interceptors: [authInterceptor(auth)],
|
|
34
|
+
});
|
|
35
|
+
return createClient(WorkerService, transport);
|
|
36
|
+
}
|
|
37
|
+
// ---------------------------------------------------------------------------
|
|
38
|
+
// RpcClient
|
|
39
|
+
// ---------------------------------------------------------------------------
|
|
40
|
+
export class RpcClient {
|
|
41
|
+
constructor(baseUrl, auth = {}) {
|
|
42
|
+
this.client = makeClient(baseUrl.replace(/\/$/, ""), auth);
|
|
43
|
+
}
|
|
44
|
+
async enqueue(queue, payloadJson) {
|
|
45
|
+
const resp = await this.client.enqueue({ queue, payloadJson });
|
|
46
|
+
return {
|
|
47
|
+
jobId: resp.jobId,
|
|
48
|
+
status: resp.status,
|
|
49
|
+
uniqueExisting: resp.uniqueExisting,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
async fail(jobId, error, backtrace = "") {
|
|
53
|
+
const resp = await this.client.fail({ jobId, error, backtrace });
|
|
54
|
+
return { status: resp.status };
|
|
55
|
+
}
|
|
56
|
+
async heartbeat(jobs) {
|
|
57
|
+
const protoJobs = {};
|
|
58
|
+
for (const [id, update] of Object.entries(jobs)) {
|
|
59
|
+
protoJobs[id] = create(HeartbeatJobUpdateSchema, {
|
|
60
|
+
progressJson: update.progressJson ?? "",
|
|
61
|
+
checkpointJson: update.checkpointJson ?? "",
|
|
62
|
+
streamDelta: update.streamDelta ?? "",
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
const resp = await this.client.heartbeat({ jobs: protoJobs });
|
|
66
|
+
const out = {};
|
|
67
|
+
for (const [id, jr] of Object.entries(resp.jobs)) {
|
|
68
|
+
out[id] = { status: jr.status };
|
|
69
|
+
}
|
|
70
|
+
return out;
|
|
71
|
+
}
|
|
72
|
+
/** Unary call to StreamLifecycle endpoint (Connect JSON protocol). */
|
|
73
|
+
async lifecycleExchange(body) {
|
|
74
|
+
// StreamLifecycle is bidi streaming in proto, but we call it as unary
|
|
75
|
+
// using the raw Connect JSON-over-HTTP protocol for compatibility.
|
|
76
|
+
// The server supports single-request/single-response over HTTP/1.1.
|
|
77
|
+
throw new Error("lifecycleExchange is not supported via createClient; use LifecycleStream instead");
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
// ---------------------------------------------------------------------------
|
|
81
|
+
// ErrNotLeader
|
|
82
|
+
// ---------------------------------------------------------------------------
|
|
83
|
+
export class ErrNotLeader extends Error {
|
|
84
|
+
constructor(leaderAddr) {
|
|
85
|
+
super(leaderAddr
|
|
86
|
+
? `NOT_LEADER: leader is at ${leaderAddr}`
|
|
87
|
+
: "NOT_LEADER: leader unknown");
|
|
88
|
+
this.name = "ErrNotLeader";
|
|
89
|
+
this.leaderAddr = leaderAddr;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
// ---------------------------------------------------------------------------
|
|
93
|
+
// LifecycleStream — bidi streaming over Connect RPC
|
|
94
|
+
// ---------------------------------------------------------------------------
|
|
95
|
+
export class LifecycleStream {
|
|
96
|
+
constructor(baseUrl, auth = {}) {
|
|
97
|
+
this.closed = false;
|
|
98
|
+
this.baseUrl = baseUrl.replace(/\/$/, "");
|
|
99
|
+
this.auth = auth;
|
|
100
|
+
}
|
|
101
|
+
async exchange(req) {
|
|
102
|
+
if (this.closed) {
|
|
103
|
+
throw new Error("stream closed");
|
|
104
|
+
}
|
|
105
|
+
// StreamLifecycle is bidi streaming in the proto, but the server
|
|
106
|
+
// supports Connect protocol unary-style JSON calls. We use raw HTTP
|
|
107
|
+
// because createClient's bidi streaming requires HTTP/2 and a long-lived
|
|
108
|
+
// stream, which doesn't match our request-per-exchange pattern.
|
|
109
|
+
const h = { "content-type": "application/json" };
|
|
110
|
+
if (this.auth.headers) {
|
|
111
|
+
for (const [k, v] of Object.entries(this.auth.headers)) {
|
|
112
|
+
h[k] = v;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
if (this.auth.apiKey) {
|
|
116
|
+
h[this.auth.apiKeyHeader || "X-API-Key"] = this.auth.apiKey;
|
|
117
|
+
}
|
|
118
|
+
let token = this.auth.bearerToken || "";
|
|
119
|
+
if (this.auth.tokenProvider) {
|
|
120
|
+
token = await this.auth.tokenProvider();
|
|
121
|
+
}
|
|
122
|
+
if (token) {
|
|
123
|
+
h["authorization"] = `Bearer ${token}`;
|
|
124
|
+
}
|
|
125
|
+
const url = `${this.baseUrl}/corvo.v1.WorkerService/StreamLifecycle`;
|
|
126
|
+
const body = JSON.stringify({
|
|
127
|
+
requestId: req.requestId,
|
|
128
|
+
queues: req.queues,
|
|
129
|
+
workerId: req.workerId,
|
|
130
|
+
hostname: req.hostname,
|
|
131
|
+
leaseDuration: req.leaseDuration,
|
|
132
|
+
fetchCount: req.fetchCount,
|
|
133
|
+
acks: req.acks.map((a) => ({
|
|
134
|
+
jobId: a.jobId,
|
|
135
|
+
resultJson: a.resultJson,
|
|
136
|
+
})),
|
|
137
|
+
enqueues: req.enqueues.map((e) => ({
|
|
138
|
+
queue: e.queue,
|
|
139
|
+
payloadJson: e.payloadJson,
|
|
140
|
+
})),
|
|
141
|
+
});
|
|
142
|
+
const resp = await globalThis.fetch(url, {
|
|
143
|
+
method: "POST",
|
|
144
|
+
headers: h,
|
|
145
|
+
body,
|
|
146
|
+
});
|
|
147
|
+
if (!resp.ok) {
|
|
148
|
+
throw new Error(`RPC StreamLifecycle failed: HTTP ${resp.status}`);
|
|
149
|
+
}
|
|
150
|
+
const msg = (await resp.json());
|
|
151
|
+
if (msg.error === "NOT_LEADER") {
|
|
152
|
+
throw new ErrNotLeader(msg.leaderAddr ?? "");
|
|
153
|
+
}
|
|
154
|
+
return {
|
|
155
|
+
requestId: msg.requestId ?? 0,
|
|
156
|
+
jobs: msg.jobs ?? [],
|
|
157
|
+
acked: msg.acked ?? 0,
|
|
158
|
+
enqueuedJobIds: msg.enqueuedJobIds ?? [],
|
|
159
|
+
error: msg.error ?? "",
|
|
160
|
+
leaderAddr: msg.leaderAddr ?? "",
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
close() {
|
|
164
|
+
this.closed = true;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
// ---------------------------------------------------------------------------
|
|
168
|
+
// ResilientLifecycleStream — reconnects on NOT_LEADER
|
|
169
|
+
// ---------------------------------------------------------------------------
|
|
170
|
+
export class ResilientLifecycleStream {
|
|
171
|
+
constructor(baseUrl, auth = {}) {
|
|
172
|
+
this.baseUrl = baseUrl.replace(/\/$/, "");
|
|
173
|
+
this.auth = auth;
|
|
174
|
+
this.stream = new LifecycleStream(this.baseUrl, auth);
|
|
175
|
+
}
|
|
176
|
+
async exchange(req) {
|
|
177
|
+
try {
|
|
178
|
+
return await this.stream.exchange(req);
|
|
179
|
+
}
|
|
180
|
+
catch (err) {
|
|
181
|
+
if (err instanceof ErrNotLeader && err.leaderAddr) {
|
|
182
|
+
this.stream.close();
|
|
183
|
+
this.baseUrl = err.leaderAddr;
|
|
184
|
+
this.stream = new LifecycleStream(this.baseUrl, this.auth);
|
|
185
|
+
return this.stream.exchange(req);
|
|
186
|
+
}
|
|
187
|
+
throw err;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
close() {
|
|
191
|
+
this.stream.close();
|
|
192
|
+
}
|
|
193
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@corvohq/worker",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "TypeScript worker runtime for Corvo",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"main": "dist/index.js",
|
|
8
|
+
"types": "dist/index.d.ts",
|
|
9
|
+
"files": ["dist"],
|
|
10
|
+
"scripts": {
|
|
11
|
+
"build": "tsc -p tsconfig.json"
|
|
12
|
+
},
|
|
13
|
+
"dependencies": {
|
|
14
|
+
"@corvohq/client": "0.2.0",
|
|
15
|
+
"@connectrpc/connect": "^2.1.1",
|
|
16
|
+
"@connectrpc/connect-node": "^2.1.1",
|
|
17
|
+
"@bufbuild/protobuf": "^2.11.0"
|
|
18
|
+
},
|
|
19
|
+
"devDependencies": {
|
|
20
|
+
"typescript": "^5.6.3",
|
|
21
|
+
"@bufbuild/buf": "^1.65.0",
|
|
22
|
+
"@bufbuild/protoc-gen-es": "^2.11.0"
|
|
23
|
+
}
|
|
24
|
+
}
|