More editor functionality

- Posted in Tech by - Permalink

Now I'm going to scare you ;-)
I got over to another important milestone: multiple applications, possibility to create new applications and editor able to edit any of the applications (boot, and the editor, of course, are also applications, so you can edit them while they run). Everything was done inside the platform, without external action.
Inside configure.js I also reused the trick I used before the multiapp editor and so the files needed to deploy are only two. First of them is the filesaving script fs.rb, which wasn't changed at all, the second is large configure.js. Here it is:

// ====== variables to configure ======
var pwd = "undefined";
var domain = "appdomain.net";

// please configure the path to save script in saveRawSource below if needed
// ====== verbatim copy of appjet._boot definition from boot.js ====== begin ======
appjet._boot = {
run: 
function (amd) {
    appjet.mainDomain = amd;
    if (request.headers.Host == "rescue." + amd) {
        var app = request.params.app;
        var appdata = new this.storable("obj-! " + app);
        if (appdata.doesNotExist) {
            printp("rescue: unknown app: ", app);
            response.stop();
        }
        var rescue = appdata.getString("server.bak");
        this.saveAppSource(app, rescue);
        printp("Last known good code of ", app, " restored.", BR(), CODE(rescue));
        printp(link("http://edit."+amd+"/?app="+app, "Go edit it!"));
        response.stop();
    } else if (request.headers.Host == "edit." + amd) {
        import("lib-! edit");
    } else if (request.headers.Host == "create." + amd) {
        import("lib-! create");
    } else {
        import("lib-! worker");
    }
}
,

saveRawSource: 
function (files) {
    var info = {pwd:new this.storable("obj-root").getString("pwd")};
    for (var p in files) {
        info[p + ".js"] = files[p];
    }
    wpost("http://staticdomain.net/fs.rb", info);
}
,

storable: 
function (id) {
    if (appjet._native.storage_getById(id).status != 0) {
        return {doesNotExist:true};
    }
    this.getString = function (key) {
        return appjet._native.storage_get(id, key).value;
    };
    this.putString = function (key, value) {
        return appjet._native.storage_put(id, key, "string", value).status == 0;
    };
}
,

saveAppSource: 
function (name, code, doBackup) {
    var appdata = new this.storable("obj-! " + name);
    if (doBackup) {
        appdata.putString("server.bak", appdata.getString("server"));
    }
    var files = {};
    files["lib-! " + name] = "/* appjet:version 0.1 */\n" + "/* appjet:library */\n" + code;
    files[name] = "/* appjet:version 0.1 */\n" + code;
    this.saveRawSource(files);
    appdata.putString("server", code);
}
,

};
// ====== verbatim copy of appjet._boot definition from boot.js ====== end ======

