Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 5 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -259,7 +259,7 @@ This package provides synchronization objects that use `Atomics` to cross the th
This is the web worker equivalent of `AbortController` and provides a token that can be passed to workers in a task's
payload.

The worker can use `CancellationSource.isSignalled()` or `CancellationSource.throwIfSignalled()` through polling in order
The worker can use `CancellationSource.isSignaled()` or `CancellationSource.throwIfSignaled()` through polling in order
to abort the current work:

```typescript
Expand All @@ -268,7 +268,7 @@ import { CancellationSource, type Token } from '@wjfe/async-workers';
function computeSomeStuff(cancelToken?: Token) {
for (let i = 0; i < Number.MAX_SAFE_INTEGER; ++i) {
// No thowing will be done if cancelToken is undefined.
CancellationSource.throwIfSignalled(cancelToken);
CancellationSource.throwIfSignaled(cancelToken);
...
}
}
Expand Down Expand Up @@ -300,7 +300,7 @@ If you're using `CancellationSource` on your own:

### ManualResetEvent

This is a synchronization object that can be used to signal multiple threads at once because it will remain signalled
This is a synchronization object that can be used to signal multiple threads at once because it will remain signaled
until the `reset()` event is invoked. A typical use case is to use it for pausing a worker's work.

### AutoResetEvent
Expand Down Expand Up @@ -373,7 +373,7 @@ work properly in Node.
| - | - | - |
| [x] ManualResetEvent | [x] Simple request/response scenario | [x] Simple request/response scenario |
| [x] AutoResetEvent | [x] Request/multiple response scenario | [x] Request/multiple response scenario |
| [ ] Semaphore | [x] Strongly-typed tasks | [x] Strongly-typed tasks |
| [x] Semaphore | [x] Strongly-typed tasks | [x] Strongly-typed tasks |
| [x] CancellationSource | [x] Worker termination | |
| | [x] Out-of-order work items | [x] Out-of-order work items |
| [x] Mutex | [x] Out-of-order work items | [x] Out-of-order work items |
| | [x] Task cancellation | [x] Task cancellation|
16 changes: 8 additions & 8 deletions pages/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions pages/src/lib/CrossOriginNotIsolated.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@
<li><code>ManualResetEvent</code></li>
<li><code>AutoResetEvent</code></li>
<li><code>CancellationSource</code></li>
<li><code>Semaphore</code></li>
<li><code>Mutex</code></li>
</ul>
<p>
In order to solve this problem in your own project/site,
Expand Down
33 changes: 25 additions & 8 deletions pages/src/lib/Instructions.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@
<h3><i class="bi bi-journal-text"></i>&nbsp;Instructions</h3>
<p>
Play around with the three work items below. Each will run the same example worker, which is a worker that
calculates prime numbers using a not-so-good algorithm that runs up to the shown number. All three work items
use the same worker object, so they run serially between each other.
calculates prime numbers using a not-so-good algorithm that runs up to the specified number in the slider. All
three work items use the same worker object, so they run serially between each other.
</p>
{#if crossOriginIsolated}
<p>
Expand All @@ -13,13 +13,30 @@
</a> here), you may pause or cancel the worker at any time.
</p>
<p>
<strong>NOTE:</strong> Notice how significant the pause wait is in terms of performance. Waiting on the
token is expensive for sure, so do this only on well-thought-out scenarios.
<strong>NOTE ON PERFORMANCE:</strong> Atomic operations are less performant than polling. Don't go crazy
with the synchronization objects, and see if polling fits the bill first. For example, the primes worker
in this demo page does polling to condition the wait as opposed to blindly waiting:
</p>
<code class="fs-5"><pre>if (!ManualResetEvent.isSignaled(pause)) &#123;
ManualResetEvent.wait(pause);
&#125;</pre></code>
<p>
Still, even with this modification, you'll see a big difference in times between a pausable and a
non-pausable run for the same numerical limit.
</p>
{:else}
Since cross origin is not isolated (see
<a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/SharedArrayBuffer#security_requirements" target="_blank">
the requirements
</a> here), you may only cancel the worker before it starts. Furthermore, you cannot pause the worker.
<p>
Since cross origin is not isolated (see
<a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/SharedArrayBuffer#security_requirements" target="_blank">
the requirements
</a> here), you may only cancel the worker before it starts. Furthermore, you cannot pause the worker.
</p>
<p>
To experience the power of the synchronization objects, head to the
<a href="https://github.com/WJSoftware/wjfe-async-workers" target="_blank">GitHub repository</a> and clone
the source code. This demonstration's code is inside the folder named <code>pages</code>. Open a console,
change to the <code>pages</code> folder, install the packages running <code>npm ci</code>, and then execute
the development server with <code>npm run dev</code>.
</p>
{/if}
</div>
22 changes: 20 additions & 2 deletions pages/src/lib/Primes.svelte
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
<script lang="ts">
import { untrack } from "svelte";
import { CancelledMessage, ManualResetEvent, WorkItemStatus, type AsyncWorker, type WorkItem } from "../../../dist/index.js";
import { type ExampleWorker } from "../workers/exampleWorker.js";
import { nextControlId } from "./nextControlId.js";
import Stopwatch from "./Stopwatch.svelte";

type Props = {
toNumber: number;
Expand All @@ -24,13 +26,26 @@
let paused = $state(false);
let pauseEvent = crossOriginIsolated ? new ManualResetEvent() : undefined;
const id = nextControlId();
let sw: Stopwatch;

$effect(() => {
if (paused) {
if (pausable && paused) {
pauseEvent?.reset();
untrack(() => sw.stop());
}
else {
else if (pausable && running) {
pauseEvent?.signal();
untrack(() => sw.start());
}
});

$effect(() => {
if (running) {
untrack(() => sw.reset());
untrack(() => sw.start());
}
else {
untrack(() => sw.stop());
}
});

Expand Down Expand Up @@ -159,5 +174,8 @@
Cancel process
</button>
</li>
<li class="list-group-item text-center fs-2">
<Stopwatch bind:this={sw} />
</li>
</ul>
</div>
80 changes: 80 additions & 0 deletions pages/src/lib/Stopwatch.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
<script lang="ts">
import type { HTMLAttributes } from "svelte/elements";
import { fly } from "svelte/transition";

type Props = HTMLAttributes<HTMLSpanElement> & {
class?: string;
};

let {
class: cssClass,
...restProps
}: Props = $props();

let elapsed = $state(0);
let startTime: Date;
let timer = 0;

let hh = $derived(Math.floor(elapsed / 3600));
let mm = $derived(Math.floor((elapsed - hh * 3600) / 60));
let ss = $derived(Math.trunc(elapsed - hh * 3600 - mm * 60));

// Animation-related.
const duration = 180;
const delay = 90;

function f(value: number) {
if (value < 10) {
return `0${value}`;
}
return value.toString();
}

export function start() {
startTime = new Date();
const elapsedSnapshot = $state.snapshot(elapsed);
timer = setInterval(() => {
elapsed = elapsedSnapshot + (new Date().getTime() - startTime.getTime()) / 1_000;
}, 100);
}

export function stop() {
clearInterval(timer);
return elapsed;
}

export function reset() {
stop();
elapsed = 0;
}
</script>

<span class="timer {cssClass}" {...restProps}>
<span class="value">
{#key hh}
<span class="value" in:fly={{ delay, duration, y: '1em'}} out:fly={{ duration, y: '-1em'}}>{f(hh)}</span>
{/key}
</span>:<span class="value">
{#key mm}
<span class="value" in:fly={{ delay, duration, y: '1em'}} out:fly={{ duration, y: '-1em'}}>{f(mm)}</span>
{/key}
</span>:<span class="value">
{#key ss}
<span class="value" in:fly={{ delay, duration, y: '1em'}} out:fly={{ duration, y: '-1em'}}>{f(ss)}</span>
{/key}
</span>
</span>

<style>
span.timer {
padding: 0 0.2em;
}
span.value {
display: inline-flex;
flex-flow: column;
height: 1em;
}
span.value > span {
display: inline-block;
}
</style>
109 changes: 109 additions & 0 deletions pages/src/lib/Timer.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
<script lang="ts">
import type { HTMLAttributes } from 'svelte/elements';
import { readable } from 'svelte/store';
import { fly } from 'svelte/transition';

type Props = HTMLAttributes<HTMLSpanElement> & {
countFrom: number;
class?: string;
onTimesup?: () => void;
};

let {
countFrom,
class: cssClass,
onTimesup,
...restProps
}: Props = $props();

let endDate = $derived(new Date(Date.now() + countFrom * 1000));
// Reactive to account for changes in countFrom:
// $: endDate = (function(secs) {
// const e = Date.now() + secs * 1000;
// return new Date(e);
// })(countFrom);
let remaining = $derived(readable(countFrom, function start(set) {
const interval = setInterval(() => {
let r = Math.round((endDate.getTime() - new Date().getTime()) / 1000);
r = Math.max(r, 0);
set(r);
if (r <= 0) {
clearInterval(interval);
}
}, 1000);
return () => clearInterval(interval);
}));

// $: remaining = readable(countFrom, function start(set) {
// const interval = setInterval(() => {
// let r = Math.round((endDate - new Date()) / 1000);
// r = Math.max(r, 0);
// set(r);
// if (r <= 0) {
// clearInterval(interval);
// }
// }, 1000);

// return function stop() {
// clearInterval(interval);
// };
// });

let hh = $derived(Math.floor($remaining / 3600));
// $: hh = Math.floor($remaining / 3600);
let mm = $derived(Math.floor(($remaining - hh * 3600) / 60));
// $: mm = Math.floor(($remaining - hh * 3600) / 60);
let ss = $derived($remaining - hh * 3600 - mm * 60);
// $: ss = $remaining - hh * 3600 - mm * 60;

$effect(() => {
if ($remaining === 0) {
onTimesup?.();
}
})

// $: if ($remaining === 0) {
// dispatch('timesup');
// }

// Animation-related.
const duration = 180;
const delay = 90;

function f(value: number) {
if (value < 10) {
return `0${value}`;
}
return value.toString();
}
</script>

<span class="timer {cssClass}" {...restProps}>
<span class="value">
{#key hh}
<span class="value" in:fly={{ delay, duration, y: '1em'}} out:fly={{ duration, y: '-1em'}}>{f(hh)}</span>
{/key}
</span>:<span class="value">
{#key mm}
<span class="value" in:fly={{ delay, duration, y: '1em'}} out:fly={{ duration, y: '-1em'}}>{f(mm)}</span>
{/key}
</span>:<span class="value">
{#key ss}
<span class="value" in:fly={{ delay, duration, y: '1em'}} out:fly={{ duration, y: '-1em'}}>{f(ss)}</span>
{/key}
</span>
</span>

<style>
span.timer {
padding: 0 0.2em;
}
span.value {
display: inline-flex;
flex-flow: column;
height: 1em;
}
span.value > span {
display: inline-block;
}
</style>
10 changes: 6 additions & 4 deletions pages/src/workers/exampleWorker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { CancellationSource, ManualResetEvent, workerListener, type PostFn, type
function isPrime(n: number, cancelToken?: Token) {
// Made unecessarily inefficient for demo purposes.
for (let i = 2; i <= n / 2; ++i) {
CancellationSource.throwIfSignalled(cancelToken);
CancellationSource.throwIfSignaled(cancelToken);
if (n % i === 0) {
return false;
}
Expand All @@ -14,8 +14,10 @@ function isPrime(n: number, cancelToken?: Token) {
function isPrimePausable(n: number, pause: Token, cancelToken?: Token) {
// Made unecessarily inefficient for demo purposes.
for (let i = 2; i <= n / 2; ++i) {
CancellationSource.throwIfSignalled(cancelToken);
ManualResetEvent.wait(pause);
CancellationSource.throwIfSignaled(cancelToken);
if (!ManualResetEvent.isSignaled(pause)) {
ManualResetEvent.wait(pause);
}
if (n % i === 0) {
return false;
}
Expand All @@ -33,7 +35,7 @@ function getAllPrimes(max: number, pause: Token | undefined, post: PostFn, cance
}

export const exampleWorker = {
sayHello(payload: { name: string; }) {
iExistToShowOffIntellisense(payload: { name: string; }) {
console.log('Hello, %s!', payload.name);
},
calculatePrimes(payload: { to: number; pause?: Token }, post: PostFn, cancelToken?: Token) {
Expand Down
Loading