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
4 changes: 1 addition & 3 deletions .github/workflows/npm-publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,13 +25,11 @@ jobs:
cache: npm
registry-url: https://registry.npmjs.org
- name: Install dependencies
run: npm ci --ignore-scripts --no-audit --fund-no
run: npm ci --no-audit --no-fund
- name: Run tests
run: npm test
- name: Build Package
run: npm run build --if-present
- name: Publish to npm
run: npm publish --provenance --access public
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_AUTH_TOKEN }}

2 changes: 1 addition & 1 deletion .nvmrc
Original file line number Diff line number Diff line change
@@ -1 +1 @@
20.9.0
24.10.0
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

## [v2.0.0] - 2026-03-01

## Added
- Add support for `DisposableStack` and `AsyncDisposableStack` disposal of keys

### Changed
- Creating and registering callbacks now returns an object with `[Symbol.dispose]` extending `String`

## [v1.0.3] - 2025-09-29

### Added
Expand Down
25 changes: 23 additions & 2 deletions callbackRegistry.test.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import '@shgysk8zer0/polyfills';
import './shims.js';
import { createCallback, unregisterCallback, hasCallback, callCallback, closeRegistration } from '@aegisjsproject/callback-registry/callbacks.js';
import { createCallback, unregisterCallback, hasCallback, callCallback, closeRegistration, registerCallback, CallbackRegistryKey } from '@aegisjsproject/callback-registry/callbacks.js';
import { describe, test } from 'node:test';
import assert from 'node:assert';

Expand All @@ -9,7 +9,7 @@ const sum = createCallback((...nums) => nums.reduce((sum, num) => sum + num));

