Skip to content

Commit

Permalink
v1.13.0.0: Add LISTENER wrapper type
Browse files Browse the repository at this point in the history
Also fixes bug related to OVERRIDE wrappers and options.bind,
plus miscellaneous code cleanup.
  • Loading branch information
ruipin committed Sep 26, 2024
1 parent 48a86ab commit 8e36d03
Show file tree
Hide file tree
Showing 16 changed files with 261 additions and 116 deletions.
13 changes: 13 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,16 @@
# 1.13.0.0 (2024-09-17)

- Implement support for listeners, i.e. functions that are called immediately before the target method is called, but aren't part of the usual call chain.
- Use when you just need to know a method is being called and the parameters used for the call.
- Listeners are always called before any other wrapper types, but are not able to modify the parameters or wait until the target method completes execution.
- You can register listeners using the libWrapper.LISTENER wrapper type.
- Note that asynchronous listeners are *not* awaited before execution is allowed to proceed.
- Because listeners are not part of the call chain and therefore do not provide the `wrapped` parameter.

- Internal libWrapper wrappers (such as `Game.initialize` and `Hooks.onError`) now use listeners where possible, which reduces call stack pollution.

- Fix broken argument passing when using `options.bind` with `OVERRIDE` wrappers.

# 1.12.15.0 (2024-08-13)

