@scrypted/server 0.138.4 → 0.138.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,32 +1,28 @@
1
+ # Copy this here before it gets populated with the rest of this file's code
2
+ base_globals = globals().copy()
3
+
1
4
  import asyncio
2
- import concurrent.futures
3
- from functools import partial
4
5
  import inspect
5
- import os
6
6
  import prompt_toolkit
7
7
  from prompt_toolkit import print_formatted_text
8
8
  from prompt_toolkit.application import Application
9
9
  import prompt_toolkit.application.current
10
+ from prompt_toolkit.application.current import create_app_session
11
+ from prompt_toolkit.data_structures import Size
10
12
  import prompt_toolkit.key_binding.key_processor
11
- import prompt_toolkit.contrib.telnet.server
12
- from prompt_toolkit.contrib.telnet.server import TelnetServer, TelnetConnection
13
+ from prompt_toolkit.input import create_pipe_input
14
+ from prompt_toolkit.output.vt100 import Vt100_Output
13
15
  from prompt_toolkit.output.color_depth import ColorDepth
14
- from prompt_toolkit.shortcuts import clear_title, set_title
15
- from ptpython.repl import embed, PythonRepl, _has_coroutine_flag
16
+ from ptpython.repl import embed, PythonRepl
16
17
  import ptpython.key_bindings
17
18
  import ptpython.python_input
18
19
  import ptpython.history_browser
19
20
  import ptpython.layout
20
- import socket
21
- import sys
22
- import telnetlib
23
- import threading
24
- import traceback
25
- import types
26
21
  from typing import List, Dict, Any
27
22
 
28
23
  from scrypted_python.scrypted_sdk import ScryptedStatic, ScryptedDevice
29
24
 
25
+ from cluster_setup import cluster_listen_zero
30
26
  from rpc import maybe_await
31
27
 
32
28
 
@@ -38,175 +34,72 @@ ColorDepth.default = lambda *args, **kwargs: ColorDepth.DEPTH_4_BIT
38
34
  # that there is only one global Application, so multiple REPLs will confuse
39
35
  # the library. The patches here allow us to scope a particular call stack
40
36
  # to a particular REPL, and to get the current Application from the stack.
41
- default_get_app = prompt_toolkit.application.current.get_app
37
+ def patch_prompt_toolkit():
38
+ default_get_app = prompt_toolkit.application.current.get_app
39
+
40
+ def get_app_patched() -> Application[Any]:
41
+ stack = inspect.stack()
42
+ for frame in stack:
43
+ self_var = frame.frame.f_locals.get("self")
44
+ if self_var is not None and isinstance(self_var, Application):
45
+ return self_var
46
+ return default_get_app()
47
+
48
+ prompt_toolkit.application.current.get_app = get_app_patched
49
+ prompt_toolkit.key_binding.key_processor.get_app = get_app_patched
50
+ ptpython.python_input.get_app = get_app_patched
51
+ ptpython.key_bindings.get_app = get_app_patched
52
+ ptpython.history_browser.get_app = get_app_patched
53
+ ptpython.layout.get_app = get_app_patched
54
+ patch_prompt_toolkit()
55
+
56
+
57
+ def configure(repl: PythonRepl) -> None:
58
+ repl.confirm_exit = False
59
+ repl.enable_open_in_editor = False
60
+ repl.enable_system_bindings = False
42
61
 
43
62
 
44
- def get_app_patched() -> Application[Any]:
45
- stack = inspect.stack()
46
- for frame in stack:
47
- self_var = frame.frame.f_locals.get("self")
48
- if self_var is not None and isinstance(self_var, Application):
49
- return self_var
50
- return default_get_app()
63
+ class AsyncStreamStdout:
64
+ """
65
+ Wrapper around StreamReader and StreamWriter to provide `write` and `flush`
66
+ methods for Vt100_Output.
67
+ """
51
68
 
