@webpieces/cloudtasks-client 0.4.956 → 0.4.958
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/README.md
CHANGED
|
@@ -9,7 +9,7 @@ where it runs through the full server filter chain.
|
|
|
9
9
|
// one shared contract
|
|
10
10
|
@PubSub() @ApiPath('/email')
|
|
11
11
|
abstract class EmailApi {
|
|
12
|
-
@Endpoint(POST, '/send', WRITE, CLOUDTASKS)
|
|
12
|
+
@Endpoint(POST, '/send', WRITE, CLOUDTASKS, { responseBody: NONE }) // Promise<void>: delivered as 204, 0 bytes
|
|
13
13
|
@WpAuth([oidcSameProject()])
|
|
14
14
|
@WpAuthorization({ authType: AuthorizationType.SERVICE_ONLY })
|
|
15
15
|
abstract sendEmail(r: SendEmailRequest): Promise<void>;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@webpieces/cloudtasks-client",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.958",
|
|
4
4
|
"description": "Cloud Tasks enqueue client generated from a shared @PubSub API contract (twin of http-client)",
|
|
5
5
|
"type": "commonjs",
|
|
6
6
|
"main": "./src/index.js",
|
|
@@ -23,9 +23,9 @@
|
|
|
23
23
|
"access": "public"
|
|
24
24
|
},
|
|
25
25
|
"dependencies": {
|
|
26
|
-
"@webpieces/core-context": "0.4.
|
|
27
|
-
"@webpieces/core-util": "0.4.
|
|
28
|
-
"@webpieces/gcp-identity": "0.4.
|
|
26
|
+
"@webpieces/core-context": "0.4.958",
|
|
27
|
+
"@webpieces/core-util": "0.4.958",
|
|
28
|
+
"@webpieces/gcp-identity": "0.4.958",
|
|
29
29
|
"@google-cloud/tasks": "6.3.0",
|
|
30
30
|
"google-gax": "5.0.8",
|
|
31
31
|
"inversify": "7.10.4",
|
package/src/GcpTaskRegistry.d.ts
CHANGED
|
@@ -18,8 +18,7 @@ import { TaskCustomizer } from './TaskTypes';
|
|
|
18
18
|
* invoker has no Google request and ignores both).
|
|
19
19
|
*/
|
|
20
20
|
export declare class GcpTaskRegistry {
|
|
21
|
-
private static
|
|
22
|
-
private static readonly apis;
|
|
21
|
+
private static readonly ladder;
|
|
23
22
|
static setCustomizer(customizer: TaskCustomizer | undefined, ...scope: CallScope): void;
|
|
24
23
|
static setGax(gax: CallOptions | undefined, ...scope: CallScope): void;
|
|
25
24
|
/** The registered customizer, method -> API -> ALL, or undefined. */
|
|
@@ -27,5 +26,4 @@ export declare class GcpTaskRegistry {
|
|
|
27
26
|
/** The registered gax CallOptions, method -> API -> ALL, or undefined. */
|
|
28
27
|
static gaxFor(api: Function, methodName: string): CallOptions | undefined;
|
|
29
28
|
static clear(): void;
|
|
30
|
-
private static policy;
|
|
31
29
|
}
|
package/src/GcpTaskRegistry.js
CHANGED
|
@@ -1,14 +1,11 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.GcpTaskRegistry = void 0;
|
|
4
|
+
const core_util_1 = require("@webpieces/core-util");
|
|
4
5
|
class GcpTaskPolicy {
|
|
5
6
|
customizer;
|
|
6
7
|
gax;
|
|
7
8
|
}
|
|
8
|
-
class ApiGcpTaskPolicies {
|
|
9
|
-
policy = new GcpTaskPolicy();
|
|
10
|
-
methods = new Map();
|
|
11
|
-
}
|
|
12
9
|
/**
|
|
13
10
|
* Process-wide Cloud Tasks enqueue defaults, set at startup and resolved method -> API -> ALL — the
|
|
14
11
|
* same {@link CallScope} ladder as `CallRegistry`. A call site's `ScheduleOptions` wins over both.
|
|
@@ -26,53 +23,30 @@ class ApiGcpTaskPolicies {
|
|
|
26
23
|
* invoker has no Google request and ignores both).
|
|
27
24
|
*/
|
|
28
25
|
class GcpTaskRegistry {
|
|
29
|
-
static
|
|
30
|
-
static apis = new Map();
|
|
26
|
+
static ladder = new core_util_1.CallScopeLadder(() => new GcpTaskPolicy());
|
|
31
27
|
// webpieces-disable no-function-outside-class -- process-global startup registry
|
|
32
28
|
static setCustomizer(customizer, ...scope) {
|
|
33
|
-
GcpTaskRegistry.
|
|
29
|
+
GcpTaskRegistry.ladder.at(...scope).customizer = customizer;
|
|
34
30
|
}
|
|
35
31
|
// webpieces-disable no-function-outside-class -- process-global startup registry
|
|
36
32
|
static setGax(gax, ...scope) {
|
|
37
|
-
GcpTaskRegistry.
|
|
33
|
+
GcpTaskRegistry.ladder.at(...scope).gax = gax;
|
|
38
34
|
}
|
|
39
35
|
/** The registered customizer, method -> API -> ALL, or undefined. */
|
|
40
36
|
// webpieces-disable no-function-outside-class -- process-global registry lookup
|
|
41
37
|
static customizerFor(api, methodName) {
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
GcpTaskRegistry.all.customizer);
|
|
38
|
+
return GcpTaskRegistry.ladder
|
|
39
|
+
.rungs(api, methodName)
|
|
40
|
+
.find((p) => p.customizer !== undefined)?.customizer;
|
|
46
41
|
}
|
|
47
42
|
/** The registered gax CallOptions, method -> API -> ALL, or undefined. */
|
|
48
43
|
// webpieces-disable no-function-outside-class -- process-global registry lookup
|
|
49
44
|
static gaxFor(api, methodName) {
|
|
50
|
-
|
|
51
|
-
return policies?.methods.get(methodName)?.gax ?? policies?.policy.gax ?? GcpTaskRegistry.all.gax;
|
|
45
|
+
return GcpTaskRegistry.ladder.rungs(api, methodName).find((p) => p.gax !== undefined)?.gax;
|
|
52
46
|
}
|
|
53
47
|
// webpieces-disable no-function-outside-class -- process-global registry reset for tests
|
|
54
48
|
static clear() {
|
|
55
|
-
GcpTaskRegistry.
|
|
56
|
-
GcpTaskRegistry.apis.clear();
|
|
57
|
-
}
|
|
58
|
-
// webpieces-disable no-function-outside-class -- process-global registry storage
|
|
59
|
-
static policy(api, methodName) {
|
|
60
|
-
if (api === 'ALL') {
|
|
61
|
-
return GcpTaskRegistry.all;
|
|
62
|
-
}
|
|
63
|
-
let policies = GcpTaskRegistry.apis.get(api);
|
|
64
|
-
if (!policies) {
|
|
65
|
-
policies = new ApiGcpTaskPolicies();
|
|
66
|
-
GcpTaskRegistry.apis.set(api, policies);
|
|
67
|
-
}
|
|
68
|
-
if (methodName === undefined)
|
|
69
|
-
return policies.policy;
|
|
70
|
-
let policy = policies.methods.get(methodName);
|
|
71
|
-
if (!policy) {
|
|
72
|
-
policy = new GcpTaskPolicy();
|
|
73
|
-
policies.methods.set(methodName, policy);
|
|
74
|
-
}
|
|
75
|
-
return policy;
|
|
49
|
+
GcpTaskRegistry.ladder.clear();
|
|
76
50
|
}
|
|
77
51
|
}
|
|
78
52
|
exports.GcpTaskRegistry = GcpTaskRegistry;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"GcpTaskRegistry.js","sourceRoot":"","sources":["../../../../../packages/cloud/cloudtasks-client/src/GcpTaskRegistry.ts"],"names":[],"mappings":";;;
|
|
1
|
+
{"version":3,"file":"GcpTaskRegistry.js","sourceRoot":"","sources":["../../../../../packages/cloud/cloudtasks-client/src/GcpTaskRegistry.ts"],"names":[],"mappings":";;;AACA,oDAAuD;AAIvD,MAAM,aAAa;IACf,UAAU,CAAkB;IAC5B,GAAG,CAAe;CACrB;AAED;;;;;;;;;;;;;;;GAeG;AACH,MAAa,eAAe;IAChB,MAAM,CAAU,MAAM,GAAG,IAAI,2BAAe,CAAgB,GAAG,EAAE,CAAC,IAAI,aAAa,EAAE,CAAC,CAAC;IAE/F,iFAAiF;IACjF,MAAM,CAAC,aAAa,CAAC,UAAsC,EAAE,GAAG,KAAgB;QAC5E,eAAe,CAAC,MAAM,CAAC,EAAE,CAAC,GAAG,KAAK,CAAC,CAAC,UAAU,GAAG,UAAU,CAAC;IAChE,CAAC;IAED,iFAAiF;IACjF,MAAM,CAAC,MAAM,CAAC,GAA4B,EAAE,GAAG,KAAgB;QAC3D,eAAe,CAAC,MAAM,CAAC,EAAE,CAAC,GAAG,KAAK,CAAC,CAAC,GAAG,GAAG,GAAG,CAAC;IAClD,CAAC;IAED,qEAAqE;IACrE,gFAAgF;IAChF,MAAM,CAAC,aAAa,CAAC,GAAa,EAAE,UAAkB;QAClD,OAAO,eAAe,CAAC,MAAM;aACxB,KAAK,CAAC,GAAG,EAAE,UAAU,CAAC;aACtB,IAAI,CAAC,CAAC,CAAgB,EAAE,EAAE,CAAC,CAAC,CAAC,UAAU,KAAK,SAAS,CAAC,EAAE,UAAU,CAAC;IAC5E,CAAC;IAED,0EAA0E;IAC1E,gFAAgF;IAChF,MAAM,CAAC,MAAM,CAAC,GAAa,EAAE,UAAkB;QAC3C,OAAO,eAAe,CAAC,MAAM,CAAC,KAAK,CAAC,GAAG,EAAE,UAAU,CAAC,CAAC,IAAI,CAAC,CAAC,CAAgB,EAAE,EAAE,CAAC,CAAC,CAAC,GAAG,KAAK,SAAS,CAAC,EAAE,GAAG,CAAC;IAC9G,CAAC;IAED,yFAAyF;IACzF,MAAM,CAAC,KAAK;QACR,eAAe,CAAC,MAAM,CAAC,KAAK,EAAE,CAAC;IACnC,CAAC;;AA9BL,0CA+BC","sourcesContent":["import type { CallOptions } from 'google-gax';\nimport { CallScopeLadder } from '@webpieces/core-util';\nimport type { CallScope } from '@webpieces/core-util';\nimport { TaskCustomizer } from './TaskTypes';\n\nclass GcpTaskPolicy {\n customizer?: TaskCustomizer;\n gax?: CallOptions;\n}\n\n/**\n * Process-wide Cloud Tasks enqueue defaults, set at startup and resolved method -> API -> ALL — the\n * same {@link CallScope} ladder as `CallRegistry`. A call site's `ScheduleOptions` wins over both.\n *\n * - {@link GcpTaskRegistry.setCustomizer} — a {@link TaskCustomizer} applied to the Google\n * `ICreateTaskRequest` of every matching enqueue whose call site set none. It changes the REQUEST\n * only (headers, dispatch deadline, ...); webpieces still owns timing.\n * - {@link GcpTaskRegistry.setGax} — gax `CallOptions` for matching enqueues. Setting them means\n * YOU own timing for that scope: no webpieces retry strategy runs, there is exactly one attempt,\n * `createTask(request, yourGax)` is sent as given, and `CallRegistry`'s timeout ladder (method ->\n * API -> ALL -> 30 s) is a safety cap only. Raise the cap with `CallRegistry.setTimeout(...)` to\n * give gax retries room.\n *\n * Read only by `TaskProxyClient` (the customizer is applied by `GcpTaskInvoker`; the in-memory\n * invoker has no Google request and ignores both).\n */\nexport class GcpTaskRegistry {\n private static readonly ladder = new CallScopeLadder<GcpTaskPolicy>(() => new GcpTaskPolicy());\n\n // webpieces-disable no-function-outside-class -- process-global startup registry\n static setCustomizer(customizer: TaskCustomizer | undefined, ...scope: CallScope): void {\n GcpTaskRegistry.ladder.at(...scope).customizer = customizer;\n }\n\n // webpieces-disable no-function-outside-class -- process-global startup registry\n static setGax(gax: CallOptions | undefined, ...scope: CallScope): void {\n GcpTaskRegistry.ladder.at(...scope).gax = gax;\n }\n\n /** The registered customizer, method -> API -> ALL, or undefined. */\n // webpieces-disable no-function-outside-class -- process-global registry lookup\n static customizerFor(api: Function, methodName: string): TaskCustomizer | undefined {\n return GcpTaskRegistry.ladder\n .rungs(api, methodName)\n .find((p: GcpTaskPolicy) => p.customizer !== undefined)?.customizer;\n }\n\n /** The registered gax CallOptions, method -> API -> ALL, or undefined. */\n // webpieces-disable no-function-outside-class -- process-global registry lookup\n static gaxFor(api: Function, methodName: string): CallOptions | undefined {\n return GcpTaskRegistry.ladder.rungs(api, methodName).find((p: GcpTaskPolicy) => p.gax !== undefined)?.gax;\n }\n\n // webpieces-disable no-function-outside-class -- process-global registry reset for tests\n static clear(): void {\n GcpTaskRegistry.ladder.clear();\n }\n}\n"]}
|
package/src/TaskProxyClient.js
CHANGED
|
@@ -130,9 +130,9 @@ let TaskProxyClient = class TaskProxyClient {
|
|
|
130
130
|
// the ONE seam above BOTH GcpTaskInvoker (prod) and InMemoryTaskInvoker (localhost) — whichever
|
|
131
131
|
// TaskInvoker is bound, this method sits above it — so the enqueue edge gets structured
|
|
132
132
|
// `jsonPayload.api.*` logging uniformly, without touching either invoker impl.
|
|
133
|
-
const info = new core_util_1.ApiMethodInfo('client', this.apiName, methodName, undefined, plan.mask);
|
|
133
|
+
const info = new core_util_1.ApiMethodInfo('client', this.apiName, methodName, this.apiClass, undefined, plan.mask);
|
|
134
134
|
await this.logApiCall.execute(info, requestDto, async () => {
|
|
135
|
-
const callContext = new core_util_1.CallContext(this.apiName, methodName, core_util_1.WRITE_IDEMPOTENT);
|
|
135
|
+
const callContext = new core_util_1.CallContext(this.apiName, methodName, core_util_1.WRITE_IDEMPOTENT, this.apiClass);
|
|
136
136
|
const attempt = (timeoutMs) => core_util_1.CallDeadline.run(timeoutMs, callContext, (signal) => this.attempt(plan, enqueuePlan, frame.info, requestDto, timeoutMs, signal));
|
|
137
137
|
// Only the winning attempt may publish the reference. A late enqueue cannot overwrite it.
|
|
138
138
|
frame.jobRef = await this.dedupAware(methodName, () => enqueuePlan.gax === undefined
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"TaskProxyClient.js","sourceRoot":"","sources":["../../../../../packages/cloud/cloudtasks-client/src/TaskProxyClient.ts"],"names":[],"mappings":"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","sourcesContent":["import { inject } from 'inversify';\nimport {\n isApiPath,\n getApiPath,\n getEndpoints,\n getAuthMeta,\n getQueueName,\n getMaskSpec,\n assertPubSubConventions,\n assertEveryEndpointHasAuthMode,\n assertNotInternalApi,\n AuthMode,\n DestinationTrust,\n MaskSpec,\n LogManager,\n LogApiCallImpl,\n ApiMethodInfo,\n CallRegistry,\n CallDeadline,\n CallContext,\n RetryStrategy,\n TransportDefault,\n WRITE_IDEMPOTENT,\n toError,\n} from '@webpieces/core-util';\nimport {\n RequestContextApiCallContext,\n RequestContextHeaders,\n provideFrameworkTransient,\n} from '@webpieces/core-context';\nimport { TaskInvoker, TaskRequest, ScheduleInfo, EnqueuePlan, JobReference } from './TaskTypes';\nimport { TaskAlreadyExistsError } from './TaskAlreadyExistsError';\nimport { GcpTaskRegistry } from './GcpTaskRegistry';\nimport { currentScheduleFrame } from './ScheduleContext';\nimport { ApiPrototype, TaskClientConfig } from './TaskClientConfig';\n\nconst log = LogManager.getLogger('TaskProxyClient');\n\n/** Per-endpoint routing plan resolved once from the contract's decorators. */\nclass EndpointPlan {\n path: string;\n queueName: string;\n authMode: AuthMode;\n /** @MaskLog spec for this endpoint's request DTO, read once here so the enqueue log path masks\n * a secret on the task payload without a per-enqueue reflection hit. Undefined = log verbatim. */\n mask?: MaskSpec;\n\n constructor(path: string, queueName: string, authMode: AuthMode, mask?: MaskSpec) {\n this.path = path;\n this.queueName = queueName;\n this.authMode = authMode;\n this.mask = mask;\n }\n}\n\n/**\n * TaskProxyClient - the enqueue engine behind one @PubSub API contract's client proxy.\n *\n * The fire-and-forget twin of http-client-core's ProxyClient, and TWO-PHASE for the same reason:\n * its COLLABORATORS (invoker, headers) come from the container, while the PER-CLIENT state (which\n * contract, which target) arrives on {@link init}. That is what lets {@link ClientCloudTasksFactory}\n * hold a `Provider<TaskProxyClient>` and hand out a fresh, independently-configured client per\n * contract.\n *\n * Calling an endpoint ENQUEUES a task (it does not call remotely); the task is later delivered to\n * the same endpoint's controller through the full server filter chain.\n *\n * It owns:\n * - @ApiPath / @PubSub convention validation + the endpoint plans from the contract's decorators\n * - Resolving the callee's base URL from svcName through `ClientRegistry.resolve` — the SAME chain the\n * RPC client runs: a registered mapping, else the installed deriver, else throw\n * - Context propagation onto the task headers (a credential is never a context key, so none can ride along)\n * - Handing a fully-built TaskRequest to the bound {@link TaskInvoker}\n */\n@provideFrameworkTransient()\nexport class TaskProxyClient {\n /**\n * The enqueue transport's default for `CallRegistry.execute` (Path A): 30 s if a caller only\n * registers a strategy-free timeout ladder, otherwise webpieces' shared `RetryStrategy` — 5 s per\n * attempt, 3 retries, backoff 500 ms x2 capped at 4 s. RPC passes `new TransportDefault(30_000)`\n * (no retry); this is the ONLY place the two transports differ.\n */\n static readonly ENQUEUE_TRANSPORT_DEFAULT = new TransportDefault(\n 30_000,\n new RetryStrategy(5_000, 3, 500, 2, 4_000).run,\n );\n\n /** Path B's safety cap when no timeout is registered: caller-owned gax timing still ends. */\n static readonly ENQUEUE_SAFETY_CAP_MS = 30_000;\n\n // Assigned by init(), which the factory calls immediately after construction.\n private plans!: Map<string, EndpointPlan>;\n private apiName!: string;\n private apiClass!: ApiPrototype<object>;\n private config!: TaskClientConfig;\n\n /**\n * The api-call log seam, built around this node-only package's OWN RequestContext-backed\n * ApiCallContext. Constructed here rather than installed at startup, so a plain NestJS/Express\n * host needs no webpieces STARTUP INSTALL to enqueue — it only has to run the call inside a\n * `RequestContext.run(...)` scope, which such a host already opens per request. That is the defect\n * that made every enqueue throw `ApiCallContext is not installed`.\n */\n private readonly logApiCall = new LogApiCallImpl(new RequestContextApiCallContext());\n\n constructor(\n @inject(TaskInvoker) private readonly invoker: TaskInvoker,\n @inject(RequestContextHeaders) private readonly headers: RequestContextHeaders,\n ) {}\n\n /** Bind this client to one @PubSub contract + target. */\n init(apiClass: ApiPrototype<object>, config: TaskClientConfig): void {\n assertNotInternalApi(apiClass, 'Cloud Tasks client');\n if (!isApiPath(apiClass)) {\n throw new Error(\n `Class ${apiClass.name || 'Unknown'} must be decorated with @ApiPath()`,\n );\n }\n assertPubSubConventions(apiClass);\n assertEveryEndpointHasAuthMode(apiClass);\n\n this.config = config;\n this.apiClass = apiClass;\n this.apiName = apiClass.name || 'UnknownApi';\n this.plans = this.buildPlans(apiClass);\n }\n\n /** Check whether the contract declares a @PubSub endpoint with this method name. */\n hasEndpoint(methodName: string): boolean {\n return this.plans.has(methodName);\n }\n\n /**\n * Enqueue one task for the named endpoint. Must run inside a CloudTaskScheduler lambda\n * (which supplies the ScheduleInfo) within an active RequestContext, e.g.:\n * scheduler.addToQueue(() => taskClient.foo(req), new ScheduleOptions(TaskName.dedup(id)));\n *\n * Before any attempt it resolves, ONCE: the task name (so every attempt carries the same one),\n * and the effective customizer and gax options (call site, else `GcpTaskRegistry` method -> API\n * -> ALL). Then exactly ONE retry layer runs:\n *\n * - **Path A, no gax — webpieces owns timing.** `CallRegistry.execute` with\n * {@link TaskProxyClient.ENQUEUE_TRANSPORT_DEFAULT}: a registered strategy (method -> API -> ALL), else a\n * registered timeout, else `RetryStrategy(5_000, 3, 500, 2, 4_000)`. Each attempt sends\n * `createTask(request, { timeout: attemptMs })`.\n * - **Path B, gax set — you own timing.** No strategy lookup and ONE attempt, sending\n * `createTask(request, yourGax)` under a safety cap from the timeout ladder (method -> API ->\n * ALL -> 30 s) so a hung channel cannot block forever.\n *\n * Either way the endpoint reports `WRITE_IDEMPOTENT`: one name for every attempt makes a repeat\n * safe (delivery is at-least-once anyway, and `TaskName.serverAssigned()` accepts duplicates).\n * A `TaskAlreadyExistsError` is caught HERE, above the retry layer: `WARN [ENQUEUE-DEDUP]`, then\n * success with `JobReference.alreadyExisted = true`.\n */\n // webpieces-disable no-any-unknown -- the request DTO's type is erased at the proxy boundary\n async enqueue(methodName: string, requestDto: unknown): Promise<void> {\n const plan = this.plans.get(methodName);\n if (!plan) {\n throw new Error(`No @PubSub endpoint '${methodName}' on ${this.apiName}`);\n }\n\n const frame = currentScheduleFrame();\n if (!frame) {\n throw new Error(\n 'Cloud task enqueue must run inside a CloudTaskScheduler lambda, e.g. ' +\n 'scheduler.addToQueue(() => taskClient.method(req), new ScheduleOptions(TaskName.dedup(id))).',\n );\n }\n const enqueuePlan = new EnqueuePlan(\n frame.info.taskName.resolve(),\n frame.info.customizeTask ?? GcpTaskRegistry.customizerFor(this.apiClass, methodName),\n frame.info.gax ?? GcpTaskRegistry.gaxFor(this.apiClass, methodName),\n );\n\n // Route through LogApiCall exactly like ProxyClient does for http. TaskProxyClient.enqueue is\n // the ONE seam above BOTH GcpTaskInvoker (prod) and InMemoryTaskInvoker (localhost) — whichever\n // TaskInvoker is bound, this method sits above it — so the enqueue edge gets structured\n // `jsonPayload.api.*` logging uniformly, without touching either invoker impl.\n const info = new ApiMethodInfo('client', this.apiName, methodName, undefined, plan.mask);\n await this.logApiCall.execute(info, requestDto, async () => {\n const callContext = new CallContext(this.apiName, methodName, WRITE_IDEMPOTENT);\n const attempt = (timeoutMs: number): Promise<JobReference> =>\n CallDeadline.run(timeoutMs, callContext, (signal: AbortSignal) =>\n this.attempt(plan, enqueuePlan, frame.info, requestDto, timeoutMs, signal),\n );\n // Only the winning attempt may publish the reference. A late enqueue cannot overwrite it.\n frame.jobRef = await this.dedupAware(methodName, () =>\n enqueuePlan.gax === undefined\n ? CallRegistry.execute(this.apiClass, callContext, attempt, TaskProxyClient.ENQUEUE_TRANSPORT_DEFAULT)\n : attempt(CallRegistry.resolveTimeout(this.apiClass, methodName, TaskProxyClient.ENQUEUE_SAFETY_CAP_MS)),\n );\n // Fire-and-forget: the enqueue ack is not a meaningful response, but it is non-null so\n // LogApiCall's null-response guard passes; a recorded test asserts the enqueued REQUEST.\n return frame.jobRef;\n });\n }\n\n /** ALREADY_EXISTS is the task the caller asked for, already there: success, logged once. */\n private async dedupAware(\n methodName: string,\n enqueue: () => Promise<JobReference>,\n ): Promise<JobReference> {\n // webpieces-disable no-unmanaged-exceptions -- ALREADY_EXISTS is success; everything else propagates unchanged\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n return await enqueue();\n } catch (err: unknown) {\n const error = toError(err);\n if (!(error instanceof TaskAlreadyExistsError)) {\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions -- rethrows the enqueue's own failure unchanged\n throw err;\n }\n log.warn(\n `[ENQUEUE-DEDUP] ${this.apiName}.${methodName} task ${error.taskName} already exists ` +\n '(a retry whose earlier attempt landed, or a dedup name reused) — treated as success',\n );\n return new JobReference(error.taskName, true);\n }\n }\n\n /** ONE enqueue attempt: a fresh TaskRequest, handed to the bound invoker. */\n private async attempt(\n plan: EndpointPlan,\n enqueuePlan: EnqueuePlan,\n scheduleInfo: ScheduleInfo,\n // webpieces-disable no-any-unknown -- the request DTO's type is erased at the proxy boundary\n requestDto: unknown,\n timeoutMs: number,\n signal: AbortSignal,\n ): Promise<JobReference> {\n // Resolved lazily (not at client construction) so building a client stays synchronous.\n // Every metadata read beneath resolveUrl is memoized process-wide, so only the first\n // enqueue in the process pays a lookup.\n const targetUrl = await this.config.resolveUrl();\n CallDeadline.throwIfAborted(signal);\n\n const request = new TaskRequest(\n targetUrl,\n plan.path,\n plan.queueName,\n requestDto,\n this.buildContextHeaders(plan.authMode),\n plan.authMode,\n scheduleInfo,\n enqueuePlan,\n );\n\n // svcName, not the URL, is the stable name across demo/qa/prod.\n log.debug(`enqueue task ${plan.queueName} -> ${this.config.svcName}${plan.path}`);\n return this.invoker.enqueue(request, timeoutMs, signal);\n }\n\n /** Endpoint name -> its resolved path / queue / auth mode, read once from the decorators. */\n private buildPlans(apiClass: ApiPrototype<object>): Map<string, EndpointPlan> {\n const basePath = getApiPath(apiClass) ?? '';\n const endpoints = getEndpoints(apiClass) ?? {};\n const plans = new Map<string, EndpointPlan>();\n\n for (const methodName of Object.keys(endpoints)) {\n const authMeta = getAuthMeta(apiClass, methodName);\n const authMode = authMeta ? this.invoker.selectAuthentication(authMeta) : undefined;\n if (!authMode) {\n throw new Error(`Endpoint '${methodName}' on ${this.apiName} has no auth mode`);\n }\n const plan = new EndpointPlan(\n basePath + endpoints[methodName],\n getQueueName(apiClass, methodName),\n authMode,\n getMaskSpec(apiClass, methodName),\n );\n plans.set(methodName, plan);\n }\n return plans;\n }\n\n /**\n * Every transferred context key (txId/requestId/tenant…) the DELIVERY endpoint may receive,\n * request-id chained. Throws if there is no active RequestContext — an enqueue with no trace is\n * a bug.\n *\n * No credential can appear here: `authorization` is read off the inbound HttpRequest and is not\n * a ContextKey, so it never enters the RequestContext to be transferred. The invoker mints the\n * task's own delivery auth per the endpoint's oidcSameProject() / oidcSvcs(...) / sharedSecret(...) mode.\n *\n * TRUSTED keys DO flow on this path, and that is the point of deriving {@link DestinationTrust}\n * from `plan.authMode` rather than hardcoding it: a task is delivered under the OIDC token the\n * invoker mints, so the delivery endpoint authenticates its caller and its AuthFilter admits the\n * userId/orgId we vouched for. A @PubSub endpoint declared @WpAuthPublic or jwt() would not, and it\n * correctly stops receiving them — the enqueue keeps working, minus context the callee would\n * have 401'd on.\n */\n private buildContextHeaders(authMode: AuthMode): Map<string, string> {\n return this.headers.buildOutboundHeaders(DestinationTrust.forAuthMode(authMode));\n }\n}\n\n/**\n * DI token for the `Provider<TaskProxyClient>` that hands out enqueue clients — one per @PubSub\n * contract. `Provider<T>` is erased at runtime, so it cannot be its own token; this Symbol names T.\n *\n * Because TaskProxyClient is bound TRANSIENT, every `get()` constructs a new one. (Were it bound\n * `@provideFrameworkSingleton`, the very same Provider would hand back one lazily-created instance\n * instead — the provider caches nothing, so the target's scope decides.)\n */\n// webpieces-disable no-symbol-di-tokens -- Provider<T> is erased at runtime; the Symbol names T\nexport const TASK_PROXY_CLIENT_PROVIDER = Symbol.for('Provider<TaskProxyClient>');\n"]}
|
|
1
|
+
{"version":3,"file":"TaskProxyClient.js","sourceRoot":"","sources":["../../../../../packages/cloud/cloudtasks-client/src/TaskProxyClient.ts"],"names":[],"mappings":"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","sourcesContent":["import { inject } from 'inversify';\nimport {\n isApiPath,\n getApiPath,\n getEndpoints,\n getAuthMeta,\n getQueueName,\n getMaskSpec,\n assertPubSubConventions,\n assertEveryEndpointHasAuthMode,\n assertNotInternalApi,\n AuthMode,\n DestinationTrust,\n MaskSpec,\n LogManager,\n LogApiCallImpl,\n ApiMethodInfo,\n CallRegistry,\n CallDeadline,\n CallContext,\n RetryStrategy,\n TransportDefault,\n WRITE_IDEMPOTENT,\n toError,\n} from '@webpieces/core-util';\nimport {\n RequestContextApiCallContext,\n RequestContextHeaders,\n provideFrameworkTransient,\n} from '@webpieces/core-context';\nimport { TaskInvoker, TaskRequest, ScheduleInfo, EnqueuePlan, JobReference } from './TaskTypes';\nimport { TaskAlreadyExistsError } from './TaskAlreadyExistsError';\nimport { GcpTaskRegistry } from './GcpTaskRegistry';\nimport { currentScheduleFrame } from './ScheduleContext';\nimport { ApiPrototype, TaskClientConfig } from './TaskClientConfig';\n\nconst log = LogManager.getLogger('TaskProxyClient');\n\n/** Per-endpoint routing plan resolved once from the contract's decorators. */\nclass EndpointPlan {\n path: string;\n queueName: string;\n authMode: AuthMode;\n /** @MaskLog spec for this endpoint's request DTO, read once here so the enqueue log path masks\n * a secret on the task payload without a per-enqueue reflection hit. Undefined = log verbatim. */\n mask?: MaskSpec;\n\n constructor(path: string, queueName: string, authMode: AuthMode, mask?: MaskSpec) {\n this.path = path;\n this.queueName = queueName;\n this.authMode = authMode;\n this.mask = mask;\n }\n}\n\n/**\n * TaskProxyClient - the enqueue engine behind one @PubSub API contract's client proxy.\n *\n * The fire-and-forget twin of http-client-core's ProxyClient, and TWO-PHASE for the same reason:\n * its COLLABORATORS (invoker, headers) come from the container, while the PER-CLIENT state (which\n * contract, which target) arrives on {@link init}. That is what lets {@link ClientCloudTasksFactory}\n * hold a `Provider<TaskProxyClient>` and hand out a fresh, independently-configured client per\n * contract.\n *\n * Calling an endpoint ENQUEUES a task (it does not call remotely); the task is later delivered to\n * the same endpoint's controller through the full server filter chain.\n *\n * It owns:\n * - @ApiPath / @PubSub convention validation + the endpoint plans from the contract's decorators\n * - Resolving the callee's base URL from svcName through `ClientRegistry.resolve` — the SAME chain the\n * RPC client runs: a registered mapping, else the installed deriver, else throw\n * - Context propagation onto the task headers (a credential is never a context key, so none can ride along)\n * - Handing a fully-built TaskRequest to the bound {@link TaskInvoker}\n */\n@provideFrameworkTransient()\nexport class TaskProxyClient {\n /**\n * The enqueue transport's default for `CallRegistry.execute` (Path A): 30 s if a caller only\n * registers a strategy-free timeout ladder, otherwise webpieces' shared `RetryStrategy` — 5 s per\n * attempt, 3 retries, backoff 500 ms x2 capped at 4 s. RPC passes `new TransportDefault(30_000)`\n * (no retry); this is the ONLY place the two transports differ.\n */\n static readonly ENQUEUE_TRANSPORT_DEFAULT = new TransportDefault(\n 30_000,\n new RetryStrategy(5_000, 3, 500, 2, 4_000).run,\n );\n\n /** Path B's safety cap when no timeout is registered: caller-owned gax timing still ends. */\n static readonly ENQUEUE_SAFETY_CAP_MS = 30_000;\n\n // Assigned by init(), which the factory calls immediately after construction.\n private plans!: Map<string, EndpointPlan>;\n private apiName!: string;\n private apiClass!: ApiPrototype<object>;\n private config!: TaskClientConfig;\n\n /**\n * The api-call log seam, built around this node-only package's OWN RequestContext-backed\n * ApiCallContext. Constructed here rather than installed at startup, so a plain NestJS/Express\n * host needs no webpieces STARTUP INSTALL to enqueue — it only has to run the call inside a\n * `RequestContext.run(...)` scope, which such a host already opens per request. That is the defect\n * that made every enqueue throw `ApiCallContext is not installed`.\n */\n private readonly logApiCall = new LogApiCallImpl(new RequestContextApiCallContext());\n\n constructor(\n @inject(TaskInvoker) private readonly invoker: TaskInvoker,\n @inject(RequestContextHeaders) private readonly headers: RequestContextHeaders,\n ) {}\n\n /** Bind this client to one @PubSub contract + target. */\n init(apiClass: ApiPrototype<object>, config: TaskClientConfig): void {\n assertNotInternalApi(apiClass, 'Cloud Tasks client');\n if (!isApiPath(apiClass)) {\n throw new Error(\n `Class ${apiClass.name || 'Unknown'} must be decorated with @ApiPath()`,\n );\n }\n assertPubSubConventions(apiClass);\n assertEveryEndpointHasAuthMode(apiClass);\n\n this.config = config;\n this.apiClass = apiClass;\n this.apiName = apiClass.name || 'UnknownApi';\n this.plans = this.buildPlans(apiClass);\n }\n\n /** Check whether the contract declares a @PubSub endpoint with this method name. */\n hasEndpoint(methodName: string): boolean {\n return this.plans.has(methodName);\n }\n\n /**\n * Enqueue one task for the named endpoint. Must run inside a CloudTaskScheduler lambda\n * (which supplies the ScheduleInfo) within an active RequestContext, e.g.:\n * scheduler.addToQueue(() => taskClient.foo(req), new ScheduleOptions(TaskName.dedup(id)));\n *\n * Before any attempt it resolves, ONCE: the task name (so every attempt carries the same one),\n * and the effective customizer and gax options (call site, else `GcpTaskRegistry` method -> API\n * -> ALL). Then exactly ONE retry layer runs:\n *\n * - **Path A, no gax — webpieces owns timing.** `CallRegistry.execute` with\n * {@link TaskProxyClient.ENQUEUE_TRANSPORT_DEFAULT}: a registered strategy (method -> API -> ALL), else a\n * registered timeout, else `RetryStrategy(5_000, 3, 500, 2, 4_000)`. Each attempt sends\n * `createTask(request, { timeout: attemptMs })`.\n * - **Path B, gax set — you own timing.** No strategy lookup and ONE attempt, sending\n * `createTask(request, yourGax)` under a safety cap from the timeout ladder (method -> API ->\n * ALL -> 30 s) so a hung channel cannot block forever.\n *\n * Either way the endpoint reports `WRITE_IDEMPOTENT`: one name for every attempt makes a repeat\n * safe (delivery is at-least-once anyway, and `TaskName.serverAssigned()` accepts duplicates).\n * A `TaskAlreadyExistsError` is caught HERE, above the retry layer: `WARN [ENQUEUE-DEDUP]`, then\n * success with `JobReference.alreadyExisted = true`.\n */\n // webpieces-disable no-any-unknown -- the request DTO's type is erased at the proxy boundary\n async enqueue(methodName: string, requestDto: unknown): Promise<void> {\n const plan = this.plans.get(methodName);\n if (!plan) {\n throw new Error(`No @PubSub endpoint '${methodName}' on ${this.apiName}`);\n }\n\n const frame = currentScheduleFrame();\n if (!frame) {\n throw new Error(\n 'Cloud task enqueue must run inside a CloudTaskScheduler lambda, e.g. ' +\n 'scheduler.addToQueue(() => taskClient.method(req), new ScheduleOptions(TaskName.dedup(id))).',\n );\n }\n const enqueuePlan = new EnqueuePlan(\n frame.info.taskName.resolve(),\n frame.info.customizeTask ?? GcpTaskRegistry.customizerFor(this.apiClass, methodName),\n frame.info.gax ?? GcpTaskRegistry.gaxFor(this.apiClass, methodName),\n );\n\n // Route through LogApiCall exactly like ProxyClient does for http. TaskProxyClient.enqueue is\n // the ONE seam above BOTH GcpTaskInvoker (prod) and InMemoryTaskInvoker (localhost) — whichever\n // TaskInvoker is bound, this method sits above it — so the enqueue edge gets structured\n // `jsonPayload.api.*` logging uniformly, without touching either invoker impl.\n const info = new ApiMethodInfo(\n 'client',\n this.apiName,\n methodName,\n this.apiClass,\n undefined,\n plan.mask,\n );\n await this.logApiCall.execute(info, requestDto, async () => {\n const callContext = new CallContext(this.apiName, methodName, WRITE_IDEMPOTENT, this.apiClass);\n const attempt = (timeoutMs: number): Promise<JobReference> =>\n CallDeadline.run(timeoutMs, callContext, (signal: AbortSignal) =>\n this.attempt(plan, enqueuePlan, frame.info, requestDto, timeoutMs, signal),\n );\n // Only the winning attempt may publish the reference. A late enqueue cannot overwrite it.\n frame.jobRef = await this.dedupAware(methodName, () =>\n enqueuePlan.gax === undefined\n ? CallRegistry.execute(this.apiClass, callContext, attempt, TaskProxyClient.ENQUEUE_TRANSPORT_DEFAULT)\n : attempt(CallRegistry.resolveTimeout(this.apiClass, methodName, TaskProxyClient.ENQUEUE_SAFETY_CAP_MS)),\n );\n // Fire-and-forget: the enqueue ack is not a meaningful response, but it is non-null so\n // LogApiCall's null-response guard passes; a recorded test asserts the enqueued REQUEST.\n return frame.jobRef;\n });\n }\n\n /** ALREADY_EXISTS is the task the caller asked for, already there: success, logged once. */\n private async dedupAware(\n methodName: string,\n enqueue: () => Promise<JobReference>,\n ): Promise<JobReference> {\n // webpieces-disable no-unmanaged-exceptions -- ALREADY_EXISTS is success; everything else propagates unchanged\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n return await enqueue();\n } catch (err: unknown) {\n const error = toError(err);\n if (!(error instanceof TaskAlreadyExistsError)) {\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions -- rethrows the enqueue's own failure unchanged\n throw err;\n }\n log.warn(\n `[ENQUEUE-DEDUP] ${this.apiName}.${methodName} task ${error.taskName} already exists ` +\n '(a retry whose earlier attempt landed, or a dedup name reused) — treated as success',\n );\n return new JobReference(error.taskName, true);\n }\n }\n\n /** ONE enqueue attempt: a fresh TaskRequest, handed to the bound invoker. */\n private async attempt(\n plan: EndpointPlan,\n enqueuePlan: EnqueuePlan,\n scheduleInfo: ScheduleInfo,\n // webpieces-disable no-any-unknown -- the request DTO's type is erased at the proxy boundary\n requestDto: unknown,\n timeoutMs: number,\n signal: AbortSignal,\n ): Promise<JobReference> {\n // Resolved lazily (not at client construction) so building a client stays synchronous.\n // Every metadata read beneath resolveUrl is memoized process-wide, so only the first\n // enqueue in the process pays a lookup.\n const targetUrl = await this.config.resolveUrl();\n CallDeadline.throwIfAborted(signal);\n\n const request = new TaskRequest(\n targetUrl,\n plan.path,\n plan.queueName,\n requestDto,\n this.buildContextHeaders(plan.authMode),\n plan.authMode,\n scheduleInfo,\n enqueuePlan,\n );\n\n // svcName, not the URL, is the stable name across demo/qa/prod.\n log.debug(`enqueue task ${plan.queueName} -> ${this.config.svcName}${plan.path}`);\n return this.invoker.enqueue(request, timeoutMs, signal);\n }\n\n /** Endpoint name -> its resolved path / queue / auth mode, read once from the decorators. */\n private buildPlans(apiClass: ApiPrototype<object>): Map<string, EndpointPlan> {\n const basePath = getApiPath(apiClass) ?? '';\n const endpoints = getEndpoints(apiClass) ?? {};\n const plans = new Map<string, EndpointPlan>();\n\n for (const methodName of Object.keys(endpoints)) {\n const authMeta = getAuthMeta(apiClass, methodName);\n const authMode = authMeta ? this.invoker.selectAuthentication(authMeta) : undefined;\n if (!authMode) {\n throw new Error(`Endpoint '${methodName}' on ${this.apiName} has no auth mode`);\n }\n const plan = new EndpointPlan(\n basePath + endpoints[methodName],\n getQueueName(apiClass, methodName),\n authMode,\n getMaskSpec(apiClass, methodName),\n );\n plans.set(methodName, plan);\n }\n return plans;\n }\n\n /**\n * Every transferred context key (txId/requestId/tenant…) the DELIVERY endpoint may receive,\n * request-id chained. Throws if there is no active RequestContext — an enqueue with no trace is\n * a bug.\n *\n * No credential can appear here: `authorization` is read off the inbound HttpRequest and is not\n * a ContextKey, so it never enters the RequestContext to be transferred. The invoker mints the\n * task's own delivery auth per the endpoint's oidcSameProject() / oidcSvcs(...) / sharedSecret(...) mode.\n *\n * TRUSTED keys DO flow on this path, and that is the point of deriving {@link DestinationTrust}\n * from `plan.authMode` rather than hardcoding it: a task is delivered under the OIDC token the\n * invoker mints, so the delivery endpoint authenticates its caller and its AuthFilter admits the\n * userId/orgId we vouched for. A @PubSub endpoint declared @WpAuthPublic or jwt() would not, and it\n * correctly stops receiving them — the enqueue keeps working, minus context the callee would\n * have 401'd on.\n */\n private buildContextHeaders(authMode: AuthMode): Map<string, string> {\n return this.headers.buildOutboundHeaders(DestinationTrust.forAuthMode(authMode));\n }\n}\n\n/**\n * DI token for the `Provider<TaskProxyClient>` that hands out enqueue clients — one per @PubSub\n * contract. `Provider<T>` is erased at runtime, so it cannot be its own token; this Symbol names T.\n *\n * Because TaskProxyClient is bound TRANSIENT, every `get()` constructs a new one. (Were it bound\n * `@provideFrameworkSingleton`, the very same Provider would hand back one lazily-created instance\n * instead — the provider caches nothing, so the target's scope decides.)\n */\n// webpieces-disable no-symbol-di-tokens -- Provider<T> is erased at runtime; the Symbol names T\nexport const TASK_PROXY_CLIENT_PROVIDER = Symbol.for('Provider<TaskProxyClient>');\n"]}
|