@naanlang/naan 1.7.0 → 1.7.1

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.
@@ -208,6 +208,7 @@ closure APIClient(imp, options, local client, url, guid) {
208
208
  closure WebSocketClient(options, imp, local wesc, ws, pending) {
209
209
  global(js)
210
210
  wesc = new(object, this)
211
+
211
212
 
212
213
  // reconnect
213
214
  //
@@ -226,40 +227,29 @@ closure WebSocketClient(options, imp, local wesc, ws, pending) {
226
227
  //
227
228
  // Establish a websocket connection with the host.
228
229
  //
229
- wesc.connect = closure connect(local url, result) {
230
+ wesc.connect = closure connect(local url, result, error) {
230
231
  if ws {
231
232
  if pending
232
233
  return (pending.wait()) // connect in process
233
234
  if !wesc.fatal
234
235
  return(list(false, { connected: true })) // already open
235
236
  }
236
- try {
237
- url = xnew(js.w.URL, options.url)
238
- } catch {
239
- return(list(Error("invalid URL ${options.url}", exception)))
240
- }
241
- if url.protocol == "https:"
242
- url.protocol = "wss:"
243
- else
244
- url.protocol = "ws:"
245
- ws = xnew(js.w.WebSocket, url.href)
246
- if wesc.fatal
247
- debuglog("WebSocketClient reopen") // ### not pretty to see this
248
- wesc.fatal = false
249
- pending = new(nonce)
250
237
 
251
238
  // onopen
252
239
  // The WebSocket connection is now open.
253
- ws.onopen = function onopen(e) {
240
+ function onopen(e) {
254
241
  if wesc.impconn {
255
242
  result = wesc.impconn.reopen()
256
243
  pending.signal(result)
257
244
  return
258
245
  }
259
- wesc.impconn = imp.requester(function(data) {
246
+ wesc.impconn = imp.requester(function(data, local error, text) {
260
247
  if !pending && reconnect() // !pending means not opening/reopening
261
248
  return // failed to reconnect
262
- ws.send(js.w.JSON.stringify(data))
249
+ `(error, text) = JsonStringify(data)
250
+ if error
251
+ return (list(error))
252
+ ws.send(text)
263
253
  list(false, { sent: true })
264
254
  }, {
265
255
  secret: options.guid
@@ -274,7 +264,7 @@ closure WebSocketClient(options, imp, local wesc, ws, pending) {
274
264
 
275
265
  // onmessage
276
266
  // A message was received via WebSockets.
277
- ws.onmessage = function onmessage(e, local error, message) {
267
+ function onmessage(e, local error, message) {
278
268
  `(error, message) = JsonParse(e.data)
279
269
  if !error {
280
270
  message = new(message)
@@ -289,7 +279,7 @@ closure WebSocketClient(options, imp, local wesc, ws, pending) {
289
279
  // onerror
290
280
  // An error occurred on the WebSocket connection.
291
281
  // This can happen in normal operation when the connection times out.
292
- ws.onerror = function onerror(e) {
282
+ function onerror(e) {
293
283
  if !wesc.fatal
294
284
  wesc.fatal = Error("websocket failed", e)
295
285
  pending.signal(list(wesc.fatal))
@@ -298,7 +288,7 @@ closure WebSocketClient(options, imp, local wesc, ws, pending) {
298
288
 
299
289
  // onclose
300
290
  // The WebSocket connection was closed.
301
- ws.onclose = function onclose(e) {
291
+ function onclose(e) {
302
292
  // if e.code != 1000 && e.code != 1001 && e.code != 1005 && e.code != 1006
303
293
  debuglog("WebSocketClient: closed:", e.code, e.reason) // ### not pretty to see this
304
294
  ws = false
@@ -308,6 +298,19 @@ closure WebSocketClient(options, imp, local wesc, ws, pending) {
308
298
  pending = false
309
299
  }
310
300
 
301
+ `(error, ws) = call(clirWebsocket(), options.url, {
302
+ onopen: onopen.proc
303
+ onmessage: onmessage.proc
304
+ onerror: onerror.proc
305
+ onclose: onclose.proc
306
+ })
307
+ if error
308
+ return (list(wesc.fatal = error)) // failed to even get started
309
+
310
+ if wesc.fatal
311
+ debuglog("WebSocketClient reopen") // ### not pretty to see this
312
+ wesc.fatal = false
313
+ pending = new(nonce)
311
314
  result = pending.wait(10000, list(Error("IMP connection timeout")))
312
315
  pending = false
313
316
  result
@@ -6,7 +6,7 @@
6
6
  *
7
7
  * column positioning: // // !
8
8
  *
9
- * Copyright (c) 2020-2025 by Richard C. Zulch
9
+ * Copyright (c) 2020-2026 by Richard C. Zulch
10
10
  *
11
11
  */
12
12
 
@@ -31,6 +31,26 @@ function clirRequester() {
31
31
  };
32
32
 
33
33
 
34
+ /*
35
+ * clirWebsocket
36
+ *
37
+ * Return an appropriate Websocket client based on our environment.
38
+ *
39
+ */
40
+
41
+ clirWebsocketApi = false;
42
+
43
+ function clirWebsocket() {
44
+ if !clirWebsocketApi {
45
+ if js.w
46
+ clirWebsocketApi = require("naanlib:frameworks/browser/ws_client.nlg").WebSocket
47
+ else
48
+ clirWebsocketApi = require("naanlib:frameworks/node/ws_client.nlg").WebSocket
49
+ }
50
+ clirWebsocketApi
51
+ };
52
+
53
+
34
54
  /*
35
55
  * clirInit
36
56
  *
@@ -40,7 +60,7 @@ function clirRequester() {
40
60
  */
41
61
 
42
62
  function clirInit(local manifest) {
43
- manifest = `(clirRequester, clirInit)
63
+ manifest = `(clirRequester, clirWebsocket, clirInit)
44
64
 
45
65
  Naan.module.build(module.id, "client", function(modobj, compobj) {
46
66
  require("../common/").LiveImport()
@@ -52,7 +52,7 @@ closure RelayCon(imp, options, local relay, nlregex)
52
52
  serverID: "Workers"
53
53
  workerID: "NideServer"
54
54
  }, options.config)
55
- relay.instanceID = UUID()
55
+ relay.instanceID = imp.getUUID("RelayCon(${options.name}) instanceID")
56
56
  relay.exec = execsMod.ExecutorBase(false, "Host", options.name)
57
57
  nlregex = RegExp("\\n", "g")
58
58
 
@@ -252,14 +252,16 @@ macro LiveExport() {
252
252
  importList = []
253
253
  //
254
254
  // LiveImport
255
- // Return a new LiveImport function to be exported by the module who called MakeLiveImport.
255
+ // Return a new LiveImport function to be exported by the module who called LiveExport.
256
256
  //
257
- macro LiveImport(fixproc, local importingMod, mdex, updater) {
257
+ macro LiveImport(fixproc, local importingMod, item, updater) {
258
258
  importingMod = car(nsrights().lookup("module"))
259
- mdex = importList.indexOf(importingMod)
260
- if mdex >= 0
261
- updater = importList[mdex] // already registered
262
- else {
259
+ for item in importList
260
+ if item.proc.1.namespace === importingMod.namespace {
261
+ updater = item
262
+ break
263
+ }
264
+ if !updater {
263
265
  updater = closure(importingMod, fixproc) { // create new updater
264
266
  putproc(compress(updateImports), function updateImports(local goback) {
265
267
  if importingMod !== exporterMod {
@@ -269,10 +271,10 @@ macro LiveExport() {
269
271
  try {
270
272
  call(fixproc, exporterMod) // let caller do any fixups
271
273
  } catch {
272
- if (true)
273
- debuglog("LiveImport fixup failed in", nsrights(), "exception:", exception)
274
+ debuglog("LiveImport fixup failed in", nsrights(), "exception:", exception)
275
+ } finally {
276
+ goback()
274
277
  }
275
- goback()
276
278
  })
277
279
  updateImports
278
280
  } (importingMod, eval(fixproc))
@@ -44,7 +44,7 @@ closure APIServer(options, local server, chroot) {
44
44
  server.guid = options.guid
45
45
  server.notifyq = []
46
46
  server.responseq = []
47
- server.instanceID = UUID()
47
+ server.instanceID = App.imp.getUUID("APIServer instanceID")
48
48
  options = merge({
49
49
  relpath: ""
50
50
  name: "NaanServer"
@@ -44,7 +44,7 @@ closure CurBot(options, local curbot) {
44
44
  curbot.guid = 1
45
45
  curbot.host = js.t // our magic Naan controller reference
46
46
 
47
- curbot.instanceID = UUID()
47
+ curbot.instanceID = App.imp.getUUID("CurBot(${options.name}) instanceID")
48
48
  curbot.sender = App.imp.register(curbot, {
49
49
  name: curbot.name
50
50
  type: curbot.type
@@ -266,7 +266,7 @@ closure NowBot(options, local nowbot, nodeops, subChannel, ptySpawned) {
266
266
  nowbot.thread.postMessage({ hereIsYourPort: subChannel.port1 }, [subChannel.port1])
267
267
  nowbot.msgport = subChannel.port2
268
268
 
269
- nowbot.instanceID = UUID()
269
+ nowbot.instanceID = App.imp.getUUID("NowBot(${options.name}) instanceID")
270
270
  nowbot.sender = App.imp.register(nowbot, {
271
271
  name: options.name
272
272
  type: options.type
@@ -428,6 +428,16 @@ closure NowBot(options, local nowbot, nodeops, subChannel, ptySpawned) {
428
428
  nowbot.thread = false
429
429
  destroy()
430
430
  })
431
+
432
+ // close
433
+ //
434
+ // Close and remove our IMP resources, but leave the worker alone.
435
+ //
436
+ nowbot.close = function close() {
437
+ nowbot.impconn.destroy()
438
+ nowbot.impconn = false
439
+ App.imp.unregister(nowbot.instanceID)
440
+ }
431
441
 
432
442
  // destroy
433
443
  //
@@ -436,9 +446,7 @@ closure NowBot(options, local nowbot, nodeops, subChannel, ptySpawned) {
436
446
  nowbot.destroy = function destroy() {
437
447
  terminate()
438
448
  nowbot.thread = false
439
- nowbot.impconn.destroy()
440
- nowbot.impconn = false
441
- App.imp.unregister(nowbot.instanceID)
449
+ close()
442
450
  }
443
451
 
444
452
  // NaaN message
@@ -446,7 +454,7 @@ closure NowBot(options, local nowbot, nodeops, subChannel, ptySpawned) {
446
454
  // A message has been received from NaaN running on the worker thread.
447
455
  //
448
456
  nowbot.msgport.on("message", function (msg, local startmsg) {
449
- if msg.id == "naan-imp-node" {
457
+ if msg.id == "naan-imp-out" {
450
458
  nowbot.impconn.dispatch(new(msg.data))
451
459
  return
452
460
  } else if msg.id == "loaded" {
@@ -542,7 +550,7 @@ closure NobBot(options, local nobbot)
542
550
 
543
551
  nobbot.impconn = App.imp.responder(function(data) {
544
552
  nobbot.msgport.postMessage({ // send to worker
545
- id: "naan-imp-node" // scoped for NodeWorkerNub
553
+ id: "naan-imp-in" // scoped for NodeWorkerNub
546
554
  data: data
547
555
  })
548
556
  list(false, { sent: true })
@@ -13,106 +13,47 @@
13
13
  /*
14
14
  * WebSocket
15
15
  *
16
- * Create a WebSocket client connection object.
16
+ * Create a WebSocket client connection object, returning a tuple.
17
17
  *
18
18
  * Required:
19
- * host: <string> // host:port
19
+ * urlstr: <string>
20
20
  *
21
21
  * Options:
22
22
  * {
23
23
  * wss: <boolean> // true to use secure connection
24
+ * query: <string> // query string starting with ?
24
25
  * onopen: <proc>(e) // connection has opened
25
26
  * onmessage: <proc>(e, message) // message received
26
27
  * onerror: <proc>(e) // error occurred
27
28
  * onclose: <proc>(e) // connection has closed
28
- * query: <string> // query string starting with ? for insecure connections
29
- * querys: <string> // query string starting with ? for secure connections
30
29
  * }
31
30
  *
32
31
  */
33
32
 
34
- closure WebSocket(host, options, local wsock) {
35
- global(js)
36
- wsock = new(object, this)
37
- options = new(options) || { }
33
+ closure WebSocket(urlstr, options, local error, wslib, url, ws) {
34
+ `(error, wslib) = await(js.i("ws"))
35
+ if error
36
+ return (list(error))
38
37
 
39
- // connect
40
- //
41
- // Connect with the host if not already connected, returning a result tuple.
42
- //
43
- wsock.connect = closure connect(local error, ws, url, pending) {
44
- if wsock.ws
45
- return (list(false, { open: true }))
46
- `(error, ws) = await(js.i("ws"))
47
- if error
48
- return (list(error))
49
- if options.wss
50
- url = "wss://${host}${options.querys||''}"
51
- else
52
- url = "ws://${host}${options.query||''}"
53
- wsock.ws = xnew(ws.default, url)
54
- pending = new(nonce)
55
-
56
- // onopen
57
- // The WebSocket connection is now open.
58
- wsock.ws.onopen = closure onopen(e) {
59
- pending.signal(list(false, { ok: true }))
60
- options.onopen(e, e.target, e.type)
61
- }
62
-
63
- // onmessage
64
- // A message was received from the worker via WebSockets.
65
- wsock.ws.onmessage = closure onmessage(e, local message) {
66
- try {
67
- message = new(JSONparse(e.data))
68
- } catch {
69
- debuglog("can't decode incoming ws message:", exception)
70
- options.onerror(Error("can't decode incoming message:", exception))
71
- return
72
- }
73
- options.onmessage(e, message)
74
- }
75
-
76
- // onerror
77
- // An error occurred on the WebSocket connection.
78
- wsock.ws.onerror = closure onerror(e, local error) {
79
- error = Error("websocket failed", e)
80
- options.onerror(e, error)
81
- pending.signal(list(error)) // ensure connect completes
82
- }
83
-
84
- // onclose
85
- // The WebSocket connection was closed.
86
- wsock.ws.onclose = closure onclose(e) {
87
- options.onclose(e, e.code, e.reason)
88
- pending.signal(list(Error("connection closed"))) // ensure connect completes
89
- wsock.ws = false
90
- }
91
-
92
- pending.wait()
38
+ try {
39
+ url = xnew(js.g.URL, urlstr)
40
+ } catch {
41
+ return (list(Error("invalid URL ${urlstr}", exception)))
93
42
  }
94
-
95
- // send
96
- //
97
- // Send a message dictionary.
98
- //
99
- wsock.send = function send(message) {
100
- wsock.ws.send(JSONstringify(message))
101
- }
102
-
103
- // close
104
- //
105
- // Close the connection.
106
- //
107
- wsock.close = function close(reason) {
108
- if !reason
109
- reason = 1000 // normal close
110
- wsock.ws.close(reason)
111
- }
112
-
113
- // finis
114
-
115
- wsock
43
+ options = merge({
44
+ query: url.search || ''
45
+ wss: url.protocol == "https:"
46
+ }, options)
47
+ if options.wss
48
+ url = "wss://${url.host}${options.query}"
49
+ else
50
+ url = "ws://${url.host}${options.query}"
51
+ ws = xnew(wslib.default, url)
52
+ ws.onopen = options.onopen
53
+ ws.onmessage = options.onmessage
54
+ ws.onerror = options.onerror
55
+ ws.onclose = options.onclose
56
+ list(false, ws)
116
57
  };
117
58
 
118
59
 
@@ -304,18 +304,26 @@ closure Builder(rules, operation, fs, srcpath, destpath, track, local build, gro
304
304
  //
305
305
  // Apply substitutions from readSubstitutionFile to string, returning a tuple of the resulting
306
306
  // text and the number of substitution patterns actually used from the supplied dictionary.
307
- //
308
- function applySubstitutions(text, subs, local subbed) {
309
- subbed = { }
310
- list(text.replace(RegExp("[$][^$\\s]+[$]", "g"), function(match, local subst) {
311
- subst = subs[match.slice(1,-1)]
312
- if subst {
313
- subbed[match] = true
314
- subst
315
- }
316
- else
317
- match
318
- }), length(subbed))
307
+ // Specify iterative to repeat the substitution on the result until no more substitutions occur.
308
+ //
309
+ function applySubstitutions(text, subs, iterative, local count, subbed) {
310
+ count = 0
311
+ loop {
312
+ subbed = { }
313
+ text = text.replace(RegExp("[$][^$\\s]+[$]", "g"), function(match, local subst) {
314
+ subst = subs[match.slice(1,-1)]
315
+ if subst {
316
+ subbed[match] = true
317
+ subst
318
+ }
319
+ else
320
+ match
321
+ })
322
+ count += length(subbed)
323
+ if length(subbed) == 0 || !iterative
324
+ break
325
+ }
326
+ list(text, count)
319
327
  }
320
328
 
321
329
  //
@@ -1044,9 +1052,9 @@ closure Builder(rules, operation, fs, srcpath, destpath, track, local build, gro
1044
1052
  }
1045
1053
  else if ((`(error, content) = fs.readFile(entry.readpath, readOptions)) && !error) {
1046
1054
  if entry.license
1047
- `(content) = applySubstitutions(content, licFileDefs)
1055
+ `(content) = applySubstitutions(content, licFileDefs, true)
1048
1056
  if entry.version
1049
- `(content) = applySubstitutions(content, verFileDefs)
1057
+ `(content) = applySubstitutions(content, verFileDefs, true)
1050
1058
  if entry.naanpack
1051
1059
  `(error, content) = parseAndPack(content, entry.readpath, { naanpack: true })
1052
1060
  else if entry.jspack
@@ -1251,7 +1259,8 @@ closure Builder(rules, operation, fs, srcpath, destpath, track, local build, gro
1251
1259
  //
1252
1260
  // Run the current build product by spawning a new worker.
1253
1261
  //
1254
- build.runSpawn = function runSpawn(local error, maintext, executor, vpath, site, dirpath) {
1262
+ build.runSpawn = function runSpawn(stage,
1263
+ local error, maintext, executor, vpath, site, dirpath) {
1255
1264
  `(error, maintext) = fs.readFile(JSpath.join(build.destpath, build.runway.main))
1256
1265
  if error
1257
1266
  error = Error("Builder: cannot read main file", error)
@@ -1291,13 +1300,12 @@ closure Builder(rules, operation, fs, srcpath, destpath, track, local build, gro
1291
1300
  //
1292
1301
  // Run the current build product by opening a new window.
1293
1302
  //
1294
- build.runWindow = function runWindow(
1303
+ build.runWindow = function runWindow(stage,
1295
1304
  local title, main, url, features, vpath, error, window, site) {
1296
1305
  if !js.w
1297
1306
  return (list(Error('Run method "window" requires browser host')))
1298
1307
  if !build.runway."v-site"
1299
1308
  return (list(Error('Run method "window" requires v-site specification')))
1300
- title = rules.name.concat("-", build.buildnum)
1301
1309
  main = build.runway.main
1302
1310
  if !main
1303
1311
  main = "index.html"
@@ -1313,6 +1321,7 @@ closure Builder(rules, operation, fs, srcpath, destpath, track, local build, gro
1313
1321
  if error
1314
1322
  return (list(Error("Builder: cannot create virtual site", error)))
1315
1323
  sleep(1) // needed on Windows or it won't open, fiik why
1324
+ title = rules.name.concat("-", stage, "-", operation)
1316
1325
  window = js.w.open(url, title, features)
1317
1326
  list(false, { ok: true }) // we don't know the executor
1318
1327
  }
@@ -1323,13 +1332,13 @@ closure Builder(rules, operation, fs, srcpath, destpath, track, local build, gro
1323
1332
  // Run the current build product, which would be after the make call. Returns a standard result
1324
1333
  // tuple.
1325
1334
  //
1326
- build.run = function run() {
1335
+ build.run = function run(stage) {
1327
1336
  if build.runway.open
1328
1337
  fs.shellOpen(JSpath.join(build.destpath, build.runway.open), build.runway.args)
1329
1338
  else if build.runway.spawn
1330
- build.runSpawn()
1339
+ build.runSpawn(stage)
1331
1340
  else if build.runway.window
1332
- build.runWindow()
1341
+ build.runWindow(stage)
1333
1342
  else
1334
1343
  list(false, { ok: true })
1335
1344
  }
@@ -296,7 +296,7 @@ closure projConnector(projman, projtype, local connector, watch) {
296
296
  *
297
297
  * Nide.proj/
298
298
  * nide_cliser.cfg - JSON definition of the project (see below)
299
- * NaanIDE_version.txt - [default] substitution file defining 1.7.0+1 etc.
299
+ * NaanIDE_version.txt - [default] substitution file defining 1.7.1+1 etc.
300
300
  * NaanIDE_version_builds.txt - [default] contains current build number as decimal string
301
301
  * build/ - [optional] default build output location
302
302
  * NaanIDE.lic - [optional] defines Zulch Laboratories, Inc. and other license info
@@ -646,7 +646,7 @@ closure projConfig(projman, where, projID, local procon, fs, configpath) {
646
646
  if builder.refresh()
647
647
  buildonly = !buildonly // reverse if refresh
648
648
  if !buildonly
649
- `(error) = builder.run()
649
+ `(error) = builder.run(stage)
650
650
  }
651
651
  builder.destroy()
652
652
  procon.builder = false
@@ -23,13 +23,21 @@ closure ExecutorBase(track, type, name, local exec) {
23
23
  exec.type = type
24
24
  exec.name = name
25
25
  exec.contexts = { }
26
-
26
+
27
27
  // execReset
28
28
  //
29
29
  // Initialize remote communications for a new or restarted instance.
30
30
  //
31
- exec.execReset = function execReset() {
31
+ exec.execReset = function execReset(error, local xid, pending) {
32
+ if !error
33
+ error = Error("execReset")
34
+ for `(xid, pending) in exec.pending {
35
+ pending.signal(list(error))
36
+ exec.pending[xid] = undefined
37
+ }
32
38
  exec.pending = { }
39
+ exec.inst_ready = false
40
+ exec.inst_error = undefined
33
41
  exec.nextSendID = toint(RandomBytes(3)) // keep our sequence code unique for shared servers
34
42
  }
35
43
  execReset()
@@ -52,7 +60,7 @@ closure ExecutorBase(track, type, name, local exec) {
52
60
  pending.signal(Error("execSend failed", error))
53
61
  pending
54
62
  }
55
-
63
+
56
64
  // execReceived
57
65
  //
58
66
  // The specified message was received from the instance, so signal it.
@@ -81,7 +89,7 @@ closure ExecutorBase(track, type, name, local exec) {
81
89
  exec.pending[data.xid] = undefined
82
90
  pending.signal(list(false, data))
83
91
  }
84
-
92
+
85
93
  // execSetStatus
86
94
  //
87
95
  // Received a status update from the instance. This makes a copy of the specified dictionary for
@@ -93,7 +101,7 @@ closure ExecutorBase(track, type, name, local exec) {
93
101
  exec.execSetStatus = function execSetStatus(status) {
94
102
  exec.xstatus = new(status) // for GC (!)
95
103
  }
96
-
104
+
97
105
  // execFailed
98
106
  //
99
107
  // Communication has failed.
@@ -103,19 +111,15 @@ closure ExecutorBase(track, type, name, local exec) {
103
111
  for `(xid, pending) in exec.pending
104
112
  pending.signal(list(error))
105
113
  }
106
-
114
+
107
115
  // execClosed
108
116
  //
109
- //
110
117
  // Note that the connection has closed, so signal all the waiters.
111
118
  //
112
- exec.execClosed = closure execClosed(error, local xid, pending) {
113
- for `(xid, pending) in exec.pending {
114
- pending.signal(list(error))
115
- exec.pending[xid] = undefined
116
- }
119
+ exec.execClosed = closure execClosed(error) {
120
+ execReset(error)
117
121
  }
118
-
122
+
119
123
  // context
120
124
  //
121
125
  // Return a context object for the instance, or an error, in a result tuple. The optional state
@@ -157,7 +161,7 @@ closure ExecutorBase(track, type, name, local exec) {
157
161
  exec.attention = function attention() {
158
162
  track.attention(exec)
159
163
  }
160
-
164
+
161
165
  // finis
162
166
  exec
163
167
  };
@@ -220,6 +224,8 @@ closure ExecutorContext(exec, state, coder, local context, util) {
220
224
  // an optional tuple of symbols to retrieve after evaluation.
221
225
  //
222
226
  context.eval = closure evalf(expr, put, get, local roots, pkg, data, pending, result) {
227
+ if !exec.inst_ready
228
+ return (list(Error("execution context closed")))
223
229
  if tuple(put)
224
230
  roots = cons(`expr, put)
225
231
  else
@@ -288,6 +294,8 @@ closure ExecutorContext(exec, state, coder, local context, util) {
288
294
  // The procedure must be specified as a symbol or string.
289
295
  //
290
296
  context.rpc = closure rpc(proc, args, local data, pending, result) {
297
+ if !exec.inst_ready
298
+ return (list(Error("execution context closed")))
291
299
  data = {
292
300
  contextID: state.contextID
293
301
  proc: proc
@@ -611,7 +619,7 @@ closure ExecutorTracker(local xtrak) {
611
619
  }
612
620
 
613
621
  // add
614
- // Add an execution instance at the specified location.
622
+ // Add an execution instance.
615
623
 
616
624
  xtrak.add = closure add(executor, local instance) {
617
625
  instance = new(nonce)