javascript-solid-server 0.0.9 → 0.0.11

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/benchmark.js CHANGED
@@ -1,286 +1,182 @@
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();
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Benchmark script for JavaScript Solid Server
4
+ *
5
+ * Measures throughput and latency for common operations.
6
+ * Run: node benchmark.js
7
+ */
8
+
9
+ import autocannon from 'autocannon';
10
+ import { createServer } from './src/server.js';
11
+ import fs from 'fs-extra';
12
+
13
+ const PORT = 3030;
14
+ const DURATION = 10; // seconds per test
15
+ const CONNECTIONS = 10;
16
+
17
+ let server;
18
+ let token;
19
+
20
+ async function setup() {
21
+ // Clean data directory
22
+ await fs.emptyDir('./data');
23
+
24
+ // Start server (no logging for clean benchmark)
25
+ server = createServer({ logger: false });
26
+ await server.listen({ port: PORT, host: '127.0.0.1' });
27
+
28
+ // Create a test pod
29
+ const res = await fetch(`http://127.0.0.1:${PORT}/.pods`, {
30
+ method: 'POST',
31
+ headers: { 'Content-Type': 'application/json' },
32
+ body: JSON.stringify({ name: 'bench' })
48
33
  });
34
+ const data = await res.json();
35
+ token = data.token;
49
36
 
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}`, {
37
+ // Create some test resources
38
+ for (let i = 0; i < 100; i++) {
39
+ await fetch(`http://127.0.0.1:${PORT}/bench/public/item${i}.json`, {
91
40
  method: 'PUT',
92
41
  headers: {
93
- 'Content-Type': 'text/turtle',
42
+ 'Content-Type': 'application/ld+json',
94
43
  'Authorization': `Bearer ${token}`
95
44
  },
96
- body: turtleContent
45
+ body: JSON.stringify({ '@id': `#item${i}`, 'http://example.org/value': i })
97
46
  });
98
- return response.status;
99
- });
47
+ }
100
48
 
101
- config.results.writeTime.push(time);
102
- return result;
49
+ console.log('Setup complete: created pod with 100 resources\n');
103
50
  }
104
51
 
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}`);
52
+ async function teardown() {
53
+ await server.close();
54
+ await fs.emptyDir('./data');
55
+ }
109
56
 
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
- }
57
+ function runBenchmark(opts) {
58
+ return new Promise((resolve) => {
59
+ const instance = autocannon({
60
+ ...opts,
61
+ duration: DURATION,
62
+ connections: CONNECTIONS,
63
+ }, (err, result) => {
64
+ resolve(result);
116
65
  });
117
- return response.status;
118
- });
119
66
 
120
- config.results.readTime.push(time);
121
- return result;
67
+ autocannon.track(instance, { renderProgressBar: false });
68
+ });
122
69
  }
123
70
 
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}`);
71
+ function formatResult(result) {
72
+ return {
73
+ 'Requests/sec': Math.round(result.requests.average),
74
+ 'Latency avg': `${result.latency.average.toFixed(2)}ms`,
75
+ 'Latency p99': `${result.latency.p99.toFixed(2)}ms`,
76
+ 'Throughput': `${(result.throughput.average / 1024 / 1024).toFixed(2)} MB/s`
77
+ };
78
+ }
128
79
 
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;
80
+ async function benchmarkGET() {
81
+ console.log('📖 Benchmarking GET (read resource)...');
82
+ const result = await runBenchmark({
83
+ url: `http://127.0.0.1:${PORT}/bench/public/item0.json`,
84
+ method: 'GET'
137
85
  });
138
-
139
- config.results.deleteTime.push(time);
140
- return result;
86
+ return formatResult(result);
141
87
  }
