@seenn/node 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/http.ts ADDED
@@ -0,0 +1,179 @@
1
+ import {
2
+ SeennError,
3
+ RateLimitError,
4
+ ValidationError,
5
+ NotFoundError,
6
+ AuthenticationError,
7
+ } from './errors.js';
8
+ import type { SeennConfig } from './client.js';
9
+
10
+ interface RequestOptions {
11
+ method: 'GET' | 'POST' | 'PUT' | 'DELETE';
12
+ path: string;
13
+ body?: unknown;
14
+ idempotencyKey?: string;
15
+ }
16
+
17
+ interface ErrorResponse {
18
+ error: {
19
+ code: string;
20
+ message: string;
21
+ details?: Record<string, unknown>;
22
+ };
23
+ }
24
+
25
+ export class HttpClient {
26
+ private readonly config: Required<SeennConfig>;
27
+
28
+ constructor(config: Required<SeennConfig>) {
29
+ this.config = config;
30
+ }
31
+
32
+ async get<T>(path: string): Promise<T> {
33
+ return this.request<T>({ method: 'GET', path });
34
+ }
35
+
36
+ async post<T>(path: string, body?: unknown): Promise<T> {
37
+ return this.request<T>({ method: 'POST', path, body });
38
+ }
39
+
40
+ async put<T>(path: string, body?: unknown): Promise<T> {
41
+ return this.request<T>({ method: 'PUT', path, body });
42
+ }
43
+
44
+ async delete<T>(path: string): Promise<T> {
45
+ return this.request<T>({ method: 'DELETE', path });
46
+ }
47
+
48
+ private async request<T>(options: RequestOptions): Promise<T> {
49
+ const { method, path, body } = options;
50
+ const url = `${this.config.baseUrl}${path}`;
51
+
52
+ const headers: Record<string, string> = {
53
+ 'Authorization': `Bearer ${this.config.apiKey}`,
54
+ 'Content-Type': 'application/json',
55
+ 'User-Agent': '@seenn/node',
56
+ };
57
+
58
+ if (options.idempotencyKey) {
59
+ headers['Idempotency-Key'] = options.idempotencyKey;
60
+ }
61
+
62
+ return this.executeWithRetry<T>(async () => {
63
+ const controller = new AbortController();
64
+ const timeoutId = setTimeout(() => controller.abort(), this.config.timeout);
65
+
66
+ try {
67
+ const response = await fetch(url, {
68
+ method,
69
+ headers,
70
+ body: body ? JSON.stringify(body) : undefined,
71
+ signal: controller.signal,
72
+ });
73
+
74
+ clearTimeout(timeoutId);
75
+
76
+ if (!response.ok) {
77
+ await this.handleErrorResponse(response);
78
+ }
79
+
80
+ return (await response.json()) as T;
81
+ } catch (error) {
82
+ clearTimeout(timeoutId);
83
+
84
+ if (error instanceof SeennError) {
85
+ throw error;
86
+ }
87
+
88
+ if (error instanceof Error) {
89
+ if (error.name === 'AbortError') {
90
+ throw new SeennError('Request timeout', 'TIMEOUT', 408);
91
+ }
92
+ throw new SeennError(error.message, 'NETWORK_ERROR', 0);
93
+ }
94
+
95
+ throw new SeennError('Unknown error', 'UNKNOWN', 0);
96
+ }
97
+ }, method === 'GET' || !!options.idempotencyKey);
98
+ }
99
+
100
+ private async executeWithRetry<T>(
101
+ fn: () => Promise<T>,
102
+ idempotent: boolean
103
+ ): Promise<T> {
104
+ let lastError: Error | undefined;
105
+ const maxRetries = idempotent ? this.config.maxRetries : 1;
106
+
107
+ for (let attempt = 0; attempt < maxRetries; attempt++) {
108
+ try {
109
+ return await fn();
110
+ } catch (error) {
111
+ lastError = error as Error;
112
+
113
+ // Don't retry on client errors (4xx) except rate limits
114
+ if (error instanceof SeennError) {
115
+ if (error.statusCode >= 400 && error.statusCode < 500) {
116
+ if (!(error instanceof RateLimitError)) {
117
+ throw error;
118
+ }
119
+ }
120
+ }
121
+
122
+ // Calculate delay with exponential backoff + jitter
123
+ if (attempt < maxRetries - 1) {
124
+ const baseDelay = 1000; // 1 second
125
+ const exponentialDelay = baseDelay * Math.pow(2, attempt);
126
+ const jitter = Math.random() * 0.2 * exponentialDelay;
127
+ const delay = Math.min(exponentialDelay + jitter, 30000); // Max 30s
128
+
129
+ if (this.config.debug) {
130
+ console.log(`[seenn] Retry attempt ${attempt + 1}/${maxRetries} after ${delay}ms`);
131
+ }
132
+
133
+ await this.sleep(delay);
134
+ }
135
+ }
136
+ }
137
+
138
+ throw lastError;
139
+ }
140
+
141
+ private async handleErrorResponse(response: Response): Promise<never> {
142
+ let errorData: ErrorResponse | undefined;
143
+
144
+ try {
145
+ errorData = (await response.json()) as ErrorResponse;
146
+ } catch {
147
+ // Response might not be JSON
148
+ }
149
+
150
+ const message = errorData?.error?.message || response.statusText;
151
+ const code = errorData?.error?.code || 'UNKNOWN';
152
+ const details = errorData?.error?.details;
153
+
154
+ switch (response.status) {
155
+ case 400:
156
+ throw new ValidationError(message, details);
157
+
158
+ case 401:
159
+ throw new AuthenticationError(message);
160
+
161
+ case 404:
162
+ throw new NotFoundError('Resource', details?.id as string || 'unknown');
163
+
164
+ case 429: {
165
+ const retryAfter = parseInt(response.headers.get('Retry-After') || '60', 10);
166
+ const limit = parseInt(response.headers.get('X-RateLimit-Limit') || '0', 10);
167
+ const remaining = parseInt(response.headers.get('X-RateLimit-Remaining') || '0', 10);
168
+ throw new RateLimitError(retryAfter, limit, remaining);
169
+ }
170
+
171
+ default:
172
+ throw new SeennError(message, code, response.status, details);
173
+ }
174
+ }
175
+
176
+ private sleep(ms: number): Promise<void> {
177
+ return new Promise((resolve) => setTimeout(resolve, ms));
178
+ }
179
+ }
package/src/index.ts ADDED
@@ -0,0 +1,20 @@
1
+ // @seenn/node - Job State Transport SDK
2
+
3
+ export { SeennClient } from './client.js';
4
+ export type { SeennConfig } from './client.js';
5
+
6
+ export { Job } from './job.js';
7
+ export type { JobStatus, JobData, ProgressOptions, CompleteOptions, FailOptions } from './job.js';
8
+
9
+ export type {
10
+ StartJobParams,
11
+ ProgressParams,
12
+ CompleteParams,
13
+ FailParams,
14
+ QueueInfo,
15
+ StageInfo,
16
+ JobResult,
17
+ JobError,
18
+ } from './types.js';
19
+
20
+ export { SeennError, RateLimitError, ValidationError, NotFoundError } from './errors.js';
package/src/job.ts ADDED
@@ -0,0 +1,201 @@
1
+ import type { HttpClient } from './http.js';
2
+ import type {
3
+ JobResponse,
4
+ ProgressParams,
5
+ CompleteParams,
6
+ FailParams,
7
+ QueueInfo,
8
+ StageInfo,
9
+ JobResult,
10
+ JobError,
11
+ } from './types.js';
12
+
13
+ export type JobStatus = 'pending' | 'running' | 'completed' | 'failed';
14
+
15
+ export interface JobData {
16
+ id: string;
17
+ appId: string;
18
+ userId: string;
19
+ jobType: string;
20
+ title: string;
21
+ status: JobStatus;
22
+ progress: number;
23
+ message?: string;
24
+ queue?: QueueInfo;
25
+ stage?: StageInfo;
26
+ result?: JobResult;
27
+ error?: JobError;
28
+ metadata?: Record<string, unknown>;
29
+ estimatedCompletionAt?: string;
30
+ createdAt: Date;
31
+ updatedAt: Date;
32
+ completedAt?: Date;
33
+ }
34
+
35
+ export interface ProgressOptions {
36
+ message?: string;
37
+ queue?: QueueInfo;
38
+ stage?: StageInfo;
39
+ estimatedCompletionAt?: string;
40
+ metadata?: Record<string, unknown>;
41
+ }
42
+
43
+ export interface CompleteOptions {
44
+ result?: JobResult;
45
+ message?: string;
46
+ }
47
+
48
+ export interface FailOptions {
49
+ error: JobError;
50
+ retryable?: boolean;
51
+ }
52
+
53
+ /**
54
+ * Job instance with fluent API for updates
55
+ */
56
+ export class Job implements JobData {
57
+ readonly id: string;
58
+ readonly appId: string;
59
+ readonly userId: string;
60
+ readonly jobType: string;
61
+ readonly title: string;
62
+ status: JobStatus;
63
+ progress: number;
64
+ message?: string;
65
+ queue?: QueueInfo;
66
+ stage?: StageInfo;
67
+ result?: JobResult;
68
+ error?: JobError;
69
+ metadata?: Record<string, unknown>;
70
+ estimatedCompletionAt?: string;
71
+ readonly createdAt: Date;
72
+ updatedAt: Date;
73
+ completedAt?: Date;
74
+
75
+ private readonly http: HttpClient;
76
+
77
+ constructor(data: JobResponse, http: HttpClient) {
78
+ this.id = data.id;
79
+ this.appId = data.appId;
80
+ this.userId = data.userId;
81
+ this.jobType = data.jobType;
82
+ this.title = data.title;
83
+ this.status = data.status;
84
+ this.progress = data.progress;
85
+ this.message = data.message;
86
+ this.queue = data.queue;
87
+ this.stage = data.stage;
88
+ this.result = data.result;
89
+ this.error = data.error;
90
+ this.metadata = data.metadata;
91
+ this.estimatedCompletionAt = data.estimatedCompletionAt;
92
+ this.createdAt = new Date(data.createdAt);
93
+ this.updatedAt = new Date(data.updatedAt);
94
+ this.completedAt = data.completedAt ? new Date(data.completedAt) : undefined;
95
+
96
+ this.http = http;
97
+ }
98
+
99
+ /**
100
+ * Update job progress (0-100)
101
+ */
102
+ async setProgress(progress: number, options?: ProgressOptions): Promise<this> {
103
+ const params: ProgressParams = {
104
+ progress,
105
+ ...options,
106
+ };
107
+
108
+ const response = await this.http.post<JobResponse>(
109
+ `/v1/jobs/${this.id}/progress`,
110
+ params
111
+ );
112
+
113
+ this.updateFromResponse(response);
114
+ return this;
115
+ }
116
+
117
+ /**
118
+ * Mark job as completed
119
+ */
120
+ async complete(options?: CompleteOptions): Promise<this> {
121
+ const params: CompleteParams = options || {};
122
+
123
+ const response = await this.http.post<JobResponse>(
124
+ `/v1/jobs/${this.id}/complete`,
125
+ params
126
+ );
127
+
128
+ this.updateFromResponse(response);
129
+ return this;
130
+ }
131
+
132
+ /**
133
+ * Mark job as failed
134
+ */
135
+ async fail(options: FailOptions): Promise<this> {
136
+ const params: FailParams = options;
137
+
138
+ const response = await this.http.post<JobResponse>(
139
+ `/v1/jobs/${this.id}/fail`,
140
+ params
141
+ );
142
+
143
+ this.updateFromResponse(response);
144
+ return this;
145
+ }
146
+
147
+ /**
148
+ * Refresh job data from server
149
+ */
150
+ async refresh(): Promise<this> {
151
+ const response = await this.http.get<JobResponse>(`/v1/jobs/${this.id}`);
152
+ this.updateFromResponse(response);
153
+ return this;
154
+ }
155
+
156
+ /**
157
+ * Check if job is in a terminal state
158
+ */
159
+ get isTerminal(): boolean {
160
+ return this.status === 'completed' || this.status === 'failed';
161
+ }
162
+
163
+ /**
164
+ * Get plain object representation
165
+ */
166
+ toJSON(): JobData {
167
+ return {
168
+ id: this.id,
169
+ appId: this.appId,
170
+ userId: this.userId,
171
+ jobType: this.jobType,
172
+ title: this.title,
173
+ status: this.status,
174
+ progress: this.progress,
175
+ message: this.message,
176
+ queue: this.queue,
177
+ stage: this.stage,
178
+ result: this.result,
179
+ error: this.error,
180
+ metadata: this.metadata,
181
+ estimatedCompletionAt: this.estimatedCompletionAt,
182
+ createdAt: this.createdAt,
183
+ updatedAt: this.updatedAt,
184
+ completedAt: this.completedAt,
185
+ };
186
+ }
187
+
188
+ private updateFromResponse(response: JobResponse): void {
189
+ this.status = response.status;
190
+ this.progress = response.progress;
191
+ this.message = response.message;
192
+ this.queue = response.queue;
193
+ this.stage = response.stage;
194
+ this.result = response.result;
195
+ this.error = response.error;
196
+ this.metadata = response.metadata;
197
+ this.estimatedCompletionAt = response.estimatedCompletionAt;
198
+ this.updatedAt = new Date(response.updatedAt);
199
+ this.completedAt = response.completedAt ? new Date(response.completedAt) : undefined;
200
+ }
201
+ }
package/src/types.ts ADDED
@@ -0,0 +1,111 @@
1
+ // Job Parameters
2
+
3
+ export interface StartJobParams {
4
+ /** Unique job type identifier (e.g., 'video-generation', 'image-processing') */
5
+ jobType: string;
6
+ /** User ID who owns this job */
7
+ userId: string;
8
+ /** Human-readable title for the job */
9
+ title: string;
10
+ /** Optional metadata (max 10KB) */
11
+ metadata?: Record<string, unknown>;
12
+ /** Optional initial queue information */
13
+ queue?: QueueInfo;
14
+ /** Optional initial stage information */
15
+ stage?: StageInfo;
16
+ /** Optional estimated completion time (ISO 8601) */
17
+ estimatedCompletionAt?: string;
18
+ /** Optional TTL in seconds (default: 30 days) */
19
+ ttlSeconds?: number;
20
+ }
21
+
22
+ export interface ProgressParams {
23
+ /** Progress percentage (0-100) */
24
+ progress: number;
25
+ /** Optional status message */
26
+ message?: string;
27
+ /** Optional updated queue information */
28
+ queue?: QueueInfo;
29
+ /** Optional updated stage information */
30
+ stage?: StageInfo;
31
+ /** Optional updated ETA */
32
+ estimatedCompletionAt?: string;
33
+ /** Optional metadata update (merged with existing) */
34
+ metadata?: Record<string, unknown>;
35
+ }
36
+
37
+ export interface CompleteParams {
38
+ /** Result data (max 100KB) */
39
+ result?: JobResult;
40
+ /** Optional completion message */
41
+ message?: string;
42
+ }
43
+
44
+ export interface FailParams {
45
+ /** Error information */
46
+ error: JobError;
47
+ /** Whether the job can be retried */
48
+ retryable?: boolean;
49
+ }
50
+
51
+ // Nested Types
52
+
53
+ export interface QueueInfo {
54
+ /** Current position in queue (1-based) */
55
+ position: number;
56
+ /** Total items in queue */
57
+ total?: number;
58
+ /** Optional queue name/tier */
59
+ queueName?: string;
60
+ }
61
+
62
+ export interface StageInfo {
63
+ /** Current stage name */
64
+ name: string;
65
+ /** Current stage number (1-based) */
66
+ current: number;
67
+ /** Total number of stages */
68
+ total: number;
69
+ /** Optional stage description */
70
+ description?: string;
71
+ }
72
+
73
+ export interface JobResult {
74
+ /** Result type identifier */
75
+ type?: string;
76
+ /** Result URL (e.g., generated video URL) */
77
+ url?: string;
78
+ /** Additional result data */
79
+ data?: Record<string, unknown>;
80
+ }
81
+
82
+ export interface JobError {
83
+ /** Error code */
84
+ code: string;
85
+ /** Human-readable error message */
86
+ message: string;
87
+ /** Additional error details */
88
+ details?: Record<string, unknown>;
89
+ }
90
+
91
+ // API Response Types
92
+
93
+ export interface JobResponse {
94
+ id: string;
95
+ appId: string;
96
+ userId: string;
97
+ jobType: string;
98
+ title: string;
99
+ status: 'pending' | 'running' | 'completed' | 'failed';
100
+ progress: number;
101
+ message?: string;
102
+ queue?: QueueInfo;
103
+ stage?: StageInfo;
104
+ result?: JobResult;
105
+ error?: JobError;
106
+ metadata?: Record<string, unknown>;
107
+ estimatedCompletionAt?: string;
108
+ createdAt: string;
109
+ updatedAt: string;
110
+ completedAt?: string;
111
+ }