Skip to content

Commit

Permalink
feat(core): added handleWakeLock utility
Browse files Browse the repository at this point in the history
  • Loading branch information
novaotp committed Feb 1, 2025
1 parent 07d9a51 commit 351d1a5
Show file tree
Hide file tree
Showing 8 changed files with 359 additions and 1 deletion.
2 changes: 1 addition & 1 deletion packages/core/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@sv-use/core",
"version": "1.4.3",
"version": "1.5.0",
"license": "MIT",
"description": "A collection of Svelte 5 utilities.",
"main": "./dist/index.js",
Expand Down
5 changes: 5 additions & 0 deletions packages/core/src/__internal__/configurable.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,10 @@ export interface ConfigurableDocument {
document?: Document;
}

export interface ConfigurableNavigator {
navigator?: Navigator;
}

export const defaultWindow = BROWSER ? window : undefined;
export const defaultDocument = BROWSER ? document : undefined;
export const defaultNavigator = BROWSER ? navigator : undefined;
10 changes: 10 additions & 0 deletions packages/core/src/__internal__/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,13 @@ export type MaybeGetter<T> = T | Getter<T>;
export type Arrayable<T> = T | T[];

export type CleanupFunction = () => void;

export interface AutoCleanup {
/**
* Whether to auto-cleanup the event listener or not.
*
* If set to `true`, it must run in the component initialization lifecycle.
* @default true
*/
autoCleanup?: boolean;
}
179 changes: 179 additions & 0 deletions packages/core/src/browser/handle-wake-lock/index.svelte.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
import { describe, expect, it, vi } from 'vitest';
import { tick } from 'svelte';
import { handleWakeLock } from './index.svelte.js';
import { asyncEffectRoot } from '../../__internal__/utils.svelte.js';

class MockWakeLockSentinel extends EventTarget {
released = false;

release() {
this.released = true;
return Promise.resolve();
}
}

function defineWakeLockAPI() {
const sentinel = new MockWakeLockSentinel();

Object.defineProperty(navigator, 'wakeLock', {
value: { request: async () => sentinel as WakeLockSentinel },
writable: true
});

return sentinel;
}

class MockDocument extends EventTarget {
visibilityState = 'hidden';
}

describe('Wake Lock API is not supported', () => {
it("doesn't change isActive if it isn't supported", async () => {
const cleanup = asyncEffectRoot(async () => {
const wakeLock = handleWakeLock({ autoCleanup: false, navigator: {} as Navigator });

expect(wakeLock.isActive).toBeFalsy();

await wakeLock.request('screen');

expect(wakeLock.isActive).toBeFalsy();

await wakeLock.release();

expect(wakeLock.isActive).toBeFalsy();

wakeLock.cleanup();
});

await cleanup();
});
});

describe('Wake Lock API is supported', () => {
it('changes isActive if it is supported', async () => {
const cleanup = asyncEffectRoot(async () => {
defineWakeLockAPI();

const wakeLock = handleWakeLock({ autoCleanup: false });

expect(wakeLock.isActive).toBeFalsy();

await wakeLock.forceRequest('screen');

expect(wakeLock.isActive).toBeTruthy();

await wakeLock.release();

expect(wakeLock.isActive).toBeFalsy();

wakeLock.cleanup();
});

await cleanup();
});

it('changes isActive if show other tabs or minimize window', async () => {
const cleanup = asyncEffectRoot(async () => {
vi.useFakeTimers();
defineWakeLockAPI();

const wakeLock = handleWakeLock({ autoCleanup: false });

expect(wakeLock.isActive).toBeFalsy();

await wakeLock.request('screen');
await vi.advanceTimersByTimeAsync(10);

expect(wakeLock.isActive).toBeTruthy();

document.dispatchEvent(new window.Event('visibilitychange'));

expect(wakeLock.isActive).toBeTruthy();

wakeLock.cleanup();
});

await cleanup();
});

it('delays requesting if the document is hidden', async () => {
const cleanup = asyncEffectRoot(async () => {
defineWakeLockAPI();
const mockDocument = new MockDocument();

const wakeLock = handleWakeLock({ autoCleanup: false, document: mockDocument as Document });

await wakeLock.request('screen');

expect(wakeLock.isActive).toBeFalsy();

mockDocument.visibilityState = 'visible';
mockDocument.dispatchEvent(new Event('visibilitychange'));

await tick();
await tick();

expect(wakeLock.isActive).toBeTruthy();

wakeLock.cleanup();
});

await cleanup();
});

it('cancels requesting if release is called before the document becomes visible', async () => {
const cleanup = asyncEffectRoot(async () => {
defineWakeLockAPI();
const mockDocument = new MockDocument();

const wakeLock = handleWakeLock({ autoCleanup: false, document: mockDocument as Document });

await wakeLock.request('screen');

expect(wakeLock.isActive).toBeFalsy();

await wakeLock.release();

expect(wakeLock.isActive).toBeFalsy();

mockDocument.visibilityState = 'visible';
mockDocument.dispatchEvent(new Event('visibilitychange'));

expect(wakeLock.isActive).toBeFalsy();

wakeLock.cleanup();
});

await cleanup();
});

it('becomes inactive if wake lock is released', async () => {
const cleanup = asyncEffectRoot(async () => {
const sentinel = defineWakeLockAPI();
const mockDocument = new MockDocument();
mockDocument.visibilityState = 'visible';

const wakeLock = handleWakeLock({ autoCleanup: false, document: mockDocument as Document });

await wakeLock.request('screen');

expect(wakeLock.isActive).toBeTruthy();

mockDocument.visibilityState = 'hidden';
mockDocument.dispatchEvent(new Event('visibilitychange'));
sentinel.dispatchEvent(new Event('release'));

expect(wakeLock.isActive).toBeFalsy();

mockDocument.visibilityState = 'visible';
mockDocument.dispatchEvent(new Event('visibilitychange'));
await wakeLock.request('screen');

expect(wakeLock.isActive).toBeTruthy();

wakeLock.cleanup();
});

await cleanup();
});
});
116 changes: 116 additions & 0 deletions packages/core/src/browser/handle-wake-lock/index.svelte.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
import { onDestroy } from 'svelte';
import { getDocumentVisibility } from '../../elements/index.js';
import { whenever } from '../../lifecycle/whenever/index.svelte.js';
import { handleEventListener } from '../handle-event-listener/index.svelte.js';
import { noop } from '../../__internal__/utils.svelte.js';
import {
defaultDocument,
defaultNavigator,
type ConfigurableDocument,
type ConfigurableNavigator
} from '../../__internal__/configurable.js';
import type { AutoCleanup, CleanupFunction } from '../../__internal__/types.js';

