gitoriaLog in with ident

notes

All repositories: gitoria

ReadmeCodePull requestsReleasesTicketsSettings
Commiteee693b8eee693b8deploy.sh: never send .git or .gitignore to Byrodinmreeee693b8/plugins/time/server.js

8.2 KB

  1. // hl:time — the clock AND time as an event source. JS twin of
  2. // plugins/time/time.zig, and the one file BOTH other realms run: the JavaScript
  3. // transpiler target copies it into `hl-modules/time.js`, and hl:web serves it to
  4. // the BROWSER as `hl-time.js` (hl:time declares no `"realm": "server"`, so a
  5. // client handler may reach it — plugins/web/web_framework.hl `packagePaths`).
  6. //
  7. // Parity contract with the native plugin:
  8. // now() → epoch milliseconds, integer-valued
  9. // timestamp(ms?) → "YYYY-MM-DDTHH:MM:SS.mmmZ", exactly 24 chars, UTC.
  10. // No argument (or a non-number) means "now"; an epoch-ms
  11. // argument renders THAT instant, which is what makes the
  12. // call testable — timestamp(0) is always the epoch.
  13. // monotonic() → nanoseconds off a monotonic counter. Origin arbitrary;
  14. // only differences are meaningful.
  15. // every(s) → a repeating Timer; `on t.tick()` until `t.stop()`
  16. // after(s) → a one-shot Timer, `s` seconds from now
  17. // until(epochMs) → a one-shot Timer at that instant
  18. // sleep(s) → BLOCKING. Yes, in a browser too — see below.
  19. export function now() { return Date.now(); }
  20. export function timestamp(ms) {
  21. // Date#toISOString already emits exactly the native format for in-range
  22. // instants. Pre-epoch values are clamped the same way the native plugin
  23. // clamps them (std.time.epoch is u64-only, so 1970 is the floor on both
  24. // sides and the two targets must agree on what happens below it).
  25. let t = typeof ms === 'number' && Number.isFinite(ms) ? Math.trunc(ms) : Date.now();
  26. if (t < 0) t = 0;
  27. return new Date(t).toISOString();
  28. }
  29. export function monotonic() {
  30. // process.hrtime.bigint() is Node's CLOCK_MONOTONIC. Converted to a plain
  31. // Number because Hybriel has no bigint; same f64 caveat as the native side
  32. // (exact for the first ~104 days of counter uptime).
  33. if (typeof process !== 'undefined' && process.hrtime && process.hrtime.bigint) {
  34. return Number(process.hrtime.bigint());
  35. }
  36. // Browser/other host fallback: performance.now() is ms with sub-ms
  37. // resolution off a monotonic origin.
  38. return Math.round(performance.now() * 1e6);
  39. }
  40. // ═══════════════════════════════════════════════════════════════════════════
  41. // TIME AS AN EVENT SOURCE (mission 254)
  42. // ═══════════════════════════════════════════════════════════════════════════
  43. //
  44. // THE FLOOR IS THE SAME NUMBER AS THE NATIVE PLUGIN'S, and this is the realm
  45. // that sets it. Server-side a timerfd carries nanoseconds and the event loop
  46. // blocks on it, so there is no floor there at all; the browser has one, and it
  47. // was MEASURED rather than quoted from the spec — tests/browser/tests/66-timers.mjs
  48. // drives a real page and prints the number it measured on every run. HTML's
  49. // timer nesting rule clamps a chained `setTimeout(…, 0)` to 4ms from the fifth
  50. // nesting level, and a `setInterval` asked for 1ms delivers ~4ms periods.
  51. //
  52. // The same `on t.tick()` handler is meant to run in both realms, so `every(0.001)`
  53. // must not mean two different things depending on where it ran: both sides raise
  54. // anything below the floor to it. (plugins/time/time.zig FLOOR_NS = 4_000_000.)
  55. export const TIMER_FLOOR_SECONDS = 0.004;
  56. /** Monotonic milliseconds, fractional. `performance` is a global in Node ≥16 and
  57. * in every browser; `Date.now()` is the last-resort fallback and is not
  58. * monotonic, which only costs accuracy across a clock adjustment. */
  59. function monoMs() {
  60. if (typeof performance !== 'undefined' && performance.now) return performance.now();
  61. return Date.now();
  62. }
  63. /** ONE ARMED TIMER — the twin of plugins/time/Timer.hl.
  64. *
  65. * `on t.tick()` reaches this through `__hlOn`, which is the ONE registration
  66. * protocol both JS realms use: the transpiler target's `hlScopedBus`
  67. * (js/src/runtime/runtime.js) and hl:web's client runtime (hl-core.js
  68. * `__hlBindSources`) each end up calling it with the event name and a handler.
  69. * A plugin object is not an hl class instance and has no `constructor.__events__`,
  70. * so it could not otherwise be the target of an instance-scoped handler.
  71. *
  72. * SCHEDULED FROM THE ORIGIN, like the native half: each round's delay is
  73. * computed against `origin + n × period`, never `period` from where the last
  74. * handler finished, so the handler's own runtime does not accumulate. A round
  75. * the host was too busy to deliver is SKIPPED rather than queued — a periodic
  76. * timer that owes you a backlog is a stampede.
  77. *
  78. * A CHAINED `setTimeout` RATHER THAN `setInterval`, for the same reason: the
  79. * chain lets each delay be recomputed from the origin, and `setInterval`'s
  80. * behaviour when a callback overruns its period differs between hosts. */
  81. class HlTimer {
  82. constructor(seconds, repeating) {
  83. this.seconds = Math.max(TIMER_FLOOR_SECONDS, Number(seconds) || 0);
  84. this.repeating = !!repeating;
  85. this.running = true;
  86. this.count = 0;
  87. this.__handlers = new Map();
  88. this.__origin = monoMs();
  89. this.__n = 0;
  90. this.__h = null;
  91. this.__arm();
  92. }
  93. __arm() {
  94. const period = this.seconds * 1000;
  95. this.__n += 1;
  96. const due = this.__origin + this.__n * period;
  97. this.__h = setTimeout(() => this.__fire(), Math.max(0, due - monoMs()));
  98. }
  99. __fire() {
  100. this.__h = null;
  101. if (!this.running) return;
  102. this.count += 1;
  103. const ev = { count: this.count, at: Date.now() };
  104. if (this.repeating) {
  105. // skip whatever the host was too busy to deliver, keeping the phase
  106. const period = this.seconds * 1000;
  107. const now = monoMs();
  108. while (this.__origin + this.__n * period <= now) this.__n += 1;
  109. this.__n -= 1;
  110. this.__arm();
  111. } else {
  112. // A SPENT ONE-SHOT RETIRES ITSELF before the tick is delivered — the
  113. // same rule as the native half (mission 249: a source that will never
  114. // speak again must not keep the program alive; in Node that is
  115. // literally true, an un-cleared timer keeps the process running).
  116. this.running = false;
  117. }
  118. const list = this.__handlers.get('tick');
  119. if (list) for (const fn of list.slice()) fn(ev);
  120. }
  121. /** The registration protocol — see the class comment. */
  122. __hlOn(event, fn) {
  123. let list = this.__handlers.get(event);
  124. if (!list) { list = []; this.__handlers.set(event, list); }
  125. list.push(fn);
  126. return this;
  127. }
  128. /** Stop an interval. True when this call was the one that stopped it. */
  129. stop() {
  130. if (!this.running) return false;
  131. this.running = false;
  132. if (this.__h !== null) { clearTimeout(this.__h); this.__h = null; }
  133. return true;
  134. }
  135. }
  136. /** A REPEATING timer: `on t.tick()` fires every `seconds` until `t.stop()`. */
  137. export function every(seconds) { return new HlTimer(seconds, true); }
  138. /** A ONE-SHOT: `on t.tick()` fires once, `seconds` from now, and retires. */
  139. export function after(seconds) { return new HlTimer(seconds, false); }
  140. /** A ONE-SHOT AT AN INSTANT. `epochMs` is what `now()` answers; an instant
  141. * already past fires immediately (at the floor). */
  142. export function until(epochMs) {
  143. const delay = (Number(epochMs) - Date.now()) / 1000;
  144. return new HlTimer(delay > 0 ? delay : 0, false);
  145. }
  146. /** BLOCK for `seconds`. The creator's ruling: a basic tool for tests and
  147. * debugging, and it blocks — inside a server, `after()` is the thing to use.
  148. *
  149. * `Atomics.wait` is the only real block JavaScript has, and it is FORBIDDEN on
  150. * a browser's main thread (it throws TypeError). So the browser gets a spin on
  151. * the monotonic clock instead, which is the same observable behaviour — the tab
  152. * is frozen for the duration — reached a different way. That is what a blocking
  153. * sleep IS; the docs say so beside it rather than pretending otherwise. */
  154. export function sleep(seconds) {
  155. const ms = Number(seconds) * 1000;
  156. if (!(ms > 0)) return false;
  157. try {
  158. if (typeof SharedArrayBuffer !== 'undefined' && typeof Atomics !== 'undefined') {
  159. Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
  160. return true;
  161. }
  162. } catch { /* main thread — fall through to the spin */ }
  163. const until_ = monoMs() + ms;
  164. while (monoMs() < until_) { /* the block */ }
  165. return true;
  166. }

Branches

Latest commits

  • eee693b8deploy.sh: never send .git or .gitignore to Byrodinmre
  • c8904061State of 2026-09-27, before the move to gitoriamre