@xeonr/upload-pool-sdk 1.0.0 → 1.1.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/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/job-context.d.ts +2 -1
- package/dist/job-context.d.ts.map +1 -1
- package/dist/job-context.js +33 -1
- package/dist/job-context.js.map +1 -1
- package/dist/logger.d.ts +45 -0
- package/dist/logger.d.ts.map +1 -0
- package/dist/logger.js +132 -0
- package/dist/logger.js.map +1 -0
- package/dist/pool.d.ts +1 -0
- package/dist/pool.d.ts.map +1 -1
- package/dist/pool.js +60 -13
- package/dist/pool.js.map +1 -1
- package/dist/rpc-clients.d.ts +2 -1
- package/dist/rpc-clients.d.ts.map +1 -1
- package/dist/rpc-clients.js +34 -2
- package/dist/rpc-clients.js.map +1 -1
- package/dist/sse-client.d.ts +4 -0
- package/dist/sse-client.d.ts.map +1 -1
- package/dist/sse-client.js +58 -4
- package/dist/sse-client.js.map +1 -1
- package/dist/types.d.ts +14 -1
- package/dist/types.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/index.ts +2 -0
- package/src/job-context.ts +34 -0
- package/src/logger.ts +159 -0
- package/src/pool.ts +63 -11
- package/src/rpc-clients.ts +45 -2
- package/src/sse-client.ts +68 -4
- package/src/types.ts +14 -1
package/src/rpc-clients.ts
CHANGED
|
@@ -6,21 +6,64 @@
|
|
|
6
6
|
* as the in-band `updateToken` field on each request.
|
|
7
7
|
* - IntegrationQueueService — accept / complete / report-error.
|
|
8
8
|
* Auth: the pool token, passed as `queueToken` on each request.
|
|
9
|
+
*
|
|
10
|
+
* Each request is wrapped in a logging interceptor that emits one
|
|
11
|
+
* `rpc.request` line on dispatch and one of `rpc.response` /
|
|
12
|
+
* `rpc.error` on completion. Latency and connect-error codes are
|
|
13
|
+
* captured so failed RPCs are diagnosable from worker logs alone.
|
|
9
14
|
*/
|
|
10
15
|
import { createConnectTransport } from "@connectrpc/connect-node";
|
|
11
|
-
import {
|
|
16
|
+
import {
|
|
17
|
+
createClient,
|
|
18
|
+
type Client,
|
|
19
|
+
type Interceptor,
|
|
20
|
+
ConnectError,
|
|
21
|
+
Code,
|
|
22
|
+
} from "@connectrpc/connect";
|
|
12
23
|
import { InternalUploadsService } from "./protocol/uplim/api/v1/uploads_pb.js";
|
|
13
24
|
import { IntegrationQueueService } from "./protocol/uplim/workflow/v1/integration_queue_pb.js";
|
|
25
|
+
import type { Logger } from "./logger.js";
|
|
14
26
|
|
|
15
27
|
export interface RpcClients {
|
|
16
28
|
internalUploads: Client<typeof InternalUploadsService>;
|
|
17
29
|
integrationQueue: Client<typeof IntegrationQueueService>;
|
|
18
30
|
}
|
|
19
31
|
|
|
20
|
-
|
|
32
|
+
function loggingInterceptor(logger: Logger): Interceptor {
|
|
33
|
+
return (next) => async (req) => {
|
|
34
|
+
const startedAt = Date.now();
|
|
35
|
+
const method = `${req.service.typeName}/${req.method.name}`;
|
|
36
|
+
logger.debug("rpc.request", { method });
|
|
37
|
+
try {
|
|
38
|
+
const res = await next(req);
|
|
39
|
+
logger.debug("rpc.response", {
|
|
40
|
+
method,
|
|
41
|
+
durationMs: Date.now() - startedAt,
|
|
42
|
+
});
|
|
43
|
+
return res;
|
|
44
|
+
} catch (err) {
|
|
45
|
+
const code =
|
|
46
|
+
err instanceof ConnectError ? Code[err.code] : undefined;
|
|
47
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
48
|
+
logger.error("rpc.error", {
|
|
49
|
+
method,
|
|
50
|
+
code,
|
|
51
|
+
message,
|
|
52
|
+
durationMs: Date.now() - startedAt,
|
|
53
|
+
});
|
|
54
|
+
throw err;
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function createRpcClients(
|
|
60
|
+
endpoint: string,
|
|
61
|
+
logger: Logger,
|
|
62
|
+
): RpcClients {
|
|
21
63
|
const transport = createConnectTransport({
|
|
22
64
|
baseUrl: endpoint,
|
|
23
65
|
httpVersion: "1.1",
|
|
66
|
+
interceptors: [loggingInterceptor(logger.child({ component: "rpc" }))],
|
|
24
67
|
});
|
|
25
68
|
return {
|
|
26
69
|
internalUploads: createClient(InternalUploadsService, transport),
|
package/src/sse-client.ts
CHANGED
|
@@ -5,14 +5,21 @@
|
|
|
5
5
|
*
|
|
6
6
|
* Reconnect strategy: exponential backoff (1s, 2s, 4s, 8s, capped at 30s).
|
|
7
7
|
* On `job:dispatch` events the SDK invokes the registered onJob callback.
|
|
8
|
+
*
|
|
9
|
+
* All lifecycle transitions emit structured logs via the injected
|
|
10
|
+
* `Logger` so connection failures are visible in the worker's k8s logs
|
|
11
|
+
* (otherwise a worker that can't connect at all looks identical to one
|
|
12
|
+
* that's idle).
|
|
8
13
|
*/
|
|
9
14
|
import { EventSource } from "eventsource";
|
|
15
|
+
import type { Logger } from "./logger.js";
|
|
10
16
|
|
|
11
17
|
export interface SseClientConfig {
|
|
12
18
|
endpoint: string;
|
|
13
19
|
token: string;
|
|
14
20
|
workerId: string;
|
|
15
21
|
capabilities: string[];
|
|
22
|
+
logger: Logger;
|
|
16
23
|
onConnected: () => void;
|
|
17
24
|
onDisconnected: (reason: string) => void;
|
|
18
25
|
onJobDispatch: (payload: unknown) => void;
|
|
@@ -25,6 +32,8 @@ export class SseClient {
|
|
|
25
32
|
private es: EventSource | null = null;
|
|
26
33
|
private stopped = false;
|
|
27
34
|
private reconnectDelay = RECONNECT_INITIAL_MS;
|
|
35
|
+
private connectAttempt = 0;
|
|
36
|
+
private connectedAt: number | null = null;
|
|
28
37
|
|
|
29
38
|
constructor(private readonly config: SseClientConfig) {}
|
|
30
39
|
|
|
@@ -39,11 +48,13 @@ export class SseClient {
|
|
|
39
48
|
this.es.close();
|
|
40
49
|
this.es = null;
|
|
41
50
|
}
|
|
51
|
+
this.config.logger.info("sse.stopped");
|
|
42
52
|
}
|
|
43
53
|
|
|
44
54
|
private connect(): void {
|
|
45
55
|
if (this.stopped) return;
|
|
46
56
|
|
|
57
|
+
this.connectAttempt++;
|
|
47
58
|
const url = new URL(`${this.config.endpoint}/queue/connect`);
|
|
48
59
|
url.searchParams.set("queueToken", this.config.token);
|
|
49
60
|
url.searchParams.set("workerId", this.config.workerId);
|
|
@@ -51,31 +62,84 @@ export class SseClient {
|
|
|
51
62
|
url.searchParams.set("capabilities", this.config.capabilities.join(","));
|
|
52
63
|
}
|
|
53
64
|
|
|
65
|
+
// Strip the token from the logged URL — the queueToken param is the
|
|
66
|
+
// pool secret, no reason to bake it into telemetry.
|
|
67
|
+
const loggedUrl = new URL(url.toString());
|
|
68
|
+
loggedUrl.searchParams.set("queueToken", "[redacted]");
|
|
69
|
+
|
|
70
|
+
this.config.logger.info("sse.connect.start", {
|
|
71
|
+
url: loggedUrl.toString(),
|
|
72
|
+
attempt: this.connectAttempt,
|
|
73
|
+
capabilities: this.config.capabilities,
|
|
74
|
+
});
|
|
75
|
+
|
|
54
76
|
this.es = new EventSource(url.toString());
|
|
55
77
|
|
|
78
|
+
this.es.addEventListener("open", () => {
|
|
79
|
+
this.config.logger.debug("sse.open");
|
|
80
|
+
});
|
|
81
|
+
|
|
56
82
|
this.es.addEventListener("connected", () => {
|
|
57
83
|
this.reconnectDelay = RECONNECT_INITIAL_MS;
|
|
84
|
+
this.connectedAt = Date.now();
|
|
85
|
+
this.config.logger.info("sse.connect.opened", {
|
|
86
|
+
attempt: this.connectAttempt,
|
|
87
|
+
});
|
|
58
88
|
this.config.onConnected();
|
|
59
89
|
});
|
|
60
90
|
|
|
61
91
|
this.es.addEventListener("job:dispatch", (event) => {
|
|
62
92
|
try {
|
|
63
93
|
const data = JSON.parse((event as MessageEvent).data);
|
|
94
|
+
this.config.logger.debug("sse.event.job_dispatch", {
|
|
95
|
+
jobId: (data as { jobId?: string }).jobId,
|
|
96
|
+
});
|
|
64
97
|
this.config.onJobDispatch(data);
|
|
65
98
|
} catch (err) {
|
|
66
|
-
|
|
99
|
+
const reason = `bad job payload: ${(err as Error).message}`;
|
|
100
|
+
this.config.logger.error("sse.event.parse_error", {
|
|
101
|
+
err,
|
|
102
|
+
rawData: (event as MessageEvent).data,
|
|
103
|
+
});
|
|
104
|
+
this.config.onDisconnected(reason);
|
|
67
105
|
}
|
|
68
106
|
});
|
|
69
107
|
|
|
70
|
-
// heartbeat events are no-ops
|
|
108
|
+
// heartbeat events are no-ops at the dispatch layer — receipt alone
|
|
109
|
+
// keeps the connection alive. We log at debug so noisy keep-alive
|
|
110
|
+
// traffic doesn't drown the info stream.
|
|
111
|
+
this.es.addEventListener("heartbeat", () => {
|
|
112
|
+
this.config.logger.debug("sse.event.heartbeat");
|
|
113
|
+
});
|
|
71
114
|
|
|
72
|
-
this.es.onerror = () => {
|
|
115
|
+
this.es.onerror = (err: unknown) => {
|
|
73
116
|
if (this.stopped) return;
|
|
74
|
-
|
|
117
|
+
const errMessage =
|
|
118
|
+
(err as { message?: string; type?: string; status?: number })?.message ??
|
|
119
|
+
(err as { type?: string })?.type ??
|
|
120
|
+
"unknown";
|
|
121
|
+
const status = (err as { status?: number; code?: number })?.status ??
|
|
122
|
+
(err as { code?: number })?.code;
|
|
123
|
+
const wasConnected = this.connectedAt !== null;
|
|
124
|
+
const uptimeMs = wasConnected ? Date.now() - this.connectedAt! : null;
|
|
125
|
+
|
|
126
|
+
this.config.logger.warn("sse.disconnected", {
|
|
127
|
+
message: errMessage,
|
|
128
|
+
status,
|
|
129
|
+
wasConnected,
|
|
130
|
+
uptimeMs,
|
|
131
|
+
attempt: this.connectAttempt,
|
|
132
|
+
nextReconnectMs: this.reconnectDelay,
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
this.config.onDisconnected(`sse error: ${errMessage}`);
|
|
136
|
+
this.connectedAt = null;
|
|
137
|
+
|
|
75
138
|
if (this.es) {
|
|
76
139
|
this.es.close();
|
|
77
140
|
this.es = null;
|
|
78
141
|
}
|
|
142
|
+
|
|
79
143
|
setTimeout(() => this.connect(), this.reconnectDelay);
|
|
80
144
|
this.reconnectDelay = Math.min(this.reconnectDelay * 2, RECONNECT_MAX_MS);
|
|
81
145
|
};
|
package/src/types.ts
CHANGED
|
@@ -25,8 +25,21 @@ export interface PoolConfig {
|
|
|
25
25
|
handlers: Record<string, JobHandler>;
|
|
26
26
|
/** Fallback handler if URN has no specific entry in `handlers`. Optional. */
|
|
27
27
|
onUnhandled?: JobHandler;
|
|
28
|
-
/**
|
|
28
|
+
/**
|
|
29
|
+
* Lifecycle / error hook. Receives every handler exception alongside
|
|
30
|
+
* the job context. The SDK also logs every failure via `logger` —
|
|
31
|
+
* `onError` is for hooks that need to fan out to external systems
|
|
32
|
+
* (sentry, pagerduty). Optional.
|
|
33
|
+
*/
|
|
29
34
|
onError?: (err: Error, ctx?: JobContext) => void;
|
|
35
|
+
/**
|
|
36
|
+
* Structured logger. Defaults to a JSON-line logger that writes to
|
|
37
|
+
* stdout (`info`/`debug`) and stderr (`warn`/`error`), honoring
|
|
38
|
+
* `UPL_LOG_LEVEL`. Pass `noopLogger` from this package for silence,
|
|
39
|
+
* or supply your own implementing the `Logger` interface to bridge
|
|
40
|
+
* to pino/winston.
|
|
41
|
+
*/
|
|
42
|
+
logger?: import("./logger.js").Logger;
|
|
30
43
|
/** Max concurrent in-flight jobs. Defaults to 1. */
|
|
31
44
|
concurrency?: number;
|
|
32
45
|
}
|