type WakeLockType = 'screen';

interface WakeLockSentinel extends EventTarget {
type: WakeLockType;
released: boolean;
release: () => Promise<void>;
}

type NavigatorWithWakeLock = Navigator & {
wakeLock: { request: (type: WakeLockType) => Promise<WakeLockSentinel> };
};

interface HandleWakeLockOptions extends ConfigurableNavigator, ConfigurableDocument, AutoCleanup {}

type HandleWakeLockReturn = {
readonly isSupported: boolean | undefined;
readonly isActive: boolean;
sentinel: WakeLockSentinel | null;
request: (type: WakeLockType) => Promise<void>;
forceRequest: (type: WakeLockType) => Promise<void>;
release: () => Promise<void>;
cleanup: CleanupFunction;
};

/**
* Provides a way to prevent devices from dimming or locking the screen when an application needs to keep running.
* @param options Additional options to customize the behavior.
*/
export function handleWakeLock(options: HandleWakeLockOptions = {}): HandleWakeLockReturn {
const { autoCleanup = true, navigator = defaultNavigator, document = defaultDocument } = options;

let eventListenerCleanup: CleanupFunction = noop;

let requestedType = $state<WakeLockType | false>(false);
let sentinel = $state<WakeLockSentinel | null>(null);
const documentVisibility = getDocumentVisibility({ autoCleanup, document });
const isSupported = $derived.by(() => !!navigator && 'wakeLock' in navigator);
const isActive = $derived.by(() => !!sentinel && documentVisibility.current === 'visible');

if (isSupported) {
eventListenerCleanup = handleEventListener(
sentinel!,
'release',
() => {
requestedType = sentinel?.type ?? false;
},
{ autoCleanup, passive: true }
);

whenever(
() => documentVisibility.current === 'visible' && !!requestedType,
() => {
requestedType = false;
forceRequest('screen');
}
);
}

if (autoCleanup) {
onDestroy(() => cleanup());
}

async function forceRequest(type: WakeLockType): Promise<void> {
await sentinel?.release();
sentinel = isSupported
? await (navigator as NavigatorWithWakeLock).wakeLock.request(type)
: null;
}

async function request(type: WakeLockType): Promise<void> {
if (documentVisibility.current === 'visible') {
await forceRequest(type);
} else {
requestedType = type;
}
}

async function release(): Promise<void> {
requestedType = false;
sentinel?.release().then(() => {
sentinel = null;
});
}

function cleanup() {
documentVisibility.cleanup();
eventListenerCleanup();
}

return {
get isSupported() {
return isSupported;
},
get isActive() {
return isActive;
},
sentinel,
request,
forceRequest,
release,
cleanup
};
}
1 change: 1 addition & 0 deletions packages/core/src/browser/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@ export * from './create-web-notification/index.svelte.js';
export * from './get-clipboard-text/index.svelte.js';
export * from './get-permission/index.svelte.js';
export * from './handle-event-listener/index.svelte.js';
export * from './handle-wake-lock/index.svelte.js';
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<script lang="ts">
import { handleWakeLock } from '$sv-use/core';
const wakeLock = handleWakeLock();
function onclick() {
return wakeLock.isActive ? wakeLock.release() : wakeLock.request('screen');
}
</script>

<div class="relative flex w-full flex-col gap-2">
{#if wakeLock.isSupported}
<p>Is Active: {wakeLock.isActive}</p>
<button {onclick} class="bg-svelte rounded-md px-3 py-1 text-white">
{wakeLock.isActive ? 'Deactivate' : 'Activate'}
</button>
{:else}
<p>Your browser doesn't support the Screen Wake Lock API :(</p>
{/if}
</div>
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# handleWakeLock

Provides a way to prevent devices from dimming or locking the screen when an application needs to keep running.

You may read more about the [Screen Wake Lock API](https://developer.mozilla.org/en-US/docs/Web/API/Screen_Wake_Lock_API).

## Usage

> [!IMPORTANT]
> Since it uses `$effect` internally, you must either call `handleWakeLock` in
> the component initialization lifecycle or call it inside `$effect.root`.
```svelte
<script>
import { handleWakeLock } from '@sv-use/core';
const wakeLock = handleWakeLock();
// When you need to prevent the screen from locking or dimming
await wakeLock.request('screen');
// ...
// When you don't need it anymore
await wakeLock.release();
</script>
```

0 comments on commit 351d1a5

Please sign in to comment.