@webpieces/cloudtasks-client 0.4.894 → 0.4.895
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 +74 -13
- package/package.json +10 -5
- package/src/ClientCloudTasksFactory.d.ts +1 -1
- package/src/ClientCloudTasksFactory.js +1 -1
- package/src/ClientCloudTasksFactory.js.map +1 -1
- package/src/CloudTaskScheduler.d.ts +37 -13
- package/src/CloudTaskScheduler.js +38 -14
- package/src/CloudTaskScheduler.js.map +1 -1
- package/src/CloudTasksClientProvider.d.ts +29 -0
- package/src/CloudTasksClientProvider.js +41 -0
- package/src/CloudTasksClientProvider.js.map +1 -0
- package/src/GcpTaskInvoker.d.ts +27 -4
- package/src/GcpTaskInvoker.js +61 -19
- package/src/GcpTaskInvoker.js.map +1 -1
- package/src/GcpTaskRegistry.d.ts +31 -0
- package/src/GcpTaskRegistry.js +79 -0
- package/src/GcpTaskRegistry.js.map +1 -0
- package/src/GrpcErrorMapper.d.ts +27 -0
- package/src/GrpcErrorMapper.js +52 -0
- package/src/GrpcErrorMapper.js.map +1 -0
- package/src/InMemoryTaskInvoker.d.ts +9 -1
- package/src/InMemoryTaskInvoker.js +16 -3
- package/src/InMemoryTaskInvoker.js.map +1 -1
- package/src/TaskAlreadyExistsError.d.ts +18 -0
- package/src/TaskAlreadyExistsError.js +26 -0
- package/src/TaskAlreadyExistsError.js.map +1 -0
- package/src/TaskName.d.ts +33 -0
- package/src/TaskName.js +71 -0
- package/src/TaskName.js.map +1 -0
- package/src/TaskProxyClient.d.ts +32 -1
- package/src/TaskProxyClient.js +77 -27
- package/src/TaskProxyClient.js.map +1 -1
- package/src/TaskTypes.d.ts +110 -23
- package/src/TaskTypes.js +72 -22
- package/src/TaskTypes.js.map +1 -1
- package/src/__tests__/FakeCloudTasksServer.d.ts +51 -0
- package/src/__tests__/FakeCloudTasksServer.js +121 -0
- package/src/__tests__/FakeCloudTasksServer.js.map +1 -0
- package/src/index.d.ts +7 -1
- package/src/index.js +13 -1
- package/src/index.js.map +1 -1
package/README.md
CHANGED
|
@@ -17,11 +17,11 @@ abstract class EmailApi {
|
|
|
17
17
|
|
|
18
18
|
// build the client once (sync); 'email-svc' is the callee's Cloud Run service name
|
|
19
19
|
const emailTasks = factory.createPubSubClient(EmailApi, new TaskClientConfig('email-svc'));
|
|
20
|
-
// ...or pin a URL lookup cannot describe (other region/project); svcName stays the log name
|
|
21
|
-
const other = factory.createPubSubClient(EmailApi, new TaskClientConfig('email-svc', 'https://email.eu.example'));
|
|
22
20
|
|
|
23
21
|
// producer (inside a request → RequestContext active)
|
|
24
|
-
await scheduler.addToQueue(() => emailTasks.sendEmail(req),
|
|
22
|
+
await scheduler.addToQueue(() => emailTasks.sendEmail(req)); // retried, auto-named
|
|
23
|
+
await scheduler.addToQueue(() => emailTasks.sendEmail(req), new ScheduleOptions(TaskName.dedup(req.id), 900));
|
|
24
|
+
await scheduler.schedule(() => emailTasks.sendEmail(req), Date.now() + 60_000, new ScheduleOptions(TaskName.dedup(req.id)));
|
|
25
25
|
```
|
|
26
26
|
|
|
27
27
|
- `ClientCloudTasksFactory.createPubSubClient(Api, TaskClientConfig)` — builds the enqueue proxy. It
|
|
@@ -35,13 +35,74 @@ await scheduler.addToQueue(() => emailTasks.sendEmail(req), { dedupName: req.id
|
|
|
35
35
|
- An enqueue outside `RequestContext.run(...)` **throws**: a task with no caller trace is a bug
|
|
36
36
|
- `CloudTaskScheduler` — `addToQueue` / `schedule` / `cancelJob`; carries scheduling
|
|
37
37
|
options out-of-band so the contract signature stays identical on both sides
|
|
38
|
-
- `TaskInvoker` (abstract token) with two impls:
|
|
39
|
-
- `GcpTaskInvoker` — real `@google-cloud/tasks` delivery (OIDC / shared-secret)
|
|
40
|
-
- `InMemoryTaskInvoker` —
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
Enqueue
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
38
|
+
- `TaskInvoker` (abstract token) with two impls, each making ONE attempt per call:
|
|
39
|
+
- `GcpTaskInvoker` — real `@google-cloud/tasks` 6.3.0 (google-gax 5) delivery (OIDC / shared-secret)
|
|
40
|
+
- `InMemoryTaskInvoker` — delivers over real HTTP to the target's filter chain (tests + local dev,
|
|
41
|
+
no GCP, production parity); ignores the customizer and gax, but still runs the same retry paths
|
|
42
|
+
|
|
43
|
+
## Enqueue retries (this is ENQUEUE, not delivery)
|
|
44
|
+
|
|
45
|
+
The `createTask` gRPC call is retried by webpieces' shared `RetryStrategy` — the same
|
|
46
|
+
`CallRegistry.execute` the RPC clients use, with the enqueue transport default
|
|
47
|
+
`RetryStrategy(5_000, 3, 500, 2, 4_000)`: 5 s per attempt, 3 retries, backoff 500 ms ×2 capped at
|
|
48
|
+
4 s. Only a transient failure is retried (gRPC UNAVAILABLE, DEADLINE_EXCEEDED, RESOURCE_EXHAUSTED →
|
|
49
|
+
`GrpcErrorMapper`); giving up throws a 502 `ApiRetriesExhaustedError`. Each retried attempt logs
|
|
50
|
+
`WARN [API-client-attempt-TRANSIENT]`. Delivery retries (Cloud Tasks re-POSTing the target) are the
|
|
51
|
+
queue's `retryConfig` and are out of scope here.
|
|
52
|
+
|
|
53
|
+
Retries are safe because every enqueue is NAMED ONCE, before the first attempt:
|
|
54
|
+
|
|
55
|
+
| `ScheduleOptions.taskName` | name | a retry after a lost response |
|
|
56
|
+
|---|---|---|
|
|
57
|
+
| `TaskName.generated()` (default) | one UUID per logical enqueue | ALREADY_EXISTS → success |
|
|
58
|
+
| `TaskName.dedup(key)` | your key (MessageSid, org+period, …): true dedup across callers | ALREADY_EXISTS → success |
|
|
59
|
+
| `TaskName.serverAssigned()` | none — fastest `createTask` | may create a duplicate |
|
|
60
|
+
|
|
61
|
+
ALREADY_EXISTS is success: `WARN [ENQUEUE-DEDUP]`, and the `JobReference` comes back with
|
|
62
|
+
`alreadyExisted: true`. Delete any hand-rolled `isAlreadyExists` catch. A used name stays blocked
|
|
63
|
+
for about an hour after its task runs or is deleted.
|
|
64
|
+
|
|
65
|
+
## Three levels of Google settings
|
|
66
|
+
|
|
67
|
+
```ts
|
|
68
|
+
new ScheduleOptions(taskName, taskTimeoutSeconds?, customizeTask?, gax?)
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
- **`customizeTask`** (or `GcpTaskRegistry.setCustomizer(c, ...scope)`) — the Google
|
|
72
|
+
`ICreateTaskRequest`, run LAST on every attempt's freshly built request. Request only, never timing.
|
|
73
|
+
Call site wins over the registry (method → API → ALL).
|
|
74
|
+
- **`gax`** (or `GcpTaskRegistry.setGax(options, ...scope)`) — `CallOptions` for the one `createTask`
|
|
75
|
+
RPC. **Set ⇒ you own timing:** no webpieces strategy, ONE attempt, `createTask(request, yourGax)`
|
|
76
|
+
as given, under a safety cap from `CallRegistry`'s timeout ladder (method → API → ALL → 30 s). Raise
|
|
77
|
+
the cap with `CallRegistry.setTimeout(60_000, EmailApi)` to give gax retries room. Exactly one retry
|
|
78
|
+
layer per enqueue — ours or gax's, never both.
|
|
79
|
+
- **The client** — override `CloudTasksClientProvider` for channel/client settings:
|
|
80
|
+
|
|
81
|
+
```ts
|
|
82
|
+
class KeepaliveCloudTasks extends CloudTasksClientProvider {
|
|
83
|
+
override create(): CloudTasksClient {
|
|
84
|
+
return new CloudTasksClient({ 'grpc.keepalive_time_ms': 30_000 });
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
(await options.rebind(CloudTasksClientProvider)).to(KeepaliveCloudTasks).inSingletonScope();
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
Unbound, it is `new CloudTasksClient()` — nobody has to bind anything. (A provider class rather than
|
|
91
|
+
an optional `CloudTasksClient` binding, because the app container's inversify `autobind` tries to
|
|
92
|
+
construct any unbound class token, `@optional()` included, and fails on `CloudTasksClient`.)
|
|
93
|
+
|
|
94
|
+
```ts
|
|
95
|
+
// you own timing AND retries (gax retries UNAVAILABLE); raise the safety cap
|
|
96
|
+
await scheduler.addToQueue(() => api.sendEmail(req),
|
|
97
|
+
new ScheduleOptions(TaskName.dedup(id), undefined, undefined, { timeout: 10_000, retry: { retryCodes: [14] } }));
|
|
98
|
+
CallRegistry.setTimeout(60_000, EmailApi);
|
|
99
|
+
|
|
100
|
+
// process-wide defaults, no call-site edits
|
|
101
|
+
GcpTaskRegistry.setCustomizer((request) => { request.task!.dispatchDeadline = { seconds: 900 }; return request; }, 'ALL');
|
|
102
|
+
CallRegistry.setStrategy(new RetryStrategy(2_000, 5, 200, 2, 2_000).run, WhatsAppProcessApi); // tune OUR retries
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
`createTask`'s public promise exposes no `cancel()`; on the default path the gRPC deadline equals the
|
|
106
|
+
attempt deadline, so gRPC cancels a hung RPC itself. See [client timeouts](../../../docs/client-timeouts.md).
|
|
107
|
+
`GcpTaskInvoker.grpc.spec.ts` proves all of it against a local fake gRPC server with the real
|
|
108
|
+
`CloudTasksClient`.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@webpieces/cloudtasks-client",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.895",
|
|
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,11 +23,16 @@
|
|
|
23
23
|
"access": "public"
|
|
24
24
|
},
|
|
25
25
|
"dependencies": {
|
|
26
|
-
"@webpieces/core-context": "0.4.
|
|
27
|
-
"@webpieces/core-util": "0.4.
|
|
28
|
-
"@webpieces/gcp-identity": "0.4.
|
|
29
|
-
"@google-cloud/tasks": "
|
|
26
|
+
"@webpieces/core-context": "0.4.895",
|
|
27
|
+
"@webpieces/core-util": "0.4.895",
|
|
28
|
+
"@webpieces/gcp-identity": "0.4.895",
|
|
29
|
+
"@google-cloud/tasks": "6.3.0",
|
|
30
|
+
"google-gax": "5.0.8",
|
|
30
31
|
"inversify": "7.10.4",
|
|
31
32
|
"reflect-metadata": "0.2.2"
|
|
33
|
+
},
|
|
34
|
+
"devDependencies": {
|
|
35
|
+
"@grpc/grpc-js": "1.14.4",
|
|
36
|
+
"@grpc/proto-loader": "0.8.1"
|
|
32
37
|
}
|
|
33
38
|
}
|
|
@@ -11,7 +11,7 @@ import { ApiPrototype, TaskClientConfig } from './TaskClientConfig';
|
|
|
11
11
|
* ```typescript
|
|
12
12
|
* // same project + region as this container; the URL is derived, you maintain nothing
|
|
13
13
|
* const emailTasks = factory.createPubSubClient(EmailApi, new TaskClientConfig('email-svc'));
|
|
14
|
-
* await scheduler.addToQueue(() => emailTasks.sendEmail(req),
|
|
14
|
+
* await scheduler.addToQueue(() => emailTasks.sendEmail(req), new ScheduleOptions(TaskName.dedup(id)));
|
|
15
15
|
* ```
|
|
16
16
|
*
|
|
17
17
|
* Every client it builds gets its OWN {@link TaskProxyClient} from the injected
|
|
@@ -27,7 +27,7 @@ const FRAMEWORK_INSPECTION_PROPERTIES = new Set([
|
|
|
27
27
|
* ```typescript
|
|
28
28
|
* // same project + region as this container; the URL is derived, you maintain nothing
|
|
29
29
|
* const emailTasks = factory.createPubSubClient(EmailApi, new TaskClientConfig('email-svc'));
|
|
30
|
-
* await scheduler.addToQueue(() => emailTasks.sendEmail(req),
|
|
30
|
+
* await scheduler.addToQueue(() => emailTasks.sendEmail(req), new ScheduleOptions(TaskName.dedup(id)));
|
|
31
31
|
* ```
|
|
32
32
|
*
|
|
33
33
|
* Every client it builds gets its OWN {@link TaskProxyClient} from the injected
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ClientCloudTasksFactory.js","sourceRoot":"","sources":["../../../../../packages/cloud/cloudtasks-client/src/ClientCloudTasksFactory.ts"],"names":[],"mappings":";;;;AAAA,yCAAmC;AACnC,oDAAmE;AACnE,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;AAGI,IAAM,uBAAuB,GAA7B,MAAM,uBAAuB;IAEyB;IADzD,YACyD,uBAAkD;QAAlD,4BAAuB,GAAvB,uBAAuB,CAA2B;IACxG,CAAC;IAEJ,kFAAkF;IAClF,kBAAkB,CAAmB,QAAyB,EAAE,MAAwB;QACpF,8DAA8D;QAC9D,MAAM,WAAW,GAAG,IAAI,CAAC,uBAAuB,CAAC,GAAG,EAAE,CAAC;QACvD,WAAW,CAAC,IAAI,CAAC,QAAQ,EAAE,MAAM,CAAC,CAAC;QACnC,MAAM,OAAO,GAAG,QAAQ,CAAC,IAAI,IAAI,YAAY,CAAC;QAE9C,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,kFAAkF;gBAClF,gDAAgD;gBAChD,oFAAoF;gBACpF,MAAM,YAAY,GAAG,CAAC,UAAmB,EAAiB,EAAE;oBACxD,MAAM,QAAQ,GAAG,uBAAW,CAAC,OAAO,CAAC,OAAO,EAAE,IAAI,EAAE,YAAY,CAAC,CAAC;oBAClE,6EAA6E;oBAC7E,OAAO,WAAW,CAAC,OAAO,CAAC,IAAI,EAAE,UAAU,CAAC,CAAC,KAAK,CAAC,CAAC,OAAgB,EAAE,EAAE;wBACpE,QAAQ,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAC;wBAC3B,6GAA6G;wBAC7G,MAAM,OAAO,CAAC;oBAClB,CAAC,CAAC,CAAC;gBACP,CAAC,CAAC;gBACF,OAAO,YAAY,CAAC;YACxB,CAAC;SACJ,CAAC,CAAC;IACP,CAAC;CACJ,CAAA;AAxCY,0DAAuB;kCAAvB,uBAAuB;IAFnC,IAAA,0BAAc,GAAE;IAChB,IAAA,wCAAyB,GAAE;IAGnB,mBAAA,IAAA,kBAAM,EAAC,4CAA0B,CAAC,CAAA;6CAA2C,uBAAQ;GAFjF,uBAAuB,CAwCnC","sourcesContent":["import { inject } from 'inversify';\nimport { ApiCallSite, 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.createPubSubClient(EmailApi, new TaskClientConfig('email-svc'));\n * await scheduler.addToQueue(() => emailTasks.sendEmail(req),
|
|
1
|
+
{"version":3,"file":"ClientCloudTasksFactory.js","sourceRoot":"","sources":["../../../../../packages/cloud/cloudtasks-client/src/ClientCloudTasksFactory.ts"],"names":[],"mappings":";;;;AAAA,yCAAmC;AACnC,oDAAmE;AACnE,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;AAGI,IAAM,uBAAuB,GAA7B,MAAM,uBAAuB;IAEyB;IADzD,YACyD,uBAAkD;QAAlD,4BAAuB,GAAvB,uBAAuB,CAA2B;IACxG,CAAC;IAEJ,kFAAkF;IAClF,kBAAkB,CAAmB,QAAyB,EAAE,MAAwB;QACpF,8DAA8D;QAC9D,MAAM,WAAW,GAAG,IAAI,CAAC,uBAAuB,CAAC,GAAG,EAAE,CAAC;QACvD,WAAW,CAAC,IAAI,CAAC,QAAQ,EAAE,MAAM,CAAC,CAAC;QACnC,MAAM,OAAO,GAAG,QAAQ,CAAC,IAAI,IAAI,YAAY,CAAC;QAE9C,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,kFAAkF;gBAClF,gDAAgD;gBAChD,oFAAoF;gBACpF,MAAM,YAAY,GAAG,CAAC,UAAmB,EAAiB,EAAE;oBACxD,MAAM,QAAQ,GAAG,uBAAW,CAAC,OAAO,CAAC,OAAO,EAAE,IAAI,EAAE,YAAY,CAAC,CAAC;oBAClE,6EAA6E;oBAC7E,OAAO,WAAW,CAAC,OAAO,CAAC,IAAI,EAAE,UAAU,CAAC,CAAC,KAAK,CAAC,CAAC,OAAgB,EAAE,EAAE;wBACpE,QAAQ,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAC;wBAC3B,6GAA6G;wBAC7G,MAAM,OAAO,CAAC;oBAClB,CAAC,CAAC,CAAC;gBACP,CAAC,CAAC;gBACF,OAAO,YAAY,CAAC;YACxB,CAAC;SACJ,CAAC,CAAC;IACP,CAAC;CACJ,CAAA;AAxCY,0DAAuB;kCAAvB,uBAAuB;IAFnC,IAAA,0BAAc,GAAE;IAChB,IAAA,wCAAyB,GAAE;IAGnB,mBAAA,IAAA,kBAAM,EAAC,4CAA0B,CAAC,CAAA;6CAA2C,uBAAQ;GAFjF,uBAAuB,CAwCnC","sourcesContent":["import { inject } from 'inversify';\nimport { ApiCallSite, 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.createPubSubClient(EmailApi, new TaskClientConfig('email-svc'));\n * await scheduler.addToQueue(() => emailTasks.sendEmail(req), new ScheduleOptions(TaskName.dedup(id)));\n * ```\n *\n * Every client it builds gets its OWN {@link TaskProxyClient} from the injected\n * `Provider<TaskProxyClient>` (bound transient), which `createPubSubClient` 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()\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 createPubSubClient<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 const apiName = apiClass.name || 'UnknownApi';\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 // Same call-site capture as the HTTP clients (#1175): synchronous, trimmed to the\n // caller, parked on the SAME rejected instance.\n // webpieces-disable no-any-unknown -- request DTO type is erased at the proxy layer\n const clientMethod = (requestDto: unknown): Promise<void> => {\n const callSite = ApiCallSite.capture(apiName, prop, clientMethod);\n // webpieces-disable no-any-unknown -- a rejection value is genuinely unknown\n return proxyClient.enqueue(prop, requestDto).catch((failure: unknown) => {\n callSite.attachTo(failure);\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions -- rethrows the caller's own failure unchanged\n throw failure;\n });\n };\n return clientMethod;\n },\n });\n }\n}\n"]}
|
|
@@ -1,23 +1,45 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import type { CallOptions } from 'google-gax';
|
|
2
|
+
import { TaskInvoker, JobReference, TaskCustomizer } from './TaskTypes';
|
|
3
|
+
import { TaskName } from './TaskName';
|
|
2
4
|
/**
|
|
3
|
-
* Options for a scheduled task. These are QUEUE/TRANSPORT knobs (
|
|
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
|
-
*
|
|
5
|
+
* Options for a scheduled task. These are QUEUE/TRANSPORT knobs (naming, deadline, the Google
|
|
6
|
+
* request, gax) that belong on the enqueue CALL, never on the shared API method — the controller
|
|
7
|
+
* implementing that method has no use for them. See CloudTaskScheduler's class doc for the full
|
|
8
|
+
* rationale.
|
|
9
|
+
*
|
|
10
|
+
* Three levels, three owners:
|
|
11
|
+
* - the `CloudTasksClient` (via an overridden `CloudTasksClientProvider`): the channel and client
|
|
12
|
+
* defaults (keepalive, endpoint, clientConfig);
|
|
13
|
+
* - `gax` here, or `GcpTaskRegistry.setGax(...)`: how the ONE `createTask` RPC of this enqueue
|
|
14
|
+
* behaves. Set ⇒ YOU own timing: no webpieces retries, one attempt, under a safety cap;
|
|
15
|
+
* - `customizeTask` here, or `GcpTaskRegistry.setCustomizer(...)`: the Google request itself.
|
|
7
16
|
*/
|
|
8
17
|
export declare class ScheduleOptions {
|
|
9
|
-
/**
|
|
10
|
-
|
|
11
|
-
/** Per-task dispatch deadline in seconds. */
|
|
12
|
-
taskTimeoutSeconds?: number;
|
|
13
|
-
|
|
18
|
+
/** How the task is named. Default: one generated UUID per logical enqueue, reused on retry. */
|
|
19
|
+
readonly taskName: TaskName;
|
|
20
|
+
/** Per-task dispatch deadline in seconds (`task.dispatchDeadline`). */
|
|
21
|
+
readonly taskTimeoutSeconds?: number | undefined;
|
|
22
|
+
/** Changes the Google request on every attempt; NEVER gax. See {@link TaskCustomizer}. */
|
|
23
|
+
readonly customizeTask?: TaskCustomizer | undefined;
|
|
24
|
+
/** gax `CallOptions` for `createTask`. Set ⇒ you own timing/retries for this enqueue. */
|
|
25
|
+
readonly gax?: CallOptions | undefined;
|
|
26
|
+
constructor(
|
|
27
|
+
/** How the task is named. Default: one generated UUID per logical enqueue, reused on retry. */
|
|
28
|
+
taskName?: TaskName,
|
|
29
|
+
/** Per-task dispatch deadline in seconds (`task.dispatchDeadline`). */
|
|
30
|
+
taskTimeoutSeconds?: number | undefined,
|
|
31
|
+
/** Changes the Google request on every attempt; NEVER gax. See {@link TaskCustomizer}. */
|
|
32
|
+
customizeTask?: TaskCustomizer | undefined,
|
|
33
|
+
/** gax `CallOptions` for `createTask`. Set ⇒ you own timing/retries for this enqueue. */
|
|
34
|
+
gax?: CallOptions | undefined);
|
|
14
35
|
}
|
|
15
36
|
/**
|
|
16
37
|
* Schedules Cloud Tasks. You wrap an enqueue-client call in a lambda so the shared
|
|
17
38
|
* contract's method signature stays `foo(req)` on both client and controller; the
|
|
18
39
|
* scheduling options ride out-of-band in the schedule frame:
|
|
19
40
|
*
|
|
20
|
-
* await scheduler.addToQueue(() => taskClient.sendEmail(req)
|
|
41
|
+
* await scheduler.addToQueue(() => taskClient.sendEmail(req));
|
|
42
|
+
* await scheduler.addToQueue(() => taskClient.sendEmail(req), new ScheduleOptions(TaskName.dedup(id), 900));
|
|
21
43
|
* await scheduler.schedule(() => taskClient.sendEmail(req), runAtEpochMs);
|
|
22
44
|
*
|
|
23
45
|
* Must be called inside an active RequestContext (a server request scope).
|
|
@@ -25,12 +47,12 @@ export declare class ScheduleOptions {
|
|
|
25
47
|
* WHY scheduling options are here and NOT parameters on the API method
|
|
26
48
|
* ------------------------------------------------------------------------
|
|
27
49
|
* The enqueue client and the controller implement the SAME abstract API method
|
|
28
|
-
* (`sendEmail(req): Promise<void>`). Scheduling options —
|
|
50
|
+
* (`sendEmail(req): Promise<void>`). Scheduling options — the task name, "run in the
|
|
29
51
|
* future", `taskTimeoutSeconds` — are QUEUE/TRANSPORT concerns, not part of the
|
|
30
52
|
* business contract, so they must not appear on the method signature:
|
|
31
53
|
*
|
|
32
54
|
* - They are meaningless on the SERVER side. The controller's `sendEmail` just does
|
|
33
|
-
* the work (send the email). It has no use for
|
|
55
|
+
* the work (send the email). It has no use for a task name (a Cloud Tasks resource
|
|
34
56
|
* name), a future run-time (Cloud Tasks already delivered it), or a dispatch
|
|
35
57
|
* deadline. Putting them on the method would force the controller to accept and
|
|
36
58
|
* ignore transport metadata it can't act on.
|
|
@@ -56,5 +78,7 @@ export declare class CloudTaskScheduler {
|
|
|
56
78
|
schedule(runnable: () => Promise<void>, epochMsToRunAt: number, opts?: ScheduleOptions): Promise<JobReference>;
|
|
57
79
|
/** Cancel a previously scheduled task. */
|
|
58
80
|
cancelJob(ref: JobReference): Promise<void>;
|
|
81
|
+
/** No options = a generated name, no deadline, no customizer, webpieces-owned timing. */
|
|
82
|
+
private infoFor;
|
|
59
83
|
private runWithFrame;
|
|
60
84
|
}
|
|
@@ -7,22 +7,40 @@ const core_context_1 = require("@webpieces/core-context");
|
|
|
7
7
|
const core_context_2 = require("@webpieces/core-context");
|
|
8
8
|
const core_util_1 = require("@webpieces/core-util");
|
|
9
9
|
const TaskTypes_1 = require("./TaskTypes");
|
|
10
|
+
const TaskName_1 = require("./TaskName");
|
|
10
11
|
const ScheduleContext_1 = require("./ScheduleContext");
|
|
11
12
|
const log = core_util_1.LogManager.getLogger('CloudTaskScheduler');
|
|
12
13
|
/**
|
|
13
|
-
* Options for a scheduled task. These are QUEUE/TRANSPORT knobs (
|
|
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
|
-
*
|
|
14
|
+
* Options for a scheduled task. These are QUEUE/TRANSPORT knobs (naming, deadline, the Google
|
|
15
|
+
* request, gax) that belong on the enqueue CALL, never on the shared API method — the controller
|
|
16
|
+
* implementing that method has no use for them. See CloudTaskScheduler's class doc for the full
|
|
17
|
+
* rationale.
|
|
18
|
+
*
|
|
19
|
+
* Three levels, three owners:
|
|
20
|
+
* - the `CloudTasksClient` (via an overridden `CloudTasksClientProvider`): the channel and client
|
|
21
|
+
* defaults (keepalive, endpoint, clientConfig);
|
|
22
|
+
* - `gax` here, or `GcpTaskRegistry.setGax(...)`: how the ONE `createTask` RPC of this enqueue
|
|
23
|
+
* behaves. Set ⇒ YOU own timing: no webpieces retries, one attempt, under a safety cap;
|
|
24
|
+
* - `customizeTask` here, or `GcpTaskRegistry.setCustomizer(...)`: the Google request itself.
|
|
17
25
|
*/
|
|
18
26
|
class ScheduleOptions {
|
|
19
|
-
|
|
20
|
-
dedupName;
|
|
21
|
-
/** Per-task dispatch deadline in seconds. */
|
|
27
|
+
taskName;
|
|
22
28
|
taskTimeoutSeconds;
|
|
23
|
-
|
|
24
|
-
|
|
29
|
+
customizeTask;
|
|
30
|
+
gax;
|
|
31
|
+
constructor(
|
|
32
|
+
/** How the task is named. Default: one generated UUID per logical enqueue, reused on retry. */
|
|
33
|
+
taskName = TaskName_1.TaskName.generated(),
|
|
34
|
+
/** Per-task dispatch deadline in seconds (`task.dispatchDeadline`). */
|
|
35
|
+
taskTimeoutSeconds,
|
|
36
|
+
/** Changes the Google request on every attempt; NEVER gax. See {@link TaskCustomizer}. */
|
|
37
|
+
customizeTask,
|
|
38
|
+
/** gax `CallOptions` for `createTask`. Set ⇒ you own timing/retries for this enqueue. */
|
|
39
|
+
gax) {
|
|
40
|
+
this.taskName = taskName;
|
|
25
41
|
this.taskTimeoutSeconds = taskTimeoutSeconds;
|
|
42
|
+
this.customizeTask = customizeTask;
|
|
43
|
+
this.gax = gax;
|
|
26
44
|
}
|
|
27
45
|
}
|
|
28
46
|
exports.ScheduleOptions = ScheduleOptions;
|
|
@@ -31,7 +49,8 @@ exports.ScheduleOptions = ScheduleOptions;
|
|
|
31
49
|
* contract's method signature stays `foo(req)` on both client and controller; the
|
|
32
50
|
* scheduling options ride out-of-band in the schedule frame:
|
|
33
51
|
*
|
|
34
|
-
* await scheduler.addToQueue(() => taskClient.sendEmail(req)
|
|
52
|
+
* await scheduler.addToQueue(() => taskClient.sendEmail(req));
|
|
53
|
+
* await scheduler.addToQueue(() => taskClient.sendEmail(req), new ScheduleOptions(TaskName.dedup(id), 900));
|
|
35
54
|
* await scheduler.schedule(() => taskClient.sendEmail(req), runAtEpochMs);
|
|
36
55
|
*
|
|
37
56
|
* Must be called inside an active RequestContext (a server request scope).
|
|
@@ -39,12 +58,12 @@ exports.ScheduleOptions = ScheduleOptions;
|
|
|
39
58
|
* WHY scheduling options are here and NOT parameters on the API method
|
|
40
59
|
* ------------------------------------------------------------------------
|
|
41
60
|
* The enqueue client and the controller implement the SAME abstract API method
|
|
42
|
-
* (`sendEmail(req): Promise<void>`). Scheduling options —
|
|
61
|
+
* (`sendEmail(req): Promise<void>`). Scheduling options — the task name, "run in the
|
|
43
62
|
* future", `taskTimeoutSeconds` — are QUEUE/TRANSPORT concerns, not part of the
|
|
44
63
|
* business contract, so they must not appear on the method signature:
|
|
45
64
|
*
|
|
46
65
|
* - They are meaningless on the SERVER side. The controller's `sendEmail` just does
|
|
47
|
-
* the work (send the email). It has no use for
|
|
66
|
+
* the work (send the email). It has no use for a task name (a Cloud Tasks resource
|
|
48
67
|
* name), a future run-time (Cloud Tasks already delivered it), or a dispatch
|
|
49
68
|
* deadline. Putting them on the method would force the controller to accept and
|
|
50
69
|
* ignore transport metadata it can't act on.
|
|
@@ -68,16 +87,21 @@ let CloudTaskScheduler = class CloudTaskScheduler {
|
|
|
68
87
|
}
|
|
69
88
|
/** Enqueue a task to run as soon as possible. Returns its JobReference. */
|
|
70
89
|
async addToQueue(runnable, opts) {
|
|
71
|
-
return this.runWithFrame(
|
|
90
|
+
return this.runWithFrame(this.infoFor(undefined, opts), runnable);
|
|
72
91
|
}
|
|
73
92
|
/** Enqueue a task to run at an absolute epoch-millis time. Returns its JobReference. */
|
|
74
93
|
async schedule(runnable, epochMsToRunAt, opts) {
|
|
75
|
-
return this.runWithFrame(
|
|
94
|
+
return this.runWithFrame(this.infoFor(epochMsToRunAt, opts), runnable);
|
|
76
95
|
}
|
|
77
96
|
/** Cancel a previously scheduled task. */
|
|
78
97
|
async cancelJob(ref) {
|
|
79
98
|
await this.invoker.delete(ref);
|
|
80
99
|
}
|
|
100
|
+
/** No options = a generated name, no deadline, no customizer, webpieces-owned timing. */
|
|
101
|
+
infoFor(epochMsToRunAt, opts) {
|
|
102
|
+
const options = opts ?? new ScheduleOptions();
|
|
103
|
+
return new TaskTypes_1.ScheduleInfo(options.taskName, epochMsToRunAt, options.taskTimeoutSeconds, options.customizeTask, options.gax);
|
|
104
|
+
}
|
|
81
105
|
async runWithFrame(info, runnable) {
|
|
82
106
|
if (!core_context_2.RequestContext.isActive()) {
|
|
83
107
|
throw new Error('CloudTaskScheduler must run inside a RequestContext (a server request scope).');
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"CloudTaskScheduler.js","sourceRoot":"","sources":["../../../../../packages/cloud/cloudtasks-client/src/CloudTaskScheduler.ts"],"names":[],"mappings":";;;;AAAA,yCAAmC;AACnC,0DAAoE;AACpE,0DAAyD;AACzD,oDAAkD;
|
|
1
|
+
{"version":3,"file":"CloudTaskScheduler.js","sourceRoot":"","sources":["../../../../../packages/cloud/cloudtasks-client/src/CloudTaskScheduler.ts"],"names":[],"mappings":";;;;AAAA,yCAAmC;AACnC,0DAAoE;AACpE,0DAAyD;AACzD,oDAAkD;AAElD,2CAAsF;AACtF,yCAAsC;AACtC,uDAAwF;AAExF,MAAM,GAAG,GAAG,sBAAU,CAAC,SAAS,CAAC,oBAAoB,CAAC,CAAC;AAEvD;;;;;;;;;;;;GAYG;AACH,MAAa,eAAe;IAGX;IAEA;IAEA;IAEA;IARb;IACI,+FAA+F;IACtF,WAAqB,mBAAQ,CAAC,SAAS,EAAE;IAClD,uEAAuE;IAC9D,kBAA2B;IACpC,0FAA0F;IACjF,aAA8B;IACvC,yFAAyF;IAChF,GAAiB;QANjB,aAAQ,GAAR,QAAQ,CAAiC;QAEzC,uBAAkB,GAAlB,kBAAkB,CAAS;QAE3B,kBAAa,GAAb,aAAa,CAAiB;QAE9B,QAAG,GAAH,GAAG,CAAc;IAC3B,CAAC;CACP;AAXD,0CAWC;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmCG;AAEI,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,CAAC,OAAO,CAAC,SAAS,EAAE,IAAI,CAAC,EAC7B,QAAQ,CACX,CAAC;IACN,CAAC;IAED,wFAAwF;IACxF,KAAK,CAAC,QAAQ,CACV,QAA6B,EAC7B,cAAsB,EACtB,IAAsB;QAEtB,OAAO,IAAI,CAAC,YAAY,CACpB,IAAI,CAAC,OAAO,CAAC,cAAc,EAAE,IAAI,CAAC,EAClC,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;IAED,yFAAyF;IACjF,OAAO,CAAC,cAAkC,EAAE,IAAiC;QACjF,MAAM,OAAO,GAAG,IAAI,IAAI,IAAI,eAAe,EAAE,CAAC;QAC9C,OAAO,IAAI,wBAAY,CACnB,OAAO,CAAC,QAAQ,EAChB,cAAc,EACd,OAAO,CAAC,kBAAkB,EAC1B,OAAO,CAAC,aAAa,EACrB,OAAO,CAAC,GAAG,CACd,CAAC;IACN,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;AApEY,gDAAkB;6BAAlB,kBAAkB;IAD9B,IAAA,wCAAyB,GAAE;IAGnB,mBAAA,IAAA,kBAAM,EAAC,uBAAW,CAAC,CAAA;6CAA2B,uBAAW;GAFrD,kBAAkB,CAoE9B","sourcesContent":["import { inject } from 'inversify';\nimport { provideFrameworkSingleton } from '@webpieces/core-context';\nimport { RequestContext } from '@webpieces/core-context';\nimport { LogManager } from '@webpieces/core-util';\nimport type { CallOptions } from 'google-gax';\nimport { TaskInvoker, JobReference, ScheduleInfo, TaskCustomizer } from './TaskTypes';\nimport { TaskName } from './TaskName';\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 (naming, deadline, the Google\n * request, gax) 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 for the full\n * rationale.\n *\n * Three levels, three owners:\n * - the `CloudTasksClient` (via an overridden `CloudTasksClientProvider`): the channel and client\n * defaults (keepalive, endpoint, clientConfig);\n * - `gax` here, or `GcpTaskRegistry.setGax(...)`: how the ONE `createTask` RPC of this enqueue\n * behaves. Set ⇒ YOU own timing: no webpieces retries, one attempt, under a safety cap;\n * - `customizeTask` here, or `GcpTaskRegistry.setCustomizer(...)`: the Google request itself.\n */\nexport class ScheduleOptions {\n constructor(\n /** How the task is named. Default: one generated UUID per logical enqueue, reused on retry. */\n readonly taskName: TaskName = TaskName.generated(),\n /** Per-task dispatch deadline in seconds (`task.dispatchDeadline`). */\n readonly taskTimeoutSeconds?: number,\n /** Changes the Google request on every attempt; NEVER gax. See {@link TaskCustomizer}. */\n readonly customizeTask?: TaskCustomizer,\n /** gax `CallOptions` for `createTask`. Set ⇒ you own timing/retries for this enqueue. */\n readonly gax?: CallOptions,\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));\n * await scheduler.addToQueue(() => taskClient.sendEmail(req), new ScheduleOptions(TaskName.dedup(id), 900));\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 — the task name, \"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 a task name (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@provideFrameworkSingleton()\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 this.infoFor(undefined, opts),\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 this.infoFor(epochMsToRunAt, opts),\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 /** No options = a generated name, no deadline, no customizer, webpieces-owned timing. */\n private infoFor(epochMsToRunAt: number | undefined, opts: ScheduleOptions | undefined): ScheduleInfo {\n const options = opts ?? new ScheduleOptions();\n return new ScheduleInfo(\n options.taskName,\n epochMsToRunAt,\n options.taskTimeoutSeconds,\n options.customizeTask,\n options.gax,\n );\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,29 @@
|
|
|
1
|
+
import { CloudTasksClient } from '@google-cloud/tasks';
|
|
2
|
+
/**
|
|
3
|
+
* Builds the ONE `CloudTasksClient` that `GcpTaskInvoker` enqueues through — the place for
|
|
4
|
+
* process-wide CLIENT settings: the gRPC channel (keepalive), the endpoint, `clientConfig`.
|
|
5
|
+
*
|
|
6
|
+
* The default builds `new CloudTasksClient()`, exactly what `GcpTaskInvoker` always did, so nobody has
|
|
7
|
+
* to bind anything. An app that needs client-level settings overrides it at startup:
|
|
8
|
+
*
|
|
9
|
+
* ```ts
|
|
10
|
+
* class KeepaliveCloudTasks extends CloudTasksClientProvider {
|
|
11
|
+
* override create(): CloudTasksClient {
|
|
12
|
+
* return new CloudTasksClient({ 'grpc.keepalive_time_ms': 30_000 });
|
|
13
|
+
* }
|
|
14
|
+
* }
|
|
15
|
+
* (await options.rebind(CloudTasksClientProvider)).to(KeepaliveCloudTasks).inSingletonScope();
|
|
16
|
+
* ```
|
|
17
|
+
*
|
|
18
|
+
* Why a provider class and not an optional `CloudTasksClient` binding: the app container is created
|
|
19
|
+
* with inversify `autobind`, which auto-constructs ANY unbound class token — `@optional()` included —
|
|
20
|
+
* and `CloudTasksClient` has no injectable metadata, so resolving it unbound throws
|
|
21
|
+
* ("CloudTasksClient constructor requires at least 2 arguments"). A provider that is always bound
|
|
22
|
+
* keeps the same contract — optional to override, today's client by default — with nothing to throw.
|
|
23
|
+
*
|
|
24
|
+
* Per-CALL settings are not here: gax `CallOptions` belong on `ScheduleOptions.gax` /
|
|
25
|
+
* `GcpTaskRegistry.setGax`, and the Google request on a `TaskCustomizer`.
|
|
26
|
+
*/
|
|
27
|
+
export declare class CloudTasksClientProvider {
|
|
28
|
+
create(): CloudTasksClient;
|
|
29
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.CloudTasksClientProvider = void 0;
|
|
4
|
+
const tslib_1 = require("tslib");
|
|
5
|
+
const tasks_1 = require("@google-cloud/tasks");
|
|
6
|
+
const core_context_1 = require("@webpieces/core-context");
|
|
7
|
+
/**
|
|
8
|
+
* Builds the ONE `CloudTasksClient` that `GcpTaskInvoker` enqueues through — the place for
|
|
9
|
+
* process-wide CLIENT settings: the gRPC channel (keepalive), the endpoint, `clientConfig`.
|
|
10
|
+
*
|
|
11
|
+
* The default builds `new CloudTasksClient()`, exactly what `GcpTaskInvoker` always did, so nobody has
|
|
12
|
+
* to bind anything. An app that needs client-level settings overrides it at startup:
|
|
13
|
+
*
|
|
14
|
+
* ```ts
|
|
15
|
+
* class KeepaliveCloudTasks extends CloudTasksClientProvider {
|
|
16
|
+
* override create(): CloudTasksClient {
|
|
17
|
+
* return new CloudTasksClient({ 'grpc.keepalive_time_ms': 30_000 });
|
|
18
|
+
* }
|
|
19
|
+
* }
|
|
20
|
+
* (await options.rebind(CloudTasksClientProvider)).to(KeepaliveCloudTasks).inSingletonScope();
|
|
21
|
+
* ```
|
|
22
|
+
*
|
|
23
|
+
* Why a provider class and not an optional `CloudTasksClient` binding: the app container is created
|
|
24
|
+
* with inversify `autobind`, which auto-constructs ANY unbound class token — `@optional()` included —
|
|
25
|
+
* and `CloudTasksClient` has no injectable metadata, so resolving it unbound throws
|
|
26
|
+
* ("CloudTasksClient constructor requires at least 2 arguments"). A provider that is always bound
|
|
27
|
+
* keeps the same contract — optional to override, today's client by default — with nothing to throw.
|
|
28
|
+
*
|
|
29
|
+
* Per-CALL settings are not here: gax `CallOptions` belong on `ScheduleOptions.gax` /
|
|
30
|
+
* `GcpTaskRegistry.setGax`, and the Google request on a `TaskCustomizer`.
|
|
31
|
+
*/
|
|
32
|
+
let CloudTasksClientProvider = class CloudTasksClientProvider {
|
|
33
|
+
create() {
|
|
34
|
+
return new tasks_1.CloudTasksClient();
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
exports.CloudTasksClientProvider = CloudTasksClientProvider;
|
|
38
|
+
exports.CloudTasksClientProvider = CloudTasksClientProvider = tslib_1.__decorate([
|
|
39
|
+
(0, core_context_1.provideFrameworkSingleton)()
|
|
40
|
+
], CloudTasksClientProvider);
|
|
41
|
+
//# sourceMappingURL=CloudTasksClientProvider.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"CloudTasksClientProvider.js","sourceRoot":"","sources":["../../../../../packages/cloud/cloudtasks-client/src/CloudTasksClientProvider.ts"],"names":[],"mappings":";;;;AAAA,+CAAuD;AACvD,0DAAoE;AAEpE;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AAEI,IAAM,wBAAwB,GAA9B,MAAM,wBAAwB;IACjC,MAAM;QACF,OAAO,IAAI,wBAAgB,EAAE,CAAC;IAClC,CAAC;CACJ,CAAA;AAJY,4DAAwB;mCAAxB,wBAAwB;IADpC,IAAA,wCAAyB,GAAE;GACf,wBAAwB,CAIpC","sourcesContent":["import { CloudTasksClient } from '@google-cloud/tasks';\nimport { provideFrameworkSingleton } from '@webpieces/core-context';\n\n/**\n * Builds the ONE `CloudTasksClient` that `GcpTaskInvoker` enqueues through — the place for\n * process-wide CLIENT settings: the gRPC channel (keepalive), the endpoint, `clientConfig`.\n *\n * The default builds `new CloudTasksClient()`, exactly what `GcpTaskInvoker` always did, so nobody has\n * to bind anything. An app that needs client-level settings overrides it at startup:\n *\n * ```ts\n * class KeepaliveCloudTasks extends CloudTasksClientProvider {\n * override create(): CloudTasksClient {\n * return new CloudTasksClient({ 'grpc.keepalive_time_ms': 30_000 });\n * }\n * }\n * (await options.rebind(CloudTasksClientProvider)).to(KeepaliveCloudTasks).inSingletonScope();\n * ```\n *\n * Why a provider class and not an optional `CloudTasksClient` binding: the app container is created\n * with inversify `autobind`, which auto-constructs ANY unbound class token — `@optional()` included —\n * and `CloudTasksClient` has no injectable metadata, so resolving it unbound throws\n * (\"CloudTasksClient constructor requires at least 2 arguments\"). A provider that is always bound\n * keeps the same contract — optional to override, today's client by default — with nothing to throw.\n *\n * Per-CALL settings are not here: gax `CallOptions` belong on `ScheduleOptions.gax` /\n * `GcpTaskRegistry.setGax`, and the Google request on a `TaskCustomizer`.\n */\n@provideFrameworkSingleton()\nexport class CloudTasksClientProvider {\n create(): CloudTasksClient {\n return new CloudTasksClient();\n }\n}\n"]}
|
package/src/GcpTaskInvoker.d.ts
CHANGED
|
@@ -1,20 +1,43 @@
|
|
|
1
1
|
import { AuthMeta, AuthMode } from '@webpieces/core-util';
|
|
2
2
|
import { Secrets } from '@webpieces/core-util';
|
|
3
3
|
import { TaskInvoker, TaskRequest, JobReference } from './TaskTypes';
|
|
4
|
+
import { CloudTasksClientProvider } from './CloudTasksClientProvider';
|
|
4
5
|
/**
|
|
5
6
|
* TaskInvoker that enqueues to Google Cloud Tasks (`@google-cloud/tasks`). Builds an
|
|
6
7
|
* HTTP-target task delivered as POST to `targetUrl + path`, authenticated per the
|
|
7
8
|
* endpoint's auth mode (OIDC token minted as this service's runtime SA, or a
|
|
8
|
-
* shared-secret header).
|
|
9
|
-
*
|
|
9
|
+
* shared-secret header).
|
|
10
|
+
*
|
|
11
|
+
* ONE attempt per call, never a retry of its own:
|
|
12
|
+
* - it builds a FRESH `ICreateTaskRequest` (gax mutates what it is handed), names the task with the
|
|
13
|
+
* id `TaskProxyClient` resolved once for the whole logical enqueue, then applies the effective
|
|
14
|
+
* {@link TaskCustomizer} last;
|
|
15
|
+
* - it sends `createTask(request, { timeout: timeoutMs })` — a real gRPC deadline per attempt, and no
|
|
16
|
+
* gax retry — unless the caller owns timing (`plan.gax` set), in which case it sends a shallow copy
|
|
17
|
+
* of the caller's `CallOptions`, untouched;
|
|
18
|
+
* - it maps the gRPC failure with {@link GrpcErrorMapper}: ALREADY_EXISTS becomes
|
|
19
|
+
* `TaskAlreadyExistsError` (success, upstream), UNAVAILABLE / DEADLINE_EXCEEDED /
|
|
20
|
+
* RESOURCE_EXHAUSTED become transient webpieces errors that `RetryStrategy` retries.
|
|
21
|
+
*
|
|
22
|
+
* Cancellation: the gRPC deadline IS the attempt deadline, so on Path A gRPC itself cancels the RPC
|
|
23
|
+
* at the same moment `CallDeadline` gives up on it. `createTask`'s public promise carries no
|
|
24
|
+
* `cancel()` (the client chains `.then` on gax's cancellable promise, which drops it), so the abort
|
|
25
|
+
* signal is checked before sending rather than wired to a cancel.
|
|
10
26
|
*/
|
|
11
27
|
export declare class GcpTaskInvoker extends TaskInvoker {
|
|
12
28
|
private readonly secrets?;
|
|
13
29
|
private readonly client;
|
|
14
|
-
|
|
30
|
+
private readonly grpcErrorMapper;
|
|
31
|
+
constructor(clientProvider: CloudTasksClientProvider, secrets?: Secrets | undefined);
|
|
15
32
|
selectAuthentication(meta: AuthMeta): AuthMode;
|
|
16
|
-
enqueue(request: TaskRequest): Promise<JobReference>;
|
|
33
|
+
enqueue(request: TaskRequest, timeoutMs: number, signal: AbortSignal): Promise<JobReference>;
|
|
17
34
|
delete(ref: JobReference): Promise<void>;
|
|
35
|
+
/**
|
|
36
|
+
* A NEW object on every attempt. webpieces owns timing → `{ timeout }` only: a real gRPC deadline
|
|
37
|
+
* and no gax retry loop. The caller owns timing → a shallow copy of their options, so gax's own
|
|
38
|
+
* mutation (`otherArgs.headers`) never lands on an object the caller may reuse.
|
|
39
|
+
*/
|
|
40
|
+
private callOptions;
|
|
18
41
|
private buildTask;
|
|
19
42
|
private applyAuth;
|
|
20
43
|
}
|