javascript-solid-server 0.0.8 → 0.0.10

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.
@@ -13,7 +13,9 @@
13
13
  "Bash(pkill:*)",
14
14
  "Bash(curl:*)",
15
15
  "Bash(npm test:*)",
16
- "Bash(git add:*)"
16
+ "Bash(git add:*)",
17
+ "WebFetch(domain:solid.github.io)",
18
+ "Bash(node:*)"
17
19
  ]
18
20
  }
19
21
  }
package/README.md CHANGED
@@ -35,12 +35,32 @@ const server = createServer();
35
35
  const serverWithConneg = createServer({ conneg: true });
36
36
  ```
37
37
 
38
+ ## Performance
39
+
40
+ This server is designed for speed. Benchmark results on a typical development machine:
41
+
42
+ | Operation | Requests/sec | Avg Latency | p99 Latency |
43
+ |-----------|-------------|-------------|-------------|
44
+ | GET resource | 5,400+ | 1.2ms | 3ms |
45
+ | GET container | 4,700+ | 1.6ms | 3ms |
46
+ | PUT (write) | 5,700+ | 1.1ms | 2ms |
47
+ | POST (create) | 5,200+ | 1.3ms | 3ms |
48
+ | OPTIONS | 10,000+ | 0.4ms | 1ms |
49
+
50
+ Run benchmarks yourself:
51
+ ```bash
52
+ npm run benchmark
53
+ ```
54
+
38
55
  ## Features
39
56
 
40
- ### Implemented (v0.0.8)
57
+ ### Implemented (v0.0.10)
41
58
 
42
59
  - **LDP CRUD Operations** - GET, PUT, POST, DELETE, HEAD
43
60
  - **N3 Patch** - Solid's native patch format for RDF updates
61
+ - **SPARQL Update** - Standard SPARQL UPDATE protocol for PATCH
62
+ - **Conditional Requests** - If-Match/If-None-Match headers (304, 412)
63
+ - **WebSocket Notifications** - Real-time updates via solid-0.1 protocol (SolidOS compatible)
44
64
  - **Container Management** - Create, list, and manage containers
45
65
  - **Multi-user Pods** - Create pods at `/<username>/`
46
66
  - **WebID Profiles** - JSON-LD structured data in HTML at pod root
@@ -129,6 +149,44 @@ curl -X PATCH http://localhost:3000/alice/public/data.json \
129
149
  solid:inserts { <#data> <http://example.org/name> "Updated" }.'
130
150
  ```
131
151
 
152
+ ### PATCH with SPARQL Update
153
+
154
+ ```bash
155
+ curl -X PATCH http://localhost:3000/alice/public/data.json \
156
+ -H "Authorization: Bearer YOUR_TOKEN" \
157
+ -H "Content-Type: application/sparql-update" \
158
+ -d 'PREFIX ex: <http://example.org/>
159
+ DELETE DATA { <#data> ex:value 42 } ;
160
+ INSERT DATA { <#data> ex:value 43 }'
161
+ ```
162
+
163
+ ### Conditional Requests
164
+
165
+ Use `If-Match` for safe updates (optimistic concurrency):
166
+
167
+ ```bash
168
+ # Get current ETag
169
+ ETAG=$(curl -sI http://localhost:3000/alice/public/data.json | grep -i etag | awk '{print $2}')
170
+
171
+ # Update only if ETag matches
172
+ curl -X PUT http://localhost:3000/alice/public/data.json \
173
+ -H "Authorization: Bearer YOUR_TOKEN" \
174
+ -H "Content-Type: application/ld+json" \
175
+ -H "If-Match: $ETAG" \
176
+ -d '{"@id": "#data", "http://example.org/value": 100}'
177
+ ```
178
+
179
+ Use `If-None-Match: *` for create-only semantics:
180
+
181
+ ```bash
182
+ # Create only if resource doesn't exist (returns 412 if it does)
183
+ curl -X PUT http://localhost:3000/alice/public/new-resource.json \
184
+ -H "Authorization: Bearer YOUR_TOKEN" \
185
+ -H "Content-Type: application/ld+json" \
186
+ -H "If-None-Match: *" \
187
+ -d '{"@id": "#new"}'
188
+ ```
189
+
132
190
  ## Pod Structure
133
191
 
134
192
  ```
@@ -171,18 +229,42 @@ curl -H "Authorization: DPoP ACCESS_TOKEN" \
171
229
 
172
230
  ```javascript
173
231
  createServer({
174
- logger: true, // Enable Fastify logging (default: true)
175
- conneg: false // Enable content negotiation (default: false)
232
+ logger: true, // Enable Fastify logging (default: true)
233
+ conneg: false, // Enable content negotiation (default: false)
234
+ notifications: false // Enable WebSocket notifications (default: false)
176
235
  });
