char-slop/berry
multipurpose self-modifiable agentic harness
git clone https://git.t4t.associates/char-slop/berry
efe2e1f
main
1import type { JsonValue } from "@earendil-works/chord" ; 2import { diffRevisions , type Op } from "@earendil-works/chord/delta" ; 3 4/** One json value: its current revision, then the ops of each next one. Durable watches are feeds. */ 5export type Feed = { 6readonly value :unknown ; 7/** Never invoked inline; ops apply to `value` as read just before `start`. */ 8start ( listener :( value :unknown , ops :readonly Op []) => Promise < void >) :void ; 9stop () :Promise < unknown >; 10}; 11 12export type ClientMessage = 13| { t :"auth" ; token :string } 14| { t :"sub" ; topic :string } 15| { t :"unsub" ; topic :string }; 16 17export type ServerMessage = 18| { t :"ready" } 19/** `older` counts the entries before `value.entries`, which `older` messages bring, newest first, before any ops. */ 20| { t :"snapshot" ; topic :string ; value :unknown ; older ?:number } 21| { t :"older" ; topic :string ; entries :unknown [] } 22| { t :"ops" ; topic :string ; ops :readonly Op [] } 23| { t :"error" ; topic ?:string ; message :string }; 24 25export type PubsubOptions = { 26authorize ( token :string ) :boolean ; 27/** `undefined` for unknown topics. */ 28resolve ( topic :string ) :Promise < Feed | undefined >; 29}; 30 31const AUTH_TIMEOUT_MS = 10_000 ; 32 33/** How much of a history its snapshot carries, of the newest; the rest follows in chunks of about `CHUNK_BYTES`. */ 34const TAIL_BYTES = 32_768 ; 35const CHUNK_BYTES = 262_144 ; 36 37/** 38* A snapshot's frames. A value with `entries`, as a conversation is, sends its newest entries first and then the rest, 39* so that a reader far away sees the latest soonest; each frame is encoded only as it is sent. 40*/ 41function * snapshot ( topic :string , value :unknown ) :Generator < string > { 42const entries = ( value as { entries ?:unknown } | null )?. entries ; 43if ( ! Array . isArray ( entries )) return yield JSON . stringify ({ t :"snapshot" , topic, value}); 44// the encoded entries from `end` back that fit in `budget`; at least one fits 45const take = ( end :number , budget :number ) => { 46const taken :string [] = []; 47for ( let at = end , bytes = 0 ; at > 0 ;) { 48const encoded = JSON . stringify ( entries [ at - 1 ]); 49if ( taken . length > 0 && bytes + encoded . length > budget ) break ; 50taken . push ( encoded ); 51bytes += encoded . length ; 52at -- ; 53} 54return taken . reverse (); 55}; 56const tail = take ( entries . length , TAIL_BYTES ); 57const from = entries . length - tail . length ; 58const { entries :_ , ...others } = value as { entries :unknown [] }; 59const rest = JSON . stringify ( others ). slice ( 1 , - 1 ); 60yield `{"t":"snapshot","topic": ${ JSON . stringify ( topic )} , ${ 61from ? `"older": ${ from } ,` : "" 62} "value":{ ${ rest } ${ rest ? "," : "" } "entries":[ ${ tail . join ( "," )} ]}}` ; 63for ( let end = from ; end > 0 ;) { 64const chunk = take ( end , CHUNK_BYTES ); 65yield `{"t":"older","topic": ${ JSON . stringify ( topic )} ,"entries":[ ${ chunk . join ( "," )} ]}` ; 66end -= chunk . length ; 67} 68} 69 70export function servePubsub ( socket :WebSocket , { authorize, resolve} :PubsubOptions ) :void { 71let authorized = false ; 72const subscriptions = new Map < string , Promise < Feed | undefined >>(); 73const sendFrame = ( frame :string ) => { 74if ( socket . readyState === WebSocket . OPEN ) socket . send ( frame ); 75}; 76const send = ( message :ServerMessage ) => sendFrame ( JSON . stringify ( message )); 77const authTimeout = setTimeout (() => socket . close ( 4001 , "unauthorized" ), AUTH_TIMEOUT_MS ); 78 79async function subscribe ( topic :string ) { 80if ( subscriptions . has ( topic )) return ; 81const pending = resolve ( topic ); 82const entry = pending . catch (() => undefined ); 83subscriptions . set ( topic , entry ); 84let feed :Feed | undefined ; 85try { 86feed = await pending ; 87} catch ( error ) { 88feed = undefined ; 89console . error ( `[pubsub] ${ topic } :` , error ); 90} 91// unsubscribed meanwhile: unsubscribe() stops the feed 92if ( subscriptions . get ( topic ) !== entry ) return ; 93if ( feed === undefined ) { 94subscriptions . delete ( topic ); 95send ({ t :"error" , topic, message :`no topic ${ topic } ` }); 96return ; 97} 98for ( const frame of snapshot ( topic , feed . value )) sendFrame ( frame ); 99feed . start (( _ , ops ) => Promise . resolve ( send ({ t :"ops" , topic, ops}))); 100} 101 102async function unsubscribe ( topic :string ) { 103const pending = subscriptions . get ( topic ); 104subscriptions . delete ( topic ); 105await ( await pending )?. stop (); 106} 107 108socket . addEventListener ( "message" , ( event ) => { 109let message :ClientMessage ; 110try { 111message = JSON . parse ( String ( event . data )); 112} catch { 113socket . close ( 4000 , "malformed message" ); 114return ; 115} 116if ( ! authorized ) { 117clearTimeout ( authTimeout ); 118if ( message . t !== "auth" || ! authorize ( message . token )) { 119socket . close ( 4001 , "unauthorized" ); 120return ; 121} 122authorized = true ; 123send ({ t :"ready" }); 124return ; 125} 126if ( message . t === "sub" ) void subscribe ( message . topic ); 127else if ( message . t === "unsub" ) void unsubscribe ( message . topic ); 128}); 129 130socket . addEventListener ( "close" , () => { 131clearTimeout ( authTimeout ); 132for ( const topic of [ ...subscriptions . keys ()]) void unsubscribe ( topic ); 133}); 134} 135 136/** A json value the server changes, which any number of feeds follow as a snapshot and then diffs. */ 137export function published < T > ( initial :T ) { 138let value = initial ; 139const listeners = new Set <( value :unknown , ops :readonly Op []) => Promise < void >>(); 140return { 141get value () { 142return value ; 143}, 144set ( next :T ) { 145if ( next === value ) return ; 146const ops = diffRevisions ( value as JsonValue , next as JsonValue ); 147value = next ; 148if ( ops . length === 0 ) return ; 149for ( const listener of listeners ) void listener ( value , ops ); 150}, 151feed () :Feed { 152let listener :(( value :unknown , ops :readonly Op []) => Promise < void >) | undefined ; 153return { 154get value () { 155return value ; 156}, 157start ( next ) { 158listener = next ; 159listeners . add ( next ); 160}, 161stop () { 162if ( listener ) listeners . delete ( listener ); 163return Promise . resolve (); 164}, 165}; 166}, 167}; 168}