@sogni-ai/sogni-client 5.60.2 → 5.60.4

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.
@@ -502,68 +502,83 @@ class Project extends DataEntity<ProjectData, ProjectEventMap> {
502
502
  * window, which is why a bare 404 must never be read as "lost".
503
503
  *
504
504
  * - socket says alive -> waiting is normal, keep the project alive.
505
- * - socket says gone AND REST has no record -> genuinely lost, count a strike.
506
- * - liveness unknown (older socket, unauthenticated, transport error) -> fall
507
- * back to the lenient rule: only non-404 REST failures count.
505
+ * - recovery confirms absence from the live list and both REST lookups ->
506
+ * genuinely lost, count a strike.
507
+ * - inconclusive recovery -> keep waiting without canceling the generation.
508
508
  */
509
509
  private async _runStalenessCheck() {
510
510
  const liveProjectIds = await this._api._listActiveProjectIds();
511
+ if (this.finished) return;
511
512
  if (liveProjectIds?.includes(this.id)) {
512
513
  // A queued project emits no events by design; this is not staleness.
513
514
  this._failedSyncAttempts = 0;
514
515
  this._keepAlive();
515
516
  return;
516
517
  }
517
- const socketConfirmsGone = liveProjectIds !== null;
518
-
519
- return this._syncToServer()
520
- .then(() => {
518
+ const queueRevision = this._queueRevision;
519
+ try {
520
+ const resolved = await this._api.resolveMissing([this.id], { attempts: 1, delayMs: 0 });
521
+ if (this.finished) return; // a live event may have settled it during recovery
522
+ const resolution = resolved[this.id];
523
+ if (resolution?.state === 'finished' || resolution?.state === 'terminal') {
524
+ const data =
525
+ resolution.state === 'finished'
526
+ ? resolution.project
527
+ : {
528
+ ...resolution.project,
529
+ status:
530
+ resolution.project.status === 'failed'
531
+ ? ('errored' as const)
532
+ : ('cancelled' as const)
533
+ };
534
+ await this._syncToServer(data, queueRevision);
535
+ }
536
+ if (resolution?.state !== 'lost') {
521
537
  this._failedSyncAttempts = 0;
522
- })
523
- .catch((error) => {
524
- // A 404 alone is ambiguous: it is the normal state for a queued project
525
- // as well as for a lost one. It only becomes evidence of loss when the
526
- // socket has also confirmed the project is no longer in flight.
527
- if (error.status === 404 && !socketConfirmsGone) {
528
- return;
529
- }
530
- if (error.status !== 404) {
531
- this._logger.error(error);
532
- }
533
- this._failedSyncAttempts++;
534
- if (this._failedSyncAttempts >= MAX_FAILED_SYNC_ATTEMPTS) {
535
- this._logger.error(
536
- `Failed to sync project data after ${MAX_FAILED_SYNC_ATTEMPTS} attempts. Stopping further attempts.`
537
- );
538
- this._api._notifyProjectTimedOut(this.id).catch((cancelError) => {
539
- this._logger.error(`Failed to notify socket server that project ${this.id} timed out`);
540
- this._logger.error(cancelError);
541
- });
542
- clearInterval(this._timeout!);
543
- this._timeout = null;
544
- this.jobs.forEach((job) => {
545
- if (!job.finished) {
546
- job._update({
547
- status: 'failed',
548
- error: { code: 0, message: 'Job timed out' }
549
- });
550
- }
551
- });
552
- this._update({
553
- status: 'failed',
554
- error: { code: 0, message: 'Project timed out. Please try again or contact support.' }
555
- });
556
- }
557
- });
538
+ this._keepAlive();
539
+ return;
540
+ }
541
+ this._failedSyncAttempts++;
542
+ if (this._failedSyncAttempts >= MAX_FAILED_SYNC_ATTEMPTS) {
543
+ this._logger.error(
544
+ `Failed to sync project data after ${MAX_FAILED_SYNC_ATTEMPTS} attempts. Stopping further attempts.`
545
+ );
546
+ this._api._notifyProjectTimedOut(this.id).catch((cancelError) => {
547
+ this._logger.error(`Failed to notify socket server that project ${this.id} timed out`);
548
+ this._logger.error(cancelError);
549
+ });
550
+ clearInterval(this._timeout!);
551
+ this._timeout = null;
552
+ this.jobs.forEach((job) => {
553
+ if (!job.finished) {
554
+ job._update({
555
+ status: 'failed',
556
+ error: { code: 0, message: 'Job timed out' }
557
+ });
558
+ }
559
+ });
560
+ this._update({
561
+ status: 'failed',
562
+ error: { code: 0, message: 'Project timed out. Please try again or contact support.' }
563
+ });
564
+ }
565
+ } catch (error) {
566
+ // A failed request is not evidence that the server lost the generation.
567
+ this._failedSyncAttempts = 0;
568
+ this._logger.error(error);
569
+ }
558
570
  }
559
571
 
560
572
  /**
561
573
  * Sync project data with the data received from the REST API.
562
574
  * @internal
563
575
  */
564
- async _syncToServer() {
565
- const queueRevision = this._queueRevision;
566
- const data = await this._api.get(this.id);
576
+ async _syncToServer(
577
+ snapshot?: Partial<RawProject> & Pick<RawProject, 'status' | 'completedWorkerJobs'>,
578
+ queueRevision = this._queueRevision
579
+ ) {
580
+ const wasFinished = this.finished;
581
+ const data = snapshot ?? (await this._api.get(this.id));
567
582
  this._setQueueState(data.waitingReason, data.jobWaitingReasons, queueRevision);
568
583
  const jobData = data.completedWorkerJobs.reduce((acc: Record<string, RawJob>, job) => {
569
584
  const jobId = job.imgID || getUUID();
@@ -589,11 +604,18 @@ class Project extends DataEntity<ProjectData, ProjectEventMap> {
589
604
  // If there are any jobs left in jobData, it means they are new jobs that are not in the project yet
590
605
  if (Object.keys(jobData).length) {
591
606
  for (const job of Object.values(jobData)) {
592
- const jobInstance = Job.fromRaw(data, job, {
593
- api: this._api,
594
- logger: this._logger,
595
- project: this
596
- });
607
+ const jobInstance = Job.fromRaw(
608
+ {
609
+ id: this.id,
610
+ stepCount: data.stepCount ?? this.params.steps ?? 0
611
+ },
612
+ job,
613
+ {
614
+ api: this._api,
615
+ logger: this._logger,
616
+ project: this
617
+ }
618
+ );
597
619
  this._addJob(jobInstance);
598
620
  // A job discovered for the first time through the REST snapshot needs
599
621
  // the same sync a tracked job gets. `Job.fromRaw` only copies the
@@ -611,14 +633,15 @@ class Project extends DataEntity<ProjectData, ProjectEventMap> {
611
633
  }
612
634
  }
613
635
 
636
+ if (!wasFinished && this.finished) return;
614
637
  const delta: Partial<ProjectData> = {
615
638
  params: {
616
639
  ...this.data.params,
617
- numberOfMedia: data.imageCount,
618
- steps: data.stepCount
640
+ ...(data.imageCount !== undefined ? { numberOfMedia: data.imageCount } : {}),
641
+ ...(data.stepCount !== undefined ? { steps: data.stepCount } : {})
619
642
  }
620
643
  };
621
- if (delta.params && isImageParams(delta.params)) {
644
+ if (delta.params && isImageParams(delta.params) && data.previewCount !== undefined) {
622
645
  delta.params.numberOfPreviews = data.previewCount;
623
646
  }
624
647
  if (PROJECT_STATUS_MAP[data.status]) {
@@ -906,6 +906,9 @@ class ProjectsApi extends ApiGroup<ProjectApiEvents> {
906
906
  this.emit('project', { type: 'completed', projectId: data.jobID });
907
907
  return;
908
908
  case 'initiatingModel':
909
+ // LLM startup notifications share this channel but have no render id.
910
+ // ChatApi handles those; only media frames can produce a JobEvent.
911
+ if (typeof data.imgID !== 'string') return;
909
912
  this.emit('job', {
910
913
  type: 'initiating',
911
914
  projectId: data.jobID,
@@ -918,6 +921,7 @@ class ProjectsApi extends ApiGroup<ProjectApiEvents> {
918
921
  });
919
922
  return;
920
923
  case 'jobStarted': {
924
+ if (typeof data.imgID !== 'string') return;
921
925
  this.emit('job', {
922
926
  type: 'started',
923
927
  projectId: data.jobID,
@@ -1753,9 +1757,8 @@ class ProjectsApi extends ApiGroup<ProjectApiEvents> {
1753
1757
  // Before failing anything, ask the owner-scoped live lookup. It can
1754
1758
  // confirm an active project or a terminal failure/cancellation without
1755
1759
  // a full record. A successful completion still needs its result record
1756
- // and stays unverified until that arrives. Anything else, including
1757
- // an unauthenticated client or an older API without the lookup, keeps the
1758
- // `lost` verdict.
1760
+ // and stays unverified until that arrives. Failed lookups are inconclusive;
1761
+ // only a 404 together with an available live list can confirm absence.
1759
1762
  const unlisted = pending.filter((id) => !live?.includes(id));
1760
1763
  const checks = new Map(
1761
1764
  await Promise.all(
@@ -1773,6 +1776,13 @@ class ProjectsApi extends ApiGroup<ProjectApiEvents> {
1773
1776
  result[id] = check;
1774
1777
  continue;
1775
1778
  }
1779
+ if (live === null) {
1780
+ result[id] = {
1781
+ state: 'unknown',
1782
+ error: new Error('The live project list is not available')
1783
+ };
1784
+ continue;
1785
+ }
1776
1786
  // Nothing on the server knows it. A request that died with a dropped
1777
1787
  // connection is sent again; one just (re)sent is still being admitted.
1778
1788
  result[id] =
@@ -1842,7 +1852,8 @@ class ProjectsApi extends ApiGroup<ProjectApiEvents> {
1842
1852
 
1843
1853
  /**
1844
1854
  * Second opinion for a project neither the terminal REST record nor the live
1845
- * socket list knows. Returns `undefined` when the lookup cannot vouch for it.
1855
+ * socket list knows. Returns `undefined` for a 404; other lookup failures
1856
+ * remain unknown, preserving the error and any retry-after information.
1846
1857
  */
1847
1858
  private async _lookupUnlistedProject(projectId: string): Promise<ProjectResolution | undefined> {
1848
1859
  try {
@@ -1864,8 +1875,8 @@ class ProjectsApi extends ApiGroup<ProjectApiEvents> {
1864
1875
  };
1865
1876
  }
1866
1877
  return undefined;
1867
- } catch {
1868
- return undefined;
1878
+ } catch (error: any) {
1879
+ return error?.status === 404 ? undefined : { state: 'unknown', error };
1869
1880
  }
1870
1881
  }
1871
1882
 
@@ -2744,7 +2755,8 @@ class ProjectsApi extends ApiGroup<ProjectApiEvents> {
2744
2755
  };
2745
2756
  projects.set(projectId, project);
2746
2757
  }
2747
- const finishedAt = typeof job.endTime === 'number' && job.endTime > 0 ? job.endTime : undefined;
2758
+ const finishedAt =
2759
+ typeof job.endTime === 'number' && job.endTime > 0 ? job.endTime : undefined;
2748
2760
  project.jobs.push({
2749
2761
  id: job.imgID || job.id,
2750
2762
  status: resultJobStatus(job),
@@ -3449,7 +3461,8 @@ class ProjectsApi extends ApiGroup<ProjectApiEvents> {
3449
3461
  );
3450
3462
  }
3451
3463
  const keyframeCount =
3452
- params.keyframeCount ?? (Array.isArray(params.keyframes) ? params.keyframes.length : undefined);
3464
+ params.keyframeCount ??
3465
+ (Array.isArray(params.keyframes) ? params.keyframes.length : undefined);
3453
3466
  if (Number.isFinite(keyframeCount) && (keyframeCount as number) > 0) {
3454
3467
  query.set('keyframeCount', String(Math.floor(keyframeCount as number)));
3455
3468
  }
@@ -4,14 +4,14 @@ interface ErrorData {
4
4
  code: number;
5
5
  originalCode?: string;
6
6
  message: string;
7
- /** Coarse server-reported cause of an external generation failure. */
8
- vendorFailureCategory?:
9
- | 'content_policy'
10
- | 'input_validation'
11
- | 'timeout'
12
- | 'result_storage'
13
- | 'cancelled'
14
- | 'vendor_failed';
7
+ /**
8
+ * Coarse server-reported cause of an external generation failure. Known
9
+ * values include `content_policy`, `input_validation`, `timeout`,
10
+ * `result_storage`, `cancelled`, `vendor_failed`, `asset_resolution`, and
11
+ * `vendor_transient`. Servers may add categories; handle unknown strings
12
+ * with a generic failure message.
13
+ */
14
+ vendorFailureCategory?: string;
15
15
  /**
16
16
  * Discriminator set to `true` when this render-path error is a subscription
17
17
  * FEATURE-gate denial (socket error code 4081). The four fields below are
package/src/version.ts CHANGED
@@ -1,3 +1,3 @@
1
1
  // AUTO-GENERATED by scripts/generate-version.mjs — do not edit.
2
2
  // Source of truth: package.json version.
3
- export const LIB_VERSION = '5.60.2';
3
+ export const LIB_VERSION = '5.60.4';