69
+ def __init__(self, reader: asyncio.StreamReader, writer: asyncio.StreamWriter):
70
+ self.reader = reader
71
+ self.writer = writer
72
+ self.loop = asyncio.get_event_loop()
52
73
 
53
- prompt_toolkit.application.current.get_app = get_app_patched
54
- prompt_toolkit.key_binding.key_processor.get_app = get_app_patched
55
- prompt_toolkit.contrib.telnet.server.get_app = get_app_patched
56
- ptpython.python_input.get_app = get_app_patched
57
- ptpython.key_bindings.get_app = get_app_patched
58
- ptpython.history_browser.get_app = get_app_patched
59
- ptpython.layout.get_app = get_app_patched
74
+ def write(self, data: bytes) -> None:
75
+ if isinstance(data, str):
76
+ data = data.encode()
77
+ self.writer.write(data)
60
78
 
79
+ def flush(self) -> None:
80
+ self.loop.create_task(self.writer.drain())
81
+
82
+ def isatty(self) -> bool:
83
+ return True
61
84
 
62
- # This is a patched version of PythonRepl.run_async to handle an
63
- # AssertionError raised by prompt_toolkit when the TelnetServer exits.
64
- # Original: https://github.com/prompt-toolkit/ptpython/blob/3.0.26/ptpython/repl.py#L215
65
- async def run_async_patched(self: PythonRepl) -> None:
66
- """
67
- Run the REPL loop, but run the blocking parts in an executor, so that
68
- we don't block the event loop. Both the input and output (which can
69
- display a pager) will run in a separate thread with their own event
70
- loop, this way ptpython's own event loop won't interfere with the
71
- asyncio event loop from where this is called.
72
-
73
- The "eval" however happens in the current thread, which is important.
74
- (Both for control-C to work, as well as for the code to see the right
75
- thread in which it was embedded).
76
- """
77
- loop = asyncio.get_running_loop()
78
-
79
- if self.terminal_title:
80
- set_title(self.terminal_title)
81
-
82
- self._add_to_namespace()
83
-
84
- try:
85
- while True:
86
- try:
87
- # Read.
88
- try:
89
- text = await loop.run_in_executor(None, self.read)
90
- except EOFError:
91
- return
92
- except asyncio.CancelledError:
93
- return
94
- except AssertionError:
95
- return
96
- except BaseException:
97
- # Something went wrong while reading input.
98
- # (E.g., a bug in the completer that propagates. Don't
99
- # crash the REPL.)
100
- traceback.print_exc()
101
- continue
102
-
103
- # Eval.
104
- await self.run_and_show_expression_async(text)
105
-
106
- except KeyboardInterrupt as e:
107
- # XXX: This does not yet work properly. In some situations,
108
- # `KeyboardInterrupt` exceptions can end up in the event
109
- # loop selector.
110
- self._handle_keyboard_interrupt(e)
111
- except SystemExit:
112
- return
113
- finally:
114
- if self.terminal_title:
115
- clear_title()
116
- self._remove_from_namespace()
117
-
118
-
119
- # This is a patched version of PythonRepl.eval_async to cross thread
120
- # boundaries when running user REPL instructions.
121
- # Original: https://github.com/prompt-toolkit/ptpython/blob/3.0.26/ptpython/repl.py#L304
122
- async def eval_async_patched(self: PythonRepl, line: str) -> object:
123
- """
124
- Evaluate the line and print the result.
125
- """
126
- scrypted_loop: asyncio.AbstractEventLoop = self.scrypted_loop
127
-
128
- def task_done_cb(future: concurrent.futures.Future, task: asyncio.Task):
129
- try:
130
- result = task.result()
131
- future.set_result(result)
132
- except BaseException as e:
133
- future.set_exception(e)
134
-
135
- def eval_in_scrypted(future: concurrent.futures.Future, code, *args, **kwargs):
136
- try:
137
- result = eval(code, *args, **kwargs)
138
- if _has_coroutine_flag(code):
139
- task = scrypted_loop.create_task(result)
140
- task.add_done_callback(partial(task_done_cb, future))
141
- else:
142
- future.set_result(result)
143
- except BaseException as e:
144
- future.set_exception(e)
145
-
146
- def eval_across_loops(code, *args, **kwargs):
147
- future = concurrent.futures.Future()
148
- scrypted_loop.call_soon_threadsafe(
149
- partial(eval_in_scrypted, future), code, *args, **kwargs
150
- )
151
- return future.result()
152
-
153
- # WORKAROUND: Due to a bug in Jedi, the current directory is removed
154
- # from sys.path. See: https://github.com/davidhalter/jedi/issues/1148
155
- if "" not in sys.path:
156
- sys.path.insert(0, "")
157
-
158
- if line.lstrip().startswith("!"):
159
- # Run as shell command
160
- os.system(line[1:])
161
- else:
162
- # Try eval first
163
- try:
164
- code = self._compile_with_flags(line, "eval")
165
- except SyntaxError:
166
- pass
167
- else:
168
- # No syntax errors for eval. Do eval.
169
-
170
- result = eval_across_loops(code, self.get_globals(), self.get_locals())
171
- self._store_eval_result(result)
172
- return result
173
-
174
- # If not a valid `eval` expression, compile as `exec` expression
175
- # but still run with eval to get an awaitable in case of a
176
- # awaitable expression.
177
- code = self._compile_with_flags(line, "exec")
178
- result = eval_across_loops(code, self.get_globals(), self.get_locals())
179
-
180
- return None
181
-
182
-
183
- def configure(scrypted_loop: asyncio.AbstractEventLoop, repl: PythonRepl) -> None:
184
- repl.confirm_exit = False
185
- repl.enable_system_bindings = False
186
- repl.enable_mouse_support = False
187
- repl.run_async = types.MethodType(run_async_patched, repl)
188
- repl.eval_async = types.MethodType(eval_async_patched, repl)
189
- repl.scrypted_loop = scrypted_loop
85
+
86
+ # keep a reference to the server alive so it doesn't get garbage collected
87
+ repl_server = None
190
88
 
