import type { CloudlyApiClient } from './classes.cloudlyapiclient.js'; import * as plugins from './plugins.js'; export interface IImageRequestOptions { readonly abortSignal?: AbortSignal; } export interface IImagePushOptions extends IImageRequestOptions { /** Fixed sender wait for receiver acceptance after FIN; defaults to ten minutes, maximum one hour. */ readonly acceptanceTimeoutMs?: number; } export class Image implements plugins.servezoneInterfaces.data.IImage { public static async getImages(cloudlyClientRef: CloudlyApiClient) { const getAllImagesTR = cloudlyClientRef.requireTypedSocket().createTypedRequest( 'getAllImages' ); const response = await getAllImagesTR.fire({ identity: cloudlyClientRef.requireIdentity(), }); const resultImages: Image[] = []; for (const image of response.images) { const newImage = new Image(cloudlyClientRef); Object.assign(newImage, image); resultImages.push(newImage); } return resultImages; } public static async getImageById(cloudlyClientRef: CloudlyApiClient, imageIdArg: string) { const getImageByIdTR = cloudlyClientRef.requireTypedSocket().createTypedRequest( 'getImage' ); const response = await getImageByIdTR.fire({ identity: cloudlyClientRef.requireIdentity(), imageId: imageIdArg, }); const newImage = new Image(cloudlyClientRef); Object.assign(newImage, response.image); return newImage; } /** * creates a new image */ public static async createImage(cloudlyClientRef: CloudlyApiClient, imageDataArg: Partial) { const createImageTR = cloudlyClientRef.requireTypedSocket().createTypedRequest( 'createImage' ); const response = await createImageTR.fire({ identity: cloudlyClientRef.requireIdentity(), name: imageDataArg.name, description: imageDataArg.description, } as plugins.servezoneInterfaces.requests.image.IRequest_CreateImage['request']); const newImage = new Image(cloudlyClientRef); Object.assign(newImage, response.image); return newImage; } // INSTANCE cloudlyClientRef: CloudlyApiClient; id!: plugins.servezoneInterfaces.data.IImage['id']; data!: plugins.servezoneInterfaces.data.IImage['data']; constructor(cloudlyClientRef: CloudlyApiClient) { this.cloudlyClientRef = cloudlyClientRef; } /** * updates the image data */ public async update(optionsArg: IImageRequestOptions = {}) { const abortSignal = optionsArg.abortSignal; abortSignal?.throwIfAborted(); const getVersionsTR = this.cloudlyClientRef.requireTypedSocket().createTypedRequest( 'getImage' ); const response = await getVersionsTR.fire({ identity: this.cloudlyClientRef.requireIdentity(), imageId: this.id, }, { maxRetries: 0, abortSignal }); abortSignal?.throwIfAborted(); Object.assign(this, response.image); } /** * pushes a new version of the image * @param imageVersion * @param imageReadableArg */ public async pushImageVersion( imageVersion: string, imageReadableArg: ReadableStream, optionsArg: IImagePushOptions = {}, ): Promise { const requestedAcceptanceTimeoutMs = optionsArg.acceptanceTimeoutMs; const acceptanceTimeoutMs = requestedAcceptanceTimeoutMs === undefined ? 600_000 : requestedAcceptanceTimeoutMs; const abortSignal = optionsArg.abortSignal; const abortController = new AbortController(); let virtualStream: plugins.typedRequestInterfaces.TVirtualStream<'send'> | undefined; let callerAbort: Promise | undefined; const onAbort = () => { abortController.abort(abortSignal?.reason); // A pipe already closing its writer can ignore pipeTo's signal after readable EOF. // Abort the endpoint directly so receipt waiting is cancelled in that phase too. if (virtualStream) { callerAbort = virtualStream.abort(abortController.signal.reason); void callerAbort.catch(() => {}); } }; abortSignal?.addEventListener('abort', onAbort, { once: true }); if (abortSignal?.aborted) onAbort(); let response: Promise | undefined; let transfer: Promise | undefined; try { abortController.signal.throwIfAborted(); const identity = this.cloudlyClientRef.requireIdentity(); const socket = this.cloudlyClientRef.requireTypedSocket(); const transport = socket.virtualStreams.getClientTransport(); if (!transport) throw new Error('Image upload requires a connected TypedSocket transport.'); const pushImageTR = socket.createTypedRequest( 'pushImageVersion' ); const stream = plugins.typedrequest.VirtualStream.fromRegistration({ transport, registration: socket.virtualStreams.createRegistration({ creatorDirection: 'send', contentType: 'application/octet-stream', acceptanceTimeoutMs, }), }); virtualStream = stream; response = pushImageTR.fire({ identity, imageId: this.id, versionString: imageVersion, imageStream: stream, }, { maxRetries: 0, abortSignal: abortController.signal }).then((responseArg) => { if (!responseArg.allowed) throw new Error('Cloudly rejected the image upload.'); }); transfer = (async () => { await stream.opened; abortController.signal.throwIfAborted(); await imageReadableArg.pipeTo(stream.writable, { signal: abortController.signal }); await stream.completion; })(); await Promise.all([response, transfer]); abortController.signal.throwIfAborted(); await this.update({ abortSignal: abortController.signal }); } catch (errorArg) { abortController.abort(errorArg); await Promise.allSettled([ virtualStream?.abort(errorArg), callerAbort, response, transfer, virtualStream?.closed, ...(imageReadableArg.locked ? [] : [imageReadableArg.cancel(errorArg)]), ]); throw errorArg; } finally { abortSignal?.removeEventListener('abort', onAbort); } }; /** * pulls a version of the image */ public async pullImageVersion(versionStringArg: string): Promise> { const pullImageTR = this.cloudlyClientRef.requireTypedSocket().createTypedRequest( 'pullImageVersion' ); const response = await pullImageTR.fire({ identity: this.cloudlyClientRef.requireIdentity(), imageId: this.id, versionString: versionStringArg, }); const imageStream = response.imageStream; return new ReadableStream({ pull: async (controllerArg) => { try { const chunk = await imageStream.receive(); if (chunk === undefined) { await imageStream.accept(); controllerArg.close(); } else { controllerArg.enqueue(chunk); } } catch (errorArg) { await imageStream.abort(errorArg); controllerArg.error(errorArg); } }, cancel: async (reasonArg) => imageStream.reject(reasonArg), }, { highWaterMark: 0 }); }; }