mirror of
https://github.com/HeyPuter/puter
synced 2024-11-15 06:15:47 +00:00
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:
parent
4f8304ec15
commit
c12ae2a923
@ -1,5 +1,5 @@
|
|||||||
import OS from './modules/OS.js';
|
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 Hosting from './modules/Hosting.js';
|
||||||
import Email from './modules/Email.js';
|
import Email from './modules/Email.js';
|
||||||
import Apps from './modules/Apps.js';
|
import Apps from './modules/Apps.js';
|
||||||
@ -203,7 +203,7 @@ window.puter = (function() {
|
|||||||
new OS(this.authToken, this.APIOrigin, this.appID, this.env));
|
new OS(this.authToken, this.APIOrigin, this.appID, this.env));
|
||||||
// FileSystem
|
// FileSystem
|
||||||
this.registerModule('fs',
|
this.registerModule('fs',
|
||||||
new FileSystem(this.authToken, this.APIOrigin, this.appID, this.env));
|
new PuterJSFileSystemModule(this.authToken, this.APIOrigin, this.appID, this.env));
|
||||||
// UI
|
// UI
|
||||||
this.registerModule('ui',
|
this.registerModule('ui',
|
||||||
new UI(this.appInstanceID, this.parentInstanceID, this.appID, this.env, this.util));
|
new UI(this.appInstanceID, this.parentInstanceID, this.appID, this.env, this.util));
|
||||||
|
107
src/puter-js/src/modules/FileSystem/definitions.js
Normal file
107
src/puter-js/src/modules/FileSystem/definitions.js
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,8 +1,6 @@
|
|||||||
import io from '../../lib/socket.io/socket.io.esm.min.js';
|
import io from '../../lib/socket.io/socket.io.esm.min.js';
|
||||||
|
|
||||||
// Operations
|
// Operations
|
||||||
import readdir from "./operations/readdir.js";
|
|
||||||
import stat from "./operations/stat.js";
|
|
||||||
import space from "./operations/space.js";
|
import space from "./operations/space.js";
|
||||||
import mkdir from "./operations/mkdir.js";
|
import mkdir from "./operations/mkdir.js";
|
||||||
import copy from "./operations/copy.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
|
// Why is this called deleteFSEntry instead of just delete? because delete is
|
||||||
// a reserved keyword in javascript
|
// a reserved keyword in javascript
|
||||||
import deleteFSEntry from "./operations/deleteFSEntry.js";
|
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;
|
space = space;
|
||||||
mkdir = mkdir;
|
mkdir = mkdir;
|
||||||
copy = copy;
|
copy = copy;
|
||||||
@ -33,6 +31,21 @@ class FileSystem{
|
|||||||
write = write;
|
write = write;
|
||||||
sign = sign;
|
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,
|
* Creates a new instance with the given authentication token, API origin, and app ID,
|
||||||
* and connects to the socket.
|
* and connects to the socket.
|
||||||
@ -43,13 +56,30 @@ class FileSystem{
|
|||||||
* @param {string} appID - ID of the app to use.
|
* @param {string} appID - ID of the app to use.
|
||||||
*/
|
*/
|
||||||
constructor (authToken, APIOrigin, appID) {
|
constructor (authToken, APIOrigin, appID) {
|
||||||
|
super();
|
||||||
this.authToken = authToken;
|
this.authToken = authToken;
|
||||||
this.APIOrigin = APIOrigin;
|
this.APIOrigin = APIOrigin;
|
||||||
this.appID = appID;
|
this.appID = appID;
|
||||||
// Connect socket.
|
// Connect socket.
|
||||||
this.initializeSocket();
|
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.
|
* 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.
|
* If a socket connection already exists, it disconnects it before creating a new one.
|
||||||
@ -136,5 +166,3 @@ class FileSystem{
|
|||||||
this.initializeSocket();
|
this.initializeSocket();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default FileSystem;
|
|
@ -26,6 +26,7 @@ class AdvancedBase extends FeatureBase {
|
|||||||
require('./features/NodeModuleDIFeature'),
|
require('./features/NodeModuleDIFeature'),
|
||||||
require('./features/PropertiesFeature'),
|
require('./features/PropertiesFeature'),
|
||||||
require('./features/TraitsFeature'),
|
require('./features/TraitsFeature'),
|
||||||
|
require('./features/NariMethodsFeature'),
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
108
src/putility/src/features/NariMethodsFeature.js
Normal file
108
src/putility/src/features/NariMethodsFeature.js
Normal 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;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
Loading…
Reference in New Issue
Block a user