浏览代码

dev: prepare puter.js fs for decorator pattern

- de-coupled xhr callback passing from the interface of the underlying
  filesystem implementation.
- This makes the interface to delegate calls more suitable for use with
  the decorator pattern.
- The decorator pattern will be used to manage the complexity of the
  caching layer by separating the concerns of different caching methods.
KernelDeimos 7 月之前
父节点
当前提交
c12ae2a923

+ 2 - 2
src/puter-js/src/index.js

@@ -1,5 +1,5 @@
 import OS from './modules/OS.js';
-import FileSystem from './modules/FileSystem/index.js';
+import { PuterJSFileSystemModule } from './modules/FileSystem/index.js';
 import Hosting from './modules/Hosting.js';
 import Email from './modules/Email.js';
 import Apps from './modules/Apps.js';
@@ -203,7 +203,7 @@ window.puter = (function() {
                              new OS(this.authToken, this.APIOrigin, this.appID, this.env));
             // FileSystem
             this.registerModule('fs',
-                             new FileSystem(this.authToken, this.APIOrigin, this.appID, this.env));
+                             new PuterJSFileSystemModule(this.authToken, this.APIOrigin, this.appID, this.env));
             // UI
             this.registerModule('ui',
                              new UI(this.appInstanceID, this.parentInstanceID, this.appID, this.env, this.util));

+ 107 - 0
src/puter-js/src/modules/FileSystem/definitions.js

@@ -0,0 +1,107 @@
+import * as utils from '../../lib/utils.js';
+import putility from "@heyputer/putility";
+import { TeePromise } from "@heyputer/putility/src/libs/promise";
+import getAbsolutePathForApp from './utils/getAbsolutePathForApp.js';
+
+export const TFilesystem = 'TFilesystem';
+
+// TODO: UNUSED (eventually putility will support these definitions)
+//       This is here so that the idea is not forgotten.
+export const IFilesystem = {
+    methods: {
+        stat: {
+            parameters: {
+                path: {
+                    alias: 'uid',
+                }
+            }
+        }
+    }
+
+};
+
+export class PuterAPIFilesystem extends putility.AdvancedBase {
+    constructor ({ api_info }) {
+        super();
+        this.api_info = api_info;
+    }
+
+    static IMPLEMENTS = {
+        [TFilesystem]: {
+            stat: async function (options) {
+                this.ensure_auth_();
+                const tp = new TeePromise();
+
+                const xhr = new utils.initXhr('/stat', this.api_info.APIOrigin, this.api_info.authToken);
+                utils.setupXhrEventHandlers(xhr, undefined, undefined,
+                    tp.resolve.bind(tp),
+                    tp.reject.bind(tp),
+                );
+
+                let dataToSend = {};
+                if (options.uid !== undefined) {
+                    dataToSend.uid = options.uid;
+                } else if (options.path !== undefined) {
+                    // If dirPath is not provided or it's not starting with a slash, it means it's a relative path
+                    // in that case, we need to prepend the app's root directory to it
+                    dataToSend.path = getAbsolutePathForApp(options.path);
+                }
+
+                dataToSend.return_subdomains = options.returnSubdomains;
+                dataToSend.return_permissions = options.returnPermissions;
+                dataToSend.return_versions = options.returnVersions;
+                dataToSend.return_size = options.returnSize;
+
+                xhr.send(JSON.stringify(dataToSend));
+
+                return await tp;
+            },
+            readdir: async function (options) {
+                this.ensure_auth_();
+                const tp = new TeePromise();
+
+                const xhr = new utils.initXhr('/readdir', this.api_info.APIOrigin, this.api_info.authToken);
+                utils.setupXhrEventHandlers(xhr, undefined, undefined,
+                    tp.resolve.bind(tp),
+                    tp.reject.bind(tp),
+                );
+
+                xhr.send(JSON.stringify({path: getAbsolutePathForApp(options.path)}));
+
+                return await tp;
+            },
+        }
+    }
+
+    ensure_auth_ () {
+        // TODO: remove reference to global 'puter'; get 'env' via context
+        if ( ! this.api_info.authToken && puter.env === 'web' ) {
+            try {
+                this.ui.authenticateWithPuter();
+            } catch (e) {
+                throw new Error('Authentication failed.');
+            }
+        }
+    }
+}
+
+export class ProxyFilesystem extends putility.AdvancedBase {
+    static PROPERTIES = {
+        delegate: () => {}, 
+    }
+    // TODO: constructor implied by properties
+    constructor ({ delegate }) {
+        super();
+        this.delegate = delegate;
+    }
+    static IMPLEMENTS = {
+        [TFilesystem]: {
+            stat: async function (o) {
+                return this.delegate.stat(o);
+            },
+            readdir: async function (o) {
+                return this.delegate.readdir(o);
+            }
+        }
+    }
+}

+ 35 - 7
src/puter-js/src/modules/FileSystem/index.js

@@ -1,8 +1,6 @@
 import io from '../../lib/socket.io/socket.io.esm.min.js';
 
 // Operations
-import readdir from "./operations/readdir.js";
-import stat from "./operations/stat.js";
 import space from "./operations/space.js";
 import mkdir from "./operations/mkdir.js";
 import copy from "./operations/copy.js";
@@ -15,11 +13,11 @@ import sign from "./operations/sign.js";
 // Why is this called deleteFSEntry instead of just delete? because delete is 
 // a reserved keyword in javascript
 import deleteFSEntry from "./operations/deleteFSEntry.js";
+import { ProxyFilesystem, PuterAPIFilesystem, TFilesystem } from './definitions.js';
+import { AdvancedBase } from '../../../../putility/index.js';
 
