@webpieces/cloudtasks-client 0.3.269
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 +23 -0
- package/package.json +33 -0
- package/src/CloudTaskScheduler.d.ts +60 -0
- package/src/CloudTaskScheduler.js +109 -0
- package/src/CloudTaskScheduler.js.map +1 -0
- package/src/GcpTaskInvoker.d.ts +15 -0
- package/src/GcpTaskInvoker.js +80 -0
- package/src/GcpTaskInvoker.js.map +1 -0
- package/src/InMemoryTaskInvoker.d.ts +32 -0
- package/src/InMemoryTaskInvoker.js +115 -0
- package/src/InMemoryTaskInvoker.js.map +1 -0
- package/src/ScheduleContext.d.ts +18 -0
- package/src/ScheduleContext.js +36 -0
- package/src/ScheduleContext.js.map +1 -0
- package/src/TaskClientCreator.d.ts +24 -0
- package/src/TaskClientCreator.js +48 -0
- package/src/TaskClientCreator.js.map +1 -0
- package/src/TaskClientFactory.d.ts +27 -0
- package/src/TaskClientFactory.js +124 -0
- package/src/TaskClientFactory.js.map +1 -0
- package/src/TaskTypes.d.ts +55 -0
- package/src/TaskTypes.js +77 -0
- package/src/TaskTypes.js.map +1 -0
- package/src/index.d.ts +15 -0
- package/src/index.js +37 -0
- package/src/index.js.map +1 -0
package/README.md
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# @webpieces/cloudtasks-client
|
|
2
|
+
|
|
3
|
+
The Cloud Tasks twin of `@webpieces/http-client`. A `@PubSub` API contract is shared
|
|
4
|
+
by the enqueue client and the controller, exactly like RPC — calling a method on the
|
|
5
|
+
client **enqueues a Cloud Task** that is later delivered (POST) to the SAME endpoint,
|
|
6
|
+
where it runs through the full server filter chain.
|
|
7
|
+
|
|
8
|
+
```ts
|
|
9
|
+
// one shared contract
|
|
10
|
+
@PubSub() @AuthOidc() @ApiPath('/email')
|
|
11
|
+
abstract class EmailApi { @Endpoint('/send') sendEmail(r: SendEmailRequest): Promise<void> {…} }
|
|
12
|
+
|
|
13
|
+
// producer (inside a request → RequestContext active)
|
|
14
|
+
await scheduler.addToQueue(() => emailTasks.sendEmail(req), { dedupName: req.id });
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
- `createTaskClient(Api, TaskClientConfig)` / `TaskClientCreator` — the enqueue proxy
|
|
18
|
+
- `CloudTaskScheduler` — `addToQueue` / `schedule` / `cancelJob`; carries scheduling
|
|
19
|
+
options out-of-band so the contract signature stays identical on both sides
|
|
20
|
+
- `TaskInvoker` (abstract token) with two impls:
|
|
21
|
+
- `GcpTaskInvoker` — real `@google-cloud/tasks` delivery (OIDC / shared-secret)
|
|
22
|
+
- `InMemoryTaskInvoker` — dispatches through the real server filter chain in-process
|
|
23
|
+
(tests + local dev, no GCP, production parity)
|
package/package.json
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@webpieces/cloudtasks-client",
|
|
3
|
+
"version": "0.3.269",
|
|
4
|
+
"description": "Cloud Tasks enqueue client generated from a shared @PubSub API contract (twin of http-client)",
|
|
5
|
+
"type": "commonjs",
|
|
6
|
+
"main": "./src/index.js",
|
|
7
|
+
"types": "./src/index.d.ts",
|
|
8
|
+
"author": "Dean Hiller",
|
|
9
|
+
"license": "Apache-2.0",
|
|
10
|
+
"repository": {
|
|
11
|
+
"type": "git",
|
|
12
|
+
"url": "https://github.com/deanhiller/webpieces-ts.git",
|
|
13
|
+
"directory": "packages/cloud/cloudtasks-client"
|
|
14
|
+
},
|
|
15
|
+
"keywords": [
|
|
16
|
+
"webpieces",
|
|
17
|
+
"gcp",
|
|
18
|
+
"cloud-tasks",
|
|
19
|
+
"pubsub",
|
|
20
|
+
"queue"
|
|
21
|
+
],
|
|
22
|
+
"publishConfig": {
|
|
23
|
+
"access": "public"
|
|
24
|
+
},
|
|
25
|
+
"dependencies": {
|
|
26
|
+
"@webpieces/core-context": "0.3.269",
|
|
27
|
+
"@webpieces/core-util": "0.3.269",
|
|
28
|
+
"@webpieces/gcp-identity": "0.3.269",
|
|
29
|
+
"@google-cloud/tasks": "5.5.2",
|
|
30
|
+
"inversify": "7.10.4",
|
|
31
|
+
"reflect-metadata": "0.2.2"
|
|
32
|
+
}
|
|
33
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { TaskInvoker, JobReference } from './TaskTypes';
|
|
2
|
+
/**
|
|
3
|
+
* Options for a scheduled task. These are QUEUE/TRANSPORT knobs (dedup, deadline)
|
|
4
|
+
* that belong on the enqueue CALL, never on the shared API method — the controller
|
|
5
|
+
* implementing that method has no use for them. See CloudTaskScheduler's class doc
|
|
6
|
+
* for the full rationale.
|
|
7
|
+
*/
|
|
8
|
+
export declare class ScheduleOptions {
|
|
9
|
+
/** Deterministic Cloud Task name → idempotent dedup (duplicate = ALREADY_EXISTS). */
|
|
10
|
+
dedupName?: string;
|
|
11
|
+
/** Per-task dispatch deadline in seconds. */
|
|
12
|
+
taskTimeoutSeconds?: number;
|
|
13
|
+
constructor(dedupName?: string, taskTimeoutSeconds?: number);
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Schedules Cloud Tasks. You wrap an enqueue-client call in a lambda so the shared
|
|
17
|
+
* contract's method signature stays `foo(req)` on both client and controller; the
|
|
18
|
+
* scheduling options ride out-of-band in the schedule frame:
|
|
19
|
+
*
|
|
20
|
+
* await scheduler.addToQueue(() => taskClient.sendEmail(req), { dedupName: id });
|
|
21
|
+
* await scheduler.schedule(() => taskClient.sendEmail(req), runAtEpochMs);
|
|
22
|
+
*
|
|
23
|
+
* Must be called inside an active RequestContext (a server request scope).
|
|
24
|
+
*
|
|
25
|
+
* WHY scheduling options are here and NOT parameters on the API method
|
|
26
|
+
* ------------------------------------------------------------------------
|
|
27
|
+
* The enqueue client and the controller implement the SAME abstract API method
|
|
28
|
+
* (`sendEmail(req): Promise<void>`). Scheduling options — `dedupName`, "run in the
|
|
29
|
+
* future", `taskTimeoutSeconds` — are QUEUE/TRANSPORT concerns, not part of the
|
|
30
|
+
* business contract, so they must not appear on the method signature:
|
|
31
|
+
*
|
|
32
|
+
* - They are meaningless on the SERVER side. The controller's `sendEmail` just does
|
|
33
|
+
* the work (send the email). It has no use for `dedupName` (a Cloud Tasks resource
|
|
34
|
+
* name), a future run-time (Cloud Tasks already delivered it), or a dispatch
|
|
35
|
+
* deadline. Putting them on the method would force the controller to accept and
|
|
36
|
+
* ignore transport metadata it can't act on.
|
|
37
|
+
* - They vary per CALL SITE, not per method. The same `sendEmail` might be enqueued
|
|
38
|
+
* plain in one place, deduped in another, and scheduled for later in a third. That
|
|
39
|
+
* is a property of THIS enqueue, not of the API — so it belongs on the enqueue
|
|
40
|
+
* call (the scheduler), exactly like Cloud Tasks itself separates the task's
|
|
41
|
+
* schedule/dedup/deadline from the HTTP body it delivers.
|
|
42
|
+
* - Symmetry with RPC: an http-client RPC call is `foo(req)` with no transport knobs
|
|
43
|
+
* in the contract; the async (queue) client keeps that same clean contract and
|
|
44
|
+
* moves the queue knobs to the scheduler wrapper.
|
|
45
|
+
*
|
|
46
|
+
* So the rule is: the API method is `method(req)` on BOTH sides forever; anything
|
|
47
|
+
* queue-shaped (dedup, delay, timeout, cancel) lives on CloudTaskScheduler /
|
|
48
|
+
* ScheduleOptions here, never on the API.
|
|
49
|
+
*/
|
|
50
|
+
export declare class CloudTaskScheduler {
|
|
51
|
+
private readonly invoker;
|
|
52
|
+
constructor(invoker: TaskInvoker);
|
|
53
|
+
/** Enqueue a task to run as soon as possible. Returns its JobReference. */
|
|
54
|
+
addToQueue(runnable: () => Promise<void>, opts?: ScheduleOptions): Promise<JobReference>;
|
|
55
|
+
/** Enqueue a task to run at an absolute epoch-millis time. Returns its JobReference. */
|
|
56
|
+
schedule(runnable: () => Promise<void>, epochMsToRunAt: number, opts?: ScheduleOptions): Promise<JobReference>;
|
|
57
|
+
/** Cancel a previously scheduled task. */
|
|
58
|
+
cancelJob(ref: JobReference): Promise<void>;
|
|
59
|
+
private runWithFrame;
|
|
60
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.CloudTaskScheduler = exports.ScheduleOptions = void 0;
|
|
4
|
+
const tslib_1 = require("tslib");
|
|
5
|
+
const inversify_1 = require("inversify");
|
|
6
|
+
const core_context_1 = require("@webpieces/core-context");
|
|
7
|
+
const core_context_2 = require("@webpieces/core-context");
|
|
8
|
+
const core_util_1 = require("@webpieces/core-util");
|
|
9
|
+
const TaskTypes_1 = require("./TaskTypes");
|
|
10
|
+
const ScheduleContext_1 = require("./ScheduleContext");
|
|
11
|
+
const log = core_util_1.LogManager.getLogger('CloudTaskScheduler');
|
|
12
|
+
/**
|
|
13
|
+
* Options for a scheduled task. These are QUEUE/TRANSPORT knobs (dedup, deadline)
|
|
14
|
+
* that belong on the enqueue CALL, never on the shared API method — the controller
|
|
15
|
+
* implementing that method has no use for them. See CloudTaskScheduler's class doc
|
|
16
|
+
* for the full rationale.
|
|
17
|
+
*/
|
|
18
|
+
class ScheduleOptions {
|
|
19
|
+
/** Deterministic Cloud Task name → idempotent dedup (duplicate = ALREADY_EXISTS). */
|
|
20
|
+
dedupName;
|
|
21
|
+
/** Per-task dispatch deadline in seconds. */
|
|
22
|
+
taskTimeoutSeconds;
|
|
23
|
+
constructor(dedupName, taskTimeoutSeconds) {
|
|
24
|
+
this.dedupName = dedupName;
|
|
25
|
+
this.taskTimeoutSeconds = taskTimeoutSeconds;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
exports.ScheduleOptions = ScheduleOptions;
|
|
29
|
+
/**
|
|
30
|
+
* Schedules Cloud Tasks. You wrap an enqueue-client call in a lambda so the shared
|
|
31
|
+
* contract's method signature stays `foo(req)` on both client and controller; the
|
|
32
|
+
* scheduling options ride out-of-band in the schedule frame:
|
|
33
|
+
*
|
|
34
|
+
* await scheduler.addToQueue(() => taskClient.sendEmail(req), { dedupName: id });
|
|
35
|
+
* await scheduler.schedule(() => taskClient.sendEmail(req), runAtEpochMs);
|
|
36
|
+
*
|
|
37
|
+
* Must be called inside an active RequestContext (a server request scope).
|
|
38
|
+
*
|
|
39
|
+
* WHY scheduling options are here and NOT parameters on the API method
|
|
40
|
+
* ------------------------------------------------------------------------
|
|
41
|
+
* The enqueue client and the controller implement the SAME abstract API method
|
|
42
|
+
* (`sendEmail(req): Promise<void>`). Scheduling options — `dedupName`, "run in the
|
|
43
|
+
* future", `taskTimeoutSeconds` — are QUEUE/TRANSPORT concerns, not part of the
|
|
44
|
+
* business contract, so they must not appear on the method signature:
|
|
45
|
+
*
|
|
46
|
+
* - They are meaningless on the SERVER side. The controller's `sendEmail` just does
|
|
47
|
+
* the work (send the email). It has no use for `dedupName` (a Cloud Tasks resource
|
|
48
|
+
* name), a future run-time (Cloud Tasks already delivered it), or a dispatch
|
|
49
|
+
* deadline. Putting them on the method would force the controller to accept and
|
|
50
|
+
* ignore transport metadata it can't act on.
|
|
51
|
+
* - They vary per CALL SITE, not per method. The same `sendEmail` might be enqueued
|
|
52
|
+
* plain in one place, deduped in another, and scheduled for later in a third. That
|
|
53
|
+
* is a property of THIS enqueue, not of the API — so it belongs on the enqueue
|
|
54
|
+
* call (the scheduler), exactly like Cloud Tasks itself separates the task's
|
|
55
|
+
* schedule/dedup/deadline from the HTTP body it delivers.
|
|
56
|
+
* - Symmetry with RPC: an http-client RPC call is `foo(req)` with no transport knobs
|
|
57
|
+
* in the contract; the async (queue) client keeps that same clean contract and
|
|
58
|
+
* moves the queue knobs to the scheduler wrapper.
|
|
59
|
+
*
|
|
60
|
+
* So the rule is: the API method is `method(req)` on BOTH sides forever; anything
|
|
61
|
+
* queue-shaped (dedup, delay, timeout, cancel) lives on CloudTaskScheduler /
|
|
62
|
+
* ScheduleOptions here, never on the API.
|
|
63
|
+
*/
|
|
64
|
+
let CloudTaskScheduler = class CloudTaskScheduler {
|
|
65
|
+
invoker;
|
|
66
|
+
constructor(invoker) {
|
|
67
|
+
this.invoker = invoker;
|
|
68
|
+
}
|
|
69
|
+
/** Enqueue a task to run as soon as possible. Returns its JobReference. */
|
|
70
|
+
async addToQueue(runnable, opts) {
|
|
71
|
+
return this.runWithFrame(new TaskTypes_1.ScheduleInfo(undefined, opts?.taskTimeoutSeconds, opts?.dedupName), runnable);
|
|
72
|
+
}
|
|
73
|
+
/** Enqueue a task to run at an absolute epoch-millis time. Returns its JobReference. */
|
|
74
|
+
async schedule(runnable, epochMsToRunAt, opts) {
|
|
75
|
+
return this.runWithFrame(new TaskTypes_1.ScheduleInfo(epochMsToRunAt, opts?.taskTimeoutSeconds, opts?.dedupName), runnable);
|
|
76
|
+
}
|
|
77
|
+
/** Cancel a previously scheduled task. */
|
|
78
|
+
async cancelJob(ref) {
|
|
79
|
+
await this.invoker.delete(ref);
|
|
80
|
+
}
|
|
81
|
+
async runWithFrame(info, runnable) {
|
|
82
|
+
if (!core_context_2.RequestContext.isActive()) {
|
|
83
|
+
throw new Error('CloudTaskScheduler must run inside a RequestContext (a server request scope).');
|
|
84
|
+
}
|
|
85
|
+
const frame = new ScheduleContext_1.ScheduleFrame(info);
|
|
86
|
+
(0, ScheduleContext_1.setScheduleFrame)(frame);
|
|
87
|
+
// eslint-disable-next-line @webpieces/no-unmanaged-exceptions -- always clear the frame; the runnable's error propagates unchanged
|
|
88
|
+
try {
|
|
89
|
+
await runnable();
|
|
90
|
+
}
|
|
91
|
+
finally {
|
|
92
|
+
(0, ScheduleContext_1.clearScheduleFrame)();
|
|
93
|
+
}
|
|
94
|
+
if (!frame.jobRef) {
|
|
95
|
+
throw new Error('CloudTaskScheduler runnable did not enqueue a task — the lambda must call ' +
|
|
96
|
+
'a task-client method exactly once.');
|
|
97
|
+
}
|
|
98
|
+
log.debug(`scheduled task ${frame.jobRef.taskId}`);
|
|
99
|
+
return frame.jobRef;
|
|
100
|
+
}
|
|
101
|
+
};
|
|
102
|
+
exports.CloudTaskScheduler = CloudTaskScheduler;
|
|
103
|
+
exports.CloudTaskScheduler = CloudTaskScheduler = tslib_1.__decorate([
|
|
104
|
+
(0, core_context_1.provideSingleton)(),
|
|
105
|
+
(0, inversify_1.injectable)(),
|
|
106
|
+
tslib_1.__param(0, (0, inversify_1.inject)(TaskTypes_1.TaskInvoker)),
|
|
107
|
+
tslib_1.__metadata("design:paramtypes", [TaskTypes_1.TaskInvoker])
|
|
108
|
+
], CloudTaskScheduler);
|
|
109
|
+
//# sourceMappingURL=CloudTaskScheduler.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"CloudTaskScheduler.js","sourceRoot":"","sources":["../../../../../packages/cloud/cloudtasks-client/src/CloudTaskScheduler.ts"],"names":[],"mappings":";;;;AAAA,yCAA+C;AAC/C,0DAA2D;AAC3D,0DAAyD;AACzD,oDAAkD;AAClD,2CAAsE;AACtE,uDAAwF;AAExF,MAAM,GAAG,GAAG,sBAAU,CAAC,SAAS,CAAC,oBAAoB,CAAC,CAAC;AAEvD;;;;;GAKG;AACH,MAAa,eAAe;IACxB,qFAAqF;IACrF,SAAS,CAAU;IACnB,6CAA6C;IAC7C,kBAAkB,CAAU;IAE5B,YAAY,SAAkB,EAAE,kBAA2B;QACvD,IAAI,CAAC,SAAS,GAAG,SAAS,CAAC;QAC3B,IAAI,CAAC,kBAAkB,GAAG,kBAAkB,CAAC;IACjD,CAAC;CACJ;AAVD,0CAUC;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkCG;AAGI,IAAM,kBAAkB,GAAxB,MAAM,kBAAkB;IAEe;IAD1C,YAC0C,OAAoB;QAApB,YAAO,GAAP,OAAO,CAAa;IAC3D,CAAC;IAEJ,2EAA2E;IAC3E,KAAK,CAAC,UAAU,CAAC,QAA6B,EAAE,IAAsB;QAClE,OAAO,IAAI,CAAC,YAAY,CACpB,IAAI,wBAAY,CAAC,SAAS,EAAE,IAAI,EAAE,kBAAkB,EAAE,IAAI,EAAE,SAAS,CAAC,EACtE,QAAQ,CACX,CAAC;IACN,CAAC;IAED,wFAAwF;IACxF,KAAK,CAAC,QAAQ,CACV,QAA6B,EAC7B,cAAsB,EACtB,IAAsB;QAEtB,OAAO,IAAI,CAAC,YAAY,CACpB,IAAI,wBAAY,CAAC,cAAc,EAAE,IAAI,EAAE,kBAAkB,EAAE,IAAI,EAAE,SAAS,CAAC,EAC3E,QAAQ,CACX,CAAC;IACN,CAAC;IAED,0CAA0C;IAC1C,KAAK,CAAC,SAAS,CAAC,GAAiB;QAC7B,MAAM,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;IACnC,CAAC;IAEO,KAAK,CAAC,YAAY,CACtB,IAAkB,EAClB,QAA6B;QAE7B,IAAI,CAAC,6BAAc,CAAC,QAAQ,EAAE,EAAE,CAAC;YAC7B,MAAM,IAAI,KAAK,CACX,+EAA+E,CAClF,CAAC;QACN,CAAC;QACD,MAAM,KAAK,GAAG,IAAI,+BAAa,CAAC,IAAI,CAAC,CAAC;QACtC,IAAA,kCAAgB,EAAC,KAAK,CAAC,CAAC;QACxB,mIAAmI;QACnI,IAAI,CAAC;YACD,MAAM,QAAQ,EAAE,CAAC;QACrB,CAAC;gBAAS,CAAC;YACP,IAAA,oCAAkB,GAAE,CAAC;QACzB,CAAC;QACD,IAAI,CAAC,KAAK,CAAC,MAAM,EAAE,CAAC;YAChB,MAAM,IAAI,KAAK,CACX,4EAA4E;gBAC5E,oCAAoC,CACvC,CAAC;QACN,CAAC;QACD,GAAG,CAAC,KAAK,CAAC,kBAAkB,KAAK,CAAC,MAAM,CAAC,MAAM,EAAE,CAAC,CAAC;QACnD,OAAO,KAAK,CAAC,MAAM,CAAC;IACxB,CAAC;CACJ,CAAA;AAxDY,gDAAkB;6BAAlB,kBAAkB;IAF9B,IAAA,+BAAgB,GAAE;IAClB,IAAA,sBAAU,GAAE;IAGJ,mBAAA,IAAA,kBAAM,EAAC,uBAAW,CAAC,CAAA;6CAA2B,uBAAW;GAFrD,kBAAkB,CAwD9B","sourcesContent":["import { inject, injectable } from 'inversify';\nimport { provideSingleton } from '@webpieces/core-context';\nimport { RequestContext } from '@webpieces/core-context';\nimport { LogManager } from '@webpieces/core-util';\nimport { TaskInvoker, JobReference, ScheduleInfo } from './TaskTypes';\nimport { ScheduleFrame, setScheduleFrame, clearScheduleFrame } from './ScheduleContext';\n\nconst log = LogManager.getLogger('CloudTaskScheduler');\n\n/**\n * Options for a scheduled task. These are QUEUE/TRANSPORT knobs (dedup, deadline)\n * that belong on the enqueue CALL, never on the shared API method — the controller\n * implementing that method has no use for them. See CloudTaskScheduler's class doc\n * for the full rationale.\n */\nexport class ScheduleOptions {\n /** Deterministic Cloud Task name → idempotent dedup (duplicate = ALREADY_EXISTS). */\n dedupName?: string;\n /** Per-task dispatch deadline in seconds. */\n taskTimeoutSeconds?: number;\n\n constructor(dedupName?: string, taskTimeoutSeconds?: number) {\n this.dedupName = dedupName;\n this.taskTimeoutSeconds = taskTimeoutSeconds;\n }\n}\n\n/**\n * Schedules Cloud Tasks. You wrap an enqueue-client call in a lambda so the shared\n * contract's method signature stays `foo(req)` on both client and controller; the\n * scheduling options ride out-of-band in the schedule frame:\n *\n * await scheduler.addToQueue(() => taskClient.sendEmail(req), { dedupName: id });\n * await scheduler.schedule(() => taskClient.sendEmail(req), runAtEpochMs);\n *\n * Must be called inside an active RequestContext (a server request scope).\n *\n * WHY scheduling options are here and NOT parameters on the API method\n * ------------------------------------------------------------------------\n * The enqueue client and the controller implement the SAME abstract API method\n * (`sendEmail(req): Promise<void>`). Scheduling options — `dedupName`, \"run in the\n * future\", `taskTimeoutSeconds` — are QUEUE/TRANSPORT concerns, not part of the\n * business contract, so they must not appear on the method signature:\n *\n * - They are meaningless on the SERVER side. The controller's `sendEmail` just does\n * the work (send the email). It has no use for `dedupName` (a Cloud Tasks resource\n * name), a future run-time (Cloud Tasks already delivered it), or a dispatch\n * deadline. Putting them on the method would force the controller to accept and\n * ignore transport metadata it can't act on.\n * - They vary per CALL SITE, not per method. The same `sendEmail` might be enqueued\n * plain in one place, deduped in another, and scheduled for later in a third. That\n * is a property of THIS enqueue, not of the API — so it belongs on the enqueue\n * call (the scheduler), exactly like Cloud Tasks itself separates the task's\n * schedule/dedup/deadline from the HTTP body it delivers.\n * - Symmetry with RPC: an http-client RPC call is `foo(req)` with no transport knobs\n * in the contract; the async (queue) client keeps that same clean contract and\n * moves the queue knobs to the scheduler wrapper.\n *\n * So the rule is: the API method is `method(req)` on BOTH sides forever; anything\n * queue-shaped (dedup, delay, timeout, cancel) lives on CloudTaskScheduler /\n * ScheduleOptions here, never on the API.\n */\n@provideSingleton()\n@injectable()\nexport class CloudTaskScheduler {\n constructor(\n @inject(TaskInvoker) private readonly invoker: TaskInvoker,\n ) {}\n\n /** Enqueue a task to run as soon as possible. Returns its JobReference. */\n async addToQueue(runnable: () => Promise<void>, opts?: ScheduleOptions): Promise<JobReference> {\n return this.runWithFrame(\n new ScheduleInfo(undefined, opts?.taskTimeoutSeconds, opts?.dedupName),\n runnable,\n );\n }\n\n /** Enqueue a task to run at an absolute epoch-millis time. Returns its JobReference. */\n async schedule(\n runnable: () => Promise<void>,\n epochMsToRunAt: number,\n opts?: ScheduleOptions,\n ): Promise<JobReference> {\n return this.runWithFrame(\n new ScheduleInfo(epochMsToRunAt, opts?.taskTimeoutSeconds, opts?.dedupName),\n runnable,\n );\n }\n\n /** Cancel a previously scheduled task. */\n async cancelJob(ref: JobReference): Promise<void> {\n await this.invoker.delete(ref);\n }\n\n private async runWithFrame(\n info: ScheduleInfo,\n runnable: () => Promise<void>,\n ): Promise<JobReference> {\n if (!RequestContext.isActive()) {\n throw new Error(\n 'CloudTaskScheduler must run inside a RequestContext (a server request scope).',\n );\n }\n const frame = new ScheduleFrame(info);\n setScheduleFrame(frame);\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions -- always clear the frame; the runnable's error propagates unchanged\n try {\n await runnable();\n } finally {\n clearScheduleFrame();\n }\n if (!frame.jobRef) {\n throw new Error(\n 'CloudTaskScheduler runnable did not enqueue a task — the lambda must call ' +\n 'a task-client method exactly once.',\n );\n }\n log.debug(`scheduled task ${frame.jobRef.taskId}`);\n return frame.jobRef;\n }\n}\n"]}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { TaskInvoker, TaskRequest, JobReference } from './TaskTypes';
|
|
2
|
+
/**
|
|
3
|
+
* TaskInvoker that enqueues to Google Cloud Tasks (`@google-cloud/tasks`). Builds an
|
|
4
|
+
* HTTP-target task delivered as POST to `targetUrl + path`, authenticated per the
|
|
5
|
+
* endpoint's auth mode (OIDC token minted as this service's runtime SA, or a
|
|
6
|
+
* shared-secret header). The task name is derived from the dedup name for idempotency
|
|
7
|
+
* (a duplicate enqueue is rejected ALREADY_EXISTS = idempotent success upstream).
|
|
8
|
+
*/
|
|
9
|
+
export declare class GcpTaskInvoker extends TaskInvoker {
|
|
10
|
+
private readonly client;
|
|
11
|
+
enqueue(request: TaskRequest): Promise<JobReference>;
|
|
12
|
+
delete(ref: JobReference): Promise<void>;
|
|
13
|
+
private buildTask;
|
|
14
|
+
private applyAuth;
|
|
15
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.GcpTaskInvoker = void 0;
|
|
4
|
+
const tslib_1 = require("tslib");
|
|
5
|
+
const inversify_1 = require("inversify");
|
|
6
|
+
const tasks_1 = require("@google-cloud/tasks");
|
|
7
|
+
const core_context_1 = require("@webpieces/core-context");
|
|
8
|
+
const gcp_identity_1 = require("@webpieces/gcp-identity");
|
|
9
|
+
const core_util_1 = require("@webpieces/core-util");
|
|
10
|
+
const TaskTypes_1 = require("./TaskTypes");
|
|
11
|
+
const log = core_util_1.LogManager.getLogger('GcpTaskInvoker');
|
|
12
|
+
/**
|
|
13
|
+
* TaskInvoker that enqueues to Google Cloud Tasks (`@google-cloud/tasks`). Builds an
|
|
14
|
+
* HTTP-target task delivered as POST to `targetUrl + path`, authenticated per the
|
|
15
|
+
* endpoint's auth mode (OIDC token minted as this service's runtime SA, or a
|
|
16
|
+
* shared-secret header). The task name is derived from the dedup name for idempotency
|
|
17
|
+
* (a duplicate enqueue is rejected ALREADY_EXISTS = idempotent success upstream).
|
|
18
|
+
*/
|
|
19
|
+
let GcpTaskInvoker = class GcpTaskInvoker extends TaskTypes_1.TaskInvoker {
|
|
20
|
+
client = new tasks_1.CloudTasksClient();
|
|
21
|
+
async enqueue(request) {
|
|
22
|
+
const projectId = await (0, gcp_identity_1.getProjectId)();
|
|
23
|
+
const region = await (0, gcp_identity_1.getRegion)();
|
|
24
|
+
const parent = this.client.queuePath(projectId, region, request.queueName);
|
|
25
|
+
const task = await this.buildTask(request, parent);
|
|
26
|
+
const result = await this.client.createTask({ parent: parent, task: task });
|
|
27
|
+
const created = result[0];
|
|
28
|
+
const name = created.name ?? request.scheduleInfo.dedupName ?? 'unknown';
|
|
29
|
+
log.info(`enqueued cloud task ${name}`);
|
|
30
|
+
return new TaskTypes_1.JobReference(name);
|
|
31
|
+
}
|
|
32
|
+
async delete(ref) {
|
|
33
|
+
await this.client.deleteTask({ name: ref.taskId });
|
|
34
|
+
}
|
|
35
|
+
async buildTask(request, parent) {
|
|
36
|
+
const headers = new Map(request.contextHeaders);
|
|
37
|
+
headers.set('content-type', 'application/json');
|
|
38
|
+
const httpRequest = {
|
|
39
|
+
httpMethod: 'POST',
|
|
40
|
+
url: request.targetUrl + request.path,
|
|
41
|
+
headers: Object.fromEntries(headers),
|
|
42
|
+
body: Buffer.from(JSON.stringify(request.body ?? {}), 'utf8').toString('base64'),
|
|
43
|
+
};
|
|
44
|
+
await this.applyAuth(request, httpRequest, headers);
|
|
45
|
+
const task = { httpRequest: httpRequest };
|
|
46
|
+
if (request.scheduleInfo.dedupName) {
|
|
47
|
+
task.name = `${parent}/tasks/${request.scheduleInfo.dedupName}`;
|
|
48
|
+
}
|
|
49
|
+
if (request.scheduleInfo.epochMsToRunAt) {
|
|
50
|
+
task.scheduleTime = { seconds: Math.floor(request.scheduleInfo.epochMsToRunAt / 1000) };
|
|
51
|
+
}
|
|
52
|
+
if (request.scheduleInfo.taskTimeoutSeconds) {
|
|
53
|
+
task.dispatchDeadline = { seconds: request.scheduleInfo.taskTimeoutSeconds };
|
|
54
|
+
}
|
|
55
|
+
return task;
|
|
56
|
+
}
|
|
57
|
+
async applyAuth(request, httpRequest, headers) {
|
|
58
|
+
const mode = request.authMode;
|
|
59
|
+
if (mode.kind === 'oidc') {
|
|
60
|
+
httpRequest.oidcToken = {
|
|
61
|
+
serviceAccountEmail: await (0, gcp_identity_1.getRuntimeServiceAccountEmail)(),
|
|
62
|
+
audience: request.targetUrl,
|
|
63
|
+
};
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
if (mode.kind === 'shared-secret') {
|
|
67
|
+
const secret = process.env[mode.secretEnv];
|
|
68
|
+
if (secret) {
|
|
69
|
+
headers.set('x-webpieces-shared-secret', secret);
|
|
70
|
+
httpRequest.headers = Object.fromEntries(headers);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
exports.GcpTaskInvoker = GcpTaskInvoker;
|
|
76
|
+
exports.GcpTaskInvoker = GcpTaskInvoker = tslib_1.__decorate([
|
|
77
|
+
(0, core_context_1.provideSingleton)(),
|
|
78
|
+
(0, inversify_1.injectable)()
|
|
79
|
+
], GcpTaskInvoker);
|
|
80
|
+
//# sourceMappingURL=GcpTaskInvoker.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"GcpTaskInvoker.js","sourceRoot":"","sources":["../../../../../packages/cloud/cloudtasks-client/src/GcpTaskInvoker.ts"],"names":[],"mappings":"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","sourcesContent":["import { injectable } from 'inversify';\nimport { CloudTasksClient, protos } from '@google-cloud/tasks';\nimport { provideSingleton } from '@webpieces/core-context';\nimport {\n getProjectId,\n getRegion,\n getRuntimeServiceAccountEmail,\n} from '@webpieces/gcp-identity';\nimport { LogManager } from '@webpieces/core-util';\nimport { TaskInvoker, TaskRequest, JobReference } from './TaskTypes';\n\nconst log = LogManager.getLogger('GcpTaskInvoker');\n\ntype ITask = protos.google.cloud.tasks.v2.ITask;\n\n/**\n * TaskInvoker that enqueues to Google Cloud Tasks (`@google-cloud/tasks`). Builds an\n * HTTP-target task delivered as POST to `targetUrl + path`, authenticated per the\n * endpoint's auth mode (OIDC token minted as this service's runtime SA, or a\n * shared-secret header). The task name is derived from the dedup name for idempotency\n * (a duplicate enqueue is rejected ALREADY_EXISTS = idempotent success upstream).\n */\n@provideSingleton()\n@injectable()\nexport class GcpTaskInvoker extends TaskInvoker {\n private readonly client = new CloudTasksClient();\n\n override async enqueue(request: TaskRequest): Promise<JobReference> {\n const projectId = await getProjectId();\n const region = await getRegion();\n const parent = this.client.queuePath(projectId, region, request.queueName);\n\n const task = await this.buildTask(request, parent);\n const result = await this.client.createTask({ parent: parent, task: task });\n const created = result[0];\n const name = created.name ?? request.scheduleInfo.dedupName ?? 'unknown';\n log.info(`enqueued cloud task ${name}`);\n return new JobReference(name);\n }\n\n override async delete(ref: JobReference): Promise<void> {\n await this.client.deleteTask({ name: ref.taskId });\n }\n\n private async buildTask(request: TaskRequest, parent: string): Promise<ITask> {\n const headers = new Map<string, string>(request.contextHeaders);\n headers.set('content-type', 'application/json');\n\n const httpRequest: protos.google.cloud.tasks.v2.IHttpRequest = {\n httpMethod: 'POST',\n url: request.targetUrl + request.path,\n headers: Object.fromEntries(headers),\n body: Buffer.from(JSON.stringify(request.body ?? {}), 'utf8').toString('base64'),\n };\n await this.applyAuth(request, httpRequest, headers);\n\n const task: ITask = { httpRequest: httpRequest };\n if (request.scheduleInfo.dedupName) {\n task.name = `${parent}/tasks/${request.scheduleInfo.dedupName}`;\n }\n if (request.scheduleInfo.epochMsToRunAt) {\n task.scheduleTime = { seconds: Math.floor(request.scheduleInfo.epochMsToRunAt / 1000) };\n }\n if (request.scheduleInfo.taskTimeoutSeconds) {\n task.dispatchDeadline = { seconds: request.scheduleInfo.taskTimeoutSeconds };\n }\n return task;\n }\n\n private async applyAuth(\n request: TaskRequest,\n httpRequest: protos.google.cloud.tasks.v2.IHttpRequest,\n headers: Map<string, string>,\n ): Promise<void> {\n const mode = request.authMode;\n if (mode.kind === 'oidc') {\n httpRequest.oidcToken = {\n serviceAccountEmail: await getRuntimeServiceAccountEmail(),\n audience: request.targetUrl,\n };\n return;\n }\n if (mode.kind === 'shared-secret') {\n const secret = process.env[mode.secretEnv];\n if (secret) {\n headers.set('x-webpieces-shared-secret', secret);\n httpRequest.headers = Object.fromEntries(headers);\n }\n }\n }\n}\n"]}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { TaskInvoker, TaskRequest, JobReference } from './TaskTypes';
|
|
2
|
+
/**
|
|
3
|
+
* TaskInvoker for tests + local dev — the in-memory-queue twin of GcpTaskInvoker
|
|
4
|
+
* (a TypeScript port of webpieces-java's LocalRemoteInvoker).
|
|
5
|
+
*
|
|
6
|
+
* Instead of enqueuing to Google Cloud Tasks, it drops the delivery onto an in-process
|
|
7
|
+
* timer queue and returns IMMEDIATELY (breaking from the caller, exactly like handing a
|
|
8
|
+
* task to Cloud Tasks). The queued job then delivers the task the SAME way real Cloud
|
|
9
|
+
* Tasks does: a plain HTTP POST to `targetUrl + path` with the JSON body and the
|
|
10
|
+
* synthesized delivery auth (OIDC bearer / shared-secret). It does NOT invoke the server
|
|
11
|
+
* in-process — the target may be a different process entirely — so the request travels
|
|
12
|
+
* over real HTTP (e.g. http://localhost:{port}/queue/path) and is served by the target's
|
|
13
|
+
* real routing + filter chain, giving production parity without GCP.
|
|
14
|
+
*
|
|
15
|
+
* Bind it (via appOverrides) in place of GcpTaskInvoker:
|
|
16
|
+
* bind(TaskInvoker).to(InMemoryTaskInvoker)
|
|
17
|
+
*/
|
|
18
|
+
export declare class InMemoryTaskInvoker extends TaskInvoker {
|
|
19
|
+
private counter;
|
|
20
|
+
/** Scheduled (not-yet-delivered) jobs, so delete() can cancel them. */
|
|
21
|
+
private readonly pending;
|
|
22
|
+
enqueue(request: TaskRequest): Promise<JobReference>;
|
|
23
|
+
delete(ref: JobReference): Promise<void>;
|
|
24
|
+
/** ms until the scheduled run time (0 = as soon as possible / already due). */
|
|
25
|
+
private computeDelayMs;
|
|
26
|
+
/** Deliver the queued task over real HTTP, mirroring Cloud Tasks' HTTP callback. */
|
|
27
|
+
private deliver;
|
|
28
|
+
/** content-type + propagated context headers + synthesized delivery credential. */
|
|
29
|
+
private buildHeaders;
|
|
30
|
+
/** Synthesize the delivery credential Cloud Tasks would attach, per the auth mode. */
|
|
31
|
+
private attachAuth;
|
|
32
|
+
}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.InMemoryTaskInvoker = void 0;
|
|
4
|
+
const tslib_1 = require("tslib");
|
|
5
|
+
const inversify_1 = require("inversify");
|
|
6
|
+
const core_context_1 = require("@webpieces/core-context");
|
|
7
|
+
const gcp_identity_1 = require("@webpieces/gcp-identity");
|
|
8
|
+
const core_util_1 = require("@webpieces/core-util");
|
|
9
|
+
const TaskTypes_1 = require("./TaskTypes");
|
|
10
|
+
const log = core_util_1.LogManager.getLogger('InMemoryTaskInvoker');
|
|
11
|
+
/**
|
|
12
|
+
* TaskInvoker for tests + local dev — the in-memory-queue twin of GcpTaskInvoker
|
|
13
|
+
* (a TypeScript port of webpieces-java's LocalRemoteInvoker).
|
|
14
|
+
*
|
|
15
|
+
* Instead of enqueuing to Google Cloud Tasks, it drops the delivery onto an in-process
|
|
16
|
+
* timer queue and returns IMMEDIATELY (breaking from the caller, exactly like handing a
|
|
17
|
+
* task to Cloud Tasks). The queued job then delivers the task the SAME way real Cloud
|
|
18
|
+
* Tasks does: a plain HTTP POST to `targetUrl + path` with the JSON body and the
|
|
19
|
+
* synthesized delivery auth (OIDC bearer / shared-secret). It does NOT invoke the server
|
|
20
|
+
* in-process — the target may be a different process entirely — so the request travels
|
|
21
|
+
* over real HTTP (e.g. http://localhost:{port}/queue/path) and is served by the target's
|
|
22
|
+
* real routing + filter chain, giving production parity without GCP.
|
|
23
|
+
*
|
|
24
|
+
* Bind it (via appOverrides) in place of GcpTaskInvoker:
|
|
25
|
+
* bind(TaskInvoker).to(InMemoryTaskInvoker)
|
|
26
|
+
*/
|
|
27
|
+
let InMemoryTaskInvoker = class InMemoryTaskInvoker extends TaskTypes_1.TaskInvoker {
|
|
28
|
+
counter = 0;
|
|
29
|
+
/** Scheduled (not-yet-delivered) jobs, so delete() can cancel them. */
|
|
30
|
+
pending = new Map();
|
|
31
|
+
async enqueue(request) {
|
|
32
|
+
this.counter += 1;
|
|
33
|
+
const taskId = request.scheduleInfo.dedupName ?? `inmem-${request.queueName}-${this.counter}`;
|
|
34
|
+
const delayMs = this.computeDelayMs(request.scheduleInfo.epochMsToRunAt);
|
|
35
|
+
// Break from the caller: queue the HTTP delivery and return the reference NOW.
|
|
36
|
+
const timer = setTimeout(() => {
|
|
37
|
+
this.pending.delete(taskId);
|
|
38
|
+
void this.deliver(request, taskId);
|
|
39
|
+
}, delayMs);
|
|
40
|
+
// A queued task must not keep the process alive on its own.
|
|
41
|
+
timer.unref?.();
|
|
42
|
+
this.pending.set(taskId, timer);
|
|
43
|
+
log.debug(`queued local task ${taskId} -> ${request.targetUrl}${request.path} (delay ${delayMs}ms)`);
|
|
44
|
+
return new TaskTypes_1.JobReference(taskId);
|
|
45
|
+
}
|
|
46
|
+
async delete(ref) {
|
|
47
|
+
const timer = this.pending.get(ref.taskId);
|
|
48
|
+
if (timer) {
|
|
49
|
+
clearTimeout(timer);
|
|
50
|
+
this.pending.delete(ref.taskId);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
/** ms until the scheduled run time (0 = as soon as possible / already due). */
|
|
54
|
+
computeDelayMs(epochMsToRunAt) {
|
|
55
|
+
if (epochMsToRunAt === undefined) {
|
|
56
|
+
return 0;
|
|
57
|
+
}
|
|
58
|
+
const delay = epochMsToRunAt - Date.now();
|
|
59
|
+
return delay > 0 ? delay : 0;
|
|
60
|
+
}
|
|
61
|
+
/** Deliver the queued task over real HTTP, mirroring Cloud Tasks' HTTP callback. */
|
|
62
|
+
async deliver(request, taskId) {
|
|
63
|
+
const url = `${request.targetUrl}${request.path}`;
|
|
64
|
+
const headers = await this.buildHeaders(request);
|
|
65
|
+
log.info(`delivering local task ${taskId}: POST ${url}`);
|
|
66
|
+
// eslint-disable-next-line @webpieces/no-unmanaged-exceptions -- detached queue job: log delivery failure, never crash the queue
|
|
67
|
+
try {
|
|
68
|
+
// webpieces-disable no-fetch -- in-memory queue delivers over real HTTP like Cloud Tasks (no server to invoke in-process)
|
|
69
|
+
const response = await fetch(url, {
|
|
70
|
+
method: 'POST',
|
|
71
|
+
headers: headers,
|
|
72
|
+
body: JSON.stringify(request.body ?? {}),
|
|
73
|
+
});
|
|
74
|
+
if (!response.ok) {
|
|
75
|
+
log.error(`local task ${taskId} delivery to ${url} failed: HTTP ${response.status}`);
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
log.debug(`local task ${taskId} delivered: HTTP ${response.status}`);
|
|
79
|
+
}
|
|
80
|
+
catch (err) {
|
|
81
|
+
const error = (0, core_util_1.toError)(err);
|
|
82
|
+
log.error(`local task ${taskId} delivery to ${url} threw: ${error.message}`);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
/** content-type + propagated context headers + synthesized delivery credential. */
|
|
86
|
+
async buildHeaders(request) {
|
|
87
|
+
const headers = { 'content-type': 'application/json' };
|
|
88
|
+
for (const entry of request.contextHeaders.entries()) {
|
|
89
|
+
headers[entry[0]] = entry[1];
|
|
90
|
+
}
|
|
91
|
+
await this.attachAuth(request, headers);
|
|
92
|
+
return headers;
|
|
93
|
+
}
|
|
94
|
+
/** Synthesize the delivery credential Cloud Tasks would attach, per the auth mode. */
|
|
95
|
+
async attachAuth(request, headers) {
|
|
96
|
+
const mode = request.authMode;
|
|
97
|
+
if (mode.kind === 'oidc') {
|
|
98
|
+
headers['authorization'] = `Bearer ${await (0, gcp_identity_1.mintIdToken)(request.targetUrl)}`;
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
if (mode.kind === 'shared-secret') {
|
|
102
|
+
const secret = process.env[mode.secretEnv];
|
|
103
|
+
if (secret) {
|
|
104
|
+
headers['x-webpieces-shared-secret'] = secret;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
// public / jwt → no service credential is synthesized.
|
|
108
|
+
}
|
|
109
|
+
};
|
|
110
|
+
exports.InMemoryTaskInvoker = InMemoryTaskInvoker;
|
|
111
|
+
exports.InMemoryTaskInvoker = InMemoryTaskInvoker = tslib_1.__decorate([
|
|
112
|
+
(0, core_context_1.provideSingleton)(),
|
|
113
|
+
(0, inversify_1.injectable)()
|
|
114
|
+
], InMemoryTaskInvoker);
|
|
115
|
+
//# sourceMappingURL=InMemoryTaskInvoker.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"InMemoryTaskInvoker.js","sourceRoot":"","sources":["../../../../../packages/cloud/cloudtasks-client/src/InMemoryTaskInvoker.ts"],"names":[],"mappings":"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","sourcesContent":["import { injectable } from 'inversify';\nimport { provideSingleton } from '@webpieces/core-context';\nimport { mintIdToken } from '@webpieces/gcp-identity';\nimport { LogManager, toError } from '@webpieces/core-util';\nimport { TaskInvoker, TaskRequest, JobReference } from './TaskTypes';\n\nconst log = LogManager.getLogger('InMemoryTaskInvoker');\n\n/**\n * TaskInvoker for tests + local dev — the in-memory-queue twin of GcpTaskInvoker\n * (a TypeScript port of webpieces-java's LocalRemoteInvoker).\n *\n * Instead of enqueuing to Google Cloud Tasks, it drops the delivery onto an in-process\n * timer queue and returns IMMEDIATELY (breaking from the caller, exactly like handing a\n * task to Cloud Tasks). The queued job then delivers the task the SAME way real Cloud\n * Tasks does: a plain HTTP POST to `targetUrl + path` with the JSON body and the\n * synthesized delivery auth (OIDC bearer / shared-secret). It does NOT invoke the server\n * in-process — the target may be a different process entirely — so the request travels\n * over real HTTP (e.g. http://localhost:{port}/queue/path) and is served by the target's\n * real routing + filter chain, giving production parity without GCP.\n *\n * Bind it (via appOverrides) in place of GcpTaskInvoker:\n * bind(TaskInvoker).to(InMemoryTaskInvoker)\n */\n@provideSingleton()\n@injectable()\nexport class InMemoryTaskInvoker extends TaskInvoker {\n private counter = 0;\n /** Scheduled (not-yet-delivered) jobs, so delete() can cancel them. */\n private readonly pending = new Map<string, ReturnType<typeof setTimeout>>();\n\n override async enqueue(request: TaskRequest): Promise<JobReference> {\n this.counter += 1;\n const taskId = request.scheduleInfo.dedupName ?? `inmem-${request.queueName}-${this.counter}`;\n const delayMs = this.computeDelayMs(request.scheduleInfo.epochMsToRunAt);\n\n // Break from the caller: queue the HTTP delivery and return the reference NOW.\n const timer = setTimeout(() => {\n this.pending.delete(taskId);\n void this.deliver(request, taskId);\n }, delayMs);\n // A queued task must not keep the process alive on its own.\n timer.unref?.();\n this.pending.set(taskId, timer);\n\n log.debug(`queued local task ${taskId} -> ${request.targetUrl}${request.path} (delay ${delayMs}ms)`);\n return new JobReference(taskId);\n }\n\n override async delete(ref: JobReference): Promise<void> {\n const timer = this.pending.get(ref.taskId);\n if (timer) {\n clearTimeout(timer);\n this.pending.delete(ref.taskId);\n }\n }\n\n /** ms until the scheduled run time (0 = as soon as possible / already due). */\n private computeDelayMs(epochMsToRunAt?: number): number {\n if (epochMsToRunAt === undefined) {\n return 0;\n }\n const delay = epochMsToRunAt - Date.now();\n return delay > 0 ? delay : 0;\n }\n\n /** Deliver the queued task over real HTTP, mirroring Cloud Tasks' HTTP callback. */\n private async deliver(request: TaskRequest, taskId: string): Promise<void> {\n const url = `${request.targetUrl}${request.path}`;\n const headers = await this.buildHeaders(request);\n log.info(`delivering local task ${taskId}: POST ${url}`);\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions -- detached queue job: log delivery failure, never crash the queue\n try {\n // webpieces-disable no-fetch -- in-memory queue delivers over real HTTP like Cloud Tasks (no server to invoke in-process)\n const response = await fetch(url, {\n method: 'POST',\n headers: headers,\n body: JSON.stringify(request.body ?? {}),\n });\n if (!response.ok) {\n log.error(`local task ${taskId} delivery to ${url} failed: HTTP ${response.status}`);\n return;\n }\n log.debug(`local task ${taskId} delivered: HTTP ${response.status}`);\n } catch (err: unknown) {\n const error = toError(err);\n log.error(`local task ${taskId} delivery to ${url} threw: ${error.message}`);\n }\n }\n\n /** content-type + propagated context headers + synthesized delivery credential. */\n private async buildHeaders(request: TaskRequest): Promise<Record<string, string>> {\n const headers: Record<string, string> = { 'content-type': 'application/json' };\n for (const entry of request.contextHeaders.entries()) {\n headers[entry[0]] = entry[1];\n }\n await this.attachAuth(request, headers);\n return headers;\n }\n\n /** Synthesize the delivery credential Cloud Tasks would attach, per the auth mode. */\n private async attachAuth(request: TaskRequest, headers: Record<string, string>): Promise<void> {\n const mode = request.authMode;\n if (mode.kind === 'oidc') {\n headers['authorization'] = `Bearer ${await mintIdToken(request.targetUrl)}`;\n return;\n }\n if (mode.kind === 'shared-secret') {\n const secret = process.env[mode.secretEnv];\n if (secret) {\n headers['x-webpieces-shared-secret'] = secret;\n }\n }\n // public / jwt → no service credential is synthesized.\n }\n}\n"]}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { ScheduleInfo, JobReference } from './TaskTypes';
|
|
2
|
+
/**
|
|
3
|
+
* The scheduler→proxy bridge for the current async scope. The CloudTaskScheduler
|
|
4
|
+
* sets a frame around a runnable; the enqueue proxy reads its ScheduleInfo and, after
|
|
5
|
+
* enqueuing, writes back the JobReference. Stored in the request's AsyncLocalStorage
|
|
6
|
+
* frame so it never leaks across requests.
|
|
7
|
+
*/
|
|
8
|
+
export declare class ScheduleFrame {
|
|
9
|
+
info: ScheduleInfo;
|
|
10
|
+
jobRef?: JobReference;
|
|
11
|
+
constructor(info: ScheduleInfo);
|
|
12
|
+
}
|
|
13
|
+
/** Install a schedule frame for the current scope (called by the scheduler). */
|
|
14
|
+
export declare function setScheduleFrame(frame: ScheduleFrame): void;
|
|
15
|
+
/** The active schedule frame, or undefined if no scheduler lambda is running. */
|
|
16
|
+
export declare function currentScheduleFrame(): ScheduleFrame | undefined;
|
|
17
|
+
/** Remove the schedule frame (called by the scheduler in a finally). */
|
|
18
|
+
export declare function clearScheduleFrame(): void;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.ScheduleFrame = void 0;
|
|
4
|
+
exports.setScheduleFrame = setScheduleFrame;
|
|
5
|
+
exports.currentScheduleFrame = currentScheduleFrame;
|
|
6
|
+
exports.clearScheduleFrame = clearScheduleFrame;
|
|
7
|
+
const core_context_1 = require("@webpieces/core-context");
|
|
8
|
+
/**
|
|
9
|
+
* The scheduler→proxy bridge for the current async scope. The CloudTaskScheduler
|
|
10
|
+
* sets a frame around a runnable; the enqueue proxy reads its ScheduleInfo and, after
|
|
11
|
+
* enqueuing, writes back the JobReference. Stored in the request's AsyncLocalStorage
|
|
12
|
+
* frame so it never leaks across requests.
|
|
13
|
+
*/
|
|
14
|
+
class ScheduleFrame {
|
|
15
|
+
info;
|
|
16
|
+
jobRef;
|
|
17
|
+
constructor(info) {
|
|
18
|
+
this.info = info;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
exports.ScheduleFrame = ScheduleFrame;
|
|
22
|
+
/** Non-transferrable context key for the active schedule frame. */
|
|
23
|
+
const SCHEDULE_FRAME_KEY = '__webpieces_schedule_frame';
|
|
24
|
+
/** Install a schedule frame for the current scope (called by the scheduler). */
|
|
25
|
+
function setScheduleFrame(frame) {
|
|
26
|
+
core_context_1.RequestContext.put(SCHEDULE_FRAME_KEY, frame);
|
|
27
|
+
}
|
|
28
|
+
/** The active schedule frame, or undefined if no scheduler lambda is running. */
|
|
29
|
+
function currentScheduleFrame() {
|
|
30
|
+
return core_context_1.RequestContext.get(SCHEDULE_FRAME_KEY);
|
|
31
|
+
}
|
|
32
|
+
/** Remove the schedule frame (called by the scheduler in a finally). */
|
|
33
|
+
function clearScheduleFrame() {
|
|
34
|
+
core_context_1.RequestContext.remove(SCHEDULE_FRAME_KEY);
|
|
35
|
+
}
|
|
36
|
+
//# sourceMappingURL=ScheduleContext.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ScheduleContext.js","sourceRoot":"","sources":["../../../../../packages/cloud/cloudtasks-client/src/ScheduleContext.ts"],"names":[],"mappings":";;;AAsBA,4CAEC;AAGD,oDAEC;AAGD,gDAEC;AAlCD,0DAAyD;AAGzD;;;;;GAKG;AACH,MAAa,aAAa;IACtB,IAAI,CAAe;IACnB,MAAM,CAAgB;IAEtB,YAAY,IAAkB;QAC1B,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC;IACrB,CAAC;CACJ;AAPD,sCAOC;AAED,mEAAmE;AACnE,MAAM,kBAAkB,GAAG,4BAA4B,CAAC;AAExD,gFAAgF;AAChF,SAAgB,gBAAgB,CAAC,KAAoB;IACjD,6BAAc,CAAC,GAAG,CAAC,kBAAkB,EAAE,KAAK,CAAC,CAAC;AAClD,CAAC;AAED,iFAAiF;AACjF,SAAgB,oBAAoB;IAChC,OAAO,6BAAc,CAAC,GAAG,CAAgB,kBAAkB,CAAC,CAAC;AACjE,CAAC;AAED,wEAAwE;AACxE,SAAgB,kBAAkB;IAC9B,6BAAc,CAAC,MAAM,CAAC,kBAAkB,CAAC,CAAC;AAC9C,CAAC","sourcesContent":["import { RequestContext } from '@webpieces/core-context';\nimport { ScheduleInfo, JobReference } from './TaskTypes';\n\n/**\n * The scheduler→proxy bridge for the current async scope. The CloudTaskScheduler\n * sets a frame around a runnable; the enqueue proxy reads its ScheduleInfo and, after\n * enqueuing, writes back the JobReference. Stored in the request's AsyncLocalStorage\n * frame so it never leaks across requests.\n */\nexport class ScheduleFrame {\n info: ScheduleInfo;\n jobRef?: JobReference;\n\n constructor(info: ScheduleInfo) {\n this.info = info;\n }\n}\n\n/** Non-transferrable context key for the active schedule frame. */\nconst SCHEDULE_FRAME_KEY = '__webpieces_schedule_frame';\n\n/** Install a schedule frame for the current scope (called by the scheduler). */\nexport function setScheduleFrame(frame: ScheduleFrame): void {\n RequestContext.put(SCHEDULE_FRAME_KEY, frame);\n}\n\n/** The active schedule frame, or undefined if no scheduler lambda is running. */\nexport function currentScheduleFrame(): ScheduleFrame | undefined {\n return RequestContext.get<ScheduleFrame>(SCHEDULE_FRAME_KEY);\n}\n\n/** Remove the schedule frame (called by the scheduler in a finally). */\nexport function clearScheduleFrame(): void {\n RequestContext.remove(SCHEDULE_FRAME_KEY);\n}\n"]}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { HeaderRegistry } from '@webpieces/core-util';
|
|
2
|
+
import { TaskInvoker } from './TaskTypes';
|
|
3
|
+
/** Constructor whose prototype is T (the abstract @PubSub API class). */
|
|
4
|
+
type ApiPrototype<T> = Function & {
|
|
5
|
+
prototype: T;
|
|
6
|
+
};
|
|
7
|
+
/**
|
|
8
|
+
* Injectable factory for Cloud Tasks enqueue clients — the twin of http-client's
|
|
9
|
+
* RpcClientCreator. Resolves the bound TaskInvoker + a context-propagating ContextMgr
|
|
10
|
+
* so a service just asks for a typed client:
|
|
11
|
+
*
|
|
12
|
+
* const emailTasks = creator.createClientOnService(EmailApi, 'email-svc'); // self/other svc
|
|
13
|
+
*/
|
|
14
|
+
export declare class TaskClientCreator {
|
|
15
|
+
private readonly invoker;
|
|
16
|
+
private readonly registry;
|
|
17
|
+
constructor(invoker: TaskInvoker, registry: HeaderRegistry);
|
|
18
|
+
/** Enqueue client whose delivery URL is another Cloud Run service (by name). */
|
|
19
|
+
createClientOnService<T extends object>(apiClass: ApiPrototype<T>, serviceName: string): T;
|
|
20
|
+
/** Enqueue client whose delivery URL is a fixed base URL. */
|
|
21
|
+
createClientOnUrl<T extends object>(apiClass: ApiPrototype<T>, url: string): T;
|
|
22
|
+
private buildContextMgr;
|
|
23
|
+
}
|
|
24
|
+
export {};
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.TaskClientCreator = void 0;
|
|
4
|
+
const tslib_1 = require("tslib");
|
|
5
|
+
const inversify_1 = require("inversify");
|
|
6
|
+
const core_context_1 = require("@webpieces/core-context");
|
|
7
|
+
const core_util_1 = require("@webpieces/core-util");
|
|
8
|
+
const gcp_identity_1 = require("@webpieces/gcp-identity");
|
|
9
|
+
const TaskTypes_1 = require("./TaskTypes");
|
|
10
|
+
const TaskClientFactory_1 = require("./TaskClientFactory");
|
|
11
|
+
/**
|
|
12
|
+
* Injectable factory for Cloud Tasks enqueue clients — the twin of http-client's
|
|
13
|
+
* RpcClientCreator. Resolves the bound TaskInvoker + a context-propagating ContextMgr
|
|
14
|
+
* so a service just asks for a typed client:
|
|
15
|
+
*
|
|
16
|
+
* const emailTasks = creator.createClientOnService(EmailApi, 'email-svc'); // self/other svc
|
|
17
|
+
*/
|
|
18
|
+
let TaskClientCreator = class TaskClientCreator {
|
|
19
|
+
invoker;
|
|
20
|
+
registry;
|
|
21
|
+
constructor(invoker, registry) {
|
|
22
|
+
this.invoker = invoker;
|
|
23
|
+
this.registry = registry;
|
|
24
|
+
}
|
|
25
|
+
/** Enqueue client whose delivery URL is another Cloud Run service (by name). */
|
|
26
|
+
createClientOnService(apiClass, serviceName) {
|
|
27
|
+
const config = new TaskClientFactory_1.TaskClientConfig(() => (0, gcp_identity_1.getCloudRunUrl)(serviceName), this.invoker, this.buildContextMgr());
|
|
28
|
+
return (0, TaskClientFactory_1.createTaskClient)(apiClass, config);
|
|
29
|
+
}
|
|
30
|
+
/** Enqueue client whose delivery URL is a fixed base URL. */
|
|
31
|
+
createClientOnUrl(apiClass, url) {
|
|
32
|
+
const config = new TaskClientFactory_1.TaskClientConfig(url, this.invoker, this.buildContextMgr());
|
|
33
|
+
return (0, TaskClientFactory_1.createTaskClient)(apiClass, config);
|
|
34
|
+
}
|
|
35
|
+
buildContextMgr() {
|
|
36
|
+
return new core_context_1.ContextMgr(new core_context_1.RequestContextReader(), this.registry);
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
exports.TaskClientCreator = TaskClientCreator;
|
|
40
|
+
exports.TaskClientCreator = TaskClientCreator = tslib_1.__decorate([
|
|
41
|
+
(0, core_context_1.provideSingleton)(),
|
|
42
|
+
(0, inversify_1.injectable)(),
|
|
43
|
+
tslib_1.__param(0, (0, inversify_1.inject)(TaskTypes_1.TaskInvoker)),
|
|
44
|
+
tslib_1.__param(1, (0, inversify_1.inject)(core_util_1.HeaderRegistry)),
|
|
45
|
+
tslib_1.__metadata("design:paramtypes", [TaskTypes_1.TaskInvoker,
|
|
46
|
+
core_util_1.HeaderRegistry])
|
|
47
|
+
], TaskClientCreator);
|
|
48
|
+
//# sourceMappingURL=TaskClientCreator.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"TaskClientCreator.js","sourceRoot":"","sources":["../../../../../packages/cloud/cloudtasks-client/src/TaskClientCreator.ts"],"names":[],"mappings":";;;;AAAA,yCAA+C;AAC/C,0DAA6F;AAC7F,oDAAsD;AACtD,0DAAyD;AACzD,2CAA0C;AAC1C,2DAAyE;AAKzE;;;;;;GAMG;AAGI,IAAM,iBAAiB,GAAvB,MAAM,iBAAiB;IAEgB;IACG;IAF7C,YAC0C,OAAoB,EACjB,QAAwB;QAD3B,YAAO,GAAP,OAAO,CAAa;QACjB,aAAQ,GAAR,QAAQ,CAAgB;IAClE,CAAC;IAEJ,gFAAgF;IAChF,qBAAqB,CAAmB,QAAyB,EAAE,WAAmB;QAClF,MAAM,MAAM,GAAG,IAAI,oCAAgB,CAC/B,GAAG,EAAE,CAAC,IAAA,6BAAc,EAAC,WAAW,CAAC,EACjC,IAAI,CAAC,OAAO,EACZ,IAAI,CAAC,eAAe,EAAE,CACzB,CAAC;QACF,OAAO,IAAA,oCAAgB,EAAC,QAAQ,EAAE,MAAM,CAAC,CAAC;IAC9C,CAAC;IAED,6DAA6D;IAC7D,iBAAiB,CAAmB,QAAyB,EAAE,GAAW;QACtE,MAAM,MAAM,GAAG,IAAI,oCAAgB,CAAC,GAAG,EAAE,IAAI,CAAC,OAAO,EAAE,IAAI,CAAC,eAAe,EAAE,CAAC,CAAC;QAC/E,OAAO,IAAA,oCAAgB,EAAC,QAAQ,EAAE,MAAM,CAAC,CAAC;IAC9C,CAAC;IAEO,eAAe;QACnB,OAAO,IAAI,yBAAU,CAAC,IAAI,mCAAoB,EAAE,EAAE,IAAI,CAAC,QAAQ,CAAC,CAAC;IACrE,CAAC;CACJ,CAAA;AAzBY,8CAAiB;4BAAjB,iBAAiB;IAF7B,IAAA,+BAAgB,GAAE;IAClB,IAAA,sBAAU,GAAE;IAGJ,mBAAA,IAAA,kBAAM,EAAC,uBAAW,CAAC,CAAA;IACnB,mBAAA,IAAA,kBAAM,EAAC,0BAAc,CAAC,CAAA;6CADwB,uBAAW;QACP,0BAAc;GAH5D,iBAAiB,CAyB7B","sourcesContent":["import { inject, injectable } from 'inversify';\nimport { provideSingleton, RequestContextReader, ContextMgr } from '@webpieces/core-context';\nimport { HeaderRegistry } from '@webpieces/core-util';\nimport { getCloudRunUrl } from '@webpieces/gcp-identity';\nimport { TaskInvoker } from './TaskTypes';\nimport { createTaskClient, TaskClientConfig } from './TaskClientFactory';\n\n/** Constructor whose prototype is T (the abstract @PubSub API class). */\ntype ApiPrototype<T> = Function & { prototype: T };\n\n/**\n * Injectable factory for Cloud Tasks enqueue clients — the twin of http-client's\n * RpcClientCreator. Resolves the bound TaskInvoker + a context-propagating ContextMgr\n * so a service just asks for a typed client:\n *\n * const emailTasks = creator.createClientOnService(EmailApi, 'email-svc'); // self/other svc\n */\n@provideSingleton()\n@injectable()\nexport class TaskClientCreator {\n constructor(\n @inject(TaskInvoker) private readonly invoker: TaskInvoker,\n @inject(HeaderRegistry) private readonly registry: HeaderRegistry,\n ) {}\n\n /** Enqueue client whose delivery URL is another Cloud Run service (by name). */\n createClientOnService<T extends object>(apiClass: ApiPrototype<T>, serviceName: string): T {\n const config = new TaskClientConfig(\n () => getCloudRunUrl(serviceName),\n this.invoker,\n this.buildContextMgr(),\n );\n return createTaskClient(apiClass, config);\n }\n\n /** Enqueue client whose delivery URL is a fixed base URL. */\n createClientOnUrl<T extends object>(apiClass: ApiPrototype<T>, url: string): T {\n const config = new TaskClientConfig(url, this.invoker, this.buildContextMgr());\n return createTaskClient(apiClass, config);\n }\n\n private buildContextMgr(): ContextMgr {\n return new ContextMgr(new RequestContextReader(), this.registry);\n }\n}\n"]}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { ContextMgr } from '@webpieces/core-context';
|
|
2
|
+
import { TaskInvoker } from './TaskTypes';
|
|
3
|
+
/** Constructor whose prototype is T (the abstract API class). */
|
|
4
|
+
type ApiPrototype<T> = Function & {
|
|
5
|
+
prototype: T;
|
|
6
|
+
};
|
|
7
|
+
/** Configuration for an enqueue client. */
|
|
8
|
+
export declare class TaskClientConfig {
|
|
9
|
+
/** Callee base URL, or an async resolver (e.g. getCloudRunUrl(serviceName)). */
|
|
10
|
+
targetUrl: string | (() => Promise<string>);
|
|
11
|
+
/** The transport that enqueues the task (GcpTaskInvoker / InMemoryTaskInvoker). */
|
|
12
|
+
invoker: TaskInvoker;
|
|
13
|
+
/** Optional context propagation (txId/requestId/tenant…) onto the task headers. */
|
|
14
|
+
contextMgr?: ContextMgr;
|
|
15
|
+
constructor(targetUrl: string | (() => Promise<string>), invoker: TaskInvoker, contextMgr?: ContextMgr);
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Create a Cloud Tasks enqueue client from a shared @PubSub API contract. Calling a
|
|
19
|
+
* method ENQUEUES a task (it does not call remotely); the task is later delivered to
|
|
20
|
+
* the same endpoint's controller through the full server filter chain.
|
|
21
|
+
*
|
|
22
|
+
* Must be called inside a CloudTaskScheduler lambda (which supplies the ScheduleInfo)
|
|
23
|
+
* within an active RequestContext, e.g.:
|
|
24
|
+
* scheduler.addToQueue(() => taskClient.foo(req), { dedupName });
|
|
25
|
+
*/
|
|
26
|
+
export declare function createTaskClient<T extends object>(apiClass: ApiPrototype<T>, config: TaskClientConfig): T;
|
|
27
|
+
export {};
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.TaskClientConfig = void 0;
|
|
4
|
+
exports.createTaskClient = createTaskClient;
|
|
5
|
+
const core_util_1 = require("@webpieces/core-util");
|
|
6
|
+
const core_util_2 = require("@webpieces/core-util");
|
|
7
|
+
const TaskTypes_1 = require("./TaskTypes");
|
|
8
|
+
const ScheduleContext_1 = require("./ScheduleContext");
|
|
9
|
+
const log = core_util_2.LogManager.getLogger('TaskClientFactory');
|
|
10
|
+
/**
|
|
11
|
+
* Auth headers are NEVER propagated from the caller's context onto an enqueued task:
|
|
12
|
+
* the caller's inbound user JWT / secret must not leak to an internal service, and
|
|
13
|
+
* the invoker mints fresh delivery auth (OIDC / shared-secret) per the endpoint's mode.
|
|
14
|
+
*/
|
|
15
|
+
const AUTH_HEADER_NAMES = new Set(['authorization', 'x-webpieces-shared-secret']);
|
|
16
|
+
/**
|
|
17
|
+
* Properties DI frameworks / Promise checks / serializers probe on the proxy; return
|
|
18
|
+
* undefined instead of treating them as endpoints. Mirrors http-client's ClientFactory.
|
|
19
|
+
*/
|
|
20
|
+
const FRAMEWORK_INSPECTION_PROPERTIES = new Set([
|
|
21
|
+
'constructor', 'prototype', '__proto__', 'name', 'then', 'catch', 'finally',
|
|
22
|
+
'toJSON', 'valueOf', 'toString', 'nodeType', 'tagName', '$$typeof',
|
|
23
|
+
]);
|
|
24
|
+
/** Configuration for an enqueue client. */
|
|
25
|
+
class TaskClientConfig {
|
|
26
|
+
/** Callee base URL, or an async resolver (e.g. getCloudRunUrl(serviceName)). */
|
|
27
|
+
targetUrl;
|
|
28
|
+
/** The transport that enqueues the task (GcpTaskInvoker / InMemoryTaskInvoker). */
|
|
29
|
+
invoker;
|
|
30
|
+
/** Optional context propagation (txId/requestId/tenant…) onto the task headers. */
|
|
31
|
+
contextMgr;
|
|
32
|
+
constructor(targetUrl, invoker, contextMgr) {
|
|
33
|
+
this.targetUrl = targetUrl;
|
|
34
|
+
this.invoker = invoker;
|
|
35
|
+
this.contextMgr = contextMgr;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
exports.TaskClientConfig = TaskClientConfig;
|
|
39
|
+
/** Per-endpoint routing plan resolved once from the contract's decorators. */
|
|
40
|
+
class EndpointPlan {
|
|
41
|
+
path;
|
|
42
|
+
queueName;
|
|
43
|
+
authMode;
|
|
44
|
+
constructor(path, queueName, authMode) {
|
|
45
|
+
this.path = path;
|
|
46
|
+
this.queueName = queueName;
|
|
47
|
+
this.authMode = authMode;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Create a Cloud Tasks enqueue client from a shared @PubSub API contract. Calling a
|
|
52
|
+
* method ENQUEUES a task (it does not call remotely); the task is later delivered to
|
|
53
|
+
* the same endpoint's controller through the full server filter chain.
|
|
54
|
+
*
|
|
55
|
+
* Must be called inside a CloudTaskScheduler lambda (which supplies the ScheduleInfo)
|
|
56
|
+
* within an active RequestContext, e.g.:
|
|
57
|
+
* scheduler.addToQueue(() => taskClient.foo(req), { dedupName });
|
|
58
|
+
*/
|
|
59
|
+
function createTaskClient(apiClass, config) {
|
|
60
|
+
if (!(0, core_util_1.isApiPath)(apiClass)) {
|
|
61
|
+
throw new Error(`Class ${apiClass.name || 'Unknown'} must be decorated with @ApiPath()`);
|
|
62
|
+
}
|
|
63
|
+
(0, core_util_1.assertPubSubConventions)(apiClass);
|
|
64
|
+
(0, core_util_1.assertEveryEndpointHasAuthMode)(apiClass);
|
|
65
|
+
const basePath = (0, core_util_1.getApiPath)(apiClass) ?? '';
|
|
66
|
+
const endpoints = (0, core_util_1.getEndpoints)(apiClass) ?? {};
|
|
67
|
+
const plans = buildPlans(apiClass, basePath, endpoints);
|
|
68
|
+
return new Proxy({}, {
|
|
69
|
+
// webpieces-disable no-any-unknown -- proxy get trap returns either a method or undefined
|
|
70
|
+
get(_target, prop) {
|
|
71
|
+
if (typeof prop !== 'string' || FRAMEWORK_INSPECTION_PROPERTIES.has(prop)) {
|
|
72
|
+
return undefined;
|
|
73
|
+
}
|
|
74
|
+
const plan = plans.get(prop);
|
|
75
|
+
if (!plan) {
|
|
76
|
+
throw new Error(`No @PubSub endpoint '${prop}' on ${apiClass.name || 'Unknown'}. ` +
|
|
77
|
+
`Check for typos or a missing @Endpoint() decorator.`);
|
|
78
|
+
}
|
|
79
|
+
// webpieces-disable no-any-unknown -- request DTO type is erased at the proxy layer
|
|
80
|
+
return (requestDto) => enqueue(config, plan, requestDto);
|
|
81
|
+
},
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
function buildPlans(apiClass, basePath, endpoints) {
|
|
85
|
+
const plans = new Map();
|
|
86
|
+
for (const methodName of Object.keys(endpoints)) {
|
|
87
|
+
const authMode = (0, core_util_1.getAuthMode)(apiClass, methodName);
|
|
88
|
+
if (!authMode) {
|
|
89
|
+
throw new Error(`Endpoint '${methodName}' on ${apiClass.name} has no auth mode`);
|
|
90
|
+
}
|
|
91
|
+
const plan = new EndpointPlan(basePath + endpoints[methodName], (0, core_util_1.getQueueName)(apiClass, methodName), authMode);
|
|
92
|
+
plans.set(methodName, plan);
|
|
93
|
+
}
|
|
94
|
+
return plans;
|
|
95
|
+
}
|
|
96
|
+
async function enqueue(config, plan,
|
|
97
|
+
// webpieces-disable no-any-unknown -- request DTO type is erased at the proxy layer
|
|
98
|
+
requestDto) {
|
|
99
|
+
const frame = (0, ScheduleContext_1.currentScheduleFrame)();
|
|
100
|
+
if (!frame) {
|
|
101
|
+
throw new Error('Cloud task enqueue must run inside a CloudTaskScheduler lambda, e.g. ' +
|
|
102
|
+
'scheduler.addToQueue(() => taskClient.method(req), { dedupName }).');
|
|
103
|
+
}
|
|
104
|
+
const targetUrl = typeof config.targetUrl === 'string'
|
|
105
|
+
? config.targetUrl
|
|
106
|
+
: await config.targetUrl();
|
|
107
|
+
const contextHeaders = buildContextHeaders(config.contextMgr);
|
|
108
|
+
const request = new TaskTypes_1.TaskRequest(targetUrl, plan.path, plan.queueName, requestDto, contextHeaders, plan.authMode, frame.info ?? new TaskTypes_1.ScheduleInfo());
|
|
109
|
+
log.debug(`enqueue task ${plan.queueName} -> ${targetUrl}${plan.path}`);
|
|
110
|
+
frame.jobRef = await config.invoker.enqueue(request);
|
|
111
|
+
}
|
|
112
|
+
function buildContextHeaders(contextMgr) {
|
|
113
|
+
const headers = new Map();
|
|
114
|
+
if (!contextMgr) {
|
|
115
|
+
return headers;
|
|
116
|
+
}
|
|
117
|
+
for (const entry of contextMgr.buildOutboundHeaders().entries()) {
|
|
118
|
+
if (!AUTH_HEADER_NAMES.has(entry[0].toLowerCase())) {
|
|
119
|
+
headers.set(entry[0], entry[1]);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
return headers;
|
|
123
|
+
}
|
|
124
|
+
//# sourceMappingURL=TaskClientFactory.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"TaskClientFactory.js","sourceRoot":"","sources":["../../../../../packages/cloud/cloudtasks-client/src/TaskClientFactory.ts"],"names":[],"mappings":"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","sourcesContent":["import {\n isApiPath,\n getApiPath,\n getEndpoints,\n getAuthMode,\n getQueueName,\n assertPubSubConventions,\n assertEveryEndpointHasAuthMode,\n AuthMode,\n} from '@webpieces/core-util';\nimport { ContextMgr } from '@webpieces/core-context';\nimport { LogManager } from '@webpieces/core-util';\nimport { TaskInvoker, TaskRequest, ScheduleInfo } from './TaskTypes';\nimport { currentScheduleFrame } from './ScheduleContext';\n\nconst log = LogManager.getLogger('TaskClientFactory');\n\n/** Constructor whose prototype is T (the abstract API class). */\ntype ApiPrototype<T> = Function & { prototype: T };\n\n/**\n * Auth headers are NEVER propagated from the caller's context onto an enqueued task:\n * the caller's inbound user JWT / secret must not leak to an internal service, and\n * the invoker mints fresh delivery auth (OIDC / shared-secret) per the endpoint's mode.\n */\nconst AUTH_HEADER_NAMES = new Set<string>(['authorization', 'x-webpieces-shared-secret']);\n\n/**\n * Properties DI frameworks / Promise checks / serializers probe on the proxy; return\n * undefined instead of treating them as endpoints. Mirrors http-client's ClientFactory.\n */\nconst FRAMEWORK_INSPECTION_PROPERTIES = new Set<string>([\n 'constructor', 'prototype', '__proto__', 'name', 'then', 'catch', 'finally',\n 'toJSON', 'valueOf', 'toString', 'nodeType', 'tagName', '$$typeof',\n]);\n\n/** Configuration for an enqueue client. */\nexport class TaskClientConfig {\n /** Callee base URL, or an async resolver (e.g. getCloudRunUrl(serviceName)). */\n targetUrl: string | (() => Promise<string>);\n /** The transport that enqueues the task (GcpTaskInvoker / InMemoryTaskInvoker). */\n invoker: TaskInvoker;\n /** Optional context propagation (txId/requestId/tenant…) onto the task headers. */\n contextMgr?: ContextMgr;\n\n constructor(\n targetUrl: string | (() => Promise<string>),\n invoker: TaskInvoker,\n contextMgr?: ContextMgr,\n ) {\n this.targetUrl = targetUrl;\n this.invoker = invoker;\n this.contextMgr = contextMgr;\n }\n}\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\n constructor(path: string, queueName: string, authMode: AuthMode) {\n this.path = path;\n this.queueName = queueName;\n this.authMode = authMode;\n }\n}\n\n/**\n * Create a Cloud Tasks enqueue client from a shared @PubSub API contract. Calling a\n * method 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 * Must be called inside a CloudTaskScheduler lambda (which supplies the ScheduleInfo)\n * within an active RequestContext, e.g.:\n * scheduler.addToQueue(() => taskClient.foo(req), { dedupName });\n */\nexport function createTaskClient<T extends object>(\n apiClass: ApiPrototype<T>,\n config: TaskClientConfig,\n): T {\n if (!isApiPath(apiClass)) {\n throw new Error(`Class ${apiClass.name || 'Unknown'} must be decorated with @ApiPath()`);\n }\n assertPubSubConventions(apiClass);\n assertEveryEndpointHasAuthMode(apiClass);\n\n const basePath = getApiPath(apiClass) ?? '';\n const endpoints = getEndpoints(apiClass) ?? {};\n const plans = buildPlans(apiClass, basePath, endpoints);\n\n return new Proxy({} as T, {\n // webpieces-disable no-any-unknown -- proxy get trap returns either a method or undefined\n get(_target: T, prop: string | symbol): unknown {\n if (typeof prop !== 'string' || FRAMEWORK_INSPECTION_PROPERTIES.has(prop)) {\n return undefined;\n }\n const plan = plans.get(prop);\n if (!plan) {\n throw new Error(\n `No @PubSub endpoint '${prop}' on ${apiClass.name || 'Unknown'}. ` +\n `Check for typos or a missing @Endpoint() decorator.`,\n );\n }\n // webpieces-disable no-any-unknown -- request DTO type is erased at the proxy layer\n return (requestDto: unknown): Promise<void> => enqueue(config, plan, requestDto);\n },\n });\n}\n\nfunction buildPlans(\n apiClass: Function,\n basePath: string,\n endpoints: Record<string, string>,\n): Map<string, EndpointPlan> {\n const plans = new Map<string, EndpointPlan>();\n for (const methodName of Object.keys(endpoints)) {\n const authMode = getAuthMode(apiClass, methodName);\n if (!authMode) {\n throw new Error(`Endpoint '${methodName}' on ${apiClass.name} has no auth mode`);\n }\n const plan = new EndpointPlan(\n basePath + endpoints[methodName],\n getQueueName(apiClass, methodName),\n authMode,\n );\n plans.set(methodName, plan);\n }\n return plans;\n}\n\nasync function enqueue(\n config: TaskClientConfig,\n plan: EndpointPlan,\n // webpieces-disable no-any-unknown -- request DTO type is erased at the proxy layer\n requestDto: unknown,\n): Promise<void> {\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), { dedupName }).',\n );\n }\n\n const targetUrl = typeof config.targetUrl === 'string'\n ? config.targetUrl\n : await config.targetUrl();\n\n const contextHeaders = buildContextHeaders(config.contextMgr);\n\n const request = new TaskRequest(\n targetUrl,\n plan.path,\n plan.queueName,\n requestDto,\n contextHeaders,\n plan.authMode,\n frame.info ?? new ScheduleInfo(),\n );\n\n log.debug(`enqueue task ${plan.queueName} -> ${targetUrl}${plan.path}`);\n frame.jobRef = await config.invoker.enqueue(request);\n}\n\nfunction buildContextHeaders(contextMgr?: ContextMgr): Map<string, string> {\n const headers = new Map<string, string>();\n if (!contextMgr) {\n return headers;\n }\n for (const entry of contextMgr.buildOutboundHeaders().entries()) {\n if (!AUTH_HEADER_NAMES.has(entry[0].toLowerCase())) {\n headers.set(entry[0], entry[1]);\n }\n }\n return headers;\n}\n"]}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { AuthMode } from '@webpieces/core-util';
|
|
2
|
+
/**
|
|
3
|
+
* Scheduling options for an enqueued task. Travels out-of-band (via the scheduler
|
|
4
|
+
* frame in RequestContext) so the shared API method signature stays `foo(req)` on
|
|
5
|
+
* both the client and the controller.
|
|
6
|
+
*/
|
|
7
|
+
export declare class ScheduleInfo {
|
|
8
|
+
/** Absolute epoch-millis to run at; omitted = run as soon as possible. */
|
|
9
|
+
epochMsToRunAt?: number;
|
|
10
|
+
/** Per-task dispatch deadline in seconds. */
|
|
11
|
+
taskTimeoutSeconds?: number;
|
|
12
|
+
/**
|
|
13
|
+
* Deterministic dedup name → the Cloud Task resource name. A second enqueue with
|
|
14
|
+
* the same name is rejected ALREADY_EXISTS (treated as success = idempotent).
|
|
15
|
+
*/
|
|
16
|
+
dedupName?: string;
|
|
17
|
+
constructor(epochMsToRunAt?: number, taskTimeoutSeconds?: number, dedupName?: string);
|
|
18
|
+
}
|
|
19
|
+
/** Handle to an enqueued task (its Cloud Tasks id), returned by the scheduler. */
|
|
20
|
+
export declare class JobReference {
|
|
21
|
+
taskId: string;
|
|
22
|
+
constructor(taskId: string);
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Everything an invoker needs to enqueue one task. Built by the enqueue proxy from
|
|
26
|
+
* the shared @PubSub contract's decorators + the ambient RequestContext.
|
|
27
|
+
*/
|
|
28
|
+
export declare class TaskRequest {
|
|
29
|
+
/** Callee base URL (e.g. https://email-svc-123.us-central1.run.app). */
|
|
30
|
+
targetUrl: string;
|
|
31
|
+
/** Endpoint path the task is delivered to (basePath + endpoint, e.g. /email/send). */
|
|
32
|
+
path: string;
|
|
33
|
+
/** Cloud Tasks queue name (getQueueName: @Queue override or `${Api}-${method}`). */
|
|
34
|
+
queueName: string;
|
|
35
|
+
/** The request DTO (serialized to the POST body on delivery). */
|
|
36
|
+
body: unknown;
|
|
37
|
+
/** Context headers to propagate (txId/requestId/tenant…), already resolved. */
|
|
38
|
+
contextHeaders: Map<string, string>;
|
|
39
|
+
/** The endpoint's auth mode — how the invoker authenticates delivery. */
|
|
40
|
+
authMode: AuthMode;
|
|
41
|
+
/** Scheduling options (dedup name, run-at, timeout). */
|
|
42
|
+
scheduleInfo: ScheduleInfo;
|
|
43
|
+
constructor(targetUrl: string, path: string, queueName: string, body: unknown, contextHeaders: Map<string, string>, authMode: AuthMode, scheduleInfo: ScheduleInfo);
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* The transport that actually enqueues a task. Abstract class so it doubles as the
|
|
47
|
+
* inversify DI token (inject by type, no Symbol). Bind GcpTaskInvoker in prod or
|
|
48
|
+
* InMemoryTaskInvoker in tests/local dev. Both deliver a task the same way — a plain
|
|
49
|
+
* HTTP POST to `targetUrl + path` — GcpTaskInvoker via Google Cloud Tasks, and
|
|
50
|
+
* InMemoryTaskInvoker via an in-process queue that fetches the target directly.
|
|
51
|
+
*/
|
|
52
|
+
export declare abstract class TaskInvoker {
|
|
53
|
+
abstract enqueue(request: TaskRequest): Promise<JobReference>;
|
|
54
|
+
abstract delete(ref: JobReference): Promise<void>;
|
|
55
|
+
}
|
package/src/TaskTypes.js
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.TaskInvoker = exports.TaskRequest = exports.JobReference = exports.ScheduleInfo = void 0;
|
|
4
|
+
/**
|
|
5
|
+
* Scheduling options for an enqueued task. Travels out-of-band (via the scheduler
|
|
6
|
+
* frame in RequestContext) so the shared API method signature stays `foo(req)` on
|
|
7
|
+
* both the client and the controller.
|
|
8
|
+
*/
|
|
9
|
+
class ScheduleInfo {
|
|
10
|
+
/** Absolute epoch-millis to run at; omitted = run as soon as possible. */
|
|
11
|
+
epochMsToRunAt;
|
|
12
|
+
/** Per-task dispatch deadline in seconds. */
|
|
13
|
+
taskTimeoutSeconds;
|
|
14
|
+
/**
|
|
15
|
+
* Deterministic dedup name → the Cloud Task resource name. A second enqueue with
|
|
16
|
+
* the same name is rejected ALREADY_EXISTS (treated as success = idempotent).
|
|
17
|
+
*/
|
|
18
|
+
dedupName;
|
|
19
|
+
constructor(epochMsToRunAt, taskTimeoutSeconds, dedupName) {
|
|
20
|
+
this.epochMsToRunAt = epochMsToRunAt;
|
|
21
|
+
this.taskTimeoutSeconds = taskTimeoutSeconds;
|
|
22
|
+
this.dedupName = dedupName;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
exports.ScheduleInfo = ScheduleInfo;
|
|
26
|
+
/** Handle to an enqueued task (its Cloud Tasks id), returned by the scheduler. */
|
|
27
|
+
class JobReference {
|
|
28
|
+
taskId;
|
|
29
|
+
constructor(taskId) {
|
|
30
|
+
this.taskId = taskId;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
exports.JobReference = JobReference;
|
|
34
|
+
/**
|
|
35
|
+
* Everything an invoker needs to enqueue one task. Built by the enqueue proxy from
|
|
36
|
+
* the shared @PubSub contract's decorators + the ambient RequestContext.
|
|
37
|
+
*/
|
|
38
|
+
class TaskRequest {
|
|
39
|
+
/** Callee base URL (e.g. https://email-svc-123.us-central1.run.app). */
|
|
40
|
+
targetUrl;
|
|
41
|
+
/** Endpoint path the task is delivered to (basePath + endpoint, e.g. /email/send). */
|
|
42
|
+
path;
|
|
43
|
+
/** Cloud Tasks queue name (getQueueName: @Queue override or `${Api}-${method}`). */
|
|
44
|
+
queueName;
|
|
45
|
+
/** The request DTO (serialized to the POST body on delivery). */
|
|
46
|
+
// webpieces-disable no-any-unknown -- request DTO type is erased at the task boundary
|
|
47
|
+
body;
|
|
48
|
+
/** Context headers to propagate (txId/requestId/tenant…), already resolved. */
|
|
49
|
+
contextHeaders;
|
|
50
|
+
/** The endpoint's auth mode — how the invoker authenticates delivery. */
|
|
51
|
+
authMode;
|
|
52
|
+
/** Scheduling options (dedup name, run-at, timeout). */
|
|
53
|
+
scheduleInfo;
|
|
54
|
+
constructor(targetUrl, path, queueName,
|
|
55
|
+
// webpieces-disable no-any-unknown -- request DTO type is erased at the task boundary
|
|
56
|
+
body, contextHeaders, authMode, scheduleInfo) {
|
|
57
|
+
this.targetUrl = targetUrl;
|
|
58
|
+
this.path = path;
|
|
59
|
+
this.queueName = queueName;
|
|
60
|
+
this.body = body;
|
|
61
|
+
this.contextHeaders = contextHeaders;
|
|
62
|
+
this.authMode = authMode;
|
|
63
|
+
this.scheduleInfo = scheduleInfo;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
exports.TaskRequest = TaskRequest;
|
|
67
|
+
/**
|
|
68
|
+
* The transport that actually enqueues a task. Abstract class so it doubles as the
|
|
69
|
+
* inversify DI token (inject by type, no Symbol). Bind GcpTaskInvoker in prod or
|
|
70
|
+
* InMemoryTaskInvoker in tests/local dev. Both deliver a task the same way — a plain
|
|
71
|
+
* HTTP POST to `targetUrl + path` — GcpTaskInvoker via Google Cloud Tasks, and
|
|
72
|
+
* InMemoryTaskInvoker via an in-process queue that fetches the target directly.
|
|
73
|
+
*/
|
|
74
|
+
class TaskInvoker {
|
|
75
|
+
}
|
|
76
|
+
exports.TaskInvoker = TaskInvoker;
|
|
77
|
+
//# sourceMappingURL=TaskTypes.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"TaskTypes.js","sourceRoot":"","sources":["../../../../../packages/cloud/cloudtasks-client/src/TaskTypes.ts"],"names":[],"mappings":";;;AAEA;;;;GAIG;AACH,MAAa,YAAY;IACrB,0EAA0E;IAC1E,cAAc,CAAU;IACxB,6CAA6C;IAC7C,kBAAkB,CAAU;IAC5B;;;OAGG;IACH,SAAS,CAAU;IAEnB,YAAY,cAAuB,EAAE,kBAA2B,EAAE,SAAkB;QAChF,IAAI,CAAC,cAAc,GAAG,cAAc,CAAC;QACrC,IAAI,CAAC,kBAAkB,GAAG,kBAAkB,CAAC;QAC7C,IAAI,CAAC,SAAS,GAAG,SAAS,CAAC;IAC/B,CAAC;CACJ;AAhBD,oCAgBC;AAED,kFAAkF;AAClF,MAAa,YAAY;IACrB,MAAM,CAAS;IAEf,YAAY,MAAc;QACtB,IAAI,CAAC,MAAM,GAAG,MAAM,CAAC;IACzB,CAAC;CACJ;AAND,oCAMC;AAED;;;GAGG;AACH,MAAa,WAAW;IACpB,wEAAwE;IACxE,SAAS,CAAS;IAClB,sFAAsF;IACtF,IAAI,CAAS;IACb,oFAAoF;IACpF,SAAS,CAAS;IAClB,iEAAiE;IACjE,sFAAsF;IACtF,IAAI,CAAU;IACd,+EAA+E;IAC/E,cAAc,CAAsB;IACpC,yEAAyE;IACzE,QAAQ,CAAW;IACnB,wDAAwD;IACxD,YAAY,CAAe;IAE3B,YACI,SAAiB,EACjB,IAAY,EACZ,SAAiB;IACjB,sFAAsF;IACtF,IAAa,EACb,cAAmC,EACnC,QAAkB,EAClB,YAA0B;QAE1B,IAAI,CAAC,SAAS,GAAG,SAAS,CAAC;QAC3B,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC;QACjB,IAAI,CAAC,SAAS,GAAG,SAAS,CAAC;QAC3B,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC;QACjB,IAAI,CAAC,cAAc,GAAG,cAAc,CAAC;QACrC,IAAI,CAAC,QAAQ,GAAG,QAAQ,CAAC;QACzB,IAAI,CAAC,YAAY,GAAG,YAAY,CAAC;IACrC,CAAC;CACJ;AAnCD,kCAmCC;AAED;;;;;;GAMG;AACH,MAAsB,WAAW;CAGhC;AAHD,kCAGC","sourcesContent":["import { AuthMode } from '@webpieces/core-util';\n\n/**\n * Scheduling options for an enqueued task. Travels out-of-band (via the scheduler\n * frame in RequestContext) so the shared API method signature stays `foo(req)` on\n * both the client and the controller.\n */\nexport class ScheduleInfo {\n /** Absolute epoch-millis to run at; omitted = run as soon as possible. */\n epochMsToRunAt?: number;\n /** Per-task dispatch deadline in seconds. */\n taskTimeoutSeconds?: number;\n /**\n * Deterministic dedup name → the Cloud Task resource name. A second enqueue with\n * the same name is rejected ALREADY_EXISTS (treated as success = idempotent).\n */\n dedupName?: string;\n\n constructor(epochMsToRunAt?: number, taskTimeoutSeconds?: number, dedupName?: string) {\n this.epochMsToRunAt = epochMsToRunAt;\n this.taskTimeoutSeconds = taskTimeoutSeconds;\n this.dedupName = dedupName;\n }\n}\n\n/** Handle to an enqueued task (its Cloud Tasks id), returned by the scheduler. */\nexport class JobReference {\n taskId: string;\n\n constructor(taskId: string) {\n this.taskId = taskId;\n }\n}\n\n/**\n * Everything an invoker needs to enqueue one task. Built by the enqueue proxy from\n * the shared @PubSub contract's decorators + the ambient RequestContext.\n */\nexport class TaskRequest {\n /** Callee base URL (e.g. https://email-svc-123.us-central1.run.app). */\n targetUrl: string;\n /** Endpoint path the task is delivered to (basePath + endpoint, e.g. /email/send). */\n path: string;\n /** Cloud Tasks queue name (getQueueName: @Queue override or `${Api}-${method}`). */\n queueName: string;\n /** The request DTO (serialized to the POST body on delivery). */\n // webpieces-disable no-any-unknown -- request DTO type is erased at the task boundary\n body: unknown;\n /** Context headers to propagate (txId/requestId/tenant…), already resolved. */\n contextHeaders: Map<string, string>;\n /** The endpoint's auth mode — how the invoker authenticates delivery. */\n authMode: AuthMode;\n /** Scheduling options (dedup name, run-at, timeout). */\n scheduleInfo: ScheduleInfo;\n\n constructor(\n targetUrl: string,\n path: string,\n queueName: string,\n // webpieces-disable no-any-unknown -- request DTO type is erased at the task boundary\n body: unknown,\n contextHeaders: Map<string, string>,\n authMode: AuthMode,\n scheduleInfo: ScheduleInfo,\n ) {\n this.targetUrl = targetUrl;\n this.path = path;\n this.queueName = queueName;\n this.body = body;\n this.contextHeaders = contextHeaders;\n this.authMode = authMode;\n this.scheduleInfo = scheduleInfo;\n }\n}\n\n/**\n * The transport that actually enqueues a task. Abstract class so it doubles as the\n * inversify DI token (inject by type, no Symbol). Bind GcpTaskInvoker in prod or\n * InMemoryTaskInvoker in tests/local dev. Both deliver a task the same way — a plain\n * HTTP POST to `targetUrl + path` — GcpTaskInvoker via Google Cloud Tasks, and\n * InMemoryTaskInvoker via an in-process queue that fetches the target directly.\n */\nexport abstract class TaskInvoker {\n abstract enqueue(request: TaskRequest): Promise<JobReference>;\n abstract delete(ref: JobReference): Promise<void>;\n}\n"]}
|
package/src/index.d.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @webpieces/cloudtasks-client
|
|
3
|
+
*
|
|
4
|
+
* Cloud Tasks enqueue client generated from a shared @PubSub API contract — the
|
|
5
|
+
* fire-and-forget twin of @webpieces/http-client. The client and the controller
|
|
6
|
+
* share ONE abstract API class; calling a method enqueues a task that is later
|
|
7
|
+
* delivered to the same endpoint through the full server filter chain.
|
|
8
|
+
*/
|
|
9
|
+
export { ScheduleInfo, JobReference, TaskRequest, TaskInvoker, } from './TaskTypes';
|
|
10
|
+
export { createTaskClient, TaskClientConfig } from './TaskClientFactory';
|
|
11
|
+
export { TaskClientCreator } from './TaskClientCreator';
|
|
12
|
+
export { CloudTaskScheduler, ScheduleOptions } from './CloudTaskScheduler';
|
|
13
|
+
export { InMemoryTaskInvoker } from './InMemoryTaskInvoker';
|
|
14
|
+
export { GcpTaskInvoker } from './GcpTaskInvoker';
|
|
15
|
+
export { ScheduleFrame, setScheduleFrame, currentScheduleFrame, clearScheduleFrame, } from './ScheduleContext';
|
package/src/index.js
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* @webpieces/cloudtasks-client
|
|
4
|
+
*
|
|
5
|
+
* Cloud Tasks enqueue client generated from a shared @PubSub API contract — the
|
|
6
|
+
* fire-and-forget twin of @webpieces/http-client. The client and the controller
|
|
7
|
+
* share ONE abstract API class; calling a method enqueues a task that is later
|
|
8
|
+
* delivered to the same endpoint through the full server filter chain.
|
|
9
|
+
*/
|
|
10
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
11
|
+
exports.clearScheduleFrame = exports.currentScheduleFrame = exports.setScheduleFrame = exports.ScheduleFrame = exports.GcpTaskInvoker = exports.InMemoryTaskInvoker = exports.ScheduleOptions = exports.CloudTaskScheduler = exports.TaskClientCreator = exports.TaskClientConfig = exports.createTaskClient = exports.TaskInvoker = exports.TaskRequest = exports.JobReference = exports.ScheduleInfo = void 0;
|
|
12
|
+
var TaskTypes_1 = require("./TaskTypes");
|
|
13
|
+
Object.defineProperty(exports, "ScheduleInfo", { enumerable: true, get: function () { return TaskTypes_1.ScheduleInfo; } });
|
|
14
|
+
Object.defineProperty(exports, "JobReference", { enumerable: true, get: function () { return TaskTypes_1.JobReference; } });
|
|
15
|
+
Object.defineProperty(exports, "TaskRequest", { enumerable: true, get: function () { return TaskTypes_1.TaskRequest; } });
|
|
16
|
+
Object.defineProperty(exports, "TaskInvoker", { enumerable: true, get: function () { return TaskTypes_1.TaskInvoker; } });
|
|
17
|
+
var TaskClientFactory_1 = require("./TaskClientFactory");
|
|
18
|
+
Object.defineProperty(exports, "createTaskClient", { enumerable: true, get: function () { return TaskClientFactory_1.createTaskClient; } });
|
|
19
|
+
Object.defineProperty(exports, "TaskClientConfig", { enumerable: true, get: function () { return TaskClientFactory_1.TaskClientConfig; } });
|
|
20
|
+
var TaskClientCreator_1 = require("./TaskClientCreator");
|
|
21
|
+
Object.defineProperty(exports, "TaskClientCreator", { enumerable: true, get: function () { return TaskClientCreator_1.TaskClientCreator; } });
|
|
22
|
+
var CloudTaskScheduler_1 = require("./CloudTaskScheduler");
|
|
23
|
+
Object.defineProperty(exports, "CloudTaskScheduler", { enumerable: true, get: function () { return CloudTaskScheduler_1.CloudTaskScheduler; } });
|
|
24
|
+
Object.defineProperty(exports, "ScheduleOptions", { enumerable: true, get: function () { return CloudTaskScheduler_1.ScheduleOptions; } });
|
|
25
|
+
// The two task transports (local HTTP-queue + remote GCP), both delivering over real HTTP.
|
|
26
|
+
var InMemoryTaskInvoker_1 = require("./InMemoryTaskInvoker");
|
|
27
|
+
Object.defineProperty(exports, "InMemoryTaskInvoker", { enumerable: true, get: function () { return InMemoryTaskInvoker_1.InMemoryTaskInvoker; } });
|
|
28
|
+
var GcpTaskInvoker_1 = require("./GcpTaskInvoker");
|
|
29
|
+
Object.defineProperty(exports, "GcpTaskInvoker", { enumerable: true, get: function () { return GcpTaskInvoker_1.GcpTaskInvoker; } });
|
|
30
|
+
// NOTE: ServiceAuthFilter (server-side delivery-auth Filter) lives in @webpieces/http-server —
|
|
31
|
+
// a client library has no server filters / routing machinery.
|
|
32
|
+
var ScheduleContext_1 = require("./ScheduleContext");
|
|
33
|
+
Object.defineProperty(exports, "ScheduleFrame", { enumerable: true, get: function () { return ScheduleContext_1.ScheduleFrame; } });
|
|
34
|
+
Object.defineProperty(exports, "setScheduleFrame", { enumerable: true, get: function () { return ScheduleContext_1.setScheduleFrame; } });
|
|
35
|
+
Object.defineProperty(exports, "currentScheduleFrame", { enumerable: true, get: function () { return ScheduleContext_1.currentScheduleFrame; } });
|
|
36
|
+
Object.defineProperty(exports, "clearScheduleFrame", { enumerable: true, get: function () { return ScheduleContext_1.clearScheduleFrame; } });
|
|
37
|
+
//# sourceMappingURL=index.js.map
|
package/src/index.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../../../packages/cloud/cloudtasks-client/src/index.ts"],"names":[],"mappings":";AAAA;;;;;;;GAOG;;;AAEH,yCAKqB;AAJjB,yGAAA,YAAY,OAAA;AACZ,yGAAA,YAAY,OAAA;AACZ,wGAAA,WAAW,OAAA;AACX,wGAAA,WAAW,OAAA;AAEf,yDAAyE;AAAhE,qHAAA,gBAAgB,OAAA;AAAE,qHAAA,gBAAgB,OAAA;AAC3C,yDAAwD;AAA/C,sHAAA,iBAAiB,OAAA;AAC1B,2DAA2E;AAAlE,wHAAA,kBAAkB,OAAA;AAAE,qHAAA,eAAe,OAAA;AAC5C,2FAA2F;AAC3F,6DAA4D;AAAnD,0HAAA,mBAAmB,OAAA;AAC5B,mDAAkD;AAAzC,gHAAA,cAAc,OAAA;AACvB,+FAA+F;AAC/F,8DAA8D;AAC9D,qDAK2B;AAJvB,gHAAA,aAAa,OAAA;AACb,mHAAA,gBAAgB,OAAA;AAChB,uHAAA,oBAAoB,OAAA;AACpB,qHAAA,kBAAkB,OAAA","sourcesContent":["/**\n * @webpieces/cloudtasks-client\n *\n * Cloud Tasks enqueue client generated from a shared @PubSub API contract — the\n * fire-and-forget twin of @webpieces/http-client. The client and the controller\n * share ONE abstract API class; calling a method enqueues a task that is later\n * delivered to the same endpoint through the full server filter chain.\n */\n\nexport {\n ScheduleInfo,\n JobReference,\n TaskRequest,\n TaskInvoker,\n} from './TaskTypes';\nexport { createTaskClient, TaskClientConfig } from './TaskClientFactory';\nexport { TaskClientCreator } from './TaskClientCreator';\nexport { CloudTaskScheduler, ScheduleOptions } from './CloudTaskScheduler';\n// The two task transports (local HTTP-queue + remote GCP), both delivering over real HTTP.\nexport { InMemoryTaskInvoker } from './InMemoryTaskInvoker';\nexport { GcpTaskInvoker } from './GcpTaskInvoker';\n// NOTE: ServiceAuthFilter (server-side delivery-auth Filter) lives in @webpieces/http-server —\n// a client library has no server filters / routing machinery.\nexport {\n ScheduleFrame,\n setScheduleFrame,\n currentScheduleFrame,\n clearScheduleFrame,\n} from './ScheduleContext';\n"]}
|