@3fv/electron-utility-process-manager 1.0.6 → 1.0.7

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 (51) hide show
  1. package/README.md +52 -2
  2. package/lib/cjs/index.js +17 -7
  3. package/lib/cjs/index.js.map +1 -1
  4. package/lib/cjs/main/UPMMainService.js +24 -12
  5. package/lib/cjs/main/UPMMainService.js.map +1 -1
  6. package/lib/cjs/main/UPMMainServiceManager.js +17 -7
  7. package/lib/cjs/main/UPMMainServiceManager.js.map +1 -1
  8. package/lib/cjs/node/UPMNodeProcess.js +17 -7
  9. package/lib/cjs/node/UPMNodeProcess.js.map +1 -1
  10. package/lib/cjs/package.json +1 -0
  11. package/lib/cjs/renderer/UPMRendererClientFactory.js +17 -7
  12. package/lib/cjs/renderer/UPMRendererClientFactory.js.map +1 -1
  13. package/lib/mjs/main/UPMMainService.js +5 -3
  14. package/lib/mjs/main/UPMMainService.js.map +1 -1
  15. package/lib/mjs/package.json +1 -0
  16. package/package.json +79 -41
  17. package/src/common/clients.ts +276 -0
  18. package/src/common/guards.ts +11 -0
  19. package/src/common/index.ts +3 -0
  20. package/src/common/types.ts +106 -0
  21. package/src/index.ts +8 -0
  22. package/src/main/UPMMainService.ts +454 -0
  23. package/src/main/UPMMainServiceManager.ts +178 -0
  24. package/src/main/index.ts +2 -0
  25. package/src/node/UPMNodeProcess.ts +164 -0
  26. package/src/node/index.ts +3 -0
  27. package/src/renderer/UPMRendererClientFactory.ts +57 -0
  28. package/src/renderer/index.ts +4 -0
  29. package/.prettierrc.js +0 -14
  30. package/examples/lib-examples/complex/complex-main.d.ts +0 -1
  31. package/examples/lib-examples/complex/complex-main.js +0 -108
  32. package/examples/lib-examples/complex/complex-main.js.map +0 -1
  33. package/examples/lib-examples/complex/complex-node.d.ts +0 -1
  34. package/examples/lib-examples/complex/complex-node.js +0 -17
  35. package/examples/lib-examples/complex/complex-node.js.map +0 -1
  36. package/examples/lib-examples/complex/complex-renderer.d.ts +0 -1
  37. package/examples/lib-examples/complex/complex-renderer.js +0 -18
  38. package/examples/lib-examples/complex/complex-renderer.js.map +0 -1
  39. package/examples/lib-examples/complex/complex-types.d.ts +0 -3
  40. package/examples/lib-examples/complex/complex-types.js +0 -3
  41. package/examples/lib-examples/complex/complex-types.js.map +0 -1
  42. package/examples/lib-examples/simple/simple-main.d.ts +0 -1
  43. package/examples/lib-examples/simple/simple-main.js +0 -113
  44. package/examples/lib-examples/simple/simple-main.js.map +0 -1
  45. package/examples/lib-examples/simple/simple-node.d.ts +0 -1
  46. package/examples/lib-examples/simple/simple-node.js +0 -17
  47. package/examples/lib-examples/simple/simple-node.js.map +0 -1
  48. package/examples/lib-examples/simple/simple-renderer.d.ts +0 -1
  49. package/examples/lib-examples/simple/simple-renderer.js +0 -17
  50. package/examples/lib-examples/simple/simple-renderer.js.map +0 -1
  51. package/jest.config.js +0 -20
