mirror of
https://github.com/denoland/deno.git
synced 2024-11-24 15:19:26 -05:00
Add dispatch pub/sub
This commit is contained in:
parent
9a66216599
commit
08307fb841
9 changed files with 244 additions and 124 deletions
13
Makefile
13
Makefile
|
@ -1,5 +1,6 @@
|
||||||
TS_FILES = \
|
TS_FILES = \
|
||||||
tsconfig.json \
|
tsconfig.json \
|
||||||
|
dispatch.ts \
|
||||||
main.ts \
|
main.ts \
|
||||||
msg.pb.d.ts \
|
msg.pb.d.ts \
|
||||||
msg.pb.js \
|
msg.pb.js \
|
||||||
|
@ -10,7 +11,17 @@ TS_FILES = \
|
||||||
util.ts \
|
util.ts \
|
||||||
v8_source_maps.ts
|
v8_source_maps.ts
|
||||||
|
|
||||||
deno: assets.go msg.pb.go main.go
|
GO_FILES = \
|
||||||
|
assets.go \
|
||||||
|
deno_dir.go \
|
||||||
|
dispatch.go \
|
||||||
|
handlers.go \
|
||||||
|
main.go \
|
||||||
|
main_test.go \
|
||||||
|
msg.pb.go \
|
||||||
|
util.go
|
||||||
|
|
||||||
|
deno: $(GO_FILES)
|
||||||
go build -o deno
|
go build -o deno
|
||||||
|
|
||||||
assets.go: dist/main.js
|
assets.go: dist/main.js
|
||||||
|
|
90
dispatch.go
Normal file
90
dispatch.go
Normal file
|
@ -0,0 +1,90 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/golang/protobuf/proto"
|
||||||
|
"github.com/ry/v8worker2"
|
||||||
|
"sync"
|
||||||
|
)
|
||||||
|
|
||||||
|
// There is a single global worker for this process.
|
||||||
|
// This file should be the only part of deno that directly access it, so that
|
||||||
|
// all interaction with V8 can go through a single point.
|
||||||
|
var worker *v8worker2.Worker
|
||||||
|
|
||||||
|
var channels = make(map[string][]Subscriber)
|
||||||
|
|
||||||
|
type Subscriber func(payload []byte) []byte
|
||||||
|
|
||||||
|
func createWorker() {
|
||||||
|
worker = v8worker2.New(recv)
|
||||||
|
}
|
||||||
|
|
||||||
|
func recv(buf []byte) (response []byte) {
|
||||||
|
msg := &BaseMsg{}
|
||||||
|
check(proto.Unmarshal(buf, msg))
|
||||||
|
assert(len(msg.Payload) > 0, "BaseMsg has empty payload.")
|
||||||
|
subscribers, ok := channels[msg.Channel]
|
||||||
|
if !ok {
|
||||||
|
panic("No subscribers for channel " + msg.Channel)
|
||||||
|
}
|
||||||
|
for i := 0; i < len(subscribers); i++ {
|
||||||
|
s := subscribers[i]
|
||||||
|
r := s(msg.Payload)
|
||||||
|
if r != nil {
|
||||||
|
response = r
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return response
|
||||||
|
}
|
||||||
|
|
||||||
|
func Sub(channel string, cb Subscriber) {
|
||||||
|
subscribers, ok := channels[channel]
|
||||||
|
if !ok {
|
||||||
|
subscribers = make([]Subscriber, 0)
|
||||||
|
}
|
||||||
|
subscribers = append(subscribers, cb)
|
||||||
|
channels[channel] = subscribers
|
||||||
|
}
|
||||||
|
|
||||||
|
func Pub(channel string, payload []byte) {
|
||||||
|
resChan <- &BaseMsg{
|
||||||
|
Channel: channel,
|
||||||
|
Payload: payload,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var resChan = make(chan *BaseMsg, 10)
|
||||||
|
var doneChan = make(chan bool)
|
||||||
|
var wg sync.WaitGroup
|
||||||
|
|
||||||
|
func DispatchLoop() {
|
||||||
|
wg.Add(1)
|
||||||
|
first := true
|
||||||
|
|
||||||
|
// In a goroutine, we wait on for all goroutines to complete (for example
|
||||||
|
// timers). We use this to signal to the main thread to exit.
|
||||||
|
go func() {
|
||||||
|
wg.Wait()
|
||||||
|
doneChan <- true
|
||||||
|
}()
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case msg := <-resChan:
|
||||||
|
out, err := proto.Marshal(msg)
|
||||||
|
err = worker.SendBytes(out)
|
||||||
|
check(err)
|
||||||
|
case <-doneChan:
|
||||||
|
// All goroutines have completed. Now we can exit main().
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// We don't want to exit until we've received at least one message.
|
||||||
|
// This is so the program doesn't exit after sending the "start"
|
||||||
|
// message.
|
||||||
|
if first {
|
||||||
|
wg.Done()
|
||||||
|
}
|
||||||
|
first = false
|
||||||
|
}
|
||||||
|
}
|
61
dispatch.ts
Normal file
61
dispatch.ts
Normal file
|
@ -0,0 +1,61 @@
|
||||||
|
import { typedArrayToArrayBuffer } from "./util";
|
||||||
|
import { _global } from "./globals";
|
||||||
|
import { main as pb } from "./msg.pb";
|
||||||
|
|
||||||
|
type MessageCallback = (msg: Uint8Array) => void;
|
||||||
|
|
||||||
|
const send = V8Worker2.send;
|
||||||
|
const channels = new Map<string, MessageCallback[]>();
|
||||||
|
|
||||||
|
export function sub(channel: string, cb: MessageCallback): void {
|
||||||
|
let subscribers = channels.get(channel);
|
||||||
|
if (!subscribers) {
|
||||||
|
subscribers = [];
|
||||||
|
channels.set(channel, subscribers);
|
||||||
|
}
|
||||||
|
subscribers.push(cb);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function pub(channel: string, payload: Uint8Array): null | ArrayBuffer {
|
||||||
|
const msg = pb.BaseMsg.fromObject({ channel, payload });
|
||||||
|
const ui8 = pb.BaseMsg.encode(msg).finish();
|
||||||
|
const ab = typedArrayToArrayBuffer(ui8);
|
||||||
|
return send(ab);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Internal version of "pub".
|
||||||
|
// TODO add internal version of "sub"
|
||||||
|
// TODO rename to pubInternal()
|
||||||
|
export function sendMsgFromObject(
|
||||||
|
channel: string,
|
||||||
|
obj: pb.IMsg
|
||||||
|
): null | pb.Msg {
|
||||||
|
const msg = pb.Msg.fromObject(obj);
|
||||||
|
const ui8 = pb.Msg.encode(msg).finish();
|
||||||
|
const resBuf = pub(channel, ui8);
|
||||||
|
if (resBuf != null && resBuf.byteLength > 0) {
|
||||||
|
const res = pb.Msg.decode(new Uint8Array(resBuf));
|
||||||
|
if (res != null && res.error != null && res.error.length > 0) {
|
||||||
|
throw Error(res.error);
|
||||||
|
}
|
||||||
|
return res;
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
V8Worker2.recv((ab: ArrayBuffer) => {
|
||||||
|
const msg = pb.BaseMsg.decode(new Uint8Array(ab));
|
||||||
|
const subscribers = channels.get(msg.channel);
|
||||||
|
if (subscribers == null) {
|
||||||
|
throw Error(`No subscribers for channel "${msg.channel}".`);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const subscriber of subscribers) {
|
||||||
|
subscriber(msg.payload);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Delete the V8Worker2 from the global object, so that no one else can receive
|
||||||
|
// messages.
|
||||||
|
_global["V8Worker2"] = null;
|
59
handlers.go
59
handlers.go
|
@ -10,29 +10,38 @@ import (
|
||||||
|
|
||||||
const assetPrefix string = "/$asset$/"
|
const assetPrefix string = "/$asset$/"
|
||||||
|
|
||||||
func recv(buf []byte) []byte {
|
func InitHandlers() {
|
||||||
msg := &Msg{}
|
Sub("os", func(buf []byte) []byte {
|
||||||
err := proto.Unmarshal(buf, msg)
|
msg := &Msg{}
|
||||||
check(err)
|
check(proto.Unmarshal(buf, msg))
|
||||||
switch msg.Payload.(type) {
|
switch msg.Payload.(type) {
|
||||||
case *Msg_Exit:
|
case *Msg_Exit:
|
||||||
payload := msg.GetExit()
|
payload := msg.GetExit()
|
||||||
os.Exit(int(payload.Code))
|
os.Exit(int(payload.Code))
|
||||||
case *Msg_SourceCodeFetch:
|
case *Msg_SourceCodeFetch:
|
||||||
payload := msg.GetSourceCodeFetch()
|
payload := msg.GetSourceCodeFetch()
|
||||||
return HandleSourceCodeFetch(payload.ModuleSpecifier, payload.ContainingFile)
|
return HandleSourceCodeFetch(payload.ModuleSpecifier, payload.ContainingFile)
|
||||||
case *Msg_SourceCodeCache:
|
case *Msg_SourceCodeCache:
|
||||||
payload := msg.GetSourceCodeCache()
|
payload := msg.GetSourceCodeCache()
|
||||||
return HandleSourceCodeCache(payload.Filename, payload.SourceCode,
|
return HandleSourceCodeCache(payload.Filename, payload.SourceCode,
|
||||||
payload.OutputCode)
|
payload.OutputCode)
|
||||||
case *Msg_TimerStart:
|
default:
|
||||||
payload := msg.GetTimerStart()
|
panic("[os] Unexpected message " + string(buf))
|
||||||
return HandleTimerStart(payload.Id, payload.Interval, payload.Duration)
|
}
|
||||||
default:
|
return nil
|
||||||
panic("Unexpected message")
|
})
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
Sub("timers", func(buf []byte) []byte {
|
||||||
|
msg := &Msg{}
|
||||||
|
check(proto.Unmarshal(buf, msg))
|
||||||
|
switch msg.Payload.(type) {
|
||||||
|
case *Msg_TimerStart:
|
||||||
|
payload := msg.GetTimerStart()
|
||||||
|
return HandleTimerStart(payload.Id, payload.Interval, payload.Duration)
|
||||||
|
default:
|
||||||
|
panic("[timers] Unexpected message " + string(buf))
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func HandleSourceCodeFetch(moduleSpecifier string, containingFile string) (out []byte) {
|
func HandleSourceCodeFetch(moduleSpecifier string, containingFile string) (out []byte) {
|
||||||
|
@ -107,13 +116,15 @@ func HandleTimerStart(id int32, interval bool, duration int32) []byte {
|
||||||
go func() {
|
go func() {
|
||||||
defer wg.Done()
|
defer wg.Done()
|
||||||
time.Sleep(time.Duration(duration) * time.Millisecond)
|
time.Sleep(time.Duration(duration) * time.Millisecond)
|
||||||
resChan <- &Msg{
|
payload, err := proto.Marshal(&Msg{
|
||||||
Payload: &Msg_TimerReady{
|
Payload: &Msg_TimerReady{
|
||||||
TimerReady: &TimerReadyMsg{
|
TimerReady: &TimerReadyMsg{
|
||||||
Id: id,
|
Id: id,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
})
|
||||||
|
check(err)
|
||||||
|
Pub("timers", payload)
|
||||||
}()
|
}()
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
35
main.go
35
main.go
|
@ -8,7 +8,6 @@ import (
|
||||||
"net/url"
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
"path"
|
"path"
|
||||||
"sync"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var flagReload = flag.Bool("reload", false, "Reload cached remote source code.")
|
var flagReload = flag.Bool("reload", false, "Reload cached remote source code.")
|
||||||
|
@ -19,9 +18,6 @@ var DenoDir string
|
||||||
var CompileDir string
|
var CompileDir string
|
||||||
var SrcDir string
|
var SrcDir string
|
||||||
|
|
||||||
var wg sync.WaitGroup
|
|
||||||
var resChan chan *Msg
|
|
||||||
|
|
||||||
func ResolveModule(moduleSpecifier string, containingFile string) (
|
func ResolveModule(moduleSpecifier string, containingFile string) (
|
||||||
moduleName string, filename string, err error) {
|
moduleName string, filename string, err error) {
|
||||||
moduleUrl, err := url.Parse(moduleSpecifier)
|
moduleUrl, err := url.Parse(moduleSpecifier)
|
||||||
|
@ -58,7 +54,8 @@ func main() {
|
||||||
args = v8worker2.SetFlags(args)
|
args = v8worker2.SetFlags(args)
|
||||||
|
|
||||||
createDirs()
|
createDirs()
|
||||||
worker := v8worker2.New(recv)
|
createWorker()
|
||||||
|
InitHandlers()
|
||||||
|
|
||||||
main_js := stringAsset("main.js")
|
main_js := stringAsset("main.js")
|
||||||
check(worker.Load("/main.js", main_js))
|
check(worker.Load("/main.js", main_js))
|
||||||
|
@ -67,9 +64,6 @@ func main() {
|
||||||
cwd, err := os.Getwd()
|
cwd, err := os.Getwd()
|
||||||
check(err)
|
check(err)
|
||||||
|
|
||||||
resChan = make(chan *Msg)
|
|
||||||
doneChan := make(chan bool)
|
|
||||||
|
|
||||||
out, err := proto.Marshal(&Msg{
|
out, err := proto.Marshal(&Msg{
|
||||||
Payload: &Msg_Start{
|
Payload: &Msg_Start{
|
||||||
Start: &StartMsg{
|
Start: &StartMsg{
|
||||||
|
@ -82,28 +76,7 @@ func main() {
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
check(err)
|
check(err)
|
||||||
err = worker.SendBytes(out)
|
Pub("start", out)
|
||||||
if err != nil {
|
|
||||||
os.Stderr.WriteString(err.Error())
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
// In a goroutine, we wait on for all goroutines to complete (for example
|
DispatchLoop()
|
||||||
// timers). We use this to signal to the main thread to exit.
|
|
||||||
go func() {
|
|
||||||
wg.Wait()
|
|
||||||
doneChan <- true
|
|
||||||
}()
|
|
||||||
|
|
||||||
for {
|
|
||||||
select {
|
|
||||||
case msg := <-resChan:
|
|
||||||
out, err := proto.Marshal(msg)
|
|
||||||
err = worker.SendBytes(out)
|
|
||||||
check(err)
|
|
||||||
case <-doneChan:
|
|
||||||
// All goroutines have completed. Now we can exit main().
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
46
main.ts
46
main.ts
|
@ -1,47 +1,33 @@
|
||||||
|
import * as dispatch from "./dispatch";
|
||||||
import { main as pb } from "./msg.pb";
|
import { main as pb } from "./msg.pb";
|
||||||
import "./util";
|
|
||||||
import * as runtime from "./runtime";
|
import * as runtime from "./runtime";
|
||||||
import * as timers from "./timers";
|
|
||||||
import * as util from "./util";
|
import * as util from "./util";
|
||||||
|
|
||||||
|
// These have top-level functions that need to execute.
|
||||||
|
import { initTimers } from "./timers";
|
||||||
|
|
||||||
// To control internal logging output
|
// To control internal logging output
|
||||||
// Set with the -debug command-line flag.
|
// Set with the -debug command-line flag.
|
||||||
export let debug = false;
|
export let debug = false;
|
||||||
|
let startCalled = false;
|
||||||
|
|
||||||
|
dispatch.sub("start", (payload: Uint8Array) => {
|
||||||
|
if (startCalled) {
|
||||||
|
throw Error("start message received more than once!");
|
||||||
|
}
|
||||||
|
startCalled = true;
|
||||||
|
|
||||||
|
const msg = pb.Msg.decode(payload);
|
||||||
|
const { cwd, argv, debugFlag, mainJs, mainMap } = msg.start;
|
||||||
|
|
||||||
function start(
|
|
||||||
cwd: string,
|
|
||||||
argv: string[],
|
|
||||||
debugFlag: boolean,
|
|
||||||
mainJs: string,
|
|
||||||
mainMap: string
|
|
||||||
): void {
|
|
||||||
debug = debugFlag;
|
debug = debugFlag;
|
||||||
util.log("start", { cwd, argv, debugFlag });
|
util.log("start", { cwd, argv, debugFlag });
|
||||||
|
|
||||||
|
initTimers();
|
||||||
runtime.setup(mainJs, mainMap);
|
runtime.setup(mainJs, mainMap);
|
||||||
|
|
||||||
const inputFn = argv[0];
|
const inputFn = argv[0];
|
||||||
const mod = runtime.resolveModule(inputFn, cwd + "/");
|
const mod = runtime.resolveModule(inputFn, cwd + "/");
|
||||||
mod.compileAndRun();
|
mod.compileAndRun();
|
||||||
}
|
|
||||||
|
|
||||||
V8Worker2.recv((ab: ArrayBuffer) => {
|
|
||||||
const msg = pb.Msg.decode(new Uint8Array(ab));
|
|
||||||
switch (msg.payload) {
|
|
||||||
case "start":
|
|
||||||
start(
|
|
||||||
msg.start.cwd,
|
|
||||||
msg.start.argv,
|
|
||||||
msg.start.debugFlag,
|
|
||||||
msg.start.mainJs,
|
|
||||||
msg.start.mainMap
|
|
||||||
);
|
|
||||||
break;
|
|
||||||
case "timerReady":
|
|
||||||
timers.timerReady(msg.timerReady.id, msg.timerReady.done);
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
console.log("Unknown message", msg);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,6 +1,11 @@
|
||||||
syntax = "proto3";
|
syntax = "proto3";
|
||||||
package main;
|
package main;
|
||||||
|
|
||||||
|
message BaseMsg {
|
||||||
|
string channel = 1;
|
||||||
|
bytes payload = 2;
|
||||||
|
}
|
||||||
|
|
||||||
message Msg {
|
message Msg {
|
||||||
string error = 1;
|
string error = 1;
|
||||||
oneof payload {
|
oneof payload {
|
||||||
|
|
32
os.ts
32
os.ts
|
@ -1,18 +1,15 @@
|
||||||
import { main as pb } from "./msg.pb";
|
|
||||||
import { ModuleInfo } from "./types";
|
import { ModuleInfo } from "./types";
|
||||||
import { typedArrayToArrayBuffer } from "./util";
|
import { sendMsgFromObject } from "./dispatch";
|
||||||
|
|
||||||
export function exit(code = 0): void {
|
export function exit(code = 0): void {
|
||||||
sendMsgFromObject({
|
sendMsgFromObject("os", { exit: { code } });
|
||||||
exit: { code }
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function sourceCodeFetch(
|
export function sourceCodeFetch(
|
||||||
moduleSpecifier: string,
|
moduleSpecifier: string,
|
||||||
containingFile: string
|
containingFile: string
|
||||||
): ModuleInfo {
|
): ModuleInfo {
|
||||||
const res = sendMsgFromObject({
|
const res = sendMsgFromObject("os", {
|
||||||
sourceCodeFetch: { moduleSpecifier, containingFile }
|
sourceCodeFetch: { moduleSpecifier, containingFile }
|
||||||
});
|
});
|
||||||
return res.sourceCodeFetchRes;
|
return res.sourceCodeFetchRes;
|
||||||
|
@ -23,28 +20,7 @@ export function sourceCodeCache(
|
||||||
sourceCode: string,
|
sourceCode: string,
|
||||||
outputCode: string
|
outputCode: string
|
||||||
): void {
|
): void {
|
||||||
const res = sendMsgFromObject({
|
sendMsgFromObject("os", {
|
||||||
sourceCodeCache: { filename, sourceCode, outputCode }
|
sourceCodeCache: { filename, sourceCode, outputCode }
|
||||||
});
|
});
|
||||||
throwOnError(res);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function sendMsgFromObject(obj: pb.IMsg): null | pb.Msg {
|
|
||||||
const msg = pb.Msg.fromObject(obj);
|
|
||||||
const ui8 = pb.Msg.encode(msg).finish();
|
|
||||||
const ab = typedArrayToArrayBuffer(ui8);
|
|
||||||
const resBuf = V8Worker2.send(ab);
|
|
||||||
if (resBuf != null && resBuf.byteLength > 0) {
|
|
||||||
const res = pb.Msg.decode(new Uint8Array(resBuf));
|
|
||||||
throwOnError(res);
|
|
||||||
return res;
|
|
||||||
} else {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function throwOnError(res: pb.Msg) {
|
|
||||||
if (res != null && res.error != null && res.error.length > 0) {
|
|
||||||
throw Error(res.error);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
27
timers.ts
27
timers.ts
|
@ -1,4 +1,5 @@
|
||||||
import { sendMsgFromObject } from "./os";
|
import { main as pb } from "./msg.pb";
|
||||||
|
import * as dispatch from "./dispatch";
|
||||||
|
|
||||||
let nextTimerId = 1;
|
let nextTimerId = 1;
|
||||||
|
|
||||||
|
@ -14,6 +15,20 @@ interface Timer {
|
||||||
|
|
||||||
const timers = new Map<number, Timer>();
|
const timers = new Map<number, Timer>();
|
||||||
|
|
||||||
|
export function initTimers() {
|
||||||
|
dispatch.sub("timers", onMessage);
|
||||||
|
}
|
||||||
|
|
||||||
|
function onMessage(payload: Uint8Array) {
|
||||||
|
const msg = pb.Msg.decode(payload);
|
||||||
|
const { id, done } = msg.timerReady;
|
||||||
|
const timer = timers.get(id);
|
||||||
|
timer.cb();
|
||||||
|
if (done) {
|
||||||
|
timers.delete(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export function setTimeout(cb: TimerCallback, duration: number): number {
|
export function setTimeout(cb: TimerCallback, duration: number): number {
|
||||||
const timer = {
|
const timer = {
|
||||||
id: nextTimerId++,
|
id: nextTimerId++,
|
||||||
|
@ -22,7 +37,7 @@ export function setTimeout(cb: TimerCallback, duration: number): number {
|
||||||
cb
|
cb
|
||||||
};
|
};
|
||||||
timers.set(timer.id, timer);
|
timers.set(timer.id, timer);
|
||||||
sendMsgFromObject({
|
dispatch.sendMsgFromObject("timers", {
|
||||||
timerStart: {
|
timerStart: {
|
||||||
id: timer.id,
|
id: timer.id,
|
||||||
interval: false,
|
interval: false,
|
||||||
|
@ -31,11 +46,3 @@ export function setTimeout(cb: TimerCallback, duration: number): number {
|
||||||
});
|
});
|
||||||
return timer.id;
|
return timer.id;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function timerReady(id: number, done: boolean): void {
|
|
||||||
const timer = timers.get(id);
|
|
||||||
timer.cb();
|
|
||||||
if (done) {
|
|
||||||
timers.delete(id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
Loading…
Reference in a new issue