57 - ChatOS - Timer

Livestream

I plan to add a countdown timer to ChatOS to make it possible to achieve Pomodoro Workflow without using other timer apps. I was using Session and Toggl.

I create Timer.svelte component and find out that Web Notifications API alone will not work if I’m in another browser tab as the main thread stops the timer from running, I will have to use Web Worker to count the time in background, and Service Worker to post the notification when the timer is ended.

Timer in web worker

I’m using SvelteKit on Vite. I can create a web worker like so. (Types are omitted)

// timer.worker.ts
onmessage = (message) => {
	const { endAt } = message.data;

	const intervalId = setInterval(() => {
		if (Date.now() >= endAt) {
			clearInterval(intervalId);
			postMessage({ ended: true });
		}
	}, 1000);
};

export {};

Then, and dynamic import it with ?worker suffix to tell Vite that it’s a web worker script. This component will send { endAt: Date } to the worker, then worker will post { ended: true } back when the timer ends.

<!-- Timer.svelte -->

<script lang="ts">
    export let endAt: Date
    
    async function runTimerWorker() {
		const TimerWorker = await import('$lib/workers/timer.worker?worker');
		timerWorker = new TimerWorker.default();

		const message = { endAt };
		timerWorker.postMessage(message);
		
		timerWorker.onmessage = (event) => {
			const message = event.data;

			if (message.ended) {
				console.log("Received ended: true from the worker")
			}
		};
	}

    onMount(runTimerWorker)
</script>

...

Notification in service worker

Create another service worker that can send web notification, it may be blank but I added some logging code so I can hack on it if needed.

// service-worker.ts

/// <reference types="@sveltejs/kit" />
/// <reference no-default-lib="true"/>
/// <reference lib="esnext" />
/// <reference lib="webworker" />
const sw = self as unknown as ServiceWorkerGlobalScope;

sw.addEventListener('activate', async () => {
	// This will be called only once when the service worker is activated.
	console.log('service worker activated');
});

Then import and register the service worker in the same component of the timer web worker.

// Timer.svelte

<script lang="ts">
    let worker: ServiceWorkerRegistration;

    onMount(async () => {
		if ('serviceWorker' in navigator) {
			worker = await navigator.serviceWorker.register('./service-worker.js', {
				type: dev ? 'module' : 'classic' 
			});
		}
    })
</script>

...

Now you can use worker.showNotification to send notifications. But before that make sure you have notification permission by calling Notification.requestPermission()

// Timer.svelte

<script lang="ts">
    ...

    async function runTimerWorker() {
        ...

        timerWorker.onmessage = (event) => {
			const message = event.data;

			if (message.ended) {
				console.log("Received ended: true from the worker")

                worker.showNotification("Timer", { body: "Timer is ended" })
			}
		};
    }
</script>

<!-- Don't forget to allow notification permission -->
<button on:click={() => Notification.requestPermission()}>
    Allow notification
</button>

...

See the working code (at the time of this writing) here:

Try the timer command on ChatOS by typing timer mm:ss

The finished timer

The notification

Possible improvements

  • Play around with Notification properties, adding icon, custom sound, timestamp, etc.
  • Refactor notification related code so that other commands can use it as well
  • Support Web Push API, so that the tab can be closed but notification still can be sent to user (This may need persisting user…)

References