switch (request.path) {
case '/1':
    var bootBuilder = ["// (c) 2009, Herbert Voj\u010d\xedk"+"""
// Licensed by MIT license (http://www.opensource.org/licenses/mit-license.php)

(appjet._boot = {
"""];
    eachProperty(appjet._boot, function(name, f) {
        bootBuilder.push(name, ": ", f, ",\n\n");
    });
    bootBuilder.push("}).run(\"", domain, "\");");
    
    // ====== Storage dump, edited ====== begin ======
       (function () {
           var _an = appjet._native;
           _an.storage_create("obj-! create");
           _an.storage_create("obj-! worker");
           _an.storage_create("obj-! edit");
           _an.storage_create("obj-! boot");
           _an.storage_coll_create("coll-LyqehQFl7");
           _an.storage_put("obj-! create", "server.bak", "string", "// (c) 2009, Herbert Voj\u010d\xedk \r\n// Licensed by MIT license (http://www.opensource.org/licenses/mit-license.php)\r\n\r\nimport(\"storage\");\r\n(function(app){\r\n\r\nif (!app) {\r\n    return;\r\n}\r\nvar id = \"obj-! \"+app;\r\nif (appjet._native.storage_getById(id).status == 0) {\r\n    printp(\"Application \", B(CODE(app)), \" already exists.\");\r\n    return;\r\n}\r\nif (!app.match(/[a-z][a-z0-9-]+[a-z0-9]/)) {\r\n    printp(\"Application name must\",UL(\r\n        LI(\"be at least three characters long,\"),\r\n        LI(\"begin with smallcaps letter,\"),\r\n        LI(\"end with smallcaps letter or digit,\"),\r\n        LI(\"and contain only smallcaps letters, digits, or minus sign/dash (-) characters.\")\r\n    ));\r\n    return;\r\n}\r\n\r\nappjet._native.storage_create(id);\r\nvar appdata = getStorable(id);\r\nappdata[\"server.bak\"] = 'printp(\"Hello, world!\");';\r\nstorage.apps.add(appdata);\r\nresponse.redirect(\"http://rescue.\"+appjet.mainDomain+\"/?app=\"+app);\r\n\r\n})(request.params.app);\r\n\r\nprint(FORM(\"Supply application name:\", INPUT({name:'app'}), INPUT({type:'submit'})));\r\n");
           _an.storage_put("obj-root", "pwd", "string", pwd);
           _an.storage_put("obj-root", "apps", "object", "coll-LyqehQFl7");
           _an.storage_put("obj-! worker", "server.bak", "string", "printp(\"Hello, world!\");");
           _an.storage_put("obj-! edit", "server.bak", "string", "// (c) 2009, Herbert Voj\u010d\xedk \r\n// Licensed by MIT license (http://www.opensource.org/licenses/mit-license.php) \r\n\r\nvar app = request.params.app;\r\nvar appdata = new appjet._boot.storable(\"obj-! \"+app);\r\nif (appdata.doesNotExist) { printp(\"editor: unknown app: \", app); response.stop(); }\r\n\r\nvar _code = request.params.code;\r\nif (_code) {\r\n\tappjet._boot.saveAppSource(app, _code, true);\r\n\tresponse.redirect(request.path+\"?app=\"+encodeURIComponent(app));\r\n}\r\n\r\nprintp(\r\nB(app,\".\",appjet.mainDomain),BR(),\r\nB({style:'color:#0c0'},\"/* appjet:server */\"),BR(),\r\nFORM({method:'post', action:request.path},\r\n\tTEXTAREA({name:'code', style:'width:100%;height:80ex'},\r\n\t\thtml(appdata.getString(\"server\"))\r\n\t),\r\n\tINPUT({type:'hidden', name:'app', value:app}),\r\n\tINPUT({type:'submit'})\r\n));\r\n");
           _an.storage_put("obj-! boot", "server.bak", "string", bootBuilder.join(""));
           _an.storage_coll_add("coll-LyqehQFl7", "obj-! worker");
           _an.storage_coll_add("coll-LyqehQFl7", "obj-! edit");
           _an.storage_coll_add("coll-LyqehQFl7", "obj-! boot");
           _an.storage_coll_add("coll-LyqehQFl7", "obj-! create");
       })();
    // ====== Storage dump, edited ====== end ======
    
    printp("Storage configured. Backup version of boot, create, edit and worker apps included; you may restore them afterwards using rescue."+domain+".");
    printp("MD5 of password is:", BR(), md5(pwd), BR(),
        "Please update the hash in the filesaving script and proceed to ", link("/2", "step 2"), " to restore boot.js.");
    break;

case '/2':
    var appdata = new appjet._boot.storable("obj-! boot");
    var rescue = appdata.getString("server.bak");
    appjet._boot.saveAppSource("boot", rescue);
    printp("boot.js restored.");
    printp("You may stop running configure.js and start running boot.js.");
    break;

default:
    printp(H1("Platform configuration"));
    printp(BIG(link("/1", "Start the wizard")));
    break;
}

Bulk data for the configure.js were prepared by the platform itself (the code of the appjet._boot as well as storage dump were presented by the platform and were only copypasted into configure.js; the storage dump was edited to be minimal needed).
The structure of boot.js being

(appjet._boot={
... big inline object ..
... where all functionality resides ...
}).run("appdomain.net");

allowed for a cool metacircularity, which is present in configure.js as well.

  1. There is verbatim copy of the big inline object in the code of configure.js, as a living code, not as a string.
  2. Then, due to structure of boot.js, its complete source is prepared in a few lines using reflexion on this live instance.
  3. Then, later, this live instance is used to write its own code that was created from it into the boot.js file.

I simply like it.

Configure now has three pages, path / leads to welcome screen with link to /1 which fills the storage and emits the password hash, but does not save anything (the hash may need to be updated, the saves would not work anyway). From there link goes to path /2 which then simply rescues (using the live instance of appjet._boot) the boot application, thus creating boot.js file.

After successful configuring, there are no applications except integrated rescue, but it can be used to unleash the rest, which is only present as backup in storage: edit, create, worker.

Platform now recognizes multiple urls.

  • rescue.appdomain.net/?app=xxx rescues application xxx from backup (every app has now its own backup).
  • edit.appdomain.net/?app=xxx edits application xxx. Application "edit" must be present, rescue it first.
  • create.appdomain.net/ creates new application. Application "create" must be present, rescue it first.
  • anythingelse.appdomain.net/ runs the application "worker" (properly parsing hostname was postponed for later, multiapp editor was more urgent). Of course, the worker also must be rescued first, or you get an error.

I also think that now my idea of using import instead of eval is much more clearly visible, in the appjet._boot; in the way how saveAppSource saves the sourcecode and how run runs multiple applications.

To finish it, one of the cutest things is this: running rescue.appdomain.net/?app=boot is probably doomed. If you screwed your boot.js, it probably won't be working to rescue itself. No problem, stop the server (it's useless with broken boot.js anyway), start it with configure.js and run just the second step...
and it wasn't planned, it just came out this way.