1'use strict';
2
3const {
4  FunctionPrototypeBind,
5  Promise,
6  PromiseReject,
7  ReflectConstruct,
8  SafePromisePrototypeFinally,
9  Symbol,
10} = primordials;
11
12const {
13  Timeout,
14  Immediate,
15  insert,
16} = require('internal/timers');
17const {
18  clearImmediate,
19  clearInterval,
20  clearTimeout,
21} = require('timers');
22
23const {
24  AbortError,
25  codes: {
26    ERR_ILLEGAL_CONSTRUCTOR,
27    ERR_INVALID_ARG_TYPE,
28    ERR_INVALID_THIS,
29  },
30} = require('internal/errors');
31
32const {
33  validateAbortSignal,
34  validateBoolean,
35  validateObject,
36} = require('internal/validators');
37
38const {
39  kEmptyObject,
40} = require('internal/util');
41
42const kScheduler = Symbol('kScheduler');
43let kResistStopPropagation;
44
45function cancelListenerHandler(clear, reject, signal) {
46  if (!this._destroyed) {
47    clear(this);
48    reject(new AbortError(undefined, { cause: signal?.reason }));
49  }
50}
51
52function setTimeout(after, value, options = kEmptyObject) {
53  const args = value !== undefined ? [value] : value;
54  if (options == null || typeof options !== 'object') {
55    return PromiseReject(
56      new ERR_INVALID_ARG_TYPE(
57        'options',
58        'Object',
59        options));
60  }
61  const { signal, ref = true } = options;
62  try {
63    validateAbortSignal(signal, 'options.signal');
64  } catch (err) {
65    return PromiseReject(err);
66  }
67  if (typeof ref !== 'boolean') {
68    return PromiseReject(
69      new ERR_INVALID_ARG_TYPE(
70        'options.ref',
71        'boolean',
72        ref));
73  }
74
75  if (signal?.aborted) {
76    return PromiseReject(new AbortError(undefined, { cause: signal.reason }));
77  }
78  let oncancel;
79  const ret = new Promise((resolve, reject) => {
80    const timeout = new Timeout(resolve, after, args, false, ref);
81    insert(timeout, timeout._idleTimeout);
82    if (signal) {
83      oncancel = FunctionPrototypeBind(cancelListenerHandler,
84                                       timeout, clearTimeout, reject, signal);
85      kResistStopPropagation ??= require('internal/event_target').kResistStopPropagation;
86      signal.addEventListener('abort', oncancel, { __proto__: null, [kResistStopPropagation]: true });
87    }
88  });
89  return oncancel !== undefined ?
90    SafePromisePrototypeFinally(
91      ret,
92      () => signal.removeEventListener('abort', oncancel)) : ret;
93}
94
95function setImmediate(value, options = kEmptyObject) {
96  if (options == null || typeof options !== 'object') {
97    return PromiseReject(
98      new ERR_INVALID_ARG_TYPE(
99        'options',
100        'Object',
101        options));
102  }
103  const { signal, ref = true } = options;
104  try {
105    validateAbortSignal(signal, 'options.signal');
106  } catch (err) {
107    return PromiseReject(err);
108  }
109  if (typeof ref !== 'boolean') {
110    return PromiseReject(
111      new ERR_INVALID_ARG_TYPE(
112        'options.ref',
113        'boolean',
114        ref));
115  }
116
117  if (signal?.aborted) {
118    return PromiseReject(new AbortError(undefined, { cause: signal.reason }));
119  }
120  let oncancel;
121  const ret = new Promise((resolve, reject) => {
122    const immediate = new Immediate(resolve, [value]);
123    if (!ref) immediate.unref();
124    if (signal) {
125      oncancel = FunctionPrototypeBind(cancelListenerHandler,
126                                       immediate, clearImmediate, reject,
127                                       signal);
128      kResistStopPropagation ??= require('internal/event_target').kResistStopPropagation;
129      signal.addEventListener('abort', oncancel, { __proto__: null, [kResistStopPropagation]: true });
130    }
131  });
132  return oncancel !== undefined ?
133    SafePromisePrototypeFinally(
134      ret,
135      () => signal.removeEventListener('abort', oncancel)) : ret;
136}
137
138async function* setInterval(after, value, options = kEmptyObject) {
139  validateObject(options, 'options');
140  const { signal, ref = true } = options;
141  validateAbortSignal(signal, 'options.signal');
142  validateBoolean(ref, 'options.ref');
143
144  if (signal?.aborted)
145    throw new AbortError(undefined, { cause: signal?.reason });
146
147  let onCancel;
148  let interval;
149  try {
150    let notYielded = 0;
151    let callback;
152    interval = new Timeout(() => {
153      notYielded++;
154      if (callback) {
155        callback();
156        callback = undefined;
157      }
158    }, after, undefined, true, ref);
159    insert(interval, interval._idleTimeout);
160    if (signal) {
161      onCancel = () => {
162        clearInterval(interval);
163        if (callback) {
164          callback(
165            PromiseReject(
166              new AbortError(undefined, { cause: signal.reason })));
167          callback = undefined;
168        }
169      };
170      kResistStopPropagation ??= require('internal/event_target').kResistStopPropagation;
171      signal.addEventListener('abort', onCancel, { __proto__: null, once: true, [kResistStopPropagation]: true });
172    }
173
174    while (!signal?.aborted) {
175      if (notYielded === 0) {
176        await new Promise((resolve) => callback = resolve);
177      }
178      for (; notYielded > 0; notYielded--) {
179        yield value;
180      }
181    }
182    throw new AbortError(undefined, { cause: signal?.reason });
183  } finally {
184    clearInterval(interval);
185    signal?.removeEventListener('abort', onCancel);
186  }
187}
188
189// TODO(@jasnell): Scheduler is an API currently being discussed by WICG
190// for Web Platform standardization: https://github.com/WICG/scheduling-apis
191// The scheduler.yield() and scheduler.wait() methods correspond roughly to
192// the awaitable setTimeout and setImmediate implementations here. This api
193// should be considered to be experimental until the spec for these are
194// finalized. Note, also, that Scheduler is expected to be defined as a global,
195// but while the API is experimental we shouldn't expose it as such.
196class Scheduler {
197  constructor() {
198    throw new ERR_ILLEGAL_CONSTRUCTOR();
199  }
200
201  /**
202   * @returns {Promise<void>}
203   */
204  yield() {
205    if (!this[kScheduler])
206      throw new ERR_INVALID_THIS('Scheduler');
207    return setImmediate();
208  }
209
210  /**
211   * @typedef {import('../internal/abort_controller').AbortSignal} AbortSignal
212   * @param {number} delay
213   * @param {{ signal?: AbortSignal }} [options]
214   * @returns {Promise<void>}
215   */
216  wait(delay, options) {
217    if (!this[kScheduler])
218      throw new ERR_INVALID_THIS('Scheduler');
219    return setTimeout(delay, undefined, { signal: options?.signal });
220  }
221}
222
223module.exports = {
224  setTimeout,
225  setImmediate,
226  setInterval,
227  scheduler: ReflectConstruct(function() {
228    this[kScheduler] = true;
229  }, [], Scheduler),
230};
231