svelte-interval-rune
Version:
Svelte 5 utility for creating intervals
254 lines (251 loc) • 6.69 kB
JavaScript
import { createSubscriber } from 'svelte/reactivity';
// src/index.svelte.ts
var Interval = class {
#subscribe;
#update;
#interval_id = 0;
#duration_input = $state(0);
#is_active = $state(true);
#is_stopped = $state(false);
#tick_count = $state(0);
#version = $state(0);
/** @private @deprecated DO NOT USE OR YOU WILL BE FIRED */
_;
#duration = $derived(
typeof this.#duration_input === "function" ? this.#duration_input() : this.#duration_input
);
#interval = $derived.by(() => {
this.#version;
clearInterval(this.#interval_id);
this.#interval_id = setInterval(this.#run_func.bind(this), this.#duration);
return this.#interval_id;
});
#run_func = () => {
if (!this.#is_active) return;
this.#tick_count++;
this.#update?.();
};
#kickoff_subscriptions() {
this.#interval;
this.#subscribe();
}
/**
* Creates a new Interval instance.
*
* @param duration - The interval duration in milliseconds. Can be a number or a reactive function.
*/
constructor(duration, options = {}) {
const { immediate = false } = options;
this.#duration_input = duration;
const self = this;
this._ = {
get run_func() {
return self.#run_func;
},
set run_func(fn) {
self.#run_func = fn;
},
get is_active() {
return self.#is_active;
},
set is_active(active) {
self.#is_active = active;
},
get tick_count() {
return self.#tick_count;
},
increment_tick() {
self.#tick_count = self.#tick_count + 1;
},
get update() {
return self.#update;
},
trigger_update() {
if (self.#update) {
self.#update();
}
},
force_restart() {
self.#version = self.#version + 1;
}
};
this.#subscribe = createSubscriber((update) => {
this.#update = update;
return () => clearInterval(this.#interval_id);
});
if (immediate) this.#kickoff_subscriptions();
}
/**
* Resumes the interval if it was paused.
*
* @param immediate - If true, immediately triggers a tick and resets the interval timing.
*/
resume(immediate = false) {
if (this.#is_stopped) return;
this.#is_active = true;
if (immediate) {
this.#version++;
this.#run_func();
}
}
/**
* Pauses the interval. The interval continues running in the background but stops executing callbacks and incrementing tick count.
*/
pause() {
this.#is_active = false;
}
/**
* Gets the current paused state of the interval.
*/
get isActive() {
return this.#is_active;
}
/**
* Gets the number of times the interval has fired (tick count).
* This count persists across pause/resume cycles and duration changes.
*/
get tickCount() {
this.#kickoff_subscriptions();
return this.#tick_count;
}
/**
* Gets the current date and time, and starts the interval if not already started.
* This is the primary way to activate the interval's reactivity.
*/
get current() {
this.#kickoff_subscriptions();
return /* @__PURE__ */ new Date();
}
/**
* Gets the current duration of the interval in milliseconds.
*/
get duration() {
return this.#duration;
}
set duration(value) {
this.#duration_input = value;
this.#version++;
}
/**
* Completely stops and clears the interval. Cannot be resumed.
* Use pause()/resume() if you want to temporarily stop.
*/
stop() {
clearInterval(this.#interval_id);
this.#is_active = false;
this.#is_stopped = true;
this.#interval_id = 0;
}
/**
* Check if interval has been completely stopped
*/
get isStopped() {
return this.#is_stopped;
}
[Symbol.dispose]() {
clearInterval(this.#interval_id);
}
};
function sync(...intervals) {
if (intervals.length === 0) {
throw new Error("At least one interval is required for sync");
}
let leader = intervals[0];
for (const interval of intervals) {
if (interval.duration < leader.duration) {
leader = interval;
}
}
const original_run_funcs = /* @__PURE__ */ new Map();
for (const interval of intervals) {
original_run_funcs.set(interval, interval._.run_func);
}
let sync_active = $state(false);
return {
enable() {
if (sync_active) return;
sync_active = true;
for (const interval of intervals) {
if (interval === leader) {
interval._.run_func = () => {
if (!leader._.is_active) return;
for (const synced_interval of intervals) {
const original_func = original_run_funcs.get(synced_interval);
original_func?.call(synced_interval);
}
};
} else {
interval._.run_func = () => {
};
}
}
leader.current;
leader._.force_restart();
},
disable() {
if (!sync_active) return;
sync_active = false;
for (const interval of intervals) {
const original_func = original_run_funcs.get(interval);
if (original_func) {
interval._.run_func = original_func;
interval._.force_restart();
}
}
},
get isSynced() {
return sync_active;
},
get leader() {
return leader;
}
};
}
var LimitedInterval = class extends Interval {
#max_ticks;
#is_completed = false;
#completion_baseline = 0;
constructor(duration, maxTicks, options = {}) {
super(duration, options);
if (maxTicks <= 0) {
throw new Error("max_ticks must be greater than 0");
}
this.#max_ticks = maxTicks;
this._.run_func = () => {
if (this.#is_completed) return;
if (!this._.is_active) return;
this._.increment_tick();
this._.trigger_update();
const ticks_since_baseline = this._.tick_count - this.#completion_baseline;
if (ticks_since_baseline >= this.#max_ticks) {
this.#is_completed = true;
this.pause();
}
};
}
get isCompleted() {
return this.#is_completed;
}
get remainingTicks() {
const ticks_since_baseline = this.tickCount - this.#completion_baseline;
return Math.max(0, this.#max_ticks - ticks_since_baseline);
}
get maxTicks() {
return this.#max_ticks;
}
reset() {
this.#is_completed = false;
this.#completion_baseline = this.tickCount;
if (!this.isActive) {
this.resume();
}
}
set maxTicks(new_max) {
if (new_max <= 0) {
throw new Error("max_ticks must be greater than 0");
}
this.#max_ticks = new_max;
this.#is_completed = false;
}
};
export { Interval, LimitedInterval, sync };