From e2cce14a316bb7e47d5d5fe7f6baf206f7a65a95 Mon Sep 17 00:00:00 2001 From: Ilya Sher <34807039+ilyash-b@users.noreply.github.com> Date: Mon, 16 Sep 2024 09:40:50 +0400 Subject: [PATCH 01/12] WIP shell polling --- lib/autoload/globals/IO.ngs | 22 ++++++++++++++++++++-- lib/autoload/globals/Timeline.ngs | 31 +++++++++++++++++++++++++++++-- lib/autoload/globals/net.ngs | 6 +++++- lib/autoload/globals/ui.ngs | 11 ++++++++++- lib/shell.ngs | 21 +++++++++++---------- 5 files changed, 75 insertions(+), 16 deletions(-) diff --git a/lib/autoload/globals/IO.ngs b/lib/autoload/globals/IO.ngs index 3ab12a74..219f1537 100644 --- a/lib/autoload/globals/IO.ngs +++ b/lib/autoload/globals/IO.ngs @@ -139,9 +139,16 @@ ns { F init(jrc:JsonRpcClient) { jrc.callbacks = {} jrc.id = Iter(1..null) + jrc.pipeline = null debug('client', 'JsonRpcClient()') } + F handle(jrc:JsonRpcClient, hc:COR::HandlerContext, a:events::Active) { + debug('client', 'JsonRpcClient Active') + jrc.pipeline = hc.pipeline + hc.fire(a) + } + F handle(jrc:JsonRpcClient, hc:COR::HandlerContext, r:events::Read) block b { data = r.data debug('client', "JsonRpcClient Read") @@ -149,7 +156,7 @@ ns { F handle(jrc:JsonRpcClient, hc:COR::HandlerContext, w:events::Write) block b { data = w.data - debug('client', "JsonRpcClient Write") + debug('client', "JsonRpcClient Write ${w.data.SafeStr()}") id = w.data.get('id', { jrc.id.next() }) w2 = events::Write({ 'jsonrpc': '2.0' @@ -166,7 +173,7 @@ ns { # TODO: make this more resilient F handle(jrc:JsonRpcClient, hc:COR::HandlerContext, r:events::Read) block b { data = r.data - debug('client', "JsonRpcClient Read ${data}") + debug('client', "JsonRpcClient Read ${data.SafeStr()}") res = try { data.decode_json() @@ -184,6 +191,17 @@ ns { ) } + global call + F call(jrc:JsonRpcClient, method:Str, params=[], callback:Fun=nop) { + assert(jrc.pipeline, 'Before using JsonRpcClient#call, call fire(COR::Pipeline, IO::events::Active()) on the pipeline where JsonRpcClient were pushed') + jrc.pipeline.fire(events::Write({ + 'call': { + 'method': method + 'params': params + } + 'callback': callback + })) + } } diff --git a/lib/autoload/globals/Timeline.ngs b/lib/autoload/globals/Timeline.ngs index bed17f6b..a830a20f 100644 --- a/lib/autoload/globals/Timeline.ngs +++ b/lib/autoload/globals/Timeline.ngs @@ -4,6 +4,33 @@ ns { global Timeline, Time, Lines, init, each, map, sort, push, echo_cli + section "TimelineItem" { + + global TimelineItem + doc time - Time + type TimelineItem + + F init(ti:TimelineItem, time:Time) { + ti.id = "ti-${`line: uuidgen`}" # temporary + ti.time = time + } + + F Time(ti:TimelineItem) ti.time + } + + section "TextualCommandTimelineItem" { + + global TextualCommandTimelineItem + type TextualCommandTimelineItem(TimelineItem) + + F init(ti:TextualCommandTimelineItem) throw InvalidArgument("TextualCommandTimelineItem() was called without arguments") + + F init(ti:TextualCommandTimelineItem, time:Time, command:Str) { + super(ti, time) + ti.command = command + } + } + section "Timeline" { doc time_start - Time @@ -32,8 +59,8 @@ ns { t.sort() } - F push(t:Timeline, x:Timeline) t::{ - A.items.push(x) + F push(t:Timeline, ti:TimelineItem) t::{ + A.items.push(ti) A.sort() # TODO: insert in the correct place, keeping .items sorted by time # TODO: maximize time_last_update and time_end into t # TODO: minimize time_start into t diff --git a/lib/autoload/globals/net.ngs b/lib/autoload/globals/net.ngs index 42095bb4..7a401fe7 100644 --- a/lib/autoload/globals/net.ngs +++ b/lib/autoload/globals/net.ngs @@ -83,7 +83,7 @@ ns { global handle F handle(cpd:ConnectionPipelineDelegate, e:IO::events::Write) { - debug('server', "ConnectionPipelineDelegate Write") + debug('server', "ConnectionPipelineDelegate Write ${e.data.SafeStr()}") cpd.connection.send(e.data) } F handle(cpd:ConnectionPipelineDelegate, e:IO::events::Active) { @@ -255,13 +255,17 @@ ns { section "UnixStreamClient" { F connect(usc:UnixStreamClient, path:Str, cd:ClientDelegate) { + debug('client', 'UnixStreamClient#connect') sock = socket(C_PF_UNIX, C_SOCK_STREAM) usc.sock = sock connect(sock, c_sockaddr_un(path)) c = Connection(usc, sock) + debug('client', 'UnixStreamClient will call on_connect') cd.on_connect(c) while true { + debug('client', 'UnixStreamClient will recvfrom()') data = recvfrom(sock, 1024, 0, c_sockaddr_un()) + debug('client', "UnixStreamClient did recvfrom() ${data.SafeStr()}") if data == '' { cd.on_remote_close() break diff --git a/lib/autoload/globals/ui.ngs b/lib/autoload/globals/ui.ngs index 4d1bb990..8208281d 100644 --- a/lib/autoload/globals/ui.ngs +++ b/lib/autoload/globals/ui.ngs @@ -1,4 +1,4 @@ -ns { +ns(TL=Timeline, TCTI=TextualCommandTimelineItem) { # WIP global init, JsonData @@ -17,6 +17,8 @@ ns { type Element() type Screen(Element) + type Timeline(Element) + type TimelineItem(Element) type Object(Element) type Scalar(Element) @@ -85,6 +87,13 @@ ns { F keys_are_strings(h) h.keys().all(Str) F Element(h:AnyOf(Hash, HashLike)) Properties(h.assert(keys_are_strings, "Element(Hash) - keys must be strings").mapv(Element)) + F Element(tcti:TCTI) { + tcti.command.Element() + } + + F Element(tl:TL) { + Timeline(tl.items.map(Element)) + } # TODO: Fix later. It's semantically incorrect to display path as just a string F Element(p:Path) Scalar(p.path) diff --git a/lib/shell.ngs b/lib/shell.ngs index 2ce22e53..f162f68b 100644 --- a/lib/shell.ngs +++ b/lib/shell.ngs @@ -19,17 +19,16 @@ ns { F on_connect(scd:ShellClientDelegate, c:net::Connection) { debug('client', 'ShellClientDelegate#on_connect()') c.pipeline.push(IO::handlers::Splitter("\n")) - c.pipeline.push(IO::handlers::JsonRpcClient()) + jrc = IO::handlers::JsonRpcClient() + c.pipeline.push(jrc) c.pipeline.fire(IO::events::Active()) - c.pipeline.fire(IO::events::Write({ - 'call': { - "method": "eval" - "params": [cmd] - } - 'callback': F(response) { - debug('client', "CLIENT RESPONSE ${response}") - } - })) + + # TODO: something not callback-based + jrc.call('eval', [cmd], { + jrc.call('poll', [], { + echo("POLL RESULT ${A.SafeStr()}") + }) + }) } client = net::UnixStreamClient() @@ -131,6 +130,8 @@ ns { F poll() { log("poll() - not implemented yet") + result = ui::Element(tl) + result.JsonData()._censor() } section "tests" { From 0adb6b697985a05778e65a0927469e1852c9b16f Mon Sep 17 00:00:00 2001 From: Ilya Sher <34807039+ilyash-b@users.noreply.github.com> Date: Mon, 30 Sep 2024 10:33:39 +0400 Subject: [PATCH 02/12] [core] Fix edge case in del(Hash, Any) --- CHANGELOG.md | 1 + lib/lang-tests.ngs | 1 + obj.c | 1 + 3 files changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3a2e6f59..8b284bc7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -35,6 +35,7 @@ * Fix `after_last(Str, Str)` * `encode_json()` - now supports `{'pretty': 'best-effort'}` hint * Various build improvements +* Fix edge case in `del(Hash, Any)` ### Deprecated * Deprecated `Deep` in favor of `AtPath` diff --git a/lib/lang-tests.ngs b/lib/lang-tests.ngs index 8d9bd8ef..d9042603 100644 --- a/lib/lang-tests.ngs +++ b/lib/lang-tests.ngs @@ -51,6 +51,7 @@ TEST a=[1,2,3,4]; a[4..4]=["END"]; a[2..2]=["MIDDLE"]; a[0..0]=["START"]; a[0..1 # === Hash tests ================================= TEST h={"a": 1}; h.del("a"); h == {} +TEST h={"a": 1}; h.del("a"); h["b"]=2; h == {"b": 2} TEST h={}; try h.del("a") catch(e:KeyNotFound) true TEST h={"a": 1, "b": 2}; h.del("a"); h == {"b": 2} diff --git a/obj.c b/obj.c index 4f907fb6..cea7ddb0 100644 --- a/obj.c +++ b/obj.c @@ -512,6 +512,7 @@ void resize_hash_for_new_len(VALUE h, RESIZE_HASH_AFTER after) { if(!new_buckets_n) { OBJ_DATA_PTR(h) = NULL; + HASH_BUCKETS_N(h) = new_buckets_n; return; } From dd8292d015d3126e04c9d9a3e53c39650addd053 Mon Sep 17 00:00:00 2001 From: Ilya Sher <34807039+ilyash-b@users.noreply.github.com> Date: Thu, 3 Oct 2024 10:31:49 +0400 Subject: [PATCH 03/12] WIP shell polling --- lib/autoload/globals/net.ngs | 31 ++++++++++++++++++++++--------- lib/shell.ngs | 25 ++++++++++++++++--------- 2 files changed, 38 insertions(+), 18 deletions(-) diff --git a/lib/autoload/globals/net.ngs b/lib/autoload/globals/net.ngs index 7a401fe7..6e74e75e 100644 --- a/lib/autoload/globals/net.ngs +++ b/lib/autoload/globals/net.ngs @@ -254,6 +254,16 @@ ns { } section "UnixStreamClient" { + + block _ { + id = Iter(1..null) + global init + F init(usc:UnixStreamClient) { + super(usc) + usc.id = id.next() + } + } + F connect(usc:UnixStreamClient, path:Str, cd:ClientDelegate) { debug('client', 'UnixStreamClient#connect') sock = socket(C_PF_UNIX, C_SOCK_STREAM) @@ -262,16 +272,19 @@ ns { c = Connection(usc, sock) debug('client', 'UnixStreamClient will call on_connect') cd.on_connect(c) - while true { - debug('client', 'UnixStreamClient will recvfrom()') - data = recvfrom(sock, 1024, 0, c_sockaddr_un()) - debug('client', "UnixStreamClient did recvfrom() ${data.SafeStr()}") - if data == '' { - cd.on_remote_close() - break + usc.reader = Thread("UnixStreamClient-${usc.id}-receiver", { + while true { + debug('client', 'UnixStreamClient will recvfrom()') + data = recvfrom(sock, 1024, 0, c_sockaddr_un()) + debug('client', "UnixStreamClient did recvfrom() ${data.SafeStr()}") + if data == '' { + cd.on_remote_close() + break + } + c.pipeline.fire(IO::events::Read(data)) } - c.pipeline.fire(IO::events::Read(data)) - } + }) + usc } } } diff --git a/lib/shell.ngs b/lib/shell.ngs index f162f68b..50149155 100644 --- a/lib/shell.ngs +++ b/lib/shell.ngs @@ -14,25 +14,32 @@ ns { # TODO: make this method less verbose by adding functionality to libraries type ShellClientDelegate(net::ClientDelegate) scd = ShellClientDelegate() + jrc = IO::handlers::JsonRpcClient() global on_connect F on_connect(scd:ShellClientDelegate, c:net::Connection) { debug('client', 'ShellClientDelegate#on_connect()') c.pipeline.push(IO::handlers::Splitter("\n")) - jrc = IO::handlers::JsonRpcClient() c.pipeline.push(jrc) c.pipeline.fire(IO::events::Active()) - - # TODO: something not callback-based - jrc.call('eval', [cmd], { - jrc.call('poll', [], { - echo("POLL RESULT ${A.SafeStr()}") - }) - }) } client = net::UnixStreamClient() client.connect(SOCK_FILE, scd) + + # TODO: something not callback-based + jrc.call('eval', [cmd], { + log("EVAL RESULT ${A.SafeStr()}") + }) + while true { + log("Polling") + jrc.call('poll', [], { + echo("POLL RESULT ${A.SafeStr()}") + }) + $(log: sleep 5) + } + + } doc Starts shell server, JSON RPC proxy, and a browser client @@ -129,7 +136,7 @@ ns { } F poll() { - log("poll() - not implemented yet") + log("poll() - work in progress - returning Timeline") result = ui::Element(tl) result.JsonData()._censor() } From 37fa880a12f024f023ada1b2d0f3dd5e93a259e9 Mon Sep 17 00:00:00 2001 From: Ilya Sher <34807039+ilyash-b@users.noreply.github.com> Date: Thu, 3 Oct 2024 11:48:49 +0400 Subject: [PATCH 04/12] WIP shell polling --- lib/autoload/globals/IO.ngs | 25 ++++++++++++++++++++++++- lib/shell.ngs | 26 ++++++++++++++++++-------- 2 files changed, 42 insertions(+), 9 deletions(-) diff --git a/lib/autoload/globals/IO.ngs b/lib/autoload/globals/IO.ngs index 219f1537..25a43219 100644 --- a/lib/autoload/globals/IO.ngs +++ b/lib/autoload/globals/IO.ngs @@ -191,8 +191,24 @@ ns { ) } + # Is this the right way to do sync and async? + # TODO: what async should return? global call - F call(jrc:JsonRpcClient, method:Str, params=[], callback:Fun=nop) { + F call(jrc:JsonRpcClient, method:Str, params=[], callback:Fun=null) { + sync = not(callback) + + lock = Lock() + result = null + if sync { + lock.acquire() + callback = F json_rpc_client_sync_callback(data) { + # Runs in the reader thread + debug('client', 'JsonRpcClient callback') + result = data + lock.release() + } + } + assert(jrc.pipeline, 'Before using JsonRpcClient#call, call fire(COR::Pipeline, IO::events::Active()) on the pipeline where JsonRpcClient were pushed') jrc.pipeline.fire(events::Write({ 'call': { @@ -201,6 +217,13 @@ ns { } 'callback': callback })) + + not(sync) returns + + debug('client', 'JsonRpcClient blocked on waiting for result') + lock.acquire().release() + debug('client', 'JsonRpcClient got result') + result } } diff --git a/lib/shell.ngs b/lib/shell.ngs index 50149155..512c0592 100644 --- a/lib/shell.ngs +++ b/lib/shell.ngs @@ -6,6 +6,12 @@ ns { WEB_SERVER_DIR = Dir(".." / "ngs-web-ui").assert() WEB_SERVER_PORT = 52000 + # Temporary name "repr" + F repr(x:NormalTypeInstance, level:Int=0) { + h = x.Hash() + + } + doc Rudimentary client as phase 1. doc Not implemented yet. doc Send a command and poll for results. @@ -28,15 +34,15 @@ ns { client.connect(SOCK_FILE, scd) # TODO: something not callback-based - jrc.call('eval', [cmd], { - log("EVAL RESULT ${A.SafeStr()}") - }) + e = jrc.call('eval', [cmd]) + log("Eval result ${e.SafeStr()}") + while true { log("Polling") - jrc.call('poll', [], { - echo("POLL RESULT ${A.SafeStr()}") - }) - $(log: sleep 5) + p = jrc.call('poll') + log("Poll result") + echo(inspect(p)) + exit breaks } @@ -95,6 +101,10 @@ ns { SHELL_METHODS = ns { F eval(line:Str) { + section "Append line to the Timeline" { + tc = TextualCommandTimelineItem(Time(), line) + tl.push(tc) + } fname = '' section "Shortcuts" { @@ -136,7 +146,7 @@ ns { } F poll() { - log("poll() - work in progress - returning Timeline") + log("poll() - work in progress - returning the whole Timeline") result = ui::Element(tl) result.JsonData()._censor() } From 6cc5a8e11cb46a52070764e41a8b0c590128aeae Mon Sep 17 00:00:00 2001 From: Ilya Sher <34807039+ilyash-b@users.noreply.github.com> Date: Thu, 3 Oct 2024 16:29:17 +0400 Subject: [PATCH 05/12] WIP shell polling --- lib/autoload/globals/ui.ngs | 14 +++++++++----- lib/shell.ngs | 22 ++++++++++++++++------ 2 files changed, 25 insertions(+), 11 deletions(-) diff --git a/lib/autoload/globals/ui.ngs b/lib/autoload/globals/ui.ngs index 8208281d..53808a1b 100644 --- a/lib/autoload/globals/ui.ngs +++ b/lib/autoload/globals/ui.ngs @@ -19,6 +19,7 @@ ns(TL=Timeline, TCTI=TextualCommandTimelineItem) { type Screen(Element) type Timeline(Element) type TimelineItem(Element) + type TextualCommandTimelineItem(TimelineItem) type Object(Element) type Scalar(Element) @@ -87,14 +88,17 @@ ns(TL=Timeline, TCTI=TextualCommandTimelineItem) { F keys_are_strings(h) h.keys().all(Str) F Element(h:AnyOf(Hash, HashLike)) Properties(h.assert(keys_are_strings, "Element(Hash) - keys must be strings").mapv(Element)) - F Element(tcti:TCTI) { - tcti.command.Element() - } + F init(tcti:TextualCommandTimelineItem, id:Str, time:Time, command:Str) init(args()) + + F Element(tcti:TCTI) TextualCommandTimelineItem(tcti.id, tcti.time, tcti.command) - F Element(tl:TL) { - Timeline(tl.items.map(Element)) + F init(tl:Timeline, id:Str, items:Arr) { + tl.id = id + tl.children = items.map(Element) } + F Element(tl:TL) Timeline(tl.id, tl.items) + # TODO: Fix later. It's semantically incorrect to display path as just a string F Element(p:Path) Scalar(p.path) diff --git a/lib/shell.ngs b/lib/shell.ngs index 512c0592..9e77d0d3 100644 --- a/lib/shell.ngs +++ b/lib/shell.ngs @@ -7,13 +7,24 @@ ns { WEB_SERVER_PORT = 52000 # Temporary name "repr" - F repr(x:NormalTypeInstance, level:Int=0) { - h = x.Hash() - + F repr(h:Hash, level:Int=0) collector { + pfx = ' ' * level + h.each(F(k, v) { + if [k, v] =~ ['time', Int] { + v = "${v} (${v.Time().Str()})" + } + if k == 'children' { + v.each_idx_val(F(i, v) { + collect("${pfx} [${i}]") + repr(v, level + 2).each(collect) + }) + return + } + collect("${pfx}${k} = ${v.SafeStr()}") + }) } doc Rudimentary client as phase 1. - doc Not implemented yet. doc Send a command and poll for results. F eval(cmd:Str, exit:Bool=true) { @@ -33,7 +44,6 @@ ns { client = net::UnixStreamClient() client.connect(SOCK_FILE, scd) - # TODO: something not callback-based e = jrc.call('eval', [cmd]) log("Eval result ${e.SafeStr()}") @@ -41,7 +51,7 @@ ns { log("Polling") p = jrc.call('poll') log("Poll result") - echo(inspect(p)) + echo(p.repr().Lines()) exit breaks } From 9799647838e10a10520d91067a3fa305ea14a337 Mon Sep 17 00:00:00 2001 From: Ilya Sher <34807039+ilyash-b@users.noreply.github.com> Date: Sat, 5 Oct 2024 10:15:37 +0400 Subject: [PATCH 06/12] Add experimental SeqId --- lib/autoload/globals/SeqId.ngs | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 lib/autoload/globals/SeqId.ngs diff --git a/lib/autoload/globals/SeqId.ngs b/lib/autoload/globals/SeqId.ngs new file mode 100644 index 00000000..d6da0f40 --- /dev/null +++ b/lib/autoload/globals/SeqId.ngs @@ -0,0 +1,15 @@ +ns { + # TODO: more fine grained locking or lock-less mechanism + l = Lock() + ids = {} + + global SeqId + doc %STATUS - Experimental + F SeqId(s:Str) { + i = l.acquire({ + ids.dflt(s, 0) + ids[s] += 1 + }) + "${s}${i}" + } +} \ No newline at end of file From 77d0aec586513c4cc844ecd1a3c421a243d6f026 Mon Sep 17 00:00:00 2001 From: Ilya Sher <34807039+ilyash-b@users.noreply.github.com> Date: Sat, 5 Oct 2024 10:27:12 +0400 Subject: [PATCH 07/12] WIP shell polling --- lib/autoload/globals/Timeline.ngs | 34 ++++++++++++++++++++++++++++++- lib/autoload/globals/net.ngs | 2 +- lib/autoload/globals/ui.ngs | 20 +++++++++++++++++- lib/shell.ngs | 33 +++++++++++++++++------------- ngsfile | 4 ++-- 5 files changed, 74 insertions(+), 19 deletions(-) diff --git a/lib/autoload/globals/Timeline.ngs b/lib/autoload/globals/Timeline.ngs index a830a20f..9e22e5b4 100644 --- a/lib/autoload/globals/Timeline.ngs +++ b/lib/autoload/globals/Timeline.ngs @@ -10,7 +10,9 @@ ns { doc time - Time type TimelineItem - F init(ti:TimelineItem, time:Time) { + F init(ti:TimelineItem) throw InvalidArgument("TimelineItem() was called without arguments") + + F init(ti:TimelineItem, time:Time=null) { ti.id = "ti-${`line: uuidgen`}" # temporary ti.time = time } @@ -18,6 +20,36 @@ ns { F Time(ti:TimelineItem) ti.time } + section "GroupTimelineItem" { + + global GroupTimelineItem + type GroupTimelineItem(TimelineItem) + + F init(ti:GroupTimelineItem) { + super(ti) + ti.items = [] + } + + F push(gti:GroupTimelineItem, ti:TimelineItem) gti::{ + if not(gti.items) { + gti.time = ti.time + } + gti.items.push(ti) + } + } + + section "ResultTimelineItem" { + + global ResultTimelineItem + type ResultTimelineItem(TimelineItem) + + F init(ti:ResultTimelineItem, time:Time, result) { + super(ti, time) + ti.result = result + } + + } + section "TextualCommandTimelineItem" { global TextualCommandTimelineItem diff --git a/lib/autoload/globals/net.ngs b/lib/autoload/globals/net.ngs index 6e74e75e..2fb1be77 100644 --- a/lib/autoload/globals/net.ngs +++ b/lib/autoload/globals/net.ngs @@ -155,7 +155,7 @@ ns { debug('server', 'ThreadedServerDelegate on_connect()') # TODO: join the thread / use Executor - Thread("connection-${c.id}", { + Thread("ThreadedServerDelegate-Connection-${c.id}", { try { debug('server', 'ThreadedServerDelegate on_listen() - new thread') c.pipeline.fire(IO::events::Active()) diff --git a/lib/autoload/globals/ui.ngs b/lib/autoload/globals/ui.ngs index 53808a1b..9ef7ddfe 100644 --- a/lib/autoload/globals/ui.ngs +++ b/lib/autoload/globals/ui.ngs @@ -1,4 +1,4 @@ -ns(TL=Timeline, TCTI=TextualCommandTimelineItem) { +ns(TL=Timeline, TCTI=TextualCommandTimelineItem, GTI=GroupTimelineItem, RTI=ResultTimelineItem) { # WIP global init, JsonData @@ -19,7 +19,9 @@ ns(TL=Timeline, TCTI=TextualCommandTimelineItem) { type Screen(Element) type Timeline(Element) type TimelineItem(Element) + type GroupTimelineItem(TimelineItem) type TextualCommandTimelineItem(TimelineItem) + type ResultTimelineItem(TimelineItem) type Object(Element) type Scalar(Element) @@ -88,6 +90,22 @@ ns(TL=Timeline, TCTI=TextualCommandTimelineItem) { F keys_are_strings(h) h.keys().all(Str) F Element(h:AnyOf(Hash, HashLike)) Properties(h.assert(keys_are_strings, "Element(Hash) - keys must be strings").mapv(Element)) + F init(gti:GroupTimelineItem, id:Str, time:Time, items:Arr) { + gti.id = id + gti.time = time + super(gti, items) + } + + F Element(gti:GTI) GroupTimelineItem(gti.id, gti.time, gti.items.map(Element)) + + F init(rti:ResultTimelineItem, id:Str, time:Time, result) { + rti.id = id + rti.time = time + super(rti, [Element(result)]) + } + + F Element(rti:RTI) ResultTimelineItem(rti.id, rti.time, rti.result) + F init(tcti:TextualCommandTimelineItem, id:Str, time:Time, command:Str) init(args()) F Element(tcti:TCTI) TextualCommandTimelineItem(tcti.id, tcti.time, tcti.command) diff --git a/lib/shell.ngs b/lib/shell.ngs index 9e77d0d3..cf94235e 100644 --- a/lib/shell.ngs +++ b/lib/shell.ngs @@ -113,7 +113,8 @@ ns { section "Append line to the Timeline" { tc = TextualCommandTimelineItem(Time(), line) - tl.push(tc) + g = GroupTimelineItem().push(tc) + tl.push(g) } fname = '' @@ -128,19 +129,22 @@ ns { } line = shortcuts.get(line, line) } - bytecode = compile(line, fname) - # TODO: pass warnings - #bytecode.meta().warnings.each(F(w) { - # wl = w.location - # warn("${fname}:${wl.first_line}:${wl.first_column} warning: ${w.message}") - #}) - - func = load(bytecode, "") - result = func() - debug('server', "Result type ${result.Type().name}") - result = ui::Element(result) - debug('server', "Result type after transform() ${result.Type().name}") - result.JsonData()._censor() + + # TODO: join these sometimes + Thread(SeqId("shell-eval-"), { + debug('server', "Eval start: ${line}") + bytecode = compile(line, fname) + # TODO: pass warnings + #bytecode.meta().warnings.each(F(w) { + # wl = w.location + # warn("${fname}:${wl.first_line}:${wl.first_column} warning: ${w.message}") + #}) + func = load(bytecode, "") + result = func() + debug('server', "Eval end: ${line}") + g.push(ResultTimelineItem(Time(), result)) + }) + {'ti': tc.id} # Timeline Item ID } # event example: {cur={type=AWS::CodePipeline::Pipeline, id=XXXX}, ref={type=AWS::CodePipeline::Execution, id=XXXX}} @@ -158,6 +162,7 @@ ns { F poll() { log("poll() - work in progress - returning the whole Timeline") result = ui::Element(tl) + # 'token': if tl.items then tl.items[-1].id result.JsonData()._censor() } diff --git a/ngsfile b/ngsfile index 48017a45..cc2edb35 100755 --- a/ngsfile +++ b/ngsfile @@ -8,7 +8,7 @@ ns { require("./lib/shell.ngs")::server(true) } - F eval(cmd:Str) { - require("./lib/shell.ngs")::eval(cmd) + F eval(cmd:Str, exit:Bool=true) { + require("./lib/shell.ngs")::eval(cmd, exit) } } \ No newline at end of file From 9a8799c46490bcff11aef63e9433621a7fc5dcbe Mon Sep 17 00:00:00 2001 From: Ilya Sher <34807039+ilyash-b@users.noreply.github.com> Date: Sat, 5 Oct 2024 11:05:58 +0400 Subject: [PATCH 08/12] WIP shell polling Use Timeline namespace --- lib/autoload/globals/Timeline.ngs | 41 ++++++++++++++----------------- lib/autoload/globals/ui.ngs | 10 ++++---- lib/shell.ngs | 8 +++--- 3 files changed, 27 insertions(+), 32 deletions(-) diff --git a/lib/autoload/globals/Timeline.ngs b/lib/autoload/globals/Timeline.ngs index 9e22e5b4..fbf30880 100644 --- a/lib/autoload/globals/Timeline.ngs +++ b/lib/autoload/globals/Timeline.ngs @@ -2,35 +2,33 @@ ns { # WIP - timeline, mainly for UI - global Timeline, Time, Lines, init, each, map, sort, push, echo_cli + global Time, Lines, init, each, map, sort, push, echo_cli - section "TimelineItem" { + section "Item" { - global TimelineItem doc time - Time - type TimelineItem + type Item - F init(ti:TimelineItem) throw InvalidArgument("TimelineItem() was called without arguments") + F init(ti:Item) throw InvalidArgument("Item() was called without arguments") - F init(ti:TimelineItem, time:Time=null) { + F init(ti:Item, time:Time=null) { ti.id = "ti-${`line: uuidgen`}" # temporary ti.time = time } - F Time(ti:TimelineItem) ti.time + F Time(ti:Item) ti.time } - section "GroupTimelineItem" { + section "GroupItem" { - global GroupTimelineItem - type GroupTimelineItem(TimelineItem) + type GroupItem(Item) - F init(ti:GroupTimelineItem) { + F init(ti:GroupItem) { super(ti) ti.items = [] } - F push(gti:GroupTimelineItem, ti:TimelineItem) gti::{ + F push(gti:GroupItem, ti:Item) gti::{ if not(gti.items) { gti.time = ti.time } @@ -38,26 +36,23 @@ ns { } } - section "ResultTimelineItem" { + section "ResultItem" { - global ResultTimelineItem - type ResultTimelineItem(TimelineItem) + type ResultItem(Item) - F init(ti:ResultTimelineItem, time:Time, result) { + F init(ti:ResultItem, time:Time, result) { super(ti, time) ti.result = result } - } - section "TextualCommandTimelineItem" { + section "TextualCommandItem" { - global TextualCommandTimelineItem - type TextualCommandTimelineItem(TimelineItem) + type TextualCommandItem(Item) - F init(ti:TextualCommandTimelineItem) throw InvalidArgument("TextualCommandTimelineItem() was called without arguments") + F init(ti:TextualCommandItem) throw InvalidArgument("TextualCommandItem() was called without arguments") - F init(ti:TextualCommandTimelineItem, time:Time, command:Str) { + F init(ti:TextualCommandItem, time:Time, command:Str) { super(ti, time) ti.command = command } @@ -91,7 +86,7 @@ ns { t.sort() } - F push(t:Timeline, ti:TimelineItem) t::{ + F push(t:Timeline, ti:Item) t::{ A.items.push(ti) A.sort() # TODO: insert in the correct place, keeping .items sorted by time # TODO: maximize time_last_update and time_end into t diff --git a/lib/autoload/globals/ui.ngs b/lib/autoload/globals/ui.ngs index 9ef7ddfe..56b02ed9 100644 --- a/lib/autoload/globals/ui.ngs +++ b/lib/autoload/globals/ui.ngs @@ -1,4 +1,4 @@ -ns(TL=Timeline, TCTI=TextualCommandTimelineItem, GTI=GroupTimelineItem, RTI=ResultTimelineItem) { +ns(TL=Timeline) { # WIP global init, JsonData @@ -96,7 +96,7 @@ ns(TL=Timeline, TCTI=TextualCommandTimelineItem, GTI=GroupTimelineItem, RTI=Resu super(gti, items) } - F Element(gti:GTI) GroupTimelineItem(gti.id, gti.time, gti.items.map(Element)) + F Element(gti:TL::GroupItem) GroupTimelineItem(gti.id, gti.time, gti.items.map(Element)) F init(rti:ResultTimelineItem, id:Str, time:Time, result) { rti.id = id @@ -104,18 +104,18 @@ ns(TL=Timeline, TCTI=TextualCommandTimelineItem, GTI=GroupTimelineItem, RTI=Resu super(rti, [Element(result)]) } - F Element(rti:RTI) ResultTimelineItem(rti.id, rti.time, rti.result) + F Element(rti:TL::ResultItem) ResultTimelineItem(rti.id, rti.time, rti.result) F init(tcti:TextualCommandTimelineItem, id:Str, time:Time, command:Str) init(args()) - F Element(tcti:TCTI) TextualCommandTimelineItem(tcti.id, tcti.time, tcti.command) + F Element(tcti:TL::TextualCommandItem) TextualCommandTimelineItem(tcti.id, tcti.time, tcti.command) F init(tl:Timeline, id:Str, items:Arr) { tl.id = id tl.children = items.map(Element) } - F Element(tl:TL) Timeline(tl.id, tl.items) + F Element(tl:TL::Timeline) Timeline(tl.id, tl.items) # TODO: Fix later. It's semantically incorrect to display path as just a string F Element(p:Path) Scalar(p.path) diff --git a/lib/shell.ngs b/lib/shell.ngs index cf94235e..5d464b6b 100644 --- a/lib/shell.ngs +++ b/lib/shell.ngs @@ -107,13 +107,13 @@ ns { } } - tl = Timeline() + tl = Timeline::Timeline() SHELL_METHODS = ns { F eval(line:Str) { section "Append line to the Timeline" { - tc = TextualCommandTimelineItem(Time(), line) - g = GroupTimelineItem().push(tc) + tc = Timeline::TextualCommandItem(Time(), line) + g = Timeline::GroupItem().push(tc) tl.push(g) } fname = '' @@ -142,7 +142,7 @@ ns { func = load(bytecode, "") result = func() debug('server', "Eval end: ${line}") - g.push(ResultTimelineItem(Time(), result)) + g.push(Timeline::ResultItem(Time(), result)) }) {'ti': tc.id} # Timeline Item ID } From ba479afc610eac53726b8cadb1501478d0b067d3 Mon Sep 17 00:00:00 2001 From: Ilya Sher <34807039+ilyash-b@users.noreply.github.com> Date: Sat, 5 Oct 2024 12:15:39 +0400 Subject: [PATCH 09/12] WIP shell polling --- lib/autoload/globals/Timeline.ngs | 2 ++ lib/autoload/globals/ui.ngs | 5 +++-- lib/shell.ngs | 35 ++++++++++++++++++++++--------- 3 files changed, 30 insertions(+), 12 deletions(-) diff --git a/lib/autoload/globals/Timeline.ngs b/lib/autoload/globals/Timeline.ngs index fbf30880..6ea861df 100644 --- a/lib/autoload/globals/Timeline.ngs +++ b/lib/autoload/globals/Timeline.ngs @@ -70,6 +70,7 @@ ns { F init(t:Timeline) { t.id = "tl-${`line: uuidgen`}" # temporary + t.version = 1 # TODO: make sure it's thread safe t.name = '(unnamed)' t.status = 'TODO' t.error = null @@ -91,6 +92,7 @@ ns { A.sort() # TODO: insert in the correct place, keeping .items sorted by time # TODO: maximize time_last_update and time_end into t # TODO: minimize time_start into t + t.version += 1 } F each(t:Timeline, cb:Fun) t::{A.items.each(cb)} diff --git a/lib/autoload/globals/ui.ngs b/lib/autoload/globals/ui.ngs index 56b02ed9..e9e64c84 100644 --- a/lib/autoload/globals/ui.ngs +++ b/lib/autoload/globals/ui.ngs @@ -110,12 +110,13 @@ ns(TL=Timeline) { F Element(tcti:TL::TextualCommandItem) TextualCommandTimelineItem(tcti.id, tcti.time, tcti.command) - F init(tl:Timeline, id:Str, items:Arr) { + F init(tl:Timeline, id:Str, version, items:Arr) { tl.id = id + tl.version = version tl.children = items.map(Element) } - F Element(tl:TL::Timeline) Timeline(tl.id, tl.items) + F Element(tl:TL::Timeline) Timeline(tl.id, tl.version, tl.items) # TODO: Fix later. It's semantically incorrect to display path as just a string F Element(p:Path) Scalar(p.path) diff --git a/lib/shell.ngs b/lib/shell.ngs index 5d464b6b..3a516f5d 100644 --- a/lib/shell.ngs +++ b/lib/shell.ngs @@ -47,15 +47,17 @@ ns { e = jrc.call('eval', [cmd]) log("Eval result ${e.SafeStr()}") + last_seen_version = null while true { - log("Polling") - p = jrc.call('poll') - log("Poll result") - echo(p.repr().Lines()) + log("Polling last_seen_version=${last_seen_version}") + p = jrc.call('poll', { + 'last_seen_version': last_seen_version + }) + log("Poll result - timeline (ver. ${p.version})") + echo(p.timeline.repr().Lines()) + last_seen_version = p.version exit breaks } - - } doc Starts shell server, JSON RPC proxy, and a browser client @@ -143,6 +145,7 @@ ns { result = func() debug('server', "Eval end: ${line}") g.push(Timeline::ResultItem(Time(), result)) + tl.version += 1 # FIXME: Flimsy }) {'ti': tc.id} # Timeline Item ID } @@ -159,10 +162,22 @@ ns { } } - F poll() { - log("poll() - work in progress - returning the whole Timeline") - result = ui::Element(tl) - # 'token': if tl.items then tl.items[-1].id + # TODO: handle broken pipe + F poll(last_seen_version) { + log("poll(last_seen_version=${last_seen_version}) - work in progress - returning the whole Timeline") + section "Wait for later version before retuning the result" block b { + not(last_seen_version) returns + while true { + if tl.version > last_seen_version { + b.return() + } + $(sleep 0.1) + } + } + result = { + 'timeline': ui::Element(tl) + 'version': tl.version + } result.JsonData()._censor() } From 4b97566a8deba3a490e157a6a5235325398c56dc Mon Sep 17 00:00:00 2001 From: Ilya Sher <34807039+ilyash-b@users.noreply.github.com> Date: Mon, 28 Oct 2024 11:14:05 +0400 Subject: [PATCH 10/12] WIP shell polling --- lib/autoload/globals/net.ngs | 2 +- lib/shell.ngs | 50 ++++++++++++++++++- ngsfile | 97 ++++++++++++++++++++++++++++++++---- 3 files changed, 137 insertions(+), 12 deletions(-) diff --git a/lib/autoload/globals/net.ngs b/lib/autoload/globals/net.ngs index 2fb1be77..a2677e1c 100644 --- a/lib/autoload/globals/net.ngs +++ b/lib/autoload/globals/net.ngs @@ -173,7 +173,7 @@ ns { } } catch(e) { debug('server', "ThreadedServerDelegate before logging") - log("ThreadedServerDelegate - exception in thread: ${e}") + log("ThreadedServerDelegate - exception in thread: ${Str(e) tor ''}") debug('server', "ThreadedServerDelegate after logging") guard false } diff --git a/lib/shell.ngs b/lib/shell.ngs index 3a516f5d..5a56b96b 100644 --- a/lib/shell.ngs +++ b/lib/shell.ngs @@ -24,6 +24,52 @@ ns { }) } + section "Shell Client" { + type Client() + + + type ShellClientDelegate(net::ClientDelegate) + + global on_connect + F on_connect(scd:ShellClientDelegate, c:net::Connection) { + debug('client', 'ShellClientDelegate#on_connect()') + scd.jrc = IO::handlers::JsonRpcClient() + c.pipeline.push(IO::handlers::Splitter("\n")) + c.pipeline.push(scd.jrc) + c.pipeline.fire(IO::events::Active()) + } + + global init + F init(c:Client) { + c.last_seen_version = null + c.last_eval_result = null + c.scd = ShellClientDelegate() + c.client = net::UnixStreamClient() + c.client.connect(SOCK_FILE, c.scd) + } + + global eval + F eval(c:Client, cmd:Str) { + c.last_eval_result = c.scd.jrc.call('eval', [cmd])::{ + debug('client', {"Eval result ${A.SafeStr()}"}) + } + } + + global poll + F poll(c:Client, cb:Fun) { + while true { + debug('client', "Polling last_seen_version=${c.last_seen_version}") + p = c.scd.jrc.call('poll', { + 'last_seen_version': c.last_seen_version + }) + debug('client', "Poll result - timeline (ver. ${p.version})") + # echo(p.timeline.repr().Lines()) + c.last_seen_version = p.version + not(cb(p)) returns p + } + } + } + doc Rudimentary client as phase 1. doc Send a command and poll for results. F eval(cmd:Str, exit:Bool=true) { @@ -56,7 +102,7 @@ ns { log("Poll result - timeline (ver. ${p.version})") echo(p.timeline.repr().Lines()) last_seen_version = p.version - exit breaks + exit returns p } } @@ -164,7 +210,7 @@ ns { # TODO: handle broken pipe F poll(last_seen_version) { - log("poll(last_seen_version=${last_seen_version}) - work in progress - returning the whole Timeline") + debug('client', "poll(last_seen_version=${last_seen_version}) - work in progress - returning the whole Timeline") section "Wait for later version before retuning the result" block b { not(last_seen_version) returns while true { diff --git a/ngsfile b/ngsfile index cc2edb35..3b7a460d 100755 --- a/ngsfile +++ b/ngsfile @@ -1,14 +1,93 @@ #!/usr/bin/env ngs -ns { - F server() { - require("./lib/shell.ngs")::server(false) - } +ns(t=test) { - F ui() { - require("./lib/shell.ngs")::server(true) - } + sh = require("./lib/shell.ngs") + + F server() sh::server(false) + F ui() sh::server(true) + + # "eval" would conflict with the global eval(c:Client, ...) + # which is called from tests below. + F _eval(cmd:Str, exit:Bool=true) sh::eval(cmd, exit) + _exports.eval = _eval + + F test() { + # Tests where the server and the client run in the same process + # fail sporadically. Therefore, running the server in external process. + # srv = Thread("test-server", { + # log("Starting server") + # server() + # }) + srv = $(ngs . server &) + $(sleep 1) + c = sh::Client() + + F make_textual_command_timeline_result_pattern(ti:Str, cmd:Str, pat) { + { + 'timeline': { + 'children': Present({ + '$type': 'GroupTimelineItem' + 'children': [{ + '$type': 'TextualCommandTimelineItem' + 'id': ti + 'command': cmd + }, { + '$type': 'ResultTimelineItem' + 'children': [ + pat + ] + }] + }) + } + } + } + + test_ti = null + t("basic eval", { + test_ti = c.eval("{1+1}").assert({ + 'ti': Pfx('ti-') + }).ti + }) + + t("poll after basic eval", { + c.poll(F(response) { + # The response was observed to be already there. I don't think it's guaranteed. May need refactoring. + assert(response, make_textual_command_timeline_result_pattern(test_ti, '{1+1}', { '$type': 'Scalar', 'value': 2 })) + false + }) + }) + + t("eval with sleep", { + # Valid JSON for decode_json() + test_ti = c.eval("sleep 2 | echo 3").assert({ + 'ti': Pfx('ti-') + }).ti + }) + + t("poll immediately after eval with sleep", { + c.poll(F(response) { + assert(response, { + 'timeline': { + 'children': Present({ + '$type': 'GroupTimelineItem' + 'children': [{ + '$type': 'TextualCommandTimelineItem' + 'id': test_ti + 'command': 'sleep 2 | echo 3' + }] + }) + } + }) + false + }) + }) - F eval(cmd:Str, exit:Bool=true) { - require("./lib/shell.ngs")::eval(cmd, exit) + t("poll later after eval with sleep", { + $(sleep 2) + c.poll(F(response) { + assert(response, make_textual_command_timeline_result_pattern(test_ti, 'sleep 2 | echo 3', { '$type': 'Scalar', 'value': 3 })) + false + }) + }) } } \ No newline at end of file From 86939f43d2a59559bed20db864c671b2faaf3a1e Mon Sep 17 00:00:00 2001 From: Ilya Sher <34807039+ilyash-b@users.noreply.github.com> Date: Fri, 1 Nov 2024 13:40:35 +0400 Subject: [PATCH 11/12] [IO.ngs] Handle JSON RPC requests in threads Needed for polling --- lib/autoload/globals/IO.ngs | 42 ++++++++++++++++++++----------------- 1 file changed, 23 insertions(+), 19 deletions(-) diff --git a/lib/autoload/globals/IO.ngs b/lib/autoload/globals/IO.ngs index 25a43219..3979538f 100644 --- a/lib/autoload/globals/IO.ngs +++ b/lib/autoload/globals/IO.ngs @@ -106,26 +106,30 @@ ns { } } - debug('server', "JsonRpcServer on_data -- invoking ${req.method}") - result = try { - jrs.methods[req.method](*args, **kwargs) - } catch(mnf:MethodNotFound) { - guard mnf.callable === jrs.methods[req.method] - send_error(-32602, 'Invalid params', "Correct parameters can be seen in methods' descriptions: ${jrs.methods[req.method].Arr()}") - } catch(e:Error) { - send_error(-32603, 'Internal error', e.Str()) - } + debug('server', "JsonRpcServer on_data -- invoking ${req.method} in a thread") + + # TODO: join this thread + Thread(SeqId("JsonRpcServer-Request"), { + result = try { + jrs.methods[req.method](*args, **kwargs) + } catch(mnf:MethodNotFound) { + guard mnf.callable === jrs.methods[req.method] + send_error(-32602, 'Invalid params', "Correct parameters can be seen in methods' descriptions: ${jrs.methods[req.method].Arr()}") + } catch(e:Error) { + send_error(-32603, 'Internal error', e.Str()) + } - if 'id' in req { - # response expected - ret = { - 'jsonrpc': '2.0' - 'id': req.id - 'result': result - }.filterv() # Why filterv()? - debug('server', 'JsonRpcServer on_data -- sending reply') - hc.fire(events::Write(ret.encode_json() + "\n")) - } + if 'id' in req { + # response expected + ret = { + 'jsonrpc': '2.0' + 'id': req.id + 'result': result + }.filterv() # Why filterv()? + debug('server', 'JsonRpcServer on_data -- sending reply') + hc.fire(events::Write(ret.encode_json() + "\n")) + } + }) } # handle Read From b20aa3c1878b21d5e60bde6a75c3db5bee84f2f0 Mon Sep 17 00:00:00 2001 From: Ilya Sher <34807039+ilyash-b@users.noreply.github.com> Date: Fri, 1 Nov 2024 14:13:15 +0400 Subject: [PATCH 12/12] [IO.ngs] Handle JSON RPC requests in threads Fix send_error() --- lib/autoload/globals/IO.ngs | 56 ++++++++++++++++++++++--------------- 1 file changed, 33 insertions(+), 23 deletions(-) diff --git a/lib/autoload/globals/IO.ngs b/lib/autoload/globals/IO.ngs index 3979538f..38fdba7e 100644 --- a/lib/autoload/globals/IO.ngs +++ b/lib/autoload/globals/IO.ngs @@ -72,7 +72,7 @@ ns { debug('server', "JsonRpcServer Read") F send_error(code, message, data=null) { - error("JsonRpcServer on_data send_error -- ${code} ${message} ${data}") + error("JsonRpcServer on_data (or spawned thread) send_error -- ${code} ${message} ${data}") ret = { 'jsonrpc': '2.0' 'id': req.get('id') @@ -83,7 +83,6 @@ ns { }.filterv() } hc.fire(events::Write(ret.encode_json() + "\n")) - b.return() } req = {} # allow send_error() to do req.get('id') if we crash in try @@ -91,11 +90,18 @@ ns { data.decode_json() } catch(e:JsonDecodeFail) { send_error(-32700, 'Parse error', e.Hash().filterk(AnyOf('error', 'value', 'position'))) + b.return() } - if (req !~ _VALID_JSON_RPC_REQUEST) send_error(-32600, 'Invalid Request', 'See https://www.jsonrpc.org/specification') + if (req !~ _VALID_JSON_RPC_REQUEST) { + send_error(-32600, 'Invalid Request', 'See https://www.jsonrpc.org/specification') + b.return() + } - if (req.method not in jrs.methods) send_error(-32601, 'Method not found', "Method ${req.method} not found. Available methods: ${jrs.methods.keys()}") + if (req.method not in jrs.methods) { + send_error(-32601, 'Method not found', "Method ${req.method} not found. Available methods: ${jrs.methods.keys()}") + b.return() + } args = [] kwargs = {} @@ -109,25 +115,29 @@ ns { debug('server', "JsonRpcServer on_data -- invoking ${req.method} in a thread") # TODO: join this thread - Thread(SeqId("JsonRpcServer-Request"), { - result = try { - jrs.methods[req.method](*args, **kwargs) - } catch(mnf:MethodNotFound) { - guard mnf.callable === jrs.methods[req.method] - send_error(-32602, 'Invalid params', "Correct parameters can be seen in methods' descriptions: ${jrs.methods[req.method].Arr()}") - } catch(e:Error) { - send_error(-32603, 'Internal error', e.Str()) - } - - if 'id' in req { - # response expected - ret = { - 'jsonrpc': '2.0' - 'id': req.id - 'result': result - }.filterv() # Why filterv()? - debug('server', 'JsonRpcServer on_data -- sending reply') - hc.fire(events::Write(ret.encode_json() + "\n")) + Thread(SeqId("JsonRpcServer-request-"), { + block b { + result = try { + jrs.methods[req.method](*args, **kwargs) + } catch(mnf:MethodNotFound) { + guard mnf.callable === jrs.methods[req.method] + send_error(-32602, 'Invalid params', "Correct parameters can be seen in methods' descriptions: ${jrs.methods[req.method].Arr()}") + b.return() + } catch(e:Error) { + send_error(-32603, 'Internal error', e.Str()) + b.return() + } + + if 'id' in req { + # response expected + ret = { + 'jsonrpc': '2.0' + 'id': req.id + 'result': result + }.filterv() # Why filterv()? + debug('server', 'JsonRpcServer on_data -- sending reply') + hc.fire(events::Write(ret.encode_json() + "\n")) + } } })