|
|
|
@ -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 `<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()); |
|
|
|
@ -69,6 +199,15 @@ export class Mutex { |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
/* |
|
|
|
|
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()); |
|
|
|
@ -80,26 +219,37 @@ export class Mutex { |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
/* |
|
|
|
|
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. |
|
|
|
|
* |
|
|
|
|
/* |
|
|
|
|
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 = { |
|
|
|
@ -119,6 +269,36 @@ const create_observer_cleanup = (observer, node, options) => { |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/* |
|
|
|
|
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) => { |
|
|
|
@ -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 => { |
|
|
|
|