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.
This commit is contained in:
KernelDeimos 2024-10-10 01:20:17 -04:00 committed by Eric Dubé
parent 4f8304ec15
commit c12ae2a923
5 changed files with 253 additions and 9 deletions

View File

@ -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));

View File

@ -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);
}
}
}
}

View File

@ -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;

View File

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

View File

@ -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;
};
}
}
};