Konubinix' opinionated web of thoughts

Shared Blocks

Shared noweb blocks for braindump and blog notes. Consumer files use cross-file references like <<shared_blocks.org:deploy_iframe-ex()>>.

Deploy iframe

Embeds the deployed app in an iframe on the braindump/blog page. The caller sets the shell variable deploy_url to the path prefix (e.g. /tally/ or /p/bafyxyz) before expanding the block.

Deploy

Publishes the calling file’s [[root]] block to IPFS via ipfa, then embeds it in an iframe. Use with #+call: shared_blocks.org:deploy().

PWA meta tags

Generic PWA meta tags: charset, purge escape hatch, viewport, and manifest link. Callers add their own <title> and <meta name“theme-color”>=.

Service worker (cache-first + write-through)

Cache strategy that lets a returning visitor open the app without re-downloading assets, and keeps it usable offline. The handlers assume two consumer-defined constants:

  • CACHES (array of {name, pattern?}) — one or more cache storage buckets, evaluated in order: the first whose pattern matches the request URL wins. The last entry should omit pattern — it is the default that catches everything else. Splitting into multiple buckets lets the consumer invalidate them independently: bump the app cache on every deploy via a source-hash, and the vendor cache only when dependency versions change. With a single entry the SW behaves like a one-cache setup.
  • ASSETS (array of URLs) — the shell precached at install into the default (last) cache. Keep it minimal: anything the app fetches at runtime is cached on the way through (write-through on the first miss), so precaching only matters for offline cold-start.

Three event listeners:

  • install opens the default cache, precaches ASSETS, then skipWaiting() so the new SW activates immediately rather than waiting for every tab to close.
  • activate drops any cache whose name is not in CACHES and calls clients.claim() so the SW takes control on first install instead of waiting for the next navigation. Bumping a cache name in CACHES triggers eviction of the old one on next activate.
  • fetch splits two regimes. Navigations (HTML) are network-first with cache fallback: a live visit gets the latest HTML, an offline visit gets the cached shell. Everything else (JS, WASM, images, …) is cache-first with write-through, scoped to the cache picked by the consumer’s pattern: the first miss fills the bucket, the next request short-circuits the network. Offline use after the first online visit just works.

Consumers tangle a sw.js block that declares CACHES and ASSETS then noweb-includes sw-handlers-ex, and pair it with sw-register-ex in the HTML <head> to register the worker. The register block skips localhost and 127.0.0.1 because in dev/test rebuilds happen often and a stale cache on bundle.js or a WASM blob would mask any change ; the ?purge escape hatch from PWA meta tags remains the manual override.

function cacheNameFor(request){
    const url = new URL(request.url);
    for(const c of CACHES){
        if(!c.pattern || c.pattern.test(url.pathname)) return c.name;
    }
    return CACHES[CACHES.length - 1].name;
}

self.addEventListener('install', e => {
    const defaultCache = CACHES[CACHES.length - 1].name;
    e.waitUntil(caches.open(defaultCache).then(c => c.addAll(ASSETS)));
    self.skipWaiting();
});

self.addEventListener('activate', e => {
    const valid = new Set(CACHES.map(c => c.name));
    e.waitUntil(caches.keys().then(keys =>
        Promise.all(keys.filter(k => !valid.has(k)).map(k => caches.delete(k)))
    ));
    self.clients.claim();
});

self.addEventListener('fetch', e => {
    const name = cacheNameFor(e.request);
    if(e.request.mode === 'navigate'){
        e.respondWith(
            fetch(e.request)
                .then(r => caches.open(name).then(c => { c.put(e.request, r.clone()); return r; }))
                .catch(() => caches.match(e.request))
        );
    } else {
        e.respondWith(caches.open(name).then(c =>
            c.match(e.request).then(cached => {
                if(cached) return cached;
                return fetch(e.request).then(net => {
                    if(net.ok) c.put(e.request, net.clone());
                    return net;
                });
            })
        ));
    }
});

<script>
  if('serviceWorker' in navigator
     && location.hostname !== 'localhost'
     && location.hostname !== '127.0.0.1'){
      navigator.serviceWorker.register('sw.js', {updateViaCache: 'none'});
  }
</script>

Sync infrastructure

Shared Yjs WebSocket connection, remote-change rendering, and init logic. Each app calls setupSync(persistence, ydoc, onReady) where onReady is the app-specific first-render function.

// ── Shared sync infrastructure ──
var wsProvider;
var _rendering = false;
var _scheduleRender;

