You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 

339 lines
12 KiB

/*
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 300kb 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 500MB
"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;
</script>
<div use:visibility={ options } on:visible={ show } on:hidden={ hide }>
{#if visible}
Shown!
{:else}
Hidden!
{/if}
</div>
```
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);
}