diff --git a/src/gui/src/IPC.js b/src/gui/src/IPC.js
index acae4ce7..beeb0f90 100644
--- a/src/gui/src/IPC.js
+++ b/src/gui/src/IPC.js
@@ -30,6 +30,7 @@ import path from "./lib/path.js";
import UIContextMenu from './UI/UIContextMenu.js';
import update_mouse_position from './helpers/update_mouse_position.js';
import item_icon from './helpers/item_icon.js';
+import { PROCESS_IPC_ATTACHED } from './definitions.js';
window.ipc_handlers = {};
/**
@@ -92,18 +93,44 @@ window.addEventListener('message', async (event) => {
// New IPC handlers should be registered here.
// Do this by calling `register_ipc_handler` of IPCService.
if ( window.ipc_handlers.hasOwnProperty(event.data.msg) ) {
+ const services = globalThis.services;
+ const svc_process = services.get('process');
+
+ // Add version info to old puter.js messages
+ // (and coerce them into the format of new ones)
+ if ( event.data.$ === undefined ) {
+ event.data.$ = 'puter-ipc';
+ event.data.v = 1;
+ event.data.parameters = {...event.data};
+ delete event.data.parameters.msg;
+ delete event.data.parameters.appInstanceId;
+ delete event.data.parameters.env;
+ delete event.data.parameters.uuid;
+ }
+
// The IPC context contains information about the call
+ const iframe = window.iframe_for_app_instance(
+ event.data.appInstanceID);
+ const process = svc_process.get_by_uuid(event.data.appInstanceID);
const ipc_context = {
- appInstanceId: event.data.appInstanceID,
+ caller: {
+ process: process,
+ app: {
+ appInstanceID: event.data.appInstanceID,
+ iframe,
+ window: $el_parent_window,
+ },
+ },
};
// Registered IPC handlers are an object with a `handle()`
// method. We call it "spec" here, meaning specification.
const spec = window.ipc_handlers[event.data.msg];
- await spec.handler(event.data, { msg_id, ipc_context });
+ let retval = await spec.handler(
+ event.data.parameters, { msg_id, ipc_context });
+
+ puter.util.rpc.send(iframe.contentWindow, msg_id, retval);
- // Early-return to avoid redundant invokation of any
- // legacy IPC handler.
return;
}
@@ -112,25 +139,16 @@ window.addEventListener('message', async (event) => {
// READY
//-------------------------------------------------
if(event.data.msg === 'READY'){
- $(target_iframe).attr('data-appUsesSDK', 'true');
-
- // If we were waiting to launch this as a child app, report to the parent that it succeeded.
- window.report_app_launched(event.data.appInstanceID, { uses_sdk: true });
-
- // Send any saved broadcasts to the new app
- globalThis.services.get('broadcast').sendSavedBroadcastsTo(event.data.appInstanceID);
-
- // If `window-active` is set (meanign the window is focused), focus the window one more time
- // this is to ensure that the iframe is `definitely` focused and can receive keyboard events (e.g. keydown)
- if($el_parent_window.hasClass('window-active')){
- $el_parent_window.focusWindow();
- }
+ const services = globalThis.services;
+ const svc_process = services.get('process');
+ const process = svc_process.get_by_uuid(event.data.appInstanceID);
+ process.ipc_status = PROCESS_IPC_ATTACHED;
}
//-------------------------------------------------
// windowFocused
//-------------------------------------------------
- else if(event.data.msg === 'windowFocused'){
+ if(event.data.msg === 'windowFocused'){
// TODO: Respond to this
}
//--------------------------------------------------------
diff --git a/src/gui/src/definitions.js b/src/gui/src/definitions.js
index d738066b..8e5911ed 100644
--- a/src/gui/src/definitions.js
+++ b/src/gui/src/definitions.js
@@ -17,6 +17,8 @@
* along with this program. If not, see .
*/
+import { AdvancedBase } from "@heyputer/putility";
+
export class Service {
construct (o) {
this.$puter = {};
@@ -33,20 +35,36 @@ export class Service {
export const PROCESS_INITIALIZING = { i18n_key: 'initializing' };
export const PROCESS_RUNNING = { i18n_key: 'running' };
+export const PROCESS_IPC_PENDING = { i18n_key: 'pending' };
+export const PROCESS_IPC_NA = { i18n_key: 'N/A' };
+export const PROCESS_IPC_ATTACHED = { i18n_key: 'attached' };
+
// Something is cloning these objects, so '===' checks don't work.
// To work around this, the `i` property is used to compare them.
export const END_SOFT = { i: 0, end: true, i18n_key: 'end_soft' };
export const END_HARD = { i: 1, end: true, i18n_key: 'end_hard' };
-export class Process {
+export class Process extends AdvancedBase{
+ static PROPERTIES = {
+ status: () => PROCESS_INITIALIZING,
+ ipc_status: () => PROCESS_IPC_PENDING,
+ }
constructor ({ uuid, parent, name, meta }) {
+ super();
+
this.uuid = uuid;
this.parent = parent;
this.name = name;
this.meta = meta;
this.references = {};
-
- this.status = PROCESS_INITIALIZING;
+
+ Object.defineProperty(this.references, 'iframe', {
+ get: () => {
+ // note: Might eventually make sense to make the
+ // fn on window call here instead.
+ return window.iframe_for_app_instance(this.uuid);
+ }
+ })
this._construct();
}
@@ -111,6 +129,11 @@ export class PortalProcess extends Process {
});
}
}
+
+ send (channel, object, context) {
+ const target = this.references.iframe.contentWindow;
+ // NEXT: ...
+ }
};
export class PseudoProcess extends Process {
_construct () { this.type_ = 'ui' }
diff --git a/src/gui/src/helpers/launch_app.js b/src/gui/src/helpers/launch_app.js
index e1569691..00e2d2a7 100644
--- a/src/gui/src/helpers/launch_app.js
+++ b/src/gui/src/helpers/launch_app.js
@@ -358,25 +358,16 @@ const launch_app = async (options)=>{
}
}
- (async () => {
- const el = await el_win;
- $(el).on('remove', () => {
- const svc_process = globalThis.services.get('process');
- svc_process.unregister(process.uuid);
+ const el = await el_win;
+ process.references.el_win = el;
+ process.chstatus(PROCESS_RUNNING);
- // If it's a non-sdk app, report that it launched and closed.
- // FIXME: This is awkward. Really, we want some way of knowing when it's launched and reporting that immediately instead.
- const $app_iframe = $(el).find('.window-app-iframe');
- if ($app_iframe.attr('data-appUsesSdk') !== 'true') {
- window.report_app_launched(process.uuid, { uses_sdk: false });
- // We also have to report an extra close event because the real one was sent already
- window.report_app_closed(process.uuid);
- }
- });
+ $(el).on('remove', () => {
+ const svc_process = globalThis.services.get('process');
+ svc_process.unregister(process.uuid);
+ });
- process.references.el_win = el;
- process.chstatus(PROCESS_RUNNING);
- })();
+ return process;
}
export default launch_app;
diff --git a/src/gui/src/services/ExecService.js b/src/gui/src/services/ExecService.js
index 5946a075..c79266bb 100644
--- a/src/gui/src/services/ExecService.js
+++ b/src/gui/src/services/ExecService.js
@@ -1,4 +1,4 @@
-import { Service } from "../definitions.js";
+import { PROCESS_IPC_ATTACHED, Service } from "../definitions.js";
import launch_app from "../helpers/launch_app.js";
export class ExecService extends Service {
@@ -14,20 +14,61 @@ export class ExecService extends Service {
}
// This method is exposed to apps via IPCService.
- launchApp ({ app_name, args }, { ipc_context, msg_id } = {}) {
+ async launchApp ({ app_name, args }, { ipc_context, msg_id } = {}) {
+ const app = ipc_context?.caller?.app;
+ const process = ipc_context?.caller?.process;
+
// This mechanism will be replated with xdrpc soon
const child_instance_id = window.uuidv4();
- window.child_launch_callbacks[child_instance_id] = {
- parent_instance_id: event.data.appInstanceID,
- launch_msg_id: msg_id,
- };
// The "body" of this method is in a separate file
- launch_app({
+ const child_process = await launch_app({
name: app_name,
args: args ?? {},
- parent_instance_id: ipc_context?.appInstanceId,
+ parent_instance_id: app?.appInstanceID,
uuid: child_instance_id,
});
+
+ const send_child_launched_msg = (...a) => {
+ const parent_iframe = process?.references?.iframe;
+ parent_iframe.contentWindow.postMessage({
+ msg: 'childAppLaunched',
+ original_msg_id: msg_id,
+ child_instance_id,
+ ...a,
+ }, '*');
+ }
+
+ child_process.onchange('ipc_status', value => {
+ if ( value !== PROCESS_IPC_ATTACHED ) return;
+
+ $(child_process.references.iframe).attr('data-appUsesSDK', 'true');
+
+ send_child_launched_msg({ uses_sdk: true });
+
+ // Send any saved broadcasts to the new app
+ globalThis.services.get('broadcast').sendSavedBroadcastsTo(child_instance_id);
+
+ // If `window-active` is set (meanign the window is focused), focus the window one more time
+ // this is to ensure that the iframe is `definitely` focused and can receive keyboard events (e.g. keydown)
+ if(child_process.el_win.hasClass('window-active')){
+ child_process.el_win.focusWindow();
+ }
+ });
+
+ $(child_process.references.el_win).on('remove', () =>{
+ const parent_iframe = process?.references?.iframe;
+ if ($(parent_iframe).attr('data-appUsesSdk') !== 'true') {
+ send_child_launched_msg({ uses_sdk: false });
+ // We also have to report an extra close event because the real one was sent already
+ console.log('reporting app closed');
+ window.report_app_closed(child_process.uuid);
+ }
+ });
+
+ return {
+ appInstanceID: child_instance_id,
+ usesSDK: true,
+ };
}
}
diff --git a/src/puter-js/src/lib/xdrpc.js b/src/puter-js/src/lib/xdrpc.js
index e3d518cd..38b6a343 100644
--- a/src/puter-js/src/lib/xdrpc.js
+++ b/src/puter-js/src/lib/xdrpc.js
@@ -4,7 +4,7 @@
*/
// Since `Symbol` is not clonable, we use a UUID to identify RPCs.
-const $SCOPE = '9a9c83a4-7897-43a0-93b9-53217b84fde6';
+export const $SCOPE = '9a9c83a4-7897-43a0-93b9-53217b84fde6';
/**
* The CallbackManager is used to manage callbacks for RPCs.
@@ -12,7 +12,7 @@ const $SCOPE = '9a9c83a4-7897-43a0-93b9-53217b84fde6';
* the functions that are being called remotely.
*/
export class CallbackManager {
- #messageId = 0;
+ #messageId = 1;
constructor () {
this.callbacks = new Map();
@@ -32,6 +32,8 @@ export class CallbackManager {
const { id, args } = data;
const callback = this.callbacks.get(id);
if (callback) {
+ debugger;
+ console.log('callback?', callback, id, args);
callback(...args);
}
}
diff --git a/src/puter-js/src/modules/UI.js b/src/puter-js/src/modules/UI.js
index f5608416..2cf327a5 100644
--- a/src/puter-js/src/modules/UI.js
+++ b/src/puter-js/src/modules/UI.js
@@ -17,6 +17,16 @@ class AppConnection extends EventListener {
// Whether the target app uses the Puter SDK, and so accepts messages
// (Closing and close events will still function.)
#usesSDK;
+
+ static from (values, { appInstanceID, messageTarget }) {
+ const connection = new AppConnection(
+ messageTarget,
+ appInstanceID,
+ values.appInstanceID,
+ values.usesSDK
+ );
+ return connection;
+ }
constructor(messageTarget, appInstanceID, targetAppInstanceID, usesSDK) {
super([
@@ -161,6 +171,33 @@ class UI extends EventListener {
value: dehydrator.dehydrate(value),
}, '*');
}
+
+ #ipc_stub = async function ({
+ callback,
+ method,
+ parameters,
+ }) {
+ let p, resolve;
+ await new Promise(done_setting_resolve => {
+ p = new Promise(resolve_ => {
+ resolve = resolve_;
+ done_setting_resolve();
+ });
+ });
+ if ( ! resolve ) debugger;
+ const callback_id = this.util.rpc.registerCallback(resolve);
+ this.messageTarget?.postMessage({
+ $: 'puter-ipc', v: 2,
+ appInstanceID: this.appInstanceID,
+ env: this.env,
+ msg: method,
+ parameters,
+ uuid: callback_id,
+ }, '*');
+ const ret = await p;
+ if ( callback ) callback(ret);
+ return ret;
+ }
constructor (appInstanceID, parentInstanceID, appID, env, util) {
const eventNames = [
@@ -339,7 +376,7 @@ class UI extends EventListener {
}
// Determine if this is a response to a previous message and if so, is there
// a callback function for this message? if answer is yes to both then execute the callback
- else if(e.data.original_msg_id && this.#callbackFunctions[e.data.original_msg_id]){
+ else if(e.data.original_msg_id !== undefined && this.#callbackFunctions[e.data.original_msg_id]){
if(e.data.msg === 'fileOpenPicked'){
// 1 item returned
if(e.data.items.length === 1){
@@ -400,11 +437,6 @@ class UI extends EventListener {
// execute callback
this.#callbackFunctions[e.data.original_msg_id](new FSItem(e.data.saved_file));
}
- else if (e.data.msg === 'childAppLaunched') {
- // execute callback with a new AppConnection to the child
- const connection = new AppConnection(this.messageTarget, this.appInstanceID, e.data.child_instance_id, e.data.uses_sdk);
- this.#callbackFunctions[e.data.original_msg_id](connection);
- }
else{
// execute callback
this.#callbackFunctions[e.data.original_msg_id](e.data);
@@ -903,16 +935,20 @@ class UI extends EventListener {
}
// Returns a Promise
- launchApp = function(appName, args, callback) {
- return new Promise((resolve) => {
- // if appName is an object and args is not set, then appName is actually args
- if (typeof appName === 'object' && !args) {
- args = appName;
- appName = undefined;
- }
-
- this.#postMessageWithCallback('launchApp', resolve, { app_name: appName, args });
- })
+ launchApp = async function launchApp(app_name, args, callback) {
+ const app_info = await this.#ipc_stub({
+ method: 'launchApp',
+ callback,
+ parameters: {
+ app_name,
+ args,
+ },
+ });
+
+ return AppConnection.from(app_info, {
+ appInstanceID: this.appInstanceID,
+ messageTarget: this.messageTarget,
+ });
}
parentApp() {
diff --git a/src/puter-js/src/modules/Util.js b/src/puter-js/src/modules/Util.js
index 16dbe147..ef7b3e59 100644
--- a/src/puter-js/src/modules/Util.js
+++ b/src/puter-js/src/modules/Util.js
@@ -1,4 +1,4 @@
-import { CallbackManager, Dehydrator, Hydrator } from "../lib/xdrpc";
+import { $SCOPE, CallbackManager, Dehydrator, Hydrator } from "../lib/xdrpc";
/**
* The Util module exposes utilities within puter.js itself.
@@ -27,4 +27,12 @@ class UtilRPC {
getHydrator ({ target }) {
return new Hydrator({ target });
}
+
+ registerCallback (resolve) {
+ return this.callbackManager.register_callback(resolve);
+ }
+
+ send (target, id, ...args) {
+ target.postMessage({ $SCOPE, id, args }, '*');
+ }
}
diff --git a/src/putility/src/bases/BasicBase.js b/src/putility/src/bases/BasicBase.js
index 78d121f9..f64ae1af 100644
--- a/src/putility/src/bases/BasicBase.js
+++ b/src/putility/src/bases/BasicBase.js
@@ -30,8 +30,10 @@ class BasicBase {
_get_merged_static_array (key) {
const chain = this._get_inheritance_chain();
const values = [];
+ let last = null;
for ( const cls of chain ) {
- if ( cls[key] ) {
+ if ( cls[key] && cls[key] !== last ) {
+ last = cls[key];
values.push(...cls[key]);
}
}
@@ -39,6 +41,7 @@ class BasicBase {
}
_get_merged_static_object (key) {
+ // TODO: check objects by reference - same object in a subclass shouldn't count
const chain = this._get_inheritance_chain();
const values = {};
for ( const cls of chain ) {
diff --git a/src/putility/src/bases/FeatureBase.js b/src/putility/src/bases/FeatureBase.js
index 5ef78b73..9fb6b42b 100644
--- a/src/putility/src/bases/FeatureBase.js
+++ b/src/putility/src/bases/FeatureBase.js
@@ -25,7 +25,7 @@ class FeatureBase extends BasicBase {
this._ = {
features: this._get_merged_static_array('FEATURES'),
};
-
+
for ( const feature of this._.features ) {
feature.install_in_instance(
this,
diff --git a/src/putility/src/features/PropertiesFeature.js b/src/putility/src/features/PropertiesFeature.js
index 9e2c4b72..90a9572b 100644
--- a/src/putility/src/features/PropertiesFeature.js
+++ b/src/putility/src/features/PropertiesFeature.js
@@ -17,14 +17,23 @@
* along with this program. If not, see .
*/
module.exports = {
+ name: 'Properties',
install_in_instance: (instance) => {
const properties = instance._get_merged_static_object('PROPERTIES');
+ instance.onchange = (name, callback) => {
+ instance.__properties[name].listeners.push(callback);
+ };
+
+ instance.__properties = {};
+
for ( const k in properties ) {
- if ( typeof properties[k] === 'function' ) {
- instance[k] = properties[k]();
- continue;
- }
+ const state = {
+ definition: properties[k],
+ listeners: [],
+ value: undefined,
+ };
+ instance.__properties[k] = state;
if ( typeof properties[k] === 'object' ) {
// This will be supported in the future.
@@ -32,7 +41,29 @@ module.exports = {
`is not a supported property specification.`);
}
- instance[k] = properties[k];
+ let value = (() => {
+ if ( typeof properties[k] === 'function' ) {
+ return properties[k]();
+ }
+
+ return properties[k];
+ })();
+
+ Object.defineProperty(instance, k, {
+ get: () => {
+ return state.value;
+ },
+ set: (value) => {
+ for ( const listener of instance.__properties[k].listeners ) {
+ listener(value, {
+ old_value: instance[k],
+ });
+ }
+ state.value = value;
+ },
+ });
+
+ state.value = value;
}
}
}