@naanlang/naan 1.5.0 → 1.6.0

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.
Files changed (44) hide show
  1. package/LICENSE.md +2 -2
  2. package/README.md +2 -2
  3. package/bin/index.js +7 -3
  4. package/dist/env_web.js +162 -20
  5. package/dist/naan.min.js +5 -5
  6. package/frameworks/browser/https_request.nlg +2 -2
  7. package/frameworks/browser/sworker.js +28 -26
  8. package/frameworks/browser/terminals.nlg +329 -693
  9. package/frameworks/browser/worker.nlg +255 -0
  10. package/frameworks/browser/workers.nlg +5 -5
  11. package/frameworks/browser/ws_client.nlg +15 -10
  12. package/frameworks/client/apiclient.nlg +181 -9
  13. package/frameworks/client/relaycon.nlg +87 -161
  14. package/frameworks/common/events.nlg +101 -0
  15. package/frameworks/common/repltools.nlg +80 -1
  16. package/frameworks/node/apiserver.nlg +39 -246
  17. package/frameworks/node/filesystem.nlg +31 -6
  18. package/frameworks/node/http_request.nlg +34 -13
  19. package/frameworks/node/https_request.nlg +8 -3
  20. package/frameworks/node/node.nlg +2 -0
  21. package/frameworks/node/pty.nlg +346 -0
  22. package/frameworks/node/worker.nlg +409 -539
  23. package/frameworks/node/ws_client.nlg +2 -2
  24. package/frameworks/project/build.nlg +1 -1
  25. package/frameworks/project/projects.nlg +1 -1
  26. package/frameworks/running/debugnub.nlg +17 -4
  27. package/frameworks/running/executors.nlg +27 -274
  28. package/frameworks/running/instances.nlg +393 -0
  29. package/frameworks/running/remote_req.nlg +143 -0
  30. package/frameworks/running/remote_res.nlg +89 -0
  31. package/frameworks/running/taskexec.nlg +2 -2
  32. package/frameworks/service/imp.nlg +736 -0
  33. package/frameworks/service/model.nlg +71 -0
  34. package/frameworks/service/nubnode.nlg +105 -0
  35. package/frameworks/service/nubweb.nlg +106 -0
  36. package/frameworks/service/service.nlg +28 -0
  37. package/lib/browser/env_web.js +169 -27
  38. package/lib/browser/env_webworker.js +91 -4
  39. package/lib/core/naanlib.js +5 -5
  40. package/lib/env_node.js +184 -8
  41. package/lib/env_nodeworker.js +74 -1
  42. package/package.json +1 -1
  43. package/plugins/serviceNexara/speechrec.nlg +10 -6
  44. package/test/test_02_context.nlg +24 -1
