Services are registered in containerConfig.ts:
import { registerDependencies } from '@common/dependencyRegistration';
import { SERVICES } from '@common/constants';
const dependencies: InjectionObject<unknown>[] = [
{ token: SERVICES.CONFIG, provider: { useValue: configInstance } },
{ token: SERVICES.LOGGER, provider: { useValue: logger } },
{
token: SERVICES.MY_SERVICE,
provider: {
useFactory: instancePerContainerCachingFactory((container) => {
// Factory function with access to container
return new MyService(container.resolve(SERVICES.LOGGER));
}),
},
},
];
return registerDependencies(dependencies);import { injectable, inject } from 'tsyringe';
import type { Logger } from '@map-colonies/js-logger';
import { SERVICES } from '@common/constants';
@injectable()
export class CleanerService {
public constructor(
@inject(SERVICES.LOGGER) private readonly logger: Logger,
@inject(SERVICES.CONFIG) private readonly config: ConfigType
) {}
public async cleanup(resourceId: string): Promise<void> {
this.logger.info({ msg: 'Starting cleanup', resourceId });
// Implementation
}
}In common/constants.ts:
export const SERVICES = {
CONFIG: Symbol('Config'),
LOGGER: Symbol('Logger'),
TRACER: Symbol('Tracer'),
METRICS: Symbol('Metrics'),
JOBNIK_SDK: Symbol('JobnikSDK'),
WORKER: Symbol('Worker'),
// Add new tokens here
CLEANER_SERVICE: Symbol('CleanerService'),
} satisfies Record<string, symbol>;Note: Strategy classes are registered using their task type string directly (e.g., 'tiles-deletion') rather than Symbol tokens. This eliminates the need for an additional mapping layer.
import type { Task, TaskHandlerContext } from '@map-colonies/jobnik-sdk';
import { injectable } from 'tsyringe';
import type { CleanerJobTypes, CleanerStageTypes } from './types';
@injectable()
export class CleanerManager {
public async handleCleanupTask(
task: Task<CleanerStageTypes['cleanup']['task']>,
context: TaskHandlerContext<CleanerJobTypes, CleanerStageTypes, 'cleanerJob', 'cleanup'>
): Promise<void> {
const { logger, job } = context;
logger.info({ msg: 'Processing cleanup task', taskId: task.id });
// Access task data
const { resourcePath, expirationDate } = task.data;
// Access job metadata
const jobMetadata = job.userMetadata;
// Do work...
// Update stage metadata on success
await context.updateStageUserMetadata({
cleanedAt: new Date().toISOString(),
});
}
}import type { IJobnikSDK } from '@map-colonies/jobnik-sdk';
export interface CleanerJobTypes {
cleanerJob: {
data: {
targetPath: string;
retentionDays: number;
};
userMetadata: {
initiatedBy: string;
scheduledAt: string;
};
};
}
export interface CleanerStageTypes {
cleanup: {
data: { batchSize: number };
userMetadata: { cleanedAt?: string };
task: {
data: { resourcePath: string; expirationDate: string };
userMetadata: { status?: string };
};
};
}
export type CleanerSDK = IJobnikSDK<CleanerJobTypes, CleanerStageTypes>;export const workerBuilder: FactoryFunction<IWorker> = (container) => {
const sdk = container.resolve<CleanerSDK>(SERVICES.JOBNIK_SDK);
const config = container.resolve<ConfigType>(SERVICES.CONFIG);
const manager = container.resolve(CleanerManager);
const worker = sdk.createWorker<'cleanerJob', 'cleanup'>('cleanup', manager.handleCleanupTask.bind(manager), config.get('jobnik.worker'));
worker.on('error', (err) => {
const logger = container.resolve<Logger>(SERVICES.LOGGER);
logger.error({ msg: 'Worker error', err });
});
return worker;
};@injectable()
export class MyService {
public constructor(@inject(SERVICES.CONFIG) private readonly config: ConfigType) {}
public getServerPort(): number {
return this.config.get('server.port');
}
}config/
default.json # Base config (always loaded)
development.json # Merged when NODE_ENV=development
production.json # Merged when NODE_ENV=production
test.json # Merged when NODE_ENV=test
local.json # Local overrides (gitignored)
custom-environment-variables.json # Environment variable mappings
helm/
values.yaml # Helm chart default values
local.yaml # Local Helm overrides (gitignored)
templates/configmap.yaml # ConfigMap template
IMPORTANT: When adding new configuration values, you MUST update ALL configuration files to maintain sync across all deployment levels:
config/default.json- Add the configuration with default valuesconfig/custom-environment-variables.json- Add environment variable mappingshelm/values.yaml- Add Helm chart valueshelm/values-local.yaml- Add local development values (if needed)helm/templates/configmap.yaml- Add to ConfigMap template
Example:
Adding a new queue.jobManagerBaseUrl configuration:
1. config/default.json
{
"queue": {
"jobManagerBaseUrl": "http//localhost:8080"
}
}2. config/custom-environment-variables.json
{
"queue": {
"jobManagerBaseUrl": "QUEUE_JOB_MANAGER_BASE_URL"
}
}3. helm/values.yaml
env:
queue:
jobManagerBaseUrl: 'http//localhost:8080'4. helm/local.yaml (for local development)
env:
queue:
jobManagerBaseUrl: 'http://localhost:8080'5. helm/templates/configmap.yaml
data:
QUEUE_JOB_MANAGER_BASE_URL: { { .Values.env.queue.jobManagerBaseUrl | quote } }This ensures configuration works correctly in:
- Local development (default.json, local.json)
- CI/CD environments (environment variables)
- Kubernetes deployments (Helm charts)
Throwing an error automatically fails the task:
public async handleTask(task: Task<...>, context: ...): Promise<void> {
if (!task.data.resourcePath) {
throw new Error('Resource path is required');
}
try {
await this.processResource(task.data.resourcePath);
} catch (error) {
context.logger.error({ msg: 'Processing failed', error });
throw error; // Task will be marked as failed
}
}// Good - structured context
logger.info({ msg: 'Task started', taskId: task.id, resourcePath });
logger.error({ msg: 'Task failed', error, taskId: task.id });
// Avoid - string concatenation
logger.info(`Task ${task.id} started`);The onSignal handler in containerConfig.ts handles cleanup:
{
token: 'onSignal',
provider: {
useFactory: (container) => {
const worker = container.resolve<IWorker>(SERVICES.WORKER);
return async (): Promise<void> => {
await Promise.all([
getTracing().stop(),
worker.stop(),
// Add additional cleanup here
]);
};
},
},
}