6.2 KiB
Design — @suaveplan/sms
Universal-tier SMS core, provider-agnostic, no Node/Bun-only APIs.
Module layout
src/
index.ts # root barrel (named re-exports)
error-codes/
error-codes.ts # defineCodes("SMS", [...])
error-codes.test.ts
error-codes.md
index.ts
legacy-errors/
legacy-errors.ts # back-compat SmsError + factories
legacy-errors.test.ts
legacy-errors.md
index.ts
legacy-metadata/
legacy-metadata.ts # SMS_ERROR_METADATA + registerSmsErrorCodes
legacy-metadata.test.ts
legacy-metadata.md
index.ts
schemas/
schemas.ts # Zod for SmsMessage/Template/Provider/etc.
schemas.test.ts
schemas.md
index.ts
types/
sms.types.ts # public type contracts
validation/
phone.ts # isValidE164 helper
phone.test.ts
phone.md
index.ts
templates/
templates.ts # TemplateRegistry + renderTemplate
templates.test.ts
templates.md
index.ts
opt-out/
opt-out.ts # OptOutRegistry
opt-out.test.ts
opt-out.md
index.ts
rate-limiter/
rate-limiter.ts # token-bucket
rate-limiter.test.ts
rate-limiter.md
index.ts
retry/
retry.ts # exponential backoff w/ jitter
retry.test.ts
retry.md
index.ts
scheduler/
scheduler.ts # MessageScheduler (Clock + Timers)
scheduler.test.ts
scheduler.md
index.ts
delivery-tracker/
delivery-tracker.ts # ingest + lookup + cache
delivery-tracker.test.ts
delivery-tracker.md
index.ts
in-memory-provider/
in-memory-provider.ts # InMemorySmsProvider (test prov)
in-memory-provider.test.ts
in-memory-provider.md
index.ts
service/
sms-service.ts # SmsService — orchestrator
sms-service.test.ts
sms-service.md
index.ts
routes/
problem-json.ts # respondProblemJson Hono helper
problem-json.test.ts
problem-json.md
index.ts
otel/
metrics.ts # 4 instruments
metrics.test.ts
metrics.md
index.ts
Public contracts
- Types:
SmsDeliveryStatus,SmsMessage,SmsDeliveryReport,SmsProvider,OptOutEntry,SmsTemplate,SmsEventMap,SmsServiceOptions,ScheduledMessage,ScheduleHandle,RetryPolicyOptions,RateLimiterOptions,DeliveryTrackerOptions. - Classes:
SmsService,TemplateRegistry,OptOutRegistry,RateLimiter,RetryPolicy,MessageScheduler,DeliveryTracker,InMemorySmsProvider. - Errors:
SMS_CODES,SmsError(legacy back-compat),SmsSendFailedError,SmsProviderError,SmsInvalidNumberError,SmsRateLimitedError,SmsConfigMissingError,SmsForbiddenError,SmsTemplateNotFoundError,SmsRecipientOptedOutError,SmsProviderNotConfiguredError,SmsScheduleCancelledError,SmsAbortedError,SmsInvalidConfigError. - Helpers:
respondProblemJson,PROBLEM_JSON_CONTENT_TYPE,registerSmsErrorCodes,SMS_ERROR_METADATA,isValidE164,renderTemplate. - OTel:
recordSmsSent,recordSmsFailed,recordSmsRetry,recordSmsSendDuration,refreshSmsMeters.
Key behaviours
- Phone validation — strict E.164 (
^\+[1-9]\d{1,14}$). - Template render —
{{var}}substitution; missing vars → empty string; supportsdefaultsmap. Brace literal escape via{{{. - Opt-out — set/clear/check, channel-scoped, bulk import/export.
- Rate limiter — token bucket per
(channel, recipient)key. Configurable capacity + refill-per-second; uses injectableClock. - Retry — exponential backoff with full-jitter;
maxAttempts,initialDelayMs,maxDelayMsconfigurable; abort-aware. - Scheduler —
sendAt(date, message)enqueues against an injectableTimers-like seam; returns aScheduleHandlewithcancel(). On fire, the message goes through the full pipeline (validation → rate-limit → retry → provider send → events). - Delivery tracker —
ingestReceipt(report)records the latest status keyed bymessageId;getStatus(messageId)returns the cached entry orundefined. Cache size bounded; LRU eviction. - Events — typed
EventBus<SmsEventMap>onservice.bus; legacyon/off/emitmirrors against the bus. - Abort —
service.send(msg, { signal })propagates through the rate limiter, retry, scheduler timer, and the provider's send call (when the provider accepts a signal). Aborted operations throwSmsAbortedErrorwith codeSMS.ABORTED.
OTel instruments
- Counter
sms.sent_total{channel,from_me} - Counter
sms.failed_total{code} - Counter
sms.retry_total{attempt} - Histogram
sms.send_duration_ms{outcome}
Error codes (genesis-only registry)
defineCodes("SMS", [...]) — 13 codes:
INVALID_NUMBERINVALID_CONFIGPROVIDER_NOT_CONFIGUREDPROVIDER_ERRORSEND_FAILEDTEMPLATE_NOT_FOUNDRECIPIENT_OPTED_OUTRATE_LIMITEDFORBIDDENCONFIG_MISSINGSCHEDULE_CANCELLEDABORTEDRECEIPT_NOT_FOUND
The upstream SMS_* runtime keys (used by SMS_ERROR_METADATA and
the back-compat SmsError class — SMS_SEND_FAILED,
SMS_PROVIDER_ERROR, SMS_INVALID_NUMBER, SMS_RATE_LIMITED,
SMS_CONFIG_MISSING, SMS_FORBIDDEN, SMS_INVALID_PHONE_NUMBER,
SMS_TEMPLATE_NOT_FOUND, SMS_RECIPIENT_OPTED_OUT,
SMS_PROVIDER_NOT_CONFIGURED) are preserved verbatim so legacy
callers keep compiling.
Subpath exports
Every feature module gets a subpath: ./error-codes, ./schemas,
./service, ./templates, ./opt-out, ./rate-limiter, ./retry,
./scheduler, ./delivery-tracker, ./in-memory-provider,
./routes/problem-json, ./otel, ./types, ./validation/phone.
Tier — universal
The package consumes only:
@suaveplan/error,@suaveplan/events,@suaveplan/types,@opentelemetry/api,tslib,zod.
No Node/Bun-only APIs; uses globalThis.setTimeout /
clearTimeout (universally available) via injectable Timers seam.