142
88
 
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
- }
89
+ async function benchmarkGETContainer() {
90
+ console.log('📂 Benchmarking GET (container listing)...');
91
+ const result = await runBenchmark({
92
+ url: `http://127.0.0.1:${PORT}/bench/public/`,
93
+ method: 'GET'
94
+ });
95
+ return formatResult(result);
96
+ }
156
97
 
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));
98
+ let putCounter = 1000;
99
+ async function benchmarkPUT() {
100
+ console.log('✏️ Benchmarking PUT (create/update resource)...');
101
+ const result = await runBenchmark({
102
+ url: `http://127.0.0.1:${PORT}/bench/public/new`,
103
+ method: 'PUT',
104
+ headers: {
105
+ 'Content-Type': 'application/ld+json',
106
+ 'Authorization': `Bearer ${token}`
107
+ },
108
+ setupClient: (client) => {
109
+ client.setBody(JSON.stringify({ '@id': '#test', 'http://example.org/v': putCounter++ }));
165
110
  }
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
111
  });
202
-
203
- console.log(`Completed ${completedOps} operations in ${actualDuration.toFixed(2)}ms`);
204
- console.log(`Throughput: ${throughput.toFixed(2)} operations/second`);
112
+ return formatResult(result);
205
113
  }
206
114
 
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
- };
115
+ async function benchmarkPOST() {
116
+ console.log('📝 Benchmarking POST (create in container)...');
117
+ const result = await runBenchmark({
118
+ url: `http://127.0.0.1:${PORT}/bench/public/`,
119
+ method: 'POST',
120
+ headers: {
121
+ 'Content-Type': 'application/ld+json',
122
+ 'Authorization': `Bearer ${token}`
123
+ },
124
+ body: JSON.stringify({ '@id': '#new', 'http://example.org/created': true })
125
+ });
126
+ return formatResult(result);
127
+ }
217
128
 
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
- };
129
+ async function benchmarkOPTIONS() {
130
+ console.log('🔍 Benchmarking OPTIONS (discovery)...');
131
+ const result = await runBenchmark({
132
+ url: `http://127.0.0.1:${PORT}/bench/public/item0.json`,
133
+ method: 'OPTIONS'
134
+ });
135
+ return formatResult(result);
136
+ }
226
137
 
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
- );
138
+ async function benchmarkHEAD() {
139
+ console.log('📋 Benchmarking HEAD (metadata only)...');
140
+ const result = await runBenchmark({
141
+ url: `http://127.0.0.1:${PORT}/bench/public/item0.json`,
142
+ method: 'HEAD'
143
+ });
144
+ return formatResult(result);
145
+ }
232
146
 
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`);
147
+ async function main() {
148
+ console.log('🚀 JavaScript Solid Server Benchmark');
149
+ console.log('=====================================');
150
+ console.log(`Duration: ${DURATION}s per test, ${CONNECTIONS} concurrent connections\n`);
241
151
 
242
- console.log('\nThroughput Results:');
243
- config.results.throughput.forEach(result => {
244
- console.log(` ${result.concurrentUsers} users: ${result.throughput.toFixed(2)} ops/sec`);
245
- });
152
+ await setup();
246
153
 
247
- console.log('\nReport saved to file.');
248
- }
154
+ const results = {};
249
155
 
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
- }
156
+ results['GET resource'] = await benchmarkGET();
157
+ results['GET container'] = await benchmarkGETContainer();
158
+ results['HEAD'] = await benchmarkHEAD();
159
+ results['OPTIONS'] = await benchmarkOPTIONS();
160
+ results['PUT'] = await benchmarkPUT();
161
+ results['POST'] = await benchmarkPOST();
255
162
 
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`);
163
+ console.log('\n📊 Results Summary');
164
+ console.log('==================\n');
261
165
 
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;
166
+ // Print as table
167
+ console.log('| Operation | Req/sec | Avg Latency | p99 Latency |');
168
+ console.log('|-----------|---------|-------------|-------------|');
169
+ for (const [op, data] of Object.entries(results)) {
170
+ console.log(`| ${op.padEnd(13)} | ${String(data['Requests/sec']).padStart(7)} | ${data['Latency avg'].padStart(11)} | ${data['Latency p99'].padStart(11)} |`);
272
171
  }
273
172
 
274
- // Run tests for each concurrency level
275
- for (const concurrentUsers of config.concurrentUsers) {
276
- await runBenchmark(concurrentUsers);
277
- }
173
+ console.log('\n');
174
+
175
+ await teardown();
278
176
 