describe('Test callback registry', () => {
test('Creating a callback should return the string key.', { signal }, () => {
assert.ok(typeof sum === 'string', 'Callback key should be a string.');
assert.ok(sum instanceof CallbackRegistryKey, 'Callback key should be a `CallbackRegistryKey`.');
});

test('Callbacks are registered correctly', { signal }, () => {
Expand All @@ -25,6 +25,27 @@ describe('Test callback registry', () => {
assert.strictEqual(hasCallback(sum), false, 'Callbacks should be removed from registry.');
});

test('Check callback registry with optional `stack`', { signal }, () => {
const stack = new DisposableStack();
const cb = registerCallback('disposable:log', console.log, { stack });
assert.ok(cb instanceof CallbackRegistryKey, 'Registering callbacks should return disposable String keys.');
assert.ok(hasCallback(cb), 'Callback should be registered');
stack.dispose();
assert.ok(! hasCallback(cb), 'Callback should be unregistered after disposal.');
});

test('Callbacks keys should be disposable.', { signal }, () => {
const name = 'disposable:callback';

{
using cb = registerCallback(name, console.log);
assert.ok(hasCallback(name), 'Callbacks should match the string name/key as well as `CallbackRegistryKey`.');
assert.ok(hasCallback(cb), 'Callbacks should match the string name/key as well as `CallbackRegistryKey`.');
}

assert.ok(! hasCallback(name), 'Callbacks should be removed when disposed.');
});

test('Cannot register new callbacks after closing registry', { signal }, () => {
closeRegistration();
assert.throws(() => createCallback(() => 'test'), 'Creating callbacks should throw if registry is closed.');
Expand Down
62 changes: 42 additions & 20 deletions callbacks.js
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,9 @@ export const FUNCS = {
},
};

/**
* @type {Map<string, function>}
*/
const registry = new Map([
[FUNCS.debug.log, console.log],
[FUNCS.debug.warn, console.warn],
Expand Down Expand Up @@ -165,6 +168,12 @@ const registry = new Map([
[FUNCS.ui.exitFullsceen, () => document.exitFullscreen()],
]);

export class CallbackRegistryKey extends String {
[Symbol.dispose]() {
registry.delete(this.toString());
}
}

/**
* Check if callback registry is open
*
Expand All @@ -189,26 +198,26 @@ export const listCallbacks = () => Object.freeze(Array.from(registry.keys()));
/**
* Check if a callback is registered
*
* @param {string} name The name/key to check for in callback registry
* @param {CallbackRegistryKey|string} name The name/key to check for in callback registry
* @returns {boolean} Whether or not a callback is registered
*/
export const hasCallback = name => registry.has(name);
export const hasCallback = name => registry.has(name?.toString());

/**
* Get a callback from the registry by name/key
*
* @param {string} name The name/key of the callback to get
* @param {CallbackRegistryKey|string} name The name/key of the callback to get
* @returns {Function|undefined} The corresponding function registered under that name/key
*/
export const getCallback = name => registry.get(name);
export const getCallback = name => registry.get(name?.toString());

/**
* Remove a callback from the registry
*
* @param {string} name The name/key of the callback to get
* @param {CallbackRegistryKey|string} name The name/key of the callback to get
* @returns {boolean} Whether or not the callback was successfully unregisterd
*/
export const unregisterCallback = name => _isRegistrationOpen && registry.delete(name);
export const unregisterCallback = name => _isRegistrationOpen && registry.delete(name?.toString());

/**
* Remove all callbacks from the registry
Expand All @@ -221,21 +230,23 @@ export const clearRegistry = () => registry.clear();
* Create a registered callback with a randomly generated name
*
* @param {Function} callback Callback function to register
* @param {object} [config]
* @param {DisposableStack|AsyncDisposableStack} [config.stack] Optional `DisposableStack` to handle disposal and unregistering.
* @returns {string} The automatically generated key/name of the registered callback
*/
export const createCallback = (callback) => registerCallback('aegis:callback:' + crypto.randomUUID(), callback);
export const createCallback = (callback, { stack } = {}) => registerCallback('aegis:callback:' + crypto.randomUUID(), callback, { stack });

/**
* Call a callback fromt the registry by name/key
*
* @param {string} name The name/key of the registered function
* @param {CallbackRegistryKey|string} name The name/key of the registered function
* @param {...any} args Any arguments to pass along to the function
* @returns {any} Whatever the return value of the function is
* @throws {Error} Throws if callback is not found or any error resulting from calling the function
*/
export function callCallback(name, ...args) {
if (registry.has(name)) {
return registry.get(name).apply(this || globalThis, args);
if (registry.has(name?.toString())) {
return registry.get(name?.toString()).apply(this || globalThis, args);
} else {
throw new Error(`No ${name} function registered.`);
}
Expand All @@ -244,22 +255,33 @@ export function callCallback(name, ...args) {
/**
* Register a named callback in registry
*
* @param {string} name The name/key to register the callback under
* @param {CallbackRegistryKey|string} name The name/key to register the callback under
* @param {Function} callback The callback value to register
* @param {object} config
* @param {DisposableStack|AsyncDisposableStack} [config.stack] Optional `DisposableStack` to handle disposal and unregistering.
* @returns {string} The registered name/key
*/
export function registerCallback(name, callback) {
if (typeof name !== 'string' || name.length === 0) {
throw new TypeError('Callback name must be a string.');
export function registerCallback(name, callback, { stack } = {}) {
if (typeof name === 'string') {
return registerCallback(new CallbackRegistryKey(name), callback, { stack });
}else if (! (name instanceof CallbackRegistryKey)) {
throw new TypeError('Callback name must be a disposable string/CallbackRegistryKey.');
} if (! (callback instanceof Function)) {
throw new TypeError('Callback must be a function.');
} else if (! _isRegistrationOpen) {
throw new TypeError('Cannot register new callbacks because registry is closed.');
} else if (registry.has(name)) {
} else if (registry.has(name?.toString())) {
throw new Error(`Handler "${name}" is already registered.`);
} else if (stack instanceof DisposableStack || stack instanceof AsyncDisposableStack) {
const key = stack.use(new CallbackRegistryKey(name));
registry.set(key.toString(), callback);

return key;
} else {
registry.set(name, callback);
return name;
const key = new CallbackRegistryKey(name);
registry.set(key.toString(), callback);

return key;
}
}

Expand All @@ -283,10 +305,10 @@ export function getHost(target) {
}
}

export function on(event, callback, { capture = false, passive = false, once = false, signal } = {}) {
export function on(event, callback, { capture = false, passive = false, once = false, signal, stack } = {}) {
if (callback instanceof Function) {
return on(event, createCallback(callback), { capture, passive, once, signal });
} else if (typeof callback !== 'string' || callback.length === 0) {
return on(event, createCallback(callback, { stack }), { capture, passive, once, signal });
} else if (! (callback instanceof String || typeof callback === 'string') || callback.length === 0) {
throw new TypeError('Callback must be a function or a registered callback string.');
} else if (typeof event !== 'string' || event.length === 0) {
throw new TypeError('Event must be a non-empty string.');
Expand Down
Loading
Loading