char/avril

(wip) why'd you have to go and make things so complicaaaaated?

git clone https://git.t4t.associates/char/avril

Charlotte Somport timers and structured clone extensions to typescriptc42f8c8

main
7.4 KiB259 linesraw
1import {
2  Map,
3  MapPrototypeDelete as remove,
4  MapPrototypeGet as get,
5  MapPrototypeHas as has,
6  MapPrototypeSet as set,
7  MathMin as min,
8  ReflectApply as apply,
9  TypeError,
10} from "ext:core/primordials";
11import {
12  op_timer_cancel as cancel,
13  op_timer_now as now,
14  op_timer_wait as wait,
15  op_timer_wait_unreferenced as waitUnreferenced,
16} from "ext:core/ops";
17import { promiseThen as then } from "ext:core/promises";
18import { enqueueMicrotask } from "ext:core/microtasks";
19import { enqueueTask } from "ext:core/tasks";
20import { array, missing, required, string } from "ext:web/idl.js";
21import { reportErrorValue } from "ext:web/events.js";
22
23interface Timer {
24  id: number;
25  handler: Function | null;
26  args: unknown[] | null;
27  repeat: boolean;
28  delay: number;
29  privateTimer: boolean;
30  nesting: number;
31  deadline: number;
32  sequence: number;
33}
34
35const receiver = globalThis;
36const timers = new Map<number, Timer>(), queue = array<Timer>();
37let nextId = 1, pending: { deadline: number; referenced: boolean } | null = null,
38  firing = false, nesting = 0;
39// Heap order is (deadline, sequence); the sequence restores FIFO among equals.
40let sequence = 0;
41// Cleared entries stay in the heap until they surface; stale counts them so
42// schedule() can rebuild when garbage dominates.
43let stale = 0;
44// Non-private timers in the map, including the one currently firing.
45let referencedCount = 0;
46
47function report(error: unknown): void {
48  reportErrorValue(error);
49}
50function earlier(a: Timer, b: Timer): boolean {
51  return a.deadline < b.deadline ||
52    (a.deadline === b.deadline && a.sequence < b.sequence);
53}
54function siftUp(index: number): void {
55  while (index > 0) {
56    const parent = (index - 1) >> 1;
57    if (!earlier(queue[index], queue[parent])) break;
58    const entry = queue[index];
59    queue[index] = queue[parent];
60    queue[parent] = entry;
61    index = parent;
62  }
63}
64function siftDown(index: number): void {
65  const count = queue.length;
66  for (;;) {
67    const left = index * 2 + 1;
68    const right = left + 1;
69    let least = index;
70    if (left < count && earlier(queue[left], queue[least])) least = left;
71    if (right < count && earlier(queue[right], queue[least])) least = right;
72    if (least === index) break;
73    const entry = queue[index];
74    queue[index] = queue[least];
75    queue[least] = entry;
76    index = least;
77  }
78}
79function insert(entry: Timer): void {
80  entry.sequence = sequence++;
81  const index = queue.length;
82  queue[index] = entry;
83  siftUp(index);
84}
85function popHead(): Timer {
86  const head = queue[0];
87  const last = queue[queue.length - 1];
88  queue.length = queue.length - 1;
89  if (queue.length > 0) {
90    queue[0] = last;
91    siftDown(0);
92  }
93  return head;
94}
95// Drop cleared heads so the live head is on top. Interior garbage stays
96// until it surfaces; schedule() rebuilds if it dominates.
97function peekHead(): Timer | undefined {
98  while (queue.length > 0) {
99    const head = queue[0];
100    if (get(timers, head.id) === head) return head;
101    popHead();
102    if (stale > 0) stale--;
103  }
104  return undefined;
105}
106function compactIfNeeded(): void {
107  if (stale <= 0 || queue.length < 128 || stale * 2 <= queue.length) return;
108  let live = 0;
109  for (let index = 0; index < queue.length; index++) {
110    const entry = queue[index];
111    if (get(timers, entry.id) === entry) queue[live++] = entry;
112  }
113  queue.length = live;
114  stale = 0;
115  for (let index = (live >> 1) - 1; index >= 0; index--) siftDown(index);
116}
117function schedule(): void {
118  if (firing) return;
119  compactIfNeeded();
120  const head = peekHead();
121  if (!head) {
122    pending = null;
123    cancel(null);
124    return;
125  }
126  const referenced = referencedCount > 0;
127  if (
128    pending && pending.deadline <= head.deadline &&
129    pending.referenced === referenced
130  ) return;
131  const record = { deadline: head.deadline, referenced };
132  pending = record;
133  const promise = (referenced ? wait : waitUnreferenced)(record.deadline);
134  // Promise reactions must not consult guest-controlled constructor/species hooks.
135  then(promise, () => {
136    if (pending !== record) return;
137    pending = null;
138    const entry = peekHead();
139    if (!entry || entry.deadline > now(null)) {
140      schedule();
141      return;
142    }
143    popHead();
144    firing = true;
145    enqueueTask(() => {
146      if (get(timers, entry.id) !== entry) return;
147      nesting = entry.privateTimer ? 0 : entry.nesting;
148      try {
149        // Map membership above guarantees this entry has not been cleared.
150        apply(entry.handler!, receiver, entry.args!);
151      } catch (error) {
152        report(error);
153      } finally {
154        nesting = 0;
155      }
156    }, () => {
157      if (get(timers, entry.id) === entry) {
158        if (entry.repeat) {
159          const level = entry.nesting++;
160          entry.deadline = now(null) +
161            (level > 5 && entry.delay < 4 ? 4 : entry.delay);
162          insert(entry);
163        } else {
164          remove(timers, entry.id);
165          if (!entry.privateTimer) referencedCount--;
166        }
167      }
168      firing = false;
169      schedule();
170    });
171  }, (error) => {
172    if (pending !== record) return;
173    pending = null;
174    report(error);
175  });
176}
177function start(handler: unknown, rawDelay: unknown, args: unknown[], repeat: boolean, privateTimer = false): number {
178  if (typeof handler !== "function") handler = string(handler);
179  // Keep unary +: Number() would accept BigInt.
180  let delay = +(rawDelay as number) >> 0;
181  if (typeof handler !== "function") missing("string timer callbacks");
182  if (delay < 0) delay = 0;
183  const id = nextId;
184  do {
185    nextId = nextId === 2147483647 ? 1 : nextId + 1;
186  } while (has(timers, nextId));
187  const entry = {
188    id,
189    handler,
190    args,
191    repeat,
192    delay,
193    privateTimer,
194    nesting: nesting + 1,
195    deadline: now(null) +
196      (!privateTimer && nesting > 5 && delay < 4 ? 4 : delay),
197    sequence: 0,
198  };
199  set(timers, id, entry);
200  if (!privateTimer) referencedCount++;
201  insert(entry);
202  schedule();
203  return id;
204}
205function scheduleTimeout(handler: () => void, milliseconds: number): void {
206  let remaining = milliseconds, previous = now(null);
207  function fire(): void {
208    const current = now(null);
209    remaining -= current - previous;
210    previous = current;
211    if (remaining <= 0) handler();
212    else start(fire, min(remaining, 2147483647), array(), false, true);
213  }
214  start(fire, min(remaining, 2147483647), array(), false, true);
215}
216function clear(rawId: unknown): void {
217  const id = +(rawId as number) >> 0;
218  const entry = get(timers, id);
219  if (!entry || entry.privateTimer) return;
220  remove(timers, id);
221  referencedCount--;
222  entry.handler = null;
223  entry.args = null;
224  stale++;
225  schedule();
226}
227const api = {
228  setTimeout(handler: unknown, timeout: unknown = 0, ...args: unknown[]): number {
229    required(arguments.length, 1);
230    return start(handler, timeout, args, false);
231  },
232  setInterval(handler: unknown, timeout: unknown = 0, ...args: unknown[]): number {
233    required(arguments.length, 1);
234    return start(handler, timeout, args, true);
235  },
236  clearTimeout(id: unknown = 0): void {
237    clear(id);
238  },
239  clearInterval(id: unknown = 0): void {
240    clear(id);
241  },
242  queueMicrotask(callback: unknown): void {
243    required(arguments.length, 1);
244    if (typeof callback !== "function") {
245      throw new TypeError("Expected a microtask callback");
246    }
247    enqueueMicrotask(callback);
248  },
249};
250const { setTimeout, setInterval, clearTimeout, clearInterval, queueMicrotask } =
251  api;
252export {
253  clearInterval,
254  clearTimeout,
255  queueMicrotask,
256  scheduleTimeout,
257  setInterval,
258  setTimeout,
259};