diff --git a/client/helpers.js b/client/helpers.js index 17a4cfb..6ac214a 100644 --- a/client/helpers.js +++ b/client/helpers.js @@ -1,26 +1,86 @@ +/* + A bunch of handy helper functions and classes. + */ import { log } from "./logging.js"; -/* A simple function that uses the Crypto.getRandomValues to fill an - * array with 32 bit random numbers. +/* + A simple function that uses the Crypto.getRandomValues to fill an + array with 32 bit random numbers. */ export const random_numbers = (count) => { let out = new Uint32Array(count); return window.crypto.getRandomValues(out); } -/* The stupid web crypto api doesn't have the randomInt function that the - * node crypto library has so I have to hack in this complete BS. I really - * hate browser manufacturers. +/* + The stupid web crypto api doesn't have the randomInt function that the + node crypto library has so I have to hack in this complete BS. I really + hate browser manufacturers. + + ___FOOTGUN___: This isn't cryptographically secure. It's mostly just used + for generating random CSS ids and ___not___ for anything secure. */ export const rand_int = (min, max) => { return Math.floor((Math.random() * (max - min)) + min); } -/** - * Partially taken from https://lea.verou.me/2016/12/resolve-promises-externally-with-this-one-weird-trick/ on - * how to externally resolve a promise. You can use this in the very common situation in svelte where you need - * to await on some condition, but you can't create the promise until the onMount. Instead start it off with defer() - * then resolve it when you're ready. +/* + Partially taken from [a blog by Lea Verou](https://lea.verou.me/2016/12/resolve-promises-externally-with-this-one-weird-trick/) + on how to externally resolve a promise. You can use this in the very common + situation in svelte where you need to await on some condition, but you can't + create the promise until the onMount. Instead start it off with defer() then + resolve it when you're ready. + + The main reason I use this is to block some processing until a later event happens, + but that later thing happens randomly or in some other part of the code. Another + great place for `defer()` is when you have an old callback style system and you + need to wait for it in a separate `async/await` function. + + For example, if you want to wait on a resource to be available in the browser, then + you can create a `defer()`, grab the resource, and anything that needs this resource + can wait on the `defer()` to know it's ready. As an example, pretend the `setTimeout` + in this code is a video downloading: + + ```javascript + const video_promise = defer("blocker"); + + // pretend this is loading a video + setTimeout(() => blocker.resolve(), 3000); + + const play_video => async() => { + await video_promise; + // do some stuff after the video is ready + } + + onMount(() => await play_video()); + ``` + + In this code we don't want to play the video until it's totally ready (which + we're pretending to do with a `setTimeout`). Rather than some crazy await + callback setup we just have a single `video_promise`, wait on it in `play_video`, + and whatever does the video loding resolves it. + + ### Rejection + + When you call `reject` on the `defer` it will trigger an exception just like with + a `Promise.reject`. So the above code can be: + + ```javascript + const play_video => async() => { + try { + await video_promise; + // do some stuff after the video is ready + } catch(error) { + console.error(error); + } + } + ``` + + If the video loading code has a problem it can _signal this_ by rejecting the + `defer`. That will trigger error handling with simple `try/catch` semantics. + + + `debug string` -- A debug message to print out whenever this is resolved or rejected. + + ___return___ `Promise` -- A normal `Promise`, but now it's `reject` and `resolve` callbacks are attached as methods you can call. */ export const defer = (debug="") => { let res; @@ -49,16 +109,86 @@ export const defer = (debug="") => { return promise; } + +/* + This implements a very simple Mutex style blocker for coordinating multiple + async actors waiting on a single resource. The best example is in the + `client/components/Source.svelte` where a `Mutex` is used so that mutliple + pages are blocked until an external slow JavaScript source is loaded. + + This hardly never comes up in JavaScript, but when it does you really need + something like this. In the `Source.svelte` code we want to load an external + `.js` code, but multiple pages might try to load it at the same time. The + solution is to: + + 1. Keep track of what sources have already loaded. + 2. Use a single `Mutex` all `Source.svelte` objects use in a `context="module"`. + 3. When `Source.svelte` runs it checks to see if the code is already loaded after calling `lock.hold()`. This makes sure that it's waited its turn to try to load the source. + 4. If it is ready then it immediately releases the lock. + 5. If it hasn't been loaded yet then it injects the ` + +
+ {#if visible} + Shown! + {:else} + Hidden! + {/if} +
+ ``` + + In this example (from `client/components/IsVisible.svelte`) you simply have + some callbacks that change a variable and Svelte then displays the correct + text. It's also used in `client/components/SnapImage.svelte`. + */ export const visibility = (node, opts) => { let options = opts || {}; // NOTE: does JS default values have the ruby problem? const callback = (entries) => { @@ -137,6 +317,13 @@ export const visibility = (node, opts) => { return create_observer_cleanup(vis_observer, node, options); } +/* + Similar to `visibility()` it's a Svelte `use:` function that adds + resize detection to a node. Not really used in my code, but this + comes up whenever you _must_ maintain an aspect ratio and CSS simply + can't pull it off. It will be slow though, and use a lot of CPU, but + sometimes that's all you can do. + */ export const resize = (node, options) => { const callback = (entries) => { entries.forEach(entry => {