@molecule/app-ai-image-generator-default
Provider bond · ai-image-generator · App (browser) · v1.0.2 · Apache-2.0
Default HTTP image generator provider implementation
npm install @molecule/app-ai-image-generator-defaultnpm · Source on GitHub · Implements @molecule/app-ai-image-generator
How it works
@molecule/app-ai-image-generator-default is a provider bond on the app (browser) side: it implements the ai-image-generator core interface (@molecule/app-ai-image-generator) with a concrete vendor or library behind it.
Your code calls the core; you wire this provider once at startup. Swapping vendors later is one line in that wiring, not a rewrite.
import { setProvider } from '@molecule/app-ai-image-generator'
import { provider } from '@molecule/app-ai-image-generator-default'
setProvider(provider) // custom baseUrl/headers: setProvider(createProvider({...}))Works with: @molecule/app-i18n
Reference
Default HTTP image generator provider for molecule.dev — generates images through YOUR backend endpoint (JSON or SSE), with history load/delete.
Auto-generated, AI-first package reference for the molecule.dev ecosystem. It is written to be read by coding agents as much as by people, and is generated from this package's source — edit
src/index.tsJSDoc, not this file.
Quick Start
import { setProvider } from '@molecule/app-ai-image-generator'
import { provider } from '@molecule/app-ai-image-generator-default'
setProvider(provider) // custom baseUrl/headers: setProvider(createProvider({...}))
Type
provider
Installation
npm install @molecule/app-ai-image-generator-default @molecule/app-ai-image-generator @molecule/app-i18n
API
Interfaces
HttpImageGeneratorConfig
Configuration for the default HTTP image generator provider.
interface HttpImageGeneratorConfig {
/** Base URL for API requests. Defaults to `''` (same origin). */
baseUrl?: string
/** Custom headers to include in requests. */
headers?: Record<string, string>
}
Classes
HttpImageGeneratorProvider
HTTP-based implementation of AIImageGeneratorProvider.
Sends generation requests via POST, reads progress via SSE or JSON responses, and supports history loading and image deletion.
class HttpImageGeneratorProvider implements AIImageGeneratorProvider {
readonly name = 'default'
private config: HttpImageGeneratorConfig
private abortController: AbortController | null = null
/**
* Creates a new HTTP image generator provider.
*
* @param config - HTTP-specific configuration (base URL, headers).
*/
constructor(config: HttpImageGeneratorConfig = {}) {
this.config = config
}
/**
* Generates images by sending a POST request to the backend.
* Handles both SSE streaming responses (with progress events) and
* plain JSON responses (single batch of images).
*
* @param request - The generation parameters.
* @param config - API endpoint and model configuration.
* @param onEvent - Callback for generation lifecycle events.
* @returns The array of generated images.
*/
async generate(
request: ImageGenerationRequest,
config: ImageGenerationConfig,
onEvent: ImageGenerationEventHandler,
): Promise<GeneratedImage[]> {
this.abortController?.abort()
const controller = new AbortController()
this.abortController = controller
const { signal } = controller
const url = `${this.config.baseUrl ?? ''}${config.endpoint}`
const { width, height } = parseSize(request.size)
try {
const response = await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
...this.config.headers,
},
body: JSON.stringify({
prompt: request.prompt,
...(request.negativePrompt ? { negativePrompt: request.negativePrompt } : {}),
...(request.size ? { size: request.size } : {}),
...(request.count != null ? { count: request.count } : {}),
...(request.format ? { format: request.format } : {}),
...(request.quality ? { quality: request.quality } : {}),
...(request.style ? { style: request.style } : {}),
model: request.model ?? config.model,
}),
signal,
})
if (!response.ok) {
const text = await response
.text()
.catch(() =>
t('imageGenerator.error.unknown', undefined, { defaultValue: 'Unknown error' }),
)
let errorMessage: string | undefined
try {
const body = JSON.parse(text)
if (typeof body.error === 'string') errorMessage = body.error
} catch (_error) {
// Not JSON — use raw text as-is; parse failure is expected for plain-text error bodies
}
onEvent({
type: 'error',
message:
errorMessage ??
t(
'imageGenerator.error.httpError',
{ status: response.status, text },
{ defaultValue: 'HTTP {{status}}: {{text}}' },
),
})
return []
}
const contentType = response.headers.get('content-type') ?? ''
// SSE streaming response — parse progress and image events
if (contentType.includes('text/event-stream')) {
return await this.handleStreamResponse(response, onEvent, controller, signal)
}
// Plain JSON response — single batch of images
return await this.handleJsonResponse(response, onEvent, width, height)
} catch (err) {
if (err instanceof Error && err.name === 'AbortError') {
return []
}
const message =
err instanceof Error
? err.message
: t('imageGenerator.error.generateFailed', undefined, {
defaultValue: 'Image generation failed',
})
onEvent({ type: 'error', message })
return []
} finally {
if (this.abortController === controller) {
this.abortController = null
}
}
}
/**
* Handles an SSE streaming response, parsing progress and image events.
*
* @param response - The fetch Response with an SSE body.
* @param onEvent - Event callback.
* @param controller - The AbortController for this request.
* @param signal - The abort signal.
* @returns The collected generated images.
*/
private async handleStreamResponse(
response: Response,
onEvent: ImageGenerationEventHandler,
controller: AbortController,
signal: AbortSignal,
): Promise<GeneratedImage[]> {
if (!response.body) {
onEvent({
type: 'error',
message: t('imageGenerator.error.noResponseBody', undefined, {
defaultValue: 'No response body',
}),
})
return []
}
const reader = response.body.getReader()
const decoder = new TextDecoder()
let buffer = ''
const images: GeneratedImage[] = []
while (true) {
const { done, value } = await reader.read()
if (done) break
buffer += decoder.decode(value, { stream: true })
const lines = buffer.split('\n')
buffer = lines.pop() ?? ''
for (const line of lines) {
if (!line.startsWith('data: ')) continue
const data = line.slice(6).trim()
if (!data) continue
try {
const event = JSON.parse(data) as ImageGenerationEvent
if (event.type === 'image') {
images.push(event.image)
}
onEvent(event)
} catch (_error) {
// Skip malformed SSE data — a single bad line should not abort the stream
}
}
// Yield to the main thread between chunks
if (!signal.aborted) {
await new Promise((r) => setTimeout(r, 0))
}
}
// Process any remaining buffer
if (buffer.startsWith('data: ')) {
const data = buffer.slice(6).trim()
if (data) {
try {
const event = JSON.parse(data) as ImageGenerationEvent
if (event.type === 'image') {
images.push(event.image)
}
onEvent(event)
} catch (_error) {
// Skip malformed data in the trailing buffer — safe to discard incomplete/invalid events
}
}
}
// Only clear if this call's controller is still the active one
if (this.abortController === controller) {
this.abortController = null
}
return images
}
/**
* Handles a plain JSON response containing generated images.
*
* @param response - The fetch Response with a JSON body.
* @param onEvent - Event callback.
* @param width - Expected image width.
* @param height - Expected image height.
* @returns The generated images.
*/
private async handleJsonResponse(
response: Response,
onEvent: ImageGenerationEventHandler,
width: number,
height: number,
): Promise<GeneratedImage[]> {
const body = (await response.json()) as {
images?: Record<string, unknown>[]
}
const rawImages = body.images ?? []
const images: GeneratedImage[] = rawImages.map((raw, i) => {
const img = normalizeImage(raw, i)
// Fall back to request dimensions if the server didn't include them
if (!raw.width) img.width = width
if (!raw.height) img.height = height
return img
})
onEvent({ type: 'started' })
for (const image of images) {
onEvent({ type: 'image', image })
}
onEvent({ type: 'done', images })
return images
}
/**
* Aborts the current in-flight generation request, if any.
*/
abort(): void {
this.abortController?.abort()
this.abortController = null
}
/**
* Loads previously generated images from the server via GET request.
*
* @param config - API endpoint configuration.
* @returns An array of generated images, or an empty array on failure.
*/
async loadHistory(config: ImageGenerationConfig): Promise<GeneratedImage[]> {
const url = `${this.config.baseUrl ?? ''}${config.endpoint}`
try {
const response = await fetch(url, {
headers: this.config.headers,
})
if (!response.ok) return []
const data = (await response.json()) as {
images?: Record<string, unknown>[]
// … truncated — the full declaration is in node_modules/@molecule/app-ai-image-generator-default/dist/provider.d.ts
Functions
createProvider(config)
Creates an HttpImageGeneratorProvider instance.
function createProvider(config?: HttpImageGeneratorConfig): HttpImageGeneratorProvider
config— HTTP-specific configuration (base URL, headers).
Returns: A configured HttpImageGeneratorProvider.
Constants
provider
Pre-instantiated provider singleton.
const provider: HttpImageGeneratorProvider
Core Interface
Implements @molecule/app-ai-image-generator interface.
Bond Wiring
Setup function to register this provider with the core interface:
import { setProvider } from '@molecule/app-ai-image-generator'
import { provider } from '@molecule/app-ai-image-generator-default'
export function setupAiImageGeneratorDefault(): void {
setProvider(provider)
}
Injection Notes
Requirements
Peer dependencies:
@molecule/app-ai-image-generator^1.0.1@molecule/app-i18n^1.0.1
Runtime Dependencies
@molecule/app-ai-image-generator@molecule/app-i18n
Talks to YOUR backend at config.endpoint — no vendor key in the browser.
Server contract: POST { prompt, negativePrompt?, size?, count?, format?, quality?, style?, model? }; reply EITHER as plain JSON
{ images: [{ id, url, prompt, width?, height?, createdAt? }] } OR as an
SSE stream (Content-Type: text/event-stream) of
data: <ImageGenerationEvent JSON> lines (started / progress /
image / done / error) — the bond auto-detects by content type. GET
the same endpoint → { images } for history (fails open to []); DELETE
${endpoint}/${id} removes one image. generate() NEVER rejects: on any
failure it emits an error event and resolves [] — drive error UI from
the event callback, not try/catch.
E2E Tests
Integration checklist — drive the real UI (live preview, no mocks), adapt each item to this app's actual screens/flows, and check every box off one by one. A box you can't check is an integration bug to fix — not a skip:
- Entering a prompt and generating renders the resulting
GeneratedImage(s) as real pixels on screen: theimage/doneevents deliver objects whoseurlloads to a visible picture, not a broken or placeholder tile. - A generating indicator shows while the request is in flight — driven
by the
started/progress(progress.percent) events — and clears whendonefires andgenerate()resolves; it never spins forever. - A generation failure surfaces visibly: the
errorevent'smessage(provider failure, or a moderation-rejected prompt) renders as a message in the UI, never a silent no-op or a stuck spinner. - When the request sets
count> 1 (bounded 1–10), ALL requested images render — thedoneevent'simagesarray length matches what was asked, not just the first one. - The request's
sizepreset (e.g.'1024x1024'vs'1792x1024') is honored in the output: the rendered image'swidth/heightmatch the requested dimensions rather than a default square. - The generated image is usable downstream as the app wires it
(insert / download / select), carrying the real
GeneratedImagedata (id,url); becauseurlmay be temporary, an image kept in a gallery still loads after a full reload — proving the app persisted the bytes, not a dead upstream link. - Generations are scoped to the requesting user:
loadHistory/ the gallery returns only that user's own images, and a moderation-rejected prompt shows the rejection rather than a blank tile.