191
89
 
192
90
  async def createREPLServer(sdk: ScryptedStatic, plugin: ScryptedDevice) -> int:
91
+ global repl_server
92
+
93
+ if repl_server is not None:
94
+ return repl_server["port"]
95
+
193
96
  deviceManager = sdk.deviceManager
194
97
  systemManager = sdk.systemManager
195
98
  mediaManager = sdk.mediaManager
196
99
 
197
- # Create the proxy server to handle initial control messages
198
- sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
199
- sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
200
- sock.settimeout(None)
201
- # TODO: this should use an equivalent to cluster_listen_zero
202
- sock.bind(("0.0.0.0" if os.getenv("SCRYPTED_CLUSTER_ADDRESS") else "127.0.0.1", 0))
203
- sock.listen()
204
-
205
- scrypted_loop: asyncio.AbstractEventLoop = asyncio.get_event_loop()
206
-
207
- async def start_telnet_repl(future: concurrent.futures.Future, filter: str) -> None:
208
- repl_loop = asyncio.get_event_loop()
209
-
100
+ async def on_repl_client(reader: asyncio.StreamReader, writer: asyncio.StreamWriter):
101
+ filter = await reader.read(1024)
102
+ filter = filter.decode()
210
103
  if filter == "undefined":
211
104
  filter = None
212
105
 
@@ -224,141 +117,52 @@ async def createREPLServer(sdk: ScryptedStatic, plugin: ScryptedDevice) -> int:
224
117
  device = plugin
225
118
  for c in chain:
226
119
  device = await maybe_await(device.getDevice(c))
227
-
228
120
  realDevice = systemManager.getDeviceById(device.id)
229
121
 
