Last active
January 15, 2026 23:03
-
-
Save petsel/671d53bc0729e5604a5ad5dd4cb6c7ce to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| function getTypeSignature(...args) { | |
| const [value] = args; | |
| return ((args.length > 0) && Object.prototype.toString.call(value).trim()) || value; | |
| } | |
| function getTaggedType(...args) { | |
| return getTypeSignature(...args)?.slice(8, -1); | |
| } | |
| function getDefinedConstructor(value = null) { | |
| return (value !== null) | |
| ? Object.getPrototypeOf(value)?.constructor | |
| : void 0; | |
| } | |
| function getDefinedConstructorName(value) { | |
| return getDefinedConstructor(value)?.name; | |
| } | |
| function isTypeofFunction(value) { | |
| return (typeof value === 'function'); | |
| } | |
| function isBoolean(value) { | |
| return ((typeof value === 'boolean') || ( | |
| !!value && | |
| (getTaggedType(value) === 'Boolean') && | |
| (getDefinedConstructorName(value) === 'Boolean') | |
| )); | |
| } | |
| function isString(value) { | |
| return ((typeof value === 'string') || ( | |
| !!value && | |
| (getTaggedType(value) === 'String') && | |
| (getDefinedConstructorName(value) === 'String') | |
| )); | |
| } | |
| function isWakeLockSentinel(value) { | |
| return ( | |
| !!value && | |
| (getTaggedType(value) === 'WakeLockSentinel') && | |
| (getDefinedConstructorName(value) === 'WakeLockSentinel') | |
| ); | |
| } | |
| function isWakeLockSignal(value) { | |
| return ( | |
| !!value && | |
| (getTaggedType(value) === 'WakeLockSignal') && | |
| (getDefinedConstructorName(value) === 'WakeLockSignal') | |
| ); | |
| } | |
| function isWakeLockController(value) { | |
| return ( | |
| !!value && | |
| (getTaggedType(value) === 'WakeLockController') && | |
| (getDefinedConstructorName(value) === 'WakeLockController') | |
| ); | |
| } | |
| function parseArrayLikeValues(data) { | |
| const iterator = isTypeofFunction(data?.values) && data.values(); | |
| return ( | |
| ((iterator[Symbol.iterator] === [].values()[Symbol.iterator]) && [...iterator]) || [] | |
| ); | |
| } | |
| const connectorLookup = new Set; | |
| const gateKeeper = new Set; | |
| const masterKey = Symbol('create'); | |
| class WakeLockSignal extends EventTarget { | |
| #state = { | |
| type: void 0, | |
| released: void 0, | |
| keepAwake: false, | |
| }; | |
| constructor(...args) { | |
| // supresses arity hint. | |
| const [connector] = args; | |
| // guard. | |
| if (!connectorLookup.has(connector)) { | |
| // malicious usage ... throw immediately. | |
| throw new TypeError("Failed to construct 'WakeLockSignal': Illegal constructor usage."); | |
| } | |
| super(); | |
| this.onrelease = null; | |
| connector(this, this.#state); | |
| } | |
| get [Symbol.toStringTag]() { return 'WakeLockSignal'; } | |
| get type() { | |
| return this.#state.type; | |
| } | |
| get released() { | |
| return this.#state.released; | |
| } | |
| get keepAwake() { | |
| return this.#state.keepAwake; | |
| } | |
| set keepAwake(value) { | |
| if (isBoolean(value)) { | |
| const result = Boolean(value); | |
| if (result !== this.keepAwake) { | |
| this.#state.keepAwake = result; | |
| // - dispatch the status change of `signal.keepAwake`. | |
| // - listened to by e.g. plugins or other third party | |
| // event-handlers that e.g. need to alter `request` | |
| // behavior or need to track/sync/manage keep-awake | |
| // releated state. | |
| this.dispatchEvent( | |
| new Event('keepawake:change'), | |
| ); | |
| } | |
| } | |
| return this.keepAwake; | |
| } | |
| static isWakeLockSignal(value) { | |
| return isWakeLockSignal(value); | |
| } | |
| } | |
| /* | |
| * to be bound wake-lock 'release' event handler. | |
| */ | |
| function handleRelease(signal, __signalState, evt) { | |
| const wakeLock = evt.target; | |
| console.log('handleRelease ...', { evt }); | |
| __signalState.released = wakeLock.released; | |
| // - listener-notification before | |
| // callback-method invocation. | |
| signal.dispatchEvent( | |
| new CustomEvent(evt.type, { | |
| detail: { | |
| origin: { | |
| target: wakeLock, | |
| event: evt, | |
| }, | |
| }, | |
| }) | |
| ); | |
| if (isTypeofFunction(signal.onrelease)) { | |
| signal.onrelease(evt); | |
| } else { | |
| signal.onrelease = null; | |
| } | |
| } | |
| function handleRequestSuccess(__ctrState, signal, __signalState, wakeLock) { | |
| /* | |
| * deregisters itself immediately with/after the one-time `release` action. | |
| */ | |
| wakeLock.addEventListener('release', handleRelease.bind(null, signal, __signalState)); | |
| __ctrState.wakeLock = wakeLock; | |
| __signalState.released = wakeLock.released; | |
| queueMicrotask(() => { | |
| signal.dispatchEvent(new CustomEvent('request:success', { | |
| detail: { wakeLock }, | |
| })); | |
| }); | |
| return wakeLock; | |
| } | |
| function handleRequestFailure(signal, cause) { | |
| const error = new ReferenceError( | |
| "Wake lock request failed; Look into 'error.cause'.", | |
| { cause }, | |
| ); | |
| queueMicrotask(() => { | |
| signal.dispatchEvent(new CustomEvent('request:failure', { | |
| detail: { error }, | |
| })); | |
| }); | |
| return error; | |
| } | |
| function handleReleaseSuccess(signal, wakeLock, result) { | |
| result = (result ?? true); | |
| queueMicrotask(() => { | |
| signal.dispatchEvent(new CustomEvent('release:success', { | |
| detail: { wakeLock, result }, | |
| })); | |
| }); | |
| return result; | |
| } | |
| function handleReleaseFailure(signal, wakeLock, cause) { | |
| const error = new Error( | |
| "Wake lock release failed; Look into 'error.cause'.", | |
| { cause }, | |
| ); | |
| queueMicrotask(() => { | |
| signal.dispatchEvent(new CustomEvent('release:failure', { | |
| detail: { wakeLock, error }, | |
| })); | |
| }); | |
| return error; | |
| } | |
| async function request(__ctrState, signal, __signalState) { | |
| const { wakeLock } = __ctrState; | |
| // guard. | |
| if (wakeLock && !wakeLock.released) { | |
| return wakeLock; | |
| } | |
| try { | |
| const wakeLock = await navigator.wakeLock.request(__signalState.type); | |
| return handleRequestSuccess(__ctrState, signal, __signalState, wakeLock); | |
| } catch (exception) { | |
| return handleRequestFailure(signal, exception); | |
| } | |
| } | |
| async function release(__ctrState, signal) { | |
| const { wakeLock } = __ctrState; | |
| // guard. | |
| if (wakeLock.released) { | |
| return false; | |
| } | |
| try { | |
| const result = await wakeLock.release(); | |
| return handleReleaseSuccess(signal, wakeLock, result); | |
| } catch (exception) { | |
| return handleReleaseFailure(signal, wakeLock, exception); | |
| } | |
| } | |
| async function create(options) { | |
| gateKeeper.add(masterKey); | |
| const controller = new WakeLockController(options); | |
| gateKeeper.delete(masterKey); | |
| const result = await controller.request(); | |
| const success = isWakeLockSentinel(result); | |
| const error = (!success && result || null); | |
| const value = (success && controller || null); | |
| return { | |
| error, value, success, | |
| *[Symbol.iterator]() { | |
| yield error; yield value; yield success; | |
| }, | |
| }; | |
| } | |
| /*export */class WakeLockController { | |
| #state = { | |
| wakeLock: null, | |
| }; | |
| #signal; | |
| #signalState; | |
| constructor(options) { | |
| // guard. | |
| if (!gateKeeper.has(masterKey)) { | |
| /* | |
| * - disallow the sole usage of just the constructor itself. | |
| * - force usage of the async, static `WakeLockSignal.create` | |
| * method in order to always guaranty a from the beginning | |
| * available and managable `WakeLockSentinel` instance. | |
| */ | |
| throw new TypeError("Failed to construct 'WakeLockController': Illegal constructor usage."); | |
| } | |
| const { type, onrelease, keepAwake } = (options = (options || {})); | |
| const connector = (signal, __signalState) => { | |
| this.#signal = signal; | |
| this.#signalState = __signalState; | |
| }; | |
| connectorLookup.add(connector); | |
| /* | |
| * - the only time/point at which the | |
| * related signal can be constructed, | |
| * and the latter's state gets shared. | |
| */ | |
| new WakeLockSignal(connector); | |
| connectorLookup.delete(connector); | |
| pluginRegistry.set(this, new Map); | |
| const signal = this.#signal; | |
| const __signalState = this.#signalState; | |
| __signalState.type = (isString(type) && type.trim()) || 'screen'; | |
| signal.onrelease = (isTypeofFunction(onrelease) && onrelease) || null; | |
| parseArrayLikeValues(options.plugins) | |
| .forEach(pluginOptions => addPlugin(pluginOptions, this)); | |
| /* | |
| * - invoking the `keepAwake` setter might already | |
| * trigger an initial 'keepawake:change' event. | |
| */ | |
| signal.keepAwake = keepAwake; | |
| } | |
| get [Symbol.toStringTag]() { return 'WakeLockController'; } | |
| get signal() { | |
| return this.#signal; | |
| } | |
| async request() { | |
| return request(this.#state, this.#signal, this.#signalState); | |
| } | |
| async release() { | |
| return release(this.#state, this.#signal); | |
| } | |
| addPlugin(options) { | |
| return addPlugin(options, this); | |
| } | |
| removePlugin(options) { | |
| return removePlugin(options, this); | |
| } | |
| get pluginLabels() { | |
| return getPluginLabels(this); | |
| } | |
| static async create(options) { | |
| return create(options); | |
| } | |
| static isWakeLockController(value) { | |
| return isWakeLockController(value); | |
| } | |
| } | |
| const pluginRegistry = new WeakMap; | |
| function addPlugin(options, controller, pluginLabels) { | |
| const { register } = (options = (options || {})); | |
| const storage = pluginRegistry.get(controller); | |
| // guard ... already registered. | |
| if (storage.has(register) || !isTypeofFunction(register)) { | |
| return false; | |
| } | |
| const secretStorage = Object.create(null); | |
| try { | |
| register(controller, secretStorage); | |
| } catch(cause) { | |
| /* | |
| * - a failing plugin registry | |
| * does not get handled silently. | |
| * - it throws an error with cause. | |
| */ | |
| throw new Error('Plugin registry failure.', { cause }); | |
| } | |
| const { label, deregister } = options; | |
| storage.set(register, { | |
| label: (isString(label) && label.trim()) || register.name.trim(), | |
| deregister: (isTypeofFunction(deregister) && deregister) || null, | |
| secretStorage, | |
| }); | |
| return true; | |
| } | |
| function removePlugin(options, controller) { | |
| const { register } = (options = (options || {})); | |
| const storage = pluginRegistry.get(controller); | |
| const plugin = storage.get(register); | |
| const deregister = (plugin && (plugin.deregister || options.deregister)); | |
| // guard ... either not existing or without valid de-registering. | |
| if (!isTypeofFunction(deregister)) { | |
| return false; | |
| } | |
| try { | |
| deregister(controller, plugin.secretStorage); | |
| } catch(cause) { | |
| /* | |
| * - a failing plugin de-registry | |
| * does not get handled silently. | |
| * - it throws an error with cause. | |
| */ | |
| throw new Error('Plugin de-registry failure.', { cause }); | |
| } | |
| return storage.delete(register); | |
| } | |
| function getPluginLabels(controller) { | |
| return [ | |
| ...(pluginRegistry.get(controller) ?? new Map()).values() | |
| ] | |
| .map(({ label }) => label); | |
| } | |
| /* | |
| * Plugin :: START | |
| * | |
| * - manages the controllers auto-request behavior | |
| * at a document's 'visibilitychange' event. | |
| */ | |
| /* | |
| * to be bound 'visibilitychange' related auto-request behavior. | |
| */ | |
| function handleLifecycleAtVisibilityChange(controller, evt) { | |
| console.log('handle lifecycle at visibilitychange ...', { evt }); | |
| if (document.visibilityState === 'visible') { | |
| controller.request(); | |
| } else if (controller.signal.released === false) { | |
| controller.release(); | |
| } | |
| } | |
| /* | |
| * to be bound 'visibilitychange' related un/subscribe behavior. | |
| */ | |
| function manageVisibilityChangeLifecycleAtKeepAwakeChange(handleLifecycle, lifecycleAC, evt) { | |
| const { target: signal } = evt; | |
| console.log('manage visibilitychange lifecycle at keep-awake change ...', { evt }); | |
| if (signal.keepAwake === true) { | |
| document.addEventListener( | |
| 'visibilitychange', | |
| handleLifecycle, | |
| // deregistering at plugin-removal. | |
| { signal: lifecycleAC.signal }, | |
| ); | |
| } else { | |
| // deregistering default. | |
| document.removeEventListener('visibilitychange', handleLifecycle); | |
| } | |
| } | |
| function registerLifecycleManagementAtVisibilityChange(controller, secretStorage) { | |
| const { signal } = controller; | |
| // throwing guard. | |
| if (signal.type !== 'screen') { | |
| throw new TypeError( | |
| "Plugin registry failure. Plugin expects a 'screen'-type wake-lock environment." | |
| ); | |
| } | |
| const lifecycleAC = new AbortController(); | |
| const keepawakeAC = new AbortController(); | |
| Object.assign(secretStorage, { lifecycleAC, keepawakeAC }); | |
| const handleLifecycle = handleLifecycleAtVisibilityChange.bind(null, controller); | |
| signal.addEventListener( | |
| 'keepawake:change', | |
| manageVisibilityChangeLifecycleAtKeepAwakeChange.bind(null, handleLifecycle, lifecycleAC), | |
| { signal: keepawakeAC.signal }, | |
| ); | |
| // initial management call. | |
| manageVisibilityChangeLifecycleAtKeepAwakeChange | |
| .call(null, handleLifecycle, lifecycleAC, { target: signal }); | |
| /* | |
| * - final assurance of the initially correct state, | |
| * granted with/at the plugin's register time. | |
| */ | |
| if((document.visibilityState === 'visible') && signal.released && signal.keepAwake) { | |
| controller.request(); | |
| } | |
| } | |
| function deregisterLifecycleManagementAtVisibilityChange(controller, secretStorage) { | |
| const { lifecycleAC, keepawakeAC } = secretStorage; | |
| lifecycleAC.abort(); | |
| keepawakeAC.abort(); | |
| } | |
| /* | |
| * Plugin :: END | |
| */ | |
| const [error, controller, success] = await WakeLockController.create({ | |
| onrelease(evt) { | |
| console.log('custom onrelease method ...', { evt }); | |
| }/*, | |
| plugins: [{ | |
| register: registerLifecycleManagementAtVisibilityChange, | |
| deregister: deregisterLifecycleManagementAtVisibilityChange, | |
| }]*/, | |
| keepAwake: true, | |
| }); | |
| console.log({error, controller, success}); | |
| controller.addPlugin({ | |
| register: registerLifecycleManagementAtVisibilityChange, | |
| deregister: deregisterLifecycleManagementAtVisibilityChange, | |
| }); | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment