const scheduled = new Map();
let nextId = 1;
let clockNow = 0;
globalThis.performance = { now: () => clockNow };
globalThis.setInterval = (fn, ms) => {
const id = nextId++;
scheduled.set(id, { fn, ms, at: clockNow + ms, repeat: true, kind: 'interval' });
return id;
};
globalThis.clearInterval = (id) => scheduled.delete(id);
globalThis.setTimeout = (fn, ms) => {
const id = nextId++;
scheduled.set(id, { fn, ms, at: clockNow + ms, repeat: false, kind: 'timeout' });
return id;
};
globalThis.clearTimeout = (id) => scheduled.delete(id);
globalThis.requestAnimationFrame = (fn) => {
const id = nextId++;
scheduled.set(id, { fn, ms: 16, at: clockNow + 16, repeat: false, kind: 'frame' });
return id;
};
globalThis.cancelAnimationFrame = (id) => scheduled.delete(id);
function advance(ms) {
const target = clockNow + ms;
for (let guard = 0; guard < 1000; guard += 1) {
let earliest = null;
scheduled.forEach((entry, id) => {
if (entry.at <= target && (earliest === null || entry.at < earliest.at)) {
earliest = { id, ...entry };
}
});
if (earliest === null) break;
clockNow = earliest.at;
if (earliest.repeat) {
scheduled.get(earliest.id).at = clockNow + earliest.ms;
} else {
scheduled.delete(earliest.id);
}
earliest.fn(clockNow);
}
clockNow = target;
}
function booked() {
return scheduled.size;
}
function reset() {
scheduled.clear();
clockNow = 0;
}
test('an interval signal starts at zero and reads elapsed milliseconds', () => {
reset();
const [elapsed] = owned(() => everyMs(250));
assert.equal(elapsed(), 0, 'before the first tick');
advance(250);
assert.equal(elapsed(), 250, 'after one tick');
advance(500);
assert.equal(elapsed(), 750, 'after three');
});
test('an interval signal reaches the bindings that read it', () => {
reset();
const seen = [];
owned(() => {
const elapsed = everyMs(100);
effect(() => seen.push(elapsed()));
});
advance(300);
assert.deepEqual(seen, [0, 100, 200, 300]);
});
test('disposing the owner stops the interval', () => {
reset();
const seen = [];
const [, dispose] = owned(() => {
const elapsed = everyMs(100);
effect(() => seen.push(elapsed()));
});
advance(200);
assert.equal(seen.length, 3, 'it ran while it was alive');
dispose();
assert.equal(booked(), 0, 'the scheduler is holding nothing');
advance(1000);
assert.equal(seen.length, 3, 'and nothing arrived after the dispose');
});
test('an interval reads time since it started, not since the page did', () => {
reset();
advance(1000);
const [elapsed] = owned(() => everyMs(250));
advance(250);
assert.equal(elapsed(), 250, 'the origin is the declaration, not the epoch');
});
test('two clocks in different owners are disposed independently', () => {
reset();
const [kept] = owned(() => everyMs(100));
const [dropped, dispose] = owned(() => everyMs(100));
advance(100);
assert.equal(kept(), 100);
assert.equal(dropped(), 100);
dispose();
advance(200);
assert.equal(dropped(), 100, 'the disposed one stopped');
assert.equal(kept(), 300, 'and the other did not');
assert.equal(booked(), 1, 'exactly one timer is left');
});
test('a frame signal starts at zero on its own first frame', () => {
reset();
advance(500);
const [motion] = owned(() => everyFrame());
assert.equal(motion(), 0, 'before any frame');
advance(16);
assert.equal(motion(), 0, 'the first frame is the origin');
advance(16);
assert.equal(motion(), 16, 'and the second is one frame past it');
});
test('a frame loop keeps rescheduling itself', () => {
reset();
const seen = [];
owned(() => {
const motion = everyFrame();
effect(() => seen.push(motion()));
});
advance(16 * 5);
assert.ok(seen.length >= 5, `expected at least five frames, saw ${seen.length}`);
assert.ok(booked() > 0, 'and one is still booked');
});
test('disposing the owner stops the frame loop', () => {
reset();
const seen = [];
const [, dispose] = owned(() => {
const motion = everyFrame();
effect(() => seen.push(motion()));
});
advance(16 * 3);
const before = seen.length;
dispose();
assert.equal(booked(), 0, 'the scheduler is holding nothing');
advance(16 * 100);
assert.equal(seen.length, before, 'and no frame arrived after the dispose');
});
test('a delay signal is false until it fires and true forever after', () => {
reset();
const [ready] = owned(() => afterMs(2000));
assert.equal(ready(), false, 'before');
advance(1999);
assert.equal(ready(), false, 'one millisecond short');
advance(1);
assert.equal(ready(), true, 'on time');
advance(10000);
assert.equal(ready(), true, 'and it stays');
});
test('a delay fires once and leaves nothing scheduled', () => {
reset();
const seen = [];
owned(() => {
const ready = afterMs(50);
effect(() => seen.push(ready()));
});
advance(500);
assert.deepEqual(seen, [false, true]);
assert.equal(booked(), 0, 'a one-shot clears itself by firing');
});
test('disposing before the delay elapses cancels it', () => {
reset();
const seen = [];
const [, dispose] = owned(() => {
const ready = afterMs(1000);
effect(() => seen.push(ready()));
});
dispose();
assert.equal(booked(), 0, 'the timeout was cleared');
advance(5000);
assert.deepEqual(seen, [false], 'and it never became true');
});
test('every clock constructor returns a read function and nothing else', () => {
reset();
owned(() => {
for (const made of [everyMs(100), everyFrame(), afterMs(100)]) {
assert.equal(typeof made, 'function', 'a clock is read like any signal');
assert.equal(made.length, 0, 'and it takes no argument');
}
});
});