279
- // Generate final report
280
- await generateReport();
177
+ // Output JSON for README
178
+ console.log('JSON results:');
179
+ console.log(JSON.stringify(results, null, 2));
281
180
  }
282
181
 
283
- // Start the benchmark
284
- startBenchmark().catch(error => {
285
- console.error('Benchmark error:', error);
286
- });
182
+ main().catch(console.error);
package/bin/jss.js ADDED
@@ -0,0 +1,208 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * JavaScript Solid Server CLI
5
+ *
6
+ * Usage:
7
+ * jss start [options] Start the server
8
+ * jss init Initialize configuration
9
+ */
10
+
11
+ import { Command } from 'commander';
12
+ import { createServer } from '../src/server.js';
13
+ import { loadConfig, saveConfig, printConfig, defaults } from '../src/config.js';
14
+ import fs from 'fs-extra';
15
+ import path from 'path';
16
+ import { fileURLToPath } from 'url';
17
+ import readline from 'readline';
18
+
19
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
20
+ const pkg = JSON.parse(await fs.readFile(path.join(__dirname, '../package.json'), 'utf8'));
21
+
22
+ const program = new Command();
23
+
24
+ program
25
+ .name('jss')
26
+ .description('JavaScript Solid Server - A minimal, fast, JSON-LD native Solid server')
27
+ .version(pkg.version);
28
+
29
+ /**
30
+ * Start command
31
+ */
32
+ program
33
+ .command('start')
34
+ .description('Start the Solid server')
35
+ .option('-p, --port <number>', 'Port to listen on', parseInt)
36
+ .option('-h, --host <address>', 'Host to bind to')
37
+ .option('-r, --root <path>', 'Data directory')
38
+ .option('-c, --config <file>', 'Config file path')
39
+ .option('--ssl-key <path>', 'Path to SSL private key (PEM)')
40
+ .option('--ssl-cert <path>', 'Path to SSL certificate (PEM)')
41
+ .option('--multiuser', 'Enable multi-user mode')
42
+ .option('--no-multiuser', 'Disable multi-user mode')
43
+ .option('--conneg', 'Enable content negotiation (Turtle support)')
44
+ .option('--no-conneg', 'Disable content negotiation')
45
+ .option('--notifications', 'Enable WebSocket notifications')
46
+ .option('--no-notifications', 'Disable WebSocket notifications')
47
+ .option('-q, --quiet', 'Suppress log output')
48
+ .option('--print-config', 'Print configuration and exit')
49
+ .action(async (options) => {
50
+ try {
51
+ const config = await loadConfig(options, options.config);
52
+
53
+ if (options.printConfig) {
54
+ printConfig(config);
55
+ process.exit(0);
56
+ }
57
+
58
+ // Create and start server
59
+ const server = createServer({
60
+ logger: config.logger,
61
+ conneg: config.conneg,
62
+ notifications: config.notifications,
63
+ ssl: config.ssl ? {
64
+ key: await fs.readFile(config.sslKey),
65
+ cert: await fs.readFile(config.sslCert),
66
+ } : null,
67
+ root: config.root,
68
+ });
69
+
70
+ await server.listen({ port: config.port, host: config.host });
71
+
72
+ const protocol = config.ssl ? 'https' : 'http';
73
+ const address = config.host === '0.0.0.0' ? 'localhost' : config.host;
74
+
75
+ if (!config.quiet) {
76
+ console.log(`\n JavaScript Solid Server v${pkg.version}`);
77
+ console.log(` ${protocol}://${address}:${config.port}/`);
78
+ console.log(`\n Data: ${path.resolve(config.root)}`);
79
+ if (config.ssl) console.log(' SSL: enabled');
80
+ if (config.conneg) console.log(' Conneg: enabled');
81
+ if (config.notifications) console.log(' WebSocket: enabled');
82
+ console.log('\n Press Ctrl+C to stop\n');
83
+ }
84
+
85
+ // Handle shutdown
86
+ const shutdown = async () => {
87
+ if (!config.quiet) console.log('\n Shutting down...');
88
+ await server.close();
89
+ process.exit(0);
90
+ };
91
+
92
+ process.on('SIGINT', shutdown);
93
+ process.on('SIGTERM', shutdown);
94
+
95
+ } catch (err) {
96
+ console.error(`Error: ${err.message}`);
97
+ process.exit(1);
98
+ }
99
+ });
100
+
101
+ /**
102
+ * Init command - interactive configuration
103
+ */
104
+ program
105
+ .command('init')
106
+ .description('Initialize server configuration')
107
+ .option('-c, --config <file>', 'Config file path', './config.json')
108
+ .option('-y, --yes', 'Accept defaults without prompting')
109
+ .action(async (options) => {
110
+ const configFile = path.resolve(options.config);
111
+
112
+ // Check if config already exists
113
+ if (await fs.pathExists(configFile)) {
114
+ console.log(`Config file already exists: ${configFile}`);
115
+ const overwrite = options.yes ? true : await confirm('Overwrite?');
116
+ if (!overwrite) {
117
+ console.log('Aborted.');
118
+ process.exit(0);
119
+ }
120
+ }
121
+
122
+ let config;
123
+
124
+ if (options.yes) {
125
+ // Use defaults
126
+ config = { ...defaults };
127
+ } else {
128
+ // Interactive prompts
129
+ console.log('\n JavaScript Solid Server Setup\n');
130
+
131
+ config = {
132
+ port: await prompt('Port', defaults.port),
133
+ root: await prompt('Data directory', defaults.root),
134
+ conneg: await confirm('Enable content negotiation (Turtle support)?', defaults.conneg),
135
+ notifications: await confirm('Enable WebSocket notifications?', defaults.notifications),
136
+ };
137
+
138
+ // Ask about SSL
139
+ const useSSL = await confirm('Configure SSL?', false);
140
+ if (useSSL) {
141
+ config.sslKey = await prompt('SSL key path', './ssl/key.pem');
142
+ config.sslCert = await prompt('SSL certificate path', './ssl/cert.pem');
143
+ }
144
+
145
+ console.log('');
146
+ }
147
+
148
+ // Save config
149
+ await saveConfig(config, configFile);
150
+ console.log(`Configuration saved to: ${configFile}`);
151
+
152
+ // Create data directory
153
+ const dataDir = path.resolve(config.root);
154
+ await fs.ensureDir(dataDir);
155
+ console.log(`Data directory created: ${dataDir}`);
156
+
157
+ console.log('\nRun `jss start` to start the server.\n');
158
+ });
159
+
160
+ /**
161
+ * Helper: Prompt for input
162
+ */
163
+ async function prompt(question, defaultValue) {
164
+ const rl = readline.createInterface({
165
+ input: process.stdin,
166
+ output: process.stdout
167
+ });
168
+
169
+ return new Promise((resolve) => {
170
+ const defaultStr = defaultValue !== undefined ? ` (${defaultValue})` : '';
171
+ rl.question(` ${question}${defaultStr}: `, (answer) => {
172
+ rl.close();
173
+ const value = answer.trim() || defaultValue;
174
+ // Parse numbers
175
+ if (typeof defaultValue === 'number' && !isNaN(value)) {
176
+ resolve(parseInt(value, 10));
177
+ } else {
178
+ resolve(value);
179
+ }
180
+ });
181
+ });
182
+ }
183
+
184
+ /**
185
+ * Helper: Confirm yes/no
186
+ */
187
+ async function confirm(question, defaultValue = false) {
188
+ const rl = readline.createInterface({
189
+ input: process.stdin,
190
+ output: process.stdout
191
+ });
192
+
193
+ return new Promise((resolve) => {
194
+ const hint = defaultValue ? '[Y/n]' : '[y/N]';
195
+ rl.question(` ${question} ${hint}: `, (answer) => {
196
+ rl.close();
197
+ const normalized = answer.trim().toLowerCase();
198
+ if (normalized === '') {
199
+ resolve(defaultValue);
200
+ } else {
201
+ resolve(normalized === 'y' || normalized === 'yes');
202
+ }
203
+ });
204
+ });
205
+ }
206
+
207
+ // Parse and run
208
+ program.parse();