gitoriaLog in with ident

gitoria

All repositories: gitoria

ReadmeCodePull requestsReleasesTicketsSettings
Commit4a2d71254a2d7125initial commitmre4a2d7125/plugins/web/client.hl

76.7 KB

  1. // The framework's client half — a file-class constructed ONCE in the browser by
  2. // the page's module script, with the seed the server rendered from. The
  3. // components are NOT preloaded: each mount carries the url its module is served
  4. // at and `hlLoad` fetches it when the mount happens. This file is the client
  5. // realm's entrypoint, and the page script sets that realm before constructing
  6. // it.
  7. //
  8. // Its whole job: CLAIM the DOM the server rendered for the mounted instances,
  9. // remember which elements read which member (the SITES), repaint a site when a
  10. // member is written, and turn a same-origin link into a navigation that fetches
  11. // the next route's tree and state and swaps the slot — the shell stays.
  12. import View from './view.hl'
  13. import Router from './router.hl'
  14. seedText = '' // the server's seed, as JSON text (the one construction argument)
  15. seed = null // { page: { key module params state view }, shell: … | null }
  16. view = new View
  17. router = null // the client's route table, from the seed
  18. shell = null // { key, instance, view, sites, root }
  19. page = null
  20. appHead = null // the app's manifest head defaults, off the seed: what a page
  21. // member that is null falls back to (title, description, image,
  22. // favicon, and the app's own meta list)
  23. slotEl = null // the shell element whose children are the route component
  24. detachedSlot = null // the slot element a shell region's rebuild took out of the DOM (see paint)
  25. socket = null // the websocket carrier, while it is open
  26. nextId = 0 // the pairing id an emit's ack comes back on
  27. pending = {} // 'k<i>' → the resolver waiting for that ack
  28. // AN ANSWER THAT ARRIVES BEFORE ITS PAGE IS MOUNTED (ticket #80). A component's root
  29. // runs while `instantiate` constructs it, and an `emit server` there goes out at once
  30. // (over REST: the socket opens after boot). What the face raised for this tab comes
  31. // back in the ack, before `page` — or, on a navigation, the NEXT page — is the mount
  32. // that holds the listener; delivered then, it reached the old mounts or none and was
  33. // lost. While a mount is being built the frames wait here, and go out in arrival
  34. // order once it stands.
  35. mounting = false
  36. held = []
  37. blueprints = {} // component key → { module, view }: every tree the page can
  38. // build, taken from each seed and never dropped (takeBlueprints)
  39. pendingFills = [] // fills a region rebuild anchored but could not build in place:
  40. // the host is named by address and re-entered in settle() below
  41. // THE EVENTS A REFERENCE OVERWROTE ON THE COMPOSITION'S ROOT, while the walk is inside
  42. // that composition and has not yet reached an element. A composed child's View may be a
  43. // bare reference to another component (routger's dialog-form.hl is one), so the element
  44. // that FINALLY stands as the child's root can be any number of references deeper — this
  45. // rides down through them and is spent on the first elements the walk claims or builds,
  46. // which are exactly those roots.
  47. rootOverwrites = null
  48. // IS THE PAGE ON THE DOCUMENT? A shell decides per request whether its render contains
  49. // a `slot` (COMPONENTS §7, "THE PARENT IS THE GATE"): `if (loggedIn) { main { slot } }`
  50. // standing false renders no page at all, and there is then nothing to claim. False here
  51. // means the page is constructed and seeded but has no DOM — it arrives when the region
  52. // holding the slot turns true, and leaves again when it turns false.
  53. pageShown = false
  54. // ---- THE PACKAGE'S BROWSER SURFACE: what `import { … } from 'hl:web'` binds ------
  55. //
  56. // A file's STATICS are what a braced import may take (the language's one rule for
  57. // `import { db } from './store.hl'`, and the same rule across a package's url),
  58. // so everything this half offers an app's component is declared here and nowhere
  59. // else. A static belongs to the CLASS, which is why these two reach each other
  60. // and why they are the SAME pair for every component module the page imports —
  61. // the browser loads this file once.
  62. //
  63. // `live` IS THE PAGE'S ONE CLIENT, written by the root below. The statics cannot
  64. // see an instance (they are evaluated at file load, before there is one), and a
  65. // static that is a FUNCTION does not need to: its body runs at the CALL, long
  66. // after the root has put the constructed client here.
  67. static live = { client = null }
  68. // PROGRAMMATIC NAVIGATION — the same act a click on a same-origin `<a>` performs,
  69. // asked for by code instead of by a pointer: a new history entry, then the next
  70. // route's tree and state over the boundary and into the shell's slot. A handler
  71. // that has just been told an id and must go to that thing's page has no link to
  72. // click, and this is what it calls instead. Everything about what a navigation IS
  73. // stays in `goTo` below; this is only the way in from outside the file.
  74. static navigate = (path) => { return live.client.goTo(path) }
  75. // ---- boot: construct the instances the server rendered, claim their DOM ----------
  76. // THE PAGE'S CLIENT, PUBLISHED TO THE FILE'S OWN CLASS, before anything else the
  77. // root does: `boot()` below constructs the components, and a component's module
  78. // may reach `navigate` from the moment it is loaded.
  79. Client.live.client = this
  80. seed = JSON.parse(seedText)
  81. router = new Router(routes = seed.routes)
  82. boot()
  83. boot() {
  84. mounting = true
  85. takeBlueprints(seed.blueprints)
  86. if (seed.shell != null) {
  87. shell = instantiate(seed.shell, [], 'shell')
  88. shell.isShell = true
  89. // (the BODY ELEMENT, nothing to do with `rootKind` below — a mount's `rootKind`
  90. // says which of the two roots it hangs from, 'page' or 'shell'. This field held
  91. // the DOM node under the same name until 2026-09-14, and the shell's address
  92. // therefore read as an element: every lift and every fill re-entry from a
  93. // composition IN THE SHELL resolved against the page instead, so a composed
  94. // control there wrote nothing — creator, W15.)
  95. shell.root = document.body
  96. // the body node's children are claimed against document.body, so the body
  97. // element takes the node itself here: a region hosted directly on it (an `if`
  98. // at the shell's root) rebuilds from it (measured 2026-09-13: a lone child
  99. // under `body` stayed on screen after its condition turned false)
  100. claim(&shell, document.body)
  101. }
  102. page = instantiate(seed.page, [], 'page')
  103. // no shell: the page IS the document's body content
  104. if (shell == null) { slotEl = document.body }
  105. // THE SHELL RENDERED NO SLOT — its region stood false for this request, so the
  106. // document carries no page. Claiming one anyway walked the page's tree against the
  107. // SHELL's own elements and silently attached the page's nodes to them ("claim: no
  108. // element for … under …" as soon as the two shapes differ), and the page never
  109. // appeared when the region later turned true. It waits instead.
  110. if (slotEl != null) {
  111. claim(&page, slotEl)
  112. pageShown = true
  113. }
  114. // the head members are sites of this mount from here on. They are NOT painted
  115. // now: the server's `document()` already wrote them into this document, and a
  116. // write to one of them repaints it the way every other member's write does.
  117. appHead = seed.head
  118. headSites(&page)
  119. release()
  120. listen()
  121. connect()
  122. }
  123. // the frames that waited for the mounts (above), now that they stand
  124. release() {
  125. mounting = false
  126. let frames = held
  127. held = []
  128. for (f of frames) { outward(f) }
  129. return null
  130. }
  131. // a mount's instance: the module LOADED (hlLoad — the loader primitive, which
  132. // on this target is the dynamic import of the url the server compiled the
  133. // component to), the class constructed, then the server's state laid over it.
  134. // The import is the browser's own cache: a second mount of the same component
  135. // fetches nothing.
  136. // The route's params and the server's state are the construction's NAMED
  137. // ARGUMENTS, pinned before the root runs — so a member the server evaluated
  138. // (a seeded one, declared without its initializer in this realm's projection)
  139. // is already there when a later line reads it, and a client-evaluable
  140. // initializer keeps the server's value instead of recomputing it.
  141. // THE BLUEPRINT TABLE, read by component key: the url the component's module is
  142. // served at and its View tree, one copy for the whole page. Every mount's tree is
  143. // read from here — the page's, the shell's, and every child's — and so is every
  144. // child the browser has to build itself.
  145. //
  146. // IT ONLY GROWS. Each seed carries the table for ITS route, and a navigation takes
  147. // the new entries WITHOUT dropping the old: a key is a component and a component is
  148. // one tree, so an entry is never wrong, and constructing a mount awaits its module —
  149. // long enough for a second navigation to have replaced the seed underneath it. A
  150. // table that was replaced left that half-built page reading the wrong route's
  151. // entries ("no blueprint for components/post.hl", on `/`, measured in the reference
  152. // gate 2026-09-13).
  153. takeBlueprints(table) {
  154. for (k of table.keys()) {
  155. if (blueprints[k] == null) { blueprints[k] = table[k] }
  156. }
  157. return null
  158. }
  159. blueprintOf(key) {
  160. return blueprints[key]
  161. }
  162. treeOf(key) {
  163. let bp = blueprintOf(key)
  164. // the table covers every component the page can reach, so this is a torn seed
  165. // and not a missing feature — say so loudly and paint nothing for that mount
  166. if (bp == null) { console.error('framework: the seed carries no blueprint for ' + key)
  167. return [] }
  168. return bp.view
  169. }
  170. // `chain` and `root` are the mount's ADDRESS: the kid keys from the page (or the
  171. // shell) down to it. A mount record is a value, so a site cannot hold the mount that
  172. // owns its fill — it holds this address, and `fillAt` re-enters the mount by
  173. // reference from the root when the fill has to be rebuilt (see settle below).
  174. instantiate(m, chain, root) {
  175. let args = {}
  176. for (k of m.state.keys()) { args[k] = m.state[k] }
  177. for (k of m.params.keys()) { args[k] = m.params[k] }
  178. let inst = hlLoad(m.module, args)
  179. // THE TREE COMES FROM THE TABLE, by this mount's key: a View belongs to the
  180. // COMPONENT, and the seed carries one copy of it however many mounts share it.
  181. let out = { key = m.key; instance = inst; view = treeOf(m.key); sites = []; isShell = false; kids = {}; bindings = []; chain = chain; rootKind = root; }
  182. // THE MIRROR: the components this one composes are instances here too, exactly
  183. // as the server stacked them — one per component node, constructed with the
  184. // bindings the server evaluated; a binding to a host MEMBER is remembered, so a
  185. // host write reaches the child's member and repaints the child's sites
  186. for (k of m.kids.keys()) { out.kids[k] = instantiate(m.kids[k], childChain(chain, k), root) }
  187. bindKids(&out, out.view)
  188. // A BOUND MEMBER'S VALUE IS THE HOST'S, HERE. The seed carried the server's
  189. // value, which is right for data and null for a FUNCTION (JSON has no form for
  190. // one) — and a function the host hands its child (`PostForm { onCancel = cancel
  191. // }`) is the way a child talks upward without knowing its host: it calls what it
  192. // was given, and the function writes the host's members through the instance
  193. // that wrote it. So every member binding is applied from the host instance now.
  194. for (b of out.bindings) {
  195. let kid = out.kids[b.kid]
  196. kid.instance[b.target] = bindingValue(b, &out)
  197. out.kids[b.kid] = kid
  198. }
  199. return out
  200. }
  201. bindKids(&m, nodes) {
  202. for (n of nodes) {
  203. if (n.k == 'component') {
  204. let kid = m.kids[view.kidKey(n.path)]
  205. if (kid != null) {
  206. // A BINDING IS A READ at the reference site: a member (`count = likes`)
  207. // or a field path off one (`postId = row.id`). Both follow the host's
  208. // write — the field path under the name it starts at.
  209. for (b of n.bindings) {
  210. // A MEMBER THAT ONLY FEEDS A BINDING still follows a write: `repaint`
  211. // walks the bindings after the sites, so a host member no element of the
  212. // host shows still reaches the child that was bound to it (measured
  213. // 2026-09-13 on the social app: `FollowButton { following =
  214. // authorFollowed }` stayed false). Under the digest that needed the
  215. // member to be in the compared map; under the write sets it needs
  216. // nothing but the edge itself.
  217. if (b.member != null) { m.bindings.push({ name = b.member; kid = view.kidKey(n.path); target = b.name; ref = null; }) }
  218. else if (b.ref != null) { m.bindings.push({ name = b.ref.name; kid = view.kidKey(n.path); target = b.name; ref = b.ref; }) }
  219. }
  220. }
  221. // the fill is THIS mount's fragment: a reference inside it binds against this instance
  222. if (n.fill != null) { bindKids(&m, n.fill) }
  223. } else if (n.k == 'el') {
  224. bindKids(&m, n.children)
  225. } else if (n.k == 'if') {
  226. bindKids(&m, n.then)
  227. bindKids(&m, n.other)
  228. }
  229. }
  230. return null
  231. }
  232. // a body-rooted View's children are the body's children
  233. childrenOf(nodes) {
  234. if (nodes.length == 1 && nodes[0].k == 'el' && nodes[0].tag == 'body') { return nodes[0].children }
  235. return nodes
  236. }
  237. // ---- WHAT THE COMPILE STEP ANSWERED, asked by name ---------------------------------
  238. // A local event's write set stands at its own listener and a region's names at its own
  239. // region: the compile step wrote them into the statements. These three are asked for by
  240. // NAME at run time and cannot be — an inbound frame names its event, a routine a host
  241. // handed down names the member that holds it, and a write set names the derivations
  242. // that follow it — so the component's compiled half carries them beside its walks.
  243. // Nothing here reduces a table: the answers are already the answers.
  244. eventWrites(key, event) {
  245. let comp = compiledOf(key)
  246. if (comp == null) { return [] }
  247. let names = comp.ev[event]
  248. return names == null ? [] : names
  249. }
  250. memberWrites(key, name) {
  251. let comp = compiledOf(key)
  252. if (comp == null) { return [] }
  253. let names = comp.mw[name]
  254. return names == null ? [] : names
  255. }
  256. derivationsOf(key) {
  257. let comp = compiledOf(key)
  258. if (comp == null) { return [] }
  259. return comp.dv
  260. }
  261. // ---- THE COMPILED COMPONENT: this framework's own output for this file ------------
  262. // The old hl:web walked a tree the seed carries — it pairs an element with a node, asks a table
  263. // what the element shows, and asks `view.value` what each bound spot holds. hl:web
  264. // ships the answers as the component's own code (plugins/web/compile.hl), and the
  265. // functions below are the FIXED runtime that code calls: the mount records, the keyed
  266. // region diff, the binding edges, the derivations, the socket and navigation stay here,
  267. // where they belong to the framework and not to any one component.
  268. //
  269. // EVERY CALL INTO GENERATED CODE PASSES BY REFERENCE. A plain call copies its
  270. // arguments (the language's value semantics), which for an instance holding a 2000-row
  271. // list would copy the list on every paint; `&` at the call site hands the value itself,
  272. // the way every walk in this file already hands a mount record.
  273. compiledOf(key) {
  274. let all = window.__hlC
  275. if (all == null) { return null }
  276. return all[key]
  277. }
  278. // the walk of a component's whole View, of one `for` body, of an `if` branch, of the
  279. // list an `if` stands in, or of the fragment a reference fills — each under its own site
  280. walkOf(key, slot, site) {
  281. let comp = compiledOf(key)
  282. if (comp == null) { return null }
  283. if (slot == 'v') { return comp.v }
  284. if (slot == 'b') { return comp.b[site] }
  285. if (slot == 't') { return comp.t[site] }
  286. if (slot == 'e') { return comp.e[site] }
  287. if (slot == 'f') { return comp.f[site] }
  288. if (slot == 'r') { return comp.r[site] }
  289. return null
  290. }
  291. // ONE ENTRY INTO GENERATED CODE. `f` is the walk, `m` the mount it runs for, `o` the
  292. // mount whose fragment a `slot` inside it would place.
  293. run(f, &m, &o, host, i, rows, rk, fill, cr, opt) {
  294. if (f == null) { return i }
  295. let c = this
  296. return f(&c, &m, &o, &host, i, &rows, rk, &fill, cr, &opt)
  297. }
  298. acqFail(tag, host) {
  299. console.error('claim: no element for', tag, 'under', host == null ? null : host.tagName)
  300. return null
  301. }
  302. // the classes a reference's `#Child` rule put on the composition root, beside whatever
  303. // class the root itself carries (view.hl withRootClasses, on the build walk)
  304. rootClasses(el, opt) {
  305. if (opt == null || opt.classes == null || opt.classes.length == 0) { return null }
  306. let add = ''
  307. for (c of opt.classes) { add = add == '' ? c : add + ' ' + c }
  308. let have = el.getAttribute('class')
  309. el.setAttribute('class', have == null || have == '' ? add : have + ' ' + add)
  310. return null
  311. }
  312. // AN ELEMENT THAT SHOWS SOMETHING is a site of this mount. The names come from the
  313. // generated call — the compile step read them off the View — and the two-way `value`
  314. // bind is applied where the element is a form control the member paints.
  315. siteAt(&m, el, names, site, valueMember, isSelect, rows) {
  316. let keep = []
  317. for (name of names) {
  318. // the SHELL's `slot` is where the route component hangs: swapped by navigation,
  319. // never painted (the compile step already drops `Style`, folded at build)
  320. if (!(name == 'slot' && m.isShell)) { keep.push(name) }
  321. }
  322. if (valueMember != null && rows[valueMember] == null && el.value != null) {
  323. bindValue(&m, valueMember, el, isSelect)
  324. }
  325. if (keep.length > 0) { m.sites.push({ el = el; site = site; names = keep; region = false; rows = rows; }) }
  326. return null
  327. }
  328. applySelectAt(&m, el, v) {
  329. el.value = v == null ? '' : '' + v
  330. return null
  331. }
  332. // A HANDLER, WIRED AT CREATION. The literal is the element's own — its named entries
  333. // are the generated `__hlLit`, the `for` rows in scope ride on it, and the owner is the
  334. // instance BY REFERENCE (a handler writing a copy would repaint nothing).
  335. onAt(&m, el, event, site, rows, wrote, touched) {
  336. if (el.__hlLits == null) { el.__hlLits = {} }
  337. el.__hlLits[event] = literalFor(&m, el)
  338. let at = m.chain
  339. let root = m.rootKind
  340. let rowNames = rows == null ? [] : rows.keys()
  341. el.addEventListener(event, (ev) => {
  342. let lit = el.__hlLits[event]
  343. for (rk of rowNames) { lit[rk] = el.__hlRows[rk] }
  344. fire(&lit, event, ev)
  345. el.__hlLits[event] = lit
  346. patch(at, root, wrote, touched)
  347. })
  348. return null
  349. }
  350. // THE SHELL'S SLOT: where the route component hangs. On the build walk the re-created
  351. // element adopts the page's nodes, and a page the shell never rendered is built here.
  352. slotShell(&m, host, cr) {
  353. slotEl = host
  354. if (!cr) { return null }
  355. if (detachedSlot != null && detachedSlot != host) {
  356. let moving = Array.from(detachedSlot.childNodes)
  357. for (node of moving) { host.appendChild(node) }
  358. }
  359. detachedSlot = null
  360. if (!pageShown && page != null) {
  361. page.sites = []
  362. create(&page, host)
  363. headSites(&page)
  364. pageShown = true
  365. }
  366. return null
  367. }
  368. // A CHILD'S SLOT: the fragment its HOST wrote, built in the host's frame. The fill names
  369. // that frame by address, and `run` re-enters it when this walk is not it.
  370. slotFill(&m, &o, host, i, fill, cr) {
  371. if (fill == null) { return i }
  372. if (cr && fill.detached) {
  373. let anchor = document.createComment('hl:slot')
  374. host.appendChild(anchor)
  375. pendingFills[] = { key = fill.key; site = fill.site; rows = fill.rows; rowKey = fill.rowKey; outer = fill.outer; chain = fill.chain; rootKind = fill.rootKind; anchor = anchor; }
  376. return i
  377. }
  378. if (ownsFill(o, fill)) {
  379. let f = walkOf(fill.key, 'f', fill.site)
  380. return run(f, &o, &o, host, i, fill.rows, fill.rowKey, fill.outer, cr, null)
  381. }
  382. return fillFromRoot(fill, host, i, cr)
  383. }
  384. // the fill's owner, re-entered from the page or the shell by the address it carries
  385. fillFromRoot(fill, host, at, cr) {
  386. if (fill.rootKind == 'shell') {
  387. if (shell == null) { return at }
  388. return fillInto(&shell, fill, 0, host, at, cr)
  389. }
  390. if (page == null) { return at }
  391. return fillInto(&page, fill, 0, host, at, cr)
  392. }
  393. fillInto(&m, fill, depth, host, at, cr) {
  394. if (depth >= fill.chain.length) {
  395. let f = walkOf(fill.key, 'f', fill.site)
  396. return run(f, &m, &m, host, at, fill.rows, fill.rowKey, fill.outer, cr, null)
  397. }
  398. let kid = m.kids[fill.chain[depth]]
  399. if (kid == null) { return at }
  400. let out = fillInto(&kid, fill, depth + 1, host, at, cr)
  401. m.kids[fill.chain[depth]] = kid
  402. return out
  403. }
  404. // the fill context a reference hands its child: the fragment's owner by address, the
  405. // host's rows and rowKey, and the fill the host itself was standing in
  406. fillFor(&m, spec, rows, rowKey, fill) {
  407. if (!spec.fill) { return null }
  408. return { key = m.key; site = spec.site; rows = rows; rowKey = rowKey; outer = fill; chain = m.chain; rootKind = m.rootKind; detached = false; }
  409. }
  410. // ---- a `for`: the region, its rows, its anchor ------------------------------------
  411. forAt(&m, &o, host, i, site, rowName, listFn, rows, rk, fill, cr, names) {
  412. regionAt(&m, host, site, rows, rk, fill, true, rowName, listFn, names)
  413. startRegionAt(host, site)
  414. let inst = m.instance
  415. let rws = rows
  416. let entries = listFn(&inst, &rws)
  417. let body = walkOf(m.key, 'b', site)
  418. if (entries != null) {
  419. let keys = listKeys(entries)
  420. let ri = 0
  421. for (entry of entries) {
  422. let inner = rows + {}
  423. inner[rowName] = entry
  424. let began = cr ? host.children.length : i
  425. let out = run(body, &m, &o, host, cr ? 0 : i, inner, rk + '#' + keys[ri], fill, cr, null)
  426. if (!cr) { i = out }
  427. noteRowAt(host, site, keys[ri], entry, elementsBetween(host, began, cr ? host.children.length : i))
  428. ri = ri + 1
  429. }
  430. }
  431. endRegionAt(host, site, cr ? null : (host.children[i] == null ? null : host.children[i]))
  432. return i
  433. }
  434. ifAt(&m, &o, host, i, site, condFn, rows, rk, fill, cr, names) {
  435. regionAt(&m, host, site, rows, rk, fill, false, null, condFn, names)
  436. let inst = m.instance
  437. let rws = rows
  438. let branch = condFn(&inst, &rws) ? walkOf(m.key, 't', site) : walkOf(m.key, 'e', site)
  439. let start = cr ? host.childNodes.length : i
  440. let out = run(branch, &m, &o, host, i, rows, rk, fill, cr, null)
  441. // a branch something can flip is marked, so a flip replaces THE BRANCH and nothing
  442. // else the host holds (ticket #32); a condition nothing can move needs no mark
  443. if (names != null && names.length > 0) { noteIfAt(host, site, cr, start, cr ? host.childNodes.length : out) }
  444. return out
  445. }
  446. // THE NODES AN `if` PUT INTO ITS HOST, and an anchor comment standing right after them.
  447. // Built: every node the walk appended. Claimed from the server's HTML: the elements the
  448. // walk claimed and the text between them. The anchor is where a branch that is empty
  449. // now puts the next one — the same mark a `for` leaves (endRegionAt).
  450. noteIfAt(host, site, cr, start, to) {
  451. let nodes = []
  452. if (cr) {
  453. let k = start
  454. while (k < to) { nodes.push(host.childNodes[k]) k = k + 1 }
  455. } else {
  456. let els = elementsBetween(host, start, to)
  457. if (els.length > 0) {
  458. let last = els[els.length - 1]
  459. let n = els[0]
  460. let more = true
  461. while (more && n != null) {
  462. nodes.push(n)
  463. if (n == last) { more = false } else { n = n.nextSibling }
  464. }
  465. }
  466. }
  467. let anchor = document.createComment('hl:if')
  468. let next = null
  469. if (!cr && host.children[to] != null) { next = host.children[to] }
  470. host.insertBefore(anchor, next)
  471. if (host.__hlIf == null) { host.__hlIf = {} }
  472. host.__hlIf[site] = { nodes = nodes; anchor = anchor; }
  473. return null
  474. }
  475. // A REGION SITE carries what the patch needs and nothing else: the names its list or
  476. // its condition reads (the compiler's answer, by this site), and the read itself as the
  477. // generated closure. One per element and site — a rebuild walks this node again.
  478. regionAt(&m, host, site, rows, rowKey, fill, isFor, rowName, readFn, names) {
  479. // a row-scoped read is its row's, and the language resolved it as one, so the
  480. // compile step names none here: an empty set is a region nothing can move
  481. if (names == null || names.length == 0) { return null }
  482. for (s of m.sites) {
  483. if (s.region && s.el == host && s.site == site) { return null }
  484. }
  485. let held = null
  486. if (fill != null) { held = fill + {} held.detached = true }
  487. m.sites.push({ el = host; site = site; names = names; region = true; rows = rows; rowKey = rowKey; fill = held; isFor = isFor; row = rowName; read = readFn; })
  488. return null
  489. }
  490. startRegionAt(host, site) {
  491. if (host.__hlFor == null) { host.__hlFor = {} }
  492. host.__hlFor[site] = { keys = []; rows = {}; anchor = null; }
  493. return null
  494. }
  495. noteRowAt(host, site, key, entry, els) {
  496. let box = host.__hlFor[site]
  497. if (box == null) { return null }
  498. box.keys.push(key)
  499. box.rows[key] = { els = els; entry = entry; }
  500. host.__hlFor[site] = box
  501. return null
  502. }
  503. endRegionAt(host, site, before) {
  504. let box = host.__hlFor[site]
  505. if (box == null) { return null }
  506. let anchor = document.createComment('hl:for')
  507. host.insertBefore(anchor, before)
  508. box.anchor = anchor
  509. host.__hlFor[site] = box
  510. return null
  511. }
  512. // ---- a composed child --------------------------------------------------------------
  513. // The reference's own description is written at its use site by the compile step: what
  514. // it binds, what it overwrites, the classes it puts on the child's roots.
  515. kidAt(&m, host, i, spec, rows, rk, fill, cr, opt) {
  516. let kk = spec.kid + rk
  517. let kid = m.kids[kk]
  518. if (kid == null) { kid = mintFrom(&m, spec, rows) }
  519. if (kid == null) { return i }
  520. bindFrom(&kid, &m, spec, rows)
  521. if (cr) { kid.sites = [] }
  522. kid.chain = childChain(m.chain, kk)
  523. kid.rootKind = m.rootKind
  524. let firstAt = cr ? host.children.length : i
  525. // WHAT A REFERENCE OVERWRITES REACHES THE FIRST ELEMENT, however deep. A child
  526. // whose View is a bare reference to another component renders no element of its
  527. // own, so the events the host overwrote travel on through it — the old hl:web kept that in
  528. // a field that the first element consumes; here it rides on the option the walk is
  529. // handed, and a reference with ons of its own replaces it.
  530. let over = []
  531. if (spec.ons != null && spec.ons.length > 0) { for (o of spec.ons) { over.push(o.event) } }
  532. else if (opt != null && opt.over != null) { over = opt.over }
  533. let kidOpt = { over = over; classes = spec.classes; }
  534. let f = walkOf(kid.key, 'v', null)
  535. let out = run(f, &kid, &m, host, cr ? 0 : i, {}, '', fillFor(&m, spec, rows, rk, fill), cr, kidOpt)
  536. if (!cr) { i = out }
  537. if (cr || kid.bindings.length == 0) { kid.bindings = [] bindKids(&kid, kid.view) }
  538. let roots = rootsBetween(host, firstAt, cr ? host.children.length : i)
  539. refOnsFrom(&m, spec, roots, rows)
  540. for (el of roots) { el.__hlKid = true }
  541. m.kids[kk] = kid
  542. return i
  543. }
  544. // the value a binding of this reference carries, off the host instance
  545. specValue(b, &m, rows) {
  546. if (b.text != null) { return b.value }
  547. if (b.member != null) { return view.value({ k = 'member'; name = b.member; }, m.instance, rows) }
  548. if (b.ref != null) {
  549. let inst = m.instance
  550. let rws = rows
  551. let f = b.ref
  552. return f(&inst, &rws)
  553. }
  554. return null
  555. }
  556. mintFrom(&m, spec, rows) {
  557. let bp = blueprintOf(spec.key)
  558. if (bp == null) { return null }
  559. let args = {}
  560. for (b of spec.bindings) { args[b.name] = specValue(b, &m, rows) }
  561. let inst = hlLoad(bp.module, args)
  562. return { key = spec.key; instance = inst; view = bp.view; sites = []; isShell = false; kids = {}; bindings = []; chain = []; rootKind = m.rootKind; }
  563. }
  564. bindFrom(&kid, &m, spec, rows) {
  565. for (b of spec.bindings) { kid.instance[b.name] = specValue(b, &m, rows) }
  566. return null
  567. }
  568. // A HANDLER WRITTEN ON A REFERENCE is the HOST's: its literal is minted here with the
  569. // reference's bindings as its own entries, and its write set is filed under the
  570. // reference's own `on` site in the host's table.
  571. refOnsFrom(&m, spec, els, rows) {
  572. if (spec.ons == null || spec.ons.length == 0) { return null }
  573. let named = {}
  574. for (b of spec.bindings) { named[b.name] = specValue(b, &m, rows) }
  575. for (k of rows.keys()) { named[k] = rows[k] }
  576. let inst = m.instance
  577. let lit = hlLiteralNew(spec.site, named, &inst)
  578. for (el of els) {
  579. for (o of spec.ons) { listenRef(&m, &lit, el, o) }
  580. }
  581. return null
  582. }
  583. // ---- claim: walk the tree in lockstep with the DOM the same tree produced --------
  584. // Round one granularity: an element whose children include a member is a SITE for
  585. // each of those members; a repaint rewrites that element's children from the tree.
  586. // `i` is the running index into `host.children`; a `for` consumes one run of
  587. // elements per entry, an `if` the run of the branch that stands.
  588. claim(&m, host) {
  589. let f = walkOf(m.key, 'v', null)
  590. run(f, &m, &m, host, 0, {}, '', null, false, null)
  591. return null
  592. }
  593. // a kid's address: its host's, plus the key the host holds it under
  594. childChain(chain, kk) {
  595. let out = chain == null ? [] : chain.slice(0)
  596. out.push(kk)
  597. return out
  598. }
  599. // the elements a walk placed under `host` between two indices
  600. elementsBetween(host, at, to) {
  601. let out = []
  602. let i = at
  603. while (i < to) {
  604. if (host.children[i] != null) { out.push(host.children[i]) }
  605. i = i + 1
  606. }
  607. return out
  608. }
  609. // the keys a list has, in order: the record's id (view.rowKey), an index where the row
  610. // is no record, and a suffix where one id stands twice. THE SERVER DOES NOT DEDUPE —
  611. // its `mountKids` gives two rows of one id one mount key; a list with repeated ids is
  612. // written down here rather than answered twice.
  613. listKeys(entries) {
  614. let out = []
  615. let seen = {}
  616. let ri = 0
  617. for (entry of entries) {
  618. let k = view.rowKey(entry, ri)
  619. if (seen[k] != null) { k = k + ':' + ri }
  620. seen[k] = true
  621. out.push(k)
  622. ri = ri + 1
  623. }
  624. return out
  625. }
  626. // ---- the patch: old keys against new ---------------------------------------------
  627. // A write to the list is answered by a DIFF and nothing else: the rows that are gone
  628. // are removed, the rows that arrived are built, the rows that stayed are MOVED where
  629. // the order changed and are left alone where it did not — and each of them is handed
  630. // the record it now stands for, which rewrites only the attributes and the text the
  631. // DOM does not already hold. The region's element is never emptied.
  632. paintFor(&m, s) {
  633. let host = s.el
  634. if (host.__hlFor == null) { return null }
  635. let box = host.__hlFor[s.site]
  636. if (box == null || box.anchor == null) { return null }
  637. // ONE PAINT OF A LIST AT A TIME. Building a row can wait (a composed child's module
  638. // loads on first use), and a second write to the list in that time — a push right
  639. // behind the handler's own write — read the box before the first paint had filed
  640. // its row, and built the row again. The first paint used to be hidden by the `if`
  641. // beside the list rebuilding the whole host; since an `if` replaces only its branch
  642. // (ticket #32) it showed as a doubled comment. A paint that arrives while one runs
  643. // is remembered, and the running one paints again from the list as it then stands.
  644. if (host.__hlForBusy == null) { host.__hlForBusy = {} }
  645. if (host.__hlForBusy[s.site] != null) { host.__hlForBusy[s.site] = 'again' return null }
  646. host.__hlForBusy[s.site] = 'busy'
  647. let inst = m.instance
  648. let rws = s.rows
  649. let read = s.read
  650. let entries = read(&inst, &rws)
  651. if (entries == null) { entries = [] }
  652. // the keys this list has now, in order
  653. let keys = listKeys(entries)
  654. let next = []
  655. let ri = 0
  656. for (entry of entries) {
  657. next.push({ key = keys[ri]; entry = entry; })
  658. ri = ri + 1
  659. }
  660. // 1. THE ROWS THAT ARE GONE: their elements, their sites and their children's
  661. // mounts leave together.
  662. let keep = {}
  663. for (r of next) { keep[r.key] = true }
  664. for (k of box.keys) {
  665. if (keep[k] == null) { dropRow(&m, s, &box, k) }
  666. }
  667. // 2. THE ROWS IN ORDER. `at` is the node standing where the next row belongs: a
  668. // row already there advances it, a row that is not is moved or built before it.
  669. let at = box.anchor
  670. let found = false
  671. for (k of box.keys) {
  672. if (!found && keep[k] != null) {
  673. let held = box.rows[k]
  674. if (held != null && held.els.length > 0) { at = held.els[0] found = true }
  675. }
  676. }
  677. let placed = []
  678. for (r of next) {
  679. let held = box.rows[r.key]
  680. if (held == null) {
  681. buildRow(&m, s, &box, r, at)
  682. } else if (held.els.length > 0 && held.els[0] == at) {
  683. // already in place: step over it
  684. at = afterRow(held, box.anchor)
  685. updateRow(&m, s, &box, r)
  686. } else {
  687. moveRow(host, held, at)
  688. updateRow(&m, s, &box, r)
  689. }
  690. placed.push(r.key)
  691. }
  692. box.keys = placed
  693. host.__hlFor[s.site] = box
  694. let again = host.__hlForBusy[s.site] == 'again'
  695. host.__hlForBusy[s.site] = null
  696. if (again) { paintFor(&m, s) }
  697. return null
  698. }
  699. // the node that follows a row's elements — where the walk goes on from
  700. afterRow(held, anchor) {
  701. if (held.els.length == 0) { return anchor }
  702. let last = held.els[held.els.length - 1]
  703. if (last.nextSibling == null) { return anchor }
  704. return last.nextSibling
  705. }
  706. // A ROW LEAVES: its elements go off the document, the sites that were registered
  707. // inside it are dropped (they point at nodes nobody can see), and so do the mounts of
  708. // the children it held — a child of a row is keyed by that row (view.rowKey).
  709. dropRow(&m, s, &box, key) {
  710. let held = box.rows[key]
  711. if (held == null) { return null }
  712. let kept = []
  713. for (site of m.sites) {
  714. if (!insideRow(held, site.el)) { kept.push(site) }
  715. }
  716. m.sites = kept
  717. let full = (s.rowKey == null ? '' : s.rowKey) + '#' + key
  718. for (kk of m.kids.keys()) {
  719. if (kk.includes(full + '#') || kk.endsWith(full)) { delete m.kids[kk] }
  720. }
  721. for (el of held.els) { el.remove() }
  722. delete box.rows[key]
  723. return null
  724. }
  725. insideRow(held, el) {
  726. if (el == null) { return false }
  727. for (top of held.els) {
  728. if (top == el) { return true }
  729. if (top.contains(el)) { return true }
  730. }
  731. return false
  732. }
  733. // A ROW MOVES: `moveBefore` where the browser has it, because it keeps the element's
  734. // state (focus, a playing video, an open dialog) across the move; `insertBefore` else.
  735. moveRow(host, held, at) {
  736. for (el of held.els) {
  737. if (host.moveBefore != null) { host.moveBefore(el, at) }
  738. else { host.insertBefore(el, at) }
  739. }
  740. return null
  741. }
  742. // A ROW ARRIVES: built detached and moved in, because `createInto` appends and the
  743. // row's place is where `at` stands — which may be in the middle of the region.
  744. buildRow(&m, s, &box, r, at) {
  745. let host = s.el
  746. let inner = s.rows + {}
  747. inner[s.row] = r.entry
  748. let bin = document.createElement('div')
  749. let body = walkOf(m.key, 'b', s.site)
  750. run(body, &m, &m, bin, 0, inner, (s.rowKey == null ? '' : s.rowKey) + '#' + r.key, s.fill, true, null)
  751. let els = []
  752. let moving = Array.from(bin.children)
  753. for (el of moving) { els.push(el) }
  754. let nodes = Array.from(bin.childNodes)
  755. for (node of nodes) { host.insertBefore(node, at) }
  756. box.rows[r.key] = { els = els; entry = r.entry; }
  757. return null
  758. }
  759. // A ROW STAYS, AND THE RECORD IT SHOWS MAY BE ANOTHER ONE — `upsert` hands the row a
  760. // new record under the same id, and `items[0].title = 'edited'` writes into the one it
  761. // already has. Both are answered here: the row's elements are handed the record (their
  762. // `__hlRows`, which is also what a handler on the row reads), and every attribute and
  763. // text leaf is rewritten ONLY where the DOM does not already hold the value. That is
  764. // why a push into 2000 rows costs no mutation on the 2000 that did not change.
  765. updateRow(&m, s, &box, r) {
  766. let held = box.rows[r.key]
  767. if (held == null) { return null }
  768. held.entry = r.entry
  769. box.rows[r.key] = held
  770. for (el of held.els) { refreshRow(&m, el, s.row, r.entry) }
  771. rebindRow(&m, s, r)
  772. return null
  773. }
  774. // A ROW'S CHILDREN TAKE THEIR BINDINGS AGAIN. A reference inside a `for` body is not
  775. // in `m.bindings` — that list is built from the View's own walk, which does not enter a
  776. // `for` (the row is the scope, and there is one child per row) — so a host member that
  777. // feeds a row's child used to reach it only because the region REBUILT and the child
  778. // was minted again with the value of the moment (the framework gate's own case:
  779. // home.hl writes `noteOpen`, every row's RowNote takes it and hands it to RowMark, and
  780. // the grandchild's region appears). Nothing rebuilds any more, so the edge is walked
  781. // here — and only what MOVED is written and repainted, or every list write would
  782. // repaint every row's child.
  783. rebindRow(&m, s, r) {
  784. let rk = (s.rowKey == null ? '' : s.rowKey) + '#' + r.key
  785. let inner = s.rows + {}
  786. inner[s.row] = r.entry
  787. rebindNodes(&m, rowBody(m.key, s.site), inner, rk)
  788. return null
  789. }
  790. // THE ROW'S OWN NODES, for the binding edges a row's child takes again. The tree is
  791. // still what the seed carries (it is the mount's `view`), and this is a read of it, not
  792. // a walk of the DOM: the compiled row factory builds, this re-binds.
  793. rowBody(key, site) {
  794. let bp = blueprints[key]
  795. if (bp == null) { return [] }
  796. return bodyIn(bp.view, site)
  797. }
  798. bodyIn(nodes, site) {
  799. for (n of nodes) {
  800. if (n.k == 'for' && n.site == site) { return n.body }
  801. if (n.k == 'el') { let got = bodyIn(n.children, site) if (got.length > 0) { return got } }
  802. if (n.k == 'if') {
  803. let a = bodyIn(n.then, site)
  804. if (a.length > 0) { return a }
  805. let b = bodyIn(n.other, site)
  806. if (b.length > 0) { return b }
  807. }
  808. if (n.k == 'for') { let got = bodyIn(n.body, site) if (got.length > 0) { return got } }
  809. if (n.k == 'component' && n.fill != null) { let got = bodyIn(n.fill, site) if (got.length > 0) { return got } }
  810. }
  811. return []
  812. }
  813. rebindNodes(&m, nodes, rows, rk) {
  814. for (n of nodes) {
  815. if (n.k == 'component') {
  816. let kk = view.kidKey(n.path) + rk
  817. let kid = m.kids[kk]
  818. if (kid != null) {
  819. let moved = []
  820. for (b of n.bindings) {
  821. let v = null
  822. if (b.text != null) { v = view.refValue(b) }
  823. else if (b.member != null) { v = view.value({ k = 'member'; name = b.member; }, m.instance, rows) }
  824. else if (b.ref != null) { v = view.value(b.ref, m.instance, rows) }
  825. if (kid.instance[b.name] != v) { kid.instance[b.name] = v moved.push(b.name) }
  826. }
  827. if (moved.length > 0) { repaintAll(&kid, moved) }
  828. m.kids[kk] = kid
  829. }
  830. if (n.fill != null) { rebindNodes(&m, n.fill, rows, rk) }
  831. } else if (n.k == 'el') {
  832. rebindNodes(&m, n.children, rows, rk)
  833. } else if (n.k == 'if') {
  834. rebindNodes(&m, n.then, rows, rk)
  835. rebindNodes(&m, n.other, rows, rk)
  836. }
  837. }
  838. return null
  839. }
  840. refreshRow(&m, el, name, entry) {
  841. // A COMPOSED CHILD'S DOM IS ITS OWN MOUNT'S: its elements read the CHILD's
  842. // instance, and painting them from this one would show the wrong values. The walk
  843. // stops at the roots a reference claimed (`__hlKid`, set where they are claimed
  844. // and built).
  845. if (el.__hlKid == true) { return null }
  846. if (el.__hlRows != null) {
  847. let next = el.__hlRows + {}
  848. next[name] = entry
  849. el.__hlRows = next
  850. // (The literal a handler on this element fires takes its row entries from
  851. // `__hlRows` when it fires — see `bind` — so there is nothing to update here.
  852. // A handler written on a REFERENCE (bindRefOns) mints one literal for the
  853. // child's roots and does not: its bindings are re-applied by rebindRow, its
  854. // own entries are not.)
  855. }
  856. rowPaint(&m, el)
  857. let kids = Array.from(el.children)
  858. for (k of kids) { refreshRow(&m, k, name, entry) }
  859. return null
  860. }
  861. // ONE ELEMENT OF A ROW, REDRAWN WHERE IT IS WRONG. The guard is the DOM's own value,
  862. // not a remembered one: nothing is serialised and nothing is compared to a digest.
  863. rowPaint(&m, el) {
  864. let site = el.__hlSite
  865. if (site == null) { return null }
  866. let comp = compiledOf(el.__hlKey == null ? m.key : el.__hlKey)
  867. if (comp == null) { return null }
  868. let entry = comp.p[site]
  869. if (entry == null) { return null }
  870. let f = entry.r
  871. if (f == null) { return null }
  872. let inst = m.instance
  873. let rws = el.__hlRows
  874. let e = el
  875. f(&e, &inst, &rws)
  876. return null
  877. }
  878. bindValue(&m, name, el, isSelect) {
  879. let held = m.instance[name] == null ? '' : '' + m.instance[name]
  880. // A SELECT IS PAINTED BY THE MEMBER FIRST: what it shows is one of its options,
  881. // and the member decides which. The server already marked it (view.hl `selected`),
  882. // so this is a no-op there; on a select the client built it is the paint. Only
  883. // then is the read-back right — before it, an unpainted select reports its FIRST
  884. // option and the read-back would write that back into the member.
  885. if (isSelect && held != '') { el.value = held }
  886. if (el.value != held) { m.instance[name] = el.value liftValue(m.chain, m.rootKind, name, el.value) }
  887. el.addEventListener('input', (ev) => {
  888. m.instance[name] = el.value
  889. // AND UP THROUGH THE REFERENCE THAT BOUND IT, if this control stands inside a
  890. // composed child: the host's member is what the app reads (see liftValue)
  891. liftValue(m.chain, m.rootKind, name, el.value)
  892. })
  893. return null
  894. }
  895. // ---- `value` IS TWO-WAY THROUGH A COMPOSITION TOO --------------------------------
  896. // `value = member` on a control is the framework's two-way name: the member paints the
  897. // field and the field writes the member (mission 132). A composed child is a mount of
  898. // its own, so that write landed on the CHILD's member and stopped there — `Field
  899. // { value = who }` left the host's `who` empty while the DOM held what was typed, and
  900. // routger's login submit read null (creator, W12). The host→child binding is an edge
  901. // the mount already carries; this is the SAME edge run backwards on input, and only
  902. // for the name `value`: every other binding name stays one-way.
  903. //
  904. // The child cannot name its host (a mount record is a value), so it names it by the
  905. // ADDRESS it already carries — the kid keys from the page or the shell — and the walk
  906. // below re-enters the host by reference. It carries on upward as long as the reference
  907. // it came through was itself a `value` binding, which is what makes a component whose
  908. // View is a bare reference to a field reach the page's member through both hops.
  909. liftValue(chain, root, target, value) {
  910. if (target != 'value') { return null }
  911. if (chain == null || chain.length == 0) { return null }
  912. if (root == 'shell') {
  913. if (shell == null) { return null }
  914. liftInto(&shell, chain, 0, target, value)
  915. } else {
  916. if (page == null) { return null }
  917. liftInto(&page, chain, 0, target, value)
  918. }
  919. return null
  920. }
  921. // walk the address down to the mount that HOLDS the last key — that mount is the host —
  922. // and write what its reference bound to the child's `target`, repainting its own sites
  923. liftInto(&m, chain, at, target, value) {
  924. if (at >= chain.length - 1) {
  925. for (b of m.bindings) {
  926. // a field path (`value = row.text`) names no member to write back into
  927. if (b.kid == chain[at] && b.target == target && b.ref == null) {
  928. if (m.instance[b.name] != value) {
  929. m.instance[b.name] = value
  930. repaintAll(&m, [b.name])
  931. }
  932. liftValue(m.chain, m.rootKind, b.name, value)
  933. }
  934. }
  935. return null
  936. }
  937. let kid = m.kids[chain[at]]
  938. if (kid == null) { return null }
  939. liftInto(&kid, chain, at + 1, target, value)
  940. m.kids[chain[at]] = kid
  941. return null
  942. }
  943. // THE ELEMENT'S LITERAL: a value of the class the module minted for the literal's
  944. // SITE (`file:line:col`, the id the JavaScript target registers at load), with the
  945. // element's own named entries — its attributes as the tree has them — and the
  946. // `for` row variables in scope as its own entries, the way the language builds a
  947. // row's literal (the row rides on the value). Built HERE and not looked up in the
  948. // instance's View: a row created after the list changed has no value there, and
  949. // a name-path cannot address ordered content. The handlers read own entries first,
  950. // then the owner's members — the owner is the component instance.
  951. literalFor(&m, el) {
  952. let rows = el.__hlRows
  953. let inst = m.instance
  954. let rws = rows
  955. // THE ELEMENT'S OWN ENTRIES, compiled: its attributes by name, with the reads
  956. // written in (compile.hl `litNamed`)
  957. let mk = el.__hlLit
  958. let named = mk == null ? {} : mk(&inst, &rws)
  959. for (k of rows.keys()) { named[k] = rows[k] }
  960. // the owner by REFERENCE: a call argument is copied, and a handler writing a
  961. // copy's member would repaint nothing
  962. return hlLiteralNew(el.__hlSite, named, &inst)
  963. }
  964. // WHAT A HANDLER WROTE, REPAINTED — the whole of the answer to a local event.
  965. // `&m` is the mount whose instance the handler's owner is, so a write reaches this
  966. // mount's sites and, through its bindings, the children that read the member.
  967. //
  968. // AND UPWARD, THROUGH A ROUTINE THE HOST HANDED DOWN. `Badge { onHide = hideBadge }`
  969. // gives the child a FUNCTION of the host's; the child's handler calls it and the
  970. // host's member moves. The child's own table cannot say so — the member it called
  971. // has no initializer there — but the HOST's does: the binding edge names the host
  972. // member (`hideBadge`), and the host's table says what calling it writes. That is
  973. // the same table read one mount up, not a new mechanism.
  974. patch(chain, root, names, targets) {
  975. patchAt(chain, root, names)
  976. patchUp(chain, root, targets)
  977. settle()
  978. return null
  979. }
  980. // A MOUNT RECORD IS A VALUE (the rule this whole file is written around), so a
  981. // listener cannot hold the live one — the record it was bound against is a copy the
  982. // walk wrote back, and a region rebuild replaces it again. It holds the mount's
  983. // ADDRESS instead and the walk re-enters from the root, exactly as `liftInto` and
  984. // `fillAt` do. `refresh()` used to get this for free by starting at the root every
  985. // time; a write set has to say where it lands.
  986. patchAt(chain, root, names) {
  987. if (names == null || names.length == 0) { return null }
  988. if (root == 'shell') {
  989. if (shell == null) { return null }
  990. patchDown(&shell, chain, 0, names)
  991. } else {
  992. if (page == null) { return null }
  993. patchDown(&page, chain, 0, names)
  994. }
  995. return null
  996. }
  997. patchDown(&m, chain, at, names) {
  998. if (chain == null || at >= chain.length) {
  999. repaintAll(&m, names)
  1000. return null
  1001. }
  1002. let kid = m.kids[chain[at]]
  1003. if (kid == null) { return null }
  1004. patchDown(&kid, chain, at + 1, names)
  1005. m.kids[chain[at]] = kid
  1006. return null
  1007. }
  1008. patchUp(chain, root, targets) {
  1009. if (targets == null || targets.length == 0) { return null }
  1010. if (chain == null || chain.length == 0) { return null }
  1011. if (root == 'shell') {
  1012. if (shell == null) { return null }
  1013. patchInto(&shell, chain, 0, targets)
  1014. } else {
  1015. if (page == null) { return null }
  1016. patchInto(&page, chain, 0, targets)
  1017. }
  1018. return null
  1019. }
  1020. // walk the address down to the mount that HOLDS the last key — that mount is the host
  1021. // — and repaint what the routines it bound into the child write (and carry on upward,
  1022. // because the host may have received them from ITS host)
  1023. patchInto(&m, chain, at, targets) {
  1024. if (at >= chain.length - 1) {
  1025. let names = []
  1026. let up = []
  1027. for (b of m.bindings) {
  1028. if (b.kid == chain[at] && b.ref == null && targets.includes(b.target)) {
  1029. if (!up.includes(b.name)) { up.push(b.name) }
  1030. for (w of memberWrites(m.key, b.name)) { if (!names.includes(w)) { names.push(w) } }
  1031. }
  1032. }
  1033. repaintAll(&m, names)
  1034. patchUp(m.chain, m.rootKind, up)
  1035. return null
  1036. }
  1037. let kid = m.kids[chain[at]]
  1038. if (kid == null) { return null }
  1039. patchInto(&kid, chain, at + 1, targets)
  1040. m.kids[chain[at]] = kid
  1041. return null
  1042. }
  1043. // one listener, in its own frame so the event name it closes over is this one.
  1044. // `&m` is the HOST — a handler written on a reference is the host's, and its write
  1045. // set is filed under the reference's own `on` site in the host's table.
  1046. listenRef(&m, &lit, el, o) {
  1047. let wrote = o.sets[0]
  1048. let touched = o.sets[1]
  1049. let at = m.chain
  1050. let root = m.rootKind
  1051. el.addEventListener(o.event, (ev) => {
  1052. fire(&lit, o.event, ev)
  1053. patch(at, root, wrote, touched)
  1054. })
  1055. return null
  1056. }
  1057. // the elements a composed child rendered directly under `host`, between two marks in
  1058. // the child list — its roots, whatever its tree put there
  1059. rootsBetween(host, firstAt, to) {
  1060. let out = []
  1061. let i = firstAt
  1062. while (i < to) {
  1063. if (host.children[i] != null) { out.push(host.children[i]) }
  1064. i = i + 1
  1065. }
  1066. return out
  1067. }
  1068. fire(&lit, event, ev) {
  1069. if (event == 'click') { emit lit.click(ev) return null }
  1070. if (event == 'input') { emit lit.input(ev) return null }
  1071. if (event == 'change') { emit lit.change(ev) return null }
  1072. if (event == 'submit') { emit lit.submit(ev) return null }
  1073. if (event == 'keydown') { emit lit.keydown(ev) return null }
  1074. if (event == 'keyup') { emit lit.keyup(ev) return null }
  1075. if (event == 'focus') { emit lit.focus(ev) return null }
  1076. if (event == 'blur') { emit lit.blur(ev) return null }
  1077. if (event == 'dblclick') { emit lit.dblclick(ev) return null }
  1078. if (event == 'pointerdown') { emit lit.pointerdown(ev) return null }
  1079. if (event == 'pointermove') { emit lit.pointermove(ev) return null }
  1080. if (event == 'pointerup') { emit lit.pointerup(ev) return null }
  1081. if (event == 'pointercancel') { emit lit.pointercancel(ev) return null }
  1082. // EVERY OTHER STANDARD DOM EVENT, by name (ticket #31): `on mouseover()` was bound
  1083. // and never ran, because only the thirteen names above were written out. The
  1084. // language's own dynamic emit dispatches the same handler an `emit lit.x(ev)` does.
  1085. if (domEvents.includes(event)) { hlEmitArgs(&lit, event, [ev]) return null }
  1086. console.warn('framework: not a standard DOM event, so nothing handles it here:', event)
  1087. return null
  1088. }
  1089. // THE STANDARD DOM EVENTS a View handler may name beyond the thirteen `fire` writes out.
  1090. // A name outside this list — a custom event an element dispatches itself — is not bound
  1091. // yet: whether a View may handle one is the creator's to rule (ticket #31).
  1092. static domEvents = ['mouseover' 'mouseout' 'mouseenter' 'mouseleave' 'mousedown' 'mouseup' 'mousemove' 'contextmenu' 'wheel' 'auxclick'
  1093. 'pointerover' 'pointerout' 'pointerenter' 'pointerleave' 'gotpointercapture' 'lostpointercapture'
  1094. 'touchstart' 'touchmove' 'touchend' 'touchcancel'
  1095. 'keypress' 'focusin' 'focusout' 'beforeinput' 'compositionstart' 'compositionupdate' 'compositionend'
  1096. 'select' 'selectionchange' 'invalid' 'reset' 'search' 'toggle' 'cancel' 'close'
  1097. 'drag' 'dragstart' 'dragend' 'dragenter' 'dragleave' 'dragover' 'drop'
  1098. 'copy' 'cut' 'paste' 'scroll' 'scrollend' 'resize' 'load' 'error' 'abort'
  1099. 'play' 'pause' 'ended' 'playing' 'timeupdate' 'volumechange' 'seeking' 'seeked' 'loadeddata' 'loadedmetadata' 'canplay' 'canplaythrough' 'waiting' 'ratechange' 'durationchange' 'emptied' 'stalled' 'suspend' 'progress'
  1100. 'animationstart' 'animationend' 'animationiteration' 'animationcancel' 'transitionstart' 'transitionend' 'transitionrun' 'transitioncancel']
  1101. // ---- C2: MEMBERS WITH INITIALIZERS ARE DERIVATIONS -------------------------------
  1102. // "Members with initializers are derivations that re-run locally when what they read
  1103. // changes" (CONCEPT §2). Everything that sentence needs is compiler output: the
  1104. // module's `derivations` table says what each initializer reads, and the class
  1105. // carries each initializer as a callable keyed by its own site (`__derive__`, which
  1106. // the emitter writes for EVERY member of EVERY class — it knows nothing of a View and
  1107. // nothing of this framework; the root itself calls it instead of carrying a second
  1108. // copy of the expression).
  1109. //
  1110. // So a write is followed by this: every derivation that reads a member which MOVED
  1111. // runs again, in declaration order, and what it changes has moved too. The sites are
  1112. // repainted afterwards, once per member — which is why this answers with the whole
  1113. // moved set instead of painting as it goes.
  1114. //
  1115. // A DERIVATION IS NOT ITS OWN INPUT. A second assignment to a member at a root IS a
  1116. // second initializer of that member (`out = out + 'x'` is one the language accepts),
  1117. // and re-running that on a write to `out` would accumulate rather than derive. The
  1118. // member's own name is therefore not one of its inputs here. Measured 2026-09-14:
  1119. // none of the four reference apps has an initializer that reads its own member.
  1120. //
  1121. // THE BOUND IS THE NUMBER OF DERIVATIONS. One pass answers a file written top to
  1122. // bottom; the rounds are there for a file that is not, and they stop where a cycle
  1123. // between two members would otherwise spin.
  1124. moved(&m, names) {
  1125. let out = names.slice(0)
  1126. let list = derivationsOf(m.key)
  1127. if (list.length == 0) { return out }
  1128. // A CLASS THAT COMPUTES NOTHING HERE HAS NO CALLABLE: every member of it is a
  1129. // declaration without an initializer, a static, or the other realm's (demo-blog's
  1130. // post page is one — its `post` comes from the server). The table still lists
  1131. // those initializers, so the method is asked for before it is used.
  1132. if (m.instance.__derive__ == null) { return out }
  1133. let rounds = 0
  1134. let again = true
  1135. while (again && rounds <= list.length) {
  1136. again = false
  1137. rounds = rounds + 1
  1138. for (d of list) {
  1139. // AN EXPLICIT WRITE OUTRANKS A COMPUTED DEFAULT — the language's own rule
  1140. // for construction (hlNew pins the caller's values while the root runs: "a
  1141. // run-body assignment is a computed DEFAULT; an explicit caller value
  1142. // outranks it"), read here for the same relation between a handler and an
  1143. // initializer. demo-blog's post page is the case: its `postsChanged`
  1144. // handler writes `title`, `paragraphs` and the rest from what the server
  1145. // answered, and those members' initializers would otherwise recompute them
  1146. // from a `_post` the browser never refreshed. What the handler wrote stands;
  1147. // everything DOWNSTREAM of it is derived.
  1148. if (names.includes(d.name)) { }
  1149. else if (feedsFrom(d, out)) {
  1150. let before = m.instance[d.name]
  1151. // THE CLASS'S OWN CALLABLE, by the site the table named it with.
  1152. // A member this realm does not compute (a server-realm initializer,
  1153. // a static, the View) has no branch there and nothing happens.
  1154. m.instance.__derive__(d.site)
  1155. if (m.instance[d.name] != before) {
  1156. if (!out.includes(d.name)) { out.push(d.name) again = true }
  1157. }
  1158. }
  1159. }
  1160. }
  1161. return out
  1162. }
  1163. // does this derivation read one of the members that moved — its own name aside?
  1164. feedsFrom(d, names) {
  1165. for (r of d.reads) {
  1166. if (r != d.name && names.includes(r)) { return true }
  1167. }
  1168. return false
  1169. }
  1170. // A WRITE SET, DERIVED AND THEN PAINTED: the one way in for every caller that knows
  1171. // which members moved. Painting is the last act, so a derived member's sites are
  1172. // drawn once, with its final value.
  1173. repaintAll(&m, names) {
  1174. if (names == null || names.length == 0) { return null }
  1175. for (n of moved(&m, names)) { repaint(&m, n) }
  1176. return null
  1177. }
  1178. // ---- repaint: a member was written; every site that reads it is redrawn ---------
  1179. write(&m, name, value) {
  1180. m.instance[name] = value
  1181. repaintAll(&m, [name])
  1182. settle()
  1183. return null
  1184. }
  1185. repaint(&m, name) {
  1186. let sites = m.sites.slice(0) // a paint may add sites; walk what was there
  1187. for (s of sites) {
  1188. if (s.names.includes(name)) { paint(&m, s, name) }
  1189. }
  1190. // DOWN THE BINDINGS: a child bound to this member at its reference site gets
  1191. // the value as its own member and repaints its own sites — the host knows the
  1192. // binding, the child knows nothing
  1193. for (b of m.bindings) {
  1194. if (b.name == name) {
  1195. let kid = m.kids[b.kid]
  1196. kid.instance[b.target] = bindingValue(b, &m)
  1197. // …and the CHILD's own derivations follow the member it was given
  1198. repaintAll(&kid, [b.target])
  1199. m.kids[b.kid] = kid
  1200. }
  1201. }
  1202. return null
  1203. }
  1204. // THE DIGEST IS GONE (mission 309). `refresh()` stood here: after every DOM event and
  1205. // every inbound frame it serialised every painted member of every mount to JSON and
  1206. // string-compared it against the last paint, to find out what a handler had written —
  1207. // a dirty-check over state, the thing FRAMEWORK_AUDIT §3 measured and named. What a
  1208. // handler writes is a fact about its syntax, so the compiler now says it (`hlTablesDef`
  1209. // in the component's own module) and the two callers that used to compare — a local
  1210. // event's listener and an inbound frame — repaint exactly the members it names. The
  1211. // `painted` map went with it: nothing compares any more.
  1212. // A SITE REPAINTS WHAT THE CHANGED MEMBER FEEDS, and nothing else (FRAMEWORK_AUDIT
  1213. // §5). `name` is the member that moved — the one `repaint` matched this site on.
  1214. // An element can read two members in two places (`div { class = tone count }`);
  1215. // writing `tone` used to set the attribute AND rewrite the element's whole text,
  1216. // which swaps the text node for an identical one: a second mutation record, a lost
  1217. // selection inside it, and work proportional to the site's content for a write that
  1218. // never touched it (measured 2026-09-14 on the probe: `tone = 'b'` → 2 records).
  1219. // A member read in BOTH places still repaints both — the two tests below are
  1220. // independent, and `names` carries the member once per place it is read.
  1221. // THE COMPONENT'S COMPILED PAINT (mission 313). The framework compiled this View's
  1222. // bound spots into statements when it produced the module (plugins/web/compile.hl):
  1223. // `p` writes what one member feeds, `r` rewrites a row's element where the DOM does not
  1224. // already hold the value. A component whose View binds nothing has no entry, and an
  1225. // element with no bound spot has none either — both are "nothing to paint".
  1226. compiledPaint(key, site) {
  1227. if (site == null) { return null }
  1228. let all = window.__hlPaint
  1229. if (all == null) { return null }
  1230. let t = all[key]
  1231. if (t == null) { return null }
  1232. return t[site]
  1233. }
  1234. paint(&m, s, name) {
  1235. // a head site writes the document's head, not an element of the page
  1236. if (s.head == true) { paintHead(&m, s.names[0]) return null }
  1237. if (s.region) {
  1238. // A `for` IS KEYED AND PATCHED (C3/C9): its rows are diffed by the record's id
  1239. // and nothing else on the host is touched. An `if` re-runs the list it stands
  1240. // in — the compile step registered that list's own walk under this site.
  1241. if (s.isFor) { paintFor(&m, s) return null }
  1242. rebuildIf(&m, s)
  1243. return null
  1244. }
  1245. // THE COMPILED STATEMENTS FOR THIS SITE: one per bound spot, with the read written
  1246. // in, under the member each reads. This is the walk the old hl:web did over the element's
  1247. // node — its attributes, then its text leaves — decided when the component was
  1248. // compiled instead of on every paint.
  1249. let comp = compiledOf(m.key)
  1250. if (comp == null) { return null }
  1251. let entry = comp.p[s.site]
  1252. if (entry == null) { return null }
  1253. let f = entry.p
  1254. if (f == null) { return null }
  1255. let el = s.el
  1256. let inst = m.instance
  1257. let rws = s.rows
  1258. f(&el, &inst, &rws, name)
  1259. return null
  1260. }
  1261. // AN `if` MOVED: the element it stands in is emptied and its child list walked again,
  1262. // which was the old hl:web's region rebuild — the host's own handlers stay, because the element
  1263. // is kept and only its children are built (the compiled list carries no `on` node).
  1264. rebuildIf(&m, s) {
  1265. let box = s.el.__hlIf == null ? null : s.el.__hlIf[s.site]
  1266. if (box != null) { rebuildBranch(&m, s, box) return null }
  1267. let walkList = walkOf(m.key, 'r', s.site)
  1268. if (walkList == null) { return null }
  1269. // A SHELL REGION THAT HOLDS THE SLOT: the page's DOM lives under the slot element,
  1270. // and the rebuild re-creates the element around it — so the slot is kept aside and
  1271. // the re-created one adopts the page's nodes (slotShell).
  1272. if (m.isShell && slotEl != null && slotEl != s.el && s.el.contains(slotEl)) { detachedSlot = slotEl }
  1273. s.el.replaceChildren()
  1274. run(walkList, &m, &m, s.el, 0, s.rows, s.rowKey != null ? s.rowKey : '', s.fill, true, null)
  1275. // THE SLOT WAS PUT ASIDE AND THE REBUILD NEVER REACHED ONE: the branch that now
  1276. // stands places no slot, so the page is off the document.
  1277. if (m.isShell && detachedSlot != null) {
  1278. detachedSlot = null
  1279. slotEl = null
  1280. pageShown = false
  1281. if (page != null) { page.sites = [] }
  1282. }
  1283. return null
  1284. }
  1285. // AN `if` MOVED: ITS BRANCH IS REPLACED, AND NOTHING ELSE (ticket #32, #41). The whole
  1286. // host used to be emptied and walked again, so every sibling of the `if` — an input
  1287. // being typed into, a `script { src }` (which then ran again), a custom element with
  1288. // state of its own — came back as a NEW node, and a write to the condition that did
  1289. // not even change it cost the input its focus. The branch that stands now is built
  1290. // into a fragment, what the walk registered on the fragment is re-homed on the host,
  1291. // and the fragment takes the old branch's place — in one DOM operation when the old
  1292. // branch had a first node, before the anchor when it was empty.
  1293. rebuildBranch(&m, s, box) {
  1294. let host = s.el
  1295. let inst = m.instance
  1296. let rws = s.rows
  1297. let f = s.read
  1298. let branch = f(&inst, &rws) ? walkOf(m.key, 't', s.site) : walkOf(m.key, 'e', s.site)
  1299. let old = []
  1300. for (n of box.nodes) { if (n.parentNode == host) { old.push(n) } }
  1301. // A SHELL BRANCH THAT HOLDS THE SLOT: the page's DOM lives under the slot element and
  1302. // is kept aside, for the rebuilt branch's slot to adopt (slotShell)
  1303. if (m.isShell && slotEl != null) {
  1304. for (n of old) { if (n == slotEl || (n.contains != null && n.contains(slotEl))) { detachedSlot = slotEl } }
  1305. }
  1306. let frag = document.createDocumentFragment()
  1307. let before = m.sites.length
  1308. run(branch, &m, &m, frag, 0, s.rows, s.rowKey != null ? s.rowKey : '', s.fill, true, null)
  1309. let fresh = Array.from(frag.childNodes)
  1310. // what the walk marked on the fragment belongs to the host
  1311. if (frag.__hlFor != null) {
  1312. if (host.__hlFor == null) { host.__hlFor = {} }
  1313. for (k of frag.__hlFor.keys()) { host.__hlFor[k] = frag.__hlFor[k] }
  1314. }
  1315. if (frag.__hlIf != null) {
  1316. for (k of frag.__hlIf.keys()) { host.__hlIf[k] = frag.__hlIf[k] }
  1317. }
  1318. // …and so do the regions it registered there; the old registrations of the same
  1319. // regions on the host are dropped, so each region is registered once
  1320. let again = []
  1321. let k = before
  1322. while (k < m.sites.length) {
  1323. let e = m.sites[k]
  1324. if (e.el == frag) {
  1325. e.el = host
  1326. m.sites[k] = e
  1327. if (e.region) { again.push(e.site) }
  1328. }
  1329. k = k + 1
  1330. }
  1331. if (again.length > 0) {
  1332. let keep = []
  1333. let idx = 0
  1334. for (e of m.sites) {
  1335. if (!(idx < before && e.region && e.el == host && again.includes(e.site))) { keep.push(e) }
  1336. idx = idx + 1
  1337. }
  1338. m.sites = keep
  1339. }
  1340. if (old.length > 0) {
  1341. old[0].replaceWith(frag)
  1342. let r = 1
  1343. while (r < old.length) { old[r].remove() r = r + 1 }
  1344. } else {
  1345. host.insertBefore(frag, box.anchor)
  1346. }
  1347. box.nodes = fresh
  1348. host.__hlIf[s.site] = box
  1349. // THE SLOT WAS PUT ASIDE AND THE BRANCH THAT NOW STANDS PLACES NONE: the page is off
  1350. // the document
  1351. if (m.isShell && detachedSlot != null) {
  1352. detachedSlot = null
  1353. slotEl = null
  1354. pageShown = false
  1355. if (page != null) { page.sites = [] }
  1356. }
  1357. return null
  1358. }
  1359. // the tree node of an element by its key path
  1360. // ---- create: DOM from a tree, for a route component that was not on the page ------
  1361. create(&m, host) {
  1362. let f = walkOf(m.key, 'v', null)
  1363. run(f, &m, &m, host, 0, {}, '', null, true, null)
  1364. return null
  1365. }
  1366. // the shell's View is a `body` element and the document already has one: the walk runs
  1367. // against document.body, and the element itself only takes the marks a region rebuild
  1368. // on it would need
  1369. markHost(&m, host, site) {
  1370. host.__hlSite = site
  1371. host.__hlKey = m.key
  1372. if (host.__hlRows == null) { host.__hlRows = {} }
  1373. return null
  1374. }
  1375. // ---- WHOSE FRAGMENT IS THIS? ------------------------------------------------------
  1376. // A fill carries the ADDRESS of the mount that wrote it — the kid keys from the page or
  1377. // the shell. While the walk is one composition deep the frame's `owner` IS that mount
  1378. // and is used directly, which costs nothing and cannot go stale. Deeper — a fill passed
  1379. // on through a second reference — it is not, and the mount is re-entered from the root
  1380. // by that address instead.
  1381. ownsFill(m, fill) {
  1382. if (m == null || fill == null) { return false }
  1383. if (fill.chain == null || m.chain == null) { return false }
  1384. if (fill.rootKind != m.rootKind) { return false }
  1385. if (fill.chain.length != m.chain.length) { return false }
  1386. let i = 0
  1387. while (i < fill.chain.length) {
  1388. if (fill.chain[i] != m.chain[i]) { return false }
  1389. i = i + 1
  1390. }
  1391. return true
  1392. }
  1393. // ---- settle: the fills a region rebuild left standing -----------------------------
  1394. // A region that rebuilt inside a composed child reached the child's `slot` and left
  1395. // an ANCHOR comment there: the fragment belongs to the HOST, and the frame doing the
  1396. // rebuild is the child's. The entry below names the host by ADDRESS — the kid keys
  1397. // from the page or the shell — because a mount record is a value and cannot be
  1398. // handed over. Draining happens after the whole walk has unwound and every mount has
  1399. // been written back, so re-entering the host from the root reaches the live record
  1400. // and the sites the fill registers survive.
  1401. settle() {
  1402. if (pendingFills.length == 0) { return null }
  1403. let due = pendingFills.slice(0)
  1404. pendingFills = []
  1405. for (f of due) { fillAt(f) }
  1406. return null
  1407. }
  1408. // ONE PENDING FILL, built from the mount its address names. Built DETACHED and moved
  1409. // in: the walk appends, and the fill's place is where the anchor stands, which may be
  1410. // in the middle of the child's own content.
  1411. fillAt(f) {
  1412. if (f.anchor.parentNode == null) { return null }
  1413. let box = document.createElement('div')
  1414. fillFromRoot(f, box, 0, true)
  1415. let moving = Array.from(box.childNodes)
  1416. for (node of moving) { f.anchor.parentNode.insertBefore(node, f.anchor) }
  1417. return null
  1418. }
  1419. // what a reference-site binding reads off the host instance: a member, or a field
  1420. // path starting at one
  1421. bindingValue(b, &m) {
  1422. if (b.ref != null) { return view.value(b.ref, m.instance, {}) }
  1423. return m.instance[b.name]
  1424. }
  1425. // ---- THE BOUNDARY, this side ----------------------------------------------------
  1426. // This file is the client realm's EDGE MODULE: it taps the realm broadcast bus,
  1427. // turns a crossing emit into the frame SPEC's wire protocol describes, and
  1428. // dispatches an inbound one at the mounted instances. The frames are the
  1429. // language's, not this framework's — what belongs to this file is only which
  1430. // carrier they ride.
  1431. connect() {
  1432. let proto = location.protocol == 'https:' ? 'wss:' : 'ws:'
  1433. socket = new WebSocket(proto + '//' + location.host + '/__hl/socket')
  1434. // HELLO: what this tab has mounted, for the server's fan-out; resent after every
  1435. // navigation. The session is not named here: the handshake carried the cookie.
  1436. socket.addEventListener('open', () => { socket.send(JSON.stringify({ t = 'hello'; i = 0; mounts = mountedKeys(); })) })
  1437. socket.addEventListener('message', (ev) => { frame(JSON.parse(ev.data)) })
  1438. socket.addEventListener('close', () => { socket = null })
  1439. return null
  1440. }
  1441. // every component key this tab has mounted: the shell, the page, their children
  1442. mountedKeys() {
  1443. let out = []
  1444. if (shell != null) { for (k of keysOf(&shell)) { if (!out.includes(k)) { out.push(k) } } }
  1445. if (page != null) { for (k of keysOf(&page)) { if (!out.includes(k)) { out.push(k) } } }
  1446. return out
  1447. }
  1448. // (built from return values: a list handed down as an argument is a COPY, and a
  1449. // push into it would be lost — the language's value semantics)
  1450. keysOf(&m) {
  1451. let out = [m.key]
  1452. for (k of m.kids.keys()) {
  1453. let kid = m.kids[k]
  1454. for (kk of keysOf(&kid)) { out.push(kk) }
  1455. }
  1456. return out
  1457. }
  1458. announceMounts() {
  1459. if (socket != null && socket.readyState == 1) { socket.send(JSON.stringify({ t = 'mounts'; i = 0; mounts = mountedKeys(); })) }
  1460. return null
  1461. }
  1462. // THE TAP. `emit server x(…)` anywhere in this realm — in a component's View
  1463. // handler, in this file — announces itself here as `hlRealm.peerEmit(transport,
  1464. // event, payload)`, and what this returns is what the emit ANSWERS: a
  1465. // value-form emit rides the ack (SPEC "An emit ANSWERS").
  1466. //
  1467. // The transport NAME is what an edge module is meant to filter on, and this one
  1468. // ignores it: a compiled module carries no manifest, so the language announces
  1469. // `undefined` here. Until the JS target compiles the project's transport table
  1470. // in (the creator's open question), the carrier is chosen by what is OPEN — the
  1471. // websocket while it is, the declared REST fallback while it is not.
  1472. on hlRealm.peerEmit(transport, event, payload) {
  1473. return crossing(event, payload)
  1474. }
  1475. crossing(event, payload) {
  1476. nextId = nextId + 1
  1477. let i = nextId
  1478. let text = JSON.stringify({ t = 'emit'; i = i; event = event; payload = payload; })
  1479. if (socket != null && socket.readyState == 1) {
  1480. // The ACK is what resolves this: the executor registers the pairing and
  1481. // puts the frame on the wire, and this line does not continue until the
  1482. // answer comes back on the same `i`.
  1483. return new Promise((res, rej) => {
  1484. pending['k' + i] = res
  1485. socket.send(text)
  1486. })
  1487. }
  1488. // The REST carrier — the same emit/ack pair over one POST, for a peer that
  1489. // has no socket yet. IT CARRIES THE OUTWARD EMITS BACK (creator, 2026-09-14):
  1490. // there is no connection for the far side to push to, so what a face raised
  1491. // for this caller rides in the ack as `emits`, and they are delivered HERE,
  1492. // before the value-form emit answers — the same order a socket sees (the
  1493. // frames first, the ack after). The far side's fan-out to the OTHER
  1494. // connections of this session is unaffected: they get theirs over their own
  1495. // sockets.
  1496. let res = fetch('/__hl/emit', { method = 'POST' body = text headers = { 'Content-Type' = 'application/json' } })
  1497. if (!res.ok) { console.error('framework: the boundary is unreachable over REST', res.status) return null }
  1498. let ack = res.json()
  1499. if (ack.emits != null) { for (f of ack.emits) { outward(f) } }
  1500. return answerOf(ack)
  1501. }
  1502. // `ok` is the whole truth: a false ack is the far realm saying it did not
  1503. // handle the emit, and its message is the located one — the event, the realm
  1504. // and the fact that no class there declares a handler for it.
  1505. answerOf(ack) {
  1506. if (!ack.ok) {
  1507. console.error('framework: the boundary refused an emit — ' + ack.error)
  1508. return null
  1509. }
  1510. return ack.value
  1511. }
  1512. frame(f) {
  1513. if (f.t == 'ack') {
  1514. let key = 'k' + f.i
  1515. let res = pending[key]
  1516. pending[key] = null
  1517. if (res != null) { res(answerOf(f)) }
  1518. return null
  1519. }
  1520. if (f.t == 'emit') { return outward(f) }
  1521. if (f.t == 'pong') { return null }
  1522. // `build`: the server re-analysed after a save (its dev watcher) — this page was
  1523. // rendered under the old analysis, so it reloads and comes back under the new one
  1524. if (f.t == 'build') { location.reload() return null }
  1525. console.warn('framework: unknown frame kind', f.t)
  1526. return null
  1527. }
  1528. // AN EMIT FROM THE OTHER REALM, dispatched at the MOUNTED INSTANCES: the event
  1529. // name is data, so it goes through the language's boundary dispatch at each
  1530. // instance, and then what a handler wrote is repainted. A handler that writes a
  1531. // member is the whole point of the direction — the server says something
  1532. // happened and the page shows it.
  1533. outward(f) {
  1534. if (mounting) { held.push(f) return null }
  1535. // BY REFERENCE, or the handler writes a copy: a call argument is a VALUE,
  1536. // and an instance is copied like any other. `&` suppresses that — through a
  1537. // local, because the ampersand reaches a bare name and not a field path.
  1538. if (shell != null) { deliver(&shell, f) }
  1539. if (page != null) { deliver(&page, f) }
  1540. settle()
  1541. return null
  1542. }
  1543. // at a mount and every mount composed into it: whichever declares the listener runs
  1544. // it, and the members THAT mount's declared handler for this event writes are
  1545. // repainted. The frame names the event; the module's table names what a handler for
  1546. // that event writes (mission 309), so nothing is compared afterwards either.
  1547. deliver(&m, f) {
  1548. let inst = m.instance
  1549. hlEmitArgs(&inst, f.event, f.payload)
  1550. repaintAll(&m, eventWrites(m.key, f.event))
  1551. for (k of m.kids.keys()) {
  1552. let kid = m.kids[k]
  1553. deliver(&kid, f)
  1554. m.kids[k] = kid
  1555. }
  1556. return null
  1557. }
  1558. // ---- routing: a same-origin link is a navigation, the shell stays -----------------
  1559. listen() {
  1560. document.addEventListener('click', (ev) => { onClick(ev) })
  1561. window.addEventListener('popstate', () => { show(location.pathname, false) })
  1562. return null
  1563. }
  1564. // THE CLICK IS CLAIMED INSIDE THE DISPATCH. Every CALL in this language is
  1565. // awaited, and an awaited `preventDefault` runs a microtask after the listener
  1566. // returned — after the browser has already carried out the link's activation
  1567. // when the click was DISPATCHED FROM SCRIPT (`a.click()`), which performs it
  1568. // synchronously at the end of dispatch. So the page reloaded instead of
  1569. // navigating: the whole document was replaced under the framework, the fresh one
  1570. // was still parsing, nothing was hydrated, and the next click landed on an
  1571. // element that had no handler yet (measured 2026-09-13 on the social app — the
  1572. // "first click after a navigation does nothing" report; a REAL click happened to
  1573. // work because Chrome performs a trusted click's navigation in a later task).
  1574. //
  1575. // Only a READ of a property is free of that (it compiles to no call), so the
  1576. // decision to claim the click is made out of properties alone — the anchor is
  1577. // found by walking up `parentElement`, and its `origin`, `pathname` and `search`
  1578. // are the anchor's own URL pieces. `preventDefault` is then the FIRST call this
  1579. // handler makes, and lands while the event is still being dispatched.
  1580. onClick(ev) {
  1581. // what this framework never claims: a click already handled, a middle or right
  1582. // button, and a modified click — the browser's own tab, window and download
  1583. if (ev.defaultPrevented || ev.button != 0 || ev.metaKey || ev.ctrlKey || ev.shiftKey || ev.altKey) { return null }
  1584. let a = ev.target
  1585. while (a != null && a.tagName != 'A') { a = a.parentElement }
  1586. if (a == null || a.href == '' || a.target != '' || a.download != '') { return null }
  1587. if (a.origin != location.origin) { return null }
  1588. ev.preventDefault()
  1589. // only a PAGE of this app is a client-side navigation; anything else — an
  1590. // asset, a function route, an unknown path — is the browser's own request,
  1591. // made here because the default was already taken away
  1592. let m = router.match(a.pathname)
  1593. if (m == null || m.kind != 'component') { location.href = a.href return null }
  1594. goTo(a.pathname + a.search)
  1595. return null
  1596. }
  1597. // THE NAVIGATION ITSELF, whoever asked for it: the link interception above, or an
  1598. // app's own code through the package's `navigate` static at the top of this file.
  1599. // It is NOT named `navigate` because a file's statics are copied onto its
  1600. // instances, so a static and a method of the same name would be one name with two
  1601. // meanings and the instance's would be the static's (measured 2026-09-13).
  1602. goTo(path) {
  1603. history.pushState({}, '', path)
  1604. show(path, true)
  1605. return null
  1606. }
  1607. // THE NEXT ROUTE'S TREE AND STATE, over the boundary: `emit server page(path)`
  1608. // is a value-form crossing emit, so the answer rides the ack back into this
  1609. // assignment — the same carrier every other emit uses, chosen by what is open.
  1610. // The shell stays if it is the same one.
  1611. show(path, pushed) {
  1612. let next = emit server page(path)
  1613. if (next == null) { location.href = path return null }
  1614. if (seed.shell == null || next.shell == null || next.shell.key != seed.shell.key) {
  1615. location.href = path // another shell: a full document
  1616. return null
  1617. }
  1618. takeBlueprints(next.blueprints)
  1619. seed = next
  1620. mounting = true
  1621. page = instantiate(next.page, [], 'page')
  1622. // the shell may be showing something else: the next page has nowhere to stand and
  1623. // waits, exactly as one that booted into a closed region does
  1624. if (slotEl != null) {
  1625. slotEl.replaceChildren()
  1626. create(&page, slotEl)
  1627. pageShown = true
  1628. } else {
  1629. pageShown = false
  1630. }
  1631. // the next page's head members are the next mount's sites; the document was not
  1632. // replaced, so they are painted once here the way the server's `document()` would
  1633. headSites(&page)
  1634. wearHead(&page)
  1635. release()
  1636. announceMounts()
  1637. return null
  1638. }
  1639. // ---- THE HEAD IS STATE (creator, 2026-09-13) -------------------------------------
  1640. // `__title`, `__description`, `__image`, `__favicon` and `__meta` are members of the
  1641. // page like any other, so they are SITES of the page mount, repainted by `paint()`
  1642. // below whenever a handler's write set names one. A client handler's write, an inbound
  1643. // push and a navigation therefore all reach the head on the ONE path every other
  1644. // member takes; nothing here polls and nothing compares.
  1645. headMembers() { return ['__title' '__description' '__image' '__favicon' '__meta'] }
  1646. // the page mount's head sites. `el` is null and `node` is null: the element these
  1647. // write is the document's head, found per member when it is painted.
  1648. headSites(&m) {
  1649. for (name of headMembers()) {
  1650. m.sites.push({ el = null; node = null; names = [name]; region = false; head = true; rows = {}; })
  1651. }
  1652. return null
  1653. }
  1654. // every head member at once: a navigation's new mount, where each one may have moved
  1655. wearHead(&m) {
  1656. for (name of headMembers()) { paintHead(&m, name) }
  1657. return null
  1658. }
  1659. // THE VALUE A HEAD MEMBER SHOWS: the page's own when it declares one, the app's
  1660. // manifest default otherwise (`appHead`, off the seed) — the same rule the server's
  1661. // `headOf` applies, so a page that clears its title gets the app's back and never
  1662. // the previous page's.
  1663. headValue(&m, name, fallback) {
  1664. if (m.instance[name] != null) { return m.instance[name] }
  1665. return fallback
  1666. }
  1667. // ONE HEAD MEMBER, into the document. The element is reused when the first document
  1668. // carried it and created when it did not (a page that declares an image where the
  1669. // app declared none).
  1670. paintHead(&m, name) {
  1671. if (name == '__title') {
  1672. let v = headValue(&m, '__title', appHead != null ? appHead.title : null)
  1673. if (v != null) { document.title = v }
  1674. headMeta('property', 'og:title', v)
  1675. } else if (name == '__description') {
  1676. let v = headValue(&m, '__description', appHead != null ? appHead.description : null)
  1677. headMeta('name', 'description', v)
  1678. headMeta('property', 'og:description', v)
  1679. } else if (name == '__image') {
  1680. headMeta('property', 'og:image', headValue(&m, '__image', appHead != null ? appHead.image : null))
  1681. } else if (name == '__favicon') {
  1682. // AT ONCE, because apps swap the icon as an indicator: a new href on the
  1683. // existing link is what the browser redraws the tab from
  1684. let v = headValue(&m, '__favicon', appHead != null ? appHead.favicon : null)
  1685. if (v != null) {
  1686. let el = document.querySelector('link[rel="icon"]')
  1687. if (el == null) {
  1688. el = document.createElement('link')
  1689. el.setAttribute('rel', 'icon')
  1690. document.head.appendChild(el)
  1691. }
  1692. el.setAttribute('href', v)
  1693. }
  1694. } else if (name == '__meta') {
  1695. // A GROUP, not a value: the page's list replaces the page's tags and leaves the
  1696. // app's manifest tags alone. The server marks the ones it wrote for the page
  1697. // with `data-hl-page-meta` and this writes the mark too, so the two halves
  1698. // address the same set.
  1699. let old = document.querySelectorAll('meta[data-hl-page-meta]')
  1700. let i = 0
  1701. while (i < old.length) { old.item(i).remove() i = i + 1 }
  1702. let list = m.instance['__meta']
  1703. if (list != null) {
  1704. for (h of list) {
  1705. let el = document.createElement('meta')
  1706. for (k of h.keys()) { el.setAttribute(k, '' + h[k]) }
  1707. el.setAttribute('data-hl-page-meta', '')
  1708. document.head.appendChild(el)
  1709. }
  1710. }
  1711. }
  1712. return null
  1713. }
  1714. // one meta tag's content. The tag the first document carried is reused; a value
  1715. // this document has no tag for yet gets one appended.
  1716. headMeta(attr, name, content) {
  1717. if (content == null) { return null }
  1718. let el = document.querySelector('meta[' + attr + '="' + name + '"]')
  1719. if (el == null) {
  1720. el = document.createElement('meta')
  1721. el.setAttribute(attr, name)
  1722. document.head.appendChild(el)
  1723. }
  1724. el.setAttribute('content', content)
  1725. return null
  1726. }

Branches

Latest commits

  • 4a2d7125initial commitmre