Middleware & Lifecycle Hooks
Hook into Gluonic’s sync lifecycle for logging, analytics, transformations, and custom logic.
Overview
Middleware lets you intercept sync operations at various lifecycle points:
- Before/after mutations
- Before/after bootstrap
- Before/after delta sync
- On WebSocket connect/disconnect
- On conflicts
- On errors
Client Middleware
Basic Setup
import { createSyncClient } from '@gluonic/client'
const { store } = createSyncClient({
middleware: {
beforeMutation: async (mutation) => {
console.log('Sending mutation:', mutation)
// Return mutation (or modified version)
return mutation
},
afterMutation: async (mutation, result) => {
if (result.ok) {
console.log('Mutation succeeded:', mutation.type)
} else {
console.error('Mutation failed:', result.error)
}
},
beforeBootstrap: async () => {
console.log('Starting bootstrap...')
},
afterBootstrap: async (rows) => {
console.log(`Bootstrap complete: ${rows.length} rows`)
},
onFrame: async (frame) => {
console.log('Received frame:', frame)
},
onError: async (error, context) => {
console.error('Sync error:', error, context)
}
}
})Analytics Tracking
import { analytics } from './analytics'
middleware: {
afterMutation: async (mutation, result) => {
analytics.track('mutation', {
model: mutation.type,
operation: mutation.op,
success: result.ok,
error: result.error
})
},
afterBootstrap: async (rows) => {
analytics.track('bootstrap_complete', {
rowCount: rows.length,
duration: performance.now() - startTime
})
}
}Error Reporting
import * as Sentry from '@sentry/react-native'
middleware: {
onError: async (error, context) => {
Sentry.captureException(error, {
tags: {
syncOperation: context.operation,
model: context.model
}
})
},
afterMutation: async (mutation, result) => {
if (!result.ok) {
Sentry.captureMessage('Mutation failed', {
level: 'warning',
extra: { mutation, result }
})
}
}
}Data Transformation
middleware: {
beforeMutation: async (mutation) => {
// Transform data before sending
if (mutation.type === 'post') {
const patch = { ...mutation.patch }
// Sanitize HTML
if (patch.content) {
patch.content = sanitizeHTML(patch.content)
}
// Add metadata
patch._clientVersion = appVersion
patch._platform = Platform.OS
return { ...mutation, patch }
}
return mutation
}
}Rate Limiting
middleware: {
beforeMutation: async (mutation) => {
const count = await rateLimiter.increment(mutation.type)
if (count > 100) {
throw new Error('Rate limit exceeded')
}
return mutation
}
}Server Middleware
Request Logging
await SyncServer(app, {
adapter,
auth,
middleware: {
beforeRequest: async (req, operation) => {
console.log(`[${operation}] ${req.user?.id}`)
},
afterRequest: async (req, operation, duration) => {
console.log(`[${operation}] completed in ${duration}ms`)
}
}
})Mutation Auditing
middleware: {
afterMutation: async (mutation, result, context) => {
// Log all data changes for compliance
await audit.log({
timestamp: Date.now(),
userId: context.userId,
orgId: context.orgId,
model: mutation.type,
modelId: mutation.id,
operation: mutation.op,
patch: mutation.patch,
success: result.ok,
ip: context.req.ip
})
}
}Performance Monitoring
middleware: {
beforeBootstrap: async (orgId) => {
return { startTime: Date.now(), orgId }
},
afterBootstrap: async (orgId, rows, context) => {
const duration = Date.now() - context.startTime
await metrics.record('bootstrap_duration', duration, {
orgId,
rowCount: rows.length
})
}
}Lifecycle Events
Complete Lifecycle
// Client-side lifecycle
{
// Initialization
onInit: async (store) => { ... },
// Authentication
onAuth: async (token) => { ... },
onUnauthenticated: async () => { ... },
// Bootstrap
beforeBootstrap: async () => { ... },
afterBootstrap: async (rows) => { ... },
// Delta Sync
beforeDelta: async (since) => { ... },
afterDelta: async (frames) => { ... },
// Mutations
beforeMutation: async (mutation) => { ... },
afterMutation: async (mutation, result) => { ... },
// Frames
onFrame: async (frame) => { ... },
// Network
onOnline: async () => { ... },
onOffline: async () => { ... },
// Errors
onError: async (error, context) => { ... },
onConflict: async (conflict) => { ... }
}Use Cases
Feature Flagging
middleware: {
beforeMutation: async (mutation) => {
const flags = await featureFlags.get(userId)
if (mutation.type === 'post' && !flags.canCreatePosts) {
throw new Error('Feature not enabled for this user')
}
return mutation
}
}Data Encryption
middleware: {
beforeMutation: async (mutation) => {
if (mutation.type === 'message') {
const patch = { ...mutation.patch }
// Encrypt content
if (patch.content) {
patch.content = await encrypt(patch.content)
}
return { ...mutation, patch }
}
return mutation
},
onFrame: async (frame) => {
if (frame.t === 'message' && frame.p?.content) {
// Decrypt content
frame.p.content = await decrypt(frame.p.content)
}
return frame
}
}Quota Enforcement
middleware: {
beforeMutation: async (mutation, context) => {
if (mutation.op === 'i') {
const count = await prisma[mutation.type].count({
where: { userId: context.userId }
})
const quota = await getQuota(context.userId, mutation.type)
if (count >= quota) {
throw new Error(`Quota exceeded: max ${quota} ${mutation.type}s`)
}
}
return mutation
}
}Chaining Middleware
Multiple middleware functions are chained:
middleware: {
beforeMutation: [
validateMutation,
checkQuota,
transformData,
logMutation
]
}
// Executed in order:
// 1. validateMutation(mutation)
// 2. checkQuota(mutation)
// 3. transformData(mutation)
// 4. logMutation(mutation)
// 5. Actually send to serverAsync Middleware
All middleware can be async:
beforeMutation: async (mutation) => {
// Async operations allowed
const userPermissions = await fetchPermissions(userId)
if (!userPermissions.canEdit) {
throw new Error('No permission')
}
return mutation
}Next Steps
- Validation Adapter - Schema validation
- Built-In Helpers - Common middleware patterns
- Testing - Test middleware
Last updated on