Files
gh-asleep-ai-sleeptrack-ski…/skills/sleeptrack-be/references/nodejs_client_implementation.md
2025-11-29 17:58:23 +08:00

12 KiB

Node.js Client Implementation Guide

This reference provides complete Node.js client implementations for the Asleep API, including webhook servers and production patterns.

Complete Node.js API Client

const axios = require('axios');

class AsleepClient {
  constructor(apiKey, baseURL = 'https://api.asleep.ai') {
    this.apiKey = apiKey;
    this.baseURL = baseURL;
    this.client = axios.create({
      baseURL: baseURL,
      headers: { 'x-api-key': apiKey }
    });
  }

  async _request(method, path, options = {}) {
    try {
      const response = await this.client.request({
        method,
        url: path,
        ...options
      });
      return response.data;
    } catch (error) {
      if (error.response) {
        const status = error.response.status;
        const detail = error.response.data?.detail || 'Unknown error';

        if (status === 401) {
          throw new Error('Invalid API key');
        } else if (status === 403) {
          throw new Error(`API access error: ${detail}`);
        } else if (status === 404) {
          throw new Error('Resource not found');
        }
      }
      throw error;
    }
  }

  // User management
  async createUser(metadata = null) {
    const data = metadata ? { metadata } : {};
    const result = await this._request('POST', '/ai/v1/users', { data });
    return result.result.user_id;
  }

  async getUser(userId) {
    const result = await this._request('GET', `/ai/v1/users/${userId}`);
    return result.result;
  }

  async deleteUser(userId) {
    await this._request('DELETE', `/ai/v1/users/${userId}`);
  }

  // Session management
  async getSession(sessionId, userId, timezone = 'UTC') {
    const result = await this._request('GET', `/data/v3/sessions/${sessionId}`, {
      headers: { 'x-user-id': userId, 'timezone': timezone }
    });
    return result.result;
  }

  async listSessions(userId, options = {}) {
    const { dateGte, dateLte, offset = 0, limit = 20, orderBy = 'DESC' } = options;
    const params = { offset, limit, order_by: orderBy };
    if (dateGte) params.date_gte = dateGte;
    if (dateLte) params.date_lte = dateLte;

    const result = await this._request('GET', '/data/v1/sessions', {
      headers: { 'x-user-id': userId },
      params
    });
    return result.result;
  }

  async deleteSession(sessionId, userId) {
    await this._request('DELETE', `/ai/v1/sessions/${sessionId}`, {
      headers: { 'x-user-id': userId }
    });
  }

  // Statistics
  async getAverageStats(userId, startDate, endDate, timezone = 'UTC') {
    const result = await this._request('GET', `/data/v1/users/${userId}/average-stats`, {
      headers: { 'timezone': timezone },
      params: { start_date: startDate, end_date: endDate }
    });
    return result.result;
  }
}

// Usage
const client = new AsleepClient(process.env.ASLEEP_API_KEY);

Express Webhook Server

const express = require('express');
const app = express();

app.use(express.json());

const EXPECTED_API_KEY = process.env.ASLEEP_API_KEY;

app.post('/asleep-webhook', async (req, res) => {
  // Verify authentication
  const apiKey = req.headers['x-api-key'];
  const userId = req.headers['x-user-id'];

  if (apiKey !== EXPECTED_API_KEY) {
    console.warn('Unauthorized webhook attempt');
    return res.status(401).json({ error: 'Unauthorized' });
  }

  const { event, session_id, stat } = req.body;
  console.log(`Received ${event} event for user ${userId}`);

  try {
    switch (event) {
      case 'INFERENCE_COMPLETE':
        await handleInferenceComplete(req.body);
        break;
      case 'SESSION_COMPLETE':
        await handleSessionComplete(req.body);
        break;
      default:
        console.warn(`Unknown event type: ${event}`);
    }

    res.status(200).json({ status: 'success' });
  } catch (error) {
    console.error('Webhook processing error:', error);
    res.status(500).json({ error: 'Processing failed' });
  }
});