@@ -0,0 +1,454 @@
1
+ import {
2
+ app, MessageChannelMain, MessagePortMain, utilityProcess, UtilityProcess
3
+ } from "electron"
4
+ import { assert, getValue, isNumber, isString } from "@3fv/guard"
5
+ import Fs from "fs"
6
+ import { Deferred } from "@3fv/deferred"
7
+ import * as UPM from "../common/index.js"
8
+ import defaults from "lodash/defaults.js"
9
+ import isEmpty from "lodash/isEmpty.js"
10
+ import negate from "lodash/negate.js"
11
+ import Tracer from "tracer"
12
+ import { asOption } from "@3fv/prelude-ts"
13
+ import { match } from "ts-pattern"
14
+
15
+ const log = Tracer.colorConsole()
16
+ const isNotEmpty = negate(isEmpty)
17
+
18
+ /**
19
+ * Contains & Manages a `Utility/Process`
20
+ * providing simplified messaging & event
21
+ * propagation.
22
+ */
23
+ export class UPMMainService<ReqMap extends UPM.MessageRequestMap = any, MType extends UPM.MessageRequestNames<ReqMap> = UPM.MessageRequestNames<ReqMap>> implements UPM.IMessageClient<ReqMap, MType> {
24
+
25
+ /**
26
+ * As this container/wrapper implements
27
+ * `IMessageClient`, it needs to provide
28
+ * a `clientId`, which is constant in the
29
+ * main process
30
+ */
31
+ readonly clientId:string = "main"
32
+
33
+ /**
34
+ * Send an event as `fire-and-forget`
35
+ *
36
+ * @param data
37
+ * @param port
38
+ */
39
+ sendEvent(data:any, port:UPM.Port = this.childProcess_):void {
40
+ const messageId = this.generateMessageId()
41
+ const payload:UPM.NodeEnvelope = {
42
+ channel: UPM.IPCChannel.UPMServiceMessage,
43
+ payload: { messageId, eventData: data, kind: UPM.MessageKind.Event }
44
+ }
45
+
46
+ port.postMessage(payload)
47
+ }
48
+
49
+ /**
50
+ * Send request
51
+ *
52
+ * @param type
53
+ * @param args
54
+ */
55
+ async executeRequest<Type extends MType, R extends UPM.MessageRequestReturnType<ReqMap, Type> = UPM.MessageRequestReturnType<ReqMap, Type>>(
56
+ type:Type,
57
+ ...args:UPM.MessageRequestParams<ReqMap, Type>
58
+ ):Promise<R> {
59
+ assert(!!this.childProcess_, "The process is not running")
60
+ const messageId = this.generateMessageId(),
61
+ pending:UPM.PendingRequestMessage<ReqMap, Type, R> = {
62
+ deferred: new Deferred<R>(),
63
+ messageId,
64
+ timeoutId: setTimeout(
65
+ () => this.removePendingMessage(messageId),
66
+ UPM.Defaults.RequestTimeout
67
+ )
68
+ }
69
+
70
+ this.pendingMessages_.set(messageId, pending)
71
+ const payload:UPM.NodeMessage<ReqMap, Type> = {
72
+ channel: UPM.IPCChannel.UPMServiceMessage,
73
+ payload: { type, messageId, args, kind: UPM.MessageKind.Request }
74
+ }
75
+
76
+ this.childProcess_.postMessage(payload)
77
+
78
+ try {
79
+ const result = await pending.deferred.promise
80
+ log.debug(`Completed utility request (messageId=${messageId})`)
81
+ return result
82
+ } catch (err) {
83
+ log.error(`Failed utility request (messageId=${messageId})`, err)
84
+ throw err
85
+ }
86
+ }
87
+
88
+ close():void {
89
+ log.warn("Close has no effect when called via the main process")
90
+ }
91
+
92
+ /**
93
+ * Resolves when ready
94
+ *
95
+ * @returns {any}
96
+ */
97
+ whenReady():Promise<UPM.IMessageClient<ReqMap, MType>> {
98
+ return this.init()
99
+ }
100
+
101
+ /**
102
+ * Return a simple proxy facade wrapping
103
+ * `executeRequest` into individual methods
104
+ */
105
+ getServiceClient():UPM.IServiceClient<ReqMap, MType> {
106
+ return new Proxy<UPM.IServiceClient<ReqMap, MType>>({} as any, {
107
+ get: (_target, prop) => {
108
+ if (prop === "then") {
109
+ return undefined
110
+ }
111
+ return (...args:any[]) => this.whenReady()
112
+ .then(client => client.executeRequest(
113
+ prop as MType,
114
+ ...args as any
115
+ ))
116
+
117
+ }
118
+ })
119
+ }
120
+
121
+ private readonly config_:UPM.CreateServiceConfig
122
+
123
+ private readyDeferred_:Deferred<this> = null
124
+
125
+ private killDeferred_:Deferred<number> = null
126
+
127
+ private messageIdSeq_ = 0
128
+
129
+ private childProcess_:UtilityProcess = null
130
+
131
+ private pendingMessages_ = new Map<number, UPM.PendingRequestMessage>()
132
+
133
+ private messageChannels_ = new Map<string, MessageChannelMain>()
134
+
135
+ private onServiceExit(code:number) {
136
+ log.warn(`Service Exit (code=${code})`)
137
+ let deferred = this.killDeferred_
138
+ if (deferred) {
139
+ if (!deferred.isSettled()) {
140
+ deferred.resolve(code)
141
+ } else {
142
+ log.warn(`kill deferred is already settled (code=${code})`)
143
+ }
144
+ return
145
+ }
146
+ this.killDeferred_ = Deferred.resolve(code)
147
+
148
+ this.cancelAllPendingMessages()
149
+ this.removeAllMessageChannels()
150
+ }
151
+
152
+ /**
153
+ * Returned client side port (for Renderer)
154
+ * @param clientId
155
+ */
156
+ createMessageChannel(clientId:string):MessagePortMain {
157
+ assert(
158
+ this.readyDeferred_?.isFulfilled() ?? false,
159
+ `Service (${this.serviceName}) is not fulfilled or has an error`
160
+ )
161
+ const channel = new MessageChannelMain(), { port1, port2 } = channel
162
+
163
+ this.messageChannels_.set(clientId, channel)
164
+ try {
165
+ this.childProcess_.postMessage({
166
+ message: {
167
+ channel: UPM.IPCChannel.UPMServiceNewClient, payload: { clientId }
168
+ }
169
+ }, [port1])
170
+ } catch (err) {
171
+ log.error(`Unable to create channel id=${clientId}`, err)
172
+ this.closeMessageChannel(clientId, channel)
173
+ throw err
174
+ }
175
+ return port2
176
+ }
177
+
178
+ /**
179
+ * Create a port client for the main process
180
+ * (useful to avoid congestion on default child_process channel)
181
+ *
182
+ * @param {string} clientId
183
+ * @returns {UPM.MessagePortClient}
184
+ */
185
+ createMainClient(clientId:string):UPM.MessagePortClient<ReqMap, MType> {
186
+ return new UPM.MessagePortClient(
187
+ clientId,
188
+ this.createMessageChannel(clientId)
189
+ )
190
+ }
191
+
192
+ /**
193
+ * Service is still in a healthy/running/runnable state
194
+ */
195
+ get isRunning() {
196
+ return !this.killDeferred_
197
+ }
198
+
199
+ /**
200
+ * Stop the service (kill)
201
+ */
202
+ async stop():Promise<void> {
203
+ await this.kill()
204
+ }
205
+
206
+ /**
207
+ * Resource management & cleanup
208
+ */
209
+ async [Symbol.asyncDispose]() {
210
+ await this.stop()
211
+ }
212
+
213
+ /**
214
+ * Initialize the service
215
+ *
216
+ * @private
217
+ */
218
+ private async init():Promise<this> {
219
+ let deferred = this.readyDeferred_
220
+ if (deferred) {
221
+ return deferred.promise
222
+ }
223
+ deferred = this.readyDeferred_ = new Deferred()
224
+
225
+ try {
226
+ const exitListener = (code:number) => {
227
+ if (code !== 0) {
228
+ deferred.reject(new Error(`process exited with code ${code} upon starting`))
229
+ }
230
+ }
231
+ const spawnListener = async () => {
232
+ this.childProcess_.off("exit", exitListener)
233
+ this.childProcess_.on("exit", this.onServiceExit.bind(this))
234
+ this.childProcess_.on("message", this.onMessage.bind(this))
235
+ deferred.resolve(this)
236
+ }
237
+
238
+ // BUILD ADDITIONAL FORK OPTIONS
239
+ const childProcessOptions = asOption(this.config_.inspect)
240
+ .filter(it => it !== false)
241
+ .map(it => (
242
+ {
243
+ execArgv: [
244
+ match(it as UPM.ServiceInspectConfig)
245
+ .when(isNumber, port => `--inspect=${port}`)
246
+ .otherwise(({ port, break: shouldBreak }) => {
247
+ const ext = shouldBreak ? "-brk" : ""
248
+ return `--inspect${ext}=${port}`
249
+ })
250
+ ]
251
+ }
252
+ ))
253
+ .getOrElse({} as any)
254
+
255
+ // FORK
256
+ this.childProcess_ = utilityProcess
257
+ .fork(this.entryFile, [], {
258
+ ...childProcessOptions, serviceName: this.serviceName, env: {
259
+ ...process.env, IN_CHILD_PROCESS: "true", APP_NAME: app.name
260
+ }
261
+ })
262
+ .on("spawn", spawnListener)
263
+ .on("exit", exitListener)
264
+
265
+ await deferred.promise
266
+ } catch (err) {
267
+ deferred.reject(err)
268
+ }
269
+
270
+ return deferred.promise
271
+ }
272
+
273
+ /**
274
+ * OnMessage really only handles `results` from
275
+ * `executeRequest(...)`; this may be expanded in
276
+ * the future
277
+ *
278
+ * @param payload
279
+ * @private
280
+ */
281
+ private onMessage<Type extends MType = any>(payload:UPM.Message<ReqMap, Type>) {
282
+ const { messageId, result, error } = payload
283
+
284
+ try {
285
+ const pending = this.pendingMessages_.get(messageId)
286
+ if (!pending) {
287
+ log.info(`Unable to find pending record ${messageId}`)
288
+ return
289
+ }
290
+
291
+ if (pending.deferred.isSettled()) {
292
+ log.error(`Already settled record ${messageId}`)
293
+ return
294
+ }
295
+
296
+ this.removePendingMessage(messageId)
297
+ if (!!error) {
298
+ log.error(error)
299
+ pending.deferred.reject(new Error(error))
300
+ return
301
+ }
302
+ pending.deferred.resolve(result)
303
+ } catch (err) {
304
+ log.error(`Failed to handle message`, err)
305
+ }
306
+ }
307
+
308
+ /**
309
+ * This handler is currently unused, but
310
+ * could be attached to a `MessagePort.on('close')`
311
+ * event
312
+ *
313
+ * @param id
314
+ * @param channel
315
+ * @private
316
+ */
317
+ // noinspection JSUnusedLocalSymbols
318
+ private onPortClose(id:string, channel:MessageChannelMain) {
319
+ log.info(`main side of message channel(id=${id}) closed`)
320
+ this.closeMessageChannel(id, channel)
321
+ }
322
+
323
+ /**
324
+ * Close a message channel
325
+ *
326
+ * @param id
327
+ * @param channel
328
+ * @private
329
+ */
330
+ private closeMessageChannel(id:string, channel:MessageChannelMain) {
331
+ if (this.messageChannels_.has(id)) {
332
+ this.messageChannels_.delete(id)
333
+ }
334
+
335
+ Array(channel.port1, channel.port2)
336
+ .forEach(p => {
337
+ getValue(() => p.close())
338
+ })
339
+ }
340
+
341
+ /**
342
+ * Kill the service
343
+ *
344
+ * @private
345
+ */
346
+ private async kill():Promise<number> {
347
+ if (!this.readyDeferred_ ||
348
+ !this.readyDeferred_.isSettled() ||
349
+ !this.childProcess_ ||
350
+ this.killDeferred_) {
351
+ log.error("Service is not ready OR the process is not running")
352
+ // noinspection ES6MissingAwait
353
+ return this.killDeferred_?.promise ?? Promise.resolve<number>(0)
354
+ }
355
+
356
+ let killDeferred = this.killDeferred_ = new Deferred<number>()
357
+
358
+ // GET THE PROCESS
359
+ const process = this.childProcess_
360
+
361
+ // CANCEL ALL PENDING MESSAGES & REMOVE ALL CHANNELS
362
+ this.cancelAllPendingMessages()
363
+ this.removeAllMessageChannels()
364
+
365
+ try {
366
+ process.kill()
367
+ } catch (err) {
368
+ log.error("Unable to kill utility process", err)
369
+ killDeferred.reject(err)
370
+ }
371
+
372
+ return await killDeferred.promise
373
+ }
374
+
375
+ /**
376
+ * Close all message channels
377
+ *
378
+ * @private
379
+ */
380
+ private removeAllMessageChannels() {
381
+ for (const [id, channel] of [...this.messageChannels_.entries()]) {
382
+ this.closeMessageChannel(id, channel)
383
+ }
384
+ }
385
+
386
+ /**
387
+ * Remove all pending messages
388
+ *
389
+ * @private
390
+ */
391
+ private cancelAllPendingMessages():void {
392
+ for (const key of [...this.pendingMessages_.keys()]) {
393
+ this.removePendingMessage(key)
394
+ }
395
+ }
396
+
397
+ /**
398
+ * Remove a pending message from the queue,
399
+ * whether settled or not
400
+ *
401
+ * @param messageId
402
+ * @private
403
+ */
404
+ private removePendingMessage(messageId:number):void {
405
+ const pending = this.pendingMessages_.get(messageId)
406
+ if (!pending) {
407
+ return
408
+ }
409
+ clearTimeout(pending.timeoutId)
410
+ if (!pending.deferred.isSettled()) {
411
+ pending.deferred.reject(
412
+ "The pending message is being removed before promise is settled")
413
+ }
414
+ this.pendingMessages_.delete(messageId)
415
+ }
416
+
417
+ /**
418
+ * Generate next message id
419
+ *
420
+ * @private
421
+ */
422
+ private generateMessageId():number {
423
+ return this.messageIdSeq_++
424
+ }
425
+
426
+ /**
427
+ * Construct a new service
428
+ *
429
+ * @param serviceName
430
+ * @param entryFile
431
+ * @param options
432
+ */
433
+ constructor(
434
+ readonly serviceName:string,
435
+ readonly entryFile:string,
436
+ options:UPM.CreateServiceOptions = {}
437
+ ) {
438
+ assert(isString(entryFile) &&
439
+ isNotEmpty(entryFile) &&
440
+ Fs.existsSync(entryFile), `entry file must be provided (${entryFile})`)
441
+
442
+ this.config_ = defaults({
443
+ ...options
444
+ }, {
445
+ serviceName, inspect: false
446
+ })
447
+
448
+ this.init().catch(err => {
449
+ log.error(`Failed to init utility process`, err)
450
+ })
451
+ }
452
+ }
453
+
454
+ export default UPMMainService
@@ -0,0 +1,178 @@
1
+ import { ipcMain } from "electron"
2
+ import { assert, isString } from "@3fv/guard"
3
+ import { Future } from "@3fv/prelude-ts"
4
+ import * as UPM from "../common/index.js"
5
+ import UPMMainService from "./UPMMainService.js"
6
+
7
+ import Tracer from "tracer"
8
+
9
+ const log = Tracer.colorConsole()
10
+
11
+
12
+
13
+ export class UPMMainServiceManager {
14
+ private services_ = new Map<string, UPMMainService<any,any>>()
15
+
16
+ /**
17
+ * On new client request from renderer
18
+ *
19
+ * @param ev
20
+ * @param serviceName
21
+ * @param clientId
22
+ * @private
23
+ */
24
+ private onNewClient(ev: Electron.IpcMainEvent, serviceName: string, clientId: string) {
25
+ const sender = ev.sender
26
+ if (!sender) {
27
+ log.error(`sender is invalid on new client ipc message`, ev)
28
+ return
29
+ }
30
+
31
+ const proc = this.getService(serviceName)
32
+ if (!proc) {
33
+ log.error(`no proc found for ${serviceName}`, ev)
34
+ return
35
+ }
36
+ try {
37
+ const clientPort = proc.createMessageChannel(clientId)
38
+ log.info(`Received client port for proc (id=${serviceName},clientId=${clientId})`)
39
+ sender.postMessage(
40
+ UPM.IPCChannel.UPMServiceNewClient,
41
+ {serviceName, clientId},
42
+ [clientPort]
43
+ )
44
+ } catch (err) {
45
+ log.error(`failed to register new client`, err)
46
+ }
47
+ }
48
+
49
+ createMainChannel
50
+ <
51
+ Args extends UPM.MessageRequestMap = any,
52
+ MType extends UPM.MessageRequestNames<Args> = UPM.MessageRequestNames<Args>,
53
+ >
54
+ (serviceOrName: string | UPMMainService<Args, MType>, clientId: string): UPM.Port {
55
+ try {
56
+ const service = isString(serviceOrName) ? this.getService<Args,MType>(serviceOrName) : serviceOrName
57
+
58
+ const clientPort = service.createMessageChannel(clientId)
59
+ log.info(`Received client port for proc (id=${service.serviceName},clientId=${clientId})`)
60
+ return clientPort
61
+ } catch (err) {
62
+ log.error(`failed to register new client`, err)
63
+ throw err
64
+ }
65
+ }
66
+
67
+ createMainClient<
68
+ ReqMap extends UPM.MessageRequestMap = any,
69
+ MType extends UPM.MessageRequestNames<ReqMap> = UPM.MessageRequestNames<ReqMap>,
70
+ >(serviceName: string, clientId: string) {
71
+ const service = this.getService<ReqMap,MType>(serviceName)
72
+ assert(!!service,`no proc found for ${serviceName}`)
73
+
74
+ try {
75
+ return service.createMainClient(clientId)
76
+ } catch (err) {
77
+ log.error(`failed to register new client`, err)
78
+ throw err
79
+ }
80
+ }
81
+
82
+ /**
83
+ * Destroy all services
84
+ *
85
+ * @private
86
+ */
87
+ private async destroy() {
88
+ for (const [id] of [...this.services_.keys()]) {
89
+ await this.destroyService(id)
90
+ }
91
+ }
92
+
93
+ /**
94
+ * Get the service for a given name
95
+ *
96
+ * @param serviceName
97
+ */
98
+ getService<
99
+ ReqMap extends UPM.MessageRequestMap,
100
+ Type extends UPM.MessageRequestNames<ReqMap> = UPM.MessageRequestNames<ReqMap>
101
+ >(serviceName: string): UPMMainService<ReqMap, Type> {
102
+ return this.services_.has(serviceName) ? this.services_.get(serviceName) : null
103
+ }
104
+
105
+ /**
106
+ * Create a utility process/service
107
+ *
108
+ * @param serviceName
109
+ * @param entryFile
110
+ * @param options
111
+ */
112
+ async createService<
113
+ ReqMap extends UPM.MessageRequestMap,
114
+ Type extends UPM.MessageRequestNames<ReqMap> = UPM.MessageRequestNames<ReqMap>
115
+ >(serviceName: string, entryFile: string, options: UPM.CreateServiceOptions = {}): Promise<UPMMainService<ReqMap,Type>> {
116
+ assert(!this.services_.has(serviceName), `utility process with id (${serviceName}) is already registered`)
117
+ const proc = new UPMMainService(serviceName, entryFile, {
118
+ ...options,
119
+ serviceName: options.serviceName ?? serviceName,
120
+ })
121
+
122
+ this.services_.set(serviceName, proc)
123
+
124
+ try {
125
+ await proc.whenReady()
126
+ return proc
127
+ } catch (err) {
128
+ await this.destroyService(serviceName)
129
+ throw err
130
+ }
131
+ }
132
+
133
+ /**
134
+ * Destroy a specific service
135
+ *
136
+ * @param serviceName
137
+ */
138
+ async destroyService(serviceName: string) {
139
+ if (!this.services_.has(serviceName)) return
140
+
141
+ const proc = this.services_.get(serviceName)
142
+ this.services_.delete(serviceName)
143
+ await proc.stop()
144
+ }
145
+
146
+ /**
147
+ * Async dispose symbol
148
+ */
149
+ async [Symbol.asyncDispose]() {
150
+ await this.destroy()
151
+ }
152
+
153
+ private constructor() {
154
+ ipcMain.on(UPM.IPCChannel.UPMServiceNewClient, this.onNewClient.bind(this))
155
+ }
156
+
157
+ /**
158
+ * Singleton instance
159
+ *
160
+ * @private
161
+ */
162
+ private static sInstance_: UPMMainServiceManager = null
163
+
164
+ /**
165
+ * Get singleton instance
166
+ */
167
+ static get(): UPMMainServiceManager {
168
+ if (!this.sInstance_) {
169
+ this.sInstance_ = new UPMMainServiceManager()
170
+ }
171
+
172
+ return this.sInstance_
173
+ }
174
+ }
175
+
176
+ export const upmMainServiceManager = UPMMainServiceManager.get()
177
+
178
+ export default upmMainServiceManager
@@ -0,0 +1,2 @@
1
+ export * from "./UPMMainServiceManager.js"
2
+ export * from "./UPMMainService.js"