@loop/core
Foundational patterns for the Loop Health ecosystem. Zero internal dependencies (except @upstash/redis for distributed features).
Installation
pnpm add @loop/coreResult Type
Monadic error handling without exceptions. Every fallible operation returns Result<T> instead of throwing.
import { ok, err, map, flatMap, combine, tryCatch, tryCatchSync } from '@loop/core';
import type { Result } from '@loop/core';
// Create results
const success = ok(42); // Result<number>
const failure = err(notFound()); // Result<never>
// Check and unwrap
if (success.ok) {
console.log(success.value); // 42
}
// Transform
const doubled = map(success, (n) => n * 2); // ok(84)
// Chain operations
const result = flatMap(getUserId(), (id) => getProfile(id));
// Combine multiple results
const combined = combine([ok(1), ok(2), ok(3)]); // ok([1, 2, 3])
// Wrap throwing code
const fetched = await tryCatch(() => fetch('/api'));
const parsed = tryCatchSync(() => JSON.parse(str));Utility Functions
| Function | Description |
|---|---|
ok(value) | Create a success result |
err(error) | Create a failure result |
isOk(result) | Type guard for success |
isErr(result) | Type guard for failure |
unwrap(result) | Get value or throw |
unwrapOr(result, fallback) | Get value or fallback |
map(result, fn) | Transform the success value |
mapErr(result, fn) | Transform the error |
flatMap(result, fn) | Chain results |
combine(results) | Combine array of results |
tryCatch(fn) | Wrap async throwing function |
tryCatchSync(fn) | Wrap sync throwing function |
Error Types
Structured error types for consistent error handling across all packages.
import {
AppError, ErrorCode,
createError, createValidationError,
notFound, unauthorized, forbidden,
dbError, networkError, fromUnknown,
} from '@loop/core';
// Create errors
const error = createError('VALIDATION', 'Email is required');
const notFoundErr = notFound('User', 'user_123');
const authErr = unauthorized('Invalid token');
const forbiddenErr = forbidden('Insufficient permissions');
const dbErr = dbError('Connection failed');
const netErr = networkError('Timeout');
// Wrap unknown errors
const wrapped = fromUnknown(caughtError);Error Codes
NOT_FOUND, UNAUTHORIZED, FORBIDDEN, VALIDATION, CONFLICT, RATE_LIMIT, DATABASE, NETWORK, EXTERNAL_SERVICE, INTERNAL
Logger
Structured logging with pretty output in development and JSON in production.
import { createLogger, logger } from '@loop/core';
// Default logger
logger.info('Server started', { port: 3000 });
// Named logger
const log = createLogger('patient-graph');
log.info('Request received', { path: '/profiles', method: 'GET' });
log.warn('Slow query', { duration: 500 });
log.error('Failed to process', { error });Distributed Locks
Redis-based distributed locking using Upstash.
import { acquireLock, releaseLock, withLock } from '@loop/core';
// Manual lock management
const lock = await acquireLock('checkout:user_123', { ttl: 30000 });
try {
await processCheckout();
} finally {
await releaseLock(lock);
}
// Automatic lock management
const result = await withLock('sync:user_123', async () => {
return await syncData();
}, { ttl: 60000 });Lock Key Helpers
import { checkoutLockKey, cronLockKey } from '@loop/core';
checkoutLockKey('user_123'); // 'checkout:user_123'
cronLockKey('sync-products'); // 'cron:sync-products'Rate Limiting
Sliding-window rate limiting via Upstash Redis.
import { checkRateLimit } from '@loop/core';
const result = await checkRateLimit({
key: `api:${userId}`,
limit: 100,
window: 60, // seconds
});
if (!result.allowed) {
// Rate limited — result.retryAfter contains seconds to wait
}Circuit Breaker
Protect against failing external services.
import { createCircuitBreaker, CircuitOpenError } from '@loop/core';
const breaker = createCircuitBreaker({
failureThreshold: 5,
resetTimeout: 30000,
halfOpenRequests: 2,
});
try {
const result = await breaker.execute(() => callExternalAPI());
} catch (error) {
if (error instanceof CircuitOpenError) {
// Circuit is open — external service is down
}
}Cache
In-memory caching with namespace support and TTL.
import { getCache, setCache, deleteCache, invalidateNamespace, withCache } from '@loop/core';
import { CACHE_NAMESPACES, CACHE_TTL } from '@loop/core';
// Manual caching
await setCache('user:123', userData, CACHE_TTL.SHORT);
const cached = await getCache('user:123');
await deleteCache('user:123');
// Cache-aside pattern
const data = await withCache('profile:123', () => fetchProfile('123'), {
ttl: CACHE_TTL.MEDIUM,
namespace: CACHE_NAMESPACES.PROFILES,
});
// Invalidate all entries in a namespace
await invalidateNamespace(CACHE_NAMESPACES.PROFILES);Used By
@loop/core is used by all applications in the Loop Platform:
- @loop/admin — CMS error handling, logging, and distributed locks
- @loop/my-loop-health — API error handling, circuit breakers for external services
- @loop/loop-health — Error handling and logging
- @loop/loop-health-mobile — Mobile error handling
- @loop/call-booking — API error handling, rate limiting
- @loop/embeddings-api — Error handling, circuit breakers for OpenAI, rate limiting