@sogni-ai/expo-client 1.0.0-alpha.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.
- package/CHANGELOG.md +69 -0
- package/README.md +411 -0
- package/dist/cjs/Account/index.d.ts +60 -0
- package/dist/cjs/Account/index.d.ts.map +1 -0
- package/dist/cjs/Account/index.js +188 -0
- package/dist/cjs/Account/index.js.map +1 -0
- package/dist/cjs/Account/types.d.ts +43 -0
- package/dist/cjs/Account/types.d.ts.map +1 -0
- package/dist/cjs/Account/types.js +18 -0
- package/dist/cjs/Account/types.js.map +1 -0
- package/dist/cjs/ApiClient/WebSocketClient/events.d.ts +139 -0
- package/dist/cjs/ApiClient/WebSocketClient/events.d.ts.map +1 -0
- package/dist/cjs/ApiClient/WebSocketClient/events.js +3 -0
- package/dist/cjs/ApiClient/WebSocketClient/events.js.map +1 -0
- package/dist/cjs/ApiClient/WebSocketClient/index.d.ts +45 -0
- package/dist/cjs/ApiClient/WebSocketClient/index.d.ts.map +1 -0
- package/dist/cjs/ApiClient/WebSocketClient/index.js +283 -0
- package/dist/cjs/ApiClient/WebSocketClient/index.js.map +1 -0
- package/dist/cjs/ApiClient/WebSocketClient/normalize.d.ts +45 -0
- package/dist/cjs/ApiClient/WebSocketClient/normalize.d.ts.map +1 -0
- package/dist/cjs/ApiClient/WebSocketClient/normalize.js +245 -0
- package/dist/cjs/ApiClient/WebSocketClient/normalize.js.map +1 -0
- package/dist/cjs/ApiClient/WebSocketClient/types.d.ts +9 -0
- package/dist/cjs/ApiClient/WebSocketClient/types.d.ts.map +1 -0
- package/dist/cjs/ApiClient/WebSocketClient/types.js +3 -0
- package/dist/cjs/ApiClient/WebSocketClient/types.js.map +1 -0
- package/dist/cjs/ApiClient/WebSocketClient/wireTypes.d.ts +161 -0
- package/dist/cjs/ApiClient/WebSocketClient/wireTypes.d.ts.map +1 -0
- package/dist/cjs/ApiClient/WebSocketClient/wireTypes.js +11 -0
- package/dist/cjs/ApiClient/WebSocketClient/wireTypes.js.map +1 -0
- package/dist/cjs/ApiClient/index.d.ts +60 -0
- package/dist/cjs/ApiClient/index.d.ts.map +1 -0
- package/dist/cjs/ApiClient/index.js +87 -0
- package/dist/cjs/ApiClient/index.js.map +1 -0
- package/dist/cjs/Chat/ChatStream.d.ts +42 -0
- package/dist/cjs/Chat/ChatStream.d.ts.map +1 -0
- package/dist/cjs/Chat/ChatStream.js +146 -0
- package/dist/cjs/Chat/ChatStream.js.map +1 -0
- package/dist/cjs/Chat/index.d.ts +61 -0
- package/dist/cjs/Chat/index.d.ts.map +1 -0
- package/dist/cjs/Chat/index.js +179 -0
- package/dist/cjs/Chat/index.js.map +1 -0
- package/dist/cjs/Chat/tools.d.ts +13 -0
- package/dist/cjs/Chat/tools.d.ts.map +1 -0
- package/dist/cjs/Chat/tools.js +22 -0
- package/dist/cjs/Chat/tools.js.map +1 -0
- package/dist/cjs/Chat/types.d.ts +153 -0
- package/dist/cjs/Chat/types.d.ts.map +1 -0
- package/dist/cjs/Chat/types.js +3 -0
- package/dist/cjs/Chat/types.js.map +1 -0
- package/dist/cjs/Projects/ControlNet.d.ts +30 -0
- package/dist/cjs/Projects/ControlNet.d.ts.map +1 -0
- package/dist/cjs/Projects/ControlNet.js +45 -0
- package/dist/cjs/Projects/ControlNet.js.map +1 -0
- package/dist/cjs/Projects/createJobRequestMessage.d.ts +11 -0
- package/dist/cjs/Projects/createJobRequestMessage.d.ts.map +1 -0
- package/dist/cjs/Projects/createJobRequestMessage.js +239 -0
- package/dist/cjs/Projects/createJobRequestMessage.js.map +1 -0
- package/dist/cjs/Projects/index.d.ts +96 -0
- package/dist/cjs/Projects/index.d.ts.map +1 -0
- package/dist/cjs/Projects/index.js +536 -0
- package/dist/cjs/Projects/index.js.map +1 -0
- package/dist/cjs/Projects/restNormalize.d.ts +60 -0
- package/dist/cjs/Projects/restNormalize.d.ts.map +1 -0
- package/dist/cjs/Projects/restNormalize.js +89 -0
- package/dist/cjs/Projects/restNormalize.js.map +1 -0
- package/dist/cjs/Projects/types.d.ts +233 -0
- package/dist/cjs/Projects/types.d.ts.map +1 -0
- package/dist/cjs/Projects/types.js +3 -0
- package/dist/cjs/Projects/types.js.map +1 -0
- package/dist/cjs/Projects/utils.d.ts +23 -0
- package/dist/cjs/Projects/utils.d.ts.map +1 -0
- package/dist/cjs/Projects/utils.js +65 -0
- package/dist/cjs/Projects/utils.js.map +1 -0
- package/dist/cjs/index.d.ts +63 -0
- package/dist/cjs/index.d.ts.map +1 -0
- package/dist/cjs/index.js +116 -0
- package/dist/cjs/index.js.map +1 -0
- package/dist/cjs/lib/AuthManager/ApiKeyAuthManager.d.ts +15 -0
- package/dist/cjs/lib/AuthManager/ApiKeyAuthManager.d.ts.map +1 -0
- package/dist/cjs/lib/AuthManager/ApiKeyAuthManager.js +47 -0
- package/dist/cjs/lib/AuthManager/ApiKeyAuthManager.js.map +1 -0
- package/dist/cjs/lib/AuthManager/AuthManagerBase.d.ts +23 -0
- package/dist/cjs/lib/AuthManager/AuthManagerBase.d.ts.map +1 -0
- package/dist/cjs/lib/AuthManager/AuthManagerBase.js +14 -0
- package/dist/cjs/lib/AuthManager/AuthManagerBase.js.map +1 -0
- package/dist/cjs/lib/AuthManager/AuthStorage.d.ts +11 -0
- package/dist/cjs/lib/AuthManager/AuthStorage.d.ts.map +1 -0
- package/dist/cjs/lib/AuthManager/AuthStorage.js +50 -0
- package/dist/cjs/lib/AuthManager/AuthStorage.js.map +1 -0
- package/dist/cjs/lib/AuthManager/TokenAuthManager.d.ts +35 -0
- package/dist/cjs/lib/AuthManager/TokenAuthManager.d.ts.map +1 -0
- package/dist/cjs/lib/AuthManager/TokenAuthManager.js +164 -0
- package/dist/cjs/lib/AuthManager/TokenAuthManager.js.map +1 -0
- package/dist/cjs/lib/AuthManager/index.d.ts +7 -0
- package/dist/cjs/lib/AuthManager/index.d.ts.map +1 -0
- package/dist/cjs/lib/AuthManager/index.js +13 -0
- package/dist/cjs/lib/AuthManager/index.js.map +1 -0
- package/dist/cjs/lib/Cache.d.ts +12 -0
- package/dist/cjs/lib/Cache.d.ts.map +1 -0
- package/dist/cjs/lib/Cache.js +36 -0
- package/dist/cjs/lib/Cache.js.map +1 -0
- package/dist/cjs/lib/DefaultLogger.d.ts +17 -0
- package/dist/cjs/lib/DefaultLogger.d.ts.map +1 -0
- package/dist/cjs/lib/DefaultLogger.js +36 -0
- package/dist/cjs/lib/DefaultLogger.js.map +1 -0
- package/dist/cjs/lib/RestClient.d.ts +18 -0
- package/dist/cjs/lib/RestClient.d.ts.map +1 -0
- package/dist/cjs/lib/RestClient.js +67 -0
- package/dist/cjs/lib/RestClient.js.map +1 -0
- package/dist/cjs/lib/TypedEventEmitter.d.ts +16 -0
- package/dist/cjs/lib/TypedEventEmitter.d.ts.map +1 -0
- package/dist/cjs/lib/TypedEventEmitter.js +46 -0
- package/dist/cjs/lib/TypedEventEmitter.js.map +1 -0
- package/dist/cjs/lib/base64.d.ts +3 -0
- package/dist/cjs/lib/base64.d.ts.map +1 -0
- package/dist/cjs/lib/base64.js +21 -0
- package/dist/cjs/lib/base64.js.map +1 -0
- package/dist/cjs/lib/getUUID.d.ts +12 -0
- package/dist/cjs/lib/getUUID.d.ts.map +1 -0
- package/dist/cjs/lib/getUUID.js +29 -0
- package/dist/cjs/lib/getUUID.js.map +1 -0
- package/dist/cjs/lib/mediaInput.d.ts +33 -0
- package/dist/cjs/lib/mediaInput.d.ts.map +1 -0
- package/dist/cjs/lib/mediaInput.js +32 -0
- package/dist/cjs/lib/mediaInput.js.map +1 -0
- package/dist/cjs/lib/utils.d.ts +10 -0
- package/dist/cjs/lib/utils.d.ts.map +1 -0
- package/dist/cjs/lib/utils.js +24 -0
- package/dist/cjs/lib/utils.js.map +1 -0
- package/dist/cjs/types/ErrorData.d.ts +7 -0
- package/dist/cjs/types/ErrorData.d.ts.map +1 -0
- package/dist/cjs/types/ErrorData.js +3 -0
- package/dist/cjs/types/ErrorData.js.map +1 -0
- package/dist/cjs/types/json.d.ts +6 -0
- package/dist/cjs/types/json.d.ts.map +1 -0
- package/dist/cjs/types/json.js +3 -0
- package/dist/cjs/types/json.js.map +1 -0
- package/dist/cjs/types/token.d.ts +2 -0
- package/dist/cjs/types/token.d.ts.map +1 -0
- package/dist/cjs/types/token.js +3 -0
- package/dist/cjs/types/token.js.map +1 -0
- package/dist/cjs/version.d.ts +2 -0
- package/dist/cjs/version.d.ts.map +1 -0
- package/dist/cjs/version.js +5 -0
- package/dist/cjs/version.js.map +1 -0
- package/dist/cjs/wallet.d.ts +59 -0
- package/dist/cjs/wallet.d.ts.map +1 -0
- package/dist/cjs/wallet.js +125 -0
- package/dist/cjs/wallet.js.map +1 -0
- package/dist/esm/Account/index.js +183 -0
- package/dist/esm/Account/index.js.map +1 -0
- package/dist/esm/Account/types.js +15 -0
- package/dist/esm/Account/types.js.map +1 -0
- package/dist/esm/ApiClient/WebSocketClient/events.js +2 -0
- package/dist/esm/ApiClient/WebSocketClient/events.js.map +1 -0
- package/dist/esm/ApiClient/WebSocketClient/index.js +278 -0
- package/dist/esm/ApiClient/WebSocketClient/index.js.map +1 -0
- package/dist/esm/ApiClient/WebSocketClient/normalize.js +229 -0
- package/dist/esm/ApiClient/WebSocketClient/normalize.js.map +1 -0
- package/dist/esm/ApiClient/WebSocketClient/types.js +2 -0
- package/dist/esm/ApiClient/WebSocketClient/types.js.map +1 -0
- package/dist/esm/ApiClient/WebSocketClient/wireTypes.js +10 -0
- package/dist/esm/ApiClient/WebSocketClient/wireTypes.js.map +1 -0
- package/dist/esm/ApiClient/index.js +80 -0
- package/dist/esm/ApiClient/index.js.map +1 -0
- package/dist/esm/Chat/ChatStream.js +144 -0
- package/dist/esm/Chat/ChatStream.js.map +1 -0
- package/dist/esm/Chat/index.js +174 -0
- package/dist/esm/Chat/index.js.map +1 -0
- package/dist/esm/Chat/tools.js +19 -0
- package/dist/esm/Chat/tools.js.map +1 -0
- package/dist/esm/Chat/types.js +2 -0
- package/dist/esm/Chat/types.js.map +1 -0
- package/dist/esm/Projects/ControlNet.js +41 -0
- package/dist/esm/Projects/ControlNet.js.map +1 -0
- package/dist/esm/Projects/createJobRequestMessage.js +236 -0
- package/dist/esm/Projects/createJobRequestMessage.js.map +1 -0
- package/dist/esm/Projects/index.js +531 -0
- package/dist/esm/Projects/index.js.map +1 -0
- package/dist/esm/Projects/restNormalize.js +86 -0
- package/dist/esm/Projects/restNormalize.js.map +1 -0
- package/dist/esm/Projects/types.js +2 -0
- package/dist/esm/Projects/types.js.map +1 -0
- package/dist/esm/Projects/utils.js +55 -0
- package/dist/esm/Projects/utils.js.map +1 -0
- package/dist/esm/index.js +72 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/esm/lib/AuthManager/ApiKeyAuthManager.js +42 -0
- package/dist/esm/lib/AuthManager/ApiKeyAuthManager.js.map +1 -0
- package/dist/esm/lib/AuthManager/AuthManagerBase.js +9 -0
- package/dist/esm/lib/AuthManager/AuthManagerBase.js.map +1 -0
- package/dist/esm/lib/AuthManager/AuthStorage.js +13 -0
- package/dist/esm/lib/AuthManager/AuthStorage.js.map +1 -0
- package/dist/esm/lib/AuthManager/TokenAuthManager.js +159 -0
- package/dist/esm/lib/AuthManager/TokenAuthManager.js.map +1 -0
- package/dist/esm/lib/AuthManager/index.js +5 -0
- package/dist/esm/lib/AuthManager/index.js.map +1 -0
- package/dist/esm/lib/Cache.js +33 -0
- package/dist/esm/lib/Cache.js.map +1 -0
- package/dist/esm/lib/DefaultLogger.js +32 -0
- package/dist/esm/lib/DefaultLogger.js.map +1 -0
- package/dist/esm/lib/RestClient.js +62 -0
- package/dist/esm/lib/RestClient.js.map +1 -0
- package/dist/esm/lib/TypedEventEmitter.js +44 -0
- package/dist/esm/lib/TypedEventEmitter.js.map +1 -0
- package/dist/esm/lib/base64.js +17 -0
- package/dist/esm/lib/base64.js.map +1 -0
- package/dist/esm/lib/getUUID.js +26 -0
- package/dist/esm/lib/getUUID.js.map +1 -0
- package/dist/esm/lib/mediaInput.js +28 -0
- package/dist/esm/lib/mediaInput.js.map +1 -0
- package/dist/esm/lib/utils.js +19 -0
- package/dist/esm/lib/utils.js.map +1 -0
- package/dist/esm/types/ErrorData.js +2 -0
- package/dist/esm/types/ErrorData.js.map +1 -0
- package/dist/esm/types/json.js +2 -0
- package/dist/esm/types/json.js.map +1 -0
- package/dist/esm/types/token.js +2 -0
- package/dist/esm/types/token.js.map +1 -0
- package/dist/esm/version.js +2 -0
- package/dist/esm/version.js.map +1 -0
- package/dist/esm/wallet.js +117 -0
- package/dist/esm/wallet.js.map +1 -0
- package/package.json +92 -0
- package/src/Account/index.ts +214 -0
- package/src/Account/types.ts +61 -0
- package/src/ApiClient/WebSocketClient/events.ts +168 -0
- package/src/ApiClient/WebSocketClient/index.ts +345 -0
- package/src/ApiClient/WebSocketClient/normalize.ts +287 -0
- package/src/ApiClient/WebSocketClient/types.ts +10 -0
- package/src/ApiClient/WebSocketClient/wireTypes.ts +183 -0
- package/src/ApiClient/index.ts +147 -0
- package/src/Chat/ChatStream.ts +164 -0
- package/src/Chat/index.ts +224 -0
- package/src/Chat/tools.ts +23 -0
- package/src/Chat/types.ts +165 -0
- package/src/Projects/ControlNet.ts +84 -0
- package/src/Projects/createJobRequestMessage.ts +241 -0
- package/src/Projects/index.ts +608 -0
- package/src/Projects/restNormalize.ts +154 -0
- package/src/Projects/types.ts +266 -0
- package/src/Projects/utils.ts +60 -0
- package/src/index.ts +217 -0
- package/src/lib/AuthManager/ApiKeyAuthManager.ts +52 -0
- package/src/lib/AuthManager/AuthManagerBase.ts +35 -0
- package/src/lib/AuthManager/AuthStorage.ts +21 -0
- package/src/lib/AuthManager/TokenAuthManager.ts +179 -0
- package/src/lib/AuthManager/index.ts +8 -0
- package/src/lib/Cache.ts +45 -0
- package/src/lib/DefaultLogger.ts +47 -0
- package/src/lib/RestClient.ts +77 -0
- package/src/lib/TypedEventEmitter.ts +52 -0
- package/src/lib/base64.ts +17 -0
- package/src/lib/getUUID.ts +27 -0
- package/src/lib/mediaInput.ts +54 -0
- package/src/lib/utils.ts +21 -0
- package/src/types/ErrorData.ts +7 -0
- package/src/types/json.ts +5 -0
- package/src/types/token.ts +1 -0
- package/src/version.ts +1 -0
- package/src/wallet.ts +158 -0
|
@@ -0,0 +1,608 @@
|
|
|
1
|
+
import ApiClient, { ApiError, ApiResponse } from '../ApiClient';
|
|
2
|
+
import { SupernetType } from '../ApiClient/WebSocketClient/types';
|
|
3
|
+
import {
|
|
4
|
+
ArtistCancelConfirmationEvent,
|
|
5
|
+
JobErrorEvent,
|
|
6
|
+
JobETAEvent,
|
|
7
|
+
JobInitiatingModelEvent,
|
|
8
|
+
JobLifecycleCompletedEvent,
|
|
9
|
+
JobProgressEvent,
|
|
10
|
+
JobResultEvent,
|
|
11
|
+
JobStartedEvent
|
|
12
|
+
} from '../ApiClient/WebSocketClient/events';
|
|
13
|
+
import TypedEventEmitter from '../lib/TypedEventEmitter';
|
|
14
|
+
import getUUID from '../lib/getUUID';
|
|
15
|
+
import { MediaInput, toFetchBody } from '../lib/mediaInput';
|
|
16
|
+
import ErrorData from '../types/ErrorData';
|
|
17
|
+
import createJobRequestMessage from './createJobRequestMessage';
|
|
18
|
+
import {
|
|
19
|
+
AvailableModel,
|
|
20
|
+
ImageProjectParams,
|
|
21
|
+
Job,
|
|
22
|
+
JobStatus,
|
|
23
|
+
Project,
|
|
24
|
+
ProjectParams,
|
|
25
|
+
ProjectStatus,
|
|
26
|
+
VideoProjectParams
|
|
27
|
+
} from './types';
|
|
28
|
+
import { getMaxContextImages } from './ControlNet';
|
|
29
|
+
import { RawProject, rawProjectToProject } from './restNormalize';
|
|
30
|
+
|
|
31
|
+
interface ProjectsApiEvents {
|
|
32
|
+
/** Fired for any project state change (status, progress, jobs, etc.). */
|
|
33
|
+
changed: Project;
|
|
34
|
+
/** Progress tick on any project. */
|
|
35
|
+
progress: { project: Project; jobId: string };
|
|
36
|
+
/** Project reached terminal state 'completed' with all result URLs populated. */
|
|
37
|
+
completed: Project;
|
|
38
|
+
/** Project reached terminal state 'failed' or 'canceled'. */
|
|
39
|
+
failed: Project;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const TERMINAL_PROJECT_STATUSES: ProjectStatus[] = ['completed', 'failed', 'canceled'];
|
|
43
|
+
const TERMINAL_JOB_STATUSES: JobStatus[] = ['completed', 'failed', 'canceled'];
|
|
44
|
+
const GC_DELAY_MS = 30_000;
|
|
45
|
+
|
|
46
|
+
class ProjectsApi extends TypedEventEmitter<ProjectsApiEvents> {
|
|
47
|
+
private _client: ApiClient;
|
|
48
|
+
private _projects = new Map<string, Project>();
|
|
49
|
+
private _subscribers = new Map<string, Set<(p: Project) => void>>();
|
|
50
|
+
private _gcTimers = new Map<string, ReturnType<typeof setTimeout>>();
|
|
51
|
+
|
|
52
|
+
constructor(client: ApiClient) {
|
|
53
|
+
super();
|
|
54
|
+
this._client = client;
|
|
55
|
+
this._wireSocketHandlers();
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** List models available on the given network. Defaults to the client's current network. */
|
|
59
|
+
async getAvailableModels(network?: SupernetType): Promise<AvailableModel[]> {
|
|
60
|
+
const net = network ?? this._client.socket.supernetType;
|
|
61
|
+
const res = await this._client.socketRest.get<ApiResponse<unknown>>(
|
|
62
|
+
`/api/v1/status/network/${net}/models`
|
|
63
|
+
);
|
|
64
|
+
// The endpoint returns either an array of models directly or wrapped in
|
|
65
|
+
// { models: [...] }. Accept either shape.
|
|
66
|
+
const data: any = res.data;
|
|
67
|
+
if (Array.isArray(data)) return data as AvailableModel[];
|
|
68
|
+
if (data && Array.isArray(data.models)) return data.models as AvailableModel[];
|
|
69
|
+
return [];
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Create a generation project (image, video, or audio).
|
|
74
|
+
*
|
|
75
|
+
* Returns the initial Project snapshot immediately; subscribe via
|
|
76
|
+
* `subscribe(id, cb)` or `waitForCompletion(id)` to observe state changes as
|
|
77
|
+
* the server processes it.
|
|
78
|
+
*
|
|
79
|
+
* For image projects, `startingImage`, `contextImages[]`, and
|
|
80
|
+
* `controlNet.image` are uploaded first. For video projects, `referenceImage`,
|
|
81
|
+
* `referenceImageEnd`, `referenceAudio`, and `referenceVideo` are uploaded
|
|
82
|
+
* first. Reference uploads happen in parallel; the WS `jobRequest` is sent
|
|
83
|
+
* once all uploads complete.
|
|
84
|
+
*/
|
|
85
|
+
async create(params: ProjectParams): Promise<Project> {
|
|
86
|
+
if (!this._client.socket.isConnected) {
|
|
87
|
+
throw new Error(
|
|
88
|
+
'Cannot create project: WebSocket is not connected. Authenticate first and wait for `connected` event.'
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
const id = getUUID();
|
|
92
|
+
const project: Project = {
|
|
93
|
+
id,
|
|
94
|
+
params,
|
|
95
|
+
status: 'pending',
|
|
96
|
+
progress: 0,
|
|
97
|
+
eta: null,
|
|
98
|
+
jobs: [],
|
|
99
|
+
resultUrls: [],
|
|
100
|
+
error: null,
|
|
101
|
+
createdAt: Date.now(),
|
|
102
|
+
updatedAt: Date.now()
|
|
103
|
+
};
|
|
104
|
+
this._projects.set(id, project);
|
|
105
|
+
|
|
106
|
+
const type = params.type ?? 'image';
|
|
107
|
+
if (type === 'image') {
|
|
108
|
+
await this._uploadImageRefs(id, params as ImageProjectParams);
|
|
109
|
+
} else if (type === 'video') {
|
|
110
|
+
await this._uploadVideoRefs(id, params as VideoProjectParams);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const message = createJobRequestMessage(id, params);
|
|
114
|
+
await this._client.socket.send('jobRequest', message);
|
|
115
|
+
this._notifyChanged(project);
|
|
116
|
+
return project;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
private async _uploadImageRefs(projectId: string, params: ImageProjectParams) {
|
|
120
|
+
if (params.startingImage) {
|
|
121
|
+
await this._uploadMedia(projectId, 'startingImage', params.startingImage);
|
|
122
|
+
}
|
|
123
|
+
if (params.contextImages && params.contextImages.length) {
|
|
124
|
+
const max = getMaxContextImages(params.modelId);
|
|
125
|
+
if (params.contextImages.length > max) {
|
|
126
|
+
throw new Error(
|
|
127
|
+
`Model ${params.modelId} accepts at most ${max} context images (got ${params.contextImages.length})`
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
await Promise.all(
|
|
131
|
+
params.contextImages.map((img, idx) =>
|
|
132
|
+
this._uploadMedia(projectId, `contextImage${idx + 1}`, img)
|
|
133
|
+
)
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
if (params.controlNet?.image) {
|
|
137
|
+
await this._uploadMedia(projectId, 'cnImage', params.controlNet.image);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
private async _uploadVideoRefs(projectId: string, params: VideoProjectParams) {
|
|
142
|
+
const uploads: Promise<unknown>[] = [];
|
|
143
|
+
if (params.referenceImage) {
|
|
144
|
+
uploads.push(this._uploadMedia(projectId, 'referenceImage', params.referenceImage));
|
|
145
|
+
}
|
|
146
|
+
if (params.referenceImageEnd) {
|
|
147
|
+
uploads.push(this._uploadMedia(projectId, 'referenceImageEnd', params.referenceImageEnd));
|
|
148
|
+
}
|
|
149
|
+
if (params.referenceAudio) {
|
|
150
|
+
uploads.push(this._uploadMedia(projectId, 'referenceAudio', params.referenceAudio));
|
|
151
|
+
}
|
|
152
|
+
if (params.referenceVideo) {
|
|
153
|
+
uploads.push(this._uploadMedia(projectId, 'referenceVideo', params.referenceVideo));
|
|
154
|
+
}
|
|
155
|
+
if (uploads.length) await Promise.all(uploads);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
private async _uploadMedia(projectId: string, type: string, media: MediaInput): Promise<string> {
|
|
159
|
+
const imageId = getUUID();
|
|
160
|
+
const { body, contentType } = await toFetchBody(media);
|
|
161
|
+
const presignedUrl = await this._getUploadUrl({
|
|
162
|
+
imageId,
|
|
163
|
+
jobId: projectId,
|
|
164
|
+
type,
|
|
165
|
+
...(contentType ? { contentType } : {})
|
|
166
|
+
});
|
|
167
|
+
const headers: Record<string, string> = {};
|
|
168
|
+
if (contentType) headers['Content-Type'] = contentType;
|
|
169
|
+
const res = await fetch(presignedUrl, {
|
|
170
|
+
method: 'PUT',
|
|
171
|
+
headers,
|
|
172
|
+
body
|
|
173
|
+
});
|
|
174
|
+
if (!res.ok) {
|
|
175
|
+
throw new ApiError(res.status, {
|
|
176
|
+
status: 'error',
|
|
177
|
+
errorCode: 0,
|
|
178
|
+
message: `Failed to upload ${type} (HTTP ${res.status})`
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
return imageId;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
private async _getUploadUrl(params: {
|
|
185
|
+
imageId: string;
|
|
186
|
+
jobId: string;
|
|
187
|
+
type: string;
|
|
188
|
+
contentType?: string;
|
|
189
|
+
}): Promise<string> {
|
|
190
|
+
const res = await this._client.socketRest.get<ApiResponse<string | { url: string }>>(
|
|
191
|
+
'/v1/image/uploadUrl',
|
|
192
|
+
params
|
|
193
|
+
);
|
|
194
|
+
const d: any = res.data;
|
|
195
|
+
return typeof d === 'string' ? d : d.url;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/** Get the cached snapshot for a project, or undefined if unknown. */
|
|
199
|
+
get(projectId: string): Project | undefined {
|
|
200
|
+
return this._projects.get(projectId.toUpperCase());
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Fetch a project's current state from REST and cache it.
|
|
205
|
+
*
|
|
206
|
+
* Use this on cold launch when you have a persisted project ID but no
|
|
207
|
+
* in-memory snapshot (e.g., the app was killed mid-generation). The server
|
|
208
|
+
* returns the latest known state; if the project is still in flight, the
|
|
209
|
+
* SDK will keep updating the snapshot via socket events.
|
|
210
|
+
*
|
|
211
|
+
* @param projectId Server project ID.
|
|
212
|
+
* @param paramsOverride Optional. The original ProjectParams used to create
|
|
213
|
+
* the project. The REST response only includes a subset of generation
|
|
214
|
+
* settings; pass the original params if you persisted them (e.g., in MMKV)
|
|
215
|
+
* so the returned snapshot's `params` field is fully populated.
|
|
216
|
+
*/
|
|
217
|
+
async fetch(projectId: string, paramsOverride?: ProjectParams): Promise<Project> {
|
|
218
|
+
const id = projectId.toUpperCase();
|
|
219
|
+
const res = await this._client.rest.get<ApiResponse<RawProject>>(`/v1/projects/${id}`);
|
|
220
|
+
const project = rawProjectToProject(res.data, paramsOverride);
|
|
221
|
+
this._projects.set(project.id, project);
|
|
222
|
+
this._notifyChanged(project);
|
|
223
|
+
return project;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/** Subscribe to changes for a specific project. Returns an unsubscribe function. */
|
|
227
|
+
subscribe(projectId: string, listener: (project: Project) => void): () => void {
|
|
228
|
+
const id = projectId.toUpperCase();
|
|
229
|
+
let set = this._subscribers.get(id);
|
|
230
|
+
if (!set) {
|
|
231
|
+
set = new Set();
|
|
232
|
+
this._subscribers.set(id, set);
|
|
233
|
+
}
|
|
234
|
+
set.add(listener);
|
|
235
|
+
return () => {
|
|
236
|
+
const current = this._subscribers.get(id);
|
|
237
|
+
if (!current) return;
|
|
238
|
+
current.delete(listener);
|
|
239
|
+
if (current.size === 0) this._subscribers.delete(id);
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Resolve when the project reaches a terminal state. On success, returns the
|
|
245
|
+
* downloadable result URLs (fetched from the server). On failure or cancel,
|
|
246
|
+
* rejects with an Error describing the reason.
|
|
247
|
+
*/
|
|
248
|
+
async waitForCompletion(projectId: string): Promise<string[]> {
|
|
249
|
+
const id = projectId.toUpperCase();
|
|
250
|
+
const project = this._projects.get(id);
|
|
251
|
+
if (!project) {
|
|
252
|
+
throw new Error(`Project ${projectId} is not known to this client`);
|
|
253
|
+
}
|
|
254
|
+
if (project.status === 'completed') {
|
|
255
|
+
return project.resultUrls.length ? project.resultUrls : this._fetchResultUrls(project);
|
|
256
|
+
}
|
|
257
|
+
if (project.status === 'failed' || project.status === 'canceled') {
|
|
258
|
+
throw new Error(project.error?.message || `Project ${project.status}`);
|
|
259
|
+
}
|
|
260
|
+
return new Promise<string[]>((resolve, reject) => {
|
|
261
|
+
const unsubscribe = this.subscribe(id, (p) => {
|
|
262
|
+
if (p.status === 'completed') {
|
|
263
|
+
unsubscribe();
|
|
264
|
+
if (p.resultUrls.length) resolve(p.resultUrls);
|
|
265
|
+
else this._fetchResultUrls(p).then(resolve).catch(reject);
|
|
266
|
+
} else if (p.status === 'failed' || p.status === 'canceled') {
|
|
267
|
+
unsubscribe();
|
|
268
|
+
reject(new Error(p.error?.message || `Project ${p.status}`));
|
|
269
|
+
}
|
|
270
|
+
});
|
|
271
|
+
});
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/** Request a download URL for a specific job's result image. */
|
|
275
|
+
async getResultUrl(projectId: string, jobId: string): Promise<string> {
|
|
276
|
+
const res = await this._client.socketRest.get<ApiResponse<string | { url: string }>>(
|
|
277
|
+
`/v1/image/downloadUrl`,
|
|
278
|
+
{ projectId: projectId.toUpperCase(), jobId: jobId.toUpperCase(), type: 'complete' }
|
|
279
|
+
);
|
|
280
|
+
const d: any = res.data;
|
|
281
|
+
return typeof d === 'string' ? d : d.url;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* Request cancellation of a project. Server confirms via
|
|
286
|
+
* `artistCancelConfirmation`; the project's status flips to 'canceled' if
|
|
287
|
+
* accepted, otherwise stays in its current state.
|
|
288
|
+
*/
|
|
289
|
+
async cancel(projectId: string): Promise<void> {
|
|
290
|
+
const id = projectId.toUpperCase();
|
|
291
|
+
await this._client.socket.send('jobError', {
|
|
292
|
+
jobID: id,
|
|
293
|
+
error: 'artistCanceled',
|
|
294
|
+
error_message: 'Artist Cancelled Job',
|
|
295
|
+
isFromWorker: false
|
|
296
|
+
});
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// ===========================================================================
|
|
300
|
+
// Internal: event wiring
|
|
301
|
+
// ===========================================================================
|
|
302
|
+
|
|
303
|
+
private _wireSocketHandlers() {
|
|
304
|
+
const s = this._client.socket;
|
|
305
|
+
s.on('jobStarted', (e) => this._onJobStarted(e));
|
|
306
|
+
s.on('jobInitiatingModel', (e) => this._onJobInitiatingModel(e));
|
|
307
|
+
s.on('jobLifecycleCompleted', (e) => this._onJobLifecycleCompleted(e));
|
|
308
|
+
s.on('jobProgress', (e) => this._onJobProgress(e));
|
|
309
|
+
s.on('jobResult', (e) => this._onJobResult(e));
|
|
310
|
+
s.on('jobError', (e) => this._onJobError(e));
|
|
311
|
+
s.on('jobETA', (e) => this._onJobETA(e));
|
|
312
|
+
s.on('artistCancelConfirmation', (e) => this._onCancelConfirmation(e));
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
private _onJobStarted(e: JobStartedEvent) {
|
|
316
|
+
const project = this._projects.get(e.projectId);
|
|
317
|
+
if (!project) return;
|
|
318
|
+
const next = this._withJobMutation(project, e.jobId, (job) => ({
|
|
319
|
+
...job,
|
|
320
|
+
status: 'processing',
|
|
321
|
+
workerName: e.workerName
|
|
322
|
+
}));
|
|
323
|
+
this._setStatusIfPending(next, 'processing');
|
|
324
|
+
this._notifyChanged(next);
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
private _onJobInitiatingModel(e: JobInitiatingModelEvent) {
|
|
328
|
+
const project = this._projects.get(e.projectId);
|
|
329
|
+
if (!project) return;
|
|
330
|
+
const next = this._withJobMutation(project, e.jobId, (job) => ({
|
|
331
|
+
...job,
|
|
332
|
+
status: 'initiatingModel'
|
|
333
|
+
}));
|
|
334
|
+
this._setStatusIfPending(next, 'processing');
|
|
335
|
+
this._notifyChanged(next);
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
private _onJobLifecycleCompleted(e: JobLifecycleCompletedEvent) {
|
|
339
|
+
// jobState.type === 'jobCompleted' is the lifecycle marker from server.
|
|
340
|
+
// The real result data arrives via jobResult; we just ensure the job is
|
|
341
|
+
// marked completed here if it wasn't already.
|
|
342
|
+
const project = this._projects.get(e.projectId);
|
|
343
|
+
if (!project) return;
|
|
344
|
+
const next = this._withJobMutation(project, e.jobId, (job) => ({
|
|
345
|
+
...job,
|
|
346
|
+
status: job.status === 'failed' ? 'failed' : 'completed',
|
|
347
|
+
progress: 1
|
|
348
|
+
}));
|
|
349
|
+
this._maybeFinalizeProject(next);
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
private _onJobProgress(e: JobProgressEvent) {
|
|
353
|
+
const project = this._projects.get(e.projectId);
|
|
354
|
+
if (!project) return;
|
|
355
|
+
const next = this._withJobMutation(project, e.jobId, (job) => ({
|
|
356
|
+
...job,
|
|
357
|
+
status: 'processing',
|
|
358
|
+
progress: e.progress,
|
|
359
|
+
step: e.step,
|
|
360
|
+
stepCount: e.stepCount
|
|
361
|
+
}));
|
|
362
|
+
this._setStatusIfPending(next, 'processing');
|
|
363
|
+
next.progress = this._computeOverallProgress(next.jobs);
|
|
364
|
+
this._notifyChanged(next);
|
|
365
|
+
this.emit('progress', { project: next, jobId: e.jobId });
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
private _onJobResult(e: JobResultEvent) {
|
|
369
|
+
const project = this._projects.get(e.projectId);
|
|
370
|
+
if (!project) return;
|
|
371
|
+
if (e.userCanceled) {
|
|
372
|
+
const error: ErrorData = {
|
|
373
|
+
code: 5004,
|
|
374
|
+
originalCode: 'artistCanceled',
|
|
375
|
+
message: 'Artist cancelled the job'
|
|
376
|
+
};
|
|
377
|
+
const next = this._withJobMutation(project, e.jobId, (job) => ({
|
|
378
|
+
...job,
|
|
379
|
+
status: 'canceled',
|
|
380
|
+
error
|
|
381
|
+
}));
|
|
382
|
+
this._maybeFinalizeProject(next);
|
|
383
|
+
return;
|
|
384
|
+
}
|
|
385
|
+
if (e.triggeredNSFWFilter) {
|
|
386
|
+
const error: ErrorData = {
|
|
387
|
+
code: 5006,
|
|
388
|
+
originalCode: 'sensitiveContent',
|
|
389
|
+
message: 'NSFW filter triggered'
|
|
390
|
+
};
|
|
391
|
+
const next = this._withJobMutation(project, e.jobId, (job) => ({
|
|
392
|
+
...job,
|
|
393
|
+
status: 'failed',
|
|
394
|
+
isNSFW: true,
|
|
395
|
+
seed: e.seed,
|
|
396
|
+
error
|
|
397
|
+
}));
|
|
398
|
+
this._maybeFinalizeProject(next);
|
|
399
|
+
return;
|
|
400
|
+
}
|
|
401
|
+
const next = this._withJobMutation(project, e.jobId, (job) => ({
|
|
402
|
+
...job,
|
|
403
|
+
status: 'completed',
|
|
404
|
+
progress: 1,
|
|
405
|
+
seed: e.seed
|
|
406
|
+
}));
|
|
407
|
+
this._maybeFinalizeProject(next);
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
private _onJobError(e: JobErrorEvent) {
|
|
411
|
+
const project = this._projects.get(e.projectId);
|
|
412
|
+
if (!project) return;
|
|
413
|
+
const error: ErrorData = {
|
|
414
|
+
code: 5000,
|
|
415
|
+
originalCode: e.rawCode,
|
|
416
|
+
message: e.message
|
|
417
|
+
};
|
|
418
|
+
if (e.jobId) {
|
|
419
|
+
const next = this._withJobMutation(project, e.jobId, (job) => ({
|
|
420
|
+
...job,
|
|
421
|
+
status: 'failed',
|
|
422
|
+
error
|
|
423
|
+
}));
|
|
424
|
+
this._maybeFinalizeProject(next);
|
|
425
|
+
return;
|
|
426
|
+
}
|
|
427
|
+
// Project-level error
|
|
428
|
+
const next: Project = {
|
|
429
|
+
...project,
|
|
430
|
+
status: 'failed',
|
|
431
|
+
error,
|
|
432
|
+
updatedAt: Date.now()
|
|
433
|
+
};
|
|
434
|
+
this._projects.set(project.id, next);
|
|
435
|
+
this._notifyChanged(next);
|
|
436
|
+
this.emit('failed', next);
|
|
437
|
+
this._scheduleGC(next.id);
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
private _onJobETA(e: JobETAEvent) {
|
|
441
|
+
const project = this._projects.get(e.projectId);
|
|
442
|
+
if (!project) return;
|
|
443
|
+
const next: Project = { ...project, eta: e.etaSeconds, updatedAt: Date.now() };
|
|
444
|
+
this._projects.set(project.id, next);
|
|
445
|
+
this._notifyChanged(next);
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
private _onCancelConfirmation(e: ArtistCancelConfirmationEvent) {
|
|
449
|
+
const project = this._projects.get(e.projectId);
|
|
450
|
+
if (!project) return;
|
|
451
|
+
if (!e.didCancel) {
|
|
452
|
+
this._client.logger.warn(
|
|
453
|
+
`Cancel request for ${e.projectId} was rejected: ${e.errorMessage || 'unknown reason'}`
|
|
454
|
+
);
|
|
455
|
+
return;
|
|
456
|
+
}
|
|
457
|
+
const next: Project = {
|
|
458
|
+
...project,
|
|
459
|
+
status: 'canceled',
|
|
460
|
+
error: { code: 5004, originalCode: 'artistCanceled', message: 'Artist canceled project' },
|
|
461
|
+
updatedAt: Date.now()
|
|
462
|
+
};
|
|
463
|
+
this._projects.set(project.id, next);
|
|
464
|
+
this._notifyChanged(next);
|
|
465
|
+
this.emit('failed', next);
|
|
466
|
+
this._scheduleGC(next.id);
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
// ===========================================================================
|
|
470
|
+
// Internal: state machinery
|
|
471
|
+
// ===========================================================================
|
|
472
|
+
|
|
473
|
+
private _withJobMutation(
|
|
474
|
+
project: Project,
|
|
475
|
+
jobId: string | null,
|
|
476
|
+
mutate: (job: Job) => Job
|
|
477
|
+
): Project {
|
|
478
|
+
if (!jobId) {
|
|
479
|
+
// Some events (jobStarted before imgID is assigned) carry no jobId. We
|
|
480
|
+
// don't synthesize a job in that case; just bump the updatedAt.
|
|
481
|
+
const next = { ...project, updatedAt: Date.now() };
|
|
482
|
+
this._projects.set(project.id, next);
|
|
483
|
+
return next;
|
|
484
|
+
}
|
|
485
|
+
const existingIdx = project.jobs.findIndex((j) => j.id === jobId);
|
|
486
|
+
const existing: Job =
|
|
487
|
+
existingIdx >= 0 ? project.jobs[existingIdx]! : this._emptyJob(project.id, jobId);
|
|
488
|
+
const updated = mutate(existing);
|
|
489
|
+
const jobs =
|
|
490
|
+
existingIdx >= 0
|
|
491
|
+
? project.jobs.map((j, i) => (i === existingIdx ? updated : j))
|
|
492
|
+
: [...project.jobs, updated];
|
|
493
|
+
const next: Project = { ...project, jobs, updatedAt: Date.now() };
|
|
494
|
+
this._projects.set(project.id, next);
|
|
495
|
+
return next;
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
private _emptyJob(projectId: string, jobId: string): Job {
|
|
499
|
+
return {
|
|
500
|
+
id: jobId,
|
|
501
|
+
projectId,
|
|
502
|
+
status: 'pending',
|
|
503
|
+
progress: 0,
|
|
504
|
+
step: 0,
|
|
505
|
+
stepCount: 0,
|
|
506
|
+
seed: null,
|
|
507
|
+
resultUrl: null,
|
|
508
|
+
previewUrl: null,
|
|
509
|
+
isNSFW: false,
|
|
510
|
+
workerName: null,
|
|
511
|
+
error: null
|
|
512
|
+
};
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
private _setStatusIfPending(project: Project, status: ProjectStatus) {
|
|
516
|
+
if (project.status !== 'pending') return;
|
|
517
|
+
const next: Project = { ...project, status, updatedAt: Date.now() };
|
|
518
|
+
this._projects.set(project.id, next);
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
private _computeOverallProgress(jobs: Job[]): number {
|
|
522
|
+
if (!jobs.length) return 0;
|
|
523
|
+
const sum = jobs.reduce((acc, j) => acc + (j.progress || 0), 0);
|
|
524
|
+
return sum / jobs.length;
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
private _maybeFinalizeProject(project: Project) {
|
|
528
|
+
const expected = Math.max(1, project.params.numberOfMedia);
|
|
529
|
+
const knownJobs = project.jobs.length;
|
|
530
|
+
const completedJobs = project.jobs.filter((j) => j.status === 'completed').length;
|
|
531
|
+
const allKnownTerminal =
|
|
532
|
+
knownJobs > 0 && project.jobs.every((j) => TERMINAL_JOB_STATUSES.includes(j.status));
|
|
533
|
+
|
|
534
|
+
if (knownJobs >= expected && allKnownTerminal) {
|
|
535
|
+
const anyCompleted = completedJobs > 0;
|
|
536
|
+
const next: Project = {
|
|
537
|
+
...project,
|
|
538
|
+
status: anyCompleted ? 'completed' : 'failed',
|
|
539
|
+
progress: this._computeOverallProgress(project.jobs),
|
|
540
|
+
error: anyCompleted
|
|
541
|
+
? null
|
|
542
|
+
: (project.jobs.find((j) => j.error)?.error ?? {
|
|
543
|
+
code: 5000,
|
|
544
|
+
message: 'All jobs failed'
|
|
545
|
+
}),
|
|
546
|
+
updatedAt: Date.now()
|
|
547
|
+
};
|
|
548
|
+
this._projects.set(project.id, next);
|
|
549
|
+
this._notifyChanged(next);
|
|
550
|
+
if (next.status === 'completed') {
|
|
551
|
+
this.emit('completed', next);
|
|
552
|
+
} else {
|
|
553
|
+
this.emit('failed', next);
|
|
554
|
+
}
|
|
555
|
+
this._scheduleGC(next.id);
|
|
556
|
+
return;
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
// Not terminal yet — just notify of the in-progress change.
|
|
560
|
+
const nextProgress = this._computeOverallProgress(project.jobs);
|
|
561
|
+
const next: Project = { ...project, progress: nextProgress, updatedAt: Date.now() };
|
|
562
|
+
this._projects.set(project.id, next);
|
|
563
|
+
this._notifyChanged(next);
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
private async _fetchResultUrls(project: Project): Promise<string[]> {
|
|
567
|
+
const urls: string[] = [];
|
|
568
|
+
for (const job of project.jobs) {
|
|
569
|
+
if (job.status !== 'completed') continue;
|
|
570
|
+
try {
|
|
571
|
+
const url = await this.getResultUrl(project.id, job.id);
|
|
572
|
+
urls.push(url);
|
|
573
|
+
// Update cached snapshot with the resolved URL
|
|
574
|
+
const cached = this._projects.get(project.id);
|
|
575
|
+
if (cached) {
|
|
576
|
+
const jobs = cached.jobs.map((j) => (j.id === job.id ? { ...j, resultUrl: url } : j));
|
|
577
|
+
const next: Project = { ...cached, jobs, resultUrls: urls.slice() };
|
|
578
|
+
this._projects.set(project.id, next);
|
|
579
|
+
this._notifyChanged(next);
|
|
580
|
+
}
|
|
581
|
+
} catch (e) {
|
|
582
|
+
this._client.logger.warn(`Failed to fetch result URL for job ${job.id}:`, e);
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
return urls;
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
private _notifyChanged(project: Project) {
|
|
589
|
+
const subs = this._subscribers.get(project.id);
|
|
590
|
+
if (subs) {
|
|
591
|
+
for (const fn of subs) fn(project);
|
|
592
|
+
}
|
|
593
|
+
this.emit('changed', project);
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
private _scheduleGC(projectId: string) {
|
|
597
|
+
const existing = this._gcTimers.get(projectId);
|
|
598
|
+
if (existing) clearTimeout(existing);
|
|
599
|
+
const timer = setTimeout(() => {
|
|
600
|
+
this._projects.delete(projectId);
|
|
601
|
+
this._subscribers.delete(projectId);
|
|
602
|
+
this._gcTimers.delete(projectId);
|
|
603
|
+
}, GC_DELAY_MS);
|
|
604
|
+
this._gcTimers.set(projectId, timer);
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
export default ProjectsApi;
|