appmesh 1.2.3 → 1.3.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.
package/src/server.js CHANGED
@@ -1,47 +1,85 @@
1
- // server.js - App Mesh Server SDK for Node.js
1
+ // server.js - App Mesh Worker SDK for Node.js
2
2
  // This is a Node.js-only module - not included in browser builds
3
3
 
4
4
  // Environment check - only works in Node.js
5
5
  if (typeof window !== 'undefined' && typeof window.document !== 'undefined') {
6
6
  throw new Error(
7
- 'AppMeshServer is only available in Node.js environment. Use AppMeshClient for browser applications.'
7
+ 'AppMeshWorker is only available in Node.js environment. Use AppMeshClient for browser applications.'
8
8
  )
9
9
  }
10
10
 
11
- import AppMeshClient from './appmesh.js'
11
+ import AppMeshClient, { AppMeshError } from './appmesh.js'
12
12
 
13
13
  /**
14
- * Server SDK for App Mesh application interacting with the local App Mesh REST service over HTTPS.
14
+ * Error thrown when the daemon reports HTTP 412: this process key was superseded
15
+ * by a newer process instance and the caller should stop serving.
16
+ *
17
+ * Subclasses AppMeshError with `statusCode` fixed to 412, so existing catch-based
18
+ * callers keep working. Counterpart of Python `AppMeshProcessSupersededError`,
19
+ * Go `ErrProcessSuperseded` and Rust `AppMeshError::ProcessSuperseded`.
20
+ */
21
+ class ProcessSupersededError extends AppMeshError {
22
+ /**
23
+ * @param {string} message - Error message
24
+ * @param {any} responseData - Raw 412 response body
25
+ */
26
+ constructor (message, responseData = null) {
27
+ super(message, 412, responseData, 'PROCESS_SUPERSEDED')
28
+ this.name = 'ProcessSupersededError'
29
+ }
30
+ }
31
+
32
+ /**
33
+ * True when `value` looks like a {host, port} address object rather than an SSL config.
34
+ * Mirrors the acceptance logic in appmesh_tcp.js (kept local: appmesh_tcp.js is lazy-imported).
35
+ * @private
36
+ */
37
+ function _isTcpAddressObject (value) {
38
+ return !!value && typeof value === 'object' && !Array.isArray(value) && !Buffer.isBuffer(value) &&
39
+ ('host' in value || 'port' in value) &&
40
+ !('ca' in value) && !('cert' in value) && !('key' in value) && !('rejectUnauthorized' in value)
41
+ }
42
+
43
+ /**
44
+ * Internal sentinel: AppMeshWorkerTCP passes this as `options.client` to tell the base
45
+ * constructor to skip building a default HTTP client (the TCP client is created lazily
46
+ * by _ensureClient()). Callers passing `client: null`/`undefined` still get the default.
47
+ * @private
48
+ */
49
+ const _DEFER_CLIENT = Symbol('appmesh.deferClient')
50
+
51
+ /**
52
+ * Worker SDK for App Mesh application interacting with the local App Mesh REST service over HTTPS.
15
53
  *
16
54
  * Build-in runtime environment variables required:
17
55
  * - APP_MESH_PROCESS_KEY
18
56
  * - APP_MESH_APPLICATION_NAME
19
57
  *
20
58
  * @example
21
- * // HTTP Server Example
22
- * import { AppMeshServer } from 'appmesh/server';
59
+ * // HTTP Worker Example
60
+ * import { AppMeshWorker } from 'appmesh/server';
23
61
  *
24
- * const server = new AppMeshServer();
25
- * const payload = await server.task_fetch();
62
+ * const worker = new AppMeshWorker();
63
+ * const payload = await worker.fetch_task();
26
64
  * const result = processPayload(payload);
27
- * await server.task_return(result);
65
+ * await worker.send_task_result(result);
28
66
  *
29
67
  * @example
30
- * // TCP Server Example
31
- * import { AppMeshServerTCP } from 'appmesh/server';
68
+ * // TCP Worker Example
69
+ * import { AppMeshWorkerTCP } from 'appmesh/server';
32
70
  *
33
- * const server = new AppMeshServerTCP();
71
+ * const worker = new AppMeshWorkerTCP();
34
72
  * try {
35
- * const payload = await server.task_fetch();
73
+ * const payload = await worker.fetch_task();
36
74
  * const result = processPayload(payload);
37
- * await server.task_return(result);
75
+ * await worker.send_task_result(result);
38
76
  * } finally {
39
- * server.close();
77
+ * worker.close();
40
78
  * }
41
79
  */