function setupSync(persistence, ydoc, onReady, scheduleRenderFn) {
    _scheduleRender = scheduleRenderFn || function() {
        if (_rendering) return;
        _rendering = true;
        requestAnimationFrame(function() { _rendering = false; onReady(); });
    };

    // Re-render on any non-local Yjs update (including IndexedDB sync)
    ydoc.on('update', function(update, origin) {
        if (origin === null) return;
        _scheduleRender();
    });

    // WebSocket — connect only after IndexedDB synced, retry on denial
    var _wsRetryTimer = null;
    function connectWs() {
        try {
            if (wsProvider) return;
            var params = new URLSearchParams(location.search);
            var paramWsUrl = params.get('websocket_url');
            if (paramWsUrl) {
                localStorage.setItem('websocket_url', paramWsUrl);
                var cleanUrl = new URL(location);
                cleanUrl.searchParams.delete('websocket_url');
                history.replaceState(null, '', cleanUrl);
            }
            var wsUrl = paramWsUrl || localStorage.getItem('websocket_url');
            if (!wsUrl) return;
            var wsEverConnected = false;
            wsProvider = new WebsocketProvider(wsUrl, _roomName, ydoc);
            var syncBtn = document.getElementById('syncDot');
            wsProvider.on('status', function(e) {
                if (e.status === 'connected') {
                    wsEverConnected = true;
                    if (_wsRetryTimer) { clearInterval(_wsRetryTimer); _wsRetryTimer = null; }
                }
                var state = e.status === 'connected' ? 'connected' : 'offline';
                document.body.setAttribute('data-ws', state);
                if (syncBtn) syncBtn.title = state === 'connected' ? 'Synced' : 'Offline';
            });
            wsProvider.on('connection-close', function(e) {
                if (!wsEverConnected) {
                    document.body.setAttribute('data-ws', 'denied');
                    if (syncBtn) syncBtn.title = 'Not authorized';
                    wsProvider.destroy();
                    wsProvider = null;
                    if (!_wsRetryTimer) {
                        _wsRetryTimer = setInterval(connectWs, 10000);
                    }
                }
            });
        } catch(e) { console.warn('WebSocket setup failed:', e); }
    }

    function init() {
        onReady();
        document.body.setAttribute('data-ready', 'true');
    }

    persistence.once('synced', connectWs);
    persistence.once('synced', function() { setTimeout(init, 0); });
    if (persistence.synced) { connectWs(); setTimeout(init, 0); }

    if ('serviceWorker' in navigator) navigator.serviceWorker.register('sw.js', { updateViaCache: 'none' });
}

Reorder engine (drag-and-drop)

Framework-agnostic pointer-based drag-and-drop for reorderable lists. Each .reorder-list[data-reorder“key”]= is a list of .reorder-item[data-idx] items with a .reorder-grip handle. The engine handles the drag gesture, floating clone, placeholder marking, and edge-of-viewport auto-scroll. Slot detection is 2D — the cursor’s (clientX, clientY) must fall inside an item’s bounding box — so the same engine drives a single column or a wrapping flex grid (intra-row drags pick the right column by X, not just any item on the same Y). It does not touch the consumer’s state: when the cursor crosses into a new slot it fires a reorder:move CustomEvent on the list with detail = {from, to}. The consumer listens and mutates its own store (Alpine, Vue, vanilla). Because that re-render is asynchronous, the engine identifies the dragged element by the reference captured at pointerdown rather than by DOM index, so the placeholder slot stays on it regardless of where the re-render lands it in the list.

Decoupled like that, the same engine drives the player reorder in score_counter_tally (Alpine) and the ballot ranking in the Condorcet app (petite-vue), without either framework leaking into the other.

Why custom (vs SortableJS, vuedraggable, pragmatic-drag-and-drop): those libs rely on the browser’s native HTML5 drag, which is not reliably reproducible by Playwright/CDP synthesis1 — the browser only enters “drag mode” for real human pointer input. Observed directly in the Condorcet app under SortableJS: a human drag reproduced a DOM desync2 (stale <li> left behind after submit) while page.mouse.* and Input.dispatchDragEvent3 never triggered it. Playwright maintainers have themselves closed similar dragTo-vs-sortable bugs as “not planned”4. The community-recommended workaround (double-hover5 or manual DataTransfer dispatch) is fragile with libs that run their own state machines. Atlassian’s pragmatic-drag-and-drop6 is the cleanest “proper” option but still rides on HTML5 native drag under the hood7, so the test-synthesis gap persists. Pointer events, by contrast, take the same path in tests and in prod; no gap8. The consumer keeps full control of state (no DOM mutation by the lib), so Vue/Alpine reactivity stays the single source of truth.

Build hash

