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
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);
|
|
}
|
|
|