@webpieces/cloudtasks-client 0.3.316 → 0.3.322
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 +9 -4
- package/package.json +4 -4
- package/src/ClientCloudTasksFactory.d.ts +14 -15
- package/src/ClientCloudTasksFactory.js +24 -22
- package/src/ClientCloudTasksFactory.js.map +1 -1
- package/src/GcpTaskInvoker.js +2 -1
- package/src/GcpTaskInvoker.js.map +1 -1
- package/src/InMemoryTaskInvoker.js +2 -1
- package/src/InMemoryTaskInvoker.js.map +1 -1
- package/src/TaskClientConfig.d.ts +42 -10
- package/src/TaskClientConfig.js +31 -11
- package/src/TaskClientConfig.js.map +1 -1
- package/src/TaskProxyClient.d.ts +36 -14
- package/src/TaskProxyClient.js +52 -37
- package/src/TaskProxyClient.js.map +1 -1
- package/src/index.d.ts +1 -1
- package/src/index.js +2 -1
- package/src/index.js.map +1 -1
package/README.md
CHANGED
|
@@ -12,15 +12,20 @@ abstract class EmailApi { @Endpoint('/send') sendEmail(r: SendEmailRequest): Pro
|
|
|
12
12
|
|
|
13
13
|
// build the client once (sync); 'email-svc' is the callee's Cloud Run service name
|
|
14
14
|
const emailTasks = factory.createClient(EmailApi, new TaskClientConfig('email-svc'));
|
|
15
|
+
// ...or pin a URL lookup cannot describe (other region/project); svcName stays the log name
|
|
16
|
+
const other = factory.createClient(EmailApi, new TaskClientConfig('email-svc', 'https://email.eu.example'));
|
|
15
17
|
|
|
16
18
|
// producer (inside a request → RequestContext active)
|
|
17
19
|
await scheduler.addToQueue(() => emailTasks.sendEmail(req), { dedupName: req.id });
|
|
18
20
|
```
|
|
19
21
|
|
|
20
|
-
- `ClientCloudTasksFactory.createClient(Api, TaskClientConfig)` — builds the enqueue proxy
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
22
|
+
- `ClientCloudTasksFactory.createClient(Api, TaskClientConfig)` — builds the enqueue proxy. It
|
|
23
|
+
injects a `Provider<TaskProxyClient>` and calls `get()` per contract; `TaskProxyClient` is bound
|
|
24
|
+
TRANSIENT, so each client gets its own. The delivery URL is resolved at enqueue time from
|
|
25
|
+
`svcName` (same project + region as this container, so you maintain no URL table) unless you pass
|
|
26
|
+
an explicit `targetUrl`. `getCloudRunUrl` also honours a `CLOUD_RUN_URL_<UPPER_SNAKE_NAME>` env
|
|
27
|
+
override for local multi-service runs and integration tests
|
|
28
|
+
- An enqueue outside `RequestContext.run(...)` **throws**: a task with no caller trace is a bug
|
|
24
29
|
- `CloudTaskScheduler` — `addToQueue` / `schedule` / `cancelJob`; carries scheduling
|
|
25
30
|
options out-of-band so the contract signature stays identical on both sides
|
|
26
31
|
- `TaskInvoker` (abstract token) with two impls:
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@webpieces/cloudtasks-client",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.322",
|
|
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.3.
|
|
27
|
-
"@webpieces/core-util": "0.3.
|
|
28
|
-
"@webpieces/gcp-identity": "0.3.
|
|
26
|
+
"@webpieces/core-context": "0.3.322",
|
|
27
|
+
"@webpieces/core-util": "0.3.322",
|
|
28
|
+
"@webpieces/gcp-identity": "0.3.322",
|
|
29
29
|
"@google-cloud/tasks": "5.5.2",
|
|
30
30
|
"inversify": "7.10.4",
|
|
31
31
|
"reflect-metadata": "0.2.2"
|
|
@@ -1,30 +1,29 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { Provider } from '@webpieces/core-context';
|
|
2
|
+
import { TaskProxyClient } from './TaskProxyClient';
|
|
2
3
|
import { ApiPrototype, TaskClientConfig } from './TaskClientConfig';
|
|
3
4
|
/**
|
|
4
5
|
* ClientCloudTasksFactory - builds Cloud Tasks enqueue clients from a shared @PubSub API
|
|
5
|
-
* contract. The fire-and-forget twin of http-client's ClientHttpFactory.
|
|
6
|
+
* contract. The fire-and-forget twin of http-client-node's ClientHttpFactory.
|
|
6
7
|
*
|
|
7
|
-
* Calling a method on the returned client ENQUEUES a task (it does not call remotely);
|
|
8
|
-
*
|
|
9
|
-
* filter chain. A service just asks for a typed client:
|
|
8
|
+
* Calling a method on the returned client ENQUEUES a task (it does not call remotely); the task
|
|
9
|
+
* is later delivered to the same endpoint's controller through the full server filter chain.
|
|
10
10
|
*
|
|
11
11
|
* ```typescript
|
|
12
|
+
* // same project + region as this container; the URL is derived, you maintain nothing
|
|
12
13
|
* const emailTasks = factory.createClient(EmailApi, new TaskClientConfig('email-svc'));
|
|
13
14
|
* await scheduler.addToQueue(() => emailTasks.sendEmail(req), { dedupName });
|
|
14
15
|
* ```
|
|
15
16
|
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
17
|
+
* Every client it builds gets its OWN {@link TaskProxyClient} from the injected
|
|
18
|
+
* `Provider<TaskProxyClient>` (bound transient), which `createClient` then `init`s for one
|
|
19
|
+
* contract. Their collaborators (TaskInvoker, RequestContextHeaders) come from the container.
|
|
19
20
|
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
* the ContextMgr is a fixed field: RequestContextReader is the one and only right reader.
|
|
21
|
+
* Node-only, so the factory IS the inversify entry point. An enqueue outside
|
|
22
|
+
* `RequestContext.run(...)` throws rather than silently dropping the caller's trace.
|
|
23
23
|
*/
|
|
24
24
|
export declare class ClientCloudTasksFactory {
|
|
25
|
-
private readonly
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
/** Typed enqueue client for a @PubSub contract, delivered to `config.gcpCloudRunSvcName`. */
|
|
25
|
+
private readonly taskProxyClientProvider;
|
|
26
|
+
constructor(taskProxyClientProvider: Provider<TaskProxyClient>);
|
|
27
|
+
/** Typed enqueue client for a @PubSub contract, delivered to `config.svcName`. */
|
|
29
28
|
createClient<T extends object>(apiClass: ApiPrototype<T>, config: TaskClientConfig): T;
|
|
30
29
|
}
|
|
@@ -3,12 +3,15 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.ClientCloudTasksFactory = void 0;
|
|
4
4
|
const tslib_1 = require("tslib");
|
|
5
5
|
const inversify_1 = require("inversify");
|
|
6
|
+
const core_util_1 = require("@webpieces/core-util");
|
|
6
7
|
const core_context_1 = require("@webpieces/core-context");
|
|
7
|
-
const TaskTypes_1 = require("./TaskTypes");
|
|
8
8
|
const TaskProxyClient_1 = require("./TaskProxyClient");
|
|
9
|
+
// Teach the container how to hand out fresh TaskProxyClients. TaskProxyClient is bound TRANSIENT
|
|
10
|
+
// (@provideFrameworkTransient), so each provider.get() constructs a new one.
|
|
11
|
+
(0, core_context_1.bindFrameworkProvider)(TaskProxyClient_1.TASK_PROXY_CLIENT_PROVIDER, TaskProxyClient_1.TaskProxyClient);
|
|
9
12
|
/**
|
|
10
13
|
* Properties DI frameworks / Promise checks / serializers probe on the proxy; return
|
|
11
|
-
* undefined instead of treating them as endpoints. Mirrors http-client's
|
|
14
|
+
* undefined instead of treating them as endpoints. Mirrors http-client-core's buildClientProxy.
|
|
12
15
|
*/
|
|
13
16
|
const FRAMEWORK_INSPECTION_PROPERTIES = new Set([
|
|
14
17
|
'constructor', 'prototype', '__proto__', 'name', 'then', 'catch', 'finally',
|
|
@@ -16,36 +19,34 @@ const FRAMEWORK_INSPECTION_PROPERTIES = new Set([
|
|
|
16
19
|
]);
|
|
17
20
|
/**
|
|
18
21
|
* ClientCloudTasksFactory - builds Cloud Tasks enqueue clients from a shared @PubSub API
|
|
19
|
-
* contract. The fire-and-forget twin of http-client's ClientHttpFactory.
|
|
22
|
+
* contract. The fire-and-forget twin of http-client-node's ClientHttpFactory.
|
|
20
23
|
*
|
|
21
|
-
* Calling a method on the returned client ENQUEUES a task (it does not call remotely);
|
|
22
|
-
*
|
|
23
|
-
* filter chain. A service just asks for a typed client:
|
|
24
|
+
* Calling a method on the returned client ENQUEUES a task (it does not call remotely); the task
|
|
25
|
+
* is later delivered to the same endpoint's controller through the full server filter chain.
|
|
24
26
|
*
|
|
25
27
|
* ```typescript
|
|
28
|
+
* // same project + region as this container; the URL is derived, you maintain nothing
|
|
26
29
|
* const emailTasks = factory.createClient(EmailApi, new TaskClientConfig('email-svc'));
|
|
27
30
|
* await scheduler.addToQueue(() => emailTasks.sendEmail(req), { dedupName });
|
|
28
31
|
* ```
|
|
29
32
|
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
+
* Every client it builds gets its OWN {@link TaskProxyClient} from the injected
|
|
34
|
+
* `Provider<TaskProxyClient>` (bound transient), which `createClient` then `init`s for one
|
|
35
|
+
* contract. Their collaborators (TaskInvoker, RequestContextHeaders) come from the container.
|
|
33
36
|
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
* the ContextMgr is a fixed field: RequestContextReader is the one and only right reader.
|
|
37
|
+
* Node-only, so the factory IS the inversify entry point. An enqueue outside
|
|
38
|
+
* `RequestContext.run(...)` throws rather than silently dropping the caller's trace.
|
|
37
39
|
*/
|
|
38
40
|
let ClientCloudTasksFactory = class ClientCloudTasksFactory {
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
this.invoker = invoker;
|
|
41
|
+
taskProxyClientProvider;
|
|
42
|
+
constructor(taskProxyClientProvider) {
|
|
43
|
+
this.taskProxyClientProvider = taskProxyClientProvider;
|
|
43
44
|
}
|
|
44
|
-
/** Typed enqueue client for a @PubSub contract, delivered to `config.
|
|
45
|
+
/** Typed enqueue client for a @PubSub contract, delivered to `config.svcName`. */
|
|
45
46
|
createClient(apiClass, config) {
|
|
46
|
-
//
|
|
47
|
-
|
|
48
|
-
|
|
47
|
+
// Fresh instance per contract — TaskProxyClient is transient.
|
|
48
|
+
const proxyClient = this.taskProxyClientProvider.get();
|
|
49
|
+
proxyClient.init(apiClass, config);
|
|
49
50
|
return new Proxy({}, {
|
|
50
51
|
// webpieces-disable no-any-unknown -- proxy get trap returns either an endpoint method or undefined
|
|
51
52
|
get(target, prop) {
|
|
@@ -64,9 +65,10 @@ let ClientCloudTasksFactory = class ClientCloudTasksFactory {
|
|
|
64
65
|
};
|
|
65
66
|
exports.ClientCloudTasksFactory = ClientCloudTasksFactory;
|
|
66
67
|
exports.ClientCloudTasksFactory = ClientCloudTasksFactory = tslib_1.__decorate([
|
|
68
|
+
(0, core_util_1.DocumentDesign)(),
|
|
67
69
|
(0, core_context_1.provideFrameworkSingleton)(),
|
|
68
70
|
(0, inversify_1.injectable)(),
|
|
69
|
-
tslib_1.__param(0, (0, inversify_1.inject)(
|
|
70
|
-
tslib_1.__metadata("design:paramtypes", [
|
|
71
|
+
tslib_1.__param(0, (0, inversify_1.inject)(TaskProxyClient_1.TASK_PROXY_CLIENT_PROVIDER)),
|
|
72
|
+
tslib_1.__metadata("design:paramtypes", [core_context_1.Provider])
|
|
71
73
|
], ClientCloudTasksFactory);
|
|
72
74
|
//# sourceMappingURL=ClientCloudTasksFactory.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ClientCloudTasksFactory.js","sourceRoot":"","sources":["../../../../../packages/cloud/cloudtasks-client/src/ClientCloudTasksFactory.ts"],"names":[],"mappings":";;;;AAAA,yCAA+C;AAC/C,
|
|
1
|
+
{"version":3,"file":"ClientCloudTasksFactory.js","sourceRoot":"","sources":["../../../../../packages/cloud/cloudtasks-client/src/ClientCloudTasksFactory.ts"],"names":[],"mappings":";;;;AAAA,yCAA+C;AAC/C,oDAAsD;AACtD,0DAAqG;AACrG,uDAAgF;AAGhF,iGAAiG;AACjG,6EAA6E;AAC7E,IAAA,oCAAqB,EAAC,4CAA0B,EAAE,iCAAe,CAAC,CAAC;AAEnE;;;GAGG;AACH,MAAM,+BAA+B,GAAG,IAAI,GAAG,CAAS;IACpD,aAAa,EAAE,WAAW,EAAE,WAAW,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,SAAS;IAC3E,QAAQ,EAAE,SAAS,EAAE,UAAU,EAAE,UAAU,EAAE,SAAS,EAAE,UAAU;CACrE,CAAC,CAAC;AAEH;;;;;;;;;;;;;;;;;;;GAmBG;AAII,IAAM,uBAAuB,GAA7B,MAAM,uBAAuB;IAEyB;IADzD,YACyD,uBAAkD;QAAlD,4BAAuB,GAAvB,uBAAuB,CAA2B;IACxG,CAAC;IAEJ,kFAAkF;IAClF,YAAY,CAAmB,QAAyB,EAAE,MAAwB;QAC9E,8DAA8D;QAC9D,MAAM,WAAW,GAAG,IAAI,CAAC,uBAAuB,CAAC,GAAG,EAAE,CAAC;QACvD,WAAW,CAAC,IAAI,CAAC,QAAQ,EAAE,MAAM,CAAC,CAAC;QAEnC,OAAO,IAAI,KAAK,CAAC,EAAO,EAAE;YACtB,oGAAoG;YACpG,GAAG,CAAC,MAAS,EAAE,IAAqB;gBAChC,IAAI,OAAO,IAAI,KAAK,QAAQ,IAAI,+BAA+B,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC;oBACxE,OAAO,SAAS,CAAC;gBACrB,CAAC;gBACD,IAAI,CAAC,WAAW,CAAC,WAAW,CAAC,IAAI,CAAC,EAAE,CAAC;oBACjC,MAAM,IAAI,KAAK,CACX,wBAAwB,IAAI,QAAQ,QAAQ,CAAC,IAAI,IAAI,SAAS,IAAI;wBAClE,qDAAqD,CACxD,CAAC;gBACN,CAAC;gBACD,oFAAoF;gBACpF,OAAO,CAAC,UAAmB,EAAiB,EAAE,CAAC,WAAW,CAAC,OAAO,CAAC,IAAI,EAAE,UAAU,CAAC,CAAC;YACzF,CAAC;SACJ,CAAC,CAAC;IACP,CAAC;CACJ,CAAA;AA5BY,0DAAuB;kCAAvB,uBAAuB;IAHnC,IAAA,0BAAc,GAAE;IAChB,IAAA,wCAAyB,GAAE;IAC3B,IAAA,sBAAU,GAAE;IAGJ,mBAAA,IAAA,kBAAM,EAAC,4CAA0B,CAAC,CAAA;6CAA2C,uBAAQ;GAFjF,uBAAuB,CA4BnC","sourcesContent":["import { inject, injectable } from 'inversify';\nimport { DocumentDesign } from '@webpieces/core-util';\nimport { Provider, bindFrameworkProvider, provideFrameworkSingleton } from '@webpieces/core-context';\nimport { TASK_PROXY_CLIENT_PROVIDER, TaskProxyClient } from './TaskProxyClient';\nimport { ApiPrototype, TaskClientConfig } from './TaskClientConfig';\n\n// Teach the container how to hand out fresh TaskProxyClients. TaskProxyClient is bound TRANSIENT\n// (@provideFrameworkTransient), so each provider.get() constructs a new one.\nbindFrameworkProvider(TASK_PROXY_CLIENT_PROVIDER, TaskProxyClient);\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-core's buildClientProxy.\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/**\n * ClientCloudTasksFactory - builds Cloud Tasks enqueue clients from a shared @PubSub API\n * contract. The fire-and-forget twin of http-client-node's ClientHttpFactory.\n *\n * Calling a method on the returned client ENQUEUES a task (it does not call remotely); the task\n * is later delivered to the same endpoint's controller through the full server filter chain.\n *\n * ```typescript\n * // same project + region as this container; the URL is derived, you maintain nothing\n * const emailTasks = factory.createClient(EmailApi, new TaskClientConfig('email-svc'));\n * await scheduler.addToQueue(() => emailTasks.sendEmail(req), { dedupName });\n * ```\n *\n * Every client it builds gets its OWN {@link TaskProxyClient} from the injected\n * `Provider<TaskProxyClient>` (bound transient), which `createClient` then `init`s for one\n * contract. Their collaborators (TaskInvoker, RequestContextHeaders) come from the container.\n *\n * Node-only, so the factory IS the inversify entry point. An enqueue outside\n * `RequestContext.run(...)` throws rather than silently dropping the caller's trace.\n */\n@DocumentDesign()\n@provideFrameworkSingleton()\n@injectable()\nexport class ClientCloudTasksFactory {\n constructor(\n @inject(TASK_PROXY_CLIENT_PROVIDER) private readonly taskProxyClientProvider: Provider<TaskProxyClient>,\n ) {}\n\n /** Typed enqueue client for a @PubSub contract, delivered to `config.svcName`. */\n createClient<T extends object>(apiClass: ApiPrototype<T>, config: TaskClientConfig): T {\n // Fresh instance per contract — TaskProxyClient is transient.\n const proxyClient = this.taskProxyClientProvider.get();\n proxyClient.init(apiClass, config);\n\n return new Proxy({} as T, {\n // webpieces-disable no-any-unknown -- proxy get trap returns either an endpoint 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 if (!proxyClient.hasEndpoint(prop)) {\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> => proxyClient.enqueue(prop, requestDto);\n },\n });\n }\n}\n"]}
|
package/src/GcpTaskInvoker.js
CHANGED
|
@@ -72,7 +72,8 @@ let GcpTaskInvoker = class GcpTaskInvoker extends TaskTypes_1.TaskInvoker {
|
|
|
72
72
|
if (mode.kind === 'shared-secret') {
|
|
73
73
|
const secret = this.secrets?.get(mode.secretKey);
|
|
74
74
|
if (secret) {
|
|
75
|
-
|
|
75
|
+
// One credential header; the 'Webpieces' scheme says a shared secret follows, not a token.
|
|
76
|
+
headers.set('authorization', `Webpieces ${secret}`);
|
|
76
77
|
httpRequest.headers = Object.fromEntries(headers);
|
|
77
78
|
}
|
|
78
79
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"GcpTaskInvoker.js","sourceRoot":"","sources":["../../../../../packages/cloud/cloudtasks-client/src/GcpTaskInvoker.ts"],"names":[],"mappings":"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
|
|
1
|
+
{"version":3,"file":"GcpTaskInvoker.js","sourceRoot":"","sources":["../../../../../packages/cloud/cloudtasks-client/src/GcpTaskInvoker.ts"],"names":[],"mappings":"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","sourcesContent":["import { injectable, inject, optional } from 'inversify';\nimport { CloudTasksClient, protos } from '@google-cloud/tasks';\nimport { provideFrameworkSingleton } from '@webpieces/core-context';\nimport {\n getProjectId,\n getRegion,\n getRuntimeServiceAccountEmail,\n} from '@webpieces/gcp-identity';\nimport { LogManager, Secrets } 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@provideFrameworkSingleton()\n@injectable()\nexport class GcpTaskInvoker extends TaskInvoker {\n private readonly client = new CloudTasksClient();\n\n // @optional: only @AuthSharedSecret task endpoints need it; the client sends its bound value.\n constructor(@optional() @inject(Secrets) private readonly secrets?: Secrets) {\n super();\n }\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 = this.secrets?.get(mode.secretKey);\n if (secret) {\n // One credential header; the 'Webpieces' scheme says a shared secret follows, not a token.\n headers.set('authorization', `Webpieces ${secret}`);\n httpRequest.headers = Object.fromEntries(headers);\n }\n }\n }\n}\n"]}
|
|
@@ -107,7 +107,8 @@ let InMemoryTaskInvoker = class InMemoryTaskInvoker extends TaskTypes_1.TaskInvo
|
|
|
107
107
|
if (mode.kind === 'shared-secret') {
|
|
108
108
|
const secret = this.secrets?.get(mode.secretKey);
|
|
109
109
|
if (secret) {
|
|
110
|
-
|
|
110
|
+
// One credential header; the 'Webpieces' scheme says a shared secret follows, not a token.
|
|
111
|
+
headers['authorization'] = `Webpieces ${secret}`;
|
|
111
112
|
}
|
|
112
113
|
}
|
|
113
114
|
// public / jwt → no service credential is synthesized.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"InMemoryTaskInvoker.js","sourceRoot":"","sources":["../../../../../packages/cloud/cloudtasks-client/src/InMemoryTaskInvoker.ts"],"names":[],"mappings":"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
|
|
1
|
+
{"version":3,"file":"InMemoryTaskInvoker.js","sourceRoot":"","sources":["../../../../../packages/cloud/cloudtasks-client/src/InMemoryTaskInvoker.ts"],"names":[],"mappings":"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","sourcesContent":["import { injectable, inject, optional } from 'inversify';\nimport { provideFrameworkSingleton } from '@webpieces/core-context';\nimport { mintIdToken } from '@webpieces/gcp-identity';\nimport { LogManager, toError, Secrets } 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@provideFrameworkSingleton()\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 // @optional: only @AuthSharedSecret task endpoints need it; the client sends its bound value.\n constructor(@optional() @inject(Secrets) private readonly secrets?: Secrets) {\n super();\n }\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 = this.secrets?.get(mode.secretKey);\n if (secret) {\n // One credential header; the 'Webpieces' scheme says a shared secret follows, not a token.\n headers['authorization'] = `Webpieces ${secret}`;\n }\n }\n // public / jwt → no service credential is synthesized.\n }\n}\n"]}
|
|
@@ -5,16 +5,48 @@ export type ApiPrototype<T> = Function & {
|
|
|
5
5
|
/**
|
|
6
6
|
* Per-client STATE for a Cloud Tasks enqueue client — nothing else.
|
|
7
7
|
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
* `http://<svc>.localhost.invalid`. So there is no separate "fixed URL" client shape.
|
|
12
|
-
*
|
|
13
|
-
* Collaborators (TaskInvoker, ContextMgr) are NOT config: they are dependencies of
|
|
14
|
-
* {@link ClientCloudTasksFactory} and shared by every client it builds.
|
|
8
|
+
* Collaborators (TaskInvoker, RequestContextHeaders) are NOT config: they are dependencies of
|
|
9
|
+
* {@link ClientCloudTasksFactory} and shared by every client it builds. This is the fire-and-forget
|
|
10
|
+
* twin of http-client-node's ClientConfig, and takes the same two fields.
|
|
15
11
|
*/
|
|
16
12
|
export declare class TaskClientConfig {
|
|
17
|
-
/**
|
|
18
|
-
|
|
19
|
-
|
|
13
|
+
/**
|
|
14
|
+
* TYPICALLY the GCP Cloud Run service name — and it MUST be the Cloud Run service name
|
|
15
|
+
* when you do not supply a `targetUrl`, because we derive the URL from it.
|
|
16
|
+
*
|
|
17
|
+
* We lookup your service in the same project, same region, and form the url from the
|
|
18
|
+
* container information unless you pass in a targetUrl, so you do not have to maintain
|
|
19
|
+
* targetUrls. This works across your demo, qa, prod environments as long as each
|
|
20
|
+
* environment is in its own projectId, which is typical.
|
|
21
|
+
*
|
|
22
|
+
* When you DO supply a `targetUrl`, svcName is used only for logging, so any readable
|
|
23
|
+
* name works.
|
|
24
|
+
*/
|
|
25
|
+
readonly svcName: string;
|
|
26
|
+
/**
|
|
27
|
+
* Optional explicit base URL, for the cases lookup cannot describe: another region,
|
|
28
|
+
* another project, or a host that is not Cloud Run at all. It wins over `svcName`.
|
|
29
|
+
*/
|
|
30
|
+
readonly targetUrl?: string | undefined;
|
|
31
|
+
constructor(
|
|
32
|
+
/**
|
|
33
|
+
* TYPICALLY the GCP Cloud Run service name — and it MUST be the Cloud Run service name
|
|
34
|
+
* when you do not supply a `targetUrl`, because we derive the URL from it.
|
|
35
|
+
*
|
|
36
|
+
* We lookup your service in the same project, same region, and form the url from the
|
|
37
|
+
* container information unless you pass in a targetUrl, so you do not have to maintain
|
|
38
|
+
* targetUrls. This works across your demo, qa, prod environments as long as each
|
|
39
|
+
* environment is in its own projectId, which is typical.
|
|
40
|
+
*
|
|
41
|
+
* When you DO supply a `targetUrl`, svcName is used only for logging, so any readable
|
|
42
|
+
* name works.
|
|
43
|
+
*/
|
|
44
|
+
svcName: string,
|
|
45
|
+
/**
|
|
46
|
+
* Optional explicit base URL, for the cases lookup cannot describe: another region,
|
|
47
|
+
* another project, or a host that is not Cloud Run at all. It wins over `svcName`.
|
|
48
|
+
*/
|
|
49
|
+
targetUrl?: string | undefined);
|
|
50
|
+
/** Resolved per enqueue, not at construction — so building a client stays synchronous. */
|
|
51
|
+
resolveTargetUrl(): Promise<string>;
|
|
20
52
|
}
|
package/src/TaskClientConfig.js
CHANGED
|
@@ -1,22 +1,42 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.TaskClientConfig = void 0;
|
|
4
|
+
const gcp_identity_1 = require("@webpieces/gcp-identity");
|
|
4
5
|
/**
|
|
5
6
|
* Per-client STATE for a Cloud Tasks enqueue client — nothing else.
|
|
6
7
|
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
* `http://<svc>.localhost.invalid`. So there is no separate "fixed URL" client shape.
|
|
11
|
-
*
|
|
12
|
-
* Collaborators (TaskInvoker, ContextMgr) are NOT config: they are dependencies of
|
|
13
|
-
* {@link ClientCloudTasksFactory} and shared by every client it builds.
|
|
8
|
+
* Collaborators (TaskInvoker, RequestContextHeaders) are NOT config: they are dependencies of
|
|
9
|
+
* {@link ClientCloudTasksFactory} and shared by every client it builds. This is the fire-and-forget
|
|
10
|
+
* twin of http-client-node's ClientConfig, and takes the same two fields.
|
|
14
11
|
*/
|
|
15
12
|
class TaskClientConfig {
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
constructor(
|
|
19
|
-
|
|
13
|
+
svcName;
|
|
14
|
+
targetUrl;
|
|
15
|
+
constructor(
|
|
16
|
+
/**
|
|
17
|
+
* TYPICALLY the GCP Cloud Run service name — and it MUST be the Cloud Run service name
|
|
18
|
+
* when you do not supply a `targetUrl`, because we derive the URL from it.
|
|
19
|
+
*
|
|
20
|
+
* We lookup your service in the same project, same region, and form the url from the
|
|
21
|
+
* container information unless you pass in a targetUrl, so you do not have to maintain
|
|
22
|
+
* targetUrls. This works across your demo, qa, prod environments as long as each
|
|
23
|
+
* environment is in its own projectId, which is typical.
|
|
24
|
+
*
|
|
25
|
+
* When you DO supply a `targetUrl`, svcName is used only for logging, so any readable
|
|
26
|
+
* name works.
|
|
27
|
+
*/
|
|
28
|
+
svcName,
|
|
29
|
+
/**
|
|
30
|
+
* Optional explicit base URL, for the cases lookup cannot describe: another region,
|
|
31
|
+
* another project, or a host that is not Cloud Run at all. It wins over `svcName`.
|
|
32
|
+
*/
|
|
33
|
+
targetUrl) {
|
|
34
|
+
this.svcName = svcName;
|
|
35
|
+
this.targetUrl = targetUrl;
|
|
36
|
+
}
|
|
37
|
+
/** Resolved per enqueue, not at construction — so building a client stays synchronous. */
|
|
38
|
+
resolveTargetUrl() {
|
|
39
|
+
return (0, gcp_identity_1.resolveTargetUrl)(this.svcName, this.targetUrl);
|
|
20
40
|
}
|
|
21
41
|
}
|
|
22
42
|
exports.TaskClientConfig = TaskClientConfig;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"TaskClientConfig.js","sourceRoot":"","sources":["../../../../../packages/cloud/cloudtasks-client/src/TaskClientConfig.ts"],"names":[],"mappings":";;;
|
|
1
|
+
{"version":3,"file":"TaskClientConfig.js","sourceRoot":"","sources":["../../../../../packages/cloud/cloudtasks-client/src/TaskClientConfig.ts"],"names":[],"mappings":";;;AAAA,0DAA2D;AAK3D;;;;;;GAMG;AACH,MAAa,gBAAgB;IAcL;IAMA;IAnBpB;IACI;;;;;;;;;;;OAWG;IACa,OAAe;IAE/B;;;OAGG;IACa,SAAkB;QANlB,YAAO,GAAP,OAAO,CAAQ;QAMf,cAAS,GAAT,SAAS,CAAS;IACnC,CAAC;IAEJ,0FAA0F;IAC1F,gBAAgB;QACZ,OAAO,IAAA,+BAAgB,EAAC,IAAI,CAAC,OAAO,EAAE,IAAI,CAAC,SAAS,CAAC,CAAC;IAC1D,CAAC;CACJ;AA3BD,4CA2BC","sourcesContent":["import { resolveTargetUrl } from '@webpieces/gcp-identity';\n\n/** Constructor whose prototype is T (the abstract @PubSub API class). */\nexport type ApiPrototype<T> = Function & { prototype: T };\n\n/**\n * Per-client STATE for a Cloud Tasks enqueue client — nothing else.\n *\n * Collaborators (TaskInvoker, RequestContextHeaders) are NOT config: they are dependencies of\n * {@link ClientCloudTasksFactory} and shared by every client it builds. This is the fire-and-forget\n * twin of http-client-node's ClientConfig, and takes the same two fields.\n */\nexport class TaskClientConfig {\n constructor(\n /**\n * TYPICALLY the GCP Cloud Run service name — and it MUST be the Cloud Run service name\n * when you do not supply a `targetUrl`, because we derive the URL from it.\n *\n * We lookup your service in the same project, same region, and form the url from the\n * container information unless you pass in a targetUrl, so you do not have to maintain\n * targetUrls. This works across your demo, qa, prod environments as long as each\n * environment is in its own projectId, which is typical.\n *\n * When you DO supply a `targetUrl`, svcName is used only for logging, so any readable\n * name works.\n */\n public readonly svcName: string,\n\n /**\n * Optional explicit base URL, for the cases lookup cannot describe: another region,\n * another project, or a host that is not Cloud Run at all. It wins over `svcName`.\n */\n public readonly targetUrl?: string,\n ) {}\n\n /** Resolved per enqueue, not at construction — so building a client stays synchronous. */\n resolveTargetUrl(): Promise<string> {\n return resolveTargetUrl(this.svcName, this.targetUrl);\n }\n}\n"]}
|
package/src/TaskProxyClient.d.ts
CHANGED
|
@@ -1,27 +1,33 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { RequestContextHeaders } from '@webpieces/core-context';
|
|
2
2
|
import { TaskInvoker } from './TaskTypes';
|
|
3
3
|
import { ApiPrototype, TaskClientConfig } from './TaskClientConfig';
|
|
4
4
|
/**
|
|
5
5
|
* TaskProxyClient - the enqueue engine behind one @PubSub API contract's client proxy.
|
|
6
6
|
*
|
|
7
|
-
* The fire-and-forget twin of http-client's ProxyClient, and the
|
|
8
|
-
*
|
|
9
|
-
*
|
|
7
|
+
* The fire-and-forget twin of http-client-core's ProxyClient, and TWO-PHASE for the same reason:
|
|
8
|
+
* its COLLABORATORS (invoker, headers) come from the container, while the PER-CLIENT state (which
|
|
9
|
+
* contract, which target) arrives on {@link init}. That is what lets {@link ClientCloudTasksFactory}
|
|
10
|
+
* hold a `Provider<TaskProxyClient>` and hand out a fresh, independently-configured client per
|
|
11
|
+
* contract.
|
|
12
|
+
*
|
|
13
|
+
* Calling an endpoint ENQUEUES a task (it does not call remotely); the task is later delivered to
|
|
14
|
+
* the same endpoint's controller through the full server filter chain.
|
|
15
|
+
*
|
|
16
|
+
* It owns:
|
|
10
17
|
* - @ApiPath / @PubSub convention validation + the endpoint plans from the contract's decorators
|
|
11
|
-
* - Resolving the callee's
|
|
12
|
-
* - Context propagation onto the task headers (
|
|
18
|
+
* - Resolving the callee's base URL (from svcName, or the explicit targetUrl)
|
|
19
|
+
* - Context propagation onto the task headers (a credential is never a context key, so none can ride along)
|
|
13
20
|
* - Handing a fully-built TaskRequest to the bound {@link TaskInvoker}
|
|
14
|
-
*
|
|
15
|
-
* Calling an endpoint ENQUEUES a task (it does not call remotely); the task is later
|
|
16
|
-
* delivered to the same endpoint's controller through the full server filter chain.
|
|
17
21
|
*/
|
|
18
22
|
export declare class TaskProxyClient {
|
|
19
|
-
private
|
|
20
|
-
private
|
|
21
|
-
private contextMgr;
|
|
23
|
+
private readonly invoker;
|
|
24
|
+
private readonly headers;
|
|
22
25
|
private plans;
|
|
23
26
|
private apiName;
|
|
24
|
-
|
|
27
|
+
private config;
|
|
28
|
+
constructor(invoker: TaskInvoker, headers: RequestContextHeaders);
|
|
29
|
+
/** Bind this client to one @PubSub contract + target. */
|
|
30
|
+
init(apiClass: ApiPrototype<object>, config: TaskClientConfig): void;
|
|
25
31
|
/** Check whether the contract declares a @PubSub endpoint with this method name. */
|
|
26
32
|
hasEndpoint(methodName: string): boolean;
|
|
27
33
|
/**
|
|
@@ -32,6 +38,22 @@ export declare class TaskProxyClient {
|
|
|
32
38
|
enqueue(methodName: string, requestDto: unknown): Promise<void>;
|
|
33
39
|
/** Endpoint name -> its resolved path / queue / auth mode, read once from the decorators. */
|
|
34
40
|
private buildPlans;
|
|
35
|
-
/**
|
|
41
|
+
/**
|
|
42
|
+
* Every transferred context key (txId/requestId/tenant…), request-id chained.
|
|
43
|
+
* Throws if there is no active RequestContext — an enqueue with no trace is a bug.
|
|
44
|
+
*
|
|
45
|
+
* No credential can appear here: `authorization` is read off the inbound HttpRequest and is not
|
|
46
|
+
* a ContextKey, so it never enters the RequestContext to be transferred. The invoker mints the
|
|
47
|
+
* task's own delivery auth per the endpoint's @AuthOidc / @AuthSharedSecret mode.
|
|
48
|
+
*/
|
|
36
49
|
private buildContextHeaders;
|
|
37
50
|
}
|
|
51
|
+
/**
|
|
52
|
+
* DI token for the `Provider<TaskProxyClient>` that hands out enqueue clients — one per @PubSub
|
|
53
|
+
* contract. `Provider<T>` is erased at runtime, so it cannot be its own token; this Symbol names T.
|
|
54
|
+
*
|
|
55
|
+
* Because TaskProxyClient is bound TRANSIENT, every `get()` constructs a new one. (Were it bound
|
|
56
|
+
* `@provideFrameworkSingleton`, the very same Provider would hand back one lazily-created instance
|
|
57
|
+
* instead — the provider caches nothing, so the target's scope decides.)
|
|
58
|
+
*/
|
|
59
|
+
export declare const TASK_PROXY_CLIENT_PROVIDER: unique symbol;
|
package/src/TaskProxyClient.js
CHANGED
|
@@ -1,20 +1,13 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.TaskProxyClient = void 0;
|
|
3
|
+
exports.TASK_PROXY_CLIENT_PROVIDER = exports.TaskProxyClient = void 0;
|
|
4
4
|
const tslib_1 = require("tslib");
|
|
5
|
+
const inversify_1 = require("inversify");
|
|
5
6
|
const core_util_1 = require("@webpieces/core-util");
|
|
6
7
|
const core_context_1 = require("@webpieces/core-context");
|
|
7
|
-
const gcp_identity_1 = require("@webpieces/gcp-identity");
|
|
8
8
|
const TaskTypes_1 = require("./TaskTypes");
|
|
9
9
|
const ScheduleContext_1 = require("./ScheduleContext");
|
|
10
|
-
const TaskClientConfig_1 = require("./TaskClientConfig");
|
|
11
10
|
const log = core_util_1.LogManager.getLogger('TaskProxyClient');
|
|
12
|
-
/**
|
|
13
|
-
* Auth headers are NEVER propagated from the caller's context onto an enqueued task:
|
|
14
|
-
* the caller's inbound user JWT / secret must not leak to an internal service, and
|
|
15
|
-
* the invoker mints fresh delivery auth (OIDC / shared-secret) per the endpoint's mode.
|
|
16
|
-
*/
|
|
17
|
-
const AUTH_HEADER_NAMES = new Set(['authorization', 'x-webpieces-shared-secret']);
|
|
18
11
|
/** Per-endpoint routing plan resolved once from the contract's decorators. */
|
|
19
12
|
class EndpointPlan {
|
|
20
13
|
path;
|
|
@@ -29,32 +22,40 @@ class EndpointPlan {
|
|
|
29
22
|
/**
|
|
30
23
|
* TaskProxyClient - the enqueue engine behind one @PubSub API contract's client proxy.
|
|
31
24
|
*
|
|
32
|
-
* The fire-and-forget twin of http-client's ProxyClient, and the
|
|
33
|
-
*
|
|
34
|
-
*
|
|
25
|
+
* The fire-and-forget twin of http-client-core's ProxyClient, and TWO-PHASE for the same reason:
|
|
26
|
+
* its COLLABORATORS (invoker, headers) come from the container, while the PER-CLIENT state (which
|
|
27
|
+
* contract, which target) arrives on {@link init}. That is what lets {@link ClientCloudTasksFactory}
|
|
28
|
+
* hold a `Provider<TaskProxyClient>` and hand out a fresh, independently-configured client per
|
|
29
|
+
* contract.
|
|
30
|
+
*
|
|
31
|
+
* Calling an endpoint ENQUEUES a task (it does not call remotely); the task is later delivered to
|
|
32
|
+
* the same endpoint's controller through the full server filter chain.
|
|
33
|
+
*
|
|
34
|
+
* It owns:
|
|
35
35
|
* - @ApiPath / @PubSub convention validation + the endpoint plans from the contract's decorators
|
|
36
|
-
* - Resolving the callee's
|
|
37
|
-
* - Context propagation onto the task headers (
|
|
36
|
+
* - Resolving the callee's base URL (from svcName, or the explicit targetUrl)
|
|
37
|
+
* - Context propagation onto the task headers (a credential is never a context key, so none can ride along)
|
|
38
38
|
* - Handing a fully-built TaskRequest to the bound {@link TaskInvoker}
|
|
39
|
-
*
|
|
40
|
-
* Calling an endpoint ENQUEUES a task (it does not call remotely); the task is later
|
|
41
|
-
* delivered to the same endpoint's controller through the full server filter chain.
|
|
42
39
|
*/
|
|
43
40
|
let TaskProxyClient = class TaskProxyClient {
|
|
44
|
-
config;
|
|
45
41
|
invoker;
|
|
46
|
-
|
|
42
|
+
headers;
|
|
43
|
+
// Assigned by init(), which the factory calls immediately after construction.
|
|
47
44
|
plans;
|
|
48
45
|
apiName;
|
|
49
|
-
|
|
50
|
-
|
|
46
|
+
config;
|
|
47
|
+
constructor(invoker, headers) {
|
|
51
48
|
this.invoker = invoker;
|
|
52
|
-
this.
|
|
49
|
+
this.headers = headers;
|
|
50
|
+
}
|
|
51
|
+
/** Bind this client to one @PubSub contract + target. */
|
|
52
|
+
init(apiClass, config) {
|
|
53
53
|
if (!(0, core_util_1.isApiPath)(apiClass)) {
|
|
54
54
|
throw new Error(`Class ${apiClass.name || 'Unknown'} must be decorated with @ApiPath()`);
|
|
55
55
|
}
|
|
56
56
|
(0, core_util_1.assertPubSubConventions)(apiClass);
|
|
57
57
|
(0, core_util_1.assertEveryEndpointHasAuthMode)(apiClass);
|
|
58
|
+
this.config = config;
|
|
58
59
|
this.apiName = apiClass.name || 'UnknownApi';
|
|
59
60
|
this.plans = this.buildPlans(apiClass);
|
|
60
61
|
}
|
|
@@ -79,11 +80,12 @@ let TaskProxyClient = class TaskProxyClient {
|
|
|
79
80
|
'scheduler.addToQueue(() => taskClient.method(req), { dedupName }).');
|
|
80
81
|
}
|
|
81
82
|
// Resolved lazily (not at client construction) so building a client stays synchronous.
|
|
82
|
-
// Every metadata read beneath
|
|
83
|
+
// Every metadata read beneath resolveTargetUrl is memoized process-wide, so only the
|
|
83
84
|
// first enqueue in the process pays a lookup.
|
|
84
|
-
const targetUrl = await
|
|
85
|
+
const targetUrl = await this.config.resolveTargetUrl();
|
|
85
86
|
const request = new TaskTypes_1.TaskRequest(targetUrl, plan.path, plan.queueName, requestDto, this.buildContextHeaders(), plan.authMode, frame.info ?? new TaskTypes_1.ScheduleInfo());
|
|
86
|
-
|
|
87
|
+
// svcName, not the URL, is the stable name across demo/qa/prod.
|
|
88
|
+
log.debug(`enqueue task ${plan.queueName} -> ${this.config.svcName}${plan.path}`);
|
|
87
89
|
frame.jobRef = await this.invoker.enqueue(request);
|
|
88
90
|
}
|
|
89
91
|
/** Endpoint name -> its resolved path / queue / auth mode, read once from the decorators. */
|
|
@@ -101,22 +103,35 @@ let TaskProxyClient = class TaskProxyClient {
|
|
|
101
103
|
}
|
|
102
104
|
return plans;
|
|
103
105
|
}
|
|
104
|
-
/**
|
|
106
|
+
/**
|
|
107
|
+
* Every transferred context key (txId/requestId/tenant…), request-id chained.
|
|
108
|
+
* Throws if there is no active RequestContext — an enqueue with no trace is a bug.
|
|
109
|
+
*
|
|
110
|
+
* No credential can appear here: `authorization` is read off the inbound HttpRequest and is not
|
|
111
|
+
* a ContextKey, so it never enters the RequestContext to be transferred. The invoker mints the
|
|
112
|
+
* task's own delivery auth per the endpoint's @AuthOidc / @AuthSharedSecret mode.
|
|
113
|
+
*/
|
|
105
114
|
buildContextHeaders() {
|
|
106
|
-
|
|
107
|
-
for (const entry of this.contextMgr.buildOutboundHeaders().entries()) {
|
|
108
|
-
if (!AUTH_HEADER_NAMES.has(entry[0].toLowerCase())) {
|
|
109
|
-
headers.set(entry[0], entry[1]);
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
return headers;
|
|
115
|
+
return this.headers.buildOutboundHeaders();
|
|
113
116
|
}
|
|
114
117
|
};
|
|
115
118
|
exports.TaskProxyClient = TaskProxyClient;
|
|
116
119
|
exports.TaskProxyClient = TaskProxyClient = tslib_1.__decorate([
|
|
117
|
-
(0,
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
120
|
+
(0, core_context_1.provideFrameworkTransient)(),
|
|
121
|
+
(0, inversify_1.injectable)(),
|
|
122
|
+
tslib_1.__param(0, (0, inversify_1.inject)(TaskTypes_1.TaskInvoker)),
|
|
123
|
+
tslib_1.__param(1, (0, inversify_1.inject)(core_context_1.RequestContextHeaders)),
|
|
124
|
+
tslib_1.__metadata("design:paramtypes", [TaskTypes_1.TaskInvoker,
|
|
125
|
+
core_context_1.RequestContextHeaders])
|
|
121
126
|
], TaskProxyClient);
|
|
127
|
+
/**
|
|
128
|
+
* DI token for the `Provider<TaskProxyClient>` that hands out enqueue clients — one per @PubSub
|
|
129
|
+
* contract. `Provider<T>` is erased at runtime, so it cannot be its own token; this Symbol names T.
|
|
130
|
+
*
|
|
131
|
+
* Because TaskProxyClient is bound TRANSIENT, every `get()` constructs a new one. (Were it bound
|
|
132
|
+
* `@provideFrameworkSingleton`, the very same Provider would hand back one lazily-created instance
|
|
133
|
+
* instead — the provider caches nothing, so the target's scope decides.)
|
|
134
|
+
*/
|
|
135
|
+
// webpieces-disable no-symbol-di-tokens -- Provider<T> is erased at runtime; the Symbol names T
|
|
136
|
+
exports.TASK_PROXY_CLIENT_PROVIDER = Symbol.for('Provider<TaskProxyClient>');
|
|
122
137
|
//# sourceMappingURL=TaskProxyClient.js.map
|
|
@@ -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 {\n isApiPath,\n getApiPath,\n getEndpoints,\n getAuthMode,\n getQueueName,\n assertPubSubConventions,\n assertEveryEndpointHasAuthMode,\n AuthMode,\n DocumentDesign,\n LogManager,\n} from '@webpieces/core-util';\nimport { ContextMgr } from '@webpieces/core-context';\nimport { getCloudRunUrl } from '@webpieces/gcp-identity';\nimport { TaskInvoker, TaskRequest, ScheduleInfo } from './TaskTypes';\nimport { currentScheduleFrame } from './ScheduleContext';\nimport { ApiPrototype, TaskClientConfig } from './TaskClientConfig';\n\nconst log = LogManager.getLogger('TaskProxyClient');\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/** 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 * TaskProxyClient - the enqueue engine behind one @PubSub API contract's client proxy.\n *\n * The fire-and-forget twin of http-client's ProxyClient, and the @DocumentDesign design\n * root for this package: its constructor params ARE the enqueue client's dependency graph.\n * Built by {@link ClientCloudTasksFactory} (one per API contract), it owns:\n * - @ApiPath / @PubSub convention validation + the endpoint plans from the contract's decorators\n * - Resolving the callee's Cloud Run base URL from the service name\n * - Context propagation onto the task headers (MINUS the caller's auth headers)\n * - Handing a fully-built TaskRequest to the bound {@link TaskInvoker}\n *\n * Calling an endpoint ENQUEUES a task (it does not call remotely); the task is later\n * delivered to the same endpoint's controller through the full server filter chain.\n */\n@DocumentDesign()\nexport class TaskProxyClient {\n private plans: Map<string, EndpointPlan>;\n private apiName: string;\n\n constructor(\n apiClass: ApiPrototype<object>,\n private config: TaskClientConfig,\n private invoker: TaskInvoker,\n private contextMgr: ContextMgr,\n ) {\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 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), { dedupName });\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), { dedupName }).',\n );\n }\n\n // Resolved lazily (not at client construction) so building a client stays synchronous.\n // Every metadata read beneath getCloudRunUrl is memoized process-wide, so only the\n // first enqueue in the process pays a lookup.\n const targetUrl = await getCloudRunUrl(this.config.gcpCloudRunSvcName);\n\n const request = new TaskRequest(\n targetUrl,\n plan.path,\n plan.queueName,\n requestDto,\n this.buildContextHeaders(),\n plan.authMode,\n frame.info ?? new ScheduleInfo(),\n );\n\n log.debug(`enqueue task ${plan.queueName} -> ${targetUrl}${plan.path}`);\n frame.jobRef = await this.invoker.enqueue(request);\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 authMode = getAuthMode(apiClass, methodName);\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 );\n plans.set(methodName, plan);\n }\n return plans;\n }\n\n /** Transferred context keys (txId/requestId/tenant…) MINUS the caller's auth credentials. */\n private buildContextHeaders(): Map<string, string> {\n const headers = new Map<string, string>();\n for (const entry of this.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}\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, injectable } from 'inversify';\nimport {\n isApiPath,\n getApiPath,\n getEndpoints,\n getAuthMode,\n getQueueName,\n assertPubSubConventions,\n assertEveryEndpointHasAuthMode,\n AuthMode,\n LogManager,\n} from '@webpieces/core-util';\nimport {\n RequestContextHeaders,\n provideFrameworkTransient,\n} from '@webpieces/core-context';\nimport { TaskInvoker, TaskRequest, ScheduleInfo } from './TaskTypes';\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\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 * 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, or the explicit targetUrl)\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()\n@injectable()\nexport class TaskProxyClient {\n // Assigned by init(), which the factory calls immediately after construction.\n private plans!: Map<string, EndpointPlan>;\n private apiName!: string;\n private config!: TaskClientConfig;\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 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 this.config = config;\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), { dedupName });\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), { dedupName }).',\n );\n }\n\n // Resolved lazily (not at client construction) so building a client stays synchronous.\n // Every metadata read beneath resolveTargetUrl is memoized process-wide, so only the\n // first enqueue in the process pays a lookup.\n const targetUrl = await this.config.resolveTargetUrl();\n\n const request = new TaskRequest(\n targetUrl,\n plan.path,\n plan.queueName,\n requestDto,\n this.buildContextHeaders(),\n plan.authMode,\n frame.info ?? new ScheduleInfo(),\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 frame.jobRef = await this.invoker.enqueue(request);\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 authMode = getAuthMode(apiClass, methodName);\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 );\n plans.set(methodName, plan);\n }\n return plans;\n }\n\n /**\n * Every transferred context key (txId/requestId/tenant…), request-id chained.\n * Throws if there is no active RequestContext — an enqueue with no trace is 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 @AuthOidc / @AuthSharedSecret mode.\n */\n private buildContextHeaders(): Map<string, string> {\n return this.headers.buildOutboundHeaders();\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"]}
|
package/src/index.d.ts
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
*/
|
|
9
9
|
export { ScheduleInfo, JobReference, TaskRequest, TaskInvoker, } from './TaskTypes';
|
|
10
10
|
export { ClientCloudTasksFactory } from './ClientCloudTasksFactory';
|
|
11
|
-
export { TaskProxyClient } from './TaskProxyClient';
|
|
11
|
+
export { TaskProxyClient, TASK_PROXY_CLIENT_PROVIDER } from './TaskProxyClient';
|
|
12
12
|
export { TaskClientConfig } from './TaskClientConfig';
|
|
13
13
|
export type { ApiPrototype } from './TaskClientConfig';
|
|
14
14
|
export { CloudTaskScheduler, ScheduleOptions } from './CloudTaskScheduler';
|
package/src/index.js
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* delivered to the same endpoint through the full server filter chain.
|
|
9
9
|
*/
|
|
10
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.TaskClientConfig = exports.TaskProxyClient = exports.ClientCloudTasksFactory = exports.TaskInvoker = exports.TaskRequest = exports.JobReference = exports.ScheduleInfo = void 0;
|
|
11
|
+
exports.clearScheduleFrame = exports.currentScheduleFrame = exports.setScheduleFrame = exports.ScheduleFrame = exports.GcpTaskInvoker = exports.InMemoryTaskInvoker = exports.ScheduleOptions = exports.CloudTaskScheduler = exports.TaskClientConfig = exports.TASK_PROXY_CLIENT_PROVIDER = exports.TaskProxyClient = exports.ClientCloudTasksFactory = exports.TaskInvoker = exports.TaskRequest = exports.JobReference = exports.ScheduleInfo = void 0;
|
|
12
12
|
var TaskTypes_1 = require("./TaskTypes");
|
|
13
13
|
Object.defineProperty(exports, "ScheduleInfo", { enumerable: true, get: function () { return TaskTypes_1.ScheduleInfo; } });
|
|
14
14
|
Object.defineProperty(exports, "JobReference", { enumerable: true, get: function () { return TaskTypes_1.JobReference; } });
|
|
@@ -18,6 +18,7 @@ var ClientCloudTasksFactory_1 = require("./ClientCloudTasksFactory");
|
|
|
18
18
|
Object.defineProperty(exports, "ClientCloudTasksFactory", { enumerable: true, get: function () { return ClientCloudTasksFactory_1.ClientCloudTasksFactory; } });
|
|
19
19
|
var TaskProxyClient_1 = require("./TaskProxyClient");
|
|
20
20
|
Object.defineProperty(exports, "TaskProxyClient", { enumerable: true, get: function () { return TaskProxyClient_1.TaskProxyClient; } });
|
|
21
|
+
Object.defineProperty(exports, "TASK_PROXY_CLIENT_PROVIDER", { enumerable: true, get: function () { return TaskProxyClient_1.TASK_PROXY_CLIENT_PROVIDER; } });
|
|
21
22
|
var TaskClientConfig_1 = require("./TaskClientConfig");
|
|
22
23
|
Object.defineProperty(exports, "TaskClientConfig", { enumerable: true, get: function () { return TaskClientConfig_1.TaskClientConfig; } });
|
|
23
24
|
var CloudTaskScheduler_1 = require("./CloudTaskScheduler");
|
package/src/index.js.map
CHANGED
|
@@ -1 +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,qEAAoE;AAA3D,kIAAA,uBAAuB,OAAA;AAChC,
|
|
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,qEAAoE;AAA3D,kIAAA,uBAAuB,OAAA;AAChC,qDAAgF;AAAvE,kHAAA,eAAe,OAAA;AAAE,6HAAA,0BAA0B,OAAA;AACpD,uDAAsD;AAA7C,oHAAA,gBAAgB,OAAA;AAEzB,2DAA2E;AAAlE,wHAAA,kBAAkB,OAAA;AAAE,qHAAA,eAAe,OAAA;AAC5C,2FAA2F;AAC3F,6DAA4D;AAAnD,0HAAA,mBAAmB,OAAA;AAC5B,mDAAkD;AAAzC,gHAAA,cAAc,OAAA;AACvB,4FAA4F;AAC5F,2FAA2F;AAC3F,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 { ClientCloudTasksFactory } from './ClientCloudTasksFactory';\nexport { TaskProxyClient, TASK_PROXY_CLIENT_PROVIDER } from './TaskProxyClient';\nexport { TaskClientConfig } from './TaskClientConfig';\nexport type { ApiPrototype } from './TaskClientConfig';\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: server-side delivery-auth is enforced by the framework AuthFilter (AuthMode-driven)\n// in @webpieces/http-routing — a client library has no server filters / routing machinery.\nexport {\n ScheduleFrame,\n setScheduleFrame,\n currentScheduleFrame,\n clearScheduleFrame,\n} from './ScheduleContext';\n"]}
|