keryx 0.29.7 → 0.29.9
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/Connection.ts +136 -76
- package/package.json +1 -1
- package/templates/scaffold/env.example.mustache +1 -1
- package/util/glob.ts +19 -21
package/classes/Connection.ts
CHANGED
|
@@ -59,6 +59,10 @@ export type AfterActHook = (
|
|
|
59
59
|
outcome: ActOutcome,
|
|
60
60
|
) => Promise<void> | void;
|
|
61
61
|
|
|
62
|
+
type ActionParamsState = {
|
|
63
|
+
value: Record<string, unknown>;
|
|
64
|
+
};
|
|
65
|
+
|
|
62
66
|
/**
|
|
63
67
|
* Represents a client connection to the server — HTTP request, WebSocket, or internal caller.
|
|
64
68
|
* Each connection tracks its own session, channel subscriptions, and rate-limit state.
|
|
@@ -156,61 +160,19 @@ export class Connection<
|
|
|
156
160
|
|
|
157
161
|
let action: Action | undefined;
|
|
158
162
|
let formattedParams: Record<string, unknown> | undefined;
|
|
159
|
-
|
|
160
|
-
// beforeActRan guards afterAct so we only fire after-hooks for invocations
|
|
161
|
-
// where before-hooks also fired (i.e. action found + params validated).
|
|
163
|
+
let paramsState: ActionParamsState | undefined;
|
|
162
164
|
const actCtx: ActContext = { metadata: {} };
|
|
163
165
|
let beforeActRan = false;
|
|
164
166
|
try {
|
|
165
|
-
action = this.
|
|
166
|
-
if (!action) {
|
|
167
|
-
throw new TypedError({
|
|
168
|
-
message: `Action not found${actionName ? `: ${actionName}` : ""}`,
|
|
169
|
-
type: ErrorType.CONNECTION_ACTION_NOT_FOUND,
|
|
170
|
-
});
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
// load the session once, if it hasn't been loaded yet
|
|
167
|
+
action = this.resolveAction(actionName);
|
|
174
168
|
if (!this.sessionLoaded) await this.loadSession();
|
|
175
|
-
|
|
176
169
|
formattedParams = await this.formatParams(params, action);
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
await hook(action.name, formattedParams, this, actCtx);
|
|
180
|
-
}
|
|
170
|
+
paramsState = { value: formattedParams };
|
|
171
|
+
await this.runBeforeActHooks(action, paramsState.value, actCtx);
|
|
181
172
|
beforeActRan = true;
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
const middlewareResponse = await middleware.runBefore(
|
|
186
|
-
formattedParams,
|
|
187
|
-
this,
|
|
188
|
-
);
|
|
189
|
-
if (middlewareResponse && middlewareResponse?.updatedParams)
|
|
190
|
-
formattedParams = middlewareResponse.updatedParams;
|
|
191
|
-
}
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
const timeoutMs = action.timeout ?? config.actions.timeout;
|
|
195
|
-
if (timeoutMs > 0) {
|
|
196
|
-
const controller = new AbortController();
|
|
197
|
-
const timeoutError = new TypedError({
|
|
198
|
-
message: `Action '${action.name}' timed out after ${timeoutMs}ms`,
|
|
199
|
-
type: ErrorType.CONNECTION_ACTION_TIMEOUT,
|
|
200
|
-
});
|
|
201
|
-
const timeoutPromise = new Promise<never>((_, reject) => {
|
|
202
|
-
setTimeout(() => {
|
|
203
|
-
controller.abort();
|
|
204
|
-
reject(timeoutError);
|
|
205
|
-
}, timeoutMs);
|
|
206
|
-
});
|
|
207
|
-
response = await Promise.race([
|
|
208
|
-
action.run(formattedParams, this, controller.signal),
|
|
209
|
-
timeoutPromise,
|
|
210
|
-
]);
|
|
211
|
-
} else {
|
|
212
|
-
response = await action.run(formattedParams, this);
|
|
213
|
-
}
|
|
173
|
+
await this.runMiddlewareBefore(action, paramsState);
|
|
174
|
+
formattedParams = paramsState.value;
|
|
175
|
+
response = await this.executeWithTimeout(action, formattedParams);
|
|
214
176
|
} catch (e) {
|
|
215
177
|
loggerResponsePrefix = "ERROR";
|
|
216
178
|
error =
|
|
@@ -222,34 +184,23 @@ export class Connection<
|
|
|
222
184
|
cause: e,
|
|
223
185
|
});
|
|
224
186
|
} finally {
|
|
225
|
-
if (
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
if (middlewareResponse && middlewareResponse?.updatedResponse) {
|
|
234
|
-
if (response instanceof StreamingResponse) {
|
|
235
|
-
logger.warn(
|
|
236
|
-
`Middleware cannot replace a StreamingResponse for action '${actionName}'`,
|
|
237
|
-
);
|
|
238
|
-
} else {
|
|
239
|
-
response = middlewareResponse.updatedResponse;
|
|
240
|
-
}
|
|
241
|
-
}
|
|
242
|
-
}
|
|
243
|
-
}
|
|
244
|
-
}
|
|
187
|
+
if (paramsState) formattedParams = paramsState.value;
|
|
188
|
+
if (action && formattedParams)
|
|
189
|
+
response = await this.runMiddlewareAfter(
|
|
190
|
+
action,
|
|
191
|
+
formattedParams,
|
|
192
|
+
error,
|
|
193
|
+
response,
|
|
194
|
+
);
|
|
245
195
|
if (beforeActRan && action && formattedParams) {
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
196
|
+
await this.runAfterActHooks(
|
|
197
|
+
action,
|
|
198
|
+
formattedParams,
|
|
199
|
+
actCtx,
|
|
200
|
+
reqStartTime,
|
|
201
|
+
response,
|
|
202
|
+
error,
|
|
203
|
+
);
|
|
253
204
|
}
|
|
254
205
|
this._actDepth--;
|
|
255
206
|
}
|
|
@@ -379,6 +330,115 @@ export class Connection<
|
|
|
379
330
|
return api.actions.actions.find((a: Action) => a.name === actionName);
|
|
380
331
|
}
|
|
381
332
|
|
|
333
|
+
private resolveAction(actionName: string | undefined) {
|
|
334
|
+
const action = this.findAction(actionName);
|
|
335
|
+
if (!action) {
|
|
336
|
+
throw new TypedError({
|
|
337
|
+
message: `Action not found${actionName ? `: ${actionName}` : ""}`,
|
|
338
|
+
type: ErrorType.CONNECTION_ACTION_NOT_FOUND,
|
|
339
|
+
});
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
return action;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
private async runBeforeActHooks(
|
|
346
|
+
action: Action,
|
|
347
|
+
params: Record<string, unknown>,
|
|
348
|
+
actCtx: ActContext,
|
|
349
|
+
) {
|
|
350
|
+
for (const hook of api.hooks.actions.beforeActHooks) {
|
|
351
|
+
await hook(action.name, params, this, actCtx);
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
private async runAfterActHooks(
|
|
356
|
+
action: Action,
|
|
357
|
+
params: Record<string, unknown>,
|
|
358
|
+
actCtx: ActContext,
|
|
359
|
+
reqStartTime: number,
|
|
360
|
+
response: Object,
|
|
361
|
+
error: TypedError | undefined,
|
|
362
|
+
) {
|
|
363
|
+
const actDuration = new Date().getTime() - reqStartTime;
|
|
364
|
+
const outcome: ActOutcome = error
|
|
365
|
+
? { success: false, error, duration: actDuration }
|
|
366
|
+
: { success: true, response, duration: actDuration };
|
|
367
|
+
|
|
368
|
+
for (const hook of api.hooks.actions.afterActHooks) {
|
|
369
|
+
await hook(action.name, params, this, actCtx, outcome);
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
private async runMiddlewareBefore(
|
|
374
|
+
action: Action,
|
|
375
|
+
paramsState: ActionParamsState,
|
|
376
|
+
) {
|
|
377
|
+
for (const middleware of action.middleware ?? []) {
|
|
378
|
+
if (middleware.runBefore) {
|
|
379
|
+
const middlewareResponse = await middleware.runBefore(
|
|
380
|
+
paramsState.value,
|
|
381
|
+
this,
|
|
382
|
+
);
|
|
383
|
+
if (middlewareResponse && middlewareResponse?.updatedParams)
|
|
384
|
+
paramsState.value = middlewareResponse.updatedParams;
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
private async runMiddlewareAfter(
|
|
390
|
+
action: Action,
|
|
391
|
+
params: Record<string, unknown>,
|
|
392
|
+
error: TypedError | undefined,
|
|
393
|
+
response: Object,
|
|
394
|
+
) {
|
|
395
|
+
let updatedResponse = response;
|
|
396
|
+
for (const middleware of action.middleware ?? []) {
|
|
397
|
+
if (middleware.runAfter) {
|
|
398
|
+
const middlewareResponse = await middleware.runAfter(
|
|
399
|
+
params,
|
|
400
|
+
this,
|
|
401
|
+
error,
|
|
402
|
+
);
|
|
403
|
+
if (middlewareResponse && middlewareResponse?.updatedResponse) {
|
|
404
|
+
if (updatedResponse instanceof StreamingResponse) {
|
|
405
|
+
logger.warn(
|
|
406
|
+
`Middleware cannot replace a StreamingResponse for action '${action.name}'`,
|
|
407
|
+
);
|
|
408
|
+
} else {
|
|
409
|
+
updatedResponse = middlewareResponse.updatedResponse;
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
return updatedResponse;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
private async executeWithTimeout(
|
|
419
|
+
action: Action,
|
|
420
|
+
params: Record<string, unknown>,
|
|
421
|
+
): Promise<Object> {
|
|
422
|
+
const timeoutMs = action.timeout ?? config.actions.timeout;
|
|
423
|
+
if (timeoutMs <= 0) return action.run(params, this);
|
|
424
|
+
|
|
425
|
+
const controller = new AbortController();
|
|
426
|
+
const timeoutError = new TypedError({
|
|
427
|
+
message: `Action '${action.name}' timed out after ${timeoutMs}ms`,
|
|
428
|
+
type: ErrorType.CONNECTION_ACTION_TIMEOUT,
|
|
429
|
+
});
|
|
430
|
+
const timeoutPromise = new Promise<never>((_, reject) => {
|
|
431
|
+
setTimeout(() => {
|
|
432
|
+
controller.abort();
|
|
433
|
+
reject(timeoutError);
|
|
434
|
+
}, timeoutMs);
|
|
435
|
+
});
|
|
436
|
+
|
|
437
|
+
return Promise.race([
|
|
438
|
+
action.run(params, this, controller.signal),
|
|
439
|
+
timeoutPromise,
|
|
440
|
+
]);
|
|
441
|
+
}
|
|
382
442
|
private async formatParams(params: Record<string, unknown>, action: Action) {
|
|
383
443
|
if (!action.inputs) return {} as ActionParams<Action>;
|
|
384
444
|
|
package/package.json
CHANGED
package/util/glob.ts
CHANGED
|
@@ -13,34 +13,32 @@ import { ErrorType, TypedError } from "../classes/TypedError";
|
|
|
13
13
|
*/
|
|
14
14
|
export async function globLoader<T>(searchDir: string) {
|
|
15
15
|
const results: T[] = [];
|
|
16
|
-
const
|
|
16
|
+
const glob = new Glob("**/*.{ts,tsx}");
|
|
17
17
|
const dir = path.isAbsolute(searchDir)
|
|
18
18
|
? searchDir
|
|
19
19
|
: path.join(api.rootDir, searchDir);
|
|
20
20
|
|
|
21
|
-
for (const
|
|
22
|
-
|
|
23
|
-
if (file.startsWith(".")) continue;
|
|
21
|
+
for await (const file of glob.scan(dir)) {
|
|
22
|
+
if (file.startsWith(".")) continue;
|
|
24
23
|
|
|
25
|
-
|
|
26
|
-
|
|
24
|
+
const fullPath = path.join(dir, file);
|
|
25
|
+
const modules = (await import(fullPath)) as Record<string, unknown>;
|
|
27
26
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
27
|
+
for (const [name, klass] of Object.entries(modules)) {
|
|
28
|
+
// Skip non-class exports (constants, enums, functions)
|
|
29
|
+
if (typeof klass !== "function" || klass.prototype === undefined) {
|
|
30
|
+
continue;
|
|
31
|
+
}
|
|
33
32
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
}
|
|
33
|
+
try {
|
|
34
|
+
const instance = new (klass as new () => T)();
|
|
35
|
+
results.push(instance);
|
|
36
|
+
} catch (error) {
|
|
37
|
+
throw new TypedError({
|
|
38
|
+
message: `Error loading from ${dir} - ${name} - ${error}`,
|
|
39
|
+
type: ErrorType.SERVER_INITIALIZATION,
|
|
40
|
+
cause: error,
|
|
41
|
+
});
|
|
44
42
|
}
|
|
45
43
|
}
|
|
46
44
|
}
|