177
236
  ```
178
237
 
238
+ ### WebSocket Notifications
239
+
240
+ Enable real-time notifications for resource changes:
241
+
242
+ ```javascript
243
+ const server = createServer({ notifications: true });
244
+ ```
245
+
246
+ Clients discover the WebSocket URL via the `Updates-Via` header:
247
+
248
+ ```bash
249
+ curl -I http://localhost:3000/alice/public/
250
+ # Updates-Via: ws://localhost:3000/.notifications
251
+ ```
252
+
253
+ Protocol (solid-0.1, compatible with SolidOS):
254
+ ```
255
+ Server: protocol solid-0.1
256
+ Client: sub http://localhost:3000/alice/public/data.json
257
+ Server: ack http://localhost:3000/alice/public/data.json
258
+ Server: pub http://localhost:3000/alice/public/data.json (on change)
259
+ ```
260
+
179
261
  ## Running Tests
180
262
 
181
263
  ```bash
182
264
  npm test
183
265
  ```
184
266
 
185
- Currently passing: **105 tests**
267
+ Currently passing: **136 tests**
186
268
 
187
269
  ## Project Structure
188
270
 
@@ -208,12 +290,18 @@ src/
208
290
  ā”œā”€ā”€ webid/
209
291
  │ └── profile.js # WebID generation
210
292
  ā”œā”€ā”€ patch/
211
- │ └── n3-patch.js # N3 Patch support
293
+ │ ā”œā”€ā”€ n3-patch.js # N3 Patch support
294
+ │ └── sparql-update.js # SPARQL Update support
295
+ ā”œā”€ā”€ notifications/
296
+ │ ā”œā”€ā”€ index.js # WebSocket plugin
297
+ │ ā”œā”€ā”€ events.js # Event emitter
298
+ │ └── websocket.js # solid-0.1 protocol
212
299
  ā”œā”€ā”€ rdf/
213
300
  │ ā”œā”€ā”€ turtle.js # Turtle <-> JSON-LD
214
301
  │ └── conneg.js # Content negotiation
215
302
  └── utils/
216
- └── url.js # URL utilities
303
+ ā”œā”€ā”€ url.js # URL utilities
304
+ └── conditional.js # If-Match/If-None-Match
217
305
  ```
218
306
 
219
307
  ## Dependencies
@@ -221,6 +309,7 @@ src/
221
309
  Minimal dependencies for a fast, secure server:
222
310
 
223
311
  - **fastify** - High-performance HTTP server
312
+ - **@fastify/websocket** - WebSocket support for notifications
224
313
  - **fs-extra** - Enhanced file operations
225
314
  - **jose** - JWT/JWK handling for Solid-OIDC
226
315
  - **n3** - Turtle parsing (only used when conneg enabled)
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/package.json CHANGED
@@ -1,15 +1,25 @@
1
1
  {
2
2
  "name": "javascript-solid-server",
3
- "version": "0.0.8",
3
+ "version": "0.0.10",
4
4
  "description": "A minimal, fast Solid server",
5
5
  "main": "src/index.js",
6
6
  "type": "module",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/JavaScriptSolidServer/JavaScriptSolidServer.git"
10
+ },
11
+ "bugs": {
12
+ "url": "https://github.com/JavaScriptSolidServer/JavaScriptSolidServer/issues"
13
+ },
14
+ "homepage": "https://github.com/JavaScriptSolidServer/JavaScriptSolidServer#readme",
7
15
  "scripts": {
8
16
  "start": "node src/index.js",
9
17
  "dev": "node --watch src/index.js",
10
- "test": "node --test --test-concurrency=1"
18
+ "test": "node --test --test-concurrency=1",
19
+ "benchmark": "node benchmark.js"
11
20
  },
12
21
  "dependencies": {
22
+ "@fastify/websocket": "^8.3.1",
13
23
  "fastify": "^4.25.2",
14
24
  "fs-extra": "^11.2.0",
15
25
  "jose": "^6.1.3",
@@ -24,5 +34,8 @@
24
34
  "linked-data",
25
35
  "decentralized"
26
36
  ],
27
- "license": "MIT"
37
+ "license": "MIT",
38
+ "devDependencies": {
39
+ "autocannon": "^8.0.0"
40
+ }
28
41
  }
@@ -5,6 +5,7 @@ import { generateProfile, generatePreferences, generateTypeIndex, serialize } fr
5
5
  import { generateOwnerAcl, generatePrivateAcl, generateInboxAcl, serializeAcl } from '../wac/parser.js';
6
6
  import { createToken } from '../auth/token.js';
7
7
  import { canAcceptInput, toJsonLd, getVaryHeader, RDF_TYPES } from '../rdf/conneg.js';
8
+ import { emitChange } from '../notifications/events.js';
8
9
 
9
10
  /**
10
11
  * Handle POST request to container (create new resource)
@@ -97,6 +98,12 @@ export async function handlePost(request, reply) {
97
98
  headers['Vary'] = getVaryHeader(connegEnabled);
98
99
 
99
100
  Object.entries(headers).forEach(([k, v]) => reply.header(k, v));
101
+
102
+ // Emit change notification for WebSocket subscribers
103
+ if (request.notificationsEnabled) {
104
+ emitChange(resourceUrl);
105
+ }
106
+
100
107
  return reply.code(201).send();
101
108
  }
102
109