@@ -0,0 +1,255 @@
1
+ /*
2
+ * worker.nlg
3
+ * Naanlib/frameworks/browser
4
+ *
5
+ * Manage NaaN instances.
6
+ *
7
+ * NaaN instances are managed by a top/bottom pair of of objects. InBot (INstance BOTtom) objects
8
+ * reside in the same JavaScript environment (node/browser) as their worker and communicate with the
9
+ * appropriate env_xxx.js adapter for NaaN in that environment.
10
+ *
11
+ * Zero or more InTop objects connect to each InBot object via the IMP network, providing remote
12
+ * evaluation and terminal access services. Each InTop supports only one terminal simultaneously, but
13
+ * they can be attached and detached at any time.
14
+ *
15
+ * column positioning: // // !
16
+ *
17
+ * Copyright (c) 2026 by Richard C. Zulch
18
+ *
19
+ */
20
+
21
+
22
+ /*
23
+ * InBot
24
+ *
25
+ * Create a remote interface for a single execution instance, returning the object. This provides
26
+ * instance access to zero or more InTops via IMP connections. The tops subscribe to receive
27
+ * notifications/responses.
28
+ *
29
+ * Options:
30
+ * {
31
+ * name: <string> // target name, e.g. "Play"
32
+ * type: <string> // target category, e.g. "Local"
33
+ * startup: <dictionary> // startup options
34
+ * workerID: <string> // initialize worker with this
35
+ * workerGUID: <string> // initialize worker with this
36
+ * topOriginID: <string> // so top can route to us through IMP
37
+ * }
38
+ *
39
+ */
40
+
41
+ closure InBot(worker, options, local inbot)
42
+ {
43
+ global(App)
44
+ inbot = new(object, this)
45
+ inbot.topsubs = []
46
+ options = merge({
47
+ type: "Local"
48
+ }, options)
49
+ inbot.workerID = options.workerID
50
+ inbot.worker = worker
51
+
52
+ inbot.instanceID = UUID()
53
+ inbot.sender = App.imp.register(inbot, {
54
+ name: options.name
55
+ type: options.type
56
+ instanceID: inbot.instanceID
57
+ services: ["naan-worker"]
58
+ })
59
+
60
+ // If this InBot is created remotely then we need to send a message from our new instanceID
61
+ // to the InTop origin so that it can send messages to us. The intermediate gateways save the
62
+ // route to allow replies.
63
+ if options.topOriginID && options.topOriginID != App.imp.us.instanceID
64
+ inbot.sender(options.topOriginID) // create route top->bottom
65
+
66
+ // subscribe
67
+ //
68
+ // Add an InTop to our subscribers if not already known.
69
+ //
70
+ function subscribe(destID) {
71
+ if !inbot.topsubs.find(function(item) { item == destID }) {
72
+ inbot.topsubs.push(destID)
73
+ if inbot.workerDestID
74
+ inbot.sender(destID, {
75
+ id: "workerDestID"
76
+ workerDestID: inbot.workerDestID
77
+ })
78
+ }
79
+ }
80
+
81
+ // unsubscribe
82
+ //
83
+ // Remove an InTop from our subscribers if present.
84
+ //
85
+ function unsubscribe(destID) {
86
+ inbot.topsubs = inbot.topsubs.filter(function(item) { item != destID })
87
+ }
88
+
89
+ // notifyAll
90
+ //
91
+ // Send the message to all our subscribers.
92
+ //
93
+ inbot.notifyAll = function notifyAll(message, exceptID, local topDestID) {
94
+ for topDestID in inbot.topsubs
95
+ if topDestID != exceptID
96
+ inbot.sender(topDestID, message)
97
+ }
98
+
99
+ // impUnknown
100
+ //
101
+ // Received an unknown destination notice from the other side.
102
+ //
103
+ inbot.impUnknown = function impUnknown(unknownID, sourceID, gateway) {
104
+ if inbot.topsubs[unknownID] { // did not have courtesy to unsubscribe!
105
+ unsubscribe(unknownID)
106
+ return
107
+ }
108
+ if !gateway
109
+ debuglog("InBot.impUnknownID:", unknownID, "sourceID:", sourceID)
110
+ }
111
+
112
+ // impReceive
113
+ //
114
+ // Received a message from InTop.
115
+ //
116
+ inbot.impReceive = function impReceive(message, sourceID) {
117
+ if message.id == "destroy"
118
+ destroy() // told to destroy worker
119
+ else if message.id == "subscribe"
120
+ subscribe(sourceID)
121
+ else if message.id == "unsubscribe"
122
+ unsubscribe(sourceID)
123
+ else if message.id == "typed"
124
+ notifyAll(message, sourceID)
125
+ else { // relay to worker
126
+ message.scope = "naan-env-web"
127
+ inbot.worker.postMessage(message)
128
+ }
129
+ }
130
+
131
+ // onmessage
132
+ //
133
+ // A message was received from the worker.
134
+ //
135
+ inbot.worker.addEventListener("message", function(e, local msg) {
136
+ msg = new(e.data)
137
+ if msg.scope == "naan-imp-web" {
138
+ inbot.impconn.dispatch(msg.data)
139
+ return
140
+ }
141
+ if msg.scope != "naan-env-web"
142
+ return
143
+ if msg.id == "loaded" {
144
+ if !inbot.started
145
+ inbot.worker.postMessage({
146
+ scope: "naan-env-web"
147
+ id: "start",
148
+ state: false // resume state, if we have any
149
+ altcmd: options.startup.initcmds
150
+ workerID: options.workerID
151
+ workerGUID: options.workerGUID
152
+ metadata: {
153
+ mainInstanceID: App.imp.us.instanceID
154
+ name: options.name
155
+ }
156
+ })
157
+ inbot.started = true
158
+ }
159
+ notifyAll(msg)
160
+ })
161
+
162
+ // onerror
163
+ //
164
+ // The worker had an error.
165
+ //
166
+ inbot.worker.addEventListener("error", function(e) {
167
+ debuglog("worker error:", e.message)
168
+ notifyAll({
169
+ id: "error"
170
+ data: e.message
171
+ })
172
+ inbot.worker = false
173
+ destroy()
174
+ })
175
+
176
+ // destroy
177
+ //
178
+ // Ensure the worker is destroyed and notify everyone.
179
+ //
180
+ inbot.destroy = function destroy() {
181
+ if inbot.worker {
182
+ inbot.worker.terminate() // kill without saving
183
+ inbot.worker = false
184
+ }
185
+ notifyAll({
186
+ id: "terminated"
187
+ })
188
+ inbot.impconn.close()
189
+ inbot.impconn = false
190
+ App.imp.unregister(inbot.instanceID)
191
+ }
192
+
193
+ // register our connects *after* the receivers are configured above
194
+
195
+ // finis
196
+
197
+ inbot
198
+ };
199
+
200
+
201
+ /*
202
+ * NubBot
203
+ *
204
+ * Create a remote interface like InBot, but also provide messaging support for WebWorkerNub inside
205
+ * the worker.
206
+ *
207
+ */
208
+
209
+ closure NubBot(worker, options, local nubbot)
210
+ {
211
+ global(App)
212
+ nubbot = InBot(worker, options)
213
+
214
+ App.imp.addEventListener("active", function(event, info) {
215
+ if info.ourID == nubbot.instanceID {
216
+ nubbot.workerDestID = info.destID // handshake completed
217
+ nubbot.notifyAll({ // send to tops
218
+ id: "workerDestID"
219
+ workerDestID: nubbot.workerDestID
220
+ })
221
+ }
222
+ })
223
+
224
+ nubbot.impconn = App.imp.responder(function(data) {
225
+ nubbot.worker.postMessage({ // send to worker
226
+ scope: "naan-imp-web" // scoped for WorkerNub
227
+ data: data
228
+ })
229
+ list(false, { sent: true })
230
+ }, {
231
+ ourID: nubbot.instanceID
232
+ })
233
+
234
+ // finis
235
+
236
+ nubbot
237
+ };
238
+
239
+
240
+ /*
241
+ * workInit
242
+ *
243
+ * Initialize the component.
244
+ *
245
+ */
246
+
247
+ function workInit(local manifest) {
248
+ manifest = `(InBot, NubBot, workInit)
249
+
250
+ Naan.module.build(module.id, "worker", function(modobj, compobj) {
251
+ require("./browser.nlg")
252
+ compobj.manifest = manifest
253
+ modobj.exports.InBot = InBot
254
+ })
255
+ } ();
@@ -6,7 +6,7 @@
6
6
  *
7
7
  * column positioning: // // !
8
8
  *
9
- * Copyright (c) 2020-2023 by Richard C. Zulch
9
+ * Copyright (c) 2020-2026 by Richard C. Zulch
10
10
  *
11
11
  */
12
12
 
@@ -427,22 +427,22 @@ closure TrackServiceWorkers(track, pubID, pubVersion, callback, local error) {
427
427
  `(error, serviceWorker) = ScopedServiceWorker(pubID, pubVersion, callback)
428
428
  if error
429
429
  ErrorDebugLog("TrackServiceWorkers: cannot load service worker", error)
430
- track.register(TrackedScope, "V-Site")
430
+ track.register("V-Site", TrackedScope)
431
431
  serviceWorker
432
432
  };
