geonix 1.10.8 → 1.11.0-beta.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.
package/index.d.ts CHANGED
@@ -124,7 +124,7 @@ export class Service {
124
124
  #private;
125
125
  }
126
126
  /**
127
- * Converts data to stream
127
+ * Converts input data to stream
128
128
  *
129
129
  * @param {*} data
130
130
  * @param {*} automated
@@ -133,6 +133,8 @@ export class Service {
133
133
  export function Stream(data: any, tag?: string): any;
134
134
  export function isStream(object: any): any;
135
135
  export function getReadable(object: any): Promise<any>;
136
+ export function getReadableOverHTTP(object: any): Promise<any>;
137
+ export function getReadableOverNATS(object: any): Promise<any>;
136
138
  export function streamToBuffer(object: any): Promise<any>;
137
139
  export function streamToString(object: any): Promise<any>;
138
140
  export const stats: {};
@@ -147,6 +149,10 @@ export function parseURL(url: string): {
147
149
  pass: any;
148
150
  token: any;
149
151
  };
152
+ export function timeoutAsyncGenerator(target: any, ms: any): {};
153
+ export function waitForAbort(abortSignal: any, result: any): Promise<any>;
154
+ export function abortableAsyncGenerator(target: any, abortSignal: any): {};
155
+ export function getNetworkAddresses(): any[];
150
156
  export function sleep(delay: number): Promise<any>;
151
157
  export function picoid(size?: number): any;
152
158
  export function hash(data: string | Buffer): any;
@@ -165,7 +171,6 @@ export function ServeStatic(root: any, options?: {}): any;
165
171
  export const webserver: WebServer;
166
172
  declare class WebServer {
167
173
  start(): Promise<void>;
168
- getAddresses(): any[];
169
174
  getPort(): any;
170
175
  router(): any;
171
176
  waitUntilReady(): Promise<void>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "geonix",
3
- "version": "1.10.8",
3
+ "version": "1.11.0-beta.1",
4
4
  "type": "module",
5
5
  "description": "",
6
6
  "bin": {
@@ -20,9 +20,9 @@
20
20
  "express-async-errors": "^3.1.1",
21
21
  "express-ws": "^5.0.2",
22
22
  "multer": "^1.4.5-lts.1",
23
- "nats": "^2.16.0",
23
+ "nats": "^2.19.0",
24
24
  "semver": "^7.5.4",
25
- "ws": "^8.13.0"
25
+ "ws": "^8.16.0"
26
26
  },
27
27
  "publishConfig": {
28
28
  "registry": "https://registry.npmjs.org/"
package/src/Service.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { codec, connection } from './Connection.js'
2
- import { picoid, sleep, hash, getSecondsSinceMidnight, OverlayObject, GeonixVersion } from './Util.js'
2
+ import { picoid, sleep, hash, getSecondsSinceMidnight, OverlayObject, GeonixVersion, getNetworkAddresses } from './Util.js'
3
3
  import { webserver } from './WebServer.js'
4
4
  import { createConnection } from 'net'
5
5
  import { EOL } from 'os'
@@ -67,7 +67,7 @@ export class Service {
67
67
  .filter(methodName => !protectedMethodNames.includes(methodName))
68
68
  .filter(methodName => !methodName.startsWith('$')),
69
69
  // IP addresses
70
- a: webserver.getAddresses().map(address => `${address}:${webserver.getPort()}`)
70
+ a: getNetworkAddresses().map(address => `${address}:${webserver.getPort()}`)
71
71
  }
72
72
 
73
73
  // check if method takes context as first argument
package/src/Stream.js CHANGED
@@ -1,13 +1,28 @@
1
1
  import { Readable } from 'stream'
2
2
  import { connection } from './Connection.js'
3
- import { picoid, StreamChunker } from './Util.js'
3
+ import { abortableAsyncGenerator, createServerAtFreePort, getNetworkAddresses, picoid, StreamChunker } from './Util.js'
4
+ import http from 'http'
4
5
 
5
6
  const CHUNK_SIZE = 1024 * 128
7
+ const STREAM_TIMEOUT = 120000
6
8
 
7
9
  export const stats = {}
8
10
 
11
+ // HTTP stream server
12
+ const streams = {}
13
+ const { port: streamServerPort } = await createServerAtFreePort(http, (req, res) => {
14
+ const stream = streams[decodeURIComponent(req.url.substring(1))]
15
+ if (stream) {
16
+ stream.pipe(res)
17
+ } else {
18
+ res.statusCode = 404
19
+ res.end()
20
+ return
21
+ }
22
+ }, 40000)
23
+
9
24
  /**
10
- * Converts data to stream
25
+ * Converts input data to stream
11
26
  *
12
27
  * @param {*} data
13
28
  * @param {*} automated
@@ -18,6 +33,7 @@ export function Stream(data, tag = '_') {
18
33
  return data
19
34
 
20
35
  const id = picoid()
36
+ const result = { $: 'stream', id }
21
37
  let readable = data
22
38
 
23
39
  // convert Buffer or string to a Readable
@@ -31,10 +47,18 @@ export function Stream(data, tag = '_') {
31
47
 
32
48
  stats[tag] = stats[tag] !== undefined ? stats[tag] + 1 : 1
33
49
 
34
- const controlHandler = async () => {
50
+ const acDeliveryOverNATS = new AbortController();
51
+
52
+ const deliverOverNATS = async () => {
35
53
  const control = await connection.subscribe(`gx2.stream.${id}.a`, { max: 1 })
36
54
 
37
- for await (const event of control)
55
+ // abort if no request to start streaming
56
+ const timeout = setTimeout(() => {
57
+ acDeliveryOverNATS.abort()
58
+ }, STREAM_TIMEOUT)
59
+
60
+ // deliver the stream
61
+ for await (const event of abortableAsyncGenerator(control, acDeliveryOverNATS.signal)) {
38
62
  if (event.data.length === 0) {
39
63
  readable.on('data', chunk => connection.publishRaw(`gx2.stream.${id}.b`, chunk))
40
64
  readable.on('close', () => {
@@ -42,11 +66,37 @@ export function Stream(data, tag = '_') {
42
66
  stats[tag]--
43
67
  })
44
68
  }
69
+ }
70
+
71
+ // cleanup
72
+ clearTimeout(timeout)
73
+ await control.drain()
74
+ await connection.unsubscribe(control)
45
75
  }
46
76
 
47
- controlHandler()
77
+ const deliverOverHTTP = async () => {
78
+ // stop listening for NATS request
79
+ acDeliveryOverNATS.abort()
80
+
81
+ // register stream as available over HTTP
82
+ streams[id] = readable
48
83
 
49
- return { $: 'stream', id }
84
+ // cleanup
85
+ readable.on('finish', () => {
86
+ delete streams[id]
87
+ stats[tag]--
88
+ })
89
+ }
90
+
91
+ deliverOverNATS()
92
+
93
+ if (streamServerPort != -1) {
94
+ result.a = getNetworkAddresses()
95
+ result.p = streamServerPort;
96
+ deliverOverHTTP()
97
+ }
98
+
99
+ return result
50
100
  }
51
101
 
52
102
  export function isStream(object) {
@@ -57,6 +107,43 @@ export async function getReadable(object) {
57
107
  if (!isStream(object))
58
108
  return object
59
109
 
110
+ try {
111
+ return await getReadableOverHTTP(object);
112
+ } catch (e) {
113
+ return getReadableOverNATS(object);
114
+ }
115
+ }
116
+
117
+ export async function getReadableOverHTTP(object) {
118
+ if (!object.a) {
119
+ throw new Error('Stream is not available over HTTP')
120
+ }
121
+
122
+ for (const address of object.a) {
123
+ try {
124
+ const response = await new Promise((resolve, reject) => {
125
+ const request = http.request(`http://${address}:${object.p}/${object.id}`, { method: 'GET', timeout: 5000 })
126
+ request.on('response', (response) => {
127
+ resolve(response)
128
+ })
129
+
130
+ request.on('error', (e) => {
131
+ reject(e)
132
+ })
133
+
134
+ request.end()
135
+ })
136
+
137
+ return response
138
+ } catch (e) {
139
+ console.error(`Stream.getReadableOverHTTP.error:`, e)
140
+ }
141
+ }
142
+
143
+ throw new Error('No data')
144
+ }
145
+
146
+ export async function getReadableOverNATS(object) {
60
147
  const readable = new Readable({ read: () => null })
61
148
  const subscription = await connection.subscribe(`gx2.stream.${object.id}.b`)
62
149
 
package/src/Util.js CHANGED
@@ -3,11 +3,9 @@ import { URL, fileURLToPath } from 'url'
3
3
  import { readFile } from 'fs/promises'
4
4
  import { join } from 'path'
5
5
  import { Transform } from 'node:stream'
6
- import WebSocket from 'ws'
7
- import * as http from 'http'
8
- import * as https from 'http'
9
- import * as net from 'net'
10
-
6
+ import { networkInterfaces } from 'os'
7
+ import http from 'http'
8
+ import https from 'http'
11
9
  /**
12
10
  * Wait for {delay} ms
13
11
  * @param {number} delay
@@ -196,3 +194,54 @@ export const StreamChunker = (chunkSize = 65536) => {
196
194
 
197
195
  return chunker;
198
196
  }
197
+
198
+ export async function* timeoutAsyncGenerator(target, ms) {
199
+ const iterator = target[Symbol.asyncIterator]()
200
+
201
+ while (true) {
202
+ const { value, done } = await Promise.race([iterator.next(), sleep(ms)]) || {};
203
+ if (done || (value === undefined && done === undefined)) {
204
+ break;
205
+ }
206
+
207
+ yield value;
208
+ }
209
+ }
210
+
211
+ export async function waitForAbort(abortSignal, result) {
212
+ return new Promise(resolve => {
213
+ if (abortSignal.aborted) {
214
+ resolve(result)
215
+ }
216
+
217
+ abortSignal.addEventListener('abort', () => {
218
+ console.log('aborted')
219
+ resolve(result)
220
+ })
221
+ })
222
+ }
223
+
224
+ export async function* abortableAsyncGenerator(target, abortSignal) {
225
+ const iterator = target[Symbol.asyncIterator]()
226
+
227
+ while (true) {
228
+ const { value, done } = await Promise.race([iterator.next(), waitForAbort(abortSignal)]) || {};
229
+ if (done || (value === undefined && done === undefined)) {
230
+ break;
231
+ }
232
+
233
+ yield value;
234
+ }
235
+ }
236
+
237
+ export function getNetworkAddresses() {
238
+ const list = []
239
+ const interfaces = networkInterfaces()
240
+
241
+ for (let interfaceAddresses of Object.values(interfaces))
242
+ for (let addressObject of interfaceAddresses)
243
+ if (addressObject.family === 'IPv4')
244
+ list.push(addressObject.address)
245
+
246
+ return list
247
+ }
package/src/WebServer.js CHANGED
@@ -102,18 +102,6 @@ class WebServer {
102
102
  this.#ready = true
103
103
  }
104
104
 
105
- getAddresses() {
106
- const list = []
107
- const interfaces = networkInterfaces()
108
-
109
- for (let interfaceAddresses of Object.values(interfaces))
110
- for (let addressObject of interfaceAddresses)
111
- if (addressObject.family === 'IPv4')
112
- list.push(addressObject.address)
113
-
114
- return list
115
- }
116
-
117
105
  getPort() {
118
106
  return this.#port
119
107
  }