javascript-solid-server 0.0.2
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.
- package/.claude/settings.local.json +15 -0
- package/README.md +209 -0
- package/benchmark-report-2025-03-31T14-25-24.234Z.json +44 -0
- package/benchmark.js +286 -0
- package/package.json +21 -0
- package/src/handlers/container.js +110 -0
- package/src/handlers/resource.js +162 -0
- package/src/index.js +8 -0
- package/src/ldp/container.js +43 -0
- package/src/ldp/headers.js +78 -0
- package/src/server.js +68 -0
- package/src/storage/filesystem.js +151 -0
- package/src/utils/url.js +83 -0
- package/visualize-results.js +258 -0
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
{
|
|
2
|
+
"permissions": {
|
|
3
|
+
"allow": [
|
|
4
|
+
"WebSearch",
|
|
5
|
+
"WebFetch(domain:github.com)",
|
|
6
|
+
"WebFetch(domain:raw.githubusercontent.com)",
|
|
7
|
+
"Bash(cat:*)",
|
|
8
|
+
"WebFetch(domain:www.inrupt.com)",
|
|
9
|
+
"Bash(npm install:*)",
|
|
10
|
+
"Bash(timeout 3 node:*)",
|
|
11
|
+
"Bash(PORT=3030 timeout 3 node:*)",
|
|
12
|
+
"Bash(git commit:*)"
|
|
13
|
+
]
|
|
14
|
+
}
|
|
15
|
+
}
|
package/README.md
ADDED
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
# Vision
|
|
2
|
+
|
|
3
|
+
The goal of this project is to create a hyper-modern, performant, and minimalist JavaScript Solid server. While drawing inspiration from Node Solid Server (NSS), this new implementation will address its shortcomings and prioritize scalability, modularity, and developer usability.
|
|
4
|
+
|
|
5
|
+
## Key Objectives
|
|
6
|
+
|
|
7
|
+
- **Performance First:** Capable of handling enterprise-scale loads, targeting thousands to millions of users.
|
|
8
|
+
- **Minimalist Design:** Remove unused and experimental features; focus on what matters most.
|
|
9
|
+
- **Modularity:** Clear separation of identity, authentication, storage, and onboarding.
|
|
10
|
+
- **Developer Friendly:** Clean, well-documented, and extensible codebase that adheres to the Solid specification.
|
|
11
|
+
- **Modern Tooling:** Leverage async/await, native modules, fast HTTP servers like Fastify, and cutting-edge JavaScript runtimes.
|
|
12
|
+
- **HTTP Simplicity:** Prioritize simple HTTP/1.1 compatibility for maximum interoperability.
|
|
13
|
+
- **Frontend Agnostic:** Work with any frontend or application layer via standardized APIs.
|
|
14
|
+
- **Testable and CI Ready:** Fully integrated with Solid test suites and modern CI/CD pipelines.
|
|
15
|
+
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
# ARCHITECTURE
|
|
19
|
+
|
|
20
|
+
## Overview
|
|
21
|
+
|
|
22
|
+
The architecture is inspired by NSS but modernized and streamlined. Each subsystem is designed to operate independently and follow the single-responsibility principle.
|
|
23
|
+
|
|
24
|
+
### Components
|
|
25
|
+
|
|
26
|
+
- **HTTP Layer**
|
|
27
|
+
|
|
28
|
+
- Fastify server
|
|
29
|
+
- Routing and middleware based on HTTP verbs and Solid operations
|
|
30
|
+
- Blazingly fast, with benchmarks from the start
|
|
31
|
+
|
|
32
|
+
- **Identity Provider (IDP)**
|
|
33
|
+
|
|
34
|
+
- Handles Pod based WebIDs
|
|
35
|
+
- Handles external WebIDs
|
|
36
|
+
- Minimal by default, extendable via plugins
|
|
37
|
+
|
|
38
|
+
- **Authenticaion Module (AUthn)**
|
|
39
|
+
|
|
40
|
+
- Handles WebID-based authentication, including WebID-TLS
|
|
41
|
+
- OIDC-compliant with modular Authentication
|
|
42
|
+
- Single sign-on including WebID-TLS
|
|
43
|
+
|
|
44
|
+
- **Authorization Module (Authz)**
|
|
45
|
+
|
|
46
|
+
- Supports Web Access Control (WAC)
|
|
47
|
+
- Token-based permissions model
|
|
48
|
+
- Modular Authorization system
|
|
49
|
+
|
|
50
|
+
- **Storage Engine**
|
|
51
|
+
|
|
52
|
+
- Modular backend adapters (e.g. file system, S3, memory)
|
|
53
|
+
- POD-level quota management (optional)
|
|
54
|
+
- Interoperable with existing Cloud
|
|
55
|
+
|
|
56
|
+
- **Account and Onboarding**
|
|
57
|
+
- API-first registration
|
|
58
|
+
- Public, private, invite modes
|
|
59
|
+
- Extensible account templates
|
|
60
|
+
|
|
61
|
+
### Deployment Model
|
|
62
|
+
|
|
63
|
+
- Works as a single binary or serverless function
|
|
64
|
+
- Container-friendly (Docker, Deno, etc.)
|
|
65
|
+
- CLI for local dev setup and testing
|
|
66
|
+
|
|
67
|
+
### Separation of Concerns
|
|
68
|
+
|
|
69
|
+
- Each subsystem lives in its own module/package
|
|
70
|
+
- Clear boundaries between IDP and storage
|
|
71
|
+
- Frontend-independent API endpoints
|
|
72
|
+
|
|
73
|
+
### Compatibility
|
|
74
|
+
|
|
75
|
+
- Solid-compliant, LWS Compliant
|
|
76
|
+
- API parity with NSS where applicable
|
|
77
|
+
- API parity with CSS where applicable
|
|
78
|
+
|
|
79
|
+
---
|
|
80
|
+
|
|
81
|
+
# MVP Implementation
|
|
82
|
+
|
|
83
|
+
This is a minimal viable product (MVP) implementation of the JavaScriptSolid server. It includes the core components needed to demonstrate the concept while omitting some of the more complex features for simplicity.
|
|
84
|
+
|
|
85
|
+
## Getting Started
|
|
86
|
+
|
|
87
|
+
### Prerequisites
|
|
88
|
+
|
|
89
|
+
- Node.js 18 or higher
|
|
90
|
+
|
|
91
|
+
### Installation
|
|
92
|
+
|
|
93
|
+
```bash
|
|
94
|
+
# Clone the repository
|
|
95
|
+
git clone https://github.com/yourusername/javascript-solid-server.git
|
|
96
|
+
cd javascript-solid-server
|
|
97
|
+
|
|
98
|
+
# Install dependencies
|
|
99
|
+
npm install
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
### Running the Server
|
|
103
|
+
|
|
104
|
+
```bash
|
|
105
|
+
# Start the server
|
|
106
|
+
npm start
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
The server will be available at http://localhost:3000 by default.
|
|
110
|
+
|
|
111
|
+
## Features Included in MVP
|
|
112
|
+
|
|
113
|
+
- **HTTP Server**: Based on Fastify for high performance
|
|
114
|
+
- **Basic Identity Provider**: Simple user registration and login with JWT tokens
|
|
115
|
+
- **Simple Authorization**: Basic implementation of WAC (Web Access Control)
|
|
116
|
+
- **File-based Storage**: Local filesystem storage for Solid resources
|
|
117
|
+
- **Basic Solid Protocol Support**: GET, PUT, DELETE, PATCH, and HEAD operations
|
|
118
|
+
|
|
119
|
+
## Features Omitted in MVP (to be added later)
|
|
120
|
+
|
|
121
|
+
1. WebID-TLS Authentication
|
|
122
|
+
2. Full OIDC implementation
|
|
123
|
+
3. Advanced WAC features and ACL file parsing
|
|
124
|
+
4. Quotas and resource limits
|
|
125
|
+
5. Advanced container management
|
|
126
|
+
6. SPARQL and N3 Patch support
|
|
127
|
+
7. Notification systems
|
|
128
|
+
|
|
129
|
+
## API Usage Examples
|
|
130
|
+
|
|
131
|
+
### User Registration
|
|
132
|
+
|
|
133
|
+
```bash
|
|
134
|
+
curl -X POST http://localhost:3000/register \
|
|
135
|
+
-H "Content-Type: application/json" \
|
|
136
|
+
-d '{"username": "alice", "password": "secret", "email": "alice@example.com"}'
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
### Login
|
|
140
|
+
|
|
141
|
+
```bash
|
|
142
|
+
curl -X POST http://localhost:3000/login \
|
|
143
|
+
-H "Content-Type: application/json" \
|
|
144
|
+
-d '{"username": "alice", "password": "secret"}'
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
### Accessing Resources
|
|
148
|
+
|
|
149
|
+
```bash
|
|
150
|
+
# Get a resource
|
|
151
|
+
curl -X GET http://localhost:3000/alice/profile \
|
|
152
|
+
-H "Authorization: Bearer YOUR_TOKEN_HERE"
|
|
153
|
+
|
|
154
|
+
# Create or update a resource
|
|
155
|
+
curl -X PUT http://localhost:3000/alice/profile \
|
|
156
|
+
-H "Authorization: Bearer YOUR_TOKEN_HERE" \
|
|
157
|
+
-H "Content-Type: text/turtle" \
|
|
158
|
+
-d '@prefix foaf: <http://xmlns.com/foaf/0.1/>. <#me> a foaf:Person; foaf:name "Alice".'
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
## Performance Benchmarking
|
|
162
|
+
|
|
163
|
+
This project includes a comprehensive benchmarking tool to measure server performance under various loads.
|
|
164
|
+
|
|
165
|
+
### Running the Benchmark
|
|
166
|
+
|
|
167
|
+
```bash
|
|
168
|
+
# Make sure the server is running in a separate terminal
|
|
169
|
+
npm start
|
|
170
|
+
|
|
171
|
+
# In another terminal, run the benchmark
|
|
172
|
+
npm run benchmark
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
The benchmark will:
|
|
176
|
+
|
|
177
|
+
1. Create multiple test users
|
|
178
|
+
2. Execute various operations (register, login, read, write, delete)
|
|
179
|
+
3. Measure response times for each operation type
|
|
180
|
+
4. Test different concurrency levels (1, 5, 10, 50, 100 users)
|
|
181
|
+
5. Calculate throughput (operations per second)
|
|
182
|
+
|
|
183
|
+
### Visualizing Results
|
|
184
|
+
|
|
185
|
+
After running the benchmark, you can generate a visual report:
|
|
186
|
+
|
|
187
|
+
```bash
|
|
188
|
+
# Generate an HTML report with charts
|
|
189
|
+
npm run visualize benchmark-report-[timestamp].json
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
Open the generated HTML file in a browser to see:
|
|
193
|
+
|
|
194
|
+
- Average response times for each operation type
|
|
195
|
+
- Throughput metrics at different concurrency levels
|
|
196
|
+
- Visual charts for easy performance analysis
|
|
197
|
+
|
|
198
|
+
### Customizing Benchmarks
|
|
199
|
+
|
|
200
|
+
You can customize the benchmark parameters in `benchmark.js`:
|
|
201
|
+
|
|
202
|
+
- Concurrent users levels
|
|
203
|
+
- Operations per user
|
|
204
|
+
- Test duration
|
|
205
|
+
- Test user credentials
|
|
206
|
+
|
|
207
|
+
## Contributing
|
|
208
|
+
|
|
209
|
+
Contributions are welcome! Please feel free to submit a Pull Request.
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
{
|
|
2
|
+
"timestamp": "2025-03-31T14:25:24.234Z",
|
|
3
|
+
"server": "http://nostr.social:3000",
|
|
4
|
+
"testDuration": 30000,
|
|
5
|
+
"averageResponseTimes": {
|
|
6
|
+
"register": 49.28774029878249,
|
|
7
|
+
"login": 49.07647125548627,
|
|
8
|
+
"read": 145.50252931779679,
|
|
9
|
+
"write": 143.20483738812226,
|
|
10
|
+
"delete": 145.5004399698014
|
|
11
|
+
},
|
|
12
|
+
"throughputResults": [
|
|
13
|
+
{
|
|
14
|
+
"concurrentUsers": 1,
|
|
15
|
+
"operations": 300,
|
|
16
|
+
"duration": 629.3636230230331,
|
|
17
|
+
"throughput": 476.67197312581374
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
"concurrentUsers": 5,
|
|
21
|
+
"operations": 1500,
|
|
22
|
+
"duration": 3072.7389999628067,
|
|
23
|
+
"throughput": 488.16381736885444
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
"concurrentUsers": 10,
|
|
27
|
+
"operations": 3000,
|
|
28
|
+
"duration": 6042.516402959824,
|
|
29
|
+
"throughput": 496.4818959416479
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
"concurrentUsers": 50,
|
|
33
|
+
"operations": 14000,
|
|
34
|
+
"duration": 30000,
|
|
35
|
+
"throughput": 466.6666666666667
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
"concurrentUsers": 100,
|
|
39
|
+
"operations": 13900,
|
|
40
|
+
"duration": 30000,
|
|
41
|
+
"throughput": 463.3333333333333
|
|
42
|
+
}
|
|
43
|
+
]
|
|
44
|
+
}
|
package/benchmark.js
ADDED
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
import fetch from 'node-fetch';
|
|
2
|
+
import { performance } from 'perf_hooks';
|
|
3
|
+
import { promises as fs } from 'fs';
|
|
4
|
+
import path from 'path';
|
|
5
|
+
|
|
6
|
+
// Configuration
|
|
7
|
+
const config = {
|
|
8
|
+
baseUrl: 'http://nostr.social:3000',
|
|
9
|
+
concurrentUsers: [1, 5, 10, 50, 100], // Different concurrency levels to test
|
|
10
|
+
operations: 100, // Operations per user
|
|
11
|
+
testDuration: 30000, // 30 seconds per test
|
|
12
|
+
testUserPrefix: 'testuser',
|
|
13
|
+
testPassword: 'benchmark123',
|
|
14
|
+
results: {
|
|
15
|
+
registerTime: [],
|
|
16
|
+
loginTime: [],
|
|
17
|
+
readTime: [],
|
|
18
|
+
writeTime: [],
|
|
19
|
+
deleteTime: [],
|
|
20
|
+
throughput: []
|
|
21
|
+
}
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
// Store tokens for authenticated requests
|
|
25
|
+
const tokens = new Map();
|
|
26
|
+
|
|
27
|
+
// Utility function to measure execution time
|
|
28
|
+
async function measureTime (fn) {
|
|
29
|
+
const start = performance.now();
|
|
30
|
+
const result = await fn();
|
|
31
|
+
const end = performance.now();
|
|
32
|
+
return { result, time: end - start };
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Register a test user
|
|
36
|
+
async function registerUser (username) {
|
|
37
|
+
const { result, time } = await measureTime(async () => {
|
|
38
|
+
const response = await fetch(`${config.baseUrl}/register`, {
|
|
39
|
+
method: 'POST',
|
|
40
|
+
headers: { 'Content-Type': 'application/json' },
|
|
41
|
+
body: JSON.stringify({
|
|
42
|
+
username,
|
|
43
|
+
password: config.testPassword,
|
|
44
|
+
email: `${username}@benchmark.test`
|
|
45
|
+
})
|
|
46
|
+
});
|
|
47
|
+
return response.json();
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
config.results.registerTime.push(time);
|
|
51
|
+
return result;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Login a test user
|
|
55
|
+
async function loginUser (username) {
|
|
56
|
+
const { result, time } = await measureTime(async () => {
|
|
57
|
+
const response = await fetch(`${config.baseUrl}/login`, {
|
|
58
|
+
method: 'POST',
|
|
59
|
+
headers: { 'Content-Type': 'application/json' },
|
|
60
|
+
body: JSON.stringify({
|
|
61
|
+
username,
|
|
62
|
+
password: config.testPassword
|
|
63
|
+
})
|
|
64
|
+
});
|
|
65
|
+
return response.json();
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
config.results.loginTime.push(time);
|
|
69
|
+
|
|
70
|
+
if (result.id_token) {
|
|
71
|
+
tokens.set(username, result.id_token);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return result;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Create a resource
|
|
78
|
+
async function createResource (username, resourcePath, content = null) {
|
|
79
|
+
const token = tokens.get(username);
|
|
80
|
+
if (!token) throw new Error(`No token for user ${username}`);
|
|
81
|
+
|
|
82
|
+
const turtleContent = content || `
|
|
83
|
+
@prefix foaf: <http://xmlns.com/foaf/0.1/>.
|
|
84
|
+
<#me> a foaf:Person;
|
|
85
|
+
foaf:name "${username}";
|
|
86
|
+
foaf:mbox <mailto:${username}@benchmark.test>.
|
|
87
|
+
`;
|
|
88
|
+
|
|
89
|
+
const { result, time } = await measureTime(async () => {
|
|
90
|
+
const response = await fetch(`${config.baseUrl}/${username}/${resourcePath}`, {
|
|
91
|
+
method: 'PUT',
|
|
92
|
+
headers: {
|
|
93
|
+
'Content-Type': 'text/turtle',
|
|
94
|
+
'Authorization': `Bearer ${token}`
|
|
95
|
+
},
|
|
96
|
+
body: turtleContent
|
|
97
|
+
});
|
|
98
|
+
return response.status;
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
config.results.writeTime.push(time);
|
|
102
|
+
return result;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Read a resource
|
|
106
|
+
async function readResource (username, resourcePath) {
|
|
107
|
+
const token = tokens.get(username);
|
|
108
|
+
if (!token) throw new Error(`No token for user ${username}`);
|
|
109
|
+
|
|
110
|
+
const { result, time } = await measureTime(async () => {
|
|
111
|
+
const response = await fetch(`${config.baseUrl}/${username}/${resourcePath}`, {
|
|
112
|
+
method: 'GET',
|
|
113
|
+
headers: {
|
|
114
|
+
'Authorization': `Bearer ${token}`
|
|
115
|
+
}
|
|
116
|
+
});
|
|
117
|
+
return response.status;
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
config.results.readTime.push(time);
|
|
121
|
+
return result;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Delete a resource
|
|
125
|
+
async function deleteResource (username, resourcePath) {
|
|
126
|
+
const token = tokens.get(username);
|
|
127
|
+
if (!token) throw new Error(`No token for user ${username}`);
|
|
128
|
+
|
|
129
|
+
const { result, time } = await measureTime(async () => {
|
|
130
|
+
const response = await fetch(`${config.baseUrl}/${username}/${resourcePath}`, {
|
|
131
|
+
method: 'DELETE',
|
|
132
|
+
headers: {
|
|
133
|
+
'Authorization': `Bearer ${token}`
|
|
134
|
+
}
|
|
135
|
+
});
|
|
136
|
+
return response.status;
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
config.results.deleteTime.push(time);
|
|
140
|
+
return result;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Run benchmark for specific concurrency
|
|
144
|
+
async function runBenchmark (concurrentUsers) {
|
|
145
|
+
console.log(`\n=== Running benchmark with ${concurrentUsers} concurrent users ===`);
|
|
146
|
+
|
|
147
|
+
// Create test users
|
|
148
|
+
console.log('Creating test users...');
|
|
149
|
+
const users = [];
|
|
150
|
+
for (let i = 0; i < concurrentUsers; i++) {
|
|
151
|
+
const username = `${config.testUserPrefix}${i}`;
|
|
152
|
+
users.push(username);
|
|
153
|
+
await registerUser(username);
|
|
154
|
+
await loginUser(username);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Prepare operation queue (read/write/delete)
|
|
158
|
+
const operations = [];
|
|
159
|
+
for (const username of users) {
|
|
160
|
+
for (let i = 0; i < config.operations; i++) {
|
|
161
|
+
const resourcePath = `benchmark/resource${i}.ttl`;
|
|
162
|
+
operations.push(async () => await createResource(username, resourcePath));
|
|
163
|
+
operations.push(async () => await readResource(username, resourcePath));
|
|
164
|
+
operations.push(async () => await deleteResource(username, resourcePath));
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Run operations with measured throughput
|
|
169
|
+
console.log(`Starting operations (${operations.length} total)...`);
|
|
170
|
+
const startTime = performance.now();
|
|
171
|
+
let completedOps = 0;
|
|
172
|
+
const endTime = startTime + config.testDuration;
|
|
173
|
+
|
|
174
|
+
// Create chunks of operations to run in parallel
|
|
175
|
+
const chunks = [];
|
|
176
|
+
const chunkSize = operations.length > 100 ? 100 : operations.length;
|
|
177
|
+
|
|
178
|
+
for (let i = 0; i < operations.length; i += chunkSize) {
|
|
179
|
+
chunks.push(operations.slice(i, i + chunkSize));
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
for (const chunk of chunks) {
|
|
183
|
+
if (performance.now() >= endTime) break;
|
|
184
|
+
|
|
185
|
+
await Promise.all(chunk.map(async (operation) => {
|
|
186
|
+
if (performance.now() < endTime) {
|
|
187
|
+
await operation();
|
|
188
|
+
completedOps++;
|
|
189
|
+
}
|
|
190
|
+
}));
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// Calculate throughput (ops/sec)
|
|
194
|
+
const actualDuration = Math.min(performance.now() - startTime, config.testDuration);
|
|
195
|
+
const throughput = (completedOps / actualDuration) * 1000;
|
|
196
|
+
config.results.throughput.push({
|
|
197
|
+
concurrentUsers,
|
|
198
|
+
operations: completedOps,
|
|
199
|
+
duration: actualDuration,
|
|
200
|
+
throughput
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
console.log(`Completed ${completedOps} operations in ${actualDuration.toFixed(2)}ms`);
|
|
204
|
+
console.log(`Throughput: ${throughput.toFixed(2)} operations/second`);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// Generate report
|
|
208
|
+
async function generateReport () {
|
|
209
|
+
// Calculate averages
|
|
210
|
+
const averages = {
|
|
211
|
+
register: calculateAverage(config.results.registerTime),
|
|
212
|
+
login: calculateAverage(config.results.loginTime),
|
|
213
|
+
read: calculateAverage(config.results.readTime),
|
|
214
|
+
write: calculateAverage(config.results.writeTime),
|
|
215
|
+
delete: calculateAverage(config.results.deleteTime)
|
|
216
|
+
};
|
|
217
|
+
|
|
218
|
+
// Create report
|
|
219
|
+
const report = {
|
|
220
|
+
timestamp: new Date().toISOString(),
|
|
221
|
+
server: config.baseUrl,
|
|
222
|
+
testDuration: config.testDuration,
|
|
223
|
+
averageResponseTimes: averages,
|
|
224
|
+
throughputResults: config.results.throughput
|
|
225
|
+
};
|
|
226
|
+
|
|
227
|
+
// Save report to file
|
|
228
|
+
await fs.writeFile(
|
|
229
|
+
`benchmark-report-${new Date().toISOString().replace(/:/g, '-')}.json`,
|
|
230
|
+
JSON.stringify(report, null, 2)
|
|
231
|
+
);
|
|
232
|
+
|
|
233
|
+
// Display summary
|
|
234
|
+
console.log('\n=== BENCHMARK RESULTS ===');
|
|
235
|
+
console.log('Average Response Times (ms):');
|
|
236
|
+
console.log(` Register: ${averages.register.toFixed(2)} ms`);
|
|
237
|
+
console.log(` Login: ${averages.login.toFixed(2)} ms`);
|
|
238
|
+
console.log(` Read: ${averages.read.toFixed(2)} ms`);
|
|
239
|
+
console.log(` Write: ${averages.write.toFixed(2)} ms`);
|
|
240
|
+
console.log(` Delete: ${averages.delete.toFixed(2)} ms`);
|
|
241
|
+
|
|
242
|
+
console.log('\nThroughput Results:');
|
|
243
|
+
config.results.throughput.forEach(result => {
|
|
244
|
+
console.log(` ${result.concurrentUsers} users: ${result.throughput.toFixed(2)} ops/sec`);
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
console.log('\nReport saved to file.');
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// Calculate average of an array
|
|
251
|
+
function calculateAverage (array) {
|
|
252
|
+
if (array.length === 0) return 0;
|
|
253
|
+
return array.reduce((sum, value) => sum + value, 0) / array.length;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// Main benchmark function
|
|
257
|
+
async function startBenchmark () {
|
|
258
|
+
console.log('=== JavaScript Solid Server Benchmark ===');
|
|
259
|
+
console.log(`Server URL: ${config.baseUrl}`);
|
|
260
|
+
console.log(`Test Duration: ${config.testDuration / 1000} seconds per concurrency level`);
|
|
261
|
+
|
|
262
|
+
try {
|
|
263
|
+
// Check if server is running
|
|
264
|
+
const response = await fetch(config.baseUrl);
|
|
265
|
+
if (response.status < 200 || response.status >= 500) {
|
|
266
|
+
throw new Error(`Server responded with status ${response.status}`);
|
|
267
|
+
}
|
|
268
|
+
} catch (error) {
|
|
269
|
+
console.error('Error connecting to server:', error.message);
|
|
270
|
+
console.error('Please make sure the server is running before starting the benchmark.');
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// Run tests for each concurrency level
|
|
275
|
+
for (const concurrentUsers of config.concurrentUsers) {
|
|
276
|
+
await runBenchmark(concurrentUsers);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// Generate final report
|
|
280
|
+
await generateReport();
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// Start the benchmark
|
|
284
|
+
startBenchmark().catch(error => {
|
|
285
|
+
console.error('Benchmark error:', error);
|
|
286
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "javascript-solid-server",
|
|
3
|
+
"version": "0.0.2",
|
|
4
|
+
"description": "A minimal, fast Solid server",
|
|
5
|
+
"main": "src/index.js",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"scripts": {
|
|
8
|
+
"start": "node src/index.js",
|
|
9
|
+
"dev": "node --watch src/index.js",
|
|
10
|
+
"test": "node --test"
|
|
11
|
+
},
|
|
12
|
+
"dependencies": {
|
|
13
|
+
"fastify": "^4.25.2",
|
|
14
|
+
"fs-extra": "^11.2.0"
|
|
15
|
+
},
|
|
16
|
+
"engines": {
|
|
17
|
+
"node": ">=18.0.0"
|
|
18
|
+
},
|
|
19
|
+
"keywords": ["solid", "ldp", "linked-data", "decentralized"],
|
|
20
|
+
"license": "MIT"
|
|
21
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import * as storage from '../storage/filesystem.js';
|
|
2
|
+
import { getAllHeaders } from '../ldp/headers.js';
|
|
3
|
+
import { isContainer } from '../utils/url.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Handle POST request to container (create new resource)
|
|
7
|
+
*/
|
|
8
|
+
export async function handlePost(request, reply) {
|
|
9
|
+
const urlPath = request.url.split('?')[0];
|
|
10
|
+
|
|
11
|
+
// Ensure target is a container
|
|
12
|
+
if (!isContainer(urlPath)) {
|
|
13
|
+
return reply.code(405).send({ error: 'POST only allowed on containers' });
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// Check container exists
|
|
17
|
+
const stats = await storage.stat(urlPath);
|
|
18
|
+
if (!stats || !stats.isDirectory) {
|
|
19
|
+
// Create container if it doesn't exist
|
|
20
|
+
await storage.createContainer(urlPath);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// Get slug from header or generate UUID
|
|
24
|
+
const slug = request.headers.slug;
|
|
25
|
+
const linkHeader = request.headers.link || '';
|
|
26
|
+
|
|
27
|
+
// Check if creating a container (Link header contains ldp:Container or ldp:BasicContainer)
|
|
28
|
+
const isCreatingContainer = linkHeader.includes('Container') || linkHeader.includes('BasicContainer');
|
|
29
|
+
|
|
30
|
+
// Generate unique filename
|
|
31
|
+
const filename = await storage.generateUniqueFilename(urlPath, slug, isCreatingContainer);
|
|
32
|
+
const newPath = urlPath + filename + (isCreatingContainer ? '/' : '');
|
|
33
|
+
|
|
34
|
+
let success;
|
|
35
|
+
if (isCreatingContainer) {
|
|
36
|
+
success = await storage.createContainer(newPath);
|
|
37
|
+
} else {
|
|
38
|
+
// Get content from request body
|
|
39
|
+
let content = request.body;
|
|
40
|
+
if (Buffer.isBuffer(content)) {
|
|
41
|
+
// Already a buffer
|
|
42
|
+
} else if (typeof content === 'string') {
|
|
43
|
+
content = Buffer.from(content);
|
|
44
|
+
} else if (content && typeof content === 'object') {
|
|
45
|
+
content = Buffer.from(JSON.stringify(content));
|
|
46
|
+
} else {
|
|
47
|
+
content = Buffer.from('');
|
|
48
|
+
}
|
|
49
|
+
success = await storage.write(newPath, content);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (!success) {
|
|
53
|
+
return reply.code(500).send({ error: 'Create failed' });
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const location = `${request.protocol}://${request.hostname}${newPath}`;
|
|
57
|
+
const origin = request.headers.origin;
|
|
58
|
+
|
|
59
|
+
const headers = getAllHeaders({
|
|
60
|
+
isContainer: isCreatingContainer,
|
|
61
|
+
origin
|
|
62
|
+
});
|
|
63
|
+
headers['Location'] = location;
|
|
64
|
+
|
|
65
|
+
Object.entries(headers).forEach(([k, v]) => reply.header(k, v));
|
|
66
|
+
return reply.code(201).send();
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Create a pod (container) for a user
|
|
71
|
+
* POST /.pods with { "name": "alice" }
|
|
72
|
+
*/
|
|
73
|
+
export async function handleCreatePod(request, reply) {
|
|
74
|
+
const { name } = request.body || {};
|
|
75
|
+
|
|
76
|
+
if (!name || typeof name !== 'string') {
|
|
77
|
+
return reply.code(400).send({ error: 'Pod name required' });
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Validate pod name (alphanumeric, dash, underscore)
|
|
81
|
+
if (!/^[a-zA-Z0-9_-]+$/.test(name)) {
|
|
82
|
+
return reply.code(400).send({ error: 'Invalid pod name. Use alphanumeric, dash, or underscore only.' });
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const podPath = `/${name}/`;
|
|
86
|
+
|
|
87
|
+
// Check if pod already exists
|
|
88
|
+
if (await storage.exists(podPath)) {
|
|
89
|
+
return reply.code(409).send({ error: 'Pod already exists' });
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Create pod container
|
|
93
|
+
const success = await storage.createContainer(podPath);
|
|
94
|
+
if (!success) {
|
|
95
|
+
return reply.code(500).send({ error: 'Failed to create pod' });
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const location = `${request.protocol}://${request.hostname}${podPath}`;
|
|
99
|
+
const origin = request.headers.origin;
|
|
100
|
+
|
|
101
|
+
const headers = getAllHeaders({ isContainer: true, origin });
|
|
102
|
+
headers['Location'] = location;
|
|
103
|
+
|
|
104
|
+
Object.entries(headers).forEach(([k, v]) => reply.header(k, v));
|
|
105
|
+
|
|
106
|
+
return reply.code(201).send({
|
|
107
|
+
name,
|
|
108
|
+
url: location
|
|
109
|
+
});
|
|
110
|
+
}
|