42
- class AppMeshServer {
80
+ class AppMeshWorker {
43
81
  /**
44
- * Initialize an App Mesh HTTP Server for interacting with the App Mesh service via secure HTTPS.
82
+ * Initialize an App Mesh HTTP Worker for interacting with the App Mesh service via secure HTTPS.
45
83
  *
46
84
  * @param {string} [baseURL='https://127.0.0.1:6060'] - The server's base URI
47
85
  * @param {Object} [sslConfig=null] - SSL configuration object
@@ -52,9 +90,9 @@ class AppMeshServer {
52
90
  * @param {Object} [options={}] - Additional options
53
91
  * @param {Object} [options.logger=console] - Logger instance
54
92
  * @param {AppMeshClient} [options.client] - Optional pre-built `AppMeshClient` instance to reuse.
55
- * When supplied, `baseURL` and `sslConfig` are ignored and the server shares the caller's
93
+ * When supplied, `baseURL` and `sslConfig` are ignored and the worker shares the caller's
56
94
  * client (and therefore its token-refresh state). Use this when a single process needs both
57
- * client (outbound calls like `AddApp`) and server (`task_fetch`/`task_return`) roles to
95
+ * client (outbound calls like `AddApp`) and worker (`fetch_task`/`send_task_result`) roles to
58
96
  * avoid two independent `/token/renew` loops fighting each other — the daemon blacklists
59
97
  * the previous token on every renew, so the slower refresher gets 401 "Token has been revoked".
60
98
  *
@@ -66,26 +104,39 @@ class AppMeshServer {
66
104
  * ca: fs.readFileSync('/opt/appmesh/ssl/ca.pem'),
67
105
  * rejectUnauthorized: true
68
106
  * };
69
- * const server = new AppMeshServer('https://127.0.0.1:6060', sslConfig);
107
+ * const worker = new AppMeshWorker('https://127.0.0.1:6060', sslConfig);
70
108
  *
71
109
  * @example
72
- * // Share a single client between client-role and server-role usage:
110
+ * // Share a single client between client-role and worker-role usage:
73
111
  * const client = new AppMeshClient('https://127.0.0.1:6060', sslConfig);
74
112
  * await client.login('user', 'pass');
75
- * const server = new AppMeshServer('https://127.0.0.1:6060', sslConfig, { client });
113
+ * const worker = new AppMeshWorker('https://127.0.0.1:6060', sslConfig, { client });
76
114
  */
77
115
  constructor (
78
116
  baseURL = 'https://127.0.0.1:6060',
79
117
  sslConfig = null,
80
118
  options = {}
81
119
  ) {
82
- if (options.client) {
120
+ if (options.client === _DEFER_CLIENT) {
121
+ // Subclass (AppMeshWorkerTCP) creates its own client lazily — do not build a
122
+ // throwaway HTTP client here.
123
+ this._client = null
124
+ } else if (options.client) {
83
125
  this._client = options.client
84
126
  } else {
85
127
  this._client = new AppMeshClient(baseURL, sslConfig)
86
- this._client.setAutoRefreshToken(false) // Server endpoints use APP_MESH_PROCESS_KEY; no JWT refresh needed.
128
+ this._client.set_auto_refresh_token(false) // Task endpoints use APP_MESH_PROCESS_KEY; no JWT refresh needed.
87
129
  }
88
130
  this._logger = options.logger || console
131
+ this._stopped = false
132
+ }
133
+
134
+ /**
135
+ * Request cancellation of the fetch_task retry loop (safe to call at any time);
136
+ * a pending fetch_task resolves `null` at its next retry iteration.
137
+ */
138
+ stop () {
139
+ this._stopped = true
89
140
  }
90
141
 
91
142
  /**
@@ -116,26 +167,31 @@ class AppMeshServer {
116
167
  * Fetch task data in the currently running App Mesh application process.
117
168
  *
118
169
  * Used by App Mesh application process to obtain the payload from App Mesh service
119
- * that a client pushed to it. This method will retry indefinitely until successful.
170
+ * that a client pushed to it. This method will retry indefinitely until successful
171
+ * or cancelled via {@link AppMeshWorker#stop}, in which case `null` is resolved.
120
172
  *
121
173
  * @async
122
- * @returns {Promise<string|Buffer>} The raw payload bytes/body provided by the invoking client
174
+ * @returns {Promise<string|Buffer|null>} The raw payload bytes/body provided by the
175
+ * invoking client, or `null` when the fetch loop was cancelled via stop()
176
+ * @throws {ProcessSupersededError} The daemon reported HTTP 412 — this process key was
177
+ * superseded by a newer process instance; the caller should stop serving (an app
178
+ * entry point typically catches this and exits).
123
179
  *
124
180
  * @example
125
- * const server = new AppMeshServer();
126
- * const payload = await server.task_fetch();
181
+ * const worker = new AppMeshWorker();
182
+ * const payload = await worker.fetch_task();
127
183
  * console.log('Received payload:', payload);
128
184
  */
129
- async task_fetch () {
185
+ async fetch_task () {
130
186
  const { processKey, appName } = this._getRuntimeEnv()
131
187
  const path = `/appmesh/app/${appName}/task`
132
188
 
133
189
  const RETRY_DELAY = 100
134
190
 
135
- while (true) {
191
+ while (!this._stopped) {
136
192
  const attemptStart = Date.now()
137
193
  try {
138
- const response = await this._client._request('get', path, null, {
194
+ const response = await this._client.request('get', path, null, {
139
195
  params: { process_key: processKey }
140
196
  })
141
197
 
@@ -143,16 +199,31 @@ class AppMeshServer {
143
199
  return response.data
144
200
  }
145
201
 
202
+ // request() throws on non-200; this branch only guards injected clients
203
+ // that resolve with non-200 responses.
146
204
  if (response.status === 412) {
147
- this._logger.error('Process key mismatch (412): this process has been superseded, exiting')
148
- process.exit(1)
205
+ throw new ProcessSupersededError(
206
+ 'Process key mismatch (412): this process has been superseded by a newer instance',
207
+ response.data
208
+ )
149
209
  }
150
210
 
151
211
  this._logger.warn(
152
- `task_fetch failed with status ${response.status}: ${response.data}, retrying...`
212
+ `fetch_task failed with status ${response.status}: ${response.data}, retrying...`
153
213
  )
154
214
  } catch (error) {
155
- this._logger.warn(`task_fetch error: ${error.message}, retrying...`)
215
+ // ProcessSupersededError fixes statusCode to 412, so this covers both the
216
+ // branch above and 412 AppMeshErrors thrown by request().
217
+ if (error.statusCode === 412) {
218
+ this._logger.error('Process key mismatch (412): this process has been superseded')
219
+ throw error instanceof ProcessSupersededError
220
+ ? error
221
+ : new ProcessSupersededError(
222
+ 'Process key mismatch (412): this process has been superseded by a newer instance',
223
+ error.responseData
224
+ )
225
+ }
226
+ this._logger.warn(`fetch_task error: ${error.message}, retrying...`)
156
227
  }
157
228
 
158
229
  const remainingDelay = RETRY_DELAY - (Date.now() - attemptStart)
@@ -160,6 +231,7 @@ class AppMeshServer {
160
231
  await this._sleep(remainingDelay)
161
232
  }
162
233
  }
234
+ return null // cancelled via stop()
163
235
  }
164
236
 
165
237
  /**
@@ -174,20 +246,27 @@ class AppMeshServer {
174
246
  * @throws {Error} If the task return fails
175
247
  *
176
248
  * @example
177
- * const server = new AppMeshServer();
249
+ * const worker = new AppMeshWorker();
178
250
  * const result = { status: 'success', data: processedData };
179
- * await server.task_return(JSON.stringify(result));
251
+ * await worker.send_task_result(JSON.stringify(result));
180
252
  */
181
- async task_return (result) {
253
+ async send_task_result (result) {
182
254
  const { processKey, appName } = this._getRuntimeEnv()
183
255
  const path = `/appmesh/app/${appName}/task`
184
256
 
185
- const response = await this._client._request('put', path, result, {
186
- params: { process_key: processKey }
187
- })
257
+ let response
258
+ try {
259
+ response = await this._client.request('put', path, result, {
260
+ params: { process_key: processKey }
261
+ })
262
+ } catch (error) {
263
+ // request() throws AppMeshError on non-200
264
+ this._logger.error(`send_task_result failed with status ${error.statusCode}: ${error.message}`)
265
+ throw error
266
+ }
188
267
 
189
268
  if (response.status !== 200) {
190
- const msg = `task_return failed with status ${response.status}: ${response.data}`
269
+ const msg = `send_task_result failed with status ${response.status}: ${response.data}`
191
270
  this._logger.error(msg)
192
271
  throw new Error(msg)
193
272
  }
@@ -205,41 +284,45 @@ class AppMeshServer {
205
284
  }
206
285
 
207
286
  /**
208
- * Server SDK for interacting with the local App Mesh service over TCP (TLS).
287
+ * Worker SDK for interacting with the local App Mesh service over TCP (TLS).
209
288
  *
210
- * This class extends AppMeshServer to use TCP transport instead of HTTP.
289
+ * This class extends AppMeshWorker to use TCP transport instead of HTTP.
211
290
  * Requires the AppMeshClientTCP implementation.
212
291
  *
213
- * @extends AppMeshServer
292
+ * @extends AppMeshWorker
214
293
  *
215
294
  * @example
216
- * import { AppMeshServerTCP } from 'appmesh/server';
295
+ * import { AppMeshWorkerTCP } from 'appmesh/server';
217
296
  *
218
- * const server = new AppMeshServerTCP();
297
+ * const worker = new AppMeshWorkerTCP();
219
298
  * try {
220
- * const payload = await server.task_fetch();
299
+ * const payload = await worker.fetch_task();
221
300
  * const result = await processPayload(payload);
222
- * await server.task_return(result);
301
+ * await worker.send_task_result(result);
223
302
  * } finally {
224
- * server.close();
303
+ * worker.close();
225
304
  * }
226
305
  */
227
- class AppMeshServerTCP extends AppMeshServer {
306
+ class AppMeshWorkerTCP extends AppMeshWorker {
228
307
  /**
229
- * Construct an App Mesh server TCP object to communicate securely with an App Mesh server over TLS.
308
+ * Construct an App Mesh worker TCP object to communicate securely with an App Mesh server over TLS.
309
+ *
310
+ * Like AppMeshClientTCP, the TCP address may be given as a legacy `[host, port]` array or an
311
+ * explicit `{host, port}` object, in either of the first two positions:
312
+ * `new AppMeshWorkerTCP(sslConfig, [host, port])` or `new AppMeshWorkerTCP({host, port}, sslConfig)`.
230
313
  *
231
- * @param {Object} [sslConfig=null] - SSL configuration
314
+ * @param {Object} [sslConfig=null] - SSL configuration (or `{host, port}` address)
232
315
  * @param {Buffer|string} [sslConfig.ca] - CA certificate for verification
233
316
  * @param {Buffer|string} [sslConfig.cert] - Client certificate
234
317
  * @param {Buffer|string} [sslConfig.key] - Client key
235
318
  * @param {boolean} [sslConfig.rejectUnauthorized=true] - Whether to verify SSL
236
- * @param {Array<string, number>} [tcpAddress=['127.0.0.1', 6059]] - TCP server address [host, port]
319
+ * @param {Array<string, number>|{host: string, port: number}} [tcpAddress=['127.0.0.1', 6059]] - TCP server address
237
320
  * @param {Object} [options={}] - Additional options
238
321
  * @param {Object} [options.logger=console] - Logger instance
239
322
  * @param {AppMeshClientTCP} [options.client] - Optional pre-built `AppMeshClientTCP` instance to reuse.
240
- * When supplied, `sslConfig` and `tcpAddress` are ignored and the server shares the caller's
323
+ * When supplied, `sslConfig` and `tcpAddress` are ignored and the worker shares the caller's
241
324
  * client (and therefore its token-refresh state). Use this when a single process needs both
242
- * client and server roles to avoid two independent `/token/renew` loops fighting each other —
325
+ * client and worker roles to avoid two independent `/token/renew` loops fighting each other —
243
326
  * the daemon blacklists the previous token on every renew, so the slower refresher gets
244
327
  * 401 "Token has been revoked".
245
328
  *
@@ -250,25 +333,31 @@ class AppMeshServerTCP extends AppMeshServer {
250
333
  * cert: fs.readFileSync('/opt/appmesh/ssl/client.pem'),
251
334
  * key: fs.readFileSync('/opt/appmesh/ssl/client-key.pem')
252
335
  * };
253
- * const server = new AppMeshServerTCP(sslConfig, ['127.0.0.1', 6059]);
336
+ * const worker = new AppMeshWorkerTCP(sslConfig, ['127.0.0.1', 6059]);
254
337
  *
255
338
  * @example
256
- * // Share a single TCP client between client-role and server-role usage:
257
- * import { AppMeshClientTCP } from 'appmesh/src/appmesh_tcp.js';
339
+ * // Share a single TCP client between client-role and worker-role usage:
340
+ * import { AppMeshClientTCP } from 'appmesh/tcp';
258
341
  * const client = new AppMeshClientTCP(sslConfig, ['127.0.0.1', 6059]);
259
342
  * await client.login('user', 'pass');
260
- * const server = new AppMeshServerTCP(sslConfig, ['127.0.0.1', 6059], { client });
343
+ * const worker = new AppMeshWorkerTCP(sslConfig, ['127.0.0.1', 6059], { client });
261
344
  */
262
345
  constructor (
263
346
  sslConfig = null,
264
347
  tcpAddress = ['127.0.0.1', 6059],
265
348
  options = {}
266
349
  ) {
267
- super(undefined, sslConfig)
268
- this._logger = options.logger || console
350
+ // Accept the address-first object form: new AppMeshWorkerTCP({host, port}, sslConfig)
351
+ if (_isTcpAddressObject(sslConfig)) {
352
+ const addr = sslConfig
353
+ sslConfig = (Array.isArray(tcpAddress) || _isTcpAddressObject(tcpAddress)) ? null : tcpAddress
354
+ tcpAddress = addr
355
+ }
356
+ // The TCP client is created lazily by _ensureClient(); the sentinel tells the
357
+ // parent not to construct a throwaway HTTP client.
358
+ super(undefined, sslConfig, { ...options, client: options.client || _DEFER_CLIENT })
269
359
  this._sslConfig = sslConfig
270
360
  this._tcpAddress = tcpAddress
271
- this._client = options.client ?? null
272
361
  }
273
362
 
274
363
  /**
@@ -279,7 +368,7 @@ class AppMeshServerTCP extends AppMeshServer {
279
368
  if (!this._client) {
280
369
  const { AppMeshClientTCP } = await import('./appmesh_tcp.js')
281
370
  this._client = new AppMeshClientTCP(this._sslConfig, this._tcpAddress)
282
- this._client.setAutoRefreshToken(false) // Server endpoints use APP_MESH_PROCESS_KEY; no JWT refresh needed.
371
+ this._client.set_auto_refresh_token(false) // Task endpoints use APP_MESH_PROCESS_KEY; no JWT refresh needed.
283
372
  }
284
373
  return this._client
285
374
  }
@@ -287,17 +376,17 @@ class AppMeshServerTCP extends AppMeshServer {
287
376
  /**
288
377
  * Fetch task data (overrides parent to ensure TCP client is initialized)
289
378
  */
290
- async task_fetch () {
379
+ async fetch_task () {
291
380
  await this._ensureClient()
292
- return super.task_fetch()
381
+ return super.fetch_task()
293
382
  }
294
383
 
295
384
  /**
296
385
  * Return task result (overrides parent to ensure TCP client is initialized)
297
386
  */
298
- async task_return (result) {
387
+ async send_task_result (result) {
299
388
  await this._ensureClient()
300
- return super.task_return(result)
389
+ return super.send_task_result(result)
301
390
  }
302
391
 
303
392
  /**
@@ -305,11 +394,11 @@ class AppMeshServerTCP extends AppMeshServer {
305
394
  * Should be called when done using the server.
306
395
  *
307
396
  * @example
308
- * const server = new AppMeshServerTCP();
397
+ * const worker = new AppMeshWorkerTCP();
309
398
  * try {
310
- * // Use server...
399
+ * // Use worker...
311
400
  * } finally {
312
- * server.close();
401
+ * worker.close();
313
402
  * }
314
403
  */
315
404
  close () {
@@ -320,22 +409,22 @@ class AppMeshServerTCP extends AppMeshServer {
320
409
  }
321
410
 
322
411
  /**
323
- * Context manager helper for AppMeshServer
412
+ * Context manager helper for AppMeshWorker
324
413
  * Provides automatic resource cleanup
325
414
  *
326
- * @param {Function} serverFactory - Function that creates a server instance
327
- * @param {Function} callback - Async function to execute with the server
415
+ * @param {Function} serverFactory - Function that creates a worker instance
416
+ * @param {Function} callback - Async function to execute with the worker
328
417
  * @returns {Promise<any>} Result from callback
329
418
  *
330
419
  * @example
331
- * import { withServer, AppMeshServerTCP } from 'appmesh/server';
420
+ * import { withServer, AppMeshWorkerTCP } from 'appmesh/server';
332
421
  *
333
422
  * await withServer(
334
- * () => new AppMeshServerTCP(),
335
- * async (server) => {
336
- * const payload = await server.task_fetch();
423
+ * () => new AppMeshWorkerTCP(),
424
+ * async (worker) => {
425
+ * const payload = await worker.fetch_task();
337
426
  * const result = process(payload);
338
- * await server.task_return(result);
427
+ * await worker.send_task_result(result);
339
428
  * }
340
429
  * );
341
430
  */
@@ -350,5 +439,10 @@ async function withServer (serverFactory, callback) {
350
439
  }
351
440
  }
352
441
 
353
- export { AppMeshServer, AppMeshServerTCP, withServer }
354
- export default AppMeshServer
442
+ export {
443
+ AppMeshWorker,
444
+ AppMeshWorkerTCP,
445
+ ProcessSupersededError,
446
+ withServer
447
+ }
448
+ export default AppMeshWorker