@hautechai/sdk 0.1.4 → 0.2.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.
package/dist/sdk/index.js CHANGED
@@ -10,6 +10,9 @@ import pipelines from './pipelines';
10
10
  import stacks from './stacks';
11
11
  import storage from './storage';
12
12
  import utils from './utils';
13
+ import { OperationsListener } from './listeners';
14
+ import { getAPI, getBaseUrl } from './api';
15
+ import { OperationsApi } from '../autogenerated';
13
16
  export const createSDK = (options) => {
14
17
  let token = undefined;
15
18
  const authToken = async () => {
@@ -23,6 +26,17 @@ export const createSDK = (options) => {
23
26
  return token;
24
27
  };
25
28
  const optionsWithTokenRefresher = { ...options, authToken };
29
+ const operationsListener = new OperationsListener({
30
+ ws: options.useWebsocket
31
+ ? {
32
+ endpoint: getBaseUrl(options),
33
+ token: authToken,
34
+ }
35
+ : null,
36
+ // TODO: Refactor the API initialization
37
+ operations: () => getAPI(OperationsApi, optionsWithTokenRefresher),
38
+ });
39
+ operationsListener.subscribe();
26
40
  return {
27
41
  access: access(optionsWithTokenRefresher),
28
42
  accounts: accounts(optionsWithTokenRefresher),
@@ -30,10 +44,13 @@ export const createSDK = (options) => {
30
44
  collections: collections(optionsWithTokenRefresher),
31
45
  groups: groups(optionsWithTokenRefresher),
32
46
  images: images(optionsWithTokenRefresher),
33
- operations: operations(optionsWithTokenRefresher),
47
+ operations: operations(optionsWithTokenRefresher, operationsListener),
34
48
  pipelines: pipelines(optionsWithTokenRefresher),
35
49
  stacks: stacks(optionsWithTokenRefresher),
36
50
  storage: storage(optionsWithTokenRefresher),
37
51
  utils: utils(optionsWithTokenRefresher),
52
+ close: () => {
53
+ operationsListener.close();
54
+ },
38
55
  };
39
56
  };
@@ -0,0 +1,25 @@
1
+ import { OperationEntity, OperationsApi } from '../../autogenerated';
2
+ import { WebSocket } from 'ws';
3
+ export declare class OperationsListener {
4
+ useWebsocket: {
5
+ endpoint: string;
6
+ token: () => string | Promise<string>;
7
+ } | null;
8
+ ws: WebSocket | null;
9
+ operations: () => Promise<OperationsApi>;
10
+ constructor({ ws, operations, }: {
11
+ ws: {
12
+ endpoint: string;
13
+ token: () => string | Promise<string>;
14
+ } | null;
15
+ operations: () => Promise<OperationsApi>;
16
+ });
17
+ operationsStore: Record<string, OperationEntity>;
18
+ getOperation(id: string): Promise<OperationEntity | null>;
19
+ private updateOperation;
20
+ subscribe(): Promise<void>;
21
+ onOpen(): void;
22
+ onMessage(msg: string): void;
23
+ onClose(number: number, reason: Buffer): void;
24
+ close(): void;
25
+ }
@@ -0,0 +1,81 @@
1
+ import { WebSocket } from 'ws';
2
+ import { HautechException } from '../exceptions';
3
+ // This is pretty much a dirty solution until we need to rework this part.
4
+ export class OperationsListener {
5
+ constructor({ ws, operations, }) {
6
+ this.useWebsocket = null;
7
+ this.ws = null;
8
+ this.operationsStore = {};
9
+ if (ws) {
10
+ this.useWebsocket = {
11
+ endpoint: ws?.endpoint,
12
+ token: ws?.token,
13
+ };
14
+ }
15
+ this.operations = operations;
16
+ }
17
+ async getOperation(id) {
18
+ const isWsReady = this.ws?.readyState == WebSocket.OPEN;
19
+ if (!this.operationsStore[id] || !isWsReady) {
20
+ const api = await this.operations();
21
+ const operation = await api.operationsControllerGetOperationV1(id);
22
+ if (operation.status == 200)
23
+ this.updateOperation(id, operation.data);
24
+ }
25
+ return this.operationsStore[id] || null;
26
+ }
27
+ updateOperation(id, operation) {
28
+ const stored = this.operationsStore[id];
29
+ if (!stored || stored.updatedAt < operation.updatedAt) {
30
+ this.operationsStore[id] = operation;
31
+ }
32
+ }
33
+ async subscribe() {
34
+ if (!this.useWebsocket)
35
+ return;
36
+ try {
37
+ this.ws = new WebSocket(this.useWebsocket.endpoint, ['1', await this.useWebsocket.token()]);
38
+ this.ws.on('open', () => this.onOpen());
39
+ this.ws.on('message', (msg) => this.onMessage(msg));
40
+ this.ws.on('close', (number, reason) => this.onClose(number, reason));
41
+ }
42
+ catch (err) {
43
+ throw new HautechException(`SDK failed to open websocket: ${err}`);
44
+ }
45
+ }
46
+ onOpen() {
47
+ if (!this.ws)
48
+ throw new HautechException('Semantics error: this is a bug.');
49
+ this.ws.send(JSON.stringify({
50
+ event: 'subscribe',
51
+ data: {
52
+ channel: 'own_resources',
53
+ },
54
+ }));
55
+ }
56
+ onMessage(msg) {
57
+ if (!this.ws)
58
+ throw new HautechException('Semantics error: this is a bug.');
59
+ const { event, data } = JSON.parse(msg);
60
+ switch (event) {
61
+ case 'subscription:created':
62
+ break;
63
+ case 'operation:created':
64
+ case 'operation:updated':
65
+ this.updateOperation(data.id, data);
66
+ break;
67
+ }
68
+ }
69
+ onClose(number, reason) {
70
+ if (!this.ws)
71
+ throw new HautechException('Semantics error: this is a bug.');
72
+ // Reset dirty state.
73
+ this.operationsStore = {};
74
+ this.ws = null;
75
+ }
76
+ close() {
77
+ if (!this.ws)
78
+ return;
79
+ this.ws.close();
80
+ }
81
+ }
@@ -1,5 +1,6 @@
1
1
  import { CompositeV1Input, CompositeV1Response, CutV1Input, CutV1Response, GiseleVtonV1Input, GPTV1Input, GptV1Response, HauteLindaV1Response, HauteNaomiV1Response, ImagineKateV1Response, KateImagineV1Input, KateInpaintV1Input, LindaHauteV1Input, NaomiHauteV1Input, NegateImageV1Input, NegateImageV1Response, ObjectDetectionV1Input, ObjectDetectionV1Response, OperationEntity, OperationEntityStatusEnum, PoseEstimationV1Input, PoseEstimationV1Response, SegmentAnythingEmbeddingsV1Input, SegmentAnythingEmbeddingsV1Response, SegmentAnythingMaskV1Input, SegmentAnythingMaskV1Response, UpscaleV1Input, UpscaleV1Response, VtonGiseleV1Response } from '../../autogenerated';
2
2
  import { ListProps, ListResponse, SDKOptions } from '../../types';
3
+ import { OperationsListener } from '../listeners';
3
4
  type Waited<T extends OperationEntity> = T & ({
4
5
  status: typeof OperationEntityStatusEnum.Failed;
5
6
  output: null;
@@ -10,7 +11,7 @@ type Waited<T extends OperationEntity> = T & ({
10
11
  status: typeof OperationEntityStatusEnum.Finished;
11
12
  output: NonNullable<T['output']>;
12
13
  });
13
- declare const operations: (options: SDKOptions) => {
14
+ declare const operations: (options: SDKOptions, relevantOperations: OperationsListener) => {
14
15
  run: {
15
16
  haute: {
16
17
  linda: {
@@ -120,6 +121,6 @@ declare const operations: (options: SDKOptions) => {
120
121
  }) => Promise<void>;
121
122
  wait: <T extends OperationEntity | {
122
123
  id: string;
123
- }>(props: T) => Promise<Waited<T extends OperationEntity ? T : OperationEntity>>;
124
+ }>(props: T, timeoutMs?: number) => Promise<Waited<T extends OperationEntity ? T : OperationEntity> | null>;
124
125
  };
125
126
  export default operations;
@@ -1,7 +1,8 @@
1
1
  import { OperationsApi, } from '../../autogenerated';
2
2
  import { useAutogeneratedAPI } from '../api';
3
3
  import { transformToListResponse } from '../transformers';
4
- const operations = (options) => {
4
+ const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
5
+ const operations = (options, relevantOperations) => {
5
6
  const api = useAutogeneratedAPI({ API: OperationsApi, options });
6
7
  const createOperation = (callMethod) => (props) => api.call({ run: (methods) => callMethod(methods, props) });
7
8
  return {
@@ -72,20 +73,23 @@ const operations = (options) => {
72
73
  updateMetadata: async (props) => api.call({
73
74
  run: (methods) => methods.operationsControllerUpdateMetadataV1(props.id, { overwrite: props.metadata }),
74
75
  }),
75
- wait: async (props) => new Promise((resolve, reject) => {
76
- const initialDelay = 5000;
77
- const delay = 2000;
78
- let timeoutId;
79
- const poll = async () => {
80
- const operation = await api.call({
81
- run: (methods) => methods.operationsControllerGetOperationV1(props.id),
82
- });
83
- if (operation.status !== 'pending')
84
- return resolve(operation);
85
- timeoutId = setTimeout(poll, delay);
76
+ wait: async (props, timeoutMs) => {
77
+ const deadline = timeoutMs ? Date.now() + timeoutMs : undefined;
78
+ const delay = 1000;
79
+ const poll = async (id) => {
80
+ const operation = await relevantOperations.getOperation(id);
81
+ if (operation?.status !== 'pending')
82
+ return operation;
83
+ return null;
86
84
  };
87
- timeoutId = setTimeout(poll, initialDelay);
88
- }),
85
+ while (!deadline || Date.now() < deadline) {
86
+ const polled = await poll(props.id);
87
+ if (polled)
88
+ return polled;
89
+ await sleep(delay);
90
+ }
91
+ return null;
92
+ },
89
93
  };
90
94
  };
91
95
  export default operations;