Short SHA-256 tag for the caller’s [[root]] block, shown in the UI so you can tell which build is loaded on a given device (PWAs cache aggressively). Each consumer defines a one-line build-hash delegator whose body noweb-splices build-hash-body below; the elisp then runs in the consumer’s buffer and finds its local root. The raw block source contains a constant noweb marker, so the hash is stable without any self-reference stripping.

By default the hash covers a single block named root. When the build pulls from several blocks (HTML root + service worker + manifest, say), a consumer can wrap the splice in a let that binds build-hash-blocks to a list of block names, and the bodies are concatenated before hashing.

(let ((build-hash-blocks '("root" "manifest" "sw")))
  [[shared_blocks.org:build-hash-body-ex()]])

Notes linking here


  1. “Browser switches from mousemove to drag events during operation […] custom implementations continue to emit mousemove events throughout the operation.” — Playwright PR #34882 (fix(drag-n-drop): send two mousemove events to target to make Angular CDK work), closed in favour of a steps parameter. ↩︎

  2. “we have two sources of truth: Vue and SortableJS […] unpredictable behaviour.” — VueUse #3727 (useSortable clashes with Vue reactivity). The proposed workaround (bumping a key) proved insufficient in practice: tried in the Condorcet app, the bug still reproduced. ↩︎

  3. Input.dispatchDragEvent is marked Experimental in the CDP spec and only fires drag events after Input.setInterceptDrags enables interception — it injects drag events into the page without putting the browser into its real drag mode. — CDP Input.dispatchDragEvent↩︎

  4. “The dragTo action doesn’t work [with] html5sortable [on] Chromium.” — Playwright #38796, regression 1.56 → 1.57, closed not planned↩︎

  5. “If your page relies on the dragover event being dispatched, you need at least two mouse moves to trigger it in all browsers.” — Playwright docs, Actions / Input↩︎

  6. “Powering some of the biggest products on the web, including Trello, Jira and Confluence […] enables safe and successful usage of the browsers built in drag and drop functionality.” — atlassian/pragmatic-drag-and-drop↩︎

  7. “leverages the native HTML Drag and Drop API to create seamless drag-and-drop experiences across any tech stack.” — LogRocket, Implement the Pragmatic drag and drop library. Same dragstart / dragover / drop path as raw HTML5 drag, so the same CDP-synthesis gap applies. ↩︎

  8. Playwright’s page.mouse.* routes to CDP Input.dispatchMouseEvent; the browser then fires pointerdown / pointermove / pointerup on the same DOM path as for OS-level input (every mouse interaction has an associated pointer event per the spec). No “drag mode” gate, no trusted-event gate. — CDP Input.dispatchMouseEvent, W3C Pointer Events 3.

    .reorder-list{display:flex;flex-direction:column;gap:6px;margin-bottom:8px;position:relative}
    .reorder-item{
        display:flex;align-items:center;gap:10px;
        padding:10px 14px;background:rgba(255,255,255,.05);border-radius:12px;
        user-select:none;
    }
    .reorder-item.drag-placeholder{
        opacity:.3;border:2px dashed rgba(255,255,255,.25);background:transparent;
    }
    .drag-clone{
        position:fixed;z-index:9999;pointer-events:none;
        display:flex;align-items:center;gap:10px;
        padding:10px 14px;border-radius:12px;
        background:var(--accent);color:#fff;
        box-shadow:0 8px 24px rgba(0,0,0,.4);
        transform:scale(1.04);opacity:.95;
        font-family:inherit;font-size:inherit;
    }
    .drag-clone .reorder-grip{color:#fff}
    .reorder-grip{
        display:grid;place-items:center;width:36px;height:36px;
        cursor:grab;touch-action:none;color:var(--muted);font-size:1.2rem;
        flex-shrink:0;border-radius:8px;
    }
    .reorder-grip:active{cursor:grabbing;background:rgba(255,255,255,.1)}
    .reorder-rank{font-weight:800;font-size:1.1rem;min-width:1.4em;text-align:center}
    .reorder-name{flex:1;font-weight:600}
    

    When the list overflows the viewport, the user would otherwise have to release the drag, scroll, and re-grip. The engine watches the cursor: once it enters an 80-pixel margin at the top or bottom edge, window.scrollBy runs every animation frame at depth/6 pixels (so up to about 13 px/frame at the very edge), until the cursor leaves the margin or the drag ends. The cursor’s last known viewport position is re-applied each tick so the floating clone and the slot detection stay in sync as the page scrolls underneath.

    // ── Drag-and-drop reorder engine (framework-agnostic) ──
    // Binds once on document. Handles any =.reorder-list= with =.reorder-item=
    // children and a =.reorder-grip= handle on each item. Fires
    // =reorder:move= on the list (bubbling CustomEvent with
    // =detail = {from, to}=) when the cursor crosses into a new slot during
    // drag. Consumers listen and update their own state; the engine never
    // mutates the consumer's store.
    (function initReorderEngine() {
        var dragging = null;
    
        function fireMove(list, fromIdx, toIdx) {
            list.dispatchEvent(new CustomEvent('reorder:move', {
                detail: { from: fromIdx, to: toIdx },
                bubbles: true,
            }));
        }
    
        function markPlaceholder() {
            var items = dragging.list.querySelectorAll('.reorder-item');
            items.forEach(function(el) {
                el.classList.toggle('drag-placeholder', el === dragging.itemEl);
            });
        }
    
        function stripFrameworkAttrs(root) {
            root.querySelectorAll('*').forEach(function(el) {
                Array.from(el.attributes).forEach(function(a) {
                    if (a.name.indexOf('x-') === 0 ||
                        a.name.indexOf('v-') === 0 ||
                        a.name.indexOf(':') === 0 ||
                        a.name.indexOf('@') === 0) {
                        el.removeAttribute(a.name);
                    }
                });
            });
        }
    
        function onStart(e) {
            var grip = e.target.closest('.reorder-grip');
            if (!grip) return;
            var list = grip.closest('.reorder-list');
            var item = grip.closest('.reorder-item');
            if (!list || !item) return;
            var idx = parseInt(item.dataset.idx, 10);
            var rect = item.getBoundingClientRect();
            var clientX = e.clientX;
            var clientY = e.clientY;
    
            var clone = document.createElement('div');
            clone.className = 'drag-clone';
            clone.style.width = rect.width + 'px';
            clone.innerHTML = item.innerHTML;
            stripFrameworkAttrs(clone);
            clone.style.left = rect.left + 'px';
            clone.style.top = rect.top + 'px';
            document.body.appendChild(clone);
    
            dragging = {
                list: list, curIdx: idx, itemEl: item, clone: clone,
                offsetX: clientX - rect.left, offsetY: clientY - rect.top,
                clientX: clientX, clientY: clientY, scrollRaf: 0,
            };
            markPlaceholder();
            e.preventDefault();
        }
    
        function applyPointer(clientX, clientY) {
            dragging.clone.style.left = (clientX - dragging.offsetX) + 'px';
            dragging.clone.style.top = (clientY - dragging.offsetY) + 'px';
            var items = dragging.list.querySelectorAll('.reorder-item');
            for (var i = 0; i < items.length; i++) {
                if (i === dragging.curIdx) continue;
                var rect = items[i].getBoundingClientRect();
                if (clientX >= rect.left && clientX <= rect.right &&
                    clientY >= rect.top  && clientY <= rect.bottom) {
                    fireMove(dragging.list, dragging.curIdx, i);
                    dragging.curIdx = i;
                    markPlaceholder();
                    var rank = dragging.clone.querySelector('.reorder-rank');
                    if (rank) rank.textContent = i + 1;
                    break;
                }
            }
        }
    
        function autoScrollTick() {
            if (!dragging) return;
            var margin = 80;
            var y = dragging.clientY;
            var dy = 0;
            if (y < margin) dy = -Math.ceil((margin - y) / 6);
            else if (y > window.innerHeight - margin)
                dy = Math.ceil((y - (window.innerHeight - margin)) / 6);
            if (dy) {
                window.scrollBy(0, dy);
                applyPointer(dragging.clientX, y);
            }
            dragging.scrollRaf = requestAnimationFrame(autoScrollTick);
        }
    
        function onMove(e) {
            if (!dragging) return;
            e.preventDefault();
            dragging.clientX = e.clientX;
            dragging.clientY = e.clientY;
            applyPointer(e.clientX, e.clientY);
            if (!dragging.scrollRaf)
                dragging.scrollRaf = requestAnimationFrame(autoScrollTick);
        }
    
        function onEnd() {
            if (!dragging) return;
            if (dragging.scrollRaf) cancelAnimationFrame(dragging.scrollRaf);
            dragging.clone.remove();
            dragging.list.querySelectorAll('.drag-placeholder').forEach(function(el) {
                el.classList.remove('drag-placeholder');
            });
            dragging = null;
        }
    
        document.addEventListener('pointerdown', function(e) {
            if (e.target.closest('.reorder-grip')) onStart(e);
        });
        document.addEventListener('pointermove', onMove);
        document.addEventListener('pointerup', onEnd);
        document.addEventListener('pointercancel', onEnd);
    })();
    
     ↩︎