@acedatacloud/sdk 2026.722.0 → 2026.727.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.
@@ -1,4 +1,15 @@
1
- /** Task polling abstraction. */
1
+ /**
2
+ * Task polling for long-running operations.
3
+ *
4
+ * A generation call always returns a handle, never sometimes a handle and
5
+ * sometimes a plain object — the caller decides whether to wait. When the server
6
+ * answers synchronously (some endpoints do for fast or cached results) the
7
+ * handle is born already complete, so `wait()` returns immediately rather than
8
+ * polling for something that has already arrived.
9
+ *
10
+ * Kept behaviourally identical to the Python and Go implementations; a
11
+ * divergence here is a cross-language bug that only shows up in production.
12
+ */
2
13
 
3
14
  import { Transport } from './transport';
4
15
 
@@ -7,17 +18,135 @@ export interface TaskHandleOptions {
7
18
  maxWait?: number;
8
19
  }
9
20
 
10
- function taskStatus(state: Record<string, unknown>): 'succeeded' | 'failed' | '' {
21
+ const DONE = new Set(['succeed', 'succeeded', 'success', 'completed', 'complete', 'finished']);
22
+ const FAILED = new Set(['failed', 'failure', 'error', 'cancelled', 'canceled', 'rejected']);
23
+
24
+ function statusWords(node: unknown, depth = 0): string[] {
25
+ if (depth > 6) return [];
26
+ const out: string[] = [];
27
+ if (Array.isArray(node)) {
28
+ for (const item of node) out.push(...statusWords(item, depth + 1));
29
+ } else if (node && typeof node === 'object') {
30
+ for (const [key, value] of Object.entries(node as Record<string, unknown>)) {
31
+ if ((key === 'state' || key === 'status') && typeof value === 'string') {
32
+ out.push(value.toLowerCase());
33
+ } else {
34
+ out.push(...statusWords(value, depth + 1));
35
+ }
36
+ }
37
+ }
38
+ return out;
39
+ }
40
+
41
+ function collectUrls(node: unknown, out: string[], depth = 0): void {
42
+ if (depth > 6) return;
43
+ if (Array.isArray(node)) {
44
+ for (const item of node) collectUrls(item, out, depth + 1);
45
+ } else if (node && typeof node === 'object') {
46
+ for (const [key, value] of Object.entries(node as Record<string, unknown>)) {
47
+ const looksLikeUrl = key.endsWith('_url') || key === 'url' || key.endsWith('_urls');
48
+ if (typeof value === 'string' && value.startsWith('http') && looksLikeUrl) {
49
+ out.push(value);
50
+ } else {
51
+ collectUrls(value, out, depth + 1);
52
+ }
53
+ }
54
+ }
55
+ }
56
+
57
+ /**
58
+ * Every artifact URL in a task response, outermost first.
59
+ *
60
+ * Where the artifact lives is not derivable from the OpenAPI spec — `response`
61
+ * is typed as a bare object and the key differs per service (`video_url`,
62
+ * `image_url`, `data[].image_url`). Rather than keep a per-service table that
63
+ * goes stale, collect anything URL-shaped.
64
+ */
65
+ export function artifactUrls(state: Record<string, unknown> | null): string[] {
66
+ if (!state) return [];
67
+ const found: string[] = [];
68
+ collectUrls(state.response ?? state, found);
69
+ return [...new Set(found)];
70
+ }
71
+
72
+ /** Percent complete when the service reports it, else null. */
73
+ export function taskProgress(state: Record<string, unknown> | null): number | null {
74
+ if (!state) return null;
75
+ for (const value of findKeys(state.response ?? state, ['progress', 'percent', 'percentage'])) {
76
+ if (typeof value === 'boolean') continue;
77
+ if (typeof value === 'number') {
78
+ const pct = value > 0 && value <= 1 ? Math.round(value * 100) : Math.round(value);
79
+ return Math.max(0, Math.min(100, pct));
80
+ }
81
+ if (typeof value === 'string') {
82
+ const parsed = Number.parseFloat(value.trim().replace(/%$/, ''));
83
+ if (!Number.isNaN(parsed)) return Math.max(0, Math.min(100, Math.round(parsed)));
84
+ }
85
+ }
86
+ return null;
87
+ }
88
+
89
+ function* findKeys(node: unknown, names: string[], depth = 0): Generator<unknown> {
90
+ if (depth > 6) return;
91
+ if (Array.isArray(node)) {
92
+ for (const item of node) yield* findKeys(item, names, depth + 1);
93
+ } else if (node && typeof node === 'object') {
94
+ for (const [key, value] of Object.entries(node as Record<string, unknown>)) {
95
+ if (names.includes(key)) yield value;
96
+ else yield* findKeys(value, names, depth + 1);
97
+ }
98
+ }
99
+ }
100
+
101
+ /** The upstream's own words for why a task failed. */
102
+ export function failureReason(state: Record<string, unknown> | null): string {
103
+ if (!state) return 'Task failed.';
11
104
  const response = (state.response ?? state) as Record<string, unknown>;
12
- if (response.status === 'succeeded' || response.status === 'failed') return response.status;
13
- if (response.status !== undefined && response.status !== null) return '';
105
+ const error = response.error;
106
+ if (error && typeof error === 'object') {
107
+ const message = (error as Record<string, unknown>).message ?? (error as Record<string, unknown>).detail;
108
+ if (typeof message === 'string' && message) return message;
109
+ } else if (typeof error === 'string' && error) {
110
+ return error;
111
+ }
112
+ for (const key of ['message', 'failure_reason', 'fail_reason']) {
113
+ const value = response[key];
114
+ if (typeof value === 'string' && value) return value;
115
+ }
116
+ return 'Task failed.';
117
+ }
118
+
119
+ /**
120
+ * Reduce a poll response to succeeded | failed | '' (still running).
121
+ *
122
+ * Services report completion inconsistently. A status word is the strongest
123
+ * signal and outranks the `success` flag, since a response can carry
124
+ * `success: false` for a retryable hiccup while the task is still running.
125
+ * This normaliser is deliberately broad; narrowing it silently hangs whichever
126
+ * service it drops.
127
+ */
128
+ export function taskStatus(state: Record<string, unknown>): 'succeeded' | 'failed' | '' {
129
+ const response = (state.response ?? state) as Record<string, unknown> | null;
130
+ if (!response || typeof response !== 'object') return '';
131
+
132
+ const words = statusWords(response);
133
+ if (words.some((w) => FAILED.has(w))) return 'failed';
134
+ if (words.some((w) => DONE.has(w))) {
135
+ // A terminal word with no artifact means the job ended without output.
136
+ return artifactUrls(state).length > 0 ? 'succeeded' : 'failed';
137
+ }
138
+ if (words.length > 0) return '';
139
+
14
140
  const finished =
15
141
  (response.finished_at !== undefined && response.finished_at !== null) ||
16
142
  (state.finished_at !== undefined && state.finished_at !== null);
17
- if (!finished) return '';
18
- if (response.success === true) return 'succeeded';
19
- if (response.success === false) return 'failed';
20
- return '';
143
+ if (finished) {
144
+ if (response.success === true) return 'succeeded';
145
+ if (response.success === false) return 'failed';
146
+ if (artifactUrls(state).length > 0) return 'succeeded';
147
+ }
148
+
149
+ return artifactUrls(state).length > 0 ? 'succeeded' : '';
21
150
  }
22
151
 
23
152
  export class TaskHandle {
@@ -26,10 +155,33 @@ export class TaskHandle {
26
155
  private transport: Transport;
27
156
  private _result: Record<string, unknown> | null = null;
28
157
 
29
- constructor(taskId: string, pollEndpoint: string, transport: Transport) {
158
+ constructor(
159
+ taskId: string,
160
+ pollEndpoint: string,
161
+ transport: Transport,
162
+ submitted?: Record<string, unknown>,
163
+ ) {
30
164
  this.id = taskId;
31
165
  this.pollEndpoint = pollEndpoint;
32
166
  this.transport = transport;
167
+ // A submission that already carried the artifact is a finished task. The
168
+ // caller should not have to detect that and skip wait() themselves.
169
+ if (submitted && artifactUrls({ response: submitted }).length > 0) {
170
+ this._result = { response: submitted };
171
+ }
172
+ }
173
+
174
+ get done(): boolean {
175
+ return this._result !== null;
176
+ }
177
+
178
+ /** Artifact URLs, once completed. */
179
+ urls(): string[] {
180
+ return artifactUrls(this._result);
181
+ }
182
+
183
+ progress(): number | null {
184
+ return taskProgress(this._result);
33
185
  }
34
186
 
35
187
  async get(): Promise<Record<string, unknown>> {
@@ -39,12 +191,14 @@ export class TaskHandle {
39
191
  }
40
192
 
41
193
  async isCompleted(): Promise<boolean> {
42
- const state = await this.get();
43
- const status = taskStatus(state);
194
+ if (this.done) return true;
195
+ const status = taskStatus(await this.get());
44
196
  return status === 'succeeded' || status === 'failed';
45
197
  }
46
198
 
47
199
  async wait(opts: TaskHandleOptions = {}): Promise<Record<string, unknown>> {
200
+ if (this._result !== null) return this._result;
201
+
48
202
  const pollInterval = opts.pollInterval ?? 3000;
49
203
  const maxWait = opts.maxWait ?? 600_000;
50
204
  const start = Date.now();