calendar
All repositories: gitoria
146.8 KB
// The framework's client half — a file-class constructed ONCE in the browser by// the page's module script, with the seed the server rendered from. The// components are NOT preloaded: each mount carries the url its module is served// at and `hlLoad` fetches it when the mount happens. This file is the client// realm's entrypoint, and the page script sets that realm before constructing// it.//// Its whole job: CLAIM the DOM the server rendered for the mounted instances,// remember which elements read which member (the SITES), repaint a site when a// member is written, and turn a same-origin link into a navigation that fetches// the next route's tree and state and swaps the slot — the shell stays.import HlwView from './view.hl'import HlwRouter from './router.hl'seedText = '' // the server's seed, as JSON text (the one construction argument)seed = null // { page: { key module params state view }, shell: … | null }view = new HlwViewrouter = null // the client's route table, from the seedshell = null // { key, instance, view, sites, root }page = nullappHead = null // the app's manifest head defaults, off the seed: what a page// member that is null falls back to (title, description, image,// favicon, and the app's own meta list)slotEl = null // the shell element whose children are the route component// WHERE IN IT (ticket #81's finding): the shell's slot may stand beside other elements// of the shell — `body { header { … } slot }` — and the page's nodes are then not all// of slotEl's children. `slotAt` is the index of the page's first element among them,// and `slotStart` the open mark of the region the page is (a page in a shell stands// between two marks like every region), so the page is claimed from the right element// and a navigation replaces what stands between the marks and nothing of the shell's.slotAt = 0slotStart = nulldetachedSlot = null // the slot element a shell region's rebuild took out of the DOM (see paint)socket = null // the websocket carrier, while it is opensocketOpened = false // it has opened (a socket that closes without opening is a failed reconnect)retryMs = 500 // the wait before the next reconnect: doubles per failure up to 15 s, reset on opennavigating = false // a navigation is running (show); navNext is the one asked for meanwhilenavNext = nullpinging = false // the ping interval is started once, with the first socketpongDue = null // the timer that takes the socket for dead when its pong is latenextId = 0 // the pairing id an emit's ack comes back onpending = {} // 'k<i>' → the resolver waiting for that ack// AN ANSWER THAT ARRIVES BEFORE ITS PAGE IS MOUNTED (ticket #80). A component's root// runs while `instantiate` constructs it, and an `emit server` there goes out at once// (over REST: the socket opens after boot). What the face raised for this tab comes// back in the ack, before `page` — or, on a navigation, the NEXT page — is the mount// that holds the listener; delivered then, it reached the old mounts or none and was// lost. While a mount is being built the frames wait here, and go out in arrival// order once it stands.mounting = falseheld = []blueprints = {} // component key → { module, view }: every tree the page can// build, taken from each seed and never dropped (takeBlueprints)strict = false // the gates' client: a repaint that finds a stale site throws (staleSite)painting = 0 // repaints running: 0 when the page stands still (the gates wait for it)turning = false // a patch is running (patch): the ones asked for meanwhile wait in patchQueuepatchQueue = []sidNext = 0 // every site's own number, so a repaint can tell one that has leftdropGen = 0 // counts the drops of sites (dropSitesIn): a repaint's list is checked again after onependingDrops = [] // ranges that left holding a host's fill: that host's sites and children in them// go in settle(), once every mount on the walk has been written backpendingFills = [] // fills a region rebuild anchored but could not build in place:// the host is named by address and re-entered in settle() belowlateJobs = [] // late patches (#98, #99): see latePatchlateDue = false// THE EVENTS A REFERENCE OVERWROTE ON THE COMPOSITION'S ROOT, while the walk is inside// that composition and has not yet reached an element. A composed child's View may be a// bare reference to another component (routger's dialog-form.hl is one), so the element// that FINALLY stands as the child's root can be any number of references deeper — this// rides down through them and is spent on the first elements the walk claims or builds,// which are exactly those roots.rootOverwrites = null// IS THE PAGE ON THE DOCUMENT? A shell decides per request whether its render contains// a `slot` (COMPONENTS §7, "THE PARENT IS THE GATE"): `if (loggedIn) { main { slot } }`// standing false renders no page at all, and there is then nothing to claim. False here// means there IS no page: the server did not construct it and the seed carries none// (creator, 2026-08-26 — the parent is the gate). It is asked for and constructed when// the region holding the slot turns true, and dropped again when it turns false.pageShown = falsepageArrived = false // a region's rebuild constructed the page: its held frames go out after it// THE OFFLINE LAYER (COMPONENTS §10), live only when the seed says the app keeps pages// offline. `store` is the IndexedDB database, opened at boot; `unacked` holds what a// queueable emit sent on the socket carried until its ack, so a socket that closes// under it queues the emit instead of losing it; `flushing` is true while the queue// is replayed; `retryTimer` is the reconnect that is waiting.store = nullunacked = {}replays = {} // 'k<i>' → what ends a replayed entry's wait when its socket closes// AN EMIT IN FLIGHT OUTLIVES ITS SOCKET (ticket #107). Every emit that went out on the// socket is here until its ack, as the frame it was: a socket that closes under it// hands it on to the POST fallback or the next socket, with the same `q`, and the// server's exactly-once path answers the repeat instead of running the face again.// `post` is true while the POST fallback carries it, `lost` once a POST failed.inflight = {} // 'k<i>' → { text; event; post; lost; }// A PAGE BEING LEFT starts nothing. Firefox closes the page's socket the moment a// document navigation starts, right after `beforeunload` and long before `pagehide`;// a reconnect or a POST made then, and an answer that loads a document, would cancel// the navigation the user started. `beforeunload` is only a maybe (a download, a// navigation the server answers with 204), so after it the page takes itself for// staying once `stayMs` have passed and it is still here.leaving = falsestayTimer = nullstatic stayMs = 3000flushing = falseretryTimer = nullowner = '' // whose the kept copy is (settleWho): the last user logged inonlineBound = false // the online/offline listeners are on the window (syncOnline)holding = false // an anonymous page over a user's copy: nothing is kept or laid over itheldBack = false // the server said a queued entry is for another user than the session'squeueRetryTimer = null // a round that did not fully drain tries again after queueRetryWait// ---- THE PACKAGE'S BROWSER SURFACE: what `import { … } from 'hl:web'` binds ------//// A file's STATICS are what a braced import may take (the language's one rule for// `import { db } from './store.hl'`, and the same rule across a package's url),// so everything this half offers an app's component is declared here and nowhere// else. A static belongs to the CLASS, which is why these two reach each other// and why they are the SAME pair for every component module the page imports —// the browser loads this file once.//// `live` IS THE PAGE'S ONE CLIENT, written by the root below. The statics cannot// see an instance (they are evaluated at file load, before there is one), and a// static that is a FUNCTION does not need to: its body runs at the CALL, long// after the root has put the constructed client here.static live = { client = null }// PROGRAMMATIC NAVIGATION — the same act a click on a same-origin `<a>` performs,// asked for by code instead of by a pointer: a new history entry, then the next// route's tree and state over the boundary and into the shell's slot. A handler// that has just been told an id and must go to that thing's page has no link to// click, and this is what it calls instead. Everything about what a navigation IS// stays in `goTo` below; this is only the way in from outside the file.static navigate = (path) => { return live.client.goTo(path) }// A NOTIFICATION, now or at a set time (COMPONENTS §10, ticket #95). `at` is epoch// milliseconds. The browser is asked for permission the first time. It is shown by// the service worker when the app has one (so an installed app shows it as the app),// else by the page. A notification AT A SET TIME is a timer of this page: it fires// while the page is open — a browser offers no way to wake a closed app at a time// without a server push, and this does not pretend otherwise.static notify = (title, options) => { return live.client.showNotice(title, options) }static notifyAt = (at, title, options) => { return live.client.noticeAt(at, title, options) }// …or after `ms` millisecondsstatic notifyIn = (ms, title, options) => { return live.client.noticeIn(ms, title, options) }// FORGET WHAT THIS BROWSER KEPT OFFLINE (ticket #125): the kept state, the queue and the// worker's kept documents. hl:web does it itself when the session's user changes (a log-out// included) at the next page it is told about; an app that logs out in place calls this.static forgetOffline = () => { return live.client.forgetOffline() }// ---- boot: construct the instances the server rendered, claim their DOM ----------// THE PAGE'S CLIENT, PUBLISHED TO THE FILE'S OWN CLASS, before anything else the// root does: `boot()` below constructs the components, and a component's module// may reach `navigate` from the moment it is loaded.Client.live.client = this// A VIEW EXPRESSION in a module's code is evaluated by view.hl's one `evalExpr`, so the// language's own operators answer in the browser as they do on the serverwindow.__hlX = (x, inst, rows) => { return view.evalExpr(x, inst, rows) }seed = JSON.parse(seedText)router = new HlwRouter(routes = seed.routes)boot()boot() {mounting = truestrict = seed.strict == truetakeBlueprints(seed.blueprints)if (seed.shell != null) {shell = instantiate(seed.shell, [], 'shell')shell.isShell = true// (the BODY ELEMENT, nothing to do with `rootKind` below — a mount's `rootKind`// says which of the two roots it hangs from, 'page' or 'shell'. This field held// the DOM node under the same name until 2026-09-14, and the shell's address// therefore read as an element: every lift and every fill re-entry from a// composition IN THE SHELL resolved against the page instead, so a composed// control there wrote nothing — creator, W15.)shell.root = document.body// the body node's children are claimed against document.body, so the body// element takes the node itself here: a region hosted directly on it (an `if`// at the shell's root) rebuilds from it (measured 2026-09-13: a lone child// under `body` stayed on screen after its condition turned false)claim(&shell, document.body)}page = seed.page != null ? instantiate(seed.page, [], 'page') : null// no shell: the page IS the document's body contentif (shell == null) { slotEl = document.body }// THE SHELL RENDERED NO SLOT — its region stood false for this request, so the// document carries no page. Claiming one anyway walked the page's tree against the// SHELL's own elements and silently attached the page's nodes to them ("claim: no// element for … under …" as soon as the two shapes differ), and the page never// appeared when the region later turned true. It waits instead.if (slotEl != null && page != null) {slotEl.__hlCur = slotStartclaim(&page, slotEl, slotAt)pageShown = true}// the head members are sites of this mount from here on. They are NOT painted// now: the server's `document()` already wrote them into this document, and a// write to one of them repaints it the way every other member's write does.appHead = seed.headif (page != null) { headSites(&page) }release()listen()syncOnline()if (seed.offline != null) { standIn() }if (seed.offline != null) { offlineBoot() }connect()}// A DOCUMENT THAT STANDS IN FOR THIS URL (#124): the worker answered an unvisited param page// with a kept document of the same component, so the seed's parameters are another url's.// This url's are the truth: they go into the instance, and what they feed is derived again// here (what only the server can derive stays as that document had it, until a push).standIn() {if (page == null || seed.page == null) { return null }let m = router.match(location.pathname)if (m == null || m.route.component != true) { return null }let mine = JSON.stringify(m.params)if (mine == JSON.stringify(seed.page.params)) { return null }for (k of m.params.keys()) { page.instance[k] = m.params[k] }seed.page.params = m.paramspatch(page.chain, 'page', moved(&page, m.params.keys()), [])return null}// THE NEXT PAGE INTO THE SLOT, and nothing else of the shell's: the slot's element may// hold the shell's own elements beside the page (`body { header { … } slot }`), which// emptying it took with the old page. The new page is built where the old one stood.placePage(&m) {if (!standing(slotEl, slotStart)) {// the page is all the slot's element holds: empty it, as it always wasslotEl.replaceChildren()create(&m, slotEl)return null}for (n of between(slotStart)) { n.remove() }let before = slotEl.childNodes.lengthcreate(&m, slotEl)let made = Array.from(slotEl.childNodes).slice(before)for (n of made) { slotEl.insertBefore(n, slotStart.__hlEnd) }return null}// the frames that waited for the mounts (above), now that they standrelease() {mounting = falselet frames = heldheld = []for (f of frames) { outward(f) }return null}// a mount's instance: the module LOADED (hlLoad — the loader primitive, which// on this target is the dynamic import of the url the server compiled the// component to), the class constructed, then the server's state laid over it.// The import is the browser's own cache: a second mount of the same component// fetches nothing.// The route's params and the server's state are the construction's NAMED// ARGUMENTS, pinned before the root runs — so a member the server evaluated// (a seeded one, declared without its initializer in this realm's projection)// is already there when a later line reads it, and a client-evaluable// initializer keeps the server's value instead of recomputing it.// THE BLUEPRINT TABLE, read by component key: the url the component's module is// served at and its View tree, one copy for the whole page. Every mount's tree is// read from here — the page's, the shell's, and every child's — and so is every// child the browser has to build itself.//// IT ONLY GROWS. Each seed carries the table for ITS route, and a navigation takes// the new entries WITHOUT dropping the old: a key is a component and a component is// one tree, so an entry is never wrong, and constructing a mount awaits its module —// long enough for a second navigation to have replaced the seed underneath it. A// table that was replaced left that half-built page reading the wrong route's// entries ("no blueprint for components/post.hl", on `/`, measured in the reference// gate 2026-09-13).takeBlueprints(table) {for (k of table.keys()) {if (blueprints[k] == null) { blueprints[k] = table[k] }}return null}blueprintOf(key) {return blueprints[key]}treeOf(key) {let bp = blueprintOf(key)// the table covers every component the page can reach, so this is a torn seed// and not a missing feature — say so loudly and paint nothing for that mountif (bp == null) { console.error('framework: the seed carries no blueprint for ' + key)return [] }return bp.view}// `chain` and `root` are the mount's ADDRESS: the kid keys from the page (or the// shell) down to it. A mount record is a value, so a site cannot hold the mount that// owns its fill — it holds this address, and `fillAt` re-enters the mount by// reference from the root when the fill has to be rebuilt (see settle below).instantiate(m, chain, root) {let args = {}for (k of m.state.keys()) { args[k] = m.state[k] }for (k of m.params.keys()) { args[k] = m.params[k] }let inst = hlLoad(m.module, args)// THE TREE COMES FROM THE TABLE, by this mount's key: a View belongs to the// COMPONENT, and the seed carries one copy of it however many mounts share it.let out = { key = m.key; instance = inst; view = treeOf(m.key); sites = []; isShell = false; kids = {}; bindings = []; chain = chain; rootKind = root; }// THE MIRROR: the components this one composes are instances here too, exactly// as the server stacked them — one per component node, constructed with the// bindings the server evaluated; a binding to a host MEMBER is remembered, so a// host write reaches the child's member and repaints the child's sitesfor (k of m.kids.keys()) { out.kids[k] = instantiate(m.kids[k], childChain(chain, k), root) }bindKids(&out, out.view)// A BOUND MEMBER'S VALUE IS THE HOST'S, HERE. The seed carried the server's// value, which is right for data and null for a FUNCTION (JSON has no form for// one) — and a function the host hands its child (`PostForm { onCancel = cancel// }`) is the way a child talks upward without knowing its host: it calls what it// was given, and the function writes the host's members through the instance// that wrote it. So every member binding is applied from the host instance now.for (b of out.bindings) {let kid = out.kids[b.kid]if (kid != null) {kid.instance[b.target] = bindingValue(b, &out)out.kids[b.kid] = kid}}return out}bindKids(&m, nodes) {for (n of nodes) {if (n.k == 'component') {// THE EDGE STANDS WHETHER OR NOT THE SERVER MOUNTED THE CHILD: a branch that does// not stand is not mounted, and the browser builds the child when it comes to// stand — which must then follow the host's member (every reader of `m.bindings`// passes over an edge whose child is not there){// A BINDING IS A READ at the reference site: a member (`count = likes`)// or a field path off one (`postId = row.id`). Both follow the host's// write — the field path under the name it starts at.for (b of n.bindings) {// A MEMBER THAT ONLY FEEDS A BINDING still follows a write: `repaint`// walks the bindings after the sites, so a host member no element of the// host shows still reaches the child that was bound to it (measured// 2026-09-13 on the social app: `FollowButton { following =// authorFollowed }` stayed false). Under the digest that needed the// member to be in the compared map; under the write sets it needs// nothing but the edge itself.if (b.member != null) { m.bindings.push({ name = b.member; kid = view.kidKey(n.path); target = b.name; ref = null; }) }else if (b.ref != null) { m.bindings.push({ name = b.ref.name; kid = view.kidKey(n.path); target = b.name; ref = b.ref; }) }}}// the fill is THIS mount's fragment: a reference inside it binds against this instanceif (n.fill != null) { bindKids(&m, n.fill) }} else if (n.k == 'el') {bindKids(&m, n.children)} else if (n.k == 'if') {bindKids(&m, n.then)bindKids(&m, n.other)}}return null}// a body-rooted View's children are the body's childrenchildrenOf(nodes) {if (nodes.length == 1 && nodes[0].k == 'el' && nodes[0].tag == 'body') { return nodes[0].children }return nodes}// ---- WHAT THE COMPILE STEP ANSWERED, asked by name ---------------------------------// A local event's write set stands at its own listener and a region's names at its own// region: the compile step wrote them into the statements. These three are asked for by// NAME at run time and cannot be — an inbound frame names its event, a routine a host// handed down names the member that holds it, and a write set names the derivations// that follow it — so the component's compiled half carries them beside its walks.// Nothing here reduces a table: the answers are already the answers.eventWrites(key, event) {let comp = compiledOf(key)if (comp == null) { return [] }let names = comp.ev[event]return names == null ? [] : names}memberWrites(key, name) {let comp = compiledOf(key)if (comp == null) { return [] }let names = comp.mw[name]return names == null ? [] : names}derivationsOf(key) {let comp = compiledOf(key)if (comp == null) { return [] }return comp.dv}// ---- THE COMPILED COMPONENT: this framework's own output for this file ------------// The old hl:web walked a tree the seed carries — it pairs an element with a node, asks a table// what the element shows, and asks `view.value` what each bound spot holds. hl:web// ships the answers as the component's own code (plugins/web/compile.hl), and the// functions below are the FIXED runtime that code calls: the mount records, the keyed// region diff, the binding edges, the derivations, the socket and navigation stay here,// where they belong to the framework and not to any one component.//// EVERY CALL INTO GENERATED CODE PASSES BY REFERENCE. A plain call copies its// arguments (the language's value semantics), which for an instance holding a 2000-row// list would copy the list on every paint; `&` at the call site hands the value itself,// the way every walk in this file already hands a mount record.compiledOf(key) {let all = window.__hlCif (all == null) { return null }return all[key]}// the walk of a component's whole View, of one `for` body, of an `if` branch, or of// the fragment a reference fills — each under its own sitewalkOf(key, slot, site) {let comp = compiledOf(key)if (comp == null) { return null }if (slot == 'v') { return comp.v }if (slot == 'b') { return comp.b[site] }if (slot == 't') { return comp.t[site] }if (slot == 'e') { return comp.e[site] }if (slot == 'f') { return comp.f[site] }return null}// ONE ENTRY INTO GENERATED CODE. `f` is the walk, `m` the mount it runs for, `o` the// mount whose fragment a `slot` inside it would place.run(f, &m, &o, host, i, rows, rk, fill, cr, opt) {if (f == null) { return i }let c = thisreturn f(&c, &m, &o, &host, i, &rows, rk, &fill, cr, &opt)}acqFail(tag, host) {console.error('claim: no element for', tag, 'under', host == null ? null : host.tagName)return null}// the classes a reference's `#Child` rule put on the composition root, beside whatever// class the root itself carries (view.hl withRootClasses, on the build walk)rootClasses(el, opt) {if (opt == null || opt.classes == null || opt.classes.length == 0) { return null }let add = ''for (c of opt.classes) { add = add == '' ? c : add + ' ' + c }let have = el.getAttribute('class')el.setAttribute('class', have == null || have == '' ? add : have + ' ' + add)return null}// AN ELEMENT THAT SHOWS SOMETHING is a site of this mount. The names come from the// generated call — the compile step read them off the View — and the two-way `value`// bind is applied where the element is a form control the member paints.siteAt(&m, el, names, site, valueMember, isSelect, rows) {let keep = []for (name of names) {// the SHELL's `slot` is where the route component hangs: swapped by navigation,// never painted (the compile step already drops `Style`, folded at build)if (!(name == 'slot' && m.isShell)) { keep.push(name) }}if (valueMember != null && rows[valueMember] == null && el.value != null) {bindValue(&m, valueMember, el, isSelect)}if (keep.length > 0) { m.sites.push({ sid = nextSid(); el = el; site = site; names = keep; region = false; rows = rows; }) }return null}applySelectAt(&m, el, v) {el.value = v == null ? '' : '' + vreturn null}// A HANDLER, WIRED AT CREATION. The literal is the element's own — its named entries// are the generated `__hlLit`, the `for` rows in scope ride on it, and the owner is the// instance BY REFERENCE (a handler writing a copy would repaint nothing).onAt(&m, el, event, site, rows, wrote, touched) {if (el.__hlLits == null) { el.__hlLits = {} }el.__hlLits[event] = literalFor(&m, el)let at = m.chainlet root = m.rootKindlet rowNames = rows == null ? [] : rows.keys()el.addEventListener(event, (ev) => {// THE DEFAULT ACTION OF `submit` IS A PAGE NAVIGATION (COMPONENTS.md §6b), so the// framework suppresses it itself — a handler declared with no parameter has no// `ev` to call `preventDefault()` on, and one declared with it may call it again,// harmlessly. This is the FIRST statement, unawaited, for the same reason `onClick`// claims a link before anything else: called after an `await`, it lands a// microtask late, once the browser already navigated (mission 132 / ticket submit).if (event == 'submit') { ev.preventDefault() }let lit = el.__hlLits[event]for (rk of rowNames) { lit[rk] = el.__hlRows[rk] }fire(&lit, event, ev)el.__hlLits[event] = litpatch(at, root, wrote, touched)})return null}// THE SHELL'S SLOT: where the route component hangs. On the build walk the re-created// element adopts the page's nodes, and a page the shell never rendered is built here.slotShell(&m, host, i, cr) {slotEl = hostif (!cr) {// claiming: the page's elements stand here, and the shell's own elements after// the slot come after themslotAt = iif (seed.page == null) { return i }// the page's region: its marks, and the elements between them the shell steps overslotStart = markAt(host, false, true)slotStart.__hlEnd = closeOf(slotStart)host.__hlCur = slotStart.__hlEndlet span = 0for (n of between(slotStart)) { if (n.nodeType == 1) { span = span + 1 } }return i + span}slotAt = host.children.lengthlet start = markAt(host, true, true)if (detachedSlot != null && detachedSlot != host) {// the page's own nodes: what stood between its marks, or all of the old host'slet moving = standing(detachedSlot, slotStart) ? between(slotStart) : Array.from(detachedSlot.childNodes)for (node of moving) { host.appendChild(node) }}detachedSlot = nullif (!pageShown) {// THE PAGE ARRIVES WITH ITS SLOT: it was never constructed while the shell// placed none, so it is asked for now — the route's state, from the serverif (page == null) { page = arrive() }if (page != null) {page.sites = []create(&page, host)headSites(&page)wearHead(&page)pageShown = true}}start.__hlEnd = markAt(host, true, false)slotStart = startreturn i}// THE ROUTE'S PAGE, for a slot that just opened: the navigation face answers it for// this path, told the slot stands here, and the mount is constructed from that. The// frames it raises wait until the rebuild that placed it is done (pageArrived).arrive() {let next = emit server page(location.pathname + location.search, true)if (next == null || next.page == null) { return null }takeBlueprints(next.blueprints)seed.page = next.pagemounting = truepageArrived = truereturn instantiate(next.page, [], 'page')}// THE SLOT CLOSED: the page is dropped, not kept aside — the next time the slot opens// it is constructed again. The head falls back to the app's own.dropPage() {slotEl = nullpageShown = falsepage = nullslotStart = nullseed.page = nulllet none = { instance = {}; }wearHead(&none)announceMounts()return null}// a rebuild that constructed the page: what waited for it goes out, and the server// learns the tab mounts itarrived() {if (!pageArrived) { return null }pageArrived = falserelease()announceMounts()return null}// A CHILD'S SLOT: the fragment its HOST wrote, built in the host's frame. The fill names// that frame by address, and `run` re-enters it when this walk is not it.slotFill(&m, &o, host, i, fill, cr) {if (fill == null) { return i }if (cr && fill.detached) {let anchor = document.createComment('hl:slot')host.appendChild(anchor)pendingFills[] = { key = fill.key; site = fill.site; rows = fill.rows; rowKey = fill.rowKey; outer = fill.outer; chain = fill.chain; rootKind = fill.rootKind; anchor = anchor; }return i}if (ownsFill(o, fill)) {let f = walkOf(fill.key, 'f', fill.site)return run(f, &o, &o, host, i, fill.rows, fill.rowKey, fill.outer, cr, null)}return fillFromRoot(fill, host, i, cr)}// the fill's owner, re-entered from the page or the shell by the address it carriesfillFromRoot(fill, host, at, cr) {if (fill.rootKind == 'shell') {if (shell == null) { return at }return fillInto(&shell, fill, 0, host, at, cr)}if (page == null) { return at }return fillInto(&page, fill, 0, host, at, cr)}fillInto(&m, fill, depth, host, at, cr) {if (depth >= fill.chain.length) {let f = walkOf(fill.key, 'f', fill.site)return run(f, &m, &m, host, at, fill.rows, fill.rowKey, fill.outer, cr, null)}let kid = m.kids[fill.chain[depth]]if (kid == null) { return at }let out = fillInto(&kid, fill, depth + 1, host, at, cr)m.kids[fill.chain[depth]] = kidreturn out}// the fill context a reference hands its child: the fragment's owner by address, the// host's rows and rowKey, and the fill the host itself was standing infillFor(&m, spec, rows, rowKey, fill) {if (!spec.fill) { return null }return { key = m.key; site = spec.site; rows = rows; rowKey = rowKey; outer = fill; chain = m.chain; rootKind = m.rootKind; detached = false; }}// ---- THE MARKS: every region stands between two comments ---------------------------// THE INVARIANT (tickets #103, #104). Every region — an `if`, a `for`, each row of a// `for`, the shell's page — stands on its host between an OPEN mark (a comment `[`)// and a CLOSE mark (`]`), and ITS NODES ARE EXACTLY THE NODES BETWEEN THEM. A region// inside another one on the same host stands between the outer one's marks. So:// - a region's nodes are never remembered, they are read off the DOM between its// marks when they are needed — whatever a region inside it did since, they are// right (a list taken when the branch was built went stale, #103);// - a region is replaced by removing what stands between its marks and putting the// new nodes before its close mark; a row moves and leaves WITH its marks, so a row// is always whole and in its list;// - nothing outside a region is touched by it, and nothing inside it outlives it: the// sites registered in the nodes it drops are dropped with them, and a region whose// marks have left its host has nothing to paint.// The server writes the same marks (compile.hl `markOpen`), the claim walk takes them in// document order and the build walk makes them, so a region looks the same however it// came to stand — and text beside a region never runs into the region's own text.mark(open) { return document.createComment(open ? '[' : ']') }isMark(n) { return n != null && n.nodeType == 8 && (n.data == '[' || n.data == ']') }// the nodes strictly between a region's marksbetween(start) {let out = []let end = start.__hlEndlet n = start.nextSiblingwhile (n != null && n != end) { out.push(n) n = n.nextSibling }return out}// …and with the marks themselves: a row as it moves or leavesspanOf(start) {let out = [start]for (n of between(start)) { out.push(n) }out.push(start.__hlEnd)return out}// a region stands while both its marks are on its hoststanding(host, start) {return start != null && start.parentNode == host && start.__hlEnd != null && start.__hlEnd.parentNode == host}// A REGION'S MARK, made (building) or taken (claiming). The claim takes the next mark// after the host's cursor: the walk meets the regions in document order, so it is this// region's. A page whose marks do not match its tree is said so, and gets a mark where// the walk stands.markAt(host, cr, open) {if (cr) {let made = mark(open)host.appendChild(made)return made}let n = host.__hlCur == null ? host.firstChild : host.__hlCur.nextSiblingwhile (n != null && !isMark(n)) { n = n.nextSibling }let mk = nullif (n != null && n.data == (open ? '[' : ']')) { mk = n }else {console.error('claim: no region mark under', host.tagName)mk = mark(open)host.insertBefore(mk, host.__hlCur == null ? host.firstChild : host.__hlCur.nextSibling)}host.__hlCur = mkreturn mk}// the close mark that pairs with an open one, past the regions inside itcloseOf(start) {let depth = 0let n = start.nextSiblingwhile (n != null) {if (isMark(n)) {if (n.data == '[') { depth = depth + 1 }else if (depth == 0) { return n }else { depth = depth - 1 }}n = n.nextSibling}let made = mark(false)start.parentNode.appendChild(made)return made}// THE SITES IN NODES THAT LEAVE go with them: an element's, a text leaf's, and a// region's whose open mark is among them. Marked and walked up once per site.//// NO CALL BETWEEN READING THE LIST AND WRITING IT BACK. Every call in this language is// awaited, and an await lets another repaint run — one that registers sites of its// own, which a list read before it and written after it would lose. The loops that// read `m.sites` and write it back hold property reads, index writes and `[] =` only.dropSitesIn(&m, host, nodes) {if (nodes.length == 0) { return null }for (n of nodes) { n.__hlGone = true }let kept = []let dropped = 0for (site of m.sites) {let n = (site.region && site.el == host && site.start != null) ? site.start : site.ellet gone = falsewhile (n != null && n != host && !gone) {if (n.__hlGone == true) { gone = true }n = n.parentNode}if (gone) { dropped = dropped + 1 } else { kept[] = site }}if (dropped > 0) {m.sites = keptdropGen = dropGen + 1}for (n of nodes) { n.__hlGone = null }return null}// THE SITES A WALK REGISTERED ON THE FRAGMENT IT BUILT INTO belong to the host it lands// in — this mount's, and those of the children composed straight onto the fragment,// whose own regions (a child's View may begin with an `if`) stand on the same host.// Those children are named by address on the fragment as the walk composes them// (kidAt, `__hlOnFrag`), so only they are re-entered.rehome(&m, src, dst, before) {sitesOnto(&m, src, dst, before)let due = src.__hlOnFrag == null ? [] : src.__hlOnFragsrc.__hlOnFrag = nulllet base = m.chain == null ? 0 : m.chain.lengthfor (chain of due) {if (chain.length > base) { rehomeAt(&m, chain, base, src, dst) }}return null}// (`after`: the sites numbered after it are the walk's — an index would move when// another repaint drops sites in the meantime)sitesOnto(&m, src, dst, after) {let k = 0while (k < m.sites.length) {let e = m.sites[k]if (e.el == src && e.sid > after) {e.el = dstm.sites[k] = e}k = k + 1}return null}rehomeAt(&m, chain, depth, src, dst) {if (depth >= chain.length) { sitesOnto(&m, src, dst, 0) return null }let kid = m.kids[chain[depth]]if (kid == null) { return null }rehomeAt(&kid, chain, depth + 1, src, dst)m.kids[chain[depth]] = kidreturn null}// A RANGE LEAVES THE PAGE — a branch, a row: every site registered in it goes (at any// depth: an element's, a text's, a region's whose marks are in it), and so does every// child composed in it (its nodes carry its address), with its own sites, its children// and its handlers.// A range can hold a FILL — a host's fragment placed at this component's `slot` — whose// sites and children are the host's: `fill` names that host (and the one it was filled// by in turn), and those go in settle(), where a host is re-entered by its address after// every mount on this walk has been written back.dropRange(&m, host, nodes, fill) {dropSitesIn(&m, host, nodes)let others = disposeIn(&m, nodes)let f = fillwhile (f != null) {pendingDrops.push({ chain = f.chain; rootKind = f.rootKind; host = host; nodes = nodes; kids = others; })others = []f = f.outer}return null}// THE CHILDREN COMPOSED IN NODES go: those of this mount at once, and the others (a// host's, composed in its fill) are answered for the caller to hand ondisposeIn(&m, nodes) {let chains = []for (n of nodes) { kidsFiledIn(n, &chains) }let base = m.chain == null ? [] : m.chainlet others = []for (c of chains) {// (a range inside a child never takes the child itself, nor a mount above it: the// child's own nodes carry its address, a region of its own among them)let above = c.length <= base.length && JSON.stringify(base.slice(0, c.length)) == JSON.stringify(c)if (!above) {if (c.length > base.length && JSON.stringify(c.slice(0, base.length)) == JSON.stringify(base)) { disposeAt(&m, c, base.length) }else { others.push(c) }}}return others}// the ranges a fill's host still has to let go of (see dropRange)dropPending() {if (pendingDrops.length == 0) { return null }let due = pendingDrops.slice(0)pendingDrops = []for (d of due) {if (d.rootKind == 'shell') {if (shell != null) { dropAt(&shell, d, 0) }} else if (page != null) { dropAt(&page, d, 0) }}return null}dropAt(&m, d, depth) {if (depth >= d.chain.length) {dropSitesIn(&m, d.host, d.nodes)for (c of d.kids) { if (c.length > depth) { disposeAt(&m, c, depth) } }return null}let kid = m.kids[d.chain[depth]]if (kid == null) { return null }dropAt(&kid, d, depth + 1)m.kids[d.chain[depth]] = kidreturn null}// a node in a range whose host's sites are still to go (dropPending)dropDue(n) {let top = n.getRootNode()for (d of pendingDrops) { if (d.nodes.includes(top)) { return true } }return false}// the children whose nodes are this node or stand under it (kidAt files them there)kidsFiledIn(n, &out) {if (n.__hlKidChains != null) { for (c of n.__hlKidChains) { out.push(c) } }if (n.nodeType != 1) { return null }let w = document.createTreeWalker(n, 4294967295)let c = w.nextNode()while (c != null) {if (c.__hlKidChains != null) { for (k of c.__hlKidChains) { out.push(k) } }c = w.nextNode()}return null}disposeAt(&m, chain, depth) {if (depth == chain.length - 1) {if (m.kids[chain[depth]] != null) { delete m.kids[chain[depth]] }return null}let kid = m.kids[chain[depth]]if (kid == null) { return null }disposeAt(&kid, chain, depth + 1)m.kids[chain[depth]] = kidreturn null}// a node no longer on the page: not in the document, not in a fragment a walk is// still building, not in the slot a shell's rebuild holds asideplacedOff(n) {if (n.isConnected) { return false }if (n.getRootNode().nodeType == 11) { return false }if (detachedSlot != null && detachedSlot.contains(n)) { return false }return true}nextSid() {sidNext = sidNext + 1return sidNext}isLive(&m, s) {for (e of m.sites) { if (e.sid == s.sid) { return true } }return false}liveSids(&m) {let out = {}for (e of m.sites) { out[e.sid] = true }return out}// A SITE WHOSE NODES LEFT THE PAGE, found by a repaint. Every range that leaves takes// its sites with it (dropRange), so this is a bug: the gates' client raises it as an// uncaught exception (the page's error event, what every gate fails on), a page in the// field says so on the console; neither paints it.staleSite(s) {let msg = 'hl:web: a repaint found a site whose nodes have left the page (site ' + s.site + ')'if (strict) { window.reportError(window.Reflect.construct(window.Error, [msg])) }else { console.error(msg) }return null}// A RUN OF TEXT LEAVES BESIDE ELEMENTS OR REGIONS (compile.hl `mixed`): one text node,// kept on the element under the run's index so a paint rewrites it and nothing else.// `k` null is a text the walk only has to step over (literal, or painted as a site of// its own by `textAt`). Built, it is appended. Claimed, it is the text node right// after the host's cursor — the last node the walk took there — which the server may// have run together with the readable form's line break after it, so it is split off// by the value it has now. A run that rendered empty has no node and gets one.leafAt(host, cr, i, k, text) {let t = leafNode(host, cr, text)if (k != null) {if (host.__hlLeaves == null) { host.__hlLeaves = {} }host.__hlLeaves[k] = t}return null}leafNode(host, cr, text) {let t = nullif (cr) {t = document.createTextNode(text)host.appendChild(t)return t}let at = host.__hlCur == null ? host.firstChild : host.__hlCur.nextSiblingif (at != null && at.nodeType == 3 && text != '') {if (at.data == text) { t = at }else if (at.data.startsWith(text)) { at.splitText(text.length) t = at }else {// a line break of the readable form in front of it (a filled slot's line)let lead = at.data.length - at.data.trimStart().lengthif (lead > 0 && at.data.startsWith(View.newline) && at.data.slice(lead).startsWith(text)) {t = at.splitText(lead)if (t.data != text) { t.splitText(text.length) }}}}if (t == null) {t = document.createTextNode(text)host.insertBefore(t, at)}host.__hlCur = treturn t}// A TEXT LEAF STANDING IN A LIST (an `if` branch, a `for` body): its own text node, and// a site of the member it reads — or of the row it stands in — painted by rewriting it.textAt(&m, host, cr, readFn, names, rows, rk) {let inst = m.instancelet rws = rowslet t = leafNode(host, cr, readFn(&inst, &rws))if (names.length > 0 || (rk != null && rk != '')) {m.sites.push({ sid = nextSid(); el = t; site = null; names = names; region = false; text = true; rows = rows; rowKey = rk; read = readFn; })}return null}paintText(&m, s) {let inst = m.instancelet rws = s.rowslet f = s.readlet v = f(&inst, &rws)let t = s.elif (t.data != v) { t.data = v }return null}// ---- a `for`: the region, its rows, each between its marks -------------------------forAt(&m, &o, host, i, site, rowName, listFn, rows, rk, fill, cr, names) {let start = markAt(host, cr, true)regionAt(&m, host, site, rows, rk, fill, true, rowName, listFn, names, start)let box = { keys = []; rows = {}; }let inst = m.instancelet rws = rowslet entries = listFn(&inst, &rws)let body = walkOf(m.key, 'b', site)if (entries != null) {let keys = listKeys(entries)let ri = 0for (entry of entries) {let inner = rows + {}inner[rowName] = entrylet rs = markAt(host, cr, true)let out = run(body, &m, &o, host, cr ? 0 : i, inner, rk + '#' + keys[ri], fill, cr, null)if (!cr) { i = out }rs.__hlEnd = markAt(host, cr, false)box.keys.push(keys[ri])box.rows[keys[ri]] = { start = rs; entry = entry; }ri = ri + 1}}start.__hlEnd = markAt(host, cr, false)start.__hlBox = boxreturn i}// an `if`: the branch that stands, between its marksifAt(&m, &o, host, i, site, condFn, rows, rk, fill, cr, names) {let start = markAt(host, cr, true)regionAt(&m, host, site, rows, rk, fill, false, null, condFn, names, start)let inst = m.instancelet rws = rowslet branch = condFn(&inst, &rws) ? walkOf(m.key, 't', site) : walkOf(m.key, 'e', site)let out = run(branch, &m, &o, host, i, rows, rk, fill, cr, null)start.__hlEnd = markAt(host, cr, false)return out}// A REGION SITE carries what the patch needs and nothing else: the names its list or// its condition reads (the compiler's answer, by this site), the read itself as the// generated closure, and its open mark — the region itself, one per row it stands in.regionAt(&m, host, site, rows, rowKey, fill, isFor, rowName, readFn, names, start) {// a row-scoped read is its row's, and the language resolved it as one, so the// compile step names none here: an empty set is a region no member can move. INSIDE// A ROW it is still registered, with no names: the row's new record moves it// (reseatRows, ticket #86). Outside one nothing ever can.if (names == null) { names = [] }if (names.length == 0 && (rowKey == null || rowKey == '')) { return null }let held = nullif (fill != null) { held = fill + {} held.detached = true }m.sites.push({ sid = nextSid(); el = host; site = site; names = names; region = true; rows = rows; rowKey = rowKey; fill = held; isFor = isFor; row = rowName; read = readFn; start = start; })return null}// ---- a composed child --------------------------------------------------------------// The reference's own description is written at its use site by the compile step: what// it binds, what it overwrites, the classes it puts on the child's roots.kidAt(&m, host, i, spec, rows, rk, fill, cr, opt) {let kk = spec.kid + rklet kid = m.kids[kk]if (kid == null) { kid = mintFrom(&m, spec, rows, childChain(m.chain, kk)) }if (kid == null) { return i }bindFrom(&kid, &m, spec, rows)if (cr) { kid.sites = [] }kid.chain = childChain(m.chain, kk)kid.rootKind = m.rootKindlet firstAt = cr ? host.children.length : i// WHAT A REFERENCE OVERWRITES REACHES THE FIRST ELEMENT, however deep. A child// whose View is a bare reference to another component renders no element of its// own, so the events the host overwrote travel on through it — the old hl:web kept that in// a field that the first element consumes; here it rides on the option the walk is// handed, and a reference with ons of its own replaces it.let over = []if (spec.ons != null && spec.ons.length > 0) { for (o of spec.ons) { over.push(o.event) } }else if (opt != null && opt.over != null) { over = opt.over }let kidOpt = { over = over; classes = spec.classes; }let f = walkOf(kid.key, 'v', null)// a child composed straight onto a fragment has its regions on the host the fragment// lands in: the fragment names it, for the re-homing (rehome)if (cr && host.nodeType == 11) {let onFrag = host.__hlOnFrag == null ? [] : host.__hlOnFragonFrag.push(kid.chain)host.__hlOnFrag = onFrag}let nodesBefore = host.childNodes.lengthlet curBefore = host.__hlCurlet out = run(f, &kid, &m, host, cr ? 0 : i, {}, '', fillFor(&m, spec, rows, rk, fill), cr, kidOpt)if (!cr) { i = out }// THE NODES THE CHILD PUT ON ITS HOST CARRY ITS ADDRESS: when a range holding them// leaves the page, the child goes with it (dropRange)let made = []if (cr) {let k = nodesBeforewhile (k < host.childNodes.length) { made.push(host.childNodes[k]) k = k + 1 }} else {let n = curBefore == null ? host.firstChild : curBefore.nextSiblinglet stop = host.__hlCur == null ? null : host.__hlCur.nextSiblingwhile (n != null && n != stop) { made.push(n) n = n.nextSibling }}for (n of made) {let filed = n.__hlKidChains == null ? [] : n.__hlKidChainsfiled.push(kid.chain)n.__hlKidChains = filed}if (cr || kid.bindings.length == 0) { kid.bindings = [] bindKids(&kid, kid.view) }let roots = rootsBetween(host, firstAt, cr ? host.children.length : i)refOnsFrom(&m, spec, roots, rows)for (el of roots) { el.__hlKid = true }m.kids[kk] = kidreturn i}// the value a binding of this reference carries, off the host instancespecValue(b, &m, rows) {if (b.text != null) { return b.value }if (b.member != null) { return view.value({ k = 'member'; name = b.member; }, m.instance, rows) }if (b.ref != null) {let inst = m.instancelet rws = rowslet f = b.refreturn f(&inst, &rws)}return null}// A CHILD A REGION BUILDS is born with its address: the host's chain and its own key.// Every patch, and every late patch a function or a waiting handler of it asks for, is// filed and cancelled by that address (tickets #98, #99).mintFrom(&m, spec, rows, chain) {let bp = blueprintOf(spec.key)if (bp == null) { return null }let args = {}for (b of spec.bindings) { args[b.name] = specValue(b, &m, rows) }let inst = hlLoad(bp.module, args)return { key = spec.key; instance = inst; view = bp.view; sites = []; isShell = false; kids = {}; bindings = []; chain = chain; rootKind = m.rootKind; }}bindFrom(&kid, &m, spec, rows) {for (b of spec.bindings) { kid.instance[b.name] = specValue(b, &m, rows) }return null}// A HANDLER WRITTEN ON A REFERENCE is the HOST's: its literal is minted here with the// reference's bindings as its own entries, and its write set is filed under the// reference's own `on` site in the host's table.refOnsFrom(&m, spec, els, rows) {if (spec.ons == null || spec.ons.length == 0) { return null }let named = {}for (b of spec.bindings) { named[b.name] = specValue(b, &m, rows) }for (k of rows.keys()) { named[k] = rows[k] }let inst = m.instancelet lit = hlLiteralNew(spec.site, named, &inst)for (el of els) {for (o of spec.ons) { listenRef(&m, &lit, el, o) }}return null}// ---- claim: walk the tree in lockstep with the DOM the same tree produced --------// Round one granularity: an element whose children include a member is a SITE for// each of those members; a repaint rewrites that element's children from the tree.// `i` is the running index into `host.children`; a `for` consumes one run of// elements per entry, an `if` the run of the branch that stands.claim(&m, host, Number at = 0) {let f = walkOf(m.key, 'v', null)return run(f, &m, &m, host, at, {}, '', null, false, null)}// a kid's address: its host's, plus the key the host holds it underchildChain(chain, kk) {let out = chain == null ? [] : chain.slice(0)out.push(kk)return out}// the keys a list has, in order: the record's id (view.rowKey), an index where the row// is no record, and a suffix where one id stands twice. THE SERVER DOES NOT DEDUPE —// its `mountKids` gives two rows of one id one mount key; a list with repeated ids is// written down here rather than answered twice.listKeys(entries) {let out = []let seen = {}let ri = 0for (entry of entries) {let k = view.rowKey(entry, ri)if (seen[k] != null) { k = k + ':' + ri }seen[k] = trueout.push(k)ri = ri + 1}return out}// ---- the patch: old keys against new ---------------------------------------------// A write to the list is answered by a DIFF and nothing else: the rows that are gone// are removed, the rows that arrived are built, the rows that stayed are MOVED where// the order changed and are left alone where it did not — and each of them is handed// the record it now stands for, which rewrites only the attributes and the text the// DOM does not already hold. The region's element is never emptied. Each row is the// span between its own marks, and every row stands between the list's marks.paintFor(&m, s) {let host = s.ellet start = s.startif (!standing(host, start)) {// dropped while this paint was on its way (a paint that was running when the// range holding it left, and comes back for another round): it is gone, and// nothing is painted — a site still REGISTERED here is the bugif (!isLive(&m, s)) { return null }if (start != null && dropDue(start)) { return null }return staleSite(s)}let box = start.__hlBoxif (box == null) { return null }// ONE PAINT OF A LIST AT A TIME. Building a row can wait (a composed child's module// loads on first use), and a second write to the list in that time — a push right// behind the handler's own write — read the box before the first paint had filed// its row, and built the row again. A paint that arrives while one runs is// remembered, and the running one paints again from the list as it then stands.if (start.__hlBusy != null) { start.__hlBusy = 'again' return null }start.__hlBusy = 'busy'let inst = m.instancelet rws = s.rowslet read = s.readlet entries = read(&inst, &rws)if (entries == null) { entries = [] }// the keys this list has now, in orderlet keys = listKeys(entries)let next = []let ri = 0for (entry of entries) {next.push({ key = keys[ri]; entry = entry; })ri = ri + 1}// 1. THE ROWS THAT ARE GONE: their nodes, their sites and their children's// mounts leave together.let keep = {}for (r of next) { keep[r.key] = true }for (k of box.keys) {if (keep[k] == null) { dropRow(&m, s, &box, k) }}// 2. THE ROWS IN ORDER. `at` is the node standing where the next row belongs: a// row already there advances it, a row that is not is moved or built before it.let at = start.__hlEndlet found = falsefor (k of box.keys) {if (!found && keep[k] != null && box.rows[k] != null) { at = box.rows[k].start found = true }}let placed = []let changed = []for (r of next) {let held = box.rows[r.key]if (held == null) {buildRow(&m, s, &box, r, at)} else if (held.start == at) {// already in place: step over itat = held.start.__hlEnd.nextSiblingif (updateRow(&m, s, &box, r)) { changed.push(box.rows[r.key]) }} else {moveRow(host, held, at)if (updateRow(&m, s, &box, r)) { changed.push(box.rows[r.key]) }}placed.push(r.key)}box.keys = placedstart.__hlBox = boxif (changed.length > 0) { reseatRows(&m, s, changed) }let again = start.__hlBusy == 'again'start.__hlBusy = nullif (again) { paintFor(&m, s) }return null}// a row's own elements: the ones between its marksrowEls(held) {let out = []for (n of between(held.start)) { if (n.nodeType == 1) { out.push(n) } }return out}// A ROW LEAVES: its span goes off the document, the sites that were registered inside// it are dropped (they point at nodes nobody can see), and so do the mounts of the// children it held — a child of a row is keyed by that row (view.rowKey).dropRow(&m, s, &box, key) {let held = box.rows[key]if (held == null) { return null }let host = s.ellet nodes = spanOf(held.start)dropRange(&m, host, nodes, s.fill)for (n of nodes) { n.remove() }delete box.rows[key]return null}// A ROW MOVES, marks and all: `moveBefore` where the browser has it, because it keeps// an element's state (focus, a playing video, an open dialog) across the move;// `insertBefore` else.moveRow(host, held, at) {for (n of spanOf(held.start)) {if (host.moveBefore != null) { host.moveBefore(n, at) }else { host.insertBefore(n, at) }}return null}// A ROW ARRIVES: built into a fragment between its marks and put in with one insertion,// because the walk appends and the row's place is where `at` stands — which may be in// the middle of the region. What the walk registered on the fragment belongs to the host.buildRow(&m, s, &box, r, at) {let host = s.ellet inner = s.rows + {}inner[s.row] = r.entrylet bin = document.createDocumentFragment()let body = walkOf(m.key, 'b', s.site)let before = sidNextlet rs = markAt(bin, true, true)run(body, &m, &m, bin, 0, inner, (s.rowKey == null ? '' : s.rowKey) + '#' + r.key, s.fill, true, null)rs.__hlEnd = markAt(bin, true, false)rehome(&m, bin, host, before)// THE LIST LEFT WHILE THE ROW WAS BEING BUILT (a module to load is a wait, and a// branch around the list can be replaced in it): the row never stands, and what// its walk registered goes with itif (!standing(host, s.start)) {dropRange(&m, host, Array.from(bin.childNodes), s.fill)return null}host.insertBefore(bin, at)box.rows[r.key] = { start = rs; entry = r.entry; }return null}// A ROW STAYS, AND THE RECORD IT SHOWS MAY BE ANOTHER ONE — `upsert` hands the row a// new record under the same id, and `items[0].title = 'edited'` writes into the one it// already has. Both are answered here: the row's elements are handed the record (their// `__hlRows`, which is also what a handler on the row reads), and every attribute and// text leaf is rewritten ONLY where the DOM does not already hold the value. That is// why a push into 2000 rows costs no mutation on the 2000 that did not change.updateRow(&m, s, &box, r) {let held = box.rows[r.key]if (held == null) { return false }// the row holds its own copy of the record, so the two are compared by what they saylet changed = JSON.stringify(held.entry) != JSON.stringify(r.entry)held.entry = r.entrybox.rows[r.key] = heldfor (el of rowEls(held)) { refreshRow(&m, el, s.row, r.entry) }rebindRow(&m, s, r)return changed}// A ROW THAT NOW SHOWS ANOTHER RECORD HANDS IT TO THE SITES INSIDE IT (ticket #86). A// row without an id is keyed by its place, so a new list of the same shape keeps every// row — and refreshRow repaints only the row's own elements. The sites registered in// the row still hold the rows of the moment they were built: a nested `for` read the// old record's list, and a `td` inside it the old inner record. Each site whose element// stands in a changed row takes the new record, and the regions among them are patched// again, outermost first — a nested row that changed in turn does the same for its own.// One pass over the sites, marked rows and an ancestor walk, not a scan per row.reseatRows(&m, s, changed) {let host = s.el// every node of a changed row carries its record for the walk up — its text and// its regions' marks as well as its elements, so a text leaf or an `if` standing// in the row directly on the host takes the record toofor (held of changed) { for (n of between(held.start)) { n.__hlReseat = held.entry } }// (one pass that reads and writes the list back, with no call in it — see// dropSitesIn — and the paints after it)let hits = []let k = 0while (k < m.sites.length) {let site = m.sites[k]if (site.el != null && site.rows != null && !(site.region && site.start == s.start)) {let n = (site.region && site.el == host && site.start != null) ? site.start : site.ellet hit = nullwhile (n != null && n != host && hit == null) {if (n.__hlReseat != null) { hit = n }n = n.parentNode}if (hit != null) {let rows = site.rows + {}rows[s.row] = hit.__hlReseatsite.rows = rowsm.sites[k] = sitehits[] = site}}k = k + 1}let regions = []let texts = []for (site of hits) {if (site.region) { regions.push(site.start) }if (site.text == true) { texts.push(site) }}for (held of changed) { for (n of between(held.start)) { n.__hlReseat = null } }for (t of texts) { paintText(&m, t) }for (r of regions) {// read again from the mount: a region an outer one already patched away is gonelet site = nullfor (e of m.sites) { if (e.region && e.start == r) { site = e } }if (site != null) {if (site.isFor) { paintFor(&m, site) } else { rebuildIf(&m, site) }}}return null}// A ROW'S CHILDREN TAKE THEIR BINDINGS AGAIN. A reference inside a `for` body is not// in `m.bindings` — that list is built from the View's own walk, which does not enter a// `for` (the row is the scope, and there is one child per row) — so a host member that// feeds a row's child used to reach it only because the region REBUILT and the child// was minted again with the value of the moment (the framework gate's own case:// home.hl writes `noteOpen`, every row's RowNote takes it and hands it to RowMark, and// the grandchild's region appears). Nothing rebuilds any more, so the edge is walked// here — and only what MOVED is written and repainted, or every list write would// repaint every row's child.rebindRow(&m, s, r) {let rk = (s.rowKey == null ? '' : s.rowKey) + '#' + r.keylet inner = s.rows + {}inner[s.row] = r.entryrebindNodes(&m, rowBody(m.key, s.site), inner, rk)return null}// THE ROW'S OWN NODES, for the binding edges a row's child takes again. The tree is// still what the seed carries (it is the mount's `view`), and this is a read of it, not// a walk of the DOM: the compiled row factory builds, this re-binds.rowBody(key, site) {let bp = blueprints[key]if (bp == null) { return [] }return bodyIn(bp.view, site)}bodyIn(nodes, site) {for (n of nodes) {if (n.k == 'for' && n.site == site) { return n.body }if (n.k == 'el') { let got = bodyIn(n.children, site) if (got.length > 0) { return got } }if (n.k == 'if') {let a = bodyIn(n.then, site)if (a.length > 0) { return a }let b = bodyIn(n.other, site)if (b.length > 0) { return b }}if (n.k == 'for') { let got = bodyIn(n.body, site) if (got.length > 0) { return got } }if (n.k == 'component' && n.fill != null) { let got = bodyIn(n.fill, site) if (got.length > 0) { return got } }}return []}rebindNodes(&m, nodes, rows, rk) {for (n of nodes) {if (n.k == 'component') {let kk = view.kidKey(n.path) + rklet kid = m.kids[kk]if (kid != null) {let moved = []for (b of n.bindings) {let v = nullif (b.text != null) { v = view.refValue(b) }else if (b.member != null) { v = view.value({ k = 'member'; name = b.member; }, m.instance, rows) }else if (b.ref != null) { v = view.value(b.ref, m.instance, rows) }if (kid.instance[b.name] != v) { kid.instance[b.name] = v moved.push(b.name) }}if (moved.length > 0) { repaintAll(&kid, moved) }m.kids[kk] = kid}if (n.fill != null) { rebindNodes(&m, n.fill, rows, rk) }} else if (n.k == 'el') {rebindNodes(&m, n.children, rows, rk)} else if (n.k == 'if') {rebindNodes(&m, n.then, rows, rk)rebindNodes(&m, n.other, rows, rk)}}return null}refreshRow(&m, el, name, entry) {// A COMPOSED CHILD'S DOM IS ITS OWN MOUNT'S: its elements read the CHILD's// instance, and painting them from this one would show the wrong values. The walk// stops at the roots a reference claimed (`__hlKid`, set where they are claimed// and built) — and at any element another component's walk built (`__hlKey`): a// root the child's own region built after the reference was claimed is its too.// The walk goes on INTO it: a fill this host wrote stands inside the child's// elements, and that is this host's row again.if (el.__hlKid == true || (el.__hlKey != null && el.__hlKey != m.key)) {let inside = Array.from(el.children)for (k of inside) { refreshRow(&m, k, name, entry) }return null}if (el.__hlRows != null) {let next = el.__hlRows + {}next[name] = entryel.__hlRows = next// (The literal a handler on this element fires takes its row entries from// `__hlRows` when it fires — see `bind` — so there is nothing to update here.// A handler written on a REFERENCE (bindRefOns) mints one literal for the// child's roots and does not: its bindings are re-applied by rebindRow, its// own entries are not.)}rowPaint(&m, el)let kids = Array.from(el.children)for (k of kids) { refreshRow(&m, k, name, entry) }return null}// ONE ELEMENT OF A ROW, REDRAWN WHERE IT IS WRONG. The guard is the DOM's own value,// not a remembered one: nothing is serialised and nothing is compared to a digest.rowPaint(&m, el) {let site = el.__hlSiteif (site == null) { return null }let comp = compiledOf(el.__hlKey == null ? m.key : el.__hlKey)if (comp == null) { return null }let entry = comp.p[site]if (entry == null) { return null }let f = entry.rif (f == null) { return null }let inst = m.instancelet rws = el.__hlRowslet e = elf(&e, &inst, &rws)return null}bindValue(&m, name, el, isSelect) {let held = m.instance[name] == null ? '' : '' + m.instance[name]// A SELECT IS PAINTED BY THE MEMBER FIRST: what it shows is one of its options,// and the member decides which. The server already marked it (view.hl `selected`),// so this is a no-op there; on a select the client built it is the paint. Only// then is the read-back right — before it, an unpainted select reports its FIRST// option and the read-back would write that back into the member.if (isSelect && held != '') { el.value = held }if (el.value != held) { m.instance[name] = el.value liftValue(m.chain, m.rootKind, name, el.value) }el.addEventListener('input', (ev) => {m.instance[name] = el.value// AND UP THROUGH THE REFERENCE THAT BOUND IT, if this control stands inside a// composed child: the host's member is what the app reads (see liftValue)liftValue(m.chain, m.rootKind, name, el.value)})return null}// ---- `value` IS TWO-WAY THROUGH A COMPOSITION TOO --------------------------------// `value = member` on a control is the framework's two-way name: the member paints the// field and the field writes the member (mission 132). A composed child is a mount of// its own, so that write landed on the CHILD's member and stopped there — `Field// { value = who }` left the host's `who` empty while the DOM held what was typed, and// routger's login submit read null (creator, W12). The host→child binding is an edge// the mount already carries; this is the SAME edge run backwards on input, and only// for the name `value`: every other binding name stays one-way.//// The child cannot name its host (a mount record is a value), so it names it by the// ADDRESS it already carries — the kid keys from the page or the shell — and the walk// below re-enters the host by reference. It carries on upward as long as the reference// it came through was itself a `value` binding, which is what makes a component whose// View is a bare reference to a field reach the page's member through both hops.liftValue(chain, root, target, value) {if (target != 'value') { return null }if (chain == null || chain.length == 0) { return null }if (root == 'shell') {if (shell == null) { return null }liftInto(&shell, chain, 0, target, value)} else {if (page == null) { return null }liftInto(&page, chain, 0, target, value)}return null}// walk the address down to the mount that HOLDS the last key — that mount is the host —// and write what its reference bound to the child's `target`, repainting its own sitesliftInto(&m, chain, at, target, value) {if (at >= chain.length - 1) {for (b of m.bindings) {// a field path (`value = row.text`) names no member to write back intoif (b.kid == chain[at] && b.target == target && b.ref == null) {if (m.instance[b.name] != value) {m.instance[b.name] = valuerepaintAll(&m, [b.name])}liftValue(m.chain, m.rootKind, b.name, value)}}return null}let kid = m.kids[chain[at]]if (kid == null) { return null }liftInto(&kid, chain, at + 1, target, value)m.kids[chain[at]] = kidreturn null}// THE ELEMENT'S LITERAL: a value of the class the module minted for the literal's// SITE (`file:line:col`, the id the JavaScript target registers at load), with the// element's own named entries — its attributes as the tree has them — and the// `for` row variables in scope as its own entries, the way the language builds a// row's literal (the row rides on the value). Built HERE and not looked up in the// instance's View: a row created after the list changed has no value there, and// a name-path cannot address ordered content. The handlers read own entries first,// then the owner's members — the owner is the component instance.literalFor(&m, el) {let rows = el.__hlRowslet inst = m.instancelet rws = rows// THE ELEMENT'S OWN ENTRIES, compiled: its attributes by name, with the reads// written in (compile.hl `litNamed`)let mk = el.__hlLitlet named = mk == null ? {} : mk(&inst, &rws)for (k of rows.keys()) { named[k] = rows[k] }// the owner by REFERENCE: a call argument is copied, and a handler writing a// copy's member would repaint nothingreturn hlLiteralNew(el.__hlSite, named, &inst)}// WHAT A HANDLER WROTE, REPAINTED — the whole of the answer to a local event.// `&m` is the mount whose instance the handler's owner is, so a write reaches this// mount's sites and, through its bindings, the children that read the member.//// AND UPWARD, THROUGH A ROUTINE THE HOST HANDED DOWN. `Badge { onHide = hideBadge }`// gives the child a FUNCTION of the host's; the child's handler calls it and the// host's member moves. The child's own table cannot say so — the member it called// has no initializer there — but the HOST's does: the binding edge names the host// member (`hideBadge`), and the host's table says what calling it writes. That is// the same table read one mount up, not a new mechanism.//// ONE PATCH AT A TIME, in the order they were asked for. A patch awaits (a composed// child's module loads on first use, and every call in the language is awaited), and a// second one — the next handler, a frame from the server — running into the first// would read a branch the first is in the middle of replacing. A patch asked for while// one runs is queued, and the running one goes on with it; the state a handler wrote// is already on the instance, so the queued patch paints what stands when it runs.patch(chain, root, names, targets) {patchQueue[] = { chain = chain; root = root; names = names; targets = targets; }if (turning) { return null }turning = truewhile (patchQueue.length > 0) {let job = patchQueue[0]patchQueue = patchQueue.slice(1)patchAt(job.chain, job.root, job.names)patchUp(job.chain, job.root, job.targets)settle()}turning = falsereturn null}// ---- A WRITE AFTER ITS LISTENER (tickets #98, #99) --------------------------------// A listener repaints its handler's write set when the handler is done. What runs// LATER is repainted the same way, from the same tables, when it is done:// • a function made inside a component (an XMLHttpRequest's `onload`, a timer, a// Promise's executor, a `then`) — the module writes it as `hlFnSite(…)`, and its// return is announced here with its site; its write set is the file's `fns`// table entry, closed over the methods and handlers it calls (a local `emit`);// • a handler that is still waiting after the task that ran it (a network// answer, a Promise): the runtime says so (`hlRealm.handlerWaiting`), and its// write set is painted then, and again by its listener when it ends.// Both are LATE patches: queued, and run after the current task, unless a patch of// the same mount paints those members first — so a function called and a handler// finished within one task are painted once, by their listener. (`lateJobs` and// `lateDue` are declared at the top: boot() patches before a member declared down// here is initialized.)latePatch(chain, root, names) {if (names == null || names.length == 0) { return null }lateJobs[] = { at = addressOf(chain, root); chain = chain; root = root; names = names.slice(0); }if (!lateDue) {lateDue = truesetTimeout(() => { runLate() }, 0)}return null}runLate() {lateDue = falselet jobs = lateJobslateJobs = []for (j of jobs) {if (j.names.length > 0) { patch(j.chain, j.root, j.names, []) }}return null}// `names` were painted at this mount: a late job there has nothing left of them to dolateDone(chain, root, names) {if (lateJobs.length == 0 || names == null) { return null }let at = addressOf(chain, root)let i = 0while (i < lateJobs.length) {if (lateJobs[i].at == at) {let left = []for (n of lateJobs[i].names) { if (!names.includes(n)) { left.push(n) } }lateJobs[i].names = left}i = i + 1}return null}addressOf(chain, root) {return root + ':' + (chain == null ? '' : chain.join('/'))}// a handler still waiting after the task that ran it: what it writes is painted now// (and again by its listener when it ends)on hlRealm.handlerWaiting(ownerId, event) {let at = nullif (page != null) { at = mountById(&page, ownerId) }if (at == null && shell != null) { at = mountById(&shell, ownerId) }if (at == null) { return null }latePatch(at.chain, at.root, eventWrites(at.key, event))return null}on hlRealm.fnReturned(ownerId, site) {let at = nullif (page != null) { at = mountById(&page, ownerId) }if (at == null && shell != null) { at = mountById(&shell, ownerId) }if (at == null) { return null }latePatch(at.chain, at.root, fnWrites(at.key, site))return null}mountById(&m, id) {if (m.instance.__hlOwnerId == id) { return { key = m.key; chain = m.chain; root = m.rootKind; } }for (k of m.kids.keys()) {let kid = m.kids[k]let at = mountById(&kid, id)m.kids[k] = kidif (at != null) { return at }}return null}// what the function made at `site` writes: the compile step's `fn` tablefnWrites(key, site) {let comp = compiledOf(key)if (comp == null || comp.fn == null) { return [] }let names = comp.fn[site]return names == null ? [] : names}// A MOUNT RECORD IS A VALUE (the rule this whole file is written around), so a// listener cannot hold the live one — the record it was bound against is a copy the// walk wrote back, and a region rebuild replaces it again. It holds the mount's// ADDRESS instead and the walk re-enters from the root, exactly as `liftInto` and// `fillAt` do. `refresh()` used to get this for free by starting at the root every// time; a write set has to say where it lands.patchAt(chain, root, names) {if (names == null || names.length == 0) { return null }if (root == 'shell') {if (shell == null) { return null }patchDown(&shell, chain, 0, names)} else {if (page == null) { return null }patchDown(&page, chain, 0, names)}return null}patchDown(&m, chain, at, names) {if (chain == null || at >= chain.length) {repaintAll(&m, names)return null}let kid = m.kids[chain[at]]if (kid == null) { return null }patchDown(&kid, chain, at + 1, names)m.kids[chain[at]] = kidreturn null}patchUp(chain, root, targets) {if (targets == null || targets.length == 0) { return null }if (chain == null || chain.length == 0) { return null }if (root == 'shell') {if (shell == null) { return null }patchInto(&shell, chain, 0, targets)} else {if (page == null) { return null }patchInto(&page, chain, 0, targets)}return null}// walk the address down to the mount that HOLDS the last key — that mount is the host// — and repaint what the routines it bound into the child write (and carry on upward,// because the host may have received them from ITS host)patchInto(&m, chain, at, targets) {if (at >= chain.length - 1) {let names = []let up = []for (b of m.bindings) {if (b.kid == chain[at] && b.ref == null && targets.includes(b.target)) {if (!up.includes(b.name)) { up.push(b.name) }for (w of memberWrites(m.key, b.name)) { if (!names.includes(w)) { names.push(w) } }}}repaintAll(&m, names)patchUp(m.chain, m.rootKind, up)return null}let kid = m.kids[chain[at]]if (kid == null) { return null }patchInto(&kid, chain, at + 1, targets)m.kids[chain[at]] = kidreturn null}// one listener, in its own frame so the event name it closes over is this one.// `&m` is the HOST — a handler written on a reference is the host's, and its write// set is filed under the reference's own `on` site in the host's table.listenRef(&m, &lit, el, o) {let wrote = o.sets[0]let touched = o.sets[1]let at = m.chainlet root = m.rootKindel.addEventListener(o.event, (ev) => {// same reason as `onAt`: the framework, not the handler, owns `submit`'s defaultif (o.event == 'submit') { ev.preventDefault() }fire(&lit, o.event, ev)patch(at, root, wrote, touched)})return null}// the elements a composed child rendered directly under `host`, between two marks in// the child list — its roots, whatever its tree put thererootsBetween(host, firstAt, to) {let out = []let i = firstAtwhile (i < to) {if (host.children[i] != null) { out.push(host.children[i]) }i = i + 1}return out}fire(&lit, event, ev) {if (event == 'click') { emit lit.click(ev) return null }if (event == 'input') { emit lit.input(ev) return null }if (event == 'change') { emit lit.change(ev) return null }if (event == 'submit') { emit lit.submit(ev) return null }if (event == 'keydown') { emit lit.keydown(ev) return null }if (event == 'keyup') { emit lit.keyup(ev) return null }if (event == 'focus') { emit lit.focus(ev) return null }if (event == 'blur') { emit lit.blur(ev) return null }if (event == 'dblclick') { emit lit.dblclick(ev) return null }if (event == 'pointerdown') { emit lit.pointerdown(ev) return null }if (event == 'pointermove') { emit lit.pointermove(ev) return null }if (event == 'pointerup') { emit lit.pointerup(ev) return null }if (event == 'pointercancel') { emit lit.pointercancel(ev) return null }// EVERY OTHER STANDARD DOM EVENT, by name (ticket #31): `on mouseover()` was bound// and never ran, because only the thirteen names above were written out. The// language's own dynamic emit dispatches the same handler an `emit lit.x(ev)` does.if (domEvents.includes(event)) { hlEmitArgs(&lit, event, [ev]) return null }console.warn('framework: not a standard DOM event, so nothing handles it here:', event)return null}// THE STANDARD DOM EVENTS a View handler may name beyond the thirteen `fire` writes out.// A name outside this list — a custom event an element dispatches itself — is not bound// yet: whether a View may handle one is the creator's to rule (ticket #31).static domEvents = ['mouseover' 'mouseout' 'mouseenter' 'mouseleave' 'mousedown' 'mouseup' 'mousemove' 'contextmenu' 'wheel' 'auxclick''pointerover' 'pointerout' 'pointerenter' 'pointerleave' 'gotpointercapture' 'lostpointercapture''touchstart' 'touchmove' 'touchend' 'touchcancel''keypress' 'focusin' 'focusout' 'beforeinput' 'compositionstart' 'compositionupdate' 'compositionend''select' 'selectionchange' 'invalid' 'reset' 'search' 'toggle' 'cancel' 'close''drag' 'dragstart' 'dragend' 'dragenter' 'dragleave' 'dragover' 'drop''copy' 'cut' 'paste' 'scroll' 'scrollend' 'resize' 'load' 'error' 'abort''play' 'pause' 'ended' 'playing' 'timeupdate' 'volumechange' 'seeking' 'seeked' 'loadeddata' 'loadedmetadata' 'canplay' 'canplaythrough' 'waiting' 'ratechange' 'durationchange' 'emptied' 'stalled' 'suspend' 'progress''animationstart' 'animationend' 'animationiteration' 'animationcancel' 'transitionstart' 'transitionend' 'transitionrun' 'transitioncancel']// ---- C2: MEMBERS WITH INITIALIZERS ARE DERIVATIONS -------------------------------// "Members with initializers are derivations that re-run locally when what they read// changes" (CONCEPT §2). Everything that sentence needs is compiler output: the// module's `derivations` table says what each initializer reads, and the class// carries each initializer as a callable keyed by its own site (`__derive__`, which// the emitter writes for EVERY member of EVERY class — it knows nothing of a View and// nothing of this framework; the root itself calls it instead of carrying a second// copy of the expression).//// So a write is followed by this: every derivation that reads a member which MOVED// runs again, in declaration order, and what it changes has moved too. The sites are// repainted afterwards, once per member — which is why this answers with the whole// moved set instead of painting as it goes.//// A DERIVATION IS NOT ITS OWN INPUT. A second assignment to a member at a root IS a// second initializer of that member (`out = out + 'x'` is one the language accepts),// and re-running that on a write to `out` would accumulate rather than derive. The// member's own name is therefore not one of its inputs here. Measured 2026-09-14:// none of the four reference apps has an initializer that reads its own member.//// THE BOUND IS THE NUMBER OF DERIVATIONS. One pass answers a file written top to// bottom; the rounds are there for a file that is not, and they stop where a cycle// between two members would otherwise spin.moved(&m, names) {let out = names.slice(0)let list = derivationsOf(m.key)if (list.length == 0) { return out }// A CLASS THAT COMPUTES NOTHING HERE HAS NO CALLABLE: every member of it is a// declaration without an initializer, a static, or the other realm's (demo-blog's// post page is one — its `post` comes from the server). The table still lists// those initializers, so the method is asked for before it is used.if (m.instance.__derive__ == null) { return out }let rounds = 0let again = truewhile (again && rounds <= list.length) {again = falserounds = rounds + 1for (d of list) {// AN EXPLICIT WRITE OUTRANKS A COMPUTED DEFAULT — the language's own rule// for construction (hlNew pins the caller's values while the root runs: "a// run-body assignment is a computed DEFAULT; an explicit caller value// outranks it"), read here for the same relation between a handler and an// initializer. demo-blog's post page is the case: its `postsChanged`// handler writes `title`, `paragraphs` and the rest from what the server// answered, and those members' initializers would otherwise recompute them// from a `_post` the browser never refreshed. What the handler wrote stands;// everything DOWNSTREAM of it is derived.if (names.includes(d.name)) { }// A SERVER REFERENCE SITE (compile.hl `derivList`'s `srv`: the initializer// calls an imported class, e.g. `conversations = … ? store.chatsOf(…) : []`)// is NEVER re-derived here, session sync or not — it is evaluated on the// SERVER, per request, and a browser build may not even carry what it calls// (routger, 2026-09-27: reached hl:time, which ships no client half, the// moment `session` moving from a sync made this member "moved" too). Its// fresh value ships in `sync` instead (WebFramework.hl `serverSyncNames`)// and lands here through the `names.includes(d.name)` branch above, an// explicit write like any other — this branch only stops the LOCAL derive// when the server did not (yet) ship one, leaving the site stale rather// than broken.else if (d.srv) { }else if (feedsFrom(d, out)) {let before = m.instance[d.name]// THE CLASS'S OWN CALLABLE, by the site the table named it with.// A member this realm does not compute (a server-realm initializer,// a static, the View) has no branch there and nothing happens.m.instance.__derive__(d.site)if (m.instance[d.name] != before) {if (!out.includes(d.name)) { out.push(d.name) again = true }}}}}return out}// does this derivation read one of the members that moved — its own name aside?feedsFrom(d, names) {for (r of d.reads) {if (r != d.name && names.includes(r)) { return true }}return false}// A WRITE SET, DERIVED AND THEN PAINTED: the one way in for every caller that knows// which members moved. Painting is the last act, so a derived member's sites are// drawn once, with its final value.repaintAll(&m, names) {if (names == null || names.length == 0) { return null }// whatever paints these members here, a late patch of them here has nothing left to dolateDone(m.chain, m.rootKind, names)painting = painting + 1for (n of moved(&m, names)) { repaint(&m, n) }painting = painting - 1return null}// ---- repaint: a member was written; every site that reads it is redrawn ---------write(&m, name, value) {m.instance[name] = valuepatch(m.chain, m.rootKind, [name], [])return null}repaint(&m, name) {let sites = m.sites.slice(0) // a paint may add sites; walk what was there// …and may DROP some: a branch that goes takes the sites inside it, and a site that// is no longer registered is not painted (the list is checked again after a drop)let gen = dropGenlet live = nullfor (s of sites) {if (s.names.includes(name)) {if (dropGen != gen) { live = liveSids(&m) gen = dropGen }if (live == null || live[s.sid] == true) { paint(&m, s, name) }}}// DOWN THE BINDINGS: a child bound to this member at its reference site gets// the value as its own member and repaints its own sites — the host knows the// binding, the child knows nothingfor (b of m.bindings) {// (a child whose branch is not standing is not composed: nothing to hand down)if (b.name == name && m.kids[b.kid] != null) {let kid = m.kids[b.kid]kid.instance[b.target] = bindingValue(b, &m)// …and the CHILD's own derivations follow the member it was givenrepaintAll(&kid, [b.target])
Only the first lines are shown.
Branches
- mainmain branch
Latest commits
- c6fbd011calendar mission 001 (1/4): code order — files moved: lib/events.hl, lib/users.hl, components/styles.hl (imports only); gates 78/0 + 18/0, live-data run identicalmre
- d6c59290calendar: Hybriel master 06617221 (plugin allocators 3a781359 + 413f60e4, mpackdb 2cb7ae5e, http1 773de63e); gates 78/0 + 18/0mre
- 7bd0337ccalendar: Hybriel master 190aa11d (fc838894 GC correctness, #126 closure scopes, #127); gates 78/0 + 18/0mre
- ff41310ccalendar: Hybriel master 8efba065 (#126 memory, #48 lambda copy; audit: no & needed)mre
- 14ba08c7antcolony#40: mission references point to the moved missionsmre
- 99c73346antcolony#40: history (LOG.md), worker briefs (missions/) and reports moved here from antcolony, numbered per project; old numbers in antcolony docs/mission-map.mdmre
- 76edaa62calendar: Hybriel master ff51cf46 (re-vendor round, static workaround removed)mre
- 90a3fc2cdeploy.sh: back up live storage/.sessions/.env before every deploy (newest 5 kept)mre
- 6722b72ddeploy.sh: never send .git or .gitignore to Byrodinmre
- be099807State of 2026-09-27, before the move to gitoriamre