230
- # Select a free port for the telnet server
231
- s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
232
- s.bind(("localhost", 0))
233
- telnet_port = s.getsockname()[1]
234
- s.close()
235
-
236
- async def interact(connection: TelnetConnection) -> None:
237
- # repl_loop owns the print capabilities, but the prints will
238
- # be executed in scrypted_loop. We need to bridge the two here
239
- repl_print = partial(print_formatted_text, output=connection.vt100_output)
240
-
241
- def print_across_loops(*args, **kwargs):
242
- repl_loop.call_soon_threadsafe(repl_print, *args, **kwargs)
243
-
244
- global_dict = {
245
- **globals(),
246
- "print": print_across_loops,
247
- "help": lambda *args, **kwargs: print_across_loops(
248
- "Help is not available in this environment"
249
- ),
250
- "input": lambda *args, **kwargs: print_across_loops(
251
- "Input is not available in this environment"
252
- ),
253
- }
254
- locals_dict = {
255
- "device": device,
256
- "systemManager": systemManager,
257
- "deviceManager": deviceManager,
258
- "mediaManager": mediaManager,
259
- "sdk": sdk,
260
- "realDevice": realDevice,
261
- }
262
- vars_prompt = "\n".join([f" {k}" for k in locals_dict.keys()])
263
- banner = f"Python REPL variables:\n{vars_prompt}"
264
- print_formatted_text(banner)
265
- await embed(
266
- return_asyncio_coroutine=True,
267
- globals=global_dict,
268
- locals=locals_dict,
269
- configure=partial(configure, scrypted_loop),
270
- )
271
-
272
- server_task: asyncio.Task = None
273
-
274
- def ready_cb():
275
- future.set_result(
276
- (
277
- telnet_port,
278
- lambda: repl_loop.call_soon_threadsafe(server_task.cancel),
279
- )
280
- )
281
-
282
- # Start the REPL server
283
- telnet_server = TelnetServer(
284
- interact=interact, port=telnet_port, enable_cpr=False
285
- )
286
- server_task = asyncio.create_task(telnet_server.run(ready_cb=ready_cb))
287
- try:
288
- await server_task
289
- except:
290
- pass
291
-
292
- def handle_connection(conn: socket.socket):
293
- conn.settimeout(None)
294
- filter = conn.recv(1024).decode()
295
- server_started_future = concurrent.futures.Future()
296
- repl_loop = asyncio.SelectorEventLoop()
297
-
298
- # we're not in the main loop, so can't handle any signals anyways
299
- repl_loop.add_signal_handler = lambda sig, cb: None
300
- repl_loop.remove_signal_handler = lambda sig: True
301
-
302
- def finish_setup():
303
- telnet_port, exit_server = server_started_future.result()
304
-
305
- telnet_client = telnetlib.Telnet("localhost", telnet_port, timeout=None)
306
-
307
- def telnet_negotiation_cb(telnet_socket, command, option):
308
- pass # ignore telnet negotiation
309
-
310
- telnet_client.set_option_negotiation_callback(telnet_negotiation_cb)
311
-
312
- # initialize telnet terminal
313
- # this tells the telnet server we are a vt100 terminal
314
- telnet_client.get_socket().sendall(
315
- b"\xff\xfb\x18\xff\xfa\x18\x00\x61\x6e\x73\x69\xff\xf0"
122
+ with create_pipe_input() as vt100_input:
123
+ vt100_output = Vt100_Output(
124
+ AsyncStreamStdout(reader, writer),
125
+ lambda: Size(rows=24, columns=80),
126
+ term=None,
316
127
  )
317
- telnet_client.get_socket().sendall(b"\r\n")
318
128
 
319
- # Bridge the connection to the telnet server, two way
320
- def forward_to_telnet():
129
+ async def vt100_input_coro():
321
130
  while True:
322
- data = conn.recv(1024)
131
+ data = await reader.read(1024)
323
132
  if not data:
324
133
  break
325
- telnet_client.write(data)
326
- telnet_client.close()
327
- exit_server()
134
+ vt100_input.send_bytes(data)
135
+
136
+ asyncio.create_task(vt100_input_coro())
137
+
138
+ with create_app_session(input=vt100_input, output=vt100_output):
139
+ global_dict = {
140
+ **base_globals.copy(),
141
+ "print": print_formatted_text,
142
+ "help": lambda *args, **kwargs: print_formatted_text(
143
+ "Help is not available in this environment"
144
+ ),
145
+ "input": lambda *args, **kwargs: print_formatted_text(
146
+ "Input is not available in this environment"
147
+ ),
148
+ }
149
+ locals_dict = {
150
+ "device": device,
151
+ "systemManager": systemManager,
152
+ "deviceManager": deviceManager,
153
+ "mediaManager": mediaManager,
154
+ "sdk": sdk,
155
+ "realDevice": realDevice,
156
+ }
157
+ vars_prompt = "\n".join([f" {k}" for k in locals_dict.keys()])
158
+ banner = f"Python REPL variables:\n{vars_prompt}"
159
+ print_formatted_text(banner)
160
+ await embed(
161
+ return_asyncio_coroutine=True,
162
+ globals=global_dict,
163
+ locals=locals_dict,
164
+ configure=configure,
165
+ )
328
166
 
329
- def forward_to_socket():
330
- prompt_count = 0
331
- while True:
332
- data = telnet_client.read_some()
333
- if not data:
334
- conn.sendall("REPL exited".encode())
335
- break
336
- if b">>>" in data:
337
- # This is an ugly hack - somewhere in ptpython, the
338
- # initial prompt is being printed many times. Normal
339
- # telnet clients handle it properly, but xtermjs doesn't
340
- # like it. We just replace the first few with spaces
341
- # so it's not too ugly.
342
- prompt_count += 1
343
- if prompt_count < 5:
344
- data = data.replace(b">>>", b" ")
345
- conn.sendall(data)
346
- conn.close()
347
- exit_server()
348
-
349
- threading.Thread(target=forward_to_telnet).start()
350
- threading.Thread(target=forward_to_socket).start()
351
-
352
- threading.Thread(target=finish_setup).start()
353
-
354
- repl_loop.run_until_complete(start_telnet_repl(server_started_future, filter))
355
-
356
- def accept_connection():
357
- while True:
358
- conn, addr = sock.accept()
359
- threading.Thread(target=handle_connection, args=(conn,)).start()
360
-
361
- threading.Thread(target=accept_connection).start()
362
-
363
- proxy_port = sock.getsockname()[1]
364
- return proxy_port
167
+ repl_server = await cluster_listen_zero(on_repl_client)
168
+ return repl_server["port"]
@@ -38,7 +38,7 @@ export class PluginHostAPI extends PluginAPIManagedListeners implements PluginAP
38
38
  logger.log('w', 'plugin restart was requested, but a different instance was found. restart cancelled.');
39
39
  return;
40
40
  }