433
433
 
434
434
 
435
435
  /*
436
- * workInit
436
+ * worksInit
437
437
  *
438
438
  * Initialize the debugger page.
439
439
  *
440
440
  */
441
441
 
442
- function workInit(local manifest) {
442
+ function worksInit(local manifest) {
443
443
 
444
444
  manifest = `(ServiceWorker, ScopedServiceWorker, findTrackedScope, TrackedScope,
445
- TrackServiceWorkers, workInit)
445
+ TrackServiceWorkers, worksInit)
446
446
 
447
447
  Naan.module.build(module.id, "workers", function(modobj, compobj) {
448
448
  require("./browser.nlg")
@@ -5,7 +5,7 @@
5
5
  *
6
6
  * column positioning: // // !
7
7
  *
8
- * Copyright (c) 2023-2024 by Richard C. Zulch
8
+ * Copyright (c) 2023-2026 by Richard C. Zulch
9
9
  *
10
10
  */
11
11
 
@@ -13,11 +13,14 @@
13
13
  /*
14
14
  * WebSocket
15
15
  *
16
- * Create a WebSocket client connection object using the existing HTTPS connection.
16
+ * Create a WebSocket client connection object.
17
+ *
18
+ * Optional, default Host:
19
+ * host: <string> // host:port
17
20
  *
18
21
  * Options:
19
22
  * {
20
- * host: <string> // host:port
23
+ * wss: <boolean> // true to use secure connection
21
24
  * onopen: <proc>(e) // connection has opened
22
25
  * onmessage: <proc>(e, message) // message received
23
26
  * onerror: <proc>(e) // error occurred
@@ -28,12 +31,14 @@
28
31
  *
29
32
  */
30
33
 
31
- closure WebSocket(options, local wsock) {
34
+ closure WebSocket(host, options, local wsock) {
32
35
  global(window)
33
36
  wsock = new(object, this)
34
37
  options = new(options) || { }
35
- if !options.host
36
- options.host = window.location.host
38
+ if !host {
39
+ host = window.location.host
40
+ options.wss = window.location.protocol == "https:"
41
+ }
37
42
 
38
43
  // connect
39
44
  //
@@ -42,10 +47,10 @@ closure WebSocket(options, local wsock) {
42
47
  wsock.connect = closure connect(local url, pending) {
43
48
  if wsock.ws
44
49
  return (list(false, { open: true }))
45
- if window.location.protocol == "https:"
46
- url = "wss://${options.host}${options.querys||''}"
50
+ if options.wss
51
+ url = "wss://${host}${options.querys||''}"
47
52
  else
48
- url = "ws://${options.host}${options.query||''}"
53
+ url = "ws://${host}${options.query||''}"
49
54
  wsock.ws = xnew(window.WebSocket, url)
50
55
  pending = new(nonce)
51
56
 
@@ -58,7 +63,7 @@ closure WebSocket(options, local wsock) {
58
63
 
59
64
  // onmessage
60
65
  // A message was received from the worker via WebSockets.
61
- wsock.ws.onmessage = closure onmessage(e, message) {
66
+ wsock.ws.onmessage = closure onmessage(e, local message) {
62
67
  try {
63
68
  message = new(JSONparse(e.data))
64
69
  } catch {
@@ -2,43 +2,63 @@
2
2
  * apiclient.nlg
3
3
  * Naanlib/frameworks/client
4
4
  *
5
- * Access to Nide API with specified HTTP client.
5
+ * Access to Nide API with specified HTTP client.
6
6
  *
7
7
  * column positioning: // // !
8
8
  *
9
- * Copyright (c) 2017-2025 by Richard C. Zulch
9
+ * Copyright (c) 2017-2026 by Richard C. Zulch
10
10
  *
11
11
  */
12
12
 
13
13
 
14
+ // Globals
15
+
16
+ serviceMod;;
17
+
18
+
14
19
  /*
15
20
  * APIClient
16
21
  *
17
- * Make an API client object.
22
+ * Make an API client object.
18
23
  *
19
24
  * Options:
20
25
  * {
21
26
  * url: <string> // hostname or URL of the remote server
22
27
  * guid: <string> // shared secret for access to remote server
23
- * instanceID: <string> // optional UUID for our client; can be persisted by caller
24
28
  * }
25
29
  *
26
30
  */
27
31
 
28
- closure APIClient(options, local client, url, guid) {
29
- global(App)
32
+ closure APIClient(imp, options, local client, url, guid) {
33
+ global(App, serviceMod)
30
34
  client = new(object, this)
35
+ options = merge({
36
+ }, options)
31
37
  url = options.url
32
38
  if !url.match(RegExp("^https?:\/\/", "i"))
33
39
  url = "http://${url}" // a protocol is required
34
40
  if url.slice(-1) != "/"
35
41
  url = url.concat("/") // must end in "/"
36
42
  client.url = url
43
+ options.url = url
37
44
  client.guid = options.guid
38
- client.instanceID = options.instanceID || UUID() // our client's unique ID
39
45
  client.notifyAfter = 0
40
46
  client.requester = clirRequester()
41
47
 
48
+ //
49
+ // wsopen
50
+ //
51
+
52
+ client.wsopen = function wsopen(error) {
53
+ if !client.wesc {
54
+ client.wesc = WebSocketClient(options, imp)
55
+ `(error) = client.wesc.connect()
56
+ if error
57
+ ErrorDebuglog("APIClient: cannot connect to", url)
58
+ }
59
+ }
60
+ wsopen()
61
+
42
62
  // clientRequest() - append our guid to the headers
