char-slop/berry
multipurpose self-modifiable agentic harness
git clone https://git.t4t.associates/char-slop/berry
c582293
main
1import { Signal } from "@char/aftercare" ; 2 3// signals hold small values one view owns, such as whether a card is open; replicated state stays plain data, which 4// views take apart themselves. a derive of a signal outliving the view would never be released, since a derive 5// attaches to its root signal for good: use `computed`, or a subscription the view's scope ends. 6 7/** Cleanups of one view. Aftercare has no unmount, so views that subscribe own a scope. */ 8export class Scope { 9 #cleanups:(() => void )[] = []; 10add ( cleanup :() => void ) { 11this . #cleanups. push ( cleanup ); 12} 13dispose () { 14for ( const cleanup of this . #cleanups. splice ( 0 )) cleanup (); 15} 16} 17 18/** One item's view in a `keyed` list: `update` gets each new value of the item, and `dispose` runs once it goes. */ 19export type Handle < T > = { 20readonly node :Node ; 21update ?( item :T ) :void ; 22dispose ?() :void ; 23}; 24 25/** 26* `parent`'s children, one per key of the items `sync` is given, in order; a new key makes its handle. 27*/ 28export function keyed < T , K > ( 29parent :Element , 30key :( item :T , index :number ) => K , 31create :( item :T , index :number ) => Handle < T >, 32) { 33let rendered = new Map < K , { handle :Handle < T >; item :T }>(); 34return { 35sync ( items :readonly T [], more = () => true , newestFirst = false ) :boolean { 36const next = new Map < K , { handle :Handle < T >; item :T }>(); 37let complete = true ; 38for ( let offset = 0 ; offset < items . length ; offset ++ ) { 39const index = newestFirst ?items . length - offset - 1 :offset ; 40const item = items [ index ]; 41const k = key ( item , index ); 42const entry = rendered . get ( k ); 43if ( entry !== undefined ) { 44if ( entry . item !== item ) entry . handle . update ?.( item ); 45next . set ( k , { handle :entry . handle , item}); 46} else if ( more ()) next . set ( k , { handle :create ( item , index ), item}); 47else complete = false ; 48} 49for ( const [ k , { handle}] of rendered ) { 50if ( next . has ( k )) continue ; 51handle . dispose ?.(); 52handle . node . parentNode ?. removeChild ( handle . node ); 53} 54if ( newestFirst ) { 55let following :Node | null = null ; 56for ( const { handle :{ node} } of next . values ()) { 57const expected :Node | null = following === null 58 ?parent . lastChild 59 :following . previousSibling ; 60if ( node !== expected ) parent . insertBefore ( node , following ); 61following = node ; 62} 63} else { 64let previous :Node | null = null ; 65for ( const { handle :{ node} } of next . values ()) { 66const expected :Node | null = previous === null 67 ?parent . firstChild 68 :previous . nextSibling ; 69if ( node !== expected ) parent . insertBefore ( node , expected ); 70previous = node ; 71} 72} 73rendered = next ; 74return complete ; 75}, 76dispose () { 77for ( const { handle} of rendered . values ()) handle . dispose ?.(); 78rendered = new Map (); 79}, 80}; 81} 82 83/** Render `parent`'s content afresh whenever `value` changes. */ 84export function swap < T > ( 85scope :Scope , 86parent :Element , 87value :Signal < T >, 88render :( value :T , scope :Scope ) => Node | Node [] | null , 89) { 90let current = new Scope (); 91const { unsubscribe} = value . subscribeImmediate (( v ) => { 92current . dispose (); 93current = new Scope (); 94const content = render ( v , current ); 95parent . replaceChildren ( 96 ...( content === null ?[] :Array . isArray ( content ) ?content :[ content ]), 97); 98}); 99scope . add (() => { 100unsubscribe (); 101current . dispose (); 102}); 103} 104 105/** A signal computed from others, notifying only when `compute` returns a different value. */ 106export function computed < T > ( 107scope :Scope , 108sources :readonly Signal < unknown >[], 109compute :() => T , 110) :Signal < T > { 111const out = new Signal ( compute ()); 112for ( const source of sources ) { 113const { unsubscribe} = source . subscribe (() => out . set ( compute ())); 114scope . add ( unsubscribe ); 115} 116return out ; 117} 118 119export const json = ( value :unknown ) => JSON . stringify ( value , null , 2 ); 120 121const TID_ALPHABET = "234567abcdefghijklmnopqrstuvwxyz" ; 122 123/** When a TID was minted. */ 124export function tidDate ( tid :string ) :Date { 125let n = 0n ; 126for ( const char of tid ) n = ( n << 5n ) | BigInt ( TID_ALPHABET . indexOf ( char )); 127return new Date ( Number ( n >> 10n ) / 1000 ); 128} 129 130const relative = new Intl . RelativeTimeFormat ( undefined , { numeric :"auto" }); 131const RELATIVE_UNITS :[ Intl . RelativeTimeFormatUnit , number ][] = [ 132[ "year" , 365 * 24 * 60 * 60 ], 133[ "month" , 30 * 24 * 60 * 60 ], 134[ "week" , 7 * 24 * 60 * 60 ], 135[ "day" , 24 * 60 * 60 ], 136[ "hour" , 60 * 60 ], 137[ "minute" , 60 ], 138]; 139 140export function relativeTime ( date :Date , now = Date . now ()) :string { 141const seconds = ( date . getTime () - now ) / 1000 ; 142for ( const [ unit , duration ] of RELATIVE_UNITS ) { 143if ( Math . abs ( seconds ) >= duration ) return relative . format ( Math . trunc ( seconds / duration ), unit ); 144} 145return relative . format ( 0 , "second" ); 146} 147 148export function when ( date :Date , { seconds= false } = {}) :string { 149const sameDay = date . toDateString () === new Date (). toDateString (); 150return sameDay 151 ?date . toLocaleTimeString ( undefined , { 152hour :"2-digit" , 153minute :"2-digit" , 154 ...( seconds ?{ second :"2-digit" } :{}), 155}) 156 :date . toLocaleDateString ( undefined , { month :"short" , day :"numeric" }); 157}