- Fixes to error message involved packages injection to ensure more errors display this information.
Expand Down
30 changes: 25 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ Library for [Foundry VTT](https://foundryvtt.com/) which provides package develo
- [1.3.1. Summary](#131-summary)
- [1.3.2. Common Issues and Pitfalls](#132-common-issues-and-pitfalls)
- [1.3.2.1. Not allowed to register wrappers before the `init` hook.](#1321-not-allowed-to-register-wrappers-before-the-init-hook)
- [1.3.2.2. OVERRIDE wrappers have a different call signature](#1322-override-wrappers-have-a-different-call-signature)
- [1.3.2.2. LISTENER and OVERRIDE wrappers have a different call signature](#1322-listener-and-override-wrappers-have-a-different-call-signature)
- [1.3.2.3. Arrow Functions do not support `this`](#1323-arrow-functions-do-not-support-this)
- [1.3.2.4. Using `super` inside wrappers](#1324-using-super-inside-wrappers)
- [1.3.2.5. Patching Mixins](#1325-patching-mixins)
Expand Down Expand Up @@ -164,10 +164,14 @@ See [CONTRIBUTING.md](CONTRIBUTING.md).
In order to wrap a method, you should call the `libWrapper.register` method during or after the `init` hook, and provide it with your package ID, the scope of the method you want to override, and a wrapper function.
You can also specify the type of wrapper you want in the fourth (optional) parameter:
- `LISTENER`:
- Use when you just need to know a method is being called and the parameters used for the call, without needing to modify the parameters or execute any code after the method finishes execution.
- Listeners will always be called first, before any other type, and should be used whenever possible as they have a virtually zero chance of conflict.
- `WRAPPER`:
- Use if your wrapper will *always* continue the chain (i.e. call `wrapped`).
- This type has priority over every other type. It should be used whenever possible as it massively reduces the likelihood of conflicts.
- This type has priority over `MIXED` and `OVERRIDE` wrappers. It should be used instead of those two when possible, as it massively reduces the chance of conflict.
- ⚠ If you use this type but do not call the original function, your wrapper will be automatically unregistered.
- `MIXED` (default):
Expand Down Expand Up @@ -206,9 +210,9 @@ Any attempts to register wrappers before then will throw an exception. If using
⚠ Note that while the full library provides the `libWrapper.Ready` hook, which fires as soon as libWrapper is ready to register wrappers, this hook is not provided by the [shim](#135-compatibility-shim).
#### 1.3.2.2. OVERRIDE wrappers have a different call signature
#### 1.3.2.2. LISTENER and OVERRIDE wrappers have a different call signature
When using `OVERRIDE`, wrappers do not receive the next function in the wrapper chain as the first parameter. Make sure to account for this.
When using `LISTENER` or `OVERRIDE`, wrappers do not receive the next function in the wrapper chain as the first parameter. Make sure to account for this.
```javascript
libWrapper.register('my-fvtt-package', 'Foo.prototype.bar', function (...args) { // There is no 'wrapped' parameter in the wrapper signature
Expand All @@ -217,6 +221,13 @@ libWrapper.register('my-fvtt-package', 'Foo.prototype.bar', function (...args) {
}, 'OVERRIDE');
```
```javascript
libWrapper.register('my-fvtt-package', 'Foo.prototype.bar', function (...args) { // There is no 'wrapped' parameter in the wrapper signature
console.log('Foo.prototype.bar was called');
return;
}, 'LISTENER');
```
#### 1.3.2.3. Arrow Functions do not support `this`
Expand Down Expand Up @@ -345,9 +356,17 @@ To register a wrapper function, you should call the method `libWrapper.register(
*
* The possible types are:
*
* 'LISTENER' / libWrapper.LISTENER:
* Use this to register a listener function. This function will be called immediately before the target is called, but is not part of the call chain.
* Use when you just need to know a method is being called and the parameters used for the call, without needing to modify the parameters or execute any
* code after the method finishes execution.
* Listeners will always be called first, before any other type, and should be used whenever possible as they have a virtually zero chance of conflict.
* Note that asynchronous listeners are *not* awaited before execution is allowed to proceed.
* First introduced in v1.13.0.0.
*
* 'WRAPPER' / libWrapper.WRAPPER:
* Use if your wrapper will *always* continue the chain.
* This type has priority over every other type. It should be used whenever possible as it massively reduces the likelihood of conflicts.
* This type has priority over MIXED and OVERRIDE. It should be preferred over those whenever possible as it massively reduces the likelihood of conflicts.
* Note that the library will auto-detect if you use this type but do not call the original function, and automatically unregister your wrapper.
*
* 'MIXED' / libWrapper.MIXED:
Expand Down Expand Up @@ -687,6 +706,7 @@ A full list of the enumeration values provided by libWrapper follows:
static get WRAPPER() { /* ... */ };
static get MIXED() { /* ... */ };
static get OVERRIDE() { /* ... */ };
static get LISTENER() { /* ... */ };

static get PERF_NORMAL() { /* ... */ };
static get PERF_AUTO() { /* ... */ };
Expand Down
2 changes: 1 addition & 1 deletion module.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"name": "lib-wrapper",
"title": "libWrapper",
"description": "Library for wrapping core Foundry VTT methods, meant to improve compatibility between packages that wrap the same methods.",
"version": "1.12.15.0",
"version": "1.13.0.0",
"authors": [{
"name": "Rui Pinheiro",
"url": "https://github.com/ruipin"
Expand Down
2 changes: 1 addition & 1 deletion shim/SHIM.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ This section contains a list of the most significant differences between the shi

4. This shim does not support dynamic dispatch. The next method in the wrapper chain is calculated at the time of each `register` call, and never changed/reordered later. This has many implications:

1. The wrapper type metadata (`WRAPPER`, `MIXED`, `OVERRIDE`) is completely ignored. Unlike the full library, nothing guarantees `MIXED` wrappers come after all `WRAPPER` wrappers, nor that `OVERRIDE` wrappers come after all `MIXED` wrappers. The wrapper call order will match the order in which they are registered. For instance, if a module registers an `OVERRIDE`, previously registered wrappers (`OVERRIDE` or not) will never be called.
1. The wrapper type metadata (`WRAPPER`, `MIXED`, `OVERRIDE`, `LISTENER`) is completely ignored. Unlike the full library, nothing guarantees `MIXED` wrappers come after all `WRAPPER` wrappers, nor that `OVERRIDE` wrappers come after all `MIXED` wrappers. The wrapper call order will match the order in which they are registered. For instance, if a module registers an `OVERRIDE`, previously registered wrappers (`OVERRIDE` or not) will never be called.

2. Inheritance chains are static and calculated at `register` time. For instance, if there is `class B extends A` and a module overrides `B.prototype.foo` before another overrides `A.prototype.foo`, calling `B.prototype.foo` will skip the `A.prototype.foo` wrapper.

Expand Down
11 changes: 8 additions & 3 deletions shim/shim.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
// A shim for the libWrapper library
export let libWrapper = undefined;

export const VERSIONS = [1,12,2];
export const VERSIONS = [1,13,0];
export const TGT_SPLIT_RE = new RegExp("([^.[]+|\\[('([^'\\\\]|\\\\.)+?'|\"([^\"\\\\]|\\\\.)+?\")\\])", 'g');
export const TGT_CLEANUP_RE = new RegExp("(^\\['|'\\]$|^\\[\"|\"\\]$)", 'g');

Expand All @@ -26,6 +26,7 @@ Hooks.once('init', () => {
static get WRAPPER() { return 'WRAPPER' };
static get MIXED() { return 'MIXED' };
static get OVERRIDE() { return 'OVERRIDE' };
static get LISTENER() { return 'LISTENER' };

static register(package_id, target, fn, type="MIXED", {chain=undefined, bind=[]}={}) {
const is_setter = target.endsWith('#set');
Expand Down Expand Up @@ -54,10 +55,14 @@ Hooks.once('init', () => {
if(!descriptor || descriptor?.configurable === false) throw new Error(`libWrapper Shim: '${target}' does not exist, could not be found, or has a non-configurable descriptor.`);

let original = null;
const wrapper = (chain ?? (type.toUpperCase?.() != 'OVERRIDE' && type != 3)) ?
const is_override = (type == 3 || type.toUpperCase?.() == 'OVERRIDE' || type == 3);
const is_listener = (type == 4 || type.toUpperCase?.() == 'LISTENER' || type == 4);
const wrapper = is_listener ? (
function(...args) { fn.call(this, ...bind, ...args); return original.call(this, ...args); }
) : ((chain ?? !is_override) ?
function(...args) { return fn.call(this, original.bind(this), ...bind, ...args); } :
function(...args) { return fn.call(this, ...bind, ...args); }
;
);

if(!is_setter) {
if(descriptor.value) {
Expand Down
7 changes: 2 additions & 5 deletions src/errors/listeners.js
Original file line number Diff line number Diff line change
Expand Up @@ -130,15 +130,12 @@ function init_hooksOnError_listener() {
// Wrap Hooks._onError to intercept unhandled exceptions
// We could use the 'error' hook instead, but then we wouldn't be able to see an exception before it gets logged to the console
try {
libWrapper.register('lib-wrapper', 'Hooks.onError', function(wrapped, ...args) {
libWrapper.register('lib-wrapper', 'Hooks.onError', function(...args) {
// Handle error ourselves first
const err = args[1];
const msg = args?.[2]?.msg;
onUnhandledError(err, msg);

// Let Foundry do its thing after
return wrapped(...args);
}, 'WRAPPER', {perf_mode: 'FAST'});
}, 'LISTENER', {perf_mode: 'FAST'});
}
catch(e) {
// Handle a possible error gracefully
Expand Down
34 changes: 23 additions & 11 deletions src/lib/api.js
Original file line number Diff line number Diff line change
Expand Up @@ -174,7 +174,10 @@ function _find_wrapper_by_id(id) {
}

function _find_package_data_in_wrapper(package_info, wrapper, is_setter) {
return wrapper.get_fn_data(is_setter).find((x) => x.package_info?.equals(package_info));
let result = wrapper.get_fn_data(is_setter, /*is_listener=*/ false).find((x) => x.package_info?.equals(package_info));
if(!result)
result = wrapper.get_fn_data(is_setter, /*is_listener=*/ true ).find((x) => x.package_info?.equals(package_info));
return result;
}

function _find_package_data_with_target(package_info, target) {
Expand Down Expand Up @@ -353,6 +356,7 @@ export class libWrapper {
static get WRAPPER() { return WRAPPER_TYPES.WRAPPER };
static get MIXED() { return WRAPPER_TYPES.MIXED };
static get OVERRIDE() { return WRAPPER_TYPES.OVERRIDE };
static get LISTENER() { return WRAPPER_TYPES.LISTENER };

static get PERF_NORMAL() { return PERF_MODES.NORMAL };
static get PERF_AUTO() { return PERF_MODES.AUTO };
Expand Down Expand Up @@ -408,9 +412,17 @@ export class libWrapper {
*
* The possible types are:
*
* 'LISTENER' / libWrapper.LISTENER:
* Use this to register a listener function. This function will be called immediately before the target is called, but is not part of the call chain.
* Use when you just need to know a method is being called and the parameters used for the call, without needing to modify the parameters or execute any
* code after the method finishes execution.
* Listeners will always be called first, before any other type, and should be used whenever possible as they have a virtually zero chance of conflict.
* Note that asynchronous listeners are *not* awaited before execution is allowed to proceed.
* First introduced in v1.13.0.0.
*
* 'WRAPPER' / libWrapper.WRAPPER:
* Use if your wrapper will *always* continue the chain.
* This type has priority over every other type. It should be used whenever possible as it massively reduces the likelihood of conflicts.
* This type has priority over MIXED and OVERRIDE. It should be preferred over those whenever possible as it massively reduces the likelihood of conflicts.
* Note that the library will auto-detect if you use this type but do not call the original function, and automatically unregister your wrapper.
*
* 'MIXED' / libWrapper.MIXED:
Expand Down Expand Up @@ -485,11 +497,13 @@ export class libWrapper {
if(type === null)
throw new ERRORS.package(`Parameter 'type' must be one of [${WRAPPER_TYPES.list.join(', ')}].`, package_info);

const chain = options?.chain ?? (type.value < WRAPPER_TYPES.OVERRIDE.value);
const chain = options?.chain ?? (type !== WRAPPER_TYPES.OVERRIDE && type !== WRAPPER_TYPES.LISTENER);
if(typeof chain !== 'boolean')
throw new ERRORS.package(`Parameter 'options.chain' must be a boolean.`, package_info);
if(!chain && type.value < WRAPPER_TYPES.OVERRIDE.value)
throw new ERRORS.package(`Parameter 'options.chain' must be 'true' for non-OVERRIDE wrappers.`, package_info);
if(!chain && (type === WRAPPER_TYPES.WRAPPER || type === WRAPPER_TYPES.MIXED))
throw new ERRORS.package(`Parameter 'options.chain' must be 'true' for ${type.name} wrappers.`, package_info);
if(chain && (type === WRAPPER_TYPES.LISTENER))
throw new ERRORS.package(`Parameter 'options.chain' must be 'false' for ${type.name} wrappers.`, package_info);

if(IS_UNITTEST && FORCE_FAST_MODE)
options.perf_mode = 'FAST';
Expand Down Expand Up @@ -550,7 +564,7 @@ export class libWrapper {
LibWrapperStats.register_package(package_info);

// Only allow one 'OVERRIDE' type
if(type.value >= WRAPPER_TYPES.OVERRIDE.value) {
if(type === WRAPPER_TYPES.OVERRIDE) {
const existing = wrapper.get_fn_data(is_setter).find((x) => { return x.type === WRAPPER_TYPES.OVERRIDE });

if(existing) {
Expand Down Expand Up @@ -777,7 +791,7 @@ init_error_listeners();

const libWrapperInit = decorate_name('libWrapperInit');
const obj = {
[libWrapperInit]: async function(wrapped, ...args) {
[libWrapperInit]: function(...args) {
// Unregister our pre-initialisation patches as they are no longer necessary
if(!IS_UNITTEST) {
const lw_info = new PackageInfo('lib-wrapper', PACKAGE_TYPES.MODULE);
Expand All @@ -792,7 +806,7 @@ init_error_listeners();
parse_manifest_version();
//#endif

await i18n.init();
i18n.init(); // async method, but we don't await on purpose - we need to initialise everything before FVTT initializes
LibWrapperSettings.init();
LibWrapperStats.init();
LibWrapperConflicts.init();
Expand All @@ -801,13 +815,11 @@ init_error_listeners();
// Notify everyone the library has loaded and is ready to start registering wrappers
Log.fn(Log.ALWAYS, /*fn_verbosity=*/ Log.INFO)(`Version ${VERSION.full_git} ready.`);
Hooks.callAll(`${HOOKS_SCOPE}.Ready`, libWrapper);

return wrapped(...args);
}
};

if(!IS_UNITTEST) {
GAME_INITIALIZE_ID = libWrapper.register('lib-wrapper', 'Game.prototype.initialize', obj[libWrapperInit], libWrapper.WRAPPER, {perf_mode: libWrapper.PERF_FAST});
GAME_INITIALIZE_ID = libWrapper.register('lib-wrapper', 'Game.prototype.initialize', obj[libWrapperInit], libWrapper.LISTENER, {perf_mode: libWrapper.PERF_FAST});

// We need to prevent people patching 'Game' and breaking libWrapper.
// Unfortunately we cannot re-define 'Game' as a non-settable property, but we can prevent people from using 'Game.toString'.
Expand Down
3 changes: 2 additions & 1 deletion src/lib/enums.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@ import {Enum} from '../shared/enums.js';
export const WRAPPER_TYPES = Enum('WrapperType', {
'WRAPPER' : 1,
'MIXED' : 2,
'OVERRIDE': 3
'OVERRIDE': 3,
'LISTENER': 4
});


Expand Down
Loading

0 comments on commit 8e36d03

Please sign in to comment.