43
63
 
44
64
  closure clientRequest(url, reqops) {
@@ -152,24 +172,176 @@ closure APIClient(options, local client, url, guid) {
152
172
  else
153
173
  callback(error, content)
154
174
  }
175
+
176
+ // destroy
177
+ //
178
+ // Close down the client and release all resources.
179
+ //
180
+ client.destroy = closure destroy() {
181
+ debuglog("client destroy websocket")
182
+ client.wesc.destroy()
183
+ client.wesc = false
184
+ }
155
185
 
156
186
  client
157
187
  };
158
188
 
159
189
 
190
+ /*
191
+ * WebSocketClient
192
+ *
193
+ * WebSocket client for websocket communications with a server. The Instance Messaage Processor (IMP)
194
+ * handles routing between instances using intermediaries.
195
+ *
196
+ * Options:
197
+ * {
198
+ * url: <string> // hostname or URL of the remote server
199
+ * guid: <string> // shared secret for access to remote server
200
+ * instanceID: <string> // UUID for our client; can be persisted by caller
201
+ * }
202
+ *
203
+ */
204
+
205
+ closure WebSocketClient(options, imp, local wesc, ws) {
206
+ global(js)
207
+ wesc = new(object, this)
208
+
209
+ // connect
210
+ //
211
+ // Establish a websocket connection with the host.
212
+ //
213
+ wesc.connect = closure connect(local url, pending) {
214
+ if ws && !wesc.fatal
215
+ return(list(false, { connected: true }))
216
+ if wesc.fatal
217
+ debuglog("WebSocketClient reopen")
218
+ wesc.fatal = false
219
+ try {
220
+ url = xnew(js.w.URL, options.url)
221
+ } catch {
222
+ return(list(Error("invalid URL ${options.url}", exception)))
223
+ }
224
+ if url.protocol == "https:"
225
+ url.protocol = "wss:"
226
+ else
227
+ url.protocol = "ws:"
228
+ ws = xnew(js.w.WebSocket, url.href)
229
+ pending = new(nonce)
230
+
231
+ // onopen
232
+ // The WebSocket connection is now open.
233
+ ws.onopen = function onopen(e, local error) {
234
+ wesc.impconn = imp.requester(function(data) {
235
+ if wesc.fatal // raw send function
236
+ return (list(wesc.fatal))
237
+ ws.send(js.w.JSON.stringify(data))
238
+ list(false, { sent: true })
239
+ }, {
240
+ secret: options.guid
241
+ label: "api-server"
242
+ wesc: wesc
243
+ })
244
+ if error
245
+ pending.signal(list(error))
246
+ else
247
+ pending.signal(list(false, { ok: true }))
248
+ }
249
+
250
+ // onmessage
251
+ // A message was received via WebSockets.
252
+ ws.onmessage = function onmessage(e, local error, message) {
253
+ `(error, message) = JsonParse(e.data)
254
+ if !error {
255
+ message = new(message)
256
+ if wesc.impconn.dispatch(message)
257
+ return
258
+ }
259
+ debuglog("socket message:", e.data)
260
+ }
261
+
262
+ // onerror
263
+ // An error occurred on the WebSocket connection.
264
+ // This can happen in normal operation when the connection times out.
265
+ ws.onerror = function onerror(e) {
266
+ if !wesc.fatal
267
+ wesc.fatal = Error("websocket failed", e)
268
+ pending.signal(list(wesc.fatal))
269
+ }
270
+
271
+ // onclose
272
+ // The WebSocket connection was closed.
273
+ ws.onclose = function onclose(e) {
274
+ // if e.code != 1000 && e.code != 1001 && e.code != 1005 && e.code != 1006
275
+ debuglog("WebSocketClient: closed:", e.code, e.reason)
276
+ ws = false
277
+ if !wesc.fatal
278
+ wesc.fatal = Error("websocket closed", e.code, e.reason)
279
+ pending.signal(list(wesc.fatal))
280
+ wesc.impconn.close({
281
+ code: e.code
282
+ reason: e.reason
283
+ })
284
+ wesc.impconn = false
285
+ }
286
+
287
+ pending.wait()
288
+ }
289
+
290
+ // close
291
+ //
292
+ // Close the connection, but can be opened again with connect().
293
+ //
294
+ wesc.close = function close() {
295
+ if !ws
296
+ return
297
+ ws.close(1000) // 1000: normal close
298
+ }
299
+
300
+ // destroy
301
+ //
302
+ // Destroy the connection permanently.
303
+ //
304
+ wesc.destroy = function destroy() {
305
+ close()
306
+ }
307
+
308
+ // send
309
+ //
310
+ // Send a message.
311
+ //
312
+ wesc.send = closure send(data) {
313
+ if wesc.fatal
314
+ return (wesc.fatal)
315
+ wesc.impconn.send(data)
316
+ }
317
+
318
+ // sendRaw
319
+ //
320
+ // Send a message with reconnect but no routing.
321
+ //
322
+ wesc.sendRaw = closure sendRaw(data) {
323
+ ws.send(data)
324
+ }
325
+
326
+ // finis
327
+ wesc
328
+ };
329
+
330
+
160
331
  /*
161
332
  * apicInit
162
333
  *
163
- * Initialize the apic component.
334
+ * Initialize the apiclient component.
164
335
  *
165
336
  */
166
337
 
167
338
  function apicInit(local manifest) {
168
339
 
169
- manifest = `(APIClient, apicInit)
340
+ manifest = `(APIClient, WebSocketClient, apicInit)
170
341
 
171
342
  Naan.module.build(module.id, "apiclient", function(modobj, compobj) {
172
343
  Naan.module.require("./client.nlg")
344
+ serviceMod = Naan.module.require("naanlib:frameworks/service/imp.nlg")
173
345
  compobj.manifest = manifest
174
346
  modobj.exports.APIClient = APIClient
175
347
  })