nodio-cli 1.0.7 → 1.0.9

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nodio-cli",
3
- "version": "1.0.7",
3
+ "version": "1.0.9",
4
4
  "description": "Nodio distributed storage network",
5
5
  "main": "src/server/index.js",
6
6
  "type": "commonjs",
@@ -5,7 +5,7 @@ function getServerConfig() {
5
5
  return {
6
6
  port: Number(process.env.PORT || process.env.NODIO_SERVER_PORT || 4000),
7
7
  mongoUri: process.env.NODIO_MONGO_URI || 'mongodb://127.0.0.1:27017/nodio',
8
- heartbeatIntervalMs: Number(process.env.NODIO_HEARTBEAT_INTERVAL_MS || 30000),
8
+ heartbeatIntervalMs: Number(process.env.NODIO_HEARTBEAT_INTERVAL_MS || 10000),
9
9
  offlineAfterMisses: Number(process.env.NODIO_OFFLINE_AFTER_MISSES || 3),
10
10
  minReplicas: Number(process.env.NODIO_MIN_REPLICAS || 5),
11
11
  emergencyReplicaFloor: Number(process.env.NODIO_EMERGENCY_REPLICA_FLOOR || 2)
@@ -9,7 +9,9 @@ const nodeSchema = new mongoose.Schema(
9
9
  capacityBytes: { type: Number, required: true, min: 1 },
10
10
  freeBytes: { type: Number, required: true, min: 0 },
11
11
  status: { type: String, enum: ['online', 'offline'], default: 'online', index: true },
12
- lastHeartbeatAt: { type: Date, default: Date.now, index: true }
12
+ lastHeartbeatAt: { type: Date, default: Date.now, index: true },
13
+ pendingRelayAlert: { type: Boolean, default: false },
14
+ pendingRelayAlertAt: { type: Date, default: null }
13
15
  },
14
16
  { timestamps: true }
15
17
  );
@@ -271,6 +271,23 @@ function buildRoutes(config) {
271
271
  }
272
272
  });
273
273
 
