@inferencesh/sdk 0.5.9 → 0.5.11
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/dist/api/apps.d.ts +4 -0
- package/dist/api/apps.js +6 -0
- package/dist/api/files.js +58 -12
- package/dist/api/tasks.js +1 -1
- package/dist/http/client.js +3 -1
- package/dist/http/index.d.ts +1 -0
- package/dist/http/index.js +1 -0
- package/dist/http/streamable.d.ts +67 -0
- package/dist/http/streamable.js +195 -0
- package/dist/http/streamable.test.d.ts +4 -0
- package/dist/http/streamable.test.js +410 -0
- package/dist/streamable.integration.test.d.ts +11 -0
- package/dist/streamable.integration.test.js +150 -0
- package/dist/types.d.ts +2 -346
- package/dist/types.js +0 -29
- package/package.json +1 -1
package/dist/api/apps.d.ts
CHANGED
|
@@ -58,5 +58,9 @@ export declare class AppsAPI {
|
|
|
58
58
|
* Save app license
|
|
59
59
|
*/
|
|
60
60
|
saveLicense(appId: string, license: string): Promise<LicenseRecord>;
|
|
61
|
+
/**
|
|
62
|
+
* Set the current (active) version of an app
|
|
63
|
+
*/
|
|
64
|
+
setCurrentVersion(appId: string, versionId: string): Promise<App>;
|
|
61
65
|
}
|
|
62
66
|
export declare function createAppsAPI(http: HttpClient): AppsAPI;
|
package/dist/api/apps.js
CHANGED
|
@@ -83,6 +83,12 @@ export class AppsAPI {
|
|
|
83
83
|
async saveLicense(appId, license) {
|
|
84
84
|
return this.http.request('post', `/apps/${appId}/license`, { data: { license } });
|
|
85
85
|
}
|
|
86
|
+
/**
|
|
87
|
+
* Set the current (active) version of an app
|
|
88
|
+
*/
|
|
89
|
+
async setCurrentVersion(appId, versionId) {
|
|
90
|
+
return this.http.request('post', `/apps/${appId}/current-version`, { data: { version_id: versionId } });
|
|
91
|
+
}
|
|
86
92
|
}
|
|
87
93
|
export function createAppsAPI(http) {
|
|
88
94
|
return new AppsAPI(http);
|
package/dist/api/files.js
CHANGED
|
@@ -1,3 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Parse a data URI and return the media type and decoded data.
|
|
3
|
+
*
|
|
4
|
+
* Supports formats:
|
|
5
|
+
* - data:image/jpeg;base64,/9j/4AAQ...
|
|
6
|
+
* - data:text/plain,Hello%20World
|
|
7
|
+
* - data:;base64,SGVsbG8= (defaults to text/plain)
|
|
8
|
+
*/
|
|
9
|
+
function parseDataUri(uri) {
|
|
10
|
+
// Match: data:[<mediatype>][;base64],<data>
|
|
11
|
+
const match = uri.match(/^data:([^;,]*)?(?:;(base64))?,(.*)$/s);
|
|
12
|
+
if (!match) {
|
|
13
|
+
throw new Error('Invalid data URI format');
|
|
14
|
+
}
|
|
15
|
+
const mediaType = match[1] || 'text/plain';
|
|
16
|
+
const isBase64 = match[2] === 'base64';
|
|
17
|
+
let dataStr = match[3];
|
|
18
|
+
if (isBase64) {
|
|
19
|
+
// Handle URL-safe base64 (- and _ instead of + and /)
|
|
20
|
+
dataStr = dataStr.replace(/-/g, '+').replace(/_/g, '/');
|
|
21
|
+
// Add padding if needed
|
|
22
|
+
const padding = 4 - (dataStr.length % 4);
|
|
23
|
+
if (padding !== 4) {
|
|
24
|
+
dataStr += '='.repeat(padding);
|
|
25
|
+
}
|
|
26
|
+
const binaryStr = atob(dataStr);
|
|
27
|
+
const bytes = new Uint8Array(binaryStr.length);
|
|
28
|
+
for (let i = 0; i < binaryStr.length; i++) {
|
|
29
|
+
bytes[i] = binaryStr.charCodeAt(i);
|
|
30
|
+
}
|
|
31
|
+
return { mediaType, data: bytes };
|
|
32
|
+
}
|
|
33
|
+
else {
|
|
34
|
+
// URL-encoded data
|
|
35
|
+
const decoded = decodeURIComponent(dataStr);
|
|
36
|
+
const encoder = new TextEncoder();
|
|
37
|
+
return { mediaType, data: encoder.encode(decoded) };
|
|
38
|
+
}
|
|
39
|
+
}
|
|
1
40
|
/**
|
|
2
41
|
* Files API
|
|
3
42
|
*/
|
|
@@ -27,11 +66,26 @@ export class FilesAPI {
|
|
|
27
66
|
* Upload a file (Blob or base64 string)
|
|
28
67
|
*/
|
|
29
68
|
async upload(data, options = {}) {
|
|
69
|
+
// Determine content type
|
|
70
|
+
let contentType = options.contentType;
|
|
71
|
+
if (!contentType) {
|
|
72
|
+
if (data instanceof Blob) {
|
|
73
|
+
contentType = data.type;
|
|
74
|
+
}
|
|
75
|
+
else if (typeof data === 'string' && data.startsWith('data:')) {
|
|
76
|
+
// Extract media type from data URI
|
|
77
|
+
const match = data.match(/^data:([^;,]*)?/);
|
|
78
|
+
contentType = match?.[1] || 'application/octet-stream';
|
|
79
|
+
}
|
|
80
|
+
else {
|
|
81
|
+
contentType = 'application/octet-stream';
|
|
82
|
+
}
|
|
83
|
+
}
|
|
30
84
|
// Step 1: Create the file record
|
|
31
85
|
const fileRequest = {
|
|
32
86
|
uri: '', // Empty URI as it will be set by the server
|
|
33
87
|
filename: options.filename,
|
|
34
|
-
content_type:
|
|
88
|
+
content_type: contentType,
|
|
35
89
|
path: options.path,
|
|
36
90
|
size: data instanceof Blob ? data.size : undefined,
|
|
37
91
|
};
|
|
@@ -50,16 +104,8 @@ export class FilesAPI {
|
|
|
50
104
|
else {
|
|
51
105
|
// If it's a base64 string, convert it to a Blob
|
|
52
106
|
if (data.startsWith('data:')) {
|
|
53
|
-
const
|
|
54
|
-
|
|
55
|
-
throw new Error('Invalid base64 data URI format');
|
|
56
|
-
}
|
|
57
|
-
const binaryStr = atob(matches[2]);
|
|
58
|
-
const bytes = new Uint8Array(binaryStr.length);
|
|
59
|
-
for (let i = 0; i < binaryStr.length; i++) {
|
|
60
|
-
bytes[i] = binaryStr.charCodeAt(i);
|
|
61
|
-
}
|
|
62
|
-
contentToUpload = new Blob([bytes], { type: matches[1] });
|
|
107
|
+
const parsed = parseDataUri(data);
|
|
108
|
+
contentToUpload = new Blob([parsed.data.buffer], { type: parsed.mediaType });
|
|
63
109
|
}
|
|
64
110
|
else {
|
|
65
111
|
// Assume it's a clean base64 string
|
|
@@ -68,7 +114,7 @@ export class FilesAPI {
|
|
|
68
114
|
for (let i = 0; i < binaryStr.length; i++) {
|
|
69
115
|
bytes[i] = binaryStr.charCodeAt(i);
|
|
70
116
|
}
|
|
71
|
-
contentToUpload = new Blob([bytes], { type: options.contentType || 'application/octet-stream' });
|
|
117
|
+
contentToUpload = new Blob([bytes.buffer], { type: options.contentType || 'application/octet-stream' });
|
|
72
118
|
}
|
|
73
119
|
}
|
|
74
120
|
// Upload to S3 using the signed URL
|
package/dist/api/tasks.js
CHANGED
|
@@ -192,7 +192,7 @@ export class TasksAPI {
|
|
|
192
192
|
* Feature/unfeature a task
|
|
193
193
|
*/
|
|
194
194
|
async feature(taskId, featured) {
|
|
195
|
-
return this.http.request('post', `/tasks/${taskId}/featured`, { data: { featured } });
|
|
195
|
+
return this.http.request('post', `/tasks/${taskId}/featured`, { data: { is_featured: featured } });
|
|
196
196
|
}
|
|
197
197
|
}
|
|
198
198
|
export function createTasksAPI(http) {
|
package/dist/http/client.js
CHANGED
|
@@ -80,7 +80,9 @@ export class HttpClient {
|
|
|
80
80
|
if (options.params) {
|
|
81
81
|
Object.entries(options.params).forEach(([key, value]) => {
|
|
82
82
|
if (value !== undefined && value !== null) {
|
|
83
|
-
|
|
83
|
+
// Serialize arrays and objects as JSON, primitives as strings
|
|
84
|
+
const serialized = typeof value === 'object' ? JSON.stringify(value) : String(value);
|
|
85
|
+
targetUrl.searchParams.append(key, serialized);
|
|
84
86
|
}
|
|
85
87
|
});
|
|
86
88
|
}
|
package/dist/http/index.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
1
|
export { HttpClient, HttpClientConfig, ErrorHandler, createHttpClient } from './client';
|
|
2
2
|
export { StreamManager, StreamManagerOptions, PartialDataWrapper } from './stream';
|
|
3
|
+
export { streamable, streamableRaw, StreamableManager, StreamableOptions, StreamableMessage, StreamableManagerOptions } from './streamable';
|
|
3
4
|
export { InferenceError, RequirementsNotMetException } from './errors';
|
package/dist/http/index.js
CHANGED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Streamable HTTP client using fetch + ReadableStream.
|
|
3
|
+
* This is the NDJSON alternative to SSE/EventSource that avoids
|
|
4
|
+
* the browser's ~6 connection limit per domain.
|
|
5
|
+
*/
|
|
6
|
+
export interface StreamableOptions {
|
|
7
|
+
/** Additional headers to include */
|
|
8
|
+
headers?: Record<string, string>;
|
|
9
|
+
/** Request body (will be JSON stringified) */
|
|
10
|
+
body?: unknown;
|
|
11
|
+
/** HTTP method (defaults to GET, or POST if body is provided) */
|
|
12
|
+
method?: 'GET' | 'POST';
|
|
13
|
+
/** AbortSignal for cancellation */
|
|
14
|
+
signal?: AbortSignal;
|
|
15
|
+
/** Skip heartbeat messages (default: true) */
|
|
16
|
+
skipHeartbeats?: boolean;
|
|
17
|
+
}
|
|
18
|
+
export interface StreamableMessage<T = unknown> {
|
|
19
|
+
/** Optional event type */
|
|
20
|
+
event?: string;
|
|
21
|
+
/** Data payload */
|
|
22
|
+
data?: T;
|
|
23
|
+
/** Updated fields (for partial updates) */
|
|
24
|
+
fields?: string[];
|
|
25
|
+
/** Message type (e.g., "heartbeat") */
|
|
26
|
+
type?: string;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Stream NDJSON from an HTTP endpoint using fetch + ReadableStream.
|
|
30
|
+
* Yields parsed JSON objects, automatically filtering heartbeats.
|
|
31
|
+
*/
|
|
32
|
+
export declare function streamable<T = unknown>(url: string, options?: StreamableOptions): AsyncGenerator<T>;
|
|
33
|
+
/**
|
|
34
|
+
* StreamableManager provides a callback-based API similar to StreamManager
|
|
35
|
+
* but uses fetch + ReadableStream instead of EventSource.
|
|
36
|
+
*/
|
|
37
|
+
export interface StreamableManagerOptions<T> {
|
|
38
|
+
/** URL to connect to */
|
|
39
|
+
url: string;
|
|
40
|
+
/** Additional headers */
|
|
41
|
+
headers?: Record<string, string>;
|
|
42
|
+
/** Request body */
|
|
43
|
+
body?: unknown;
|
|
44
|
+
/** Called for each message */
|
|
45
|
+
onData?: (data: T) => void;
|
|
46
|
+
/** Called for partial updates with fields list */
|
|
47
|
+
onPartialData?: (data: T, fields: string[]) => void;
|
|
48
|
+
/** Called on error */
|
|
49
|
+
onError?: (error: Error) => void;
|
|
50
|
+
/** Called when stream starts */
|
|
51
|
+
onStart?: () => void;
|
|
52
|
+
/** Called when stream ends */
|
|
53
|
+
onEnd?: () => void;
|
|
54
|
+
}
|
|
55
|
+
export declare class StreamableManager<T> {
|
|
56
|
+
private options;
|
|
57
|
+
private abortController;
|
|
58
|
+
private isRunning;
|
|
59
|
+
constructor(options: StreamableManagerOptions<T>);
|
|
60
|
+
start(): Promise<void>;
|
|
61
|
+
stop(): void;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Raw streamable that yields the full message including wrapper.
|
|
65
|
+
* Use this when you need access to event type or fields.
|
|
66
|
+
*/
|
|
67
|
+
export declare function streamableRaw<T = unknown>(url: string, options?: StreamableOptions): AsyncGenerator<StreamableMessage<T> | T>;
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Streamable HTTP client using fetch + ReadableStream.
|
|
3
|
+
* This is the NDJSON alternative to SSE/EventSource that avoids
|
|
4
|
+
* the browser's ~6 connection limit per domain.
|
|
5
|
+
*/
|
|
6
|
+
/**
|
|
7
|
+
* Stream NDJSON from an HTTP endpoint using fetch + ReadableStream.
|
|
8
|
+
* Yields parsed JSON objects, automatically filtering heartbeats.
|
|
9
|
+
*/
|
|
10
|
+
export async function* streamable(url, options = {}) {
|
|
11
|
+
const { headers = {}, body, method = body ? 'POST' : 'GET', signal, skipHeartbeats = true, } = options;
|
|
12
|
+
const requestHeaders = {
|
|
13
|
+
Accept: 'application/x-ndjson',
|
|
14
|
+
...headers,
|
|
15
|
+
};
|
|
16
|
+
if (body) {
|
|
17
|
+
requestHeaders['Content-Type'] = 'application/json';
|
|
18
|
+
}
|
|
19
|
+
const res = await fetch(url, {
|
|
20
|
+
method,
|
|
21
|
+
headers: requestHeaders,
|
|
22
|
+
body: body ? JSON.stringify(body) : undefined,
|
|
23
|
+
signal,
|
|
24
|
+
});
|
|
25
|
+
if (!res.ok) {
|
|
26
|
+
const text = await res.text();
|
|
27
|
+
throw new Error(`HTTP ${res.status}: ${text}`);
|
|
28
|
+
}
|
|
29
|
+
if (!res.body) {
|
|
30
|
+
throw new Error('No response body');
|
|
31
|
+
}
|
|
32
|
+
const reader = res.body.getReader();
|
|
33
|
+
const decoder = new TextDecoder();
|
|
34
|
+
let buffer = '';
|
|
35
|
+
try {
|
|
36
|
+
while (true) {
|
|
37
|
+
const { done, value } = await reader.read();
|
|
38
|
+
if (done)
|
|
39
|
+
break;
|
|
40
|
+
buffer += decoder.decode(value, { stream: true });
|
|
41
|
+
const lines = buffer.split('\n');
|
|
42
|
+
buffer = lines.pop();
|
|
43
|
+
for (const line of lines) {
|
|
44
|
+
if (!line.trim())
|
|
45
|
+
continue;
|
|
46
|
+
const parsed = JSON.parse(line);
|
|
47
|
+
// Skip heartbeats
|
|
48
|
+
if (skipHeartbeats && parsed.type === 'heartbeat') {
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
// If it's a wrapped message with event/data/fields, yield the data
|
|
52
|
+
// Otherwise yield the entire parsed object
|
|
53
|
+
if ('data' in parsed && parsed.data !== undefined) {
|
|
54
|
+
yield parsed.data;
|
|
55
|
+
}
|
|
56
|
+
else {
|
|
57
|
+
yield parsed;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
// Handle remaining buffer
|
|
62
|
+
if (buffer.trim()) {
|
|
63
|
+
const parsed = JSON.parse(buffer);
|
|
64
|
+
if (!(skipHeartbeats && parsed.type === 'heartbeat')) {
|
|
65
|
+
if ('data' in parsed && parsed.data !== undefined) {
|
|
66
|
+
yield parsed.data;
|
|
67
|
+
}
|
|
68
|
+
else {
|
|
69
|
+
yield parsed;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
finally {
|
|
75
|
+
reader.releaseLock();
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
export class StreamableManager {
|
|
79
|
+
constructor(options) {
|
|
80
|
+
this.abortController = null;
|
|
81
|
+
this.isRunning = false;
|
|
82
|
+
this.options = options;
|
|
83
|
+
}
|
|
84
|
+
async start() {
|
|
85
|
+
if (this.isRunning)
|
|
86
|
+
return;
|
|
87
|
+
this.isRunning = true;
|
|
88
|
+
this.abortController = new AbortController();
|
|
89
|
+
try {
|
|
90
|
+
this.options.onStart?.();
|
|
91
|
+
for await (const message of streamableRaw(this.options.url, {
|
|
92
|
+
headers: this.options.headers,
|
|
93
|
+
body: this.options.body,
|
|
94
|
+
signal: this.abortController.signal,
|
|
95
|
+
})) {
|
|
96
|
+
if (!this.isRunning)
|
|
97
|
+
break;
|
|
98
|
+
// Check if it's a partial update
|
|
99
|
+
if (typeof message === 'object' &&
|
|
100
|
+
message !== null &&
|
|
101
|
+
'data' in message &&
|
|
102
|
+
'fields' in message &&
|
|
103
|
+
Array.isArray(message.fields)) {
|
|
104
|
+
const wrapper = message;
|
|
105
|
+
if (this.options.onPartialData && wrapper.data !== undefined) {
|
|
106
|
+
this.options.onPartialData(wrapper.data, wrapper.fields);
|
|
107
|
+
}
|
|
108
|
+
else if (wrapper.data !== undefined) {
|
|
109
|
+
this.options.onData?.(wrapper.data);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
else {
|
|
113
|
+
this.options.onData?.(message);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
catch (err) {
|
|
118
|
+
if (err instanceof Error && err.name === 'AbortError') {
|
|
119
|
+
// Intentional stop, not an error
|
|
120
|
+
}
|
|
121
|
+
else {
|
|
122
|
+
this.options.onError?.(err instanceof Error ? err : new Error(String(err)));
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
finally {
|
|
126
|
+
this.isRunning = false;
|
|
127
|
+
this.options.onEnd?.();
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
stop() {
|
|
131
|
+
this.isRunning = false;
|
|
132
|
+
this.abortController?.abort();
|
|
133
|
+
this.abortController = null;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* Raw streamable that yields the full message including wrapper.
|
|
138
|
+
* Use this when you need access to event type or fields.
|
|
139
|
+
*/
|
|
140
|
+
export async function* streamableRaw(url, options = {}) {
|
|
141
|
+
const { headers = {}, body, method = body ? 'POST' : 'GET', signal, skipHeartbeats = true, } = options;
|
|
142
|
+
const requestHeaders = {
|
|
143
|
+
Accept: 'application/x-ndjson',
|
|
144
|
+
...headers,
|
|
145
|
+
};
|
|
146
|
+
if (body) {
|
|
147
|
+
requestHeaders['Content-Type'] = 'application/json';
|
|
148
|
+
}
|
|
149
|
+
const res = await fetch(url, {
|
|
150
|
+
method,
|
|
151
|
+
headers: requestHeaders,
|
|
152
|
+
body: body ? JSON.stringify(body) : undefined,
|
|
153
|
+
signal,
|
|
154
|
+
});
|
|
155
|
+
if (!res.ok) {
|
|
156
|
+
const text = await res.text();
|
|
157
|
+
throw new Error(`HTTP ${res.status}: ${text}`);
|
|
158
|
+
}
|
|
159
|
+
if (!res.body) {
|
|
160
|
+
throw new Error('No response body');
|
|
161
|
+
}
|
|
162
|
+
const reader = res.body.getReader();
|
|
163
|
+
const decoder = new TextDecoder();
|
|
164
|
+
let buffer = '';
|
|
165
|
+
try {
|
|
166
|
+
while (true) {
|
|
167
|
+
const { done, value } = await reader.read();
|
|
168
|
+
if (done)
|
|
169
|
+
break;
|
|
170
|
+
buffer += decoder.decode(value, { stream: true });
|
|
171
|
+
const lines = buffer.split('\n');
|
|
172
|
+
buffer = lines.pop();
|
|
173
|
+
for (const line of lines) {
|
|
174
|
+
if (!line.trim())
|
|
175
|
+
continue;
|
|
176
|
+
const parsed = JSON.parse(line);
|
|
177
|
+
// Skip heartbeats
|
|
178
|
+
if (skipHeartbeats && parsed.type === 'heartbeat') {
|
|
179
|
+
continue;
|
|
180
|
+
}
|
|
181
|
+
yield parsed;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
// Handle remaining buffer
|
|
185
|
+
if (buffer.trim()) {
|
|
186
|
+
const parsed = JSON.parse(buffer);
|
|
187
|
+
if (!(skipHeartbeats && parsed.type === 'heartbeat')) {
|
|
188
|
+
yield parsed;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
finally {
|
|
193
|
+
reader.releaseLock();
|
|
194
|
+
}
|
|
195
|
+
}
|