Skip to content

Commit e4353a3

Browse files
authored
fix(wave1): address augment review feedback
1 parent fc11322 commit e4353a3

7 files changed

Lines changed: 173 additions & 14 deletions

File tree

packages/config/mod.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@
4141
*
4242
* const config = await initConfig();
4343
* const report = inspectConfig(config);
44-
* await publishConfigSummary(report.summary);
44+
* const summary = report.summary;
4545
* ```
4646
*
4747
* @see README.md

packages/config/src/diagnostics/inspect-config.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ export interface InspectionReport {
2323
* import { inspectConfig } from "@netscript/config";
2424
*
2525
* const report = inspectConfig({ name: "app", version: "1.0.0" });
26-
* await publishConfigSummary(report.summary);
26+
* const summary = report.summary;
2727
* ```
2828
*/
2929
export function inspectConfig(target: Partial<NetScriptConfig> | string): InspectionReport {

packages/config/src/domain/schemas/netscript-config-schema.ts

Lines changed: 95 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,4 @@
11
import { z } from 'zod';
2-
import type { SagasConfig, TriggersConfig } from '../../../types.ts';
3-
42
import { AppConfigSchema } from './app-schema.ts';
53

64
import { AspireConfigSchema } from './aspire-schema.ts';
@@ -21,15 +19,104 @@ import { SdkConfigSchema } from './sdk-schema.ts';
2119

2220
import { ServiceConfigSchema } from './service-schema.ts';
2321

22+
const SagaRetryConfigSchema = z.object({
23+
maxAttempts: z.number(),
24+
initialDelay: z.number(),
25+
maxDelay: z.number(),
26+
backoffMultiplier: z.number(),
27+
jitter: z.boolean(),
28+
});
29+
30+
const SagaTimeoutConfigSchema = z.object({
31+
completionTimeout: z.number().optional(),
32+
minTimeout: z.number(),
33+
maxTimeout: z.number(),
34+
});
35+
36+
const SagaDefinitionSchema = z.object({
37+
id: z.string(),
38+
topic: z.string().optional(),
39+
name: z.string(),
40+
description: z.string().optional(),
41+
entrypoint: z.string(),
42+
enabled: z.boolean().default(true),
43+
retry: SagaRetryConfigSchema.optional(),
44+
timeout: SagaTimeoutConfigSchema.optional(),
45+
tags: z.array(z.string()).optional(),
46+
metadata: z.record(z.string(), z.unknown()).optional(),
47+
});
48+
49+
const SagaScalingConfigSchema = z.object({
50+
concurrency: z.number(),
51+
mode: z.enum(['combined', 'distributed']),
52+
});
53+
54+
const SagaRetentionConfigSchema = z.object({
55+
activeDays: z.number(),
56+
completedDays: z.number(),
57+
archiveToDb: z.boolean(),
58+
});
59+
60+
const SagaGroupSchema = z.object({
61+
topic: z.string(),
62+
scaling: SagaScalingConfigSchema.optional(),
63+
retention: SagaRetentionConfigSchema.optional(),
64+
sagas: z.array(SagaDefinitionSchema).default([]),
65+
});
66+
2467
const SagasConfigSectionSchema = z
25-
.unknown()
26-
.optional()
27-
.transform((value) => value as SagasConfig | undefined);
68+
.object({
69+
sagasDir: z.string().default('sagas'),
70+
transportProvider: z.enum(['auto', 'redis', 'rabbitmq', 'inmemory']).default('auto'),
71+
storeProvider: z.enum(['auto', 'redis', 'postgres', 'inmemory']).default('auto'),
72+
concurrency: z.number().default(1),
73+
retry: SagaRetryConfigSchema.optional(),
74+
timeout: SagaTimeoutConfigSchema.optional(),
75+
sagas: z.array(SagaDefinitionSchema).default([]),
76+
groups: z.array(SagaGroupSchema).default([]),
77+
enabled: z.boolean().default(true),
78+
})
79+
.optional();
80+
81+
const TriggerDefinitionSchema = z.object({
82+
id: z.string(),
83+
name: z.string(),
84+
type: z.enum(['file', 'webhook', 'cron', 'manual']).default('webhook'),
85+
enabled: z.boolean().default(true),
86+
entrypoint: z.string().optional(),
87+
tags: z.array(z.string()).default([]),
88+
});
89+
90+
const TriggerScalingConfigSchema = z.object({
91+
concurrency: z.number(),
92+
});
93+
94+
const TriggerRetentionConfigSchema = z.object({
95+
kvDays: z.number(),
96+
dbDays: z.number(),
97+
});
98+
99+
const TriggerGroupSchema = z.object({
100+
topic: z.string(),
101+
scaling: TriggerScalingConfigSchema.default({ concurrency: 10 }),
102+
retention: TriggerRetentionConfigSchema.default({ kvDays: 7, dbDays: 90 }),
103+
triggers: z.array(TriggerDefinitionSchema).default([]),
104+
});
105+
106+
const WebhookConfigSchema = z.object({
107+
enabled: z.boolean().default(false),
108+
basePath: z.string().default('/api/v1/webhooks'),
109+
rateLimitPerMinute: z.number().default(60),
110+
});
28111

29112
const TriggersConfigSectionSchema = z
30-
.unknown()
31-
.optional()
32-
.transform((value) => value as TriggersConfig | undefined);
113+
.object({
114+
triggersDir: z.string().default('triggers'),
115+
groups: z.array(TriggerGroupSchema).default([]),
116+
webhooks: WebhookConfigSchema.optional(),
117+
enabled: z.boolean().default(true),
118+
})
119+
.optional();
33120

34121
/**
35122
* Main NetScript configuration schema.
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import { assertEquals, assertThrows } from 'jsr:@std/assert@^1';
2+
import { defineConfig } from '../../mod.ts';
3+
4+
Deno.test('defineConfig: applies defaults to validated saga and trigger sections', () => {
5+
const config = defineConfig({
6+
name: 'orders',
7+
databases: { config: [] },
8+
sagas: {
9+
groups: [{ topic: 'orders' }],
10+
},
11+
triggers: {
12+
groups: [{ topic: 'events' }],
13+
},
14+
});
15+
16+
assertEquals(config.sagas?.sagasDir, 'sagas');
17+
assertEquals(config.sagas?.transportProvider, 'auto');
18+
assertEquals(config.sagas?.groups[0]?.sagas, []);
19+
assertEquals(config.triggers?.triggersDir, 'triggers');
20+
assertEquals(config.triggers?.groups[0]?.scaling.concurrency, 10);
21+
assertEquals(config.triggers?.enabled, true);
22+
});
23+
24+
Deno.test('defineConfig: rejects unrelated saga and trigger section shapes', () => {
25+
assertThrows(() =>
26+
defineConfig(
27+
{
28+
name: 'orders',
29+
databases: { config: [] },
30+
sagas: ['not-a-saga-section'],
31+
} as unknown as Parameters<typeof defineConfig>[0],
32+
)
33+
);
34+
35+
assertThrows(() =>
36+
defineConfig(
37+
{
38+
name: 'orders',
39+
databases: { config: [] },
40+
triggers: ['not-a-trigger-section'],
41+
} as unknown as Parameters<typeof defineConfig>[0],
42+
)
43+
);
44+
});

packages/runtime-config/src/application/loader.ts

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -53,8 +53,8 @@ async function readPointerFile(path: string): Promise<VersionPointer | null> {
5353
if (!text) return null;
5454

5555
try {
56-
const parsed = JSON.parse(text) as VersionPointer;
57-
if (parsed && typeof parsed === 'object') {
56+
const parsed = JSON.parse(text) as unknown;
57+
if (isVersionPointer(parsed)) {
5858
return parsed;
5959
}
6060
} catch {
@@ -77,6 +77,15 @@ async function readPointerFile(path: string): Promise<VersionPointer | null> {
7777
};
7878
}
7979

80+
function isVersionPointer(value: unknown): value is VersionPointer {
81+
if (!value || typeof value !== 'object' || Array.isArray(value)) return false;
82+
83+
const pointer = value as Record<string, unknown>;
84+
return ['version', 'jobs', 'sagas', 'tasks', 'triggers', 'features'].every((key) =>
85+
pointer[key] === undefined || typeof pointer[key] === 'string'
86+
);
87+
}
88+
8089
/**
8190
* Load runtime overrides from the configured runtime config directory.
8291
*

packages/runtime-config/src/application/watcher.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,11 +38,11 @@ export function watchRuntimeConfig(
3838

3939
debounceTimer = setTimeout(async () => {
4040
debounceTimer = null;
41-
const config = await loadRuntimeConfig();
4241
try {
42+
const config = await loadRuntimeConfig();
4343
await onChange(config);
4444
} catch {
45-
// Watch callbacks are owned by the consumer; presentation code decides how to report.
45+
// Watch reloads and callbacks are owned by the consumer; presentation code decides how to report.
4646
}
4747
}, DEFAULT_DEBOUNCE_MS);
4848
}

packages/runtime-config/tests/loader_test.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,25 @@ Deno.test('loadRuntimeConfig: derives conventional topic files from plain pointe
7171
});
7272
});
7373

74+
Deno.test('loadRuntimeConfig: returns empty defaults when JSON pointer paths are malformed', async () => {
75+
await withRuntimeDir(async (dir) => {
76+
await writeJson(join(dir, 'current'), {
77+
version: '1.0.0',
78+
jobs: 123,
79+
});
80+
81+
const config = await loadRuntimeConfig();
82+
83+
assertEquals(config, {
84+
jobs: [],
85+
sagas: [],
86+
triggers: [],
87+
features: [],
88+
tasks: [],
89+
});
90+
});
91+
});
92+
7493
async function withRuntimeDir(test: (dir: string) => Promise<void>): Promise<void> {
7594
const previous = Deno.env.get('NETSCRIPT_RUNTIME_CONFIG_DIR');
7695
const dir = await Deno.makeTempDir();

0 commit comments

Comments
 (0)