char-slop/berry
multipurpose self-modifiable agentic harness
git clone https://git.t4t.associates/char-slop/berry
3d23fd2
main
1import { Signal } from "@char/aftercare" ; 2import { MAX_IMAGES , validateImage } from "@berry/api/images.ts" ; 3import { storedDraft } from "./draft-storage.ts" ; 4 5export type DraftImage = { 6id :string ; 7name :string ; 8blob :Blob ; 9status :"loading" | "ready" | "error" ; 10error ?:string ; 11}; 12type StoredImage = Pick < DraftImage , "id" | "name" | "blob" >; 13 14const message = ( error :unknown ) => error instanceof Error ?error . message :String ( error ); 15 16/** A route's shared image draft, with serialized durable changes across mounted composers. */ 17class ImageDraft { 18readonly items = new Signal < readonly DraftImage []>([]); 19readonly error = new Signal < string | null >( null ); 20readonly loading = new Signal ( typeof indexedDB !== "undefined" ); 21readonly ready :Promise < void >; 22submitting = false ; 23 #queue= Promise . resolve (); 24 25constructor ( readonly route :string ) { 26this . ready = this . loading . get () 27 ?storedDraft < StoredImage >( route ). then (( images ) => { 28this . items . set ( images . map (( image ) => ({ ...image , status :"ready" }))); 29}). catch (( error ) => { 30this . error . set ( `Could not restore image drafts: ${ message ( error )} ` ); 31}). finally (() => this . loading . set ( false )) 32 :Promise . resolve (); 33} 34 35 #enqueue( task :() => Promise < void >) :Promise < void > { 36const pending = this . #queue. then ( task ); 37this . #queue= pending . catch (() => {}); 38return pending ; 39} 40 41async #save( items = this . items . get ()) :Promise < void > { 42await storedDraft ( 43this . route , 44items . filter (( image ) => image . status === "ready" ). map ( 45({ id, name, blob}) => ({ id, name, blob}), 46), 47); 48this . error . set ( null ); 49} 50 51async add ( files :readonly File []) :Promise < void > { 52await this . ready ; 53if ( this . error . get ()) throw new Error ( this . error . get () ! ); 54if ( this . items . get (). length + files . length > MAX_IMAGES ) { 55throw new Error ( `A message can have at most ${ MAX_IMAGES } images` ); 56} 57const added :DraftImage [] = files . map (( blob ) => ({ 58id :Array . from ( 59crypto . getRandomValues ( new Uint8Array ( 16 )), 60( byte ) => byte . toString ( 16 ). padStart ( 2 , "0" ), 61). join ( "" ), 62name :blob . name || "Pasted image" , 63 blob, 64status :"loading" , 65})); 66this . items . set ([ ...this . items . get (), ...added ]); 67await Promise . all ( added . map (( image ) => 68this . #enqueue( async () => { 69try { 70const bytes = new Uint8Array ( await image . blob . slice ( 0 , 12 ). arrayBuffer ()); 71validateImage ( bytes , image . blob . type , image . blob . size ); 72const bitmap = await createImageBitmap ( image . blob ); 73try { 74if ( bitmap . width * bitmap . height > 40_000_000 ) { 75throw new Error ( "Images must be no larger than 40 megapixels" ); 76} 77} finally { 78bitmap . close (); 79} 80if ( ! this . items . get (). some (( item ) => item . id === image . id )) return ; 81const next = this . items . get (). map (( item ) => 82item . id === image . id ?{ ...item , status :"ready" as const } :item 83); 84await this . #save( next ); 85this . items . set ( 86this . items . get (). map (( item ) => 87item . id === image . id ?{ ...item , status :"ready" } :item 88), 89); 90} catch ( error ) { 91this . items . set ( 92this . items . get (). map (( item ) => 93item . id === image . id ?{ ...item , status :"error" , error :message ( error ) } :item 94), 95); 96throw error ; 97} 98}) 99)); 100} 101 102remove ( ids :readonly string []) :Promise < void > { 103this . items . set ( this . items . get (). filter (( image ) => ! ids . includes ( image . id ))); 104return this . #enqueue(() => this . #save()). catch (( error ) => { 105this . error . set ( `Could not save image drafts: ${ message ( error )} ` ); 106throw error ; 107}); 108} 109} 110 111const drafts = new Map < string , ImageDraft >(); 112 113export function imageDraft ( route :string ) :ImageDraft { 114if ( ! drafts . has ( route )) drafts . set ( route , new ImageDraft ( route )); 115return drafts . get ( route ) ! ; 116}