ident
All repositories: gitoria
8.2 KB
// hl:time — the clock AND time as an event source. JS twin of// plugins/time/time.zig, and the one file BOTH other realms run: the JavaScript// transpiler target copies it into `hl-modules/time.js`, and hl:web serves it to// the BROWSER as `hl-time.js` (hl:time declares no `"realm": "server"`, so a// client handler may reach it — plugins/web/web_framework.hl `packagePaths`).//// Parity contract with the native plugin:// now() → epoch milliseconds, integer-valued// timestamp(ms?) → "YYYY-MM-DDTHH:MM:SS.mmmZ", exactly 24 chars, UTC.// No argument (or a non-number) means "now"; an epoch-ms// argument renders THAT instant, which is what makes the// call testable — timestamp(0) is always the epoch.// monotonic() → nanoseconds off a monotonic counter. Origin arbitrary;// only differences are meaningful.// every(s) → a repeating Timer; `on t.tick()` until `t.stop()`// after(s) → a one-shot Timer, `s` seconds from now// until(epochMs) → a one-shot Timer at that instant// sleep(s) → BLOCKING. Yes, in a browser too — see below.export function now() { return Date.now(); }export function timestamp(ms) {// Date#toISOString already emits exactly the native format for in-range// instants. Pre-epoch values are clamped the same way the native plugin// clamps them (std.time.epoch is u64-only, so 1970 is the floor on both// sides and the two targets must agree on what happens below it).let t = typeof ms === 'number' && Number.isFinite(ms) ? Math.trunc(ms) : Date.now();if (t < 0) t = 0;return new Date(t).toISOString();}export function monotonic() {// process.hrtime.bigint() is Node's CLOCK_MONOTONIC. Converted to a plain// Number because Hybriel has no bigint; same f64 caveat as the native side// (exact for the first ~104 days of counter uptime).if (typeof process !== 'undefined' && process.hrtime && process.hrtime.bigint) {return Number(process.hrtime.bigint());}// Browser/other host fallback: performance.now() is ms with sub-ms// resolution off a monotonic origin.return Math.round(performance.now() * 1e6);}// ═══════════════════════════════════════════════════════════════════════════// TIME AS AN EVENT SOURCE (mission 254)// ═══════════════════════════════════════════════════════════════════════════//// THE FLOOR IS THE SAME NUMBER AS THE NATIVE PLUGIN'S, and this is the realm// that sets it. Server-side a timerfd carries nanoseconds and the event loop// blocks on it, so there is no floor there at all; the browser has one, and it// was MEASURED rather than quoted from the spec — tests/browser/tests/66-timers.mjs// drives a real page and prints the number it measured on every run. HTML's// timer nesting rule clamps a chained `setTimeout(…, 0)` to 4ms from the fifth// nesting level, and a `setInterval` asked for 1ms delivers ~4ms periods.//// The same `on t.tick()` handler is meant to run in both realms, so `every(0.001)`// must not mean two different things depending on where it ran: both sides raise// anything below the floor to it. (plugins/time/time.zig FLOOR_NS = 4_000_000.)export const TIMER_FLOOR_SECONDS = 0.004;/** Monotonic milliseconds, fractional. `performance` is a global in Node ≥16 and* in every browser; `Date.now()` is the last-resort fallback and is not* monotonic, which only costs accuracy across a clock adjustment. */function monoMs() {if (typeof performance !== 'undefined' && performance.now) return performance.now();return Date.now();}/** ONE ARMED TIMER — the twin of plugins/time/Timer.hl.** `on t.tick()` reaches this through `__hlOn`, which is the ONE registration* protocol both JS realms use: the transpiler target's `hlScopedBus`* (js/src/runtime/runtime.js) and hl:web's client runtime (hl-core.js* `__hlBindSources`) each end up calling it with the event name and a handler.* A plugin object is not an hl class instance and has no `constructor.__events__`,* so it could not otherwise be the target of an instance-scoped handler.** SCHEDULED FROM THE ORIGIN, like the native half: each round's delay is* computed against `origin + n × period`, never `period` from where the last* handler finished, so the handler's own runtime does not accumulate. A round* the host was too busy to deliver is SKIPPED rather than queued — a periodic* timer that owes you a backlog is a stampede.** A CHAINED `setTimeout` RATHER THAN `setInterval`, for the same reason: the* chain lets each delay be recomputed from the origin, and `setInterval`'s* behaviour when a callback overruns its period differs between hosts. */class HlTimer {constructor(seconds, repeating) {this.seconds = Math.max(TIMER_FLOOR_SECONDS, Number(seconds) || 0);this.repeating = !!repeating;this.running = true;this.count = 0;this.__handlers = new Map();this.__origin = monoMs();this.__n = 0;this.__h = null;this.__arm();}__arm() {const period = this.seconds * 1000;this.__n += 1;const due = this.__origin + this.__n * period;this.__h = setTimeout(() => this.__fire(), Math.max(0, due - monoMs()));}__fire() {this.__h = null;if (!this.running) return;this.count += 1;const ev = { count: this.count, at: Date.now() };if (this.repeating) {// skip whatever the host was too busy to deliver, keeping the phaseconst period = this.seconds * 1000;const now = monoMs();while (this.__origin + this.__n * period <= now) this.__n += 1;this.__n -= 1;this.__arm();} else {// A SPENT ONE-SHOT RETIRES ITSELF before the tick is delivered — the// same rule as the native half (mission 249: a source that will never// speak again must not keep the program alive; in Node that is// literally true, an un-cleared timer keeps the process running).this.running = false;}const list = this.__handlers.get('tick');if (list) for (const fn of list.slice()) fn(ev);}/** The registration protocol — see the class comment. */__hlOn(event, fn) {let list = this.__handlers.get(event);if (!list) { list = []; this.__handlers.set(event, list); }list.push(fn);return this;}/** Stop an interval. True when this call was the one that stopped it. */stop() {if (!this.running) return false;this.running = false;if (this.__h !== null) { clearTimeout(this.__h); this.__h = null; }return true;}}/** A REPEATING timer: `on t.tick()` fires every `seconds` until `t.stop()`. */export function every(seconds) { return new HlTimer(seconds, true); }/** A ONE-SHOT: `on t.tick()` fires once, `seconds` from now, and retires. */export function after(seconds) { return new HlTimer(seconds, false); }/** A ONE-SHOT AT AN INSTANT. `epochMs` is what `now()` answers; an instant* already past fires immediately (at the floor). */export function until(epochMs) {const delay = (Number(epochMs) - Date.now()) / 1000;return new HlTimer(delay > 0 ? delay : 0, false);}/** BLOCK for `seconds`. The creator's ruling: a basic tool for tests and* debugging, and it blocks — inside a server, `after()` is the thing to use.** `Atomics.wait` is the only real block JavaScript has, and it is FORBIDDEN on* a browser's main thread (it throws TypeError). So the browser gets a spin on* the monotonic clock instead, which is the same observable behaviour — the tab* is frozen for the duration — reached a different way. That is what a blocking* sleep IS; the docs say so beside it rather than pretending otherwise. */export function sleep(seconds) {const ms = Number(seconds) * 1000;if (!(ms > 0)) return false;try {if (typeof SharedArrayBuffer !== 'undefined' && typeof Atomics !== 'undefined') {Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);return true;}} catch { /* main thread — fall through to the spin */ }const until_ = monoMs() + ms;while (monoMs() < until_) { /* the block */ }return true;}
Branches
- mainmain branch
Latest commits
- 81b15b7bState of 2026-09-27, before the move to gitoriamre