@hybrid-compute/remote 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,70 @@
1
+ {
2
+ "git": false,
3
+ "github": {
4
+ "release": true,
5
+ "tokenRef": "GH_TOKEN"
6
+ },
7
+ "npm": {
8
+ "publish": true,
9
+ "skipChecks": true
10
+ },
11
+ "hooks": {
12
+ "after:release": "echo Successfully released ${name} v${version} to ${repo.repository}."
13
+ },
14
+ "plugins": {
15
+ "@release-it/bumper": {
16
+ "out": {
17
+ "file": "package.json",
18
+ "path": ["dependencies.@hybrid-compute/core"]
19
+ }
20
+ },
21
+ "@release-it/conventional-changelog": {
22
+ "header": "# Changelog",
23
+ "preset": {
24
+ "name": "conventionalcommits",
25
+ "types": [
26
+ {
27
+ "type": "chore",
28
+ "section": "Tasks"
29
+ },
30
+ {
31
+ "type": "docs",
32
+ "section": "Documentation"
33
+ },
34
+ {
35
+ "type": "feat",
36
+ "section": "Feature"
37
+ },
38
+ {
39
+ "type": "fix",
40
+ "section": "Bug"
41
+ },
42
+ {
43
+ "type": "perf",
44
+ "section": "Performance change"
45
+ },
46
+ {
47
+ "type": "refactor",
48
+ "section": "Refactoring"
49
+ },
50
+ {
51
+ "type": "release",
52
+ "section": "Create a release commit",
53
+ "hidden": true
54
+ },
55
+ {
56
+ "type": "style",
57
+ "section": "Markup, white-space, formatting, missing semi-colons...",
58
+ "hidden": true
59
+ },
60
+ {
61
+ "type": "test",
62
+ "section": "Adding missing tests",
63
+ "hidden": true
64
+ }
65
+ ]
66
+ },
67
+ "infile": "CHANGELOG.md"
68
+ }
69
+ }
70
+ }
package/README.md ADDED
@@ -0,0 +1,88 @@
1
+ # @hybrid-compute/remote
2
+
3
+ [![Commitizen friendly](https://img.shields.io/badge/commitizen-friendly-brightgreen.svg)](http://commitizen.github.io/cz-cli/)
4
+ [![PRs Welcome](https://img.shields.io/badge/PRs-welcome-green.svg)](http://makeapullrequest.com)
5
+ [![SemVer 2.0](https://img.shields.io/badge/SemVer-2.0-green.svg)](http://semver.org/spec/v2.0.0.html)
6
+ ![npm version](https://img.shields.io/npm/v/@hybrid-compute/core)
7
+ ![issues](https://img.shields.io/github/issues/phun-ky/hybrid-compute)
8
+ ![license](https://img.shields.io/npm/l/@hybrid-compute/core)
9
+ ![size](https://img.shields.io/bundlephobia/min/@hybrid-compute/core)
10
+ ![npm](https://img.shields.io/npm/dm/%40hybrid-compute/core)
11
+ ![GitHub Repo stars](https://img.shields.io/github/stars/phun-ky/hybrid-compute)
12
+ [![codecov](https://codecov.io/gh/phun-ky/hybrid-compute/graph/badge.svg?token=VA91DL7ZLZ)](https://codecov.io/gh/phun-ky/hybrid-compute)
13
+ [![build](https://github.com/phun-ky/hybrid-compute/actions/workflows/check.yml/badge.svg)](https://github.com/phun-ky/hybrid-compute/actions/workflows/check.yml)
14
+
15
+ Part of the [`@hybrid-compute`](https://github.com/phun-ky/hybrid-compute)
16
+ monorepo.
17
+
18
+ > See the [main README](https://github.com/phun-ky/hybrid-compute#readme) for
19
+ > full project overview, usage examples, architecture, and contribution
20
+ > guidelines.
21
+
22
+ ## API Docs
23
+
24
+ [RemoteCompute API Documentation](https://github.com/phun-ky/hybrid-compute/blob/main/docs/api/remote/src/classes/RemoteCompute.md)
25
+
26
+ ## 📦 Package Info
27
+
28
+ This package provides:
29
+
30
+ - A remote compute backend that delegates tasks over HTTP or WebSocket
31
+ - Transport-agnostic JSON message protocol
32
+ - Suitable for offloading work to distributed compute services
33
+
34
+ ## Usage
35
+
36
+ ```bash
37
+ npm install @hybrid-compute/remote
38
+ ```
39
+
40
+ ```ts
41
+ import { createRemoteCompute } from '@hybrid-compute/remote';
42
+
43
+ const remote = createRemoteCompute({
44
+ transport: 'fetch',
45
+ endpoint: '/api/compute'
46
+ });
47
+ ```
48
+
49
+ You can see
50
+ [examples here](https://github.com/phun-ky/hybrid-compute/blob/main/docs/api/remote/docs/example.md),
51
+ and a
52
+ [testing guide here](https://github.com/phun-ky/hybrid-compute/blob/main/docs/api/remote/docs/testing.md)
53
+
54
+ ---
55
+
56
+ ## Contributing
57
+
58
+ Want to contribute? Please read the
59
+ [CONTRIBUTING.md](https://github.com/phun-ky/hybrid-compute/blob/main/CONTRIBUTING.md)
60
+ and
61
+ [CODE_OF_CONDUCT.md](https://github.com/phun-ky/hybrid-compute/blob/main/CODE_OF_CONDUCT.md)
62
+
63
+ ## License
64
+
65
+ This project is licensed under the MIT License - see the
66
+ [LICENSE](https://github.com/phun-ky/hybrid-compute/blob/main/LICENSE) file for
67
+ details.
68
+
69
+ ## Sponsor me
70
+
71
+ I'm an Open Source evangelist, creating stuff that does not exist yet to help
72
+ get rid of secondary activities and to enhance systems already in place, be it
73
+ documentation, tools or web sites.
74
+
75
+ The sponsorship is an unique opportunity to alleviate more hours for me to
76
+ maintain my projects, create new ones and contribute to the large community
77
+ we're all part of :)
78
+
79
+ [Support me on GitHub Sponsors](https://github.com/sponsors/phun-ky).
80
+
81
+ p.s. **Ukraine is still under brutal Russian invasion. A lot of Ukrainian people
82
+ are hurt, without shelter and need help**. You can help in various ways, for
83
+ instance, directly helping refugees, spreading awareness, putting pressure on
84
+ your local government or companies. You can also support Ukraine by donating
85
+ e.g. to [Red Cross](https://www.icrc.org/en/donate/ukraine),
86
+ [Ukraine humanitarian organisation](https://savelife.in.ua/en/donate-en/#donate-army-card-weekly)
87
+ or
88
+ [donate Ambulances for Ukraine](https://www.gofundme.com/f/help-to-save-the-lives-of-civilians-in-a-war-zone).
@@ -0,0 +1,154 @@
1
+ # RemoteCompute Usage Guide
2
+
3
+ This guide explains how to use the `RemoteCompute` backend for remote task
4
+ execution via `fetch` or `WebSocket`, and also illustrates how to implement the
5
+ server side for each transport.
6
+
7
+ ## Table of Contents<!-- omit from toc -->
8
+
9
+ - [RemoteCompute Usage Guide](#remotecompute-usage-guide)
10
+ - [Overview](#overview)
11
+ - [Client-Side Usage](#client-side-usage)
12
+ - [1. Using `fetch` Transport](#1-using-fetch-transport)
13
+ - [2. Using `websocket` Transport](#2-using-websocket-transport)
14
+ - [Backend Implementation](#backend-implementation)
15
+ - [1. Backend for Fetch (Node.js/Express)](#1-backend-for-fetch-nodejsexpress)
16
+ - [2. Backend for WebSocket (Node.js/ws)](#2-backend-for-websocket-nodejsws)
17
+ - [Best Practices](#best-practices)
18
+ - [Troubleshooting](#troubleshooting)
19
+ - [References](#references)
20
+
21
+ ---
22
+
23
+ ## Overview
24
+
25
+ `RemoteCompute` allows clients to send computation tasks to a remote service
26
+ using one of two transport protocols:
27
+
28
+ - `fetch`: HTTP POST requests
29
+ - `websocket`: Persistent WebSocket connection
30
+
31
+ Each task must be known to the client (optionally restricted via `canRunTasks`)
32
+ and supported by the server.
33
+
34
+ ---
35
+
36
+ ## Client-Side Usage
37
+
38
+ ### 1. Using `fetch` Transport
39
+
40
+ ```ts
41
+ import { createRemoteCompute } from '@hybrid-compute/remote';
42
+
43
+ const remote = createRemoteCompute({
44
+ transport: 'fetch',
45
+ endpoint: 'https://api.example.com/compute',
46
+ canRunTasks: ['generatePDF']
47
+ });
48
+
49
+ const result = await remote.runTask('generatePDF', {
50
+ content: 'Hello, world!'
51
+ });
52
+ console.log(result);
53
+ ```
54
+
55
+ ### 2. Using `websocket` Transport
56
+
57
+ ```ts
58
+ import { createRemoteCompute } from '@hybrid-compute/remote';
59
+
60
+ const remote = createRemoteCompute({
61
+ transport: 'websocket',
62
+ endpoint: 'wss://api.example.com/ws',
63
+ canRunTasks: ['summarizeText']
64
+ });
65
+
66
+ const result = await remote.runTask('summarizeText', {
67
+ text: 'This is a long text...'
68
+ });
69
+ console.log(result);
70
+ ```
71
+
72
+ ---
73
+
74
+ ## Backend Implementation
75
+
76
+ ### 1. Backend for Fetch (Node.js/Express)
77
+
78
+ ```ts
79
+ import express from 'express';
80
+ const app = express();
81
+ app.use(express.json());
82
+
83
+ const handlers = {
84
+ generatePDF: async ({ content }) => {
85
+ // simulate PDF generation
86
+ return `PDF with content: ${content}`;
87
+ }
88
+ };
89
+
90
+ app.post('/compute', async (req, res) => {
91
+ const { task, input } = req.body;
92
+ try {
93
+ if (!handlers[task]) throw new Error('Unknown task');
94
+ const result = await handlers[task](input);
95
+ res.json({ result });
96
+ } catch (error) {
97
+ res.json({ error: error.message });
98
+ }
99
+ });
100
+
101
+ app.listen(3000);
102
+ ```
103
+
104
+ ### 2. Backend for WebSocket (Node.js/ws)
105
+
106
+ ```ts
107
+ import { WebSocketServer } from 'ws';
108
+
109
+ const wss = new WebSocketServer({ port: 8080 });
110
+
111
+ const handlers = {
112
+ summarizeText: async ({ text }) => {
113
+ return `Summary: ${text.slice(0, 20)}...`;
114
+ }
115
+ };
116
+
117
+ wss.on('connection', (ws) => {
118
+ ws.on('message', async (message) => {
119
+ const { task, input, id } = JSON.parse(message.toString());
120
+ try {
121
+ if (!handlers[task]) throw new Error('Unknown task');
122
+ const result = await handlers[task](input);
123
+ ws.send(JSON.stringify({ id, result }));
124
+ } catch (error) {
125
+ ws.send(JSON.stringify({ id, error: error.message }));
126
+ }
127
+ });
128
+ });
129
+ ```
130
+
131
+ ---
132
+
133
+ ## Best Practices
134
+
135
+ - Validate task names on both ends
136
+ - Enforce authentication and authorization for remote compute endpoints
137
+ - In WebSocket mode, monitor socket health and auto-reconnect if needed
138
+ - Always wrap remote responses with task `id` for proper correlation
139
+
140
+ ---
141
+
142
+ ## Troubleshooting
143
+
144
+ | Issue | Solution |
145
+ | ------------------------------- | --------------------------------------------------------------- |
146
+ | `WebSocket not connected` error | Ensure WebSocket is open before sending messages |
147
+ | Task not found | Confirm the task name is registered and listed in `canRunTasks` |
148
+ | JSON parse error | Ensure backend and client speak the same message protocol |
149
+
150
+ ## References
151
+
152
+ - [MDN: fetch API](https://developer.mozilla.org/en-US/docs/Web/API/fetch)
153
+ - [MDN: WebSocket API](https://developer.mozilla.org/en-US/docs/Web/API/WebSocket)
154
+ - [WebSocket on NPM](https://www.npmjs.com/package/ws)
@@ -0,0 +1,132 @@
1
+ # RemoteCompute Test Harness & CURL Examples
2
+
3
+ This guide provides a simple test harness and `curl` commands to validate that
4
+ your remote backend endpoints (both fetch and websocket) are working correctly.
5
+
6
+ ## Table of Contents<!-- omit from toc -->
7
+
8
+ - [RemoteCompute Test Harness \& CURL Examples](#remotecompute-test-harness--curl-examples)
9
+ - [HTTP `fetch` Test Harness (Node.js)](#http-fetch-test-harness-nodejs)
10
+ - [File: `test-fetch.js`](#file-test-fetchjs)
11
+ - [CURL Test: `fetch`](#curl-test-fetch)
12
+ - [WebSocket Test Harness (Node.js)](#websocket-test-harness-nodejs)
13
+ - [File: `test-websocket.js`](#file-test-websocketjs)
14
+ - [WebSocket Manual Testing (via `wscat`)](#websocket-manual-testing-via-wscat)
15
+ - [What to Verify](#what-to-verify)
16
+ - [Security Tip](#security-tip)
17
+
18
+ ---
19
+
20
+ ## HTTP `fetch` Test Harness (Node.js)
21
+
22
+ ### File: `test-fetch.js`
23
+
24
+ ```js
25
+ const taskName = 'generatePDF';
26
+ const input = { content: 'Hello, PDF World!' };
27
+
28
+ async function run() {
29
+ const res = await fetch('http://localhost:3000/compute', {
30
+ method: 'POST',
31
+ headers: { 'Content-Type': 'application/json' },
32
+ body: JSON.stringify({ task: taskName, input })
33
+ });
34
+
35
+ const json = await res.json();
36
+ console.log('Response:', json);
37
+ }
38
+
39
+ run().catch(console.error);
40
+ ```
41
+
42
+ ### CURL Test: `fetch`
43
+
44
+ ```bash
45
+ curl -X POST http://localhost:3000/compute \
46
+ -H "Content-Type: application/json" \
47
+ -d '{"task": "generatePDF", "input": { "content": "Hello from CURL" }}'
48
+ ```
49
+
50
+ Expected output:
51
+
52
+ ```json
53
+ { "result": "PDF with content: Hello from CURL" }
54
+ ```
55
+
56
+ ## WebSocket Test Harness (Node.js)
57
+
58
+ ### File: `test-websocket.js`
59
+
60
+ ```js
61
+ import WebSocket from 'ws';
62
+
63
+ const socket = new WebSocket('ws://localhost:8080');
64
+ const taskId = 1;
65
+
66
+ socket.onopen = () => {
67
+ socket.send(
68
+ JSON.stringify({
69
+ task: 'summarizeText',
70
+ input: {
71
+ text: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit.'
72
+ },
73
+ id: taskId
74
+ })
75
+ );
76
+ };
77
+
78
+ socket.onmessage = (event) => {
79
+ const data = JSON.parse(event.data);
80
+ console.log('WS response:', data);
81
+ socket.close();
82
+ };
83
+
84
+ socket.onerror = (err) => {
85
+ console.error('WebSocket error:', err);
86
+ };
87
+ ```
88
+
89
+ ### WebSocket Manual Testing (via `wscat`)
90
+
91
+ You can use `wscat` from the terminal:
92
+
93
+ ```bash
94
+ npm install -g wscat
95
+ wscat -c ws://localhost:8080
96
+ ```
97
+
98
+ Once connected, send this JSON payload:
99
+
100
+ ```json
101
+ {
102
+ "task": "summarizeText",
103
+ "input": { "text": "WebSocket test with a longer text to summarize" },
104
+ "id": 42
105
+ }
106
+ ```
107
+
108
+ Expected response:
109
+
110
+ ```json
111
+ {
112
+ "id": 42,
113
+ "result": "Summary: WebSocket test with..."
114
+ }
115
+ ```
116
+
117
+ ## What to Verify
118
+
119
+ | Test | Expected |
120
+ | ----------------- | ----------------------------------------------- |
121
+ | fetch POST | JSON response with `"result"` or `"error"` |
122
+ | WebSocket send | JSON message with matching `"id"` and result |
123
+ | Unregistered task | `"error": "Unknown task"` |
124
+ | Missing input | backend should throw or return error gracefully |
125
+
126
+ ## Security Tip
127
+
128
+ In production, always validate:
129
+
130
+ - Origin and authentication headers
131
+ - Rate limits
132
+ - Schema of incoming requests (e.g. using Zod or Yup)
@@ -0,0 +1,3 @@
1
+ import SharedConfig from '../../eslint.config.mjs';
2
+
3
+ export default SharedConfig;
package/package.json ADDED
@@ -0,0 +1,54 @@
1
+ {
2
+ "name": "@hybrid-compute/remote",
3
+ "version": "0.0.1",
4
+ "description": "Remote compute backend using fetch or WebSocket transport for distributed task execution.",
5
+ "keywords": [
6
+ "remote",
7
+ "compute",
8
+ "websocket",
9
+ "fetch",
10
+ "transport",
11
+ "distributed",
12
+ "json-rpc",
13
+ "http",
14
+ "cloud",
15
+ "task"
16
+ ],
17
+ "homepage": "https://phun-ky.net/projects/hybrid-compute",
18
+ "bugs": {
19
+ "url": "https://github.com/phun-ky/hybrid-compute/issues"
20
+ },
21
+ "repository": {
22
+ "type": "git",
23
+ "url": "git+https://github.com/phun-ky/hybrid-compute.git"
24
+ },
25
+ "funding": "https://github.com/phun-ky/hybrid-compute?sponsor=1",
26
+ "license": "MIT",
27
+ "author": "Alexander Vassbotn Røyne-Helgesen <alexander@phun-ky.net>",
28
+ "type": "module",
29
+ "exports": "./dist/index.js",
30
+ "types": "./dist/index.d.ts",
31
+ "scripts": {
32
+ "release": "release-it",
33
+ "clean": "rm -rf dist tsconfig.tsbuildinfo",
34
+ "test": "tsx --test **/__tests__/**/*.spec.ts",
35
+ "pretest:ci": "rm -rf coverage && mkdir -p coverage",
36
+ "test:ci": "glob -c \"node --import tsx --test --no-warnings --experimental-test-coverage --test-reporter=cobertura --test-reporter-destination=coverage/cobertura-coverage.xml --test-reporter=spec --test-reporter-destination=stdout\" \"**/__tests__/**/*.spec.ts\""
37
+ },
38
+ "dependencies": {
39
+ "@hybrid-compute/core": "0.0.1"
40
+ },
41
+ "devDependencies": {
42
+ "eslint": "^9.27.0",
43
+ "eslint-config-phun-ky": "^1.0.3",
44
+ "prettier": "^3.5.3",
45
+ "typescript": "^5.8.3"
46
+ },
47
+ "engines": {
48
+ "node": ">=22.0.0",
49
+ "npm": ">=10.8.2"
50
+ },
51
+ "publishConfig": {
52
+ "access": "public"
53
+ }
54
+ }
@@ -0,0 +1,176 @@
1
+ import test, { describe } from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import { createRemoteCompute, RemoteCompute } from '..';
4
+ globalThis.fetch = async (
5
+ input: RequestInfo | URL,
6
+ init?: RequestInit
7
+ ): Promise<Response> => {
8
+ const body = JSON.parse((init?.body as string) ?? '{}');
9
+
10
+ const isError = body.task === 'fail';
11
+
12
+ return {
13
+ ok: !isError,
14
+ status: isError ? 400 : 200,
15
+ headers: new Headers(),
16
+ redirected: false,
17
+ statusText: isError ? 'Bad Request' : 'OK',
18
+ type: 'basic',
19
+ url: String(input),
20
+ clone: function () {
21
+ return this;
22
+ },
23
+ body: null,
24
+ bodyUsed: true,
25
+ arrayBuffer: async () => new ArrayBuffer(0),
26
+ blob: async () => new Blob(),
27
+ formData: async () => new FormData(),
28
+ text: async () =>
29
+ JSON.stringify(
30
+ isError
31
+ ? { error: 'Task failed' }
32
+ : { result: `Result for ${body.task}` }
33
+ ),
34
+ json: async () =>
35
+ isError ? { error: 'Task failed' } : { result: `Result for ${body.task}` }
36
+ } as Response;
37
+ };
38
+
39
+ describe('RemoteCompute', () => {
40
+ test('fetch transport returns result from remote', async () => {
41
+ const compute = new RemoteCompute({
42
+ transport: 'fetch',
43
+ endpoint: 'https://api.example.com/compute'
44
+ });
45
+
46
+ const result = await compute.runTask('echo', { message: 'hi' });
47
+ assert.equal(result, 'Result for echo');
48
+ });
49
+
50
+ test('fetch transport throws on error response', async () => {
51
+ const compute = new RemoteCompute({
52
+ transport: 'fetch',
53
+ endpoint: 'https://api.example.com/compute'
54
+ });
55
+
56
+ await assert.rejects(() => compute.runTask('fail', {}), /Task failed/);
57
+ });
58
+
59
+ test('canRun returns true if task is allowed', () => {
60
+ const compute = new RemoteCompute({
61
+ transport: 'fetch',
62
+ endpoint: '/compute',
63
+ canRunTasks: ['foo', 'bar']
64
+ });
65
+
66
+ assert.equal(compute.canRun('foo'), true);
67
+ assert.equal(compute.canRun('baz'), false);
68
+ });
69
+
70
+ test('canRun returns true if canRunTasks is undefined', () => {
71
+ const compute = new RemoteCompute({
72
+ transport: 'fetch',
73
+ endpoint: '/compute'
74
+ });
75
+
76
+ assert.equal(compute.canRun('anything'), true);
77
+ });
78
+
79
+ test(
80
+ 'WebSocket transport sends and resolves a message',
81
+ { timeout: 200 },
82
+ async () => {
83
+ const OriginalWebSocket = globalThis.WebSocket;
84
+ let sentData = '';
85
+
86
+ class MockWebSocket {
87
+ public readyState = WebSocket.OPEN;
88
+ public onmessage: ((event: MessageEvent) => void) | null = null;
89
+
90
+ constructor(public url: string) {}
91
+
92
+ send(data: string) {
93
+ sentData = data;
94
+ const { id } = JSON.parse(data);
95
+ const fakeResponse = { id, result: 'websocket-result' };
96
+ setTimeout(() => {
97
+ this.onmessage?.({
98
+ data: JSON.stringify(fakeResponse)
99
+ } as MessageEvent);
100
+ }, 10);
101
+ }
102
+ }
103
+
104
+ // @ts-expect-error override native WebSocket
105
+ globalThis.WebSocket = MockWebSocket;
106
+
107
+ try {
108
+ const compute = new RemoteCompute({
109
+ transport: 'websocket',
110
+ endpoint: 'wss://example.com/ws'
111
+ });
112
+
113
+ // Wait a tick before calling runTask (mimics real socket delay)
114
+ await new Promise((resolve) => setTimeout(resolve, 0));
115
+
116
+ const result = await compute.runTask('test', { foo: 1 });
117
+ assert.equal(result, 'websocket-result');
118
+ } finally {
119
+ globalThis.WebSocket = OriginalWebSocket;
120
+ }
121
+ }
122
+ );
123
+
124
+ test('WebSocket rejects if not connected', async () => {
125
+ const OriginalWebSocket = globalThis.WebSocket;
126
+
127
+ class MockWebSocket {
128
+ public readyState = 3; // WebSocket.CLOSED
129
+ public onmessage: ((event: MessageEvent) => void) | null = null;
130
+
131
+ constructor(public url: string) {
132
+ console.log('[MOCK] MockWebSocket constructor called');
133
+ }
134
+
135
+ send() {
136
+ console.log('[MOCK] send() should not be called');
137
+ }
138
+ }
139
+
140
+ // @ts-expect-error override
141
+ globalThis.WebSocket = MockWebSocket;
142
+
143
+ try {
144
+ const compute = new RemoteCompute({
145
+ transport: 'websocket',
146
+ endpoint: 'wss://example.com/ws'
147
+ });
148
+
149
+ const resultPromise = compute.runTask('any', {});
150
+ await assert.rejects(resultPromise, /WebSocket not connected/);
151
+ } finally {
152
+ globalThis.WebSocket = OriginalWebSocket;
153
+ }
154
+ });
155
+
156
+ test('runTask() manually rejects for testing', async () => {
157
+ const compute = {
158
+ runTask: () => Promise.reject(new Error('WebSocket not connected'))
159
+ } as unknown as RemoteCompute;
160
+
161
+ await assert.rejects(
162
+ () => compute.runTask('x', {}),
163
+ /WebSocket not connected/
164
+ );
165
+ });
166
+ });
167
+ describe('createRemoteCompute', () => {
168
+ test('returns instance of RemoteCompute', () => {
169
+ const instance = createRemoteCompute({
170
+ transport: 'fetch',
171
+ endpoint: '/compute'
172
+ });
173
+
174
+ assert.ok(instance instanceof RemoteCompute);
175
+ });
176
+ });
package/src/index.ts ADDED
@@ -0,0 +1,145 @@
1
+ /* eslint-disable @typescript-eslint/no-explicit-any */
2
+ import { ComputeBackendInterface } from '@hybrid-compute/core';
3
+
4
+ import { RemoteComputeOptionsInterface, RemoteTransportType } from './types.js';
5
+
6
+ export * from './types.js';
7
+
8
+ /**
9
+ * RemoteCompute is a backend that delegates compute tasks to a remote API
10
+ * using either HTTP requests (fetch) or a persistent WebSocket connection.
11
+ *
12
+ * It supports bidirectional communication, which is useful for low-latency
13
+ * or streaming scenarios using WebSocket, or traditional stateless interaction
14
+ * using fetch.
15
+ *
16
+ * @remarks
17
+ * WebSocket-based transport allows concurrent request handling via an internal
18
+ * request/response map using `id`. This is useful when running multiple tasks in parallel.
19
+ *
20
+ * Fetch transport is simpler and more interoperable with typical REST APIs.
21
+ *
22
+ * @example Fetch transport
23
+ * ```ts
24
+ * const remote = new RemoteCompute({
25
+ * transport: 'fetch',
26
+ * endpoint: 'https://api.example.com/compute',
27
+ * canRunTasks: ['translateText']
28
+ * });
29
+ *
30
+ * const result = await remote.runTask('translateText', { text: 'hello' });
31
+ * ```
32
+ *
33
+ * @example WebSocket transport
34
+ * ```ts
35
+ * const remote = new RemoteCompute({
36
+ * transport: 'websocket',
37
+ * endpoint: 'wss://api.example.com/ws',
38
+ * canRunTasks: ['analyzeSentiment']
39
+ * });
40
+ *
41
+ * const result = await remote.runTask('analyzeSentiment', { text: 'It works!' });
42
+ * ```
43
+ *
44
+ * @see https://developer.mozilla.org/en-US/docs/Web/API/fetch
45
+ * @see https://developer.mozilla.org/en-US/docs/Web/API/WebSocket
46
+ */
47
+ export class RemoteCompute implements ComputeBackendInterface {
48
+ private transport: RemoteTransportType;
49
+ private endpoint: string;
50
+ private canRunSet: Set<string>;
51
+ private socket?: WebSocket;
52
+ private pending = new Map<
53
+ number,
54
+ {
55
+ resolve: (value: any | PromiseLike<any>) => void;
56
+ reject: (value: any | PromiseLike<any>) => void;
57
+ }
58
+ >();
59
+ private nextId = 1;
60
+
61
+ /**
62
+ * Initializes the remote compute backend.
63
+ *
64
+ * @param options - Transport type and endpoint configuration.
65
+ */
66
+ constructor(options: RemoteComputeOptionsInterface) {
67
+ this.transport = options.transport;
68
+ this.endpoint = options.endpoint;
69
+ this.canRunSet = new Set(options.canRunTasks ?? []);
70
+
71
+ if (this.transport === 'websocket') {
72
+ this.socket = new WebSocket(this.endpoint);
73
+
74
+ this.socket.onmessage = (event: MessageEvent) => {
75
+ const { id, result, error } = JSON.parse(event.data);
76
+
77
+ if (error) this.pending.get(id)?.reject(error);
78
+ else this.pending.get(id)?.resolve(result);
79
+
80
+ this.pending.delete(id);
81
+ };
82
+ }
83
+ }
84
+
85
+ /**
86
+ * Determines if this backend is allowed to handle the given task.
87
+ *
88
+ * @param taskName - Name of the task.
89
+ * @returns `true` if task is permitted, or if no restrictions are set.
90
+ */
91
+ canRun(taskName: string): boolean {
92
+ return this.canRunSet.size === 0 || this.canRunSet.has(taskName);
93
+ }
94
+
95
+ /**
96
+ * Executes the specified task using remote communication.
97
+ *
98
+ * @typeParam Input - The input data structure expected by the task.
99
+ * @typeParam Output - The output structure returned by the task.
100
+ *
101
+ * @param taskName - Name of the remote task.
102
+ * @param input - Input data to send.
103
+ * @returns A promise resolving to the result from the server.
104
+ */
105
+ async runTask<Input, Output>(
106
+ taskName: string,
107
+ input: Input
108
+ ): Promise<Output> {
109
+ const id = this.nextId++;
110
+
111
+ if (this.transport === 'fetch') {
112
+ const response = await fetch(this.endpoint, {
113
+ method: 'POST',
114
+ headers: { 'Content-Type': 'application/json' },
115
+ body: JSON.stringify({ task: taskName, input })
116
+ });
117
+ const { result, error } = await response.json();
118
+
119
+ if (error) throw new Error(error);
120
+
121
+ return result;
122
+ }
123
+
124
+ return new Promise<Output>((resolve, reject) => {
125
+ if (!this.socket || this.socket.readyState !== WebSocket.OPEN) {
126
+ reject(new Error('WebSocket not connected'));
127
+
128
+ return;
129
+ }
130
+
131
+ this.pending.set(id, { resolve, reject });
132
+ this.socket.send(JSON.stringify({ task: taskName, input, id }));
133
+ });
134
+ }
135
+ }
136
+
137
+ /**
138
+ * Factory to create a RemoteCompute instance with given options.
139
+ *
140
+ * @param options - Remote connection configuration.
141
+ * @returns Instance of RemoteCompute.
142
+ */
143
+ export function createRemoteCompute(options: RemoteComputeOptionsInterface) {
144
+ return new RemoteCompute(options);
145
+ }
package/src/types.ts ADDED
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Represents the communication method used by the `RemoteCompute` backend
3
+ * to interact with a remote server.
4
+ *
5
+ * - `'fetch'`: Sends HTTP POST requests for each task.
6
+ * - `'websocket'`: Maintains a persistent WebSocket connection for bi-directional messaging.
7
+ *
8
+ * @example
9
+ * ```ts
10
+ * const transport: RemoteTransportType = 'fetch';
11
+ * ```
12
+ */
13
+ export type RemoteTransportType = 'fetch' | 'websocket';
14
+
15
+ /**
16
+ * Configuration options for initializing a `RemoteCompute` backend.
17
+ *
18
+ * @property transport - The transport mechanism to use (`fetch` or `websocket`).
19
+ * @property endpoint - The server URL for handling task requests.
20
+ * @property canRunTasks - An optional list of task names this backend can handle. If omitted, it is assumed the backend can attempt all tasks.
21
+ *
22
+ * @example
23
+ * ```ts
24
+ * const options: RemoteComputeOptionsInterface = {
25
+ * transport: 'websocket',
26
+ * endpoint: 'wss://api.example.com/ws',
27
+ * canRunTasks: ['resizeImage', 'generatePDF']
28
+ * };
29
+ * ```
30
+ *
31
+ * @see https://developer.mozilla.org/en-US/docs/Web/API/fetch
32
+ * @see https://developer.mozilla.org/en-US/docs/Web/API/WebSocket
33
+ */
34
+ export interface RemoteComputeOptionsInterface {
35
+ transport: RemoteTransportType;
36
+ endpoint: string;
37
+ canRunTasks?: string[];
38
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,12 @@
1
+ {
2
+ "extends": "../../tsconfig.base.json",
3
+ "compilerOptions": {
4
+ "outDir": "dist",
5
+ "rootDir": "src",
6
+ "composite": true,
7
+ "declaration": true,
8
+ "declarationMap": true
9
+ },
10
+ "include": ["src"],
11
+ "references": [{ "path": "../core" }]
12
+ }