forked from cory/tildefriends
		
	
		
			
				
	
	
		
			253 lines
		
	
	
		
			5.6 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			253 lines
		
	
	
		
			5.6 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
| /**
 | |
|  * \file
 | |
|  * \defgroup tfapp Tilde Friends App JS
 | |
|  * Tilde Friends server-side app wrapper.
 | |
|  * @{
 | |
|  */
 | |
| 
 | |
| /** \cond */
 | |
| import * as core from './core.js';
 | |
| 
 | |
| export {App};
 | |
| /** \endcond */
 | |
| 
 | |
| /** A sequence number of apps. */
 | |
| let g_session_index = 0;
 | |
| 
 | |
| /**
 | |
|  ** App constructor.
 | |
|  ** @return An app instance.
 | |
|  */
 | |
| function App() {
 | |
| 	this._send_queue = [];
 | |
| 	this.calls = {};
 | |
| 	this._next_call_id = 1;
 | |
| 	return this;
 | |
| }
 | |
| 
 | |
| /**
 | |
|  ** Create a function wrapper that when called invokes a function on the app
 | |
|  ** itself.
 | |
|  ** @param api The function and argument names.
 | |
|  ** @return A function.
 | |
|  */
 | |
| App.prototype.makeFunction = function (api) {
 | |
| 	let self = this;
 | |
| 	let result = function () {
 | |
| 		let id = self._next_call_id++;
 | |
| 		while (!id || self.calls[id]) {
 | |
| 			id = self._next_call_id++;
 | |
| 		}
 | |
| 		let promise = new Promise(function (resolve, reject) {
 | |
| 			self.calls[id] = {resolve: resolve, reject: reject};
 | |
| 		});
 | |
| 		let message = {
 | |
| 			action: 'tfrpc',
 | |
| 			method: api[0],
 | |
| 			params: [...arguments],
 | |
| 			id: id,
 | |
| 		};
 | |
| 		self.send(message);
 | |
| 		return promise;
 | |
| 	};
 | |
| 	Object.defineProperty(result, 'name', {value: api[0], writable: false});
 | |
| 	return result;
 | |
| };
 | |
| 
 | |
| /**
 | |
|  ** Send a message to the app.
 | |
|  ** @param message The message to send.
 | |
|  */
 | |
| App.prototype.send = function (message) {
 | |
| 	if (this._send_queue) {
 | |
| 		if (this._on_output) {
 | |
| 			this._send_queue.forEach((x) => this._on_output(x));
 | |
| 			this._send_queue = null;
 | |
| 		} else if (message) {
 | |
| 			this._send_queue.push(message);
 | |
| 		}
 | |
| 	}
 | |
| 	if (message && this._on_output) {
 | |
| 		this._on_output(message);
 | |
| 	}
 | |
| };
 | |
| 
 | |
| /**
 | |
|  ** App socket handler.
 | |
|  ** @param request The HTTP request of the WebSocket connection.
 | |
|  ** @param response The HTTP response.
 | |
|  */
 | |