async function handleInferenceComplete(event) {
  const { session_id, user_id, sleep_stages } = event;

  // Update real-time dashboard
  await updateLiveDashboard(session_id, sleep_stages);

  // Store incremental data
  await db.collection('incremental_data').insertOne(event);

  console.log(`Processed INFERENCE_COMPLETE for session ${session_id}`);
}

async function handleSessionComplete(event) {
  const { session_id, user_id, stat, session } = event;

  // Store complete report
  await db.collection('sleep_reports').insertOne({
    user_id,
    session_id,
    date: session.start_time,
    statistics: stat,
    session_data: session,
    created_at: new Date()
  });

  // Send user notification
  await sendPushNotification(user_id, {
    title: 'Sleep Report Ready',
    body: `Sleep time: ${stat.sleep_time}, Efficiency: ${stat.sleep_efficiency.toFixed(1)}%`
  });

  // Update user statistics
  await updateUserAggregatedStats(user_id);

  console.log(`Processed SESSION_COMPLETE for session ${session_id}`);
}

const PORT = process.env.PORT || 5000;
app.listen(PORT, () => {
  console.log(`Webhook server listening on port ${PORT}`);
});

Retry with Exponential Backoff

async function retryWithExponentialBackoff(
  func,
  maxRetries = 3,
  baseDelay = 1000,
  maxDelay = 60000
) {
  for (let attempt = 0; attempt < maxRetries; attempt++) {
    try {
      return await func();
    } catch (error) {
      if (error.response?.status === 403) {
        const detail = error.response.data?.detail || '';
        if (detail.toLowerCase().includes('rate limit')) {
          if (attempt < maxRetries - 1) {
            const delay = Math.min(baseDelay * Math.pow(2, attempt), maxDelay);
            console.log(`Rate limited, retrying in ${delay}ms...`);
            await new Promise(resolve => setTimeout(resolve, delay));
            continue;
          }
        }
      }
      throw error;
    }
  }
}

// Usage
const result = await retryWithExponentialBackoff(
  () => client.getSession('session123', 'user123')
);

Basic Usage Examples

Creating Users

// Create user with metadata
const userId = await client.createUser({
  birth_year: 1990,
  gender: 'male',
  height: 175.5,
  weight: 70.0
});
console.log(`Created user: ${userId}`);

// Create user without metadata
const userId = await client.createUser();

Getting Sessions

// Get sessions for date range
const sessions = await client.listSessions('user123', {
  dateGte: '2024-01-01',
  dateLte: '2024-01-31',
  limit: 50,
  orderBy: 'DESC'
});

console.log(`Found ${sessions.sleep_session_list.length} sessions`);

sessions.sleep_session_list.forEach(session => {
  console.log(`Session ${session.session_id}: ${session.session_start_time}`);
  console.log(`  State: ${session.state}, Time in bed: ${session.time_in_bed}s`);
});

Getting Session Details

const session = await client.getSession(
  'session123',
  'user123',
  'America/New_York'
);

console.log(`Sleep efficiency: ${session.stat.sleep_efficiency.toFixed(1)}%`);
console.log(`Total sleep time: ${session.stat.sleep_time}`);
console.log(`Sleep stages: ${session.session.sleep_stages}`);
console.log(`Sleep cycles: ${session.stat.sleep_cycle.length}`);

Getting Statistics

const stats = await client.getAverageStats(
  'user123',
  '2024-01-01',
  '2024-01-31',
  'UTC'
);

const avg = stats.average_stats;
console.log(`Average sleep time: ${avg.sleep_time}`);
console.log(`Average efficiency: ${avg.sleep_efficiency.toFixed(1)}%`);
console.log(`Average bedtime: ${avg.start_time}`);
console.log(`Average wake time: ${avg.end_time}`);
console.log(`Light sleep ratio: ${(avg.light_ratio * 100).toFixed(1)}%`);
console.log(`Deep sleep ratio: ${(avg.deep_ratio * 100).toFixed(1)}%`);
console.log(`REM sleep ratio: ${(avg.rem_ratio * 100).toFixed(1)}%`);
console.log(`Number of sessions: ${stats.slept_sessions.length}`);

