keryx 0.4.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/classes/Action.ts +20 -0
- package/classes/Connection.ts +20 -1
- package/classes/TypedError.ts +2 -0
- package/config/actions.ts +7 -0
- package/config/index.ts +2 -0
- package/initializers/actionts.ts +5 -5
- package/package.json +1 -1
package/classes/Action.ts
CHANGED
|
@@ -50,6 +50,9 @@ export type ActionConstructorInputs = {
|
|
|
50
50
|
method?: HTTP_METHOD;
|
|
51
51
|
};
|
|
52
52
|
|
|
53
|
+
/** Per-action timeout in ms (overrides global `config.server.web.actionTimeout`; 0 disables) */
|
|
54
|
+
timeout?: number;
|
|
55
|
+
|
|
53
56
|
/** Configure this action as a background task/job */
|
|
54
57
|
task?: {
|
|
55
58
|
/** Optional recurring frequency in milliseconds */
|
|
@@ -85,6 +88,7 @@ export abstract class Action {
|
|
|
85
88
|
route: RegExp | string;
|
|
86
89
|
method: HTTP_METHOD;
|
|
87
90
|
};
|
|
91
|
+
timeout?: number;
|
|
88
92
|
task?: {
|
|
89
93
|
frequency?: number;
|
|
90
94
|
queue: string;
|
|
@@ -95,6 +99,7 @@ export abstract class Action {
|
|
|
95
99
|
this.description = args.description ?? `An Action: ${this.name}`;
|
|
96
100
|
this.inputs = args.inputs;
|
|
97
101
|
this.middleware = args.middleware ?? [];
|
|
102
|
+
this.timeout = args.timeout;
|
|
98
103
|
this.mcp = { enabled: true, ...args.mcp };
|
|
99
104
|
this.web = {
|
|
100
105
|
route: args.web?.route ?? `/${this.name}`,
|
|
@@ -111,11 +116,26 @@ export abstract class Action {
|
|
|
111
116
|
* It can be `async`.
|
|
112
117
|
* Usually the goal of this run method is to return the data that you want to be sent to API consumers.
|
|
113
118
|
* If error is thrown in this method, it will be logged, caught, and returned to the client as `error`
|
|
119
|
+
*
|
|
120
|
+
* @param params - The validated and coerced action inputs. The type is inferred from the
|
|
121
|
+
* action's `inputs` Zod schema (falls back to `Record<string, unknown>` when no schema is
|
|
122
|
+
* defined). By the time `run` is called, all middleware `runBefore` hooks have already
|
|
123
|
+
* executed and may have mutated the params.
|
|
124
|
+
* @param connection - The connection that initiated this action. Provides access to the
|
|
125
|
+
* caller's session (`connection.session`), subscription state, and raw transport handle.
|
|
126
|
+
* It is `undefined` when the action is invoked outside an HTTP/WebSocket request context
|
|
127
|
+
* (e.g., as a background task via the Resque worker or via `api.actions.run()`).
|
|
128
|
+
* @param abortSignal - An `AbortSignal` tied to the action's timeout. The signal is aborted
|
|
129
|
+
* when the per-action `timeout` (or the global `config.actions.timeout`, default 300 000 ms)
|
|
130
|
+
* elapses. Long-running actions should check `abortSignal.aborted` or pass the signal to
|
|
131
|
+
* cancellable APIs (e.g., `fetch`) to exit promptly. Not provided when timeouts are
|
|
132
|
+
* disabled (`timeout: 0`).
|
|
114
133
|
* @throws {TypedError} All errors thrown should be TypedError instances
|
|
115
134
|
*/
|
|
116
135
|
abstract run(
|
|
117
136
|
params: ActionParams<Action>,
|
|
118
137
|
connection?: Connection,
|
|
138
|
+
abortSignal?: AbortSignal,
|
|
119
139
|
): Promise<any>;
|
|
120
140
|
}
|
|
121
141
|
|
package/classes/Connection.ts
CHANGED
|
@@ -76,7 +76,26 @@ export class Connection<T extends Record<string, any> = Record<string, any>> {
|
|
|
76
76
|
}
|
|
77
77
|
}
|
|
78
78
|
|
|
79
|
-
|
|
79
|
+
const timeoutMs = action.timeout ?? config.actions.timeout;
|
|
80
|
+
if (timeoutMs > 0) {
|
|
81
|
+
const controller = new AbortController();
|
|
82
|
+
const timeoutError = new TypedError({
|
|
83
|
+
message: `Action '${action.name}' timed out after ${timeoutMs}ms`,
|
|
84
|
+
type: ErrorType.CONNECTION_ACTION_TIMEOUT,
|
|
85
|
+
});
|
|
86
|
+
const timeoutPromise = new Promise<never>((_, reject) => {
|
|
87
|
+
setTimeout(() => {
|
|
88
|
+
controller.abort();
|
|
89
|
+
reject(timeoutError);
|
|
90
|
+
}, timeoutMs);
|
|
91
|
+
});
|
|
92
|
+
response = await Promise.race([
|
|
93
|
+
action.run(formattedParams, this, controller.signal),
|
|
94
|
+
timeoutPromise,
|
|
95
|
+
]);
|
|
96
|
+
} else {
|
|
97
|
+
response = await action.run(formattedParams, this);
|
|
98
|
+
}
|
|
80
99
|
|
|
81
100
|
for (const middleware of action.middleware ?? []) {
|
|
82
101
|
if (middleware.runAfter) {
|
package/classes/TypedError.ts
CHANGED
|
@@ -27,6 +27,7 @@ export enum ErrorType {
|
|
|
27
27
|
"CONNECTION_CHANNEL_AUTHORIZATION" = "CONNECTION_CHANNEL_AUTHORIZATION",
|
|
28
28
|
"CONNECTION_CHANNEL_VALIDATION" = "CONNECTION_CHANNEL_VALIDATION",
|
|
29
29
|
|
|
30
|
+
"CONNECTION_ACTION_TIMEOUT" = "CONNECTION_ACTION_TIMEOUT",
|
|
30
31
|
"CONNECTION_RATE_LIMITED" = "CONNECTION_RATE_LIMITED",
|
|
31
32
|
|
|
32
33
|
"CONNECTION_TASK_DEFINITION" = "CONNECTION_TASK_DEFINITION",
|
|
@@ -56,6 +57,7 @@ export const ErrorStatusCodes: Record<ErrorType, number> = {
|
|
|
56
57
|
[ErrorType.CONNECTION_NOT_SUBSCRIBED]: 406,
|
|
57
58
|
[ErrorType.CONNECTION_CHANNEL_AUTHORIZATION]: 403,
|
|
58
59
|
[ErrorType.CONNECTION_CHANNEL_VALIDATION]: 400,
|
|
60
|
+
[ErrorType.CONNECTION_ACTION_TIMEOUT]: 408,
|
|
59
61
|
[ErrorType.CONNECTION_RATE_LIMITED]: 429,
|
|
60
62
|
|
|
61
63
|
[ErrorType.CONNECTION_TASK_DEFINITION]: 500,
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { loadFromEnvIfSet } from "../util/config";
|
|
2
|
+
|
|
3
|
+
export const configActions = {
|
|
4
|
+
timeout: await loadFromEnvIfSet("ACTION_TIMEOUT", 300_000),
|
|
5
|
+
fanOutBatchSize: await loadFromEnvIfSet("ACTION_FAN_OUT_BATCH_SIZE", 100),
|
|
6
|
+
fanOutResultTtl: await loadFromEnvIfSet("ACTION_FAN_OUT_RESULT_TTL", 600),
|
|
7
|
+
};
|
package/config/index.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { configActions } from "./actions";
|
|
1
2
|
import { configChannels } from "./channels";
|
|
2
3
|
import { configDatabase } from "./database";
|
|
3
4
|
import { configLogger } from "./logger";
|
|
@@ -11,6 +12,7 @@ import { configSession } from "./session";
|
|
|
11
12
|
import { configTasks } from "./tasks";
|
|
12
13
|
|
|
13
14
|
export const config = {
|
|
15
|
+
actions: configActions,
|
|
14
16
|
channels: configChannels,
|
|
15
17
|
process: configProcess,
|
|
16
18
|
logger: configLogger,
|
package/initializers/actionts.ts
CHANGED
|
@@ -5,13 +5,11 @@ import { api, logger } from "../api";
|
|
|
5
5
|
import { DEFAULT_QUEUE, type Action } from "../classes/Action";
|
|
6
6
|
import { Initializer } from "../classes/Initializer";
|
|
7
7
|
import { ErrorType, TypedError } from "../classes/TypedError";
|
|
8
|
+
import { config } from "../config";
|
|
8
9
|
import { globLoader } from "../util/glob";
|
|
9
10
|
|
|
10
11
|
const namespace = "actions";
|
|
11
12
|
|
|
12
|
-
const DEFAULT_FAN_OUT_BATCH_SIZE = 100;
|
|
13
|
-
const DEFAULT_FAN_OUT_RESULT_TTL = 600; // 10 minutes in seconds
|
|
14
|
-
|
|
15
13
|
export type FanOutJob = {
|
|
16
14
|
action: string;
|
|
17
15
|
inputs?: TaskInputs;
|
|
@@ -130,8 +128,10 @@ export class Actions extends Initializer {
|
|
|
130
128
|
return { ...job, queue: resolvedQueue, inputs: job.inputs ?? {} };
|
|
131
129
|
});
|
|
132
130
|
|
|
133
|
-
const batchSize =
|
|
134
|
-
|
|
131
|
+
const batchSize =
|
|
132
|
+
resolvedOptions.batchSize ?? config.actions.fanOutBatchSize;
|
|
133
|
+
const resultTtl =
|
|
134
|
+
resolvedOptions.resultTtl ?? config.actions.fanOutResultTtl;
|
|
135
135
|
const fanOutId = randomUUID();
|
|
136
136
|
const metaKey = `fanout:${fanOutId}`;
|
|
137
137
|
|