mirror of
https://github.com/HeyPuter/puter.git
synced 2026-01-07 05:30:31 -06:00
feat: puter workers
* experimental Cloudflare Workers for Platforms support * add support for the puter driver * stop hardcoding api.puter.localhost * support destroy from express route as well * initial worker support * xhrshim + fixes (incomplete) * change order of readyState + load event * remove some debug logs * change worker/puterUtils into a cjs module * Cloudflare workers eventtarget workaround * worker preamble webpack * edit worker readme to reflect reality * allow a way to code in api endpoint instead of hardcoding it to api.puter.com * move cloudflare eventtarget fix to puter-portable template This is so it gets run before the rest of puter-js initializes * remove express route for worker
This commit is contained in:
@@ -4,7 +4,8 @@
|
||||
"description": "Backend/Kernel for Puter",
|
||||
"main": "exports.js",
|
||||
"scripts": {
|
||||
"test": "npx mocha src/**/*.test.js && node ./tools/test.js"
|
||||
"test": "npx mocha src/**/*.test.js && node ./tools/test.js",
|
||||
"build:worker": "cd src/services/worker && npm run build"
|
||||
},
|
||||
"dependencies": {
|
||||
"@anthropic-ai/sdk": "^0.26.1",
|
||||
|
||||
@@ -391,6 +391,9 @@ const install = async ({ services, app, useapi, modapi }) => {
|
||||
|
||||
const { ChatAPIService } = require('./services/ChatAPIService');
|
||||
services.registerService('__chat-api', ChatAPIService);
|
||||
|
||||
const { WorkerService } = require('./services/worker/WorkerService');
|
||||
services.registerService("worker-service", WorkerService)
|
||||
}
|
||||
|
||||
const install_legacy = async ({ services }) => {
|
||||
|
||||
1
src/backend/src/services/worker/.gitignore
vendored
Normal file
1
src/backend/src/services/worker/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
dist/
|
||||
53
src/backend/src/services/worker/README.md
Normal file
53
src/backend/src/services/worker/README.md
Normal file
@@ -0,0 +1,53 @@
|
||||
# Worker Service
|
||||
|
||||
This directory contains the worker service components for Puter's server-to-web (s2w) worker functionality.
|
||||
|
||||
## Build Process
|
||||
|
||||
The `dist/workerPreamble.js` file is **generated** by webpack and c-preprocessor and should not be edited directly. Instead, edit the source files in the `src/` directory and rebuild.
|
||||
|
||||
### Building
|
||||
|
||||
To build the worker preamble:
|
||||
|
||||
```bash
|
||||
# From this directory
|
||||
npm install
|
||||
npm run build
|
||||
```
|
||||
|
||||
Or from the backend root:
|
||||
|
||||
```bash
|
||||
npm run build:worker
|
||||
```
|
||||
|
||||
### Development
|
||||
|
||||
For development with auto-rebuild:
|
||||
|
||||
```bash
|
||||
npm run build:watch
|
||||
```
|
||||
|
||||
This will watch for changes in the source files and automatically rebuild the `workerPreamble.js`.
|
||||
|
||||
## Source Files
|
||||
|
||||
- `template/puter-portable.js` - Puter portable API wrapper
|
||||
- `src/s2w-router.js` - Server-to-web router implementation
|
||||
- `src/index.js` - Main entry point that combines both components
|
||||
|
||||
## Dependencies
|
||||
|
||||
- `path-to-regexp` - URL pattern matching library used by the s2w router
|
||||
|
||||
## Generated Output
|
||||
|
||||
The webpack build process creates `dist/workerPreamble.js` which contains:
|
||||
1. The bundled `path-to-regexp` library
|
||||
2. The puter portable API
|
||||
3. The s2w router with proper initialization
|
||||
4. Initialization code that sets up both systems
|
||||
|
||||
This file is then read by `WorkerService.js` and injected into worker environments.
|
||||
143
src/backend/src/services/worker/WorkerService.js
Normal file
143
src/backend/src/services/worker/WorkerService.js
Normal file
@@ -0,0 +1,143 @@
|
||||
/*
|
||||
* Copyright (C) 2024-present Puter Technologies Inc.
|
||||
*
|
||||
* This file is part of Puter.
|
||||
*
|
||||
* Puter is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published
|
||||
* by the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
const configurable_auth = require("../../middleware/configurable_auth");
|
||||
const { Endpoint } = require("../../util/expressutil");
|
||||
const BaseService = require("../BaseService");
|
||||
const fs = require("node:fs");
|
||||
|
||||
const { createWorker, setCloudflareKeys, deleteWorker } = require("./workerUtils/cloudflareDeploy");
|
||||
const { getUserInfo } = require("./workerUtils/puterUtils");
|
||||
|
||||
// This file is generated by webpack. To rebuild: cd to this directory and run `npm run build`
|
||||
let preamble;
|
||||
try {
|
||||
preamble = fs.readFileSync(__dirname + "/dist/workerPreamble.js", "utf-8");
|
||||
} catch (e) {
|
||||
preamble = "";
|
||||
console.error("WORKERS ERROR: Preamble has not been built! Workers will not have access to puter.js\nTo fix this cd into src/backend/src/worker and run npm run build")
|
||||
}
|
||||
const PREAMBLE_LENGTH = preamble.split("\n").length - 1
|
||||
|
||||
class WorkerService extends BaseService {
|
||||
['__on_install.routes'](_, { app }) {
|
||||
setCloudflareKeys(this.config);
|
||||
|
||||
}
|
||||
static IMPLEMENTS = {
|
||||
['workers']: {
|
||||
async create({ fileData, workerName, authorization }) {
|
||||
try {
|
||||
const userData = await getUserInfo(authorization, this.global_config.api_base_url);
|
||||
return await createWorker(userData, authorization, workerName, preamble + fileData, PREAMBLE_LENGTH);
|
||||
} catch (e) {
|
||||
return {success: false, e}
|
||||
}
|
||||
},
|
||||
async destroy({ workerName, authorization }) {
|
||||
try {
|
||||
const userData = await getUserInfo(authorization, this.global_config.api_base_url);
|
||||
return await deleteWorker(userData, authorization, workerName);
|
||||
} catch (e) {
|
||||
return {success: false, e}
|
||||
}
|
||||
},
|
||||
async startLogs({ workerName, authorization }) {
|
||||
return await this.exec_({ runtime, code });
|
||||
},
|
||||
async endLogs({ workerName, authorization }) {
|
||||
return await this.exec_({ runtime, code });
|
||||
},
|
||||
}
|
||||
}
|
||||
async ['__on_driver.register.interfaces']() {
|
||||
const svc_registry = this.services.get('registry');
|
||||
const col_interfaces = svc_registry.get('interfaces');
|
||||
|
||||
col_interfaces.set('workers', {
|
||||
description: 'Execute code with various languages.',
|
||||
methods: {
|
||||
create: {
|
||||
description: 'Create a backend worker',
|
||||
parameters: {
|
||||
fileData: {
|
||||
type: "string",
|
||||
description: "The code of the worker to upload"
|
||||
},
|
||||
workerName: {
|
||||
type: "string",
|
||||
description: "The name of the worker you want to upload"
|
||||
},
|
||||
authorization: {
|
||||
type: "string",
|
||||
description: "Puter token"
|
||||
}
|
||||
},
|
||||
result: { type: 'json' },
|
||||
},
|
||||
startLogs: {
|
||||
description: 'Get logs for your backend worker',
|
||||
parameters: {
|
||||
workerName: {
|
||||
type: "string",
|
||||
description: "The name of the worker you want the logs of"
|
||||
},
|
||||
authorization: {
|
||||
type: "string",
|
||||
description: "Puter token"
|
||||
}
|
||||
},
|
||||
result: { type: 'json' },
|
||||
},
|
||||
endLogs: {
|
||||
description: 'Get logs for your backend worker',
|
||||
parameters: {
|
||||
workerName: {
|
||||
type: "string",
|
||||
description: "The name of the worker you want the logs of"
|
||||
},
|
||||
authorization: {
|
||||
type: "string",
|
||||
description: "Puter token"
|
||||
}
|
||||
},
|
||||
result: { type: 'json' },
|
||||
},
|
||||
destroy: {
|
||||
description: 'Get rid of your backend worker',
|
||||
parameters: {
|
||||
workerName: {
|
||||
type: "string",
|
||||
description: "The name of the worker you want to destroy"
|
||||
},
|
||||
authorization: {
|
||||
type: "string",
|
||||
description: "Puter token"
|
||||
}
|
||||
},
|
||||
result: { type: 'json' },
|
||||
},
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
WorkerService,
|
||||
};
|
||||
24
src/backend/src/services/worker/package.json
Normal file
24
src/backend/src/services/worker/package.json
Normal file
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"name": "@heyputer/worker-service",
|
||||
"version": "1.0.0",
|
||||
"description": "Worker service components for Puter",
|
||||
"main": "src/index.js",
|
||||
"scripts": {
|
||||
"build": "webpack --mode production && npm run preprocess",
|
||||
"preprocess": "c-preprocessor template/puter-portable.js dist/workerPreamble.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"c-preprocessor": "^0.2.13",
|
||||
"path-to-regexp": "^8.2.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"imports-loader": "^5.0.0",
|
||||
"raw-loader": "^4.0.2",
|
||||
"script-loader": "^0.7.2",
|
||||
"terser-webpack-plugin": "^5.3.14",
|
||||
"webpack": "^5.88.2",
|
||||
"webpack-cli": "^5.1.1"
|
||||
},
|
||||
"author": "Puter Technologies Inc.",
|
||||
"license": "AGPL-3.0-only"
|
||||
}
|
||||
4
src/backend/src/services/worker/src/index.js
Normal file
4
src/backend/src/services/worker/src/index.js
Normal file
@@ -0,0 +1,4 @@
|
||||
import inits2w from './s2w-router.js';
|
||||
// Initialize s2w router
|
||||
inits2w();
|
||||
|
||||
64
src/backend/src/services/worker/src/s2w-router.js
Normal file
64
src/backend/src/services/worker/src/s2w-router.js
Normal file
@@ -0,0 +1,64 @@
|
||||
import { match } from 'path-to-regexp';
|
||||
|
||||
function inits2w() {
|
||||
// s2w router itself: Not part of any package, just a simple router.
|
||||
const s2w = {
|
||||
routing: true,
|
||||
map: new Map(),
|
||||
custom(eventName, route, eventListener) {
|
||||
const matchExp = match(route);
|
||||
if (!this.map.has(eventName)) {
|
||||
this.map.set(eventName, [[matchExp, eventListener]])
|
||||
} else {
|
||||
this.map.get(eventName).push([matchExp, eventListener])
|
||||
}
|
||||
},
|
||||
get(...args) {
|
||||
this.custom("GET", ...args)
|
||||
},
|
||||
post(...args) {
|
||||
this.custom("POST", ...args)
|
||||
},
|
||||
options(...args) {
|
||||
this.custom("OPTIONS", ...args)
|
||||
},
|
||||
put(...args) {
|
||||
this.custom("PUT", ...args)
|
||||
},
|
||||
delete(...args) {
|
||||
this.custom("DELETE", ...args)
|
||||
},
|
||||
async route(event) {
|
||||
if (!globalThis.puter) {
|
||||
console.log("Puter not loaded, initializing...");
|
||||
const success = init_puter_portable(globalThis.puter_auth, globalThis.puter_endpoint || "https://api.puter.com");
|
||||
console.log("Puter.js initialized successfully");
|
||||
}
|
||||
|
||||
const mappings = this.map.get(event.request.method);
|
||||
const url = new URL(event.request.url);
|
||||
try {
|
||||
for (const mapping of mappings) {
|
||||
// return new Response(JSON.stringify(mapping))
|
||||
const results = mapping[0](url.pathname)
|
||||
if (results) {
|
||||
event.params = results.params;
|
||||
return mapping[1](event);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
return new Response(e, {status: 500, statusText: "Server Error"})
|
||||
}
|
||||
|
||||
return new Response("Path not found", {status: 404, statusText: "Not found"});
|
||||
}
|
||||
}
|
||||
globalThis.s2w = s2w;
|
||||
self.addEventListener("fetch", (event)=> {
|
||||
if (!s2w.routing)
|
||||
return false;
|
||||
event.respondWith(s2w.route(event));
|
||||
})
|
||||
}
|
||||
|
||||
export default inits2w;
|
||||
33
src/backend/src/services/worker/template/puter-portable.js
Normal file
33
src/backend/src/services/worker/template/puter-portable.js
Normal file
@@ -0,0 +1,33 @@
|
||||
// This file is not actually in the webpack project, it is handled seperately.
|
||||
|
||||
if (globalThis.Cloudflare) {
|
||||
// Cloudflare Workers has a faulty EventTarget implementation which doesn't bind "this" to the event handler
|
||||
// This is a workaround to bind "this" to the event handler
|
||||
// https://github.com/cloudflare/workerd/issues/4453
|
||||
const __cfEventTarget = EventTarget;
|
||||
globalThis.EventTarget = class EventTarget extends __cfEventTarget {
|
||||
constructor(...args) {
|
||||
super(...args)
|
||||
}
|
||||
addEventListener(type, listener, options) {
|
||||
super.addEventListener(type, listener.bind(this), options);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
globalThis.init_puter_portable = (auth, apiOrigin) => {
|
||||
console.log("Starting puter.js initialization");
|
||||
|
||||
// Who put C in my JS??
|
||||
/*
|
||||
* This is a hack to include the puter.js file.
|
||||
* It is not a good idea to do this, but it is the only way to get the puter.js file to work.
|
||||
* The puter.js file is handled by the C preprocessor here because webpack cant behave with already minified files.
|
||||
* The C preprocessor basically just includes the file and then we can use the puter.js file in the worker.
|
||||
*/
|
||||
#include "../../../../../puter-js/dist/puter.js"
|
||||
puter.setAPIOrigin(apiOrigin);
|
||||
puter.setAuthToken(auth);
|
||||
}
|
||||
#include "../dist/webpackPreamplePart.js"
|
||||
|
||||
57
src/backend/src/services/worker/webpack.config.js
Normal file
57
src/backend/src/services/worker/webpack.config.js
Normal file
@@ -0,0 +1,57 @@
|
||||
const path = require('path');
|
||||
const webpack = require('webpack');
|
||||
|
||||
module.exports = {
|
||||
entry: './src/index.js',
|
||||
output: {
|
||||
path: path.resolve(__dirname, 'dist'),
|
||||
filename: 'webpackPreamplePart.js',
|
||||
library: {
|
||||
type: 'var',
|
||||
name: 'WorkerPreamble'
|
||||
},
|
||||
globalObject: 'this'
|
||||
},
|
||||
mode: 'production',
|
||||
target: 'webworker',
|
||||
resolve: {
|
||||
extensions: ['.js'],
|
||||
},
|
||||
externals: {
|
||||
'https://puter-net.b-cdn.net/rustls.js': 'undefined'
|
||||
},
|
||||
optimization: {
|
||||
minimize: true,
|
||||
minimizer: [
|
||||
new (require('terser-webpack-plugin'))({
|
||||
terserOptions: {
|
||||
keep_fnames: true,
|
||||
mangle: {
|
||||
keep_fnames: true
|
||||
},
|
||||
compress: {
|
||||
keep_fnames: true
|
||||
}
|
||||
}
|
||||
})
|
||||
]
|
||||
},
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
test: /\.js$/,
|
||||
exclude: /puter\.js$/,
|
||||
parser: {
|
||||
dynamicImports: false
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
plugins: [
|
||||
new webpack.BannerPlugin({
|
||||
banner: '// This file is pasted before user code',
|
||||
raw: false,
|
||||
entryOnly: false
|
||||
})
|
||||
]
|
||||
};
|
||||
@@ -0,0 +1,99 @@
|
||||
const fs = require('fs')
|
||||
const { calculateWorkerName } = require("./nameUtils.js");
|
||||
let config = {};
|
||||
// Constants
|
||||
const CF_BASE_URL = "https://api.cloudflare.com/"
|
||||
let WORKERS_BASE_URL;
|
||||
// Workers for Platforms support
|
||||
|
||||
function cfFetch(url, method = "GET", body, givenHeaders) {
|
||||
const headers = { "Authorization": "Bearer " + config["XAUTHKEY"] };
|
||||
if (givenHeaders) {
|
||||
for (const header of givenHeaders) {
|
||||
headers[header[0]] = header[1];
|
||||
}
|
||||
}
|
||||
return fetch(url, { headers, method, body })
|
||||
}
|
||||
async function getWorker(userData, authorization, workerId) {
|
||||
await cfFetch(`${WORKERS_BASE_URL}/scripts/${calculateWorkerName(userData.username, workerId)}`, "GET");
|
||||
}
|
||||
async function createWorker(userData, authorization, workerId, body, PREAMBLE_LENGTH) {
|
||||
console.log(body)
|
||||
const formData = new FormData();
|
||||
|
||||
const workerMetaData = {
|
||||
|
||||
body_part: "swCode",
|
||||
bindings: [
|
||||
{
|
||||
type: "secret_text",
|
||||
name: "puter_auth",
|
||||
text: authorization
|
||||
},
|
||||
{
|
||||
type: "plain_text",
|
||||
name: "puter_endpoint",
|
||||
text: config.internetExposedUrl || "https://api.puter.com"
|
||||
},
|
||||
|
||||
]
|
||||
|
||||
}
|
||||
formData.append("metadata", JSON.stringify(workerMetaData));
|
||||
formData.append("swCode", body);
|
||||
const cfReturnCodes = await (await cfFetch(`${WORKERS_BASE_URL}/scripts/${calculateWorkerName(userData.username, workerId)}/`, "PUT", formData)).json();
|
||||
|
||||
if (cfReturnCodes.success) {
|
||||
return JSON.stringify({ success: true, errors: [], url: `${calculateWorkerName(userData.username, workerId)}.puter.work` });
|
||||
} else {
|
||||
const parsedErrors = [];
|
||||
for (const error of cfReturnCodes.errors) {
|
||||
const message = error.message;
|
||||
let finalMessage = ""
|
||||
const lines = message.split("\n");
|
||||
finalMessage += lines.shift() + "\n"
|
||||
try {
|
||||
// throw new Error("test")
|
||||
for (const line of lines) {
|
||||
if (line.includes("at worker.js:")) {
|
||||
let positions = line.trimStart().replace("at worker.js:", "").split(":");
|
||||
positions[0] = parseInt(positions[0]) - PREAMBLE_LENGTH;
|
||||
finalMessage += ` at worker.js:${positions.join(":")}\n`;
|
||||
} else {
|
||||
finalMessage += line + "\n"
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("Failed to parse V8 Stack trace\n" + message);
|
||||
finalMessage = message;
|
||||
}
|
||||
|
||||
parsedErrors.push(finalMessage)
|
||||
}
|
||||
return JSON.stringify({ success: false, errors: parsedErrors, url: null, body });
|
||||
}
|
||||
}
|
||||
function setPreambleLength(length) {
|
||||
|
||||
}
|
||||
function setCloudflareKeys(givenConfig) {
|
||||
config = givenConfig;
|
||||
WORKERS_BASE_URL = CF_BASE_URL + `client/v4/accounts/${config.ACCOUNTID}/workers`;
|
||||
if (config.namespace) {
|
||||
WORKERS_BASE_URL += `/dispatch/namespaces/${config.namespace}`;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
async function deleteWorker(userData, authorization, workerId) {
|
||||
return await (await cfFetch(`${WORKERS_BASE_URL}/scripts/${calculateWorkerName(userData.username, workerId)}/`, "DELETE")).json();
|
||||
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
createWorker,
|
||||
deleteWorker,
|
||||
getWorker,
|
||||
setCloudflareKeys
|
||||
};
|
||||
14
src/backend/src/services/worker/workerUtils/nameUtils.js
Normal file
14
src/backend/src/services/worker/workerUtils/nameUtils.js
Normal file
@@ -0,0 +1,14 @@
|
||||
// import crypto from 'node:crypto'
|
||||
const crypto = require("node:crypto");
|
||||
|
||||
function sha1(input) {
|
||||
return crypto.createHash('sha1').update(input, 'utf8').digest().toString("hex").slice(0, 7)
|
||||
}
|
||||
function calculateWorkerName(username, workerId) {
|
||||
return `${username}-${sha1(workerId).slice(0, 7)}`
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
sha1,
|
||||
calculateWorkerName
|
||||
}
|
||||
14
src/backend/src/services/worker/workerUtils/puterUtils.js
Normal file
14
src/backend/src/services/worker/workerUtils/puterUtils.js
Normal file
@@ -0,0 +1,14 @@
|
||||
function getUserInfo(authorization, apiBase = "https://puter.com") {
|
||||
return fetch(apiBase + "/whoami", { headers: { authorization, origin: "https://docs.puter.com" } }).then(async res => {
|
||||
if (res.status != 200) {
|
||||
throw ("User data endpoint returned error code " + await res.text());
|
||||
return;
|
||||
}
|
||||
|
||||
return res.json();
|
||||
})
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
getUserInfo
|
||||
}
|
||||
@@ -23,13 +23,15 @@ import { PTLSSocket } from "./modules/networking/PTLS.js"
|
||||
import Threads from './modules/Threads.js';
|
||||
import Perms from './modules/Perms.js';
|
||||
import { pFetch } from './modules/networking/requests.js';
|
||||
import localStorageMemory from './lib/polyfills/localStorage.js'
|
||||
import xhrshim from './lib/polyfills/xhrshim.js'
|
||||
|
||||
// TODO: This is for a safe-guard below; we should check if we can
|
||||
// generalize this behavior rather than hard-coding it.
|
||||
// (using defaultGUIOrigin breaks locally-hosted apps)
|
||||
const PROD_ORIGIN = 'https://puter.com';
|
||||
|
||||
export default window.puter = (function() {
|
||||
export default globalThis.puter = (function() {
|
||||
'use strict';
|
||||
|
||||
class Puter{
|
||||
@@ -123,15 +125,48 @@ export default window.puter = (function() {
|
||||
context.services = this.services;
|
||||
|
||||
// Holds the query parameters found in the current URL
|
||||
let URLParams = new URLSearchParams(window.location.search);
|
||||
let URLParams = new URLSearchParams(globalThis.location?.search);
|
||||
|
||||
// Figure out the environment in which the SDK is running
|
||||
if (URLParams.has('puter.app_instance_id'))
|
||||
this.env = 'app';
|
||||
else if(window.puter_gui_enabled === true)
|
||||
else if(globalThis.puter_gui_enabled === true)
|
||||
this.env = 'gui';
|
||||
else
|
||||
else if (globalThis.WorkerGlobalScope) {
|
||||
if (globalThis.ServiceWorkerGlobalScope) {
|
||||
this.env = 'service-worker'
|
||||
if (!globalThis.XMLHttpRequest) {
|
||||
globalThis.XMLHttpRequest = xhrshim
|
||||
}
|
||||
if (!globalThis.location) {
|
||||
globalThis.location = new URL("https://puter.site/");
|
||||
}
|
||||
// XHRShimGlobalize here
|
||||
} else {
|
||||
this.env = 'web-worker'
|
||||
}
|
||||
if (!globalThis.localStorage) {
|
||||
globalThis.localStorage = localStorageMemory;
|
||||
}
|
||||
} else if (globalThis.process) {
|
||||
this.env = 'nodejs';
|
||||
if (!globalThis.localStorage) {
|
||||
globalThis.localStorage = localStorageMemory;
|
||||
}
|
||||
if (!globalThis.XMLHttpRequest) {
|
||||
globalThis.XMLHttpRequest = xhrshim
|
||||
}
|
||||
if (!globalThis.location) {
|
||||
globalThis.location = new URL("https://nodejs.puter.site/");
|
||||
}
|
||||
if (!globalThis.addEventListener) {
|
||||
globalThis.addEventListener = () => {} // API Stub
|
||||
}
|
||||
} else {
|
||||
this.env = 'web';
|
||||
}
|
||||
|
||||
|
||||
|
||||
// There are some specific situations where puter is definitely loaded in GUI mode
|
||||
// we're going to check for those situations here so that we don't break anything unintentionally
|
||||
@@ -294,6 +329,8 @@ export default window.puter = (function() {
|
||||
// Handle the error here
|
||||
console.error('Error accessing localStorage:', error);
|
||||
}
|
||||
} else if (this.env === 'web-worker' || this.env === 'service-worker' || this.env === 'nodejs') {
|
||||
this.initSubmodules();
|
||||
}
|
||||
|
||||
// Add prefix logger (needed to happen after modules are initialized)
|
||||
@@ -368,7 +405,8 @@ export default window.puter = (function() {
|
||||
const resp = await fetch(this.APIOrigin + '/rao', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: `Bearer ${this.authToken}`
|
||||
Authorization: `Bearer ${this.authToken}`,
|
||||
Origin: location.origin // This is ignored in the browser but needed for workers and nodejs
|
||||
}
|
||||
});
|
||||
return await resp.json();
|
||||
@@ -454,7 +492,7 @@ export default window.puter = (function() {
|
||||
statusCode = 1;
|
||||
}
|
||||
|
||||
window.parent.postMessage({
|
||||
globalThis.parent.postMessage({
|
||||
msg: "exit",
|
||||
appInstanceID: this.appInstanceID,
|
||||
statusCode,
|
||||
@@ -505,7 +543,6 @@ export default window.puter = (function() {
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const xhr = utils.initXhr('/whoami', this.APIOrigin, this.authToken, 'get');
|
||||
|
||||
// set up event handlers for load and error events
|
||||
utils.setupXhrEventHandlers(xhr, options.success, options.error, resolve, reject);
|
||||
|
||||
@@ -548,7 +585,7 @@ export default window.puter = (function() {
|
||||
return puterobj;
|
||||
}());
|
||||
|
||||
window.addEventListener('message', async (event) => {
|
||||
globalThis.addEventListener('message', async (event) => {
|
||||
// if the message is not from Puter, then ignore it
|
||||
if(event.origin !== puter.defaultGUIOrigin) return;
|
||||
|
||||
|
||||
92
src/puter-js/src/lib/polyfills/localStorage.js
Normal file
92
src/puter-js/src/lib/polyfills/localStorage.js
Normal file
@@ -0,0 +1,92 @@
|
||||
// https://github.com/gr2m/localstorage-memory under MIT
|
||||
|
||||
const root = {};
|
||||
var localStorageMemory = {}
|
||||
var cache = {}
|
||||
|
||||
/**
|
||||
* number of stored items.
|
||||
*/
|
||||
localStorageMemory.length = 0
|
||||
|
||||
/**
|
||||
* returns item for passed key, or null
|
||||
*
|
||||
* @para {String} key
|
||||
* name of item to be returned
|
||||
* @returns {String|null}
|
||||
*/
|
||||
localStorageMemory.getItem = function (key) {
|
||||
if (key in cache) {
|
||||
return cache[key]
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* sets item for key to passed value, as String
|
||||
*
|
||||
* @para {String} key
|
||||
* name of item to be set
|
||||
* @para {String} value
|
||||
* value, will always be turned into a String
|
||||
* @returns {undefined}
|
||||
*/
|
||||
localStorageMemory.setItem = function (key, value) {
|
||||
if (typeof value === 'undefined') {
|
||||
localStorageMemory.removeItem(key)
|
||||
} else {
|
||||
if (!(cache.hasOwnProperty(key))) {
|
||||
localStorageMemory.length++
|
||||
}
|
||||
|
||||
cache[key] = '' + value
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* removes item for passed key
|
||||
*
|
||||
* @para {String} key
|
||||
* name of item to be removed
|
||||
* @returns {undefined}
|
||||
*/
|
||||
localStorageMemory.removeItem = function (key) {
|
||||
if (cache.hasOwnProperty(key)) {
|
||||
delete cache[key]
|
||||
localStorageMemory.length--
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* returns name of key at passed index
|
||||
*
|
||||
* @para {Number} index
|
||||
* Position for key to be returned (starts at 0)
|
||||
* @returns {String|null}
|
||||
*/
|
||||
localStorageMemory.key = function (index) {
|
||||
return Object.keys(cache)[index] || null
|
||||
}
|
||||
|
||||
/**
|
||||
* removes all stored items and sets length to 0
|
||||
*
|
||||
* @returns {undefined}
|
||||
*/
|
||||
localStorageMemory.clear = function () {
|
||||
cache = {}
|
||||
localStorageMemory.length = 0
|
||||
}
|
||||
|
||||
if (typeof exports === 'object') {
|
||||
module.exports = localStorageMemory
|
||||
} else {
|
||||
root.localStorage = localStorageMemory
|
||||
}
|
||||
|
||||
|
||||
export default localStorageMemory;
|
||||
|
||||
|
||||
189
src/puter-js/src/lib/polyfills/xhrshim.js
Normal file
189
src/puter-js/src/lib/polyfills/xhrshim.js
Normal file
@@ -0,0 +1,189 @@
|
||||
// https://www.npmjs.com/package/xhr-shim under MIT
|
||||
|
||||
/* global module */
|
||||
/* global EventTarget, AbortController, DOMException */
|
||||
|
||||
const sReadyState = Symbol("readyState");
|
||||
const sHeaders = Symbol("headers");
|
||||
const sRespHeaders = Symbol("response headers");
|
||||
const sAbortController = Symbol("AbortController");
|
||||
const sMethod = Symbol("method");
|
||||
const sURL = Symbol("URL");
|
||||
const sMIME = Symbol("MIME");
|
||||
const sDispatch = Symbol("dispatch");
|
||||
const sErrored = Symbol("errored");
|
||||
const sTimeout = Symbol("timeout");
|
||||
const sTimedOut = Symbol("timedOut");
|
||||
const sIsResponseText = Symbol("isResponseText");
|
||||
|
||||
const XMLHttpRequestShim = class XMLHttpRequest extends EventTarget {
|
||||
onreadystatechange() {
|
||||
|
||||
}
|
||||
|
||||
set readyState(value) {
|
||||
this[sReadyState] = value;
|
||||
this.dispatchEvent(new Event("readystatechange"));
|
||||
this.onreadystatechange(new Event("readystatechange"));
|
||||
|
||||
}
|
||||
get readyState() {
|
||||
return this[sReadyState];
|
||||
}
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.readyState = this.constructor.UNSENT;
|
||||
this.response = null;
|
||||
this.responseType = "";
|
||||
this.responseURL = "";
|
||||
this.status = 0;
|
||||
this.statusText = "";
|
||||
this.timeout = 0;
|
||||
this.withCredentials = false;
|
||||
this[sHeaders] = Object.create(null);
|
||||
this[sHeaders].accept = "*/*";
|
||||
this[sRespHeaders] = Object.create(null);
|
||||
this[sAbortController] = new AbortController();
|
||||
this[sMethod] = "";
|
||||
this[sURL] = "";
|
||||
this[sMIME] = "";
|
||||
this[sErrored] = false;
|
||||
this[sTimeout] = 0;
|
||||
this[sTimedOut] = false;
|
||||
this[sIsResponseText] = true;
|
||||
}
|
||||
static get UNSENT() {
|
||||
return 0;
|
||||
}
|
||||
static get OPENED() {
|
||||
return 1;
|
||||
}
|
||||
static get HEADERS_RECEIVED() {
|
||||
return 2;
|
||||
}
|
||||
static get LOADING() {
|
||||
return 3;
|
||||
}
|
||||
static get DONE() {
|
||||
return 4;
|
||||
}
|
||||
upload = {
|
||||
addEventListener() {
|
||||
// stub, doesn't do anything since its not possible to monitor with fetch and http/1.1
|
||||
}
|
||||
}
|
||||
get responseText() {
|
||||
if (this[sErrored]) return null;
|
||||
if (this.readyState < this.constructor.HEADERS_RECEIVED) return "";
|
||||
if (this[sIsResponseText]) return this.response;
|
||||
throw new DOMException("Response type not set to text", "InvalidStateError");
|
||||
}
|
||||
get responseXML() {
|
||||
throw new Error("XML not supported");
|
||||
}
|
||||
[sDispatch](evt) {
|
||||
const attr = `on${evt.type}`;
|
||||
if (typeof this[attr] === "function") {
|
||||
this.addEventListener(evt.type, this[attr].bind(this), {
|
||||
once: true
|
||||
});
|
||||
}
|
||||
this.dispatchEvent(evt);
|
||||
}
|
||||
abort() {
|
||||
this[sAbortController].abort();
|
||||
this.status = 0;
|
||||
this.readyState = this.constructor.UNSENT;
|
||||
}
|
||||
open(method, url) {
|
||||
this.status = 0;
|
||||
this[sMethod] = method;
|
||||
this[sURL] = url;
|
||||
this.readyState = this.constructor.OPENED;
|
||||
}
|
||||
setRequestHeader(header, value) {
|
||||
header = String(header).toLowerCase();
|
||||
if (typeof this[sHeaders][header] === "undefined") {
|
||||
this[sHeaders][header] = String(value);
|
||||
} else {
|
||||
this[sHeaders][header] += `, ${value}`;
|
||||
}
|
||||
}
|
||||
overrideMimeType(mimeType) {
|
||||
this[sMIME] = String(mimeType);
|
||||
}
|
||||
getAllResponseHeaders() {
|
||||
if (this[sErrored] || this.readyState < this.constructor.HEADERS_RECEIVED) return "";
|
||||
return Array.from(this[sRespHeaders].entries().map(([header, value]) => `${header}: ${value}`)).join("\r\n");
|
||||
}
|
||||
getResponseHeader(headerName) {
|
||||
const value = this[sRespHeaders].get(String(headerName).toLowerCase());
|
||||
return typeof value === "string" ? value : null;
|
||||
}
|
||||
send(body = null) {
|
||||
if (this.timeout > 0) {
|
||||
this[sTimeout] = setTimeout(() => {
|
||||
this[sTimedOut] = true;
|
||||
this[sAbortController].abort();
|
||||
}, this.timeout);
|
||||
}
|
||||
const responseType = this.responseType || "text";
|
||||
this[sIsResponseText] = responseType === "text";
|
||||
|
||||
this.setRequestHeader('user-agent', "puter-js/1.0")
|
||||
this.setRequestHeader('origin', "https://puter.work");
|
||||
this.setRequestHeader('referer', "https://puter.work/");
|
||||
|
||||
fetch(this[sURL], {
|
||||
method: this[sMethod] || "GET",
|
||||
signal: this[sAbortController].signal,
|
||||
headers: this[sHeaders],
|
||||
credentials: this.withCredentials ? "include" : "same-origin",
|
||||
body
|
||||
}).then(async resp => {
|
||||
this.responseURL = resp.url;
|
||||
this.status = resp.status;
|
||||
this.statusText = resp.statusText;
|
||||
this[sRespHeaders] = resp.headers;
|
||||
const finalMIME = this[sMIME] || this[sRespHeaders].get("content-type") || "text/plain";
|
||||
switch (responseType) {
|
||||
case "text":
|
||||
this.response = await resp.text();
|
||||
break;
|
||||
case "blob":
|
||||
this.response = new Blob([await resp.arrayBuffer()], { type: finalMIME });
|
||||
break;
|
||||
case "arraybuffer":
|
||||
this.response = await resp.arrayBuffer();
|
||||
break;
|
||||
case "json":
|
||||
this.response = await resp.json();
|
||||
break;
|
||||
}
|
||||
this.readyState = this.constructor.DONE;
|
||||
this[sDispatch](new CustomEvent("load"));
|
||||
}, err => {
|
||||
let eventName = "abort";
|
||||
if (err.name !== "AbortError") {
|
||||
this[sErrored] = true;
|
||||
eventName = "error";
|
||||
} else if (this[sTimedOut]) {
|
||||
eventName = "timeout";
|
||||
}
|
||||
this.readyState = this.constructor.DONE;
|
||||
this[sDispatch](new CustomEvent(eventName));
|
||||
}).finally(() => this[sDispatch](new CustomEvent("loadend"))).finally(() => {
|
||||
clearTimeout(this[sTimeout]);
|
||||
this[sDispatch](new CustomEvent("loadstart"));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof module === "object" && module.exports) {
|
||||
module.exports = XMLHttpRequestShim;
|
||||
} else {
|
||||
(globalThis || self).XMLHttpRequestShim = XMLHttpRequestShim;
|
||||
}
|
||||
|
||||
export default XMLHttpRequestShim
|
||||
@@ -17,9 +17,9 @@ export class Debug {
|
||||
this.context.puter.logger.on(category);
|
||||
}
|
||||
|
||||
window.addEventListener('message', async e => {
|
||||
globalThis.addEventListener('message', async e => {
|
||||
// Ensure message is from parent window
|
||||
if ( e.source !== window.parent ) return;
|
||||
if ( e.source !== globalThis.parent ) return;
|
||||
// (parent window is allowed to be anything)
|
||||
|
||||
// Check if it's a debug message
|
||||
|
||||
@@ -4,6 +4,10 @@ import path from "../../../lib/path.js"
|
||||
|
||||
const upload = async function(items, dirPath, options = {}){
|
||||
return new Promise(async (resolve, reject) => {
|
||||
const DataTransferItem = globalThis.DataTransfer || (class DataTransferItem {});
|
||||
const FileList = globalThis.FileList || (class FileList {});
|
||||
const DataTransferItemList = globalThis.DataTransferItemList || (class DataTransferItemList {});
|
||||
|
||||
// If auth token is not provided and we are in the web environment,
|
||||
// try to authenticate with Puter
|
||||
if(!puter.authToken && puter.env === 'web'){
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
class PuterDialog extends HTMLElement {
|
||||
class PuterDialog extends (globalThis.HTMLElement || Object) { // It will fall back to only extending Object in environments without a DOM
|
||||
/**
|
||||
* Detects if the current page is loaded using the file:// protocol.
|
||||
* @returns {boolean} True if using file:// protocol, false otherwise.
|
||||
@@ -475,6 +475,7 @@ class PuterDialog extends HTMLElement {
|
||||
this.shadowRoot.querySelector('dialog').close();
|
||||
}
|
||||
}
|
||||
customElements.define('puter-dialog', PuterDialog);
|
||||
if (PuterDialog.__proto__ === globalThis.HTMLElement)
|
||||
customElements.define('puter-dialog', PuterDialog);
|
||||
|
||||
export default PuterDialog;
|
||||
|
||||
@@ -53,7 +53,7 @@ class AppConnection extends EventListener {
|
||||
|
||||
// TODO: Set this.#puterOrigin to the puter origin
|
||||
|
||||
window.addEventListener('message', event => {
|
||||
(globalThis.document) && window.addEventListener('message', event => {
|
||||
if (event.data.msg === 'messageToApp') {
|
||||
if (event.data.appInstanceID !== this.targetAppInstanceID) {
|
||||
// Message is from a different AppConnection; ignore it.
|
||||
@@ -261,7 +261,7 @@ class UI extends EventListener {
|
||||
}, '*');
|
||||
|
||||
// When this app's window is focused send a message to the host environment
|
||||
window.addEventListener('focus', (e) => {
|
||||
(globalThis.document) && window.addEventListener('focus', (e) => {
|
||||
this.messageTarget?.postMessage({
|
||||
msg: "windowFocused",
|
||||
appInstanceID: this.appInstanceID,
|
||||
@@ -270,7 +270,7 @@ class UI extends EventListener {
|
||||
|
||||
// Bind the message event listener to the window
|
||||
let lastDraggedOverElement = null;
|
||||
window.addEventListener('message', async (e) => {
|
||||
(globalThis.document) && window.addEventListener('message', async (e) => {
|
||||
// `error`
|
||||
if(e.data.error){
|
||||
throw e.data.error;
|
||||
@@ -520,7 +520,7 @@ class UI extends EventListener {
|
||||
// and the host environment needs to know the mouse position to show these elements correctly.
|
||||
// The host environment can't just get the mouse position since when the mouse is over an iframe it
|
||||
// will not be able to get the mouse position. So we need to send the mouse position to the host environment.
|
||||
document.addEventListener('mousemove', async (event)=>{
|
||||
globalThis.document?.addEventListener('mousemove', async (event)=>{
|
||||
// Get the mouse position from the event object
|
||||
this.mouseX = event.clientX;
|
||||
this.mouseY = event.clientY;
|
||||
@@ -535,7 +535,7 @@ class UI extends EventListener {
|
||||
});
|
||||
|
||||
// click
|
||||
document.addEventListener('click', async (event)=>{
|
||||
globalThis.document?.addEventListener('click', async (event)=>{
|
||||
// Get the mouse position from the event object
|
||||
this.mouseX = event.clientX;
|
||||
this.mouseY = event.clientY;
|
||||
@@ -563,7 +563,7 @@ class UI extends EventListener {
|
||||
// This should also be done only the very first time the callback is set (hence the if(!this.#onItemsOpened) check) since
|
||||
// the URL parameters will be checked every time the callback is set which can cause problems if the callback is set multiple times.
|
||||
if(!this.#onItemsOpened){
|
||||
let URLParams = new URLSearchParams(window.location.search);
|
||||
let URLParams = new URLSearchParams(globalThis.location.search);
|
||||
if(URLParams.has('puter.item.name') && URLParams.has('puter.item.uid') && URLParams.has('puter.item.read_url')){
|
||||
let fpath = URLParams.get('puter.item.path');
|
||||
|
||||
@@ -591,7 +591,7 @@ class UI extends EventListener {
|
||||
// Check if the app was launched with items
|
||||
// This is useful for apps that are launched with items (e.g. when a file is opened with the app)
|
||||
wasLaunchedWithItems = function() {
|
||||
const URLParams = new URLSearchParams(window.location.search);
|
||||
const URLParams = new URLSearchParams(globalThis.location.search);
|
||||
return URLParams.has('puter.item.name') &&
|
||||
URLParams.has('puter.item.uid') &&
|
||||
URLParams.has('puter.item.read_url');
|
||||
@@ -604,7 +604,7 @@ class UI extends EventListener {
|
||||
// This should also be done only the very first time the callback is set (hence the if(!this.#onLaunchedWithItems) check) since
|
||||
// the URL parameters will be checked every time the callback is set which can cause problems if the callback is set multiple times.
|
||||
if(!this.#onLaunchedWithItems){
|
||||
let URLParams = new URLSearchParams(window.location.search);
|
||||
let URLParams = new URLSearchParams(globalThis.location.search);
|
||||
if(URLParams.has('puter.item.name') && URLParams.has('puter.item.uid') && URLParams.has('puter.item.read_url')){
|
||||
let fpath = URLParams.get('puter.item.path');
|
||||
|
||||
@@ -648,7 +648,10 @@ class UI extends EventListener {
|
||||
}
|
||||
|
||||
showDirectoryPicker = function(options, callback){
|
||||
return new Promise((resolve) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (!globalThis.open) {
|
||||
return reject("This API is not compatible in Web Workers.");
|
||||
}
|
||||
const msg_id = this.#messageID++;
|
||||
if(this.env === 'app'){
|
||||
this.messageTarget?.postMessage({
|
||||
@@ -675,7 +678,10 @@ class UI extends EventListener {
|
||||
}
|
||||
|
||||
showOpenFilePicker = function(options, callback){
|
||||
return new Promise((resolve) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (!globalThis.open) {
|
||||
return reject("This API is not compatible in Web Workers.");
|
||||
}
|
||||
const msg_id = this.#messageID++;
|
||||
|
||||
if(this.env === 'app'){
|
||||
@@ -714,7 +720,10 @@ class UI extends EventListener {
|
||||
}
|
||||
|
||||
showSaveFilePicker = function(content, suggestedName, type){
|
||||
return new Promise((resolve) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (!globalThis.open) {
|
||||
return reject("This API is not compatible in Web Workers.");
|
||||
}
|
||||
const msg_id = this.#messageID++;
|
||||
if ( ! type && Object.prototype.toString.call(content) === '[object URL]' ) {
|
||||
type = 'url';
|
||||
|
||||
@@ -17,7 +17,7 @@ export default class Util {
|
||||
class UtilRPC {
|
||||
constructor () {
|
||||
this.callbackManager = new CallbackManager();
|
||||
this.callbackManager.attach_to_source(window);
|
||||
this.callbackManager.attach_to_source(globalThis);
|
||||
}
|
||||
|
||||
getDehydrator () {
|
||||
|
||||
@@ -42,7 +42,7 @@ export class FilesystemService extends putility.concepts.Service {
|
||||
|
||||
init_app_fs_ () {
|
||||
this.fs_nocache_ = new PostMessageFilesystem({
|
||||
messageTarget: window.parent,
|
||||
messageTarget: globalThis.parent,
|
||||
rpc: this._.context.util.rpc,
|
||||
}).as(TFilesystem);
|
||||
this.filesystem = this.fs_nocache_;
|
||||
|
||||
@@ -1,19 +1,19 @@
|
||||
import putility from "@heyputer/putility";
|
||||
|
||||
/**
|
||||
* Runs commands on the special `window.when_puter_happens` global, for
|
||||
* Runs commands on the special `globalThis.when_puter_happens` global, for
|
||||
* situations where the `puter` global doesn't exist soon enough.
|
||||
*/
|
||||
export class NoPuterYetService extends putility.concepts.Service {
|
||||
_init () {
|
||||
if ( ! window.when_puter_happens ) return;
|
||||
if ( ! globalThis.when_puter_happens ) return;
|
||||
if ( puter && puter.env !== 'gui' ) return;
|
||||
|
||||
if ( ! Array.isArray(window.when_puter_happens) ) {
|
||||
window.when_puter_happens = [window.when_puter_happens];
|
||||
if ( ! Array.isArray(globalThis.when_puter_happens) ) {
|
||||
globalThis.when_puter_happens = [globalThis.when_puter_happens];
|
||||
}
|
||||
|
||||
for ( const fn of window.when_puter_happens ) {
|
||||
for ( const fn of globalThis.when_puter_happens ) {
|
||||
fn({ context: this._.context });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ export class XDIncomingService extends putility.concepts.Service {
|
||||
}
|
||||
|
||||
_init () {
|
||||
window.addEventListener('message', async event => {
|
||||
globalThis.addEventListener('message', async event => {
|
||||
for ( const fn of this.filter_listeners_ ) {
|
||||
const tp = new TeePromise();
|
||||
fn(event, tp);
|
||||
|
||||
Reference in New Issue
Block a user