274
+ router.post('/nodes/:nodeId/alert-relay-pending', async (req, res, next) => {
275
+ try {
276
+ const { nodeId } = req.params;
277
+ const node = await NodeModel.findOne({ nodeId });
278
+ if (!node) {
279
+ return res.status(404).json({ error: 'node not found' });
280
+ }
281
+ // Set pending relay flag so donor knows to check urgently
282
+ node.pendingRelayAlert = true;
283
+ node.pendingRelayAlertAt = new Date();
284
+ await node.save();
285
+ res.json({ ok: true, message: 'relay pending alert sent' });
286
+ } catch (error) {
287
+ next(error);
288
+ }
289
+ });
290
+
274
291
  router.post('/replication-tasks/:taskId/complete', async (req, res, next) => {
275
292
  try {
276
293
  const { taskId } = req.params;
@@ -31,7 +31,7 @@ function sleep(ms) {
31
31
  return new Promise((resolve) => setTimeout(resolve, ms));
32
32
  }
33
33
 
34
- async function relayStoreShard(api, { shardId, fileId, nodeIds, dataBuffer, timeoutMs = 120000, pollMs = 1500 }) {
34
+ async function relayStoreShard(api, { shardId, fileId, nodeIds, dataBuffer, timeoutMs = 90000, pollMs = 500 }) {
35
35
  const opId = uuidv4();
36
36
  await api.post('/relay/shards/store', {
37
37
  opId,
@@ -41,6 +41,16 @@ async function relayStoreShard(api, { shardId, fileId, nodeIds, dataBuffer, time
41
41
  dataBase64: dataBuffer.toString('base64')
42
42
  });
43
43
 
44
+ // Alert server that relay tasks are pending so donors check urgently
45
+ try {
46
+ for (const nodeId of nodeIds) {
47
+ // eslint-disable-next-line no-await-in-loop
48
+ await api.post(`/nodes/${nodeId}/alert-relay-pending`);
49
+ }
50
+ } catch (alertError) {
51
+ console.warn('relay alert failed:', alertError.message);
52
+ }
53
+
44
54
  const deadline = Date.now() + timeoutMs;
45
55
  while (Date.now() < deadline) {
46
56
  const response = await api.get(`/relay/shards/store/${opId}`);
@@ -51,7 +61,7 @@ async function relayStoreShard(api, { shardId, fileId, nodeIds, dataBuffer, time
51
61
  failed: payload.failed || []
52
62
  };
53
63
  }
54
- // Wait for donor heartbeats to pull relay tasks.
64
+ // Polls every 500ms; with 10s heartbeat, donor should respond within 10s+network latency
55
65
  // eslint-disable-next-line no-await-in-loop
56
66
  await sleep(pollMs);
57
67
  }
@@ -59,7 +69,7 @@ async function relayStoreShard(api, { shardId, fileId, nodeIds, dataBuffer, time
59
69
  throw new Error(`relay store timed out for shard ${shardId}`);
60
70
  }
61
71
 
62
- async function relayFetchShard(api, { shardId, nodeIds, timeoutMs = 120000, pollMs = 1500 }) {
72
+ async function relayFetchShard(api, { shardId, nodeIds, timeoutMs = 90000, pollMs = 500 }) {
63
73
  const opId = uuidv4();
64
74
  await api.post('/relay/shards/fetch', {
65
75
  opId,
@@ -67,6 +77,16 @@ async function relayFetchShard(api, { shardId, nodeIds, timeoutMs = 120000, poll
67
77
  nodeIds
68
78
  });
69
79
 
80
+ // Alert server that relay tasks are pending so donors check urgently
81
+ try {
82
+ for (const nodeId of nodeIds) {
83
+ // eslint-disable-next-line no-await-in-loop
84
+ await api.post(`/nodes/${nodeId}/alert-relay-pending`);
85
+ }
86
+ } catch (alertError) {
87
+ console.warn('relay alert failed:', alertError.message);
88
+ }
89
+
70
90
  const deadline = Date.now() + timeoutMs;
71
91
  while (Date.now() < deadline) {
72
92
  const response = await api.get(`/relay/shards/fetch/${opId}`);
@@ -100,6 +120,7 @@ async function uploadFile(options) {
100
120
  const serverUrl = options.server;
101
121
  const shardSizeMb = Number(options.shardSizeMb || 1);
102
122
  const replicas = Number(options.replicas || 5);
123
+ const directTimeoutMs = Number(options.directTimeoutMs || 4000);
103
124
 
104
125
  if (!Number.isFinite(shardSizeMb) || shardSizeMb <= 0) {
105
126
  throw new Error('shard-size-mb must be greater than 0');
@@ -107,6 +128,9 @@ async function uploadFile(options) {
107
128
  if (!Number.isInteger(replicas) || replicas < 5) {
108
129
  throw new Error('replicas must be an integer >= 5');
109
130
  }
131
+ if (!Number.isFinite(directTimeoutMs) || directTimeoutMs <= 0) {
132
+ throw new Error('direct-timeout-ms must be greater than 0');
133
+ }
110
134
 
111
135
  const shardSizeBytes = Math.floor(shardSizeMb * 1024 * 1024);
112
136
  const plainBuffer = await fs.readFile(filePath);
@@ -153,21 +177,29 @@ async function uploadFile(options) {
153
177
  throw new Error(`placement failed for ${shardId}: expected ${replicas} replicas`);
154
178
  }
155
179
 
156
- const successfulReplicas = [];
157
- const failedReplicas = [];
158
-
159
- for (const replica of plannedReplicas) {
160
- try {
180
+ const directWriteResults = await Promise.allSettled(
181
+ plannedReplicas.map(async (replica) => {
161
182
  const putUrl = `${normalizeUrl(replica.url)}/shards/${shardId}`;
162
183
  await axios.put(putUrl, encrypted.cipherText, {
163
184
  headers: { 'Content-Type': 'application/octet-stream' },
164
- timeout: 30000
185
+ timeout: directTimeoutMs
165
186
  });
166
- successfulReplicas.push(replica);
167
- } catch (error) {
168
- failedReplicas.push({ nodeId: replica.nodeId, url: replica.url, error: error.message });
169
- }
170
- }
187
+ return replica;
188
+ })
189
+ );
190
+
191
+ const successfulReplicas = directWriteResults
192
+ .filter((result) => result.status === 'fulfilled')
193
+ .map((result) => result.value);
194
+
195
+ const failedReplicas = directWriteResults
196
+ .map((result, index) => ({ result, replica: plannedReplicas[index] }))
197
+ .filter((entry) => entry.result.status === 'rejected')
198
+ .map((entry) => ({
199
+ nodeId: entry.replica.nodeId,
200
+ url: entry.replica.url,
201
+ error: entry.result.reason?.message || 'direct write failed'
202
+ }));
171
203
 
172
204
  if (failedReplicas.length > 0) {
173
205
  try {
@@ -240,6 +272,11 @@ async function downloadFile(options) {
240
272
  const serverUrl = options.server;
241
273
  const output = options.output ? path.resolve(options.output) : path.resolve(`./${fileId}.downloaded`);
242
274
  const keyBuffer = parseAesKey(options.keyBase64);
275
+ const directTimeoutMs = Number(options.directTimeoutMs || 4000);
276
+
277
+ if (!Number.isFinite(directTimeoutMs) || directTimeoutMs <= 0) {
278
+ throw new Error('direct-timeout-ms must be greater than 0');
279
+ }
243
280
 
244
281
  const api = createApiClient(serverUrl);
245
282
  const manifestResponse = await api.get(`/files/${fileId}/manifest`);
@@ -263,22 +300,24 @@ async function downloadFile(options) {
263
300
  }
264
301
 
265
302
  let encryptedBuffer = null;
266
- for (const replica of shard.replicas || []) {
267
- try {
303
+ const directFetchResults = await Promise.allSettled(
304
+ (shard.replicas || []).map(async (replica) => {
268
305
  const getUrl = `${normalizeUrl(replica.url)}/shards/${shard.shardId}`;
269
306
  const response = await axios.get(getUrl, {
270
307
  responseType: 'arraybuffer',
271
- timeout: 30000
308
+ timeout: directTimeoutMs
272
309
  });
273
310
  const candidate = Buffer.from(response.data);
274
311
  if (sha256Hex(candidate) !== shard.checksum) {
275
- continue;
312
+ throw new Error('checksum mismatch');
276
313
  }
277
- encryptedBuffer = candidate;
278
- break;
279
- } catch {
280
- continue;
281
- }
314
+ return candidate;
315
+ })
316
+ );
317
+
318
+ const successfulDirectFetch = directFetchResults.find((result) => result.status === 'fulfilled');
319
+ if (successfulDirectFetch) {
320
+ encryptedBuffer = successfulDirectFetch.value;
282
321
  }
283
322
 
284
323
  if (!encryptedBuffer && Array.isArray(shard.replicas) && shard.replicas.length > 0) {
package/src/user/index.js CHANGED
@@ -14,6 +14,7 @@ program
14
14
  .option('--file-id <id>', 'custom file ID (optional)')
15
15
  .option('--shard-size-mb <mb>', 'plaintext shard size in MB', '1')
16
16
  .option('--replicas <count>', 'replicas per shard (minimum 5)', '5')
17
+ .option('--direct-timeout-ms <ms>', 'timeout for each direct donor attempt before relay fallback', '4000')
17
18
  .option('--key-base64 <key>', '32-byte AES key in base64 (optional)')
18
19
  .action(async (options) => {
19
20
  await uploadFile(options);
@@ -26,6 +27,7 @@ program
26
27
  .requiredOption('--key-base64 <key>', '32-byte AES key in base64 from upload output')
27
28
  .option('--server <url>', 'central server URL', 'https://api.nodio.me')
28
29
  .option('--output <path>', 'output file path')
30
+ .option('--direct-timeout-ms <ms>', 'timeout for each direct donor attempt before relay fallback', '4000')
29
31
  .action(async (options) => {
30
32
  await downloadFile(options);
31
33
  });