Asynchronous Webhook Processing

const Queue = require('bull');

const webhookQueue = new Queue('asleep-webhooks', {
  redis: {
    host: 'localhost',
    port: 6379
  }
});

app.post('/webhook', async (req, res) => {
  const event = req.body;

  // Queue for async processing
  await webhookQueue.add(event);

  // Respond immediately
  res.status(200).json({ status: 'queued' });
});

// Process queued webhooks
webhookQueue.process(async (job) => {
  const event = job.data;

  if (event.event === 'SESSION_COMPLETE') {
    await handleSessionComplete(event);
  } else if (event.event === 'INFERENCE_COMPLETE') {
    await handleInferenceComplete(event);
  }
});

Idempotency Pattern

async function handleSessionComplete(event) {
  const sessionId = event.session_id;

  // Check if already processed
  const existing = await db.collection('processed_webhooks').findOne({
    session_id: sessionId,
    event: 'SESSION_COMPLETE'
  });

  if (existing) {
    console.log(`Session ${sessionId} already processed, skipping`);
    return;
  }

  // Process event
  await saveSleepReport(event);

  // Mark as processed
  await db.collection('processed_webhooks').insertOne({
    session_id: sessionId,
    event: 'SESSION_COMPLETE',
    processed_at: new Date()
  });
}

Comprehensive Error Handling

class AsleepAPIError extends Error {
  constructor(message, statusCode, detail) {
    super(message);
    this.name = 'AsleepAPIError';
    this.statusCode = statusCode;
    this.detail = detail;
  }
}

class RateLimitError extends AsleepAPIError {
  constructor(detail) {
    super('Rate limit exceeded', 403, detail);
    this.name = 'RateLimitError';
  }
}

class ResourceNotFoundError extends AsleepAPIError {
  constructor(detail) {
    super('Resource not found', 404, detail);
    this.name = 'ResourceNotFoundError';
  }
}

async function safeApiRequest(requestFunc) {
  try {
    return await requestFunc();
  } catch (error) {
    if (error.response) {
      const status = error.response.status;
      const detail = error.response.data?.detail || 'Unknown error';

      if (status === 401) {
        throw new AsleepAPIError('Authentication failed', 401, detail);
      } else if (status === 403) {
        if (detail.toLowerCase().includes('rate limit')) {
          throw new RateLimitError(detail);
        } else {
          throw new AsleepAPIError('Access forbidden', 403, detail);
        }
      } else if (status === 404) {
        throw new ResourceNotFoundError(detail);
      } else {
        throw new AsleepAPIError(`API error (${status})`, status, detail);
      }
    }
    throw error;
  }
}

// Usage
try {
  const user = await safeApiRequest(() => client.getUser('user123'));
} catch (error) {
  if (error instanceof ResourceNotFoundError) {
    console.log('User not found, creating new user...');
    const userId = await client.createUser();
  } else if (error instanceof RateLimitError) {
    console.log('Rate limited, try again later');
  } else if (error instanceof AsleepAPIError) {
    console.error(`API error: ${error.message}`);
  }
}

Production Configuration

// config.js
require('dotenv').config();

class Config {
  static get ASLEEP_API_KEY() {
    return process.env.ASLEEP_API_KEY;
  }

  static get ASLEEP_BASE_URL() {
    return process.env.ASLEEP_BASE_URL || 'https://api.asleep.ai';
  }

  static get DATABASE_URL() {
    return process.env.DATABASE_URL;
  }

  static get REDIS_URL() {
    return process.env.REDIS_URL;
  }

  static get WEBHOOK_SECRET() {
    return process.env.WEBHOOK_SECRET;
  }

  static get ENABLE_CACHING() {
    return process.env.ENABLE_CACHING !== 'false';
  }

  static validate() {
    if (!this.ASLEEP_API_KEY) {
      throw new Error('ASLEEP_API_KEY environment variable required');
    }
    if (!this.DATABASE_URL) {
      throw new Error('DATABASE_URL environment variable required');
    }
  }
}

module.exports = Config;