Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add strict null checks to @pixi/ticker #10087

Open
wants to merge 4 commits into
base: v7.x
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
47 changes: 36 additions & 11 deletions packages/ticker/src/Ticker.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import { UPDATE_PRIORITY } from './const';
import { TickerListener } from './TickerListener';

export type TickerCallback<T> = (this: T, dt: number) => any;
import type { TickerCallback, TickerListenerContext } from './TickerListener';

export type { TickerCallback };

/**
* A Ticker class that runs an update loop that other objects listen to.
Expand Down Expand Up @@ -85,9 +87,9 @@ export class Ticker
public started = false;

/** The first listener. All new listeners added are chained on this. */
private _head: TickerListener;
private _head: TickerListener | null;
/** Internal current frame request ID */
private _requestId: number = null;
private _requestId: number | null = null;
/**
* Internal value managed by minFPS property setter and getter.
* This is the maximum allowed milliseconds between updates.
Expand Down Expand Up @@ -128,7 +130,7 @@ export class Ticker
// Invoke listeners now
this.update(time);
// Listener side effects may have modified ticker state.
if (this.started && this._requestId === null && this._head.next)
if (this.started && this._requestId === null && this._head?.next)
{
this._requestId = requestAnimationFrame(this._tick);
}
Expand All @@ -144,7 +146,7 @@ export class Ticker
*/
private _requestIfNeeded(): void
{
if (this._requestId === null && this._head.next)
if (this._requestId === null && this._head?.next)
{
// ensure callbacks get correct delta
this.lastTime = performance.now();
Expand Down Expand Up @@ -195,7 +197,11 @@ export class Ticker
* @param {number} [priority=PIXI.UPDATE_PRIORITY.NORMAL] - The priority for emitting
* @returns This instance of a ticker
*/
add<T = any>(fn: TickerCallback<T>, context?: T, priority = UPDATE_PRIORITY.NORMAL): this
add<T extends TickerListenerContext = null>(
fn: TickerCallback<T>,
context?: T,
priority = UPDATE_PRIORITY.NORMAL
): this
{
return this._addListener(new TickerListener(fn, context, priority));
}
Expand All @@ -207,7 +213,11 @@ export class Ticker
* @param {number} [priority=PIXI.UPDATE_PRIORITY.NORMAL] - The priority for emitting
* @returns This instance of a ticker
*/
addOnce<T = any>(fn: TickerCallback<T>, context?: T, priority = UPDATE_PRIORITY.NORMAL): this
addOnce<T extends TickerListenerContext = null>(
fn: TickerCallback<T>,
context?: T,
priority = UPDATE_PRIORITY.NORMAL
): this
{
return this._addListener(new TickerListener(fn, context, priority, true));
}
Expand All @@ -220,8 +230,13 @@ export class Ticker
* @param listener - Current listener being added.
* @returns This instance of a ticker
*/
private _addListener(listener: TickerListener): this
private _addListener<T extends TickerListenerContext>(listener: TickerListener<T>): this
{
if (!this._head)
{
return this;
}

// For attaching to head
let current = this._head.next;
let previous = this._head;
Expand Down Expand Up @@ -264,8 +279,13 @@ export class Ticker
* @param context - The listener context to be removed
* @returns This instance of a ticker
*/
remove<T = any>(fn: TickerCallback<T>, context?: T): this
remove<T extends TickerListenerContext = null>(fn: TickerCallback<T>, context?: T): this
{
if (!this._head)
{
return this;
}

let listener = this._head.next;

while (listener)
Expand Down Expand Up @@ -304,7 +324,7 @@ export class Ticker
}

let count = 0;
let current = this._head;
let current: TickerListener | null = this._head;

while ((current = current.next))
{
Expand Down Expand Up @@ -337,7 +357,7 @@ export class Ticker
/** Destroy the ticker and don't use after this. Calling this method removes all references to internal events. */
destroy(): void
{
if (!this._protected)
if (!this._protected && this._head)
{
this.stop();

Expand Down Expand Up @@ -419,6 +439,11 @@ export class Ticker
// during the emit, we can still check for head.next
const head = this._head;

if (!head)
{
return;
}
Comment on lines 440 to +445
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe it make sense to check this.head, and only after create a variable ?

Suggested change
const head = this._head;
if (!head)
{
return;
}
if (!this._head)
{
return;
}
const head = this._head;


// Invoke listeners added to internal emitter
let listener = head.next;

Expand Down
48 changes: 25 additions & 23 deletions packages/ticker/src/TickerListener.ts
Original file line number Diff line number Diff line change
@@ -1,26 +1,24 @@
import type { TickerCallback } from './Ticker';
export type TickerListenerContext = Record<string, any> | null;

export type TickerCallback<
T extends TickerListenerContext = TickerListenerContext
> =
| ((deltaTime: number) => void)
| (T extends null ? never : (this: T, deltaTime: number) => void);

/**
* Internal class for handling the priority sorting of ticker handlers.
* @private
* @class
* @memberof PIXI
*/
export class TickerListener<T = any>
export class TickerListener<T extends TickerListenerContext = TickerListenerContext>
{
/** The current priority. */
public priority: number;
/** The next item in chain. */
public next: TickerListener = null;
public next: TickerListener | null = null;
/** The previous item in chain. */
public previous: TickerListener = null;

/** The handler function to execute. */
private fn: TickerCallback<T>;
/** The calling to execute. */
private context: T;
/** If this should only execute once. */
private once: boolean;
public previous: TickerListener | null = null;

/** `true` if this listener has been destroyed already. */
private _destroyed = false;

Expand All @@ -32,12 +30,16 @@ export class TickerListener<T = any>
* @param priority - The priority for emitting
* @param once - If the handler should fire once
*/
constructor(fn: TickerCallback<T>, context: T = null, priority = 0, once = false)
constructor(
/** The handler function to execute. */
private fn: TickerCallback<T> | null,
/** The calling to execute. */
private context: T | null = null,
/** The current priority. */
public priority = 0,
/** If this should only execute once. */
private once = false)
{
this.fn = fn;
this.context = context;
this.priority = priority;
this.once = once;
}

/**
Expand All @@ -47,7 +49,7 @@ export class TickerListener<T = any>
* @param context - The listener context
* @returns `true` if the listener match the arguments
*/
match(fn: TickerCallback<T>, context: any = null): boolean
match(fn: TickerCallback, context: any = null): boolean
{
return this.fn === fn && this.context === context;
}
Expand All @@ -58,17 +60,17 @@ export class TickerListener<T = any>
* @param deltaTime - time since the last emit.
* @returns Next ticker
*/
emit(deltaTime: number): TickerListener
emit(deltaTime: number): TickerListener | null
{
if (this.fn)
{
if (this.context)
if (this.context !== null)
{
this.fn.call(this.context, deltaTime);
}
else
{
(this as TickerListener<any>).fn(deltaTime);
this.fn(deltaTime);
}
}

Expand Down Expand Up @@ -112,7 +114,7 @@ export class TickerListener<T = any>
* is considered a hard destroy. Soft destroy maintains the next reference.
* @returns The listener to redirect while emitting or removing.
*/
destroy(hard = false): TickerListener
destroy(hard = false): TickerListener | null
{
this._destroyed = true;
this.fn = null;
Expand Down
8 changes: 4 additions & 4 deletions packages/ticker/src/TickerPlugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,8 @@ export class TickerPlugin

static start: () => void;
static stop: () => void;
static _ticker: Ticker;
static ticker: Ticker;
static _ticker: Ticker | null;
static ticker: Ticker | null;

/**
* Initialize the plugin with scope of application instance
Expand Down Expand Up @@ -82,7 +82,7 @@ export class TickerPlugin
*/
this.stop = (): void =>
{
this._ticker.stop();
this._ticker?.stop();
};

/**
Expand All @@ -93,7 +93,7 @@ export class TickerPlugin
*/
this.start = (): void =>
{
this._ticker.start();
this._ticker?.start();
};

/**
Expand Down
4 changes: 3 additions & 1 deletion packages/ticker/test/Ticker.tests.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { Ticker, UPDATE_PRIORITY } from '@pixi/ticker';

import type { TickerListener } from '../src/TickerListener';

const { shared, system } = Ticker;

describe('Ticker', () =>
Expand All @@ -17,7 +19,7 @@ describe('Ticker', () =>
return 0;
}

let listener = ticker['_head'].next;
let listener: TickerListener | null = ticker['_head'].next;
let i = 0;

while (listener)
Expand Down
18 changes: 9 additions & 9 deletions packages/ticker/test/TickerPlugin.tests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,25 +4,25 @@ describe('TickerPlugin', () =>
{
interface App
{
_ticker?: Ticker;
ticker?: Ticker;
_ticker?: Ticker | null;
ticker?: Ticker | null;
render?(): void;
start?(): void;
}
let app: App;

it('should not start application before calling start method if options.autoStart is false', (done) =>
{
const appp = {} as App;
const appp: App = {};

TickerPlugin.init.call(appp, { autoStart: false });

expect(appp.ticker).toBeInstanceOf(Ticker);
expect(appp.ticker.started).toBe(false);
expect(appp.ticker?.started).toBe(false);
Comment on lines 20 to +21
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry, maybe I don't get this part, but it feels this 2 lines have conflicting logic, no ?


appp.start();
appp.start?.();

appp.ticker.addOnce(() =>
appp.ticker?.addOnce(() =>
{
TickerPlugin.destroy.call(appp);
done();
Expand All @@ -41,7 +41,7 @@ describe('TickerPlugin', () =>
};
TickerPlugin.init.call(app);
/* remove default listener to prevent uncaught exception */
app._ticker.remove(app.render, app);
app._ticker?.remove(app.render as () => void, app);
});

afterAll(() =>
Expand All @@ -67,10 +67,10 @@ describe('TickerPlugin', () =>

it('should assign ticker if no ticker', () =>
{
const ticker = { add: jest.fn() };
const ticker = { add: jest.fn() } as unknown as Ticker;

app._ticker = null;
app.ticker = ticker as unknown as Ticker;
app.ticker = ticker;

expect(app._ticker).toEqual(ticker);
expect(ticker.add).toHaveBeenCalledOnce();
Expand Down
2 changes: 1 addition & 1 deletion scripts/filterTypeScriptErrors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ const pathPrefixs = [
// 'packages/text/',
// 'packages/text-bitmap/',
'packages/text-html/',
// 'packages/ticker/',
'packages/ticker/',
'packages/unsafe-eval/',
'packages/utils/',
'scripts/',
Expand Down