Refactor CPTimer: Remove CPP dependencies and mark legacy anti-patterns

As part of the ongoing technical debt cleanup, this commit modernizes
CPTimer.j by removing its reliance on the C-preprocessor and updating
syntax to modern JavaScript standards.

Key changes:
- Replaced `#define CPTimerDefaultTimeInterval` with a native `const`.
- Replaced `var` with `const`/`let` for proper block scoping and immutability.
- Replaced the build-time `PLATFORM(DOM)` guard with a runtime
  `typeof(window) !== 'undefined'` check, ensuring Foundation does not
  depend on build-time platform flags.
- Added `FIXME` comments to explicitly document architectural anti-patterns:
  - The invasive global override of `window.setTimeout` and `window.setInterval`.
  - The use of `new Function` for string evaluation (CSP violation).
  - The questionable exposure of `CPTimerDefaultTimeInterval` as a fallback.
This commit is contained in:
David Richardson
2026-07-23 15:26:31 -06:00
parent 87c1727ea0
commit a745af8ead
+163 -155
View File
@@ -25,285 +25,293 @@
@import "CPObject.j" @import "CPObject.j"
@import "CPRunLoop.j" @import "CPRunLoop.j"
#define CPTimerDefaultTimeInterval 0.1 // FIXME: Expose CPTimerDefaultTimeInterval via public API or eliminate the fallback behaviour.
const CPTimerDefaultTimeInterval = 0.1;
/*! /*!
@class CPTimer @class CPTimer
@ingroup foundation @ingroup foundation
@brief A timer object that can send a message after the given time interval. @brief A timer object that can send a message after the given time interval.
*/ */
@implementation CPTimer : CPObject @implementation CPTimer : CPObject
{ {
CPTimeInterval _timeInterval; CPTimeInterval _timeInterval;
CPInvocation _invocation; CPInvocation _invocation;
Function _callback; Function _callback;
BOOL _repeats; BOOL _repeats;
BOOL _isValid; BOOL _isValid;
CPDate _fireDate; CPDate _fireDate;
id _userInfo; id _userInfo;
} }
/*! /*!
Returns a new CPTimer object and adds it to the current CPRunLoop object in the default mode. Returns a new CPTimer object and adds it to the current CPRunLoop object in the default mode.
*/ */
+ (CPTimer)scheduledTimerWithTimeInterval:(CPTimeInterval)seconds invocation:(CPInvocation)anInvocation repeats:(BOOL)shouldRepeat + (CPTimer)scheduledTimerWithTimeInterval:(CPTimeInterval)seconds invocation:(CPInvocation)anInvocation repeats:(BOOL)shouldRepeat
{ {
var timer = [[self alloc] initWithFireDate:[CPDate dateWithTimeIntervalSinceNow:seconds] interval:seconds invocation:anInvocation repeats:shouldRepeat]; const timer = [[self alloc] initWithFireDate:[CPDate dateWithTimeIntervalSinceNow:seconds] interval:seconds invocation:anInvocation repeats:shouldRepeat];
[[CPRunLoop currentRunLoop] addTimer:timer forMode:CPDefaultRunLoopMode]; [[CPRunLoop currentRunLoop] addTimer:timer forMode:CPDefaultRunLoopMode];
return timer; return timer;
} }
/*! /*!
Returns a new CPTimer object and adds it to the current CPRunLoop object in the default mode. Returns a new CPTimer object and adds it to the current CPRunLoop object in the default mode.
*/ */
+ (CPTimer)scheduledTimerWithTimeInterval:(CPTimeInterval)seconds target:(id)aTarget selector:(SEL)aSelector userInfo:(id)userInfo repeats:(BOOL)shouldRepeat + (CPTimer)scheduledTimerWithTimeInterval:(CPTimeInterval)seconds target:(id)aTarget selector:(SEL)aSelector userInfo:(id)userInfo repeats:(BOOL)shouldRepeat
{ {
var timer = [[self alloc] initWithFireDate:[CPDate dateWithTimeIntervalSinceNow:seconds] interval:seconds target:aTarget selector:aSelector userInfo:userInfo repeats:shouldRepeat]; const timer = [[self alloc] initWithFireDate:[CPDate dateWithTimeIntervalSinceNow:seconds] interval:seconds target:aTarget selector:aSelector userInfo:userInfo repeats:shouldRepeat];
[[CPRunLoop currentRunLoop] addTimer:timer forMode:CPDefaultRunLoopMode]; [[CPRunLoop currentRunLoop] addTimer:timer forMode:CPDefaultRunLoopMode];
return timer; return timer;
} }
/*! /*!
Returns a new CPTimer object and adds it to the current CPRunLoop object in the default mode. Returns a new CPTimer object and adds it to the current CPRunLoop object in the default mode.
*/ */
+ (CPTimer)scheduledTimerWithTimeInterval:(CPTimeInterval)seconds callback:(Function)aFunction repeats:(BOOL)shouldRepeat + (CPTimer)scheduledTimerWithTimeInterval:(CPTimeInterval)seconds callback:(Function)aFunction repeats:(BOOL)shouldRepeat
{ {
var timer = [[self alloc] initWithFireDate:[CPDate dateWithTimeIntervalSinceNow:seconds] interval:seconds callback:aFunction repeats:shouldRepeat]; const timer = [[self alloc] initWithFireDate:[CPDate dateWithTimeIntervalSinceNow:seconds] interval:seconds callback:aFunction repeats:shouldRepeat];
[[CPRunLoop currentRunLoop] addTimer:timer forMode:CPDefaultRunLoopMode]; [[CPRunLoop currentRunLoop] addTimer:timer forMode:CPDefaultRunLoopMode];
return timer; return timer;
} }
/*! /*!
Returns a new CPTimer that, when added to a run loop, will fire after seconds. Returns a new CPTimer that, when added to a run loop, will fire after seconds.
*/ */
+ (CPTimer)timerWithTimeInterval:(CPTimeInterval)seconds invocation:(CPInvocation)anInvocation repeats:(BOOL)shouldRepeat + (CPTimer)timerWithTimeInterval:(CPTimeInterval)seconds invocation:(CPInvocation)anInvocation repeats:(BOOL)shouldRepeat
{ {
return [[self alloc] initWithFireDate:[CPDate dateWithTimeIntervalSinceNow:seconds] interval:seconds invocation:anInvocation repeats:shouldRepeat]; return [[self alloc] initWithFireDate:[CPDate dateWithTimeIntervalSinceNow:seconds] interval:seconds invocation:anInvocation repeats:shouldRepeat];
} }
/*! /*!
Returns a new CPTimer that, when added to a run loop, will fire after seconds. Returns a new CPTimer that, when added to a run loop, will fire after seconds.
*/ */
+ (CPTimer)timerWithTimeInterval:(CPTimeInterval)seconds target:(id)aTarget selector:(SEL)aSelector userInfo:(id)userInfo repeats:(BOOL)shouldRepeat + (CPTimer)timerWithTimeInterval:(CPTimeInterval)seconds target:(id)aTarget selector:(SEL)aSelector userInfo:(id)userInfo repeats:(BOOL)shouldRepeat
{ {
return [[self alloc] initWithFireDate:[CPDate dateWithTimeIntervalSinceNow:seconds] interval:seconds target:aTarget selector:aSelector userInfo:userInfo repeats:shouldRepeat]; return [[self alloc] initWithFireDate:[CPDate dateWithTimeIntervalSinceNow:seconds] interval:seconds target:aTarget selector:aSelector userInfo:userInfo repeats:shouldRepeat];
} }
/*! /*!
Returns a new CPTimer that, when added to a run loop, will fire after seconds. Returns a new CPTimer that, when added to a run loop, will fire after seconds.
*/ */
+ (CPTimer)timerWithTimeInterval:(CPTimeInterval)seconds callback:(Function)aFunction repeats:(BOOL)shouldRepeat + (CPTimer)timerWithTimeInterval:(CPTimeInterval)seconds callback:(Function)aFunction repeats:(BOOL)shouldRepeat
{ {
return [[self alloc] initWithFireDate:[CPDate dateWithTimeIntervalSinceNow:seconds] interval:seconds callback:aFunction repeats:shouldRepeat]; return [[self alloc] initWithFireDate:[CPDate dateWithTimeIntervalSinceNow:seconds] interval:seconds callback:aFunction repeats:shouldRepeat];
} }
/*! /*!
Initializes a new CPTimer that, when added to a run loop, will fire at date and then, if repeats is YES, every seconds after that. Initializes a new CPTimer that, when added to a run loop, will fire at date and then, if repeats is YES, every seconds after that.
*/ */
- (id)initWithFireDate:(CPDate)aDate interval:(CPTimeInterval)seconds invocation:(CPInvocation)anInvocation repeats:(BOOL)shouldRepeat - (id)initWithFireDate:(CPDate)aDate interval:(CPTimeInterval)seconds invocation:(CPInvocation)anInvocation repeats:(BOOL)shouldRepeat
{ {
self = [super init]; self = [super init];
if (self) if (self)
{ {
_timeInterval = (seconds <= 0) ? CPTimerDefaultTimeInterval : seconds; _timeInterval = (seconds <= 0) ? CPTimerDefaultTimeInterval : seconds;
_invocation = anInvocation; _invocation = anInvocation;
_repeats = shouldRepeat; _repeats = shouldRepeat;
_isValid = YES; _isValid = YES;
_fireDate = aDate; _fireDate = aDate;
} }
return self; return self;
} }
/*! /*!
Initializes a new CPTimer that, when added to a run loop, will fire at date and then, if repeats is YES, every seconds after that. Initializes a new CPTimer that, when added to a run loop, will fire at date and then, if repeats is YES, every seconds after that.
*/ */
- (id)initWithFireDate:(CPDate)aDate interval:(CPTimeInterval)seconds target:(id)aTarget selector:(SEL)aSelector userInfo:(id)userInfo repeats:(BOOL)shouldRepeat - (id)initWithFireDate:(CPDate)aDate interval:(CPTimeInterval)seconds target:(id)aTarget selector:(SEL)aSelector userInfo:(id)userInfo repeats:(BOOL)shouldRepeat
{ {
var invocation = [CPInvocation invocationWithMethodSignature:1]; const invocation = [CPInvocation invocationWithMethodSignature:1];
[invocation setTarget:aTarget]; [invocation setTarget:aTarget];
[invocation setSelector:aSelector]; [invocation setSelector:aSelector];
[invocation setArgument:self atIndex:2]; [invocation setArgument:self atIndex:2];
self = [self initWithFireDate:aDate interval:seconds invocation:invocation repeats:shouldRepeat]; self = [self initWithFireDate:aDate interval:seconds invocation:invocation repeats:shouldRepeat];
if (self) if (self)
_userInfo = userInfo; _userInfo = userInfo;
return self; return self;
} }
/*! /*!
Initializes a new CPTimer that, when added to a run loop, will fire at date and then, if repeats is YES, every seconds after that. Initializes a new CPTimer that, when added to a run loop, will fire at date and then, if repeats is YES, every seconds after that.
*/ */
- (id)initWithFireDate:(CPDate)aDate interval:(CPTimeInterval)seconds callback:(Function)aFunction repeats:(BOOL)shouldRepeat - (id)initWithFireDate:(CPDate)aDate interval:(CPTimeInterval)seconds callback:(Function)aFunction repeats:(BOOL)shouldRepeat
{ {
self = [super init]; self = [super init];
if (self) if (self)
{ {
_timeInterval = (seconds <= 0) ? CPTimerDefaultTimeInterval : seconds; _timeInterval = (seconds <= 0) ? CPTimerDefaultTimeInterval : seconds;
_callback = aFunction; _callback = aFunction;
_repeats = shouldRepeat; _repeats = shouldRepeat;
_isValid = YES; _isValid = YES;
_fireDate = aDate; _fireDate = aDate;
} }
return self; return self;
} }
/*! /*!
Returns the receivers time interval. Returns the receivers time interval.
*/ */
- (CPTimeInterval)timeInterval - (CPTimeInterval)timeInterval
{ {
return _timeInterval; return _timeInterval;
} }
/*! /*!
Returns the date at which the receiver will fire. Returns the date at which the receiver will fire.
*/ */
- (CPDate)fireDate - (CPDate)fireDate
{ {
return _fireDate; return _fireDate;
} }
/*! /*!
Resets the receiver to fire next at a given date. Resets the receiver to fire next at a given date.
*/ */
- (void)setFireDate:(CPDate)aDate - (void)setFireDate:(CPDate)aDate
{ {
_fireDate = aDate; _fireDate = aDate;
} }
/*! /*!
Causes the receivers message to be sent to its target. Causes the receivers message to be sent to its target.
*/ */
- (void)fire - (void)fire
{ {
if (!_isValid) if (!_isValid)
return; return;
if (_callback) if (_callback)
_callback(); _callback();
else else
[_invocation invoke]; [_invocation invoke];
if (!_isValid) if (!_isValid)
return; return;
if (_repeats) if (_repeats)
_fireDate = [CPDate dateWithTimeIntervalSinceNow:_timeInterval]; _fireDate = [CPDate dateWithTimeIntervalSinceNow:_timeInterval];
else
else [self invalidate];
[self invalidate];
} }
/*! /*!
Returns a Boolean value that indicates whether the receiver is currently valid. Returns a Boolean value that indicates whether the receiver is currently valid.
*/ */
- (BOOL)isValid - (BOOL)isValid
{ {
return _isValid; return _isValid;
} }
/*! /*!
Stops the receiver from ever firing again and requests its removal from its CPRunLoop object. Stops the receiver from ever firing again and requests its removal from its CPRunLoop object.
*/ */
- (void)invalidate - (void)invalidate
{ {
_isValid = NO; _isValid = NO;
_userInfo = nil; _userInfo = nil;
_invocation = nil; _invocation = nil;
_callback = nil; _callback = nil;
} }
/*! /*!
Returns the receiver's userInfo object. Returns the receiver's userInfo object.
*/ */
- (id)userInfo - (id)userInfo
{ {
return _userInfo; return _userInfo;
} }
@end @end
var CPTimersTimeoutID = 1000, // FIXME: Anti-pattern: Global DOM Override. This section invasively overrides global DOM timing
CPTimersForTimeoutIDs = {}; // functions (window.setTimeout, setInterval) to force external execution through CPRunLoop.
// This deep coupling creates unpredictable side effects for third-party libraries and should
// be replaced with a non-invasive run loop integration strategy.
let CPTimersTimeoutID = 1000;
var _CPTimerBridgeTimer = function(codeOrFunction, aDelay, shouldRepeat, functionArgs) // FIXME: Anti-pattern: Manual Global Tracking. Tracking bridged DOM timers in a global map like
// this is brittle and prone to memory leaks in long-running processes.
const CPTimersForTimeoutIDs = {};
const _CPTimerBridgeTimer = function(codeOrFunction, aDelay, shouldRepeat, functionArgs)
{ {
var timeoutID = CPTimersTimeoutID++, const timeoutID = CPTimersTimeoutID++;
theFunction = nil; let theFunction = nil;
if (typeof codeOrFunction === "string") if (typeof codeOrFunction === "string")
{ {
theFunction = function() // FIXME: Anti-pattern: Dynamic Evaluation. Evaluating string payloads via `new Function`
{ // is a strict Content Security Policy (CSP) violation.
new Function(codeOrFunction)(); theFunction = function()
{
new Function(codeOrFunction)();
if (!shouldRepeat) if (!shouldRepeat)
CPTimersForTimeoutIDs[timeoutID] = nil; delete CPTimersForTimeoutIDs[timeoutID];
} }
} }
else else
{ {
if (!functionArgs) if (!functionArgs)
functionArgs = []; functionArgs = [];
theFunction = function() theFunction = function()
{ {
codeOrFunction.apply(window, functionArgs); codeOrFunction.apply(window, functionArgs);
if (!shouldRepeat) if (!shouldRepeat)
CPTimersForTimeoutIDs[timeoutID] = nil; delete CPTimersForTimeoutIDs[timeoutID];
} }
} }
// A call such as setTimeout(f) is technically invalid but browsers seem to treat it as setTimeout(f, 0), so so will we. // A call such as setTimeout(f) is technically invalid but browsers seem to treat it as setTimeout(f, 0), so so will we.
aDelay = aDelay | 0.0; aDelay = aDelay | 0.0;
CPTimersForTimeoutIDs[timeoutID] = [CPTimer scheduledTimerWithTimeInterval:aDelay / 1000 callback:theFunction repeats:shouldRepeat]; CPTimersForTimeoutIDs[timeoutID] = [CPTimer scheduledTimerWithTimeInterval:aDelay / 1000 callback:theFunction repeats:shouldRepeat];
return timeoutID; return timeoutID;
}; };
// Avoid "TypeError: Result of expression 'window' [undefined] is not an object" when running unit tests. // Avoid "TypeError: Result of expression 'window' [undefined] is not an object" when running unit tests.
// We can't use a regular PLATFORM(DOM) check because that platform constant is not defined in Foundation.
if (typeof(window) !== 'undefined') if (typeof(window) !== 'undefined')
{ {
window.setTimeout = function(codeOrFunction, aDelay) window.setTimeout = function(codeOrFunction, aDelay)
{ {
return _CPTimerBridgeTimer(codeOrFunction, aDelay, NO, Array.prototype.slice.apply(arguments, [2])); return _CPTimerBridgeTimer(codeOrFunction, aDelay, NO, Array.prototype.slice.apply(arguments, [2]));
}; };
window.clearTimeout = function(aTimeoutID) window.clearTimeout = function(aTimeoutID)
{ {
var timer = CPTimersForTimeoutIDs[aTimeoutID]; const timer = CPTimersForTimeoutIDs[aTimeoutID];
if (timer) if (timer)
[timer invalidate]; [timer invalidate];
CPTimersForTimeoutIDs[aTimeoutID] = nil; delete CPTimersForTimeoutIDs[aTimeoutID];
}; };
window.setInterval = function(codeOrFunction, aDelay, functionArgs) window.setInterval = function(codeOrFunction, aDelay, functionArgs)
{ {
return _CPTimerBridgeTimer(codeOrFunction, aDelay, YES, Array.prototype.slice.apply(arguments, [2])); return _CPTimerBridgeTimer(codeOrFunction, aDelay, YES, Array.prototype.slice.apply(arguments, [2]));
}; };
window.clearInterval = function(aTimeoutID) window.clearInterval = function(aTimeoutID)
{ {
window.clearTimeout(aTimeoutID); window.clearTimeout(aTimeoutID);
}; };
} }