/ *
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 .
* /
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 .
_ _ _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 [ 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 ;
let rej ;
let promise = new Promise ( ( resolve , reject ) => {
if ( debug ) {
res = ( result ) => {
log . debug ( "resolved defer" , debug ) ;
resolve ( result ) ;
}
rej = ( error ) => {
log . debug ( "REJECT defer" , debug ) ;
reject ( error ) ;
}
} else {
res = resolve ;
rej = reject ;
}
} ) ;
promise . resolve = res ;
promise . reject = rej ;
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 ` <script> ` tags and creates an ` onLoad ` handler to wait for the script to load .
6. The ` onLoad ` handler then calls ` lock.release() ` which frees up all of the other pages waiting .
7. All of the other pages then exit their ` lock.hold() ` , see that this source is open , and do their own ` lock.release() ` and move on .
8. The final trick is a call ` lock.wait() ` which just does a quick ` lock.hold()/lock.release() ` combo . This makes the JavaScript engine "bounce" to another waiting caller in the queue .
As I said , this rarely comes up , but the situation of trying to have multiple pages on an SPA use an
external JavaScript file fits the situation exactly . It ' s multiple actors ( pages ) attempting to load
the same slow randomly loading resource ( a ` .js ` file ) .
# # # Why Not ` <script> ` Defer or Async
The ` defer ` and ` async ` attributes to a ` <script> ` tag do _not _ solve the
problem of requiring an external ` .js ` file be _completely ready _ before
other code runs in an SPA . Both keywords come from the era when scripts ran
in the context of a single "page" and were bounded by a full HTTP request
download . In that context , these features work to determine when a script
loads since it ' s triggered when the user transitions to a new page using a
full HTTP request .
In an SPA these don ' t work because "page" is simply a visual construct and
not bounded by an HTTP request . If a user clicks a link in an SPA and sees a
new "page" there is probably no actual HTTP transition that loads it , and the
HTTP request is only when the _whole application _ loads . That means when you
put a ` <script> ` at the top of the SPA it will load for all pages even if a
user never hits a page needing that code .
In an SPA like the Svelte app what you want is an ability to say , " Only try
to load the gigantic HLS . js module when the user views a page with Videos on
it . " With classic ` <script> ` tags you ' ll download all 300 kb of the ` HLS.js `
code right when someone tries to log in and isn ' t even seeing a video . In
the world of Google using its monopoly to punish anyone with "slow" load
times this is a death to your website .
The ` Mutex ` helps the ` Source.svelte ` component load code only when it ' s
actually needed , which improves the load times of the whole application , and
makes it nicer for users . Instead of waiting for a massive 500 MB
"application" to download they get a quick initial download for the majority
of the application and then it gradually adds more code as they encounter more features .
_ _ _WARNING _ _ _ : I think this is right but please let me know if you find bugs or have improvements
to this code .
* /
export class Mutex {
/ *
Not much to it , just make the Mutex .
* /
constructor ( ) {
this . waiting = [ ] ;
}
/ *
Get the count of actors waiting on this mutex . It ' s a get attribute .
* /
get count ( ) {
return this . waiting . length ;
}
/ *
This will attempt the lock , and if it can ' t get one then it _ _ _does not block _ _ _ .
+ _ _ _return _ _ _ ` boolean ` -- If you got the lock then it 's `true`, otherwise it' s ` false ` .
* /
async attempt ( ) {
if ( this . waiting . length === 0 ) {
this . waiting . push ( defer ( ) ) ;
return true ;
} else {
// they don't want to get it so return false
return false ;
}
}
/ *
Grab the lock , and if you ' re the winner then you get a ` true ` . If you have to wait
then it ' s a Promise . You ` await m.hold() ` and then wait on the Promise , or just pass
through if you ' re the winner . Later , as actors call ` release() ` your Promise is popped
off the list of waiting actors and when it ' s your turn your Promise is resolved and you
continue on . The ` Promise ` is then ` resolve(true) ` so you always receive a ` true ` result .
+ _ _ _return _ _ _ ` boolean|Promise ` -- If you await you either get the boolean or wait .
* /
async hold ( ) {
if ( this . waiting . length === 0 ) {
this . waiting . push ( defer ( ) ) ;
return true ;
} else {
const lock = defer ( ) ;
this . waiting . push ( lock ) ;
return lock ;
}
}
/ *
This pops the next waiting actor , then calls ` resolve(true) ` on the promise .
If you ' ve called ` hold() ` then you need to call ` release() ` at some point or
everyone will be blocked .
* /
release ( ) {
const lock = this . waiting . pop ( ) ;
lock . resolve ( true ) ;
}
/ *
This effectively "context switches" so other waiting actors can go .
It causes the caller to call ` hold() ` and then ` release() ` . This means
when you call this you ' ll push your Promise on the list , then other
actors will go , and when they ' re all done , your release will cause
the next one to go after you .
* /
async wait ( ) {
await this . hold ( ) ;
await this . release ( ) ;
}
}
/ *
Used inside observer actions ( see Svelte actions ) to create a
cleanup function when the action is destroyed . This by default
calls unobserve on the node , but it will also do a callback if
provided to options . Callbacks are :
+ ` update(node, opts) ` - Called whenever the values of options is updated , includes the node .
+ ` destroy(node) ` - Called when the action is destroyed , includes the node .
* /
const create _observer _cleanup = ( observer , node , options ) => {
const cleaner = {
destroy ( ) {
if ( options . destroy ) options . destroy ( node ) ;
observer . unobserve ( node ) ;
}
}
if ( options . update ) {
cleaner . update = ( opts ) => {
options . update ( node , opts ) ;
}
}
return cleaner ;
}
/ *
A Svelte ` use: ` handler that adds a ` visible ` and ` hidden ` callback to a
Svelte node . Very useful in forever scroll UIs . It uses the
` IntersectionObserver ` to do the main work , then triggers a ` visible ` or
` hidden ` events on the node .
# # # Usage
You use this as a Svelte ` use ` function to add the new events like this :
` ` ` html
< script >
let visible = false ;
const show = ( ) => visible = true ;
const hide = ( ) => visible = false ;
< / s c r i p t >
< div use : visibility = { options } on : visible = { show } on : hidden = { hide } >
{ # if visible }
Shown !
{ : else }
Hidden !
{ / i f }
< / d i v >
` ` `
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 ) => {
for ( let entry of entries ) {
if ( entry . isIntersecting ) {
node . dispatchEvent ( new CustomEvent ( "visible" , { detail : entry } ) ) ;
} else {
node . dispatchEvent ( new CustomEvent ( "hidden" , { detail : entry } ) ) ;
}
}
}
const vis _observer = new IntersectionObserver ( callback , options || { } ) ;
vis _observer . observe ( node ) ;
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 => {
node . dispatchEvent ( new CustomEvent ( "resize" , { detail : entry } ) ) ;
} ) ;
}
const resize _observer = new ResizeObserver ( callback , options || { } ) ;
resize _observer . observe ( node ) ;
return create _observer _cleanup ( resize _observer , node , options ) ;
}