@xeonr/upload-pool-sdk 1.0.0 → 1.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/index.d.ts +4 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -1
- 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 +66 -14
- package/dist/pool.js.map +1 -1
- package/dist/rpc-clients.d.ts +7 -1
- package/dist/rpc-clients.d.ts.map +1 -1
- package/dist/rpc-clients.js +58 -12
- 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 +34 -3
- package/dist/types.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/index.ts +4 -1
- package/src/job-context.ts +34 -0
- package/src/logger.ts +159 -0
- package/src/pool.ts +69 -12
- package/src/rpc-clients.ts +73 -12
- package/src/sse-client.ts +68 -4
- package/src/types.ts +34 -3
package/dist/rpc-clients.js
CHANGED
|
@@ -1,24 +1,70 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* ConnectRPC clients for the two services the SDK talks to:
|
|
3
|
-
* - InternalUploadsService — per-upload callbacks
|
|
4
|
-
* RequestMetaUpload, ConfirmMetaUpload,
|
|
5
|
-
* Auth: the per-job update_token from the
|
|
6
|
-
* as the in-band `updateToken` field on each
|
|
7
|
-
*
|
|
8
|
-
*
|
|
3
|
+
* - InternalUploadsService (`apiEndpoint`) — per-upload callbacks
|
|
4
|
+
* (UpdateUpload, RequestMetaUpload, ConfirmMetaUpload,
|
|
5
|
+
* GetProcessingContext). Auth: the per-job `update_token` from the
|
|
6
|
+
* job envelope, passed as the in-band `updateToken` field on each
|
|
7
|
+
* request.
|
|
8
|
+
* - IntegrationQueueService (`pipelineEndpoint`) — accept / complete /
|
|
9
|
+
* report-error. Auth: the pool token, passed as `queueToken` on each
|
|
10
|
+
* request.
|
|
11
|
+
*
|
|
12
|
+
* The two services live on different processes (Go uploads-api vs Node
|
|
13
|
+
* pipeline-api) so we keep them on separate transports and don't try to
|
|
14
|
+
* share a baseUrl.
|
|
15
|
+
*
|
|
16
|
+
* Each request is wrapped in a logging interceptor that emits one
|
|
17
|
+
* `rpc.request` line on dispatch and one of `rpc.response` /
|
|
18
|
+
* `rpc.error` on completion. Latency and connect-error codes are
|
|
19
|
+
* captured so failed RPCs are diagnosable from worker logs alone.
|
|
9
20
|
*/
|
|
10
21
|
import { createConnectTransport } from "@connectrpc/connect-node";
|
|
11
|
-
import { createClient } from "@connectrpc/connect";
|
|
22
|
+
import { createClient, ConnectError, Code, } 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";
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
25
|
+
function loggingInterceptor(logger, target) {
|
|
26
|
+
return (next) => async (req) => {
|
|
27
|
+
const startedAt = Date.now();
|
|
28
|
+
const method = `${req.service.typeName}/${req.method.name}`;
|
|
29
|
+
logger.debug("rpc.request", { method, target });
|
|
30
|
+
try {
|
|
31
|
+
const res = await next(req);
|
|
32
|
+
logger.debug("rpc.response", {
|
|
33
|
+
method,
|
|
34
|
+
target,
|
|
35
|
+
durationMs: Date.now() - startedAt,
|
|
36
|
+
});
|
|
37
|
+
return res;
|
|
38
|
+
}
|
|
39
|
+
catch (err) {
|
|
40
|
+
const code = err instanceof ConnectError ? Code[err.code] : undefined;
|
|
41
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
42
|
+
logger.error("rpc.error", {
|
|
43
|
+
method,
|
|
44
|
+
target,
|
|
45
|
+
code,
|
|
46
|
+
message,
|
|
47
|
+
durationMs: Date.now() - startedAt,
|
|
48
|
+
});
|
|
49
|
+
throw err;
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
export function createRpcClients(config) {
|
|
54
|
+
const rpcLogger = config.logger.child({ component: "rpc" });
|
|
55
|
+
const apiTransport = createConnectTransport({
|
|
56
|
+
baseUrl: config.apiEndpoint,
|
|
57
|
+
httpVersion: "1.1",
|
|
58
|
+
interceptors: [loggingInterceptor(rpcLogger, "api")],
|
|
59
|
+
});
|
|
60
|
+
const pipelineTransport = createConnectTransport({
|
|
61
|
+
baseUrl: config.pipelineEndpoint,
|
|
17
62
|
httpVersion: "1.1",
|
|
63
|
+
interceptors: [loggingInterceptor(rpcLogger, "pipeline")],
|
|
18
64
|
});
|
|
19
65
|
return {
|
|
20
|
-
internalUploads: createClient(InternalUploadsService,
|
|
21
|
-
integrationQueue: createClient(IntegrationQueueService,
|
|
66
|
+
internalUploads: createClient(InternalUploadsService, apiTransport),
|
|
67
|
+
integrationQueue: createClient(IntegrationQueueService, pipelineTransport),
|
|
22
68
|
};
|
|
23
69
|
}
|
|
24
70
|
//# sourceMappingURL=rpc-clients.js.map
|
package/dist/rpc-clients.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"rpc-clients.js","sourceRoot":"","sources":["../src/rpc-clients.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"rpc-clients.js","sourceRoot":"","sources":["../src/rpc-clients.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AACH,OAAO,EAAE,sBAAsB,EAAE,MAAM,0BAA0B,CAAC;AAClE,OAAO,EACN,YAAY,EAGZ,YAAY,EACZ,IAAI,GACJ,MAAM,qBAAqB,CAAC;AAC7B,OAAO,EAAE,sBAAsB,EAAE,MAAM,uCAAuC,CAAC;AAC/E,OAAO,EAAE,uBAAuB,EAAE,MAAM,sDAAsD,CAAC;AAQ/F,SAAS,kBAAkB,CAAC,MAAc,EAAE,MAAc;IACzD,OAAO,CAAC,IAAI,EAAE,EAAE,CAAC,KAAK,EAAE,GAAG,EAAE,EAAE;QAC9B,MAAM,SAAS,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;QAC7B,MAAM,MAAM,GAAG,GAAG,GAAG,CAAC,OAAO,CAAC,QAAQ,IAAI,GAAG,CAAC,MAAM,CAAC,IAAI,EAAE,CAAC;QAC5D,MAAM,CAAC,KAAK,CAAC,aAAa,EAAE,EAAE,MAAM,EAAE,MAAM,EAAE,CAAC,CAAC;QAChD,IAAI,CAAC;YACJ,MAAM,GAAG,GAAG,MAAM,IAAI,CAAC,GAAG,CAAC,CAAC;YAC5B,MAAM,CAAC,KAAK,CAAC,cAAc,EAAE;gBAC5B,MAAM;gBACN,MAAM;gBACN,UAAU,EAAE,IAAI,CAAC,GAAG,EAAE,GAAG,SAAS;aAClC,CAAC,CAAC;YACH,OAAO,GAAG,CAAC;QACZ,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACd,MAAM,IAAI,GACT,GAAG,YAAY,YAAY,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC;YAC1D,MAAM,OAAO,GAAG,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;YACjE,MAAM,CAAC,KAAK,CAAC,WAAW,EAAE;gBACzB,MAAM;gBACN,MAAM;gBACN,IAAI;gBACJ,OAAO;gBACP,UAAU,EAAE,IAAI,CAAC,GAAG,EAAE,GAAG,SAAS;aAClC,CAAC,CAAC;YACH,MAAM,GAAG,CAAC;QACX,CAAC;IACF,CAAC,CAAC;AACH,CAAC;AAQD,MAAM,UAAU,gBAAgB,CAAC,MAAwB;IACxD,MAAM,SAAS,GAAG,MAAM,CAAC,MAAM,CAAC,KAAK,CAAC,EAAE,SAAS,EAAE,KAAK,EAAE,CAAC,CAAC;IAE5D,MAAM,YAAY,GAAG,sBAAsB,CAAC;QAC3C,OAAO,EAAE,MAAM,CAAC,WAAW;QAC3B,WAAW,EAAE,KAAK;QAClB,YAAY,EAAE,CAAC,kBAAkB,CAAC,SAAS,EAAE,KAAK,CAAC,CAAC;KACpD,CAAC,CAAC;IACH,MAAM,iBAAiB,GAAG,sBAAsB,CAAC;QAChD,OAAO,EAAE,MAAM,CAAC,gBAAgB;QAChC,WAAW,EAAE,KAAK;QAClB,YAAY,EAAE,CAAC,kBAAkB,CAAC,SAAS,EAAE,UAAU,CAAC,CAAC;KACzD,CAAC,CAAC;IACH,OAAO;QACN,eAAe,EAAE,YAAY,CAAC,sBAAsB,EAAE,YAAY,CAAC;QACnE,gBAAgB,EAAE,YAAY,CAAC,uBAAuB,EAAE,iBAAiB,CAAC;KAC1E,CAAC;AACH,CAAC"}
|
package/dist/sse-client.d.ts
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
|
+
import type { Logger } from "./logger.js";
|
|
1
2
|
export interface SseClientConfig {
|
|
2
3
|
endpoint: string;
|
|
3
4
|
token: string;
|
|
4
5
|
workerId: string;
|
|
5
6
|
capabilities: string[];
|
|
7
|
+
logger: Logger;
|
|
6
8
|
onConnected: () => void;
|
|
7
9
|
onDisconnected: (reason: string) => void;
|
|
8
10
|
onJobDispatch: (payload: unknown) => void;
|
|
@@ -12,6 +14,8 @@ export declare class SseClient {
|
|
|
12
14
|
private es;
|
|
13
15
|
private stopped;
|
|
14
16
|
private reconnectDelay;
|
|
17
|
+
private connectAttempt;
|
|
18
|
+
private connectedAt;
|
|
15
19
|
constructor(config: SseClientConfig);
|
|
16
20
|
start(): void;
|
|
17
21
|
stop(): void;
|
package/dist/sse-client.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sse-client.d.ts","sourceRoot":"","sources":["../src/sse-client.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"sse-client.d.ts","sourceRoot":"","sources":["../src/sse-client.ts"],"names":[],"mappings":"AAcA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAE1C,MAAM,WAAW,eAAe;IAC/B,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,MAAM,CAAC;IACjB,YAAY,EAAE,MAAM,EAAE,CAAC;IACvB,MAAM,EAAE,MAAM,CAAC;IACf,WAAW,EAAE,MAAM,IAAI,CAAC;IACxB,cAAc,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,IAAI,CAAC;IACzC,aAAa,EAAE,CAAC,OAAO,EAAE,OAAO,KAAK,IAAI,CAAC;CAC1C;AAKD,qBAAa,SAAS;IAOT,OAAO,CAAC,QAAQ,CAAC,MAAM;IANnC,OAAO,CAAC,EAAE,CAA4B;IACtC,OAAO,CAAC,OAAO,CAAS;IACxB,OAAO,CAAC,cAAc,CAAwB;IAC9C,OAAO,CAAC,cAAc,CAAK;IAC3B,OAAO,CAAC,WAAW,CAAuB;gBAEb,MAAM,EAAE,eAAe;IAEpD,KAAK,IAAI,IAAI;IAKb,IAAI,IAAI,IAAI;IASZ,OAAO,CAAC,OAAO;CA6Ff"}
|
package/dist/sse-client.js
CHANGED
|
@@ -5,6 +5,11 @@
|
|
|
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";
|
|
10
15
|
const RECONNECT_INITIAL_MS = 1_000;
|
|
@@ -14,6 +19,8 @@ export class SseClient {
|
|
|
14
19
|
es = null;
|
|
15
20
|
stopped = false;
|
|
16
21
|
reconnectDelay = RECONNECT_INITIAL_MS;
|
|
22
|
+
connectAttempt = 0;
|
|
23
|
+
connectedAt = null;
|
|
17
24
|
constructor(config) {
|
|
18
25
|
this.config = config;
|
|
19
26
|
}
|
|
@@ -27,35 +34,82 @@ export class SseClient {
|
|
|
27
34
|
this.es.close();
|
|
28
35
|
this.es = null;
|
|
29
36
|
}
|
|
37
|
+
this.config.logger.info("sse.stopped");
|
|
30
38
|
}
|
|
31
39
|
connect() {
|
|
32
40
|
if (this.stopped)
|
|
33
41
|
return;
|
|
42
|
+
this.connectAttempt++;
|
|
34
43
|
const url = new URL(`${this.config.endpoint}/queue/connect`);
|
|
35
44
|
url.searchParams.set("queueToken", this.config.token);
|
|
36
45
|
url.searchParams.set("workerId", this.config.workerId);
|
|
37
46
|
if (this.config.capabilities.length > 0) {
|
|
38
47
|
url.searchParams.set("capabilities", this.config.capabilities.join(","));
|
|
39
48
|
}
|
|
49
|
+
// Strip the token from the logged URL — the queueToken param is the
|
|
50
|
+
// pool secret, no reason to bake it into telemetry.
|
|
51
|
+
const loggedUrl = new URL(url.toString());
|
|
52
|
+
loggedUrl.searchParams.set("queueToken", "[redacted]");
|
|
53
|
+
this.config.logger.info("sse.connect.start", {
|
|
54
|
+
url: loggedUrl.toString(),
|
|
55
|
+
attempt: this.connectAttempt,
|
|
56
|
+
capabilities: this.config.capabilities,
|
|
57
|
+
});
|
|
40
58
|
this.es = new EventSource(url.toString());
|
|
59
|
+
this.es.addEventListener("open", () => {
|
|
60
|
+
this.config.logger.debug("sse.open");
|
|
61
|
+
});
|
|
41
62
|
this.es.addEventListener("connected", () => {
|
|
42
63
|
this.reconnectDelay = RECONNECT_INITIAL_MS;
|
|
64
|
+
this.connectedAt = Date.now();
|
|
65
|
+
this.config.logger.info("sse.connect.opened", {
|
|
66
|
+
attempt: this.connectAttempt,
|
|
67
|
+
});
|
|
43
68
|
this.config.onConnected();
|
|
44
69
|
});
|
|
45
70
|
this.es.addEventListener("job:dispatch", (event) => {
|
|
46
71
|
try {
|
|
47
72
|
const data = JSON.parse(event.data);
|
|
73
|
+
this.config.logger.debug("sse.event.job_dispatch", {
|
|
74
|
+
jobId: data.jobId,
|
|
75
|
+
});
|
|
48
76
|
this.config.onJobDispatch(data);
|
|
49
77
|
}
|
|
50
78
|
catch (err) {
|
|
51
|
-
|
|
79
|
+
const reason = `bad job payload: ${err.message}`;
|
|
80
|
+
this.config.logger.error("sse.event.parse_error", {
|
|
81
|
+
err,
|
|
82
|
+
rawData: event.data,
|
|
83
|
+
});
|
|
84
|
+
this.config.onDisconnected(reason);
|
|
52
85
|
}
|
|
53
86
|
});
|
|
54
|
-
// heartbeat events are no-ops
|
|
55
|
-
|
|
87
|
+
// heartbeat events are no-ops at the dispatch layer — receipt alone
|
|
88
|
+
// keeps the connection alive. We log at debug so noisy keep-alive
|
|
89
|
+
// traffic doesn't drown the info stream.
|
|
90
|
+
this.es.addEventListener("heartbeat", () => {
|
|
91
|
+
this.config.logger.debug("sse.event.heartbeat");
|
|
92
|
+
});
|
|
93
|
+
this.es.onerror = (err) => {
|
|
56
94
|
if (this.stopped)
|
|
57
95
|
return;
|
|
58
|
-
|
|
96
|
+
const errMessage = err?.message ??
|
|
97
|
+
err?.type ??
|
|
98
|
+
"unknown";
|
|
99
|
+
const status = err?.status ??
|
|
100
|
+
err?.code;
|
|
101
|
+
const wasConnected = this.connectedAt !== null;
|
|
102
|
+
const uptimeMs = wasConnected ? Date.now() - this.connectedAt : null;
|
|
103
|
+
this.config.logger.warn("sse.disconnected", {
|
|
104
|
+
message: errMessage,
|
|
105
|
+
status,
|
|
106
|
+
wasConnected,
|
|
107
|
+
uptimeMs,
|
|
108
|
+
attempt: this.connectAttempt,
|
|
109
|
+
nextReconnectMs: this.reconnectDelay,
|
|
110
|
+
});
|
|
111
|
+
this.config.onDisconnected(`sse error: ${errMessage}`);
|
|
112
|
+
this.connectedAt = null;
|
|
59
113
|
if (this.es) {
|
|
60
114
|
this.es.close();
|
|
61
115
|
this.es = null;
|
package/dist/sse-client.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sse-client.js","sourceRoot":"","sources":["../src/sse-client.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"sse-client.js","sourceRoot":"","sources":["../src/sse-client.ts"],"names":[],"mappings":"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"}
|
package/dist/types.d.ts
CHANGED
|
@@ -16,16 +16,47 @@
|
|
|
16
16
|
export interface PoolConfig {
|
|
17
17
|
/** Pool token, prefixed with "tpq_". From the pool's creation response. */
|
|
18
18
|
token: string;
|
|
19
|
-
/**
|
|
20
|
-
|
|
19
|
+
/**
|
|
20
|
+
* Public uploads-api base URL, e.g. "https://uploads-api.xeonr.dev".
|
|
21
|
+
* Hosts `InternalUploadsService` (RequestMetaUpload, ConfirmMetaUpload,
|
|
22
|
+
* UpdateUpload, GetProcessingContext).
|
|
23
|
+
*
|
|
24
|
+
* Use the public https URL even when running inside the same k8s cluster
|
|
25
|
+
* as the API — keeps third-party workers and first-party workers on a
|
|
26
|
+
* single code path (TLS, normal ingress, no service-mesh assumptions).
|
|
27
|
+
*/
|
|
28
|
+
apiEndpoint: string;
|
|
29
|
+
/**
|
|
30
|
+
* Public pipeline-api base URL, e.g. "https://uploads-pipeline-api.xeonr.dev".
|
|
31
|
+
* Hosts the SSE `/queue/connect` endpoint and `IntegrationQueueService`
|
|
32
|
+
* (AcceptJob, CompleteJob, ReportError).
|
|
33
|
+
*
|
|
34
|
+
* Distinct from `apiEndpoint` because the pipeline runs as a separate
|
|
35
|
+
* Node service; conflating the two would silently route worker traffic
|
|
36
|
+
* to the wrong process.
|
|
37
|
+
*/
|
|
38
|
+
pipelineEndpoint: string;
|
|
21
39
|
/** Optional worker identifier. Defaults to hostname + random suffix. */
|
|
22
40
|
workerId?: string;
|
|
23
41
|
/** Handlers keyed by content type URN (e.g. "default:image"). */
|
|
24
42
|
handlers: Record<string, JobHandler>;
|
|
25
43
|
/** Fallback handler if URN has no specific entry in `handlers`. Optional. */
|
|
26
44
|
onUnhandled?: JobHandler;
|
|
27
|
-
/**
|
|
45
|
+
/**
|
|
46
|
+
* Lifecycle / error hook. Receives every handler exception alongside
|
|
47
|
+
* the job context. The SDK also logs every failure via `logger` —
|
|
48
|
+
* `onError` is for hooks that need to fan out to external systems
|
|
49
|
+
* (sentry, pagerduty). Optional.
|
|
50
|
+
*/
|
|
28
51
|
onError?: (err: Error, ctx?: JobContext) => void;
|
|
52
|
+
/**
|
|
53
|
+
* Structured logger. Defaults to a JSON-line logger that writes to
|
|
54
|
+
* stdout (`info`/`debug`) and stderr (`warn`/`error`), honoring
|
|
55
|
+
* `UPL_LOG_LEVEL`. Pass `noopLogger` from this package for silence,
|
|
56
|
+
* or supply your own implementing the `Logger` interface to bridge
|
|
57
|
+
* to pino/winston.
|
|
58
|
+
*/
|
|
59
|
+
logger?: import("./logger.js").Logger;
|
|
29
60
|
/** Max concurrent in-flight jobs. Defaults to 1. */
|
|
30
61
|
concurrency?: number;
|
|
31
62
|
}
|
package/dist/types.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,MAAM,WAAW,UAAU;IAC1B,2EAA2E;IAC3E,KAAK,EAAE,MAAM,CAAC;IACd,
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xeonr/upload-pool-sdk",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.2.0",
|
|
4
4
|
"description": "Self-hosted worker SDK for the upl.im content type pipeline. Implement a handler per content type URN; the SDK handles SSE connection, job acceptance, presigned thumbnail upload, and metadata callbacks.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
package/src/index.ts
CHANGED
|
@@ -8,7 +8,8 @@
|
|
|
8
8
|
*
|
|
9
9
|
* await createPool({
|
|
10
10
|
* token: process.env.UPL_POOL_TOKEN!,
|
|
11
|
-
*
|
|
11
|
+
* apiEndpoint: process.env.UPL_API_URL!,
|
|
12
|
+
* pipelineEndpoint: process.env.UPL_PIPELINE_API_URL!,
|
|
12
13
|
* handlers: {
|
|
13
14
|
* "my-integration:invoice": async (ctx) => {
|
|
14
15
|
* const buf = await ctx.downloadBuffer();
|
|
@@ -23,6 +24,8 @@ import type { PoolConfig } from "./types.js";
|
|
|
23
24
|
|
|
24
25
|
export { Pool } from "./pool.js";
|
|
25
26
|
export { NonRetryableError } from "./errors.js";
|
|
27
|
+
export { JsonLogger, noopLogger } from "./logger.js";
|
|
28
|
+
export type { Logger, LogLevel } from "./logger.js";
|
|
26
29
|
export type {
|
|
27
30
|
PoolConfig,
|
|
28
31
|
JobContext,
|
package/src/job-context.ts
CHANGED
|
@@ -17,6 +17,7 @@ import {
|
|
|
17
17
|
} from "./protocol/uplim/api/v1/uploads_pb.js";
|
|
18
18
|
import { UploadMetadataSchema } from "./protocol/uplim/api/v1/metadata_pb.js";
|
|
19
19
|
import type { RpcClients } from "./rpc-clients.js";
|
|
20
|
+
import type { Logger } from "./logger.js";
|
|
20
21
|
import type {
|
|
21
22
|
FolderContext,
|
|
22
23
|
JobContext,
|
|
@@ -54,6 +55,7 @@ export interface JobEnvelope {
|
|
|
54
55
|
export function createJobContext(
|
|
55
56
|
envelope: JobEnvelope,
|
|
56
57
|
rpc: RpcClients,
|
|
58
|
+
logger: Logger,
|
|
57
59
|
): JobContext {
|
|
58
60
|
const ctx: JobContext = {
|
|
59
61
|
jobId: envelope.jobId,
|
|
@@ -71,28 +73,45 @@ export function createJobContext(
|
|
|
71
73
|
sourceUrl: envelope.sourceUrl,
|
|
72
74
|
|
|
73
75
|
async download() {
|
|
76
|
+
logger.debug("source.download.start");
|
|
74
77
|
const resp = await fetch(envelope.sourceUrl);
|
|
75
78
|
if (!resp.ok || !resp.body) {
|
|
79
|
+
logger.error("source.download.failed", {
|
|
80
|
+
status: resp.status,
|
|
81
|
+
statusText: resp.statusText,
|
|
82
|
+
});
|
|
76
83
|
throw new Error(`source download failed: ${resp.status} ${resp.statusText}`);
|
|
77
84
|
}
|
|
78
85
|
return resp.body;
|
|
79
86
|
},
|
|
80
87
|
|
|
81
88
|
async downloadBuffer() {
|
|
89
|
+
logger.debug("source.download.start", { mode: "buffer" });
|
|
82
90
|
const resp = await fetch(envelope.sourceUrl);
|
|
83
91
|
if (!resp.ok) {
|
|
92
|
+
logger.error("source.download.failed", {
|
|
93
|
+
status: resp.status,
|
|
94
|
+
statusText: resp.statusText,
|
|
95
|
+
});
|
|
84
96
|
throw new Error(`source download failed: ${resp.status} ${resp.statusText}`);
|
|
85
97
|
}
|
|
86
98
|
const buf = await resp.arrayBuffer();
|
|
99
|
+
logger.debug("source.download.complete", { sizeBytes: buf.byteLength });
|
|
87
100
|
return new Uint8Array(buf);
|
|
88
101
|
},
|
|
89
102
|
|
|
90
103
|
async downloadToFile(path: string) {
|
|
91
104
|
const data = await ctx.downloadBuffer();
|
|
92
105
|
await writeFile(path, data);
|
|
106
|
+
logger.debug("source.download.to_file", { path, sizeBytes: data.byteLength });
|
|
93
107
|
},
|
|
94
108
|
|
|
95
109
|
async uploadMeta(opts: UploadMetaOpts) {
|
|
110
|
+
logger.debug("meta.upload.start", {
|
|
111
|
+
type: opts.type,
|
|
112
|
+
instance: opts.instance,
|
|
113
|
+
filename: opts.filename,
|
|
114
|
+
});
|
|
96
115
|
const protoType = META_TYPE_MAP[opts.type];
|
|
97
116
|
const metaType = create(MetaTypeSchema, {
|
|
98
117
|
type: protoType,
|
|
@@ -135,6 +154,10 @@ export function createJobContext(
|
|
|
135
154
|
metadataId: [meta.metadataId],
|
|
136
155
|
}),
|
|
137
156
|
);
|
|
157
|
+
logger.info("meta.upload.complete", {
|
|
158
|
+
type: opts.type,
|
|
159
|
+
metadataId: meta.metadataId,
|
|
160
|
+
});
|
|
138
161
|
},
|
|
139
162
|
|
|
140
163
|
async setMetadata(metadata: UploadMetadataPartial | UploadMetadataProto) {
|
|
@@ -163,6 +186,7 @@ export function createJobContext(
|
|
|
163
186
|
thumbnailGenerationVersion: thumbnailVersion,
|
|
164
187
|
}),
|
|
165
188
|
);
|
|
189
|
+
logger.debug("metadata.set", { thumbnailVersion });
|
|
166
190
|
},
|
|
167
191
|
|
|
168
192
|
async markHasThumbnail(version: number) {
|
|
@@ -173,6 +197,7 @@ export function createJobContext(
|
|
|
173
197
|
thumbnailGenerationVersion: version,
|
|
174
198
|
}),
|
|
175
199
|
);
|
|
200
|
+
logger.info("thumbnail.marked", { version });
|
|
176
201
|
},
|
|
177
202
|
|
|
178
203
|
async setDescription(text: string, tags?: string[]) {
|
|
@@ -185,12 +210,20 @@ export function createJobContext(
|
|
|
185
210
|
}),
|
|
186
211
|
}),
|
|
187
212
|
);
|
|
213
|
+
logger.info("description.set", {
|
|
214
|
+
length: text.length,
|
|
215
|
+
tags: tags?.length ?? 0,
|
|
216
|
+
});
|
|
188
217
|
},
|
|
189
218
|
|
|
190
219
|
async withPresignedUploads<T>(
|
|
191
220
|
opts: PresignedUploadRequest[],
|
|
192
221
|
callback: (handles: import("./types.js").MetaUploadHandle[]) => Promise<T>,
|
|
193
222
|
): Promise<T> {
|
|
223
|
+
logger.debug("presigned.upload.request", {
|
|
224
|
+
count: opts.length,
|
|
225
|
+
types: opts.map((o) => o.type),
|
|
226
|
+
});
|
|
194
227
|
const metaTypes = opts.map((opt) => create(MetaTypeSchema, {
|
|
195
228
|
type: META_TYPE_MAP[opt.type],
|
|
196
229
|
n: opt.instance,
|
|
@@ -222,6 +255,7 @@ export function createJobContext(
|
|
|
222
255
|
metadataId: requested.metaUploads.map((m) => m.metadataId),
|
|
223
256
|
}),
|
|
224
257
|
);
|
|
258
|
+
logger.info("presigned.upload.complete", { count: handles.length });
|
|
225
259
|
return result;
|
|
226
260
|
},
|
|
227
261
|
};
|
package/src/logger.ts
ADDED
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Logging interface for the SDK. Customers can plug in their own
|
|
3
|
+
* logger (pino, winston, console) by passing `logger` to createPool;
|
|
4
|
+
* by default we emit structured JSON lines to stdout/stderr so logs
|
|
5
|
+
* are grep-friendly in k8s.
|
|
6
|
+
*
|
|
7
|
+
* The shape mirrors the pipeline-api's logger contract for consistency
|
|
8
|
+
* across the upl.im stack.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
export type LogLevel = "debug" | "info" | "warn" | "error";
|
|
12
|
+
|
|
13
|
+
export interface Logger {
|
|
14
|
+
debug(event: string, context?: Record<string, unknown>): void;
|
|
15
|
+
info(event: string, context?: Record<string, unknown>): void;
|
|
16
|
+
warn(event: string, context?: Record<string, unknown>): void;
|
|
17
|
+
error(event: string, context?: Record<string, unknown>): void;
|
|
18
|
+
/**
|
|
19
|
+
* Return a derived logger that merges these context fields into every
|
|
20
|
+
* subsequent log call. Used by Pool.handleDispatch to attach jobId /
|
|
21
|
+
* uploadId / urn to every line emitted during the job.
|
|
22
|
+
*/
|
|
23
|
+
child(context: Record<string, unknown>): Logger;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const SENSITIVE_KEYS = new Set([
|
|
27
|
+
"token",
|
|
28
|
+
"queueToken",
|
|
29
|
+
"queue_token",
|
|
30
|
+
"workerToken",
|
|
31
|
+
"worker_token",
|
|
32
|
+
"updateToken",
|
|
33
|
+
"update_token",
|
|
34
|
+
"authorization",
|
|
35
|
+
"Authorization",
|
|
36
|
+
]);
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Redact obvious secrets so a careless `logger.info("rpc.request", { headers })`
|
|
40
|
+
* doesn't leak the pool token to log aggregators. Best-effort; not a
|
|
41
|
+
* substitute for thinking about what you're logging.
|
|
42
|
+
*/
|
|
43
|
+
function redact(value: unknown): unknown {
|
|
44
|
+
if (value && typeof value === "object" && !Array.isArray(value)) {
|
|
45
|
+
const out: Record<string, unknown> = {};
|
|
46
|
+
for (const [k, v] of Object.entries(value as Record<string, unknown>)) {
|
|
47
|
+
if (SENSITIVE_KEYS.has(k)) {
|
|
48
|
+
out[k] = typeof v === "string" && v.length > 8
|
|
49
|
+
? `${v.slice(0, 4)}…(${v.length - 4} redacted)`
|
|
50
|
+
: "[redacted]";
|
|
51
|
+
} else if (v && typeof v === "object") {
|
|
52
|
+
out[k] = redact(v);
|
|
53
|
+
} else {
|
|
54
|
+
out[k] = v;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return out;
|
|
58
|
+
}
|
|
59
|
+
if (Array.isArray(value)) return value.map(redact);
|
|
60
|
+
return value;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function serializeError(err: unknown): Record<string, unknown> {
|
|
64
|
+
if (err instanceof Error) {
|
|
65
|
+
return {
|
|
66
|
+
name: err.name,
|
|
67
|
+
message: err.message,
|
|
68
|
+
stack: err.stack,
|
|
69
|
+
...(err as unknown as Record<string, unknown>),
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
return { value: String(err) };
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Default JSON logger. Emits one line per call. `error` and `warn` go to
|
|
77
|
+
* stderr; `info` and `debug` go to stdout. Honors `UPL_LOG_LEVEL` env var
|
|
78
|
+
* (`debug` | `info` | `warn` | `error`) to filter out noise in prod.
|
|
79
|
+
*/
|
|
80
|
+
export class JsonLogger implements Logger {
|
|
81
|
+
private readonly base: Record<string, unknown>;
|
|
82
|
+
private readonly minLevel: LogLevel;
|
|
83
|
+
|
|
84
|
+
constructor(base: Record<string, unknown> = {}, minLevel?: LogLevel) {
|
|
85
|
+
this.base = base;
|
|
86
|
+
this.minLevel = minLevel ?? (process.env.UPL_LOG_LEVEL as LogLevel) ?? "info";
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
child(context: Record<string, unknown>): Logger {
|
|
90
|
+
return new JsonLogger({ ...this.base, ...context }, this.minLevel);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
debug(event: string, context?: Record<string, unknown>): void {
|
|
94
|
+
this.emit("debug", event, context);
|
|
95
|
+
}
|
|
96
|
+
info(event: string, context?: Record<string, unknown>): void {
|
|
97
|
+
this.emit("info", event, context);
|
|
98
|
+
}
|
|
99
|
+
warn(event: string, context?: Record<string, unknown>): void {
|
|
100
|
+
this.emit("warn", event, context);
|
|
101
|
+
}
|
|
102
|
+
error(event: string, context?: Record<string, unknown>): void {
|
|
103
|
+
this.emit("error", event, context);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
private shouldEmit(level: LogLevel): boolean {
|
|
107
|
+
const order: Record<LogLevel, number> = {
|
|
108
|
+
debug: 10,
|
|
109
|
+
info: 20,
|
|
110
|
+
warn: 30,
|
|
111
|
+
error: 40,
|
|
112
|
+
};
|
|
113
|
+
return order[level] >= order[this.minLevel];
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
private emit(
|
|
117
|
+
level: LogLevel,
|
|
118
|
+
event: string,
|
|
119
|
+
context?: Record<string, unknown>,
|
|
120
|
+
): void {
|
|
121
|
+
if (!this.shouldEmit(level)) return;
|
|
122
|
+
|
|
123
|
+
const merged: Record<string, unknown> = {
|
|
124
|
+
...this.base,
|
|
125
|
+
...(context ? (redact(context) as Record<string, unknown>) : {}),
|
|
126
|
+
};
|
|
127
|
+
if (merged.err !== undefined) {
|
|
128
|
+
merged.err = serializeError(merged.err);
|
|
129
|
+
}
|
|
130
|
+
if (merged.error !== undefined && merged.error instanceof Error) {
|
|
131
|
+
merged.error = serializeError(merged.error);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const line = JSON.stringify({
|
|
135
|
+
ts: new Date().toISOString(),
|
|
136
|
+
level,
|
|
137
|
+
logger: "@xeonr/upload-pool-sdk",
|
|
138
|
+
event,
|
|
139
|
+
...merged,
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
// eslint-disable-next-line no-console
|
|
143
|
+
(level === "error" || level === "warn" ? console.error : console.log)(line);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* No-op logger for tests or callers who want silence. createPool ignores
|
|
149
|
+
* this if `logger` is unset and uses JsonLogger by default.
|
|
150
|
+
*/
|
|
151
|
+
export const noopLogger: Logger = {
|
|
152
|
+
debug() {},
|
|
153
|
+
info() {},
|
|
154
|
+
warn() {},
|
|
155
|
+
error() {},
|
|
156
|
+
child() {
|
|
157
|
+
return noopLogger;
|
|
158
|
+
},
|
|
159
|
+
};
|