livedesk 0.1.418 → 0.1.419

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.
@@ -4,6 +4,7 @@ import { setImmediate as yieldToEventLoop } from 'node:timers/promises';
4
4
 
5
5
  const CHUNK_BYTES = 512 * 1024;
6
6
  const JOB_RETENTION_MS = 10 * 60 * 1000;
7
+ const COMMAND_RESULT_TIMEOUT_MS = 30 * 1000;
7
8
 
8
9
  function snapshot(job) {
9
10
  return {
@@ -28,13 +29,15 @@ function markUpdated(job) {
28
29
  }
29
30
 
30
31
  export class HubTransferJobs {
31
- constructor({ filesystem, remoteHub, maxConcurrent = 2 } = {}) {
32
+ constructor({ filesystem, remoteHub, maxConcurrent = 2, commandResultTimeoutMs = COMMAND_RESULT_TIMEOUT_MS } = {}) {
32
33
  this.filesystem = filesystem;
33
34
  this.remoteHub = remoteHub;
34
35
  this.maxConcurrent = Math.max(1, Number(maxConcurrent) || 2);
36
+ this.commandResultTimeoutMs = Math.max(1_000, Number(commandResultTimeoutMs) || COMMAND_RESULT_TIMEOUT_MS);
35
37
  this.jobs = new Map();
36
38
  this.queue = [];
37
39
  this.running = 0;
40
+ this.pendingCommands = new Map();
38
41
  }
39
42
 
40
43
  create({ itemIds = [], deviceIds = [], remoteDirectory = '', files = null, onComplete = null } = {}) {
@@ -90,6 +93,7 @@ export class HubTransferJobs {
90
93
  job.state = 'cancelled';
91
94
  job.error = 'transfer-cancelled';
92
95
  if (job.currentStream) job.currentStream.destroy();
96
+ this.finishPendingCommands(job.jobId, 'transfer-cancelled');
93
97
  markUpdated(job);
94
98
  return snapshot(job);
95
99
  }
@@ -181,10 +185,13 @@ export class HubTransferJobs {
181
185
  }
182
186
 
183
187
  async sendChunk(job, file, offset, buffer, final, targets) {
184
- for (const deviceId of targets) {
185
- if (job.cancelled) return;
188
+ const outcomes = await Promise.all(targets.map(async deviceId => {
189
+ if (job.cancelled) return { deviceId, ok: false, error: 'transfer-cancelled' };
190
+ const commandId = crypto.randomUUID();
191
+ const pending = this.waitForCommandResult(job.jobId, deviceId, commandId);
186
192
  const result = this.remoteHub.sendCommand(deviceId, {
187
193
  command: 'file.transfer.chunk',
194
+ commandId,
188
195
  payload: {
189
196
  transferId: job.jobId,
190
197
  remoteDirectory: job.remoteDirectory,
@@ -199,9 +206,87 @@ export class HubTransferJobs {
199
206
  }
200
207
  });
201
208
  if (!result?.ok) {
202
- job.failedTargets.add(deviceId);
203
- job.targetErrors.set(deviceId, String(result?.error || 'device-not-connected'));
209
+ this.settlePendingCommand(commandId, {
210
+ ok: false,
211
+ error: String(result?.error || 'device-not-connected')
212
+ });
204
213
  }
214
+ return { deviceId, ...await pending };
215
+ }));
216
+
217
+ for (const outcome of outcomes) {
218
+ if (outcome.ok || outcome.error === 'transfer-cancelled') continue;
219
+ job.failedTargets.add(outcome.deviceId);
220
+ job.targetErrors.set(outcome.deviceId, outcome.error || 'transfer-failed');
221
+ }
222
+ if (!job.cancelled && outcomes.length > 0 && outcomes.every(outcome => !outcome.ok)) {
223
+ throw new Error('all-targets-failed');
224
+ }
225
+ }
226
+
227
+ waitForCommandResult(jobId, deviceId, commandId) {
228
+ return new Promise(resolve => {
229
+ const timer = setTimeout(() => {
230
+ this.settlePendingCommand(commandId, {
231
+ ok: false,
232
+ error: 'file-transfer-ack-timeout'
233
+ });
234
+ }, this.commandResultTimeoutMs);
235
+ timer.unref?.();
236
+ this.pendingCommands.set(commandId, {
237
+ jobId,
238
+ deviceId,
239
+ timer,
240
+ resolve
241
+ });
242
+ });
243
+ }
244
+
245
+ settlePendingCommand(commandId, outcome) {
246
+ const pending = this.pendingCommands.get(String(commandId || ''));
247
+ if (!pending) return false;
248
+ this.pendingCommands.delete(commandId);
249
+ clearTimeout(pending.timer);
250
+ pending.resolve(outcome);
251
+ return true;
252
+ }
253
+
254
+ finishPendingCommands(jobId, error) {
255
+ for (const [commandId, pending] of this.pendingCommands) {
256
+ if (pending.jobId !== jobId) continue;
257
+ this.settlePendingCommand(commandId, { ok: false, error });
258
+ }
259
+ }
260
+
261
+ handleRemoteEvent(type, event) {
262
+ if (type === 'RemoteCommandResult') {
263
+ const commandId = String(event?.commandId || '');
264
+ const pending = this.pendingCommands.get(commandId);
265
+ if (!pending) return;
266
+ const eventDeviceId = String(event?.deviceId || event?.device?.deviceId || '');
267
+ if (eventDeviceId && eventDeviceId !== pending.deviceId) return;
268
+ const result = event?.result;
269
+ const error = String(
270
+ event?.error
271
+ || (result?.ok === false || result?.status === 'failed' || result?.status === 'rejected'
272
+ ? result?.error || 'file-transfer-rejected'
273
+ : '')
274
+ );
275
+ this.settlePendingCommand(commandId, error
276
+ ? { ok: false, error }
277
+ : { ok: true, result });
278
+ return;
279
+ }
280
+
281
+ if (type !== 'RemoteDeviceDisconnected') return;
282
+ const deviceId = String(event?.deviceId || event?.device?.deviceId || '');
283
+ if (!deviceId) return;
284
+ for (const [commandId, pending] of this.pendingCommands) {
285
+ if (pending.deviceId !== deviceId) continue;
286
+ this.settlePendingCommand(commandId, {
287
+ ok: false,
288
+ error: 'device-disconnected'
289
+ });
205
290
  }
206
291
  }
207
292
 
package/hub/src/server.js CHANGED
@@ -128,9 +128,10 @@ let verifiedLicense = {
128
128
  verifiedAt: 0
129
129
  };
130
130
  let frameClientSeq = 0;
131
- let inputClientSeq = 0;
132
- let audioClientSeq = 0;
131
+ let inputClientSeq = 0;
132
+ let audioClientSeq = 0;
133
133
  let liveDeskUpdateManager = null;
134
+ let hubTransferJobs = null;
134
135
  const HUB_HOST_TARGET_LEASE_MS = Math.max(5000, readPositiveIntegerEnv('LIVEDESK_HOST_TARGET_LEASE_MS', 60_000));
135
136
  const HUB_HOST_TARGET_RENEW_MS = Math.max(1000, Math.min(
136
137
  readPositiveIntegerEnv('LIVEDESK_HOST_TARGET_RENEW_MS', 20_000),
@@ -168,6 +169,7 @@ function readPositiveIntegerEnv(name, fallback) {
168
169
 
169
170
  function handleRemoteHubEvent(type, event) {
170
171
  liveDeskUpdateManager?.handleRemoteEvent(type, event);
172
+ hubTransferJobs?.handleRemoteEvent(type, event);
171
173
  if (type === 'RemoteInputApplied') {
172
174
  for (const ws of inputClients) {
173
175
  sendJson(ws, {
@@ -1208,11 +1210,12 @@ async function synchronizeAgentEnablement() {
1208
1210
  }
1209
1211
 
1210
1212
  const hubFilesystem = createHubFilesystem();
1211
- const hubTransferJobs = createHubTransferJobs({
1212
- filesystem: hubFilesystem,
1213
- remoteHub,
1214
- maxConcurrent: Number(process.env.LIVEDESK_MAX_FILE_JOBS || 2)
1215
- });
1213
+ hubTransferJobs = createHubTransferJobs({
1214
+ filesystem: hubFilesystem,
1215
+ remoteHub,
1216
+ maxConcurrent: Number(process.env.LIVEDESK_MAX_FILE_JOBS || 2),
1217
+ commandResultTimeoutMs: readPositiveIntegerEnv('LIVEDESK_FILE_CHUNK_ACK_TIMEOUT_MS', 30_000)
1218
+ });
1216
1219
  const hubSharedFolders = createHubSharedFolders({
1217
1220
  filesystem: hubFilesystem,
1218
1221
  transferJobs: hubTransferJobs,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "livedesk",
3
- "version": "0.1.418",
3
+ "version": "0.1.419",
4
4
  "livedeskClientVersion": "0.1.182",
5
5
  "buildFlavor": "production",
6
6
  "description": "LiveDesk Hub and client launcher",