Skip to content

Instantly share code, notes, and snippets.

@petsel
Last active January 15, 2026 23:03
Show Gist options
  • Select an option

  • Save petsel/671d53bc0729e5604a5ad5dd4cb6c7ce to your computer and use it in GitHub Desktop.

Select an option

Save petsel/671d53bc0729e5604a5ad5dd4cb6c7ce to your computer and use it in GitHub Desktop.
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