@sogni-ai/sogni-client 0.3.3 → 0.4.0-aplha.10

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.
Files changed (54) hide show
  1. package/README.md +3 -2
  2. package/dist/Account/CurrentAccount.d.ts +12 -2
  3. package/dist/Account/CurrentAccount.js.map +1 -1
  4. package/dist/Account/index.d.ts +130 -7
  5. package/dist/Account/index.js +135 -7
  6. package/dist/Account/index.js.map +1 -1
  7. package/dist/ApiClient/WebSocketClient/events.d.ts +7 -1
  8. package/dist/ApiClient/WebSocketClient/index.d.ts +1 -1
  9. package/dist/ApiClient/WebSocketClient/index.js +14 -5
  10. package/dist/ApiClient/WebSocketClient/index.js.map +1 -1
  11. package/dist/ApiClient/WebSocketClient/messages.d.ts +2 -0
  12. package/dist/Projects/Job.d.ts +25 -1
  13. package/dist/Projects/Job.js +78 -1
  14. package/dist/Projects/Job.js.map +1 -1
  15. package/dist/Projects/Project.d.ts +21 -3
  16. package/dist/Projects/Project.js +110 -2
  17. package/dist/Projects/Project.js.map +1 -1
  18. package/dist/Projects/createJobRequestMessage.d.ts +1 -61
  19. package/dist/Projects/createJobRequestMessage.js +5 -1
  20. package/dist/Projects/createJobRequestMessage.js.map +1 -1
  21. package/dist/Projects/index.d.ts +22 -3
  22. package/dist/Projects/index.js +82 -14
  23. package/dist/Projects/index.js.map +1 -1
  24. package/dist/Projects/types/RawProject.d.ts +87 -0
  25. package/dist/Projects/types/RawProject.js +3 -0
  26. package/dist/Projects/types/RawProject.js.map +1 -0
  27. package/dist/Projects/types/events.d.ts +2 -0
  28. package/dist/Projects/types/index.d.ts +4 -0
  29. package/dist/lib/DataEntity.d.ts +1 -0
  30. package/dist/lib/DataEntity.js +2 -0
  31. package/dist/lib/DataEntity.js.map +1 -1
  32. package/dist/lib/base64.js +8 -6
  33. package/dist/lib/base64.js.map +1 -1
  34. package/dist/types/ErrorData.d.ts +1 -0
  35. package/dist/version.d.ts +1 -1
  36. package/dist/version.js +1 -1
  37. package/dist/version.js.map +1 -1
  38. package/package.json +1 -1
  39. package/src/Account/CurrentAccount.ts +11 -1
  40. package/src/Account/index.ts +137 -9
  41. package/src/ApiClient/WebSocketClient/events.ts +5 -1
  42. package/src/ApiClient/WebSocketClient/index.ts +15 -6
  43. package/src/ApiClient/WebSocketClient/messages.ts +2 -0
  44. package/src/Projects/Job.ts +90 -1
  45. package/src/Projects/Project.ts +134 -5
  46. package/src/Projects/createJobRequestMessage.ts +5 -1
  47. package/src/Projects/index.ts +87 -16
  48. package/src/Projects/types/RawProject.ts +121 -0
  49. package/src/Projects/types/events.ts +2 -0
  50. package/src/Projects/types/index.ts +4 -0
  51. package/src/lib/DataEntity.ts +3 -0
  52. package/src/lib/base64.ts +8 -4
  53. package/src/types/ErrorData.ts +1 -0
  54. package/src/version.ts +1 -1
@@ -17,6 +17,14 @@ import { SupernetType } from '../ApiClient/WebSocketClient/types';
17
17
 
18
18
  /**
19
19
  * Account API methods that let you interact with the user's account.
20
+ * Can be accessed via `client.account`. Look for more samples below.
21
+ *
22
+ * @example Retrieve the current account balance
23
+ * ```typescript
24
+ * const balance = await client.account.refreshBalance();
25
+ * console.log(balance);
26
+ * ```
27
+ *
20
28
  */
