apcore-js 0.1.0 → 0.1.2
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/.claude/settings.local.json +2 -1
- package/.pre-commit-config.yaml +2 -2
- package/CHANGELOG.md +38 -0
- package/README.md +1 -1
- package/package.json +9 -1
- package/src/acl.ts +37 -38
- package/src/bindings.ts +11 -18
- package/src/context.ts +1 -1
- package/src/errors.ts +2 -2
- package/src/executor.ts +62 -49
- package/src/index.ts +1 -1
- package/src/middleware/logging.ts +1 -1
- package/src/middleware/manager.ts +2 -2
- package/src/observability/tracing.ts +1 -1
- package/src/registry/registry.ts +72 -52
- package/src/registry/schema-export.ts +1 -4
- package/src/schema/exporter.ts +1 -4
- package/src/schema/loader.ts +45 -56
- package/src/schema/ref-resolver.ts +1 -4
- package/src/schema/strict.ts +1 -3
- package/src/utils/index.ts +4 -0
- package/tests/schema/test-annotations.test.ts +135 -0
- package/tests/schema/test-exporter.test.ts +171 -0
- package/tests/test-logging-middleware.test.ts +150 -0
package/.pre-commit-config.yaml
CHANGED
|
@@ -5,13 +5,13 @@ repos:
|
|
|
5
5
|
hooks:
|
|
6
6
|
- id: check-chars
|
|
7
7
|
name: apdev-js check-chars
|
|
8
|
-
entry: apdev-js check-chars
|
|
8
|
+
entry: npx apdev-js check-chars
|
|
9
9
|
language: system
|
|
10
10
|
types_or: [text, ts, javascript]
|
|
11
11
|
|
|
12
12
|
- id: check-imports
|
|
13
13
|
name: apdev-js check-imports
|
|
14
|
-
entry: apdev-js check-imports
|
|
14
|
+
entry: npx apdev-js check-imports --package apcore-js
|
|
15
15
|
language: system
|
|
16
16
|
pass_filenames: false
|
|
17
17
|
always_run: true
|
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,44 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [0.1.2] - 2026-02-18
|
|
9
|
+
|
|
10
|
+
### Fixed
|
|
11
|
+
|
|
12
|
+
- **Timer leak in executor** — `_executeWithTimeout` now calls `clearTimeout` in `.finally()` to prevent timer leak on normal completion
|
|
13
|
+
- **Path traversal protection** — `resolveTarget` in binding loader rejects module paths containing `..` segments before dynamic `import()`
|
|
14
|
+
- **Bare catch blocks** — 6 silent `catch {}` blocks in registry and middleware manager now log warnings with `[apcore:<subsystem>]` prefix
|
|
15
|
+
- **Python-style error messages** — Fixed `FuncMissingTypeHintError` and `FuncMissingReturnTypeError` to use TypeScript syntax (`: string`, `: Record<string, unknown>`)
|
|
16
|
+
- **Console.log in production** — Replaced `console.log` with `console.info` in logging middleware and `process.stdout.write` in tracing exporter
|
|
17
|
+
|
|
18
|
+
### Changed
|
|
19
|
+
|
|
20
|
+
- **Long method decomposition** — Broke up 4 oversized methods to meet ≤50 line guideline:
|
|
21
|
+
- `Executor.call()` (108 → 6 private helpers)
|
|
22
|
+
- `Registry.discover()` (110 → 7 private helpers)
|
|
23
|
+
- `ACL.load()` (71 → extracted `parseAclRule`)
|
|
24
|
+
- `jsonSchemaToTypeBox()` (80 → 5 converter helpers)
|
|
25
|
+
- **Deeply readonly callChain** — `Context.callChain` type narrowed from `readonly string[]` to `readonly (readonly string[])` preventing mutation via push/splice
|
|
26
|
+
- **Consolidated `deepCopy`** — Removed 4 duplicate `deepCopy` implementations; single shared version now lives in `src/utils/index.ts`
|
|
27
|
+
|
|
28
|
+
### Added
|
|
29
|
+
|
|
30
|
+
- **42 new tests** for previously uncovered modules:
|
|
31
|
+
- `tests/schema/test-annotations.test.ts` — 16 tests for `mergeAnnotations`, `mergeExamples`, `mergeMetadata`
|
|
32
|
+
- `tests/schema/test-exporter.test.ts` — 14 tests for `SchemaExporter` across all 4 export profiles
|
|
33
|
+
- `tests/test-logging-middleware.test.ts` — 12 tests for `LoggingMiddleware` before/after/onError
|
|
34
|
+
|
|
35
|
+
## [0.1.1] - 2026-02-17
|
|
36
|
+
|
|
37
|
+
### Fixed
|
|
38
|
+
|
|
39
|
+
- Updated logo URL in README
|
|
40
|
+
|
|
41
|
+
### Changed
|
|
42
|
+
|
|
43
|
+
- Renamed package from `apcore` to `apcore-js`
|
|
44
|
+
- Updated installation instructions
|
|
45
|
+
|
|
8
46
|
## [0.1.0] - 2026-02-16
|
|
9
47
|
|
|
10
48
|
### Added
|
package/README.md
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "apcore-js",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"description": "AI-Perceivable Core — schema-driven module development framework",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -21,6 +21,14 @@
|
|
|
21
21
|
"engines": {
|
|
22
22
|
"node": ">=18.0.0"
|
|
23
23
|
},
|
|
24
|
+
"repository": {
|
|
25
|
+
"type": "git",
|
|
26
|
+
"url": "https://github.com/aipartnerup/apcore-typescript.git"
|
|
27
|
+
},
|
|
28
|
+
"homepage": "https://github.com/aipartnerup",
|
|
29
|
+
"bugs": {
|
|
30
|
+
"url": "https://github.com/aipartnerup/apcore-typescript/issues"
|
|
31
|
+
},
|
|
24
32
|
"license": "MIT",
|
|
25
33
|
"dependencies": {
|
|
26
34
|
"@sinclair/typebox": "^0.34.0",
|
package/src/acl.ts
CHANGED
|
@@ -16,6 +16,42 @@ export interface ACLRule {
|
|
|
16
16
|
conditions?: Record<string, unknown> | null;
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
+
function parseAclRule(rawRule: unknown, index: number): ACLRule {
|
|
20
|
+
if (typeof rawRule !== 'object' || rawRule === null || Array.isArray(rawRule)) {
|
|
21
|
+
throw new ACLRuleError(`Rule ${index} must be a mapping, got ${typeof rawRule}`);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const ruleObj = rawRule as Record<string, unknown>;
|
|
25
|
+
for (const key of ['callers', 'targets', 'effect']) {
|
|
26
|
+
if (!(key in ruleObj)) {
|
|
27
|
+
throw new ACLRuleError(`Rule ${index} missing required key '${key}'`);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const effect = ruleObj['effect'] as string;
|
|
32
|
+
if (effect !== 'allow' && effect !== 'deny') {
|
|
33
|
+
throw new ACLRuleError(`Rule ${index} has invalid effect '${effect}', must be 'allow' or 'deny'`);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const callers = ruleObj['callers'];
|
|
37
|
+
if (!Array.isArray(callers)) {
|
|
38
|
+
throw new ACLRuleError(`Rule ${index} 'callers' must be a list, got ${typeof callers}`);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const targets = ruleObj['targets'];
|
|
42
|
+
if (!Array.isArray(targets)) {
|
|
43
|
+
throw new ACLRuleError(`Rule ${index} 'targets' must be a list, got ${typeof targets}`);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return {
|
|
47
|
+
callers: callers as string[],
|
|
48
|
+
targets: targets as string[],
|
|
49
|
+
effect,
|
|
50
|
+
description: (ruleObj['description'] as string) ?? '',
|
|
51
|
+
conditions: (ruleObj['conditions'] as Record<string, unknown>) ?? null,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
19
55
|
export class ACL {
|
|
20
56
|
private _rules: ACLRule[];
|
|
21
57
|
private _defaultEffect: string;
|
|
@@ -56,44 +92,7 @@ export class ACL {
|
|
|
56
92
|
}
|
|
57
93
|
|
|
58
94
|
const defaultEffect = (dataObj['default_effect'] as string) ?? 'deny';
|
|
59
|
-
const rules
|
|
60
|
-
|
|
61
|
-
for (let i = 0; i < rawRules.length; i++) {
|
|
62
|
-
const rawRule = rawRules[i];
|
|
63
|
-
if (typeof rawRule !== 'object' || rawRule === null || Array.isArray(rawRule)) {
|
|
64
|
-
throw new ACLRuleError(`Rule ${i} must be a mapping, got ${typeof rawRule}`);
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
const ruleObj = rawRule as Record<string, unknown>;
|
|
68
|
-
for (const key of ['callers', 'targets', 'effect']) {
|
|
69
|
-
if (!(key in ruleObj)) {
|
|
70
|
-
throw new ACLRuleError(`Rule ${i} missing required key '${key}'`);
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
const effect = ruleObj['effect'] as string;
|
|
75
|
-
if (effect !== 'allow' && effect !== 'deny') {
|
|
76
|
-
throw new ACLRuleError(`Rule ${i} has invalid effect '${effect}', must be 'allow' or 'deny'`);
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
const callers = ruleObj['callers'];
|
|
80
|
-
if (!Array.isArray(callers)) {
|
|
81
|
-
throw new ACLRuleError(`Rule ${i} 'callers' must be a list, got ${typeof callers}`);
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
const targets = ruleObj['targets'];
|
|
85
|
-
if (!Array.isArray(targets)) {
|
|
86
|
-
throw new ACLRuleError(`Rule ${i} 'targets' must be a list, got ${typeof targets}`);
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
rules.push({
|
|
90
|
-
callers: callers as string[],
|
|
91
|
-
targets: targets as string[],
|
|
92
|
-
effect,
|
|
93
|
-
description: (ruleObj['description'] as string) ?? '',
|
|
94
|
-
conditions: (ruleObj['conditions'] as Record<string, unknown>) ?? null,
|
|
95
|
-
});
|
|
96
|
-
}
|
|
95
|
+
const rules = rawRules.map((raw, i) => parseAclRule(raw, i));
|
|
97
96
|
|
|
98
97
|
const acl = new ACL(rules, defaultEffect);
|
|
99
98
|
acl._yamlPath = yamlPath;
|
package/src/bindings.ts
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
import { readFileSync, existsSync, readdirSync, statSync } from 'node:fs';
|
|
6
|
-
import { resolve, dirname, join, basename } from 'node:path';
|
|
6
|
+
import { resolve, dirname, join, basename, isAbsolute } from 'node:path';
|
|
7
7
|
import { Type, type TSchema } from '@sinclair/typebox';
|
|
8
8
|
import yaml from 'js-yaml';
|
|
9
9
|
import { FunctionModule } from './decorator.js';
|
|
@@ -18,19 +18,6 @@ import {
|
|
|
18
18
|
import type { Registry } from './registry/registry.js';
|
|
19
19
|
import { jsonSchemaToTypeBox } from './schema/loader.js';
|
|
20
20
|
|
|
21
|
-
const JSON_SCHEMA_TYPE_MAP: Record<string, () => TSchema> = {
|
|
22
|
-
string: () => Type.String(),
|
|
23
|
-
integer: () => Type.Integer(),
|
|
24
|
-
number: () => Type.Number(),
|
|
25
|
-
boolean: () => Type.Boolean(),
|
|
26
|
-
array: () => Type.Array(Type.Unknown()),
|
|
27
|
-
object: () => Type.Record(Type.String(), Type.Unknown()),
|
|
28
|
-
};
|
|
29
|
-
|
|
30
|
-
function buildSchemaFromJsonSchema(schema: Record<string, unknown>): TSchema {
|
|
31
|
-
return jsonSchemaToTypeBox(schema);
|
|
32
|
-
}
|
|
33
|
-
|
|
34
21
|
export class BindingLoader {
|
|
35
22
|
async loadBindings(filePath: string, registry: Registry): Promise<FunctionModule[]> {
|
|
36
23
|
const bindingFileDir = dirname(filePath);
|
|
@@ -113,6 +100,12 @@ export class BindingLoader {
|
|
|
113
100
|
|
|
114
101
|
const [modulePath, callableName] = targetString.split(':', 2);
|
|
115
102
|
|
|
103
|
+
if (modulePath.includes('..')) {
|
|
104
|
+
throw new BindingInvalidTargetError(
|
|
105
|
+
`Module path '${modulePath}' must not contain '..' segments`,
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
|
|
116
109
|
let mod: Record<string, unknown>;
|
|
117
110
|
try {
|
|
118
111
|
mod = await import(modulePath);
|
|
@@ -165,8 +158,8 @@ export class BindingLoader {
|
|
|
165
158
|
if ('input_schema' in binding || 'output_schema' in binding) {
|
|
166
159
|
const inputSchemaDict = (binding['input_schema'] as Record<string, unknown>) ?? {};
|
|
167
160
|
const outputSchemaDict = (binding['output_schema'] as Record<string, unknown>) ?? {};
|
|
168
|
-
inputSchema =
|
|
169
|
-
outputSchema =
|
|
161
|
+
inputSchema = jsonSchemaToTypeBox(inputSchemaDict);
|
|
162
|
+
outputSchema = jsonSchemaToTypeBox(outputSchemaDict);
|
|
170
163
|
} else if ('schema_ref' in binding) {
|
|
171
164
|
const refPath = resolve(bindingFileDir, binding['schema_ref'] as string);
|
|
172
165
|
if (!existsSync(refPath)) {
|
|
@@ -178,10 +171,10 @@ export class BindingLoader {
|
|
|
178
171
|
} catch (e) {
|
|
179
172
|
throw new BindingFileInvalidError(refPath, `YAML parse error: ${e}`);
|
|
180
173
|
}
|
|
181
|
-
inputSchema =
|
|
174
|
+
inputSchema = jsonSchemaToTypeBox(
|
|
182
175
|
(refData['input_schema'] as Record<string, unknown>) ?? {},
|
|
183
176
|
);
|
|
184
|
-
outputSchema =
|
|
177
|
+
outputSchema = jsonSchemaToTypeBox(
|
|
185
178
|
(refData['output_schema'] as Record<string, unknown>) ?? {},
|
|
186
179
|
);
|
|
187
180
|
} else {
|
package/src/context.ts
CHANGED
|
@@ -21,7 +21,7 @@ export function createIdentity(
|
|
|
21
21
|
export class Context {
|
|
22
22
|
readonly traceId: string;
|
|
23
23
|
readonly callerId: string | null;
|
|
24
|
-
readonly callChain: string[];
|
|
24
|
+
readonly callChain: readonly string[];
|
|
25
25
|
readonly executor: unknown;
|
|
26
26
|
readonly identity: Identity | null;
|
|
27
27
|
redactedInputs: Record<string, unknown> | null;
|
package/src/errors.ts
CHANGED
|
@@ -242,7 +242,7 @@ export class FuncMissingTypeHintError extends ModuleError {
|
|
|
242
242
|
constructor(functionName: string, parameterName: string, options?: { cause?: Error; traceId?: string }) {
|
|
243
243
|
super(
|
|
244
244
|
'FUNC_MISSING_TYPE_HINT',
|
|
245
|
-
`Parameter '${parameterName}' in function '${functionName}' has no type annotation. Add a type
|
|
245
|
+
`Parameter '${parameterName}' in function '${functionName}' has no type annotation. Add a type annotation like '${parameterName}: string'.`,
|
|
246
246
|
{ functionName, parameterName },
|
|
247
247
|
options?.cause,
|
|
248
248
|
options?.traceId,
|
|
@@ -255,7 +255,7 @@ export class FuncMissingReturnTypeError extends ModuleError {
|
|
|
255
255
|
constructor(functionName: string, options?: { cause?: Error; traceId?: string }) {
|
|
256
256
|
super(
|
|
257
257
|
'FUNC_MISSING_RETURN_TYPE',
|
|
258
|
-
`Function '${functionName}' has no return type annotation. Add a return type like '
|
|
258
|
+
`Function '${functionName}' has no return type annotation. Add a return type like ': Record<string, unknown>'.`,
|
|
259
259
|
{ functionName },
|
|
260
260
|
options?.cause,
|
|
261
261
|
options?.traceId,
|
package/src/executor.ts
CHANGED
|
@@ -157,60 +157,85 @@ export class Executor {
|
|
|
157
157
|
context?: Context | null,
|
|
158
158
|
): Promise<Record<string, unknown>> {
|
|
159
159
|
let effectiveInputs = inputs ?? {};
|
|
160
|
+
const ctx = this._createContext(moduleId, context);
|
|
161
|
+
this._checkSafety(moduleId, ctx);
|
|
162
|
+
|
|
163
|
+
const mod = this._lookupModule(moduleId);
|
|
164
|
+
this._checkAcl(moduleId, ctx);
|
|
165
|
+
|
|
166
|
+
effectiveInputs = this._validateInputs(mod, effectiveInputs, ctx);
|
|
167
|
+
|
|
168
|
+
return this._executeWithMiddleware(mod, moduleId, effectiveInputs, ctx);
|
|
169
|
+
}
|
|
160
170
|
|
|
161
|
-
|
|
162
|
-
let ctx: Context;
|
|
171
|
+
private _createContext(moduleId: string, context?: Context | null): Context {
|
|
163
172
|
if (context == null) {
|
|
164
|
-
|
|
165
|
-
ctx = ctx.child(moduleId);
|
|
166
|
-
} else {
|
|
167
|
-
ctx = context.child(moduleId);
|
|
173
|
+
return Context.create(this).child(moduleId);
|
|
168
174
|
}
|
|
175
|
+
return context.child(moduleId);
|
|
176
|
+
}
|
|
169
177
|
|
|
170
|
-
|
|
171
|
-
this._checkSafety(moduleId, ctx);
|
|
172
|
-
|
|
173
|
-
// Step 3 -- Lookup
|
|
178
|
+
private _lookupModule(moduleId: string): Record<string, unknown> {
|
|
174
179
|
const module = this._registry.get(moduleId);
|
|
175
180
|
if (module === null) {
|
|
176
181
|
throw new ModuleNotFoundError(moduleId);
|
|
177
182
|
}
|
|
183
|
+
return module as Record<string, unknown>;
|
|
184
|
+
}
|
|
178
185
|
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
// Step 4 -- ACL
|
|
186
|
+
private _checkAcl(moduleId: string, ctx: Context): void {
|
|
182
187
|
if (this._acl !== null) {
|
|
183
188
|
const allowed = this._acl.check(ctx.callerId, moduleId, ctx);
|
|
184
189
|
if (!allowed) {
|
|
185
190
|
throw new ACLDeniedError(ctx.callerId, moduleId);
|
|
186
191
|
}
|
|
187
192
|
}
|
|
193
|
+
}
|
|
188
194
|
|
|
189
|
-
|
|
195
|
+
private _validateInputs(
|
|
196
|
+
mod: Record<string, unknown>,
|
|
197
|
+
inputs: Record<string, unknown>,
|
|
198
|
+
ctx: Context,
|
|
199
|
+
): Record<string, unknown> {
|
|
190
200
|
const inputSchema = mod['inputSchema'] as TSchema | undefined;
|
|
191
|
-
if (inputSchema
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
201
|
+
if (inputSchema == null) return inputs;
|
|
202
|
+
|
|
203
|
+
this._validateSchema(inputSchema, inputs, 'Input');
|
|
204
|
+
ctx.redactedInputs = redactSensitive(
|
|
205
|
+
inputs,
|
|
206
|
+
inputSchema as unknown as Record<string, unknown>,
|
|
207
|
+
);
|
|
208
|
+
return inputs;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
private _validateSchema(
|
|
212
|
+
schema: TSchema,
|
|
213
|
+
data: Record<string, unknown>,
|
|
214
|
+
direction: string,
|
|
215
|
+
): void {
|
|
216
|
+
if (Value.Check(schema, data)) return;
|
|
203
217
|
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
218
|
+
const errors: Array<Record<string, unknown>> = [];
|
|
219
|
+
for (const error of Value.Errors(schema, data)) {
|
|
220
|
+
errors.push({
|
|
221
|
+
field: error.path || '/',
|
|
222
|
+
code: String(error.type),
|
|
223
|
+
message: error.message,
|
|
224
|
+
});
|
|
208
225
|
}
|
|
226
|
+
throw new SchemaValidationError(`${direction} validation failed`, errors);
|
|
227
|
+
}
|
|
209
228
|
|
|
229
|
+
private async _executeWithMiddleware(
|
|
230
|
+
mod: Record<string, unknown>,
|
|
231
|
+
moduleId: string,
|
|
232
|
+
inputs: Record<string, unknown>,
|
|
233
|
+
ctx: Context,
|
|
234
|
+
): Promise<Record<string, unknown>> {
|
|
235
|
+
let effectiveInputs = inputs;
|
|
210
236
|
let executedMiddlewares: Middleware[] = [];
|
|
211
237
|
|
|
212
238
|
try {
|
|
213
|
-
// Step 6 -- Middleware Before
|
|
214
239
|
try {
|
|
215
240
|
[effectiveInputs, executedMiddlewares] = this._middlewareManager.executeBefore(moduleId, effectiveInputs, ctx);
|
|
216
241
|
} catch (e) {
|
|
@@ -226,29 +251,14 @@ export class Executor {
|
|
|
226
251
|
throw e;
|
|
227
252
|
}
|
|
228
253
|
|
|
229
|
-
// Step 7 -- Execute with timeout
|
|
230
254
|
let output = await this._executeWithTimeout(mod, moduleId, effectiveInputs, ctx);
|
|
231
255
|
|
|
232
|
-
// Step 8 -- Output Validation
|
|
233
256
|
const outputSchema = mod['outputSchema'] as TSchema | undefined;
|
|
234
257
|
if (outputSchema != null) {
|
|
235
|
-
|
|
236
|
-
const errors: Array<Record<string, unknown>> = [];
|
|
237
|
-
for (const error of Value.Errors(outputSchema, output)) {
|
|
238
|
-
errors.push({
|
|
239
|
-
field: error.path || '/',
|
|
240
|
-
code: String(error.type),
|
|
241
|
-
message: error.message,
|
|
242
|
-
});
|
|
243
|
-
}
|
|
244
|
-
throw new SchemaValidationError('Output validation failed', errors);
|
|
245
|
-
}
|
|
258
|
+
this._validateSchema(outputSchema, output, 'Output');
|
|
246
259
|
}
|
|
247
260
|
|
|
248
|
-
// Step 9 -- Middleware After
|
|
249
261
|
output = this._middlewareManager.executeAfter(moduleId, effectiveInputs, output, ctx);
|
|
250
|
-
|
|
251
|
-
// Step 10 -- Return
|
|
252
262
|
return output;
|
|
253
263
|
} catch (exc) {
|
|
254
264
|
if (executedMiddlewares.length > 0) {
|
|
@@ -337,12 +347,15 @@ export class Executor {
|
|
|
337
347
|
return executionPromise;
|
|
338
348
|
}
|
|
339
349
|
|
|
350
|
+
let timer: ReturnType<typeof setTimeout>;
|
|
340
351
|
const timeoutPromise = new Promise<never>((_, reject) => {
|
|
341
|
-
setTimeout(() => {
|
|
352
|
+
timer = setTimeout(() => {
|
|
342
353
|
reject(new ModuleTimeoutError(moduleId, timeoutMs));
|
|
343
354
|
}, timeoutMs);
|
|
344
355
|
});
|
|
345
356
|
|
|
346
|
-
return Promise.race([executionPromise, timeoutPromise])
|
|
357
|
+
return Promise.race([executionPromise, timeoutPromise]).finally(() => {
|
|
358
|
+
clearTimeout(timer);
|
|
359
|
+
});
|
|
347
360
|
}
|
|
348
361
|
}
|
package/src/index.ts
CHANGED
|
@@ -78,4 +78,4 @@ export type { Span, SpanExporter } from './observability/tracing.js';
|
|
|
78
78
|
export { MetricsCollector, MetricsMiddleware } from './observability/metrics.js';
|
|
79
79
|
export { ContextLogger, ObsLoggingMiddleware } from './observability/context-logger.js';
|
|
80
80
|
|
|
81
|
-
export const VERSION = '0.1.
|
|
81
|
+
export const VERSION = '0.1.2';
|
|
@@ -12,7 +12,7 @@ export interface Logger {
|
|
|
12
12
|
|
|
13
13
|
const defaultLogger: Logger = {
|
|
14
14
|
info(message: string, extra?: Record<string, unknown>) {
|
|
15
|
-
console.
|
|
15
|
+
console.info(message, extra ?? '');
|
|
16
16
|
},
|
|
17
17
|
error(message: string, extra?: Record<string, unknown>) {
|
|
18
18
|
console.error(message, extra ?? '');
|
|
@@ -95,8 +95,8 @@ export class MiddlewareManager {
|
|
|
95
95
|
if (result !== null) {
|
|
96
96
|
return result;
|
|
97
97
|
}
|
|
98
|
-
} catch {
|
|
99
|
-
|
|
98
|
+
} catch (e) {
|
|
99
|
+
console.warn('[apcore:middleware] Error in onError handler, continuing:', e);
|
|
100
100
|
continue;
|
|
101
101
|
}
|
|
102
102
|
}
|