@cityofzion/bs-electron 0.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/dist/main.d.ts ADDED
@@ -0,0 +1,3 @@
1
+ import { TApi, TExposedApi } from './utils';
2
+ export declare function eraseApi(api: any, prefix?: string): TExposedApi;
3
+ export declare function exposeApiToRenderer<T extends TApi>(api: T): void;
package/dist/main.js ADDED
@@ -0,0 +1,114 @@
1
+ "use strict";
2
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
3
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
4
+ return new (P || (P = Promise))(function (resolve, reject) {
5
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
6
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
7
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
8
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
9
+ });
10
+ };
11
+ var __importDefault = (this && this.__importDefault) || function (mod) {
12
+ return (mod && mod.__esModule) ? mod : { "default": mod };
13
+ };
14
+ Object.defineProperty(exports, "__esModule", { value: true });
15
+ exports.exposeApiToRenderer = exports.eraseApi = void 0;
16
+ const utils_1 = require("./utils");
17
+ const utils_2 = require("./utils");
18
+ const electron_1 = require("electron");
19
+ const lodash_clonedeep_1 = __importDefault(require("lodash.clonedeep"));
20
+ const exposedApis = new Map();
21
+ let initialized = false;
22
+ // Erase the api to expose all methods and properties to renderer. It also supports nested objects
23
+ function eraseApi(api, prefix) {
24
+ const { asyncMethods, syncMethods, properties } = (0, utils_1.getPropertiesAndMethods)(api);
25
+ // As it is a recursive function, we need to add the prefix to the methods and properties
26
+ const response = {
27
+ asyncMethods: asyncMethods.map(method => (prefix ? `${prefix}.${method}` : method)),
28
+ syncMethods: syncMethods.map(method => (prefix ? `${prefix}.${method}` : method)),
29
+ properties: properties.map(property => (prefix ? `${prefix}.${property}` : property)),
30
+ };
31
+ // Iterate for all properties to discover nested objects
32
+ properties.forEach(property => {
33
+ const propertyValue = api[property];
34
+ // If the property is not an object, we don't need to iterate over it. Array is also considered an object so we need to disconsider it
35
+ if (typeof propertyValue !== 'object' ||
36
+ Array.isArray(propertyValue) ||
37
+ propertyValue instanceof Map ||
38
+ propertyValue instanceof Set)
39
+ return;
40
+ const propertyWithPrefix = prefix ? `${prefix}.${property}` : property;
41
+ // Recursive call to discover nested properties and methods
42
+ const nestedPropertiesAndMethods = eraseApi(propertyValue, propertyWithPrefix);
43
+ // Add the nested properties and methods to the response
44
+ response.syncMethods.push(...nestedPropertiesAndMethods.syncMethods);
45
+ response.asyncMethods.push(...nestedPropertiesAndMethods.asyncMethods);
46
+ response.properties.push(...nestedPropertiesAndMethods.properties);
47
+ // Remove the actual property because it is a instance and can't be serialized by ipc
48
+ response.properties.splice(response.properties.indexOf(propertyWithPrefix), 1);
49
+ });
50
+ return response;
51
+ }
52
+ exports.eraseApi = eraseApi;
53
+ function exposeApiToRenderer(api) {
54
+ init();
55
+ // Class name
56
+ const apiName = api.constructor.name;
57
+ const apiIsAlreadyExposed = exposedApis.has(apiName);
58
+ if (apiIsAlreadyExposed) {
59
+ console.warn(`API ${apiName} is already exposed to renderer`);
60
+ return;
61
+ }
62
+ const apiClone = (0, lodash_clonedeep_1.default)(api);
63
+ const { asyncMethods, properties, syncMethods } = eraseApi(apiClone);
64
+ // For each property, we need to create a listener so renderer can request the property value
65
+ properties.forEach(property => {
66
+ electron_1.ipcMain.on((0, utils_2.buildIpcChannelName)(apiName, property), event => {
67
+ try {
68
+ const value = (0, utils_1.getValueFromPath)(api, property);
69
+ event.returnValue = { data: value };
70
+ }
71
+ catch (error) {
72
+ event.returnValue = { error: error.message };
73
+ }
74
+ });
75
+ });
76
+ // For each syncMethods, we need to create a listener so renderer can request the property value. We need to bind the function to the api instance to keep the context, the getValueFromPath function will do this for us
77
+ syncMethods.forEach(method => {
78
+ electron_1.ipcMain.on((0, utils_2.buildIpcChannelName)(apiName, method), (event, ...args) => {
79
+ try {
80
+ const func = (0, utils_1.getValueFromPath)(api, method);
81
+ const data = func(...args);
82
+ event.returnValue = { data };
83
+ }
84
+ catch (error) {
85
+ event.returnValue = { error: error.message };
86
+ }
87
+ });
88
+ });
89
+ // For each asyncMethods, we need to create a listener so renderer can request the property value. We need to bind the function to the api instance to keep the context, the getValueFromPath function will do this for us
90
+ asyncMethods.forEach(method => {
91
+ electron_1.ipcMain.handle((0, utils_2.buildIpcChannelName)(apiName, method), (_event, ...args) => __awaiter(this, void 0, void 0, function* () {
92
+ try {
93
+ const func = (0, utils_1.getValueFromPath)(api, method);
94
+ const data = yield func(...args);
95
+ return { data };
96
+ }
97
+ catch (error) {
98
+ return { error: error.message };
99
+ }
100
+ }));
101
+ });
102
+ exposedApis.set(apiName, { properties, asyncMethods, syncMethods });
103
+ }
104
+ exports.exposeApiToRenderer = exposeApiToRenderer;
105
+ // This function is called only once to initialize the ipcMain listener so renderer can request the exposed api
106
+ function init() {
107
+ if (initialized)
108
+ return;
109
+ initialized = true;
110
+ electron_1.ipcMain.on(utils_1.GET_EXPOSED_API_CHANNEL, (event, apiName) => {
111
+ const exposedApi = exposedApis.get(apiName);
112
+ event.returnValue = exposedApi;
113
+ });
114
+ }
@@ -0,0 +1,32 @@
1
+ {
2
+ "name": "@cityofzion/bs-electron",
3
+ "version": "0.0.1",
4
+ "repository": "https://github.com/CityOfZion/blockchain-services",
5
+ "author": "Coz",
6
+ "license": "MIT",
7
+ "files": [
8
+ "/dist"
9
+ ],
10
+ "scripts": {
11
+ "build": "tsc",
12
+ "lint": "eslint .",
13
+ "format": "eslint --fix"
14
+ },
15
+ "dependencies": {
16
+ "@cityofzion/blockchain-service": "workspace:*",
17
+ "lodash.clonedeep": "~4.5.0"
18
+ },
19
+ "devDependencies": {
20
+ "@types/node": "~20.2.5",
21
+ "@typescript-eslint/eslint-plugin": "^6.5.0",
22
+ "@typescript-eslint/parser": "^6.5.0",
23
+ "eslint": "^8.48.0",
24
+ "ts-node": "10.9.1",
25
+ "typescript": "4.9.5",
26
+ "@types/lodash.clonedeep": "~4.5.9"
27
+ },
28
+ "peerDependencies": {
29
+ "electron": "*",
30
+ "@electron-toolkit/preload": "*"
31
+ }
32
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,16 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const electron_1 = require("electron");
4
+ const preload_1 = require("@electron-toolkit/preload");
5
+ if (process.contextIsolated) {
6
+ try {
7
+ electron_1.contextBridge.exposeInMainWorld('ipcBsElectron', preload_1.electronAPI.ipcRenderer);
8
+ }
9
+ catch (error) {
10
+ console.error(error);
11
+ }
12
+ }
13
+ else {
14
+ // @ts-ignore (define in dts)
15
+ window.ipcBsElectron = ipcRenderer;
16
+ }
@@ -0,0 +1,2 @@
1
+ import { TApi } from './utils';
2
+ export declare function bindApiFromMain<T extends TApi = any>(apiName: string): T;
@@ -0,0 +1,65 @@
1
+ "use strict";
2
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
3
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
4
+ return new (P || (P = Promise))(function (resolve, reject) {
5
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
6
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
7
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
8
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
9
+ });
10
+ };
11
+ Object.defineProperty(exports, "__esModule", { value: true });
12
+ exports.bindApiFromMain = void 0;
13
+ const utils_1 = require("./utils");
14
+ const boundApis = new Map();
15
+ function bindApiFromMain(apiName) {
16
+ const boundApi = boundApis.get(apiName);
17
+ if (boundApi) {
18
+ return boundApi;
19
+ }
20
+ // Request the exposed api from main
21
+ const exposedApi = window.ipcBsElectron.sendSync(utils_1.GET_EXPOSED_API_CHANNEL, apiName);
22
+ if (!exposedApi) {
23
+ throw new Error(`API ${apiName} is not exposed to renderer`);
24
+ }
25
+ const api = {};
26
+ // For each property, we need to populate the api with a getter so the renderer can request the property value from main
27
+ exposedApi.properties.forEach(property => {
28
+ (0, utils_1.populateObjectFromPath)(api, String(property), {
29
+ get: () => {
30
+ const result = window.ipcBsElectron.sendSync((0, utils_1.buildIpcChannelName)(apiName, property));
31
+ if (result.error) {
32
+ throw new Error('Ipc only supports stringified data');
33
+ }
34
+ return result.data;
35
+ },
36
+ });
37
+ });
38
+ // For each syncMethods, we need to populate the api with a function that will send a sync message to main to request the method execution result
39
+ exposedApi.syncMethods.forEach(method => {
40
+ (0, utils_1.populateObjectFromPath)(api, String(method), {
41
+ value: (...args) => {
42
+ const result = window.ipcBsElectron.sendSync((0, utils_1.buildIpcChannelName)(apiName, method), ...args);
43
+ if (result.error) {
44
+ throw new Error(result.error);
45
+ }
46
+ return result.data;
47
+ },
48
+ });
49
+ });
50
+ // For each asyncMethods, we need to populate the api with a function that will send a sync message to main to request the method execution result
51
+ exposedApi.asyncMethods.forEach(method => {
52
+ (0, utils_1.populateObjectFromPath)(api, String(method), {
53
+ value: (...args) => __awaiter(this, void 0, void 0, function* () {
54
+ const result = yield window.ipcBsElectron.invoke((0, utils_1.buildIpcChannelName)(apiName, method), ...args);
55
+ if (result.error) {
56
+ throw new Error(result.error);
57
+ }
58
+ return result.data;
59
+ }),
60
+ });
61
+ });
62
+ boundApis.set(apiName, api);
63
+ return api;
64
+ }
65
+ exports.bindApiFromMain = bindApiFromMain;
@@ -0,0 +1,18 @@
1
+ export declare const CHANNEL_PREFIX = "bsElectron";
2
+ export declare const GET_EXPOSED_API_CHANNEL: string;
3
+ export type TApi = {
4
+ [K in keyof any]: any;
5
+ };
6
+ export type TExposedApi = {
7
+ properties: string[];
8
+ syncMethods: string[];
9
+ asyncMethods: string[];
10
+ };
11
+ export declare function getPropertiesAndMethods(object: any): {
12
+ properties: string[];
13
+ syncMethods: string[];
14
+ asyncMethods: string[];
15
+ };
16
+ export declare function getValueFromPath(obj: any, path: string): any;
17
+ export declare function populateObjectFromPath(obj: any, path: string, value: PropertyDescriptor & ThisType<any>): any;
18
+ export declare function buildIpcChannelName<T>(apiName: string, methodOrProperty: keyof T): string;
package/dist/utils.js ADDED
@@ -0,0 +1,71 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.buildIpcChannelName = exports.populateObjectFromPath = exports.getValueFromPath = exports.getPropertiesAndMethods = exports.GET_EXPOSED_API_CHANNEL = exports.CHANNEL_PREFIX = void 0;
4
+ exports.CHANNEL_PREFIX = 'bsElectron';
5
+ exports.GET_EXPOSED_API_CHANNEL = `${exports.CHANNEL_PREFIX}:getExposedApi`;
6
+ // It returns all properties and methods from an object and its prototype chain, that is, extended classes are also considered
7
+ function getPropertiesAndMethods(object) {
8
+ const syncMethods = new Set();
9
+ const asyncMethods = new Set();
10
+ const properties = new Set();
11
+ do {
12
+ for (const key of Reflect.ownKeys(object)) {
13
+ if (key === 'constructor')
14
+ continue;
15
+ const keyString = String(key);
16
+ if (typeof object[key] === 'function') {
17
+ try {
18
+ const funcResponse = object[key].call(object, {});
19
+ if (funcResponse instanceof Promise) {
20
+ funcResponse.catch(() => { }).then(() => { });
21
+ asyncMethods.add(keyString);
22
+ continue;
23
+ }
24
+ syncMethods.add(keyString);
25
+ }
26
+ catch (_a) {
27
+ syncMethods.add(keyString);
28
+ }
29
+ continue;
30
+ }
31
+ properties.add(keyString);
32
+ }
33
+ } while ((object = Reflect.getPrototypeOf(object)) && object !== Object.prototype);
34
+ return {
35
+ properties: Array.from(properties),
36
+ syncMethods: Array.from(syncMethods),
37
+ asyncMethods: Array.from(asyncMethods),
38
+ };
39
+ }
40
+ exports.getPropertiesAndMethods = getPropertiesAndMethods;
41
+ function getValueFromPath(obj, path) {
42
+ return path.split('.').reduce((acc, key) => {
43
+ // If the key is a function, we need to bind it to the object to keep the context
44
+ if (typeof acc[key] === 'function') {
45
+ return acc[key].bind(acc);
46
+ }
47
+ return acc[key];
48
+ }, obj);
49
+ }
50
+ exports.getValueFromPath = getValueFromPath;
51
+ function populateObjectFromPath(obj, path, value) {
52
+ var _a;
53
+ const splittedPath = path.split('.');
54
+ let tempObj = obj;
55
+ for (let i = 0; i < splittedPath.length; i++) {
56
+ const property = splittedPath[i];
57
+ if (i === splittedPath.length - 1) {
58
+ Object.defineProperty(tempObj, property, value);
59
+ }
60
+ else {
61
+ tempObj[property] = (_a = tempObj[property]) !== null && _a !== void 0 ? _a : {};
62
+ tempObj = tempObj[property];
63
+ }
64
+ }
65
+ return obj;
66
+ }
67
+ exports.populateObjectFromPath = populateObjectFromPath;
68
+ function buildIpcChannelName(apiName, methodOrProperty) {
69
+ return `${exports.CHANNEL_PREFIX}:${apiName}:${String(methodOrProperty)}`;
70
+ }
71
+ exports.buildIpcChannelName = buildIpcChannelName;
package/package.json ADDED
@@ -0,0 +1,32 @@
1
+ {
2
+ "name": "@cityofzion/bs-electron",
3
+ "version": "0.0.1",
4
+ "repository": "https://github.com/CityOfZion/blockchain-services",
5
+ "author": "Coz",
6
+ "license": "MIT",
7
+ "files": [
8
+ "/dist"
9
+ ],
10
+ "scripts": {
11
+ "build": "tsc && cp package.json dist/",
12
+ "lint": "eslint .",
13
+ "format": "eslint --fix"
14
+ },
15
+ "dependencies": {
16
+ "@cityofzion/blockchain-service": "workspace:*",
17
+ "lodash.clonedeep": "~4.5.0"
18
+ },
19
+ "devDependencies": {
20
+ "@types/node": "~20.2.5",
21
+ "@typescript-eslint/eslint-plugin": "^6.5.0",
22
+ "@typescript-eslint/parser": "^6.5.0",
23
+ "eslint": "^8.48.0",
24
+ "ts-node": "10.9.1",
25
+ "typescript": "4.9.5",
26
+ "@types/lodash.clonedeep": "~4.5.9"
27
+ },
28
+ "peerDependencies": {
29
+ "electron": "*",
30
+ "@electron-toolkit/preload": "*"
31
+ }
32
+ }