21
29
  class AccountApi extends ApiGroup {
22
30
  readonly currentAccount = new CurrentAccount();
@@ -43,13 +51,27 @@ class AccountApi extends ApiGroup {
43
51
  this.currentAccount._clear();
44
52
  }
45
53
 
46
- async getNonce(walletAddress: string): Promise<string> {
54
+ private async getNonce(walletAddress: string): Promise<string> {
47
55
  const res = await this.client.rest.post<ApiReponse<Nonce>>('/v1/account/nonce', {
48
56
  walletAddress
49
57
  });
50
58
  return res.data.nonce;
51
59
  }
52
60
 
61
+ /**
62
+ * Create Ethers.js Wallet instance from username and password.
63
+ * This method is used internally to create a wallet for the user.
64
+ * You can use this method to create a wallet if you need to sign transactions.
65
+ *
66
+ * @example Create a wallet from username and password
67
+ * ```typescript
68
+ * const wallet = client.account.getWallet('username', 'password');
69
+ * console.log(wallet.address);
70
+ * ```
71
+ *
72
+ * @param username - Sogni account username
73
+ * @param password - Sogni account password
74
+ */
53
75
  getWallet(username: string, password: string): Wallet {
54
76
  const pwd = toUtf8Bytes(username.toLowerCase() + password);
55
77
  const salt = toUtf8Bytes('sogni-salt-value');
@@ -57,6 +79,16 @@ class AccountApi extends ApiGroup {
57
79
  return new Wallet(pkey, this.provider);
58
80
  }
59
81
 
82
+ /**
83
+ * Create a new account with the given username, email, and password.
84
+ * @internal
85
+ *
86
+ * @param username
87
+ * @param email
88
+ * @param password
89
+ * @param subscribe
90
+ * @param referralCode
91
+ */
60
92
  async create(
61
93
  username: string,
62
94
  email: string,
@@ -83,6 +115,31 @@ class AccountApi extends ApiGroup {
83
115
  return res.data;
84
116
  }
85
117
 
118
+ /**
119
+ * Restore session with username and access token.
120
+ *
121
+ * You can save access token that you get from the login method and restore the session with this method.
122
+ *
123
+ * @example Store access token to local storage
124
+ * ```typescript
125
+ * const { username, token } = await client.account.login('username', 'password');
126
+ * localStorage.setItem('sogni-username', username);
127
+ * localStorage.setItem('sogni-token', token);
128
+ * ```
129
+ *
130
+ * @example Restore session from local storage
131
+ * ```typescript
132
+ * const username = localStorage.getItem('sogni-username');
133
+ * const token = localStorage.getItem('sogni-token');
134
+ * if (username && token) {
135
+ * client.account.setToken(username, token);
136
+ * console.log('Session restored');
137
+ * }
138
+ * ```
139
+ *
140
+ * @param username
141
+ * @param token
142
+ */
86
143
  setToken(username: string, token: string): void {
87
144
  this.client.authenticate(token);
88
145
  this.currentAccount._update({
@@ -93,6 +150,13 @@ class AccountApi extends ApiGroup {
93
150
 
94
151
  /**
95
152
  * Login with username and password. WebSocket connection is established after successful login.
153
+ *
154
+ * @example Login with username and password
155
+ * ```typescript
156
+ * await client.account.login('username', 'password');
157
+ * console.log('Logged in');
158
+ * ```
159
+ *
96
160
  * @param username
97
161
  * @param password
98
162
  */
@@ -113,6 +177,12 @@ class AccountApi extends ApiGroup {
113
177
 
114
178
  /**
115
179
  * Logout the user and close the WebSocket connection.
180
+ *
181
+ * @example Logout the user
182
+ * ```typescript
183
+ * await client.account.logout();
184
+ * console.log('Logged out');
185
+ * ```
116
186
  */
117
187
  async logout(): Promise<void> {
118
188
  this.client.rest.post('/v1/account/logout').catch((e) => {
@@ -124,6 +194,16 @@ class AccountApi extends ApiGroup {
124
194
 
125
195
  /**
126
196
  * Refresh the balance of the current account.
197
+ *
198
+ * Usually, you don't need to call this method manually. Balance is updated automatically
199
+ * through WebSocket events. But you can call this method to force a balance refresh.
200
+ *
201
+ * @example Refresh user account balance
202
+ * ```typescript
203
+ * const balance = await client.account.refreshBalance();
204
+ * console.log(balance);
205
+ * // { net: '100.000000', settled: '100.000000', credit: '0.000000', debit: '0.000000' }
206
+ * ```
127
207
  */
128
208
  async refreshBalance(): Promise<BalanceData> {
129
209
  const res = await this.client.rest.get<ApiReponse<BalanceData>>('/v1/account/balance');
@@ -132,7 +212,18 @@ class AccountApi extends ApiGroup {
132
212
  }
133
213
 
134
214
  /**
135
- * Get the balance of a account wallet.
215
+ * Get the balance of the wallet address.
216
+ *
217
+ * This method is used to get the balance of the wallet address. It returns $SOGNI and ETH balance.
218
+ *
219
+ * @example Get the balance of the wallet address
220
+ * ```typescript
221
+ * const address = client.account.currentAccount.walletAddress;
222
+ * const balance = await client.account.walletBalance(address);
223
+ * console.log(balance);
224
+ * // { token: '100.000000', ether: '0.000000' }
225
+ * ```
226
+ *
136
227
  * @param walletAddress
137
228
  */
138
229
  async walletBalance(walletAddress: string) {
@@ -145,6 +236,11 @@ class AccountApi extends ApiGroup {
145
236
  return res.data;
146
237
  }
147
238
 
239
+ /**
240
+ * Validate the username before signup
241
+ * @internal
242
+ * @param username
243
+ */
148
244
  async validateUsername(username: string) {
149
245
  try {
150
246
  return await this.client.rest.post<ApiReponse<undefined>>('/v1/account/username/validate', {
@@ -163,21 +259,44 @@ class AccountApi extends ApiGroup {
163
259
 
164
260
  /**
165
261
  * Switch between fast and relaxed networks.
166
- * Note: This method will close the current WebSocket connection and establish a new one.
167
- * Do not call this method if you have any active projects.
168
- * @param network
262
+ * This will change default network used to process projects. After switching, you will updated
263
+ * list of AI models available for on selected network.
264
+ *
265
+ * @example Switch to the fast network
266
+ * ```typescript
267
+ * await client.account.switchNetwork('fast');
268
+ * console.log('Switched to the fast network, now lets wait until we get list of models');
269
+ * await client.projects.waitForModels();
270
+ * ```
271
+ * @param network - Network type to switch to
169
272
  */
170
- async switchNetwork(network: SupernetType) {
273
+ async switchNetwork(network: SupernetType): Promise<SupernetType> {
171
274
  this.currentAccount._update({
172
- networkStatus: 'connecting',
275
+ networkStatus: 'switching',
173
276
  network: null
174
277
  });
175
- this.client.socket.switchNetwork(network);
278
+ const newNetwork = await this.client.socket.switchNetwork(network);
279
+ this.currentAccount._update({
280
+ networkStatus: 'connected',
281
+ network: newNetwork
282
+ });
283
+ return newNetwork;
176
284
  }
177
285
 
178
286
  /**
179
287
  * Get the transaction history of the current account.
180
- * @param params
288
+ *
289
+ * @example Get the transaction history
290
+ * ```typescript
291
+ * const { entries, next } = await client.account.transactionHistory({
292
+ * status: 'completed',
293
+ * limit: 10,
294
+ * address: client.account.currentAccount.walletAddress
295
+ * });
296
+ * ```
297
+ *
298
+ * @param params - Transaction history query parameters
299
+ * @returns Transaction history entries and next query parameters
181
300
  */
182
301
  async transactionHistory(
183
302
  params: TxHistoryParams
@@ -211,6 +330,10 @@ class AccountApi extends ApiGroup {
211
330
  };
212
331
  }
213
332
 
333
+ /**
334
+ * Get the rewards of the current account.
335
+ * @internal
336
+ */
214
337
  async rewards(): Promise<Reward[]> {
215
338
  const r =
216
339
  await this.client.rest.get<ApiReponse<{ rewards: RewardRaw[] }>>('/v2/account/rewards');
@@ -233,6 +356,11 @@ class AccountApi extends ApiGroup {
233
356
  );
234
357
  }
235
358
 
359
+ /**
360
+ * Claim rewards by reward IDs.
361
+ * @internal
362
+ * @param rewardIds
363
+ */
236
364
  async claimRewards(rewardIds: string[]): Promise<void> {
237
365
  await this.client.rest.post('/v2/account/reward/claim', {
238
366
  claims: rewardIds
@@ -12,7 +12,7 @@ export type JobErrorData = {
12
12
  imgID?: string;
13
13
  isFromWorker: boolean;
14
14
  error_message: string;
15
- error: number;
15
+ error: number | string;
16
16
  };
17
17
 
18
18
  export type JobProgressData = {
@@ -63,6 +63,10 @@ export type SocketEventMap = {
63
63
  * @event WebSocketClient#balanceUpdate - Received balance update
64
64
  */
65
65
  balanceUpdate: BalanceData;
66
+ /**
67
+ * @event WebSocketClient#changeNetwork - Default network changed
68
+ */
69
+ changeNetwork: { network: SupernetType };
66
70
  /**
67
71
  * @event WebSocketClient#jobError - Job error occurred
68
72
  */
@@ -19,9 +19,13 @@ class WebSocketClient extends RestClient<SocketEventMap> {
19
19
  private _pingInterval: NodeJS.Timeout | null = null;
20
20
 
21
21
  constructor(baseUrl: string, appId: string, supernetType: SupernetType, logger: Logger) {
22
- super(baseUrl, logger);
22
+ const _baseUrl = new URL(baseUrl);
23
+ if (_baseUrl.protocol === 'wss:') {
24
+ _baseUrl.protocol = 'https:';
25
+ }
26
+ super(_baseUrl.toString(), logger);
23
27
  this.appId = appId;
24
- this.baseUrl = baseUrl;
28
+ this.baseUrl = _baseUrl.toString();
25
29
  this._supernetType = supernetType;
26
30
  }
27
31
 
@@ -56,6 +60,7 @@ class WebSocketClient extends RestClient<SocketEventMap> {
56
60
  }
57
61
  const userAgent = `Sogni/${LIB_VERSION} (sogni-client)`;
58
62
  const url = new URL(this.baseUrl);
63
+ url.protocol = 'wss:';
59
64
  url.searchParams.set('appId', this.appId);
60
65
  url.searchParams.set('clientName', userAgent);
61
66
  url.searchParams.set('clientType', 'artist');
@@ -108,10 +113,14 @@ class WebSocketClient extends RestClient<SocketEventMap> {
108
113
  }
109
114
  }
110
115
 
111
- switchNetwork(supernetType: SupernetType) {
112
- this._supernetType = supernetType;
113
- this.disconnect();
114
- this.connect();
116
+ switchNetwork(supernetType: SupernetType): Promise<SupernetType> {
117
+ return new Promise<SupernetType>(async (resolve, reject) => {
118
+ this.once('changeNetwork', ({ network }) => {
119
+ this._supernetType = network;
120
+ resolve(network);
121
+ });
122
+ await this.send('changeNetwork', supernetType);
123
+ });
115
124
  }
116
125
 
117
126
  /**
@@ -1,7 +1,9 @@
1
1
  import { JobRequestRaw } from '../../Projects/createJobRequestMessage';
2
+ import { SupernetType } from './types';
2
3
 
3
4
  export interface SocketMessageMap {
4
5
  jobRequest: JobRequestRaw;
6
+ changeNetwork: SupernetType;
5
7
  }
6
8
 
7
9
  export type MessageType = keyof SocketMessageMap;
@@ -1,5 +1,9 @@
1
1
  import DataEntity, { EntityEvents } from '../lib/DataEntity';
2
2
  import ErrorData from '../types/ErrorData';
3
+ import { RawJob, RawProject } from './types/RawProject';
4
+ import ProjectsApi from './index';
5
+ import { Logger } from '../lib/DefaultLogger';
6
+ import getUUID from '../lib/getUUID';
3
7
 
4
8
  export type JobStatus =
5
9
  | 'pending'
@@ -9,14 +13,27 @@ export type JobStatus =
9
13
  | 'failed'
10
14
  | 'canceled';
11
15
 
16
+ const JOB_STATUS_MAP: Record<RawJob['status'], JobStatus> = {
17
+ created: 'pending',
18
+ queued: 'pending',
19
+ assigned: 'initiating',
20
+ initiatingModel: 'initiating',
21
+ jobStarted: 'processing',
22
+ jobProgress: 'processing',
23
+ jobCompleted: 'completed',
24
+ jobError: 'failed'
25
+ };
26
+
12
27
  /**
13
28
  * @inline
14
29
  */
15
30
  export interface JobData {
16
31
  id: string;
32
+ projectId: string;
17
33
  status: JobStatus;
18
34
  step: number;
19
35
  stepCount: number;
36
+ workerName?: string;
20
37
  seed?: number;
21
38
  isNSFW?: boolean;
22
39
  userCanceled?: boolean;
@@ -31,9 +48,37 @@ export interface JobEventMap extends EntityEvents {
31
48
  failed: ErrorData;
32
49
  }
33
50
 
51
+ export interface JobOptions {
52
+ api: ProjectsApi;
53
+ logger: Logger;
54
+ }
55
+
34
56
  class Job extends DataEntity<JobData, JobEventMap> {
35
- constructor(data: JobData) {
57
+ static fromRaw(rawProject: RawProject, rawJob: RawJob, options: JobOptions) {
58
+ return new Job(
59
+ {
60
+ id: rawJob.imgID || getUUID(),
61
+ projectId: rawProject.id,
62
+ status: JOB_STATUS_MAP[rawJob.status],
63
+ step: rawJob.performedSteps,
64
+ stepCount: rawProject.stepCount,
65
+ workerName: rawJob.worker.name,
66
+ seed: rawJob.seedUsed,
67
+ isNSFW: rawJob.triggeredNSFWFilter
68
+ },
69
+ options
70
+ );
71
+ }
72
+
73
+ private readonly _api: ProjectsApi;
74
+ private readonly _logger: Logger;
75
+
76
+ constructor(data: JobData, options: JobOptions) {
36
77
  super(data);
78
+
79
+ this._api = options.api;
80
+ this._logger = options.logger;
81
+
37
82
  this.on('updated', this.handleUpdated.bind(this));
38
83
  }
39
84
 
@@ -41,6 +86,10 @@ class Job extends DataEntity<JobData, JobEventMap> {
41
86
  return this.data.id;
42
87
  }
43
88
 
89
+ get projectId() {
90
+ return this.data.projectId;
91
+ }
92
+
44
93
  /**
45
94
  * Current status of the job.
46
95
  */
@@ -48,6 +97,10 @@ class Job extends DataEntity<JobData, JobEventMap> {
48
97
  return this.data.status;
49
98
  }
50
99
 
100
+ get finished() {
101
+ return ['completed', 'failed', 'canceled'].includes(this.status);
102
+ }
103
+
51
104
  /**
52
105
  * Progress of the job in percentage (0-100).
53
106
  */
@@ -108,6 +161,42 @@ class Job extends DataEntity<JobData, JobEventMap> {
108
161
  return !!this.data.isNSFW;
109
162
  }
110
163
 
164
+ /**
165
+ * Name of the worker that is processing this job.
166
+ */
167
+ get workerName() {
168
+ return this.data.workerName;
169
+ }
170
+
171
+ /**
172
+ * Syncs the job data with the data received from the REST API.
173
+ * @internal
174
+ * @param data
175
+ */
176
+ async _syncWithRestData(data: RawJob) {
177
+ const delta: Partial<JobData> = {
178
+ step: data.performedSteps,
179
+ workerName: data.worker.name,
180
+ seed: data.seedUsed,
181
+ isNSFW: data.triggeredNSFWFilter
182
+ };
183
+ if (JOB_STATUS_MAP[data.status]) {
184
+ delta.status = JOB_STATUS_MAP[data.status];
185
+ }
186
+ if (!this.data.resultUrl && delta.status === 'completed' && !data.triggeredNSFWFilter) {
187
+ try {
188
+ delta.resultUrl = await this._api.downloadUrl({
189
+ jobId: this.projectId,
190
+ imageId: this.id,
191
+ type: 'complete'
192
+ });
193
+ } catch (error) {
194
+ this._logger.error(error);
195
+ }
196
+ }
197
+ this._update(delta);
198
+ }
199
+
111
200
  private handleUpdated(keys: string[]) {
112
201
  if (keys.includes('step') || keys.includes('stepCount')) {
113
202
  this.emit('progress', this.progress);
@@ -1,11 +1,34 @@
1
- import Job, { JobData } from './Job';
1
+ import Job, { JobData, JobStatus } from './Job';
2
2
  import DataEntity, { EntityEvents } from '../lib/DataEntity';
3
3
  import { ProjectParams } from './types';
4
4
  import cloneDeep from 'lodash/cloneDeep';
5
5
  import ErrorData from '../types/ErrorData';
6
6
  import getUUID from '../lib/getUUID';
7
+ import { RawJob, RawProject } from './types/RawProject';
8
+ import ProjectsApi from './index';
9
+ import { Logger } from '../lib/DefaultLogger';
7
10
 
8
- export type ProjectStatus = 'pending' | 'queued' | 'processing' | 'completed' | 'failed';
11
+ // If project is not finished and had no updates for 1 minute, force refresh
12
+ const PROJECT_TIMEOUT = 60 * 1000;
13
+ const MAX_FAILED_SYNC_ATTEMPTS = 3;
14
+
15
+ export type ProjectStatus =
16
+ | 'pending'
17
+ | 'queued'
18
+ | 'processing'
19
+ | 'completed'
20
+ | 'failed'
21
+ | 'canceled';
22
+
23
+ const PROJECT_STATUS_MAP: Record<RawProject['status'], ProjectStatus> = {
24
+ pending: 'pending',
25
+ active: 'queued',
26
+ assigned: 'processing',
27
+ progress: 'processing',
28
+ completed: 'completed',
29
+ errored: 'failed',
30
+ cancelled: 'canceled'
31
+ };
9
32
 
10
33
  /**
11
34
  * @inline
@@ -31,11 +54,20 @@ export interface ProjectEventMap extends EntityEvents {
31
54
  jobFailed: Job;
32
55
  }
33
56
 
57
+ export interface ProjectOptions {
58
+ api: ProjectsApi;
59
+ logger: Logger;
60
+ }
61
+
34
62
  class Project extends DataEntity<ProjectData, ProjectEventMap> {
35
63
  private _jobs: Job[] = [];
36
64
  private _lastEmitedProgress = -1;
65
+ private readonly _api: ProjectsApi;
66
+ private readonly _logger: Logger;
67
+ private _timeout: NodeJS.Timeout | null = null;
68
+ private _failedSyncAttempts = 0;
37
69
 
38
- constructor(data: ProjectParams) {
70
+ constructor(data: ProjectParams, options: ProjectOptions) {
39
71
  super({
40
72
  id: getUUID(),
41
73
  startedAt: new Date(),
@@ -44,6 +76,11 @@ class Project extends DataEntity<ProjectData, ProjectEventMap> {
44
76
  status: 'pending'
45
77
  });
46
78
 
79
+ this._api = options.api;
80
+ this._logger = options.logger;
81
+
82
+ this._timeout = setInterval(this._checkForTimeout.bind(this), PROJECT_TIMEOUT);
83
+
47
84
  this.on('updated', this.handleUpdated.bind(this));
48
85
  }
49
86
 
@@ -59,6 +96,10 @@ class Project extends DataEntity<ProjectData, ProjectEventMap> {
59
96
  return this.data.status;
60
97
  }
61
98
 
99
+ get finished() {
100
+ return ['completed', 'failed', 'canceled'].includes(this.status);
101
+ }
102
+
62
103
  get error() {
63
104
  return this.data.error;
64
105
  }
@@ -137,6 +178,11 @@ class Project extends DataEntity<ProjectData, ProjectEventMap> {
137
178
  this.emit('progress', progress);
138
179
  this._lastEmitedProgress = progress;
139
180
  }
181
+ // If project is finished stop watching for timeout
182
+ if (this._timeout && this.finished) {
183
+ clearInterval(this._timeout!);
184
+ this._timeout = null;
185
+ }
140
186
  if (keys.includes('status') || keys.includes('jobs')) {
141
187
  const allJobsDone = this.jobs.every((job) =>
142
188
  ['completed', 'failed', 'canceled'].includes(job.status)
@@ -155,21 +201,104 @@ class Project extends DataEntity<ProjectData, ProjectEventMap> {
155
201
  * @internal
156
202
  * @param data
157
203
  */
158
- _addJob(data: JobData) {
159
- const job = new Job(data);
204
+ _addJob(data: JobData | Job) {
205
+ const job =
206
+ data instanceof Job ? data : new Job(data, { api: this._api, logger: this._logger });
160
207
  this._jobs.push(job);
161
208
  job.on('updated', () => {
209
+ this.lastUpdated = new Date();
162
210
  this.emit('updated', ['jobs']);
163
211
  });
164
212
  job.on('completed', () => {
165
213
  this.emit('jobCompleted', job);
214
+ this._handleJobFinished(job);
166
215
  });
167
216
  job.on('failed', () => {
168
217
  this.emit('jobFailed', job);
218
+ this._handleJobFinished(job);
169
219
  });
170
220
  return job;
171
221
  }
172
222
 
223
+ private _handleJobFinished(job: Job) {
224
+ const finalStatus: JobStatus[] = ['completed', 'failed', 'canceled'];
225
+ const allJobsDone = this.jobs.every((job) => finalStatus.includes(job.status));
226
+ // If all jobs are done and project is not already failed or completed, update the project status
227
+ if (allJobsDone && this.status !== 'failed' && this.status !== 'completed') {
228
+ const allJobsFailed = this.jobs.every((job) => job.status === 'failed');
229
+ if (allJobsFailed) {
230
+ this._update({ status: 'failed' });
231
+ } else {
232
+ this._update({ status: 'completed' });
233
+ }
234
+ }
235
+ }
236
+
237
+ private _checkForTimeout() {
238
+ if (this.lastUpdated.getTime() + PROJECT_TIMEOUT < Date.now()) {
239
+ this._syncToServer().catch((error) => {
240
+ this._logger.error(error);
241
+ this._failedSyncAttempts++;
242
+ if (this._failedSyncAttempts > MAX_FAILED_SYNC_ATTEMPTS) {
243
+ this._logger.error(
244
+ `Failed to sync project data after ${MAX_FAILED_SYNC_ATTEMPTS} attempts. Stopping further attempts.`
245
+ );
246
+ clearInterval(this._timeout!);
247
+ this._timeout = null;
248
+ }
249
+ });
250
+ }
251
+ }
252
+
253
+ /**
254
+ * Sync project data with the data received from the REST API.
255
+ * @internal
256
+ */
257
+ async _syncToServer() {
258
+ const data = await this._api.get(this.id);
259
+ const jobData = data.completedWorkerJobs.reduce((acc: Record<string, RawJob>, job) => {
260
+ const jobId = job.imgID || getUUID();
261
+ acc[jobId] = job;
262
+ return acc;
263
+ }, {});
264
+ for (const job of this._jobs) {
265
+ const restJob = jobData[job.id];
266
+ // This should never happen, but just in case we log a warning
267
+ if (!restJob) {
268
+ this._logger.warn(`Job with id ${job.id} not found in the REST project data`);
269
+ return;
270
+ }
271
+ try {
272
+ await job._syncWithRestData(restJob);
273
+ } catch (error) {
274
+ this._logger.error(error);
275
+ this._logger.error(`Failed to sync job ${job.id}`);
276
+ }
277
+ delete jobData[job.id];
278
+ }
279
+
280
+ // If there are any jobs left in jobData, it means they are new jobs that are not in the project yet
281
+ if (Object.keys(jobData).length) {
282
+ for (const job of Object.values(jobData)) {
283
+ const jobInstance = Job.fromRaw(data, job, { api: this._api, logger: this._logger });
284
+ this._addJob(jobInstance);
285
+ }
286
+ }
287
+
288
+ const delta: Partial<ProjectData> = {
289
+ params: {
290
+ ...this.data.params,
291
+ numberOfImages: data.imageCount,
292
+ steps: data.stepCount,
293
+ numberOfPreviews: data.previewCount
294
+ }
295
+ };
296
+ if (PROJECT_STATUS_MAP[data.status]) {
297
+ delta.status = PROJECT_STATUS_MAP[data.status];
298
+ }
299
+ this._update(delta);
300
+ }
301
+
173
302
  /**
174
303
  * Get full project data snapshot. Can be used to serialize the project and store it in a database.
175
304
  */
@@ -67,7 +67,7 @@ function getTemplate() {
67
67
 
68
68
  function createJobRequestMessage(id: string, params: ProjectParams) {
69
69
  const template = getTemplate();
70
- return {
70
+ const jobRequest: Record<string, any> = {
71
71
  ...template,
72
72
  keyFrames: [
73
73
  {
@@ -92,6 +92,10 @@ function createJobRequestMessage(id: string, params: ProjectParams) {
92
92
  jobID: id,
93
93
  disableSafety: !!params.disableNSFWFilter
94
94
  };
95
+ if (params.network) {
96
+ jobRequest.network = params.network;
97
+ }
98
+ return jobRequest;
95
99
  }
96
100
 
97
101
  export type JobRequestRaw = ReturnType<typeof createJobRequestMessage>;