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 "CPRunLoop.j"
#define CPTimerDefaultTimeInterval 0.1
// FIXME: Expose CPTimerDefaultTimeInterval via public API or eliminate the fallback behaviour.
const CPTimerDefaultTimeInterval = 0.1;
/*!
@class CPTimer
@ingroup foundation
@class CPTimer
@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
{
CPTimeInterval _timeInterval;
CPInvocation _invocation;
Function _callback;
CPTimeInterval _timeInterval;
CPInvocation _invocation;
Function _callback;
BOOL _repeats;
BOOL _isValid;
CPDate _fireDate;
id _userInfo;
BOOL _repeats;
BOOL _isValid;
CPDate _fireDate;
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
{
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
{
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
{
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
{
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
{
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
{
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
{
self = [super init];
self = [super init];
if (self)
{
_timeInterval = (seconds <= 0) ? CPTimerDefaultTimeInterval : seconds;
_invocation = anInvocation;
_repeats = shouldRepeat;
_isValid = YES;
_fireDate = aDate;
}
if (self)
{
_timeInterval = (seconds <= 0) ? CPTimerDefaultTimeInterval : seconds;
_invocation = anInvocation;
_repeats = shouldRepeat;
_isValid = YES;
_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
{
var invocation = [CPInvocation invocationWithMethodSignature:1];
const invocation = [CPInvocation invocationWithMethodSignature:1];
[invocation setTarget:aTarget];
[invocation setSelector:aSelector];
[invocation setArgument:self atIndex:2];
[invocation setTarget:aTarget];
[invocation setSelector:aSelector];
[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)
_userInfo = userInfo;
if (self)
_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
{
self = [super init];
self = [super init];
if (self)
{
_timeInterval = (seconds <= 0) ? CPTimerDefaultTimeInterval : seconds;
_callback = aFunction;
_repeats = shouldRepeat;
_isValid = YES;
_fireDate = aDate;
}
if (self)
{
_timeInterval = (seconds <= 0) ? CPTimerDefaultTimeInterval : seconds;
_callback = aFunction;
_repeats = shouldRepeat;
_isValid = YES;
_fireDate = aDate;
}
return self;
return self;
}
/*!
Returns the receivers time interval.
*/
Returns the receivers time interval.
*/
- (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
{
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
{
_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
{
if (!_isValid)
return;
if (!_isValid)
return;
if (_callback)
_callback();
else
[_invocation invoke];
if (_callback)
_callback();
else
[_invocation invoke];
if (!_isValid)
return;
if (!_isValid)
return;
if (_repeats)
_fireDate = [CPDate dateWithTimeIntervalSinceNow:_timeInterval];
else
[self invalidate];
if (_repeats)
_fireDate = [CPDate dateWithTimeIntervalSinceNow:_timeInterval];
else
[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
{
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
{
_isValid = NO;
_userInfo = nil;
_invocation = nil;
_callback = nil;
_isValid = NO;
_userInfo = nil;
_invocation = nil;
_callback = nil;
}
/*!
Returns the receiver's userInfo object.
*/
Returns the receiver's userInfo object.
*/
- (id)userInfo
{
return _userInfo;
return _userInfo;
}
@end
var CPTimersTimeoutID = 1000,
CPTimersForTimeoutIDs = {};
// FIXME: Anti-pattern: Global DOM Override. This section invasively overrides global DOM timing
// 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++,
theFunction = nil;
const timeoutID = CPTimersTimeoutID++;
let theFunction = nil;
if (typeof codeOrFunction === "string")
{
theFunction = function()
{
new Function(codeOrFunction)();
if (typeof codeOrFunction === "string")
{
// FIXME: Anti-pattern: Dynamic Evaluation. Evaluating string payloads via `new Function`
// is a strict Content Security Policy (CSP) violation.
theFunction = function()
{
new Function(codeOrFunction)();
if (!shouldRepeat)
CPTimersForTimeoutIDs[timeoutID] = nil;
}
}
else
{
if (!functionArgs)
functionArgs = [];
if (!shouldRepeat)
delete CPTimersForTimeoutIDs[timeoutID];
}
}
else
{
if (!functionArgs)
functionArgs = [];
theFunction = function()
{
codeOrFunction.apply(window, functionArgs);
theFunction = function()
{
codeOrFunction.apply(window, functionArgs);
if (!shouldRepeat)
CPTimersForTimeoutIDs[timeoutID] = nil;
}
}
if (!shouldRepeat)
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.
aDelay = aDelay | 0.0;
// 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;
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.
// We can't use a regular PLATFORM(DOM) check because that platform constant is not defined in Foundation.
if (typeof(window) !== 'undefined')
{
window.setTimeout = function(codeOrFunction, aDelay)
{
return _CPTimerBridgeTimer(codeOrFunction, aDelay, NO, Array.prototype.slice.apply(arguments, [2]));
};
window.setTimeout = function(codeOrFunction, aDelay)
{
return _CPTimerBridgeTimer(codeOrFunction, aDelay, NO, Array.prototype.slice.apply(arguments, [2]));
};
window.clearTimeout = function(aTimeoutID)
{
var timer = CPTimersForTimeoutIDs[aTimeoutID];
window.clearTimeout = function(aTimeoutID)
{
const timer = CPTimersForTimeoutIDs[aTimeoutID];
if (timer)
[timer invalidate];
if (timer)
[timer invalidate];
CPTimersForTimeoutIDs[aTimeoutID] = nil;
};
delete CPTimersForTimeoutIDs[aTimeoutID];
};
window.setInterval = function(codeOrFunction, aDelay, functionArgs)
{
return _CPTimerBridgeTimer(codeOrFunction, aDelay, YES, Array.prototype.slice.apply(arguments, [2]));
};
window.setInterval = function(codeOrFunction, aDelay, functionArgs)
{
return _CPTimerBridgeTimer(codeOrFunction, aDelay, YES, Array.prototype.slice.apply(arguments, [2]));
};
window.clearInterval = function(aTimeoutID)
{
window.clearTimeout(aTimeoutID);
};
window.clearInterval = function(aTimeoutID)
{
window.clearTimeout(aTimeoutID);
};
}