41
- this.scrypted.runPlugin(plugin);
41
+ await this.scrypted.runPlugin(plugin);
42
42
  }, 15000);
43
43
 
44
44
  constructor(public scrypted: ScryptedRuntime, pluginId: string, public pluginHost: PluginHost, public mediaManager: MediaManager) {
@@ -450,10 +450,6 @@ export class PluginHost {
450
450
  this.peer.kill('plugin disconnected');
451
451
  };
452
452
 
453
- this.worker.on('close', () => {
454
- logger.log('e', `${this.pluginName} close`);
455
- disconnect();
456
- });
457
453
  this.worker.on('exit', async (code, signal) => {
458
454
  logger.log('e', `${this.pluginName} exited ${code} ${signal}`);
459
455
  disconnect();
@@ -1,4 +1,5 @@
1
1
  import child_process from 'child_process';
2
+ import { once } from 'events';
2
3
  import { EventEmitter } from "ws";
3
4
  import { RpcMessage, RpcPeer } from "../../rpc";
4
5
  import { RuntimeWorker, RuntimeWorkerOptions } from "./runtime-worker";
@@ -6,6 +7,7 @@ import { RuntimeWorker, RuntimeWorkerOptions } from "./runtime-worker";
6
7
  export abstract class ChildProcessWorker extends EventEmitter implements RuntimeWorker {
7
8
  public pluginId: string;
8
9
  protected worker: child_process.ChildProcess;
10
+ killPromise: Promise<void>;
9
11
 
10
12
  get childProcess() {
11
13
  return this.worker;
@@ -14,10 +16,11 @@ export abstract class ChildProcessWorker extends EventEmitter implements Runtime
14
16
  constructor(options: RuntimeWorkerOptions) {
15
17
  super();
16
18
  this.pluginId = options.packageJson.name;
19
+
17
20
  }
18
21
 
19
22
  setupWorker() {
20
- this.worker.on('close', (code: number | null, signal: NodeJS.Signals | null) => this.emit('close', code, signal));
23
+ this.worker.on('close', () => this.emit('error', new Error('close')));
21
24
  this.worker.on('disconnect', () => this.emit('error', new Error('disconnect')));
22
25
  this.worker.on('exit', (code, signal) => this.emit('exit', code, signal));
23
26
  this.worker.on('error', e => this.emit('error', e));
@@ -27,6 +30,8 @@ export abstract class ChildProcessWorker extends EventEmitter implements Runtime
27
30
  if (stdio)
28
31
  stdio.on('error', e => this.emit('error', e));
29
32
  }
33
+
34
+ this.killPromise = once(this.worker, 'exit').then(() => {}).catch(() => {});
30
35
  }
31
36
 
32
37
  get pid() {
@@ -1,4 +1,4 @@
1
- import { EventEmitter } from "events";
1
+ import { EventEmitter, once } from "events";
2
2
  import worker_threads from "worker_threads";
3
3
  import { RpcMessage, RpcPeer, RpcSerializer } from "../../rpc";
4
4
  import { BufferSerializer } from '../../rpc-buffer-serializer';
@@ -47,6 +47,7 @@ interface PortMessage {
47
47
  export class NodeThreadWorker extends EventEmitter implements RuntimeWorker {
48
48
  worker: worker_threads.Worker;
49
49
  port: worker_threads.MessagePort;
50
+ killPromise: Promise<void>;
50
51
 
51
52
  constructor(mainFilename: string, public pluginId: string, options: RuntimeWorkerOptions, workerOptions?: worker_threads.WorkerOptions, workerData?: any, transferList: Array<worker_threads.TransferListItem> = []) {
52
53
  super();
@@ -82,6 +83,8 @@ export class NodeThreadWorker extends EventEmitter implements RuntimeWorker {
82
83
  this.port.on('close', () => {
83
84
  this.emit('error', new Error('port closed'));
84
85
  });
86
+
87
+ this.killPromise = once(this.worker, 'exit').then(() => {}).catch(() => {});
85
88
  }
86
89
 
87
90
  get pid() {
@@ -1,7 +1,7 @@
1
+ import net from "net";
2
+ import { Readable } from "stream";
1
3
  import { RpcMessage, RpcPeer } from "../../rpc";
2
4
  import { PluginDebug } from "../plugin-debug";
3
- import { Readable } from "stream";
4
- import net from "net";
5
5
 
6
6
  export interface RuntimeWorkerOptions {
7
7
  packageJson: any;
@@ -16,6 +16,7 @@ export interface RuntimeWorker {
16
16
  pid: number;
17
17
  stdout: Readable;
18
18
  stderr: Readable;
19
+ killPromise: Promise<any>;
19
20
 
20
21
  kill(): void;
21
22
 
@@ -23,15 +24,12 @@ export interface RuntimeWorker {
23
24
 
24
25
  on(event: 'error', listener: (err: Error) => void): this;
25
26
  on(event: 'exit', listener: (code: number | null, signal: NodeJS.Signals | null) => void): this;
26
- on(event: 'close', listener: (code: number | null, signal: NodeJS.Signals | null) => void): this;
27
27
 
28
28
  once(event: 'error', listener: (err: Error) => void): this;
29
29
  once(event: 'exit', listener: (code: number | null, signal: NodeJS.Signals | null) => void): this;
30
- once(event: 'close', listener: (code: number | null, signal: NodeJS.Signals | null) => void): this;
31
30
 
32
31
  removeListener(event: 'error', listener: (err: Error) => void): this;
33
32
  removeListener(event: 'exit', listener: (code: number | null, signal: NodeJS.Signals | null) => void): this;
34
- removeListener(event: 'close', listener: (code: number | null, signal: NodeJS.Signals | null) => void): this;
35
33
 
36
34
  send(message: RpcMessage, reject?: (e: Error) => void, serializationContext?: any): void;
37
35
 
@@ -0,0 +1,96 @@
1
+ export interface RefreshPromise<T> {
2
+ promise: Promise<T>;
3
+ cacheDuration: number;
4
+ }
5
+
6
+ export function singletonPromise<T>(rp: undefined | RefreshPromise<T>, method: () => Promise<T>, cacheDuration = 0) {
7
+ if (rp?.promise)
8
+ return rp;
9
+
10
+ const promise = method();
11
+ if (!rp) {
12
+ rp = {
13
+ promise,
14
+ cacheDuration,
15
+ }
16
+ }
17
+ else {
18
+ rp.promise = promise;
19
+ }
20
+ promise.finally(() => setTimeout(() => rp.promise = undefined, rp.cacheDuration));
21
+ return rp;
22
+ }
23
+
24
+ export class TimeoutError<T> extends Error {
25
+ constructor(public promise: Promise<T>) {
26
+ super('Operation Timed Out');
27
+ }
28
+ }
29
+
30
+ export function timeoutPromise<T>(timeout: number, promise: Promise<T>): Promise<T> {
31
+ return new Promise<T>((resolve, reject) => {
32
+ const t = setTimeout(() => reject(new TimeoutError(promise)), timeout);
33
+
34
+ promise
35
+ .then(v => {
36
+ clearTimeout(t);
37
+ resolve(v);
38
+ })
39
+ .catch(e => {
40
+ clearTimeout(t);
41
+ reject(e);
42
+ });
43
+ })
44
+ }
45
+
46
+ export function timeoutFunction<T>(timeout: number, f: (isTimedOut: () => boolean) => Promise<T>): Promise<T> {
47
+ return new Promise<T>((resolve, reject) => {
48
+ let isTimedOut = false;
49
+ const promise = f(() => isTimedOut);
50
+
51
+ const t = setTimeout(() => {
52
+ isTimedOut = true;
53
+ reject(new TimeoutError(promise));
54
+ }, timeout);
55
+
56
+ promise
57
+ .then(v => {
58
+ clearTimeout(t);
59
+ resolve(v);
60
+ })
61
+ .catch(e => {
62
+ clearTimeout(t);
63
+ reject(e);
64
+ });
65
+ })
66
+ }
67
+
68
+ export function createPromiseDebouncer<T>() {
69
+ let current: Promise<T>;
70
+
71
+ return (func: () => Promise<T>): Promise<T> => {
72
+ if (!current)
73
+ current = func().finally(() => current = undefined);
74
+ return current;
75
+ }
76
+ }
77
+
78
+ export function createMapPromiseDebouncer<T>() {
79
+ const map = new Map<string, Promise<T>>();
80
+
81
+ return (key: any, debounce: number, func: () => Promise<T>): Promise<T> => {
82
+ const keyStr = JSON.stringify(key);
83
+ let value = map.get(keyStr);
84
+ if (!value) {
85
+ value = func().finally(() => {
86
+ if (!debounce) {
87
+ map.delete(keyStr);
88
+ return;
89
+ }
90
+ setTimeout(() => map.delete(keyStr), debounce);
91
+ });
92
+ map.set(keyStr, value);
93
+ }
94
+ return value;
95
+ }
96
+ }