npm package: @fireonce/sdk
Description: Official TypeScript SDK for the FireOnce guaranteed-execution job scheduling platform
Author: Alex Carroll
Latest version: 0.2.0

# @fireonce/sdk

Official TypeScript SDK for [FireOnce](https://fireonce.com) -- the guaranteed-execution job scheduling platform.

Zero dependencies. Supports ESM and CommonJS. TypeScript strict mode.

## Installation

```bash
pnpm add @fireonce/sdk
# or
npm install @fireonce/sdk
```

## Quick Start

```typescript
import { FireOnce, defineJob } from '@fireonce/sdk';

// 1. Define a job type
const sendEmail = defineJob<{ to: string; subject: string; body: string }>({
  name: 'send-email',
  handler: 'https://api.myapp.com/webhooks/fireonce/email',
  retry: { strategy: 'exponential', maxAttempts: 3, delayMs: 1000 },
  timeout: '30s',
});

// 2. Create a client
const fireonce = new FireOnce({
  apiKey: process.env.FIREONCE_API_KEY!,
});

// 3. Schedule a job — fires in 2 days
const handle = await fireonce.schedule(sendEmail, {
  id: 'welcome-email-user-123',
  payload: { to: 'user@example.com', subject: 'Welcome!', body: 'Hello...' },
}).inDays(2);

// 4. Use the handle
const job = await handle.status();
console.log(job.status); // 'scheduled'
```

## Concepts

### Job Definitions

Job definitions capture the *what* and *how* of a job without scheduling anything. They are reusable templates that describe the handler, retry strategy, and timeout.

```typescript
import { defineJob } from '@fireonce/sdk';

// HTTP callback mode -- FireOnce sends a POST to your URL when the job executes
const sendEmail = defineJob<{ to: string; subject: string; body: string }>({
  name: 'send-email',
  handler: 'https://api.myapp.com/webhooks/fireonce/email',
  retry: { strategy: 'exponential', maxAttempts: 3, delayMs: 1000 },
  timeout: '30s',
});

// Local handler mode -- define the handler as a function (requires FireOnce Worker)
const processPayment = defineJob<{ orderId: string; amount: number }, { transactionId: string }>({
  name: 'process-payment',
  handler: async (payload) => {
    const result = await stripe.charges.create({ amount: payload.amount });
    return { transactionId: result.id };
  },
  retry: { strategy: 'exponential', maxAttempts: 5, delayMs: 2000 },
  timeout: '60s',
});
```

The first generic parameter (`TPayload`) types the payload. The second (`TResult`) types the return value and defaults to `void`.

### Scheduling Jobs

Use `fireonce.schedule()` to create a job instance. Chain fluent time methods to set when it fires:

```typescript
// Execute immediately
const handle = await fireonce.schedule(sendEmail, {
  payload: { to: 'user@example.com', subject: 'Welcome!', body: 'Hello...' },
});

// Execute in 2 days, 3 hours, and 30 minutes
const handle = await fireonce.schedule(sendEmail, {
  id: 'welcome-email-user-123',
  payload: { to: 'user@example.com', subject: 'Welcome!', body: 'Hello...' },
}).inDays(2).inHours(3).inMinutes(30);

// Execute at an absolute time
const handle = await fireonce.schedule(sendEmail, {
  payload: { to: 'user@example.com', subject: 'Welcome!', body: 'Hello...' },
}).at(new Date('2024-12-25T00:00:00Z'));

// All options
const handle = await fireonce.schedule(sendEmail, {
  id: 'welcome-email-user-123',
  payload: { to: 'user@example.com', subject: 'Welcome!', body: 'Hello...' },
  idempotencyKey: 'welcome-user-123',
  webhookUrl: 'https://myapp.com/job-status',
}).inDays(1);
```

Available time methods (all additive):

| Method | Description |
|---|---|
| `.inWeeks(n)` | Add `n` weeks |
| `.inDays(n)` | Add `n` days |
| `.inHours(n)` | Add `n` hours |
| `.inMinutes(n)` | Add `n` minutes |
| `.inSeconds(n)` | Add `n` seconds |
| `.at(date)` | Set absolute time (overrides relative offsets) |

### Job Handles

A `JobHandle` is your interface to a running (or completed) job:

```typescript
// Get current status
const job = await handle.status();
console.log(job.status); // 'queued' | 'scheduled' | 'processing' | 'completed' | ...

// Cancel a pending job
await handle.cancel();

// Replay a failed job
const replayed = await handle.replay();

// Poll until the job reaches a terminal state
const result = await handle.result({ timeout: 30_000, pollInterval: 2_000 });
console.log(result.status); // 'completed' | 'failed' | 'dead_lettered' | 'cancelled'
```

You can also get a handle for an existing job without scheduling:

```typescript
const handle = fireonce.getHandle('job-id-123');
const status = await handle.status();
```

### Bulk Scheduling

Schedule many jobs in a single API call:

```typescript
const handles = await fireonce.scheduleBulk([
  {
    job: sendEmail,
    options: {
      id: 'email-1',
      payload: { to: 'alice@example.com', subject: 'Hi', body: 'Hello Alice' },
    },
  },
  {
    job: sendEmail,
    options: {
      id: 'email-2',
      payload: { to: 'bob@example.com', subject: 'Hi', body: 'Hello Bob' },
    },
  },
]);

// Each item in the array is a JobHandle
for (const handle of handles) {
  console.log(handle.id);
}
```

## Management APIs

### API Keys

```typescript
// Create a new API key
const key = await fireonce.apiKeys.create({ label: 'Production' });
console.log(key.key); // Only returned once -- store securely

// List all keys
const keys = await fireonce.apiKeys.list();

// Revoke a key
await fireonce.apiKeys.revoke('key-id');
```

### Usage

```typescript
// Current billing period
const usage = await fireonce.usage.current();
console.log(usage.jobsCreated, usage.jobsCompleted);

// Historical usage
const history = await fireonce.usage.history();
```

### Billing

```typescript
// Create a checkout session
const { url } = await fireonce.billing.checkout({ plan: 'pro' });

// Upgrade plan
const { url: upgradeUrl } = await fireonce.billing.upgrade({ plan: 'enterprise' });
```

### Authentication

```typescript
// Register
const { token, tenant } = await fireonce.auth.register({
  name: 'Acme Corp',
  email: 'admin@acme.com',
  password: 'secure-password',
});

// Login
const { token: loginToken } = await fireonce.auth.login({
  email: 'admin@acme.com',
  password: 'secure-password',
});
```

## Error Handling

All API errors throw `FireOnceError` with structured fields:

```typescript
import { FireOnceError } from '@fireonce/sdk';

try {
  await handle.cancel();
} catch (err) {
  if (err instanceof FireOnceError) {
    console.error(err.message);  // Human-readable message
    console.error(err.status);   // HTTP status (0 for network/timeout)
    console.error(err.code);     // Machine-readable code (e.g. 'NOT_FOUND')
  }
}
```

### Error Codes

| Code | Description |
|------|-------------|
| `CONFIG_ERROR` | Invalid client configuration |
| `TIMEOUT` | Request timed out |
| `NETWORK_ERROR` | Network connectivity issue |
| `LOCAL_HANDLER_NO_CALLBACK` | Local handler used without a Worker or callbackUrl fallback |
| `RESULT_TIMEOUT` | `handle.result()` timed out waiting for terminal state |

## Configuration

| Option | Type | Default | Description |
|---|---|---|---|
| `apiKey` | `string` | *required* | Bearer token for API authentication |
| `baseUrl` | `string` | `https://api.fireonce.com` | Base URL of the FireOnce API |
| `timeout` | `number` | `30000` | Request timeout in milliseconds |
| `maxRetries` | `number` | `3` | Max retries on 429/5xx responses |
| `retryDelayMs` | `number` | `500` | Initial retry delay (doubles each attempt) |

## Local Handler Mode

When you define a job with a function handler, the function captures your execution intent but requires a [FireOnce Worker](https://fireonce.com/docs/workers) to actually run. If you schedule a local-handler job without a Worker, provide a `callbackUrl` fallback:

```typescript
const processPayment = defineJob<{ orderId: string }, { txId: string }>({
  name: 'process-payment',
  handler: async (payload) => {
    // This runs in the Worker, not when scheduling
    return { txId: await charge(payload.orderId) };
  },
});

// Without a Worker, provide a callbackUrl fallback
const handle = await fireonce.schedule(processPayment, {
  payload: { orderId: 'order-123' },
  callbackUrl: 'https://api.myapp.com/webhooks/fireonce/payments',
});
```

## Requirements

- Node.js >= 18.0.0 (uses native `fetch`)
- TypeScript >= 5.0 (for best type inference)
- Zero runtime dependencies