@kabacorp/kaba-electron-rpc 7.0.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/README.md ADDED
@@ -0,0 +1,138 @@
1
+ # kaba-electron-rpc
2
+
3
+ Features:
4
+
5
+ - Supports RPC calls to/from the renderer or webview or a node child-process to the background process
6
+ - Supports methods which return:
7
+ - Sync values
8
+ - Async CBs
9
+ - Promises
10
+ - Readable streams
11
+ - Writable streams
12
+ - Duplex streams
13
+ - Permissions by examining the sender of the call
14
+ - Monitors renderer/webview lifetime to automatically release streams
15
+ - Optional timeout for async methods
16
+
17
+ ## Example usage
18
+
19
+ In a shared `example-api-manifest.js`:
20
+
21
+ ```js
22
+ module.exports = {
23
+ // simple method-types
24
+ readFile: 'async',
25
+ readFileSync: 'sync',
26
+ sayHello: 'promise',
27
+ createReadStream: 'readable',
28
+ createWriteStream: 'writable',
29
+ createDuplexStream: 'duplex'
30
+ }
31
+ ```
32
+
33
+ In the main electron process:
34
+
35
+ ```js
36
+ var rpc = require('kaba-electron-rpc')
37
+ var manifest = require('./example-api-manifest')
38
+ var fs = require('fs')
39
+
40
+ // export over the 'example-api' channel
41
+ var api = rpc.exportAPI('example-api', manifest, {
42
+ // the exported API behaves like normal calls:
43
+ readFile: fs.readFile,
44
+ readFileSync: fs.readFileSync,
45
+ sayHello: () => return Promise.resolve('hello!'),
46
+ createReadStream: fs.createReadStream,
47
+ createWriteStream: /* ... */,
48
+ createDuplexStream: /* ... */
49
+ })
50
+
51
+ // log any errors
52
+ api.on('error', console.log)
53
+ ```
54
+
55
+ In the renderer or webview process:
56
+
57
+ ```js
58
+ var rpc = require('kaba-electron-rpc')
59
+ var manifest = require('./example-api-manifest')
60
+
61
+ // import over the 'example-api' channel
62
+ var api = rpc.importAPI('example-api', manifest, { timeout: 30e3 })
63
+
64
+ // now use, as usual:
65
+ api.readFileSync('/etc/hosts') // => '...'
66
+ ```
67
+
68
+ ## API
69
+
70
+ ### rpc.exportAPI(channelName, manifest, methods, [globalPermissionCheck])
71
+
72
+ Methods will be called with a `this` set to the `event` object from [electron ipc](http://electron.atom.io/docs/api/ipc-main/#event-object).
73
+ Don't touch `returnValue`.
74
+
75
+ You can optionally specify a method for `globalPermissionCheck` with the following signature:
76
+
77
+ ```js
78
+ function globalPermissionCheck (event, methodName, args) {
79
+ if (event.sender.getURL() != 'url-I-trust') return false
80
+ return true
81
+ }
82
+ ```
83
+
84
+ If `globalPermissionCheck` is specified, and does not return true, the method call will respond with a 'Denied' error.
85
+
86
+ ### rpc.importAPI(channelName, manifest [,options])
87
+
88
+ - `options.timeout` Number. Specify how long in ms that async methods wait before erroring. Set to `false` to disable timeout.
89
+ - `options.errors` Object. Provides custom error constructors.
90
+ - `options.wc` WebContents. The web-contents that is exporting the API. Use this when importing an API from a webContents into the main thread.
91
+ - `options.proc` ChildProcess. The child-process that is exporting the API. Use this when importing an API from a node child process into the main thread.
92
+
93
+ ## Readable Streams
94
+
95
+ Readable streams in the clientside are given a `.close()` method.
96
+ All serverside streams MUST implement `.close()` or `.destroy()`, either of which will be called.
97
+
98
+ Stream methods can return a promise that resolves to a stream.
99
+
100
+ ## Buffers and ArrayBuffers
101
+
102
+ Arguments and return values are massaged so that they are Buffers on the exporter's side, and ArrayBuffers on the importer side.
103
+
104
+ ## Custom Errors
105
+
106
+ ```js
107
+ // shared code
108
+ // =
109
+
110
+ var manifest = {
111
+ testThrow: 'promise'
112
+ }
113
+
114
+ class MyCustomError extends Error {
115
+ constructor() {
116
+ super()
117
+ this.name = 'MyCustomError'
118
+ this.message = 'Custom error!'
119
+ }
120
+ }
121
+
122
+ // server
123
+ // =
124
+
125
+ rpc.exportAPI('error-api', manifest, {
126
+ testThrow() {
127
+ return Promise.reject(new MyCustomError())
128
+ }
129
+ })
130
+
131
+ // client
132
+ // =
133
+
134
+ var rpcClient = rpc.importAPI('error-api', manifest, {
135
+ errors: {MyCustomError} // pass in custom error constructors
136
+ })
137
+ rpcClient.testThrow().catch(console.log) // => MyCustomError
138
+ ```
package/index.js ADDED
@@ -0,0 +1,2 @@
1
+ module.exports.exportAPI = require('./lib/export-api')
2
+ module.exports.importAPI = require('./lib/import-api')
@@ -0,0 +1,351 @@
1
+ const EventEmitter = require("events");
2
+ const {
3
+ IPCValueToValue,
4
+ valueToIPCValue,
5
+ isNodeProcess,
6
+ isRenderer,
7
+ } = require("./util");
8
+
9
+ const _ = require("lodash");
10
+
11
+ module.exports = function (
12
+ channelName,
13
+ manifest,
14
+ methods,
15
+ globalPermissionCheck
16
+ ) {
17
+ var api = new EventEmitter();
18
+ var webcontentsStreams = {};
19
+
20
+ var channel;
21
+ if (isNodeProcess()) {
22
+ var mockedSender = new EventEmitter();
23
+ mockedSender.id = "main-process";
24
+ mockedSender.send = (channelName, msgType, requestId, ...args) => {
25
+ // console.log('export-api#send', {channelName, msgType, requestId, args})
26
+ process.send({ channelName, msgType, requestId, args });
27
+ };
28
+ channel = {
29
+ onMessage(cb) {
30
+ process.on("message", (msg) => {
31
+ if (msg.channelName !== channelName) return;
32
+ let mockedEvent = { sender: mockedSender };
33
+ // console.log('export-api#onMessage', msg)
34
+ cb(mockedEvent, msg.methodName, msg.requestId, ...msg.args);
35
+ });
36
+ },
37
+ };
38
+ } else if (isRenderer()) {
39
+ var { ipcRenderer } = require("electron");
40
+ channel = {
41
+ onMessage(cb) {
42
+ ipcRenderer.on(channelName, cb);
43
+ },
44
+ };
45
+ } else {
46
+ var { ipcMain } = require("electron");
47
+ channel = {
48
+ onMessage(cb) {
49
+ ipcMain.on(channelName, cb);
50
+ },
51
+ };
52
+ }
53
+
54
+ // wire up handler
55
+ channel.onMessage(async function (event, methodName, requestId, ...args) {
56
+ // console.log('received', channelName, methodName, requestId, ...args)
57
+ args = args.map(IPCValueToValue);
58
+
59
+ // watch for a navigation event
60
+ var hasNavigated = false;
61
+ function onDidNavigate() {
62
+ hasNavigated = true;
63
+ }
64
+ event.sender.on("did-navigate", onDidNavigate);
65
+
66
+ // helper to send
67
+ const send = function (
68
+ msgType,
69
+ err,
70
+ value,
71
+ keepListeningForDidNavigate = false
72
+ ) {
73
+ if (event.sender.isDestroyed && event.sender.isDestroyed()) return; // dont send response if destroyed
74
+ if (!keepListeningForDidNavigate)
75
+ event.sender.removeListener("did-navigate", onDidNavigate);
76
+ if (hasNavigated) return; // dont send response if the page changed
77
+
78
+ console.log("sending", channelName, msgType, requestId, err, value);
79
+ //
80
+ if (event.reply) {
81
+ var v = _.cloneDeep(value);
82
+ event.sender.send(channelName, msgType, requestId, err, v);
83
+ // event.reply(channelName, msgType, requestId, err, value);
84
+ } else if (event.sender && event.sender.send) {
85
+ var v = _.cloneDeep(value);
86
+ event.sender.send(channelName, msgType, requestId, err, v);
87
+ // event.sender.send(channelName, msgType, requestId, err, value);
88
+ }
89
+ };
90
+
91
+ // handle special methods
92
+ if (methodName == "stream-request-write") {
93
+ event.returnValue = true;
94
+ return streamRequestWrite(event.sender.id, requestId, args);
95
+ }
96
+ if (methodName == "stream-request-end") {
97
+ event.returnValue = true;
98
+ return streamRequestEnd(event.sender.id, requestId, args);
99
+ }
100
+ if (methodName == "stream-request-close") {
101
+ event.returnValue = true;
102
+ return streamRequestClose(event.sender.id, requestId, args);
103
+ }
104
+
105
+ // look up the method called
106
+ var type = manifest[methodName];
107
+ var method = methods[methodName];
108
+ if (!type || !method) {
109
+ api.emit(
110
+ "error",
111
+ new Error(`Method not found: "${methodName}"`),
112
+ arguments
113
+ );
114
+ return;
115
+ }
116
+
117
+ // global permission check
118
+ if (
119
+ globalPermissionCheck &&
120
+ !globalPermissionCheck(event, methodName, args)
121
+ ) {
122
+ // repond according to method type
123
+ if (type == "async" || type == "promise") {
124
+ send("async-reply", "Method Access Denied");
125
+ } else {
126
+ event.returnValue = { error: "Method Access Denied" };
127
+ }
128
+ return;
129
+ }
130
+
131
+ // run method by type
132
+ if (type == "sync") {
133
+ // call sync
134
+ try {
135
+ event.returnValue = {
136
+ success: valueToIPCValue(method.apply(event, args)),
137
+ };
138
+ } catch (e) {
139
+ event.returnValue = { error: e.message };
140
+ }
141
+ return;
142
+ }
143
+ if (type == "async") {
144
+ // create a reply cb
145
+ const replyCb = (err, value) => {
146
+ if (err) err = errorObject(err);
147
+ send("async-reply", err, valueToIPCValue(value));
148
+ };
149
+ args.push(replyCb);
150
+
151
+ // call async
152
+ method.apply(event, args);
153
+ return;
154
+ }
155
+ if (type == "promise") {
156
+ // call promise
157
+ let p;
158
+ try {
159
+ p = method.apply(event, args);
160
+ if (typeof p === "undefined") p = Promise.resolve();
161
+ if (typeof p.then === "undefined") p = Promise.resolve(p);
162
+ } catch (e) {
163
+ p = Promise.reject(errorObject(e));
164
+ }
165
+
166
+ // handle response
167
+ p.then(
168
+ (value) => send("async-reply", null, valueToIPCValue(value)),
169
+ (error) => send("async-reply", errorObject(error))
170
+ );
171
+ return;
172
+ }
173
+
174
+ var streamTypes = {
175
+ readable: createReadableEvents,
176
+ writable: createWritableEvents,
177
+ duplex: createDuplexEvents,
178
+ };
179
+
180
+ if (streamTypes[type]) {
181
+ return await handleStream(
182
+ event,
183
+ method,
184
+ requestId,
185
+ args,
186
+ streamTypes[type],
187
+ send
188
+ );
189
+ }
190
+
191
+ api.emit(
192
+ "error",
193
+ new Error(`Invalid method type "${type}" for "${methodName}"`),
194
+ arguments
195
+ );
196
+ });
197
+
198
+ async function handleStream(
199
+ event,
200
+ method,
201
+ requestId,
202
+ args,
203
+ createStreamEvents,
204
+ send
205
+ ) {
206
+ // call duplex
207
+ let stream;
208
+ let error;
209
+ try {
210
+ stream = method.apply(event, args);
211
+ if (!stream) {
212
+ send("stream-error", "Empty stream response");
213
+ return;
214
+ }
215
+ } catch (e) {
216
+ send("stream-error", "" + e);
217
+ return;
218
+ }
219
+
220
+ // handle promises
221
+ if (stream.then) {
222
+ try {
223
+ stream = await stream; // wait for it
224
+ } catch (e) {
225
+ send("stream-error", "" + e);
226
+ return;
227
+ }
228
+ }
229
+
230
+ trackWebcontentsStreams(event.sender, requestId, stream);
231
+ var events = createStreamEvents(event, stream, requestId, send);
232
+ hookUpEventsAndUnregister(stream, events);
233
+
234
+ // done
235
+ event.returnValue = { success: true };
236
+ return;
237
+ }
238
+
239
+ function hookUpEventsAndUnregister(stream, events) {
240
+ Object.keys(events).forEach((key) => stream.on(key, events[key]));
241
+ stream.unregisterEvents = () => {
242
+ Object.keys(events).forEach((key) =>
243
+ stream.removeListener(key, events[key])
244
+ );
245
+ };
246
+ }
247
+
248
+ function createReadableEvents(event, stream, requestId, send) {
249
+ return {
250
+ data: (chunk) =>
251
+ send("stream-data", valueToIPCValue(chunk), undefined, true),
252
+ close: () => send("stream-close"),
253
+ error: (err) => {
254
+ stream.unregisterEvents();
255
+ send("stream-error", err ? err.message : "");
256
+ },
257
+ end: () => {
258
+ stream.unregisterEvents(); // TODO does calling this in 'end' mean that 'close' will never be sent?
259
+ send("stream-end");
260
+ webcontentsStreams[event.sender.id][requestId] = null;
261
+ },
262
+ };
263
+ }
264
+
265
+ function createWritableEvents(event, stream, requestId, send) {
266
+ return {
267
+ drain: () => send("stream-drain", undefined, undefined, true),
268
+ close: () => send("stream-close"),
269
+ error: (err) => {
270
+ stream.unregisterEvents();
271
+ send("stream-error", err ? err.message : "");
272
+ },
273
+ finish: () => {
274
+ stream.unregisterEvents();
275
+ send("stream-finish");
276
+ webcontentsStreams[event.sender.id][requestId] = null;
277
+ },
278
+ };
279
+ }
280
+
281
+ function createDuplexEvents(event, stream, requestId, send) {
282
+ return Object.assign(
283
+ createWritableEvents(event, stream, requestId, send),
284
+ createReadableEvents(event, stream, requestId, send)
285
+ );
286
+ }
287
+
288
+ // special methods
289
+ function trackWebcontentsStreams(webcontents, requestId, stream) {
290
+ // track vs. sender's lifecycle
291
+ if (!webcontentsStreams[webcontents.id]) {
292
+ webcontentsStreams[webcontents.id] = {};
293
+ // listen for webcontent close event
294
+ webcontents.once(
295
+ "did-navigate",
296
+ closeAllWebcontentsStreams(webcontents.id)
297
+ );
298
+ webcontents.once("destroyed", closeAllWebcontentsStreams(webcontents.id));
299
+ }
300
+ webcontentsStreams[webcontents.id][requestId] = stream;
301
+ }
302
+
303
+ function streamRequestWrite(webcontentsId, requestId, args) {
304
+ var stream = webcontentsStreams[webcontentsId][requestId];
305
+
306
+ if (stream && typeof stream.write == "function") {
307
+ stream.write(...args);
308
+ }
309
+ }
310
+ function streamRequestEnd(webcontentsId, requestId, args) {
311
+ var stream = webcontentsStreams[webcontentsId][requestId];
312
+ if (stream && typeof stream.end == "function") stream.end(...args);
313
+ }
314
+ function streamRequestClose(webcontentsId, requestId, args) {
315
+ var stream = webcontentsStreams[webcontentsId][requestId];
316
+ if (!stream) return;
317
+ // try .close
318
+ if (typeof stream.close == "function") stream.close(...args);
319
+ // hmm, try .destroy
320
+ else if (typeof stream.destroy == "function") stream.destroy(...args);
321
+ // oye, last shot: end()
322
+ else if (typeof stream.end == "function") stream.end(...args);
323
+ }
324
+
325
+ // helpers
326
+ function closeAllWebcontentsStreams(webcontentsId) {
327
+ return (e) => {
328
+ if (!webcontentsStreams[webcontentsId]) return;
329
+
330
+ // close all of the open streams
331
+ for (var requestId in webcontentsStreams[webcontentsId]) {
332
+ if (webcontentsStreams[webcontentsId][requestId]) {
333
+ webcontentsStreams[webcontentsId][requestId].unregisterEvents();
334
+ streamRequestClose(webcontentsId, requestId, []);
335
+ }
336
+ }
337
+
338
+ // stop tracking
339
+ delete webcontentsStreams[webcontentsId];
340
+ };
341
+ }
342
+
343
+ return api;
344
+ };
345
+
346
+ function errorObject(error) {
347
+ var copy = Object.assign({}, error);
348
+ copy.message = error.message || error.toString();
349
+ if (error.name) copy.name = error.name;
350
+ return copy;
351
+ }