-class FileSystem{
+export class PuterJSFileSystemModule extends AdvancedBase {
 
-    readdir = readdir;
-    stat = stat;
     space = space;
     mkdir = mkdir;
     copy = copy;
@@ -33,6 +31,21 @@ class FileSystem{
     write = write;
     sign = sign;
 
+    static NARI_METHODS = {
+        stat: {
+            positional: ['path'],
+            fn (parameters) {
+                return this.filesystem.stat(parameters);
+            }
+        },
+        readdir: {
+            positional: ['path'],
+            fn (parameters) {
+                return this.filesystem.readdir(parameters);
+            }
+        },
+    }
+
     /**
      * Creates a new instance with the given authentication token, API origin, and app ID,
      * and connects to the socket.
@@ -43,13 +56,30 @@ class FileSystem{
      * @param {string} appID - ID of the app to use.
      */
     constructor (authToken, APIOrigin, appID) {
+        super();
         this.authToken = authToken;
         this.APIOrigin = APIOrigin;
         this.appID = appID;
         // Connect socket.
         this.initializeSocket();
+
+        // We need to use `Object.defineProperty` instead of passing
+        // `authToken` and `APIOrigin` because they will change.
+        const api_info = {};
+        Object.defineProperty(api_info, 'authToken', {
+            get: () => this.authToken,
+        });
+        Object.defineProperty(api_info, 'APIOrigin', {
+            get: () => this.APIOrigin,
+        });
+
+        // Construct the decorator chain for the client-side filesystem.
+        let fs = new PuterAPIFilesystem({ api_info }).as(TFilesystem);
+        fs = new ProxyFilesystem({ delegate: fs }).as(TFilesystem);
+        this.filesystem = fs;
     }
 
+
     /**
      * Initializes the socket connection to the server using the current API origin.
      * If a socket connection already exists, it disconnects it before creating a new one.
@@ -136,5 +166,3 @@ class FileSystem{
         this.initializeSocket();
     }
 }
-
-export default FileSystem;

+ 1 - 0
src/putility/src/AdvancedBase.js

@@ -26,6 +26,7 @@ class AdvancedBase extends FeatureBase {
         require('./features/NodeModuleDIFeature'),
         require('./features/PropertiesFeature'),
         require('./features/TraitsFeature'),
+        require('./features/NariMethodsFeature'),
     ]
 }
 

+ 108 - 0
src/putility/src/features/NariMethodsFeature.js

@@ -0,0 +1,108 @@
+module.exports = {
+    readme: `
+        Normalized Asynchronous Request Invocation (NARI) Methods Feature
+
+        This feature allows a class to define "Nari methods", which are methods
+        that support both async/await and callback-style invocation, have
+        positional arguments, and an options argument.
+
+        "the expected interface for methods in puter.js"
+
+        The underlying method will receive parameters as an object, with the
+        positional arguments as keys in the object. The options argument will
+        be merged into the parameters object unless the method spec specifies
+        \`separate_options: true\`.
+
+        Example:
+
+        \`\`\`
+        class MyClass extends AdvancedBase {
+            static NARI_METHODS = {
+                myMethod: {
+                    positional: ['param1', 'param2'],
+                    fn: ({ param1, param2 }) => {
+                        return param1 + param2;
+                    }
+                }
+            }
+        }
+
+        const instance = new MyClass();
+        const result = instance.myMethod(1, 2); // returns 3
+        \`\`\`
+
+        The method can also be called with options and callbacks:
+
+        \`\`\`
+        instance.myMethod(1, 2, { option1: 'value' }, (result) => {
+            console.log('success', result);
+        }, (error) => {
+            console.error('error', error);
+        });
+        \`\`\`
+    `,
+    install_in_instance: (instance) => {
+        const nariMethodSpecs = instance._get_merged_static_object('NARI_METHODS');
+
+        instance._.nariMethods = {};
+
+        for ( const method_name in nariMethodSpecs ) {
+            const spec = nariMethodSpecs[method_name];
+            const bound_fn = spec.fn.bind(instance);
+            instance._.nariMethods[method_name] = bound_fn;
+
+            instance[method_name] = async (...args) => {
+                const endArgsIndex = spec.positional.length;
+                const posArgs = args.slice(0, endArgsIndex);
+                const endArgs = args.slice(endArgsIndex);
+
+                const parameters = {};
+                const options = {};
+                const callbacks = {};
+                for ( const [index, arg] of posArgs.entries() ) {
+                    parameters[spec.positional[index]] = arg;
+                }
+                
+                if ( typeof endArgs[0] === 'object' ) {
+                    Object.assign(options, endArgs[0]);
+                    endArgs.shift();
+                }
+
+                if ( typeof endArgs[0] === 'function' ) {
+                    callbacks.success = endArgs[0];
+                    endArgs.shift();
+                }
+
+                if ( typeof endArgs[0] === 'function' ) {
+                    callbacks.error = endArgs[0];
+                    endArgs.shift();
+                }
+
+                if ( spec.separate_options ) {
+                    parameters.options = options;
+                } else {
+                    Object.assign(parameters, options);
+                }
+
+                console.log('parameters being passed', parameters);
+
+                let retval;
+                try {
+                    retval = await bound_fn(parameters);
+                } catch (e) {
+                    if ( callbacks.error ) {
+                        callbacks.error(e);
+                    } else {
+                        throw e;
+                    }
+                }
+
+                if ( callbacks.success ) {
+                    callbacks.success(retval);
+                }
+
+                return retval;
+            };
+        }
+    }
+};