@objectstack/runtime 4.0.2 → 4.0.3
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/.turbo/turbo-build.log +8 -8
- package/CHANGELOG.md +9 -0
- package/dist/index.cjs +135 -54
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +135 -54
- package/dist/index.js.map +1 -1
- package/package.json +6 -6
- package/src/dispatcher-plugin.ts +93 -59
- package/src/http-dispatcher.ts +83 -7
- package/vitest.config.ts +1 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@objectstack/runtime",
|
|
3
|
-
"version": "4.0.
|
|
3
|
+
"version": "4.0.3",
|
|
4
4
|
"license": "Apache-2.0",
|
|
5
5
|
"description": "ObjectStack Core Runtime & Query Engine",
|
|
6
6
|
"type": "module",
|
|
@@ -15,14 +15,14 @@
|
|
|
15
15
|
},
|
|
16
16
|
"dependencies": {
|
|
17
17
|
"zod": "^4.3.6",
|
|
18
|
-
"@objectstack/core": "4.0.
|
|
19
|
-
"@objectstack/
|
|
20
|
-
"@objectstack/spec": "4.0.
|
|
21
|
-
"@objectstack/
|
|
18
|
+
"@objectstack/core": "4.0.3",
|
|
19
|
+
"@objectstack/types": "4.0.3",
|
|
20
|
+
"@objectstack/spec": "4.0.3",
|
|
21
|
+
"@objectstack/rest": "4.0.3"
|
|
22
22
|
},
|
|
23
23
|
"devDependencies": {
|
|
24
24
|
"typescript": "^6.0.2",
|
|
25
|
-
"vitest": "^4.1.
|
|
25
|
+
"vitest": "^4.1.4"
|
|
26
26
|
},
|
|
27
27
|
"scripts": {
|
|
28
28
|
"build": "tsup --config ../../tsup.config.ts",
|
package/src/dispatcher-plugin.ts
CHANGED
|
@@ -22,6 +22,75 @@ interface RouteDefinition {
|
|
|
22
22
|
handler: (req: any) => Promise<any>;
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
+
/**
|
|
26
|
+
* Register a single RouteDefinition on the HTTP server.
|
|
27
|
+
* Returns true if the route was successfully registered.
|
|
28
|
+
*/
|
|
29
|
+
function mountRouteOnServer(route: RouteDefinition, server: IHttpServer, routePath: string): boolean {
|
|
30
|
+
const handler = async (req: any, res: any) => {
|
|
31
|
+
try {
|
|
32
|
+
const result = await route.handler({
|
|
33
|
+
body: req.body,
|
|
34
|
+
params: req.params,
|
|
35
|
+
query: req.query,
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
if (result.stream && result.events) {
|
|
39
|
+
// SSE streaming response
|
|
40
|
+
res.status(result.status);
|
|
41
|
+
|
|
42
|
+
// Apply headers from the route result if available
|
|
43
|
+
if (result.headers) {
|
|
44
|
+
for (const [k, v] of Object.entries(result.headers)) {
|
|
45
|
+
res.header(k, String(v));
|
|
46
|
+
}
|
|
47
|
+
} else {
|
|
48
|
+
res.header('Content-Type', 'text/event-stream');
|
|
49
|
+
res.header('Cache-Control', 'no-cache');
|
|
50
|
+
res.header('Connection', 'keep-alive');
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Write the stream — events are pre-encoded SSE strings
|
|
54
|
+
if (typeof res.write === 'function' && typeof res.end === 'function') {
|
|
55
|
+
for await (const event of result.events) {
|
|
56
|
+
res.write(typeof event === 'string' ? event : `data: ${JSON.stringify(event)}\n\n`);
|
|
57
|
+
}
|
|
58
|
+
res.end();
|
|
59
|
+
} else {
|
|
60
|
+
// Fallback: collect events into array
|
|
61
|
+
const events = [];
|
|
62
|
+
for await (const event of result.events) {
|
|
63
|
+
events.push(event);
|
|
64
|
+
}
|
|
65
|
+
res.json({ events });
|
|
66
|
+
}
|
|
67
|
+
} else {
|
|
68
|
+
res.status(result.status);
|
|
69
|
+
if (result.body !== undefined) {
|
|
70
|
+
res.json(result.body);
|
|
71
|
+
} else {
|
|
72
|
+
res.end();
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
} catch (err: any) {
|
|
76
|
+
errorResponse(err, res);
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
const m = route.method.toLowerCase();
|
|
81
|
+
if (m === 'get' && typeof server.get === 'function') {
|
|
82
|
+
server.get(routePath, handler);
|
|
83
|
+
return true;
|
|
84
|
+
} else if (m === 'post' && typeof server.post === 'function') {
|
|
85
|
+
server.post(routePath, handler);
|
|
86
|
+
return true;
|
|
87
|
+
} else if (m === 'delete' && typeof server.delete === 'function') {
|
|
88
|
+
server.delete(routePath, handler);
|
|
89
|
+
return true;
|
|
90
|
+
}
|
|
91
|
+
return false;
|
|
92
|
+
}
|
|
93
|
+
|
|
25
94
|
/**
|
|
26
95
|
* Send an HttpDispatcherResult through IHttpResponse.
|
|
27
96
|
* Differentiates between handled, unhandled (404), and special results.
|
|
@@ -402,68 +471,33 @@ export function createDispatcherPlugin(config: DispatcherPluginConfig = {}): Plu
|
|
|
402
471
|
const routePath = route.path.startsWith('/api/v1')
|
|
403
472
|
? route.path
|
|
404
473
|
: `${prefix}${route.path}`;
|
|
405
|
-
|
|
406
|
-
const handler = async (req: any, res: any) => {
|
|
407
|
-
try {
|
|
408
|
-
const result = await route.handler({
|
|
409
|
-
body: req.body,
|
|
410
|
-
params: req.params,
|
|
411
|
-
query: req.query,
|
|
412
|
-
});
|
|
413
|
-
|
|
414
|
-
if (result.stream && result.events) {
|
|
415
|
-
// SSE streaming response
|
|
416
|
-
res.status(result.status);
|
|
417
|
-
|
|
418
|
-
// Apply headers from the route result if available
|
|
419
|
-
if (result.headers) {
|
|
420
|
-
for (const [k, v] of Object.entries(result.headers)) {
|
|
421
|
-
res.header(k, v);
|
|
422
|
-
}
|
|
423
|
-
} else {
|
|
424
|
-
res.header('Content-Type', 'text/event-stream');
|
|
425
|
-
res.header('Cache-Control', 'no-cache');
|
|
426
|
-
res.header('Connection', 'keep-alive');
|
|
427
|
-
}
|
|
428
|
-
|
|
429
|
-
// Write the stream — events are pre-encoded SSE strings
|
|
430
|
-
if (typeof res.write === 'function' && typeof res.end === 'function') {
|
|
431
|
-
for await (const event of result.events) {
|
|
432
|
-
res.write(typeof event === 'string' ? event : `data: ${JSON.stringify(event)}\n\n`);
|
|
433
|
-
}
|
|
434
|
-
res.end();
|
|
435
|
-
} else {
|
|
436
|
-
// Fallback: collect events into array
|
|
437
|
-
const events = [];
|
|
438
|
-
for await (const event of result.events) {
|
|
439
|
-
events.push(event);
|
|
440
|
-
}
|
|
441
|
-
res.json({ events });
|
|
442
|
-
}
|
|
443
|
-
} else {
|
|
444
|
-
res.status(result.status);
|
|
445
|
-
if (result.body !== undefined) {
|
|
446
|
-
res.json(result.body);
|
|
447
|
-
} else {
|
|
448
|
-
res.end();
|
|
449
|
-
}
|
|
450
|
-
}
|
|
451
|
-
} catch (err: any) {
|
|
452
|
-
errorResponse(err, res);
|
|
453
|
-
}
|
|
454
|
-
};
|
|
455
|
-
|
|
456
|
-
const m = route.method.toLowerCase();
|
|
457
|
-
if (m === 'get' && typeof server.get === 'function') {
|
|
458
|
-
server.get(routePath, handler);
|
|
459
|
-
} else if (m === 'post' && typeof server.post === 'function') {
|
|
460
|
-
server.post(routePath, handler);
|
|
461
|
-
} else if (m === 'delete' && typeof server.delete === 'function') {
|
|
462
|
-
server.delete(routePath, handler);
|
|
463
|
-
}
|
|
474
|
+
mountRouteOnServer(route, server, routePath);
|
|
464
475
|
}
|
|
465
476
|
ctx.logger.info(`[Dispatcher] Registered ${routes.length} AI routes`);
|
|
466
477
|
});
|
|
478
|
+
|
|
479
|
+
// ── Fallback: recover routes cached before hook was registered ──
|
|
480
|
+
// If AIServicePlugin.start() ran before DispatcherPlugin.start()
|
|
481
|
+
// (possible when plugin start order differs from registration order),
|
|
482
|
+
// the 'ai:routes' trigger fires with no listener. The AIServicePlugin
|
|
483
|
+
// caches the routes on the kernel as __aiRoutes (see AIServicePlugin.start())
|
|
484
|
+
// as an internal cross-plugin protocol so we can recover them here.
|
|
485
|
+
// TODO: replace with a formal kernel.getCachedRoutes('ai') API in a future release.
|
|
486
|
+
const cachedRoutes = (kernel as any).__aiRoutes as RouteDefinition[] | undefined;
|
|
487
|
+
if (cachedRoutes && Array.isArray(cachedRoutes) && cachedRoutes.length > 0) {
|
|
488
|
+
let registered = 0;
|
|
489
|
+
for (const route of cachedRoutes) {
|
|
490
|
+
const routePath = route.path.startsWith('/api/v1')
|
|
491
|
+
? route.path
|
|
492
|
+
: `${prefix}${route.path}`;
|
|
493
|
+
if (mountRouteOnServer(route, server, routePath)) {
|
|
494
|
+
registered++;
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
if (registered > 0) {
|
|
498
|
+
ctx.logger.info(`[Dispatcher] Recovered ${registered} cached AI routes (hook timing fallback)`);
|
|
499
|
+
}
|
|
500
|
+
}
|
|
467
501
|
},
|
|
468
502
|
};
|
|
469
503
|
}
|
package/src/http-dispatcher.ts
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import { ObjectKernel, getEnv, resolveLocale } from '@objectstack/core';
|
|
4
4
|
import { CoreServiceName } from '@objectstack/spec/system';
|
|
5
|
+
import { pluralToSingular } from '@objectstack/spec/shared';
|
|
5
6
|
|
|
6
7
|
/** Browser-safe UUID generator — prefers Web Crypto, falls back to RFC 4122 v4 */
|
|
7
8
|
function randomUUID(): string {
|
|
@@ -341,22 +342,82 @@ export class HttpDispatcher {
|
|
|
341
342
|
|
|
342
343
|
// GET /metadata/types
|
|
343
344
|
if (parts[0] === 'types') {
|
|
344
|
-
// Try
|
|
345
|
+
// PRIORITY 1: Try MetadataService directly (includes both typeRegistry with agent/tool AND runtime-registered types)
|
|
346
|
+
console.log('[HttpDispatcher] Attempting to resolve MetadataService...');
|
|
347
|
+
console.log('[HttpDispatcher] Available kernel methods:', {
|
|
348
|
+
hasGetServiceAsync: typeof this.kernel.getServiceAsync === 'function',
|
|
349
|
+
hasGetService: typeof this.kernel.getService === 'function',
|
|
350
|
+
hasContext: !!this.kernel.context,
|
|
351
|
+
hasContextGetService: typeof this.kernel.context?.getService === 'function',
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
// Try all service resolution paths with detailed logging
|
|
355
|
+
let metadataService: any = null;
|
|
356
|
+
|
|
357
|
+
// Path 1: kernel.getServiceAsync
|
|
358
|
+
if (typeof this.kernel.getServiceAsync === 'function') {
|
|
359
|
+
try {
|
|
360
|
+
metadataService = await this.kernel.getServiceAsync('metadata');
|
|
361
|
+
console.log('[HttpDispatcher] kernel.getServiceAsync("metadata") returned:', !!metadataService);
|
|
362
|
+
} catch (e: any) {
|
|
363
|
+
console.log('[HttpDispatcher] kernel.getServiceAsync("metadata") failed:', e.message);
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
// Path 2: kernel.getService (if not found via async)
|
|
368
|
+
if (!metadataService && typeof this.kernel.getService === 'function') {
|
|
369
|
+
try {
|
|
370
|
+
metadataService = await this.kernel.getService('metadata');
|
|
371
|
+
console.log('[HttpDispatcher] kernel.getService("metadata") returned:', !!metadataService);
|
|
372
|
+
} catch (e: any) {
|
|
373
|
+
console.log('[HttpDispatcher] kernel.getService("metadata") failed:', e.message);
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
// Path 3: kernel.context.getService (if not found)
|
|
378
|
+
if (!metadataService && this.kernel.context?.getService) {
|
|
379
|
+
try {
|
|
380
|
+
metadataService = await this.kernel.context.getService('metadata');
|
|
381
|
+
console.log('[HttpDispatcher] kernel.context.getService("metadata") returned:', !!metadataService);
|
|
382
|
+
} catch (e: any) {
|
|
383
|
+
console.log('[HttpDispatcher] kernel.context.getService("metadata") failed:', e.message);
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
console.log('[HttpDispatcher] Final metadataService:', !!metadataService, 'has getRegisteredTypes:', typeof (metadataService as any)?.getRegisteredTypes);
|
|
388
|
+
|
|
389
|
+
if (metadataService && typeof (metadataService as any).getRegisteredTypes === 'function') {
|
|
390
|
+
try {
|
|
391
|
+
const types = await (metadataService as any).getRegisteredTypes();
|
|
392
|
+
console.log('[HttpDispatcher] MetadataService.getRegisteredTypes() returned:', types);
|
|
393
|
+
return { handled: true, response: this.success({ types }) };
|
|
394
|
+
} catch (e: any) {
|
|
395
|
+
// Log error but continue to fallbacks
|
|
396
|
+
console.warn('[HttpDispatcher] MetadataService.getRegisteredTypes() failed:', e.message, e.stack);
|
|
397
|
+
}
|
|
398
|
+
} else {
|
|
399
|
+
console.log('[HttpDispatcher] MetadataService not available or missing getRegisteredTypes, falling back to protocol service');
|
|
400
|
+
}
|
|
401
|
+
// PRIORITY 2: Try protocol service (returns SchemaRegistry types only - missing agent/tool)
|
|
345
402
|
const protocol = await this.resolveService('protocol');
|
|
346
403
|
if (protocol && typeof protocol.getMetaTypes === 'function') {
|
|
347
404
|
const result = await protocol.getMetaTypes({});
|
|
405
|
+
console.log('[HttpDispatcher] Protocol service returned types:', result);
|
|
348
406
|
return { handled: true, response: this.success(result) };
|
|
349
407
|
}
|
|
350
|
-
//
|
|
408
|
+
// PRIORITY 3: ask broker for registered types
|
|
351
409
|
if (broker) {
|
|
352
410
|
try {
|
|
353
411
|
const data = await broker.call('metadata.types', {}, { request: context.request });
|
|
412
|
+
console.log('[HttpDispatcher] Broker returned types:', data);
|
|
354
413
|
return { handled: true, response: this.success(data) };
|
|
355
|
-
} catch {
|
|
414
|
+
} catch (e) {
|
|
415
|
+
console.log('[HttpDispatcher] Broker call failed:', e);
|
|
356
416
|
// fall through to hardcoded defaults
|
|
357
417
|
}
|
|
358
418
|
}
|
|
359
419
|
// Last resort: hardcoded defaults
|
|
420
|
+
console.warn('[HttpDispatcher] Falling back to hardcoded defaults for metadata types');
|
|
360
421
|
return { handled: true, response: this.success({ types: ['object', 'app', 'plugin'] }) };
|
|
361
422
|
}
|
|
362
423
|
|
|
@@ -429,9 +490,8 @@ export class HttpDispatcher {
|
|
|
429
490
|
return { handled: true, response: this.error('Not found', 404) };
|
|
430
491
|
}
|
|
431
492
|
|
|
432
|
-
//
|
|
433
|
-
|
|
434
|
-
const singularType = type.endsWith('s') ? type.slice(0, -1) : type;
|
|
493
|
+
// Normalize plural URL paths to singular registry type names
|
|
494
|
+
const singularType = pluralToSingular(type);
|
|
435
495
|
|
|
436
496
|
// Try Protocol Service First (Preferred)
|
|
437
497
|
const protocol = await this.resolveService('protocol');
|
|
@@ -464,7 +524,7 @@ export class HttpDispatcher {
|
|
|
464
524
|
const typeOrName = parts[0];
|
|
465
525
|
// Extract optional package filter from query string
|
|
466
526
|
const packageId = query?.package || undefined;
|
|
467
|
-
|
|
527
|
+
|
|
468
528
|
// Try protocol service first for any type
|
|
469
529
|
const protocol = await this.resolveService('protocol');
|
|
470
530
|
if (protocol && typeof protocol.getMetaItems === 'function') {
|
|
@@ -479,6 +539,22 @@ export class HttpDispatcher {
|
|
|
479
539
|
}
|
|
480
540
|
}
|
|
481
541
|
|
|
542
|
+
// Try MetadataService directly for runtime-registered metadata (agents, tools, etc.)
|
|
543
|
+
const metadataService = await this.getService(CoreServiceName.enum.metadata);
|
|
544
|
+
if (metadataService && typeof (metadataService as any).list === 'function') {
|
|
545
|
+
try {
|
|
546
|
+
const items = await (metadataService as any).list(typeOrName);
|
|
547
|
+
if (items && items.length > 0) {
|
|
548
|
+
return { handled: true, response: this.success({ type: typeOrName, items }) };
|
|
549
|
+
}
|
|
550
|
+
} catch (e: any) {
|
|
551
|
+
// MetadataService doesn't know this type or failed, continue to other fallbacks
|
|
552
|
+
// Sanitize typeOrName to prevent log injection (CodeQL warning)
|
|
553
|
+
const sanitizedType = String(typeOrName).replace(/[\r\n\t]/g, '');
|
|
554
|
+
console.debug(`[HttpDispatcher] MetadataService.list() failed for type:`, sanitizedType, 'error:', e.message);
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
|
|
482
558
|
// Try broker for the type
|
|
483
559
|
if (broker) {
|
|
484
560
|
try {
|
package/vitest.config.ts
CHANGED
|
@@ -12,6 +12,7 @@ export default defineConfig({
|
|
|
12
12
|
'@objectstack/spec/contracts': path.resolve(__dirname, '../spec/src/contracts/index.ts'),
|
|
13
13
|
'@objectstack/spec/data': path.resolve(__dirname, '../spec/src/data/index.ts'),
|
|
14
14
|
'@objectstack/spec/kernel': path.resolve(__dirname, '../spec/src/kernel/index.ts'),
|
|
15
|
+
'@objectstack/spec/shared': path.resolve(__dirname, '../spec/src/shared/index.ts'),
|
|
15
16
|
'@objectstack/spec/system': path.resolve(__dirname, '../spec/src/system/index.ts'),
|
|
16
17
|
'@objectstack/spec': path.resolve(__dirname, '../spec/src/index.ts'),
|
|
17
18
|
'@objectstack/types': path.resolve(__dirname, '../types/src/index.ts'),
|