| exports.app_socket = async function socket(request, response) {
 | |
| 	let process;
 | |
| 	let options = {};
 | |
| 	let credentials = await httpd.auth_query(request.headers);
 | |
| 
 | |
| 	response.onClose = async function () {
 | |
| 		if (process && process.task) {
 | |
| 			process.task.kill();
 | |
| 		}
 | |
| 		if (process) {
 | |
| 			process.timeout = 0;
 | |
| 		}
 | |
| 	};
 | |
| 
 | |
| 	response.onMessage = async function (event) {
 | |
| 		if (event.opCode == 0x1 || event.opCode == 0x2) {
 | |
| 			let message;
 | |
| 			try {
 | |
| 				message = JSON.parse(event.data);
 | |
| 			} catch (error) {
 | |
| 				print(
 | |
| 					'WebSocket error:',
 | |
| 					error,
 | |
| 					event.data,
 | |
| 					event.data.length,
 | |
| 					event.opCode
 | |
| 				);
 | |
| 				return;
 | |
| 			}
 | |
| 			if (!process && message.action == 'hello') {
 | |
| 				let packageOwner;
 | |
| 				let packageName;
 | |
| 				let blobId;
 | |
| 				let match;
 | |
| 				let parentApp;
 | |
| 				if (
 | |
| 					(match = /^\/([&%][^\.]{44}(?:\.\w+)?)(\/?.*)/.exec(message.path))
 | |
| 				) {
 | |
| 					blobId = match[1];
 | |
| 				} else if ((match = /^\/\~([^\/]+)\/([^\/]+)\/$/.exec(message.path))) {
 | |
| 					packageOwner = match[1];
 | |
| 					packageName = match[2];
 | |
| 					blobId = await new Database(packageOwner).get('path:' + packageName);
 | |
| 					if (!blobId) {
 | |
| 						response.send(
 | |
| 							JSON.stringify({
 | |
| 								action: 'tfrpc',
 | |
| 								method: 'error',
 | |
| 								params: [message.path + ' not found'],
 | |
| 								id: -1,
 | |
| 							}),
 | |
| 							0x1
 | |
| 						);
 | |
| 						return;
 | |
| 					}
 | |
| 					if (packageOwner != 'core') {
 | |
| 						let coreId = await new Database('core').get('path:' + packageName);
 | |
| 						parentApp = {
 | |
| 							path: '/~core/' + packageName + '/',
 | |
| 							id: coreId,
 | |
| 						};
 | |
| 					}
 | |
| 				}
 | |
| 				response.send(
 | |
| 					JSON.stringify(
 | |
| 						Object.assign(
 | |
| 							{
 | |
| 								action: 'session',
 | |
| 								credentials: credentials,
 | |
| 								parentApp: parentApp,
 | |
| 								id: blobId,
 | |
| 							},
 | |
| 							await ssb_internal.getIdentityInfo(
 | |
| 								credentials?.session?.name,
 | |
| 								packageOwner,
 | |
| 								packageName
 | |
| 							)
 | |
| 						)
 | |
| 					),
 | |
| 					0x1
 | |
| 				);
 | |
| 
 | |
| 				options.api = message.api || [];
 | |
| 				options.credentials = credentials;
 | |
| 				options.packageOwner = packageOwner;
 | |
| 				options.packageName = packageName;
 | |
| 				options.url = message.url;
 | |
| 				let sessionId = 'session_' + (g_session_index++).toString();
 | |
| 				if (blobId) {
 | |
| 					if (message.edit_only) {
 | |
| 						response.send(
 | |
| 							JSON.stringify({action: 'ready', edit_only: true}),
 | |
| 							0x1
 | |
| 						);
 | |
| 					} else {
 | |
| 						process = await core.getProcessBlob(blobId, sessionId, options);
 | |
| 					}
 | |
| 				}
 | |
| 				if (process) {
 | |
| 					process.client_api.tfrpc = function (message) {
 | |
| 						if (message.id) {
 | |
| 							let calls = process?.app?.calls;
 | |
| 							if (calls) {
 | |
| 								let call = calls[message.id];
 | |
| 								if (call) {
 | |
| 									if (message.error !== undefined) {
 | |
| 										call.reject(message.error);
 | |
| 									} else {
 | |
| 										call.resolve(message.result);
 | |
| 									}
 | |
| 									delete calls[message.id];
 | |
| 								}
 | |
| 							}
 | |
| 						}
 | |
| 					};
 | |
| 					process.app._on_output = (message) =>
 | |
| 						response.send(JSON.stringify(message), 0x1);
 | |
| 					process.app.send();
 | |
| 				}
 | |
| 
 | |
| 				let ping = function () {
 | |
| 					let now = Date.now();
 | |
| 					let again = true;
 | |
| 					if (now - process.lastActive < process.timeout) {
 | |
| 						// Active.
 | |
| 					} else if (process.lastPing > process.lastActive) {
 | |
| 						// We lost them.
 | |
| 						if (process.task) {
 | |
| 							process.task.kill();
 | |
| 						}
 | |
| 						again = false;
 | |
| 					} else {
 | |
| 						// Idle.  Ping them.
 | |
| 						response.send('', 0x9);
 | |
| 						process.lastPing = now;
 | |
| 					}
 | |
| 
 | |
| 					if (again && process.timeout) {
 | |
| 						setTimeout(ping, process.timeout);
 | |
| 					}
 | |
| 				};
 | |
| 
 | |
| 				if (process && process.timeout > 0) {
 | |
| 					setTimeout(ping, process.timeout);
 | |
| 				}
 | |
| 			} else {
 | |
| 				if (process) {
 | |
| 					if (process.client_api[message.action]) {
 | |
| 						process.client_api[message.action](message);
 | |
| 					} else if (process.eventHandlers['message']) {
 | |
| 						await core.invoke(process.eventHandlers['message'], [message]);
 | |
| 					}
 | |
| 				}
 | |
| 			}
 | |
| 		} else if (event.opCode == 0x8) {
 | |
| 			// Close.
 | |
| 			if (process && process.task) {
 | |
| 				process.task.kill();
 | |
| 			}
 | |
| 			response.send(event.data, 0x8);
 | |
| 		} else if (event.opCode == 0xa) {
 | |
| 			// PONG
 | |
| 		}
 | |
| 
 | |
| 		if (process) {
 | |
| 			process.lastActive = Date.now();
 | |
| 		}
 | |
| 	};
 | |
| 
 | |
| 	response.upgrade(100, {});
 | |
| };
 | |
| 
 | |
| /** @} */
 |