From c622b8bd21519927dcde628c90628663c5a88af4 Mon Sep 17 00:00:00 2001 From: Aboyeji-Isaac <141873116+Aboyeji-Isaac@users.noreply.github.com> Date: Tue, 27 Oct 2026 10:53:34 +0100 Subject: [PATCH 01/12] feat(flags): add validated typed feature flag registry --- src/config/flags.ts | 73 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 73 insertions(+) create mode 100644 src/config/flags.ts diff --git a/src/config/flags.ts b/src/config/flags.ts new file mode 100644 index 0000000..199ad34 --- /dev/null +++ b/src/config/flags.ts @@ -0,0 +1,73 @@ +import env from './env'; + +export const FEATURE_FLAG_DEFAULTS = { + webhookBatching: true, + deliveryPooling: true, + chat: true, + milestones: false, +} as const; + +export type FeatureFlag = keyof typeof FEATURE_FLAG_DEFAULTS; +export type FeatureFlagValues = Record; +export type FeatureFlagOverrides = Partial>; + +/** + * Parse a comma-separated list of feature names or name=value pairs. + * A bare name enables that flag; an explicit value must be true or false. + * Unknown names and malformed values fail fast during application startup. + */ +export const parseFeatureFlags = ( + input: string, + overrides: FeatureFlagOverrides = {}, +): FeatureFlagValues => { + const flags: FeatureFlagValues = { ...FEATURE_FLAG_DEFAULTS }; + + if (input.trim()) { + for (const entry of input.split(',')) { + const token = entry.trim(); + if (!token) continue; + + const parts = token.split('='); + if (parts.length > 2) { + throw new Error('Invalid FEATURE_FLAGS entry: ' + token); + } + + const name = parts[0].trim(); + if (!Object.prototype.hasOwnProperty.call(FEATURE_FLAG_DEFAULTS, name)) { + throw new Error('Unknown feature flag: ' + name); + } + + let enabled = true; + if (parts.length === 2) { + const value = parts[1].trim().toLowerCase(); + if (value !== 'true' && value !== 'false') { + throw new Error('Invalid boolean for feature flag ' + name + ': ' + parts[1]); + } + enabled = value === 'true'; + } + + flags[name as FeatureFlag] = enabled; + } + } + + for (const [name, value] of Object.entries(overrides) as Array<[FeatureFlag, boolean | undefined]>) { + if (value !== undefined) { + flags[name] = value; + } + } + + return flags; +}; + +const featureFlags = parseFeatureFlags(env.FEATURE_FLAGS, { + webhookBatching: env.FEATURE_WEBHOOK_BATCHING_ENABLED, + deliveryPooling: env.FEATURE_DELIVERY_POOLING_ENABLED, + chat: env.FEATURE_CHAT_ENABLED, + milestones: env.FEATURE_MILESTONES_ENABLED, +}); + +/** Return whether a known optional feature is enabled in this process. */ +export const isEnabled = (flag: FeatureFlag): boolean => featureFlags[flag]; + +/** Return a defensive copy of the resolved feature-flag registry. */ +export const getFeatureFlags = (): FeatureFlagValues => ({ ...featureFlags }); From 1da940ecca8bcfacc92e3af658a42e26d04825ab Mon Sep 17 00:00:00 2001 From: Aboyeji-Isaac <141873116+Aboyeji-Isaac@users.noreply.github.com> Date: Tue, 27 Oct 2026 10:53:44 +0100 Subject: [PATCH 02/12] feat(flags): validate feature flag environment variables --- src/config/env.ts | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/src/config/env.ts b/src/config/env.ts index a542efb..b5320de 100644 --- a/src/config/env.ts +++ b/src/config/env.ts @@ -18,6 +18,11 @@ interface EnvConfig { CORS_ORIGIN: string; RATE_LIMIT_WINDOW_MS: number; RATE_LIMIT_MAX_REQUESTS: number; + FEATURE_FLAGS: string; + FEATURE_WEBHOOK_BATCHING_ENABLED?: boolean; + FEATURE_DELIVERY_POOLING_ENABLED?: boolean; + FEATURE_CHAT_ENABLED?: boolean; + FEATURE_MILESTONES_ENABLED?: boolean; DISPUTE_NOTIFICATION_WEBHOOK_URL: string; UPLOAD_STORAGE_DRIVER: string; UPLOAD_LOCAL_DIR: string; @@ -230,6 +235,23 @@ const envSchema = z.object({ RATE_LIMIT_WINDOW_MS: numeric(z.coerce.number().int().min(1000).default(900000)), RATE_LIMIT_MAX_REQUESTS: numeric(z.coerce.number().int().min(1).default(100)), DISPUTE_NOTIFICATION_WEBHOOK_URL: z.string().default(''), + FEATURE_FLAGS: z.string().trim().default(''), + FEATURE_WEBHOOK_BATCHING_ENABLED: z.preprocess( + blankToUndefined, + z.enum(['true', 'false']).transform((value) => value === 'true').optional(), + ), + FEATURE_DELIVERY_POOLING_ENABLED: z.preprocess( + blankToUndefined, + z.enum(['true', 'false']).transform((value) => value === 'true').optional(), + ), + FEATURE_CHAT_ENABLED: z.preprocess( + blankToUndefined, + z.enum(['true', 'false']).transform((value) => value === 'true').optional(), + ), + FEATURE_MILESTONES_ENABLED: z.preprocess( + blankToUndefined, + z.enum(['true', 'false']).transform((value) => value === 'true').optional(), + ), // ── Uploads / storage ────────────────────────────────────────────────────── UPLOAD_STORAGE_DRIVER: z.enum(['local', 's3']).default('local'), From 2ebd28773cbc91df93ef34ac4e91255a6579bfbe Mon Sep 17 00:00:00 2001 From: Aboyeji-Isaac <141873116+Aboyeji-Isaac@users.noreply.github.com> Date: Tue, 27 Oct 2026 10:53:53 +0100 Subject: [PATCH 03/12] docs(flags): document feature flag environment options --- .env.example | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/.env.example b/.env.example index 3f957cc..f1b35f1 100644 --- a/.env.example +++ b/.env.example @@ -324,6 +324,21 @@ SHUTDOWN_TIMEOUT_MS=30000 # silly. [default: info] LOG_LEVEL=info +# ─── Optional feature flags ────────────────────────────────────────────────── + +# Comma-separated flag names or name=true/false pairs. Bare names enable a +# flag; explicit values allow an individual flag to be disabled. Unknown names +# or invalid boolean values fail application startup. Defaults preserve the +# existing webhook retry, delivery pooling, and chat behavior; milestones are +# disabled because no milestone service is currently implemented. +FEATURE_FLAGS= + +# Optional per-flag overrides. Leave blank to use FEATURE_FLAGS/registry defaults. +FEATURE_WEBHOOK_BATCHING_ENABLED= +FEATURE_DELIVERY_POOLING_ENABLED= +FEATURE_CHAT_ENABLED= +FEATURE_MILESTONES_ENABLED= + # ─── Notes on variables that are NOT part of the app schema ────────────────── # JEST_WORKER_ID is read by Jest itself to detect the test runner, and is From e1661bf46d43775c8add547a0a3111684aec0fab Mon Sep 17 00:00:00 2001 From: Aboyeji-Isaac <141873116+Aboyeji-Isaac@users.noreply.github.com> Date: Tue, 27 Oct 2026 10:54:06 +0100 Subject: [PATCH 04/12] feat(flags): gate webhook retry batching --- src/services/webhookService.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/services/webhookService.ts b/src/services/webhookService.ts index 39b5073..27d8033 100644 --- a/src/services/webhookService.ts +++ b/src/services/webhookService.ts @@ -41,6 +41,7 @@ import { import { IDelivery, DeliveryStatus } from '../models/Delivery'; import AppError from '../utils/AppError'; import env from '../config/env'; +import { isEnabled } from '../config/flags'; import logger from '../config/logger'; // ─── DTOs ────────────────────────────────────────────────────────────────────── @@ -332,6 +333,11 @@ export class WebhookService { * @returns Number of attempts processed in this sweep. */ async retryDueAttempts(): Promise { + if (!isEnabled('webhookBatching')) { + logger.debug('[WebhookService] Webhook retry batching is disabled by feature flag.'); + return 0; + } + const due = await WebhookDeliveryAttempt.find({ status: WebhookDeliveryStatus.FAILED, nextRetryAt: { $lte: new Date() }, From a0320ed1565640aefb8d1dbfb5d684c465413834 Mon Sep 17 00:00:00 2001 From: Aboyeji-Isaac <141873116+Aboyeji-Isaac@users.noreply.github.com> Date: Tue, 27 Oct 2026 10:54:10 +0100 Subject: [PATCH 05/12] feat(flags): gate delivery pooling and assignment --- src/services/poolingService.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/services/poolingService.ts b/src/services/poolingService.ts index 83c0a10..da30ca9 100644 --- a/src/services/poolingService.ts +++ b/src/services/poolingService.ts @@ -19,6 +19,7 @@ import { encodeGeohash } from '../utils/geohash'; import AppError from '../utils/AppError'; import logger from '../config/logger'; import env from '../config/env'; +import { isEnabled } from '../config/flags'; // ─── Types ──────────────────────────────────────────────────────────────────── @@ -72,6 +73,11 @@ export class PoolingService { * Find pending/funded deliveries and group them into proximity-based pools. */ async createPools(options: PoolingOptions = {}): Promise { + if (!isEnabled('deliveryPooling')) { + logger.debug('[PoolingService] Delivery pooling is disabled by feature flag.'); + return []; + } + const proximity = options.maxProximityMeters ?? this.DEFAULT_PROXIMITY_M; const maxSize = options.maxPoolSize ?? this.DEFAULT_MAX_POOL_SIZE; const statuses = options.statuses ?? [DeliveryStatus.PENDING, DeliveryStatus.FUNDED]; @@ -150,6 +156,10 @@ export class PoolingService { * deliveries in the pool. */ async assignPoolToNearestDriver(pool: DeliveryPool): Promise { + if (!isEnabled('deliveryPooling')) { + return { assigned: false, reason: 'Delivery pooling is disabled by feature flag' }; + } + if (pool.deliveries.length === 0) { return { assigned: false, reason: 'Empty pool' }; } From f79b734e85c5d27679fc6e476ddfc9520d67ac70 Mon Sep 17 00:00:00 2001 From: Aboyeji-Isaac <141873116+Aboyeji-Isaac@users.noreply.github.com> Date: Tue, 27 Oct 2026 10:54:21 +0100 Subject: [PATCH 06/12] feat(flags): gate chat persistence when disabled --- src/sockets/chatMessage.service.ts | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/sockets/chatMessage.service.ts b/src/sockets/chatMessage.service.ts index 4a04574..dab8394 100644 --- a/src/sockets/chatMessage.service.ts +++ b/src/sockets/chatMessage.service.ts @@ -1,4 +1,5 @@ import { IChatMessage } from '../models/ChatMessage'; +import { isEnabled } from '../config/flags'; import { ChatMessageRepository, chatMessageRepository, @@ -30,6 +31,14 @@ export class InvalidChatMessageError extends Error { } } +/** Raised when chat is disabled and a caller attempts to persist a message. */ +export class ChatFeatureDisabledError extends Error { + constructor() { + super('Chat is disabled by feature flag'); + this.name = 'ChatFeatureDisabledError'; + } +} + /** * Chat business logic, isolated from Socket.IO. * @@ -49,6 +58,8 @@ export class ChatMessageService { * every caller gets the same ordering. */ async getRecentTranscript(limit: number = RECENT_MESSAGE_LIMIT): Promise { + if (!isEnabled('chat')) return []; + const recent = await this.messages.findRecent(limit); return recent.reverse(); } @@ -102,6 +113,8 @@ export class ChatMessageService { * @throws {InvalidChatMessageError} If the payload is unusable. */ async createMessage(payload: IncomingChatMessage): Promise { + if (!isEnabled('chat')) throw new ChatFeatureDisabledError(); + const validated = this.validate(payload); return this.messages.create(validated as Partial); } From 70dd20149bb73def1e766a7f083c95835542ab02 Mon Sep 17 00:00:00 2001 From: Aboyeji-Isaac <141873116+Aboyeji-Isaac@users.noreply.github.com> Date: Tue, 27 Oct 2026 10:54:25 +0100 Subject: [PATCH 07/12] feat(flags): stop chat socket handlers when disabled --- src/sockets/socketService.ts | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/sockets/socketService.ts b/src/sockets/socketService.ts index 8e5153a..3866a76 100644 --- a/src/sockets/socketService.ts +++ b/src/sockets/socketService.ts @@ -1,9 +1,11 @@ import { Namespace, Socket } from 'socket.io'; import { IChatMessage } from '../models/ChatMessage'; import logger from '../config/logger'; +import { isEnabled } from '../config/flags'; import { ChatMessageService, IncomingChatMessage, + ChatFeatureDisabledError, InvalidChatMessageError, chatMessageService, } from './chatMessage.service'; @@ -33,6 +35,8 @@ export class SocketService { * client's failed backlog must not tear down the connection handler. */ public async handleConnection(socket: Socket, _nsp: Namespace): Promise { + if (!isEnabled('chat')) return; + try { const recent = await this.chat.getRecentTranscript(); socket.emit('recentMessages', recent); @@ -56,10 +60,20 @@ export class SocketService { payload: IncomingChatMessage, socket?: Socket, ): Promise { + if (!isEnabled('chat')) { + socket?.emit('error', { message: 'Chat is disabled' }); + return; + } + try { const message = await this.chat.createMessage(payload); nsp.emit('message', message); } catch (error) { + if (error instanceof ChatFeatureDisabledError) { + socket?.emit('error', { message: 'Chat is disabled' }); + return; + } + if (error instanceof InvalidChatMessageError) { logger.warn(`[SocketService] Rejected invalid message: ${error.message}`); socket?.emit('error', { message: error.message }); From fc562baa43b8ec58e5ce4d90a43e6d5e9f36be9d Mon Sep 17 00:00:00 2001 From: Aboyeji-Isaac <141873116+Aboyeji-Isaac@users.noreply.github.com> Date: Tue, 27 Oct 2026 10:54:50 +0100 Subject: [PATCH 08/12] test(flags): cover defaults parsing and invalid flags --- tests/featureFlags.test.ts | 45 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) create mode 100644 tests/featureFlags.test.ts diff --git a/tests/featureFlags.test.ts b/tests/featureFlags.test.ts new file mode 100644 index 0000000..e8c5d92 --- /dev/null +++ b/tests/featureFlags.test.ts @@ -0,0 +1,45 @@ +import { + FEATURE_FLAG_DEFAULTS, + parseFeatureFlags, +} from '../src/config/flags'; + +describe('feature flag configuration', () => { + it('preserves existing optional behavior by default and leaves milestones disabled', () => { + expect(FEATURE_FLAG_DEFAULTS).toEqual({ + webhookBatching: true, + deliveryPooling: true, + chat: true, + milestones: false, + }); + }); + + it('parses bare names and explicit boolean values', () => { + expect(parseFeatureFlags('chat=false, deliveryPooling=true')).toEqual({ + webhookBatching: true, + deliveryPooling: true, + chat: false, + milestones: false, + }); + expect(parseFeatureFlags('milestones')).toEqual({ + webhookBatching: true, + deliveryPooling: true, + chat: true, + milestones: true, + }); + }); + + it('lets individual environment overrides take precedence over FEATURE_FLAGS', () => { + expect(parseFeatureFlags('chat=false', { chat: true, deliveryPooling: false })).toEqual({ + webhookBatching: true, + deliveryPooling: false, + chat: true, + milestones: false, + }); + }); + + it('rejects unknown flag names and invalid boolean values', () => { + expect(() => parseFeatureFlags('unknownFeature=true')).toThrow('Unknown feature flag'); + expect(() => parseFeatureFlags('chat=maybe')).toThrow('Invalid boolean'); + expect(() => parseFeatureFlags('chat=true=false')).toThrow('Invalid FEATURE_FLAGS entry'); + }); +}); From 28b4e9a0c4313a7af2c1fbad94ba1e5b5a6361f3 Mon Sep 17 00:00:00 2001 From: Aboyeji-Isaac <141873116+Aboyeji-Isaac@users.noreply.github.com> Date: Tue, 27 Oct 2026 10:55:00 +0100 Subject: [PATCH 09/12] test(flags): verify disabled chat has no persistence side effects --- tests/chatFeatureFlag.test.ts | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 tests/chatFeatureFlag.test.ts diff --git a/tests/chatFeatureFlag.test.ts b/tests/chatFeatureFlag.test.ts new file mode 100644 index 0000000..a3b5d2f --- /dev/null +++ b/tests/chatFeatureFlag.test.ts @@ -0,0 +1,34 @@ +jest.mock('../src/config/flags', () => ({ + isEnabled: (flag: string) => flag !== 'chat', +})); + +import { + ChatFeatureDisabledError, + ChatMessageService, +} from '../src/sockets/chatMessage.service'; +import type { ChatMessageRepository } from '../src/repositories/ChatMessageRepository'; + +describe('chat feature flag', () => { + const repository = { + findRecent: jest.fn(), + create: jest.fn(), + }; + + const service = new ChatMessageService(repository as unknown as ChatMessageRepository); + + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('does not read chat history while chat is disabled', async () => { + await expect(service.getRecentTranscript()).resolves.toEqual([]); + expect(repository.findRecent).not.toHaveBeenCalled(); + }); + + it('rejects message persistence while chat is disabled', async () => { + await expect(service.createMessage({ content: 'hello' })).rejects.toBeInstanceOf( + ChatFeatureDisabledError, + ); + expect(repository.create).not.toHaveBeenCalled(); + }); +}); From 3b08a1296894b8329a09ebbef9b8ba8cd493f6f5 Mon Sep 17 00:00:00 2001 From: Aboyeji-Isaac <141873116+Aboyeji-Isaac@users.noreply.github.com> Date: Tue, 27 Oct 2026 10:55:18 +0100 Subject: [PATCH 10/12] docs(flags): document feature flag deployment and defaults --- docs/feature-flags-deployment.md | 46 ++++++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) create mode 100644 docs/feature-flags-deployment.md diff --git a/docs/feature-flags-deployment.md b/docs/feature-flags-deployment.md new file mode 100644 index 0000000..cd9d7c6 --- /dev/null +++ b/docs/feature-flags-deployment.md @@ -0,0 +1,46 @@ +# Feature flag deployment guide + +The feature flag registry is defined in src/config/flags.ts and validated environment variables are declared in src/config/env.ts. + +## Known flags and defaults + +| Flag | Default | Gated behavior | +| --- | --- | --- | +| webhookBatching | true | Retrying due webhook delivery attempts in batches | +| deliveryPooling | true | Creating delivery pools and assigning a pool to a driver | +| chat | true | Replaying chat history and persisting/broadcasting chat messages | +| milestones | false | Reserved for milestone behavior; no milestone service or entry point currently exists in this repository | + +Defaults preserve the existing webhook retry, pooling, and chat behavior. The milestones flag remains off until a milestone implementation is introduced. + +## Configure flags + +FEATURE_FLAGS accepts comma-separated flag names or name=value pairs. A bare name enables that flag; a value must be true or false. Examples: + + FEATURE_FLAGS=chat=false,deliveryPooling=false + +Individual variables override values supplied through FEATURE_FLAGS: + + FEATURE_WEBHOOK_BATCHING_ENABLED=false + FEATURE_DELIVERY_POOLING_ENABLED=true + FEATURE_CHAT_ENABLED=false + FEATURE_MILESTONES_ENABLED=false + +Leave individual variables unset or blank to use the registry default or the value supplied in FEATURE_FLAGS. Unknown names and invalid boolean values cause startup to fail with a clear configuration error rather than silently enabling or disabling behavior. + +Flags are resolved at process startup. Updating environment configuration does not require a code change or new image, but the process must be restarted to load the new values. + +## Operational behavior + +- With webhookBatching disabled, the retry sweep returns zero processed attempts without querying due attempts. New webhook dispatch behavior is unchanged. +- With deliveryPooling disabled, pool creation returns an empty list and direct pool assignment returns an unassigned result. Ordinary one-to-one driver assignment is unchanged. +- With chat disabled, recent transcript reads return an empty list and incoming messages are not persisted or broadcast. Socket clients receive a clear disabled response when attempting to send a message. +- The milestones flag is registered and configurable, but cannot gate a milestone flow until that flow exists. Do not treat enabling the flag as enabling milestone functionality. + +## Tests + +Run the feature flag parsing and chat gate tests: + + pnpm test -- tests/featureFlags.test.ts tests/chatFeatureFlag.test.ts + +Then run the full suite, type check/build, and lint before merging. Do not commit secrets or production credentials in this configuration. From 1ae4c6a01b5678ab04e7b276e3edfef58a16bee4 Mon Sep 17 00:00:00 2001 From: Aboyeji-Isaac <141873116+Aboyeji-Isaac@users.noreply.github.com> Date: Tue, 27 Oct 2026 11:16:46 +0100 Subject: [PATCH 11/12] test(flags): verify pooling and webhook gates are no-ops --- tests/featureFlagServiceGates.test.ts | 30 +++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 tests/featureFlagServiceGates.test.ts diff --git a/tests/featureFlagServiceGates.test.ts b/tests/featureFlagServiceGates.test.ts new file mode 100644 index 0000000..2b5bbf7 --- /dev/null +++ b/tests/featureFlagServiceGates.test.ts @@ -0,0 +1,30 @@ +jest.mock('../src/config/flags', () => ({ + isEnabled: (flag: string) => flag !== 'deliveryPooling' && flag !== 'webhookBatching', +})); + +jest.mock('../src/models/WebhookDeliveryAttempt', () => ({ + WebhookDeliveryAttempt: { + find: jest.fn(), + }, +})); + +import poolingService, { DeliveryPool } from '../src/services/poolingService'; +import webhookService from '../src/services/webhookService'; +import { WebhookDeliveryAttempt } from '../src/models/WebhookDeliveryAttempt'; + +describe('feature-flag service gates', () => { + it('does not query or create delivery pools when pooling is disabled', async () => { + await expect(poolingService.createPools()).resolves.toEqual([]); + await expect( + poolingService.assignPoolToNearestDriver({} as DeliveryPool), + ).resolves.toEqual({ + assigned: false, + reason: 'Delivery pooling is disabled by feature flag', + }); + }); + + it('does not query due webhook attempts when webhook batching is disabled', async () => { + await expect(webhookService.retryDueAttempts()).resolves.toBe(0); + expect(WebhookDeliveryAttempt.find).not.toHaveBeenCalled(); + }); +}); From 944c801f0e1c7bfe640d3ca3ba8f4741a7f89971 Mon Sep 17 00:00:00 2001 From: Aboyeji-Isaac <141873116+Aboyeji-Isaac@users.noreply.github.com> Date: Tue, 27 Oct 2026 11:20:17 +0100 Subject: [PATCH 12/12] refactor(flags): isolate feature flag interface fields --- src/config/env.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/config/env.ts b/src/config/env.ts index b5320de..a611186 100644 --- a/src/config/env.ts +++ b/src/config/env.ts @@ -18,12 +18,12 @@ interface EnvConfig { CORS_ORIGIN: string; RATE_LIMIT_WINDOW_MS: number; RATE_LIMIT_MAX_REQUESTS: number; + DISPUTE_NOTIFICATION_WEBHOOK_URL: string; FEATURE_FLAGS: string; FEATURE_WEBHOOK_BATCHING_ENABLED?: boolean; FEATURE_DELIVERY_POOLING_ENABLED?: boolean; FEATURE_CHAT_ENABLED?: boolean; FEATURE_MILESTONES_ENABLED?: boolean; - DISPUTE_NOTIFICATION_WEBHOOK_URL: string; UPLOAD_STORAGE_DRIVER: string; UPLOAD_LOCAL_DIR: string; UPLOAD_MAX_FILE_SIZE_MB: number;