From c12ae2a9236f6c1f751a2d361106f14aef175b57 Mon Sep 17 00:00:00 2001 From: KernelDeimos Date: Thu, 10 Oct 2024 01:20:17 -0400 Subject: [PATCH] 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. --- src/puter-js/src/index.js | 4 +- .../src/modules/FileSystem/definitions.js | 107 +++++++++++++++++ src/puter-js/src/modules/FileSystem/index.js | 42 +++++-- src/putility/src/AdvancedBase.js | 1 + .../src/features/NariMethodsFeature.js | 108 ++++++++++++++++++ 5 files changed, 253 insertions(+), 9 deletions(-) create mode 100644 src/puter-js/src/modules/FileSystem/definitions.js create mode 100644 src/putility/src/features/NariMethodsFeature.js diff --git a/src/puter-js/src/index.js b/src/puter-js/src/index.js index c2e8bb56..5dc97795 100644 --- a/src/puter-js/src/index.js +++ b/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)); diff --git a/src/puter-js/src/modules/FileSystem/definitions.js b/src/puter-js/src/modules/FileSystem/definitions.js new file mode 100644 index 00000000..a9e59de0 --- /dev/null +++ b/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); + } + } + } +} diff --git a/src/puter-js/src/modules/FileSystem/index.js b/src/puter-js/src/modules/FileSystem/index.js index 0e6b2679..a4a18a13 100644 --- a/src/puter-js/src/modules/FileSystem/index.js +++ b/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; \ No newline at end of file diff --git a/src/putility/src/AdvancedBase.js b/src/putility/src/AdvancedBase.js index 3712a8a4..51b59fb2 100644 --- a/src/putility/src/AdvancedBase.js +++ b/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'), ] } diff --git a/src/putility/src/features/NariMethodsFeature.js b/src/putility/src/features/NariMethodsFeature.js new file mode 100644 index 00000000..7fcc71b4 --- /dev/null +++ b/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; + }; + } + } +};