@kabacorp/kaba-electron-rpc 7.1.2 → 8.1.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/.nvmrc ADDED
@@ -0,0 +1 @@
1
+ 14.21.3
package/README.md CHANGED
@@ -135,4 +135,4 @@ var rpcClient = rpc.importAPI('error-api', manifest, {
135
135
  errors: {MyCustomError} // pass in custom error constructors
136
136
  })
137
137
  rpcClient.testThrow().catch(console.log) // => MyCustomError
138
- ```
138
+ ```
package/index.js CHANGED
@@ -1,2 +1,2 @@
1
1
  module.exports.exportAPI = require('./lib/export-api')
2
- module.exports.importAPI = require('./lib/import-api')
2
+ module.exports.importAPI = require('./lib/import-api')
package/lib/export-api.js CHANGED
@@ -6,12 +6,8 @@ const {
6
6
  isRenderer,
7
7
  } = require("./util");
8
8
 
9
- function removeCircular(ref) {
10
- for (let i in ref) {
11
- if (ref[i] === ref) delete ref[i];
12
- else if (typeof ref[i] == "Object") removeCircular(ref[i]);
13
- }
14
- }
9
+ // Persistent map to avoid leaking cleanup functions
10
+ const cleanupRoutines = new Map();
15
11
 
16
12
  module.exports = function (
17
13
  channelName,
@@ -19,54 +15,72 @@ module.exports = function (
19
15
  methods,
20
16
  globalPermissionCheck
21
17
  ) {
22
- var api = new EventEmitter();
23
- var webcontentsStreams = {};
18
+ const api = new EventEmitter();
19
+ const webcontentsStreams = {};
20
+
21
+ // Internal Debugging helper
22
+ const debug = (...args) => {
23
+ if (process.env.RPC_DEBUG) {
24
+ console.log(`[RPC-EXPORT:${channelName}]`, ...args);
25
+ }
26
+ };
24
27
 
25
- var channel;
28
+ let channel;
26
29
  if (isNodeProcess()) {
27
- var mockedSender = new EventEmitter();
30
+ const mockedSender = new EventEmitter();
28
31
  mockedSender.id = "main-process";
32
+ mockedSender.isDestroyed = () => false;
29
33
  mockedSender.send = (channelName, msgType, requestId, ...args) => {
30
- // console.log('export-api#send', {channelName, msgType, requestId, args})
34
+ debug("send (node-process)", { msgType, requestId });
31
35
  process.send({ channelName, msgType, requestId, args });
32
36
  };
33
37
  channel = {
34
38
  onMessage(cb) {
35
39
  process.on("message", (msg) => {
36
- if (msg.channelName !== channelName) return;
37
- let mockedEvent = { sender: mockedSender };
38
- // console.log('export-api#onMessage', msg)
39
- cb(mockedEvent, msg.methodName, msg.requestId, ...msg.args);
40
+ if (msg && msg.channelName === channelName) {
41
+ debug("onMessage (node-process)", msg.methodName, msg.requestId);
42
+ const mockedEvent = { sender: mockedSender };
43
+ cb(mockedEvent, msg.methodName, msg.requestId, ...msg.args);
44
+ }
40
45
  });
41
46
  },
42
47
  };
43
48
  } else if (isRenderer()) {
44
- var { ipcRenderer } = require("electron");
49
+ const { ipcRenderer } = require("electron");
45
50
  channel = {
46
51
  onMessage(cb) {
47
- ipcRenderer.on(channelName, cb);
52
+ ipcRenderer.on(channelName, (event, methodName, requestId, ...args) => {
53
+ debug("onMessage (renderer)", methodName, requestId);
54
+ cb(event, methodName, requestId, ...args);
55
+ });
48
56
  },
49
57
  };
50
58
  } else {
51
- var { ipcMain } = require("electron");
59
+ const { ipcMain } = require("electron");
52
60
  channel = {
53
61
  onMessage(cb) {
54
- ipcMain.on(channelName, cb);
62
+ ipcMain.on(channelName, (event, methodName, requestId, ...args) => {
63
+ debug("onMessage (main)", methodName, requestId);
64
+ cb(event, methodName, requestId, ...args);
65
+ });
55
66
  },
56
67
  };
57
68
  }
58
69
 
59
70
  // wire up handler
60
71
  channel.onMessage(async function (event, methodName, requestId, ...args) {
61
- // console.log('received', channelName, methodName, requestId, ...args)
62
72
  args = args.map(IPCValueToValue);
63
73
 
64
74
  // watch for a navigation event
65
- var hasNavigated = false;
66
- function onDidNavigate() {
75
+ let hasNavigated = false;
76
+ const onDidNavigate = () => {
77
+ debug("navigation detected", { requestId, methodName });
67
78
  hasNavigated = true;
79
+ };
80
+
81
+ if (event.sender && !event.sender.isDestroyed()) {
82
+ event.sender.once("did-navigate", onDidNavigate);
68
83
  }
69
- event.sender.on("did-navigate", onDidNavigate);
70
84
 
71
85
  // helper to send
72
86
  const send = function (
@@ -75,48 +89,84 @@ module.exports = function (
75
89
  value,
76
90
  keepListeningForDidNavigate = false
77
91
  ) {
78
- if (event.sender.isDestroyed && event.sender.isDestroyed()) return; // dont send response if destroyed
79
- if (!keepListeningForDidNavigate)
92
+ if (
93
+ !event.sender ||
94
+ (event.sender.isDestroyed && event.sender.isDestroyed())
95
+ ) {
96
+ debug("send aborted: sender destroyed", { requestId, msgType });
97
+ return;
98
+ }
99
+
100
+ if (!keepListeningForDidNavigate) {
80
101
  event.sender.removeListener("did-navigate", onDidNavigate);
81
- if (hasNavigated) return; // dont send response if the page changed
102
+ }
82
103
 
83
- if (!value) {
84
- console.log("sending", channelName, msgType, requestId, err, value);
85
- value = err;
86
- } else {
87
- // console.log("sending", channelName, msgType, requestId, err, value);
104
+ if (hasNavigated) {
105
+ debug("send aborted: navigated away", { requestId, msgType });
106
+ return;
88
107
  }
89
- //
90
- if (event.reply) {
91
- event.reply(channelName, msgType, requestId, err, value);
92
- } else if (event.sender && event.sender.send) {
93
- event.sender.send(channelName, msgType, requestId, err, value);
108
+
109
+ const target = event.reply ? event : event.sender;
110
+
111
+ try {
112
+ target.send(channelName, msgType, requestId, err, value);
113
+ } catch (serializationError) {
114
+ debug("serialization failed, attempting sanitization", {
115
+ requestId,
116
+ methodName,
117
+ });
118
+
119
+ let sanitizedValue = value;
120
+ if (value && typeof value === "object") {
121
+ if (typeof value.toJSON === "function") {
122
+ sanitizedValue = value.toJSON();
123
+ } else {
124
+ try {
125
+ sanitizedValue = JSON.parse(JSON.stringify(value));
126
+ } catch (e) {
127
+ console.error(
128
+ `[RPC ERROR] Circular/Complex object at "${methodName}":`,
129
+ e
130
+ );
131
+ sanitizedValue = {
132
+ id: value.id,
133
+ error:
134
+ "Object too complex to serialize: " +
135
+ serializationError.message,
136
+ stack: serializationError.stack,
137
+ };
138
+ }
139
+ }
140
+ }
141
+ target.send(channelName, msgType, requestId, err, sanitizedValue);
94
142
  }
95
143
  };
96
144
 
97
145
  // handle special methods
98
- if (methodName == "stream-request-write") {
146
+ if (methodName === "stream-request-write") {
99
147
  event.returnValue = true;
100
148
  return streamRequestWrite(event.sender.id, requestId, args);
101
149
  }
102
- if (methodName == "stream-request-end") {
150
+ if (methodName === "stream-request-end") {
103
151
  event.returnValue = true;
104
152
  return streamRequestEnd(event.sender.id, requestId, args);
105
153
  }
106
- if (methodName == "stream-request-close") {
154
+ if (methodName === "stream-request-close") {
107
155
  event.returnValue = true;
108
156
  return streamRequestClose(event.sender.id, requestId, args);
109
157
  }
110
158
 
111
159
  // look up the method called
112
- var type = manifest[methodName];
113
- var method = methods[methodName];
160
+ const type = manifest[methodName];
161
+ const method = methods[methodName];
162
+
114
163
  if (!type || !method) {
115
- api.emit(
116
- "error",
117
- new Error(`Method not found: "${methodName}"`),
118
- arguments
119
- );
164
+ const err = new Error(`Method not found: "${methodName}"`);
165
+ debug("error: method not found", methodName);
166
+ api.emit("error", err, { methodName, requestId, args });
167
+ // If async/promise, we should probably let the caller know
168
+ if (type === "async" || type === "promise")
169
+ send("async-reply", errorObject(err));
120
170
  return;
121
171
  }
122
172
 
@@ -125,8 +175,8 @@ module.exports = function (
125
175
  globalPermissionCheck &&
126
176
  !globalPermissionCheck(event, methodName, args)
127
177
  ) {
128
- // repond according to method type
129
- if (type == "async" || type == "promise") {
178
+ debug("permission denied", methodName);
179
+ if (type === "async" || type === "promise") {
130
180
  send("async-reply", "Method Access Denied");
131
181
  } else {
132
182
  event.returnValue = { error: "Method Access Denied" };
@@ -135,49 +185,55 @@ module.exports = function (
135
185
  }
136
186
 
137
187
  // run method by type
138
- if (type == "sync") {
139
- // call sync
188
+ if (type === "sync") {
140
189
  try {
141
190
  event.returnValue = {
142
191
  success: valueToIPCValue(method.apply(event, args)),
143
192
  };
144
193
  } catch (e) {
194
+ debug("sync method error", methodName, e.message);
145
195
  event.returnValue = { error: e.message };
146
196
  }
147
197
  return;
148
198
  }
149
- if (type == "async") {
150
- // create a reply cb
199
+
200
+ if (type === "async") {
151
201
  const replyCb = (err, value) => {
152
202
  if (err) err = errorObject(err);
153
203
  send("async-reply", err, valueToIPCValue(value));
154
204
  };
155
205
  args.push(replyCb);
156
-
157
- // call async
158
- method.apply(event, args);
206
+ try {
207
+ method.apply(event, args);
208
+ } catch (e) {
209
+ debug("async method throw", methodName, e.message);
210
+ send("async-reply", errorObject(e));
211
+ }
159
212
  return;
160
213
  }
161
- if (type == "promise") {
162
- // call promise
214
+
215
+ if (type === "promise") {
163
216
  let p;
164
217
  try {
165
218
  p = method.apply(event, args);
166
- if (typeof p === "undefined") p = Promise.resolve();
167
- if (typeof p.then === "undefined") p = Promise.resolve(p);
219
+ if (!(p instanceof Promise) && (!p || typeof p.then !== "function")) {
220
+ p = Promise.resolve(p);
221
+ }
168
222
  } catch (e) {
169
- p = Promise.reject(errorObject(e));
223
+ p = Promise.reject(e);
170
224
  }
171
225
 
172
- // handle response
173
226
  p.then(
174
227
  (value) => send("async-reply", null, valueToIPCValue(value)),
175
- (error) => send("async-reply", errorObject(error))
228
+ (error) => {
229
+ debug("promise rejected", methodName, error?.message);
230
+ send("async-reply", errorObject(error));
231
+ }
176
232
  );
177
233
  return;
178
234
  }
179
235
 
180
- var streamTypes = {
236
+ const streamTypes = {
181
237
  readable: createReadableEvents,
182
238
  writable: createWritableEvents,
183
239
  duplex: createDuplexEvents,
@@ -194,11 +250,11 @@ module.exports = function (
194
250
  );
195
251
  }
196
252
 
197
- api.emit(
198
- "error",
199
- new Error(`Invalid method type "${type}" for "${methodName}"`),
200
- arguments
253
+ const typeErr = new Error(
254
+ `Invalid method type "${type}" for "${methodName}"`
201
255
  );
256
+ debug("error: invalid type", type);
257
+ api.emit("error", typeErr, { methodName, type, requestId });
202
258
  });
203
259
 
204
260
  async function handleStream(
@@ -209,45 +265,32 @@ module.exports = function (
209
265
  createStreamEvents,
210
266
  send
211
267
  ) {
212
- // call duplex
213
268
  let stream;
214
- let error;
215
269
  try {
216
270
  stream = method.apply(event, args);
271
+ if (stream && typeof stream.then === "function") stream = await stream;
217
272
  if (!stream) {
218
- send("stream-error", "Empty stream response");
219
- return;
273
+ debug("stream-error: method returned null", requestId);
274
+ return send("stream-error", "Empty stream response");
220
275
  }
221
276
  } catch (e) {
222
- send("stream-error", "" + e);
223
- return;
224
- }
225
-
226
- // handle promises
227
- if (stream.then) {
228
- try {
229
- stream = await stream; // wait for it
230
- } catch (e) {
231
- send("stream-error", "" + e);
232
- return;
233
- }
277
+ debug("stream instantiation error", e.message);
278
+ return send("stream-error", "" + e);
234
279
  }
235
280
 
236
281
  trackWebcontentsStreams(event.sender, requestId, stream);
237
- var events = createStreamEvents(event, stream, requestId, send);
282
+ const events = createStreamEvents(event, stream, requestId, send);
238
283
  hookUpEventsAndUnregister(stream, events);
239
284
 
240
- // done
241
285
  event.returnValue = { success: true };
242
- return;
243
286
  }
244
287
 
245
288
  function hookUpEventsAndUnregister(stream, events) {
246
289
  Object.keys(events).forEach((key) => stream.on(key, events[key]));
247
290
  stream.unregisterEvents = () => {
248
- Object.keys(events).forEach((key) =>
249
- stream.removeListener(key, events[key])
250
- );
291
+ Object.keys(events).forEach((key) => {
292
+ stream.removeListener(key, events[key]);
293
+ });
251
294
  };
252
295
  }
253
296
 
@@ -255,15 +298,22 @@ module.exports = function (
255
298
  return {
256
299
  data: (chunk) =>
257
300
  send("stream-data", valueToIPCValue(chunk), undefined, true),
258
- close: () => send("stream-close"),
301
+ close: () => {
302
+ debug("stream-close (readable)", requestId);
303
+ stream.unregisterEvents();
304
+ send("stream-close");
305
+ },
259
306
  error: (err) => {
307
+ debug("stream-error (readable)", requestId, err?.message);
260
308
  stream.unregisterEvents();
261
- send("stream-error", err ? err.message : "");
309
+ send("stream-error", err ? err.message : "Unknown stream error");
262
310
  },
263
311
  end: () => {
264
- stream.unregisterEvents(); // TODO does calling this in 'end' mean that 'close' will never be sent?
312
+ debug("stream-end (readable)", requestId);
265
313
  send("stream-end");
266
- webcontentsStreams[event.sender.id][requestId] = null;
314
+ if (webcontentsStreams[event.sender.id]) {
315
+ webcontentsStreams[event.sender.id][requestId] = null;
316
+ }
267
317
  },
268
318
  };
269
319
  }
@@ -271,87 +321,93 @@ module.exports = function (
271
321
  function createWritableEvents(event, stream, requestId, send) {
272
322
  return {
273
323
  drain: () => send("stream-drain", undefined, undefined, true),
274
- close: () => send("stream-close"),
324
+ close: () => {
325
+ debug("stream-close (writable)", requestId);
326
+ stream.unregisterEvents();
327
+ send("stream-close");
328
+ },
275
329
  error: (err) => {
330
+ debug("stream-error (writable)", requestId, err?.message);
276
331
  stream.unregisterEvents();
277
- send("stream-error", err ? err.message : "");
332
+ send("stream-error", err ? err.message : "Unknown stream error");
278
333
  },
279
334
  finish: () => {
280
- stream.unregisterEvents();
335
+ debug("stream-finish (writable)", requestId);
281
336
  send("stream-finish");
282
- webcontentsStreams[event.sender.id][requestId] = null;
337
+ if (webcontentsStreams[event.sender.id]) {
338
+ webcontentsStreams[event.sender.id][requestId] = null;
339
+ }
283
340
  },
284
341
  };
285
342
  }
286
343
 
287
344
  function createDuplexEvents(event, stream, requestId, send) {
345
+ // Note: unregisterEvents will be shared, which is correct
288
346
  return Object.assign(
289
347
  createWritableEvents(event, stream, requestId, send),
290
348
  createReadableEvents(event, stream, requestId, send)
291
349
  );
292
350
  }
293
351
 
294
- // special methods
295
352
  function trackWebcontentsStreams(webcontents, requestId, stream) {
296
- // track vs. sender's lifecycle
297
- if (!webcontentsStreams[webcontents.id]) {
298
- webcontentsStreams[webcontents.id] = {};
299
- // listen for webcontent close event
300
- webcontents.once(
301
- "did-navigate",
302
- closeAllWebcontentsStreams(webcontents.id)
303
- );
304
- webcontents.once("destroyed", closeAllWebcontentsStreams(webcontents.id));
353
+ const wcId = webcontents.id;
354
+ if (!webcontentsStreams[wcId]) {
355
+ webcontentsStreams[wcId] = {};
356
+
357
+ const cleanup = () => {
358
+ debug("cleaning up all streams for webcontents", wcId);
359
+ if (!webcontentsStreams[wcId]) return;
360
+ for (let rid in webcontentsStreams[wcId]) {
361
+ if (webcontentsStreams[wcId][rid]) {
362
+ const s = webcontentsStreams[wcId][rid];
363
+ if (s.unregisterEvents) s.unregisterEvents();
364
+ streamRequestClose(wcId, rid, []);
365
+ }
366
+ }
367
+ delete webcontentsStreams[wcId];
368
+ cleanupRoutines.delete(wcId);
369
+ };
370
+
371
+ cleanupRoutines.set(wcId, cleanup);
372
+ webcontents.once("did-navigate", cleanup);
373
+ webcontents.once("destroyed", cleanup);
305
374
  }
306
- webcontentsStreams[webcontents.id][requestId] = stream;
375
+ webcontentsStreams[wcId][requestId] = stream;
307
376
  }
308
377
 
309
378
  function streamRequestWrite(webcontentsId, requestId, args) {
310
- var stream = webcontentsStreams[webcontentsId][requestId];
311
-
312
- if (stream && typeof stream.write == "function") {
379
+ const stream = (webcontentsStreams[webcontentsId] || {})[requestId];
380
+ if (stream && typeof stream.write === "function") {
313
381
  stream.write(...args);
382
+ } else {
383
+ debug("write failed: stream not found or not writable", requestId);
314
384
  }
315
385
  }
386
+
316
387
  function streamRequestEnd(webcontentsId, requestId, args) {
317
- var stream = webcontentsStreams[webcontentsId][requestId];
318
- if (stream && typeof stream.end == "function") stream.end(...args);
388
+ const stream = (webcontentsStreams[webcontentsId] || {})[requestId];
389
+ if (stream && typeof stream.end === "function") {
390
+ stream.end(...args);
391
+ }
319
392
  }
393
+
320
394
  function streamRequestClose(webcontentsId, requestId, args) {
321
- var stream = webcontentsStreams[webcontentsId][requestId];
395
+ const stream = (webcontentsStreams[webcontentsId] || {})[requestId];
322
396
  if (!stream) return;
323
- // try .close
324
- if (typeof stream.close == "function") stream.close(...args);
325
- // hmm, try .destroy
326
- else if (typeof stream.destroy == "function") stream.destroy(...args);
327
- // oye, last shot: end()
328
- else if (typeof stream.end == "function") stream.end(...args);
329
- }
330
-
331
- // helpers
332
- function closeAllWebcontentsStreams(webcontentsId) {
333
- return (e) => {
334
- if (!webcontentsStreams[webcontentsId]) return;
335
-
336
- // close all of the open streams
337
- for (var requestId in webcontentsStreams[webcontentsId]) {
338
- if (webcontentsStreams[webcontentsId][requestId]) {
339
- webcontentsStreams[webcontentsId][requestId].unregisterEvents();
340
- streamRequestClose(webcontentsId, requestId, []);
341
- }
342
- }
343
-
344
- // stop tracking
345
- delete webcontentsStreams[webcontentsId];
346
- };
397
+ debug("force closing stream", requestId);
398
+ if (typeof stream.close === "function") stream.close(...args);
399
+ else if (typeof stream.destroy === "function") stream.destroy(...args);
400
+ else if (typeof stream.end === "function") stream.end(...args);
347
401
  }
348
402
 
349
403
  return api;
350
404
  };
351
405
 
352
406
  function errorObject(error) {
353
- var copy = Object.assign({}, error);
407
+ if (!error) return { message: "Unknown Error" };
408
+ const copy = Object.assign({}, error);
354
409
  copy.message = error.message || error.toString();
355
410
  if (error.name) copy.name = error.name;
411
+ if (error.stack) copy.stack = error.stack; // Added for better loggic
356
412
  return copy;
357
413
  }