Skip to content

Commit bddc872

Browse files
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
1 parent 8fa8dd0 commit bddc872

File tree

24 files changed

+874
-32
lines changed

24 files changed

+874
-32
lines changed

src/backend/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@
44
"description": "Backend/Kernel for Puter",
55
"main": "exports.js",
66
"scripts": {
7-
"test": "npx mocha src/**/*.test.js && node ./tools/test.js"
7+
"test": "npx mocha src/**/*.test.js && node ./tools/test.js",
8+
"build:worker": "cd src/services/worker && npm run build"
89
},
910
"dependencies": {
1011
"@anthropic-ai/sdk": "^0.26.1",

src/backend/src/CoreModule.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -391,6 +391,9 @@ const install = async ({ services, app, useapi, modapi }) => {
391391

392392
const { ChatAPIService } = require('./services/ChatAPIService');
393393
services.registerService('__chat-api', ChatAPIService);
394+
395+
const { WorkerService } = require('./services/worker/WorkerService');
396+
services.registerService("worker-service", WorkerService)
394397
}
395398

396399
const install_legacy = async ({ services }) => {
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
dist/
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
# Worker Service
2+
3+
This directory contains the worker service components for Puter's server-to-web (s2w) worker functionality.
4+
5+
## Build Process
6+
7+
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.
8+
9+
### Building
10+
11+
To build the worker preamble:
12+
13+
```bash
14+
# From this directory
15+
npm install
16+
npm run build
17+
```
18+
19+
Or from the backend root:
20+
21+
```bash
22+
npm run build:worker
23+
```
24+
25+
### Development
26+
27+
For development with auto-rebuild:
28+
29+
```bash
30+
npm run build:watch
31+
```
32+
33+
This will watch for changes in the source files and automatically rebuild the `workerPreamble.js`.
34+
35+
## Source Files
36+
37+
- `template/puter-portable.js` - Puter portable API wrapper
38+
- `src/s2w-router.js` - Server-to-web router implementation
39+
- `src/index.js` - Main entry point that combines both components
40+
41+
## Dependencies
42+
43+
- `path-to-regexp` - URL pattern matching library used by the s2w router
44+
45+
## Generated Output
46+
47+
The webpack build process creates `dist/workerPreamble.js` which contains:
48+
1. The bundled `path-to-regexp` library
49+
2. The puter portable API
50+
3. The s2w router with proper initialization
51+
4. Initialization code that sets up both systems
52+
53+
This file is then read by `WorkerService.js` and injected into worker environments.
Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
/*
2+
* Copyright (C) 2024-present Puter Technologies Inc.
3+
*
4+
* This file is part of Puter.
5+
*
6+
* Puter is free software: you can redistribute it and/or modify
7+
* it under the terms of the GNU Affero General Public License as published
8+
* by the Free Software Foundation, either version 3 of the License, or
9+
* (at your option) any later version.
10+
*
11+
* This program is distributed in the hope that it will be useful,
12+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
13+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14+
* GNU Affero General Public License for more details.
15+
*
16+
* You should have received a copy of the GNU Affero General Public License
17+
* along with this program. If not, see <https://www.gnu.org/licenses/>.
18+
*/
19+
20+
const configurable_auth = require("../../middleware/configurable_auth");
21+
const { Endpoint } = require("../../util/expressutil");
22+
const BaseService = require("../BaseService");
23+
const fs = require("node:fs");
24+
25+
const { createWorker, setCloudflareKeys, deleteWorker } = require("./workerUtils/cloudflareDeploy");
26+
const { getUserInfo } = require("./workerUtils/puterUtils");
27+
28+
// This file is generated by webpack. To rebuild: cd to this directory and run `npm run build`
29+
let preamble;
30+
try {
31+
preamble = fs.readFileSync(__dirname + "/dist/workerPreamble.js", "utf-8");
32+
} catch (e) {
33+
preamble = "";
34+
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")
35+
}
36+
const PREAMBLE_LENGTH = preamble.split("\n").length - 1
37+
38+
class WorkerService extends BaseService {
39+
['__on_install.routes'](_, { app }) {
40+
setCloudflareKeys(this.config);
41+
42+
}
43+
static IMPLEMENTS = {
44+
['workers']: {
45+
async create({ fileData, workerName, authorization }) {
46+
try {
47+
const userData = await getUserInfo(authorization, this.global_config.api_base_url);
48+
return await createWorker(userData, authorization, workerName, preamble + fileData, PREAMBLE_LENGTH);
49+
} catch (e) {
50+
return {success: false, e}
51+
}
52+
},
53+
async destroy({ workerName, authorization }) {
54+
try {
55+
const userData = await getUserInfo(authorization, this.global_config.api_base_url);
56+
return await deleteWorker(userData, authorization, workerName);
57+
} catch (e) {
58+
return {success: false, e}
59+
}
60+
},
61+
async startLogs({ workerName, authorization }) {
62+
return await this.exec_({ runtime, code });
63+
},
64+
async endLogs({ workerName, authorization }) {
65+
return await this.exec_({ runtime, code });
66+
},
67+
}
68+
}
69+
async ['__on_driver.register.interfaces']() {
70+
const svc_registry = this.services.get('registry');
71+
const col_interfaces = svc_registry.get('interfaces');
72+
73+
col_interfaces.set('workers', {
74+
description: 'Execute code with various languages.',
75+
methods: {
76+
create: {
77+
description: 'Create a backend worker',
78+
parameters: {
79+
fileData: {
80+
type: "string",
81+
description: "The code of the worker to upload"
82+
},
83+
workerName: {
84+
type: "string",
85+
description: "The name of the worker you want to upload"
86+
},
87+
authorization: {
88+
type: "string",
89+
description: "Puter token"
90+
}
91+
},
92+
result: { type: 'json' },
93+
},
94+
startLogs: {
95+
description: 'Get logs for your backend worker',
96+
parameters: {
97+
workerName: {
98+
type: "string",
99+
description: "The name of the worker you want the logs of"
100+
},
101+
authorization: {
102+
type: "string",
103+
description: "Puter token"
104+
}
105+
},
106+
result: { type: 'json' },
107+
},
108+
endLogs: {
109+
description: 'Get logs for your backend worker',
110+
parameters: {
111+
workerName: {
112+
type: "string",
113+
description: "The name of the worker you want the logs of"
114+
},
115+
authorization: {
116+
type: "string",
117+
description: "Puter token"
118+
}
119+
},
120+
result: { type: 'json' },
121+
},
122+
destroy: {
123+
description: 'Get rid of your backend worker',
124+
parameters: {
125+
workerName: {
126+
type: "string",
127+
description: "The name of the worker you want to destroy"
128+
},
129+
authorization: {
130+
type: "string",
131+
description: "Puter token"
132+
}
133+
},
134+
result: { type: 'json' },
135+
},
136+
}
137+
});
138+
}
139+
}
140+
141+
module.exports = {
142+
WorkerService,
143+
};
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
{
2+
"name": "@heyputer/worker-service",
3+
"version": "1.0.0",
4+
"description": "Worker service components for Puter",
5+
"main": "src/index.js",
6+
"scripts": {
7+
"build": "webpack --mode production && npm run preprocess",
8+
"preprocess": "c-preprocessor template/puter-portable.js dist/workerPreamble.js"
9+
},
10+
"dependencies": {
11+
"c-preprocessor": "^0.2.13",
12+
"path-to-regexp": "^8.2.0"
13+
},
14+
"devDependencies": {
15+
"imports-loader": "^5.0.0",
16+
"raw-loader": "^4.0.2",
17+
"script-loader": "^0.7.2",
18+
"terser-webpack-plugin": "^5.3.14",
19+
"webpack": "^5.88.2",
20+
"webpack-cli": "^5.1.1"
21+
},
22+
"author": "Puter Technologies Inc.",
23+
"license": "AGPL-3.0-only"
24+
}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
import inits2w from './s2w-router.js';
2+
// Initialize s2w router
3+
inits2w();
4+
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
import { match } from 'path-to-regexp';
2+
3+
function inits2w() {
4+
// s2w router itself: Not part of any package, just a simple router.
5+
const s2w = {
6+
routing: true,
7+
map: new Map(),
8+
custom(eventName, route, eventListener) {
9+
const matchExp = match(route);
10+
if (!this.map.has(eventName)) {
11+
this.map.set(eventName, [[matchExp, eventListener]])
12+
} else {
13+
this.map.get(eventName).push([matchExp, eventListener])
14+
}
15+
},
16+
get(...args) {
17+
this.custom("GET", ...args)
18+
},
19+
post(...args) {
20+
this.custom("POST", ...args)
21+
},
22+
options(...args) {
23+
this.custom("OPTIONS", ...args)
24+
},
25+
put(...args) {
26+
this.custom("PUT", ...args)
27+
},
28+
delete(...args) {
29+
this.custom("DELETE", ...args)
30+
},
31+
async route(event) {
32+
if (!globalThis.puter) {
33+
console.log("Puter not loaded, initializing...");
34+
const success = init_puter_portable(globalThis.puter_auth, globalThis.puter_endpoint || "https://api.puter.com");
35+
console.log("Puter.js initialized successfully");
36+
}
37+
38+
const mappings = this.map.get(event.request.method);
39+
const url = new URL(event.request.url);
40+
try {
41+
for (const mapping of mappings) {
42+
// return new Response(JSON.stringify(mapping))
43+
const results = mapping[0](url.pathname)
44+
if (results) {
45+
event.params = results.params;
46+
return mapping[1](event);
47+
}
48+
}
49+
} catch (e) {
50+
return new Response(e, {status: 500, statusText: "Server Error"})
51+
}
52+
53+
return new Response("Path not found", {status: 404, statusText: "Not found"});
54+
}
55+
}
56+
globalThis.s2w = s2w;
57+
self.addEventListener("fetch", (event)=> {
58+
if (!s2w.routing)
59+
return false;
60+
event.respondWith(s2w.route(event));
61+
})
62+
}
63+
64+
export default inits2w;
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
// This file is not actually in the webpack project, it is handled seperately.
2+
3+
if (globalThis.Cloudflare) {
4+
// Cloudflare Workers has a faulty EventTarget implementation which doesn't bind "this" to the event handler
5+
// This is a workaround to bind "this" to the event handler
6+
// https://github.com/cloudflare/workerd/issues/4453
7+
const __cfEventTarget = EventTarget;
8+
globalThis.EventTarget = class EventTarget extends __cfEventTarget {
9+
constructor(...args) {
10+
super(...args)
11+
}
12+
addEventListener(type, listener, options) {
13+
super.addEventListener(type, listener.bind(this), options);
14+
}
15+
}
16+
}
17+
18+
globalThis.init_puter_portable = (auth, apiOrigin) => {
19+
console.log("Starting puter.js initialization");
20+
21+
// Who put C in my JS??
22+
/*
23+
* This is a hack to include the puter.js file.
24+
* It is not a good idea to do this, but it is the only way to get the puter.js file to work.
25+
* The puter.js file is handled by the C preprocessor here because webpack cant behave with already minified files.
26+
* The C preprocessor basically just includes the file and then we can use the puter.js file in the worker.
27+
*/
28+
#include "../../../../../puter-js/dist/puter.js"
29+
puter.setAPIOrigin(apiOrigin);
30+
puter.setAuthToken(auth);
31+
}
32+
#include "../dist/webpackPreamplePart.js"
33+

0 commit comments

Comments
 (0)