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:
Neal Shah
2025-07-05 17:01:44 -04:00
committed by GitHub
parent 8fa8dd0987
commit bddc872e00
24 changed files with 874 additions and 32 deletions

View File

@@ -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",

View File

@@ -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 }) => {

View File

@@ -0,0 +1 @@
dist/

View 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.

View 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,
};

View 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"
}

View File

@@ -0,0 +1,4 @@
import inits2w from './s2w-router.js';
// Initialize s2w router
inits2w();

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

View 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"

View 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
})
]
};

View File

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

View 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
}

View 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
}

View File

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

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

View 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

View File

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

View File

@@ -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'){

View File

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

View File

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

View File

@@ -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 () {

View File

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

View File

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

View File

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