apcore-js 0.18.0 → 0.20.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/README.md +112 -9
- package/dist/acl-handlers.d.ts +14 -0
- package/dist/acl-handlers.d.ts.map +1 -1
- package/dist/acl-handlers.js +37 -4
- package/dist/acl-handlers.js.map +1 -1
- package/dist/acl.d.ts +22 -1
- package/dist/acl.d.ts.map +1 -1
- package/dist/acl.js +90 -34
- package/dist/acl.js.map +1 -1
- package/dist/async-task.d.ts +70 -16
- package/dist/async-task.d.ts.map +1 -1
- package/dist/async-task.js +212 -72
- package/dist/async-task.js.map +1 -1
- package/dist/bindings.d.ts.map +1 -1
- package/dist/bindings.js +113 -11
- package/dist/bindings.js.map +1 -1
- package/dist/builtin-steps.d.ts +33 -8
- package/dist/builtin-steps.d.ts.map +1 -1
- package/dist/builtin-steps.js +119 -47
- package/dist/builtin-steps.js.map +1 -1
- package/dist/client.d.ts +1 -0
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js.map +1 -1
- package/dist/config.d.ts +38 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +163 -33
- package/dist/config.js.map +1 -1
- package/dist/context.d.ts +34 -7
- package/dist/context.d.ts.map +1 -1
- package/dist/context.js +108 -40
- package/dist/context.js.map +1 -1
- package/dist/decorator.d.ts +3 -0
- package/dist/decorator.d.ts.map +1 -1
- package/dist/decorator.js +3 -0
- package/dist/decorator.js.map +1 -1
- package/dist/errors.d.ts +88 -2
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +231 -56
- package/dist/errors.js.map +1 -1
- package/dist/events/circuit-breaker.d.ts +45 -0
- package/dist/events/circuit-breaker.d.ts.map +1 -0
- package/dist/events/circuit-breaker.js +115 -0
- package/dist/events/circuit-breaker.js.map +1 -0
- package/dist/events/emitter.d.ts +24 -1
- package/dist/events/emitter.d.ts.map +1 -1
- package/dist/events/emitter.js +86 -12
- package/dist/events/emitter.js.map +1 -1
- package/dist/events/index.d.ts +4 -2
- package/dist/events/index.d.ts.map +1 -1
- package/dist/events/index.js +3 -2
- package/dist/events/index.js.map +1 -1
- package/dist/events/subscribers.d.ts +33 -1
- package/dist/events/subscribers.d.ts.map +1 -1
- package/dist/events/subscribers.js +124 -1
- package/dist/events/subscribers.js.map +1 -1
- package/dist/executor.d.ts +14 -3
- package/dist/executor.d.ts.map +1 -1
- package/dist/executor.js +155 -48
- package/dist/executor.js.map +1 -1
- package/dist/generated/version.d.ts +1 -1
- package/dist/generated/version.js +1 -1
- package/dist/index.d.ts +47 -25
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +35 -18
- package/dist/index.js.map +1 -1
- package/dist/middleware/base.d.ts +25 -3
- package/dist/middleware/base.d.ts.map +1 -1
- package/dist/middleware/base.js +24 -0
- package/dist/middleware/base.js.map +1 -1
- package/dist/middleware/circuit-breaker.d.ts +54 -0
- package/dist/middleware/circuit-breaker.d.ts.map +1 -0
- package/dist/middleware/circuit-breaker.js +168 -0
- package/dist/middleware/circuit-breaker.js.map +1 -0
- package/dist/middleware/context-namespace.d.ts +30 -0
- package/dist/middleware/context-namespace.d.ts.map +1 -0
- package/dist/middleware/context-namespace.js +38 -0
- package/dist/middleware/context-namespace.js.map +1 -0
- package/dist/middleware/index.d.ts +8 -2
- package/dist/middleware/index.d.ts.map +1 -1
- package/dist/middleware/index.js +5 -2
- package/dist/middleware/index.js.map +1 -1
- package/dist/middleware/logging.d.ts +6 -0
- package/dist/middleware/logging.d.ts.map +1 -1
- package/dist/middleware/logging.js +13 -3
- package/dist/middleware/logging.js.map +1 -1
- package/dist/middleware/manager.d.ts +11 -4
- package/dist/middleware/manager.d.ts.map +1 -1
- package/dist/middleware/manager.js +26 -9
- package/dist/middleware/manager.js.map +1 -1
- package/dist/middleware/platform-notify.d.ts +8 -4
- package/dist/middleware/platform-notify.d.ts.map +1 -1
- package/dist/middleware/platform-notify.js +15 -7
- package/dist/middleware/platform-notify.js.map +1 -1
- package/dist/middleware/retry.d.ts +16 -7
- package/dist/middleware/retry.d.ts.map +1 -1
- package/dist/middleware/retry.js +21 -15
- package/dist/middleware/retry.js.map +1 -1
- package/dist/middleware/tracing.d.ts +50 -0
- package/dist/middleware/tracing.d.ts.map +1 -0
- package/dist/middleware/tracing.js +89 -0
- package/dist/middleware/tracing.js.map +1 -0
- package/dist/observability/batch-span-processor.d.ts +48 -0
- package/dist/observability/batch-span-processor.d.ts.map +1 -0
- package/dist/observability/batch-span-processor.js +89 -0
- package/dist/observability/batch-span-processor.js.map +1 -0
- package/dist/observability/context-logger.d.ts +54 -1
- package/dist/observability/context-logger.d.ts.map +1 -1
- package/dist/observability/context-logger.js +287 -10
- package/dist/observability/context-logger.js.map +1 -1
- package/dist/observability/error-history.d.ts +36 -7
- package/dist/observability/error-history.d.ts.map +1 -1
- package/dist/observability/error-history.js +169 -50
- package/dist/observability/error-history.js.map +1 -1
- package/dist/observability/index.d.ts +16 -5
- package/dist/observability/index.d.ts.map +1 -1
- package/dist/observability/index.js +8 -3
- package/dist/observability/index.js.map +1 -1
- package/dist/observability/metrics-utils.d.ts.map +1 -1
- package/dist/observability/metrics-utils.js +3 -5
- package/dist/observability/metrics-utils.js.map +1 -1
- package/dist/observability/metrics.d.ts +15 -1
- package/dist/observability/metrics.d.ts.map +1 -1
- package/dist/observability/metrics.js +37 -3
- package/dist/observability/metrics.js.map +1 -1
- package/dist/observability/prometheus-exporter.d.ts +37 -0
- package/dist/observability/prometheus-exporter.d.ts.map +1 -0
- package/dist/observability/prometheus-exporter.js +135 -0
- package/dist/observability/prometheus-exporter.js.map +1 -0
- package/dist/observability/storage.d.ts +43 -0
- package/dist/observability/storage.d.ts.map +1 -0
- package/dist/observability/storage.js +58 -0
- package/dist/observability/storage.js.map +1 -0
- package/dist/observability/store.d.ts +29 -0
- package/dist/observability/store.d.ts.map +1 -0
- package/dist/observability/store.js +36 -0
- package/dist/observability/store.js.map +1 -0
- package/dist/observability/tracing.d.ts +2 -0
- package/dist/observability/tracing.d.ts.map +1 -1
- package/dist/observability/tracing.js +12 -2
- package/dist/observability/tracing.js.map +1 -1
- package/dist/observability/usage-exporter.d.ts +58 -0
- package/dist/observability/usage-exporter.d.ts.map +1 -0
- package/dist/observability/usage-exporter.js +86 -0
- package/dist/observability/usage-exporter.js.map +1 -0
- package/dist/observability/usage.d.ts +18 -1
- package/dist/observability/usage.d.ts.map +1 -1
- package/dist/observability/usage.js +35 -4
- package/dist/observability/usage.js.map +1 -1
- package/dist/pipeline-config.d.ts +24 -7
- package/dist/pipeline-config.d.ts.map +1 -1
- package/dist/pipeline-config.js +113 -19
- package/dist/pipeline-config.js.map +1 -1
- package/dist/pipeline.d.ts +123 -2
- package/dist/pipeline.d.ts.map +1 -1
- package/dist/pipeline.js +249 -50
- package/dist/pipeline.js.map +1 -1
- package/dist/registry/conflicts.d.ts +2 -2
- package/dist/registry/conflicts.d.ts.map +1 -1
- package/dist/registry/conflicts.js +10 -11
- package/dist/registry/conflicts.js.map +1 -1
- package/dist/registry/dependencies.d.ts +1 -1
- package/dist/registry/dependencies.d.ts.map +1 -1
- package/dist/registry/dependencies.js +69 -20
- package/dist/registry/dependencies.js.map +1 -1
- package/dist/registry/index.d.ts +2 -0
- package/dist/registry/index.d.ts.map +1 -1
- package/dist/registry/index.js +1 -0
- package/dist/registry/index.js.map +1 -1
- package/dist/registry/multi-class.d.ts +57 -0
- package/dist/registry/multi-class.d.ts.map +1 -0
- package/dist/registry/multi-class.js +120 -0
- package/dist/registry/multi-class.js.map +1 -0
- package/dist/registry/registry.d.ts +99 -4
- package/dist/registry/registry.d.ts.map +1 -1
- package/dist/registry/registry.js +291 -33
- package/dist/registry/registry.js.map +1 -1
- package/dist/registry/scanner.d.ts.map +1 -1
- package/dist/registry/scanner.js +6 -0
- package/dist/registry/scanner.js.map +1 -1
- package/dist/registry/version.d.ts +1 -0
- package/dist/registry/version.d.ts.map +1 -1
- package/dist/registry/version.js +33 -4
- package/dist/registry/version.js.map +1 -1
- package/dist/schema/constants.d.ts +9 -0
- package/dist/schema/constants.d.ts.map +1 -0
- package/dist/schema/constants.js +9 -0
- package/dist/schema/constants.js.map +1 -0
- package/dist/schema/extractor.d.ts +69 -0
- package/dist/schema/extractor.d.ts.map +1 -0
- package/dist/schema/extractor.js +142 -0
- package/dist/schema/extractor.js.map +1 -0
- package/dist/schema/index.d.ts +3 -1
- package/dist/schema/index.d.ts.map +1 -1
- package/dist/schema/index.js +2 -1
- package/dist/schema/index.js.map +1 -1
- package/dist/schema/loader.d.ts +27 -3
- package/dist/schema/loader.d.ts.map +1 -1
- package/dist/schema/loader.js +137 -32
- package/dist/schema/loader.js.map +1 -1
- package/dist/schema/ref-resolver.d.ts.map +1 -1
- package/dist/schema/ref-resolver.js +10 -1
- package/dist/schema/ref-resolver.js.map +1 -1
- package/dist/schema/types.d.ts +4 -0
- package/dist/schema/types.d.ts.map +1 -1
- package/dist/schema/types.js.map +1 -1
- package/dist/schema/validator.d.ts +9 -0
- package/dist/schema/validator.d.ts.map +1 -1
- package/dist/schema/validator.js +153 -4
- package/dist/schema/validator.js.map +1 -1
- package/dist/sys-modules/audit.d.ts +50 -0
- package/dist/sys-modules/audit.d.ts.map +1 -0
- package/dist/sys-modules/audit.js +89 -0
- package/dist/sys-modules/audit.js.map +1 -0
- package/dist/sys-modules/control.d.ts +32 -4
- package/dist/sys-modules/control.d.ts.map +1 -1
- package/dist/sys-modules/control.js +197 -23
- package/dist/sys-modules/control.js.map +1 -1
- package/dist/sys-modules/index.d.ts +7 -2
- package/dist/sys-modules/index.d.ts.map +1 -1
- package/dist/sys-modules/index.js +3 -1
- package/dist/sys-modules/index.js.map +1 -1
- package/dist/sys-modules/overrides.d.ts +58 -0
- package/dist/sys-modules/overrides.d.ts.map +1 -0
- package/dist/sys-modules/overrides.js +106 -0
- package/dist/sys-modules/overrides.js.map +1 -0
- package/dist/sys-modules/registration.d.ts +18 -1
- package/dist/sys-modules/registration.d.ts.map +1 -1
- package/dist/sys-modules/registration.js +115 -11
- package/dist/sys-modules/registration.js.map +1 -1
- package/dist/sys-modules/toggle.d.ts +7 -2
- package/dist/sys-modules/toggle.d.ts.map +1 -1
- package/dist/sys-modules/toggle.js +61 -5
- package/dist/sys-modules/toggle.js.map +1 -1
- package/dist/trace-context.d.ts +47 -9
- package/dist/trace-context.d.ts.map +1 -1
- package/dist/trace-context.js +139 -16
- package/dist/trace-context.js.map +1 -1
- package/dist/utils/index.d.ts.map +1 -1
- package/dist/utils/index.js +2 -1
- package/dist/utils/index.js.map +1 -1
- package/package.json +1 -1
|
@@ -3,9 +3,11 @@
|
|
|
3
3
|
*/
|
|
4
4
|
import { getDefault } from '../config.js';
|
|
5
5
|
import { InvalidInputError, ModuleNotFoundError } from '../errors.js';
|
|
6
|
+
import { detectIdConflicts } from './conflicts.js';
|
|
6
7
|
import { resolveDependencies } from './dependencies.js';
|
|
7
8
|
import { resolveEntryPoint } from './entry-point.js';
|
|
8
9
|
import { mergeModuleMetadata, parseDependencies } from './metadata.js';
|
|
10
|
+
import { _discoverMultiClass } from './multi-class.js';
|
|
9
11
|
import { getSchema } from './schema-export.js';
|
|
10
12
|
import { toStrictSchema } from '../schema/strict.js';
|
|
11
13
|
import { deepCopy } from '../utils/index.js';
|
|
@@ -64,17 +66,17 @@ export const MODULE_ID_PATTERN = /^[a-z][a-z0-9_]*(\.[a-z][a-z0-9_]*)*$/;
|
|
|
64
66
|
*/
|
|
65
67
|
export const MAX_MODULE_ID_LENGTH = 192;
|
|
66
68
|
/**
|
|
67
|
-
* Reserved words that cannot appear as
|
|
69
|
+
* Reserved words that cannot appear as the first segment of a module ID.
|
|
68
70
|
*/
|
|
69
71
|
export const RESERVED_WORDS = new Set(['system', 'internal', 'core', 'apcore', 'plugin', 'schema', 'acl']);
|
|
70
72
|
/**
|
|
71
73
|
* Validate a module ID against PROTOCOL_SPEC §2.7 in canonical order.
|
|
72
74
|
*
|
|
73
|
-
* Order: empty → pattern → length → reserved (
|
|
75
|
+
* Order: empty → pattern → length → reserved (first-segment).
|
|
74
76
|
* Duplicate detection is the caller's responsibility (it requires registry
|
|
75
77
|
* state).
|
|
76
78
|
*
|
|
77
|
-
* When `allowReserved` is true the
|
|
79
|
+
* When `allowReserved` is true the first-segment reserved word check is
|
|
78
80
|
* skipped — used by `Registry.registerInternal` so sys modules can use the
|
|
79
81
|
* `system.*` prefix. All other validations (empty, pattern, length) still
|
|
80
82
|
* apply.
|
|
@@ -98,12 +100,11 @@ function validateModuleId(moduleId, allowReserved) {
|
|
|
98
100
|
if (moduleId.length > MAX_MODULE_ID_LENGTH) {
|
|
99
101
|
throw new InvalidInputError(`Module ID exceeds maximum length of ${MAX_MODULE_ID_LENGTH}: ${moduleId.length}`);
|
|
100
102
|
}
|
|
101
|
-
// 4. reserved word
|
|
103
|
+
// 4. reserved word first-segment check (skipped for registerInternal)
|
|
102
104
|
if (!allowReserved) {
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
}
|
|
105
|
+
const firstSegment = moduleId.split('.')[0];
|
|
106
|
+
if (RESERVED_WORDS.has(firstSegment)) {
|
|
107
|
+
throw new InvalidInputError(`Module ID contains reserved word: '${firstSegment}'`);
|
|
107
108
|
}
|
|
108
109
|
}
|
|
109
110
|
}
|
|
@@ -116,6 +117,7 @@ export class Registry {
|
|
|
116
117
|
[REGISTRY_EVENTS.UNREGISTER, []],
|
|
117
118
|
]);
|
|
118
119
|
_idMap = {};
|
|
120
|
+
_lowercaseMap = new Map();
|
|
119
121
|
_schemaCache = new Map();
|
|
120
122
|
_config;
|
|
121
123
|
_watchers;
|
|
@@ -173,9 +175,21 @@ export class Registry {
|
|
|
173
175
|
async _discoverCustom() {
|
|
174
176
|
const rootPaths = this._extensionRoots.map((r) => r['root']);
|
|
175
177
|
const customModules = await this._customDiscoverer.discover(rootPaths);
|
|
178
|
+
if (!Array.isArray(customModules)) {
|
|
179
|
+
console.warn(`[apcore:registry] Custom discoverer returned non-array (${typeof customModules}); expected Array<{moduleId, module}>. Ignoring.`);
|
|
180
|
+
return 0;
|
|
181
|
+
}
|
|
176
182
|
let count = 0;
|
|
177
183
|
for (const entry of customModules) {
|
|
184
|
+
if (entry === null || typeof entry !== 'object') {
|
|
185
|
+
console.warn(`[apcore:registry] Malformed entry from custom discoverer (expected object, got ${entry === null ? 'null' : typeof entry}); skipping.`);
|
|
186
|
+
continue;
|
|
187
|
+
}
|
|
178
188
|
const { moduleId, module: mod } = entry;
|
|
189
|
+
if (typeof moduleId !== 'string' || mod === undefined) {
|
|
190
|
+
console.warn(`[apcore:registry] Malformed entry from custom discoverer (missing 'moduleId' string or 'module'); skipping.`);
|
|
191
|
+
continue;
|
|
192
|
+
}
|
|
179
193
|
// Apply custom validator if set
|
|
180
194
|
if (this._customValidator !== null) {
|
|
181
195
|
const errors = await this._customValidator.validate(mod);
|
|
@@ -184,8 +198,19 @@ export class Registry {
|
|
|
184
198
|
continue;
|
|
185
199
|
}
|
|
186
200
|
}
|
|
201
|
+
// PROTOCOL_SPEC §2.7 ID validation — sync finding A-D-102.
|
|
202
|
+
// Mirrors apcore-python `Registry._discover_custom` which calls
|
|
203
|
+
// `_validate_module_id` before registration. Invalid IDs are skipped
|
|
204
|
+
// with a warning rather than aborting the whole discover run.
|
|
187
205
|
try {
|
|
188
|
-
|
|
206
|
+
validateModuleId(moduleId, false);
|
|
207
|
+
}
|
|
208
|
+
catch (e) {
|
|
209
|
+
console.warn(`[apcore:registry] Skipping custom-discovered module with invalid ID '${moduleId}': ${e.message}`);
|
|
210
|
+
continue;
|
|
211
|
+
}
|
|
212
|
+
try {
|
|
213
|
+
this._registerImpl(moduleId, mod);
|
|
189
214
|
count++;
|
|
190
215
|
}
|
|
191
216
|
catch (e) {
|
|
@@ -194,6 +219,20 @@ export class Registry {
|
|
|
194
219
|
}
|
|
195
220
|
return count;
|
|
196
221
|
}
|
|
222
|
+
/**
|
|
223
|
+
* Default discovery pipeline (D-32 — 8 canonical stages, mirroring
|
|
224
|
+
* apcore-rust `default_discoverer.rs`):
|
|
225
|
+
*
|
|
226
|
+
* 1. _ensureIdMap — lazy-load the optional id_map.json
|
|
227
|
+
* 2. _scanRoots — walk extension roots
|
|
228
|
+
* 3. _applyIdMapOverrides — rewrite canonical IDs from the map
|
|
229
|
+
* 4. _loadAllMetadata — load each module's `module.yaml`
|
|
230
|
+
* 5. _resolveAllEntryPoints — import the JS/TS entry point
|
|
231
|
+
* 6. _validateAll — run module/custom validators
|
|
232
|
+
* 7. _filterIdConflicts — batch-drop conflicting / invalid IDs
|
|
233
|
+
* 8. _resolveLoadOrder + _registerInOrder
|
|
234
|
+
* — topological sort then register.
|
|
235
|
+
*/
|
|
197
236
|
async _discoverDefault() {
|
|
198
237
|
await this._ensureIdMap();
|
|
199
238
|
const discovered = await this._scanRoots();
|
|
@@ -201,8 +240,9 @@ export class Registry {
|
|
|
201
240
|
const rawMetadata = await this._loadAllMetadata(discovered);
|
|
202
241
|
const resolvedModules = await this._resolveAllEntryPoints(discovered, rawMetadata);
|
|
203
242
|
const validModules = await this._validateAll(resolvedModules);
|
|
204
|
-
const
|
|
205
|
-
|
|
243
|
+
const filteredModules = this._filterIdConflicts(validModules, rawMetadata);
|
|
244
|
+
const loadOrder = this._resolveLoadOrder(filteredModules, rawMetadata);
|
|
245
|
+
return this._registerInOrder(loadOrder, filteredModules, rawMetadata);
|
|
206
246
|
}
|
|
207
247
|
async _scanRoots() {
|
|
208
248
|
let maxDepth = 8;
|
|
@@ -229,7 +269,13 @@ export class Registry {
|
|
|
229
269
|
? dm.filePath.slice(root.length + 1)
|
|
230
270
|
: null;
|
|
231
271
|
if (relPath && relPath in this._idMap) {
|
|
232
|
-
|
|
272
|
+
const rawId = this._idMap[relPath]['id'];
|
|
273
|
+
if (typeof rawId === 'string' && rawId.length > 0) {
|
|
274
|
+
dm.canonicalId = rawId;
|
|
275
|
+
}
|
|
276
|
+
else {
|
|
277
|
+
console.warn(`[apcore:registry] ID map entry for '${relPath}' has invalid 'id' field (got ${typeof rawId}), skipping override`);
|
|
278
|
+
}
|
|
233
279
|
break;
|
|
234
280
|
}
|
|
235
281
|
}
|
|
@@ -276,24 +322,99 @@ export class Registry {
|
|
|
276
322
|
}
|
|
277
323
|
return validModules;
|
|
278
324
|
}
|
|
325
|
+
/**
|
|
326
|
+
* Stage 7 (D-32) — batch-drop modules with invalid or conflicting IDs.
|
|
327
|
+
*
|
|
328
|
+
* Mirrors apcore-python `_filter_id_conflicts` and apcore-rust
|
|
329
|
+
* `default_discoverer::filter_id_conflicts`. Two failure modes drop a
|
|
330
|
+
* module here (warn + skip) rather than aborting the whole batch:
|
|
331
|
+
* - PROTOCOL_SPEC §2.7 ID validation (empty / pattern / length /
|
|
332
|
+
* reserved-word first segment), and
|
|
333
|
+
* - Algorithm A03 conflict detection (duplicate against an existing
|
|
334
|
+
* registration, lowercase collision, reserved-word collision).
|
|
335
|
+
*
|
|
336
|
+
* Soft-severity conflicts (e.g. case-insensitive match against an
|
|
337
|
+
* already-registered ID at `warn` level) are NOT dropped here — the
|
|
338
|
+
* warning is logged and the module flows through to registration so
|
|
339
|
+
* existing behaviour is preserved. Only `error`-severity conflicts and
|
|
340
|
+
* invalid IDs are filtered out.
|
|
341
|
+
*
|
|
342
|
+
* `rawMetadata` is accepted for cross-language signature parity with
|
|
343
|
+
* the Rust/Python helpers (which may inspect metadata for additional
|
|
344
|
+
* checks); the TS implementation reads only the module ID.
|
|
345
|
+
*/
|
|
346
|
+
_filterIdConflicts(validModules, _rawMetadata) {
|
|
347
|
+
const filtered = new Map();
|
|
348
|
+
// Track within-batch IDs (case-insensitive) so two newly-discovered
|
|
349
|
+
// modules whose IDs collide on lowercase don't both slip through.
|
|
350
|
+
const batchLowercase = new Map(this._lowercaseMap);
|
|
351
|
+
const batchIds = new Set(this._modules.keys());
|
|
352
|
+
for (const [modId, mod] of validModules.entries()) {
|
|
353
|
+
try {
|
|
354
|
+
validateModuleId(modId, false);
|
|
355
|
+
}
|
|
356
|
+
catch (e) {
|
|
357
|
+
console.warn(`[apcore:registry] Skipping discovered module with invalid ID '${modId}': ${e.message}`);
|
|
358
|
+
continue;
|
|
359
|
+
}
|
|
360
|
+
const conflict = detectIdConflicts(modId, batchIds, RESERVED_WORDS, batchLowercase);
|
|
361
|
+
if (conflict !== null) {
|
|
362
|
+
if (conflict.severity === 'error') {
|
|
363
|
+
console.warn(`[apcore:registry] Skipping discovered module '${modId}' due to ID conflict: ${conflict.message}`);
|
|
364
|
+
continue;
|
|
365
|
+
}
|
|
366
|
+
// Soft severity — log but keep the module.
|
|
367
|
+
console.warn(`[apcore:registry] ID conflict: ${conflict.message}`);
|
|
368
|
+
}
|
|
369
|
+
filtered.set(modId, mod);
|
|
370
|
+
batchIds.add(modId);
|
|
371
|
+
batchLowercase.set(modId.toLowerCase(), modId);
|
|
372
|
+
}
|
|
373
|
+
return filtered;
|
|
374
|
+
}
|
|
279
375
|
_resolveLoadOrder(validModules, rawMetadata) {
|
|
280
376
|
const modulesWithDeps = [];
|
|
281
|
-
|
|
377
|
+
const moduleVersions = new Map();
|
|
378
|
+
for (const [modId, cls] of validModules.entries()) {
|
|
282
379
|
const meta = rawMetadata.get(modId) ?? {};
|
|
283
380
|
const depsRaw = meta['dependencies'] ?? [];
|
|
284
381
|
modulesWithDeps.push([modId, depsRaw.length > 0 ? parseDependencies(depsRaw) : []]);
|
|
382
|
+
const yamlVersion = meta['version'];
|
|
383
|
+
const codeVersion = cls?.version;
|
|
384
|
+
const resolvedVersion = (typeof yamlVersion === 'string' && yamlVersion) ||
|
|
385
|
+
(typeof codeVersion === 'string' && codeVersion) ||
|
|
386
|
+
'1.0.0';
|
|
387
|
+
moduleVersions.set(modId, resolvedVersion);
|
|
388
|
+
}
|
|
389
|
+
// Include already-registered modules so inter-batch version constraints
|
|
390
|
+
// resolve against the live registry too.
|
|
391
|
+
for (const [existingId, existingMod] of this._modules.entries()) {
|
|
392
|
+
if (!moduleVersions.has(existingId)) {
|
|
393
|
+
const existingVersion = existingMod?.version;
|
|
394
|
+
if (typeof existingVersion === 'string') {
|
|
395
|
+
moduleVersions.set(existingId, existingVersion);
|
|
396
|
+
}
|
|
397
|
+
}
|
|
285
398
|
}
|
|
286
|
-
const knownIds = new Set(
|
|
287
|
-
|
|
399
|
+
const knownIds = new Set([
|
|
400
|
+
...modulesWithDeps.map(([id]) => id),
|
|
401
|
+
...this._modules.keys(),
|
|
402
|
+
]);
|
|
403
|
+
return resolveDependencies(modulesWithDeps, knownIds, moduleVersions);
|
|
288
404
|
}
|
|
289
405
|
_registerInOrder(loadOrder, validModules, rawMetadata) {
|
|
406
|
+
// Stage 8 (D-32). Conflict detection / ID validation already happened in
|
|
407
|
+
// `_filterIdConflicts` (stage 7), so this loop is purely a register pass.
|
|
290
408
|
let count = 0;
|
|
291
409
|
for (const modId of loadOrder) {
|
|
292
410
|
const mod = validModules.get(modId);
|
|
411
|
+
if (mod === undefined)
|
|
412
|
+
continue;
|
|
293
413
|
const modObj = mod;
|
|
294
414
|
const mergedMeta = mergeModuleMetadata(modObj, rawMetadata.get(modId) ?? {});
|
|
295
415
|
this._modules.set(modId, mod);
|
|
296
416
|
this._moduleMeta.set(modId, mergedMeta);
|
|
417
|
+
this._lowercaseMap.set(modId.toLowerCase(), modId);
|
|
297
418
|
if (typeof modObj['onLoad'] === 'function') {
|
|
298
419
|
try {
|
|
299
420
|
modObj['onLoad']();
|
|
@@ -302,6 +423,7 @@ export class Registry {
|
|
|
302
423
|
console.warn(`[apcore:registry] onLoad failed for ${modId}, skipping:`, e);
|
|
303
424
|
this._modules.delete(modId);
|
|
304
425
|
this._moduleMeta.delete(modId);
|
|
426
|
+
this._lowercaseMap.delete(modId.toLowerCase());
|
|
305
427
|
continue;
|
|
306
428
|
}
|
|
307
429
|
}
|
|
@@ -316,16 +438,49 @@ export class Registry {
|
|
|
316
438
|
* Validation order (PROTOCOL_SPEC §2.7, aligned with apcore-python and
|
|
317
439
|
* apcore-rust): empty → pattern → length → reserved (per-segment) →
|
|
318
440
|
* duplicate.
|
|
441
|
+
*
|
|
442
|
+
* The optional `version` and `metadata` parameters mirror apcore-python's
|
|
443
|
+
* `Registry.register(module_id, module, version=None, metadata=None)` for
|
|
444
|
+
* cross-language signature parity (sync finding A-001). Multi-version
|
|
445
|
+
* coexistence is not yet implemented in this SDK — when supplied, both
|
|
446
|
+
* fields are merged into the module's metadata so callers can read them
|
|
447
|
+
* back via `getDefinition()` and `list({tags})`. See PROTOCOL_SPEC §5.4.
|
|
319
448
|
*/
|
|
320
|
-
register(moduleId, module) {
|
|
449
|
+
register(moduleId, module, version, metadata) {
|
|
321
450
|
validateModuleId(moduleId, false);
|
|
322
|
-
if (this.
|
|
323
|
-
|
|
451
|
+
if (this._customValidator !== null) {
|
|
452
|
+
const result = this._customValidator.validate(module);
|
|
453
|
+
if (result instanceof Promise) {
|
|
454
|
+
throw new InvalidInputError(`Custom validator for '${moduleId}' is async — use discover() which awaits the validator, or register after awaiting validation manually.`);
|
|
455
|
+
}
|
|
456
|
+
if (result.length > 0) {
|
|
457
|
+
throw new InvalidInputError(`Custom validator rejected module '${moduleId}': ${result.join('; ')}`);
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
const overrides = { ...(metadata ?? {}) };
|
|
461
|
+
if (version !== undefined && version !== null) {
|
|
462
|
+
overrides['version'] = version;
|
|
463
|
+
}
|
|
464
|
+
this._registerImpl(moduleId, module, overrides);
|
|
465
|
+
}
|
|
466
|
+
/** Inner registration — no validator, no ID validation. Used by discover() paths that run their own checks. */
|
|
467
|
+
_registerImpl(moduleId, module, metadataOverrides = {}) {
|
|
468
|
+
// Algorithm A03: detect ID conflicts (exact duplicate, reserved word, case collision)
|
|
469
|
+
const conflict = detectIdConflicts(moduleId, new Set(this._modules.keys()), RESERVED_WORDS, this._lowercaseMap);
|
|
470
|
+
if (conflict !== null) {
|
|
471
|
+
if (conflict.severity === 'error') {
|
|
472
|
+
throw new InvalidInputError(conflict.message);
|
|
473
|
+
}
|
|
474
|
+
else {
|
|
475
|
+
console.warn(`[apcore:registry] ID conflict: ${conflict.message}`);
|
|
476
|
+
}
|
|
324
477
|
}
|
|
325
478
|
this._modules.set(moduleId, module);
|
|
326
|
-
|
|
479
|
+
this._lowercaseMap.set(moduleId.toLowerCase(), moduleId);
|
|
480
|
+
// Populate metadata from the module object, layering any explicit overrides
|
|
481
|
+
// (e.g. the `version` / `metadata` args passed to `register()`) on top.
|
|
327
482
|
const modObj = module;
|
|
328
|
-
this._moduleMeta.set(moduleId, mergeModuleMetadata(modObj,
|
|
483
|
+
this._moduleMeta.set(moduleId, mergeModuleMetadata(modObj, metadataOverrides));
|
|
329
484
|
// Call onLoad if available
|
|
330
485
|
if (typeof modObj['onLoad'] === 'function') {
|
|
331
486
|
try {
|
|
@@ -334,6 +489,7 @@ export class Registry {
|
|
|
334
489
|
catch (e) {
|
|
335
490
|
this._modules.delete(moduleId);
|
|
336
491
|
this._moduleMeta.delete(moduleId);
|
|
492
|
+
this._lowercaseMap.delete(moduleId.toLowerCase());
|
|
337
493
|
throw e;
|
|
338
494
|
}
|
|
339
495
|
}
|
|
@@ -346,6 +502,7 @@ export class Registry {
|
|
|
346
502
|
this._modules.delete(moduleId);
|
|
347
503
|
this._moduleMeta.delete(moduleId);
|
|
348
504
|
this._schemaCache.delete(moduleId);
|
|
505
|
+
this._lowercaseMap.delete(moduleId.toLowerCase());
|
|
349
506
|
// Call onUnload if available
|
|
350
507
|
const modObj = module;
|
|
351
508
|
if (typeof modObj['onUnload'] === 'function') {
|
|
@@ -359,7 +516,19 @@ export class Registry {
|
|
|
359
516
|
this._triggerEvent(REGISTRY_EVENTS.UNREGISTER, moduleId, module);
|
|
360
517
|
return true;
|
|
361
518
|
}
|
|
362
|
-
|
|
519
|
+
/**
|
|
520
|
+
* Look up a registered module by ID.
|
|
521
|
+
*
|
|
522
|
+
* @param moduleId - Module identifier (must be non-empty).
|
|
523
|
+
* @param _versionHint - Optional semver range for multi-version coexistence
|
|
524
|
+
* (PROTOCOL_SPEC §5.4). This SDK currently exposes a single-version
|
|
525
|
+
* registry, so the hint is accepted for cross-language API parity with
|
|
526
|
+
* apcore-python (sync finding A-002) but does NOT participate in
|
|
527
|
+
* resolution: the latest registered module for `moduleId` is returned
|
|
528
|
+
* regardless of the hint. When multi-version registration lands, this
|
|
529
|
+
* parameter will gate semver-range matching.
|
|
530
|
+
*/
|
|
531
|
+
get(moduleId, _versionHint) {
|
|
363
532
|
if (moduleId === '') {
|
|
364
533
|
throw new ModuleNotFoundError('');
|
|
365
534
|
}
|
|
@@ -401,7 +570,18 @@ export class Registry {
|
|
|
401
570
|
get moduleIds() {
|
|
402
571
|
return [...this._modules.keys()].sort();
|
|
403
572
|
}
|
|
404
|
-
getDefinition(moduleId) {
|
|
573
|
+
getDefinition(moduleId, _versionHint) {
|
|
574
|
+
// `_versionHint` accepted for cross-language API parity with apcore-python
|
|
575
|
+
// (sync finding A-002 / §5.4). Ignored under the single-version registry;
|
|
576
|
+
// see `get()` for the rationale.
|
|
577
|
+
//
|
|
578
|
+
// D10-011: spec registry-system.md:382 says any error that `get(module_id)`
|
|
579
|
+
// raises is propagated. The empty-string guard mirrors `get()` (line 669)
|
|
580
|
+
// so callers using getDefinition see the same ModuleNotFoundError as
|
|
581
|
+
// get(), matching apcore-python where getDefinition routes through get().
|
|
582
|
+
if (moduleId === '') {
|
|
583
|
+
throw new ModuleNotFoundError('');
|
|
584
|
+
}
|
|
405
585
|
const module = this._modules.get(moduleId);
|
|
406
586
|
if (module == null)
|
|
407
587
|
return null;
|
|
@@ -500,6 +680,29 @@ export class Registry {
|
|
|
500
680
|
}
|
|
501
681
|
}
|
|
502
682
|
}
|
|
683
|
+
/**
|
|
684
|
+
* Watch the configured extension roots for filesystem changes and
|
|
685
|
+
* unregister any module whose source file is modified or deleted.
|
|
686
|
+
*
|
|
687
|
+
* **Cross-language divergence (sync finding A-D-104):** unlike apcore-python
|
|
688
|
+
* (which re-imports the file via `importlib.reload`) and apcore-rust (which
|
|
689
|
+
* triggers full rediscovery), the TypeScript SDK is **event-only**. On a
|
|
690
|
+
* file change the registry:
|
|
691
|
+
* 1. unregisters the previously-loaded module (calling its `onUnload`),
|
|
692
|
+
* 2. emits a `'file_changed'` event with `{ filePath }` payload.
|
|
693
|
+
*
|
|
694
|
+
* Consumers are expected to subscribe and re-register the module
|
|
695
|
+
* themselves (e.g. by calling `registry.discover()` or registering a fresh
|
|
696
|
+
* import). ES module specifiers are immutable in Node — there is no
|
|
697
|
+
* portable "reload from disk" primitive — so a transparent dynamic
|
|
698
|
+
* `import()` would silently return the cached old module on every
|
|
699
|
+
* invocation. A workaround using a cache-busting query (`?v=Date.now()`)
|
|
700
|
+
* leaks the old module each reload and breaks browser bundlers, so it is
|
|
701
|
+
* intentionally **not** offered here.
|
|
702
|
+
*
|
|
703
|
+
* If your application needs Python-style hot-reload semantics, listen for
|
|
704
|
+
* `'file_changed'` and re-discover or re-import explicitly.
|
|
705
|
+
*/
|
|
503
706
|
async watch() {
|
|
504
707
|
if (this._watchers && this._watchers.length > 0) {
|
|
505
708
|
return; // Already watching
|
|
@@ -524,24 +727,32 @@ export class Registry {
|
|
|
524
727
|
if (now - last < 300)
|
|
525
728
|
return;
|
|
526
729
|
this._debounceTimers?.set(fullPath, now);
|
|
730
|
+
const handle = (p) => {
|
|
731
|
+
p.catch((e) => {
|
|
732
|
+
console.warn(`[apcore:registry] Watch handler failed for ${fullPath}:`, e);
|
|
733
|
+
});
|
|
734
|
+
};
|
|
527
735
|
if (eventType === "rename") {
|
|
528
736
|
// Could be create or delete
|
|
529
737
|
try {
|
|
530
738
|
fs.accessSync(fullPath);
|
|
531
|
-
this._handleFileChange(fullPath);
|
|
739
|
+
handle(this._handleFileChange(fullPath));
|
|
532
740
|
}
|
|
533
741
|
catch {
|
|
534
|
-
this._handleFileDeletion(fullPath);
|
|
742
|
+
handle(this._handleFileDeletion(fullPath));
|
|
535
743
|
}
|
|
536
744
|
}
|
|
537
745
|
else {
|
|
538
|
-
this._handleFileChange(fullPath);
|
|
746
|
+
handle(this._handleFileChange(fullPath));
|
|
539
747
|
}
|
|
540
748
|
});
|
|
541
749
|
this._watchers.push(watcher);
|
|
542
750
|
}
|
|
543
|
-
catch {
|
|
544
|
-
//
|
|
751
|
+
catch (e) {
|
|
752
|
+
// Surface real failures (EMFILE, EACCES, Linux kernels < 4.7 without
|
|
753
|
+
// recursive support, etc.). A silently non-functional watch misleads
|
|
754
|
+
// users who expect hot-reload to be active.
|
|
755
|
+
console.warn(`[apcore:registry] fs.watch failed for '${rootPath}' — hot-reload disabled for this root:`, e);
|
|
545
756
|
}
|
|
546
757
|
}
|
|
547
758
|
}
|
|
@@ -564,12 +775,17 @@ export class Registry {
|
|
|
564
775
|
try {
|
|
565
776
|
oldModule.onUnload();
|
|
566
777
|
}
|
|
567
|
-
catch {
|
|
778
|
+
catch (e) {
|
|
779
|
+
console.warn(`[apcore:registry] onUnload failed for '${moduleId}':`, e);
|
|
780
|
+
}
|
|
568
781
|
}
|
|
569
782
|
this.unregister(moduleId);
|
|
570
783
|
}
|
|
571
|
-
// Re-import is complex in ES modules
|
|
572
|
-
|
|
784
|
+
// Re-import is complex in ES modules — tell consumers that a watched file
|
|
785
|
+
// changed so they can re-import and re-register. The earlier design
|
|
786
|
+
// emitted a 'register' event with a null module, which crashed any
|
|
787
|
+
// consumer that accessed fields on the module argument.
|
|
788
|
+
this._triggerEvent("file_changed", moduleId ?? basename(filePath, extname(filePath)), { filePath });
|
|
573
789
|
}
|
|
574
790
|
async _handleFileDeletion(path) {
|
|
575
791
|
const moduleId = this._pathToModuleId(path);
|
|
@@ -579,7 +795,9 @@ export class Registry {
|
|
|
579
795
|
try {
|
|
580
796
|
module.onUnload();
|
|
581
797
|
}
|
|
582
|
-
catch {
|
|
798
|
+
catch (e) {
|
|
799
|
+
console.warn(`[apcore:registry] onUnload failed for '${moduleId}':`, e);
|
|
800
|
+
}
|
|
583
801
|
}
|
|
584
802
|
this.unregister(moduleId);
|
|
585
803
|
}
|
|
@@ -606,12 +824,30 @@ export class Registry {
|
|
|
606
824
|
*/
|
|
607
825
|
registerInternal(moduleId, module) {
|
|
608
826
|
validateModuleId(moduleId, true);
|
|
609
|
-
|
|
610
|
-
|
|
827
|
+
// D11-007: route duplicate detection through detectIdConflicts (with an
|
|
828
|
+
// empty reserved-words set so the bypass for system.* prefixes is
|
|
829
|
+
// preserved). This restores the case-collision branch present in
|
|
830
|
+
// apcore-python (registry.py:1674) and apcore-rust (registry.rs:727).
|
|
831
|
+
// The lowercase-only EBNF in validateModuleId makes case collisions
|
|
832
|
+
// unreachable today, but the contract surface stays aligned across SDKs.
|
|
833
|
+
const conflict = detectIdConflicts(moduleId, new Set(this._modules.keys()), new Set(), this._lowercaseMap);
|
|
834
|
+
if (conflict !== null) {
|
|
835
|
+
if (conflict.severity === 'error') {
|
|
836
|
+
throw new InvalidInputError(conflict.message);
|
|
837
|
+
}
|
|
838
|
+
else {
|
|
839
|
+
console.warn(`[apcore:registry] ID conflict: ${conflict.message}`);
|
|
840
|
+
}
|
|
611
841
|
}
|
|
612
842
|
this._modules.set(moduleId, module);
|
|
613
843
|
const modObj = module;
|
|
614
844
|
this._moduleMeta.set(moduleId, mergeModuleMetadata(modObj, {}));
|
|
845
|
+
// Mirror apcore-python register_internal and apcore-rust register_core:
|
|
846
|
+
// every registration site (including sys/internal) populates the lowercase
|
|
847
|
+
// index. The lowercase-only EBNF pattern enforced by validateModuleId makes
|
|
848
|
+
// case collisions unreachable today, but keeping _lowercaseMap consistent
|
|
849
|
+
// with _modules preserves the invariant for downstream conflict detection.
|
|
850
|
+
this._lowercaseMap.set(moduleId.toLowerCase(), moduleId);
|
|
615
851
|
if (typeof modObj['onLoad'] === 'function') {
|
|
616
852
|
try {
|
|
617
853
|
modObj['onLoad']();
|
|
@@ -619,6 +855,7 @@ export class Registry {
|
|
|
619
855
|
catch (e) {
|
|
620
856
|
this._modules.delete(moduleId);
|
|
621
857
|
this._moduleMeta.delete(moduleId);
|
|
858
|
+
this._lowercaseMap.delete(moduleId.toLowerCase());
|
|
622
859
|
throw e;
|
|
623
860
|
}
|
|
624
861
|
}
|
|
@@ -649,6 +886,18 @@ export class Registry {
|
|
|
649
886
|
this._schemaCache.clear();
|
|
650
887
|
}
|
|
651
888
|
// ── Safe Hot-Reload (F09 / Algorithm A21) ───────────────────────
|
|
889
|
+
/**
|
|
890
|
+
* Discover module IDs for the classes in a single file under multi-class
|
|
891
|
+
* mode (PROTOCOL_SPEC §2.1.1).
|
|
892
|
+
*
|
|
893
|
+
* D-15: cross-language alignment with Python `Registry.discover_multi_class`
|
|
894
|
+
* and the Rust trait method. Internally delegates to the free function
|
|
895
|
+
* {@link discoverMultiClass} (re-exported as `_discoverMultiClass` for
|
|
896
|
+
* scanner internals), so behaviour is identical.
|
|
897
|
+
*/
|
|
898
|
+
discoverMultiClass(filePath, classes, extensionsRoot = 'extensions', multiClassEnabled = false) {
|
|
899
|
+
return _discoverMultiClass(filePath, classes, extensionsRoot, multiClassEnabled);
|
|
900
|
+
}
|
|
652
901
|
/**
|
|
653
902
|
* Number of in-flight executions per module.
|
|
654
903
|
*/
|
|
@@ -710,9 +959,18 @@ export class Registry {
|
|
|
710
959
|
}
|
|
711
960
|
/**
|
|
712
961
|
* Remove the draining mark and clean up drain state.
|
|
962
|
+
*
|
|
963
|
+
* If any waitDrained waiters are still pending (e.g., refCount briefly
|
|
964
|
+
* hit zero and then a new acquire bumped it back up), resolve them first
|
|
965
|
+
* so they do not wait for their individual timeouts before returning.
|
|
713
966
|
*/
|
|
714
967
|
endDrain(moduleId) {
|
|
715
968
|
this._draining.delete(moduleId);
|
|
969
|
+
const resolvers = this._drainResolvers.get(moduleId);
|
|
970
|
+
if (resolvers) {
|
|
971
|
+
for (const resolve of resolvers)
|
|
972
|
+
resolve();
|
|
973
|
+
}
|
|
716
974
|
this._drainResolvers.delete(moduleId);
|
|
717
975
|
this._refCounts.delete(moduleId);
|
|
718
976
|
}
|